diff --git a/.JuliaFormatter.toml b/.JuliaFormatter.toml new file mode 100644 index 000000000..3af314291 --- /dev/null +++ b/.JuliaFormatter.toml @@ -0,0 +1,9 @@ +style = "sciml" +ignore = [ + "@INPROGRESS", + "bibliography", + "designs", + "papers", + "src/parametricbuilder/gridspace", + "temp", +] diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 000000000..a1178d17e --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,23 @@ +coverage: + precision: 2 + round: down + range: "95...100" + status: + project: + default: + target: 95% + threshold: 0% + paths: + - "src/" + - "ext/" + patch: + default: + target: 95% + threshold: 0% + paths: + - "src/" + - "ext/" + +comment: + layout: "reach,diff,flags,files" + require_changes: true diff --git a/.github/ISSUE_TEMPLATE/release_approval.yml b/.github/ISSUE_TEMPLATE/release_approval.yml deleted file mode 100644 index cdd787268..000000000 --- a/.github/ISSUE_TEMPLATE/release_approval.yml +++ /dev/null @@ -1,146 +0,0 @@ -name: "Release approval request" -description: "Validation checklist before public release" -title: "Release approval request" -labels: ["release-approval"] -assignees: - - amaurigmartins - -body: - - type: input - id: repository_name - attributes: - label: "Repository name" - description: "Select the repository being released" - validations: - required: true - - - type: input - id: version_tag - attributes: - label: "Version tag" - description: "Version being released (must follow semantic versioning pattern vX.Y.Z)" - placeholder: "v1.2.3" - validations: - required: true - - - type: markdown - attributes: - value: | - ## 1. Intellectual Property (IP) and Legal Clearance - Complete all items before requesting approval. - - - type: checkboxes - id: ip_legal - attributes: - label: "IP and Legal Requirements" - options: - - label: "Written consent secured from faculty advisor or PI" - required: true - - label: "Software disclosure submitted to tech transfer/IP office" - required: true - - label: "Funding/sponsorship contracts reviewed for restrictions or obligations" - required: true - - label: "Written consent secured from all co-developers and collaborators" - required: true - - label: "Verified no active or pending patents are compromised by open release" - required: true - - label: "Verified absence of sensitive data (personal, confidential, regulated tech)" - required: true - - label: "Third-party licenses audited and documented to confirm compatibility with the repository" - required: true - - label: "All findings and products of the codebase being open-sourced have been published/accepted for publication with scientific journals" - required: true - - - type: markdown - attributes: - value: | - ## 2. Code Quality and Basic QA - - - type: checkboxes - id: code_quality - attributes: - label: "Code Quality Requirements" - options: - - label: "Codebase cleaned: no credentials, passwords, API keys, sensitive URLs, or debug code" - required: true - - label: "Out-of-the-box functionality confirmed in a clean environment" - required: true - - label: "Reproducibility ensured: scripts or datasets included to reproduce main results" - required: true - - label: "Dependencies clearly documented (requirements.txt, compiler info, etc.)" - required: true - - label: "Basic tests or validation scripts included demonstrating key functionality" - required: true - - label: "Peer or self-review performed for code readability and usability" - required: true - - - type: markdown - attributes: - value: | - ## 3. Documentation and Licensing - - - type: checkboxes - id: documentation - attributes: - label: "Documentation Requirements" - options: - - label: "Comprehensive README.md created with clear purpose, installation and usage instructions" - required: true - - label: "CHANGELOG.md created following proper standards (Keep a Changelog)" - required: true - - label: "Citation instructions provided (CITATION.cff, DOI links, publication references)" - required: true - - label: "Appropriate LICENSE.md file attached at repository root" - required: true - - label: "Funding agencies, institutional support, and contributors properly acknowledged" - required: true - - label: "Contributing guidelines included for external collaborators (if applicable)" - required: false - - - type: markdown - attributes: - value: | - ## 4. Repository Finalization and Public Release - - - type: checkboxes - id: finalization - attributes: - label: "Repository Finalization Requirements" - options: - - label: "Repository structure clearly organized (src/, examples/, docs/, etc.)" - required: true - - label: "Final pre-release sanity check performed (installation from scratch, test examples)" - required: true - - label: "Release clearly tagged and versioned (e.g., v1.0.0) with release notes" - required: true - - label: "Repository ready to be set to public visibility" - required: true - - label: "Release announcement prepared for internal and external channels" - required: true - - label: "Repository archived with DOI (optional, via Zenodo)" - required: false - - - type: textarea - id: additional_notes - attributes: - label: "Additional Notes" - description: "Provide any additional context or special considerations" - placeholder: "Add any relevant information or clarifications here..." - validations: - required: false - - - type: input - id: institutional_email - attributes: - label: "Institutional email" - description: "Provide your university/institutional email" - placeholder: "researcher.name@kuleuven.be" - validations: - required: true - - - type: markdown - attributes: - value: | - ## Declaration - - By submitting this form, I confirm that all required checks have been completed and this software is ready for public release under the institution standards. diff --git a/.github/OSS_PUBLISH_CHECKLIST.md b/.github/OSS_PUBLISH_CHECKLIST.md new file mode 100644 index 000000000..32b0f1da3 --- /dev/null +++ b/.github/OSS_PUBLISH_CHECKLIST.md @@ -0,0 +1,49 @@ +### Repository name + +{{ repository_name }} + +### Version tag + +{{ version_tag }} + +### Intellectual property and legal checks + +- [ ] Obtain written approval from the faculty adviser or principal investigator. +- [ ] Submit the required software disclosure to the technology-transfer office. +- [ ] Review funding and sponsorship agreements for release restrictions. +- [ ] Obtain written consent from contributors whose work is included. +- [ ] Check active and pending patent applications before release. +- [ ] Remove personal, confidential, and export-controlled data. +- [ ] Record third-party licenses and verify redistribution terms. +- [ ] Confirm that releasing the software does not violate publication embargoes. + +### Source and test checks + +- [ ] Remove credentials, passwords, API keys, private URLs, and debug code. +- [ ] Install and test the package in a clean environment. +- [ ] Include the scripts and data required to reproduce published results. +- [ ] Record language, package, compiler, and external-program dependencies. +- [ ] Test the public calculations used in examples and publications. +- [ ] Review the source for readable names, error messages, and public APIs. + +### Documentation checks + +- [ ] State the package purpose, installation, and first calculation in `README.md`. +- [ ] Maintain `CHANGELOG.md` using Keep a Changelog. +- [ ] Provide citation metadata, DOI links, and publication references. +- [ ] Place the selected license at the repository root. +- [ ] Name funding bodies, institutional support, and contributors. +- [ ] Document the contribution procedure. + +### Publication checks + +- [ ] Verify that source, examples, documentation, and tests use their documented paths. +- [ ] Repeat installation and documented examples from a clean checkout. +- [ ] Create the version tag and release notes. +- [ ] Complete the hosting service's public-release checks. +- [ ] Prepare the release announcement. +- [ ] Archive the release with a DOI, or record why no DOI is required. + +### Institutional email + +{{ institutional_email }} diff --git a/.github/prose/cspell.json b/.github/prose/cspell.json new file mode 100644 index 000000000..da5ec5698 --- /dev/null +++ b/.github/prose/cspell.json @@ -0,0 +1,73 @@ +{ + "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", + "version": "0.2", + "language": "en-US", + "unknownWords": "report-common-typos", + "dictionaries": [ + "julia", + "project" + ], + "dictionaryDefinitions": [ + { + "name": "project", + "path": "words.txt", + "addWords": false + } + ], + "useGitignore": false, + "noConfigSearch": true, + "validateDirectives": true, + "ignoreRegExpList": [ + "/\\$\\$[\\s\\S]*?\\$\\$/g", + "/\\\\[A-Za-z]+/g", + "/\\[Cc\\]andidate/g", + "/Approche complète de la propagation multifilaire en haute fréquence\\s+par utilisation des matrices complexes/g", + "/\\bconductivity_micro_siemens_per_centimetre\\b/g", + "/\\bearth_spectral_neighbourhood!/g" + ], + "overrides": [ + { + "filename": "**/*", + "languageId": "" + }, + { + "filename": [ + "**/src/datamodel/placement/bounded.jl", + "**/test/unit/datamodel/baseparams/geometry.jl" + ], + "words": ["neighbour"] + }, + { + "filename": ["**/*.md", "**/*.markdown"], + "ignoreRegExpList": ["/(? "$source_files" +while IFS= read -r -d '' file; do + case "$file" in + LICENSE|*/Manifest.toml|Manifest.toml|.github/prose/package-lock.json) continue ;; + .github/prose/fixtures/*|.github/prose/styles/config/*|.github/prose/words.txt) continue ;; + test/fixtures/reference/*/capture-*/*) continue ;; + test/fixtures/reference/*) + [[ "$file" == *.md || "$file" == *.jl ]] || continue ;; + esac + case "$file" in + *.md|*.markdown|*.txt|*.jl|*.js|*.css|*.pro|*.sh|*.yml|*.yaml|*.json|*.toml|.gitignore) + test -f "$file" + printf '%s\n' "$file" >> "$source_files" ;; + esac +done < <(git ls-files -z) +test -s "$source_files" +test -s docs/build/index.html +test -d docs/src/tutorials +find docs/src -type f -name '*.md' -print > "$reports/generated-files.txt" +grep -q '^docs/src/tutorials/.*\.md$' "$reports/generated-files.txt" +cat "$source_files" "$reports/generated-files.txt" | sort -u > "$reports/cspell-files.txt" +# Rule patterns and deliberate misspellings are data, not spelling inputs. +sed -i -e '\|^\.github/prose/styles/|d' -e '\|^\.github/prose/cspell.json$|d' "$reports/cspell-files.txt" +find docs/build -type f -name '*.html' ! -path 'docs/build/bibliography/*' ! -path 'docs/build/bibliography.html' -print > "$reports/html-files.txt" +test -s "$reports/html-files.txt" +cat "$source_files" "$reports/generated-files.txt" "$reports/html-files.txt" | sort -u > "$reports/vale-files.txt" diff --git a/.github/prose/fixtures/check.sh b/.github/prose/fixtures/check.sh new file mode 100644 index 000000000..f843cb728 --- /dev/null +++ b/.github/prose/fixtures/check.sh @@ -0,0 +1,234 @@ +#!/usr/bin/env bash +# Verify CLI exit status, rendered exclusions, and source selection. +set -euo pipefail +root=$(git rev-parse --show-toplevel) +scratch=$(mktemp -d) +trap 'rm -rf "$scratch"' EXIT +vale_args=(--no-global --config="$root/.github/prose/vale.ini" --output=JSON) +cspell_args=(lint --root="$scratch" --config="$root/.github/prose/cspell.json" --no-progress --no-cache) + +# An advisory modal finding must remain visible without failing the command. +printf 'The result should match.\n' > "$scratch/advisory.md" +vale "${vale_args[@]}" "$scratch/advisory.md" > "$scratch/advisory.json" +grep -Fq 'STE.Modals' "$scratch/advisory.json" + +cat > "$scratch/scientific.md" <<'MARKDOWN' +Apply the boundary conditions in the canonical basis. + +Window closure stops the event loop. + +The Etch Competence Hub uses HTTP/TLS. + +The component's objective is identical to the optimum remainder. + +The result is equivalent. +MARKDOWN +vale "${vale_args[@]}" "$scratch/scientific.md" > "$scratch/scientific.json" + +printf 'provenance\n' > "$scratch/invalid.md" +if vale "${vale_args[@]}" "$scratch/invalid.md" > "$scratch/invalid.json"; then + echo 'Vale accepted a forbidden word.' >&2 + exit 1 +else + test "$?" -eq 1 +fi +grep -Fq 'Project.BannedTerms' "$scratch/invalid.json" + +cat > "$scratch/rendered.html" <<'HTML' + +
contract +closure +

Apply the boundary conditions in the canonical basis.

+

provenance

+HTML +if vale "${vale_args[@]}" "$scratch/rendered.html" > "$scratch/rendered.json"; then + echo 'Vale missed a rendered docstring.' >&2 + exit 1 +else + test "$?" -eq 1 +fi +test "$(grep -c '"Check": "Project.BannedTerms"' "$scratch/rendered.json")" -eq 1 +! grep -Fq 'Project.Closure' "$scratch/rendered.json" + +# Generated binding labels and typed field labels retain their punctuation. +cat > "$scratch/fields.html" <<'HTML' +
f — Function +
+ +

Read the file; contract

+HTML +if vale "${vale_args[@]}" "$scratch/fields.html" > "$scratch/fields.json"; then + exit 1 +fi +! grep -Fq 'Project.EmDashUsage' "$scratch/fields.json" +test "$(grep -c '"Check": "Project.ColonUsage"' "$scratch/fields.json")" -eq 1 +grep -Fq '"Match": ": Ordinary"' "$scratch/fields.json" +grep -Fq 'Project.Semicolons' "$scratch/fields.json" +grep -Fq 'Project.BannedTerms' "$scratch/fields.json" + +cat > "$scratch/signature.jl" <<'JULIA' +""" + f(x; contract=1) + +Read the file; check its contents. +""" +f(x; contract=1) = x +JULIA +if vale "${vale_args[@]}" "$scratch/signature.jl" > "$scratch/signature.json"; then + exit 1 +fi +test "$(grep -c '"Check": "Project.Semicolons"' "$scratch/signature.json")" -eq 1 +! grep -Fq 'Project.BannedTerms' "$scratch/signature.json" + +# Constant fields are valid Julia syntax that the bundled grammar cannot parse. +# Documentation must remain visible around such declarations. +cat > "$scratch/modern.jl" <<'JULIA' +""" +$(TYPEDEF) + +contract +""" +mutable struct Sample + const value::Int +end +""" +$(TYPEDSIGNATURES) + +provenance +""" +f(x) = x +JULIA +if vale "${vale_args[@]}" "$scratch/modern.jl" > "$scratch/modern.json"; then + exit 1 +fi +grep -Fq '"Line": 4' "$scratch/modern.json" +# Check the later docstring independently of the earlier error. +sed 's/^contract$/A physical field./' "$scratch/modern.jl" > "$scratch/modern-function.jl" +if vale "${vale_args[@]}" "$scratch/modern-function.jl" > "$scratch/modern-function.json"; then + exit 1 +fi +grep -Fq '"Match": "provenance"' "$scratch/modern-function.json" +grep -Fq '"Line": 12' "$scratch/modern-function.json" + +# Known misspellings fail; unfamiliar names and identifiers are accepted. +printf '"""Read and recieve the result."""\nf() = 1\n' > "$scratch/unpublished.jl" +if cspell "${cspell_args[@]}" "$scratch/unpublished.jl" > "$scratch/spelling.log" 2>&1; then + echo 'cspell missed an unpublished docstring.' >&2 + exit 1 +else + test "$?" -eq 1 +fi +grep -Fq 'Unknown word (recieve)' "$scratch/spelling.log" +grep -Fq 'receive' "$scratch/spelling.log" +printf '# Compute the admittance with GetDP.\n' > "$scratch/science.jl" +cspell "${cspell_args[@]}" "$scratch/science.jl" + +cat > "$scratch/names.md" <<'MARKDOWN' +Fatou and Quorvexium check source files. +The names jolars, fatou, kwarg, badconfig, numchildren, cconvert and relint are accepted. +An unknown word such as prosezztypo has no known correction. +MARKDOWN +cat > "$scratch/names.jl" <<'JULIA' +using Fatou +badconfig = kwarg(numchildren(node), cconvert(value)) +relint(badconfig) +JULIA +cat > "$scratch/names.yml" <<'YAML' +- name: Install Fatou + uses: jolars/fatou-action +YAML +cat > "$scratch/names.toml" <<'TOML' +[lint] +select = ["kwarg-default-mismatch"] +TOML +cspell "${cspell_args[@]}" "$scratch/names.md" "$scratch/names.jl" \ + "$scratch/names.yml" "$scratch/names.toml" + +# GitHub places this repository under directories ending in .jl. The bundled +# Julia override must not replace the language inferred from each filename. +checkout="$scratch/LineCableModels.jl/LineCableModels.jl" +mkdir -p "$checkout" +cat > "$checkout/example.sh" <<'SHELL' +#!/usr/bin/env bash +case value in + value) ;; +esac +SHELL +printf 'document.addEventListener("mouseout", () => {});\n' > "$checkout/example.js" +printf 'Read the [execution report](../local/validation-refoundation/report.md).\n' > "$checkout/example.md" +cspell "${cspell_args[@]}" "$checkout/example.sh" "$checkout/example.js" "$checkout/example.md" + +# Language dictionaries and Markdown link handling must preserve prose checks. +printf '# Read and recieve the result.\n' >> "$checkout/example.sh" +printf '// Read and recieve the result.\n' >> "$checkout/example.js" +printf '\nRead and recieve the result.\n' >> "$checkout/example.md" +cp "$scratch/unpublished.jl" "$checkout/unpublished.jl" +if cspell "${cspell_args[@]}" "$checkout/example.sh" "$checkout/example.js" \ + "$checkout/example.md" "$checkout/unpublished.jl" > "$scratch/languages.log" 2>&1; then + echo 'cspell skipped prose under a Julia checkout directory.' >&2 + exit 1 +else + test "$?" -eq 1 +fi +test "$(grep -c 'Unknown word (recieve)' "$scratch/languages.log")" -eq 4 + +# A tracked README remains checked even when Git ignores its directory. +git init --quiet "$scratch/repository" +mkdir -p "$scratch/repository/test/fixtures/reference" "$scratch/repository/docs/src/tutorials" "$scratch/repository/docs/build" +printf 'test/fixtures/reference/\n' > "$scratch/repository/.gitignore" +printf 'Read and recieve the result.\n' > "$scratch/repository/test/fixtures/reference/README.md" +printf 'License CONTRACT\n' > "$scratch/repository/LICENSE" +printf 'Generated prose.\n' > "$scratch/repository/docs/src/tutorials/example.md" +printf '

Generated prose.

\n' > "$scratch/repository/docs/build/index.html" +git -C "$scratch/repository" add .gitignore LICENSE +git -C "$scratch/repository" add --force test/fixtures/reference/README.md +(cd "$scratch/repository" && bash "$root/.github/prose/files.sh" "$scratch/selection") +grep -Fxq test/fixtures/reference/README.md "$scratch/selection/vale-files.txt" +grep -Fxq docs/src/tutorials/example.md "$scratch/selection/cspell-files.txt" +grep -Fxq docs/build/index.html "$scratch/selection/vale-files.txt" +! grep -Fxq LICENSE "$scratch/selection/vale-files.txt" +if cspell "${cspell_args[@]}" "$scratch/repository/test/fixtures/reference/README.md" > "$scratch/ignored.log" 2>&1; then + echo 'cspell skipped a tracked file under an ignored directory.' >&2 + exit 1 +fi +grep -Fq 'Unknown word (recieve)' "$scratch/ignored.log" + +# Both native reports remain readable after independent checker failures. +test -s "$scratch/invalid.json" +test -s "$scratch/spelling.log" +test "$(grep -c 'steps.prose_tools.outcome.*steps.prose_inputs.outcome' "$root/.github/workflows/CI.yml")" -eq 2 +grep -Fq 'if: ${{ success()' "$root/.github/workflows/CI.yml" +grep -Fq 'steps.docs_build.outcome' "$root/.github/workflows/CI.yml" + +if vale --no-global --config="$scratch/missing.ini" "$scratch/advisory.md" > "$scratch/bad-config.log" 2>&1; then + echo 'Vale accepted a missing configuration.' >&2 + exit 1 +fi +if cspell "${cspell_args[@]}" --config="$scratch/missing.json" "$scratch/science.jl" >> "$scratch/bad-config.log" 2>&1; then + echo 'cspell accepted a missing configuration.' >&2 + exit 1 +fi +grep -Fq 'Configuration Error' "$scratch/bad-config.log" +printf '{broken json\n' > "$scratch/malformed.json" +if cspell "${cspell_args[@]}" --config="$scratch/malformed.json" "$scratch/science.jl" > "$scratch/malformed.log" 2>&1; then + echo 'cspell accepted a malformed configuration.' >&2 + exit 1 +fi +grep -Fq 'Configuration Error' "$scratch/malformed.log" +if cspell "${cspell_args[@]}" "$scratch/missing.md" > "$scratch/missing-input.log" 2>&1; then + echo 'cspell accepted a missing input.' >&2 + exit 1 +fi + +rm "$scratch/repository/test/fixtures/reference/README.md" +if (cd "$scratch/repository" && bash "$root/.github/prose/files.sh" "$scratch/deleted"); then + echo 'Input selection accepted a missing tracked file.' >&2 + exit 1 +fi +printf 'Maintained prose.\n' > "$scratch/repository/test/fixtures/reference/README.md" +rm "$scratch/repository/docs/build/index.html" +if (cd "$scratch/repository" && bash "$root/.github/prose/files.sh" "$scratch/missing"); then + echo 'Input selection accepted a missing documentation build.' >&2 + exit 1 +fi +echo 'Writing checks passed.' diff --git a/.github/prose/fixtures/extraction.test.yml b/.github/prose/fixtures/extraction.test.yml new file mode 100644 index 000000000..943418b16 --- /dev/null +++ b/.github/prose/fixtures/extraction.test.yml @@ -0,0 +1,149 @@ +- name: unpublished Julia docstrings and comments + rule: ../styles/Project/BannedTerms.yml + format: jl + view: Julia + input: | + """ + provenance + """ + function f() end + "contract" + g() = 1 + raw"""provenance""" + h() = 2 + @doc "contract" f + # provenance + #= contract =# + identifier = "provenance" + contains: '2:' + absent: + - '12:' +- name: ordinary strings are code + rule: ../styles/Project/BannedTerms.yml + format: jl + view: Julia + input: | + identifier = "provenance" + canonical = :contract + @warn "provenance" key="contract" + want: "" +- name: ordinary single-line docstring + rule: ../styles/Project/BannedTerms.yml + format: jl + view: Julia + input: | + "contract" + f() = 1 + contains: '1:' +- name: block comment + rule: ../styles/Project/BannedTerms.yml + format: jl + view: Julia + input: | + #= contract =# + contains: '1:' +- name: raw single-line docstring + rule: ../styles/Project/BannedTerms.yml + format: jl + view: Julia + input: | + raw"contract" + f() = 1 + contains: Project.BannedTerms +- name: explicit triple-quoted docstring + rule: ../styles/Project/BannedTerms.yml + format: jl + view: Julia + input: | + @doc """ + provenance + """ f + contains: '2:' +- name: explicit raw docstring + rule: ../styles/Project/BannedTerms.yml + format: jl + view: Julia + input: | + @doc raw"contract" f + contains: Project.BannedTerms +- name: explicit raw triple-quoted docstring + rule: ../styles/Project/BannedTerms.yml + format: jl + view: Julia + input: | + @doc raw""" + provenance + """ f + contains: '2:' +- name: comments beside identifiers + rule: ../styles/Project/BannedTerms.yml + format: jl + view: Julia + input: | + contract = 1 # provenance + contains: '1:16:Project.BannedTerms:' +- name: docstring after an inline definition + rule: ../styles/Project/BannedTerms.yml + format: jl + view: Julia + input: | + @inline first(x) = x + """ + $(TYPEDSIGNATURES) + + contract + """ + function second(x) + x + end + contains: '5:' +- name: field documentation + rule: ../styles/Project/BannedTerms.yml + format: jl + view: Julia + input: | + struct Sample + "contract" + value::Int + end + contains: '2:' +- name: Literate directives + rule: ../styles/Project/NounString.yml + format: jl + view: Julia + input: | + f[1] #hide + f[2] #hide + f[3] #hide + f[4] #hide + want: "" +- name: GetDP comments + rule: ../styles/Project/BannedTerms.yml + format: pro + view: GetDP + input: | + // provenance + a = "contract"; + contains: Project.BannedTerms +- name: workflow names + rule: ../styles/Project/BannedTerms.yml + format: yml + view: Workflow + input: | + name: contract + run: echo provenance + contains: Project.BannedTerms +- name: JSON descriptions + rule: ../styles/Project/BannedTerms.yml + format: json + view: Descriptions + input: '{"description": "provenance", "key": "contract"}' + contains: Project.BannedTerms +- name: ignore-file comments + rule: ../styles/Project/BannedTerms.yml + format: txt + view: HashComments + input: | + # provenance + contract/ + contains: Project.BannedTerms diff --git a/.github/prose/package-lock.json b/.github/prose/package-lock.json new file mode 100644 index 000000000..3fc798f09 --- /dev/null +++ b/.github/prose/package-lock.json @@ -0,0 +1,1050 @@ +{ + "name": "linecablemodels-prose-tools", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "linecablemodels-prose-tools", + "dependencies": { + "cspell": "10.3.6" + } + }, + "node_modules/@cspell/cspell-bundled-dicts": { + "version": "10.3.6", + "resolved": "https://registry.npmjs.org/@cspell/cspell-bundled-dicts/-/cspell-bundled-dicts-10.3.6.tgz", + "integrity": "sha512-C3ggnJkA3q6/uap0h/UY1CH6SsAzfn6Y5xUDoA1PH7OJUtgGgrW/LY/vo2orN6ppH072L0Ke+sxSEY1OSBRpnA==", + "license": "MIT", + "dependencies": { + "@cspell/dict-ada": "^4.1.1", + "@cspell/dict-al": "^1.1.1", + "@cspell/dict-aws": "^4.0.17", + "@cspell/dict-bash": "^4.2.3", + "@cspell/dict-companies": "^3.2.13", + "@cspell/dict-cpp": "^7.1.2", + "@cspell/dict-cryptocurrencies": "^5.0.5", + "@cspell/dict-csharp": "^4.0.8", + "@cspell/dict-css": "^4.1.2", + "@cspell/dict-dart": "^2.3.2", + "@cspell/dict-data-science": "^2.0.16", + "@cspell/dict-django": "^4.1.6", + "@cspell/dict-docker": "^1.1.17", + "@cspell/dict-dotnet": "^5.0.15", + "@cspell/dict-elixir": "^4.0.8", + "@cspell/dict-en_us": "^4.4.40", + "@cspell/dict-en-common-misspellings": "^2.2.2", + "@cspell/dict-en-gb-mit": "^3.1.29", + "@cspell/dict-filetypes": "^3.0.18", + "@cspell/dict-flutter": "^1.1.1", + "@cspell/dict-fonts": "^4.0.6", + "@cspell/dict-fsharp": "^1.1.1", + "@cspell/dict-fullstack": "^3.2.9", + "@cspell/dict-gaming-terms": "^1.1.2", + "@cspell/dict-git": "^3.1.1", + "@cspell/dict-golang": "^6.0.29", + "@cspell/dict-google": "^1.0.9", + "@cspell/dict-haskell": "^4.0.6", + "@cspell/dict-html": "^4.0.16", + "@cspell/dict-html-symbol-entities": "^4.0.5", + "@cspell/dict-java": "^5.0.12", + "@cspell/dict-julia": "^1.1.1", + "@cspell/dict-k8s": "^1.0.13", + "@cspell/dict-kotlin": "^1.1.1", + "@cspell/dict-latex": "^5.1.0", + "@cspell/dict-lorem-ipsum": "^4.0.5", + "@cspell/dict-lua": "^4.0.8", + "@cspell/dict-makefile": "^1.0.5", + "@cspell/dict-markdown": "^2.0.18", + "@cspell/dict-monkeyc": "^1.1.2", + "@cspell/dict-node": "^5.0.9", + "@cspell/dict-npm": "^5.2.50", + "@cspell/dict-php": "^4.1.3", + "@cspell/dict-powershell": "^5.0.17", + "@cspell/dict-public-licenses": "^2.0.16", + "@cspell/dict-python": "^4.5.2", + "@cspell/dict-r": "^2.1.1", + "@cspell/dict-ruby": "^5.1.4", + "@cspell/dict-rust": "^4.1.2", + "@cspell/dict-scala": "^5.0.11", + "@cspell/dict-shell": "^1.2.0", + "@cspell/dict-software-terms": "^5.4.5", + "@cspell/dict-sql": "^2.2.1", + "@cspell/dict-svelte": "^1.0.7", + "@cspell/dict-swift": "^2.0.6", + "@cspell/dict-terraform": "^1.1.4", + "@cspell/dict-typescript": "^3.2.3", + "@cspell/dict-vue": "^3.0.5", + "@cspell/dict-zig": "^1.0.0" + }, + "engines": { + "node": ">=22.18.0" + } + }, + "node_modules/@cspell/cspell-json-reporter": { + "version": "10.3.6", + "resolved": "https://registry.npmjs.org/@cspell/cspell-json-reporter/-/cspell-json-reporter-10.3.6.tgz", + "integrity": "sha512-eYwTqMZDIpNntPN+9pLD64ZFWJ9EiQrbas8kB6JXrS7gd+xXnDg6U+k2CmZGCpFKsArPE66PSudYRv+PoAb4CQ==", + "license": "MIT", + "dependencies": { + "@cspell/cspell-types": "10.3.6" + }, + "engines": { + "node": ">=22.18.0" + } + }, + "node_modules/@cspell/cspell-performance-monitor": { + "version": "10.3.6", + "resolved": "https://registry.npmjs.org/@cspell/cspell-performance-monitor/-/cspell-performance-monitor-10.3.6.tgz", + "integrity": "sha512-NISfRQ1bENcT+U9otrj+N1uyizZb7NJ8bXXnB8WYp4OHp7Hztb7wUDiNNcWoF3S8joMfMjxNr6HABeThl/Ohow==", + "license": "MIT", + "engines": { + "node": ">=22.18.0" + } + }, + "node_modules/@cspell/cspell-pipe": { + "version": "10.3.6", + "resolved": "https://registry.npmjs.org/@cspell/cspell-pipe/-/cspell-pipe-10.3.6.tgz", + "integrity": "sha512-gh/a0TL5hNSegkDiRFPC4nz4qK0BaLXi+tyoZWWZA9cfkOWsQMHApBxmJcukKeFFPsId2jyep814yy8O3xwdrg==", + "license": "MIT", + "engines": { + "node": ">=22.18.0" + } + }, + "node_modules/@cspell/cspell-resolver": { + "version": "10.3.6", + "resolved": "https://registry.npmjs.org/@cspell/cspell-resolver/-/cspell-resolver-10.3.6.tgz", + "integrity": "sha512-P+90E0ED3qfGhmYqV0JZcASeWBoXqddOsknnU5SYrEbEd5+LAU0SYX2H2p+GPk7uYuPE0IP1eGATfY3KkFlSfg==", + "license": "MIT", + "dependencies": { + "global-directory": "^5.0.0" + }, + "engines": { + "node": ">=22.18.0" + } + }, + "node_modules/@cspell/cspell-service-bus": { + "version": "10.3.6", + "resolved": "https://registry.npmjs.org/@cspell/cspell-service-bus/-/cspell-service-bus-10.3.6.tgz", + "integrity": "sha512-4voVfeS+HEJy+bsWTQw9+cX4V2v6aGf97BRQDf0QQs/mlOfa07tLIYto/ZCbjCYUBMix64xziELrmqWkWcf2Kw==", + "license": "MIT", + "engines": { + "node": ">=22.18.0" + } + }, + "node_modules/@cspell/cspell-types": { + "version": "10.3.6", + "resolved": "https://registry.npmjs.org/@cspell/cspell-types/-/cspell-types-10.3.6.tgz", + "integrity": "sha512-EJW6qFg/N1KcuNhOBo25nfL6QgrtBg1/KTzzsHpITxHSAnnZmLYCsSmEkunP8Voy4px2lkjDYN3OYEDBvPfd7A==", + "license": "MIT", + "engines": { + "node": ">=22.18.0" + } + }, + "node_modules/@cspell/cspell-worker": { + "version": "10.3.6", + "resolved": "https://registry.npmjs.org/@cspell/cspell-worker/-/cspell-worker-10.3.6.tgz", + "integrity": "sha512-gzmadwr0R20S8w2gUMqRHd1qQjzJmm8KgFyMVo9Hb7RBaVkqthwITw1FLJW6XqGGcBBZ1nNCfc8OrTOaeA2hxQ==", + "license": "MIT", + "dependencies": { + "cspell-lib": "10.3.6" + }, + "engines": { + "node": ">=22.18.0" + } + }, + "node_modules/@cspell/dict-ada": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@cspell/dict-ada/-/dict-ada-4.1.1.tgz", + "integrity": "sha512-E+0YW9RhZod/9Qy2gxfNZiHJjCYFlCdI69br1eviQQWB8yOTJX0JHXLs79kOYhSW0kINPVUdvddEBe6Lu6CjGQ==", + "license": "MIT" + }, + "node_modules/@cspell/dict-al": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@cspell/dict-al/-/dict-al-1.1.1.tgz", + "integrity": "sha512-sD8GCaZetgQL4+MaJLXqbzWcRjfKVp8x+px3HuCaaiATAAtvjwUQ5/Iubiqwfd1boIh2Y1/3EgM3TLQ7Q8e0wQ==", + "license": "MIT" + }, + "node_modules/@cspell/dict-aws": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@cspell/dict-aws/-/dict-aws-4.0.17.tgz", + "integrity": "sha512-ORcblTWcdlGjIbWrgKF+8CNEBQiLVKdUOFoTn0KPNkAYnFcdPP0muT4892h7H4Xafh3j72wqB4/loQ6Nti9E/w==", + "license": "MIT" + }, + "node_modules/@cspell/dict-bash": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@cspell/dict-bash/-/dict-bash-4.2.3.tgz", + "integrity": "sha512-ljUZoKHbDqw5Sx0qpL2qTUlmkmr+vhZH/sCNrNaBZKTbdgiswErSnIF1jRbGmEitJNxHRHWsuZyVgnTGfVO1Yw==", + "license": "MIT", + "dependencies": { + "@cspell/dict-shell": "1.2.0" + } + }, + "node_modules/@cspell/dict-companies": { + "version": "3.2.13", + "resolved": "https://registry.npmjs.org/@cspell/dict-companies/-/dict-companies-3.2.13.tgz", + "integrity": "sha512-mBJeYzCkiiWgNQxKg7zyQOpFPwcXO4IAzXGbOEFfcQ4Q0G5Fa7Kqd8z+gvnvSK7NPWKJaFmiApC6I0cka02qmQ==", + "license": "MIT" + }, + "node_modules/@cspell/dict-cpp": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@cspell/dict-cpp/-/dict-cpp-7.1.2.tgz", + "integrity": "sha512-bkR33oE+ToyD1hwSV4XeEUB9oDkizHRX03CrQfj23tX4VUVbie5vkEHzlKVhcw/Q+fO2dtV/Vz4f6O5M4a2iZQ==", + "license": "MIT" + }, + "node_modules/@cspell/dict-cryptocurrencies": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@cspell/dict-cryptocurrencies/-/dict-cryptocurrencies-5.0.5.tgz", + "integrity": "sha512-R68hYYF/rtlE6T/dsObStzN5QZw+0aQBinAXuWCVqwdS7YZo0X33vGMfChkHaiCo3Z2+bkegqHlqxZF4TD3rUA==", + "license": "MIT" + }, + "node_modules/@cspell/dict-csharp": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@cspell/dict-csharp/-/dict-csharp-4.0.8.tgz", + "integrity": "sha512-qmk45pKFHSxckl5mSlbHxmDitSsGMlk/XzFgt7emeTJWLNSTUK//MbYAkBNRtfzB4uD7pAFiKgpKgtJrTMRnrQ==", + "license": "MIT" + }, + "node_modules/@cspell/dict-css": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@cspell/dict-css/-/dict-css-4.1.2.tgz", + "integrity": "sha512-+ylGoKdwZ2sVOCOnU2Eq5wDZx+RaVX3HoKyNHGGsFvhSw6IidQ6tH/mAPKBDofViHJoWCPNlklE0lTr6MDG3QA==", + "license": "MIT" + }, + "node_modules/@cspell/dict-dart": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@cspell/dict-dart/-/dict-dart-2.3.2.tgz", + "integrity": "sha512-sUiLW56t9gfZcu8iR/5EUg+KYyRD83Cjl3yjDEA2ApVuJvK1HhX+vn4e4k4YfjpUQMag8XO2AaRhARE09+/rqw==", + "license": "MIT" + }, + "node_modules/@cspell/dict-data-science": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@cspell/dict-data-science/-/dict-data-science-2.0.16.tgz", + "integrity": "sha512-M72mxv5asuAnORurz4iXRJ+Tw9XBq6eu7D2Ne7biP0Z1RciKGNxXWu9JycA/KlVvK1hAlKj/fANlXhuEWpXKFg==", + "license": "MIT" + }, + "node_modules/@cspell/dict-django": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@cspell/dict-django/-/dict-django-4.1.6.tgz", + "integrity": "sha512-SdbSFDGy9ulETqNz15oWv2+kpWLlk8DJYd573xhIkeRdcXOjskRuxjSZPKfW7O3NxN/KEf3gm3IevVOiNuFS+w==", + "license": "MIT" + }, + "node_modules/@cspell/dict-docker": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@cspell/dict-docker/-/dict-docker-1.1.17.tgz", + "integrity": "sha512-OcnVTIpHIYYKhztNTyK8ShAnXTfnqs43hVH6p0py0wlcwRIXe5uj4f12n7zPf2CeBI7JAlPjEsV0Rlf4hbz/xQ==", + "license": "MIT" + }, + "node_modules/@cspell/dict-dotnet": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/@cspell/dict-dotnet/-/dict-dotnet-5.0.15.tgz", + "integrity": "sha512-j2MUKPlzMeDy/mnb1EjmtkX8e6VzZdw60Qyf6vPlp7WZhUwwp2LBlKOWx7+hASjP6k4a0nDpmn77G+4upxvbZA==", + "license": "MIT" + }, + "node_modules/@cspell/dict-elixir": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@cspell/dict-elixir/-/dict-elixir-4.0.8.tgz", + "integrity": "sha512-CyfphrbMyl4Ms55Vzuj+mNmd693HjBFr9hvU+B2YbFEZprE5AG+EXLYTMRWrXbpds4AuZcvN3deM2XVB80BN/Q==", + "license": "MIT" + }, + "node_modules/@cspell/dict-en_us": { + "version": "4.4.40", + "resolved": "https://registry.npmjs.org/@cspell/dict-en_us/-/dict-en_us-4.4.40.tgz", + "integrity": "sha512-TGyOaWP1dD28GRHH2Lt04ciMZC8Z6UX+5kJsi04pbQHq6uRxv14/bBVCBeSsajUXRZsJYUqXyHaHB9O0ODuFuQ==", + "license": "MIT" + }, + "node_modules/@cspell/dict-en-common-misspellings": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@cspell/dict-en-common-misspellings/-/dict-en-common-misspellings-2.2.2.tgz", + "integrity": "sha512-gu0zS7nm0jgVA2EswgWOR8od8wbNYVgcGzo0pGaDqcSkKS4WhABehLOwbyI0NIEVB+ME6SmckLVGk7g+VD1C6A==", + "license": "CC BY-SA 4.0" + }, + "node_modules/@cspell/dict-en-gb-mit": { + "version": "3.1.29", + "resolved": "https://registry.npmjs.org/@cspell/dict-en-gb-mit/-/dict-en-gb-mit-3.1.29.tgz", + "integrity": "sha512-mInExsCFZui0f9Vg/MToWfQZzwMfPgHX8BCYdZNdXWbYz+jITXcnvssbdUkui/DVbriZVJ74Zu9bv2jOEqTCNA==", + "license": "MIT" + }, + "node_modules/@cspell/dict-filetypes": { + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/@cspell/dict-filetypes/-/dict-filetypes-3.0.18.tgz", + "integrity": "sha512-yU7RKD/x1IWmDLzWeiItMwgV+6bUcU/af23uS0+uGiFUbsY1qWV/D4rxlAAO6Z7no3J2z8aZOkYIOvUrJq0Rcw==", + "license": "MIT" + }, + "node_modules/@cspell/dict-flutter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@cspell/dict-flutter/-/dict-flutter-1.1.1.tgz", + "integrity": "sha512-UlOzRcH2tNbFhZmHJN48Za/2/MEdRHl2BMkCWZBYs+30b91mWvBfzaN4IJQU7dUZtowKayVIF9FzvLZtZokc5A==", + "license": "MIT" + }, + "node_modules/@cspell/dict-fonts": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@cspell/dict-fonts/-/dict-fonts-4.0.6.tgz", + "integrity": "sha512-aR/0csY01dNb0A1tw/UmN9rKgHruUxsYsvXu6YlSBJFu60s26SKr/k1o4LavpHTQ+lznlYMqAvuxGkE4Flliqw==", + "license": "MIT" + }, + "node_modules/@cspell/dict-fsharp": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@cspell/dict-fsharp/-/dict-fsharp-1.1.1.tgz", + "integrity": "sha512-imhs0u87wEA4/cYjgzS0tAyaJpwG7vwtC8UyMFbwpmtw+/bgss+osNfyqhYRyS/ehVCWL17Ewx2UPkexjKyaBA==", + "license": "MIT" + }, + "node_modules/@cspell/dict-fullstack": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/@cspell/dict-fullstack/-/dict-fullstack-3.2.9.tgz", + "integrity": "sha512-diZX+usW5aZ4/b2T0QM/H/Wl9aNMbdODa1Jq0ReBr/jazmNeWjd+PyqeVgzd1joEaHY+SAnjrf/i9CwKd2ZtWQ==", + "license": "MIT" + }, + "node_modules/@cspell/dict-gaming-terms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@cspell/dict-gaming-terms/-/dict-gaming-terms-1.1.2.tgz", + "integrity": "sha512-9XnOvaoTBscq0xuD6KTEIkk9hhdfBkkvJAIsvw3JMcnp1214OCGW8+kako5RqQ2vTZR3Tnf3pc57o7VgkM0q1Q==", + "license": "MIT" + }, + "node_modules/@cspell/dict-git": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@cspell/dict-git/-/dict-git-3.1.1.tgz", + "integrity": "sha512-XvuGVYj9YEu4EzntqyhH8XlTRyy57C6j8oxkIbUQfR2UYzxZGAP77XHl7qY7eC/40r/haDwnBaccyFR6BRmUCg==", + "license": "MIT" + }, + "node_modules/@cspell/dict-golang": { + "version": "6.0.29", + "resolved": "https://registry.npmjs.org/@cspell/dict-golang/-/dict-golang-6.0.29.tgz", + "integrity": "sha512-EZCgfxrr5lPnYQLmSvB6YtpfoDFt2KKxAD4ppq2xGSIgDFXAF7Mv4doiXIFUQaynCBIEfAMl3f/uDfrIUo4fyQ==", + "license": "MIT" + }, + "node_modules/@cspell/dict-google": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@cspell/dict-google/-/dict-google-1.0.9.tgz", + "integrity": "sha512-biL65POqialY0i4g6crj7pR6JnBkbsPovB2WDYkj3H4TuC/QXv7Pu5pdPxeUJA6TSCHI7T5twsO4VSVyRxD9CA==", + "license": "MIT" + }, + "node_modules/@cspell/dict-haskell": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@cspell/dict-haskell/-/dict-haskell-4.0.6.tgz", + "integrity": "sha512-ib8SA5qgftExpYNjWhpYIgvDsZ/0wvKKxSP+kuSkkak520iPvTJumEpIE+qPcmJQo4NzdKMN8nEfaeci4OcFAQ==", + "license": "MIT" + }, + "node_modules/@cspell/dict-html": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@cspell/dict-html/-/dict-html-4.0.16.tgz", + "integrity": "sha512-9B6/Cpb5YVcYgsEAlKudun1yd4ONllYS/ZibKLYea2apPR+l2vQdARnPrP9kTqa7NUNfRKl7m9Fb6k9rjimnow==", + "license": "MIT" + }, + "node_modules/@cspell/dict-html-symbol-entities": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@cspell/dict-html-symbol-entities/-/dict-html-symbol-entities-4.0.5.tgz", + "integrity": "sha512-429alTD4cE0FIwpMucvSN35Ld87HCyuM8mF731KU5Rm4Je2SG6hmVx7nkBsLyrmH3sQukTcr1GaiZsiEg8svPA==", + "license": "MIT" + }, + "node_modules/@cspell/dict-java": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/@cspell/dict-java/-/dict-java-5.0.12.tgz", + "integrity": "sha512-qPSNhTcl7LGJ5Qp6VN71H8zqvRQK04S08T67knMq9hTA8U7G1sTKzLmBaDOFhq17vNX/+rT+rbRYp+B5Nwza1A==", + "license": "MIT" + }, + "node_modules/@cspell/dict-julia": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@cspell/dict-julia/-/dict-julia-1.1.1.tgz", + "integrity": "sha512-WylJR9TQ2cgwd5BWEOfdO3zvDB+L7kYFm0I9u0s9jKHWQ6yKmfKeMjU9oXxTBxIufhCXm92SKwwVNAC7gjv+yA==", + "license": "MIT" + }, + "node_modules/@cspell/dict-k8s": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@cspell/dict-k8s/-/dict-k8s-1.0.13.tgz", + "integrity": "sha512-ELGkS13k7K/NEfVimBSrxVTfqXvOF/Kvxj4I62YxRm8bvHbfoXgrGaOx28lPiNRz+dmu+yYtvuXbnURKtYbC6g==", + "license": "MIT" + }, + "node_modules/@cspell/dict-kotlin": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@cspell/dict-kotlin/-/dict-kotlin-1.1.1.tgz", + "integrity": "sha512-J3NzzfgmxRvEeOe3qUXnSJQCd38i/dpF9/t3quuWh6gXM+krsAXP75dY1CzDmS8mrJAlBdVBeAW5eAZTD8g86Q==", + "license": "MIT" + }, + "node_modules/@cspell/dict-latex": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@cspell/dict-latex/-/dict-latex-5.1.0.tgz", + "integrity": "sha512-qxT4guhysyBt0gzoliXYEBYinkAdEtR2M7goRaUH0a7ltCsoqqAeEV8aXYRIdZGcV77gYSobvu3jJL038tlPAw==", + "license": "MIT" + }, + "node_modules/@cspell/dict-lorem-ipsum": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@cspell/dict-lorem-ipsum/-/dict-lorem-ipsum-4.0.5.tgz", + "integrity": "sha512-9a4TJYRcPWPBKkQAJ/whCu4uCAEgv/O2xAaZEI0n4y1/l18Yyx8pBKoIX5QuVXjjmKEkK7hi5SxyIsH7pFEK9Q==", + "license": "MIT" + }, + "node_modules/@cspell/dict-lua": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@cspell/dict-lua/-/dict-lua-4.0.8.tgz", + "integrity": "sha512-N4PkgNDMu9JVsRu7JBS/3E/dvfItRgk9w5ga2dKq+JupP2Y3lojNaAVFhXISh4Y0a6qXDn2clA6nvnavQ/jjLA==", + "license": "MIT" + }, + "node_modules/@cspell/dict-makefile": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@cspell/dict-makefile/-/dict-makefile-1.0.5.tgz", + "integrity": "sha512-4vrVt7bGiK8Rx98tfRbYo42Xo2IstJkAF4tLLDMNQLkQ86msDlYSKG1ZCk8Abg+EdNcFAjNhXIiNO+w4KflGAQ==", + "license": "MIT" + }, + "node_modules/@cspell/dict-markdown": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@cspell/dict-markdown/-/dict-markdown-2.0.18.tgz", + "integrity": "sha512-KLRSwVrwKDz4n7bl2XQjzvaBZbGgx5QKkP5Ok1b24xaO3TpXsLhAbAn1mItvFlI2tF6Rkt83iYjQcYppywuf5Q==", + "license": "MIT", + "peerDependencies": { + "@cspell/dict-css": "^4.1.2", + "@cspell/dict-html": "^4.0.16", + "@cspell/dict-html-symbol-entities": "^4.0.5", + "@cspell/dict-typescript": "^3.2.3" + } + }, + "node_modules/@cspell/dict-monkeyc": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@cspell/dict-monkeyc/-/dict-monkeyc-1.1.2.tgz", + "integrity": "sha512-4VzwdSUVqyUVmAKAzMkVwVa7AkvSi1l1OHdQr58MWq4VR4NQDO8sVQSibd2PMxruXTkxJt9UwVQVHgaaVTiV1Q==", + "license": "MIT" + }, + "node_modules/@cspell/dict-node": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/@cspell/dict-node/-/dict-node-5.0.9.tgz", + "integrity": "sha512-hO+ga+uYZ/WA4OtiMEyKt5rDUlUyu3nXMf8KVEeqq2msYvAPdldKBGH7lGONg6R/rPhv53Rb+0Y1SLdoK1+7wQ==", + "license": "MIT" + }, + "node_modules/@cspell/dict-npm": { + "version": "5.2.50", + "resolved": "https://registry.npmjs.org/@cspell/dict-npm/-/dict-npm-5.2.50.tgz", + "integrity": "sha512-JZhJb0wrWpqnzDBM1VG8f6zZPRCOo6YMxct0hFhgBnbOTIHQiqZf9S1BSXCRMmmQlAWqFXjKNDp+rf7ZiJcO9w==", + "license": "MIT" + }, + "node_modules/@cspell/dict-php": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@cspell/dict-php/-/dict-php-4.1.3.tgz", + "integrity": "sha512-41oKzl7rLVrrVZA/Gs0ESnP6KliEX7c+IwaaNmOL3PAoHasFDVEmIFeENIzuDdM12UcHT+MlLkAm3Qw/lS/ERA==", + "license": "MIT" + }, + "node_modules/@cspell/dict-powershell": { + "version": "5.0.17", + "resolved": "https://registry.npmjs.org/@cspell/dict-powershell/-/dict-powershell-5.0.17.tgz", + "integrity": "sha512-khOoIdBgKRk2scnr7888pf70yJ1Tr3RGdnYcAN0MJ2vtaNRKoOTCFc13gXUkNDHwEyhMI1h9oz3KsT180F2amw==", + "license": "MIT" + }, + "node_modules/@cspell/dict-public-licenses": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@cspell/dict-public-licenses/-/dict-public-licenses-2.0.16.tgz", + "integrity": "sha512-EQRrPvEOmwhwWezV+W7LjXbIBjiy6y/shrET6Qcpnk3XANTzfvWflf9PnJ5kId/oKWvihFy0za0AV1JHd03pSQ==", + "license": "MIT" + }, + "node_modules/@cspell/dict-python": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@cspell/dict-python/-/dict-python-4.5.2.tgz", + "integrity": "sha512-CFcTDGN/ycn45WXC5ZZ06pG09WbGKMpPm1rkMuqMP98/jwjw3iiK8c0izWRd8x027n1rOZ9kJ8f9Sd+bQ22HJg==", + "license": "MIT", + "dependencies": { + "@cspell/dict-data-science": "^2.0.16" + } + }, + "node_modules/@cspell/dict-r": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@cspell/dict-r/-/dict-r-2.1.1.tgz", + "integrity": "sha512-71Ka+yKfG4ZHEMEmDxc6+blFkeTTvgKbKAbwiwQAuKl3zpqs1Y0vUtwW2N4b3LgmSPhV3ODVY0y4m5ofqDuKMw==", + "license": "MIT" + }, + "node_modules/@cspell/dict-ruby": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@cspell/dict-ruby/-/dict-ruby-5.1.4.tgz", + "integrity": "sha512-Slw1dLY/pW5liJ5Fle+MIPphIdleZkIM6STDoXCN7sy298GfbfT34sJTcSZcXzZJVXiCEAvlLp+s1YNr6DuWNg==", + "license": "MIT" + }, + "node_modules/@cspell/dict-rust": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@cspell/dict-rust/-/dict-rust-4.1.2.tgz", + "integrity": "sha512-O1FHrumYcO+HZti3dHfBPUdnDFkI+nbYK3pxYmiM1sr+G0ebOd6qchmswS0Wsc6ZdEVNiPYJY/gZQR6jfW3uOg==", + "license": "MIT" + }, + "node_modules/@cspell/dict-scala": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/@cspell/dict-scala/-/dict-scala-5.0.11.tgz", + "integrity": "sha512-+D59847Y61KZ0MBoXJzBMtOXfFhezEb6ShjvJlL7bnryyy6iBwAfozbz7OJAe+KrEpYm+GlVV7Wy4FmJSgxBag==", + "license": "MIT" + }, + "node_modules/@cspell/dict-shell": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@cspell/dict-shell/-/dict-shell-1.2.0.tgz", + "integrity": "sha512-PVctvT22lJ49niMiakO8xieY7ELCAzjSqhejWR7bAMb5AZ9F4WDEs+XdGMnoVHWeXq7K5rcepLPmEJb+37zzIw==", + "license": "MIT" + }, + "node_modules/@cspell/dict-software-terms": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/@cspell/dict-software-terms/-/dict-software-terms-5.4.5.tgz", + "integrity": "sha512-5PgLJNdaFKeA6iklB0XXjmgNaoLJJqf54oRTD1raG/mgwojVlRnWE1G7x4QH9Gz07h3B3+lMDv1F57Xxs4jang==", + "license": "MIT" + }, + "node_modules/@cspell/dict-sql": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@cspell/dict-sql/-/dict-sql-2.2.1.tgz", + "integrity": "sha512-qDHF8MpAYCf4pWU8NKbnVGzkoxMNrFqBHyG/dgrlic5EQiKANCLELYtGlX5auIMDLmTf1inA0eNtv74tyRJ/vg==", + "license": "MIT" + }, + "node_modules/@cspell/dict-svelte": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@cspell/dict-svelte/-/dict-svelte-1.0.7.tgz", + "integrity": "sha512-hGZsGqP0WdzKkdpeVLBivRuSNzOTvN036EBmpOwxH+FTY2DuUH7ecW+cSaMwOgmq5JFSdTcbTNFlNC8HN8lhaQ==", + "license": "MIT" + }, + "node_modules/@cspell/dict-swift": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@cspell/dict-swift/-/dict-swift-2.0.6.tgz", + "integrity": "sha512-PnpNbrIbex2aqU1kMgwEKvCzgbkHtj3dlFLPMqW1vSniop7YxaDTtvTUO4zA++ugYAEL+UK8vYrBwDPTjjvSnA==", + "license": "MIT" + }, + "node_modules/@cspell/dict-terraform": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@cspell/dict-terraform/-/dict-terraform-1.1.4.tgz", + "integrity": "sha512-Ere42ilvMFvQA4GlcN0OKlruMPR6EsvaB+iTHzj2xc+NJGRK64V7yApUcWrOrSgTiM/vhWXPIsK3OMfiAiNdmA==", + "license": "MIT" + }, + "node_modules/@cspell/dict-typescript": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@cspell/dict-typescript/-/dict-typescript-3.2.3.tgz", + "integrity": "sha512-zXh1wYsNljQZfWWdSPYwQhpwiuW0KPW1dSd8idjMRvSD0aSvWWHoWlrMsmZeRl4qM4QCEAjua8+cjflm41cQBg==", + "license": "MIT" + }, + "node_modules/@cspell/dict-vue": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@cspell/dict-vue/-/dict-vue-3.0.5.tgz", + "integrity": "sha512-Mqutb8jbM+kIcywuPQCCaK5qQHTdaByoEO2J9LKFy3sqAdiBogNkrplqUK0HyyRFgCfbJUgjz3N85iCMcWH0JA==", + "license": "MIT" + }, + "node_modules/@cspell/dict-zig": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@cspell/dict-zig/-/dict-zig-1.0.0.tgz", + "integrity": "sha512-XibBIxBlVosU06+M6uHWkFeT0/pW5WajDRYdXG2CgHnq85b0TI/Ks0FuBJykmsgi2CAD3Qtx8UHFEtl/DSFnAQ==", + "license": "MIT" + }, + "node_modules/@cspell/dynamic-import": { + "version": "10.3.6", + "resolved": "https://registry.npmjs.org/@cspell/dynamic-import/-/dynamic-import-10.3.6.tgz", + "integrity": "sha512-HGPM+lUJYIJqzjqEGteEy4w2WSiWxwjc1vBRVTncaxVMpRk5llk6SxnmautqGwvIGbEkipZ1CBuRnFpdwX9dGQ==", + "license": "MIT", + "dependencies": { + "@cspell/url": "10.3.6", + "import-meta-resolve": "^4.2.0" + }, + "engines": { + "node": ">=22.18.0" + } + }, + "node_modules/@cspell/filetypes": { + "version": "10.3.6", + "resolved": "https://registry.npmjs.org/@cspell/filetypes/-/filetypes-10.3.6.tgz", + "integrity": "sha512-0zCfqsPhZY/1v6qB87AwpCgduLcVRAMyq+MQN6XhmRTfxrMZF8B4qsJafspOpYQ4XTNw5lyciwx60y1ba151cQ==", + "license": "MIT", + "engines": { + "node": ">=22.18.0" + } + }, + "node_modules/@cspell/rpc": { + "version": "10.3.6", + "resolved": "https://registry.npmjs.org/@cspell/rpc/-/rpc-10.3.6.tgz", + "integrity": "sha512-ZWwwYhrlcILvmH389pShyG0ef17lKMza25eSiG6snVnpVcIK96o1Pfr4ylNkbtMlYayvmbt9kLjHroMYdmFmRQ==", + "license": "MIT", + "engines": { + "node": ">=22.18.0" + } + }, + "node_modules/@cspell/strong-weak-map": { + "version": "10.3.6", + "resolved": "https://registry.npmjs.org/@cspell/strong-weak-map/-/strong-weak-map-10.3.6.tgz", + "integrity": "sha512-tkvKTTu8GRdNGZbC/Fl8Sat5UsAVhX7qsi4YX9mfd4a/Lrp9x73/mP8wGB637qqBPoN7f9x8ZR6YtTpOFVVfDA==", + "license": "MIT", + "engines": { + "node": ">=22.18.0" + } + }, + "node_modules/@cspell/url": { + "version": "10.3.6", + "resolved": "https://registry.npmjs.org/@cspell/url/-/url-10.3.6.tgz", + "integrity": "sha512-IaEu6ePAWmb8QG+MFdhEnnGBAVhI+Qhph97x0v6RrwytwlyuO6MynaIcuYDCb9aLEDBVrDIGDj/+dplL+Sj8LQ==", + "license": "MIT", + "engines": { + "node": ">=22.18.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "license": "MIT" + }, + "node_modules/chalk": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-6.0.1.tgz", + "integrity": "sha512-/Ce6KNm3vIbWdMlNna6RVIZ/ICQxnJxCicet5LBKK9ZffBkqzDw0xh9EiKSljdRtiIQ1S1z4YgcscUUGzNCWrA==", + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk-template": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-1.1.3.tgz", + "integrity": "sha512-KDt7oQ+LRF2VLFsTAZd76ZNCKQa8LuW7SZkNJMkriMzvlSNj6dI+S+f41mfK9oTtil49seHVyabtt1753sFL2w==", + "license": "MIT", + "dependencies": { + "chalk": "^5.2.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/chalk/chalk-template?sponsor=1" + } + }, + "node_modules/chalk-template/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/comment-json": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-5.0.0.tgz", + "integrity": "sha512-uiqLcOiVDJtBP8WGkZHEP+FZIhTzP1dxvn59EfoYUi9gqupjrBWVQkO2atDrbnKPwLeotFYDsuNb26uBMqB+hw==", + "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cspell": { + "version": "10.3.6", + "resolved": "https://registry.npmjs.org/cspell/-/cspell-10.3.6.tgz", + "integrity": "sha512-RZDeSxy1L82U7I+cF00pG2Bh3nBy0UoNRJF3Xbq4xbbKMZHwQRYrwospDyuIvsYmKjqDSAAciNvAjHOeEhP+5A==", + "license": "MIT", + "dependencies": { + "@cspell/cspell-json-reporter": "10.3.6", + "@cspell/cspell-performance-monitor": "10.3.6", + "@cspell/cspell-pipe": "10.3.6", + "@cspell/cspell-types": "10.3.6", + "@cspell/cspell-worker": "10.3.6", + "@cspell/dynamic-import": "10.3.6", + "@cspell/url": "10.3.6", + "ansi-regex": "^6.3.0", + "chalk": "^6.0.0", + "chalk-template": "^1.1.2", + "commander": "^15.0.0", + "cspell-config-lib": "10.3.6", + "cspell-dictionary": "10.3.6", + "cspell-gitignore": "10.3.6", + "cspell-glob": "10.3.6", + "cspell-io": "10.3.6", + "cspell-lib": "10.3.6", + "flatted": "^3.4.4", + "semver": "^7.8.5", + "tinyglobby": "^0.2.17" + }, + "bin": { + "cspell": "bin.mjs", + "cspell-esm": "bin.mjs" + }, + "engines": { + "node": ">=22.18.0" + }, + "funding": { + "url": "https://github.com/streetsidesoftware/cspell?sponsor=1" + } + }, + "node_modules/cspell-config-lib": { + "version": "10.3.6", + "resolved": "https://registry.npmjs.org/cspell-config-lib/-/cspell-config-lib-10.3.6.tgz", + "integrity": "sha512-3LcA2uIgB90Ucn3xa1lO5VVncHtsD4LZUf2ENBtHVmxFVFw1wvm1hbXL1mhenLY2xJDONGvLKRnNoowUWIZoDQ==", + "license": "MIT", + "dependencies": { + "@cspell/cspell-types": "10.3.6", + "comment-json": "^5.0.0", + "smol-toml": "^1.8.0", + "yaml": "^2.9.0" + }, + "engines": { + "node": ">=22.18.0" + } + }, + "node_modules/cspell-dictionary": { + "version": "10.3.6", + "resolved": "https://registry.npmjs.org/cspell-dictionary/-/cspell-dictionary-10.3.6.tgz", + "integrity": "sha512-retLLs4RFAxrduMfcgGXrHbMBc4fEqFt9pFRAyyH2HOlBmEU7CsBupx91A1FUJXUrGWDnfYwSzhQcWuFAAX8iQ==", + "license": "MIT", + "dependencies": { + "@cspell/cspell-performance-monitor": "10.3.6", + "@cspell/cspell-pipe": "10.3.6", + "@cspell/cspell-types": "10.3.6", + "cspell-trie-lib": "10.3.6", + "fast-equals": "^6.0.2" + }, + "engines": { + "node": ">=22.18.0" + } + }, + "node_modules/cspell-gitignore": { + "version": "10.3.6", + "resolved": "https://registry.npmjs.org/cspell-gitignore/-/cspell-gitignore-10.3.6.tgz", + "integrity": "sha512-Nvvbz2JA4c/ykzfzmdRazmKFMAV/dmt7pg+PB5WCCe7oKPjyKTbWoWFMEF3ActteYatV+lyamGLdncJov3rREw==", + "license": "MIT", + "dependencies": { + "@cspell/url": "10.3.6", + "cspell-glob": "10.3.6", + "cspell-io": "10.3.6" + }, + "bin": { + "cspell-gitignore": "bin.mjs" + }, + "engines": { + "node": ">=22.18.0" + } + }, + "node_modules/cspell-glob": { + "version": "10.3.6", + "resolved": "https://registry.npmjs.org/cspell-glob/-/cspell-glob-10.3.6.tgz", + "integrity": "sha512-mWlrUYSZh2DnXyfi5ZsHlclcd/grSbtZmv1Ug7EO2hPEWzIVctYp4RtaTEwOdjUit3dzybtSXqzqVhd4leZOKQ==", + "license": "MIT", + "dependencies": { + "@cspell/url": "10.3.6", + "picomatch": "^4.0.7" + }, + "engines": { + "node": ">=22.18.0" + } + }, + "node_modules/cspell-grammar": { + "version": "10.3.6", + "resolved": "https://registry.npmjs.org/cspell-grammar/-/cspell-grammar-10.3.6.tgz", + "integrity": "sha512-Z9CCWEcp+Y6lVAUtY8DykG4bcp9LEh09An0qdthRXVutAvkfNKzGeSK+m6IgJNIh+eb6jW1pZ69XoSOHFDADxg==", + "license": "MIT", + "dependencies": { + "@cspell/cspell-pipe": "10.3.6", + "@cspell/cspell-types": "10.3.6" + }, + "bin": { + "cspell-grammar": "bin.mjs" + }, + "engines": { + "node": ">=22.18.0" + } + }, + "node_modules/cspell-io": { + "version": "10.3.6", + "resolved": "https://registry.npmjs.org/cspell-io/-/cspell-io-10.3.6.tgz", + "integrity": "sha512-n8iBChxmLLjpFR2BaSYGVwWwM5MK3PQe83QZj6qzastb3lYff0oyH5NVXVQspyglUDy7muViddKkNJ/MbxuV/A==", + "license": "MIT", + "dependencies": { + "@cspell/cspell-service-bus": "10.3.6", + "@cspell/url": "10.3.6" + }, + "engines": { + "node": ">=22.18.0" + } + }, + "node_modules/cspell-lib": { + "version": "10.3.6", + "resolved": "https://registry.npmjs.org/cspell-lib/-/cspell-lib-10.3.6.tgz", + "integrity": "sha512-zlH+vh5R6/7mEARy0Qsf9fNOihYeb15r0+bKZ0bwx0CDLuyVM1rs/BEAdRIPoaAcqdvdig0ARLdakkgM1zco/g==", + "license": "MIT", + "dependencies": { + "@cspell/cspell-bundled-dicts": "10.3.6", + "@cspell/cspell-performance-monitor": "10.3.6", + "@cspell/cspell-pipe": "10.3.6", + "@cspell/cspell-resolver": "10.3.6", + "@cspell/cspell-types": "10.3.6", + "@cspell/dynamic-import": "10.3.6", + "@cspell/filetypes": "10.3.6", + "@cspell/rpc": "10.3.6", + "@cspell/strong-weak-map": "10.3.6", + "@cspell/url": "10.3.6", + "cspell-config-lib": "10.3.6", + "cspell-dictionary": "10.3.6", + "cspell-glob": "10.3.6", + "cspell-grammar": "10.3.6", + "cspell-io": "10.3.6", + "cspell-trie-lib": "10.3.6", + "env-paths": "^4.0.0", + "gensequence": "^8.0.8", + "import-fresh": "^4.0.0", + "resolve-from": "^5.0.0", + "vscode-languageserver-textdocument": "^1.0.14", + "vscode-uri": "^3.2.0", + "xdg-basedir": "^5.1.0" + }, + "engines": { + "node": ">=22.18.0" + } + }, + "node_modules/cspell-trie-lib": { + "version": "10.3.6", + "resolved": "https://registry.npmjs.org/cspell-trie-lib/-/cspell-trie-lib-10.3.6.tgz", + "integrity": "sha512-8PWHpsMUAl8RcB5Ho+lhrSapjcjO/f7wkF8XOxw4oSAF2mERJEm9ejuTRJyXtDmUtt831HB4f/RwKmVgjJyLGg==", + "license": "MIT", + "engines": { + "node": ">=22.18.0" + }, + "peerDependencies": { + "@cspell/cspell-types": "10.3.6" + } + }, + "node_modules/env-paths": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-4.0.0.tgz", + "integrity": "sha512-pxP8eL2SwwaTRi/KHYwLYXinDs7gL3jxFcBYmEdYfZmZXbaVDvdppd0XBU8qVz03rDfKZMXg1omHCbsJjZrMsw==", + "license": "MIT", + "dependencies": { + "is-safe-filename": "^0.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-equals": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-6.0.4.tgz", + "integrity": "sha512-JiBeR0k76FIahi9zAtQR7X1jgZDKLE+fs5S9ryjOGYZWZCOcm8QiwgeahkFvaboS3i6PkGBopRznHKuKhm9zpg==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "license": "ISC" + }, + "node_modules/gensequence": { + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/gensequence/-/gensequence-8.0.8.tgz", + "integrity": "sha512-omMVniXEXpdx/vKxGnPRoO2394Otlze28TyxECbFVyoSpZ9H3EO7lemjcB12OpQJzRW4e5tt/dL1rOxry6aMHg==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/global-directory": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-5.0.0.tgz", + "integrity": "sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w==", + "license": "MIT", + "dependencies": { + "ini": "6.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-4.0.1.tgz", + "integrity": "sha512-/XFeXYgkuWgbtqPC7qPKvciXTvhtz7WMNQKQsAM1Kz+j0I5OWJDrybfiya6QtHF9oEvPkLwTHmC9+4OR/MmfIA==", + "license": "MIT", + "engines": { + "node": ">=22.15" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/is-safe-filename": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-safe-filename/-/is-safe-filename-0.1.1.tgz", + "integrity": "sha512-4SrR7AdnY11LHfDKTZY1u6Ga3RuxZdl3YKWWShO5iyuG5h8QS4GD2tOb04peBJ5I7pXbR+CGBNEhTcwK+FzN3g==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/smol-toml": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.0.tgz", + "integrity": "sha512-hpd+HLON7HdZXqYchMM/+LaTTbdK0AU3NngIJ4KVyWbY9bfQqdL9cD+4yf6dUoU2Ap4VsU0JkQi6FxAI1B2mXQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.15.tgz", + "integrity": "sha512-Gsfr9S8LU3S6885cS+NaleasJ0n3V5y/SOTqHCifQiXWFw3G9w9/YItDfwHrlCz+dSZWWZ8ilYVMcJe7Sz7Sxw==", + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.2.0.tgz", + "integrity": "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==", + "license": "MIT" + }, + "node_modules/xdg-basedir": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", + "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/.github/prose/package.json b/.github/prose/package.json new file mode 100644 index 000000000..655eb2260 --- /dev/null +++ b/.github/prose/package.json @@ -0,0 +1,7 @@ +{ + "name": "linecablemodels-prose-tools", + "private": true, + "dependencies": { + "cspell": "10.3.6" + } +} diff --git a/.github/prose/styles/Project/Ambiguity.yml b/.github/prose/styles/Project/Ambiguity.yml new file mode 100644 index 000000000..b053e08f9 --- /dev/null +++ b/.github/prose/styles/Project/Ambiguity.yml @@ -0,0 +1,14 @@ +extends: STE.Ambiguity +tokens-: + - '\betc\.?' + - '\b[a-z]{3,}/[a-z]{3,}\b' +tokens+: + - '\betc\b\.?' + - '(?-i:\b[a-z]{3,}/[a-z]{3,}\b)' +tests: + - name: abbreviations and alternatives + input: "Use bolts, etc. Select input/output." + contains: Project.Ambiguity + - name: institution and protocol names + input: "Etch Competence Hub uses HTTP/TLS." + want: "" diff --git a/.github/prose/styles/Project/BannedTerms.yml b/.github/prose/styles/Project/BannedTerms.yml new file mode 100644 index 000000000..8b58bf824 --- /dev/null +++ b/.github/prose/styles/Project/BannedTerms.yml @@ -0,0 +1,26 @@ +extends: existence +message: "Replace '%s' with a specific description." +level: error +ignorecase: true +tokens: + - provenances? + - contracts? +tests: + - name: forbidden words and plurals + input: "Provenance PROVENANCES contract Contracts" + contains: Project.BannedTerms + - name: scientific word with a different meaning + input: "The contraction maps the interval into itself." + want: "" + - name: uppercase + input: "PROVENANCE" + contains: Project.BannedTerms + - name: plural + input: "provenances" + contains: Project.BannedTerms + - name: other forbidden word + input: "contract" + contains: Project.BannedTerms + - name: uppercase plural + input: "CONTRACTS" + contains: Project.BannedTerms diff --git a/.github/prose/styles/Project/Boundary.yml b/.github/prose/styles/Project/Boundary.yml new file mode 100644 index 000000000..dc457c72b --- /dev/null +++ b/.github/prose/styles/Project/Boundary.yml @@ -0,0 +1,30 @@ +extends: existence +message: "Use '%s' only for geometry or a physical condition." +level: error +ignorecase: true +tokens: + - boundar(?:y|ies) +exceptions: + # Geometric surfaces and prescribed conditions in field equations. + - '(?i)(?:physical|geometric|material|conductor|domain|outer|inner|earth|air|far|surface|mesh)\s+boundar(?:y|ies)' + - '(?i)boundar(?:y|ies)\s+(?:conditions?|values?|elements?|integrals?|charges?|surfaces?|curves?|edges?|nodes?|facets?|segments?|loops?)' + - '(?i)boundary[- ]value\s+problems?' + # Cross-sectional outlines, mesh topology, and local field discretizations. + - '(?i)(?:core|cable|cell|disk|sector|enclosure|layer|dielectric|metal|host|source|hole|filler|polygon|polygonal|circular|curved|parallel|outward|formation|Robin)\s+boundar(?:y|ies)' + - '(?i)boundary[- ](?:shunt|collocation|grid|integral|domain|resolved|discretization|circle|edge|incidence|perimeter|coefficients?|equations?|correction|solve)\s*' +tests: + - name: software use fails + input: "The API boundary validates the request." + contains: Project.Boundary + - name: geometry + input: "Apply the boundary conditions at the outer boundary." + want: "" + - name: adjacent software use still fails + input: "The boundary conditions are fixed. The API boundary validates input." + contains: Project.Boundary + - name: uppercase plural + input: "BOUNDARIES" + contains: Project.Boundary + - name: software plural + input: "The API boundaries are fixed." + contains: Project.Boundary diff --git a/.github/prose/styles/Project/Canonical.yml b/.github/prose/styles/Project/Canonical.yml new file mode 100644 index 000000000..4a35f3306 --- /dev/null +++ b/.github/prose/styles/Project/Canonical.yml @@ -0,0 +1,31 @@ +extends: existence +message: "Use '%s' only for a specified linear-algebra construction." +level: error +ignorecase: true +tokens: + - canonical(?:s|ly)? +exceptions: + # Standard coordinate vectors in linear algebra. + - '(?i)canonical\s+bas(?:is|es)' +tests: + - name: software use fails + input: "Read the canonical configuration." + contains: Project.Canonical + - name: linear algebra + input: "Express the vector in the canonical basis." + want: "" + - name: uppercase + input: "CANONICAL" + contains: Project.Canonical + - name: adverb + input: "Describe it canonically." + contains: Project.Canonical + - name: noun plural + input: "CANONICALS" + contains: Project.Canonical + - name: nearby software use + input: "Compare the canonical bases. Read the canonical configuration." + contains: Project.Canonical + - name: uppercase scientific phrase + input: "Use the CANONICAL BASIS." + want: "" diff --git a/.github/prose/styles/Project/Closure.yml b/.github/prose/styles/Project/Closure.yml new file mode 100644 index 000000000..dd2647fea --- /dev/null +++ b/.github/prose/styles/Project/Closure.yml @@ -0,0 +1,26 @@ +extends: existence +message: "Use '%s' only for the literal act of closing something." +level: error +ignorecase: true +tokens: + - closures? +exceptions: + # Closing a GUI window terminates its event loop. + - '(?i)(?:window|GUI|viewer|dialog)\s+closures?' + - '(?i)closures?\s+of\s+(?:the\s+)?(?:window|GUI|viewer|dialog)' +tests: + - name: generic use fails + input: "The review provides closure." + contains: Project.Closure + - name: closing a window + input: "Window closure stops the event loop." + want: "" + - name: uppercase plural + input: "CLOSURES" + contains: Project.Closure + - name: generic plural + input: "Review closures are recorded." + contains: Project.Closure + - name: nearby generic use + input: "Window closure stops the event loop. Review closure remains pending." + contains: Project.Closure diff --git a/.github/prose/styles/Project/ColonUsage.yml b/.github/prose/styles/Project/ColonUsage.yml new file mode 100644 index 000000000..604970b74 --- /dev/null +++ b/.github/prose/styles/Project/ColonUsage.yml @@ -0,0 +1,24 @@ +extends: ai-tells.ColonUsage +# Vale masks inline code with asterisks. A code label can introduce a standalone +# description, including DocStringExtensions field labels. Check later colons +# in the same description normally. +tokens: + - '(? summary)' +exceptions: + # Numerical ranges and established mathematical names retain their spelling. + - '\d+(?:\.\d+)?[–−]\d+(?:\.\d+)?\s*' + - '(?:Levenberg–Marquardt|Gauss–Kronrod|Gauss–Jacobi|Newton–Raphson|Cauchy–Riemann|Gauss–Legendre|Euler–Mascheroni|Clenshaw–Curtis)\s*' +tests: + - name: numerical range + input: "Read pages 12–15." + want: "" + - name: prose aside + input: "Read the file — then check it." + contains: Project.EmDashUsage diff --git a/.github/prose/styles/Project/EmphaticCallouts.yml b/.github/prose/styles/Project/EmphaticCallouts.yml new file mode 100644 index 000000000..04ab5dcaf --- /dev/null +++ b/.github/prose/styles/Project/EmphaticCallouts.yml @@ -0,0 +1,12 @@ +extends: ai-tells.Metacommentary +# Retain the parent's bold callout checks after limiting it to parsed prose. +scope: strong +tokens: + - '^(?:Key insight|Key takeaway|Key point|Important|Note|Bottom line|The point):?$' +tests: + - name: bold callout + input: "**Key insight:** Use this value." + contains: Project.EmphaticCallouts + - name: ordinary text + input: "Note the units." + want: "" diff --git a/.github/prose/styles/Project/EmphaticCopula.yml b/.github/prose/styles/Project/EmphaticCopula.yml new file mode 100644 index 000000000..0445efe15 --- /dev/null +++ b/.github/prose/styles/Project/EmphaticCopula.yml @@ -0,0 +1,12 @@ +extends: ai-tells.EmphaticCopula +# Vale 3.23 supports semantic inline scopes. Raw rules also inspect source code. +scope: emphasis +tokens: + - '^(?:is|are|was|were|the|and|not|or|actually|really|never|always|truly|literally)$' +tests: + - name: emphasized word + input: "The value *is* finite." + contains: Project.EmphaticCopula + - name: code identifier + input: "Call `_is_uq_product(request)`." + want: "" diff --git a/.github/prose/styles/Project/FigurativeNouns.yml b/.github/prose/styles/Project/FigurativeNouns.yml new file mode 100644 index 000000000..63c2b2084 --- /dev/null +++ b/.github/prose/styles/Project/FigurativeNouns.yml @@ -0,0 +1,13 @@ +extends: ai-tells.FigurativeNouns +exceptions: + # Physical material interfaces and the equations evaluated on them. + - '(?i)(?:the|its|each|an\s+explicit)\s+(?:conductor\s+)?surface[-\s]+(?:terms|impedances?|equations?|recipe|current|approximations|designation|and\s+transfer)' + - '(?i)(?:its\s+plane|a\s+wire|same\s+separating)\s+surface' + - '(?i)the\s+surface\s+z' +tests: + - name: physical interface + input: "Compute the surface impedance." + want: "" + - name: figurative interface + input: "This changes the prose surface." + contains: Project.FigurativeNouns diff --git a/.github/prose/styles/Project/FigurativeShape.yml b/.github/prose/styles/Project/FigurativeShape.yml new file mode 100644 index 000000000..f3eb81931 --- /dev/null +++ b/.github/prose/styles/Project/FigurativeShape.yml @@ -0,0 +1,18 @@ +extends: ai-tells.FigurativeShape +exceptions: + # Exact geometric outlines, as distinct from figurative descriptions. + - '(?i)(?:natural|copper|radial|exact\s+outer)\s+shape' + - '(?i)A\s+shape\s+is\s+independent\s+of\s+any\s+plotting\s+tessellation' + - '(?i)geometric\s+boundary\s+constrains\s+the\s+aggregate\s+shape' + # Cross-sectional outlines used by conductor geometry. + - '(?i)(?:sector|ellipse|polygon|rectangle|stadium)-shaped' +tests: + - name: geometric outline + input: "Use sector-shaped conductors." + want: "" + - name: metaphor + input: "Change the review shape." + contains: Project.FigurativeShape + - name: physical aggregate outline + input: "The geometric boundary constrains the aggregate shape." + want: "" diff --git a/.github/prose/styles/Project/FillerIntensifier.yml b/.github/prose/styles/Project/FillerIntensifier.yml new file mode 100644 index 000000000..764a0c337 --- /dev/null +++ b/.github/prose/styles/Project/FillerIntensifier.yml @@ -0,0 +1,11 @@ +extends: ai-tells.FillerIntensifier +exceptions: + # Singular means a noninvertible matrix in this numerical iteration. + - '(?i)A\s+singular\s+iteration' +tests: + - name: singular matrix + input: "A singular iteration fails." + want: "" + - name: filler + input: "A single call works." + contains: Project.FillerIntensifier diff --git a/.github/prose/styles/Project/FormalRegister.yml b/.github/prose/styles/Project/FormalRegister.yml new file mode 100644 index 000000000..4c3335fdb --- /dev/null +++ b/.github/prose/styles/Project/FormalRegister.yml @@ -0,0 +1,13 @@ +extends: ai-tells.FormalRegister +exceptions: + # These are literal programming and numerical operations in this package. + - '(?i)implement(?:s|ing|ation)?' + - '(?i)(?:optimi[sz]|maximi[sz]|minimi[sz])(?:e|es|ing)' + - '(?i)terminat(?:e|es|ed|ing)' +tests: + - name: numerical operation + input: "Minimize the objective." + want: "" + - name: inflated wording + input: "Utilize the result." + contains: Project.FormalRegister diff --git a/.github/prose/styles/Project/Metacommentary.yml b/.github/prose/styles/Project/Metacommentary.yml new file mode 100644 index 000000000..1c46fd73d --- /dev/null +++ b/.github/prose/styles/Project/Metacommentary.yml @@ -0,0 +1,9 @@ +extends: ai-tells.Metacommentary +scope: text +tests: + - name: filler explanation + input: "This matters because the file exists." + contains: Project.Metacommentary + - name: literal code + input: '`println("This matters because the file exists.")`' + want: "" diff --git a/.github/prose/styles/Project/MotionMetaphors.yml b/.github/prose/styles/Project/MotionMetaphors.yml new file mode 100644 index 000000000..8b963c505 --- /dev/null +++ b/.github/prose/styles/Project/MotionMetaphors.yml @@ -0,0 +1,15 @@ +extends: ai-tells.MotionMetaphors +exceptions+: + # A stranded conductor consists of multiple physical wires. + - '(?i)stranded[-\s]+(?:conductors?|cores?|cables?|wires?|bundles?|constructions?|assembl(?:y|ies)|geometry|layouts?|regions?|designs?|sectors?|aluminum|copper|coaxials?|metallic)' + - '(?i)(?:wire|rectangular|circular|source|tangent|twisted|segment|clipped|disk-bounded)\s+strands' + - '(?i)(?:wire|sector)\s+stranding' + - '(?i)circular,\s+tubular,\s+stranded,\s+and\s+noncircular\s+terminal\s+geometries' + - '(?i)solid,\s+tubular,\s+stranded,\s+screened,\s+and\s+armored\s+cable\s+models' +tests: + - name: physical wires + input: "Use stranded conductors." + want: "" + - name: metaphor + input: "The default is baked into the code." + contains: Project.MotionMetaphors diff --git a/.github/prose/styles/Project/NounString.yml b/.github/prose/styles/Project/NounString.yml new file mode 100644 index 000000000..75db6b676 --- /dev/null +++ b/.github/prose/styles/Project/NounString.yml @@ -0,0 +1,11 @@ +extends: ai-tells.NounString +exceptions: + # Exact French title of Gary's 1976 paper. + - 'Approche\s+complète\s+de\s+la\s+propagation\s+multifilaire\s+en\s+haute\s+fréquence\s+par\s+utilisation\s+des\s+matrices\s+complexes' +tests: + - name: published title + input: 'C. Gary, “Approche complète de la propagation multifilaire en haute fréquence par utilisation des matrices complexes.”' + want: "" + - name: noun stack + input: "The customer feedback analysis pipeline stalled." + contains: Project.NounString diff --git a/.github/prose/styles/Project/OverusedVocabulary.yml b/.github/prose/styles/Project/OverusedVocabulary.yml new file mode 100644 index 000000000..a6a72c58f --- /dev/null +++ b/.github/prose/styles/Project/OverusedVocabulary.yml @@ -0,0 +1,12 @@ +extends: ai-tells.OverusedVocabulary +exceptions: + # Numerical precision and Julia dispatch terminology. + - '(?i)significant\s+digits' + - '(?i)dynamically\s+typed' +tests: + - name: numerical precision + input: "Print three significant digits." + want: "" + - name: vague emphasis + input: "This is a significant improvement." + contains: Project.OverusedVocabulary diff --git a/.github/prose/styles/Project/RhetoricalSelfAnswer.yml b/.github/prose/styles/Project/RhetoricalSelfAnswer.yml new file mode 100644 index 000000000..591ff9a00 --- /dev/null +++ b/.github/prose/styles/Project/RhetoricalSelfAnswer.yml @@ -0,0 +1,9 @@ +extends: ai-tells.RhetoricalSelfAnswer +scope: text +tests: + - name: rhetorical question + input: "The result? A failure." + contains: Project.RhetoricalSelfAnswer + - name: literal code + input: '`println("The result? A failure.")`' + want: "" diff --git a/.github/prose/styles/Project/Semicolons.yml b/.github/prose/styles/Project/Semicolons.yml new file mode 100644 index 000000000..34c0b10ad --- /dev/null +++ b/.github/prose/styles/Project/Semicolons.yml @@ -0,0 +1,13 @@ +extends: existence +message: "Split the statements at '%s' into separate sentences." +level: error +nonword: true +tokens: + - ';' +tests: + - name: prose punctuation + input: "Read the file; check its contents." + contains: Project.Semicolons + - name: code punctuation + input: "Call `f(x; y = 1)`." + want: "" diff --git a/.github/prose/styles/config/views/Comments.yml b/.github/prose/styles/config/views/Comments.yml new file mode 100644 index 000000000..e0b8efe65 --- /dev/null +++ b/.github/prose/styles/config/views/Comments.yml @@ -0,0 +1,4 @@ +engine: tree-sitter +scopes: + - expr: (comment)+ @comment + type: md diff --git a/.github/prose/styles/config/views/Descriptions.yml b/.github/prose/styles/config/views/Descriptions.yml new file mode 100644 index 000000000..ec2541d03 --- /dev/null +++ b/.github/prose/styles/config/views/Descriptions.yml @@ -0,0 +1,6 @@ +engine: dasel +scopes: + - expr: search($key == "description") + type: md + - expr: search($key == "title") + type: md diff --git a/.github/prose/styles/config/views/GetDP.yml b/.github/prose/styles/config/views/GetDP.yml new file mode 100644 index 000000000..7932d8a5f --- /dev/null +++ b/.github/prose/styles/config/views/GetDP.yml @@ -0,0 +1,9 @@ +engine: textfsm +template: | + Value List Comment (.+) + + Start + ^\s*//\s?${Comment} +scopes: + - expr: Comment + type: md diff --git a/.github/prose/styles/config/views/HashComments.yml b/.github/prose/styles/config/views/HashComments.yml new file mode 100644 index 000000000..2891b2881 --- /dev/null +++ b/.github/prose/styles/config/views/HashComments.yml @@ -0,0 +1,9 @@ +engine: textfsm +template: | + Value List Comment ([^!].*) + + Start + ^\s*#\s?${Comment} +scopes: + - expr: Comment + type: md diff --git a/.github/prose/styles/config/views/Julia.yml b/.github/prose/styles/config/views/Julia.yml new file mode 100644 index 000000000..7321be7cf --- /dev/null +++ b/.github/prose/styles/config/views/Julia.yml @@ -0,0 +1,73 @@ +engine: tree-sitter +scopes: + # Builder directives contain no prose. + - expr: | + ((line_comment)+ @comment + (#not-match? @comment "^#(?:hide|src|md|nb) *$")) + type: md + - expr: (block_comment) @comment + type: md + - expr: | + ([ + (ERROR (string_literal) @docstring . [(abstract_definition) (assignment) (function_definition) (identifier) (macro_definition) (macrocall_expression) (module_definition) (primitive_definition) (struct_definition) (typed_expression)]) + (macro_argument_list (string_literal) @docstring . [(function_definition) (struct_definition)]) + (source_file (string_literal) @docstring . [(abstract_definition) (assignment) (call_expression) (const_statement) (function_definition) (identifier) (macro_definition) (macrocall_expression) (module_definition) (primitive_definition) (struct_definition) (typed_expression)]) + (module_definition (string_literal) @docstring . [(abstract_definition) (assignment) (call_expression) (const_statement) (function_definition) (identifier) (macro_definition) (macrocall_expression) (module_definition) (primitive_definition) (struct_definition) (typed_expression)]) + (function_definition (string_literal) @docstring . [(abstract_definition) (assignment) (call_expression) (const_statement) (function_definition) (identifier) (macro_definition) (macrocall_expression) (module_definition) (primitive_definition) (struct_definition) (typed_expression)]) + (macro_definition (string_literal) @docstring . [(abstract_definition) (assignment) (call_expression) (const_statement) (function_definition) (identifier) (macro_definition) (macrocall_expression) (module_definition) (primitive_definition) (struct_definition) (typed_expression)]) + (struct_definition (string_literal) @docstring . [(abstract_definition) (assignment) (call_expression) (const_statement) (function_definition) (identifier) (macro_definition) (macrocall_expression) (module_definition) (primitive_definition) (struct_definition) (typed_expression)]) + (compound_statement (string_literal) @docstring . [(abstract_definition) (assignment) (call_expression) (const_statement) (function_definition) (identifier) (macro_definition) (macrocall_expression) (module_definition) (primitive_definition) (struct_definition) (typed_expression)]) + (let_statement (string_literal) @docstring . [(abstract_definition) (assignment) (call_expression) (const_statement) (function_definition) (identifier) (macro_definition) (macrocall_expression) (module_definition) (primitive_definition) (struct_definition) (typed_expression)]) + (quote_statement (string_literal) @docstring . [(abstract_definition) (assignment) (call_expression) (const_statement) (function_definition) (identifier) (macro_definition) (macrocall_expression) (module_definition) (primitive_definition) (struct_definition) (typed_expression)]) + ] + (#not-match? @docstring "^\"\"\"\n\\$\\((TYPEDSIGNATURES|SIGNATURES|TYPEDEF|TYPEDFIELDS|FIELDS|IMPORTS|EXPORTS)\\)")) + type: md + - expr: | + ([ + (source_file (prefixed_string_literal prefix: (identifier) @_prefix) @docstring . [(abstract_definition) (assignment) (call_expression) (const_statement) (function_definition) (identifier) (macro_definition) (macrocall_expression) (module_definition) (primitive_definition) (struct_definition) (typed_expression)]) + (module_definition (prefixed_string_literal prefix: (identifier) @_prefix) @docstring . [(abstract_definition) (assignment) (call_expression) (const_statement) (function_definition) (identifier) (macro_definition) (macrocall_expression) (module_definition) (primitive_definition) (struct_definition) (typed_expression)]) + (function_definition (prefixed_string_literal prefix: (identifier) @_prefix) @docstring . [(abstract_definition) (assignment) (call_expression) (const_statement) (function_definition) (identifier) (macro_definition) (macrocall_expression) (module_definition) (primitive_definition) (struct_definition) (typed_expression)]) + (macro_definition (prefixed_string_literal prefix: (identifier) @_prefix) @docstring . [(abstract_definition) (assignment) (call_expression) (const_statement) (function_definition) (identifier) (macro_definition) (macrocall_expression) (module_definition) (primitive_definition) (struct_definition) (typed_expression)]) + (struct_definition (prefixed_string_literal prefix: (identifier) @_prefix) @docstring . [(abstract_definition) (assignment) (call_expression) (const_statement) (function_definition) (identifier) (macro_definition) (macrocall_expression) (module_definition) (primitive_definition) (struct_definition) (typed_expression)]) + (compound_statement (prefixed_string_literal prefix: (identifier) @_prefix) @docstring . [(abstract_definition) (assignment) (call_expression) (const_statement) (function_definition) (identifier) (macro_definition) (macrocall_expression) (module_definition) (primitive_definition) (struct_definition) (typed_expression)]) + (let_statement (prefixed_string_literal prefix: (identifier) @_prefix) @docstring . [(abstract_definition) (assignment) (call_expression) (const_statement) (function_definition) (identifier) (macro_definition) (macrocall_expression) (module_definition) (primitive_definition) (struct_definition) (typed_expression)]) + (quote_statement (prefixed_string_literal prefix: (identifier) @_prefix) @docstring . [(abstract_definition) (assignment) (call_expression) (const_statement) (function_definition) (identifier) (macro_definition) (macrocall_expression) (module_definition) (primitive_definition) (struct_definition) (typed_expression)]) + ] + (#eq? @_prefix "raw")) + type: md + - expr: | + ((macrocall_expression (macro_identifier) @_macro + (macro_argument_list (string_literal) @docstring)) + (#eq? @_macro "@doc") + (#not-match? @docstring "^\"\"\"\n\\$\\((TYPEDSIGNATURES|SIGNATURES|TYPEDEF|TYPEDFIELDS|FIELDS|IMPORTS|EXPORTS)\\)")) + type: md + # The bundled grammar attaches a docstring after an @inline definition to + # that macro's arguments. The following declaration still identifies it. + - expr: | + ((_ (macrocall_expression + (macro_argument_list + [(function_definition) (assignment)] . (string_literal) @docstring .)) + . [(function_definition) (assignment) (struct_definition) (macrocall_expression)]) + (#not-match? @docstring "^\"\"\"\n\\$\\((TYPEDSIGNATURES|SIGNATURES|TYPEDEF|TYPEDFIELDS|FIELDS|IMPORTS|EXPORTS)\\)")) + type: md + # DocStringExtensions marks documentation even when newer Julia syntax + # produces an error node in Vale's bundled grammar. + - expr: | + ((string_literal) @docstring + (#match? @docstring "^\"\"\"\n\\$\\((TYPEDSIGNATURES|SIGNATURES|TYPEDEF|TYPEDFIELDS|FIELDS|IMPORTS|EXPORTS)\\)")) + type: md + - expr: | + ((_ (macrocall_expression + (macro_argument_list + [(function_definition) (assignment)] . + (prefixed_string_literal prefix: (identifier) @_prefix) @docstring .)) + . [(function_definition) (assignment) (struct_definition) (macrocall_expression)]) + (#eq? @_prefix "raw")) + type: md + - expr: | + ((macrocall_expression (macro_identifier) @_macro + (macro_argument_list + (prefixed_string_literal prefix: (identifier) @_prefix) @docstring)) + (#eq? @_macro "@doc") + (#eq? @_prefix "raw")) + type: md diff --git a/.github/prose/styles/config/views/Workflow.yml b/.github/prose/styles/config/views/Workflow.yml new file mode 100644 index 000000000..5c03e091e --- /dev/null +++ b/.github/prose/styles/config/views/Workflow.yml @@ -0,0 +1,8 @@ +engine: dasel +scopes: + - expr: search($key == "name") + type: md + - expr: search($key == "description") + type: md + - expr: search($key == "title") + type: md diff --git a/.github/prose/vale.ini b/.github/prose/vale.ini new file mode 100644 index 000000000..7c30e6420 --- /dev/null +++ b/.github/prose/vale.ini @@ -0,0 +1,56 @@ +StylesPath = styles +MinAlertLevel = suggestion +Packages = https://github.com/tbhb/vale-ai-tells/releases/download/v1.37.0/ai-tells.zip, https://github.com/Syntaf/vale-llm-slop/releases/download/v0.1.0/STE.zip + +IgnoredScopes = code, tt +SkippedScopes = script, style, pre, svg +IgnoredClasses = math-container, docs-sidebar, docs-navbar, docs-footer, docs-right, docstring-binding, modal + +[*] +TokenIgnores = (? ".fem.cov"))) + end + end + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + if: ${{ !cancelled() && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }} + with: + name: fem-production-coverage + path: | + src/**/*.fem.cov + ext/**/*.fem.cov + if-no-files-found: error + - name: Remove coverage traces after workers and uploads finish + if: always() + run: julia --startup-file=no test/coverage.jl traces + + quality: + name: Quality tests + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + persist-credentials: false + - uses: julia-actions/setup-julia@fa02766e078afaaf09b14210362cee14137e6a32 # v3.0.2 + with: + # The measured ceilings in `test/quality/preservation.toml` hold for this version. + version: "1.12.7" + - uses: julia-actions/cache@e32267b41e0fa7df82a3d53e8c7273620d52c586 # v3.4.0 + with: + delete-old-caches: "false" + - name: Install Fatou + uses: jolars/fatou-action@6b6a66a1f28fce0d1ff31048c609e63b7006136e + with: + version: v0.22.0 + format: "false" + lint: "false" + verify-checksum: "true" + - name: Reject added or raised architecture baseline entries + shell: bash + env: + BASELINE_REVISION: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }} + run: | + set -euo pipefail + if [ -z "$BASELINE_REVISION" ] || [ "$BASELINE_REVISION" = "0000000000000000000000000000000000000000" ]; then + echo "No earlier revision to compare with the architecture baseline." + exit 0 + fi + git fetch --no-tags --depth=1 origin "$BASELINE_REVISION" + julia --startup-file=no --color=yes test/tools/baseline_ratchet.jl "$BASELINE_REVISION" + - name: Enforce package and architecture tests + shell: bash + run: | + set -o pipefail + # Instantiates the committed Manifest.toml: the measured ceilings hold for it. + julia --project=test --color=yes -e 'using Pkg; Pkg.instantiate()' + julia --project=test --color=yes -e ' + push!(ARGS, "tag:quality") + include("test/runtests.jl") + ' 2>&1 | tee "$RUNNER_TEMP/quality-tests.log" + - name: Retain complete quality results + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: quality-test-report + path: ${{ runner.temp }}/quality-tests.log + if-no-files-found: ignore + + coverage: + name: Merged production coverage + needs: [fem-numerical] + runs-on: ubuntu-latest + timeout-minutes: 180 + env: + LINECABLEMODELS_TEST_PLOTTING: "true" steps: - - uses: actions/checkout@v7 - - uses: julia-actions/setup-julia@v3 - with: - version: ${{ matrix.version }} - arch: ${{ matrix.arch }} - - uses: julia-actions/cache@v3 - with: - # Prerelease Julia builds change often, making compiled caches poor value. - cache-compiled: ${{ matrix.version != 'pre' }} - # Do not publish incomplete depots from failed test runs. - save-always: 'false' - - uses: julia-actions/julia-buildpkg@v1 - - uses: julia-actions/julia-runtest@v1 - - uses: julia-actions/julia-processcoverage@v1 - if: matrix.version == '1.12' - - uses: codecov/codecov-action@v7 - if: matrix.version == '1.12' + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + persist-credentials: false + - name: Resolve dependencies afresh, without the committed Manifest + run: rm -f Manifest.toml + - uses: julia-actions/setup-julia@fa02766e078afaaf09b14210362cee14137e6a32 # v3.0.2 + with: + version: "1.12" + - uses: julia-actions/cache@e32267b41e0fa7df82a3d53e8c7273620d52c586 # v3.4.0 + with: + delete-old-caches: "false" + - name: Instantiate coverage tools + run: julia --project=test/coverage --color=yes -e 'using Pkg; Pkg.instantiate()' + - name: Remove stale coverage traces + run: julia --project=test/coverage --color=yes test/coverage.jl clean + - name: Collect core and non-graphical extension coverage + # Drafts already run this suite in the ordinary Julia 1.12 job. + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} + run: julia --project=. --color=yes -e 'using Pkg; Pkg.test(coverage = true)' + - name: Collect core-only loading coverage + if: ${{ !cancelled() }} + run: | + julia --project=test/core --code-coverage=@. --color=yes -e 'using Pkg; Pkg.instantiate()' + julia --project=test/core --code-coverage=@. --color=yes -e ' + push!(ARGS, "tag:core_only") + include("test/runtests.jl") + ' + - name: Collect Cairo and visual coverage + if: ${{ !cancelled() }} + run: | + julia --project=test/visual --compiled-modules=no --code-coverage=@. --color=yes -e 'using Pkg; Pkg.instantiate()' + julia --project=test/visual --compiled-modules=no --code-coverage=@. --color=yes -e ' + push!(ARGS, "tag:visual") + include("test/runtests.jl") + ' + - name: Collect Cairo activation coverage + if: ${{ !cancelled() }} + run: julia --project=test/visual --code-coverage=@. test/runtests.jl "loaded extension activation" + - name: Collect GLMakie loading coverage + if: ${{ !cancelled() }} + shell: bash + run: | + sudo apt-get update + sudo apt-get install --yes xvfb xauth libgl1-mesa-dri + xvfb-run -a julia --startup-file=no --code-coverage=@. --color=yes -e ' + using Pkg + Pkg.activate(; temp = true) + Pkg.develop(PackageSpec(path = pwd())) + Pkg.add(PackageSpec(name = "GLMakie", version = "0.13")) + Pkg.add(PackageSpec(name = "TestItemRunner", version = "1")) + push!(ARGS, "loaded extension activation") + include(joinpath(pwd(), "test", "runtests.jl")) + ' + - name: Collect native FEM UI lifecycle coverage + if: ${{ !cancelled() }} + run: | + julia --project=test --color=yes -e 'using Pkg; Pkg.instantiate()' + xvfb-run -a julia --project=test --code-coverage=@. test/runtests.jl extensions/fem_ui.jl + - name: Collect WGLMakie loading coverage + if: ${{ !cancelled() }} + run: | + julia --project=. --code-coverage=@. --color=yes -e ' + using Pkg + Pkg.activate(; temp = true) + Pkg.develop(PackageSpec(path = pwd())) + Pkg.add(PackageSpec(name = "WGLMakie", version = "0.13")) + Pkg.add(PackageSpec(name = "TestItemRunner", version = "1")) + push!(ARGS, "loaded extension activation") + include(joinpath(pwd(), "test", "runtests.jl")) + ' + - name: Collect remote PSCAD runner coverage + if: ${{ !cancelled() }} + run: | + julia --project=src/pscad/remote --code-coverage=@. --color=yes -e 'using Pkg; Pkg.instantiate()' + julia --project=src/pscad/remote --code-coverage=@. --color=yes test/integration/pscad/remote_runner_protocol.jl + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + if: ${{ !cancelled() && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }} + with: + name: fem-production-coverage + path: . + - name: Enforce source-amended 95% coverage + if: ${{ !cancelled() && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }} + run: julia --project=test/coverage --color=yes test/coverage.jl check + - name: Retain the coverage report, including failed checks + if: ${{ always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && hashFiles('lcov.info') != '' }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: production-coverage-report + path: lcov.info + if-no-files-found: error + - uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7 + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} with: files: lcov.info token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: false + fail_ci_if_error: true + + - name: Remove coverage traces after workers and uploads finish + if: always() + run: julia --startup-file=no test/coverage.jl traces + + aqua: + name: Aqua + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + persist-credentials: false + - name: Resolve dependencies afresh, without the committed Manifest + run: rm -f Manifest.toml + - uses: julia-actions/setup-julia@fa02766e078afaaf09b14210362cee14137e6a32 # v3.0.2 + with: + version: "1.12" + - uses: julia-actions/cache@e32267b41e0fa7df82a3d53e8c7273620d52c586 # v3.4.0 + with: + delete-old-caches: "false" + - name: Run Aqua independently + shell: julia --color=yes {0} + run: | + using Pkg + Pkg.activate(; temp = true) + Pkg.develop(PackageSpec(path = pwd())) + Pkg.add(["Aqua", "TestItemRunner"]) + push!(ARGS, "tag:aqua") + include(joinpath(pwd(), "test", "runtests.jl")) + + clean-install: + name: Clean installation + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + persist-credentials: false + - name: Resolve dependencies afresh, without the committed Manifest + run: rm -f Manifest.toml + - uses: julia-actions/setup-julia@fa02766e078afaaf09b14210362cee14137e6a32 # v3.0.2 + with: + version: "1.12" + - name: Install core into an empty depot + shell: bash + run: | + set -euo pipefail + export JULIA_DEPOT_PATH + JULIA_DEPOT_PATH="$(mktemp -d)" + julia --startup-file=no --color=yes -e ' + using Pkg + using TOML + package_dependencies = TOML.parsefile(joinpath(pwd(), "Project.toml"))["deps"] + @assert !haskey(package_dependencies, "Pluto") + @assert !haskey(package_dependencies, "PlutoUI") + @assert !haskey(package_dependencies, "CairoMakie") + Pkg.activate(; temp = true) + Pkg.develop(PackageSpec(path = pwd())) + using LineCableModels + @assert Base.get_extension(LineCableModels, :LineCableModelsMakieExt) === nothing + @assert !any(info -> info.name == "CairoMakie", values(Pkg.dependencies())) + ' + docs: - if: ${{ !contains(github.event.head_commit.message, 'skip ci') }} name: Documentation runs-on: ubuntu-latest + timeout-minutes: 45 permissions: - actions: write # needed to allow julia-actions/cache to proactively delete old caches that it has created contents: write statuses: write steps: - - uses: actions/checkout@v7 - - uses: julia-actions/setup-julia@v3 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: - version: '1.12' - - uses: julia-actions/cache@v3 + persist-credentials: false + - name: Resolve dependencies afresh, without the committed Manifest + run: rm -f Manifest.toml + - uses: julia-actions/setup-julia@fa02766e078afaaf09b14210362cee14137e6a32 # v3.0.2 with: - # Documentation is rebuilt once per run, so compiled caches consume - # substantial storage without enough reuse to justify their size. - cache-compiled: 'false' - save-always: 'false' - - name: Configure doc environment - shell: julia --project=docs --color=yes {0} - run: | - using Pkg - Pkg.develop(PackageSpec(path=pwd())) - Pkg.instantiate() - - uses: julia-actions/julia-buildpkg@v1 + version: "1.12" + - uses: julia-actions/cache@e32267b41e0fa7df82a3d53e8c7273620d52c586 # v3.4.0 + with: + cache-compiled: "false" + save-always: "false" + delete-old-caches: "false" + - name: Instantiate documentation + run: julia --project=docs --color=yes docs/instantiate.jl + - name: Generate package-server usage map + run: julia --project=docs --color=yes docs/package_stats.jl + - name: Upload package-server usage map + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: package-server-usage-map + path: docs/src/assets/user-statistics.svg - name: Run doctests - shell: julia --project=docs --color=yes {0} + run: julia --project=docs --color=yes docs/doctest.jl + - name: Build documentation + id: docs_build + run: julia --project=docs --color=yes docs/make.jl + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 + with: + node-version: "24" + package-manager-cache: false + - name: Install writing tools + id: prose_tools + shell: bash + run: | + set -euo pipefail + tools="$RUNNER_TEMP/prose-tools" + mkdir -p "$tools" "$RUNNER_TEMP/prose-reports" + release=https://github.com/vale-cli/vale/releases/download/v3.23.0 + curl --fail --location --retry 3 "$release/vale_3.23.0_Linux_64-bit.tar.gz" -o "$tools/vale.tar.gz" + curl --fail --location --retry 3 "$release/vale_3.23.0_checksums.txt" -o "$tools/checksums.txt" + expected=$(awk '$2 == "vale_3.23.0_Linux_64-bit.tar.gz" {print $1}' "$tools/checksums.txt") + test -n "$expected" + echo "$expected $tools/vale.tar.gz" | sha256sum --check + tar -xzf "$tools/vale.tar.gz" -C "$tools" + cp .github/prose/package{,-lock}.json "$tools/" + npm ci --prefix "$tools" + echo "$tools" >> "$GITHUB_PATH" + echo "$tools/node_modules/.bin" >> "$GITHUB_PATH" + "$tools/vale" sync --no-global --config=.github/prose/vale.ini + - name: Select all maintained writing + id: prose_inputs + run: bash .github/prose/files.sh "$RUNNER_TEMP/prose-reports" + - name: Verify writing rules and exclusions + shell: bash run: | - using Documenter: DocMeta, doctest - using LineCableModels - DocMeta.setdocmeta!(LineCableModels, :DocTestSetup, :(using LineCableModels); recursive=true) - doctest(LineCableModels) - - uses: julia-actions/julia-docdeploy@v1 + set -euo pipefail + vale test --no-global --config=.github/prose/vale.ini --coverage .github/prose/styles/Project .github/prose/fixtures | tee "$RUNNER_TEMP/prose-reports/rules.log" + bash .github/prose/fixtures/check.sh | tee "$RUNNER_TEMP/prose-reports/checks.log" + - name: Check prose with Vale + if: ${{ !cancelled() && steps.prose_tools.outcome == 'success' && steps.prose_inputs.outcome == 'success' }} + shell: bash + run: | + set -euo pipefail + mapfile -t files < "$RUNNER_TEMP/prose-reports/vale-files.txt" + vale --no-global --config=.github/prose/vale.ini --no-color "${files[@]}" 2>&1 | tee "$RUNNER_TEMP/prose-reports/vale.log" + - name: Check spelling with cspell + if: ${{ !cancelled() && steps.prose_tools.outcome == 'success' && steps.prose_inputs.outcome == 'success' }} + shell: bash + run: | + set -euo pipefail + mapfile -t files < "$RUNNER_TEMP/prose-reports/cspell-files.txt" + cspell lint --config=.github/prose/cspell.json --no-progress --no-cache "${files[@]}" 2>&1 | tee "$RUNNER_TEMP/prose-reports/cspell.log" + - name: Retain writing reports + if: ${{ !cancelled() && steps.docs_build.outcome == 'success' }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: writing-reports + path: ${{ runner.temp }}/prose-reports + if-no-files-found: error + - name: Deploy documentation + if: ${{ success() && github.event_name == 'push' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} + run: julia --project=docs --color=yes docs/deploy.jl diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml index 0cd3114ec..92a7c41db 100644 --- a/.github/workflows/TagBot.yml +++ b/.github/workflows/TagBot.yml @@ -1,31 +1,23 @@ name: TagBot + on: issue_comment: - types: - - created + types: [created] workflow_dispatch: inputs: lookback: default: "3" + permissions: - actions: read - checks: read contents: write - deployments: read issues: read - discussions: read - packages: read - pages: read - pull-requests: read - repository-projects: read - security-events: read - statuses: read + jobs: TagBot: if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' runs-on: ubuntu-latest steps: - - uses: JuliaRegistries/TagBot@v1 + - uses: JuliaRegistries/TagBot@cded32665b34ca0496d3ac10a67d08ddfc726963 # v1.25.11 with: token: ${{ secrets.GITHUB_TOKEN }} ssh: ${{ secrets.DOCUMENTER_KEY }} diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml deleted file mode 100644 index d1ae45999..000000000 --- a/.github/workflows/create-release.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Create Release from Tag - -on: - push: - tags: - - 'v*' - -jobs: - create-release: - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Checkout code - uses: actions/checkout@v7 - - - name: Extract Changelog Section - id: changelog - env: - TAG_NAME: ${{ github.ref_name }} - run: | - awk -v tag="$TAG_NAME" ' - BEGIN { heading = "## [" tag "]" } - index($0, heading) == 1 { found = 1; print; next } - found && /^## \[/ { exit } - found && /^\[[^]]+\]:/ { exit } - found { print } - ' CHANGELOG.md > release_notes.md - test -s release_notes.md - cat release_notes.md - - - name: Create GitHub Release - uses: softprops/action-gh-release@v3 - with: - tag_name: ${{ github.ref_name }} - name: ${{ github.ref_name }} - body_path: release_notes.md - draft: false - prerelease: false diff --git a/.github/workflows/publish-oss-repository.yml b/.github/workflows/publish-oss-repository.yml new file mode 100644 index 000000000..dd7e56d0c --- /dev/null +++ b/.github/workflows/publish-oss-repository.yml @@ -0,0 +1,136 @@ +name: Publish OSS repository + +on: + workflow_dispatch: + inputs: + version_tag: + description: "Release tag to publish. Must use semantic version format vX.Y.Z." + required: true + type: string + institutional_email: + description: "Requester institutional email." + required: true + type: string + +permissions: + contents: read + issues: write + +jobs: + create-publish-checklist: + name: Create OSS publish checklist + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + persist-credentials: false + + - name: Validate workflow inputs + shell: bash + env: + VERSION_TAG: ${{ inputs.version_tag }} + INSTITUTIONAL_EMAIL: ${{ inputs.institutional_email }} + run: | + set -euo pipefail + + if [[ ! "${VERSION_TAG}" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then + echo "Invalid version tag: ${VERSION_TAG}" + echo "Expected semantic version format: vX.Y.Z" + exit 1 + fi + + if [[ ! "${INSTITUTIONAL_EMAIL}" =~ ^[^@[:space:]]+@kuleuven\.be$ ]]; then + echo "Invalid institutional email: ${INSTITUTIONAL_EMAIL}" + echo "Expected address ending in @kuleuven.be" + exit 1 + fi + + - name: Ensure workflow labels exist + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + gh label create "oss-publish" \ + --description "OSS publish checklist" \ + --color "5319e7" \ + --force + + gh label create "oss-publish-pending" \ + --description "OSS publish pending checklist completion and release tag" \ + --color "fbca04" \ + --force + + gh label create "oss-publish-invalid" \ + --description "OSS publish checklist is incomplete or invalid" \ + --color "d73a4a" \ + --force + + gh label create "oss-publish-published" \ + --description "OSS publish completed by controller" \ + --color "0e8a16" \ + --force + + - name: Render checklist issue body + shell: bash + env: + VERSION_TAG: ${{ inputs.version_tag }} + INSTITUTIONAL_EMAIL: ${{ inputs.institutional_email }} + run: | + set -euo pipefail + + TEMPLATE=".github/OSS_PUBLISH_CHECKLIST.md" + + if [[ ! -f "${TEMPLATE}" ]]; then + echo "Missing required template: ${TEMPLATE}" + exit 1 + fi + + REPOSITORY_NAME="${GITHUB_REPOSITORY#*/}" + + python - <<'PY' + from pathlib import Path + import os + + template = Path(".github/OSS_PUBLISH_CHECKLIST.md").read_text(encoding="utf-8") + rendered = ( + template + .replace("{{ repository_name }}", os.environ["GITHUB_REPOSITORY"].split("/", 1)[1]) + .replace("{{ version_tag }}", os.environ["VERSION_TAG"]) + .replace("{{ institutional_email }}", os.environ["INSTITUTIONAL_EMAIL"]) + ) + Path("oss_publish_checklist.md").write_text(rendered, encoding="utf-8") + PY + + - name: Create or update checklist issue + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION_TAG: ${{ inputs.version_tag }} + run: | + set -euo pipefail + + EXISTING_ISSUE="$( + gh issue list \ + --state open \ + --label "oss-publish" \ + --search "OSS publish checklist: ${VERSION_TAG} in:title" \ + --json number \ + --jq '.[0].number // empty' + )" + + if [[ -n "${EXISTING_ISSUE}" ]]; then + gh issue comment "${EXISTING_ISSUE}" \ + --body "OSS publish checklist workflow was run again by @${GITHUB_ACTOR} for ${VERSION_TAG}." + echo "Existing publish issue found: #${EXISTING_ISSUE}" + exit 0 + fi + + gh issue create \ + --title "OSS publish checklist: ${VERSION_TAG}" \ + --body-file oss_publish_checklist.md \ + --label "oss-publish" \ + --label "oss-publish-pending" diff --git a/.github/workflows/secret-audit.yml b/.github/workflows/secret-audit.yml new file mode 100644 index 000000000..debbd6fb8 --- /dev/null +++ b/.github/workflows/secret-audit.yml @@ -0,0 +1,40 @@ +name: Secret audit + +on: + schedule: + - cron: "17 3 1 * *" + workflow_dispatch: + +permissions: + contents: read + +jobs: + full-history: + name: Full-history Gitleaks + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + fetch-depth: 0 + persist-credentials: false + - name: Install Gitleaks 8.30.1 + shell: bash + run: | + set -euo pipefail + archive="$RUNNER_TEMP/gitleaks.tar.gz" + curl --fail --location --retry 3 https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_x64.tar.gz -o "$archive" + echo "551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb $archive" | sha256sum --check + tar -xzf "$archive" -C "$RUNNER_TEMP" gitleaks + - name: Scan complete fetched history + shell: bash + run: | + "$RUNNER_TEMP/gitleaks" git --log-opts=--all --gitleaks-ignore-path=.gitleaksignore --redact --no-banner --no-color --report-format=json --report-path="$RUNNER_TEMP/gitleaks-history.json" . + - name: Retain redacted audit results + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: gitleaks-history + path: ${{ runner.temp }}/gitleaks-history.json + retention-days: 7 + if-no-files-found: ignore diff --git a/.github/workflows/timing.yml b/.github/workflows/timing.yml new file mode 100644 index 000000000..5df878290 --- /dev/null +++ b/.github/workflows/timing.yml @@ -0,0 +1,73 @@ +name: Timing + +# The timing comparison of `test/tools/performance.jl`. Each push to a pull request compares +# the new head with the previous one. A manual run compares with its `base` input and runs +# only this workflow. +on: + pull_request: + types: [synchronize] + workflow_dispatch: + inputs: + base: + description: "Commit that the timing comparison compares with" + required: true + type: string + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + JULIA_NUM_THREADS: "1" + OPENBLAS_NUM_THREADS: "1" + OMP_NUM_THREADS: "1" + +jobs: + timing: + name: Timing comparison + if: >- + github.event_name == 'workflow_dispatch' || + (github.event.before != '' && + github.event.before != '0000000000000000000000000000000000000000') + runs-on: ubuntu-latest + timeout-minutes: 90 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + persist-credentials: false + # The pull request head, not the merge commit, because the base is its previous head. + ref: ${{ github.event.pull_request.head.sha || github.sha }} + # A manual run may give its base as an abbreviated commit, which needs the whole history. + fetch-depth: 0 + - uses: julia-actions/setup-julia@fa02766e078afaaf09b14210362cee14137e6a32 # v3.0.2 + with: + # The version of the quality job. Both sides start from the committed Manifest.toml. + version: "1.12.7" + - uses: julia-actions/cache@e32267b41e0fa7df82a3d53e8c7273620d52c586 # v3.4.0 + with: + delete-old-caches: "false" + - name: Compare the timings of the checkout with the base revision + shell: bash + env: + TIMING_BASE: ${{ inputs.base || github.event.before }} + run: | + set -euo pipefail + if ! base=$(git rev-parse --verify --quiet "$TIMING_BASE^{commit}"); then + git fetch --no-tags origin "$TIMING_BASE" + base=$(git rev-parse --verify "$TIMING_BASE^{commit}") + fi + julia --project=test --color=yes -e 'using Pkg; Pkg.instantiate()' + status=0 + julia --project=test --color=yes test/tools/performance.jl "$base" 2>&1 | + tee "$RUNNER_TEMP/timing.log" || status=$? + # 1 is a slowdown or a failing scenario. 2 is a possible slowdown or an unstable + # scenario: no verdict, and not a regression. + if [ "$status" -eq 2 ]; then + scenarios=$(sed -n 's/^Possible slowdown or unstable, no verdict: //p' "$RUNNER_TEMP/timing.log") + echo "::warning title=Timing comparison::No verdict against ${base:0:8} for: ${scenarios:-see the log}" + elif [ "$status" -ne 0 ]; then + exit "$status" + fi diff --git a/.github/workflows/todo-to-issue.yml b/.github/workflows/todo-to-issue.yml deleted file mode 100644 index e2cbe0005..000000000 --- a/.github/workflows/todo-to-issue.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: "Run TODO to Issue" -on: - push: - branches: - - main - workflow_dispatch: - inputs: - MANUAL_COMMIT_REF: - description: "The SHA of the commit to get the diff for" - required: true - MANUAL_BASE_REF: - description: "By default, the commit entered above is compared to the one directly before it; to go back further, enter an earlier SHA here" - required: false -concurrency: - group: todo-to-issue-main - cancel-in-progress: false -jobs: - build: - runs-on: "ubuntu-latest" - permissions: - contents: write - issues: write - pull-requests: write - steps: - - uses: "actions/checkout@v7" - with: - # URL insertion commits back to main, regardless of the event source. - ref: main - - name: "TODO to Issue" - uses: "alstr/todo-to-issue-action@v5" - env: - MANUAL_COMMIT_REF: ${{ inputs.MANUAL_COMMIT_REF }} - MANUAL_BASE_REF: ${{ inputs.MANUAL_BASE_REF }} - with: - INSERT_ISSUE_URLS: "true" - CLOSE_ISSUES: "true" - - name: Set Git user - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - name: Commit and Push Changes - run: | - # Only stage tracked files modified by URL insertion. - git add --update - if ! git diff --cached --quiet; then - git commit -m "Automatically added GitHub issue links to TODOs" - git push origin HEAD:main - else - echo "No changes to commit" - fi diff --git a/.gitignore b/.gitignore index 662012494..3c985069b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ -# Files generated by invoking Julia with --code-coverage +# Files generated by invoking Julia with `--code-coverage` *.jl.cov *.jl.*.cov +lcov.info -# Files generated by invoking Julia with --track-allocation +# Files generated by invoking Julia with `--track-allocation` *.jl.mem # System-specific files and directories generated by the BinaryProvider and BinDeps packages @@ -19,21 +20,32 @@ docs/site/ docs/src/tutorials/ # File generated by Pkg, the package manager, based on a corresponding Project.toml -# It records a fixed state of all packages used by the project. As such, it should not be +# It records a fixed state of all packages used by the project. It should not be # committed for packages, but should be committed for applications that require a static # environment. Manifest.toml */Manifest.toml +# The workspace Manifest is committed: the CI quality job measures the allocation and JET +# counts of `test/quality/preservation.toml` with exactly these versions. Every other CI +# job deletes it and resolves afresh. +!/Manifest.toml +!showcase/bonito/Manifest.toml +!src/pscad/remote/Manifest.toml # the hidden folder of shame temp/ @INPROGRESS/ +local/ +.linecablemodels/fem/ +.linecablemodels/qa/ +docs/notes/ # examples artifacts !examples/*.jl +showcase/bonito/build/ -# Internal files -dev +# Private manual-test configuration +test/manual/pscad/local-pscad.toml bibliography/ presentations/ projects/ @@ -48,9 +60,25 @@ papers sprint */fem_output .github/*.md +!.github/OSS_PUBLISH_CHECKLIST.md .github/instructions .github/prompts test/jl_* *.sh +!.github/prose/*.sh +!.github/prose/fixtures/check.sh .vscode/ designs/ +benchmarks/ + +test/fixtures/reference/* +# Python junk +__pycache__/ +*.py[cod] + +.linecablemodels/ + +# Vale packages downloaded by the documentation job. +.github/prose/styles/ai-tells/ +.github/prose/styles/STE/ +.github/prose/styles/config/templates/ diff --git a/.gitleaksignore b/.gitleaksignore new file mode 100644 index 000000000..cec0b3b73 --- /dev/null +++ b/.gitleaksignore @@ -0,0 +1,38 @@ +# Reviewed historical false positives from Gitleaks 8.30.1. +# Each entry identifies one commit, file, rule and line. + +# Formula corpus identifiers, not credentials +02f51d3e7d899349b9e76c718f4f63fc729bbee2:line_cable_formula_corpus.json:generic-api-key:1089 +02f51d3e7d899349b9e76c718f4f63fc729bbee2:line_cable_formula_corpus.json:generic-api-key:1170 +02f51d3e7d899349b9e76c718f4f63fc729bbee2:line_cable_formula_corpus.json:generic-api-key:1495 +02f51d3e7d899349b9e76c718f4f63fc729bbee2:line_cable_formula_corpus.json:generic-api-key:1558 +02f51d3e7d899349b9e76c718f4f63fc729bbee2:line_cable_formula_corpus.json:generic-api-key:1628 +02f51d3e7d899349b9e76c718f4f63fc729bbee2:line_cable_formula_corpus.json:generic-api-key:175 +02f51d3e7d899349b9e76c718f4f63fc729bbee2:line_cable_formula_corpus.json:generic-api-key:1985 +02f51d3e7d899349b9e76c718f4f63fc729bbee2:line_cable_formula_corpus.json:generic-api-key:2260 +02f51d3e7d899349b9e76c718f4f63fc729bbee2:line_cable_formula_corpus.json:generic-api-key:2317 +02f51d3e7d899349b9e76c718f4f63fc729bbee2:line_cable_formula_corpus.json:generic-api-key:2370 +02f51d3e7d899349b9e76c718f4f63fc729bbee2:line_cable_formula_corpus.json:generic-api-key:2433 +02f51d3e7d899349b9e76c718f4f63fc729bbee2:line_cable_formula_corpus.json:generic-api-key:2488 +02f51d3e7d899349b9e76c718f4f63fc729bbee2:line_cable_formula_corpus.json:generic-api-key:263 +02f51d3e7d899349b9e76c718f4f63fc729bbee2:line_cable_formula_corpus.json:generic-api-key:2678 +02f51d3e7d899349b9e76c718f4f63fc729bbee2:line_cable_formula_corpus.json:generic-api-key:2741 +02f51d3e7d899349b9e76c718f4f63fc729bbee2:line_cable_formula_corpus.json:generic-api-key:2950 +02f51d3e7d899349b9e76c718f4f63fc729bbee2:line_cable_formula_corpus.json:generic-api-key:3225 +02f51d3e7d899349b9e76c718f4f63fc729bbee2:line_cable_formula_corpus.json:generic-api-key:3298 +02f51d3e7d899349b9e76c718f4f63fc729bbee2:line_cable_formula_corpus.json:generic-api-key:3367 +02f51d3e7d899349b9e76c718f4f63fc729bbee2:line_cable_formula_corpus.json:generic-api-key:3476 +02f51d3e7d899349b9e76c718f4f63fc729bbee2:line_cable_formula_corpus.json:generic-api-key:3555 +02f51d3e7d899349b9e76c718f4f63fc729bbee2:line_cable_formula_corpus.json:generic-api-key:3604 +02f51d3e7d899349b9e76c718f4f63fc729bbee2:line_cable_formula_corpus.json:generic-api-key:438 +02f51d3e7d899349b9e76c718f4f63fc729bbee2:line_cable_formula_corpus.json:generic-api-key:701 + +# AxisKeys package UUIDs, not credentials +16d29119492709b164eadda12cd7e2510545adde:playground/worker/profiles/power-flow/Manifest.toml:generic-api-key:47 +4367ffd692e1945de0bec28f8c56867700680c10:showcase/bonito/Manifest.toml:generic-api-key:60 + +# Julia quantity-key expression, not a credential +16ea5d2d5f0bf3ce0cd3c8d13df6be67808f7d25:test/quality/formula_routes.jl:generic-api-key:67 + +# Vendored JavaScript property-key expression, not a credential +6c2c3bd2b411ad630af66c35589d984ca0324b13:playground/assets/vendor/geographic-map.bundle.js:generic-api-key:3 diff --git a/.gitlint b/.gitlint new file mode 100644 index 000000000..deec075a1 --- /dev/null +++ b/.gitlint @@ -0,0 +1,6 @@ +[general] +contrib = CT1 +ignore = body-max-line-length,body-min-length,body-is-missing + +[title-match-regex] +regex = ^[a-z]+\([^()]+\)!?: [a-z].*$ diff --git a/Artifacts.toml b/Artifacts.toml new file mode 100644 index 000000000..824c65be3 --- /dev/null +++ b/Artifacts.toml @@ -0,0 +1,20 @@ +[[getdp]] +arch = "x86_64" +git-tree-sha1 = "51e049a32eeb2ffebcc8a5945bb00f70470a3d29" +lazy = true +libc = "glibc" +os = "linux" + + [[getdp.download]] + sha256 = "d3c28fa18f20d6147b4c7367d4dd802e9f7ddb58c608688bbb71919dbca8041d" + url = "https://getdp.info/bin/Linux/getdp-3.5.0-Linux64c.tgz" + +[[getdp]] +arch = "x86_64" +git-tree-sha1 = "7b00816601f7ad2ea7e83f6b12262fd27e0e1d6d" +lazy = true +os = "macos" + + [[getdp.download]] + sha256 = "ba514f4d6009b385ff27f6083f2843ff11a4cb1f3a829fdc0a5f8a2613646077" + url = "https://getdp.info/bin/MacOSX/getdp-3.5.0-MacOSXc.tgz" diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e7fa7527..25bb8a659 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,31 +1,288 @@ # Changelog -All notable changes to this project will be documented in this file. +All changes to this project are documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] -- / +### Added + +- `Engine.AbstractModalOperators` is the supertype of modal-to-phase operator bases. + `ModalOperators` subtypes it and implements `size`. +- `Pose2` is callable. `pose(point)` rotates a point by the pose's orientation and + translates it by the pose's position. +- `initialize_buffers(reduction::ReductionPlan, T, input, plan, buffers)` adds + `reduction`, the `ReductionBuffers` of a reduction plan, to a buffer record. ### Changed -- Refactored CodeComponent structure and constructors. +- `validate` is owned by `Commons`. `LineCableModels.validate` and + `import LineCableModels: validate` are unchanged. Every `validate` method takes the + checked input first and returns it unchanged or throws. Further arguments say what the + input is checked for. These signatures changed: + - `validate(settings, compare)` checks the RMS comparison settings of `compare`. + - `validate(workbooks, definition)` checks XLSX workbooks. + - The dielectric and temperature law checks take the law result first and no longer + convert it. + - The Unified check is `validate(Γ, formula type)`. + - The earth checks take the earth model or the resistivity first. + - A custom equivalent-earth reduction is admitted with `validate(reduction, expression)`. + - PSCAD checks take `PSCADFormulation` instead of `Val(:pscad)`. + - Result-space element types are checked with `validate(T, result space)`. +- `Commons.Expression(formula, operation, selectors...)` is one expression of a formula. + Calling it evaluates `operation(formula, selectors..., functor, workspace)`. +- `Commons.Functor(formula, input, state)` stores the input of one evaluation point and the + state of the formula, for every formula family. +- Formula methods take the formula object first. An expression's operation reads its input + from `functor.input`. +- `Commons.formulas(family)` returns the identifiers that a formula family registers. +- Earth formulas and equivalent-earth reductions resolve one expression per pair with + `Expression(formula, pair)`. A missing expression throws before the frequency loop. +- One `formulation_options(formula, expressions)` projection serves every formula family and + reports an unused option as `unused formulation options (...) for :id`. +- `Formulation(tag; kwargs...)` builds the backend formulations `:coaxial`, + `:cable_constants`, `:fem` and `:pscad`, and throws for an unknown tag. +- A saved `LineParameters` record keeps its `details` whole and loads them as data. +- On an earth with more than two layers, an earth formula with no expression beyond layer 2 + takes the `:default` reduction. +- `InternalImpedance` takes one formula per slot. +- The ShuntModel formulas are `:equivalent` and `:boundary`. `:default` resolves to + `:equivalent`. +- Unified's `EarthAdmittance.source_coefficients` returns the axial and potential + coefficients of one pair for both earth families. +- `ModalAnalysis` is a child module of `Engine`. The package root exports it and its public + names. +- The Unified Γ is validated when the formula is built. The `:unified` docstring and the + theory page on earth-return impedance state the validity ranges of the formula. +- Normalized author-formula identifiers to lowercase main-author-year symbols + and made their compact descriptions readable author names. +- Renamed the dielectric constitutive choices to `:lossy` and `:lossless`, + with `:default` retained as a routing alias. Ametani (2004) remains an + application reference rather than the formula name. +- Registered the literature identities for the Schelkunoff internal-impedance, + Ametani insulation-impedance, and Chrysochos modal routes while retaining + their package-owned `:default` selections. Formula hook IDs are now directly + queryable through `formula_id`. +- Refactored the `Material`-to-`compute` path around natural Julia promotion, + owner-local validation, explicit definitions, immutable solver input, and scoped console + logging. +- Cable parts and `CableDesign` now represent the common material reference state. + Operating temperature is owned by the line problem and applied once to local + resistivity values inside `compute`. +- Earth models now store static physical layers only. Analysis frequencies + belong to the problem. Frequency-dependent soil laws and their ephemeral + `EarthMaterial` values belong to `EarthProps` and are selected by the + line-parameter formulation. The default relation is an exact static + pass-through. +- Mathematical functions use short physical names without the former `calc_` prefix. +- Material and cable JSON files use an explicit versioned schema and dispatched + type tags. + JLS loading remains supported for trusted matching package types only. +- Wire-pattern searches return typed `WireEstimate` results, including ranked + best-effort candidates for feasible search inputs that cannot meet every limit. +- Consolidated shared problem, formulation, and result roots plus the common + action generics under `LineCableModels.Commons`. +- The coaxial and FEM backends share one matrix reduction, + `Commons.reduce_line_matrices!`. Ideal transposition now averages the + potential-coefficient matrix ``P`` before inverting it to ``Y`` in every backend. + The coaxial engine previously averaged ``Y`` after the inversion. +- `ideal_transposition` now defaults to `false` for line-parameter and FEM + formulations. +- Defined the vacuum constants once as `Commons.vacuum_permittivity(T)` and + `Commons.vacuum_permeability(T)`. The FEM input writer, PSCAD import, and boundary + shunt model now use the shared Float64 permittivity, one ulp below the former + `8.8541878128e-12` literal. +- Replaced the prototype parameter and uncertainty paths with typed `Grid` and + inferred `Gridspace` construction from the public declarative builders. +- Converged computation ownership around `Commons` action generics, + `ParametricBuilder` deterministic traversal, `UQ` uncertainty propagation, + and complete native execution in `Engine`. +- Reduced Gridspace to explicit finite `Grid` sources, local product or zip + composition, one internal unresolved point, and recursive materialization or + realization through concrete callable builders. Scalar public construction + calls now invoke the corresponding scalar action. +- Replaced the declarative PlotBuilder renderer with a compact native Makie + layer. `plot` retains automatic `(R, X, G, B)` views, scientific formatting, + log correction, visible-series limits, uncertainty bars, widgets, previews, + Monte Carlo plots, reports, and live SVG export while returning caller-owned + figures, axes, legends, colorbars, and controls. Legends and colorbars now + accept independent positions and native attributes. Observable quantities + now determine axes, while `layout` only arranges those axes in arbitrary + horizontal, vertical, or grid compositions. +- Restored `PlotBuilder` as a deliberately thin owner of optional plotting + entry points and live `UIPlot` handles. Plot request normalization, preview + presentation, and material palettes now live exclusively in the Makie + extension. Engine and DataModel retain only scientific results, public + observations, physical geometry, and property ranges. +- Split material visualization into the reusable `materialcolors(property, + range)` palette and `materialscale!(position, scheme)` single-colorbar + primitive. The three-scale reference remains optional preview sugar. +- Added `@observe` as three-index syntax over the native `observe` protocol. +- Added typed computation details with explicit higher-order retention and no + default per-point or per-trial record collection. +- Moved human-facing XLSX line-parameter workbooks to ReportBuilder. The + existing `export_data(:xlsx, ...)` call now delegates to + `XLSXReportDefinition`. +- Routed ordinary and higher-order execution through `compute`, with + `Combinatorial(inner)`, `LinearError(inner)`, and `MonteCarlo(inner)` selected + explicitly. +- Named the concentric backend `LineCableModelsCoaxial`, retained + `Formulation()` as the default line-parameter method bundle, and exposed + optional trace data through `details(result)`. +- Replaced nominal author-formula types with stable literature symbols, + overridable leaf routes, deterministic formula discovery, and an explicit + longitudinal propagation-constant path. +- Ported nine legacy soil-dispersion laws into discovered + `earth/frequencydependent/formulas/mainauthorYear.jl` files per literature + identity, with SI conversion and typed parameters in place of MATLAB flags + and runtime coefficient files. +- Moved equivalent homogeneous-earth rules to `EarthProps.EHEM`, separated + before-FD and after-FD composition by dispatch, and evaluated the resulting + material per conductor pair before the shared earth-impedance and admittance + calculations. Added the conductivity-only `:martinsbritto2020` and complex + propagation-constant `:xue2021` recurrences as discovered formulas. +- Added the public `formula(:AuthorYear; ...)` selector. Each formulation owner + resolves the same wrapper locally, while EHEM order is selected with + `order=:before` or `order=:after` without exposing sequence wrappers. +- Separated modal transformations from the coaxial backend into an independent + problem-formulation-compute workflow. Modal results retain complete + frequency-dependent voltage and current operators for reverse transformation. +- Made `build` the complete construction action for cable designs and systems. + Explicit `Grid` inputs materialize the same action through `Gridspace`. +- Separated unresolved `*Definition` geometry from resolved primitives carrying + absolute poses. +- Removed `NominalData` from `CableDesign`. `CablesLibrary` now binds optional + named-tuple catalog records beside stored designs. +- Moved Measurements.jl and Distributions.jl integrations into package + extensions. +- Restricted radial declarations to numeric radius or thickness semantics. +- Line-parameter plots and tables now select quantities with accessor tuples, + such as `(R, L, G, C)` or `(abs, angle)`. Direct plotting of + `LineParameters` returns R, X, G, and B matrix-dashboard pages in that order. + A direct complex `Z` or `Y` coordinate request expands to a paired component + page. Matrix coordinates identify semantic subplots, while legends identify + overlaid result containers through `series_labels`. +- The PSCAD sources moved from the extensions directory to `src/pscad/`, and its remote + runner project to `src/pscad/remote/`. `LineCableModels.PSCAD` is unchanged. +- `Commons` owns `initialize_buffers`, which builds every buffer of a computation. + Each formula and shared component extends `Commons.initialize_buffers(selected, T, + input, plan, buffers)` and shapes its own buffers, usually as a named tuple of arrays. + The fourth argument is the computation plan. +- The coaxial workspace fields are `input`, `plan`, `buffers` and `trace`. +- `SpectralIntegral` is a formulation, a subtype of `AbstractFormulation`, and + `integrate` takes it first: `integrate(integral, Val(:quad), controls, buffers)`. The + buffers are the record that `initialize_buffers(SpectralIntegral, Val(:quad), T, input, + plan, buffers)` extends with `quadrature`. + +### Removed + +- Removed PlotBuilder pages, recipes, rendering contexts, backend registration, + and fixed legend and colorbar docks. Plotting backends are selected per call or + inherited from Makie's active backend. +- Removed the former `Commons` and `Utils` utility modules, package scalar-union aliases, + coercion macros, operating-temperature cable fields, `EMTWorkspace`, + intermediate-storage options, file logging, and the constructor proxy types `MaxFill` + and `WireArray`. +- Retired unversioned and legacy JSON loading. The error identifies commit + `a71bdfe1ac832f27a0c88b1d02596194aac46ec7` as the last snapshot able to migrate those + files. +- Removed the former parameter tuple grammar, duplicate execution entrypoints, + specialized analysis containers, and radial proxy wrapper types. +- Removed Grid identity and binding machinery, public temporary point records, + traversal coordinates and metadata, result-side failure dictionaries, and + passive forwarding definitions from the parametric construction path. +- Removed the `mode=:ZY`/`:RLCG` and `coord=:cart`/`:polar` keywords from + line-parameter presentation. +- Removed the `InputValidation` module. `Commons` owns `validate`. +- Removed `Commons.check_core_result`. Use `validate(T, AbstractResultSpace)`. +- Removed `Engine.validate_modal_operators`. Modal operators subtype + `AbstractModalOperators` and implement `size`. +- Removed `TextDisplay.quantity`, which no package code used. `show` of a + `Units.Quantity` displays the quantity identity. +- Removed the `ParametricBuilder.Conductor`, `Insulator` and `Semiconductor` modules and + their root exports. Each of their constructions forwarded to a public builder with a + `tag`: `Conductor.Solid` to `core`, `Conductor.Shell` to `sheath`, `Conductor.Wires` to + `wires`, `Conductor.Strip` to `tape` within `terminal`, `Conductor.Tubular` to `core` + within `Group`, `Insulator.Shell` to `insulation` and `Semiconductor.Shell` to `screen`. +- Removed `Engine.integration_workspace`. `initialize_buffers(SpectralIntegral, Val(:quad), + T, input, plan, buffers)` builds the quadrature buffers. + +### Fixed + +- `uncertainty` of a complex value or an array gives the uncertainty of each part or + element, as `nominal` does. +- `uncertainty` of a deterministic number is the zero of its type. Values that are not + numbers throw a `MethodError`. +- The methods that `TextDisplay.@showfields` defines record their call site, so `@which`, + `methods` and stack traces show the caller. + +## [0.2.0] - 2026-08-13 ### Added -- Included interface to run finite element simulations using [Onelab](https://onelab.info/). +- Julia package extensions for the Makie backends. +- Aqua, SciML formatting, gitlint, clean-install, and modular documentation + checks. +- Citation and contribution metadata. +- Type-stable core and statistical result containers. +- One declarative PlotBuilder renderer with interactive legends and + one-click, non-overwriting SVG export. + +### Changed + +- Makie is an optional dependency. Core loading no longer imports Makie, + CairoMakie, GLMakie, or WGLMakie. +- Plotting requires the caller to load CairoMakie, GLMakie, or WGLMakie + explicitly. +- Documentation examples and development conventions were consolidated. +- Line-parameter results now include an explicit `:pul` or `:total` basis, + and use `Z`, `Y`, `R`, `X`, `L`, `G`, `B`, and `C` accessors consistently. +- `Units` now maps physical accessors to quantity, unit, label, symbol, + and scaling semantics without extracting values from result containers. +- `preview` and statistical plots return one `UIPlot`. Line-parameter plots + return `Vector{UIPlot}`. + +### Removed + +- Binder experiments and Binder-specific notebook bootstrapping. +- The accidental standalone cable-construction subsystem. Parameter-space and + covariance work remain under `ParametricBuilder`. +- FEM/Gmsh/GetDP support and sector-shaped cable support. The final + pre-removal snapshot is `legacy/fem-sector` at commit + `b75dd2723f90a83ec090b20605ea42af57f4a9c3`. +- The obsolete TODO scraper and duplicate tag-release workflow. +- The old `ResultsView`, `CableDesignMC`, `LineParametersPDF`, `plotmetadata`, + duplicated plotting UIs, and direct Makie renderers. + +### Migration + +Load optional integrations explicitly: + +```julia +using LineCableModels + +using CairoMakie +# preview(...) and plot(...) are now available + +R(line_parameters, 1, 1) # complete frequency response +R(line_parameters, 1, 1, 2:5) # selected frequencies +``` -- Included basic coverage tests. +Projects that require the removed FEM or sector APIs must pin the archived +snapshot: -- Included import/export of CableDesigns to JSON files. +```sh +julia --project=. -e "using Pkg; Pkg.add(Pkg.PackageSpec(url=ARGS[1], rev=ARGS[2]))" https://github.com/Electa-Git/LineCableModels.jl.git b75dd2723f90a83ec090b20605ea42af57f4a9c3 +``` -## [v0.1.0] - 2025-03-29 +## [0.1.0] - 2025-03-29 ### Added -- First release. See [README.md](https://github.com/Electa-Git/LineCableModels.jl/blob/main/README.md) for more details. +- Initial release. -[Unreleased]: [https://github.com/Electa-Git/LineCableModels.jl/compare/v0.1.0...HEAD](https://github.com/Electa-Git/LineCableModels.jl/compare/v0.1.0...HEAD) -[v0.1.0]: [https://github.com/Electa-Git/LineCableModels.jl/releases/tag/v0.1.0](https://github.com/Electa-Git/LineCableModels.jl/releases/tag/v0.1.0) +[Unreleased]: https://github.com/Electa-Git/LineCableModels.jl/compare/v0.2.0...HEAD +[0.2.0]: https://github.com/Electa-Git/LineCableModels.jl/compare/v0.1.0...v0.2.0 +[0.1.0]: https://github.com/Electa-Git/LineCableModels.jl/releases/tag/v0.1.0 diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 000000000..fc1fa22b4 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,13 @@ +cff-version: 1.2.0 +message: "If you use LineCableModels.jl, please cite this software." +title: "LineCableModels.jl" +type: software +version: 0.2.0 +date-released: 2026-08-13 +authors: + - family-names: Martins + given-names: Amauri + alias: amaurigmartins +repository-code: "https://github.com/Electa-Git/LineCableModels.jl" +url: "https://electa-git.github.io/LineCableModels.jl/" +license: BSD-3-Clause diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..8304bc2b2 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,3 @@ +# KU Leuven Code of Conduct + +This project follows the KU Leuven Sustainability Code / Code of conduct. Contributors are expected to adhere to these principles. You can read it on the [KU Leuven](https://admin.kuleuven.be/sab/jd/en/sustainability-code) website. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..432f565ab --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,108 @@ +# Contributing + +Use Julia 1.12 and instantiate the package environment before making changes: + +```julia +using Pkg +Pkg.instantiate() +Pkg.test() +``` + +Format maintained Julia files with `JuliaFormatter.format(".")`. +Use `type(scope): description` for commit subjects, with a lowercase imperative +description. Keep the complete subject within 72 characters. A breaking change +may use `type(scope)!: description`. Commit bodies are optional. + +Install Gitlint and enable its native commit hook once per clone: + +```bash +uv tool install gitlint-core +gitlint install-hook +``` + +The hook checks messages against `.gitlint` before creating a commit. +Gitlint keeps its default exemptions for merge, revert and fixup commits. + +Keep pull requests focused. Add tests for changed behavior and update public +documentation when an API changes. Optional plotting integrations must remain +outside core loading and must be checked with their dedicated workflows. + +## Writing checks + +The Documentation job checks writing after the documentation build and before +deployment. It installs Vale 3.23.0 and cspell 10.3.6 into runner storage, with +Node 24 for cspell. Vale downloads `ai-tells` 1.37.0 and the separate `STE.zip` +from Syntaf's 0.1.0 release. The Slop style is excluded. These checks require +no local installation, Python dependencies or Julia project dependencies. + +CI checks the complete maintained input on each workflow run. Git selects +tracked files, including tracked prose under ignored directories. All documentation +Markdown and rendered pages are added after the build. +Source checks include unpublished Julia docstrings and comments. Configuration +descriptions and workflow names are also checked. Edit Literate sources in +`examples/` and `docs/literate/` instead of their generated pages. + +### Rule levels + +Vale errors and known misspellings reported by cspell block deployment. +Vale warnings and suggestions remain visible in the uploaded `writing-reports` +artifact. Both checkers run +after successful setup and input selection, even if another writing check fails. +Missing inputs, invalid configuration and failed tool downloads also fail CI. + +The scientific STE profile detects contractions and ambiguous alternatives. +Sentence length, articles, noun clusters, passive voice and other grammatical +heuristics remain advisory. Modal substitutions are advisory because changing +a modal verb can change a scientific claim. The official approved-word dictionary +is absent from this profile. Full ASD-STE100 compliance requires a separate review. + +Project rules reject `provenance` and `contract`, including plurals and changes +in capitalization. `boundary` requires an approved geometric or physical phrase. +`canonical` is accepted in `canonical basis` and `canonical bases`. +`closure` requires a reviewed phrase about physically closing something. +Prose semicolons are errors. Code punctuation is preserved. + +The upstream error rules remain blocking. Native rule inheritance corrects +scientific false positives, including physical wire strands, significant digits, +numerical ranges and established mathematical names. Parsed prose scopes keep +raw-source rules from checking code identifiers. These corrections have regression +cases beside their rule definitions. + +### Exclusions and spelling + +Keep code and identifiers formatted as code. Vale excludes code and mathematics. +cspell checks whole source files using `unknownWords: "report-common-typos"`. +Only known misspellings and explicitly flagged words are reported. Unfamiliar +tool names, scientific terms and code identifiers pass without dictionary entries. +This mode deliberately leaves unknown words without a known correction unreported. + +US English, the Julia dictionary and `.github/prose/words.txt` remain configured. +Use an exact identifier exclusion if a valid identifier matches a known misspelling. +The spelling dictionary has no effect on Vale's rules. + +cspell infers each file's language from its filename. The empty `languageId` +override prevents the bundled Julia configuration from treating every file under +a `LineCableModels.jl` checkout directory as Julia. Keep the regression that +checks shell comments, JavaScript comments, Markdown and Julia docstrings under +that directory layout. + +Preserve licenses, copyright notices, published titles and verbatim external +quotations. Use an exact passage or a reviewed phrase in the relevant rule, +with a comment explaining the exception. Surrounding authored text remains +checked. The license file, immutable reference captures, machine data and +intentional checker fixtures are excluded. Maintained fixture documentation +remains included. Rendered navigation, theme controls and the external +bibliography are excluded from website checks. + +### Updating the tools + +Update the Vale and cspell versions in `.github/workflows/CI.yml` and the package +release URLs in `.github/prose/vale.ini`. Download only the STE archive from +Syntaf's release. Review changes to inherited rules and their exception lists. +Update the version numbers in this guide in the same change. + +CI runs native `vale test`, cspell regression cases and shell assertions before +checking the maintained writing. An update requires passing those cases, +successful doctests and a documentation build, and zero blocking findings on +the complete selected input. Review advisory findings without changing technical +meaning to satisfy a suggested substitution. diff --git a/JuliaTestItems.toml b/JuliaTestItems.toml new file mode 100644 index 000000000..41d8179c4 --- /dev/null +++ b/JuliaTestItems.toml @@ -0,0 +1,10 @@ +config-version = 1 +include = ["test/**"] +exclude = [ + "test/manual/**", + "test/**/.work/**", + "test/**/.artifacts/**", + "test/jl_*/**", + "test/tools/**", + "test/fixtures/**", +] diff --git a/Manifest.toml b/Manifest.toml new file mode 100644 index 000000000..cedc8b235 --- /dev/null +++ b/Manifest.toml @@ -0,0 +1,2199 @@ +# This file is machine-generated - editing it directly is not advised + +julia_version = "1.12.7" +manifest_format = "2.0" +project_hash = "98775e4833db5b32a74799a0d3ab641521fc1b02" + +[[deps.AbstractFFTs]] +deps = ["LinearAlgebra"] +git-tree-sha1 = "d92ad398961a3ed262d8bf04a1a2b8340f915fef" +uuid = "621f4979-c628-5d54-868e-fcf4e3e8185c" +version = "1.5.0" +weakdeps = ["ChainRulesCore", "Test"] + + [deps.AbstractFFTs.extensions] + AbstractFFTsChainRulesCoreExt = "ChainRulesCore" + AbstractFFTsTestExt = "Test" + +[[deps.AbstractTrees]] +git-tree-sha1 = "2d9c9a55f9c93e8887ad391fbae72f8ef55e1177" +uuid = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" +version = "0.4.5" + +[[deps.Accessors]] +deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "MacroTools"] +git-tree-sha1 = "7063ad1083578215c7c4bf410368150abe8d5524" +uuid = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697" +version = "0.1.45" + + [deps.Accessors.extensions] + AxisKeysExt = "AxisKeys" + IntervalSetsExt = "IntervalSets" + LinearAlgebraExt = "LinearAlgebra" + StaticArraysExt = "StaticArrays" + StructArraysExt = "StructArrays" + TestExt = "Test" + UnitfulExt = "Unitful" + + [deps.Accessors.weakdeps] + AxisKeys = "94b1ba4f-4ee9-5380-92f1-94cde586c3c5" + IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953" + LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" + StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" + StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a" + Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" + +[[deps.Adapt]] +deps = ["LinearAlgebra"] +git-tree-sha1 = "7c2c19b5a26e601634bf718490b89d59685f122e" +uuid = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" +version = "4.7.1" +weakdeps = ["SparseArrays", "StaticArrays"] + + [deps.Adapt.extensions] + AdaptSparseArraysExt = "SparseArrays" + AdaptStaticArraysExt = "StaticArrays" + +[[deps.AdaptivePredicates]] +git-tree-sha1 = "7e651ea8d262d2d74ce75fdf47c4d63c07dba7a6" +uuid = "35492f91-a3bd-45ad-95db-fcad7dcfedb7" +version = "1.2.0" + +[[deps.AliasTables]] +deps = ["PtrArrays", "Random"] +git-tree-sha1 = "9876e1e164b144ca45e9e3198d0b689cadfed9ff" +uuid = "66dad0bd-aa9a-41b7-9441-69ab47430ed8" +version = "1.1.3" + +[[deps.Animations]] +deps = ["Colors"] +git-tree-sha1 = "e092fa223bf66a3c41f9c022bd074d916dc303e7" +uuid = "27a7e980-b3e6-11e9-2bcd-0b925532e340" +version = "0.4.2" + +[[deps.Aqua]] +deps = ["Compat", "Pkg", "Test"] +git-tree-sha1 = "4537cc8ed3fba5d88b844a18f7e9bb93f07d2742" +uuid = "4c88cf16-eb10-579e-8560-4a9242c79595" +version = "0.8.16" + +[[deps.ArgCheck]] +git-tree-sha1 = "f9e9a66c9b7be1ad7372bbd9b062d9230c30c5ce" +uuid = "dce04be8-c92d-5529-be00-80e4d2c0e197" +version = "2.5.0" + +[[deps.ArgTools]] +uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" +version = "1.1.2" + +[[deps.Argus]] +deps = ["InteractiveUtils", "JuliaSyntax", "MacroTools", "RuntimeGeneratedFunctions", "Test"] +git-tree-sha1 = "72c07f04fae8460d54273ff22a82b7acbf6aa962" +uuid = "e39743ec-b2cc-45ef-a990-b666fc093538" +version = "0.4.2" + +[[deps.Artifacts]] +uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" +version = "1.11.0" + +[[deps.Automa]] +deps = ["PrecompileTools", "TranscodingStreams"] +git-tree-sha1 = "94eab0b3ccdcac361188cc661daf69d4433c1818" +uuid = "67c07d97-cdcb-5c2c-af73-a7f9c32a568b" +version = "1.2.0" + +[[deps.AxisAlgorithms]] +deps = ["LinearAlgebra", "Random", "SparseArrays", "WoodburyMatrices"] +git-tree-sha1 = "01b8ccb13d68535d73d2b0c23e39bd23155fb712" +uuid = "13072b0f-2c55-5437-9ae7-d433b7a33950" +version = "1.1.0" + +[[deps.AxisArrays]] +deps = ["Dates", "IntervalSets", "IterTools", "RangeArrays"] +git-tree-sha1 = "4126b08903b777c88edf1754288144a0492c05ad" +uuid = "39de3d68-74b9-583c-8d2d-e117c070f3a9" +version = "0.4.8" + +[[deps.Base64]] +uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" +version = "1.11.0" + +[[deps.BaseDirs]] +git-tree-sha1 = "8c290a1b223deaeea9aea44b235d24546da8eb98" +uuid = "18cc8868-cbac-4acf-b575-c8ff214dc66f" +version = "1.4.0" + +[[deps.BenchmarkTools]] +deps = ["Compat", "JSON", "Logging", "PrecompileTools", "Printf", "Profile", "Statistics", "UUIDs"] +git-tree-sha1 = "9670d3febc2b6da60a0ae57846ba74670290653f" +uuid = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +version = "1.8.0" + +[[deps.Bzip2_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "1b96ea4a01afe0ea4090c5c8039690672dd13f2e" +uuid = "6e34b625-4abd-537c-b88f-471c36dfa7a0" +version = "1.0.9+0" + +[[deps.CEnum]] +git-tree-sha1 = "389ad5c84de1ae7cf0e28e381131c98ea87d54fc" +uuid = "fa961155-64e5-5f13-b03f-caf6b980ea82" +version = "0.5.0" + +[[deps.CRC32c]] +uuid = "8bf52ea8-c179-5cab-976a-9e18b702a9bc" +version = "1.11.0" + +[[deps.CRlibm]] +deps = ["CRlibm_jll"] +git-tree-sha1 = "66188d9d103b92b6cd705214242e27f5737a1e5e" +uuid = "96374032-68de-5a5b-8d9e-752f78720389" +version = "1.0.2" + +[[deps.CRlibm_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "e329286945d0cfc04456972ea732551869af1cfc" +uuid = "4e9b3aee-d8a1-5a3d-ad8b-7d824db253f0" +version = "1.0.1+0" + +[[deps.Cairo]] +deps = ["Cairo_jll", "Colors", "Glib_jll", "Graphics", "Libdl", "Pango_jll"] +git-tree-sha1 = "71aa551c5c33f1a4415867fe06b7844faadb0ae9" +uuid = "159f3aea-2a34-519c-b102-8c37f9878175" +version = "1.1.1" + +[[deps.CairoMakie]] +deps = ["CRC32c", "Cairo", "Cairo_jll", "Colors", "FileIO", "FreeType", "GeometryBasics", "LinearAlgebra", "Makie", "PrecompileTools"] +git-tree-sha1 = "3495bfc164949714579501b825b8e5e2cce7c56f" +uuid = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +version = "0.15.14" + +[[deps.Cairo_jll]] +deps = ["Artifacts", "Bzip2_jll", "CompilerSupportLibraries_jll", "Fontconfig_jll", "FreeType2_jll", "Glib_jll", "JLLWrappers", "Libdl", "Pixman_jll", "Xorg_libXext_jll", "Xorg_libXrender_jll", "Zlib_jll", "libpng_jll"] +git-tree-sha1 = "7b841680738c19948120f6e4cf8d0200518bd564" +uuid = "83423d85-b0ee-5818-9007-b63ccbeb887a" +version = "1.18.8+0" + +[[deps.Calculus]] +deps = ["LinearAlgebra"] +git-tree-sha1 = "9cb23bbb1127eefb022b022481466c0f1127d430" +uuid = "49dc2e85-a5d0-5ad3-a950-438e2897f1b9" +version = "0.5.2" + +[[deps.ChainRulesCore]] +deps = ["Compat", "LinearAlgebra"] +git-tree-sha1 = "12177ad6b3cad7fd50c8b3825ce24a99ad61c18f" +uuid = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" +version = "1.26.1" +weakdeps = ["SparseArrays"] + + [deps.ChainRulesCore.extensions] + ChainRulesCoreSparseArraysExt = "SparseArrays" + +[[deps.ChunkCodecCore]] +git-tree-sha1 = "3496e2b359c27793d12bec54ec94be7983a6ceb2" +uuid = "0b6fb165-00bc-4d37-ab8b-79f91016dbe1" +version = "1.0.2" + +[[deps.ChunkCodecLibZlib]] +deps = ["ChunkCodecCore", "Zlib_jll"] +git-tree-sha1 = "d4101e848e8d3f585d61d244c2fe0c80a70e6b3b" +uuid = "4c0bbee4-addc-4d73-81a0-b6caacae83c8" +version = "1.1.0" + +[[deps.ChunkCodecLibZstd]] +deps = ["ChunkCodecCore", "Zstd_jll"] +git-tree-sha1 = "34d9873079e4cb3d0c62926a225136824677073f" +uuid = "55437552-ac27-4d47-9aa3-63184e8fd398" +version = "1.0.0" + +[[deps.CodeTracking]] +deps = ["InteractiveUtils", "REPL", "UUIDs"] +git-tree-sha1 = "cfb7a2e89e245a9d5016b70323db412b3a7438d5" +uuid = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2" +version = "3.0.2" + +[[deps.CodecInflate64]] +deps = ["TranscodingStreams"] +git-tree-sha1 = "d981a6e8656b1e363a2731716f46851a2257deb7" +uuid = "6309b1aa-fc58-479c-8956-599a07234577" +version = "0.1.3" + +[[deps.CodecZlib]] +deps = ["TranscodingStreams", "Zlib_jll"] +git-tree-sha1 = "970758a3d591a2a5c2a907c53f2e2f8c1b1d3537" +uuid = "944b1d66-785c-5afd-91f1-9de20f533193" +version = "0.7.9" + +[[deps.CodecZstd]] +deps = ["TranscodingStreams", "Zstd_jll"] +git-tree-sha1 = "da54a6cd93c54950c15adf1d336cfd7d71f51a56" +uuid = "6b39b394-51ab-5f42-8807-6242bab2b4c2" +version = "0.8.7" + +[[deps.ColorBrewer]] +deps = ["Colors", "JSON"] +git-tree-sha1 = "07da79661b919001e6863b81fc572497daa58349" +uuid = "a2cac450-b92f-5266-8821-25eda20663c8" +version = "0.4.2" + +[[deps.ColorSchemes]] +deps = ["ColorTypes", "ColorVectorSpace", "Colors", "FixedPointNumbers", "PrecompileTools", "Random"] +git-tree-sha1 = "b0fd3f56fa442f81e0a47815c92245acfaaa4e34" +uuid = "35d6a980-a343-548e-a6ea-1d62b119f2f4" +version = "3.31.0" + +[[deps.ColorTypes]] +deps = ["FixedPointNumbers", "Random"] +git-tree-sha1 = "61761f58648aa7217445f24f841839b78c712232" +uuid = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" +version = "0.12.3" +weakdeps = ["StyledStrings"] + + [deps.ColorTypes.extensions] + StyledStringsExt = "StyledStrings" + +[[deps.ColorVectorSpace]] +deps = ["ColorTypes", "FixedPointNumbers", "LinearAlgebra", "Requires", "Statistics", "TensorCore"] +git-tree-sha1 = "8b3b6f87ce8f65a2b4f857528fd8d70086cd72b1" +uuid = "c3611d14-8923-5661-9e6a-0046d554d3a4" +version = "0.11.0" +weakdeps = ["SpecialFunctions"] + + [deps.ColorVectorSpace.extensions] + SpecialFunctionsExt = "SpecialFunctions" + +[[deps.Colors]] +deps = ["ColorTypes", "FixedPointNumbers", "LinearAlgebra", "Reexport"] +git-tree-sha1 = "291665b547f137df070e4dd83e432b5fee8cc4a0" +uuid = "5ae59095-9a9b-59fe-a467-6f913c188581" +version = "0.13.2" + +[[deps.CommonSolve]] +deps = ["PrecompileTools"] +git-tree-sha1 = "6c389fa857f6ca5a95474b52a52023fd77f24cb7" +uuid = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" +version = "0.2.14" + +[[deps.Compat]] +deps = ["TOML", "UUIDs"] +git-tree-sha1 = "9d8a54ce4b17aa5bdce0ea5c34bc5e7c340d16ad" +uuid = "34da2185-b29b-5c13-b0c7-acf172513d20" +version = "4.18.1" +weakdeps = ["Dates", "LinearAlgebra"] + + [deps.Compat.extensions] + CompatLinearAlgebraExt = "LinearAlgebra" + +[[deps.Compiler]] +git-tree-sha1 = "382d79bfe72a406294faca39ef0c3cef6e6ce1f1" +uuid = "807dbc54-b67e-4c79-8afb-eafe4df6f2e1" +version = "0.1.1" + +[[deps.CompilerSupportLibraries_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" +version = "1.3.1+2" + +[[deps.CompositionsBase]] +git-tree-sha1 = "802bb88cd69dfd1509f6670416bd4434015693ad" +uuid = "a33af91c-f02d-484b-be07-31d278c5ca2b" +version = "0.1.2" +weakdeps = ["InverseFunctions"] + + [deps.CompositionsBase.extensions] + CompositionsBaseInverseFunctionsExt = "InverseFunctions" + +[[deps.ComputePipeline]] +deps = ["Observables", "Preferences"] +git-tree-sha1 = "7bc84b769c1d384315e7b5c4ac03a6c303e6cf35" +uuid = "95dc2771-c249-4cd0-9c9f-1f3b4330693c" +version = "0.1.8" + +[[deps.ConstructionBase]] +git-tree-sha1 = "b4b092499347b18a015186eae3042f72267106cb" +uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9" +version = "1.6.0" +weakdeps = ["IntervalSets", "LinearAlgebra", "StaticArrays"] + + [deps.ConstructionBase.extensions] + ConstructionBaseIntervalSetsExt = "IntervalSets" + ConstructionBaseLinearAlgebraExt = "LinearAlgebra" + ConstructionBaseStaticArraysExt = "StaticArrays" + +[[deps.Contour]] +git-tree-sha1 = "439e35b0b36e2e5881738abc8857bd92ad6ff9a8" +uuid = "d38c429a-6771-53c6-b99e-75d170b6e991" +version = "0.6.3" + +[[deps.CoreMath]] +deps = ["CoreMath_jll"] +git-tree-sha1 = "8c0480f92b1b1796239156a1b9b1bfb1b39499b4" +uuid = "b7a15901-be09-4a0e-87d2-2e66b0e09b5a" +version = "0.1.0" + +[[deps.CoreMath_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "a692a4c1dc59a4b8bc0b6403876eb3250fde2bc3" +uuid = "a38c48d9-6df1-5ac9-9223-b6ada3b5572b" +version = "0.1.0+0" + +[[deps.Crayons]] +git-tree-sha1 = "54b76cbb40d9a0f5368c880725b2f141da77c94f" +uuid = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f" +version = "4.2.0" + +[[deps.DataAPI]] +git-tree-sha1 = "abe83f3a2f1b857aac70ef8b269080af17764bbe" +uuid = "9a962f9c-6df0-11e9-0e5d-c546b8b5ee8a" +version = "1.16.0" + +[[deps.DataFrames]] +deps = ["Compat", "DataAPI", "DataStructures", "Future", "InlineStrings", "InvertedIndices", "IteratorInterfaceExtensions", "LinearAlgebra", "Markdown", "Missings", "PooledArrays", "PrecompileTools", "PrettyTables", "Printf", "Random", "Reexport", "SentinelArrays", "SortingAlgorithms", "Statistics", "TableTraits", "Tables", "Unicode"] +git-tree-sha1 = "5fab31e2e01e70ad66e3e24c968c264d1cf166d6" +uuid = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" +version = "1.8.2" + +[[deps.DataStructures]] +deps = ["OrderedCollections"] +git-tree-sha1 = "b0bc6d2cad1fed8b7fd59a1551a991cb3d2809e6" +uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" +version = "0.19.6" + +[[deps.DataValueInterfaces]] +git-tree-sha1 = "bfc1187b79289637fa0ef6d4436ebdfe6905cbd6" +uuid = "e2d170a0-9d28-54be-80f0-106bbe20a464" +version = "1.0.0" + +[[deps.Dates]] +deps = ["Printf"] +uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" +version = "1.11.0" + +[[deps.DelaunayTriangulation]] +deps = ["AdaptivePredicates", "EnumX", "ExactPredicates", "Random"] +git-tree-sha1 = "4ac548adcad90c1d5d677af13568a748af4c952b" +uuid = "927a84f5-c5f4-47a5-9785-b46e178433df" +version = "1.6.7" + +[[deps.Distributed]] +deps = ["Random", "Serialization", "Sockets"] +uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" +version = "1.11.0" + +[[deps.Distributions]] +deps = ["AliasTables", "FillArrays", "LinearAlgebra", "PDMats", "Printf", "QuadGK", "Random", "Roots", "SpecialFunctions", "Statistics", "StatsAPI", "StatsBase", "StatsFuns"] +git-tree-sha1 = "a958ab3a40c755563f5e1405c0846cb0446bf19d" +uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" +version = "0.25.131" + + [deps.Distributions.extensions] + DistributionsChainRulesCoreExt = "ChainRulesCore" + DistributionsDensityInterfaceExt = "DensityInterface" + DistributionsSparseConnectivityTracerExt = "SparseConnectivityTracer" + DistributionsTestExt = "Test" + + [deps.Distributions.weakdeps] + ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" + DensityInterface = "b429d917-457f-4dbc-8f4c-0cc954292b1d" + SparseConnectivityTracer = "9f842d2f-2579-4b1d-911e-f412cf18a3f5" + Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[[deps.DocStringExtensions]] +git-tree-sha1 = "7442a5dfe1ebb773c29cc2962a8980f47221d76c" +uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +version = "0.9.5" + +[[deps.Downloads]] +deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] +uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" +version = "1.7.0" + +[[deps.EarCut_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "e3290f2d49e661fbd94046d7e3726ffcb2d41053" +uuid = "5ae413db-bbd1-5e63-b57d-d24a61df00f5" +version = "2.2.4+0" + +[[deps.EnumX]] +git-tree-sha1 = "c49898e8438c828577f04b92fc9368c388ac783c" +uuid = "4e289a0a-7415-4d19-859d-a7e5c4648b56" +version = "1.0.7" + +[[deps.ExactPredicates]] +deps = ["IntervalArithmetic", "Random", "StaticArrays"] +git-tree-sha1 = "83231673ea4d3d6008ac74dc5079e77ab2209d8f" +uuid = "429591f6-91af-11e9-00e2-59fbe8cec110" +version = "2.2.9" + +[[deps.Expat_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "2bfb1e047e2ad0a5ca94365340bde8005d637568" +uuid = "2e619515-83b5-522b-bb60-26c02a35a201" +version = "2.8.4+0" + +[[deps.ExplicitImports]] +deps = ["Markdown", "Pkg", "PrecompileTools", "TOML", "Test"] +git-tree-sha1 = "a8b6c26bf0ea9f8c4bad71e109a5fa64f34be392" +uuid = "7d51a73a-1435-4ff3-83d9-f097790105c7" +version = "1.15.0" + +[[deps.ExprTools]] +git-tree-sha1 = "d2e49e7efd29719d6f28b891b0e0e159daa9d2b4" +uuid = "e2ba6199-217a-4e67-a87a-7c52f15ade04" +version = "0.1.11" + +[[deps.EzXML]] +deps = ["Printf", "XML2_jll"] +git-tree-sha1 = "7ea1aa5869e2626ccae84480e4f37185bc6f41d3" +uuid = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615" +version = "1.2.3" + +[[deps.FFMPEG_jll]] +deps = ["Artifacts", "Bzip2_jll", "FreeType2_jll", "FriBidi_jll", "JLLWrappers", "LAME_jll", "Libdl", "Ogg_jll", "OpenSSL_jll", "Opus_jll", "PCRE2_jll", "Zlib_jll", "libaom_jll", "libass_jll", "libfdk_aac_jll", "libva_jll", "libvorbis_jll", "x264_jll", "x265_jll"] +git-tree-sha1 = "7a58e45171b63ed4782f2d36fdee8713a469e6e0" +uuid = "b22a6f82-2f65-5046-a5b2-351ab43fb4e5" +version = "8.1.2+0" + +[[deps.FFTA]] +deps = ["AbstractFFTs", "DocStringExtensions", "LinearAlgebra", "MuladdMacro", "Primes", "Random", "Reexport"] +git-tree-sha1 = "65e55303b72f4a567a51b174dd2c47496efeb95a" +uuid = "b86e33f2-c0db-4aa1-a6e0-ab43e668529e" +version = "0.3.1" + +[[deps.FLTK_jll]] +deps = ["Artifacts", "Fontconfig_jll", "FreeType2_jll", "JLLWrappers", "JpegTurbo_jll", "Libdl", "Libglvnd_jll", "Pkg", "Xorg_libX11_jll", "Xorg_libXext_jll", "Xorg_libXfixes_jll", "Xorg_libXft_jll", "Xorg_libXinerama_jll", "Xorg_libXrender_jll", "Zlib_jll", "libpng_jll"] +git-tree-sha1 = "72a4842f93e734f378cf381dae2ca4542f019d23" +uuid = "4fce6fc7-ba6a-5f4c-898f-77e99806d6f8" +version = "1.3.8+0" + +[[deps.FileIO]] +deps = ["Pkg", "Requires", "UUIDs"] +git-tree-sha1 = "6621fef488e496356c9c9625d0562c12a6070819" +uuid = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" +version = "1.20.0" + + [deps.FileIO.extensions] + HTTPExt = "HTTP" + + [deps.FileIO.weakdeps] + HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" + +[[deps.FilePaths]] +deps = ["FilePathsBase", "MacroTools", "Reexport"] +git-tree-sha1 = "a1b2fbfe98503f15b665ed45b3d149e5d8895e4c" +uuid = "8fc22ac5-c921-52a6-82fd-178b2807b824" +version = "0.9.0" + + [deps.FilePaths.extensions] + FilePathsGlobExt = "Glob" + FilePathsURIParserExt = "URIParser" + FilePathsURIsExt = "URIs" + + [deps.FilePaths.weakdeps] + Glob = "c27321d9-0574-5035-807b-f59d2c89b15c" + URIParser = "30578b45-9adc-5946-b283-645ec420af67" + URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" + +[[deps.FilePathsBase]] +deps = ["Compat", "Dates"] +git-tree-sha1 = "3bab2c5aa25e7840a4b065805c0cdfc01f3068d2" +uuid = "48062228-2e41-5def-b9a4-89aafe57970f" +version = "0.9.24" +weakdeps = ["Mmap", "Test"] + + [deps.FilePathsBase.extensions] + FilePathsBaseMmapExt = "Mmap" + FilePathsBaseTestExt = "Test" + +[[deps.FileWatching]] +uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" +version = "1.11.0" + +[[deps.FillArrays]] +deps = ["LinearAlgebra"] +git-tree-sha1 = "086b5fbd032baf544678cc15b52b015e4c4aceb8" +uuid = "1a297f60-69ca-5386-bcde-b61e274b549b" +version = "1.17.1" +weakdeps = ["PDMats", "SparseArrays", "StaticArrays", "Statistics"] + + [deps.FillArrays.extensions] + FillArraysPDMatsExt = "PDMats" + FillArraysSparseArraysExt = "SparseArrays" + FillArraysStaticArraysExt = "StaticArrays" + FillArraysStatisticsExt = "Statistics" + +[[deps.FixedPointNumbers]] +deps = ["Random", "Statistics"] +git-tree-sha1 = "59af96b98217c6ef4ae0dfe065ac7c20831d1a84" +uuid = "53c48c17-4a7d-5ca2-90c5-79b7896eea93" +version = "0.8.6" + +[[deps.Fontconfig_jll]] +deps = ["Artifacts", "Bzip2_jll", "Expat_jll", "FreeType2_jll", "JLLWrappers", "Libdl", "Libuuid_jll", "Zlib_jll"] +git-tree-sha1 = "f85dac9a96a01087df6e3a749840015a0ca3817d" +uuid = "a3f928ae-7b40-5064-980b-68af3947d34b" +version = "2.17.1+0" + +[[deps.Format]] +git-tree-sha1 = "9c68794ef81b08086aeb32eeaf33531668d5f5fc" +uuid = "1fa38f19-a742-5d3f-a2b9-30dd87b9d5f8" +version = "1.3.7" + +[[deps.FreeType]] +deps = ["CEnum", "FreeType2_jll"] +git-tree-sha1 = "907369da0f8e80728ab49c1c7e09327bf0d6d999" +uuid = "b38be410-82b0-50bf-ab77-7b57e271db43" +version = "4.1.1" + +[[deps.FreeType2_jll]] +deps = ["Artifacts", "Bzip2_jll", "JLLWrappers", "Libdl", "Zlib_jll"] +git-tree-sha1 = "70329abc09b886fd2c5d94ad2d9527639c421e3e" +uuid = "d7e528f0-a631-5988-bf34-fe36492bcfd7" +version = "2.14.3+1" + +[[deps.FreeTypeAbstraction]] +deps = ["BaseDirs", "ColorVectorSpace", "Colors", "FreeType", "GeometryBasics", "Mmap"] +git-tree-sha1 = "4ebb930ef4a43817991ba35db6317a05e59abd11" +uuid = "663a7486-cb36-511b-a19d-713bb74d65c9" +version = "0.10.8" + +[[deps.FriBidi_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "7a214fdac5ed5f59a22c2d9a885a16da1c74bbc7" +uuid = "559328eb-81f9-559d-9380-de523a88c83c" +version = "1.0.17+0" + +[[deps.Future]] +deps = ["Random"] +uuid = "9fa8497b-333b-5362-9e8d-4d0656e87820" +version = "1.11.0" + +[[deps.GLU_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Libglvnd_jll", "Pkg"] +git-tree-sha1 = "65af046f4221e27fb79b28b6ca89dd1d12bc5ec7" +uuid = "bd17208b-e95e-5925-bf81-e2f59b3e5c61" +version = "9.0.1+0" + +[[deps.GMP_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "781609d7-10c4-51f6-84f2-b8444358ff6d" +version = "6.3.0+2" + +[[deps.Gamma]] +deps = ["LogExpFunctions"] +git-tree-sha1 = "becc397f7cfb06e343496ae6ffb04818a851da51" +uuid = "a0844989-3bd2-4988-8bea-c9407ab0941b" +version = "1.2.0" + +[[deps.GeometryBasics]] +deps = ["EarCut_jll", "LinearAlgebra", "PrecompileTools", "Random", "StaticArrays"] +git-tree-sha1 = "ec46c5825710fa1a15d468acb2d93cc939a7a5fe" +uuid = "5c1252a2-5f33-56bf-86c9-59e7332b4326" +version = "0.5.13" + + [deps.GeometryBasics.extensions] + ExtentsExt = "Extents" + GeometryBasicsGeoInterfaceExt = "GeoInterface" + IntervalSetsExt = "IntervalSets" + + [deps.GeometryBasics.weakdeps] + Extents = "411431e0-e8b7-467b-b5e0-f676ba4f2910" + GeoInterface = "cf35fbd7-0cd7-5166-be24-54bfbe79505f" + IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953" + +[[deps.GettextRuntime_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl", "Libiconv_jll"] +git-tree-sha1 = "45288942190db7c5f760f59c04495064eedf9340" +uuid = "b0724c58-0f36-5564-988d-3bb0596ebc4a" +version = "0.22.4+0" + +[[deps.Giflib_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "a3efbbc027441271444dcd0c0a46f2d119dc4329" +uuid = "59f7168a-df46-5410-90c8-f2779963d0ec" +version = "6.1.3+0" + +[[deps.Glib_jll]] +deps = ["Artifacts", "GettextRuntime_jll", "JLLWrappers", "Libdl", "Libffi_jll", "Libiconv_jll", "Libmount_jll", "PCRE2_jll", "Zlib_jll"] +git-tree-sha1 = "090526e65de8f69648ac156daae153de8b56df62" +uuid = "7746bdde-850d-59dc-9ae8-88ece973131d" +version = "2.88.3+0" + +[[deps.Gmsh]] +deps = ["gmsh_jll"] +git-tree-sha1 = "6d815101e62722f4e323514c9fc704007d4da2e3" +uuid = "705231aa-382f-11e9-3f0c-b7cb4346fdeb" +version = "0.3.1" + +[[deps.Graphics]] +deps = ["Colors", "LinearAlgebra", "NaNMath"] +git-tree-sha1 = "a641238db938fff9b2f60d08ed9030387daf428c" +uuid = "a2bd30eb-e257-5431-a919-1863eab51364" +version = "1.1.3" + +[[deps.Graphite2_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "69ffb934a5c5b7e086a0b4fee3427db2556fba6e" +uuid = "3b182d85-2403-5c21-9c21-1e1f0cc25472" +version = "1.3.16+0" + +[[deps.GridLayoutBase]] +deps = ["GeometryBasics", "InteractiveUtils", "Observables"] +git-tree-sha1 = "ef70da5e123a06a29e2d6ddff0f09985bc226491" +uuid = "3955a311-db13-416c-9275-1d80ed98e5e9" +version = "0.11.3" + +[[deps.HDF5_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "LibCURL_jll", "Libdl", "MPIABI_jll", "MPICH_jll", "MPIPreferences", "MPItrampoline_jll", "MicrosoftMPI_jll", "OpenMPI_jll", "OpenSSL_jll", "TOML", "Zlib_jll", "aws_c_s3_jll", "dlfcn_win32_jll", "libaec_jll", "mpif_jll"] +git-tree-sha1 = "45337643a2d97262d5fe72ce1f13e8a662d13d62" +uuid = "0234f1f7-429e-5d53-9886-15a909be8d59" +version = "2.1.2+0" + +[[deps.HarfBuzz_jll]] +deps = ["Artifacts", "Cairo_jll", "Fontconfig_jll", "FreeType2_jll", "Glib_jll", "Graphite2_jll", "JLLWrappers", "Libdl", "Libffi_jll"] +git-tree-sha1 = "9d9531a9cb63a9edc33836414e82a07e81710de2" +uuid = "2e76f6c2-a576-52d4-95c1-20adfe4de566" +version = "100.14004.0+0" + +[[deps.HashArrayMappedTries]] +git-tree-sha1 = "2eaa69a7cab70a52b9687c8bf950a5a93ec895ae" +uuid = "076d061b-32b6-4027-95e0-9a2c6f6d7e74" +version = "0.2.0" + +[[deps.Hwloc_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "XML2_jll", "Xorg_libpciaccess_jll"] +git-tree-sha1 = "8015ef94425626913eaa6933eb32e7c9bbe08030" +uuid = "e33a78d0-f292-5ffc-b300-72abe9b543c8" +version = "2.14.1+0" + +[[deps.HypergeometricFunctions]] +deps = ["Gamma", "LinearAlgebra"] +git-tree-sha1 = "31bb6c92405c084617facc1d7ed9eb6c402d061e" +uuid = "34004b35-14d8-5ef3-9330-4cdb6864b03a" +version = "0.3.30" + +[[deps.ImageAxes]] +deps = ["AxisArrays", "ImageBase", "ImageCore", "Reexport", "SimpleTraits"] +git-tree-sha1 = "e12629406c6c4442539436581041d372d69c55ba" +uuid = "2803e5a7-5153-5ecf-9a86-9b4c37f5f5ac" +version = "0.6.12" + +[[deps.ImageBase]] +deps = ["ImageCore", "Reexport"] +git-tree-sha1 = "eb49b82c172811fd2c86759fa0553a2221feb909" +uuid = "c817782e-172a-44cc-b673-b171935fbb9e" +version = "0.1.7" + +[[deps.ImageCore]] +deps = ["ColorVectorSpace", "Colors", "FixedPointNumbers", "MappedArrays", "MosaicViews", "OffsetArrays", "PaddedViews", "PrecompileTools", "Reexport"] +git-tree-sha1 = "8c193230235bbcee22c8066b0374f63b5683c2d3" +uuid = "a09fc81d-aa75-5fe9-8630-4744c3626534" +version = "0.10.5" + +[[deps.ImageIO]] +deps = ["FileIO", "IndirectArrays", "JpegTurbo", "LazyModules", "Netpbm", "OpenEXR", "PNGFiles", "QOI", "Sixel", "TiffImages", "UUIDs", "WebP"] +git-tree-sha1 = "f0f005f997dfb8c5fe23920d99458a9619873893" +uuid = "82e4d734-157c-48bb-816b-45c225c6df19" +version = "0.6.10" + +[[deps.ImageMetadata]] +deps = ["AxisArrays", "ImageAxes", "ImageBase", "ImageCore"] +git-tree-sha1 = "2a81c3897be6fbcde0802a0ebe6796d0562f63ec" +uuid = "bc367c6b-8a6b-528e-b4bd-a4b897500b49" +version = "0.9.10" + +[[deps.Imath_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "dcc8d0cd653e55213df9b75ebc6fe4a8d3254c65" +uuid = "905a6f67-0a94-5f89-b386-d35d92009cd1" +version = "3.2.2+0" + +[[deps.IndirectArrays]] +git-tree-sha1 = "012e604e1c7458645cb8b436f8fba789a51b257f" +uuid = "9b13fd28-a010-5f03-acff-a1bbcff69959" +version = "1.0.0" + +[[deps.Inflate]] +git-tree-sha1 = "d1b1b796e47d94588b3757fe84fbf65a5ec4a80d" +uuid = "d25df0c9-e2be-5dd7-82c8-3ad0b3e990b9" +version = "0.1.5" + +[[deps.InlineStrings]] +git-tree-sha1 = "06b65886c7577a3784d616e29f1302c2e36e389d" +uuid = "842dd82b-1e85-43dc-bf29-5d0ee9dffc48" +version = "1.4.6" + + [deps.InlineStrings.extensions] + ArrowTypesExt = "ArrowTypes" + ParsersExt = "Parsers" + + [deps.InlineStrings.weakdeps] + ArrowTypes = "31f734f8-188a-4ce0-8406-c8a06bd891cd" + Parsers = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" + +[[deps.InputBuffers]] +git-tree-sha1 = "e5392ea00942566b631e991dd896942189937b2f" +uuid = "0c81fc1b-5583-44fc-8770-48be1e1cca08" +version = "1.1.1" + +[[deps.IntegerMathUtils]] +git-tree-sha1 = "c72458f1962faeb003bf23cbdb75164fe6280906" +uuid = "18e54dd8-cb9d-406c-a71d-865a43cbb235" +version = "0.1.4" + +[[deps.InteractiveUtils]] +deps = ["Markdown"] +uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" +version = "1.11.0" + +[[deps.Interpolations]] +deps = ["Adapt", "AxisAlgorithms", "ChainRulesCore", "LinearAlgebra", "OffsetArrays", "Random", "Ratios", "SharedArrays", "SparseArrays", "StaticArrays", "WoodburyMatrices"] +git-tree-sha1 = "48922d06068130f87e43edef52382e6a94305ae6" +uuid = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" +version = "0.16.3" + + [deps.Interpolations.extensions] + InterpolationsForwardDiffExt = "ForwardDiff" + InterpolationsUnitfulExt = "Unitful" + + [deps.Interpolations.weakdeps] + ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" + Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" + +[[deps.IntervalArithmetic]] +deps = ["CRlibm", "CoreMath", "MacroTools", "OpenBLASConsistentFPCSR_jll", "Printf", "Random", "RoundingEmulator"] +git-tree-sha1 = "1c531bf0f8a5c60a340926e058fd3f209b5eef5d" +uuid = "d1acc4aa-44c8-5952-acd4-ba5d80a2a253" +version = "1.0.12" + + [deps.IntervalArithmetic.extensions] + IntervalArithmeticArblibExt = "Arblib" + IntervalArithmeticDiffRulesExt = "DiffRules" + IntervalArithmeticForwardDiffExt = "ForwardDiff" + IntervalArithmeticIntervalSetsExt = "IntervalSets" + IntervalArithmeticIrrationalConstantsExt = "IrrationalConstants" + IntervalArithmeticLinearAlgebraExt = "LinearAlgebra" + IntervalArithmeticMakieExt = "Makie" + IntervalArithmeticRecipesBaseExt = "RecipesBase" + IntervalArithmeticSparseArraysExt = "SparseArrays" + + [deps.IntervalArithmetic.weakdeps] + Arblib = "fb37089c-8514-4489-9461-98f9c8763369" + DiffRules = "b552c78f-8df3-52c6-915a-8e097449b14b" + ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" + IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953" + IrrationalConstants = "92d709cd-6900-40b7-9082-c6be49f344b6" + LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" + Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" + RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" + SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" + +[[deps.IntervalSets]] +git-tree-sha1 = "0db6aea5b64caa1c47e9fdd27394f0296f05e8bf" +uuid = "8197267c-284f-5f27-9208-e0e47529a953" +version = "0.7.15" + + [deps.IntervalSets.extensions] + IntervalSetsMakieExt = "Makie" + IntervalSetsRandomExt = "Random" + IntervalSetsRecipesBaseExt = "RecipesBase" + IntervalSetsStatisticsExt = "Statistics" + + [deps.IntervalSets.weakdeps] + Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" + Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" + RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" + Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" + +[[deps.InverseFunctions]] +git-tree-sha1 = "a779299d77cd080bf77b97535acecd73e1c5e5cb" +uuid = "3587e190-3f89-42d0-90ee-14403ec27112" +version = "0.1.17" +weakdeps = ["Dates", "Test"] + + [deps.InverseFunctions.extensions] + InverseFunctionsDatesExt = "Dates" + InverseFunctionsTestExt = "Test" + +[[deps.InvertedIndices]] +git-tree-sha1 = "6da3c4316095de0f5ee2ebd875df8721e7e0bdbe" +uuid = "41ab1584-1d38-5bbf-9106-f11c6c58b48f" +version = "1.3.1" + +[[deps.IrrationalConstants]] +git-tree-sha1 = "b2d91fe939cae05960e760110b328288867b5758" +uuid = "92d709cd-6900-40b7-9082-c6be49f344b6" +version = "0.2.6" + +[[deps.Isoband]] +deps = ["isoband_jll"] +git-tree-sha1 = "f9b6d97355599074dc867318950adaa6f9946137" +uuid = "f1662d9f-8043-43de-a69a-05efc1cc6ff4" +version = "0.1.1" + +[[deps.IterTools]] +git-tree-sha1 = "42d5f897009e7ff2cf88db414a389e5ed1bdd023" +uuid = "c8e1da08-722c-5040-9ed9-7db0dc04731e" +version = "1.10.0" + +[[deps.IteratorInterfaceExtensions]] +git-tree-sha1 = "a3f24677c21f5bbe9d2a714f95dcd58337fb2856" +uuid = "82899510-4779-5014-852e-03e436cf321d" +version = "1.0.0" + +[[deps.JET]] +deps = ["CodeTracking", "Compiler", "InteractiveUtils", "JuliaInterpreter", "JuliaSyntax", "Libdl", "LoweredCodeUtils", "MacroTools", "PrecompileTools", "Preferences", "Revise", "Test"] +git-tree-sha1 = "30213b9b1cafa1e70dd4de155ef0998c4561521f" +uuid = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +version = "0.12.2" + +[[deps.JLD2]] +deps = ["ChunkCodecLibZlib", "ChunkCodecLibZstd", "FileIO", "MacroTools", "Mmap", "OrderedCollections", "PrecompileTools", "ScopedValues"] +git-tree-sha1 = "877edc1d2f51adcef0bfacd19464a19e7cfddddb" +uuid = "033835bb-8acc-5ee8-8aae-3f567f8a3819" +version = "0.6.6" + + [deps.JLD2.extensions] + UnPackExt = "UnPack" + + [deps.JLD2.weakdeps] + UnPack = "3a884ed6-31ef-47d7-9d2a-63182c4928ed" + +[[deps.JLLWrappers]] +deps = ["Artifacts", "Preferences"] +git-tree-sha1 = "7204148362dafe5fe6a273f855b8ccbe4df8173e" +uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" +version = "1.8.0" + +[[deps.JSON]] +deps = ["Dates", "Logging", "Parsers", "PrecompileTools", "StructUtils", "UUIDs", "Unicode"] +git-tree-sha1 = "4657a834b01ce00f2b94546095c4350617b9af2c" +uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +version = "1.10.0" + + [deps.JSON.extensions] + JSONArrowExt = ["ArrowTypes"] + + [deps.JSON.weakdeps] + ArrowTypes = "31f734f8-188a-4ce0-8406-c8a06bd891cd" + +[[deps.JSON3]] +deps = ["Dates", "Mmap", "Parsers", "PrecompileTools", "StructTypes", "UUIDs"] +git-tree-sha1 = "411eccfe8aba0814ffa0fdf4860913ed09c34975" +uuid = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" +version = "1.14.3" + + [deps.JSON3.extensions] + JSON3ArrowExt = ["ArrowTypes"] + + [deps.JSON3.weakdeps] + ArrowTypes = "31f734f8-188a-4ce0-8406-c8a06bd891cd" + +[[deps.JSONSchema]] +deps = ["Downloads", "JSON", "URIs"] +git-tree-sha1 = "d13f79c4242969874da7d00bda17d59bc7699aa7" +uuid = "7d188eb4-7ad8-530c-ae41-71a32a6d4692" +version = "1.5.0" +weakdeps = ["JSON3"] + + [deps.JSONSchema.extensions] + JSONSchemaJSON3Ext = "JSON3" + +[[deps.JpegTurbo]] +deps = ["CEnum", "FileIO", "ImageCore", "JpegTurbo_jll", "TOML"] +git-tree-sha1 = "9496de8fb52c224a2e3f9ff403947674517317d9" +uuid = "b835a17e-a41a-41e7-81f0-2f016b05efe0" +version = "0.1.6" + +[[deps.JpegTurbo_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "037babc10853eeb8e585418922246cb97b8e5b74" +uuid = "aacddb02-875f-59d6-b918-886e6ef4fbf8" +version = "3.2.0+1" + +[[deps.JuliaInterpreter]] +deps = ["CodeTracking", "InteractiveUtils", "Random"] +git-tree-sha1 = "32c78da2a5d36c37dfbae36bcdd08f1d23238eca" +uuid = "aa1ae85d-cabe-5617-a682-6adf51b2e16a" +version = "0.11.6" + +[[deps.JuliaSyntax]] +git-tree-sha1 = "0d4b3dab95018bcf3925204475693d9f09dc45b8" +uuid = "70703baa-626e-46a2-a12c-08ffd08c73b4" +version = "1.0.2" + +[[deps.JuliaSyntaxHighlighting]] +deps = ["StyledStrings"] +uuid = "ac6e5ff7-fb65-4e79-a425-ec3bc9c03011" +version = "1.12.0" + +[[deps.KernelDensity]] +deps = ["Distributions", "DocStringExtensions", "FFTA", "Interpolations", "StatsBase"] +git-tree-sha1 = "9eda8292dd3268b3b7ec9df21bbfac24e177ec52" +uuid = "5ab0869b-81aa-558d-bb23-cbf5423bbe9b" +version = "0.6.12" + +[[deps.LAME_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "059aabebaa7c82ccb853dd4a0ee9d17796f7e1bc" +uuid = "c1c5ebd0-6772-5130-a774-d5fcae4a789d" +version = "3.100.3+0" + +[[deps.LERC_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "39bca05343661c347aae0bca57a5994a0bf4f08d" +uuid = "88015f11-f218-50d7-93a8-a6af411a945d" +version = "4.2.0+0" + +[[deps.LLVMOpenMP_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "e5b100780d4d30d63b4618d7930d48af409c1772" +uuid = "1d63c593-3942-5779-bab2-d838dc0a180e" +version = "23.1.1+0" + +[[deps.LaTeXStrings]] +git-tree-sha1 = "f88f3ccef05a6a72a0cf0ed417c8fd68530f4ab2" +uuid = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" +version = "1.4.1" + +[[deps.LazyArtifacts]] +deps = ["Artifacts", "Pkg"] +uuid = "4af54fe1-eca0-43a8-85a7-787d91b784e3" +version = "1.11.0" + +[[deps.LazyModules]] +git-tree-sha1 = "a560dd966b386ac9ae60bdd3a3d3a326062d3c3e" +uuid = "8cdb02fc-e678-4876-92c5-9defec4f444e" +version = "0.3.1" + +[[deps.LibCURL]] +deps = ["LibCURL_jll", "MozillaCACerts_jll"] +uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" +version = "0.6.4" + +[[deps.LibCURL_jll]] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll", "Zlib_jll", "nghttp2_jll"] +uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" +version = "8.15.0+0" + +[[deps.LibGit2]] +deps = ["LibGit2_jll", "NetworkOptions", "Printf", "SHA"] +uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" +version = "1.11.0" + +[[deps.LibGit2_jll]] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll"] +uuid = "e37daf67-58a4-590a-8e99-b0245dd2ffc5" +version = "1.9.0+0" + +[[deps.LibSSH2_jll]] +deps = ["Artifacts", "Libdl", "OpenSSL_jll"] +uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" +version = "1.11.3+1" + +[[deps.Libdl]] +uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" +version = "1.11.0" + +[[deps.Libffi_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "c8da7e6a91781c41a863611c7e966098d783c57a" +uuid = "e9f186c6-92d2-5b65-8a66-fee21dc1b490" +version = "3.4.7+0" + +[[deps.Libglvnd_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libX11_jll", "Xorg_libXext_jll"] +git-tree-sha1 = "d36c21b9e7c172a44a10484125024495e2625ac0" +uuid = "7e76a0d4-f3c7-5321-8279-8d96eeed0f29" +version = "1.7.1+1" + +[[deps.Libiconv_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "be484f5c92fad0bd8acfef35fe017900b0b73809" +uuid = "94ce4f54-9a6c-5748-9c1c-f9c7231a4531" +version = "1.18.0+0" + +[[deps.Libmount_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "cc3ad4faf30015a3e8094c9b5b7f19e85bdf2386" +uuid = "4b2f31a3-9ecc-558c-b454-b3730dcb73e9" +version = "2.42.0+0" + +[[deps.Libtiff_jll]] +deps = ["Artifacts", "JLLWrappers", "JpegTurbo_jll", "LERC_jll", "Libdl", "XZ_jll", "Zlib_jll", "Zstd_jll"] +git-tree-sha1 = "aebd334d06cee9f24cea70bd19a39749daf73881" +uuid = "89763e89-9b03-5906-acba-b20f662cd828" +version = "4.7.3+0" + +[[deps.Libuuid_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "d620582b1f0cbe2c72dd1d5bd195a9ce73370ab1" +uuid = "38a345b3-de98-5d2b-a5d3-14cd9215e700" +version = "2.42.0+0" + +[[deps.LineCableModels]] +deps = ["Base64", "DataFrames", "Dates", "DocStringExtensions", "EzXML", "GeometryBasics", "JSON3", "LinearAlgebra", "Logging", "Pkg", "Printf", "QuadGK", "Random", "RequiredInterfaces", "SHA", "Serialization", "SpecialFunctions", "Statistics", "TOML", "UUIDs"] +path = "." +uuid = "dffb7669-8cd4-4628-99dd-7694e356ba37" +version = "0.2.0" + + [deps.LineCableModels.extensions] + LineCableModelsCairoMakieExt = ["Makie", "CairoMakie"] + LineCableModelsDistributionsExt = "Distributions" + LineCableModelsGLMakieExt = ["Makie", "GLMakie"] + LineCableModelsGmshExt = "Gmsh" + LineCableModelsMakieExt = ["Makie", "Colors"] + LineCableModelsMeasurementsExt = ["Measurements", "Calculus"] + LineCableModelsWGLMakieExt = ["Makie", "WGLMakie"] + LineCableModelsXLSXExt = "XLSX" + + [deps.LineCableModels.weakdeps] + CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" + Calculus = "49dc2e85-a5d0-5ad3-a950-438e2897f1b9" + Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" + Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" + GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" + Gmsh = "705231aa-382f-11e9-3f0c-b7cb4346fdeb" + Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" + Measurements = "eff96d63-e80a-5855-80a2-b1b0885c5ab7" + WGLMakie = "276b4fcb-3e11-5398-bf8b-a0c2d153d008" + XLSX = "fdbf4ff8-1666-58a4-91e7-1b58723a45e0" + +[[deps.LinearAlgebra]] +deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"] +uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +version = "1.12.0" + +[[deps.LinearElasticity_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "c6977219e48f984c96ece60d148f884a5ae574f4" +uuid = "18c40d15-f7cd-5a6d-bc92-87468d86c5db" +version = "5.0.1+0" + +[[deps.LogExpFunctions]] +deps = ["DocStringExtensions", "IrrationalConstants", "LinearAlgebra"] +git-tree-sha1 = "b85e2797b2409570e84c4de46238c0ed5f6476ae" +uuid = "2ab3a3ac-af41-5b50-aa03-7779005ae688" +version = "1.0.2" + + [deps.LogExpFunctions.extensions] + LogExpFunctionsChainRulesCoreExt = "ChainRulesCore" + LogExpFunctionsChangesOfVariablesExt = "ChangesOfVariables" + LogExpFunctionsInverseFunctionsExt = "InverseFunctions" + + [deps.LogExpFunctions.weakdeps] + ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" + ChangesOfVariables = "9e997f8a-9a97-42d5-a9f1-ce6bfc15e2c0" + InverseFunctions = "3587e190-3f89-42d0-90ee-14403ec27112" + +[[deps.Logging]] +uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" +version = "1.11.0" + +[[deps.LoweredCodeUtils]] +deps = ["CodeTracking", "Compiler", "JuliaInterpreter"] +git-tree-sha1 = "e16fd69604bef06cb3fe9da09b315005c9c34564" +uuid = "6f1432cf-f94c-5a45-995e-cdbf5db27b0b" +version = "3.9.0" + +[[deps.METIS_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "5ecd92439c60a50c49bd6e96192e9d486b24b050" +uuid = "d00139f3-1899-568f-a2f0-47f597d42d70" +version = "5.1.4+0" + +[[deps.MMG_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "LinearElasticity_jll", "SCOTCH_jll"] +git-tree-sha1 = "129a77ce6f626f84759e93c8db7ec869377fdb97" +uuid = "86086c02-e288-5929-a127-40944b0018b7" +version = "5.8.0+0" + +[[deps.MPIABI_jll]] +deps = ["Artifacts", "Hwloc_jll", "JLLWrappers", "LazyArtifacts", "Libdl", "MPIPreferences", "TOML"] +git-tree-sha1 = "9be143b6045719e8fb019d2b3bc2aebad1184fef" +uuid = "b5ada748-db0f-5fc0-8972-9331c762740c" +version = "0.1.5+0" + +[[deps.MPICH_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Hwloc_jll", "JLLWrappers", "Libdl", "MPIPreferences", "TOML"] +git-tree-sha1 = "bdbb39057d6897527a702983b5fd5184207d23a2" +uuid = "7cb0a576-ebde-5e09-9194-50597f1243b4" +version = "5.0.2+0" + +[[deps.MPIPreferences]] +deps = ["Libdl", "Preferences"] +git-tree-sha1 = "8e98d5d80b87403c311fd51e8455d4546ba7a5f8" +uuid = "3da0fdf6-3ccc-4f1b-acd9-58baa6c99267" +version = "0.1.12" + +[[deps.MPItrampoline_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "LazyArtifacts", "Libdl", "MPIPreferences", "TOML"] +git-tree-sha1 = "675df097f8eeb28998b2cfe3b25655af73d5f7df" +uuid = "f1f71cc9-e9ae-5b93-9b94-4fe0e1ad3748" +version = "5.5.6+0" + +[[deps.MacroTools]] +git-tree-sha1 = "1e0228a030642014fe5cfe68c2c0a818f9e3f522" +uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" +version = "0.5.16" + +[[deps.Makie]] +deps = ["Animations", "Base64", "CRC32c", "ColorBrewer", "ColorSchemes", "ColorTypes", "Colors", "ComputePipeline", "Contour", "Dates", "DelaunayTriangulation", "Distributions", "DocStringExtensions", "Downloads", "FFMPEG_jll", "FileIO", "FilePaths", "FixedPointNumbers", "Format", "FreeType", "FreeTypeAbstraction", "GeometryBasics", "GridLayoutBase", "ImageBase", "ImageIO", "InteractiveUtils", "Interpolations", "IntervalSets", "InverseFunctions", "Isoband", "KernelDensity", "LaTeXStrings", "LinearAlgebra", "MacroTools", "Markdown", "MathTeXEngine", "Observables", "OffsetArrays", "PNGFiles", "Packing", "Pkg", "PlotUtils", "PolygonOps", "PrecompileTools", "Printf", "REPL", "Random", "RelocatableFolders", "Scratch", "ShaderAbstractions", "SignedDistanceFields", "SparseArrays", "Statistics", "StatsBase", "StatsFuns", "StructArrays", "TriplotBase", "UnicodeFun", "Unitful"] +git-tree-sha1 = "37b10d17f74f54dc5fa7d3c6c20fd75613c71d80" +uuid = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" +version = "0.24.14" + + [deps.Makie.extensions] + MakieDynamicQuantitiesExt = "DynamicQuantities" + + [deps.Makie.weakdeps] + DynamicQuantities = "06fc5a27-2a28-4c7c-a15d-362465fb6821" + +[[deps.MappedArrays]] +git-tree-sha1 = "0ee4497a4e80dbd29c058fcee6493f5219556f40" +uuid = "dbb5928d-eab1-5f90-85c2-b9b0edb7c900" +version = "0.4.3" + +[[deps.Markdown]] +deps = ["Base64", "JuliaSyntaxHighlighting", "StyledStrings"] +uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" +version = "1.11.0" + +[[deps.MathTeXEngine]] +deps = ["AbstractTrees", "Automa", "DataStructures", "FreeTypeAbstraction", "GeometryBasics", "LaTeXStrings", "REPL", "RelocatableFolders", "UnicodeFun"] +git-tree-sha1 = "aa1078778be5a8e5259ff04fbc3d258b3e78d464" +uuid = "0a4f8689-d25c-4efe-a92b-7142dfc1aa53" +version = "0.6.9" + +[[deps.Measurements]] +deps = ["Calculus", "LinearAlgebra", "Printf"] +git-tree-sha1 = "cb47f69a1cab9dcec7ff4a5d6e163410d6905866" +uuid = "eff96d63-e80a-5855-80a2-b1b0885c5ab7" +version = "2.14.1" + + [deps.Measurements.extensions] + MeasurementsBaseTypeExt = "BaseType" + MeasurementsJunoExt = "Juno" + MeasurementsMakieExt = "Makie" + MeasurementsRecipesBaseExt = "RecipesBase" + MeasurementsSpecialFunctionsExt = "SpecialFunctions" + MeasurementsUnitfulExt = "Unitful" + + [deps.Measurements.weakdeps] + BaseType = "7fbed51b-1ef5-4d67-9085-a4a9b26f478c" + Juno = "e5e0dc1b-0480-54bc-9374-aad01c23163d" + Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" + RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" + SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" + Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" + +[[deps.MicrosoftMPI_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "bc95bf4149bf535c09602e3acdf950d9b4376227" +uuid = "9237b28f-5490-5468-be7b-bb81f5f5e6cf" +version = "10.1.4+3" + +[[deps.Missings]] +deps = ["DataAPI"] +git-tree-sha1 = "ec4f7fbeab05d7747bdf98eb74d130a2a2ed298d" +uuid = "e1d29d7a-bbdc-5cf2-9ac0-f12de2c33e28" +version = "1.2.0" + +[[deps.Mmap]] +uuid = "a63ad114-7e13-5084-954f-fe012c677804" +version = "1.11.0" + +[[deps.MosaicViews]] +deps = ["MappedArrays", "OffsetArrays", "PaddedViews", "StackViews"] +git-tree-sha1 = "7b86a5d4d70a9f5cdf2dacb3cbe6d251d1a61dbe" +uuid = "e94cdb99-869f-56ef-bcf0-1ae2bcbe0389" +version = "0.3.4" + +[[deps.MozillaCACerts_jll]] +uuid = "14a3606d-f60d-562e-9121-12d972cd8159" +version = "2025.11.4" + +[[deps.MuladdMacro]] +deps = ["PrecompileTools"] +git-tree-sha1 = "283bf85d4a767481dd924dff0eee1735e95f449e" +uuid = "46d2c3a1-f734-5fdb-9937-b9b9aeba4221" +version = "0.2.7" + +[[deps.NaNMath]] +deps = ["OpenLibm_jll"] +git-tree-sha1 = "dbd2e8cd2c1c27f0b584f6661b4309609c5a685e" +uuid = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" +version = "1.1.4" + +[[deps.Netpbm]] +deps = ["FileIO", "ImageCore", "ImageMetadata"] +git-tree-sha1 = "d92b107dbb887293622df7697a2223f9f8176fcd" +uuid = "f09324ee-3d7c-5217-9330-fc30815ba969" +version = "1.1.1" + +[[deps.NetworkOptions]] +uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" +version = "1.3.0" + +[[deps.OCCT_jll]] +deps = ["Artifacts", "FreeType2_jll", "JLLWrappers", "Libdl", "Libglvnd_jll", "Xorg_libX11_jll", "Xorg_libXext_jll", "Xorg_libXfixes_jll", "Xorg_libXft_jll", "Xorg_libXinerama_jll", "Xorg_libXrender_jll"] +git-tree-sha1 = "b4d728dcd7f9c42862180d2ee881eeb4b471a02a" +uuid = "baad4e97-8daa-5946-aac2-2edac59d34e1" +version = "7.9.3+0" + +[[deps.Observables]] +git-tree-sha1 = "7438a59546cf62428fc9d1bc94729146d37a7225" +uuid = "510215fc-4207-5dde-b226-833fc4488ee2" +version = "0.5.5" + +[[deps.OffsetArrays]] +git-tree-sha1 = "117432e406b5c023f665fa73dc26e79ec3630151" +uuid = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" +version = "1.17.0" +weakdeps = ["Adapt"] + + [deps.OffsetArrays.extensions] + OffsetArraysAdaptExt = "Adapt" + +[[deps.Ogg_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "b6aa4566bb7ae78498a5e68943863fa8b5231b59" +uuid = "e7412a2a-1a6e-54c0-be00-318e2571c051" +version = "1.3.6+0" + +[[deps.OpenBLASConsistentFPCSR_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl"] +git-tree-sha1 = "38a93f17e431141c6470bb67a88952a7c4f0e928" +uuid = "6cdc7f73-28fd-5e50-80fb-958a8875b1af" +version = "0.3.34+0" + +[[deps.OpenBLAS_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] +uuid = "4536629a-c528-5b80-bd46-f80d51c5b363" +version = "0.3.29+0" + +[[deps.OpenEXR]] +deps = ["Colors", "FileIO", "OpenEXR_jll"] +git-tree-sha1 = "97db9e07fe2091882c765380ef58ec553074e9c7" +uuid = "52e1d378-f018-4a11-a4be-720524705ac7" +version = "0.3.3" + +[[deps.OpenEXR_jll]] +deps = ["Artifacts", "Imath_jll", "JLLWrappers", "Libdl", "Zlib_jll"] +git-tree-sha1 = "1bcebd887dd33f1108210b3954049b1bb8af0e7a" +uuid = "18a262bb-aa17-5467-a713-aee519bc75cb" +version = "3.4.15+0" + +[[deps.OpenLibm_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "05823500-19ac-5b8b-9628-191a04bc5112" +version = "0.8.7+0" + +[[deps.OpenMPI_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Hwloc_jll", "JLLWrappers", "LazyArtifacts", "Libdl", "MPIPreferences", "TOML", "Zlib_jll"] +git-tree-sha1 = "75eceed7df8ebb6af9c18c4ae89c070a99d91b00" +uuid = "fe0851c0-eecd-5654-98d4-656369965a5c" +version = "5.0.13+0" + +[[deps.OpenSSL_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" +version = "3.5.6+0" + +[[deps.OpenSpecFun_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl"] +git-tree-sha1 = "1346c9208249809840c91b26703912dff463d335" +uuid = "efe28fd5-8261-553b-a9e1-b2916fc3738e" +version = "0.5.6+0" + +[[deps.Opus_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "e2bb57a313a74b8104064b7efd01406c0a50d2ff" +uuid = "91d4177d-7536-5919-b921-800302f37372" +version = "1.6.1+0" + +[[deps.OrderedCollections]] +git-tree-sha1 = "f9b03759e9ef463718934fbed30820b39997511e" +uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +version = "2.0.2" + +[[deps.PCRE2_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "efcefdf7-47ab-520b-bdef-62a2eaa19f15" +version = "10.44.0+1" + +[[deps.PDMats]] +deps = ["LinearAlgebra", "SparseArrays", "SuiteSparse"] +git-tree-sha1 = "123266c25174ef6c8d4718920abc206452cf8de6" +uuid = "90014a1f-27ba-587c-ab20-58faa44d9150" +version = "0.11.41" +weakdeps = ["StatsBase"] + + [deps.PDMats.extensions] + StatsBaseExt = "StatsBase" + +[[deps.PNGFiles]] +deps = ["Base64", "CEnum", "ImageCore", "IndirectArrays", "OffsetArrays", "libpng_jll"] +git-tree-sha1 = "32b657a0d57c310a1a172bfc8c8cf68c5e674323" +uuid = "f57f5aa1-a3ce-4bc8-8ab9-96f992907883" +version = "0.4.5" + +[[deps.Packing]] +deps = ["GeometryBasics"] +git-tree-sha1 = "bc5bf2ea3d5351edf285a06b0016788a121ce92c" +uuid = "19eb6ba3-879d-56ad-ad62-d5c202156566" +version = "0.5.1" + +[[deps.PaddedViews]] +deps = ["OffsetArrays"] +git-tree-sha1 = "0fac6313486baae819364c52b4f483450a9d793f" +uuid = "5432bcbf-9aad-5242-b902-cca2824c8663" +version = "0.5.12" + +[[deps.Pango_jll]] +deps = ["Artifacts", "Cairo_jll", "Fontconfig_jll", "FreeType2_jll", "FriBidi_jll", "Glib_jll", "HarfBuzz_jll", "JLLWrappers", "Libdl"] +git-tree-sha1 = "1912a9f1b9ca55005b03ba075f8e19993583e237" +uuid = "36c8627f-9965-5494-a995-c6b170f724f3" +version = "1.58.2+0" + +[[deps.Parsers]] +deps = ["Dates", "PrecompileTools", "UUIDs"] +git-tree-sha1 = "ba0dc8a8a67cacac4842631f960c046e4e563675" +uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" +version = "2.8.8" + +[[deps.Pixman_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "LLVMOpenMP_jll", "Libdl"] +git-tree-sha1 = "e4a6721aa89e62e5d4217c0b21bd714263779dda" +uuid = "30392449-352a-5448-841d-b1acce4e97dc" +version = "0.46.4+0" + +[[deps.Pkg]] +deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"] +uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +version = "1.12.1" +weakdeps = ["REPL"] + + [deps.Pkg.extensions] + REPLExt = "REPL" + +[[deps.PkgVersion]] +deps = ["Pkg"] +git-tree-sha1 = "f9501cc0430a26bc3d156ae1b5b0c1b47af4d6da" +uuid = "eebad327-c553-4316-9ea0-9fa01ccd7688" +version = "0.3.3" + +[[deps.PlotUtils]] +deps = ["ColorSchemes", "Colors", "Dates", "PrecompileTools", "Printf", "Reexport", "Statistics"] +git-tree-sha1 = "f20e945b895d2009c6c28d8bbf40a5cd846f7c2f" +uuid = "995b91a9-d308-5afd-9ec6-746e21dbc043" +version = "1.5.0" + +[[deps.PolygonOps]] +git-tree-sha1 = "77b3d3605fc1cd0b42d95eba87dfcd2bf67d5ff6" +uuid = "647866c9-e3ac-4575-94e7-e3d426903924" +version = "0.1.2" + +[[deps.PooledArrays]] +deps = ["DataAPI", "Future"] +git-tree-sha1 = "36d8b4b899628fb92c2749eb488d884a926614d3" +uuid = "2dfb63ee-cc39-5dd5-95bd-886bf059d720" +version = "1.4.3" + +[[deps.PrecompileTools]] +deps = ["Preferences"] +git-tree-sha1 = "edbeefc7a4889f528644251bdb5fc9ab5348bc2c" +uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a" +version = "1.3.4" + +[[deps.Preferences]] +deps = ["TOML"] +git-tree-sha1 = "5005266de4bfe50e53ff44a5cb5c540b6e47a254" +uuid = "21216c6a-2e73-6563-6e65-726566657250" +version = "1.6.0" + +[[deps.PrettyTables]] +deps = ["Crayons", "LaTeXStrings", "Markdown", "PrecompileTools", "Printf", "REPL", "Reexport", "StringManipulation", "StyledStrings", "Tables"] +git-tree-sha1 = "99fd8f32ab6728ff205cddd24e2d9ea6cbb498d3" +uuid = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d" +version = "3.5.0" + + [deps.PrettyTables.extensions] + PrettyTablesExcelExt = "XLSX" + PrettyTablesTypstryExt = "Typstry" + PrettyTablesWriteDocxExt = "WriteDocx" + + [deps.PrettyTables.weakdeps] + Typstry = "f0ed7684-a786-439e-b1e3-3b82803b501e" + WriteDocx = "d049ceea-54ee-41d7-a26f-ba29db3b6599" + XLSX = "fdbf4ff8-1666-58a4-91e7-1b58723a45e0" + +[[deps.Primes]] +deps = ["IntegerMathUtils"] +git-tree-sha1 = "25cdd1d20cd005b52fc12cb6be3f75faaf59bb9b" +uuid = "27ebfcd6-29c5-5fa9-bf4b-fb8fc14df3ae" +version = "0.5.7" + +[[deps.Printf]] +deps = ["Unicode"] +uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" +version = "1.11.0" + +[[deps.Profile]] +deps = ["StyledStrings"] +uuid = "9abbd945-dff8-562f-b5e8-e1ebf5ef1b79" +version = "1.11.0" + +[[deps.ProgressMeter]] +deps = ["Distributed", "Printf"] +git-tree-sha1 = "fbb92c6c56b34e1a2c4c36058f68f332bec840e7" +uuid = "92933f4c-e287-5a05-a399-4b506db050ca" +version = "1.11.0" + +[[deps.PtrArrays]] +git-tree-sha1 = "4fbbafbc6251b883f4d2705356f3641f3652a7fe" +uuid = "43287f4e-b6f4-7ad1-bb20-aadabca52c3d" +version = "1.4.0" + +[[deps.QOI]] +deps = ["ColorTypes", "FileIO", "FixedPointNumbers"] +git-tree-sha1 = "472daaa816895cb7aee81658d4e7aec901fa1106" +uuid = "4b34888f-f399-49d4-9bb3-47ed5cae4e65" +version = "1.0.2" + +[[deps.QuadGK]] +deps = ["DataStructures", "LinearAlgebra"] +git-tree-sha1 = "5e8e8b0ab68215d7a2b14b9921a946fee794749e" +uuid = "1fd47b50-473d-5c70-9696-f719f8f3bcdc" +version = "2.11.3" + + [deps.QuadGK.extensions] + QuadGKEnzymeExt = "Enzyme" + + [deps.QuadGK.weakdeps] + Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" + +[[deps.REPL]] +deps = ["InteractiveUtils", "JuliaSyntaxHighlighting", "Markdown", "Sockets", "StyledStrings", "Unicode"] +uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" +version = "1.11.0" + +[[deps.Random]] +deps = ["SHA"] +uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +version = "1.11.0" + +[[deps.RangeArrays]] +git-tree-sha1 = "b9039e93773ddcfc828f12aadf7115b4b4d225f5" +uuid = "b3c3ace0-ae52-54e7-9d0b-2c1406fd6b9d" +version = "0.3.2" + +[[deps.Ratios]] +deps = ["Requires"] +git-tree-sha1 = "1342a47bf3260ee108163042310d26f2be5ec90b" +uuid = "c84ed2f1-dad5-54f0-aa8e-dbefe2724439" +version = "0.4.5" +weakdeps = ["FixedPointNumbers"] + + [deps.Ratios.extensions] + RatiosFixedPointNumbersExt = "FixedPointNumbers" + +[[deps.ReLint]] +deps = ["Argus", "Dates", "JSON3", "JuliaSyntax"] +git-tree-sha1 = "fa6df0d111947d57b3c0f7f90d4d8ff408480efb" +repo-rev = "61079427d9fc91cb3ead8f5b287aa3ae9b2269cb" +repo-url = "https://github.com/RelationalAI-oss/ReLint.jl" +uuid = "b3cc710f-9c33-5bdb-a03d-a94903873e97" +version = "0.9.0" + +[[deps.Reexport]] +git-tree-sha1 = "45e428421666073eab6f2da5c9d310d99bb12f9b" +uuid = "189a3867-3050-52da-a836-e630ba90ab69" +version = "1.2.2" + +[[deps.RelocatableFolders]] +deps = ["SHA", "Scratch"] +git-tree-sha1 = "ffdaf70d81cf6ff22c2b6e733c900c3321cab864" +uuid = "05181044-ff0b-4ac5-8273-598c1e38db00" +version = "1.0.1" + +[[deps.RequiredInterfaces]] +deps = ["InteractiveUtils", "Logging", "Test"] +git-tree-sha1 = "f4e7fec4fa52d0919f18fec552d2fabf9e94811d" +uuid = "97f35ef4-7bc5-4ec1-a41a-dcc69c7308c6" +version = "0.1.7" + +[[deps.Requires]] +deps = ["UUIDs"] +git-tree-sha1 = "62389eeff14780bfe55195b7204c0d8738436d64" +uuid = "ae029012-a4dd-5104-9daa-d747884805df" +version = "1.3.1" + +[[deps.Revise]] +deps = ["CRC32c", "CodeTracking", "FileWatching", "JuliaInterpreter", "LibGit2", "LoweredCodeUtils", "OrderedCollections", "Preferences", "REPL", "UUIDs"] +git-tree-sha1 = "177e0fc0e89b18c567e90d04f841a8304b22f097" +uuid = "295af30f-e4ad-537b-8983-00126c2a3abe" +version = "3.17.2" +weakdeps = ["Distributed"] + + [deps.Revise.extensions] + DistributedExt = "Distributed" + +[[deps.Rmath]] +deps = ["Random", "Rmath_jll"] +git-tree-sha1 = "5b3d50eb374cea306873b371d3f8d3915a018f0b" +uuid = "79098fc4-a85e-5d69-aa6a-4863f24498fa" +version = "0.9.0" + +[[deps.Rmath_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "6d40b2fe70437b01397d2a4d5b020008da4e7019" +uuid = "f50d1b31-88e8-58de-be2c-1cc44531875f" +version = "0.5.2+0" + +[[deps.Roots]] +deps = ["Accessors", "CommonSolve", "Printf"] +git-tree-sha1 = "a700562d106889a6e103b0d4db2a14f94c43db0c" +uuid = "f2b01f46-fcfa-551c-844a-d8ac1e96c665" +version = "3.0.9" + + [deps.Roots.extensions] + RootsChainRulesCoreExt = "ChainRulesCore" + RootsForwardDiffExt = "ForwardDiff" + RootsIntervalRootFindingExt = "IntervalRootFinding" + RootsSymPyExt = "SymPy" + RootsSymPyPythonCallExt = "SymPyPythonCall" + RootsUnitfulExt = "Unitful" + + [deps.Roots.weakdeps] + ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" + ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" + IntervalRootFinding = "d2bf35a9-74e0-55ec-b149-d360ff49b807" + SymPy = "24249f21-da20-56a4-8eb1-6a02cf4ae2e6" + SymPyPythonCall = "bc8888f7-b21e-4b7c-a06a-5d9c9496438c" + Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" + +[[deps.RoundingEmulator]] +git-tree-sha1 = "40b9edad2e5287e05bd413a38f61a8ff55b9557b" +uuid = "5eaf0fd0-dfba-4ccb-bf02-d820a40db705" +version = "0.2.1" + +[[deps.RuntimeGeneratedFunctions]] +deps = ["ExprTools", "PrecompileTools", "SHA", "Serialization"] +git-tree-sha1 = "9bb075002b40849582905394bb3031fb77aa102d" +uuid = "7e49a35a-f44a-4d26-94aa-eba1b4ca6b47" +version = "0.5.27" + +[[deps.SCOTCH_jll]] +deps = ["Artifacts", "Bzip2_jll", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl", "XZ_jll", "Zlib_jll"] +git-tree-sha1 = "081c66492f9280b4fe7a8bb2628b62ab4b91cc5c" +uuid = "a8d0f55d-b80e-548d-aff6-1a04c175f0f9" +version = "7.0.11+0" + +[[deps.SHA]] +uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" +version = "0.7.0" + +[[deps.SIMD]] +deps = ["PrecompileTools"] +git-tree-sha1 = "e24dc23107d426a096d3eae6c165b921e74c18e4" +uuid = "fdea26ae-647d-5447-a871-4b548cad5224" +version = "3.7.2" + +[[deps.ScopedValues]] +deps = ["HashArrayMappedTries", "Logging"] +git-tree-sha1 = "67a144433c4ce877ee6d1ada69a124d6b1ecf7be" +uuid = "7e506255-f358-4e82-b7e4-beb19740aa63" +version = "1.6.2" + +[[deps.Scratch]] +deps = ["Dates"] +git-tree-sha1 = "9b81b8393e50b7d4e6d0a9f14e192294d3b7c109" +uuid = "6c6a2e73-6563-6170-7368-637461726353" +version = "1.3.0" + +[[deps.SentinelArrays]] +deps = ["Dates", "Random"] +git-tree-sha1 = "084c47c7c5ce5cfecefa0a98dff69eb3646b5a80" +uuid = "91c51154-3ec4-41a3-a24f-3f23e20d615c" +version = "1.4.10" + +[[deps.Serialization]] +uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" +version = "1.11.0" + +[[deps.ShaderAbstractions]] +deps = ["ColorTypes", "FixedPointNumbers", "GeometryBasics", "LinearAlgebra", "Observables", "StaticArrays"] +git-tree-sha1 = "57aa595158717ef165e6f5ab639fe2e3178c0a2b" +uuid = "65257c39-d410-5151-9873-9b3e5be5013e" +version = "0.5.1" + +[[deps.SharedArrays]] +deps = ["Distributed", "Mmap", "Random", "Serialization"] +uuid = "1a1011a3-84de-559e-8e89-a11a2f7dc383" +version = "1.11.0" + +[[deps.SignedDistanceFields]] +deps = ["Statistics"] +git-tree-sha1 = "3949ad92e1c9d2ff0cd4a1317d5ecbba682f4b92" +uuid = "73760f76-fbc4-59ce-8f25-708e95d2df96" +version = "0.4.1" + +[[deps.SimpleTraits]] +deps = ["InteractiveUtils", "MacroTools"] +git-tree-sha1 = "7ddb0b49c109481b046972c0e4ab02b2127d6a75" +uuid = "699a6c99-e7fa-54fc-8d76-47d257e15c1d" +version = "0.9.6" + +[[deps.Sixel]] +deps = ["Dates", "FileIO", "ImageCore", "IndirectArrays", "OffsetArrays", "REPL", "libsixel_jll"] +git-tree-sha1 = "2c79185e5261b159474903598c571ddd9f12ff7b" +uuid = "45858cf5-a6b0-47a3-bbea-62219f50df47" +version = "0.1.6" + +[[deps.Sockets]] +uuid = "6462fe0b-24de-5631-8697-dd941f90decc" +version = "1.11.0" + +[[deps.SortingAlgorithms]] +deps = ["DataStructures"] +git-tree-sha1 = "13cd91cc9be159e3f4d95b857fa2aa383b53772a" +uuid = "a2af1166-a08f-5f64-846c-94a0d3cef48c" +version = "1.2.3" + +[[deps.SparseArrays]] +deps = ["Libdl", "LinearAlgebra", "Random", "Serialization", "SuiteSparse_jll"] +uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" +version = "1.12.0" + +[[deps.SpecialFunctions]] +deps = ["IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"] +git-tree-sha1 = "429071b23f4c9a13fb6582f807cc2ef454082408" +uuid = "276daf66-3868-5448-9aa4-cd146d93841b" +version = "2.9.0" +weakdeps = ["ChainRulesCore"] + + [deps.SpecialFunctions.extensions] + SpecialFunctionsChainRulesCoreExt = "ChainRulesCore" + +[[deps.StackViews]] +deps = ["OffsetArrays"] +git-tree-sha1 = "be1cf4eb0ac528d96f5115b4ed80c26a8d8ae621" +uuid = "cae243ae-269e-4f55-b966-ac2d0dc13c15" +version = "0.1.2" + +[[deps.StaticArrays]] +deps = ["LinearAlgebra", "PrecompileTools", "Random", "StaticArraysCore"] +git-tree-sha1 = "39e70e0ab5d7f89833a62ab7c79df15d4fc417c1" +uuid = "90137ffa-7385-5640-81b9-e52037218182" +version = "1.9.22" +weakdeps = ["ChainRulesCore", "Statistics"] + + [deps.StaticArrays.extensions] + StaticArraysChainRulesCoreExt = "ChainRulesCore" + StaticArraysStatisticsExt = "Statistics" + +[[deps.StaticArraysCore]] +git-tree-sha1 = "6ab403037779dae8c514bad259f32a447262455a" +uuid = "1e83bf80-4336-4d27-bf5d-d5a4f845583c" +version = "1.4.4" + +[[deps.Statistics]] +deps = ["LinearAlgebra"] +git-tree-sha1 = "e2b53ce13a53367e96601081e33d34746b571bad" +uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +version = "1.11.5" +weakdeps = ["SparseArrays"] + + [deps.Statistics.extensions] + SparseArraysExt = ["SparseArrays"] + +[[deps.StatsAPI]] +deps = ["LinearAlgebra"] +git-tree-sha1 = "178ed29fd5b2a2cfc3bd31c13375ae925623ff36" +uuid = "82ae8749-77ed-4fe6-ae5f-f523153014b0" +version = "1.8.0" + +[[deps.StatsBase]] +deps = ["AliasTables", "DataAPI", "DataStructures", "IrrationalConstants", "LinearAlgebra", "LogExpFunctions", "Missings", "Printf", "Random", "SortingAlgorithms", "SparseArrays", "Statistics", "StatsAPI"] +git-tree-sha1 = "adb9da019510162e67a4493fc235c23203d8b09e" +uuid = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" +version = "0.34.13" + +[[deps.StatsFuns]] +deps = ["HypergeometricFunctions", "IrrationalConstants", "LogExpFunctions", "Reexport", "Rmath", "SpecialFunctions"] +git-tree-sha1 = "91a5737baed20ee31f3faea0e51f57461f6a689e" +uuid = "4c63d2b9-4356-54db-8cca-17b64c39e42c" +version = "2.2.1" +weakdeps = ["ChainRulesCore", "InverseFunctions"] + + [deps.StatsFuns.extensions] + StatsFunsChainRulesCoreExt = "ChainRulesCore" + StatsFunsInverseFunctionsExt = "InverseFunctions" + +[[deps.StringManipulation]] +deps = ["PrecompileTools", "StyledStrings"] +git-tree-sha1 = "9e3a02d73a8f0a9be04e677dbef758aab40f8bea" +uuid = "892a3eda-7b42-436c-8928-eab12a02cf0e" +version = "0.6.1" + +[[deps.StructArrays]] +deps = ["ConstructionBase", "DataAPI", "Tables"] +git-tree-sha1 = "ad8002667372439f2e3611cfd14097e03fa4bccd" +uuid = "09ab397b-f2b6-538f-b94a-2f83cf4a842a" +version = "0.7.3" + + [deps.StructArrays.extensions] + StructArraysAdaptExt = "Adapt" + StructArraysGPUArraysCoreExt = ["GPUArraysCore", "KernelAbstractions"] + StructArraysLinearAlgebraExt = "LinearAlgebra" + StructArraysSparseArraysExt = "SparseArrays" + StructArraysStaticArraysExt = "StaticArrays" + + [deps.StructArrays.weakdeps] + Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" + GPUArraysCore = "46192b85-c4d5-4398-a991-12ede77f4527" + KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c" + LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" + SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" + StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" + +[[deps.StructTypes]] +deps = ["Dates", "UUIDs"] +git-tree-sha1 = "159331b30e94d7b11379037feeb9b690950cace8" +uuid = "856f2bd8-1eba-4b0a-8007-ebc267875bd4" +version = "1.11.0" + +[[deps.StructUtils]] +deps = ["Dates", "UUIDs"] +git-tree-sha1 = "b814d5005d6a529d740ffe06f8a86396f6501138" +uuid = "ec057cc2-7a8d-4b58-b3b3-92acb9f63b42" +version = "2.9.2" + + [deps.StructUtils.extensions] + StructUtilsLazilyInitializedFieldsExt = ["LazilyInitializedFields"] + StructUtilsMeasurementsExt = ["Measurements"] + StructUtilsStaticArraysCoreExt = ["StaticArraysCore"] + StructUtilsTablesExt = ["Tables"] + + [deps.StructUtils.weakdeps] + LazilyInitializedFields = "0e77f7df-68c5-4e49-93ce-4cd80f5598bf" + Measurements = "eff96d63-e80a-5855-80a2-b1b0885c5ab7" + StaticArraysCore = "1e83bf80-4336-4d27-bf5d-d5a4f845583c" + Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" + +[[deps.StyledStrings]] +uuid = "f489334b-da3d-4c2e-b8f0-e476e12c162b" +version = "1.11.0" + +[[deps.SuiteSparse]] +deps = ["Libdl", "LinearAlgebra", "Serialization", "SparseArrays"] +uuid = "4607b0f0-06f3-5cda-b6b1-a6196a1729e9" + +[[deps.SuiteSparse_jll]] +deps = ["Artifacts", "Libdl", "libblastrampoline_jll"] +uuid = "bea87d4a-7f5b-5778-9afe-8cc45184846c" +version = "7.8.3+2" + +[[deps.TOML]] +deps = ["Dates"] +uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" +version = "1.0.3" + +[[deps.TableTraits]] +deps = ["IteratorInterfaceExtensions"] +git-tree-sha1 = "c06b2f539df1c6efa794486abfb6ed2022561a39" +uuid = "3783bdb8-4a98-5b6b-af9a-565f29a5fe9c" +version = "1.0.1" + +[[deps.Tables]] +deps = ["DataAPI", "DataValueInterfaces", "IteratorInterfaceExtensions", "OrderedCollections", "TableTraits"] +git-tree-sha1 = "a94d9bdda1b7bed0046cea645639ab3f62196fac" +uuid = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" +version = "1.14.0" + +[[deps.Tar]] +deps = ["ArgTools", "SHA"] +uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" +version = "1.10.0" + +[[deps.TensorCore]] +deps = ["LinearAlgebra"] +git-tree-sha1 = "1feb45f88d133a655e001435632f019a9a1bcdb6" +uuid = "62fd8b95-f654-4bbd-a8a5-9c27f68ccd50" +version = "0.1.1" + +[[deps.Test]] +deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] +uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +version = "1.11.0" + +[[deps.TestItemRunner]] +deps = ["Pkg", "TOML", "Test", "TestItems", "UUIDs"] +git-tree-sha1 = "9edf3b25f95dd92da044289a5c13cbdcfa142a77" +uuid = "f8b46487-2199-4994-9208-9a1283c18c0a" +version = "1.3.2" + +[[deps.TestItems]] +git-tree-sha1 = "a6dd904babc04670c784f81b67957a3545271610" +uuid = "1c621080-faea-4a02-84b6-bbd5e436b8fe" +version = "1.1.0" + +[[deps.TiffImages]] +deps = ["CodecZstd", "ColorTypes", "DataStructures", "DocStringExtensions", "FileIO", "FixedPointNumbers", "IndirectArrays", "Inflate", "Mmap", "OffsetArrays", "PkgVersion", "PrecompileTools", "ProgressMeter", "SIMD", "UUIDs"] +git-tree-sha1 = "9ca5f1f2d42f80df4b8c9f6ab5a64f438bbd9976" +uuid = "731e570b-9d59-4bfa-96dc-6df516fadf69" +version = "0.11.9" + +[[deps.TranscodingStreams]] +git-tree-sha1 = "0c45878dcfdcfa8480052b6ab162cdd138781742" +uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa" +version = "0.11.3" + +[[deps.TriplotBase]] +git-tree-sha1 = "4d4ed7f294cda19382ff7de4c137d24d16adc89b" +uuid = "981d1d27-644d-49a2-9326-4793e63143c3" +version = "0.1.0" + +[[deps.URIs]] +git-tree-sha1 = "908fec9df6c5de98548ead82a468c95ccf6cd263" +uuid = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" +version = "1.7.0" + +[[deps.UUIDs]] +deps = ["Random", "SHA"] +uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" +version = "1.11.0" + +[[deps.Unicode]] +uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" +version = "1.11.0" + +[[deps.UnicodeFun]] +deps = ["REPL"] +git-tree-sha1 = "53915e50200959667e78a92a418594b428dffddf" +uuid = "1cfade01-22cf-5700-b092-accc4b62d6e1" +version = "0.4.1" + +[[deps.Unitful]] +deps = ["Dates", "LinearAlgebra", "Random"] +git-tree-sha1 = "1f0f9f401753701a7e4113b5056ca38d33875b55" +uuid = "1986cc42-f94f-5a68-af5c-568840ba703d" +version = "1.29.0" + + [deps.Unitful.extensions] + ConstructionBaseUnitfulExt = "ConstructionBase" + ForwardDiffExt = "ForwardDiff" + InverseFunctionsUnitfulExt = "InverseFunctions" + LatexifyExt = ["Latexify", "LaTeXStrings"] + NaNMathExt = "NaNMath" + PrintfExt = "Printf" + + [deps.Unitful.weakdeps] + ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9" + ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" + InverseFunctions = "3587e190-3f89-42d0-90ee-14403ec27112" + LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" + Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316" + NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" + Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" + +[[deps.WebP]] +deps = ["CEnum", "ColorTypes", "FileIO", "FixedPointNumbers", "ImageCore", "libwebp_jll"] +git-tree-sha1 = "aa1ca3c47f119fbdae8770c29820e5e6119b83f2" +uuid = "e3aaa7dc-3e4b-44e0-be63-ffb868ccd7c1" +version = "0.1.3" + +[[deps.WoodburyMatrices]] +deps = ["LinearAlgebra", "SparseArrays"] +git-tree-sha1 = "248a7031b3da79a127f14e5dc5f417e26f9f6db7" +uuid = "efce3f68-66dc-5838-9240-27a6d6f5f9b6" +version = "1.1.0" + +[[deps.XLSX]] +deps = ["Colors", "Dates", "OrderedCollections", "PrecompileTools", "Printf", "Random", "Tables", "UUIDs", "Unicode", "XML", "ZipArchives"] +git-tree-sha1 = "b2b8d94256b237ce2e951498455a9506bfa63089" +uuid = "fdbf4ff8-1666-58a4-91e7-1b58723a45e0" +version = "0.12.3" +weakdeps = ["FileIO", "StyledStrings"] + + [deps.XLSX.extensions] + FileIOExt = "FileIO" + StyledStringsSstsExt = "StyledStrings" + +[[deps.XML]] +git-tree-sha1 = "92e8ddd9edfd0f374ec738689cbd1d50abfbf01e" +uuid = "72c71f33-b9b6-44de-8c94-c961784809e2" +version = "0.4.6" +weakdeps = ["AbstractTrees"] + + [deps.XML.extensions] + XMLAbstractTreesExt = "AbstractTrees" + +[[deps.XML2_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Libiconv_jll", "Zlib_jll"] +git-tree-sha1 = "b826aedca5fce2e3c91904e338c279e5c1a2da74" +uuid = "02c8fc9c-b97f-50b9-bbe4-9be30ff0a78a" +version = "2.15.4+0" + +[[deps.XZ_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "e52eca002a11c30a858185efdfb15311e1c7a6bf" +uuid = "ffd25f8a-64ca-5728-b0f7-c24cf3aae800" +version = "5.8.4+0" + +[[deps.Xorg_libX11_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libxcb_jll", "Xorg_xtrans_jll"] +git-tree-sha1 = "808090ede1d41644447dd5cbafced4731c56bd2f" +uuid = "4f6342f7-b3d2-589e-9d20-edeb45f2b2bc" +version = "1.8.13+0" + +[[deps.Xorg_libXau_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "aa1261ebbac3ccc8d16558ae6799524c450ed16b" +uuid = "0c0b7dd1-d40b-584c-a123-a41640f87eec" +version = "1.0.13+0" + +[[deps.Xorg_libXdmcp_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "52858d64353db33a56e13c341d7bf44cd0d7b309" +uuid = "a3789734-cfe1-5b06-b2d0-1dd0d9d62d05" +version = "1.1.6+0" + +[[deps.Xorg_libXext_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libX11_jll"] +git-tree-sha1 = "1a4a26870bf1e5d26cd585e38038d399d7e65706" +uuid = "1082639a-0dae-5f34-9b06-72781eeb8cb3" +version = "1.3.8+0" + +[[deps.Xorg_libXfixes_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libX11_jll"] +git-tree-sha1 = "75e00946e43621e09d431d9b95818ee751e6b2ef" +uuid = "d091e8ba-531a-589c-9de9-94069b037ed8" +version = "6.0.2+0" + +[[deps.Xorg_libXft_jll]] +deps = ["Artifacts", "Fontconfig_jll", "JLLWrappers", "Libdl", "Xorg_libXrender_jll"] +git-tree-sha1 = "d893c27836da7986c3248997a2a9535e5e4d8a95" +uuid = "2c808117-e144-5220-80d1-69d4eaa9352c" +version = "2.3.9+0" + +[[deps.Xorg_libXinerama_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libXext_jll"] +git-tree-sha1 = "0ba01bc7396896a4ace8aab67db31403c71628f4" +uuid = "d1454406-59df-5ea1-beac-c340f2130bc3" +version = "1.1.7+0" + +[[deps.Xorg_libXrender_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libX11_jll"] +git-tree-sha1 = "7ed9347888fac59a618302ee38216dd0379c480d" +uuid = "ea2f1a96-1ddc-540d-b46f-429655e07cfa" +version = "0.9.12+0" + +[[deps.Xorg_libpciaccess_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Zlib_jll"] +git-tree-sha1 = "58972370b81423fc546c56a60ed1a009450177c3" +uuid = "a65dc6b1-eb27-53a1-bb3e-dea574b5389e" +version = "0.19.0+0" + +[[deps.Xorg_libxcb_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libXau_jll", "Xorg_libXdmcp_jll"] +git-tree-sha1 = "bfcaf7ec088eaba362093393fe11aa141fa15422" +uuid = "c7cfdc94-dc32-55de-ac96-5a1b8d977c5b" +version = "1.17.1+0" + +[[deps.Xorg_xtrans_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "a63799ff68005991f9d9491b6e95bd3478d783cb" +uuid = "c5fb5394-a638-5e4d-96e5-b29de1b5cf10" +version = "1.6.0+0" + +[[deps.ZipArchives]] +deps = ["ArgCheck", "CodecInflate64", "CodecZlib", "InputBuffers", "PrecompileTools", "TranscodingStreams", "Zlib_jll"] +git-tree-sha1 = "3e6acd912522133953125645b263934ae2ac745e" +uuid = "49080126-0e18-4c2a-b176-c102e4b3760c" +version = "2.6.1" + +[[deps.Zlib_jll]] +deps = ["Libdl"] +uuid = "83775a58-1f1d-513f-b197-d71354ab007a" +version = "1.3.1+2" + +[[deps.Zstd_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "446b23e73536f84e8037f5dce465e92275f6a308" +uuid = "3161d3a3-bdf6-5164-811a-617609db77b4" +version = "1.5.7+1" + +[[deps.aws_c_auth_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "aws_c_cal_jll", "aws_c_http_jll", "aws_c_sdkutils_jll"] +git-tree-sha1 = "8cab83c96af80a1be968251ce1a0548a7545484d" +uuid = "2b3700d1-4306-52e2-a478-c162f0c514be" +version = "0.9.6+0" + +[[deps.aws_c_cal_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "aws_c_common_jll"] +git-tree-sha1 = "22c0f42f4a1f0dc5dcfa8fd267c4ac407c455e7a" +uuid = "70f11efc-bab2-57f1-b0f3-22aad4e67c4b" +version = "0.9.13+0" + +[[deps.aws_c_common_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "a759cb9bf456ad792cc7898a81ae333cce9ef02a" +uuid = "73048d1d-b8c4-5092-a58d-866c5e8d1e50" +version = "0.12.6+0" + +[[deps.aws_c_compression_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "aws_c_common_jll"] +git-tree-sha1 = "7910c72f45f44afd297c39fe43b99c56d5ed22ec" +uuid = "73a04cd5-f3d7-5bac-9290-e8adb709f224" +version = "0.3.2+0" + +[[deps.aws_c_http_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "aws_c_compression_jll", "aws_c_io_jll"] +git-tree-sha1 = "3fb8685778068de502c72fec5dd8075e037cee15" +uuid = "3254fc65-9028-534d-aa9d-d76d128babc6" +version = "0.10.15+0" + +[[deps.aws_c_io_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "aws_c_cal_jll", "aws_c_common_jll", "s2n_tls_jll"] +git-tree-sha1 = "7e481d474b2087ee8bbf55b81bf9119f21e396d9" +uuid = "13c41daa-f319-5298-b5eb-5754e0170d52" +version = "0.26.3+0" + +[[deps.aws_c_s3_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "aws_c_auth_jll", "aws_c_common_jll", "aws_c_http_jll", "aws_checksums_jll", "s2n_tls_jll"] +git-tree-sha1 = "3e9917ab25114feba657e71be41cad068b9f6595" +uuid = "bd1f34fb-993f-5903-a121-aaf302eed6d4" +version = "0.11.5+0" + +[[deps.aws_c_sdkutils_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "aws_c_common_jll"] +git-tree-sha1 = "c43dfba2c1ab9ea9f02f2c80e86fa16f6460244e" +uuid = "1282aa60-004d-510b-9f52-12498d409daa" +version = "0.2.4+1" + +[[deps.aws_checksums_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "aws_c_common_jll"] +git-tree-sha1 = "2570c8e23f4771a087b12a47edcaaa670ac05a01" +uuid = "b2a88e68-78e7-5e94-8c20-c02986ec140e" +version = "0.2.10+0" + +[[deps.dlfcn_win32_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "e141d67ffe550eadfb5af1bdbdaf138031e4805f" +uuid = "c4b69c83-5512-53e3-94e6-de98773c479f" +version = "1.4.2+0" + +[[deps.gmsh_jll]] +deps = ["Artifacts", "Cairo_jll", "CompilerSupportLibraries_jll", "FLTK_jll", "FreeType2_jll", "GLU_jll", "GMP_jll", "HDF5_jll", "JLLWrappers", "JpegTurbo_jll", "LLVMOpenMP_jll", "Libdl", "Libglvnd_jll", "METIS_jll", "MMG_jll", "OCCT_jll", "Xorg_libX11_jll", "Xorg_libXext_jll", "Xorg_libXfixes_jll", "Xorg_libXft_jll", "Xorg_libXinerama_jll", "Xorg_libXrender_jll", "Zlib_jll", "libpng_jll"] +git-tree-sha1 = "4b81ac8f1fe6a1a473ac9b980de24e9143fa6211" +uuid = "630162c2-fc9b-58b3-9910-8442a8a132e6" +version = "4.15.2+0" + +[[deps.isoband_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "51b5eeb3f98367157a7a12a1fb0aa5328946c03c" +uuid = "9a68df92-36a6-505f-a73e-abb412b6bfb4" +version = "0.2.3+0" + +[[deps.libaec_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "60f4792734488db6f42e2c7699f1d4594780bd03" +uuid = "477f73a3-ac25-53e9-8cc3-50b2fa2566f0" +version = "1.1.7+0" + +[[deps.libaom_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "1210ba774d3427387d307bf1f416d699b7c39417" +uuid = "a4ae2306-e953-59d6-aa16-d00cac43593b" +version = "3.15.1+0" + +[[deps.libass_jll]] +deps = ["Artifacts", "Bzip2_jll", "FreeType2_jll", "FriBidi_jll", "HarfBuzz_jll", "JLLWrappers", "Libdl", "Zlib_jll"] +git-tree-sha1 = "cb007192783c56d8249db4cf0e3495001edfe414" +uuid = "0ac62f75-1d6f-5e53-bd7c-93b484bb37c0" +version = "0.17.5+0" + +[[deps.libblastrampoline_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "8e850b90-86db-534c-a0d3-1478176c7d93" +version = "5.15.0+0" + +[[deps.libdrm_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libpciaccess_jll"] +git-tree-sha1 = "28e57478e8a160d346a19c28b3fffb9273bcc9c2" +uuid = "8e53e030-5e6c-5a89-a30b-be5b7263a166" +version = "2.4.134+0" + +[[deps.libfdk_aac_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "646634dd19587a56ee2f1199563ec056c5f228df" +uuid = "f638f0a6-7fb0-5443-88ba-1cc74229b280" +version = "2.0.4+0" + +[[deps.libpng_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Zlib_jll"] +git-tree-sha1 = "32781be40fe86735af02eae0dd22754e4b5f779d" +uuid = "b53b4c65-9356-5827-b1ea-8c7a1a84506f" +version = "1.6.59+0" + +[[deps.libsixel_jll]] +deps = ["Artifacts", "JLLWrappers", "JpegTurbo_jll", "Libdl", "libpng_jll"] +git-tree-sha1 = "e067c8bae65bb40866552296a2d5b4dc65b8928f" +uuid = "075b6546-f08a-558a-be8f-8157d0f608a5" +version = "1.80.702+0" + +[[deps.libva_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libX11_jll", "Xorg_libXext_jll", "Xorg_libXfixes_jll", "libdrm_jll"] +git-tree-sha1 = "7dbf96baae3310fe2fa0df0ccbb3c6288d5816c9" +uuid = "9a156e7d-b971-5f62-b2c9-67348b8fb97c" +version = "2.23.0+0" + +[[deps.libvorbis_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Ogg_jll"] +git-tree-sha1 = "11e1772e7f3cc987e9d3de991dd4f6b2602663a5" +uuid = "f27f6e37-5d2b-51aa-960f-b287f2bc3b7a" +version = "1.3.8+0" + +[[deps.libwebp_jll]] +deps = ["Artifacts", "Giflib_jll", "JLLWrappers", "JpegTurbo_jll", "Libdl", "Libglvnd_jll", "Libtiff_jll", "libpng_jll"] +git-tree-sha1 = "52d3b9475133c3bc8c0a7f90f18bfc8cdb5a443a" +uuid = "c5f90fcd-3b7e-5836-afba-fc50a0988cb2" +version = "1.6.1+0" + +[[deps.mpif_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "LazyArtifacts", "Libdl", "MPIABI_jll", "MPICH_jll", "MPIPreferences", "MPItrampoline_jll", "MicrosoftMPI_jll", "OpenMPI_jll", "TOML"] +git-tree-sha1 = "a8083ee0737c243c8f40a4ba86a0956997facb73" +uuid = "9aeb927a-4695-514f-a259-621a69f20ec0" +version = "0.1.7+0" + +[[deps.nghttp2_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" +version = "1.64.0+1" + +[[deps.p7zip_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] +uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" +version = "17.7.0+0" + +[[deps.s2n_tls_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "8ee2baf330dde17c6b711cee37dc18053433ddcf" +uuid = "cddc5d3d-934d-5d3a-9747-62fc12ea3f48" +version = "1.7.10+0" + +[[deps.x264_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "14cc7083fc6dff3cc44f2bc435ee96d06ed79aa7" +uuid = "1270edf5-f2f9-52d2-97e9-ab00b5d0237a" +version = "10164.0.1+0" + +[[deps.x265_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "e7b67590c14d487e734dcb925924c5dc43ec85f3" +uuid = "dfaa095f-4041-5dcd-9319-2fabd8486b76" +version = "4.1.0+0" diff --git a/Project.toml b/Project.toml index 466fb2e5a..08cbfa760 100644 --- a/Project.toml +++ b/Project.toml @@ -1,97 +1,85 @@ name = "LineCableModels" uuid = "dffb7669-8cd4-4628-99dd-7694e356ba37" -version = "0.1.1-DEV" +version = "0.2.0" authors = ["Amauri Martins"] git_url = "https://github.com/Electa-Git/LineCableModels.jl" +[workspace] +projects = ["test"] + [deps] -CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" -Calculus = "49dc2e85-a5d0-5ad3-a950-438e2897f1b9" -Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" +Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" -DisplayAs = "0b91fe84-8a4c-11e9-3e1d-67c38462b6d6" -Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" EzXML = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615" -ForceImport = "9dda63f9-cce7-5873-89fa-eccbb2fffcde" -GetDP = "dbfd83e6-7aba-450b-9c2b-93ccd973023a" -Gmsh = "705231aa-382f-11e9-3f0c-b7cb4346fdeb" +GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" -LoggingExtras = "e6f89c97-d47a-5376-807f-9c37f3926c36" -MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" -Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" -Measurements = "eff96d63-e80a-5855-80a2-b1b0885c5ab7" -NLsolve = "2774e3e8-f4cf-5e23-947b-6d7e65073b56" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" QuadGK = "1fd47b50-473d-5c70-9696-f719f8f3bcdc" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -Reexport = "189a3867-3050-52da-a836-e630ba90ab69" +RequiredInterfaces = "97f35ef4-7bc5-4ec1-a41a-dcc69c7308c6" +SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" -StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" -Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" +TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" -XLSX = "fdbf4ff8-1666-58a4-91e7-1b58723a45e0" [weakdeps] +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +Calculus = "49dc2e85-a5d0-5ad3-a950-438e2897f1b9" +Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" +Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" +Gmsh = "705231aa-382f-11e9-3f0c-b7cb4346fdeb" +Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" +Measurements = "eff96d63-e80a-5855-80a2-b1b0885c5ab7" WGLMakie = "276b4fcb-3e11-5398-bf8b-a0c2d153d008" +XLSX = "fdbf4ff8-1666-58a4-91e7-1b58723a45e0" -[sources.GetDP] -rev = "main" -url = "https://github.com/Electa-Git/GetDP.jl" +[extensions] +LineCableModelsCairoMakieExt = ["Makie", "CairoMakie"] +LineCableModelsDistributionsExt = "Distributions" +LineCableModelsGLMakieExt = ["Makie", "GLMakie"] +LineCableModelsGmshExt = "Gmsh" +LineCableModelsMakieExt = ["Makie", "Colors"] +LineCableModelsMeasurementsExt = ["Measurements", "Calculus"] +LineCableModelsWGLMakieExt = ["Makie", "WGLMakie"] +LineCableModelsXLSXExt = "XLSX" [compat] -Aqua = "0.8" +Base64 = "1.11" CairoMakie = "0.15" Calculus = "0.5.2" Colors = "0.13.0" DataFrames = "1.7.0" Dates = "1.11.0" -DisplayAs = "0.1.6" Distributions = "0.25.122" DocStringExtensions = "0.9.3" EzXML = "1.2.0" -ForceImport = "0.0.3" GLMakie = "0.13" -GetDP = "0.1" +GeometryBasics = "0.5" Gmsh = "0.3.1" JSON3 = "1.14.2" LinearAlgebra = "1.11.0" Logging = "1.11.0" -LoggingExtras = "1.1.0" -MacroTools = "0.5.16" -Makie = "0.24.6" +Makie = "0.24.11" Measurements = "2.11.0" -NLsolve = "4.5.1, 5.0" -Pkg = "1.11.0" -Plots = "1.40.9" +Pkg = "1.12" Printf = "1.11.0" QuadGK = "2.11.2" Random = "1.11.0" -Reexport = "1.2.2" +RequiredInterfaces = "0.1.7" +SHA = "0.7" Serialization = "1.11.0" SpecialFunctions = "2.5.0" Statistics = "1.11.1" -StatsBase = "0.34.7" -Tables = "1.12.1" -Test = "1.11.0" -TestItemRunner = "1.1.0" -UUIDs = "1.11.0" +TOML = "1.0" +UUIDs = "1.11" WGLMakie = "0.13" -XLSX = "0.10.4, 0.12" +XLSX = "0.12" julia = "1.12" - -[extras] -Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" - -[targets] -test = ["Test", "TestItemRunner", "Aqua"] diff --git a/README.md b/README.md index 52b95b86e..566718330 100644 --- a/README.md +++ b/README.md @@ -4,55 +4,207 @@ [![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://electa-git.github.io/LineCableModels.jl/dev/) [![Build Status](https://github.com/Electa-Git/LineCableModels.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/Electa-Git/LineCableModels.jl/actions/workflows/CI.yml?query=branch%3Amain) -[![License](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) +[![License](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](LICENSE) [![codecov](https://codecov.io/gh/Electa-Git/LineCableModels.jl/graph/badge.svg?token=6H12DDBZ0T)](https://codecov.io/gh/Electa-Git/LineCableModels.jl) -`LineCableModels.jl` is a Julia package for computing the electrical parameters of arbitrary arrangements of underground and overhead power cables, with built-in uncertainty quantification. It is designed as a general-purpose and scalable toolbox to calculate transmission line parameters and to construct models for steady-state analysis and electromagnetic transient (EMT) simulations. - -## Main features +LineCableModels.jl computes electrical parameters for underground and overhead +power cables and supports uncertainty propagation through cable geometry and +material data. -- **Comprehensive cable modeling:** Detailed representation of conductors (solid, tubular, stranded), insulation layers, screens, armoring, and semicons. -- **Line and cable constants:** Accurate DC and AC parameters (R, L, C, G) with correction factors for temperature, stranding, and helical effects. -- **Propagation characteristics:** Rigorous electromagnetic models for cable internal impedances and earth-return paths. -- **Multiple solvers:** Analytical formulations, finite element modeling, and interfaces to EMT programs, including ATPDraw and PSCAD. -- **Materials and cables library:** Store and reuse standardized material properties and cable designs across projects. +## Features -## Documentation +- Solid, tubular, stranded, screened, and armored cable models. +- Frequency-dependent series impedance and shunt admittance calculations. +- Earth-return calculations, modal transformations, and ATPDraw/PSCAD data + exchange. +- Material and cable libraries with JSON import and export. +- `Grid` and `Gridspace` for deterministic and uncertain designs. +- Ordinary, combinatorial, and conditional Monte Carlo execution through + `compute`. +- Optional Makie plotting through Julia package extensions. -See the [full documentation](https://electa-git.github.io/LineCableModels.jl/) for detailed usage instructions, technical background, and examples. +## Installation -## Usage - -Clone the package and add to the Julia environment: +After version 0.2.0 is accepted into the Julia General registry: ```julia-repl -pkg> add https://github.com/Electa-Git/LineCableModels.jl.git +pkg> add LineCableModels ``` -If you are using the finite-element solver, it is recommended to run the build script to retrieve the binaries needed by the [GetDP.jl](https://github.com/Electa-Git/GetDP.jl) front-end: +Until registration is complete, install the release commit from GitHub: ```julia-repl -pkg> build LineCableModels +pkg> add https://github.com/Electa-Git/LineCableModels.jl ``` -Then, in your Julia code, import the package: +Core usage has no plotting dependency: ```julia using LineCableModels ``` -For application examples, please refer to the [tutorials section](https://electa-git.github.io/LineCableModels.jl/) and the [examples folder](examples). +The physical model is built directly from materials, primitives, and cable +parts: + +```julia +copper = Material(; kind=:conductor, rho=1.7241e-8) +xlpe = Material(; kind=:insulator, rho=1.97e14, eps_r=2.5) + +origin = Stack( + Group(:core, core(copper; r=10e-3, tag=:core_metal)), + insulation(xlpe; t=8e-3), +) +design = build(CableDesign, "example", origin) + +constants = CableConstants(design) +``` + +Scalar `build` calls return completed domain objects. An explicit `Grid` at a +construction call returns a `Gridspace` whose points invoke the same +scalar `build` method. + +## Optional plotting + +Load one Makie backend explicitly before calling `preview` or `plot`: + +```julia +using LineCableModels +using CairoMakie + +plot_pages = plot(line_parameters) # R, X, G, B matrix-dashboard pages +export_svg(plot_pages[1]; path = "series_resistance.svg") + +self_impedance = plot(line_parameters, @observe Z[1, 1, :]) +# `self_impedance` is one UIPlot with separate R and X axes. +``` + +`GLMakie` and `WGLMakie` are supported in the same way. Load the backend you want +for display and also `import CairoMakie` if SVG export is needed. The optional +`backend=:cairo`, `:gl`, or `:wgl` keyword selects a backend for one call. Observation requests determine +physical quantity and coordinate axes. `layout` only groups those facets into +figures. Singular recipes return a `UIPlot`. Calls that produce several figures +return a vector. Each handle exposes native Makie objects that remain +caller-owned. `figure_title` names the whole figure, `panel_titles` names its +logical axes, and `series_labels` names overlaid result containers in legends. +Matrix coordinates belong in semantic axis titles, never legend entries. +Legends accept outer docks or +`legend_position=:inside` with an anchor such as `:rt`. `figurelegend!`, +`panellegend!`, `figuretitle!`, and `paneltitle!` can change those native blocks +after construction. The Export SVG control appears only when CairoMakie is loaded +before plot construction. It saves the current live state without switching +display backends. For GL interactivity with export, import both backends and +select `backend=:gl`. After loading CairoMakie later, `export_svg` also works on +existing plots. Recreate the plot to add its button. + +## Result access + +`CableConstants` stores R/L/C values per meter. `LineParameters` +stores its frequency domain and either a `:pul` or `:total` basis: + +```julia +basis(line_parameters) +@observe line_parameters R[1, 1, :] # complete frequency response +@observe line_parameters Z[1, 1, 2:5] # selected frequency samples +@observe line_parameters (Z, abs)[1, 2, :] + +label(R) # "Series resistance" +symbol(Z, angle) # "∠Z" +label(display_unit(R, :pul)) # "Ω/km" +``` + +Complete parameter traversals return `ParametricResult{T}`, including a space +with cardinality one. `Formulation`, `CableConstantsFormulation`, and +`ModalTransformationFormulation` accept explicit `Grid` fields. Combinatorial +calculation forms the Cartesian product of problem and formulation points and +retains both axes for `result(run, problem_index, formulation_index)` lookup. +Conditional Monte Carlo propagation returns +`MonteCarloResult{T}`. Use `statistics`, `samples`, `histograms`, and +`uncertain` to inspect stored calculation data. Load `Measurements` before +computation: MC stores marginal Measurements built from accepted sample means +and sample standard deviations, without joint output correlations. `uncertain` +and downstream transport reuse those stored values. Result order is +problem-index-fastest within formulation order. Parametric, linear-error, and +Monte Carlo results are ordinary finite collections: indexing and iteration +return stored core results, and Base +`first`, `last`, `only`, `collect`, `map`, and `zip` retain their standard +meanings. +Select the statistics to include in a marginal summary before converting it +to a table: + +```julia +using DataFrames + +summary = DataFrame(observables( + monte_carlo_result, + ((statistics, R, 1), (statistics, L, 1), + (statistics, G, 1), (statistics, C, 1)), +)) +``` + +After loading a Makie package, `Makie.hist`, `Makie.stairs`, `Makie.ecdfplot`, +`Makie.lines`, and `Makie.qqplot` display retained distribution information. + +Higher-order calculations keep supplemental computation output separate from +their scientific products. `details(result)` returns an empty `ComputationDetails` by +default. Its payload is available through `.data`. Construct `Combinatorial`, `LinearError`, or `MonteCarlo` with +`options=(retain_details=true,)` only when the core computation owner has +registered a `computation_details` method and those records are needed. +Monte Carlo uses strict failure propagation by default. Physically unsupported +draws can be rejected explicitly with +`options=(retain_details=true, on_error=:resample, max_failures=100)`. Only +`DomainError` permits resampling. Retained details report every rejected argument +tuple and its error summary. The resulting statistics are conditional on a +successful realization. + +Physics and numerical-method choices belong to `Formulation`. Execution choices +are passed as a named tuple. For a materialized line system, pass +`options=(output_basis=:total,)` to `compute` to scale both Z and Y by the line +length. Composite calculations select their operation explicitly, such as +`compute(ParametricProblem(space), Combinatorial(Formulation()))`. + +Write results to an XLSX workbook with `report`: + +```julia +using XLSX + +artifact = report( + XLSXReportDefinition(file_name="line_parameters.xlsx"), + line_parameters, +) +artifact.output +``` + +XLSX is an optional dependency. Loading it activates the workbook writer. The +established `export_data(:xlsx, line_parameters; ...)` call delegates to the +same report and returns its output path. Relative paths and the default +`ZY_export.xlsx` resolve from the caller's current working directory. + +## Finite-element calculations + +Load `Gmsh` to activate the optional `LineCableModelsFEM` formulation, which +uses Gmsh for meshing and GetDP for field calculations. The geometry model +includes sector-shaped conductors. Supported geometry and material assumptions +depend on the selected formulation. See the [FEM guide](docs/src/fem.md) +and [cable data model](docs/src/data-model.md). + +See the [documentation](https://electa-git.github.io/LineCableModels.jl/) and +[examples](examples) for model construction and parameter calculations. -## License +## License and citation -The source code is provided under the [BSD 3-Clause License](LICENSE). +LineCableModels.jl is distributed under the [BSD 3-Clause License](LICENSE). +The optional FEM backend invokes third-party GetDP under its own license. See +[Third-party notices](THIRD_PARTY_NOTICES.md). Citation metadata is provided in +[CITATION.cff](CITATION.cff). -## Acknowledgements +## Acknowledgments -This work is supported by the Etch Competence Hub of EnergyVille, financed by the Flemish Government. The primary developer is Amauri Martins ([@amaurigmartins](https://github.com/amaurigmartins)). +This work is supported by the Etch Competence Hub of EnergyVille, financed by +the Flemish Government. The primary developer is Amauri Martins +([@amaurigmartins](https://github.com/amaurigmartins)). -

-


Etch logo

-

EV logo

-

KUL logo

+

+
Etch logo +
EnergyVille logo +
KU Leuven logo

diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 000000000..648d16049 --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,20 @@ +# Third-party notices + +## GetDP + +The optional Gmsh/GetDP finite-element backend can download and execute GetDP +3.5.0 from the package's lazy `getdp` artifact. GetDP is copyright (C) +1997–2022 P. Dular and C. Geuzaine, University of Liege, and is distributed +under the GNU General Public License, version 2 or later. + +- Project: https://getdp.info/ +- Source: https://getdp.info/src/getdp-3.5.0-source.tgz +- Source SHA-256: `d6814dc3f81431f1db30b3d5318553efab616d7ea53b352a2c2d0640d130a328` +- License: https://getdp.info/doc/texinfo/getdp.html#License + +The upstream binary archives bound by `Artifacts.toml` include `LICENSE.txt`, +`CREDITS.txt`, and `README.txt`. LineCableModels invokes GetDP as an external +program. GetDP is not incorporated into the LineCableModels library. + +Maintainers can revalidate every supported archive, tree hash, executable and +license file with `julia test/manual/verification/verify_getdp_artifact.jl`. diff --git a/TODO.md b/TODO.md deleted file mode 100644 index e044b1574..000000000 --- a/TODO.md +++ /dev/null @@ -1,19 +0,0 @@ -# TODO for LineCableModels.jl - -This is a living document intended to track scientific development priorities and research directions for new features, methods and solutions to be included in the package. - -For bugs, features and implementation taks, the [Issues](https://github.com/Electa-Git/LineCableModels.jl/issues) page is used. - -## Wishlist - -- [ ] Pipe-type cables and MoM-SO implementation. - -## In progress - -- [ ] Implementation of frequency-dependent soil properties. -- [ ] Development of novel formulations for cables composed of N concentrical layers, allowing for accurate representations of semiconductor materials. -- [ ] Implementation of an interface to run finite element simulations using [Onelab](https://onelab.info/). - -## Done ✓ - -- [x] Object-oriented data model for cables, conductors, insulations and materials. diff --git a/binder/Project.toml b/binder/Project.toml deleted file mode 100644 index 0bdc76be6..000000000 --- a/binder/Project.toml +++ /dev/null @@ -1,30 +0,0 @@ -name = "Showcase" - -[deps] -Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" -WebIO = "0f1e0344-ec1d-5b48-a673-e5cf874b6c29" -Pluto = "c3e4b0f8-55cb-11ea-2926-15256bba5781" -PlutoUI = "7f904dfe-b85e-4ff6-b463-dae2292396a8" -GetDP = "dbfd83e6-7aba-450b-9c2b-93ccd973023a" -LineCableModels = "dffb7669-8cd4-4628-99dd-7694e356ba37" -CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" -DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" -LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" -Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" -MathTeXEngine = "0a4f8689-d25c-4efe-a92b-7142dfc1aa53" -Measurements = "eff96d63-e80a-5855-80a2-b1b0885c5ab7" -Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -PrettyTables = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d" -Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" -Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" -WGLMakie = "276b4fcb-3e11-5398-bf8b-a0c2d153d008" -FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" -PlutoStyles = "16e07271-a41c-4e3e-b73a-b807cf2ea0eb" - -[sources] -GetDP = {rev = "main", url = "https://github.com/Electa-Git/GetDP.jl"} -LineCableModels = {path = ".."} - - -[compat] -julia = "1.12" diff --git a/binder/postBuild b/binder/postBuild deleted file mode 100644 index 76f9d749f..000000000 --- a/binder/postBuild +++ /dev/null @@ -1,78 +0,0 @@ -#!/bin/bash -set -euxo pipefail - -# TODO: Make this stupid Binder+Pluto setup bow to my will. Need to find some way to add the necessary (unregistered) packages to whatever is the default env under jupyterlab -> pluto. - -julia -e ' -using Pkg, TOML - -# 1) Activate the default versioned env, not a folder named "@v#.#" -Pkg.activate("@v#.#") - -# 2) Develop the local repo (assume postBuild is running at repo root) -try - Pkg.develop(path=".") -catch e - @warn "Pkg.develop(path=\".\") failed" exception=(e, catch_backtrace()) -end - -# Helper to add deps from a Project.toml -function add_deps_from(project_file::String) - if !isfile(project_file) - @info "No $project_file found"; return - end - proj = TOML.parsefile(project_file) - deps = get(proj, "deps", Dict{String,Any}()) - compat = get(proj, "compat", Dict{String,Any}()) - extras = get(proj, "extras", Dict{String,Any}()) # in case you keep dev/test stuff here - targets = get(proj, "targets", Dict{String,Any}()) - - # Collect package names to install (deps + optionally selected extras) - names = Set{String}(keys(deps)) - - # If you keep things like IJulia/Pluto in [extras] with a "binder" target, include them - if haskey(targets, "binder") && isa(targets["binder"], Vector) - for extra in targets["binder"] - extra in keys(extras) && push!(names, extra) - end - end - - for name in sort(collect(names)) - # Skip your own devved package if it appears in deps - if name == "LineCableModels" - continue - end - ver = get(compat, name, nothing) - spec = ver === nothing ? Pkg.PackageSpec(name=name) : - Pkg.PackageSpec(name=name, version=ver) - try - Pkg.add(spec) - catch e - @warn "Pkg.add failed for $name" exception=(e, catch_backtrace()) - end - end -end - -# Prefer binder/Project.toml, fall back to repo root -add_deps_from("binder/Project.toml") -add_deps_from("Project.toml") - -# 3) Nice-to-haves commonly needed in Binder + Pluto -try - Pkg.add(["IJulia", "Pluto"]) -catch e - @warn "Optional adds failed" exception=(e, catch_backtrace()) -end - -# 4) Precompile for faster startup -Pkg.precompile() - -# 5) Make sure the IJulia kernel points to the default env (useful in JupyterLab) -try - using IJulia - envpath = Base.load_path_expand("@v#.#") - IJulia.installkernel("Julia (@v#.#)", env=envpath) -catch e - @warn "IJulia kernel install failed (non-fatal)" exception=(e, catch_backtrace()) -end -' diff --git a/binder/requirements.txt b/binder/requirements.txt deleted file mode 100644 index b8cc81228..000000000 --- a/binder/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -jupyterlab>=4 -jupyter-server-proxy>=4 -jupyter-pluto-proxy==0.1.1 \ No newline at end of file diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index ba3f00680..000000000 --- a/codecov.yml +++ /dev/null @@ -1,3 +0,0 @@ -ignore: - - "LineCableModels/src/legacy/**" - diff --git a/docs/Project.toml b/docs/Project.toml index e647e1322..96022d8cc 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,14 +1,32 @@ [deps] -Changelog = "5217a498-cd5d-4ec6-b8c2-9b85a09b6e3e" +BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" DocumenterCitations = "daee34ce-89f3-4625-b898-19384cb65244" +Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" +GeoInterface = "cf35fbd7-0cd7-5166-be24-54bfbe79505f" +GeoJSON = "61d90e0f-e114-555e-ac52-39dfb47a3ef9" +LineCableModels = "dffb7669-8cd4-4628-99dd-7694e356ba37" Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" +Measurements = "eff96d63-e80a-5855-80a2-b1b0885c5ab7" + +[sources] +LineCableModels = {path = ".."} [compat] -Changelog = "1" +BenchmarkTools = "1" +AbstractTrees = "0.4" +CairoMakie = "0.15" +CSV = "0.10" DataFrames = "1" Documenter = "1" DocumenterCitations = "1" +GeoInterface = "1.5" +GeoJSON = "0.8" +LineCableModels = "0.2" Literate = "2" +Measurements = "2.11.0" julia = "1.12" diff --git a/docs/deploy.jl b/docs/deploy.jl new file mode 100644 index 000000000..e633b932e --- /dev/null +++ b/docs/deploy.jl @@ -0,0 +1,8 @@ +using Documenter + +deploydocs(; + repo = "github.com/Electa-Git/LineCableModels.jl.git", + devbranch = "main", + versions = ["stable" => "v^", "dev" => "main"], + branch = "gh-pages" +) diff --git a/docs/doctest.jl b/docs/doctest.jl new file mode 100644 index 000000000..e5ebc09d5 --- /dev/null +++ b/docs/doctest.jl @@ -0,0 +1,13 @@ +using Documenter +using LineCableModels + +DocMeta.setdocmeta!( + LineCableModels, + :DocTestSetup, + quote + using LineCableModels + using LineCableModels.DataModel.BaseParams + end; + recursive = true +) +doctest(LineCableModels) diff --git a/docs/instantiate.jl b/docs/instantiate.jl new file mode 100644 index 000000000..73a864499 --- /dev/null +++ b/docs/instantiate.jl @@ -0,0 +1,4 @@ +using Pkg + +Pkg.activate(@__DIR__) +Pkg.instantiate() diff --git a/docs/literate/plotting.jl b/docs/literate/plotting.jl new file mode 100644 index 000000000..8ed5cc1f4 --- /dev/null +++ b/docs/literate/plotting.jl @@ -0,0 +1,1325 @@ +# # Makie plotting: implementation guide and complete gallery + +# Plot cable geometry, electrical parameters, and uncertainty results with Makie. +# Each example shows the call that produces its figure. The returned `UIPlot` +# contains native Makie objects that you can modify directly. + +using LineCableModels +using CairoMakie +using LinearAlgebra: diag +using Measurements: measurement +using Statistics: mean + +# ## Figures, titles, and legends + +# A call that produces one figure returns a [`UIPlot`](@ref). A call that +# produces several figures returns `Vector{UIPlot}`. The `figure`, `axes`, +# `controls`, `legend`, `panel_legends`, and `colorbars` fields expose the +# displayed Makie objects. + +# ### Naming titles and legends + +# Layout text has one name per scope. These keywords are presentation metadata. +# The result tensors and geometry tags remain unchanged. +# +# | Keyword | Scope | +# |:--|:--| +# | `figure_title` | One visible title above the whole native figure | +# | `title_attributes` | Native Makie `Label` attributes for `figure_title` | +# | `panel_titles` | Axis-title overrides by position, request, or panel identifier | +# | `legend_title` | Heading of the controlled figure legend | +# | `series_labels` | Names of the overlaid gridpoints or physical coordinates. These are the legend entries | +# | `series_attributes` | Native Makie attributes for all overlaid curves, or one named tuple per curve | +# | `legend_position` | `:inside`, a named outer dock, or a positive dock grid position | +# | `legend_attributes` | Native `halign`/`valign`, orientation, banks, fonts, and padding | +# +# Some established recipes also accept `title` for the window or exported file, +# or as the heading of one recipe. Use the explicit scoped names above when composing a +# dashboard. +# Benchmark windows default to `case ID — quantity`, followed by block indices +# when split. Deterministic, mean ± std, and explicit-statistic comparisons use +# this same rule. An explicit `title` overrides the default case prefix. Subplot +# titles and `figure_title` remain independent. +# +# `series_attributes` uses the shared PlotBuilder controls for matrix and benchmark +# plots, observed results, statistical plots, and geometry previews. +# A named tuple applies to every group. A tuple or vector of named tuples styles +# each group separately. +# `series_attributes=((marker=:circle, markersize=8), (;), (linestyle=:dash,))` +# adds markers to the first series, keeps the second's defaults and dashes the +# third. Styles apply across facets and pages, including their legends and +# visibility controls. Attributes must be supported by the group's native plots. +# In `plotwindow`, groups follow native plot insertion order across its axes. +# +# The same scopes are mutable after construction. `figuretitle!` and +# `paneltitle!` replace titles. `figurelegend!` and `panellegend!` rebuild a +# native legend from the plots in each visibility group, preserving grouped +# visibility controls when labels change. + +# ### Observation, facet, page, and legend semantics + +# `LineParameters` owns dense ``Z`` and ``Y`` tensors. The observation grammar +# is what turns those tensors into displayable physical quantities: ``Z`` +# expands to ``R`` then ``X``. ``Y`` expands to ``G`` then ``B``. An exact +# request such as `@observe Z[1,1,:]` keeps its conductor coordinates while +# selecting the complete frequency range. Plotting consumes that resolved +# request and displays both components. +# The plot-facing name for this ordinate selection is `ydata`. Pass it +# positionally or as a keyword, such as `plot(result; ydata=(R, L))`. +# +# With matrix gridpoint overlays, every `(quantity, row, column)` has one axis. +# Its default title uses `Units.label` and the retained coordinate labels, such as `Self series resistance — conductor 1` or +# `Mutual series reactance — conductor 1 → 2`. With gridpoint overlays, +# coordinates identify panels and legends distinguish result sets. With +# coordinate overlays, each selected coordinate is a labelled curve. One +# unlabeled curve has no legend by default. +# +# `layout` is the only nominal panel capacity. Every selected quantity or +# statistical meaning has its own figure family. Omitted layout chooses capacity +# independently for each family. An explicit `(rows, columns)` sets the same +# nominal capacity for all families. +# +# | `layout` | Page grouping | +# |:--|:--| +# | `nothing` | Each family's selected matrix span, or a near-square flow capacity | +# | `(1, 1)` | One selected panel per figure, per quantity | +# | `(1, 2)` | Up to two panels in one row per page | +# | `(2, 1)` | Up to two panels in one column per page | +# | `(N, N)` | Up to `N²` panels per figure, per quantity | +# +# Layout follows observation selection. It cannot add an excluded coefficient or +# merge different quantities. `overlay` controls the panel and curve dimensions: +# +# | `overlay` | Panels | Curves | +# |:--|:--|:--| +# | `:auto` | Mode panels for several gridpoints or a larger explicit layout. Matrix coefficient panels otherwise | Gridpoints | +# | `:auto` for one modal vector with omitted layout or `(1,1)` | One panel per component | Selected modes | +# | `:gridpoints` | Selected coordinates | Gridpoints | +# | `:coordinates` | Selected gridpoints | Selected coordinates | +# | `:rows` (matrices) | Selected columns, with separate figures per gridpoint | Selected rows | +# +# Filtering occurs before this choice. An explicit reference counts as another +# gridpoint. Gridpoint overlays retain equivalent-result grouping. Coordinate +# overlays retain a panel for every selected point. Tuple/vector labels and styles +# address the overlaid dimension, so incompatible mixed families need separate +# calls. A shared style NamedTuple applies to every curve. +# +# `overlay=:rows` also retains every selected point, including equivalent results +# and a reference, in its own figure sequence. For Tv/Ti, columns are modes and +# rows are conductors: each panel title identifies its mode and its legend lists +# conductors. Figure titles use compact gridpoint descriptions. Other matrices +# use the same operation with their own physical row and column labels. +# +# Row-overlay panels follow selected column order, left to right and then top to +# bottom. `layout=(r,c)` applies the same block capacity to each point. Pagination +# restarts at each point, even when the previous page has empty cells. Automatic +# capacity uses the selected column count per point. Panel titles and controls +# accept `(original_point_position, original_column)` addresses. An explicit +# reference uses its appended input position. Row colors and markers remain +# consistent across columns, points, references and reordered selections. +# Positional `series_labels` and `series_attributes` follow selected row order. +# Use separate calls for matrix and vector quantities with this explicit option. + +# ### Matrix pagination and overlays +# +# `plot(results; ydata=(R,L), layout=(2,2))` produces four R pages and four L +# pages for a full 3×3 matrix. Their actual extents are 2×2, 2×1, 1×2, and 1×1. +# Pages follow request order and then row-major matrix-page order. Original +# coefficient identities remain the addresses for titles, legends, scales, and reset. +# Positional `panel_titles` bind to the complete selected population before paging. +# +# Automatic matrix pages cover the selected original-coordinate rectangle. Consider this example: selecting columns 2 and 3 gives a two-column overview without splitting +# at the original block edge. Explicit capacities keep blocks anchored at +# original coordinate (1,1). Both remove unselected exterior tracks and retain +# internal holes: selecting (1,1) and (1,3) still spans three columns. +# +# Native decoration measurements establish equal initial data frames. `fig_size` +# is the reference size for the nominal capacity. `figure.size` takes precedence. +# Legends use at most the fraction specified by `legend_cap=0.5` +# of their associated data area. Extra entries become `(...)` within that +# allocation. All curves remain plotted. +# Each finished window fits its actual occupied panels, titles, guides, and +# enabled controls. A singleton with explicit `layout=(1,2)` retains the smaller +# frame from that two-column reference, without allocating an empty second cell. +# Each page can be resized independently in both dimensions. +# Explicit diagonal observations and preview collections use the same capacity, +# preserving their source order and original identities in compact flow pages. +# +# Result overlays use solid lines with sparse, staggered markers on saved +# sample points: black curves and hollow circles for references, colored curves +# and filled shapes for results. References may themselves include uncertainty. +# Default routes and explicit implementations have the same styling semantics. +# Deterministic references mark both endpoints. +# Colors and marker identities remain stable +# when formulations are filtered. No curves are merged because they agree. +# Use `series_attributes=(marker=nothing,)` for lines only, or an explicit native +# `marker` for all-sample placement. References remain comparison operands, not +# declarations of physical truth. +# +# With multiple displayed series, `errorbar_sampling=:staggered` selects sparse error +# bars at retained samples separately from automatic markers. Both references +# and results retain their full mean curves. Coordinates, uncertainties, +# comparison calculations, and full-data axis limits are unchanged. X and Y +# intervals use the same indices. When very few samples are available, intervals +# take priority over conflicting automatic markers. The legend retains identity. +# Use `errorbar_sampling=:all` to inspect every interval, with automatic markers +# omitted on uncertain series. One displayed series uses `:all` by default. +# Explicit native markers still use +# all samples. Native `whiskerwidth` and `linewidth` overrides take priority. +# Sparse glyphs are an overview: use full intervals or separate standard-deviation +# curves to inspect uncertainty variation. Mean ± std is not mean ± standard error. +# Many exactly coincident methods cannot all remain distinguishable at finite +# screen resolution. Use legend visibility to inspect them separately. +# +# Within one identified physical point, observation-owned groups may share a trace +# for equivalent quantity-relevant selections: impedance choices on Z/R/L/X pages, +# admittance choices on Y/G/C/B pages. Every relevant composite route, control, +# coordinate and uncertainty meaning participates. Equal curves or descriptions +# alone never merge cases. Different physical points remain separate. Conflicting +# observations under the same selection raise an error. Saved results remain intact. +# Default labels show only relevant differences across results and reference, +# omitting common physical inputs and individual controls. Names are captured from +# owner-dispatched `description(...; compact=true)` methods used by report tables: +# `FEM (reference)`, `PSCAD (reference)`, `Monte Carlo (reference)`, and results +# such as `LEP` or `earth Z=Saad`, without result numbering. +# `formulations=[3,1]` selects recorded original formulation identities, preserving +# order and colors. Explicit `series_labels` and styles follow the retained source. +# When nothing varies, labels use the applicable compact owner description. +# Automatic results are chromatic. The separate reference is black. +# Full scientific explanations and common settings remain in `formula_details`. +# +# Top/bottom legends fit a measured row-major grid and wrap long labels without +# dropping fields. Explicit `legend_attributes=(orientation=..., nbanks=...)` +# retains native manual layout control. + +# ## Gallery data + +# The frequency responses are deliberately small but non-constant so that +# logarithmic axes, engineering units, legends, and automatic limits are all +# visible in the generated documentation. + +frequency = collect(10.0 .^ range(1, 4; length = 24)); +angular_frequency = reshape(2π .* frequency, 1, 1, :); +resistance = cat( + ( + [1.0 0.22; 0.22 1.8] .* 1.0e-4 .* (1 + 0.12 * log10(f / first(frequency))) + for f in frequency + )...; + dims = 3); +inductance = cat( + ( + [2.0 0.28; 0.28 2.5] .* 1.0e-7 .* (1 - 0.04 * log10(f / first(frequency))) + for f in frequency + )...; + dims = 3); +conductance = cat( + ( + [3.0 -0.45; -0.45 4.0] .* 1.0e-9 .* (1 + 0.08 * log10(f / first(frequency))) + for f in frequency + )...; + dims = 3); +capacitance = repeat([4.0 -0.7; -0.7 5.0] .* 1.0e-10, 1, 1, length(frequency)); +parameters = LineParameters( + complex.(resistance, inductance .* angular_frequency), + complex.(conductance, capacitance .* angular_frequency), + frequency +); + +# The geometry gallery uses the example library included in the repository. +# A system is assembled from two placements without introducing a plotting-only +# geometry representation. + +cable_library = CablesLibrary(); +LineCableModels.load!( + cable_library; + file_name = joinpath(pkgdir(LineCableModels), "examples", "cables_library.json") +); +mv_design = cable_library["18kV_1000mm2"]; +hv_design = cable_library["525kV_1600mm2"]; +earth = EarthModel(100.0, 10.0, 1.0); +cable_system = build( + LineCableSystem, + [mv_design, mv_design], + [(-0.06, -0.20), (0.06, -0.20)]; + environment = earth, + system_id = "two-cable-gallery", + line_length = 1_000.0 +); + +# The following synthetic samples, moments, and histograms illustrate the +# UQ plotting methods. +retained_samples = ( + R = reshape([2.0, 3.0, 5.0, 8.0] .* 1e-4, 1, :), + L = reshape([11.0, 13.0, 17.0, 19.0] .* 1e-7, 1, :), + C = reshape([23.0, 29.0, 31.0, 37.0] .* 1e-11, 1, :), + G = reshape([41.0, 43.0, 47.0, 53.0] .* 1e-10, 1, :) +); +retained_statistics = map(x -> [SampleSummary(vec(x))], retained_samples); +retained_histograms = map(x -> [HistogramDensity(vec(x); bins = 2)], retained_samples); +retained_core = CableConstants(mean(retained_samples.R), mean(retained_samples.L), + mean(retained_samples.C), mean(retained_samples.G)); +retained_core = LineCableModels.materialize(retained_core, retained_statistics); +mc_formulation = MonteCarlo(Formulation(); trials = 4, seed = 41, + return_samples = true, return_histograms = true); +mc_result = MonteCarloResult(mc_formulation, [retained_core], [retained_statistics], + [retained_samples], [retained_histograms], UInt64(41), UInt64[42], [4]); + +# ## Line-parameter recipes + +# ### Complete default view + +# `Makie.plot(parameters)` is the minimal call. It observes everything in the +# order ``Z`` then ``Y``, expands those families to ``R``, ``X``, +# ``G``, and ``B``, and returns four matrix-dashboard pages. For this 2×2 +# result, every page contains four axes. The calls below render the full default +# gallery in exactly that order. + +default_line_pages = Makie.plot( + parameters; + backend = :cairo, # choose the already-loaded native backend + display_plot = false, # Documenter owns display. Interactive use may omit this + controls = false, # omit toolbar chrome from this static gallery + xscale = :log10, # initial scale. Interactive controls may change it + fig_size = (900, 680) # reference size of the nominal capacity +) +default_line_pages[1].figure #hide +default_line_pages[2].figure #hide +default_line_pages[3].figure #hide +default_line_pages[4].figure #hide + +# To change only appearance, mutate the returned native axes. To change which +# physical values exist, pass an observation selector or exact `@observe` +# request. To change pagination, pass one of the layouts in the table above. + +# ### Cartesian series impedance + +# This exact observation selects the self impedance of conductor 1 over the +# full frequency range. `ObservedResult` construction expands ``Z`` into resistance and +# reactance. They remain separate quantity figures, even with a multi-panel layout. + +## `xscale=:log10` is an initial state. The live toolbar can still change it. +## `figure_title` labels each figure. `panel_titles` follows the selected quantity order. +## `series_labels` names result sets, so supplying one explicitly opts into a legend. +## `:inside` overlays the logical frame region with native alignment attributes. +series_cartesian = Makie.plot( + parameters, + @observe Z[1, 1, :]; # self impedance, conductor 1, every frequency + backend = :cairo, + display_plot = false, + controls = false, + xscale = :log10, + fig_size = (1000, 500), + figure_title = "Conductor 1 self impedance", + panel_titles = ("Resistance", "Reactance"), + legend_title = "Result set", + series_labels = ("reference",), + legend_position = :inside, + legend_attributes = (; + halign = :right, valign = :bottom, backgroundcolor = (:white, 0.92)), + legend_cap = 0.5 +) +series_cartesian[1].figure #hide +#- +series_cartesian[2].figure #hide + +# To modify this recipe, select other matrix coordinates with an explicit +# observation request, pass native legend attributes, or mutate either returned +# axis. `series_cartesian[1].axes[1].title[] = "Measured resistance"` +# changes the live title without asking LineCableModels to rebuild anything. + +# ### Cartesian shunt admittance + +# Conductance and susceptance are the real and imaginary parts of ``Y``. They +# use the same dashboard implementation, retained units, limits, legend +# groups, and controls as impedance. Only the scientific requests differ. + +## A bottom dock is horizontal by default and remains a native Makie Legend. +shunt_cartesian = Makie.plot( + parameters, + @observe Y[1, 1, :]; + backend = :cairo, + display_plot = false, + controls = false, + xscale = :log10, + fig_size = (900, 480), + panel_titles = Dict(:G => "Self conductance", :B => "Self susceptance") +) +shunt_cartesian[1].figure #hide +#- +shunt_cartesian[2].figure #hide + +# Any accepted Makie `Legend` keyword belongs in `legend_attributes`. For a +# labeled source or comparison, moving the +# block uses `legend_position = :inside`, `:left`, `:right`, `:top`, `:bottom`, +# or a positive `(row, column)` dock coordinate. `(2, 2)` remains the plot +# canvas. Native `halign` and `valign` also position inside legends. + +# ### Figure-wide and panel-scoped legends + +# A controlled comparison registers one group per result set, then exposes two +# views of those groups. The figure legend spans every panel. A panel legend +# filters the same source handles to one logical plot position. Hiding a global +# result entry changes that source's visibility across both resistance coefficients. + +legend_scope_demo = Makie.plot( + parameters, + parameters, + @observe R[1, 1:2, :]; + series_labels = ("reference", "result"), + backend = :cairo, + display_plot = false, + controls = false, + xscale = :log10, + fig_size = (1100, 600), + figure_title = "Resistance dashboard", + panel_titles = ("Self resistance", "Mutual resistance"), + legend_position = nothing +) +# A figure-scoped legend can be placed in any addon dock and given any native +# Makie Legend attributes. +figurelegend!( + legend_scope_demo; + position = :top, + title = "Result set", + legend_labels = Dict( + :result_1 => "baseline", + :result_2 => "alternative" + ), + orientation = :horizontal, + nbanks = 2, + max_fraction = 0.5 +) +# Logical position `(1, 1)` is the resistance panel. +panellegend!( + legend_scope_demo, + (1, 1); + position = :inside, + halign = :left, valign = :bottom, + title = "Resistance result", + legend_labels = ("base R", "alternative R"), + backgroundcolor = (:white, 0.92), + max_fraction = 0.5 +) +# Titles use the same logical panel address as panel legends. +figuretitle!(legend_scope_demo, "Resistance dashboard"; fontsize = 20) +paneltitle!(legend_scope_demo, (1, 2), "Mutual resistance") +legend_scope_demo.figure #hide + +# The same panel legends can be requested at construction time with +# `panel_legends=Dict((1, 1) => (position=:inside, halign=:left, valign=:bottom, +# legend_labels=("base R", "alternative R")))`. Runtime dictionaries target +# stable source keys such as `:result_1`. + +# ### Derived R/L/G/C dashboard + +# `L` and `C` are observation-owned proxies derived from reactance or susceptance +# and angular frequency. Their DC samples are retained as unavailable. Independently +# valid components remain available. The observation owner assigns their physical +# units. For this 2×2 result, `layout=(2,2)` requests one matrix dashboard per +# physical quantity. + +## `layout` sets the initial panel grid. The returned Makie objects remain editable. +rlgc_dashboards = Makie.plot( + parameters, + (R, L, G, C); + backend = :cairo, + display_plot = false, + controls = false, + xscale = :log10, + layout = (2, 2), + fig_size = (950, 700), + legend_position = nothing +) +rlgc_dashboards[1].figure #hide +rlgc_dashboards[2].figure #hide +rlgc_dashboards[3].figure #hide +rlgc_dashboards[4].figure #hide + +# With `layout=(1,1)`, each quantity and coordinate facet has its own figure. + +# ### Polar impedance and admittance + +# Function selectors are resolved through the same observation grammar. +# `abs` and `angle` publish magnitude and phase for both ``Z`` and ``Y``. + +## The 4 transformed quantities become 4 matrix dashboards. +polar_dashboards = Makie.plot( + parameters, + (abs, angle); + backend = :cairo, + display_plot = false, + controls = false, + xscale = :log10, + layout = (2, 2), + fig_size = (950, 700), + legend_position = nothing +) +polar_dashboards[1].figure #hide +polar_dashboards[2].figure #hide +polar_dashboards[3].figure #hide +polar_dashboards[4].figure #hide + +# `real`, `imag`, `Z`, and `Y` are also valid selectors. Implement additional +# scientific transforms in the observation grammar. + +# ### Standalone series-impedance result + +# `SeriesImpedance` can be plotted before it is bundled into `LineParameters`. +# Supply its frequency samples as an explicit positional argument. The same +# native dashboard controls are available. + +standalone_impedance = Makie.plot( + parameters.Z, + parameters.f, + (Z, 1, 1, Colon()); + backend = :cairo, + display_plot = false, + controls = false, + xscale = :log10, + fig_size = (900, 440), + legend_cap = 0.5 +) +standalone_impedance[1].figure #hide +#- +standalone_impedance[2].figure #hide + +# ### Standalone shunt-admittance result + +# `ShuntAdmittance` follows the same rule: frequencies are explicit and its +# default scientific family is `(G, B)`. + +standalone_admittance = Makie.plot( + parameters.Y, + parameters.f, + (Y, 1, 1, Colon()); + backend = :cairo, + display_plot = false, + controls = false, + xscale = :log10, + fig_size = (900, 440), + legend_cap = 0.5 +) +standalone_admittance[1].figure #hide +#- +standalone_admittance[2].figure #hide + +# ### Exact observation requests and modal coordinates + +# An `@observe` request is the precise extension point for matrix coordinates. +# It passes through `Commons.observation_request`, so selection rules stay shared +# by plotting, tables, and reports. This example keeps only one diagonal entry +# from each quantity. + +resistance_request = @observe R[1, 1, :]; +inductance_request = @observe L[1, 1, :]; +selected_response = Makie.plot( + parameters, + (resistance_request, inductance_request); + backend = :cairo, + display_plot = false, + controls = false, + xscale = :log10, + fig_size = (900, 420), + legend_cap = 0.5 +) +selected_response[1].figure #hide +#- +selected_response[2].figure #hide + +# ### An explicit capacity for one selected coefficient +# +# The previous call uses an automatic 1×1 reference for each quantity. This call +# intentionally uses a two-column reference. Its one selected panel keeps the +# corresponding cell size. The returned figure fits around that panel. + +capacity_example = Makie.plot( + parameters, resistance_request; + backend = :cairo, display_plot = false, controls = false, + layout = (1, 2), fig_size = (900, 420) +) +capacity_example.figure #hide + +# ### Family requests and a vertical capacity + +# The observation owner expands `Z[1,1,:]` into retained R and X products. +# `layout=(2,1)` gives each quantity its own nominal two-row capacity. It never +# puts different quantities into the same figure. Only selected original panels +# are drawn, and unselected exterior rows are not allocated. + +self_impedance_request = @observe Z[1, 1, :]; +stacked_self_impedance = Makie.plot( + parameters, + self_impedance_request; + backend = :cairo, + display_plot = false, + controls = false, + xscale = :log10, + layout = (2, 1), # two-row nominal capacity per quantity + panel_titles = ("Self resistance", "Self reactance"), + legend_position = :inside, # native overlay. Use any outer dock instead + legend_attributes = (halign = :right, valign = :top), + legend_title = "Result set", + series_labels = ("solution",), # explicitly opt into a one-source legend + legend_cap = 0.5, + fig_size = (720, 720) +) +stacked_self_impedance[1].figure #hide +#- +stacked_self_impedance[2].figure #hide + +# With the default matrix arrangement, select more coordinates to add panels, +# more result containers to add curves or more quantities to add figure families. `overlay=:coordinates` instead makes selected coordinates the +# curves and selected gridpoints the panels. + +# Modal results retain their physical domain. Use `diag` to select the diagonal +# coefficients, as in the following inductance plot. + +modal_parameters = compute( + ModalAnalysisProblem(parameters), + ModalAnalysisFormulation(:default); + options = (offdiagonal_tolerance = 1.0,) +); +modal_inductance = Makie.plot( + modal_parameters, + (@observe((L, diag)[:, :]),); + backend = :cairo, + display_plot = false, + controls = false, + xscale = :log10, + fig_size = (820, 430), + legend_cap = 0.5 +) +modal_inductance.figure #hide + +# Extend coordinate behavior in the grammar and domain owner. Extend only visual +# appearance by mutating the returned axes or forwarding Makie plot attributes. + +# ### Measurement uncertainty + +# Uncertain retained values draw native intervals around their nominal curves. +# Full uncertainty support participates in limits even when intervals are sparse. +# `ObservedResult` classifies engineering zero using absolute nominal magnitude +# and owner-defined cutoffs. With `clip=true`, values within those cutoffs +# become exact zero with zero uncertainty. Phase unavailability and +# linked X/L or B/C thresholds are resolved when observations are acquired. +# Plotting uses the resulting values and the stored RMS errors directly. +# `clip=false` and `atol` are raw acquisition options. Compatible display units +# also work directly on retained inputs: `plot(observed; length_unit=:base)`. +# Plotting delegates unit conversion to ObservedResult, preserving the recorded +# clipping decisions and saved RMS values and units. + +# Legend actions hide or restore the nominal line, its markers and its x/y +# error bars together. Figure legends act across the figure. Panel legends act +# only on their panel. This also applies to overlays and report illustrations. +# Native error-bar handles remain independently editable. The next action to +# hide or show the series restores the complete set of components. + +measured_parameters = LineParameters( + complex.( + measurement.(resistance, 0.05 .* resistance), + measurement.(inductance .* angular_frequency, + 0.05 .* inductance .* angular_frequency) + ), + complex.( + measurement.(conductance, abs.(0.05 .* conductance)), + measurement.(capacitance .* angular_frequency, + abs.(0.05 .* capacitance .* angular_frequency)) + ), + frequency +); +uncertainty_plot = Makie.plot( + measured_parameters, + (@observe(R[1, 1, :]), @observe(L[1, 1, :])); + backend = :cairo, + display_plot = false, + controls = false, + xscale = :log10, + fig_size = (900, 440), + legend_cap = 0.5 +) +uncertainty_plot[1].figure #hide +#- +uncertainty_plot[2].figure #hide + +# The scalar type defines how to extract nominal values and errors for plotting. + +# ### Comparing completed line results + +# A named tuple supplies both completed results and default legend labels. The +# observation owner validates usable units and coordinates. Plotting overlays sources in one axis for every selected matrix position. Each +# result may retain its own frequency samples. + +result = LineParameters( + parameters.Z.values .* (1.06 + 0im), + parameters.Y.values .* (0.94 + 0im), + parameters.f +); +comparison_plot = Makie.plot( + (; reference = parameters, result), + (R,); + backend = :cairo, + display_plot = false, + controls = false, + xscale = :log10, + layout = (2, 2), + fig_size = (900, 700), + legend_position = :top, + legend_attributes = (; orientation = :horizontal), + legend_cap = 0.5 +) +comparison_plot.figure #hide + +# The exact-request positional form is +# `Makie.plot(reference, result, @observe(Z[1,1,:]); +# series_labels=("reference", "result"))`. +# Change line styling after construction through the native plot objects in each +# axis. Change source identity through `series_labels` or named-tuple keys. + +# ### Detached observed results + +# `ObservedResult` retains complete primary representations and their original +# matrix coordinates. The existing matrix renderer selects these records for +# display. Report illustrations use the same observed-input plotting method. + +observed = ObservedResult( + parameters, + (resistance_request, inductance_request) +); +observed_plot = Makie.plot( + observed; + ydata = (resistance_request, inductance_request), + title = "Retained coefficient observations", + figure_title = "Retained coefficient observations", + panel_titles = ("R[1,1]", "L[1,1]"), + backend = :cairo, + display_plot = false, + controls = false, + layout = (1, 2), + fig_size = (900, 480), + legend_title = "Observed result", + ## R and L retain the same original coefficient in separate figures. + series_labels = ("self impedance",), + legend_position = :bottom, + legend_attributes = (; orientation = :horizontal), + legend_cap = 0.5 +) +observed_plot[1].figure #hide +#- +observed_plot[2].figure #hide + +# A new primary result owner supplies observation methods. The same retained +# quantity records reuse table and plot consumption. + +# ## Geometry preview recipes + +# ### Cable-design cross-section + +# `DataModel.preview_shapes` exposes detached physical polygons with only their +# material and construction tag. The Makie preview adapter derives optional +# presentation groups. `_preview_axis!` draws the polygons with native +# `poly!` and locks the axis to `DataAspect`. It also computes +# geometry limits. Preview `size` is an initial reference allocation. The shared +# shell preserves physical frame sizes and view limits, then fits the window +# around the panels and their complete decorations. A circle or sector row can +# retain necessary space beside its shorter panel. A row of circles need not +# retain unused vertical allocation. The same fitted native `.figure` is returned +# for interactive display and documentation. + +## `display_id=true` promotes the design identifier into the panel title. +## Preview scales form a horizontal strip below the plot by default, +## with each property label on the left. +cable_preview = preview( + mv_design; + backend = :cairo, + display_plot = false, + controls = false, + display_id = true, + size = (950, 700), + ## Group the physical core-wire regions into one presentation entry. + ## Geometry tags, terminals, and electrical construction remain untouched. + legend_group = region -> region.source.tag === :wire ? + :stranded_core : region.source.tag, + legend_labels = Dict(:stranded_core => "Stranded core"), + legend_position = :right, + legend_attributes = (; nbanks = 2) +) +cable_preview.figure #hide + +# `legend_group` accepts either the shown callback over a `PlacedRegion` or a +# tag-to-group dictionary. `legend_labels` maps the resulting presentation group +# to text. This keeps display grouping independent of physical tags. For detailed +# annotation, mutate `cable_preview.axes[1]` and add ordinary Makie plots. Each +# object in `cable_preview.colorbars` is a native `Colorbar`. +# All preview routes use `guide_gap=(8,8,24,8)` in left, right, bottom, top order. +# The 24-pixel bottom clearance separates the axis label from the scale strip. +# Pass a scalar or four-sided `guide_gap` to change that clearance. + +# ### Independent scale arrangement and guide spacing + +# Bar orientation, group cells, and placement are independent. This uses the +# `mv_design` constructed above. Physical properties and material ranges remain unchanged. +# The 3 horizontal bars form one row below the preview directly. + +design = mv_design +scale_arrangement = preview( + design; + backend = :cairo, display_plot = false, controls = false, + size = (1200, 900), + legend_position = :right, + legend_attributes = (halign = :left, valign = :top), + colorbar_position = :bottom, + colorbar_attributes = (vertical = false, width = 160, height = 14), + colorbar_group_attributes = (layout = (1, 3), colgap = 16, halign = :center), + guide_spacing = (rowgap = 14, colgap = 16) +) +scale_arrangement.figure #hide + +# Move the same native objects to a column on the right. The legend-to-group +# minimum of 14 logical pixels differs from the 12-pixel internal row gap. + +figurecolorbars!(scale_arrangement; + position = :right, + group_attributes = (layout = (3, 1), rowgap = 12, valign = :bottom), + vertical = false +) +figurelegend!(scale_arrangement; position = :right, valign = :top, guide_spacing = 14) +scale_arrangement.figure #hide + +# Gaps separate complete visible siblings, including labels and endpoint ticks. +# Bars in a column share their left and right edges. Bars in a row share their +# baseline. Different label widths and fonts change the reserved decoration +# space, not those alignments. `halign` moves the complete group once. +# Figure padding, plot clearance (`guide_gap`), legend-entry spacing, and bar +# dimensions have separate controls. Hidden scales keep their native handles +# for restoration and release their occupied space. +# +# Omitted group settings survive live updates. `layout=nothing` restores one +# row at top and bottom or one column at left and right and explicit side-grid slots. +# Standalone scales in main content default to one column. Explicit capacity +# fills row-major and omits unoccupied tracks within the colorbar group. +# `rowgap=nothing` and `colgap=nothing` restore inheritance from `guide_spacing`. +# A partial live `guide_spacing=(rowgap=18,)` preserves the current column gap. +# Omitted constructor components use 12. Fractions and zero are valid, while +# negative, nonfinite and Boolean gaps are rejected. Spacing is counted only +# between neighbors, so one guide has zero exterior padding. + +# ### Cable-design collection + +# A collection preview repeats the same detached geometry path for every design +# and assigns the caller-requested layout. The material ranges are aggregated +# once. Each panel then uses comparable colors and one shared set of scales. +# Insulating regions include sparse diagonal marks over their existing material +# colors. Pass `display_dielectric_pattern=false` to omit these marks in a +# design, collection, or system preview. Semicon and conductor fills retain +# their own material colors. + +design_collection = preview( + [mv_design, hv_design, hv_design, mv_design]; + layout = (2, 2), + backend = :cairo, + display_plot = false, + controls = false, + size = (1000, 850), + figure_title = "Cable design family", + panel_titles = ("MV option A", "HV option A", "HV option B", "MV option B") +) +design_collection.figure #hide + +# Use any sufficient `(rows, columns)` layout. The default omits layer legends. +# Request selected local legends with `panel_legends`, using the same logical +# grid-position rules as line dashboards. + +# ### Cable-system cross-section + +# A system preview resolves each placed region into the system frame and adds reference geometry such as the earth interface. Limits are derived from the physical placement or `zoom_factor`. Earth properties use the same atomic +# color-scheme rules as cable materials, with a separate logarithmic +# resistivity palette: slate at 0.1 Ω·m, taupe at 100 Ω·m, and ochre at 10⁴ Ω·m. +# Horizontal earth fills follow pan, zoom and figure resizing while interfaces +# stay at their physical depths. A semi-infinite basement covers the remainder +# of the view. A finite final layer retains its declared bottom. + +system_preview = preview( + cable_system; + earth_model = earth, + zoom_factor = 1.35, + backend = :cairo, + display_plot = false, + controls = false, + display_id = true, + size = (1000, 700), + legend_position = :right +) +system_preview.figure #hide + +# Modify physical contents before previewing. Modify visual annotations after +# previewing. `zoom_factor` changes only the initial view, and the reset control +# returns to those computed limits. +# The light blue sky fades from the upper axis limit to transparency at `z=0`, +# stretching with the view and disappearing in entirely underground views. +# It is a visual cue with no material-property meaning. Use +# `display_surface_gradient=false` to disable it independently of earth colors. +# Vertical strata are currently not rendered in the system preview. + +# ## Material colors and native colorbars + +# ### One reusable material scheme + +# A color scheme is the atom. `material_property_ranges` obtains values from a +# design, a design collection, or the material defaults. `materialcolors` +# turns exactly one property and range into a Makie-compatible named tuple: +# `label`, `colormap`, `limits`, and `ticks`. `materialscale!` places exactly +# that one scheme wherever the caller chooses. + +material_ranges = LineCableModels.DataModel.material_property_ranges(mv_design); +rho_scheme = materialcolors( + :rho, + material_ranges.rho +); +rho_scale_figure = Figure(size = (850, 180), figure_padding = 24) +materialscale!( + rho_scale_figure[1, 1], + rho_scheme; + vertical = false, + width = Relative(0.85) +) +rho_scale_figure + +# The scheme contains no placement. Put that `Colorbar` in any `GridPosition`, +# combine it with a heatmap, or reuse the same scheme in another figure. Define a +# new property by defining another palette producer next to `materialcolors`. +# Physical range collection remains a DataModel concern. +# Relative permeability uses the existing logarithmic range from 1 to 300. +# Unity leaves the base color unchanged. Indigo tint becomes visible before +# the progression toward magenta at high permeability. The permeability scale +# shows that same tint over a neutral reference color. +# Dielectric marks are native Makie pattern tiles. Cairo embeds the small +# bitmap tiles in SVG/PDF output while preserving the surrounding geometry. + +# ### High-level material-scale reference + +# `show_material_scale` is a preview option that combines the three +# default property schemes and returns three independent native colorbars. + +material_scale = show_material_scale( + backend = :cairo, + display_plot = false, + controls = false, + size = (850, 360), + figure_title = "Reusable material-property schemes", + colorbar_attributes = (; vertical = false) +) +material_scale.figure #hide + +# For one property, use the preceding `materialscale!(position, scheme)` pattern. +# For a different high-level combination, compose schemes in the caller's own +# Makie layout. + +# ## Monte Carlo result recipes + +# Monte Carlo methods first publish the requested marginal from retained +# samples, `HistogramDensity`, or both. Cable-constant requests accept `R`, `L`, +# `C`, `G`, or `(selector, assembly)`. Matrix-valued line results require an +# exact request such as `@observe R[1, 1, 3]`. Native Makie function identity +# chooses the visual primitive. + +# ### Sample histogram + +# `Makie.hist` uses retained samples and calls native `hist!`. `bins` and +# `normalization` retain their Makie meanings. Unit options are consumed while +# publishing the marginal. + +sample_histogram = Makie.hist( + mc_result, + R; + bins = 12, + normalization = :pdf, + backend = :cairo, + display_plot = false, + controls = false, + fig_size = (820, 400), + figure_title = "Retained Monte Carlo samples", + legend_cap = 0.5, + color = :steelblue +) +sample_histogram.figure #hide + +# Pass ordinary `hist!` attributes such as `color`, `strokewidth`, or +# `transparency` through the remaining keywords. Use `(R, assembly)` when a +# `CableConstants` result contains more than one assembly. + +# ### Retained-model probability density + +# `Makie.stairs` consumes the retained histogram model and calls native +# `stairs!` with post-step edges. + +model_density = Makie.stairs( + mc_result, + R; + backend = :cairo, + display_plot = false, + controls = false, + fig_size = (820, 400), + legend_cap = 0.5, + color = :darkorange, + linewidth = 3 +) +model_density.figure #hide + +# With `bins=n`, a different binning is derived from retained samples. The +# stored model is unchanged. Changing its bin count requires retained samples. +# With `bins=nothing` (the default), the retained model is reused, or a model +# is derived with automatic binning if only samples were retained. Constant +# samples always produce one finite-width bin. Use native `stairs!` keywords +# for appearance. + +# ### Empirical cumulative distribution + +# `Makie.ecdfplot` consumes retained samples and delegates the empirical curve +# to Makie's `ecdfplot!` recipe. + +empirical_cdf = Makie.ecdfplot( + mc_result, + R; + backend = :cairo, + display_plot = false, + controls = false, + fig_size = (820, 400), + legend_cap = 0.5, + color = :seagreen, + linewidth = 3 +) +empirical_cdf.figure #hide + +# The returned axis can be combined with confidence bands or additional native +# curves. The addon only defines the initial empirical series and its legend group. + +# ### Retained-model cumulative distribution + +# Raw `Makie.lines` first acquires the UQ-owned CDF product. Its observed method +# draws the retained CDF coordinates with `lines!`. + +model_cdf = Makie.lines( + mc_result, + R; + backend = :cairo, + display_plot = false, + controls = false, + fig_size = (820, 400), + legend_cap = 0.5, + color = :firebrick, + linewidth = 3 +) +model_cdf.figure #hide + +# Increase visual resolution by extending the owner-side model grid settings. Add +# purely visual reference curves directly to `model_cdf.axes[1]`. + +# ### Sample/model Q-Q plot + +# `Makie.qqplot` requests both retained products. UQ calculates matching quantile pairs for the native scatter plot. The identity reference line is optional. + +quantile_plot = Makie.qqplot( + mc_result, + R; + qqline = :identity, + backend = :cairo, + display_plot = false, + controls = false, + fig_size = (820, 440), + panel_titles = ("Sample versus retained model",), + legend_title = "Q–Q elements", + legend_labels = ("sample quantiles", "identity reference"), + legend_position = :inside, + legend_attributes = (; halign = :left, valign = :top, backgroundcolor = (:white, 0.92)), + legend_cap = 0.5, + color = :purple, + markersize = 10 +) +quantile_plot.figure #hide + +# Set `qqline=:none` to remove the reference. Other scatter attributes are +# forwarded to `scatter!`. Data and units remain observation concerns. + +# ## Callable controls and retained assembly points + +# A widget callable receives the final live handle once per figure. Its native +# callback uses the same status and frame-preserving shell operations as built-ins. +# `controls=false` skips standard and custom widgets. Closing a display window +# leaves the retained handle editable, exportable, and redisplayable. + +function add_reset_y!(p) + addwidget!((plot, cell) -> Button(cell; label = "Reset Y"), p, :reset_y; + event = button -> button.clicks, + callback = (plot, _) -> resetview!(plot; x = false, y = true), + success = "Y view reset") + nothing +end +widget_plot=Makie.plot(parameters; ydata = ((R, 1, 1, :),), layout = (1, 1), + backend = :cairo, display_plot = false, widgets = (add_reset_y!,)); +widget_plot.figure #hide + +# CableConstants supply categorical assembly coordinates. The first-seen union +# below is core, sheath, screen. Each observation uses its own requested point. +# Their intervals retain their original uncertainty meaning. Categorical points +# are primary data, so decorative-marker sampling does not remove them. + +assembly_a=CableConstants([:core, :sheath], [1.0, 2.0] .* 1e-4, [2.0, 3.0] .* 1e-7, + [3.0, 4.0] .* 1e-10, [1.0, 2.0] .* 1e-9, 50.0); +assembly_b=CableConstants([:core, :screen], [1.1, 2.2] .* 1e-4, [2.1, 3.1] .* 1e-7, + [3.1, 4.1] .* 1e-10, [1.1, 2.1] .* 1e-9, 50.0); +assembly_plot=Makie.plot((first = assembly_a, second = assembly_b); ydata = (R,), + backend = :cairo, display_plot = false, controls = false); +assembly_plot.figure #hide + +# ## Composing figures with `plotwindow` + +# ### A caller-owned 2×2 plot + +# `plotwindow` is the escape hatch when no high-level recipe is appropriate. It +# creates the figure and controls, then passes its content `GridLayout` to the +# callback. The callback uses normal Makie constructors. Afterward `plotwindow` +# discovers the native axes and attaches shared numeric scale, reset and export +# controls. Explicit `axis=(...)` overrides apply to callback-created axes. +# Otherwise their native construction settings are retained. + +custom_dashboard = LineCableModels.plotwindow( + title = "Caller-owned diagnostics", + figure_title = "Four caller-owned Makie axes", + size = (900, 650), + backend = :cairo, + display_plot = false, + controls = false +) do grid + for row in 1:2, column in 1:2 + + axis = Axis( + grid[row, column]; + title = "Response $row,$column", + xlabel = "Frequency [Hz]", + ylabel = "Amplitude" + ) + lines!( + axis, + frequency, + @. (row + column) * sin(log10(frequency)); + color = Makie.wong_colors()[(row - 1) * 2 + column], + linewidth = 2 + ) + axis.xscale = log10 + end +end +custom_dashboard.figure #hide + +# The callback supports nested layouts, `Axis3`, `Colorbar`, `Slider`, custom +# Makie recipes, and arbitrary plot primitives. Use `Figure` directly for a composition that omits LineCableModels controls and export settings. + +# ## Plot controls and layout + +# ### Scientific axes, scale controls, and limits + +# Scientific recipes supply quantity and unit labels. PlotBuilder formats +# every linear numeric axis, including previews, statistical plots, and `plotwindow` +# axes: the displayed limits determine one engineering power-of-ten multiplier +# (powers of three) in the axis label. Ticks show plain decimal mantissas. Tick density follows each data rectangle and the rendered +# label size. A linear frequency view from 0 to 10 MHz can show ticks at +# `0, 2, 4, 6, 8, 10` with the label `Frequency [Hz] ×10⁶`. The retained +# frequencies are unchanged. +# Zooming, panning, changing limits, and resetting keep the ticks and multiplier +# synchronized. Native custom tick formatters or explicit tick labels override +# automatic formatting. Setting the formatter back to `Makie.automatic` restores +# it. Native tick positions persist across scale changes. Caller tick functions +# and custom locator objects retain native formatting. Resetting the tick +# attribute to `Makie.automatic` restores the shared locator. +# Date/category axes and other native transforms retain Makie's own presentation. +# These defaults use the native tick locators in Makie 0.24.11 or newer. +# Positive and signed logarithmic views spanning less than two base-10 units +# after transformation show decimal mantissas, with one engineering multiplier +# when needed. Broader views show scientific tick labels without another axis +# multiplier. Those labels retain the physical sign and any nonunit coefficient. +# Zero is displayed as `0`. The signed-log reference determines the transform, +# not the displayed units or the engineering multiplier. Both x and y use the +# same tick-spacing rule, fitting actual label spacing after the coordinate transform. +# The x/y toggles validate current visible data and uncertainty bounds before +# changing the page. Native numeric `plotwindow` axes share these controls. +# Automatic near-constant positive log ranges use modest multiplicative padding +# (`c/1.05` to `c*1.05`, enlarged for uncertainty). +# +# Native Axis keywords such as `xticks`, `limits`, and `ytickformat`, and native +# series attributes such as `linewidth` and `color` can be passed at construction. +# Explicit `axis=(...)`, `figure=(...)` and per-series `series_attributes` +# override shared defaults. Subsequent native mutations remain authoritative. +# For names shared by Axis and a plot, the unqualified form targets Axis. +# An explicit native `figure.size` overrides `fig_size`. Portrait sizes stay portrait. +# +# UQ benchmark plots with ordinary `ydata=(R,L,G,C)` overlay mean ±1 standard +# deviation from retained moments. Explicit `(statistics,L,std)` requests show +# the standard deviation alone. Request these two products in separate calls. +# `uncertain(result, configuration)` returns the uncertainty-bearing core stored +# during MC aggregation. This marginal representation retains each output's mean +# and standard deviation. Joint output correlations require the retained samples. +# Every eligible numeric route retains log controls for zero or negative support. +# Adaptive logarithmic panels use a sign-preserving pseudo-log transform with +# `log1p`/`expm1` evaluation: `sign(v)*log10(1+abs(v)/s)`. The reference `s` is +# the smallest finite nonzero magnitude among eligible visible samples, +# enabled uncertainty endpoints and explicit limits, in the displayed units. +# If no nonzero value exists, `s=1`. Ticks retain physical values and units. +# This transform lets small signed conductances span decades instead of +# appearing linear. +# Reapply `axisscale!(page, :y, :log10)` or switch log off/on to select the +# reference from current data. Zooming and resetting limits keep it fixed. +# Explicit `:pseudolog10` retains a reference of 1 in displayed units. +# Switching log off selects a linear axis. Observation clipping is set during +# acquisition. Use `clip=false` or component cutoffs such as `atol=(G=0., B=0.)` +# when acquiring observations to preserve nonzero components. Retained +# observations must be reconstructed from their primary result to undo clipping. +# +# Limits are calculated from finite visible data, including measurement error +# bounds. Constant and near-constant series receive at least ±5% padding around +# a nonzero baseline, enlarged for visible uncertainty. An exactly zero series +# without uncertainty uses a neutral nonzero range. Near-constant means that the +# endpoints agree within `sqrt(eps(Float64))` relatively in view coordinates. +# These bounds affect the displayed view. The samples retain their original +# values. Small values with measurable relative variation receive a tightly +# fitted view. Legend visibility changes trigger another limit pass, so hiding +# a dominant curve fits the view to the remaining data. +# Explicit native limits, including one-sided limits, remain authoritative. +# Reset refits automatic bounds and restores explicit bounds. Changing x/y scale +# refits that automatic dimension while preserving the other dimension's current +# view. Incompatible log limits are rejected before the page changes. Use native +# `autolimits!(axis)` explicitly when manual bounds should be discarded. + +# ### Legends and docks + +# `legend_position` selects only placement. `:inside` overlays the union of the +# figure's axis viewports or the one axis viewport for a panel legend. +# Native `halign`/`valign` select corner, center, or fractional alignment. Side-grid slots +# remain outside the plot area. `legend_attributes` is merged into the native +# `Legend` constructor, so orientation, bank count, padding, background, +# alignment, and other Makie options remain available. `legend_cap=0.5` +# is a finite, non-Boolean real number in `(0,1]`. It caps top and bottom legend +# height or side legend width relative to the associated data area. The other +# dimension must also fit. Inside legends use the height cap. Figure legends +# use the combined panel footprint. Panel legends use their own data area. +# Entries that exceed the cap are replaced by `(...)` and return when resizing +# provides room. If the title and ellipsis cannot fit, the legend remains hidden +# until space returns. The cap includes padding and margins, and never removes +# plotted curves. Use `max_fraction` with `figurelegend!`, `panellegend!`, or a +# `panel_legends` configuration. +# `figurelegend!` and `panellegend!` move retained native objects. Label changes +# retain their source bindings, so placement, title, and legend labels can be +# changed after construction. Clicking/toggling a grouped Makie legend entry +# continues to affect every plot handle in that group. +# +# High-level recipes may put a legend and colorbars in the same dock. PlotBuilder +# creates a nested `GridLayout` and gives each block its own cell. Resizing moves +# the canvas and dock blocks together. + +# ### Colorbars + +# `colorbar_position` and `colorbar_attributes` mirror the legend placement +# rules. `colorbar_group_attributes.layout` determines the independent group +# arrangement. `guide_spacing` supplies minimum sibling gaps unless group +# `rowgap`/`colgap` overrides them. A preview supplies several schemes, but +# `_colorbar!` always consumes one scheme and creates one native +# `Colorbar`. The reusable public atom remains +# `materialcolors(property, range)` plus `materialscale!(position, scheme)`. + +# ### Observables and ownership + +# `@observe` selects a scientific quantity and its matrix and frequency +# coordinates. Makie's `Observable` type drives live UI state: controls, scales, +# limits, visibility, and layout bounds. The adapter renders the selected +# observations and connects the native UI subscriptions. +# Keeping the `UIPlot` alive keeps the native figure and its subscriptions alive. +# +# Native mutation is the normal extension mechanism: + +owned_plot = Makie.plot( + parameters, + @observe R[1, 1, :]; + backend = :cairo, + display_plot = false, + controls = false, + xscale = :log10, + fig_size = (820, 400), + legend_cap = 0.5 +); +owned_axis = only(owned_plot.axes); +owned_axis.title[] = "Caller-owned resistance"; +vlines!(owned_axis, [100.0, 1_000.0]; color = :black, linestyle = :dash); +owned_plot.figure #hide + +# ### Responsive layout + +# Automatic diagonal and preview flow pages may reflow locally when resized. +# Their page membership and identities remain fixed. Explicit layouts and matrix +# topology remain fixed. Physical aspect belongs to each panel, so circular designs, +# wide systems, and 1×4 preview collections remain correctly scaled. A later guide, +# title, or widget change refits only that window around its current data frames. + +# ### Current-state SVG export + +# Load CairoMakie before creating a plot to include its Save button. For GL +# interactivity and SVG export, import both CairoMakie and GLMakie, then select +# `backend=:gl`. Loading CairoMakie later enables direct export of an existing +# plot. Recreate the plot to add its button. The toolbar displays +# file errors in the status row. Direct `export_svg` calls throw them to the caller. +# +# [`export_svg`](@ref) saves the current live figure through CairoMakie. For a +# publication export it temporarily hides the toolbar and status row, switches +# the figure's font roles to Makie's LaTeX font theme, uses a white background, +# and then restores every changed observable, preserving the active backend. +# The SVG includes caller-added plots, visibility, scales, limits, and annotations. +# Interactive zoom and pan are retained in both the SVG and the live window. + +export_directory = mktempdir(); +export_svg( + owned_plot; + path = joinpath(export_directory, "caller_owned_resistance.svg"), + theme = :publication, + open_file = false +); #hide + +# ## Adding or changing a managed recipe + +# A new high-level recipe should keep these responsibilities with their owners: +# +# 1. Expose numerical observations or physical geometry through the public +# methods of the module that defines them. Do not add plot-data construction, labels, +# colors, layout, or Makie types to scientific owners. +# 2. Add request normalization and the narrow public dispatch method in +# `LineCableModelsMakieExt`, then call +# native Makie constructors or primitives there. +# 3. Reuse only the shared Makie functions the recipe needs: `_figure_layout`, +# `_axis!`, `_finish_plot!`, or `plotwindow`. Do not create a second +# plot specification or an optional adapter hierarchy. +# 4. Return `UIPlot` with the actual native objects and leave further mutation to +# the caller. +# 5. Add the real call to this literate gallery. Add a Cairo rendering assertion and an interactive GL inspection fixture when resizing or widgets matter. +# +# A purely visual variation usually does not need a new managed recipe. Pass a native attribute or mutate the returned block. Add a Makie primitive, or start from +# `plotwindow`. A new recipe is justified when LineCableModels implements application-specific +# retained coordinate presentation, physical geometry, or a reusable piece +# of scientific interaction. + +# ## Implementation + +# Scientific objects supply observations, geometry, material properties, and +# units. `LineCableModelsMakieExt` converts these values into Makie plots. +# Matrix coordinates identify subplots. Result containers identify overlaid +# series. PlotBuilder adds labels, controls, and SVG export. +# +# | Implementation | Responsibility | +# |:--|:--| +# | `src/plotbuilder/` | Optional entry points and `UIPlot` | +# | `ext/LineCableModelsMakieExt/recipes/line_data.jl` and `comparison_data.jl` | Select observations for plotting | +# | `src/datamodel/preview/geometry.jl` and `materials.jl` | Geometry and material ranges | +# | `ext/LineCableModelsMakieExt/recipes/preview_data.jl` | Extract geometry for drawing | +# | `ext/LineCableModelsMakieExt/material_colors.jl` | Material palettes | +# | `ext/LineCableModelsMakieExt/shell.jl` | Figure layout, axes, limits, and controls | +# | `ext/LineCableModelsMakieExt/recipes/*_render.jl` | Draw declaration geometry | +# | `ext/LineCableModelsMakieExt/layout.jl` and `guides.jl` | Capacity, native frames, and guide composition | +# | `ext/LineCableModelsMakieExt/controls.jl` | Native widget ownership and common actions | +# | `ext/LineCableModelsMakieExt/export_presentation.jl` | Temporary native export presentation and restoration | +# | `ext/LineCableModelsMakieExt/montecarlo.jl` | Statistical plots | +# | `ext/LineCableModelsMakieExt/native_export.jl` | SVG export | diff --git a/docs/make.jl b/docs/make.jl index 05b8db68a..0100e434e 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,204 +1,265 @@ using Documenter using DocumenterCitations +using CairoMakie +using LineCableModels using Literate -using Pkg -using Changelog +using TOML -function get_project_toml() - # Get the current active environment (docs) - docs_env = Pkg.project().path +include("type_trees.jl") +include("owned_doc_links.jl") - # Path to the main project (one level up from docs) - main_project_path = joinpath(dirname(docs_env), "..") +const ROOT_DIR = normpath(joinpath(@__DIR__, "..")) +const DOCS_SRC_DIR = joinpath(@__DIR__, "src") +const REPOSITORY = "Electa-Git/LineCableModels.jl" +const REPOSITORY_URL = "https://github.com/$(REPOSITORY)" +const SITE_URL = "https://electa-git.github.io/LineCableModels.jl" +const TUTORIAL_SOURCE = joinpath(ROOT_DIR, "examples") +const TUTORIAL_OUTPUT = joinpath(DOCS_SRC_DIR, "tutorials") +const PLOTTING_SOURCE = joinpath(@__DIR__, "literate", "plotting.jl") - # Parse the main project's TOML - project_toml = Pkg.TOML.parsefile(joinpath(main_project_path, "Project.toml")) +const CONVENIENCE_API_OBJECTS = () - return project_toml -end +const EXTENSION_API_OBJECTS = ( + LineCableModels.Commons.validate_observables, + LineCableModels.Commons.unit_targets, + LineCableModels.Commons.detach, + LineCableModels.Commons.observation_request, + LineCableModels.Commons.observation_indices, + LineCableModels.Commons.materialize_observation, + LineCableModels.Commons.request_identity, + LineCableModels.Commons.request_quantity, + LineCableModels.Commons.request_indices, + LineCableModels.ObservedResult, + LineCableModels.Commons.observation_quantity, + LineCableModels.Commons.observation_gridpoint, + LineCableModels.Commons.observation_groups, + LineCableModels.Units.family, + LineCableModels.DataModel.preview_shapes, + LineCableModels.DataModel.preview_materials, + LineCableModels.DataModel.PreviewShape, + LineCableModels.DataModel.material_property_ranges, + LineCableModels.materialcolors, + LineCableModels.materialscale!, + LineCableModels.Engine.has_uncertainty_type, + LineCableModels.materialize, + LineCableModels.ParametricBuilder.traverse, + LineCableModels.sample_uncertainty, + LineCableModels.UIPlot, + LineCableModels.plotwindow, + LineCableModels.export_svg, + LineCableModels.figurelegend!, + LineCableModels.panellegend!, + LineCableModels.figuretitle!, + LineCableModels.paneltitle!, + LineCableModels.figurecolorbars!, + LineCableModels.axisscale!, + LineCableModels.resetview!, + LineCableModels.addwidget!, + LineCableModels.removewidget!, + LineCableModels.ReportBuilder, + LineCableModels.ReportBuilder.AbstractReportDefinition, + LineCableModels.ReportBuilder.CableConstantsTableDefinition, + LineCableModels.ReportBuilder.LineParametersTableDefinition, + LineCableModels.ReportBuilder.BenchmarkTableDefinition, + LineCableModels.ReportBuilder.MonteCarloTableDefinition, + LineCableModels.ReportBuilder.select, + LineCableModels.ReportBuilder.tabulate, + LineCableModels.ReportBuilder.illustrate, + LineCableModels.ReportBuilder.encode, + LineCableModels.ReportBuilder.write, + LineCableModels.ReportBuilder.observation_columns, + LineCableModels.ReportBuilder.encode_cell, + LineCableModels.ReportBuilder.XLSXSheet, + LineCableModels.ReportBuilder.XLSXWorkbook, + LineCableModels.ImportExport.serialize_value, + LineCableModels.ImportExport.deserialize_value, + LineCableModels.ImportExport.deserialize_extension +) -function open_in_default_browser(url::AbstractString)::Bool - try - if Sys.isapple() - Base.run(`open $url`) - true - elseif Sys.iswindows() - Base.run(`powershell.exe Start "'$url'"`) - true - elseif Sys.islinux() - Base.run(`xdg-open $url`, devnull, devnull, devnull) - true - else - false - end - catch ex - false - end +_contains_identity(collection, object) = any(entry -> entry === object, collection) +function api_reference_entry(object) + !_contains_identity(CONVENIENCE_API_OBJECTS, object) && + !_contains_identity(EXTENSION_API_OBJECTS, object) end +developer_reference_entry(object) = _contains_identity(EXTENSION_API_OBJECTS, object) - - -# Get project data -PROJECT_TOML = get_project_toml() -PROJECT_VERSION = PROJECT_TOML["version"] -NAME = PROJECT_TOML["name"] -AUTHORS = join(PROJECT_TOML["authors"], ", ") * " and contributors." -GITHUB = PROJECT_TOML["git_url"] - -@eval using $(Symbol(NAME)) -main_module = @eval $(Symbol(NAME)) - -function customize_literate_footer(content, custom_footer="") - if isempty(custom_footer) - return replace( - content, - r"---\s*\n\*This page was generated using \[Literate\.jl\]\(.*?\)\.\*\s*$" => "", - ) - else - return replace(content, - r"\*This page was generated using \[Literate\.jl\]\(.*?\)\.\*\s*$" => - custom_footer) - end +function project_metadata() + project = TOML.parsefile(joinpath(ROOT_DIR, "Project.toml")) + authors = get(project, "authors", String[]) + return ( + name = get(project, "name", "LineCableModels"), + version = get(project, "version", "dev"), + authors = isempty(authors) ? "LineCableModels contributors" : join(authors, ", ") + ) end -function post_process_literate(content) - content = customize_literate_footer(content, "🏠 Back to [Tutorials](@ref)\n") - return content +function strip_literate_footer(content::AbstractString) + return replace( + content, + r"(?ms)^---\s*\n\*This page was generated using \[Literate\.jl\]\(.*?\)\.\*\s*$" => "Back to [Tutorials](../tutorials.md)\n" + ) end -tutorial_source = joinpath(@__DIR__, "..", "examples") -tutorial_output = joinpath(@__DIR__, "src", "tutorials") -# Remove the directory if it exists and then create it fresh -if isdir(tutorial_output) - rm(tutorial_output, recursive=true) +normalize_literate_page(content::AbstractString) = rstrip(content) * "\n" + +function tutorial_title(path::AbstractString) + content = read(path, String) + matchobj = match(r"(?m)^#\s+(.+)$", content) + isnothing(matchobj) || + return replace(String(matchobj.captures[1]), r"^#+\s*" => "") + stem = splitext(basename(path))[1] + return titlecase(replace(stem, "_" => " ", "-" => " ")) end -mkpath(tutorial_output) -for file in readdir(tutorial_source) - if endswith(file, ".jl") +function build_tutorials!() + rm(TUTORIAL_OUTPUT; recursive = true, force = true) + mkpath(TUTORIAL_OUTPUT) + + for file in sort(readdir(TUTORIAL_SOURCE)) + endswith(file, ".jl") || continue Literate.markdown( - joinpath(tutorial_source, file), - tutorial_output, - documenter=true, - postprocess=content -> - post_process_literate( - content, - ), + joinpath(TUTORIAL_SOURCE, file), + TUTORIAL_OUTPUT; + documenter = true, + postprocess = strip_literate_footer ) end -end - -# Get all .md files in tutorial_output -tutorial_files = filter( - file -> endswith(file, ".md") && file != "index.md", - readdir(tutorial_output), -) -# Build menu from existing files only -tutorial_menu = ["Contents" => "tutorials.md"] -for file in tutorial_files - relative_path = String(joinpath("tutorials", file)) # Convert to full String - # Get title from file content - content = read(joinpath(tutorial_output, file), String) - m = match(r"#\s+(.*)", content) - # Make sure title is a full String too, not SubString - if m !== nothing - title = String(m.captures[1]) - else - title = String(titlecase(replace(basename(file)[1:end-3], "_" => " "))) - end - push!(tutorial_menu, title => relative_path) + files = sort(filter(file -> endswith(file, ".md"), readdir(TUTORIAL_OUTPUT))) + return [tutorial_title(joinpath(TUTORIAL_OUTPUT, file)) => joinpath("tutorials", file) + for + file in files] end -tutorial_pages = [String(joinpath("tutorials", file)) for file in tutorial_files] +function generate_maintained_pages!() + Literate.markdown( + PLOTTING_SOURCE, + DOCS_SRC_DIR; + documenter = true, + credit = false, + postprocess = normalize_literate_page + ) + return nothing +end -bib = CitationBibliography( - joinpath(@__DIR__, "src", "refs.bib"), - style=:numeric, # default -) +metadata = project_metadata() +tutorials = build_tutorials!() +tutorial_pages = last.(tutorials) +generate_maintained_pages!() DocMeta.setdocmeta!( - main_module, + LineCableModels, :DocTestSetup, - :(using $(Symbol(NAME))); - recursive=true, + quote + using LineCableModels + using LineCableModels.DataModel.BaseParams + end; + recursive = true ) -mathengine = MathJax3( - Dict( - :loader => Dict("load" => ["[tex]/physics"]), - :tex => Dict( - "inlineMath" => [["\$", "\$"], ["\\(", "\\)"]], - "tags" => "ams", - "packages" => ["base", "ams", "autoload", "physics"], - ), - :chtml => Dict( - :scale => 1.1, - ), - ), -) - -Changelog.generate( - Changelog.Documenter(), # output type - joinpath(@__DIR__, "..", "CHANGELOG.md"), # input file - joinpath(@__DIR__, "src", "CHANGELOG.md"); # output file - repo="Electa-Git/LineCableModels.jl", # default repository for links -) - -todo_src = joinpath(@__DIR__, "..", "TODO.md") -todo_dest = joinpath(@__DIR__, "src", "TODO.md") -cp(todo_src, todo_dest, force=true) +bibliography = CitationBibliography(joinpath(DOCS_SRC_DIR, "bibliography.bib"); style = :numeric) +owned_doc_links = OwnedDocLinks.OwnedDocLinker(LineCableModels) makedocs(; - modules=[main_module], - authors="Amauri Martins", - sitename="$NAME.jl", - format=Documenter.HTML(; - mathengine=mathengine, - edit_link="main", - assets=[ + modules = [LineCableModels], + authors = metadata.authors, + sitename = "$(metadata.name).jl", + format = Documenter.HTML(; + canonical = SITE_URL, + edit_link = "main", + assets = [ "assets/citations.css", "assets/favicon.ico", "assets/custom.css", - "assets/custom.js", + "assets/custom.js" ], - prettyurls=get(ENV, "CI", "false") == "true", - ansicolor=true, - collapselevel=1, - footer="[$NAME.jl]($GITHUB) v$PROJECT_VERSION supported by the Etch Competence Hub of EnergyVille, financed by the Flemish Government.", - size_threshold=nothing, + mathengine = MathJax3( + Dict( + :loader => Dict("load" => ["[tex]/physics"]), + :tex => Dict( + "inlineMath" => [["\$", "\$"], ["\\(", "\\)"]], + "tags" => "ams", + "packages" => ["base", "ams", "autoload", "physics"] + ), + :chtml => Dict(:scale => 1.1) + ), + ), + prettyurls = get(ENV, "CI", "false") == "true", + footer = "[$(metadata.name).jl]($(REPOSITORY_URL)) v$(metadata.version) supported by the Etch Competence Hub of EnergyVille, financed by the Flemish Government.", + size_threshold_warn = 700 * 1024, + size_threshold = 1024 * 1024 ), - pages=[ + pages = [ "Home" => "index.md", - "Tutorials" => tutorial_menu, + "Theory" => Any[ + "Contents" => "theory/contents.md", + "Matrix formulation" => "theory/matrix_formulation.md", + "Modal decomposition" => Any[ + "Overview" => "theory/modal_decomposition.md", + "Default modal decomposition and eigenvalue tracking" => "theory/modal-decomposition/default.md" + ], + "Earth properties" => Any[ + "Overview" => "theory/earth_properties.md", + "Default frequency-dependent earth material" => "theory/earth-properties/frequency-dependent/default.md", + "Registered frequency-dependent soil relations" => "theory/earth-properties/frequency-dependent/formulas.md", + "Default equivalent homogeneous-earth rule" => "theory/earth-properties/equivalent-homogeneous/default.md" + ], + "Earth return admittance" => Any[ + "Overview" => "theory/earth_return_admittance.md", + "Pollaczek underground earth-return admittance" => "theory/external-admittance/1926/homogeneous-earth-generalized-induction-green-function/Pollaczek1926.md", + "Wise homogeneous-earth overhead potential coefficient" => "theory/external-admittance/1948/homogeneous-earth-overhead-potential-coefficient/Wise1948.md", + "Xue underground earth-return admittance" => "theory/external-admittance/2018/complete-field-and-quasi-tem-underground/Xue2018.md" + ], + "Earth return impedance" => Any[ + "Overview" => "theory/earth_return_impedance.md", + "Carson homogeneous-earth overhead correction integral" => "theory/external-impedance/1926/homogeneous-earth-overhead-integral/Carson1926.md", + "Pollaczek generalized induction coefficients" => "theory/external-impedance/1926/homogeneous-earth-generalized-induction-green-function/Pollaczek1926.md", + "Wise high-frequency overhead displacement-current integral" => "theory/external-impedance/1934/homogeneous-earth-overhead-displacement-current-integral/Wise1934.md", + "Xue underground earth-return impedance" => "theory/external-impedance/2018/complete-field-and-quasi-tem-underground/Xue2018.md", + "Ametani mixed-pair exponential-image approximation" => "theory/external-impedance/2009/homogeneous-earth-mixed-exponential-image/Ametani2009.md" + ], + "Insulation parameters" => Any[ + "Overview" => "theory/insulation_parameters.md", + "Default cable-insulation admittivity" => "theory/insulation-admittance/default.md", + "Lossless cable-layer admittivity" => "theory/insulation-admittance/lossless.md", + "Lossy cable-layer admittivity (Ametani 2004 application)" => "theory/insulation-admittance/2004/semiconducting-screen-complex-permittivity/Ametani2004.md", + "Default coaxial-insulation magnetic series impedance" => "theory/insulation-impedance/default.md" + ], + "Internal impedance" => Any[ + "Overview" => "theory/internal_impedance.md", + "Default cylindrical-conductor surface impedances" => "theory/internal-impedance/default.md", + "Default analytical pipe-type treatment" => "theory/internal-impedance/pipe-default.md" + ] + ], + "Tutorials" => Any["Contents" => "tutorials.md", tutorials...], + "User guide" => Any[ + "Cable data model" => "data-model.md", + "Modeling and results" => "usage.md", + "Modal analysis" => "modal_analysis.md", + "Gmsh/GetDP FEM backend" => "fem.md", + "Gridspace and uncertainty" => "gridspace.md" + ], "API reference" => "reference.md", - "Development" => Any[ - "Validation module"=>"validation.md", - "Docstrings"=>"docstrings.md", - "TODO"=>"TODO.md", - "Changelog"=>"CHANGELOG.md", + "Conveniences" => Any[ + "Overview" => "conveniences.md", + "Data entry validation" => "validation.md" + ], + "Developers" => Any[ + "Commons invariants" => "developers.md", + "Extension API" => "extensions.md", + "Conventions" => "conventions.md", + "Computational engine" => "engine.md", + "Makie plotting" => "plotting.md", + "Contributing" => "contributing.md" ], - "Bibliography" => "bib.md", + "Bibliography" => "bibliography.md" ], - clean=true, - plugins=[bib], - checkdocs=:exports, - pagesonly=true, - warnonly=true, + clean = true, + plugins = [bibliography, owned_doc_links], + checkdocs = :exports, + pagesonly = true ) -if haskey(ENV, "CI") - deploydocs( - repo="github.com/Electa-Git/LineCableModels.jl.git", - devbranch="main", - versions=["stable" => "v^", "dev" => "main"], - branch="gh-pages", - ) -else - open_in_default_browser( - "file://$(abspath(joinpath(@__DIR__, "build", "index.html")))", - ) || - println("Failed to open the documentation in the browser.") -end -@info "Finished docs build." # Good to know the script completed +owned_doc_links.linked > 0 || error("owned documentation linker did not process any names") +@info "Finished documentation build." diff --git a/docs/owned_doc_links.jl b/docs/owned_doc_links.jl new file mode 100644 index 000000000..d1fc24065 --- /dev/null +++ b/docs/owned_doc_links.jl @@ -0,0 +1,151 @@ +module OwnedDocLinks + +import Documenter + +const MarkdownAST = Documenter.MarkdownAST + +export OwnedDocLinker + +""" + OwnedDocLinker(roots...) + +Turn unlinked inline-code names owned by `roots` into ordinary Documenter `@ref` +links. Only bare or module-qualified names that Documenter has included in the +current build are linked. Existing links, code snippets, values, missing names, +and ambiguous names are left unchanged. +""" +mutable struct OwnedDocLinker <: Documenter.Plugin + roots::Vector{Module} + linked::Int + + function OwnedDocLinker(roots::Module...) + isempty(roots) && throw(ArgumentError("at least one owning module is required")) + return new(collect(roots), 0) + end +end + +abstract type LinkOwnedDocNames <: Documenter.Builder.DocumentPipeline end + +# Template expansion registers the docstring inventory. CrossReferences then +# resolves the @ref nodes created here using Documenter's normal machinery. +Documenter.Selectors.order(::Type{LinkOwnedDocNames}) = 2.9 + +function Documenter.Selectors.runner( + ::Type{LinkOwnedDocNames}, doc::Documenter.Document) + linker = get(doc.plugins, OwnedDocLinker, nothing) + linker === nothing && return + Documenter.is_doctest_only(doc, "LinkOwnedDocNames") && return + + linker.linked = 0 + for page in values(doc.blueprint.pages) + meta = copy(doc.user.meta) + link_owned_names!(page.mdast, meta, doc, linker) + end + @info "LinkOwnedDocNames: linked $(linker.linked) owned inline-code references." + return +end + +function link_owned_names!(node, meta, doc, linker) + element = node.element + + if element isa Documenter.MetaNode + merge!(meta, element.dict) + elseif element isa Documenter.DocsNode + for (docstring, docmeta) in zip(element.mdasts, element.metas) + docstring_meta = copy(meta) + module_ = get(docmeta, :module, nothing) + isnothing(module_) || (docstring_meta[:CurrentModule] = module_) + link_owned_names!(docstring, docstring_meta, doc, linker) + end + return + elseif element isa Union{MarkdownAST.Link, MarkdownAST.Image} + # Existing links and image descriptions must never acquire nested links. + return + elseif element isa MarkdownAST.Code + code = element.code + target = documented_owned_target(code, meta, doc, linker.roots) + if target !== nothing + node.element = MarkdownAST.Link("@ref $target", "") + push!(node.children, MarkdownAST.Node(element)) + linker.linked += 1 + end + return + end + + for child in node.children + link_owned_names!(child, meta, doc, linker) + end + return +end + +function is_name(code::AbstractString) + expression = try + Meta.parse(code) + catch + return false + end + return is_name(expression) +end + +is_name(::Symbol) = true +function is_name(expression::Expr) + Meta.isexpr(expression, :., 2) || return false + field = expression.args[2] + return is_name(expression.args[1]) && + field isa QuoteNode && field.value isa Symbol +end +is_name(_) = false + +function documented_owned_target(code, meta, doc, roots) + is_name(code) || return nothing + expression = Meta.parse(code) + current_module = get(meta, :CurrentModule, Main) + modules = current_module === Main ? (Main,) : (current_module, Main) + + for module_ in (modules..., roots...) + binding = try + Documenter.DocSystem.binding(module_, expression) + catch + continue + end + object = Documenter.find_object(doc, binding, Union{}) + object === nothing && continue + is_owned(object.binding, roots) || continue + return Documenter.bindingstring(object.binding) + end + + # Public-but-unexported names are common in this package. A bare name is + # still safe to link when the generated inventory has exactly one owned + # binding with that name. Duplicate names remain deliberately unlinked. + expression isa Symbol || return nothing + targets = Set{String}() + for binding in keys(doc.internal.bindings) + binding.var === expression || continue + object = Documenter.find_object(doc, binding, Union{}) + object === nothing && continue + is_owned(object.binding, roots) || continue + push!(targets, Documenter.bindingstring(object.binding)) + end + return length(targets) == 1 ? only(targets) : nothing +end + +function is_owned(binding, roots) + any(root -> is_descendant(binding.mod, root), roots) && return true + + # A module's binding belongs to its parent, so handle the root module + # (and its documented submodules) explicitly. + isdefined(binding.mod, binding.var) || return false + value = getfield(binding.mod, binding.var) + return value isa Module && any(root -> is_descendant(value, root), roots) +end + +function is_descendant(module_::Module, root::Module) + while true + module_ === root && return true + parent = parentmodule(module_) + parent === module_ && return false + module_ = parent + end +end + +end diff --git a/docs/package_stats.jl b/docs/package_stats.jl new file mode 100644 index 000000000..a1eae9e6a --- /dev/null +++ b/docs/package_stats.jl @@ -0,0 +1,166 @@ +using CairoMakie +using CSV +using Downloads +using GeoInterface +using GeoJSON + +const PACKAGE_UUID = "dffb7669-8cd4-4628-99dd-7694e356ba37" +const LOG_URL = "https://julialang-logs.s3.amazonaws.com/public_outputs/current/package_requests_by_region.csv.gz" +const LAND_URL = "https://raw.githubusercontent.com/nvkelso/natural-earth-vector/v5.1.2/geojson/ne_110m_land.geojson" +const DEFAULT_OUTPUT = joinpath(@__DIR__, "src", "assets", "user-statistics.svg") + +# Julia package-server regions are operational regions, not user countries. +const REGION_LOCATION = Dict( + "us-east" => (label = "US East", longitude = -77.0, latitude = 38.0, + offset = (12, 14), align = (:left, :bottom)), + "us-west" => (label = "US West", longitude = -122.0, latitude = 38.0, + offset = (-12, 14), align = (:right, :bottom)), + "eu-central" => (label = "EU Central", longitude = 10.0, latitude = 50.0, + offset = (0, 18), align = (:center, :bottom)), + "eu-north" => (label = "EU North", longitude = 18.0, latitude = 60.0, + offset = (0, 18), align = (:center, :bottom)), + "au" => (label = "Australia", longitude = 151.0, latitude = -33.0, + offset = (0, 18), align = (:center, :bottom)), + "jp" => (label = "Japan", longitude = 139.7, latitude = 35.7, + offset = (12, 14), align = (:left, :bottom)), + "in" => (label = "India", longitude = 77.2, latitude = 28.6, + offset = (0, 18), align = (:center, :bottom)), + "kr" => (label = "Korea", longitude = 127.0, latitude = 37.5, + offset = (-12, 14), align = (:right, :bottom)), + "sa" => (label = "South America", longitude = -46.6, latitude = -23.5, + offset = (0, 18), align = (:center, :bottom)), + "sg" => (label = "Singapore", longitude = 103.8, latitude = 1.3, + offset = (0, -18), align = (:center, :top)), + "cn-east" => (label = "China East", longitude = 121.5, latitude = 31.2, + offset = (-12, 14), align = (:right, :bottom)), + "cn-northeast" => (label = "China Northeast", longitude = 123.4, latitude = 41.8, + offset = (12, 14), align = (:left, :bottom)), + "cn-southeast" => (label = "China Southeast", longitude = 113.3, latitude = 23.1, + offset = (0, -18), align = (:center, :top)) +) + +function package_regions(path::AbstractString) + counts = Dict{String, Float64}() + minimum_date = nothing + maximum_date = nothing + + for row in CSV.File(path) + ismissing(row.package_uuid) && continue + String(row.package_uuid) == PACKAGE_UUID || continue + ismissing(row.client_type) && continue + String(row.client_type) == "user" || continue + ismissing(row.region) && continue + region = String(row.region) + haskey(REGION_LOCATION, region) || continue + ismissing(row.request_addrs) && continue + + counts[region] = get(counts, region, 0.0) + Float64(row.request_addrs) + if !ismissing(row.date_min) + minimum_date = isnothing(minimum_date) ? row.date_min : + min(minimum_date, row.date_min) + end + if !ismissing(row.date_max) + maximum_date = isnothing(maximum_date) ? row.date_max : + max(maximum_date, row.date_max) + end + end + + rows = [(; region, request_addrs) for (region, request_addrs) in counts] + sort!(rows; by = row -> row.request_addrs, rev = true) + dates = (; minimum = minimum_date, maximum = maximum_date) + return first(rows, min(10, length(rows))), dates +end + +function land_geometries(path::AbstractString) + collection = GeoJSON.read(read(path, String)) + return GeoInterface.geometry.(collect(collection)) +end + +function draw_statistics(rows, dates, land_path::AbstractString, output::AbstractString) + figure = Figure( + size = (1050, 480), + figure_padding = 8, + backgroundcolor = RGBf(0.95, 0.97, 0.99) + ) + axis = Axis( + figure[2, 1]; + aspect = DataAspect(), + limits = (-180, 180, -60, 88), + width = 1000, + height = 395, + backgroundcolor = RGBf(0.68, 0.86, 0.97) + ) + hidedecorations!(axis) + hidespines!(axis) + poly!( + axis, + land_geometries(land_path); + color = RGBf(0.96, 0.96, 0.93), + strokecolor = RGBf(0.68, 0.72, 0.73), + strokewidth = 0.5 + ) + + if isempty(rows) + text!(axis, 0, 5; + text = "No public package-server usage recorded yet", + align = (:center, :center), + fontsize = 24, + color = RGBf(0.12, 0.18, 0.24)) + period = "awaiting the first registered-package requests" + else + maximum_count = maximum(row.request_addrs for row in rows) + for row in rows + location = REGION_LOCATION[String(row.region)] + size = 14 + 32sqrt(row.request_addrs / maximum_count) + scatter!(axis, [location.longitude], [location.latitude]; + markersize = size, + color = RGBAf(0.05, 0.29, 0.53, 0.78), + strokecolor = :white, + strokewidth = 1.5) + text!(axis, location.longitude, location.latitude; + text = "$(location.label) · $(round(Int, row.request_addrs))", + offset = location.offset, + align = location.align, + fontsize = 13, + color = RGBf(0.05, 0.10, 0.15)) + end + period = "$(dates.minimum) to $(dates.maximum)" + end + + Label(figure[1, 1], "LineCableModels.jl package users"; + fontsize = 25, + font = :bold, + color = RGBf(0.05, 0.10, 0.15)) + Label(figure[3, 1], + "Top Julia package-server regions · summed request addresses · $period"; + fontsize = 13, + color = RGBf(0.22, 0.28, 0.34)) + rowgap!(figure.layout, 3) + mkpath(dirname(output)) + save(output, figure) + return output +end + +function replace_statistics(rows, dates, land_path::AbstractString, output::AbstractString) + mkpath(dirname(output)) + mktempdir(dirname(output)) do directory + staged = joinpath(directory, basename(output)) + draw_statistics(rows, dates, land_path, staged) + mv(staged, output; force = true) + end + return output +end + +function main(args = ARGS) + source = isempty(args) ? Downloads.download(LOG_URL) : abspath(args[1]) + output = length(args) < 2 ? DEFAULT_OUTPUT : abspath(args[2]) + land = length(args) < 3 ? Downloads.download(LAND_URL) : abspath(args[3]) + rows, dates = package_regions(source) + result = replace_statistics(rows, dates, land, output) + @info "Generated package-server usage map" result regions = length(rows) + return result +end + +if abspath(PROGRAM_FILE) == @__FILE__ + main() +end diff --git a/docs/src/CHANGELOG.md b/docs/src/CHANGELOG.md deleted file mode 100644 index ed9a2d870..000000000 --- a/docs/src/CHANGELOG.md +++ /dev/null @@ -1,35 +0,0 @@ -```@meta -EditURL = "https://github.com/Electa-Git/LineCableModels.jl/blob/master/CHANGELOG.md" -``` - -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] - -- / - -### Changed - -- Refactored CodeComponent structure and constructors. - -### Added - -- Included interface to run finite element simulations using [Onelab](https://onelab.info/). - -- Included basic coverage tests. - -- Included import/export of CableDesigns to JSON files. - -## [v0.1.0](https://github.com/Electa-Git/LineCableModels.jl/releases/tag/v0.1.0) - 2025-03-29 - -### Added - -- First release. See [README.md](https://github.com/Electa-Git/LineCableModels.jl/blob/main/README.md) for more details. - -[Unreleased]: [https://github.com/Electa-Git/LineCableModels.jl/compare/v0.1.0...HEAD](https://github.com/Electa-Git/LineCableModels.jl/compare/v0.1.0...HEAD) -[v0.1.0](https://github.com/Electa-Git/LineCableModels.jl/releases/tag/v0.1.0): [https://github.com/Electa-Git/LineCableModels.jl/releases/tag/v0.1.0](https://github.com/Electa-Git/LineCableModels.jl/releases/tag/v0.1.0) diff --git a/docs/src/TODO.md b/docs/src/TODO.md deleted file mode 100644 index e044b1574..000000000 --- a/docs/src/TODO.md +++ /dev/null @@ -1,19 +0,0 @@ -# TODO for LineCableModels.jl - -This is a living document intended to track scientific development priorities and research directions for new features, methods and solutions to be included in the package. - -For bugs, features and implementation taks, the [Issues](https://github.com/Electa-Git/LineCableModels.jl/issues) page is used. - -## Wishlist - -- [ ] Pipe-type cables and MoM-SO implementation. - -## In progress - -- [ ] Implementation of frequency-dependent soil properties. -- [ ] Development of novel formulations for cables composed of N concentrical layers, allowing for accurate representations of semiconductor materials. -- [ ] Implementation of an interface to run finite element simulations using [Onelab](https://onelab.info/). - -## Done ✓ - -- [x] Object-oriented data model for cables, conductors, insulations and materials. diff --git a/docs/src/assets/cable_sc_armor_uncertainties.svg b/docs/src/assets/cable_sc_armor_uncertainties.svg new file mode 100644 index 000000000..8e4b6235f --- /dev/null +++ b/docs/src/assets/cable_sc_armor_uncertainties.svg @@ -0,0 +1,1329 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Rosca + + + + + + Rosca.5 + + + + + + Rosca.6 + + + + + + Círculo de Arrasto Central + + + + + + Planilha.8 + + + Planilha.9 + + + Planilha.10 + + + Planilha.11 + + + Planilha.12 + + + Planilha.13 + + + Planilha.14 + + + Planilha.15 + + + Planilha.16 + + + Planilha.17 + 𝑟1±δ + + + + 𝑟1±δr1 + Planilha.19 + + + Planilha.24 + + + Planilha.26 + + + Planilha.28 + + + Planilha.30 + + + Planilha.33 + + + Planilha.35 + Core (𝜌1±δ, 𝜇1±δ, 𝜀1±δ) + + + + Core (𝜌1±δρ1, 𝜇1±δμ1, 𝜀1±δε1) + Planilha.36 + Sheath (𝜌2±δ, 𝜇2±δ, 𝜀2±δ) + + + + Sheath (𝜌2±δρ2, 𝜇2±δμ2, 𝜀2±δε2) + Planilha.37 + Armor (𝜌3±δ, 𝜇3±δ, 𝜀3±δ) + + + + Armor (𝜌3±δρ3, 𝜇3±δμ3, 𝜀3±δε3) + Planilha.38 + + + Planilha.39 + + + Planilha.40 + + + Planilha.41 + Insulation 1 (𝜀i1±δ) + + + + Insulation 1 (𝜀i1±δεi1) + Planilha.42 + + + Planilha.43 + Insulation 2 (𝜀i2±δ) + + + + Insulation 2 (𝜀i2±δεi2) + Planilha.44 + + + Planilha.45 + Insulation 3 (𝜀i3±δ) + + + + Insulation 3 (𝜀i3±δεi3) + Planilha.46 + + + Planilha.60 + 𝑟2±δ + + + + 𝑟2±δr2 + Planilha.62 + 𝑟3±δ + + + + 𝑟3±δr3 + Planilha.63 + 𝑟4±δ + + + + 𝑟4±δr4 + Planilha.64 + 𝑟5±δ + + + + 𝑟5±δr5 + Planilha.65 + 𝑟6±δ + + + + 𝑟6±δr6 + Planilha.66 + 𝑟7±δ + + + + 𝑟7±δr7 + Planilha.67 + Surrounding earth (𝜌g±δ, 𝜇g±δ, 𝜀g±δ) + + + + Surrounding earth (𝜌g±δρg, 𝜇g±δμg, 𝜀g±δεg) + Planilha.68 + Core center (𝑥±δ, 𝑧±δ) + + + + Core center(𝑥±δx, 𝑧±δz) + diff --git a/docs/src/assets/custom.css b/docs/src/assets/custom.css index 94934f62a..f49604630 100644 --- a/docs/src/assets/custom.css +++ b/docs/src/assets/custom.css @@ -20,7 +20,7 @@ article#documenter-page img { display: block !important; - margin: auto !important; + margin: 0.75rem auto !important; } /* Improved scrollbar styling for code blocks rendered in Chrome */ @@ -60,7 +60,7 @@ table { overflow-x: auto; /* Use auto instead of scroll */ max-width: 100%; - /* Ensure content doesn't exceed container */ + /* Ensure content does not exceed its container */ } /* Modern scrollbar styling using standard CSS */ @@ -147,4 +147,4 @@ table { .data-frame { scrollbar-width: thin; scrollbar-color: #888 transparent; -} \ No newline at end of file +} diff --git a/docs/src/assets/custom.js b/docs/src/assets/custom.js index 34f1e03e2..e060bb450 100644 --- a/docs/src/assets/custom.js +++ b/docs/src/assets/custom.js @@ -1,6 +1,8 @@ +// ```javascript // document.addEventListener("DOMContentLoaded", function () { // localStorage.setItem("documenter-theme", "catppuccin-mocha"); // }); +// ``` (function () { const theme = "documenter-dark"; @@ -14,6 +16,7 @@ document.documentElement.setAttribute("data-theme", theme); })(); +// ```javascript // document.addEventListener("DOMContentLoaded", function () { // document.querySelectorAll("html.theme--catppuccin-mocha a").forEach(el => { // el.addEventListener("mouseover", function () { @@ -21,9 +24,10 @@ // this.style.color = "#4493f8"; // } // }); - +// // el.addEventListener("mouseout", function () { // this.style.color = ""; // Resets to default when not hovered // }); // }); -// }); \ No newline at end of file +// }); +// ``` \ No newline at end of file diff --git a/docs/src/assets/user-statistics.svg b/docs/src/assets/user-statistics.svg new file mode 100644 index 000000000..b8a164994 --- /dev/null +++ b/docs/src/assets/user-statistics.svg @@ -0,0 +1,574 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/src/bibliography.bib b/docs/src/bibliography.bib new file mode 100644 index 000000000..215b7bb07 --- /dev/null +++ b/docs/src/bibliography.bib @@ -0,0 +1,789 @@ +@misc{NIST:DLMF, +key = {DLMF}, +title = {NIST Digital Library of Mathematical Functions}, +note = {Olver, F. W. J. and Olde Daalhuis, A. B. and Lozier, D. W. and Schneider, B. I. and Boisvert, R. F. and Clark, C. W. and Miller, B. R. and Saunders, B. V. and Cohl, H. S. and McClain, M. A., eds.}, +year = {2025}, +institution = {National Institute of Standards and Technology (NIST)}, +howpublished = {[{https://dlmf.nist.gov/}]({https://dlmf.nist.gov/}), Release 1.2.4 of 2025-03-15}, +url = {https://dlmf.nist.gov/}, +} + +@ARTICLE{6897971, + author={Vujević, Slavko and Lovrić, Dino and Boras, Vedran}, + journal={IEEE Transactions on Electromagnetic Compatibility}, + title={High-Accurate Numerical Computation of Internal Impedance of Cylindrical Conductors for Complex Arguments of Arbitrary Magnitude}, + year={2014}, + volume={56}, + number={6}, + pages={1431-1438}, + keywords={Impedance;Conductors;Approximation methods;Skin effect;Mathematical model;Computational modeling;Large complex arguments;modified Bessel functions;solid cylindrical conductor;tubular cylindrical conductor;Large complex arguments;modified Bessel functions;solid cylindrical conductor;tubular cylindrical conductor}, + doi={10.1109/TEMC.2014.2352398}} + + +@ARTICLE{5437464, + author={Papadopoulos, Theofilos A. and Tsiamitros, Dimitrios A. and Papagiannis, Grigoris K.}, + journal={IEEE Transactions on Power Delivery}, + title={Impedances and Admittances of Underground Cables for the Homogeneous Earth Case}, + year={2010}, + volume={25}, + number={2}, + pages={961-969}, + keywords={Impedance;Earth;Conductors;Admittance;Electromagnetic transients;Power system transients;Underground power cables;Electromagnetic propagation;Integral equations;Power cables;Earth return admittance;earth return impedance;electromagnetic transients;power cable modeling}, + doi={10.1109/TPWRD.2009.2034797} +} + +@ARTICLE{app14198982, + author = {Riba, Jordi-Roger}, + title = {The Role of AC Resistance of Bare Stranded Conductors for Developing Dynamic Line Rating Approaches}, + journal = {Applied Sciences}, + volume = {14}, + year = {2024}, + number = {19}, + article-number = {8982}, + url = {https://www.mdpi.com/2076-3417/14/19/8982}, + issn = {2076-3417}, + doi = {10.3390/app14198982} +} + +@ARTICLE{6521501, + author={Morgan, Vincent T.}, + journal={IEEE Transactions on Power Delivery}, + title={The Current Distribution, Resistance and Internal Inductance of Linear Power System Conductors—A Review of Explicit Equations}, + year={2013}, + volume={28}, + number={3}, + pages={1252-1262}, + keywords={Conductors;Resistance;Inductance;Current density;Wires;Solids;Equations;Current density;eddy currents;hysteresis;internal inductance;power loss;proximity effect;resistance;skin effect;transformer effect}, + doi={10.1109/TPWRD.2012.2213617} +} + +@INPROCEEDINGS{yang2008gmr, + author = {Yang, Y. and Fortin, S. and Ma, J. and Dawalibi, F. P.}, + title = {GMR of Stranded Multizone Conductors}, + booktitle = {The 4th IASTED Asian Conference on Power and Energy Systems (AsiaPES)}, + year = {2008}, + keywords = {GMR, multizone, stranded conductor, strand pattern, current density, frequency dependence}, + note = {[Link]({https://www.actapress.com/Abstract.aspx?paperId=33058})} +} + +@INPROCEEDINGS{916943, + author={Gustavsen, B.}, + booktitle={2001 IEEE Power Engineering Society Winter Meeting. Conference Proceedings (Cat. No.01CH37194)}, + title={Panel session on data for modeling system transients insulated cables}, + year={2001}, + volume={2}, + number={}, + pages={718-723 vol.2}, + keywords={Power cable insulation}, + doi={10.1109/PESW.2001.916943} +} + +@ARTICLE{1458878, + author={Gustavsen, B. and Martinez, J.A. and Durbak, D.}, + journal={IEEE Transactions on Power Delivery}, + title={Parameter determination for modeling system transients-Part II: Insulated cables}, + year={2005}, + volume={20}, + number={3}, + pages={2045-2050}, + keywords={Cable insulation;Cables;Magnetic materials;Conducting materials;Material properties;Semiconductor materials;Frequency;Impedance;Shunt (electrical);Admittance;Insulated cables;modeling;power system transients;simulation}, + doi={10.1109/TPWRD.2005.848774} +} + +@ARTICLE{5743045, + author={Gudmundsdottir, Unnur Stella and Gustavsen, Bjørn and Bak, Claus Leth and Wiechowski, Wojciech}, + journal={IEEE Transactions on Power Delivery}, + title={Field Test and Simulation of a 400-kV Cross-Bonded Cable System}, + year={2011}, + volume={26}, + number={3}, + pages={1403-1410}, + keywords={Power cables;Cable shielding;Current measurement;Voltage measurement;Conductors;Bonding;Grounding;Insulated cables;model accuracy;modeling;power system transients;proximity effect;simulation}, + doi={10.1109/TPWRD.2010.2084600} +} + +@ARTICLE{4113884, + author={Ametani, A.}, + journal={IEEE Transactions on Power Apparatus and Systems}, + title={A General Formulation of Impedance and Admittance of Cables}, + year={1980}, + volume={PAS-99}, + number={3}, + pages={902-910}, + keywords={Impedance;Admittance;Coaxial cables;Transient analysis;Earth;Conductors;Voltage;Equations;Dielectric losses;Underwater cables}, + doi={10.1109/TPAS.1980.319718} +} + +@techreport{cigre345, + author = {{CIGRE Working Group B2.12}}, + title = {Alternating Current (AC) Resistance of Helically Stranded Conductors}, + institution = {CIGRE}, + year = {2008}, + month = {April}, + number = {345}, + type = {Technical Brochure}, + note = {[Link]({https://www.e-cigre.org/publications/detail/345-alternating-current-ac-resistance-of-helically-stranded-conductors.html})} +} + +@techreport{cigre531, + author = {{CIGRE Working Group B1.30}}, + title = {Cable Systems Electrical Characteristics}, + institution = {CIGRE}, + year = {2013}, + month = {April}, + number = {531}, + type = {Technical Brochure}, + note = {[Link]({https://www.e-cigre.org/publications/detail/531-cable-systems-electrical-characteristics.html})} +} + +@book{rosa1908, + title = {The self and mutual-inductances of linear conductors}, + author = {Rosa, E. B.}, + year = {1908}, + publisher = {National Bureau of Standards}, + volume = {4}, + number = {2}, + issn = {0096-8579}, + doi = {10.6028/bulletin.088}, +} + +@ARTICLE{Karmokar2025, + author = {Karmokar, T. and Popov, M.}, + title = {Enhanced Modelling and Parameter Determination of HVDC Cables Using Practice-Oriented Methodology}, + year = {2025}, + journal = {CIGRE Science and Engineering}, + volume = {2025-February}, + number = {36}, + url = {https://www.scopus.com/inward/record.uri?eid=2-s2.0-85218793172&partnerID=40&md5=415867c192cfa930318607b68cd8cd69}, + type = {Article}, +} + +@book{grover1981inductance, + title={Inductance Calculations: Working Formulas and Tables}, + author={Grover, F.W. and Instrument Society of America}, + isbn={9780876645574}, + lccn={lc82126260}, + series={Dover books on engineering and engineering physics}, + year={1981}, + publisher={Instrument Society of America} +} + +@techreport{CENELEC50182, + type = {Standard}, + key = {EN 50182:2001}, + author = {Technical Committee CENELEC TC 7}, + title = {Conductors for Overhead Lines---Round Wire Concentric Lay Stranded Conductors}, + institution = {European Committee for Electrotechnical Standardization}, + year = {2001}, + isbn = {0 580 38001 7}, + pages = {1--73}, + note = {[Link]({https://www.en-standard.eu/bs-en-50182-2001-conductors-for-overhead-lines-round-wire-concentric-lay-stranded-conductors/})} +} + +@ARTICLE{4389974, + author={Oussalah, Naima and Zebboudj, Youcef and Boggs, Steven A.}, + journal={IEEE Electrical Insulation Magazine}, + title={Partial Discharge Pulse Propagation in Shielded Power Cable and Implications for Detection Sensitivity}, + year={2007}, + volume={23}, + number={6}, + pages={5-10}, + keywords={Partial discharges;Power cables;Attenuation measurement;Cable shielding;Frequency;Power measurement;Corona;Wideband;Material properties;Software tools}, + doi={10.1109/MEI.2007.4389974} +} + +@techreport{IEC60287, + title = {{IEC 60287} - Electric cables -- Calculation of the current rating}, + author = {{IEC technical committee 20: Electric cables}}, + institution = {{International Electrotechnical Commission}}, + year = {2023}, + type = {Standard}, + note = {[Link]({https://webstore.iec.ch/publication/261})}, +} + +@techreport{CENELEC_HD620_S3_2023, + type = {Standard}, + key = {HD 620 S3:2023}, + author = {Technical Committee CENELEC TC 20}, + title = {Distribution cables with extruded insulation for rated voltages from 3.6/6 (7.2) kV up to and including 20.8/36 (42) kV}, + institution = {European Committee for Electrotechnical Standardization}, + year = {2023}, + isbn = {978-2-8322-6203-0}, + pages = {1--150}, + note = {[Link]({https://standards.iteh.ai/catalog/standards/clc/ac6f2d1f-5ce8-4580-8d64-f40ac82b7125/hd-620-s3-2023})} +} + +@techreport{VDE_DIN_VDE_0276_620_2024, + type = {Standard}, + key = {DIN VDE 0276-620:2024-12}, + author = {Technical Committee VDE}, + title = {Distribution cables with extruded insulation for rated voltages from 3.6/6 (7.2) kV up to and including 20.8/36 (42) kV}, + institution = {VDE}, + year = {2024}, + isbn = {978-3-8007-6204-0}, + pages = {1--120}, + note = {[Link]({https://www.vde-verlag.de/standards/0200084/din-vde-0276-620-vde-0276-620-2024-12.html})} +} +@article{Ciuprina2024, + author = {Ciuprina, Gabriela and Sabriego, Ruth V.}, + title = {Electric circuit element boundary conditions for electromagneto-quasistatic and full wave models in {A}, {$\varphi$} potentials and their finite element implementation}, + journal = {Journal of Mathematics in Industry}, + volume = {14}, + number = {1}, + pages = {27}, + year = {2024}, + doi = {10.1186/s13362-024-00165-6} +} + +@article{Martins-BrittoLopes2020, + author = {Martins-Britto, Amauri Gutierrez and Lopes, Felipe V. and Rondineau, Sebastien Roland Marie Joseph}, + title = {Multilayer {Earth} {Structure} {Approximation} by a {Homogeneous} {Conductivity} {Soil} for {Ground} {Return} {Impedance} {Calculations}}, + journal = {IEEE Transactions on Power Delivery}, + volume = {35}, + number = {2}, + pages = {881--891}, + year = {2020}, + month = apr, + doi = {10.1109/TPWRD.2019.2930406}, + url = {https://doi.org/10.1109/TPWRD.2019.2930406} +} + +@article{Bernal1997, + author = {Bernal, Joaqu{\'i}n and Medina, Francisco and Horno, Manuel}, + title = {Quick quasi-{TEM} analysis of multiconductor transmission lines with rectangular cross section}, + journal = {IEEE Transactions on Microwave Theory and Techniques}, + year = {1997}, + volume = {45}, + number = {9}, + pages = {1619--1626}, + doi = {10.1109/22.622930} +} + +@article{Meixner1972, + author = {Meixner, J.}, + title = {The behavior of electromagnetic fields at edges}, + journal = {IEEE Transactions on Antennas and Propagation}, + year = {1972}, + volume = {20}, + number = {4}, + pages = {442--446}, + doi = {10.1109/TAP.1972.1140243} +} + +@article{VanBladel1985, + author = {Van Bladel, J.}, + title = {Field singularities at metal-dielectric wedges}, + journal = {IEEE Transactions on Antennas and Propagation}, + year = {1985}, + volume = {33}, + number = {4}, + pages = {450--455}, + doi = {10.1109/TAP.1985.1143588} +} + +@article{Classen2011, + author = {Classen, C. and Gjonaj, E. and R{\"o}mer, U. and Schuhmann, R. and Weiland, T.}, + title = {Modeling of field singularities at dielectric edges using grid based methods}, + journal = {Advances in Radio Science}, + year = {2011}, + volume = {9}, + pages = {39--44}, + doi = {10.5194/ars-9-39-2011} +} + +@article{HelsingOjala2008, + author = {Helsing, Johan and Ojala, Rikard}, + title = {On the evaluation of layer potentials close to their sources}, + journal = {Journal of Computational Physics}, + year = {2008}, + volume = {227}, + number = {5}, + pages = {2899--2921}, + doi = {10.1016/j.jcp.2007.11.024} +} + +@article{Campione2018, + author = {Campione, Salvatore and Warne, Larry K. and Langston, William L. and Basilio, Lorena I.}, + title = {First Principles Model of Electric Cable Braid Penetration with Dielectrics}, + journal = {Progress In Electromagnetics Research C}, + year = {2018}, + volume = {82}, + pages = {1--11}, + doi = {10.2528/PIERC17103010} +} + +@article{Schelkunoff1934, + author = {Schelkunoff, S. A.}, + title = {The Electromagnetic Theory of Coaxial Transmission Lines and Cylindrical Shields}, + journal = {Bell System Technical Journal}, + year = {1934}, + volume = {13}, + number = {4}, + pages = {532--579}, + doi = {10.1002/j.1538-7305.1934.tb00679.x} +} + +@book{Sunde1968, + author = {Sunde, Erling D.}, + title = {Earth Conduction Effects in Transmission Systems}, + publisher = {Dover Publications}, + address = {New York}, + year = {1968}, + note = {Corrected republication of the 1949 edition} +} +@article{Ametani1980, + title = {A {{General Formulation}} of {{Impedance}} and {{Admittance}} of {{Cables}}}, + author = {Ametani, A.}, + year = 1980, + month = may, + journal = {IEEE Transactions on Power Apparatus and Systems}, + volume = {PAS-99}, + number = {3}, + pages = {902--910}, + issn = {0018-9510}, + doi = {10.1109/TPAS.1980.319718}, + urldate = {2026-09-03}, + abstract = {Interest in the analysis of wave propagation characteristics and transients associated with cable systems has rapidly increased. In order to answer the need of the analyst,impedances and admittances of various cables have to be known. This paper describes a general formulation of impedances and admittances of single-core coaxial and pipe-type cables. The formulation presented here can handle a coaxial cable consisting of a core, sheath and armor, a pipe-type cable of which the pipe thickness is finite and an overhead cable, which has not been discussed in the literature heretofore.}, + copyright = {https://ieeexplore.ieee.org/Xplorehelp/downloads/license-information/IEEE.html}, + langid = {english}, + keywords = {docling/done}, +} + +@article{Ametani1992, + title = {Approximate Method for Calculating the Impedances of Multiconductors with Cross Sections of Arbitrary Shapes}, + author = {Ametani, Akihiro and Fuse, Ikuko}, + year = 1992, + journal = {Electrical Engineering in Japan}, + volume = {112}, + number = {2}, + pages = {117--123}, + issn = {0424-7760}, + doi = {10.1002/eej.4391120213}, + langid = {english}, + keywords = {formula-corpus/source} +} + +@article{Ametani2004, + title = {Semiconducting {{Layer Impedance}} and Its {{Effect}} on {{Cable Wave-Propagation}} and {{Transient Characteristics}}}, + author = {Ametani, A. and Miyamoto, Y. and Nagaoka, N.}, + year = 2004, + month = oct, + journal = {IEEE Transactions on Power Delivery}, + volume = {19}, + number = {4}, + pages = {1523--1531}, + issn = {0885-8977}, + doi = {10.1109/TPWRD.2003.822502}, + urldate = {2026-09-03}, + abstract = {This paper has derived an impedance formula for conductor's semiconducting layer based on a conventional circuit theory. The formula is confirmed to be identical to an accurate one derived by solving Maxwell's equation. A wave-propagation characteristic and a transient voltage on a cable having the semiconducting layer on the conductor's surface are evaluated by applying the derived formula, and are compared with those on a cable with no semiconducting layer. The semiconducting layer increases the conductor impedance, and thus, the attenuation constant is increased, and the propagation velocity and the characteristic impedance are decreased for a coaxial mode by the semiconducting layer, but the inter-phase mode of propagation is not affected. A transient voltage is attenuated more and its oscillating period becomes greater than those on a cable with no semiconducting layer. The effect of the semiconducting layer impedance on the wave-propagation characteristic and the transient voltage is rather minor when the layer thickness is small and the resistivity is high, and the semiconducting layer effect is dominated by its admittance.}, + copyright = {https://ieeexplore.ieee.org/Xplorehelp/downloads/license-information/IEEE.html}, + langid = {english}, +} + +@article{Ametani2009, + title = {An {{Investigation}} of {{Earth-Return Impedance Between Overhead}} and {{Underground Conductors}} and {{Its Approximation}}}, + author = {Ametani, Akihiro and Yoneda, Tetsuzo and Baba, Yoshihiro and Nagaoka, Naoto}, + year = 2009, + month = aug, + journal = {IEEE Transactions on Electromagnetic Compatibility}, + volume = {51}, + number = {3}, + pages = {860--867}, + issn = {0018-9375}, + doi = {10.1109/TEMC.2009.2019953}, + urldate = {2026-09-03}, + abstract = {This paper investigates numerical instability issues in the mutual impedance formula between an overhead conductor and a buried conductor proposed by Pollaczek with special reference to the integrand of Pollaczek's infinite integral. The integrand shows a negative real part and positive imaginary part at a high frequency, which result in negative resistance and inductance. The reason for this is the assumption of TEM-mode propagation in Pollaczek's formula as is well known. The fact suggests that numerical instability of the infinite integral is caused by incorrect integrand in a high-frequency region. Based on the observations, the applicable limit of Pollacezk's formula is analytically derived. Finally, a simple closed-form formula is developed, which approximates Pollaczek's formula with satisfactory accuracy.}, + copyright = {https://ieeexplore.ieee.org/Xplorehelp/downloads/license-information/IEEE.html}, + langid = {english}, + keywords = {Analytical,Closed-form,docling/error,Earth impedance,Electromagnetic interference,Ground return,Underground cables}, +} + +@incollection{Ametani2015b, + title = {Impedance and Admittance Formulas}, + author = {Ametani, Akihiro}, + booktitle = {Cable System Transients: Theory, Modeling and Simulation}, + bookauthor = {Ametani, Akihiro and Ohno, Teruo and Nagaoka, Naoto}, + year = {2015}, + chapter = {2}, + pages = {21--62}, + publisher = {Wiley-IEEE Press}, + doi = {10.1002/9781118702154.ch2} +} + +@article{Bormann2013, + title = {Reluctance Network Method for Calculating the Series Impedance Matrix of Multi-Conductor Transmission Lines}, + author = {Bormann, Dierk and Tavakoli, Hanif}, + year = 2013, + journal = {IEEE Transactions on Magnetics}, + volume = {49}, + number = {10}, + pages = {5270--5279}, + doi = {10.1109/TMAG.2013.2261999} +} + +@article{Carson1926, + title = {Wave {{Propagation}} in {{Overhead Wires}} with {{Ground Return}}}, + author = {Carson, John R.}, + year = 1926, + month = oct, + journal = {Bell System Technical Journal}, + volume = {5}, + number = {4}, + pages = {539--554}, + issn = {00058580}, + doi = {10.1002/j.1538-7305.1926.tb00122.x}, + urldate = {2026-09-04}, + langid = {english}, + keywords = {Ground return}, +} + +@article{DeConti2023a, + title = {Closed-{{Form Expressions}} for the {{Calculation}} of the {{Ground-Return Impedance}} and {{Admittance}} of {{Underground Cables}}}, + author = {De Conti, Alberto and Duarte, Naiara and Alipio, Rafael}, + year = 2023, + month = aug, + journal = {IEEE Transactions on Power Delivery}, + volume = {38}, + number = {4}, + pages = {2891--2900}, + issn = {0885-8977, 1937-4208}, + doi = {10.1109/TPWRD.2023.3264614}, + urldate = {2026-09-03}, + abstract = {This paper proposes closed-form expressions for the calculation of the ground-return impedance and admittance of underground cables. The proposed expressions are shown to reproduce the integral equations of Xue with great accuracy for a wide range of ground resistivities and frequencies. An error analysis, a comparison with closed-form expressions available in the literature, and time-domain simulations demonstrate the performance of the proposed equations, which avoid the calculation of improper integrals and present an accurate performance for typical cable arrangements.}, + copyright = {https://ieeexplore.ieee.org/Xplorehelp/downloads/license-information/IEEE.html}, + langid = {english}, + keywords = {Analytical,Closed-form,docling/done,Earth impedance,Ground return,Underground cables}, +} + +@inproceedings{DeSilva2019, + title = {On Combining Classical and Numerical Techniques for Extracting the Impedance and Admittance of Cables and Overhead Lines}, + author = {De Silva, H. M. J. and Shafieipour, M.}, + year = 2019, + booktitle = {International Conference on Power Systems Transients (IPST)}, + address = {Perpignan, France} +} + +@article{Deri1981, + title = {The {{Complex Ground Return Plane}} a {{Simplified Model}} for {{Homogeneous}} and {{Multi-Layer Earth Return}}}, + author = {Deri, A. and Tevan, G. and Semlyen, A. and Castanheira, A.}, + year = 1981, + month = aug, + journal = {IEEE Transactions on Power Apparatus and Systems}, + volume = {PAS-100}, + number = {8}, + pages = {3686--3693}, + issn = {0018-9510}, + doi = {10.1109/TPAS.1981.317011}, + urldate = {2026-09-03}, + abstract = {For modelling current return in homogeneous ground, the paper introduces the concept of an ideal (superconducting) current return plane placed below the ground surface at a complex distance p equal to the complex penetration depth for plane waves. This "complex"' plane appears as a mirroring surface, so that conductor images can be used to derive very simple formulae for self and mutual impedances under ground return conditions. Such equations, without proofs, were originally proposed by Dubanton and published by Gary.1 In this paper, plausibility arguments serve to initially justify the procedure, then the equations are analytically related to those of Carson and, finally, the errors, which in most cases are less than a few percent, are numerically evaluated. The ideal return plane at complex depth can also be used for multi-layer earth return.}, + copyright = {https://ieeexplore.ieee.org/Xplorehelp/downloads/license-information/IEEE.html}, + langid = {english}, + keywords = {docling/done,Ground return,Multilayer soil}, +} + +@inproceedings{Fortin2005, + title = {Effects of Eddy Current on the Impedance of Pipe-Type Cables With Arbitrary Pipe Thickness}, + author = {Fortin, Simon and Yang, Y. and Ma, J. and Dawalibi, F. P.}, + year = 2005, + booktitle = {International Conference on Electrical Engineering}, + keywords = {formula-corpus/source} +} + +@article{Ghosh2022, + title = {A Generalized Approach to Deduce the Impedance and Admittance of Underground Cable Containing N Semiconductor Screens}, + author = {Ghosh, Swarnankur and Das, Supriyo}, + year = 2022, + journal = {Engineering Science and Technology, an International Journal}, + volume = {28}, + pages = {101029}, + doi = {10.1016/j.jestch.2021.06.009} +} + +@inproceedings{Gustavsen2001, + title = {Panel Session on Data for Modeling System Transients: Insulated Cables}, + author = {Gustavsen, B.}, + booktitle = {2001 IEEE Power Engineering Society Winter Meeting. Conference Proceedings}, + year = {2001}, + volume = {2}, + pages = {718--723}, + publisher = {IEEE}, + doi = {10.1109/PESW.2001.916943} +} + +@article{Gustavsen2005, + title = {Parameter Determination for Modeling System Transients---Part II: Insulated Cables}, + author = {Gustavsen, B. and Martinez, J. A. and Durbak, D.}, + year = 2005, + journal = {IEEE Transactions on Power Delivery}, + doi = {10.1109/TPWRD.2005.848774} +} + +@article{Gustavsen2023, + title = {A 2D FEM Model for Impedance and Loss Calculation of Armored Three-Core Cables With Inclusion of 3D Pitching Effects}, + author = {Gustavsen, Bjørn}, + year = 2023, + journal = {IEEE Transactions on Power Delivery}, + volume = {38}, + number = {5}, + pages = {3010--3020}, + doi = {10.1109/TPWRD.2023.3266868} +} + +@article{Hoidalen2013, + title = {Analysis of {{Pipe-Type Cable Impedance Formulations}} at {{Low Frequencies}}}, + author = {Hoidalen, Hans Kr.}, + year = 2013, + month = oct, + journal = {IEEE Transactions on Power Delivery}, + volume = {28}, + number = {4}, + pages = {2419--2427}, + issn = {0885-8977, 1937-4208}, + doi = {10.1109/TPWRD.2013.2272343}, + urldate = {2026-09-03}, + abstract = {The paper analyses series impedances of screen-less cables enclosed by a common conducting pipe. Established analytical formulas for pipe-type cables parameters are analyzed taking finite pipe thickness and proximity effect into account. Analytical low frequency approximations are developed and analyzed. The analysis shows that a classical formulation mixing infinite and finite pipe thickness can result in incorrect inductance and even negative self-resistance at low frequency. The paper analyses corrections to take a finite pipe thickness into account. Finite element simulations reveal that core-to-core proximity effect is substantial for the analyzed cable design with much lower high frequency inductance than obtained from analytical formulas. The paper also analyses given proximityeffect formulations and propose corrections based on analytical low frequency approximations. The proposed proximity effect corrections improve both resistance and inductance in both common and differential mode.}, + copyright = {https://ieeexplore.ieee.org/Xplorehelp/downloads/license-information/IEEE.html}, + langid = {english}, + keywords = {Analytical,FEM,Proximity effect}, +} + +@misc{Hoidalen2025, + title = {On Proximity Correction of Pipe-Type Cable Parameters with Method of Moment Approach}, + author = {Høidalen, Hans Kristian and Høyer-Hansen, Martin and Neto, Felipe Camara and Bak, Claus Leth}, + year = 2025, + doi = {10.2139/ssrn.5084160}, + howpublished = {SSRN preprint} +} + +@article{Kane1995, + title = {Multiwire {{Shielded Cable Parameter Computation}}}, + author = {Kane, Mamadou and Ahmad, Ahmad S. and Auriol, Philippe}, + year = 1995, + month = may, + journal = {IEEE Transactions on Magnetics}, + volume = {31}, + number = {3}, + pages = {1646--1649}, + issn = {0018-9464}, + doi = {10.1109/20.376350}, + langid = {english}, + keywords = {formula-corpus/source} +} + +@article{MartinsBritto2024, + title = {Transient {{Electromagnetic Interference}} between {{Overhead}} and {{Underground Conductors}}}, + author = {{Martins-Britto}, Amauri G. and Papadopoulos, Theofilos A. and Chrysochos, Andreas I.}, + year = 2024, + month = jun, + journal = {IEEE Transactions on Electromagnetic Compatibility}, + volume = {66}, + number = {3}, + pages = {983--992}, + issn = {0018-9375, 1558-187X}, + doi = {10.1109/TEMC.2024.3376971}, + urldate = {2026-09-04}, + abstract = {Electromagnetic interference (EMI) between overhead lines (OHL) and nearby pipelines may cause high-induced voltages. Their computation is important for the safe and reliable operation of the pipeline and necessitates the accurate evaluation of earth conduction effects on the impedance and admittance parameters of all conductors. In this article, a detailed formulation of the impedance and admittance parameters of OHL/pipeline configurations is presented and new expressions for the calculation of the mutual impedance and admittance between the overhead conductors and the buried pipeline are introduced. The mutual parameters for an OHL/pipeline configuration are evaluated by the new formulas and compared with those derived from existing ones, the method of moments and the finite element method. The propagation characteristics are also calculated justifying the use of the mutual admittance, that is typically disregarded in power systems EMI calculations. Transient simulation results with the developed formulas are performed to evaluate the importance of the proposed approach on EMI studies.}, + copyright = {https://ieeexplore.ieee.org/Xplorehelp/downloads/license-information/IEEE.html}, + langid = {english}, + keywords = {Electromagnetic interference,Electromagnetic transients,FEM,MoM,Overhead lines,Pipelines}, +} + +@article{Papadopoulos2010b, + title = {Impedances and {{Admittances}} of {{Underground Cables}} for the {{Homogeneous Earth Case}}}, + author = {Papadopoulos, Theofilos A. and Tsiamitros, Dimitrios A. and Papagiannis, Grigoris K.}, + year = 2010, + month = apr, + journal = {IEEE Transactions on Power Delivery}, + volume = {25}, + number = {2}, + pages = {961--969}, + issn = {0885-8977, 1937-4208}, + doi = {10.1109/TPWRD.2009.2034797}, + urldate = {2026-09-03}, + abstract = {A general formulation for the calculation of the influence of the earth return path on the impedances and the admittances of underground multiconductor power cable arrangements is presented in this paper. The expressions for the self and mutual earth correction terms are derived by a rigorous solution of the electromagnetic-field equations. The involved semiinfinite integrals are calculated by using a suitable numerical integration technique. The propagation characteristics of a single insulated conductor and of a typical three-phase single-core cable arrangement are investigated and are compared to the corresponding ones obtained by other approaches. Finally, the cable parameters calculated by the proposed method are used in a simulation of a fast transient in a three-phase single-core cable.}, + copyright = {https://ieeexplore.ieee.org/Xplorehelp/downloads/license-information/IEEE.html}, + langid = {english}, + keywords = {docling/done,docling/error,Ground return,Line parameters,Numerical integration,Power cables,Underground cables}, +} + +@article{Papadopoulos2011, + title = {Earth Return Admittances and Impedances of Underground Cables in Non-Homogeneous Earth}, + author = {Papadopoulos, T.A. and Tsiamitros, D.A. and Papagiannis, G.K.}, + year = 2011, + month = feb, + journal = {IET Generation, Transmission \& Distribution}, + volume = {5}, + number = {2}, + pages = {161--171}, + issn = {1751-8687, 1751-8695}, + doi = {10.1049/iet-gtd.2010.0228}, + urldate = {2026-09-03}, + abstract = {The influence of the stratified earth on the admittances and impedances of underground multiconductor arrangements is examined here. The electromagnetic characteristics of the stratified earth are included in a rigorous solution of the electromagnetic field equations, leading to new expressions for the self and mutual ground admittances and impedances of underground conductors. The new expressions are implemented for both a single insulated conductor and a typical three-phase single-core cable arrangement in various two-layer earth configurations. The derived propagation characteristics are compared with the corresponding ones obtained either by an approximate approach or for the homogeneous earth case. Finally, the modal propagation characteristics calculated by the proposed method are used in the simulation of steady-state high-frequency responses in the modal domain.}, + langid = {english}, + keywords = {docling/error,Earth admittance,Ground return,Modal analysis,Multilayer soil,Underground cables}, +} + +@article{Patel2014, + title = {Proximity-{{Aware Calculation}} of {{Cable Series Impedance}} for {{Systems}} of {{Solid}} and {{Hollow Conductors}}}, + author = {Patel, Utkarsh R. and Gustavsen, Bjorn and Triverio, Piero}, + year = 2014, + month = oct, + journal = {IEEE Transactions on Power Delivery}, + volume = {29}, + number = {5}, + pages = {2101--2109}, + issn = {0885-8977, 1937-4208}, + doi = {10.1109/TPWRD.2014.2330994}, + urldate = {2026-09-03}, + abstract = {Wideband cable models for the prediction of electromagnetic transients in power systems require an accurate calculation of the cable series impedance as a function of frequency. A surface current approach was recently proposed for systems of round solid conductors, with the inclusion of skin and proximity effects. In this paper, we extend the approach to include tubular conductors, allowing to model realistic cables with tubular sheaths, armors, and pipes. We also include the effect of a lossy ground. A noteworthy feature of the proposed technique is the accurate prediction of proximity effects, which can be of major importance in three-phase, pipe-type, and closely packed single-core cables. The new approach is highly efficient compared to finite elements. In the case of a cross-bonded cable system featuring three-phase conductors and three screens, the proposed technique computes the required 120 frequency samples in only six seconds of CPU time.}, + copyright = {https://ieeexplore.ieee.org/Xplorehelp/downloads/license-information/IEEE.html}, + langid = {english}, + keywords = {docling/error,Electromagnetic transients,FEM}, +} + +@article{Pollaczek1926, + title = {{\"Uber das Feld einer unendlich langen wechselstromdurchflossenen Einfachleitung}}, + author = {Pollaczek, Felix}, + year = 1926, + journal = {Elektrische Nachrichtentechnik}, + volume = {3}, + number = {9}, + pages = {339--359}, + langid = {ngerman}, + keywords = {formula-corpus/source} +} + +@article{Saad1996, + title = {A Closed-Form Approximation for Ground Return Impedance of Underground Cables}, + author = {Saad, O. and Gaba, G. and Giroux, M.}, + year = 1996, + month = jul, + journal = {IEEE Transactions on Power Delivery}, + volume = {11}, + number = {3}, + pages = {1536--1545}, + issn = {08858977}, + doi = {10.1109/61.517514}, + urldate = {2026-09-03}, + copyright = {https://ieeexplore.ieee.org/Xplorehelp/downloads/license-information/IEEE.html}, + langid = {english}, + keywords = {Analytical,Closed-form,docling/error,Earth impedance,Ground return,Underground cables}, +} + +@article{Tsiamitros2008, + title = {Earth {{Return Impedances}} of {{Conductor Arrangements}} in {{Multilayer Soils}}---{{Part I}}: {{Theoretical Model}}}, + shorttitle = {Earth {{Return Impedances}} of {{Conductor Arrangements}} in {{Multilayer Soils}}---{{Part I}}}, + author = {Tsiamitros, D.A. and Papagiannis, G.K. and Dokopoulos, P.S.}, + year = 2008, + month = oct, + journal = {IEEE Transactions on Power Delivery}, + volume = {23}, + number = {4}, + pages = {2392--2400}, + issn = {0885-8977, 1937-4208}, + doi = {10.1109/TPWRS.2008.923816}, + urldate = {2026-09-03}, + abstract = {The influence of earth stratification on the conductor impedances is investigated in this paper. A general solution of the electromagnetic-field equations for the case of overhead and underground transmission-line conductors of arbitrary topology and multilayer earth is presented. Generalized expressions for the self and mutual impedances of the conductors are derived. All existing approaches result from the generalized equations, when the corresponding approximations are applied. A suitable integration scheme is also proposed for the calculation of the complex integrals involved in the expressions.}, + copyright = {https://ieeexplore.ieee.org/Xplorehelp/downloads/license-information/IEEE.html}, + langid = {english}, + keywords = {docling/error,Earth impedance,Ground return,Multilayer soil,Soil modeling}, +} + +@article{Uribe2008, + title = {Calculating {{Mutual Ground Impedances Between Overhead}} and {{Buried Cables}}}, + author = {Uribe, F. A.}, + year = 2008, + journal = {IEEE Transactions on Electromagnetic Compatibility}, + volume = {50}, + number = {1}, + pages = {198--203}, + issn = {0018-9375}, + doi = {10.1109/TEMC.2007.915286}, + urldate = {2026-09-03}, + abstract = {Aerial and buried electric conductors often share the same right of way. When a phase-to-ground fault occurs, high transient-induced overvoltages appear from short-circuit currents to the ground. The mutual ground-loop impedances between both systems are given by the Pollaczek coupling integral that does not possess an analytic closed-form solution, and its integrand is highly oscillatory to perform a direct numerical integration. An efficient and accurate algorithmic evaluation of the Pollaczek coupling integral is presented in this paper. The obtained result is used in the numerical assessment of approximated formulas previously issued by Lucca, Wedepohl, and Comit\textasciiacute e Consultatif International T\textasciiacute el\textasciiacute ephonique et T\textasciiacute el\textasciiacute egraphique.}, + copyright = {https://ieeexplore.ieee.org/Xplorehelp/downloads/license-information/IEEE.html}, + langid = {english}, + keywords = {Analytical,Closed-form,docling/error,Earth impedance,Numerical integration,Short circuit,Underground cables}, +} + +@article{Wait1978, + title = {Excitation of Currents on a Buried Insulated Cable}, + author = {Wait, James R.}, + year = 1978, + journal = {Journal of Applied Physics}, + volume = {49}, + number = {2}, + pages = {876--881}, + doi = {10.1063/1.324619} +} + +@article{Weeks1984, + title = {Wave Propagation Characteristics in Underground Power Cable}, + author = {Weeks, W. and Diao, Yi}, + year = 1984, + journal = {IEEE Transactions on Power Apparatus and Systems}, + volume = {PAS-103}, + number = {10}, + pages = {2816--2826}, + doi = {10.1109/TPAS.1984.318279} +} + +@article{Wise1934, + title = {Propagation of {{High-Frequency Currents}} in {{Ground Return Circuits}}}, + author = {Wise, W.H.}, + year = 1934, + month = apr, + journal = {Proceedings of the IRE}, + volume = {22}, + number = {4}, + pages = {522--527}, + issn = {0096-8390}, + doi = {10.1109/JRPROC.1934.225868}, + urldate = {2026-09-03}, + abstract = {The electric field parallel to a ground return circuit is calculated without assuming that the frequency is so low that polarization currents in the ground may be neglected. It is found that the polarization currents may be included by replacing the r in Carson's well-known formulas by rV1l+i(e-1) 2cXo-.}, + copyright = {https://ieeexplore.ieee.org/Xplorehelp/downloads/license-information/IEEE.html}, + langid = {english}, + keywords = {docling/error,Ground return}, +} + +@article{Wise1948, + title = {Potential {{Coefficients}} for {{Ground Return Circuits}}}, + author = {Wise, W. Howard}, + year = 1948, + month = apr, + journal = {Bell System Technical Journal}, + volume = {27}, + number = {2}, + pages = {365--371}, + issn = {00058580}, + doi = {10.1002/j.1538-7305.1948.tb00913.x}, + urldate = {2026-09-03}, + langid = {english}, + keywords = {docling/error,Ground return}, +} + +@article{Xue2018b, + title = {Generalized {{Formulation}} of {{Earth-Return Impedance}}/{{Admittance}} and {{Surge Analysis}} on {{Underground Cables}}}, + author = {Xue, Haoyan and Ametani, Akihiro and Mahseredjian, Jean and Kocar, Ilhan}, + year = 2018, + month = dec, + journal = {IEEE Transactions on Power Delivery}, + volume = {33}, + number = {6}, + pages = {2654--2663}, + issn = {0885-8977, 1937-4208}, + doi = {10.1109/TPWRD.2018.2796089}, + urldate = {2026-09-03}, + abstract = {A generalized formulation of earth-return impedance and admittance for underground cables is proposed based on a complete solution of Maxwell equations. The frequency responses of wave propagation characteristics are evaluated by the newly derived formulas and compared to those found from existing formulas in software used for the simulation of electromagnetic transients. Surge simulation results with the proposed formulas show more damping and smooth convergence to steady state in comparison with those with existing formulas. The proposed formulas are validated with a field test on a 110-kV cross-bonded cable. It is also shown that the MoM-So method, augmented with admittance representation, verifies the results found from the new formulas presented in this paper.}, + copyright = {https://ieeexplore.ieee.org/Xplorehelp/downloads/license-information/IEEE.html}, + langid = {english}, + keywords = {Earth impedance,Electromagnetic transients,Ground return,MoM,MoM-SO,Underground cables}, +} + +@inproceedings{Yang2001, + title = {Computation of Cable Parameters for Pipe-Type Cables With Arbitrary Pipe Thicknesses}, + author = {Yang, Yixin and Ma, J. and Dawalibi, F. P.}, + year = 2001, + booktitle = {IEEE/PES Transmission and Distribution Conference and Exposition}, + doi = {10.1109/TDC.2001.971316} +} diff --git a/docs/src/bib.md b/docs/src/bibliography.md similarity index 89% rename from docs/src/bib.md rename to docs/src/bibliography.md index 0b1be7e1b..a987675ae 100644 --- a/docs/src/bib.md +++ b/docs/src/bibliography.md @@ -1,4 +1,4 @@ # Bibliography ```@bibliography -``` \ No newline at end of file +``` diff --git a/docs/src/cable-builder.md b/docs/src/cable-builder.md deleted file mode 100644 index 5431de25d..000000000 --- a/docs/src/cable-builder.md +++ /dev/null @@ -1,512 +0,0 @@ -# CableBuilder Developer Guide - -## Extending the Cable Modeling DSL - -This document describes the **internal grammar of the CableBuilder API** and the **required steps to add new cable elements**. - -The architecture separates responsibilities into four layers: - -1. **Shape payloads** — geometric primitives -2. **Cable parts** — attach electrical meaning -3. **Builders** — materialization functors -4. **Specs** — lazy blueprints supporting combinatorics (`Grid`) - -All cable layers follow the same extension pattern. - ---- - -# 1. Core Concepts - -## 1.1 Cable Parts - -Cable parts attach **electrical role** to **geometric shapes**. - -```julia -abstract type AbstractCablePart end - -@inline r_ex(p::AbstractCablePart) = r_ex(p.shape) -``` - -Two built-in roles exist: - -```julia -struct ConductorPart{L,T,S<:AbstractShape{L,T}} <: AbstractCablePart - cmp::Symbol - shape::S - material::Material{T} -end - -struct InsulatorPart{L,T,S<:AbstractShape{L,T}} <: AbstractCablePart - cmp::Symbol - shape::S - material::Material{T} -end -``` - -Both rely on identical promotion logic: - -```julia -function ConductorPart(cmp::Symbol, - shape::AbstractShape{L,Tshape}, - mat::Material{Tmat}) where {L,Tshape<:Real,Tmat<:Real} - - T = promote_type(Tshape,Tmat) - - s = convert(AbstractShape{L,T}, shape) - m = convert(Material{T}, mat) - - ConductorPart{L,T,typeof(s)}(cmp,s,m) -end -``` - -Identical pattern applies to `InsulatorPart`. - -### Contract - -A valid cable part requires: - -* `cmp::Symbol` (used to group parts into functional components) -* `shape <: AbstractShape` (used to determine geometric features of parts) -* `material::Material` (used to determine physical properties of parts) - -No logic is allowed inside the struct. - -Promotion must occur in outer constructors. - ---- - -# 2. Layout and Shape System - -Shapes encode **pure geometry**. - -Electrical semantics are handled by cable parts. - -## Layout markers - -```julia -abstract type AbstractLayout end - -struct Concentric <: AbstractLayout end -struct SectorShaped <: AbstractLayout end -``` - -## Shape interface - -```julia -abstract type AbstractShape{L<:AbstractLayout,T<:Real} end -``` - -Every shape must implement: - -```julia -r_in(shape) -r_ex(shape) -``` - -or fallback to the global default. These accessors are **mandatory**. - -All geometry logic in downstream physics relies on them. - ---- - -# 3. Shape Implementation Pattern - -Every shape file contains **three components**. - -### 3.1 The Vault (struct) - -Strict parametric storage. - -No logic. - -Example: - -```julia -struct SolidCore{L,T<:Real} <: AbstractShape{L,T} - r_ex::T -end -``` - ---- - -### 3.2 The Janitor (outer constructor) - -Handles promotion. - -```julia -SolidCore{L}(r_ex::T) where {L,T<:Real} = SolidCore{L,T}(r_ex) -``` - ---- - -### 3.3 The Diplomat (`convert`) - -Allows upgrading precision. - -Required for compatibility with: - -* `Measurements` -* grid sampling -* solver promotion - -```julia -function Base.convert(::Type{<:AbstractShape{L,T}}, - s::SolidCore{L}) where {L,T<:Real} - - SolidCore{L,T}(convert(T,r_ex(s))) -end -``` - ---- - -### 3.4 Accessors - -```julia -@inline r_in(s::SolidCore) = zero(typeof(s.r_ex)) -@inline r_ex(s::SolidCore) = s.r_ex -``` - -These must always exist, and are defined globally. Cases non-conformal to the typical `r_in` / `r_ex` pattern must specify custom accessors, e.g. `SolidCore`, `Enclosure`. - -Never access fields directly outside the shape file. - ---- - -# 4. Builders - -Builders are **functors that materialize cable parts**. - -They do not perform combinatorics. - -They receive the current stacking radius. - ---- - -## Example: Solid core - -```julia -struct SolidCoreBuilder{P,Tgeom<:Real,Tmat<:Real} - cmp::Symbol - r_ex::Tgeom - mat::Material{Tmat} -end -``` - -Constructor: - -```julia -@inline function SolidCoreBuilder{P}(cmp::Symbol, - r_ex::Tgeom, - mat::Material{Tmat}) where {P,Tgeom,Tmat} - - SolidCoreBuilder{P,Tgeom,Tmat}(cmp,r_ex,mat) -end -``` - -Materialization: - -```julia -@inline function (b::SolidCoreBuilder{P})(current_r::T) where {P,T<:Real} - - current_r != zero(T) && - error("Topological violation: Solid core must be at r=0.") - - P(b.cmp, SolidCore{Concentric}(b.r_ex), b.mat) -end -``` - ---- - -## Example: Tubular layer - -```julia -struct TubularBuilder{P,Tgeom<:Real,Tmat<:Real} - cmp::Symbol - t::Tgeom - mat::Material{Tmat} -end -``` - -Materialization: - -```julia -@inline function (b::TubularBuilder{P})(current_r::T) where {P,T<:Real} - - r_ex = current_r + b.t - - P(b.cmp, - TubularShape{Concentric}(current_r,r_ex), - b.mat) -end -``` - ---- - -# 5. Specs (Blueprint Layer) - -Specs represent **lazy parameter spaces**. - -They expand into builders during iteration. - -All specs subtype: - -```julia -abstract type AbstractSpec{Target} end -``` - ---- - -## SolidCoreSpec - -```julia -struct SolidCoreSpec{P,Tcmp,Tr,M<:AbstractSpec{Material}} <: - AbstractSpec{SolidCoreBuilder{P}} - - cmp::Tcmp - r_ex::Tr - mat::M -end -``` - -Constructor: - -```julia -SolidCoreSpec(::Type{P}, - cmp::Tcmp, - r_ex::Tr, - mat::M) where {P,Tcmp,Tr,M<:AbstractSpec{Material}} -``` - ---- - -## TubularPartSpec - -```julia -struct TubularPartSpec{P,Tcmp,Tt,M<:AbstractSpec{Material}} <: - AbstractSpec{TubularBuilder{P}} - - cmp::Tcmp - t::Tt - mat::M -end -``` - -Constructor: - -```julia -TubularPartSpec(::Type{P}, - cmp::Tcmp, - t::Tt, - mat::M) -``` - ---- - -# 6. Grid System - -`Grid` represents **deterministic parameter variation**. - -Example: - -```julia -Grid([0.02,0.025,0.03]) -``` - -Specs store parameters as grids. - -Iteration is performed through: - -```julia -Iterators.product(grid_args(spec)...) -``` - -Materialization occurs lazily. - ---- - -# 7. Builder Materialization - -Builders are executed by the stacking engine: - -```julia -build_layer(current_r, builders) -``` - -Each builder: - -``` -(current_r) -> CablePart -``` - -Stacking radius evolves via: - -``` -current_r → r_ex(part) -``` - ---- - -# 8. API layer - -User-facing constructors exist in DSL modules: - -Example: - -```julia -Conductor.Solid(cmp,material;r) -Conductor.Tubular(cmp,material;t) -``` - -They construct specs: - -``` -SolidCoreSpec(...) -TubularPartSpec(...) -``` - ---- - -# 9. Extending the API - -To add a new cable element **all steps below are mandatory**. - ---- - -# Step 1 — Implement Shape - -File: `newshape.jl` - -Required components: - -``` -struct NewShape{L,T} <: AbstractShape{L,T} - fields... -end -``` - -Janitor: - -``` -function NewShape{L}(args...) where {L} - T = promote_type(typeof(arg1), typeof(arg2)...) - return NewShape{L, T}(convert(T, arg1), convert(T, arg2)...) -end -``` - -Diplomat: - -``` -function Base.convert(::Type{<:AbstractShape{L,T}}, s::NewShape{L}) where {L, T <: Real} - return NewShape{L, T}(convert(T, ...), convert(T, ...), ...) -end -``` - -Accessors: - -``` -r_in(s) -r_ex(s) -``` - ---- - -# Step 2 — Implement Builder - -``` -struct NewShapeBuilder{P,Tgeom,Tmat} -``` - -Constructor: - -``` -NewShapeBuilder{P}(args...) -``` - -Functor: - -``` -(b::NewShapeBuilder)(current_r) -``` - -Must return: - -``` -P(cmp, NewShape(...), material) -``` - ---- - -# Step 3 — Implement Spec - -``` -struct NewShapeSpec{P,...} <: AbstractSpec{NewShapeBuilder{P}} -``` - -Constructor: - -``` -NewShapeSpec(::Type{P}, ...) -``` - ---- - -# Step 4 — Define Grid Arguments - -If parameters are gridable, ensure they propagate through the global `grid_args` or specialize if necessary. - ---- - -# Step 5 — Implement DSL Constructor - -Example: - -```julia -function Conductor.NewShape(cmp::Symbol, mat; parameters...) -``` - -Must return: - -``` -NewShapeSpec(ConductorPart,...) -``` - ---- - -# 10. Stacking Engine - -Stacking is implemented through tuple recursion. - -```julia -build_layer(r,builders) -``` - -Each layer returns: - -``` -(part, build_layer(...)) -``` - -Final object: - -``` -CableDesign{Tuple}(parts) -``` - ---- - -# 11. Required Guarantees - -Every new element must satisfy: - -* No abstract fields in builders -* No closures -* Pure struct functors -* Explicit promotion -* Correct `convert` implementation -* Valid `r_in` / `r_ex` accessors - -Failure to implement any of these breaks stacking or solver compatibility. - ---- - -# End of Guide diff --git a/docs/src/contributing.md b/docs/src/contributing.md new file mode 100644 index 000000000..5a483b843 --- /dev/null +++ b/docs/src/contributing.md @@ -0,0 +1,28 @@ +# Contributing + +Use Julia 1.12 and instantiate the package environment before making changes: + +```julia +using Pkg +Pkg.instantiate() +Pkg.test() +``` + +Format maintained Julia files with `JuliaFormatter.format(".")`. +Use `type(scope): description` for commit subjects, with a lowercase imperative +description. Keep the complete subject within 72 characters. A breaking change +may use `type(scope)!: description`. Commit bodies are optional. + +Install Gitlint and enable its native commit hook once per clone: + +```bash +uv tool install gitlint-core +gitlint install-hook +``` + +The hook checks messages against `.gitlint` before creating a commit. +Gitlint keeps its default exemptions for merge, revert and fixup commits. + +Keep pull requests focused. Add tests for changed behavior and update public +documentation when an API changes. Optional plotting integrations must remain +outside core loading and must be checked with their dedicated workflows. diff --git a/docs/src/conveniences.md b/docs/src/conveniences.md new file mode 100644 index 000000000..d2dfdd575 --- /dev/null +++ b/docs/src/conveniences.md @@ -0,0 +1,203 @@ +# Construction vocabulary + +The practical construction functions lower directly to `Region`, `Stack`, +`Group`, `Assembly`, and `Enclosure`. These stateless functions omit serialized wrapper types. + +## Regions and ordered layers + +```julia +phase = terminal( + :phase, + core(copper; r=8e-3), + screen(semicon; t=0.8e-3), + insulation(xlpe; t=6e-3), +) +``` + +Arguments to `terminal`, `layers`, and `build(CableDesign, ...)` are ordered +from the center outward. `solid` and `shell` are the general escape hatches for +physical roles not covered by `core`, `insulation`, `screen`, `sheath`, +`bedding`, `jacket`, and `filler`. + +## Wires, wire strands, and ropes + +Declare one fixed wire course with `wires`: + +```julia +screen_wires = wires( + copper; + shape=Disk(0.5e-3), + n=40, + r=20e-3, + gap_frac=0.02, + lay=LayRatio(10), +) +``` + +With a `Disk` geometric boundary, `stranded` creates a center wire and infers the maximum +complete `6k` course inventory. Without compaction circular wires retain their +natural shape. `compact=true` requests area-preserving deformation of those +same complete courses. A `Sector` geometric boundary maps a center strand and complete +`6k` courses into the sector, then reconstructs each strand as an +area-preserving clipped disk. Its deformation is intrinsic. It has no +compaction selector or separately supplied center wire. + +```julia +compacted_core = stranded( + copper; + shape=Disk(0.5e-3), + lay=(LayRatio(13), Pitch(0.15), LayAngle(0.2)), + compact=true, + boundary=Disk(sqrt(37) * 0.5e-3), +) +``` + +A schedule sharing one definition lifts its raw course values directly: + +```julia +round_core = stranded( + copper; + shape=Disk(0.5e-3), + lay=LayRatio(13, 12, 11), + boundary=Disk(3.5e-3), +) +``` + +The explicit tuple form above remains available when course definitions differ. + +`rope(item; ...)` applies the same course grammar to an existing physical +item. Every child retains its internal path declarations. An outer course adds +its own path. `armor` uses the same ring, path, clearance, and compaction +operations while resolving its course radius from the preceding geometric boundary. + +`@distribute` inserts only `n=capacity()`: + +```julia +automatic = @distribute wires( + copper; + shape=Disk(0.5e-3), + r=20e-3, + gap_frac=0.03, +) +``` + +## Tape + +One tape function covers conductive, semiconductive, and insulating systems: + +```julia +insulating_tapes = @distribute tape( + tape_insulation; + section=Rectangle(1.4e-3, 0.5e-3), + gap_frac=0.02, + lay=LayRatio(10), +) +``` + +This group contributes no terminal. A conductive tape becomes a terminal only +under an explicit terminal scope: + +```julia +screen_tape = @terminal :screen begin + tape( + copper; + section=Rectangle(3.0e-3, 0.5e-3), + n=4, + lay=LayRatio(12), + ) +end +``` + +## Independent members and ducts + +`cores(item; n, r, names)` retains one prototype and a repetition pattern. +`cores(member1, member2, ...)` and `assembly(member1, member2, ...)` preserve +explicit heterogeneous members and their poses. + +```julia +cells = @assembly begin + @at cell_a (-12e-3, 0.0) + @at cell_b ( 12e-3, 2e-3) φ=0.1 +end + +bank = @duct shape=Rectangle(40e-3, 24e-3) fill=concrete begin + cells +end +``` + +The corresponding pipe notation uses the same block grammar: + +```julia +contained = @pipe shape=Disk(20e-3) fill=air begin + cells +end +``` + +Homogeneous repeated ducts use a pattern and retain one prototype: + +```julia +bank = duct( + cell; + formation=Lattice(nx=3, ny=2, dx=12e-3, dy=12e-3), + shape=Rectangle(44e-3, 30e-3), + fill=concrete, +) +``` + +## Formations + +Formation functions return ordinary placed-cable declarations: + +```julia +placements = @trefoil design spacing=0.09 center=(0.0, -1.0) phase=(1, 2, 3) sheath=0 +``` + +Nonzero one-based values identify active phases. `0` marks a conductor selected +for grounded or eliminated-conductor reduction. The values are identifiers, not +electrical polarity or phase-angle signs. + +The function forms `trefoil`, `hflat`, and `vflat` accept the same data. +`at(placements, ...)` composes an outer translation and rotation without +copying the cable designs. + +## Wire-pattern estimates + +[`estimate_stranding`](@ref) and [`estimate_screen`](@ref) search deterministic wire +patterns and return a [`WireEstimate`](@ref). An infeasible estimate retains +ranked patterns and states which limits were not met. + +```julia +estimate = estimate_stranding(1000.0) +closest = estimate[:closest_area] +fewest_layers = estimate[:fewest_layers] +``` + +## VDE designation parsing + +The qualified [`LineCableModels.DataModel.vdeparse`](@ref) function decodes the +supported VDE/DIN 0271 and 0276 designation fields: + +```julia +fields = LineCableModels.DataModel.vdeparse("N2XS(FL)2Y 1x630/35 76/132 kV RM") +``` + +The returned fields describe the designation. Unparsed compact-token text is +stored under `:unparsed_stub`. + +## Reference + +```@docs +LineCableModels.DataModel.vdeparse +Base.get +Base.delete! +``` + +```@autodocs +Modules = [ + LineCableModels.DataModel.BaseParams, + LineCableModels.ParametricBuilder.WirePatterns, +] +Order = [:module, :constant, :type, :function, :macro] +Public = true +Private = false +``` diff --git a/docs/src/conventions.md b/docs/src/conventions.md index 268866c87..d6d38acf3 100644 --- a/docs/src/conventions.md +++ b/docs/src/conventions.md @@ -1,244 +1,460 @@ -# Package conventions +# Conventions ---- +## Documentation language -## Function and method names +Use US English in docstrings and project documentation. Preserve API identifiers, +file paths, quoted titles, and proper names. State what a calculation does, +define its physical inputs and units, and distinguish implemented behavior from +approximations and literature results. Omit promotional claims and process +terminology when a concrete description suffices. -### Multi-dispatch resolution pattern - -The codebase employs a consistent three-tier resolution pattern for handling user input processing through multi-dispatch. This standardized approach allows for predictable code organization and improved maintainability. - -#### Resolution pattern naming structure +LineCableModels follows the SciML formatter style. Run: ```julia -_resolve_ # Primary resolution function -├─ _parse_inputs_ # Type conversion & normalization -└─ _do_resolve_ # Core implementation logic +using JuliaFormatter +format(".") ``` -The naming pattern has been carefully selected to reflect the purpose of each dispatch layer: - -1. **`_resolve_`**: The primary entry point that coordinates the resolution process. The term "resolve" indicates that the function must determine an appropriate course of action based on input types that are not known in advance. This function validates inputs and delegates to specialized implementations. - -2. **`_parse_inputs_`**: The intermediate layer responsible for converting diverse input types into standardized forms that can be processed by the implementation layer. This function normalizes inputs through type-specific conversions. - -3. **`_do_resolve_`**: The implementation layer that performs the actual computations or transformations once inputs have been standardized. This function contains the core logic specific to each component type. - -#### Function scope and visibility - -All functions in this pattern are prefixed with an underscore (`_`) to indicate they are internal implementation details not intended for direct use by the package consumers. +before committing Julia source. -### Calculation vs. computation pattern +## Native-first semantic economy -The codebase distinguishes between calculation and computation methods through a clear naming convention: +Use Julia's existing meanings before adding package vocabulary. Check, in +order, whether the operation is already expressed by Core, Base, a standard +library, a direct dependency, or an existing package generic. -#### Calculation methods (`calc_`) +New names require a domain meaning. They can describe an invariant or numerical method, identify an external format or represent real state. Mere forwarding and field access are insufficient reasons to add a name. Tuple repacking and merging defaults are also insufficient. -Functions prefixed with `calc_` handle intermediate steps within a broader computational framework. These methods: +| Prefer | Avoid | +|:--|:--| +| `Base.length`, iteration, indexing, `show`, and `showerror` | package copies of collection and display operations | +| constructors, `promote`, and `convert` | a second conversion vocabulary | +| another method on an owned generic | a synonym that forwards to that generic | +| dispatch on a scientific type | a symbol switch or dictionary that repeats dispatch | +| a local method beside its owner | a global `helpers` or `utils` bucket | -- Perform specific mathematical operations on well-defined inputs. -- Typically represent a single conceptual step in a larger process. -- Return intermediate results that will be used by higher-level functions. -- Are often associated with specific physical or mathematical formulations. +For a result that represents a finite collection, implement Julia's collection +methods as shown below: -#### Computation methods (`comp_`) - -Functions prefixed with `comp_` represent higher-level operations that perform multiple calculation steps to achieve a complete analysis. These methods: +```julia +Base.length(result::MyResult) = length(result.values) +Base.getindex(result::MyResult, index::Integer) = result.values[index] +Base.iterate(result::MyResult, state...) = iterate(result.values, state...) +``` -- Coordinate multiple calculation steps toward a final result. -- Often work with complex objects rather than primitive types. -- Represent the primary technical capabilities of the toolbox. -- May store results in appropriate data structures for further processing. +It does not add `get_results`, `result_count`, and `iterate_results` as parallel +names. -This distinction reflects the hierarchical nature of the [`LineCableModels.jl`](@ref) package, where individual calculations support the broader computational objectives of modeling transmission lines and cables. +Small methods are appropriate when each method defines a dispatch choice or an +invariant. Tiny forwarding helpers that only rename another operation are not. +Do not add speculative compatibility shims, runtime `eval`, exception-driven +feature tests, or lookup tables that duplicate Julia methods. -### Library management pattern +Mutating functions use `!` only when the operation can mutate an argument or +externally visible state. -The codebase implements a consistent pattern for managing libraries of models and components through standardized naming conventions. This pattern facilitates the storage, retrieval, and management of reusable objects within the framework. +## Dispatch-driven fixed actions -#### Library operations naming structure +Use one public action when an operation has one required stage order. The +action method shows the complete sequence, and concrete definition types add +methods for the stages they implement. ```julia -store_! # Add or update an object in a library -remove_! # Remove an object from a library -save_ # Writes the entire library contents to a file -list_ # Display contents of a library +function process(definition::AbstractDefinition, source) + selected = select(definition, source) + product = build(definition, selected) + return Product(product) +end ``` -Each library operation follows a predictable naming convention: +Good definition types are concrete and passive: their fields state scientific +choices or completed configuration. Runtime work belongs to stage methods. -1. **`store_!`**: Adds or updates objects in the specified library. The exclamation mark indicates that this operation modifies the library state. +```julia +struct FrequencyReport <: AbstractReportDefinition + requests::Tuple +end -2. **`remove_!`**: Removes an object from the specified library. The exclamation mark indicates that this operation modifies the library state. +select(definition::FrequencyReport, source) = + observables(source, definition.requests) +``` -3. **`save_`**: Persists the current state of the library to external storage, typically a file. This operation does not modify the library itself. +Do not replace type-directed stages with a generic `mode`, `style`, or options +dictionary interpreted by one large switch. Normalize public symbols once at +the owner's entry point when symbols are part of the public syntax, then use +the existing `Val` method family: -4. **`list_`**: Displays the contents of the library for inspection without modifying its state. +```julia +owned_action(selector::Symbol, args...; kwargs...) = + owned_action(Val(selector), args...; kwargs...) -### Object modification pattern +owned_action(::Val{:example}, args...; kwargs...) = ... +``` -For operations that modify components within larger structures (e.g., [`AbstractConductorPart`](@ref) within a parent [`ConductorGroup`](@ref)), the codebase employs the `addto_` prefix: +Use an explicit no-op method only when doing nothing is a valid stage result. +Reject unsupported definition and source pairs through required stage dispatch +before partial work. Introduce a mutable context only when several stages +share buffers, resources, or changing state. CI checks the fixed +actions listed in [Commons invariants](developers.md) directly. Runtime +metadata that merely repeats their method definitions is not part of the +grammar. + +## Ownership-centered recursive module layout + +Place code first by the owner that defines when it changes, then by its precise +responsibility. Files, directories, and Julia modules solve different +problems: + +- a file separates one responsibility within an owner. +- a directory groups several responsibilities that still belong to one owner. +- a submodule supplies a separate namespace, dependency set, or stated + interface. +- a package is warranted only when the code has independent users and releases. + +Grow code recursively: + +```text +single responsibility in owner file +└─ several responsibilities in owner directory, same module + └─ separate namespace or dependency set in child module + └─ independent package only when independently consumed +``` -```julia -addto_! # Add or modify a subcomponent within a larger component +A module entry file is an index. It contains the module description, explicit +imports, public names, includes in dependency order, and deliberate child +reexports. Constructors, algorithms, validation, plotting descriptions, and +format translations belong in focused files selected by the owner. + +Place a method according to the reason it changes. Keep `observe` methods beside the Engine results they expose. Place methods that draw native Makie figures in the Makie extension. Scientific +observations and physical preview geometry remain with their owner. Plot +request normalization, presentation groups, palettes, Makie blocks, layouts, +widgets, callbacks, and backend activation do not. A method that parses an +external file belongs with the format owner. + +Optional dependencies remain in package extensions. Core source may define +package-neutral requests and completed values, but it does not import Makie, +XLSX, Measurements, or Distributions. + +`ext/` holds package extensions only. The source of a core submodule is under `src/`, +also when the submodule runs an external program. PSCAD is in `src/pscad/`. The project +of its remote runner, which runs apart from the package, is in `src/pscad/remote/`. + +Prefer conceptual groupings such as: + +```text +Owner +├─ types +├─ interfaces +├─ constructors +├─ action +├─ Base protocols +└─ owned optional translations ``` -The `addto_!` pattern: +Avoid global mechanism-first trees such as `types/services/managers/handlers`, +one module per file or per type, empty directory scaffolds, and a common base +file that accumulates unrelated methods. Do not split a fixed call sequence +across files merely to make each stage visually separate. -- Indicates that a subcomponent is being added to or modified within a parent component. -- Always includes an exclamation mark to denote state modification. -- Typically invokes `calc_` methods to update derived properties. +## Scientific reads, tables, and plots -This pattern allows for hierarchical composition of components while maintaining a clear distinction from library management operations. +`observe` reads native scientific values. `ObservedResult` captures detached +products for one completed gridpoint. Ordinary collections lift that constructor. +Reports, tables, plots, exports, and saved report inspection consume observations. -### DataFrame view pattern +```text +completed numerical result + completed comparisons + recorded timings +→ ObservedResult(gridpoint, quantities, errors, timings) +→ retained selection → tables / existing Makie renderers / persistence +``` -The codebase implements a consistent pattern for generating `DataFrame` views of complex objects through a standardized naming convention: +The result owner implements `observation_quantity` and declares its requests. +`Commons.observation_gridpoint` reads the description captured in completed result +storage. Completion captures actual physical inputs, formulation selections and +controls, coordinates, and original point identity. Basic descriptions are always +retained. Observation and presentation never reconstruct a problem from lazy axes. + +The complete primary pairs are R/X, magnitude and angle of Z, or R/L for Z, and G/B, +magnitude and angle of Y, or G/C for Y. Defaults are R/X and G/B. The strict constructor +rejects an incomplete pair. Raw plotting conveniences use the same request +normalizer with `complete_pairs=true`. `plot(line; ydata=(R,))` retains R/X and G/B +and displays R. `plot(observed; ydata=(R,))` selects retained R only. Different +representations require a new explicit construction. No consumer derives an +absent quantity, repeats clipping or acquires a raw source. + +Re-observation selects retained products and preserves their recorded units by +default. Explicit unit changes convert from those recorded units. They never +reapply native-unit scaling or clipping. Comparison and timing associations +survive selection. The atomic constructor validates coordinates, dimensions, +units, masks, and completed-comparison records, including during archive loading. + +The shared `Commons.observation_product(points, request)` operation aligns matrix +coefficients by original indices and converts compatible units for overlays. +Each trace retains its original frequency samples without interpolation. Z and Y +products may retain different frequency selections. + +UQ acquisition belongs to the UQ owner: `ObservedResult(uq, point, requests)` joins +that point's primary values and requested statistics, samples, or histograms. +Product requests omit the point index. Mean/std estimates and precomputed +histogram/CDF/Q-Q coordinates remain ordinary records in `quantities`. Sampling +information belongs to `gridpoint.sampling`. Uncertain primary values in an ordinary +collection use the primary owner and preserve their dependencies. ```julia -_todf # Convert entity to a `DataFrame` representation +observed = ObservedResult(parameters, (R, L, G, C)) +tables = ReportBuilder.tabulate(observed) # tables.Z.R, tables.Z.L, tables.Y.G, tables.Y.C +plot(observed; ydata=(R,)) ``` -This pattern: - -- Takes an entity object as input and creates a `DataFrame` visualization. -- Uses the suffix `_todf` to clearly indicate the conversion operation. -- Produces non-mutating transformations (no exclamation mark needed). -- Facilitates analysis, visualization, and reporting of complex data structures. +Each quantity has its own table. A full n×n matrix on m frequencies has m rows and +1+n² columns, in row-major coefficient order. Both off-diagonals remain present. +Sparse and diagonal requests preserve original indices and matrix extent. +Modal vectors use one `:vector` mode axis with original mode positions and +retained frequency samples. Bare complex modal requests acquire Cartesian +component pairs. Explicit components display only the selected product. +`alpha` and `beta` share the physical identities of the real and imaginary +parts of `gamma`, and `velocity` is a separate real quantity. Modal archives +written before these coordinate and identity changes must be reacquired. +`DataFrame(observed)` rejects aggregate conversion and directs the caller to a +quantity leaf such as `ReportBuilder.tabulate(observed, R)`. `ObservedResult` is +not a Tables.jl table. CableConstants quantity tables have one operating-frequency +row and a named column per assembly. XLSX writes one numeric value and standard-deviation workbook per point and quantity. +Native observation persistence preserves precision and uncertainty dependencies +across the complete result-reference archive. XLSX preflights all destinations +and worksheet sizes before writing. Existing files require `overwrite=true`. + +Explicit benchmark comparison precedes construction: -The `_todf` suffix provides a concise and immediately recognizable identifier for functions that expose object data in tabular format. +```julia +completed = compare(reference, results, [R, L]; bands=(:all,)) +points = observables(results; comparisons=completed, timings=recorded_timings) +reference_point = ObservedResult(reference) +artifact = report(BenchmarkTableDefinition(), points; reference=reference_point) +plot(artifact; ydata=(R,)) +``` ---- +Comparison records join by original result identities. A reference remains +outside the result collection. External data needs explicit retained identity +for a benchmark. Absent physical descriptions remain explicitly absent. Arithmetic +checks cover coordinates, dimensions, units, basis, and frequency agreement. +Scientific comparability is the caller's responsibility. The samples retain their original frequencies without interpolation. +`ReportArtifact.observed` and `.reference` contain only observations. `.tables` +contains the organized tables. Raw conveniences construct and delegate once. + +`observation_groups` is the shared grouping owner. Eligibility requires the same +physical point, relevant selected formulas and controls, quantity and statistical +meaning, units, uncertainty interpretation, and coordinates. Exact numerical and +dependency agreement verifies that eligibility. It does not discover equivalence. +Conflicting numerical values under the same semantics and uncertainty dependencies +raise a consistency diagnostic. Captured physical fields include owner-defined +names and units. Common formulation fields are compared structurally before +labels are formatted. +All group identities, original observations, tables, and files remain available. + +## Numerical reporting + +An available scalar is engineering zero precisely when +`abs(nominal(value)) <= cutoff`. Nonfinite and unavailable values are separate. +Defaults per meter are R=1e-10 Ω/m, L=1e-15 H/m, G=1e-12 S/m, C=1e-16 F/m. +X and B use 2πf times their L and C cutoffs. Total quantities scale by a retained +physical length or require explicit cutoffs. L/C are unavailable at DC. No inferred +`eps`, matrix-norm, largest-coefficient, or uncertainty contribution is added. +Complex zero requires both Cartesian components to be zero. Polar products come +from the original complex values. Clipped values become exact zero, including +their uncertainty. Unclipped values and source computations preserve their +uncertainty dependencies. Undefined first-order magnitude retains its components and zero +nominal magnitude with an explicit reason. Its value and phase remain missing. + +Comparison classifies original operands first. Any ineligible sample makes both +RMS metrics missing for that coefficient and band. Otherwise existing RMS mathematics +applies to original values. Small and zero errors remain valid. Operand cutoffs +are never applied to errors. Actual cutoffs, units, selections, settings, and +missing reasons are retained. + +Recorded timings remain associated with the original result and separate +reference in report tables. Matching elapsed times alone are insufficient to identify a shared event. +A measurement for a whole computation retains that scope when several observed +points include it. Reporting does not invent per-point timings. + +Each `ObservedResult` stores one point's inputs, selected formulations, +quantities, comparisons, and execution measurements. Quantity records include +basis, units, coordinates, applied cutoffs, availability, and missing-value +reasons. `ReportBuilder.tabulate` creates tables from these records on demand. +`ReportArtifact` retains `.observed`, a separate `.reference`, and `.tables`. +FEM completion records identify the selected GetDP executable through +`getdp_selection`. File checksums verify integrity. Source revisions identify +the code used for the computation. + +## Text display and tables + +Human inspection (`show`), scientific extraction (`observe`), detached acquisition +(`ObservedResult`/`observables`), and tabulation (`tabulate`) are separate actions. +Each public owned type defines `summary`, two-argument `show`, and `text/plain` +`show`. Display reads stored state only. It must not run builders, comparisons, +solvers, or lazy-grid materialization. Ordinary report display renders retained +tables and never constructs a figure implicitly. Existing Makie axes and the +plot window own drawing, controls, layout, and backend behavior. + +## Docstrings + +Docstrings use DocStringExtensions abbreviations so declarations remain aligned +with the implementation. + +### Placement and content + +- Place a docstring immediately before the documented module, type, + constructor, function, or constant. +- Use triple double quotes, except for concise field and constant docstrings. +- Describe implemented behavior. Do not infer equations, units, or defaults + from a name. +- Describe the current API directly. Keep development history, removed APIs, + and migration notes out of reference documentation and docstrings. +- State each fact once. +- Link related local bindings inline when the relationship helps the reader. + +Use `@doc` for an inner constructor written inside a `struct`. An outer +constructor at module scope uses an ordinary preceding docstring. + +### Physical quantities and equations + +State the SI unit for every physical argument, return value, field, and +constant. In Julia docstring source, escape square brackets and LaTeX commands: -## Module organization +```julia +"Series resistance `\\[Ω/m\\]`." +"Relative permeability `\\[dimensionless\\]`." +``` -### Exports and visibility +Comments inside Julia examples use ordinary brackets, such as `# [m]`. -- Public API functions have no underscore prefix and are explicitly exported. -- Internal functions use leading underscore prefix (`_function_name`) and are not exported. -- Modules use `@reexport` to propagate exports from submodules to parent modules. -- Exports are placed at the top of each module for visibility. +When code directly evaluates a physical law, approximation, or reduction that +matters to the method's meaning, include the equation and define its symbols: -### Module structure +````julia +""" +$(TYPEDSIGNATURES) -- Main module (`LineCableModels`) reexports from submodules. -- Submodules (`DataModel`, `Materials`, etc.) handle their own exports. -- Maximum nesting depth is 3 levels (parent → child → grandchild). -- Documentation is maintained at all levels using `DocStringExtensions`. +Return the series impedance: -### Code navigation +```math +Z(f) = R + \\mathrm{j} 2 \\pi f L, +``` -- Module docstrings should list key exported functions and types. -- All exported items require docstrings. -- Internal implementation details use minimal documentation. +where ``R`` is resistance, ``L`` is inductance, and ``f`` is frequency. +""" +```` ---- +Accessors, forwarding methods, and bookkeeping functions do not need a +mathematical section unless they evaluate the documented expression. -## Framework design pattern +### DocStringExtensions abbreviations -### Core architecture +- `$(TYPEDSIGNATURES)` is the default opening for functions and constructors. +- `$(SIGNATURES)` is suitable when typed signatures obscure the public call. +- `$(FUNCTIONNAME)` keeps executable examples aligned with renames. +- `$(TYPEDEF)` inserts a type declaration. +- `$(TYPEDFIELDS)` inserts fields, declared types, and field docstrings. +- `$(FIELDS)` omits declared field types when they would distract from the + public meaning. +- `$(METHODLIST)` is reserved for a multi-method interface whose purpose is to + list implementations. +- `$(IMPORTS)` and `$(EXPORTS)` maintain a module inventory. -The `LineCableModels.jl` package implements a consistent framework pattern across all modules, designed to provide flexibility while maintaining type stability and performance. The architecture is built around three key components: +Do not repeat generated text by hand. -1. **Problem definition**: Defines the physics/mathematical approach -2. **Solver**: Controls execution parameters -3. **Workspace**: Centralizes all state during computation +### Function structure -This pattern separates *what* is being calculated (problem definition, formulation) from *how* calculations are executed (engine used, solver) and *where* data is stored (workspace). +Use this section order, omitting sections that do not add information: -### The workspace pattern +1. description and any implemented equation. +2. `# Arguments`. +3. `# Keywords`. +4. `# Returns`. +5. `# Notes` for assumptions or limitations. +6. `# Errors` for deliberate exceptions. +7. `# Examples`. -At the core of the framework is the Workspace pattern, exemplified by `FEMWorkspace` in the `FEMTools` module. This pattern can be replicated across other modules (e.g., `EMTWorkspace`). +````julia +""" +$(TYPEDSIGNATURES) -A workspace: +Describe the implemented operation. -- Acts as a centralized state container. -- Stores all intermediate computation data. -- Manages entity tracking and lookup tables. -- Provides consistent interfaces for data access. -- Maintains configuration and results. +# Arguments -### Type hierarchy +- `value`: Physical input `\\[unit\\]`. -Each module follows a consistent type hierarchy: +# Keywords -```julia -AbstractProblemFormulation - ├── FEMFormulation :> {Darwin, Electrodynamics, ...} - ├── AbstractFDEMFormulation :> {CPEarth, CIGRE, ...} - ├── AbstractEHEMFormulation :> {EnforceLayer, EquivalentSigma, ...} - ├── ... - └── [Other specialized formulations, concrete or abstract] - -AbstractWorkspace - ├── FEMWorkspace - ├── EMTWorkspace - ├── ... - └── [Other specialized workspaces] - -AbstractEntityData - └── [Domain-specific entity types] -``` +- `basis`: `:pul` or `:total`. Default: `:pul`. -This hierarchy enables both specialization and shared interfaces. +# Returns -### Data flow +- Completed value in `\\[unit\\]`. -Data flows through the system in a consistent pattern: +# Errors -1. System definition (LineCableSystem instance). -2. Problem definition (physics parameters, formulations to employ). -3. Solver configuration (execution parameters). -4. Workspace initialization (state container). -5. Execution (multi-phase processing). -6. Result extraction (from workspace). +- Throws `ArgumentError` when `basis` is unsupported. -This pattern applies regardless of the specific module or calculation type. +# Examples -### Multi-phase processing +```jldoctest +result = $(FUNCTIONNAME)(1.0; basis=:pul) # [unit] +@assert isfinite(result) +# output +``` +""" +```` -All modules implement a multi-phase execution pattern with clear separation between phases. For example, the `FEMTools` module follows this pattern: +List arguments in declaration order and document each returned tuple member. +Prefer `jldoctest` for a self-contained public example. Examples requiring an +external executable, graphical interaction, network access, or repository +fixtures belong in the developer guides. -1. **Initialization phase**: Setup workspace, load configurations. -2. **Construction phase**: Create entities based on system definition (may include specific preliminary tasks, e.g. fragments/synchronization steps FEM simulations). -3. **Processing phase**: Execute main computation loops, store raw results in workspace container. -4. **Post processing phase**: Assign properties to processed entities. -5. **Result phase**: Extract and format results. +### Type and module structure -This pattern ensures clean separation of concerns, making the code more maintainable. +Use `$(TYPEDEF)` and `$(TYPEDFIELDS)` for a type. Put a concise docstring above +each field: -### State management +````julia +""" +$(TYPEDEF) -State is managed exclusively through the Workspace, which contains: +Represent a cable section. -1. **Configuration state**: Original system, formulation, and opts. -2. **Entity state**: Collections of typed entities. -3. **Lookup maps**: Efficient mappings between entities and properties. -4. **Processing state**: Temporary calculation state. -5. **Result state**: Final calculation outputs. +$(TYPEDFIELDS) +""" +struct CableSection{T <: Real} + "Section thickness `\\[m\\]`." + thickness::T +end +```` -This centralized approach eliminates global state and ensures thread safety. +A module docstring begins with the indented module name, states its purpose, +then uses `$(IMPORTS)` and `$(EXPORTS)` when those lists aid the reader. For a physical constant, use a concise single-line docstring with its symbol and SI +unit. -### Implementation example: FEMTools.jl +## Repository practice -The FEMTools module exemplifies this pattern: +Stable releases follow [Semantic Versioning](https://semver.org/). -- `FEMFormulation`: Physics parameters for FEM simulation. -- `FEMSolver`: Execution parameters for meshing and solving. -- `FEMWorkspace`: Central state container for all FEM operations. -- Entity types: Typed data containers for different geometric elements. -- Multi-phase workflow: Creation → Fragmentation → Identification → Assignment → Meshing → Solving → Post-processing. +Commit subjects use scoped Conventional Commits, begin with a lowercase +description, and stay within 72 characters: -### Extension to new modules +```text +fix(engine): reject unsupported formulation options +``` -When creating new modules, the following patterns should be followed: +Every change includes tests at the closest relevant scope. Core tests load only required packages. CairoMakie is an optional dependency. Its rendering extension activates when +CairoMakie and Makie are loaded. Rendering and other extension paths run in their +dedicated test environments. Public examples should be executable and self-contained. -1. Define problem & formulation type (physics parameters). -2. Define solver type (execution parameters). -3. Define workspace type (state container). -4. Implement entity types specific to the domain. -5. Implement multi-phase workflow with clear separation. -6. Use the workspace pattern for state management. -7. Follow the standard data flow. +## Testing requirements -This framework ensures consistency, maintainability, and performance across all modules within the package. +The [developer testing requirements](developers.md#testing-requirements) define +release status, regression terminology, test scope, architecture, and the +95% coverage gate. +The harness verifies correctness of the implementation and architectural conformance. +Scientific acceptance is outside it. User-selected numerical snapshots are +deferred until after the first stable publication. Do not duplicate those requirements +as a separate validation or baseline-approval scheme. diff --git a/docs/src/data-model.md b/docs/src/data-model.md new file mode 100644 index 000000000..ba0537851 --- /dev/null +++ b/docs/src/data-model.md @@ -0,0 +1,1018 @@ +# Cable data model + +Cable construction follows one path: + +```text +physical declaration + ↓ + @cable / build + ↓ +completed CableDesign + ↓ + @system / build + ↓ +completed LineCableSystem ──────────┐ + ├→ LineParametersProblem → compute → LineParameters +@earth / build → EarthModel ────────┘ +``` + +The construction macros specify physical order and electrical terminals. + +## Constructing a cable + +The following declaration describes one coaxial cable. Expressions inside +`@cable` run from the center outward. `@terminal` assigns one electrical name +to every conductive descendant in its block. + +```julia +copper = Material(kind=:conductor, rho=1.7241e-8) +xlpe = Material(kind=:insulator, rho=1.0e14, eps_r=2.3) + +design = @cable "example" begin + @terminal :phase begin + core(copper; r=10e-3) + insulation(xlpe; t=8e-3) + end +end +``` + +`design` is a completed `CableDesign`. A second resolution call is unnecessary. +Its physical declaration remains authoritative while geometry and terminal +indices are derived once by `build`. + +Place the cable, complete the system declaration, and construct an ordinary +computation problem: + +```julia +system = @system "example-system" line_length=1.0 begin + @at design (0.0, -1.0) phase=1 +end + +problem = LineParametersProblem( + system; + earth_props=homogeneous(rho=100.0), + frequencies=[50.0], +) +parameters = compute(problem) +``` + +System `phase` values are one-based active phase identifiers. They label retained +electrical matrix coordinates and include no voltage polarity or phase-angle +meaning. Use `0` for a conductor selected as a grounded or eliminated conductor by +the line-parameter reduction. + +System placement composes an outer transform with the design. It does not +rewrite the design's local geometry. + +## Declaring earth + +`homogeneous(...)` declares one homogeneous earth. A layered model is +declared from the earth surface downward with `layer(...)`. The semi-infinite air +layer is implicit: + +```julia +earth = @earth begin + layer(rho=100.0, eps_r=10.0, thickness=5.0) + layer(rho=500.0, eps_r=20.0) +end +``` + +Omitting `thickness` makes that earth layer semi-infinite, so layers with a +finite thickness must precede the bottom half-space. Use +`@earth vertical_layers=true` for vertical interfaces, or supply +`air_layer=layer(...)` when the air properties must be explicit. Layer +properties may contain `Grid` values. The result is then a +`Gridspace{EarthModel}` through the same construction path. + +The completed `EarthModel` is immutable. Its read-only `layers` tuple begins +with the implicit or explicitly supplied air layer, followed by the declared +earth layers in block order. Programmatic code that already has a complete +layer collection uses `build(EarthModel, layers; ...)`. Earth models are never +extended incrementally with `add!`. + +## Terminals and physical tags + +Terminal names and physical tags have different jobs: + +```julia +@terminal :a begin + core(copper; r=4e-3, tag=:core) + insulation(xlpe; t=2e-3, tag=:insulation) +end +``` + +`:a` is the retained electrical terminal. `:core` and `:insulation` identify +physical regions locally. The same local tags may be used under another +terminal because qualified identities are derived from the tree. + +A nonconductive repeated group is valid and contributes no terminal. Request a separate terminal explicitly for a conductive screen: + +```julia +@terminal :screen begin + sheath(copper; t=0.5e-3, tag=:metallic_screen) +end +``` + +## Repeated conductors + +`stranded` accepts circular wire shapes. For a `Disk` geometric boundary, it infers the +largest complete family of concentric courses: one center wire and exactly +`6k` wires in course `k`. With `compact=nothing`, those wires remain circles on +their natural course radii. + +```julia +core_part = stranded( + copper; + shape=Disk(0.5e-3), + lay=LayRatio(13, 12, 11), + boundary=Disk(4.0e-3), +) +``` + +`boundary` is the authoritative finished core. The wire radius and geometric boundary +infer the maximum complete `6k` course inventory. A lay schedule must match the +radial courses found by that inventory. No `Ring`, layer count, or +wire count is stored as a second description of the same geometry. + +Omitting `compact` preserves the natural circular wires. `compact=true` +selects the maximum complete `6k` inventory admitted by area and deforms every +strand while preserving its source area: + +```julia +compact_core = stranded( + copper; + shape=Disk(0.5e-3), + compact=true, + lay=LayRatio(11), + boundary=Disk(sqrt(7) * 0.5e-3), +) +``` + +A `Sector` geometric boundary infers one bundle-center strand and complete `6k` courses, +with total inventory `1 + 3L(L+1)`. The circular sites are mapped into the +resolved sector and retained while a prescribed-area power diagram allocates +space. Each strand is a disk grown inside its allocated cell until its clipped +area matches the source wire area. Free portions remain round. Contact faces +follow cell boundaries. Sector deformation is intrinsic, so it has no separate +compaction choice. All remaining area belongs to the declared fill material. + +Course schedules are ordinary physical tuples. Wrap a complete schedule in +`Grid` only when the schedule itself should vary. + +A stranded construction may prescribe the aggregate geometric boundary separately from +the geometry of each strand: + +```julia +sector_core = stranded( + copper; + shape=Disk(0.5e-3), + boundary=Sector( + span=deg2rad(119), + r_base=1.10e-3, + r_back=10.24e-3, + fillet=1.02e-3, + ), +) +``` + +`shape` specifies each source strand and its conserved area, while sector +reconstruction determines the resolved outline. The geometric boundary constrains +the aggregate shape without adding a homogeneous conductor or a fictitious wire +at the cable origin. +Rectangular strands are admitted only in a circular core with an explicit +center wire. They are always bent area-preservingly into annular courses. + +## Independent cores and enclosures + +`@assembly` preserves the terminals of independent members: + +```julia +phase_a = @terminal :a begin + core(copper, Sector(span=pi / 3, r_base=1e-3, r_back=4e-3)) + insulation(xlpe; t=1e-3) +end + +phase_b = @terminal :b begin + core(copper, Sector(span=pi / 3, r_base=1e-3, r_back=4e-3)) + insulation(xlpe; t=1e-3) +end + +members = @assembly begin + @at phase_a (-5e-3, 0.0) + @at phase_b ( 5e-3, 0.0) φ=pi +end +``` + +`pipe` and `duct` describe containment. Both accept block notation when several +members make the physical nesting clearer: + +```julia +contained = @pipe shape=Disk(15e-3) fill=air begin + @at phase_a (-5e-3, 0.0) + @at phase_b ( 5e-3, 0.0) +end +``` + +Nested ducts use the same operation. There is no separate duct-bank object. + +When repeated sectors share an origin, their intrinsic span must equal the +angular pitch imposed by the repetition. This keeps neighboring sides parallel. +Bare sectors must retain positive physical side clearance. A conformal outer +insulation may close that clearance to zero, but it may not overlap its +neighbor. + +## Stored construction types + +The notation and practical functions lower immediately to the stored grammar: + +```text +Material + primitive + ↓ + Region + ↓ +Stack / Group / Assembly / Enclosure + ↓ + build + ↓ + CableDesign +``` + +`Stack` owns ordered outward composition. `Group` repeats and coalesces +conductive descendants into zero or one terminal. `Assembly` preserves child +terminal identities. `Enclosure` defines a containing domain together with its fill and contents, and an optional wall. These types remain available for extension +code and unusual geometry. Ordinary cable declarations use the vocabulary +shown above. + +Primitives describe intrinsic local geometry. Construction resolves them +against a geometric boundary and records absolute geometry in `PlacedRegion` values +inside `CableGeometry`. `EmptyBoundary` represents the first stacking state. + +Adding a primitive requires local `resolve`, `boundary`, `area`, `centroid`, and +`support` methods rather than a central type switch. + +## Sector cores + +`Sector` describes one material-neutral cable-sector cross-section with an +optional fillet. Its symmetry axis is local `+x`. Placement and terminal +identity remain outside the primitive. + +```julia +sector = Sector( + span=2pi / 3, + r_base=1.10e-3, + r_back=10.24e-3, + fillet=1.02e-3, +) + +phase = @terminal :phase begin + solid(copper, sector; tag=:core) + insulation(xlpe; t=0.5e-3) +end + +sectorized = @cable "three-core-sector" begin + cores(phase; n=3, r=0, names=(:a, :b, :c)) +end +``` + +`resolve` derives exact arc contacts and composes each `Pose2`. A pose applied to a +point, `pose(point)`, rotates the point and then translates it. `area`, +`perimeter`, `centroid`, and `support` use the exact geometric boundary. `tessellate` +returns ordinary coordinate tuples solely for renderers and mesh adapters. +Resolving `Shell(t)` against one sector produces its exact parallel boundary. +A common layer around the disconnected assembly requires an explicit +`Enclosure`. + +The package-owned formulation converts each sector conductor and conformal +dielectric to equivalent-area circles only while constructing its numerical +input. The exact sectors and their centroids remain authoritative in the +completed design and in persisted declarations. + +`design.origin` retains the physical declaration used to build the cable. +`design.geometry` is its resolved geometry. Serialized cable-design records +store the declaration under `"origin"`. The outer JSON document uses `"root"` +for its top-level value. + +## Parameter spaces + +Only an explicit `Grid` introduces variation: + +```julia +designs = @cable "radius-study" begin + @terminal :phase begin + core(copper; r=Grid((8e-3, 10e-3, 12e-3))) + insulation(xlpe; t=8e-3) + end +end + +eltype(designs) === CableDesign +``` + +Every block macro forwards `combine=:product` or `combine=:zip` to the same +construction used by its functional form. `@cable` also accepts +`nominal_data=(...)`. That descriptive data follows each completed design +without affecting its physical resolution. + +Iteration and stochastic realization return completed ordinary designs. Raw +tuples, vectors, polygon points, course schedules, and frequency vectors remain +atomic unless wrapped in `Grid`. + +## Formulation support + +A physically valid design may build even when a formulation does not support +its geometry. `DataModel.flatten(design, frequency)` performs local scalar circuit +reductions. It combines conductor resistances in parallel, calculates GMR +recursively and combines dielectric admittances in series. Inverse conversions +then give effective material properties. The returned component payload is flat, +and the design remains unchanged. + +`homogenize(design)` is an explicit request for a new homogeneous +`CableDesign`. Independent assembly members are flattened separately. The +operation calculates no mutual coupling, earth return, or line-parameter +matrix. The selected engine separately validates whether its formulation +supports the resulting cable topology. + +Homogeneous dielectric geometry retains a `RadialDielectric`: the original +insulation and semicon materials with their logarithmic radius weights. +In computation, the selected law is evaluated for each constituent at each +frequency, and the radial responses combine in series. +`:default` remains lossless before and after homogenization. Explicitly +selected lossy formulations retain their frequency dependence. The displayed +resistivity is the DC series value and the displayed permittivity is the +lossless series value, not a broadband complex-permittivity fit. + +Scalar exports such as PSCAD select a formulation and a reference frequency +explicitly. Their native frequency laws remain approximations to the original +multi-material response away from that reference point. + +`CableConstants(design)` uses this reduction through the +Engine-owned `CableConstantsProblem → CableConstantsFormulation → compute` +workflow. It evaluates one concentric assembly at a time at the requested +temperature and frequency. It contains no earth-return calculation. The +innermost terminal is active and every outward terminal is grounded. The +result stores aligned `R`, `L`, `C`, and `G` vectors with one entry per +assembly. + +## Persistence and tables + +JSON records authoritative materials, physical declarations, patterns, paths, +compaction laws, poses, terminal names, tags, and explicit Grid declarations. +Decoding invokes the same constructors. Resolved geometry, terminal +maps, engine workspaces, and solver results are not serialized. + +Model declarations use their bounded `text/plain` displays. Electrical tables +come from `CableConstants(design)` or observed `LineParameters` results. + +## Supported conductor formations + +The following gallery uses the high-level block macros: `@cable` completes a +design, `@terminal` names its conductive descendants, and `@pipe` / `@duct` +express containment. Shapes and leaf operations such as `stranded` remain +ordinary calls inside those blocks. Each example uses simple insulation so +the conductor formation remains visible. + +### Solid circular and sector cores + +A solid conductor may use either a circular disk or the exact filleted sector +primitive. Insulation follows the resolved geometric boundary in both cases. + +```@example supported_formations +using LineCableModels +using CairoMakie + +gallery_copper = Material(kind=:conductor, rho=1.7241e-8, mu_r=1.0) +gallery_xlpe = Material(kind=:insulator, rho=1.0e14, eps_r=2.3) +gallery_air = Material(kind=:insulator, rho=Inf, eps_r=1.0) +gallery_jacket = Material(kind=:insulator, rho=1.0e13, eps_r=2.8) +gallery_interstitial = Material(kind=:insulator, rho=2.0e12, eps_r=3.5) +gallery_concrete = Material(kind=:insulator, rho=100.0, eps_r=5.0, mu_r=1.0) + +solid_circular = @cable "solid-circular" begin + @terminal :core begin + core(gallery_copper; r=3.5e-3) + insulation(gallery_xlpe; t=1.0e-3) + end +end + +solid_sector = @cable "solid-sector" begin + @terminal :core begin + solid( + gallery_copper, + Sector( + span=deg2rad(118), + r_base=1.5e-3, + r_back=7.0e-3, + fillet=0.6e-3, + ); + tag=:core, + ) + insulation(gallery_xlpe; t=1.0e-3) + end +end + +preview( + [solid_circular, solid_sector]; + backend=:cairo, + display_plot=false, + controls=false, +).figure +``` + +### Circular stranded cores + +Omitting `compact` preserves each strand as a circle on its concentric `6k` +courses. `compact=true` infers a complete `6k` inventory by area and deforms it +while preserving each strand area and identity. + +```@example supported_formations +circular_strand_radius = 0.45e-3 +circular_strand_count = 1 + 6 + 12 + +circular_stranded = @cable "circular-stranded" begin + @terminal :core begin + stranded( + gallery_copper; + shape=Disk(circular_strand_radius), + lay=LayRatio(14, 12), + boundary=Disk(5circular_strand_radius), + fill=gallery_air, + ) + insulation(gallery_xlpe; t=1.0e-3) + end +end + +circular_compacted = @cable "circular-compacted" begin + @terminal :core begin + stranded( + gallery_copper; + shape=Disk(circular_strand_radius), + lay=LayRatio(14, 12), + compact=true, + boundary=Disk(sqrt(circular_strand_count) * circular_strand_radius), + fill=gallery_air, + ) + insulation(gallery_xlpe; t=1.0e-3) + end +end + +preview( + [circular_stranded, circular_compacted]; + backend=:cairo, + display_plot=false, + controls=false, +).figure +``` + +Bounded circular and sector compaction preserves continuous input uncertainty: +cell weights are differentiated through their prescribed-area constraint, and +clipped strands through their area constraint. This retains motion of cell +boundaries, sites, centroids, scale and fillets. It does not model mechanical +deformation. Nominal polygons, strand areas, filler regions and terminal groups +remain the same construction used by deterministic rebuilding. + +Course counts and clipping choices are discrete nominal decisions. Linear +uncertainty propagation is valid locally within a stable topology, not across a +course-count transition. A joint uncertainty declaration must preserve feasible +geometry throughout its support. See [joint geometric inputs](gridspace.md#Feasible-geometric-dependence). + +### Rectangular stranded core + +Rectangular source strands require a circular center wire. They are always +bent into contiguous annular courses. The radial deformation preserves the +area of every source rectangle. A one-strand course remains a complete annulus. +The specified `boundary` determines the strand inventory, but the resolved +physical boundary is the actual occupied metal disk. No outer `stranded_fill` +film is generated. Insulation starts at that occupied geometric boundary. Rectangular +`stranded` declarations return a bounded `Group`, while circular and +sector stranding retain their material-complete `Enclosure` declarations. + +When a filled circular-wire course follows the core, its declared filler fills the space from the occupied core boundary to the course enclosure, including +the interstices between wires. Explicit wire-center radii remain fixed. +Contextual `Ring(...; r=nothing)` radii follow the occupied core boundary. +Explicit enclosing layers and coatings are retained, including thin layers. + +```@example supported_formations +rectangular_stranded = @cable "rectangular-stranded" begin + @terminal :core begin + stranded( + gallery_copper; + center=Disk(0.55e-3), + shape=Rectangle(0.75e-3, 0.35e-3), + lay=LayRatio(13), + boundary=Disk(3.5e-3), + fill=gallery_air, + ) + insulation(gallery_xlpe; t=1.0e-3) + end +end + +preview( + rectangular_stranded; + backend=:cairo, + display_plot=false, + controls=false, +).figure +``` + +### Sectorized stranded cores + +A sectorized phase core contains its own bundle-center strand and complete +`6k` courses. The source wire area and sector area determine the inventory. +Mapped circular sites retain these identities. Prescribed-area power cells +allocate space, and a clipped-disk area solve determines each copper shape. +The polygons approximate circular arcs while preserving the source areas. + +The supplied 0.55 mm wire radius below admits 37 wires and 74.23% copper fill. +The second design explicitly changes the wire radius to obtain 93% fill with +the same inventory and geometric boundary. Compaction cannot remove the remaining void +while preserving both the source wire area and its count. + +```@example supported_formations +sector_boundary = Sector( + span=deg2rad(118), + r_base=1.5e-3, + r_back=8.0e-3, + fillet=0.6e-3, +) +sector_stranded = @cable "sector-stranded" begin + @terminal :core begin + stranded( + gallery_copper; + shape=Disk(0.55e-3), + boundary=sector_boundary, + fill=gallery_air, + ) + insulation(gallery_xlpe; t=1.0e-3) + end +end + +dense_sector_wire_radius = sqrt( + 0.93 * area(LineCableModels.DataModel.resolve( + LineCableModels.DataModel.EmptyBoundary(), sector_boundary + )) / (37pi) +) +sector_dense = @cable "sector-stranded-93-percent" begin + @terminal :core begin + stranded( + gallery_copper; + shape=Disk(dense_sector_wire_radius), + boundary=sector_boundary, + fill=gallery_air, + ) + insulation(gallery_xlpe; t=1.0e-3) + end +end + +preview( + [sector_stranded, sector_dense]; + panel_titles=("supplied wire, 74.23% fill", "resized wire, 93% fill"), + backend=:cairo, + display_plot=false, + controls=false, +).figure +``` + +### Three- and four-core sector cables + +A complete sector cable preserves the electrical identity of every core. Each +solid or stranded sector has its own insulation before `cores` rotates +the members into a three- or four-phase assembly. The outer `@pipe` fills the +interstices and adds one common tubular insulation layer. A sector's angular +span equals the assembly pitch, which makes each pair of facing wedge sides +parallel. The rows below show solid and intrinsically compacted stranded cores. +At the supplied 0.35 mm strand radius, both stranded cores contain 37 wires. +The three-core sector has 66.62% copper fill and the four-core sector 88.53%. + +```@example supported_formations +three_core_sector = Sector( + span=2pi / 3, + r_base=0.60e-3, + r_back=5.0e-3, + fillet=0.20e-3, +) +four_core_sector = Sector( + span=pi / 2, + r_base=0.60e-3, + r_back=5.0e-3, + fillet=0.20e-3, +) + +sector_wall = insulation(gallery_xlpe; t=0.8e-3, tag=:tubular_insulation) + +solid_three_member = @terminal :segment begin + solid(gallery_copper, three_core_sector; tag=:solid_sector) + insulation(gallery_xlpe; t=0.25e-3) +end +solid_three_core_sector = @cable "solid-three-core-sector" begin + @pipe shape=Disk(5.5e-3) fill=gallery_air wall=sector_wall begin + cores(solid_three_member; n=3, r=0, names=(:phase_1, :phase_2, :phase_3)) + end +end + +solid_four_member = @terminal :segment begin + solid(gallery_copper, four_core_sector; tag=:solid_sector) + insulation(gallery_xlpe; t=0.25e-3) +end +solid_four_core_sector = @cable "solid-four-core-sector" begin + @pipe shape=Disk(5.5e-3) fill=gallery_air wall=sector_wall begin + cores(solid_four_member; n=4, r=0, names=(:phase_1, :phase_2, :phase_3, :phase_4)) + end +end + +stranded_three_member = @terminal :segment begin + stranded( + gallery_copper; + shape=Disk(0.35e-3), + lay=LayRatio(14), + boundary=three_core_sector, + fill=gallery_air, + ) + insulation(gallery_xlpe; t=0.25e-3) +end +stranded_three_core_sector = @cable "stranded-three-core-sector" begin + @pipe shape=Disk(5.5e-3) fill=gallery_air wall=sector_wall begin + cores(stranded_three_member; n=3, r=0, names=(:phase_1, :phase_2, :phase_3)) + end +end + +stranded_four_member = @terminal :segment begin + stranded( + gallery_copper; + shape=Disk(0.35e-3), + lay=LayRatio(14), + boundary=four_core_sector, + fill=gallery_air, + ) + insulation(gallery_xlpe; t=0.25e-3) +end +stranded_four_core_sector = @cable "stranded-four-core-sector" begin + @pipe shape=Disk(5.5e-3) fill=gallery_air wall=sector_wall begin + cores(stranded_four_member; n=4, r=0, names=(:phase_1, :phase_2, :phase_3, :phase_4)) + end +end + +preview( + [ + solid_three_core_sector, + solid_four_core_sector, + stranded_three_core_sector, + stranded_four_core_sector, + ]; + layout=(2, 2), + panel_titles=( + "solid - three cores", + "solid - four cores", + "stranded - three cores", + "stranded - four cores", + ), + backend=:cairo, + display_plot=false, + controls=false, +).figure +``` + +### Milliken core + +A Milliken core contains one cable-center wire and six separately bounded stranded +wire segments, each with its own bundle-center strand and `6k` courses. +`milliken` binds every conductor to one terminal. The segment +geometric boundaries govern packing without becoming overlapping material domains. One +explicit fill occupies the interstices around the center and segment wires. Its +center-wire radius is inferred after resolving one segment so that the center +is tangent to the innermost strand in every rotated segment. + +This example deliberately uses a densely filled segment: a source-wire radius +of approximately **0.238628 mm** gives **61 wires per segment**, with the +inventory `1 + 6 + 12 + 18 + 24`, and **93% segment copper fill**. There are +366 segment wires plus the shared central conductor. The latter has an inferred +radius of approximately 0.560 mm. Including the inter-segment separators, the +complete core is approximately **84.6% copper by area**. These dimensions describe an illustrative +design. They were not measured from a cable photograph. + +The radius is chosen from ``a=\sqrt{\eta A_{\mathrm{segment}}/(61\pi)}``, +with ``\eta=0.93``. Only the radius and geometric boundary are passed to `milliken`: +the constructor still infers the complete-course inventory. Copper fill within +a segment is distinct from copper fill across the complete core, which also +includes the central conductor and the material between segments. + +```@example supported_formations +milliken_segment_boundary = Sector( + span=pi / 3, + r_base=0.45e-3, + r_back=5.0e-3, + fillet=0.18e-3, +) +milliken_design = @cable "six-segment-milliken" begin + @terminal :phase begin + milliken( + gallery_copper; + shape=Disk(0.23862789195414474e-3), + segment=milliken_segment_boundary, + segments=6, + fill=gallery_interstitial, + ) + insulation(gallery_xlpe; t=1.0e-3) + end +end + +preview( + milliken_design; + title="Milliken - six 61-wire segments and a central conductor", + backend=:cairo, + display_plot=false, + controls=false, +).figure +``` + +### Four-pair umbilical + +An umbilical can contain complete subcables rather than copies of one +terminal. Here four screened two-core subcables surround a central polyethylene +(PE) filler, with four more PE fillers between them. Each subcable contains two +copper cores with high-density polyethylene (HDPE) insulation, two PE fillers, +an explicit nonwoven (TNT) filling region, a copper sheath and a PE jacket. +The complete assembly has thermoplastic elastomer (TPE) filling, an outer TNT +wrap and a finite galvanized-steel armor layer. + +The dimensions and material constants below are **illustrative**, chosen to +reproduce the supplied cross-section's composition without claiming a +manufacturer specification. TNT is represented as a homogeneous insulating +material. The galvanized steel is one effective material, without a separate +zinc coating. This example demonstrates construction and preview, not support +for this topology in every numerical backend. + +The ordinary Julia function describes one reusable subcable with the public +macros. Its name prefix gives each copy two independent core terminals and one +sheath terminal. Enclosing them in `@pipe` does **not** short them together. +All dimensions in the code are in meters. + +```@example supported_formations +umbilical_pe = Material(kind=:insulator, rho=1.0e15, eps_r=2.3) +umbilical_hdpe = Material(kind=:insulator, rho=1.0e15, eps_r=2.4) +umbilical_tnt = Material(kind=:insulator, rho=1.0e13, eps_r=3.0) +umbilical_tpe = Material(kind=:insulator, rho=1.0e12, eps_r=3.5) +umbilical_steel = Material(kind=:conductor, rho=1.5e-7, mu_r=100.0) + +function umbilical_pair(prefix) + first_core = @terminal Symbol(prefix, :_a) begin + core(gallery_copper; r=1.5e-3, tag=:copper_core) + insulation(umbilical_hdpe; t=0.65e-3, tag=:hdpe_insulation) + end + second_core = @terminal Symbol(prefix, :_b) begin + core(gallery_copper; r=1.5e-3, tag=:copper_core) + insulation(umbilical_hdpe; t=0.65e-3, tag=:hdpe_insulation) + end + pair_wall = @terminal Symbol(prefix, :_sheath) begin + sheath(gallery_copper; t=0.10e-3, tag=:copper_sheath) + jacket(umbilical_pe; t=0.35e-3, tag=:pe_jacket) + end + pair_filler = filler(umbilical_pe, Disk(1.45e-3); tag=:pe_filler) + + return @pipe shape=Disk(4.5e-3) fill=umbilical_tnt wall=pair_wall begin + @at first_core (-2.25e-3, 0.0) + @at second_core (2.25e-3, 0.0) + @at pair_filler (0.0, -2.85e-3) + @at pair_filler (0.0, 2.85e-3) + end +end + +umbilical_pair_design = @cable "umbilical-subcable" begin + umbilical_pair(:pair) +end + +umbilical_legend_labels = Dict( + :pipe_fill => "TNT fill", :tpe_fill => "TPE fill", + :pe_filler => "PE filler", :pe_jacket => "PE jacket", + :hdpe_insulation => "HDPE insulation", :tnt_wrap => "TNT wrap", +) + +preview( + umbilical_pair_design; + title="Umbilical subcable - two cores and a separate copper sheath", + legend_labels=umbilical_legend_labels, + backend=:cairo, + display_plot=false, + controls=false, +).figure +``` + +The top and bottom subcables are rotated by 90° with `@at`. Their complete +geometry rotates together. The 4 independent subcables retain **8 core +terminals and four sheath terminals**, plus the common armor terminal. +Each remaining region is assigned an insulating material, including the +interstices. The construction excludes implicit air and unassigned gaps. + +```@example supported_formations +umbilical_filler = filler(umbilical_pe, Disk(2.0e-3); tag=:pe_filler) +umbilical_diagonal = 9.85e-3 / sqrt(2) +umbilical_wall = @terminal :armour begin + bedding(umbilical_tnt; t=1.5e-3, tag=:tnt_wrap) + sheath(umbilical_steel; t=2.0e-3, tag=:galvanized_steel) +end + +umbilical_design = @cable "four-pair-umbilical" begin + @pipe shape=Disk(12.5e-3) fill=umbilical_tpe wall=umbilical_wall begin + @at umbilical_pair(:east) (7.2e-3, 0.0) + @at umbilical_pair(:north) (0.0, 7.2e-3, pi / 2) + @at umbilical_pair(:west) (-7.2e-3, 0.0) + @at umbilical_pair(:south) (0.0, -7.2e-3, pi / 2) + umbilical_filler + @at umbilical_filler (umbilical_diagonal, umbilical_diagonal) + @at umbilical_filler (-umbilical_diagonal, umbilical_diagonal) + @at umbilical_filler (-umbilical_diagonal, -umbilical_diagonal) + @at umbilical_filler (umbilical_diagonal, -umbilical_diagonal) + end +end + +preview( + umbilical_design; + title="Umbilical - four screened pairs with steel armour", + legend_group=region -> region.source.material == umbilical_tpe ? + :tpe_fill : region.source.tag, + legend_labels=umbilical_legend_labels, + backend=:cairo, + display_plot=false, + controls=false, +).figure +``` + +### Rope formation + +`rope` repeats a complete stranded core as one central member surrounded by six +peers. Here each member's six course wires have `LayRatio(12)` about their own +member axis. The 6 outer members also have `LayRatio(18)` about the rope +axis. These are separate paths: the accepted approximation **multiplies their +overlength factors**. Both lays contribute to the overlength. +The enclosing terminal and insulation belong to the finished rope. + +```@example supported_formations +rope_member_radius = 0.28e-3 +strand_lay = LayRatio(12) +rope_lay = LayRatio(18) +rope_member = stranded( + gallery_copper; + shape=Disk(rope_member_radius), + lay=strand_lay, + compact=true, + boundary=Disk(sqrt(7) * rope_member_radius), + fill=gallery_air, +) + +rope_design = @cable "stranded-rope" begin + @terminal :core begin + rope( + rope_member; + layers=1, + n=6, + lay=rope_lay, + ) + insulation(gallery_xlpe; t=1.0e-3) + end +end + +preview( + rope_design; + backend=:cairo, + display_plot=false, + controls=false, +).figure +``` + +For local helix radius ``r`` and pitch ``p``, the length factor is +``\lambda=\sqrt{1+(2\pi r/p)^2}``. With `LayRatio(q)`, ``p=2rq``, so +``\lambda(q)=\sqrt{1+(\pi/q)^2}``, independent of the local radius. +For a wire following both paths, + +```math +\lambda_{\mathrm{wire}}=\lambda_{\mathrm{strand}}\lambda_{\mathrm{rope}}. +``` + +The central wire of the central member has neither path and factor 1. Its six +neighbors have only the strand factor. The central wire of each outer member +has only the rope factor. The remaining 36 wires have both. The table below +evaluates these factors using [`overlength`](@ref). Its radius argument is +immaterial for these `LayRatio` laws. A `Pitch` law would require the actual +local radius of each path. + +```@example supported_formations +strand_factor = overlength(Helix(strand_lay), rope_member_radius) +rope_factor = overlength(Helix(rope_lay), rope_member_radius) + +using DataFrames +DataFrame( + wires=["central wire", "central member: course", "outer members: centers", + "outer members: courses"], + count=[1, 6, 6, 36], + strand=[1.0, strand_factor, 1.0, strand_factor], + rope=[1.0, 1.0, rope_factor, rope_factor], + total=[1.0, strand_factor, rope_factor, strand_factor * rope_factor], +) +``` + +These factors multiply each wire's resistance before the parallel reduction. +One blanket factor is not applied to all 49 wires. The transverse wire areas +and preview are unchanged by the lay choices. + +### 3 coaxial cores in a pipe + +3 independently named solid-core coaxials form a trefoil inside a circular +pipe. The pipe includes one outward insulating wall. + +```@example supported_formations +coaxial_member = @terminal :phase begin + core(gallery_copper; r=1.6e-3) + insulation(gallery_xlpe; t=1.2e-3) +end +coaxial_trefoil = cores( + coaxial_member; + n=3, + r=3.5e-3, + names=(:a, :b, :c), + φ0=-pi / 2, +) +coaxial_wall = jacket(gallery_jacket; t=1.0e-3, tag=:pipe_insulation) +coaxial_pipe_design = @cable "coaxial-trefoil-pipe" begin + @pipe shape=Disk(7.0e-3) fill=gallery_air wall=coaxial_wall begin + coaxial_trefoil + end +end + +preview( + coaxial_pipe_design; + backend=:cairo, + display_plot=false, + controls=false, +).figure +``` + +### Mixed pipe formations in an elliptical duct + +The composition grammar is not restricted to geometries currently admitted by +every numerical backend. Here a pipe containing three stranded coaxials sits +beside a pipe containing three insulated sector-stranded cores, all inside one +elliptical duct. The duct interior remains air. Its concrete wall is a finite +normal offset of the ellipse rather than a zero-thickness outline. + +```@example supported_formations +duct_round_member = @terminal :round begin + stranded( + gallery_copper; + shape=Disk(0.30e-3), + compact=true, + boundary=Disk(sqrt(7) * 0.30e-3), + fill=gallery_air, + ) + insulation(gallery_xlpe; t=0.8e-3) +end +duct_round_trefoil = cores( + duct_round_member; + n=3, + r=2.8e-3, + names=(:r1, :r2, :r3), + φ0=-pi / 2, +) +duct_round_wall = jacket(gallery_jacket; t=0.8e-3, tag=:round_pipe_insulation) +duct_round_pipe = @pipe shape=Disk(6.0e-3) fill=gallery_air wall=duct_round_wall begin + duct_round_trefoil +end + +duct_sector_boundary = Sector( + span=2pi / 3, + r_base=0.8e-3, + r_back=3.8e-3, + fillet=0.3e-3, +) +duct_sector_member = @terminal :sector begin + stranded( + gallery_copper; + shape=Disk(0.30e-3), + boundary=duct_sector_boundary, + fill=gallery_air, + ) + insulation(gallery_xlpe; t=0.6e-3) +end +duct_sector_trefoil = cores( + duct_sector_member; + n=3, + r=0.0, + names=(:s1, :s2, :s3), +) +duct_sector_wall = jacket(gallery_jacket; t=0.8e-3, tag=:sector_pipe_insulation) +duct_sector_pipe = @pipe shape=Disk(5.5e-3) fill=gallery_air wall=duct_sector_wall begin + duct_sector_trefoil +end + +duct_wall = jacket(gallery_concrete; t=1.5e-3, tag=:concrete_duct_wall) +mixed_duct_design = @cable "mixed-elliptical-duct" begin + @duct shape=Ellipse(24.0e-3, 10.0e-3) fill=gallery_air wall=duct_wall begin + @at duct_round_pipe (-8.0e-3, 0.0) + @at duct_sector_pipe (8.0e-3, 0.0) + end +end + +preview( + mixed_duct_design; + backend=:cairo, + display_plot=false, + controls=false, +).figure +``` diff --git a/docs/src/developers.md b/docs/src/developers.md new file mode 100644 index 000000000..65ae1bc16 --- /dev/null +++ b/docs/src/developers.md @@ -0,0 +1,476 @@ +# Commons invariants + +LineCableModels has three global computation roles: a complete problem and a formulation that selects how to calculate it, followed by a completed result. Package +modules may add concrete types below these roots, but they do not add parallel +computation entry points. + +The type trees below are generated from the loaded package during the +documentation build. + +```@example grammar_type_trees +using LineCableModels + +println("Problem definitions") +print(Main.DocumentationTrees.type_tree(AbstractProblemDefinition)) +println("\nFormulations") +print(Main.DocumentationTrees.type_tree(AbstractFormulation)) +println("\nResults") +print(Main.DocumentationTrees.type_tree(AbstractProblemResult)) +``` + +`Combinatorial`, `LinearError`, and `MonteCarlo` occupy the same formulation +tree. UQ uses the same computation supertypes. Its completed +collections are subtypes of `AbstractUncertaintyResult`, while deterministic finite +collections are subtypes of `AbstractParametricResult`. + +`LineParamsDomain` is independent of this computation grammar. It tags the +physical coordinate system of a completed line-parameter matrix: + +```@example grammar_type_trees +print(Main.DocumentationTrees.type_tree(LineCableModels.LineParamsDomain)) +``` + +## Fixed actions + +One declarative action defines a fixed sequence selected by an abstract definition +type: + +| Action | Definition root | Fixed sequence | +|:--|:--|:--| +| `ReportBuilder.report` | `AbstractReportDefinition` | `select → tabulate → illustrate → encode → write → ReportArtifact` | + +```@example grammar_type_trees +using LineCableModels.ReportBuilder: AbstractReportDefinition + +println("Report definitions") +print(Main.DocumentationTrees.type_tree(AbstractReportDefinition)) +``` + +Each action has one method at its declared abstract root. Concrete definitions +implement stage methods. They do not specialize the public action itself. +Required stages are declared with RequiredInterfaces where the type family +admits that form. Optional stages have an explicit no-op at the abstract root. +Plotting is deliberately not such an action: the optional Makie extension +constructs native figures directly from published observations. + +Observation uses a different structure. Result owners add `observe` methods. +Commons owns one `observables(source, requests::Tuple; ...)` publication +method. Standalone arrays and external result types do not need a shared +abstract observation type. + +## Architecture checks + +The architecture that the quality, core and integration tests protect requires: + +- the action and its abstract root to belong to the same module. +- one public action method, with no more-specific definition methods. +- every fixed stage to remain visible in the declared action. +- every package-owned concrete definition to implement its required stages. +- every package-owned definition to remain below its declared root. +- one Commons-owned observation publication method with positional requests. +- wide scientific tables whose quantity and unit metadata remain attached to + their columns. +- no calls to another package module's private functions or types, including + through aliases, and no private wrappers whose methods only forward unchanged + arguments to the same package-owned function. Owner-local numerical kernels + and type-dispatch branches remain valid. + +A new definition must satisfy these standards. Native interface and import checks, +tests through actual composed consumers and the structural guards below provide the +protection. Unchanged forwarding wrappers remain an advisory ReLint finding. + +The quality selection also runs advisory source diagnostics. Fatou applies the +explicit rules in `fatou.toml`. ReLint identifies runtime-evaluation spellings, +empty or constant-result catches, and unchanged private forwarding methods. +These scans read all Julia files under `src/` and `ext/` without executing them. +They report candidates for review, including typed forwarding methods whose +dispatch purpose has not been determined. Macro expansion, arbitrary binding +ownership, and global architectural redundancy are outside their analysis. +Source findings do not fail the job. Tool setup errors, incomplete scans, and +failed integration controls do. The scans leave source files unchanged and +do not contribute executed-code coverage. See the +[test instructions](https://github.com/Electa-Git/LineCableModels.jl/blob/main/test/README.md#advisory-source-diagnostics) +for pinned tool installation, commands, and the retained CI report. + +Integration tests also count actual blueprint lowering calls: one per selected +design point, shared across its formulation alternatives and frequency sweep. +They check material evaluation before local calculations, paired exterior +calculation before assembly and reduction at each frequency and explicit +phase-to-modal result transport without another geometry lowering. The visual +suite applies the ownership checks to the loaded Makie extensions and verifies +that material colors consume `Material` or `EarthLayer` objects directly. + +`validate(subject, context...)` checks an input before its consumer uses it. It throws on +invalid input and returns its subject. It may warn on input that is computable but +questionable. + +The `validate` tests check owner dispatch, required interfaces, unchanged valid +inputs and rejection of damaged inputs through validation and computation. The +standards require checks to remain with the defining validator. Inspection of every method body requires a separate review beyond representative behavioral tests. + +The native FEM environment uses pinned GetDP 3.5.0. Keep tests of its actual +execution, extraction, terminal identity, material and option transport and resume +behavior. Physical cross-backend accuracy and domain-convergence acceptance +belong to explicit research work under the testing requirements below. See the +[test commands](https://github.com/Electa-Git/LineCableModels.jl/blob/main/test/README.md) for the implementation checks and their +execution environments. + +The workspace provides the buffers, and formulas use them. +`Commons.initialize_buffers` is the one action that builds buffers, for every formula and +every part of the workflow. A formula or shared component builds its own buffers in its +`initialize_buffers` method, usually as a named tuple of preallocated arrays. The method +returns them in the workspace's `buffers` record. A named `*Buffers` type exists only for a +component that several owners share, in `Commons`, such as `ReductionBuffers`. The +storage vocabulary guard below checks the names. + +### Structural guards + +`test/quality/architecture.jl` enforces the ownership rule stated in `AGENTS.md`, +which assigns one owner, one name and one implementation to a concept and one +meaning to a name. The guards load the +package with the extensions that `explicit_imports.jl` loads. They enumerate every +package method and parse every Julia file under `src/` and `ext/`. + +- Ownership (`ownership`). A core method that extends a function owned by another package + module mentions a type owned by its own module or a descendant. For a constructor, + the function owner is the owner of the constructed type. For methods of the root + module, functions and types of its submodules count as owned by another module. + Extension methods are exempt. +- Placement (`placement`). No core method is defined in a file under `ext/`. The home of a + module is the directory that holds its `.jl` file. The nearest home + around a method's file belongs to the method's module or one of its ancestors. +- Direction (`direction`). The submodules have the order Units, Commons, TextDisplay, + PlotBuilder, Materials, Earth, DataModel, Engine, ParametricBuilder, UQ, ReportBuilder, + ImportExport, PSCAD. Engine's child modules, the formula families and ModalAnalysis, take + Engine's position. `MODULE_OWNERS` in + `test/support/taxonomy.jl` defines this order once, for the guards and for the test + runner. The lowered code of a submodule method references earlier submodules, the + ancestors and descendants of its own module, and no later submodule. Each top-level submodule has a position in + the order. The root module and the extensions are exempt. +- Names (`names`). Functions and types owned by different package modules have + distinct names. The per-family `Formula` types are exempt. +- Shadowing (`shadowing`). Each name that the root module exports is in scope after + `using LineCableModels`. It refers to the same object as the name that Base, + LinearAlgebra, Statistics, Random, Dates or Logging exports. A name that only a + submodule exports stays in that namespace, and a name that DataFrames also exports + does not conflict. +- validate returns its subject (`validate`). Each `validate` definition names its first + positional argument. Each path through its body returns that argument or calls + `throw`, `rethrow` or `error`. A body that does more than return the argument also uses + it in that work, so an identity method passes and a check of another argument fails. The + guard reads source code. A path that it cannot classify counts as a violation. Its + baseline table is empty. +- Reserved verbs (`reserved_verbs`). No function name starts with `validate_`, `check_`, + `require_`, `assert_`, `verify_` or `ensure_`, after any leading `_` characters. + `validate` is the input-check verb. +- Symbol switches and probes (`switches`). Each source file has counts of `applicable` + calls, `@eval` calls, and comparisons of a `kind` field against symbols, negated + comparisons included. These counts can decrease and never increase. +- Storage vocabulary (`storage`). A `*Workspace` struct has exactly the fields `input`, + `plan` and `buffers`, and optionally `trace`. No function name ends in `_workspace` or + `_storage`, and only `initialize_buffers` ends in `_buffers`. Only `Commons` defines a + `*Buffers` struct, and no type name ends in `Buffer`, `Storage`, `Scratch` or `Cache`. + No field, named tuple key, parameter, local or loop variable takes the name `work`, + `scratch`, `storage` or `cache`. The guard reads source code. The modal and + cable-constant workspaces keep other fields until their restructuring, and the + baseline lists them. +- Dispatch convention (`dispatch`). A formula method takes the formula object first, and + its `Val` tags follow it. The guard examines each function with a method whose first + argument is a formula, a subtype of `AbstractFormulation`, or a `Union` that contains + one. Another method of that function whose first argument is a `Val`, or a `Union` that + contains one, is a violation. In any function, a first argument `Val{ID}`, or a `Union` + that contains one, where `ID` is an identifier that a formula family registers through + `formulas`, is a violation: it passes the formula as its tag. Type constructors are + exempt, such as `Formula(::Val{ID})`. The guard reads the method table. Its baseline + table is empty. + +`Commons` contains only definitions that several owners use. An owner is the root +module, a top-level submodule or a package extension. The Commons guards stop +helpers from accumulating elsewhere. + +- Commons admission (`commons`). Each function, type and constant defined in `Commons` is + exported or declared `public`, has a docstring and is named in a test file under + `test/unit/commons/`. A hook is a `Commons` function with a method defined outside + `Commons`. The user API is the `Commons` names that the root module exports or + declares `public`. Unless a name is user API, methods of at least two owners + outside `Commons` use it. Unless it is a hook or user API, it has an entry in the + reserved vocabulary. A use is a reference in lowered code, a method + signature, a method extension, a supertype or a field type. Uses by a public + `Commons` definition count for the definitions it uses. A definition with one + remaining owner moves to that owner. Algorithm-local helpers are nested functions + inside the public definition. +- Reserved vocabulary (`vocabulary`). `VOCABULARY` maps each `Commons` public name to the + definition names it reserves. Outside `src/commons/`, no function, constant or + assigned local takes a reserved name. A local assigned from a call to the + reserving definition is exempt, as in `μ0 = vacuum_permeability(T)`. +- Literal fingerprints (`fingerprints`). Outside `src/commons/consts.jl`, no numeric literal + contains the digits `8854187` or `299792458`, and no statement that names `π` + contains `1e-7` or a power of a base containing 10 with exponent `-7`. +- Clones of Commons bodies (`clones`). Function bodies are tokenized with each identifier replaced by its + role (call, field or name), and compared through runs of 8 tokens. Statements of + the form `x || throw(...)` or `x && throw(...)`, with `throw`, `rethrow` or + `error`, are left out. No function body under `src/` or `ext/` contains 75 % of the + runs of a `Commons` public function body of at least 30 tokens. Hooks are not + compared. +- Tiny private helpers (`helpers`). A tiny helper is a private module-level function with one + method and at most three body statements. Exactly one method references it, and + no test file names it. Files under `test/quality/` and `test/tools/` are not test + files here. An exact forwarder counts as a tiny helper for any number of + references. Its body is one call with the forwarder's own arguments, + unchanged and in order, keywords included. Inline a new tiny helper, import the + owner's definition, or test it directly. +- Root freeze (`root`). The number of functions, types and constants defined by the root + module does not grow. New shared definitions go to `Commons` under Commons admission. + +Each guard is a function of its inputs. The negative controls of the architecture +guards and the negative controls of the Commons guards apply each guard to a probe +package with planted violations and to a clean probe package. A guard reports exactly +the planted violations and nothing in the clean probe. + +`test/quality/architecture_baseline.toml` records the violations present when the guards +were introduced, with one table per guard. A table whose violations are all removed +stays in the file, empty. The ratchet keeps comparing it, and its guard fails on any +violation. Every table name must appear in `TABLES`, or the architecture test fails. +Keys contain no line numbers. An unlisted violation or a count above its entry fails. A +listed entry without a live violation, or a count below it, also fails. A change that +removes a violation deletes or lowers its entry. `test/tools/architecture_inventory.jl` +prints the live inventory in the baseline format. Fix a new violation in the source. +Never add it to the baseline. + +Entries can be deleted or lowered, and none can be added or raised. Before the tests, +the quality CI job runs `test/tools/baseline_ratchet.jl` against the pull request +base or the previous push. An added entry or a raised count fails the job. The same +ratchet checks `test/quality/preservation.toml`, described below, in the direction that +each of its tables declares. Its allocation table admits new keys, as the preservation +locks describe. + +The ratchet first applies to the earlier keys the file renames that git detects, and the +module renames of renamed or moved entry files `.jl` that declare their module on +both sides. A module's full name on each side comes from the entry files of the enclosing +directories, so an entry file that moves into another module's directory takes that +module's name as a prefix. An +ownership key has the form `defining module | Owner.function | file`. A change of `Owner` +alone, the module that defines the extended function, keeps the entry and its count. +Any other key renamed without a matching git rename counts as added. A table absent from the +earlier baseline belongs to a guard introduced since, and the ratchet lists it +without comparing its keys. Run +`julia test/tools/baseline_ratchet.jl HEAD` to compare local changes with the last +commit. + +### Preservation locks + +A refactor keeps type stability, allocations, speed and results. Quality checks and +local tools protect them. `test/quality/preservation.toml` records the counts +of the quality checks. Each table declares its direction in `[directions]`. The +`@inferred` table is a floor, which may only rise. The JET and allocation tables are +ceilings, which may only fall. A quality check fails when a count passes its limit. It +also fails when a count improves on the table, so the table always records the current +state. A change that improves a count updates the table in the same commit. +`test/tools/baseline_ratchet.jl` rejects a lowered floor or a raised ceiling against +the earlier revision. + +- `@inferred` floor. The number of `@inferred` uses in each test file. Deleting a + failing inference test lowers the floor and fails. +- JET ceilings. `JET.report_opt` with `target_modules = (LineCableModels,)` analyses the + frequency-loop kernels with the concrete arguments of the default coaxial computation + of a three-phase cable system: `_solve!`, `cable_impedance!`, `cable_potential!`, + `earth!`, `reduce_line_matrices!` and the modal `decompose!` of each formula. The + table counts the reports of each kernel. The current reports all come from the boxed + earth-return records. +- Allocation ceilings. `preservation_corpus(n)` in `test/support/scenarios.jl` defines one + scenario per computation shape. Each scenario lists the positional arguments of + `compute` for a coaxial cable system, three bare wires in air and in earth, a modal + composition, CableConstants, a product and a zip parametric study, LinearError and + MonteCarlo with a fixed seed. `three_layer` puts the coaxial system on an earth of three + soil layers with the default formulation, which consumes the `:default` reduction. + `three_layer_before_fd` uses the same earth with a dispersive soil law before the + bottommost reduction. `test/tools/allocations.jl` runs the corpus at 2 and at 4 + frequencies in a new process and prints the table rows. A row records the allocation + count of one warmed-up call, which must repeat exactly on each call, and the smallest + byte total of three calls. Measurements stores a partial derivative only when it is + nonzero. Whether a round-off derivative is exactly zero depends on the machine's last + bits. So a count may differ from its row by the scenario's allowance, the number of + uncertain real scalars that its result publishes. Real and imaginary parts count + separately. The tool computes the allowances from the live results and does not record + them. Scenarios of plain numbers have allowance zero and compare exactly. Byte totals + fail above the recorded total plus 512 B for byte accounting and the allowance times one + derivative entry. The counts depend on what the + process computed before. New scenarios go at the end of the corpus. A change that + reorders or edits a scenario records again every row from that scenario onward. An + allocation key is a corpus scenario, and a new scenario cannot hide another scenario's + count. So the ratchet accepts a new row when every row of the earlier revision is still + present. A row that appears while another disappears is a rename, and the ratchet + rejects it. + +A test that moves to another file keeps its `@inferred` uses. The ratchet accepts a +lower `@inferred` floor when the removed `@inferred` lines appear again in another file, +unchanged apart from indentation, and cover the difference. + +The JET and allocation tables hold for the environment in `[environment]`: the Julia +version and the SHA-256 of the committed `Manifest.toml`. The CI quality job uses that +version and instantiates that Manifest. The other CI jobs delete the Manifest and +resolve the newest compatible versions. In another environment, both checks fail with +a request to record the tables again. A dependency update records both tables again in +the same change as the new `Manifest.toml`, with the counts of +`test/tools/allocations.jl` and of the JET check. A JET or allocation ceiling may rise only in a change that also changes +`[environment]`. + +The local tools compare the working tree with a git revision `REF`. Each checks out +`REF` in a temporary git worktree. That test environment starts from the repository +`Manifest.toml`, so both sides use the same dependency versions where `REF` allows +them. Each side runs its own revision's corpus. A revision without one uses the working +tree's copy, and the report says which copy each side used. + +The timing comparison is `julia --project=test test/tools/performance.jl REF`. One worker +process per side holds its corpus. Both workers run on the same logical CPU, a +performance core on a hybrid machine. Only one of them runs at any moment. Each +scenario runs in 6 rounds of 0.5 s per side, and the side that goes first alternates. +Machine load only adds time. Each side keeps its fastest sample. When the fastest +samples of the rounds on one side differ by more than 2 %, the scenario is unstable and +has no verdict. Separate builds of identical code differ by up to about 4 % on the +development machine, because each worktree builds its own package image. That is the +noise floor of the tool. A scenario more than 10 % slower fails with exit status 1. A +scenario 5 to 10 % slower is a possible slowdown below the resolution of the tool, and +an unstable scenario also gives exit status 2. Exit status 2 means no verdict, not a +regression. A possible slowdown that repeats over three runs goes to a person as a likely +regression. Both revisions run on the machine that runs the tool, a developer machine or a +CI runner. Other load on that machine makes scenarios unstable. +The exact performance checks are the `@inferred` floor and the JET and allocation +ceilings. The timing comparison catches large algorithmic slowdowns. + +The workflow `.github/workflows/timing.yml` runs the timing comparison on a CI runner, +with the Julia version and the `Manifest.toml` of the quality job. Each push to a pull +request compares the new head with the previous one. A manual run of the workflow +compares with the commit given as its `base` input. Exit status 1 fails the run. Exit +status 2 means no verdict, and the run passes with a warning that lists those scenarios. +A push never waits for an idle local machine. + +The equivalence check is `julia --project=test test/tools/equivalence.jl REF [ALLOWED]`. +On each side, `test/tools/fingerprint.jl` records every node of the inputs and the result +of each scenario, with the type of the node and the bits of its value. Type names consist +of the module that defines them followed by the name. The recording also counts +the points that each parametric scenario materializes and the blueprint lowerings of its +designs. The optional `ALLOWED` file declares the expected differences, one per line: + +```text +rename | Old => New +scenario | path prefix | value +``` + +Renames apply to whole identifiers in the type names, path segments and Symbol values +recorded at `REF`. Each declaration states one kind of difference: `value`, `type` or +`path`. An empty prefix declares the whole scenario. A `value` declaration covers +different values under the same type and never hides a changed type. Any undeclared +difference fails. The report lists renames and declarations that match no difference. + +## Developer paths + +- [Extension API](extensions.md) lists the equation methods and definition types omitted + from the user API reference. +- [Computational engine](engine.md) covers formulations, options, supplemental + computation output, and external implementations. +- [Makie plotting](plotting.md) covers the small high-level API and native ownership. +- [Conventions](conventions.md) defines placement, dispatch, naming, and + docstring rules. + +## Testing requirements + +### Release status and regressions + +The package has no published stable API. Development changes update the +implementation, callers, and relevant tests together. + +In this repository, a regression test protects against a bug detected in a +published stable release. It identifies the reported tracker issue and affected release, together with the protected behavior. Tests of current development behavior belong +to the behavior, mathematical implementation, integration, or architecture +suites, according to what they exercise. + +Tests follow the current API and its intended behavior. Preserve independent +scientific expectations when updating callers. Architectural tests verify each module through its consumers and native interfaces. + +### Verification coverage + +The harness checks that the current implementation works and follows the +codebase standards. It exercises actual public workflows and their owned +kernels: calculations, dispatch, units, ordering, data transport, errors, +resource handling and side effects. Tests use current input builders and small, +distinguishable examples with an expected result or rejection. + +Mathematical implementation tests remain appropriate. Checking an implemented +formula against a directly calculable result, a matrix reduction against a +constrained solve, or a derivative of a simple function verifies code correctness. +It does not certify the physical applicability of the model. Imports, shapes, +finiteness and round trips provide useful limited checks. They do not replace +value assertions where the implemented behavior has a decidable expectation. + +Float32 support means that valid ordinary inputs containing Float32 values work +without type-induced crashes. It includes no additional numerical accuracy +promise. Do not impose a high-precision reference target on Float32 and then +change owned numerical code to meet it. No Float32-specific widening, compensated +arithmetic or precision infrastructure is justified by such a test. Preserve +existing dispatch, hooks, uncertainty and supported types. Keep API conversions +when an actual API or dependency interface requires them. A requested tolerance +does not create an accuracy guarantee. + +Scientific validity, comparison of physical approximations, broad accuracy +claims, convergence research and scientific acceptance are outside the test +harness. Scientific acceptance belongs to the researcher's interpretation, not +the test runner. Such research is not a required CI job, and unfinished scientific +evidence is not a failing software check. Apply this distinction to passing and failing +experiments alike. + +A fabricated failure is as unacceptable as a fabricated success. Verify an +assertion's premise before treating its outcome as a product defect. Correct or +remove a defective criterion with its reason. Do not preserve it merely because +it was fixed before execution. Do not change inputs, outputs or tolerances just +to obtain a pass. Preserve observed differences and distinguish assertion +failures from execution errors or unavailable verification. + +### Architecture and coverage + +Architectural tests protect current responsibilities through real behavior and +native method and interface checks: owner-local dispatch, fixed report stages, +observation and table owners, validated inputs, optional integrations and +caller-owned state. A conforming new leaf must work through the actual composed consumer. + +The existing source-amended production line-coverage gate remains at 95%, with +its current `src/` and `ext/` inventory. Measure executed code, then cover actual +missing behavior in its existing test owner. Assertion counts, obsolete guards, +denominator changes and fabricated expectations cannot satisfy this objective. +Keep incomplete executions and real bugs visible. Coverage does not turn them +into successes. + +## External interface methods + +`test/quality/explicit_imports.jl` loads the numerical, XLSX and Cairo adapters +explicitly. All mechanical ownership and import checks remain active. Each +exception identifies an exact consumer, owner and name for a documented +upstream interface that lacks a Julia `public` annotation. Access to +package-private names remains prohibited. + +The following external interfaces are used by the package: + +| Access | Supported use | +| --- | --- | +| `CairoMakie.activate!` | Documented [backend activation](https://docs.makie.org/stable/explanations/backends/cairomakie.html). Cairo adapter only. | +| `Base.IOError` | Native I/O exception, including [filesystem errors](https://docs.julialang.org/en/v1/base/file/). Renderer export errors and FEM recovery from file and process errors. | +| `Base.unalias` | Documented native preventative-copy operation in Julia 1.12's `base/abstractarray.jl`. Used only by the allocating Kron entry point in `Commons` to preserve source and destination aliasing. The reduction path uses separate preallocated buffers. | +| `Base.get_extension` | Identifies the loaded Cairo extension. Its public `CairoMakie` binding supplies the native save backend. | +| `Makie.automatic` | Documented [native attribute default](https://docs.makie.org/stable/api). Renderer only. | +| `Makie.current_backend` | Documented [backend-dependent API default](https://docs.makie.org/stable/api). Renderer only. | +| `Makie.get_ticks`, `Makie.get_tickvalues` | Documented [axis extension hooks](https://docs.makie.org/stable/reference/blocks/axis.html). Renderer only. | +| `Makie.pseudolog10` | Documented [axis scale](https://docs.makie.org/stable/reference/blocks/axis.html). Renderer only. | +| `Makie.inverse_transform` | Documented [custom axis scale interface](https://docs.makie.org/stable/reference/blocks/axis.html#xscale). The renderer applies axis margins to full uncertainty bounds in the selected scale, then maps them back without a second inverse-scale mapping. | +| `Makie.CategoricalConversion` | Documented [categorical axis conversion](https://docs.makie.org/stable/reference/generic/dimensional/). Renderer assembly axes only. | +| `Makie.defaultlimits` | The documented native scale-default hook named by Makie's Axis attributes. The renderer queries it only when an empty axis changes scale. The native scale then supplies its valid interval. | +| `propertynames`, `Makie.default_theme` | Axis attributes use `propertynames` and the native `palette` keyword. Scatter attributes use the exported [`default_theme`](https://docs.makie.org/v0.24/explanations/recipes) method. | +| `LegendElement.plots` | Associates legend glyphs with source plots through the documented [LegendElement extension interface](https://github.com/MakieOrg/Makie.jl/blob/v0.24.13/Makie/src/makielayout/types.jl). | +| `on`, `off`, `ObserverFunction.observable` | Manage visibility subscriptions using the documented [`ObserverFunction.observable`](https://juliagizmos.github.io/Observables.jl/stable/#Observables.ObserverFunction) notification target. | +| `Makie.fast_string_boundingboxes(Text)` | Public attribute subscriptions query the documented [text bounds](https://github.com/MakieOrg/Makie.jl/blob/v0.24.13/Makie/src/basic_recipes/text.jl), retaining marker-space extents. | +| `GridLayoutBase.remove_from_gridlayout!` | Retained as the [maintainer-prescribed nested-layout removal](https://discourse.julialang.org/t/makie-removing-gridlayouts/103935) workaround. It is not an exported stable API. Only this renderer call is admitted. Existing legend recreation and layout tests protect it. | + +These integration methods are scoped to the supported Makie 0.24 family. +The layout workaround needs review when that compatibility range changes. +The owned legend, uncertainty visibility, series style and colorbar endpoint tests +exercise the actual affected paths. Behavioral compatibility requires execution of the affected paths. Negative gate controls reject unlisted renderer +internals, different consumers and package-owned private calls. diff --git a/docs/src/docstrings.md b/docs/src/docstrings.md deleted file mode 100644 index 01ee70ffb..000000000 --- a/docs/src/docstrings.md +++ /dev/null @@ -1,197 +0,0 @@ -# Docstrings - -The following docstring standards are generally adopted across the codebase. - -## General principles - -1. **Placement:** Docstrings must immediately precede the code entity (struct, function, module, constant) they describe. -2. **Delimiter:** Use triple double quotes (`"""Docstring content"""`) for all docstrings, *except* for individual struct field documentation. -3. **Conciseness:** Avoid redundancy. Information should be presented clearly and concisely in the appropriate section. -4. **Tone:** Use formal, precise scientific language suitable for technical documentation. Avoid contractions, colloquialisms, and ambiguous phrasing. - -## Physical unit formatting - -All variables corresponding to physical quantities must be annotated with their SI units and according to the following rules: - -1. **Mandatory units:** ALL arguments, return values, struct fields, and constants representing **physical quantities** MUST have their SI units specified. -2. **Dimensionless quantities:** Physical quantities that are dimensionless MUST be explicitly marked as `\\[dimensionless\\]`. -3. **Non-physical quantities:** Do *not* add unit annotations to arguments, variables, or fields that do not represent physical quantities (e.g., counters, flags, indices). -4. **Standard format:** Units MUST be enclosed in double-backslash escaped square brackets: `\\[unit\\]`. - - **Correct:** `\\[m\\]`, `\\[Hz\\]`, `\\[Ω\\]`, `\\[H/m\\]`, `\\[dimensionless\\]` - - **Incorrect:** `[m]`, `\[m]`, `m` (as a standalone unit identifier) -5. **Exception for example comments:** Inside ` ```julia` code blocks within the `# Examples` section, use *regular* (non-escaped) square brackets for units within comments. - - **Correct:** ```julia result = calculation(10.0) # Output in [m]``` - - **Incorrect:** ```julia result = calculation(10.0) # Output in \\[m\\]``` -6. **Common units:** Use standard SI abbreviations (e.g., `m`, `s`, `kg`, `A`, `K`, `mol`, `cd`, `Hz`, `N`, `Pa`, `J`, `W`, `C`, `V`, `F`, `Ω`, `S`, `T`, `H`, `lm`, `lx`, `Bq`, `Gy`, `Sv`, `°C`). Use the Unicode middle dot `·` for multiplication where appropriate (e.g., `\\[Ω·m\\]`). - -## Mathematical formulation formatting - -1. **Requirement:** Mathematical formulas rendered using LaTeX are MANDATORY *only* for functions/methods whose names start with the prefix `calc_`. -2. **Location:** For `calc_` functions, the LaTeX formula MUST be placed within a ```math ...``` block inside the `# Notes` section. -3. **Forbidden:** Do NOT include ```math``` blocks or LaTeX formulations for any functions or methods *not* prefixed with `calc_`. -4. **LaTeX escaping:** Within documentation text AND inside ```math``` blocks, all LaTeX commands (like `\frac`, `\mu`) MUST have their backslashes escaped (`\\`). - - **Correct:** `\\mu_r`, ```math \\frac{a}{b}``` - - **Incorrect:** `\mu_r`, ```math \frac{a}{b}``` - -## Documentation templates - -The subsections below contain templates for different types of code elements. - -### Structs - -- **Main docstring:** Use `$(TYPEDEF)` for the signature and `$(TYPEDFIELDS)` to list the fields automatically. Provide a concise description of the struct purpose. - - ```julia - """ - $(TYPEDEF) - - Represents a physical entity with specific properties... - - $(TYPEDFIELDS) - """ - struct StructName - # Field definitions follow - end - ``` - -- **Field documentation:** - - Place *directly above* each field definition. - - Use single-line double quotes: `"Description with units \\[unit\\] or \\[dimensionless\\] if applicable."` - - Do NOT use `""" """` block quotes or inline comments (`#`) for documenting struct fields. - - -### Constructors (inside or outside structs) - -- ALL constructors MUST be documented using the `@doc` macro placed immediately before the `function` keyword or the compact assignment form (`TypeName(...) = ...`). This applies even to default constructors if explicitly defined. -- **Format:** Use `$(TYPEDSIGNATURES)`. Include standard sections (`Arguments`, `Returns`, `Examples`). - - ````julia - @doc """ - $(TYPEDSIGNATURES) - - Constructs a [`StructName`](@ref) instance. - - # Arguments - - - `arg_name`: Description including units `\\[unit\\]` if physical. - - # Returns - - - A [`StructName`](@ref) object. [Optionally add details about initialization]. - - # Examples - - ```julia - instance = $(FUNCTIONNAME)(...) # Provide meaningful example values - ``` - - """ - function StructName(...) - # Implementation - end - ```` - -### Functions / methods - -- **Format:** Start with `$(TYPEDSIGNATURES)`. Follow the section order described. - - ````julia - """ - $(TYPEDSIGNATURES) - - Concise description of the function's purpose. - - # Arguments - - - `arg1`: Description, units `\\[unit\\]` if physical. Specify `Default: value` if applicable. - - `arg2`: Description, `\\[dimensionless\\]` if physical and dimensionless. - - # Returns - - - Description of the return value, including units `\\[unit\\]` if physical. Document multiple return values individually if using tuples. - - # Notes (OPTIONAL - MANDATORY ONLY for `calc_` functions for the formula) - - [For `calc_` functions: Explanation and formula] - ```math - \\LaTeX... \\escaped... \\formula... - ``` - - # Errors - - - Describes potential errors or exceptions thrown. - - # Examples - - ```julia - result = $(FUNCTIONNAME)(...) # Use representative values. Add expected output comment. - # Example: result = $(FUNCTIONNAME)(0.02, 0.01, 1.0) # Expected output: ~0.0135 [m] - ``` - - # See also - - - [`related_package_function`](@ref) - """ - function function_name(...) - # Implementation - end - ```` - -- **Section order:** - 1. Description (no heading) - 2. `# Arguments` - 3. `# Returns` - 4. `# Notes` (Only if needed; mandatory for `calc_` functions) - 5. `# Errors` (Only if needed) - 6. `# Examples` - 7. `# See also` (Only if needed) -- **Spacing:** Ensure exactly one blank line separates the description from `# Arguments` and precedes every subsequent section heading. -- **Examples:** Use the `$(FUNCTIONNAME)` macro instead of hardcoding the function name. Use meaningful, realistic input values. Include expected output or behavior in a comment, using *non-escaped* brackets for units (`[unit]`). -- **See also:** Only link to other functions *within this package* using `[`function_name`](@ref)`. Do not link to Base Julia functions or functions from external packages unless absolutely necessary for context. Only include if the linked function provides relevant context or alternatives. - -### Modules - -- **Format:** The first line must be the module name indented by four spaces. Use `$(IMPORTS)` and `$(EXPORTS)` literals. - - ````julia - """ - ModuleName - - Brief description of the module purpose within the broader package (e.g., for [`Package.jl`](index.md)). - - # Overview - - - Bullet points describing key capabilities or features provided by the module. - - # Dependencies - - $(IMPORTS) - - # Exports - - $(EXPORTS) - """ - module ModuleName - # Contents - end - ```` - -### Constants - -- **Format:** Use a single-line docstring with double quotes (`"..."`). Include a brief description, the symbol of the constant if standard (e.g., `μ₀`), its value, and its units using the `\\[unit\\]` format. - - ```julia - "Magnetic constant (vacuum permeability), μ₀ = 4π * 1e-7 \\[H/m\\]]." - const μ₀ = 4π * 1e-7 - ``` - -## Common mistakes to avoid - -Double-check the docstrings to avoid these common errors: - -- **Missing `@doc` for constructors:** ALL constructors require the `@doc` macro before their definition. -- **Incorrect struct field docstrings:** Use single-line `"..."` *above* the field, not block `"""..."""` quotes or inline `#` comments. -- **Incorrect section order:** Follow the specified order for function docstring sections precisely. -- **Hard-coding function names in examples:** Always use `$(FUNCTIONNAME)`. -- **Incorrect unit formatting:** Ensure `\\[unit\\]` syntax is used everywhere except comments within `Examples` blocks (`[unit]`). Double-check escaping (`\\`) for LaTeX. -- **Adding math formulas to non-`calc_` functions:** Math blocks are *only* for functions prefixed with `calc_`. diff --git a/docs/src/engine.md b/docs/src/engine.md new file mode 100644 index 000000000..f75b1d04f --- /dev/null +++ b/docs/src/engine.md @@ -0,0 +1,1468 @@ +# Computational engine + +LineCableModels separates the physical problem, selected equations, and numerical +execution. Source equations and bibliography belong in the implementing formula +file. Backend/formula methods select an implementation through Julia dispatch. + +Each family owns a `:default` routing identifier and resolves it to an explicit +implementation before evaluation. Literature references remain attached to +the equations without determining their software names. + +| Family | Registered choices | +|---|---| +| Internal impedance | `:default`, `:schelkunoff1934`, `:wedepohl1973` | +| Insulation impedance | `:default`, `:ametani1980` | +| Pipe impedance | `:default`, `:none` | +| Insulation admittance, semicon admittance | `:default`, `:lossless`, `:lossy` | +| Local shunt geometry | `:default` (equivalent annular layer), `:equivalent`, `:boundary` | +| Earth impedance | `:default`, `:unified`, `:carson1926`, `:pollaczek1926`, `:gary1976`, `:wedepohl1973`, `:saad1996`, `:ametani2009`, `:lucca1994`, `:wise1934`, `:xue2018` | +| Earth admittance | `:default`, `:unified`, `:ideal`, `:pollaczek1926`, `:wise1948`, `:xue2018` | +| Frequency-dependent soil properties | `:default`, `:constant`, `:alipio2014`, `:cigre2019`, `:datsios2019`, `:longmire1975`, `:messier1985`, `:portela1999`, `:scott1967`, `:visacro1987`, `:visacro2012` | +| Equivalent earth | `:default`, `:bottommost` | +| Modal transformation | `:default`, `:chrysochos2014`, `:vieira2026`, `:wedepohl1996` | +| Temperature-dependent resistivity | `:default`, `:linear` | + +The internal default and `:schelkunoff1934` retain Schelkunoff's tubular +conductor expressions. Insulation impedance's default and `:ametani1980` +retain the annular magnetic term documented by Ametani. +Both earth defaults route to `:unified`, which implements the supplied circumferentially averaged framework +with complete enclosed-current normalization. The coaxial backend also evaluates +`:gary1976` aerial impedance, `:saad1996` and `:wedepohl1973` buried impedance, +`:lucca1994` mixed impedance, and `:ideal` external potential coefficients. +Other registered earth identities throw "not yet implemented". They never substitute Unified. +Independent external-backend and consumer-defined implementations are unaffected. +Dielectric `:default` selections +route to explicit `:lossless` equations. `:lossy` retains conductivity and the +material's supplied polarization losses. The FrequencyDependent `:default` +routes to `:constant`, which preserves static properties. Its explicit +literature relations model measured soil dispersion. Their references remain +attached to the equations without determining their software names. +EquivalentHomogeneous selects the basement when explicitly requested, or when a +homogeneous earth formula meets an earth model with more than two layers, and the +modal default performs Levenberg–Marquardt tracking. The EquivalentHomogeneous +default is a package-defined selection rather than an author equation. + +## Coaxial computation + +The workflow is design, system, problem, formulation, then `compute`. Initialization +flattens the selected designs into blueprint tables. It binds equations to their required inputs and selected output entries before allocating one +`LineParametersWorkspace`. + +The computation evaluates fixed-temperature conductor properties and required +frequency-dependent earth tables, then follows this order at each frequency: + +1. Complete equivalent-earth evaluation and dielectric material admittivities. +2. Calculate cable-local impedance and potential coefficients. +3. Calculate the selected exterior impedance and potential coefficients together + through `earth!`. +4. Assemble primitive matrices, apply the selected reductions and store Z/Y. + +`BeforeFD` and `AfterFD` retain their distinct ordering. Material-law calls receive +valid material objects. Field formulas receive completed material properties. +Their wave numbers and mathematical approximations do not replace those values. +Boundary-shunt coefficients remain blueprint-time quantities. + +The workspace input and plan retain geometric tables and bind equations +to their inputs and outputs. Its buffers hold evaluated material tables, local coefficients, +separate exterior Z/P destinations, primitive and reduction matrices, QuadGK storage +and any numerical arrays that the selected equations build in `initialize_buffers`. Trace +storage is optional. Mutable calculation buffers belong to each independent computation. + +Ordinary earth equations evaluate their indexed cases. A coupled equation may +require full-system inputs even when only some of its entries are selected. +Those inputs and compatible Z/P consumers are bound during initialization. +Compatible consumers calculate one response per frequency. Different +configurations calculate separately and publish their selected entries before +reusing buffers. The binding records the correspondence between inputs and +selected output entries. Each call evaluates a fresh response. + +## Internal shunt geometry + +`shunt_model` selects the cable-local geometry approximation independently of +the dielectric material laws. Both `Formulation()` and +`CableConstantsFormulation()` default to `shunt_model=:default`: the equivalent +annular layer, with no boundary solve. `:equivalent` selects it explicitly. + +`insulation_admittance` and `semicon_admittance` select the material +admittivity κ [S/m]. For one homogeneous annulus, +the shunt branch is `y = 2πκ/log(ro/ri)` [S/m]. Successive dielectric layers +combine in series. This is the same reduced geometry used by the coaxial +backend, including its equivalent wire-screen geometry. + +Select `shunt_model=:boundary` to resolve eligible open wire and finite-tape +groups inside a closed circular shield before the frequency sweep. This local +shunt calculation supplies a coupled terminal operator that retains direct +coupling across open intermediate screens. The same blueprint construction +supplies `CableConstants`. + +The resolved local calculation preserves physical filler permittivity, finite +tape thickness, conductor terminal membership and concentric dielectric layers. +It couples all intermediate terminals in a qualifying domain together. Round +wires use auxiliary sources. Tapes use integrated corner-weighted charges on +their two circular faces and two ends. The inner core retains the existing +equivalent-core assumption, including bounded circular, rectangular, compacted +and sector stranded constructions. This calculation supplies the internal +shunt contribution and requires the radial terminal ordering of coaxial lowering. +Series impedance, earth return, and matrix reductions use their respective +selected formulations. + +Qualification currently requires an explicitly filled annular host, continuous +concentric dielectric paths, exposed whole wire and tape geometric boundaries, and a closed +circular reference shield. Separate shielded assemblies are handled in their +own local frames. Overlapping same-terminal faces, interacting courses in +different hosts, nonconcentric dielectric interfaces and noncircular reference +shields retain the existing equivalent-coaxial treatment. They are not fed to +an inapplicable circular Green function. The resolved path currently uses the +built-in `:lossless` insulation or semicon laws or their `:default` aliases. +Lossy or custom constitutive selections are unsupported by `:boundary` and raise `BoundarySolveError` for +eligible domains. Select `:equivalent` to retain their radial calculation without +losing conductivity or frequency dependence. + +Inspect `details(result).data.shunt_model` for requested and effective model, domain +terminal ranges, solve counts, and diagnostics. Boundary-resolved domains +coexist with ordinary radial intervals outside their coverage. `effective` +describes the qualifying domains. `:mixed` indicates explicit fallback in some +of them. Numerical grid residuals and small-coupling indicators are not +certified error bounds. Whole-matrix convergence can conceal substantial +relative changes in weak individual couplings. + +Formulation-aware blueprint construction uses bounded dense storage and +in-place pivoted QR. The completed `CableBlueprint` owns lossless terminal +capacitance and potential coefficients, local terminal coverage and numerical +outcomes. Workspaces consume these coefficients without performing a boundary solve. +Repeated equivalent domains share coefficient matrices within the same +construction call. Formulations with identical local selections share blueprints +even when earth-return choices differ. Independent uncertainty sources prevent +sharing, and dense geometric boundary matrices are released after construction. + +Select the formulation and call `compute` directly: + +```julia +formulation = Formulation(shunt_model=:boundary) +result = @time compute(problem, formulation) +``` + +The compute call constructs the selected local shunt model. Blueprints with the default +equivalent annular layer contain no geometric boundary blocks, extract no geometric boundary domains, and evaluate +no geometric boundary material law. Their reports describe the concentric assembly ranges. +No geometric boundary audit is implied. +Boundary coefficients are independent of the frequency grid and external earth +model. They are reused across the frequency sweep, but a fresh `compute` call +constructs fresh blueprints: there is no process-global or cross-call cache. +Changed geometry and Monte Carlo realizations receive new coefficients. +Do not mutate coefficient arrays shared by completed blueprints. + +Production reports unmet quadrature targets, estimated rank, reciprocity and +positive-capacitance concerns as warnings. The computed finite result is retained +without retry, repair or substitution. Dense-storage limits, degenerate columns, +actual solve failure and nonfinite physical results remain errors. The independent +validation grid and derivative step-refinement are opt-in: + +```julia +boundary = formula(:boundary; + parameters=(fallback=:error,), + options=( + resolution=(wire=64, order=32, quadrature=256, modes=1024), + integration=(rtol=1e-8, atol=1e-10, maxevals=100_000), + audit=false, + )) +formulation = Formulation(shunt_model=boundary) +``` + +Set `audit=true` for the independent checks. Unaudited residual fields are +`nothing`, not zero. The audit warns on unmet criteria and does not alter the +computed terminal matrix. +The integration tolerances control dimensionless logarithmic moments, not +the error in an individual terminal coupling. + +See [`BoundarySolveError`](@ref) and [`LineCableModels.Engine.ShuntModel.Formula`](@ref) +for the failure categories and formula selections. + +Actual failures propagate by default. An explicit `parameters=(fallback=:equivalent,)` +permits the equivalent annular layer as replacement only after recognized numerical or unsupported-law +failures, with a warning and recorded reason. Finite-result quality warnings do +not trigger that fallback. Invalid inputs and unexpected +exceptions propagate. UQ wrappers reject automatic fallback. Choose strict +`:boundary` or `:equivalent` for the whole study. Monte Carlo also checks that +shunt-model coverage remains fixed across realizations and never retries a +`BoundarySolveError` as a geometry rejection. + +With Measurements loaded, local sensitivities preserve the original correlated +inputs. The nominal QR is reused in an implicit least-squares derivative that +includes its residual term. Centered kernel derivatives use one step in +production. The audit also checks step halving. Derivative matrices +are streamed in bounded blocks rather than stored +as dense Measurement arrays. This is fixed-topology linear propagation, not a +claim about differentiability across a contact or strand-count transition. +Each Monte Carlo realization constructs its own physical geometry and material +operator. Identical cables and formulations within that realization share it. +Concentricity checks include coordinate dependencies: two centers with equal +nominal positions but independent uncertainty do not qualify as concentric. +The local geometric boundary calculation uses Float64 workspaces. It does not promise +arbitrary-precision geometric boundary accuracy when surrounding scalar inputs use a +wider type. + +### Numerical method and attribution + +The annular Green function is assembled from classical cylindrical Laplace +harmonics: `r^m` and `r^-m`, with `1` and `log(r)` for the zero mode. +[Schelkunoff1934](@cite), Eqs. (122)-(124), p. 573, gives this radial basis +in its treatment of cylindrical fields in the small-radius-to-wavelength +limit. Here the same basis solves the electrostatic potential problem. +[Sunde1968](@cite), Section 1.5, Eq. (1.20), p. 11, supports the coaxial +capacitance normalization. Section 1.6, Eqs. (1.39)-(1.42), p. 15, gives +the load and reflection transformation applied here in log-radius coordinates. +Matching the dielectric interfaces and grounded inner and outer boundaries +assembles these ingredients into the implemented layered annular kernel. + +The finite-thickness, layered-Green-function, whole-face charge approach has +precedent in [Bernal1997](@cite), especially Section IV, Eq. (10). Their +Maxwell-weighted Chebyshev/Galerkin formulation is not reproduced verbatim: +this implementation uses a concentric circular Green function, normalized +Jacobi face charges and oversampled boundary collocation with pivoted QR. + +[Campione2018](@cite), Section 2, Eq. (3), provides a cable-screen application +of dielectric image factors `(εhost − εadjacent)/(εhost + εadjacent)`, also +used in the extracted nearest-interface terms here. Their cylindrical braid +is locally approximated by a plane. That work is not a derivation of this +implementation's concentric annular kernel or radial layer recursion. + +The corner weights encode the finite-energy edge behavior of +[Meixner1972](@cite). Corners on dielectric interfaces use the actual adjacent +permittivities, following the metal-dielectric wedge principle of +[VanBladel1985](@cite). The homogeneous exponent is insufficient for all corners. [Classen2011](@cite), Sections 2 and 4, supports incorporating known +singularities into an approximation space. Its FIT/DG methods are not this +tape element. The implemented corner equation and normalized charge measure +are documented below. + +The need for special evaluation near source boundaries is discussed by +[HelsingOjala2008](@cite), Section 1. Here logarithmic direct and image moments use +adaptive Gauss–Kronrod quadrature after a cosine change of variable, while +the smooth remainder uses Gauss–Jacobi quadrature. This is a different +algorithm from their rational-quadrature scheme. + +The following internal numerical methods document the equations, charge +normalization and quadrature needed to reproduce the calculation. These implementation details are internal to the calculation. Source placement, +resolution controls, validation thresholds and uncertainty differentiation +remain package-specific choices, not accuracy guarantees supplied by these +references. + +```@docs +LineCableModels.Engine.ShuntModel._shunt_load +LineCableModels.Engine.ShuntModel._shunt_kernel_coefficients +LineCableModels.Engine.ShuntModel._shunt_junction_exponent +LineCableModels.Engine.ShuntModel._shunt_tape_faces +LineCableModels.Engine.ShuntModel._shunt_log_moments +LineCableModels.Engine.ShuntModel._shunt_capacitance +``` + +## Formula selection + +```julia +selected = Formulation( + earth_impedance=formula(:default; + options=(integration=(method=:quad, options=(rtol=1e-8,)),)), + earth_admittance=formula(:default), + earth_properties=formula(:default), +) +result = compute(problem, selected; options=(trace=true,)) +``` + +`FormulaDefinition` includes an identifier, physical `parameters`, numerical +`options`, and an optional formula-local `equivalent_earth`. It is a passive +request. A family constructor resolves a symbol or declaration to a concrete +selection. A completed user-owned selection passes through unchanged. Built-in +formula lists enumerate the implementations supplied by the package. + +An `Expression(selection, operation, Val(...), ...)` is one expression of the +selected formula: the family operation, called with that selection first. `formula_id` supplies descriptive metadata. The family constructor +resolves `:default` to a concrete implementation before computation. + +For the analytical families, the default routes are: + +| Family | Explicit implementation | +|---|---| +| Internal impedance | `:schelkunoff1934` | +| Insulation impedance | `:ametani1980` | +| Earth impedance and potential coefficients | `:unified` | +| Insulation and semicon admittivity | `:lossless` | +| Frequency-dependent earth properties | `:constant` | +| Equivalent homogeneous earth | `:bottommost` | +| Temperature-dependent resistivity | `:linear` | +| Modal transformation | `:chrysochos2014` | +| Local shunt geometry | `:equivalent` | +| Pipe contribution | `:none` | + +Each selected equation enforces its applicability requirements. `:none` accepts +coaxial geometry, which does not require an additional pipe term. Conductive pipe systems +require a pipe formulation. `:unified` requires supported earth geometry. PSCAD defines its native selections. FEM defines its field equations and accepts supported +material selections. + +Numerical defaults belong to `formulation_options(::Expression{<:MySelection, +typeof(operation), ...})`. Empty defaults exclude controls. Unknown or unused +numerical sections are errors. The selected formula provisions QuadGK storage +through `initialize_buffers` when its required indexed consumers need integration. +Full-system earth equations provision their required storage through the same +selected-formulation dispatch. + +Custom types implement the existing family operation and expose `parameters` +and `options` records. Internal selections expose per-surface options. +Earth selections expose `equivalent_earth`. Constructors own parameter checking and +option normalization. +Implement `formula_id`, `description`, `NamedTuple`, and `formulation_options` +for scientific inspection and persistence. A saved declaration is not executable +code. Unknown saved leaf identities remain passive identities. + +Unified accepts a prescribed longitudinal argument [1/m] through +`formula(:unified; options=(Γ=value,))`. Use a finite scalar for a value that applies to every sample. A finite vector aligns one-to-one with the problem's frequency vector, +in the supplied order. Zero is the default. Material constitutive laws evaluate +material objects before field calculations. Selected earth equations consume +those properties and calculate their own wave numbers and field approximations. + +The implemented equations use `s=jω` and the positive-time `exp(jωt)` phasor +convention. For the same real field expressed as `F̂₋ exp(Γ₋ x-jωt)`, the +positive-time representation has `F̂₊=conj(F̂₋)` and `Γ₊=conj(Γ₋)`. Complex +material coefficients and response phasors must use that convention consistently. +Supply Γ in this positive-time convention. The API uses its value directly. +Uncertainty propagation applies to the physical material inputs. + +`EarthPair` includes conductor row and column indices, integer source-target layer indices, +heights, horizontal separation and an explicit self radius. Self means the same +conductor. Distinct conductors in one layer remain mutuals. A self pair has zero +horizontal separation, with its radius supplied separately. The geometry substitution +needed by a published self expression occurs at equation evaluation. + +The external expression signatures are: + +```julia +earth_impedance(formula::MyEarthImpedance, ::Val{Kind}, ::Val{S}, ::Val{T}, functor, workspace) +earth_potential_coefficient(formula::MyEarthPotential, ::Val{Kind}, ::Val{S}, ::Val{T}, functor, workspace) +``` + +`Kind` is `:self` or `:mutual`. Source `S` is the matrix column, and target `T` +is the row. Layer 1 is air. Soils occupy layers 2 through N. Required methods +are invoked through indexed dispatch. Before the frequency loop, the plan checks +that the formula has a method for each interaction. Domain-defining methods accept +the Functor and the workspace without extra subtype constraints. Numerical +specializations can optimize an admitted case. Julia method availability determines +which expressions can be selected. + +Each earth slot also accepts an explicit NamedTuple recipe: + +```julia +selected = Formulation(earth_impedance = ( + earth = formula(:unified), +)) +``` + +The same syntax applies independently to `earth_admittance`. The labels resolve +`(1,1)`, `(2,2)` and the two cross-layer directions before the existing indexed +method dispatch. Each case retains its own formula, numerical options and +material inputs. The example covers an all-buried problem. Whole-family +omission or `nothing` routes to `:default`. An explicit recipe must cover every +required case. Allocation and evaluation include only the leaves used by those +cases. True layered inputs require a scalar selection. + +The default potential formulation supplies both mixed directions. Selecting a +default leaf for only part of a matrix still assembles its complete auxiliary +system and then selects the requested final entries. Other selected output +equations never become inputs to its axial-field or source-potential coefficients. + +The workspace binds every required ordered pair before frequency evaluation. +Geometry and layer indices follow the same order. Each direction retains its +inputs and output position. Assembly, reduction and modal transformation preserve +the returned ordered entries. +No implicit reciprocity operation supplies a missing equation or averages its result. + +Compatible impedance and potential selections share one bound calculation +with explicit output indices for each quantity. Distinct controls retain separate +calculations and material arrays. The bound calculations and their equation +groups are concrete tuples. Conductor interactions and numerical storage remain +arrays. The complete scan specializes on those tuples before entering its +frequency loop, while the workspace type remains independent of conductor layout. + +Repeated numerical interactions use the shared `Engine.earth!` traversal of +the parts of each earth calculation. During workspace construction, +`same_physical_state(formula, a, b, geometry)` compares the invariant arithmetic inputs +of two conductor pairs. The default compares their destination indices. A formula +compares the inputs it reads instead only when its arithmetic does not use the indices. +These inputs serve reuse alone: expressions consume the existing `EarthPair` and +evaluated material data, without a second pair representation or callback interface. + +Earlier interactions belong to the same bound equation and resolved controls. At each +frequency, Engine uses `same_physical_state` to compare their current material +values. It evaluates each distinct interaction and distributes the resulting +scalar or tuple of coefficients. Equal nominal values with independent +uncertainty sources remain distinct. Each invocation overwrites representative +indices and diagnostic ranges. The calculation starts afresh at each frequency. Geometry +changes require a new workspace. Numerical types and tolerances are preserved. + +The source and target layers in the signatures of a formula's methods declare the +media it handles. Layer 1 is air and layer 2 the first earth layer. A formula admits a +layer when one of its methods accepts it as source or target, whatever the types of +the other arguments. On an earth model with more layers, a formula that admits layers +1 and 2 at most consumes its explicit `equivalent_earth` reduction. Without one, it +consumes the EquivalentHomogeneous `:default` reduction. Any admitted layer from 3 to +the deepest layer of the model gives the formula the whole layered earth and its +interfaces. + +The computation checks before the frequency loop that the formula has a method for +each interaction. Its `ArgumentError` states the number of earth layers and the +highest of them that the formula admits. Arbitrary-layer +Green-function generation remains deferred. Buried +placement in a vertical multilayer earth is rejected because its physical layer +indexing has no defined origin in the present geometry definition. + +The analytical impedance selections execute direct indexed equations: `:gary1976` +for aerial self and mutual, `:saad1996` or `:wedepohl1973` for buried self and mutual, +and `:lucca1994` for both mixed directions. The `:ideal` potential selection +uses electrostatic images for aerial pairs and zero external potential whenever +either conductor is buried. Insulation contributions remain in the total matrix. +Earth-kernel quadrature and extra buffers are unnecessary for these methods. +The following selection combines their impedance and potential equations: + +```julia +selection = Formulation( + earth_impedance = (air=:gary1976, earth=:saad1996, mixed=:lucca1994), + earth_admittance = :ideal, +) +phase = compute(problem, selection; options=phase_options) +``` + +The numerical-method docstrings state each formula's material approximations. +The formulas consume the material values evaluated by the frequency loop. +Registered identities with unimplemented numerical methods raise an error. +The defaults supply air-air, earth-earth and both mixed directions with +independent medium permeabilities. Every circumference must lie wholly in its +half-space and exterior circles must not overlap. The default requires homogeneous +earth or an explicitly globally consistent equivalent earth. Indexed dispatch +for arbitrary layer numbers is required for extension methods. It does not implement +new multilayer Unified equations. + +Unified computes the averaged axial-field coefficient and the referenced +source-potential coefficient of each conductor pair in one expression, +[`source_coefficients`](@ref LineCableModels.Engine.EarthAdmittance.source_coefficients(::Union{LineCableModels.Engine.EarthImpedance.Formula{:unified}, LineCableModels.Engine.EarthAdmittance.Formula{:unified}}, ::Union{Val{:self}, Val{:mutual}}, ::Union{Val{1}, Val{2}}, ::Union{Val{1}, Val{2}}, ::Any, ::Any)), +which returns them in that order. +This expression uses the same indexed traversal as the ordinary impedance and +potential expressions. Both coefficients are required by either +selected physical output and are calculated from the selected Unified formulation. + +The [complete-current matrix calculation](@ref LineCableModels.Engine.earth!(::Union{LineCableModels.Engine.EarthImpedance.Formula{:unified}, LineCableModels.Engine.EarthAdmittance.Formula{:unified}}, ::Any, ::Any)) +converts the source coefficients into physical exterior matrices before Engine +copies selected entries. Compatible impedance and potential selections share +one calculation and factorization. Incompatible controls or equivalent-media +selections retain separate calculations. Internal conductor and insulation +contributions retain the existing composition and terminal reductions. + +`axial_field`, `source_potential`, and `current_map` are physical work-array +names. Their documented exponential source-column scaling cancels from the +right solves. Per-conductor factors use Engine-resolved layers and radii. +Geometry validation remains with the problem and system. The shared spectral +kernels own contour and cancellation treatment. Generic integration consumes +the complete integrand without selecting a physical field or voltage reference. + +With `options=(trace=true,)`, `trace.Zg` and `trace.Pg` expose the exterior +matrices. The returned total line admittance also includes insulation effects. +`trace.integrals` retains the native integral values and QuadGK error estimates, +with formula, frequency, receiver-source and term identifiers. These estimates +are not final-matrix error bounds. Trace-off computations do not retain such history. +For repeated inputs, each logical receiver-source retains the evaluated integral +and its error estimate with that position's context. Warnings retain the same +requested controls and affected positions without repeating quadrature. + +Select an equivalent homogeneous earth on each consuming formula: + +```julia +selected = Formulation( + earth_impedance=formula(:default; + equivalent_earth=formula(:default; order=:before)), + earth_admittance=formula(:default; + equivalent_earth=formula(:default; order=:after)), + earth_properties=formula(:default), +) +``` + +The reduction's expression +`equivalent_material(formula, Val(kind), Val(s), Val(t), functor, workspace)` receives the +physical selectors. The input of its Functor stores all physical layer properties +(`rho`, `eps_r`, `mu_r`), the model, the physical pair, the frequency and the options. Its +result is one `EarthMaterial`. It defines its numerical sections independently +of the external equation. The consuming source explicitly admits compatible +reductions. An explicit reduction applies to every consumer, a layered formula included. + +The default selects the deepest soil layer. Martins-Britto et al. +[Martins-BrittoLopes2020](@cite) found that deep-layer conductivity predominated +in magnetic ground-return impedance for the multilayer soil cases they studied. +The default resistivity follows this limited approximation. Accuracy depends +on layer contrasts and frequency. Selecting one layer does not implement the +paper's equivalent-conductivity formula or establish the accuracy of the selected +permittivity and permeability. Choose another built-in rule or a user-owned subtype of +`EquivalentHomogeneous.AbstractRule` implementing `equivalent_material`. + +`:after` applies the selected frequency law to physical layers first. `:before` reduces static +properties and applies that same law to the resulting material. Physical and +effective pairs remain distinct in the binding, and reductions run for each +ordered interaction on which they depend. Layerwise evaluated properties are reused +between consumers when needed. The air material remains static. + +The following declaration sets numerical options for a user-defined outer +surface equation: + +```julia +using LineCableModels: Expression, formulation_options, FormulationOptions +const II = LineCableModels.Engine.InternalImpedance +# MyConductor is a concrete InternalImpedanceFormulation owned by the user. +formulation_options( + ::Expression{<:MyConductor,typeof(II.internal_impedance),Tuple{Val{:outer}}}, +) = FormulationOptions() +``` + +The equation is `II.internal_impedance(selected::MyConductor, ::Val{:outer}, +functor, workspace)`. The formula's `Functor` method builds its state once per +conductor and frequency, from the input `(r_in, r_ex, rho, mu_r, jω)`. All surface +equations of that selection share this state. An integral equation declares its own integration section. An +algebraic equation does not inherit another formula's numerical options. + +`InternalImpedance.surface_impedances(resolved_formula, r_in, r_ex, rho, mu_r, jω)` +returns `(inner,outer,transfer)` coefficients in Ω/m, with per-kind +numerical options applied. Internal kinds have no earth-layer selectors. Assemblers +own the current-basis transformation and matrix placement. The deferred pipe +contribution concerns one contained metal and its enclosing pipe. Recursive +assembly and pipe equations are outside this implementation. + +Internal impedance takes one formula, built-in or user-owned. Its inner, outer and +transfer surfaces are the expressions of that formula, and they share its conductor +state. Before the frequency loop, the computation checks that the formula has an +expression for each surface impedance that the geometry needs: all three with a tubular conductor, +otherwise `outer`. The internal term is called `transfer`. Earth `self`/`mutual` +interaction names are unchanged. + +Equation numerical sections belong to `FormulationOptions`. The existing +`formulation_options` methods validate and normalize them at the defining stage: + +Spectral integration uses adaptive Gauss–Kronrod quadrature (`method=:quad`) +over the full spectral interval. Its controls are: + +```julia +integration = (method = :quad, options = (rtol = 1e-8, atol = 0.0, maxevals = 10^7)) +``` + +`SpectralIntegral(f)`, the formulation of a Sommerfeld-type integral over the spatial +Fourier variable, stores only the complete callable integrated over `[0, Inf)`. A formula includes its weights, any admissible contour and the +coordinate Jacobian in `f`, and supplies numerical subdivision points to +`integrate`. Engine knows none of those physical choices. Its half-line map +has no finite cutoff. Algebraic cases omit both integration calls and +integration controls. They still receive the same computation workspace. + +QuadGK retains physical scalar types, including BigFloat and correlated +Measurements inputs. Numerical error norms and sampling coordinates are nominal. +Physical values and derivatives are not replaced by nominal values. + +Quadrature returns its native `(value, estimated_error)`. An unmet requested +target produces a warning and returns the finite value without an outer retry, +tolerance tightening or matrix-error rejection. Invalid inputs, nonfinite values +and singular physical solves remain errors. The human judges accuracy. Meaningful +matrix-accuracy assertions belong in tests. + +Selected numerical formulas within one computation share reusable segment storage. +The main workspace holds formula-owned subdivision and coupled-response arrays. +`initialize_buffers` allocates storage for both earth and local formulas through +dispatch on the selected formulation. Each formula supplies the complete +integrand and its numeric subdivision hints. Compatible +unified consumers share one current calculation per frequency and publish the +completed entries directly. Independent computations never share mutable buffers. + +Each family explicitly includes its supported formula files. A file returns one identifier. An `Expression` holds a selected formulation, its indexed case and +the family-owned operation. +The frequency loop calls the bound methods directly. Register each distinct +equation under its own identity. + +Result details retain one requested and resolved formulation record. Its `requested` +field preserves the declaration. `methods` records the selected identities, +physical parameters and owner-held numerical controls, including each explicit +equivalent-earth choice and order. Unspecified numerical defaults remain owned by +the selected equation. +Absence of an equivalent-earth selection remains `nothing`. + +PSCAD extends the same equation generics with a variant that takes the PSCAD +formulation in place of the runtime arguments: + +```julia +earth_impedance(selection::EarthImpedance.Formula, ::Val{Kind}, ::Val{S}, ::Val{T}, pscad::PSCADFormulation) +earth_potential_coefficient(selection::EarthAdmittance.Formula, ::Val{Kind}, ::Val{S}, ::Val{T}, pscad::PSCADFormulation) +internal_impedance(selection::InternalImpedance.Formula, ::Val{Kind}, pscad::PSCADFormulation) +``` + +These methods compile native settings. Each actual ordered pair is validated +using the Engine's physical geometry. A complete native settings record is used +for project export, execution, readback and numerical-input fingerprinting. +Unsupported selections fail before export. All selected formula types and scientific +identifiers belong to the Engine, including equations awaiting an owned numerical +implementation. PSCAD's defaults resolve explicitly to: + +```julia +internal_impedance = :wedepohl1973 +insulation_impedance = :ametani1980 +earth_impedance = (air=:carson1926, earth=:pollaczek1926, mixed=:lucca1994) +earth_admittance = :ideal +``` + +The earth-impedance composition is retained and described by its `air`, `earth`, +and `mixed` branches. Ametani and Lucca select only mixed mutual impedance. +The reverse ordered pair uses the same reciprocal model. `:ametani2009` retains +the journal publication year. PSCAD's help incorrectly dates that article as 2005. +Layer 1 is air and layer 2 is soil. Carson/Gary apply to `(1,1)`, and Pollaczek/Wedepohl/Saad to `(2,2)`. + +The `:ideal` external potential model uses Maxwell's electrostatic image +coefficients for aerial conductors, with zero external coefficients for buried +self and mutual pairs, as well as mixed air-buried pairs. Cable insulation contributes its separate +potential coefficients. In this mathematical model, lossless admittance is purely +imaginary, while capacitance is real. + +PSCAD has no independent potential-model selector. Real PSCAD 5.1 computations +with lossless insulation and direct earth-return integration showed a small +aerial conductance. At 1 kHz, one diagonal entry had +`real(Y) = 5.4551e-13 S/m` and `imag(Y) = 5.3337e-8 S/m`, with a raw phase of +`89.999414°`. The Gary/Wedepohl computation gave `90°` and agreed with the ideal +image-potential reference. Buried and mixed mutual entries were zero in both. +A general error bound and an explanation of its internal cause require further investigation. +The adapter retains `:ideal` as the requested scientific identity, documents this +native deviation in result assumptions and preserves the complete native complex +matrices without zeroing conductance or substituting another equation. + +Conductor inner, outer and transfer impedances use the registered +`:wedepohl1973` analytical approximations. Magnetic insulation impedance uses the +registered `:ametani1980` annular expression. PSCAD's native component name is +only an XML binding and does not identify an equation. These distinctions follow +PSCAD's [matrix derivation](https://www.pscad.com/webhelp-pscad-v5.1.0-ol/EMTDC/Transmission_Lines/Deriving_System_Y_and_Z_Matrices.htm) +and [external potential matrix](https://www.pscad.com/webhelp-pscad-v5.1.0-ol/EMTDC/Transmission_Lines/Mutual_Impedance_with_Earth_Return.htm). +The coaxial backend implements `:ideal` potential coefficients. Internal +impedance `:wedepohl1973` remains unimplemented on that backend. Its PSCAD +dispatch is available. + +PSCAD dispatch maps retained equations to native settings. Gary1976 maps to PSCAD's +`DERISEMLYEN` spelling. This does not create a second mathematical registration. Carson1926 +(overhead) and Pollaczek1926 (underground) map to native direct numerical integration. +These names follow PSCAD's [documented earth-return selections](https://www.pscad.com/webhelp-pscad-v5.1.0-ol/EMTDC/Transmission_Lines/Mutual_Impedance_with_Earth_Return.htm). +They identify the requested native controls, not a guarantee that native equations, +material assumptions or results equal LCM's implementations. The exported ground +permittivity remains the supplied material value. +PSCAD's earth-impedance `:default` retains the explicit three-branch composition above. PSCAD rejects analytical +kernel selections or numerical controls it cannot execute. FEM accepts only its four constitutive +selections and rejects analytical kernel keywords at construction. It executes +resolved material contributions without a second author registration. Constitutive +selections passed to PSCAD remain subject to its documented export limits. + +PSCAD export applies the selected temperature law to the same resolved conductor +materials used by the analytical engine. It evaluates each physical dielectric +layer before radial homogenization, explicitly enables native loss-tangent +handling and retains the equivalent dielectric at its 50 Hz reference frequency. +The native loss-tangent cap is 10. +The aerial shunt setting uses the component's minimum, `1e-38 S/m`. These native +limits and the complete exported project accompany the results. Frequency-dependent +soil laws are rejected until their native parameter convention is verified. A +Julia material law is never converted into guessed Portela coefficients. + +FEM batches reuse a field solve only when effective material, mesh and execution +inputs agree. Every request retains its metadata and independent result arrays. +Saved-run checks include inputs, implementation sources, executable identity and +artifact checksums. Incomplete compatible runs resume missing jobs. UI and explicit +remeshing requests execute separately. See [FEM](fem.md) for execution details. + +`pipe_impedance=formula(:default)` uses the same selection grammar. Concentric +assemblies do not require an additional pipe term. Eccentric or multicore conducting +enclosures fail explicitly on the coaxial backend. FEM retains its supported +physical enclosure geometry. + +## Cable-material temperature dependence + +Select the constitutive law in the formulation and prescribe the operating +condition in the problem: + +```julia +selected = Formulation(temperature_dependence=formula(:default)) +problem = LineParametersProblem(system; temperature=80.0, frequencies=[50.0,1000.0], + earth_props=homogeneous(rho=100.0)) +result = compute(problem, selected) +``` + +The Materials-owned `TemperatureDependent` family evaluates electrical +resistivity. Its `:default` routes to `:linear`, which implements +``\rho(T)=\rho_0[1+\alpha(T-T_0)]`` using each material's reference calibration. +`temperature_dependence=nothing` retains reference resistivity. The same slot +is available in `CableConstantsFormulation` and `LineCableModelsFEM`. +Temperature is prescribed in this electromagnetic computation. No thermal +rating or temperature-field equation is implied. + +Conductors consume the evaluated resistivity. Insulation/semicon constitutive +relations receive an ephemeral material with evaluated resistivity before their +electromagnetic equation. The stored reference material remains unchanged. +Original radial dielectric constituents are evaluated before aggregation. + +A custom temperature law is a concrete selection, with no function-valued field: + +```julia +const TD = LineCableModels.Materials.TemperatureDependent +struct ExponentialResistivity{P,O<:FormulationOptions} <: TD.TemperatureDependentFormulation + parameters::P + options::O +end +function ExponentialResistivity(; scale=1000.0) + isfinite(scale) && scale > 0 || throw(ArgumentError("scale must be positive [K]")) + ExponentialResistivity((scale=scale,), FormulationOptions()) +end +function TD.temperature_resistivity(law::ExponentialResistivity, functor, workspace) + (; material, temperature) = functor.input + return material.rho * exp((temperature - material.T0) / law.parameters.scale) +end +LineCableModels.formulation_options( + ::LineCableModels.Expression{<:ExponentialResistivity,typeof(TD.temperature_resistivity)}) = FormulationOptions() +LineCableModels.formula_id(::ExponentialResistivity) = :exponential_resistivity +LineCableModels.description(::ExponentialResistivity; compact=false) = "Exponential resistivity" +Base.NamedTuple(law::ExponentialResistivity) = + (identifier=formula_id(law), parameters=law.parameters, options=law.options.data) +LineCableModels.formulation_options(law::ExponentialResistivity) = + law.options +selected = Formulation(temperature_dependence=ExponentialResistivity()) +``` + +The law defines its validity domain. All responses require positive real +resistivity [Ω·m], finite for conductors. The built-in linear approximation also +enforces `|T-T₀| < 150` K and a positive finite linear factor. The problem itself +validates finite temperature without imposing an unselected law. + +Scalar equation suffixes are uniform: physical inputs, `parameters`, +`options`, then `workspace`. Insulation and semicon laws return finite +admittivity [S/m], normalized to `Complex{T}` for the input scalar type `T`. +A response requiring a wider scalar type is rejected. Precision and measurement +uncertainty are never silently discarded. Soil laws return `EarthMaterial`. +Their fitted coefficients are checked at construction, before a frequency sweep. + +## Finite formulation selection + +The final formulation constructors participate in the same `Gridspace` +grammar as physical problem construction. Any line-parameter method slot or +the complete options tuple may be an explicit finite source: + +```julia +formulations = Formulation( + insulation_admittance = Grid(( + :lossy, + :default, + )), + earth_properties = Grid(( + :constant, + :longmire1975, + )), + combine = :product, +) +``` + +The result is `Gridspace{LineParametersFormulation}`. Every point is a +completely resolved scalar formulation. Before `compute` is called, each `Grid`, +symbol selector and `FormulaDefinition` has been resolved. `combine=:zip` aligns formulation fields and +broadcasts singleton fields. This local composition is separate from +`Combinatorial`, which always evaluates the Cartesian product of problem and +formulation points. + +`CableConstantsFormulation`, `ModalAnalysisFormulation`, and backend +formulation constructors follow the same rule. A deterministic `Grid` of +already completed formulations, including externally supplied formulations is also accepted by +`Combinatorial`. + +For each selected problem, the Coaxial collection dispatch validates and +lowers the physical declaration once. LineParameters flattens each design and +constructs `LocalCableData` plus geometry and index input once before creating a +separate workspace for every formulation. CableConstants performs its own +calculation sequence, also flattening each design once. Formula-dependent mutable matrices, +earth/EquivalentHomogeneous values, reduction maps, and trace buffers remain workspace-local. +The generic collection dispatch simply invokes established scalar `compute` +methods and supports external problem-formulation pairs without a +new registration layer. + +## Earth-free cable constants + +`CableConstantsProblem`, `CableConstantsFormulation`, and `CableConstants` +belong to Engine. They reuse the registered internal-impedance, +insulation-impedance, insulation-admittance, and semicon-admittance formulas, +and the same earth-free local primitive assemblers used by LineParameters. +Each computation has its own solve and reduction methods. +The default bundle is: + +```julia +CableConstantsFormulation( + internal_impedance = formula(:default), + insulation_impedance = formula(:default), + insulation_admittance = formula(:default), + semicon_admittance = formula(:default), +) +``` + +`Engine.flatten(LineCableModelsCoaxial(), design, formulation)` supplies a +frequency-independent, unreduced `CableBlueprint`. Omitting the formulation +uses the default annular model. Contiguous components sharing one radial center +form one concentric assembly. Explicit boundary shunt coefficients are completed +during flattening. Frequency-dependent constitutive evaluation and conductor +temperature corrections remain in the computation. The Engine retains +each assembly's innermost terminal, grounds every additional outward terminal, +assembles and reduces the local N-terminal series-impedance matrix, and combines +the physical dielectric layers in radial series. A one-terminal assembly uses +the declared outer dielectric boundary directly. It does not require a metallic +sheath. Earth impedance, earth admittance, EquivalentHomogeneous, Γ, position, transposition, +and bundle reduction never enter this workflow. + +`CableConstants(design; temperature=20, frequency=50)` is the convenience +entry point. CableConstants admits only the 50 Hz and 60 Hz datasheet base +frequencies. The result owns `cores`, aligned `R/L/C/G` vectors, and the +evaluation frequency. A conventional coaxial cable has one row and supports +`only(constants)`. + +## Completed-result read side + +`AbstractCoreResult` marks direct LineCableModels-owned computation results. +`CableConstants` and `LineParameters` are the current core result types. +`AbstractResultSpace{T}` marks completed finite collections of stored core +results. Its element type remains open so an external solver's concrete result +can be stored without subtyping a LineCableModels type. Result-space +constructors reject abstract element types and nested result-space envelopes. + +Core results own their scientific extraction methods. +[`observe`](@ref) reads native numerical values through function-object +selectors: + +```julia +observe(parameters, Z) +observe(parameters, L, 1, 1, Colon()) +observe(parameters, Y, angle, 1, 1, Colon()) +``` + +The public `Z`, `L`, and other quantity accessors delegate to these methods, +which define the R/X/L/G/B/C extraction formulas. + +Direct numerical access remains available: + +```julia +parameters.Z[1, 1, :] +@view parameters.Y[1, 1, :] +Z(parameters, 1, 2) +@observe parameters L[1, 2, :] +``` + +`@observe` expands the indexed expression to `observe(parameters, L, +1, 2, Colon())`. With no source argument it constructs the same plain request +tuple without reading a result: + +```julia +request = @observe R[:, :, :] +magnitude_request = @observe (Z, abs)[:, :, :] +magnitude = @observe parameters (Z, abs)[1, 2, :] +``` + +Each request tuple contains the selected quantity and its coordinates. +`quantity(Z, abs)` and `quantity(Z, angle)` identify the transformed quantities. + +[`ObservedResult`](@ref) detaches the selected scientific representation of one +completed gridpoint. `observables` lifts the same construction over collections: + +```julia +observed = ObservedResult(parameters, (R, L, G, C); length_unit=:kilo) +resistance = observe(observed, R) +quantity_tables = LineCableModels.ReportBuilder.tabulate(observed) +quantity_tables.Z.R +plot(observed; ydata=(R,)) +``` + +The 4 sections are `gridpoint`, `quantities`, `errors`, and `timings`. +Coordinates, units, cutoffs, availability reasons, and uncertainty dependencies +belong to the detached records. No raw result, lazy builder, or parent collection +is retained. Physical inputs and actual formulation descriptions are captured +when the computation completes, independently of optional tracing. + +`Commons.observation_requests` owns request normalization. Primary line results +retain one complete representation per available family. The series choices are +`R/X`, magnitude and angle of Z, or R/L. The shunt choices are G/B, magnitude +and angle of Y, or G/C. An atomic request for R alone +fails. Raw plotting and table conveniences complete the pair through that same +operation and display the requested selection. Observed-input methods only select +retained quantities. Frequency is a coordinate of each product. + +`Commons.observation_quantity` owns acquisition, unit conversion, and resolution. +`Commons.observation_groups` establishes display groups from the original +physical identity, relevant formulation controls, statistical meaning, output +coordinates, and uncertainty dependencies. Both reporting and plotting consume +that decision. All individual observations and quantity tables remain available. + +`LineCableModels.Units` owns `Unit`, `UnitExpr`, `Quantity`, `units`, +`quantity`, `native_unit`, `display_unit`, `scale_factor`, `label`, and +`symbol`. The plotting extension derives scientific axes from publication payloads. +ReportBuilder derives human-facing tables through `report`. Both consumers use +the quantities, transforms, and unit metadata supplied by the observation grammar. + +`Quantity{Q}` is a fieldless typed identity for extension methods and internal +publication payloads. Ordinary calls use scientific selector functions: + +```julia +label(R) +symbol(Z, angle) +native_unit(R, :pul) +display_unit(Z, abs, :total) +``` + +The selector methods delegate to the quantity's label and unit methods. An +external selector adds its identity and metadata at the Units API: + +```julia +function profile_response end + +LineCableModels.Units.quantity(::typeof(profile_response)) = + LineCableModels.Units.Quantity{:profile_response}() + +LineCableModels.Units.native_unit( + ::LineCableModels.Units.Quantity{:profile_response}, +) = LineCableModels.Units.units(:base, :ohm) + +LineCableModels.Units.display_unit( + ::LineCableModels.Units.Quantity{:profile_response}, +) = LineCableModels.Units.units(:milli, :ohm) + +LineCableModels.Units.label( + ::LineCableModels.Units.Quantity{:profile_response}, +) = "Profile response" + +LineCableModels.Units.symbol( + ::LineCableModels.Units.Quantity{:profile_response}, +) = "u" + +label(profile_response) +display_unit(profile_response) +``` + +`Quantity`, `Unit`, and `UnitExpr` remain qualified extension vocabulary. The +package root exports only the six metadata functions used with scientific +selectors. + +Higher-order results remain containers of owned products. `result`, +`statistics`, `samples`, and `histograms` select those products. UQ reads trial results with +`observe`, while its retained statistics, samples, and histograms implement +the same selector grammar for later publication. Monte Carlo run settings and +resolved point values are read through `root_seed`, `point_seed`, +`trial_count`, `confidence`, `cdf_tolerance`, and `sampling_distribution`. + +## Coaxial workspace and supplemental output + +`LineCableModelsCoaxial` solves concentric coaxial assemblies. A sector, +stranded conductor or otherwise nonconcentric part must be represented by the equivalent +round and concentric properties owned by DataModel before it reaches this backend. +The backend then owns frequency scans, self and mutual line parameters, earth +effects, and reduction. DataModel supplies the equivalent cable properties. +ModalAnalysis defines transformations to modal coordinates. + +`LineParametersWorkspace` is the coaxial backend's per-computation working +state. Its constructor adapts a completed physical system once. It binds equations and constructs cable and reduction indices, and allocates numerical buffers. Material +evaluation follows workspace construction. QuadGK arrays are allocated when +a selected equation requires integration. Its shunt solvers consume DataModel's ordered physical +dielectric layers directly, so the analysis is independent of the frequency +used when a lossy homogeneous export representation is requested. + +The workspace separates four owned concerns: + +- `input`: immutable numerical input derived from the problem. +- `plan`: index maps, geometry, bound earth calculations and the reduction plan, fixed + before the frequency loop. +- `buffers`: mutable storage reused while solving each frequency. +- `trace`: optional diagnostic matrices allocated before the loop. + +The ordinary result is always `LineParameters`. Requesting +`options=(trace=true,)` retains completed diagnostic arrays under +`details(parameters).data.trace`. + +## Modal transformations + +Modal decomposition is independent of the backend that produced fully coupled +phase-domain matrices. `LineCableModels.ModalAnalysis` defines its own problem, +formulation, registered formula files, and default backend: + +```julia +phase = compute(line_problem, line_formulation) +modal = compute( + ModalAnalysisProblem(phase), + ModalAnalysisFormulation( + formula(:default; options=(iteration=(convergence=1e-8,),)), + ), +) +rebuilt = transform(PhaseDomain, modal) +``` + +`LineCableModelsModal` is the default backend for this workflow. Each formula +has one selected `initialize_buffers` and `decompose!` route. It fills the +modal-to-phase `Tv` and `Ti` bases and the retained propagation roots. The +common calculation changes coordinates with `Tv \ (Z * Ti)` and +`Ti \ (Y * Tv)`. + +The modal `LineParameters` result includes `ModalDomain(operators, gamma)` as +its domain value. The stored bases preserve the resolved mode +order, scaling, and complex phase convention and make the transformation +bidirectional without rerunning the decomposition. An operator-less modal +result cannot be constructed through the admitted `LineParameters` interface. +The formula selection and numerical diagnostics live in computation details. +The domain holds numerical state. Inverse conversion uses that state without +rerunning the decomposition. + +The retained modal formula is selected by `ModalAnalysisFormulation()`. +Explicit controls use `formula(:default; options=(iteration=(convergence=1e-8,),))`. +A custom decomposition is a completed formulation implementing +`Commons.initialize_buffers(selected, T, input, plan, common)` and +`ModalAnalysis.decompose!(selected, workspace, parameters, options)`. +The latter fills `workspace.Tv`, `workspace.Ti`, and `workspace.roots`. +`ModalAnalysisFormulation` retains its +requested declaration and resolved selection for the common inspection protocol. +The default tracks eigenpairs with Levenberg–Marquardt iteration, retaining a +matched conventional eigensolution when iteration fails. Its bibliography stays +in `src/engine/modalanalysis/formulas/chrysochos2014.jl`. + +[`ComputationDetails`](@ref) is an immutable record of supplemental computation +output, parameterized by its named-tuple payload. Access its fields through `.data`. +[`computation_details`](@ref) returns the fixed-key details record supplied by the +formulation's owner. Higher-order computations dispatch on `typeof(formulation)` +when collecting these records. A formulation without a method raises `MethodError`. + +[`ParametricResult`](@ref), [`LinearErrorResult`](@ref), and +[`MonteCarloResult`](@ref) store the concrete type of the details record. Retention is +disabled by default, so `details(result) == ComputationDetails()`. The higher-order formulation +defines the retention option: + +```julia +Combinatorial(formulation; options=(retain_details=true,)) +LinearError(formulation; options=(retain_details=true,)) +MonteCarlo(formulation; trials=100, options=(retain_details=true,)) +``` + +Parametric and linear computations retain `ComputationDetails(points=records)`, with one record +per core result. Monte Carlo retains `trials`, `failures`, and +`failure_summary`, each aligned by Gridspace point. `trials` contains one inner +computation record per accepted trial. Each failure record contains the +attempt, target trial, failure stage, realized argument tuple, error type and +message, and a bounded stack summary. Statistics, samples, histograms, seeds, +and accepted-trial counts remain dedicated result fields. + +## Formulation options + +[`formulation_options`](@ref) validates values that alter the mathematical +computation represented by a formulation. Dispatch uses the formulation owner +type rather than the public construction selector: + +```julia +formulation_options(LineParametersFormulation, FormulationOptions(reduce_bundle=false)) +``` + +The default line-parameter formulation owns: + +- bundle and Kron reduction, `reduce_bundle=true` and `kron_reduction=true` by + default. +- ideal transposition, `ideal_transposition=false` by default. When selected, the + retained ``Z`` and potential-coefficient matrix ``P`` are averaged over cyclic + transposition, and ``Y`` is the inverse of the averaged ``P`` times ``j\omega``. + +The normalized `FormulationOptions` record is stored in +`LineParametersFormulation.options`. Read its payload through `.data`. +`PSCADFormulation` uses the shared physical options and currently requires +unreduced, untransposed matrices. + +Connection assignments use one-based active phase IDs. A zero assignment marks +a grounded or eliminated conductor, while repeated active IDs identify conductors +that belong to the same bundle. + +`Formulation()` constructs the default coaxial formulation. +`LineParametersFormulation` defines its formulation options, and +`LineCableModelsCoaxial` executes it. External backends are selected through +their registered Symbol or `Val` selectors. + +Modal formulas include an `iteration` section containing convergence, iteration +count, damping and the `:matched` or `:none` fallback settings. The computation action +accepts `offdiagonal_tolerance` separately. Frequency continuation belongs to one +run. Result details record the frequency indices where matched eigensolutions were +used. The stored voltage and current operators are retained for inverse transformation. + +## Computation options + +[`computation_options`](@ref) validates values belonging to one execution. +Selected equation controls belong to `FormulationOptions`, even when they set +quadrature or iteration tolerances. Backend execution options govern output, +tracing, logging and callbacks. + +Public `options=(...)` keywords accept ordinary named tuples or the appropriate +owned record. They wrap tuples once before passing them to the owner. Direct +`computation_options(Owner, options)` calls require `ComputationOptions`. Direct +`formulation_options` calls require `FormulationOptions`. The owner checks keys, +fills defaults and validates values. Constructing a wrapper alone does none of +that. Normalized controls are forwarded without reapplying normalization. + +The 3 record types preserve the exact payload type, including callback +and sampler types. Access payload fields through `.data`. Implicit conversions and tuple forwarding are unsupported. Immutability is shallow: contained +arrays are not copied or frozen. Nested numerical groups, physical parameters, +scientific products, axes and plotting attributes remain ordinary named tuples. + +`Combinatorial`, `LinearError` and `MonteCarlo` normalize their own execution +options inside their constructors, including positional construction. +The complete set of Monte Carlo controls is stored in `MonteCarlo.options` and passes through +`computation_options(MonteCarlo, options)`. Keyword shorthand remains available: + +```julia +MonteCarlo(formulation; options=(trials=100, seed=42, retain_details=true)) +MonteCarlo(formulation; trials=100, seed=42, options=(retain_details=true,)) +``` + +These two calls are equivalent. Supplying the same key both as a keyword and +inside `options` raises `ArgumentError`. Unknown keys and invalid values also +raise `ArgumentError`. + +`ParametricProblem(space, ComputationOptions(...))` stores options for the **inner computation**. +Grid, batch, combinatorial and uncertainty traversal forward that record to the +selected core solver, whose `computation_options` method validates it. The +problem cannot normalize it at construction because the solver has not yet +been selected. Traversal retention and Monte Carlo sampling controls belong to +the higher-order formulation's own `ComputationOptions` record. + +### Scan timing and progress + +Line-parameter computations accept `timing::Bool=false` independently of logging: + +```julia +execution = ComputationOptions(timing=true, verbosity=(default=0, progress=1)) +result = compute(problem, formulation; options=execution) +scan = details(result).data.timing +``` + +For a higher-order computation, keep these controls in +`ParametricProblem(problem_space, execution)`. Cable-constant computations and +modal actions retain their existing option handling. + +One measurement describes one materialized problem and formulation over the +complete requested frequency vector. The owned backend uses `Base.@timed` around +its existing workspace construction, solve, and validated result construction. +Batch-shared input construction, timing attachment, `on_result`, and subsequent +progress logging lie outside the measured expression. 3 executed formulations produce +three measurements. Shared input construction is neither duplicated nor apportioned. +Compilation that occurs before the measured expression begins is not included. + +| Backend | Fields in `details(result).data.timing` | +|---|---| +| Owned | `wall_seconds`, `bytes`, `gc_seconds`, `compile_seconds`, `recompile_seconds` | +| FEM | `wall_seconds`, `constraint_seconds`, `assembly_seconds`, `solve_seconds`, `output_seconds`, `worker_wall_seconds` | +| PSCAD | `wall_seconds`, `compile_call_seconds` | + +All durations are in seconds. `bytes` is Julia allocation volume, not retained +size or peak memory. Native `wall_seconds` covers caller-side execution and +completed result construction after shared input construction. FEM phase durations sum +the existing native column measurements. `worker_wall_seconds` sums newly +executed process durations, not elapsed scan time. PSCAD `compile_call_seconds` +measures the worker's `line.compile()` call and excludes output-readiness waiting. +Caller Julia allocation measurements do not describe native memory use in either backend. + +Fully reused, partially recovered, and within-batch reused results have empty +timing records `(;)`. A fresh complete scan retains available measurements and +uses `nothing` for unavailable optional native metrics. With `timing=false`, the +field is absent. Required native timing files and recovery checks remain active. +FEM records reuse, recovery and factorization facts in `details(result).data.fem.run`. +PSCAD execution facts remain in `details(result).data.execution`. + +Parametric and linear-error results keep measurements in their scalar values. +Monte Carlo retains one record per accepted and stored trial at +`details(result).data.timing[population_index][accepted_trial_index]`, independently +of `retain_details`, `return_samples`, and histogram retention. Failed attempts +are not successful measurements, and aggregate mean and standard-deviation values do not receive scan +timing. Supported scientific serialization preserves optional timing, including +through the Measurements extension. Records without timing remain readable. + +Progress uses ordinary structured Julia `@info` messages with `_group=:progress`. +The effective level is `get(verbosity, :progress, verbosity.default)`. Other +messages use the nearest explicitly configured module ancestor, then `default`. +Levels 0, 1, and 2 permit warnings, information, and debug messages respectively. +The caller's current logger still controls acceptance and output: verbosity does +not override a `NullLogger`, parent threshold or custom filter. Explicit FEM file +logging and native Gmsh/GetDP/PSCAD diagnostic settings retain their own behavior. + +An outer traversal suppresses child progress while preserving other options and +backend diagnostics. It logs start and successful completion and checks for +intermediate publication at existing completion points, at most every five +seconds per traversal. +ETA uses an exponential moving average with weight 0.2 on the newest completed +interval. Parametric intervals are divided by the returned batch's result count. +MC intervals between accepted trials include rejected attempts. MC resets the +sampling estimate for each population and estimates overall remaining time only +after a population completes. Estimates use the current traversal's completed +intervals. +Successful completion is reported after result construction and callbacks. + +For bounded repetition, use [`LineCableModels.benchmark`](@ref): + +```julia +measurement = LineCableModels.benchmark(; samples=3, warmup=1) do + compute(problem, formulation; + options=(timing=true, verbosity=(default=0,))) +end +result, timings = measurement +``` + +The defaults are one measured repetition and no warmup. Positive measured counts +and nonnegative warmup counts must be integers other than `Bool`. The function +returns `(; result, timings)` with the final scientific result and detached small +measurements for each repetition. Scalar records stay scalar. Ordinary batches +retain formulation order. Parametric results retain problem-index-fastest value +order. Linear-error results retain value order. MC retains population and trial order. +Missing requested timing raises an error. Empty reuse records remain empty. + +The callable defines options, seeds, callbacks, and reuse. The returned timings +are the measurements recorded by each computation. Earlier results are released +after their timing records are extracted. Read the repetition records through +the returned `timings` value. + +The coaxial backend accepts: + +```julia +( + verbosity = (default = 0,), + output_basis = :pul, + trace = false, + timing = false, + on_result = nothing, +) +``` + +`trace=true` preallocates the workspace's trace record and attaches +the retained matrices to `details(result).data.trace` after computation. + +Coaxial, FEM, and PSCAD computations accept an optional callable +`on_result(problem, index, result)`. It runs synchronously after each completed +formulation, including a reused result, before computing the next selection. +The index refers to the submitted formulation collection (`1` for a scalar +call). Manual campaigns can save each completed result before the rest of the batch finishes. The callback must not mutate its arguments. Its return +value is ignored and any exception stops execution. The default is `nothing`. + +The PSCAD backend accepts: + +```julia +( + output_stem = "case_name", + remote = remote_config, + verbosity = (default = 0, PSCAD = 2), + output_basis = :pul, + on_result = nothing, +) +``` + +`remote` must be a `PSCAD.RemoteConfig`. `output_stem` names files +created by that execution. Both values belong to that execution rather than `PSCADFormulation`. + +Construct a station from a user-selected TOML filename: + +```julia +station = PSCAD.RemoteConfig(ENV["LINECABLEMODELS_PSCAD_CONFIG"]) +result = compute(problem, Formulation(:pscad); options=(remote=station,)) +``` + +Pass the configuration filename explicitly. This example reads it from a +caller-selected environment variable. Loading the TOML file reads station +settings and resolves paths. Keep machine settings outside version control. +A sanitized template is provided in `examples/pscad/remote.example.toml`. +Relative `local_root` paths are resolved against the configuration file's directory. +`local_root` and `shared_root` expose the same files on caller and station. +`remote_root` is separate station scratch storage. Executable and station paths +are supplied by the user. + +The default `transport="ssh"` supports ordinary SSH aliases, tunnels, and network +addresses. `transport="local"` invokes PowerShell directly from a Windows caller. +For a custom wrapper, use `transport="command"` and an argument array such as +`command=["ts", "ssh", "{host}", "--direct", "--"]`. Exact `{host}` arguments are +replaced by the configured host. Encoded PowerShell arguments are appended without +shell evaluation. Caller-defined `remote_command` methods remain supported. + +Fresh calls execute PSCAD once per distinct native request. Explicit +`resume_run_directory=:latest` or a completed-run path requests verified native +reuse. `solver_identity` optionally pins the installation. Reuse checks the current +station, exported project, complete native settings, worker sources, and outputs. +Resuming requires a complete version-4 run record that passes these checks. +`work_root` defaults to the configured `local_root`. These are computation options. + +PSCAD requires deterministic inputs, including base frequency, and rejects +measurement objects even when their standard deviation is zero. It returns normal +phase-domain `LineParameters`, with both matrices permuted to the requested terminal +order. Stored native outputs retain their original ordering. + +Both option sets are ordinary `NamedTuple`s, aliased as +[`FormulationOptions`](@ref) and [`ComputationOptions`](@ref). Callers can +compose them with `merge`. Each owner rejects unknown keys and returns a +fixed-key normalized tuple. There is no general fallback and no conversion +from dictionaries, pairs, or `nothing`. + +`MonteCarlo` defines a separate outer computation-option tuple. Its normalized +keys are `retain_details`, `on_error`, and `max_failures`. `on_error=:fail` is +the default and rethrows every exception. `on_error=:resample` requires +`retain_details=true` and rejects only `DomainError` realizations until the +requested accepted-trial count is reached or `max_failures` is exhausted. +Other exception types always propagate immediately. + +## Extending the engine + +An external package may own a backend identity and a separate formulation +type. The backend's `compute` method normalizes execution options before doing +work: + +```julia +import LineCableModels: + AbstractFormulation, + AbstractCoreResult, + ComputationOptions, + FormulationOptions, + ComputationDetails, + computation_details, + computation_options, + compute, + formulation_options + +struct ExternalEngine end + +struct ExternalFormulation{O <: FormulationOptions} <: AbstractFormulation + options::O +end + +function formulation_options( + ::Type{ExternalFormulation}, + options::FormulationOptions, +)::FormulationOptions + isempty(options.data) || throw(ArgumentError("unsupported formulation option")) + return FormulationOptions() +end + +function ExternalFormulation(; + options::Union{NamedTuple,FormulationOptions} = FormulationOptions(), +) + options = options isa NamedTuple ? FormulationOptions(options) : options + return ExternalFormulation(formulation_options(ExternalFormulation, options)) +end + +function computation_options( + ::Type{ExternalEngine}, + options::ComputationOptions, +)::ComputationOptions + unknown = filter(key -> key != :tolerance, keys(options.data)) + isempty(unknown) || throw(ArgumentError("unsupported computation option")) + normalized = merge((tolerance = 1.0e-8,), options.data) + normalized.tolerance > 0 || throw(ArgumentError("tolerance must be positive")) + return ComputationOptions(tolerance = Float64(normalized.tolerance)) +end + +function compute( + ::ExternalEngine, + problem, + formulation::ExternalFormulation; + options::Union{NamedTuple,ComputationOptions} = ComputationOptions(), +) + options = options isa NamedTuple ? ComputationOptions(options) : options + execution = computation_options(ExternalEngine, options) + # Use `problem`, `formulation`, and `execution` here. +end +``` + +External implementations extend these two normalization functions for their +formulation and backend types. An omitted method raises `MethodError`. + +The same backend may expose supplemental output without changing the generic +higher-order result types: + +```julia +import LineCableModels: ComputationDetails, computation_details + +struct ExternalResult <: AbstractCoreResult + parameters + diagnostics::NamedTuple + raw::Dict{String,Any} +end + +function computation_details( + ::Type{<:ExternalFormulation}, + output::ExternalResult, +)::ComputationDetails + return ComputationDetails(; + diagnostics=output.diagnostics, + raw=output.raw, + ) +end +``` + +An external backend that cannot modify or wrap its solver's concrete return +type may store that type directly in a result space. `AbstractCoreResult` marks +owned direct results. It is not an admission requirement for external result +payloads. + +The outer keys and their types are fixed for `ExternalFormulation`. Dynamic +vendor channels remain inside the explicit `raw` leaf. ParametricBuilder and +UQ collect these records when `retain_details=true`, preserving the fields +provided by the backend. + +## Reports and XLSX output + +[`report`](@ref) executes `tabulate`, `illustrate`, `encode`, and `write`, then +constructs a `ReportArtifact`. Tabulation is implemented by the report definition. The +other stages have optional defaults. In-memory reports have `output === nothing`. +Raw conveniences construct observations before entering this sequence. + +For formulation comparisons, ReportBuilder retains unformatted data in +`artifact.observed` and exposes `summary`, `maxima`, `formulations`, `computations`, +`comparisons` and `terms` through `artifact.tables`: + +```julia +using LineCableModels.ReportBuilder: BenchmarkTableDefinition + +results = compute(problem, Formulation(earth_properties=Grid((:constant, :longmire1975)))) +reference = compute(problem, Formulation()) +artifact = report(BenchmarkTableDefinition(), (; reference, result=results)) +artifact.tables.summary +artifact.tables.terms +# After loading a Makie backend: +plot(artifact, (Z, Y)) +``` + +The default request compares all Z/Y/R/L/G/C matrix terms in five bands and +returns tables in memory. Completed comparisons are joined to their result +identities before observation construction. `artifact.reference` is a separate +atomic observation. To change numerical settings, compute the comparison again. +Arithmetic dimensions, coordinates, and units must be usable. Scientific +comparability remains the caller's responsibility. One reference is overlaid +once alongside all selected study points. + +[`XLSXReportDefinition`](@ref) defines the human-facing line-parameter workbook: + +```julia +using XLSX + +artifact = report( + XLSXReportDefinition(file_name="line_parameters.xlsx"), + parameters, +) +artifact.output +``` + +ReportBuilder creates one table and XLSX file per gridpoint and quantity. A full +matrix table has one frequency row and all n² coefficient columns, including both +off-diagonals. Each workbook contains numeric `values` and `std` sheets plus +metadata for coordinates, units, applied cutoffs, and missing-value reasons. +Loading XLSX activates the writer for these encoded tables. `artifact.output` is +the list of written paths. `export_data(:xlsx, parameters; ...)` delegates to the +same workflow. Relative paths resolve from the caller's working directory. + +For persistence of uncertainty dependencies and scalar precision, use +`save(artifact, "observed.jls")` or JSON and `import_data(:observed, path)`. +The restored observations can be reported or plotted without original sources. + + + +Scalar computation selections are retained in `details(result).data.formulations`. +Its `requested` and `methods` fields hold complete requested and resolved records, +including physical parameters and numerical controls. Formula identifiers are available +as `record.requested.earth_admittance.identifier` (or through the corresponding +`air`, `earth`, `mixed` leaf). Resolved identities are under `methods`. A leaf's +`equivalent_earth` field contains its reduction choice and order. Realized local +shunt outcomes, including explicit fallback, remain in `details(result).data.shunt_model`. +Optional `trace` contains detached evaluation data. Reports use the recorded +formulation selections and voltage references to describe each result. diff --git a/docs/src/extensions.md b/docs/src/extensions.md new file mode 100644 index 000000000..1cddd7d9a --- /dev/null +++ b/docs/src/extensions.md @@ -0,0 +1,336 @@ +# Extension API + +Extension code adds methods to the owner that defines their meaning. The public +computation entry points are `compute`, `observe`, `observables`, +`report`, `plot`, and `preview`. + +`PlotBuilder` owns only optional plotting entry points and the live `UIPlot` +handle. Scientific owners expose observations, quantities, geometry, and +physical property ranges. The Makie extension owns request normalization, +palettes, legend grouping, layout helpers, widgets, and native rendering. + +## Formula selection and descriptions + +`formula_id` identifies a scientific selection. `description` supplies its +human-readable text. Registered formula types implement +`description(::Type{<:OwnedFormula{:ID}}; compact=false)`, and their instances +delegate with the same keyword. The default is the detailed scientific +description. `compact=true` is the short name used in legends. Override this +method to customize a name, not `formula_id`. + +A family declares independently selectable children through +`pairs(Family.Formula; quantity=nothing)`, returning ordered `slot => family` +pairs, or an empty mapping for a scalar leaf. Constructors, retained-record +readers and formulation projections use that same owner declaration. +Internal impedance admits `inner`, `outer`, `transfer`. The external earth +families admit `air`, `earth`, `mixed`. Explicit composites retain every branch, +including default branches, in quantity-relevant legends. + +Formulation owners expose ordered `(owner, route_tuple) => selection` pairs +through `pairs(source; quantity)` and `pairs(owner, retained; quantity)`. +Each selected leaf is paired with its declaration controls. +`formulation_options(selection)` reads the live selection's `FormulationOptions`. +Consumers obtain identity from `formula_id`, structure and ordering from `pairs`, +and display text from `description`. The contextual +`description(owner, selection; compact)` method describes the backend's equations, +including PSCAD's native defaults. + +## User-owned formulas + +A formula is a collection of expressions. A user-owned formula is a concrete type below its +family's formulation supertype. It stores its model `parameters` and its normalized +`options`, a `FormulationOptions` record, and the user selects it directly in its physical +slot, such as `Formulation(earth_properties=MySoil(...))`. Its methods dispatch on its own +type: the family operation for each of its expressions, `formulation_options` for the +options of each expression, and the metadata methods `formula_id`, `description` and +`NamedTuple`. A formula that checks its input or shares values adds a `Functor` method, and +one that needs arrays adds an `initialize_buffers` method. IDs are for inspection only. +`:default` resolves to an explicit implementation before physical validation or computation. + +### Expressions + +`Commons.Expression(formula, operation, selectors...)` names one expression of a formula. +Its `Val` selectors identify the part, such as the kind and the source and target layers of +an earth interaction, or the inner, outer or transfer kind of an internal impedance. Calling the expression with a Functor and a workspace evaluates +`operation(formula, selectors..., functor, workspace)`. The formula object comes first, and +the selectors follow it. + +A formula declares the defaults of each expression with +`formulation_options(::Expression{<:MyFormula, typeof(operation), ...})`. The family projects +the options supplied to the formula onto the expressions that consume them. Before the +frequency loop, `validate(expression)` checks that the operation has a method for the +formula and its selectors. + +### The Functor + +`Commons.Functor(formula, input, state)` is a formula at one evaluation point. `input` +stores the values of that point, such as the material and the frequency, with the options +of the expression evaluated there. `state` stores plain values that the expressions of the +formula share at that point, such as Schelkunoff's scaled Bessel values or Unified's +per-frequency system. Arrays come from `workspace.buffers`, never from the state. Every +family evaluates its expressions through the same path: + +```julia +functor = Functor(formula, input; workspace) +value = Expression(formula, operation, selectors...)(functor, workspace) +# This evaluates operation(formula, selectors..., functor, workspace). +``` + +Without a method of its own, a formula gets an empty state. A family or a formula that +checks its input, shares values or reads buffers adds a method +`Functor(formula::MyFormula, input::NamedTuple; workspace)` that returns +`Functor(formula, input, state)`. `Functor(functor, extension)` keeps the formula and the +state and extends the input, such as with one conductor pair of an earth calculation. + +| Family | Formulation supertype | Operation | Functor input | +|---|---|---|---| +| Internal impedance | `Engine.InternalImpedanceFormulation` | `InternalImpedance.internal_impedance(formula, Val(kind), functor, workspace)`, with `kind` one of `:inner`, `:outer`, `:transfer` | `r_in`, `r_ex`, `rho`, `mu_r`, `jω` and the `options` of that kind | +| Insulation impedance | `Engine.InsulationImpedanceFormulation` | `InsulationImpedance.insulation_impedance(formula, functor, workspace)` | `r_in`, `r_ex`, `mu_r`, `jω`, `options` | +| Insulation and semicon admittivity | `Engine.InsulationAdmittanceFormulation`, `Engine.SemiconAdmittanceFormulation` | `insulation_material` or `semicon_material(formula, functor, workspace)` | `material`, `frequency`, `temperature`, `options` | +| Earth impedance and potential | `Engine.EarthImpedanceFormulation`, `Engine.EarthAdmittanceFormulation` | `earth_impedance` or `earth_potential_coefficient(formula, Val(kind), Val(source), Val(target), functor, workspace)` | `pair`, `physical`, the pair's columns of `rho`, `epsilon` and `mu`, `thickness`, `jω`, `frequency`, `media`, `options` and the `destinations` | +| Unified earth return | `EarthImpedance.Formula{:unified}`, `EarthAdmittance.Formula{:unified}` | `EarthAdmittance.source_coefficients(formula, Val(kind), Val(source), Val(target), functor, workspace)`, which returns `(axial, potential)` | as for the earth families, with Unified's per-frequency state | +| Soil frequency dependence | `Earth.FrequencyDependent.FrequencyDependentFormulation` | `earth_material(formula, functor, workspace)` | `material`, `frequency`, `options` | +| Temperature dependence | `Materials.TemperatureDependent.TemperatureDependentFormulation` | `temperature_resistivity(formula, functor, workspace)` | `material`, `temperature`, `options` | +| Equivalent earth | `Earth.EquivalentHomogeneous.AbstractRule` | `equivalent_material(formula, Val(kind), Val(source), Val(target), functor, workspace)` | `rho`, `eps_r`, `mu_r`, `model`, the physical `pair`, `frequency`, `options` | +| Modal decomposition | `AbstractFormulation`, selected by `ModalAnalysisFormulation` | `Commons.initialize_buffers(formula, T, input, plan, common)` and `ModalAnalysis.decompose!(formula, workspace, parameters, options)` | none | +| Local shunt geometry | `Engine.ShuntModelFormulation` | `Engine.internal_shunt_response(formula, design, geometry, T, material_selections, solutions, design_index)`, during blueprint construction | none | +| Pipe applicability | `Engine.PipeImpedanceFormulation` | `validate(design, formula, backend)` admits the topology of `design` or throws. No analytical pipe equation is supplied. | none | + +An earth expression returns one coefficient per destination of its calculation. It returns a +number for one destination and a tuple, ordered as the destinations, for several. + +### Layered earth + +The source and target layers of an earth expression select its method. Layer 1 is air, +and layer 2 is the first earth layer. Methods written for `Val{1}` and `Val{2}` declare a +formula for air and one homogeneous earth. On an earth model with more layers, such a +formula consumes its explicit `equivalent_earth` reduction, or the `:default` reduction +without one. A layer left as a generic `Val{S}` declares the expression for every layer: + +```julia +function earth_impedance(formula::MyLayeredImpedance, ::Val{:self}, ::Val{S}, ::Val{S}, + functor, workspace) where {S} + # functor.input.thickness holds the thicknesses of the layered earth. +end +``` + +A formula that admits a layer above 2 consumes the whole layered earth and its interfaces. +Before the frequency loop, the plan checks that the formula has an expression for every +conductor pair on the earth it consumes. The `ArgumentError` names the number of earth +layers and the highest layer that the formula admits. + +`parameters` are model data and `options` are formulation-owned physical choices +and numerical controls. Custom constructors validate and normalize their own `FormulationOptions`. +Execution controls use `ComputationOptions`. Completed supplemental output uses +`ComputationDetails`. Read their payloads explicitly through `.data`. +Indexed families declare numerical defaults for the actual selected type and +case with `formulation_options(::Expression{<:MyType,typeof(operation),...})`. +Earth field equations consume evaluated material properties and define their +own wave numbers and field approximations. Material-law families implement +`constitutive` with a valid material argument. +Unified's formulation options accept a prescribed longitudinal coefficient Γ +as a scalar or a frequency-aligned vector. Its precision participates in +allocation of the computation's numerical storage. The positive-time convention +and explicit convention conversions are documented with the Unified equations. + +Coaxial equations and material laws receive the defining computation workspace, +or `nothing` for a standalone evaluation that does not require it. Numerical arrays are in +`workspace.buffers`. The workspace input and plan remain the authority for geometry and +material mappings. + +The workspace provides the buffers, and formulas use them. `initialize_buffers` is the +one action that builds buffers. Each equation builds its own in its method, usually as a +named tuple of preallocated arrays, without a storage type of its own. An allocation +method is unnecessary for an algebraic equation. For an equation that needs buffers, extend +`Commons.initialize_buffers(selected, T, input, plan, buffers)` to return +the record extended with owned arrays only. Only selections reached by the +required indexed calls participate in initialization, before evaluating materials +or equations. Unused recipe branches remain unallocated. +The default uses only existing storage. Existing arrays may not be replaced. +Numerical formulas provision the common quadrature storage through +`Commons.initialize_buffers(SpectralIntegral, Val(:quad), T, input, plan, buffers)`. An +integration option is not a capability declaration. Cable constants uses this +same buffer-initialization method with its local numerical input and an empty plan. + +Each formula defines its complete integrand, transformations, Jacobians, branch +choices and physical subdivision hints. `SpectralIntegral` selects a Sommerfeld-type +integral over the spatial Fourier variable and contains only that callable. +`integrate(integral, Val(:quad), controls, buffers)` passes it and the supplied numeric +subdivision points to QuadGK. Physical expressions and subdivision choices belong to each formula. + +`ShuntModel` owns conductor and dielectric geometry extraction, numerical coefficients and the requested fallback +model. Its `blueprint_dependencies` methods identify the actual local selections +that affect those coefficients. Engine uses that dependency record for reuse +within one blueprint construction. Completed coefficient blocks are retained +after construction. The charge-system factorization and workspace are reused +only during construction. + +Conductor pairs with the same inputs share one computed value when their media agree. +By default the pairs' destination indices take part, so distinct pairs are computed +separately. A formula whose arithmetic does not read the indices declares the inputs it +reads with `same_physical_state(formula, a::EarthPair, b::EarthPair, geometry)`. A formula +that computes the whole system at once, such as Unified, adds an `Engine.EarthPlan` +constructor method on its own type and an `Engine.earth!(formula, functor, workspace)` +method that converts the coefficients of its parts into the physical matrices. Each +selection uses the Engine frequency sequence and its computation workspace. + +An internal-impedance formula builds the values that its inner, outer and transfer impedances +share in its `Functor` method, once per conductor and frequency. Shunt geometry construction runs once before the +frequency loop and returns frequency-independent blueprint blocks and diagnostics. A +consuming earth formula explicitly admits a custom equivalent-earth rule using `validate`. + +Results use result-type and unit checks for each formula family, including custom types. +Impedances are in Ω/m, material admittivities in S/m, earth potential coefficients +in m/F, and temperature-law resistivities in Ω·m. Scalar material laws return +`EarthMaterial` or a finite scalar as appropriate. Modal equations write +`workspace.Tv`, `workspace.Ti`, and `workspace.roots`. The owner constructs +`ModalOperators` and intrinsic coefficients after decomposition. Supply +`formula_id`, `description`, `NamedTuple` and +`formulation_options` methods for metadata. Serialized identities and data do +not reconstruct executable methods. See the [temperature-law example](engine.md#Cable-material-temperature-dependence). + +### A custom modal formula + +The following complete one-mode example implements a custom modal formula. Its +methods take the formula object first, `Formula{:diagonal_example}`. The modal workspace supplies common slices, coordinate conversion buffers, the +admittance-impedance product, eigenpair history and voltage-vector buffers. +`initialize_buffers` extends that record only for additional numerical work. +It assumes a completed one-mode phase scan named `phase`, with nonzero +diagonal coefficients and known source length. The example is algebraic. It +does not replace a broadband modal model. + +```julia +using LineCableModels +import LineCableModels.Engine: description +import LineCableModels.Commons: formulation_options, FormulationOptions, initialize_buffers +import LineCableModels.ModalAnalysis: decompose!, Formula +import LineCableModels: Expression + +description(::Type{<:Formula{:diagonal_example}}; compact=false) = + compact ? "diagonal example" : "one-mode diagonal example" +formulation_options(::Expression{<:Formula{:diagonal_example},typeof(decompose!)}) = + FormulationOptions() + +function initialize_buffers(::Formula{:diagonal_example}, ::Type{T}, input, + plan, common) where {T<:Complex} + plan.n == 1 || throw(DimensionMismatch("diagonal example requires one mode")) + return merge(common, (diagonal_product=Vector{T}(undef,plan.nf),)) +end + +function decompose!(::Formula{:diagonal_example}, workspace, + parameters::NamedTuple, options::FormulationOptions) + product=workspace.buffers.diagonal_product + for k in eachindex(product) + z=workspace.input.Z[1,1,k]/workspace.input.root_scale + y=workspace.input.Y[1,1,k]/workspace.input.root_scale + product[k]=z*y + root=sqrt(product[k]) + (real(root)<0 || (iszero(real(root)) && imag(root)<0)) && (root=-root) + workspace.roots[1,k]=root*workspace.input.root_scale + workspace.Tv[1,1,k]=one(root) + workspace.Ti[1,1,k]=one(root) + workspace.diagnostics.eigen_residual[1,k]=zero(real(root)) + workspace.diagnostics.iterations[1,k]=0 + workspace.diagnostics.converged[1,k]=true + end + return workspace +end + +selected=ModalAnalysisFormulation(Formula(:diagonal_example)) +modal=compute(ModalAnalysisProblem(phase),selected) +segment=PropagationParameters(modal) +size(gamma(modal)) == (1,length(frequencies(phase))) +size(H(segment)) == size(gamma(modal)) +``` + +Only the selected formula's `initialize_buffers` method runs. The common +workspace supplies per-frequency normalization and coordinate buffers. The +formula owns `diagonal_product`. It writes phase-row by mode-column bases +and mode-by-frequency roots. The owner checks structural shape and finite arithmetic before computing intrinsic coefficients. It then copies the returned arrays and records diagnostics. Numerical targets are reported as warnings and facts, +without becoming result-admission rules. + +## Input validation + +A materialized input defines one direct `validate(::OwnedType)` method. The method +returns its argument unchanged or throws a native Julia exception identifying +the rejected field and value: + +```julia +import LineCableModels: validate + +struct Annulus{T <: Real} + r_in::T + r_ex::T + + function Annulus{T}(r_in::T, r_ex::T) where {T <: Real} + return validate(new{T}(r_in, r_ex)) + end +end + +function validate(value::Annulus) + value.r_in >= zero(value.r_in) || throw(DomainError( + value.r_in, + "Annulus.r_in must be nonnegative" + )) + value.r_ex > value.r_in || throw(DomainError( + value.r_ex, + "Annulus.r_ex must be greater than r_in" + )) + return value +end +``` + +Constructors normalize their admitted grammar. The owning `validate` method +checks the completed value directly and returns it unchanged. [`validate`](@ref) +documents the rules that every method follows. + +## Commons, observations, and units + +```@autodocs +Modules = [ + LineCableModels.Commons, + LineCableModels.Units, +] +Order = [:module, :constant, :type, :function, :macro] +Filter = developer_reference_entry +Public = true +Private = false +``` + +## Data model and computations + +```@autodocs +Modules = [ + LineCableModels.DataModel, + LineCableModels.Engine, + LineCableModels.ParametricBuilder, + LineCableModels.ImportExport, +] +Order = [:module, :constant, :type, :function, :macro] +Filter = developer_reference_entry +Public = true +Private = false +``` + +## Report definitions + +```@autodocs +Modules = [LineCableModels.ReportBuilder] +Order = [:module, :constant, :type, :function, :macro] +Filter = developer_reference_entry +Public = true +Private = false +``` + +## Plotting functions + +```@docs +LineCableModels.PlotBuilder +``` + +## Index + +```@index +Pages = ["extensions.md"] +Order = [:constant, :type, :function, :macro] +``` diff --git a/docs/src/fem.md b/docs/src/fem.md new file mode 100644 index 000000000..199421b98 --- /dev/null +++ b/docs/src/fem.md @@ -0,0 +1,459 @@ +# Gmsh/GetDP finite-element backend + +[`LineCableModelsFEM`](@ref) is the Julia-native finite-element +backend for `LineParametersProblem`. Gmsh is a weak dependency: the public +formulation and option normalizers are always available, while the `compute` method +is activated by loading Gmsh. + +```julia +using LineCableModels +using Gmsh + +fem = Formulation( + :fem; + insulation_admittance = formula(:default), + semicon_admittance = formula(:default), + earth_properties = formula(:default), + temperature_dependence = formula(:default), + options = ( + physics = :quasi_tem, + reduce_bundle = true, + kron_reduction = true, + ideal_transposition = false, + ), +) + +parameters = compute(problem, fem; + options = ( + mesh_mode = :reuse, + gmsh_verbosity = 2, + getdp_verbosity = 2, + frequency_workers = 2, + solver_threads = 1, + ), +) +``` + +All execution controls pass through `compute(...; options=(...))` and are +validated by `computation_options(LineCableModelsFEM, ...)`. This includes +meshing, workers, native verbosity, Julia milestones, logs, and resume: + +```julia +parameters = compute( + problem, + fem; + options = ( + verbosity = (default = 1,), + log_file = "fem-julia.log", + ), +) +``` + +GetDP remains an external process, and the bundled executable runs on +the supported artifact platforms. The first FEM computation there downloads +the package's lazy, hash-verified GetDP 3.5.0 complex-PETSc artifact. Loading +LineCableModels or Gmsh alone does not download it. No Python runtime or +`GetDP.jl` problem generator is used. + +The adapter writes one run-local `input/model_data.pro` containing resolved +region tags, terminal names, material coefficients and domain dimensions. +Maintained GetDP files own material-domain bindings, the equations and the +parameterized field-map operation. Material conductivity is the effective +real part of the selected complex admittivity: dielectric losses are already +included, so GetDP does not add another loss-tangent contribution. + +Each solver job selects its material coefficients by frequency index. When +`plot_field_maps=true`, basis-specific output operations write the nine common field +quantities with frequency and source-specific filenames and labels. The maintained GetDP +files are captured with each run so later edits cannot change an active scan. + +Pass execution controls through `compute(...; options=(...))` and select +`physics` through formulation options. Benchmark calls use `reference_options` +for FEM execution controls. Supplemental run metadata is a named tuple under +`details(result).data.fem.run`. + +## Physics selection + +Set formulation `options=(physics=:quasi_tem,)` for the default independent magnetic and +electric blocks, or `(physics=:quasi_fw,)` for the coupled first-order Maxwell +model. Strings `"quasi-tem"` and `"quasi-fw"` and their `Symbol` values are also +accepted. Julia requires `Symbol("quasi-fw")` for a hyphenated symbol. +`:quasi-fw` is parsed as subtraction. + +```julia +fem = Formulation(:fem; options=(physics=:quasi_fw,)) +parameters = compute(problem, fem) +``` + +`model.pro` exposes the ONELAB number `LineCableModels/FEM/physics`, with +choices `0 = quasi-tem` and `1 = quasi-fw`. It includes `quasi-tem.pro` or +`quasi-full.pro` to match that selection. Headless jobs set the same constant with +`-setnumber Physics 0` or `-setnumber Physics 1`. Both use the resolution +`LineCableModelsFEMScan`. The Julia-managed GUI displays this choice read-only, +since the numerical inputs of a run are fixed before meshing. Choose physics +in formulation `options` for a new computation. Physics is saved with the formulation +inputs and column checkpoints. A run cannot resume under different physics. + +The quasi-full option retains ``A_t/\Gamma`` and ``\phi/\Gamma`` in a first-order +``\Gamma\to0`` reduction of the vector-potential and continuity equations. +One axial-current excitation supplies both responses. Its voltage extraction +includes ``j\omega\int(A_t/\Gamma)\cdot d\ell`` along a physical vertical path +from earth infinity to each terminal. The backend constructs these paths from +the first-order triangular mesh, choosing the lowest node of each terminal +contour (lowest x breaks ties). Transverse fields are zero inside equipotential +metal, so paths can cross shields or other terminals. The infinite-shell part +uses the pulled-back edge field and 128 straight segments. Integration within +each crossed triangle is exact for its lowest-order edge element. This is a +voltage-path convention without assuming path independence in an inductive field. + +The full equations, units, boundary conditions, gauge and extraction are +documented in [`LineCableModelsFEM`](@ref), with the potential-equation reference +of [Ciuprina2024](@cite). This backend's 2D longitudinal reduction is distinct +from that paper's 3D ECE implementation. It retains displacement and does not +solve a finite-``\Gamma`` eigenproblem. Both public physics options retain the +specified finite metal conductivity in the axial problem. + +Quasi-full also retains the scalar-only `Pscalar.tsv` per column, +which is gauge dependent and must not be inverted for Y. With field maps +enabled it also writes `bt_mesh`, `v_local` and `hz_scaled`, for twelve maps +per excitation. Its `e`, `em` and `jm` maps represent ``E_t/\Gamma`` [V], its +magnitude [V], and ``|J_t/\Gamma|`` [A/m], respectively. + +## Material laws + +FEM selects insulation and semicon admittivity, soil frequency dependence, and +cable-material temperature dependence. Analytical `internal_impedance`, +`insulation_impedance`, `earth_impedance`, `earth_admittance`, and +`pipe_impedance` keywords are rejected, including explicit `:default` values. +Supported enclosing geometry is represented directly in the field domain. + +`temperature_dependence=formula(:default)` evaluates each cable material's +resistivity as ``\rho(T)=\rho_0[1+\alpha(T-T_0)]``. `T` comes from +`problem.temperature`. Reference resistivity, `T0`, and `alpha` come from the +material. Select `nothing` to retain reference resistivity. This law is shared +with analytical computations, cable constants, and PSCAD export. + +The default law requires a positive finite correction factor and +``|T-T_0|<150`` K. These are limits of this approximation, independent of thermal +rating. Custom laws own their applicability and use the usual contribution-hook +signature. FEM author registration is unnecessary. Passive materials can retain +infinite resistivity. Dielectric constituents are evaluated before radial +aggregation, and polarization loss is not corrected a second time. + +`earth_properties` calls the same soil constitutive law as the analytical engine +at each frequency. Evaluated resistivity, permittivity, and permeability feed +GetDP's coefficients for the soil and transformed exterior, as well as the skin-depth mesh rule. +`:default` and `nothing` retain static soil. Air uses its explicitly declared +static permittivity and permeability, independently of the soil law. The current +FEM geometry requires one horizontal semi-infinite soil. A non-finite conductive +skin depth is unsupported. Equivalent homogeneous-earth reductions are rejected. +An ordinary `EarthModel` supplied after an external reduction includes no history +from which FEM could detect that prior approximation. + +Saved FEM formulation details contain the four consumed `selections`, their +parameters and numerical options, alongside requested and resolved records. +Custom selections retain their identities and data. Saved records never +reconstruct executable methods. +The selected propagation approximation remains recorded separately. + +## Field equations and matrix extraction + +The default `:quasi_tem` physics evaluates the series and shunt problems at ``\Gamma=0``. It retains +diffusion and displacement in the surrounding media, with phasors proportional +to ``e^{j\omega t}`` and complex admittivity ``\kappa=\sigma+j\omega\epsilon``. +2 independent blocks share one assembled GetDP system and factorization. + +The magnetic block solves for the axial vector potential ``A_z`` and one axial +electric unknown ``u_i`` per terminal. In each material it solves + +```math +-\nabla\cdot(\mu^{-1}\nabla A_z)+\kappa(j\omega A_z+u_i)=0, +\qquad +I_i=-\int_{\Omega_i}\kappa(j\omega A_z+u_i)\,dS. +``` + +Here ``u_i`` is supported on its conductor region. Exciting terminal ``s`` with +1 A and setting the axial current on the others to zero gives ``Z_{is}=-u_i/I_s``. +Metal conductivity and its internal field remain part of this series problem. + +The electric block solves the scalar electrodynamic problem in air, soil and +passive cable materials, excluding conductor interiors: + +```math +\nabla\cdot(\kappa\nabla v)+\kappa k^2 v=0, +\qquad k^2=-j\omega\mu\kappa. +``` + +Each terminal has one equipotential degree of freedom ``V_i``. Prescribing +1 A/m of outward transverse terminal current at terminal ``s``, and zero at the +others, gives the column ``P_{is}=V_i/(1\ \mathrm{A/m})``. GetDP's associated +quantity has the opposite sign, so this drive is imposed as ``Q_s=-1``. +Here ``Q`` is a current per unit length. Its units differ from electrostatic charge. +``P`` has units Ω m and ``Y=P^{-1}`` has units S/m. + +Equivalently, prescribe a unit voltage on one source terminal and ground the others. Repeat for each source terminal to extract ``Y_{is}=-Q_i/(1\ \mathrm{V})``. +For any other set of voltage excitations, terminal currents satisfy ``J=YV``. Extracting +``Y`` requires the complete voltage matrix, not just a source-voltage rescaling. + +The magnetic and electric blocks are separate unit excitations. Electric +potentials are obtained directly from the scalar operator, without dividing a +magnetically driven potential by a small ``\Gamma``. This also keeps the shunt +calculation independent of metal-interior discretization. Bare conductors have +no passive coating in their electric domain. Finite-conductivity metal remains +in their magnetic domain. + +The earlier coupled `A_z/u/phi` model retained only the axial vector potential +and used continuity to recover `Phi/Gamma`. At material interfaces that +reduction omits a transverse Ampère balance at the same order in Γ as the +electric response being extracted. With +``\mathbf r=\kappa\nabla_t(\phi/\Gamma)-\mu^{-1}\nabla_t A_z``, continuity +enforces ``\nabla_t\cdot\mathbf r=0``, whereas transverse Ampère requires +``\mathbf r=0``. A material interface makes those conditions inequivalent. +Reducing Γ does not remove the error after normalization by Γ. + +## Execution model + +One call to `compute` builds one complete two-dimensional Gmsh mesh for each +distinct physical frequency. The finite air-earth radius is +``R=\max(R_{\mathrm{layout}},m\delta)`` with the skin depth of conductive soil +``\delta=\sqrt{\rho/(\pi f\mu)}``. The computation option +`domain_skin_depths=2.0` sets ``m``. The transformed exterior occupies the annulus +from ``R`` to ``1.25R``. To compare this domain with a smaller one, use +`compute(problem, fem; options=(domain_skin_depths=1.5,))`. +The 1.5–2 range is an empirical operating range for the basic buried-wire checks. Accuracy outside these checks requires separate evaluation. Radius changes preserve the local mesh-size +targets and enter mesh-cache and resume identity. The final, highest-frequency mesh is the +displayed mesh. Each frequency-specific mesh is reused by all of that +frequency's terminal excitations. Cable topology is constructed once: successive +meshes retain its vertices and geometric faces while updating the exterior circles and +mesh-size fields. Only one native geometry and mesh is retained in memory. After +generating all meshes, Julia launches +up to `frequency_workers` standalone GetDP processes concurrently. Each process +handles one frequency: it assembles and factors the system for its first +requested terminal, then updates the right-hand side and reuses those factors +for the remaining terminals. Frequencies with different meshes have separate +systems and factorizations. Local mesh sizes also resolve the attenuation and +phase scales of the evaluated air and soil properties. + +The default is two frequency workers with one BLAS/OpenMP thread per solver. +These are independent OS processes. They do not require multiple Julia threads. +Set `frequency_workers=1` for serial frequency execution. Increase the worker +count only within available memory: every active frequency requires its own sparse +factorization. `solver_threads` sets each child's BLAS/OpenMP environment without +changing Julia's environment. GetDP must support `GenerateRHSGroup` and +`SolveAgain`. The package-owned artifact is GetDP 3.5.0 with PETSc complex +arithmetic. + +Each attempt has a separate working directory, solver prefix, raw columns, +maps and log. Inputs are explicit command arguments and data files. The solvers run independently of ONELAB. One Julia coordinator manages Gmsh and publishes UI progress. It validates columns before writing checksummed checkpoints. Backend calls in the same +Julia process serialize access to the shared Gmsh session. A filesystem lock +prevents two coordinators from writing the same run. + +Once every column is validated, Julia assembles `raw/Z.tsv` and `raw/P.tsv` in +frequency and terminal order and publishes the scan completion marker. Validation +checks headers, row counts, identities, frequencies and finite values. + +The primitive matrices have dimensions +`(nterminal, nterminal, nfrequency)`. The extracted coefficient is +``P_{\mathrm{FEM}} = Y^{-1}`` in Ω·m. The backend passes the charge-based +coefficient ``p = j\omega P_{\mathrm{FEM}}`` in m/F to the reduction shared with the +analytical engine. It applies terminal ordering, bundle merging and Kron reduction +to ``Z`` and ``p``. With `ideal_transposition=true` (the default is `false`), it +averages the retained ``Z`` and ``p`` over cyclic transposition. It then obtains +``Y = j\omega p^{-1}`` by a condition-checked direct solve, with the residual and +condition number of each frequency in the result details. + +The returned value is the package-native `LineParameters` in `PhaseDomain`, +with ``Z`` in Ω/m, ``Y`` in S/m, and the exact input frequency vector in Hz. +Pass `options=(trace=true,)` to `compute` to retain primitive ``Z/P`` in result +details. `output_basis=:total` continues to use the shared computation option +and scales by line length. + +## Schema authority and reconciliation + +The existing LineCableModels typed objects are the only physical-model +authority. The extension creates only derived tags, surface ownership, mesh +sizes, and GetDP tables. It defines no second cable, material, or project +schema. + +Each FEM computation starts with a numeric preflight before model adaptation, +runtime-directory creation, Gmsh initialization, or meshing. The preflight +rebuilds continuous problem data as `Float64`. When a +`Measurements.Measurement` scalar is present, only its nominal value is +retained. Discrete topology such as terminal assignments, material tags, and +pattern counts remains integral. The caller-owned problem is not mutated. Evaluated material-law outputs pass the +same checked conversion to nominal `Float64` before transport. Finite overflow is +rejected. + +| FEM datum | Authoritative LineCableModels property | Handling | +|---|---|---| +| Material class and electrical properties | each resolved `PlacedRegion.source.material`: `kind`, `rho`, `eps_r`, `mu_r`, `tan_delta`, `T0`, `alpha` | Reused. Resistivity follows the selected shared temperature law and constant intrinsic loss tangent contributes ``\omega\epsilon\tan\delta`` to conductivity | +| Material geometry and topology | `CableDesign.geometry.regions`, each resolved `PlacedRegion.primitive`, and `CableDesign.geometry.outer` | Requires an area-complete material partition, then adapts it to built-in `gmsh.model.geo` loops and cut-hole surfaces | +| Cable identity and placement | `LineCableSystem.designs`, `CableDesign.cable_id`, `LineCableSystem.positions`, and resolved `LineCableSystem.geometry` | Reused in declared order. Stable IDs form physical names | +| Terminal ownership and order | `LineCableSystem.terminal_order`, `terminal_map`, and `connection_order` | Reused exactly. Disconnected surfaces of one electrical Group share one terminal physical group | +| Phase, bundle, and grounded-conductor reduction | `LineCableSystem.connection_order` plus shared formulation options | Delegated to the Engine reduction implementation for both ``Z`` and ``P`` | +| Frequencies | `LineParametersProblem.frequencies` | Published as indexed, hidden, read-only ONELAB numbers. Each isolated GetDP job also receives its exact physical frequency and matching transformation radii directly | +| Temperature | `LineParametersProblem.temperature` | Prescribed input to the selected temperature law. The default uses material `T0` and `alpha` | +| Earth material | `LineParametersProblem.earth_props` | Declared air plus one horizontal soil half-space. The soil law is evaluated per frequency | +| Optional environment declaration | `LineCableSystem.environment` | `nothing` and `EarthModel` are accepted. Other declarations produce a typed unsupported-feature error | +| Line length and output basis | `LineCableSystem.line_length` and shared `compute` options | Per-unit-length is the default. Total basis scales Z and Y by line length | +| Propagation constant | backend-owned ``\Gamma\to0`` limit, with independent or first-order coupled fields selected by formulation `options.physics` | Selected by the formulation. The FEM problem supplies geometry, materials, and frequencies | +| Mesh resolution | local characteristic lengths derived from each resolved solid, tube, strand, foil, and passive region. Per-frequency earth skin depth controls the exterior domain, and air-soil propagation scales constrain surrounding-medium resolution | Thin internal features remain local and cannot refine unrelated layers or the earth domain | + +Disks, ellipses, and cable sectors retain exact Gmsh circle or ellipse arcs. +Rectangles and schema polygons retain exact line segments. Annuli, conformal +sector shells, enclosure differences, and +assembly geometric boundaries use shared oriented loops. Circular geometric boundaries are +pre-segmented at sector endpoints and circle contacts, so adjacent materials +reuse the same curve and tangent strands reuse the same point. Compacted strand +polygons are used unchanged. Touching hole boundaries are partitioned into +connected filler faces. Metal-metal seams are excluded from filler boundaries. +Annular wire-ring compartments use that same face tracing: a wire-wire point +contact separates the inner and outer filler lobes into distinct CAD faces, +without adding clearance or changing any material boundary. Separated wires +retain their connecting filler gap. All filler faces retain their declared +material. Equal evaluated material laws +share a physical material group, independently of geometric strand identity and +electrical terminal groups. A shared +material interface takes the smaller of its two local characteristic lengths. +Thin internal foils and wire strands do not export their size to the cable-earth +boundary. One `Distance`/`Threshold` field per actual cable exterior grows +from that exterior layer's size to ``R_{\mathrm{resolution}}/20``, where +``R_{\mathrm{resolution}}=\max(R_{\mathrm{layout}},\delta)``, using an adjacent-element +growth factor of 1.2. This resolution scale is independent of `domain_skin_depths`. +The transformed shell's maximum size is twice that value. +Additional fields restrict the surrounding-medium size to +``h\leq 1/(8|q|)``, where ``q=\sqrt{j\omega\mu\kappa}``, within six attenuation +lengths of cable exteriors and the air-soil interface, capped at +``2R_{\mathrm{resolution}}``. The bound resolves both decay and phase. +It transitions back to the surrounding-medium size beyond that distance. +Gmsh `Restrict` fields apply each bound to its own air or soil surfaces, and +`Min` combines overlapping fields. Refinement follows these fields without artificial rings. The adapter +rejects an incomplete area partition before starting Gmsh and rejects any +internal material curve without exactly two adjacent geometric faces after +synchronization. After meshing, boundary-edge incidence and material coverage +are checked before a mesh can be cached or passed to GetDP. Nonempty but partial +material meshes are rejected. + +Rectangular stranded cores supply their occupied disk boundary directly from +physical resolution. FEM uses the same geometric boundary as preview, analytical +flattening and subsequent layers. Complete bounded formations are recognized +using the same floating-point area tolerance as enclosure resolution, not an +independent engineering fill-fraction cutoff. Retained filler is not replaced +by an expanded conductor. + +The current FEM domain explicitly rejects vertical earth layers, more than one +earth half-space layer, a problem-supplied propagation constant, unsupported +environment types, incomplete material partitions, and any resolved primitive +without a two-dimensional built-in-`geo` geometric boundary adaptation. These failures use +`LineCableModelsFEMError`, including the defining object ID and offending field, +before Gmsh is touched where possible. + +## Mesh lifecycle and diagnostics + +`mesh_mode=:reuse` first validates an explicit highest-frequency `mesh_path`, +then checks the fingerprinted repository-local cache for each frequency, and +otherwise generates the missing frequency-specific mesh. +Compatibility checks cover mesh dimension, terminal count, material and +terminal physical groups, physical names, complete boundary incidence, material +areas (with curved-boundary discretization allowances), and conductor ownership. +Owned MSH 4.1 files retain all boundary elements, including same-material seams. +Explicit mesh files must retain these elements too. `mesh_mode=:remesh` always +regenerates and atomically refreshes the matching cache. The fingerprint +includes the serialized problem, stable physical metadata, every local and +exterior mesh size, the physical mesh frequency, transformation radii, growth +law, and Gmsh version. + +Runs live under `.linecablemodels/fem/runs/`. Cached meshes live under +`.linecablemodels/fem/meshes/`. A successful run directory is deleted after the +result is constructed unless `keep_run_directory=true`. Failed or incomplete +runs are retained, and their typed error reports the path. Retained runs contain +the problem snapshot, immutable GetDP data, mesh snapshot and metadata, raw +tables, maps, logger output, and atomic `run.json` state transitions. Numerical +process logs and attempt metadata live in `attempts/fNNNN-*/`. Per-column timing +records separate constraint updates, assembly, solve and output. At +`getdp_verbosity>=4`, each attempt also retains PETSc profiling output. + +Field maps are off by default. With `plot_field_maps=true`, nine supplied +quantities are written for every frequency and source pair, with names such as +`bm_f0002_b0003.pos`. Every expected file must exist before the scan succeeds. +They remain separate during headless execution. UI execution merges them only after the +complete numerical scan validates. Map paths are retained in result details +only when the run directory is retained. + +Maps `az`, `b`, `bm`, `ez`, `jz`, and `rhoj2` describe the axial 1 A drive. +Maps `e`, `em` and `jm` describe the transverse 1 A/m drive: respectively +``-\nabla v``, its magnitude and ``|\kappa\nabla v|`` in the surrounding media. +Their view labels identify the drive. These axial and transverse fields belong +to different excitations and do not form one full-wave field vector. + +The executable resolution order is: + +1. `compute(...; options=(getdp_executable="/absolute/path/to/getdp",))`. +2. the `LINECABLEMODELS_GETDP` environment variable. +3. the package's GetDP 3.5.0 lazy artifact. +4. `getdp` on `PATH` only when the current platform has no artifact binding. + +The artifact currently supports glibc Linux and macOS on x86-64. GetDP 3.5.0 +publishes its Windows build only as a ZIP, which Julia's artifact installer +cannot consume directly. Windows uses an installed GetDP selected +explicitly, through the environment variable, or on `PATH`. The same external +selection applies on every other unsupported platform. An explicitly selected +or environment-selected invalid path is an error. It is never silently +replaced by another solver. The backend records the resolved source and path +for computation records, while +resume compatibility uses the executable SHA-256 and reported build identity +instead of its filesystem location. See +[`THIRD_PARTY_NOTICES.md`](https://github.com/Electa-Git/LineCableModels.jl/blob/main/THIRD_PARTY_NOTICES.md) +for GetDP's GPL notice and upstream source location. + +A nonzero client failure is reported as a typed +error with its frequency, missing basis indices, retained attempt directory +and GetDP log tail. Scheduling stops on failure. The coordinator terminates and reaps the other +active workers. Completed columns remain available for recovery. A zero exit +code is insufficient without valid completion records and numerical output. +Result details distinguish actual process launches (`getdp_invocations`), +`completed_columns`, and `completed_frequencies`. A fresh complete scan normally +launches one process per frequency. Retries add invocations. + +Resume an interrupted compatible run with +`options=(resume_run_directory="/path/to/run",)` (or `:latest`). Recovery checks mesh identities and column checksums before adopting complete attempt outputs. It requests only missing or invalid terminal columns. The first requested column +always builds fresh factors, even when its terminal index is not one. Worker +count may change during recovery. Solver thread settings, physical inputs and +source and executable identities must match. A surviving solver from an interrupted +coordinator prevents retry until it exits. Completed runs are reused read-only +after their aggregate checksums pass. Runs from older solver protocols remain +preserved comparison artifacts and require a fresh computation. Indexed soil and +declared-air coefficients use run-input schema 7 and solver protocol 3. Older +schemas cannot resume. Evaluated cable, soil, and air coefficients participate +in solve reuse identity. Numerically identical laws can share a solve while +retaining separate selection computation records and independent result arrays. + +## Optional Gmsh UI + +The UI displays the model, fields, and solver diagnostics: + +```julia +interactive_fem = Formulation(:fem) +parameters = compute(problem, interactive_fem; + options = ( + ui = true, + plot_field_maps = true, + ), +) +``` + +It publishes read-only problem summaries, separate mesh and solve states, and +status text, completed frequency and column counts, and `Generate mesh` and +`Run model` buttons. `Run model` refuses +to proceed before a valid mesh exists. +Closing the window before solving raises a typed `:not_executed` error that +distinguishes window closure before mesh generation from window closure after meshing. +The event loop remains active while solver processes run. Closing the window +during solving cancels those processes and retains completed checkpoints. After +a successful scan, validated maps remain visible until the user closes the UI. + +The extension finalizes only Gmsh sessions it owns. A caller-owned initialized +session retains its current model, unrelated models and views, Gmsh verbosity +options, and pre-existing `LineCableModels/FEM/` ONELAB parameters. + +The backend has no Python dependency. diff --git a/docs/src/gridspace.md b/docs/src/gridspace.md new file mode 100644 index 000000000..bf4cd7d79 --- /dev/null +++ b/docs/src/gridspace.md @@ -0,0 +1,619 @@ +# Gridspace + +Gridspace combines finite sets of inputs and constructs one object for each +selected combination. The cable API uses it to construct designs, systems, +and problems with varying parameters. + +The calculation sequence is: + +```text +Grid declares explicit finite variation +Gridspace composes finite sources and selects one point +callable invokes the existing complete construction action +Engine.compute evaluates one complete core problem +ParametricBuilder/UQ collect stored computation data +``` + +`DataModel` constructors validate physical invariants. Engine calculates +core results. UQ performs repeated stochastic realization and aggregation. + +## Core invariants + +A Gridspace has this structure: + +```julia +Gridspace{Target}(build, grids::Tuple; combine=:product) +Gridspace{Target}(grids::Tuple; combine=:product) +``` + +Every member of `grids` must already be a `Grid` or nested `Gridspace`. Wrap +finite alternatives in `Grid` before passing them to this constructor. +`Target` is the result family used for dispatch and +`build` is the callable that constructs it. A nonempty deterministic space +advertises a concrete iterator element type when Julia can prove that type +without evaluating a point. Otherwise-including uncertainty-bearing and empty +spaces-its iterator uses `Base.EltypeUnknown`. `combine` is normalized into the concrete Gridspace +type. The space is lazy and has an analytic length. `rand(space)` selects and +realizes one point without collecting the space. + +## Grid is the variation marker + +`Grid` is the only public marker for finite variation: + +```julia +Grid((1.0, 2.0, 3.0)) +Grid(1.0) # one deterministic point +Grid((1.0, 2.0), (1.0, 5.0)) # nominal × relative error [%] +Grid((1.0, 2.0), AbsoluteError(0.1)) # nominal × absolute error +``` + +The uncertainty-bearing forms yield `UncertainValue(nominal, sigma)` +descriptors defined by the core package. A descriptor becomes a Measurements +value during direct propagation or an ordinary scalar during Monte Carlo realization. + +The constructor laws are: + +```text +Grid(existing Grid) = the existing Grid +Grid(existing Gridspace) = the existing Gridspace +Grid(tuple or array) = its elements are alternatives +Grid(other value) = one alternative +``` + +Grid instances include no selection identity. Reusing an instance in two source +positions has the same behavior as placing two equal, separately constructed +Grids in those positions. + +## Collections are atomic until explicitly varied + +Public domain builders know which of their inputs are complete domain values. +An ordinary tuple, vector, or matrix remains atomic: + +```julia +frequencies = [50.0, 100.0, 1000.0] # one frequency scan +payload = [1.0 2.0; 3.0 4.0] # one matrix + +frequency_sets = Grid(( + [50.0, 100.0], + [50.0, 500.0, 5_000.0], +)) # two complete scans +``` + +When one sibling varies, the builder wraps every ordinary sibling in a +one-point source. +The matrix or frequency vector remains one complete value at every point. +Calling `Grid(matrix)` is different and explicit: the matrix elements become +alternatives because the caller requested that variation. + +A tuple or vector containing a `Grid` or `Gridspace` is not atomic: the +explicit finite sources are composed and the collection is rebuilt from their +selected, completed values. This lets domain collections such as +`[design_space, design_space]` reach their scalar builder as completed +`CableDesign` objects. Ordinary collections containing no explicit finite +source remain one complete value. + +## Product and zip composition + +Composition is local to each Gridspace node. + +### Product + +`:product` is the default and forms the lazy Cartesian product. Julia product +ordering is preserved, with the first source changing fastest: + +```julia +space = Gridspace{Tuple}( + tuple, + (Grid((1, 2, 3)), Grid((10, 20))), +) + +collect(space) +# [(1, 10), (2, 10), (3, 10), (1, 20), (2, 20), (3, 20)] +``` + +Its length is computed directly as the product of the source lengths. + +### Zip + +`:zip` pairs non-singleton sources row by row and broadcasts singleton +sources: + +```julia +space = Gridspace{Tuple}( + tuple, + (Grid((1, 2, 3)), Grid((10, 20, 30)), Grid(:fixed)); + combine=:zip, +) + +collect(space) +# [(1, 10, :fixed), (2, 20, :fixed), (3, 30, :fixed)] +``` + +All non-singleton direct sources must have equal cardinality. A mismatch +throws `DimensionMismatch` when the Gridspace is constructed, before any point +is materialized. Zip traversal is linear in the number of rows. + +### Nesting + +A nested Gridspace is one finite source at its parent. Its selected value remains unresolved until recursive materialization or realization reaches it. Nested +resolution lets one child zip local parameters while its parent forms a +Cartesian product: + +```julia +paired = Gridspace{Tuple}( + tuple, + (Grid((1, 2)), Grid((10, 20))); + combine=:zip, +) + +outer = Gridspace{Tuple}(tuple, (paired, Grid((:a, :b)))) +collect(outer) +# [((1, 10), :a), ((2, 20), :a), +# ((1, 10), :b), ((2, 20), :b)] +``` + +## Public constructors + +Gridspace delays finite selection. Each lifted public action applies one rule: + +```text +no admitted Grid or Gridspace -> invoke the scalar action now +at least one explicit source -> preserve sources, singleton-wrap siblings, + and return a Gridspace +``` + +For domain arguments that are tuples or vectors, an admitted source may be a +collection member. The collection itself is reconstructed before the scalar +action runs. + +The current behavior is: + +| Entry point | Scalar or complete input | Explicit varying input | +|---|---|---| +| `Material` | `Materials.Material` | `Gridspace{Materials.Material}` | +| `Disk`, `Shell`, and other physical geometry | concrete primitive or contextual shell | `Gridspace{Primitive}` or `Gridspace{Shell}` | +| `Region`, `Stack`, `Group`, `Assembly`, `Enclosure` | concrete cable part | `Gridspace{TargetPart}` | +| `CableDesign` | `DataModel.CableDesign` | `Gridspace{DataModel.CableDesign}` | +| `at`, `trefoil`, `hflat`, `vflat` | `Pose2` or `Vector{Pose2}` | corresponding `Gridspace` | +| `homogeneous` | `EarthModel` | `Gridspace{EarthModel}` | +| `LineCableSystem` | `DataModel.LineCableSystem` | `Gridspace{DataModel.LineCableSystem}` | +| `LineParametersProblem` | `Engine.LineParametersProblem` | `Gridspace{Engine.LineParametersProblem}` | +| `CableConstantsProblem` | `Engine.CableConstantsProblem` | `Gridspace{Engine.CableConstantsProblem}` | +| `CableConstants` | `Engine.CableConstants` | `Gridspace{Engine.CableConstants}` | +| `Formulation` | `Engine.LineParametersFormulation` | `Gridspace{Engine.LineParametersFormulation}` | +| `CableConstantsFormulation` | `Engine.CableConstantsFormulation` | `Gridspace{Engine.CableConstantsFormulation}` | +| `ModalAnalysisFormulation` | `ModalAnalysis.ModalAnalysisFormulation` | `Gridspace{ModalAnalysis.ModalAnalysisFormulation}` | +| backend formulation constructor | completed backend formulation | target-bearing formulation `Gridspace` | +| `@gridspace` keyword constructor | strict struct | `Gridspace{Target}` | + +Scalar-complete calls invoke their domain action immediately. A varying call +stores only that callable and explicit finite sources. Each selected point +invokes the same scalar action. + +## Gridspace callables + +A Gridspace callable should be a concrete immutable functor whose field types +are concrete. The callable should perform one construction step and delegate physical +validation to the target constructors: + +```julia +struct PairValue end +(::PairValue)(left, right) = (left, right) + +space = Gridspace{Tuple}( + PairValue(), + (Grid((1, 2)), Grid((10, 20))), +) +``` + +Avoid `Function`-typed fields in frequently called builders. A function that captures +local variables is suitable for local experiments. Reusable callables use concrete types. +Do not introduce a passive record merely to store arguments that another +function immediately unpacks. + +`@gridspace` applies the same rule to a keyword-constructed struct. The macro +retains the strict positional constructor, returns the struct immediately for +scalar keyword input and creates a Gridspace only when a field is an explicit +finite source. + +## Materialization and realization + +Ordinary iteration selects an internal target-bearing `Gridpoint{Target}` and recursively +materializes its arguments. Deterministic values pass through unchanged. +Nested points invoke their own callable before the parent callable is invoked. +After loading Measurements, an `UncertainValue` materializes as one +`Measurement`. + +Stochastic realization follows the same recursion with a caller-owned random +number generator. Only `UncertainValue` leaves are redrawn. Deterministic +selections remain fixed. A zero-sigma descriptor resolves deterministically. + +Materialization and realization are internal. Developers extend the public grammar through +concrete builders and supported uncertainty extensions, not +by exposing unresolved points as application data. + +## Higher-order computation + +Passing a formulation `Gridspace` directly to `compute` selects default +`Combinatorial` traversal: + +```julia +run = compute(problem, formulation_space; options=(;)) +run = compute(problem_space, formulation_space; options=(;)) +run = compute(ParametricProblem(problem_space, ComputationOptions(options)), formulation_space) +``` + +The result is a `ParametricResult` in all three cases. A scalar problem forms +a singleton problem axis. `options` belong to the core computations. An +existing `ParametricProblem` retains its stored options. For traversal settings +such as retaining supplemental details, select `Combinatorial` explicitly: + +```julia +run = compute(ParametricProblem(problem_space, ComputationOptions(options)), + Combinatorial(formulation_space; options=(retain_details=true,))) +``` + +`Combinatorial` accepts one completed formulation, a deterministic +target-bearing formulation `Gridspace`, or a deterministic `Grid` containing +completed formulations. Formulation points are resolved once. Traversal then +materializes each selected problem exactly once and gives the complete +formulation vector to `compute`: + +```text +resolve every formulation point once + +for each `Gridpoint{Problem}` + materialize the scalar problem once + compute(problem, resolved_formulations) +end +``` + +Each problem-formulation pair is evaluated. Composition inside a formulation +constructor remains local to that formulation: `combine=:product` or +`combine=:zip` determines its formulation points, while the outer +problem-formulation relation is always Cartesian. + +The generic vector `compute` method delegates to established scalar dispatch. +Owners may specialize that vector method to share immutable lowering work. The +Coaxial line-parameter path validates and flattens every selected design once +per problem point, constructs its formulation-independent local input once, +then allocates and solves one independent workspace per formulation. The +cable-constant path independently follows the same one-flatten rule. Mutable +matrices, formula-dependent earth data, reduction maps, and diagnostic storage +belong to each workspace. Modal transformation does not need special lowering and uses the +generic route on the same phase-domain matrices. + +`ParametricResult` retains both axes: + +```julia +run.axes.problems +run.axes.formulations +run[problem_index, formulation_index] +``` + +Its `values` vector and ordinary linear iteration remain available. Storage is +column-major in `(problem, formulation)` coordinates: the problem index varies +fastest. The formulation for column `j` is stored in `run.axes.formulations[j]`. +Use `formula_id(run.axes.formulations[j].methods.earth_impedance)` to identify +its earth-impedance method. + +Direct linear propagation uses the same traversal. The Measurements extension +changes only how an uncertain descriptor materializes. `LinearErrorResult` +stores only its formulation and ordered core results. + +ParametricBuilder defines this shared traversal as the qualified `traverse` +method. It computes the first problem-formulation batch, allocates a vector of +that exact result type with the analytic Cartesian cardinality and rejects any +later type change. Optional detail records follow the same rule and are +resolved through each scalar formulation's computation owner. `Combinatorial` +constructs its result space from `values`, `axes` and `details`. `LinearError` +uses one scalar formulation and consumes `values` and `details`. + +Monte Carlo selects each outer point once and derives a deterministic seed for that point. It repeatedly realizes the same point: + +```text +for each selected outer point + until the requested successful trials are collected + redraw uncertain leaves within that point + build a fresh complete core problem + Engine.compute + optionally reject DomainError realizations with a bounded number of resampling attempts + aggregate that point's draws +end +``` + +Each nominal and error point produces its own aggregate. `MonteCarloResult` directly owns sample-mean core results, statistics, +optional retained samples, optional histograms, the root seed, point seeds, +and trial counts. + +The default `on_error=:fail` rethrows every exception. With +`options=(retain_details=true, on_error=:resample, max_failures=n)`, only +`DomainError` is treated as an unsupported realization. Rejected draws do not +enter samples or statistics, and resampling stops when the requested accepted-trial +count is reached or `n` failures have occurred. This estimates the conditional +distribution of the output when problem construction and computation +succeed. The retained failure summary makes the conditioning rate explicit. + +For cable-constant Monte Carlo computations, the representative stored in the +result space remains a `CableConstants` core result. Retained samples, +statistics, and histograms are concrete named tuples with keys `R`, `L`, `C`, +and `G`. Cable samples have assembly × trial dimensions. Line-parameter +products retain conductor × conductor × frequency × trial dimensions. +`MonteCarloResult` validates these point-aligned arrays' keys and dimensions +and supplies their public observation methods. + +### Observe and compare retained uncertainty + +Statistical selectors use the same observation grammar as deterministic values: + +```julia +using Statistics +using LineCableModels.ReportBuilder: BenchmarkTableDefinition +request = @observe (statistics, R, mean)[1, :, :, :] +table = observables(mc_result, (request, (statistics, R, std, 1)); length_unit=:base) +``` + +Both MC and LEP expose selected `mean` and `std`. MC means are empirical. +LEP means are first-order nominal predictions. `std` is physical spread, not +uncertainty of the estimated mean. A full MC request `(statistics, R, 1)` +publishes mean, standard deviation, minimum, fifth percentile, median, 95th percentile, maximum and the successful trial count. +`Base.Fix2(quantile, 0.05)` selects the retained fifth percentile. Unsupported +percentiles fail rather than interpolate an invented distribution. +X/B scale the retained L/C summaries by positive `2πf`. Complex Z/Y support +mean and the nonnegative complex standard deviation. Ordered complex +percentiles are undefined. Joint samples and histograms remain separate +products, available only when retained or derivable from retained samples. + +```julia +definition = BenchmarkTableDefinition(((statistics, R, mean), (statistics, R, std)); + bands=(:all, :dc, :harmonic, :narrow, :wide)) +comparison = report(definition, (reference=mc_result, result=lep_result)) +comparison.table.features # Numeric formulation rows, frequency-band columns +comparison.table.statistics +comparison.table.sampling +``` + +Results without terminal identities require explicit `(result, metadata)` +operands. Multiple reference points require explicit `pairing`. Populations +are never pooled. The shared Engine RMS applies the same two-sided numerical +resolution rule to each selected statistic and frequency band. + +`confidence(mc_result, point)` reports the actual simultaneous DKW bound, +configured target, marginal count, trial count and conditioning. It counts +both matrix orientations conservatively and applies per outer point, not +campaign-wide. Fixed trials need not certify the configured target. Its +`mean_standard_error` is `s/sqrt(n)` for `n>1`, not an exact confidence interval. +One trial cannot establish population spread. No LEP distribution is inferred +from two moments, and no report initiates sampling or physical computation. + +Portable scientific records preserve the complete set of retained MC products and shared MC/LEP +Measurement sources, including signed sensitivities. Built-in `:normal` and +`:uniform` law choices are retained directly. Distributions.jl `Normal` and +`Uniform` records retain their actual parameters. Other custom input laws or +formulation declarations require an owner-provided codec and fail explicitly +when no codec exists. They are never changed into a supported law on recovery. + +All completed result spaces are one-dimensional finite Julia collections. +Iteration and indexing return one stored core result per computation. A +`ParametricResult` with several formulations contains the Cartesian +problem-formulation cardinality in its documented storage order. Monte Carlo +iteration returns the stored uncertainty-bearing +core result constructed during aggregation from each point's sample means and +sample standard deviations. Individual trials +remain available only through `samples`. Standard `first`, `last`, `only`, +`collect`, `map`, and `zip` operations apply. `only` asserts singleton +cardinality and does not perform statistical selection or result transport. + +## Transporting completed result spaces + +A completed result space enters another scalar computation through the target +`Gridspace` constructor: + +```julia +modal_problems = Gridspace{ModalAnalysisProblem}(phase_results) +``` + +The following example starts with two completed phase scans and a completed +line formulation. It uses public collection constructors and compute methods +to calculate modal parameters and then finite-length responses: + +```julia +# phase_a and phase_b are completed phase LineParameters with declared lengths. +phase_results = ParametricResult(Combinatorial(line_formulation), + [phase_a, phase_b]) +modal_formulation = ModalAnalysisFormulation(:default) + +# One scalar action and direct target-bearing Gridspace action. +scalar_modal = compute(ModalAnalysisProblem(phase_a), modal_formulation) +modal_problems = Gridspace{ModalAnalysisProblem}(phase_results) +modal_results = compute(modal_problems, modal_formulation) +length(modal_results) == 2 # N → N, original source order + +# Explicit Combinatorial uses the same finite traversal. +explicit_results = compute(ParametricProblem(modal_problems), + Combinatorial(modal_formulation)) +length(explicit_results) == 2 + +two_formulations = ModalAnalysisFormulation( + Grid((modal_formulation, modal_formulation))) +product_results = compute(modal_problems, two_formulations) +length(product_results) == 4 # N × M, source index varies fastest + +segments = collect(Gridspace{PropagationParameters}(modal_results)) +short_segments = PropagationParameters(scalar_modal; + line_length=Grid((150.0, 300.0))) +roots = gamma.(modal_results) # each element is a mode × frequency array +voltage_bases = Tv.(modal_results) # each element is a phase × mode × frequency array +responses = H.(segments) # each element is a mode × frequency array +``` + +`modal_results` and `product_results` store concrete scalar `LineParameters` +elements. `segments` stores concrete scalar `PropagationParameters` elements. +The first axis of each completed result space follows the source order. For +the product, the linear index is `source_index + (formulation_index-1)*N`. +The quantity arrays +remain single outer elements under ordinary Julia broadcast. A completed +formulation `Grid` can be passed directly to `compute`. + +The dispatched method `Gridspace{Target}(source::SourceResult)` defines how arguments from the source family are supplied to the target problem constructor. The +transport preserves source cardinality and order unless that source-specific +method explicitly documents another operation. It produces a `Gridspace` of +the target problem type. + +`ParametricResult` transports its completed combinatorial results directly. +`LinearErrorResult` and `MonteCarloResult` transport their stored +uncertainty-bearing results directly. Load `Measurements` before MC computation. +The extension is required before sampling starts. Aggregation constructs each +marginal from the accepted raw samples, independently of histogram bins and +sample-retention options. The uncertainty is the output sample standard +deviation, not the standard error of its mean. Covariance between observables remains unavailable from this marginal surrogate. Repeated indexing, `uncertain`, and +transport reuse its existing uncertainty-source identities without rebuilding +Measurements. + +Only result families with defined semantics are admitted. An unsupported +source-target pair raises an error identifying both types and directs the caller to +`?Gridspace` and this page. Extension code adds a transport by defining: + +```julia +import LineCableModels: Gridspace + +function Gridspace{Target}(source::OwnedResultSpace) + # Expose stored source values, then return Gridspace{Target}. +end +``` + +This keeps every scientific computation scalar. Finite composition recurses +through the same typed result-to-problem conversion: + +```text +ResultSpace{A} → Gridspace{ProblemB} → ResultSpace{B} +``` + +## Pairing, exact reuse, and correlation + +Zip pairing, exact argument reuse, and stochastic correlation have different +semantics. + +`combine=:zip` is deterministic row pairing between finite sources. Zip pairing says +nothing about covariance. + +Exact reuse is structural. Pass one selected uncertain argument once to a +callable and use that argument more than once: + +```julia +struct Duplicate end +(::Duplicate)(value) = (value, value) + +space = Gridspace{Tuple}( + Duplicate(), + (Grid(10.0, AbsoluteError(0.5)),), +) +``` + +Direct propagation constructs one Measurement and both tuple positions retain +that variable. Monte Carlo draws once and passes the same scalar to both +positions. But two separately declared uncertain source positions are +independent, even when they contain the same Grid instance or numerically equal +descriptors. + +General correlation between distinct variables is a UQ concern. Correlation requires a +joint stochastic source or distribution that returns a tuple or vector sample +consumed by one builder. Gridspace does not infer or register correlation. + +### Feasible geometric dependence + +For a fixed-count wire ring, varying the ring radius and wire diameter +independently can produce overlaps even when the nominal ring fits. Declare the +intended dependence in an ordinary builder, before constructing physical parts: + +```julia +using LineCableModels, Measurements, Random + +joint = Gridspace{NamedTuple{(:inner, :diameter, :x)}}( + (scale, x) -> (inner=0.0679scale, diameter=0.006scale, x=x), + (Grid(1.0, 10.0), Grid(0.0, AbsoluteError(0.002))), +) +geometry = Gridspace{Tuple}( + p -> ( + Ring(68; r=p.inner+p.diameter/2), + Disk(p.diameter/2), + Annulus(p.inner, p.inner+p.diameter, Pose2(p.x, 0.0)), + ), + (joint,), +) +propagated = only(geometry) +sampled = rand(Xoshiro(42), geometry; distribution=:uniform) +``` + +Lengths are in meters. Here the common scale has mean 1, standard deviation +0.1 and uniform support `[1-sqrt(3)*0.1, 1+sqrt(3)*0.1]`. The nominal chord +clearance is positive. Shared positive scaling preserves that sign and the +68-wire inventory throughout this support. The annulus thickness is derived +from the same diameter, and its independent coordinate retains its own +uncertainty. For a complete stack, derive successive layer radii from positive +thicknesses inside the same builder. Nest the resulting source once in the +`LineParametersProblem` builder, then use it with `ParametricProblem` and +either `LinearError` or `MonteCarlo`. + +This is a specified correlated model, not independent manufacturing tolerances. +It preserves each length's nominal mean and 10% standard deviation, but derived +areas scale quadratically: their mean is `1.01` times nominal area. Linear +propagation is local and does not include that second-order mean shift. +Normal sampling remains the default and has unbounded support. Finite reserve +distances cannot make all independent normal draws feasible. Resampling +estimates a distribution conditional on success, not the original input law. + +Executable native checkpoints retain the joint builder and its sources. +Marginal Measurement JSON retains values and standard uncertainties only. It is +not a format for archiving the joint statistical law or covariance. + +## Optional package extensions + +The core package declares uncertainty without loading Measurements or +Distributions. + +- Loading Measurements adds direct materialization of `UncertainValue` while + retaining exact structural reuse. +- Loading Distributions adds standardized univariate sampling families. The + selected distribution must have finite mean and positive finite standard + deviation. Samples are transformed to the descriptor's nominal value and + standard uncertainty. + +## Performance and conformance + +The implementation relies on tuple-specialized recursion and Julia's public +product and zip iterators. The implementation guarantees: + +- `length` is computed analytically from source cardinalities. +- Product and zip traversal are linear in yielded work. +- Materialization and realization do not use a dictionary or identity lookup. +- Immutable scalar targets infer through selection, materialization, and + realization. +- After warmup, deterministic iteration and a bare 10,000-realization scalar + loop run without heap allocation. +- Allocations during full cable and line construction are limited to the requirements of existing vectors and domain constructors, the Engine computation and the requested result storage. + +The conformance tests in `test/unit/commons/gridspace.jl` and +`test/unit/parametricbuilder/conformance.jl` check these properties, exact structural +reuse, explicit variation, and scalar public construction. + +## Implementation map + +The implementation is split across: + +- `src/grid.jl`: finite values and uncertainty descriptors. +- `src/gridspace.jl`: composition, point selection, recursive + materialization, and realization. +- `src/parametricbuilder/macros.jl`: strict scalar construction and explicit + Gridspace lifting for `@gridspace`. +- material, cable, position, and system files: scalar construction and lifting + rules and concrete callable algorithms. +- `src/parametricbuilder/traversal.jl`: combinatorial traversal and the + phase-to-modal composition of a `ParametricProblem`. +- `src/engine/modalanalysis/composition.jl`: public phase-to-modal composition. + `src/engine/modalanalysis/problems.jl`, `compute.jl`, and `propagation.jl` own + modal scalar computation and line segment binding. +- `src/uq/linearerror.jl` and `src/uq/montecarlo/compute.jl`: direct and + repeated stochastic traversal. +- Measurements and Distributions extensions: dependency-specific uncertainty + behavior only. diff --git a/docs/src/index.md b/docs/src/index.md index 3796e339c..1d3fcb605 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,54 +1,90 @@ # LineCableModels.jl -[`LineCableModels.jl`](https://github.com/Electa-Git/LineCableModels.jl) is a specialized Julia package designed to compute the electrical parameters of coaxial arbitrarily-layered underground/overhead cables with uncertainty quantification. It focuses on calculating line and cable impedances and admittances in the frequency-domain, accounting for skin effect, insulation properties, and earth-return impedances with frequency-dependent soil models. - -## Documentation outline - -```@contents -Pages = [ - "index.md", - "tutorials.md", - "reference.md", - "bib.md", -] -Depth = 1 -``` +[`LineCableModels.jl`](https://github.com/Electa-Git/LineCableModels.jl) +calculates frequency-domain electrical parameters for underground and overhead +cable systems. The models include conductor skin effect, dielectric loss, +earth return, frequency-dependent earth properties, and declared uncertainty +in geometry and material data. + +## Documentation + +- [Tutorials](tutorials.md) introduce cable construction and computation. +- [Modeling and results](usage.md) covers computations, result access, + uncertainty, tables, and plots. +- [Gridspace and uncertainty](gridspace.md) specifies finite variation and + uncertainty realization. +- [API reference](reference.md) lists the line and cable computation API. +- [Conveniences](conveniences.md) covers estimates, scalar formulas, and VDE + designation parsing. +- [Developers](developers.md) records grammar invariants, CI checks, extension + APIs, and project conventions. ## Features -- Calculates all base DC parameters of a given cable design (R, L, C and G), for solid, tubular or stranded cores, semiconductors, screens, armors, sheaths, tapes, and water-blocking materials, with uncertainty propagation using the [Measurements.jl](https://github.com/JuliaPhysics/Measurements.jl) package. -- Correction factors to account for temperature, stranding and twisting effects on the DC resistance [app14198982](@cite), GMR [6521501](@cite) and base inductance of stranded cores and wire screens [yang2008gmr](@cite). -- Explicit computation of dielectric losses and effective resistances for insulators and semiconductors [916943](@cite). Correction of the magnetic constant of insulation layers to account for the solenoid effect introduced by twisted strands [5743045](@cite). -- Computes phase-domain Z/Y matrices for poliphase systems with any number of conductors per phase, and sequence-domain components for three-phase systems, with uncertainty propagation. -- Improved equivalent tubular representation for EMT simulations and direct export to ATPDraw and PSCAD formats. -- Computes internal impedances of solid, tubular or coaxial multi-layered single-core (SC) cables, using rigorous [4113884](@cite) or equivalent approximate formulas available in [industry-standard EMT software](https://www.pscad.com/webhelp/EMTDC/Transmission_Lines/Deriving_System_Y_and_Z_Matrices.htm). -- Computes earth-return impedances and admittances of underground conductors in homogeneous soil, based on a rigorous solution of Helmholtz equation on the electric Hertzian vector, valid up to 10 MHz [5437464](@cite). +- Construct deterministic and uncertain designs with the typed + `Grid`/`Gridspace` grammar and evaluate them with `compute`. +- Calculate base cable parameters for solid, tubular, and stranded cores, + semiconductors, screens, armors, sheaths, tapes, and water-blocking materials. +- Apply temperature, wire stranding and twisting corrections to DC resistance + [app14198982](@cite), GMR [6521501](@cite) and base inductance + [yang2008gmr](@cite). +- Calculate dielectric loss and equivalent insulation resistance + [916943](@cite), including the solenoid contribution of twisted strands to + insulation permeability [5743045](@cite). +- Assemble phase-domain Z/Y matrices for polyphase systems with any number of + conductors per phase, with optional Measurements-based direct propagation or + conditional Monte Carlo analysis. +- Replace stranded assemblies with equivalent tubular conductors for EMT + computations and export cable data to ATPDraw and PSCAD. +- Calculate internal impedance for solid, tubular, and multilayer coaxial + single-core cables with the formulation in [4113884](@cite) or the + approximations documented by + [PSCAD](https://www.pscad.com/webhelp/EMTDC/Transmission_Lines/Deriving_System_Y_and_Z_Matrices.htm). +- Calculate earth-return impedance and admittance for underground conductors in + homogeneous soil from the electric-Hertz-vector solution of the Helmholtz + equation, up to 10 MHz [5437464](@cite). ## Installation -Clone the package and add to the Julia environment: +Install the registered package from Julia's package manager: ```julia-repl -pkg> add https://github.com/Electa-Git/LineCableModels.jl.git +pkg> add LineCableModels ``` -If you are using the finite-element solver, it is recommended to run the build script to retrieve the binaries needed by the [GetDP.jl](https://github.com/Electa-Git/GetDP.jl) front-end: +Then load the core package: -```julia-repl -pkg> build LineCableModels +```julia +using LineCableModels ``` -Then, in your Julia code, import the package: +Plotting is optional. Load one backend explicitly before calling `preview` or +`plot`: ```julia using LineCableModels +using CairoMakie ``` +## User statistics + +![Top Julia package-server regions observed for LineCableModels.jl](assets/user-statistics.svg) + +The map is generated in CI from Julia's public package-server request logs. It +shows the top server regions by the sum of `request_addrs` for requests marked +as user traffic. These regional aggregates are not a count of distinct people +and do not represent country-level telemetry. + + ## License -The source code is provided under the [BSD 3-Clause License](https://github.com/Electa-Git/LineCableModels.jl/LICENSE). +The source code is licensed under the +[BSD 3-Clause License](https://github.com/Electa-Git/LineCableModels.jl/LICENSE). +The optional FEM backend invokes GetDP as an external program. Its notice and +source link are recorded in +[`THIRD_PARTY_NOTICES.md`](https://github.com/Electa-Git/LineCableModels.jl/blob/main/THIRD_PARTY_NOTICES.md). --- ```@raw html

Documentation generated using Documenter.jl and Literate.jl.

-``` \ No newline at end of file +``` diff --git a/docs/src/modal_analysis.md b/docs/src/modal_analysis.md new file mode 100644 index 000000000..9012e2b96 --- /dev/null +++ b/docs/src/modal_analysis.md @@ -0,0 +1,324 @@ +# Modal analysis and line segments + +`ModalAnalysisProblem` consumes one completed phase-domain `LineParameters`. +`ModalAnalysisFormulation` selects the decomposition. The returned +`LineParameters` contains modal coefficients, modal-to-phase voltage and +current bases `Tv` and `Ti`, and retained propagation roots `gamma`. + +```julia +phase = compute(line_problem, line_formulation) +modal = compute(ModalAnalysisProblem(phase), ModalAnalysisFormulation(:default)) +restored = transform(PhaseDomain, modal) + +Tv(modal) # phase × mode × frequency +Ti(modal) # phase × mode × frequency +gamma(modal) # mode × frequency +Zc(modal) # mode × frequency +Yc(modal) # mode × frequency +Zc(modal, PhaseDomain) # ordered phase × phase × frequency +``` + +The bases map modal coordinates to phase coordinates. At each frequency the +coefficients are `Tv \ (Zphase * Ti)` and `Ti \ (Yphase * Tv)`. Inverse conversion +uses the retained bases. The characteristic quantities use the diagonal modal +approximation and the retained root branch. Phase characteristic matrices +preserve row and column order and need not be symmetric. + +The convenience call performs the same two public actions in sequence: + +```julia +modal = compute(line_problem, line_formulation; modal=:default) +modal = compute(line_problem, line_formulation; + modal=formula(:default; options=(iteration=(convergence=1e-8,),)), + modal_options=(offdiagonal_tolerance=1e-6,)) +``` + +The native, FEM and PSCAD line-parameter calls and the parametric studies accept the +`modal` keyword. The positional form takes a `ModalAnalysisFormulation` and composes any +formulation whose `compute(problem, formulation; options)` returns `LineParameters`. A +user-owned formulation composes too: + +```julia +modal = compute(line_problem, user_formulation, ModalAnalysisFormulation(:default)) +``` + +A problem `Gridspace`, one of its grid points and a `ParametricProblem` compose in the +same way. Cable constants are not line parameters and do not compose with modal analysis. + +`modal_options` controls the modal action only. A nonempty value requires +`modal`. The upstream solver, including its selected backend and batched path, +completes before modal analysis begins. Upstream `on_result`, progress, and +timing belong to that upstream call. Modal `on_result`, progress, and timing +belong to the downstream call. Timed results preserve the source gridpoint +identity. A missed iteration or coupling target produces a warning and a +diagnostic record. Finite results are returned. The iteration convergence +setting is a numerical target. It does not bound the physical error. Inspect +`details(modal).data.modal.diagnostics` for fallback and missed frequency +indices, Z/Y residual coupling, returned eigen-residuals, iteration counts, +and convergence flags. These diagnostics describe the numerical solve. Scientific +acceptance requires a separate decision. The scalar characteristic and response construction +uses the selected modal approximation when coupling remains. + +The built-in formula selects paired voltage and current eigenvectors and +retains a forward root with nonnegative real part, or nonnegative imaginary +part for a purely imaginary root. The modal workspace supplies impedance and +admittance slices, coordinate conversion buffers, the admittance-impedance +product, eigenpair history and voltage-vector buffers. The built-in formula +tracks eigenvalues of its normalized, shifted eigenproblem. Multiplying +`eigenvalue + 1` by the scale gives an eigenvalue of the physical product, +distinct from a propagation root. The selected formula builds the buffers of its +normalized eigenproblem, Hungarian assignment and Levenberg–Marquardt least squares +in its `initialize_buffers` method. Its arithmetic remains in +`decompose!(::Formula{:chrysochos2014}, ...)`. `Tv` +and `Ti` map modal coordinates to phase coordinates. Total source coefficients +are normalized by their declared source length before the line segment is +bound. + +## Alternative complex LM implementation + +Select `:vieira2026` for the complex Levenberg–Marquardt implementation adapted +from the supplied Vieira tracker, based on Chrysochos et al. (2014): + +```julia +selected = ModalAnalysisFormulation(:vieira2026) +modal = compute(ModalAnalysisProblem(phase), selected) + +# The convenience call performs the same two computations. +modal = compute(line_problem, line_formulation; modal=:vieira2026) +``` + +The method consumes the completed matrices at the stored frequencies. It uses +a two-sample predictor, SVD minimum-norm steps, greedy eigenvalue assignment and +clustered eigenvector refinement. When tracking misses its matching or predictor +target, it uses a correlation-matched eigensolution at that same frequency. +There is no interpolation, adaptive frequency insertion or physical reevaluation. +The default formula remains `:chrysochos2014`. + +Numerical controls belong to the selected formula: + +```julia +selected = ModalAnalysisFormulation(:vieira2026; options=( + iteration=(convergence=1e-13, max_iterations=100), + tracking=(predictor_tolerance=0.25, eigenvalue_tolerance=1e-5, + cluster_tolerance=1e-3, order_by_velocity=true), +)) +``` + +`convergence` is the Euclidean norm target for the complex eigenpair residual, +including its bilinear normalization constraint. It is not the differently +interpreted convergence control of `:chrysochos2014`, nor an accuracy guarantee. +Matching compares eigenvalue distances against `eigenvalue_tolerance` times the +larger of one and the largest normalized eigenvalue magnitude. Clusters connect +normalized eigenvalues whose gap is below `cluster_tolerance` times the larger +of their shifted magnitudes and `1e-3`. `predictor_tolerance` bounds relative +eigenvalue and isolated-vector changes from the predictor. These decisions select +the numerical tracking route. Finite results are retained with warning diagnostics. + +Tracking history uses `transpose(t)*t = 1`. Published current bases have unit +Euclidean column norms. Voltage bases are the normalized paired `Z*Ti` columns. +With `order_by_velocity=true`, decreasing phase constant at the final frequency +sets one mode order for the entire scan, including its diagnostics. Disable this +option to retain the seed's tracked order. The first frequency is obtained by +direct eigendecomposition and has no LM iteration count or convergence flag. +Subsequent flags describe LM convergence even when a conventional eigensolution +is ultimately returned. The reported eigen-residual describes that returned pair. + +This adaptation uses `s=j2πf` and the package's vacuum permittivity +`8.8541878128e-12 F/m`. The supplied `EigTrack.jl` used `8.854187817e-12 F/m` and +also supported complex-frequency reevaluation. The formula builds its complex +least-squares, predictor and assignment buffers in its `initialize_buffers` method and +reuses the common modal buffers. The detailed method attribution is attached to its +formula description. + +## Newton–Raphson implementation + +Select `:wedepohl1996` for the Newton–Raphson eigenpair continuation adapted +from `eig_newton` in the supplied UniversalLineModel implementation of +Wedepohl, Nguyen and Irwin (1996), DOI: 10.1109/59.535695: + +```julia +selected = ModalAnalysisFormulation(:wedepohl1996; options=( + iteration=(convergence=1e-9, max_iterations=60),)) +modal = compute(ModalAnalysisProblem(phase), selected) +``` + +The seed modes are ordered by decreasing attenuation. Subsequent samples use +the previous eigenpairs in Newton corrections of `YZ / opnorm(YZ, 2)` (the matrix +spectral norm), with the bilinear constraint `transpose(t)*t = 1`. The +convergence target bounds the largest absolute correction in this normalized +problem. A singular iteration, missed target or duplicate eigenpair causes +same-frequency direct eigendecomposition and greedy correlation matching. +These events remain warning diagnostics. No physical samples are reevaluated. + +## Shared basis rotation and sign continuity + +All modal formulations, including user-defined formulations, use +`options=(rotate=true,)` by default. After decomposition and before computing +modal Z/Y, each Ti column is multiplied by a unit complex factor that minimizes +its squared imaginary norm. The same factor multiplies its Tv column, preserving +the voltage and current pairing. This does not independently minimize the imaginary +part of Tv. The remaining 180-degree ambiguity is resolved by requiring a +nonnegative real overlap with the previous frequency's Ti column. Mode order +and the decomposition's tracking history are unchanged. + +```julia +modal = compute(ModalAnalysisProblem(phase), selected; options=(rotate=false,)) +modal = compute(line_problem, line_formulation; modal=:wedepohl1996, + modal_options=(rotate=false,)) +``` + +`rotate=false` retains the formula's complex column phases apart from fixing +180-degree flips. It does not disable the formula's internal normalization or +tracking. The choice is retained at `details(modal).data.modal.rotate` and in +observation assumptions. Rotated and unrotated observations remain distinct +when grouped. + +Column norms, diagonal modal Z/Y/Zc/Yc, propagation constants and reconstructed +phase quantities (including H) remain unchanged under the paired operation. Residual +off-diagonal modal entries retain their magnitudes but can change phase with the +coordinate choice. No values are clipped or set to zero by this operation. + +## Finite segments and source length + +`PropagationParameters` binds a modal scan to one physical segment. A source +with total coefficients retains its positive source normalization length. +The line segment may have another length. For an external scan without a +known source length, supply the target length explicitly. + +```julia +segment = PropagationParameters(modal; line_length=150.0) +next_segment = PropagationParameters(segment; line_length=300.0) +length_space = PropagationParameters(modal; + line_length=Grid((150.0, 300.0))) # lazy line segment Gridspace + +H(segment) # mode × frequency +alpha(segment) # attenuation constant, Np/m +beta(segment) # phase constant, rad/m +velocity(segment) # phase velocity, m/s +H(segment, PhaseDomain; field=:voltage) # ordered phase voltage response +H(segment, PhaseDomain; field=:current) # ordered phase current response +``` + +The modal forward factors are `exp.(-gamma(segment) .* line_length(segment))`. +`alpha` and `beta` are the real and imaginary parts of the per-unit-length +`gamma`, independent of segment length. `velocity` is +`2πf / beta` at each retained mode and frequency. Undefined values remain +unavailable in observations, with their masks and reasons retained. The default +display units for the first two are Np/km and rad/km. +The phase voltage and current responses use `Tv` and `Ti`, respectively. +Zero length gives identity propagation. Rebinding preserves the embedded source. +A changed length receives a new gridpoint identity. Indexing a +modal or finite result by frequency slices coefficients, bases, roots, and +frequency-aligned diagnostics together without a new solve. + +## Collections and observations + +Completed phase result spaces transport through +`Gridspace{ModalAnalysisProblem}(phase_results)`. One modal formulation +keeps one downstream result per source point. A formulation Gridspace forms +the ordinary product or zip selection. Completed modal results transport +through `Gridspace{PropagationParameters}(modal_results)` for finite lengths. +Scalar quantities compose with broadcasting, such as `gamma.(modal_results)` +and `H.(segment_results)`. Each tensor remains one value. + +`ObservedResult` retains detached quantities and the existing four sections: +gridpoint, quantities, errors, and timings. Requests can mix phase coefficients +with `gamma`, `Zc`, `Yc`, `Tv`, and `Ti`. A line segment also offers `H`, +`alpha`, `beta`, and `velocity`. Bare complex requests acquire complete real and +imaginary pairs. A raw plotting convenience also completes one component +while displaying only that component. An observed-input plot selects retained +components without calculating an absent representation. +Bind phase representations explicitly: + +```julia +phase_Zc = Base.Fix2(Zc, (domain=PhaseDomain,)) +voltage_H = Base.Fix2(H, (domain=PhaseDomain, field=:voltage)) +current_H = Base.Fix2(H, (domain=PhaseDomain, field=:current)) +observed = ObservedResult(segment, + (gamma, Zc, H, voltage_H, current_H, Tv, Ti, velocity)) +another = PropagationParameters(segment; line_length=300.0) +observed_pair = ObservedResult.((segment, another), Ref((H, gamma))) +plot(observed; ydata=(gamma, velocity)) # all modes overlaid for one gridpoint +plot(observed_pair; ydata=(gamma,), overlay=:coordinates) +``` + +Mode vectors have a mode axis and a frequency axis. `Tv` and `Ti` have phase +rows, mode columns, and frequency samples. Retained coordinates keep both +label sets. Phase matrices have ordered phase rows and columns. `abs`, `angle`, +`real`, and `imag` can be requested through the existing request tuples. +`@observe alpha[:, :]` and `@observe Tv[:, :, :]` keep original +indices. `@observe (H, abs)[:, :]` requests the explicit magnitude component. +Complete magnitude and angle pairs are acquired separately from the default +Cartesian components. Each component has its own physical name, unit, table, +and plot family. A vector component table has frequency rows and one column +per selected mode, named `Mode i`, where `i` is the mode index. Full transformation +tables include every phase × mode entry, including off-diagonals. + +To view how each conductor participates in each mode, overlay the rows of the +transformation matrices: + +```julia +transform_matrices = observables(segment, + ((Tv, abs), (Tv, angle), (Ti, abs), (Ti, angle))) +display(report(transform_matrices)) +plot(transform_matrices; overlay=:rows, layout=(3,3)) +``` + +Each component has mode panels containing conductor curves. Panel titles name +modes. Legends name conductors. Selected modes fill each page left to right and +then top to bottom. 18 modes produce two pages per component with +`layout=(3,3)`. Every selected coefficient remains plotted. + +For a collection, acquire the same requests at each point and pass the retained +results to the same plotting call: + +```julia +requests = ((Tv, abs), (Tv, angle), (Ti, abs), (Ti, angle)) +transform_space = observables.(segment_results, Ref(requests)) +plot(transform_space; overlay=:rows, layout=(3,3)) +``` + +With `overlay=:rows`, each gridpoint starts its own figures with the same block +layout, including equivalent results and an explicit reference. Figure titles +identify the point through its compact description. Conductor styles stay +consistent across modes, pages, and points. + +Unavailable nonlinear values retain masks and reasons. Selection, tables, +plots, unit conversion, and archive loading use stored observations. Save and +load observations with the `.json` or `.jls` archive functions. Loaded selectors +identify the retained quantities and coordinates. + +`H` uses the length bound to a line segment. A phase `H` selector binds both +`domain=PhaseDomain` and +`field=:voltage` or `:current`. A phase `Zc` or `Yc` selector binds +`domain=PhaseDomain`. Exact request unit overrides take precedence over +broader selector overrides. The retained rows contain indices of modes, ordered phase +labels where applicable, source gridpoint ancestry, and relevant upstream and +modal formulation assumptions. Unknown assumptions remain unknown. + +## Manual broadband study + +The nine-cable coaxial research study is a direct, editable Julia +script under `test/manual/calculations/`. It is excluded from TestItemRunner +discovery and ordinary tests. Parse or inspect it without executing the study. +With LineCableModels and GLMakie in the active REPL project, include it from the +repository root. The existing manual plotting environment includes GLMakie: + +```sh +julia --project=test/manual/plotting +``` + +Then in that REPL: + +```julia +include("test/manual/calculations/modal_analysis.jl") +modal +line +plots +diagnostic_figure +``` + +The script keeps each computation and live GLMakie figure in a named REPL +variable. Save any retained observation explicitly after inspection if needed. Modal +order, residual coupling, and conditioning are research diagnostics, not +acceptance thresholds. diff --git a/docs/src/plotting.md b/docs/src/plotting.md new file mode 100644 index 000000000..1d7bc5832 --- /dev/null +++ b/docs/src/plotting.md @@ -0,0 +1,1434 @@ +```@meta +EditURL = "../literate/plotting.jl" +``` + +# Makie plotting: implementation guide and complete gallery + +Plot cable geometry, electrical parameters, and uncertainty results with Makie. +Each example shows the call that produces its figure. The returned `UIPlot` +contains native Makie objects that you can modify directly. + +````@example plotting +using LineCableModels +using CairoMakie +using LinearAlgebra: diag +using Measurements: measurement +using Statistics: mean +```` + +## Figures, titles, and legends + +A call that produces one figure returns a [`UIPlot`](@ref). A call that +produces several figures returns `Vector{UIPlot}`. The `figure`, `axes`, +`controls`, `legend`, `panel_legends`, and `colorbars` fields expose the +displayed Makie objects. + +### Naming titles and legends + +Layout text has one name per scope. These keywords are presentation metadata. +The result tensors and geometry tags remain unchanged. + +| Keyword | Scope | +|:--|:--| +| `figure_title` | One visible title above the whole native figure | +| `title_attributes` | Native Makie `Label` attributes for `figure_title` | +| `panel_titles` | Axis-title overrides by position, request, or panel identifier | +| `legend_title` | Heading of the controlled figure legend | +| `series_labels` | Names of the overlaid gridpoints or physical coordinates. These are the legend entries | +| `series_attributes` | Native Makie attributes for all overlaid curves, or one named tuple per curve | +| `legend_position` | `:inside`, a named outer dock, or a positive dock grid position | +| `legend_attributes` | Native `halign`/`valign`, orientation, banks, fonts, and padding | + +Some established recipes also accept `title` for the window or exported file, +or as the heading of one recipe. Use the explicit scoped names above when composing a +dashboard. +Benchmark windows default to `case ID — quantity`, followed by block indices +when split. Deterministic, mean ± std, and explicit-statistic comparisons use +this same rule. An explicit `title` overrides the default case prefix. Subplot +titles and `figure_title` remain independent. + +`series_attributes` uses the shared PlotBuilder controls for matrix and benchmark +plots, observed results, statistical plots, and geometry previews. +A named tuple applies to every group. A tuple or vector of named tuples styles +each group separately. +`series_attributes=((marker=:circle, markersize=8), (;), (linestyle=:dash,))` +adds markers to the first series, keeps the second's defaults and dashes the +third. Styles apply across facets and pages, including their legends and +visibility controls. Attributes must be supported by the group's native plots. +In `plotwindow`, groups follow native plot insertion order across its axes. + +The same scopes are mutable after construction. `figuretitle!` and +`paneltitle!` replace titles. `figurelegend!` and `panellegend!` rebuild a +native legend from the plots in each visibility group, preserving grouped +visibility controls when labels change. + +### Observation, facet, page, and legend semantics + +`LineParameters` owns dense ``Z`` and ``Y`` tensors. The observation grammar +is what turns those tensors into displayable physical quantities: ``Z`` +expands to ``R`` then ``X``. ``Y`` expands to ``G`` then ``B``. An exact +request such as `@observe Z[1,1,:]` keeps its conductor coordinates while +selecting the complete frequency range. Plotting consumes that resolved +request and displays both components. +The plot-facing name for this ordinate selection is `ydata`. Pass it +positionally or as a keyword, such as `plot(result; ydata=(R, L))`. + +With matrix gridpoint overlays, every `(quantity, row, column)` has one axis. +Its default title uses `Units.label` and the retained coordinate labels, such as `Self series resistance — conductor 1` or +`Mutual series reactance — conductor 1 → 2`. With gridpoint overlays, +coordinates identify panels and legends distinguish result sets. With +coordinate overlays, each selected coordinate is a labelled curve. One +unlabeled curve has no legend by default. + +`layout` is the only nominal panel capacity. Every selected quantity or +statistical meaning has its own figure family. Omitted layout chooses capacity +independently for each family. An explicit `(rows, columns)` sets the same +nominal capacity for all families. + +| `layout` | Page grouping | +|:--|:--| +| `nothing` | Each family's selected matrix span, or a near-square flow capacity | +| `(1, 1)` | One selected panel per figure, per quantity | +| `(1, 2)` | Up to two panels in one row per page | +| `(2, 1)` | Up to two panels in one column per page | +| `(N, N)` | Up to `N²` panels per figure, per quantity | + +Layout follows observation selection. It cannot add an excluded coefficient or +merge different quantities. `overlay` controls the panel and curve dimensions: + +| `overlay` | Panels | Curves | +|:--|:--|:--| +| `:auto` | Mode panels for several gridpoints or a larger explicit layout. Matrix coefficient panels otherwise | Gridpoints | +| `:auto` for one modal vector with omitted layout or `(1,1)` | One panel per component | Selected modes | +| `:gridpoints` | Selected coordinates | Gridpoints | +| `:coordinates` | Selected gridpoints | Selected coordinates | +| `:rows` (matrices) | Selected columns, with separate figures per gridpoint | Selected rows | + +Filtering occurs before this choice. An explicit reference counts as another +gridpoint. Gridpoint overlays retain equivalent-result grouping. Coordinate +overlays retain a panel for every selected point. Tuple/vector labels and styles +address the overlaid dimension, so incompatible mixed families need separate +calls. A shared style NamedTuple applies to every curve. + +`overlay=:rows` also retains every selected point, including equivalent results +and a reference, in its own figure sequence. For Tv/Ti, columns are modes and +rows are conductors: each panel title identifies its mode and its legend lists +conductors. Figure titles use compact gridpoint descriptions. Other matrices +use the same operation with their own physical row and column labels. + +Row-overlay panels follow selected column order, left to right and then top to +bottom. `layout=(r,c)` applies the same block capacity to each point. Pagination +restarts at each point, even when the previous page has empty cells. Automatic +capacity uses the selected column count per point. Panel titles and controls +accept `(original_point_position, original_column)` addresses. An explicit +reference uses its appended input position. Row colors and markers remain +consistent across columns, points, references and reordered selections. +Positional `series_labels` and `series_attributes` follow selected row order. +Use separate calls for matrix and vector quantities with this explicit option. + +### Matrix pagination and overlays + +`plot(results; ydata=(R,L), layout=(2,2))` produces four R pages and four L +pages for a full 3×3 matrix. Their actual extents are 2×2, 2×1, 1×2, and 1×1. +Pages follow request order and then row-major matrix-page order. Original +coefficient identities remain the addresses for titles, legends, scales, and reset. +Positional `panel_titles` bind to the complete selected population before paging. + +Automatic matrix pages cover the selected original-coordinate rectangle. Consider this example: selecting columns 2 and 3 gives a two-column overview without splitting +at the original block edge. Explicit capacities keep blocks anchored at +original coordinate (1,1). Both remove unselected exterior tracks and retain +internal holes: selecting (1,1) and (1,3) still spans three columns. + +Native decoration measurements establish equal initial data frames. `fig_size` +is the reference size for the nominal capacity. `figure.size` takes precedence. +Legends use at most the fraction specified by `legend_cap=0.5` +of their associated data area. Extra entries become `(...)` within that +allocation. All curves remain plotted. +Each finished window fits its actual occupied panels, titles, guides, and +enabled controls. A singleton with explicit `layout=(1,2)` retains the smaller +frame from that two-column reference, without allocating an empty second cell. +Each page can be resized independently in both dimensions. +Explicit diagonal observations and preview collections use the same capacity, +preserving their source order and original identities in compact flow pages. + +Result overlays use solid lines with sparse, staggered markers on saved +sample points: black curves and hollow circles for references, colored curves +and filled shapes for results. References may themselves include uncertainty. +Default routes and explicit implementations have the same styling semantics. +Deterministic references mark both endpoints. +Colors and marker identities remain stable +when formulations are filtered. No curves are merged because they agree. +Use `series_attributes=(marker=nothing,)` for lines only, or an explicit native +`marker` for all-sample placement. References remain comparison operands, not +declarations of physical truth. + +With multiple displayed series, `errorbar_sampling=:staggered` selects sparse error +bars at retained samples separately from automatic markers. Both references +and results retain their full mean curves. Coordinates, uncertainties, +comparison calculations, and full-data axis limits are unchanged. X and Y +intervals use the same indices. When very few samples are available, intervals +take priority over conflicting automatic markers. The legend retains identity. +Use `errorbar_sampling=:all` to inspect every interval, with automatic markers +omitted on uncertain series. One displayed series uses `:all` by default. +Explicit native markers still use +all samples. Native `whiskerwidth` and `linewidth` overrides take priority. +Sparse glyphs are an overview: use full intervals or separate standard-deviation +curves to inspect uncertainty variation. Mean ± std is not mean ± standard error. +Many exactly coincident methods cannot all remain distinguishable at finite +screen resolution. Use legend visibility to inspect them separately. + +Within one identified physical point, observation-owned groups may share a trace +for equivalent quantity-relevant selections: impedance choices on Z/R/L/X pages, +admittance choices on Y/G/C/B pages. Every relevant composite route, control, +coordinate and uncertainty meaning participates. Equal curves or descriptions +alone never merge cases. Different physical points remain separate. Conflicting +observations under the same selection raise an error. Saved results remain intact. +Default labels show only relevant differences across results and reference, +omitting common physical inputs and individual controls. Names are captured from +owner-dispatched `description(...; compact=true)` methods used by report tables: +`FEM (reference)`, `PSCAD (reference)`, `Monte Carlo (reference)`, and results +such as `LEP` or `earth Z=Saad`, without result numbering. +`formulations=[3,1]` selects recorded original formulation identities, preserving +order and colors. Explicit `series_labels` and styles follow the retained source. +When nothing varies, labels use the applicable compact owner description. +Automatic results are chromatic. The separate reference is black. +Full scientific explanations and common settings remain in `formula_details`. + +Top/bottom legends fit a measured row-major grid and wrap long labels without +dropping fields. Explicit `legend_attributes=(orientation=..., nbanks=...)` +retains native manual layout control. + +## Gallery data + +The frequency responses are deliberately small but non-constant so that +logarithmic axes, engineering units, legends, and automatic limits are all +visible in the generated documentation. + +````@example plotting +frequency = collect(10.0 .^ range(1, 4; length = 24)); +angular_frequency = reshape(2π .* frequency, 1, 1, :); +resistance = cat( + ( + [1.0 0.22; 0.22 1.8] .* 1.0e-4 .* (1 + 0.12 * log10(f / first(frequency))) + for f in frequency + )...; + dims = 3); +inductance = cat( + ( + [2.0 0.28; 0.28 2.5] .* 1.0e-7 .* (1 - 0.04 * log10(f / first(frequency))) + for f in frequency + )...; + dims = 3); +conductance = cat( + ( + [3.0 -0.45; -0.45 4.0] .* 1.0e-9 .* (1 + 0.08 * log10(f / first(frequency))) + for f in frequency + )...; + dims = 3); +capacitance = repeat([4.0 -0.7; -0.7 5.0] .* 1.0e-10, 1, 1, length(frequency)); +parameters = LineParameters( + complex.(resistance, inductance .* angular_frequency), + complex.(conductance, capacitance .* angular_frequency), + frequency +); +nothing #hide +```` + +The geometry gallery uses the example library included in the repository. +A system is assembled from two placements without introducing a plotting-only +geometry representation. + +````@example plotting +cable_library = CablesLibrary(); +LineCableModels.load!( + cable_library; + file_name = joinpath(pkgdir(LineCableModels), "examples", "cables_library.json") +); +mv_design = cable_library["18kV_1000mm2"]; +hv_design = cable_library["525kV_1600mm2"]; +earth = EarthModel(100.0, 10.0, 1.0); +cable_system = build( + LineCableSystem, + [mv_design, mv_design], + [(-0.06, -0.20), (0.06, -0.20)]; + environment = earth, + system_id = "two-cable-gallery", + line_length = 1_000.0 +); +nothing #hide +```` + +The following synthetic samples, moments, and histograms illustrate the +UQ plotting methods. + +````@example plotting +retained_samples = ( + R = reshape([2.0, 3.0, 5.0, 8.0] .* 1e-4, 1, :), + L = reshape([11.0, 13.0, 17.0, 19.0] .* 1e-7, 1, :), + C = reshape([23.0, 29.0, 31.0, 37.0] .* 1e-11, 1, :), + G = reshape([41.0, 43.0, 47.0, 53.0] .* 1e-10, 1, :) +); +retained_statistics = map(x -> [SampleSummary(vec(x))], retained_samples); +retained_histograms = map(x -> [HistogramDensity(vec(x); bins = 2)], retained_samples); +retained_core = CableConstants(mean(retained_samples.R), mean(retained_samples.L), + mean(retained_samples.C), mean(retained_samples.G)); +retained_core = LineCableModels.materialize(retained_core, retained_statistics); +mc_formulation = MonteCarlo(Formulation(); trials = 4, seed = 41, + return_samples = true, return_histograms = true); +mc_result = MonteCarloResult(mc_formulation, [retained_core], [retained_statistics], + [retained_samples], [retained_histograms], UInt64(41), UInt64[42], [4]); +nothing #hide +```` + +## Line-parameter recipes + +### Complete default view + +`Makie.plot(parameters)` is the minimal call. It observes everything in the +order ``Z`` then ``Y``, expands those families to ``R``, ``X``, +``G``, and ``B``, and returns four matrix-dashboard pages. For this 2×2 +result, every page contains four axes. The calls below render the full default +gallery in exactly that order. + +````@example plotting +default_line_pages = Makie.plot( + parameters; + backend = :cairo, # choose the already-loaded native backend + display_plot = false, # Documenter owns display. Interactive use may omit this + controls = false, # omit toolbar chrome from this static gallery + xscale = :log10, # initial scale. Interactive controls may change it + fig_size = (900, 680) # reference size of the nominal capacity +) +default_line_pages[1].figure #hide +default_line_pages[2].figure #hide +default_line_pages[3].figure #hide +default_line_pages[4].figure #hide +```` + +To change only appearance, mutate the returned native axes. To change which +physical values exist, pass an observation selector or exact `@observe` +request. To change pagination, pass one of the layouts in the table above. + +### Cartesian series impedance + +This exact observation selects the self impedance of conductor 1 over the +full frequency range. `ObservedResult` construction expands ``Z`` into resistance and +reactance. They remain separate quantity figures, even with a multi-panel layout. + +````@example plotting +# `xscale=:log10` is an initial state. The live toolbar can still change it. +# `figure_title` labels each figure. `panel_titles` follows the selected quantity order. +# `series_labels` names result sets, so supplying one explicitly opts into a legend. +# `:inside` overlays the logical frame region with native alignment attributes. +series_cartesian = Makie.plot( + parameters, + @observe Z[1, 1, :]; # self impedance, conductor 1, every frequency + backend = :cairo, + display_plot = false, + controls = false, + xscale = :log10, + fig_size = (1000, 500), + figure_title = "Conductor 1 self impedance", + panel_titles = ("Resistance", "Reactance"), + legend_title = "Result set", + series_labels = ("reference",), + legend_position = :inside, + legend_attributes = (; + halign = :right, valign = :bottom, backgroundcolor = (:white, 0.92)), + legend_cap = 0.5 +) +series_cartesian[1].figure #hide +```` + +````@example plotting +series_cartesian[2].figure #hide +```` + +To modify this recipe, select other matrix coordinates with an explicit +observation request, pass native legend attributes, or mutate either returned +axis. `series_cartesian[1].axes[1].title[] = "Measured resistance"` +changes the live title without asking LineCableModels to rebuild anything. + +### Cartesian shunt admittance + +Conductance and susceptance are the real and imaginary parts of ``Y``. They +use the same dashboard implementation, retained units, limits, legend +groups, and controls as impedance. Only the scientific requests differ. + +````@example plotting +# A bottom dock is horizontal by default and remains a native Makie Legend. +shunt_cartesian = Makie.plot( + parameters, + @observe Y[1, 1, :]; + backend = :cairo, + display_plot = false, + controls = false, + xscale = :log10, + fig_size = (900, 480), + panel_titles = Dict(:G => "Self conductance", :B => "Self susceptance") +) +shunt_cartesian[1].figure #hide +```` + +````@example plotting +shunt_cartesian[2].figure #hide +```` + +Any accepted Makie `Legend` keyword belongs in `legend_attributes`. For a +labeled source or comparison, moving the +block uses `legend_position = :inside`, `:left`, `:right`, `:top`, `:bottom`, +or a positive `(row, column)` dock coordinate. `(2, 2)` remains the plot +canvas. Native `halign` and `valign` also position inside legends. + +### Figure-wide and panel-scoped legends + +A controlled comparison registers one group per result set, then exposes two +views of those groups. The figure legend spans every panel. A panel legend +filters the same source handles to one logical plot position. Hiding a global +result entry changes that source's visibility across both resistance coefficients. + +````@example plotting +legend_scope_demo = Makie.plot( + parameters, + parameters, + @observe R[1, 1:2, :]; + series_labels = ("reference", "result"), + backend = :cairo, + display_plot = false, + controls = false, + xscale = :log10, + fig_size = (1100, 600), + figure_title = "Resistance dashboard", + panel_titles = ("Self resistance", "Mutual resistance"), + legend_position = nothing +) +```` + +A figure-scoped legend can be placed in any addon dock and given any native +Makie Legend attributes. + +````@example plotting +figurelegend!( + legend_scope_demo; + position = :top, + title = "Result set", + legend_labels = Dict( + :result_1 => "baseline", + :result_2 => "alternative" + ), + orientation = :horizontal, + nbanks = 2, + max_fraction = 0.5 +) +```` + +Logical position `(1, 1)` is the resistance panel. + +````@example plotting +panellegend!( + legend_scope_demo, + (1, 1); + position = :inside, + halign = :left, valign = :bottom, + title = "Resistance result", + legend_labels = ("base R", "alternative R"), + backgroundcolor = (:white, 0.92), + max_fraction = 0.5 +) +```` + +Titles use the same logical panel address as panel legends. + +````@example plotting +figuretitle!(legend_scope_demo, "Resistance dashboard"; fontsize = 20) +paneltitle!(legend_scope_demo, (1, 2), "Mutual resistance") +legend_scope_demo.figure #hide +```` + +The same panel legends can be requested at construction time with +`panel_legends=Dict((1, 1) => (position=:inside, halign=:left, valign=:bottom, +legend_labels=("base R", "alternative R")))`. Runtime dictionaries target +stable source keys such as `:result_1`. + +### Derived R/L/G/C dashboard + +`L` and `C` are observation-owned proxies derived from reactance or susceptance +and angular frequency. Their DC samples are retained as unavailable. Independently +valid components remain available. The observation owner assigns their physical +units. For this 2×2 result, `layout=(2,2)` requests one matrix dashboard per +physical quantity. + +````@example plotting +# `layout` sets the initial panel grid. The returned Makie objects remain editable. +rlgc_dashboards = Makie.plot( + parameters, + (R, L, G, C); + backend = :cairo, + display_plot = false, + controls = false, + xscale = :log10, + layout = (2, 2), + fig_size = (950, 700), + legend_position = nothing +) +rlgc_dashboards[1].figure #hide +rlgc_dashboards[2].figure #hide +rlgc_dashboards[3].figure #hide +rlgc_dashboards[4].figure #hide +```` + +With `layout=(1,1)`, each quantity and coordinate facet has its own figure. + +### Polar impedance and admittance + +Function selectors are resolved through the same observation grammar. +`abs` and `angle` publish magnitude and phase for both ``Z`` and ``Y``. + +````@example plotting +# The 4 transformed quantities become 4 matrix dashboards. +polar_dashboards = Makie.plot( + parameters, + (abs, angle); + backend = :cairo, + display_plot = false, + controls = false, + xscale = :log10, + layout = (2, 2), + fig_size = (950, 700), + legend_position = nothing +) +polar_dashboards[1].figure #hide +polar_dashboards[2].figure #hide +polar_dashboards[3].figure #hide +polar_dashboards[4].figure #hide +```` + +`real`, `imag`, `Z`, and `Y` are also valid selectors. Implement additional +scientific transforms in the observation grammar. + +### Standalone series-impedance result + +`SeriesImpedance` can be plotted before it is bundled into `LineParameters`. +Supply its frequency samples as an explicit positional argument. The same +native dashboard controls are available. + +````@example plotting +standalone_impedance = Makie.plot( + parameters.Z, + parameters.f, + (Z, 1, 1, Colon()); + backend = :cairo, + display_plot = false, + controls = false, + xscale = :log10, + fig_size = (900, 440), + legend_cap = 0.5 +) +standalone_impedance[1].figure #hide +```` + +````@example plotting +standalone_impedance[2].figure #hide +```` + +### Standalone shunt-admittance result + +`ShuntAdmittance` follows the same rule: frequencies are explicit and its +default scientific family is `(G, B)`. + +````@example plotting +standalone_admittance = Makie.plot( + parameters.Y, + parameters.f, + (Y, 1, 1, Colon()); + backend = :cairo, + display_plot = false, + controls = false, + xscale = :log10, + fig_size = (900, 440), + legend_cap = 0.5 +) +standalone_admittance[1].figure #hide +```` + +````@example plotting +standalone_admittance[2].figure #hide +```` + +### Exact observation requests and modal coordinates + +An `@observe` request is the precise extension point for matrix coordinates. +It passes through `Commons.observation_request`, so selection rules stay shared +by plotting, tables, and reports. This example keeps only one diagonal entry +from each quantity. + +````@example plotting +resistance_request = @observe R[1, 1, :]; +inductance_request = @observe L[1, 1, :]; +selected_response = Makie.plot( + parameters, + (resistance_request, inductance_request); + backend = :cairo, + display_plot = false, + controls = false, + xscale = :log10, + fig_size = (900, 420), + legend_cap = 0.5 +) +selected_response[1].figure #hide +```` + +````@example plotting +selected_response[2].figure #hide +```` + +### An explicit capacity for one selected coefficient + +The previous call uses an automatic 1×1 reference for each quantity. This call +intentionally uses a two-column reference. Its one selected panel keeps the +corresponding cell size. The returned figure fits around that panel. + +````@example plotting +capacity_example = Makie.plot( + parameters, resistance_request; + backend = :cairo, display_plot = false, controls = false, + layout = (1, 2), fig_size = (900, 420) +) +capacity_example.figure #hide +```` + +### Family requests and a vertical capacity + +The observation owner expands `Z[1,1,:]` into retained R and X products. +`layout=(2,1)` gives each quantity its own nominal two-row capacity. It never +puts different quantities into the same figure. Only selected original panels +are drawn, and unselected exterior rows are not allocated. + +````@example plotting +self_impedance_request = @observe Z[1, 1, :]; +stacked_self_impedance = Makie.plot( + parameters, + self_impedance_request; + backend = :cairo, + display_plot = false, + controls = false, + xscale = :log10, + layout = (2, 1), # two-row nominal capacity per quantity + panel_titles = ("Self resistance", "Self reactance"), + legend_position = :inside, # native overlay. Use any outer dock instead + legend_attributes = (halign = :right, valign = :top), + legend_title = "Result set", + series_labels = ("solution",), # explicitly opt into a one-source legend + legend_cap = 0.5, + fig_size = (720, 720) +) +stacked_self_impedance[1].figure #hide +```` + +````@example plotting +stacked_self_impedance[2].figure #hide +```` + +With the default matrix arrangement, select more coordinates to add panels, +more result containers to add curves or more quantities to add figure families. `overlay=:coordinates` instead makes selected coordinates the +curves and selected gridpoints the panels. + +Modal results retain their physical domain. Use `diag` to select the diagonal +coefficients, as in the following inductance plot. + +````@example plotting +modal_parameters = compute( + ModalAnalysisProblem(parameters), + ModalAnalysisFormulation(:default); + options = (offdiagonal_tolerance = 1.0,) +); +modal_inductance = Makie.plot( + modal_parameters, + (@observe((L, diag)[:, :]),); + backend = :cairo, + display_plot = false, + controls = false, + xscale = :log10, + fig_size = (820, 430), + legend_cap = 0.5 +) +modal_inductance.figure #hide +```` + +Extend coordinate behavior in the grammar and domain owner. Extend only visual +appearance by mutating the returned axes or forwarding Makie plot attributes. + +### Measurement uncertainty + +Uncertain retained values draw native intervals around their nominal curves. +Full uncertainty support participates in limits even when intervals are sparse. +`ObservedResult` classifies engineering zero using absolute nominal magnitude +and owner-defined cutoffs. With `clip=true`, values within those cutoffs +become exact zero with zero uncertainty. Phase unavailability and +linked X/L or B/C thresholds are resolved when observations are acquired. +Plotting uses the resulting values and the stored RMS errors directly. +`clip=false` and `atol` are raw acquisition options. Compatible display units +also work directly on retained inputs: `plot(observed; length_unit=:base)`. +Plotting delegates unit conversion to ObservedResult, preserving the recorded +clipping decisions and saved RMS values and units. + +Legend actions hide or restore the nominal line, its markers and its x/y +error bars together. Figure legends act across the figure. Panel legends act +only on their panel. This also applies to overlays and report illustrations. +Native error-bar handles remain independently editable. The next action to +hide or show the series restores the complete set of components. + +````@example plotting +measured_parameters = LineParameters( + complex.( + measurement.(resistance, 0.05 .* resistance), + measurement.(inductance .* angular_frequency, + 0.05 .* inductance .* angular_frequency) + ), + complex.( + measurement.(conductance, abs.(0.05 .* conductance)), + measurement.(capacitance .* angular_frequency, + abs.(0.05 .* capacitance .* angular_frequency)) + ), + frequency +); +uncertainty_plot = Makie.plot( + measured_parameters, + (@observe(R[1, 1, :]), @observe(L[1, 1, :])); + backend = :cairo, + display_plot = false, + controls = false, + xscale = :log10, + fig_size = (900, 440), + legend_cap = 0.5 +) +uncertainty_plot[1].figure #hide +```` + +````@example plotting +uncertainty_plot[2].figure #hide +```` + +The scalar type defines how to extract nominal values and errors for plotting. + +### Comparing completed line results + +A named tuple supplies both completed results and default legend labels. The +observation owner validates usable units and coordinates. Plotting overlays sources in one axis for every selected matrix position. Each +result may retain its own frequency samples. + +````@example plotting +result = LineParameters( + parameters.Z.values .* (1.06 + 0im), + parameters.Y.values .* (0.94 + 0im), + parameters.f +); +comparison_plot = Makie.plot( + (; reference = parameters, result), + (R,); + backend = :cairo, + display_plot = false, + controls = false, + xscale = :log10, + layout = (2, 2), + fig_size = (900, 700), + legend_position = :top, + legend_attributes = (; orientation = :horizontal), + legend_cap = 0.5 +) +comparison_plot.figure #hide +```` + +The exact-request positional form is +`Makie.plot(reference, result, @observe(Z[1,1,:]); +series_labels=("reference", "result"))`. +Change line styling after construction through the native plot objects in each +axis. Change source identity through `series_labels` or named-tuple keys. + +### Detached observed results + +`ObservedResult` retains complete primary representations and their original +matrix coordinates. The existing matrix renderer selects these records for +display. Report illustrations use the same observed-input plotting method. + +````@example plotting +observed = ObservedResult( + parameters, + (resistance_request, inductance_request) +); +observed_plot = Makie.plot( + observed; + ydata = (resistance_request, inductance_request), + title = "Retained coefficient observations", + figure_title = "Retained coefficient observations", + panel_titles = ("R[1,1]", "L[1,1]"), + backend = :cairo, + display_plot = false, + controls = false, + layout = (1, 2), + fig_size = (900, 480), + legend_title = "Observed result", + # R and L retain the same original coefficient in separate figures. + series_labels = ("self impedance",), + legend_position = :bottom, + legend_attributes = (; orientation = :horizontal), + legend_cap = 0.5 +) +observed_plot[1].figure #hide +```` + +````@example plotting +observed_plot[2].figure #hide +```` + +A new primary result owner supplies observation methods. The same retained +quantity records reuse table and plot consumption. + +## Geometry preview recipes + +### Cable-design cross-section + +`DataModel.preview_shapes` exposes detached physical polygons with only their +material and construction tag. The Makie preview adapter derives optional +presentation groups. `_preview_axis!` draws the polygons with native +`poly!` and locks the axis to `DataAspect`. It also computes +geometry limits. Preview `size` is an initial reference allocation. The shared +shell preserves physical frame sizes and view limits, then fits the window +around the panels and their complete decorations. A circle or sector row can +retain necessary space beside its shorter panel. A row of circles need not +retain unused vertical allocation. The same fitted native `.figure` is returned +for interactive display and documentation. + +````@example plotting +# `display_id=true` promotes the design identifier into the panel title. +# Preview scales form a horizontal strip below the plot by default, +# with each property label on the left. +cable_preview = preview( + mv_design; + backend = :cairo, + display_plot = false, + controls = false, + display_id = true, + size = (950, 700), + # Group the physical core-wire regions into one presentation entry. + # Geometry tags, terminals, and electrical construction remain untouched. + legend_group = region -> region.source.tag === :wire ? + :stranded_core : region.source.tag, + legend_labels = Dict(:stranded_core => "Stranded core"), + legend_position = :right, + legend_attributes = (; nbanks = 2) +) +cable_preview.figure #hide +```` + +`legend_group` accepts either the shown callback over a `PlacedRegion` or a +tag-to-group dictionary. `legend_labels` maps the resulting presentation group +to text. This keeps display grouping independent of physical tags. For detailed +annotation, mutate `cable_preview.axes[1]` and add ordinary Makie plots. Each +object in `cable_preview.colorbars` is a native `Colorbar`. +All preview routes use `guide_gap=(8,8,24,8)` in left, right, bottom, top order. +The 24-pixel bottom clearance separates the axis label from the scale strip. +Pass a scalar or four-sided `guide_gap` to change that clearance. + +### Independent scale arrangement and guide spacing + +Bar orientation, group cells, and placement are independent. This uses the +`mv_design` constructed above. Physical properties and material ranges remain unchanged. +The 3 horizontal bars form one row below the preview directly. + +````@example plotting +design = mv_design +scale_arrangement = preview( + design; + backend = :cairo, display_plot = false, controls = false, + size = (1200, 900), + legend_position = :right, + legend_attributes = (halign = :left, valign = :top), + colorbar_position = :bottom, + colorbar_attributes = (vertical = false, width = 160, height = 14), + colorbar_group_attributes = (layout = (1, 3), colgap = 16, halign = :center), + guide_spacing = (rowgap = 14, colgap = 16) +) +scale_arrangement.figure #hide +```` + +Move the same native objects to a column on the right. The legend-to-group +minimum of 14 logical pixels differs from the 12-pixel internal row gap. + +````@example plotting +figurecolorbars!(scale_arrangement; + position = :right, + group_attributes = (layout = (3, 1), rowgap = 12, valign = :bottom), + vertical = false +) +figurelegend!(scale_arrangement; position = :right, valign = :top, guide_spacing = 14) +scale_arrangement.figure #hide +```` + +Gaps separate complete visible siblings, including labels and endpoint ticks. +Bars in a column share their left and right edges. Bars in a row share their +baseline. Different label widths and fonts change the reserved decoration +space, not those alignments. `halign` moves the complete group once. +Figure padding, plot clearance (`guide_gap`), legend-entry spacing, and bar +dimensions have separate controls. Hidden scales keep their native handles +for restoration and release their occupied space. + +Omitted group settings survive live updates. `layout=nothing` restores one +row at top and bottom or one column at left and right and explicit side-grid slots. +Standalone scales in main content default to one column. Explicit capacity +fills row-major and omits unoccupied tracks within the colorbar group. +`rowgap=nothing` and `colgap=nothing` restore inheritance from `guide_spacing`. +A partial live `guide_spacing=(rowgap=18,)` preserves the current column gap. +Omitted constructor components use 12. Fractions and zero are valid, while +negative, nonfinite and Boolean gaps are rejected. Spacing is counted only +between neighbors, so one guide has zero exterior padding. + +### Cable-design collection + +A collection preview repeats the same detached geometry path for every design +and assigns the caller-requested layout. The material ranges are aggregated +once. Each panel then uses comparable colors and one shared set of scales. +Insulating regions include sparse diagonal marks over their existing material +colors. Pass `display_dielectric_pattern=false` to omit these marks in a +design, collection, or system preview. Semicon and conductor fills retain +their own material colors. + +````@example plotting +design_collection = preview( + [mv_design, hv_design, hv_design, mv_design]; + layout = (2, 2), + backend = :cairo, + display_plot = false, + controls = false, + size = (1000, 850), + figure_title = "Cable design family", + panel_titles = ("MV option A", "HV option A", "HV option B", "MV option B") +) +design_collection.figure #hide +```` + +Use any sufficient `(rows, columns)` layout. The default omits layer legends. +Request selected local legends with `panel_legends`, using the same logical +grid-position rules as line dashboards. + +### Cable-system cross-section + +A system preview resolves each placed region into the system frame and adds reference geometry such as the earth interface. Limits are derived from the physical placement or `zoom_factor`. Earth properties use the same atomic +color-scheme rules as cable materials, with a separate logarithmic +resistivity palette: slate at 0.1 Ω·m, taupe at 100 Ω·m, and ochre at 10⁴ Ω·m. +Horizontal earth fills follow pan, zoom and figure resizing while interfaces +stay at their physical depths. A semi-infinite basement covers the remainder +of the view. A finite final layer retains its declared bottom. + +````@example plotting +system_preview = preview( + cable_system; + earth_model = earth, + zoom_factor = 1.35, + backend = :cairo, + display_plot = false, + controls = false, + display_id = true, + size = (1000, 700), + legend_position = :right +) +system_preview.figure #hide +```` + +Modify physical contents before previewing. Modify visual annotations after +previewing. `zoom_factor` changes only the initial view, and the reset control +returns to those computed limits. +The light blue sky fades from the upper axis limit to transparency at `z=0`, +stretching with the view and disappearing in entirely underground views. +It is a visual cue with no material-property meaning. Use +`display_surface_gradient=false` to disable it independently of earth colors. +Vertical strata are currently not rendered in the system preview. + +## Material colors and native colorbars + +### One reusable material scheme + +A color scheme is the atom. `material_property_ranges` obtains values from a +design, a design collection, or the material defaults. `materialcolors` +turns exactly one property and range into a Makie-compatible named tuple: +`label`, `colormap`, `limits`, and `ticks`. `materialscale!` places exactly +that one scheme wherever the caller chooses. + +````@example plotting +material_ranges = LineCableModels.DataModel.material_property_ranges(mv_design); +rho_scheme = materialcolors( + :rho, + material_ranges.rho +); +rho_scale_figure = Figure(size = (850, 180), figure_padding = 24) +materialscale!( + rho_scale_figure[1, 1], + rho_scheme; + vertical = false, + width = Relative(0.85) +) +rho_scale_figure +```` + +The scheme contains no placement. Put that `Colorbar` in any `GridPosition`, +combine it with a heatmap, or reuse the same scheme in another figure. Define a +new property by defining another palette producer next to `materialcolors`. +Physical range collection remains a DataModel concern. +Relative permeability uses the existing logarithmic range from 1 to 300. +Unity leaves the base color unchanged. Indigo tint becomes visible before +the progression toward magenta at high permeability. The permeability scale +shows that same tint over a neutral reference color. +Dielectric marks are native Makie pattern tiles. Cairo embeds the small +bitmap tiles in SVG/PDF output while preserving the surrounding geometry. + +### High-level material-scale reference + +`show_material_scale` is a preview option that combines the three +default property schemes and returns three independent native colorbars. + +````@example plotting +material_scale = show_material_scale( + backend = :cairo, + display_plot = false, + controls = false, + size = (850, 360), + figure_title = "Reusable material-property schemes", + colorbar_attributes = (; vertical = false) +) +material_scale.figure #hide +```` + +For one property, use the preceding `materialscale!(position, scheme)` pattern. +For a different high-level combination, compose schemes in the caller's own +Makie layout. + +## Monte Carlo result recipes + +Monte Carlo methods first publish the requested marginal from retained +samples, `HistogramDensity`, or both. Cable-constant requests accept `R`, `L`, +`C`, `G`, or `(selector, assembly)`. Matrix-valued line results require an +exact request such as `@observe R[1, 1, 3]`. Native Makie function identity +chooses the visual primitive. + +### Sample histogram + +`Makie.hist` uses retained samples and calls native `hist!`. `bins` and +`normalization` retain their Makie meanings. Unit options are consumed while +publishing the marginal. + +````@example plotting +sample_histogram = Makie.hist( + mc_result, + R; + bins = 12, + normalization = :pdf, + backend = :cairo, + display_plot = false, + controls = false, + fig_size = (820, 400), + figure_title = "Retained Monte Carlo samples", + legend_cap = 0.5, + color = :steelblue +) +sample_histogram.figure #hide +```` + +Pass ordinary `hist!` attributes such as `color`, `strokewidth`, or +`transparency` through the remaining keywords. Use `(R, assembly)` when a +`CableConstants` result contains more than one assembly. + +### Retained-model probability density + +`Makie.stairs` consumes the retained histogram model and calls native +`stairs!` with post-step edges. + +````@example plotting +model_density = Makie.stairs( + mc_result, + R; + backend = :cairo, + display_plot = false, + controls = false, + fig_size = (820, 400), + legend_cap = 0.5, + color = :darkorange, + linewidth = 3 +) +model_density.figure #hide +```` + +With `bins=n`, a different binning is derived from retained samples. The +stored model is unchanged. Changing its bin count requires retained samples. +With `bins=nothing` (the default), the retained model is reused, or a model +is derived with automatic binning if only samples were retained. Constant +samples always produce one finite-width bin. Use native `stairs!` keywords +for appearance. + +### Empirical cumulative distribution + +`Makie.ecdfplot` consumes retained samples and delegates the empirical curve +to Makie's `ecdfplot!` recipe. + +````@example plotting +empirical_cdf = Makie.ecdfplot( + mc_result, + R; + backend = :cairo, + display_plot = false, + controls = false, + fig_size = (820, 400), + legend_cap = 0.5, + color = :seagreen, + linewidth = 3 +) +empirical_cdf.figure #hide +```` + +The returned axis can be combined with confidence bands or additional native +curves. The addon only defines the initial empirical series and its legend group. + +### Retained-model cumulative distribution + +Raw `Makie.lines` first acquires the UQ-owned CDF product. Its observed method +draws the retained CDF coordinates with `lines!`. + +````@example plotting +model_cdf = Makie.lines( + mc_result, + R; + backend = :cairo, + display_plot = false, + controls = false, + fig_size = (820, 400), + legend_cap = 0.5, + color = :firebrick, + linewidth = 3 +) +model_cdf.figure #hide +```` + +Increase visual resolution by extending the owner-side model grid settings. Add +purely visual reference curves directly to `model_cdf.axes[1]`. + +### Sample/model Q-Q plot + +`Makie.qqplot` requests both retained products. UQ calculates matching quantile pairs for the native scatter plot. The identity reference line is optional. + +````@example plotting +quantile_plot = Makie.qqplot( + mc_result, + R; + qqline = :identity, + backend = :cairo, + display_plot = false, + controls = false, + fig_size = (820, 440), + panel_titles = ("Sample versus retained model",), + legend_title = "Q–Q elements", + legend_labels = ("sample quantiles", "identity reference"), + legend_position = :inside, + legend_attributes = (; halign = :left, valign = :top, backgroundcolor = (:white, 0.92)), + legend_cap = 0.5, + color = :purple, + markersize = 10 +) +quantile_plot.figure #hide +```` + +Set `qqline=:none` to remove the reference. Other scatter attributes are +forwarded to `scatter!`. Data and units remain observation concerns. + +## Callable controls and retained assembly points + +A widget callable receives the final live handle once per figure. Its native +callback uses the same status and frame-preserving shell operations as built-ins. +`controls=false` skips standard and custom widgets. Closing a display window +leaves the retained handle editable, exportable, and redisplayable. + +````@example plotting +function add_reset_y!(p) + addwidget!((plot, cell) -> Button(cell; label = "Reset Y"), p, :reset_y; + event = button -> button.clicks, + callback = (plot, _) -> resetview!(plot; x = false, y = true), + success = "Y view reset") + nothing +end +widget_plot=Makie.plot(parameters; ydata = ((R, 1, 1, :),), layout = (1, 1), + backend = :cairo, display_plot = false, widgets = (add_reset_y!,)); +widget_plot.figure #hide +```` + +CableConstants supply categorical assembly coordinates. The first-seen union +below is core, sheath, screen. Each observation uses its own requested point. +Their intervals retain their original uncertainty meaning. Categorical points +are primary data, so decorative-marker sampling does not remove them. + +````@example plotting +assembly_a=CableConstants([:core, :sheath], [1.0, 2.0] .* 1e-4, [2.0, 3.0] .* 1e-7, + [3.0, 4.0] .* 1e-10, [1.0, 2.0] .* 1e-9, 50.0); +assembly_b=CableConstants([:core, :screen], [1.1, 2.2] .* 1e-4, [2.1, 3.1] .* 1e-7, + [3.1, 4.1] .* 1e-10, [1.1, 2.1] .* 1e-9, 50.0); +assembly_plot=Makie.plot((first = assembly_a, second = assembly_b); ydata = (R,), + backend = :cairo, display_plot = false, controls = false); +assembly_plot.figure #hide +```` + +## Composing figures with `plotwindow` + +### A caller-owned 2×2 plot + +`plotwindow` is the escape hatch when no high-level recipe is appropriate. It +creates the figure and controls, then passes its content `GridLayout` to the +callback. The callback uses normal Makie constructors. Afterward `plotwindow` +discovers the native axes and attaches shared numeric scale, reset and export +controls. Explicit `axis=(...)` overrides apply to callback-created axes. +Otherwise their native construction settings are retained. + +````@example plotting +custom_dashboard = LineCableModels.plotwindow( + title = "Caller-owned diagnostics", + figure_title = "Four caller-owned Makie axes", + size = (900, 650), + backend = :cairo, + display_plot = false, + controls = false +) do grid + for row in 1:2, column in 1:2 + + axis = Axis( + grid[row, column]; + title = "Response $row,$column", + xlabel = "Frequency [Hz]", + ylabel = "Amplitude" + ) + lines!( + axis, + frequency, + @. (row + column) * sin(log10(frequency)); + color = Makie.wong_colors()[(row - 1) * 2 + column], + linewidth = 2 + ) + axis.xscale = log10 + end +end +custom_dashboard.figure #hide +```` + +The callback supports nested layouts, `Axis3`, `Colorbar`, `Slider`, custom +Makie recipes, and arbitrary plot primitives. Use `Figure` directly for a composition that omits LineCableModels controls and export settings. + +## Plot controls and layout + +### Scientific axes, scale controls, and limits + +Scientific recipes supply quantity and unit labels. PlotBuilder formats +every linear numeric axis, including previews, statistical plots, and `plotwindow` +axes: the displayed limits determine one engineering power-of-ten multiplier +(powers of three) in the axis label. Ticks show plain decimal mantissas. Tick density follows each data rectangle and the rendered +label size. A linear frequency view from 0 to 10 MHz can show ticks at +`0, 2, 4, 6, 8, 10` with the label `Frequency [Hz] ×10⁶`. The retained +frequencies are unchanged. +Zooming, panning, changing limits, and resetting keep the ticks and multiplier +synchronized. Native custom tick formatters or explicit tick labels override +automatic formatting. Setting the formatter back to `Makie.automatic` restores +it. Native tick positions persist across scale changes. Caller tick functions +and custom locator objects retain native formatting. Resetting the tick +attribute to `Makie.automatic` restores the shared locator. +Date/category axes and other native transforms retain Makie's own presentation. +These defaults use the native tick locators in Makie 0.24.11 or newer. +Positive and signed logarithmic views spanning less than two base-10 units +after transformation show decimal mantissas, with one engineering multiplier +when needed. Broader views show scientific tick labels without another axis +multiplier. Those labels retain the physical sign and any nonunit coefficient. +Zero is displayed as `0`. The signed-log reference determines the transform, +not the displayed units or the engineering multiplier. Both x and y use the +same tick-spacing rule, fitting actual label spacing after the coordinate transform. +The x/y toggles validate current visible data and uncertainty bounds before +changing the page. Native numeric `plotwindow` axes share these controls. +Automatic near-constant positive log ranges use modest multiplicative padding +(`c/1.05` to `c*1.05`, enlarged for uncertainty). + +Native Axis keywords such as `xticks`, `limits`, and `ytickformat`, and native +series attributes such as `linewidth` and `color` can be passed at construction. +Explicit `axis=(...)`, `figure=(...)` and per-series `series_attributes` +override shared defaults. Subsequent native mutations remain authoritative. +For names shared by Axis and a plot, the unqualified form targets Axis. +An explicit native `figure.size` overrides `fig_size`. Portrait sizes stay portrait. + +UQ benchmark plots with ordinary `ydata=(R,L,G,C)` overlay mean ±1 standard +deviation from retained moments. Explicit `(statistics,L,std)` requests show +the standard deviation alone. Request these two products in separate calls. +`uncertain(result, configuration)` returns the uncertainty-bearing core stored +during MC aggregation. This marginal representation retains each output's mean +and standard deviation. Joint output correlations require the retained samples. +Every eligible numeric route retains log controls for zero or negative support. +Adaptive logarithmic panels use a sign-preserving pseudo-log transform with +`log1p`/`expm1` evaluation: `sign(v)*log10(1+abs(v)/s)`. The reference `s` is +the smallest finite nonzero magnitude among eligible visible samples, +enabled uncertainty endpoints and explicit limits, in the displayed units. +If no nonzero value exists, `s=1`. Ticks retain physical values and units. +This transform lets small signed conductances span decades instead of +appearing linear. +Reapply `axisscale!(page, :y, :log10)` or switch log off/on to select the +reference from current data. Zooming and resetting limits keep it fixed. +Explicit `:pseudolog10` retains a reference of 1 in displayed units. +Switching log off selects a linear axis. Observation clipping is set during +acquisition. Use `clip=false` or component cutoffs such as `atol=(G=0., B=0.)` +when acquiring observations to preserve nonzero components. Retained +observations must be reconstructed from their primary result to undo clipping. + +Limits are calculated from finite visible data, including measurement error +bounds. Constant and near-constant series receive at least ±5% padding around +a nonzero baseline, enlarged for visible uncertainty. An exactly zero series +without uncertainty uses a neutral nonzero range. Near-constant means that the +endpoints agree within `sqrt(eps(Float64))` relatively in view coordinates. +These bounds affect the displayed view. The samples retain their original +values. Small values with measurable relative variation receive a tightly +fitted view. Legend visibility changes trigger another limit pass, so hiding +a dominant curve fits the view to the remaining data. +Explicit native limits, including one-sided limits, remain authoritative. +Reset refits automatic bounds and restores explicit bounds. Changing x/y scale +refits that automatic dimension while preserving the other dimension's current +view. Incompatible log limits are rejected before the page changes. Use native +`autolimits!(axis)` explicitly when manual bounds should be discarded. + +### Legends and docks + +`legend_position` selects only placement. `:inside` overlays the union of the +figure's axis viewports or the one axis viewport for a panel legend. +Native `halign`/`valign` select corner, center, or fractional alignment. Side-grid slots +remain outside the plot area. `legend_attributes` is merged into the native +`Legend` constructor, so orientation, bank count, padding, background, +alignment, and other Makie options remain available. `legend_cap=0.5` +is a finite, non-Boolean real number in `(0,1]`. It caps top and bottom legend +height or side legend width relative to the associated data area. The other +dimension must also fit. Inside legends use the height cap. Figure legends +use the combined panel footprint. Panel legends use their own data area. +Entries that exceed the cap are replaced by `(...)` and return when resizing +provides room. If the title and ellipsis cannot fit, the legend remains hidden +until space returns. The cap includes padding and margins, and never removes +plotted curves. Use `max_fraction` with `figurelegend!`, `panellegend!`, or a +`panel_legends` configuration. +`figurelegend!` and `panellegend!` move retained native objects. Label changes +retain their source bindings, so placement, title, and legend labels can be +changed after construction. Clicking/toggling a grouped Makie legend entry +continues to affect every plot handle in that group. + +High-level recipes may put a legend and colorbars in the same dock. PlotBuilder +creates a nested `GridLayout` and gives each block its own cell. Resizing moves +the canvas and dock blocks together. + +### Colorbars + +`colorbar_position` and `colorbar_attributes` mirror the legend placement +rules. `colorbar_group_attributes.layout` determines the independent group +arrangement. `guide_spacing` supplies minimum sibling gaps unless group +`rowgap`/`colgap` overrides them. A preview supplies several schemes, but +`_colorbar!` always consumes one scheme and creates one native +`Colorbar`. The reusable public atom remains +`materialcolors(property, range)` plus `materialscale!(position, scheme)`. + +### Observables and ownership + +`@observe` selects a scientific quantity and its matrix and frequency +coordinates. Makie's `Observable` type drives live UI state: controls, scales, +limits, visibility, and layout bounds. The adapter renders the selected +observations and connects the native UI subscriptions. +Keeping the `UIPlot` alive keeps the native figure and its subscriptions alive. + +Native mutation is the normal extension mechanism: + +````@example plotting +owned_plot = Makie.plot( + parameters, + @observe R[1, 1, :]; + backend = :cairo, + display_plot = false, + controls = false, + xscale = :log10, + fig_size = (820, 400), + legend_cap = 0.5 +); +owned_axis = only(owned_plot.axes); +owned_axis.title[] = "Caller-owned resistance"; +vlines!(owned_axis, [100.0, 1_000.0]; color = :black, linestyle = :dash); +owned_plot.figure #hide +```` + +### Responsive layout + +Automatic diagonal and preview flow pages may reflow locally when resized. +Their page membership and identities remain fixed. Explicit layouts and matrix +topology remain fixed. Physical aspect belongs to each panel, so circular designs, +wide systems, and 1×4 preview collections remain correctly scaled. A later guide, +title, or widget change refits only that window around its current data frames. + +### Current-state SVG export + +Load CairoMakie before creating a plot to include its Save button. For GL +interactivity and SVG export, import both CairoMakie and GLMakie, then select +`backend=:gl`. Loading CairoMakie later enables direct export of an existing +plot. Recreate the plot to add its button. The toolbar displays +file errors in the status row. Direct `export_svg` calls throw them to the caller. + +[`export_svg`](@ref) saves the current live figure through CairoMakie. For a +publication export it temporarily hides the toolbar and status row, switches +the figure's font roles to Makie's LaTeX font theme, uses a white background, +and then restores every changed observable, preserving the active backend. +The SVG includes caller-added plots, visibility, scales, limits, and annotations. +Interactive zoom and pan are retained in both the SVG and the live window. + +````@example plotting +export_directory = mktempdir(); +export_svg( + owned_plot; + path = joinpath(export_directory, "caller_owned_resistance.svg"), + theme = :publication, + open_file = false +); #hide +nothing #hide +```` + +## Adding or changing a managed recipe + +A new high-level recipe should keep these responsibilities with their owners: + +1. Expose numerical observations or physical geometry through the public + methods of the module that defines them. Do not add plot-data construction, labels, + colors, layout, or Makie types to scientific owners. +2. Add request normalization and the narrow public dispatch method in + `LineCableModelsMakieExt`, then call + native Makie constructors or primitives there. +3. Reuse only the shared Makie functions the recipe needs: `_figure_layout`, + `_axis!`, `_finish_plot!`, or `plotwindow`. Do not create a second + plot specification or an optional adapter hierarchy. +4. Return `UIPlot` with the actual native objects and leave further mutation to + the caller. +5. Add the real call to this literate gallery. Add a Cairo rendering assertion and an interactive GL inspection fixture when resizing or widgets matter. + +A purely visual variation usually does not need a new managed recipe. Pass a native attribute or mutate the returned block. Add a Makie primitive, or start from +`plotwindow`. A new recipe is justified when LineCableModels implements application-specific +retained coordinate presentation, physical geometry, or a reusable piece +of scientific interaction. + +## Implementation + +Scientific objects supply observations, geometry, material properties, and +units. `LineCableModelsMakieExt` converts these values into Makie plots. +Matrix coordinates identify subplots. Result containers identify overlaid +series. PlotBuilder adds labels, controls, and SVG export. + +| Implementation | Responsibility | +|:--|:--| +| `src/plotbuilder/` | Optional entry points and `UIPlot` | +| `ext/LineCableModelsMakieExt/recipes/line_data.jl` and `comparison_data.jl` | Select observations for plotting | +| `src/datamodel/preview/geometry.jl` and `materials.jl` | Geometry and material ranges | +| `ext/LineCableModelsMakieExt/recipes/preview_data.jl` | Extract geometry for drawing | +| `ext/LineCableModelsMakieExt/material_colors.jl` | Material palettes | +| `ext/LineCableModelsMakieExt/shell.jl` | Figure layout, axes, limits, and controls | +| `ext/LineCableModelsMakieExt/recipes/*_render.jl` | Draw declaration geometry | +| `ext/LineCableModelsMakieExt/layout.jl` and `guides.jl` | Capacity, native frames, and guide composition | +| `ext/LineCableModelsMakieExt/controls.jl` | Native widget ownership and common actions | +| `ext/LineCableModelsMakieExt/export_presentation.jl` | Temporary native export presentation and restoration | +| `ext/LineCableModelsMakieExt/montecarlo.jl` | Statistical plots | +| `ext/LineCableModelsMakieExt/native_export.jl` | SVG export | diff --git a/docs/src/reference.md b/docs/src/reference.md index 596b9ae5d..c573d99b0 100644 --- a/docs/src/reference.md +++ b/docs/src/reference.md @@ -1,121 +1,198 @@ # API reference -This page provides a comprehensive API reference for the [`LineCableModels.jl`](@ref) package. It documents all public modules, types, functions, and constants, organized by functional area. Each section corresponds to a major module in the package, with detailed information about parameters, return values, and usage examples. +This page lists the public computation API by its defining module. Within each +module, Documenter orders constants, types, functions and methods, then macros. +Convenience functions and extension interfaces have separate references. -## Contents -```@contents -Pages = ["reference.md"] -Depth = 3 -``` - ---- +## Computation grammar -## Data model ```@autodocs -Modules = [LineCableModels.DataModel] +Modules = [ + LineCableModels, + LineCableModels.Commons, + LineCableModels.Units, +] Order = [:module, :constant, :type, :function, :macro] +Filter = api_reference_entry Public = true Private = false ``` -### Base parameters (R, L, C, G) +## Materials + ```@autodocs -Modules = [LineCableModels.DataModel.BaseParams] +Modules = [LineCableModels.Materials, LineCableModels.Materials.TemperatureDependent] Order = [:module, :constant, :type, :function, :macro] +Filter = api_reference_entry Public = true Private = false ``` ---- +## Data model -## Earth properties ```@autodocs -Modules = [LineCableModels.EarthProps] +Modules = [ + LineCableModels.DataModel, + LineCableModels.Earth, + LineCableModels.Earth.FrequencyDependent, + LineCableModels.Earth.EquivalentHomogeneous, +] Order = [:module, :constant, :type, :function, :macro] +Filter = api_reference_entry Public = true Private = false ``` ---- +## Line and cable computations -## Import & export ```@autodocs -Modules = [LineCableModels.ImportExport] +Modules = [ + LineCableModels.Engine, + LineCableModels.Engine.EarthAdmittance, + LineCableModels.Engine.EarthImpedance, + LineCableModels.Engine.InsulationAdmittance, + LineCableModels.Engine.SemiconAdmittance, + LineCableModels.Engine.InsulationImpedance, + LineCableModels.Engine.InternalImpedance, + LineCableModels.Engine.PipeImpedance, + LineCableModels.Engine.ShuntModel, + LineCableModels.Engine.ModalAnalysis, +] Order = [:module, :constant, :type, :function, :macro] +Filter = api_reference_entry Public = true Private = false ``` ---- +## Parameter spaces -## Materials library ```@autodocs -Modules = [LineCableModels.Materials] +Modules = [ + LineCableModels.ParametricBuilder, +] Order = [:module, :constant, :type, :function, :macro] +Filter = api_reference_entry Public = true Private = false ``` ---- +## Uncertainty quantification -## Utilities ```@autodocs -Modules = [LineCableModels.Utils] +Modules = [LineCableModels.UQ] Order = [:module, :constant, :type, :function, :macro] +Filter = api_reference_entry Public = true Private = false ``` ---- +## Import and export -## Uncertain Bessels ```@autodocs -Modules = [LineCableModels.UncertainBessels] +Modules = [LineCableModels.ImportExport] Order = [:module, :constant, :type, :function, :macro] +Filter = api_reference_entry Public = true Private = false ``` ---- - -## Private API +## PSCAD backend -#### Data model ```@autodocs -Modules = [LineCableModels.DataModel] +Modules = [LineCableModels.PSCAD] Order = [:module, :constant, :type, :function, :macro] -Public = false -Private = true +Filter = api_reference_entry +Public = true +Private = false ``` -#### Earth properties -```@autodocs -Modules = [LineCableModels.EarthProps] -Order = [:module, :constant, :type, :function, :macro] -Public = false -Private = true +## Plots, reports, and tables + +```@docs +LineCableModels.UIPlot +LineCableModels.plot +LineCableModels.preview +LineCableModels.show_material_scale +LineCableModels.export_svg +LineCableModels.figurecolorbars! +LineCableModels.axisscale! +LineCableModels.resetview! +LineCableModels.addwidget! +LineCableModels.removewidget! +LineCableModels.figurelegend! +LineCableModels.panellegend! +LineCableModels.figuretitle! +LineCableModels.paneltitle! +LineCableModels.plotwindow +LineCableModels.materialcolors +LineCableModels.materialscale! +LineCableModels.ReportBuilder.ReportArtifact +LineCableModels.ReportBuilder.TableReportDefinition +LineCableModels.ReportBuilder.XLSXReportDefinition +LineCableModels.report ``` -#### Materials library -```@autodocs -Modules = [LineCableModels.Materials] -Order = [:module, :constant, :type, :function, :macro] -Public = false -Private = true -``` +### Reporting selections and units -#### Utilities -```@autodocs -Modules = [LineCableModels.Utils] -Order = [:module, :constant, :type, :function, :macro] -Public = false -Private = true +Ordinary reporting accepts `report(source; values=nothing, ...)` or +`report(source, selection; ...)`. Sources include completed cable constants, +line parameters, standalone Z/Y results with `frequencies` context, result +collections, parametric studies, UQ results, and retained observations. + +```julia +report(constants) +report(constants; values=(R, L, G, C), length_unit=:kilo, + quantity_units=(R=:base, L=:milli, G=:micro, C=:micro)) +report(line_parameters; values=@observe(R[1, 1, 1:12])) ``` ---- +Omitted `values`, `nothing`, and `()` select the source owner's defaults or all +retained products. Complete line results default to R/X and G/B. Selecting +R/L/G/C requests those four quantities instead. One selector such as `R` +produces only that quantity's table. Positional and keyword selections cannot +be supplied together. Reporting uses `values`. Plotting uses `ydata`. + +`units`, `length_unit`, `quantity_units`, and `frequency_unit` express display +units through the observation owner. `freq_unit` is an alternative spelling of +`frequency_unit`. Supplying both is an error. Retained observations preserve +recorded units when these options are omitted. Raw-only `clip`, `atol`, and +`frequencies` cannot be supplied for retained inputs. + +A separate atomic `reference` is retained without computing comparisons. +`illustration=true` or a callable explicitly requests a plot. Its options belong +in `plot_options`, and its `ydata` selection must agree with the report's `values`. +The illustration receives the retained observations. Default reporting returns +in-memory tables without loading a plotting backend or writing files. + +Explicit snapshots are useful for saving detached scientific products. Report +definitions remain useful for specialized operations: `BenchmarkTableDefinition` +organizes comparisons, and `XLSXReportDefinition` requests file output. +`TableReportDefinition` selects retained products within the definition-based +extension workflow. Ordinary quantity tables need only `report(source)`. + +### Display and quantity-table access + +Ordinary quantity reports display their completed tables, with recorded units +and owner-provided gridpoint descriptions. Plain and HTML output provide bounded +previews. An IO context with `:limit => false` shows all reported rows, columns, +and gridpoints. Compact display remains a single-line summary. + +`artifact[R]` returns the stored resistance DataFrame for an atomic report, or a +vector in reported-result order for a collection. `artifact[i, R]` returns the +table for result position `i`. Complete transformation and statistical requests +retain their own identities. Lookup preserves the selection made by `values`, +and requires an exact match for indexed requests. +Returned tables are shared with the report. Use `copy` for independent edits. +The `.tables` field remains available for inspection of the underlying grouped +tables and specialized summaries. + +```@docs +Base.getindex(::LineCableModels.ReportBuilder.ReportArtifact, ::Any) +``` ## Index + ```@index -Pages = ["reference.md"] -Order = [:module, :constant, :type, :function, :macro] -``` \ No newline at end of file +Pages = ["reference.md"] +Order = [:module, :constant, :type, :function, :macro] +``` diff --git a/docs/src/refs.bib b/docs/src/refs.bib deleted file mode 100644 index 7a3bfb803..000000000 --- a/docs/src/refs.bib +++ /dev/null @@ -1,226 +0,0 @@ -@misc{NIST:DLMF, -key = {DLMF}, -title = {NIST Digital Library of Mathematical Functions}, -note = {Olver, F. W. J. and Olde Daalhuis, A. B. and Lozier, D. W. and Schneider, B. I. and Boisvert, R. F. and Clark, C. W. and Miller, B. R. and Saunders, B. V. and Cohl, H. S. and McClain, M. A., eds.}, -year = {2025}, -institution = {National Institute of Standards and Technology (NIST)}, -howpublished = {[{https://dlmf.nist.gov/}]({https://dlmf.nist.gov/}), Release 1.2.4 of 2025-03-15}, -url = {https://dlmf.nist.gov/}, -} - -@ARTICLE{6897971, - author={Vujević, Slavko and Lovrić, Dino and Boras, Vedran}, - journal={IEEE Transactions on Electromagnetic Compatibility}, - title={High-Accurate Numerical Computation of Internal Impedance of Cylindrical Conductors for Complex Arguments of Arbitrary Magnitude}, - year={2014}, - volume={56}, - number={6}, - pages={1431-1438}, - keywords={Impedance;Conductors;Approximation methods;Skin effect;Mathematical model;Computational modeling;Large complex arguments;modified Bessel functions;solid cylindrical conductor;tubular cylindrical conductor;Large complex arguments;modified Bessel functions;solid cylindrical conductor;tubular cylindrical conductor}, - doi={10.1109/TEMC.2014.2352398}} - - -@ARTICLE{5437464, - author={Papadopoulos, Theofilos A. and Tsiamitros, Dimitrios A. and Papagiannis, Grigoris K.}, - journal={IEEE Transactions on Power Delivery}, - title={Impedances and Admittances of Underground Cables for the Homogeneous Earth Case}, - year={2010}, - volume={25}, - number={2}, - pages={961-969}, - keywords={Impedance;Earth;Conductors;Admittance;Electromagnetic transients;Power system transients;Underground power cables;Electromagnetic propagation;Integral equations;Power cables;Earth return admittance;earth return impedance;electromagnetic transients;power cable modeling}, - doi={10.1109/TPWRD.2009.2034797} -} - -@ARTICLE{app14198982, - author = {Riba, Jordi-Roger}, - title = {The Role of AC Resistance of Bare Stranded Conductors for Developing Dynamic Line Rating Approaches}, - journal = {Applied Sciences}, - volume = {14}, - year = {2024}, - number = {19}, - article-number = {8982}, - url = {https://www.mdpi.com/2076-3417/14/19/8982}, - issn = {2076-3417}, - doi = {10.3390/app14198982} -} - -@ARTICLE{6521501, - author={Morgan, Vincent T.}, - journal={IEEE Transactions on Power Delivery}, - title={The Current Distribution, Resistance and Internal Inductance of Linear Power System Conductors—A Review of Explicit Equations}, - year={2013}, - volume={28}, - number={3}, - pages={1252-1262}, - keywords={Conductors;Resistance;Inductance;Current density;Wires;Solids;Equations;Current density;eddy currents;hysteresis;internal inductance;power loss;proximity effect;resistance;skin effect;transformer effect}, - doi={10.1109/TPWRD.2012.2213617} -} - -@INPROCEEDINGS{yang2008gmr, - author = {Yang, Y. and Fortin, S. and Ma, J. and Dawalibi, F. P.}, - title = {GMR of Stranded Multizone Conductors}, - booktitle = {The 4th IASTED Asian Conference on Power and Energy Systems (AsiaPES)}, - year = {2008}, - keywords = {GMR, multizone, stranded conductor, strand pattern, current density, frequency dependence}, - note = {[Link]({https://www.actapress.com/Abstract.aspx?paperId=33058})} -} - -@INPROCEEDINGS{916943, - author={Gustavsen, B.}, - booktitle={2001 IEEE Power Engineering Society Winter Meeting. Conference Proceedings (Cat. No.01CH37194)}, - title={Panel session on data for modeling system transients insulated cables}, - year={2001}, - volume={2}, - number={}, - pages={718-723 vol.2}, - keywords={Power cable insulation}, - doi={10.1109/PESW.2001.916943} -} - -@ARTICLE{1458878, - author={Gustavsen, B. and Martinez, J.A. and Durbak, D.}, - journal={IEEE Transactions on Power Delivery}, - title={Parameter determination for modeling system transients-Part II: Insulated cables}, - year={2005}, - volume={20}, - number={3}, - pages={2045-2050}, - keywords={Cable insulation;Cables;Magnetic materials;Conducting materials;Material properties;Semiconductor materials;Frequency;Impedance;Shunt (electrical);Admittance;Insulated cables;modeling;power system transients;simulation}, - doi={10.1109/TPWRD.2005.848774} -} - -@ARTICLE{5743045, - author={Gudmundsdottir, Unnur Stella and Gustavsen, Bjørn and Bak, Claus Leth and Wiechowski, Wojciech}, - journal={IEEE Transactions on Power Delivery}, - title={Field Test and Simulation of a 400-kV Cross-Bonded Cable System}, - year={2011}, - volume={26}, - number={3}, - pages={1403-1410}, - keywords={Power cables;Cable shielding;Current measurement;Voltage measurement;Conductors;Bonding;Grounding;Insulated cables;model accuracy;modeling;power system transients;proximity effect;simulation}, - doi={10.1109/TPWRD.2010.2084600} -} - -@ARTICLE{4113884, - author={Ametani, A.}, - journal={IEEE Transactions on Power Apparatus and Systems}, - title={A General Formulation of Impedance and Admittance of Cables}, - year={1980}, - volume={PAS-99}, - number={3}, - pages={902-910}, - keywords={Impedance;Admittance;Coaxial cables;Transient analysis;Earth;Conductors;Voltage;Equations;Dielectric losses;Underwater cables}, - doi={10.1109/TPAS.1980.319718} -} - -@techreport{cigre345, - author = {{CIGRE Working Group B2.12}}, - title = {Alternating Current (AC) Resistance of Helically Stranded Conductors}, - institution = {CIGRE}, - year = {2008}, - month = {April}, - number = {345}, - type = {Technical Brochure}, - note = {[Link]({https://www.e-cigre.org/publications/detail/345-alternating-current-ac-resistance-of-helically-stranded-conductors.html})} -} - -@techreport{cigre531, - author = {{CIGRE Working Group B1.30}}, - title = {Cable Systems Electrical Characteristics}, - institution = {CIGRE}, - year = {2013}, - month = {April}, - number = {531}, - type = {Technical Brochure}, - note = {[Link]({https://www.e-cigre.org/publications/detail/531-cable-systems-electrical-characteristics.html})} -} - -@book{rosa1908, - title = {The self and mutual-inductances of linear conductors}, - author = {Rosa, E. B.}, - year = {1908}, - publisher = {National Bureau of Standards}, - volume = {4}, - number = {2}, - issn = {0096-8579}, - doi = {10.6028/bulletin.088}, -} - -@ARTICLE{Karmokar2025, - author = {Karmokar, T. and Popov, M.}, - title = {Enhanced Modelling and Parameter Determination of HVDC Cables Using Practice-Oriented Methodology}, - year = {2025}, - journal = {CIGRE Science and Engineering}, - volume = {2025-February}, - number = {36}, - url = {https://www.scopus.com/inward/record.uri?eid=2-s2.0-85218793172&partnerID=40&md5=415867c192cfa930318607b68cd8cd69}, - type = {Article}, -} - -@book{grover1981inductance, - title={Inductance Calculations: Working Formulas and Tables}, - author={Grover, F.W. and Instrument Society of America}, - isbn={9780876645574}, - lccn={lc82126260}, - series={Dover books on engineering and engineering physics}, - year={1981}, - publisher={Instrument Society of America} -} - -@techreport{CENELEC50182, - type = {Standard}, - key = {EN 50182:2001}, - author = {Technical Committee CENELEC TC 7}, - title = {Conductors for Overhead Lines---Round Wire Concentric Lay Stranded Conductors}, - institution = {European Committee for Electrotechnical Standardization}, - year = {2001}, - isbn = {0 580 38001 7}, - pages = {1--73}, - note = {[Link]({https://www.en-standard.eu/bs-en-50182-2001-conductors-for-overhead-lines-round-wire-concentric-lay-stranded-conductors/})} -} - -@ARTICLE{4389974, - author={Oussalah, Naima and Zebboudj, Youcef and Boggs, Steven A.}, - journal={IEEE Electrical Insulation Magazine}, - title={Partial Discharge Pulse Propagation in Shielded Power Cable and Implications for Detection Sensitivity}, - year={2007}, - volume={23}, - number={6}, - pages={5-10}, - keywords={Partial discharges;Power cables;Attenuation measurement;Cable shielding;Frequency;Power measurement;Corona;Wideband;Material properties;Software tools}, - doi={10.1109/MEI.2007.4389974} -} - -@techreport{IEC60287, - title = {{IEC 60287} - Electric cables -- Calculation of the current rating}, - author = {{IEC technical committee 20: Electric cables}}, - institution = {{International Electrotechnical Commission}}, - year = {2023}, - type = {Standard}, - note = {[Link]({https://webstore.iec.ch/publication/261})}, -} - -@techreport{CENELEC_HD620_S3_2023, - type = {Standard}, - key = {HD 620 S3:2023}, - author = {Technical Committee CENELEC TC 20}, - title = {Distribution cables with extruded insulation for rated voltages from 3.6/6 (7.2) kV up to and including 20.8/36 (42) kV}, - institution = {European Committee for Electrotechnical Standardization}, - year = {2023}, - isbn = {978-2-8322-6203-0}, - pages = {1--150}, - note = {[Link]({https://standards.iteh.ai/catalog/standards/clc/ac6f2d1f-5ce8-4580-8d64-f40ac82b7125/hd-620-s3-2023})} -} - -@techreport{VDE_DIN_VDE_0276_620_2024, - type = {Standard}, - key = {DIN VDE 0276-620:2024-12}, - author = {Technical Committee VDE}, - title = {Distribution cables with extruded insulation for rated voltages from 3.6/6 (7.2) kV up to and including 20.8/36 (42) kV}, - institution = {VDE}, - year = {2024}, - isbn = {978-3-8007-6204-0}, - pages = {1--120}, - note = {[Link]({https://www.vde-verlag.de/standards/0200084/din-vde-0276-620-vde-0276-620-2024-12.html})} -} \ No newline at end of file diff --git a/docs/src/theory/contents.md b/docs/src/theory/contents.md new file mode 100644 index 000000000..f88f71de2 --- /dev/null +++ b/docs/src/theory/contents.md @@ -0,0 +1,14 @@ +# Contents + +The matrix formulation defines how conductor, insulation, pipe, and earth +contributions enter the line-parameter matrices. The following sections +identify the formulas for those contributions, the modal transformations +applied to the assembled matrices, and the earth-material models. + +- [Matrix-based formulation of line and cable coaxial parameters](matrix_formulation.md) +- [Internal impedance of conductors](internal_impedance.md) +- [Insulation parameters](insulation_parameters.md) +- [Earth return impedance](earth_return_impedance.md) +- [Earth return admittance](earth_return_admittance.md) +- [Modal decomposition and eigenvalue tracking](modal_decomposition.md) +- [Earth properties](earth_properties.md) diff --git a/docs/src/theory/earth-properties/equivalent-homogeneous/default.md b/docs/src/theory/earth-properties/equivalent-homogeneous/default.md new file mode 100644 index 000000000..7592eb900 --- /dev/null +++ b/docs/src/theory/earth-properties/equivalent-homogeneous/default.md @@ -0,0 +1,47 @@ +# Default equivalent homogeneous-earth rule + +## Identification and source + +| Field | Value | +| --- | --- | +| Family | Equivalent homogeneous earth | +| Formula identifier | `:bottommost`. `:default` routes to this rule | +| Documentation status | Registered and documented. | + +**Description.** `:default` routes to the explicit `:bottommost` rule. +The bottommost soil layer supplies the equivalent +homogeneous resistivity, relative permittivity, and relative permeability. +This selection rule is package-defined, not an author-named implementation. + +**Assumptions.** + +The layer property vectors include air at index 1 and soil layers at indices +2 through N. + +**Expression.** + +``(\rho,\varepsilon_r,\mu_r)_{equivalent}= + (\rho_N,\varepsilon_{r,N},\mu_{r,N})``. + +**Approximation.** + +The selected material is passed to the consuming homogeneous-earth formula. + +Martins-Britto et al. reported that deep-layer conductivity can predominate in +the cases they studied, but this registration does not implement their +equivalent-conductivity equation. + +**Limitations.** + +The approximation may be unsuitable for strong conductivity contrasts or +frequency ranges outside the supporting study, and accuracy is not claimed +for selecting permittivity or permeability from the deepest layer. + +**Reference.** + +A. G. Martins-Britto, F. V. Lopes, and S. R. M. J. Rondineau, *Multilayer +Earth Structure Approximation by a Homogeneous Conductivity Soil for Ground +Return Impedance Calculations*, 2020 (context only. Not the implemented +equation). + +[Back to the relevant theory overview](../../earth_properties.md) diff --git a/docs/src/theory/earth-properties/frequency-dependent/default.md b/docs/src/theory/earth-properties/frequency-dependent/default.md new file mode 100644 index 000000000..96ea44017 --- /dev/null +++ b/docs/src/theory/earth-properties/frequency-dependent/default.md @@ -0,0 +1,39 @@ +# Default frequency-dependent earth material + +## Identification and source + +| Field | Value | +| --- | --- | +| Family | Frequency-dependent earth properties | +| Formula identifier | `:default` | +| Route | `:constant` | +| Documentation status | Routing alias. The pass-through is documented separately. | + +**Description.** `:default` routes to the explicit `:constant` static +earth-material pass-through. The registered frequency-dependent relations are +listed in [Registered frequency-dependent soil relations](formulas.md). + +**Assumptions.** + +The input material is homogeneous and its properties are already represented +at the requested frequency. + +**Expression.** + +``\rho(f)=\rho_0``, ``\varepsilon_r(f)=\varepsilon_{r,0}``, and +``\mu_r(f)=\mu_{r,0}``. + +**Approximation.** + +No frequency-dependent constitutive correction is applied. + +**Limitations.** + +Selecting `:default` does not model soil dispersion. Select `:constant` for the +same pass-through explicitly, or select one of the registered empirical laws. + +**Reference.** + +Package routing default. No author equation is claimed by this registration. + +[Back to the relevant theory overview](../../earth_properties.md) diff --git a/docs/src/theory/earth-properties/frequency-dependent/formulas.md b/docs/src/theory/earth-properties/frequency-dependent/formulas.md new file mode 100644 index 000000000..cdcbbfa7c --- /dev/null +++ b/docs/src/theory/earth-properties/frequency-dependent/formulas.md @@ -0,0 +1,24 @@ +# Registered frequency-dependent soil relations + +The frequency-dependent earth-property registry contains the following +explicit relations. Their names identify the principal source contributor. +The source references remain part of each implementation's documentation. + +| Formula identifier | Compact description | Long description | +| --- | --- | --- | +| `:constant` | Constant | Frequency-independent earth-material pass-through | +| `:alipio2014` | Alipio | Alipio-Visacro causal soil dispersion (2014) | +| `:cigre2019` | CIGRE | CIGRE WG C4.33 recommended soil dispersion (2019) | +| `:datsios2019` | Datsios | Datsios-Mikropoulos two-limit soil fit (2019) | +| `:longmire1975` | Longmire | Longmire-Smith 13-term dielectric relaxation (1975) | +| `:messier1985` | Messier | Messier square-root soil dispersion (1985) | +| `:portela1999` | Portela | Portela power-law soil dispersion (1999) | +| `:scott1967` | Scott | Scott-Carroll-Cunningham empirical moist-soil fit (1967) | +| `:visacro1987` | Visacro | Visacro-Portela empirical soil dispersion (1987) | +| `:visacro2012` | Visacro | Visacro-Alipio empirical soil dispersion (2012) | + +`:default` selects `:constant`. +The concrete formulas accept their source-specific physical parameters through +the `parameters` field of the formula selection. + +[Back to Earth properties](../../earth_properties.md) diff --git a/docs/src/theory/earth_properties.md b/docs/src/theory/earth_properties.md new file mode 100644 index 000000000..b02a112eb --- /dev/null +++ b/docs/src/theory/earth_properties.md @@ -0,0 +1,26 @@ +# Earth properties + +Frequency-dependent soil relations determine material conductivity and +permittivity. Equivalent homogeneous-earth models (EHEM) instead reduce a +layered earth to effective material values for a separately selected +earth-return formula. Neither operation is itself an impedance or +admittance matrix. + +## Frequency-dependent soil relations + +Each relation retains its reference resistivity, units, fitted frequency +range, and source assumptions. `:default` is a routing alias for the explicit +`:constant` pass-through. + +- [Default frequency-dependent earth material](earth-properties/frequency-dependent/default.md) +- [Registered frequency-dependent soil relations](earth-properties/frequency-dependent/formulas.md) + +## Equivalent homogeneous earth + +These reductions replace the layered response by a homogeneous +approximation. They must be paired with the earth-return formula and +material reconstruction stated by the source. + +- [Default equivalent homogeneous-earth rule](earth-properties/equivalent-homogeneous/default.md) + +[Back to Contents](contents.md) diff --git a/docs/src/theory/earth_return_admittance.md b/docs/src/theory/earth_return_admittance.md new file mode 100644 index 000000000..8b3c4567f --- /dev/null +++ b/docs/src/theory/earth_return_admittance.md @@ -0,0 +1,26 @@ +# Earth return admittance + +```@meta +CurrentModule = LineCableModels.Engine.EarthAdmittance +``` + +These formulations describe the external electric field through potential +coefficients or source-defined scalar admittances. The complete potential +matrix, including the local insulation contribution, is assembled before +conversion to shunt admittance. A multiconductor admittance matrix requires matrix inversion instead of +scalar reciprocals. Geometry, voltage reference, and +propagation assumptions remain those of each source. + +- [Source coefficients of the default formula in two half-spaces](@ref LineCableModels.Engine.EarthAdmittance.source_coefficients(::Union{LineCableModels.Engine.EarthImpedance.Formula{:unified}, LineCableModels.Engine.EarthAdmittance.Formula{:unified}}, ::Union{Val{:self}, Val{:mutual}}, ::Union{Val{1}, Val{2}}, ::Union{Val{1}, Val{2}}, ::Any, ::Any)) +- [Ideal-earth electrostatic image potential coefficients](@ref earth_potential_coefficient(::Formula{:ideal}, ::Val{:self}, ::Val{1}, ::Val{1}, ::Any, ::Any)) +- [Pollaczek underground earth-return admittance](external-admittance/1926/homogeneous-earth-generalized-induction-green-function/Pollaczek1926.md) +- [Wise homogeneous-earth overhead potential coefficient](external-admittance/1948/homogeneous-earth-overhead-potential-coefficient/Wise1948.md) +- [Xue underground earth-return admittance](external-admittance/2018/complete-field-and-quasi-tem-underground/Xue2018.md) + +The default formula computes both source coefficients of a pair together. The ranges in +[Validity of the default formula](earth_return_impedance.md#Validity-of-the-default-formula) +apply to its potential coefficients too. + +The default coefficients require the [complete-current matrix calculation](@ref LineCableModels.Engine.earth!(::Union{LineCableModels.Engine.EarthImpedance.Formula{:unified}, LineCableModels.Engine.EarthAdmittance.Formula{:unified}}, ::Any, ::Any)) before selecting physical exterior entries. + +[Back to Contents](contents.md) diff --git a/docs/src/theory/earth_return_impedance.md b/docs/src/theory/earth_return_impedance.md new file mode 100644 index 000000000..e87263e33 --- /dev/null +++ b/docs/src/theory/earth_return_impedance.md @@ -0,0 +1,57 @@ +# Earth return impedance + +```@meta +CurrentModule = LineCableModels.Engine.EarthImpedance +``` + +Earth-return formulas supply the self and mutual series terms associated +with the media outside each conductor or cable boundary. Their assumptions +specify the conductor arrangement (overhead, buried, or mixed), the earth +structure (homogeneous or layered), and the treatment of displacement current +and longitudinal propagation. The [matrix formulation](matrix_formulation.md) determines +where each contribution enters. + +- [Source coefficients of the default formula in two half-spaces](@ref LineCableModels.Engine.EarthAdmittance.source_coefficients(::Union{LineCableModels.Engine.EarthImpedance.Formula{:unified}, LineCableModels.Engine.EarthAdmittance.Formula{:unified}}, ::Union{Val{:self}, Val{:mutual}}, ::Union{Val{1}, Val{2}}, ::Union{Val{1}, Val{2}}, ::Any, ::Any)) +- [Carson homogeneous-earth overhead correction integral](external-impedance/1926/homogeneous-earth-overhead-integral/Carson1926.md) +- [Gary approximation for overhead wires using complex depth](@ref earth_impedance(::Formula{:gary1976}, ::Val{:self}, ::Val{1}, ::Val{1}, ::Any, ::Any)) +- [Lucca impedance for a mixed pair in homogeneous earth](@ref earth_impedance(::Formula{:lucca1994}, ::Val{:mutual}, ::Val{1}, ::Val{2}, ::Any, ::Any)) +- [Pollaczek generalized induction coefficients for overhead, buried, and mixed conductors](external-impedance/1926/homogeneous-earth-generalized-induction-green-function/Pollaczek1926.md) +- [Saad homogeneous-earth underground closed form](@ref earth_impedance(::Formula{:saad1996}, ::Val{:self}, ::Val{2}, ::Val{2}, ::Any, ::Any)) +- [Wise high-frequency overhead displacement-current integral](external-impedance/1934/homogeneous-earth-overhead-displacement-current-integral/Wise1934.md) +- [Wedepohl-Wilcox underground low-order impedance](@ref earth_impedance(::Formula{:wedepohl1973}, ::Val{:self}, ::Val{2}, ::Val{2}, ::Any, ::Any)) +- [Xue underground earth-return impedance](external-impedance/2018/complete-field-and-quasi-tem-underground/Xue2018.md) +- [Ametani mixed-pair exponential-image approximation](external-impedance/2009/homogeneous-earth-mixed-exponential-image/Ametani2009.md) + +## Validity of the default formula + +The default formula, `:unified`, holds within two ranges. + +- **Prescribed Γ.** A nonzero Γ lies between the propagation constants of the two media. It + is in range at a frequency where Im γ_air ≤ Im Γ ≤ Im γ_earth and Re Γ ≤ Re γ_earth. Γ = 0, + the default, always lies in range. +- **Thick receiver.** The formula represents each receiving conductor by the mean field on + its exterior circumference. A conductor that is thick compared with the transverse + wavelength of its medium receives a voltage that departs from the full-field value. The + error grows as (κ_m r_p)², where κ_m is the outgoing root of γ_m² − Γ² in the medium of + conductor p and r_p is its exterior radius, the jacket for an insulated cable. The range + is |κ_m r_p| ≤ 0.1. Emission from a thick conductor is accurate. + +The calibration below is indicative. It compares the default formula with a finite-element +reference for two bare copper conductors buried at 1 m depth and 2 m apart, in earth of +1000 Ω·m and relative permittivity 12, at 100 MHz with Γ = 0, where |κ_earth| ≈ 7.26 1/m. + +| Radii (m) | κr | Scaled Y error | P error, thick receiver | P error, thin receiver | +| --- | ---: | ---: | ---: | ---: | +| 0.002 / 0.002 | 0.015 | 0.74 % | none | ≤ 1.0 % (all entries) | +| 0.01 / 0.01 | 0.073 | 1.08 % | none | not reported | +| 0.0425 / 0.002 | 0.31 / 0.015 | 1.55 % | 11.0 % (mutual) | 2.0 % (mutual) | +| 0.0425 / 0.0425 | 0.31 | 5.18 % | 11 % mutual, 4.5 % self | none | + +The scaled Y error is max|ΔY| / max|diag Y_ref|. P = jω Y⁻¹, with one row per receiver. The +reference itself has an error floor of about 0.7 %, the first row. The calibration covers +buried conductors only. In air at Γ = 0, κ ≈ k₀, so a 20 mm radius reaches κr ≈ 0.13 at +300 MHz, a regime outside the calibration. + +The default coefficients require the [complete-current matrix calculation](@ref LineCableModels.Engine.earth!(::Union{LineCableModels.Engine.EarthImpedance.Formula{:unified}, LineCableModels.Engine.EarthAdmittance.Formula{:unified}}, ::Any, ::Any)) before selecting physical exterior entries. + +[Back to Contents](contents.md) diff --git a/docs/src/theory/external-admittance/1926/homogeneous-earth-generalized-induction-green-function/Pollaczek1926.md b/docs/src/theory/external-admittance/1926/homogeneous-earth-generalized-induction-green-function/Pollaczek1926.md new file mode 100644 index 000000000..c9f5487e3 --- /dev/null +++ b/docs/src/theory/external-admittance/1926/homogeneous-earth-generalized-induction-green-function/Pollaczek1926.md @@ -0,0 +1,34 @@ +# Pollaczek underground earth-return admittance + +## Identification and source + +| Field | Value | +| --- | --- | +| Family | External admittance | +| Formula identifier | `:pollaczek1926` | +| Documentation status | Placeholder. Formula transcription is intentionally pending. | + +**Description.** This page reserves the documentation slot for the registered +`:pollaczek1926` formulation. + +**Assumptions.** + +_To be documented._ + +**Expression.** + +_Formula transcription to be added._ + +**Approximation.** + +_To be documented._ + +**Limitations.** + +_To be documented._ + +**Reference.** + +_To be documented._ + +[Back to the relevant theory overview](../../../earth_return_admittance.md) diff --git a/docs/src/theory/external-admittance/1948/homogeneous-earth-overhead-potential-coefficient/Wise1948.md b/docs/src/theory/external-admittance/1948/homogeneous-earth-overhead-potential-coefficient/Wise1948.md new file mode 100644 index 000000000..e7c23704c --- /dev/null +++ b/docs/src/theory/external-admittance/1948/homogeneous-earth-overhead-potential-coefficient/Wise1948.md @@ -0,0 +1,107 @@ +# Wise homogeneous-earth overhead potential coefficient + +## Identification and source + +| Field | Value | +| --- | --- | +| Family | External admittance | +| Geometry | Infinitely long, straight, thin parallel wire(s). Self example uses wire radius ``a`` and height ``h``. Mutual geometry uses direct distance ``\rho'`` and image distance ``\rho''``. No insulation layer is included. | +| Calculated quantities | Self and mutual scalar potential coefficients for one or two parallel overhead wires. Source-provided capacitance and admittance assembly statements | +| Earth structure | Homogeneous flat half-space. | +| Model and approximation | Equations (7)-(8) are the source's integral representation after assigning ``\gamma=jk`` as a first approximation to longitudinal propagation. They are not full-wave expressions for an independently solved ``\gamma``. Wise's separate analytical approximation to this integral is recorded in the companion approximation record. | +| Main source | W. Howard Wise (1948) | +| Formula identifier | `:wise1948` | +| Citation key(s) | `Wise1948` | +| Evidence status | Original PDF page images checked | + +**Description.** Scalar self and mutual potential coefficients for infinitely long parallel wires above a flat homogeneous earth of finite conductivity and dielectric constant. The source retains an earth-related correction ``M+jN`` and states the subsequent one-wire relation between potential coefficient and capacitance. + +**Assumptions.** + +| Field | Treatment | Evidence | +| --- | --- | --- | +| Impressed longitudinal propagation constant ``Γ`` | Source current varies as ``e^{-\gamma x}``. For evaluation of the correction, ``\gamma`` is assigned ``jk`` as a first approximation. The source then proposes a second one-wire estimate ``\gamma=\sqrt{(z+Z_{11})(G+j\omega/p_{11})}``. | Stated, text below (4), printed pp. 367–368. | +| Air propagation constant ``γ_air`` | ``k=2\pi/\lambda``. The first approximation sets the line propagation constant to ``jk``. | Stated, definitions on printed pp. 366–367. | +| Earth propagation constant ``γ_earth`` | In electromagnetic cgs notation, ``k_2^2=\omega^2\mu\varepsilon-j4\pi\omega\mu\sigma=k^2(\varepsilon-j2c\lambda\sigma)/\mu`` when ``\varepsilon`` is in electrostatic units. | Stated, printed p. 366. | +| Earth permittivity and displacement current | Earth dielectric constant ``\varepsilon`` and conductivity ``\sigma`` are both retained in ``k_2^2`` and in (8). | Stated, abstract, definitions, and (8), printed pp. 365–366. | +| Range of validity | Infinite-line use presupposes reasonably efficient transmission. The source calls ``\gamma=jk`` a first approximation and states that ``M+jN`` vanishes as ``f\to0``, ``f\to\infty``, ``\varepsilon\to\infty``, or ``\sigma\to\infty``. No universal numerical frequency interval is given. | Stated, printed pp. 365, 367–369. | +| Earth permeability ``μ_earth`` | ``\mu`` is assumed unity everywhere in electromagnetic units. | Stated, printed p. 367. | +| Arrangement | Overhead. One-wire self term and two-parallel-wire mutual term. | Stated, abstract, Fig. 1, and text below (8), printed pp. 365–366. | +| Earth structure | Homogeneous flat half-space. | Stated, abstract and derivation opening, printed pp. 365–366. | +| Conductor and insulation geometry | Infinitely long, straight, thin parallel wire(s). Self example uses wire radius ``a`` and height ``h``. Mutual geometry uses direct distance ``\rho'`` and image distance ``\rho''``. No insulation layer is included. | Stated, Fig. 1 and text following (8), printed pp. 365–366. | +| Constitutive and field assumptions | Harmonic fields in homogeneous air and earth. Scalar ``\varepsilon,\sigma`` and unit permeability. The imposed ``\gamma=jk`` is an evaluation approximation rather than a solved modal propagation constant. | Stated, printed pp. 366–368. | +| Conventions | Time factor ``e^{j\omega t}``, longitudinal dependence ``e^{-\gamma x}``. ``j^2=-1``. Coordinates and ``h,z,y`` follow Fig. 1. Source quantities and equations are in electromagnetic cgs units, with distances in centimeters. | Stated, printed pp. 365–367. | + +**Expression.** Source equations (7)-(8), in the source's electromagnetic-cgs notation. + +```math +p_{12}=c^2\left[2\log\frac{\rho''}{\rho'}+4(M+jN)\right], +\qquad\text{(7)} +``` + +```math +\begin{aligned} +M+jN&=\int_0^\infty +\frac{e^{-(h+z)\xi\sqrt{\alpha}\,t} + \cos\!\left(y\xi\sqrt{\alpha}\,t\right)} +{\sqrt{t^2+j e^{j2\eta}}+(\varepsilon-j2c\lambda\sigma)t}\,dt, \\ +\alpha&=4\pi\omega\sigma \\ +\xi e^{j\eta}&=\sqrt{1+j\frac{\varepsilon-1}{2c\lambda\sigma}}=s. +\end{aligned}\qquad\text{(8)} +``` + +Here ``p_{12}=V_1/Q_2`` is the mutual potential coefficient, ``\rho'`` is the direct distance and ``\rho''`` the image distance shown in Fig. 1. For the self case the source writes ``p_{11}=c^2[2\log(2h/a)+4(M+jN)]`` with ``z=h``. It states that ``1/p_{11}`` is the capacitance to ground and, for two wires, ``V_1=p_{11}Q_1+p_{12}Q_2``. Those are source-provided assembly relations. Independent inversion of a potential-coefficient matrix is outside this record. + +**Approximation.** Equations (7)-(8) are the source's integral representation after assigning ``\gamma=jk`` as a first approximation to longitudinal propagation. They are not full-wave expressions for an independently solved ``\gamma``. Wise's separate analytical approximation to this integral is recorded in the companion approximation record. + +**Limitations.** The source uses electromagnetic-cgs units and unit permeability, so its printed coefficients must not be read as SI without the source's conversion. It treats a flat homogeneous earth and infinitely long parallel thin wires. The actual output is a potential coefficient, not admittance itself. Square-root branches remain unspecified beyond the displayed complex definitions. + +**Reference.** [Wise1948](@cite), equations (7)-(8), printed pp. 365 and 369. Definitions and propagation approximation, printed pp. 366–368. + +**Transcription source.** Original publication PDF. Equations (7)-(8), their transformed equality on printed p. 369, geometry, time convention, material definitions, and the ``\gamma=jk`` statement were checked against rendered page images. PDF text extraction was used only for navigation. + +## Source transcription + +The source also gives the equivalent pre-scaled form on printed p. 369: + +```math +\begin{aligned} +M+jN&=\int_0^\infty +\frac{e^{-w'v}\cos(y'v)}{\sqrt{v^2+js^2}+v/\tau^2}\,dv \\ +w'&=w\sqrt{\alpha} \\ +y'&=y\sqrt{\alpha} \\ +w&=h+z \\ +\tau^2&=k^2/k_2^2. +\end{aligned} +``` + +The paper states ``k=2\pi/\lambda`` and ``l=(v^2-k^2)^{1/2}``, ``m=(v^2-k_2^2)^{1/2}`` during the derivation. These dependencies are retained as source context and are not independent corpus formulas. + +## Notation map + +| Source symbol | Display symbol | Physical meaning | Units/convention | +| --- | --- | --- | --- | +| ``p_{11},p_{12}`` | unchanged | Self/mutual scalar potential coefficients | electromagnetic-cgs per unit length as printed | +| ``M+jN`` | unchanged | Complex earth correction to the potential coefficient | dimensionless in the displayed normalization | +| ``\rho',\rho''`` | unchanged | Direct and image distances between source wire and observation point | cm | +| ``h,z,y`` | unchanged | Source height, observation height, and transverse separation | cm. Fig. 1 | +| ``a`` | unchanged | Wire radius in the self expression | cm | +| ``c,\lambda,\omega`` | unchanged | Light speed, wavelength, angular frequency | cgs source conventions | +| ``\varepsilon,\sigma,\mu`` | unchanged | Earth dielectric constant, conductivity, and permeability | electrostatic and electromagnetic cgs. ``\mu=1`` | +| ``\gamma`` | ``Γ`` only in the assumptions table | Longitudinal line propagation constant | source uses ``e^{-\gamma x}`` | +| ``k,k_2`` | unchanged | Air and earth wave numbers | source definitions above | +| ``\alpha,\xi,\eta,s,\tau`` | unchanged | Dimensionless/scaling auxiliaries in (8) and its transformed form | as defined by the source | + +## Evidence and approximation sources + +- Headline and self and mutual assembly: printed p. 365, equations (7)-(8) and following paragraph. +- Geometry, time factor, and medium wave numbers: printed p. 366, Fig. 1 and definitions. +- Unit permeability and imposed propagation: printed p. 367 and continuation on p. 368. +- Final derivation of (7)-(8): printed p. 369. +- The 1981 Nakagawa paper is a later MKS restatement and numerical study, not the priority source for Wise's formulation. + +## Limitations and discrepancies + +- The Markdown conversion omits the equations. The record is transcribed from page images. +- Nakagawa's later MKS restatement prints an undefined ``\mu_2`` in the second denominator factor of its equation (2) and repeats it in appendix (A-3), although only media ``0`` and ``1`` are defined. This record does not use that token to alter Wise's original. +- Nakagawa's discussant classifies the restatement as quasi-static or low-frequency because it is independent of the wire propagation constant. Nakagawa replies that comparisons show useful behavior over a wider tested range. That disagreement is evidence about the secondary restatement and does not erase Wise's explicit ``\gamma=jk`` approximation. diff --git a/docs/src/theory/external-admittance/2018/complete-field-and-quasi-tem-underground/Xue2018.md b/docs/src/theory/external-admittance/2018/complete-field-and-quasi-tem-underground/Xue2018.md new file mode 100644 index 000000000..36adb4567 --- /dev/null +++ b/docs/src/theory/external-admittance/2018/complete-field-and-quasi-tem-underground/Xue2018.md @@ -0,0 +1,34 @@ +# Xue underground earth-return admittance + +## Identification and source + +| Field | Value | +| --- | --- | +| Family | External admittance | +| Formula identifier | `:xue2018` | +| Documentation status | Placeholder. Formula transcription is intentionally pending. | + +**Description.** This page reserves the documentation slot for the registered +`:xue2018` formulation. + +**Assumptions.** + +_To be documented._ + +**Expression.** + +_Formula transcription to be added._ + +**Approximation.** + +_To be documented._ + +**Limitations.** + +_To be documented._ + +**Reference.** + +_To be documented._ + +[Back to the relevant theory overview](../../../earth_return_admittance.md) diff --git a/docs/src/theory/external-impedance/1926/homogeneous-earth-generalized-induction-green-function/Pollaczek1926.md b/docs/src/theory/external-impedance/1926/homogeneous-earth-generalized-induction-green-function/Pollaczek1926.md new file mode 100644 index 000000000..c980b738c --- /dev/null +++ b/docs/src/theory/external-impedance/1926/homogeneous-earth-generalized-induction-green-function/Pollaczek1926.md @@ -0,0 +1,176 @@ +# Pollaczek generalized induction coefficients for overhead, buried, and mixed conductors + +## Identification and source + +| Field | Value | +| --- | --- | +| Family | External impedance | +| Geometry | Infinitely long straight parallel filamentary conductors. A physical round wire of radius ``\rho`` is treated only through the stated small-radius self averaging. A buried wire is galvanically and dielectrically isolated from earth. No finite insulation impedance and admittance is supplied. | +| Calculated quantities | Complex generalized mutual-induction coefficient for air-air, earth-earth, and both mixed source-observation placements. Source-prescribed finite-radius self coefficient and series-impedance assembly | +| Earth structure | Plane homogeneous conducting half-space (medium 1) below homogeneous air (medium 2). | +| Model and approximation | Not an analytical approximation within the explicitly reduced filamentary, two-dimensional physical model. The reductions ``\Gamma=0``, ``\varepsilon_1=0``, ``\mu_1=\mu_2=1`` and ``k_2=0`` precede application. The finite-radius self rule is an additional small-radius approximation. Separate small- and large-``\|k\eta\|`` asymptotic self expressions (59a)-(59b) are not substituted here. | +| Main source | F. Pollaczek (1926) | +| Formula identifier | `:pollaczek1926` | +| Citation key(s) | `Pollaczek1926` | +| Evidence status | Original-publication scan image verified for the displayed equations and definitions | + +**Description.** Source-defined complex generalized induction coefficients per unit length between infinitely long parallel filamentary conductors above a homogeneous conducting earth, below its plane surface, or on opposite sides of the interface. The longitudinal electric Green function is converted by the author to mutual induction. The corresponding ``j\omega M`` term is the external series contribution under the source convention. + +**Assumptions.** + +| Field | Treatment | Evidence | +| --- | --- | --- | +| Impressed longitudinal propagation constant ``Γ`` | Fixed zero: axial attenuation and variation is neglected and ``\partial/\partial z=0``. | Stated, reduction (b) and equation (2), printed p. 341. | +| Air propagation constant ``γ_air`` | Formal source symbol ``k_2`` is retained while constructing the Green functions, then set to zero in (22) when air conductivity and permittivity are neglected. | Stated, reduction (c), p. 341, and (22), p. 345. | +| Earth propagation constant ``γ_earth`` | Source symbol ``k_1``, renamed ``k`` after (22). Under the selected reductions ``k=e^{-j\pi/4}\sqrt{4\pi\omega\sigma_1}/c`` in source cgs notation. The root has positive real part and negative imaginary part. | Stated, (1a), (10), pp. 340–342, and (22), p. 345. | +| Earth permittivity and displacement current | Neglected: the earth dielectric constant is set aside in reduction (c). Conductivity remains. | Stated, p. 341. | +| Range of validity | Infinite filamentary source. Planar homogeneous half-spaces. Steady harmonic state. No longitudinal attenuation. For finite-radius self use, radius ``\rho`` must be very small relative to conductor height or depth, and earth-induced asymmetry of conductor current is neglected. | Stated, pp. 339–341 and finite-wire discussion on p. 354. | +| Earth permeability ``μ_earth`` | Relative permeability fixed to one throughout the physical reduction. | Stated, reduction (c), p. 341. | +| Arrangement | Source and observation conductors can both be overhead, both buried or mixed. Reciprocity is stated in (9). Self is obtained by the source's boundary-circle averaging prescription. | Stated, (9), pp. 342 and 344–345. §8, pp. 354–355. | +| Earth structure | Plane homogeneous conducting half-space (medium 1) below homogeneous air (medium 2). | Stated, Fig. 1 and pp. 340–341. | +| Conductor and insulation geometry | Infinitely long straight parallel filamentary conductors. A physical round wire of radius ``\rho`` is treated only through the stated small-radius self averaging. A buried wire is galvanically and dielectrically isolated from earth. No finite insulation impedance and admittance is supplied. | Stated, reductions (a) and text after (19), pp. 341 and 344. | +| Constitutive and field assumptions | Homogeneous linear isotropic media. Time-harmonic Maxwell equations reduced to a two-dimensional axial-electric-field problem. Air constitutive terms and earth displacement current omitted in the final specialization. Finite-wire internal field is delegated to the standard solid-conductor skin-effect solution. | Stated, pp. 340–341 and §8, p. 354. | +| Conventions | Common factor ``e^{j\omega t}``. ``z`` along the conductors. ``y`` positive upward with earth ``y<0``. Source point ``(\xi,\eta)`` and observation point ``(x,y)``. Original formulas use cgs and practical electromagnetic units. | Stated, coordinate and time convention, p. 340. Unit conversion discussion below (58), p. 354. | + +**Expression.** The longitudinal electric Green functions for all four source-observation placements, with the author's conversion to generalized induction coefficient, equations (19a), (19c), (20a), (20b), and (58). + +Let + +```math +\begin{aligned} +\alpha_m(s)&=\sqrt{s^2-k_m^2} \\ +d&=\sqrt{(x-\xi)^2+(y-\eta)^2} \\ +D&=\sqrt{(x-\xi)^2+(y+\eta)^2}, +\end{aligned} +``` + +and ``A=2jJ\omega/c^2`` as in (12). For an overhead source ``\eta\ge0`` and earth observation ``y\le0``, + +```math +\mathfrak E_{-+} +=\frac{2A}{j\pi}\int_{-\infty}^{\infty} +\frac{ +e^{,js(x-\xi)+y\alpha_1(s)-\eta\alpha_2(s)} +}{\alpha_2(s)+\alpha_1(s)}\,ds. +\qquad\text{(19a)} +``` + +For overhead source and observation, ``y\ge0,\eta\ge0``, + +```math +\begin{aligned} +\mathfrak E_{++}={}&A\left[H_0^{(1)}(k_2d)-H_0^{(1)}(k_2D)\right]\\ +&+\frac{2A}{j\pi}\int_{-\infty}^{\infty} +\frac{e^{,js(x-\xi)-(y+\eta)\alpha_2(s)}} +{\alpha_2(s)+\alpha_1(s)}\,ds. +\end{aligned} +\qquad\text{(19c)} +``` + +For a buried source ``\eta\le0`` and air observation ``y\ge0``, + +```math +\mathfrak E_{+-} +=\frac{2A}{j\pi}\int_{-\infty}^{\infty} +\frac{ +e^{,js(x-\xi)-y\alpha_2(s)+\eta\alpha_1(s)} +}{\alpha_2(s)+\alpha_1(s)}\,ds. +\qquad\text{(20a)} +``` + +For buried source and observation, ``y\le0,\eta\le0``, + +```math +\begin{aligned} +\mathfrak E_{--}={}&A\left[H_0^{(1)}(k_1d)-H_0^{(1)}(k_1D)\right]\\ +&+\frac{2A}{j\pi}\int_{-\infty}^{\infty} +\frac{e^{,js(x-\xi)+(y+\eta)\alpha_1(s)}} +{\alpha_2(s)+\alpha_1(s)}\,ds. +\end{aligned} +\qquad\text{(20b)} +``` + +The physical specialization is + +```math +\begin{aligned} +k_1&=k \\ +k_2&=0 \\ +\xi&=0. +\end{aligned}\qquad\text{(22)} +``` + +For the applicable placement-specific field ``\mathfrak E``, the source defines + +```math +\begin{aligned} +\mathfrak E&=-J\frac{\partial M}{\partial t}=-j\omega J M \\ +M&=\frac{j}{\omega J}\mathfrak E, +\end{aligned}\qquad\text{(58)} +``` + +and for a physical conductor defines self ``L`` by circumferentially averaging ``M``. The total per-length series impedance with earth return is + +```math +j\omega L+r, +\qquad\text{(60)} +``` + +where ``r`` is the conductor's per-length alternating-current impedance. ``M`` and ``L`` are complex, frequency-dependent induction coefficients, not purely geometric inductances. + +The square roots follow (14a)-(14c): ``\alpha_m(s)\sim s`` as real ``s\to+\infty``, ``\alpha_m(s)\sim-s`` as ``s\to-\infty``, and ``\alpha_m(0)=jk_m`` for the source's ``\mathop{\mathrm{Im}}k_m<0`` branch. ``H_0^{(1)}`` is the first-kind Hankel function. + +**Approximation.** Not an analytical approximation within the explicitly reduced filamentary, two-dimensional physical model. The reductions ``\Gamma=0``, ``\varepsilon_1=0``, ``\mu_1=\mu_2=1`` and ``k_2=0`` precede application. The finite-radius self rule is an additional small-radius approximation. Separate small- and large-``|k\eta|`` asymptotic self expressions (59a)-(59b) are not substituted here. + +**Limitations.** The record preserves the source's cgs field normalization and its own conversion relation rather than silently inserting a modern SI prefactor. It supplies no earth displacement current, arbitrary permeability, longitudinal propagation, finite insulation layer, or proximity-distorted finite-conductor boundary solution. The ``k_2\to0`` Hankel differences are limiting expressions and must not be evaluated as two independent singular terms. Signs and normalizations of later formulas attributed to Pollaczek require an independent comparison with this exact spectral representation. + +**Reference.** [Pollaczek1926](@cite), equations (19a), (19c), (20a), (20b), (22), and (58)-(60), printed pp. 344–345 and 354–355 (PDF pages 6–7 and 16–17), with assumptions on pp. 340–342. + +**Transcription source.** Original 1926 scan. The 4 signed placement exponents, full real-line measures, Hankel direct-minus-image terms, denominator ordering, root continuation, time convention, and field-to-induction conversion were checked visually on the printed pages. OCR text was used only to locate the sections. It was not used to resolve mathematical tokens. + +## Source transcription + +The original uses ``\mathfrak E`` for the only nonzero electric-field component after the two-dimensional reduction and indexes the fields by observation medium first and source medium second. Medium 1 is earth and medium 2 is air. Equations (19a), (19c), (20a), and (20b) above preserve that order. + +For finite-radius self, the source says to average ``M`` around the conductor boundary. When radius ``\rho`` is small relative to height ``\eta``, this reduces to replacing the singular logarithmic factor + +```math +\log\frac{2}{\sqrt{|k|^2\left[x^2+(y-\eta)^2\right]}} +``` + +by ``\log\!left(2/(\sqrt{|k|^2}\rho)\right)`` and then setting ``x=0,y=\eta`` in the remaining terms. This prescription comes from the source without a finite-radius rederivation of the spectral kernel. + +## Notation map + +| Source symbol | Display symbol | Physical meaning | Units/convention | +| --- | --- | --- | --- | +| ``\mathfrak E`` | unchanged | Longitudinal electric field induced at ``(x,y)`` | original cgs field units. Common time factor suppressed | +| ``J`` | unchanged | Source conductor current amplitude | source current convention | +| ``M,L`` | unchanged | Generalized mutual and self induction coefficient per unit length | source gives H/cm after stated practical conversion | +| ``r`` in (60) | unchanged | Conductor AC impedance per unit length | distinct from distance ``d`` used here | +| ``x,y;\xi,\eta`` | unchanged | Observation and source transverse coordinates | earth ``y,\eta<0``. Air ``y,\eta>0`` | +| ``d,D`` | introduced display abbreviations | Direct and reflected-point distances appearing explicitly under the source radicals | one-to-one definitions shown above | +| ``k_1,k_2`` | unchanged | Earth and air medium constants | ``\mathrm{length}^{-1}``. Source root convention | +| ``\alpha_m`` | introduced display abbreviation | ``\sqrt{s^2-k_m^2}`` | no coordinate or branch change | +| ``s`` | unchanged | Fourier integration variable | full real line | +| ``A`` | unchanged | ``2jJ\omega/c^2`` | source cgs normalization | +| ``H_0^{(1)}`` | unchanged | First-kind order-zero Hankel function | source branch | +| ``j,\omega,c`` | unchanged | Imaginary unit, angular frequency, speed of light | common factor ``e^{j\omega t}`` | + +Only the explicitly defined ``d,D,\alpha_m`` abbreviations are added. Their maps are exact and reversible. + +## Evidence and approximation sources + +- Source geometry, time convention and homogeneous media: printed pp. 339–340. +- Filament, zero-longitudinal-variation, constitutive and permeability reductions: printed p. 341. +- Medium-root definition and branch: (10), (14a)-(14c), printed pp. 342–343. +- Exact Green functions and reciprocity: (9), (19a), (19c), (20a), (20b), printed pp. 342 and 344–345. +- Final air-earth specialization: (22), printed p. 345. +- Generalized mutual coefficient, finite-radius self prescription, and series assembly: §8, (58)-(60), printed pp. 354–355. + +## Limitations and discrepancies + +- The scan's OCR loses radicals, signs and subscripts in the central equations. Every displayed token above follows the page image. +- The normalized ``e^{j\omega t}`` convention uses the source's lossy-medium root with negative imaginary part. Later transcriptions often choose a positive-imaginary diffusion constant. The transcribed expressions retain the source conjugation convention. +- The real part of ``j\omega L`` is identified by the author with the earth-return ohmic resistance, while ``r`` contains the conductor contribution. A complete terminal impedance requires additional contributions beyond the Green function. +- The source contains additional asymptotic series and self limits beyond this exact representation. Those require separate records and are not claimed complete here. diff --git a/docs/src/theory/external-impedance/1926/homogeneous-earth-overhead-integral/Carson1926.md b/docs/src/theory/external-impedance/1926/homogeneous-earth-overhead-integral/Carson1926.md new file mode 100644 index 000000000..8b5bef21f --- /dev/null +++ b/docs/src/theory/external-impedance/1926/homogeneous-earth-overhead-integral/Carson1926.md @@ -0,0 +1,124 @@ +# Carson homogeneous-earth overhead correction integral + +## Identification and source + +| Field | Value | +| --- | --- | +| Family | External impedance | +| Geometry | Infinitely long parallel wires. Self expression includes wire radius ``a`` only in the perfect-ground base term, while the finite-ground correction uses height. | +| Calculated quantities | Per-unit-length self and mutual finite-conductivity ground-return corrections for overhead wires | +| Earth structure | Plane homogeneous semi-infinite ground ``y\leq0`` beneath a nonconducting dielectric ``y>0``. | +| Model and approximation | Integral representation exact within Carson's stated reduced field model. The reductions are the very-small-``\Gamma`` assumption and neglect of transverse ground electric-field components. Earth displacement current and an independent permeability are absent. Carson's later series evaluations of ``J`` are distinct evaluator forms and are not substituted here. | +| Main source | John R. Carson (1926) | +| Formula identifier | `:carson1926` | +| Citation key(s) | `Carson1926` | +| Evidence status | PDF page images checked | + +**Description.** Carson's per-unit-length correction to the self and mutual series impedance of parallel overhead wires caused by finite conductivity of a homogeneous semi-infinite ground, in the source's electromagnetic c.g.s. notation. + +**Assumptions.** + +| Field | Treatment | Evidence | +| --- | --- | --- | +| Impressed longitudinal propagation constant ``Γ`` | Fields initially contain ``\exp(-\Gamma z+j\omega t)`` and ``\Gamma`` is assumed very small. It is absent from the final correction integral. | Stated, printed p. 539. Equations (27)-(31), p. 545. | +| Air propagation constant ``γ_air`` | The source leaves this undefined. The upper dielectric is assigned zero conductivity and no air propagation term occurs in the correction. | Stated, printed p. 539. | +| Earth propagation constant ``γ_earth`` | No independently named ``\gamma_{earth}``. Conduction enters through ``\alpha=4\pi\lambda\omega`` and ``\sqrt{\mu^2+j}`` after normalization. | Stated, printed p. 540 and (27)-(29), p. 545. | +| Earth permittivity and displacement current | Earth permittivity does not occur. The ground is represented by conductivity ``\lambda``. | Equation-implied, definition of ``\alpha`` and field equation (1), p. 540. | +| Range of validity | ``\Gamma`` is assumed very small in electromagnetic c.g.s. units. The ground transverse field components ``E_x,E_y`` are assumed negligible compared with axial ``E_z``. No universal frequency bound is supplied. | Stated, printed pp. 539–540. | +| Earth permeability ``μ_earth`` | The source expression omits an independent earth-permeability parameter. | Equation-implied, ``\alpha=4\pi\lambda\omega`` in the declared c.g.s. formulation, p. 540. | +| Arrangement | Overhead. Self term at height ``h`` and mutual term for parallel wires at heights ``h_1,h_2`` separated horizontally by ``x``. | Stated, printed pp. 539, 544–545. | +| Earth structure | Plane homogeneous semi-infinite ground ``y\leq0`` beneath a nonconducting dielectric ``y>0``. | Stated, printed p. 539. | +| Conductor and insulation geometry | Infinitely long parallel wires. Self expression includes wire radius ``a`` only in the perfect-ground base term, while the finite-ground correction uses height. | Stated, (23)-(28), printed pp. 544–545. | +| Constitutive and field assumptions | Homogeneous conductive ground. Axial ground electric field retained, transverse components neglected. Linear harmonic fields. | Stated, printed pp. 539–540. | +| Conventions | Source coordinates place the wire parallel to ``z`` and ground below ``y=0``. Common factor ``\exp(-\Gamma z+j\omega t)``. ``j=\sqrt{-1}``. All displayed formulas use electromagnetic c.g.s. units. | Stated, printed pp. 539–540. | + +**Expression.** Self and mutual finite-conductivity ground-return correction integrals, equations (27)-(31). + +```math +\begin{aligned} +Z&=Z^0+Z' \\ +Z_{12}&=Z_{12}^{0}+Z'_{12}, +\end{aligned}\qquad\text{(25--26)} +``` + +```math +Z'=4\omega\int_0^\infty +\left(\sqrt{\mu^2+j}-\mu\right)e^{-2h'\mu}\,d\mu, +\qquad\text{(27)} +``` + +```math +Z'_{12}=4\omega\int_0^\infty +\left(\sqrt{\mu^2+j}-\mu\right) +e^{-(h'_1+h'_2)\mu}\cos(x'\mu)\,d\mu, +\qquad\text{(28)} +``` + +```math +J(p,q)=\int_0^\infty +\left(\sqrt{\mu^2+j}-\mu\right)e^{-p\mu}\cos(q\mu)\,d\mu, +\qquad\text{(29)} +``` + +```math +\begin{aligned} +Z'&=4\omega J(2h',0) \\ +Z'_{12}&=4\omega J(h'_1+h'_2,x'), \\ +\alpha&=4\pi\lambda\omega \\ +h'&=h\sqrt{\alpha} \\ +h'_1&=h_1\sqrt{\alpha} \\ +h'_2&=h_2\sqrt{\alpha} \\ +x'&=x\sqrt{\alpha}. +\end{aligned}\qquad\text{(30--31)} +``` + +``Z^0`` and ``Z_{12}^0`` are the corresponding self and mutual impedances for perfectly conducting ground. ``\lambda`` is ground conductivity in electromagnetic c.g.s. units. ``\mu`` is the dimensionless integration variable and is not permeability. The source's correction is reported in its c.g.s. impedance-per-length convention. + +**Approximation.** Integral representation exact within Carson's stated reduced field model. The reductions are the very-small-``\Gamma`` assumption and neglect of transverse ground electric-field components. Earth displacement current and an independent permeability are absent. Carson's later series evaluations of ``J`` are distinct evaluator forms and are not substituted here. + +**Limitations.** Homogeneous plane ground and infinite parallel overhead wires only. The displayed ``Z'`` terms are finite-ground corrections, not total line impedances. The source uses electromagnetic c.g.s. units. This record does not perform hidden SI rescaling. The square-root branch is not explicitly stated beside (27)-(29). + +**Reference.** [Carson1926](@cite), equations (25)-(31), printed p. 545 (PDF page 7), with assumptions and definitions on printed pp. 539–540. + +**Transcription source.** Original publication scan. Integral limits, the two separate self and mutual exponentials, cosine, normalization, and all factors were checked visually against printed p. 545. The scan has no usable text layer. No OCR reconstruction was treated as authority. + +## Source transcription + +The formula section retains Carson's source notation. The immediately preceding total impedances are + +```math +Z=z+j2\omega\log(\rho''/a)+Z', +\qquad\text{(23)} +``` + +```math +Z_{12}=j2\omega\log(\rho''/\rho')+Z'_{12}, +\qquad\text{(24)} +``` + +with ``\rho''=\sqrt{(h_1+h_2)^2+x^2}`` and ``\rho'=\sqrt{(h_1-h_2)^2+x^2}``. They show explicitly that (27)-(28) are corrections rather than complete conductor impedances. + +## Notation map + +| Source symbol | Display symbol | Physical meaning | Units/convention | +| --- | --- | --- | --- | +| ``Z',Z'_{12}`` | unchanged | Self/mutual finite-ground corrections | per unit length, source c.g.s. system | +| ``Z^0,Z^0_{12}`` | unchanged | Perfectly conducting-ground base impedances | per unit length | +| ``J(p,q)`` | unchanged | Dimensionless normalized Carson integral | source definition (29) | +| ``\mu`` | unchanged | Integration variable | dimensionless. Not permeability | +| ``\lambda`` | unchanged | Ground conductivity | electromagnetic c.g.s. units | +| ``\alpha`` | unchanged | Normalization ``4\pi\lambda\omega`` | inverse-length squared in source normalization | +| ``h,h_1,h_2`` | unchanged | Wire heights | source length units | +| ``x`` | unchanged | Horizontal separation of wire planes | source length units | +| primed geometry | unchanged | Geometry multiplied by ``\sqrt\alpha`` | dimensionless | +| ``j`` | unchanged | Imaginary unit | ``j^2=-1``. Time factor ``e^{j\omega t}`` | +| ``\Gamma`` | unchanged | Longitudinal propagation constant | common factor ``e^{-\Gamma z}`` | + +## Evidence and approximation sources + +Problem geometry, half-spaces, and longitudinal and time convention: p. 539. Conductivity normalization and c.g.s. declaration: p. 540. Total/self and mutual decomposition and normalized correction integrals: (23)-(31), pp. 544–545. + +## Limitations and discrepancies + +- All available copies are scans with no useful mathematical text layer. Visual transcription was mandatory. +- This record does not convert Carson's c.g.s. coefficient ``4\omega`` into an SI kernel. diff --git a/docs/src/theory/external-impedance/1934/homogeneous-earth-overhead-displacement-current-integral/Wise1934.md b/docs/src/theory/external-impedance/1934/homogeneous-earth-overhead-displacement-current-integral/Wise1934.md new file mode 100644 index 000000000..697ea18a3 --- /dev/null +++ b/docs/src/theory/external-impedance/1934/homogeneous-earth-overhead-displacement-current-integral/Wise1934.md @@ -0,0 +1,114 @@ +# Wise high-frequency overhead displacement-current integral + +## Identification and source + +| Field | Value | +| --- | --- | +| Family | External impedance | +| Geometry | Straight infinitely long thin wires. The external formula uses direct and image distances ``\rho'`` and ``\rho''`` and no insulation layer. | +| Calculated quantities | P.u.l. mutual series impedance of two parallel overhead ground-return wires, with earth polarization current retained | +| Earth structure | Plane homogeneous earth half-space below air. | +| Model and approximation | The earth-polarization integral is not approximated after the source fixes ``\gamma=jk`` and unit permeability. Wise then states that Carson's asymptotic and convergent series are reused by replacing Carson's ``r`` by ``r\sqrt{1+j(\varepsilon-1)/(2c\lambda\sigma)}``. Those evaluator series are not reproduced as separate physical formulations here. | +| Main source | W. H. Wise (1934), modifying Carson's homogeneous-earth formula to retain earth dielectric polarization | +| Formula identifier | `:wise1934` | +| Citation key(s) | `Wise1934` | +| Evidence status | Original publication page images checked | + +**Description.** Mutual per-unit-length overhead ground-return impedance over a homogeneous earth when earth displacement and polarization current is retained under Wise's imposed air-speed longitudinal propagation. + +**Assumptions.** + +| Field | Treatment | Evidence | +| --- | --- | --- | +| Impressed longitudinal propagation constant ``Γ`` | Imposed as ``\gamma=jk`` for a lossless wave at the velocity of light. Wise calls it a first approximation and gives ``-\gamma^2=(z\pm Z_{12})(G+j\omega C)`` as a subsequent line-mode estimate that is not reinserted into the integral. | Stated, pp. 524–525. | +| Air propagation constant ``γ_air`` | ``k=2\pi/\lambda`` in the source's electromagnetic units and the imposed longitudinal constant is ``jk``. | Stated, pp. 523–524. | +| Earth propagation constant ``γ_earth`` | The source defines ``k_2^2=k^2(\varepsilon-j2c\lambda\sigma)`` and so ``j(k_2^2-k^2)=4\pi\omega\sigma+j(\varepsilon-1)k^2``. | Stated, pp. 523 and 526. | +| Earth permittivity and displacement current | Retained through relative dielectric constant ``\varepsilon``. Removing Carson's neglected-polarization restriction is the stated purpose. | Stated, p. 522 and the definition on p. 526. | +| Range of validity | Infinite straight line and the imposed ``\gamma=jk`` first approximation require reasonably efficient transmission. A universal numerical error bound is absent from the source. | Stated, p. 524. | +| Earth permeability ``μ_earth`` | Fixed to unity relative permeability. | Stated, p. 522 and ``\mu=1`` below (1), p. 523. | +| Arrangement | 2 parallel overhead wires for mutual impedance. Evaluation at a wire surface is mentioned for the subsequent line-mode estimate. | Stated, pp. 523–526. | +| Earth structure | Plane homogeneous earth half-space below air. | Stated, introduction and Fig. 1. | +| Conductor and insulation geometry | Straight infinitely long thin wires. The external formula uses direct and image distances ``\rho'`` and ``\rho''`` and no insulation layer. | Stated, pp. 522–526 and Fig. 2. | +| Constitutive and field assumptions | Linear homogeneous isotropic media, earth conductivity ``\sigma`` and relative dielectric constant ``\varepsilon`` retained, unit permeability. Finite-velocity phase retained under prescribed longitudinal propagation. | Stated, pp. 522–526. | +| Conventions | Time factor ``e^{j\omega t}``. Current varies exponentially along the wire before the choice ``\gamma=jk``. Source electromagnetic units and per-length impedance are retained. | Stated, pp. 523–526. | + +**Expression.** On printed p. 526, Wise reduces the mutual impedance to + +```math +\begin{aligned} +Z_{12} +&=j2\omega\log\frac{\rho''}{\rho'} ++\frac{4\omega}{j(k_2^2-k^2)} +\int_0^\infty +\left(\sqrt{l^2+j(k_2^2-k^2)}-l\right) +e^{-wl}\cos(yl)\,dl \\ +&=j2\omega\log\frac{\rho''}{\rho'}+4\omega(P+jQ). +\end{aligned} +``` + +With + +```math +j(k_2^2-k^2)=4\pi\omega\sigma+j(\varepsilon-1)k^2 +=4\pi\omega\sigma\left(1+j\frac{\varepsilon-1}{2c\lambda\sigma}\right) +=\alpha s^2, +``` + +the normalized correction printed immediately below is + +```math +P+jQ=\frac{1}{s^2}\int_0^\infty +\left(\sqrt{v^2+js^2}-v\right)e^{-w'v}\cos(y'v)\,dv, +``` + +where ``\sqrt{\alpha}=\sqrt{4\pi\omega\sigma}``, ``w'=w\sqrt{\alpha}``, ``y'=y\sqrt{\alpha}``, and ``l=v\sqrt{\alpha}`` as printed. + +**Approximation.** The earth-polarization integral is not approximated after the source fixes ``\gamma=jk`` and unit permeability. Wise then states that Carson's asymptotic and convergent series are reused by replacing Carson's ``r`` by ``r\sqrt{1+j(\varepsilon-1)/(2c\lambda\sigma)}``. Those evaluator series are not reproduced as separate physical formulations here. + +**Limitations.** The imposed air-speed, zero-attenuation longitudinal wave is explicitly a first approximation. The formulation retains earth polarization but fixes relative permeability to unity and uses 1934 electromagnetic units. This transcription retains the original constants and treats later full-wave formulations separately. + +**Reference.** [Wise1934](@cite). Wise, “Propagation of High-Frequency Currents in Ground Return Circuits,” *Proceedings of the IRE* 22(4), 1934, pp. 522–527. + +**Transcription source.** Original publication page image, printed p. 526 (PDF p. 5). The ``4\omega/[j(k_2^2-k^2)]`` prefactor, radical subtraction, exponent, cosine, normalized ``1/s^2`` factor, and constitutive definition were visually checked. + +## Source transcription + +The expressions above retain Wise's source order. The paper first integrates a horizontal-current-element wave function along an infinite wire, imposes ``\gamma=jk``, and only then reduces the spectral integral. Its statement that Carson's series can be modified by one complex scaling is a prescription for evaluating this integral. It does not separately claim physical priority. + +## Notation map + +### Numerical interpretation + +The executable SI form uses lossless air and nonmagnetic earth, as in the +source, with ``\beta^2=j\omega\mu_0\sigma_g+ +(j\omega)^2\mu_0(\varepsilon_g-\varepsilon_a)``. Rationalizing +``[\sqrt{u^2+\beta^2}-u]/\beta^2`` gives the bounded denominator +``[u+\sqrt{u^2+\beta^2}]^{-1}``. The SI correction multiplier is +``j\omega\mu_0/\pi``. The geometrical multiplier is +``j\omega\mu_0/(2\pi)``. + +Self assembly sets the lateral separation to zero in the correction and the +wire radius only in the ideal-ground logarithm. The numerical integration +is checked against the independent difference-of-roots expression. +The original electromagnetic-unit equations above remain unchanged. + +| Source symbol | Display symbol | Physical meaning | Units/convention | +| --- | --- | --- | --- | +| ``k`` | unchanged | air wavenumber ``2\pi/\lambda`` | source electromagnetic units | +| ``k_2`` | unchanged | complex bulk earth wavenumber | source electromagnetic units | +| ``\varepsilon,\sigma`` | unchanged | relative earth dielectric constant and conductivity | source convention | +| ``w=h+z`` | unchanged | sum of source and observation heights | source length units | +| ``y`` | unchanged | transverse wire separation | source length units | +| ``\rho',\rho''`` | unchanged | direct and image distances | source length units | +| ``P+jQ`` | unchanged | normalized finite-earth correction | source-defined | + +Notation and units remain as printed in the source. + +## Evidence and approximation sources + +Wise explicitly lists the three Carson restrictions and removes only the neglected-polarization restriction. The proposed complex rescaling follows algebraically from the p. 526 normalized integral. Its retained and discarded-series order is whatever Carson's selected evaluator uses and is not generalized here. + +## Limitations and discrepancies + +- Optical extraction confuses ``l``, ``1``, primes, and radicals. The page image controls this record. +- A source-provided capacitance integral is described but not evaluated on p. 525, so no external-admittance record is created from this paper. diff --git a/docs/src/theory/external-impedance/2009/homogeneous-earth-mixed-exponential-image/Ametani2009.md b/docs/src/theory/external-impedance/2009/homogeneous-earth-mixed-exponential-image/Ametani2009.md new file mode 100644 index 000000000..81641309f --- /dev/null +++ b/docs/src/theory/external-impedance/2009/homogeneous-earth-mixed-exponential-image/Ametani2009.md @@ -0,0 +1,142 @@ +# Ametani-Yoneda-Baba-Nagaoka mixed-conductor approximation + +## Identification and source + +| Field | Value | +| --- | --- | +| Family | External impedance | +| Geometry | Parallel horizontal isolated conductors with heights or depths ``h_1,h_2`` and transverse separation ``y``. Their radii are drawn in Fig. 1 but do not enter this mutual earth term. | +| Calculated quantities | Mutual earth-return impedance per unit length between one overhead and one buried conductor | +| Earth structure | Homogeneous conducting half-space below homogeneous air with a planar interface. | +| Model and approximation | The parent is the Pollaczek mixed integral as restated in (5)-(6). The authors first substitute ``\sqrt{s^2+m^2}\simeq s+m`` in the exponent, (17), then differentiate with respect to ``y``, change variables ``s=mt``, deform the contour under a stated no-pole assumption, and substitute ``t/(\sqrt{t^2+1}+t)\simeq(1-e^{-2t})/2``, (24). Integration and the subsequent antiderivative in ``y`` lead to (27). No controlled retained and discarded series order or uniform expansion parameter is assigned to these two substitutions. A further power-frequency approximation is recorded separately. | +| Main source | Akihiro Ametani, Tetsuzo Yoneda, Yoshihiro Baba, and Naoto Nagaoka (2009) | +| Formula identifier | `:ametani2009` | +| Citation key(s) | `Ametani2009` | +| Evidence status | Original PDF equations checked against page images. Source coordinate convention unresolved. | + +**Description.** Exponential-image approximation to the mutual earth-return impedance between parallel horizontal conductors on opposite sides of a homogeneous air-earth interface. + +**Assumptions.** + +| Field | Treatment | Evidence | +| --- | --- | --- | +| Impressed longitudinal propagation constant ``Γ`` | The parent is described as a TEM-mode approximation. No independent imposed longitudinal constant appears in (5)-(6) or (27). Its longitudinal phase prescription is not stated. | Stated, sections II-III, pp. 860–862. Equation-implied, (5)-(6), (27). | +| Air propagation constant ``γ_air`` | ``m_1=0`` in the reduced air model: zero air conductivity and omitted displacement current. | Stated/equation-implied, (4), section II-B, and the ``|s|`` air kernel in (6), p. 861. | +| Earth propagation constant ``γ_earth`` | ``m=\sqrt{j\omega\mu_0/\rho_e}=|m|\exp(j\pi/4)``, with ``h_e=1/m``. | Stated — (22) and definitions following (27), p. 864. | +| Earth permittivity and displacement current | Omitted from this approximation. The more general medium definition in (3) includes ``j\omega\varepsilon_i``, but is reduced to conduction-only (4). It must not be substituted back into (27). | Stated, (3)-(4) and section III, p. 861. (22), p. 864. | +| Range of validity | Restricted to the parent TEM approximation. The authors propose a burial-depth condition ``h\leq\lambda/8`` and a critical-frequency formula (15), whose printed consistency is unresolved. The reported errors below 10% overall and below 3% at low frequency refer to the Fig. 10 examples, not a universal bound. | Stated, (11), p. 862. (15), p. 863. Section IV-B and Fig. 10, p. 865. | +| Earth permeability ``μ_earth`` | Fixed ``\mu_0``. | Stated, definitions after (3), p. 861. (22), (27). | +| Arrangement | Mixed mutual term: one overhead conductor and one buried conductor. No self formula in this new approximation. | Stated, title, Fig. 1, (5), and section IV. | +| Earth structure | Homogeneous conducting half-space below homogeneous air with a planar interface. | Stated, Fig. 1 and section II, p. 861. | +| Conductor and insulation geometry | Parallel horizontal isolated conductors with heights or depths ``h_1,h_2`` and transverse separation ``y``. Their radii are drawn in Fig. 1 but do not enter this mutual earth term. | Stated/equation-implied, Fig. 1 and (27). | +| Constitutive and field assumptions | Scalar uniform resistivity, nonmagnetic media, conduction-dominated earth, and the source's TEM assumption. Internal conductor and insulation contributions are separate from this output. | Stated/equation-implied, sections II-IV and (5). | +| Conventions | ``j`` is the imaginary unit. ``\omega=2\pi f``. Fig. 1 uses an upward ``z`` axis and section III writes ``h_2=-h<0``, but section IV uses ``\exp(-h_2m)`` and sums ``h_1+h_2`` without explicitly changing the depth convention. The record retains this ambiguity. ``H`` is redefined after (27). Output is per unit length, plotted in ``\Omega/\mathrm m``. | Stated, Fig. 1, section III-B, definitions after (18) and (27), Fig. 9. | + +**Expression.** Source equation (27), including the definitions immediately below it. + +```math +Z_m=j\omega\left(\frac{\mu_0}{2\pi}\right) +\exp\left(\frac{-h_2}{h_e}\right)\ln\left(\frac{S}{D}\right), +\qquad\text{(27)} +``` + +```math +\begin{aligned} +h_e&=\frac{1}{m} \\ +S&=\sqrt{H^2+y^2} \\ +D&=\sqrt{(h_1+h_2)^2+y^2} \\ +H&=h_1+h_2+2h_e, \\ +m&=\sqrt{\frac{j\omega\mu_0}{\rho_e}} +=|m|\exp\left(\frac{j\pi}{4}\right). +\end{aligned}\qquad\text{(22)} +``` + +``\rho_e`` is earth resistivity in ``\Omega\,\mathrm m``. ``h_e`` is a complex length. ``h_1,h_2,y,S,D,H`` have dimensions of length. The paper does not separately prescribe branches for ``S`` and the logarithm. In the derivation before (27), ``H`` instead means ``h_1+h_2``. The 2 definitions are kept at their original locations below. + +**Approximation.** The parent is the Pollaczek mixed integral as restated in (5)-(6). The authors first substitute ``\sqrt{s^2+m^2}\simeq s+m`` in the exponent, (17), then differentiate with respect to ``y``, change variables ``s=mt``, deform the contour under a stated no-pole assumption, and substitute ``t/(\sqrt{t^2+1}+t)\simeq(1-e^{-2t})/2``, (24). Integration and the subsequent antiderivative in ``y`` lead to (27). No controlled retained and discarded series order or uniform expansion parameter is assigned to these two substitutions. A further power-frequency approximation is recorded separately. + +**Limitations.** The printed depth convention and reuse of ``H`` prevent an unambiguous single-coordinate interpretation of the entire derivation. The source's critical-frequency and accuracy statements apply to its stated cases. The model neglects earth displacement current and does not provide multilayer or self-interaction extensions of the new mixed approximation. + +**Reference.** [Ametani2009](@cite), (5)-(6), p. 861. (17)-(27), p. 864. Section IV-B, p. 865. The existing bibliography entry conflicts with the four-author byline on p. 860. + +**Transcription source.** Original publication PDF images, pp. 860–865. The source equation, exponential sign, logarithmic distances, explicit branch of ``m``, parent integral, and approximation substitutions were visually checked. No equation-preserving Markdown was used as authority. + +## Source transcription + +Source parent, attributed in this article to Pollaczek (not newly attributed to the 2009 authors): + +```math +Z_m=Z(1,2)=j\omega\left(\frac{\mu_0}{2\pi}\right) +\int_{-\infty}^{\infty}F_c(s)\exp(jys)\,ds, +\qquad\text{(5)} +``` + +```math +F_c(s)=\frac{\exp\{-h_1|s|+h_2\sqrt{s^2+m^2}\}} +{\sqrt{s^2+m^2}+|s|}. +\qquad\text{(6)} +``` + +Here ``s`` is a real spectral integration variable, with units of inverse length. In this parent, buried ``h_2`` is negative. The authors write the following substitutions in section IV: + +```math +\sqrt{s^2+m^2}\simeq s+m, +\qquad\text{(17)} +``` + +```math +\begin{aligned} +H&=h_1+h_2>0,\qquad\text{definition following (18)}, \\ +s&=mt \\ +ds&=m\,dt, +\end{aligned}\qquad\text{(22, change of variable)} +``` + +```math +\frac{t}{\sqrt{t^2+1}+t}\simeq\frac{1-e^{-2t}}{2}. +\qquad\text{(24)} +``` + +The source then prints + +```math +Z_m'=-j\omega\left(\frac{\mu_0}{2\pi}\right)e^{-h_2m} +\left[\frac{y}{y^2+H^2}-\frac{y}{y^2+(H+2/m)^2}\right]. +\qquad\text{(26)} +``` + +``Z_m'`` is the derivative with respect to ``y``. The final (27) and its redefined ``H`` are reproduced above without notation changes. Equation (28) is explicitly a secondary transcription of Lucca's 1994 approximation. + +## Notation map + +Notation is unchanged. No sign or positive-depth normalization has been applied. + +| Source symbol | Display symbol | Physical meaning | Units/convention | +| --- | --- | --- | --- | +| ``Z_m,Z(1,2)`` | unchanged | Mixed mutual earth-return impedance | ``\Omega/\mathrm m`` | +| ``Z_m'`` | unchanged | Derivative of ``Z_m`` with respect to separation ``y`` | ``\Omega/\mathrm m^2`` | +| ``h_1,h_2`` | unchanged | Overhead height and buried coordinate and depth | m. Conflicting sign treatment noted above | +| ``h=-h_2`` | unchanged | Positive burial depth used in section III | m | +| ``y`` | unchanged | Horizontal separation | m | +| ``\rho_e,\mu_0`` | unchanged | Earth resistivity and vacuum permeability | ``\Omega\,\mathrm m``, H/m | +| ``m,h_e`` | unchanged | Earth propagation constant and its reciprocal | inverse m, m | +| ``H`` | unchanged | ``h_1+h_2`` in (18)-(26). ``h_1+h_2+2h_e`` after (27) | m. Local redefinition | +| ``S,D`` | unchanged | Complex image distance and geometric distance in (27) | m | +| ``s,t`` | unchanged | Original spectral variable and variable after ``s=mt`` | inverse m, dimensionless | +| ``F_c`` | unchanged | Parent mixed-interface kernel | m | +| ``j,\omega,f`` | unchanged | Imaginary unit, angular frequency, frequency | dimensionless, rad/s, Hz | +| ``\lambda`` | unchanged | Approximate soil wavelength in the source validity discussion | m | + +## Evidence and approximation sources + +The new contribution is (27), not the earlier Pollaczek or Lucca kernels reproduced for comparison. The Fig. 10 tests vary overhead heights 5, 10, 50 m, burial depths 0.1, 1, 3 m, resistivities 10, 100, 1000 ``\Omega\,\mathrm m``, and separations 0, 10, 50 m in the captioned geometries. The graphs extend to high frequencies where their errors exceed the prose's blanket 10% statement. Error (29) is printed as ``100(Z_{\mathrm{app}}-Z_P)/Z_P``. The panels separately label resistance and inductance errors. + +The article states ``h\leq\lambda/8`` in (11), with ``\lambda\simeq2\pi\sqrt{2\rho_e/(\omega\mu_0)}`` in (10), and prints ``f_0=1.407\times10^5(\rho_e/h)`` in (15). These are author-stated claims, with an apparent inconsistent dependence on ``h``. No corrected critical frequency is supplied here. + +## Limitations and discrepancies + +- Source convention conflict: (6) uses the negative buried coordinate. (17)-(27) use a decaying factor for a positive depth without declaring the change. Existing LCM uses positive height or depth magnitudes. That implementation is not authority for repairing this source. +- Directly observable source redefinition: ``H`` gains ``2h_e`` only in the definitions after (27). +- Suspected published defect: (26) lacks the factor needed to obtain the printed (27) by an antiderivative as claimed. Both expressions are retained exactly. +- Source layout defects: (18), (20), (21), and (23) place the integral sign in the numerator of a displayed fraction and the differential in its denominator. Those intermediate display layouts are not silently rewritten as purportedly source-exact integral equations. The well-formed parent (5)-(6) and actual kernel operations are retained above. The prose immediately before (21) also prints identical exponential terms in its sine identity. +- Suspected source inconsistency: the claimed frequency criterion (15) has a different burial-depth dependence from the immediately preceding ``ah/\sqrt2=\pi/4`` with ``a^2=\omega\mu_0/\rho_e``. The transcription preserves this inconsistency. diff --git a/docs/src/theory/external-impedance/2018/complete-field-and-quasi-tem-underground/Xue2018.md b/docs/src/theory/external-impedance/2018/complete-field-and-quasi-tem-underground/Xue2018.md new file mode 100644 index 000000000..4e25f8b1e --- /dev/null +++ b/docs/src/theory/external-impedance/2018/complete-field-and-quasi-tem-underground/Xue2018.md @@ -0,0 +1,34 @@ +# Xue underground earth-return impedance + +## Identification and source + +| Field | Value | +| --- | --- | +| Family | External impedance | +| Formula identifier | `:xue2018` | +| Documentation status | Placeholder. Formula transcription is intentionally pending. | + +**Description.** This page reserves the documentation slot for the registered +`:xue2018` formulation. + +**Assumptions.** + +_To be documented._ + +**Expression.** + +_Formula transcription to be added._ + +**Approximation.** + +_To be documented._ + +**Limitations.** + +_To be documented._ + +**Reference.** + +_To be documented._ + +[Back to the relevant theory overview](../../../earth_return_impedance.md) diff --git a/docs/src/theory/insulation-admittance/2004/semiconducting-screen-complex-permittivity/Ametani2004.md b/docs/src/theory/insulation-admittance/2004/semiconducting-screen-complex-permittivity/Ametani2004.md new file mode 100644 index 000000000..dc3788f9b --- /dev/null +++ b/docs/src/theory/insulation-admittance/2004/semiconducting-screen-complex-permittivity/Ametani2004.md @@ -0,0 +1,109 @@ +# Lossy cable-layer admittivity (Ametani 2004 application) + +## Identification and source + +| Field | Value | +| --- | --- | +| Family | Insulation admittance. Semicon admittance | +| Geometry | Semiconductor occupies annulus ``b' 0`. -* `IntegerField(:field)` — value `isa Integer`. -* `Less(:a,:b)` — strict ordering `a < b`. -* `LessEq(:a,:b)` — non‑strict ordering `a ≤ b`. -* `IsA{M}(:field)` — type membership check. -* `OneOf(:field, set)` — membership in a finite set. - -### Custom rule pattern +[`validate`](@ref) checks materialized inputs before they enter another +construction or numerical operation. It returns its argument unchanged when +the value is usable and throws a native Julia exception when it is not: ```julia -struct InRange{T} <: Validation.Rule - name::Symbol; lo::T; hi::T -end - -@inline function Validation._apply(r::InRange, nt, ::Type{T}) where {T} - x = getfield(nt, r.name) - (x isa Number && !(x isa Complex)) || throw(ArgumentError("[$(String(nameof(T)))] $(r.name) must be real")) - (r.lo ≤ x ≤ r.hi) || throw(ArgumentError("[$(String(nameof(T)))] $(r.name) out of range $(r.lo):$(r.hi), got $(x)")) -end +validated = validate(value) +@assert validated === value ``` -Attach via `extra_rules(::Type{X}) = (InRange(:alpha, 0.0, 1.0), ...)`. +Constructors normalize admitted scalar grammar and fill defaults before +validating the resulting object. A selected `Gridpoint` is checked when it +materializes, and computation entry points check their complete problem again. ---- +## Validation failures -## Example implementation — `DataModel.Tubular` +Construction and mutation check complete values before returning or changing +them. Rechecking at computation entry points detects changes to mutable vectors or +dictionaries retained by an otherwise immutable object. -**Typed core** (numbers only): +The failure type communicates the category: -```julia -function Tubular(r_in::T, r_ex::T, material_props::Material{T}, temperature::T) where {T<:REALSCALAR} -``` +- `ArgumentError` for invalid choices or value kinds. +- `DomainError` for values outside a physical or mathematical domain. +- `DimensionMismatch` for incompatible shapes. +- `MethodError` when no supported operation exists for the supplied types. +- `KeyError` for missing library entries. -Rationale: all proxy resolution must happen before reaching the typed core to avoid duplicate parsing and to keep type promotion deterministic. +## Materialized constructors -**Trait configuration**: +Cable-part constructors accept resolved numeric geometry. This call constructs +a circular core from its radius and material: ```julia -const _REQ_TUBULAR = (:r_in, :r_ex, :material_props,) -const _OPT_TUBULAR = (:temperature,) -const _DEFS_TUBULAR = (T₀,) - -Validation.has_radii(::Type{Tubular}) = true -Validation.has_temperature(::Type{Tubular}) = true -Validation.required_fields(::Type{Tubular}) = _REQ_TUBULAR -Validation.keyword_fields(::Type{Tubular}) = _OPT_TUBULAR +part = Region(:core, Disk(radius), material) ``` -* `has_radii = true` enables the radii bundle: `Normalized(:r_in)`, `Normalized(:r_ex)`, `Finite`, `Nonneg`, `Less(:r_in,:r_ex)`. -* `has_temperature = true` adds `Finite(:temperature)`. -* `required_fields` defines the mandatory positional fields. -* `keyword_fields` defines the optional fields that will receive default values. - -**Raw proxy acceptance**: - -```julia -Validation.is_radius_input(::Type{Tubular}, ::Val{:r_in}, x::AbstractCablePart) = true -Validation.is_radius_input(::Type{Tubular}, ::Val{:r_ex}, x::Thickness) = true -Validation.is_radius_input(::Type{Tubular}, ::Val{:r_ex}, x::Diameter) = true -``` - -The inner radius may take an existing cable part (its `r_ex`); the outer radius may take a `Thickness` or `Diameter` wrapper. - -**Extra rules**: - -```julia -Validation.extra_rules(::Type{Tubular}) = (IsA{Material}(:material_props),) -``` - -**Parsing**: - -```julia -Validation.parse(::Type{Tubular}, nt) = begin - rin, rex = _normalize_radii(Tubular, nt.r_in, nt.r_ex) - (; nt..., r_in=rin, r_ex=rex) -end -``` - -**Convenience constructor**: - -```julia -@construct Tubular _REQ_TUBULAR _OPT_TUBULAR _DEFS_TUBULAR -``` - -This expands to a weakly‑typed method that calls `validate!`, promotes using `_promotion_T`, coerces via `_coerced_args`, and delegates to the numeric core. - -**Failure modes intentionally trapped**: - -* Wrong arity, missing keys → `sanitize` via `required_fields`. -* String/complex radii → `sanitize` via default `is_radius_input` (unless explicitly allowed). -* Forgotten parsing after allowing proxies → caught by `Normalized` rules. -* Geometry violations (`r_in ≥ r_ex`) → `Less(:r_in,:r_ex)`. - ---- - -## Template for a new component - -```julia -# 1) Numeric core (numbers only) -function NewPart(a::T, b::T, material::Material{T}, temperature::T) where {T<:REALSCALAR} - # compute derived, then construct -end - -# 2) Trait config -Validation.has_radii(::Type{NewPart}) = true -Validation.has_temperature(::Type{NewPart}) = true -Validation.required_fields(::Type{NewPart}) = (:a, :b, :material) -Validation.keyword_fields(::Type{NewPart}) = (:temperature,) - -# 3) Raw acceptance (extend only what you intend to parse) -Validation.is_radius_input(::Type{NewPart}, ::Val{:r_in}, x::AbstractCablePart) = true -Validation.is_radius_input(::Type{NewPart}, ::Val{:r_ex}, x::Thickness) = true - -# 4) Extra rules -Validation.extra_rules(::Type{NewPart}) = ( - IsA{Material}(:material), -) - -# 5) Parsing (proxy → numeric) -Validation.parse(::Type{NewPart}, nt) = begin - a′, b′ = _normalize_radii(NewPart, nt.a, nt.b) - (; nt..., a=a′, b=b′) -end - -# 6) Convenience constructor — generated -@construct NewPart (:a, :b, :material) (:temperature,) (T₀,) -``` - ---- - -## Extending traits - -Traits are just methods. Add traits only when behavior toggles; avoid proliferation. - -### New feature flags - -Example: a shielding flag with its own bundle. - -```julia -Validation.has_shield(::Type) = false -Validation.has_shield(::Type{SomeType}) = true -``` - -Extend `_rules` inside `Validation` to splice the corresponding checks when `has_shield(T)` is true. - -### Field‑specific admissibility - -If only `:r_in` should accept proxies, extend the field‑tagged predicate: - -```julia -Validation.is_radius_input(::Type{X}, ::Val{:r_in}, p::AbstractCablePart) = true -Validation.is_radius_input(::Type{X}, ::Val{:r_ex}, ::AbstractCablePart) = false -``` - ---- - -## Testing guidelines - -* Test arity: `()`, `(1)`, `(1,2)` → `ArgumentError`. -* Test raw type rejections: strings, complex numbers. -* Test proxy acceptance: prior layer objects, `Thickness`, `Diameter` when allowed. -* Test parse correctness: outputs numeric and respect uncertainty rules. -* Test rule violations: negative radii, inverted radii, non‑finite values, invalid sets. -* Test constructor round‑trip: convenience path and numeric core produce equivalent instances after coercion. - -## Usage notes - -* Keep all proxy handling in `parse`. Do not call normalizers in constructors. -* Error messages must be terse and contextualized with the component type name. -* Prefer tuple returns and `NamedTuple` updates to avoid allocations. -* When adding rules, benchmark `_apply` implementations. - ---- - -## API reference - -```@autodocs -Modules = [LineCableModels.Validation] -Order = [:module, :constant, :type, :function, :macro] -Public = true -Private = true -``` - ---- - -## Index -```@index -Pages = ["validation.md"] -Order = [:module, :constant, :type, :function, :macro] -``` +Radius or thickness selection, repetition, and variation use explicit +`Grid` inputs. Scalar-complete calls invoke the action directly. Varying calls +materialize the same action through `Gridspace`. Completed objects +contain one resolved geometry and cannot drift from their declarations. + +Mutable libraries validate a complete library replacement before changing owned state. +Earth models, cable designs, and line systems are immutable descriptions. +Rebuild the authoritative declaration when it changes. + +Cable designs represent a common material reference state and reject mixed +material reference temperatures. The line problem defines the finite operating +temperature. The formulation selects +`temperature_dependence`. Its default linear law validates the material-specific +range and positive correction factor. Identity and custom laws retain their own +applicability. [`compute`](@ref) applies the selected +correction without mutating the design. + +## Uncertain geometry + +A feasible nominal design is not a guarantee about its input distribution. +Internal wire overlaps and crossing layer boundaries remain construction +errors. Sampling does not inflate bedding, shrink metal, change counts, or clip +inputs to make them fit. The ring diagnostic reports its count, radius, member +width, required chord, available chord and deficit in meters. + +Declare dependent dimensions with a joint builder and justify feasibility over +its complete support. Successful draws establish feasibility for those +realizations only. Arbitrary callbacks and independent unbounded normal +inputs cannot be certified from marginal means and standard uncertainties. +`MonteCarlo(...; options=(on_error=:resample, retain_details=true))` conditions +the output distribution on successful construction and calculation. + +The existing exterior cable-clearance rule is separate. System construction +may adjust practically touching cable placements with a warning, retaining its +propagated uncertainty reserve across Monte Carlo draws. Internal geometric and statistical repairs remain prohibited. Gmsh's tolerances are not +used as manufacturing clearances. + +## Observable resolution and comparison + +`observables(...; clip=true)` and `compare` share declared, quantity-aware +resolution in native physical units. No dimensionless epsilon is applied after +unit conversion. This reporting rule is independent of matrix blocks, selected +formulations and visible plot ranges. A certified bound on solver error or physical uncertainty cannot be inferred from this reporting cutoff. + +Relative RMS requires both operands above resolution at every selected sample. +If not, it returns `missing`, with the operand counts and reason. It never removes +samples to obtain a favorable subset. Empty bands and unsupported quantities +remain distinct. Absolute RMS always measures the original arrays. A resolved +discrepancy is not evidence that the designated reference is physical truth. + +`clip=false` and `observe` retain raw values. Detachment preserves measurement +uncertainty, measurement correlations, and Monte Carlo standard deviations. diff --git a/docs/type_trees.jl b/docs/type_trees.jl new file mode 100644 index 000000000..c101e2e98 --- /dev/null +++ b/docs/type_trees.jl @@ -0,0 +1,26 @@ +module DocumentationTrees + +using AbstractTrees +using InteractiveUtils: subtypes + +struct TypeTree + value::Type +end + +function AbstractTrees.children(node::TypeTree) + descendants = sort!(subtypes(node.value); by = string) + return TypeTree.(descendants) +end + +function AbstractTrees.printnode(io::IO, node::TypeTree; kwargs...) + name = replace(string(node.value), "LineCableModels." => "") + return print(io, name) +end + +function type_tree(root::Type) + io = IOBuffer() + AbstractTrees.print_tree(io, TypeTree(root); maxdepth = typemax(Int)) + return String(take!(io)) +end + +end diff --git a/examples/18kV_1000mm2_trefoil_atp_export.xml b/examples/18kV_1000mm2_trefoil_atp_export.xml new file mode 100644 index 000000000..f8f4fd340 --- /dev/null +++ b/examples/18kV_1000mm2_trefoil_atp_export.xml @@ -0,0 +1,39 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/18kV_1000mm2_trefoil_pscad_export.pscx b/examples/18kV_1000mm2_trefoil_pscad_export.pscx new file mode 100644 index 000000000..4be00183d --- /dev/null +++ b/examples/18kV_1000mm2_trefoil_pscad_export.pscx @@ -0,0 +1,2 @@ + +
diff --git a/examples/18kV_1000mm2_trifoil_atp_export.xml b/examples/18kV_1000mm2_trifoil_atp_export.xml index fcc2d2524..bab32c986 100644 --- a/examples/18kV_1000mm2_trifoil_atp_export.xml +++ b/examples/18kV_1000mm2_trifoil_atp_export.xml @@ -2,7 +2,7 @@
- + @@ -16,20 +16,20 @@ - - - - + + + + - - - - + + + + - - - - + + + + diff --git a/examples/18kV_1000mm2_trifoil_pscad_export.pscx b/examples/18kV_1000mm2_trifoil_pscad_export.pscx index ed83577cd..d35fef1ce 100644 --- a/examples/18kV_1000mm2_trifoil_pscad_export.pscx +++ b/examples/18kV_1000mm2_trifoil_pscad_export.pscx @@ -1,2 +1,2 @@ - + diff --git a/examples/525kV_1600mm2_bipole_ZY_export.xml b/examples/525kV_1600mm2_bipole_ZY_export.xml index 740bb6c49..874900dc2 100644 --- a/examples/525kV_1600mm2_bipole_ZY_export.xml +++ b/examples/525kV_1600mm2_bipole_ZY_export.xml @@ -1,10 +1,490 @@ - -1.3031061215348929E-05+1.7571291114131740E-08i,1.8889009383384745E-11+1.3079179673125637E-08i -1.8889009383384745E-11+1.3079179673125637E-08i,1.3031061212679173E-05+1.7566797721091601E-08i + +2.0025795128774345E-05+1.7671023105080167E-05i,5.9021998136095927E-06+7.6106885556590288E-06i +5.9021998136095927E-06+7.6106885556590288E-06i,2.0025795128774345E-05+1.7671023105080167E-05i - -4.6141103335853766E-14+1.1630062363677931E-12i,2.8489012198149370E-30+2.8047053738059645E-29i -2.8489012198149370E-30+2.8047053738059645E-29i,4.6131313664309618E-14+1.1630164701437787E-12i + +2.2500003341088987E-05+2.0638074590286440E-05i,7.9477560420780778E-06+8.0261067608412520E-06i +7.9477560420780778E-06+8.0261067608412520E-06i,2.2500003341088987E-05+2.0638074590286440E-05i + + +2.5534554060753776E-05+2.3660131021776162E-05i,1.0312763866488548E-05+7.8872898824903476E-06i +1.0312763866488548E-05+7.8872898824903476E-06i,2.5534554060753776E-05+2.3660131021776162E-05i + + +2.9045679025480928E-05+2.6643898790951867E-05i,1.2786713746240576E-05+6.9911869852393467E-06i +1.2786713746240576E-05+6.9911869852393467E-06i,2.9045679025480928E-05+2.6643898790951867E-05i + + +3.2893592519772272E-05+2.9581246794828511E-05i,1.5048399915910975E-05+5.2328384305255263E-06i +1.5048399915910975E-05+5.2328384305255263E-06i,3.2893592519772272E-05+2.9581246794828511E-05i + + +3.6953809603627123E-05+3.2574127247553223E-05i,1.6729518368757920E-05+2.6628041443037231E-06i +1.6729518368757920E-05+2.6628041443037231E-06i,3.6953809603627123E-05+3.2574127247553223E-05i + + +4.1188848484325856E-05+3.5793629893817499E-05i,1.7498432035434908E-05-5.0350782945214010E-07i +1.7498432035434908E-05-5.0350782945214010E-07i,4.1188848484325856E-05+3.5793629893817499E-05i + + +4.5662368909849897E-05+3.9392668002243457E-05i,1.7129448051729343E-05-3.9155179097870703E-06i +1.7129448051729343E-05-3.9155179097870703E-06i,4.5662368909849897E-05+3.9392668002243457E-05i + + +5.0472932172518077E-05+4.3434164564568567E-05i,1.5554501479316325E-05-7.1332546052238745E-06i +1.5554501479316325E-05-7.1332546052238745E-06i,5.0472932172518077E-05+4.3434164564568567E-05i + + +5.5640171753960374E-05+4.7898122023271202E-05i,1.2908619625419166E-05-9.6943335902907658E-06i +1.2908619625419166E-05-9.6943335902907658E-06i,5.5640171753960374E-05+4.7898122023271202E-05i + + +6.1021795095418328E-05+5.2790285911737971E-05i,9.5458606248843166E-06-1.1223338590983841E-05i +9.5458606248843166E-06-1.1223338590983841E-05i,6.1021795095418328E-05+5.2790285911737971E-05i + + +6.6342723843326257E-05+5.8298524288537447E-05i,5.9714521468000160E-06-1.1557500935888668E-05i +5.9714521468000160E-06-1.1557500935888668E-05i,6.6342723843326257E-05+5.8298524288537447E-05i + + +7.1340281794105877E-05+6.4883024797475659E-05i,2.6871168135839352E-06-1.0800938798273080E-05i +2.6871168135839352E-06-1.0800938798273080E-05i,7.1340281794105877E-05+6.4883024797475659E-05i + + +7.5923397316047912E-05+7.3231624430742184E-05i,4.1774806729329725E-08-9.2537036745489964E-06i +4.1774806729329725E-08-9.2537036745489964E-06i,7.5923397316047912E-05+7.3231624430742184E-05i + + +8.0238785853141526E-05+8.4130937425144477E-05i,-1.8171788134155004E-06-7.2733450042495034E-06i +-1.8171788134155004E-06-7.2733450042495034E-06i,8.0238785853141526E-05+8.4130937425144477E-05i + + +8.4633217807015453E-05+9.8351727326252388E-05i,-2.8949679849334127E-06-5.1704412536647440E-06i +-2.8949679849334127E-06-5.1704412536647440E-06i,8.4633217807015453E-05+9.8351727326252388E-05i + + +8.9564226767915547E-05+1.1659341085286129E-04i,-3.2747210219652195E-06-3.1796665835826475E-06i +-3.2747210219652195E-06-3.1796665835826475E-06i,8.9564226767915547E-05+1.1659341085286129E-04i + + +9.5492988847378831E-05+1.3948092892784875E-04i,-3.0747269610661950E-06-1.4794536131844806E-06i +-3.0747269610661950E-06-1.4794536131844806E-06i,9.5492988847378831E-05+1.3948092892784875E-04i + + +1.0275997236874196E-04+1.6761452127959779E-04i,-2.4491457928115017E-06-2.1155046035678223E-07i +-2.4491457928115017E-06-2.1155046035678223E-07i,1.0275997236874196E-04+1.6761452127959779E-04i + + +1.1145613060917954E-04+2.0170343592444706E-04i,-1.6046042636074808E-06+5.3504714967730427E-07i +-1.6046042636074808E-06+5.3504714967730427E-07i,1.1145613060917954E-04+2.0170343592444706E-04i + + +1.2135207736858232E-04+2.4280462666678303E-04i,-7.8380316604991212E-07+7.7317415233450846E-07i +-7.8380316604991212E-07+7.7317415233450846E-07i,1.2135207736858232E-04+2.4280462666678303E-04i + + +1.3196620861478155E-04+2.9261315670214242E-04i,-1.8960160632619234E-07+6.4511170288128385E-07i +-1.8960160632619234E-07+6.4511170288128385E-07i,1.3196620861478155E-04+2.9261315670214242E-04i + + +1.4277549838488749E-04+3.5367877958519594E-04i,1.0253384256441280E-07+3.6916184846774291E-07i +1.0253384256441280E-07+3.6916184846774291E-07i,1.4277549838488749E-04+3.5367877958519594E-04i + + +1.5344584749854936E-04+4.2946295416132116E-04i,1.5655213572380916E-07+1.2916415817985916E-07i +1.5655213572380916E-07+1.2916415817985916E-07i,1.5344584749854936E-04+4.2946295416132116E-04i + + +1.6392803396617111E-04+5.2429847616640478E-04i,9.9588619997700812E-08+2.0159998761516695E-09i +9.9588619997700812E-08+2.0159998761516695E-09i,1.6392803396617111E-04+5.2429847616640478E-04i + + +1.7438140472685764E-04+6.4341425128403322E-04i,3.3924680845976232E-08-2.9645524414652347E-08i +3.3924680845976232E-08-2.9645524414652347E-08i,1.7438140472685764E-04+6.4341425128403322E-04i + + +1.8501770368561715E-04+7.9314206642432626E-04i,6.4381537728526277E-10-1.8616307036070992E-08i +6.4381537728526277E-10-1.8616307036070992E-08i,1.8501770368561715E-04+7.9314206642432626E-04i + + +1.9598339906588325E-04+9.8131346658821353E-04i,-5.3446139149028633E-09-4.4712757205512405E-09i +-5.3446139149028633E-09-4.4712757205512405E-09i,1.9598339906588325E-04+9.8131346658821353E-04i + + +2.0734389361286082E-04+1.2177838779021699E-03i,-2.2177827849472094E-09+7.2032625697548980E-10i +-2.2177827849472094E-09+7.2032625697548980E-10i,2.0734389361286082E-04+1.2177838779021699E-03i + + +2.1914807657620508E-04+1.5150145480808284E-03i,-9.0409242212933366E-11+6.8368566103621520E-10i +-9.0409242212933366E-11+6.8368566103621520E-10i,2.1914807657620508E-04+1.5150145480808284E-03i + + +2.3149141306692041E-04+1.8887136069633244E-03i,1.6034492006725132E-10+7.7027328525963723E-11i +1.6034492006725132E-10+7.7027328525963723E-11i,2.3149141306692041E-04+1.8887136069633244E-03i + + +2.4452924011902238E-04+2.3586256674895554E-03i,2.2847118511318265E-11-3.2123883414447718E-11i +2.2847118511318265E-11-3.2123883414447718E-11i,2.4452924011902238E-04+2.3586256674895554E-03i + + +2.5846873898776679E-04+2.9495621182075507E-03i,-5.9884282862382454E-12-4.3077815620184978E-12i +-5.9884282862382454E-12-4.3077815620184978E-12i,2.5846873898776679E-04+2.9495621182075507E-03i + + +2.7357740548836522E-04+3.6927169786449016E-03i,-4.8954481961238638E-13+1.0331943633978824E-12i +-4.8954481961238638E-13+1.0331943633978824E-12i,2.7357740548836522E-04+3.6927169786449016E-03i + + +2.9020901710871943E-04+4.6273141182337687E-03i,1.4337402792774512E-13+7.2502767678450653E-15i +1.4337402792774512E-13+7.2502767678450653E-15i,2.9020901710871943E-04+4.6273141182337687E-03i + + +3.0883918453920509E-04+5.8026686564880310E-03i,-7.8036535566816667E-15-1.1919285003436642E-14i +-7.8036535566816667E-15-1.1919285003436642E-14i,3.0883918453920509E-04+5.8026686564880310E-03i + + +3.3011271359444788E-04+7.2807696189035494E-03i,-9.8879238130678004E-17+1.0824674490095276E-15i +-9.8879238130678004E-17+1.0824674490095276E-15i,3.3011271359444788E-04+7.2807696189035494E-03i + + +3.5490422317238732E-04+9.1395095671964049E-03i,2.7321894746634712E-17-5.0306980803327406E-17i +2.7321894746634712E-17-5.0306980803327406E-17i,3.5490422317238732E-04+9.1395095671964049E-03i + + +3.8438362110901174E-04+1.1476716023056360E-02i,-1.3010426069826053E-18+3.4694469519536142E-18i +-1.3010426069826053E-18+3.4694469519536142E-18i,3.8438362110901174E-04+1.1476716023056360E-02i + + +4.2005993577454940E-04+1.4415182059656702E-02i,-8.6736173798840355E-19-3.4694469519536142E-18i +-8.6736173798840355E-19-3.4694469519536142E-18i,4.2005993577454940E-04+1.4415182059656702E-02i + + +4.6374635810403179E-04+1.8108962995821341E-02i,-8.6736173798840355E-19+3.4694469519536142E-18i +-8.6736173798840355E-19+3.4694469519536142E-18i,4.6374635810403179E-04+1.8108962995821341E-02i + + +5.1735846667163091E-04+2.2751329718940068E-02i,0.0000000000000000E+00+0.0000000000000000E+00i +0.0000000000000000E+00+0.0000000000000000E+00i,5.1735846667163091E-04+2.2751329718940068E-02i + + +5.8247830502766197E-04+2.8584966054756619E-02i,0.0000000000000000E+00+0.0000000000000000E+00i +0.0000000000000000E+00+0.0000000000000000E+00i,5.8247830502766197E-04+2.8584966054756619E-02i + + +6.5977763720450819E-04+3.5915212692024454E-02i,-1.7347234759768071E-18+6.9388939039072284E-18i +-1.7347234759768071E-18+6.9388939039072284E-18i,6.5977763720450819E-04+3.5915212692024454E-02i + + +7.4869209648099205E-04+4.5127197981899639E-02i,0.0000000000000000E+00+0.0000000000000000E+00i +0.0000000000000000E+00+0.0000000000000000E+00i,7.4869209648099205E-04+4.5127197981899639E-02i + + +8.4785908363867873E-04+5.6707400709091554E-02i,6.9388939039072284E-18+0.0000000000000000E+00i +6.9388939039072284E-18+0.0000000000000000E+00i,8.4785908363867873E-04+5.6707400709091554E-02i + + +9.5633435634683711E-04+7.1269932315720180E-02i,0.0000000000000000E+00+0.0000000000000000E+00i +0.0000000000000000E+00+0.0000000000000000E+00i,9.5633435634683711E-04+7.1269932315720180E-02i + + +1.0747602828661482E-03+8.9588393705612973E-02i,-6.9388939039072284E-18-2.7755575615628914E-17i +-6.9388939039072284E-18-2.7755575615628914E-17i,1.0747602828661482E-03+8.9588393705612973E-02i + + +1.2055220835087699E-03+1.1263553558266176E-01i,-6.9388939039072284E-18-2.7755575615628914E-17i +-6.9388939039072284E-18-2.7755575615628914E-17i,1.2055220835087699E-03+1.1263553558266176E-01i + + +1.3518156177584201E-03+1.4163401778837698E-01i,6.9388939039072284E-18-2.7755575615628914E-17i +6.9388939039072284E-18-2.7755575615628914E-17i,1.3518156177584201E-03+1.4163401778837698E-01i + + +1.5165187461875299E-03+1.7812166047739930E-01i,-1.3877787807814457E-17-2.7755575615628914E-17i +-1.3877787807814457E-17-2.7755575615628914E-17i,1.5165187461875299E-03+1.7812166047739930E-01i + + +1.7018527642115588E-03+2.2403422952640883E-01i,1.3877787807814457E-17+2.7755575615628914E-17i +1.3877787807814457E-17+2.7755575615628914E-17i,1.7018527642115588E-03+2.2403422952640883E-01i + + +1.9099239106593058E-03+2.8180908912198954E-01i,1.3877787807814457E-17+5.5511151231257827E-17i +1.3877787807814457E-17+5.5511151231257827E-17i,1.9099239106593058E-03+2.8180908912198954E-01i + + +2.1433148468965602E-03+3.5451476145492278E-01i,0.0000000000000000E+00+5.5511151231257827E-17i +0.0000000000000000E+00+5.5511151231257827E-17i,2.1433148468965602E-03+3.5451476145492278E-01i + + +2.4051496660366933E-03+4.4601384933366650E-01i,-2.7755575615628914E-17+0.0000000000000000E+00i +-2.7755575615628914E-17+0.0000000000000000E+00i,2.4051496660366933E-03+4.4601384933366650E-01i + + +2.6989382728921651E-03+5.6116853905101061E-01i,0.0000000000000000E+00+5.5511151231257827E-17i +0.0000000000000000E+00+5.5511151231257827E-17i,2.6989382728921651E-03+5.6116853905101061E-01i + + +3.0285779438743443E-03+7.0609947921166594E-01i,-5.5511151231257827E-17+2.2204460492503131E-16i +-5.5511151231257827E-17+2.2204460492503131E-16i,3.0285779438743443E-03+7.0609947921166594E-01i + + +3.3984399555219702E-03+8.8851159094904864E-01i,-5.5511151231257827E-17-1.1102230246251565E-16i +-5.5511151231257827E-17-1.1102230246251565E-16i,3.3984399555219702E-03+8.8851159094904864E-01i + + +3.8134324717664092E-03+1.1181041960560385E+00i,0.0000000000000000E+00+0.0000000000000000E+00i +0.0000000000000000E+00+0.0000000000000000E+00i,3.8134324717664092E-03+1.1181041960560385E+00i + + +4.2790622394307531E-03+1.4070873445431702E+00i,0.0000000000000000E+00-1.1102230246251565E-16i +0.0000000000000000E+00-1.1102230246251565E-16i,4.2790622394307531E-03+1.4070873445431702E+00i + + +4.8015078704033431E-03+1.7708318248412027E+00i,-1.1102230246251565E-16+2.2204460492503131E-16i +-1.1102230246251565E-16+2.2204460492503131E-16i,4.8015078704033431E-03+1.7708318248412027E+00i + + +-1.1581120963931842E-31+1.1314233509020744E-09i,0.0000000000000000E+00+0.0000000000000000E+00i +0.0000000000000000E+00+0.0000000000000000E+00i,-1.1581120963931842E-31+1.1314233509020744E-09i + + +5.8319069914223858E-32+1.4243776079479309E-09i,-5.8319069914223858E-32-3.6449418696389911E-33i +-5.8319069914223858E-32-3.6449418696389911E-33i,5.8319069914223858E-32+1.4243776079479309E-09i + + +0.0000000000000000E+00+1.7931851666362393E-09i,-7.3419359107217055E-32+4.5887099442010659E-33i +-7.3419359107217055E-32+4.5887099442010659E-33i,0.0000000000000000E+00+1.7931851666362393E-09i + + +1.8485899379543422E-31+2.2574863743307206E-09i,-9.2429496897717110E-32+1.5137565380518256E-53i +-9.2429496897717110E-32+1.5137565380518256E-53i,1.8485899379543422E-31+2.2574863743307206E-09i + + +2.3272368488777230E-31+2.8420069634240230E-09i,0.0000000000000000E+00+0.0000000000000000E+00i +0.0000000000000000E+00+0.0000000000000000E+00i,2.3272368488777230E-31+2.8420069634240230E-09i + + +0.0000000000000000E+00+3.5778747867504791E-09i,-2.9298176083159477E-31-1.8311360051974673E-32i +-2.9298176083159477E-31-1.8311360051974673E-32i,0.0000000000000000E+00+3.5778747867504791E-09i + + +0.0000000000000000E+00+4.5042774892578141E-09i,0.0000000000000000E+00+0.0000000000000000E+00i +0.0000000000000000E+00+0.0000000000000000E+00i,0.0000000000000000E+00+4.5042774892578141E-09i + + +-1.8573791930290603E-30+5.6705493929990943E-09i,-4.6434479825726508E-31-3.0419076068330391E-52i +-4.6434479825726508E-31-3.0419076068330391E-52i,-1.8573791930290603E-30+5.6705493929990943E-09i + + +0.0000000000000000E+00+7.1387987296805479E-09i,-1.1691509327210128E-30+0.0000000000000000E+00i +-1.1691509327210128E-30+0.0000000000000000E+00i,0.0000000000000000E+00+7.1387987296805479E-09i + + +1.4718738194253359E-30+8.9872151304787605E-09i,-6.0263733227066917E-53-1.8398422742816698E-31i +-6.0263733227066917E-53-1.8398422742816698E-31i,1.4718738194253359E-30+8.9872151304787605E-09i + + +1.8529793542290933E-30+1.1314233509020740E-08i,1.8529793542290933E-30+2.3162241927863667E-31i +1.8529793542290933E-30+2.3162241927863667E-31i,1.8529793542290933E-30+1.1314233509020740E-08i + + +0.0000000000000000E+00+1.4243776079479311E-08i,0.0000000000000000E+00+0.0000000000000000E+00i +0.0000000000000000E+00+0.0000000000000000E+00i,0.0000000000000000E+00+1.4243776079479311E-08i + + +5.8735487285773598E-30+1.7931851666362385E-08i,-4.8096782346921759E-52-7.3419359107216998E-31i +-4.8096782346921759E-52-7.3419359107216998E-31i,5.8735487285773598E-30+1.7931851666362385E-08i + + +7.3943597518173620E-30+2.2574863743307196E-08i,0.0000000000000000E+00+0.0000000000000000E+00i +0.0000000000000000E+00+0.0000000000000000E+00i,7.3943597518173620E-30+2.2574863743307196E-08i + + +-1.8617894791021791E-29+2.8420069634240233E-08i,-1.5245652584184058E-51+1.1636184244388619E-30i +-1.5245652584184058E-51+1.1636184244388619E-30i,-1.8617894791021791E-29+2.8420069634240233E-08i + + +2.3438540866527593E-29+3.5778747867504795E-08i,2.3438540866527593E-29-3.0709023132183559E-50i +2.3438540866527593E-29-3.0709023132183559E-50i,2.3438540866527593E-29+3.5778747867504795E-08i + + +-2.9507374712247657E-29+4.5042774892578156E-08i,0.0000000000000000E+00+0.0000000000000000E+00i +0.0000000000000000E+00+0.0000000000000000E+00i,-2.9507374712247657E-29+4.5042774892578156E-08i + + +0.0000000000000000E+00+5.6705493929990916E-08i,3.7147583860581167E-29-4.6434479825726459E-30i +3.7147583860581167E-29-4.6434479825726459E-30i,0.0000000000000000E+00+5.6705493929990916E-08i + + +2.8059622385304306E-28+7.1387987296805476E-08i,-9.3532074617681021E-29+7.3527067902184066E-49i +-9.3532074617681021E-29+7.3527067902184066E-49i,2.8059622385304306E-28+7.1387987296805476E-08i + + +1.1774990555402680E-28+8.9872151304787565E-08i,-1.1774990555402680E-28+3.0855031412258239E-49i +-1.1774990555402680E-28+3.0855031412258239E-49i,1.1774990555402680E-28+8.9872151304787565E-08i + + +5.9295339335330992E-28+1.1314233509020740E-07i,0.0000000000000000E+00+0.0000000000000000E+00i +0.0000000000000000E+00+0.0000000000000000E+00i,5.9295339335330992E-28+1.1314233509020740E-07i + + +0.0000000000000000E+00+1.4243776079479306E-07i,3.7324204745103241E-28+4.6655255931379051E-29i +3.7324204745103241E-28+4.6655255931379051E-29i,0.0000000000000000E+00+1.4243776079479306E-07i + + +9.3976779657237827E-28+1.7931851666362399E-07i,6.1563881404059914E-49+5.8735487285773642E-29i +6.1563881404059914E-49+5.8735487285773642E-29i,9.3976779657237827E-28+1.7931851666362399E-07i + + +-1.1830975602907779E-27+2.2574863743307188E-07i,-7.7504334748253368E-49+7.3943597518173620E-29i +-7.7504334748253368E-49+7.3943597518173620E-29i,-1.1830975602907779E-27+2.2574863743307188E-07i + + +1.4894315832817418E-27+2.8420069634240216E-07i,7.4471579164087091E-28-7.8057741231022238E-48i +7.4471579164087091E-28-7.8057741231022238E-48i,1.4894315832817418E-27+2.8420069634240216E-07i + + +0.0000000000000000E+00+3.5778747867504782E-07i,0.0000000000000000E+00-2.3438540866527570E-28i +0.0000000000000000E+00-2.3438540866527570E-28i,0.0000000000000000E+00+3.5778747867504782E-07i + + +-2.3605899769798116E-27+4.5042774892578147E-07i,2.3605899769798116E-27-2.9507374712247645E-28i +2.3605899769798116E-27-2.9507374712247645E-28i,-2.3605899769798116E-27+4.5042774892578147E-07i + + +0.0000000000000000E+00+5.6705493929990955E-07i,-2.9718067088464965E-27+0.0000000000000000E+00i +-2.9718067088464965E-27+0.0000000000000000E+00i,0.0000000000000000E+00+5.6705493929990955E-07i + + +0.0000000000000000E+00+7.1387987296805458E-07i,0.0000000000000000E+00+0.0000000000000000E+00i +0.0000000000000000E+00+0.0000000000000000E+00i,0.0000000000000000E+00+7.1387987296805458E-07i + + +2.8259977332966447E-26+8.9872151304787597E-07i,0.0000000000000000E+00+0.0000000000000000E+00i +0.0000000000000000E+00+0.0000000000000000E+00i,2.8259977332966447E-26+8.9872151304787597E-07i + + +2.3718135734132407E-26+1.1314233509020745E-06i,2.3718135734132410E-26-5.9295339335331026E-27i +2.3718135734132410E-26-5.9295339335331026E-27i,2.3718135734132407E-26+1.1314233509020745E-06i + + +5.9718727592165188E-26+1.4243776079479307E-06i,2.9859363796082594E-26-3.7324204745103243E-27i +2.9859363796082594E-26-3.7324204745103243E-27i,5.9718727592165188E-26+1.4243776079479307E-06i + + +7.5181423725790319E-26+1.7931851666362401E-06i,3.9400884098598385E-46+4.6988389828618949E-27i +3.9400884098598385E-46+4.6988389828618949E-27i,7.5181423725790319E-26+1.7931851666362401E-06i + + +0.0000000000000000E+00+2.2574863743307184E-06i,-4.7323902411631092E-26+0.0000000000000000E+00i +-4.7323902411631092E-26+0.0000000000000000E+00i,0.0000000000000000E+00+2.2574863743307184E-06i + + +-1.1915452666253935E-25+2.8420069634240222E-06i,-5.9577263331269676E-26+2.9788631665634838E-26i +-5.9577263331269676E-26+2.9788631665634838E-26i,-1.1915452666253935E-25+2.8420069634240222E-06i + + +1.5000666154577651E-25+3.5778747867504787E-06i,1.5723019843677972E-45+1.8750832693222064E-26i +1.5723019843677972E-45+1.8750832693222064E-26i,1.5000666154577651E-25+3.5778747867504787E-06i + + +0.0000000000000000E+00+4.5042774892578159E-06i,-1.8884719815838501E-25+4.7211799539596252E-26i +-1.8884719815838501E-25+4.7211799539596252E-26i,0.0000000000000000E+00+4.5042774892578159E-06i + + +-4.7548907341543893E-25+5.6705493929990929E-06i,-4.7548907341543893E-25-7.9741782768563852E-44i +-4.7548907341543893E-25-7.9741782768563852E-44i,-4.7548907341543893E-25+5.6705493929990929E-06i + + +-5.9860527755315870E-25+7.1387987296805470E-06i,5.9860527755315870E-25+1.0038895670911538E-43i +5.9860527755315870E-25+1.0038895670911538E-43i,-5.9860527755315870E-25+7.1387987296805470E-06i + + +-3.7679969777288559E-24+8.9872151304787546E-06i,-1.5071987910915424E-24-1.2638220866460969E-42i +-1.5071987910915424E-24-1.2638220866460969E-42i,-3.7679969777288559E-24+8.9872151304787546E-06i + + +3.7949017174611843E-24+1.1314233509020742E-05i,1.5910577408655023E-43+2.3718135734132402E-25i +1.5910577408655023E-43+2.3718135734132402E-25i,3.7949017174611843E-24+1.1314233509020742E-05i + + +-2.3887491036866059E-24+1.4243776079479294E-05i,-1.1943745518433029E-24-2.9859363796082573E-25i +-1.1943745518433029E-24-2.9859363796082573E-25i,-2.3887491036866059E-24+1.4243776079479294E-05i + + +-6.0145138980632192E-24+1.7931851666362395E-05i,-3.0072569490316096E-24-2.0173252658482342E-42i +-3.0072569490316096E-24-2.0173252658482342E-42i,-6.0145138980632192E-24+1.7931851666362395E-05i + + +0.0000000000000000E+00+2.2574863743307185E-05i,0.0000000000000000E+00+1.8929560964652439E-24i +0.0000000000000000E+00+1.8929560964652439E-24i,0.0000000000000000E+00+2.2574863743307185E-05i + + +0.0000000000000000E+00+2.8420069634240261E-05i,-4.7661810665015831E-24+2.3830905332507915E-24i +-4.7661810665015831E-24+2.3830905332507915E-24i,0.0000000000000000E+00+2.8420069634240261E-05i + + +1.2000532923662120E-23+3.5778747867504790E-05i,-1.2000532923662123E-23+8.0501861599631232E-42i +-1.2000532923662123E-23+8.0501861599631232E-42i,1.2000532923662120E-23+3.5778747867504790E-05i + + +1.5107775852670794E-23+4.5042774892578116E-05i,0.0000000000000000E+00+0.0000000000000000E+00i +0.0000000000000000E+00+0.0000000000000000E+00i,1.5107775852670794E-23+4.5042774892578116E-05i + + +-3.8039125873235159E-23+5.6705493929990963E-05i,6.3793426214851200E-42-4.7548907341543948E-24i +6.3793426214851200E-42-4.7548907341543948E-24i,-3.8039125873235159E-23+5.6705493929990963E-05i + + +4.7888422204252635E-23+7.1387987296805413E-05i,0.0000000000000000E+00+0.0000000000000000E+00i +0.0000000000000000E+00+0.0000000000000000E+00i,4.7888422204252635E-23+7.1387987296805413E-05i + + +-1.2057590328732353E-22+8.9872151304787651E-05i,6.0287951643661765E-23+3.0143975821830883E-23i +6.0287951643661765E-23+3.0143975821830883E-23i,-1.2057590328732353E-22+8.9872151304787651E-05i + + +1.5179606869844732E-22+1.1314233509020739E-04i,7.5898034349223671E-23+1.8974508587305918E-23i +7.5898034349223671E-23+1.8974508587305918E-23i,1.5179606869844732E-22+1.1314233509020739E-04i + + +1.9109992829492833E-22+1.4243776079479289E-04i,9.5549964147464165E-23-9.5549964147464165E-23i +9.5549964147464165E-23-9.5549964147464165E-23i,1.9109992829492833E-22+1.4243776079479289E-04i + + +7.2174166776758679E-22+1.7931851666362400E-04i,4.8415806380357667E-40+6.0145138980632232E-23i +4.8415806380357667E-40+6.0145138980632232E-23i,7.2174166776758679E-22+1.7931851666362400E-04i + + +-9.0861892630331701E-22+2.2574863743307183E-04i,-6.0951888984738359E-40+7.5718243858609743E-23i +-6.0951888984738359E-40+7.5718243858609743E-23i,-9.0861892630331701E-22+2.2574863743307183E-04i + + +7.6258897064025249E-22+2.8420069634240244E-04i,1.0231184258632558E-39+1.9064724266006315E-22i +1.0231184258632558E-39+1.9064724266006315E-22i,7.6258897064025249E-22+2.8420069634240244E-04i + + +-9.6004263389297018E-22+3.5778747867504793E-04i,0.0000000000000000E+00+0.0000000000000000E+00i +0.0000000000000000E+00+0.0000000000000000E+00i,-9.6004263389297018E-22+3.5778747867504793E-04i + + +-4.8344882728546493E-21+4.5042774892578103E-04i,-6.4861337129288096E-39+3.0215551705341558E-22i +-6.4861337129288096E-39+3.0215551705341558E-22i,-4.8344882728546493E-21+4.5042774892578103E-04i + + +0.0000000000000000E+00+5.6705493929990944E-04i,-7.6078251746470268E-22-7.6078251746470268E-22i +-7.6078251746470268E-22-7.6078251746470268E-22i,0.0000000000000000E+00+5.6705493929990944E-04i + + +0.0000000000000000E+00+7.1387987296805445E-04i,0.0000000000000000E+00+0.0000000000000000E+00i +0.0000000000000000E+00+0.0000000000000000E+00i,0.0000000000000000E+00+7.1387987296805445E-04i + + +4.8230361314929426E-21+8.9872151304787665E-04i,4.8230361314929426E-21+1.2057590328732356E-21i +4.8230361314929426E-21+1.2057590328732356E-21i,4.8230361314929426E-21+8.9872151304787665E-04i + + +1.2143685495875793E-20+1.1314233509020742E-03i,0.0000000000000000E+00+0.0000000000000000E+00i +0.0000000000000000E+00+0.0000000000000000E+00i,1.2143685495875793E-20+1.1314233509020742E-03i diff --git a/examples/525kV_1600mm2_bipole_atp_export.xml b/examples/525kV_1600mm2_bipole_atp_export.xml index e3cbcadf7..faacc08fb 100644 --- a/examples/525kV_1600mm2_bipole_atp_export.xml +++ b/examples/525kV_1600mm2_bipole_atp_export.xml @@ -14,15 +14,15 @@ - - - - + + + + - - - - + + + + diff --git a/examples/525kV_1600mm2_bipole_pscad_export.pscx b/examples/525kV_1600mm2_bipole_pscad_export.pscx index f1d10a1da..99bc4f637 100644 --- a/examples/525kV_1600mm2_bipole_pscad_export.pscx +++ b/examples/525kV_1600mm2_bipole_pscad_export.pscx @@ -1,2 +1,2 @@ - + diff --git a/examples/cable_layers.csv b/examples/cable_layers.csv deleted file mode 100644 index b9a78f013..000000000 --- a/examples/cable_layers.csv +++ /dev/null @@ -1,204 +0,0 @@ -layer,type,element,x0,y0,radius_out,rin,rext -1,circstrands,1,0.0,0.0,0.00183245,, -2,circstrands,1,0.0036649,0.0,0.00183245,, -2,circstrands,2,0.0018324500000000004,0.0031738965023295892,0.00183245,, -2,circstrands,3,-0.0018324499999999991,0.0031738965023295892,0.00183245,, -2,circstrands,4,-0.0036649,4.488208054195135e-19,0.00183245,, -2,circstrands,5,-0.0018324500000000017,-0.003173896502329589,0.00183245,, -2,circstrands,6,0.0018324499999999976,-0.0031738965023295905,0.00183245,, -3,circstrands,1,0.0073298,0.0,0.00183245,, -3,circstrands,2,0.0063477930046591784,0.0036648999999999996,0.00183245,, -3,circstrands,3,0.003664900000000001,0.0063477930046591784,0.00183245,, -3,circstrands,4,4.488208054195135e-19,0.0073298,0.00183245,, -3,circstrands,5,-0.0036648999999999983,0.0063477930046591784,0.00183245,, -3,circstrands,6,-0.006347793004659178,0.0036649000000000026,0.00183245,, -3,circstrands,7,-0.0073298,8.97641610839027e-19,0.00183245,, -3,circstrands,8,-0.006347793004659179,-0.003664899999999998,0.00183245,, -3,circstrands,9,-0.0036649000000000035,-0.006347793004659178,0.00183245,, -3,circstrands,10,-1.3464624162585403e-18,-0.0073298,0.00183245,, -3,circstrands,11,0.0036648999999999952,-0.006347793004659181,0.00183245,, -3,circstrands,12,0.006347793004659177,-0.0036649000000000035,0.00183245,, -4,circstrands,1,0.0109947,0.0,0.00183245,, -4,circstrands,2,0.010331638457754828,0.0037604088698227297,0.00183245,, -4,circstrands,3,0.008422428838760227,0.0070672569322205924,0.00183245,, -4,circstrands,4,0.005497350000000001,0.009521689506988767,0.00183245,, -4,circstrands,5,0.0019092096189945998,0.010827665802043323,0.00183245,, -4,circstrands,6,-0.0019092096189945985,0.010827665802043323,0.00183245,, -4,circstrands,7,-0.005497349999999997,0.009521689506988769,0.00183245,, -4,circstrands,8,-0.008422428838760225,0.007067256932220595,0.00183245,, -4,circstrands,9,-0.010331638457754826,0.0037604088698227314,0.00183245,, -4,circstrands,10,-0.0109947,1.3464624162585405e-18,0.00183245,, -4,circstrands,11,-0.010331638457754828,-0.003760408869822729,0.00183245,, -4,circstrands,12,-0.008422428838760229,-0.0070672569322205924,0.00183245,, -4,circstrands,13,-0.005497350000000005,-0.009521689506988765,0.00183245,, -4,circstrands,14,-0.0019092096189945989,-0.010827665802043323,0.00183245,, -4,circstrands,15,0.001909209618994595,-0.010827665802043324,0.00183245,, -4,circstrands,16,0.005497350000000001,-0.009521689506988767,0.00183245,, -4,circstrands,17,0.008422428838760225,-0.007067256932220597,0.00183245,, -4,circstrands,18,0.010331638457754823,-0.0037604088698227375,0.00183245,, -5,circstrands,1,0.0146596,0.0,0.00183245,, -5,circstrands,2,0.014160086243067227,0.003794183673584913,0.00183245,, -5,circstrands,3,0.012695586009318357,0.007329799999999999,0.00183245,, -5,circstrands,4,0.010365902569482313,0.010365902569482312,0.00183245,, -5,circstrands,5,0.007329800000000002,0.012695586009318357,0.00183245,, -5,circstrands,6,0.003794183673584916,0.014160086243067225,0.00183245,, -5,circstrands,7,8.97641610839027e-19,0.0146596,0.00183245,, -5,circstrands,8,-0.0037941836735849114,0.014160086243067227,0.00183245,, -5,circstrands,9,-0.0073297999999999966,0.012695586009318357,0.00183245,, -5,circstrands,10,-0.010365902569482312,0.010365902569482313,0.00183245,, -5,circstrands,11,-0.012695586009318355,0.007329800000000005,0.00183245,, -5,circstrands,12,-0.014160086243067225,0.003794183673584917,0.00183245,, -5,circstrands,13,-0.0146596,1.795283221678054e-18,0.00183245,, -5,circstrands,14,-0.014160086243067228,-0.0037941836735849075,0.00183245,, -5,circstrands,15,-0.012695586009318359,-0.007329799999999996,0.00183245,, -5,circstrands,16,-0.010365902569482317,-0.010365902569482307,0.00183245,, -5,circstrands,17,-0.007329800000000007,-0.012695586009318355,0.00183245,, -5,circstrands,18,-0.0037941836735849244,-0.014160086243067223,0.00183245,, -5,circstrands,19,-2.6929248325170805e-18,-0.0146596,0.00183245,, -5,circstrands,20,0.0037941836735849066,-0.014160086243067228,0.00183245,, -5,circstrands,21,0.0073297999999999905,-0.012695586009318362,0.00183245,, -5,circstrands,22,0.01036590256948231,-0.010365902569482315,0.00183245,, -5,circstrands,23,0.012695586009318353,-0.007329800000000007,0.00183245,, -5,circstrands,24,0.014160086243067223,-0.0037941836735849253,0.00183245,, -6,circstrands,1,0.0183245,0.0,0.00183245,, -6,circstrands,2,0.017924065709646622,0.0038098777783900305,0.00183245,, -6,circstrands,3,0.01674026373857184,0.0074532456160425,0.00183245,, -6,circstrands,4,0.014824831913423726,0.010770870855633424,0.00183245,, -6,circstrands,5,0.012261483796222898,0.013617757354460509,0.00183245,, -6,circstrands,6,0.009162250000000002,0.015869482511647947,0.00183245,, -6,circstrands,7,0.0056625819134237245,0.01742763513285054,0.00183245,, -6,circstrands,8,0.00191543182514812,0.018224116471675926,0.00183245,, -6,circstrands,9,-0.0019154318251481135,0.018224116471675926,0.00183245,, -6,circstrands,10,-0.005662581913423723,0.017427635132850543,0.00183245,, -6,circstrands,11,-0.009162249999999997,0.015869482511647947,0.00183245,, -6,circstrands,12,-0.012261483796222892,0.013617757354460516,0.00183245,, -6,circstrands,13,-0.014824831913423722,0.010770870855633427,0.00183245,, -6,circstrands,14,-0.016740263738571837,0.007453245616042505,0.00183245,, -6,circstrands,15,-0.017924065709646622,0.0038098777783900383,0.00183245,, -6,circstrands,16,-0.0183245,2.2441040270975676e-18,0.00183245,, -6,circstrands,17,-0.017924065709646622,-0.003809877778390026,0.00183245,, -6,circstrands,18,-0.016740263738571844,-0.007453245616042494,0.00183245,, -6,circstrands,19,-0.014824831913423726,-0.010770870855633422,0.00183245,, -6,circstrands,20,-0.012261483796222902,-0.013617757354460507,0.00183245,, -6,circstrands,21,-0.009162250000000009,-0.015869482511647943,0.00183245,, -6,circstrands,22,-0.005662581913423727,-0.01742763513285054,0.00183245,, -6,circstrands,23,-0.0019154318251481302,-0.018224116471675926,0.00183245,, -6,circstrands,24,0.0019154318251481072,-0.018224116471675926,0.00183245,, -6,circstrands,25,0.005662581913423721,-0.017427635132850543,0.00183245,, -6,circstrands,26,0.009162249999999988,-0.015869482511647954,0.00183245,, -6,circstrands,27,0.01226148379622289,-0.013617757354460518,0.00183245,, -6,circstrands,28,0.014824831913423722,-0.010770870855633429,0.00183245,, -6,circstrands,29,0.016740263738571834,-0.007453245616042515,0.00183245,, -6,circstrands,30,0.017924065709646622,-0.003809877778390041,0.00183245,, -7,circstrands,1,0.0219894,0.0,0.00183245,, -7,circstrands,2,0.021655331604086645,0.0038184192379891978,0.00183245,, -7,circstrands,3,0.020663276915509655,0.007520817739645459,0.00183245,, -7,circstrands,4,0.019043379013977537,0.010994699999999998,0.00183245,, -7,circstrands,5,0.016844857677520454,0.014134513864441185,0.00183245,, -7,circstrands,6,0.014134513864441188,0.016844857677520454,0.00183245,, -7,circstrands,7,0.010994700000000001,0.019043379013977534,0.00183245,, -7,circstrands,8,0.007520817739645462,0.020663276915509652,0.00183245,, -7,circstrands,9,0.0038184192379891995,0.021655331604086645,0.00183245,, -7,circstrands,10,1.3464624162585405e-18,0.0219894,0.00183245,, -7,circstrands,11,-0.003818419237989197,0.021655331604086645,0.00183245,, -7,circstrands,12,-0.007520817739645459,0.020663276915509655,0.00183245,, -7,circstrands,13,-0.010994699999999994,0.019043379013977537,0.00183245,, -7,circstrands,14,-0.014134513864441188,0.016844857677520454,0.00183245,, -7,circstrands,15,-0.01684485767752045,0.01413451386444119,0.00183245,, -7,circstrands,16,-0.019043379013977537,0.010994699999999998,0.00183245,, -7,circstrands,17,-0.020663276915509652,0.007520817739645463,0.00183245,, -7,circstrands,18,-0.021655331604086645,0.0038184192379892064,0.00183245,, -7,circstrands,19,-0.0219894,2.692924832517081e-18,0.00183245,, -7,circstrands,20,-0.02165533160408665,-0.003818419237989191,0.00183245,, -7,circstrands,21,-0.020663276915509655,-0.007520817739645458,0.00183245,, -7,circstrands,22,-0.019043379013977537,-0.010994699999999993,0.00183245,, -7,circstrands,23,-0.016844857677520458,-0.014134513864441185,0.00183245,, -7,circstrands,24,-0.01413451386444119,-0.01684485767752045,0.00183245,, -7,circstrands,25,-0.01099470000000001,-0.01904337901397753,0.00183245,, -7,circstrands,26,-0.007520817739645474,-0.02066327691550965,0.00183245,, -7,circstrands,27,-0.0038184192379891978,-0.021655331604086645,0.00183245,, -7,circstrands,28,-4.039387248775621e-18,-0.0219894,0.00183245,, -7,circstrands,29,0.00381841923798919,-0.02165533160408665,0.00183245,, -7,circstrands,30,0.007520817739645447,-0.020663276915509655,0.00183245,, -7,circstrands,31,0.010994700000000001,-0.019043379013977534,0.00183245,, -7,circstrands,32,0.014134513864441185,-0.016844857677520458,0.00183245,, -7,circstrands,33,0.01684485767752045,-0.014134513864441194,0.00183245,, -7,circstrands,34,0.01904337901397753,-0.01099470000000001,0.00183245,, -7,circstrands,35,0.020663276915509645,-0.007520817739645475,0.00183245,, -7,circstrands,36,0.021655331604086645,-0.003818419237989199,0.00183245,, -8,semicon,1,0.0,0.0,0.02582185,0.02382185,0.02582185 -9,insulator,1,0.0,0.0,0.05182185,0.02582185,0.05182185 -10,semicon,1,0.0,0.0,0.053621850000000006,0.05182185,0.053621850000000006 -11,semicon,1,0.0,0.0,0.05392185000000001,0.053621850000000006,0.05392185000000001 -12,tubular,1,0.0,0.0,0.057221850000000005,0.05392185000000001,0.057221850000000005 -13,insulator,1,0.0,0.0,0.06022185000000001,0.057221850000000005,0.06022185000000001 -14,insulator,1,0.0,0.0,0.06322185000000001,0.06022185000000001,0.06322185000000001 -15,circstrands,1,0.06613535000000001,0.0,0.0029135,, -15,circstrands,2,0.06585322825623381,0.00610220024703129,0.0029135,, -15,circstrands,3,0.06500926998817974,0.012152338673130116,0.0029135,, -15,circstrands,4,0.06361067555020951,0.018098797630463726,0.0029135,, -15,circstrands,5,0.06166937725693737,0.023890844027870527,0.0029135,, -15,circstrands,6,0.05920193758091903,0.029479062167705884,0.0029135,, -15,circstrands,7,0.05622940784747555,0.03481577534315093,0.0029135,, -15,circstrands,8,0.05277714863220819,0.03985545259906486,0.0029135,, -15,circstrands,9,0.04887461339350932,0.044555097186040345,0.0029135,, -15,circstrands,10,0.044555097186040345,0.04887461339350932,0.0029135,, -15,circstrands,11,0.03985545259906486,0.05277714863220819,0.0029135,, -15,circstrands,12,0.03481577534315094,0.05622940784747554,0.0029135,, -15,circstrands,13,0.029479062167705888,0.05920193758091903,0.0029135,, -15,circstrands,14,0.023890844027870534,0.06166937725693737,0.0029135,, -15,circstrands,15,0.018098797630463723,0.06361067555020951,0.0029135,, -15,circstrands,16,0.012152338673130115,0.06500926998817974,0.0029135,, -15,circstrands,17,0.006102200247031292,0.06585322825623381,0.0029135,, -15,circstrands,18,4.049622234399496e-18,0.06613535000000001,0.0029135,, -15,circstrands,19,-0.006102200247031284,0.06585322825623383,0.0029135,, -15,circstrands,20,-0.012152338673130121,0.06500926998817974,0.0029135,, -15,circstrands,21,-0.01809879763046373,0.06361067555020951,0.0029135,, -15,circstrands,22,-0.023890844027870527,0.06166937725693737,0.0029135,, -15,circstrands,23,-0.02947906216770588,0.05920193758091903,0.0029135,, -15,circstrands,24,-0.034815775343150945,0.05622940784747554,0.0029135,, -15,circstrands,25,-0.03985545259906485,0.0527771486322082,0.0029135,, -15,circstrands,26,-0.044555097186040345,0.04887461339350932,0.0029135,, -15,circstrands,27,-0.04887461339350931,0.04455509718604036,0.0029135,, -15,circstrands,28,-0.05277714863220819,0.03985545259906486,0.0029135,, -15,circstrands,29,-0.05622940784747555,0.03481577534315093,0.0029135,, -15,circstrands,30,-0.059201937580919024,0.02947906216770589,0.0029135,, -15,circstrands,31,-0.06166937725693737,0.023890844027870527,0.0029135,, -15,circstrands,32,-0.06361067555020951,0.01809879763046374,0.0029135,, -15,circstrands,33,-0.06500926998817974,0.012152338673130118,0.0029135,, -15,circstrands,34,-0.06585322825623383,0.006102200247031281,0.0029135,, -15,circstrands,35,-0.06613535000000001,8.099244468798992e-18,0.0029135,, -15,circstrands,36,-0.06585322825623381,-0.006102200247031294,0.0029135,, -15,circstrands,37,-0.06500926998817974,-0.012152338673130102,0.0029135,, -15,circstrands,38,-0.06361067555020951,-0.018098797630463726,0.0029135,, -15,circstrands,39,-0.061669377256937365,-0.023890844027870537,0.0029135,, -15,circstrands,40,-0.05920193758091903,-0.029479062167705877,0.0029135,, -15,circstrands,41,-0.05622940784747554,-0.03481577534315094,0.0029135,, -15,circstrands,42,-0.0527771486322082,-0.039855452599064846,0.0029135,, -15,circstrands,43,-0.04887461339350932,-0.044555097186040345,0.0029135,, -15,circstrands,44,-0.04455509718604034,-0.048874613393509334,0.0029135,, -15,circstrands,45,-0.03985545259906487,-0.052777148632208185,0.0029135,, -15,circstrands,46,-0.03481577534315095,-0.056229407847475536,0.0029135,, -15,circstrands,47,-0.02947906216770587,-0.05920193758091904,0.0029135,, -15,circstrands,48,-0.02389084402787053,-0.06166937725693737,0.0029135,, -15,circstrands,49,-0.018098797630463744,-0.06361067555020951,0.0029135,, -15,circstrands,50,-0.012152338673130094,-0.06500926998817975,0.0029135,, -15,circstrands,51,-0.0061022002470312855,-0.06585322825623383,0.0029135,, -15,circstrands,52,-1.2148866703198487e-17,-0.06613535000000001,0.0029135,, -15,circstrands,53,0.00610220024703126,-0.06585322825623383,0.0029135,, -15,circstrands,54,0.012152338673130128,-0.06500926998817974,0.0029135,, -15,circstrands,55,0.018098797630463723,-0.06361067555020951,0.0029135,, -15,circstrands,56,0.02389084402787051,-0.06166937725693738,0.0029135,, -15,circstrands,57,0.0294790621677059,-0.059201937580919024,0.0029135,, -15,circstrands,58,0.03481577534315094,-0.05622940784747554,0.0029135,, -15,circstrands,59,0.039855452599064846,-0.05277714863220821,0.0029135,, -15,circstrands,60,0.04455509718604036,-0.048874613393509306,0.0029135,, -15,circstrands,61,0.048874613393509334,-0.04455509718604034,0.0029135,, -15,circstrands,62,0.052777148632208185,-0.03985545259906487,0.0029135,, -15,circstrands,63,0.05622940784747553,-0.03481577534315096,0.0029135,, -15,circstrands,64,0.05920193758091904,-0.029479062167705874,0.0029135,, -15,circstrands,65,0.06166937725693737,-0.023890844027870534,0.0029135,, -15,circstrands,66,0.0636106755502095,-0.018098797630463747,0.0029135,, -15,circstrands,67,0.06500926998817974,-0.012152338673130099,0.0029135,, -15,circstrands,68,0.06585322825623383,-0.006102200247031289,0.0029135,, -16,insulator,1,0.0,0.0,0.07904885,0.06904885000000001,0.07904885 diff --git a/examples/cables_library.json b/examples/cables_library.json index ae62438a8..95f0e867a 100644 --- a/examples/cables_library.json +++ b/examples/cables_library.json @@ -1,778 +1,1390 @@ { - "__julia_type__": "LineCableModels.DataModel.CablesLibrary", - "data": { - "18kV_1000mm2": { - "cable_id": "18kV_1000mm2", - "nominal_data": { - "U": { - "value": 30, - "__type__": "Float" - }, - "screen_cross_section": { - "value": 35, - "__type__": "Float" - }, - "capacitance": { - "value": 0.39, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.DataModel.NominalData", - "U0": { - "value": 18, - "__type__": "Float" - }, - "conductor_cross_section": { - "value": 1000, - "__type__": "Float" - }, - "armor_cross_section": null, - "resistance": { - "value": 0.0291, - "__type__": "Float" - }, - "inductance": { - "value": 0.3, - "__type__": "Float" + "$schema": "https://json-schema.org/draft/2020-12/schema", + "root": { + "kind": "cable_library", + "cables": { + "18kV_1000mm2": { + "kind": "cable_design", + "cable_id": "18kV_1000mm2", + "nominal_data": [ + ], + "datasheet": { + "U": { + "value": 30, + "__type__": "Float64" + }, + "screen_cross_section": { + "value": 35, + "__type__": "Float64" + }, + "capacitance": { + "value": 0.39, + "__type__": "Float64" + }, + "U0": { + "value": 18, + "__type__": "Float64" + }, + "conductor_cross_section": { + "value": 1000, + "__type__": "Float64" + }, + "resistance": { + "value": 0.0291, + "__type__": "Float64" + }, + "inductance": { + "value": 0.3, + "__type__": "Float64" + }, + "designation_code": "NA2XS(FL)2Y" }, - "designation_code": "NA2XS(FL)2Y" - }, - "__julia_type__": "LineCableModels.DataModel.CableDesign", - "components": [ - { - "insulator_group": { - "__julia_type__": "LineCableModels.DataModel.InsulatorGroup", - "layers": [ - { - "material_props": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 32.3, - "__type__": "Float" - }, - "mu_r": { - "value": 1, - "__type__": "Float" - }, - "rho": { - "value": 5300, - "__type__": "Float" + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "stack", + "items": [ + { + "kind": "enclosure", + "tag": "stranded", + "item": { + "kind": "group", + "name": "core", + "pattern": null, + "compact": true, + "item": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "strand", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "wire", + "material": "conductor_1", + "primitive": { + "kind": "disk", + "r": { + "value": 0.00235, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + }, + { + "kind": "group", + "name": "strand", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "wire", + "material": "conductor_1", + "primitive": { + "kind": "disk", + "r": { + "value": 0.00235, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": [ + { + "kind": "helix", + "φ0": { + "value": 0, + "__type__": "Float64" + }, + "lay": { + "kind": "lay_ratio", + "q": { + "value": 15, + "__type__": "Float64" + } + }, + "dir": 1 + }, + { + "kind": "helix", + "φ0": { + "value": 0, + "__type__": "Float64" + }, + "lay": { + "kind": "lay_ratio", + "q": { + "value": 13.5, + "__type__": "Float64" + } + }, + "dir": 1 + }, + { + "kind": "helix", + "φ0": { + "value": 0, + "__type__": "Float64" + }, + "lay": { + "kind": "lay_ratio", + "q": { + "value": 12.5, + "__type__": "Float64" + } + }, + "dir": 1 + }, + { + "kind": "helix", + "φ0": { + "value": 0, + "__type__": "Float64" + }, + "lay": { + "kind": "lay_ratio", + "q": { + "value": 11, + "__type__": "Float64" + } + }, + "dir": 1 + } + ], + "boundary": null + } + ] + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": { + "kind": "disk", + "r": { + "value": 0.01905, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.01905, + "__type__": "Float64" + } + }, + "fill": "insulator_2", + "wall": null + }, + { + "kind": "region", + "tag": "screen", + "material": "semicon_3", + "primitive": { + "kind": "shell", + "t": { + "value": 0.0003, + "__type__": "Float64" + } + } + }, + { + "kind": "region", + "tag": "screen", + "material": "semicon_4", + "primitive": { + "kind": "shell", + "t": { + "value": 0.0006, + "__type__": "Float64" + } + } + }, + { + "kind": "region", + "tag": "insulation", + "material": "insulator_5", + "primitive": { + "kind": "shell", + "t": { + "value": 0.008, + "__type__": "Float64" + } + } + }, + { + "kind": "region", + "tag": "screen", + "material": "semicon_6", + "primitive": { + "kind": "shell", + "t": { + "value": 0.0003, + "__type__": "Float64" + } + } + }, + { + "kind": "region", + "tag": "screen", + "material": "semicon_3", + "primitive": { + "kind": "shell", + "t": { + "value": 0.0003, + "__type__": "Float64" + } + } } - }, - "r_in": { - "value": 0.02115, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.DataModel.Semicon", - "r_ex": { - "value": 0.02145, - "__type__": "Float" - }, - "temperature": { - "value": 20, - "__type__": "Float" - } + ] }, - { - "material_props": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 1000, - "__type__": "Float" - }, - "mu_r": { - "value": 1, - "__type__": "Float" - }, - "rho": { - "value": 1000, - "__type__": "Float" - } - }, - "r_in": { - "value": 0.02145, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.DataModel.Semicon", - "r_ex": { - "value": 0.02205, - "__type__": "Float" - }, - "temperature": { - "value": 20, - "__type__": "Float" - } - }, - { - "material_props": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 2.3, - "__type__": "Float" - }, - "mu_r": { - "value": 1, - "__type__": "Float" - }, - "rho": { - "value": 197000000000000, - "__type__": "Float" - } - }, - "r_in": { - "value": 0.02205, - "__type__": "Float" + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" }, - "__julia_type__": "LineCableModels.DataModel.Insulator", - "r_ex": { - "value": 0.03005, - "__type__": "Float" + "φ": { + "value": 0, + "__type__": "Float64" }, - "temperature": { - "value": 20, - "__type__": "Float" + "y": { + "value": 0, + "__type__": "Float64" } }, - { - "material_props": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 1000, - "__type__": "Float" - }, - "mu_r": { - "value": 1, - "__type__": "Float" - }, - "rho": { - "value": 500, - "__type__": "Float" + "path": null, + "boundary": null + }, + { + "kind": "group", + "name": "sheath", + "pattern": null, + "compact": null, + "item": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "screen_wire", + "pattern": { + "kind": "ring", + "span": { + "value": 6.283185307179586, + "__type__": "Float64" + }, + "φ0": { + "value": 0, + "__type__": "Float64" + }, + "gap_frac": { + "value": 0, + "__type__": "Float64" + }, + "r": { + "value": 0.029025000000000006, + "__type__": "Float64" + }, + "n": 49 + }, + "compact": null, + "item": { + "kind": "region", + "tag": "screen_wire", + "material": "conductor_7", + "primitive": { + "kind": "disk", + "r": { + "value": 0.000475, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": { + "kind": "helix", + "φ0": { + "value": 0, + "__type__": "Float64" + }, + "lay": { + "kind": "lay_ratio", + "q": { + "value": 10, + "__type__": "Float64" + } + }, + "dir": 1 + }, + "boundary": null + }, + { + "kind": "group", + "name": "tapes", + "pattern": { + "kind": "ring", + "span": { + "value": 6.283185307179586, + "__type__": "Float64" + }, + "φ0": { + "value": 0, + "__type__": "Float64" + }, + "gap_frac": { + "value": 0, + "__type__": "Float64" + }, + "r": null, + "n": 1 + }, + "compact": null, + "item": { + "kind": "region", + "tag": "copper_tape", + "material": "conductor_7", + "primitive": { + "kind": "rectangle", + "w": { + "value": 0.01, + "__type__": "Float64" + }, + "h": { + "value": 0.0001, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": { + "kind": "helix", + "φ0": { + "value": 0, + "__type__": "Float64" + }, + "lay": { + "kind": "lay_ratio", + "q": { + "value": 10, + "__type__": "Float64" + } + }, + "dir": 1 + }, + "boundary": null } - }, - "r_in": { - "value": 0.03005, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.DataModel.Semicon", - "r_ex": { - "value": 0.030350000000000002, - "__type__": "Float" - }, - "temperature": { - "value": 20, - "__type__": "Float" - } + ] }, - { - "material_props": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 32.3, - "__type__": "Float" - }, - "mu_r": { - "value": 1, - "__type__": "Float" - }, - "rho": { - "value": 5300, - "__type__": "Float" - } - }, - "r_in": { - "value": 0.030350000000000002, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.DataModel.Semicon", - "r_ex": { - "value": 0.030650000000000004, - "__type__": "Float" - }, - "temperature": { - "value": 20, - "__type__": "Float" - } - } - ] - }, - "__julia_type__": "LineCableModels.DataModel.CableComponent", - "id": "core", - "conductor_group": { - "__julia_type__": "LineCableModels.DataModel.ConductorGroup", - "layers": [ - { - "lay_ratio": { + "at": { + "kind": "pose2", + "x": { "value": 0, - "__type__": "Float" + "__type__": "Float64" }, - "material_props": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0.00429, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 1, - "__type__": "Float" - }, - "mu_r": { - "value": 1.000022, - "__type__": "Float" - }, - "rho": { - "value": 2.8264e-8, - "__type__": "Float" - } - }, - "lay_direction": { - "value": 1, - "__type__": "Int" - }, - "r_in": { + "φ": { "value": 0, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.DataModel.CircStrands", - "radius_wire": { - "value": 0.00235, - "__type__": "Float" - }, - "num_wires": { - "value": 1, - "__type__": "Int" + "__type__": "Float64" }, - "temperature": { - "value": 20, - "__type__": "Float" + "y": { + "value": 0, + "__type__": "Float64" } }, - { - "lay_ratio": { - "value": 15, - "__type__": "Float" - }, - "material_props": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0.00429, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 1, - "__type__": "Float" - }, - "mu_r": { - "value": 1.000022, - "__type__": "Float" - }, - "rho": { - "value": 2.8264e-8, - "__type__": "Float" - } - }, - "lay_direction": { - "value": 1, - "__type__": "Int" - }, - "r_in": { - "value": 0.00235, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.DataModel.CircStrands", - "radius_wire": { - "value": 0.00235, - "__type__": "Float" - }, - "num_wires": { - "value": 6, - "__type__": "Int" - }, - "temperature": { - "value": 20, - "__type__": "Float" + "path": null, + "boundary": null + }, + { + "kind": "region", + "tag": "screen", + "material": "semicon_3", + "primitive": { + "kind": "shell", + "t": { + "value": 0.0003, + "__type__": "Float64" } - }, - { - "lay_ratio": { - "value": 13.5, - "__type__": "Float" - }, - "material_props": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0.00429, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 1, - "__type__": "Float" - }, - "mu_r": { - "value": 1.000022, - "__type__": "Float" - }, - "rho": { - "value": 2.8264e-8, - "__type__": "Float" + } + }, + { + "kind": "group", + "name": "jacket", + "pattern": null, + "compact": null, + "item": { + "kind": "stack", + "items": [ + { + "kind": "region", + "tag": "sheath", + "material": "conductor_1", + "primitive": { + "kind": "shell", + "t": { + "value": 0.00015, + "__type__": "Float64" + } + } } - }, - "lay_direction": { - "value": 1, - "__type__": "Int" - }, - "r_in": { - "value": 0.007050000000000001, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.DataModel.CircStrands", - "radius_wire": { - "value": 0.00235, - "__type__": "Float" - }, - "num_wires": { - "value": 12, - "__type__": "Int" - }, - "temperature": { - "value": 20, - "__type__": "Float" - } + ] }, - { - "lay_ratio": { - "value": 12.5, - "__type__": "Float" - }, - "material_props": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0.00429, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 1, - "__type__": "Float" - }, - "mu_r": { - "value": 1.000022, - "__type__": "Float" - }, - "rho": { - "value": 2.8264e-8, - "__type__": "Float" - } - }, - "lay_direction": { - "value": 1, - "__type__": "Int" - }, - "r_in": { - "value": 0.01175, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.DataModel.CircStrands", - "radius_wire": { - "value": 0.00235, - "__type__": "Float" + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" }, - "num_wires": { - "value": 18, - "__type__": "Int" + "φ": { + "value": 0, + "__type__": "Float64" }, - "temperature": { - "value": 20, - "__type__": "Float" + "y": { + "value": 0, + "__type__": "Float64" } }, - { - "lay_ratio": { - "value": 11, - "__type__": "Float" - }, - "material_props": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0.00429, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 1, - "__type__": "Float" - }, - "mu_r": { - "value": 1.000022, - "__type__": "Float" - }, - "rho": { - "value": 2.8264e-8, - "__type__": "Float" - } - }, - "lay_direction": { - "value": 1, - "__type__": "Int" - }, - "r_in": { - "value": 0.01645, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.DataModel.CircStrands", - "radius_wire": { - "value": 0.00235, - "__type__": "Float" - }, - "num_wires": { - "value": 24, - "__type__": "Int" - }, - "temperature": { - "value": 20, - "__type__": "Float" + "path": null, + "boundary": null + }, + { + "kind": "region", + "tag": "jacket", + "material": "insulator_5", + "primitive": { + "kind": "shell", + "t": { + "value": 5.0e-5, + "__type__": "Float64" } } - ] - } - }, - { - "insulator_group": { - "__julia_type__": "LineCableModels.DataModel.InsulatorGroup", - "layers": [ - { - "material_props": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 32.3, - "__type__": "Float" - }, - "mu_r": { - "value": 1, - "__type__": "Float" - }, - "rho": { - "value": 5300, - "__type__": "Float" - } - }, - "r_in": { - "value": 0.031700000000000006, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.DataModel.Semicon", - "r_ex": { - "value": 0.03200000000000001, - "__type__": "Float" - }, - "temperature": { - "value": 20, - "__type__": "Float" + }, + { + "kind": "region", + "tag": "jacket", + "material": "insulator_5", + "primitive": { + "kind": "shell", + "t": { + "value": 0.0024, + "__type__": "Float64" } } - ] + } + ] + } + }, + "525kV_1600mm2": { + "kind": "cable_design", + "cable_id": "525kV_1600mm2", + "nominal_data": [ + ], + "datasheet": { + "U": { + "value": 525, + "__type__": "Float64" }, - "__julia_type__": "LineCableModels.DataModel.CableComponent", - "id": "sheath", - "conductor_group": { - "__julia_type__": "LineCableModels.DataModel.ConductorGroup", - "layers": [ - { - "lay_ratio": { - "value": 10, - "__type__": "Float" - }, - "material_props": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0.00393, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 1, - "__type__": "Float" - }, - "mu_r": { - "value": 0.999994, - "__type__": "Float" - }, - "rho": { - "value": 1.7241e-8, - "__type__": "Float" + "screen_cross_section": { + "value": 1000, + "__type__": "Float64" + }, + "capacitance": null, + "U0": { + "value": 500, + "__type__": "Float64" + }, + "conductor_cross_section": { + "value": 1600, + "__type__": "Float64" + }, + "resistance": null, + "inductance": null, + "designation_code": "(N)2XH(F)RK2Y" + }, + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "stack", + "items": [ + { + "kind": "enclosure", + "tag": "stranded", + "item": { + "kind": "group", + "name": "core", + "pattern": null, + "compact": true, + "item": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "strand", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "wire", + "material": "conductor_7", + "primitive": { + "kind": "disk", + "r": { + "value": 0.00183245, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + }, + { + "kind": "group", + "name": "strand", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "wire", + "material": "conductor_7", + "primitive": { + "kind": "disk", + "r": { + "value": 0.00183245, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": { + "kind": "helix", + "φ0": { + "value": 0, + "__type__": "Float64" + }, + "lay": { + "kind": "lay_ratio", + "q": { + "value": 11, + "__type__": "Float64" + } + }, + "dir": 1 + }, + "boundary": null + } + ] + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": { + "kind": "disk", + "r": { + "value": 0.02315, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.02315, + "__type__": "Float64" + } + }, + "fill": "insulator_2", + "wall": null + }, + { + "kind": "region", + "tag": "screen", + "material": "semicon_4", + "primitive": { + "kind": "shell", + "t": { + "value": 0.002, + "__type__": "Float64" + } + } + }, + { + "kind": "region", + "tag": "insulation", + "material": "insulator_5", + "primitive": { + "kind": "shell", + "t": { + "value": 0.026, + "__type__": "Float64" + } + } + }, + { + "kind": "region", + "tag": "screen", + "material": "semicon_6", + "primitive": { + "kind": "shell", + "t": { + "value": 0.0018, + "__type__": "Float64" + } + } + }, + { + "kind": "region", + "tag": "screen", + "material": "semicon_3", + "primitive": { + "kind": "shell", + "t": { + "value": 0.0003, + "__type__": "Float64" + } + } } - }, - "lay_direction": { - "value": 1, - "__type__": "Int" - }, - "r_in": { - "value": 0.030650000000000004, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.DataModel.CircStrands", - "radius_wire": { - "value": 0.000475, - "__type__": "Float" - }, - "num_wires": { - "value": 49, - "__type__": "Int" - }, - "temperature": { - "value": 20, - "__type__": "Float" - } + ] }, - { - "lay_ratio": { - "value": 10, - "__type__": "Float" - }, - "material_props": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0.00393, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 1, - "__type__": "Float" - }, - "mu_r": { - "value": 0.999994, - "__type__": "Float" - }, - "rho": { - "value": 1.7241e-8, - "__type__": "Float" - } - }, - "lay_direction": { - "value": 1, - "__type__": "Int" - }, - "r_in": { - "value": 0.0316, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.DataModel.Strip", - "width": { - "value": 0.01, - "__type__": "Float" + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" }, - "r_ex": { - "value": 0.031700000000000006, - "__type__": "Float" + "φ": { + "value": 0, + "__type__": "Float64" }, - "temperature": { - "value": 20, - "__type__": "Float" + "y": { + "value": 0, + "__type__": "Float64" } - } - ] - } - }, - { - "insulator_group": { - "__julia_type__": "LineCableModels.DataModel.InsulatorGroup", - "layers": [ - { - "material_props": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 2.3, - "__type__": "Float" - }, - "mu_r": { - "value": 1, - "__type__": "Float" - }, - "rho": { - "value": 197000000000000, - "__type__": "Float" + }, + "path": null, + "boundary": null + }, + { + "kind": "group", + "name": "sheath", + "pattern": null, + "compact": null, + "item": { + "kind": "stack", + "items": [ + { + "kind": "region", + "tag": "sheath", + "material": "conductor_8", + "primitive": { + "kind": "shell", + "t": { + "value": 0.0033, + "__type__": "Float64" + } + } } + ] + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" }, - "r_in": { - "value": 0.032150000000000005, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.DataModel.Insulator", - "r_ex": { - "value": 0.032200000000000006, - "__type__": "Float" + "φ": { + "value": 0, + "__type__": "Float64" }, - "temperature": { - "value": 20, - "__type__": "Float" + "y": { + "value": 0, + "__type__": "Float64" } }, - { - "material_props": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 2.3, - "__type__": "Float" - }, - "mu_r": { - "value": 1, - "__type__": "Float" - }, - "rho": { - "value": 197000000000000, - "__type__": "Float" - } - }, - "r_in": { - "value": 0.032200000000000006, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.DataModel.Insulator", - "r_ex": { - "value": 0.034600000000000006, - "__type__": "Float" - }, - "temperature": { - "value": 20, - "__type__": "Float" + "path": null, + "boundary": null + }, + { + "kind": "region", + "tag": "jacket", + "material": "insulator_5", + "primitive": { + "kind": "shell", + "t": { + "value": 0.003, + "__type__": "Float64" } } - ] - }, - "__julia_type__": "LineCableModels.DataModel.CableComponent", - "id": "jacket", - "conductor_group": { - "__julia_type__": "LineCableModels.DataModel.ConductorGroup", - "layers": [ - { - "material_props": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0.00429, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 1, - "__type__": "Float" - }, - "mu_r": { - "value": 1.000022, - "__type__": "Float" - }, - "rho": { - "value": 2.8264e-8, - "__type__": "Float" + }, + { + "kind": "region", + "tag": "bedding", + "material": "insulator_9", + "primitive": { + "kind": "shell", + "t": { + "value": 0.003, + "__type__": "Float64" + } + } + }, + { + "kind": "group", + "name": "armor", + "pattern": null, + "compact": null, + "item": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "armor_wire", + "pattern": { + "kind": "ring", + "span": { + "value": 6.283185307179586, + "__type__": "Float64" + }, + "φ0": { + "value": 0, + "__type__": "Float64" + }, + "gap_frac": { + "value": 0, + "__type__": "Float64" + }, + "r": null, + "n": 68 + }, + "compact": null, + "item": { + "kind": "region", + "tag": "armor_wire", + "material": "conductor_10", + "primitive": { + "kind": "disk", + "r": { + "value": 0.0029135, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": { + "kind": "helix", + "φ0": { + "value": 0, + "__type__": "Float64" + }, + "lay": { + "kind": "lay_ratio", + "q": { + "value": 10, + "__type__": "Float64" + } + }, + "dir": 1 + }, + "boundary": null } + ] + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" }, - "r_in": { - "value": 0.03200000000000001, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.DataModel.Tubular", - "r_ex": { - "value": 0.032150000000000005, - "__type__": "Float" + "φ": { + "value": 0, + "__type__": "Float64" }, - "temperature": { - "value": 20, - "__type__": "Float" + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + }, + { + "kind": "region", + "tag": "jacket", + "material": "insulator_9", + "primitive": { + "kind": "shell", + "t": { + "value": 0.01, + "__type__": "Float64" } } - ] - } + } + ] } - ] + } + } + }, + "format": "linecablemodels.cable", + "materials": { + "semicon_3": { + "kind": "semicon", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0, + "__type__": "Float64" + }, + "eps_r": { + "value": 32.3, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 5300, + "__type__": "Float64" + } + }, + "semicon_6": { + "kind": "semicon", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0, + "__type__": "Float64" + }, + "eps_r": { + "value": 1000, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 500, + "__type__": "Float64" + } + }, + "conductor_8": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.004, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999983, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 2.14e-7, + "__type__": "Float64" + } + }, + "semicon_4": { + "kind": "semicon", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0, + "__type__": "Float64" + }, + "eps_r": { + "value": 1000, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1000, + "__type__": "Float64" + } + }, + "insulator_5": { + "kind": "insulator", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0, + "__type__": "Float64" + }, + "eps_r": { + "value": 2.3, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 197000000000000, + "__type__": "Float64" + } + }, + "conductor_1": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00429, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 1.000022, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 2.8264e-8, + "__type__": "Float64" + } + }, + "conductor_7": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "insulator_9": { + "kind": "insulator", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0, + "__type__": "Float64" + }, + "eps_r": { + "value": 2.8, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1000000000000000, + "__type__": "Float64" + } + }, + "conductor_10": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.0045, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 300, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.38e-7, + "__type__": "Float64" + } + }, + "insulator_2": { + "kind": "insulator", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "special": "Inf", + "__type__": "Float64" + } } - } -} \ No newline at end of file + }, + "version": "1.0.0" +} diff --git a/examples/materials_library.json b/examples/materials_library.json index e45ccd087..09f4e71bd 100644 --- a/examples/materials_library.json +++ b/examples/materials_library.json @@ -1,462 +1,904 @@ { - "pe": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 2.3, - "__type__": "Float" - }, - "mu_r": { - "value": 1, - "__type__": "Float" - }, - "rho": { - "value": 197000000000000, - "__type__": "Float" - } - }, - "aluminum_corrected": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0.00403, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 1, - "__type__": "Float" - }, - "mu_r": { - "value": 0.999994, - "__type__": "Float" - }, - "rho": { - "value": 3.03e-8, - "__type__": "Float" - } - }, - "carbon_pe": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 1000, - "__type__": "Float" - }, - "mu_r": { - "value": 1, - "__type__": "Float" - }, - "rho": { - "value": 0.06, - "__type__": "Float" - } - }, - "lead": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0.004, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 1, - "__type__": "Float" - }, - "mu_r": { - "value": 0.999983, - "__type__": "Float" - }, - "rho": { - "value": 2.14e-7, - "__type__": "Float" + "$schema": "https://json-schema.org/draft/2020-12/schema", + "format": "linecablemodels.materials", + "materials": { + "aluminum_corrected": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00403, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 3.03e-8, + "__type__": "Float64" + } + }, + "pe": { + "kind": "insulator", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0, + "__type__": "Float64" + }, + "eps_r": { + "value": 2.3, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 197000000000000, + "__type__": "Float64" + } + }, + "bronze": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.003, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 3.5e-8, + "__type__": "Float64" + } + }, + "copper": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "pec": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 2.220446049250313e-16, + "__type__": "Float64" + } + }, + "conductive_paper": { + "kind": "semicon", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0, + "__type__": "Float64" + }, + "eps_r": { + "value": 8.6, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 18.5, + "__type__": "Float64" + } + }, + "laminated_paper": { + "kind": "insulator", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0, + "__type__": "Float64" + }, + "eps_r": { + "value": 2.8, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1000000000000000, + "__type__": "Float64" + } + }, + "polyacrylate": { + "kind": "semicon", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0, + "__type__": "Float64" + }, + "eps_r": { + "value": 32.3, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 5300, + "__type__": "Float64" + } + }, + "semicon1": { + "kind": "semicon", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0, + "__type__": "Float64" + }, + "eps_r": { + "value": 1000, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1000, + "__type__": "Float64" + } + }, + "xlpe": { + "kind": "insulator", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0, + "__type__": "Float64" + }, + "eps_r": { + "value": 2.5, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 197000000000000, + "__type__": "Float64" + } + }, + "semicon2": { + "kind": "semicon", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0, + "__type__": "Float64" + }, + "eps_r": { + "value": 1000, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 500, + "__type__": "Float64" + } + }, + "aluminum": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00429, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 1.000022, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 2.8264e-8, + "__type__": "Float64" + } + }, + "carbon_pe": { + "kind": "semicon", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0, + "__type__": "Float64" + }, + "eps_r": { + "value": 1000, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 0.06, + "__type__": "Float64" + } + }, + "lead": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.004, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999983, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 2.14e-7, + "__type__": "Float64" + } + }, + "copper_corrected": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.835e-8, + "__type__": "Float64" + } + }, + "steel_reference": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.0045, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 300, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.38e-7, + "__type__": "Float64" + } + }, + "air": { + "kind": "insulator", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "special": "Inf", + "__type__": "Float64" + } + }, + "epr": { + "kind": "insulator", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.005, + "__type__": "Float64" + }, + "eps_r": { + "value": 3, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1000000000000000, + "__type__": "Float64" + } + }, + "pvc": { + "kind": "insulator", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.1, + "__type__": "Float64" + }, + "eps_r": { + "value": 8, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1000000000000000, + "__type__": "Float64" + } + }, + "pp": { + "kind": "insulator", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0, + "__type__": "Float64" + }, + "eps_r": { + "value": 2.8, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1000000000000000, + "__type__": "Float64" + } + }, + "lead_reference": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.004, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999983, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 2.14e-7, + "__type__": "Float64" + } + }, + "stainless_steel": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 500, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 7.0e-7, + "__type__": "Float64" + } + }, + "steel": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.0045, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 300, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.38e-7, + "__type__": "Float64" + } } }, - "steel": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0.0045, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 1, - "__type__": "Float" - }, - "mu_r": { - "value": 300, - "__type__": "Float" - }, - "rho": { - "value": 1.38e-7, - "__type__": "Float" - } - }, - "copper": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0.00393, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 1, - "__type__": "Float" - }, - "mu_r": { - "value": 0.999994, - "__type__": "Float" - }, - "rho": { - "value": 1.7241e-8, - "__type__": "Float" - } - }, - "copper_corrected": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0.00393, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 1, - "__type__": "Float" - }, - "mu_r": { - "value": 0.999994, - "__type__": "Float" - }, - "rho": { - "value": 1.835e-8, - "__type__": "Float" - } - }, - "pec": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 1, - "__type__": "Float" - }, - "mu_r": { - "value": 1, - "__type__": "Float" - }, - "rho": { - "value": 2.220446049250313e-16, - "__type__": "Float" - } - }, - "conductive_paper": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 8.6, - "__type__": "Float" - }, - "mu_r": { - "value": 1, - "__type__": "Float" - }, - "rho": { - "value": 18.5, - "__type__": "Float" - } - }, - "air": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 1, - "__type__": "Float" - }, - "mu_r": { - "value": 1, - "__type__": "Float" - }, - "rho": { - "value": "Inf", - "__type__": "SpecialFloat" - } - }, - "laminated_paper": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 2.8, - "__type__": "Float" - }, - "mu_r": { - "value": 1, - "__type__": "Float" - }, - "rho": { - "value": 1000000000000000, - "__type__": "Float" - } - }, - "polyacrylate": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 32.3, - "__type__": "Float" - }, - "mu_r": { - "value": 1, - "__type__": "Float" - }, - "rho": { - "value": 5300, - "__type__": "Float" - } - }, - "semicon1": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 1000, - "__type__": "Float" - }, - "mu_r": { - "value": 1, - "__type__": "Float" - }, - "rho": { - "value": 1000, - "__type__": "Float" - } - }, - "epr": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0.005, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 3, - "__type__": "Float" - }, - "mu_r": { - "value": 1, - "__type__": "Float" - }, - "rho": { - "value": 1000000000000000, - "__type__": "Float" - } - }, - "xlpe": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 2.5, - "__type__": "Float" - }, - "mu_r": { - "value": 1, - "__type__": "Float" - }, - "rho": { - "value": 197000000000000, - "__type__": "Float" - } - }, - "semicon2": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 1000, - "__type__": "Float" - }, - "mu_r": { - "value": 1, - "__type__": "Float" - }, - "rho": { - "value": 500, - "__type__": "Float" - } - }, - "pvc": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0.1, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 8, - "__type__": "Float" - }, - "mu_r": { - "value": 1, - "__type__": "Float" - }, - "rho": { - "value": 1000000000000000, - "__type__": "Float" - } - }, - "pp": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 2.8, - "__type__": "Float" - }, - "mu_r": { - "value": 1, - "__type__": "Float" - }, - "rho": { - "value": 1000000000000000, - "__type__": "Float" - } - }, - "stainless_steel": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 1, - "__type__": "Float" - }, - "mu_r": { - "value": 500, - "__type__": "Float" - }, - "rho": { - "value": 7.0e-7, - "__type__": "Float" - } - }, - "aluminum": { - "T0": { - "value": 20, - "__type__": "Float" - }, - "alpha": { - "value": 0.00429, - "__type__": "Float" - }, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": { - "value": 1, - "__type__": "Float" - }, - "mu_r": { - "value": 1.000022, - "__type__": "Float" - }, - "rho": { - "value": 2.8264e-8, - "__type__": "Float" - } - } + "version": "1.0.0" } \ No newline at end of file diff --git a/examples/pscad/compute.jl b/examples/pscad/compute.jl new file mode 100644 index 000000000..cc2f4e2e0 --- /dev/null +++ b/examples/pscad/compute.jl @@ -0,0 +1,19 @@ +using LineCableModels + +# The backend uses the filename supplied by the caller without searching for one. +config_path = isempty(ARGS) ? ENV["LINECABLEMODELS_PSCAD_CONFIG"] : only(ARGS) +station = PSCAD.RemoteConfig(config_path) + +copper = Material(:conductor, 1.72e-8, 1.0) +dielectric = Material(:insulator, 1e14, 2.3) +design = build(CableDesign, "insulated-wire", terminal(:core, + solid(copper, Disk(0.004)), insulation(dielectric; t=0.002))) +system = build(LineCableSystem, [design, design], [Pose2(0, -1), Pose2(1, -1)]; + connections=[Dict(:core=>1), Dict(:core=>2)]) +problem = LineParametersProblem(system; earth_props=homogeneous(rho=100.0), + frequencies=10.0 .^ range(-1, 6; length=101)) +selected = Formulation(:pscad; earth_impedance=:wedepohl1973) +@time result = compute(problem, selected; + options=(remote=station, verbosity=(default=0, PSCAD=1))) +Zresult = Z(result) +Yresult = Y(result) diff --git a/examples/pscad/remote.example.toml b/examples/pscad/remote.example.toml new file mode 100644 index 000000000..d179f4e83 --- /dev/null +++ b/examples/pscad/remote.example.toml @@ -0,0 +1,20 @@ +# Copy to a private filename of your choice and edit the station paths. +# local_root and shared_root must expose the same exchanged files. +host = "pscad-host" +local_root = "exchange" +shared_root = 'Z:\shared\exchange' +remote_root = 'C:\LCM\scratch' +julia_executable = 'C:\Julia\bin\julia.exe' +python_executable = 'C:\Python\python.exe' +pscad_version = "5.1.0" +timeout = 1800 +transport = "ssh" + +# For direct Windows execution, use transport = "local", host = "localhost", +# and the same absolute Windows folder for local_root and shared_root. +# remote_root must be separate scratch storage. + +# For a custom wrapper, replace transport above with "command" and supply: +# `command = ["ts", "ssh", "{host}", "--direct", "-o", "ServerAliveInterval=15", "-o", "ServerAliveCountMax=3", "--"]` +# Remove `--direct` to use that wrapper's Tailscale network route. +# Ordinary SSH can instead use a user-configured SSH alias or tunnel. diff --git a/examples/tutorial1.jl b/examples/tutorial1.jl index f38e8660a..5bdc55226 100644 --- a/examples/tutorial1.jl +++ b/examples/tutorial1.jl @@ -1,134 +1,292 @@ #= -# Tutorial 1 - Using the materials library +# Tutorial 1 - Using the materials library -This tutorial demonstrates how to manage material properties for power cable modeling using the package [`LineCableModels.jl`](@ref). Accurate knowledge of electromagnetic properties is essential for reliable cable design and analysis. - -Beyond showcasing the API, this guide serves as a practical reference by providing standard property values from recognized industry sources like CIGRE TB-531 [cigre531](@cite) and IEC 60287 [IEC60287](@cite) that can be stored and consistently applied across multiple design iterations and simulation studies. +Create, inspect, modify, save, and reload a materials library for cable +calculations. The examples use electromagnetic property values from CIGRE +TB-531 [cigre531](@cite) and IEC 60287 [IEC60287](@cite). =# #= **Tutorial outline** ```@contents Pages = [ - "tutorial1.md", + "tutorial1.md", ] Depth = 2:3 ``` =# #= -## Getting started +## Getting started + +[`MaterialsLibrary`](@ref) stores [`Material`](@ref) values under string names. +Its default constructor includes the package's built-in material records. +The library supports ordinary dictionary operations for lookup, replacement, +and removal. =# -# Load the package: -using LineCableModels -using DataFrames -fullfile(filename) = joinpath(@__DIR__, filename); #hide -set_verbosity!(0); #hide +using LineCableModels: Material, MaterialsLibrary, add!, load!, save + +# Create and inspect a library containing the built-in materials: +materials = MaterialsLibrary() + +# The library display summarizes the stored names and material classes. +# Retrieve an individual material to inspect its properties: +materials["copper"] #= -The [`MaterialsLibrary`](@ref) is a container for storing electromagnetic properties of -different materials used in power cables. By default, it initializes with several common -materials with their standard properties. +## Material properties + +The material declarations below specify the following properties: + +| Keyword | Meaning | Unit | +|:--|:--|:--| +| `kind` | Material class: `:conductor`, `:insulator`, or `:semicon` in this tutorial | None | +| `rho` | Electrical resistivity at `T0` | Ω·m | +| `eps_r` | Relative permittivity | Dimensionless | +| `mu_r` | Relative permeability | Dimensionless | +| `T0` | Reference temperature | °C | +| `alpha` | Temperature coefficient of resistivity | 1/°C | + +A material's library name identifies the entry, while `kind` describes its +physical class. Materials with different names and resistivities can share +`kind=:conductor` because they belong to the same physical class. + +Construct a material with named properties, then use [`add!`](@ref) to insert +it under a new name. `add!` rejects an existing name rather than silently +replacing its value. =# -# Initialize a [`MaterialsLibrary`](@ref) with default values: -materials = MaterialsLibrary() +#= +## Adding conductor materials + +The following records retain the conductor values used in this example from +[cigre531](@cite) and [IEC60287](@cite). The corrected copper and aluminum +resistivities are attributed to IEC 60287-3-2. + +The built-in copper, aluminum, lead, and steel entries remain unchanged. +Distinct names keep these additional records available alongside them. +=# + +copper_corrected = Material( + kind = :conductor, + rho = 1.835e-8, + eps_r = 1.0, + mu_r = 0.999994, + T0 = 20.0, + alpha = 0.00393, +) +add!(materials, "copper_corrected", copper_corrected); -# Inspect the contents of the materials library: -materials_df = DataFrame(materials) +aluminum_corrected = Material( + kind = :conductor, + rho = 3.03e-8, + eps_r = 1.0, + mu_r = 0.999994, + T0 = 20.0, + alpha = 0.00403, +) +add!(materials, "aluminum_corrected", aluminum_corrected); + +# Lead or lead alloy: +lead = Material( + kind = :conductor, + rho = 21.4e-8, + eps_r = 1.0, + mu_r = 0.999983, + T0 = 20.0, + alpha = 0.00400, +) +add!(materials, "lead_reference", lead); + +steel = Material( + kind = :conductor, + rho = 13.8e-8, + eps_r = 1.0, + mu_r = 300.0, + T0 = 20.0, + alpha = 0.00450, +) +add!(materials, "steel_reference", steel); + +bronze = Material( + kind = :conductor, + rho = 3.5e-8, + eps_r = 1.0, + mu_r = 1.0, + T0 = 20.0, + alpha = 0.00300, +) +add!(materials, "bronze", bronze); + +stainless_steel = Material( + kind = :conductor, + rho = 70.0e-8, + eps_r = 1.0, + mu_r = 500.0, + T0 = 20.0, + alpha = 0.0, +) +add!(materials, "stainless_steel", stainless_steel); #= -The function [`DataFrame`](@ref) returns a `DataFrame` with all materials and their properties, namely: electrical resistivity, relative permittivity, relative permeability, reference temperature, and temperature coefficient. +## Adding insulation and semiconducting materials + +The following declarations retain the insulation and semiconducting material +values used in this example, with dielectric properties attributed to Table 6 +of [cigre531](@cite). =# -# ## Adding new materials +# EPR: ethylene propylene rubber. +epr = Material( + kind = :insulator, + rho = 1e15, + eps_r = 3.0, + mu_r = 1.0, + T0 = 20.0, + alpha = 0.005, +) +add!(materials, "epr", epr); + +# PVC: polyvinyl chloride. +pvc = Material( + kind = :insulator, + rho = 1e15, + eps_r = 8.0, + mu_r = 1.0, + T0 = 20.0, + alpha = 0.1, +) +add!(materials, "pvc", pvc); + +# Laminated paper propylene. +laminated_paper = Material( + kind = :insulator, + rho = 1e15, + eps_r = 2.8, + mu_r = 1.0, + T0 = 20.0, + alpha = 0.0, +) +add!(materials, "laminated_paper", laminated_paper); + +# Carbon-polyethylene semiconducting compound. +carbon_pe = Material( + kind = :semicon, + rho = 0.06, + eps_r = 1e3, + mu_r = 1.0, + T0 = 20.0, + alpha = 0.0, +) +add!(materials, "carbon_pe", carbon_pe); + +# Conductive paper used as a semiconducting layer. +conductive_paper = Material( + kind = :semicon, + rho = 18.5, + eps_r = 8.6, + mu_r = 1.0, + T0 = 20.0, + alpha = 0.0, +) +add!(materials, "conductive_paper", conductive_paper); + +# Inspect the populated library: +materials + #= -!!! note "Note" - New materials can be added to the library using the [`Material`](@ref) constructor followed by [`add!`](@ref). +## Modifying a material entry + +`Material` values are immutable. To change properties, construct a new value +from an existing material and override the desired keywords. Unspecified +properties are retained. -It might be useful to add other conductor materials with corrected properties based on recognized standards [cigre531](@cite) [IEC60287](@cite). +Indexed assignment explicitly replaces a library entry. This differs from +`add!`, which requires a new name. Use a temporary copper entry to demonstrate +the operation without altering any of the reference records above. =# -copper_corrected = Material(1.835e-8, 1.0, 0.999994, 20.0, 0.00393) # Copper with corrected resistivity from IEC 60287-3-2 -add!(materials, "copper_corrected", copper_corrected) -aluminum_corrected = Material(3.03e-8, 1.0, 0.999994, 20.0, 0.00403) # Aluminum with corrected resistivity from IEC 60287-3-2 -add!(materials, "aluminum_corrected", aluminum_corrected) -# lead = Material(21.4e-8, 1.0, 0.999983, 20.0, 0.00400) # Lead or lead alloy -# add!(materials, "lead", lead) -# steel = Material(13.8e-8, 1.0, 300.0, 20.0, 0.00450) # Steel -# add!(materials, "steel", steel) -# bronze = Material(3.5e-8, 1.0, 1.0, 20.0, 0.00300) # Bronze -# add!(materials, "bronze", bronze) -stainless_steel = Material(70.0e-8, 1.0, 500.0, 20.0, 0.0) # Stainless steel -add!(materials, "stainless_steel", stainless_steel) +add!(materials, "copper_trial", materials["copper"]); + +materials["copper_trial"] = Material( + materials["copper_trial"]; + rho = 1.835e-8, +); + +# The trial has the new resistivity. The built-in copper is unchanged: +materials["copper_trial"] +materials["copper"] + +# Remove the temporary entry before saving the reference library: +delete!(materials, "copper_trial"); #= -When modeling cables for EMT analysis, one might be concerned with the impact of insulators and semiconductive layers on cable constants. Common insulation materials and semicons with different dielectric properties are reported in Table 6 of [cigre531](@cite). Let us include some of these materials in the [`MaterialsLibrary`](@ref) to help our future selves. +## Removing materials + +Use `delete!` with the stored name. Other entries and previously retrieved +material values remain available. +The following example adds and removes an extra EPR entry while retaining +`"epr"`. =# -epr = Material(1e15, 3.0, 1.0, 20.0, 0.005) # EPR (ethylene propylene rubber) -add!(materials, "epr", epr) -pvc = Material(1e15, 8.0, 1.0, 20.0, 0.1) # PVC (polyvinyl chloride) -add!(materials, "pvc", pvc) -laminated_paper = Material(1e15, 2.8, 1.0, 20.0, 0.0) # Laminated paper propylene -add!(materials, "laminated_paper", laminated_paper) -carbon_pe = Material(0.06, 1e3, 1.0, 20.0, 0.0) # Carbon-polyethylene compound (semicon) -add!(materials, "carbon_pe", carbon_pe) -conductive_paper = Material(18.5, 8.6, 1.0, 20.0, 0.0) # Conductive paper layer (semicon) -add!(materials, "conductive_paper", conductive_paper) - -# ## Removing materials +add!(materials, "epr_dupe", epr); +delete!(materials, "epr_dupe"); + +# Inspect the library after removing the duplicate: +materials + #= -!!! note "Note" - Materials can be removed from the library with the [`delete!`](@ref) function. +## Saving the materials library to JSON + +Save the current library to `materials_library.json` beside this tutorial. +The file includes both the built-in records and the additional entries. +Saving to an existing filename replaces that file's contents. =# -# Add a duplicate material by accident: -add!(materials, "epr_dupe", epr) +output_file = joinpath(@__DIR__, "materials_library.json") +save(materials; file_name = output_file); -# And now remove it using the [`delete!`](@ref) function: -delete!(materials, "epr_dupe") +#= +## Loading and retrieving materials -# Examine the updated library after removing the duplicate: -println("Material properties compiled from CIGRE TB-531 and IEC 60287:") -materials_df = DataFrame(materials) +Start with an empty library when the saved file supplies all its entries. +`load!` replaces the destination library's entire contents with the file's +records. A parsing or validation failure +leaves the destination unchanged. +=# -# ## Saving the materials library to JSON -output_file = fullfile("materials_library.json") -save(materials, file_name = output_file); +materials_from_json = MaterialsLibrary(add_defaults = false) +load!(materials_from_json; file_name = output_file); +materials_from_json +# Retrieve the corrected copper by its stored name. The material's native +# display shows its electromagnetic properties and reference temperature: +copper = materials_from_json["copper_corrected"] -# ## Retrieving materials for use #= -!!! note "Note" - To load from an existing JSON file, instantiate a new [`MaterialsLibrary`](@ref) followed by a call to the [`load!`](@ref) method. Materials can be retrieved from the library using the [`get`](@ref) function. +### Reading individual properties + +Named material properties are available directly. Their units are those listed +in the material-property table above. =# -# Initialize a new [`MaterialsLibrary`](@ref) and load from the JSON file: -materials_from_json = MaterialsLibrary() -load!(materials_from_json, file_name = output_file) +# Electrical resistivity at the reference temperature, in Ω·m: +copper.rho -# Retrieve a material and display the object: -copper = get(materials_from_json, "copper_corrected") +# Relative permittivity and permeability: +copper.eps_r +copper.mu_r -# Access the material properties: -println("Retrieved copper_corrected material properties:") -println("Resistivity: $(copper.rho) Ω·m") -println("Relative permittivity: $(copper.eps_r)") -println("Relative permeability: $(copper.mu_r)") -println("Reference temperature: $(copper.T0) °C") -println("Temperature coefficient: $(copper.alpha) 1/°C") +# Reference temperature in °C and resistivity coefficient in 1/°C: +copper.T0 +copper.alpha -# ## Conclusion #= -This tutorial has demonstrated how to: +### Optional lookup -1. Initialize a [`MaterialsLibrary`](@ref) with default [`Material`](@ref) objects. -2. Add new materials with specific properties. -3. Remove duplicate materials. -4. Save the library to a file for future use. -5. Retrieve materials for use in cable modeling. - -The [`MaterialsLibrary`](@ref) provides a flexible and traceable framework to manage material properties for accurate power cable modeling. Custom [`Material`](@ref) objects can be defined and used to match specific manufacturer data or standards requirements. +Indexed lookup raises `KeyError` when a required entry is absent. Use `get` +with an explicit fallback for an optional lookup. =# + +get(materials_from_json, "unlisted_material", nothing) diff --git a/examples/tutorial2.jl b/examples/tutorial2.jl index a0f317caa..797f57961 100644 --- a/examples/tutorial2.jl +++ b/examples/tutorial2.jl @@ -1,15 +1,17 @@ #= # Tutorial 2 - Building a cable design -This tutorial demonstrates how to model a typical medium-voltage single-core power cable -using the [`LineCableModels.jl`](@ref) package. The objective is to build a complete representation of a single-core 18/30 kV cable with a 1000 mm² aluminum conductor and 35 mm² copper screen. +Build an 18/30 kV single-core cable with a nominal 1000 mm² aluminum conductor +and a 35 mm² copper screen. Inspect its geometry, calculate and report its +cable constants, save the design, and arrange three cables in a buried trefoil +system for external-tool export. =# #= **Tutorial outline** ```@contents Pages = [ - "tutorial2.md", + "tutorial2.md", ] Depth = 2:3 ``` @@ -18,44 +20,57 @@ Depth = 2:3 #= ## Introduction -Single-core power cables have a complex structure consisting of multiple concentric layers, each with specific geometric and material properties -- for example, a cable of type NA2XS(FL)2Y 18/30 [is shown here](https://www.google.com/search?udm=2&q=%22NA2XS(FL)2Y%2018/30%20kV%20cable%22). Prior to building actual transmission line models that incorporate cables as part of the transmission system, e.g. for EMT simulations, power flow, harmonics, protection studies etc., it is necessary to determine the base (or DC) electrical parameters of the cable itself. - -This tutorial covers: - -1. Creating a detailed [`CableDesign`](@ref) with all its components. -2. Examining the main electrical parameters (R, L, C) of the cable core [`ConductorGroup`](@ref) and main [`InsulatorGroup`](@ref). -3. Examining the equivalent electromagnetic properties of every [`CableComponent`](@ref) (core, sheath, jacket). -4. Saving the cable design to a [`CablesLibrary`](@ref) for future use. -4. Assigning [`CableDesign`](@ref) objects to a [`LineCableSystem`](@ref) and exporting the model to PSCAD for EMT analysis. +The cable consists of concentric conductive, semiconducting, and insulating +regions. Their dimensions and material properties determine the cable +constants calculated below. + +[`CableConstants`](@ref) evaluates earth-free resistance, inductance, +conductance, and capacitance for each independent concentric assembly at a +specified frequency and temperature. These local quantities are distinct from +the frequency-dependent matrices of an installed cable system, which are the +subject of Tutorial 3. A resistance evaluated at 50 Hz is not +the same measurement as a datasheet DC resistance. + +The main operations in this tutorial are physical construction, preview, +calculation, reporting, library persistence, and system export. The completed +numerical result is passed directly to `report`. The reporting convenience +constructs its detached observation internally. =# #= ## Getting started + +Load the public modeling and reporting API, CairoMakie for the documentation +figures, and DataFrames for the construction-dimensions table. =# -# Load the package and set up the environment: using LineCableModels -using DataFrames -import LineCableModels.PlotBuilder.BackendHandler: renderfig #hide +import LineCableModels: homogenize +import CairoMakie +using DataFrames: DataFrame + fullfile(filename) = joinpath(@__DIR__, filename); #hide -set_verbosity!(0); #hide -set_backend!(:gl); #hide -# Initialize materials library with default values: +# Initialize and inspect the material library: materials = MaterialsLibrary(add_defaults = true) -materials_df = DataFrame(materials) +materials #= +The material library saved in Tutorial 1 can also be loaded: + ```julia -# Alternatively, it can be loaded from the example file built in the previous tutorial: -load!(materials, file_name = "materials_library.json") +load!(materials; file_name = "materials_library.json") ``` =# #= ## Cable dimensions -The cable under consideration is a medium-voltage, stranded aluminum conductor cable with XLPE insulation, copper wire concentric screens, water-blocking tape, and PE jacket that is rated for 18/30 kV systems. This information is typically found in the cable datasheet and is fully described in the code type under standards HD 620 10C [CENELEC_HD620_S3_2023](@cite) or DIN VDE 0276-620 [VDE_DIN_VDE_0276_620_2024](@cite): +The reference construction is an 18/30 kV cable with a stranded aluminum +conductor, XLPE insulation, a concentric copper wire screen, water-blocking +layers, an aluminum moisture barrier, and a PE jacket. Its designation is +associated with HD 620 10C [CENELEC_HD620_S3_2023](@cite) and DIN VDE 0276-620 +[VDE_DIN_VDE_0276_620_2024](@cite): ``` NA2XS(FL)2Y @@ -63,401 +78,390 @@ NA2XS(FL)2Y │ │ │ │ │ │ │ └── 2Y: Outer sheath of polyethylene (PE) │ │ └── (FL): Longitudinal watertight protection -│ │ +│ │ │ └── 2XS: XLPE insulation with screen of copper wires └── NA: Aluminum conductor ``` + +The numerical example uses the library's `:pe` material for the main +insulation as well as the outer PE layers. It also uses `:polyacrylate` for the +tapes identified below as semiconductive and water-blocking tapes. These are +the material assignments used by this example. The assigned material properties remain unchanged by construction labels. + +All dimensions in the declarations below are in metres. =# -# After some research, it is found that a typical cable of this type has the following configuration: -num_co_wires = 61 # number of core wires -num_sc_wires = 49 # number of screen wires -d_core = 38.1e-3 # nominal core overall diameter -d_w = 4.7e-3 # nominal strand diameter of the core -t_sc_in = 0.6e-3 # nominal internal semicon thickness -t_ins = 8e-3 # nominal main insulation thickness -t_sc_out = 0.3e-3 # nominal external semicon thickness -d_ws = .95e-3 # nominal wire screen diameter -t_cut = 0.1e-3 # nominal thickness of the copper tape (around wire screens) -w_cut = 10e-3 # nominal width of copper tape -t_wbt = .3e-3 # nominal thickness of the water blocking tape -t_sct = .3e-3 # nominal thickness of the semiconductive tape -t_alt = .15e-3 # nominal thickness of the aluminum tape -t_pet = .05e-3 # nominal thickness of the pe face in the aluminum tape -t_jac = 2.4e-3 # nominal PE jacket thickness - -d_overall = d_core #hide -layers = [] #hide -push!(layers, ("Conductor", missing, d_overall * 1000)) #hide -d_overall += 2 * t_sct #hide -push!(layers, ("Inner semiconductive tape", t_sct * 1000, d_overall * 1000)) #hide -d_overall += 2 * t_sc_in #hide -push!(layers, ("Inner semiconductor", t_sc_in * 1000, d_overall * 1000)) #hide -d_overall += 2 * t_ins #hide -push!(layers, ("Main insulation", t_ins * 1000, d_overall * 1000)) #hide -d_overall += 2 * t_sc_out #hide -push!(layers, ("Outer semiconductor", t_sc_out * 1000, d_overall * 1000)) #hide -d_overall += 2 * t_sct #hide -push!(layers, ("Outer semiconductive tape", t_sct * 1000, d_overall * 1000)) #hide -d_overall += 2 * d_ws #hide -push!(layers, ("Wire screen", d_ws * 1000, d_overall * 1000)) #hide -d_overall += 2 * t_cut #hide -push!(layers, ("Copper tape", t_cut * 1000, d_overall * 1000)) #hide -d_overall += 2 * t_wbt #hide -push!(layers, ("Water-blocking tape", t_wbt * 1000, d_overall * 1000)) #hide -d_overall += 2 * t_alt #hide -push!(layers, ("Aluminum tape", t_alt * 1000, d_overall * 1000)) #hide -d_overall += 2 * t_pet #hide -push!(layers, ("PE with aluminum face", t_pet * 1000, d_overall * 1000)) #hide -d_overall += 2 * t_jac #hide -push!(layers, ("PE jacket", t_jac * 1000, d_overall * 1000)); #hide - -# The cable structure is summarized in a table for better visualization, with dimensions in milimiters: -df = DataFrame( #hide - layer = first.(layers), #hide - thickness = [ #hide - ismissing(t) ? "-" : round(t, sigdigits = 2) for t in getindex.(layers, 2) #hide - ], #hide - diameter = [round(d, digits = 2) for d in getindex.(layers, 3)], #hide +num_sc_wires = 49 # Number of copper screen wires. +d_core = 38.1e-3 # Finished overall core diameter. +d_w = 4.7e-3 # Source strand diameter. +t_sc_in = 0.6e-3 # Inner semiconductor thickness. +t_ins = 8e-3 # Main insulation thickness. +t_sc_out = 0.3e-3 # Outer semiconductor thickness. +d_ws = 0.95e-3 # Screen-wire diameter. +t_cut = 0.1e-3 # Copper tape thickness. +w_cut = 10e-3 # Copper tape width. +t_wbt = 0.3e-3 # Water-blocking tape thickness. +t_sct = 0.3e-3 # Semiconductive tape thickness. +t_alt = 0.15e-3 # Aluminum tape thickness. +t_pet = 0.05e-3 # PE facing thickness over the aluminum tape. +t_jac = 2.4e-3; # Outer PE jacket thickness. + +layer_names = ( #hide + "Conductor", "Inner semiconductive tape", "Inner semiconductor", #hide + "Main insulation", "Outer semiconductor", "Outer semiconductive tape", #hide + "Wire screen", "Copper tape", "Water-blocking tape", "Aluminum tape", #hide + "PE facing", "PE jacket" #hide +) #hide +layer_thicknesses = ( #hide + missing, t_sct, t_sc_in, t_ins, t_sc_out, t_sct, d_ws, t_cut, t_wbt, #hide + t_alt, t_pet, t_jac #hide +) #hide +radial_increments = ( #hide + 0.0, t_sct, t_sc_in, t_ins, t_sc_out, t_sct, d_ws, t_cut, t_wbt, #hide + t_alt, t_pet, t_jac #hide ) #hide +layer_diameters = d_core .+ 2 .* cumsum(radial_increments) #hide + +# Summarize the nominal radial schedule in millimeters. The cable constructor +# resolves the detailed strand and tape geometry from the declarations below. +cable_dimensions = DataFrame( + "layer" => collect(layer_names), + "thickness [mm]" => [ismissing(t) ? missing : round(1000t, sigdigits = 2) + for t in layer_thicknesses], + "diameter [mm]" => collect(round.(1000 .* layer_diameters, digits = 2)) +) #= -## Using the cable constructors - -!!! note "Object hierarchy" - The [`LineCableModels.DataModel`](@ref) module implements a carefully designed component hierarchy that mirrors the physical construction of power cables while maintaining the mathematical relationships required for accurate electrical modeling. - -``` -CableDesign -├── CableComponent -│ ├── conductor_group::ConductorGroup <: AbstractConductorPart -│ │ ├── conductor_props::Material -│ │ └── layers::Vector{AbstractConductorPart} -│ │ ├── CircStrands -│ │ ├── Tubular -│ │ ├── Strip -│ │ └── … -│ └── insulator_group::InsulatorGroup <: AbstractInsulatorPart -│ ├── insulator_props::Material -│ └── layers::Vector{AbstractInsulatorPart} -│ ├── Insulator -│ ├── Semicon -│ └── … -⋮ -├── CableComponent -│ ├── … -⋮ ⋮ -``` - -### Cable designs - -The [`CableDesign`](@ref) object is the main container for all cable components. It encapsulates the entire cable structure and provides methods for calculating global cable properties. - -### Cable components +## Cable construction -Each [`CableComponent`](@ref) represents a functional group of the cable (core, sheath, armor, outer), organized into a conductor group and an insulator group with their respective effective material properties. This structure is designed to provide precise calculation of electromagnetic parameters. +Declare regions from the center outward. [`@cable`](@ref) completes the physical +design, while [`@terminal`](@ref) marks its electrical terminal groups. -### Conductor groups +### Materials and stranded core -The [`ConductorGroup`](@ref) object serves as a specialized container for organizing [`AbstractConductorPart`](@ref) elements in layers. It calculates equivalent resistance (R) and inductance (L) values for all contained conductive elements, handling the complexity of different geometrical arrangements. - -#### AbstractConductorPart implementations - -- The [`CircStrands`](@ref) object models stranded cores and screens with helical patterns and circular cross-sections. -- The [`Tubular`](@ref) object represents simple tubular conductors with straightforward parameter calculations. -- The [`Strip`](@ref) object models conductor tapes following helical patterns with rectangular cross-sections. - -### Insulator groups - -The [`InsulatorGroup`](@ref) object organizes [`AbstractInsulatorPart`](@ref) elements in concentric layers, calculating the equivalent capacitance (C) and conductance (G) parameters. - -#### AbstractInsulatorPart implementations - -- The [`Insulator`](@ref) object represents dielectric layers with very high resistivity. -- The [`Semicon`](@ref) object models semiconducting layers with intermediate resistivity and high permittivity. - -!!! note "Equivalent circuit parameters" - The hierarchical structure enables accurate calculation of equivalent circuit parameters by: - - 1. Computing geometry-specific parameters at the [`AbstractConductorPart`](@ref) and [`AbstractInsulatorPart`](@ref) levels. - 2. Aggregating these into equivalent parameters within [`ConductorGroup`](@ref) and [`InsulatorGroup`](@ref). - 3. Converting the composite structure into an equivalent coaxial model by matching lumped circuit quantities (R, L, C, G) to effective electromagnetic properties (ρ, ε, µ) at the [`CableComponent`](@ref) level. The effective properties are stored in dedicated [`Material`](@ref) objects. +The wire diameter and the 38.1 mm finished geometric boundary define the stranded core. +The 4 successive outer strand layers use the lay ratios specified below. +Compaction retains the area of each source wire rather than changing its +conductive area to fill the geometric boundary. =# +aluminum = Material(materials, :aluminum) +copper = Material(materials, :copper) +polyacrylate = Material(materials, :polyacrylate) +semicon1 = Material(materials, :semicon1) +semicon2 = Material(materials, :semicon2) +pe = Material(materials, :pe); + +stranded_core = stranded( + aluminum; + shape = Disk(d_w / 2), + lay = LayRatio(15.0, 13.5, 12.5, 11.0), + compact = true, + boundary = Disk(d_core / 2) +); #= -## Core and main insulation - -The core consists of a 4-layer AAAC stranded conductor with 61 wires arranged in (1/6/12/18/24) pattern, with respective lay ratios of (15/13.5/12.5/11) [CENELEC50182](@cite). Stranded conductors are modeled using the [`CircStrands`](@ref) object, which handles the helical pattern and twisting effects via the [`calc_helical_params`](@ref) method. -=# - -# Initialize the conductor object and assign the central wire: -material = get(materials, "aluminum") -core = ConductorGroup(CircStrands(0.0, Diameter(d_w), 1, 0.0, material)) - -#= -!!! tip "Convenience methods" - The [`add!`](@ref) method internally passes the `r_ex` of the existing object to the `r_in` argument of the new conductor. This enables easy stacking of multiple layers without redundancy. Moreover, the [`Diameter`](@ref) method is a convenience function that converts the diameter to radius at the constructor level. This maintains alignment with manufacturer specifications while enabling internal calculations to use radius values directly. This approach eliminates repetitive unit conversions and potential sources of implementation error. +### Insulation, screen, and jacket + +The inner semiconducting layer provides the conductor-to-insulation interface. +A tape lies between it and the stranded conductor. The main insulation is +followed by the outer semiconductor and a second tape. The metallic screen +provides shielding and a fault-current path. Outside the screen, the +water-blocking layer, aluminum barrier, PE facing, and outer jacket complete +the construction. + +[`screen`](@ref), [`insulation`](@ref), [`sheath`](@ref), and [`jacket`](@ref) +can state their thickness relative to the preceding outward boundary. The +wire screen instead needs the physical radius of its wire-center locus. The +copper tape retains its rectangular width and thickness when placed around +the preceding geometric boundary. =# -# Add the subsequent layers of wires and inspect the object: -add!(core, CircStrands, Diameter(d_w), 6, 15.0, material) -add!(core, CircStrands, Diameter(d_w), 12, 13.5, material) -add!(core, CircStrands, Diameter(d_w), 18, 12.5, material) -add!(core, CircStrands, Diameter(d_w), 24, 11.0, material) +conductor_outer = d_core / 2 +screen_wire_locus = conductor_outer + t_sct + t_sc_in + t_ins + + t_sc_out + t_sct + d_ws / 2 -#= -### Inner semiconductor +# Store catalogue information with the library entry. +cable_id = "18kV_1000mm2" +datasheet_info = DatasheetInfo( + designation_code = "NA2XS(FL)2Y", + U0 = 18.0, # Phase-to-ground voltage [kV]. + U = 30.0, # Phase-to-phase voltage [kV]. + conductor_cross_section = 1000.0, # [mm²]. + screen_cross_section = 35.0, # [mm²]. + resistance = 0.0291, # DC resistance [Ω/km]. + capacitance = 0.39, # Capacitance [μF/km]. + inductance = 0.3 # Inductance in trefoil [mH/km]. +) -The inner semiconductor layer ensures uniform electric field distribution between -the conductor and insulation, eliminating air gaps and reducing field concentrations. An optional semiconductive tape is often used to ensure core uniformity and enhanced adherence. -=# +cable_design = @cable cable_id begin + @terminal :core begin + stranded_core + screen(polyacrylate; t = t_sct) + screen(semicon1; t = t_sc_in) + insulation(pe; t = t_ins) + screen(semicon2; t = t_sc_out) + screen(polyacrylate; t = t_sct) + end + @terminal :sheath begin + wires( + copper; + shape = Disk(d_ws / 2), + n = num_sc_wires, + r = screen_wire_locus, + lay = LayRatio(10), + tag = :screen_wire + ) + tape( + copper; + section = Rectangle(w_cut, t_cut), + n = 1, + lay = LayRatio(10), + tag = :copper_tape + ) + end + screen(polyacrylate; t = t_wbt) + @terminal :jacket begin + sheath(aluminum; t = t_alt) + end + jacket(pe; t = t_pet) + jacket(pe; t = t_jac) +end; #= -!!! tip "Convenience methods" - The [`Thickness`](@ref) type is a convenience wrapper that simplifies layer construction. When used in a constructor, it automatically calculates the outer radius by adding the thickness to the inner radius (which is inherited from the previous layer's outer radius). +The retained terminal names are `:core`, `:sheath`, and `:jacket`. In this +example, `:sheath` groups the copper screen wires and copper tape, while +`:jacket` identifies the conductive aluminum barrier. The outer PE jacket is +an insulating region without an additional electrical terminal. =# -# Inner semiconductive tape: -material = get(materials, "polyacrylate") -main_insu = InsulatorGroup(Semicon(core, Thickness(t_sct), material)) +# Inspect the completed design and its cross-section: +cable_design -# Inner semiconductor (1000 Ω.m as per IEC 840): -material = get(materials, "semicon1") -add!(main_insu, Semicon, Thickness(t_sc_in), material) +cable_preview = preview( + cable_design; + backend = :cairo, + legend_cap = 0.5, + display_plot = false, #hide + controls = false #hide +) +cable_preview.figure #hide #= -### Main insulation +## Cable constants -XLPE (cross-linked polyethylene) is the standard insulation material for modern -medium and high voltage cables due to its excellent dielectric properties. +Calculate the local cable constants at the defaults of 50 Hz and 20 °C. +The cable-constant calculation treats the innermost terminal of each concentric +assembly as active and the outward terminals as grounded. =# -# Add the insulation layer: -material = get(materials, "pe") -add!(main_insu, Insulator, Thickness(t_ins), material) +constants = CableConstants(cable_design); +constants #= -### Outer semiconductor - -Similar to the inner semiconductor, the outer semiconductor provides a uniform -transition from insulation to the metallic screen. -=# - -# Outer semiconductor (500 Ω.m as per IEC 840): -material = get(materials, "semicon2") -add!(main_insu, Semicon, Thickness(t_sc_out), material) +### Quantity tables -# Outer semiconductive tape: -material = get(materials, "polyacrylate") -add!(main_insu, Semicon, Thickness(t_sct), material) +Pass the numerical result directly to `report`. The R/L and G/C selection +produces four separate quantity tables. Each table contains the operating +frequency and a named column for each assembly. These are not mutual-coupling +matrices between different cables. -# Group core-related components: -core_cc = CableComponent("core", core, main_insu) - -#= -With the core parts properly defined, the [`CableDesign`](@ref) object is initialized with nominal data from the datasheet. This includes voltage ratings and reference electrical parameters that will be used to benchmark the design. +`report(constants)` uses the available quantities and default display units. +Choose Ω/km, mH/km, μS/km, and μF/km explicitly so the R/L/C units match the +datasheet entries used below. =# -# Define the nominal values and instantiate the `CableDesign` with the `core_cc` component: -cable_id = "18kV_1000mm2" -datasheet_info = NominalData( - designation_code = "NA2XS(FL)2Y", - U0 = 18.0, # Phase-to-ground voltage [kV] - U = 30.0, # Phase-to-phase voltage [kV] - conductor_cross_section = 1000.0, # [mm²] - screen_cross_section = 35.0, # [mm²] - resistance = 0.0291, # DC resistance [Ω/km] - capacitance = 0.39, # Capacitance [μF/km] - inductance = 0.3, # Inductance in trifoil [mH/km] +constants_report = report( + constants; + values = (R, L, G, C), + length_unit = :kilo, + quantity_units = (R = :base, L = :milli, G = :micro, C = :micro) ) -cable_design = CableDesign(cable_id, core_cc, nominal_data = datasheet_info) - -# At this point, it becomes possible to preview the cable design: -plt1, _ = preview(cable_design) -plt1 #hide #= -### Wire screens +### Assembly selection -The metallic screen (typically copper) serves multiple purposes: -- Provides a return path for fault currents. -- Ensures radial symmetry of the electric field. -- Acts as electrical shielding. -- Provides mechanical protection. +For cable constants, an observation index selects an assembly. Matrix +coefficients and frequency samples are not selected by this index. To report only the resistance of the first +assembly, state that intent with `@observe` in the report request. The example +has one concentric assembly, named `:core`. =# -# Build the wire screens on top of the previous layer: -lay_ratio = 10.0 # typical value for wire screens -material = get(materials, "copper") -screen_con = - ConductorGroup( - CircStrands(main_insu, Diameter(d_ws), num_sc_wires, lay_ratio, material), - ) +core_resistance_report = report( + constants; + values = @observe(R[1]), + length_unit = :kilo, + quantity_units = (R = :base,) +) -# Add the equalizing copper tape wrapping the wire screen: -add!(screen_con, Strip, Thickness(t_cut), w_cut, lay_ratio, material) +#= +### Catalogue comparison -# Water blocking tape over screen: -material = get(materials, "polyacrylate") -screen_insu = InsulatorGroup(Semicon(screen_con, Thickness(t_wbt), material)) +Display the three quantity DataFrames produced by the report in their recorded +units. -# Group sheath components and assign to design: -sheath_cc = CableComponent("sheath", screen_con, screen_insu) -add!(cable_design, sheath_cc) +The datasheet resistance is a DC value, whereas the calculated resistance is +at 50 Hz. Compare their magnitudes with those different conditions in mind: +this is not an equal-condition error calculation. +=# -# Examine the newly added components: -plt2, _ = preview(cable_design) -plt2 #hide +# Catalogue DC resistance [Ω/km]: +datasheet_info.resistance -#= -### Outer jacket components +# Calculated resistance [Ω/km]. The frequency column records 50 Hz: +constants_report[R] -Modern cables often include an aluminum tape as moisture barrier -and PE (polyethylene) outer jacket for mechanical protection. +#= +The datasheet inductance is specified for trefoil. `CableConstants` supplies +the earth-free concentric-assembly value, not a calculation of that trefoil +installation. These reference values have different stated scopes and cannot form a benchmark pair. =# -# Add the aluminum foil (moisture barrier): -material = get(materials, "aluminum") -jacket_con = ConductorGroup(Tubular(screen_insu, Thickness(t_alt), material)) - -# PE layer after aluminum foil: -material = get(materials, "pe") -jacket_insu = InsulatorGroup(Insulator(jacket_con, Thickness(t_pet), material)) +# Catalogue trefoil inductance [mH/km]: +datasheet_info.inductance -# PE jacket (outer mechanical protection): -material = get(materials, "pe") -add!(jacket_insu, Insulator, Thickness(t_jac), material) +# Calculated local inductance [mH/km]: +constants_report[L] #= -!!! tip "Convenience methods" - To facilitate data entry, it is possible to call the [`add!`](@ref) method directly on the [`ConductorGroup`](@ref) and [`InsulatorGroup`](@ref) constituents of the component to include, without instantiating the [`CableComponent`](@ref) first. +Inspect the datasheet capacitance alongside the calculated capacitance in the +same displayed units. The capacitance test frequency and temperature are absent from the supplied datasheet record. No such conditions are inferred. +The main insulation in this numerical model uses `:pe`. =# -# Assign the jacket parts directly to the design: -add!(cable_design, "jacket", jacket_con, jacket_insu) +# Catalogue capacitance [μF/km]: +datasheet_info.capacitance -# Inspect the finished cable design: -plt3, _ = preview(cable_design) -plt3 #hide +# Calculated local capacitance [μF/km]: +constants_report[C] + +# The conductance table remains a separate result. No datasheet G is supplied: +constants_report[G] #= -## Examining the cable parameters (RLC) +## Homogeneous equivalent design -In this section, the cable design is examined and the calculated parameters are compared with datasheet values. [`LineCableModels.jl`](@ref) provides methods to analyze the design in different levels of detail. +`homogenize` constructs a homogeneous equivalent as a separate physical design. +The detailed design remains available for inspection and reuse. =# -# Compare with datasheet information (R, L, C values): -core_df = DataFrame(cable_design, :baseparams) - -# Obtain the equivalent electromagnetic properties of the cable: -components_df = DataFrame(cable_design, :components) - -# Get detailed description of all cable parts: -detailed_df = DataFrame(cable_design, :detailed) +equivalent_design = homogenize(cable_design; new_id = cable_id * "_equivalent") #= ## Saving the cable design -!!! note "Cables library" - Designs can be saved to a library for future use. The [`CablesLibrary`](@ref) is a container for storing multiple cable designs, allowing for easy access and reuse in different projects. Library management is performed using the [`DataFrame`](@ref), [`add!`](@ref), and [`save`](@ref) functions. +A [`CablesLibrary`](@ref) stores designs for reuse. Add the physical design and +its associated datasheet information, then save the library. Load it into a +fresh library object to retrieve the design by its identifier. =# -# Store the cable design and inspect the library contents: library = CablesLibrary() -add!(library, cable_design) -library_df = DataFrame(library) +add!(library, cable_design; datasheet = datasheet_info); +library -# Save to file for later use: -output_file = fullfile("cables_library.json") -save(library, file_name = output_file); +library_file = fullfile("cables_library.json") +save(library; file_name = library_file); +loaded_library = CablesLibrary() +load!(loaded_library; file_name = library_file) +loaded_design = loaded_library[cable_id]; +loaded_library #= -### Defining a cable system +## Defining a cable system -!!! note "Cable systems" - A cable system is a collection of cables with defined positions, length and environmental characteristics. The [`LineCableSystem`](@ref) object is the main container for all cable systems, and it allows the definition of multiple cables in different configurations (e.g., trifoil, flat etc.). This object is the entry point for all system-related calculations and analyses. -=# +The installed arrangement is represented by [`LineCableSystem`](@ref). A +[`LineParametersProblem`](@ref LineCableModels.Engine.LineParametersProblem) +adds the operating temperature, earth properties, and analysis frequencies to +that completed physical system. This tutorial constructs the problem and exports +the system. Tutorial 3 computes its frequency-dependent line parameters. -#= -### Earth model +### Earth model -The earth return path significantly affects cable impedance calculations and needs to be properly modeled. In this tutorial, only a basic model with typical soil properties is defined. This will be further elaborated in the subsequent tutorials. +Use homogeneous earth with resistivity 100 Ω·m, relative permittivity 10, and +relative permeability 1. The declared frequency scan contains ten +logarithmically spaced samples from 1 Hz to 1 MHz. The soil declaration is +independent of the frequency grid. =# -# Define a frequency-dependent earth model (1 Hz to 1 MHz): -f = 10.0 .^ range(0, stop = 6, length = 10) # Frequency range -earth_params = EarthModel(f, 100.0, 10.0, 1.0) # 100 Ω·m resistivity, εr=10, μr=1 - -# Earth model base (DC) properties: -earthmodel_df = DataFrame(earth_params) +f = collect(10.0 .^ range(0, stop = 6, length = 10)) +earth = homogeneous(rho = 100.0, eps_r = 10.0, mu_r = 1.0); #= -### Three-phase system in trifoil configuration +### Three-phase trefoil -This section ilustrates the construction of a cable system with three identical cables arranged in a trifoil formation. +Place three copies of the saved design with 70 mm center-to-center spacing +and the trefoil center at `(0, -1)` m. The declared spacing is retained +explicitly. It is not inferred from the nominal cable diameter. + +Core assignments `1`, `2`, and `3` identify the three phases. The copper-screen +and aluminum-barrier terminals are assigned `0`, marking them for grounded +terminal reduction when requested by the calculation. The system length is +1000 m. =# +placements = @trefoil loaded_design spacing=70e-3 center=(0.0, -1.0) core=(1, 2, 3) sheath=0 jacket=0 -# Define system center point (underground at 1 m depth) and the trifoil positions -x0, y0 = 0.0, -1.0 -xa, ya, xb, yb, xc, yc = trifoil_formation(x0, y0, 0.035); +cable_system = build( + LineCableSystem, + placements; + environment = earth, + system_id = "18kV_1000mm2_trefoil", + line_length = 1000.0 +) -# Initialize the `LineCableSystem` with the first cable (phase A): -cablepos = CablePosition(cable_design, xa, ya, - Dict("core" => 1, "sheath" => 0, "jacket" => 0)) -cable_system = LineCableSystem("18kV_1000mm2_trifoil", 1000.0, cablepos) +problem = LineParametersProblem( + cable_system; + temperature = 20.0, + earth_props = earth, + frequencies = f +) +earth_params = problem.earth_props; -# Add remaining cables (phases B and C): -add!(cable_system, cable_design, xb, yb, - Dict("core" => 2, "sheath" => 0, "jacket" => 0)) -add!(cable_system, cable_design, xc, yc, - Dict("core" => 3, "sheath" => 0, "jacket" => 0)) +# Inspect the earth parameters retained by the problem: +earth_params #= -!!! note "Phase mapping" - The [`add!`](@ref) function allows the specification of phase mapping for each cable. The `Dict` argument maps the cable components to their respective phases, where `core` is the conductor, `sheath` is the screen, and `jacket` is the outer jacket. The values (1, 2, 3) represent the phase numbers (A, B, C) in this case. Components mapped to phase 0 will be Kron-eliminated (grounded). Components set to the same phase will be bundled into an equivalent phase. -=# +!!! note "Terminal mapping" + Connection schedules use the terminal names retained by the design. + Reusing a nonzero assignment groups terminals for bundle reduction. + Zero assignments identify terminals for grounded-conductor reduction. + The mapping declares those relationships rather than replacing the + physical cable regions. -#= ### Cable system preview -In this section the complete three-phase cable system is examined. +Inspect the completed three-phase system and its cross-section. The earth +model supplies the background, and `zoom_factor` controls the initial view. =# -# Display system details: -system_df = DataFrame(cable_system) +cable_system -# Visualize the cross-section of the three-phase system: -plt4, _ = preview(cable_system, earth_model = earth_params, zoom_factor = 2.0) -plt4 #hide +system_preview = preview( + cable_system; + earth_model = earth_params, + zoom_factor = 2.0, + backend = :cairo, + legend_cap = 0.5, + display_plot = false, #hide + controls = false #hide +) +system_preview.figure #hide #= -## PSCAD & ATPDraw export +## PSCAD and ATPDraw export -The final step showcases how to export the model for electromagnetic transient simulations in EMT-type software. +Export the physical system and earth parameters through the public exporters. +These calls write input files for use in PSCAD and ATPDraw. Tutorial 3 covers +the frequency-dependent calculation in LineCableModels and its reports and plots. =# -# Export to PSCAD input file: -output_file = fullfile("pscad_export.pscx") -export_file = export_data(:pscad, cable_system, earth_params, file_name = output_file); - -# Export to ATPDraw project file (XML): -output_file = fullfile("atp_export.xml") -export_file = export_data(:atp, cable_system, earth_params, file_name = output_file); - -#= -## Conclusion - -This tutorial has demonstrated how to: +pscad_file = export_data( + :pscad, cable_system, earth_params; + file_name = fullfile("pscad_export.pscx") +); -1. Create a detailed model of a complex power cable with multiple concentric layers. -2. Calculate and analyze the cable base parameters (R, L, C). -3. Design a three-phase cable system in trifoil arrangement. -4. Export the model for further analysis in specialized software. - -[`LineCableModels.jl`](@ref) provides a powerful framework for accurate power cable modeling -with a physically meaningful representation of all cable components. This approach -ensures that electromagnetic parameters are calculated with high precision. Now you can go ahead and run these cable simulations like a boss! -=# +atp_file = export_data( + :atp, cable_system, earth_params; + file_name = fullfile("atp_export.xml") +); diff --git a/examples/tutorial2_sector.jl b/examples/tutorial2_sector.jl deleted file mode 100644 index 848b5ac86..000000000 --- a/examples/tutorial2_sector.jl +++ /dev/null @@ -1,172 +0,0 @@ -#= -# Tutorial 2a - Building a sector-shaped cable design - -This tutorial demonstrates how to model a typical low-voltage three-core power cable with sector-shaped conductors -using the [`LineCableModels.jl`](@ref) package. The objective is to build a complete representation of a three-core 1 kV cable with 95 mm² aluminum sector-shaped conductors and a concentric copper neutral. -=# - -#= -**Tutorial outline** -```@contents -Pages = [ - "tutorial2_sector.md", -] -Depth = 2:3 -``` -=# - -#= -## Introduction - -Three-core power cables with sector-shaped conductors are common in low-voltage distribution networks. Their compact design allows for efficient use of space. This tutorial will guide you through creating a detailed [`CableDesign`](@ref) for such a cable. - -This tutorial covers: - -1. Defining materials with corrected resistivity. -2. Creating sector-shaped conductors using [`SectorParams`](@ref) and [`Sector`](@ref). -3. Assembling a multi-core [`CableDesign`](@ref). -4. Modeling a concentric neutral wire array. -5. Previewing the final cable design. -=# - -#= -## Getting started -=# - -# Load the package and set up the environment: -using LineCableModels -using DataFrames -import LineCableModels.BackendHandler: renderfig #hide -fullfile(filename) = joinpath(@__DIR__, filename); #hide -set_verbosity!(0); #hide - -#= -## Cable and Material Data - -We start by defining the materials. We will create a custom aluminum material with a resistivity corrected based on its nominal DC resistance, and a PVC material for insulation. -=# - -# Initialize materials library and add a PVC material -materials = MaterialsLibrary(add_defaults=true) -pvc = Material(Inf, 8.0, 1.0, 20.0, 0.1) # simple PVC -add!(materials, "pvc", pvc) -copper = get(materials, "copper") -aluminum = get(materials, "aluminum") - - -#= -## Sector-Shaped Core Conductors - -The core of the cable consists of three sector-shaped aluminum conductors. We define the geometry using `SectorParams` based on datasheet or standard values. -=# - -# === Sector (core) geometry (table data) === -# Based on Urquhart's paper for a 3-core 95mm^2 cable -n_sectors = 3 -r_back_mm = 10.24 # sector radius b -d_sector_mm = 9.14 # sector depth s -r_corner_mm = 1.02 # corner radius c -theta_cond_deg = 119.0 # sector angle φ -ins_thick = 1.1e-3 # core insulation thickness - -sector_params = SectorParams( - n_sectors, - r_back_mm / 1000, - d_sector_mm / 1000, - r_corner_mm / 1000, - theta_cond_deg, - ins_thick -) - -#= -With the sector parameters defined, we can create the individual `Sector` conductors and their insulation. Each sector is rotated to form the 3-core bundle. -=# - -rot_angles = (0.0, 120.0, 240.0) -sectors = [Sector(sector_params, ang, aluminum) for ang in rot_angles] -insulators = [SectorInsulator(sectors[i], ins_thick, pvc) for i in 1:3] - -components = [ - CableComponent("core1", ConductorGroup(sectors[1]), InsulatorGroup(insulators[1])), - CableComponent("core2", ConductorGroup(sectors[2]), InsulatorGroup(insulators[2])), - CableComponent("core3", ConductorGroup(sectors[3]), InsulatorGroup(insulators[3])) -] - -#= -## Concentric Neutral and Outer Jacket - -The cable includes a concentric neutral conductor made of copper wires and an outer PVC jacket. -=# - -# === Concentric neutral (30 wires) === -n_neutral = 30 -r_strand = 0.79e-3 -R_N = 14.36e-3 # radius to center of neutral wires -R_O = 17.25e-3 # outer radius of the cable - -inner_radius_neutral = R_N - r_strand -outer_jacket_thickness = R_O - (R_N + r_strand) - -neutral_wires = WireArray( - inner_radius_neutral, - Diameter(2*r_strand), - n_neutral, - 0.0, # lay ratio - copper -) - -neutral_jacket = Insulator(neutral_wires, Thickness(outer_jacket_thickness), pvc) -neutral_component = CableComponent("neutral", ConductorGroup(neutral_wires), InsulatorGroup(neutral_jacket)) - -#= -## Assembling the Cable Design - -Now we assemble the complete `CableDesign` by adding all the components. -=# - -design = CableDesign("NAYCWY_O_3x95_30x2_5", components[1]) -add!(design, components[2]) -add!(design, components[3]) -add!(design, neutral_component) - -#= -## Examining the Cable Design - -We can now display a summary of the cable design and preview it graphically. -=# - -println("Cable design summary:") -detailed_df = DataFrame(design, :detailed) -display(detailed_df) - -println("Previewing cable design...") -plt, _ = preview(design) -plt #hide - -#= -## Storing in a Library - -Finally, we can store the cable design in a `CablesLibrary` for future reference. -=# - -library = CablesLibrary() -add!(library, design) -library_df = DataFrame(library) - -# Save to file for later use: -output_file = fullfile("cables_library.json") -save(library, file_name = output_file); - -#= -## Conclusion - -This tutorial has demonstrated how to model a three-core cable with sector-shaped conductors. Key takeaways include: - -1. Creating custom materials with corrected properties. -2. Defining complex conductor shapes like sectors. -3. Assembling a multi-core cable design component by component. -4. Visualizing the final design for verification. - -This detailed modeling capability allows for accurate analysis of various cable configurations. -=# - diff --git a/examples/tutorial3.jl b/examples/tutorial3.jl index 2bbfb3450..4527cbc60 100644 --- a/examples/tutorial3.jl +++ b/examples/tutorial3.jl @@ -1,15 +1,21 @@ #= # Tutorial 3 - Computing line parameters -This case file demonstrates how to model an armored high-voltage single-core power cable -using the [`LineCableModels.jl`](@ref) package. The objective is to build a complete representation of a single-core 525 kV cable with a 1600 mm² copper conductor, 1.2 mm tubular lead sheath and 68 x 6 mm galvanized steel armor, based on the design described in [Karmokar2025](@cite). +Compute frequency-dependent line parameters for a 525 kV cable with a +1600 mm² copper conductor, a 3.3 mm lead sheath, and 68 galvanized steel armor +wires of 5.827 mm diameter. The construction is based on [Karmokar2025](@cite). +The dimensions used in this calculation are listed below. + +The example covers cable construction, cable constants, an underground bipole, +external-tool export, frequency-dependent parameters, and modal transformation. +Tables and plots are requested directly from the computed results. =# #= **Tutorial outline** ```@contents Pages = [ - "tutorial3.md", + "tutorial3.md", ] Depth = 2:3 ``` @@ -17,358 +23,499 @@ Depth = 2:3 #= ## Introduction -HVDC cables are constructed around a central conductor enclosed by a triple-extruded insulation system (inner/outer semi-conductive layers and main insulation). A metallic screen and protective outer sheath are then applied for land cables. Subsea designs add galvanized steel wire armor over this structure to provide mechanical strength against water pressure. A reference design for a 525 kV HVDC cable [is shown here](https://nkt.widen.net/content/pnwgwjfudf/pdf/Extruded_DC_525kV_DS_EN_DEHV_HV_DS_DE-EN.pdf). + +HVDC cables have a central conductor enclosed by an insulation system comprising +inner and outer semiconductive layers and the main insulation. Metallic screens +and protective sheaths surround the insulation. Subsea designs also include +steel wire armor for mechanical protection and tensile strength. A reference +525 kV design is available in the +[manufacturer's datasheet](https://nkt.widen.net/content/pnwgwjfudf/pdf/Extruded_DC_525kV_DS_EN_DEHV_HV_DS_DE-EN.pdf). + +The reference construction is described with XLPE main insulation. This calculation uses +the library material `:pe` for the main insulation and the PE inner sheath. +The reference's XLPE designation describes a different material choice. =# #= ## Getting started + +Load the modeling and reporting API, CairoMakie for figures in the documentation, +and DataFrames for the construction-dimensions table. =# -# Load the package and set up the environment: using LineCableModels -using LineCableModels.Engine.FEM -using LineCableModels.Engine.Transforms: Fortescue -using DataFrames -using Printf +import LineCableModels: homogenize +import CairoMakie +using DataFrames: DataFrame + fullfile(filename) = joinpath(@__DIR__, filename); #hide -set_verbosity!(0); #hide -set_backend!(:gl); #hide -# Initialize library and the required materials for this design: +# Initialize and inspect the material library: materials = MaterialsLibrary(add_defaults = true) -# Inspect the contents of the materials library: -materials_df = DataFrame(materials) - #= ## Cable dimensions -The cable under consideration is a high-voltage, stranded copper conductor cable with XLPE insulation, water-blocking tape, lead tubular screens, PE inner sheath, PP bedding, steel armor and PP jacket, rated for 525 kV HVDC systems. This information is typically found in the cable datasheet and is based on the design studied in [Karmokar2025](@cite). - -The cable is found to have the following configuration: +The cable consists of a stranded copper conductor, semiconductive layers and +main insulation, water-blocking tape, a lead sheath, a PE inner sheath, PP +bedding, steel armor, and a PP jacket. All dimensions in the declarations below +are in metres. =# -num_co_wires = 127 # number of core wires -num_ar_wires = 68 # number of armor wires -d_core = 0.0463 # nominal core overall diameter -d_w = 3.6649e-3 # nominal strand diameter of the core (minimum value to match datasheet) -t_sc_in = 2e-3 # nominal internal semicon thickness -t_ins = 26e-3 # nominal main insulation thickness -t_sc_out = 1.8e-3 # nominal external semicon thickness -t_wbt = .3e-3 # nominal thickness of the water blocking tape -t_sc = 3.3e-3 # nominal lead screen thickness -t_pe = 3e-3 # nominal PE inner sheath thickness -t_bed = 3e-3 # nominal thickness of the PP bedding -d_wa = 5.827e-3 # nominal armor wire diameter -t_jac = 10e-3 # nominal PP jacket thickness - -d_overall = d_core #hide -layers = [] #hide -push!(layers, ("Conductor", missing, d_overall * 1000)) #hide -d_overall += 2 * t_sc_in #hide -push!(layers, ("Inner semiconductor", t_sc_in * 1000, d_overall * 1000)) #hide -d_overall += 2 * t_ins #hide -push!(layers, ("Main insulation", t_ins * 1000, d_overall * 1000)) #hide -d_overall += 2 * t_sc_out #hide -push!(layers, ("Outer semiconductor", t_sc_out * 1000, d_overall * 1000)) #hide -d_overall += 2 * t_wbt #hide -push!(layers, ("Swellable tape", t_wbt * 1000, d_overall * 1000)) #hide -d_overall += 2 * t_sc #hide -push!(layers, ("Lead screen", t_sc * 1000, d_overall * 1000)) #hide -d_overall += 2 * t_pe #hide -push!(layers, ("PE inner sheath", t_pe * 1000, d_overall * 1000)) #hide -d_overall += 2 * t_bed #hide -push!(layers, ("PP bedding", t_bed * 1000, d_overall * 1000)) #hide -d_overall += 2 * d_wa #hide -push!(layers, ("Stranded wire armor", d_wa * 1000, d_overall * 1000)) #hide -d_overall += 2 * t_jac #hide -push!(layers, ("PP jacket", t_jac * 1000, d_overall * 1000)); #hide - - -# The cable structure is summarized in a table for better visualization, with dimensions in milimiters: -df = DataFrame( #hide - layer = first.(layers), #hide - thickness = [ #hide - ismissing(t) ? "-" : round(t, sigdigits = 2) for t in getindex.(layers, 2) #hide - ], #hide - diameter = [round(d, digits = 2) for d in getindex.(layers, 3)], #hide +num_ar_wires = 68 # Number of armor wires. +d_core = 0.0463 # Finished overall core diameter. +d_w = 3.6649e-3 # Source strand diameter used to match the specified core. +t_sc_in = 2e-3 # Inner semiconductor thickness. +t_ins = 26e-3 # Main insulation thickness. +t_sc_out = 1.8e-3 # Outer semiconductor thickness. +t_wbt = 0.3e-3 # Water-blocking tape thickness. +t_sc = 3.3e-3 # Lead sheath thickness. +t_pe = 3e-3 # PE inner sheath thickness. +t_bed = 3e-3 # PP bedding thickness. +d_wa = 5.827e-3 # Armor wire diameter. +t_jac = 10e-3; # PP outer jacket thickness. + +layer_names = ( #hide + "Conductor", "Inner semiconductor", "Main insulation", #hide + "Outer semiconductor", "Swellable tape", "Lead sheath", #hide + "PE inner sheath", "PP bedding", "Stranded wire armor", "PP jacket" #hide +) #hide +layer_thicknesses = ( #hide + missing, t_sc_in, t_ins, t_sc_out, t_wbt, t_sc, t_pe, t_bed, d_wa, t_jac #hide +) #hide +radial_increments = ( #hide + 0.0, t_sc_in, t_ins, t_sc_out, t_wbt, t_sc, t_pe, t_bed, d_wa, t_jac #hide ) #hide +layer_diameters = d_core .+ 2 .* cumsum(radial_increments) #hide + +# Summarize the nominal radial dimensions in millimeters: +cable_dimensions = DataFrame( + "layer" => collect(layer_names), + "thickness [mm]" => [ismissing(t) ? missing : round(1000t, sigdigits = 2) + for t in layer_thicknesses], + "diameter [mm]" => collect(round.(1000 .* layer_diameters, digits = 2)) +) #= ## Core and main insulation -Initialize the conductor object and assign the central wire: +The source wire diameter and finished core boundary define the stranded core. +[`stranded`](@ref) preserves the source wire areas during compaction and retains +the common lay specification used for helical corrections. =# -material = get(materials, "copper") -core = ConductorGroup(CircStrands(0.0, Diameter(d_w), 1, 0.0, material)) +# Select the materials used throughout the cable: +copper = Material(materials, :copper) +semicon1 = Material(materials, :semicon1) +semicon2 = Material(materials, :semicon2) +pe = Material(materials, :pe) +polyacrylate = Material(materials, :polyacrylate) +lead = Material(materials, :lead) +pp = Material(materials, :pp) +steel = Material(materials, :steel); + +stranded_core = stranded( + copper; + shape = Disk(d_w / 2), + lay = LayRatio(11.0), + compact = true, + boundary = Disk(d_core / 2) +); + +#= +### Semiconductive and insulating layers -# Add the subsequent layers of wires and inspect the object: -n_strands = 6 # Strands per layer -n_layers = 6 # Layers of strands -for i in 1:n_layers - add!(core, CircStrands, Diameter(d_w), i * n_strands, 11.0, material) -end -core +The inner semiconductor, main insulation, and outer semiconductor are specified +by `t_sc_in`, `t_ins`, and `t_sc_out`. The material records are `semicon1`, `pe`, +and `semicon2`, respectively. The source example lists nominal semiconductor +resistivities of 1000 Ω·m and 500 Ω·m, with a reference to IEC 840. The calculation +uses the properties in the selected library records. + +Water-blocking tape follows the outer semiconductor. The complete declaration +below states these layers in radial order rather than constructing intermediate +cables for each layer. +=# #= -### Inner semiconductor +### Catalogue information -Inner semiconductor (1000 Ω.m as per IEC 840): +Catalogue information is retained separately from the physical construction. +The following values are those supplied for this example. =# -material = get(materials, "semicon1") -main_insu = InsulatorGroup(Semicon(core, Thickness(t_sc_in), material)) +cable_id = "525kV_1600mm2" +datasheet_info = DatasheetInfo( + designation_code = "(N)2XH(F)RK2Y", + U0 = 500.0, # Pole-to-ground rating [kV]. + U = 525.0, # Pole-to-pole rating [kV]. + conductor_cross_section = 1600.0, # [mm²]. + screen_cross_section = 1000.0, # [mm²]. + resistance = nothing, # DC resistance [Ω/km]. + capacitance = nothing, # Capacitance [μF/km]. + inductance = nothing # Inductance [mH/km]. +) #= -### Main insulation +### Lead sheath, armor, and outer jacket + +The lead sheath is followed by the PE inner sheath and PP bedding. The armor +contains 68 steel wires with `LayRatio(10)`. The PP jacket encloses the armor. +The thickness-based layers take their inner boundary from the preceding region. -Add the insulation layer: +The core, lead sheath, and armor are separate terminals. Their connections are +assigned when the cable is placed in a system. =# -material = get(materials, "pe") -add!(main_insu, Insulator, Thickness(t_ins), material) +cable_design = @cable cable_id begin + @terminal :core begin + stranded_core + screen(semicon1; t = t_sc_in) + insulation(pe; t = t_ins) + screen(semicon2; t = t_sc_out) + screen(polyacrylate; t = t_wbt) + end + @terminal :sheath begin + sheath(lead; t = t_sc) + end + jacket(pe; t = t_pe) + bedding(pp; t = t_bed) + @terminal :armor begin + armor( + steel; + shape = Disk(d_wa / 2), + n = num_ar_wires, + lay = LayRatio(10), + tag = :armor_wire + ) + end + jacket(pp; t = t_jac) +end; + +# Inspect the completed physical design: +cable_design + +# Display the cross-section with its material scales: +cable_preview = preview( + cable_design; + backend = :cairo, + legend_cap = 0.5, + display_plot = false, #hide + controls = false #hide +) +cable_preview.figure #hide #= -### Outer semiconductor +## Cable constants and equivalent design -Outer semiconductor (500 Ω.m as per IEC 840): +Calculate the cable constants and select R/L and G/C with `values`. +`report(constants)` uses the available quantities and default display units. + +The report displays four separate quantity tables. +Each cable-constant table contains the operating frequency and named assembly +columns. Quantities with different units are not combined into one table. =# -material = get(materials, "semicon2") -add!(main_insu, Semicon, Thickness(t_sc_out), material) +constants = CableConstants(cable_design); +rlgc = (R, L, G, C) -# Water blocking (swellable) tape: -material = get(materials, "polyacrylate") -add!(main_insu, Semicon, Thickness(t_wbt), material) +constants_report = report(constants; values = rlgc) -# Group core-related components: -core_cc = CableComponent("core", core, main_insu) +#= +An equivalent physical design is a separate operation from either computation +or reporting. Request it explicitly with `homogenize`: +=# -cable_id = "525kV_1600mm2" -datasheet_info = NominalData( - designation_code = "(N)2XH(F)RK2Y", - U0 = 500.0, # Phase (pole)-to-ground voltage [kV] - U = 525.0, # Phase (pole)-to-phase (pole) voltage [kV] - conductor_cross_section = 1600.0, # [mm²] - screen_cross_section = 1000.0, # [mm²] - resistance = nothing, # DC resistance [Ω/km] - capacitance = nothing, # Capacitance [μF/km] - inductance = nothing, # Inductance in trifoil [mH/km] -) -cable_design = CableDesign(cable_id, core_cc, nominal_data = datasheet_info) +equivalent_design = homogenize(cable_design; new_id = cable_id * "_equivalent") #= -### Lead screen/sheath +## Saving the cable design -Build the wire screens on top of the previous layer: +Load an existing [`CablesLibrary`](@ref) or create one, then add the design and +its datasheet information. This saves the physical declaration for reuse. =# -material = get(materials, "lead") -screen_con = ConductorGroup(Tubular(main_insu, Thickness(t_sc), material)) - -# PE inner sheath: -material = get(materials, "pe") -screen_insu = InsulatorGroup(Insulator(screen_con, Thickness(t_pe), material)) +library = CablesLibrary() +library_file = fullfile("cables_library.json") +isfile(library_file) && load!(library; file_name = library_file); +add!(library, cable_design; datasheet = datasheet_info); +library -# PP bedding: -material = get(materials, "pp") -add!(screen_insu, Insulator, Thickness(t_bed), material) +# Write the library: +save(library; file_name = library_file); -# Group sheath components and assign to design: -sheath_cc = CableComponent("sheath", screen_con, screen_insu) -add!(cable_design, sheath_cc) +# Recover the cable through a fresh library object: +loaded_library = CablesLibrary() +load!(loaded_library; file_name = library_file) +loaded_design = loaded_library[cable_id]; #= -### Armor and outer jacket components +## Defining a cable system +### Earth model and frequency scan + +Use a homogeneous earth with resistivity 100 Ω·m, relative permittivity 10, +and relative permeability 1. Earth properties are declared independently of +the scan. The problem supplies 61 logarithmically spaced frequencies from +1 Hz to 1 MHz. =# -# Add the armor wires on top of the previous layer: -lay_ratio = 10.0 # typical value for wire screens -material = get(materials, "steel") -armor_con = ConductorGroup( - CircStrands(screen_insu, Diameter(d_wa), num_ar_wires, lay_ratio, material)) +f = collect(10.0 .^ range(0, stop = 6, length = 61)) +earth = homogeneous(rho = 100.0, eps_r = 10.0, mu_r = 1.0); + +#= +### Underground bipole configuration -# PP layer after armor: -material = get(materials, "pp") -armor_insu = InsulatorGroup(Insulator(armor_con, Thickness(t_jac), material)) +Place the pole cables at horizontal positions −0.5 m and 0.5 m, both at depth +1 m. The core connections are numbered 1 and 2. The sheath and armor connections +are assigned 0. The system length is 1000 m. +=# + +positive_pole = @at loaded_design (-0.5, -1.0) connections = ( + core = 1, sheath = 0, armor = 0) +negative_pole = @at loaded_design (0.5, -1.0) connections = ( + core = 2, sheath = 0, armor = 0) +placements = [positive_pole, negative_pole] + +cable_system = build( + LineCableSystem, + placements; + environment = earth, + system_id = "525kV_1600mm2_bipole", + line_length = 1000.0 +) -# Assign the armor parts directly to the design: -add!(cable_design, "armor", armor_con, armor_insu) +# Attach temperature, earth properties, and frequencies to the physical system: +problem = LineParametersProblem( + cable_system; + temperature = 20.0, + earth_props = earth, + frequencies = f +) +earth_params = problem.earth_props; -# Inspect the finished cable design: -plt1, _ = preview(cable_design) -plt1 #hide +# Inspect the earth parameters retained by the problem: +earth_params #= -## Examining the cable parameters (RLC) +### Cable system preview +Inspect the completed bipole and its cross-section. The earth model supplies the +background layers. `zoom_factor` controls the initial view around the cables. =# -# Summarize DC lumped parameters (R, L, C): -core_df = DataFrame(cable_design, :baseparams) +cable_system -# Obtain the equivalent electromagnetic properties of the cable: -components_df = DataFrame(cable_design, :components) +system_preview = preview( + cable_system; + earth_model = earth_params, + zoom_factor = 2.0, + backend = :cairo, + legend_cap = 0.5, + display_plot = false, #hide + controls = false #hide +) +system_preview.figure #hide #= -## Saving the cable design +## PSCAD and ATPDraw export -Load an existing [`CablesLibrary`](@ref) file or create a new one: +Export the physical system and earth parameters using the public exporters. +These calls write input files for use in PSCAD and ATPDraw. =# +pscad_file = export_data( + :pscad, cable_system, earth_params; + file_name = fullfile("pscad_export.pscx") +); -library = CablesLibrary() -library_file = fullfile("cables_library.json") -load!(library, file_name = library_file) -add!(library, cable_design) -library_df = DataFrame(library) - -# Save to file for later use: -save(library, file_name = library_file); +atp_system_file = export_data( + :atp, cable_system, earth_params; + file_name = fullfile("atp_export.xml") +); #= -## Defining a cable system +## Frequency-dependent line parameters +[`Formulation`](@ref) selects the physical and numerical methods. The native +default uses scaled-Bessel internal impedance, lossless insulation models, and +the default earth-return equations. The formulation value can also be used by +parametric and uncertainty-analysis workflows. =# +formulation = Formulation() + +# Run the frequency scan: +@time line_parameters = compute( + problem, + formulation; + options = (verbosity = (default = 0,),) +); + #= -### Earth model +### Numerical access -Define a constant frequency earth model: +The immediate form of `@observe` extracts numerical values for further +calculation. Inspect the range of the +computed first self-conductance coefficient before reporting resolution is +applied: =# -f = [1e-3] # Near DC frequency for the analysis -earth_params = EarthModel(f, 100.0, 10.0, 1.0) # 100 Ω·m resistivity, εr=10, μr=1 - -# Earth model base (DC) properties: -earthmodel_df = DataFrame(earth_params) +conductance_residual = extrema(@observe line_parameters G[1, 1, :]) #= -### Underground bipole configuration +### Quantity tables + +Use the same R/L and G/C selection declared for the cable constants. The report +contains four separate quantity tables. Each full matrix table has one frequency +column followed by all ordered matrix coefficients, including both off-diagonals. +`length_unit=:kilo` displays the per-unit-length quantities per kilometer. =# -# Define the coordinates for both cables: -xp, xn, y0 = -0.5, 0.5, -1.0; +phase_report = report( + line_parameters; + values = rlgc, + length_unit = :kilo +) -# Initialize the `LineCableSystem` with positive pole: -cablepos = CablePosition(cable_design, xp, y0, - Dict("core" => 1, "sheath" => 0, "armor" => 0)) -cable_system = LineCableSystem("525kV_1600mm2_bipole", 1000.0, cablepos) +#= +To tabulate a selected coefficient or frequency subset, express that request +with `@observe` in `values`. The following selects the first +self-resistance coefficient at the first twelve frequencies. No knowledge of +the generated DataFrame column names is needed to make the selection. +=# -# Add the other pole (negative) to the system: -add!(cable_system, cable_design, xn, y0, - Dict("core" => 2, "sheath" => 0, "armor" => 0)) +first_term_report = report( + line_parameters; + values = @observe(R[1, 1, 1:12]), + length_unit = :kilo +) #= -### Cable system preview +### R/L and G/C plots -In this section the complete bipole cable system is examined. -=# +Pass the computed result directly to `LineCableModels.plot`. Plotting uses +`ydata` for the same quantity selection that reporting names `values`. -# Display system details: -system_df = DataFrame(cable_system) +Each requested quantity has its own matrix dashboard. Matrix coordinates +identify subplots. Each trace follows that coefficient over frequency. The +plots retain the complete matrices rather than assuming symmetry or discarding +small entries in the renderer. +=# -# Visualize the cross-section of the three-phase system: -plt2, _ = preview(cable_system, earth_model = earth_params, zoom_factor = 2.0) -plt2 #hide +phase_plots = LineCableModels.plot( + line_parameters; + ydata = rlgc, + xscale = :log10, + length_unit = :kilo, + fig_size = (1100, 750), + backend = :cairo, + display_plot = false, #hide + controls = false #hide +) +phase_plots[1].figure #hide +phase_plots[2].figure #hide +phase_plots[3].figure #hide +phase_plots[4].figure #hide #= -## PSCAD & ATPDraw export -Export to PSCAD input file: -=# +### Real and imaginary components of Z and Y -output_file = fullfile("pscad_export.pscx") -export_file = export_data(:pscad, cable_system, earth_params, file_name = output_file); +With no `ydata` override, the line-result convenience selects R/X and G/B: +the real and imaginary components of Z and Y. This gives four separate matrix +dashboards, using the same public call and the same reporting-unit choice. +=# -# Export to ATPDraw project file (XML): -output_file = fullfile("atp_export.xml") -export_file = export_data(:atp, cable_system, earth_params, file_name = output_file); +zy_plots = LineCableModels.plot( + line_parameters; + xscale = :log10, + length_unit = :kilo, + fig_size = (1100, 750), + backend = :cairo, + display_plot = false, #hide + controls = false #hide +) +zy_plots[1].figure #hide +zy_plots[2].figure #hide +zy_plots[3].figure #hide +zy_plots[4].figure #hide #= -## FEM calculations +### Exporting the computed Z/Y matrices + +The ATP exporter also accepts the completed line parameters. Supply the cable +system alongside the numerical result: =# -# Define a LineParametersProblem with the cable system and earth model -problem = LineParametersProblem( - cable_system, - temperature = 20.0, # Operating temperature - earth_props = earth_params, - frequencies = f, # Frequency for the analysis +atp_parameters_file = export_data( + :atp, line_parameters; + file_name = fullfile("ZY_export.xml"), + cable_system = cable_system ); -# Estimate domain size based on skin depth in the earth -domain_radius = 10.0; #calc_domain_size(earth_params, f); - -# Define custom mesh transitions around each cable -mesh_transition1 = MeshTransition( - cable_system, - [1], - r_min = 0.08, - r_length = 0.25, - mesh_factor_min = 0.01 / (domain_radius / 5), - mesh_factor_max = 0.25 / (domain_radius / 5), - n_regions = 5) - -mesh_transition2 = MeshTransition( - cable_system, - [2], - r_min = 0.08, - r_length = 0.25, - mesh_factor_min = 0.01 / (domain_radius / 5), - mesh_factor_max = 0.25 / (domain_radius / 5), - n_regions = 5); - -# Define runtime options -opts = ( - force_remesh = true, # Force remeshing - force_overwrite = true, # Overwrite existing files - plot_field_maps = false, # Do not compute/ plot field maps - mesh_only = true, # Preview the mesh - save_path = fullfile("fem_output"), # Results directory - keep_run_files = true, # Archive files after each run - verbosity = 1, # Verbosity -); +#= +## Modal transformation -# Define the FEM formulation with the specified parameters -F = FormulationSet(:FEM, - impedance = Darwin(), - admittance = Electrodynamics(), - domain_radius = domain_radius, - domain_radius_inf = domain_radius * 1.25, - elements_per_length_conductor = 1, - elements_per_length_insulator = 2, - elements_per_length_semicon = 1, - elements_per_length_interfaces = 5, - points_per_circumference = 16, - mesh_size_min = 1e-6, - mesh_size_max = domain_radius / 5, - # mesh_transitions = [mesh_transition1, - # mesh_transition2], - mesh_size_default = domain_radius / 10, - mesh_algorithm = 5, - mesh_max_retries = 20, - materials = materials, - options = opts, +Compute the default frequency-dependent modal transformation as a separate +operation on the completed phase-domain result. Retain the transformation +operators for numerical inspection. +=# + +modal_parameters = compute( + ModalAnalysisProblem(line_parameters), + ModalAnalysisFormulation(:default); + options = (offdiagonal_tolerance = 1e-5,) ); -# Run the FEM solver -@time ws, p = compute!(problem, F); +modal_voltage_basis = Tv(modal_parameters); + +# Read transformed coefficients through the same immediate observation API: +modal_impedance = @observe modal_parameters Z[1, 1, :] +modal_admittance = @observe modal_parameters Y[1, 1, :] -# Display computation results -per_km(p, 1; mode = :RLCG, tol = 1e-9) +#= +### Modal quantity tables + +The transformed result uses the same reporting API and the same full-matrix +R/L and G/C selection. Modal-domain coordinates change the interpretation of +the matrix indices, not the organization of the quantity tables. +=# + +modal_report = report( + modal_parameters; + values = rlgc, + length_unit = :kilo +) -# Export ZY matrices to ATPDraw -output_file = fullfile("ZY_export.xml") -export_file = export_data(:atp, p; file_name = output_file, cable_system = cable_system); +# Apply the same coefficient and sample selection used for the phase result: +first_modal_term_report = report( + modal_parameters; + values = @observe(R[1, 1, 1:12]), + length_unit = :kilo +) -# Obtain the symmetrical components via Fortescue transformation -Tv, p012 = Fortescue(tol = 1e-5)(p); +#= +### Full modal matrix plots -# Inspect the transformed matrices -per_km(p012, 1; mode = :ZY, tol = 1e-9) +Keep the off-diagonal coefficients in the modal view. Entries reduced to zero +by the observation layer remain visible as zero traces. Residual coupling that +survives reporting resolution remains visible in its original matrix position. -# Or the corresponding lumped circuit quantities -per_km(p012, 1; mode = :RLCG, tol = 1e-9) \ No newline at end of file +This uses the same quantity selection and plotting call as the phase-domain +view. The full transformed matrices remain available for inspection. +=# + +modal_plots = LineCableModels.plot( + modal_parameters; + ydata = rlgc, + xscale = :log10, + length_unit = :kilo, + fig_size = (1100, 750), + backend = :cairo, + display_plot = false, #hide + controls = false #hide +) +modal_plots[1].figure #hide +modal_plots[2].figure #hide +modal_plots[3].figure #hide +modal_plots[4].figure #hide diff --git a/ext/LineCableModelsCairoMakieExt.jl b/ext/LineCableModelsCairoMakieExt.jl new file mode 100644 index 000000000..fd4865aed --- /dev/null +++ b/ext/LineCableModelsCairoMakieExt.jl @@ -0,0 +1,14 @@ +""" + LineCableModelsCairoMakieExt + +Activate CairoMakie for non-interactive LineCableModels rendering. +""" +module LineCableModelsCairoMakieExt + +import CairoMakie +public CairoMakie + +activate!() = (CairoMakie.activate!(); :cairo) +make_screen(::AbstractString; kwargs...) = nothing + +end # module LineCableModelsCairoMakieExt diff --git a/ext/LineCableModelsDistributionsExt.jl b/ext/LineCableModelsDistributionsExt.jl new file mode 100644 index 000000000..2bf29f98c --- /dev/null +++ b/ext/LineCableModelsDistributionsExt.jl @@ -0,0 +1,164 @@ +""" + LineCableModelsDistributionsExt + +Sample `UncertainValue` with Distributions.jl and expose `HistogramDensity` +through the Distributions API. +""" +module LineCableModelsDistributionsExt + +import Distributions +import Random +import Statistics + +import LineCableModels.ParametricBuilder as PB +import LineCableModels.UQ +import LineCableModels: sample_uncertainty +import LineCableModels.ImportExport: serialize_value, deserialize_value, deserialize_extension + +serialize_value(distribution::Distributions.UnivariateDistribution) = throw(ArgumentError( + "no portable scientific codec for $(typeof(distribution)); supported input-law records are Normal and Uniform")) + +function serialize_value(distribution::Union{Distributions.Normal,Distributions.Uniform}) + return Dict("__type__"=>"Distribution","name"=>distribution isa Distributions.Normal ? "Normal" : "Uniform", + "parameters"=>serialize_value(Distributions.params(distribution))) +end +function deserialize_extension(::Val{:Distribution},record) + parameters=deserialize_value(record["parameters"]) + record["name"]=="Normal" && return Distributions.Normal(parameters...) + record["name"]=="Uniform" && return Distributions.Uniform(parameters...) + throw(ArgumentError("no portable codec for the recorded input distribution")) +end + +function sample_uncertainty( + rng::Random.AbstractRNG, + value::PB.UncertainValue{<:Real}, + distribution::Distributions.UnivariateDistribution +) + standardized = rand(rng, distribution) + distribution_mean = Statistics.mean(distribution) + distribution_std = Statistics.std(distribution) + isfinite(distribution_mean) && isfinite(distribution_std) && + distribution_std > zero(distribution_std) || throw(ArgumentError( + "Monte Carlo distributions must have finite mean and positive finite standard deviation", + )) + isfinite(standardized) || throw(ArgumentError( + "Monte Carlo distribution produced a non-finite realization", + )) + return value.nominal + + value.sigma * + (standardized - distribution_mean) / distribution_std +end + +function Distributions.pdf(distribution::UQ.HistogramDensity, value::Real) + value < first(distribution.edges) && return 0.0 + value > last(distribution.edges) && return 0.0 + index = value == last(distribution.edges) ? length(distribution.density) : + searchsortedlast(distribution.edges, value) + return distribution.density[index] +end + +function (distribution::UQ.HistogramDensity)(value::Real) + Distributions.pdf(distribution, value) +end +function Base.minimum(distribution::UQ.HistogramDensity) + first(distribution.edges) +end +Base.maximum(distribution::UQ.HistogramDensity) = last(distribution.edges) +function Distributions.insupport( + distribution::UQ.HistogramDensity, + value::Real +) + minimum(distribution) <= value <= maximum(distribution) +end + +function Distributions.logpdf( + distribution::UQ.HistogramDensity, + value::Real +) + probability = Distributions.pdf(distribution, value) + return probability > 0 ? log(probability) : -Inf +end + +function Distributions.cdf(distribution::UQ.HistogramDensity, value::Real) + return UQ.cumulative_probability(distribution, value) +end + +struct HistogramDensitySampler{H, T} <: + Distributions.Sampleable{Distributions.Univariate, Distributions.Continuous} + distribution::H + cumulative_probability::Vector{T} +end + +function Distributions.sampler(distribution::UQ.HistogramDensity) + probabilities = distribution.density .* diff(distribution.edges) + cumulative = cumsum(probabilities) + cumulative[end] = one(eltype(cumulative)) + return HistogramDensitySampler(distribution, cumulative) +end + +function Distributions.quantile(sampler::HistogramDensitySampler, probability::Real) + probability <= 0 && return minimum(sampler.distribution) + probability >= 1 && return maximum(sampler.distribution) + index = searchsortedfirst(sampler.cumulative_probability, probability) + prior = index == 1 ? zero(eltype(sampler.cumulative_probability)) : + sampler.cumulative_probability[index - 1] + density = sampler.distribution.density[index] + iszero(density) && return sampler.distribution.edges[index] + return sampler.distribution.edges[index] + (probability - prior) / density +end + +function Base.rand(rng::Random.AbstractRNG, sampler::HistogramDensitySampler) + probability = rand(rng) + index = searchsortedfirst(sampler.cumulative_probability, probability) + prior = index == 1 ? zero(probability) : + sampler.cumulative_probability[index - 1] + mass = sampler.cumulative_probability[index] - prior + fraction = iszero(mass) ? zero(probability) : (probability - prior) / mass + left = sampler.distribution.edges[index] + right = sampler.distribution.edges[index + 1] + return left + fraction * (right - left) +end + +function Base.rand(rng::Random.AbstractRNG, distribution::UQ.HistogramDensity) + rand(rng, Distributions.sampler(distribution)) +end + +function _raw_moment(distribution::UQ.HistogramDensity, order::Integer) + order >= 0 || throw(ArgumentError("moment order must be nonnegative")) + T = eltype(distribution.density) + total = zero(T) + exponent = order + 1 + for index in eachindex(distribution.density) + left = distribution.edges[index] + right = distribution.edges[index + 1] + total += distribution.density[index] * + (right^exponent - left^exponent) / exponent + end + return total +end + +function Distributions.moment(distribution::UQ.HistogramDensity, order::Integer) + _raw_moment(distribution, order) +end +Statistics.mean(distribution::UQ.HistogramDensity) = _raw_moment(distribution, 1) +function Statistics.var(distribution::UQ.HistogramDensity) + variance = _raw_moment(distribution, 2) - Statistics.mean(distribution)^2 + return max(variance, zero(variance)) +end +function Statistics.std(distribution::UQ.HistogramDensity) + sqrt(Statistics.var(distribution)) +end + +function Distributions.mode(distribution::UQ.HistogramDensity) + _, index = findmax(distribution.density) + return (distribution.edges[index] + distribution.edges[index + 1]) / 2 +end + +function Distributions.modes(distribution::UQ.HistogramDensity) + maximum_density = maximum(distribution.density) + indices = findall(==(maximum_density), distribution.density) + return [(distribution.edges[index] + distribution.edges[index + 1]) / 2 + for index in indices] +end + +end diff --git a/ext/LineCableModelsGLMakieExt.jl b/ext/LineCableModelsGLMakieExt.jl new file mode 100644 index 000000000..0e4ee86ca --- /dev/null +++ b/ext/LineCableModelsGLMakieExt.jl @@ -0,0 +1,26 @@ +""" + LineCableModelsGLMakieExt + +Activate GLMakie and create interactive plot screens for LineCableModels. +""" +module LineCableModelsGLMakieExt + +import GLMakie + +activate!() = (GLMakie.activate!(); :gl) +function make_screen( + title::AbstractString; + minimum_size::Tuple{Int, Int} = (1, 1), + kwargs... +) + screen = GLMakie.Screen(; title = String(title), kwargs...) + GLMakie.GLFW.SetWindowSizeLimits( + screen.glscreen, + minimum_size..., + GLMakie.GLFW.DONT_CARE, + GLMakie.GLFW.DONT_CARE + ) + return screen +end + +end # module LineCableModelsGLMakieExt diff --git a/ext/LineCableModelsGmshExt/LineCableModelsGmshExt.jl b/ext/LineCableModelsGmshExt/LineCableModelsGmshExt.jl new file mode 100644 index 000000000..888983e4a --- /dev/null +++ b/ext/LineCableModelsGmshExt/LineCableModelsGmshExt.jl @@ -0,0 +1,39 @@ +module LineCableModelsGmshExt + +using Gmsh: gmsh +import Gmsh +import JSON3 +import Logging +using Base.BinaryPlatforms: HostPlatform, triplet +using Pkg.Artifacts: artifact_hash, ensure_artifact_installed +using Logging: AbstractLogger, SimpleLogger, @debug, @info, + @warn, with_logger +using Printf: @sprintf +using SHA: sha256 + +import LineCableModels +import LineCableModels: compute, validate +import LineCableModels.DataModel +import LineCableModels.Earth +import LineCableModels.Commons +import LineCableModels.Engine +import LineCableModels.ImportExport +using LineCableModels.Commons: computation_options, ComputationOptions, ComputationDetails +using LineCableModels.Commons: vacuum_permittivity, vacuum_permeability +using LineCableModels.Commons: ReductionPlan, reduce_line_matrices!, initialize_buffers +using LineCableModels: LineCableModelsFEM, LineCableModelsFEMError, + LineParametersProblem, LineParameters, + SeriesImpedance, ShuntAdmittance, PhaseDomain + +include("model.jl") +include("formulations.jl") +include("geometry.jl") +include("mesh.jl") +include("onelab.jl") +include("getdp.jl") +include("voltage_paths.jl") +include("workers.jl") +include("results.jl") +include("compute.jl") + +end diff --git a/ext/LineCableModelsGmshExt/compute.jl b/ext/LineCableModelsGmshExt/compute.jl new file mode 100644 index 000000000..5ce7d4f8d --- /dev/null +++ b/ext/LineCableModelsGmshExt/compute.jl @@ -0,0 +1,796 @@ +struct FEMTeeLogger{A <: AbstractLogger, B <: AbstractLogger} <: AbstractLogger + first::A + second::B +end + +function Logging.min_enabled_level(logger::FEMTeeLogger) + min( + Logging.min_enabled_level(logger.first), + Logging.min_enabled_level(logger.second) + ) +end +function Logging.catch_exceptions(logger::FEMTeeLogger) + Logging.catch_exceptions(logger.first) && Logging.catch_exceptions(logger.second) +end +function Logging.shouldlog(logger::FEMTeeLogger, level, module_, group, id) + return (level >= Logging.min_enabled_level(logger.first) && + Logging.shouldlog(logger.first, level, module_, group, id)) || + (level >= Logging.min_enabled_level(logger.second) && + Logging.shouldlog(logger.second, level, module_, group, id)) +end +function Logging.handle_message( + logger::FEMTeeLogger, + level, + message, + module_, + group, + id, + file, + line; + kwargs... +) + if level >= Logging.min_enabled_level(logger.first) && + Logging.shouldlog(logger.first, level, module_, group, id) + Logging.handle_message( + logger.first, level, message, module_, group, id, file, line; kwargs... + ) + end + if level >= Logging.min_enabled_level(logger.second) && + Logging.shouldlog(logger.second, level, module_, group, id) + Logging.handle_message( + logger.second, level, message, module_, group, id, file, line; kwargs... + ) + end + return nothing +end + +struct FEMGmshSession + owned::Bool + previous_model::String + initial_models::Set{String} + initial_views::Set{Int} + terminal_option::Float64 + verbosity_option::Float64 + geometry_tolerance::Float64 + boolean_tolerance::Float64 + onelab::FEMOnelabSnapshot +end + +const FEM_SESSION_LOCK = ReentrantLock() + +# Keep this file in place: unlinking a locked inode permits another coordinator +# to lock a replacement file. OS ownership is released when the stream closes +# or its Julia process exits, including after a hard crash. +function _claim_run(run::FEMRun) + io = open(joinpath(run.path, "coordinator.lock"), "a+") + seekstart(io) + status = if Sys.iswindows() + ccall(:_locking, Cint, (Cint, Cint, Clong), fd(io), 2, 1) + else + ccall(:flock, Cint, (Cint, Cint), fd(io), 6) + end + if status != 0 + close(io) + _fem_error(:execution, "FEM run", :ownership, + "another coordinator owns this run, or the filesystem cannot lock it"; + run_directory = run.path) + end + return io +end + +function _release_run(io::IOStream) + if Sys.iswindows() + seekstart(io) + ccall(:_locking, Cint, (Cint, Cint, Clong), fd(io), 0, 1) + end + close(io) + return nothing +end + +function _start_gmsh(verbosity::Int) + owned = !Bool(gmsh.is_initialized()) + # Backend-owned meshes must not depend on an unrecorded user gmshrc. + owned && gmsh.initialize(String[], false, false) + previous_model = try + gmsh.model.get_current() + catch + "" + end + initial_models = Set(String.(gmsh.model.list())) + initial_views = Set(Int.(gmsh.view.get_tags())) + terminal_option = gmsh.option.get_number("General.Terminal") + verbosity_option = gmsh.option.get_number("General.Verbosity") + geometry_tolerance = gmsh.option.get_number("Geometry.Tolerance") + boolean_tolerance = gmsh.option.get_number("Geometry.ToleranceBoolean") + onelab = _snapshot_onelab() + gmsh.option.set_number("General.Terminal", verbosity > 0 ? 1 : 0) + gmsh.option.set_number("General.Verbosity", verbosity) + gmsh.option.set_number("Geometry.Tolerance", _GMSH_GEOMETRY_TOLERANCE) + gmsh.option.set_number("Geometry.ToleranceBoolean", _GMSH_BOOLEAN_TOLERANCE) + return FEMGmshSession( + owned, + previous_model, + initial_models, + initial_views, + terminal_option, + verbosity_option, + geometry_tolerance, + boolean_tolerance, + onelab + ) +end + +function _finish_gmsh(session::FEMGmshSession) + if session.owned + Gmsh.finalize() + return nothing + end + for tag in setdiff(Set(Int.(gmsh.view.get_tags())), session.initial_views) + try + gmsh.view.remove(tag) + catch exception + @warn "Failed to remove a backend-created Gmsh view" view = tag exception = ( + exception, catch_backtrace()) + end + end + for name in setdiff(Set(String.(gmsh.model.list())), session.initial_models) + try + gmsh.model.set_current(name) + gmsh.model.remove() + catch exception + @warn "Failed to remove a backend-created Gmsh model" model = name exception = ( + exception, catch_backtrace()) + end + end + isempty(session.previous_model) || try + gmsh.model.set_current(session.previous_model) + catch exception + @warn "Failed to restore the caller's current Gmsh model" model = session.previous_model exception = ( + exception, catch_backtrace()) + end + gmsh.option.set_number("General.Terminal", session.terminal_option) + gmsh.option.set_number("General.Verbosity", session.verbosity_option) + gmsh.option.set_number("Geometry.Tolerance", session.geometry_tolerance) + gmsh.option.set_number("Geometry.ToleranceBoolean", session.boolean_tolerance) + _restore_onelab(session.onelab) + return nothing +end + +function _runtime_root() + return joinpath(pkgdir(LineCableModels), ".linecablemodels", "fem") +end + +function _create_run(runtime_root::String) + runs = joinpath(runtime_root, "runs") + mkpath(runs) + path = mktempdir(runs; prefix = "run-", cleanup = false) + for directory in ("input", "mesh", "raw", "maps", "logs") + mkpath(joinpath(path, directory)) + end + run = FEMRun(path, created, "run created", :none, "") + _transition!(run, created, "run created") + return run +end + +function _resume_value_matches(existing::AbstractDict, expected::AbstractDict) + length(existing) == length(expected) || return false + return all(keys(expected)) do key + haskey(existing, key) && + _resume_value_matches(existing[key], expected[key]) + end +end + +function _resume_value_matches(existing::AbstractVector, expected::AbstractVector) + length(existing) == length(expected) || return false + return all(splat(_resume_value_matches), zip(existing, expected)) +end + +_resume_value_matches(existing, expected) = isequal(existing, expected) + +function _resume_inputs_match(path::String, model::FEMResolvedModel, inputs::NamedTuple) + snapshot = joinpath(path, "input", "problem.json") + computation = joinpath(path, "input", "computation.json") + state = joinpath(path, "run.json") + all(isfile, (snapshot, computation, state)) || return false + existing, recorded, run_state = try + (JSON3.read(read(snapshot, String)), JSON3.read(read(computation, String)), + JSON3.read(read(state, String))) + catch exception + exception isa Union{SystemError, Base.IOError, ArgumentError} || rethrow() + return false + end + existing isa AbstractDict && recorded isa AbstractDict && run_state isa AbstractDict || + return false + get(run_state, :state, nothing) isa AbstractString && + get(recorded, :execution, nothing) isa AbstractDict || return false + # External Gmsh sessions can include arbitrary caller-owned meshing settings. + inputs.owned_gmsh || return false + if String(run_state.state) == string(completed) + inputs.getdp_identity === nothing && return false + inputs.execution.ui && return false + inputs.execution.mesh_mode === :remesh && return false + isfile(joinpath(path, "raw", "checksums.json")) || return false + end + expected = ImportExport.serialize_value(model.problem) + comparable = Dict(String(key)=>value for (key, value) in pairs(recorded)) + requested = Dict(String(key)=>value + for (key, value) in pairs(JSON3.read(JSON3.write(inputs)))) + # A solver-input schema change requires a fresh run. + get(comparable, "schema_version", 0) == inputs.schema_version == 7 || return false + get(comparable, "solver_protocol", 0) == inputs.solver_protocol == 3 || return false + # Scheduling and executable location do not change the numerical problem. + for record in (comparable, requested) + execution = Dict(String(key)=>value for (key, value) in pairs(record["execution"])) + pop!(execution, "frequency_workers", nothing) + pop!(execution, "getdp_executable", nothing) + record["execution"] = execution + pop!(record, "getdp_selection", nothing) + end + return _resume_value_matches(existing, expected) && + isequal(comparable, requested) +end + +function _resume_run( + runtime_root::String, + requested::Union{Nothing, Symbol, String}, + model::FEMResolvedModel, + inputs::NamedTuple +) + requested === nothing && return _create_run(runtime_root) + runs = joinpath(runtime_root, "runs") + mkpath(runs) + runs_path = realpath(runs) + run_directory = if requested === :latest + directories = filter(isdir, readdir(runs_path; join = true)) + sort!(directories; by = path -> stat(path).mtime, rev = true) + index = findfirst( + path -> _resume_inputs_match(path, model, inputs), directories + ) + index === nothing ? nothing : directories[index] + elseif requested isa String + path = abspath(requested) + isdir(path) || throw(ArgumentError( + "resume_run_directory does not exist: $path", + )) + _resume_inputs_match(path, model, inputs) || throw(ArgumentError( + "resume_run_directory needs matching problem, constitutive inputs, " * + "solver and adapter identities, and backend-owned meshing settings: $path; " * + "start a new run when these inputs changed or the run predates input snapshots", + )) + path + else + throw(ArgumentError( + "resume_run_directory must be nothing, :latest, or a path string", + )) + end + run_directory === nothing && return _create_run(runtime_root) + path = realpath(run_directory) + dirname(path) == runs_path || throw(ArgumentError( + "resume_run_directory must be an immediate child of $runs_path", + )) + for directory in ("input", "mesh", "raw", "maps", "logs") + isdir(joinpath(path, directory)) || throw(ArgumentError( + "resume_run_directory is missing $directory/: $path", + )) + end + state_path = joinpath(path, "run.json") + document = try + JSON3.read(read(state_path, String)) + catch exception + exception isa Union{SystemError, Base.IOError, ArgumentError} || rethrow() + _fem_error(:execution, model.problem.system.system_id, :resume_run_directory, + "cannot read selected run metadata $state_path: $(sprint(showerror, exception))"; + run_directory=path) + end + document isa AbstractDict && + all(key -> get(document, key, nothing) isa AbstractString, + (:state, :mesh_source, :mesh_fingerprint)) && + all((:getdp_invocations, :completed_columns, :completed_frequencies)) do key + value = get(document, key, key === :getdp_invocations ? nothing : 0) + value isa Integer && !(value isa Bool) && 0 <= value <= typemax(Int) + end || _fem_error(:execution, model.problem.system.system_id, :resume_run_directory, + "invalid selected run metadata in $state_path"; run_directory=path) + run = FEMRun( + path, + created, + "resuming interrupted run", + Symbol(String(document.mesh_source)), + String(document.mesh_fingerprint), + Int(document.getdp_invocations) + ) + run.completed_columns = Int(get(document, :completed_columns, 0)) + run.completed_frequencies = Int(get(document, :completed_frequencies, 0)) + if String(document.state) == string(completed) + run.state = completed + run.message = "reading completed run" + return run + end + @debug "Resuming compatible FEM run" run_directory=path + return run +end + +function _transition!(run::FEMRun, state::FEMRunState, message::AbstractString) + run.state = state + run.message = String(message) + _write_json_atomic(joinpath(run.path, "run.json"), + ( + schema = "LineCableModels.FEMRun", + version = 2, + state = string(state), + message = run.message, + mesh_source = String(run.mesh_source), + mesh_fingerprint = run.mesh_fingerprint, + getdp_invocations = run.getdp_invocations, + completed_columns = run.completed_columns, + completed_frequencies = run.completed_frequencies, + updated_unix_seconds = time() + )) + return state +end + +function _write_run_inputs!(run::FEMRun, model::FEMResolvedModel) + asset_directory = joinpath(run.path, "input", "getdp") + mkpath(asset_directory) + for (name, path) in pairs(_getdp_assets(asset_directory)) + contents = getproperty(FEM_GETDP_SOURCES, name) + if isfile(path) + read(path, String) == contents || _fem_error( + :getdp, "GetDP", :assets, + "stored solver sources differ from this implementation; start a new run"; + run_directory = run.path) + else + write(path, contents) + end + end + problem_path = joinpath(run.path, "input", "problem.json") + model_data_path = joinpath(run.path, "input", "model_data.pro") + _write_problem_snapshot(problem_path, model.problem) + _write_model_data(model_data_path, model) + mkpath(joinpath(run.path, "raw", "jobs")) + open(joinpath(run.path, "raw", "Z.tsv"), "w") do io + println(io, join(FEM_RAW_HEADER, '\t')) + end + open(joinpath(run.path, "raw", "P.tsv"), "w") do io + println(io, join(FEM_RAW_HEADER, '\t')) + end + open(joinpath(run.path, "raw", "scan_complete.tsv"), "w") do io + println(io, join(FEM_COMPLETE_HEADER, '\t')) + end + return model_data_path +end + +function _headless_solve!( + run::FEMRun, + model::FEMResolvedModel, + formulation::LineCableModelsFEM, + execution::ComputationOptions, + runtime_root::String, + inputs::NamedTuple +) + geometry = _build_geometry!( + model, "LineCableModelsFEM-$(basename(run.path))" + ) + _transition!(run, geometry_ready, "geometry ready") + @debug "FEM geometry ready" run_directory=run.path + mesh_paths = _select_meshes!( + run, model, geometry, execution, runtime_root + ) + _transition!(run, mesh_ready, "mesh ready") + @debug "FEM mesh ready" source=run.mesh_source fingerprint=run.mesh_fingerprint + model_data_path = _write_run_inputs!(run, model) + _write_run_voltage_paths!(run, model, formulation, mesh_paths) + _publish_transport!( + run, model, model_data_path, last(mesh_paths), formulation, execution + ) + _transition!(run, running, "GetDP frequency batches running") + @debug "Starting isolated GetDP frequency batches" workers=execution.data.frequency_workers + _run_getdp!(run, model, formulation, execution, mesh_paths) + scan = _parse_scan(run, model, formulation, execution) + _write_scan_checksums(run, scan) + gmsh.onelab.set_number(_onelab_name("completion_status"), [1.0]) + _transition!(run, completed, "results validated") + parameters = _line_parameters(run, model, formulation, execution, scan, inputs) + return parameters +end + +function _ui_solve!( + run::FEMRun, + model::FEMResolvedModel, + formulation::LineCableModelsFEM, + execution::ComputationOptions, + runtime_root::String, + inputs::NamedTuple +) + geometry = _build_geometry!( + model, "LineCableModelsFEM-$(basename(run.path))" + ) + _transition!(run, geometry_ready, "geometry ready") + state = :geometry_ready + _publish_ui!(run, model, state) + Bool(gmsh.fltk.is_available()) || gmsh.fltk.initialize() + gmsh.fltk.wait(0.05) + yield() + mesh_paths = nothing + model_data_path = nothing + parameters = nothing + while true + available = Bool(gmsh.fltk.is_available()) + transition = _ui_transition(state, _take_ui_action(), available) + if transition === :closed_before_mesh + _transition!(run, not_executed, "UI closed before mesh generation") + _fem_error( + :not_executed, + model.problem.system.system_id, + :ui, + "Gmsh UI closed before mesh generation; no FEM solve was executed"; + run_directory = run.path + ) + elseif transition === :closed_before_solve + _transition!(run, not_executed, "UI closed after mesh generation before solve") + _fem_error( + :not_executed, + model.problem.system.system_id, + :ui, + "Gmsh UI closed after mesh generation but before Run model; " * + "no FEM solve was executed"; + run_directory = run.path + ) + elseif !available + parameters === nothing || return parameters + _transition!(run, not_executed, "UI closed without a solve") + _fem_error( + :not_executed, + model.problem.system.system_id, + :ui, + "Gmsh UI closed before a FEM solve was executed"; + run_directory = run.path + ) + elseif transition === :mesh_required + _set_ui_status(:mesh_required) + elseif transition === :mesh_requested + mesh_paths = _select_meshes!( + run, model, geometry, execution, runtime_root + ) + model_data_path = _write_run_inputs!(run, model) + _write_run_voltage_paths!(run, model, formulation, mesh_paths) + _publish_transport!( + run, model, model_data_path, last(mesh_paths), formulation, execution + ) + _transition!(run, mesh_ready, "mesh ready") + state = _set_ui_status(:mesh_ready) + elseif transition === :solve_requested + mesh_paths === nothing && begin + _set_ui_status(:mesh_required) + gmsh.fltk.wait(0.05) + continue + end + state = _set_ui_status(:running) + _transition!(run, running, "GetDP frequency batches running") + progress = (-1, -1) + _run_getdp!(run, + model, + formulation, + execution, + mesh_paths; + pump = () -> begin + # fltk.wait can initialize a GUI again after it was closed. + Bool(gmsh.fltk.is_available()) || return false + current = (run.completed_frequencies, run.completed_columns) + if current != progress + gmsh.onelab.set_number(_onelab_name("ui/completed_frequencies"), [current[1]]) + gmsh.onelab.set_number(_onelab_name("ui/completed_columns"), [current[2]]) + progress = current + end + gmsh.fltk.wait(0.01) + Bool(gmsh.fltk.is_available()) + end) + scan = _parse_scan(run, model, formulation, execution) + _write_scan_checksums(run, scan) + execution.data.plot_field_maps && _merge_maps!(scan.map_paths) + gmsh.onelab.set_number(_onelab_name("completion_status"), [1.0]) + gmsh.onelab.set_number(_onelab_name("ui/completed_frequencies"), [run.completed_frequencies]) + gmsh.onelab.set_number(_onelab_name("ui/completed_columns"), [run.completed_columns]) + _transition!(run, completed, "results ready") + parameters = _line_parameters(run, model, formulation, execution, scan, inputs) + state = _set_ui_status(:results_ready) + end + gmsh.fltk.wait(0.05) + yield() + end +end + +function _attach_run_directory( + exception::LineCableModelsFEMError, + run::FEMRun +) + exception.run_directory !== nothing && return exception + return LineCableModelsFEMError( + exception.category, + exception.object_id, + exception.field, + exception.message; + run_directory = run.path + ) +end + +function _compute_fem( + problem::LineParametersProblem{Float64}, + formulation::LineCableModelsFEM, + execution::ComputationOptions, + model::FEMResolvedModel = _resolved_fem_model(problem, formulation, execution) +) + runtime_root = _runtime_root() + inputs = _fem_input_record(model, formulation, execution) + run = _resume_run( + runtime_root, execution.data.resume_run_directory, model, inputs + ) + if run.state === completed + # Reuse only reads the completed run. Do not start Gmsh, reset scratch + # files, append logs, change run state or clean up its files. + scan = _parse_scan(run, model, formulation, execution) + validate(scan, run) + @debug "FEM reuses completed resolved inputs" run_directory=run.path + return _line_parameters( + run, model, formulation, execution, scan, inputs; reused = true) + end + ownership = _claim_run(run) + parameters = try + # Another coordinator can finish between run_directory selection and our + # lock acquisition. Refresh counters and state while holding ownership, + # and preserve a now-completed run as a read-only result. + if isfile(joinpath(run.path, "input", "computation.json")) && + isfile(joinpath(run.path, "input", "problem.json")) + run = _resume_run(runtime_root, run.path, model, inputs) + if run.state === completed + scan = _parse_scan(run, model, formulation, execution) + validate(scan, run) + return _line_parameters( + run, model, formulation, execution, scan, inputs; reused = true) + end + end + attempt = _live_attempt(run) + attempt === nothing || _fem_error(:execution, "GetDP", :ownership, + "cannot resume while a previous GetDP process is still alive (PID $(attempt.pid), attempt $(attempt.directory))"; + run_directory = run.path) + _compute_owned_fem( + problem, formulation, execution, model, run, runtime_root, inputs) + finally + _release_run(ownership) + end + if !execution.data.keep_run_directory + expected_parent = realpath(joinpath(runtime_root, "runs")) + realpath(dirname(run.path)) == expected_parent || error( + "refusing to remove FEM run outside the runtime root") + rm(run.path; recursive = true, force = true) + end + return parameters +end + +function _compute_owned_fem( + problem, formulation, execution, model, run, runtime_root, inputs) + _write_json_atomic(joinpath(run.path, "input", "computation.json"), inputs) + session = nothing + try + session = _start_gmsh(execution.data.gmsh_verbosity) + parameters = execution.data.ui ? + _ui_solve!(run, model, formulation, execution, runtime_root, inputs) : + _headless_solve!( + run, model, formulation, execution, runtime_root, inputs) + return parameters + catch exception + LineCableModels.verbosity(execution, :progress) > 0 && + @info "FEM computation failed" _group=:progress run_directory=run.path exception + if run.state ∉ (not_executed, canceled) + _transition!(run, failed, sprint(showerror, exception)) + end + if exception isa LineCableModelsFEMError + throw(_attach_run_directory(exception, run)) + end + _fem_error( + :execution, + problem.system.system_id, + :backend, + sprint(showerror, exception); + run_directory = run.path + ) + finally + session === nothing || _finish_gmsh(session) + end +end + +const FEM_ADAPTER_SOURCES = let files = ("model.jl", "geometry.jl", "mesh.jl", + "onelab.jl", "getdp.jl", "voltage_paths.jl", "workers.jl", "results.jl", "compute.jl") + digests = map(files) do file + path = joinpath(@__DIR__, file) + Base.include_dependency(path) + bytes2hex(sha256(read(path))) + end + NamedTuple{Symbol.(files)}(digests) +end + +function _fem_input_record(model::FEMResolvedModel, formulation::LineCableModelsFEM, + execution::ComputationOptions) + selection = _getdp_selection(execution) + getdp_identity = _getdp_identity(selection.path) + getdp_selection = selection + mesh_path = execution.data.mesh_path + return ( + schema_version = 7, + solver_protocol = 3, + mesh_fingerprint = _mesh_fingerprint(model, gmsh.GMSH_API_VERSION), + materials = [(kind = material.kind, tag = material.physical_tag, + mu_r = material.mu_r, sigma = real.(material.admittivity), + omega_epsilon = imag.(material.admittivity)) + for material in model.material_plans], + earth_materials = [(rho = state.rho, eps_r = state.eps_r, mu_r = state.mu_r) + for state in model.earth_materials], + air = (eps_r = model.problem.earth_props.layers[1].eps_r, + mu_r = model.problem.earth_props.layers[1].mu_r), + mesh_plans = model.mesh_plans, + region_mesh_sizes = getproperty.(model.region_plans, :mesh_size), + cable_outer_mesh_sizes = model.cable_outer_mesh_sizes, + mesh_growth_factor = model.mesh_growth_factor, + options = formulation.options.data, + execution = (; + (key => value + for (key, value) in pairs(execution.data) + if key ∉ (:verbosity, :output_basis, :trace, :on_result, :log_file, :timing, + :resume_run_directory))...), + supplied_mesh = mesh_path === nothing || !isfile(mesh_path) ? nothing : + bytes2hex(open(sha256, mesh_path)), + owned_gmsh = !Bool(gmsh.is_initialized()), + getdp_identity, + getdp_selection, + gmsh_version = gmsh.GMSH_API_VERSION, + gmsh_library = String(gmsh.lib), + julia_version = string(VERSION), + adapter_sources = FEM_ADAPTER_SOURCES, + solver_sources = map(source -> bytes2hex(sha256(source)), FEM_GETDP_SOURCES) + ) +end + +function compute( + problem::LineParametersProblem, + formulation::Union{LineCableModelsFEM, AbstractVector{<:LineCableModelsFEM}}; + options::Union{NamedTuple, ComputationOptions} = ComputationOptions(), + modal=nothing, modal_options::Union{NamedTuple, ComputationOptions}=ComputationOptions() +) + options = options isa NamedTuple ? ComputationOptions(options) : options + modal===nothing || return compute(problem,formulation, + LineCableModels.ModalAnalysisFormulation(modal);options,modal_options) + isempty(modal_options isa NamedTuple ? modal_options : modal_options.data) || + throw(ArgumentError("modal_options require a modal formulation")) + return lock(FEM_SESSION_LOCK) do + _compute_request(problem, formulation; options) + end +end + +function _compute_request(problem, formulation; options) + problem = _preflight_fem_problem(problem) + execution = computation_options(LineCableModelsFEM, options) + # The same rules for completion notification and reuse apply to scalar + # and collection calls. + formulations = formulation isa LineCableModelsFEM ? [formulation] : formulation + logger = LineCableModels.VerbosityLogger(Logging.current_logger(), execution.data.verbosity) + values = if execution.data.log_file === nothing + with_logger(logger) do + _compute_fem(problem, formulations, execution) + end + else + mkpath(dirname(abspath(execution.data.log_file))) + open(execution.data.log_file, "a") do io + file_logger = SimpleLogger(io, Logging.Debug) + with_logger(FEMTeeLogger(logger, file_logger)) do + _compute_fem(problem, formulations, execution) + end + end + end + return formulation isa LineCableModelsFEM ? first(values) : values +end + +function _compute_fem( + problem::LineParametersProblem{Float64}, + formulations::AbstractVector{<:LineCableModelsFEM}, + execution::ComputationOptions +) + isempty(formulations) && throw(ArgumentError( + "FEM formulation collections cannot be empty")) + progress = LineCableModels.verbosity(execution, :progress) > 0 + started = progress ? time_ns() : UInt64(0) + last_log = started + previous_completion = started + average_seconds = 0.0 + progress && @info "FEM computation started" _group=:progress total=length(formulations) + physical_inputs=Engine.completed_inputs(problem) + source_id=Commons.gridpoint_id().source_id + # Resolve and validate all requests before opening Gmsh or starting GetDP. + models = [_resolved_fem_model(problem, formulation, execution) + for formulation in formulations] + # The problem and loaded solver source are common to this batch. Only actual + # material and mesh inputs and execution settings distinguish its computations. + keys = [JSON3.write(_fem_input_record(model, formulation, execution)) + for (model, formulation) in zip(models, formulations)] + function completion_fields(formulation) + completion = Engine.completed_formulation(formulation) + return merge(completion, + ( + formulations = merge(formulation_record(formulation), completion.formulations), + inputs = physical_inputs + )) + end + # Caller wall time covers native execution and completed result construction. + # Native worker durations remain separate, never Julia allocation estimates. + scan_started = execution.data.timing ? time_ns() : UInt64(0) + first_result = Engine.retain_gridpoint( + _compute_fem(problem, first(formulations), execution, first(models)), + Commons.gridpoint_id(; source_id); + fields = completion_fields(first(formulations))) + if execution.data.timing && !isempty(first_result.details.data.timing) + wall_seconds = (time_ns() - scan_started) * 1e-9 + first_result = Engine.retain_gridpoint(first_result, first_result.details.data.gridpoint; + fields=(timing=merge((; wall_seconds), first_result.details.data.timing),)) + end + values = Vector{typeof(first_result)}(undef, length(formulations)) + values[1] = first_result + execution.data.on_result === nothing || + execution.data.on_result(problem, 1, first_result) + if progress + now = time_ns() + average_seconds = (now - previous_completion) * 1e-9 + previous_completion = now + if now - last_log >= 5_000_000_000 + @info "FEM progress" _group=:progress completed=1 total=length(formulations) elapsed_seconds=(now-started)*1e-9 eta_hours=(length(formulations)-1)*average_seconds/3600 + last_log = now + end + end + completed = Dict(first(keys) => 1) + for index in 2:length(formulations) + formulation = formulations[index] + previous = get(completed, keys[index], nothing) + scan_started = execution.data.timing ? time_ns() : UInt64(0) + value = if previous === nothing || execution.data.ui || + execution.data.mesh_mode === :remesh + _compute_fem(problem, formulation, execution, models[index]) + else + source = values[previous] + @debug "FEM reuses identical resolved inputs" formulation=index source_formulation=previous + # Results remain independently mutable and each request keeps its own + # selection record. The shared run record identifies the actual solve. + retained = deepcopy(source.details.data) + retained = merge(retained, (formulations = formulation_record(formulation), + fem=merge(retained.fem, (run=merge(retained.fem.run, (reused=true,)),)))) + execution.data.timing && (retained=merge(retained, (timing=(;),))) + metadata = Engine.completion_details(retained) + LineParameters(PhaseDomain, + SeriesImpedance(copy(source.Z.values); basis = Engine.basis(source)), + ShuntAdmittance(copy(source.Y.values); basis = Engine.basis(source)), + copy(source.f), metadata) + end + value=Engine.retain_gridpoint(value, + Commons.gridpoint_id(; source_id, formulation_index = index); + fields = completion_fields(formulations[index])) + if execution.data.timing && !isempty(value.details.data.timing) + wall_seconds = (time_ns() - scan_started) * 1e-9 + value = Engine.retain_gridpoint(value, value.details.data.gridpoint; + fields=(timing=merge((; wall_seconds), value.details.data.timing),)) + end + typeof(value) === eltype(values) || throw(ArgumentError( + "FEM formulations produced inconsistent result types")) + values[index] = value + completed[keys[index]] = index + execution.data.on_result === nothing || + execution.data.on_result(problem, index, value) + if progress + now = time_ns() + interval = (now - previous_completion) * 1e-9 + average_seconds = 0.2 * interval + 0.8 * average_seconds + previous_completion = now + if now - last_log >= 5_000_000_000 + @info "FEM progress" _group=:progress completed=index total=length(formulations) elapsed_seconds=(now-started)*1e-9 eta_hours=(length(formulations)-index)*average_seconds/3600 + last_log = now + end + end + end + progress && @info "FEM computation completed successfully" _group=:progress completed=length(values) total=length(formulations) elapsed_seconds=(time_ns()-started)*1e-9 + return values +end diff --git a/ext/LineCableModelsGmshExt/formulations.jl b/ext/LineCableModelsGmshExt/formulations.jl new file mode 100644 index 000000000..891fa4da2 --- /dev/null +++ b/ext/LineCableModelsGmshExt/formulations.jl @@ -0,0 +1,20 @@ +""" +Record the consumed FEM material laws and selected field assumptions. +""" +function formulation_record(formulation::LineCableModelsFEM) + return merge(( + schema_version = 5, + assumptions = ( + impedance = "Axial current-driven A_z/u_r finite-element equations", + admittance = _quasi_full(formulation.options.data.physics) ? + "Coupled first-order Maxwell A_z/A_t/phi equations; axial current supplies normalized leakage; vertical path voltage includes A_t/Gamma; Y = inv(P)" : + "Scalar electrodynamic Helmholtz equation in surrounding media; equipotential terminals with unit transverse-current excitation; Y = inv(P)", + earth = "Horizontal air and one semi-infinite soil; soil constitutive properties evaluated at each frequency", + propagation = _quasi_full(formulation.options.data.physics) ? + "Gamma -> 0 with A_t/Gamma and phi/Gamma retained; conduction and displacement; one coupled factorization" : + "Gamma = 0; medium diffusion and displacement retained; independent Z/P blocks in one factorization", + semicon_domain = "Passive material region, without electrical terminal ownership", + enclosure = "Supported enclosures are represented by their material and terminal domains" + ) + ),NamedTuple(formulation)) +end diff --git a/ext/LineCableModelsGmshExt/geometry.jl b/ext/LineCableModelsGmshExt/geometry.jl new file mode 100644 index 000000000..02da253c7 --- /dev/null +++ b/ext/LineCableModelsGmshExt/geometry.jl @@ -0,0 +1,1609 @@ +struct FEMLoop + ccw::Int + cw::Int + curves::Vector{Int} + oriented::Vector{Int} +end + +const FEM_BOUNDARY_ANGLE_TOLERANCE = 1.0e-12 + +mutable struct FEMLoopRegistry + points::Dict{Tuple{Float64, Float64}, Int} + point_buckets::Dict{Tuple{Int, Int}, Vector{Tuple{Float64, Float64}}} + point_sizes::Dict{Int, Float64} + lines::Dict{Tuple{Int, Int}, Int} + curve_points::Dict{Int, Tuple{Int, Int}} + curve_samples::Dict{Int, Vector{Tuple{Float64, Float64}}} + circle_arcs::Dict{Any, Tuple{Int, Int, Int}} + circle_breaks::Dict{Any, Set{Float64}} + circle_break_points::Dict{Any, Dict{Float64, Tuple{Float64, Float64}}} + loops::Dict{Any, FEMLoop} + mesh_size::Float64 +end + +function FEMLoopRegistry(mesh_size) + FEMLoopRegistry( + Dict{Tuple{Float64, Float64}, Int}(), + Dict{Tuple{Int, Int}, Vector{Tuple{Float64, Float64}}}(), + Dict{Int, Float64}(), + Dict{Tuple{Int, Int}, Int}(), + Dict{Int, Tuple{Int, Int}}(), + Dict{Int, Vector{Tuple{Float64, Float64}}}(), + Dict{Any, Tuple{Int, Int, Int}}(), + Dict{Any, Set{Float64}}(), + Dict{Any, Dict{Float64, Tuple{Float64, Float64}}}(), + Dict{Any, FEMLoop}(), + Float64(mesh_size) + ) +end + +_coordinate_key(value) = round(Float64(value); sigdigits = 15) + +function _matching_point_key(registry::FEMLoopRegistry, point) + points = registry.points + key = (_coordinate_key(point[1]), _coordinate_key(point[2])) + haskey(points, key) && return key + scale = max(abs(Float64(point[1])), abs(Float64(point[2])), 1.0) + tolerance = 64eps(scale) + for x in floor(Int, (key[1] - tolerance) / registry.mesh_size):floor(Int, (key[1] + tolerance) / registry.mesh_size), + y in floor(Int, (key[2] - tolerance) / registry.mesh_size):floor(Int, (key[2] + tolerance) / registry.mesh_size) + for stored_point in get(registry.point_buckets, (x, y), ()) + abs(stored_point[1] - key[1]) <= tolerance && + abs(stored_point[2] - key[2]) <= tolerance && return stored_point + end + end + return key +end + +function _circle_key(center, radius) + return ( + _coordinate_key(center[1]), + _coordinate_key(center[2]), + _coordinate_key(radius) + ) +end + +function _angle_key(angle) + value = mod(Float64(angle), 2π) + isapprox(value, 2π; rtol = 0, atol = 128eps(Float64)) && (value = 0.0) + return value +end + +function _matching_circle_break(breaks, angle) + for existing in breaks + difference = abs(existing - angle) + min(difference, 2π - difference) <= FEM_BOUNDARY_ANGLE_TOLERANCE && + return existing + end + return nothing +end + +function _register_circle_break!( + registry::FEMLoopRegistry, + center, + radius, + angle; + point = nothing +) + key = _circle_key(center, radius) + breaks = get!(registry.circle_breaks, key) do + Set{Float64}() + end + value = _angle_key(angle) + stored = something(_matching_circle_break(breaks, value), value) + push!(breaks, stored) + if point !== nothing + points = get!(registry.circle_break_points, key) do + Dict{Float64, Tuple{Float64, Float64}}() + end + get!(points, stored) do + (Float64(point[1]), Float64(point[2])) + end + end + return stored +end + +function _register_full_circle_breaks!(registry::FEMLoopRegistry, center, radius) + foreach( + angle -> _register_circle_break!(registry, center, radius, angle), + (0.0, π / 2, π, 3π / 2) + ) + return nothing +end + +function _tangent_circular_fill_plan(shape) + shape isa DataModel.DifferenceShape || return nothing + outer = shape.outer + outer isa DataModel.Annulus || return nothing + isempty(shape.holes) && return nothing + iszero(outer.ri) && return nothing + + center = (Float64(outer.at.x), Float64(outer.at.y)) + disks = filter(hole -> hole isa DataModel.Disk, shape.holes) + cutouts = filter(hole -> !(hole isa DataModel.Disk), shape.holes) + isempty(disks) && return nothing + all(hole -> hole isa DataModel.BentStrip, cutouts) || return nothing + entries = Tuple{Float64, Any}[] + outer_contacts = Float64[] + for hole in disks + dx = Float64(hole.at.x) - center[1] + dy = Float64(hole.at.y) - center[2] + distance = hypot(dx, dy) + radius = Float64(hole.r) + scale = max(Float64(outer.ro), distance, radius, 1.0) + tolerance = 1.0e-12 * scale + distance > tolerance || return nothing + isapprox( + distance - radius, + Float64(outer.ri); + rtol = 0, + atol = tolerance + ) || return nothing + push!(outer_contacts, distance + radius) + push!(entries, (_angle_key(atan(dy, dx)), hole)) + end + computed_outer_radius = first(outer_contacts) + tolerance = 1.0e-12 * max( + Float64(outer.ro), computed_outer_radius, 1.0 + ) + all( + radius -> isapprox( + radius, computed_outer_radius; rtol = 0, atol = tolerance + ), outer_contacts) || return nothing + computed_outer_radius <= Float64(outer.ro) + tolerance || return nothing + + # Prefer the radius declared by the enclosing material boundary. The + # contact radius obtained from distance + wire radius can differ by an ULP. + # Using it as a separate circle would make coincident material interfaces + # overlap instead of sharing the same Gmsh curves. + tangent_outer_radius = if !isempty(cutouts) + Float64(first(cutouts).ri) + elseif isapprox( + computed_outer_radius, Float64(outer.ro); rtol = 0, atol = tolerance + ) + Float64(outer.ro) + else + computed_outer_radius + end + isapprox( + tangent_outer_radius, computed_outer_radius; + rtol = 0, + atol = tolerance + ) || return nothing + + for cutout in cutouts + isapprox(Float64(cutout.at.x), center[1]; rtol = 0, atol = tolerance) || + return nothing + isapprox(Float64(cutout.at.y), center[2]; rtol = 0, atol = tolerance) || + return nothing + isapprox( + Float64(cutout.ri), tangent_outer_radius; + rtol = 0, + atol = tolerance + ) || return nothing + isapprox( + Float64(cutout.ro), Float64(outer.ro); + rtol = 0, + atol = tolerance + ) || return nothing + FEM_BOUNDARY_ANGLE_TOLERANCE < Float64(cutout.span) < + 2π - FEM_BOUNDARY_ANGLE_TOLERANCE || return nothing + end + has_outer_shell = Float64(outer.ro) - tangent_outer_radius > tolerance + has_outer_shell || isempty(cutouts) || return nothing + outer_spans = has_outer_shell ? _complementary_sector_spans(cutouts) : + Tuple{Float64, Float64}[] + outer_spans === nothing && return nothing + + sort!(entries; by = first) + if length(entries) > 1 + angles = first.(entries) + separations = [mod(angles[mod1(index + 1, end)] - angles[index], 2π) + for index in eachindex(angles)] + minimum(separations) > FEM_BOUNDARY_ANGLE_TOLERANCE || return nothing + end + return ( + center = center, + inner_radius = Float64(outer.ri), + outer_radius = tangent_outer_radius, + shell_outer_radius = Float64(outer.ro), + angles = first.(entries), + holes = last.(entries), + outer_spans + ) +end + +function _complementary_sector_spans(cutouts) + isempty(cutouts) && return Tuple{Float64, Float64}[(0.0, 2π)] + occupied = Tuple{Float64, Float64}[] + for cutout in cutouts + start = _angle_key(Float64(cutout.at.φ - cutout.span / 2)) + stop = start + Float64(cutout.span) + if stop <= 2π + FEM_BOUNDARY_ANGLE_TOLERANCE + push!(occupied, (start, min(stop, 2π))) + else + push!(occupied, (start, 2π)) + push!(occupied, (0.0, stop - 2π)) + end + end + sort!(occupied; by = first) + merged = Tuple{Float64, Float64}[] + for interval in occupied + if isempty(merged) || interval[1] > last(merged)[2] + + FEM_BOUNDARY_ANGLE_TOLERANCE + push!(merged, interval) + continue + end + interval[1] < last(merged)[2] - FEM_BOUNDARY_ANGLE_TOLERANCE && + return nothing + merged[end] = (last(merged)[1], max(last(merged)[2], interval[2])) + end + spans = Tuple{Float64, Float64}[] + cursor = 0.0 + for interval in merged + interval[1] - cursor > FEM_BOUNDARY_ANGLE_TOLERANCE && + push!(spans, (cursor, interval[1] - cursor)) + cursor = max(cursor, interval[2]) + end + 2π - cursor > FEM_BOUNDARY_ANGLE_TOLERANCE && + push!(spans, (cursor, 2π - cursor)) + return spans +end + +function _radial_point(center, radius, angle) + return ( + center[1] + radius * cos(angle), + center[2] + radius * sin(angle) + ) +end + +function _annular_sector_surface!( + registry::FEMLoopRegistry, + center, + inner_radius, + outer_radius, + start_angle, + span, + mesh_size +) + if isapprox(span, 2π; rtol = 0, atol = FEM_BOUNDARY_ANGLE_TOLERANCE) + outer = _circle_loop!( + registry, center, outer_radius; mesh_size + ) + inner = _circle_loop!( + registry, center, inner_radius; mesh_size + ) + return gmsh.model.geo.add_plane_surface([outer.ccw, inner.cw]) + end + + outer_start_point = _radial_point(center, outer_radius, start_angle) + outer_stop_point = _radial_point( + center, outer_radius, start_angle + span + ) + inner_start_point = _radial_point(center, inner_radius, start_angle) + inner_stop_point = _radial_point( + center, inner_radius, start_angle + span + ) + outer_start = _point!(registry, outer_start_point; mesh_size) + outer_stop = _point!(registry, outer_stop_point; mesh_size) + inner_start = _point!(registry, inner_start_point; mesh_size) + inner_stop = _point!(registry, inner_stop_point; mesh_size) + curves = _circle_arc_path!( + registry, + center, + outer_radius, + start_angle, + span; + mesh_size, + first_point = outer_start_point, + last_point = outer_stop_point + ) + push!(curves, _line!(registry, outer_stop, inner_stop)) + append!(curves, + _circle_arc_path!( + registry, + center, + inner_radius, + start_angle + span, + -span; + mesh_size, + first_point = inner_stop_point, + last_point = inner_start_point + )) + push!(curves, _line!(registry, inner_start, outer_start)) + loop = gmsh.model.geo.add_curve_loop(curves) + return gmsh.model.geo.add_plane_surface([loop]) +end + +function _register_tangent_fill_contacts!(registry::FEMLoopRegistry, shape) + plan = _tangent_circular_fill_plan(shape) + plan === nothing && return nothing + for (angle, hole) in zip(plan.angles, plan.holes) + outer_point = _radial_point(plan.center, plan.outer_radius, angle) + inner_point = _radial_point(plan.center, plan.inner_radius, angle) + hole_center = (Float64(hole.at.x), Float64(hole.at.y)) + hole_radius = Float64(hole.r) + _register_circle_break!( + registry, + plan.center, + plan.outer_radius, + angle; + point = outer_point + ) + _register_circle_break!( + registry, + plan.center, + plan.inner_radius, + angle; + point = inner_point + ) + _register_circle_break!( + registry, + hole_center, + hole_radius, + angle; + point = outer_point + ) + _register_circle_break!( + registry, + hole_center, + hole_radius, + angle + π; + point = inner_point + ) + _register_circle_break!( + registry, + hole_center, + hole_radius, + angle + π / 2 + ) + _register_circle_break!( + registry, + hole_center, + hole_radius, + angle - π / 2 + ) + end + return plan +end + +_register_shape_breaks!(::FEMLoopRegistry, shape) = nothing + +function _register_shape_breaks!(registry::FEMLoopRegistry, shape::DataModel.Polygon) + # Register topology first. The consuming material supplies the mesh size. + foreach(point -> _point!(registry, point; mesh_size = nothing), _shape_points(shape)) + return nothing +end + +function _register_shape_breaks!(registry::FEMLoopRegistry, shape::DataModel.Disk) + _register_full_circle_breaks!(registry, (shape.at.x, shape.at.y), shape.r) + return nothing +end + +function _register_shape_breaks!(registry::FEMLoopRegistry, shape::DataModel.Annulus) + center = (shape.at.x, shape.at.y) + _register_full_circle_breaks!(registry, center, shape.ri) + _register_full_circle_breaks!(registry, center, shape.ro) + return nothing +end + +function _register_shape_breaks!(registry::FEMLoopRegistry, shape::DataModel.BentStrip) + center = (shape.at.x, shape.at.y) + start_angle = shape.at.φ - shape.span / 2 + stop_angle = shape.at.φ + shape.span / 2 + _register_circle_break!(registry, center, shape.ro, start_angle) + _register_circle_break!(registry, center, shape.ro, stop_angle) + if !iszero(shape.ri) + _register_circle_break!(registry, center, shape.ri, start_angle) + _register_circle_break!(registry, center, shape.ri, stop_angle) + end + return nothing +end + +function _register_shape_breaks!( + registry::FEMLoopRegistry, + shape::DataModel.SectorShape +) + for arc in ( + shape.contacts.arcs.base, + shape.contacts.arcs.lower, + shape.contacts.arcs.back, + shape.contacts.arcs.upper + ) + iszero(arc.radius) && continue + center = shape.at(arc.center) + _register_circle_break!(registry, center, arc.radius, shape.at.φ + arc.start) + _register_circle_break!(registry, center, arc.radius, shape.at.φ + arc.stop) + end + return nothing +end + +function _register_shape_breaks!(registry::FEMLoopRegistry, shape::DataModel.ShellShape) + _register_shape_breaks!(registry, shape.inner) + _register_shape_breaks!(registry, shape.outer) + return nothing +end + +function _register_shape_breaks!( + registry::FEMLoopRegistry, + shape::DataModel.DifferenceShape +) + _register_shape_breaks!(registry, shape.outer) + foreach(hole -> _register_shape_breaks!(registry, hole), shape.holes) + _register_tangent_fill_contacts!(registry, shape) + return nothing +end + +function _register_shape_breaks!( + registry::FEMLoopRegistry, + shape::DataModel.AssemblyShape +) + foreach(member -> _register_shape_breaks!(registry, member), shape.members) + return nothing +end + +function _register_circle_contacts!(registry::FEMLoopRegistry) + circles = Tuple{Float64, Float64, Float64}[key for key in keys(registry.circle_breaks)] + # A polygonal strand can end on an analytic enclosing arc. Both consumers + # must share that vertex before either curve loop is constructed. + for circle in circles, point in keys(registry.points) + dx, dy = point[1] - circle[1], point[2] - circle[2] + abs(hypot(dx, dy) - circle[3]) <= 64eps(max(circle[3], 1.0)) || continue + _register_circle_break!(registry, circle[1:2], circle[3], atan(dy, dx); point) + end + for first_index in eachindex(circles) + first = circles[first_index] + first_center = (first[1], first[2]) + first_radius = first[3] + for second_index in (first_index + 1):length(circles) + second = circles[second_index] + second_center = (second[1], second[2]) + second_radius = second[3] + dx = second_center[1] - first_center[1] + dy = second_center[2] - first_center[2] + distance = hypot(dx, dy) + scale = max(first_radius, second_radius, distance, 1.0) + tolerance = 1e-12 * scale + distance <= tolerance && continue + distance > first_radius + second_radius + tolerance && continue + distance < abs(first_radius - second_radius) - tolerance && continue + + along = ( + first_radius^2 - second_radius^2 + distance^2 + ) / (2distance) + height_squared = first_radius^2 - along^2 + height_squared < -tolerance * scale && continue + tangent = isapprox( + distance, + first_radius + second_radius; + rtol = 0, + atol = tolerance + ) || isapprox( + distance, + abs(first_radius - second_radius); + rtol = 0, + atol = tolerance + ) + height = tangent ? 0.0 : sqrt(max(height_squared, 0.0)) + unit_x = dx / distance + unit_y = dy / distance + base_x = first_center[1] + along * unit_x + base_y = first_center[2] + along * unit_y + contacts = height <= tolerance ? + ((base_x, base_y),) : + ( + (base_x - height * unit_y, base_y + height * unit_x), + (base_x + height * unit_y, base_y - height * unit_x) + ) + for contact in contacts + _register_circle_break!( + registry, + first_center, + first_radius, + atan( + contact[2] - first_center[2], + contact[1] - first_center[1] + ); + point = contact + ) + _register_circle_break!( + registry, + second_center, + second_radius, + atan( + contact[2] - second_center[2], + contact[1] - second_center[1] + ); + point = contact + ) + end + end + end + return nothing +end + +function _circle_point(center, radius, angle) + cosine = cos(angle) + sine = sin(angle) + isapprox(cosine, 0; rtol = 0, atol = 128eps(Float64)) && (cosine = 0.0) + isapprox(sine, 0; rtol = 0, atol = 128eps(Float64)) && (sine = 0.0) + return ( + center[1] + radius * cosine, + center[2] + radius * sine + ) +end + +function _point!(registry::FEMLoopRegistry, point; mesh_size = registry.mesh_size) + # Equivalent geometric boundary constructions can differ by one or two Float64 ULPs. + # Reuse the existing topological point without applying a physical-scale + # tolerance to every coordinate in the model. + key = _matching_point_key(registry, point) + tag = get!(registry.points, key) do + bucket = (floor(Int, key[1] / registry.mesh_size), + floor(Int, key[2] / registry.mesh_size)) + push!(get!(Vector{Tuple{Float64, Float64}}, registry.point_buckets, bucket), key) + gmsh.model.geo.add_point( + key[1], key[2], 0.0, mesh_size === nothing ? 0.0 : Float64(mesh_size) + ) + end + if mesh_size !== nothing + registry.point_sizes[tag] = min( + get(registry.point_sizes, tag, Inf), Float64(mesh_size) + ) + end + return tag +end + +function _line!(registry::FEMLoopRegistry, first_point::Int, last_point::Int) + key = minmax(first_point, last_point) + tag = get!(registry.lines, key) do + gmsh.model.geo.add_line(key[1], key[2]) + end + registry.curve_points[tag] = key + return first_point == key[1] ? tag : -tag +end + +function _line_path!(registry::FEMLoopRegistry, first, last; + mesh_size = registry.mesh_size) + first_point = _point!(registry, first; mesh_size) + last_point = _point!(registry, last; mesh_size) + dx, dy = last[1] - first[1], last[2] - first[2] + distance = hypot(dx, dy) + distance > 0 || return Int[] + tolerance = 64eps(max(abs(first[1]), abs(first[2]), abs(last[1]), abs(last[2]), 1.0)) + points = Tuple{Float64, Int}[(0.0, first_point), (1.0, last_point)] + # Visit only buckets intersecting this edge's bounding box. Scanning every + # vertex for every strand edge is quadratic for large bounded formations. + x_bins = range(floor(Int, (min(first[1], last[1]) - tolerance) / registry.mesh_size), + floor(Int, (max(first[1], last[1]) + tolerance) / registry.mesh_size)) + y_bins = range(floor(Int, (min(first[2], last[2]) - tolerance) / registry.mesh_size), + floor(Int, (max(first[2], last[2]) + tolerance) / registry.mesh_size)) + bins = length(x_bins) <= length(registry.point_buckets) ÷ length(y_bins) ? + Iterators.product(x_bins, y_bins) : keys(registry.point_buckets) + for (x, y) in bins + x in x_bins && y in y_bins || continue + for point in get(registry.point_buckets, (x, y), ()) + tag = registry.points[point] + tag in (first_point, last_point) && continue + px, py = point[1] - first[1], point[2] - first[2] + position = (px * dx + py * dy) / distance^2 + tolerance / distance < position < 1 - tolerance / distance || continue + abs(px * dy - py * dx) <= tolerance * distance || continue + push!(points, (position, tag)) + end + end + sort!(points) + return [_line!(registry, points[index][2], points[index + 1][2]) + for index in 1:(length(points) - 1)] +end + +function _refine_loop_mesh_size!( + registry::FEMLoopRegistry, loop::FEMLoop, mesh_size +) + value = Float64(mesh_size) + for curve in loop.curves + for point in registry.curve_points[abs(curve)] + registry.point_sizes[point] = min( + get(registry.point_sizes, point, Inf), value + ) + end + end + return loop +end + +function _apply_point_mesh_sizes!(registry::FEMLoopRegistry) + for (point, mesh_size) in registry.point_sizes + gmsh.model.geo.mesh.set_size([(0, point)], mesh_size) + end + return nothing +end + +function _signed_area(points) + origin = first(points) + return sum(eachindex(points)) do index + next = mod1(index + 1, length(points)) + (points[index][1] - origin[1]) * (points[next][2] - origin[2]) - + (points[next][1] - origin[1]) * (points[index][2] - origin[2]) + end / 2 +end + +""" +Remove an approximately repeated closing vertex, orient the polygon +counterclockwise and start at the least coordinate key. Coordinates use Float64. +The closing-point distance tolerance is 1e-14 m. +""" +function _normalize_polygon_vertices(points) + values = [(Float64(point[1]), Float64(point[2])) for point in points] + if length(values) > 1 + first_point = first(values) + last_point = last(values) + hypot(first_point[1] - last_point[1], first_point[2] - last_point[2]) <= + 1e-14 && pop!(values) + end + length(values) >= 3 || throw(ArgumentError("a FEM boundary requires three points")) + _signed_area(values) < 0 && reverse!(values) + keys = [(_coordinate_key(point[1]), _coordinate_key(point[2])) for point in values] + first_index = argmin(keys) + values = [values[mod1(first_index + offset, length(values))] + for offset in 0:(length(values) - 1)] + return values +end + +function _polygon_loop!(registry::FEMLoopRegistry, points; mesh_size = registry.mesh_size) + values = _normalize_polygon_vertices(points) + key = (:polygon, + Tuple( + (_coordinate_key(point[1]), _coordinate_key(point[2])) for point in values + )) + loop = get!(registry.loops, key) do + curves = reduce(vcat, + [_line_path!(registry, values[index], values[mod1(index + 1, end)]; mesh_size) + for index in eachindex(values)]) + ccw = gmsh.model.geo.add_curve_loop(curves) + cw = gmsh.model.geo.add_curve_loop(-reverse(curves)) + FEMLoop(ccw, cw, abs.(curves), curves) + end + return _refine_loop_mesh_size!(registry, loop, mesh_size) +end + +function _circle_loop!( + registry::FEMLoopRegistry, + center, + radius; + mesh_size = registry.mesh_size +) + key = (:circle, _circle_key(center, radius)) + loop = get!(registry.loops, key) do + _register_full_circle_breaks!(registry, center, radius) + first_point = (center[1] + radius, center[2]) + curves = _circle_arc_path!( + registry, + center, + radius, + 0.0, + 2π; + mesh_size, + first_point, + last_point = first_point + ) + ccw = gmsh.model.geo.add_curve_loop(curves) + cw = gmsh.model.geo.add_curve_loop(-reverse(curves)) + FEMLoop(ccw, cw, abs.(curves), curves) + end + return _refine_loop_mesh_size!(registry, loop, mesh_size) +end + +function _ellipse_loop!( + registry::FEMLoopRegistry, + shape::DataModel.Ellipse; + mesh_size = registry.mesh_size +) + key = ( + :ellipse, + _coordinate_key(shape.at.x), + _coordinate_key(shape.at.y), + _coordinate_key(shape.at.φ), + _coordinate_key(shape.a), + _coordinate_key(shape.b) + ) + loop = get!(registry.loops, key) do + center = (shape.at.x, shape.at.y) + center_tag = _point!(registry, center; mesh_size) + local_points = ( + (shape.a, 0.0), + (0.0, shape.b), + (-shape.a, 0.0), + (0.0, -shape.b) + ) + point_tags = [_point!(registry, shape.at(point); mesh_size) + for point in local_points] + major_point = shape.a >= shape.b ? local_points[1] : local_points[2] + major_tag = _point!( + registry, shape.at(major_point); mesh_size + ) + curves = Int[] + for index in 1:4 + next_index = mod1(index + 1, 4) + curve = gmsh.model.geo.add_ellipse_arc( + point_tags[index], center_tag, major_tag, point_tags[next_index] + ) + registry.curve_points[curve] = ( + point_tags[index], point_tags[next_index] + ) + push!(curves, curve) + end + ccw = gmsh.model.geo.add_curve_loop(curves) + cw = gmsh.model.geo.add_curve_loop(-reverse(curves)) + for (index, curve) in enumerate(curves) + registry.curve_samples[curve] = [ + shape.at((shape.a * cos(angle), shape.b * sin(angle))) + for angle in range((index - 1) * π / 2, index * π / 2; length = 17) + ] + end + FEMLoop(ccw, cw, curves, curves) + end + return _refine_loop_mesh_size!(registry, loop, mesh_size) +end + +function _circle_break_point(registry::FEMLoopRegistry, circle_key, angle) + points = get(registry.circle_break_points, circle_key, nothing) + points === nothing && return nothing + stored = _matching_circle_break(keys(points), _angle_key(angle)) + stored === nothing && return nothing + return points[stored] +end + +function _circle_arc_path!( + registry::FEMLoopRegistry, + center, + radius, + start_angle, + span; + mesh_size = registry.mesh_size, + first_point = nothing, + last_point = nothing +) + value_span = Float64(span) + iszero(value_span) && return Int[] + direction = sign(value_span) + distance = abs(value_span) + distance <= 2π + 128eps(Float64) || throw(ArgumentError( + "a FEM circle-arc path cannot span more than one revolution" + )) + # Every consumer of a circular interface must use the same subdivision. + # Span-relative subdivisions create overlapping curves when another + # material traverses the same interface over a different angular range. + _register_full_circle_breaks!(registry, center, radius) + _register_circle_break!(registry, center, radius, start_angle) + _register_circle_break!(registry, center, radius, start_angle + value_span) + circle_key = _circle_key(center, radius) + tolerance = FEM_BOUNDARY_ANGLE_TOLERANCE + distances = Float64[0.0, distance] + for angle in registry.circle_breaks[circle_key] + offset = mod(direction * (angle - Float64(start_angle)), 2π) + if tolerance < offset < distance - tolerance && + all(existing -> abs(existing - offset) > tolerance, distances) + push!(distances, offset) + end + end + sort!(distances) + angles = Float64(start_angle) .+ direction .* distances + points = [something( + _circle_break_point(registry, circle_key, angle), + _circle_point(center, radius, angle) + ) for angle in angles] + first_point === nothing || (points[1] = first_point) + last_point === nothing || (points[end] = last_point) + center_tag = _point!(registry, center; mesh_size) + point_tags = [_point!(registry, point; mesh_size) for point in points] + return [begin + first_tag = point_tags[index] + last_tag = point_tags[index + 1] + key = (:circle_arc, circle_key, minmax(first_tag, last_tag)) + tag, stored_first, + stored_last = get!(registry.circle_arcs, key) do + ( + gmsh.model.geo.add_circle_arc( + first_tag, center_tag, last_tag + ), + first_tag, + last_tag + ) + end + registry.curve_points[tag] = (stored_first, stored_last) + get!(registry.curve_samples, tag) do + a = first_tag == stored_first ? angles[index] : angles[index + 1] + b = first_tag == stored_first ? angles[index + 1] : angles[index] + [_circle_point(center, radius, a + fraction * (b - a)) + for fraction in (0.0, 1e-6, ((1:15) ./ 16)..., 1 - 1e-6, 1.0)] + end + first_tag == stored_first && last_tag == stored_last ? tag : -tag + end + for index in 1:(length(point_tags) - 1)] +end + +function _sector_loop!( + registry::FEMLoopRegistry, + shape::DataModel.SectorShape; + mesh_size = registry.mesh_size +) + contacts = shape.contacts + points = contacts.points + transformed = map( + shape.at, + ( + points.base_upper, + points.base_lower, + points.side_lower, + points.back_lower, + points.back_upper, + points.side_upper + ) + ) + key = (:sector, + Tuple( + (_coordinate_key(point[1]), _coordinate_key(point[2])) + for point in transformed + )) + loop = get!(registry.loops, key) do + point_tags = [_point!(registry, point; mesh_size) for point in transformed] + arcs = contacts.arcs + arc_specs = ( + (arcs.base, 1, 2), + (arcs.lower, 3, 4), + (arcs.back, 4, 5), + (arcs.upper, 5, 6) + ) + arc_curves = Dict{Symbol, Vector{Int}}() + for (name, (arc, first_index, last_index)) in zip( + (:base, :lower, :back, :upper), arc_specs + ) + if iszero(arc.radius) + arc_curves[name] = point_tags[first_index] == point_tags[last_index] ? + Int[] : + Int[_line!( + registry, point_tags[first_index], point_tags[last_index] + )] + continue + end + center = shape.at(arc.center) + arc_curves[name] = _circle_arc_path!( + registry, + center, + arc.radius, + shape.at.φ + arc.start, + arc.stop - arc.start; + mesh_size, + first_point = transformed[first_index], + last_point = transformed[last_index] + ) + end + curves = Int[] + append!(curves, arc_curves[:base]) + append!(curves, _line_path!(registry, transformed[2], transformed[3]; mesh_size)) + append!(curves, arc_curves[:lower]) + append!(curves, arc_curves[:back]) + append!(curves, arc_curves[:upper]) + append!(curves, _line_path!(registry, transformed[6], transformed[1]; mesh_size)) + ccw = gmsh.model.geo.add_curve_loop(curves) + cw = gmsh.model.geo.add_curve_loop(-reverse(curves)) + FEMLoop(ccw, cw, abs.(curves), curves) + end + return _refine_loop_mesh_size!(registry, loop, mesh_size) +end + +function _bent_strip_loop!( + registry::FEMLoopRegistry, + shape::DataModel.BentStrip; + mesh_size = registry.mesh_size +) + center = (shape.at.x, shape.at.y) + if iszero(shape.ri) && isapprox( + shape.span, 2π; rtol = 0, atol = FEM_BOUNDARY_ANGLE_TOLERANCE + ) + return _circle_loop!(registry, center, shape.ro; mesh_size) + end + start_angle = shape.at.φ - shape.span / 2 + stop_angle = shape.at.φ + shape.span / 2 + key = ( + :bent_strip, + _coordinate_key(center[1]), + _coordinate_key(center[2]), + _coordinate_key(start_angle), + _coordinate_key(shape.span), + _coordinate_key(shape.ri), + _coordinate_key(shape.ro) + ) + loop = get!(registry.loops, key) do + outer_start_point = _circle_point(center, shape.ro, start_angle) + outer_stop_point = _circle_point(center, shape.ro, stop_angle) + outer_start = _point!(registry, outer_start_point; mesh_size) + outer_stop = _point!(registry, outer_stop_point; mesh_size) + curves = _circle_arc_path!( + registry, + center, + shape.ro, + start_angle, + shape.span; + mesh_size, + first_point = outer_start_point, + last_point = outer_stop_point + ) + if iszero(shape.ri) + center_point = _point!(registry, center; mesh_size) + push!(curves, _line!(registry, outer_stop, center_point)) + push!(curves, _line!(registry, center_point, outer_start)) + else + inner_start_point = _circle_point(center, shape.ri, start_angle) + inner_stop_point = _circle_point(center, shape.ri, stop_angle) + inner_start = _point!(registry, inner_start_point; mesh_size) + inner_stop = _point!(registry, inner_stop_point; mesh_size) + push!(curves, _line!(registry, outer_stop, inner_stop)) + append!(curves, + _circle_arc_path!( + registry, + center, + shape.ri, + stop_angle, + -shape.span; + mesh_size, + first_point = inner_stop_point, + last_point = inner_start_point + )) + push!(curves, _line!(registry, inner_start, outer_start)) + end + ccw = gmsh.model.geo.add_curve_loop(curves) + cw = gmsh.model.geo.add_curve_loop(-reverse(curves)) + FEMLoop(ccw, cw, abs.(curves), curves) + end + return _refine_loop_mesh_size!(registry, loop, mesh_size) +end + +function _shape_points(shape::DataModel.Rectangle) + half_width = shape.w / 2 + half_height = shape.h / 2 + return [shape.at(point) + for point in ( + (-half_width, -half_height), + (half_width, -half_height), + (half_width, half_height), + (-half_width, half_height) + )] +end + +function _shape_points(shape::DataModel.Polygon) + return [shape.at(point) for point in shape.points] +end + +function _shape_points(shape::DataModel.SectorShape) + return DataModel.tessellate(shape; points_per_arc = 24) +end + +function _shape_points(shape::DataModel.EllipseOffset) + return DataModel.tessellate(shape; points_per_arc = 96) +end + +function _shape_points(shape::DataModel.BentStrip) + return DataModel.tessellate(shape; points_per_arc = 24) +end + +function _shape_points(shape) + _fem_error( + :unsupported, + string(typeof(shape)), + :primitive, + "the resolved shape has no built-in geo-kernel boundary adaptation" + ) +end + +function _boundary_loop!( + registry::FEMLoopRegistry, + shape; + mesh_size = registry.mesh_size +) + if shape isa DataModel.Disk + return _circle_loop!( + registry, + (shape.at.x, shape.at.y), + shape.r; + mesh_size + ) + end + shape isa DataModel.Ellipse && return _ellipse_loop!( + registry, shape; mesh_size + ) + shape isa DataModel.SectorShape && return _sector_loop!( + registry, shape; mesh_size + ) + shape isa DataModel.BentStrip && return _bent_strip_loop!( + registry, shape; mesh_size + ) + return _polygon_loop!(registry, _shape_points(shape); mesh_size) +end + +function _surface_boundaries(shape::DataModel.Annulus) + outer = DataModel.Disk(shape.ro, shape.at) + inner = DataModel.Disk(shape.ri, shape.at) + return outer, Any[inner] +end + +_surface_boundaries(shape::DataModel.ShellShape) = (shape.outer, Any[shape.inner]) + +function _surface_boundaries(shape::DataModel.DifferenceShape) + outer, outer_holes = _surface_boundaries(shape.outer) + return outer, Any[outer_holes..., shape.holes...] +end + +_surface_boundaries(shape) = (shape, Any[]) + +function _surface_faces!(registry::FEMLoopRegistry, shape, mesh_size) + outer, holes = _surface_boundaries(shape) + outer_loop = _boundary_loop!(registry, outer; mesh_size) + hole_loops = [_boundary_loop!(registry, hole; mesh_size) + for hole in holes] + isempty(holes) && return Int[gmsh.model.geo.add_plane_surface([outer_loop.ccw])] + seen = Set{Int}() + touching = false + for loop in Iterators.flatten(((outer_loop,), hole_loops)) + points = Set(point for curve in loop.curves for point in registry.curve_points[curve]) + touching |= any(point -> point in seen, points) + union!(seen, points) + end + if !touching + return Int[gmsh.model.geo.add_plane_surface([outer_loop.ccw; getproperty.(hole_loops, :cw)])] + end + return _material_faces!(registry, + (outer_loop.oriented, (-loop.oriented for loop in hole_loops)...)) +end + +function _material_faces!(registry::FEMLoopRegistry, boundaries) + # Every oriented edge has material on its left. Cancel shared hole edges: + # a metal-metal seam cannot also bound a third (filler) material. + counts = Dict{Int, Int}() + for curves in boundaries + for curve in curves + counts[abs(curve)] = get(counts, abs(curve), 0) + sign(curve) + end + end + all(value -> abs(value) <= 1, Base.values(counts)) || throw(ArgumentError( + "overlapping boundaries in a FEM material face")) + edges = sort!([tag * count for (tag, count) in counts if !iszero(count)]; by=abs) + positions = Dict(tag => point for (point, tag) in registry.points) + samples = Dict{Int, Vector{Tuple{Float64, Float64}}}() + outgoing = Dict{Int, Vector{Int}}() + endpoints = Dict{Int, Tuple{Int, Int}}() + for edge in edges + first_point, last_point = registry.curve_points[abs(edge)] + values = copy(get(registry.curve_samples, abs(edge), + [positions[first_point], positions[last_point]])) + values[1], values[end] = positions[first_point], positions[last_point] + if edge < 0 + first_point, last_point = last_point, first_point + reverse!(values) + end + endpoints[edge] = (first_point, last_point) + samples[edge] = values + push!(get!(Vector{Int}, outgoing, first_point), edge) + end + # Compare incident curves at a common physical distance from the contact. + # Equal parameter fractions on tangent circles of different radii can give + # identical directions and the wrong face pairing. + steps = Dict{Int, Float64}() + for (first_point, last_point) in values(endpoints) + a, b = positions[first_point], positions[last_point] + step = hypot(b[1]-a[1], b[2]-a[2]) / 64 + for point in (first_point, last_point) + steps[point] = min(get(steps, point, Inf), step) + end + end + circles = Dict{Int, Tuple{Float64, Float64, Float64, Int}}() + for (key, (tag, first_point, last_point)) in registry.circle_arcs + cx, cy, radius = key[2] + a, b = positions[first_point], positions[last_point] + orientation = sign((a[1]-cx)*(b[2]-cy)-(a[2]-cy)*(b[1]-cx)) + circles[tag] = (cx, cy, radius, Int(orientation)) + end + function direction(edge, step) + if haskey(circles, abs(edge)) + cx, cy, radius, orientation = circles[abs(edge)] + first_point, last_point = registry.curve_points[abs(edge)] + point = positions[edge > 0 ? first_point : last_point] + turn = sign(edge) * orientation + return atan(turn*(point[1]-cx), -turn*(point[2]-cy)) + turn*step/(2radius) + end + points = haskey(samples, edge) ? samples[edge] : reverse(samples[-edge]) + return atan(points[2][2]-points[1][2], points[2][1]-points[1][1]) + end + successors = Dict{Int, Int}() + for edge in edges + step = steps[endpoints[edge][2]] + backwards = direction(-edge, step) + outgoing_edges = get(outgoing, endpoints[edge][2], Int[]) + isempty(outgoing_edges) && throw(ArgumentError("open FEM material boundary")) + successors[edge] = outgoing_edges[argmin(map(outgoing_edges) do edge_index + mod(backwards - direction(edge_index, step), 2π) + end)] + end + length(unique(Base.values(successors))) == length(edges) || throw(ArgumentError( + "ambiguous FEM material boundary at a contact")) + + cycles = Vector{Int}[] + polygons = Vector{Tuple{Float64, Float64}}[] + remaining = Set(edges) + for first_edge in edges + first_edge in remaining || continue + cycle = Int[] + polygon = Tuple{Float64, Float64}[] + edge = first_edge + while edge in remaining + delete!(remaining, edge) + push!(cycle, edge) + append!(polygon, samples[edge][1:end-1]) + edge = successors[edge] + end + edge == first_edge || throw(ArgumentError("nonclosing FEM material face")) + push!(cycles, cycle) + push!(polygons, polygon) + end + areas = _signed_area.(polygons) + outers = findall(>(0), areas) + isempty(outers) && throw(ArgumentError("FEM material has no positive-area face")) + interiors = Dict(index => Int[] for index in outers) + function contains(polygon, point) + inside = false + for index in eachindex(polygon) + a, b = polygon[index], polygon[mod1(index + 1, end)] + (a[2] > point[2]) == (b[2] > point[2]) && continue + point[1] < a[1] + (point[2] - a[2]) * (b[1] - a[1]) / (b[2] - a[2]) && + (inside = !inside) + end + return inside + end + for index in findall(<(0), areas) + parents = filter(parent -> contains(polygons[parent], first(polygons[index])), outers) + isempty(parents) && throw(ArgumentError("uncontained FEM material hole")) + parent = parents[argmin(areas[parents])] + push!(interiors[parent], index) + end + loops = [gmsh.model.geo.add_curve_loop(cycle) for cycle in cycles] + return [gmsh.model.geo.add_plane_surface([loops[index]; loops[interiors[index]]]) + for index in outers] +end + +function _tangent_fill_surfaces!(registry::FEMLoopRegistry, shape, mesh_size) + plan = _tangent_circular_fill_plan(shape) + plan === nothing && return nothing + surfaces = Int[] + count = length(plan.holes) + for index in eachindex(plan.holes) + next_index = mod1(index + 1, count) + angle = plan.angles[index] + next_angle = plan.angles[next_index] + span = mod(next_angle - angle, 2π) + span <= FEM_BOUNDARY_ANGLE_TOLERANCE && (span = 2π) + hole = plan.holes[index] + next_hole = plan.holes[next_index] + hole_center = (Float64(hole.at.x), Float64(hole.at.y)) + next_hole_center = ( + Float64(next_hole.at.x), Float64(next_hole.at.y) + ) + outer_point = _radial_point(plan.center, plan.outer_radius, angle) + next_outer_point = _radial_point( + plan.center, plan.outer_radius, next_angle + ) + inner_point = _radial_point(plan.center, plan.inner_radius, angle) + next_inner_point = _radial_point( + plan.center, plan.inner_radius, next_angle + ) + curves = Int[] + append!(curves, + _circle_arc_path!( + registry, + plan.center, + plan.outer_radius, + angle, + span; + mesh_size, + first_point = outer_point, + last_point = next_outer_point + )) + append!(curves, + _circle_arc_path!( + registry, + next_hole_center, + Float64(next_hole.r), + next_angle, + -π; + mesh_size, + first_point = next_outer_point, + last_point = next_inner_point + )) + append!(curves, + _circle_arc_path!( + registry, + plan.center, + plan.inner_radius, + next_angle, + -span; + mesh_size, + first_point = next_inner_point, + last_point = inner_point + )) + append!(curves, + _circle_arc_path!( + registry, + hole_center, + Float64(hole.r), + angle + π, + -π; + mesh_size, + first_point = inner_point, + last_point = outer_point + )) + # Neighboring wires may also touch each other. The annular sector is + # then a pinched walk, not one face: trace each material-left component + # with the same contact rule used by every other enclosure boundary. + append!(surfaces, _material_faces!(registry, (curves,))) + end + for (start_angle, span) in plan.outer_spans + push!(surfaces, + _annular_sector_surface!( + registry, + plan.center, + plan.outer_radius, + plan.shell_outer_radius, + start_angle, + span, + mesh_size + )) + end + return surfaces +end + +function _surfaces!(registry::FEMLoopRegistry, shape, mesh_size) + compartments = _tangent_fill_surfaces!(registry, shape, mesh_size) + compartments === nothing || return compartments + return _surface_faces!(registry, shape, mesh_size) +end + +_boundary_components(shape::DataModel.AssemblyShape) = collect(shape.members) +_boundary_components(shape) = Any[shape] + +function _physical_group(dim::Int, entities, tag::Int, name::String) + values = sort!(unique(Int.(entities))) + isempty(values) && return nothing + gmsh.model.add_physical_group(dim, values, tag) + gmsh.model.set_physical_name(dim, tag, name) + return tag +end + +function _entity_boundary(surfaces) + isempty(surfaces) && return Int[] + dimtags = [(2, surface) for surface in surfaces] + return sort!(unique(Int(tag) + for (dim, tag) in gmsh.model.get_boundary(dimtags, true, false, false) if dim == 1)) +end + +# Material surfaces of the FEM model: disjoint, and each interface between exactly two. +function validate(material_surfaces::AbstractVector, model::FEMResolvedModel) + surfaces = collect(Iterators.flatten(material_surfaces)) + length(unique(surfaces)) == length(surfaces) || _fem_error( + :geometry, + model.problem.system.system_id, + :material_partition, + "a FEM surface is assigned to more than one material" + ) + curves = Int[] + for surfaces in material_surfaces + append!(curves, _entity_boundary(surfaces)) + end + offending = Int[] + for curve in sort!(unique(curves)) + adjacent, _ = gmsh.model.get_adjacencies(1, curve) + length(unique(adjacent)) == 2 || push!(offending, curve) + end + isempty(offending) || _fem_error( + :geometry, + model.problem.system.system_id, + :material_partition, + "material interfaces must have exactly two adjacent surfaces; " * + "invalid curves: $(join(offending, ", "))" + ) + return material_surfaces +end + +function _interface_mesh_sizes(model::FEMResolvedModel, mesh_plan::FEMMeshPlan) + center_x = model.center[1] + sizes = Dict{Float64, Float64}() + function register(x, size) + key = _coordinate_key(x) + sizes[key] = min(get(sizes, key, Inf), Float64(size)) + end + register(center_x, mesh_plan.interface_mesh_size) + for offset in (-2.0, 2.0) + register(center_x + offset, mesh_plan.domain_mesh_size) + end + for (index, position) in enumerate(model.problem.system.positions) + register(position.x, mesh_plan.cable_interface_mesh_sizes[index]) + end + return sizes +end + +function _build_geometry!( + model::FEMResolvedModel, + model_name::String, + mesh_plan::FEMMeshPlan = last(model.mesh_plans); + reuse::Union{Nothing, FEMGeometry} = nothing +) + if reuse === nothing + gmsh.model.add(model_name) + registry = FEMLoopRegistry(model.fine_mesh_size) + for region in model.region_plans + _register_shape_breaks!(registry, region.shape) + end + foreach( + boundary -> _register_shape_breaks!(registry, boundary), + model.cable_boundaries + ) + _register_circle_contacts!(registry) + material_surfaces = [Int[] for _ in model.material_plans] + terminal_surfaces = [Int[] for _ in model.terminal_ids] + for region in model.region_plans + surfaces = _surfaces!(registry, region.shape, region.mesh_size) + append!(material_surfaces[region.material_index], surfaces) + region.terminal_index > 0 && append!( + terminal_surfaces[region.terminal_index], surfaces + ) + end + cable_curves = [Int[] for _ in model.cable_boundaries] + cable_loops = [Int[] for _ in model.cable_boundaries] + for cable_index in eachindex(model.cable_boundaries) + for component in _boundary_components(model.cable_boundaries[cable_index]) + loop = _boundary_loop!(registry, component; + mesh_size = model.cable_outer_mesh_sizes[cable_index]) + push!(cable_loops[cable_index], loop.cw) + append!(cable_curves[cable_index], loop.curves) + end + end + _apply_point_mesh_sizes!(registry) + else + gmsh.model.set_current(model_name) + material_surfaces = reuse.material_surfaces + terminal_surfaces = reuse.terminal_surfaces + cable_curves = reuse.cable_curves + cable_loops = reuse.cable_loops + end + + # Keep exterior bookkeeping separate so frequency changes never recreate + # or transform the authoritative cable geometry. + registry = FEMLoopRegistry(model.fine_mesh_size) + center_x, _ = model.center + radius = mesh_plan.domain_radius + shell_radius = mesh_plan.shell_outer_radius + inner = _circle_loop!( + registry, + (center_x, 0.0), + radius; + mesh_size = mesh_plan.domain_mesh_size + ) + outer = _circle_loop!( + registry, + (center_x, 0.0), + shell_radius; + mesh_size = mesh_plan.infinite_mesh_size + ) + inner_left = _point!( + registry, + (center_x - radius, 0.0); + mesh_size = mesh_plan.domain_mesh_size + ) + inner_right = _point!( + registry, + (center_x + radius, 0.0); + mesh_size = mesh_plan.domain_mesh_size + ) + outer_left = _point!( + registry, + (center_x - shell_radius, 0.0); + mesh_size = mesh_plan.infinite_mesh_size + ) + outer_right = _point!( + registry, + (center_x + shell_radius, 0.0); + mesh_size = mesh_plan.infinite_mesh_size + ) + interface_sizes = _interface_mesh_sizes(model, mesh_plan) + interface_points = Int[inner_left] + for (x, mesh_size) in sort!(collect(interface_sizes); by = first) + center_x - radius < x < center_x + radius || continue + push!(interface_points, _point!(registry, (x, 0.0); mesh_size)) + end + push!(interface_points, inner_right) + finite_interfaces = [_line!(registry, interface_points[index], interface_points[index + 1]) + for index in 1:(length(interface_points) - 1)] + left_connector = _line!(registry, outer_left, inner_left) + right_connector = _line!(registry, inner_right, outer_right) + air_holes = Int[] + earth_holes = Int[] + for cable_index in eachindex(cable_loops) + target = model.cable_hosts[cable_index] === :air ? air_holes : earth_holes + append!(target, cable_loops[cable_index]) + end + air_loop = gmsh.model.geo.add_curve_loop([finite_interfaces; inner.curves[1]; + inner.curves[2]]) + earth_loop = gmsh.model.geo.add_curve_loop([-reverse(finite_interfaces); + inner.curves[3]; inner.curves[4]]) + air_surface = gmsh.model.geo.add_plane_surface([air_loop; air_holes]) + earth_surface = gmsh.model.geo.add_plane_surface([earth_loop; earth_holes]) + + air_infinite_loop = gmsh.model.geo.add_curve_loop([ + right_connector, + outer.curves[1], + outer.curves[2], + left_connector, + -inner.curves[2], + -inner.curves[1] + ]) + earth_infinite_loop = gmsh.model.geo.add_curve_loop([ + outer.curves[3], + outer.curves[4], + -right_connector, + -inner.curves[4], + -inner.curves[3], + -left_connector + ]) + air_infinite_surface = gmsh.model.geo.add_plane_surface([air_infinite_loop]) + earth_infinite_surface = gmsh.model.geo.add_plane_surface([ + earth_infinite_loop + ]) + + _apply_point_mesh_sizes!(registry) + gmsh.model.geo.synchronize() + + validate(material_surfaces, model) + + for (index, material) in enumerate(model.material_plans) + _physical_group( + 2, + material_surfaces[index], + material.physical_tag, + material.physical_name + ) + end + terminal_curves = [_entity_boundary(surfaces) for surfaces in terminal_surfaces] + for index in eachindex(terminal_surfaces) + _physical_group( + 2, + terminal_surfaces[index], + model.tags.terminal_base + index, + model.terminal_names[index] + ) + _physical_group( + 1, + terminal_curves[index], + model.tags.terminal_contour_base + index, + @sprintf("LCM/terminal_contour/%04d", index) + ) + end + for index in eachindex(cable_curves) + _physical_group( + 1, + cable_curves[index], + model.tags.cable_contour_base + index, + @sprintf("LCM/cable_contour/%04d/%s", index, + model.problem.system.designs[index].cable_id) + ) + end + _physical_group(2, [air_surface], model.tags.air, "LCM/domain/air") + _physical_group(2, [earth_surface], model.tags.earth, "LCM/domain/earth") + _physical_group( + 2, + [air_infinite_surface], + model.tags.air_infinite, + "LCM/domain/air_infinite" + ) + _physical_group( + 2, + [earth_infinite_surface], + model.tags.earth_infinite, + "LCM/domain/earth_infinite" + ) + _physical_group( + 2, + [air_infinite_surface, earth_infinite_surface], + model.tags.infinite_domain, + "LCM/domain/infinite_shell" + ) + + interface_curves = sort!(unique(abs.([finite_interfaces; left_connector; + right_connector]))) + outer_curves = sort!(unique(outer.curves)) + outer_air_curves = intersect( + outer_curves, + _entity_boundary([air_infinite_surface]) + ) + outer_earth_curves = intersect( + outer_curves, + _entity_boundary([earth_infinite_surface]) + ) + isempty(intersect(outer_air_curves, outer_earth_curves)) || _fem_error( + :geometry, + model.problem.system.system_id, + :outer_electric_boundary, + "air and earth outer electric boundaries overlap" + ) + sort!(unique([outer_air_curves; outer_earth_curves])) == outer_curves || + _fem_error( + :geometry, + model.problem.system.system_id, + :outer_electric_boundary, + "air and earth outer electric boundaries do not partition the outer shell" + ) + inner_shell_curves = sort!(unique(inner.curves)) + _physical_group( + 1, + outer_curves, + model.tags.outer_boundary, + "LCM/boundary/magnetic_dirichlet" + ) + _physical_group( + 1, + outer_air_curves, + model.tags.outer_air_boundary, + "LCM/boundary/electric_insulation_air" + ) + _physical_group( + 1, + outer_earth_curves, + model.tags.outer_earth_boundary, + "LCM/boundary/electric_reference_earth" + ) + _physical_group( + 1, + inner_shell_curves, + model.tags.inner_shell_boundary, + "LCM/boundary/inner_infinite_shell" + ) + _physical_group(1, interface_curves, model.tags.interface, "LCM/interface/air_earth") + conductor_surfaces = reduce(vcat, + [material_surfaces[index] + for (index, material) in enumerate(model.material_plans) + if material.kind === :conductor]; + init = Int[]) + passive_surfaces = reduce(vcat, + [material_surfaces[index] + for (index, material) in enumerate(model.material_plans) + if material.kind !== :conductor]; + init = Int[]) + _physical_group(2, conductor_surfaces, 6_001, "LCM/domain/conductors") + _physical_group(2, passive_surfaces, 6_002, "LCM/domain/passive_cable_media") + _physical_group( + 2, + [conductor_surfaces; + passive_surfaces; + air_surface; + earth_surface; + air_infinite_surface; + earth_infinite_surface], + 6_003, + "LCM/domain/field_maps" + ) + + return FEMGeometry( + model_name, + terminal_surfaces, + terminal_curves, + material_surfaces, + cable_curves, + [air_surface, air_infinite_surface], + [earth_surface, earth_infinite_surface], + [air_infinite_surface, earth_infinite_surface], + outer_curves, + outer_air_curves, + outer_earth_curves, + inner_shell_curves, + interface_curves, + cable_loops, + sort!(collect(values(registry.points))) + ) +end diff --git a/ext/LineCableModelsGmshExt/getdp.jl b/ext/LineCableModelsGmshExt/getdp.jl new file mode 100644 index 000000000..2b14abf7f --- /dev/null +++ b/ext/LineCableModelsGmshExt/getdp.jl @@ -0,0 +1,269 @@ +const FEM_FIELD_QUANTITIES = ( + "az", "b", "bm", "e", "ez", "em", "jz", "jm", "rhoj2" +) + +_quasi_full(physics::Symbol) = physics === Symbol("quasi-fw") +function _fem_physics_code(formulation::LineCableModelsFEM) + Int(_quasi_full(formulation.options.data.physics)) +end +function _field_quantities(physics::Symbol) + _quasi_full(physics) ? + (FEM_FIELD_QUANTITIES..., "bt_mesh", "v_local", "hz_scaled") : FEM_FIELD_QUANTITIES +end + +function _pro_string(value::AbstractString) + escaped = replace(String(value), '\\' => "\\\\", '"' => "\\\"") + return "\"$escaped\"" +end + +function _pro_number(value::Real) + return @sprintf("%.17g", Float64(value)) +end + +_pro_array(values) = "{" * join(_pro_number.(values), ", ") * "}" + +function _write_problem_snapshot(path::String, problem::LineParametersProblem) + open(path, "w") do io + JSON3.pretty(io, ImportExport.serialize_value(problem)) + write(io, '\n') + end + return path +end + +function _write_model_data(path::String, model::FEMResolvedModel{T}) where {T} + ε0 = vacuum_permittivity(T) + μ0 = vacuum_permeability(T) + air = model.problem.earth_props.layers[1] + earth = model.earth_materials + materials = model.material_plans + open(path, "w") do io + println(io, "// Resolved LineCableModels FEM inputs; solver logic lives in getdp/*.pro") + println(io, "NumTerminals = ", length(model.terminal_ids), ";") + println(io, "NumCables = ", length(model.problem.system.designs), ";") + println(io, "NumMaterialRegions = ", length(materials), ";") + println(io, "FrequencyCount = ", length(model.problem.frequencies), ";") + println(io, "AIR_EM = ", model.tags.air, ";") + println(io, "EARTH_EM = ", model.tags.earth, ";") + println(io, "AIR_INF = ", model.tags.air_infinite, ";") + println(io, "EARTH_INF = ", model.tags.earth_infinite, ";") + println(io, "DOMAIN_INF = ", model.tags.infinite_domain, ";") + println(io, "OUTBND_EM = ", model.tags.outer_boundary, ";") + println(io, "OUTBND_ELE_INS = ", model.tags.outer_air_boundary, ";") + println(io, "OUTBND_ELE_REF = ", model.tags.outer_earth_boundary, ";") + println(io, "INTERFACE_AIR_SOIL = ", model.tags.interface, ";") + println(io, "INNER_INF_BND = ", model.tags.inner_shell_boundary, ";") + println(io, "TERMINAL = ", model.tags.terminal_base + 1, ";") + println(io, "TERMINAL_CONTOUR = ", model.tags.terminal_contour_base + 1, ";") + println(io, "CABLE_CONTOUR = ", model.tags.cable_contour_base + 1, ";") + println(io, "TerminalNames() = Str[", + join(_pro_string.(model.terminal_ids), ", "), "];") + println(io, "MaterialRegionTags() = ", + _pro_array(getproperty.(materials, :physical_tag)), ";") + println(io, "MaterialIsConductor() = ", + _pro_array([material.kind === :conductor for material in materials]), ";") + println(io, + "MaterialHasLoss() = ", + _pro_array([any(value -> !iszero(real(value)), material.admittivity) + for material in materials]), + ";") + println(io, "MaterialMu() = ", + _pro_array([material.mu_r * μ0 for material in materials]), ";") + for (index, material) in pairs(materials) + println(io, "MaterialSigma_", index, "() = ", + _pro_array(real.(material.admittivity)), ";") + println(io, "MaterialEpsilon_", index, "() = ", + _pro_array(imag.(material.admittivity) ./ + (2π .* model.problem.frequencies)), ";") + end + println(io, "EarthSigma() = ", _pro_array([inv(state.rho) for state in earth]), ";") + println( + io, "EarthEpsilon() = ", _pro_array([state.eps_r * ε0 for state in earth]), ";") + println(io, "EarthMu() = ", _pro_array([state.mu_r * μ0 for state in earth]), ";") + println(io, "AirEpsilon = ", _pro_number(air.eps_r * ε0), ";") + println(io, "AirMu = ", _pro_number(air.mu_r * μ0), ";") + println(io, "DomainRadius = ", _pro_number(model.domain_radius), ";") + println(io, "ShellOuterRadius = ", _pro_number(model.shell_outer_radius), ";") + println(io, "Xcenter = ", _pro_number(model.center[1]), ";") + println(io, "Ycenter = ", _pro_number(model.center[2]), ";") + println(io, "Zcenter = 0.0;") + end + return path +end + +function _getdp_assets(root::AbstractString = joinpath(@__DIR__, "getdp")) + return ( + model = joinpath(root, "model.pro"), + jacobian = joinpath(root, "jacobian.pro"), + integration = joinpath(root, "integration.pro"), + materials = joinpath(root, "materials.pro"), + quasi_tem = joinpath(root, "quasi-tem.pro"), + quasi_full = joinpath(root, "quasi-full.pro") + ) +end + +# Capture solver text with the loaded Julia implementation, not halfway through +# a long mesh and solve sequence. Include dependencies invalidate the Julia cache. +const FEM_GETDP_SOURCES = map(_getdp_assets()) do path + Base.include_dependency(path) + read(path, String) +end + +const GETDP_ARTIFACT_VERSION = v"3.5.0" +const GETDP_ENVIRONMENT_VARIABLE = "LINECABLEMODELS_GETDP" +const GETDP_ARTIFACTS_TOML = normpath(joinpath(@__DIR__, "..", "..", "Artifacts.toml")) + +function _artifact_getdp() + hash = artifact_hash("getdp", GETDP_ARTIFACTS_TOML) + hash === nothing && return nothing + root = ensure_artifact_installed("getdp", GETDP_ARTIFACTS_TOML) + version = string(GETDP_ARTIFACT_VERSION) + relative = if Sys.isapple() + joinpath("getdp-$version-MacOSX", "bin", "getdp") + else + joinpath("getdp-$version-Linux64", "bin", "getdp") + end + return ( + path = joinpath(root, relative), source = :artifact, artifact_hash = string(hash)) +end + +function _getdp_selection(execution::ComputationOptions; run_directory = nothing) + explicit = execution.data.getdp_executable + environment = get(ENV, GETDP_ENVIRONMENT_VARIABLE, nothing) + selection = if explicit !== nothing + (path = abspath(explicit), source = :explicit, artifact_hash = nothing) + elseif environment !== nothing + (path = abspath(environment), source = :environment, artifact_hash = nothing) + else + artifact = _artifact_getdp() + if artifact !== nothing + artifact + else + executable = Sys.which("getdp") + executable === nothing ? nothing : + (path = executable, source = :path, artifact_hash = nothing) + end + end + selection !== nothing && isfile(selection.path) || _fem_error( + :getdp, "GetDP", :getdp_executable, + "GetDP executable is unavailable for $(triplet(HostPlatform())); " * + "pass getdp_executable, set " * GETDP_ENVIRONMENT_VARIABLE * + ", or install getdp on PATH when no package artifact supports this platform"; + run_directory) + return merge(selection, (; path = realpath(selection.path))) +end + +function _getdp_identity(executable::String) + output = try + buffer = IOBuffer() + Base.run(pipeline( + Cmd([executable, "-info"]), stdout = buffer, stderr = buffer + )) + String(take!(buffer)) + catch exception + _fem_error( + :getdp, + "GetDP", + :getdp_executable, + "failed to execute $executable -info: $(sprint(showerror, exception))" + ) + end + occursin("getdp", lowercase(output)) && + occursin(r"\d+\.\d+", output) || _fem_error( + :getdp, + "GetDP", + :getdp_executable, + "executable identity check did not report GetDP: $executable" + ) + return (sha256 = bytes2hex(open(sha256, executable)), info = output) +end + +function _resolve_getdp(execution::ComputationOptions, run::FEMRun) + selection = _getdp_selection(execution; run_directory = run.path) + identity = _getdp_identity(selection.path) + recorded = JSON3.read(read(joinpath(run.path, "input", "computation.json"), String)) + recorded_identity = Dict(String(key)=>value + for (key, value) in pairs(recorded.getdp_identity)) + pop!(recorded_identity, "path", nothing) # schema 4 compatibility + expected_identity = Dict(String(key)=>value + for (key, value) in pairs(JSON3.read(JSON3.write(identity)))) + isequal(recorded_identity, expected_identity) || + _fem_error(:getdp, "GetDP", :getdp_executable, + "GetDP executable identity changed after writing the run inputs; start a new run"; + run_directory = run.path) + return selection.path +end + +function _job_raw_paths(root::AbstractString, job_name::String) + return (Z = joinpath(root, "raw", "jobs", "$job_name-Z.tsv"), + P = joinpath(root, "raw", "jobs", "$job_name-P.tsv")) +end + +_job_raw_paths(run::FEMRun, job_name::String) = _job_raw_paths(run.path, job_name) + +function _valid_job_raw( + path::String, + terminal_count::Int, + frequency_index::Int, + frequency::Real, + basis_terminal::Int +) + isfile(path) || return false + rows = readlines(path) + length(rows) == terminal_count || return false + responses = Set{Int}() + for row in rows + columns = split(row, '\t'; keepempty = true) + length(columns) == 6 || return false + parsed_frequency_index = tryparse(Int, columns[1]) + parsed_frequency = tryparse(Float64, columns[2]) + response = tryparse(Int, columns[3]) + parsed_basis = tryparse(Int, columns[4]) + real_part = tryparse(Float64, columns[5]) + imaginary_part = tryparse(Float64, columns[6]) + parsed_frequency_index == frequency_index || return false + response isa Int && response in 1:terminal_count || return false + parsed_basis == basis_terminal || return false + parsed_frequency === nothing && return false + isapprox( + parsed_frequency, + Float64(frequency); + rtol = 16eps(Float64), + atol = 0.0 + ) || return false + real_part === nothing && return false + imaginary_part === nothing && return false + isfinite(real_part) && isfinite(imaginary_part) || return false + response in responses && return false + push!(responses, response) + end + return length(responses) == terminal_count +end + +function _write_scan_completion!(run::FEMRun, model::FEMResolvedModel) + frequency_count = length(model.problem.frequencies) + terminal_count = length(model.terminal_ids) + expected_rows = frequency_count * terminal_count * terminal_count + path = joinpath(run.path, "raw", "scan_complete.tsv") + temporary = tempname(dirname(path)) + open(temporary, "w") do io + println(io, join(FEM_COMPLETE_HEADER, '\t')) + println(io, + join( + ( + frequency_count, + terminal_count, + expected_rows, + expected_rows, + expected_rows, + 1 + ), '\t')) + end + mv(temporary, path; force = true) + return path +end + +function _log_tail(records; count::Int = 20) + isempty(records) && return "(no GetDP log records)" + first_index = max(firstindex(records), lastindex(records) - count + 1) + return join(@view(records[first_index:lastindex(records)]), '\n') +end diff --git a/ext/LineCableModelsGmshExt/getdp/integration.pro b/ext/LineCableModelsGmshExt/getdp/integration.pro new file mode 100644 index 000000000..d4db112ee --- /dev/null +++ b/ext/LineCableModelsGmshExt/getdp/integration.pro @@ -0,0 +1,15 @@ +Integration { + { Name I1; + Case { + { Type Gauss; + Case { + { GeoElement Point; NumberOfPoints 1; } + { GeoElement Line; NumberOfPoints 4; } + { GeoElement Triangle; NumberOfPoints 4; } + { GeoElement Quadrangle; NumberOfPoints 4; } + { GeoElement Triangle2; NumberOfPoints 7; } + } + } + } + } +} diff --git a/ext/LineCableModelsGmshExt/getdp/jacobian.pro b/ext/LineCableModelsGmshExt/getdp/jacobian.pro new file mode 100644 index 000000000..1c66c5a98 --- /dev/null +++ b/ext/LineCableModelsGmshExt/getdp/jacobian.pro @@ -0,0 +1,14 @@ +Jacobian { + { Name Vol; + Case { + { Region DomainInf; + Jacobian VolSphShell{Val_Rint, Val_Rext, Xcenter, Ycenter, Zcenter}; } + { Region All; Jacobian Vol; } + } + } + { Name Sur; + Case { + { Region All; Jacobian Sur; } + } + } +} diff --git a/ext/LineCableModelsGmshExt/getdp/materials.pro b/ext/LineCableModelsGmshExt/getdp/materials.pro new file mode 100644 index 000000000..57bc8c55f --- /dev/null +++ b/ext/LineCableModelsGmshExt/getdp/materials.pro @@ -0,0 +1,34 @@ +// Bind resolved numerical inputs to their primary mesh material regions. +Group { + ConductorMaterialRegions = Region[{}]; + PassiveMaterialRegions = Region[{}]; + LossyMaterialRegions = Region[{}]; + For material In {1:NumMaterialRegions} + // Parentheses force a scalar tag: without them GetDP expands the list. + MaterialRegion~{material} = Region[{(MaterialRegionTags(material - 1))}]; + If(MaterialIsConductor(material - 1)) + ConductorMaterialRegions += Region[{MaterialRegion~{material}}]; + Else + PassiveMaterialRegions += Region[{MaterialRegion~{material}}]; + If(MaterialHasLoss(material - 1)) + LossyMaterialRegions += Region[{MaterialRegion~{material}}]; + EndIf + EndIf + EndFor + DomainCWithI = Region[{Terminals}]; + DomainC = Region[{ConductorMaterialRegions, Earth, EarthInf}]; + DomainCC = Region[{Air, AirInf, PassiveMaterialRegions}]; + DomainLoss = Region[{DomainC, LossyMaterialRegions}]; + Domain_Mag = Region[{DomainC, DomainCC}]; +} + +Function { + For material In {1:NumMaterialRegions} + nu[MaterialRegion~{material}] = 1. / MaterialMu(material - 1); + mu[MaterialRegion~{material}] = MaterialMu(material - 1); + // Sigma is effective conductivity: selected dielectric losses are already + // included in Re(kappa). Do not add a second tan(delta) contribution. + sigma[MaterialRegion~{material}] = MaterialSigma~{material}(FrequencyIndex - 1); + epsilon[MaterialRegion~{material}] = MaterialEpsilon~{material}(FrequencyIndex - 1); + EndFor +} diff --git a/ext/LineCableModelsGmshExt/getdp/model.pro b/ext/LineCableModelsGmshExt/getdp/model.pro new file mode 100644 index 000000000..cb5ae6364 --- /dev/null +++ b/ext/LineCableModelsGmshExt/getdp/model.pro @@ -0,0 +1,35 @@ +// One selector serves ONELAB and the headless CLI (-setnumber Physics 0|1). +DefineConstant[ + Physics = {0, Choices{0="quasi-tem", 1="quasi-fw"}, + Name "LineCableModels/FEM/physics", Label "Physics"} +]; +// Immutable inputs for one frequency and its requested terminal excitations. +If(!Exists(ModelDataPath)) + Error("Pass the immutable input path with -setstring ModelDataPath"); +EndIf +Include ModelDataPath; +UnitSource = 1.0; +UnitTransverseSource = 1.0; +If(!Exists(Val_Rint)) + Val_Rint = DomainRadius; +EndIf +If(!Exists(Val_Rext)) + Val_Rext = ShellOuterRadius; +EndIf +Group { + // Keep the two primary physical regions explicit here. DOMAIN_INF is an + // overlapping Gmsh inventory group. Direct selection would break + // GetDP's region dispatch for the VolSphShell Jacobian. + AirInfJacobian = Region[{AIR_INF}]; + EarthInfJacobian = Region[{EARTH_INF}]; + DomainInf = Region[{AirInfJacobian, EarthInfJacobian}]; +} +Include "jacobian.pro"; +Include "integration.pro"; +If(Physics == 0) + Include "quasi-tem.pro"; +ElseIf(Physics == 1) + Include "quasi-full.pro"; +Else + Error("Physics must be 0 (quasi-tem) or 1 (quasi-fw)"); +EndIf diff --git a/ext/LineCableModelsGmshExt/getdp/quasi-full.pro b/ext/LineCableModelsGmshExt/getdp/quasi-full.pro new file mode 100644 index 000000000..c00fd1c83 --- /dev/null +++ b/ext/LineCableModelsGmshExt/getdp/quasi-full.pro @@ -0,0 +1,588 @@ +// Included by model.pro when the ONELAB Physics selector is 1 (quasi-fw). +Jacobian { { Name Plain; Case { { Region All; Jacobian Vol; } } } } + + + +// First-order Maxwell formulation for exp(j omega t - Gamma z). +// A_z=a, A_t=Gamma*bt, phi=Gamma*v. The equations are the normalized +// Gamma -> 0 limit. All O(Gamma) transverse fields are retained. This is +// not a finite-Gamma modal solver. The finite-metal A_z/u branch is retained. +// PerfectConductors=1 instead uses exact PEC contour current constraints. +// +// One imposed axial current supplies both the magnetic equation and the +// normalized transverse leakage. There is no independent electric drive. +// The transverse domain treats metal interfaces as equipotential electrodes. +// the magnetic block either retains the specified finite metal conductivity +// or excludes metal interiors when PerfectConductors=1. +// +// Voltage/Gamma = v_i-v_ref + j omega integral(bt.dl). PathDataPath supplies +// mesh-coordinate integration points and oriented line weights from the +// reference at earth infinity to each electrode. Jacobian Plain evaluates +// the pulled-back 1-form: its circulation equals the physical circulation, +// including in the infinite-element shell. The backend generates these paths +// from the mesh. `test/manual/fem/run_quasi_full.jl` also provides a manual +// PEC CLI experiment. +// Raw P has units ohm m (inverse admittance). Analytical Pe = j omega P. +// +// Reference: G. Ciuprina and R. V. Sabriego, "Electric circuit element boundary +// conditions for electromagneto-quasistatic and full wave models in A, phi +// potentials and their finite element implementation", J. Math. Ind. 14, 27 +// (2024), https://doi.org/10.1186/s13362-024-00165-6, Sec. 4 and Appendix B. +// The paper gives the full-vector A/phi equations, terminal conditions and +// gauge framework. This 2D longitudinal expansion and the vertical voltage +// paths are our reduction, not the paper's 3D ECE model or its Darwin model. +// See the LineCableModelsFEM Julia docstring for equations, units and limits. + +// PerfectConductors=1 excludes metal interiors and imposes exact PEC +// current-carrying contours. Default 0 retains the working finite-metal a/u block. +If(!Exists(PerfectConductors)) + PerfectConductors = 0; +EndIf +If(!Exists(PathDataPath)) + Error("Pass voltage paths with -setstring PathDataPath"); +EndIf +Include PathDataPath; +If(#PathStart() != NumTerminals || #PathCount() != NumTerminals) + Error("Provide one oriented voltage path per terminal"); +EndIf +If(#PathX() != #PathY() || #PathX() != #PathDX() || #PathX() != #PathDY()) + Error("Voltage-path coordinates and line weights must have equal lengths"); +EndIf +For terminal In {0:NumTerminals-1} + If(PathCount(terminal) < 1 || PathStart(terminal) < 0 || + PathStart(terminal)+PathCount(terminal) > #PathX()) + Error("Invalid voltage-path point range"); + EndIf +EndFor +If(!Exists(RunDirectory)) + RunDirectory = ""; +EndIf +If(!Exists(FrequencyCount)) + FrequencyCount = 0; +EndIf +If(!Exists(FrequencyIndex)) + FrequencyIndex = 1; +EndIf +If(!Exists(FrequencyHz)) + FrequencyHz = 1.; +EndIf +If(!Exists(BasisTerminal)) + BasisTerminal = 1; +EndIf +If(Exists(BasisListPath)) + Include BasisListPath; +Else + RequestedBases() = {BasisTerminal}; +EndIf +If(!Exists(ReuseFactorization)) + ReuseFactorization = 1; +EndIf +If(!Exists(PlotFieldMaps)) + PlotFieldMaps = 0; +EndIf +If(!Exists(RawOutputStem)) + RawOutputStem = "scan"; +EndIf +RawDirectory = StrCat[RunDirectory, "/raw"]; +RawJobDirectory = StrCat[RawDirectory, "/jobs"]; +MapDirectory = StrCat[RunDirectory, "/maps"]; + +Group { + Air = Region[{AIR_EM}]; + Earth = Region[{EARTH_EM}]; + AirInf = Region[{AIR_INF}]; + EarthInf = Region[{EARTH_INF}]; + Terminals = Region[{}]; + TerminalContours = Region[{}]; + + For t In {1:NumTerminals} + Terminal~{t} = Region[{(TERMINAL + t - 1)}]; + TerminalContour~{t} = Region[{(TERMINAL_CONTOUR + t - 1)}]; + Terminals += Region[{Terminal~{t}}]; + TerminalContours += Region[{TerminalContour~{t}}]; + EndFor + Sur_Dirichlet_Mag = Region[{OUTBND_EM}]; + Sur_Insulation_Ele = Region[{OUTBND_ELE_INS}]; + Sur_Dirichlet_Ele = Region[{OUTBND_ELE_REF}]; + // Complete the gauge tree on EVERY geometric boundary with prescribed bt circulation. + // Omitting the electrode contours would constrain physical loop degrees of + // freedom as well as the potential gauge. + GaugeBoundary = Region[{Sur_Dirichlet_Mag, TerminalContours}]; +} + +Include "materials.pro"; +Group { + // Conductors are equipotential electrodes and are excluded from electric field media. + DomainMedia_Ele = Region[{Air, AirInf, Earth, EarthInf, PassiveMaterialRegions}]; + If(PerfectConductors) + DomainFields = Region[{DomainMedia_Ele}]; + DomainFieldLoss = Region[{Earth, EarthInf, LossyMaterialRegions}]; + Else + DomainFields = Region[{Domain_Mag}]; + DomainFieldLoss = Region[{DomainLoss}]; + EndIf +} + +Function { + nu[#{Air, AirInf}] = 1. / AirMu; + sigma[#{Air, AirInf}] = 0.; + epsilon[#{Air, AirInf}] = AirEpsilon; + mu[#{Air, AirInf}] = AirMu; + + nu[#{Earth, EarthInf}] = 1. / EarthMu(FrequencyIndex - 1); + sigma[#{Earth, EarthInf}] = EarthSigma(FrequencyIndex - 1); + epsilon[#{Earth, EarthInf}] = EarthEpsilon(FrequencyIndex - 1); + mu[#{Earth, EarthInf}] = EarthMu(FrequencyIndex - 1); + + omega[] = 2. * Pi * $FEMFrequencyHz; + se[] = Complex[sigma[], omega[] * epsilon[]]; +} + +Constraint { + { Name FEMMagneticVectorPotential; + Case { + { Region Sur_Dirichlet_Mag; Value 0.; } + } + } + { Name FEMTerminalCurrent; + Case { + For t In {1:NumTerminals} + { Region Terminal~{t}; Value $FEM_I~{t}; } + EndFor + } + } + { Name FEMTransverseBoundary; Type Assign; + Case { { Region Sur_Dirichlet_Mag; Value 0.; } + { Region TerminalContours; Value 0.; } } + } + { Name FEMTransverseGauge; Type Assign; + Case { { Region DomainMedia_Ele; SubRegion GaugeBoundary; Value 0.; } } + } + { Name FEMScalarPotential; Type Assign; + Case { + // The earth-side far boundary is the electric reference. The air-side + // outer shell is intentionally unconstrained: the Galerkin formulation + // supplies its natural zero-normal-current boundary condition. + { Region Sur_Dirichlet_Ele; Value 0.; } + } + } +} + +FunctionSpace { + If(PerfectConductors) + { Name Hcurl_a_FEM_2D; Type Form1P; + BasisFunction { + { Name se; NameOfCoef ae; Function BF_PerpendicularEdge; + Support Domain_Mag; Entity NodesOf[All, Not Terminals]; } + { Name sf; NameOfCoef af; Function BF_GroupOfPerpendicularEdges; + Support Domain_Mag; Entity GroupsOfNodesOf[Terminals]; } + } + GlobalQuantity { + { Name A; Type AliasOf; NameOfCoef af; } + { Name I; Type AssociatedWith; NameOfCoef af; } + } + Constraint { + { NameOfCoef ae; EntityType NodesOf; NameOfConstraint FEMMagneticVectorPotential; } + { NameOfCoef I; EntityType Auto; NameOfConstraint FEMTerminalCurrent; } + } + } + Else + { Name Hcurl_a_FEM_2D; Type Form1P; + BasisFunction { + { Name se; NameOfCoef ae; Function BF_PerpendicularEdge; + Support Domain_Mag; Entity NodesOf[All]; } + } + Constraint { + { NameOfCoef ae; EntityType NodesOf; + NameOfConstraint FEMMagneticVectorPotential; } + } + } + + { Name Hregion_u_FEM_2D; Type Form1P; + BasisFunction { + { Name sr; NameOfCoef ur; Function BF_GroupOfPerpendicularEdges; + Support ConductorMaterialRegions; + Entity GroupsOfNodesOf[Terminals]; } + } + GlobalQuantity { + { Name U; Type AliasOf; NameOfCoef ur; } + { Name I; Type AssociatedWith; NameOfCoef ur; } + } + Constraint { + { NameOfCoef I; EntityType Auto; NameOfConstraint FEMTerminalCurrent; } + } + } + + EndIf + { Name Hcurl_bt_FEM_2D; Type Form1; + BasisFunction { + { Name se; NameOfCoef be; Function BF_Edge; + Support DomainMedia_Ele; Entity EdgesOf[All]; } + } + Constraint { + { NameOfCoef be; EntityType EdgesOf; + NameOfConstraint FEMTransverseBoundary; } + { NameOfCoef be; EntityType EdgesOfTreeIn; EntitySubType StartingOn; + NameOfConstraint FEMTransverseGauge; } + } + } + { Name Hgrad_v_FEM_2D; Type Form0; + BasisFunction { + { Name sn; NameOfCoef vn; Function BF_Node; + Support Domain_Mag; Entity NodesOf[All, Not Terminals]; } + { Name sf; NameOfCoef vf; Function BF_GroupOfNodes; + Support Domain_Mag; Entity GroupsOfNodesOf[Terminals]; } + } + GlobalQuantity { + { Name V; Type AliasOf; NameOfCoef vf; } + + } + Constraint { + + { NameOfCoef vn; EntityType NodesOf; + NameOfConstraint FEMScalarPotential; } + } + } +} + +Formulation { + { Name FEM_QuasiFull_2D; Type FemEquation; + Quantity { + { Name a; Type Local; NameOfSpace Hcurl_a_FEM_2D; } + If(PerfectConductors) + { Name A; Type Global; NameOfSpace Hcurl_a_FEM_2D [A]; } + { Name I; Type Global; NameOfSpace Hcurl_a_FEM_2D [I]; } + Else + { Name ur; Type Local; NameOfSpace Hregion_u_FEM_2D; } + { Name U; Type Global; NameOfSpace Hregion_u_FEM_2D [U]; } + { Name I; Type Global; NameOfSpace Hregion_u_FEM_2D [I]; } + EndIf + { Name bt; Type Local; NameOfSpace Hcurl_bt_FEM_2D; } + { Name v; Type Local; NameOfSpace Hgrad_v_FEM_2D; } + { Name V; Type Global; NameOfSpace Hgrad_v_FEM_2D [V]; } + + } + Equation { + If(PerfectConductors) + Galerkin { [nu[] * Dof{d a}, {d a}]; + In DomainMedia_Ele; Jacobian Vol; Integration I1; } + Galerkin { [Complex[0,omega[]] * se[] * Dof{a}, {a}]; + In DomainMedia_Ele; Jacobian Vol; Integration I1; } + GlobalTerm { [-Dof{I}, {A}]; In Terminals; } + Else + Galerkin { + [nu[] * Dof{d a}, {d a}]; + In Domain_Mag; Jacobian Vol; Integration I1; + } + Galerkin { + DtDof [sigma[] * Dof{a}, {a}]; + In DomainLoss; Jacobian Vol; Integration I1; + } + Galerkin { + [sigma[] * Dof{ur}, {a}]; + In DomainLoss; Jacobian Vol; Integration I1; + } + + Galerkin { + DtDof [sigma[] * Dof{a}, {ur}]; + In DomainLoss; Jacobian Vol; Integration I1; + } + Galerkin { + [sigma[] * Dof{ur}, {ur}]; + In DomainLoss; Jacobian Vol; Integration I1; + } + + Galerkin { + DtDtDof [epsilon[] * Dof{a}, {a}]; + In Domain_Mag; Jacobian Vol; Integration I1; + } + Galerkin { + DtDof [epsilon[] * Dof{ur}, {a}]; + In Domain_Mag; Jacobian Vol; Integration I1; + } + + Galerkin { + DtDtDof [epsilon[] * Dof{a}, {ur}]; + In Domain_Mag; Jacobian Vol; Integration I1; + } + Galerkin { + DtDof [epsilon[] * Dof{ur}, {ur}]; + In Domain_Mag; Jacobian Vol; Integration I1; + } + + GlobalTerm { [Dof{I}, {U}]; In DomainCWithI; } + EndIf + // First-order transverse Ampere with `A_t = Gamma * bt` and `phi = Gamma * v`. + // `C*(nu C bt) + j omega se bt + se grad(v) - nu grad(a) = 0`. + // The tree gauge removes gradient degrees of freedom from bt. The + // continuity rows below provide that part of Ampere in the nodal space. + Galerkin { [nu[] * Dof{d bt}, {d bt}]; + In DomainMedia_Ele; Jacobian Vol; Integration I1; } + Galerkin { [Complex[0,omega[]] * se[] * Dof{bt}, {bt}]; + In DomainMedia_Ele; Jacobian Vol; Integration I1; } + Galerkin { [se[] * Dof{d v}, {bt}]; + In DomainMedia_Ele; Jacobian Vol; Integration I1; } + // d a = (d_y a, -d_x a, 0). Rotating restores grad_t(a). + Galerkin { [-nu[] * (Vector[0,0,1] /\ Dof{d a}), {bt}]; + In DomainMedia_Ele; Jacobian Vol; Integration I1; } + // Normalized continuity. The source is the axial current, not a second drive. + Galerkin { [se[] * Dof{d v}, {d v}]; + In DomainMedia_Ele; Jacobian Vol; Integration I1; } + Galerkin { [Complex[0,omega[]] * se[] * Dof{bt}, {d v}]; + In DomainMedia_Ele; Jacobian Vol; Integration I1; } + Galerkin { [Complex[0,omega[]] * se[] * (Dof{a} * Vector[0,0,1]), {v}]; + In DomainMedia_Ele; Jacobian Vol; Integration I1; } + GlobalTerm { [-Dof{I}, {V}]; In Terminals; } + } + } +} + +Macro FEMSetBasisCurrent +For t In {1:NumTerminals} + Evaluate[$FEM_I~{t} = Complex[ + UnitSource * ($FEMBasisTerminal == t), 0.]]; +EndFor +Return + +Macro FEMSolveBasis +Evaluate[$FEMConstraintStart = GetWallClockTime[]]; +Call FEMSetBasisCurrent; +UpdateConstraint[Sys_FEM]; +Evaluate[$FEMAssemblyStart = GetWallClockTime[]]; +Test[$FEMFirstSolve || !ReuseFactorization]{ + Generate[Sys_FEM]; + Evaluate[$FEMSolveStart = GetWallClockTime[]]; + Solve[Sys_FEM]; +}{ + GenerateRHSGroup[Sys_FEM, Terminals]; + Evaluate[$FEMSolveStart = GetWallClockTime[]]; + SolveAgain[Sys_FEM]; +} +Evaluate[$FEMOutputStart = GetWallClockTime[]]; +Return + +Resolution { + { Name LineCableModelsFEMScan; + System { + { Name Sys_FEM; NameOfFormulation FEM_QuasiFull_2D; + Type Complex; Frequency 1.; } + } + Operation { + CreateDir[RawDirectory]; + CreateDir[RawJobDirectory]; + CreateDir[MapDirectory]; + InitSolution[Sys_FEM]; + Evaluate[$FEMFrequencyIndex = FrequencyIndex]; + Evaluate[$FEMFrequencyHz = FrequencyHz]; + SetFrequency[Sys_FEM, FrequencyHz]; + SetTimeStep[1]; + // GetDP time is an output-step identity, not the physical frequency. + // Using the scan index avoids failures from time ranges. `SetFrequency` sets the physical frequency. + SetTime[FrequencyIndex]; + Evaluate[$FEMFirstSolve = 1]; + For basis_slot In {0:#RequestedBases()-1} + basis = RequestedBases(basis_slot); + Evaluate[$FEMBasisTerminal = basis]; + Call FEMSolveBasis; + PostOperation[FEMAppendRaw~{basis}]; + If(PlotFieldMaps) + PostOperation[FEMWriteMaps~{basis}]; + EndIf + Evaluate[$FEMOutputEnd = GetWallClockTime[]]; + PostOperation[FEMCompleteColumn~{basis}]; + Evaluate[$FEMFirstSolve = 0]; + EndFor + } + } +} + +PostProcessing { + { Name FEMFields; NameOfFormulation FEM_QuasiFull_2D; NameOfSystem Sys_FEM; + PostQuantity { + { Name ReBX; Value { Term { [Re[CompX[{bt}]]]; In DomainMedia_Ele; Jacobian Plain; } } } + { Name ImBX; Value { Term { [Im[CompX[{bt}]]]; In DomainMedia_Ele; Jacobian Plain; } } } + { Name ReBY; Value { Term { [Re[CompY[{bt}]]]; In DomainMedia_Ele; Jacobian Plain; } } } + { Name ImBY; Value { Term { [Im[CompY[{bt}]]]; In DomainMedia_Ele; Jacobian Plain; } } } + { Name bt_mesh; Value { Term { [{bt}]; In DomainMedia_Ele; Jacobian Plain; } } } + { Name v_local; Value { Term { [{v}]; In DomainMedia_Ele; Jacobian Plain; } } } + { Name hz_scaled; Value { Term { [nu[]*CompZ[{d bt}]]; In DomainMedia_Ele; Jacobian Vol; } } } + + { Name az; Value { + Term { [CompZ[{a}]]; In DomainFields; Jacobian Vol; } + }} + { Name b; Value { + Term { [{d a}]; In DomainFields; Jacobian Vol; } + }} + { Name bm; Value { + Term { [Norm[{d a}]]; In DomainFields; Jacobian Vol; } + }} + // `e` is `E_t/Gamma` [V]. `ez` is the driven axial field [V/m]. + { Name e; Value { + Term { [-{d v}-Complex[0,omega[]]*{bt}]; In DomainMedia_Ele; Jacobian Vol; } + }} + If(PerfectConductors) + { Name ez; Value { Term { [-CompZ[Dt[{a}]]]; In DomainFields; Jacobian Vol; } } } + Else + { Name ez; Value { + Term { [-CompZ[Dt[{a}] + {ur}]]; In DomainFields; Jacobian Vol; } + }} + EndIf + { Name em; Value { + Term { [Norm[-{d v}-Complex[0,omega[]]*{bt}]]; In DomainMedia_Ele; Jacobian Vol; } + }} + If(PerfectConductors) + { Name jz; Value { Term { [-CompZ[se[] * Dt[{a}]]]; In DomainFields; Jacobian Vol; } } } + Else + { Name jz; Value { + Term { [-CompZ[se[] * (Dt[{a}] + {ur})]]; + In DomainFields; Jacobian Vol; } + }} + EndIf + { Name jm; Value { + Term { [Norm[-se[] * ({d v}+Complex[0,omega[]]*{bt})]]; In DomainMedia_Ele; Jacobian Vol; } + }} + If(PerfectConductors) + { Name rhoj2; Value { Term { [0.5 * sigma[] * SquNorm[Dt[{a}]]]; In DomainFieldLoss; Jacobian Vol; } } } + Else + { Name rhoj2; Value { + Term { [0.5 * sigma[] * SquNorm[Dt[{a}] + {ur}]]; + In DomainFieldLoss; Jacobian Vol; } + }} + EndIf + If(PerfectConductors) + { Name ReZ; Value { Term { [Re[Complex[0,omega[]]*{A}/UnitSource]]; In Terminals; } } } + { Name ImZ; Value { Term { [Im[Complex[0,omega[]]*{A}/UnitSource]]; In Terminals; } } } + Else + { Name ReZ; Value { + Term { [-Re[{U} / UnitSource]]; In DomainCWithI; } + }} + { Name ImZ; Value { + Term { [-Im[{U} / UnitSource]]; In DomainCWithI; } + }} + EndIf + { Name ReP; Value { + Term { [Re[{V} / UnitSource]]; In Terminals; } + }} + { Name ImP; Value { + Term { [Im[{V} / UnitSource]]; In Terminals; } + }} + } + } +} + +// Expand output declarations only for the requested terminal columns. +For basis_slot In {0:#RequestedBases()-1} + basis = RequestedBases(basis_slot); + ColumnStem = Sprintf["getdp-f%04.0f-b%04.0f", FrequencyIndex, basis]; + RawZPath = StrCat[RawJobDirectory, "/", ColumnStem, "-Z.tsv"]; + RawScalarPath = StrCat[RawJobDirectory, "/", ColumnStem, "-Pscalar.tsv"]; + RawPPath = StrCat[RawJobDirectory, "/", ColumnStem, "-P.tsv"]; + TimingPath = StrCat[RawJobDirectory, "/", ColumnStem, "-timing.tsv"]; + CompletePath = StrCat[RawJobDirectory, "/", ColumnStem, ".done"]; +If(PlotFieldMaps) + FieldMapSuffix = Sprintf["_f%04.0f_b%04.0f.pos", FrequencyIndex, basis]; + FieldMapLabel = StrCat[Sprintf["; f=%.17g Hz; basis=", FrequencyHz], + Str[TerminalNames(basis - 1)]]; + PostOperation { + { Name FEMWriteMaps~{basis}; NameOfPostProcessing FEMFields; + LastTimeStepOnly 1; + Operation { + Print[bt_mesh, OnElementsOf DomainMedia_Ele, + Name StrCat["Pullback of A_t/Gamma [T m2]", FieldMapLabel], + File StrCat[MapDirectory, "/bt_mesh", FieldMapSuffix]]; + Print[v_local, OnElementsOf DomainMedia_Ele, + Name StrCat["phi/Gamma [V m]; gauge dependent", FieldMapLabel], + File StrCat[MapDirectory, "/v_local", FieldMapSuffix]]; + Print[hz_scaled, OnElementsOf DomainMedia_Ele, + Name StrCat["Hz/Gamma [A]", FieldMapLabel], + File StrCat[MapDirectory, "/hz_scaled", FieldMapSuffix]]; + Print[az, OnElementsOf DomainFields, Name StrCat["Az [T m]", FieldMapLabel], + File StrCat[MapDirectory, "/az", FieldMapSuffix]]; + Print[b, OnElementsOf DomainFields, Name StrCat["B [T]", FieldMapLabel], + File StrCat[MapDirectory, "/b", FieldMapSuffix]]; + Print[bm, OnElementsOf DomainFields, Name StrCat["|B| [T]", FieldMapLabel], + File StrCat[MapDirectory, "/bm", FieldMapSuffix]]; + Print[e, OnElementsOf DomainMedia_Ele, Name StrCat["E_t/Gamma [V]; axial drive 1 A", FieldMapLabel], + File StrCat[MapDirectory, "/e", FieldMapSuffix]]; + Print[ez, OnElementsOf DomainFields, Name StrCat["Ez [V/m]; axial drive 1 A", FieldMapLabel], + File StrCat[MapDirectory, "/ez", FieldMapSuffix]]; + Print[em, OnElementsOf DomainMedia_Ele, Name StrCat["|E_t/Gamma| [V]; axial drive 1 A", FieldMapLabel], + File StrCat[MapDirectory, "/em", FieldMapSuffix]]; + Print[jz, OnElementsOf DomainFieldLoss, Name StrCat["Jz [A/m2]", FieldMapLabel], + File StrCat[MapDirectory, "/jz", FieldMapSuffix]]; + Print[jm, OnElementsOf DomainMedia_Ele, Name StrCat["|J_t/Gamma| [A/m]; axial drive 1 A", FieldMapLabel], + File StrCat[MapDirectory, "/jm", FieldMapSuffix]]; + Print[rhoj2, OnElementsOf DomainFieldLoss, Name StrCat["S [W/m3]", FieldMapLabel], + File StrCat[MapDirectory, "/rhoj2", FieldMapSuffix]]; + } + } + } +EndIf + +PostOperation { + { Name FEMAppendRaw~{basis}; NameOfPostProcessing FEMFields; + LastTimeStepOnly 1; + Format Table; + NoMesh 1; + Operation { + For response_terminal In {1:NumTerminals} + Print[ReZ, OnRegion Terminal~{response_terminal}, Format Table, + File "", StoreInVariable $FEMRawRe]; + Print[ImZ, OnRegion Terminal~{response_terminal}, Format Table, + File "", StoreInVariable $FEMRawIm]; + Print[{$FEMFrequencyIndex, $FEMFrequencyHz, response_terminal, + $FEMBasisTerminal, $FEMRawRe, $FEMRawIm}, + Format "%g %.17g %g %g %.17g %.17g", + File RawZPath, AppendToExistingFile 1]; + + // Store the scalar trace separately, to make the vector contribution + // inspectable. It is gauge-dependent and is NOT an inverse admittance. + Print[ReP, OnRegion Terminal~{response_terminal}, Format Table, + File "", StoreInVariable $FEMScalarRe]; + Print[ImP, OnRegion Terminal~{response_terminal}, Format Table, + File "", StoreInVariable $FEMScalarIm]; + Print[{$FEMFrequencyIndex, $FEMFrequencyHz, response_terminal, + $FEMBasisTerminal, $FEMScalarRe, $FEMScalarIm}, + Format "%g %.17g %g %g %.17g %.17g", + File RawScalarPath, AppendToExistingFile 1]; + Print[{$FEMLineRe = 0., $FEMLineIm = 0.}, Format "%g %g", File ""]; + For point In {PathStart(response_terminal-1):PathStart(response_terminal-1)+PathCount(response_terminal-1)-1} + Print[ReBX, OnPoint {PathX(point),PathY(point),0}, File "", StoreInVariable $FEMBxRe]; + Print[ImBX, OnPoint {PathX(point),PathY(point),0}, File "", StoreInVariable $FEMBxIm]; + Print[ReBY, OnPoint {PathX(point),PathY(point),0}, File "", StoreInVariable $FEMByRe]; + Print[ImBY, OnPoint {PathX(point),PathY(point),0}, File "", StoreInVariable $FEMByIm]; + Print[{$FEMLineRe = $FEMLineRe + PathDX(point)*$FEMBxRe + PathDY(point)*$FEMByRe, + $FEMLineIm = $FEMLineIm + PathDX(point)*$FEMBxIm + PathDY(point)*$FEMByIm}, + Format "%.17g %.17g", File ""]; + EndFor + Print[{$FEMFrequencyIndex, $FEMFrequencyHz, response_terminal, + $FEMBasisTerminal, + $FEMScalarRe - 2*Pi*$FEMFrequencyHz*$FEMLineIm/UnitSource, + $FEMScalarIm + 2*Pi*$FEMFrequencyHz*$FEMLineRe/UnitSource}, + Format "%g %.17g %g %g %.17g %.17g", + File RawPPath, AppendToExistingFile 1]; + EndFor + } + } + +} + +PostOperation { + { Name FEMCompleteColumn~{basis}; NameOfPostProcessing FEMFields; + LastTimeStepOnly 1; + Operation { + Print[{$FEMFrequencyIndex, $FEMBasisTerminal, + $FEMAssemblyStart - $FEMConstraintStart, + $FEMSolveStart - $FEMAssemblyStart, + $FEMOutputStart - $FEMSolveStart, + $FEMOutputEnd - $FEMOutputStart, $FEMFirstSolve || !ReuseFactorization}, + Format "%g %g %.17g %.17g %.17g %.17g %g", + File TimingPath]; + // Written last, after raw output and optional maps have closed. + Print[{2, $FEMFrequencyIndex, $FEMFrequencyHz, $FEMBasisTerminal, + NumTerminals, PlotFieldMaps}, + Format "%g %g %.17g %g %g %g", File CompletePath]; + } + } +} +EndFor diff --git a/ext/LineCableModelsGmshExt/getdp/quasi-tem.pro b/ext/LineCableModelsGmshExt/getdp/quasi-tem.pro new file mode 100644 index 000000000..071b1e110 --- /dev/null +++ b/ext/LineCableModelsGmshExt/getdp/quasi-tem.pro @@ -0,0 +1,427 @@ +// Quasi-TEM series impedance and scalar electrodynamic potential coefficients. +// The independent A_z/u_r and v blocks share one assembled system and one +// factorization per frequency. Z uses unit axial current [A]. P uses unit +// outward transverse terminal current [A/m]. Gamma need not be small. +// Each invocation uses one mesh at one frequency and reuses its operator across +// terminal excitations. Julia schedules frequencies and writes checkpoints. + +If(!Exists(RunDirectory)) + RunDirectory = ""; +EndIf +If(!Exists(FrequencyCount)) + FrequencyCount = 0; +EndIf +If(!Exists(FrequencyIndex)) + FrequencyIndex = 1; +EndIf +If(!Exists(FrequencyHz)) + FrequencyHz = 1.; +EndIf +If(!Exists(BasisTerminal)) + BasisTerminal = 1; +EndIf +If(Exists(BasisListPath)) + Include BasisListPath; +Else + RequestedBases() = {BasisTerminal}; +EndIf +If(!Exists(ReuseFactorization)) + ReuseFactorization = 1; +EndIf +If(!Exists(PlotFieldMaps)) + PlotFieldMaps = 0; +EndIf +If(!Exists(RawOutputStem)) + RawOutputStem = "scan"; +EndIf +RawDirectory = StrCat[RunDirectory, "/raw"]; +RawJobDirectory = StrCat[RawDirectory, "/jobs"]; +MapDirectory = StrCat[RunDirectory, "/maps"]; + +Group { + Air = Region[{AIR_EM}]; + Earth = Region[{EARTH_EM}]; + AirInf = Region[{AIR_INF}]; + EarthInf = Region[{EARTH_INF}]; + Terminals = Region[{}]; + TerminalContours = Region[{}]; + + For t In {1:NumTerminals} + Terminal~{t} = Region[{(TERMINAL + t - 1)}]; + TerminalContour~{t} = Region[{(TERMINAL_CONTOUR + t - 1)}]; + Terminals += Region[{Terminal~{t}}]; + TerminalContours += Region[{TerminalContour~{t}}]; + EndFor + Sur_Dirichlet_Mag = Region[{OUTBND_EM}]; + Sur_Insulation_Ele = Region[{OUTBND_ELE_INS}]; + Sur_Dirichlet_Ele = Region[{OUTBND_ELE_REF}]; +} + +Include "materials.pro"; +Group { + // Conductors are equipotential electrodes and are excluded from electric field media. + DomainMedia_Ele = Region[{Air, AirInf, Earth, EarthInf, PassiveMaterialRegions}]; +} + +Function { + nu[#{Air, AirInf}] = 1. / AirMu; + sigma[#{Air, AirInf}] = 0.; + epsilon[#{Air, AirInf}] = AirEpsilon; + mu[#{Air, AirInf}] = AirMu; + + nu[#{Earth, EarthInf}] = 1. / EarthMu(FrequencyIndex - 1); + sigma[#{Earth, EarthInf}] = EarthSigma(FrequencyIndex - 1); + epsilon[#{Earth, EarthInf}] = EarthEpsilon(FrequencyIndex - 1); + mu[#{Earth, EarthInf}] = EarthMu(FrequencyIndex - 1); + + omega[] = 2. * Pi * $FEMFrequencyHz; + se[] = Complex[sigma[], omega[] * epsilon[]]; +} + +Constraint { + { Name FEMMagneticVectorPotential; + Case { + { Region Sur_Dirichlet_Mag; Value 0.; } + } + } + { Name FEMTerminalCurrent; + Case { + For t In {1:NumTerminals} + { Region Terminal~{t}; Value $FEM_I~{t}; } + EndFor + } + } + { Name FEMTransverseCurrent; + Case { + For t In {1:NumTerminals} + { Region Terminal~{t}; Value $FEM_Q~{t}; } + EndFor + } + } + { Name FEMScalarPotential; Type Assign; + Case { + // The earth-side far boundary is the electric reference. The air-side + // outer shell is intentionally unconstrained: the Galerkin formulation + // supplies its natural zero-normal-current boundary condition. + { Region Sur_Dirichlet_Ele; Value 0.; } + } + } +} + +FunctionSpace { + { Name Hcurl_a_FEM_2D; Type Form1P; + BasisFunction { + { Name se; NameOfCoef ae; Function BF_PerpendicularEdge; + Support Domain_Mag; Entity NodesOf[All]; } + } + Constraint { + { NameOfCoef ae; EntityType NodesOf; + NameOfConstraint FEMMagneticVectorPotential; } + } + } + + { Name Hregion_u_FEM_2D; Type Form1P; + BasisFunction { + { Name sr; NameOfCoef ur; Function BF_GroupOfPerpendicularEdges; + Support ConductorMaterialRegions; + Entity GroupsOfNodesOf[Terminals]; } + } + GlobalQuantity { + { Name U; Type AliasOf; NameOfCoef ur; } + { Name I; Type AssociatedWith; NameOfCoef ur; } + } + Constraint { + { NameOfCoef I; EntityType Auto; NameOfConstraint FEMTerminalCurrent; } + } + } + + { Name Hgrad_v_FEM_2D; Type Form0; + BasisFunction { + { Name sn; NameOfCoef vn; Function BF_Node; + Support Domain_Mag; Entity NodesOf[All, Not Terminals]; } + { Name sf; NameOfCoef vf; Function BF_GroupOfNodes; + Support Domain_Mag; Entity GroupsOfNodesOf[Terminals]; } + } + GlobalQuantity { + { Name V; Type AliasOf; NameOfCoef vf; } + { Name Q; Type AssociatedWith; NameOfCoef vf; } + } + Constraint { + { NameOfCoef Q; EntityType Auto; NameOfConstraint FEMTransverseCurrent; } + { NameOfCoef vn; EntityType NodesOf; + NameOfConstraint FEMScalarPotential; } + } + } +} + +Formulation { + { Name FEM_Z_P_2D; Type FemEquation; + Quantity { + { Name a; Type Local; NameOfSpace Hcurl_a_FEM_2D; } + { Name ur; Type Local; NameOfSpace Hregion_u_FEM_2D; } + { Name U; Type Global; NameOfSpace Hregion_u_FEM_2D [U]; } + { Name I; Type Global; NameOfSpace Hregion_u_FEM_2D [I]; } + { Name v; Type Local; NameOfSpace Hgrad_v_FEM_2D; } + { Name V; Type Global; NameOfSpace Hgrad_v_FEM_2D [V]; } + { Name Q; Type Global; NameOfSpace Hgrad_v_FEM_2D [Q]; } + } + Equation { + Galerkin { + [nu[] * Dof{d a}, {d a}]; + In Domain_Mag; Jacobian Vol; Integration I1; + } + Galerkin { + DtDof [sigma[] * Dof{a}, {a}]; + In DomainLoss; Jacobian Vol; Integration I1; + } + Galerkin { + [sigma[] * Dof{ur}, {a}]; + In DomainLoss; Jacobian Vol; Integration I1; + } + + Galerkin { + DtDof [sigma[] * Dof{a}, {ur}]; + In DomainLoss; Jacobian Vol; Integration I1; + } + Galerkin { + [sigma[] * Dof{ur}, {ur}]; + In DomainLoss; Jacobian Vol; Integration I1; + } + + Galerkin { + DtDtDof [epsilon[] * Dof{a}, {a}]; + In Domain_Mag; Jacobian Vol; Integration I1; + } + Galerkin { + DtDof [epsilon[] * Dof{ur}, {a}]; + In Domain_Mag; Jacobian Vol; Integration I1; + } + + Galerkin { + DtDtDof [epsilon[] * Dof{a}, {ur}]; + In Domain_Mag; Jacobian Vol; Integration I1; + } + Galerkin { + DtDof [epsilon[] * Dof{ur}, {ur}]; + In Domain_Mag; Jacobian Vol; Integration I1; + } + + // div(se grad(v)) + se k^2 v = 0, k^2 = -j omega mu se. + // The electric block retains diffusion and displacement at Gamma = 0. + Galerkin { + [se[] * Dof{d v}, {d v}]; + In DomainMedia_Ele; Jacobian Vol; Integration I1; + } + Galerkin { + [Complex[0, omega[]] * mu[] * se[]^2 * Dof{v}, {v}]; + In DomainMedia_Ele; Jacobian Vol; Integration I1; + } + GlobalTerm { + [Dof{Q}, {V}]; In Terminals; + } + GlobalTerm { + [Dof{I}, {U}]; In DomainCWithI; + } + } + } +} + +Macro FEMSetBasisCurrent +For t In {1:NumTerminals} + Evaluate[$FEM_I~{t} = Complex[ + UnitSource * ($FEMBasisTerminal == t), 0.]]; + Evaluate[$FEM_Q~{t} = Complex[ + -UnitTransverseSource * ($FEMBasisTerminal == t), 0.]]; +EndFor +Return + +Macro FEMSolveBasis +Evaluate[$FEMConstraintStart = GetWallClockTime[]]; +Call FEMSetBasisCurrent; +UpdateConstraint[Sys_FEM]; +Evaluate[$FEMAssemblyStart = GetWallClockTime[]]; +Test[$FEMFirstSolve || !ReuseFactorization]{ + Generate[Sys_FEM]; + Evaluate[$FEMSolveStart = GetWallClockTime[]]; + Solve[Sys_FEM]; +}{ + GenerateRHSGroup[Sys_FEM, Terminals]; + Evaluate[$FEMSolveStart = GetWallClockTime[]]; + SolveAgain[Sys_FEM]; +} +Evaluate[$FEMOutputStart = GetWallClockTime[]]; +Return + +Resolution { + { Name LineCableModelsFEMScan; + System { + { Name Sys_FEM; NameOfFormulation FEM_Z_P_2D; + Type Complex; Frequency 1.; } + } + Operation { + CreateDir[RawDirectory]; + CreateDir[RawJobDirectory]; + CreateDir[MapDirectory]; + InitSolution[Sys_FEM]; + Evaluate[$FEMFrequencyIndex = FrequencyIndex]; + Evaluate[$FEMFrequencyHz = FrequencyHz]; + SetFrequency[Sys_FEM, FrequencyHz]; + SetTimeStep[1]; + // GetDP time is an output-step identity, not the physical frequency. + // Using the scan index avoids failures from time ranges. `SetFrequency` sets the physical frequency. + SetTime[FrequencyIndex]; + Evaluate[$FEMFirstSolve = 1]; + For basis_slot In {0:#RequestedBases()-1} + basis = RequestedBases(basis_slot); + Evaluate[$FEMBasisTerminal = basis]; + Call FEMSolveBasis; + PostOperation[FEMAppendRaw~{basis}]; + If(PlotFieldMaps) + PostOperation[FEMWriteMaps~{basis}]; + EndIf + Evaluate[$FEMOutputEnd = GetWallClockTime[]]; + PostOperation[FEMCompleteColumn~{basis}]; + Evaluate[$FEMFirstSolve = 0]; + EndFor + } + } +} + +PostProcessing { + { Name FEMFields; NameOfFormulation FEM_Z_P_2D; NameOfSystem Sys_FEM; + PostQuantity { + { Name az; Value { + Term { [CompZ[{a}]]; In Domain_Mag; Jacobian Vol; } + }} + { Name b; Value { + Term { [{d a}]; In Domain_Mag; Jacobian Vol; } + }} + { Name bm; Value { + Term { [Norm[{d a}]]; In Domain_Mag; Jacobian Vol; } + }} + // The 2 terminal excitations are independent. Do not combine their + // axial and transverse electric fields into a fictitious full-wave field. + { Name e; Value { + Term { [-{d v}]; In DomainMedia_Ele; Jacobian Vol; } + }} + { Name ez; Value { + Term { [-CompZ[Dt[{a}] + {ur}]]; In Domain_Mag; Jacobian Vol; } + }} + { Name em; Value { + Term { [Norm[-{d v}]]; In DomainMedia_Ele; Jacobian Vol; } + }} + { Name jz; Value { + Term { [-CompZ[se[] * (Dt[{a}] + {ur})]]; + In Domain_Mag; Jacobian Vol; } + }} + { Name jm; Value { + Term { [Norm[-se[] * {d v}]]; In DomainMedia_Ele; Jacobian Vol; } + }} + { Name rhoj2; Value { + Term { [0.5 * sigma[] * SquNorm[Dt[{a}] + {ur}]]; + In DomainLoss; Jacobian Vol; } + }} + { Name ReZ; Value { + Term { [-Re[{U} / UnitSource]]; In DomainCWithI; } + }} + { Name ImZ; Value { + Term { [-Im[{U} / UnitSource]]; In DomainCWithI; } + }} + { Name ReP; Value { + Term { [Re[{V} / UnitTransverseSource]]; In Terminals; } + }} + { Name ImP; Value { + Term { [Im[{V} / UnitTransverseSource]]; In Terminals; } + }} + } + } +} + +// Expand output declarations only for the requested terminal columns. +For basis_slot In {0:#RequestedBases()-1} + basis = RequestedBases(basis_slot); + ColumnStem = Sprintf["getdp-f%04.0f-b%04.0f", FrequencyIndex, basis]; + RawZPath = StrCat[RawJobDirectory, "/", ColumnStem, "-Z.tsv"]; + RawPPath = StrCat[RawJobDirectory, "/", ColumnStem, "-P.tsv"]; + TimingPath = StrCat[RawJobDirectory, "/", ColumnStem, "-timing.tsv"]; + CompletePath = StrCat[RawJobDirectory, "/", ColumnStem, ".done"]; +If(PlotFieldMaps) + FieldMapSuffix = Sprintf["_f%04.0f_b%04.0f.pos", FrequencyIndex, basis]; + FieldMapLabel = StrCat[Sprintf["; f=%.17g Hz; basis=", FrequencyHz], + Str[TerminalNames(basis - 1)]]; + PostOperation { + { Name FEMWriteMaps~{basis}; NameOfPostProcessing FEMFields; + LastTimeStepOnly 1; + Operation { + Print[az, OnElementsOf Domain_Mag, Name StrCat["Az [T m]", FieldMapLabel], + File StrCat[MapDirectory, "/az", FieldMapSuffix]]; + Print[b, OnElementsOf Domain_Mag, Name StrCat["B [T]", FieldMapLabel], + File StrCat[MapDirectory, "/b", FieldMapSuffix]]; + Print[bm, OnElementsOf Domain_Mag, Name StrCat["|B| [T]", FieldMapLabel], + File StrCat[MapDirectory, "/bm", FieldMapSuffix]]; + Print[e, OnElementsOf DomainMedia_Ele, Name StrCat["E_t [V/m]; transverse drive 1 A/m", FieldMapLabel], + File StrCat[MapDirectory, "/e", FieldMapSuffix]]; + Print[ez, OnElementsOf Domain_Mag, Name StrCat["Ez [V/m]; axial drive 1 A", FieldMapLabel], + File StrCat[MapDirectory, "/ez", FieldMapSuffix]]; + Print[em, OnElementsOf DomainMedia_Ele, Name StrCat["|E_t| [V/m]; transverse drive 1 A/m", FieldMapLabel], + File StrCat[MapDirectory, "/em", FieldMapSuffix]]; + Print[jz, OnElementsOf DomainLoss, Name StrCat["Jz [A/m2]", FieldMapLabel], + File StrCat[MapDirectory, "/jz", FieldMapSuffix]]; + Print[jm, OnElementsOf DomainMedia_Ele, Name StrCat["|J_t| [A/m2]; transverse drive 1 A/m", FieldMapLabel], + File StrCat[MapDirectory, "/jm", FieldMapSuffix]]; + Print[rhoj2, OnElementsOf DomainLoss, Name StrCat["S [W/m3]", FieldMapLabel], + File StrCat[MapDirectory, "/rhoj2", FieldMapSuffix]]; + } + } + } +EndIf + +PostOperation { + { Name FEMAppendRaw~{basis}; NameOfPostProcessing FEMFields; + LastTimeStepOnly 1; + Format Table; + NoMesh 1; + Operation { + For response_terminal In {1:NumTerminals} + Print[ReZ, OnRegion Terminal~{response_terminal}, Format Table, + File "", StoreInVariable $FEMRawRe]; + Print[ImZ, OnRegion Terminal~{response_terminal}, Format Table, + File "", StoreInVariable $FEMRawIm]; + Print[{$FEMFrequencyIndex, $FEMFrequencyHz, response_terminal, + $FEMBasisTerminal, $FEMRawRe, $FEMRawIm}, + Format "%g %.17g %g %g %.17g %.17g", + File RawZPath, AppendToExistingFile 1]; + + Print[ReP, OnRegion Terminal~{response_terminal}, Format Table, + File "", StoreInVariable $FEMRawRe]; + Print[ImP, OnRegion Terminal~{response_terminal}, Format Table, + File "", StoreInVariable $FEMRawIm]; + Print[{$FEMFrequencyIndex, $FEMFrequencyHz, response_terminal, + $FEMBasisTerminal, $FEMRawRe, $FEMRawIm}, + Format "%g %.17g %g %g %.17g %.17g", + File RawPPath, AppendToExistingFile 1]; + EndFor + } + } + +} + +PostOperation { + { Name FEMCompleteColumn~{basis}; NameOfPostProcessing FEMFields; + LastTimeStepOnly 1; + Operation { + Print[{$FEMFrequencyIndex, $FEMBasisTerminal, + $FEMAssemblyStart - $FEMConstraintStart, + $FEMSolveStart - $FEMAssemblyStart, + $FEMOutputStart - $FEMSolveStart, + $FEMOutputEnd - $FEMOutputStart, $FEMFirstSolve || !ReuseFactorization}, + Format "%g %g %.17g %.17g %.17g %.17g %g", + File TimingPath]; + // Written last, after raw output and optional maps have closed. + Print[{2, $FEMFrequencyIndex, $FEMFrequencyHz, $FEMBasisTerminal, + NumTerminals, PlotFieldMaps}, + Format "%g %g %.17g %g %g %g", File CompletePath]; + } + } +} +EndFor diff --git a/ext/LineCableModelsGmshExt/mesh.jl b/ext/LineCableModelsGmshExt/mesh.jl new file mode 100644 index 000000000..654d316b3 --- /dev/null +++ b/ext/LineCableModelsGmshExt/mesh.jl @@ -0,0 +1,624 @@ +const _GMSH_GEOMETRY_TOLERANCE = 1.0e-8 +const _GMSH_BOOLEAN_TOLERANCE = 0.0 + +function _write_json_atomic(path::String, value) + mkpath(dirname(path)) + temporary = tempname(dirname(path)) + open(temporary, "w") do io + JSON3.pretty(io, value) + write(io, '\n') + end + mv(temporary, path; force = true) + return path +end + +function _gmsh_version() + try + return gmsh.option.get_string("General.Version") + catch + return "unknown" + end +end + +function _mesh_fingerprint( + model::FEMResolvedModel, + gmsh_version::String, + mesh_plan::FEMMeshPlan = last(model.mesh_plans) +) + evidence = ( + material_partition_version = 2, + system = ImportExport.serialize_value(model.problem.system), + temperature = ImportExport.serialize_value(model.problem.temperature), + earth_props = ImportExport.serialize_value(model.problem.earth_props), + # Declarations alone cannot identify geometry produced by a changed + # resolver. Fingerprint the exact domains and ownership sent to Gmsh. + regions = model.region_plans, + cable_boundaries = model.cable_boundaries, + cable_hosts = model.cable_hosts, + center = model.center, + terminal_ids = model.terminal_ids, + material_tags = getproperty.(model.material_plans, :physical_tag), + material_names = getproperty.(model.material_plans, :physical_name), + tags = model.tags, + region_mesh_sizes = getproperty.(model.region_plans, :mesh_size), + cable_outer_mesh_sizes = model.cable_outer_mesh_sizes, + mesh_growth_factor = model.mesh_growth_factor, + mesh_frequency = mesh_plan.frequency, + domain_radius = mesh_plan.domain_radius, + shell_outer_radius = mesh_plan.shell_outer_radius, + domain_mesh_size = mesh_plan.domain_mesh_size, + infinite_mesh_size = mesh_plan.infinite_mesh_size, + interface_mesh_size = mesh_plan.interface_mesh_size, + cable_interface_mesh_sizes = mesh_plan.cable_interface_mesh_sizes, + wave_mesh_sizes = mesh_plan.wave_mesh_sizes, + wave_decay_radii = mesh_plan.wave_decay_radii, + gmsh_version, + geometry_tolerance = _GMSH_GEOMETRY_TOLERANCE, + boolean_tolerance = _GMSH_BOOLEAN_TOLERANCE + ) + # Hash owned bytes: SHA's string/CodeUnits path can repeatedly hash the + # entire immutable string during copyto! alias checks on Julia 1.12. + io = IOBuffer() + JSON3.write(io, evidence) + return bytes2hex(sha256(take!(io))) +end + +function _expected_physical_groups(model::FEMResolvedModel) + groups = Tuple{Int, Int, String}[ + ( + 2, model.tags.air, "LCM/domain/air"), + ( + 2, model.tags.earth, "LCM/domain/earth"), + ( + 2, model.tags.air_infinite, "LCM/domain/air_infinite"), + ( + 2, model.tags.earth_infinite, "LCM/domain/earth_infinite"), + ( + 2, model.tags.infinite_domain, "LCM/domain/infinite_shell"), + ( + 1, + model.tags.outer_boundary, + "LCM/boundary/magnetic_dirichlet"), + ( + 1, + model.tags.outer_air_boundary, + "LCM/boundary/electric_insulation_air"), + ( + 1, + model.tags.outer_earth_boundary, + "LCM/boundary/electric_reference_earth"), + ( + 1, + model.tags.inner_shell_boundary, + "LCM/boundary/inner_infinite_shell" + ), + ( + 1, model.tags.interface, "LCM/interface/air_earth"), + ( + 2, 6_001, "LCM/domain/conductors"), + ( + 2, 6_003, "LCM/domain/field_maps") + ] + for (index, name) in enumerate(model.terminal_names) + push!(groups, (2, model.tags.terminal_base + index, name)) + push!(groups, + ( + 1, + model.tags.terminal_contour_base + index, + @sprintf("LCM/terminal_contour/%04d", index) + )) + end + for material in model.material_plans + push!(groups, (2, material.physical_tag, material.physical_name)) + end + return groups +end + +function _inspect_loaded_mesh(model::FEMResolvedModel, mesh_path::String) + isempty(gmsh.model.get_entities(2)) && _fem_error( + :mesh, + model.problem.system.system_id, + :mesh_dimension, + "mesh has no two-dimensional elements" + ) + !isempty(gmsh.model.get_entities(3)) && _fem_error( + :mesh, + model.problem.system.system_id, + :mesh_dimension, + "mesh must be two-dimensional" + ) + available = Set( + (Int(dim), Int(tag), gmsh.model.get_physical_name(dim, tag)) + for (dim, tag) in gmsh.model.get_physical_groups() + ) + for expected in _expected_physical_groups(model) + expected in available || _fem_error( + :mesh, + model.problem.system.system_id, + :physical_groups, + "mesh $(mesh_path) is missing physical group $(expected)" + ) + entities = gmsh.model.get_entities_for_physical_group(expected[1], expected[2]) + isempty(entities) && _fem_error( + :mesh, + model.problem.system.system_id, + :physical_groups, + "mesh $(mesh_path) has an empty physical group $(expected)" + ) + for entity in entities + _, element_tags, _ = gmsh.model.mesh.get_elements(expected[1], entity) + any(!isempty, element_tags) || _fem_error( + :mesh, + model.problem.system.system_id, + :physical_groups, + "mesh $(mesh_path) has no elements on entity $(entity) " * + "in physical group $(expected)" + ) + end + end + terminal_groups = count( + group -> begin + dim, tag = group + dim == 2 && + model.tags.terminal_base < tag <= + model.tags.terminal_base + length(model.terminal_ids) + end, + gmsh.model.get_physical_groups()) + terminal_groups == length(model.terminal_ids) || _fem_error( + :mesh, + model.problem.system.system_id, + :terminal_count, + "mesh terminal count $terminal_groups differs from " * + "$(length(model.terminal_ids))" + ) + _inspect_material_coverage(model, mesh_path) + return nothing +end + +# A conservative chord-area allowance, proportional to the actual boundary +# segment length squared. Polygon boundaries have no discretization allowance. +_mesh_area_allowance(::Union{DataModel.Polygon, DataModel.Rectangle}, h2) = 0.0 +_mesh_area_allowance(::DataModel.Disk, h2) = π * h2 / 3 +_mesh_area_allowance(shape::DataModel.Annulus, h2) = (iszero(shape.ri) ? π : 2π) * h2 / 3 +_mesh_area_allowance(shape::DataModel.Ellipse, h2) = π * (max(shape.a, shape.b) / min(shape.a, shape.b))^2 * h2 / 3 +# Offset ellipses use the adapter's existing polygonal boundary, not native +# arcs. Its fixed tessellation error must not shrink when mesh edges subdivide. +_mesh_area_allowance(shape::DataModel.EllipseOffset, h2) = + abs(LineCableModels.area(shape) - abs(_signed_area(_shape_points(shape)))) +_mesh_area_allowance(::DataModel.SectorShape, h2) = 4π * h2 / 3 +_mesh_area_allowance(::DataModel.BentStrip, h2) = 2π * h2 / 3 +_mesh_area_allowance(shape::DataModel.ShellShape, h2) = + _mesh_area_allowance(shape.outer, h2) + _mesh_area_allowance(shape.inner, h2) +_mesh_area_allowance(shape::DataModel.DifferenceShape, h2) = + _mesh_area_allowance(shape.outer, h2) + sum(hole -> _mesh_area_allowance(hole, h2), shape.holes; init=0.0) +_mesh_area_allowance(shape::DataModel.AssemblyShape, h2) = + sum(member -> _mesh_area_allowance(member, h2), shape.members; init=0.0) + +function _inspect_material_coverage(model::FEMResolvedModel, mesh_path::String) + fail(message) = _fem_error(:mesh, model.problem.system.system_id, + :material_partition, "mesh $mesh_path: $message") + node_tags, coordinates, _ = gmsh.model.mesh.get_nodes() + points = Dict(UInt64(tag) => (coordinates[3i-2], coordinates[3i-1]) + for (i, tag) in enumerate(node_tags)) + properties = Dict{Int, Tuple{Int, Int, Int}}() + function element_properties(kind) + get!(properties, kind) do + _, _, order, count, _, primary = gmsh.model.mesh.get_element_properties(kind) + (Int(order), Int(count), Int(primary)) + end + end + curve_edges = Dict{Int, Vector{Tuple{UInt64, UInt64}}}() + function boundary_edges(curve) + get!(curve_edges, curve) do + edges = Tuple{UInt64, UInt64}[] + kinds, _, blocks = gmsh.model.mesh.get_elements(1, curve) + for (kind, vertices) in zip(kinds, blocks) + _, count, primary = element_properties(kind) + primary == 2 || fail("unsupported boundary element $kind") + for index in 1:count:length(vertices) + push!(edges, minmax(vertices[index], vertices[index+1])) + end + end + isempty(edges) && fail("curve $curve has no boundary elements") + edges + end + end + areas = Dict{Int, Float64}() + sizes = Dict{Int, Float64}() + for (_, surface) in gmsh.model.get_entities(2) + incidence = Dict{Tuple{UInt64, UInt64}, Int}() + surface_area = 0.0 + kinds, _, blocks = gmsh.model.mesh.get_elements(2, surface) + for (kind, vertices) in zip(kinds, blocks) + order, count, primary = element_properties(kind) + primary in (3, 4) || fail("unsupported surface element $kind") + for index in 1:count:length(vertices) + for corner in 0:primary-1 + edge = minmax(vertices[index+corner], vertices[index+mod(corner+1, primary)]) + incidence[edge] = get(incidence, edge, 0) + 1 + end + if order == 1 + a = points[vertices[index]] + for corner in 1:primary-2 + b, c = points[vertices[index+corner]], points[vertices[index+corner+1]] + surface_area += abs((b[1]-a[1])*(c[2]-a[2]) - + (b[2]-a[2])*(c[1]-a[1])) / 2 + end + end + end + if order > 1 + local_points, weights = gmsh.model.mesh.get_integration_points(kind, "Gauss$(2order)") + _, determinants, _ = gmsh.model.mesh.get_jacobians(kind, local_points, surface) + surface_area += sum(abs(value) * weights[mod1(index, length(weights))] + for (index, value) in enumerate(determinants)) + end + end + surface_area > 0 || fail("surface $surface has no positive-area elements") + boundary = Set{Tuple{UInt64, UInt64}}() + maximum_edge_squared = 0.0 + for (_, curve) in gmsh.model.get_boundary([(2, surface)], false, false, false) + for edge in boundary_edges(Int(curve)) + push!(boundary, edge) + get(incidence, edge, 0) == 1 || fail( + "surface $surface does not cover boundary curve $curve") + a, b = points[edge[1]], points[edge[2]] + maximum_edge_squared = max(maximum_edge_squared, + (a[1]-b[1])^2 + (a[2]-b[2])^2) + end + end + all(count == (edge in boundary ? 1 : 2) for (edge, count) in incidence) || + fail("surface $surface has missing, overlapping or nonconformal elements") + areas[surface] = surface_area + sizes[surface] = maximum_edge_squared + end + assigned = Set{Int}() + for (index, material) in enumerate(model.material_plans) + surfaces = Int.(gmsh.model.get_entities_for_physical_group(2, material.physical_tag)) + any(surface -> surface in assigned, surfaces) && fail("a surface owns multiple material laws") + union!(assigned, surfaces) + regions = filter(region -> region.material_index == index, model.region_plans) + expected = sum(region -> Float64(LineCableModels.area(region.shape)), regions) + actual = sum(surface -> areas[surface], surfaces) + h2 = maximum(surface -> sizes[surface], surfaces) + chord_allowance = sum(region -> _mesh_area_allowance(region.shape, h2), regions) + abs(actual - expected) <= 1e-8 * expected + chord_allowance || fail( + "material $(material.physical_name) covers $actual m², expected $expected m²") + end + conductor_surfaces = Set{Int}() + for material in model.material_plans + material.kind === :conductor || continue + union!(conductor_surfaces, gmsh.model.get_entities_for_physical_group(2, material.physical_tag)) + end + terminals = Set{Int}() + for index in eachindex(model.terminal_ids) + surfaces = Int.(gmsh.model.get_entities_for_physical_group(2, model.tags.terminal_base + index)) + any(surface -> surface in terminals || surface ∉ conductor_surfaces, surfaces) && + fail("terminal $index has duplicate or nonconductive surface ownership") + union!(terminals, surfaces) + end + terminals == conductor_surfaces || fail("a conductor surface has no terminal owner") + return nothing +end + +# A mesh file that covers the FEM model: it exists and Gmsh reads it as this model. +function validate(mesh_path::String, model::FEMResolvedModel) + isfile(mesh_path) || _fem_error( + :mesh, + model.problem.system.system_id, + :mesh_path, + "mesh file does not exist: $mesh_path" + ) + current = try + gmsh.model.get_current() + catch + "" + end + validation_name = "LineCableModelsFEM-validation-$(time_ns())" + gmsh.model.add(validation_name) + try + gmsh.merge(mesh_path) + _inspect_loaded_mesh(model, mesh_path) + catch exception + exception isa LineCableModelsFEMError && rethrow() + _fem_error( + :mesh, + model.problem.system.system_id, + :mesh_path, + "failed to read mesh $mesh_path: $(sprint(showerror, exception))" + ) + finally + try + gmsh.model.remove() + catch exception + @warn "Failed to remove the Gmsh mesh-validation model" model = validation_name mesh = mesh_path exception = ( + exception, catch_backtrace()) + end + isempty(current) || try + gmsh.model.set_current(current) + catch exception + @warn "Failed to restore the caller's Gmsh model after mesh validation" model = current mesh = mesh_path exception = ( + exception, catch_backtrace()) + end + end + return mesh_path +end + +function _configure_mesh!( + model::FEMResolvedModel, + geometry::FEMGeometry, + mesh_plan::FEMMeshPlan = last(model.mesh_plans) +) + gmsh.option.set_number("Mesh.MshFileVersion", 4.1) + # Preserve boundary elements even on internal same-material seams. MSH 4 + # retains physical groups with SaveAll, enabling coverage checks on reload. + gmsh.option.set_number("Mesh.SaveAll", 1) + gmsh.option.set_number("Mesh.MeshSizeMin", + Float64(min(model.fine_mesh_size, minimum(mesh_plan.wave_mesh_sizes)))) + gmsh.option.set_number( + "Mesh.MeshSizeMax", Float64(mesh_plan.infinite_mesh_size) + ) + gmsh.option.set_number("Mesh.MeshSizeFromPoints", 1) + gmsh.option.set_number("Mesh.MeshSizeExtendFromBoundary", 0) + transition_fields = Int[] + for cable_index in eachindex(geometry.cable_curves) + cable_curves = sort!(unique(geometry.cable_curves[cable_index])) + isempty(cable_curves) && continue + distance = gmsh.model.mesh.field.add("Distance") + gmsh.model.mesh.field.set_numbers(distance, "CurvesList", cable_curves) + gmsh.model.mesh.field.set_number(distance, "Sampling", 100) + threshold = gmsh.model.mesh.field.add("Threshold") + gmsh.model.mesh.field.set_number(threshold, "InField", distance) + cable_size = model.cable_outer_mesh_sizes[cable_index] + gmsh.model.mesh.field.set_number( + threshold, "SizeMin", Float64(cable_size) + ) + gmsh.model.mesh.field.set_number( + threshold, "SizeMax", Float64(mesh_plan.domain_mesh_size) + ) + gmsh.model.mesh.field.set_number(threshold, "DistMin", 0.0) + transition_distance = max( + cable_size, + (mesh_plan.domain_mesh_size - cable_size) / + max(model.mesh_growth_factor - one(model.mesh_growth_factor), 1e-12) + ) + gmsh.model.mesh.field.set_number( + threshold, "DistMax", Float64(transition_distance) + ) + push!(transition_fields, threshold) + end + # Gmsh owns interpolation and restriction of the size field. Include the + # air-soil interface as a source of transmitted fields, also for overhead + # conductors whose distance from the soil exceeds its attenuation length. + for (surfaces, wave_size, decay_radius) in zip( + (geometry.air_surfaces, geometry.earth_surfaces), + mesh_plan.wave_mesh_sizes, mesh_plan.wave_decay_radii) + wave_size < mesh_plan.domain_mesh_size || continue + curves = unique([geometry.interface_curves; reduce(vcat, geometry.cable_curves; init=Int[])]) + distance = gmsh.model.mesh.field.add("Distance") + gmsh.model.mesh.field.set_numbers(distance, "CurvesList", curves) + gmsh.model.mesh.field.set_number(distance, "Sampling", 200) + threshold = gmsh.model.mesh.field.add("Threshold") + gmsh.model.mesh.field.set_number(threshold, "InField", distance) + gmsh.model.mesh.field.set_number(threshold, "SizeMin", Float64(wave_size)) + gmsh.model.mesh.field.set_number(threshold, "SizeMax", Float64(mesh_plan.domain_mesh_size)) + gmsh.model.mesh.field.set_number(threshold, "DistMin", Float64(decay_radius)) + gmsh.model.mesh.field.set_number(threshold, "DistMax", Float64(2decay_radius)) + restricted = gmsh.model.mesh.field.add("Restrict") + gmsh.model.mesh.field.set_number(restricted, "InField", threshold) + gmsh.model.mesh.field.set_numbers(restricted, "SurfacesList", surfaces) + gmsh.model.mesh.field.set_number(restricted, "IncludeBoundary", 1) + push!(transition_fields, restricted) + end + isempty(transition_fields) && return nothing + background = if length(transition_fields) == 1 + only(transition_fields) + else + combined = gmsh.model.mesh.field.add("Min") + gmsh.model.mesh.field.set_numbers( + combined, "FieldsList", transition_fields + ) + combined + end + gmsh.model.mesh.field.set_as_background_mesh(background) + return nothing +end + +function _mesh_metadata( + model::FEMResolvedModel, + fingerprint::String, + gmsh_version::String, + source::Symbol, + mesh_plan::FEMMeshPlan = last(model.mesh_plans) +) + return ( + schema = "LineCableModels.FEMMesh", + version = 1, + fingerprint, + gmsh_version, + source = String(source), + mesh_dimension = 2, + terminal_count = length(model.terminal_ids), + terminal_ids = model.terminal_ids, + physical_groups = [(dimension = dim, tag, name) + for (dim, tag, name) in _expected_physical_groups(model)], + frequency_index = mesh_plan.frequency_index, + frequency_hz = mesh_plan.frequency, + inner_shell_radius_m = mesh_plan.domain_radius, + outer_shell_radius_m = mesh_plan.shell_outer_radius, + minimum_mesh_size_m = min(model.fine_mesh_size, minimum(mesh_plan.wave_mesh_sizes)), + region_mesh_sizes_m = getproperty.(model.region_plans, :mesh_size), + cable_outer_mesh_sizes_m = model.cable_outer_mesh_sizes, + interface_mesh_size_m = mesh_plan.interface_mesh_size, + cable_interface_mesh_sizes_m = mesh_plan.cable_interface_mesh_sizes, + domain_mesh_size_m = mesh_plan.domain_mesh_size, + infinite_mesh_size_m = mesh_plan.infinite_mesh_size, + wave_mesh_sizes_m = mesh_plan.wave_mesh_sizes, + wave_decay_radii_m = mesh_plan.wave_decay_radii, + adjacent_growth_factor = model.mesh_growth_factor + ) +end + +function _copy_or_link_mesh(source::String, destination::String) + try + Base.Filesystem.hardlink(source, destination) + catch exception + @debug "Falling back to a physical FEM mesh copy" source destination exception + cp(source, destination; force = true) + end + return destination +end + +function _copy_mesh_snapshot!( + source::String, + run_mesh::String, + metadata_path::String, + metadata +) + mkpath(dirname(run_mesh)) + _copy_or_link_mesh(source, run_mesh) + _write_json_atomic(metadata_path, metadata) + return run_mesh +end + +function _cache_mesh!( + source::String, + cache_mesh::String, + cache_metadata::String, + metadata +) + mkpath(dirname(cache_mesh)) + temporary_mesh = tempname(dirname(cache_mesh)) + _copy_or_link_mesh(source, temporary_mesh) + mv(temporary_mesh, cache_mesh; force = true) + _write_json_atomic(cache_metadata, metadata) + return cache_mesh +end + +function _load_mesh_for_display!(mesh_path::String) + name = "LineCableModelsFEM-mesh-$(time_ns())" + gmsh.model.add(name) + gmsh.merge(mesh_path) + return name +end + +function _select_mesh!( + run::FEMRun, + model::FEMResolvedModel, + geometry::FEMGeometry, + execution::ComputationOptions, + runtime_root::String, + mesh_plan::FEMMeshPlan = last(model.mesh_plans) +) + gmsh_version = _gmsh_version() + fingerprint = _mesh_fingerprint(model, gmsh_version, mesh_plan) + displayed = mesh_plan.frequency_index == length(model.mesh_plans) + displayed && (run.mesh_fingerprint = fingerprint) + cache_directory = joinpath(runtime_root, "meshes", fingerprint) + cache_mesh = joinpath(cache_directory, "model.msh") + cache_metadata = joinpath(cache_directory, "mesh.json") + stem = displayed ? "model" : @sprintf( + "frequency_%04d", mesh_plan.frequency_index + ) + run_mesh = joinpath(run.path, "mesh", "$stem.msh") + run_metadata = joinpath(run.path, "mesh", "$stem.json") + + selected = nothing + source = :generated + if execution.data.mesh_mode === :reuse && isfile(run_mesh) && isfile(run_metadata) + try + metadata = JSON3.read(read(run_metadata, String)) + String(metadata.fingerprint) == fingerprint || error("fingerprint mismatch") + validate(run_mesh, model) + selected = run_mesh + source = :resume + catch exception + @warn "Ignoring an invalid retained FEM mesh" run_mesh exception + end + end + if selected === nothing && displayed && execution.data.mesh_mode === :reuse && + execution.data.mesh_path !== nothing + explicit = abspath(execution.data.mesh_path) + validate(explicit, model) + selected = explicit + source = :explicit + elseif selected === nothing && execution.data.mesh_mode === :reuse && + isfile(cache_mesh) && isfile(cache_metadata) + try + metadata = JSON3.read(read(cache_metadata, String)) + String(metadata.fingerprint) == fingerprint || error("fingerprint mismatch") + validate(cache_mesh, model) + selected = cache_mesh + source = :cache + catch exception + @warn "Ignoring an invalid cached FEM mesh" cache_mesh exception + end + end + + if selected === nothing + gmsh.model.set_current(geometry.model_name) + _configure_mesh!(model, geometry, mesh_plan) + gmsh.model.mesh.generate(2) + mkpath(dirname(run_mesh)) + gmsh.write(run_mesh) + _inspect_loaded_mesh(model, run_mesh) + metadata = _mesh_metadata( + model, fingerprint, gmsh_version, :generated, mesh_plan + ) + _write_json_atomic(run_metadata, metadata) + _cache_mesh!(run_mesh, cache_mesh, cache_metadata, metadata) + source = :generated + elseif selected != run_mesh + metadata = _mesh_metadata( + model, fingerprint, gmsh_version, source, mesh_plan + ) + _copy_mesh_snapshot!(selected, run_mesh, run_metadata, metadata) + execution.data.ui && displayed && _load_mesh_for_display!(run_mesh) + elseif execution.data.ui && displayed + _load_mesh_for_display!(run_mesh) + end + displayed && (run.mesh_source = source) + return run_mesh +end + +function _update_exterior_mesh!(model, geometry, plan) + gmsh.model.set_current(geometry.model_name) + gmsh.model.mesh.clear() + for tag in gmsh.model.mesh.field.list() + gmsh.model.mesh.field.remove(tag) + end + # Rebuild only the small exterior domain. GEO coordinate transforms can + # invalidate cached arcs elsewhere in the model. Cable entities remain fixed. + gmsh.model.remove_physical_groups() + surfaces = unique([geometry.air_surfaces; geometry.earth_surfaces]) + curves = unique([geometry.inner_shell_curves; geometry.outer_curves; geometry.interface_curves]) + gmsh.model.geo.remove([(2, tag) for tag in surfaces], false) + gmsh.model.geo.remove([(1, tag) for tag in curves], false) + gmsh.model.geo.remove([(0, tag) for tag in geometry.exterior_points], false) + gmsh.model.geo.synchronize() + rebuilt = _build_geometry!(model, geometry.model_name, plan; reuse=geometry) + for field in (:terminal_curves, :air_surfaces, :earth_surfaces, :infinite_surfaces, + :outer_curves, :outer_air_curves, :outer_earth_curves, + :inner_shell_curves, :interface_curves, :exterior_points) + target = getproperty(geometry, field) + empty!(target) + append!(target, getproperty(rebuilt, field)) + end + return nothing +end + +function _select_meshes!( + run::FEMRun, + model::FEMResolvedModel, + display_geometry::FEMGeometry, + execution::ComputationOptions, + runtime_root::String +) + mesh_paths = Vector{String}(undef, length(model.mesh_plans)) + for mesh_plan in model.mesh_plans + _update_exterior_mesh!(model, display_geometry, mesh_plan) + mesh_paths[mesh_plan.frequency_index] = _select_mesh!( + run, model, display_geometry, execution, runtime_root, mesh_plan + ) + end + return mesh_paths +end diff --git a/ext/LineCableModelsGmshExt/model.jl b/ext/LineCableModelsGmshExt/model.jl new file mode 100644 index 000000000..e03a72bf1 --- /dev/null +++ b/ext/LineCableModelsGmshExt/model.jl @@ -0,0 +1,885 @@ +@enum FEMRunState begin + created + geometry_ready + mesh_ready + running + completed + failed + canceled + not_executed +end + +mutable struct FEMRun + path::String + state::FEMRunState + message::String + mesh_source::Symbol + mesh_fingerprint::String + getdp_invocations::Int + completed_columns::Int + completed_frequencies::Int +end + +function FEMRun(path, state, message, mesh_source, mesh_fingerprint, invocations = 0) + FEMRun( + path, state, message, mesh_source, mesh_fingerprint, invocations, 0, 0 + ) +end + +struct FEMMaterialPlan{T <: Real} + object_id::String + field::Symbol + kind::Symbol + mu_r::T + admittivity::Vector{Complex{T}} + physical_tag::Int + physical_name::String +end + +struct FEMRegionPlan{T <: Real, S} + object_id::String + cable_index::Int + region_index::Int + terminal_index::Int + material_index::Int + shape::S + mesh_size::T +end + +struct FEMMeshPlan{T <: Real} + frequency_index::Int + frequency::T + domain_radius::T + shell_outer_radius::T + domain_mesh_size::T + infinite_mesh_size::T + interface_mesh_size::T + cable_interface_mesh_sizes::Vector{T} + wave_mesh_sizes::NTuple{2, T} + wave_decay_radii::NTuple{2, T} +end + +struct FEMResolvedModel{T <: Real, P <: LineParametersProblem} + problem::P + terminal_ids::Vector{String} + terminal_names::Vector{String} + region_plans::Vector{FEMRegionPlan} + material_plans::Vector{FEMMaterialPlan{T}} + earth_materials::Vector{Earth.EarthMaterial{T}} + cable_boundaries::Vector{Any} + cable_hosts::Vector{Symbol} + tags::NamedTuple + center::Tuple{T, T} + domain_radius::T + shell_outer_radius::T + fine_mesh_size::T + coarse_mesh_size::T + maximum_frequency::T + cable_outer_mesh_sizes::Vector{T} + mesh_growth_factor::T + mesh_plans::Vector{FEMMeshPlan{T}} +end + +struct FEMGeometry + model_name::String + terminal_surfaces::Vector{Vector{Int}} + terminal_curves::Vector{Vector{Int}} + material_surfaces::Vector{Vector{Int}} + cable_curves::Vector{Vector{Int}} + air_surfaces::Vector{Int} + earth_surfaces::Vector{Int} + infinite_surfaces::Vector{Int} + outer_curves::Vector{Int} + outer_air_curves::Vector{Int} + outer_earth_curves::Vector{Int} + inner_shell_curves::Vector{Int} + interface_curves::Vector{Int} + cable_loops::Vector{Vector{Int}} + exterior_points::Vector{Int} +end + +struct FEMScan{T <: Real} + Z::Array{Complex{T}, 3} + P::Array{Complex{T}, 3} + map_paths::Vector{String} +end + +function _fem_error( + category::Symbol, + object_id, + field::Symbol, + message::AbstractString; + run_directory = nothing +) + throw(LineCableModelsFEMError( + category, + object_id, + field, + message; + run_directory + )) +end + +const FEM_FLOAT_TAGS = ("Float16", "Float32", "Float64", "BigFloat") + +function _fem_float64_scalar(value::Real) + scalar = LineCableModels.nominal(value) + converted = Float64(scalar) + isfinite(scalar) && !isfinite(converted) && throw(OverflowError( + "finite value $scalar is outside the Float64 range")) + return converted +end + +function _fem_float64_scalar(value::Complex) + return complex(_fem_float64_scalar(real(value)), _fem_float64_scalar(imag(value))) +end + +function _fem_float64_scalar(value::AbstractDict) + return ImportExport.serialize_value( + _fem_float64_scalar(ImportExport.deserialize_value(value))) +end + +function _fem_float64_document(value::AbstractVector) + return [_fem_float64_document(item) for item in value] +end + +function _fem_float64_document(value::AbstractDict) + if haskey(value, "__type__") + marker = String(value["__type__"]) + marker in FEM_FLOAT_TAGS && return _fem_float64_scalar(value) + marker == "Measurement" && return _fem_float64_document( + get(value, "value") do + throw(ArgumentError("serialized Measurement has no nominal value")) + end + ) + marker == "Complex" && return Dict( + "__type__" => "Complex", + "re" => _fem_float64_document(get(value, "re") do + throw(ArgumentError("serialized Complex has no real component")) + end), + "im" => _fem_float64_document(get(value, "im") do + throw(ArgumentError("serialized Complex has no imaginary component")) + end) + ) + end + return Dict( + String(key) => _fem_float64_document(item) for (key, item) in value + ) +end + +_fem_float64_document(value) = value + +""" +Rebuild one FEM problem from uncertainty-free `Float64` declarations. + +The conversion is deliberately performed before adaptation or runtime setup. +Integer-valued topology is retained as integer data. All serialized floating +values, including the nominal component of `Measurements.Measurement`, become +`Float64`. +""" +function _preflight_fem_problem(problem::LineParametersProblem) + object_id = problem.system.system_id + document = try + _fem_float64_document(ImportExport.serialize_value(problem)) + catch exception + _fem_error( + :preflight, + object_id, + :numeric_type, + "could not normalize FEM inputs to nominal Float64: " * + sprint(showerror, exception) + ) + end + normalized = try + system = ImportExport.deserialize_value(document["system"]) + temperature = ImportExport.deserialize_value(document["temperature"]) + earth_props = ImportExport.deserialize_value(document["earth_props"]) + frequencies = ImportExport.deserialize_value(document["frequencies"]) + LineParametersProblem( + system; + temperature, + earth_props, + frequencies + ) + catch exception + _fem_error( + :preflight, + object_id, + :numeric_type, + "could not rebuild the nominal Float64 FEM problem: " * + sprint(showerror, exception) + ) + end + eltype(normalized) === Float64 || _fem_error( + :preflight, + object_id, + :numeric_type, + "FEM preflight produced $(eltype(normalized)); expected Float64" + ) + return normalized +end + +# A cable material that the FEM model being built can represent. +function validate(material::LineCableModels.AbstractMaterial, ::Type{FEMResolvedModel}, + object_id::String) + material.kind === :conductor && !isfinite(material.rho) && + _fem_error( + :adaptation, + object_id, + :rho, + "a conductor requires finite electrical resistivity" + ) + isfinite(material.eps_r) || _fem_error( + :adaptation, object_id, :eps_r, "relative permittivity must be finite" + ) + isfinite(material.mu_r) || _fem_error( + :adaptation, object_id, :mu_r, "relative permeability must be finite" + ) + if material isa LineCableModels.RadialDielectric + foreach(m -> validate(m, FEMResolvedModel, object_id), material.materials) + else + isfinite(material.tan_delta) || _fem_error( + :adaptation, object_id, :tan_delta, "loss tangent must be finite" + ) + end + return material +end + +# The geo kernel of the FEM model being built adapts these resolved geometry types. +function validate( + shape::Union{ + DataModel.Disk, + DataModel.Rectangle, + DataModel.Ellipse, + DataModel.EllipseOffset, + DataModel.Annulus, + DataModel.Polygon, + DataModel.BentStrip, + DataModel.SectorShape}, + ::Type{FEMResolvedModel}, + object_id::String +) + return shape +end + +function validate(shape::DataModel.ShellShape, ::Type{FEMResolvedModel}, object_id::String) + validate(shape.inner, FEMResolvedModel, object_id) + validate(shape.outer, FEMResolvedModel, object_id) + return shape +end + +function validate(shape::DataModel.DifferenceShape, ::Type{FEMResolvedModel}, + object_id::String) + validate(shape.outer, FEMResolvedModel, object_id) + foreach(hole -> validate(hole, FEMResolvedModel, object_id), shape.holes) + return shape +end + +function validate(shape::DataModel.AssemblyShape, ::Type{FEMResolvedModel}, + object_id::String) + foreach(member -> validate(member, FEMResolvedModel, object_id), shape.members) + return shape +end + +function validate(shape, ::Type{FEMResolvedModel}, object_id::String) + throw(LineCableModelsFEMError( + :unsupported, + object_id, + :primitive, + "resolved shape $(typeof(shape)) has no built-in geo-kernel adaptation" + )) +end + +# The resolved cross-section of a cable design partitions its envelope into materials. +function validate(design::DataModel.CableDesign, ::Type{FEMResolvedModel}) + envelope_area = LineCableModels.area(design.geometry.outer) + declared_area = sum( + region -> LineCableModels.area(region.primitive), + design.geometry.regions; + init = zero(envelope_area) + ) + tolerance = max(abs(envelope_area), one(envelope_area)) * 1e-10 + isapprox( + declared_area, + envelope_area; + rtol = 1e-10, + atol = tolerance + ) || _fem_error( + :adaptation, + design.cable_id, + :material_partition, + "the resolved cable cross-section is not a complete material " * + "partition: declared area $(declared_area), envelope area " * + "$(envelope_area); represent interstitial media explicitly with " * + "Enclosure" + ) + return design +end + +function _fem_region_mesh_size(region::DataModel.PlacedRegion) + source = region.source.primitive + shape = region.primitive + repeated = any(entry -> entry.owner === DataModel.Group, region.placement.patterns) + if source isa DataModel.Disk + return repeated ? source.r : source.r / 5 + end + if source isa DataModel.Annulus + return (source.ro - source.ri) / 2 + end + if source isa DataModel.Rectangle + return min(source.w, source.h) / (repeated ? 2 : 5) + end + if source isa DataModel.Ellipse + return min(source.a, source.b) / (repeated ? 1 : 5) + end + if shape isa DataModel.Annulus + return (shape.ro - shape.ri) / 2 + end + region_area = LineCableModels.area(shape) + region_perimeter = DataModel.perimeter(shape) + scale = region_area / region_perimeter + isfinite(scale) && scale > zero(scale) || _fem_error( + :adaptation, + String(region.source.tag), + :mesh_size, + "could not derive a positive local characteristic length" + ) + return scale +end + +function _fem_cable_outer_mesh_sizes( + region_plans::Vector{FEMRegionPlan}, cable_boundaries, cable_count::Int +) + sizes = Vector{Float64}(undef, cable_count) + for cable_index in 1:cable_count + indices = findall(plan -> plan.cable_index == cable_index, region_plans) + isempty(indices) && _fem_error( + :adaptation, + "cable_$cable_index", + :mesh_size, + "a cable has no resolved material regions" + ) + boundary = cable_boundaries[cable_index] + sizes[cable_index] = if boundary isa DataModel.AssemblyShape + minimum(region_plans[index].mesh_size for index in indices) + else + region_plans[last(indices)].mesh_size + end + end + return sizes +end + +function _fem_mesh_plans( + problem::LineParametersProblem{T}, + earth_materials::Vector{Earth.EarthMaterial{T}}, + center_x::T, + layout_radius::T, + cable_outer_mesh_sizes::Vector{T}, + growth_factor::T, + domain_skin_depths::Real +) where {T <: Real} + plans = FEMMeshPlan{T}[] + for (frequency_index, frequency) in enumerate(problem.frequencies) + earth = earth_materials[frequency_index] + earth_skin_depth = sqrt( + earth.rho / + (convert(T, π) * frequency * earth.mu_r * vacuum_permeability(T)) + ) + isfinite(earth_skin_depth) && earth_skin_depth > zero(T) || _fem_error( + :unsupported, problem.system.system_id, :earth_properties, + "evaluated soil at $frequency Hz requires a finite positive conductive skin depth") + domain_radius = max(layout_radius, convert(T, domain_skin_depths) * earth_skin_depth) + shell_outer_radius = convert(T, 1.25) * domain_radius + # Domain enlargement must not also coarsen the surrounding-medium mesh. + # Keep the physical resolution scale independent of the chosen radius. + resolution_radius = max(layout_radius, earth_skin_depth) + domain_mesh_size = resolution_radius / 20 + infinite_mesh_size = 2domain_mesh_size + # Resolve both attenuation and phase in each surrounding medium. + # A skin-depth-sized outer domain alone does not resolve mutual fields + # when the layout sets a much larger domain at high frequency. + air = problem.earth_props.layers[1] + omega = convert(T, 2π) * frequency + wave_numbers = map(((zero(T), air.eps_r, air.mu_r), + (inv(earth.rho), earth.eps_r, earth.mu_r))) do (sigma, eps_r, mu_r) + sqrt(complex(-omega^2 * (mu_r * vacuum_permeability(T)) * + (eps_r * vacuum_permittivity(T)), + omega * (mu_r * vacuum_permeability(T)) * sigma)) + end + wave_mesh_sizes = map(q -> min(domain_mesh_size, inv(8abs(q))), wave_numbers) + wave_decay_radii = map(q -> min(2resolution_radius, 6 / real(q)), wave_numbers) + cable_interface_mesh_sizes = T[] + for (cable_index, (design, position)) in enumerate(zip( + problem.system.designs, problem.system.positions + )) + distance = max( + zero(T), abs(position.y) - LineCableModels.outer_radius(design) + ) + push!(cable_interface_mesh_sizes, + min( + domain_mesh_size, + cable_outer_mesh_sizes[cable_index] + + (growth_factor - one(T)) * distance + )) + end + interface_mesh_size = minimum([domain_mesh_size; cable_interface_mesh_sizes]) + push!(plans, + FEMMeshPlan( + frequency_index, + frequency, + domain_radius, + shell_outer_radius, + domain_mesh_size, + infinite_mesh_size, + interface_mesh_size, + cable_interface_mesh_sizes, + wave_mesh_sizes, + wave_decay_radii + )) + end + return plans +end + +function _formations(regions, terminal_map, object_id) + formations = NamedTuple[] + assigned = falses(length(regions)) + for (region_index, region) in pairs(regions) + assigned[region_index] && continue + entry_index = findfirst( + entry -> entry.pattern isa DataModel.BoundedPlacement, + region.placement.patterns + ) + entry_index === nothing && continue + entry = region.placement.patterns[entry_index] + enclosing = region.placement.patterns[(entry_index + 1):end] + members = Int[] + member_ids = Int[] + for (peer_index, peer) in pairs(regions) + peer_entry_index = findfirst( + region -> region.pattern isa DataModel.BoundedPlacement, + peer.placement.patterns + ) + peer_entry_index === nothing && continue + peer_entry = peer.placement.patterns[peer_entry_index] + isequal(peer_entry.pattern.boundary, entry.pattern.boundary) || continue + peer.terminal === region.terminal || continue + isequal( + peer.placement.patterns[(peer_entry_index + 1):end], enclosing + ) || continue + push!(members, peer_index) + push!(member_ids, peer_entry.member) + end + order = sortperm(member_ids) + members = members[order] + member_ids = member_ids[order] + member_ids == collect(1:length(member_ids)) || _fem_error( + :adaptation, + object_id, + :material_partition, + "bounded-formation member identities must be contiguous from one" + ) + assigned[members] .= true + + boundary = entry.pattern.boundary + occupied_area = sum( + index -> LineCableModels.area(regions[index].primitive), + members + ) + boundary_area = LineCableModels.area(boundary) + complete = isapprox( + occupied_area, + boundary_area; + rtol = 0, + atol = DataModel.geometry_tolerance(boundary_area) + ) + member_shapes = [regions[index].primitive for index in members] + if complete + material = regions[first(members)].source.material + all( + index -> regions[index].source.material == material, + members + ) || _fem_error( + :unsupported, + object_id, + :material_partition, + "a complete bounded formation can collapse only when every " * + "member has the same material" + ) + terminal = terminal_map[first(members)] + all(index -> terminal_map[index] == terminal, members) || _fem_error( + :unsupported, + object_id, + :terminal, + "a complete bounded formation can collapse only when every " * + "member has the same terminal owner" + ) + else + filled = any(regions) do region + shape = region.primitive + region.source.material.kind === :conductor && return false + if shape isa DataModel.Annulus && boundary isa DataModel.Disk + concentric = isapprox(shape.at.x, boundary.at.x) && + isapprox(shape.at.y, boundary.at.y) + concentric && isapprox(shape.ro, boundary.r) || return false + isapprox(occupied_area, π * shape.ri^2; + rtol = 5e-6, atol = 0) || return false + shift = DataModel.Pose2(-boundary.at.x, -boundary.at.y) + tolerance = 64eps(shape.ri) + return all(member_shapes) do member_shape + DataModel.support(DataModel.resolve(shift, member_shape)) <= + shape.ri + tolerance + end + end + shape isa DataModel.DifferenceShape || return false + all(member_shapes) do member_shape + any(hole -> isequal(hole, member_shape), shape.holes) + end + end + filled || _fem_error( + :adaptation, + object_id, + :material_partition, + "an incomplete bounded formation leaves unassigned cross-sectional " * + "area; contain it in Enclosure with an explicit fill material" + ) + end + push!(formations, + (; + members, + member_shapes, + boundary, + complete, + mesh_size = minimum( + index -> _fem_region_mesh_size(regions[index]), members + ) + )) + end + return formations +end + +function _coalesce(shape::DataModel.DifferenceShape, formations, object_id) + holes = Any[shape.holes...] + for formation in formations + formation.complete || continue + matched = findall(eachindex(holes)) do hole_index + any( + member_shape -> isequal(holes[hole_index], member_shape), + formation.member_shapes + ) + end + isempty(matched) && continue + length(matched) == length(formation.member_shapes) || _fem_error( + :adaptation, + object_id, + :material_partition, + "an enclosing material excludes only part of a complete bounded formation" + ) + insertion = first(matched) + retained = Any[] + for hole_index in eachindex(holes) + hole_index == insertion && push!(retained, formation.boundary) + hole_index in matched || push!(retained, holes[hole_index]) + end + holes = retained + end + return DataModel.DifferenceShape(shape.outer, Tuple(holes)) +end + +_coalesce(shape, ::Any, ::Any) = shape + +function _resolved_fem_model( + problem::LineParametersProblem{T}, + formulation::LineCableModelsFEM, + options::ComputationOptions = computation_options(LineCableModelsFEM, ComputationOptions()) +) where {T <: Real} + try + LineCableModels.validate(problem) + catch exception + _fem_error( + :adaptation, + problem.system.system_id, + :problem, + sprint(showerror, exception) + ) + end + earth = problem.earth_props + earth.vertical_layers && _fem_error( + :unsupported, + problem.system.system_id, + :vertical_layers, + "vertical earth layers are not supported by the two-dimensional FEM domain" + ) + length(earth.layers) == 2 || _fem_error( + :unsupported, + problem.system.system_id, + :earth_props, + "the FEM backend currently supports one homogeneous earth half-space" + ) + environment = problem.system.environment + environment isa Union{Nothing, Earth.EarthModel} || _fem_error( + :unsupported, + problem.system.system_id, + :environment, + "the declared environment type $(typeof(environment)) has no FEM adaptation" + ) + + soil = earth.layers[2] + isinf(soil.thickness) || _fem_error(:unsupported, problem.system.system_id, + :earth_props, "the FEM soil region must be a semi-infinite half-space") + earth_materials = Earth.EarthMaterial{T}[] + for frequency in problem.frequencies + evaluated = try + state = LineCableModels.constitutive(formulation.methods.earth_properties, + Earth.EarthMaterial(soil), frequency) + material = Earth.EarthMaterial(_fem_float64_scalar(state.rho), + _fem_float64_scalar(state.eps_r), _fem_float64_scalar(state.mu_r)) + isfinite(inv(material.rho)) && inv(material.rho) > 0 || throw(DomainError( + material.rho, "soil conductivity must be positive and finite")) + material + catch exception + _fem_error(:adaptation, problem.system.system_id, :earth_properties, + "soil constitutive evaluation at $frequency Hz: $(sprint(showerror, exception))") + end + push!(earth_materials, evaluated) + end + + system = problem.system + terminal_count = length(system.terminal_order) + terminal_count > 0 || _fem_error( + :adaptation, system.system_id, :terminal_order, "no terminals were resolved" + ) + terminal_ids = [@sprintf("cable_%04d/%s/%s", + entry.cable, + system.designs[entry.cable].cable_id, + entry.terminal) + for entry in system.terminal_order] + length(unique(terminal_ids)) == terminal_count || _fem_error( + :adaptation, + system.system_id, + :terminal_order, + "global cable/terminal identities must be unique" + ) + terminal_names = [@sprintf("LCM/terminal/%04d/%s", index, terminal_ids[index]) + for index in eachindex(terminal_ids)] + + region_plans = FEMRegionPlan[] + material_plans = FEMMaterialPlan{T}[] + global_region = 0 + for (cable_index, design) in enumerate(system.designs) + validate(design, FEMResolvedModel) + count = length(design.geometry.regions) + first_global = global_region + 1 + last_global = global_region + count + regions = @view system.geometry[first_global:last_global] + terminals = @view system.terminal_map[first_global:last_global] + cable_id = @sprintf("cable_%04d/%s", cable_index, design.cable_id) + formations = _formations(regions, terminals, cable_id) + complete = Dict( + first(formation.members) => formation + for formation in formations if formation.complete + ) + skipped = Set( + member + for formation in formations if formation.complete + for member in formation.members[2:end] + ) + for local_region in eachindex(design.geometry.regions) + global_region += 1 + local_region in skipped && continue + placed = system.geometry[global_region] + source = placed.source + object_id = @sprintf("cable_%04d/%s/%s/region_%04d", + cable_index, + design.cable_id, + source.tag, + local_region) + validate(source.material, FEMResolvedModel, object_id) + terminal_index = system.terminal_map[global_region] + if source.material.kind === :conductor + terminal_index > 0 || _fem_error( + :adaptation, + object_id, + :terminal, + "a conductor surface has no electrical terminal owner" + ) + elseif terminal_index != 0 + _fem_error( + :adaptation, + object_id, + :terminal, + "a passive material surface cannot own an electrical terminal" + ) + end + material = source.material + admittivity = try + if material.kind === :conductor + rho = _fem_float64_scalar(LineCableModels.constitutive( + formulation.methods.temperature_dependence, material, problem.temperature)) + isfinite(inv(rho)) && inv(rho) > 0 || throw(DomainError(rho, + "conductor conductivity must be positive and finite")) + epsilon = convert(T, material.eps_r) * vacuum_permittivity(T) + Complex{T}[complex(inv(rho) + 2π * f * epsilon * material.tan_delta, + 2π * f * epsilon) for f in problem.frequencies] + else + selected = material isa LineCableModels.RadialDielectric ? + (formulation.methods.insulation_admittance, + formulation.methods.semicon_admittance) : + material.kind === :semicon ? formulation.methods.semicon_admittance : + formulation.methods.insulation_admittance + Complex{T}[_fem_float64_scalar(LineCableModels.constitutive( + selected, material, frequency, problem.temperature; + temperature_dependence=formulation.methods.temperature_dependence)) + for frequency in problem.frequencies] + end + catch exception + _fem_error(:adaptation, object_id, :constitutive, + "material evaluation at $(problem.temperature) °C: $(sprint(showerror, exception))") + end + all(isfinite, admittivity) || _fem_error(:adaptation, object_id, + :constitutive, "evaluated material admittivity must be finite") + formation = get(complete, local_region, nothing) + shape = formation === nothing ? + _coalesce(placed.primitive, formations, object_id) : + formation.boundary + mesh_size = formation === nothing ? + _fem_region_mesh_size(placed) : formation.mesh_size + validate(shape, FEMResolvedModel, object_id) + # Geometric regions and terminal ownership remain independent of + # constitutive identity. Equal evaluated laws share one physical + # material group, even across disconnected strand surfaces. + mu_r = convert(T, material.mu_r) + material_index = something(findfirst(material_plans) do plan + plan.kind === material.kind && plan.mu_r == mu_r && + plan.admittivity == admittivity + end, 0) + if iszero(material_index) + material_index = length(material_plans) + 1 + push!(material_plans, + FEMMaterialPlan{T}( + object_id, source.tag, material.kind, mu_r, admittivity, + 10_000 + material_index, + @sprintf("LCM/material/%04d/%s", material_index, material.kind) + )) + end + push!(region_plans, + FEMRegionPlan( + object_id, + cable_index, + local_region, + terminal_index, + material_index, + shape, + convert(T, mesh_size) + )) + end + end + global_region == length(system.geometry) || _fem_error( + :adaptation, + system.system_id, + :geometry, + "cable-design and system geometry orders are inconsistent" + ) + + cable_boundaries = Any[LineCableModels.resolve(position, design.geometry.outer) + for (design, position) in zip(system.designs, system.positions)] + for (index, boundary) in enumerate(cable_boundaries) + validate(boundary, FEMResolvedModel, @sprintf("cable_%04d/%s", index, + system.designs[index].cable_id)) + end + cable_hosts = Symbol[] + for (design, position) in zip(system.designs, system.positions) + radius = LineCableModels.outer_radius(design) + if position.y - radius > 0 + push!(cable_hosts, :air) + elseif position.y + radius < 0 + push!(cable_hosts, :earth) + else + _fem_error( + :unsupported, + design.cable_id, + :position, + "a cable cannot cross or touch the air/earth interface in " * + "the two-dimensional FEM adaptation" + ) + end + end + center_x = convert(T, sum(position.x for position in system.positions) / + length(system.positions)) + maximum_cable_distance = maximum( + ( + hypot( + system.positions[right].x - system.positions[left].x, + system.positions[right].y - system.positions[left].y + ) + for left in eachindex(system.positions) + for right in (left + 1):length(system.positions) + ); + init = zero(T)) + envelope_radius = maximum( + hypot(position.x - center_x, position.y) + + LineCableModels.outer_radius(design) + for (design, position) in zip(system.designs, system.positions) + ) + layout_radius = max( + convert(T, 5), + convert(T, 2) * maximum_cable_distance, + convert(T, envelope_radius) + ) + cable_outer_mesh_sizes = convert( + Vector{T}, + _fem_cable_outer_mesh_sizes( + region_plans, cable_boundaries, length(system.designs) + ) + ) + mesh_growth_factor = convert(T, 1.2) + mesh_plans = _fem_mesh_plans( + problem, + earth_materials, + center_x, + layout_radius, + cable_outer_mesh_sizes, + mesh_growth_factor, + options.data.domain_skin_depths + ) + display_plan = last(mesh_plans) + domain_radius = display_plan.domain_radius + shell_outer_radius = display_plan.shell_outer_radius + fine_mesh_size = minimum(getproperty.(region_plans, :mesh_size)) + coarse_mesh_size = display_plan.domain_mesh_size + maximum_frequency = maximum(problem.frequencies) + tags = ( + air = 1_001, + earth = 1_002, + outer_boundary = 2_001, + interface = 2_002, + inner_shell_boundary = 2_003, + outer_air_boundary = 2_004, + outer_earth_boundary = 2_005, + air_infinite = 1_003, + earth_infinite = 1_004, + infinite_domain = 1_005, + terminal_base = 3_000, + terminal_contour_base = 4_000, + cable_contour_base = 5_000 + ) + return FEMResolvedModel( + problem, + terminal_ids, + terminal_names, + region_plans, + material_plans, + earth_materials, + cable_boundaries, + cable_hosts, + tags, + (center_x, zero(T)), + domain_radius, + shell_outer_radius, + fine_mesh_size, + coarse_mesh_size, + maximum_frequency, + cable_outer_mesh_sizes, + mesh_growth_factor, + mesh_plans + ) +end diff --git a/ext/LineCableModelsGmshExt/onelab.jl b/ext/LineCableModelsGmshExt/onelab.jl new file mode 100644 index 000000000..b7794e852 --- /dev/null +++ b/ext/LineCableModelsGmshExt/onelab.jl @@ -0,0 +1,257 @@ +struct FEMOnelabSnapshot + names::Vector{String} + parameters::Dict{String, String} +end + +const FEM_ONELAB_ROOT = "LineCableModels/FEM" + +function _onelab_name(suffix::AbstractString) + return string(FEM_ONELAB_ROOT, "/", suffix) +end + +function _snapshot_onelab() + names = String[name for name in gmsh.onelab.get_names("^LineCableModels/FEM/")] + parameters = Dict{String, String}() + for name in names + parameters[name] = gmsh.onelab.get(name) + end + return FEMOnelabSnapshot(names, parameters) +end + +function _restore_onelab(snapshot::FEMOnelabSnapshot) + for name in gmsh.onelab.get_names("^LineCableModels/FEM/") + gmsh.onelab.clear(name) + end + for name in snapshot.names + gmsh.onelab.set(snapshot.parameters[name]) + end + return nothing +end + +function _number_parameter( + name::String, + value::Real; + visible::Bool = false, + read_only::Bool = true, + label::Union{Nothing, String} = nothing, + attributes = Dict{String, String}() +) + parameter = Dict{String, Any}( + "type" => "number", + "name" => name, + "values" => [Float64(value)], + "visible" => visible, + "readOnly" => read_only, + "attributes" => attributes + ) + label === nothing || (parameter["label"] = label) + return parameter +end + +function _string_parameter( + name::String, + value::AbstractString; + visible::Bool = false, + read_only::Bool = true, + label::Union{Nothing, String} = nothing, + attributes = Dict{String, String}() +) + parameter = Dict{String, Any}( + "type" => "string", + "name" => name, + "values" => [String(value)], + "visible" => visible, + "readOnly" => read_only, + "attributes" => attributes + ) + label === nothing || (parameter["label"] = label) + return parameter +end + +function _publish_parameters(parameters) + gmsh.onelab.set(JSON3.write(parameters)) + return nothing +end + +function _publish_transport!( + run::FEMRun, + model::FEMResolvedModel, + model_data_path::String, + mesh_path::String, + formulation::LineCableModelsFEM, + execution::ComputationOptions +) + parameters = Any[ + merge(_number_parameter(_onelab_name("physics"), _fem_physics_code(formulation); + visible=true, label="Physics"), + Dict("choices" => [0, 1], "valueLabels" => Dict("quasi-tem" => 0, "quasi-fw" => 1))), + _string_parameter(_onelab_name("run_directory"), run.path), + _string_parameter( + _onelab_name("model_data_path"), model_data_path), + _string_parameter( + _onelab_name("mesh_path"), mesh_path), + _number_parameter( + _onelab_name("terminal_count"), length(model.terminal_ids) + ), + _number_parameter( + _onelab_name("frequency_count"), length(model.problem.frequencies) + ), + _number_parameter( + _onelab_name("field_maps"), execution.data.plot_field_maps + ), + _number_parameter( + _onelab_name("completion_status"), 0), + _number_parameter( + _onelab_name("current_source_index"), 0), + _number_parameter( + _onelab_name("current_frequency_index"), 0) + ] + for (index, frequency) in enumerate(model.problem.frequencies) + push!(parameters, _number_parameter( + _onelab_name("frequency/$index"), frequency + )) + end + _publish_parameters(parameters) + # JSON parameter definitions preserve an existing ONELAB value. A managed + # run must explicitly refresh it when another formulation ran in this REPL. + gmsh.onelab.set_number(_onelab_name("physics"), [_fem_physics_code(formulation)]) + return nothing +end + +function _button_parameter(name::String, label::String, payload::String) + action = _onelab_name("ui/action") + command = "SetString[\"$action\", \"$payload\"];" + return _string_parameter( + name, + command; + visible = true, + read_only = false, + label, + attributes = Dict( + "Macro" => "GmshParseString", + "Aspect" => "Button", + "AutoCheck" => "1", + "ServerAction" => "" + ) + ) +end + +function _ui_state_labels(state::Symbol) + mesh_state = if state in (:mesh_ready, :running, :results_ready) + "ready" + elseif state === :mesh_requested + "generating" + elseif state === :mesh_required + "required" + else + "not generated" + end + solve_state = if state === :results_ready + "completed" + elseif state === :running + "running" + else + "not run" + end + return (; mesh_state, solve_state) +end + +function _publish_ui!(run::FEMRun, model::FEMResolvedModel, state::Symbol) + frequency_summary = join(model.problem.frequencies, ", ") * " Hz" + states = _ui_state_labels(state) + parameters = Any[ + _string_parameter( + _onelab_name("ui/problem"), + model.problem.system.system_id; + visible = true, + label = "Problem" + ), + _string_parameter( + _onelab_name("ui/frequencies"), + frequency_summary; + visible = true, + label = "Frequencies" + ), + _number_parameter( + _onelab_name("ui/cable_count"), + length(model.problem.system.designs); + visible = true, + label = "Cables" + ), + _number_parameter( + _onelab_name("ui/terminal_count"), + length(model.terminal_ids); + visible = true, + label = "Terminals" + ), + _string_parameter( + _onelab_name("ui/run_directory"), + run.path; + visible = true, + label = "Run directory" + ), + _string_parameter( + _onelab_name("ui/status"), + replace(string(state), '_' => ' '); + visible = true, + label = "Status" + ), + _string_parameter( + _onelab_name("ui/mesh_state"), + states.mesh_state; + visible = true, + label = "Mesh state" + ), + _string_parameter( + _onelab_name("ui/solve_state"), + states.solve_state; + visible = true, + label = "Solve state" + ), + _number_parameter(_onelab_name("ui/completed_frequencies"), 0; + visible=true, label="Completed frequencies"), + _number_parameter(_onelab_name("ui/completed_columns"), 0; + visible=true, label="Completed terminal columns"), + _string_parameter( + _onelab_name("ui/action"), ""), + _button_parameter( + _onelab_name("ui/generate_mesh"), "Generate mesh", "generate_mesh" + ), + _button_parameter( + _onelab_name("ui/run_model"), "Run model", "run_model") + ] + _publish_parameters(parameters) + return nothing +end + +function _set_ui_status(state::Symbol) + states = _ui_state_labels(state) + gmsh.onelab.set_string( + _onelab_name("ui/status"), [replace(string(state), '_' => ' ')] + ) + gmsh.onelab.set_string( + _onelab_name("ui/mesh_state"), [states.mesh_state] + ) + gmsh.onelab.set_string( + _onelab_name("ui/solve_state"), [states.solve_state] + ) + return state +end + +function _take_ui_action() + values = gmsh.onelab.get_string(_onelab_name("ui/action")) + action = isempty(values) ? "" : only(values) + isempty(action) || gmsh.onelab.set_string(_onelab_name("ui/action"), [""]) + return Symbol(action) +end + +function _ui_transition(state::Symbol, action::Symbol, available::Bool = true) + available || return state === :geometry_ready ? :closed_before_mesh : + state === :mesh_ready ? :closed_before_solve : state + action === Symbol("") && return state + state === :geometry_ready && action === :generate_mesh && return :mesh_requested + state === :geometry_ready && action === :run_model && return :mesh_required + state === :mesh_ready && action === :generate_mesh && return :mesh_requested + state === :mesh_ready && action === :run_model && return :solve_requested + return state +end diff --git a/ext/LineCableModelsGmshExt/results.jl b/ext/LineCableModelsGmshExt/results.jl new file mode 100644 index 000000000..8abd15f84 --- /dev/null +++ b/ext/LineCableModelsGmshExt/results.jl @@ -0,0 +1,405 @@ +const FEM_RAW_HEADER = [ + "frequency_index", + "frequency_hz", + "response_terminal", + "basis_terminal", + "real", + "imaginary" +] + +const FEM_COMPLETE_HEADER = [ + "frequency_count", + "terminal_count", + "expected_rows", + "z_rows", + "p_rows", + "success" +] + +function _nonempty_lines(path::String, run::FEMRun) + isfile(path) || _fem_error( + :results, + basename(path), + :file, + "required FEM output is missing"; + run_directory = run.path + ) + return filter(!isempty, strip.(readlines(path))) +end + +function _parse_integer(token::AbstractString, path::String, line::Int, run::FEMRun) + value = tryparse(Int, token) + value === nothing && _fem_error( + :results, + basename(path), + :row, + "invalid integer at line $line: $(repr(token))"; + run_directory = run.path + ) + return value +end + +function _parse_real( + ::Type{T}, token::AbstractString, path::String, line::Int, run::FEMRun +) where {T <: Real} + value = tryparse(T, token) + value === nothing && _fem_error( + :results, + basename(path), + :row, + "invalid floating-point value at line $line: $(repr(token))"; + run_directory = run.path + ) + isfinite(value) || _fem_error( + :results, + basename(path), + :row, + "non-finite floating-point value at line $line"; + run_directory = run.path + ) + return value +end + +function _parse_raw_matrix( + ::Type{T}, + path::String, + frequencies::Vector{T}, + terminal_count::Int, + run::FEMRun +) where {T <: Real} + lines = _nonempty_lines(path, run) + isempty(lines) && _fem_error( + :results, basename(path), :header, "raw output is empty"; + run_directory = run.path + ) + split(first(lines), '\t'; keepempty = true) == FEM_RAW_HEADER || _fem_error( + :results, + basename(path), + :header, + "malformed raw header: $(first(lines))"; + run_directory = run.path + ) + expected = length(frequencies) * terminal_count * terminal_count + length(lines) - 1 == expected || _fem_error( + :results, + basename(path), + :cardinality, + "expected $expected data rows, found $(length(lines) - 1)"; + run_directory = run.path + ) + matrix = Array{Complex{T}, 3}( + undef, terminal_count, terminal_count, length(frequencies) + ) + seen = Set{NTuple{3, Int}}() + for (line_index, line) in zip(2:length(lines), @view(lines[2:end])) + columns = split(line, '\t'; keepempty = true) + length(columns) == 6 || _fem_error( + :results, + basename(path), + :row, + "line $line_index has $(length(columns)) columns instead of 6"; + run_directory = run.path + ) + frequency_index = _parse_integer(columns[1], path, line_index, run) + response = _parse_integer(columns[3], path, line_index, run) + basis = _parse_integer(columns[4], path, line_index, run) + frequency_index in eachindex(frequencies) || _fem_error( + :results, basename(path), :frequency_index, + "frequency index $frequency_index is out of range"; + run_directory = run.path + ) + response in 1:terminal_count || _fem_error( + :results, basename(path), :response_terminal, + "response terminal $response is out of range"; + run_directory = run.path + ) + basis in 1:terminal_count || _fem_error( + :results, basename(path), :basis_terminal, + "basis terminal $basis is out of range"; + run_directory = run.path + ) + frequency = _parse_real(T, columns[2], path, line_index, run) + isapprox( + frequency, + frequencies[frequency_index]; + rtol = 16eps(T), + atol = zero(T) + ) || _fem_error( + :results, + basename(path), + :frequency_hz, + "frequency $frequency does not match input " * + "$(frequencies[frequency_index]) at index $frequency_index"; + run_directory = run.path + ) + key = (response, basis, frequency_index) + key in seen && _fem_error( + :results, + basename(path), + :indices, + "duplicate row for response/basis/frequency $key"; + run_directory = run.path + ) + push!(seen, key) + real_part = _parse_real(T, columns[5], path, line_index, run) + imaginary_part = _parse_real(T, columns[6], path, line_index, run) + matrix[response, basis, frequency_index] = complex(real_part, imaginary_part) + end + length(seen) == expected || _fem_error( + :results, + basename(path), + :indices, + "one or more response/basis/frequency tuples are missing"; + run_directory = run.path + ) + return matrix +end + +# The completion marker of a run reports a complete scan of every frequency and terminal pair. +function validate(path::String, run::FEMRun, frequency_count::Int, terminal_count::Int) + lines = _nonempty_lines(path, run) + length(lines) == 2 || _fem_error( + :results, + basename(path), + :completion, + "completion marker must contain exactly one data row"; + run_directory = run.path + ) + split(lines[1], '\t'; keepempty = true) == FEM_COMPLETE_HEADER || _fem_error( + :results, + basename(path), + :header, + "malformed completion header"; + run_directory = run.path + ) + columns = split(lines[2], '\t'; keepempty = true) + length(columns) == 6 || _fem_error( + :results, basename(path), :completion, "malformed completion row"; + run_directory = run.path + ) + values = [_parse_integer(column, path, 2, run) for column in columns] + expected_rows = frequency_count * terminal_count * terminal_count + values == [ + frequency_count, + terminal_count, + expected_rows, + expected_rows, + expected_rows, + 1 + ] || _fem_error( + :results, + basename(path), + :completion, + "completion marker reports an incomplete or failed scan: $values"; + run_directory = run.path + ) + return path +end + +function _expected_map_paths(run::FEMRun, frequency_count::Int, terminal_count::Int; + physics::Symbol=Symbol("quasi-tem")) + return [joinpath( + run.path, + "maps", + @sprintf("%s_f%04d_b%04d.pos", quantity, frequency, basis) + ) + for frequency in 1:frequency_count + for basis in 1:terminal_count + for quantity in _field_quantities(physics)] +end + +# The field maps of a run: every expected map exists, and none is emitted when disabled. +function validate(maps::Vector{String}, run::FEMRun, enabled::Bool) + missing = filter(!isfile, maps) + isempty(missing) || _fem_error( + :results, + "field_maps", + :files, + "missing $(length(missing)) expected field-map files"; + run_directory = run.path + ) + if !enabled + maps_directory = joinpath(run.path, "maps") + emitted = isdir(maps_directory) ? + filter( + path -> endswith(path, ".pos"), readdir(maps_directory; join = true) + ) : String[] + isempty(emitted) || _fem_error( + :results, + "field_maps", + :files, + "field maps were emitted while plot_field_maps=false"; + run_directory = run.path + ) + end + return maps +end + +function _parse_scan( + run::FEMRun, + model::FEMResolvedModel{T}, + formulation::LineCableModelsFEM, + execution::ComputationOptions +) where {T <: Real} + raw = joinpath(run.path, "raw") + terminal_count = length(model.terminal_ids) + frequency_count = length(model.problem.frequencies) + validate(joinpath(raw, "scan_complete.tsv"), run, frequency_count, terminal_count) + Z = _parse_raw_matrix( + T, + joinpath(raw, "Z.tsv"), + model.problem.frequencies, + terminal_count, + run + ) + P = _parse_raw_matrix( + T, + joinpath(raw, "P.tsv"), + model.problem.frequencies, + terminal_count, + run + ) + enabled = execution.data.plot_field_maps + maps = validate(enabled ? _expected_map_paths(run, frequency_count, terminal_count; + physics = formulation.options.data.physics) : String[], run, enabled) + return FEMScan(Z, P, maps) +end + +function _write_scan_checksums(run::FEMRun, scan::FEMScan) + paths = [joinpath(run.path, "raw", name) + for name in ("Z.tsv", "P.tsv", "scan_complete.tsv")] + append!(paths, scan.map_paths) + checksums = Dict(relpath(path, run.path) => bytes2hex(open(sha256, path)) for path in paths) + _write_json_atomic(joinpath(run.path, "raw", "checksums.json"), checksums) + return nothing +end + +# The raw files of a completed scan match the checksum manifest of its run. +function validate(scan::FEMScan, run::FEMRun) + path = joinpath(run.path, "raw", "checksums.json") + checksums = try + JSON3.read(read(path, String)) + catch exception + exception isa Union{SystemError, Base.IOError, ArgumentError} || rethrow() + _fem_error(:results, "completed scan", :checksum, + "cannot read checksum manifest $path: $(sprint(showerror, exception))"; + run_directory=run.path) + end + paths = [joinpath(run.path, "raw", name) + for name in ("Z.tsv", "P.tsv", "scan_complete.tsv")] + append!(paths, scan.map_paths) + checksums isa AbstractDict && length(checksums) == length(paths) && all(paths) do file + key = relpath(file, run.path) + haskey(checksums, key) && checksums[key] == bytes2hex(open(sha256, file)) + end || _fem_error(:results, "completed scan", :checksum, + "completed FEM raw results failed their checksum check; preserved run: $(run.path)"; + run_directory=run.path) + return scan +end + +function _line_parameters( + run::FEMRun, + model::FEMResolvedModel{T}, + formulation::LineCableModelsFEM, + execution::ComputationOptions, + scan::FEMScan{T}, + inputs::NamedTuple; + reused::Bool=false +) where {T <: Real} + options = formulation.options.data + plan = ReductionPlan(model.problem.system.connection_order; options.reduce_bundle, + options.kron_reduction, options.ideal_transposition) + buffers = initialize_buffers(plan, Complex{T}, nothing, plan, (;)) + retained = length(plan.keep) + frequency_count = length(model.problem.frequencies) + Z = Array{Complex{T}, 3}(undef, retained, retained, frequency_count) + Y = similar(Z) + residuals = Vector{T}(undef, frequency_count) + condition_numbers = similar(residuals) + for (index, frequency) in pairs(model.problem.frequencies) + # The extracted coefficient is P = 1/y in m/S. The shared reduction takes + # the charge-based p = sP in m/F and returns y = s/p. + s = Complex{T}(im * (2 * (one(T) * π) * frequency)) + diagnostics = reduce_line_matrices!(view(Z, :, :, index), view(Y, :, :, index), + view(scan.Z, :, :, index), s .* view(scan.P, :, :, index), s, plan, + buffers.reduction, Val(true)) + residuals[index] = diagnostics.residual + condition_numbers[index] = diagnostics.condition_number + end + basis = :pul + if execution.data.output_basis === Val(:total) + Z = Z .* model.problem.system.line_length + Y = Y .* model.problem.system.line_length + basis = :total + end + keep_run = execution.data.keep_run_directory + timing_path = joinpath(run.path, "timing-summary.json") + native_timing = isfile(timing_path) ? JSON3.read(read(timing_path, String), NamedTuple) : (;) + recovered_columns = get(native_timing, :recovered_columns, 0) + record = ( + state=completed, + reused, + recovered_columns, + columns=get(native_timing, :columns, nothing), + factorized_columns=get(native_timing, :factorized_columns, nothing), + run_directory=keep_run ? run.path : nothing, + mesh_source=run.mesh_source, + mesh_fingerprint=run.mesh_fingerprint, + getdp_invocations=run.getdp_invocations, + map_paths=keep_run ? scan.map_paths : String[], + completed_columns=run.completed_columns, + completed_frequencies=run.completed_frequencies + ) + trace = execution.data.trace === Val(true) ? + ( + Z_primitive = scan.Z, + P_primitive = scan.P, + phase_map = copy(model.problem.system.connection_order) + ) : nothing + files=keep_run ? [(path=relpath(joinpath(directory,name),run.path), + source=joinpath(directory,name),sha256=bytes2hex(open(sha256,joinpath(directory,name)))) + for (directory,_,names) in walkdir(run.path) for name in sort(names)] : NamedTuple[] + names=["cable:$(terminal.cable):$(terminal.terminal)" for terminal in model.problem.system.terminal_order] + coordinates=map(plan.indices) do index + phase=model.problem.system.connection_order[index] + members=findall(==(phase),model.problem.system.connection_order) + formulation.options.data.reduce_bundle && phase > 0 && length(members) > 1 ? + "bundle:[" * join(names[members],",") * "]" : names[index] + end + details = ComputationDetails(; + files, coordinates, + formulations = formulation_record(formulation), + fem = ( + run = record, + inputs, + terminal_ids = copy(model.terminal_ids), + reduced_phase_map = plan.phase_map, + inversion_residuals = residuals, + condition_numbers, + primitive = trace + ), + ) + if execution.data.timing + timing = reused || recovered_columns > 0 ? (;) : ( + constraint_seconds=get(native_timing, :constraint_seconds, nothing), + assembly_seconds=get(native_timing, :assembly_seconds, nothing), + solve_seconds=get(native_timing, :solve_seconds, nothing), + output_seconds=get(native_timing, :output_seconds, nothing), + worker_wall_seconds=get(native_timing, :worker_wall_seconds, nothing)) + details = Engine.completion_details(merge(details.data, (; timing))) + end + return LineParameters( + PhaseDomain, + SeriesImpedance(Z; basis), + ShuntAdmittance(Y; basis), + model.problem.frequencies, + details + ) +end + +function _merge_maps!(paths::Vector{String}) + for path in paths + gmsh.merge(path) + end + return nothing +end diff --git a/ext/LineCableModelsGmshExt/voltage_paths.jl b/ext/LineCableModelsGmshExt/voltage_paths.jl new file mode 100644 index 000000000..e818cdb49 --- /dev/null +++ b/ext/LineCableModelsGmshExt/voltage_paths.jl @@ -0,0 +1,139 @@ +# Clip a straight segment to each triangle. Lowest-order edge elements have +# constant tangential component on a straight line, so one midpoint is exact. +function _voltage_path_points(triangles, vertices; active=trues(length(triangles))) + length(active) == length(triangles) || throw(DimensionMismatch("one activity flag per triangle")) + points = NTuple{4,Float64}[] + cross2(a,b) = a[1]*b[2]-a[2]*b[1] + for (p,q) in zip(vertices[1:end-1],vertices[2:end]) + d = q .- p + intervals = Tuple{Float64,Float64,Bool}[] + for (triangle, integrate) in zip(triangles, active) + minimum(v[1] for v in triangle) > max(p[1],q[1]) && continue + maximum(v[1] for v in triangle) < min(p[1],q[1]) && continue + minimum(v[2] for v in triangle) > max(p[2],q[2]) && continue + maximum(v[2] for v in triangle) < min(p[2],q[2]) && continue + lo,hi = 0.0,1.0 + signarea = sign(cross2(triangle[2].-triangle[1],triangle[3].-triangle[1])) + for k in 1:3 + u,v = triangle[k],triangle[mod1(k+1,3)] + edge = v.-u + a,b = signarea*cross2(edge,p.-u),signarea*cross2(edge,d) + if iszero(b) + a < -1e-12*max(sum(abs2,edge),1e-12) && (hi = -1.0) + elseif b > 0 + lo = max(lo,-a/b) + else + hi = min(hi,-a/b) + end + end + if hi-lo > 1e-11 + push!(intervals, (lo, hi, integrate)) + end + end + # The shared tangential trace is unique on a mesh edge. Merge duplicate + # intervals there, but retain every triangle crossing elsewhere. + sort!(intervals) + covered = 0.0 + for (lo, hi, integrate) in intervals + lo <= covered + 1e-8 || error("Voltage path leaves the field domain at $p -> $q") + lo = max(lo, covered) + hi-lo > 1e-11 || continue + mid = p .+ ((lo+hi)/2).*d + weight = (hi-lo).*d + integrate && push!(points, (mid[1],mid[2],weight[1],weight[2])) + covered = hi + end + abs(covered-1) < 1e-8 || error("Incomplete voltage path on $p -> $q") + end + points +end + +# Choose an actual contour node, so endpoints lie on the discretized electrode +# for circular, tubular, stranded, and noncircular terminal geometries alike. +function _voltage_endpoint(model, terminal, nodes) + curves = gmsh.model.get_entities_for_physical_group( + 1, model.tags.terminal_contour_base + terminal) + tags = unique!(reduce(vcat, [first(gmsh.model.mesh.get_nodes(1, curve, true)) + for curve in curves]; init=UInt64[])) + isempty(tags) && error("Terminal $(model.terminal_ids[terminal]) has no contour nodes") + return nodes[argmin(tag -> (nodes[tag][2], nodes[tag][1]), tags)] +end + +function _write_voltage_paths(path, mesh, plan, model; endpoints=nothing, shell_segments=128) + shell_segments >= 2 || throw(ArgumentError("shell_segments must be at least 2")) + previous_model = gmsh.model.get_current() + scratch_model = "LineCableModels-voltage-$(basename(tempname()))" + gmsh.model.add(scratch_model) + try + # Merge into a uniquely named empty model. `gmsh.open` uses the mesh + # filename as the model name, which can collide with a caller-owned model. + gmsh.merge(mesh) + tags, coords, _ = gmsh.model.mesh.get_nodes() + nodes = Dict(tag => (coords[3i-2], coords[3i-1]) for (i, tag) in enumerate(tags)) + triangles = NTuple{3,NTuple{2,Float64}}[] + active = Bool[] + metal_tags = Set(m.physical_tag for m in model.material_plans if m.kind === :conductor) + for (_, surface) in gmsh.model.get_entities(2) + groups = gmsh.model.get_physical_groups_for_entity(2, surface) + integrate = !any(in(metal_tags), groups) + types, _, element_nodes = gmsh.model.mesh.get_elements(2, surface) + for (type, nds) in zip(types, element_nodes) + type == 2 || error("Voltage-path integration requires first-order triangles") + for i in 1:3:length(nds) + push!(triangles, (nodes[nds[i]], nodes[nds[i+1]], nodes[nds[i+2]])) + push!(active, integrate) + end + end + end + endpoints === nothing && (endpoints = [_voltage_endpoint(model, i, nodes) + for i in eachindex(model.terminal_ids)]) + length(endpoints) == length(model.terminal_ids) || + throw(DimensionMismatch("one voltage endpoint per terminal is required")) + allpoints = NTuple{4,Float64}[] + starts, counts = Int[], Int[] + ri, ro = plan.domain_radius, plan.shell_outer_radius + cx, cy = model.center + for (x, y) in endpoints + hypot(x-cx, y-cy) < ri || error("Voltage endpoint must lie in the finite field domain") + dx = x-cx + # Pull back a physical vertical ray from earth infinity through the + # spherical shell: r_phys = ri*(ro-ri)/(ro-r_mesh), t=ri/r_phys. + vertices = [(cx+(ro-(ro-ri)*t)*t*dx/ri, + cy-(ro-(ro-ri)*t)*sqrt(1-(t*dx/ri)^2)) + for t in range(0, 1; length=shell_segments+1)] + push!(vertices, (x, y)) + # The electric model extends v constantly and bt=0 inside each + # equipotential metal. Vertical paths can cross shields + # or other terminals without introducing a transverse voltage there. + points = _voltage_path_points(triangles, vertices; active) + push!(starts, length(allpoints)) + push!(counts, length(points)) + append!(allpoints, points) + end + open(path, "w") do io + println(io, "// Physical vertical voltage paths; zero transverse field inside metal.") + for (name, values) in (("PathStart", starts), ("PathCount", counts), + ("PathX", first.(allpoints)), ("PathY", getindex.(allpoints, 2)), + ("PathDX", getindex.(allpoints, 3)), ("PathDY", last.(allpoints))) + println(io, name, "() = ", _pro_array(values), ";") + end + end + return path + finally + gmsh.model.set_current(scratch_model) + gmsh.model.remove() + isempty(previous_model) || gmsh.model.set_current(previous_model) + end +end + +_voltage_path_file(run::FEMRun, frequency::Int) = + joinpath(run.path, "input", @sprintf("paths-f%04d.pro", frequency)) + +function _write_run_voltage_paths!(run, model, formulation, mesh_paths) + _quasi_full(formulation.options.data.physics) || return nothing + @debug "Writing quasi-full voltage paths" frequencies=length(mesh_paths) + for (mesh, plan) in zip(mesh_paths, model.mesh_plans) + _write_voltage_paths(_voltage_path_file(run, plan.frequency_index), mesh, plan, model) + end + return nothing +end diff --git a/ext/LineCableModelsGmshExt/workers.jl b/ext/LineCableModelsGmshExt/workers.jl new file mode 100644 index 000000000..f032778bc --- /dev/null +++ b/ext/LineCableModelsGmshExt/workers.jl @@ -0,0 +1,552 @@ +""" + FEMFrequencyJob + +Immutable process inputs for one mesh, frequency and requested terminal list. +""" +struct FEMFrequencyJob + "Position in the input frequency vector." + frequency_index::Int + "Physical frequency \\[Hz\\]." + frequency::Float64 + "Terminal columns requested from this process." + bases::Vector{Int} + "Exclusive attempt directory." + directory::String + "Digest of the mesh used by this attempt." + mesh_digest::String + "Standalone GetDP command with its own environment and working directory." + command::Cmd +end + +struct FEMActiveWorker{P} + job::FEMFrequencyJob + process::P + pid::Int + process_token::Union{Nothing, String} + log::IOStream + "Process start timestamp \\[s\\] since the Unix epoch." + started_at::Float64 + "Monotonic process start tick \\[ns\\]." + start_tick::UInt64 + pending::Set{Int} +end + +_column_stem(frequency::Int, basis::Int) = @sprintf("getdp-f%04d-b%04d", frequency, basis) + +function _column_paths(root::String, frequency::Int, basis::Int, maps::Bool; + physics::Symbol=Symbol("quasi-tem")) + stem = _column_stem(frequency, basis) + raw = _job_raw_paths(root, stem) + return (; raw..., + diagnostics = _quasi_full(physics) ? + [joinpath(root, "raw", "jobs", "$stem-Pscalar.tsv")] : String[], + timing = joinpath(root, "raw", "jobs", "$stem-timing.tsv"), + marker = joinpath(root, "raw", "jobs", "$stem.done"), + checkpoint = joinpath(root, "raw", "jobs", "$stem.json"), + maps = maps ? + [joinpath(root, "maps", + @sprintf("%s_f%04d_b%04d.pos", quantity, frequency, basis)) + for quantity in _field_quantities(physics)] : String[]) +end + +_column_files(paths) = [paths.Z, paths.P, paths.timing, paths.maps..., paths.diagnostics...] + +function _valid_column_marker(path, frequency_index, frequency, basis, terminals, maps) + isfile(path) || return false + text = read(path, String) + endswith(text, '\n') || return false + rows = split(strip(text), '\n') + length(rows) == 1 || return false + fields = split(only(rows), '\t') + length(fields) == 6 || return false + values = tryparse.(Float64, fields) + any(isnothing, values) && return false + return values[[1, 2, 4, 5, 6]] == [2, frequency_index, basis, terminals, Int(maps)] && + isapprox(values[3], frequency; rtol = 16eps(Float64), atol = 0) +end + +function _column_timing(path, frequency_index, basis) + isfile(path) || return nothing + rows = readlines(path) + length(rows) == 1 || return nothing + values = tryparse.(Float64, split(only(rows), '\t')) + length(values) == 7 && all(v -> v !== nothing && isfinite(v), values) || return nothing + values[1:2] == [frequency_index, basis] || return nothing + all(>=(0), values[3:6]) && values[7] in (0, 1) || return nothing + return (constraint_seconds = values[3], assembly_seconds = values[4], + solve_seconds = values[5], output_seconds = values[6], factorized = Bool(values[7])) +end + +function _complete_map(path) + isfile(path) && filesize(path) > 3 || return false + return open(path) do io + seek(io, max(0, filesize(path) - 128)) + endswith(strip(read(io, String)), "};") + end +end + +function _valid_column_checkpoint( + root, frequency_index, frequency, basis, terminals, maps, mesh_digest; + physics::Symbol=Symbol("quasi-tem")) + paths = _column_paths(root, frequency_index, basis, maps; physics) + isfile(paths.checkpoint) || return false + record = try + JSON3.read(read(paths.checkpoint, String)) + catch exception + exception isa Union{SystemError, Base.IOError, ArgumentError} || rethrow() + return false + end + record isa AbstractDict || return false + for (key, expected) in pairs((protocol=2, frequency_index=frequency_index, + basis=basis, terminals=terminals)) + value = get(record, key, nothing) + value isa Integer && !(value isa Bool) && value == expected || return false + end + get(record, :physics, "quasi-tem") == String(physics) && + get(record, :plot_field_maps, nothing) === maps && + get(record, :mesh_digest, nothing) == mesh_digest || return false + recorded_frequency = get(record, :frequency_hz, nothing) + recorded_frequency isa Real && !(recorded_frequency isa Bool) && + isfinite(recorded_frequency) && + isapprox(recorded_frequency, frequency; rtol = 16eps(Float64), atol = 0) || + return false + checksums = get(record, :checksums, nothing) + checksums isa AbstractDict || return false + return try + _valid_job_raw(paths.Z, terminals, frequency_index, frequency, basis) && + _valid_job_raw(paths.P, terminals, frequency_index, frequency, basis) || + return false + _column_timing(paths.timing, frequency_index, basis) === nothing && return false + files = _column_files(paths) + length(checksums) == length(files) || return false + all(files) do file + key = relpath(file, root) + isfile(file) && haskey(checksums, key) && + checksums[key] == bytes2hex(open(sha256, file)) + end + catch exception + exception isa Union{SystemError, Base.IOError} || rethrow() + false + end +end + +function _copy_column_file(source, destination) + mkpath(dirname(destination)) + temporary = tempname(dirname(destination)) + try + cp(source, temporary) + mv(temporary, destination; force = true) + finally + rm(temporary; force = true) + end +end + +function _adopt_column!( + run, source, frequency_index, frequency, basis, terminals, maps, mesh_digest; + physics::Symbol=Symbol("quasi-tem")) + paths = _column_paths(source, frequency_index, basis, maps; physics) + _valid_column_marker( + paths.marker, frequency_index, frequency, basis, terminals, maps) || return false + _valid_job_raw(paths.Z, terminals, frequency_index, frequency, basis) && + _valid_job_raw(paths.P, terminals, frequency_index, frequency, basis) || return false + all(path -> _valid_job_raw(path, terminals, frequency_index, frequency, basis), + paths.diagnostics) || return false + timing = _column_timing(paths.timing, frequency_index, basis) + timing === nothing && return false + all(_complete_map, paths.maps) || return false + destination = _column_paths(run.path, frequency_index, basis, maps; physics) + for (input, output) in zip(_column_files(paths), _column_files(destination)) + _copy_column_file(input, output) + end + checksums = Dict(relpath(file, run.path) => bytes2hex(open(sha256, file)) + for file in _column_files(destination)) + _write_json_atomic(destination.checkpoint, + (; protocol = 2, frequency_index, frequency_hz = frequency, basis, terminals, + plot_field_maps = maps, physics, mesh_digest, timing, checksums, + attempt = relpath(source, run.path))) + return true +end + +function _getdp_command(executable, model_path, mesh_path, run, formulation, execution, mesh_plan, + bases, directory; reuse_factorization = true) + basis_path = joinpath(directory, "bases.pro") + write(basis_path, "RequestedBases() = $(_pro_array(bases));\n") + verbosity = execution.data.getdp_verbosity + arguments = [executable, model_path, "-solve", "LineCableModelsFEMScan", + "-setnumber", "Physics", string(_fem_physics_code(formulation)), + "-msh", abspath(mesh_path), "-name", joinpath(directory, "solver"), + "-v", string(verbosity), + "-setstring", "ModelDataPath", joinpath(run.path, "input", "model_data.pro"), + "-setstring", "RunDirectory", directory, + "-setstring", "BasisListPath", basis_path, + "-setnumber", "FrequencyIndex", string(mesh_plan.frequency_index), + "-setnumber", "FrequencyHz", _pro_number(mesh_plan.frequency), + "-setnumber", "Val_Rint", _pro_number(mesh_plan.domain_radius), + "-setnumber", "Val_Rext", _pro_number(mesh_plan.shell_outer_radius), + "-setnumber", "PlotFieldMaps", string(Int(execution.data.plot_field_maps)), + "-setnumber", "ReuseFactorization", string(Int(reuse_factorization))] + if _quasi_full(formulation.options.data.physics) + append!(arguments, ["-setstring", "PathDataPath", + _voltage_path_file(run, mesh_plan.frequency_index)]) + end + if verbosity >= 4 + append!(arguments, [ + "-cpu", "-ksp_view", "-log_view", ":" * joinpath(directory, "petsc.log")]) + end + threads = string(execution.data.solver_threads) + return addenv(Cmd(Cmd(arguments); dir = directory), "OPENBLAS_NUM_THREADS" => threads, + "OPENBLAS_DEFAULT_NUM_THREADS" => threads, + "OMP_NUM_THREADS" => threads, "MKL_NUM_THREADS" => threads) +end + +function _frequency_job( + run, model, formulation, execution, executable, mesh, mesh_digest, frequency_index, + bases; reuse_factorization = true) + isempty(bases) && throw(ArgumentError("a frequency job requires at least one terminal")) + all(b -> b in eachindex(model.terminal_ids), bases) && allunique(bases) || + throw(ArgumentError("frequency job terminal indices must be distinct and in range")) + root = joinpath(run.path, "attempts") + mkpath(root) + directory = mktempdir(root; prefix = @sprintf("f%04d-", frequency_index), cleanup = false) + for subdirectory in ("raw/jobs", "maps") + mkpath(joinpath(directory, subdirectory)) + end + plan = model.mesh_plans[frequency_index] + command = _getdp_command( + executable, _getdp_assets(joinpath(run.path, "input", "getdp")).model, + mesh, run, formulation, execution, plan, bases, directory; reuse_factorization) + return FEMFrequencyJob(frequency_index, Float64(plan.frequency), collect(bases), + directory, mesh_digest, command) +end + +function _attempt_record(job; kwargs...) + return (; + protocol = 2, frequency_index = job.frequency_index, frequency_hz = job.frequency, + requested_bases = job.bases, mesh_digest = job.mesh_digest, command = collect(job.command), kwargs...) +end + +function _start_worker!(run, job, execution) + log = open(joinpath(job.directory, "getdp.log"), "w") + started_at, start_tick = time(), time_ns() + process = try + Base.run(pipeline(job.command; stdout = log, stderr = log); wait = false) + catch + close(log) + rethrow() + end + try + pid = try + Int(getpid(process)) + catch exception + exception isa Base.IOError && + unsafe_string(ccall(:uv_err_name, Cstring, (Cint,), exception.code)) == "ESRCH" || + rethrow() + 0 # A process can exit before its handle's PID is queried. + end + process_token = _process_token(pid) + worker = FEMActiveWorker( + job, process, pid, process_token, log, started_at, start_tick, Set(job.bases)) + _write_json_atomic(joinpath(job.directory, "attempt.json"), + _attempt_record( + job; state = "running", pid, process_token, started_unix_seconds = started_at, + solver_threads = execution.data.solver_threads)) + run.getdp_invocations += 1 + _transition!(run, running, "frequency $(job.frequency_index) launched") + return worker + catch + process_running(process) && kill(process, 9) # SIGKILL, accepted by the public kill(process, signal) API + wait(process) + close(log) + rethrow() + end +end + +function _record_progress!(run, valid) + run.completed_columns = count(valid) + run.completed_frequencies = count(all, eachcol(valid)) + _transition!(run, running, + "$(run.completed_columns)/$(length(valid)) terminal columns validated") +end + +function _collect_worker_columns!(run, worker, valid, maps; physics::Symbol=Symbol("quasi-tem")) + job = worker.job + for basis in sort!(collect(worker.pending)) + if _adopt_column!(run, job.directory, job.frequency_index, job.frequency, + basis, size(valid, 1), maps, job.mesh_digest; physics) + valid[basis, job.frequency_index] = true + delete!(worker.pending, basis) + _record_progress!(run, valid) + end + end +end + +function _finish_worker!(run, worker, execution; stopped = false) + wait(worker.process) + isopen(worker.log) && close(worker.log) + elapsed = (time_ns() - worker.start_tick) / 1e9 + status = stopped ? "stopped" : + success(worker.process) && isempty(worker.pending) ? "complete" : "failed" + _write_json_atomic(joinpath(worker.job.directory, "attempt.json"), + _attempt_record(worker.job; state = status, pid = worker.pid, + process_token = worker.process_token, + started_unix_seconds = worker.started_at, elapsed_seconds = elapsed, + solver_threads = execution.data.solver_threads, + exit_code = worker.process.exitcode, signal = worker.process.termsignal, + completed_bases = setdiff(worker.job.bases, collect(worker.pending)))) + open(joinpath(run.path, "logs", "getdp.log"), "a") do io + println(io, + "frequency=$(worker.job.frequency_index) state=$status elapsed_seconds=$elapsed " * + "exit_code=$(worker.process.exitcode) attempt=$(relpath(worker.job.directory, run.path))") + if status != "complete" + println(io, _log_tail(readlines(joinpath(worker.job.directory, "getdp.log")))) + end + end + return elapsed +end + +function _stop_workers!(run, active, valid, formulation, execution) + errors = Any[] + for worker in active + try + process_running(worker.process) && kill(worker.process) + catch exception + push!(errors, exception) + end + end + for worker in active + try + if timedwait(() -> process_exited(worker.process), 2.0; pollint = 0.02) === + :timed_out + kill(worker.process, 9) + end + wait(worker.process) + _collect_worker_columns!(run, worker, valid, execution.data.plot_field_maps; + physics=formulation.options.data.physics) + _finish_worker!(run, worker, execution; stopped = true) + catch exception + push!(errors, exception) + finally + isopen(worker.log) && close(worker.log) + end + end + empty!(active) + for exception in errors + @warn "Error while stopping a FEM worker" exception + end +end + +# A Linux PID can be reused after a crash. Include boot and process start time +# so a later unrelated process does not prevent recovery of this attempt. +function _process_token(pid::Integer) + pid > 0 && Sys.islinux() || return nothing + stat, boot = try + (read("/proc/$pid/stat", String), read("/proc/sys/kernel/random/boot_id", String)) + catch exception + exception isa Union{SystemError, Base.IOError} || rethrow() + return nothing + end + fields = split(last(split(stat, ") "; limit = 2))) + length(fields) >= 20 && !isempty(strip(boot)) || return nothing + return strip(boot) * ":" * fields[20] +end + +# The first attempt of `run` whose GetDP process is still alive, as `(pid, directory)`, +# or `nothing`. +function _live_attempt(run) + root = joinpath(run.path, "attempts") + isdir(root) || return nothing + for directory in filter(isdir, readdir(root; join = true)) + record = try + JSON3.read(read(joinpath(directory, "attempt.json"), String)) + catch + continue + end + record isa AbstractDict || continue + get(record, :state, "") == "running" || continue + pid = get(record, :pid, 0) + pid isa Integer && pid > 0 || continue + status = ccall(:uv_kill, Cint, (Cint, Cint), pid, 0) + # Query libuv's documented error name instead of Julia's private UV constant. + alive = iszero(status) || + unsafe_string(ccall(:uv_err_name, Cstring, (Cint,), status)) != "ESRCH" + token = get(record, :process_token, nothing) + current = _process_token(Int(pid)) + alive && (token === nothing || current === nothing || token == current) || continue + return (; pid, directory) + end + return nothing +end + +function _recover_columns!(run, model, maps, mesh_digests; physics::Symbol=Symbol("quasi-tem")) + valid = falses(length(model.terminal_ids), length(model.problem.frequencies)) + for frequency in axes(valid, 2), basis in axes(valid, 1) + + valid[basis, frequency] = _valid_column_checkpoint(run.path, frequency, + model.problem.frequencies[frequency], basis, size(valid, 1), maps, mesh_digests[frequency]; physics) + end + root = joinpath(run.path, "attempts") + if isdir(root) + for directory in sort!(filter(isdir, readdir(root; join = true))) + manifest = joinpath(directory, "attempt.json") + record = try + JSON3.read(read(manifest, String)) + catch + continue + end + record isa AbstractDict || continue + frequency = get(record, :frequency_index, nothing) + frequency isa Integer && frequency in axes(valid, 2) && + get(record, :protocol, 0) == 2 && + get(record, :mesh_digest, "") == mesh_digests[frequency] || continue + for basis in axes(valid, 1) + valid[basis, frequency] && continue + valid[basis, frequency] = _adopt_column!(run, directory, frequency, + model.problem.frequencies[frequency], basis, size(valid, 1), maps, mesh_digests[frequency]; physics) + end + end + end + _record_progress!(run, valid) + return valid +end + +function _assemble_columns!(run, model) + for quantity in (:Z, :P) + destination = joinpath(run.path, "raw", "$quantity.tsv") + temporary = tempname(dirname(destination)) + try + open(temporary, "w") do io + println(io, join(FEM_RAW_HEADER, '\t')) + for frequency in eachindex(model.problem.frequencies), + basis in eachindex(model.terminal_ids) + + paths = _job_raw_paths(run, _column_stem(frequency, basis)) + write(io, read(getproperty(paths, quantity))) + end + end + mv(temporary, destination; force = true) + finally + rm(temporary; force = true) + end + end + _write_scan_completion!(run, model) + return nothing +end + +function _run_getdp!(run::FEMRun, model::FEMResolvedModel, formulation::LineCableModelsFEM, + execution::ComputationOptions, mesh_paths::AbstractVector{<:AbstractString}; pump = () -> true, + reuse_factorization::Bool = true, batch_terminals::Bool = true) + length(mesh_paths) == length(model.problem.frequencies) || + throw(DimensionMismatch("one FEM mesh path is required per frequency")) + executable = _resolve_getdp(execution, run) + assets = _getdp_assets(joinpath(run.path, "input", "getdp")) + all(isfile, assets) || _fem_error(:getdp, "GetDP", :assets, + "one or more retained solver sources are missing"; run_directory = run.path) + mesh_digests = [bytes2hex(open(sha256, path)) for path in mesh_paths] + valid = _recover_columns!(run, model, execution.data.plot_field_maps, mesh_digests; + physics=formulation.options.data.physics) + recovered_columns=count(valid) + pending = Tuple{Int, Vector{Int}}[] + for frequency in axes(valid, 2) + bases = findall(!, valid[:, frequency]) + isempty(bases) && continue + if batch_terminals + push!(pending, (frequency, bases)) + else + append!(pending, [(frequency, [basis]) for basis in bases]) + end + end + active = FEMActiveWorker[] + next_job = 1 + worker_wall_seconds=0.0 + progress = LineCableModels.verbosity(execution, :progress) > 0 + started = progress ? time_ns() : UInt64(0) + previous = started + last_log = started + previous_columns = recovered_columns + average_seconds = 0.0 + try + while next_job <= length(pending) || !isempty(active) + pump() || _fem_error(:canceled, "GetDP", :ui, + "FEM solve canceled; completed terminal columns are retained"; run_directory = run.path) + while next_job <= length(pending) && + length(active) < execution.data.frequency_workers + frequency, bases = pending[next_job] + job = _frequency_job( + run, model, formulation, execution, executable, mesh_paths[frequency], + mesh_digests[frequency], frequency, bases; reuse_factorization) + push!(active, _start_worker!(run, job, execution)) + next_job += 1 + end + for index in reverse(eachindex(active)) + worker = active[index] + _collect_worker_columns!(run, worker, valid, execution.data.plot_field_maps; + physics=formulation.options.data.physics) + process_exited(worker.process) || continue + # The last marker can arrive between the poll above and exit. + _collect_worker_columns!(run, worker, valid, execution.data.plot_field_maps; + physics=formulation.options.data.physics) + duration = _finish_worker!(run, worker, execution) + worker_wall_seconds += duration + deleteat!(active, index) + if !success(worker.process) || !isempty(worker.pending) + tail = _log_tail(readlines(joinpath(worker.job.directory, "getdp.log"))) + capability = occursin(r"(?i)(unknown|syntax error).*?(GenerateRHS|SolveAgain)", tail) + requirement = capability ? + "This backend requires GetDP with GenerateRHSGroup and SolveAgain support. " : + "" + _fem_error(:getdp, "GetDP", capability ? :capability : :client, + requirement * + "GetDP frequency $(worker.job.frequency_index) failed " * + "(exit $(worker.process.exitcode)); missing columns $(sort!(collect(worker.pending))). " * + "Attempt: $(worker.job.directory)\nGetDP log tail:\n$tail"; run_directory = run.path) + end + end + if progress && run.completed_columns > previous_columns + now = time_ns() + interval = (now - previous) * 1e-9 / (run.completed_columns - previous_columns) + average_seconds = previous_columns == recovered_columns ? interval : + 0.2 * interval + 0.8 * average_seconds + previous = now + previous_columns = run.completed_columns + if now - last_log >= 5_000_000_000 + @info "FEM frequency sweep progress" _group=:progress completed_columns=run.completed_columns total_columns=length(valid) completed_frequencies=run.completed_frequencies total_frequencies=size(valid,2) elapsed_seconds=(now-started)*1e-9 eta_hours=(length(valid)-run.completed_columns)*average_seconds/3600 + last_log = now + end + end + isempty(active) || sleep(0.02) + end + catch exception + _stop_workers!(run, active, valid, formulation, execution) + if exception isa InterruptException || + exception isa LineCableModelsFEMError && exception.category === :canceled + _transition!(run, canceled, "solver processes stopped; completed columns retained") + end + rethrow() + finally + isempty(active) || _stop_workers!(run, active, valid, formulation, execution) + end + all(valid) || _fem_error(:getdp, "GetDP", :raw_output, + "frequency scan returned without all terminal columns"; run_directory = run.path) + _assemble_columns!(run, model) + timings=[_column_timing(_column_paths(run.path,frequency,basis, + execution.data.plot_field_maps).timing,frequency,basis) + for frequency in axes(valid,2) for basis in axes(valid,1)] + totals=map((:constraint_seconds,:assembly_seconds,:solve_seconds,:output_seconds)) do key + sum(getproperty(timing,key) for timing in timings) + end + _write_json_atomic(joinpath(run.path,"timing-summary.json"), + (;schema=1,backend="getdp",scope="accumulated native worker wall time; not elapsed scan time", + constraint_seconds=totals[1],assembly_seconds=totals[2],solve_seconds=totals[3], + output_seconds=totals[4],columns=length(timings),recovered_columns, + worker_wall_seconds, + worker_wall_scope="sum of newly executed process wall durations in this invocation", + factorized_columns=count(timing->timing.factorized,timings))) + return nothing +end + +function _run_getdp!(run::FEMRun, model::FEMResolvedModel, formulation::LineCableModelsFEM, + execution::ComputationOptions, mesh_path::String; kwargs...) + length(model.problem.frequencies) == 1 || throw(DimensionMismatch( + "a single FEM mesh path is valid only for a one-frequency problem")) + return _run_getdp!(run, model, formulation, execution, [mesh_path]; kwargs...) +end diff --git a/ext/LineCableModelsMakieExt/LineCableModelsMakieExt.jl b/ext/LineCableModelsMakieExt/LineCableModelsMakieExt.jl new file mode 100644 index 000000000..3dc20d8e9 --- /dev/null +++ b/ext/LineCableModelsMakieExt/LineCableModelsMakieExt.jl @@ -0,0 +1,139 @@ +""" + LineCableModelsMakieExt + +Add compact high-level LineCableModels plotting methods to native Makie. +""" +module LineCableModelsMakieExt + +import LineCableModels +using LineCableModels: EarthLayer, Material, RadialDielectric, + SeriesImpedance, ShuntAdmittance, label, nominal, + observables, outer_radius +import Makie +using Makie: Auto, Axis, Button, Colorbar, DataAspect, Figure, + Fixed, GridLayout, Label, Legend, LineElement, + Mixed, + Observable, Outside, Rect2f, Relative, Theme, Toggle, + colgap!, colsize!, content, errorbars!, + hlines!, hspan!, lift, lines!, + off, on, onany, poly!, reset_limits!, rowgap!, + rowsize!, scatter!, stairs!, text!, to_value, translate!, update!, widths, + with_theme +using Printf: @sprintf +using Colors: HSV, Oklab, RGB, RGBA, blue, green, red +import Dates +import Base: resize! +using Statistics: mean + +import LineCableModels.Units +import LineCableModels.Engine +import LineCableModels.DataModel +import LineCableModels.Commons +import LineCableModels.ImportExport +import LineCableModels.UQ +import Makie.GridLayoutBase +using Makie.GridLayoutBase: nrows, offsets, with_updates_suspended +import LineCableModels.Commons: + request_identity, validate + +struct _Omitted end +const _omitted = _Omitted() + +function current_backend_symbol() + backend = Makie.current_backend() + backend isa Module || return :none + name = nameof(backend) + name === :CairoMakie && return :cairo + name === :GLMakie && return :gl + name === :WGLMakie && return :wgl + return :unknown +end + +include("recipes/line_data.jl") +include("recipes/comparison_data.jl") +include("material_colors.jl") +include("recipes/preview_types.jl") +include("recipes/preview_data.jl") +include("attributes.jl") +include("series_styles.jl") +include("shell.jl") +include("controls.jl") +include("guides.jl") +include("layout.jl") +include("recipes/line_facets.jl") +include("recipes/preview_render.jl") +include("montecarlo.jl") +include("export_presentation.jl") +include("native_export.jl") + +import LineCableModels.PlotBuilder: plot, preview, show_material_scale + +include("plot.jl") +include("recipes/formulation_comparisons.jl") + +function preview( + design::DataModel.CableDesign; + backend = nothing, + display_plot::Bool = true, + controls::Bool = true, + kwargs... +) + return _preview( + design; + backend, + display_plot, + controls, + kwargs... + ) +end + +# The public extension method consumes backend and display choices and forwards the +# remaining preview options unchanged. DataModel retains only detached geometry +# and material attributes. Makie objects and backend state stay here. +function preview( + designs::AbstractVector{<:DataModel.CableDesign}; + backend = nothing, + display_plot::Bool = true, + controls::Bool = true, + kwargs... +) + return _preview( + designs; + backend, + display_plot, + controls, + kwargs... + ) +end + +function preview( + system::DataModel.LineCableSystem; + backend = nothing, + display_plot::Bool = true, + controls::Bool = true, + kwargs... +) + return _preview( + system; + backend, + display_plot, + controls, + kwargs... + ) +end + +function show_material_scale( + ; backend = nothing, + display_plot::Bool = true, + controls::Bool = true, + kwargs... +) + return _material_scale(; + backend, + display_plot, + controls, + kwargs... + ) +end + +end # module LineCableModelsMakieExt diff --git a/ext/LineCableModelsMakieExt/attributes.jl b/ext/LineCableModelsMakieExt/attributes.jl new file mode 100644 index 000000000..af4c0c423 --- /dev/null +++ b/ext/LineCableModelsMakieExt/attributes.jl @@ -0,0 +1,9 @@ +# Native renderer attributes follow the same semantic groups as the legends. +function _series_attributes(attributes, count::Integer) + attributes === nothing && return fill((;), count) + attributes isa NamedTuple && return fill(attributes, count) + attributes isa Union{Tuple, AbstractVector} && length(attributes) == count && + all(value -> value isa NamedTuple, attributes) || throw(ArgumentError( + "series_attributes must be a NamedTuple for all series, or one NamedTuple per legend group")) + return collect(attributes) +end diff --git a/ext/LineCableModelsMakieExt/controls.jl b/ext/LineCableModelsMakieExt/controls.jl new file mode 100644 index 000000000..64a48e1ce --- /dev/null +++ b/ext/LineCableModelsMakieExt/controls.jl @@ -0,0 +1,242 @@ +# Live axis actions and native toolbar ownership. These operations retain no +# scientific source and subscribe only to the native resources they own. +const _TOOLBAR_STANDARD_CONTROLS = (:reset, :xlog, :ylog, :export_svg, :legend) + +function _panel_axes(p, panel) + panel===nothing && return p.axes + panel=_panel_identity(panel) + haskey(p.plot_state.panel_data, panel) || + throw(ArgumentError("panel $(repr(panel)) is absent from this figure")) + return [p.plot_state.panel_data[panel].axis] +end + +function _numeric_axis(axis, dimension) + index=dimension===:x ? 1 : 2 + return !(axis.aspect[] isa DataAspect) && + getproperty(axis, Symbol(:dim, index, :_conversion))[]===nothing +end + +function LineCableModels.axisscale!(p::LineCableModels.UIPlot, dimension::Symbol, scale; panel = nothing) + dimension in (:x, :y) || throw(ArgumentError("axis dimension must be :x or :y")) + axes=_panel_axes(p, panel) + entries=filter( + entry -> entry.axis in axes && _numeric_axis(entry.axis, dimension), + p.plot_state.axis_bindings) + isempty(entries) && + throw(ArgumentError("no eligible numeric $dimension axis in the selected panels")) + _set_axis!(entries, dimension, scale) + return p +end + +function LineCableModels.resetview!(p::LineCableModels.UIPlot; panel = nothing, x::Bool = true, y::Bool = true) + axes=_panel_axes(p, panel) + for entry in p.plot_state.axis_bindings + entry.axis in axes || continue + entry.reset(; xauto = x, yauto = y) + end + return p +end + +function _detach!(object) + gc=GridLayoutBase.gridcontent(object) + gc===nothing || GridLayoutBase.remove_from_gridlayout!(gc) + return nothing +end + +function _delete_subtree!(grid::GridLayout) + for child in copy(grid.content) + object=child.content + object isa GridLayout ? _delete_subtree!(object) : delete!(object) + end + _detach!(grid) + return nothing +end + +function _belongs_to_slot(object, slot) + current=object + while current!==nothing + current===slot && return true + gc=GridLayoutBase.gridcontent(current) + current=gc===nothing ? nothing : gc.parent + end + return false +end + +function _repack_toolbar!(p) + state=p.plot_state + for (index, key) in enumerate(state.widget_order) + state.shell.toolbar[1, index]=state.widget_bindings[key].slot + colsize!(state.shell.toolbar, index, Auto(true)) + end + GridLayoutBase.trim!(state.shell.toolbar) + return nothing +end + +function _expected_callback_error(error) + error isa Union{ArgumentError, DomainError, BoundsError, + DimensionMismatch, SystemError, Base.IOError} +end + +function _widget!(builder, p, key; event = nothing, callback = nothing, + success = nothing, standard = false) + state=p.plot_state + state.controls_enabled || + throw(ArgumentError("this figure was constructed with controls=false")) + key isa Symbol || throw(ArgumentError("widget keys must be Symbols")) + haskey(p.controls, key) && throw(ArgumentError("widget key $key is already registered")) + standard || key ∉ _TOOLBAR_STANDARD_CONTROLS || + throw(ArgumentError("widget key $key is reserved")) + (event===nothing)==(callback===nothing) || + throw(ArgumentError("event and callback must be supplied together")) + slot=GridLayout(; tellwidth = true, tellheight = true) + state.shell.toolbar[1, length(state.widget_order) + 1]=slot + subscriptions=Any[] + try + control=builder(p, slot[1, 1]) + native_block=!(control isa GridLayout) && hasproperty(control,:blockscene) + (native_block || control isa GridLayout) || + throw(ArgumentError("widget builders must return a native block or GridLayout")) + _belongs_to_slot(control, slot) || + throw(ArgumentError("widget content must belong to its allocated slot")) + if event!==nothing + observable=event(control) + owner=native_block ? control : nothing + if owner===nothing + owner=findfirst(p.figure.content) do block + _belongs_to_slot(block, slot) && + any(propertynames(typeof(block))) do name + getproperty(block, name)===observable + end + end + owner=owner===nothing ? nothing : p.figure.content[owner] + end + scene=owner===nothing ? p.figure.scene : owner.blockscene + push!(subscriptions, on(scene, observable) do value + try + callback(p, value) + success===nothing || (p.status[]=string(success)) + catch error + _expected_callback_error(error) || rethrow() + p.status[]=sprint(showerror, error) + end + return nothing + end) + end + p.controls[key]=control + state.widget_bindings[key]=(; slot, subscriptions, standard) + push!(state.widget_order, key) + _repack_toolbar!(p) + return control + catch + foreach(off, subscriptions) + _delete_subtree!(slot) + GridLayoutBase.trim!(state.shell.toolbar) + rethrow() + end +end + +function LineCableModels.addwidget!(builder, p::LineCableModels.UIPlot, key::Symbol; + event = nothing, callback = nothing, success = nothing) + return _update_figure_layout!(p) do + _widget!(builder, p, key; event, callback, success) + end +end + +function LineCableModels.removewidget!(p::LineCableModels.UIPlot, key::Symbol) + key ∉ _TOOLBAR_STANDARD_CONTROLS || + throw(ArgumentError("standard widget $key cannot be removed")) + haskey(p.plot_state.widget_bindings, key) || + throw(ArgumentError("unknown widget $key")) + return _update_figure_layout!(p) do + owned=pop!(p.plot_state.widget_bindings, key) + foreach(off, owned.subscriptions) + _delete_subtree!(owned.slot) + delete!(p.controls, key) + filter!(!=(key), p.plot_state.widget_order) + _repack_toolbar!(p) + return p + end +end + +function _controls!(p, xsetters, ysetters) + p.plot_state.controls_enabled || return p + if !isempty(p.axes) + _widget!( + (p, slot) -> Button(slot; label = _toolbar_icon(_TOOLBAR_REFRESH_ICON), + width = _TOOLBAR_BUTTON_SIZE, height = _TOOLBAR_BUTTON_SIZE, buttoncolor = _TOOLBAR_BUTTON_BACKGROUND), + p, + :reset; + event = button -> button.clicks, callback = ( + p, _) -> LineCableModels.resetview!(p), + success = "Axis limits reset", standard = true) + end + if Base.get_extension(LineCableModels, :LineCableModelsCairoMakieExt)!==nothing + _widget!( + (p, slot) -> Button(slot; label = _toolbar_icon(_TOOLBAR_SAVE_ICON), + width = _TOOLBAR_BUTTON_SIZE, height = _TOOLBAR_BUTTON_SIZE, buttoncolor = _TOOLBAR_BUTTON_BACKGROUND), + p, + :export_svg; + event = button -> button.clicks, callback = (p, _) -> begin + output=LineCableModels.export_svg(p) + p.status[]="Saved SVG to $output" + end, standard = true) + end + for (dimension, entries) in ((:x, xsetters), (:y, ysetters)) + isempty(entries) && continue + changing=Ref(false) + caption=Ref{Any}(nothing) + key=Symbol(dimension, :log) + toggle=_widget!(p, key; event = control -> control.active, standard = true, + callback = (p, enabled) -> begin + changing[] && return nothing + changing[]=true + try + eligible=filter( + entry -> getproperty(entry.axis, Symbol(dimension, :scale))[] in (identity, log10) || + getproperty(entry.axis, Symbol(dimension, :scale))[] isa Makie.ReversibleScale{_SignedLog10}, + entries) + _set_axis!(eligible, dimension, enabled ? :log10 : :linear) + p.status[]=enabled ? "$dimension-axis logarithmic view" : + "$dimension-axis linear view" + catch + p.controls[key].active[]=!enabled + rethrow() + finally + changing[]=false + end + end) do p, slot + group=GridLayout(slot; halign = :left, tellwidth = true) + result=Toggle(group[1, 1]; active = false) + caption[]=Label(group[1, 2], "log $dimension") + colgap!(group, 4) + result + end + for entry in entries + subscription=on( + toggle.blockscene, getproperty(entry.axis, Symbol(dimension, :scale)); + update = true) do _ + kinds=unique(getproperty(item.axis, Symbol(dimension, :scale))[]===log10 ? + "log" : + getproperty(item.axis, Symbol(dimension, :scale))[]===identity ? + "linear" : + getproperty(item.axis, Symbol(dimension, :scale))[] isa Makie.ReversibleScale{_SignedLog10} ? + "signed log" : "custom" for item in entries) + mode=length(kinds)==1 ? only(kinds) : "mixed" + caption[].text[]=mode=="linear" ? "log $dimension" : "$mode $dimension" + if !changing[] + changing[]=true + try + active=all(kind -> kind in ("log", "signed log"), kinds) + toggle.active[]==active || (toggle.active[]=active) + finally + changing[]=false + end + end + nothing + end + push!(p.plot_state.widget_bindings[key].subscriptions, subscription) + end + end + p.legend===nothing || (p.controls[:legend]=p.legend) + return p +end diff --git a/ext/LineCableModelsMakieExt/export_presentation.jl b/ext/LineCableModelsMakieExt/export_presentation.jl new file mode 100644 index 000000000..3523e7a64 --- /dev/null +++ b/ext/LineCableModelsMakieExt/export_presentation.jl @@ -0,0 +1,169 @@ +function _snapshot_observables!(snapshot, object, names) + for name in names + hasproperty(object, name) || continue + value = getproperty(object, name) + value isa Observable || continue + push!(snapshot, value => value[]) + end + return snapshot +end + +function _set_observable!(snapshot, observable, value) + push!(snapshot, observable => observable[]) + observable[] = value + return observable +end + +function _hide_layout_content!(snapshot, content) + if content isa GridLayout + for entry in content.content + _hide_layout_content!(snapshot, entry.content) + end + elseif hasproperty(content, :blockscene) + _set_observable!(snapshot, content.blockscene.visible, false) + end + return snapshot +end + +function _apply_export_theme!(snapshot, plot::LineCableModels.UIPlot, theme::Symbol) + theme in (:default, :publication) || throw(ArgumentError( + "theme must be :default or :publication", + )) + _snapshot_observables!(snapshot, plot.figure.scene, (:backgroundcolor,)) + plot.figure.scene.backgroundcolor[] = Makie.to_color(:white) + theme === :default && return nothing + + latex_fonts = Makie.theme_latexfonts().attributes[:fonts][] + figure_fonts = plot.figure.scene.theme[:fonts] + for (role, latex_role) in ((:regular, :regular), (:italic, :italic), + (:bold, :bold), (:bold_italic, :bolditalic)) + _set_observable!(snapshot, figure_fonts[role], latex_fonts[latex_role][]) + end + regular = :regular + bold = :bold + for block in plot.figure.content + if block isa Axis + _snapshot_observables!(snapshot, block, + ( + :titlefont, + :xlabelfont, + :ylabelfont, + :xticklabelfont, + :yticklabelfont + )) + block.titlefont[] = bold + block.xlabelfont[] = regular + block.ylabelfont[] = regular + block.xticklabelfont[] = regular + block.yticklabelfont[] = regular + elseif block isa Legend + _snapshot_observables!(snapshot, block, (:labelfont, :titlefont)) + block.labelfont[] = regular + block.titlefont[] = bold + elseif block isa Colorbar + _snapshot_observables!(snapshot, block, (:labelfont, :ticklabelfont)) + block.labelfont[] = regular + block.ticklabelfont[] = regular + elseif block isa Label + _snapshot_observables!(snapshot, block, (:font,)) + block.font[] = block === plot.title ? bold : regular + end + end + return nothing +end + +function _restore_snapshot!(snapshot) + for (observable, value) in Iterators.reverse(snapshot) + observable[] = value + end + return nothing +end + +function _hide_export_controls!(snapshot, p) + shell=p.plot_state.shell + root=shell.root + first_row=1+offsets(root)[1] + last_row=nrows(root)+offsets(root)[1] + saved=( + rows = copy(root.rowsizes), gaps = copy(root.addedrowgaps), offset = first_row) + occupied=Set{Int}() + for object in shell.chrome + gc=GridLayoutBase.gridcontent(object) + gc===nothing && continue + _hide_layout_content!(snapshot, object) + union!(occupied, gc.span.rows) + end + with_updates_suspended(root) do + for row in occupied + rowsize!(root, row, Fixed(0)) + # GridLayoutBase 0.11.2 and 0.11.3 assign different meanings to + # indexed rowgap! calls for offset grids. The stored gaps retain + # one stable order, so change only the entries adjacent to chrome. + gap_after=row-first_row+1 + row>first_row && (root.addedrowgaps[gap_after-1]=Fixed(0)) + row axis.aspect[]===nothing, p.axes) && state.panel_page!==nothing && + _panel_padding!(p) + _fit_frames!(p, frames; canvas) + return write() + finally + _restore_snapshot!(snapshot) + if chrome!==nothing + with_updates_suspended(state.shell.root) do + for (row, value) in enumerate(chrome.rows) + rowsize!(state.shell.root, row+chrome.offset-1, value) + end + state.shell.root.addedrowgaps .= chrome.gaps + end + end + for (axis, alignment, view) in zip(p.axes, alignments, views) + axis.alignmode[]=alignment + axis.targetlimits[]==view || (axis.targetlimits[]=view) + end + empty!(state.frame_padding) + merge!(state.frame_padding, padding) + for record in tracks + for (i, value) in enumerate(record.rows) + rowsize!(record.grid, i+record.row_offset-1, value) + end + for (i, value) in enumerate(record.columns) + colsize!(record.grid, i+record.column_offset-1, value) + end + end + try + _resize_preserving_views!(p, size) + finally + state.fitting_geometry[]=previous_guard + end + end +end diff --git a/ext/LineCableModelsMakieExt/guides.jl b/ext/LineCableModelsMakieExt/guides.jl new file mode 100644 index 000000000..55af5dc93 --- /dev/null +++ b/ext/LineCableModelsMakieExt/guides.jl @@ -0,0 +1,775 @@ +# One current placement for each native guide. Construction and mutations enter +# the same composition operation. Detached guides retain their native edits. +function _guide_position(position;legend=false,main=false) + position===nothing && return nothing + legend && position===:inside && return position + main && position===Val(:content) && return position + position in (:left,:right,:top,:bottom) && return position + position isa Tuple && length(position)==2 && + all(v -> v isa Integer && !(v isa Bool) && v>0,position) && position!=(2,2) && return Int.(position) + throw(ArgumentError("guide position must be a side or positive side-grid slot; (2,2) is reserved for content")) +end + +function _guide_gap(value) + values=value isa Real ? ntuple(_ -> value,4) : value + values isa Tuple && length(values)==4 && all(x -> x isa Real && isfinite(x) && x>=0,values) || + throw(ArgumentError("guide_gap must be nonnegative or a nonnegative (left,right,bottom,top) tuple")) + return Float64.(values) +end + +function _guide_spacing(value,current=(rowgap=12.0,colgap=12.0)) + fields=value isa Real ? (rowgap=value,colgap=value) : value + fields isa NamedTuple && all(key -> key in (:rowgap,:colgap),keys(fields)) || + throw(ArgumentError("guide_spacing must be a nonnegative number or (rowgap=..., colgap=...)")) + result=merge(current,fields) + all(x -> x isa Real && !(x isa Bool) && isfinite(x) && x>=0,values(result)) || + throw(ArgumentError("guide spacing must be finite, nonnegative, and non-Boolean")) + return map(Float64,result) +end + +function _colorbar_group_attributes(attributes,count=nothing) + _guide_attributes(attributes) + for (key,value) in pairs(attributes) + if key===:layout + value===nothing && continue + value isa Tuple && length(value)==2 && + all(x -> x isa Integer && !(x isa Bool) && 0=count || + throw(ArgumentError("colorbar group layout $value cannot hold $count supplied scales")) + elseif key in (:rowgap,:colgap) + value===nothing || _guide_spacing((;key=>value)) + elseif key in (:width,:height) + value isa Real && (!(value isa Bool) && isfinite(value) && value>0) && continue + value===nothing || value isa Union{Auto,Relative} || + throw(ArgumentError("group $key must be a positive native size")) + elseif key in (:tellwidth,:tellheight) + value isa Bool || throw(ArgumentError("$key must be Boolean")) + elseif key ∉ (:halign,:valign,:margin) + throw(ArgumentError("unsupported colorbar group attribute $key")) + end + end + return attributes +end + +# Ordered one-dimensional packing. Explicit spacer tracks include each adjacency +# once. Native gaps on those tracks remain zero. The caller handles overflow. +function _pack_guides(extents,span,gap,fractions;desired=nothing) + isempty(extents) && return (;starts=Float64[],extent=0.0,span=Float64(span)) + suffix=reverse(cumsum(reverse(extents))) + total=first(suffix)+(length(extents)-1)*gap + span=max(span,total) + equal=desired===nothing && all(==(first(fractions)),fractions) + starts=Float64[] + cursor=0.0 + for i in eachindex(extents) + target=desired!==nothing ? desired[i] : equal ? + first(fractions)*(span-total)+(first(suffix)-suffix[i])+(i-1)*gap : + fractions[i]*(span-extents[i]) + upper=span-suffix[i]-(length(extents)-i)*gap + start=clamp(target,cursor,max(cursor,upper)) + push!(starts,start) + cursor=start+extents[i]+gap + end + return (;starts,extent=total,span) +end + +function _guide_attributes(attributes) + attributes isa NamedTuple || throw(ArgumentError("guide attributes must be a NamedTuple")) + if haskey(attributes,:margin) + _guide_gap(to_value(attributes.margin)) + end + for (key,symbols) in ((:halign,(:left,:center,:right)),(:valign,(:bottom,:center,:top))) + haskey(attributes,key) || continue + value=to_value(getproperty(attributes,key)) + value in symbols || value isa Real && 0<=value<=1 || + throw(ArgumentError("$key must be a native alignment symbol or a fraction in [0,1]")) + end + return attributes +end + +function _legend_owner(p,panel) + panel===nothing && return (body=p.plot_state.shell.body,groups=p.plot_state.groups, + order=p.plot_state.order,labels=p.plot_state.labels,bounds=p.plot_state.inside_bbox) + panel=_panel_identity(panel) + haskey(p.plot_state.panel_data,panel) || throw(ArgumentError("panel $(repr(panel)) is absent from this figure")) + data=p.plot_state.panel_data[panel] + return (body=something(data.panel.layout,p.plot_state.shell.body),groups=data.groups,order=data.order,labels=data.labels,bounds=data.axis.scene.viewport) +end + +function _guide_state(kind,scope,position,attributes;max_fraction=0.5,title=nothing) + _guide_position(position;legend=kind===:legend,main=kind===:colorbars) + _guide_attributes(attributes) + return (;kind,scope,position=Ref{Any}(position),attributes=Ref{Any}(attributes), + max_fraction=Ref(_legend_fraction(max_fraction)),title=Ref{Any}(title),placed=Ref{Any}(_omitted),object=Ref{Any}(nothing), + layout=Ref{Any}(nothing),items=Any[],fit=Ref{Any}(nothing),subscriptions=Any[],hidden=Ref(false),visibility=IdDict{Any,Bool}()) +end + +function _native_guide_attributes!(object,attributes) + previous=Pair{Any,Any}[] + try + for (key,value) in pairs(attributes) + key in propertynames(typeof(object)) || throw(ArgumentError("unsupported native guide attribute $key")) + attribute=getproperty(object,key) + push!(previous,attribute=>attribute[]) + attribute[]=value + end + catch + for (attribute,value) in reverse(previous) + attribute[]=value + end + rethrow() + end + return object +end + +# Native attributes of the Makie Legend or Colorbar that a guide builds. +function validate(attributes,type::Type{<:Union{Legend,Colorbar}}) + _guide_attributes(attributes) + for (key,attribute) in pairs(attributes) + key in propertynames(type) || throw(ArgumentError("unsupported native guide attribute $key")) + value=to_value(attribute) + convert(fieldtype(type,key).parameters[1],value) + if key in (:vertical,:labelvisible,:ticklabelsvisible,:tellwidth,:tellheight) + value isa Bool || throw(ArgumentError("$key must be Boolean")) + elseif key in (:width,:height) + if value isa Real + !(value isa Bool) && isfinite(value) && value>0 || + throw(ArgumentError("$key must be a finite positive native dimension")) + else + value===nothing || value isa Union{Auto,Relative} || throw(ArgumentError("invalid native $key")) + end + elseif key in (:labelsize,:ticklabelsize,:titlesize,:spinewidth) + value isa Real && !(value isa Bool) && isfinite(value) && value>=0 || + throw(ArgumentError("$key must be finite and nonnegative")) + elseif type===Legend && key===:orientation + value in (:horizontal,:vertical) || throw(ArgumentError("legend orientation must be :horizontal or :vertical")) + elseif type===Legend && key===:nbanks + value isa Integer && !(value isa Bool) && value>0 || throw(ArgumentError("legend nbanks must be a positive integer")) + elseif endswith(string(key),"color") + Makie.to_color(value) + elseif key===:colormap + Makie.to_colormap(value) + end + end + return attributes +end + +function _hide_guide!(p,state) + state.hidden[] && return nothing + empty!(state.visibility) + if state.layout[]!==nothing + for object in p.figure.content + if _belongs_to_slot(object,state.layout[]) + state.visibility[object]=object.blockscene.visible[] + object.blockscene.visible[]=false + end + end + end + state.layout[]===nothing || _detach!(state.layout[]) + objects=state.kind===:legend ? (state.object[],) : something(state.object[],()) + for object in objects + object===nothing && continue + get!(state.visibility,object,object.blockscene.visible[]) + object.blockscene.visible[]=false + end + state.hidden[]=true + return nothing +end + +function _restore_guide!(state) + state.hidden[] || return nothing + for (object,visible) in state.visibility + object.blockscene.visible[]=visible + end + empty!(state.visibility) + state.hidden[]=false + return nothing +end + +function _guide_extent(state,dimension) + object=state.layout[] + object===nothing && return 0.0 + dimensions=object.layoutobservables.reporteddimensions[] + value=dimensions.inner[dimension] + protrusions=dimensions.outer + extra=dimension==1 ? protrusions.left+protrusions.right : protrusions.bottom+protrusions.top + return Float64(something(value,object.layoutobservables.computedbbox[].widths[dimension])+extra) +end + +function _alignment(value,dimension) + hasproperty(value,:x) && getproperty(value,:x) isa Real && return Float64(value.x) + value isa Real && return Float64(value) + value in (:left,:bottom) && return 0.0 + value in (:right,:top) && return 1.0 + return 0.5 +end + +function _compose_guides!(p) + state=p.plot_state + state.composing_guides[] && return p + state.composing_guides[]=true + views=[axis.targetlimits[] for axis in p.axes] + try + for guide in values(state.guides) + guide.layout[]===nothing || _detach!(guide.layout[]) + if guide.kind===:legend && guide.object[]!==nothing + _detach!(guide.object[]) + end + end + for dock in state.guide_docks + _delete_subtree!(dock) + end + empty!(state.guide_docks) + bodies=Any[state.shell.body] + # Panels start with empty, zero-sized docks. Only a panel that owns a + # guide needs recomposition, including when that guide is now hidden. + # Resetting every other panel repeatedly propagates full native layout + # and text measurements through large matrix figures. + for guide in values(state.guides) + guide.scope===nothing && continue + body=state.panel_data[guide.scope].panel.layout + body===nothing || any(existing -> existing===body,bodies) || push!(bodies,body) + end + for body in bodies + # Keep the reserved 3×3 content scaffold. Remove abandoned extension tracks. + for row in size(body)[1]:-1:4 + GridLayoutBase.deleterow!(body,row) + end + for column in size(body)[2]:-1:4 + GridLayoutBase.deletecol!(body,column) + end + for i in (1,3) + rowsize!(body,i,Fixed(0));colsize!(body,i,Fixed(0)) + end + rowgap!(body,0);colgap!(body,0) + end + placements=Dict{Tuple{Any,Any},Vector{Any}}() + for key in state.guide_order + guide=state.guides[key] + position=guide.position[] + guide.kind===:colorbars && isempty(state.color_scales) && continue + if position===nothing + _hide_guide!(p,guide) + continue + end + _restore_guide!(guide) + guide.kind===:colorbars && guide.object[]!==nothing && (p.colorbars=Any[guide.object[]...]) + guide.kind===:legend && guide.object[]!==nothing && !guide.object[].blockscene.visible[] && continue + if guide.kind===:colorbars && !isempty(guide.items) && !any(item -> item.visible[],guide.items) + _restore_guide!(guide) + _reflow_colorbars!(p,guide) + continue + end + owner=guide.kind===:legend ? _legend_owner(p,guide.scope) : + (body=state.shell.body,bounds=state.inside_bbox) + guide.kind===:legend && !any(group -> haskey(owner.labels,group),owner.order) && continue + if position===:inside + _place_legend!(p,guide,owner,nothing) + continue + end + position===Val(:content) && begin + _place_colorbars!(p,guide,state.shell.canvas[1,1],:horizontal) + continue + end + push!(get!(placements,(owner.body,position),Any[]),guide) + end + for ((body,position),guides) in placements + slot,orientation=_legend_slot(body,position) + rows,columns=orientation===:vertical ? (2length(guides)+1,1) : (1,2length(guides)+1) + # Both complete extents participate in content fitting. Flexible + # construction and resize tracks still resolve the reference allocation. + dock=GridLayout(rows,columns;tellwidth=true,tellheight=true) + slot[]=dock + push!(state.guide_docks,dock) + row,column=_dock_indices(position) + gap=state.guide_gap + if row!=2 + rowsize!(body,row,Auto(true));rowgap!(body,row<2 ? row : row-1,gap[row<2 ? 4 : 3]) + end + if column!=2 + colsize!(body,column,Auto(true));colgap!(body,column<2 ? column : column-1,gap[column<2 ? 1 : 2]) + end + for (index,guide) in enumerate(guides) + target=orientation===:vertical ? dock[2index,1] : dock[1,2index] + if guide.kind===:legend + _place_legend!(p,guide,_legend_owner(p,guide.scope),target;orientation) + else + _place_colorbars!(p,guide,target,orientation) + end + end + dimension=orientation===:vertical ? 2 : 1 + bounds=body===state.shell.body ? state.inside_bbox[] : + state.panel_data[first(guides).scope].axis.scene.viewport[] + span=Float64(bounds.widths[dimension]) + extents=[max(0.,_guide_extent(guide,dimension)) for guide in guides] + anchors=map(guides) do guide + object=guide.kind===:legend ? guide.object[] : guide.layout[] + _alignment(to_value(getproperty(object,dimension==1 ? :halign : :valign)),dimension) + end + # Fixed native tracks retain complete guide extents. Equal anchors + # form one compact stack. Different anchors pack in declaration order. + fractions=dimension==1 ? anchors : 1 .- anchors + gap=dimension==1 ? state.guide_spacing[].colgap : state.guide_spacing[].rowgap + desired=if any(guide -> guide.scope!==nothing && state.panel_data[guide.scope].axis.scene.viewport[]!=bounds,guides) + map(enumerate(guides)) do (index,guide) + local_bounds=guide.scope===nothing ? bounds : state.panel_data[guide.scope].axis.scene.viewport[] + local_span=Float64(local_bounds.widths[dimension]) + local_start=dimension==1 ? Float64(local_bounds.origin[1]-bounds.origin[1]) : + Float64(bounds.origin[2]+bounds.widths[2]-local_bounds.origin[2]-local_bounds.widths[2]) + local_start+fractions[index]*max(0.,local_span-extents[index]) + end + else + nothing + end + packed=_pack_guides(extents,span,gap,fractions;desired) + span=packed.span + getproperty(dock,dimension==1 ? :width : :height)[]=span + assigned=dock.layoutobservables.suggestedbbox[] + free=Float64(assigned.widths[dimension])-span + # A strip wider than its data frame still follows the requested + # alignment, including the negative free space on either side. + anchor=all(==(first(anchors)),anchors) ? first(anchors) : 0.5 + origin=Float64(bounds.origin[dimension])-anchor*max(0.,span-Float64(bounds.widths[dimension])) + alignment=abs(free)<1 ? 0.5 : + clamp((origin-Float64(assigned.origin[dimension]))/free,0.,1.) + getproperty(dock,dimension==1 ? :halign : :valign)[]=alignment + starts=packed.starts + previous=0.0 + for index in eachindex(guides) + spacer=max(0.,starts[index]-previous) + if orientation===:vertical + rowsize!(dock,2index-1,Fixed(spacer));rowsize!(dock,2index,Fixed(extents[index])) + else + colsize!(dock,2index-1,Fixed(spacer));colsize!(dock,2index,Fixed(extents[index])) + end + previous=starts[index]+extents[index] + end + # Account for both ends of the native span. Leaving its final + # track implicit would let GridLayout redistribute free space and + # apply the guide's alignment a second time. + if orientation===:vertical + rowsize!(dock,2length(guides)+1,Fixed(max(0.,span-previous))) + else + colsize!(dock,2length(guides)+1,Fixed(max(0.,span-previous))) + end + rowgap!(dock,0);colgap!(dock,0) + end + for body in bodies + occupied_rows=Set{Int}();occupied_columns=Set{Int}() + for ((owner,position),_) in placements + owner===body || continue + row,column=_dock_indices(position) + push!(occupied_rows,row);push!(occupied_columns,column) + end + for row in 1:size(body)[1] + row==2 || row in occupied_rows || rowsize!(body,row,Fixed(0)) + end + for column in 1:size(body)[2] + column==2 || column in occupied_columns || colsize!(body,column,Fixed(0)) + end + end + for guide in values(state.guides) + if guide.kind===:legend && guide.position[]===:inside && guide.object[]!==nothing && !haskey(guide.attributes[],:bbox) + guide.object[].layoutobservables.suggestedbbox[]=_legend_owner(p,guide.scope).bounds[] + end + end + finally + for (axis,view) in zip(p.axes,views) + axis.targetlimits[]==view || (axis.targetlimits[]=view) + end + state.composing_guides[]=false + end + return p +end + +function _watch_guide!(p,guide,object) + attributes=object isa Colorbar ? (:ticks,:tickformat,:ticklabelsize,:ticklabelrotation, + :ticklabelfont,:label,:labelsize,:labelfont,:labelvisible,:ticklabelsvisible, + :labelpadding,:ticklabelpad,:spinewidth,:flipaxis,:vertical,:width,:height) : + (:labelsize,:labelfont,:titlesize,:titlefont,:padding,:margin,:patchsize,:rowgap,:colgap,:orientation,:nbanks) + append!(guide.subscriptions,_watch_presentation!(p,object,attributes)) + push!(guide.subscriptions,on(object.blockscene,object.layoutobservables.autosize) do _ + _compose_guides!(p) + nothing + end) + if object isa Legend + push!(guide.subscriptions,on(object.blockscene,object.blockscene.visible) do _ + state=p.plot_state + state.composing_guides[] || state.fitting_geometry[] || + _update_figure_layout!(() -> nothing,p) + nothing + end) + end + return object +end + +function _place_legend!(p,guide,owner,target;orientation=:vertical) + position=guide.position[] + if guide.object[]===nothing + if position!==:inside + guide.layout[]=GridLayout() + target[]=guide.layout[] + end + guide.object[],guide.fit[]=_legend!(p.figure,owner.groups,owner.order,owner.labels; + dependent_plots=p.plot_state.dependent_plots,position=guide.position, + attributes=guide.attributes[],max_fraction=guide.max_fraction,title=guide.title[], + inside_bbox=owner.bounds,target=position===:inside ? nothing : guide.layout[][1,1], + target_orientation=orientation,fitting_geometry=p.plot_state.fitting_geometry) + _watch_guide!(p,guide,guide.object[]) + end + legend=guide.object[] + _restore_guide!(guide) + if position===:inside + _detach!(legend) + if !haskey(guide.attributes[],:bbox) + legend.layoutobservables.suggestedbbox[]=owner.bounds[] + end + if guide.placed[]!==position + legend.tellwidth[]=false;legend.tellheight[]=false + end + else + guide.layout[]===nothing && (guide.layout[]=GridLayout()) + target[]=guide.layout[] + guide.layout[][1,1]=legend + if guide.placed[]!==position + guide.layout[].halign[]=position===:right ? :left : position===:left ? :right : :center + guide.layout[].valign[]=position===:top ? :bottom : position===:bottom ? :top : :center + legend.tellwidth[]=get(guide.attributes[],:tellwidth,true) + legend.tellheight[]=get(guide.attributes[],:tellheight,true) + end + end + guide.placed[]=position + guide.fit[]===nothing || guide.fit[]() + if guide.scope===nothing + p.legend=legend + p.plot_state.controls_enabled && (p.controls[:legend]=legend) + else + p.panel_legends[guide.scope]=legend + end + return legend +end + +function _watch_scale_item!(p,guide,item) + _watch_guide!(p,guide,item.bar) + for name in (:vertical,:width,:height,:labelvisible) + push!(guide.subscriptions,on(item.bar.blockscene,getproperty(item.bar,name);priority=2) do value + state=p.plot_state + state.composing_guides[] || state.fitting_geometry[] || begin + if name===:labelvisible + item.labelvisible[]=value + else + getproperty(item.managed,name)[]=false + end + end + nothing + end) + end + push!(guide.subscriptions,on(item.bar.blockscene,item.bar.blockscene.visible) do visible + state=p.plot_state + state.composing_guides[] || state.fitting_geometry[] || visible==item.visible[] || begin + _update_figure_layout!(p) do + item.visible[]=visible + end + end + nothing + end) + return item +end + +function _reflow_colorbars!(p,guide) + grid=guide.layout[] + grid===nothing && return nothing + attributes=p.plot_state.colorbar_group_attributes[] + _colorbar_group_attributes(attributes,length(p.plot_state.color_scales)) + position=guide.position[] + active=filter(item -> item.visible[],guide.items) + requested=get(attributes,:layout,nothing) + columns=requested===nothing ? (position in (:top,:bottom) ? max(1,length(active)) : 1) : Int(requested[2]) + rowgap=something(get(attributes,:rowgap,nothing),p.plot_state.guide_spacing[].rowgap) + colgap=something(get(attributes,:colgap,nothing),p.plot_state.guide_spacing[].colgap) + for item in guide.items + bar=item.bar + if item.managed.vertical[] + vertical=position ∉ (:top,:bottom,Val(:content)) + bar.vertical[]==vertical || (bar.vertical[]=vertical) + end + natural=position===Val(:content) ? Auto() : _COLORBAR_DOCK_LENGTH + # Auto keeps Makie's native `size` as the bar thickness. `nothing` + # instead stretches that dimension across the assigned group track. + for (key,value) in pairs(bar.vertical[] ? (width=Auto(),height=natural) : (width=natural,height=Auto())) + item.managed[key][] && !isequal(getproperty(bar,key)[],value) && (getproperty(bar,key)[]=value) + end + side_label=!bar.vertical[] + bar.labelvisible[]==(!side_label && item.labelvisible[]) || (bar.labelvisible[]=!side_label && item.labelvisible[]) + item.companion.blockscene.visible[]=item.visible[] && side_label && item.labelvisible[] + _detach!(item.layout) + _detach!(bar) + _detach!(item.companion) + if item.visible[] + if side_label && item.labelvisible[] + item.layout[1,1]=item.companion + item.layout[1,2]=bar + else + item.layout[1,1]=bar + end + end + with_updates_suspended(item.layout) do + GridLayoutBase.trim!(item.layout) + rowgap!(item.layout,0);colgap!(item.layout,8) + colsize!(item.layout,1,Auto()) + # Outside grids consume raw child protrusions. Include only the extra + # measured border and text offset. Folding all text into the bar's inner + # size would count native perpendicular decorations twice here. + raw=bar.layoutobservables.protrusions[] + complete=bar.layoutobservables.reporteddimensions[].outer + padding=ntuple(i -> max(0.,getfield(complete,i)-getfield(raw,i)),4) + item.layout.alignmode[]=Outside(padding...) + end + # A detached GridContent retains its parentless placement object. + # Settle its current native sizes before measuring local bar offsets. + item.layout.layoutobservables.suggestedbbox[]=item.layout.layoutobservables.suggestedbbox[] + end + # Label and endpoint tracks are shared down each group column. Keep the + # native bar dimensions. Only their surrounding decoration space expands. + for column in 1:min(columns,length(active)) + members=active[column:columns:end] + compact=filter(item -> item.companion.blockscene.visible[],members) + isempty(compact) && continue + labelwidth=maximum(item.companion.layoutobservables.autosize[][1] for item in compact) + leading=[item.bar.layoutobservables.computedbbox[].origin[1]- + item.bar.layoutobservables.suggestedbbox[].origin[1] for item in compact] + endpoint=maximum(leading) + for (item,before) in zip(compact,leading) + colsize!(item.layout,1,Fixed(labelwidth)) + colgap!(item.layout,8+endpoint-before) + item.layout.layoutobservables.suggestedbbox[]=item.layout.layoutobservables.suggestedbbox[] + end + end + # Complete item boxes are for collision clearance, not alignment anchors. + # Reserve the same measured insets about each bar in a column or row. This + # keeps equal bar edges and baselines aligned despite unequal native decoration. + insets=map(active) do item + frame=item.bar.layoutobservables.computedbbox[] + outer=item.layout.layoutobservables.computedbbox[] + before=frame.origin-outer.origin + after=outer.origin+outer.widths-frame.origin-frame.widths + (before[1],after[1],before[2],after[2]) + end + for (index,item) in enumerate(active) + row,column=cld(index,columns),mod1(index,columns) + shared=ntuple(4) do side + maximum(insets[j][side] for j in eachindex(active) + if side<=2 ? mod1(j,columns)==column : cld(j,columns)==row) + end + padding=item.layout.alignmode[].padding + with_updates_suspended(item.layout) do + item.layout.alignmode[]=Outside(ntuple(side -> + getfield(padding,side)+max(0.,shared[side]-insets[index][side]),4)...) + end + grid[row,column]=item.layout + end + GridLayoutBase.trim!(grid) + grid.default_rowgap=Fixed(rowgap);grid.default_colgap=Fixed(colgap) + rowgap!(grid,rowgap);colgap!(grid,colgap) + for (dimension,key) in enumerate((:width,:height)) + limit=to_value(getproperty(grid,key)) + required=grid.layoutobservables.autosize[][dimension] + limit isa Real && required!==nothing && limit+1to_value(getproperty(guide.object[],name)) for name in propertynames(Legend) if name!==:entrygroups)...) + guide.attributes[]=merge(guide.attributes[],native,attributes) + foreach(off,guide.subscriptions);empty!(guide.subscriptions) + _remove_legend!(guide.object[]) + guide.layout[]===nothing || _delete_subtree!(guide.layout[]) + guide.object[]=nothing;guide.layout[]=nothing;guide.fit[]=nothing + empty!(guide.visibility);guide.hidden[]=false + elseif guide.object[]!==nothing + _native_guide_attributes!(guide.object[],attributes) + end + guide.attributes[]=merge(guide.attributes[],attributes) + guide.position[]=resolved;guide.max_fraction[]=fraction + if title!==_omitted + guide.title[]=title + if guide.object[]!==nothing + entries=last(only(guide.object[].entrygroups[])) + _legend_entries!(guide.object[],title,entries) + end + end + finally + p.plot_state.composing_guides[]=previous_guard + end + guide.fit[]===nothing || guide.fit[]() + _compose_guides!(p) + if resolved===nothing + panel===nothing ? (p.legend=nothing;delete!(p.controls,:legend)) : delete!(p.panel_legends,panel) + end + return panel===nothing ? p.legend : get(p.panel_legends,panel,nothing) +end + +function LineCableModels.figurelegend!(p::LineCableModels.UIPlot;guide_spacing=_omitted,kwargs...) + state=p.plot_state + spacing=guide_spacing===_omitted ? state.guide_spacing[] : _guide_spacing(guide_spacing,state.guide_spacing[]) + # Validate native options and placement before changing the shared setting. + native=(; (key=>value for (key,value) in kwargs if key ∉ (:position,:title,:max_fraction,:legend_labels))...) + validate(native,Legend) + haskey(kwargs,:position) && _guide_position(kwargs[:position];legend=true) + haskey(kwargs,:max_fraction) && _legend_fraction(kwargs[:max_fraction]) + previous=state.guide_spacing[] + try + return _update_figure_layout!(p) do + state.guide_spacing[]=spacing + _update_legend!(p,nothing;kwargs...) + end + catch + state.guide_spacing[]=previous + rethrow() + end +end +LineCableModels.panellegend!(p::LineCableModels.UIPlot,panel;kwargs...)= + _update_figure_layout!(() -> _update_legend!(p,panel;kwargs...),p) + +function LineCableModels.figurecolorbars!(p::LineCableModels.UIPlot;position=_omitted, + group_attributes=_omitted,guide_spacing=_omitted,kwargs...) + state=p.plot_state + guide=state.guides[(:colorbars,nothing)] + resolved=position===_omitted ? guide.position[] : position + _guide_position(resolved;main=position===_omitted) + group_attributes===_omitted || group_attributes isa NamedTuple || + throw(ArgumentError("group_attributes must be a NamedTuple")) + group=_colorbar_group_attributes(group_attributes===_omitted ? state.colorbar_group_attributes[] : + merge(state.colorbar_group_attributes[],group_attributes),length(state.color_scales)) + spacing=guide_spacing===_omitted ? state.guide_spacing[] : _guide_spacing(guide_spacing,state.guide_spacing[]) + attributes=validate((;kwargs...),Colorbar) + previous=(position=guide.position[],attributes=guide.attributes[],group=state.colorbar_group_attributes[], + spacing=state.guide_spacing[],size=Tuple(p.figure.scene.viewport[].widths), + native=guide.layout[]===nothing ? nothing : (; (key=>to_value(getproperty(guide.layout[],key)) for key in + (:width,:height,:halign,:valign,:tellwidth,:tellheight,:alignmode))...)) + saved=map(guide.items) do item + native=(; (key=>to_value(getproperty(item.bar,key)) for key in + unique((keys(attributes)...,:vertical,:width,:height,:labelvisible)))...) + (;item,native,managed=map(x -> x[],item.managed),labelvisible=item.labelvisible[]) + end + try + _update_figure_layout!(p) do + state.composing_guides[]=true + try + state.colorbar_group_attributes[]=group + state.guide_spacing[]=spacing + if guide.layout[]!==nothing && group_attributes!==_omitted + _native_guide_group!(guide.layout[],group_attributes) + end + guide.attributes[]=merge(guide.attributes[],attributes) + for item in guide.items + for key in (:vertical,:width,:height) + haskey(attributes,key) && (item.managed[key][]=false) + end + haskey(attributes,:labelvisible) && (item.labelvisible[]=to_value(attributes.labelvisible)) + _native_guide_attributes!(item.bar,attributes) + end + guide.position[]=resolved + finally + state.composing_guides[]=false + end + end + catch + state.composing_guides[]=true + try + guide.position[]=previous.position;guide.attributes[]=previous.attributes + state.colorbar_group_attributes[]=previous.group;state.guide_spacing[]=previous.spacing + if previous.native!==nothing + for (key,value) in pairs(previous.native) + getproperty(guide.layout[],key)[]=value + end + elseif guide.layout[]!==nothing + foreach(off,guide.subscriptions);empty!(guide.subscriptions) + for item in guide.items + delete!(item.bar);delete!(item.companion) + _detach!(item.layout) + end + _detach!(guide.layout[]) + empty!(guide.items) + guide.layout[]=nothing;guide.object[]=nothing + empty!(p.colorbars) + end + for record in saved + _native_guide_attributes!(record.item.bar,record.native) + for key in keys(record.managed) + record.item.managed[key][]=record.managed[key] + end + record.item.labelvisible[]=record.labelvisible + end + finally + state.composing_guides[]=false + end + _resize_preserving_views!(p,previous.size) + rethrow() + end + resolved===nothing && empty!(p.colorbars) + return p.colorbars +end diff --git a/ext/LineCableModelsMakieExt/layout.jl b/ext/LineCableModelsMakieExt/layout.jl new file mode 100644 index 000000000..35b464ec0 --- /dev/null +++ b/ext/LineCableModelsMakieExt/layout.jl @@ -0,0 +1,650 @@ +# One nominal capacity, independent of quantity and actual residual extent. +function _page_capacity(layout, matrix_positions, flow_counts) + if layout !== nothing + layout isa Tuple && length(layout)==2 && + all(v -> v isa Integer && !(v isa Bool) && v>0, layout) || + throw(ArgumentError("layout must be a tuple of two positive integers or nothing")) + return Int.(layout) + end + if !isempty(matrix_positions) + spans=map(positions -> _panel_footprint(positions).dimensions, matrix_positions) + return (maximum(first, spans), maximum(last, spans)) + end + count=maximum(flow_counts; init = 1) + columns=ceil(Int, sqrt(count)) + return (cld(count, columns), columns) +end + +function _figure_size(fig_size, dimensions) + if fig_size !== nothing + fig_size isa Tuple{Int, Int} && all(>(0), fig_size) || throw(ArgumentError( + "fig_size must be a tuple of two positive integers or nothing", + )) + return fig_size + end + rows, columns = dimensions + return (max(680, 390columns + 180), max(440, 290rows + 100)) +end + +function _panel_footprint(positions) + origin=ntuple(d -> minimum(p -> p[d], positions), 2) + dimensions=ntuple(d -> maximum(p -> p[d], positions)-origin[d]+1, 2) + return (; origin, dimensions) +end + +# The original block identity and the occupied rectangle are independent. +# Explicit capacities use origin (1,1). Automatic overviews use the selected +# minimum coordinate. Empty perimeter tracks disappear, internal holes remain. +function _matrix_pages(positions, extent, capacity; origin = (1, 1)) + all(p -> all(d -> 1<=p[d]<=extent[d], 1:2), positions) || + throw(ArgumentError("selected panel coordinates exceed the retained matrix extent")) + blocks=map(p -> ntuple(d -> fld(p[d]-origin[d], capacity[d])+1, 2), positions) + pages=NamedTuple[] + for index in sort!(unique(blocks)) + members=findall(==(index), blocks) + selected=positions[members] + footprint=_panel_footprint(selected) + local_positions=Tuple(ntuple(d -> p[d]-footprint.origin[d]+1, 2) for p in selected) + push!(pages, (; members, positions = local_positions, footprint..., index)) + end + return pages +end + +function _flow_pages(items, capacity) + _, columns=capacity + pages=NamedTuple[] + for start in 1:prod(capacity):length(items) + selected=items[start:min(start + prod(capacity) - 1, length(items))] + positions=Tuple((cld(i, columns), mod1(i, columns)) for i in eachindex(selected)) + dimensions=(maximum(first, positions), maximum(last, positions)) + push!(pages, (; facets = selected, positions, dimensions, + index = (cld(start, prod(capacity)), 1))) + end + return pages +end + +# Assign retained coordinates and gridpoints to panels and curves, then paginate. +function _observation_facets(plot_data, ydata, orientations, retained, slots, reference_position, labels, + coordinate_labels, coordinate_identities; explicit_labels=false) + facets = NamedTuple[] + for request_index in eachindex(ydata) + product = first(plot_data).observations[request_index] + c = product.coordinates + rows, columns, _ = first(plot_data).coordinates[request_index] + extent=c.kind===:matrix ? + ntuple( + d -> maximum(source.observations[request_index].coordinates.extent[d] + for source in plot_data), + length(c.extent)) : c.extent + orientation=orientations[request_index] + if orientation===:rows + for index in retained[request_index] + source_product=plot_data[index].observations[request_index] + source_coordinates=source_product.coordinates + row_labels=index==1 || explicit_labels ? coordinate_labels[request_index] : + _coordinate_labels(source_product,plot_data[index].coordinates[request_index],orientation) + point=slots[index]==0 ? reference_position : slots[index] + for (local_column, column) in enumerate(columns) + curves=[(source_index=index, local_row, local_column, local_sample=nothing, + slot=row, role=:result, + group=Symbol("row_", row), label=row_labels[local_row], + position=local_row) for (local_row, row) in enumerate(rows)] + push!(facets, (;request_index, local_row=1, local_column, + row=point, column, row_label="", + column_label=get(source_coordinates,:column_labels,source_coordinates.labels)[column], + panel_identity=(point, column), curves, orientation, kind=c.kind, extent, + domain=get(source_coordinates,:domain,:unspecified), + column_domain=get(source_coordinates,:column_domain,nothing), + quantity=product.quantity, identity=request_identity(ydata[request_index]), + source_index=index, source_label=labels[request_index][index])) + end + end + continue + end + coordinates=[(local_row=local_row,local_column=local_column,local_sample=nothing,row, + column=c.kind===:diagonal ? row : column, + row_label=hasproperty(c,:labels) ? c.labels[row] : string(row), + column_label=haskey(c,:column_labels) ? c.column_labels[column] : string(column), + style_slot=c.kind===:matrix ? (row-1)*c.extent[2]+column : + c.kind in (:vector,:diagonal) ? row : ordinal, + position=ordinal) + for (ordinal,(local_row,local_column,row,column)) in enumerate( + (local_row,local_column,row,column) for (local_row,row) in enumerate(rows) + for (local_column,column) in enumerate(columns))] + if orientation===:gridpoints + for coordinate in coordinates + curves=[(source_index=index,local_row=coordinate.local_row, + local_column=coordinate.local_column,local_sample=coordinate.local_sample,slot=slots[index], + role=slots[index]==0 ? :reference : :result, + group=Symbol("result_",index),label=labels[request_index][index]) + for index in retained[request_index]] + panel_identity=c.kind in (:matrix,:diagonal) ? + (coordinate.row,coordinate.column) : + c.kind===:vector ? coordinate.row : coordinate.position + push!(facets,(;request_index,coordinate.local_row,coordinate.local_column, + coordinate.row,coordinate.column,coordinate.row_label,coordinate.column_label, + panel_identity,curves,orientation,kind=c.kind,extent, + domain=get(c,:domain,:unspecified),column_domain=get(c,:column_domain,nothing), + quantity=product.quantity,identity=request_identity(ydata[request_index]))) + end + else + for index in retained[request_index] + source_coordinates=plot_data[index].observations[request_index].coordinates + panel_coordinates=c.kind===:assemblies ? + [begin + slot=findfirst(isequal(source_coordinates.labels[assembly]), + coordinate_identities[request_index]) + (local_row=1,local_column=1,local_sample=ordinal, + style_slot=slot,position=slot) + end + for (ordinal,assembly) in enumerate(source_coordinates.assemblies)] : coordinates + curves=[(source_index=index,local_row=coordinate.local_row, + local_column=coordinate.local_column,local_sample=coordinate.local_sample, + slot=coordinate.style_slot, + role=:result,group=Symbol("coordinate_",coordinate.style_slot), + label=coordinate_labels[request_index][coordinate.position], + position=coordinate.position) + for coordinate in panel_coordinates] + push!(facets,(;request_index,local_row=1,local_column=1, + row=slots[index]==0 ? reference_position : slots[index],column=1, + row_label="",column_label="",panel_identity=slots[index]==0 ? + reference_position : slots[index],curves,orientation, + kind=c.kind,extent,domain=get(c,:domain,:unspecified), + column_domain=get(c,:column_domain,nothing),quantity=product.quantity, + identity=request_identity(ydata[request_index]), + source_label=labels[request_index][index])) + end + end + end + return facets +end + +function _observation_pages(facets, capacities; automatic = false) + pages=NamedTuple[] + for request_index in unique(facet.request_index for facet in facets) + selected=filter(facet -> facet.request_index==request_index, facets) + first_facet=first(selected) + capacity=capacities[request_index] + if first_facet.orientation===:rows + for index in unique(f.source_index for f in selected) + point_panels=filter(f -> f.source_index==index, selected) + append!(pages, (merge(page,(;capacity)) + for page in _flow_pages(point_panels, capacity))) + end + continue + end + if first_facet.kind!==:matrix || first_facet.orientation===:coordinates + append!(pages, (merge(page,(;capacity)) for page in _flow_pages(selected, capacity))) + continue + end + positions=[(f.row, f.column) for f in selected] + origin=automatic ? _panel_footprint(positions).origin : (1, 1) + for page in _matrix_pages(positions, first_facet.extent, capacity; origin) + push!(pages, + (; facets = selected[page.members], positions = page.positions,capacity, + dimensions = page.dimensions, origin = page.origin, index = page.index)) + end + end + return pages +end + +# Equalize actual native panel frames at construction. Native protrusions keep +# their scientific labels and ticks. Only the space allocated around them changes. +# The original alignmode remains available for fresh local measurements after a +# title or guide edit. A caller's subsequent alignmode edit releases this padding. +function _panel_padding!(p; minimum_decoration = (0.0, 0.0, 0.0, 0.0)) + managed=p.plot_state.frame_padding + for (axis, record) in managed + axis.alignmode[]==record.applied && (axis.alignmode[]=record.original) + end + measurements=map(collect(values(p.plot_state.panel_data))) do data + axis=data.axis + data.panel.layout===nothing && return nothing + panel=data.panel.layout.layoutobservables.computedbbox[] + frame=axis.layoutobservables.computedbbox[] + lower=frame.origin-panel.origin + upper=panel.origin+panel.widths-frame.origin-frame.widths + (; axis, decoration = (lower[1], upper[1], lower[2], upper[2])) + end + filter!(!isnothing, measurements) + isempty(measurements) && return (0.0, 0.0, 0.0, 0.0) + maximum_decoration=ntuple(i -> max(minimum_decoration[i], maximum(m -> m.decoration[i], measurements)), 4) + for m in measurements + axis=m.axis + original=haskey(managed, axis) ? managed[axis].original : axis.alignmode[] + # Respect constructor and native alignmode choices beyond ordinary Inside. + axis.alignmode[] isa GridLayoutBase.Inside || continue + native=axis.layoutobservables.protrusions[] + values=ntuple(i -> Float32(getfield(native, i)+max(0.0, maximum_decoration[i]-m.decoration[i])), 4) + applied=Mixed(; + (key=>GridLayoutBase.Protrusion(value) + for (key, value) in zip((:left, :right, :bottom, :top), values))...) + axis.alignmode[]=applied + managed[axis]=(; original, applied) + end + return maximum_decoration +end + +# Measure the physical frame before native integer-pixel rounding. The allocated +# Axis rectangle can include unused space imposed by DataAspect/AxisAspect. +function _frame_size(axis, allocation = Tuple(axis.layoutobservables.computedbbox[].widths)) + w, h=Float64.(allocation) + aspect=axis.aspect[] + ratio=aspect isa DataAspect ? + Float64(axis.finallimits[].widths[1]/axis.finallimits[].widths[2]) : + aspect isa Makie.AxisAspect ? Float64(aspect.aspect) : nothing + return ratio===nothing ? (w, h) : (min(w, h*ratio), min(h, w/ratio)) +end + +# Native width and height refer to the Axis inner size. Outside/Mixed margins +# consume part of its assigned track. Relative sizes consume a fraction of it. +function _axis_insets(axis) + mode=axis.alignmode[] + native=axis.layoutobservables.protrusions[] + sides=ntuple(4) do i + mode isa Outside && return Float64(getfield(native, i)+getfield(mode.padding, i)) + mode isa Mixed || return 0.0 + side=getfield(mode.sides, i) + return side isa Real ? Float64(getfield(native, i)+side) : 0.0 + end + return (sides[1]+sides[2], sides[3]+sides[4]) +end + +function _axis_span(axis, frame) + insets=_axis_insets(axis) + return ntuple(2) do d + size=getproperty(axis, d==1 ? :width : :height)[] + inner=size isa Real ? Float64(size) : frame[d] + (inner+insets[d])/(size isa Relative ? size.x : 1.0) + end +end + +# Native autosize includes protrusions and padding without tight_bbox's one-pixel +# minimum for zero-sized reserved guide tracks. Opaque native content can leave +# a dimension undetermined. Its current declared allocation is then retained. +function _required_size(grid) + automatic=grid.layoutobservables.autosize[] + allocated=grid.layoutobservables.computedbbox[].widths + return ntuple(d -> Float64(something(automatic[d], allocated[d])), 2) +end + +function _panel_size(data) + required=_required_size(data.panel.layout) + # GridLayout's automatic track measurement reads native protrusions. Its + # placement also honors Mixed protrusion overrides. Include the latter's + # actual extra reservation in a fitted cell, without counting either twice. + axis=data.axis + reported=axis.layoutobservables.reporteddimensions[].outer + native=axis.layoutobservables.protrusions[] + extra=(reported.left-native.left+reported.right-native.right, + reported.bottom-native.bottom+reported.top-native.top) + return required .+ extra +end + +function _frame_measurements(p) + state=p.plot_state + canvas=state.shell.canvas + decorations=map(collect(values(state.panel_data))) do data + panel=_panel_size(data) + frame=_frame_size(data.axis) + panel .- _axis_span(data.axis, frame) + end + decoration=ntuple(i -> maximum(x -> x[i], decorations; init = 0.0), 2) + gaps=(Float64(canvas.default_colgap.x), Float64(canvas.default_rowgap.x)) + # Both grids now report constrained content. Flexible allocation outside + # that content is not a decoration and cannot become a sizing budget. + outer=_required_size(p.figure.layout) .- _required_size(canvas) + return (; decoration, gaps, outer) +end + +function _resize_preserving_views!(p, size) + views=[axis.targetlimits[] for axis in p.axes] + try + resize!(p.figure, round.(Int, size)...) + _compose_guides!(p) + finally + for (axis, view) in zip(p.axes, views) + axis.targetlimits[]==view || (axis.targetlimits[]=view) + end + end + return p +end + +# Constrain only shell-owned tracks around current per-panel frames. Native +# grids measure axis protrusions, titles, panel guides, figure guides and chrome. +# Empty internal matrix tracks inherit the common cell span. The tracks remain empty. +function _fit_frames!(p, frames; canvas = nothing) + state=p.plot_state + shell=state.shell + previous=state.fitting_geometry[] + state.fitting_geometry[]=true + try + if state.panel_page!==nothing + rows, columns=state.panel_page.dimensions + widths=zeros(columns) + heights=zeros(rows) + for data in values(state.panel_data) + panel=data.panel.layout + frame=_axis_span(data.axis, frames[data.axis]) + panel.width[]=Auto() + panel.height[]=Auto() + panel.tellwidth[]=true + panel.tellheight[]=true + colsize!(panel, 2, Fixed(frame[1])) + rowsize!(panel, 2, Fixed(frame[2])) + end + _compose_guides!(p) + for data in values(state.panel_data) + panel=data.panel.layout + gc=GridLayoutBase.gridcontent(panel) + bounds=_panel_size(data) + panel.width[]=bounds[1] + panel.height[]=bounds[2] + row=first(gc.span.rows) + column=first(gc.span.cols) + widths[column]=max(widths[column], bounds[1]) + heights[row]=max(heights[row], bounds[2]) + end + # Calibrated matrix frames share decorated cell spans. Physical + # aspect galleries retain their individual row and column requirements. + if !isempty(state.frame_padding) + widths.=maximum(widths) + heights.=maximum(heights) + end + for column in 1:columns + colsize!(shell.canvas, column, Fixed(iszero(widths[column]) ? + maximum(widths) : widths[column])) + end + for row in 1:rows + rowsize!(shell.canvas, row, Fixed(iszero(heights[row]) ? maximum(heights) : + heights[row])) + end + shell.canvas.width[]=Auto() + shell.canvas.height[]=Auto() + else + # plotwindow's declared native canvas is opaque. Preserve its own + # topology and allocation. Only fit the surrounding managed shell. + canvas===nothing && + (canvas=Tuple(shell.canvas.layoutobservables.computedbbox[].widths)) + shell.canvas.width[]=canvas[1] + shell.canvas.height[]=canvas[2] + end + shell.canvas.tellwidth[]=true + shell.canvas.tellheight[]=true + shell.body.width[]=Auto() + shell.body.height[]=Auto() + shell.body.tellwidth[]=true + shell.body.tellheight[]=true + colsize!(shell.body, 2, Auto(true)) + rowsize!(shell.body, 2, Auto(true)) + body_row=first(GridLayoutBase.gridcontent(shell.body).span.rows) + rowsize!(shell.root, body_row, Auto(true)) + colsize!(shell.root, 1, Auto(true)) + _compose_guides!(p) + required=_required_size(shell.root) + # Width settles measured wrapping before the native required height. + _resize_preserving_views!(p, ( + required[1], p.figure.scene.viewport[].widths[2])) + required=_required_size(shell.root) + _resize_preserving_views!(p, required) + finally + state.fitting_geometry[]=previous + end + return p +end + +# A native resize is a new local allocation, not another content-fit request. +# Release the shell's fitted tracks before the existing aspect and reflow behavior. +function _release_frames!(p) + state=p.plot_state + shell=state.shell + if state.panel_page!==nothing + for data in values(state.panel_data) + panel=data.panel.layout + panel.width[]=Relative(1) + panel.height[]=Relative(1) + panel.tellwidth[]=false + panel.tellheight[]=false + colsize!(panel, 2, Auto(false, 1)) + rowsize!(panel, 2, Auto(false, 1)) + end + for row in 1:size(shell.canvas)[1] + rowsize!(shell.canvas, row, Auto(false, 1)) + end + for column in 1:size(shell.canvas)[2] + colsize!(shell.canvas, column, Auto(false, 1)) + end + end + shell.canvas.width[]=Auto() + shell.canvas.height[]=Auto() + shell.canvas.tellwidth[]=false + shell.canvas.tellheight[]=false + shell.body.width[]=Auto() + shell.body.height[]=Auto() + shell.body.tellwidth[]=false + shell.body.tellheight[]=false + colsize!(shell.body, 2, Auto(false, 1)) + rowsize!(shell.body, 2, Auto(false, 1)) + rowsize!(shell.root, first(GridLayoutBase.gridcontent(shell.body).span.rows), Auto(false, 1)) + colsize!(shell.root, 1, Auto(false, 1)) + return p +end + +function _frame_budget(p, capacity) + br, bc=capacity + shell=p.plot_state.shell + measured=_frame_measurements(p) + frames=((shell.reference_size[1]-measured.outer[1]-(bc-1)*measured.gaps[1])/bc-measured.decoration[1], + (shell.reference_size[2]-measured.outer[2]-(br-1)*measured.gaps[2])/br-measured.decoration[2]) + all(x -> isfinite(x) && x>1, frames) || throw(ArgumentError( + "the nominal figure size cannot fit layout=$capacity and its measured decorations; increase the figure size or reduce layout")) + return frames +end + +function _calibrate_frames!(pages, capacity) + isempty(pages) && return pages + if all(p -> all(axis -> axis.aspect[]===nothing, p.axes), pages) + measured=map(_panel_padding!, pages) + padding=ntuple(i -> maximum(x -> x[i], measured), 4) + for p in pages + _panel_padding!(p; minimum_decoration = padding) + _fit_frames!(p, _frame_snapshot(p).frames) + end + end + frame_budgets=map(p -> _frame_budget(p, capacity), pages) + frames=ntuple(i -> minimum(x -> x[i], frame_budgets), 2) + for p in pages + physical=IdDict{Any, Tuple{Float64, Float64}}() + for axis in p.axes + insets=_axis_insets(axis) + inner=ntuple(2) do d + size=getproperty(axis, d==1 ? :width : :height)[] + size isa Real ? Float64(size) : + frames[d]*(size isa Relative ? size.x : 1.0)-insets[d] + end + physical[axis]=_frame_size(axis, inner) + end + _fit_frames!(p, physical) + end + return pages +end + +# Physical aspect is a panel requirement. Fit native panel cells into the +# available canvas using each panel's data ratio, including wide systems and +# one-row collections. The figure and window aspect ratios remain free. +function _fit_panel_aspects!(p) + isempty(p.axes) && return p + all(axis -> axis.aspect[] isa DataAspect, p.axes) || return p + state=p.plot_state + shell=state.shell + panels=collect(values(state.panel_data)) + all(data -> data.panel.layout!==nothing, panels) || return p + rows, columns=size(shell.canvas) + column_ratios=zeros(columns) + column_decoration=zeros(columns) + row_decoration=zeros(rows) + for data in panels + gc=GridLayoutBase.gridcontent(data.panel.layout) + row=first(gc.span.rows) + column=first(gc.span.cols) + view=data.axis.targetlimits[] + ratio=Float64(view.widths[1]/view.widths[2]) + isfinite(ratio) && ratio>0 || continue + column_ratios[column]=max(column_ratios[column], ratio) + panel=data.panel.layout.layoutobservables.computedbbox[] + frame=data.axis.layoutobservables.computedbbox[] + decoration=panel.widths-frame.widths + column_decoration[column]=max(column_decoration[column], decoration[1]) + row_decoration[row]=max(row_decoration[row], decoration[2]) + end + any(iszero, column_ratios) && return p + body=shell.body.layoutobservables + canvas=shell.canvas.layoutobservables.computedbbox[] + outside=max.(0.0, Float64.(body.computedbbox[].widths-canvas.widths)) + available=Float64.(body.suggestedbbox[].widths) .- outside + gaps=(Float64(shell.canvas.default_colgap.x), Float64(shell.canvas.default_rowgap.x)) + frame_height=min( + (available[1]-sum(column_decoration)-(columns-1)*gaps[1])/sum(column_ratios), + (available[2]-sum(row_decoration)-(rows-1)*gaps[2])/rows) + isfinite(frame_height) && frame_height>1 || return p # native zero-sized initialization + column_widths=frame_height .* column_ratios .+ column_decoration + row_heights=frame_height .+ row_decoration + shell.canvas.width[]=sum(column_widths)+(columns-1)*gaps[1] + shell.canvas.height[]=sum(row_heights)+(rows-1)*gaps[2] + shell.canvas.tellwidth[]=true + shell.canvas.tellheight[]=true + shell.body.width[]=Auto() + shell.body.height[]=Auto() + shell.body.tellwidth[]=true + shell.body.tellheight[]=true + shell.body.halign[]=:center + shell.body.valign[]=:center + colsize!(shell.body, 2, Auto(true)) + rowsize!(shell.body, 2, Auto(true)) + for column in 1:columns + colsize!(shell.canvas, column, Fixed(column_widths[column])) + end + for row in 1:rows + rowsize!(shell.canvas, row, Fixed(row_heights[row])) + end + return p +end + +function _frame_snapshot(p) + frames=IdDict(axis=>_frame_size(axis) for axis in p.axes) + canvas=Tuple(p.plot_state.shell.canvas.layoutobservables.computedbbox[].widths) + views=[axis.targetlimits[] for axis in p.axes] + protrusions=[axis.layoutobservables.protrusions[] for axis in p.axes] + return (; frames, canvas, views, protrusions) +end + +Base.@noinline function _update_figure_layout!(action, p; before = nothing) + state=p.plot_state + state.presentation_ready[] || return action() + state.fitting_geometry[] && return action() + (; frames, canvas, views, protrusions)=before===nothing ? _frame_snapshot(p) : + before + state.fitting_geometry[]=true + try + result=action() + _compose_guides!(p) + if state.panel_page!==nothing && all(axis -> axis.aspect[]===nothing, p.axes) && + protrusions!=[axis.layoutobservables.protrusions[] for axis in p.axes] + _panel_padding!(p) + end + _fit_frames!(p, frames; canvas) + return result + finally + for (axis, view) in zip(p.axes, views) + axis.targetlimits[]==view || (axis.targetlimits[]=view) + end + state.fitting_geometry[]=false + end +end + +# Automatic flow pages may rearrange only their existing members on a manual +# window resize. Explicit layouts and matrix pages never install this callback. +function _responsive_axis_grid!(p) + state=p.plot_state + identities=state.panel_page.coordinates + length(identities)<=1 && return p + panels=[state.panel_data[id] for id in identities] + current_columns=Ref(state.panel_page.dimensions[2]) + on(p.figure.scene, p.figure.scene.viewport) do _ + p.plot_state.fitting_geometry[] && return nothing + grid=state.shell.canvas + box=state.shell.body.layoutobservables.suggestedbbox[] + all(>(0), box.widths) || return nothing + columns=clamp(round(Int, sqrt(length(panels)*box.widths[1]/box.widths[2])), 1, length(panels)) + columns==current_columns[] && return nothing + views=[data.axis.targetlimits[] for data in panels] + state.fitting_geometry[]=true + try + # Empty matrix-selection cells are not part of flow membership. + if haskey(state, :page_cells) + used=Set(data.panel.layout for data in panels) + for cell in values(state.page_cells) + cell.layout in used || _delete_subtree!(cell.layout) + end + empty!(state.page_cells) + end + rows=cld(length(panels), columns) + for (index, data) in enumerate(panels) + row, column=cld(index, columns), mod1(index, columns) + grid[row, column]=data.panel.layout + for key in (:xlabelvisible, :xticklabelsvisible, :xticksvisible) + haskey(state.shell.axis_attributes, key) || + (getproperty(data.axis, key)[]=row==rows) + end + end + GridLayoutBase.trim!(grid) + for row in 1:rows + rowsize!(grid, row, Auto(false, 1)) + end + for column in 1:columns + colsize!(grid, column, Auto(false, 1)) + end + current_columns[]=columns + p.plot_state=merge(p.plot_state, + (panel_page = merge(p.plot_state.panel_page, (dimensions = ( + rows, columns),)),)) + _fit_panel_aspects!(p) + _compose_guides!(p) + finally + for (data, view) in zip(panels, views) + data.axis.targetlimits[]==view || (data.axis.targetlimits[]=view) + end + state.fitting_geometry[]=false + end + nothing + end + return p +end + +# Native attribute changes use the same local fitting operation as public guide +# mutators. Capture frames before native layout listeners run, then measure the +# completed native edit. Internal composition and resize notifications are excluded +# from this path. Subscriptions belong to the actual edited block. +function _watch_presentation!(p, block, names) + subscriptions=Any[] + for name in names + name in propertynames(typeof(block)) || continue + attribute=getproperty(block, name) + attribute isa Observable || continue + pending=Ref{Any}(nothing) + push!(subscriptions, + on(block.blockscene, attribute; priority = 1) do _ + state=p.plot_state + pending[]=state.presentation_ready[] && !state.fitting_geometry[] && + !state.composing_guides[] ? _frame_snapshot(p) : nothing + nothing + end) + push!(subscriptions, on(block.blockscene, attribute; priority = -100) do _ + before=pending[] + pending[]=nothing + before===nothing || _update_figure_layout!(() -> nothing, p; before) + nothing + end) + end + return subscriptions +end diff --git a/ext/LineCableModelsMakieExt/material_colors.jl b/ext/LineCableModelsMakieExt/material_colors.jl new file mode 100644 index 000000000..5d9c145f9 --- /dev/null +++ b/ext/LineCableModelsMakieExt/material_colors.jl @@ -0,0 +1,262 @@ +const _CONDUCTOR_RHO_RANGE = (1.72e-8, 2.5e-7) +const _SEMICON_RHO_RANGE = (1.0e-2, 1.0e4) +const _DIELECTRIC_EPS_RANGE = (1.0, 10.0) +const _MAGNETIC_MU_RANGE = (1.0, 300.0) +const _EARTH_RHO_RANGE = (1.0e-1, 1.0e4) + +const _CONDUCTOR_COLORS = [ + RGB(221 / 255, 228 / 255, 236 / 255), + RGB(203 / 255, 185 / 255, 160 / 255), + RGB(198 / 255, 134 / 255, 66 / 255) +] +const _DIELECTRIC_COLORS = [ + RGB(1, 1, 1), + RGB(248 / 255, 244 / 255, 232 / 255), + RGB(242 / 255, 230 / 255, 200 / 255), + RGB(234 / 255, 207 / 255, 154 / 255) +] +const _SEMICON_COLORS = [ + RGB(0.349, 0.392, 0.404), + RGB(0.604, 0.467, 0.349) +] +const _MAGNETIC_COLORS = [ + RGB(89 / 255, 102 / 255, 216 / 255), + RGB(214 / 255, 79 / 255, 216 / 255) +] +const _EARTH_COLORS = [ + RGB(94 / 255, 114 / 255, 131 / 255), + RGB(139 / 255, 120 / 255, 96 / 255), + RGB(184 / 255, 154 / 255, 99 / 255) +] + +# Two staggered, disconnected slashes per tile. The signed distance gives a +# one-pixel antialiased edge. Makie's polygon fill owns clipping and placement. +const _DIELECTRIC_PATTERN_MASK = map(Iterators.product(1:32, 1:56)) do (i, j) + distance = minimum(((8, 14), (24, 42))) do (cx, cy) + x, y = i - 0.5 - cx, j - 0.5 - cy + along = clamp((x + y) / 2, -2.5, 2.5) + hypot(x - along, y - along) + end + clamp(1.0 - distance, 0.0, 1.0) +end + +function _oklab_blend(first_color, second_color, value::Real) + t = clamp(Float64(value), 0.0, 1.0) + first_oklab = convert(Oklab{Float64}, convert(RGB{Float64}, first_color)) + second_oklab = convert(Oklab{Float64}, convert(RGB{Float64}, second_color)) + mixed = Oklab( + (1 - t) * first_oklab.l + t * second_oklab.l, + (1 - t) * first_oklab.a + t * second_oklab.a, + (1 - t) * first_oklab.b + t * second_oklab.b, + ) + rgb = convert(RGB{Float64}, mixed) + return RGB( + clamp(red(rgb), 0, 1), + clamp(green(rgb), 0, 1), + clamp(blue(rgb), 0, 1), + ) +end + +function _gradient(colors, value::Real) + t = clamp(Float64(value), 0.0, 1.0) + position = t * (length(colors) - 1) + index = clamp(floor(Int, position) + 1, 1, length(colors) - 1) + fraction = position - (index - 1) + return _oklab_blend(colors[index], colors[index + 1], fraction) +end + +function _log_fraction(value, lower, upper) + clamped = clamp(Float64(value), Float64(lower), Float64(upper)) + return (log10(clamped) - log10(lower)) / (log10(upper) - log10(lower)) +end + +function _linear_fraction(value, lower, upper) + clamped = clamp(Float64(value), Float64(lower), Float64(upper)) + return (clamped - lower) / (upper - lower) +end + +_conductor_color(resistivity::Real) = _gradient( + _CONDUCTOR_COLORS, + _log_fraction(resistivity, _CONDUCTOR_RHO_RANGE...) +) + +_dielectric_color(relative_permittivity::Real) = _gradient( + _DIELECTRIC_COLORS, + _linear_fraction(max(relative_permittivity, 1.0), _DIELECTRIC_EPS_RANGE...) +) + +_semicon_color(resistivity::Real) = _gradient( + _SEMICON_COLORS, + _log_fraction(resistivity, _SEMICON_RHO_RANGE...) +) + +function _earth_color(resistivity::Real) + # The taupe anchor is 100 Ω·m, not the geometric midpoint of the range. + fraction = _log_fraction(resistivity, _EARTH_RHO_RANGE...) + position = fraction <= 0.6 ? fraction / 0.6 : 1 + (fraction - 0.6) / 0.4 + return _gradient(_EARTH_COLORS, position / 2) +end + +_material_base(material::Material) = _material_base(Val(material.kind), material) +_material_base(material::RadialDielectric) = _dielectric_color(nominal(material.eps_r)) +_material_base(layer::EarthLayer) = _earth_color(nominal(layer.rho)) +_material_base(::Val{:conductor}, material::Material) = _conductor_color(nominal(material.rho)) +_material_base(::Val{:insulator}, material::Material) = _dielectric_color(nominal(material.eps_r)) +_material_base(::Val{:semicon}, material::Material) = _semicon_color(nominal(material.rho)) +function _material_base(::Val{Kind}, material) where {Kind} + throw(ArgumentError( + "preview coloring is not defined for material kind :$Kind" + )) +end + +function _magnetic_overlay(base, relative_permeability::Real) + fraction = _log_fraction( + max(relative_permeability, 1.0), _MAGNETIC_MU_RANGE... + ) + iszero(fraction) && return RGB(base) + # Keep the visible lower and middle range indigo. Introduce magenta only in + # the upper third of the logarithmic permeability range. + tint = _gradient(_MAGNETIC_COLORS, max(0.0, (fraction - 0.65) / 0.35)) + return _oklab_blend(base, tint, 0.35fraction^0.7) +end + +function _material_color(material::Union{Material, RadialDielectric, EarthLayer}; + alpha::Real = 1.0, pattern::Bool = false) + relative_permeability = LineCableModels.nominal(material.mu_r) + base = _material_base(material) + color = _magnetic_overlay(base, relative_permeability) + fill = RGBA(red(color), green(color), blue(color), alpha) + if pattern && material isa Union{Material, RadialDielectric} && + material.kind === :insulator + ink = _oklab_blend(color, RGB(0.25, 0.27, 0.30), 0.15) + return Makie.Pattern(_DIELECTRIC_PATTERN_MASK; + color1 = RGBA(red(ink), green(ink), blue(ink), alpha), color2 = fill) + end + return fill +end + +function _compact_number(value::Real) + numeric = Float64(value) + isfinite(numeric) || return string(numeric) + compact = @sprintf("%.3g", numeric) + occursin('e', compact) || return compact + coefficient, exponent = split(compact, 'e'; limit = 2) + superscript = replace( + string(parse(Int, exponent)), + "-" => "⁻", "0" => "⁰", "1" => "¹", "2" => "²", "3" => "³", + "4" => "⁴", "5" => "⁵", "6" => "⁶", "7" => "⁷", "8" => "⁸", + "9" => "⁹" + ) + return coefficient == "1" ? "10$superscript" : "$coefficient × 10$superscript" +end + +function _colorbar_range(property_range) + lower, upper = Float64.(property_range) + lower_label = _compact_number(lower) + upper_label = _compact_number(upper) + if lower_label == upper_label + representative = lower > 0 && upper > 0 ? sqrt(lower * upper) : (lower + upper) / 2 + return representative, nextfloat(representative), ([0.5], [lower_label]) + end + return lower, upper, ([0.0, 1.0], [lower_label, upper_label]) +end + +function _color_samples( + color, + lower, + upper; + count::Int = 256, + logarithmic::Bool = true, +) + lower_value = max(Float64(lower), floatmin(Float64)) + upper_value = max(Float64(upper), nextfloat(lower_value)) + values = logarithmic ? + 10.0 .^ range(log10(lower_value), log10(upper_value); length = count) : + range(lower_value, upper_value; length = count) + return [color(value) for value in values] +end + +function _bounded_property_range(property_range, supported_range) + lower, upper = Float64.(property_range) + lower <= upper || throw(ArgumentError( + "a material property range must be ordered from lower to upper" + )) + return ( + clamp(lower, supported_range...), + clamp(upper, supported_range...) + ) +end + +function _material_scheme( + label, + color, + property_range, + supported_range, + alpha; + logarithmic::Bool = true, +) + bounded_range = _bounded_property_range(property_range, supported_range) + lower, upper, ticks = _colorbar_range(bounded_range) + colors = _color_samples(color, lower, upper; logarithmic) + colors = RGBA[RGBA(red(value), green(value), blue(value), alpha) for value in colors] + return (; label, colormap = colors, limits = (0.0, 1.0), ticks) +end + +function LineCableModels.materialcolors( + property::Symbol, + property_range = nothing; + alpha::Real = 1.0 +) + property in (:rho, :mu_r, :eps_r) || throw(ArgumentError( + "material property must be :rho, :mu_r, or :eps_r", + )) + 0 <= alpha <= 1 || throw(ArgumentError( + "alpha must be between zero and one", + )) + ranges = DataModel.material_property_ranges() + resolved_range = property_range === nothing ? getproperty(ranges, property) : + property_range + resolved_range isa Tuple && length(resolved_range) == 2 && + all(value -> value isa Real, resolved_range) || throw(ArgumentError( + "a material property range must be a tuple of two real values", + )) + + neutral = RGB(0.94, 0.94, 0.94) + permeability_color(value) = _magnetic_overlay(neutral, value) + label, color, supported_range, logarithmic = if property === :rho + "ρ [Ω·m]", _conductor_color, _CONDUCTOR_RHO_RANGE, true + elseif property === :mu_r + "μᵣ", permeability_color, _MAGNETIC_MU_RANGE, true + else + "εᵣ", _dielectric_color, _DIELECTRIC_EPS_RANGE, false + end + return _material_scheme( + label, + color, + resolved_range, + supported_range, + alpha; + logarithmic, + ) +end + +function _material_schemes(ranges; kwargs...) + return Tuple( + LineCableModels.materialcolors( + property, + getproperty(ranges, property); + kwargs... + ) for property in (:rho, :mu_r, :eps_r) + ) +end + + +function _earth_material_schemes(ranges; alpha::Real = 0.25) + return ( + _material_scheme( + "ρ [Ω·m]", _earth_color, ranges.rho, _EARTH_RHO_RANGE, alpha + ), + LineCableModels.materialcolors(:mu_r, ranges.mu_r; alpha), + LineCableModels.materialcolors(:eps_r, ranges.eps_r; alpha) + ) +end diff --git a/ext/LineCableModelsMakieExt/montecarlo.jl b/ext/LineCableModelsMakieExt/montecarlo.jl new file mode 100644 index 000000000..5ff5ba910 --- /dev/null +++ b/ext/LineCableModelsMakieExt/montecarlo.jl @@ -0,0 +1,113 @@ +function _observed_distribution(result::LineCableModels.MonteCarloResult,request; + point::Integer=1,bins=nothing,kwargs...) + selector=request isa Function ? request : first(request) + indices=request isa Function ? (1,) : Base.tail(request) + retained=(LineCableModels.histograms,selector,indices...,bins) + return Commons.ObservedResult(result,point,(retained,);kwargs...) +end + +function _distribution_product(observed,request) + products=filter(q -> q.coordinates.kind===:histogram,observed.quantities) + if request!==nothing + products=filter(q -> q.request==request || request_identity(q.request)==request_identity(request) || + request isa Function && request_identity(q.request)[2]===request,products) + end + length(products)==1 || throw(ArgumentError("select one retained histogram marginal")) + return only(products) +end + +function _distribution_plot(observed::Commons.ObservedResult,request,kind; + normalization=:none,qqline=:identity,title=nothing,fig_size=(800,400), + backend=nothing,display_plot=true,controls=true,export_theme=:default,open_export=true,kwargs...) + for key in (:point,:bins,:clip,:atol,:units,:length_unit,:quantity_units,:frequency_unit,:freq_unit,:frequencies,:complete_pairs) + haskey(kwargs,key) && throw(ArgumentError("$key belongs to observation construction; this statistical view consumes retained products")) + end + product=_distribution_product(observed,request) + distribution=product.distribution + if kind===:qq + qqline in (:identity,:none) || throw(ArgumentError("qqline must be :identity or :none")) + pairs=distribution.qq + pairs===nothing && throw(ArgumentError("Q–Q coordinates were not retained")) + x=pairs.sample;y=pairs.model + ordinate=(values=y,quantity=product.quantity,unit=product.unit) + elseif kind===:empirical + empirical=distribution.empirical_cdf + empirical===nothing && throw(ArgumentError("empirical CDF coordinates were not retained")) + x=empirical.x;y=empirical.y + ordinate=(values=y,quantity=Units.Quantity{:cumulative_probability}(),unit=Units.UnitExpr()) + elseif kind===:cdf + x=distribution.model_cdf.x;y=distribution.model_cdf.y + ordinate=(values=y,quantity=Units.Quantity{:cumulative_probability}(),unit=Units.UnitExpr()) + else + normalization in (:none,:probability,:pdf) || throw(ArgumentError("normalization must be :none, :probability or :pdf")) + field=kind===:density || normalization===:pdf ? :density : normalization===:probability ? :probability : :count + y=getproperty(product.values,field) + any(ismissing,y) && throw(ArgumentError("histogram counts were not retained")) + x=distribution.edges[1:end-1] + quantity=field===:density ? Units.Quantity{:probability_density}() : field===:count ? + Units.Quantity{:sample_count}() : Units.Quantity{:probability}() + unit=field===:density ? product.ordinate_units.density : Units.UnitExpr() + ordinate=(values=y,quantity,unit) + end + abscissa=(values=x,quantity=product.quantity,unit=product.unit) + heading=title===nothing ? "$(Units.symbol(product.quantity)) $(kind)" : String(title) + return _statistical_plot(abscissa,ordinate;title=heading,fig_size,backend, + display_plot,controls,export_theme,open_export,kwargs...) do axis,groups,order,labels,series + plot=if kind===:histogram + edges=distribution.edges + Makie.barplot!(axis,(edges[1:end-1].+edges[2:end])./2,y;width=diff(edges),gap=0,label=heading) + elseif kind===:density + stairs!(axis,distribution.edges,vcat(y,last(y));step=:post,label=heading) + elseif kind===:empirical + stairs!(axis,x,y;step=:post,label=heading) + elseif kind===:qq + scatter!(axis,x,y;label=heading) + else + lines!(axis,x,y;label=heading) + end + groups[:distribution]=Any[plot];push!(order,:distribution);labels[:distribution]=heading + push!(series,(xdata=x,ydata=y,plots=Any[plot])) + if kind===:qq && qqline===:identity + limits=collect(distribution.qq.reference) + identity=lines!(axis,limits,limits;color=:black,linestyle=:dash,label="identity") + groups[:identity]=Any[identity];push!(order,:identity);labels[:identity]="identity" + end + end +end + +Makie.hist(observed::Commons.ObservedResult,request=nothing;kwargs...) = _distribution_plot(observed,request,:histogram;kwargs...) +Makie.stairs(observed::Commons.ObservedResult,request=nothing;kwargs...) = _distribution_plot(observed,request,:density;kwargs...) +Makie.ecdfplot(observed::Commons.ObservedResult,request=nothing;kwargs...) = _distribution_plot(observed,request,:empirical;kwargs...) +Makie.lines(observed::Commons.ObservedResult,request=nothing;kwargs...) = _distribution_plot(observed,request,:cdf;kwargs...) +Makie.qqplot(observed::Commons.ObservedResult,request=nothing;kwargs...) = _distribution_plot(observed,request,:qq;kwargs...) + +function Makie.hist(source::LineCableModels.MonteCarloResult,request=LineCableModels.R; + point=1,bins=nothing,kwargs...) + acquisition,presentation=_plot_observation_options(kwargs) + observed=_observed_distribution(source,request;point,bins,acquisition...) + return Makie.hist(observed;presentation...) +end +function Makie.stairs(source::LineCableModels.MonteCarloResult,request=LineCableModels.R; + point=1,bins=nothing,kwargs...) + acquisition,presentation=_plot_observation_options(kwargs) + observed=_observed_distribution(source,request;point,bins,acquisition...) + return Makie.stairs(observed;presentation...) +end +function Makie.ecdfplot(source::LineCableModels.MonteCarloResult,request=LineCableModels.R; + point=1,bins=nothing,kwargs...) + acquisition,presentation=_plot_observation_options(kwargs) + observed=_observed_distribution(source,request;point,bins,acquisition...) + return Makie.ecdfplot(observed;presentation...) +end +function Makie.lines(source::LineCableModels.MonteCarloResult,request=LineCableModels.R; + point=1,bins=nothing,kwargs...) + acquisition,presentation=_plot_observation_options(kwargs) + observed=_observed_distribution(source,request;point,bins,acquisition...) + return Makie.lines(observed;presentation...) +end +function Makie.qqplot(source::LineCableModels.MonteCarloResult,request=LineCableModels.R; + point=1,bins=nothing,kwargs...) + acquisition,presentation=_plot_observation_options(kwargs) + observed=_observed_distribution(source,request;point,bins,acquisition...) + return Makie.qqplot(observed;presentation...) +end diff --git a/ext/LineCableModelsMakieExt/native_export.jl b/ext/LineCableModelsMakieExt/native_export.jl new file mode 100644 index 000000000..cfc7c18bb --- /dev/null +++ b/ext/LineCableModelsMakieExt/native_export.jl @@ -0,0 +1,103 @@ +function _native_sanitize_filename(value::AbstractString) + sanitized = lowercase(strip(value)) + sanitized = replace(sanitized, r"[^0-9a-z]+" => "_") + sanitized = strip(sanitized, '_') + return isempty(sanitized) ? "linecablemodels_plot" : sanitized +end + +function _native_path_within(path::AbstractString, root::AbstractString) + path_parts = collect(splitpath(normpath(realpath(path)))) + root_parts = collect(splitpath(normpath(realpath(root)))) + Sys.iswindows() && + (path_parts = lowercase.(path_parts); root_parts = lowercase.(root_parts)) + length(path_parts) >= length(root_parts) || return false + return path_parts[1:length(root_parts)] == root_parts +end + +function _native_export_directory() + current = abspath(pwd()) + package = abspath(pkgdir(LineCableModels)) + _native_path_within(current, package) || return current + fallback = joinpath(tempdir(), "linecablemodels-exports") + return fallback +end + +function _native_available_path(plot::LineCableModels.UIPlot) + base = _native_sanitize_filename(plot.export_name) + stamp = Dates.format(Dates.now(), "yyyymmdd_HHMMSS") + directory = _native_export_directory() + candidate = joinpath(directory, "$(base)_$(stamp).svg") + index = 2 + while ispath(candidate) + candidate = joinpath(directory, "$(base)_$(stamp)_$(index).svg") + index += 1 + end + return candidate +end + +function _native_open_command(path::AbstractString) + if Sys.iswindows() + return Cmd(["cmd", "/c", "start", "", path]) + elseif Sys.isapple() + executable = Sys.which("open") + return executable === nothing ? nothing : `$executable $path` + end + executable = Sys.which("xdg-open") + executable !== nothing && return `$executable $path` + executable = Sys.which("gio") + return executable === nothing ? nothing : `$executable open $path` +end + +function _native_open_export(path::AbstractString) + command = _native_open_command(path) + command === nothing && return false + try + process = run(pipeline(ignorestatus(command); stdout = devnull, stderr = devnull)) + return success(process) + catch error + @warn "could not open exported SVG with the system application" path exception = ( + error, + catch_backtrace() + ) + return false + end +end + +function LineCableModels.export_svg( + plot::LineCableModels.UIPlot; + path::Union{Nothing, AbstractString} = nothing, + theme::Union{Nothing, Symbol} = nothing, + open_file::Union{Nothing, Bool} = nothing +) + cairo = Base.get_extension(LineCableModels, :LineCableModelsCairoMakieExt) + cairo === nothing && throw(ArgumentError( + "SVG export requires CairoMakie to be loaded; run `import CairoMakie` first. " * + "For interactive plots, select backend=:gl after importing both backends.", + )) + export_theme = theme === nothing ? plot.export_theme : theme + export_theme in (:default,:publication) || throw(ArgumentError("theme must be :default or :publication")) + output = path === nothing ? _native_available_path(plot) : abspath(String(path)) + should_open = open_file === nothing ? plot.open_export : open_file + lowercase(splitext(output)[2]) == ".svg" || throw(ArgumentError( + "SVG export paths must use the .svg extension", + )) + ispath(output) && throw(ArgumentError( + "refusing to overwrite existing file: $output", + )) + mkpath(dirname(output)) + _export_presentation!(plot,export_theme) do + # Preserve the current figure and view. Saving must not run the native + # display initialization that resets automatic axes. + Makie.save(output,plot.figure;backend=cairo.CairoMakie,update=false) + end + opened = should_open && _native_open_export(output) + message = if opened + "Saved SVG to $output and opened it with the system application" + elseif should_open + "Saved SVG to $output; automatic opening was unavailable" + else + "Saved SVG to $output" + end + @info message + return output +end diff --git a/ext/LineCableModelsMakieExt/plot.jl b/ext/LineCableModelsMakieExt/plot.jl new file mode 100644 index 000000000..7a5276fd3 --- /dev/null +++ b/ext/LineCableModelsMakieExt/plot.jl @@ -0,0 +1,362 @@ +const _PrimarySource = Union{ + LineCableModels.LineParameters, LineCableModels.SeriesImpedance, + LineCableModels.ShuntAdmittance, LineCableModels.CableConstants, + LineCableModels.PropagationParameters} +const _PlotSource = Union{_PrimarySource, Commons.ObservedResult} + +function _plot_ydata(positional, keyword, default) + keyword===nothing && return positional===nothing ? default : positional + positional===nothing || + throw(ArgumentError("use positional ydata or the ydata keyword")) + return keyword +end + +# Split only public keyword ownership. Scientific normalization stays in Commons. +function _plot_observation_options(kwargs; retained = false) + haskey(kwargs, :complete_pairs) && + throw(ArgumentError("complete_pairs is owned by observation construction")) + haskey(kwargs, :freq_unit) && haskey(kwargs, :frequency_unit) && + throw(ArgumentError("use frequency_unit or freq_unit, not both")) + keys=(:clip, :atol, :units, :length_unit, :quantity_units, + :frequency_unit, :frequencies, :freq_unit) + if retained + for key in (:clip, :atol, :frequencies) + haskey(kwargs, key) && + throw(ArgumentError("$key requires primary observation construction; retained plotting only selects or re-expresses recorded values")) + end + end + acquisition=(; + (key===:freq_unit ? :frequency_unit=>value : key=>value + for (key, value) in kwargs if key in keys)...) + presentation=(; (key=>value for (key, value) in kwargs if key ∉ keys)...) + return acquisition, presentation +end + +function plot(source::_PrimarySource, selection = nothing; + ydata = nothing, reference = nothing, kwargs...) + return plot((source,), selection; ydata, reference, kwargs...) +end +function plot(source::Union{SeriesImpedance, ShuntAdmittance}, + f::AbstractVector, selection = nothing; kwargs...) + return plot(source, selection; frequencies = f, kwargs...) +end +Makie.plot(source::_PrimarySource, args...; kwargs...) = plot(source, args...; kwargs...) + +function plot(observed::Commons.ObservedResult, selection = nothing; kwargs...) + return plot([observed], selection; kwargs...) +end + +function plot(observed::AbstractVector{<:Commons.ObservedResult}, selection = nothing; + ydata = nothing, reference = nothing, + series_labels = nothing, series_attributes = nothing, problem = nothing, + formulations = nothing, band = nothing, + errorbar_sampling = nothing, title = nothing, title_prefix = nothing, figure_title = nothing, + title_attributes::NamedTuple = (;), panel_titles = nothing, fig_size = nothing, layout = nothing, + overlay::Symbol = :auto, + xscale = nothing, yscale = :linear, legend_position = _omitted, legend_title = nothing, + legend_attributes::NamedTuple = (;), legend_cap = 0.5, panel_legends = (), + backend = nothing, display_plot::Bool = true, controls::Bool = true, + export_theme::Symbol = :default, open_export::Bool = true, kwargs...) + isempty(observed) && throw(ArgumentError("plot requires at least one observed point")) + overlay in (:auto, :gridpoints, :coordinates, :rows) || throw(ArgumentError( + "overlay must be :auto, :gridpoints, :coordinates, or :rows")) + display_units, kwargs=_plot_observation_options(kwargs; retained = true) + for key in (:legend_overflow, :blocks, :series_defaults, + :series_count, :series_indices, :formulation_roles, + :dependent_plots, :marker_coordinates, :signed_ylog, :anchor, :legend_anchor) + haskey(kwargs, key) && throw(ArgumentError("unsupported plotting keyword $key")) + end + selected=findall(observed) do point + id=get(point.gridpoint, :id, nothing) + (problem===nothing || + id!==nothing && + id.problem_index in (problem isa Integer ? (problem,) : problem)) && + (formulations===nothing || + id!==nothing && id.formulation_index in + (formulations isa Integer ? (formulations,) : formulations)) + end + isempty(selected) && + throw(ArgumentError("no retained observations match the original point selection")) + if formulations isa Union{AbstractVector, Tuple} + sort!(selected; + by = index -> findfirst(==(observed[index].gridpoint.id.formulation_index), formulations)) + end + points=observed[selected] + requests=Commons.observation_selection(first(points), _plot_ydata(selection, ydata, ())) + requests=Commons.observation_requests(first(points), requests).displayed + if reference!==nothing && !(reference isa Commons.ObservedResult) + reference isa _PrimarySource || + throw(ArgumentError("construct an atomic ObservedResult for a reference collection")) + reference=Commons.ObservedResult(reference, requests; complete_pairs = true) + end + if !isempty(display_units) + points=[Commons.ObservedResult(point, requests; display_units...) + for point in points] + reference===nothing || + (reference=Commons.ObservedResult(reference, requests; display_units...)) + end + sources=reference===nothing ? Tuple(points) : (Tuple(points)..., reference) + input_point_count=length(observed)+(reference===nothing ? 0 : 1) + displayed=reference===nothing ? selected : [selected; input_point_count] + slots=reference===nothing ? selected : [selected; 0] + reference_id=reference===nothing ? nothing : get(reference.gridpoint, :id, nothing) + products=map(request -> Commons.observation_product(sources, request; band, reference_id), requests) + plot_data=map(eachindex(sources)) do index + _line_plot_data(Tuple(records[index] for records in products)) + end + # The selected cardinality includes an explicit reference and precedes + # equivalence grouping. The grouping only changes gridpoint curves. + orientations=map(eachindex(requests)) do index + kind=first(plot_data).observations[index].coordinates.kind + overlay===:rows && kind!==:matrix && throw(ArgumentError( + "overlay=:rows requires matrix observations; select matrix quantities in a separate call")) + overlay===:auto ? + (kind===:vector && length(sources)==1 && + (layout===nothing || layout==(1, 1)) ? :coordinates : :gridpoints) : overlay + end + groups=map(request -> Commons.observation_groups(points; request), requests) + retained=map(eachindex(requests)) do index + indices=orientations[index]!==:gridpoints ? collect(eachindex(sources)) : + [group.representative for group in groups[index]] + orientations[index]===:gridpoints && reference!==nothing && + push!(indices, length(sources)) + indices + end + coordinate_identities=map(eachindex(requests)) do index + first(plot_data).observations[index].coordinates.kind===:assemblies ? + unique(vcat((source.observations[index].coordinates.labels[ + source.observations[index].coordinates.assemblies] + for source in plot_data)...)) : nothing + end + coordinate_labels=map(eachindex(requests)) do index + coordinate_identities[index]===nothing ? + _coordinate_labels(first(plot_data).observations[index], + first(plot_data).coordinates[index], orientations[index]) : + unique(vcat((_coordinate_labels(source.observations[index], + source.coordinates[index], orientations[index]) for source in plot_data)...)) + end + # Positional overrides bind to the overlaid dimension. Preflight every + # family before constructing the first native figure. + if series_labels isa Union{Tuple, AbstractVector} || + series_attributes isa Union{Tuple, AbstractVector} + counts=unique(orientations[index]!==:gridpoints ? + length(coordinate_labels[index]) : + input_point_count for index in eachindex(requests)) + length(counts)==1 || throw(ArgumentError( + "positional series overrides span figure families with different overlaid dimensions; use separate plot calls")) + end + explicit_labels=series_labels!==nothing + result_labels=explicit_labels && reference!==nothing && + all(==(:gridpoints), orientations) && + length(series_labels)==length(observed) + labels=explicit_labels && any(==(:gridpoints), orientations) ? + Tuple(_comparison_labels(series_labels, result_labels ? input_point_count-1 : + input_point_count)[i] + for i in (result_labels ? selected : displayed)) : nothing + attributes=any(==(:gridpoints), orientations) ? + _series_attributes(series_attributes, input_point_count)[displayed] : nothing + coordinate_attributes=map(eachindex(requests)) do index + orientations[index]!==:gridpoints ? + _series_attributes(series_attributes, length(coordinate_labels[index])) : nothing + end + request_labels=map(eachindex(requests)) do index + request=requests[index] + if orientations[index]!==:gridpoints + result=Commons.observation_labels(sources; request, fallback = "") + reference!==nothing && + (result[end]*=isempty(result[end]) ? "Reference" : " (reference)") + result + else + result=explicit_labels ? Any[labels...] : + Commons.observation_labels(sources; request, + fallback = length(sources)==1 ? "" : nothing) + result_labels && push!(result, + last(Commons.observation_labels(sources; request))) + (!explicit_labels || result_labels) && reference!==nothing && + (result[end]*=" (reference)") + result + end + end + if explicit_labels + for index in eachindex(requests) + orientations[index]===:gridpoints && continue + coordinate_labels[index]=collect(_comparison_labels(series_labels, + length(coordinate_labels[index]))) + end + end + facets=_observation_facets( + plot_data, requests, orientations, retained, slots, input_point_count, + request_labels, coordinate_labels, coordinate_identities; explicit_labels) + capacities=map(eachindex(requests)) do index + selected_facets=filter(f -> f.request_index==index, facets) + matrix=first(selected_facets).kind===:matrix && orientations[index]===:gridpoints ? + [[(f.row, f.column) for f in selected_facets]] : () + panel_count=orientations[index]===:rows ? + length(first(plot_data).coordinates[index][2]) : length(selected_facets) + _page_capacity(layout, matrix, (panel_count,)) + end + pages=_observation_pages(facets, capacities; automatic = layout===nothing) + if panel_titles isa Union{Tuple, AbstractVector} + length(panel_titles)==length(facets) || + throw(DimensionMismatch("panel_titles must contain one title per selected panel before pagination")) + panel_titles=Dict((f.identity, f.panel_identity)=>label + for (f, label) in zip(facets, panel_titles)) + end + identities=Set(f.panel_identity for f in facets) + all(pair -> _panel_identity(first(pair)) in identities, _panel_legend_pairs(panel_legends)) || + throw(ArgumentError("panel legend identity is absent from the selected panels")) + legend_cap=_legend_fraction(legend_cap) + errorbar_sampling===nothing || errorbar_sampling in (:staggered, :all) || + throw(ArgumentError("errorbar_sampling must be :staggered or :all")) + if figure_title isa Union{Tuple, AbstractVector} + length(figure_title)==length(pages) || throw(DimensionMismatch( + "figure_title must contain one entry per generated figure")) + end + _activate_plot_backend(backend) + built=LineCableModels.UIPlot[] + for (page_index, page) in enumerate(pages) + request_index=first(page.facets).request_index + curves=unique(curve -> curve.group, + [curve for facet in page.facets for curve in facet.curves]) + coordinate_overlay=orientations[request_index]!==:gridpoints + series_count=coordinate_overlay ? length(coordinate_labels[request_index]) : + length(observed) + series_indices=[curve.slot for curve in curves] + styles=_comparison_styles(series_indices, + [curve.role for curve in curves], series_count) + page_attributes=coordinate_overlay ? + [coordinate_attributes[request_index][curve.position] + for curve in curves] : + [attributes[curve.source_index] for curve in curves] + position=legend_position===_omitted ? + (length(curves)>1 || explicit_labels ? :bottom : nothing) : + legend_position + sampling=errorbar_sampling===nothing ? (length(curves)>1 ? :staggered : :all) : + errorbar_sampling + automatic_title=_quantity_page_title(page) + page_title=title===nothing ? automatic_title : title + title!==nothing && length(pages)>1 && (page_title="$title — $automatic_title") + title===nothing && title_prefix!==nothing && !ismissing(title_prefix) && + (page_title="$title_prefix — $automatic_title") + visible_title=figure_title isa Union{Tuple, AbstractVector} ? + figure_title[page_index] : figure_title + push!(built, + with_theme(_plot_theme(export_theme = export_theme)) do + _line_page( + plot_data, page; + series_indices, + errorbar_sampling = sampling, series_defaults = styles, + series_attributes = page_attributes, title = page_title, + figure_title = visible_title, title_attributes, panel_titles, + fig_size = _figure_size(fig_size, page.capacity), + xscale, yscale, + legend_position = position, legend_title, legend_attributes, legend_cap, panel_legends, + controls, display_plot = false, export_theme, open_export, kwargs...) + end) + last(built).plot_state=merge(last(built).plot_state, + (observed = sources, + display_groups = (( + request = requests[request_index], groups = groups[request_index]),), + displayed_indices = orientations[request_index]===:rows ? + [first(page.facets).source_index] : retained[request_index], + nominal_capacity = page.capacity)) + _frame_budget(last(built), page.capacity) + end + if layout===nothing + for index in eachindex(requests) + family=findall(page -> first(page.facets).request_index==index, pages) + _calibrate_frames!(built[family], capacities[index]) + end + else + _calibrate_frames!(built, first(capacities)) + end + if layout===nothing + for (p, page) in zip(built, pages) + (first(page.facets).orientation===:rows || + first(page.facets).kind in (:diagonal, :vector) && + first(page.facets).orientation===:gridpoints) && + _responsive_axis_grid!(p) + end + end + if display_plot + foreach(page -> _display_figure!(page.figure, page.export_name), built) + end + return length(built)==1 ? only(built) : built +end +function Makie.plot(source::Commons.ObservedResult, args...; kwargs...) + plot(source, args...; kwargs...) +end +function Makie.plot(source::AbstractVector{<:Commons.ObservedResult}, args...; kwargs...) + plot(source, args...; kwargs...) +end + +function plot(sources::Union{AbstractVector{<:_PlotSource}, Tuple{Vararg{_PlotSource}}}, + selection = nothing; + ydata = nothing, reference = nothing, kwargs...) + isempty(sources) && throw(ArgumentError("plot requires at least one result")) + if all(source -> source isa Commons.ObservedResult, sources) + return plot( + Commons.ObservedResult[sources...], selection; ydata, reference, kwargs...) + end + acquisition, presentation=_plot_observation_options(kwargs) + requests=Commons.observation_selection(first(sources), _plot_ydata(selection, ydata, ())) + normalized=Commons.observation_requests(first(sources), requests; complete_pairs = true) + retained_units=(; + (key=>value + for (key, value) in pairs(acquisition) + if key in + (:units, :length_unit, :quantity_units, :frequency_unit))...) + observed=Commons.ObservedResult[source isa Commons.ObservedResult ? + (isempty(retained_units) ? source : + Commons.ObservedResult(source, normalized.displayed; retained_units...)) : + Commons.ObservedResult(source, requests; + complete_pairs = true, acquisition...) + for source in sources] + if reference!==nothing && !(reference isa Commons.ObservedResult) + reference isa _PrimarySource || + throw(ArgumentError("construct an atomic ObservedResult for a reference collection")) + reference=Commons.ObservedResult(reference, requests; complete_pairs = true, acquisition...) + end + return plot(observed; ydata = normalized.displayed, reference, presentation...) +end +function Makie.plot( + sources::Union{AbstractVector{<:_PlotSource}, Tuple{Vararg{_PlotSource}}}, + args...; kwargs...) + plot(sources, args...; kwargs...) +end + +# Public package plotting can validate heterogeneous owned collections. Native +# Makie array methods remain untouched for ordinary arrays. +function plot(sources::AbstractVector, selection = nothing; kwargs...) + all(source -> source isa _PlotSource, sources) || throw(ArgumentError( + "result collections must contain supported primary or observed results")) + return plot(_PlotSource[sources...], selection; kwargs...) +end + +function plot( + first::_PrimarySource, second::_PrimarySource, rest...; ydata = nothing, kwargs...) + sources=_PrimarySource[first, second] + selection=nothing + for item in rest + item isa _PrimarySource && selection===nothing ? push!(sources, item) : + selection===nothing ? (selection=item) : + throw(ArgumentError("one trailing ydata selection is accepted")) + end + return plot(sources; ydata = _plot_ydata(selection, ydata, ()), kwargs...) +end +function Makie.plot(first::_PrimarySource, second::_PrimarySource, rest...; kwargs...) + plot(first, second, rest...; kwargs...) +end + +function plot(sources::NamedTuple, selection = nothing; + ydata = nothing, series_labels = nothing, kwargs...) + all(source -> source isa _PlotSource, values(sources)) || + throw(ArgumentError("named result plotting requires supported primary or observed members")) + # Source names describe gridpoint curves. Row overlays use physical row labels. + labels=series_labels===nothing && get(kwargs,:overlay,:auto)!==:rows ? + Tuple(string.(keys(sources))) : series_labels + return plot(Tuple(values(sources)), selection; ydata, series_labels = labels, kwargs...) +end +function Makie.plot(sources::NamedTuple{K, <:Tuple{Vararg{_PlotSource}}}, args...; kwargs...) where {K} + plot(sources, args...; kwargs...) +end diff --git a/ext/LineCableModelsMakieExt/recipes/comparison_data.jl b/ext/LineCableModelsMakieExt/recipes/comparison_data.jl new file mode 100644 index 000000000..cd7dc5e51 --- /dev/null +++ b/ext/LineCableModelsMakieExt/recipes/comparison_data.jl @@ -0,0 +1,15 @@ +function _comparison_labels(labels, count::Int) + labels isa Tuple || labels isa AbstractVector || + throw(ArgumentError( + "series_labels must be a tuple or vector of labels", + )) + length(labels) == count || throw(DimensionMismatch( + "series_labels must contain one label for each LineParameters result", + )) + all( + label -> label isa typeof(Makie.rich("")) || + label isa AbstractString && !isempty(strip(label)), labels) || throw( + ArgumentError("series labels must be nonempty strings"), + ) + return Tuple(labels) +end diff --git a/ext/LineCableModelsMakieExt/recipes/formulation_comparisons.jl b/ext/LineCableModelsMakieExt/recipes/formulation_comparisons.jl new file mode 100644 index 000000000..c2a4824c0 --- /dev/null +++ b/ext/LineCableModelsMakieExt/recipes/formulation_comparisons.jl @@ -0,0 +1,37 @@ +import LineCableModels.ReportBuilder: ReportArtifact + +"""Construct observations, then overlay selected original study points.""" +function plot(results::LineCableModels.ParametricResult,selection=nothing; + ydata=nothing,problem=nothing,formulations=nothing,reference=nothing,kwargs...) + acquisition,presentation=_plot_observation_options(kwargs) + requests=Commons.observation_selection(first(results),_plot_ydata(selection,ydata,())) + normalized=Commons.observation_requests(first(results),requests;complete_pairs=true) + observed=observables(results,requests;complete_pairs=true,acquisition...) + if reference!==nothing && !(reference isa Commons.ObservedResult) + reference isa _PrimarySource || throw(ArgumentError("construct an atomic ObservedResult for a reference collection")) + reference=Commons.ObservedResult(reference,requests;complete_pairs=true,acquisition...) + end + return plot(observed;ydata=normalized.displayed,reference,problem,formulations,presentation...) +end + +function plot(results::LineCableModels.AbstractUncertaintyResult,selection=nothing; + ydata=nothing,point=nothing,reference=nothing,kwargs...) + acquisition,presentation=_plot_observation_options(kwargs) + requests=Commons.observation_selection(results,_plot_ydata(selection,ydata,())) + observed=observables(results,requests;complete_pairs=true,acquisition...) + retained=point===nothing ? observed : observed[point isa Integer ? [point] : point] + if reference!==nothing && !(reference isa Commons.ObservedResult) + reference isa _PrimarySource || throw(ArgumentError("construct an atomic ObservedResult for a reference collection")) + reference=Commons.ObservedResult(reference,requests;complete_pairs=true,acquisition...) + end + return plot(retained;ydata=requests,reference,presentation...) +end + +"""Render a report's retained observations and separate observed reference.""" +function plot(artifact::ReportArtifact,selection=nothing;ydata=nothing,title_prefix=nothing,reference=artifact.reference,kwargs...) + point=artifact.observed isa Commons.ObservedResult ? artifact.observed : first(artifact.observed) + prefix=title_prefix===nothing ? get(get(point.timings,:context,(;)),:id,nothing) : title_prefix + return plot(artifact.observed,selection;ydata,reference,title_prefix=prefix,kwargs...) +end +Makie.plot(artifact::ReportArtifact,args...;kwargs...) = plot(artifact,args...;kwargs...) +Makie.plot(results::LineCableModels.AbstractResultSpace,args...;kwargs...) = plot(results,args...;kwargs...) diff --git a/ext/LineCableModelsMakieExt/recipes/line_data.jl b/ext/LineCableModelsMakieExt/recipes/line_data.jl new file mode 100644 index 000000000..0061e26e9 --- /dev/null +++ b/ext/LineCableModelsMakieExt/recipes/line_data.jl @@ -0,0 +1,43 @@ +# Renderers select detached records. Scientific normalization and extraction +# belong to ObservedResult construction. +function _line_plot_data(records::Tuple) + for q in records + kind=q.coordinates.kind + kind in (:matrix,:diagonal,:vector,:assemblies,:array) || throw(ArgumentError( + "ordinary plot does not select an interpretation for retained $kind products; use a retained mean/std request or an explicit statistical plotting verb")) + kind===:array && q.values isa AbstractArray && ndims(q.values)>1 && throw(ArgumentError( + "generic arrays of rank greater than one need owner-provided coordinate interpretation")) + end + coordinates=map(records) do product + c=product.coordinates + c.kind in (:matrix,:diagonal,:vector) ? + (c.kind===:vector ? c.positions : c.rows, + c.kind===:matrix ? c.columns : [1],c.samples) : + ([1],[1],collect(1:length(product.values isa Number || ismissing(product.values) ? [product.values] : product.values))) + end + xdata=map(records) do product + c=product.coordinates + if c.kind===:assemblies + return (values=string.(c.labels[c.assemblies]),quantity=Units.Quantity{:dimensionless}(), + unit=Units.units(:base,:dimensionless),label="Assembly") + elseif c.kind===:array + n=product.values isa Number || ismissing(product.values) ? 1 : length(product.values) + indices=get(c,:indices,()) + selected=length(indices)==1 ? only(indices) : Colon() + original=selected isa Integer ? [selected] : selected isa AbstractVector || selected isa AbstractRange ? collect(selected) : nothing + original===nothing || length(original)==n || throw(DimensionMismatch("retained element indices do not match values")) + return (values=original===nothing ? collect(1:n) : original, + quantity=Units.Quantity{:dimensionless}(),unit=Units.units(:base,:dimensionless), + label=original===nothing ? "Retained element position" : "Original element index") + end + c.frequencies===nothing && throw(ArgumentError( + "a frequency plot requires a frequency axis retained during observation")) + (values=c.frequencies,quantity=Units.Quantity{:frequency}(),unit=c.frequency_unit) + end + observations=map(records,coordinates) do product,indices + merge(product,(values=reshape(product.values isa Number || ismissing(product.values) ? + [product.values] : product.values,length.(indices)...),)) + end + resolutions=map(q -> (clip=q.clipped,kind=q.thresholds===nothing ? :unassessed : q.thresholds.kind),records) + return (;xdata,observations,coordinates,resolutions) +end diff --git a/ext/LineCableModelsMakieExt/recipes/line_facets.jl b/ext/LineCableModelsMakieExt/recipes/line_facets.jl new file mode 100644 index 000000000..973661cab --- /dev/null +++ b/ext/LineCableModelsMakieExt/recipes/line_facets.jl @@ -0,0 +1,292 @@ +# Line faceting +# +# The recipe supplies physical coordinate descriptions. Shared plotting chooses +# panel and curve identities before native blocks are constructed. +function _coordinate_labels(product, coordinates, orientation) + c=product.coordinates + rows, columns, _=coordinates + if orientation===:rows + name=get(c,:domain,nothing)===:ModalDomain ? "Mode" : + get(c,:domain,nothing)===:PhaseDomain ? "Conductor" : "Row" + return ["$name $(c.labels[row])" for row in rows] + elseif c.kind===:vector + return ["$(c.axis_label) $(c.labels[row])" for row in rows] + elseif c.kind===:matrix && get(c, :column_domain, nothing)===:ModalDomain + return ["Conductor $(c.labels[row]), Mode $(c.column_labels[column])" + for row in rows for column in columns] + elseif c.kind===:matrix + return ["$(c.labels[row]) → $(c.labels[column])" + for row in rows for column in columns] + elseif c.kind===:diagonal + return ["Conductor $(c.labels[row])" for row in rows] + elseif c.kind===:assemblies + return string.(c.labels[c.assemblies]) + end + return string.(1:(length(rows) * length(columns))) +end + +function _quantity_panel_title(facet) + label=Units.label(facet.quantity) + facet.identity isa Tuple && first(facet.identity)===LineCableModels.statistics && + (label *= " · " * string(last(facet.identity))) + if facet.orientation===:rows + domain=facet.column_domain===nothing ? facet.domain : facet.column_domain + coordinate=domain===:ModalDomain ? "Mode" : domain===:PhaseDomain ? "Conductor" : "Column" + panel_label=facet.column_domain===:ModalDomain ? Units.symbol(facet.quantity) : label + return "$panel_label, $coordinate $(facet.column_label)" + end + if facet.orientation===:coordinates + panel_label=facet.column_domain===:ModalDomain ? Units.symbol(facet.quantity) : + label + return isempty(facet.source_label) ? panel_label : + "$panel_label · $(facet.source_label)" + end + facet.kind in (:matrix, :diagonal, :vector) || return label + facet.column_domain===:ModalDomain && + return "$(Units.symbol(facet.quantity)), mode $(facet.column_label) → $(facet.row_label)" + is_modal = facet.domain === :ModalDomain + coordinate = is_modal ? "mode" : "conductor" + + facet.kind===:vector && return "$label, $coordinate $(facet.row_label)" + + if facet.row == facet.column + return "$label, $coordinate $(facet.row)" + elseif is_modal + return "$label, intermodal residual ($(facet.row), $(facet.column))" + else + return "$label, conductors $(facet.row) → $(facet.column)" + end +end + +function _quantity_page_title(page) + facet=first(page.facets) + label=Units.label(facet.quantity) + facet.identity isa Tuple && first(facet.identity)===LineCableModels.statistics && + (label *= " · " * string(last(facet.identity))) + facet.orientation===:rows && !isempty(facet.source_label) && + (label *= " · " * facet.source_label) + return page.index==(1, 1) ? label : "$label ($(page.index[1]),$(page.index[2]))" +end + +function _panel_title(panel_titles, facet) + panel_titles === nothing && return _quantity_panel_title(facet) + panel_titles isa Function && return panel_titles(facet) + if panel_titles isa AbstractDict + quantity_symbol = Symbol(Units.symbol(facet.quantity)) + panel_title_keys = ( + (facet.identity, facet.panel_identity), + facet.panel_identity, + (facet.identity, facet.row, facet.column), + (quantity_symbol, facet.row, facet.column), + (facet.row, facet.column), + facet.identity, + quantity_symbol + ) + for title_key in panel_title_keys + haskey(panel_titles, title_key) && return panel_titles[title_key] + end + return _quantity_panel_title(facet) + end + throw(ArgumentError("panel_titles must be a dictionary, function, or nothing")) +end + +function _line_page( + published, + page, + ; + series_indices, + errorbar_sampling, + series_defaults, + series_attributes, + title, + figure_title, + title_attributes, + panel_titles, + fig_size, + xscale, + yscale, + legend_position, + legend_title, + legend_attributes, + legend_cap, + panel_legends, + controls, + display_plot, + export_theme, + open_export, + kwargs... +) + shell = _figure_layout(; size = fig_size, controls, kwargs...) + shell.canvas.default_rowgap = Fixed(24) + shell.canvas.default_colgap = Fixed(48) + rowgap!(shell.canvas, 24) + colgap!(shell.canvas, 48) + # The occupied rectangle preserves internal holes, not unselected perimeter. + cells=Dict{Tuple{Int, Int}, Any}() + for row in 1:page.dimensions[1], column in 1:page.dimensions[2] + + cell=_panel!(shell, (row, column)) + rowsize!(shell.canvas, row, Auto(false, 1)) + colsize!(shell.canvas, column, Auto(false, 1)) + cells[(row, column)]=cell + end + axes = Any[] + axis_series = Vector{NamedTuple}[] + panels = Any[] + resets = Function[] + requested_scales = NamedTuple[] + groups = Dict{Symbol, Vector{Any}}() + dependent_plots = Pair{Makie.Plot, Makie.Plot}[] + marker_coordinates = Dict{Makie.Plot, Any}() + group_order = Symbol[] + group_labels = Dict{Symbol, Any}() + panel_group_labels = Any[] + + for (facet, position) in zip(page.facets, page.positions) + observation = first(published).observations[facet.request_index] + curve_records=map(facet.curves) do curve + source=published[curve.source_index] + selected=curve.local_sample===nothing ? Colon() : + curve.local_sample:curve.local_sample + x=source.xdata[facet.request_index].values[selected] + y=collect(view(source.observations[facet.request_index].values, + curve.local_row, curve.local_column, selected)) + errors=get(source.observations[facet.request_index], :errors, nothing) + yerror=errors===nothing ? nothing : + collect(view(errors, curve.local_row, curve.local_column, selected)) + (; x, y, yerror) + end + xvalues = collect(Iterators.flatten( + record.x for record in curve_records)) + yvalues = collect(Iterators.flatten(record.y for record in curve_records)) + xobservation = merge(first(published).xdata[facet.request_index], (; + values = xvalues)) + yobservation = merge(observation, (; values = yvalues)) + panel=merge(cells[position], (; logical_position = facet.panel_identity)) + row, _ = position + bottom_row = maximum(first, page.positions) + attributes = (; + xlabelvisible = row == bottom_row, + xticklabelsvisible = row == bottom_row, + xticksvisible = row == bottom_row, + # Matrix cells have independent y limits. Each must expose its scale. + ylabelvisible = true, + yticklabelsvisible = true, + yticksvisible = true + ) + if all( + curve -> begin + resolution=published[curve.source_index].resolutions[facet.request_index] + resolution.clip && resolution.kind===:declared_floor + end, facet.curves) && all(ismissing, yvalues) + attributes = merge(attributes, + (subtitle = facet.identity isa Tuple && angle in facet.identity ? + "Undefined phase" : "Unavailable quantity",)) + end + axis, scales = _axis!( + panel.content, + xobservation, + yobservation; + title = _panel_title(panel_titles, facet), + xscale = xscale===nothing ? + (facet.kind in (:matrix, :diagonal, :vector) ? :log10 : :linear) : + xscale, + yscale, + xlabel = get(xobservation, :label, nothing), + ylabel = facet.column_domain===:ModalDomain ? + string(Units.symbol(facet.quantity), + isempty(Units.label(yobservation.unit)) ? "" : + " [$(Units.label(yobservation.unit))]") : nothing, + attributes = facet.kind===:assemblies ? + merge(attributes, (dim1_conversion = Makie.CategoricalConversion(),)) : + attributes, + native_attributes = shell.axis_attributes + ) + series = NamedTuple[] + scoped_labels = Dict{Symbol, Any}() + for (curve_identity, record) in zip(facet.curves, curve_records) + style_index=findfirst(==(curve_identity.slot), series_indices) + curve = record.y + yerror = record.yerror + group = curve_identity.group + curve_label = curve_identity.label + interval_support=Dict{Makie.Plot, Any}() + draw! = facet.kind in (:assemblies, :array) ? _points! : _line! + plots = draw!( + axis, + record.x, + curve; + dependent_plots, + label = curve_label, + color = series_defaults[style_index].attributes.color, + phase = series_defaults[style_index].phase, + endpoints = series_defaults[style_index].endpoints, + marker_coordinates, + errorbar_sampling, yerror, interval_support + ) + if !haskey(groups, group) + groups[group] = Any[] + push!(group_order, group) + group_labels[group] = curve_label + end + append!(groups[group], plots) + scoped_labels[group] = curve_label + binding=(; + xdata = facet.kind===:assemblies ? nothing : + record.x, + ydata = curve, + yerror, + interval_support, + sampled_intervals = facet.kind in (:matrix, :diagonal, :vector) && + errorbar_sampling === :staggered, + plots + ) + support(dimension) = begin + values=_visible_values((binding,), dimension; include_uncertainty = true) + isempty(values) ? () : extrema(values) + end + push!(series, merge(binding, (full_support = ( + x = support(:x), y = support(:y)),))) + end + reset! = _reset!(axis, series) + push!(axes, axis) + push!(axis_series, series) + push!(panels, panel) + push!(panel_group_labels, scoped_labels) + push!(resets, reset!) + push!(requested_scales, scales) + end + selected=Set(facet.panel_identity for facet in page.facets) + local_panel_legends=Tuple(pair + for pair in _panel_legend_pairs(panel_legends) if first(pair) in selected) + built = _finish_plot!( + shell, axes, resets, groups, group_order, group_labels; + requested_scales, axis_series, + dependent_plots, + series_attributes, + series_defaults, + marker_coordinates, + title, + figure_title, + title_attributes, + legend_position, + legend_attributes, + legend_cap, + legend_title, + panels, frame_cells = Tuple(values(cells)), + panel_legends = local_panel_legends, + panel_group_labels, + controls, + display_plot, + export_name = title, + export_theme, + open_export + ) + built.plot_state=merge(built.plot_state, + ( + panel_page = (; + index = page.index, dimensions = page.dimensions, origin = get(page, :origin, nothing), + coordinates = Tuple(f.panel_identity for f in page.facets)), + page_cells = cells)) + return built +end diff --git a/ext/LineCableModelsMakieExt/recipes/preview_data.jl b/ext/LineCableModelsMakieExt/recipes/preview_data.jl new file mode 100644 index 000000000..7d8f3a49e --- /dev/null +++ b/ext/LineCableModelsMakieExt/recipes/preview_data.jl @@ -0,0 +1,201 @@ +function _native_cable_title(display::Bool, design) + display ? + "Cable design preview: $(design.cable_id)" : "Cable design preview" +end + +function _native_cable_colorbars(display::Bool, design) + display ? + _material_schemes(DataModel.material_property_ranges(design)) : () +end + +function _default_preview_legend_group(region, milliken::Bool) + tag = region.source.tag + tag === :wire || return tag + patterns = region.placement.patterns + bounded = any(patterns) do entry + entry.pattern isa DataModel.BoundedPlacement + end + milliken && (bounded || isempty(patterns)) && return :stranded_sector + bounded && return :stranded_core + return tag +end + +function _preview_legend_group(region, legend_group, milliken::Bool) + tag = region.source.tag + resolved = if legend_group === nothing + _default_preview_legend_group(region, milliken) + elseif legend_group isa Function + legend_group(region) + elseif legend_group isa AbstractDict + get(legend_group, tag, tag) + else + throw(ArgumentError( + "legend_group must be a function, a tag-to-group dictionary, or nothing", + )) + end + resolved isa Union{Symbol, AbstractString} || throw(ArgumentError( + "preview presentation groups must be symbols or strings", + )) + isempty(strip(String(resolved))) && throw(ArgumentError( + "preview presentation groups cannot be empty", + )) + return Symbol(resolved) +end + +function _preview_legend_label(group::Symbol, legend_labels) + fallback = uppercasefirst(replace(String(group), '_' => ' ')) + resolved = if legend_labels === nothing + fallback + elseif legend_labels isa Function + legend_labels(group) + elseif legend_labels isa AbstractDict + get(legend_labels, group, fallback) + else + throw(ArgumentError( + "legend_labels must be a function, a group-to-label dictionary, or nothing", + )) + end + resolved isa AbstractString && !isempty(strip(resolved)) || throw(ArgumentError( + "preview legend labels must be nonempty strings", + )) + return String(resolved) +end + +function _native_design_shapes( + design, + xcenter, + ycenter; + display_legend::Bool, + display_dielectric_pattern::Bool = true, + legend_group = nothing, + legend_labels = nothing +) + offset = LineCableModels.Pose2(xcenter, ycenter, 0) + milliken = any(region -> region.source.tag === :milliken_fill, + design.geometry.regions) + identities = Dict{Symbol, NamedTuple}() + labelled_groups = Set{Symbol}() + polygons = PreviewPolygon[] + for region in design.geometry.regions + bounded = any(region.placement.patterns) do entry + entry.pattern isa DataModel.BoundedPlacement + end + enclosure_boundary = any(region.placement.patterns) do entry + entry.pattern isa DataModel.EnclosureBoundary + end + stroke = enclosure_boundary ? :black : + RGB(102 / 255, 109 / 255, 118 / 255) + width = enclosure_boundary ? 0.8 : (bounded ? 0.5 : 0.45) + presentation_group = _preview_legend_group(region, legend_group, milliken) + identity = get!(identities, presentation_group) do + (; + label = _preview_legend_label(presentation_group, legend_labels), + group = Symbol("preview_", presentation_group), + ) + end + primitive = LineCableModels.resolve(offset, region.primitive) + placed = LineCableModels.PlacedRegion( + region.source, + primitive, + region.terminal, + (patterns = region.placement.patterns,), + region.paths, + ) + for shape in DataModel.preview_shapes(placed) + label = display_legend && identity.group ∉ labelled_groups ? + identity.label : nothing + push!(polygons, PreviewPolygon( + shape.geometry, + label, + identity.group, + _material_color(shape.material; pattern = display_dielectric_pattern), + stroke, + width, + )) + end + push!(labelled_groups, identity.group) + end + return polygons +end + +function _native_system_limits(system, zoom_factor) + if zoom_factor !== nothing + zoom_factor isa Real || throw(ArgumentError( + "zoom_factor must be a positive real value", + )) + isfinite(zoom_factor) && zoom_factor > 0 || throw(ArgumentError( + "zoom_factor must be finite and greater than zero", + )) + end + horizontal = Float64[nominal(position.x) for position in system.positions] + vertical = Float64[nominal(position.y) for position in system.positions] + radii = Float64[nominal(outer_radius(design)) for design in system.designs] + center_x = isempty(horizontal) ? 0.0 : mean(horizontal) + center_y = isempty(vertical) ? -1.0 : mean(vertical) + half_x = isempty(horizontal) ? 1.0 : + max(maximum(horizontal .+ radii) - center_x, + center_x - minimum(horizontal .- radii)) + half_y = isempty(vertical) ? 1.0 : + max(maximum(vertical .+ radii) - center_y, + center_y - minimum(vertical .- radii)) + base_halfspan = max(half_x, half_y, eps(Float64)) + halfspan = base_halfspan * 1.05 * (zoom_factor === nothing ? 1.5 : Float64(zoom_factor)) + return ( + (center_x - halfspan, center_x + halfspan), + (center_y - halfspan, center_y + halfspan) + ) +end + +function _native_earth_colorbars(earth_model) + earth_model === nothing && return () + resistivities = Float64[] + permeabilities = Float64[] + permittivities = Float64[] + for layer in earth_model.layers[2:end] + push!(resistivities, nominal(layer.rho)) + push!(permeabilities, nominal(layer.mu_r)) + push!(permittivities, nominal(layer.eps_r)) + end + isempty(resistivities) && return () + ranges = (; + rho = extrema(resistivities), + mu_r = extrema(permeabilities), + eps_r = extrema(permittivities) + ) + return _earth_material_schemes(ranges; alpha = 0.25) +end + +function _native_system_shapes( + system, + display_legend; + display_dielectric_pattern::Bool = true, + legend_group = nothing, + legend_labels = nothing +) + polygons = PreviewPolygon[] + references = (PreviewReference([0.0], :air_earth, :black, 1.5),) + for (design, position) in zip(system.designs, system.positions) + append!(polygons, + _native_design_shapes( + design, + nominal(position.x), + nominal(position.y); + display_legend = display_legend && + (legend_group !== nothing || legend_labels !== nothing), + display_dielectric_pattern, + legend_group, + legend_labels + )) + end + return polygons, references +end + +function _native_system_title(display::Bool, system) + display ? + "Cable system cross-section: $(system.system_id)" : "Cable system cross-section" +end + +function _native_system_colorbars(display::Bool, earth_model) + display ? + _native_earth_colorbars(earth_model) : () +end diff --git a/ext/LineCableModelsMakieExt/recipes/preview_render.jl b/ext/LineCableModelsMakieExt/recipes/preview_render.jl new file mode 100644 index 000000000..aba266f5a --- /dev/null +++ b/ext/LineCableModelsMakieExt/recipes/preview_render.jl @@ -0,0 +1,552 @@ +function _preview_axis!( + shell, + position, + title, + polygons, + references, + limits, + groups, + group_order, + group_labels; + earth_model = nothing, + display_surface_gradient::Bool = true +) + unit = Units.units(:base, :meter) + unit_label = Units.label(unit) + panel = _panel!(shell, position) + axis = Axis( + panel.content; + merge( + (; title, + xlabel = "y [$unit_label]", + ylabel = "z [$unit_label]", + aspect = DataAspect(), + tellwidth = false, + tellheight = false), + shell.axis_attributes)... + ) + earth_spans = NamedTuple[] + surface_gradient = nothing + if earth_model !== nothing && !earth_model.vertical_layers + top = 0.0 + for (index, layer) in enumerate(earth_model.layers[2:end]) + bottom = top - nominal(layer.thickness) + span = hspan!(axis, 0.0, 0.0; + color = _material_color(layer; alpha = 0.25), + strokewidth = 0, xautolimits = false, yautolimits = false) + translate!(span, 0, 0, -100) + group = Symbol("earth_$index") + groups[group] = Any[span] + push!(group_order, group) + group_labels[group] = "Earth layer $index" + + if isfinite(bottom) + boundary = hlines!(axis, [bottom]; + color = RGBA(0.25, 0.27, 0.30, 0.25), linewidth = 0.6, + visible = span.visible, xautolimits = false, yautolimits = false) + translate!(boundary, 0, 0, -80) + end + push!(earth_spans, (; top, bottom, span)) + top = bottom + end + if display_surface_gradient + # One sky decoration, anchored at the earth surface and independent of + # the soil layers. Transparency reveals the background near z=0. + surface_gradient = hspan!(axis, zeros(64), zeros(64); + color = [RGBA(0.55, 0.76, 0.90, 0.45 * ((i - 1) / 63)^1.3) + for i in 1:64], + strokewidth = 0, xautolimits = false, yautolimits = false) + translate!(surface_gradient, 0, 0, -90) + end + end + for reference in references + plot = hlines!( + axis, + reference.values; + color = reference.color, + linewidth = reference.width + ) + if !haskey(groups, reference.group) + groups[reference.group] = Any[] + push!(group_order, reference.group) + end + push!(groups[reference.group], plot) + end + for polygon in polygons + plot = poly!( + axis, + polygon.geometry; + label = polygon.label, + color = polygon.color, + strokecolor = polygon.stroke, + strokewidth = polygon.width + ) + if !haskey(groups, polygon.group) + groups[polygon.group] = Any[] + push!(group_order, polygon.group) + end + push!(groups[polygon.group], plot) + polygon.label === nothing || (group_labels[polygon.group] = polygon.label) + end + limits === nothing || haskey(shell.axis_attributes, :limits) || (axis.limits[] = limits) + reset! = _reset!(axis) + if !isempty(earth_spans) + # HSpan owns full-width coverage. This scene-owned callback clips only + # the physical vertical extents, keeping infinite coordinates out of + # rendering and all background geometry out of automatic limits. + on(axis.scene, axis.finallimits; update = true) do view + lower = view.origin[2] + upper = lower + view.widths[2] + for entry in earth_spans + bottom = clamp(entry.bottom, lower, upper) + top = clamp(entry.top, lower, upper) + Makie.update!(entry.span, bottom, top) + end + if surface_gradient !== nothing + # Increase the blue from the transparent physical surface to + # the top of the view. Underground-only views contain no sky. + sky_height = max(upper, 0) + bands = length(surface_gradient[1][]) + lows = [clamp((i - 1) * sky_height / bands, lower, upper) for i in 1:bands] + highs = [clamp(i * sky_height / bands, lower, upper) for i in 1:bands] + Makie.update!(surface_gradient, lows, highs) + surface_gradient.visible[] = upper > 0 + end + end + end + return axis, reset!, panel +end + +function _preview_finish!( + shell, + axes, + resets, + groups, + group_order, + group_labels; + title, + figure_title = nothing, + title_attributes = (;), + series_attributes = nothing, + panels, + panel_legends, + panel_legend_titles = nothing, + display_legend, + legend_position, + legend_title, + legend_attributes, + legend_cap, + color_scales, + colorbar_position, + colorbar_attributes, + controls, + display_plot, + export_name, + export_theme, + open_export +) + return _finish_plot!( + shell, + axes, + resets, + groups, + group_order, + group_labels; + series_attributes, + title, + figure_title, + title_attributes, + legend_position = display_legend ? legend_position : nothing, + legend_title, + legend_attributes, + legend_cap, + panels, + panel_legends, + panel_legend_titles, + color_scales, + colorbar_position, + colorbar_attributes, + controls, + display_plot, + export_name, + export_theme, + open_export + ) +end + +function _preview( + design::DataModel.CableDesign; + x_offset::Real = 0.0, + y_offset::Real = 0.0, + display_dielectric_pattern::Bool = true, + display_legend::Bool = true, + display_id::Bool = false, + title = nothing, + figure_title = nothing, + title_attributes::NamedTuple = (;), + series_attributes = nothing, + panel_titles = nothing, + display_colorbars::Bool = true, + size::Tuple{Int, Int} = (900, 700), + legend_position = :right, + legend_title = nothing, + legend_attributes::NamedTuple = (; nbanks = 1), + legend_cap = 0.5, + panel_legends = (), + legend_group = nothing, + legend_labels = nothing, + colorbar_position = :bottom, + colorbar_attributes::NamedTuple = (; vertical = false), + guide_gap = (8, 8, 24, 8), + backend = nothing, + display_plot::Bool = true, + controls::Bool = true, + export_theme::Symbol = :default, + open_export::Bool = true, + kwargs... +) + _activate_plot_backend(backend) + isfinite(x_offset) && isfinite(y_offset) || throw(ArgumentError( + "preview offsets must be finite", + )) + display_title = title === nothing ? + _native_cable_title(display_id, design) : + String(title) + resolved_panel_titles = _panel_titles(panel_titles, 1; defaults = (display_title,)) + panel_title = resolved_panel_titles === nothing ? + display_title : only(resolved_panel_titles) + polygons = _native_design_shapes( + design, + x_offset, + y_offset; + # Retain presentation metadata even when the initial legend is hidden. + # `figurelegend!` may place it later without rebuilding geometry. + display_legend = true, + display_dielectric_pattern, + legend_group, + legend_labels + ) + color_scales = _material_schemes(DataModel.material_property_ranges(design)) + return with_theme(_plot_theme(export_theme = export_theme)) do + shell = _figure_layout(; size, controls, guide_gap, kwargs...) + groups = Dict{Symbol, Vector{Any}}() + order = Symbol[] + labels = Dict{Symbol, Any}() + axis, reset!, + panel = _preview_axis!( + shell, + (1, 1), + panel_title, + polygons, + (), + nothing, + groups, + order, + labels + ) + _preview_finish!( + shell, + Any[axis], + Function[reset!], + groups, + order, + labels; + title = display_title, + figure_title, + title_attributes, + panels = (panel,), + panel_legends, + display_legend, + legend_position, + legend_title, + legend_attributes, + legend_cap, + color_scales, + colorbar_position = display_colorbars ? colorbar_position : nothing, + colorbar_attributes, + controls, + display_plot, + export_name = design.cable_id, + series_attributes, + export_theme, + open_export + ) + end +end + +function _preview( + designs::AbstractVector{<:DataModel.CableDesign}; + layout = nothing, + display_dielectric_pattern::Bool = true, + title = nothing, + figure_title = nothing, + title_attributes::NamedTuple = (;), + series_attributes = nothing, + panel_titles = nothing, + display_colorbars::Bool = true, + display_legend::Bool = false, + legend_position = _omitted, + legend_title = nothing, + legend_attributes::NamedTuple = (;), + legend_cap = 0.5, + size::Tuple{Int, Int} = (1200, 900), + colorbar_position = :bottom, + colorbar_attributes::NamedTuple = (; vertical = false), + guide_gap = (8, 8, 24, 8), + panel_legends = (), + legend_group = nothing, + legend_labels = nothing, + backend = nothing, + display_plot::Bool = true, + controls::Bool = true, + export_theme::Symbol = :default, + open_export::Bool = true, + kwargs... +) + _activate_plot_backend(backend) + isempty(designs) && + throw(ArgumentError("a cable collection preview requires at least one design")) + capacity=_page_capacity(layout, (), (length(designs),)) + pages=_flow_pages(collect(eachindex(designs)), capacity) + for (identity, _) in _panel_legend_pairs(panel_legends) + identity isa Integer && !(identity isa Bool) && identity in eachindex(designs) || + throw(ArgumentError("preview panel identities are original collection indices")) + end + color_scales = _material_schemes(DataModel.material_property_ranges(designs)) + display_title = title === nothing ? "Cable design previews" : String(title) + resolved_panel_titles = _panel_titles( + panel_titles, length(designs); defaults = [design.cable_id for design in designs]) + built=LineCableModels.UIPlot[] + for page in pages + rendered=with_theme(_plot_theme(export_theme = export_theme)) do + rows, columns=page.dimensions + shell = _figure_layout(; size, + controls, guide_gap, kwargs...) + axes = Any[] + panels = Any[] + resets = Function[] + groups = Dict{Symbol, Vector{Any}}() + order = Symbol[] + labels = Dict{Symbol, Any}() + for (index, position) in zip(page.facets, page.positions) + design=designs[index] + polygons = _native_design_shapes( + design, + 0.0, + 0.0; + display_legend = true, + display_dielectric_pattern, + legend_group, + legend_labels + ) + axis, reset!, + panel = _preview_axis!( + shell, + position, + resolved_panel_titles === nothing ? + design.cable_id : resolved_panel_titles[index], + polygons, + (), + nothing, + groups, + order, + labels + ) + push!(axes, axis) + push!(panels, merge(panel, (logical_position = index,))) + push!(resets, reset!) + end + for row in 1:rows + rowsize!(shell.canvas, row, Relative(1 / rows)) + end + for column in 1:columns + colsize!(shell.canvas, column, Relative(1 / columns)) + end + _preview_finish!( + shell, + axes, + resets, + groups, + order, + labels; + title = display_title, + figure_title, + title_attributes, + panels, + panel_legends = Tuple(pair + for pair in _panel_legend_pairs(panel_legends) + if first(pair) in page.facets), + display_legend = display_legend || + (legend_position!==_omitted && legend_position!==nothing), + legend_position = legend_position===_omitted ? :right : legend_position, + legend_title, + legend_attributes, + legend_cap, + color_scales, + colorbar_position = display_colorbars ? colorbar_position : nothing, + colorbar_attributes, + controls, + display_plot = false, + export_name = length(pages)==1 ? "cable_design_previews" : + "cable_design_previews_$(page.index[1])", + series_attributes, + export_theme, + open_export + ) + end + rendered.plot_state=merge(rendered.plot_state, + (nominal_capacity = capacity, + panel_page = (index = page.index, dimensions = page.dimensions, + coordinates = Tuple(page.facets)))) + push!(built, rendered) + _frame_budget(rendered, capacity) + end + # Material meaning is computed once above and shared by every actual page. + _calibrate_frames!(built, capacity) + layout===nothing && foreach(_responsive_axis_grid!, built) + display_plot && foreach(p -> _display_figure!(p.figure, display_title), built) + return length(built)==1 ? only(built) : built +end + +function _preview( + system::DataModel.LineCableSystem; + earth_model = nothing, + zoom_factor = nothing, + display_dielectric_pattern::Bool = true, + display_surface_gradient::Bool = true, + display_legend::Bool = true, + display_id::Bool = false, + title = nothing, + figure_title = nothing, + title_attributes::NamedTuple = (;), + series_attributes = nothing, + panel_titles = nothing, + display_colorbars::Bool = true, + size::Tuple{Int, Int} = (900, 700), + legend_position = :right, + legend_title = nothing, + legend_attributes::NamedTuple = (;), + legend_cap = 0.5, + panel_legends = (), + legend_group = nothing, + legend_labels = nothing, + colorbar_position = :bottom, + colorbar_attributes::NamedTuple = (; vertical = false), + guide_gap = (8, 8, 24, 8), + backend = nothing, + display_plot::Bool = true, + controls::Bool = true, + export_theme::Symbol = :default, + open_export::Bool = true, + kwargs... +) + _activate_plot_backend(backend) + limits = _native_system_limits(system, zoom_factor) + polygons, + references = _native_system_shapes( + system, + display_legend || !isempty(_panel_legend_pairs(panel_legends)); + display_dielectric_pattern, + legend_group, + legend_labels + ) + color_scales = _native_earth_colorbars(earth_model) + display_title = title === nothing ? + _native_system_title(display_id, system) : + String(title) + resolved_panel_titles = _panel_titles(panel_titles, 1; defaults = (display_title,)) + panel_title = resolved_panel_titles === nothing ? + display_title : only(resolved_panel_titles) + return with_theme(_plot_theme(export_theme = export_theme)) do + shell = _figure_layout(; size, controls, guide_gap, kwargs...) + groups = Dict{Symbol, Vector{Any}}() + order = Symbol[] + labels = Dict{Symbol, Any}() + axis, reset!, + panel = _preview_axis!( + shell, + (1, 1), + panel_title, + polygons, + references, + limits, + groups, + order, + labels; + earth_model, + display_surface_gradient + ) + _preview_finish!( + shell, + Any[axis], + Function[reset!], + groups, + order, + labels; + title = display_title, + figure_title, + title_attributes, + panels = (panel,), + panel_legends, + display_legend, + legend_position, + legend_title, + legend_attributes, + legend_cap, + color_scales, + colorbar_position = display_colorbars ? colorbar_position : nothing, + colorbar_attributes, + controls, + display_plot, + export_name = system.system_id, + series_attributes, + export_theme, + open_export + ) + end +end + +function _material_scale(; + size::Tuple{Int, Int} = (800, 400), + figure_title = nothing, + title_attributes::NamedTuple = (;), + colorbar_position = _omitted, + colorbar_attributes::NamedTuple = (; vertical = false), + backend = nothing, + display_plot::Bool = true, + controls::Bool = true, + export_theme::Symbol = :default, + open_export::Bool = true, + kwargs... +) + _activate_plot_backend(backend) + title = "Material property color scale" + return with_theme(_plot_theme(export_theme = export_theme)) do + shell = _figure_layout(; size, controls, kwargs...) + _finish_plot!( + shell, + Any[], + Function[], + Dict{Symbol, Vector{Any}}(), + Symbol[], + Dict{Symbol, Any}(); + title, + figure_title, + title_attributes, + legend_position = nothing, + legend_attributes = (;), + legend_cap = 0.5, + color_scales = _material_schemes( + DataModel.material_property_ranges() + ), + colorbar_position = colorbar_position===_omitted ? Val(:content) : + colorbar_position, + colorbar_attributes, + controls, + display_plot, + export_name = "material_scale", + export_theme, + open_export + ) + end +end diff --git a/ext/LineCableModelsMakieExt/recipes/preview_types.jl b/ext/LineCableModelsMakieExt/recipes/preview_types.jl new file mode 100644 index 000000000..8bed96882 --- /dev/null +++ b/ext/LineCableModelsMakieExt/recipes/preview_types.jl @@ -0,0 +1,15 @@ +struct PreviewPolygon{G, C, S} + geometry::G + label::Union{Nothing, String} + group::Symbol + color::C + stroke::S + width::Float64 +end + +struct PreviewReference{V, C} + values::V + group::Symbol + color::C + width::Float64 +end diff --git a/ext/LineCableModelsMakieExt/series_styles.jl b/ext/LineCableModelsMakieExt/series_styles.jl new file mode 100644 index 000000000..98a8450dc --- /dev/null +++ b/ext/LineCableModelsMakieExt/series_styles.jl @@ -0,0 +1,186 @@ +# Apply styles to the native semantic handles before constructing legends and +# visibility controls. Every recipe uses this through the common plot shell. +function _series_styles!(groups, order, attributes; defaults=nothing, shared=(;), + marker_coordinates=nothing) + dependents = Pair{Makie.Plot,Makie.Plot}[] + # Nest coincident intervals in insertion order, without moving or sampling + # their coordinates. Stroke widths stay in screen units on logarithmic axes. + error_groups = filter(order) do group + # Only grouped curve and interval series receive defaults. A plotwindow + # caller's individually styled native primitives remain caller-owned. + any(handle -> handle isa Makie.Lines, groups[group]) && + any(handle -> handle isa Makie.Errorbars, groups[group]) + end + defaults === nothing || sort!(error_groups; + by=group -> -defaults[findfirst(==(group),order)].priority) + if length(error_groups) > 1 + for (index, group) in enumerate(error_groups) + width = (length(error_groups) - index) / (length(error_groups) - 1) + for handle in groups[group] + handle isa Makie.Errorbars || continue + handle.whiskerwidth[] = 4 + 6width + handle.linewidth[] = 1 + width + end + end + end + attributes === nothing && defaults === nothing && isempty(shared) && return dependents + styles = _series_attributes(attributes, length(order)) + shared_consumed = Set{Symbol}() + drawing_order = defaults === nothing ? collect(eachindex(order)) : + sortperm(collect(eachindex(order)); by=index -> defaults[index].priority) + for index in drawing_order + group, overrides = order[index], merge(shared,styles[index]) + automatic = defaults === nothing ? nothing : defaults[index] + automatic_attributes = automatic === nothing ? (;) : automatic.attributes + style = merge(automatic_attributes, overrides) + isempty(style) && continue + handles = groups[group] + consumed = Set{Symbol}() + markers = Any[] + for handle in handles + # Native wrappers such as PlotList forward attributes as properties. + direct = filter(key -> hasproperty(handle, key), keys(style)) + for key in direct + getproperty(handle, key)[] = getproperty(style, key) + push!(consumed, key) + end + if handle isa Union{Makie.Lines, Makie.LineSegments} && haskey(style, :marker) + push!(consumed, :marker) + style.marker === nothing && continue + marker_defaults = Makie.default_theme(handle.parent, Makie.Scatter) + marker_keys = filter(key -> haskey(marker_defaults, key) && + key ∉ (:color, :visible), keys(style)) + marker_attributes = NamedTuple{marker_keys}(map(key -> getproperty(style, key), marker_keys)) + coordinates = if automatic === nothing || haskey(overrides, :marker) + handle[1] + else + marker_coordinates[handle] + end + hollow = automatic !== nothing && automatic.hollow && + !haskey(overrides, :marker) + marker_attributes = merge(hollow ? + (strokecolor=handle.color[], strokewidth=1.5) : (;), marker_attributes) + points = scatter!(handle.parent, coordinates; + color=hollow ? :transparent : handle.color[], + visible=handle.visible[], marker_attributes...) + # Visibility follows the same owner visibility rule as uncertainty + # bars. The shared shell binds it once for the complete series. + push!(dependents, points => handle) + on(handle.parent, handle.color) do value + if hollow + haskey(overrides, :strokecolor) || (points.strokecolor[] = value) + else + points.color[] = value + end + end + push!(markers, points) + union!(consumed, marker_keys) + end + end + union!(shared_consumed,consumed) + unused = setdiff(keys(styles[index]), consumed) + isempty(unused) || throw(ArgumentError( + "series_attributes for $group are unsupported by its native plots: $(join(unused, ", "))")) + append!(handles, markers) + end + unused = setdiff(keys(shared),shared_consumed) + isempty(unused) || throw(ArgumentError("native series attributes are unsupported by these plots: $(join(unused, ", "))")) + return dependents +end + +# Select retained sample indices for both glyph kinds. Stable series slots, +# rather than visible-series count, preserve phases when formulas are filtered. +# Interval positions have priority. Short series may have no marker positions. +function _glyph_indices(n, width, phase; endpoints=false, uncertain_indices=Int[], + errorbar_sampling=:staggered) + slot, count = phase + n == 0 && return (markers=Int[], intervals=Int[]) + cycles = clamp(floor(Int, max(1, width) / max(80, 14count)), 1, 8) + stride = max(isempty(uncertain_indices) ? count : 2count, cld(n, cycles)) + # Result markers start inside each slot, leaving deterministic endpoints + # available to the independent reference role without shifting result slots. + offset = floor(Int, (slot - 0.5) * stride / count) + markers = collect((1 + offset):stride:n) + isempty(markers) && push!(markers, 1 + mod(slot - 1, n)) + if endpoints + push!(markers, 1, n) + sort!(unique!(markers)) + end + isempty(uncertain_indices) && return (markers=markers, intervals=Int[]) + errorbar_sampling === :all && return (markers=Int[], intervals=collect(1:n)) + + # Interval slots precede marker slots by half a slot in each cycle. + interval_offset = floor(Int, (slot - (endpoints ? 0.5 : 1)) * stride / count) + intervals = collect((1 + interval_offset):stride:n) + filter!(index -> index in uncertain_indices, intervals) + if isempty(intervals) + target = 1 + mod(interval_offset, n) + push!(intervals, uncertain_indices[argmin(abs.(uncertain_indices .- target))]) + end + filter!(index -> index ∉ intervals, markers) + if isempty(markers) && n > length(intervals) + for index in 1:n + if index ∉ intervals + push!(markers, index) + break + end + end + end + return (; markers, intervals) +end + +function _comparison_styles(indices, roles, count) + last_slot = maximum((index for (index, role) in zip(indices, roles) + if role !== :reference); init = 0) + # Extend once for the selected original slots, including gaps from filtering. + last_slot > 0 && _comparison_color(last_slot) + shapes = (:rect, :diamond, :dtriangle, :cross, :xcross, :pentagon, :hexagon) + return [(attributes=(; + color=role === :reference ? RGB(0.0, 0.0, 0.0) : _comparison_color(index), + marker=role === :reference ? :circle : + shapes[mod1(index, length(shapes))], + markersize=role === :reference ? 11 : 8, + linestyle=:solid), + hollow=role === :reference, endpoints=role === :reference, + phase=role === :reference ? (1,1) : (index,count), + priority=role === :reference ? 1 : 0) + for (index, role) in zip(indices, roles)] +end + +# A deterministic farthest-point palette in perceptual space. Candidate RGB +# colors are in gamut and avoid very light strokes on the white axis background. +# The prefix is stable: requesting more colors never changes existing identities. +# Oklab display criteria: lightness in [0.38,0.72], chroma >= 0.10, distance +# from white >= 0.30 and from black >= 0.40. These choices apply to a finite palette +# without guaranteeing distinguishability for arbitrarily many simultaneous curves. +_color_distance(a,b) = (a.l-b.l)^2 + (a.a-b.a)^2 + (a.b-b.b)^2 +function _curve_color_allowed(color) + lab=convert(Oklab,color) + return 0.38 <= lab.l <= 0.72 && hypot(lab.a,lab.b)>=0.10 && + _color_distance(lab,convert(Oklab,RGB(1.,1.,1.)))>=0.30^2 && + _color_distance(lab,convert(Oklab,RGB(0.,0.,0.)))>=0.40^2 +end +const _CURVE_COLORS = filter(_curve_color_allowed,RGB{Float64}[RGB(0.0,0.36,0.68)]) +function _comparison_color(index::Int) + index > 0 || throw(ArgumentError("series color indices must be positive")) + if index > length(_CURVE_COLORS) + candidates = [RGB{Float64}(HSV(hue, saturation, value)) + for hue in 0:5:355 for saturation in (0.55, 0.75, 0.95) + for value in (0.55, 0.7, 0.85)] + filter!(_curve_color_allowed,candidates) + coordinates = map(color -> convert(Oklab, color), candidates) + distance(a,b) = _color_distance(a,b) + black = convert(Oklab, RGB(0.,0.,0.)) + distances = [minimum((distance(color, convert(Oklab, selected)) + for selected in _CURVE_COLORS); init = distance(color, black)) + for color in coordinates] + while length(_CURVE_COLORS) < index + next = argmax(distances) + distances[next] > 0 || throw(ArgumentError("too many series for the distinct curve palette")) + push!(_CURVE_COLORS, candidates[next]) + selected = coordinates[next] + distances .= min.(distances, (distance(color, selected) for color in coordinates)) + end + end + return _CURVE_COLORS[index] +end diff --git a/ext/LineCableModelsMakieExt/shell.jl b/ext/LineCableModelsMakieExt/shell.jl new file mode 100644 index 000000000..20f0ee8be --- /dev/null +++ b/ext/LineCableModelsMakieExt/shell.jl @@ -0,0 +1,2093 @@ +const _TOOLBAR_BUTTON_SIZE = 32 +const _TOOLBAR_BUTTON_BACKGROUND = Makie.RGBf(0.94, 0.94, 0.94) +const _TOOLBAR_ICON_COLOR = Makie.RGBAf(0.15, 0.15, 0.15, 1.0) +const _COLORBAR_DOCK_LENGTH = 140 +const _MIN_WINDOW_SIZE = (600, 320) +const _TOOLBAR_REFRESH_ICON = "\uE5D5" +const _TOOLBAR_SAVE_ICON = "\uE161" +const _TOOLBAR_ICON_FONT = joinpath( + pkgdir(LineCableModels), + "assets", + "fonts", + "material-icons", + "MaterialIcons-Regular.ttf" +) + +function _plot_theme(; export_theme::Symbol = :default) + export_theme in (:default, :publication) || throw(ArgumentError( + "export_theme must be :default or :publication", + )) + return Theme( + backgroundcolor = :grey90, + fonts = (; icons = _TOOLBAR_ICON_FONT), + Axis = (; + titlesize = 15, + xlabelsize = 14, + ylabelsize = 14, + xticklabelsize = 14, + yticklabelsize = 14, + xminorgridvisible = false, + yminorgridvisible = false, + xminorticksvisible = false, + yminorticksvisible = false + ), + Button = (; buttoncolor = _TOOLBAR_BUTTON_BACKGROUND), + Legend = (; fontsize = 14, labelsize = 14), + Colorbar = (; labelsize = 14, ticklabelsize = 14) + ) +end + +function _activate_plot_backend(backend) + backend === nothing && begin + Makie.current_backend() isa Module || throw(ArgumentError( + "No Makie backend is active. Load CairoMakie, GLMakie, or WGLMakie first.", + )) + return nothing + end + backend isa Symbol || throw(ArgumentError( + "backend must be :cairo, :gl, :wgl, or nothing", + )) + extension_name, + package = if backend === :cairo + (:LineCableModelsCairoMakieExt, "CairoMakie") + elseif backend === :gl + (:LineCableModelsGLMakieExt, "GLMakie") + elseif backend === :wgl + (:LineCableModelsWGLMakieExt, "WGLMakie") + else + throw(ArgumentError("Unknown backend :$backend. Use :cairo, :gl, or :wgl.")) + end + extension = Base.get_extension(LineCableModels, extension_name) + extension === nothing && throw(ArgumentError( + "Backend :$backend is not loaded. Run `using $package` first.", + )) + Base.invokelatest(extension.activate!) + return nothing +end + +function _display_figure!(figure, title::AbstractString) + if current_backend_symbol() === :gl + extension = Base.get_extension(LineCableModels, :LineCableModelsGLMakieExt) + extension === nothing && + error("GLMakie is active but its LineCableModels extension is unavailable") + viewport = figure.scene.viewport[] + minimum_size = Tuple( + min(Int(viewport.widths[index]), _MIN_WINDOW_SIZE[index]) + for index in 1:2 + ) + screen = Base.invokelatest( + extension.make_screen, + String(title); + minimum_size + ) + Base.display(screen, figure) + else + display(figure) + end + return figure +end + +function _figure_layout(; + size, controls::Bool, + axis::NamedTuple = (;), figure::NamedTuple = (;), + widgets = (), guide_gap = 8, guide_spacing = (;), + colorbar_position = _omitted, colorbar_attributes = (;), colorbar_group_attributes = (;), kwargs...) + guide_gap=_guide_gap(guide_gap) + guide_spacing=_guide_spacing(guide_spacing) + _colorbar_group_attributes(colorbar_group_attributes) + axis_keys = (propertynames(Axis)..., :palette) + axis_attributes = merge( + (; + (key=>value for (key, value) in kwargs if key in axis_keys)...), axis) + series_attributes = (; (key=>value for (key, value) in kwargs if key ∉ axis_keys)...) + size isa Tuple{Int, Int} && all(>(0), size) || + throw(ArgumentError("figure size must be a tuple of two positive integers")) + figure = Figure(; merge((; size, figure_padding = (12, 12, 12, 12)), figure)...) + root = figure.layout + root.default_rowgap = Fixed(4) + rowgap!(root, 4) + body = GridLayout(3, 3; tellwidth = false, tellheight = false) + body.default_rowgap = Fixed(0) + body.default_colgap = Fixed(0) + rowgap!(body, 0) + colgap!(body, 0) + canvas = GridLayout( + ; width = Relative(1), height = Relative(1), + tellwidth = false, tellheight = false + ) + canvas.default_rowgap = Fixed(6) + canvas.default_colgap = Fixed(6) + rowgap!(canvas, 6) + colgap!(canvas, 6) + body[2, 2] = canvas + toolbar = GridLayout(; halign = :left, valign = :bottom) + toolbar.default_colgap = Fixed(4) + colgap!(toolbar, 4) + status = Observable("Ready") + chrome=Any[] + if controls + root[2, 1] = body + root[1, 1] = toolbar + status_label=Label(root[3, 1], status; halign = :left, fontsize = 11) + append!(chrome, (toolbar, status_label)) + rowsize!(root, 1, Auto(true)) + rowsize!(root, 2, Auto(false, 1)) + rowsize!(root, 3, Auto(true)) + else + root[1, 1] = body + rowsize!(root, 1, Auto(false, 1)) + end + colsize!(root, 1, Auto(false, 1)) + rowsize!(body, 1, Fixed(0)) + rowsize!(body, 2, Auto(false, 1)) + rowsize!(body, 3, Fixed(0)) + colsize!(body, 1, Fixed(0)) + colsize!(body, 2, Auto(false, 1)) + colsize!(body, 3, Fixed(0)) + return (; figure, reference_size = Tuple(figure.scene.viewport[].widths), + root, body, canvas, toolbar, status, chrome, axis_attributes, + series_attributes, widgets, guide_gap, guide_spacing, + colorbar_position, colorbar_attributes, colorbar_group_attributes) +end + +function _toolbar_icon(value) + return Makie.rich( + value; + font = :icons, + fontsize = 18, + color = _TOOLBAR_ICON_COLOR, + offset = (0, -0.18) + ) +end + +"""Smooth, sign-preserving base-10 logarithm for a native axis.""" +struct _SignedLog10 <: Function + "Positive reference magnitude in displayed axis units." + reference::Float64 +end + +function (scale::_SignedLog10)(value) + magnitude, reference = abs(value), scale.reference + return sign(value) * (magnitude <= reference ? log1p(magnitude / reference) / log(10) : + log10(magnitude) - log10(reference) + log1p(reference / magnitude) / log(10)) +end + +function Makie.inverse_transform(scale::_SignedLog10) + reference = scale.reference + return value -> begin + exponent = abs(value) * log(10) + sign(value) * (abs(value) <= log10(2) ? reference * expm1(exponent) : + exp(log(reference) + exponent + log(-expm1(-exponent)))) + end +end + +function _axis_scale(symbol::Symbol, reference::Real = 1.0) + symbol === :linear && return Base.identity + symbol === :log10 && return Base.log10 + # Keep the reference with the transform. Ticks and controls use this same scale. + symbol === :pseudolog10 && return Makie.ReversibleScale( + _SignedLog10(reference); + limits = (0.0f0, 3.0f0), name = :pseudolog10) + throw(ArgumentError("unsupported axis scale :$symbol")) +end +_axis_scale(scale) = scale + +function _scientific_exponent(values) + magnitudes = Float64[] + for value in values + numeric = LineCableModels.nominal(value) + numeric isa Real || continue + converted = Float64(numeric) + isfinite(converted) && !iszero(converted) && push!(magnitudes, abs(converted)) + end + isempty(magnitudes) && return nothing + return 3fld(floor(Int, log10(maximum(magnitudes))), 3) +end + +function _linear_tickformat(exponent::Int) + # Normalize subnormal magnitudes without underflowing the power of ten. + shift = exponent < -307 ? 308 : 0 + scale = 10.0^(exponent + shift) + return function (values) + mantissas = [(Float64(value) * 10.0^shift) / scale for value in values] + # Signed zeros are the same tick. A range transition can temporarily + # underflow values through the preceding exponent before it is replaced. + finite = sort!(unique(iszero(value) ? 0.0 : value + for value in mantissas if isfinite(value))) + isempty(finite) && return string.(mantissas) + magnitude = maximum(abs, finite) + digits = iszero(magnitude) ? 0 : max(0, 3 - floor(Int, log10(magnitude))) + if length(finite) > 1 + # Retain distinct ticks when zooming into a narrow, offset interval. + spacing = minimum(diff(finite)) + digits = max(digits, 1 - floor(Int, log10(spacing))) + end + digits = clamp(digits, 0, 17) + return map(mantissas) do value + label = @sprintf("%.*f", digits, value) + digits > 0 && (label = rstrip(rstrip(label, '0'), '.')) + label == "-0" ? "0" : label + end + end +end + +function _scientific_tickformat(values) + # Keep distinct physical ticks distinct, including nondecade multiples of + # the signed-log reference. Decimal conversion also handles subnormals. + labels = String[] + distinct = length(unique(iszero(value) ? 0.0 : value for value in values)) + for digits in 2:16 + labels = [iszero(value) ? "0" : @sprintf("%.*e", digits, value) + for value in values] + length(unique(labels)) == distinct && break + end + return map(labels) do label + label == "0" && return label + parts = split(label, 'e') + length(parts) == 2 || return label + coefficient = rstrip(rstrip(first(parts), '0'), '.') + prefix = coefficient == "1" ? "" : + coefficient == "-1" ? "−" : + replace(coefficient, "-" => "−") * "×" + exponent = replace(string(parse(Int, last(parts))), "-" => "−") + Makie.rich(prefix * "10", + Makie.superscript(exponent; offset = Makie.Vec2f(0.1, 0.0))) + end +end + +function _decade_ticks(vmin, vmax, count::Int) + isfinite(vmin) && isfinite(vmax) && 0 < vmin <= vmax || return Float64[] + span = log10(vmax)-log10(vmin) + if span < 2 + isapprox(vmin, vmax; rtol = sqrt(eps(Float64)), atol = 0) && + return unique([vmin, vmax]) + ticks = Float64[] + for exponent in floor(Int, log10(vmin)):floor(Int, log10(vmax)) + lower, upper = max(vmin, 10.0^exponent), min(vmax, 10.0^(exponent+1)) + lower < upper || continue + budget = max(2, ceil(Int, count*(log10(upper)-log10(lower))/span)+1) + # Locator arithmetic is local to one decade, including extreme SI + # magnitudes. Only the returned positions use the published units. + factor = 10.0^clamp(exponent, -307, 307) + values = Makie.get_tickvalues(Makie.LinearTicks(budget), lower/factor, upper/factor) + append!(ticks, filter(x -> isfinite(x) && lower <= x <= upper, values .* + factor)) + vmin <= 10.0^exponent <= vmax && push!(ticks, 10.0^exponent) + end + isempty(ticks) && append!(ticks, (vmin, vmax)) + return sort!(unique!(ticks)) + end + first_exponent = ceil(Int, log10(vmin)) + last_exponent = floor(Int, log10(vmax)) + step = max(1, cld(last_exponent - first_exponent, max(1, count - 1))) + return 10.0 .^ (first_exponent:step:last_exponent) +end + +function _axis_label(label, exponent::Int, scale::Symbol) + scale === :log10 && return label + iszero(exponent) && return label + formatted = replace(string(exponent), "-" => "−") + return Makie.rich( + label, + " × 10", + Makie.superscript(formatted; offset = Makie.Vec2f(0.1, 0.0)) + ) +end + +function _set_axis!(entries::AbstractVector, dim::Symbol, scale = nothing) + dim in (:x, :y) || throw(ArgumentError("axis dimension must be :x or :y")) + index = dim === :x ? 1 : 2 + # Resolve and validate the complete page before any native observable changes. + # These bindings use the native axes without reinterpreting result data. + targets = map(entries) do entry + requested_scale = scale === nothing ? entry.scale : scale + target = _axis_scale(requested_scale) + context = "axis :$dim ($(repr(entry.axis.title[])))" + requested = entry.axis.limits[] + requested = length(requested) == 4 ? (requested[1:2], requested[3:4]) : requested + bounds = requested[index] === nothing ? () : requested[index] + all(value -> value === nothing || isfinite(value), bounds) || + throw(DomainError(bounds, "$context requires finite explicit limits")) + values = _visible_values(entry.axis, dim, entry.series; + all_samples = requested_scale === :log10) + if requested_scale === :log10 && + (any(<=(0), values) || any(value -> value!==nothing && value<=0, bounds)) + reference = minimum( + (abs(value) + for value in Iterators.flatten((values, bounds)) + if value !== nothing && isfinite(value) && !iszero(value)); + init = Inf) + target = _axis_scale(:pseudolog10, isfinite(reference) ? reference : 1.0) + end + if target === Base.log10 + all(>(0), values) && all(value -> value === nothing || value > 0, bounds) || + throw(DomainError(bounds, + "logarithmic $context requires positive visible data, uncertainty bounds and explicit limits")) + end + inverse=Makie.inverse_transform(target) + inverse===nothing && + throw(ArgumentError("$context requires a native scale with an inverse")) + for value in bounds + value===nothing && continue + transformed=target(value) + isfinite(transformed) && isfinite(inverse(transformed)) || throw(DomainError( + value, "$context requires finite transformed explicit bounds and inverse values")) + end + empty_bounds=isempty(values) ? Makie.defaultlimits(requested[index], target) : + nothing + if !isempty(values) + lower, upper = extrema(values) + if isapprox(lower, upper; rtol = sqrt(eps(Float64)), atol = 0) + lower, upper = _constant_limits(values, values, target === Base.log10) + end + explicit = isempty(bounds) ? (nothing, nothing) : bounds + lower, upper = something(explicit[1], lower), something(explicit[2], upper) + transformed = (target(lower), target(upper)) + all(isfinite, transformed) && transformed[1] < transformed[2] || + throw(DomainError((lower, upper), "$context requires distinct finite transformed limits")) + end + if empty_bounds!==nothing + transformed=target.(empty_bounds) + all(isfinite, transformed) && transformed[1] ismissing(value) ? NaN : LineCableModels.nominal(value), values) + errors = map(value -> ismissing(value) ? 0.0 : LineCableModels.uncertainty(value), values) + return nominal_values, any(error -> !iszero(error), errors) ? errors : nothing +end + +function _line!(axis, xdata, ydata; dependent_plots, label, color = nothing, + visible = true, phase = (1, 1), endpoints = false, marker_coordinates = nothing, + errorbar_sampling = :all, yerror = nothing, interval_support = nothing) + x, xerror = _numeric_values(xdata) + y, inferred_error = _numeric_values(ydata) + yerror = yerror === nothing ? inferred_error : + (all(iszero, yerror) ? nothing : Float64.(yerror)) + attributes = color === nothing ? (; linewidth = 2) : (; linewidth = 2, color) + plots = Any[lines!(axis, x, y; label, visible, attributes...)] + line = first(plots) + uncertain_indices = findall(eachindex(x)) do index + isfinite(x[index]) && isfinite(y[index]) && + any( + error -> error !== nothing && isfinite(error[index]) && + !iszero(error[index]), (xerror, yerror)) + end + glyphs = if marker_coordinates !== nothing || errorbar_sampling === :staggered + lift(line[1], axis.scene.viewport) do points, viewport + _glyph_indices(length(points), viewport.widths[1], phase; + endpoints, uncertain_indices = filter(<=(length(points)), uncertain_indices), + errorbar_sampling) + end + else + nothing + end + if marker_coordinates !== nothing + marker_coordinates[line] = lift(line[1], glyphs) do points, indices + points[indices.markers] + end + end + error_color = color === nothing ? :black : color + for (direction, error) in ((:y, yerror), (:x, xerror)) + error === nothing && continue + values = only(Makie.convert_arguments(Makie.Errorbars, x, y, error)) + coordinates = if errorbar_sampling === :staggered + # A native line edit does not rewrite independent uncertainty bars. + lift(axis.scene.viewport) do viewport + indices=_glyph_indices(length(values), viewport.widths[1], phase; + endpoints, uncertain_indices, errorbar_sampling) + values[indices.intervals] + end + else + values + end + bars = errorbars!(axis, coordinates; color = error_color, direction, + whiskerwidth = 3, linewidth = 1, visible) + interval_support===nothing || (interval_support[bars]=coordinates) + on(axis.scene, line.color) do value + bars.color[] = value + end + push!(plots, bars) + end + append!(dependent_plots, (plot => first(plots) for plot in Iterators.drop(plots, 1))) + return plots +end + +function _visible_values(series, dim::Symbol; include_uncertainty::Bool = false, + all_samples::Bool = false) + values = Float64[] + for item in series + first(item.plots).visible[] || continue + data = dim === :x ? item.xdata : item.ydata + data === nothing && continue + if !include_uncertainty + bounds=Makie.data_limits(first(item.plots)) + index=dim===:x ? 1 : 2 + lower=bounds.origin[index] + upper=lower+bounds.widths[index] + isfinite(lower) && isfinite(upper) && append!(values, (lower, upper)) + continue + elseif !all_samples && haskey(item, :full_support) + append!(values, getproperty(item.full_support, dim)) + continue + end + errors = get(item, dim === :x ? :xerror : :yerror, nothing) + for (index, sample) in enumerate(data) + nominal_value = LineCableModels.nominal(sample) + nominal_value isa Real || continue + numeric = Float64(nominal_value) + isfinite(numeric) || continue + interval = abs(Float64(errors === nothing ? + LineCableModels.uncertainty(sample) : errors[index])) + if isfinite(interval) && !iszero(interval) + push!(values, numeric - interval, numeric + interval) + else + push!(values, numeric) + end + end + end + return values +end + +# Primary categorical and element-index points are all data samples, never +# decorative curve glyphs. Native conversion owns categorical coordinates. +function _points!(axis, xdata, ydata; dependent_plots, label, color = nothing, + visible = true, phase = (1, 1), endpoints = false, marker_coordinates = nothing, + errorbar_sampling = :all, yerror = nothing, interval_support = nothing) + y, inferred=_numeric_values(ydata) + errors=yerror===nothing ? inferred : yerror + attributes=color===nothing ? (;) : (; color) + points=scatter!(axis, xdata, y; label, visible, attributes...) + plots=Any[points] + if errors!==nothing && !all(iszero, errors) + bars=errorbars!(axis, xdata, y, errors; visible, color = something(color, :black)) + on(axis.scene, points.color) do value + bars.color[]=value + end + push!(plots, bars) + push!(dependent_plots, bars=>points) + end + return plots +end + +function _visible_values(axis::Axis, dim::Symbol, series = (); all_samples::Bool = false) + index = dim === :x ? 1 : 2 + exclude_plot = plot -> !to_value(get(plot, :visible, true)) || + !to_value(get(plot, Symbol(dim, :autolimits), true)) || + to_value(get(plot, :space, :data)) !== :data + bounds = Makie.data_limits(axis.scene, exclude_plot) + lower, upper = bounds.origin[index], bounds.origin[index] + bounds.widths[index] + values = isfinite(lower) && isfinite(upper) ? [lower, upper] : Float64[] + if all_samples + # Extrema omit interior samples near zero and can lose a small endpoint + # in origin + width. Native child positions also include error-bar ends. + function append_samples!(plot) + exclude_plot(plot) && return nothing + if plot isa Union{Makie.Lines, Makie.Scatter, Makie.LineSegments} + for point in plot[1][] + all(isfinite, point) && push!(values, point[index]) + end + else + foreach(append_samples!, plot.plots) + end + return nothing + end + foreach(append_samples!, axis.scene.plots) + end + for item in series + get(item, :sampled_intervals, false) || continue + # Undrawn intervals still constrain scientific axes. Independently hidden + # bars, disabled autolimits and caller-added native plots remain respected. + support=get(item, :interval_support, nothing) + any( + plot -> plot isa Makie.Errorbars && plot.direction[] === dim && + plot.visible[] && to_value(get(plot, Symbol(dim, :autolimits), true)) && + (support===nothing || !haskey(support, plot) || + isequal(plot[1][], to_value(support[plot]))), + item.plots) || continue + append!(values, _visible_values((item,), dim; include_uncertainty = true, all_samples)) + end + return all_samples || isempty(values) ? values : collect(extrema(values)) +end + +function LineCableModels.plotwindow( + callback::F; + title::AbstractString, + figure_title = nothing, + title_attributes::NamedTuple = (;), + series_attributes = nothing, + size::Tuple{Int, Int} = (800, 400), + backend = nothing, + display_plot::Bool = true, + controls::Bool = true, + export_theme::Symbol = :default, + open_export::Bool = true, + export_name::AbstractString = title, + kwargs... +) where {F} + _activate_plot_backend(backend) + return with_theme(_plot_theme(export_theme = export_theme)) do + shell = _figure_layout(; size, controls, kwargs...) + callback(shell.canvas) + axes = Any[content for content in shell.figure.content if content isa Axis] + # Only caller-supplied overrides apply to caller-constructed axes. Keep + # native defaults and conversions intact. Scale changes use common preflight. + for axis in axes, (key, value) in pairs(shell.axis_attributes) + + key in (:xscale, :yscale) || setproperty!(axis, key, value) + end + requested_scales = !any(key -> key in (:xscale, :yscale), keys(shell.axis_attributes)) ? + nothing : + [(x = get(shell.axis_attributes, :xscale, axis.xscale[]), + y = get(shell.axis_attributes, :yscale, axis.yscale[])) + for axis in axes] + resets = Function[_reset!(axis) for axis in axes] + native = Any[handle for axis in axes for handle in axis.scene.plots] + order = [Symbol("series_$index") for index in eachindex(native)] + groups = Dict(group => Any[handle] for (group, handle) in zip(order, native)) + _finish_plot!( + shell, + axes, + resets, + groups, + order, + Dict(group => something(to_value(get(handle, :label, nothing)), string(group)) + for (group, handle) in zip(order, native)); + requested_scales, + series_attributes, + title = String(title), + figure_title, + title_attributes, + legend_position = nothing, + legend_attributes = (;), + controls, + display_plot, + export_name = String(export_name), + export_theme, + open_export + ) + end +end + +function _statistical_plot( + draw::F, + xobservation, + yobservation; + title, + figure_title = nothing, + title_attributes = (;), + series_attributes = nothing, + panel_titles = nothing, + fig_size, + backend, + display_plot, + controls, + export_theme, + open_export, + legend_position = :right, + legend_title = nothing, + legend_labels = nothing, + legend_attributes = (;), + legend_cap = 0.5, + panel_legends = (), + xlabel = nothing, + ylabel = nothing, + xscale = :linear, + yscale = :linear, + kwargs... +) where {F} + _activate_plot_backend(backend) + resolved_panel_titles = _panel_titles(panel_titles, 1) + panel_title = resolved_panel_titles === nothing ? title : + only(resolved_panel_titles) + return with_theme(_plot_theme(export_theme = export_theme)) do + shell = _figure_layout(; size = fig_size, controls, kwargs...) + panel = _panel!(shell, (1, 1)) + axis, scales = _axis!( + panel.content, + xobservation, + yobservation; + title = panel_title, + xscale, + yscale, + xlabel, + ylabel, + native_attributes = shell.axis_attributes + ) + groups = Dict{Symbol, Vector{Any}}() + order = Symbol[] + labels = Dict{Symbol, Any}() + series = NamedTuple[] + draw(axis, groups, order, labels, series) + _relabel_legend!(labels, groups, order, legend_labels) + reset! = _reset!(axis, series) + _finish_plot!( + shell, + Any[axis], + Function[reset!], + groups, + order, + labels; + requested_scales = (scales,), + axis_series = (series,), + series_attributes, + title, + figure_title, + title_attributes, + legend_position, + legend_title, + legend_attributes, + legend_cap, + panels = (panel,), + panel_legends, + controls, + display_plot, + export_name = title, + export_theme, + open_export + ) + end +end + +function _constant_limits(values, interval_values, logarithmic::Bool) + if logarithmic + all(>(0), interval_values) || throw(DomainError( + interval_values, + "logarithmic axes require strictly positive data" + )) + center = sum(log, extrema(values))/2 + halfspan = max(log(1.05), 2maximum(value -> abs(log(value)-center), interval_values)) + lower, upper = exp(center-halfspan), exp(center+halfspan) + isfinite(upper) && 0 < lower < upper && log10(lower) < log10(upper) || + throw(DomainError((lower, upper), "automatic logarithmic bounds are not representable")) + return lower, upper + end + all(iszero, interval_values) && return (-1.0, 1.0) + lower, upper = extrema(values) + center = lower / 2 + upper / 2 + base_halfspan = max(0.05abs(center), eps(center)) + interval_halfspan = maximum(abs(value - center) for value in interval_values) + halfspan = max(base_halfspan, 2interval_halfspan) + return center - halfspan, center + halfspan +end + +function _axis_format!(axis) + for (index, dim) in enumerate((:x, :y)) + scale = getproperty(axis, Symbol(dim, :scale)) + ticks = getproperty(axis, Symbol(dim, :ticks)) + tickformat = getproperty(axis, Symbol(dim, :tickformat)) + label = getproperty(axis, Symbol(dim, :label)) + labelsize = getproperty(axis, Symbol(dim, :ticklabelsize)) + labelfont = getproperty(axis, Symbol(dim, :ticklabelfont)) + rotation = getproperty(axis, Symbol(dim, :ticklabelrotation)) + conversion = getproperty(axis, Symbol(:dim, index, :_conversion)) + lineaxis = getproperty(axis, Symbol(dim, :axis)) + raw_label = Ref{Any}(label[]) + rendered_label = Ref{Any}(label[]) + installed_ticks = Ref{Any}(Makie.automatic) + installed_format = Ref{Any}(Makie.automatic) + installed_mode = Ref{Any}(nothing) + previous_inputs = Ref{Any}(nothing) + updating = Ref(false) + # Reuse one native text measurement per dimension, outside the data + # scene. Font changes and long, precise labels affect available density. + probe = text!(axis.blockscene, 0, 0; text = "", markerspace = :data, + visible = false, inspectable = false) + function update!() + updating[] && return nothing + updating[] = true + try + limits = axis.finallimits[] + current_scale, current_ticks = scale[], ticks[] + current_format, current_label = tickformat[], label[] + # A transform notification precedes Makie's own limit reset. + # Do not send its old, possibly negative linear view to a log locator. + current_scale === Base.log10 && limits.origin[index] <= 0 && return nothing + label_changed = current_label !== rendered_label[] + label_changed && (raw_label[] = current_label) + owned_ticks = current_ticks === installed_ticks[] || + current_ticks === Makie.automatic + owned_format = current_format === installed_format[] || + current_format === Makie.automatic + numeric = conversion[] === nothing + lower, upper = limits.origin[index], + limits.origin[index] + limits.widths[index] + inputs=(lower, upper, current_scale, axis.scene.viewport[].widths[index], + labelsize[], labelfont[], rotation[], conversion[], current_label) + # Native layout can notify every peer axis without changing + # these inputs. Restarting density fitting then briefly installs + # denser ticks and recursively changes all peer protrusions. + # Explicit native overrides and automatic resets still run below. + owned_ticks && owned_format && !label_changed && + current_ticks===installed_ticks[] && + current_format===installed_format[] && + isequal(inputs, previous_inputs[]) && return nothing + signed = current_scale isa Makie.ReversibleScale{_SignedLog10} + logarithmic = current_scale === Base.log10 || signed + decades = logarithmic && current_scale(upper) - current_scale(lower) >= 2 + exponent = something(_scientific_exponent((lower, upper)), 0) + signed_linear = signed && + max(abs(lower), abs(upper)) < + current_scale.forward.reference + mode = if numeric && (current_scale === Base.identity || logarithmic) && + (owned_ticks || current_ticks isa AbstractVector{<:Real}) + decades ? (:scientific, 0) : (:linear, exponent) + else + nothing + end + if owned_format && (mode != installed_mode[] || current_format === Makie.automatic) + installed_format[] = if mode === nothing + Makie.automatic + elseif first(mode) === :linear + _linear_tickformat(exponent) + else + _scientific_tickformat + end + installed_mode[] = mode + tickformat[] === installed_format[] || + (tickformat[] = installed_format[]) + end + formatted = owned_format && mode !== nothing && first(mode) === :linear ? + _axis_label(raw_label[], exponent, :linear) : raw_label[] + if label_changed || !isequal(formatted, current_label) + rendered_label[] = formatted + label[] = formatted + end + if owned_ticks + pixels = axis.scene.viewport[].widths[index] + spacing = (index == 1 && current_scale === Base.identity ? 5.5 : 3.0) * + labelsize[] + count = clamp(floor(Int, pixels / max(1, spacing)), 3, 10) + probe.font[] = labelfont[] + probe.fontsize[] = labelsize[] + probe.rotation[] = rotation[] + # Native locator and formatter notification is synchronous. Fit + # its rendered strings, reducing only automatic tick density. + for _ in 1:9 + selected = if !numeric + Makie.automatic + elseif current_scale === Base.identity + # A native fit can briefly expose equal or adjacent + # Float64 endpoints before automatic padding runs. + # Preserve explicit narrow zooms too, without asking + # LinearTicks to subdivide an unrepresentable interval. + let count=count + (lo, hi) -> isapprox(lo, hi; rtol = sqrt(eps(Float64)), atol = 0) ? + unique([lo, hi]) : + Makie.get_tickvalues(Makie.LinearTicks(count), lo, hi) + end + elseif current_scale === Base.log10 + _decade_ticks(lower, upper, count) + elseif signed + # Native PseudologTicks dispatches on Makie's scale + # instance. Reuse its placement, not its cancelling + # transform, and pass numeric positions to this axis. + if isapprox(lower, upper; rtol = sqrt(eps(Float64)), atol = 0) + unique([lower, upper]) + else + locator = signed_linear ? Makie.LinearTicks(count) : + Makie.PseudologTicks(count) + reference = current_scale.forward.reference + normalized = (lower / reference, upper / reference) + if all(isfinite, normalized) + first(Makie.get_ticks(locator, Makie.pseudolog10, + Makie.automatic, normalized...)) .* reference + else + # Physical decade positions remain representable + # even when their ratio to the reference overflows. + values = Float64[] + lower <= 0 <= upper && push!(values, 0) + lower < 0 && append!(values, + -_decade_ticks(max(-upper, reference), -lower, count)) + upper > 0 && append!(values, + _decade_ticks(max(lower, reference), upper, count)) + sort!(values) + end + end + else + Makie.automatic + end + if !isequal(selected, ticks[]) + installed_ticks[] = selected + ticks[] = selected + end + selected === Makie.automatic && break + if logarithmic + # Fit labels at their transformed physical positions, + # including the central interval of signed-log views. + positions = Float64[] + extents = Float64[] + for (value, text) in zip(selected, lineaxis.ticklabels[]) + current_scale === Base.log10 && value <= 0 && continue + probe.text[] = text + push!(positions, pixels*(current_scale(value)-current_scale(lower))/(current_scale(upper)-current_scale(lower))) + push!(extents, Makie.boundingbox(probe, :data).widths[index]) + end + if length(positions) == length(selected) + retained = Int[] + for i in eachindex(positions) + if isempty(retained) || + positions[i]-positions[last(retained)] >= + (extents[i]+extents[last(retained)])/2+labelsize[]/2 + push!(retained, i) + end + end + fitted_ticks = selected[retained] + if !isequal(ticks[], fitted_ticks) + installed_ticks[] = fitted_ticks + ticks[] = fitted_ticks + end + end + break + end + extent = 0.0 + for text in lineaxis.ticklabels[] + probe.text[] = text + extent = max(extent, Makie.boundingbox(probe, :data).widths[index]) + end + fitted = max(2, floor(Int, pixels / + max(spacing, extent + labelsize[]))) + fitted >= count && break + count = fitted + end + end + previous_inputs[]=inputs + finally + updating[] = false + end + return nothing + end + # Run before native tick conversion: a newly assigned labelled tuple + # cannot be consumed with the previously installed numeric formatter. + onany((_...) -> update!(), axis.scene, ticks, tickformat; priority = 1) + # Range/transform updates must instead follow native LineAxis propagation. + # Changing its formatter while it still has the old limits is unsafe. + onany((_...) -> update!(), axis.scene, axis.finallimits, scale, label, + axis.scene.viewport, labelsize, labelfont, rotation, conversion; + priority = -3, update = true) + end + return axis +end + +# Bind native axis-limit callbacks once and return this axis's reset action. +function _reset!(axis, series = ()) + # Own numeric ticks before the first data fit, including its synchronous + # native callbacks. Every recipe and caller-owned plotwindow uses this bind. + _axis_format!(axis) + fitting = Ref(false) + corrections = Any[nothing, nothing] + function reset!(; xauto::Bool = true, yauto::Bool = true, preserve_view::Bool = false) + fitting[] && return axis + view = preserve_view ? axis.targetlimits[] : nothing + fitting[] = true + try + reset_limits!(axis; xauto, yauto) + requested = axis.limits[] + requested = length(requested) == 4 ? (requested[1:2], requested[3:4]) : + requested + for (index, dim) in enumerate((:x, :y)) + (index == 1 ? xauto : yauto) || continue + corrections[index] = nothing + explicit = requested[index] === nothing ? (nothing, nothing) : + requested[index] + all(value -> value !== nothing, explicit) && continue + rendered = _visible_values(axis, dim) + interval_values = _visible_values(axis, dim, series) + isempty(interval_values) && continue + values = isempty(series) ? interval_values : + _visible_values(series, dim) + isempty(values) && continue + constant = isapprox(extrema(values)...; rtol = sqrt(eps(Float64)), atol = 0) + constant || interval_values != rendered || continue + if constant && !isempty(series) + expected = _visible_values(series, dim; include_uncertainty = true) + # Native extra plots or independently hidden error bars own + # their actual extents, not the original observation array. + if !all(isapprox.(interval_values, collect(extrema(expected)))) + isapprox(extrema(interval_values)...; rtol = sqrt(eps(Float64)), atol = 0) || + continue + values = interval_values + end + end + any( + plot -> haskey(plot, :model) && + any(j -> plot.model[][index, j] != (index == j), 1:4), + axis.scene.plots) && continue + scale = getproperty(axis, Symbol(dim, :scale))[] + limits = if constant + _constant_limits(values, interval_values, scale === Base.log10) + else + lower, upper = scale.(extrema(interval_values)) + low_margin, high_margin = getproperty(axis, Symbol(dim, :autolimitmargin))[] + span = upper - lower + Makie.inverse_transform(scale).((lower - low_margin * span, + upper + high_margin * span)) + end + lower = something(explicit[1], limits[1]) + upper = something(explicit[2], limits[2]) + origin, widths = collect(axis.targetlimits[].origin), + collect(axis.targetlimits[].widths) + original = (origin[index], widths[index]) + origin[index], widths[index] = lower, upper - lower + corrections[index] = (; requested = requested[index], scale, original, + fitted = (origin[index], widths[index])) + axis.targetlimits[] = Makie.Rect2d(origin..., widths...) + end + finally + try + view === nothing || (axis.targetlimits[] = view) + finally + fitting[] = false + end + end + return axis + end + # Makie also refits before displaying a Figure. Preserve our degenerate-data + # padding without storing automatic limits as user requests. Only the exact + # native auto-fit for the same scale and request is corrected. Zooms are untouched. + # This callback uses two cached bounds without scanning data on view updates. + on(axis.scene, axis.targetlimits; priority = 1) do view + requested = axis.limits[] + requested = length(requested) == 4 ? (requested[1:2], requested[3:4]) : requested + origin, widths = collect(view.origin), collect(view.widths) + for (index, dim) in enumerate((:x, :y)) + explicit = requested[index] === nothing ? (nothing, nothing) : requested[index] + # Native nonlinear auto-fitting leaves an all-zero series at (0,0). + # Repair that automatic singular view before the camera's reciprocal + # scaling runs, including during the transform-triggered first fit. + # This uses only the view bounds, never a new data scan on zoom. + if iszero(widths[index]) && any(isnothing, explicit) && + getproperty(axis, Symbol(:dim, index, :_conversion))[] === nothing + bounds = _constant_limits((origin[index],), (origin[index],), + getproperty(axis, Symbol(dim, :scale))[] === Base.log10) + lower, upper = something(explicit[1], bounds[1]), + something(explicit[2], bounds[2]) + origin[index], widths[index] = lower, upper-lower + end + fitting[] && continue + correction = corrections[index] + correction === nothing && continue + if isequal(requested[index], correction.requested) && + getproperty(axis, Symbol(dim, :scale))[] === correction.scale && + (origin[index], widths[index]) == correction.original + origin[index], widths[index] = correction.fitted + end + end + fitted = Makie.Rect2d(origin..., widths...) + fitted == view || (axis.targetlimits[] = fitted) + return nothing + end + on(_ -> reset!(), axis.scene, axis.limits; priority = -3) + if any( + item -> get(item, :sampled_intervals, false) && + any(plot -> plot isa Makie.Errorbars, item.plots), + series) + # A resize changes the drawn interval subset. Refresh the cached native + # auto-fit after glyph and layout updates, without replacing the user's view. + # A subsequent native display fit then still restores complete bounds. + on(_ -> reset!(; preserve_view = true), axis.scene, axis.scene.viewport; priority = -4) + end + reset!() + return reset! +end + +function _axis!( + position, + xobservation, + yobservation; + title, + xscale, + yscale, + xlabel = nothing, + ylabel = nothing, + attributes = (;), + native_attributes = (;) +) + xaxis_label = xlabel === nothing ? + Units.label(xobservation.quantity, xobservation.unit) : + xlabel + yaxis_label = ylabel === nothing ? + Units.label(yobservation.quantity, yobservation.unit) : + ylabel + options = merge( + (; + title, + tellwidth = false, + tellheight = false, + xscale, + yscale, + xlabel = xaxis_label, + ylabel = yaxis_label + ), + attributes, native_attributes) + scales = (x = options.xscale, y = options.yscale) + # Render with safe axes, then validate complete native extents in the common + # finish before applying requested transforms to any axis on the page. + axis = Axis(position; merge(options, (xscale = identity, yscale = identity))...) + return axis, scales +end + +function _panel_titles(panel_titles, expected::Int; defaults = nothing) + panel_titles === nothing && return nothing + panel_titles isa Function && return Tuple(panel_titles(i) for i in 1:expected) + panel_titles isa AbstractDict && + return Tuple(get(panel_titles, i, defaults===nothing ? nothing : defaults[i]) + for i in 1:expected) + panel_titles isa Tuple || panel_titles isa AbstractVector || + throw(ArgumentError( + "panel_titles must be a tuple, vector, dictionary, function, or nothing", + )) + length(panel_titles) == expected || throw(DimensionMismatch( + "panel_titles must contain one entry per logical plot panel", + )) + return Tuple(panel_titles) +end + +function _panel!(shell, position::Tuple{Int, Int}) + layout = GridLayout( + 3, + 3; + width = Relative(1), + height = Relative(1), + tellwidth = false, + tellheight = false + ) + # Empty docks are layout-neutral. A legend's own padding supplies spacing + # only after the caller activates one of these tracks. + layout.default_rowgap = Fixed(0) + layout.default_colgap = Fixed(0) + rowgap!(layout, 0) + colgap!(layout, 0) + rowsize!(layout, 1, Fixed(0)) + rowsize!(layout, 2, Auto(false, 1)) + rowsize!(layout, 3, Fixed(0)) + colsize!(layout, 1, Fixed(0)) + colsize!(layout, 2, Auto(false, 1)) + colsize!(layout, 3, Fixed(0)) + shell.canvas[position...] = layout + return (; logical_position = position, layout, content = layout[2, 2]) +end + +function _legend_slot(body, position) + position === :right && return body[2, 3], :vertical + position === :left && return body[2, 1], :vertical + position === :top && return body[1, 2], :horizontal + position === :bottom && return body[3, 2], :horizontal + if position isa Tuple && length(position) == 2 && + all(value -> value isa Integer && !(value isa Bool) && value > 0, position) + position == (2, 2) && throw(ArgumentError( + "grid position (2, 2) is reserved for the plot canvas", + )) + return body[Int(position[1]), Int(position[2])], :vertical + end + throw(ArgumentError( + "legend_position must be :inside, :left, :right, :top, :bottom, a positive grid index tuple, or nothing", + )) +end + +function _dock_indices(position) + position === :right && return (2, 3) + position === :left && return (2, 1) + position === :top && return (1, 2) + position === :bottom && return (3, 2) + position isa Tuple && return Int.(position) + return nothing +end + +function _remove_legend!(legend) + legend === nothing && return nothing + content = GridLayoutBase.gridcontent(legend) + layout = content === nothing ? nothing : content.parent + delete!(legend) + if layout !== nothing && isempty(layout.content) + layout_content = GridLayoutBase.gridcontent(layout) + layout_content === nothing || + GridLayoutBase.remove_from_gridlayout!(layout_content) + end + return nothing +end + +function _legend_sources!(legend, entries, groups, dependents) + owners = IdDict{Any, Any}(dependents) + for handles in values(groups), handle in handles + + get!(owners, handle, handle) + end + # Preserve Makie's glyphs, but target the registered owning plots. Composite + # glyphs may refer to derived child attributes that are not writable inputs. + # Deduplicate across the entire entry: several glyphs still mean one action. + for entry in entries + seen = Base.IdSet{Any}() + for element in entry.elements + # Cairo's LineSegments renderer adds joinstyle=nothing to the + # source graph. Native legend extraction then mistakes that cache + # for a line style on recreation. Keep the fallback in the glyph, + # without modifying the source graph or overriding a real style. + if element isa LineElement && + to_value(get(element.attributes, :joinstyle, nothing)) === nothing + element.attributes[:joinstyle] = legend.joinstyle + end + # Makie's LegendElement interface requires this mutable + # vector to identify the plots represented by a glyph. + targets = element.plots + resolved = Makie.Plot[] + for plot in targets + owner = plot + while owner isa Makie.Plot && !haskey(owners, owner) + owner = owner.parent + end + owner = get(owners, owner, plot) + while haskey(owners, owner) && owners[owner] !== owner + owner = owners[owner] + end + if owner ∉ seen + push!(seen, owner) + push!(resolved, owner) + end + end + empty!(targets) + append!(targets, resolved) + end + end + # Targets are assigned before native listeners are constructed. Initialize + # shades after publication, including entries restored after truncation. + visibilities = map(collect(keys(owners))) do plot + # on returns an ObserverFunction with a documented `observable` field. + # Release the temporary subscription. Native legend listeners own the + # updates. notify(plot.visible) is a no-op for Makie Computed inputs. + subscription = on(identity, plot.visible) + observable = subscription.observable + off(subscription) + observable + end + on(legend.blockscene, legend.entrygroups; priority = -1) do _ + foreach(notify, visibilities) + end + return legend +end + +function _legend_entries!(legend, title, entries) + # Native entry creation changes many grid cells. Publish the completed set + # with one layout update, restoring suspension even if a listener fails. + blocked = legend.grid.block_updates + try + with_updates_suspended(legend.grid) do + legend.entrygroups[] = [(title, entries)] + end + finally + legend.grid.block_updates = blocked + end + return legend +end + +function _set_legend_capacity!(legend, entries, ellipsis, capacity, state) + total = length(entries) + 0 <= capacity <= total || throw(BoundsError(entries, capacity)) + capacity == state[] && return legend + displayed = copy(entries[1:capacity]) + capacity < total && push!(displayed, ellipsis) + _legend_entries!(legend, first(only(legend.entrygroups[])), displayed) + state[] = capacity + return legend +end + +function _wrap_legend_label(label, width, measure) + lines = String[] + for paragraph in split(label, '\n'; keepempty = true) + current = "" + for word in split(paragraph) + candidate = isempty(current) ? String(word) : "$current $word" + if !isempty(current) && measure(candidate) > width + push!(lines, current) + current = "" + end + # Very long identifiers also need a lossless fallback. No formula + # field is elided merely because its token has no spaces. + for character in (isempty(current) ? String(word) : " $word") + candidate = current * character + if !isempty(current) && measure(candidate) > width + push!(lines, current) + current = "" + end + current *= character + end + end + push!(lines, current) + end + return join(lines, '\n') +end + +function _legend_fraction(value) + value isa Real && !(value isa Bool) && isfinite(value) && 0available ? + _wrap_legend_label(original, available, + text -> measure(text, entry.labelfont[], entry.labelsize[])[1]) : + original + entry.label[]==label || (entry.label[]=label) + w, h = measure(label, entry.labelfont[], entry.labelsize[]) + push!(sizes, (Float64(patch[1]), Float64(patch[2]), w, h)) + end + orientation = managed[] ? :vertical : legend.orientation[] + Base.@noinline function extent(count, banks) + n = count+(count size[1]+size[3]+legend.patchlabelgap[], sizes) + step = narrowest+legend.colgap[] + max_banks = managed[] && position[] in (:top, :bottom) ? + (step>0 ? + clamp(floor(Int, (text_width+legend.colgap[])/step), 1, total+1) : + total+1) : + managed[] ? 1 : banks + for candidate in (managed[] ? (max_banks:-1:1) : (banks:banks)) + if all(extent(total, candidate) .<= limits) + best=total + banks=candidate + break + end + lower, upper = 0, total-1 + while lower<=upper + middle = (lower+upper)÷2 + if all(extent(middle, candidate) .<= limits) + if middle>best + best=middle + banks=candidate + end + lower=middle+1 + else + upper=middle-1 + end + end + best==total && break + end + best=max(0, best) + metrics=(sizes, title_size, padding, legend.rowgap[], + legend.colgap[], legend.patchlabelgap[]) + blocked = legend.grid.block_updates + try + with_updates_suspended(legend.grid) do + legend.orientation[]===orientation || (legend.orientation[]=orientation) + if legend.nbanks[]!=banks + legend.nbanks[]=banks + elseif metrics!=previous_metrics[] + notify(legend.nbanks) + end + _set_legend_capacity!(legend, entries, ellipsis, best, capacity) + end + finally + legend.grid.block_updates=blocked + end + previous_metrics[]=metrics + for (d, attribute) in enumerate((legend.width, legend.height)) + requested = requested_size[d] + span = requested isa Real ? Float64(requested) : + requested isa Relative ? + requested.x*legend.layoutobservables.suggestedbbox[].widths[d] : + nothing + if span!==nothing + bounded = min(span, limits[d]) + attribute[]==bounded || (attribute[]=bounded) + end + end + # Native font and layout measurements settle the final capacity. This + # also accounts for native attributes outside the text measurement. + while capacity[]>0 && + any(d -> something(legend.layoutobservables.autosize[][d], 0.0)>limits[d], 1:2) + _set_legend_capacity!( + legend, entries, ellipsis, capacity[]-1, capacity) + end + fits = all(d -> something(legend.layoutobservables.autosize[][d], 0.0)<=limits[d], 1:2) + visible = shown[] && fits + legend.blockscene.visible[]==visible || (legend.blockscene.visible[]=visible) + finally + fitting_geometry[]=previous_geometry + fitting[]=false + end + return nothing + end + on(legend.blockscene, bounds) do _ + fit!() + end + if bbox isa Observable + on(_ -> fit!(), legend.blockscene, bbox) + end + on(legend.blockscene, legend.entrygroups) do _ + fitting[] || fit!(force = true) + end + # Native labels must finish measuring before fitting. Shared frame fitting + # follows at priority -100. + onany( + (_...) -> fit!(force = true), legend.blockscene, legend.labelsize, legend.labelfont, + legend.titlesize, legend.titlefont, legend.titleposition, legend.titlevisible, + legend.padding, legend.margin, legend.colgap, legend.rowgap, legend.titlegap, + legend.patchlabelgap, legend.patchsize; priority = -50) + for (index, entry) in enumerate(entries) + on(legend.blockscene, entry.label) do label + fitting[] && return + originals[index]=label + fit!(force = true) + end + end + for setting in (legend.nbanks, legend.orientation) + on(legend.blockscene, setting; priority = 1) do _ + fitting[] && return + managed[]=false + fit!(force = true) + end + end + for (d, attribute) in enumerate((legend.width, legend.height)) + on(legend.blockscene, attribute) do value + fitting[] && return + requested_size[d]=value + fit!(force = true) + end + end + on(legend.blockscene, legend.blockscene.visible) do visible + if !fitting[] && position[]!==nothing && visible!=previous_visibility[] + shown[]=visible + end + previous_visibility[]=visible + end + fit!() + return fit! +end + +function _axes_viewport(axes, fallback; panels = (), cells = ()) + isempty(axes) && return fallback + viewports=Tuple(axis.scene.viewport for axis in axes) + if isempty(cells) + return lift(viewports...) do bounds... + left=minimum(bound.origin[1] for bound in bounds) + bottom=minimum(bound.origin[2] for bound in bounds) + right=maximum(bound.origin[1]+bound.widths[1] for bound in bounds) + top=maximum(bound.origin[2]+bound.widths[2] for bound in bounds) + Rect2f((left, bottom), (right-left, top-bottom)) + end + end + panel_boxes=Tuple(panel.layout.layoutobservables.computedbbox for panel in panels) + cell_boxes=Tuple(cell.layout.layoutobservables.computedbbox for cell in cells) + return lift(viewports..., panel_boxes..., cell_boxes...) do values... + n=length(axes) + frames=values[1:n] + selected=values[(n + 1):2n] + domain=values[(2n + 1):end] + lower=ntuple( + d -> minimum(frame.origin[d]-panel.origin[d] + for (frame, panel) in zip(frames, selected)), + 2) + upper=ntuple( + d -> minimum(panel.origin[d]+panel.widths[d]-frame.origin[d]-frame.widths[d] + for (frame, panel) in zip(frames, selected)), + 2) + origin=ntuple(d -> minimum(box.origin[d] for box in domain)+lower[d], 2) + stop=ntuple(d -> maximum(box.origin[d]+box.widths[d] for box in domain)-upper[d], 2) + Rect2f(origin, stop .- origin) + end +end + +function _relabel_legend!(labels, groups, order, requested) + requested === nothing && return labels + displayed = Any[group + for group in order if haskey(groups, group) && haskey(labels, group)] + if requested isa AbstractDict + for group in displayed + current = labels[group] + replacement = if haskey(requested, group) + requested[group] + elseif haskey(requested, current) + requested[current] + else + continue + end + labels[group] = replacement + end + return labels + end + requested isa Tuple || requested isa AbstractVector || + throw(ArgumentError( + "legend_labels must be a tuple, vector, dictionary, or nothing", + )) + length(requested) == length(displayed) || throw(DimensionMismatch( + "legend_labels must contain one entry for each displayed legend group", + )) + for (group, replacement) in zip(displayed, requested) + labels[group] = replacement + end + return labels +end + +function _legend!( + figure, + groups, + order, + labels; + dependent_plots, + position, + attributes, + max_fraction, fitting_geometry, + title = nothing, + inside_bbox = nothing, + target = nothing, + target_orientation = nothing +) + position[] === nothing && return nothing + attributes isa NamedTuple || + throw(ArgumentError("legend_attributes must be a NamedTuple")) + entries = Any[] + displayed = Any[] + for group in order + haskey(labels, group) || continue + push!(entries, groups[group]) + push!(displayed, labels[group]) + end + isempty(entries) && return nothing + if position[] === :inside + inside_bbox === nothing && throw(ArgumentError( + "inside legends require a figure or panel plot-area bounding box", + )) + options = merge( + (; + bbox = inside_bbox[], + orientation = :vertical, + halign = :right, valign = :top, + margin = (10, 10, 10, 10), + tellwidth = false, + tellheight = false + ), + attributes + ) + target = figure + else + target===nothing && + throw(ArgumentError("native legends require an assigned guide slot")) + options = merge( + (; + orientation = target_orientation, + halign = target_orientation === :vertical ? :left : :center, + valign = target_orientation === :vertical ? :top : :center, + tellwidth = position[] in (:left, :right) || position[] isa Tuple, + tellheight = position[] in (:top, :bottom) || position[] isa Tuple + ), + attributes) + end + legend = Legend(target, Any[], String[], title; options...) + native_attributes = Makie.Attributes([key=>getproperty(legend, key) + for key in propertynames(typeof(legend))]) + native_entries = [Makie.LegendEntry(label, handles, native_attributes) + for (handles, label) in zip(entries, displayed)] + ellipsis = Makie.LegendEntry("(...)", LineElement(color = :transparent), native_attributes) + _legend_sources!(legend, [native_entries; ellipsis], groups, dependent_plots) + fit = _fit_legend!( + legend, native_entries, ellipsis, inside_bbox, position, max_fraction; + automatic = !haskey(attributes, :orientation) && !haskey(attributes, :nbanks), fitting_geometry, + bbox = get(attributes, :bbox, nothing)) + return legend, fit +end + +function _plot_belongs_to_axis(plot, axis) + return plot.parent === axis.scene +end + +function _panel_legend_data( + panels, + axes, + groups, + order, + labels; + panel_labels = nothing, + panel_titles = nothing +) + length(panels) == length(axes) || throw(DimensionMismatch( + "plot panels must align with their axes", + )) + panel_labels === nothing || length(panel_labels) == length(axes) || + throw( + DimensionMismatch("panel legend labels must align with their axes"), + ) + panel_titles === nothing || length(panel_titles) == length(axes) || + throw( + DimensionMismatch("panel legend titles must align with their axes"), + ) + result = Dict{Any, Any}() + for (index, (panel, axis)) in enumerate(zip(panels, axes)) + scoped = Dict{Any, Vector{Any}}() + scoped_order = Any[] + scoped_labels = Dict(panel_labels === nothing ? labels : panel_labels[index]) + for key in order + plots = Any[plot + for plot in groups[key] if _plot_belongs_to_axis(plot, axis)] + isempty(plots) && continue + scoped[key] = plots + push!(scoped_order, key) + end + title = Ref{Any}(panel_titles === nothing ? nothing : panel_titles[index]) + result[panel.logical_position] = (; + panel, + axis, + groups = scoped, + order = scoped_order, + labels = scoped_labels, + title + ) + end + return result +end + +function _panel_legend_pairs(value) + value === nothing && return Pair[] + value === () && return Pair[] + value isa Pair && return Pair[value] + value isa AbstractDict && return collect(pairs(value)) + value isa Tuple && all(item -> item isa Pair, value) && return collect(value) + throw(ArgumentError( + "panel_legends must be a pair, a tuple of pairs, a dictionary, or nothing", + )) +end + +function _panel_identity(value) + value isa Integer && !(value isa Bool) && value>0 && return Int(value) + value isa Tuple && length(value) == 2 && + all(index -> index isa Integer && !(index isa Bool) && index > 0, value) || + throw(ArgumentError("panel identities must be positive indices or original `(row, column)` tuples")) + return (Int(value[1]), Int(value[2])) +end + +function _without_legend_controls(options::NamedTuple) + names = Tuple(filter( + name -> name ∉ (:position, :max_fraction, :title, :legend_labels), + keys(options) + )) + return NamedTuple{names}(Tuple(getproperty(options, name) for name in names)) +end + +function _legend_configuration(value; default_position, default_title = nothing) + value isa Symbol && return (; + position = value, + max_fraction = 0.5, + title = default_title, + legend_labels = nothing, + attributes = (;) + ) + value isa NamedTuple || throw(ArgumentError( + "a legend configuration must be a dock symbol or NamedTuple", + )) + position = get(value, :position, default_position) + max_fraction = _legend_fraction(get(value, :max_fraction, 0.5)) + title = get(value, :title, default_title) + haskey(value, :anchor) && + throw(ArgumentError("anchor was removed; use native halign and valign")) + legend_labels = get(value, :legend_labels, nothing) + attributes = _without_legend_controls(value) + return (; position, max_fraction, title, legend_labels, attributes) +end + +function _colorbar!(position, scale; attributes) + scale isa NamedTuple || throw(ArgumentError( + "a material color scale must be a NamedTuple", + )) + all(name -> haskey(scale, name), (:colormap, :limits, :ticks, :label)) || + throw(ArgumentError( + "a material color scale requires colormap, limits, ticks, and label", + )) + options = merge( + (; + colormap = scale.colormap, + limits = scale.limits, + ticks = scale.ticks, + label = scale.label + ), + attributes) + colorbar = Colorbar(position; options...) + if !haskey(attributes, :alignmode) + # Makie reserves space perpendicular to a colorbar, but not for labels + # extending beyond its endpoints. Include the rendered text extents in + # the native layout, independently of the bar's position or length. + labels = colorbar.axis.elements[:ticklabels] + caption = colorbar.axis.elements[:labeltext] + managed=Ref{Any}(colorbar.alignmode[]) + onany(colorbar.blockscene, + labels.text, labels.fontsize, labels.font, labels.rotation, labels.align, labels.offset, + caption.text, caption.fontsize, caption.font, + caption.rotation, caption.align, caption.offset, + colorbar.vertical, colorbar.ticklabelsvisible, colorbar.labelvisible, + colorbar.layoutobservables.computedbbox, colorbar.spinewidth, + colorbar.layoutobservables.protrusions; update = true) do _labels_text, + _labels_fontsize, _labels_font, _labels_rotation, _labels_align, + _labels_offset, _caption_text, _caption_fontsize, _caption_font, + _caption_rotation, _caption_align, _caption_offset, vertical, visible, + labelvisible, bounds, stroke, protrusions + colorbar.alignmode[]==managed[] || return nothing + boxes=Makie.fast_string_boundingboxes(labels) + captions=Makie.fast_string_boundingboxes(caption) + dimension = vertical ? 2 : 1 + finite_boxes = filter( + box -> isfinite(box.origin[dimension]) && + isfinite(box.widths[dimension]), + boxes) + before = visible ? + ceil(maximum( + box -> -box.origin[dimension], finite_boxes; init = 0.0 + )) : 0.0 + after = visible ? + ceil(maximum( + box -> box.origin[dimension] + box.widths[dimension], + finite_boxes; init = 0.0 + )) : 0.0 + if labelvisible + # The property caption is centered along the bar. Its overhang + # belongs to this item's footprint, just like endpoint ticks. + half_length=bounds.widths[dimension]/2 + for box in captions + isfinite(box.origin[dimension]) && isfinite(box.widths[dimension]) || + continue + before=max(before, ceil(-box.origin[dimension]-half_length)) + after=max(after, ceil(box.origin[dimension]+box.widths[dimension]-half_length)) + end + end + border=max(0.0, Float64(stroke)/2) + before=max(before, border) + after=max(after, border) + # Enclose the visible border as well as native perpendicular text. + # These measurements describe protrusions. Spacing between siblings + # is calculated separately. + # Native LineAxis places text beyond a full spine width, while + # its reported text protrusion omits that offset. + side(name) = GridLayoutBase.Protrusion(max( + getproperty(protrusions, name) + + (getproperty(protrusions, name)>0 ? max(0.0, stroke) : 0.0), + border)) + managed[] = vertical ? + Mixed(bottom = before, top = after, left = side(:left), right = side(:right)) : + Mixed(left = before, right = after, bottom = side(:bottom), top = side(:top)) + colorbar.alignmode[]==managed[] || (colorbar.alignmode[]=managed[]) + end + end + return colorbar +end + +function LineCableModels.materialscale!(position, scheme; kwargs...) + return _colorbar!(position, scheme; attributes = (; kwargs...)) +end + +function _colorbars!(slot, scales; attributes, orientation, main = false, scene) + attributes isa NamedTuple || + throw(ArgumentError("colorbar_attributes must be a NamedTuple")) + vertical=get(attributes, :vertical, orientation===:vertical) + length=main ? Auto() : _COLORBAR_DOCK_LENGTH + defaults=vertical ? (; vertical, height = length) : (; vertical, width = length) + grid=GridLayout(; alignmode = Outside()) + slot[]=grid + previous_scenes=copy(scene.children) + items=try + map(enumerate(scales)) do (index, scale) + item=GridLayout(; alignmode = Outside()) + grid[index, 1]=item + bar=_colorbar!(item[1, 1], scale; attributes = merge(defaults, attributes)) + companion=Label(item[1, 2], bar.label; halign = :right, valign = :center, + fontsize = bar.labelsize, font = bar.labelfont, color = bar.labelcolor) + (; layout = item, bar, companion, visible = Ref(bar.blockscene.visible[]), + labelvisible = Ref(bar.labelvisible[]), + managed = (vertical = Ref(!haskey(attributes, :vertical)), + width = Ref(!haskey(attributes, :width)), height = Ref(!haskey(attributes, :height)))) + end + catch + _delete_subtree!(grid) + # A native constructor can fail before registering its block in the + # grid. Release that incomplete scene and its owned subscriptions too. + for child in copy(scene.children) + any(previous -> previous===child, previous_scenes) || empty!(child) + end + rethrow() + end + return (; colorbars = Tuple(item.bar for item in items), layout = grid, items) +end + +function _bind_visibility!(figure, axes, resets, groups, status) + for plots in values(groups), plot in plots + + previous = Ref(plot.visible[]) + on(figure.scene, plot.visible) do visible + # A legend rebuild can notify without changing visibility. Keep + # the current zoom and avoid fitting identical series repeatedly. + visible == previous[] && return nothing + previous[] = visible + foreach(callback -> callback(), resets) + status[] = "Axis limits fitted to visible series" + return nothing + end + end + return groups +end + +function _figure_title!(shell, title, attributes) + title === nothing && return nothing + attributes isa NamedTuple || throw(ArgumentError( + "title_attributes must be a NamedTuple", + )) + options = merge( + (; tellwidth = true, halign = :center, font = :bold, fontsize = 18), + attributes + ) + return Label(shell.root[0, 1], title; options...) +end + +function _finish_plot!( + shell, + axes, + resets, + groups, + order, + group_labels; + requested_scales = nothing, + axis_series = nothing, + dependent_plots = Pair{Makie.Plot, Makie.Plot}[], + title, + figure_title = nothing, + title_attributes = (;), + series_attributes = nothing, + series_defaults = nothing, + marker_coordinates = nothing, + legend_position, + legend_attributes, + legend_cap = 0.5, + legend_title = nothing, + panels = (), + frame_cells = (), + panel_legends = (), + panel_group_labels = nothing, + panel_legend_titles = nothing, + color_scales = (), + colorbar_position = nothing, + colorbar_attributes = (;), + controls, + display_plot, + export_name, + export_theme, + open_export +) + _colorbar_group_attributes(shell.colorbar_group_attributes, length(color_scales)) + validate(merge(colorbar_attributes, shell.colorbar_attributes), Colorbar) + isempty(axes) && !isempty(shell.axis_attributes) && + throw(ArgumentError( + "native Axis attributes require a figure containing an Axis")) + append!(dependent_plots, + _series_styles!(groups, order, series_attributes; + defaults = series_defaults, shared = shell.series_attributes, marker_coordinates)) + entries = [(; axis, reset, series = axis_series === nothing ? () : axis_series[i]) + for (i, (axis, reset)) in enumerate(zip(axes, resets))] + setters = map(enumerate((:x, :y))) do (index, dim) + if requested_scales !== nothing + _set_axis!( + [merge(entry, (scale = getproperty(scales, dim),)) + for (entry, scales) in zip(entries, requested_scales)], + dim) + end + filter(entries) do entry + _numeric_axis(entry.axis, dim) && + (getproperty(entry.axis, Symbol(dim, :scale))[] in (identity, log10) || + getproperty(entry.axis, Symbol(dim, :scale))[] isa + Makie.ReversibleScale{_SignedLog10}) && + !isempty(_visible_values(entry.axis, dim, entry.series)) + end + end + xsetters, ysetters = setters + for (dependent, owner) in dependent_plots + dependent.visible[] = owner.visible[] + previous = Ref(owner.visible[]) + on(shell.figure.scene, owner.visible) do visible + # Legend relayout re-emits the current state to initialize shading. + # It is not a series action and must preserve native component edits. + visible == previous[] && return nothing + previous[] = visible + dependent.visible[] = visible + end + end + title_block = _figure_title!(shell, figure_title, title_attributes) + inside_bbox = _axes_viewport( + axes, + shell.canvas.layoutobservables.computedbbox; + panels, cells = frame_cells + ) + panel_data = if isempty(panels) + Dict{Any, Any}(i => begin + selected=Dict{Any, Vector{Any}}(key=>filter( + plot -> plot in + axis.scene.plots, + plots) + for (key, plots) in groups + if any(plot -> plot in axis.scene.plots, plots)) + (; axis, panel = (logical_position = i, layout = nothing), + groups = selected, + order = filter(key -> haskey(selected, key), order), labels = copy(group_labels), title = Ref{Any}(nothing)) + end for (i, axis) in enumerate(axes)) + else + _panel_legend_data(panels, axes, groups, order, group_labels; + panel_labels = panel_group_labels, panel_titles = panel_legend_titles) + end + _bind_visibility!(shell.figure, axes, resets, groups, shell.status) + built = LineCableModels.UIPlot( + shell.figure, + Tuple(axes); + title = title_block, + status = shell.status, + plot_state = (; + shell, + axis_bindings = entries, controls_enabled = controls, + widget_bindings = Dict{Symbol, Any}(), widget_order = Symbol[], + groups, + dependent_plots, + order, + labels = group_labels, + panel_data, + inside_bbox, + guides = Dict{Any, Any}(), guide_order = Any[], guide_docks = Any[], + composing_guides = Ref(false), guide_gap = shell.guide_gap, guide_spacing = Ref(shell.guide_spacing), + fitting_geometry = Ref(false), frame_padding = IdDict{Any, Any}(), presentation_ready = Ref(false), + panel_page = isempty(panels) ? nothing : + (index = (1, 1), dimensions = size(shell.canvas), + coordinates = Tuple(panel.logical_position for panel in panels)), + color_scales, + colorbar_group_attributes = Ref{Any}(shell.colorbar_group_attributes) + ), + export_name, + export_theme, + open_export + ) + figure_guide=_guide_state(:legend, nothing, legend_position, legend_attributes; + max_fraction = legend_cap, title = legend_title) + built.plot_state.guides[(:legend, nothing)]=figure_guide + push!(built.plot_state.guide_order, (:legend, nothing)) + for (identity, value) in _panel_legend_pairs(panel_legends) + identity=_panel_identity(identity) + haskey(panel_data, identity) || + throw(ArgumentError("panel $(repr(identity)) is absent from this figure")) + value===nothing || value===false || + begin + config=_legend_configuration(value; default_position = :right, default_title = nothing) + _relabel_legend!( + panel_data[identity].labels, panel_data[identity].groups, + panel_data[identity].order, config.legend_labels) + key=(:legend, identity) + built.plot_state.guides[key]=_guide_state( + :legend, identity, config.position, config.attributes; + max_fraction = config.max_fraction, title = config.title) + push!(built.plot_state.guide_order, key) + end + end + resolved_position=shell.colorbar_position===_omitted ? colorbar_position : + shell.colorbar_position + bars=_guide_state(:colorbars, nothing, resolved_position, + merge(colorbar_attributes, shell.colorbar_attributes)) + built.plot_state.guides[(:colorbars, nothing)]=bars + push!(built.plot_state.guide_order, (:colorbars, nothing)) + _compose_guides!(built) + on(shell.figure.scene, shell.figure.scene.viewport) do _ + if !built.plot_state.fitting_geometry[] + _release_frames!(built) + _fit_panel_aspects!(built) + end + _compose_guides!(built) + nothing + end + _controls!(built, xsetters, ysetters) + if controls + for widget in shell.widgets + widget(built) + end + end + _fit_panel_aspects!(built) + _compose_guides!(built) + for axis in axes + _watch_presentation!(built, + axis, + (:title, :titlesize, :titlefont, :subtitle, :subtitlesize, + :xlabelsize, :ylabelsize, :xticklabelsize, :yticklabelsize)) + end + title_block===nothing || + _watch_presentation!(built, title_block, (:text, :fontsize, :font)) + built.plot_state.presentation_ready[]=true + _update_figure_layout!(() -> nothing, built) + display_plot && _display_figure!(shell.figure, title) + return built +end + +function LineCableModels.figuretitle!( + plot::LineCableModels.UIPlot, + title; + kwargs... +) + data = plot.plot_state + data === nothing && throw(ArgumentError( + "this plot does not retain an addon figure shell", + )) + return _update_figure_layout!(plot) do + plot.title === nothing || delete!(plot.title) + plot.title = title === nothing ? nothing : + _figure_title!(data.shell, title, (; kwargs...)) + plot.title===nothing || + _watch_presentation!(plot, plot.title, (:text, :fontsize, :font)) + _compose_guides!(plot) + return plot.title + end +end + +function LineCableModels.paneltitle!( + plot::LineCableModels.UIPlot, + logical_position, + title +) + data = plot.plot_state + data === nothing && throw(ArgumentError( + "this plot does not retain logical plot panels", + )) + logical_position=_panel_identity(logical_position) + haskey(data.panel_data, logical_position) || + throw(ArgumentError("panel $(repr(logical_position)) is absent from this figure")) + return _update_figure_layout!(plot) do + axis = data.panel_data[logical_position].axis + axis.title[] = title === nothing ? "" : title + return axis + end +end diff --git a/ext/LineCableModelsMeasurementsExt.jl b/ext/LineCableModelsMeasurementsExt.jl new file mode 100644 index 000000000..3ba8bc809 --- /dev/null +++ b/ext/LineCableModelsMeasurementsExt.jl @@ -0,0 +1,341 @@ +""" + LineCableModelsMeasurementsExt + +Materialize `UncertainValue` as Measurements values and preserve those values +through numerical kernels, display, and data exchange. +""" +module LineCableModelsMeasurementsExt + +import Measurements +import SpecialFunctions +using LinearAlgebra: svd +#! explicit-imports: off +# Non-exported accessors documented in Measurements usage. +# Measurements.result is its documented internal propagation +# hook, required by the existing complex-Bessel adapter (Measurements appendix). +# Keep the import exception restricted to these upstream bindings. +using Measurements: value as measured_value, uncertainty as measured_uncertainty, + result as measured_result, derivative, uncertainty_components +#! explicit-imports: on + +import LineCableModels +import LineCableModels.ParametricBuilder +import LineCableModels.Engine +import LineCableModels.UQ +import LineCableModels.ImportExport + +import LineCableModels: nominal, uncertainty +import LineCableModels.Engine: has_uncertainty_type +import LineCableModels.ImportExport: + serialize_value, deserialize_extension, + deserialize_value + +# Numeric presentation hooks. +nominal(value::Measurements.Measurement) = measured_value(value) +uncertainty(value::Measurements.Measurement) = measured_uncertainty(value) + +# Refinement must still see an uncertain contribution whose nominal value is +# zero. Physical evaluations retain their correlated derivative information. +# Only the scalar numerical error metric and sampling coordinates are nominal. +function Engine.numerical_magnitude(z::Complex{<:Measurements.Measurement}) + return max(abs(measured_value(z)), + hypot(measured_uncertainty(real(z)), measured_uncertainty(imag(z)))) +end +function Engine.numerical_magnitude(z::Measurements.Measurement) + return max(abs(measured_value(z)), measured_uncertainty(z)) +end + +function LineCableModels.materialize(value::ParametricBuilder.UncertainValue{<:Real}) + Measurements.measurement(value.nominal, value.sigma) +end + +function has_uncertainty_type( + ::Type{Complex{T}}, +) where {T <: Measurements.Measurement} + true +end +has_uncertainty_type(::Type{<:Measurements.Measurement}) = true +function serialize_value(value::Measurements.Measurement) + return Dict( + "__type__" => "Measurement", + "value" => serialize_value(measured_value(value)), + "uncertainty" => serialize_value(measured_uncertainty(value)) + ) +end +function deserialize_extension(::Val{:Measurement}, value) + nominal = deserialize_value(value["value"]) + uncertainty = deserialize_value(value["uncertainty"]) + return Measurements.measurement(nominal, uncertainty) +end + +""" +Encode one UQ result with shared independent-source identities and sparse signed +sensitivities. Source IDs are local to this record and preserve shared +uncertainties across outputs when decoded. +""" +function serialize_value(value::Union{UQ.LinearErrorResult{T}, + UQ.MonteCarloResult{T}}) where { + T <: Union{Engine.LineParameters{<:Complex{<:Measurements.Measurement}}, + Engine.CableConstants{<:Measurements.Measurement}}} + isempty(value) && throw(ArgumentError("cannot encode an empty UQ result")) + initial=first(LineCableModels.observe(first(value), LineCableModels.R)) + source_set=Set(keys(uncertainty_components(initial))) + for core in value + components = core isa Engine.LineParameters ? + (real.(LineCableModels.observe(core, LineCableModels.Z)), + imag.(LineCableModels.observe(core, LineCableModels.Z)), + real.(LineCableModels.observe(core, LineCableModels.Y)), + imag.(LineCableModels.observe(core, LineCableModels.Y)), core.f) : + (core.R, core.L, core.C, core.G, (core.frequency,)) + for component in Iterators.flatten(components) + component isa Measurements.Measurement || continue + union!(source_set, keys(uncertainty_components(component))) + end + end + sources=sort!(collect(source_set); by = last) + indices=Dict(source=>index for (index, source) in enumerate(sources)) + encode = component -> begin + contributions=component isa Measurements.Measurement ? + [(source = indices[source], + sensitivity = derivative(component, source)) + for source in keys(uncertainty_components(component))] : + @NamedTuple{source::Int, sensitivity::typeof(nominal(component))}[] + sort!(contributions; by = entry->entry.source) + (nominal = nominal(component), uncertainty = uncertainty(component), + measured = component isa Measurements.Measurement, contributions) + end + points=map(value) do core + if core isa Engine.CableConstants + return (kind = :cable_constants, cores = core.cores, + R = encode.(core.R), L = encode.(core.L), C = encode.(core.C), G = encode.(core.G), + frequency = encode(core.frequency), + shunt_model = get(LineCableModels.details(core).data, :shunt_model, nothing), + gridpoint_description=(; (key=>LineCableModels.details(core).data[key] for key in + (:inputs,:gridpoint,:selections,:formulation_fields,:uncertainty,:formulations,:timing) if haskey(LineCableModels.details(core).data,key))...)) + end + LineCableModels.domain(core) === LineCableModels.PhaseDomain || + throw(ArgumentError("unsupported scientific UQ result domain")) + matrices=map((LineCableModels.Z, LineCableModels.Y)) do selector + values=LineCableModels.observe(core, selector) + components=map((real, imag)) do component_part + encode.(component_part.(values)) + end + (shape = size(values), real = vec(components[1]), + imaginary = vec(components[2])) + end + (Z = matrices[1], Y = matrices[2], + frequencies = encode.(LineCableModels.frequencies(core)), + basis = LineCableModels.basis(core), domain = :PhaseDomain, + coordinates = get(LineCableModels.details(core).data, :coordinates, nothing), + comparison_unsupported = get(LineCableModels.details(core).data, :comparison_unsupported, (;)), + shunt_model = get(LineCableModels.details(core).data, :shunt_model, nothing), + gridpoint_description=(; (key=>LineCableModels.details(core).data[key] for key in + (:inputs,:gridpoint,:selections,:formulation_fields,:uncertainty,:formulations,:timing) if haskey(LineCableModels.details(core).data,key))...)) + end + return serialize_value( + value, [serialize_value(point, Val(:scientific)) for point in points], + [(nominal = source[1], sigma = source[2]) for source in sources]) +end + +function deserialize_extension(::Val{:MeasurementPoints}, record) + sources=[Measurements.measurement(source.nominal, source.sigma) + for source in deserialize_value(record["sources"])] + restore = component -> begin + get(component, :measured, true) || return component.nominal + value=Measurements.measurement(component.nominal, zero(component.nominal)) + for entry in component.contributions + source=sources[entry.source] + value += entry.sensitivity*(source-nominal(source)) + end + isapprox(uncertainty(value), component.uncertainty; rtol = 1e-12, atol = 0) || + throw(ArgumentError("restored UQ sensitivity record changes propagated uncertainty")) + value + end + return map(record["points"]) do encoded + point=deserialize_value(encoded) + model=get(point, :shunt_model, nothing) + detail=model===nothing ? (;) : + NamedTuple{(:shunt_model,), Tuple{NamedTuple}}((model,)) + detail=merge(detail,get(point,:gridpoint_description,(;))) + if get(point, :kind, nothing)===:cable_constants + return Engine.CableConstants(point.cores, restore.(point.R), restore.(point.L), + restore.(point.C), restore.(point.G), restore(point.frequency), + LineCableModels.ComputationDetails(detail)) + end + matrices=map((point.Z, point.Y)) do matrix + parts=map((matrix.real, matrix.imaginary)) do components + restore.(components) + end + reshape(complex.(parts...), matrix.shape) + end + point.domain === :PhaseDomain || + throw(ArgumentError("unsupported UQ result domain")) + point.coordinates === nothing || + (detail=merge(detail, (coordinates = point.coordinates,))) + detail=merge(detail, (comparison_unsupported = get(point, :comparison_unsupported, (;)),)) + f = record["version"] == 1 ? point.frequencies : restore.(point.frequencies) + Engine.LineParameters(matrices..., f; basis = point.basis, + details = LineCableModels.ComputationDetails(detail)) + end +end + +function deserialize_extension(::Val{:MeasurementLinearErrorResult}, record) + record["version"] == 1 || throw(ArgumentError("unsupported shared-source LEP record")) + payload=deserialize_value(record["payload"]) + points=deserialize_extension(Val(:MeasurementPoints), + Dict("version"=>1, + "points"=>payload.points, "sources"=>payload.sources)) + retained = payload.details + restored_details = if isempty(retained) + LineCableModels.ComputationDetails() + else + records=map(retained.points) do detail + haskey(detail, :shunt_model) && (detail=merge(detail, + NamedTuple{(:shunt_model,), Tuple{NamedTuple}}((detail.shunt_model,)))) + LineCableModels.ComputationDetails(detail) + end + LineCableModels.ComputationDetails(points = records) + end + return UQ.LinearErrorResult(payload.formulation, points, restored_details) +end +# Uncertainty-aware SpecialFunctions methods used by the numerical kernels. +function _lift_complex(function_value, order, value::Complex{<:Measurements.Measurement}) + z = measured_value(value) + result = function_value(order, z) + lower, upper = function_value(order - 1, z), function_value(order + 1, z) + # DLMF 10.6.1 and 10.29.1: exact order recurrences avoid an absolute + # finite-difference step crossing the singularity at small arguments. + derivative = if function_value in (SpecialFunctions.besseli, SpecialFunctions.besselix) + (lower + upper) / 2 + elseif function_value in (SpecialFunctions.besselk, SpecialFunctions.besselkx) + -(lower + upper) / 2 + else + (lower - upper) / 2 + end + dx, dy = derivative, im * derivative + # Scaled I/J/Y are not holomorphic. Differentiate their scaling in Cartesian + # coordinates. `sign(0)=0` retains the symmetric slope at an absolute-value cusp. + if function_value === SpecialFunctions.besselix + dx -= sign(real(z)) * result + elseif function_value in (SpecialFunctions.besseljx, SpecialFunctions.besselyx) + dy -= sign(imag(z)) * result + elseif function_value === SpecialFunctions.besselkx + dx += result + dy = im * dx + elseif function_value === SpecialFunctions.besselhx + dx -= im * result + dy = im * dx + end + return measured_result(result, [real(dx), real(dy), imag(dx), imag(dy)], value) +end + +function SpecialFunctions.besselix(order::Real, value::Complex{<:Measurements.Measurement}) + return _lift_complex(SpecialFunctions.besselix, order, value) +end + +function SpecialFunctions.besselkx(order::Real, value::Complex{<:Measurements.Measurement}) + return _lift_complex(SpecialFunctions.besselkx, order, value) +end + +function SpecialFunctions.besseljx(order::Real, value::Complex{<:Measurements.Measurement}) + return _lift_complex(SpecialFunctions.besseljx, order, value) +end + +function SpecialFunctions.besselyx(order::Real, value::Complex{<:Measurements.Measurement}) + return _lift_complex(SpecialFunctions.besselyx, order, value) +end + +function SpecialFunctions.besselhx(order::Real, value::Complex{<:Measurements.Measurement}) + return _lift_complex(SpecialFunctions.besselhx, order, value) +end + +function SpecialFunctions.besseli(order::Real, value::Complex{<:Measurements.Measurement}) + return _lift_complex(SpecialFunctions.besseli, order, value) +end + +function SpecialFunctions.besselk(order::Real, value::Complex{<:Measurements.Measurement}) + return _lift_complex(SpecialFunctions.besselk, order, value) +end + +function SpecialFunctions.besselj(order::Real, value::Complex{<:Measurements.Measurement}) + return _lift_complex(SpecialFunctions.besselj, order, value) +end + +function SpecialFunctions.bessely(order::Real, value::Complex{<:Measurements.Measurement}) + return _lift_complex(SpecialFunctions.bessely, order, value) +end + +function SpecialFunctions.besselh(order::Real, value::Complex{<:Measurements.Measurement}) + return _lift_complex(SpecialFunctions.besselh, order, value) +end + +""" +Lift local capacitance through its fixed-resolution least-squares equations. +Independent uncertainty directions preserve the original Measurement graph. +Only the nominal factorization and bounded Float64 derivative blocks are dense. +Kernel derivatives use centered steps, checked by halving the step. This avoids +both a dense Measurement matrix and repeated perturbed factorizations. +""" +function Engine.internal_shunt_response(selected::Engine.ShuntModel.Formula{:boundary}, + values::AbstractVector{<:Measurements.Measurement}, domain; kwargs...) + nominal_values = Float64.(measured_value.(values)) + tags = sort!( + unique!([tag for value in values + for tag in keys(uncertainty_components(value)) if !iszero(tag[2])]); + by = last) + if isempty(tags) + result = Engine.internal_shunt_response(selected, nominal_values, domain; kwargs...) + return (; + C = eltype(values).(result.C), diagnostic = result.diagnostic, state = nothing) + end + # Columns are physical perturbations per standard deviation of each + # independent input. Their distribution remains unchanged by this normalization. + directions = [Float64(derivative(value, tag)*tag[2]) for value in values, tag in tags] + result = Engine.internal_shunt_response( + selected, nominal_values, domain; directions, kwargs...) + # Return sensitivities through the original physical arguments, including + # shared and dependent ones. A small rank-revealing solve handles redundant + # descriptors without creating new independent Measurement identities. + factor = svd(transpose(directions)) + cutoff = max(size(directions)...)*eps(Float64)*maximum(factor.S) + keep = factor.S .> cutoff + gradient = factor.V[:, keep]*((transpose(factor.U[:, keep])*result.tangents) ./ + factor.S[keep]) + lifted = [measured_result(result.C[i], @view(gradient[:, i]), values) + for i in eachindex(result.C)] + return (; C = reshape(lifted, size(result.C)), + diagnostic = result.diagnostic, state = nothing) +end +# The archive source table spans all atomic points and a separate report reference. +function ImportExport.encode_observation(value::Measurements.Measurement,source_table) + contributions=NamedTuple[] + for source in keys(uncertainty_components(value)) + index=get!(source_table.indices,source) do + push!(source_table.sources,(nominal=source[1],sigma=source[2])) + length(source_table.sources) + end + push!(contributions,(source=index,sensitivity=derivative(value,source))) + end + sort!(contributions;by=entry -> entry.source) + return Dict("__type__"=>"ObservedMeasurement", + "nominal"=>serialize_value(nominal(value)), + "uncertainty"=>serialize_value(uncertainty(value)), + "contributions"=>serialize_value(contributions,Val(:scientific))) +end +function ImportExport.observation_sources(records::AbstractVector,::Val{:measurements}) + return [Measurements.measurement(record.nominal,record.sigma) for record in records] +end +function ImportExport.decode_observation_measurement(record,sources) + center=deserialize_value(record["nominal"]) + value=Measurements.measurement(center,zero(center)) + for entry in deserialize_value(record["contributions"]) + source=sources[entry.source] + value+=entry.sensitivity*(source-nominal(source)) + end + expected=deserialize_value(record["uncertainty"]) + isapprox(uncertainty(value),expected;rtol=1e-12,atol=0) || + throw(ArgumentError("saved uncertainty sensitivities do not reproduce the retained standard deviation")) + return value +end + +end diff --git a/ext/LineCableModelsWGLMakieExt.jl b/ext/LineCableModelsWGLMakieExt.jl new file mode 100644 index 000000000..02d73fcad --- /dev/null +++ b/ext/LineCableModelsWGLMakieExt.jl @@ -0,0 +1,13 @@ +""" + LineCableModelsWGLMakieExt + +Activate WGLMakie for browser-based LineCableModels rendering. +""" +module LineCableModelsWGLMakieExt + +import WGLMakie + +activate!() = (WGLMakie.activate!(); :wgl) +make_screen(::AbstractString; kwargs...) = nothing + +end # module LineCableModelsWGLMakieExt diff --git a/ext/LineCableModelsXLSXExt.jl b/ext/LineCableModelsXLSXExt.jl new file mode 100644 index 000000000..91bb3b5f0 --- /dev/null +++ b/ext/LineCableModelsXLSXExt.jl @@ -0,0 +1,40 @@ +""" + LineCableModelsXLSXExt + +Write ReportBuilder workbook descriptions with XLSX.jl. +""" +module LineCableModelsXLSXExt + +import LineCableModels.ReportBuilder +import LineCableModels: validate +import XLSX + +function ReportBuilder.write( + definition::ReportBuilder.XLSXReportDefinition, + encoded::ReportBuilder.XLSXWorkbook +) + validate([encoded],definition) + XLSX.openxlsx(encoded.destination, mode = "w") do workbook + for (index, sheet) in enumerate(encoded.sheets) + worksheet = if index == 1 + existing = workbook["Sheet1"] + XLSX.renamesheet!(existing, sheet.name) + existing + else + XLSX.addsheet!(workbook, sheet.name) + end + for cell in CartesianIndices(sheet.cells) + value = sheet.cells[cell] + ismissing(value) || (worksheet[cell[1], cell[2]] = value) + end + end + end + return encoded.destination +end + +function ReportBuilder.write(definition::ReportBuilder.XLSXReportDefinition, books::AbstractVector{<:ReportBuilder.XLSXWorkbook}) + validate(books,definition) + return [ReportBuilder.write(definition,book) for book in books] +end + +end # module LineCableModelsXLSXExt diff --git a/fatou.toml b/fatou.toml new file mode 100644 index 000000000..a764c7ac1 --- /dev/null +++ b/fatou.toml @@ -0,0 +1,36 @@ +# Source findings are advisory. The quality item separately rejects incomplete scans. +[lint] +select = [ + "duplicate-argument", + "duplicate-keyword-argument", + "duplicate-method", + "kwarg-default-mismatch", + "missing-include-file", + "include-cycle", + "julia-version-compat", + "misnamed-suppression", + "blanket-suppression", + "unused-binding", + "unused-type-parameter", + "unreachable-code", + "duplicate-include", + "redundant-boolean", + "unnecessary-nesting", +] + +[lint.severity] +duplicate-argument = "warning" +duplicate-keyword-argument = "warning" +duplicate-method = "warning" +kwarg-default-mismatch = "warning" +missing-include-file = "warning" +include-cycle = "warning" +julia-version-compat = "warning" +misnamed-suppression = "warning" +blanket-suppression = "warning" +unused-binding = "warning" +unused-type-parameter = "warning" +unreachable-code = "warning" +duplicate-include = "warning" +redundant-boolean = "warning" +unnecessary-nesting = "warning" diff --git a/schemas/cable-design.schema.json b/schemas/cable-design.schema.json new file mode 100644 index 000000000..66549fc6f --- /dev/null +++ b/schemas/cable-design.schema.json @@ -0,0 +1,1339 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://electa-git.github.io/LineCableModels.jl/schemas/cable-design.schema.json", + "title": "LineCableModels cable declarations", + "type": "object", + "required": [ + "$schema", + "format", + "version", + "materials" + ], + "properties": { + "$schema": { + "const": "https://json-schema.org/draft/2020-12/schema" + }, + "format": { + "enum": [ + "linecablemodels.cable", + "linecablemodels.materials" + ] + }, + "version": { + "const": "1.0.0" + }, + "materials": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/material" + } + }, + "root": { + "$ref": "#/$defs/root" + } + }, + "allOf": [ + { + "if": { + "properties": { + "format": { + "const": "linecablemodels.cable" + } + } + }, + "then": { + "required": [ + "root" + ] + }, + "else": { + "not": { + "required": [ + "root" + ] + } + } + } + ], + "additionalProperties": false, + "$defs": { + "typedFloat": { + "type": "object", + "required": [ + "__type__" + ], + "properties": { + "__type__": { + "enum": [ + "Float16", + "Float32", + "Float64", + "BigFloat" + ] + }, + "value": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ] + }, + "special": { + "enum": [ + "Inf", + "-Inf", + "NaN" + ] + } + }, + "oneOf": [ + { + "required": [ + "value" + ] + }, + { + "required": [ + "special" + ] + } + ], + "additionalProperties": false + }, + "atomicScalar": { + "oneOf": [ + { + "type": "number" + }, + { + "$ref": "#/$defs/typedFloat" + } + ] + }, + "numericGrid": { + "type": "object", + "required": [ + "grid" + ], + "properties": { + "grid": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/atomicScalar" + } + }, + "rel": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/atomicScalar" + } + }, + "abs": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/atomicScalar" + } + } + }, + "not": { + "required": [ + "rel", + "abs" + ] + }, + "additionalProperties": false + }, + "symbolGrid": { + "type": "object", + "required": [ + "grid" + ], + "properties": { + "grid": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "additionalProperties": false + }, + "scalar": { + "oneOf": [ + { + "$ref": "#/$defs/atomicScalar" + }, + { + "$ref": "#/$defs/numericGrid" + } + ] + }, + "nullableScalar": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/scalar" + } + ] + }, + "capacity": { + "type": "object", + "required": [ + "kind" + ], + "properties": { + "kind": { + "const": "capacity" + } + }, + "additionalProperties": false + }, + "cardinality": { + "oneOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "$ref": "#/$defs/capacity" + } + ] + }, + "nominalData": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "value": {} + }, + "additionalProperties": false + } + }, + "material": { + "oneOf": [ + { + "$ref": "#/$defs/scalarMaterial" + }, + { + "$ref": "#/$defs/radialDielectric" + } + ] + }, + "radialDielectric": { + "type": "object", + "required": [ + "kind", + "materials", + "weights", + "mu_r" + ], + "properties": { + "kind": { + "const": "radial_dielectric" + }, + "materials": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/scalarMaterial" + } + }, + "weights": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/scalar" + } + }, + "mu_r": { + "$ref": "#/$defs/scalar" + } + }, + "additionalProperties": false + }, + "scalarMaterial": { + "type": "object", + "required": [ + "kind", + "rho", + "eps_r", + "mu_r", + "T0", + "alpha", + "rho_thermal", + "theta_max", + "tan_delta", + "sigma_solar" + ], + "properties": { + "kind": { + "oneOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "$ref": "#/$defs/symbolGrid" + } + ] + }, + "rho": { + "$ref": "#/$defs/scalar" + }, + "eps_r": { + "$ref": "#/$defs/scalar" + }, + "mu_r": { + "$ref": "#/$defs/scalar" + }, + "T0": { + "$ref": "#/$defs/scalar" + }, + "alpha": { + "$ref": "#/$defs/scalar" + }, + "rho_thermal": { + "$ref": "#/$defs/scalar" + }, + "theta_max": { + "$ref": "#/$defs/scalar" + }, + "tan_delta": { + "$ref": "#/$defs/scalar" + }, + "sigma_solar": { + "$ref": "#/$defs/scalar" + } + }, + "additionalProperties": false + }, + "pose": { + "type": "object", + "required": [ + "kind", + "x", + "y", + "φ" + ], + "properties": { + "kind": { + "const": "pose2" + }, + "x": { + "$ref": "#/$defs/scalar" + }, + "y": { + "$ref": "#/$defs/scalar" + }, + "φ": { + "$ref": "#/$defs/scalar" + } + }, + "additionalProperties": false + }, + "earthLayer": { + "type": "object", + "required": [ + "kind", + "rho", + "eps_r", + "mu_r", + "thickness" + ], + "properties": { + "kind": { + "const": "earth_layer" + }, + "rho": { + "$ref": "#/$defs/scalar" + }, + "eps_r": { + "$ref": "#/$defs/scalar" + }, + "mu_r": { + "$ref": "#/$defs/scalar" + }, + "thickness": { + "$ref": "#/$defs/scalar" + } + }, + "additionalProperties": false + }, + "earthModel": { + "type": "object", + "required": [ + "kind", + "vertical_layers", + "layers" + ], + "properties": { + "kind": { + "const": "earth_model" + }, + "vertical_layers": { + "type": "boolean" + }, + "layers": { + "type": "array", + "minItems": 2, + "items": { + "$ref": "#/$defs/earthLayer" + } + } + }, + "additionalProperties": false + }, + "primitive": { + "oneOf": [ + { + "type": "object", + "required": [ + "kind", + "r" + ], + "properties": { + "kind": { + "const": "disk" + }, + "r": { + "$ref": "#/$defs/scalar" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "kind", + "w", + "h" + ], + "properties": { + "kind": { + "const": "rectangle" + }, + "w": { + "$ref": "#/$defs/scalar" + }, + "h": { + "$ref": "#/$defs/scalar" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "kind", + "a", + "b" + ], + "properties": { + "kind": { + "const": "ellipse" + }, + "a": { + "$ref": "#/$defs/scalar" + }, + "b": { + "$ref": "#/$defs/scalar" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "kind", + "span", + "r_base", + "r_back", + "fillet" + ], + "properties": { + "kind": { + "const": "sector" + }, + "span": { + "$ref": "#/$defs/scalar" + }, + "r_base": { + "$ref": "#/$defs/scalar" + }, + "r_back": { + "$ref": "#/$defs/scalar" + }, + "fillet": { + "$ref": "#/$defs/scalar" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "kind", + "ri", + "ro" + ], + "properties": { + "kind": { + "const": "annulus" + }, + "ri": { + "$ref": "#/$defs/scalar" + }, + "ro": { + "$ref": "#/$defs/scalar" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "kind", + "t" + ], + "properties": { + "kind": { + "const": "shell" + }, + "t": { + "$ref": "#/$defs/scalar" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "kind", + "points" + ], + "properties": { + "kind": { + "const": "polygon" + }, + "points": { + "type": "array", + "minItems": 3, + "items": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/$defs/scalar" + }, + { + "$ref": "#/$defs/scalar" + } + ], + "items": false + } + } + }, + "additionalProperties": false + } + ] + }, + "pattern": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "object", + "required": [ + "kind", + "n", + "r", + "φ0", + "span", + "gap_frac" + ], + "properties": { + "kind": { + "const": "ring" + }, + "n": { + "$ref": "#/$defs/cardinality" + }, + "r": { + "$ref": "#/$defs/nullableScalar" + }, + "φ0": { + "$ref": "#/$defs/scalar" + }, + "span": { + "$ref": "#/$defs/scalar" + }, + "gap_frac": { + "$ref": "#/$defs/scalar" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "kind", + "nr", + "nφ", + "r0", + "dr", + "φ0", + "span" + ], + "properties": { + "kind": { + "const": "polar" + }, + "nr": { + "type": "integer", + "minimum": 1 + }, + "nφ": { + "type": "integer", + "minimum": 1 + }, + "r0": { + "$ref": "#/$defs/scalar" + }, + "dr": { + "$ref": "#/$defs/scalar" + }, + "φ0": { + "$ref": "#/$defs/scalar" + }, + "span": { + "$ref": "#/$defs/scalar" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "kind", + "r", + "φ", + "φ0", + "span" + ], + "properties": { + "kind": { + "const": "fill" + }, + "r": { + "$ref": "#/$defs/scalar" + }, + "φ": { + "$ref": "#/$defs/scalar" + }, + "φ0": { + "$ref": "#/$defs/scalar" + }, + "span": { + "$ref": "#/$defs/scalar" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "kind", + "nx", + "ny", + "dx", + "dy" + ], + "properties": { + "kind": { + "const": "lattice" + }, + "nx": { + "type": "integer", + "minimum": 1 + }, + "ny": { + "type": "integer", + "minimum": 1 + }, + "dx": { + "$ref": "#/$defs/scalar" + }, + "dy": { + "$ref": "#/$defs/scalar" + } + }, + "additionalProperties": false + }, + { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/pose" + } + } + ] + }, + "lay": { + "oneOf": [ + { + "type": "object", + "required": [ + "kind", + "q" + ], + "properties": { + "kind": { + "const": "lay_ratio" + }, + "q": { + "$ref": "#/$defs/scalar" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "kind", + "p" + ], + "properties": { + "kind": { + "const": "pitch" + }, + "p": { + "$ref": "#/$defs/scalar" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "kind", + "α" + ], + "properties": { + "kind": { + "const": "lay_angle" + }, + "α": { + "$ref": "#/$defs/scalar" + } + }, + "additionalProperties": false + } + ] + }, + "path": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "object", + "required": [ + "kind", + "lay", + "dir", + "φ0" + ], + "properties": { + "kind": { + "const": "helix" + }, + "lay": { + "$ref": "#/$defs/lay" + }, + "dir": { + "enum": [ + -1, + 1 + ] + }, + "φ0": { + "$ref": "#/$defs/scalar" + } + }, + "additionalProperties": false + }, + { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "kind", + "lay", + "dir", + "φ0" + ], + "properties": { + "kind": { + "const": "helix" + }, + "lay": { + "$ref": "#/$defs/lay" + }, + "dir": { + "enum": [ + -1, + 1 + ] + }, + "φ0": { + "$ref": "#/$defs/scalar" + } + }, + "additionalProperties": false + } + } + ] + }, + "compaction": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "boolean" + }, + { + "type": "object", + "required": [ + "kind", + "η" + ], + "properties": { + "kind": { + "const": "fill_factor" + }, + "η": { + "$ref": "#/$defs/scalar" + } + }, + "additionalProperties": false + } + ] + }, + "materialRef": { + "oneOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "const": "material" + }, + "value": { + "$ref": "#/$defs/material" + } + }, + "additionalProperties": false + } + ] + }, + "part": { + "oneOf": [ + { + "type": "object", + "required": [ + "kind", + "tag", + "primitive", + "material" + ], + "properties": { + "kind": { + "const": "region" + }, + "tag": { + "type": "string", + "minLength": 1 + }, + "primitive": { + "$ref": "#/$defs/primitive" + }, + "material": { + "$ref": "#/$defs/materialRef" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "kind", + "items" + ], + "properties": { + "kind": { + "const": "stack" + }, + "items": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/part" + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "kind", + "name", + "at", + "item", + "pattern", + "path", + "compact", + "boundary" + ], + "properties": { + "kind": { + "const": "group" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "at": { + "$ref": "#/$defs/pose" + }, + "item": { + "$ref": "#/$defs/part" + }, + "pattern": { + "$ref": "#/$defs/pattern" + }, + "path": { + "$ref": "#/$defs/path" + }, + "compact": { + "$ref": "#/$defs/compaction" + }, + "boundary": { + "oneOf": [ + { + "type": "null" + }, + { + "allOf": [ + { + "$ref": "#/$defs/primitive" + }, + { + "properties": { + "kind": { + "enum": [ + "disk", + "sector" + ] + } + }, + "required": [ + "kind" + ] + } + ] + } + ] + } + }, + "allOf": [ + { + "if": { + "properties": { + "boundary": { + "not": { + "type": "null" + } + } + }, + "required": [ + "boundary" + ] + }, + "then": { + "properties": { + "pattern": { + "type": "null" + }, + "path": { + "type": "null" + } + } + } + }, + { + "if": { + "properties": { + "boundary": { + "properties": { + "kind": { + "const": "sector" + } + }, + "required": [ + "kind" + ] + } + }, + "required": [ + "boundary" + ] + }, + "then": { + "properties": { + "compact": { + "type": "null" + } + } + } + } + ], + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "kind", + "at", + "item", + "pattern", + "path", + "compact", + "names" + ], + "properties": { + "kind": { + "const": "assembly" + }, + "at": { + "$ref": "#/$defs/pose" + }, + "item": { + "$ref": "#/$defs/part" + }, + "pattern": { + "$ref": "#/$defs/pattern" + }, + "path": { + "$ref": "#/$defs/path" + }, + "compact": { + "$ref": "#/$defs/compaction" + }, + "names": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "kind", + "at", + "members" + ], + "properties": { + "kind": { + "const": "assembly" + }, + "at": { + "$ref": "#/$defs/pose" + }, + "members": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "at", + "item" + ], + "properties": { + "at": { + "$ref": "#/$defs/pose" + }, + "item": { + "$ref": "#/$defs/part" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "kind", + "tag", + "at", + "primitive", + "item", + "fill", + "wall" + ], + "properties": { + "kind": { + "const": "enclosure" + }, + "tag": { + "type": "string", + "minLength": 1 + }, + "at": { + "$ref": "#/$defs/pose" + }, + "primitive": { + "$ref": "#/$defs/primitive" + }, + "item": { + "$ref": "#/$defs/part" + }, + "fill": { + "oneOf": [ + { + "$ref": "#/$defs/materialRef" + }, + { + "$ref": "#/$defs/part" + } + ] + }, + "wall": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/part" + } + ] + } + }, + "additionalProperties": false + } + ] + }, + "cableDesign": { + "type": "object", + "required": [ + "kind", + "cable_id", + "nominal_data", + "origin" + ], + "properties": { + "kind": { + "const": "cable_design" + }, + "cable_id": { + "type": "string", + "minLength": 1 + }, + "nominal_data": { + "$ref": "#/$defs/nominalData" + }, + "origin": { + "$ref": "#/$defs/part" + } + }, + "additionalProperties": false + }, + "cataloguedCable": { + "type": "object", + "required": [ + "kind", + "cable_id", + "nominal_data", + "origin", + "datasheet" + ], + "properties": { + "kind": { + "const": "cable_design" + }, + "cable_id": { + "type": "string", + "minLength": 1 + }, + "nominal_data": { + "$ref": "#/$defs/nominalData" + }, + "origin": { + "$ref": "#/$defs/part" + }, + "datasheet": { + "type": "object" + } + }, + "additionalProperties": false + }, + "lineCableSystem": { + "type": "object", + "required": [ + "kind", + "system_id", + "line_length", + "designs", + "positions", + "connections", + "environment" + ], + "properties": { + "kind": { + "const": "line_cable_system" + }, + "system_id": { + "type": "string", + "minLength": 1 + }, + "line_length": { + "$ref": "#/$defs/scalar" + }, + "designs": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/cableDesign" + } + }, + "positions": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/pose" + } + }, + "input_positions": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/pose" + } + }, + "clearances": { + "type": "array", + "minItems": 1, + "items": { + "type": "array", + "items": { + "$ref": "#/$defs/scalar" + } + } + }, + "connections": { + "type": "array", + "minItems": 1, + "items": { + "type": "array", + "items": { + "type": "integer", + "minimum": 0 + } + } + }, + "environment": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/earthModel" + } + ] + } + }, + "additionalProperties": false + }, + "root": { + "oneOf": [ + { + "$ref": "#/$defs/cableDesign" + }, + { + "$ref": "#/$defs/lineCableSystem" + }, + { + "type": "object", + "required": [ + "kind", + "cables" + ], + "properties": { + "kind": { + "const": "cable_library" + }, + "cables": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/cataloguedCable" + } + } + }, + "additionalProperties": false + } + ] + }, + "grid": { + "oneOf": [ + { + "$ref": "#/$defs/numericGrid" + }, + { + "$ref": "#/$defs/symbolGrid" + } + ] + } + } +} diff --git a/showcase/assets/img/cable_photo.jpg b/showcase/assets/img/cable_photo.jpg deleted file mode 100644 index a0124b0ad..000000000 Binary files a/showcase/assets/img/cable_photo.jpg and /dev/null differ diff --git a/showcase/assets/img/cables1.png b/showcase/assets/img/cables1.png deleted file mode 100644 index c9dc948ef..000000000 Binary files a/showcase/assets/img/cables1.png and /dev/null differ diff --git a/showcase/assets/img/cables2.png b/showcase/assets/img/cables2.png deleted file mode 100644 index fece6a13b..000000000 Binary files a/showcase/assets/img/cables2.png and /dev/null differ diff --git a/showcase/assets/img/earthreturn.png b/showcase/assets/img/earthreturn.png deleted file mode 100644 index a447c8738..000000000 Binary files a/showcase/assets/img/earthreturn.png and /dev/null differ diff --git a/showcase/assets/img/future_grids.svg b/showcase/assets/img/future_grids.svg deleted file mode 100644 index 2fe4396ee..000000000 --- a/showcase/assets/img/future_grids.svg +++ /dev/null @@ -1,225 +0,0 @@ - - - - - - - - - - - - - - - - - - TODAY - AC Cables - FUTURE - DC Cables - More underground cables - Challenge 1 - Challenge 2 - Challenge 3 - Challenge 4 - Protection of cable-based systems - Control interactions - Resilient DC networks - DC Grids - - diff --git a/showcase/assets/img/proxeffect.png b/showcase/assets/img/proxeffect.png deleted file mode 100644 index eeee0f7aa..000000000 Binary files a/showcase/assets/img/proxeffect.png and /dev/null differ diff --git a/showcase/assets/img/pscad_export.png b/showcase/assets/img/pscad_export.png deleted file mode 100644 index 5cde930b5..000000000 Binary files a/showcase/assets/img/pscad_export.png and /dev/null differ diff --git a/showcase/assets/img/skeffect.png b/showcase/assets/img/skeffect.png deleted file mode 100644 index bf5695adc..000000000 Binary files a/showcase/assets/img/skeffect.png and /dev/null differ diff --git a/showcase/showcase1.jl b/showcase/showcase1.jl deleted file mode 100644 index a15388450..000000000 --- a/showcase/showcase1.jl +++ /dev/null @@ -1,1376 +0,0 @@ -### A Pluto.jl notebook ### -# v0.20.18 - -#> [frontmatter] -#> image = "https://raw.githubusercontent.com/Electa-Git/LineCableModels.jl/main/docs/src/assets/logo.svg" -#> language = "en-US" -#> title = "Uncertainty of Frequency Dependent Impedance Parameters for Transmission Assets" -#> date = "2025-09-16" -#> description = "LineCableModels.jl showcase" -#> -#> [[frontmatter.author]] -#> name = "Amauri Martins" -#> url = "https://github.com/amaurigmartins" - -using Markdown -using InteractiveUtils - -# This Pluto notebook uses @bind for interactivity. When running this notebook outside of Pluto, the following 'mock version' of @bind gives bound variables a default value (instead of an error). -macro bind(def, element) - #! format: off - return quote - local iv = try Base.loaded_modules[Base.PkgId(Base.UUID("6e696c72-6542-2067-7265-42206c756150"), "AbstractPlutoDingetjes")].Bonds.initial_value catch; b -> missing; end - local el = $(esc(element)) - global $(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : iv(el) - el - end - #! format: on -end - -# ╔═╡ 426024d7-23c8-4261-9c20-d0045b1ab077 -begin - using Pkg - Pkg.activate() - using TOML - - # ---------- Env detection ---------- - function runtime_env()::Symbol - e = ENV - binder_vars = ("BINDER_LAUNCH_HOST", "BINDER_REQUEST", "BINDER_POD_NAME", - "BINDER_URL", "BINDER_REPO_URL", "JUPYTER_IMAGE_SPEC") - jhub = ("JUPYTERHUB_API_URL", "JUPYTERHUB_SERVICE_PREFIX", "JUPYTERHUB_HOST", - "JUPYTERHUB_SERVER_NAME", "JUPYTERHUB_USER") - if any(haskey(e, k) for k in binder_vars) || - (any(haskey(e, k) for k in jhub) && get(e, "NB_USER", "") == "jovyan") - return :binder - elseif any(haskey(e, k) for k in jhub) - return :jupyterhub - else - return :local - end - end - - # ---------- Find Project.toml going up from a start dir ---------- - function find_project(start_dir::AbstractString = @__DIR__)::Union{String, Nothing} - dir = abspath(start_dir) - while true - p = joinpath(dir, "Project.toml") - isfile(p) && return p - parent = dirname(dir) - parent == dir && return nothing - dir = parent - end - end - - # ---------- Add every [deps] entry from Project.toml (ignore compat on purpose) ---------- - function add_deps_from_project!(project_toml::AbstractString) - tbl = TOML.parsefile(project_toml) - deps = get(tbl, "deps", Dict{String, Any}()) - isempty(deps) && return - for (name, uuid) in deps - try - # Ignore [compat] here on purpose; Binder just needs things present. - Pkg.add(Pkg.PackageSpec(; name = name, uuid = string(uuid))) - catch err - @warn "Failed to add $name ($uuid)" error=err - end - end - try - Pkg.precompile() - catch - ; - end - return nothing - end - - # ---------- Public entry point ---------- - """ - binder_bootstrap!(; prefer_project_root=true) - - If running on Binder/JupyterHub: - - Activates the nearest project (folder containing Project.toml, walking upwards). - - Force-adds every package listed in `[deps]` via `Pkg.add` (ignoring compat). - Else (local): - - Leaves your local setup alone (no-op except a best-effort `Pkg.activate()`). - - Returns `nothing`. - """ - function binder_bootstrap!(; prefer_project_root::Bool = true) - env = runtime_env() - if env === :binder || env === :jupyterhub - project_toml = find_project() - project_toml === nothing && (@warn "No Project.toml found."; return nothing) - @info "Runtime environment: $env" - - # Best effort: instantiate first; if anything remains missing, add explicitly - try - Pkg.instantiate() # uses [sources] when this env is active - catch err - @warn "instantiate() failed; falling back to explicit add loop" error=err - end - add_from_project!(project_toml) - - end - return nothing - end - - function add_from_project!(project_toml::AbstractString) - tbl = TOML.parsefile(project_toml) - deps = get(tbl, "deps", Dict{String, Any}()) - sources = get(tbl, "sources", Dict{String, Any}()) # official Pkg feature - - ensure_general_registry!() - - # 1) First, add unregistered/source-pinned deps described in [sources] - for (name, src) in sources - spec_kwargs = Dict{Symbol, Any}(:name => name) - haskey(deps, name) && (spec_kwargs[:uuid] = string(deps[name])) - haskey(src, "url") && (spec_kwargs[:url] = src["url"]) - haskey(src, "rev") && (spec_kwargs[:rev] = src["rev"]) - haskey(src, "path") && (spec_kwargs[:path] = src["path"]) - haskey(src, "subdir") && (spec_kwargs[:subdir] = src["subdir"]) - @show name - @show src - try - Pkg.add(Pkg.PackageSpec(; spec_kwargs...)) - catch err - @warn "Failed to add [sources] $name" error=err spec=spec_kwargs - end - end - - # 2) Then add the remaining registered deps - for (name, uuid) in deps - haskey(sources, name) && continue - try - Pkg.add(Pkg.PackageSpec(; name = name, uuid = string(uuid))) - catch err - @warn "Failed to add registered dep $name ($uuid)" error=err - end - end - - try - Pkg.precompile() - catch - ; - end - return nothing - end - -end; - -# ╔═╡ 3183ff8d-a9fd-4035-af8a-664bec3606d4 -begin - binder_bootstrap!() - using Makie, PlutoUI, Colors - using LineCableModels - using DataFrames - using HypertextLiteral -end - -# ╔═╡ b081c88a-7959-44ea-85ff-33b980ec71b4 -begin - using Measurements: measurement, value - - # mm + percent → Measurement with absolute σ = (pct/100)*nom - _with_unc(nom_mm::Real, pct::Real) = - measurement(nom_mm/1000, abs(nom_mm/1000) * pct/100) - - # Pitch: start at 15, decrease 2.5 per layer, clamp at 10 - pitch_for_layer(ℓ::Integer) = max(10.0, 15.0 - 2.5*(ℓ - 1)) - - function build_core(materials, d_wire_mm::Real, d_wire_pct::Real, n_layers::Int) - d = _with_unc(d_wire_mm, d_wire_pct) # Measurement - core = ConductorGroup(CircStrands(0, Diameter(d), 1, 0, get(materials, "aluminum"))) - for ℓ in 1:n_layers - add!( - core, - CircStrands, - Diameter(d), - 6*ℓ, - pitch_for_layer(ℓ), - get(materials, "aluminum"), - ) - end - return core - end - - function build_geometry(materials; - d_wire_mm::Real, d_wire_pct::Real, - t_sc_in_mm::Real, t_sc_in_pct::Real, - t_ins_mm::Real, t_ins_pct::Real, - t_sc_out_mm::Real, t_sc_out_pct::Real, - n_layers::Int, - t_sct, # keep your existing t_sct (mm, can be Real or Measurement) - ) - # Core - core = build_core(materials, d_wire_mm, d_wire_pct, n_layers) - - # Layer thicknesses as Measurements (mm) - t_sc_in = _with_unc(t_sc_in_mm, t_sc_in_pct) - t_ins = _with_unc(t_ins_mm, t_ins_pct) - t_sc_out = _with_unc(t_sc_out_mm, t_sc_out_pct) - - # Insulation group - main_insu = InsulatorGroup( - Semicon(core, Thickness(t_sct), get(materials, "polyacrylate")), - ) - add!(main_insu, Semicon, Thickness(t_sc_in), get(materials, "semicon1")) - add!(main_insu, Insulator, Thickness(t_ins), get(materials, "pe")) - add!(main_insu, Semicon, Thickness(t_sc_out), get(materials, "semicon2")) - add!(main_insu, Semicon, Thickness(t_sct), get(materials, "polyacrylate")) - - core_cc = CableComponent("core", core, main_insu) - return core_cc, main_insu - end -end; - -# ╔═╡ 46cfd6fa-b4d6-44c3-83cf-d2b9b1ff1cf1 -# Override CSS settings -@htl(""" - - - -""") - -# ╔═╡ 4462e48f-0d08-4ad9-8dd9-12f4f5912f38 -begin - struct TwoColumn{A, B} - left::A - right::B - end - - # New light wrapper that carries widths (percentages) - struct TwoColumnWithWidths{A, B} - left::A - right::B - widths::NTuple{2, Float64} # (left%, right%) - end - - # Convenience “constructor” with keywords — old calls still work, - # new calls with kws return the width-aware wrapper - TwoColumn(left, right; left_pct::Real = 50.0, right_pct::Real = 50.0) = - TwoColumnWithWidths{typeof(left), typeof(right)}( - left, - right, - (float(left_pct), float(right_pct)), - ) - - # Original show (defaults to 50/50) - function Base.show(io, mime::MIME"text/html", tc::TwoColumn) - write( - io, - """ -
-
""", - ) - show(io, mime, tc.left) - write( - io, - """ -
-
""", - ) - show(io, mime, tc.right) - write( - io, - """ -
-
""", - ) - end - - # New show for width-aware variant - function Base.show(io, mime::MIME"text/html", tc::TwoColumnWithWidths) - l, r = tc.widths - write( - io, - """ -
-
""", - ) - show(io, mime, tc.left) - write( - io, - """ -
-
""", - ) - show(io, mime, tc.right) - write( - io, - """ -
-
""", - ) - end - - struct Foldable{C} - title::String - content::C - end - - function Base.show(io, mime::MIME"text/html", fld::Foldable) - write(io, "
$(fld.title)

") - show(io, mime, fld.content) - write(io, "

") - end - - LocalImage(x::AbstractString; attrs...) = - LocalResource(joinpath(@__DIR__, "assets", "img", x), pairs(attrs)...) -end; - -# ╔═╡ e90baf94-c8b8-41aa-8728-e129f7f6881e -@htl( - """ - - - -""" -) - - -# ╔═╡ 532cb61b-97b6-43e7-a8f9-3a5f12b8b3f7 -@htl(""" - - - -""") - -# ╔═╡ b16ff72c-872a-4505-9468-6cefd4a8852c - -@htl( - """ - - - - - - - -""" -) - - -# ╔═╡ 9fefeafa-63f9-43d0-a2ee-4d4fca170126 -begin - @htl(""" - - """) - - anchor(id::AbstractString = "home"; offset_px::Real = 0) = @htl(""" - - """) -end; - -# ╔═╡ fde80e93-1964-4287-acfc-a2da2d4b7d48 -TableOfContents() - -# ╔═╡ 77d3c731-bdae-46c2-8b23-eb0b860e7444 -md""" ---- -""" - -# ╔═╡ 9ef6f05f-c384-4fab-bc39-e47edb49f994 -anchor("home"; offset_px = 256) - -# ╔═╡ f08a32db-05d9-4ddb-9c46-34bc623ce5e7 -md""" -$(LocalResource(joinpath(@__DIR__, "..", "assets", "img", "ETCH_LOGO_RGB_NEG.svg"), :width => 350, :style => "margin-right: 40px;")) -""" - -# ╔═╡ 50384351-fc38-4b29-9bf6-db1556d49dee -html""" -

Uncertainty of Frequency Dependent Impedance Parameters for Transmission Assets

-

Second Annual Belgian Energy Transition Workshop

-""" - -# ╔═╡ 23913cc6-a81b-4098-bacf-7a2e09998e53 -md""" - -#### -#### Amauri Martins - -#### [amauri.martinsbritto@kuleuven.be](mailto:amauri.martinsbritto@kuleuven.be) - -#### KU Leuven – Etch / EnergyVille - -##### 16 September 2025 -""" - -# ╔═╡ d482241c-4bd5-4896-bdc1-e82387f69051 -md""" -$(LocalResource(joinpath(@__DIR__, "..", "assets", "img", "ENERGYVILLE-LOGO.svg"), :width => 150, :style => "margin-right: 40px;")) -$(LocalResource(joinpath(@__DIR__, "..", "assets", "img", "kul_logo.svg"), :width => 150, :style => "margin-right: 0px;")) -""" - -# ╔═╡ 14f07acc-5353-4b1d-b94f-9ae43f87289b -md""" -# Introduction -""" - -# ╔═╡ 6c6e4d21-cc38-46eb-8178-4cc4a99adcba -TwoColumn( - html"
-

- Energy -

-

- Transmission -

-

- Competence -

-

- Hub -

-
", - md"""#### *Pioneering research for future-proofing electricity networks with large-scale integration of High Voltage Direct Current HVDC technology and underground cables.* - - ### Key challenges - - 1. ###### More **underground cables** - 2. ###### **Protection** of cable-based systems - 3. ###### **Control** interactions - 4. ###### **Resilient HVDC** grids - $(LocalImage("future_grids.svg", width = 600, style = "display: block; float: left; margin-left: auto; margin-right: auto;")) - """) - -# ╔═╡ 3e6a9c64-827d-4491-bcac-252ee7b1dc81 -md""" -## Undergrounding of future power systems -""" - -# ╔═╡ 877a84cc-979f-48c9-ac41-59be60b4850b -TwoColumn( - md""" - ### More underground cables - - - #### Large-scale integration of HVDC technology and underground cables. - - #### Benefits in environmental impact, reliability, and public acceptance. - - #### High costs and technical complexities still poses challenges to widespread adoption. - - ### Research roadmaps - - - #### Accurate modelling of cables, joints and interaction with (complex) environment. - - #### HVDC cables systems in multiterminal HVDC grids. - - #### Enhanced computational tools in view of operation, diagnostics and condition monitoring. - - #### Cable hosting capacity. - - """, - md"""$(LocalImage("cables1.png", width=250, style="display: block; margin-left: auto; margin-right: auto; margin-bottom: 50px;")) - $(LocalImage("cables2.png", width=250, style="display: block; margin-left: auto; margin-right: auto;")) - """) - -# ╔═╡ db1944b6-c55f-4091-8128-8d297bdc9a74 -md""" -## Sources of uncertainties in cable parameters -""" - -# ╔═╡ 5397f442-8dc1-42a6-941d-0b1d58057a6b - -TwoColumn( - html""" -
-
- Internal and external origins: -
-
    -
  • Geometrical and material properties
  • -
  • - Real field data (resistivity, actual conductor layout etc.) -
  • -
  • Presence of interferences
  • -
  • - Modeling procedure (parameters and EMT) -
  • -
-
- """, - md""" -$(LocalImage("skeffect.png", width=400, style="display: block; margin-left: auto; margin-right: auto; margin-bottom: 50px;")) -$(LocalImage("earthreturn.png", width=400, style="display: block; margin-left: auto; margin-right: auto;")) - """; left_pct = 50, right_pct = 50) - -# ╔═╡ a3f5a8c5-4ab9-4a33-abab-7907ffab1347 -md""" -## Uncertainty quantification -""" - -# ╔═╡ 382252ca-ede1-4043-b921-7834e59810cb -md""" -#### - Physical quantities are treated as nominal values associated to the corresponding uncertainties, i.e. ``\hat{x} = x ± \delta x``, ``\hat{y} = y ± \delta y``. Uncertainties are propagated according to the linear error propagation theory by using the package `Measurements.jl`. -""" - -# ╔═╡ 96121e5b-6b5b-4ab1-81d0-6dcbe924cda2 - -TwoColumn( - md""" - $(LocalResource(joinpath(@__DIR__, "..", "assets", "img", "cable_dark_mode.svg"), :width => 800, :style => "display: block; margin-top: 50px; margin-left: auto; margin-right: auto;")) - """, - md""" - #### - Addition and subtraction: - #### ``\hat{z} = \hat{x} \pm \hat{y} = (x \pm y) \pm \sqrt{(\delta x)^2 + (\delta y)^2}`` - #### - Multiplication and division: - #### ``\hat{z} = (x \cdot y \text{ or } x/y) \pm \delta z`` - #### ``\frac{\delta z}{|z|} = \sqrt{\left(\frac{\delta x}{x}\right)^2 + \left(\frac{\delta y}{y}\right)^2}`` - #### - For an arbitrary function ``f(\hat{x}, \hat{y}, ...)`` - #### ``\delta f = \sqrt{\left( \frac{\partial f}{\partial x} \delta x \right)^2 + \left( \frac{\partial f}{\partial y} \delta y \right)^2 + \dots }`` - - !!! warning "Warning" - Even when subtracting the nominal values ($x-y$), the uncertainties are still combined, leading to a larger total uncertainty. - """; left_pct = 65, right_pct = 35) - - -# ╔═╡ a8ea0da0-36f1-44d4-9415-d3041f34c23f -md""" -# Application study -""" - -# ╔═╡ f5fa7e28-97a7-456b-87a9-5ac4b76be9d4 -begin - num_co_wires = 61 # number of core wires - num_sc_wires = 49 # number of screen wires - d_core = 38.1e-3 # nominal core overall diameter - d_w = 4.7e-3 # nominal strand diameter of the core - t_sc_in = 0.6e-3 # nominal internal semicon thickness - t_ins = 8e-3 # nominal main insulation thickness - t_sc_out = 0.3e-3 # nominal external semicon thickness - d_ws = .95e-3 # nominal wire screen diameter - t_cut = 0.1e-3 # nominal thickness of the copper tape (around wire screens) - w_cut = 10e-3 # nominal width of copper tape - t_wbt = .3e-3 # nominal thickness of the water blocking tape - t_sct = .3e-3 # nominal thickness of the semiconductive tape - t_alt = .15e-3 # nominal thickness of the aluminum tape - t_pet = .05e-3 # nominal thickness of the pe face in the aluminum tape - t_jac = 2.4e-3 # nominal PE jacket thickness - - d_overall = d_core # hide - layers = [] # hide - push!(layers, ("Conductor", missing, d_overall * 1000)) # hide - d_overall += 2 * t_sct # hide - push!(layers, ("Inner semiconductive tape", t_sct * 1000, d_overall * 1000)) # hide - d_overall += 2 * t_sc_in # hide - push!(layers, ("Inner semiconductor", t_sc_in * 1000, d_overall * 1000)) # hide - d_overall += 2 * t_ins # hide - push!(layers, ("Main insulation", t_ins * 1000, d_overall * 1000)) # hide - d_overall += 2 * t_sc_out # hide - push!(layers, ("Outer semiconductor", t_sc_out * 1000, d_overall * 1000)) # hide - d_overall += 2 * t_sct # hide - push!(layers, ("Outer semiconductive tape", t_sct * 1000, d_overall * 1000)) # hide - d_overall += 2 * d_ws # hide - push!(layers, ("Wire screen", d_ws * 1000, d_overall * 1000)) # hide - d_overall += 2 * t_cut # hide - push!(layers, ("Copper tape", t_cut * 1000, d_overall * 1000)) # hide - d_overall += 2 * t_wbt # hide - push!(layers, ("Water-blocking tape", t_wbt * 1000, d_overall * 1000)) # hide - d_overall += 2 * t_alt # hide - push!(layers, ("Aluminum tape", t_alt * 1000, d_overall * 1000)) # hide - d_overall += 2 * t_pet # hide - push!(layers, ("PE with aluminum face", t_pet * 1000, d_overall * 1000)) # hide - d_overall += 2 * t_jac # hide - push!(layers, ("PE jacket", t_jac * 1000, d_overall * 1000)) # hide - - nothing -end - -# ╔═╡ 8c2eaef0-4e01-41b9-b1a6-a20dfa9b2d57 -md""" -## Cable specifications - -* ##### Aluminum 1000 mm² cable, 18/30 kV, NA2XS(FL)2Y 18/30 kV -""" - -# ╔═╡ cb8f01ae-26e0-44ce-8347-298ab692ac63 -TwoColumn( - md"""$(DataFrame( # hide - layer = first.(layers), # hide - thickness = [ # hide - ismissing(t) ? "-" : round(t, sigdigits = 2) for t in getindex.(layers, 2) # hide - ], # hide - diameter = [round(d, digits = 2) for d in getindex.(layers, 3)], # hide - )) - """, - md"""$(LocalImage("cable_photo.jpg", width = 350, style = "display: block; margin-top: 50px; margin-left: auto; margin-right: auto;")) - """) - -# ╔═╡ 29222f8e-fb07-4bdb-8939-f18e668d2037 -# NominalData() will be used later to verify the calculations - -datasheet_info = NominalData( - designation_code = "NA2XS(FL)2Y", - U0 = 18.0, # Phase-to-ground voltage [kV] - U = 30.0, # Phase-to-phase voltage [kV] - conductor_cross_section = 1000.0, # [mm²] - screen_cross_section = 35.0, # [mm²] - resistance = 0.0291, # DC resistance [Ω/km] - capacitance = 0.39, # Capacitance [μF/km] - inductance = 0.3, # Inductance in trifoil [mH/km] -); - -# ╔═╡ c1595a9d-7882-4b66-a1fc-fe6de19f1ef6 -md""" -### Materials library -""" - -# ╔═╡ c2539b01-ac04-48e4-a973-6a5d8a0e2b58 -# ╠═╡ show_logs = false -# Initialize materials library with default values: -materials = MaterialsLibrary(add_defaults = true) - - -# ╔═╡ 062439db-1e3f-497e-96c1-e1f65f80399b -md""" -## Base RLC quantities -""" - -# ╔═╡ 4e1dec4b-223f-45f8-9393-523fcc4019f0 -md"#### Parameters" - -# ╔═╡ 5b005f4b-605e-4a3d-ba7f-003908f332b2 -md""" -Layers: $(@bind n_layers PlutoUI.Slider(0:10; default=4, show_value=true)) - -Wire diameter [mm]: $(@bind dd_w PlutoUI.Slider(0.45:0.01:11.7; default=4.7, show_value=true)), uncertainty [%]: $(@bind unc_d_w PlutoUI.Slider(0.0:0.01:10; default=0.0, show_value=true)) - -Inner semicon thickness [mm]: $(@bind tt_sc_in PlutoUI.Slider(0.1:0.01:3; default=0.3, show_value=true)), uncertainty [%]: $(@bind unc_t_sc_in PlutoUI.Slider(0.0:0.01:10; default=0.0, show_value=true)) - -Main insulation thickness [mm]: $(@bind tt_ins PlutoUI.Slider(0.1:0.01:30; default=8.0, show_value=true)), uncertainty [%]: $(@bind unc_t_ins PlutoUI.Slider(0.0:0.01:10; default=0.0, show_value=true)) - -Outer semicon thickness [mm]: $(@bind tt_sc_out PlutoUI.Slider(0.1:0.01:3; default=0.6, show_value=true)), uncertainty [%]: $(@bind unc_t_sc_out PlutoUI.Slider(0.0:0.01:10; default=0.0, show_value=true)) -""" - -# ╔═╡ 0b5142ef-2eb0-4c72-8ba5-da776eadb5a3 -begin - core_cc, main_insu = build_geometry(materials; - d_wire_mm = dd_w, d_wire_pct = unc_d_w, - t_sc_in_mm = tt_sc_in, t_sc_in_pct = unc_t_sc_in, - t_ins_mm = tt_ins, t_ins_pct = unc_t_ins, - t_sc_out_mm = tt_sc_out, t_sc_out_pct = unc_t_sc_out, - n_layers = n_layers, - t_sct = t_sct, # keep your existing var for semicon tape thickness (mm) - ) - - - # Build the wire screens on top of the previous layer: - lay_ratio = 10 # typical value for wire screens - screen_con = - ConductorGroup( - CircStrands( - main_insu, - Diameter(d_ws), - num_sc_wires, - lay_ratio, - get(materials, "copper"), - ), - ) - # Add the equalizing copper tape wrapping the wire screen: - add!( - screen_con, - Strip, - Thickness(t_cut), - w_cut, - lay_ratio, - get(materials, "copper"), - ) - - # Water blocking tape over screen: - screen_insu = InsulatorGroup( - Semicon(screen_con, Thickness(t_wbt), get(materials, "polyacrylate")), - ) - - # Group sheath components and assign to design: - sheath_cc = CableComponent("sheath", screen_con, screen_insu) - - - - # Add the aluminum foil (moisture barrier): - jacket_con = ConductorGroup( - Tubular(screen_insu, Thickness(t_alt), get(materials, "aluminum")), - ) - - # PE layer after aluminum foil: - jacket_insu = InsulatorGroup( - Insulator(jacket_con, Thickness(t_pet), get(materials, "pe")), - ) - - # PE jacket (outer mechanical protection): - add!( - jacket_insu, - Insulator, - Thickness(t_jac), - get(materials, "pe"), - ) - - - cable_id = "showcase" - cable_design = CableDesign(cable_id, core_cc; nominal_data = datasheet_info) - add!(cable_design, sheath_cc) - add!(cable_design, "jacket", jacket_con, jacket_insu) - - backend_sym = :cairo - plt, _ = preview(cable_design; size = (800, 500), backend = backend_sym) - plt - -end - -# ╔═╡ e0f87b28-14a3-4630-87db-6f4b51bdb30a -md""" -Cross-section: $(cable_design.components[1].conductor_group.cross_section*1e6) mm² - -Core resistance: $(cable_design.components[1].conductor_group.resistance*1000) Ω/km - -Main insulation capacitance: $(cable_design.components[1].insulator_group.shunt_capacitance*1e9) μF/km - -""" - -# ╔═╡ 1ae76282-4340-4df0-ba29-11712c184a79 -md""" -## Equivalent parameters for EMT studies -""" - -# ╔═╡ 9ddcccbe-86c8-4335-8d65-35af4ce755ab -begin - core_df = DataFrame(cable_design, :baseparams) - core_df -end - -# ╔═╡ 43ff64cb-1226-4d26-9fdf-8aff03505439 -cable_emt = equivalent(cable_design) - -# ╔═╡ ae1749c8-0f6d-4487-8857-12826eb57db3 -begin - plt2, _ = preview(cable_design; size = (800, 500), backend = backend_sym) -end - -# ╔═╡ 3d9239df-523e-40be-b6e9-f0d538638bd8 -begin - plt3, _ = preview(cable_emt; size = (800, 500), backend = backend_sym) - plt3 -end - -# ╔═╡ fd1e268a-6520-4dc8-a9ff-32a4854859df -md""" -# Frequency domain analysis -""" - -# ╔═╡ b4169697-e06d-4947-8105-9f42017f5042 -md""" -## Earth model -""" - -# ╔═╡ 0900d10f-8191-4507-af4e-50d7f4a1126f -begin - # Define a frequency-dependent earth model (1 Hz to 1 MHz): - f = 10.0 .^ range(0, stop = 6, length = 10) # Frequency range - earth_params = EarthModel(f, 100.0, 10.0, 1.0) # 100 Ω·m resistivity, εr=10, μr=1 -end - -# ╔═╡ 4ce85966-0386-4525-8cf2-35e9814f8459 -md""" -### Trifoil arrangement -""" - -# ╔═╡ 44f5823e-4b07-4f2c-8773-e4c3187a6100 -begin - import LineCableModels.Utils: to_nominal - # Define system center point (underground at 1 m depth) and the trifoil positions - x0 = 0.0 - y0 = -1.0 - S = 1e-6+to_nominal(cable_design.components[end].insulator_group.r_ex) - xa, ya, xb, yb, xc, yc = trifoil_formation(x0, y0, S) -end; - -# ╔═╡ 987902c5-5983-4815-b62f-4eabc1be2362 -begin - cablepos = CablePosition(cable_design, xa, ya, - Dict("core" => 1, "sheath" => 0, "jacket" => 0)) - cable_system = LineCableSystem("showcase", 1000.0, cablepos) - add!(cable_system, cable_design, xb, yb, - Dict("core" => 2, "sheath" => 0, "jacket" => 0)) - add!(cable_system, cable_design, xc, yc, - Dict("core" => 3, "sheath" => 0, "jacket" => 0)) -end - -# ╔═╡ 6ee6d16d-326c-4436-a750-077ecc2b3b9c -begin - plt4, _ = preview( - cable_system, - earth_model = earth_params, - zoom_factor = 2.0, - size = (800, 500), - ) - plt4 -end - -# ╔═╡ 39f7460d-8a1e-483d-94f4-14500d6c9ac2 -md""" -## Sequence-component impedances -""" - -# ╔═╡ a2e5d81f-f6a2-4b04-83cc-c95026fd283a -md""" -Layers: $(@bind nn_layers PlutoUI.Slider(0:10; default=4, show_value=true)) - -Wire diameter [mm]: $(@bind ddd_w PlutoUI.Slider(0.45:0.01:11.7; default=4.7, show_value=true)), uncertainty [%]: $(@bind uunc_d_w PlutoUI.Slider(0.0:0.01:10; default=0.0, show_value=true)) - -Inner semicon thickness [mm]: $(@bind ttt_sc_in PlutoUI.Slider(0.1:0.01:3; default=0.3, show_value=true)), uncertainty [%]: $(@bind uunc_t_sc_in PlutoUI.Slider(0.0:0.01:10; default=0.0, show_value=true)) - -Main insulation thickness [mm]: $(@bind ttt_ins PlutoUI.Slider(0.1:0.01:30; default=8.0, show_value=true)), uncertainty [%]: $(@bind uunc_t_ins PlutoUI.Slider(0.0:0.01:10; default=0.0, show_value=true)) - -Outer semicon thickness [mm]: $(@bind ttt_sc_out PlutoUI.Slider(0.1:0.01:3; default=0.6, show_value=true)), uncertainty [%]: $(@bind uunc_t_sc_out PlutoUI.Slider(0.0:0.01:10; default=0.0, show_value=true)) -""" - -# ╔═╡ ce7d068e-2831-49dc-a459-bb68138c3a00 -begin - ccore_cc, mmain_insu = build_geometry(materials; - d_wire_mm = ddd_w, d_wire_pct = uunc_d_w, - t_sc_in_mm = ttt_sc_in, t_sc_in_pct = uunc_t_sc_in, - t_ins_mm = ttt_ins, t_ins_pct = uunc_t_ins, - t_sc_out_mm = ttt_sc_out, t_sc_out_pct = uunc_t_sc_out, - n_layers = nn_layers, - t_sct = t_sct, # keep your existing var for semicon tape thickness (mm) - ) - - - # Build the wire screens on top of the previous layer: - sscreen_con = - ConductorGroup( - CircStrands( - main_insu, - Diameter(d_ws), - num_sc_wires, - lay_ratio, - get(materials, "copper"), - ), - ) - # Add the equalizing copper tape wrapping the wire screen: - add!( - sscreen_con, - Strip, - Thickness(t_cut), - w_cut, - lay_ratio, - get(materials, "copper"), - ) - - # Water blocking tape over screen: - sscreen_insu = InsulatorGroup( - Semicon(sscreen_con, Thickness(t_wbt), get(materials, "polyacrylate")), - ) - - # Group sheath components and assign to design: - ssheath_cc = CableComponent("sheath", sscreen_con, sscreen_insu) - - - - # Add the aluminum foil (moisture barrier): - jjacket_con = ConductorGroup( - Tubular(sscreen_insu, Thickness(t_alt), get(materials, "aluminum")), - ) - - # PE layer after aluminum foil: - jjacket_insu = InsulatorGroup( - Insulator(jjacket_con, Thickness(t_pet), get(materials, "pe")), - ) - - # PE jacket (outer mechanical protection): - add!( - jjacket_insu, - Insulator, - Thickness(t_jac), - get(materials, "pe"), - ) - - - ccable_design = CableDesign(cable_id, ccore_cc; nominal_data = datasheet_info) - add!(ccable_design, ssheath_cc) - add!(ccable_design, "jacket", jjacket_con, jjacket_insu) - - # Define system center point (underground at 1 m depth) and the trifoil positions - SS = 0.1+to_nominal(ccable_design.components[end].insulator_group.r_ex) - xxa, yya, xxb, yyb, xxc, yyc = trifoil_formation(x0, y0, SS) - - ccablepos = CablePosition(ccable_design, xxa, yya, - Dict("core" => 1, "sheath" => 0, "jacket" => 0)) - ccable_system = LineCableSystem("showcase", 1000.0, ccablepos) - add!(ccable_system, ccable_design, xxb, yyb, - Dict("core" => 2, "sheath" => 0, "jacket" => 0)) - add!(ccable_system, ccable_design, xxc, yyc, - Dict("core" => 3, "sheath" => 0, "jacket" => 0)) - -end; - -# ╔═╡ 83d26ac6-24e5-4ca1-817c-921d3c2375c5 -begin - fullfile(filename) = joinpath(@__DIR__, filename); #hide - - problem = LineParametersProblem( - ccable_system, - temperature = 20.0, # Operating temperature - earth_props = earth_params, - frequencies = f, # Frequency for the analysis - ) - - # Define runtime options - opts = ( - force_overwrite = true, # Overwrite existing files - save_path = fullfile("lineparams_output"), # Results directory - verbosity = 0, # Verbosity - ) -end; - -# ╔═╡ cb44ffb8-7e33-4603-a97e-47dbc507f813 -begin - using LineCableModels.Engine - using LineCableModels.Engine.Transforms: Fortescue - using LineCableModels.Engine.FEM - F = FormulationSet(:EMT, - internal_impedance = InternalImpedance.ScaledBessel(), - insulation_impedance = InsulationImpedance.Lossless(), - earth_impedance = EarthImpedance.Papadopoulos(), - insulation_admittance = InsulationAdmittance.Lossless(), - earth_admittance = EarthAdmittance.Papadopoulos(), - modal_transform = Transforms.Fortescue(), - equivalent_earth = EHEM.EnforceLayer(layer = -1), # Use the last layer as effective earth - options = opts, - ) -end; - -# ╔═╡ d20e89c6-b980-4f57-8989-f86d23ea59c6 -begin - using Measurements: Measurement, uncertainty - function rlcg_tables( - lp::LineParameters; - per::Symbol = :km, - diag_only::Bool = true, - elements::Union{Nothing, Vector{Tuple{Int, Int}}} = nothing, - labels::Union{Nothing, Vector{String}} = nothing, - epsval::Real = eps(Float64), - ) - n, _, nf = size(lp.Z) - # frequency vector (preserve potential Measurement) - f = collect(lp.f) - scale = per === :km ? 1_000.0 : 1.0 - elts = if diag_only - [(i, i) for i in 1:n] - else - elements === nothing ? [(i, j) for i in 1:n for j in 1:n] : elements - end - if labels === nothing - if diag_only && n == 3 - labels = ["0", "1", "2"] - else - labels = ["$(i),$(j)" for (i, j) in elts] - end - end - # Zero-clip helper preserving Measurement type - zero_clip(x, τ) = begin - if x isa Measurement - v = value(x) - u = uncertainty(x) - vv = abs(v) < τ ? 0.0 : v - uu = abs(u) < τ ? 0.0 : u - return measurement(vv, uu) - else - return abs(x) < τ ? zero(x) : x - end - end - out = Dict{String, DataFrame}() - @inbounds for (idx, (i, j)) in enumerate(elts) - R = Vector{Any}(undef, nf) - L = Vector{Any}(undef, nf) - G = Vector{Any}(undef, nf) - C = Vector{Any}(undef, nf) - for k in 1:nf - fk = f[k] - ω = 2π * fk - z = lp.Z.values[i, j, k] * scale - y = lp.Y.values[i, j, k] * scale - r = real(z) - g = real(y) - if (fk isa Measurement ? value(fk) == 0 : fk == 0) - l = NaN - c = NaN - else - l = imag(z) / ω - c = imag(y) / ω - end - R[k] = zero_clip(r, epsval) - L[k] = (l isa Number || l isa Measurement) ? zero_clip(l, epsval) : l - G[k] = zero_clip(g, epsval) - C[k] = (c isa Number || c isa Measurement) ? zero_clip(c, epsval) : c - end - tag = labels[idx] - df = DataFrame( - :f_Hz => f, - :R => R, - :L => L, - :C => C, - :G => G, - ) - out[string(tag)] = df - end - return out - end -end - -# ╔═╡ e8117400-adf3-45e3-bf56-59933f01e6d0 -# ╠═╡ show_logs = false -begin - @time ws, p012 = compute!(problem, F); - #Tv, p012 = Fortescue(tol = 1e-5)(p) -end; - -# ╔═╡ cebe81ec-a183-43d7-be36-6627a46de3bf -begin - fig = plot( - p012; - backend = :cairo, - mode = :RLCG, - length_unit = :kilo, - xscale = log10, - per_length = true, - ) - - fig[(:series_impedance, :resistance)].figure - -end - -# ╔═╡ c6cdfb66-1405-4208-808b-12f3e0949ed1 -rlcg_tables(p012) - -# ╔═╡ 2f28cc7a-cb14-44e6-908a-f34b8991c1bd -md""" -# Concluding remarks -""" - -# ╔═╡ fb9cfe06-1a26-443a-9669-615a4e0463b4 -html""" -
-
    -
  • Accurate modeling of the different conductor materials is crucial for the proper representation of line/cable parameters and propagation characteristics.
  • -
  • - Expansion of currently implemented routines to include different earth impedance models, FD soil properties and modal decomposition techniques. -
  • -
  • Construction of additional cable models, detailed investigations on uncertainty quantification.
  • -
  • - Development of novel formulations for cables composed of N concentrical layers, allowing for accurate representations of semiconductor materials. -
  • -
  • - Additional tests and validations using the FEM solver. -
  • -
-
- """ - -# ╔═╡ 7d77f069-930b-4451-ab7d-0e77b8fd86a7 -md""" -# Thank you! -""" - -# ╔═╡ Cell order: -# ╟─426024d7-23c8-4261-9c20-d0045b1ab077 -# ╠═3183ff8d-a9fd-4035-af8a-664bec3606d4 -# ╠═46cfd6fa-b4d6-44c3-83cf-d2b9b1ff1cf1 -# ╠═4462e48f-0d08-4ad9-8dd9-12f4f5912f38 -# ╠═e90baf94-c8b8-41aa-8728-e129f7f6881e -# ╠═532cb61b-97b6-43e7-a8f9-3a5f12b8b3f7 -# ╠═b16ff72c-872a-4505-9468-6cefd4a8852c -# ╠═9fefeafa-63f9-43d0-a2ee-4d4fca170126 -# ╠═fde80e93-1964-4287-acfc-a2da2d4b7d48 -# ╟─77d3c731-bdae-46c2-8b23-eb0b860e7444 -# ╟─9ef6f05f-c384-4fab-bc39-e47edb49f994 -# ╟─f08a32db-05d9-4ddb-9c46-34bc623ce5e7 -# ╟─50384351-fc38-4b29-9bf6-db1556d49dee -# ╟─23913cc6-a81b-4098-bacf-7a2e09998e53 -# ╟─d482241c-4bd5-4896-bdc1-e82387f69051 -# ╟─14f07acc-5353-4b1d-b94f-9ae43f87289b -# ╟─6c6e4d21-cc38-46eb-8178-4cc4a99adcba -# ╟─3e6a9c64-827d-4491-bcac-252ee7b1dc81 -# ╟─877a84cc-979f-48c9-ac41-59be60b4850b -# ╟─db1944b6-c55f-4091-8128-8d297bdc9a74 -# ╟─5397f442-8dc1-42a6-941d-0b1d58057a6b -# ╟─a3f5a8c5-4ab9-4a33-abab-7907ffab1347 -# ╟─382252ca-ede1-4043-b921-7834e59810cb -# ╟─96121e5b-6b5b-4ab1-81d0-6dcbe924cda2 -# ╟─a8ea0da0-36f1-44d4-9415-d3041f34c23f -# ╟─f5fa7e28-97a7-456b-87a9-5ac4b76be9d4 -# ╟─8c2eaef0-4e01-41b9-b1a6-a20dfa9b2d57 -# ╟─cb8f01ae-26e0-44ce-8347-298ab692ac63 -# ╟─29222f8e-fb07-4bdb-8939-f18e668d2037 -# ╟─c1595a9d-7882-4b66-a1fc-fe6de19f1ef6 -# ╠═c2539b01-ac04-48e4-a973-6a5d8a0e2b58 -# ╟─062439db-1e3f-497e-96c1-e1f65f80399b -# ╟─4e1dec4b-223f-45f8-9393-523fcc4019f0 -# ╟─5b005f4b-605e-4a3d-ba7f-003908f332b2 -# ╟─e0f87b28-14a3-4630-87db-6f4b51bdb30a -# ╟─b081c88a-7959-44ea-85ff-33b980ec71b4 -# ╟─0b5142ef-2eb0-4c72-8ba5-da776eadb5a3 -# ╠═1ae76282-4340-4df0-ba29-11712c184a79 -# ╠═9ddcccbe-86c8-4335-8d65-35af4ce755ab -# ╠═43ff64cb-1226-4d26-9fdf-8aff03505439 -# ╠═ae1749c8-0f6d-4487-8857-12826eb57db3 -# ╠═3d9239df-523e-40be-b6e9-f0d538638bd8 -# ╠═fd1e268a-6520-4dc8-a9ff-32a4854859df -# ╠═b4169697-e06d-4947-8105-9f42017f5042 -# ╠═0900d10f-8191-4507-af4e-50d7f4a1126f -# ╟─4ce85966-0386-4525-8cf2-35e9814f8459 -# ╠═44f5823e-4b07-4f2c-8773-e4c3187a6100 -# ╠═987902c5-5983-4815-b62f-4eabc1be2362 -# ╠═6ee6d16d-326c-4436-a750-077ecc2b3b9c -# ╠═39f7460d-8a1e-483d-94f4-14500d6c9ac2 -# ╟─a2e5d81f-f6a2-4b04-83cc-c95026fd283a -# ╠═cebe81ec-a183-43d7-be36-6627a46de3bf -# ╠═ce7d068e-2831-49dc-a459-bb68138c3a00 -# ╠═83d26ac6-24e5-4ca1-817c-921d3c2375c5 -# ╠═cb44ffb8-7e33-4603-a97e-47dbc507f813 -# ╠═e8117400-adf3-45e3-bf56-59933f01e6d0 -# ╠═d20e89c6-b980-4f57-8989-f86d23ea59c6 -# ╠═c6cdfb66-1405-4208-808b-12f3e0949ed1 -# ╟─2f28cc7a-cb14-44e6-908a-f34b8991c1bd -# ╟─fb9cfe06-1a26-443a-9669-615a4e0463b4 -# ╟─7d77f069-930b-4451-ab7d-0e77b8fd86a7 diff --git a/showcase/start.jl b/showcase/start.jl deleted file mode 100644 index 2ec261a66..000000000 --- a/showcase/start.jl +++ /dev/null @@ -1,26 +0,0 @@ -# This script launches Pluto and opens a specific notebook file. -using Pluto -# using PlutoStyles - -# Define the path to the notebook file. -# notebook_path = "/home/amartins/Documents/KUL/LineCableModels/showcase/showcase1.jl" - -# println("Starting Pluto and opening notebook: ", notebook_path) -println("Starting Pluto, check your browser...") - -try - # Run Pluto, telling it which notebook to open - Pluto.run(launch_browser = true) - - # Keep the script alive so the Pluto server doesn't shut down immediately. - println("\nPluto server is running. Press Ctrl+C in this terminal to stop.") - wait(Condition()) # Waits indefinitely until interrupted (Ctrl+C) - - println("\nAn error occurred while trying to run Pluto:") - showerror(stdout, e) - Base.show_backtrace(stdout, catch_backtrace()) -finally - println("\nPluto server stopped.") -end - -println("Launcher script finished.") diff --git a/src/LineCableModels.jl b/src/LineCableModels.jl index 3a3dae8fc..8d9a0fa85 100644 --- a/src/LineCableModels.jl +++ b/src/LineCableModels.jl @@ -1,91 +1,231 @@ +""" + LineCableModels + +Calculate electrical parameters for overhead and underground cable systems. + +The public API constructs materialized or finite parametric cable models, +evaluates cable constants and line-parameter matrices with selected numerical formulations. It propagates declared uncertainty and provides plotting methods when Makie is loaded. +""" module LineCableModels ## Public API # ------------------------------------------------------------------------- # Core generics: -export add!, set_verbosity!, set_backend! - -# Materials: -export Material, MaterialsLibrary - -# Data model (design + system): -export Thickness, Diameter, WireArray, Strip, Tubular, Semicon, Insulator, Sector, SectorParams, SectorInsulator -export ConductorGroup, InsulatorGroup -export CableComponent, CableDesign, NominalData -export CablesLibrary -export CablePosition, LineCableSystem -export trifoil_formation, flat_formation, preview, equivalent, MaxFill - -# Earth properties: -export EarthModel +export add!, build, homogenize, validate, description, constitutive +export formula, formula_id +export AbstractProblemDefinition, AbstractFormulation, AbstractProblemResult +export AbstractCoreResult, AbstractResultSpace +export AbstractParametricResult, AbstractUncertaintyResult +export FormulationOptions, ComputationOptions, ComputationDetails +export formulation_options, computation_options, computation_details, details +export compute, observe, @observe, observables +export ObservedResult, kron_reduce +export quantity, native_unit, display_unit, scale_factor, label, symbol +export basis, line_length, domain, frequencies, nconductors, nfrequencies, ncables, nphases +export Z, Y, R, X, L, G, B, C +export series_impedance, shunt_admittance, + resistance, reactance, inductance, + conductance, susceptance, capacitance +# High-level modelling grammar: +export Grid, AbsoluteError, DeterministicGrid, RelativeGrid, AbsoluteGrid +export AbstractGrid, AbstractUncertainGrid, UncertainValue +export Gridspace +export has_uncertainty, nominal, uncertainty +export @gridspace +export Combinatorial, LinearError, MonteCarlo, ParametricProblem +export ParametricResult, LinearErrorResult, MonteCarloResult +export SampleSummary, HistogramDensity +export statistics, samples, histograms, uncertain +export root_seed, point_seed, trial_count +export confidence, cdf_tolerance, sampling_distribution +export report, TableReportDefinition, XLSXReportDefinition, ReportArtifact +export AbstractMaterial, Material, RadialDielectric, MaterialsLibrary +export AbstractShape, AbstractPrimitive +export Disk, Rectangle, Ellipse, Sector, Annulus, Polygon, Shell +export Pose2 +export EmptyBoundary, resolve, boundary, area, perimeter, centroid, support, tessellate +export r_in, r_ex, thickness, outer_radius +export AbstractCablePart, Region, Stack +export Group, Assembly +export Enclosure +export Ring, Polar, Fill, Lattice, placements +export capacity, FillFactor +export LayRatio, Pitch, LayAngle, Helix, pitch, angle, overlength +export at, trefoil, hflat, vflat, layer, homogeneous +export AbstractEarthModel, AbstractEarthLayer, AbstractEarthMaterial, EarthMaterial, + EarthLayer, EarthModel +export terminal, core, stranded, milliken, rope, cores, tape, insulation, screen, sheath +export armor, bedding, jacket, filler, pipe, duct +export solid, shell, wires, layers, assembly +export @cable, @system, @earth, @terminal, @assembly, @pipe, @duct +export @at, @hflat, @vflat, @trefoil +export @distribute +export estimate_stranding, estimate_screen, WireEstimate + +public Gridpoint + +# Materialized results, reusable designs, and presentation: +export CableDesign, LineCableSystem, DatasheetInfo, datasheet +export CableGeometry, PlacedRegion +export CableConstants, CableConstantsProblem, CableConstantsFormulation, + LineParametersProblem, LineParameters, CablesLibrary +export preview, show_material_scale # Engine: -export LineParametersProblem, - FormulationSet, compute!, SeriesImpedance, ShuntAdmittance, per_km, per_m, kronify, - LineParameters, PhaseDomain, ModalDomain - -# Parametric builder: -# export make_stranded, make_screened -# export conductor, insulator +export Formulation, LineParametersFormulation, CableConstantsFormulation, + LineCableModelsCoaxial, + LineCableModelsFEM, LineCableModelsFEMError, BoundarySolveError, + SeriesImpedance, ShuntAdmittance, + LineParameters, PhaseDomain, ModalDomain +export ModalAnalysisProblem, ModalAnalysisFormulation, + LineCableModelsModal, ModalOperators, operators, Tv, Ti, gamma, alpha, beta, velocity, Zc, Yc, + PropagationParameters, H, transform +export ModalAnalysis # Import/Export: -export export_data, save, load! +export export_data, import_data, save, load! # ------------------------------------------------------------------------- import DocStringExtensions: DocStringExtensions +using DocStringExtensions: SIGNATURES, TYPEDSIGNATURES, TYPEDEF, TYPEDFIELDS +using Random +import Logging -# Submodule `Commons` -include("commons/Commons.jl") -using .Commons: IMPORTS, EXPORTS, add!, PhaseDomain, ModalDomain, domain -# Submodule `UncertainBessels` -include("uncertainbessels/UncertainBessels.jl") - -# Submodule `Utils` -include("utils/Utils.jl") -using .Utils: set_verbosity! +include("docstrings.jl") +include("interfaces.jl") -# Submodule `UnitHandler` -include("unithandler/UnitHandler.jl") +public FormulaDefinition, Expression, Functor -# Submodule `Validation` -include("validation/Validation.jl") +# Submodule `Units` +include("units/Units.jl") +using .Units: quantity, native_unit, display_unit, scale_factor, label, symbol -# Submodule `PlotBuilder` +# Package-local shared computation grammar. +include("commons/Commons.jl") +using .Commons: + AbstractProblemDefinition, AbstractFormulation, AbstractProblemResult, + AbstractCoreResult, AbstractResultSpace, + AbstractParametricResult, AbstractUncertaintyResult, + FormulationOptions, ComputationOptions, ComputationDetails, + formulation_options, computation_options, computation_details, details, + observe, @observe, observables, ObservedResult, kron_reduce +import .Commons: compute, validate +using .Commons: FormulaDefinition, Expression, Functor +include("logging.jl") +include("formulas.jl") + +# Bounded text formatting consumes the completed shared declaration grammar. +include("textdisplay/TextDisplay.jl") +import .Commons: nominal, uncertainty + +# Root-owned finite parameter grammar. These primitives are loaded before the +# domain modules so every constructor enters the same scalar-or-Gridspace path +# without a late-loaded bridge through ParametricBuilder. +include("grid.jl") +include("gridspace.jl") + +public parameterize, materialize, sample_uncertainty + +# Thin native plotting handles and optional-extension entry points. include("plotbuilder/PlotBuilder.jl") -using .PlotBuilder.BackendHandler: set_backend! +using .PlotBuilder: + UIPlot, plot, preview, show_material_scale, export_svg, + figurelegend!, panellegend!, figuretitle!, paneltitle!, + figurecolorbars!, axisscale!, resetview!, addwidget!, removewidget!, + plotwindow, materialcolors, materialscale! +export UIPlot, export_svg, figurelegend!, panellegend!, figuretitle!, paneltitle! +export figurecolorbars!, axisscale!, resetview!, addwidget!, removewidget! +export materialcolors, materialscale! +public PlotBuilder, plot, plotwindow # Submodule `Materials` include("materials/Materials.jl") -using .Materials: Material, MaterialsLibrary +import .Materials +using .Materials: AbstractMaterial, Material, RadialDielectric, MaterialsLibrary -# Submodule `EarthProps` -include("earthprops/EarthProps.jl") -using .EarthProps: EarthModel +# Submodule `Earth` +include("earth/Earth.jl") +import .Earth +public Earth +using .Earth: AbstractEarthModel, AbstractEarthLayer, AbstractEarthMaterial, + EarthMaterial, EarthLayer, EarthModel, layer, homogeneous # Submodule `DataModel` include("datamodel/DataModel.jl") -using .DataModel: Thickness, Diameter, CircStrands, RectStrands, Strip, Tubular, Semicon, - Insulator, ConductorGroup, InsulatorGroup, CableComponent, CableDesign, NominalData, - CablesLibrary, CablePosition, LineCableSystem, trifoil_formation, flat_formation, - preview, equivalent, MaxFill, Sector, SectorParams, SectorInsulator +using .DataModel: CableDesign, CableGeometry, PlacedRegion, + LineCableSystem, DatasheetInfo, datasheet, + CablesLibrary, ncables, nphases, + AbstractShape, AbstractPrimitive, + Disk, Rectangle, Ellipse, Sector, Annulus, Polygon, Shell, + Pose2, + EmptyBoundary, resolve, boundary, area, perimeter, centroid, support, + tessellate, + r_in, r_ex, thickness, outer_radius, + AbstractCablePart, Region, Stack, + Group, Assembly, Enclosure +using .DataModel: Ring, Polar, Fill, Lattice, capacity, placements, + FillFactor, + LayRatio, Pitch, LayAngle, Helix, pitch, angle, overlength # Submodule `Engine` include("engine/Engine.jl") -using .Engine: LineParametersProblem, compute!, LineParameters, SeriesImpedance, - ShuntAdmittance, per_km, per_m, kronify, FormulationSet +using .Engine: LineParameters, LineParametersProblem, CableConstants, + CableConstantsProblem, CableConstantsFormulation, SeriesImpedance, + ShuntAdmittance, Formulation, + LineParametersFormulation, LineCableModelsCoaxial, + LineCableModelsFEM, + LineCableModelsFEMError, BoundarySolveError, + domain, frequencies, nconductors, nfrequencies, + Z, Y, X, G, B, series_impedance, shunt_admittance, + reactance, conductance, susceptance, + LineParamsDomain, PhaseDomain, ModalDomain + +public LineParamsDomain + +# Submodule `Engine.ModalAnalysis` +using .Engine: ModalAnalysis +using .Engine.ModalAnalysis: ModalAnalysisProblem, ModalAnalysisFormulation, + LineCableModelsModal, ModalOperators, operators, + Tv, Ti, gamma, alpha, beta, velocity, Zc, Yc, PropagationParameters, H, transform # Submodule `ParametricBuilder` include("parametricbuilder/ParametricBuilder.jl") +using .ParametricBuilder: + @gridspace, + Combinatorial, ParametricProblem, ParametricResult, + terminal, core, stranded, milliken, rope, cores, tape, + insulation, screen, sheath, armor, bedding, jacket, + filler, pipe, duct, solid, shell, wires, layers, + assembly, + at, trefoil, hflat, vflat, + WireEstimate, estimate_stranding, estimate_screen +using .ParametricBuilder: @cable, @system, @earth, @terminal, @assembly, @pipe, + @duct, @at, @hflat, @vflat, @trefoil, @distribute # Submodule `UQ` include("uq/UQ.jl") +using .UQ: + LinearError, MonteCarlo, LinearErrorResult, MonteCarloResult, + SampleSummary, HistogramDensity, + statistics, samples, histograms, uncertain, + root_seed, point_seed, trial_count, + confidence, cdf_tolerance, sampling_distribution + +# Completed-result measurement projections. +include("performance.jl") +public benchmark + +# Submodule `ReportBuilder` +include("reportbuilder/ReportBuilder.jl") +using .ReportBuilder: + report, TableReportDefinition, XLSXReportDefinition, ReportArtifact # Submodule `ImportExport` include("importexport/ImportExport.jl") -using .ImportExport: export_data, load!, save +using .ImportExport: export_data, import_data, load!, save -# Aliases for backward compatibility -const WireArray = CircStrands # alias for now -export WireArray # export aliases +# External-tool integration. Native execution is deferred until compute. +include("pscad/PSCAD.jl") +export PSCAD -end \ No newline at end of file +end diff --git a/src/cablebuilder/CableBuilder.jl b/src/cablebuilder/CableBuilder.jl deleted file mode 100644 index b1d0834aa..000000000 --- a/src/cablebuilder/CableBuilder.jl +++ /dev/null @@ -1,103 +0,0 @@ -module CableBuilder - -include("grid.jl") -include("gridspace.jl") -include("macros.jl") - -include("materials.jl") - - -include("../commons/Commons.jl") -include("../uncertainbessels/UncertainBessels.jl" -) -include("../utils/Utils.jl") - -include("types.jl") -include("primitives.jl") -include("partbuilder.jl") -include("shapes.jl") -include("cabledesign.jl") - - -export Material, CableDesign, PartGroup -export Grid, AbsoluteError - -# ========================================== -# THE FRONTEND API (Compilation boundary) -# ========================================== -export Conductor, Insulator, Group - -@inline function Group(layers::Tuple; origin = (0.0, 0.0), n = 1, m = 1) - return Builder(PartGroup, origin, n, m, layers) -end - -module Conductor - import ..CableBuilder: ConductorPart, Builder - import ..CableBuilder: Circular, Rectangular, Annular - import ..CableBuilder: SolidCore, TubularLayer - import ..CableBuilder: Grid - import ..CableBuilder: Material - - @inline function Solid(cmp::Symbol, mat; r) - # The semicolon triggers the @gridspace kwarg interceptor. - # If `r` is a Grid, this returns a Gridspace{Circular}. - # If `r` is a Real, it returns a concrete Circular. - params = Circular(; r = r) - - return Builder(ConductorPart, SolidCore, cmp, mat, params) - end - - @inline function Tubular(cmp::Symbol, mat; t) - params = Annular(; t = t) - - return Builder(ConductorPart, TubularLayer, cmp, mat, params) - end - - -# @inline function Pipe(cmp::Symbol, mat; t, filler, offset = 0.0) -# inner = Tubular(cmp, mat; t = t) # tubular wall -# return EnclosureSpec(ConductorPart, inner, filler; offset = offset) -# end - -# @inline function Stranded( -# cmp::Symbol, -# mat; -# pattern::Symbol = :layer, -# r_w, -# n_w, -# lay_r, -# lay_d = 1, -# ) -# mat_spec = convert(AbstractSpec{Material}, mat) -# return CircStrandedSpec( -# ConductorPart, -# Grid(cmp), -# Grid(r_w), -# Grid(n_w), -# Grid(lay_r), -# Grid(lay_d), -# mat_spec, -# ) -# end - -end - -module Insulator - import ..CableBuilder: InsulatorPart, Builder - import ..CableBuilder: Circular, Rectangular, Annular - import ..CableBuilder: TubularLayer - - import ..CableBuilder: Grid - import ..CableBuilder: Material - - # Insulators don't usually have solid cores, but the logic holds! - @inline function Tubular(cmp::Symbol, mat; t) - params = Annular(; t = t) - return Builder(InsulatorPart, TubularLayer, cmp, mat, params) - end - -end - - - -end # module diff --git a/src/cablebuilder/cabledesign.jl b/src/cablebuilder/cabledesign.jl deleted file mode 100644 index 81527814e..000000000 --- a/src/cablebuilder/cabledesign.jl +++ /dev/null @@ -1,65 +0,0 @@ -# --------------------------------------------------------- -# The Concrete Target -# --------------------------------------------------------- -struct CableDesign{T <: Tuple} - payload::T -end - -# --------------------------------------------------------- -# The Allocation-Free Stacking Engine -# --------------------------------------------------------- -@inline build_design(bound::AbstractShapeParams, ::Tuple{}) = () - -@inline function build_design(bound::AbstractShapeParams, builders::Tuple) - b = first(builders) - - # The builder receives the absolute geometric primitive - target = b(bound) - - # Extract the new bounding primitive for the next layer - next_bound = boundary(target) - - return (target, build_design(next_bound, Base.tail(builders))...) -end - -# --------------------------------------------------------- -# The Constructor (Hit by the Gridspace Generator) -# --------------------------------------------------------- -# The generator splats the unrolled PartBuilders here. -@inline function CableDesign(builders...) - parts = build_design(Circular(0.0), builders) - return CableDesign{typeof(parts)}(parts) -end - - -# --------------------------------------------------------- -# The DSL Hook (Intent Capture & Auto-Grouping) -# --------------------------------------------------------- -# The user provided explicit topological groups. All good. -@inline CableDesign(layers::Tuple{Vararg{<:Gridspace{GroupBuilder}}}) = - Gridspace{CableDesign}(layers) - -# The user provided naked 1D physics parts. Default-group them. -@inline function CableDesign(layers::Tuple{Vararg{<:Gridspace{PartBuilder}}}) - grids = ( - Grid(Val{PartGroup}()), - Grid(((0.0, 0.0),)), # Default origin at center - Grid(1), # Default n - Grid(1), # Default m - layers..., # Splat the naked part spaces - ) - - default_group = Gridspace{GroupBuilder}(grids) - return Gridspace{CableDesign}((default_group,)) -end - -# Mixed garbage. -@inline function CableDesign(layers::Tuple) - throw( - ArgumentError( - "Topological violation: You cannot mix raw parts and explicit Groups " * - "at the top level of CableDesign. Either wrap everything in Group() " * - "or pass raw parts exclusively.", - ), - ) -end diff --git a/src/cablebuilder/enclosure.jl b/src/cablebuilder/enclosure.jl deleted file mode 100644 index 907a661e7..000000000 --- a/src/cablebuilder/enclosure.jl +++ /dev/null @@ -1,69 +0,0 @@ -struct Enclosure{T <: Real, S <: AbstractShape{T}} <: AbstractShape{T} - base_shape::S - filler_material::Material{T} -end - -function Enclosure( - base_shape::AbstractShape{T_shape}, - filler::Material{T_mat}, -) where {T_shape <: Real, T_mat <: Real} - - T = promote_type(T_shape, T_mat) - - s = convert(AbstractShape{T}, base_shape) # must return concrete - f = convert(Material{T}, filler) - - return Enclosure{T, typeof(s)}(s, f) -end - -function Base.convert(::Type{<:AbstractShape{T}}, e::Enclosure) where {T <: Real} - # Recursively upgrade the payload - s_converted = convert(AbstractShape{T}, e.base_shape) - f_converted = convert(Material{T}, e.filler_material) - - # Lock them in a new Vault - return Enclosure{T, typeof(s_converted)}(s_converted, f_converted) -end - -# Override the global accessors because Enclosure is a diva -r_in(e::Enclosure) = r_in(e.base_shape) -r_ex(e::Enclosure) = r_ex(e.base_shape) - -struct EnclosureBuilder{P, S, O, F} - inner::S - offset::O - filler::Material{F} -end - -@inline function EnclosureBuilder{P}( - inner::S, - offset::O, - filler::Material{F}, -) where {P, S, O, F} - return EnclosureBuilder{P, S, O, F}(inner, offset, filler) -end - -@inline function (b::EnclosureBuilder{P})(current_r::T) where {P, T <: Real} - r0 = current_r + b.offset - part = b.inner(r0) - newshape = Enclosure(part.shape, b.filler) - return P(part.cmp, newshape, part.material) -end - -struct EnclosureSpec{P, S, O, F} <: AbstractSpec{EnclosureBuilder{P}} - inner::S - offset::O - filler::F -end - -# # Make this diva explicit about what is iterable, and what is not. -# @inline grid_args(spec::EnclosureSpec) = (spec.inner, spec.offset, spec.filler) - -@inline function EnclosureSpec(::Type{P}, inner::S, offset::O, filler::F) where {P, S, O, F} - return EnclosureSpec{P, S, O, F}(inner, offset, filler) -end - -@inline function EnclosureSpec(::Type{P}, inner_spec, filler; offset = 0.0) where {P} - filler_spec = convert(AbstractSpec{Material}, filler) - return EnclosureSpec(P, inner_spec, Grid(offset), filler_spec) -end diff --git a/src/cablebuilder/grid.jl b/src/cablebuilder/grid.jl deleted file mode 100644 index 81590db06..000000000 --- a/src/cablebuilder/grid.jl +++ /dev/null @@ -1,134 +0,0 @@ -import Base: iterate, length, eltype, extrema -using Measurements - -# --------------------------------------------------------- -# The Vaults (Strictly type-constrained to Tuples) -# --------------------------------------------------------- -struct DeterministicGrid{V <: Tuple} - vals::V -end - -struct RelativeGrid{V <: Tuple, P <: Tuple} - vals::V - rel_err::P -end - -struct AbsoluteGrid{V <: Tuple, P <: Tuple} - vals::V - abs_err::P -end - -# The explicit tag for absolute standard deviations. -# If someone asks what this does, fire them. -struct AbsoluteError{T <: Tuple} - vals::T -end - -AbsoluteError(x::AbstractArray) = AbsoluteError(Tuple(x)) -AbsoluteError(x::Real) = AbsoluteError((x,)) - - -# --------------------------------------------------------- -# Surface API: The Formal Normalization Grammar -# Tuples and Arrays are collections. Everything else is a scalar. -# --------------------------------------------------------- - -# --- 1. Deterministic --- -Grid(v::Tuple) = DeterministicGrid(v) -Grid(v::AbstractArray) = DeterministicGrid(Tuple(v)) -Grid(v::Any) = DeterministicGrid((v,)) - -# --- 2. Relative (v, p) --- -Grid(v::Tuple, p::Tuple) = RelativeGrid(v, p) -Grid(v::Tuple, p::AbstractArray) = RelativeGrid(v, Tuple(p)) -Grid(v::Tuple, p::Any) = RelativeGrid(v, (p,)) - -Grid(v::AbstractArray, p::Tuple) = RelativeGrid(Tuple(v), p) -Grid(v::AbstractArray, p::AbstractArray) = RelativeGrid(Tuple(v), Tuple(p)) -Grid(v::AbstractArray, p::Any) = RelativeGrid(Tuple(v), (p,)) - -Grid(v::Any, p::Tuple) = RelativeGrid((v,), p) -Grid(v::Any, p::AbstractArray) = RelativeGrid((v,), Tuple(p)) -Grid(v::Any, p::Any) = RelativeGrid((v,), (p,)) - -# --- 3. Absolute (v, a) --- -Grid(v::Tuple, a::AbsoluteError) = AbsoluteGrid(v, a.vals) -Grid(v::AbstractArray, a::AbsoluteError) = AbsoluteGrid(Tuple(v), a.vals) -Grid(v::Any, a::AbsoluteError) = AbsoluteGrid((v,), a.vals) - -# Pass-through for already built vaults -Grid(g::Union{DeterministicGrid, RelativeGrid, AbsoluteGrid}) = g - -# --------------------------------------------------------- -# The Iteration Protocol (Measurements Gangbang) -# --------------------------------------------------------- - -# Deterministic -@inline Base.iterate(g::DeterministicGrid, state...) = iterate(g.vals, state...) -@inline Base.length(g::DeterministicGrid) = length(g.vals) -Base.eltype(::Type{<:DeterministicGrid{V}}) where {V} = eltype(V) - -# Relative -@inline function Base.iterate(g::RelativeGrid, state...) - res = iterate(Iterators.product(g.vals, g.rel_err), state...) - res === nothing && return nothing - ((v, p), next_state) = res - return measurement(v, abs(v) * (p / 100.0)), next_state -end -@inline Base.length(g::RelativeGrid) = length(g.vals) * length(g.rel_err) -Base.eltype(::Type{<:RelativeGrid{V, P}}) where {V, P} = - Measurement{promote_type(eltype(V), eltype(P))} - -# Absolute -@inline function Base.iterate(g::AbsoluteGrid, state...) - res = iterate(Iterators.product(g.vals, g.abs_err), state...) - res === nothing && return nothing - ((v, err), next_state) = res - return measurement(v, abs(err)), next_state -end -@inline Base.length(g::AbsoluteGrid) = length(g.vals) * length(g.abs_err) -Base.eltype(::Type{<:AbsoluteGrid{V, P}}) where {V, P} = - Measurement{promote_type(eltype(V), eltype(P))} - -# --------------------------------------------------------- -# Boundaries (For Lemonparty Solvers) -# --------------------------------------------------------- -@inline Base.extrema(g::DeterministicGrid) = (minimum(g.vals), maximum(g.vals)) - -@inline function Base.extrema(g::RelativeGrid) - v_min, v_max = minimum(g.vals), maximum(g.vals) - p_max = maximum(abs, g.rel_err) / 100.0 - return (v_min * (1.0 - p_max), v_max * (1.0 + p_max)) -end - -@inline function Base.extrema(g::AbsoluteGrid) - v_min, v_max = minimum(g.vals), maximum(g.vals) - err_max = maximum(abs, g.abs_err) - return (v_min - err_max, v_max + err_max) -end - -# --------------------------------------------------------- -# The Stochastic Sampler -# --------------------------------------------------------- -import Base: rand -using Distributions - -@inline Base.rand(g::DeterministicGrid, ::Type{D}) where {D} = rand(g.vals) - -@inline function Base.rand(g::RelativeGrid, ::Type{D}) where {D} - v, p = rand(g.vals), rand(g.rel_err) - σ = abs(v) * (p / 100.0) - σ == 0 && return float(v) - return D <: Normal ? rand(Normal(v, σ)) : rand(Uniform(v - √3*σ, v + √3*σ)) -end - -@inline function Base.rand(g::AbsoluteGrid, ::Type{D}) where {D} - v, σ = rand(g.vals), rand(g.abs_err) - σ == 0 && return float(v) - return D <: Normal ? rand(Normal(v, σ)) : rand(Uniform(v - √3*σ, v + √3*σ)) -end - -@inline Base.rand( - g::Union{DeterministicGrid, RelativeGrid, AbsoluteGrid}; - dist::Type{D} = Normal, -) where {D} = rand(g, D) diff --git a/src/cablebuilder/gridspace.jl b/src/cablebuilder/gridspace.jl deleted file mode 100644 index 26ced1bcc..000000000 --- a/src/cablebuilder/gridspace.jl +++ /dev/null @@ -1,67 +0,0 @@ -# ============================================================================== -# THE GRIDSPACE ENGINE (Replaces AbstractSpec) -# ============================================================================== - -# The Universal Staging Area. -# Target is the strict `<: Real` physics struct. Args is the tuple of Grids/Scalars. -struct Gridspace{Target, Args <: Tuple} - grids::Args -end - -Gridspace{Target}(grids::Args) where {Target, Args <: Tuple} = - Gridspace{Target, Args}(grids) - -Grid(g::Gridspace) = g - -# ============================================================================== -# THE THIN-ALLOCATION ITERATOR PROTOCOL -# ============================================================================== - -# 1. The Initializer -@inline function Base.iterate(g::Gridspace{Target}) where {Target} - # Base.Iterators.ProductIterator consumes the tuple directly, preventing the SROA leak. - iter = Base.Iterators.ProductIterator(g.grids) - next = iterate(iter) - - next === nothing && return nothing - - args, state = next - # Native splat into Target. Completely type-stable. - return Target(args...), state -end - -# 2. The Advancer -@inline function Base.iterate(g::Gridspace{Target}, state) where {Target} - iter = Base.Iterators.ProductIterator(g.grids) - next = iterate(iter, state) - - next === nothing && return nothing - - args, new_state = next - return Target(args...), new_state -end - -# 3. Utilities (So the compiler knows exactly how big the loop is) -Base.IteratorSize(::Type{<:Gridspace}) = Base.HasShape{1}() -Base.length(g::Gridspace) = prod(length, g.grids) -Base.size(g::Gridspace) = (length(g),) - -# --------------------------------------------------------- -# The Stochastic Sampler -# --------------------------------------------------------- -import Base: rand -using Distributions - -@inline function Base.rand( - g::Gridspace{Target}, - ::Type{D}, -) where {Target, D <: ContinuousUnivariateDistribution} - # Map distributes your existing grid.jl rand() over the tuple - samples = map(grid -> rand(grid, D), g.grids) - return Target(samples...) -end - -@inline Base.rand( - g::Gridspace; - dist::Type{D} = Normal, -) where {D <: ContinuousUnivariateDistribution} = rand(g, dist) \ No newline at end of file diff --git a/src/cablebuilder/helical.jl b/src/cablebuilder/helical.jl deleted file mode 100644 index 096767e79..000000000 --- a/src/cablebuilder/helical.jl +++ /dev/null @@ -1,40 +0,0 @@ -# ========================================== -# 1. THE VAULT (Fully resolved analytical geometry) -# ========================================== -struct HelicalPath{T <: Real, U <: Integer} - ratio::T - pitch::T - angle::T - overlength::T - dir::U -end - -function HelicalPath(ratio, pitch, angle, overlength, dir::U) where {U <: Integer} - T = promote_type(typeof(ratio), typeof(pitch), typeof(angle), typeof(overlength)) - return HelicalPath{T, U}( - convert(T, ratio), convert(T, pitch), convert(T, angle), convert(T, overlength), dir, - ) -end - -# ========================================== -# 2. THE BUILDERS (Materialize the math using mean_diam) -# ========================================== -struct LayRatioBuilder{T <: Real, U <: Integer} - ratio::T - dir::U -end - -@inline function (b::LayRatioBuilder)(mean_diam::T) where {T <: Real} - pitch = b.ratio * mean_diam - angle = atan(pi * mean_diam / pitch) - overlength = sqrt(1 + (pi * mean_diam / pitch)^2) - return HelicalPath(b.ratio, pitch, angle, overlength, b.dir) -end - -# ========================================== -# 3. THE BLUEPRINTS (Specs for Combinatorics) -# ========================================== -struct LayRatioSpec{T, U} <: AbstractSpec{LayRatioBuilder} - ratio::T - dir::U -end \ No newline at end of file diff --git a/src/cablebuilder/macros.jl b/src/cablebuilder/macros.jl deleted file mode 100644 index c5ea1debb..000000000 --- a/src/cablebuilder/macros.jl +++ /dev/null @@ -1,226 +0,0 @@ -# ============================================================================== -# THE AST JANITOR -# Drills through blocks, escapes, and un-evaluated macrocalls to find and replace -# the struct definition, ensuring multiple macros stack flawlessly. -# ============================================================================== - -# Recursively strips out :escape nodes. -function _strip_escapes(ex) - if ex isa Expr && ex.head === :escape - return _strip_escapes(ex.args[1]) - elseif ex isa Expr - return Expr(ex.head, map(_strip_escapes, ex.args)...) - else - return ex - end -end - -# Drills into blocks and macrocalls to find the actual :struct node. -function _get_struct_node(ex) - ex isa Expr || return nothing - ex.head === :struct && return ex - for arg in ex.args - node = _get_struct_node(arg) - node !== nothing && return node - end - return nothing -end - -# Replaces the old struct node with the new one, leaving macrocalls intact. -function _replace_struct(ex, new_struct) - if ex isa Expr && ex.head === :struct - return new_struct - elseif ex isa Expr - return Expr(ex.head, map(arg -> _replace_struct(arg, new_struct), ex.args)...) - else - return ex - end -end - -# Universal field parser. Handles `field::T = default` for BOTH macros. -function _parse_fields(struct_body) - fields = Any[] - clean_body_args = Any[] - - for arg in struct_body.args - if arg isa LineNumberNode || arg isa String - push!(clean_body_args, arg) - continue - end - - has_default = false - local field_name, default_val, clean_field - - # Detect `field::T = default` - if arg isa Expr && arg.head === :(=) - has_default = true - clean_field = arg.args[1] - default_val = arg.args[2] - else - clean_field = arg - end - - if clean_field isa Expr && clean_field.head === :(::) - field_name = clean_field.args[1] - push!(clean_body_args, clean_field) - elseif clean_field isa Symbol - field_name = clean_field - push!(clean_body_args, clean_field) - else - # Inner constructors / garbage passthrough - push!(clean_body_args, arg) - continue - end - - push!( - fields, - ( - name = field_name, - has_default = has_default, - default_val = has_default ? default_val : nothing, - ), - ) - end - return fields, clean_body_args -end - -# Rebuilds the AST block natively around whatever macrocalls are left. -function _rebuild_ast(ex, new_struct, new_funcs) - replaced = _replace_struct(ex, new_struct) - if replaced isa Expr && replaced.head === :block - return Expr(:block, replaced.args..., new_funcs...) - else - return Expr(:block, replaced, new_funcs...) - end -end - -function _extract_struct_name(struct_node) - sig = struct_node.args[2] - if sig isa Symbol - return sig - elseif sig.head === :(<:) - left = sig.args[1] - return left isa Symbol ? left : left.args[1] - elseif sig.head === :curly - return sig.args[1] - else - error("Malformed struct signature.") - end -end - - -# ============================================================================== -# MACRO 1: @gridspace (The Combinatorics Hook) -# ============================================================================== -macro gridspace(expr) - raw_ast = _strip_escapes(expr) - struct_node = _get_struct_node(raw_ast) - struct_node === nothing && error("@gridspace must be applied to a struct.") - - struct_name = _extract_struct_name(struct_node) - fields, clean_body_args = _parse_fields(struct_node.args[3]) - - # We strictly enforce the removal of `= default` from the struct definition, - # otherwise Julia will crash when it tries to compile the dumb struct. - is_mutable = struct_node.args[1] - struct_sig = struct_node.args[2] - clean_struct = Expr(:struct, is_mutable, struct_sig, Expr(:block, clean_body_args...)) - - kw_params = Any[] - grid_calls = Any[] - for f in fields - if f.has_default - push!(kw_params, Expr(:kw, f.name, f.default_val)) - else - push!(kw_params, f.name) - end - push!(grid_calls, :(Grid($(f.name)))) - end - - params = Expr(:parameters, kw_params...) - call_sig = Expr(:call, struct_name, params) - grid_tuple = Expr(:tuple, grid_calls...) - - kw_func = Expr(:function, call_sig, quote - return Gridspace{$struct_name}($grid_tuple) - end) - - final_ast = _rebuild_ast(raw_ast, clean_struct, Any[kw_func]) - return esc(final_ast) -end - - -# ============================================================================== -# MACRO 2: @relax (The Type Promoter & Converter) -# ============================================================================== -macro relax(expr) - expr.head == :struct || error("@relax must be applied to a struct definition.") - - # 1. Parse signature and supertype - sig = expr.args[2] - super_type = nothing - if sig isa Expr && sig.head == :<: - super_type = sig.args[2] - sig = sig.args[1] - end - struct_name = sig isa Expr && sig.head == :curly ? sig.args[1] : sig - - # 2. Extract fields - fields = Symbol[] - for arg in expr.args[3].args - if arg isa Symbol - push!(fields, arg) - elseif arg isa Expr && arg.head == :(::) - push!(fields, arg.args[1]) - end - end - - # 3. Build AST components - eltype_calls = [:(Base.eltype(typeof($(f)))) for f in fields] - recasts = [:(recast(T_promo, $(f))) for f in fields] - target_recasts = [:(recast(T_target, s.$(f))) for f in fields] - - # 4. Abstract Supertype Converter (Optional) - abstract_convert = :() - if super_type !== nothing && super_type isa Expr && super_type.head == :curly - abstract_name = super_type.args[1] - abstract_convert = quote - @inline function Base.convert( - ::Type{<:$(abstract_name){T_target}}, - s::$(struct_name), - ) where {T_target <: Real} - return $(struct_name)($(target_recasts...)) - end - end - end - - # 5. Emit - return esc( - quote - $expr # The original struct - - # The Untyped Outer Constructor - @inline function $struct_name($(fields...)) - T_promo = promote_type($(eltype_calls...)) - return $struct_name($(recasts...)) - end - - # The Concrete Converter - @inline function Base.convert( - ::Type{<:$struct_name{T_target}}, - s::$struct_name, - ) where {T_target <: Real} - return $struct_name($(target_recasts...)) - end - - # The eltype Hook - @inline Base.eltype(::Type{<:$struct_name{T}}) where {T} = T - - # THE NEW RECAST HOOK FOR THIS CONCRETE STRUCT - @inline recast(::Type{T_target}, s::$struct_name) where {T_target <: Real} = - $struct_name($(target_recasts...)) - - $abstract_convert - end, - ) -end \ No newline at end of file diff --git a/src/cablebuilder/materials.jl b/src/cablebuilder/materials.jl deleted file mode 100644 index fcadf291a..000000000 --- a/src/cablebuilder/materials.jl +++ /dev/null @@ -1,20 +0,0 @@ -@gridspace @relax struct Material{T <: Real} - "Electrical resistivity of the material \\[Ω·m\\]." - rho::T - "Relative permittivity \\[dimensionless\\]." - eps_r::T - "Relative permeability \\[dimensionless\\]." - mu_r::T - "Reference temperature for property evaluations \\[°C\\]." - T0::T = 20.0 - "Temperature coefficient of resistivity \\[1/°C\\]." - alpha::T = 0.0 - "Thermal resistivity \\[K·m/W\\]." - rho_thermal::T = 0.0 - "Maximum operating temperature \\[°C\\]." - theta_max::T = 90.0 - "Dielectric loss factor \\[dimensionless\\]." - tan_delta::T = 0.0 - "Solar absorption coefficient \\[dimensionless\\]." - sigma_solar::T = 0.0 -end \ No newline at end of file diff --git a/src/cablebuilder/partbuilder.jl b/src/cablebuilder/partbuilder.jl deleted file mode 100644 index 69cb9a516..000000000 --- a/src/cablebuilder/partbuilder.jl +++ /dev/null @@ -1,110 +0,0 @@ -# ========================================== -# THE VALIDATION BOUNDARY -# ========================================== -# If a specific part doesn't define topological rules, it passes. -@inline validate(part::AbstractCablePart) = part - -# ========================================== -# THE ATOMIC BUILDER (Physics & Intrinsic Geometry) -# ========================================== -struct PartBuilder{Target, Shape, P <: Tuple} - cmp::Symbol - payload::P -end - -# THE CONSTRUCTOR (The Zero-Alloc Val Interceptor) -@inline function PartBuilder( - ::Val{Target}, ::Val{Shape}, cmp::Symbol, payload..., -) where {Target, Shape} - return PartBuilder{Target, Shape, typeof(payload)}(cmp, payload) -end - -# THE FUNCTOR (The Spatial Collapse - Restored) -@inline function (b::PartBuilder{Target, Shape})( - prev_bound::AbstractShapeParams, -) where {Target, Shape} - # Pure coaxial materialization. Zero 2D awareness. We receive the - # absolute geometric boundary and pass it to the shape math. - part = build_part(Target, Shape, b.cmp, prev_bound, b.payload) - return validate(part) -end - -# ========================================== -# THE DSL HOOK -# ========================================== -@inline function Builder( - ::Type{Target}, ::Type{Shape}, cmp::Symbol, args..., -) where {Target, Shape} - grids = ( - Grid(Val{Target}()), - Grid(Val{Shape}()), - Grid(cmp), - map(Grid, args)..., - ) - return Gridspace{PartBuilder}(grids) -end - -# ========================================== -# THE GROUP BUILDER (Topology & Layout) -# ========================================== -struct GroupBuilder{P <: Tuple} - payload::P -end - -@inline function GroupBuilder(::Val{PartGroup}, payload...) - return GroupBuilder{typeof(payload)}(payload) -end - -@inline function (b::GroupBuilder)(prev_bound::Circular) - origin = b.payload[1] - n = b.payload[2] - m = b.payload[3] - - inner_builders = Base.tail(Base.tail(Base.tail(b.payload))) - - # 1. Local coaxial stacking - T_local = typeof(prev_bound.r) - local_parts = build_design(Circular(zero(T_local)), inner_builders) - local_r_ex = r_ex(local_parts[end]) - - # 2. Global topological translation - ox, oy = origin - layout_r = sqrt(ox^2 + oy^2) - r_prev = prev_bound.r - bound_r_in = r_prev - - # Outer envelope calculation: - # `layout_r` is the center of the first layer of cores. - # Each additional `m` layer adds roughly `2 * local_r_ex` to the bounding center. - # The outer boundary adds one final `local_r_ex` to clear the outermost core. - envelope_r = layout_r + ((2 * m) - 1) * local_r_ex - - bound_r_ex = max(r_prev, envelope_r) - - T = promote_type(typeof(bound_r_in), typeof(bound_r_ex), typeof(ox), typeof(oy)) - - # 3. Emit the anonymous topological folder - part = PartGroup( - convert(T, bound_r_in), - convert(T, bound_r_ex), - (convert(T, ox), convert(T, oy)), - n, m, - local_parts, - ) - - return validate(part) -end - -@inline function Builder( - ::Type{PartGroup}, origin, n, m, layers::Tuple, -) - grids = ( - Grid(Val{PartGroup}()), - Grid((origin,)), - Grid(n), - Grid(m), - layers..., - ) - - return Gridspace{GroupBuilder}(grids) -end diff --git a/src/cablebuilder/primitives.jl b/src/cablebuilder/primitives.jl deleted file mode 100644 index f841bf887..000000000 --- a/src/cablebuilder/primitives.jl +++ /dev/null @@ -1,38 +0,0 @@ -# Shape params define the shape of a primitive, but not its material, group or location/layout. -abstract type AbstractShapeParams{T <: Real} end - -# If a specific payload vault doesn't define intrinsic rules, it passes. -@inline validate(params::AbstractShapeParams) = params - -@gridspace @relax struct Circular{T <: Real} <: AbstractShapeParams{T} - r::T -end - -@inline function validate(p::Circular) - p.r > zero(p.r) || throw(DomainError(p.r, "Circular radius must be strictly positive.")) - return p -end - -@gridspace @relax struct Rectangular{T <: Real} <: AbstractShapeParams{T} - w::T - h::T -end - -@inline function validate(p::Rectangular) - p.w > zero(p.w) || - throw(DomainError(p.w, "Rectangular width must be strictly positive.")) - p.h > zero(p.h) || - throw(DomainError(p.h, "Rectangular height must be strictly positive.")) - return p -end - -@gridspace @relax struct Annular{T <: Real} <: AbstractShapeParams{T} - t::T -end - -@inline function validate(p::Annular) - p.t > zero(p.t) || - throw(DomainError(p.t, "Annular thickness must be strictly positive.")) - return p -end - diff --git a/src/cablebuilder/runme.jl b/src/cablebuilder/runme.jl deleted file mode 100644 index 580742370..000000000 --- a/src/cablebuilder/runme.jl +++ /dev/null @@ -1,157 +0,0 @@ -using Revise -include("CableBuilder.jl") -using .CableBuilder -import .CableBuilder: CableDesign - -using BenchmarkTools - -# 1. Define a dummy stochastic Material -# 2 variations of resistivity -mat = Material( - rho = Grid((1.6e-8, 1.7e-8)), - eps_r = 1.0, mu_r = 1.0, T0 = 20.0, alpha = 0.0, - rho_thermal = 0.0, theta_max = 90.0, tan_delta = 0.0, sigma_solar = 0.0, -) - -# 2. Define the Solid Core wrapping the Circle primitive -# 5 variations of radius -core = ( - Conductor.Solid(:core, mat; r = Grid((0.01, 0.02, 0.03, 0.04, 0.05))), - Conductor.Tubular(:sheath, mat; t = Grid((0.01, 0.02, 0.03, 0.04, 0.05))), - Insulator.Tubular(:sheath, mat; t = Grid((0.05))), -) - -# 3. Wrap it in the Design Blueprint -spec = CableDesign(core) - -# 4. The strict function barrier -# This forces the compiler to optimize the loop exactly as it would in production -function exhaust_generator(s) - count = 0 - for design in s - # 'design' is fully materialized right here! - # If the primitives, builders, or tuple recursion leak memory, - # it will pile up on the heap inside this loop. - count += 1 - end - return count -end - -# Warmup to compile -exhaust_generator(spec) - -# The moment of truth -println("--- Combinatorial Allocation Test ---") -@btime exhaust_generator($spec) - -des=first(spec, length(spec)) -# ms_cu = Material(1.7241e-8, 1.0, 0.999994, 20.0, 0.00393, 0.0, 0.0, 0.0, 0.0) -# ms_vac = Material(Inf, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0) - -# parts = ( -# Conductor.Solid(:core, ms_cu; r = 0.02315), -# Conductor.Tubular(:sheath, ms_cu; t = 0.005), -# Conductor.Pipe(:pipe, ms_cu; t = 0.005, filler = ms_vac, offset = 0.01), -# # Conductor.Stranded(:sheath, ms_cu; r_w = 0.005, n_w = 3, lay_r = 0.01), -# ) - -# # Flawless compilation -# cds = CableDesignSpec(parts) - -# ast = first(cds) -# println(ast.payload) - - -# 1. Define the Blueprint (The User DSL) -# Notice we mix deterministic scalars, a relative sweep, and an absolute sweep -# copper_spec = Material(; -# rho = Grid(1.68e-8:0.01e-8:1.72e-8, 2.0), # 2% manufacturing tolerance -# eps_r = 1.0, # Auto-promoted to DeterministicGrid -# mu_r = 1.0, -# T0 = Grid(20.0:10.0:90.0, AbsoluteError(2.0)), # ± 2.0°C absolute sensor error -# ) - -# @show typeof(copper_spec) -# @code_warntype rand(copper_spec) - -# @show @allocated rand(copper_spec) -# @btime @allocated rand($copper_spec) - - -# # --------------------------------------------------------- -# # Benchmark 1: The Array Comprehension (User Snippet) -# # --------------------------------------------------------- -# println("--- Benchmarking 100000 Monte Carlo Realizations (Array Allocation) ---") -# @btime mc_materials = [rand($copper_spec) for _ in 1:100000] - -# # @allocated(rand(copper_spec)) = 80 -# # 27.698 ns (0 allocations: 0 bytes) -# # --- Benchmarking 100000 Monte Carlo Realizations (Array Allocation) --- -# # 2.084 ms (3 allocations: 6.87 MiB) -# # - -# # --------------------------------------------------------- -# # Benchmark 2: The Bare-Metal Loop (Zero-Allocation Target) -# # --------------------------------------------------------- -# # This tests the pure speed of your ntuple/Grid/rand architecture -# # without the overhead of Julia allocating a 1000-element Vector -# function pure_monte_carlo_loop(spec, N) -# # We just draw the sample and discard it to test the engine's raw speed -# for _ in 1:N -# m = rand(spec) -# end -# return nothing -# end - -# println("\n--- Benchmarking 1000 Monte Carlo Realizations (Engine Only) ---") -# @btime pure_monte_carlo_loop($copper_spec, 1000) - - -# # --------------------------------------------------------- -# # Test 1: The Deterministic Single Build -# # --------------------------------------------------------- -# println("--- Benchmarking Single Deterministic Build ---") -# # We use $ to interpolate the variable into the macro so it doesn't benchmark global scope lookup -# @btime first($cds) - -# # --------------------------------------------------------- -# # Test 2: The Combinatorial Iterator (The Real Test) -# # --------------------------------------------------------- -# grid_parts = ( -# Conductor.Solid(:core, ms_cu; r = Grid([0.02, 0.025, 0.03])), # 3 variations -# Conductor.Tubular(:sheath, ms_cu; t = Grid([0.004, 0.005, 0.006, 0.007])), # 4 variations -# Conductor.Pipe( -# :pipe, -# ms_cu; -# t = Grid([0.004, 0.005, 0.006, 0.007]), -# filler = ms_vac, -# offset = Grid([0.004, 0.005, 0.006, 0.007]), -# ), # 4 variations -# ) -# grid_cds = CableDesignSpec(grid_parts) -# println("\n--- Benchmarking 1-Design Materialization from Grids ---") -# @btime first($grid_cds) - -# # A function barrier to test the loop exactly how your solver will use it -# function exhaust_generator(spec) -# count = 0 -# for design in spec -# # The design is materialized here. -# # If the compiler is happy, this loop will allocate ZERO memory. -# count += 1 -# end -# return count -# end - -# println("\n--- Benchmarking Combinatorial Sweep ---") -# @btime exhaust_generator($grid_cds) - -# function alloc_per_design(spec, n) -# s = Iterators.take(spec, n) -# a = @allocated for x in s -# nothing -# end -# return a / n -# end -# println("\n--- Alloc per design ---") -# @show alloc_per_design(grid_cds, length(grid_cds)) diff --git a/src/cablebuilder/shapes.jl b/src/cablebuilder/shapes.jl deleted file mode 100644 index e499451cf..000000000 --- a/src/cablebuilder/shapes.jl +++ /dev/null @@ -1,38 +0,0 @@ -# Global accessors -@inline r_in(s::AbstractShape) = s.r_in -@inline r_ex(s::AbstractShape) = s.r_ex - - - -include("solidcore.jl") -include("tubular.jl") -# include("enclosure.jl") -# include("wires.jl") -# include("helical.jl") -# include("stranded.jl") - -# --------------------------------------------------------- -# The Fuzzy Characteristic Length Trait -# --------------------------------------------------------- -# Returns the characteristic dimension of the primitive. -# If someone writes a Rectangle primitive and defines char_len as the diagonal, -# the stacking engine will blindly build overlapping garbage. -@inline char_len(s::SolidCore) = 2 * r_ex(s) -# @inline char_len(s::TubularLayer) = r_ex(s) - r_in(s) -# @inline char_len(s::CircularWire) = 2 * s.r -# @inline char_len(s::RectangularWire) = s.h - -# ========================================== -# THE TOPOLOGICAL FOOTPRINT -# Extracts the absolute boundary as a concrete primitive. -# ========================================== -@inline boundary(p::AbstractCablePart) = boundary(p.shape) - -# A SolidCore's footprint is a circle at r_ex. -@inline boundary(s::SolidCore) = Circular(r_ex(s)) - -# A Tubular's footprint is a circle at r_ex. -@inline boundary(s::TubularLayer) = Circular(r_ex(s)) - -# A PartGroup's bounding footprint is currently a circumscribed circle. -@inline boundary(g::PartGroup) = Circular(r_ex(g)) \ No newline at end of file diff --git a/src/cablebuilder/solidcore.jl b/src/cablebuilder/solidcore.jl deleted file mode 100644 index 81dc4da5f..000000000 --- a/src/cablebuilder/solidcore.jl +++ /dev/null @@ -1,58 +0,0 @@ -# ========================================== -# 1. THE VAULT -# ========================================== -# Just strictly holds the universal boundaries. -@relax struct SolidCore{T <: Real, P <: AbstractShapeParams{T}} <: AbstractShape{T} - r_in::T - r_ex::T - params::P -end - -@inline function validate(part::ConductorPart{T, <:SolidCore}) where {T} - shape = part.shape - - # Cascade to intrinsic validation - validate(shape.params) - - # Topological bounds checks - shape.r_in == zero(T) || throw( - DomainError( - shape.r_in, "Topological violation: SolidCore MUST start exactly at r=0.", - ), - ) - - shape.r_ex > shape.r_in || throw( - DomainError( - shape.r_ex, - "Physics violation: Outer radius ($(shape.r_ex)) must be > inner radius ($(shape.r_in)).", - ), - ) - - return part -end - -# ========================================== -# 2. THE FUNCTOR SPECIALIZATION (Spatial Collapse) -# ========================================== -@inline function build_part( - ::Type{Target}, - ::Type{SolidCore}, - grp::Symbol, - prev_bound::Circular{T}, # <-- Dispatches on the primitive - payload::Tuple{M, C}, -) where {Target, T <: Real, M <: Material, C <: Circular} - - mat, params = payload - - prev_bound.r <= eps(T) || - throw( - DomainError( - prev_bound.r, - "Topological violation: SolidCore must start at r=0.", - ), - ) - - shape = SolidCore(prev_bound.r, params.r, params) - return Target(grp, shape, mat) -end - diff --git a/src/cablebuilder/stranded.jl b/src/cablebuilder/stranded.jl deleted file mode 100644 index 15a30921d..000000000 --- a/src/cablebuilder/stranded.jl +++ /dev/null @@ -1,93 +0,0 @@ -# ========================================== -# 1. THE UNIVERSAL VAULT -# ========================================== -struct StrandedLayer{ - L, - T <: Real, - U <: Integer, - P <: AbstractWire{T}, - H <: HelicalPath, -} <: AbstractShape{T} - r_in::T - r_ex::T - n_w::U - wire::P - pitch::H -end - -function StrandedLayer( - r_in, - r_ex, - n_w::Integer, - wire::AbstractWire, - pitch::HelicalPath, -) - T = promote_type(typeof(r_in), typeof(r_ex)) - p = convert(AbstractWire{T}, wire) - return StrandedLayer{T, typeof(n_w), typeof(p), typeof(pitch)}( - convert(T, r_in), convert(T, r_ex), n_w, p, pitch, - ) -end - -function Base.convert( - ::Type{<:AbstractShape{T}}, - s::StrandedLayer, -) where {T <: Real} - p_converted = convert(AbstractWire{T}, s.wire) - return StrandedLayer{T, typeof(s.n_w), typeof(p_converted), typeof(s.pitch)}( - convert(T, s.r_in), convert(T, s.r_ex), s.n_w, p_converted, s.pitch, - ) -end - -# ========================================== -# 2. THE UNIVERSAL BUILDER -# ========================================== -struct StrandedBuilder{P, U <: Integer, W, H, T <: Real} - cmp::Symbol - n_w::U - wire_builder::W - pitch_builder::H - mat::Material{T} -end - -@inline function (b::StrandedBuilder{P})(current_r::T) where {P, T <: Real} - # If someone tries to put a stranded armor at the exact center of the universe, mock them. - current_r <= zero(T) && error( - "Topological violation: Stranded layers cannot exist at r=0. Use a SolidCore.", - ) - - # 1. Materialize the physical entity - wire = b.wire_builder() - - # 2. Extract its radial footprint via dispatch - thick = char_len(wire) - - r_ex = current_r + thick - mean_diam = current_r + (thick / 2) - - # 3. Pass context to the nested helical builder - pitch_profile = b.pitch_builder(mean_diam) - - # 4. Lock it into the unified layer - shape = StrandedLayer(current_r, r_ex, b.n_w, wire, pitch_profile) - - return P(b.cmp, shape, b.mat) -end - -# ========================================== -# 3. THE UNIVERSAL BLUEPRINT -# ========================================== -struct StrandedSpec{ - P, - G, - U, - W <: AbstractSpec, - H <: AbstractSpec, - M <: AbstractSpec{Material}, -} <: AbstractSpec{StrandedBuilder{P}} - cmp::G - n_w::U - wire_spec::W - pitch_spec::H - mat::M -end \ No newline at end of file diff --git a/src/cablebuilder/tubular.jl b/src/cablebuilder/tubular.jl deleted file mode 100644 index 18dbd6a42..000000000 --- a/src/cablebuilder/tubular.jl +++ /dev/null @@ -1,56 +0,0 @@ -# ========================================== -# THE VAULT -# ========================================== -@relax struct TubularLayer{T <: Real, P <: AbstractShapeParams{T}} <: AbstractShape{T} - r_in::T - r_ex::T - params::P -end - -# We use a Union to safely catch both Conductors and Insulators -# without introducing dynamic `isa` checks or type-pirating the base AbstractCablePart. -@inline function validate( - part::Union{ConductorPart{T, <:TubularLayer}, InsulatorPart{T, <:TubularLayer}}, -) where {T} - shape = part.shape - - # Cascade to intrinsic validation (checks t > 0) - validate(shape.params) - - # Topological bounds check - shape.r_ex > shape.r_in || throw( - DomainError( - shape.r_ex, - "Physics violation: TubularLayer outer radius ($(shape.r_ex)) must be strictly greater than inner radius ($(shape.r_in)).", - ), - ) - - return part -end - - -# ========================================== -# THE FUNCTOR SPECIALIZATION (Spatial Collapse) -# ========================================== -@inline function build_part( - ::Type{Target}, - ::Type{TubularLayer}, - cmp::Symbol, - prev_bound::Circular{T}, - payload::Tuple{M, A}, -) where {Target, T <: Real, M <: Material, A <: Annular} - - mat, params = payload - - # Conformal anchor: The tube strictly wraps the inner circular boundary. - r_in = prev_bound.r - - # Extrusion: Expand by the intrinsic payload thickness. - r_ex = r_in + params.t - - # Collapse the shape geometry - shape = TubularLayer(r_in, r_ex, params) - - # Emit the atomic physics part (Zero 2D awareness here) - return Target(cmp, shape, mat) -end \ No newline at end of file diff --git a/src/cablebuilder/types.jl b/src/cablebuilder/types.jl deleted file mode 100644 index 93b560c3a..000000000 --- a/src/cablebuilder/types.jl +++ /dev/null @@ -1,65 +0,0 @@ -abstract type AbstractShape{T <: Real} end -abstract type AbstractCablePart end - -@inline r_ex(p::AbstractCablePart) = r_ex(p.shape) -@inline r_in(p::AbstractCablePart) = r_in(p.shape) - -struct ConductorPart{T, S <: AbstractShape{T}} <: AbstractCablePart - cmp::Symbol - shape::S - material::Material{T} -end - -@inline function ConductorPart( - cmp::Symbol, - shape::AbstractShape{S}, - mat::Material{M}, -) where {S <: Real, M <: Real} - T = promote_type(S, M) - s = convert(AbstractShape{T}, shape) - m = convert(Material{T}, mat) - - return ConductorPart{T, typeof(s)}(cmp, s, m) -end - -struct InsulatorPart{T, S <: AbstractShape{T}} <: AbstractCablePart - cmp::Symbol - shape::S - material::Material{T} -end - -@inline function InsulatorPart( - cmp::Symbol, - shape::AbstractShape{S}, - mat::Material{M}, -) where {S <: Real, M <: Real} - T = promote_type(S, M) - s = convert(AbstractShape{T}, shape) - m = convert(Material{T}, mat) - - return InsulatorPart{T, typeof(s)}(cmp, s, m) -end - -# ========================================== -# THE TOPOLOGICAL VAULT -# ========================================== -struct PartGroup{T <: Real, P <: Tuple} <: AbstractCablePart - r_in::T - r_ex::T - origin::Tuple{T, T} - n::Int - m::Int - parts::P -end - -@inline r_ex(g::PartGroup) = g.r_ex -@inline r_in(g::PartGroup) = g.r_in - -# --------------------------------------------------------- -# THE GLOBAL RECAST FALLBACKS -# --------------------------------------------------------- -# 1. Reals get standard numeric conversion -@inline recast(::Type{T}, x::Real) where {T} = convert(T, x) - -# 2. Everything else (Symbols, Bools, Strings) is ignored and passed through safely -@inline recast(::Type{T}, x) where {T} = x \ No newline at end of file diff --git a/src/commons/Commons.jl b/src/commons/Commons.jl index 780fb2e30..205c1cfcd 100644 --- a/src/commons/Commons.jl +++ b/src/commons/Commons.jl @@ -1,25 +1,82 @@ -module Commons +""" + LineCableModels.Commons -include("docstringextension.jl") -include("consts.jl") +Define computation supertypes and functions shared by Engine, +ParametricBuilder, UQ, and external implementations. +# Physical constants -export get_description, add!, domain, LineParamsDomain, PhaseDomain, ModalDomain +- `vacuum_permittivity` and `vacuum_permeability` return the vacuum constants in + a requested scalar type. -function get_description end +# Matrix reductions -function add! end +- `ReductionPlan` fixes the terminal reorder, bundle merge, Kron elimination and + ideal transposition of primitive line matrices. +- `reduce_line_matrices!` applies a plan to one frequency and inverts the potential + coefficients to the shunt admittance, in `ReductionBuffers`. -abstract type LineParamsDomain end -struct PhaseDomain <: LineParamsDomain end -struct ModalDomain <: LineParamsDomain end +# Public actions +- `formulation_options` and `computation_options` normalize owner-specific options. +- `formulas` lists the identifiers that a formula family registers. +- `initialize_buffers` builds the reusable arrays of a computation into its buffer + record, one method per formula or shared component. +- `computation_details` normalizes supplemental output from a registered + computation owner, and `details` reads retained supplemental output. +- `validate` checks a materialized input before its consumer uses it and returns it + unchanged. +- `compute` evaluates a problem through a selected formulation. +- `observe` and `@observe` read native numerical values from completed results. +- `observables` publishes explicitly requested scientific values. +- `validate_observables` and `unit_targets` align publication requests and + display units for presentation consumers. """ -Return the domain tag type for objects that carry one. +module Commons -Fallback returns `nothing` for domainless objects. -""" -@inline domain(::Type) = nothing -@inline domain(x) = domain(typeof(x)) +export AbstractProblemDefinition, AbstractFormulation, AbstractProblemResult +export AbstractCoreResult, AbstractResultSpace +export AbstractParametricResult, AbstractUncertaintyResult +export FormulationOptions, ComputationOptions, ComputationDetails +export formulation_options, computation_options, computation_details, details +export compute, observe, @observe, observables, validate +export nominal, uncertainty + +using DocStringExtensions: SIGNATURES, TYPEDSIGNATURES, TYPEDEF, TYPEDFIELDS +using RequiredInterfaces: @required +import ..LineCableModels: basis +import UUIDs +import Random +import ..Units +using LinearAlgebra: I, axpy!, checksquare, cond, diag, ldiv!, lu!, mul!, norm +using ..Units: UnitExpr, quantity, native_unit, display_unit, scale_factor + +include("consts.jl") +include("matrixops.jl") +include("types.jl") +include("base.jl") +include("results.jl") +include("interfaces.jl") +include("formulas.jl") +include("observables.jl") +include("uncertainty.jl") +include("gridpoint.jl") +include("observed_validation.jl") +include("observedresult.jl") +include("retained_products.jl") -end \ No newline at end of file +public vacuum_permittivity, vacuum_permeability +public ideal_transposition!, reorder_indices, kron_reduce, kron_reduce!, + bundle_operations, merge_bundles! +public ReductionPlan, ReductionBuffers, reduce_line_matrices!, initialize_buffers +public FormulaDefinition, Expression, Functor, formulas +public validate_observables, unit_targets, detach +public observation_request, observation_indices, materialize_observation +public observation_resolution +public input_fields, observation_gridpoint, observation_requests, observation_quantity +export ObservedResult +public observation_groups, observation_labels, observation_product, gridpoint_id +public observation_selection +public request_identity, request_quantity, request_indices +public normalize_observation_selector +end # module Commons diff --git a/src/commons/base.jl b/src/commons/base.jl new file mode 100644 index 000000000..2303d1c38 --- /dev/null +++ b/src/commons/base.jl @@ -0,0 +1,30 @@ +# Records have structural value semantics, not a NamedTuple collection interface. +Base.:(==)(a::FormulationOptions, b::FormulationOptions) = a.data == b.data +Base.:(==)(a::ComputationOptions, b::ComputationOptions) = a.data == b.data +Base.:(==)(a::ComputationDetails, b::ComputationDetails) = a.data == b.data + +Base.isequal(a::FormulationOptions, b::FormulationOptions) = isequal(a.data, b.data) +Base.isequal(a::ComputationOptions, b::ComputationOptions) = isequal(a.data, b.data) +Base.isequal(a::ComputationDetails, b::ComputationDetails) = isequal(a.data, b.data) + +Base.hash(value::FormulationOptions, seed::UInt) = hash(value.data, hash(:FormulationOptions, seed)) +Base.hash(value::ComputationOptions, seed::UInt) = hash(value.data, hash(:ComputationOptions, seed)) +Base.hash(value::ComputationDetails, seed::UInt) = hash(value.data, hash(:ComputationDetails, seed)) + +function Base.show(io::IO, value::FormulationOptions) + print(io, "FormulationOptions(") + show(io, value.data) + print(io, ')') +end + +function Base.show(io::IO, value::ComputationOptions) + print(io, "ComputationOptions(") + show(io, value.data) + print(io, ')') +end + +function Base.show(io::IO, value::ComputationDetails) + print(io, "ComputationDetails(") + show(io, value.data) + print(io, ')') +end diff --git a/src/commons/consts.jl b/src/commons/consts.jl index c58b7b1df..25347eb57 100644 --- a/src/commons/consts.jl +++ b/src/commons/consts.jl @@ -1,26 +1,20 @@ -# Export public API -export f₀, μ₀, ε₀, ρ₀, T₀, TOL, ΔTmax -export BASE_FLOAT, REALSCALAR, COMPLEXSCALAR +""" +$(TYPEDSIGNATURES) -# General constants -"Base power system frequency, f₀ = 50.0 [Hz]." -const f₀ = 50.0 -"Magnetic constant (vacuum permeability), μ₀ = 4π * 1e-7 [H/m]." -const μ₀ = 4π * 1e-7 -"Electric constant (vacuum permittivity), ε₀ = 8.8541878128e-12 [F/m]." -const ε₀ = 8.8541878128e-12 -"Annealed copper reference resistivity, ρ₀ = 1.724e-08 [Ω·m]." -const ρ₀ = 1.724e-08 -"Base temperature for conductor properties, T₀ = 20.0 [°C]." -const T₀ = 20.0 -"Maximum tolerance for temperature variations, ΔTmax = 150 [°C]." -const ΔTmax = 150.0 -"Default tolerance for floating-point comparisons, TOL = 1e-6." -const TOL = 1e-6 +Return the vacuum permittivity ``\\varepsilon_0 = 8.8541878128 \\times 10^{-12}`` +\\[F/m\\] in the scalar type `T`. -# Define aliases for the type constraints -using Measurements: Measurement -const BASE_FLOAT = Float64 -const REALSCALAR = Union{BASE_FLOAT, Measurement{BASE_FLOAT}} -const COMPLEXSCALAR = Union{Complex{BASE_FLOAT}, Complex{Measurement{BASE_FLOAT}}} +The integer mantissa and the power of ten are evaluated in `T`. The value is exact +for rational `T` and has the precision of `T` otherwise. +""" +vacuum_permittivity(::Type{T}) where {T <: Real} = one(T) * 88541878128 * (one(T) * 10)^(-22) +""" +$(TYPEDSIGNATURES) + +Return the vacuum permeability ``\\mu_0 = 4\\pi \\times 10^{-7}`` \\[H/m\\] in the scalar +type `T`. + +The factors are evaluated in `T`. The value has the precision of `T`. +""" +vacuum_permeability(::Type{T}) where {T <: Real} = one(T) * 4 * (one(T) * π) * (one(T) * 10)^(-7) diff --git a/src/commons/docstringextension.jl b/src/commons/docstringextension.jl deleted file mode 100644 index 82b7ba687..000000000 --- a/src/commons/docstringextension.jl +++ /dev/null @@ -1,54 +0,0 @@ -using Pkg -using DocStringExtensions: DocStringExtensions, SIGNATURES, TYPEDSIGNATURES, TYPEDEF, TYPEDFIELDS, FIELDS, FUNCTIONNAME, IMPORTS, EXPORTS - -export SIGNATURES, TYPEDSIGNATURES, TYPEDEF, TYPEDFIELDS, FIELDS, FUNCTIONNAME, METHODLIST, IMPORTS, EXPORTS - -""" -Override `DocStringExtensions.format` for `METHODLIST`. -""" -struct _CleanMethodList <: DocStringExtensions.Abbreviation end -"Modified `METHODLIST` abbreviation with sanitized file paths." -const _CLEANMETHODLIST = _CleanMethodList() -const METHODLIST = _CLEANMETHODLIST - -function DocStringExtensions.format(::_CleanMethodList, buf, doc) - local binding = doc.data[:binding] - local typesig = doc.data[:typesig] - local modname = doc.data[:module] - local func = Docs.resolve(binding) - local groups = DocStringExtensions.methodgroups(func, typesig, modname; exact=false) - if !isempty(groups) - println(buf) - local pkg_root = Pkg.pkgdir(modname) # Use Pkg.pkgdir here - if pkg_root === nothing - @warn "Could not determine package root for module $modname using METHODLIST. Paths will be shown as basenames." - end - for group in groups - println(buf, "```julia") - for method in group - DocStringExtensions.printmethod(buf, binding, func, method) - println(buf) - end - println(buf, "```\n") - if !isempty(group) - local method = group[1] - local file = string(method.file) - local line = method.line - local path = - if pkg_root !== nothing && !isempty(file) && - startswith(file, pkg_root) - basename(file) # relpath(file, pkg_root) - # elseif !isempty(file) && isfile(file) - # basename(file) - else - string(method.file) # Fallback - end - local URL = DocStringExtensions.url(method) - isempty(URL) || println(buf, "defined at [`$path:$line`]($URL).") - end - println(buf) - end - println(buf) - end - return nothing -end \ No newline at end of file diff --git a/src/commons/formulas.jl b/src/commons/formulas.jl new file mode 100644 index 000000000..6555c6969 --- /dev/null +++ b/src/commons/formulas.jl @@ -0,0 +1,145 @@ +""" +Declare formulation-option defaults for one expression. +""" +function formulation_options(expression::Expression) + throw(ArgumentError("missing formulation-option defaults for $expression")) +end + +function formulation_options(expression::Expression, supplied::FormulationOptions) + return formulation_options(expression, formulation_options(expression), supplied) +end + +""" +$(TYPEDSIGNATURES) + +Normalize supplied formulation options against defaults declared by the actual +selected expression. A family with multiple cases projects supplied options to +each consuming expression before calling this constructor. Empty defaults exclude options. Each option's dispatched +normalizer defines its value type, including scalar physical choices and structured +numerical controls. +""" +function formulation_options(expression::Expression, defaults::FormulationOptions, supplied::FormulationOptions) + default_data, supplied_data = defaults.data, supplied.data + unknown = filter(name -> !haskey(default_data, name), keys(supplied_data)) + isempty(unknown) || throw(ArgumentError( + "unused formulation options $(Tuple(unknown)) for $expression")) + sections = map(keys(default_data)) do name + default = getproperty(default_data, name) + explicit = get(supplied_data, name, default isa NamedTuple ? (;) : default) + formulation_options(expression, Val(name), default, explicit) + end + return FormulationOptions(NamedTuple{keys(default_data)}(sections)) +end + +function formulation_options(expression::Expression, ::Val{Section}, defaults, + supplied) where {Section} + throw(ArgumentError("no formulation-option constructor for :$Section of $expression with $(typeof(supplied))")) +end + +""" +$(TYPEDSIGNATURES) + +Project the options supplied to `formula` onto the expressions it declares. Each supplied +section must be consumed by at least one expression. Each distinct expression receives the +sections its defaults declare, normalized by its own constructor. Return the distinct +expressions in order of first appearance and their `FormulationOptions`, as +`(expressions, options)`. +""" +function formulation_options(formula::AbstractFormulation, + expressions::Union{Tuple, AbstractVector{<:Expression}}) + supplied = formula.options.data + identities = unique(expressions) + defaults = map(formulation_options, identities) + admitted = union((keys(value.data) for value in defaults)...) + unknown = setdiff(keys(supplied), admitted) + isempty(unknown) || throw(ArgumentError( + "unused formulation options $(Tuple(unknown)) for :$(formula_id(formula))")) + normalized = map(eachindex(identities)) do index + declared = defaults[index] + names = Tuple(intersect(keys(supplied), keys(declared.data))) + formulation_options(identities[index], declared, FormulationOptions(supplied[names])) + end + return (expressions = identities, options = normalized) +end + +""" + formulas(family) + +Return the identifiers that a formula family registers, in registration order. `family` is +the family's `Formula` type, such as `EarthImpedance.Formula`. Each family adds one +method on its own `Formula` type. +""" +function formulas end + +import ..LineCableModels: description, formula_id + +""" +$(TYPEDSIGNATURES) + +Check that the operation of `expression` has a method for its formula and selectors, before +any evaluation. The operation takes a [`Functor`](@ref) and a workspace after the selectors, +as the evaluation passes them. A narrower method for either argument counts. A backend +variant, which takes the backend in their place, does not. Return `expression`. + +# Errors + +- Throws `ArgumentError` naming the formula and the selectors that have no expression. +""" +function validate(expression::Expression) + signature = Tuple{typeof(expression.selection), map(typeof, expression.arguments)..., + Functor, Any} + hasmethod(expression.method, signature) && return expression + isempty(methods(expression.method, signature)) || return expression + value(::Val{X}) where {X} = X + throw(ArgumentError("formula :$(formula_id(expression.selection)) has no expression for " * + join(map(selector -> repr(value(selector)), expression.arguments), ", "))) +end + +"""Read a declaration's actual formulation-owned inputs without resolving them.""" +formulation_options(value::FormulaDefinition) = value.options +formula_id(source::Pair{<:AbstractFormulation,<:NamedTuple}) = formula_id(first(source)) +description(source::Pair{<:AbstractFormulation,<:NamedTuple}; compact::Bool=false) = description(first(source);compact) + +""" +$(TYPEDSIGNATURES) + +Return the ordered, owner-scoped formula selections and controls relevant to +`quantity`. Pass `nothing` to retain the complete formulation. The formulation's owning +`pairs` methods list them. Descriptions are not identities. +Return `missing` if any selected identity is unavailable. No formula is evaluated. +""" +function formula_id(source::AbstractFormulation, quantity) + selections = Tuple((scope, formula_id(value), value isa Pair ? last(value) : (;)) + for (scope, value) in pairs(source; quantity)) + return any(selection -> ismissing(selection[2]), selections) ? missing : selections +end +formula_id(::Missing, quantity) = missing + +"""Describe a formulation's composition for one existing physical request.""" +description(source::AbstractFormulation,request;compact::Bool=true) = + only(description([source];roles=[:none],quantity=request,compact)) + +"""Describe a selection in its consuming owner's scientific context.""" +description(owner::Type,selected;compact::Bool=false,quantity=nothing) = description(selected;compact) + + +"""Describe an owner-scoped route using the selected leaf's own description.""" +description(scope::Tuple{Type,Tuple},selected;kwargs...) = description(first(scope),last(scope),selected;kwargs...) +function description(owner::Type,route::Tuple, selected; compact::Bool=true, settings::Bool=false) + name=isempty(route) ? description(owner;compact) : description(owner,Val(first(route))) + length(route)>1 && (name *= "("*join(string.(Base.tail(route)),",")*")") + controls=selected isa Pair ? last(selected) : (;) + value=settings ? "" : isempty(route) ? description(selected;compact) : description(owner,selected;compact) + isempty(controls) || (value *= (isempty(value) ? "" : " ") * + (isempty(route) ? sprint(show,controls;context=:compact=>compact) : + description(FormulaDefinition,controls;compact))) + return name*"="*value +end + +"""Render only the explicit controls admitted by a formula declaration.""" +function description(::Type{FormulaDefinition},controls::NamedTuple;compact::Bool=true) + return "("*join([string(key)*"="*description(FormulaDefinition,Val(key),value;compact) + for (key,value) in pairs(controls)],", ")*")" +end +description(::Type{FormulaDefinition},::Union{Val{:parameters},Val{:options}},value::NamedTuple; + compact::Bool=true) = sprint(show,value;context=:compact=>compact) diff --git a/src/commons/gridpoint.jl b/src/commons/gridpoint.jl new file mode 100644 index 000000000..ac0e8bb65 --- /dev/null +++ b/src/commons/gridpoint.jl @@ -0,0 +1,53 @@ +""" +$(TYPEDSIGNATURES) + +Copy retained records and arrays, preserving uncertainty-source identities. +Numerical leaves keep their scalar representation. Structured scientific owners +supply their own conversion. +""" +detach(value::Union{Number,Symbol,Nothing,Missing,Type,Val,UUIDs.UUID}) = value +detach(value::AbstractString) = String(value) +detach(value::NamedTuple) = map(detach, value) +detach(value::Tuple) = map(detach, value) +detach(value::AbstractArray) = map(detach, value) +detach(value::Pair) = detach(first(value)) => detach(last(value)) +detach(value::AbstractDict) = Dict(detach(key) => detach(item) for (key, item) in value) +detach(value::Union{Units.UnitExpr,Units.Unit,Units.Quantity}) = value +function detach(value::Function) + Base.issingletontype(typeof(value)) || throw(ArgumentError("observation records require a singleton function type or a type-specific detach method")) + return value +end +detach(value::Base.Fix2) = Base.Fix2(detach(value.f),detach(value.x)) + +""" +$(TYPEDSIGNATURES) + +Read the retained physical description and original identity of a completed +gridpoint. Result owners supply these descriptions through methods of `observation_gridpoint`. + +The fallback identifies an externally supplied result with unspecified inputs. +Such a record cannot establish physical equivalence with another observation. +""" +observation_gridpoint(source) = (id=nothing, inputs=nothing, formulations=nothing, + coordinates=nothing, uncertainty=nothing, missing_reason=:physical_inputs_not_supplied) + +""" +$(TYPEDSIGNATURES) + +Identify a completed physical point and formulation within one computation. +The default source UUID uses system randomness independently of scientific RNGs. +Collection owners share `source_id` and supply the original one-based indices. +""" +gridpoint_id(; source_id=UUIDs.uuid4(Random.RandomDevice()), + problem_index::Integer=1, formulation_index::Integer=1) = + (; source_id, problem_index=Int(problem_index), formulation_index=Int(formulation_index)) + +""" +$(TYPEDSIGNATURES) + +Return owner-defined physical field names and units for captured inputs. +Each entry is `(name, unit)`. Unregistered fields retain their explicit field +names without an inferred physical unit. Completion stores this passive metadata +beside the original values for source-free description formatting. +""" +input_fields(::Type) = (;) diff --git a/src/commons/interfaces.jl b/src/commons/interfaces.jl new file mode 100644 index 000000000..35f5402ab --- /dev/null +++ b/src/commons/interfaces.jl @@ -0,0 +1,215 @@ +""" + validate(subject, context...) + +Reject an invalid input before its consumer uses it. Return `subject`, the first +positional argument, unchanged. + +`subject` is user input or an input derived from it. The optional context says what the +subject is checked for: its consumer, such as the problem, formulation, physical model or +result being built, or, for the result of a law or record, the function that produced it. +Data that the check needs follows the context. A method returns the subject without +converting or resolving it or building a new record, and without a check when the +consumer does not restrict it. `validate` is the only verb for input checks. + +# Errors + +- Throws a native exception identifying the invalid field, value, and required + condition. +- Throws `RequiredInterfaces.NotImplementedError` when a concrete problem type + does not implement input validation. +""" +function validate end + +@required AbstractProblemDefinition begin + validate(::AbstractProblemDefinition) +end + +""" +$(SIGNATURES) + +Validate and normalize the options owned by a formulation type. + +The implementation that owns `FormulationType` defines a method for the type +itself. Dispatch requires an explicitly supported type. An unregistered formulation +raises `MethodError`. + +# Arguments + +- `owner`: formulation-type dispatch token. +- `options`: `FormulationOptions` supplied to the defining normalizer. Public + constructors also accept `options=(...)` shorthand and wrap it at entry. + +# Returns + +- A formulation-owned [`FormulationOptions`](@ref) record with a fixed set + of keys for the selected owner. +""" +function formulation_options end + +""" +$(TYPEDSIGNATURES) + +Allocate a selected formula's reusable arrays during computation initialization. +The arguments are the resolved selection, scalar type, completed numerical input, +plan of fixed indices and geometry, and existing buffer record. Return the extended +record without replacing another owner's storage. Array blocks contain no copied +geometry, material model, selection or validity state. The default uses the existing storage. No material law or integrand is evaluated here. +""" +function initialize_buffers end + +function initialize_buffers( + ::Union{AbstractFormulation, Nothing}, ::Type, input, plan, buffers) + buffers +end + +function initialize_buffers( + selections::Union{NamedTuple, Tuple}, ::Type{T}, input, plan, buffers) where {T} + return foldl(values(selections); init = buffers) do accumulated, selected + initialized = initialize_buffers(selected, T, input, plan, accumulated) + # Extension methods may append arrays but cannot replace another owner's + # storage. + retained = map(values(accumulated), + values(initialized[keys(accumulated)])) do before, after + before === after + end + all(retained) || throw(ArgumentError( + "buffer initialization replaced existing storage :$(keys(accumulated)[findfirst(!, retained)])")) + initialized + end +end + +""" +$(SIGNATURES) + +Validate and normalize the options owned by one computation. + +The implementation that owns `OwnerType` defines a method for the type itself. +`OwnerType` may identify a core solver or another composite computation owner. +Dispatch requires an explicitly supported type. An unregistered owner raises `MethodError`. + +# Arguments + +- `owner`: computation-owner dispatch token. +- `options`: `ComputationOptions` supplied to the defining normalizer. Public + compute calls also accept `options=(...)` shorthand and wrap it at entry. + +# Returns + +- A computation-owned [`ComputationOptions`](@ref) record with a fixed set + of outer keys for the selected owner. +""" +function computation_options end + +""" +$(SIGNATURES) + +Return supplemental output owned by one core or composite computation. + +The formulation type defines a method for itself and returns a +fixed-key [`ComputationDetails`](@ref) record. Dispatch requires an explicitly supported type. +An unregistered formulation raises `MethodError`. +""" +function computation_details end + +""" +$(SIGNATURES) + +Return the typed supplemental output retained by a completed +result. Result owners define narrow methods beside their result containers. +""" +function details end + +""" +$(SIGNATURES) + +Calculate a completed result from an explicit problem and formulation. + +Concrete solver methods validate and normalize execution `options` through +[`computation_options`](@ref) for their owner. Composite computations may +forward caller options to that solver for validation. Scientific choices use [`formulation_options`](@ref), and supplemental +results use [`computation_details`](@ref). Unsupported problem-formulation +pairs fail through ordinary Julia dispatch. +""" +function compute end + +""" +$(SIGNATURES) + +Return native numerical values selected from a completed scientific result. + +The selector and optional transform are function objects. Result owners define +the supported combinations beside their result representations. +""" +function observe end + +function _observe_macro_parts(request) + valid_request = request isa Expr && + request.head === :ref && + length(request.args) >= 2 + valid_request || throw(ArgumentError( + "@observe expects indexed `accessor[...]` or `(accessor, transform)[...]`; " * + "got `$(request)`.", + )) + + selector = first(request.args) + indices = request.args[2:end] + if selector isa Expr && selector.head === :tuple + length(selector.args) in (2, 3) || throw(ArgumentError( + "@observe selector tuples require two or three functions; " * + "got `$(selector)`.", + )) + return Tuple(selector.args), indices + end + return (selector,), indices +end + +""" + @observe accessor[indices...] + @observe (accessor, transform)[indices...] + @observe (statistics, quantity, statistic)[indices...] + +Construct a plain observable-request tuple without reading a result. +""" +macro observe(request) + selectors, indices = _observe_macro_parts(request) + parts = (selectors..., indices...) + return Expr(:tuple, map(esc, parts)...) +end + +""" + @observe source accessor[indices...] + @observe source (accessor, transform)[indices...] + @observe source (statistics, quantity, statistic)[indices...] + +Expand indexed observable syntax into an immediate [`observe`](@ref) call. The +indices follow the selected observable. Ordinary line quantities use row, +column, and sample indices. Diagonal transforms use mode and sample indices. +""" +macro observe(source, request) + selectors, indices = _observe_macro_parts(request) + parts = (source, selectors..., indices...) + escaped = map(esc, parts) + return :(observe($(escaped...))) +end + +""" +$(SIGNATURES) + +Publish explicitly requested scientific values for presentation or reporting. + +`observables(::Type{T})` declares the selectors supported by `T`. +`observables(source, requests; units, length_unit, frequency_unit, +quantity_units, clip, atol, frequencies)` returns one [`ObservedResult`](@ref), +or an ordinary vector for a result collection. Its four sections retain gridpoint +descriptions, quantities, completed errors, and recorded timings. Quantity-wise +tables are materialized by `ReportBuilder.tabulate` from these detached records. `units` is empty or +positionally aligned with `requests`. With `clip=true` (default), the result +owner's declared native-unit reporting resolution is applied before conversion. +`atol` optionally overrides that resolution. Multiple quantities require keyed +cutoffs. `frequencies` supplies standalone tensor context \\[Hz\\]. These cutoffs +are not certified floating-point error bounds. Each clipped value becomes exact +zero, including its uncertainty. Quantities lacking both a declared resolution and an explicit cutoff remain unchanged. `clip=false` retains raw values in +the requested display units. Absolute/relative error products are never clipped +using their operands' physical cutoffs. +""" +function observables end diff --git a/src/commons/matrixops.jl b/src/commons/matrixops.jl new file mode 100644 index 000000000..0739e0f03 --- /dev/null +++ b/src/commons/matrixops.jl @@ -0,0 +1,468 @@ +# Matrix reductions shared by the line-parameter backends. Primitive matrices are +# ordered by terminal. A `ReductionPlan` fixes the index operations once. + +""" +$(TYPEDSIGNATURES) + +Replace each cyclic diagonal of the square `matrix` by its mean, in place, and +return `matrix`. The result is the ideally transposed matrix: every phase occupies +every position for an equal share of the line length. +""" +function ideal_transposition!(matrix::AbstractMatrix) + n = checksquare(matrix) + coefficients = similar(diag(matrix)) + @inbounds for offset in 0:(n - 1) + total = zero(eltype(matrix)) + for row in 1:n + total += matrix[row, 1 + mod(row - 1 + offset, n)] + end + coefficients[offset + 1] = total / n + end + @inbounds for row in 1:n, column in 1:n + + matrix[row, column] = coefficients[mod1(column - row + 1, n)] + end + return matrix +end + +""" +$(TYPEDSIGNATURES) + +Return the terminal permutation that places the first conductor of each active +phase in encounter order, then the remaining conductors of each phase in the same +phase order, then the conductors whose `map` entry is zero. +""" +function reorder_indices(map::AbstractVector{<:Integer}) + n = length(map) + phases = Int[] # encounter order of active phase IDs + firsts = Int[] + sizehint!(firsts, n) + eliminated = Int[] # phase-zero conductors + sizehint!(eliminated, n) + tails = Dict{Int, Vector{Int}}() # phase => remaining indices + + seen = Set{Int}() + @inbounds for (i, p) in pairs(map) + if p > 0 + if !(p in seen) + push!(seen, p) + push!(phases, p) + push!(firsts, i) + else + push!(get!(tails, p, Int[]), i) + end + else + push!(eliminated, i) + end + end + + perm = Vector{Int}(undef, n) + k = 1 + @inbounds begin + for i in firsts + perm[k] = i + k += 1 + end + for p in phases + if haskey(tails, p) + for i in tails[p] + perm[k] = i + k += 1 + end + end + end + for i in eliminated + perm[k] = i + k += 1 + end + end + return perm +end + +""" +$(TYPEDSIGNATURES) + +Eliminate matrix rows and columns whose `phase_map` entry is zero. + +For retained indices `1` and eliminated indices `2`, calculate the Schur +complement + +```math +M_{\\mathrm{red}} = M_{11} - M_{12}M_{22}^{-1}M_{21}. +``` + +# Arguments + +- `M`: square complex matrix. +- `phase_map`: active-phase assignment for each row and column. Nonzero IDs + identify active phases. Zero marks a grounded or eliminated conductor. + +# Returns + +- The reduced matrix. +""" +function kron_reduce( + M::Matrix{Complex{T}}, + phase_map::Vector{Int} +) where {T <: Real} + retained = count(!=(0), phase_map) + reduced = similar(M, retained, retained) + kron_reduce!(M, phase_map, reduced) + return reduced +end + +""" +$(TYPEDSIGNATURES) + +Write the Kron-reduced matrix from [`kron_reduce`](@ref) into `Mred`. + +# Arguments + +- `M`: square complex matrix. +- `phase_map`: active-phase assignment for each row and column. Nonzero IDs + identify active phases. Zero marks a grounded or eliminated conductor. +- `Mred`: destination matrix. + +# Returns + +- `nothing`. +""" +function kron_reduce!( + M::Matrix{Complex{T}}, + phase_map::Vector{Int}, + Mred::Matrix{Complex{T}} +) where {T <: Real} + checksquare(M) == length(phase_map) || throw(DimensionMismatch( + "phase map must contain one entry per matrix row")) + keep = findall(!=(0), phase_map) + eliminate = findall(==(0), phase_map) + # This entry point also permits Mred to alias M. The reusable-buffer entry + # point below requires independent scratch, as in the computation workspace. + source = Base.unalias(Mred, M) + kron_reduce!(source, keep, eliminate, Mred, + similar(M, length(eliminate), length(eliminate)), + similar(M, length(keep), length(eliminate)), + similar(M, length(eliminate), length(keep))) + return nothing +end + +""" +$(TYPEDSIGNATURES) + +Write the Schur complement of `matrix` that retains the indices `keep` and +eliminates the indices `eliminate` into `reduced`, and return `reduced`. + +`factor`, `coupling` and `right_hand_side` are scratch blocks of sizes +`length(eliminate)²`, `length(keep)×length(eliminate)` and +`length(eliminate)×length(keep)`. The destination and the scratch blocks must not +alias `matrix` or one another. Without eliminated indices, `reduced` receives +`matrix[keep, keep]`. +""" +function kron_reduce!( + matrix::AbstractMatrix{Complex{T}}, + keep::AbstractVector{Int}, + eliminate::AbstractVector{Int}, + reduced::AbstractMatrix{Complex{T}}, + factor::AbstractMatrix{Complex{T}}, + coupling::AbstractMatrix{Complex{T}}, + right_hand_side::AbstractMatrix{Complex{T}} +) where {T <: Real} + retained = length(keep) + removed = length(eliminate) + size(reduced) == (retained, retained) || throw(DimensionMismatch( + "reduced matrix storage must be $retained×$retained" + )) + size(factor) == (removed, removed) || throw(DimensionMismatch( + "Kron factor storage must be $removed×$removed" + )) + size(coupling) == (retained, removed) || throw(DimensionMismatch( + "Kron coupling storage must be $retained×$removed" + )) + size(right_hand_side) == (removed, retained) || throw(DimensionMismatch( + "Kron right-hand-side storage must be $removed×$retained" + )) + @inbounds for column in 1:retained, row in 1:retained + + reduced[row, column] = matrix[keep[row], keep[column]] + end + iszero(removed) && return reduced + @inbounds for column in 1:removed, row in 1:removed + + factor[row, column] = matrix[eliminate[row], eliminate[column]] + end + @inbounds for column in 1:removed, row in 1:retained + + coupling[row, column] = matrix[keep[row], eliminate[column]] + end + @inbounds for column in 1:retained, row in 1:removed + + right_hand_side[row, column] = matrix[eliminate[row], keep[column]] + end + factorization = lu!(factor) + ldiv!(factorization, right_hand_side) + mul!( + reduced, + coupling, + right_hand_side, + -one(Complex{T}), + one(Complex{T}) + ) + return reduced +end + +""" +$(TYPEDSIGNATURES) + +Return the pairs `(first, duplicate)` that merge each further conductor of an +active phase into the first conductor of that phase, in the order of `phases`. +Zero entries take part in no pair. +""" +function bundle_operations(phases::AbstractVector{<:Integer}) + first_index = Dict{Int, Int}() + operations = Tuple{Int, Int}[] + @inbounds for (index, phase) in pairs(phases) + phase > 0 || continue + first = get(first_index, phase, 0) + if iszero(first) + first_index[phase] = index + else + push!(operations, (first, index)) + end + end + return operations +end + +""" +$(TYPEDSIGNATURES) + +Apply the bundle change of basis of the pairs from [`bundle_operations`](@ref) to +`matrix` in place, and return `matrix`. Each duplicate column loses the column of +its first conductor. Each duplicate row then loses the row of its first conductor. +""" +function merge_bundles!( + matrix::AbstractMatrix{T}, + operations::AbstractVector{<:Tuple{Int, Int}} +) where {T} + @inbounds for (first, duplicate) in operations + base_column = @view matrix[:, first] + column = @view matrix[:, duplicate] + if matrix isa StridedMatrix{T} && + T <: Union{Float32, Float64, ComplexF32, ComplexF64} + axpy!(-one(T), base_column, column) + else + column .-= base_column + end + end + @inbounds for (first, duplicate) in operations + base_row = @view matrix[first, :] + row = @view matrix[duplicate, :] + if matrix isa StridedMatrix{T} && + T <: Union{Float32, Float64, ComplexF32, ComplexF64} + axpy!(-one(T), base_row, row) + else + row .-= base_row + end + end + return matrix +end + +""" +$(TYPEDSIGNATURES) + +Apply the bundle change of basis to conductors assigned to the same active +phase. A zero assignment denotes an independent conductor selected for +grounded or eliminated-conductor reduction and is never interpreted as a bundle +identity. +""" +function merge_bundles!( + matrix::AbstractMatrix{T}, + phases::AbstractVector{<:Integer} +) where {T} + n = size(matrix, 1) + size(matrix, 2) == n == length(phases) || + throw(ArgumentError("shape mismatch")) + operations = bundle_operations(phases) + merge_bundles!(matrix, operations) + reduced = copy(phases) + @inbounds for (_, duplicate) in operations + reduced[duplicate] = 0 + end + return matrix, reduced +end + +""" +$(TYPEDEF) + +Fix the index operations that reduce primitive matrices, ordered by terminal, to the +matrices of the retained phases. The fields below apply in their listed order. + +$(TYPEDFIELDS) +""" +struct ReductionPlan + "Reordering of the primitive terminals, from [`reorder_indices`](@ref)." + permutation::Vector{Int} + "Bundle change-of-basis pairs in reordered indices, empty without bundle reduction." + bundles::Vector{Tuple{Int, Int}} + "Reordered indices retained by the Kron elimination." + keep::Vector{Int} + "Reordered indices eliminated by the Kron elimination." + eliminate::Vector{Int} + "Whether the retained matrices are ideally transposed." + transposition::Bool + "Phase assignment of each retained row." + phase_map::Vector{Int} + "Primitive terminal of each retained row." + indices::Vector{Int} +end + +""" +$(TYPEDSIGNATURES) + +Build the reduction plan of `phase_map`, the active-phase assignment of each +primitive terminal. Nonzero IDs identify active phases, and zero marks a grounded +or eliminated conductor. + +# Keywords + +- `reduce_bundle`: merge the conductors of each active phase into one. +- `kron_reduction`: eliminate the conductors with phase zero. +- `ideal_transposition`: average the retained matrices over cyclic transposition. +""" +function ReductionPlan(phase_map::AbstractVector{<:Integer}; reduce_bundle::Bool, + kron_reduction::Bool, ideal_transposition::Bool) + permutation = reorder_indices(phase_map) + ordered = phase_map[permutation] + reduced = copy(ordered) + seen = Set{Int}() + @inbounds for (index, phase) in pairs(ordered) + if phase > 0 && phase in seen + reduced[index] = 0 + elseif phase > 0 + push!(seen, phase) + end + end + # Without Kron reduction, bundle reduction still eliminates the merged + # duplicates and keeps the conductors with phase zero, marked -1. + retained = if reduce_bundle + kron_reduction ? reduced : + [ordered[index] == 0 ? -1 : reduced[index] for index in eachindex(reduced)] + else + kron_reduction ? ordered : nothing + end + bundles = reduce_bundle ? bundle_operations(ordered) : Tuple{Int, Int}[] + retained === nothing && return ReductionPlan(permutation, bundles, + collect(eachindex(ordered)), Int[], ideal_transposition, ordered, copy(permutation)) + keep = findall(!=(0), retained) + return ReductionPlan(permutation, bundles, keep, findall(==(0), retained), + ideal_transposition, retained[keep], permutation[keep]) +end + +""" +$(TYPEDEF) + +Hold the storage of [`reduce_line_matrices!`](@ref) for one [`ReductionPlan`](@ref) +and one element type. + +$(TYPEDFIELDS) +""" +struct ReductionBuffers{T <: Number} + "Reordered primitive matrix, reused for `Z` and `P`." + ordered::Matrix{T} + "Retained potential coefficients, factorized in place." + potential::Matrix{T} + "Kron elimination block of the eliminated indices." + factor::Matrix{T} + "Kron elimination block coupling retained to eliminated indices." + coupling::Matrix{T} + "Kron elimination right-hand side." + right_hand_side::Matrix{T} + "Identity of the retained size." + identity::Matrix{T} +end + +""" +$(TYPEDSIGNATURES) + +Allocate the storage of [`reduce_line_matrices!`](@ref) for `plan` with elements of +type `T`. +""" +function ReductionBuffers{T}(plan::ReductionPlan) where {T <: Number} + n, retained, removed = length(plan.permutation), length(plan.keep), length(plan.eliminate) + return ReductionBuffers{T}(Matrix{T}(undef, n, n), Matrix{T}(undef, retained, retained), + Matrix{T}(undef, removed, removed), Matrix{T}(undef, retained, removed), + Matrix{T}(undef, removed, retained), Matrix{T}(I, retained, retained)) +end + +""" +$(TYPEDSIGNATURES) + +Extend `buffers` with `reduction`, the storage of [`reduce_line_matrices!`](@ref) for +`reduction` with elements of type `T`. +""" +function initialize_buffers(reduction::ReductionPlan, ::Type{T}, input, plan, + buffers) where {T <: Number} + return merge(buffers, (reduction = ReductionBuffers{T}(reduction),)) +end + +""" +$(TYPEDSIGNATURES) + +Reduce one frequency of primitive line matrices to the retained phases of `plan`. + +`Zprimitive` \\[Ω/m\\] and `Pprimitive` \\[m/F\\] are ordered by terminal. Both are +reordered, merged by bundle and Kron-reduced. With ideal transposition, the +retained `Z` and `P` are averaged. `Z` receives the retained series impedance and +`Y` receives the shunt admittance ``Y = s P^{-1}`` \\[S/m\\] of the retained `P`, +with `s = jω`. `buffers` comes from [`ReductionBuffers`](@ref) for `plan`. + +With `diagnostics = Val(true)`, nonfinite `P` or `Y` throws `ArgumentError`, and the +return value is the infinity-norm residual of ``P P^{-1} - I`` and the 2-norm +condition number of `P`. A nonfinite condition estimate or a residual above +``\\max(\\sqrt{ε}, 32nε\\max(1, κ))`` produces a warning. Otherwise the return value +is `nothing`. +""" +function reduce_line_matrices!( + Z::AbstractMatrix{T}, + Y::AbstractMatrix{T}, + Zprimitive::AbstractMatrix, + Pprimitive::AbstractMatrix, + s::Number, + plan::ReductionPlan, + buffers::ReductionBuffers{T}, + diagnostics::Union{Val{true}, Val{false}} = Val(false) +) where {T <: Number} + function retain!(retained, primitive) + ordered = buffers.ordered + @inbounds for column in eachindex(plan.permutation), row in eachindex(plan.permutation) + ordered[row, column] = primitive[plan.permutation[row], plan.permutation[column]] + end + merge_bundles!(ordered, plan.bundles) + kron_reduce!(ordered, plan.keep, plan.eliminate, retained, + buffers.factor, buffers.coupling, buffers.right_hand_side) + plan.transposition && ideal_transposition!(retained) + return retained + end + retain!(Z, Zprimitive) + P = retain!(buffers.potential, Pprimitive) + if diagnostics === Val(false) + ldiv!(Y, lu!(P), buffers.identity) + Y .*= s + return nothing + end + all(isfinite, P) || throw(ArgumentError( + "retained potential coefficients contain nonfinite values")) + R = real(T) + condition_number = convert(R, cond(P)) + isfinite(condition_number) || + @warn "Potential-coefficient condition estimate is not finite" condition_number + # The reordered storage is free after the Kron elimination of P. + coefficients = copyto!(view(buffers.ordered, axes(P)...), P) + ldiv!(Y, lu!(P), buffers.identity) + all(isfinite, Y) || throw(ArgumentError("computed admittance contains nonfinite values")) + residual = convert(R, norm(coefficients * Y - buffers.identity, Inf)) + tolerance = max(sqrt(eps(R)), convert(R, 32size(Y, 1) * eps(R) * max(one(R), condition_number))) + isfinite(residual) && residual <= tolerance || + @warn "Potential-coefficient inversion residual target was not met" residual tolerance condition_number + Y .*= s + return (; residual, condition_number) +end diff --git a/src/commons/observables.jl b/src/commons/observables.jl new file mode 100644 index 000000000..c9799ab4c --- /dev/null +++ b/src/commons/observables.jl @@ -0,0 +1,294 @@ +""" +$(TYPEDSIGNATURES) + +Return the function-valued selector prefix encoded by one observable +request. Positional indices are omitted from the result. +""" +function request_identity(request) + request isa Function && return request + request isa Tuple && !isempty(request) || throw(ArgumentError( + "observable requests must be selector functions or nonempty tuples", + )) + count = findfirst(value -> !(value isa Function) || value isa Colon, request) + count = count === nothing ? length(request) : count - 1 + count > 0 || throw(ArgumentError("an observable request must begin with a selector function")) + return count == 1 ? first(request) : request[1:count] +end + +""" +$(TYPEDSIGNATURES) + +Normalize a selector prefix to the retained component selector used for lookup. +The input excludes positional indices. [`request_identity`](@ref) extracts that +prefix from a complete request. Scientific owners may extend this method for +accessor aliases. The default leaves selectors unchanged. `Val` selector names +also support keyed display-unit overrides such as `:alpha` and `:beta`. +""" +normalize_observation_selector(selector) = selector + +""" +$(TYPEDSIGNATURES) + +Return the [`LineCableModels.Units.Quantity`](@ref) encoded by a scientific +request. +""" +function request_quantity(request) + identity=request_identity(request) + return identity isa Tuple ? quantity(identity...) : quantity(identity) +end + +""" +$(TYPEDSIGNATURES) + +Return the positional indices encoded by a scientific request. +""" +function request_indices(request) + request isa Function && return () + identity = request_identity(request) + selector_count = identity isa Tuple ? length(identity) : 1 + return request[(selector_count + 1):end] +end + +function _observable_declaration(source) + supported = observables(typeof(source)) + supported isa Tuple || throw( + ArgumentError("observables($(typeof(source))) must return a tuple of selectors"), + ) + return supported +end + +""" +$(TYPEDSIGNATURES) + +Resolve one scientific request into its declared selector identity, physical +quantity, and positional indices. + +# Arguments + +- `source`: value whose type declares the supported observable identities. +- `request`: selector function or plain observable request tuple. + +# Returns + +- A named tuple containing `identity`, `quantity`, and `indices`. + +# Errors + +- `ArgumentError`: the declaration is malformed or the request is unsupported. +""" +function observation_request(source, request) + supported = _observable_declaration(source) + identity = request_identity(request) + identity in supported || throw(ArgumentError( + "$(typeof(source)) does not publish selector $(repr(identity))", + )) + selector_count = identity isa Tuple ? length(identity) : 1 + indices = request isa Function ? () : request[(selector_count + 1):end] + return (; identity, quantity = request_quantity(request), indices) +end + +""" +$(TYPEDSIGNATURES) + +Resolve an integer, range, vector, or colon selector against a dimension of +length `count`. + +# Returns + +- Concrete integer indices in selection order. + +# Errors + +- `ArgumentError`: the selector form is unsupported. +- `BoundsError`: at least one resolved index is outside `1:count`. +""" +function observation_indices(selector, count::Integer) + indices = if selector isa Colon + collect(1:Int(count)) + elseif selector isa Integer + [Int(selector)] + elseif selector isa AbstractRange || selector isa AbstractVector + collect(Int, selector) + else + throw(ArgumentError( + "observable indices must be integers, ranges, vectors, or `:`", + )) + end + all(index -> index in 1:count, indices) || throw(BoundsError(1:count, indices)) + return indices +end + +""" +$(TYPEDSIGNATURES) + +Return a plain observable request with `indices` and the selector identity from +`resolved`. +""" +function materialize_observation(resolved::NamedTuple, indices::Tuple) + identity = resolved.identity + prefix = identity isa Tuple ? identity : (identity,) + return (prefix..., indices...) +end + +""" +$(TYPEDSIGNATURES) + +Validate positional scientific requests and aligned display-unit overrides +against the observable declaration for `source`. + +# Arguments + +- `source`: result or retained product that owns observable selectors. +- `requests`: tuple of selector functions or selector tuples. +- `unit_overrides`: empty tuple or one display unit for each request. + +# Returns + +- A tuple containing the normalized selector identity for each request. + +# Errors + +- Throws `ArgumentError` when the declaration is malformed or a request is + unsupported. +- Throws `DimensionMismatch` when the display-unit tuple is not aligned with + the requests. +- A source that does not implement the declaration fails through ordinary + method dispatch. +""" +function validate_observables( + source, + requests::Tuple, + unit_overrides::Tuple = () +) + isempty(unit_overrides) || length(unit_overrides) == length(requests) || + throw( + DimensionMismatch("display units must align with observable requests"), + ) + allunique(requests) || throw(ArgumentError("observable requests must be distinct")) + return map(request -> observation_request(source, request).identity, requests) +end + +function _unit_override_keys(request, overrides) + identity = request_identity(request) + indices=request_indices(request) + selector=identity isa Tuple ? first(identity) : identity + names=selector isa Base.Fix2 ? () : selector isa Function ? (nameof(selector),) : () + prefix = identity isa Tuple && length(identity) > 2 ? (identity[1:2],) : () + component_selector=normalize_observation_selector(identity) + indexed_aliases=isempty(indices) ? () : Tuple(key for key in keys(overrides) if + key isa Tuple && !isempty(key) && first(key) isa Function && + isequal(normalize_observation_selector(request_identity(key)),component_selector) && + isequal(request_indices(key),indices) && !isequal(key,request)) + function_aliases=Tuple(key for key in keys(overrides) if key isa Function && + isequal(normalize_observation_selector(key),component_selector) && !isequal(key,identity)) + name_aliases=Tuple(key for key in keys(overrides) if key isa Symbol && + isequal(normalize_observation_selector(Val(key)),component_selector)) + bound=selector isa Base.Fix2 ? (selector,selector.f,nameof(selector.f)) : () + physical=identity isa Tuple && length(identity)>1 && identity[2] isa Function && + applicable(quantity,identity[2]) ? (identity[2],nameof(identity[2])) : () + return (request,indexed_aliases...,identity, prefix..., bound...,function_aliases...,name_aliases..., + names...,physical...) +end + +function _unit_override(overrides, request) + overrides === nothing && return nothing + overrides isa Union{Symbol, UnitExpr} && return overrides + overrides isa Union{NamedTuple, AbstractDict} || throw(ArgumentError( + "unit overrides must be a prefix, UnitExpr, keyed collection, or nothing", + )) + for override_key in _unit_override_keys(request,overrides) + if overrides isa NamedTuple + override_key isa Symbol && haskey(overrides, override_key) && + return overrides[override_key] + elseif haskey(overrides, override_key) + return overrides[override_key] + end + end + return nothing +end + +""" +$(TYPEDSIGNATURES) + +Resolve display-unit targets aligned with a tuple of scientific requests. + +# Arguments + +- `requests`: selector functions or selector tuples. +- `result_basis`: `:pul` or `:total`. + +# Keywords + +- `length_prefix`: metric prefix applied to per-length denominators. +- `overrides`: a global prefix or `UnitExpr`, a keyed collection of those values or `nothing`. + +# Returns + +- A tuple of concrete `UnitExpr` values aligned with `requests`. +""" +function unit_targets( + requests::Tuple, + result_basis::Symbol; + length_prefix::Symbol = :kilo, + overrides = nothing +) + return map(requests) do request + display_unit( + request_quantity(request), + result_basis, + _unit_override(overrides, request); + length_prefix + ) + end +end + +""" +$(TYPEDSIGNATURES) + +Detach an observed value from its result storage and apply a display-unit scale +factor. Structured result owners extend this method for their published value +types. +""" +detach(value::Number, factor) = value * factor +detach(value::AbstractFloat, factor::Real) = value * oftype(value, factor) +detach(value::Complex{T}, factor::Real) where {T<:AbstractFloat} = value * T(factor) +detach(value::Missing, factor) = missing +detach(value::Nothing, factor) = nothing +detach(values::NamedTuple, factor) = map(value -> detach(value, factor), values) +detach(values::AbstractArray, factor) = map(value -> detach(value, factor), values) + +""" +$(TYPEDSIGNATURES) + +Resolve the declared physical reporting resolution for one scientific request. +Result owners extend this operation. The fallback leaves precision unspecified. + +# Arguments + +- `source`: result defining the requested values and native physical basis. +- `request`: an observable selector or indexed request. + +# Keywords + +- `atol`: optional absolute reporting cutoff in the requested native units. +- `frequencies`: optional frequency context \\[Hz\\] for standalone tensors. + +# Returns + +- A record containing `kind`, native `atol` and `unit`, and detached + `unresolved` and `available` masks aligned with the requested values. + An unassessed request returns `nothing` for its cutoff and masks. Reporting cutoffs + are not certified numerical forward-error bounds. +""" +function observation_resolution(source, request; atol=nothing, frequencies=nothing) + return (kind=:unassessed, atol=nothing, unit=nothing, + unresolved=nothing, available=nothing) +end + +""" +$(TYPEDSIGNATURES) + +Construct detached atomic observations. For an ordinary collection, apply the +atomic constructor to each element. +""" +observables(source,requests::Tuple=();kwargs...) = ObservedResult(source,requests;kwargs...) diff --git a/src/commons/observed_validation.jl b/src/commons/observed_validation.jl new file mode 100644 index 000000000..e5d98725d --- /dev/null +++ b/src/commons/observed_validation.jl @@ -0,0 +1,188 @@ +# Validate retained quantity records consumed by tables, plots, and archives. +function _observed_fields(record,fields,what) + record isa NamedTuple && all(key -> haskey(record,key),fields) || + throw(ArgumentError("$what requires fields $(fields)")) +end + +function _observed_indices(indices,extent,name) + indices isa AbstractVector && allunique(indices) && all(i -> i isa Integer && !(i isa Bool) && 1<=i<=extent,indices) || + throw(ArgumentError("$name must contain distinct original indices within its extent")) +end + +function _validate_observed_quantity(product) + _observed_fields(product,(:request,:quantity,:family,:statistic,:values,:unit,:basis, + :coordinates,:thresholds,:available,:engineering_zero,:clipped,:missing_reason),"observed quantity") + request_quantity(product.request)==product.quantity || throw(ArgumentError("request and quantity disagree")) + product.family isa Symbol && product.statistic isa Symbol && product.clipped isa Bool || + throw(ArgumentError("family/statistic must be symbols and clipped must be Boolean")) + scale_factor(native_unit(product.quantity,product.basis),product.unit) + c=product.coordinates + _observed_fields(c,(:kind,:indices,:extent),"quantity coordinates") + c.extent isa Tuple && all(n -> n isa Integer && n>=0,c.extent) || throw(ArgumentError("invalid coordinate extent")) + c.indices isa Tuple || throw(ArgumentError("coordinate indices must be a tuple")) + declared=request_indices(product.request) + c.kind===:samples && length(declared)==length(c.indices)-1 && + (declared=(declared...,Colon())) + if !isempty(declared) && c.kind!==:array + length(declared)>=length(c.indices) && declared[1:length(c.indices)]==c.indices || + throw(ArgumentError("request selectors disagree with retained coordinate selectors")) + end + dims=if c.kind===:vector + _observed_fields(c,(:axis,:axis_label,:positions,:samples,:frequencies,:frequency_unit,:labels,:domain),"vector coordinates") + length(c.extent)==2 || throw(DimensionMismatch("vector extent requires coordinate and frequency axes")) + c.axis isa Symbol && c.axis_label isa AbstractString && c.domain isa Symbol || + throw(ArgumentError("vector axis requires physical labels and a domain")) + _observed_indices(c.positions,c.extent[1],"positions") + _observed_indices(c.samples,c.extent[2],"samples") + c.labels isa AbstractVector && length(c.labels)==c.extent[1] || + throw(DimensionMismatch("vector labels do not cover the original extent")) + c.frequencies===nothing || length(c.frequencies)==length(c.samples) || + throw(DimensionMismatch("frequency and sample counts differ")) + (length(c.positions),length(c.samples)) + elseif c.kind in (:matrix,:diagonal) || c.kind in (:samples,:histogram) && haskey(c,:rows) + _observed_fields(c,(:rows,:columns,:samples,:frequencies,:frequency_unit,:labels,:domain),"matrix coordinates") + length(c.extent)==3 || throw(DimensionMismatch("matrix extent requires three axes")) + _observed_indices(c.rows,c.extent[1],"rows") + _observed_indices(c.columns,c.extent[2],"columns") + _observed_indices(c.samples,c.extent[3],"samples") + length(c.labels)>=max(c.extent[1],c.extent[2]) || throw(DimensionMismatch("matrix labels do not cover the extent")) + if haskey(c,:column_labels) || haskey(c,:column_domain) + _observed_fields(c,(:column_labels,:column_domain),"mixed matrix coordinates") + c.kind===:matrix || throw(ArgumentError("mixed labels require matrix coordinates")) + c.column_labels isa AbstractVector && length(c.column_labels)>=c.extent[2] || + throw(DimensionMismatch("column labels do not cover the extent")) + c.column_domain isa Symbol || throw(ArgumentError("column domain must be a symbol")) + end + c.kind===:diagonal && c.rows!=c.columns && throw(ArgumentError("diagonal coordinates must agree")) + c.frequencies===nothing || length(c.frequencies)==length(c.samples) || throw(DimensionMismatch("frequency and sample counts differ")) + c.kind===:diagonal ? (length(c.rows),length(c.samples)) : (length(c.rows),length(c.columns),length(c.samples)) + elseif c.kind===:assemblies || c.kind in (:samples,:histogram) && haskey(c,:assemblies) + _observed_fields(c,(:assemblies,:labels,:frequencies,:frequency_unit),"assembly coordinates") + length(c.extent)==2 && c.extent[2]==1 || throw(DimensionMismatch("assembly extent requires one operating frequency")) + _observed_indices(c.assemblies,c.extent[1],"assemblies") + length(c.labels)==c.extent[1] && allunique(c.labels) || throw(ArgumentError("assembly labels must identify every assembly uniquely")) + length(c.frequencies)==1 || throw(DimensionMismatch("assemblies require one operating frequency")) + (length(c.assemblies),) + elseif c.kind===:array + c.extent + else + throw(ArgumentError("unsupported observed coordinate kind $(c.kind)")) + end + if haskey(c,:frequency_unit) + scale_factor(c.frequency_unit,Units.units(:base,:hertz)) + c.frequencies===nothing || all(f -> f isa Real && isfinite(nominal(f)) && nominal(f)>=0,c.frequencies) || + throw(ArgumentError("frequency coordinates must be finite and nonnegative")) + end + if c.kind===:samples + _observed_fields(c,(:trials,),"sample coordinates") + allunique(c.trials) && all(t -> t isa Integer && !(t isa Bool) && t>0,c.trials) || + throw(ArgumentError("trial coordinates must be distinct positive integers")) + dims=(dims...,length(c.trials)) + end + if c.kind===:histogram + _observed_fields(product.values,(:lower,:upper,:density,:probability,:count),"histogram values") + n=length(product.values.lower) + all(v -> v isa AbstractVector && length(v)==n,Base.values(product.values)) || throw(DimensionMismatch("histogram column lengths differ")) + _observed_fields(product,(:distribution,:ordinate_units),"histogram product") + _observed_fields(product.distribution,(:edges,:empirical_cdf,:model_cdf,:qq),"histogram distribution") + length(product.distribution.edges)==n+1 || throw(DimensionMismatch("histogram edges and bins differ")) + for cdf in (product.distribution.empirical_cdf,product.distribution.model_cdf) + cdf===nothing && continue + _observed_fields(cdf,(:x,:y),"CDF coordinates") + cdf.x isa AbstractVector && cdf.y isa AbstractVector && length(cdf.x)==length(cdf.y) || + throw(DimensionMismatch("CDF coordinate lengths differ")) + end + qq=product.distribution.qq + if qq!==nothing + _observed_fields(qq,(:model,:sample,:reference),"Q–Q coordinates") + qq.model isa AbstractVector && qq.sample isa AbstractVector && length(qq.model)==length(qq.sample) || + throw(DimensionMismatch("Q–Q model and sample coordinate lengths differ")) + qq.reference isa Tuple && length(qq.reference)==2 || throw(ArgumentError("Q–Q identity line requires two endpoints")) + end + _observed_fields(product.ordinate_units,(:density,:probability,:count),"histogram ordinate units") + scale_factor(inv(product.unit),product.ordinate_units.density) + for unit in (product.ordinate_units.probability,product.ordinate_units.count) + scale_factor(Units.units(:base,:dimensionless),unit) + end + product.available isa Bool && product.engineering_zero isa Bool && product.missing_reason===nothing || + throw(ArgumentError("histogram availability must be scalar")) + product.thresholds===nothing || throw(ArgumentError("histograms cannot carry primary clipping thresholds")) + return nothing + end + values=product.values + values isa Union{Number,Missing,AbstractArray} || throw(ArgumentError("numerical products require scalar or array values")) + (values isa AbstractArray ? length(values) : 1)==prod(dims) || throw(DimensionMismatch("value count differs from retained coordinates")) + actual=values isa AbstractArray ? size(values) : () + reduced=length(c.indices)==length(dims) ? Tuple(n for (n,i) in zip(dims,c.indices) if !(i isa Integer)) : dims + actual in (dims,reduced) || throw(DimensionMismatch("value shape differs from retained coordinate axes")) + for (field,allowed) in ((:available,x -> x isa Bool),(:engineering_zero,x -> x isa Bool), + (:missing_reason,x -> x===nothing || x isa Symbol)) + value=getproperty(product,field) + value===nothing && continue + if value isa AbstractArray + size(value)==actual || (length(value)==1 && isempty(actual)) || throw(DimensionMismatch("$field shape differs from values")) + all(allowed,value) || throw(ArgumentError("invalid $field entries")) + else + allowed(value) || throw(ArgumentError("invalid $field")) + end + end + if product.thresholds!==nothing + _observed_fields(product.thresholds,(:kind,:values,:unit),"quantity thresholds") + # Phase eligibility retains physical Cartesian thresholds, not angles. + threshold_unit=product.thresholds.unit + phase=request_identity(product.request) isa Tuple && angle in request_identity(product.request) + threshold_quantity=phase ? quantity(first(request_identity(product.request))) : product.quantity + scale_factor(native_unit(threshold_quantity,product.basis),threshold_unit) + samples=haskey(c,:samples) ? length(c.samples) : nothing + _validate_observed_cutoff(product.thresholds.values,samples) + end + return nothing +end + +function _validate_observed_cutoff(value,samples) + value===nothing && return nothing + if value isa NamedTuple + foreach(v -> _validate_observed_cutoff(v,samples),values(value)) + elseif value isa AbstractVector + samples===nothing || length(value)==samples || throw(DimensionMismatch("cutoff count differs from samples")) + foreach(v -> _validate_observed_cutoff(v,nothing),value) + else + value isa Real && isfinite(nominal(value)) && nominal(value)>=0 || throw(ArgumentError("cutoffs must be finite and nonnegative")) + end + return nothing +end + +function _validate_observed_comparison(row) + _observed_fields(row,(:result_id,:reference_id,:request,:quantity,:statistic,:band, + :normalization,:absolute,:relative,:absolute_unit,:relative_unit,:coordinates,:settings,:maxima),"completed comparison") + for id in (row.result_id,row.reference_id) + _validate_observed_id(id) + end + _observed_fields(row.settings,(:basis,:indices,:sample_count,:status,:normalization_reason),"comparison settings") + _observed_fields(row.maxima,(:absolute,:relative),"comparison maxima") + request_quantity(row.request)==row.quantity || throw(ArgumentError("comparison request and quantity disagree")) + row.absolute isa AbstractMatrix && row.relative isa AbstractMatrix && size(row.absolute)==size(row.relative) || + throw(DimensionMismatch("comparison matrices must have matching extents")) + length(row.coordinates)==size(row.absolute,1)==size(row.absolute,2) || + throw(DimensionMismatch("comparison coordinates and matrices disagree")) + size(row.settings.status)==size(row.absolute)==size(row.settings.normalization_reason) || + throw(DimensionMismatch("comparison status and reason matrices must match the error matrices")) + row.settings.sample_count==length(row.settings.indices) || throw(DimensionMismatch("comparison sample count disagrees with indices")) + allunique(row.settings.indices) && all(i -> i isa Integer && !(i isa Bool) && i>0,row.settings.indices) || + throw(ArgumentError("comparison sample indices must be distinct positive integers")) + for maximum in (row.maxima.absolute,row.maxima.relative) + _observed_fields(maximum,(:value,:index),"comparison maximum") + end + all(ismissing.(row.absolute).==ismissing.(row.relative)) || throw(ArgumentError("absolute and relative errors must share eligibility")) + scale_factor(native_unit(row.quantity,row.settings.basis),row.absolute_unit) + scale_factor(Units.units(:base,:dimensionless),row.relative_unit) + return nothing +end + +function _validate_observed_id(id) + _observed_fields(id,(:source_id,:problem_index,:formulation_index),"gridpoint identity") + id.source_id===nothing && throw(ArgumentError("identified gridpoints require a source identity")) + all(i -> i isa Integer && !(i isa Bool) && i>0,(id.problem_index,id.formulation_index)) || + throw(ArgumentError("point and formulation indices must be positive integers")) + return nothing +end diff --git a/src/commons/observedresult.jl b/src/commons/observedresult.jl new file mode 100644 index 000000000..16d20222f --- /dev/null +++ b/src/commons/observedresult.jl @@ -0,0 +1,499 @@ +""" +$(TYPEDEF) + +Retain detached scientific products for one completed gridpoint. Collections +are ordinary vectors of these objects. + +$(TYPEDFIELDS) +""" +struct ObservedResult + "Original identity, physical inputs, selections, and uncertainty interpretation." + gridpoint::NamedTuple + "Requested numerical products, coordinates, units, and availability records." + quantities::Vector{NamedTuple} + "Completed comparisons with result and separate reference identities." + errors::Vector{NamedTuple} + "Completed execution and performance measurements in their original scopes." + timings::NamedTuple + + function ObservedResult(gridpoint::NamedTuple,quantities::AbstractVector, + errors::AbstractVector,timings::NamedTuple) + foreach(_validate_observed_quantity,quantities) + allunique(q.request for q in quantities) || throw(ArgumentError("retained requests must be distinct")) + _observed_fields(gridpoint,(:id,),"gridpoint") + id=gridpoint.id + id===nothing || _validate_observed_id(id) + haskey(timings,:result_id) && timings.result_id!=id && + throw(ArgumentError("recorded timings must identify this result")) + all(row -> row isa NamedTuple && haskey(row,:result_id) && + haskey(row,:reference_id) && id!==nothing && row.result_id==id,errors) || + throw(ArgumentError("completed comparisons must identify this result and a separate reference")) + foreach(_validate_observed_comparison,errors) + allunique((row.reference_id,row.request,row.band,row.normalization) for row in errors) || + throw(ArgumentError("completed comparisons must be distinct")) + return new(detach(gridpoint),NamedTuple[detach(q) for q in quantities], + NamedTuple[detach(row) for row in errors],detach(timings)) + end +end + +""" +$(TYPEDSIGNATURES) + +Distinguish a bare selector or one composed or indexed request from a tuple of +requests using the source's declared observable identities. This operation +only resolves syntax. [`observation_requests`](@ref) resolves scientific meaning. +""" +function observation_selection(source,selection) + selection===nothing && return () + selection isa Function && return (selection,) + selection isa Tuple || throw(ArgumentError("selection must be a selector or tuple of requests")) + isempty(selection) && return () + if first(selection) isa Function + any(item -> item isa Tuple && !isempty(item) && first(item) isa Function, + selection[2:end]) && return selection + identity=request_identity(selection) + declared=source isa ObservedResult ? Tuple(request_identity(q.request) for q in source.quantities) : observables(typeof(source)) + normalize_observation_selector(identity) in normalize_observation_selector.(declared) && + (identity isa Tuple || !isempty(request_indices(selection))) && return (selection,) + end + return selection +end + +""" +$(TYPEDSIGNATURES) + +Normalize requests for retained observations. Concrete scientific +owners enforce their complete representations. `complete_pairs=true` is used +by raw display conveniences. An observed-input consumer only selects retained +products. The return record separates `retained` from `displayed` requests. +""" +function observation_requests(source,requests::Tuple;complete_pairs::Bool=false) + retained=isempty(requests) ? observables(typeof(source)) : requests + validate_observables(source,retained,()) + return (;retained,displayed=retained) +end + +function observation_requests(source::ObservedResult,requests::Tuple;complete_pairs::Bool=false) + identities=request_identity.(getproperty.(source.quantities,:request)) + selected=isempty(requests) ? Tuple(count(==(identity),identities)==1 ? identity : product.request + for (identity,product) in zip(identities,source.quantities)) : requests + displayed=Any[] + for request in selected + identity=request_identity(request) + component_selector=normalize_observation_selector(identity) + if !isequal(component_selector,identity) + prefix=component_selector isa Tuple ? component_selector : (component_selector,) + selection=(prefix...,request_indices(request)...) + observation_product(source,selection) + push!(displayed,selection) + continue + end + selector=identity isa Base.Fix2 ? identity.f : identity + family=identity isa Function ? nameof(selector) : nothing + products=filter(source.quantities) do product + prefix=request_identity(product.request) + retained_selector=prefix isa Tuple ? first(prefix) : prefix + get(product,:family,nothing)===family && + get(product,:statistic,nothing)===:value && + (identity isa Base.Fix2 ? isequal(retained_selector,identity) : + !(retained_selector isa Base.Fix2)) + end + if isempty(products) + observation_product(source,request) + push!(displayed,request) + else + for product in products + indices=request_indices(request) + prefix=request_identity(product.request) + selector=prefix isa Tuple ? prefix : (prefix,) + selection=isempty(indices) ? prefix : (selector...,indices...) + observation_product(source,selection) + push!(displayed,selection) + end + end + end + result=Tuple(displayed) + allunique(result) || throw(ArgumentError("observation requests must be distinct")) + return (retained=result,displayed=result) +end + +""" +$(TYPEDSIGNATURES) + +Extract one owned numerical product with coordinates and units. Scientific +owners extend this operation to describe their tensor coordinates and apply +their numerical resolution. The fallback provides ordinary array coordinates. +""" +function observation_quantity(source,request;unit=nothing,clip=true,atol=nothing,frequencies=nothing) + values=if request isa Function + observe(source,request) + elseif request isa Tuple + observe(source,request...) + else + throw(ArgumentError("observable requests must be selector functions or nonempty tuples")) + end + q=request_quantity(request) + native=native_unit(q,basis(source)) + displayed=unit===nothing ? display_unit(q,basis(source)) : unit + coordinate=(kind=:array,indices=request_indices(request), + extent=values isa AbstractArray ? size(values) : (),) + return (request,quantity=q,family=:other,statistic=:value, + values=detach(values,scale_factor(native,displayed)),unit=displayed, + basis=basis(source),coordinates=coordinate,thresholds=nothing, + available=nothing,engineering_zero=nothing,clipped=false,missing_reason=nothing) +end + +""" +$(TYPEDSIGNATURES) + +Construct one detached observation from a completed primary result and already +completed comparison and timing records. + +# Keywords + +- `comparisons=()`: completed records identifying this result and its reference. +- `timings=(;)`: recorded measurements. Absent evidence stays absent. +- `gridpoint=nothing`: explicit description for an external numerical source. +- `clip=true`, `atol=nothing`: replace assessed values at or below the native-unit + cutoffs with exact zero, including zero uncertainty. Unresolved phase is + unavailable. Source results and values outside the cutoffs are unchanged. +- `length_unit=:kilo`, `frequency_unit=:base`: display-unit prefixes. +- `units=()`, `quantity_units=nothing`: aligned or quantity-keyed unit overrides. +- `frequencies=nothing`: frequency context \\[Hz\\] for standalone numerical tensors. +- `complete_pairs=false`: complete a raw display selection through the shared normalizer. +""" +function ObservedResult(source,requests::Tuple=();comparisons=(),timings=(;),gridpoint=nothing, + clip::Bool=true,atol=nothing,units::Tuple=(),length_unit::Symbol=:kilo, + frequency_unit::Symbol=:base,quantity_units=nothing,frequencies=nothing, + complete_pairs::Bool=false) + selected=observation_requests(source,requests;complete_pairs).retained + isempty(units) || length(units)==length(selected) || throw(DimensionMismatch( + "one display unit is required for each retained quantity")) + isempty(units) || quantity_units===nothing || throw(ArgumentError("use units or quantity_units, not both")) + atol isa Real && length(unique(request_quantity.(selected)))>1 && throw(ArgumentError( + "multiple quantities require component-keyed native-unit cutoffs")) + description=gridpoint===nothing ? observation_gridpoint(source) : gridpoint + quantities=map(eachindex(selected)) do index + request=selected[index] + q=request_quantity(request) + unit=isempty(units) ? q isa Units.Quantity{:frequency} ? Units.units(frequency_unit,:hertz) : + display_unit(q,basis(source),_unit_override(quantity_units,request);length_prefix=length_unit) : + display_unit(q,basis(source),units[index];length_prefix=length_unit) + record=observation_quantity(source,request;unit,clip,atol,frequencies) + if haskey(record.coordinates,:frequencies) && record.coordinates.frequencies!==nothing + frequency_target=Units.units(frequency_unit,:hertz) + f=record.coordinates.frequencies + factor=isempty(f) ? 1 : scale_factor(Units.units(:base,:hertz),frequency_target,typeof(float(nominal(first(f))))) + record=merge(record,(coordinates=merge(record.coordinates, + (frequencies=detach(f,factor),frequency_unit=frequency_target)),)) + end + record + end + return ObservedResult(description,quantities,collect(comparisons),timings) +end + +function observation_request(observed::ObservedResult,request) + identity=request_identity(request) + any(q -> isequal(normalize_observation_selector(request_identity(q.request)), + normalize_observation_selector(identity)),observed.quantities) || + throw(ArgumentError("the requested quantity was not retained")) + return (;identity,quantity=request_quantity(request),indices=request_indices(request)) +end + +""" +$(TYPEDSIGNATURES) + +Read a retained quantity in its recorded unit. Selection uses original +coordinates. No source extraction, clipping, or absent-quantity derivation occurs. +""" +function observe(observed::ObservedResult,selectors...) + request=length(selectors)==1 ? only(selectors) : selectors + return detach(observation_product(observed,request).values) +end + +function basis(observed::ObservedResult) + bases=unique(q.basis for q in observed.quantities) + length(bases)==1 || throw(ArgumentError("observation does not have one quantity basis")) + return only(bases) +end + +observation_gridpoint(observed::ObservedResult) = detach(observed.gridpoint) +function Base.show(io::IO,observed::ObservedResult) + print(io,"ObservedResult(",length(observed.quantities)," quantities, ",length(observed.errors)," comparisons)") +end +Base.show(io::IO,::MIME"text/plain",observed::ObservedResult) = show(io,observed) +Base.summary(io::IO,observed::ObservedResult) = show(io,observed) + +""" +$(TYPEDSIGNATURES) + +Lift atomic observation over an ordinary collection. Completed comparisons and +point timings are joined by original identities before detachment, so filtering +and reordering cannot associate a result with another point's evidence. +""" +function observables(sources::Union{AbstractVector,Tuple,AbstractResultSpace},requests::Tuple=(); + comparisons=nothing,timings=nothing,kwargs...) + return map(collect(sources)) do source + id=get(observation_gridpoint(source),:id,nothing) + errors=comparisons===nothing ? (source isa ObservedResult ? source.errors : ()) : + filter(record -> record.result_id==id,comparisons) + recorded=timings===nothing ? (source isa ObservedResult ? source.timings : (;)) : timings isa NamedTuple ? timings : begin + matches=filter(record -> record.result_id==id,timings) + length(matches)<=1 || throw(ArgumentError("multiple timing records for one result identity")) + isempty(matches) ? (;) : only(matches) + end + ObservedResult(source,requests;comparisons=errors,timings=recorded,kwargs...) + end +end + +""" +$(TYPEDSIGNATURES) + +Select a retained quantity record and optional original coordinates. Missing or +ambiguous requests fail. +""" +function observation_product(observed::ObservedResult,request;unit=nothing,frequency_unit=nothing) + identity=normalize_observation_selector(request_identity(request)) + matches=filter(q -> isequal(normalize_observation_selector(request_identity(q.request)),identity), + observed.quantities) + length(matches)>1 && (matches=filter(q -> q.request==request,matches)) + length(matches)==1 || throw(ArgumentError("requested retained product is absent or ambiguous")) + product=only(matches) + indices=request_indices(request) + isempty(indices) || request==product.request || (product=_selected_product(product,request,indices)) + return _reexpress_product(product;unit,frequency_unit) +end + +function _selected_product(product,request,indices) + c=product.coordinates + c.kind in (:matrix,:diagonal,:vector,:assemblies,:samples) || throw(ArgumentError("select this retained product by its complete request")) + matrix=haskey(c,:rows) + dimensions=c.kind===:vector ? (c.positions,c.samples) : + matrix ? c.kind===:diagonal ? (c.rows,c.samples) : (c.rows,c.columns,c.samples) : (c.assemblies,) + if c.kind===:samples + dimensions=(dimensions...,c.trials) + length(indices)==length(dimensions)-1 && (indices=(indices...,Colon())) + end + length(indices)==length(dimensions) || throw(DimensionMismatch("request rank differs from retained coordinates")) + positions=map(indices,dimensions) do requested,retained + wanted=requested isa Colon ? retained : requested isa Integer ? [requested] : collect(requested) + selected=map(wanted) do index + position=findfirst(==(index),retained) + position===nothing && throw(ArgumentError("coordinate $index was not retained")) + position + end + requested isa Integer ? only(selected) : selected + end + selected_dimensions=map(dimensions,positions) do dimension,position + position isa Integer ? [dimension[position]] : dimension[position] + end + select_values(value)=value isa AbstractArray ? reshape(value,length.(dimensions)...)[positions...] : value + sample_axis=c.kind===:vector || c.kind===:diagonal ? 2 : matrix ? 3 : nothing + f=if c.frequencies===nothing || sample_axis===nothing + c.frequencies + else + selected_samples=positions[sample_axis] + c.frequencies[selected_samples isa Integer ? [selected_samples] : selected_samples] + end + coordinate=c.kind===:vector ? merge(c,(indices,positions=first(selected_dimensions), + samples=last(selected_dimensions),frequencies=f)) : + matrix ? merge(c,(indices,rows=first(selected_dimensions), + columns=c.kind===:diagonal ? first(selected_dimensions) : selected_dimensions[2], + samples=selected_dimensions[sample_axis],frequencies=f)) : + merge(c,(indices,assemblies=first(selected_dimensions))) + c.kind===:samples && (coordinate=merge(coordinate,(trials=last(selected_dimensions),))) + thresholds=product.thresholds + if thresholds!==nothing && sample_axis!==nothing + select_cutoff(cutoff::AbstractVector)=cutoff[positions[sample_axis]] + select_cutoff(cutoff::NamedTuple)=map(select_cutoff,cutoff) + select_cutoff(cutoff)=cutoff + thresholds=merge(thresholds,(values=select_cutoff(thresholds.values),)) + end + components=get(product,:unavailable_components,nothing) + components===nothing || (components=merge(components,(nominal_magnitude=select_values(components.nominal_magnitude), + real=select_values(components.real),imaginary=select_values(components.imaginary)))) + result=merge(product,(request,values=select_values(product.values),coordinates=coordinate, + available=select_values(product.available),engineering_zero=select_values(product.engineering_zero), + missing_reason=select_values(product.missing_reason),thresholds)) + return haskey(product,:unavailable_components) ? merge(result,(unavailable_components=components,)) : result +end + +# Numeric equality also checks the uncertainty graph. For a dependency-aware +# number, equal marginal deviations alone do not make the difference certain. +_same_observed_values(a::Number,b::Number) = isequal(nominal(a),nominal(b)) && + isequal(uncertainty(a),uncertainty(b)) && iszero(uncertainty(a-b)) +_same_observed_values(a::AbstractArray,b::AbstractArray) = size(a)==size(b) && all(_same_observed_values.(a,b)) +_same_observed_values(a::Tuple,b::Tuple) = length(a)==length(b) && all(_same_observed_values(x,y) for (x,y) in zip(a,b)) +_same_observed_values(a::NamedTuple,b::NamedTuple) = keys(a)==keys(b) && all(_same_observed_values(x,y) for (x,y) in zip(values(a),values(b))) +_same_observed_values(a,b) = isequal(a,b) + +_same_observed_dependencies(a::Number,b::Number) = iszero(uncertainty(a-b)) +_same_observed_dependencies(a::AbstractArray,b::AbstractArray) = size(a)==size(b) && all(_same_observed_dependencies.(a,b)) +_same_observed_dependencies(a::NamedTuple,b::NamedTuple) = keys(a)==keys(b) && all(_same_observed_dependencies(x,y) for (x,y) in zip(values(a),values(b))) +_same_observed_dependencies(a,b) = true + +""" +$(TYPEDSIGNATURES) + +Return display groups and every original member identity for one retained +request. Eligibility is established by physical-point identity, owner-selected +formulations and controls, statistical interpretation, coordinates, and applied +cutoffs. Exact numerical and uncertainty-dependency agreement only verifies an +already established equivalence. Unidentified assumptions remain separate. +""" +function observation_groups(observed;request,band=nothing,normalization=nothing,reference=nothing) + points=observed isa ObservedResult ? [observed] : observed + groups=NamedTuple[] + keys=Any[] + products=Any[] + for (index,point) in enumerate(points) + product=if band===nothing + observation_product(point,request) + else + matches=filter(row -> row.request==request && isequal(row.band,band) && + row.normalization==normalization && (reference===nothing || row.reference_id==reference),point.errors) + length(matches)==1 || throw(ArgumentError("requested completed comparison is absent or ambiguous")) + row=only(matches) + (quantity=row.quantity,statistic=row.statistic,coordinates=row.coordinates, + basis=get(row.settings,:basis,nothing),unit=row.absolute_unit, + thresholds=(reference=get(row.settings,:atol,nothing),result=get(row.settings,:result_atol,nothing)), + available=get(row.settings,:status,nothing),engineering_zero=nothing, + assumptions=get(row,:assumptions,nothing),values=(absolute=row.absolute,relative=row.relative), + interpretation=get(row.settings,:estimators,nothing)) + end + id=get(point.gridpoint,:id,nothing) + assumptions=get(product,:assumptions,nothing) + physical=id===nothing ? nothing : (id.source_id,id.problem_index) + identity=band===nothing ? request_identity(product.request) : request_identity(request) + selector=identity isa Tuple ? first(identity) : identity + key=(physical,assumptions,selector isa Base.Fix2 ? identity : nothing, + product.quantity,product.statistic, + get(point.gridpoint,:uncertainty,nothing),product.coordinates,product.basis, + product.unit,product.thresholds, + band,normalization,reference,get(product,:interpretation,nothing)) + semantic=physical===nothing || assumptions===nothing || ismissing(assumptions) ? Int[] : + findall(i -> _same_observed_values(keys[i],key),eachindex(keys)) + # The uncertainty interpretation includes the actual dependency graph. + # Equal scalar means alone do not establish that interpretation. + same_interpretation=filter(i -> _same_observed_dependencies(products[i].values,product.values),semantic) + for i in same_interpretation + _same_observed_values(products[i].values,product.values) || throw(ArgumentError( + "semantically equivalent observations have conflicting numerical values")) + end + matched=isempty(same_interpretation) ? nothing : first(same_interpretation) + if matched===nothing + push!(keys,key);push!(products,product) + push!(groups,(representative=index,members=[index],identities=[id])) + else + push!(groups[matched].members,index) + push!(groups[matched].identities,id) + end + end + return groups +end + +function _description_values!(output,path,value;name=path,unit="",indices=(),text=nothing) + if value isa NamedTuple + descriptions=get(value,:field_descriptions,(;)) + for (key,child) in pairs(value) + key in (:frequencies,:field_descriptions,:kind,:system_id,:cable_id) && continue + child_path=isempty(path) ? string(key) : path*"."*string(key) + field=get(descriptions,key,nothing) + child_name=(field===nothing ? string(key) : field.name)*join("[$i]" for i in indices) + _description_values!(output,child_path,child;name=child_name, + unit=field===nothing ? unit : field.unit,indices, + text=field===nothing ? nothing : get(field,:text,nothing)) + end + elseif value isa Union{AbstractArray,Tuple} + for (index,child) in enumerate(value) + suffix="["*string(index)*"]" + _description_values!(output,path*suffix,child;name=name*suffix,unit,indices=(indices...,index),text) + end + elseif value isa Union{Number,AbstractString,Symbol} + output[path]=(;value,name,unit,text) + end + return output +end + +""" +Describe differences in captured physical inputs, active methods, and individual +controls. Labels use the scientific descriptions captured when the computation +completed. Gridpoint IDs are available in the observation metadata. +`fallback` supplies text when no captured description or varying field is +available. `nothing` retains the default positional result label. +""" +function observation_labels(observed;request=nothing,fallback=nothing) + points=observed isa ObservedResult ? [observed] : observed + isempty(points) && return String[] + descriptions=[begin + values=_description_values!(Dict{String,Any}(),"",get(point.gridpoint,:inputs,nothing)) + uncertainty=get(point.gridpoint,:uncertainty,nothing) + annotations=get(point.gridpoint,:uncertainty_descriptions,nothing) + if uncertainty isa NamedTuple && !isempty(uncertainty) + annotations!==nothing && all(key -> haskey(annotations,key),keys(uncertainty)) || + throw(ArgumentError("retained uncertainty descriptions are absent; construct observations through the current UQ owner before plotting")) + uncertainty=merge(uncertainty,(field_descriptions=annotations,)) + end + _description_values!(values,"uncertainty",uncertainty) + values + end for point in points] + paths=sort(unique(collect(Iterators.flatten(keys(record) for record in descriptions)))) + varying=filter(paths) do path + haskey(first(descriptions),path) || return true + original=first(descriptions)[path] + any(record -> !haskey(record,path) || + !isequal(record[path].value,original.value) || record[path].unit!=original.unit,descriptions) + end + fields=map(points) do point + retained=get(point.gridpoint,:formulation_fields,(;)) + quantity=request===nothing ? nothing : request_quantity(request) + family=quantity===nothing || isempty(retained) || !applicable(Units.family,quantity) ? + :all : Units.family(quantity)===Val(:series) ? :Z : :Y + entries=get(retained,family,()) + all(field -> haskey(field,:meaning) && haskey(field,:control_fields),entries) || + throw(ArgumentError("retained formulation descriptions lack individual control meanings; capture descriptions with the current completion owner before plotting")) + entries + end + # Only active peers establish a varying selection. A missing backend slot + # is not an equal selection: the backend summary identifies that difference. + peers(field)=[other for entries in fields for other in entries if other.meaning==field.meaning] + varies(field)=any(other -> !isequal(other.selection.identifier,field.selection.identifier),peers(field)) + return map(eachindex(points)) do index + parts=String[] + methods=filter(varies,fields[index]) + for field in methods + # When differing child methods already identify the computation, use + # the backend itself instead of repeating its method summary. + text=isempty(field.meaning) && all(other -> isempty(other.meaning),methods) ? field.summary : field.value + if !isempty(field.name) && count(other -> !isempty(other.meaning),methods)>1 + text=field.name*"="*text + end + push!(parts,text) + end + for field in fields[index], control in field.control_fields + active=[other for peer in peers(field) for other in peer.control_fields + if other.scope==control.scope] + any(other -> !isequal(other.value,control.value),active) || continue + text=control.text + isempty(field.name) || (text=field.name*": "*text) + push!(parts,text) + end + for path in varying + haskey(descriptions[index],path) || continue + field=descriptions[index][path] + push!(parts,field.text===nothing ? + field.name*"="*string(field.value)*(isempty(field.unit) ? "" : " "*field.unit) : field.text) + end + if isempty(parts) + roots=filter(field -> isempty(field.meaning),fields[index]) + if !isempty(roots) + append!(parts,(field.summary for field in roots)) + else + source_name=get(points[index].gridpoint,:name,nothing) + push!(parts,source_name===nothing ? + (fallback===nothing ? "Result $index" : string(fallback)) : string(source_name)) + end + end + join(parts,", ") + end +end diff --git a/src/commons/results.jl b/src/commons/results.jl new file mode 100644 index 000000000..5ff11ece8 --- /dev/null +++ b/src/commons/results.jl @@ -0,0 +1,40 @@ +""" +$(TYPEDSIGNATURES) + +Validate `element` as the element type of the result space being built. + +The element type must be concrete and cannot itself be a result-space +envelope. Concrete external result types are accepted without requiring them +to subtype [`AbstractCoreResult`](@ref). + +# Arguments + +- `element`: proposed result-space element type. +- The result-space type being built, or `AbstractResultSpace` for a collection of + results. + +# Returns + +- `element` when it satisfies the result-space invariant. + +# Errors + +- `ArgumentError`: `element` is abstract, is `Any` or subtypes + [`AbstractResultSpace`](@ref). +""" +function validate(element::Type{T}, ::Type{<:AbstractResultSpace}) where {T} + isconcretetype(element) || throw(ArgumentError( + "result-space element type must be concrete; got $element", + )) + element <: AbstractResultSpace && throw(ArgumentError( + "a result space cannot contain another result-space envelope", + )) + return element +end + +#! explicit-imports: off +# Base's iterator trait protocol exposes these values without public bindings. +Base.IteratorSize(::Type{<:AbstractResultSpace}) = Base.HasShape{1}() +Base.IteratorEltype(::Type{<:AbstractResultSpace}) = Base.HasEltype() +#! explicit-imports: on +Base.eltype(::Type{<:AbstractResultSpace{T}}) where {T} = T diff --git a/src/commons/retained_products.jl b/src/commons/retained_products.jl new file mode 100644 index 000000000..66e36ec75 --- /dev/null +++ b/src/commons/retained_products.jl @@ -0,0 +1,197 @@ +# Re-expression uses recorded units. It never enters source acquisition or +# changes the retained numerical eligibility decision. +_observed_scale(value::Missing,from,to) = value +_observed_scale(value::Nothing,from,to) = value +_observed_scale(value::AbstractArray,from,to) = map(x -> _observed_scale(x,from,to),value) +_observed_scale(value::NamedTuple,from,to) = map(x -> _observed_scale(x,from,to),value) +_observed_scale(value::Tuple,from,to) = map(x -> _observed_scale(x,from,to),value) +function _observed_scale(value::Number,from,to) + from==to && return value + T=typeof(float(real(nominal(value)))) + convert_value()=value*scale_factor(from,to,T) + return T===BigFloat ? setprecision(convert_value,BigFloat,precision(real(nominal(value)))) : convert_value() +end + +function _reexpress_product(product;unit=nothing,frequency_unit=nothing) + target=unit===nothing ? product.unit : unit + scale_factor(product.unit,target) # Check dimensions even if all values are missing. + result=product + if target!=product.unit + values=if product.coordinates.kind===:histogram + merge(product.values,( + lower=_observed_scale(product.values.lower,product.unit,target), + upper=_observed_scale(product.values.upper,product.unit,target), + density=_observed_scale(product.values.density,inv(product.unit),inv(target)))) + else + _observed_scale(product.values,product.unit,target) + end + thresholds=product.thresholds + if thresholds!==nothing && thresholds.unit==product.unit + thresholds=merge(thresholds,(values=_observed_scale(thresholds.values,product.unit,target),unit=target)) + end + result=merge(product,(values,unit=target,thresholds)) + if product.coordinates.kind===:histogram + d=product.distribution + cdf(c)=c===nothing ? nothing : merge(c,(x=_observed_scale(c.x,product.unit,target),)) + distribution=merge(d,(edges=_observed_scale(d.edges,product.unit,target), + empirical_cdf=cdf(d.empirical_cdf),model_cdf=cdf(d.model_cdf), + qq=d.qq===nothing ? nothing : _observed_scale(d.qq,product.unit,target))) + result=merge(result,(distribution,ordinate_units=merge(product.ordinate_units,(density=inv(target),)))) + end + end + c=result.coordinates + if frequency_unit!==nothing && haskey(c,:frequencies) && c.frequencies!==nothing + target_frequency=frequency_unit isa Symbol ? Units.units(frequency_unit,:hertz) : frequency_unit + scale_factor(c.frequency_unit,target_frequency) + result=merge(result,(coordinates=merge(c,( + frequencies=_observed_scale(c.frequencies,c.frequency_unit,target_frequency), + frequency_unit=target_frequency)),)) + end + return result +end + +function _retained_unit(product,override,length_unit) + override isa UnitExpr && return override + override===nothing || override isa Symbol || throw(ArgumentError("unit override must be a unit expression or metric prefix")) + numerator=product.unit.numerator + if override!==nothing && !isempty(numerator) + numerator=(Units.Unit(first(numerator).name,override),Base.tail(numerator)...) + end + denominator=map(product.unit.denominator) do unit + length_unit!==nothing && unit.name===:meter ? Units.Unit(:meter,length_unit) : unit + end + return UnitExpr(numerator,denominator) +end + +""" +$(TYPEDSIGNATURES) + +Select or re-express an existing observation without acquiring a source. +Omitted unit options preserve recorded units. Explicit units convert from each +product's recorded unit. Frequency conversion preserves sample identities. +Coordinates, availability, uncertainty dependencies, comparisons, and timing +associations remain retained. New clipping, thresholds, or frequency samples +require a new observation of the primary result. +""" +function ObservedResult(source::ObservedResult,requests::Tuple=(); + comparisons=nothing,timings=nothing,gridpoint=nothing,clip=nothing,atol=nothing, + units::Tuple=(),length_unit::Union{Nothing,Symbol}=nothing, + frequency_unit=nothing,quantity_units=nothing,frequencies=nothing, + complete_pairs::Bool=false) + atol===nothing && frequencies===nothing || throw(ArgumentError( + "retained observations cannot apply new cutoffs or recover frequency samples")) + gridpoint===nothing || isequal(gridpoint,source.gridpoint) || throw(ArgumentError("retained gridpoint identity cannot be replaced")) + comparisons===nothing || isequal(collect(comparisons),source.errors) || throw(ArgumentError("retained comparisons cannot be replaced")) + timings===nothing || isequal(timings,source.timings) || throw(ArgumentError("retained timing associations cannot be replaced")) + selected=observation_requests(source,requests;complete_pairs).retained + isempty(units) || length(units)==length(selected) || throw(DimensionMismatch("units must align with retained requests")) + isempty(units) || quantity_units===nothing || throw(ArgumentError("use units or quantity_units, not both")) + products=map(eachindex(selected)) do index + request=selected[index] + product=observation_product(source,request) + clip===nothing || clip===product.clipped || throw(ArgumentError("retained clipping cannot be changed")) + override=isempty(units) ? _unit_override(quantity_units,request) : units[index] + unit=_retained_unit(product,override,length_unit) + _reexpress_product(product;unit,frequency_unit) + end + return ObservedResult(source.gridpoint,products,source.errors,source.timings) +end + +""" +$(TYPEDSIGNATURES) + +Interpret a retained request across observations. Convert compatible +quantity and frequency units to the first product's units (or explicit targets), +and order coefficients by the first product's original coordinates. Every trace +retains its samples. Missing coefficients and incompatible units fail without +interpolation, numerical acquisition, or changes to scientific eligibility. +With `band`, select original sample identities from completed comparison +records. `reference_id` disambiguates the recorded reference. A separately +included reference uses the same unambiguous saved selection. Missing or +conflicting records fail before returning any products. +""" +function observation_product(points::Union{Tuple,AbstractVector{<:ObservedResult}},request; + unit=nothing,frequency_unit=nothing,band=nothing,reference_id=nothing) + isempty(points) && throw(ArgumentError("at least one observation is required")) + first_product=observation_product(first(points),request) + target=something(unit,first_product.unit) + coordinate=first_product.coordinates + frequency_target=frequency_unit===nothing ? get(coordinate,:frequency_unit,nothing) : frequency_unit + selections=band===nothing ? nothing : _retained_band_samples(points,request,band,reference_id) + products=map(eachindex(points)) do index + point=points[index] + product=observation_product(point,request) + c=product.coordinates + c.kind==coordinate.kind || throw(ArgumentError("overlaid products require the same coordinate kind")) + if c.kind in (:matrix,:diagonal,:vector) + aligned=c.kind===:vector ? Set(c.positions)==Set(coordinate.positions) : + Set(c.rows)==Set(coordinate.rows) && Set(c.columns)==Set(coordinate.columns) + aligned || + throw(DimensionMismatch("overlaid products must retain the requested original coefficients")) + reordered=c.kind===:vector ? c.positions!=coordinate.positions : + c.rows!=coordinate.rows || c.columns!=coordinate.columns + if reordered + identity=request_identity(product.request) + prefix=identity isa Tuple ? identity : (identity,) + indices=c.kind===:matrix ? (coordinate.rows,coordinate.columns,Colon()) : + c.kind===:vector ? (coordinate.positions,Colon()) : (coordinate.rows,Colon()) + product=_selected_product(product,(prefix...,indices...),indices) + end + end + if selections!==nothing + c=product.coordinates + c.kind in (:matrix,:diagonal,:vector) || throw(ArgumentError( + "retained band selection requires frequency coordinates")) + samples=filter(in(selections[index]),c.samples) + identity=request_identity(product.request) + prefix=identity isa Tuple ? identity : (identity,) + indices=c.kind===:matrix ? (c.rows,c.columns,samples) : + c.kind===:vector ? (c.positions,samples) : (c.rows,samples) + product=_selected_product(product,(prefix...,indices...),indices) + end + detach(_reexpress_product(product;unit=target,frequency_unit=frequency_target)) + end + selections===nothing || any(p -> !isempty(p.coordinates.samples),products) || + throw(ArgumentError("comparison band $(repr(band)) contains no retained samples in the supplied observations")) + return products +end + +# Comparison records, not plotting-side frequency rules, define a retained band. +function _retained_band_samples(points,request,band,reference_id) + matching=map(points) do point + id=get(point.gridpoint,:id,nothing) + filter(point.errors) do row + isequal(row.result_id,id) && isequal(row.band,band) && + request_identity(row.request)==request_identity(request) && + (reference_id===nothing || isequal(row.reference_id,reference_id)) + end + end + records=collect(Iterators.flatten(matching)) + isempty(records) && throw(ArgumentError("no completed comparison retains band $(repr(band)) for this request and reference")) + references=unique(row.reference_id for row in records) + length(references)==1 || throw(ArgumentError("retained band has multiple references; supply reference_id")) + retained_reference=only(references) + definition(row)=(; + indices=row.settings.indices, + requested_bounds=get(row.settings,:requested_bounds,nothing), + actual_bounds=get(row.settings,:actual_bounds,nothing), + fundamental=get(row.settings,:fundamental,nothing), + harmonics=get(row.settings,:harmonics,nothing)) + first_definition=definition(first(records)) + all(row -> isequal(definition(row),first_definition),records) || throw(ArgumentError( + "completed comparisons disagree on the saved definition or sample selection of band $(repr(band))")) + return map(eachindex(points)) do index + id=get(points[index].gridpoint,:id,nothing) + !isempty(matching[index]) || isequal(id,retained_reference) || throw(ArgumentError( + "observation $(repr(id)) has no completed comparison for band $(repr(band)) and the selected reference")) + copy(first_definition.indices) + end +end + +# Direct owner dispatch obeys the same retained-input requirements as construction. +function observation_quantity(source::ObservedResult,request;unit=nothing,clip=nothing,atol=nothing,frequencies=nothing) + atol===nothing && frequencies===nothing || throw(ArgumentError("retained quantities cannot apply new cutoffs or samples")) + product=observation_product(source,request;unit) + clip===nothing || clip===product.clipped || throw(ArgumentError("retained clipping cannot be changed")) + return detach(product) +end diff --git a/src/commons/types.jl b/src/commons/types.jl new file mode 100644 index 000000000..906007b21 --- /dev/null +++ b/src/commons/types.jl @@ -0,0 +1,206 @@ +""" +$(TYPEDEF) + +Supertype for complete LineCableModels computation inputs. +""" +abstract type AbstractProblemDefinition end + +""" +$(TYPEDEF) + +Supertype for scientific and higher-order computation selections. +""" +abstract type AbstractFormulation end + +""" +$(TYPEDEF) + +Supertype for completed LineCableModels computation results. +""" +abstract type AbstractProblemResult end + +""" +$(TYPEDEF) + +Supertype for direct results owned by LineCableModels computations. +""" +abstract type AbstractCoreResult <: AbstractProblemResult end + +""" +$(TYPEDEF) + +Supertype for completed finite collections whose element type is `T`. +""" +abstract type AbstractResultSpace{T} <: AbstractProblemResult end + +""" +$(TYPEDEF) + +Supertype for deterministic result spaces whose element type is `T`. +""" +abstract type AbstractParametricResult{T} <: AbstractResultSpace{T} end + +""" +$(TYPEDEF) + +Supertype for uncertainty result spaces whose element type is `T`. +""" +abstract type AbstractUncertaintyResult{T} <: AbstractResultSpace{T} end + +""" +$(TYPEDEF) + +Retain formulation-owned inputs. The selected formulation defines defaults and +validation. Construction preserves the supplied named tuple, including the +identity of mutable values within it. + +$(TYPEDFIELDS) +""" +struct FormulationOptions{NT <: NamedTuple} + "Supplied formulation options." + data::NT + FormulationOptions(data::NamedTuple) = new{typeof(data)}(data) +end + +""" +$(TYPEDSIGNATURES) + +Construct formulation inputs from keywords, or an empty record with no keywords. +Owner-specific interpretation and validation occur when the formulation resolves +the options, not when this record is constructed. +""" +FormulationOptions(; kwargs...) = FormulationOptions((; kwargs...)) + +""" +$(TYPEDEF) + +Retain computation-owned inputs. The receiving computation or backend owns +defaults and validation. + +$(TYPEDFIELDS) +""" +struct ComputationOptions{NT <: NamedTuple} + "Supplied computation options." + data::NT + ComputationOptions(data::NamedTuple) = new{typeof(data)}(data) +end + +""" +$(TYPEDSIGNATURES) + +Construct computation inputs from keywords, or an empty record with no keywords. +The receiving computation validates the supported keys and values. +""" +ComputationOptions(; kwargs...) = ComputationOptions((; kwargs...)) + +""" +$(TYPEDEF) + +Retain computation-owned supplemental output. The producing computation defines its contents. Immutability is shallow: arrays and other mutable payload values +are neither copied nor frozen. Nested diagnostic type bounds are preserved. + +$(TYPEDFIELDS) +""" +struct ComputationDetails{NT <: NamedTuple} + "Named-tuple supplemental output, accessed explicitly through `.data`." + data::NT + ComputationDetails(data::NamedTuple) = new{typeof(data)}(data) +end + +""" +$(TYPEDSIGNATURES) + +Construct supplemental output from keywords. With no keywords, return an empty +record indicating that the computation supplied no supplemental output. +""" +ComputationDetails(; kwargs...) = ComputationDetails((; kwargs...)) + +""" +$(TYPEDEF) + +Store one passive formula selection until its defining formulation resolves the +identifier, model parameters, and formulation-owned physical and numerical options. + +$(TYPEDFIELDS) +""" +struct FormulaDefinition{ID, Order, P <: NamedTuple, O <: FormulationOptions, E} + "Explicit model parameters without evaluated physical state." + parameters::P + "Explicit physical choices and numerical controls owned by the selected equation." + options::O + "Optional equivalent homogeneous-earth selection." + equivalent_earth::E +end + +""" +$(TYPEDEF) + +Hold the formula object, the operation and the `Val` selectors of one expression of a +formula. Calling the expression passes the formula, the selectors and the runtime arguments +to the operation, in that order. + +$(TYPEDFIELDS) +""" +struct Expression{S, F, A <: Tuple} + "Selected formula whose concrete type selects the operation's method." + selection::S + "Native domain method accepting the selected formulation first." + method::F + "Semantic Val selectors inserted before runtime arguments." + arguments::A +end + +""" +$(TYPEDSIGNATURES) + +Build the expression of `selection` for the operation `method` and optional `Val` selectors. +Throw `ArgumentError` if a selector is not a `Val` instance. +""" +function Expression(selection::S, method::F, arguments...) where {S, F} + all(argument -> argument isa Val, arguments) || throw(ArgumentError( + "Expression semantic selectors must be Val instances")) + return Expression{S, F, typeof(arguments)}(selection, method, arguments) +end + +@inline function (expression::Expression)(arguments...) + return expression.method(expression.selection, expression.arguments..., arguments...) +end + +""" +$(TYPEDEF) + +Hold a formula resolved at one evaluation point: the formula, the input of that point and the +values that the formula's expressions share there. `expression(functor, workspace)` evaluates +an expression of the formula at that point. The state holds plain values. Arrays come from +the buffers of the workspace. + +$(TYPEDFIELDS) +""" +struct Functor{F, I <: NamedTuple, S <: NamedTuple} + "Formula whose expressions this Functor evaluates." + formula::F + "Inputs of the evaluation point, with the options of the expression evaluated there." + input::I + "Plain values that the expressions of the formula share at that point." + state::S +end + +""" +$(TYPEDSIGNATURES) + +Build the Functor of `formula` at the point that `input` describes. A formula without a method +of its own does not share values, and its state is empty. A family or a formula that checks its +input, shares values or reads arrays from the buffers of `workspace` adds a method on its own +type. +""" +Functor(formula, input::NamedTuple; workspace = nothing) = Functor(formula, input, (;)) + +""" +$(TYPEDSIGNATURES) + +Return the Functor of the same formula and state at a point whose input extends +`functor.input` with `extension`, such as one conductor pair of an earth calculation. +""" +function Functor(functor::Functor, extension::NamedTuple) + return Functor(functor.formula, merge(functor.input, extension), functor.state) +end diff --git a/src/commons/uncertainty.jl b/src/commons/uncertainty.jl new file mode 100644 index 000000000..57bbf4043 --- /dev/null +++ b/src/commons/uncertainty.jl @@ -0,0 +1,14 @@ +""" +Return the nominal value of a deterministic or uncertain quantity. +""" +nominal(value) = value +nominal(value::Complex) = complex(nominal(real(value)), nominal(imag(value))) +nominal(values::AbstractArray) = nominal.(values) + +""" +Return the standard uncertainty of a quantity. A deterministic number returns the zero +of its own type. +""" +uncertainty(value::Number) = zero(value) +uncertainty(value::Complex) = complex(uncertainty(real(value)), uncertainty(imag(value))) +uncertainty(values::AbstractArray) = uncertainty.(values) diff --git a/src/datamodel/DataModel.jl b/src/datamodel/DataModel.jl index 4d964a91b..42c9ad470 100644 --- a/src/datamodel/DataModel.jl +++ b/src/datamodel/DataModel.jl @@ -1,104 +1,117 @@ """ - LineCableModels.DataModel + LineCableModels.DataModel -The [`DataModel`](@ref) module provides data structures, constructors and utilities for modeling power cables within the [`LineCableModels.jl`](index.md) package. This module includes definitions for various cable components, and visualization tools for cable designs. +Define the physical cable object model and completed line arrangements. # Overview -- Provides objects for detailed cable modeling with the [`CableDesign`](@ref) and supporting types: [`CircStrands`](@ref), [`Strip`](@ref), [`Tubular`](@ref), [`Semicon`](@ref), and [`Insulator`](@ref). -- Includes objects for cable **system** modeling with the [`LineCableSystem`](@ref) type, and multiple formation patterns like trifoil and flat arrangements. -- Contains functions for calculating the base electric properties of all elements within a [`CableDesign`](@ref), namely: resistance, inductance (via GMR), shunt capacitance, and shunt conductance (via loss factor). -- Offers visualization tools for previewing cable cross-sections and system layouts. -- Provides a library system for storing and retrieving cable designs. +- Declare primitives, material-bearing regions, and resolved geometry. +- Compose regions through [`Stack`](@ref), [`Group`](@ref), + [`Assembly`](@ref), and [`Enclosure`](@ref). +- Build one physical declaration as a completed [`CableDesign`](@ref). +- Place completed designs and resolve connections as a [`LineCableSystem`](@ref). +- Expose detached cable geometry and physical material ranges to consumers. +- Store cable designs in [`CablesLibrary`](@ref). # Dependencies $(IMPORTS) -# Exports - -$(EXPORTS) """ module DataModel # Export public API -export Thickness, Diameter # Type definitions -export CircStrands, RectStrands, Strip, Tubular, SectorParams, Sector # Conductor types -export Semicon, Insulator, SectorInsulator # Insulator types -export ConductorGroup, InsulatorGroup # Group types -export CableComponent, CableDesign # Cable design types -export CablePosition, LineCableSystem # System types -export CablesLibrary, NominalData # Support types -export trifoil_formation, flat_formation, get_outer_radius, MaxFill # Helpers -export preview, equivalent +export CableDesign, CableGeometry, PlacedRegion, LineCableSystem +export build, homogenize +export CablesLibrary, DatasheetInfo, datasheet +export trefoil_formation, flat_formation, outer_radius +export AbstractShape, AbstractPrimitive +export AbstractCablePart, Region, Stack +export Group, Assembly +export Enclosure +export Disk, Rectangle, Ellipse, Sector, Annulus, Polygon, Shell +export Pose2 +export EmptyBoundary +export resolve, boundary, area, perimeter, centroid, support, r_in, r_ex, thickness +export tessellate +export Ring, Polar, Fill, Lattice, capacity, placements +export FillFactor +export LayRatio, Pitch, LayAngle, Helix, pitch, angle, overlength +export ncables, nphases + +public AssemblyMember, AssemblyShape, BentStrip, BoundedPlacement, EnclosureBoundary +public geometry_tolerance +public equivalent_dielectric_permeability, radial_components, radial_position, + conductor_zone_position, same_radial_position, bounded_members +public DifferenceShape, EllipseOffset, ShellShape, SectorShape # Module-specific dependencies -using ..Commons -import ..Commons: add! -using ..Utils: - resolve_T, to_certain, to_nominal, is_headless, - is_in_testset, to_lower, to_upper -import ..Utils: coerce_to_T, to_lower -using ..Materials: Material -import ..PlotBuilder.BackendHandler: set_backend!, ensure_backend!, current_backend_symbol, - backend_available, renderfig, next_fignum -import ..PlotBuilder.PlotUIComponents: gl_screen, with_icon, MI_REFRESH, MI_SAVE, ICON_TTF -import ..Validation: Validation, sanitize, validate!, has_radii, has_temperature, - extra_rules, IntegerField, Positive, Finite, Normalized, IsA, required_fields, - coercive_fields, keyword_fields, keyword_defaults, _kwdefaults_nt, is_radius_input, - Nonneg, OneOf, Greater, PhysicalFillLimit, Satisfies -using Measurements -using DataFrames -using Colors -using Plots -using DisplayAs: DisplayAs -using LinearAlgebra -using Makie: Point, Point2f # otherwise will require adding GeometryBasics as a dependency -# Abstract types & interfaces +#! explicit-imports: off +# IMPORTS is expanded in the module docstring rather than called as Julia code. +using DocStringExtensions: IMPORTS +#! explicit-imports: on +using DocStringExtensions: TYPEDEF, TYPEDFIELDS, TYPEDSIGNATURES, FUNCTIONNAME +import ..Units +import ..Commons +import ..TextDisplay +import ..LineCableModels: add!, build, homogenize, validate, nominal, uncertainty +import ..LineCableModels: line_length +import ..LineCableModels: parameterize +import ..LineCableModels: Gridpoint, materialize, realize, realize_arguments +import Random +using ..Materials: AbstractMaterial, Material, RadialDielectric +import GeometryBasics +using GeometryBasics: Point2f +import Base: angle +import LinearAlgebra +using SpecialFunctions: ellipe + +# Abstract types and interfaces +include("interfaces.jl") include("types.jl") -include("radii.jl") +include("geometry/pose.jl") +include("geometry/primitives.jl") +include("geometry/shell.jl") +include("geometry/sector.jl") +include("geometry/ellipse.jl") +include("geometry/resolve.jl") +include("design/region.jl") +include("design/stack.jl") +include("placement/patterns.jl") +include("placement/paths.jl") +include("placement/compaction.jl") +include("placement/bounded.jl") +include("design/group.jl") +include("design/assembly.jl") +include("design/enclosure.jl") # Submodule `BaseParams` include("baseparams/BaseParams.jl") using .BaseParams -# Constructors -include("macros.jl") -include("validation.jl") - -# Conductors -include("strands_handler.jl") -include("circstrands.jl") -include("rectstrands.jl") -include("strip.jl") -include("tubular.jl") -include("conductorgroup.jl") -include("sector.jl") - -# Insulators -include("insulator.jl") -include("semicon.jl") -include("insulatorgroup.jl") -include("sectorinsulator.jl") - - -# Groups -include("nominaldata.jl") -include("cablecomponent.jl") -include("cabledesign.jl") +include("design/cabledesign.jl") +include("flatten.jl") # Library -include("cableslibrary.jl") -include("linecablesystem.jl") - -# Helpers & overrides -include("helpers.jl") -include("preview.jl") -include("io.jl") -include("typecoercion.jl") - -# Aliases for backward compatibility -const WireArray = CircStrands -export WireArray +include("cableslibrary/datasheetinfo.jl") +include("cableslibrary/cableslibrary.jl") +include("linecablesystem/clearance.jl") +include("linecablesystem/linecablesystem.jl") + +# Geometry and language protocols +include("geometry.jl") +include("preview/geometry.jl") +include("preview/materials.jl") + +# Bounded human-readable representations for the completed physical grammar. +include("textdisplay.jl") + +public preview_shapes, preview_materials +public PreviewShape, material_property_ranges +public flatten + +# Construction interfaces shared with Engine and UQ. Not modelling options. +public clearance_geometry, interface_clearance, collect_clearance_requirements, with_clearance +public clearance_summary, warn_clearance_summary, realize_clearance end # module DataModel diff --git a/src/datamodel/baseparams/BaseParams.jl b/src/datamodel/baseparams/BaseParams.jl index e141eba35..1dc3981c3 100644 --- a/src/datamodel/baseparams/BaseParams.jl +++ b/src/datamodel/baseparams/BaseParams.jl @@ -1,1370 +1,25 @@ """ - LineCableModels.DataModel.BaseParams + LineCableModels.DataModel.BaseParams -The [`BaseParams`](@ref) submodule provides fundamental functions for determining the base electrical parameters (R, L, C, G) of cable components within the [`LineCableModels.DataModel`](@ref) module. This includes implementations of standard engineering formulas for resistance, inductance, and geometric parameters of various conductor configurations. - -# Overview - -- Implements basic electrical engineering formulas for calculating DC resistance and inductance of different conductor geometries (tubular, strip, wire arrays). -- Implements basic formulas for capacitance and dielectric losses in insulators and semiconductors. -- Provides functions for temperature correction of material properties. -- Calculates geometric mean radii for different conductor configurations. -- Includes functions for determining the effective length for helical wire arrangements. -- Calculates equivalent electrical parameters and correction factors for different geometries and configurations. - -# Dependencies - -$(IMPORTS) - -# Exports - -$(EXPORTS) +Calculate cable-geometry equivalences, reference-state resistance, and +export-oriented dielectric reductions. Each function promotes its inputs with +Base numeric rules. """ module BaseParams -# Export public API -export calc_equivalent_alpha -export calc_parallel_equivalent -export calc_helical_params -export calc_strip_resistance -export calc_temperature_correction -export calc_tubular_resistance -export calc_tubular_inductance -export calc_circstrands_coords -export calc_inductance_trifoil -export calc_circstrands_gmr -export calc_tubular_gmr -export calc_equivalent_mu -export calc_shunt_capacitance -export calc_shunt_conductance -export calc_equivalent_gmr -export calc_gmd -export calc_solenoid_correction -export calc_equivalent_rho -export calc_equivalent_eps -export calc_equivalent_lossfact -export calc_sigma_lossfact - -# Module-specific dependencies -using Measurements -using ...Commons -import ..DataModel: AbstractCablePart -using ...Utils: resolve_T, coerce_to_T - - -""" -$(TYPEDSIGNATURES) - -Calculates the equivalent temperature coefficient of resistance (`alpha`) when two conductors are connected in parallel, by cross-weighted-resistance averaging: - -```math -\\alpha_{eq} = \\frac{\\alpha_1 R_2 + \\alpha_2 R1}{R_1 + R_2} -``` -where ``\\alpha_1``, ``\\alpha_2`` are the temperature coefficients of the conductors, and ``R_1``, ``R_2`` are the respective resistances. - -# Arguments - -- `alpha1`: Temperature coefficient of resistance of the first conductor \\[1/°C\\]. -- `R1`: Resistance of the first conductor \\[Ω\\]. -- `alpha2`: Temperature coefficient of resistance of the second conductor \\[1/°C\\]. -- `R2`: Resistance of the second conductor \\[Ω\\]. - -# Returns - -- The equivalent temperature coefficient \\[1/°C\\] for the parallel combination. - -# Examples - -```julia -alpha_conductor = 0.00393 # Copper -alpha_new_part = 0.00403 # Aluminum -R_conductor = 0.5 -R_new_part = 1.0 -alpha_eq = $(FUNCTIONNAME)(alpha_conductor, R_conductor, alpha_new_part, R_new_part) -println(alpha_eq) # Output: 0.00396 (approximately) -``` -""" -function calc_equivalent_alpha(alpha1::T, R1::T, alpha2::T, R2::T) where {T <: REALSCALAR} - return (alpha1 * R2 + alpha2 * R1) / (R1 + R2) -end - -function calc_equivalent_alpha(alpha1, R1, alpha2, R2) - T = resolve_T(alpha1, R1, alpha2, R2) - return calc_equivalent_alpha( - coerce_to_T(alpha1, T), - coerce_to_T(R1, T), - coerce_to_T(alpha2, T), - coerce_to_T(R2, T), - ) -end - -""" -$(TYPEDSIGNATURES) - -Calculates the parallel equivalent of two impedances (or series equivalent of two admittances): - -```math -Z_{eq} = \\frac{Z_1 Z_2}{Z_1 + Z_2} -``` - -This expression, when applied recursively to [`LineCableModels.DataModel.CircStrands`](@ref) objects, implements the formula for the hexagonal wiring pattern described in CIGRE TB-345 [app14198982](@cite) [cigre345](@cite): - -```math -\\frac{1}{R_{\\text{dc}}} = \\frac{\\pi d^2}{4 \\rho} \\left( 1 + \\sum_{1}^{n} \\frac{6n}{k_n} \\right) -``` - -```math -k_n = \\left[ 1 + \\left( \\pi \\frac{D_n}{\\lambda_n} \\right)^2 \\right]^{1/2} -``` - -where ``R_{\\text{dc}}`` is the DC resistance, ``d`` is the diameter of each wire, ``\rho`` is the resistivity, ``n`` is the number of layers following the hexagonal pattern, ``D_n`` is the diameter of the ``n``-th layer, and ``\\lambda_n `` is the pitch length of the ``n``-th layer, obtained using [`calc_helical_params`](@ref). - -# Arguments - -- `Z1`: The total impedance of the existing system \\[Ω\\]. -- `Z2`: The impedance of the new layer being added \\[Ω\\]. - -# Returns - -- The parallel equivalent impedance \\[Ω\\]. - -# Examples - -```julia -Z1 = 5.0 -Z2 = 10.0 -Req = $(FUNCTIONNAME)(Z1, Z2) -println(Req) # Outputs: 3.3333333333333335 -``` - -# See also - -- [`calc_helical_params`](@ref) -""" -function calc_parallel_equivalent( - Z1::T, - Z2::T, -) where {T <: Union{REALSCALAR, COMPLEXSCALAR}} - - # Case 1: Inf / Inf -> NaN - # The parallel combination of an open circuit (Inf) and any finite impedance is the finite impedance. - if isinf(Z1) - return Z2 - elseif isinf(Z2) - return Z1 - end - - # Case 2: 0 / 0 -> NaN - # The parallel combination of two short circuits (0) is a short circuit. - # The standard formula works fine if only one is zero, but not if both are. - if iszero(Z1) && iszero(Z2) - return zero(T) - end - return (Z1 * Z2) / (Z1 + Z2) -end - -function calc_parallel_equivalent(Z1, Z2) - T = resolve_T(Z1, Z2) - return calc_parallel_equivalent( - coerce_to_T(Z1, T), - coerce_to_T(Z2, T), - ) -end - -""" -$(TYPEDSIGNATURES) - -Calculates the mean diameter, pitch length, and overlength based on cable geometry parameters. The lay ratio is defined as the ratio of the pitch length ``L_p`` to the external diameter ``D_e``: +export equivalent_alpha, parallel, helix, strip_resistance +export tubular_resistance, wire_coordinates +export strand_gmr, tubular_gmr, equivalent_gmr, equivalent_mu +export shunt_capacitance, shunt_conductance, series_shunt_admittance +export solenoid_factor, equivalent_rho, equivalent_eps +export equivalent_conductivity -```math -\\lambda = \\frac{L_p}{D_e} -``` -where ``D_e`` and ``L_p`` are the dimensions represented in the figure. +using DocStringExtensions: TYPEDSIGNATURES +using ...Commons: vacuum_permittivity -![](./assets/lay_ratio.svg) +include("geometry.jl") +include("resistance.jl") +include("inductance.jl") +include("dielectrics.jl") -# Arguments - -- `r_in`: Inner radius of the cable layer \\[m\\]. -- `r_ex`: Outer radius of the cable layer \\[m\\]. -- `lay_ratio`: Ratio of the pitch (lay) length to the external diameter of the corresponding layer of wires \\[dimensionless\\]. - -# Returns - -- `mean_diameter`: Mean diameter of the cable layer \\[m\\]. -- `pitch_length`: The length over which the strands complete one full twist \\[m\\]. -- `overlength`: Effective length increase resulting from the helical path \\[1/m\\]. - -# Notes - -Reference values for `lay_ratio` are given under standard EN 50182 [CENELEC50182](@cite): - -| Conductor type | Steel wires | Aluminum wires | Lay ratio - Steel | Lay ratio - Aluminum | -|---------------|----------------------|---------------------|----------------------|-------------------| -| AAAC 4 layers | - | 61 (1/6/12/18/24) | - | 15/13.5/12.5/11 | -| ACSR 3 layers | 7 (1/6) | 54 (12/18/24) | 19 | 15/13/11.5 | -| ACSR 2 layers | 7 (1/6) | 26 (10/16) | 19 | 14/11.5 | -| ACSR 1 layer | 7 (1/6) | 10 | 19 | 14 | -| ACCC/TW | - | 36 (8/12/16) | - | 15/13.5/11.5 | - -# Examples - -```julia -r_in = 0.01 -r_ex = 0.015 -lay_ratio = 12 - -mean_diam, pitch, overlength = $(FUNCTIONNAME)(r_in, r_ex, lay_ratio) -# mean_diam ≈ 0.025 [m] -# pitch ≈ 0.3 [m] -# overlength > 1.0 [1/m] -``` -""" -function calc_helical_params( - r_in::T, - r_ex::T, - lay_ratio::T, -) where {T <: REALSCALAR} - mean_diameter = 2 * (r_in + (r_ex - r_in) / 2) - pitch_length = lay_ratio * mean_diameter - overlength = - !isapprox(pitch_length, 0.0) ? sqrt(1 + (π * mean_diameter / pitch_length)^2) : 1 - - return mean_diameter, pitch_length, overlength -end - -function calc_helical_params(r_in, r_ex, lay_ratio) - T = resolve_T(r_in, r_ex, lay_ratio) - return calc_helical_params( - coerce_to_T(r_in, T), - coerce_to_T(r_ex, T), - coerce_to_T(lay_ratio, T), - ) end - -""" -$(TYPEDSIGNATURES) - -Calculates the DC resistance of a strip conductor based on its geometric and material properties, using the basic resistance formula in terms of the resistivity and cross-sectional area: - -```math -R = \\rho \\frac{\\ell}{W T} -``` -where ``\\ell`` is the length of the strip, ``W`` is the width, and ``T`` is the thickness. The length is assumed to be infinite in the direction of current flow, so the resistance is calculated per unit length. - -# Arguments - -- `thickness`: Thickness of the strip \\[m\\]. -- `width`: Width of the strip \\[m\\]. -- `rho`: Electrical resistivity of the conductor material \\[Ω·m\\]. -- `alpha`: Temperature coefficient of resistivity \\[1/°C\\]. -- `T0`: Reference temperature for the material properties \\[°C\\]. -- `Top`: Operating temperature of the conductor \\[°C\\]. - -# Returns - -- DC resistance of the strip conductor \\[Ω\\]. - -# Examples - -```julia -thickness = 0.002 -width = 0.05 -rho = 1.7241e-8 -alpha = 0.00393 -T0 = 20 -T = 25 -resistance = $(FUNCTIONNAME)(thickness, width, rho, alpha, T0, T) -# Output: ~0.0001758 Ω -``` - -# See also - -- [`calc_temperature_correction`](@ref) -""" -function calc_strip_resistance( - thickness::T, - width::T, - rho::T, - alpha::T, - T0::T, - Top::T, -) where {T <: REALSCALAR} - - cross_section = thickness * width - return calc_temperature_correction(alpha, Top, T0) * rho / cross_section -end - -function calc_strip_resistance(thickness, width, rho, alpha, T0, Top) - T = resolve_T(thickness, width, rho, alpha, T0, Top) - return calc_strip_resistance( - coerce_to_T(thickness, T), - coerce_to_T(width, T), - coerce_to_T(rho, T), - coerce_to_T(alpha, T), - coerce_to_T(T0, T), - coerce_to_T(Top, T), - ) -end - -""" -$(TYPEDSIGNATURES) - -Calculates the temperature correction factor for material properties based on the standard linear temperature model [cigre345](@cite): - -```math -k(T) = 1 + \\alpha (T - T_0) -``` -where ``\\alpha`` is the temperature coefficient of the material resistivity, ``T`` is the operating temperature, and ``T_0`` is the reference temperature. - -# Arguments - -- `alpha`: Temperature coefficient of the material property \\[1/°C\\]. -- `T`: Current temperature \\[°C\\]. -- `T0`: Reference temperature at which the base material property was measured \\[°C\\]. Defaults to T₀. - -# Returns - -- Temperature correction factor to be applied to the material property \\[dimensionless\\]. - -# Examples - -```julia - # Copper resistivity correction (alpha = 0.00393 [1/°C]) - k = $(FUNCTIONNAME)(0.00393, 75.0, 20.0) # Expected output: 1.2161 -``` -""" -function calc_temperature_correction(alpha::T, Top::T, T0::T = T₀) where {T <: REALSCALAR} - @assert abs(Top - T0) < ΔTmax """ -Temperature is outside the valid range for linear resistivity model: -Top = $Top -T0 = $T0 -ΔTmax = $ΔTmax -|Top - T0| = $(abs(Top - T0))""" - return 1 + alpha * (Top - T0) -end - -function calc_temperature_correction(alpha, Top, T0 = T₀) - T = resolve_T(alpha, Top, T0) - return calc_temperature_correction( - coerce_to_T(alpha, T), - coerce_to_T(Top, T), - coerce_to_T(T0, T), - ) -end - -""" -$(TYPEDSIGNATURES) - -Calculates the DC resistance of a tubular conductor based on its geometric and material properties, using the resistivity and cross-sectional area of a hollow cylinder with radii ``r_{in}`` and ``r_{ext}``: - -```math -R = \\rho \\frac{\\ell}{\\pi (r_{ext}^2 - r_{in}^2)} -``` -where ``\\ell`` is the length of the conductor, ``r_{in}`` and ``r_{ext}`` are the inner and outer radii, respectively. The length is assumed to be infinite in the direction of current flow, so the resistance is calculated per unit length. - -# Arguments - -- `r_in`: Internal radius of the tubular conductor \\[m\\]. -- `r_ex`: External radius of the tubular conductor \\[m\\]. -- `rho`: Electrical resistivity of the conductor material \\[Ω·m\\]. -- `alpha`: Temperature coefficient of resistivity \\[1/°C\\]. -- `T0`: Reference temperature for the material properties \\[°C\\]. -- `Top`: Operating temperature of the conductor \\[°C\\]. - -# Returns - -- DC resistance of the tubular conductor \\[Ω\\]. - -# Examples - -```julia -r_in = 0.01 -r_ex = 0.02 -rho = 1.7241e-8 -alpha = 0.00393 -T0 = 20 -T = 25 -resistance = $(FUNCTIONNAME)(r_in, r_ex, rho, alpha, T0, T) -# Output: ~9.10e-8 Ω -``` - -# See also - -- [`calc_temperature_correction`](@ref) -""" -function calc_tubular_resistance( - r_in::T, - r_ex::T, - rho::T, - alpha::T, - T0::T, - Top::T, -) where {T <: REALSCALAR} - cross_section = π * (r_ex^2 - r_in^2) - return calc_temperature_correction(alpha, Top, T0) * rho / cross_section -end - -function calc_tubular_resistance(r_in, r_ex, rho, alpha, T0, Top) - T = resolve_T(r_in, r_ex, rho, alpha, T0, Top) - return calc_tubular_resistance( - coerce_to_T(r_in, T), - coerce_to_T(r_ex, T), - coerce_to_T(rho, T), - coerce_to_T(alpha, T), - coerce_to_T(T0, T), - coerce_to_T(Top, T), - ) -end - -""" -$(TYPEDSIGNATURES) - -Calculates the inductance of a tubular conductor per unit length, disregarding skin-effects (DC approximation) [916943](@cite) [cigre345](@cite) [1458878](@cite): - -```math -L = \\frac{\\mu_r \\mu_0}{2 \\pi} \\log \\left( \\frac{r_{ext}}{r_{in}} \\right) -``` -where ``\\mu_r`` is the relative permeability of the conductor material, ``\\mu_0`` is the vacuum permeability, and ``r_{in}`` and ``r_{ext}`` are the inner and outer radii of the conductor, respectively. - -# Arguments - -- `r_in`: Internal radius of the tubular conductor \\[m\\]. -- `r_ex`: External radius of the tubular conductor \\[m\\]. -- `mu_r`: Relative permeability of the conductor material \\[dimensionless\\]. - -# Returns - -- Internal inductance of the tubular conductor per unit length \\[H/m\\]. - -# Examples - -```julia -r_in = 0.01 -r_ex = 0.02 -mu_r = 1.0 -L = $(FUNCTIONNAME)(r_in, r_ex, mu_r) -# Output: ~2.31e-7 H/m -``` - -# See also - -- [`calc_tubular_resistance`](@ref) -""" -function calc_tubular_inductance( - r_in::T, - r_ex::T, - mu_r::T, -) where {T <: REALSCALAR} - return mu_r * μ₀ / (2 * π) * log(r_ex / r_in) -end - -function calc_tubular_inductance(r_in, r_ex, mu_r) - T = resolve_T(r_in, r_ex, mu_r) - return calc_tubular_inductance( - coerce_to_T(r_in, T), - coerce_to_T(r_ex, T), - coerce_to_T(mu_r, T), - ) -end - -""" -$(TYPEDSIGNATURES) - -Calculates the center coordinates of wires arranged in a circular pattern. - -# Arguments - -- `num_wires`: Number of wires in the circular arrangement \\[dimensionless\\]. -- `radius_wire`: Radius of each individual wire \\[m\\]. -- `r_in`: Inner radius of the wire array (to wire centers) \\[m\\]. -- `C`: Optional tuple representing the center coordinates of the circular arrangement \\[m\\]. Default is (0.0, 0.0). - -# Returns - -- Vector of tuples, where each tuple contains the `(x, y)` coordinates \\[m\\] of the center of a wire. - -# Examples - -```julia -# Create a 7-wire array with 2mm wire radius and 1cm inner radius -wire_coords = $(FUNCTIONNAME)(7, 0.002, 0.01) -println(wire_coords[1]) # Output: First wire coordinates - -# Create a wire array with custom center position -wire_coords = $(FUNCTIONNAME)(7, 0.002, 0.01, C=(0.5, 0.3)) -``` - -# See also - -- [`LineCableModels.DataModel.CircStrands`](@ref) -""" -function calc_circstrands_coords( - num_wires::U, - radius_wire::T, - r_in::T, - C::Tuple{T, T}, -) where {T <: REALSCALAR, U <: Int} - wire_coords = Tuple{T, T}[] # Global coordinates of all wires - lay_radius = num_wires == 1 ? 0 : r_in + radius_wire - - # Calculate the angle between each wire - angle_step = 2 * π / num_wires - for i in 0:(num_wires-1) - angle = i * angle_step - x = C[1] + lay_radius * cos(angle) - y = C[2] + lay_radius * sin(angle) - push!(wire_coords, (x, y)) # Add wire center - end - return wire_coords -end - -function calc_circstrands_coords(num_wires::Int, radius_wire, r_in; C = nothing) - T = - C === nothing ? resolve_T(radius_wire, r_in) : - resolve_T(radius_wire, r_in, C...) - C_val = C === nothing ? coerce_to_T((0.0, 0.0), T) : coerce_to_T(C, T) - return calc_circstrands_coords( - num_wires, - coerce_to_T(radius_wire, T), - coerce_to_T(r_in, T), - C_val, - ) -end - -""" -$(TYPEDSIGNATURES) - -Calculates the positive-sequence inductance of a trifoil-configured cable system composed of core/screen assuming solid bonding, using the formula given under section 4.2.4.3 of CIGRE TB-531: - -```math -Z_d = \\left[Z_a - Z_x\\right] - \\frac{\\left( Z_m - Z_x \\right)^2}{Z_s - Z_x} -``` -```math -L = \\mathfrak{Im}\\left(\\frac{Z_d}{\\omega}\\right) -``` -where ``Z_a``, ``Z_s`` are the self impedances of the core conductor and the screen, and ``Z_m``, and ``Z_x`` are the mutual impedances between core/screen and between cables, respectively, as per sections 4.2.3.4, 4.2.3.5, 4.2.3.6 and 4.2.3.8 of the same document [cigre531](@cite). - -# Arguments - -- `r_in_co`: Internal radius of the phase conductor \\[m\\]. -- `r_ext_co`: External radius of the phase conductor \\[m\\]. -- `rho_co`: Electrical resistivity of the phase conductor material \\[Ω·m\\]. -- `mu_r_co`: Relative permeability of the phase conductor material \\[dimensionless\\]. -- `r_in_scr`: Internal radius of the metallic screen \\[m\\]. -- `r_ext_scr`: External radius of the metallic screen \\[m\\]. -- `rho_scr`: Electrical resistivity of the metallic screen material \\[Ω·m\\]. -- `mu_r_scr`: Relative permeability of the screen conductor material \\[dimensionless\\]. -- `S`: Spacing between conductors in trifoil configuration \\[m\\]. -- `rho_e`: Soil resistivity \\[Ω·m\\]. Default: 100 Ω·m. -- `f`: Frequency \\[Hz\\]. Default: [`f₀`](@ref). - -# Returns - -- Positive-sequence inductance per unit length of the cable system \\[H/m\\]. - -# Examples - -```julia -L = $(FUNCTIONNAME)(0.01, 0.015, 1.72e-8, 1.0, 0.02, 0.025, 2.83e-8, 1.0, S=0.1, rho_e=50, f=50) -println(L) # Output: Inductance value in H/m -``` - -# See also - -- [`calc_tubular_gmr`](@ref) -""" -function calc_inductance_trifoil( - r_in_co::T, - r_ext_co::T, - rho_co::T, - mu_r_co::T, - r_in_scr::T, - r_ext_scr::T, - rho_scr::T, - mu_r_scr::T, - S::T, - rho_e::T, - f::T, -) where {T <: REALSCALAR} - - ω = 2 * π * f - C = μ₀ / (2π) - - # Compute simplified earth return depth - DE = 659.0 * sqrt(rho_e / f) - - # Compute R'_E - RpE = (ω * μ₀) / 8.0 - - # Compute Xa - GMRa = calc_tubular_gmr(r_ext_co, r_in_co, mu_r_co) - Xa = (ω * C) * log(DE / GMRa) - - # Self impedance of a phase conductor with earth return - Ra = rho_co / (π * (r_ext_co^2 - r_in_co^2)) - Za = RpE + Ra + im * Xa - - # Compute rs - GMRscr = calc_tubular_gmr(r_ext_scr, r_in_scr, mu_r_scr) - # Compute Xs - Xs = (ω * C) * log(DE / GMRscr) - - # Self impedance of metal screen with earth return - Rs = rho_scr / (π * (r_ext_scr^2 - r_in_scr^2)) - Zs = RpE + Rs + im * Xs - - # Mutual impedance between phase conductor and screen - Zm = RpE + im * Xs - - # Compute GMD - GMD = S # trifoil, for flat use: 2^(1/3) * S - - # Compute Xap - Xap = (ω * C) * log(DE / GMD) - - # Equivalent mutual impedances between cables - Zx = RpE + im * Xap - - # Formula from CIGRE TB-531, 4.2.4.3, solid bonding - Z1_sb = (Za - Zx) - ((Zm - Zx)^2 / (Zs - Zx)) - - # Likewise, but for single point bonding - # Z1_sp = (Za - Zx) - return imag(Z1_sb) / ω -end - -function calc_inductance_trifoil( - r_in_co, - r_ext_co, - rho_co, - mu_r_co, - r_in_scr, - r_ext_scr, - rho_scr, - mu_r_scr, - S; - rho_e = 100.0, - f = f₀, -) - T = resolve_T( - r_in_co, - r_ext_co, - rho_co, - mu_r_co, - r_in_scr, - r_ext_scr, - rho_scr, - mu_r_scr, - S, - rho_e, - f, - ) - return calc_inductance_trifoil( - coerce_to_T(r_in_co, T), - coerce_to_T(r_ext_co, T), - coerce_to_T(rho_co, T), - coerce_to_T(mu_r_co, T), - coerce_to_T(r_in_scr, T), - coerce_to_T(r_ext_scr, T), - coerce_to_T(rho_scr, T), - coerce_to_T(mu_r_scr, T), - coerce_to_T(S, T), - coerce_to_T(rho_e, T), - coerce_to_T(f, T), - ) -end - -""" -$(TYPEDSIGNATURES) - -Calculates the geometric mean radius (GMR) of a circular wire array, using formula (62), page 335, of the book by Edward Rosa [rosa1908](@cite): - -```math -GMR = \\sqrt[n] {r n a^{n-1}} -``` - -where ``a`` is the layout radius, ``n`` is the number of wires, and ``r`` is the radius of each wire. - -# Arguments - -- `lay_rad`: Layout radius of the wire array \\[m\\]. -- `N`: Number of wires in the array \\[dimensionless\\]. -- `rad_wire`: Radius of an individual wire \\[m\\]. -- `mu_r`: Relative permeability of the wire material \\[dimensionless\\]. - -# Returns - -- Geometric mean radius (GMR) of the wire array \\[m\\]. - -# Examples - -```julia -lay_rad = 0.05 -N = 7 -rad_wire = 0.002 -mu_r = 1.0 -gmr = $(FUNCTIONNAME)(lay_rad, N, rad_wire, mu_r) -println(gmr) # Expected output: 0.01187... [m] -``` -""" -function calc_circstrands_gmr( - lay_rad::T, - N::Int, - rad_wire::T, - mu_r::T, -) where {T <: REALSCALAR} - gmr_wire = rad_wire * exp(-mu_r / 4) - log_gmr_array = log(gmr_wire * N * lay_rad^(N - 1)) / N - return exp(log_gmr_array) -end - -function calc_circstrands_gmr(lay_rad, N::Int, rad_wire, mu_r) - T = resolve_T(lay_rad, rad_wire, mu_r) - return calc_circstrands_gmr( - coerce_to_T(lay_rad, T), - N, - coerce_to_T(rad_wire, T), - coerce_to_T(mu_r, T), - ) -end - -""" -$(TYPEDSIGNATURES) - -Calculates the geometric mean radius (GMR) of a tubular conductor, using [6521501](@cite): - -```math -\\log GMR = \\log r_2 - \\mu_r \\left[ \\frac{r_1^4}{\\left(r_2^2 - r_1^2\\right)^2} \\log\\left(\\frac{r_2}{r_1}\\right) - \\frac{3r_1^2 - r_2^2}{4\\left(r_2^2 - r_1^2\\right)} \\right] -``` - -where ``\\mu_r`` is the material magnetic permeability (relative to free space), ``r_1`` and ``r_2`` are the inner and outer radii of the tubular conductor, respectively. If ``r_2`` is approximately equal to ``r_1`` , the tube collapses into a thin shell, and the GMR is equal to ``r_2``. If the tube becomes infinitely thick (e.g., ``r_2 \\gg r_1``), the GMR diverges to infinity. - -# Arguments - -- `r_ex`: External radius of the tubular conductor \\[m\\]. -- `r_in`: Internal radius of the tubular conductor \\[m\\]. -- `mu_r`: Relative permeability of the conductor material \\[dimensionless\\]. - -# Returns - -- Geometric mean radius (GMR) of the tubular conductor \\[m\\]. - -# Errors - -- Throws `ArgumentError` if `r_ex` is less than `r_in`. - -# Examples - -```julia -r_ex = 0.02 -r_in = 0.01 -mu_r = 1.0 -gmr = $(FUNCTIONNAME)(r_ex, r_in, mu_r) -println(gmr) # Expected output: ~0.0135 [m] -``` -""" -function calc_tubular_gmr(r_ex::T, r_in::T, mu_r::T) where {T <: REALSCALAR} - if (r_ex < r_in) || (r_ex <= 0.0) - throw( - ArgumentError( - "Invalid parameters: r_ex must be >= r_in and positive.", - ), - ) - end - - # Constants - if isapprox(r_in, r_ex) - # Tube collapses into a thin shell with infinitesimal thickness and the GMR is simply the radius - gmr = r_ex - elseif abs(r_in / r_ex) < eps() && abs(r_in) > TOL - # Tube becomes infinitely thick up to floating point precision - gmr = Inf - else - is_solid = isapprox(r_in, 0.0) - term1 = - is_solid ? 0.0 : - (r_in^4 / (r_ex^2 - r_in^2)^2) * log(r_ex / r_in) - term2 = (3 * r_in^2 - r_ex^2) / (4 * (r_ex^2 - r_in^2)) - Lin = (μ₀ * mu_r / (2 * π)) * (term1 - term2) - - # Compute the GMR - gmr = exp(log(r_ex) - (2 * π / μ₀) * Lin) - end - - return gmr -end - -function calc_tubular_gmr(r_ex, r_in, mu_r) - T = resolve_T(r_ex, r_in, mu_r) - return calc_tubular_gmr( - coerce_to_T(r_ex, T), - coerce_to_T(r_in, T), - coerce_to_T(mu_r, T), - ) -end - -""" -$(TYPEDSIGNATURES) - -Calculates the relative permeability (`mu_r`) based on the geometric mean radius (GMR) and conductor dimensions, by executing the inverse of [`calc_tubular_gmr`](@ref), and solving for `mu_r`: - -```math -\\log GMR = \\log r_2 - \\mu_r \\left[ \\frac{r_1^4}{\\left(r_2^2 - r_1^2\\right)^2} \\log\\left(\\frac{r_2}{r_1}\\right) - \\frac{3r_1^2 - r_2^2}{4\\left(r_2^2 - r_1^2\\right)} \\right] -``` - -```math -\\mu_r = -\\frac{\\left(\\log GMR - \\log r_2\\right)}{\\frac{r_1^4}{\\left(r_2^2 - r_1^2\\right)^2} \\log\\left(\\frac{r_2}{r_1}\\right) - \\frac{3r_1^2 - r_2^2}{4\\left(r_2^2 - r_1^2\\right)}} -``` - -where ``r_1`` is the inner radius and ``r_2`` is the outer radius. - -# Arguments - -- `gmr`: Geometric mean radius of the conductor \\[m\\]. -- `r_ex`: External radius of the conductor \\[m\\]. -- `r_in`: Internal radius of the conductor \\[m\\]. - -# Returns - -- Relative permeability (`mu_r`) of the conductor material \\[dimensionless\\]. - -# Errors - -- Throws `ArgumentError` if `r_ex` is less than `r_in`. - -# Notes - -Assumes a tubular geometry for the conductor, reducing to the solid case if `r_in` is zero. - -# Examples - -```julia -gmr = 0.015 -r_ex = 0.02 -r_in = 0.01 -mu_r = $(FUNCTIONNAME)(gmr, r_ex, r_in) -println(mu_r) # Expected output: ~1.7 [dimensionless] -``` - -# See also -- [`calc_tubular_gmr`](@ref) -""" -function calc_equivalent_mu(gmr::T, r_ex::T, r_in::T) where {T <: REALSCALAR} - if (r_ex < r_in) || (r_ex <= 0.0) - throw( - ArgumentError( - "Invalid parameters: r_ex must be >= r_in and positive.", - ), - ) - end - is_solid = isapprox(r_in, 0.0) || isapprox(r_in, r_ex) - term1 = - is_solid ? 0.0 : - (r_in^4 / (r_ex^2 - r_in^2)^2) * log(r_ex / r_in) - term2 = (3 * r_in^2 - r_ex^2) / (4 * (r_ex^2 - r_in^2)) - # Compute the log difference - log_diff = log(gmr) - log(r_ex) - - # Compute mu_r - mu_r = -log_diff / (term1 - term2) - - return mu_r -end - -function calc_equivalent_mu(gmr, r_ex, r_in) - T = resolve_T(gmr, r_ex, r_in) - return calc_equivalent_mu( - coerce_to_T(gmr, T), - coerce_to_T(r_ex, T), - coerce_to_T(r_in, T), - ) -end - -""" -$(TYPEDSIGNATURES) - -Calculates the shunt capacitance per unit length of a coaxial structure, using the standard formula for the capacitance of a coaxial structure [cigre531](@cite) [916943](@cite) [1458878](@cite): - -```math -C = \\frac{2 \\pi \\varepsilon_0 \\varepsilon_r}{\\log \\left(\\frac{r_{ext}}{r_{in}}\\right)} -``` -where ``\\varepsilon_0`` is the vacuum permittivity, ``\\varepsilon_r`` is the relative permittivity of the dielectric material, and ``r_{in}`` and ``r_{ext}`` are the inner and outer radii of the coaxial structure, respectively. - -# Arguments - -- `r_in`: Internal radius of the coaxial structure \\[m\\]. -- `r_ex`: External radius of the coaxial structure \\[m\\]. -- `epsr`: Relative permittivity of the dielectric material \\[dimensionless\\]. - -# Returns - -- Shunt capacitance per unit length \\[F/m\\]. - -# Examples - -```julia -r_in = 0.01 -r_ex = 0.02 -epsr = 2.3 -capacitance = $(FUNCTIONNAME)(r_in, r_ex, epsr) -println(capacitance) # Expected output: ~1.24e-10 [F/m] -``` -""" -function calc_shunt_capacitance( - r_in::T, - r_ex::T, - epsr::T, -) where {T <: REALSCALAR} - return 2 * π * ε₀ * epsr / log(r_ex / r_in) -end - -function calc_shunt_capacitance(r_in, r_ex, epsr) - T = resolve_T(r_in, r_ex, epsr) - return calc_shunt_capacitance( - coerce_to_T(r_in, T), - coerce_to_T(r_ex, T), - coerce_to_T(epsr, T), - ) -end - -""" -$(TYPEDSIGNATURES) - -Calculates the shunt conductance per unit length of a coaxial structure, using the improved model reported in [916943](@cite) [Karmokar2025](@cite) [4389974](@cite): - -```math -G = \\frac{2\\pi\\sigma}{\\log(\\frac{r_{ext}}{r_{in}})} -``` -where ``\\sigma = \\frac{1}{\\rho}`` is the conductivity of the dielectric/semiconducting material, ``r_{in}`` is the internal radius, and ``r_{ext}`` is the external radius of the coaxial structure. - -# Arguments - -- `r_in`: Internal radius of the coaxial structure \\[m\\]. -- `r_ex`: External radius of the coaxial structure \\[m\\]. -- `rho`: Resistivity of the dielectric/semiconducting material \\[Ω·m\\]. - -# Returns - -- Shunt conductance per unit length \\[S·m\\]. - -# Examples - -```julia -r_in = 0.01 -r_ex = 0.02 -rho = 1e9 -g = $(FUNCTIONNAME)(r_in, r_ex, rho) -println(g) # Expected output: 2.7169e-9 [S·m] -``` -""" -function calc_shunt_conductance(r_in::T, r_ex::T, rho::T) where {T <: REALSCALAR} - return 2 * π * (1 / rho) / log(r_ex / r_in) -end - -function calc_shunt_conductance(r_in, r_ex, rho) - T = resolve_T(r_in, r_ex, rho) - return calc_shunt_conductance( - coerce_to_T(r_in, T), - coerce_to_T(r_ex, T), - coerce_to_T(rho, T), - ) -end - - -""" -$(TYPEDSIGNATURES) - -Calculates the equivalent geometric mean radius (GMR) of a conductor after adding a new layer, by recursive application of the multizone stranded conductor defined as [yang2008gmr](@cite): - -```math -GMR_{eq} = {GMR_{i-1}}^{\\beta^2} \\cdot {GMR_{i}}^{(1-\\beta)^2} \\cdot {GMD}^{2\\beta(1-\\beta)} -``` -```math -\\beta = \\frac{S_{i-1}}{S_{i-1} + S_{i}} -``` -where: -- ``S_{i-1}`` is the cumulative cross-sectional area of the existing cable part, ``S_{i}`` is the total cross-sectional area after inclusion of the conducting layer ``{i}``. -- ``GMR_{i-1}`` is the cumulative GMR of the existing cable part, ``GMR_{i}`` is the GMR of the conducting layer ``{i}``. -- ``GMD`` is the geometric mean distance between the existing cable part and the new layer, calculated using [`calc_gmd`](@ref). - -# Arguments - -- `existing`: The existing cable part ([`AbstractCablePart`](@ref)). -- `new_layer`: The new layer being added ([`AbstractCablePart`](@ref)). - -# Returns - -- Updated equivalent GMR of the combined conductor \\[m\\]. - -# Examples - -```julia -material_props = Material(1.7241e-8, 1.0, 0.999994, 20.0, 0.00393) -conductor = Conductor(Strip(0.01, 0.002, 0.05, 10, material_props)) -new_layer = CircStrands(0.02, 0.002, 7, 15, material_props) -equivalent_gmr = $(FUNCTIONNAME)(conductor, new_layer) # Expected output: Updated GMR value [m] -``` - -# See also - -- [`calc_gmd`](@ref) -""" -function calc_equivalent_gmr( - existing::T, - new_layer::U, -) where {T <: AbstractCablePart, U <: AbstractCablePart} - beta = existing.cross_section / (existing.cross_section + new_layer.cross_section) - - DM = parentmodule(@__MODULE__) # DataModel - if isdefined(DM, :ConductorGroup) - CG = getproperty(DM, :ConductorGroup) - if existing isa CG - current_conductor = existing.layers[end] - else - current_conductor = existing - end - end - - # current_conductor = existing isa ConductorGroup ? existing.layers[end] : existing - gmd = calc_gmd(current_conductor, new_layer) - return existing.gmr^(beta^2) * new_layer.gmr^((1 - beta)^2) * - gmd^(2 * beta * (1 - beta)) -end - -# evil hackery to detect CircStrands types -@inline function _is_circstrands(x) - DM = parentmodule(@__MODULE__) # DataModel - return isdefined(DM, :CircStrands) && - (x isa getproperty(DM, :CircStrands)) # no compile-time ref to CircStrands -end - -""" -$(TYPEDSIGNATURES) - -Calculates the geometric mean distance (GMD) between two cable parts, by using the definition described in Grover [grover1981inductance](@cite): - -```math -\\log GMD = \\left(\\frac{\\sum_{i=1}^{n_1}\\sum_{j=1}^{n_2} (s_1 \\cdot s_2) \\cdot \\log(d_{ij})}{\\sum_{i=1}^{n_1}\\sum_{j=1}^{n_2} (s_1 \\cdot s_2)}\\right) -``` - -where: -- ``d_{ij}`` is the Euclidean distance between elements ``i`` and ``j``. -- ``s_1`` and ``s_2`` are the cross-sectional areas of the respective elements. -- ``n_1`` and ``n_2`` are the number of sub-elements in each cable part. - -# Arguments - -- `co1`: First cable part ([`AbstractCablePart`](@ref)). -- `co2`: Second cable part ([`AbstractCablePart`](@ref)). - -# Returns - -- Geometric mean distance between the cable parts \\[m\\]. - -# Notes - -For concentric structures, the GMD converges to the external radii of the outermost element. - -!!! info "Numerical stability" - This implementation uses a weighted sum of logarithms rather than the traditional product formula ``\\Pi(d_{ij})^{(1/n)}`` found in textbooks. The logarithmic approach prevents numerical underflow/overflow when dealing with many conductors or extreme distance ratios, making it significantly more stable for practical calculations. - -# Examples - -```julia -material_props = Material(1.7241e-8, 1.0, 0.999994, 20.0, 0.00393) -circstrands1 = CircStrands(0.01, 0.002, 7, 10, material_props) -circstrands2 = CircStrands(0.02, 0.002, 7, 15, material_props) -gmd = $(FUNCTIONNAME)(circstrands1, circstrands2) # Expected output: GMD value [m] - -strip = Strip(0.01, 0.002, 0.05, 10, material_props) -tubular = Tubular(0.01, 0.02, material_props) -gmd = $(FUNCTIONNAME)(strip, tubular) # Expected output: GMD value [m] -``` - -# See also - -- [`calc_circstrands_coords`](@ref) -- [`calc_equivalent_gmr`](@ref) -""" -function calc_gmd(co1::T, co2::U) where {T <: AbstractCablePart, U <: AbstractCablePart} - - if _is_circstrands(co1) #co1 isa CircStrands - coords1 = calc_circstrands_coords(co1.num_wires, co1.radius_wire, co1.r_in) - n1 = co1.num_wires - r1 = co1.radius_wire - s1 = pi * r1^2 - else - coords1 = [(0.0, 0.0)] - n1 = 1 - r1 = co1.r_ex - s1 = co1.cross_section - end - - # if co2 isa CircStrands - if _is_circstrands(co2) - coords2 = calc_circstrands_coords(co2.num_wires, co2.radius_wire, co2.r_in) - n2 = co2.num_wires - r2 = co2.radius_wire - s2 = pi * r2^2 - else - coords2 = [(0.0, 0.0)] - n2 = 1 - r2 = co2.r_ex - s2 = co2.cross_section - end - - log_sum = 0.0 - area_weights = 0.0 - - for i in 1:n1 - for j in 1:n2 - # Pair-wise distances - x1, y1 = coords1[i] - x2, y2 = coords2[j] - d_ij = sqrt((x1 - x2)^2 + (y1 - y2)^2) - if d_ij > eps() - # The GMD is computed as the Euclidean distance from center-to-center - log_dij = log(d_ij) - else - # This means two concentric structures (solid/strip or tubular, tubular/strip or tubular, strip/strip or tubular) - # In all cases the GMD is the outermost radius - # max(r1, r2) - log_dij = log(max(r1, r2)) - end - log_sum += (s1 * s2) * log_dij - area_weights += (s1 * s2) - end - end - return exp(log_sum / area_weights) -end -""" -$(TYPEDSIGNATURES) - -Calculates the solenoid correction factor for magnetic permeability in insulated cables with helical conductors ([`CircStrands`](@ref)), using the formula from Gudmundsdottir et al. [5743045](@cite): - -```math -\\mu_{r, sol} = 1 + \\frac{2 \\pi^2 N^2 (r_{ins, ext}^2 - r_{con, ext}^2)}{\\log(r_{ins, ext}/r_{con, ext})} -``` - -where: -- ``N`` is the number of turns per unit length. -- ``r_{con, ext}`` is the conductor external radius. -- ``r_{ins, ext}`` is the insulator external radius. - -# Arguments - -- `num_turns`: Number of turns per unit length \\[1/m\\]. -- `radius_ext_con`: External radius of the conductor \\[m\\]. -- `radius_ext_ins`: External radius of the insulator \\[m\\]. - -# Returns - -- Correction factor for the insulator magnetic permeability \\[dimensionless\\]. - -# Examples - -```julia -# Cable with 10 turns per meter, conductor radius 5 mm, insulator radius 10 mm -correction = $(FUNCTIONNAME)(10, 0.005, 0.01) # Expected output: > 1.0 [dimensionless] - -# Non-helical cable (straight conductor) -correction = $(FUNCTIONNAME)(NaN, 0.005, 0.01) # Expected output: 1.0 [dimensionless] -``` -""" -function calc_solenoid_correction( - num_turns::T, - radius_ext_con::T, - radius_ext_ins::T, -) where {T <: REALSCALAR} - if isnan(num_turns) - return 1.0 - else - return 1.0 + - 2 * num_turns^2 * pi^2 * (radius_ext_ins^2 - radius_ext_con^2) / - log(radius_ext_ins / radius_ext_con) - end -end - -function calc_solenoid_correction( - num_turns, - radius_ext_con, - radius_ext_ins, -) - T = resolve_T(num_turns, radius_ext_con, radius_ext_ins) - return calc_solenoid_correction( - coerce_to_T(num_turns, T), - coerce_to_T(radius_ext_con, T), - coerce_to_T(radius_ext_ins, T), - ) -end - -""" -$(TYPEDSIGNATURES) - -Calculates the equivalent resistivity of a solid tubular conductor, using the formula [916943](@cite): - -```math -\\rho_{eq} = R_{eq} S_{eff} = R_{eq} \\pi (r_{ext}^2 - r_{in}^2) -``` - -where ``S_{eff}`` is the effective cross-sectional area of the tubular conductor. - -# Arguments - -- `R`: Resistance of the conductor \\[Ω\\]. -- `radius_ext_con`: External radius of the tubular conductor \\[m\\]. -- `radius_in_con`: Internal radius of the tubular conductor \\[m\\]. - -# Returns - -- Equivalent resistivity of the tubular conductor \\[Ω·m\\]. - -# Examples - -```julia -rho_eq = $(FUNCTIONNAME)(0.01, 0.02, 0.01) # Expected output: ~9.42e-4 [Ω·m] -``` -""" -function calc_equivalent_rho( - R::T, - radius_ext_con::T, - radius_in_con::T, -) where {T <: REALSCALAR} - eff_conductor_area = π * (radius_ext_con^2 - radius_in_con^2) - return R * eff_conductor_area -end - -function calc_equivalent_rho(R, radius_ext_con, radius_in_con) - T = resolve_T(R, radius_ext_con, radius_in_con) - return calc_equivalent_rho( - coerce_to_T(R, T), - coerce_to_T(radius_ext_con, T), - coerce_to_T(radius_in_con, T), - ) -end - -""" -$(TYPEDSIGNATURES) - -Calculates the equivalent permittivity for a coaxial cable insulation, using the formula [916943](@cite): - -```math -\\varepsilon_{eq} = \\frac{C_{eq} \\log(\\frac{r_{ext}}{r_{in}})}{2\\pi \\varepsilon_0} -``` - -where ``\\varepsilon_0`` is the permittivity of free space. - -# Arguments - -- `C_eq`: Equivalent capacitance of the insulation \\[F/m\\]. -- `r_ex`: External radius of the insulation \\[m\\]. -- `r_in`: Internal radius of the insulation \\[m\\]. - -# Returns - -- Equivalent relative permittivity of the insulation \\[dimensionless\\]. - -# Examples - -```julia -eps_eq = $(FUNCTIONNAME)(1e-10, 0.01, 0.005) # Expected output: ~2.26 [dimensionless] -``` - -# See also -- [`ε₀`](@ref) -""" -function calc_equivalent_eps(C_eq::T, r_ex::T, r_in::T) where {T <: REALSCALAR} - return (C_eq * log(r_ex / r_in)) / (2 * pi) / ε₀ -end - -function calc_equivalent_eps(C_eq, r_ex, r_in) - T = resolve_T(C_eq, r_ex, r_in) - return calc_equivalent_eps( - coerce_to_T(C_eq, T), - coerce_to_T(r_ex, T), - coerce_to_T(r_in, T), - ) -end - -""" -$(TYPEDSIGNATURES) - -Calculates the equivalent loss factor (tangent) of a dielectric material: - -```math -\\tan \\delta = \\frac{G_{eq}}{\\omega \\cdot C_{eq}} -``` - -where ``\\tan \\delta`` is the loss factor (tangent). - -# Arguments - -- `G_eq`: Equivalent conductance of the material \\[S·m\\]. -- `C_eq`: Equivalent capacitance of the material \\[F/m\\]. -- `ω`: Angular frequency \\[rad/s\\]. - -# Returns - -- Equivalent loss factor of the dielectric material \\[dimensionless\\]. - -# Examples - -```julia -loss_factor = $(FUNCTIONNAME)(1e-8, 1e-10, 2π*50) # Expected output: ~0.0318 [dimensionless] -``` -""" -function calc_equivalent_lossfact(G_eq::T, C_eq::T, ω::T) where {T <: REALSCALAR} - return G_eq / (ω * C_eq) -end - -function calc_equivalent_lossfact(G_eq, C_eq, ω) - T = resolve_T(G_eq, C_eq, ω) - return calc_equivalent_lossfact( - coerce_to_T(G_eq, T), - coerce_to_T(C_eq, T), - coerce_to_T(ω, T), - ) -end - -""" -$(TYPEDSIGNATURES) - -Calculates the effective conductivity of a dielectric material from the known conductance (related to the loss factor ``\\tan \\delta``) via [916943](@cite) [Karmokar2025](@cite) [4389974](@cite): - -```math -\\sigma_{eq} = \\frac{G_{eq}}{2\\pi} \\log(\\frac{r_{ext}}{r_{in}}) -``` -where ``\\sigma_{eq} = \\frac{1}{\\rho_{eq}}`` is the conductivity of the dielectric/semiconducting material, ``G_{eq}`` is the shunt conductance per unit length, ``r_{in}`` is the internal radius, and ``r_{ext}`` is the external radius of the coaxial structure. - -# Arguments - -- `G_eq`: Equivalent conductance of the material \\[S·m\\]. -- `r_in`: Internal radius of the coaxial structure \\[m\\]. -- `r_ex`: External radius of the coaxial structure \\[m\\]. - -# Returns - -- Effective material conductivity per unit length \\[S·m\\]. - -# Examples - -```julia -Geq = 2.7169e-9 -sigma_eq = $(FUNCTIONNAME)(G_eq, r_in, r_ex) -``` -""" -function calc_sigma_lossfact(G_eq::T, r_in::T, r_ex::T) where {T <: REALSCALAR} - return G_eq * log(r_ex / r_in) / (2 * pi) -end - -function calc_sigma_lossfact(G_eq, r_in, r_ex) - T = resolve_T(G_eq, r_in, r_ex) - return calc_sigma_lossfact( - coerce_to_T(G_eq, T), - coerce_to_T(r_in, T), - coerce_to_T(r_ex, T), - ) -end - -end # module BaseParams diff --git a/src/datamodel/baseparams/dielectrics.jl b/src/datamodel/baseparams/dielectrics.jl new file mode 100644 index 000000000..0bf0a3d66 --- /dev/null +++ b/src/datamodel/baseparams/dielectrics.jl @@ -0,0 +1,127 @@ +""" +$(TYPEDSIGNATURES) + +Calculate coaxial shunt capacitance +``C=2\\pi\\varepsilon_0\\varepsilon_r/\\log(r_{ex}/r_{in})`` \\[F/m\\]. + +`r_in` and `r_ex` are the inner and outer dielectric radii in meters, +with ``0 zero(rin) && rex > rin || throw(DomainError( + (rin, rex), "shunt capacitance requires 0 < r_in < r_ex" + )) + permittivity >= zero(permittivity) || throw(DomainError( + permittivity, "relative permittivity must be nonnegative" + )) + return 2 * (one(rin) * π) * vacuum_permittivity(typeof(rin)) * permittivity / log(rex / rin) +end + +""" +$(TYPEDSIGNATURES) + +Calculate coaxial shunt conductance +``G=2\\pi/(\\rho\\log(r_{ex}/r_{in}))`` \\[S/m\\]. + +`r_in` and `r_ex` are the inner and outer dielectric radii in meters, +with ``0 zero(rin) && rex > rin || throw(DomainError( + (rin, rex), "shunt conductance requires 0 < r_in < r_ex" + )) + resistivity > zero(resistivity) || throw(DomainError( + resistivity, "resistivity must be positive" + )) + return 2 * (one(rin) * π) / resistivity / log(rex / rin) +end + +""" +$(TYPEDSIGNATURES) + +Combine two radial dielectric layers connected in series. Each layer is +represented by the shunt admittance +``Y_i=G_i+\\mathrm{j}\\omega C_i``. The equivalent is + +```math +Y_{eq}=\\frac{Y_1Y_2}{Y_1+Y_2}. +``` + +# Arguments + +- `conductance1`: shunt conductance of the accumulated dielectric \\[S/m\\]. +- `capacitance1`: shunt capacitance of the accumulated dielectric \\[F/m\\]. +- `conductance2`: shunt conductance of the added dielectric \\[S/m\\]. +- `capacitance2`: shunt capacitance of the added dielectric \\[F/m\\]. +- `omega`: angular reference frequency \\[rad/s\\]. + +# Returns + +- Named tuple containing the equivalent `conductance` \\[S/m\\] and + `capacitance` \\[F/m\\]. +""" +function series_shunt_admittance( + conductance1::Real, + capacitance1::Real, + conductance2::Real, + capacitance2::Real, + omega::Real +) + G1, C1, G2, C2, angular_frequency = promote( + float(conductance1), float(capacitance1), + float(conductance2), float(capacitance2), float(omega) + ) + angular_frequency > zero(angular_frequency) || throw(DomainError( + angular_frequency, "angular frequency must be positive" + )) + equivalent = parallel( + complex(G1, angular_frequency * C1), + complex(G2, angular_frequency * C2) + ) + return ( + conductance = real(equivalent), + capacitance = imag(equivalent) / angular_frequency + ) +end + +""" +$(TYPEDSIGNATURES) + +Recover equivalent relative permittivity from coaxial capacitance: +``\\varepsilon_{eq}=C\\log(r_{ex}/r_{in})/(2\\pi\\varepsilon_0)``. + +`capacitance` is nonnegative capacitance per unit length in F/m. +`r_ex` and `r_in` are the outer and inner dielectric radii in meters, +with ``0= zero(value) || throw(DomainError(value, "capacitance must be nonnegative")) + rin > zero(rin) && rex > rin || throw(DomainError( + (rin, rex), "equivalent permittivity requires 0 < r_in < r_ex" + )) + return value * log(rex / rin) / (2 * (one(rin) * π) * vacuum_permittivity(typeof(rin))) +end + +""" +$(TYPEDSIGNATURES) + +Recover equivalent conductivity from coaxial conductance: +``\\sigma_{eq}=G\\log(r_{ex}/r_{in})/(2\\pi)``. + +`conductance` is nonnegative conductance per unit length in S/m. +`r_in` and `r_ex` are the inner and outer dielectric radii in meters, +with ``0= zero(value) || throw(DomainError(value, "conductance must be nonnegative")) + rin > zero(rin) && rex > rin || throw(DomainError( + (rin, rex), "equivalent conductivity requires 0 < r_in < r_ex" + )) + return value * log(rex / rin) / (2 * (one(rin) * π)) +end diff --git a/src/datamodel/baseparams/geometry.jl b/src/datamodel/baseparams/geometry.jl new file mode 100644 index 000000000..d77326501 --- /dev/null +++ b/src/datamodel/baseparams/geometry.jl @@ -0,0 +1,113 @@ +""" +$(TYPEDSIGNATURES) + +Return `(mean_diameter, pitch_length, overlength)` for a helical radial layer: + +```math +D_e=r_{in}+r_{ex},\\qquad L_p=\\lambda D_e,\\qquad +k=\\sqrt{1+(\\pi D_e/L_p)^2}. +``` + +`r_in` and `r_ex` are the inner and outer layer radii in meters, with +``0\\le r_{in}\\le r_{ex}``. `lay_ratio` is the nonnegative, dimensionless +ratio ``\\lambda=L_p/D_e`` used in EN 50182. All inputs must be finite. +The returned diameter and pitch are in meters. Overlength ``k`` is dimensionless. +The overlength equals one when pitch is zero. `lay_ratio=0` represents +a straight layer and returns a pitch of zero. +""" +function helix(r_in::Real, r_ex::Real, lay_ratio::Real) + rin, rex, ratio = promote(float(r_in), float(r_ex), float(lay_ratio)) + all(isfinite, (rin, rex, ratio)) || throw(DomainError( + (rin, rex, ratio), "helix geometry must be finite" + )) + rin >= zero(rin) || throw(DomainError(rin, "inner radius must be nonnegative")) + rex >= rin || + throw(DomainError(rex, "outer radius must not be smaller than inner radius")) + ratio >= zero(ratio) || throw(DomainError(ratio, "lay ratio must be nonnegative")) + mean_diameter = rin + rex + pitch_length = ratio * mean_diameter + overlength = iszero(pitch_length) ? one(pitch_length) : + sqrt(one(pitch_length) + + (oftype(pitch_length, π) * mean_diameter / pitch_length)^2) + return mean_diameter, pitch_length, overlength +end + +""" +$(TYPEDSIGNATURES) + +Return the centers of `num_wires` equally spaced circular wires. For +``N>1`` wires, the center of wire ``i=0,\\ldots,N-1`` is + +```math +C+r_l(\\cos(2\\pi i/N),\\sin(2\\pi i/N)),\\qquad +r_l=r_{in}+r_w. +``` + +`num_wires` is the nonnegative integer count ``N``. `radius_wire` is the +positive wire radius ``r_w`` and `r_in` is the nonnegative inner radius of +the wire layer, both in meters. `C` is the layer center `(x, y)` in meters +and defaults to `(0, 0)`. Coordinates and radii must be finite. + +The result is a vector of `(x, y)` tuples in meters, ordered counterclockwise +from the positive x direction. Zero wires return an empty vector. One wire +is placed at `C`. +""" +function wire_coordinates( + num_wires::Integer, + radius_wire::Real, + r_in::Real; + C = nothing +) + num_wires >= 0 || throw(DomainError(num_wires, "number of wires must be nonnegative")) + center = C === nothing ? (zero(radius_wire), zero(radius_wire)) : C + rw, rin, cx, cy = promote( + float(radius_wire), float(r_in), float(center[1]), float(center[2]) + ) + all(isfinite, (rw, rin, cx, cy)) || throw(DomainError( + (rw, rin, cx, cy), "wire geometry must be finite" + )) + rw > zero(rw) || throw(DomainError(rw, "wire radius must be positive")) + rin >= zero(rin) || throw(DomainError(rin, "inner radius must be nonnegative")) + radius = num_wires == 1 ? zero(rin) : rin + rw + num_wires == 0 && return Tuple{typeof(rin), typeof(rin)}[] + step = 2 * (one(rin) * π) / num_wires + return [(cx + radius * cos(index * step), cy + radius * sin(index * step)) + for index in 0:(num_wires - 1)] +end + +function wire_coordinates(num_wires::Integer, radius_wire::Real, r_in::Real, C::Tuple) + wire_coordinates(num_wires, radius_wire, r_in; C) +end + +""" +$(TYPEDSIGNATURES) + +Return the dimensionless permeability factor for a helical solenoid: + +```math +k_\\mu=1+\\frac{2\\pi^2N^2(r_i^2-r_c^2)}{\\log(r_i/r_c)}. +``` + +`turns_per_length` is the nonnegative number of turns per meter ``N``. +`r_con` is the conductor radius ``r_c`` and `r_ins` is the outer insulation +radius ``r_i``, both in meters, with ``0\\le r_c= zero(turns) || throw(DomainError(turns, "turns per unit length must be nonnegative")) + conductor >= zero(conductor) || throw(DomainError( + conductor, "conductor radius must be nonnegative" + )) + insulator > conductor || throw(DomainError( + insulator, "insulator radius must exceed conductor radius" + )) + pi_value = one(turns) * π + return one(turns) + + 2 * pi_value^2 * turns^2 * + (insulator^2 - conductor^2) / log(insulator / conductor) +end diff --git a/src/datamodel/baseparams/inductance.jl b/src/datamodel/baseparams/inductance.jl new file mode 100644 index 000000000..4cb38d7b9 --- /dev/null +++ b/src/datamodel/baseparams/inductance.jl @@ -0,0 +1,197 @@ +""" +$(TYPEDSIGNATURES) + +Calculate the GMR of a circular wire array: +``GMR=(r_g N a^{N-1})^{1/N}``, with +``r_g=r_w\\exp(-\\mu_r/4)``. + +`lay_radius` is the radius `a` of the strand-center circle in meters. +`count` is the positive integer strand count `N`. `wire_radius` is the +positive strand radius `r_w` in meters. `mu_r` is the positive, +dimensionless relative permeability. The lay radius must be positive for +multiple wires and may be zero for one wire. The returned geometric mean +radius (GMR) is in meters. +""" +function strand_gmr(lay_radius::Real, count::Integer, wire_radius::Real, mu_r::Real) + count > 0 || throw(DomainError(count, "wire count must be positive")) + radius, wire, permeability = promote( + float(lay_radius), float(wire_radius), float(mu_r) + ) + wire > zero(wire) || throw(DomainError(wire, "wire radius must be positive")) + radius >= zero(radius) || throw(DomainError(radius, "lay radius must be nonnegative")) + (count == 1 || radius > zero(radius)) || throw(DomainError( + radius, "lay radius must be positive for a multi-wire layer" + )) + permeability > zero(permeability) || throw(DomainError( + permeability, "relative permeability must be positive" + )) + wire_gmr = wire * exp(-permeability / 4) + return exp(log(wire_gmr * count * radius^(count - 1)) / count) +end + +""" +$(TYPEDSIGNATURES) + +Calculate the GMR of identical circular strands from their resolved centers: + +```math +\\log GMR=\\frac{1}{N^2}\\left[ +N\\log r_g+2\\sum_{i=1}^{N}\\sum_{j=i+1}^{N}\\log d_{ij} +\\right],\\qquad +r_g=r_w\\exp(-\\mu_r/4). +``` + +# Arguments + +- `coordinates`: strand-center coordinates \\[m\\]. +- `wire_radius`: strand radius \\[m\\]. +- `mu_r`: relative permeability \\[dimensionless\\]. + +# Returns + +- Geometric mean radius of the complete strand set \\[m\\]. +""" +function strand_gmr(coordinates, wire_radius::Real, mu_r::Real) + isempty(coordinates) && throw(ArgumentError( + "strand GMR requires at least one center coordinate" + )) + wire, permeability = promote(float(wire_radius), float(mu_r)) + wire > zero(wire) || throw(DomainError( + wire, "wire radius must be positive" + )) + permeability > zero(permeability) || throw(DomainError( + permeability, "relative permeability must be positive" + )) + count = length(coordinates) + logarithmic_sum = count * log(tubular_gmr(wire, zero(wire), permeability)) + for left in eachindex(coordinates) + for right in (left + 1):length(coordinates) + distance = hypot( + coordinates[left][1] - coordinates[right][1], + coordinates[left][2] - coordinates[right][2] + ) + distance > zero(distance) || throw(ArgumentError( + "strand centers must be distinct" + )) + logarithmic_sum += 2log(distance) + end + end + return exp(logarithmic_sum / count^2) +end + +""" +$(TYPEDSIGNATURES) + +Calculate the GMR of an annular conductor. + +```math +\\log GMR=\\log r_2-\\mu_r\\left[ +\\frac{r_1^4}{(r_2^2-r_1^2)^2}\\log\\frac{r_2}{r_1} +-\\frac{3r_1^2-r_2^2}{4(r_2^2-r_1^2)}\\right]. +``` + +`r_in` and `r_ex` are the inner radius ``r_1`` and outer radius ``r_2`` +in meters, with ``0≤r_1≤r_2`` and ``r_2>0``. `mu_r` is the positive, +dimensionless relative permeability. The returned GMR is in meters. + +# Notes + +The expression reduces to the solid-conductor result at zero inner radius and +to the outer radius for an infinitesimally thin shell. +""" +function tubular_gmr(r_ex::Real, r_in::Real, mu_r::Real) + rex, rin, permeability = promote(float(r_ex), float(r_in), float(mu_r)) + rin >= zero(rin) && rex > zero(rex) && rex >= rin || throw(DomainError( + (rin, rex), "outer radius must be positive and not smaller than inner radius" + )) + permeability > zero(permeability) || throw(DomainError( + permeability, "relative permeability must be positive" + )) + isapprox(rin, rex) && return rex + iszero(rin) && return rex * exp(-permeability / 4) + denominator = rex^2 - rin^2 + term1 = rin^4 / denominator^2 * log(rex / rin) + term2 = (3 * rin^2 - rex^2) / (4 * denominator) + return exp(log(rex) - permeability * (term1 - term2)) +end + +""" +$(TYPEDSIGNATURES) + +Calculate the GMR of two heterogeneous conductor zones: + +```math +GMR_{eq}=GMR_1^{\\beta^2}GMR_2^{(1-\\beta)^2} + GMD^{2\\beta(1-\\beta)},\\qquad +\\beta=\\frac{A_1}{A_1+A_2}. +``` + +# Arguments + +- `gmr1`: GMR of the accumulated conductor zone \\[m\\]. +- `area1`: cross-sectional area of the accumulated conductor zone \\[m²\\]. +- `gmr2`: GMR of the added conductor zone \\[m\\]. +- `area2`: cross-sectional area of the added conductor zone \\[m²\\]. +- `gmd`: geometric mean distance between the zones \\[m\\]. + +# Returns + +- Equivalent GMR of the combined conductor \\[m\\]. +""" +function equivalent_gmr( + gmr1::Real, + area1::Real, + gmr2::Real, + area2::Real, + gmd::Real +) + first_gmr, first_area, second_gmr, second_area, distance = promote( + float(gmr1), float(area1), float(gmr2), float(area2), float(gmd) + ) + first_gmr > zero(first_gmr) && second_gmr > zero(second_gmr) || + throw(DomainError((first_gmr, second_gmr), "GMR values must be positive")) + first_area > zero(first_area) && second_area > zero(second_area) || + throw(DomainError((first_area, second_area), "conductor areas must be positive")) + distance > zero(distance) || throw(DomainError( + distance, "geometric mean distance must be positive" + )) + fraction = first_area / (first_area + second_area) + complement = one(fraction) - fraction + return first_gmr^(fraction^2) * second_gmr^(complement^2) * + distance^(2 * fraction * complement) +end + +""" +$(TYPEDSIGNATURES) + +Recover dimensionless relative permeability from the GMR of an annular conductor: + +```math +\\mu_r=\\frac{\\log(r_{ex}/GMR)}{A-B},\\qquad +A=\\frac{r_{in}^4}{(r_{ex}^2-r_{in}^2)^2}\\log\\frac{r_{ex}}{r_{in}},\\qquad +B=\\frac{3r_{in}^2-r_{ex}^2}{4(r_{ex}^2-r_{in}^2)}. +``` + +`gmr`, `r_ex`, and `r_in` are the geometric mean radius, outer conductor +radius, and inner conductor radius in meters. The GMR and outer radius must +be positive, with ``0≤r_{in}≤r_{ex}``. A `NaN` input returns `NaN`. + +For a solid conductor (`r_in=0`), the result is +``-4\\log(GMR/r_{ex})``. The implementation sets ``A=0`` when the radii are +approximately equal. In the thin-shell limit, GMR approaches the outer radius +regardless of permeability, so the inverse does not determine permeability +reliably. +""" +function equivalent_mu(gmr::Real, r_ex::Real, r_in::Real) + radius, rex, rin = promote(float(gmr), float(r_ex), float(r_in)) + any(isnan, (radius, rex, rin)) && return radius + rex + rin + radius > zero(radius) || throw(DomainError(radius, "GMR must be positive")) + rin >= zero(rin) && rex > zero(rex) && rex >= rin || throw(DomainError( + (rin, rex), "outer radius must be positive and not smaller than inner radius" + )) + is_solid = iszero(rin) || isapprox(rin, rex) + denominator = rex^2 - rin^2 + term1 = is_solid ? zero(rin) : rin^4 / denominator^2 * log(rex / rin) + term2 = (3 * rin^2 - rex^2) / (4 * denominator) + return -(log(radius) - log(rex)) / (term1 - term2) +end diff --git a/src/datamodel/baseparams/resistance.jl b/src/datamodel/baseparams/resistance.jl new file mode 100644 index 000000000..3a1a25d0c --- /dev/null +++ b/src/datamodel/baseparams/resistance.jl @@ -0,0 +1,104 @@ +""" +$(TYPEDSIGNATURES) + +Calculate the equivalent temperature coefficient of two parallel conductors: + +```math +\\alpha_{eq}=\\frac{\\alpha_1R_2+\\alpha_2R_1}{R_1+R_2}. +``` + +`alpha1` and `alpha2` are temperature coefficients in K⁻¹. `R1` and `R2` +are the corresponding resistances at the same reference temperature, both +in Ω/m or both in Ω. The returned coefficient is in K⁻¹. +""" +function equivalent_alpha(alpha1::Real, R1::Real, alpha2::Real, R2::Real) + a1, r1, a2, r2 = promote(float(alpha1), float(R1), float(alpha2), float(R2)) + return (a1 * r2 + a2 * r1) / (r1 + r2) +end + +""" +$(TYPEDSIGNATURES) + +Return the parallel equivalent ``Z_{eq}=Z_1Z_2/(Z_1+Z_2)``. + +`Z1` and `Z2` must have the same units and length basis, such as Ω/m +or Ω. The result has those same units. An infinite input returns the other +input. 2 zero inputs return zero. +""" +function parallel(Z1::Number, Z2::Number) + z1, z2 = promote(Z1, Z2) + isinf(z1) && return z2 + isinf(z2) && return z1 + iszero(z1) && iszero(z2) && return zero(z1) + return z1 * z2 / (z1 + z2) +end + +""" +$(TYPEDSIGNATURES) + +Calculate reference-state strip resistance per unit length: + +```math +R=\\frac{\\rho}{w t}. +``` + +# Arguments + +- `thickness`: strip thickness \\[m\\]. +- `width`: strip width \\[m\\]. +- `rho`: material resistivity at its reference temperature \\[Ω·m\\]. + +# Returns + +- Strip resistance per unit length \\[Ω/m\\]. +""" +function strip_resistance( + thickness::Real, + width::Real, + rho::Real +) + t, w, resistivity = promote(float(thickness), float(width), float(rho)) + return resistivity / (t * w) +end + +""" +$(TYPEDSIGNATURES) + +Calculate reference-state tubular resistance per unit length: + +```math +R=\\frac{\\rho}{\\pi(r_{ex}^2-r_{in}^2)}. +``` + +# Arguments + +- `r_in`: inner conductor radius \\[m\\]. +- `r_ex`: outer conductor radius \\[m\\]. +- `rho`: material resistivity at its reference temperature \\[Ω·m\\]. + +# Returns + +- Tubular resistance per unit length \\[Ω/m\\]. +""" +function tubular_resistance( + r_in::Real, + r_ex::Real, + rho::Real +) + rin, rex, resistivity = promote(float(r_in), float(r_ex), float(rho)) + return resistivity / ((one(rin) * π) * (rex^2 - rin^2)) +end + +""" +$(TYPEDSIGNATURES) + +Recover the equivalent resistivity from resistance and annular area: +``\\rho_{eq}=R\\pi(r_{ex}^2-r_{in}^2)``. + +`R` is resistance per unit length in Ω/m. `r_ex` and `r_in` are the outer +and inner conductor radii in meters. The returned resistivity is in Ω·m. +""" +function equivalent_rho(R::Real, r_ex::Real, r_in::Real) + resistance, rex, rin = promote(float(R), float(r_ex), float(r_in)) + return resistance * (one(rin) * π) * (rex^2 - rin^2) +end diff --git a/src/datamodel/cablecomponent.jl b/src/datamodel/cablecomponent.jl deleted file mode 100644 index 344ef3691..000000000 --- a/src/datamodel/cablecomponent.jl +++ /dev/null @@ -1,246 +0,0 @@ - -""" -$(TYPEDEF) - -Represents a [`CableComponent`](@ref), i.e. a group of [`AbstractCablePart`](@ref) objects, with the equivalent geometric and material properties: - -$(TYPEDFIELDS) - -!!! info "Definition & application" - Cable components operate as containers for multiple cable parts, allowing the calculation of effective electromagnetic (EM) properties (``\\sigma, \\varepsilon, \\mu``). This is performed by transforming the physical objects within the [`CableComponent`](@ref) into one equivalent coaxial homogeneous structure comprised of one conductor and one insulator, each one represented by effective [`Material`](@ref) types stored in `conductor_props` and `insulator_props` fields. - - The effective properties approach is widely adopted in EMT-type simulations, and involves locking the internal and external radii of the conductor and insulator parts, respectively, and calculating the equivalent EM properties in order to match the previously determined values of R, L, C and G [916943](@cite) [1458878](@cite). - - In applications, the [`CableComponent`](@ref) type is mapped to the main cable structures described in manufacturer datasheets, e.g., core, sheath, armor and jacket. -""" -mutable struct CableComponent{T <: REALSCALAR} - "Cable component identification (e.g. core/sheath/armor)." - id::String - "The conductor group containing all conductive parts." - conductor_group::ConductorGroup{T} - "Effective properties of the equivalent coaxial conductor." - conductor_props::Material{T} - "The insulator group containing all insulating parts." - insulator_group::InsulatorGroup{T} - "Effective properties of the equivalent coaxial insulator." - insulator_props::Material{T} - - @doc """ - $(TYPEDSIGNATURES) - - Initializes a [`CableComponent`](@ref) object based on its constituent conductor and insulator groups. The constructor performs the following sequence of steps: - - 1. Validate that the conductor and insulator groups have matching radii at their interface. - 2. Obtain the lumped-parameter values (R, L, C, G) from the conductor and insulator groups, which are computed within their respective constructors. - 3. Calculate the correction factors and equivalent electromagnetic properties of the conductor and insulator groups: - - - | Quantity | Symbol | Function | - |----------|--------|----------| - | Resistivity (conductor) | ``\\rho_{con}`` | [`calc_equivalent_rho`](@ref) | - | Permeability (conductor) | ``\\mu_{con}`` | [`calc_equivalent_mu`](@ref) | - | Resistivity (insulator) | ``\\rho_{ins}`` | [`calc_sigma_lossfact`](@ref) | - | Permittivity (insulation) | ``\\varepsilon_{ins}`` | [`calc_equivalent_eps`](@ref) | - | Permeability (insulation) | ``\\mu_{ins}`` | [`calc_solenoid_correction`](@ref) | - - # Arguments - - - `id`: Cable component identification (e.g. core/sheath/armor). - - `conductor_group`: The conductor group containing all conductive parts. - - `insulator_group`: The insulator group containing all insulating parts. - - # Returns - - A [`CableComponent`](@ref) instance with calculated equivalent properties: - - - `id::String`: Cable component identification. - - `conductor_group::ConductorGroup{T}`: The conductor group containing all conductive parts. - - `conductor_props::Material{T}`: Effective properties of the equivalent coaxial conductor. - * `rho`: Resistivity \\[Ω·m\\]. - * `eps_r`: Relative permittivity \\[dimensionless\\]. - * `mu_r`: Relative permeability \\[dimensionless\\]. - * `T0`: Reference temperature \\[°C\\]. - * `alpha`: Temperature coefficient of resistivity \\[1/°C\\]. - - `insulator_group::InsulatorGroup{T}`: The insulator group containing all insulating parts. - - `insulator_props::Material{T}`: Effective properties of the equivalent coaxial insulator. - * `rho`: Resistivity \\[Ω·m\\]. - * `eps_r`: Relative permittivity \\[dimensionless\\]. - * `mu_r`: Relative permeability \\[dimensionless\\]. - * `T0`: Reference temperature \\[°C\\]. - * `alpha`: Temperature coefficient of resistivity \\[1/°C\\]. - - # Examples - - ```julia - conductor_group = ConductorGroup(...) - insulator_group = InsulatorGroup(...) - cable = $(FUNCTIONNAME)("component_id", conductor_group, insulator_group) # Create cable component with base parameters @ 50 Hz - ``` - - # See also - - - [`calc_equivalent_rho`](@ref) - - [`calc_equivalent_mu`](@ref) - - [`calc_equivalent_eps`](@ref) - - [`calc_sigma_lossfact`](@ref) - - [`calc_solenoid_correction`](@ref) - """ - function CableComponent{T}( - id::String, - conductor_group::ConductorGroup{T}, - insulator_group::InsulatorGroup{T}, - ) where {T <: REALSCALAR} - - # Geometry interface check (exact or approximately equal) - if !( - conductor_group.r_ex == insulator_group.r_in || - isapprox(conductor_group.r_ex, insulator_group.r_in) - ) - throw( - ArgumentError("Conductor outer radius must match insulator inner radius."), - ) - end - - # Radii - r1 = conductor_group.r_in - r2 = conductor_group.r_ex - r3 = insulator_group.r_ex - - # 2) Conductor equivalents - ρ_con = calc_equivalent_rho(conductor_group.resistance, r2, r1) - μ_con = calc_equivalent_mu(conductor_group.gmr, r2, r1) - α_con = conductor_group.alpha - θ_con = conductor_group.layers[1].temperature - conductor_props = Material{T}(ρ_con, T(0), μ_con, θ_con, α_con) - - # 3) Insulator equivalents (use already-aggregated C and G) - C_eq = insulator_group.shunt_capacitance - G_eq = insulator_group.shunt_conductance - ε_ins = calc_equivalent_eps(C_eq, r3, r2) - σ_ins = calc_sigma_lossfact(G_eq, r2, r3) - ρ_ins = inv(σ_ins) # safe if σ_ins ≠ 0 - μ_ins_corr = calc_solenoid_correction(conductor_group.num_turns, r2, r3) - θ_ins = insulator_group.layers[1].temperature - insulator_props = Material{T}(ρ_ins, ε_ins, μ_ins_corr, θ_ins, T(0)) - - return new{T}( - id, - conductor_group, - conductor_props, - insulator_group, - insulator_props, - ) - end -end - -""" -$(TYPEDSIGNATURES) - -Weakly-typed constructor that infers the scalar type `T` from the two groups, coerces them if necessary, and calls the strict kernel. - -# Arguments -- `id`: Cable component identification. -- `conductor_group`: The conductor group (any `ConductorGroup{S}`). -- `insulator_group`: The insulator group (any `InsulatorGroup{R}`). - -# Returns -- A `CableComponent{T}` where `T` is the resolved scalar type. -""" -function CableComponent( - id::String, - conductor_group::ConductorGroup, - insulator_group::InsulatorGroup, -) - # Resolve target T from the two groups (honors Measurements, etc.) - T = resolve_T(conductor_group, insulator_group) - - # Coerce groups to T (identity if already T) - cgT = coerce_to_T(conductor_group, T) - igT = coerce_to_T(insulator_group, T) - - return CableComponent{T}(id, cgT, igT) -end - - -""" -$(TYPEDSIGNATURES) - -Constructs the equivalent coaxial conductor as a `Tubular` directly from a -`CableComponent`, reusing the rigorously tested positional constructor. - -# Arguments - -- `component`: The `CableComponent` providing geometry and material. - -# Returns - -- `Tubular{T}` with radii from `component.conductor_group` and material from - `component.conductor_props` at the group temperature (fallback to `T0`). -""" -function Tubular(component::CableComponent{T}) where {T <: REALSCALAR} - cg = component.conductor_group - temp = component.conductor_props.T0 - return Tubular(cg.r_in, cg.r_ex, component.conductor_props, temp) -end - -""" -$(TYPEDSIGNATURES) - -Constructs the equivalent coaxial insulation as an `Insulator` directly from a -`CableComponent`, calling the strict positional constructor. - -# Arguments - -- `component`: The `CableComponent` providing geometry and material. - -# Returns - -- `Insulator{T}` with radii from `component.insulator_group` and material from - `component.insulator_props` at the group temperature (fallback to `T0`). -""" -function Insulator(component::CableComponent{T}) where {T <: REALSCALAR} - ig = component.insulator_group - temp = component.insulator_props.T0 - return Insulator(ig.r_in, ig.r_ex, component.insulator_props, temp) -end - -""" -$(TYPEDSIGNATURES) - -Build a `ConductorGroup` equivalent for a `CableComponent`, preserving -`num_turns` and `num_wires` from the original group. - -Constructs a single-layer `ConductorGroup{T}` from the computed equivalent -`Tubular(component)`, but carries over bookkeeping fields needed by downstream -corrections (e.g., solenoid correction using `num_turns`). -""" -function ConductorGroup(component::CableComponent{T}) where {T <: REALSCALAR} - orig = component.conductor_group - t = Tubular(component) - return ConductorGroup{T}( - t.r_in, - t.r_ex, - t.cross_section, - orig.num_wires, - orig.num_turns, - t.resistance, - t.material_props.alpha, - t.gmr, - AbstractConductorPart{T}[t], - ) -end - -""" -$(TYPEDSIGNATURES) - -Build an `InsulatorGroup` equivalent for a `CableComponent` while maintaining -geometric coupling to the equivalent conductor group. - -Stacks a single insulating layer of equivalent material and thickness over the -new conductor group created from the same component. -""" -InsulatorGroup(component::CableComponent{T}) where {T <: REALSCALAR} = - InsulatorGroup{T}(Insulator(component)) - - -include("cablecomponent/base.jl") diff --git a/src/datamodel/cablecomponent/base.jl b/src/datamodel/cablecomponent/base.jl deleted file mode 100644 index 5303190d0..000000000 --- a/src/datamodel/cablecomponent/base.jl +++ /dev/null @@ -1,101 +0,0 @@ - -Base.eltype(::CableComponent{T}) where {T} = T -Base.eltype(::Type{CableComponent{T}}) where {T} = T - -""" -$(TYPEDSIGNATURES) - -Defines the display representation of a [`CableComponent`](@ref) object for REPL or text output. - -# Arguments - -- `io`: Output stream. -- `::MIME"text/plain"`: MIME type for plain text output. -- `component`: The [`CableComponent`](@ref) object to be displayed. - -# Returns - -- Nothing. Modifies `io` by writing text representation of the object. -""" -function Base.show(io::IO, ::MIME"text/plain", component::CableComponent) - # Calculate total number of parts across both groups - total_parts = - length(component.conductor_group.layers) + length(component.insulator_group.layers) - - # Print header - println(io, "$(total_parts)-element CableComponent \"$(component.id)\":") - - # Display conductor group parts in a tree structure - print(io, "├─ $(length(component.conductor_group.layers))-element ConductorGroup: [") - _print_fields( - io, - component.conductor_group, - [:r_in, :r_ex, :cross_section, :resistance, :gmr], - ) - println(io, "]") - print(io, "│ ", "├─", " Effective properties: [") - _print_fields(io, component.conductor_props, [:rho, :eps_r, :mu_r, :alpha]) - println(io, "]") - - for (i, part) in enumerate(component.conductor_group.layers) - - prefix = i == length(component.conductor_group.layers) ? "└───" : "├───" - - # Print part information with proper indentation - print(io, "│ ", prefix, " $(nameof(typeof(part))): [") - - # Print each field with proper formatting - _print_fields( - io, - part, - [:r_in, :r_ex, :cross_section, :resistance, :gmr], - ) - - println(io, "]") - end - - # Display insulator group parts - if !isempty(component.insulator_group.layers) - print( - io, - "└─ $(length(component.insulator_group.layers))-element InsulatorGroup: [", - ) - _print_fields( - io, - component.insulator_group, - [ - :r_in, - :r_ex, - :cross_section, - :shunt_capacitance, - :shunt_conductance, - ], - ) - println(io, "]") - print(io, " ", "├─", " Effective properties: [") - _print_fields(io, component.insulator_props, [:rho, :eps_r, :mu_r, :alpha]) - println(io, "]") - for (i, part) in enumerate(component.insulator_group.layers) - # Determine prefix based on whether it's the last part - prefix = i == length(component.insulator_group.layers) ? "└───" : "├───" - - # Print part information with proper indentation - print(io, " ", prefix, " $(nameof(typeof(part))): [") - - # Print each field with proper formatting - _print_fields( - io, - part, - [ - :r_in, - :r_ex, - :cross_section, - :shunt_capacitance, - :shunt_conductance, - ], - ) - - println(io, "]") - end - end -end diff --git a/src/datamodel/cabledesign.jl b/src/datamodel/cabledesign.jl deleted file mode 100644 index 2b5e19228..000000000 --- a/src/datamodel/cabledesign.jl +++ /dev/null @@ -1,271 +0,0 @@ - -""" -$(TYPEDEF) - -Represents the design of a cable, including its unique identifier, nominal data, and components. - -$(TYPEDFIELDS) -""" -mutable struct CableDesign{T <: REALSCALAR} - "Unique identifier for the cable design." - cable_id::String - "Informative reference data." - nominal_data::Union{Nothing, NominalData{T}} - "Vector of cable components." - components::Vector{CableComponent{T}} - - @doc """ - $(TYPEDSIGNATURES) - - **Strict numeric kernel**: constructs a `CableDesign{T}` from one component - (typed) and optional nominal data (typed or `nothing`). Assumes all inputs - are already at scalar type `T`. - - # Arguments - - - `cable_id`: Unique identifier for the cable design. - - `component`: Initial [`CableComponent`](@ref) for the design. - - `nominal_data`: Reference data for the cable design. Default: `NominalData()`. - - # Returns - - - A [`CableDesign`](@ref) object with the specified properties. - - # Examples - - ```julia - conductor_group = ConductorGroup(central_conductor) - insulator_group = InsulatorGroup(main_insulator) - component = CableComponent(conductor_group, insulator_group) - design = $(FUNCTIONNAME)("example", component) - ``` - - # See also - - - [`CableComponent`](@ref) - - [`ConductorGroup`](@ref) - - [`InsulatorGroup`](@ref) - """ - @inline function CableDesign{T}( - cable_id::String, - component::CableComponent{T}; - nominal_data::Union{Nothing, NominalData{T}} = nothing, - ) where {T <: REALSCALAR} - new{T}(cable_id, nominal_data, CableComponent{T}[component]) - end - - @inline function CableDesign{T}( - cable_id::String, - components::Vector{CableComponent{T}}; - nominal_data::Union{Nothing, NominalData{T}} = nothing, - ) where {T <: REALSCALAR} - new{T}(cable_id, nominal_data, components) - end -end - -""" -$(TYPEDSIGNATURES) - -**Weakly-typed constructor** that infers the scalar type from the `component` (and nominal data if present), coerces values to that type, and calls the typed kernel. -""" -function CableDesign( - cable_id::String, - component::CableComponent; - nominal_data::NominalData = NominalData(), -) - # Resolve T from component and nominal_data (ignoring `nothing` fields in the latter) - T = resolve_T(component, nominal_data) - - compT = coerce_to_T(component, T) - ndT = coerce_to_T(nominal_data, T) # identity if already T - - return CableDesign{T}(cable_id, compT; nominal_data = ndT) -end - -""" -$(TYPEDSIGNATURES) - -Constructs a [`CableDesign`](@ref) instance **from conductor and insulator groups**. -Convenience wrapper that builds the component with reduced boilerplate. -""" -function CableDesign( - cable_id::String, - conductor_group::ConductorGroup, - insulator_group::InsulatorGroup; - component_id::String = "component1", - nominal_data::NominalData = NominalData(), -) - component = CableComponent(component_id, conductor_group, insulator_group) - return CableDesign(cable_id, component; nominal_data) -end - -function add!(design::CableDesign{T}, component::CableComponent) where {T} - Tnew = resolve_T(design, component) - - if Tnew === T - compT = coerce_to_T(component, T) - if (idx = findfirst(c -> c.id == compT.id, design.components)) !== nothing - @warn "Component with ID '$(compT.id)' already exists and will be overwritten." - design.components[idx] = compT - else - push!(design.components, compT) - end - return design - else - @warn """ - Adding a `$Tnew` component to a `CableDesign{$T}` returns a **promoted** design. - Capture the result: design = add!(design, component) - """ - # promote whole design, then insert coerced component - promoted = coerce_to_T(design, Tnew) - compT = coerce_to_T(component, Tnew) - if (idx = findfirst(c -> c.id == compT.id, promoted.components)) !== nothing - promoted.components[idx] = compT - else - push!(promoted.components, compT) - end - return promoted - end -end - -# --- add!(design, by groups): wraps the above --- -function add!( - design::CableDesign{T}, - component_id::String, - conductor_group::ConductorGroup, - insulator_group::InsulatorGroup, -) where {T} - comp = CableComponent(component_id, conductor_group, insulator_group) - add!(design, comp) # may return the same or a promoted design -end - -""" -$(TYPEDSIGNATURES) - -Builds a simplified [`CableDesign`](@ref) by replacing each component with a -homogeneous equivalent and leverages shorthand constructors: - -- `ConductorGroup(component::CableComponent{T}) = ConductorGroup(Tubular(component))` -- `InsulatorGroup(component::CableComponent{T}) = InsulatorGroup(Insulator(component))` - -The geometry is preserved from the original component, while materials are -derived from the component's effective conductor and insulator properties. -""" -function equivalent( - original_design::CableDesign; - new_id::String = "", -)::CableDesign - - if isempty(original_design.components) - throw(ArgumentError("CableDesign must contain at least one component.")) - end - - # Determine the ID for the new equivalent cable. - equivalent_id = isempty(new_id) ? original_design.cable_id * "_equivalent" : new_id - - equivalent_design = nothing - - for (i, original_component) in enumerate(original_design.components) - - new_cond_group = ConductorGroup(original_component) - new_ins_group = InsulatorGroup(original_component) - - if i == 1 - new_component = - CableComponent(original_component.id, new_cond_group, new_ins_group) - equivalent_design = CableDesign( - equivalent_id, - new_component, - nominal_data = original_design.nominal_data, - ) - else - add!(equivalent_design, original_component.id, new_cond_group, new_ins_group) - end - end - - return equivalent_design -end - -""" -nonsensify(original_design::CableDesign; new_id::String="")::CableDesign - -Recreates a cable design by bulldozing reality into a "simplified" shape -with only the so-called "main" material properties. - -Translation: if you wanted physics, you came to the wrong neighborhood. - -For each component, this abomination does: -- `ConductorGroup(Tubular(...))` with radii stolen from the first and last - conductor layers, and material blindly copied from the first conductor layer. - Because high-fidelity is for losers. - -- `InsulatorGroup(Insulator(...))` spanning from the new conductor outer radius - to the original insulator group's outer radius; material is taken from the - first `Insulator` layer available (or whatever warm body it can find). - -⚠ WARNING: This is *deliberately* nonsensical. It laughs in the face of proper -equivalent property corrections and just slaps the "main" props on like duct tape. -Use only when you don’t give a damn about accuracy and just want something -that looks cable-ish, e.g., never. -""" -function nonsensify( - original_design::CableDesign; - new_id::String = "", -)::CableDesign - - if isempty(original_design.components) - throw(ArgumentError("CableDesign must contain at least one component.")) - end - - # Determine the ID for the new cable. - target_id = isempty(new_id) ? original_design.cable_id * "_nonsense" : new_id - - rebuilt_design = nothing - - for (i, original_component) in enumerate(original_design.components) - # Source data from original component - cg = original_component.conductor_group - ig = original_component.insulator_group - - # Radii from conductor group layers - rin = cg.layers[1].r_in - rex = cg.layers[end].r_ex - - # "Main" material props and temperature for conductor from first conductor layer - mat_con = cg.layers[1].material_props - temp_con = cg.layers[1].temperature - - # Build simplified parts and groups - tubular = Tubular(rin, rex, mat_con, temp_con) - new_cond_group = ConductorGroup(tubular) - - ins_rin = new_cond_group.r_ex # ensure interface matches - ins_rex = ig.r_ex # keep original outer boundary - - # Pick first Insulator layer in insulator group (skip Semicon); fallback to first layer - idx_ins = findfirst(x -> x isa Insulator, ig.layers) - idx_ins = isnothing(idx_ins) ? 1 : idx_ins - mat_ins = ig.layers[idx_ins].material_props - temp_ins = ig.layers[idx_ins].temperature - - ins = Insulator(ins_rin, ins_rex, mat_ins, temp_ins) - new_ins_group = InsulatorGroup(ins) - - if i == 1 - new_component = - CableComponent(original_component.id, new_cond_group, new_ins_group) - rebuilt_design = CableDesign( - target_id, - new_component, - nominal_data = original_design.nominal_data, - ) - else - add!(rebuilt_design, original_component.id, new_cond_group, new_ins_group) - end - end - - return rebuilt_design -end - -include("cabledesign/base.jl") -include("cabledesign/dataframe.jl") diff --git a/src/datamodel/cabledesign/base.jl b/src/datamodel/cabledesign/base.jl deleted file mode 100644 index 173078d0e..000000000 --- a/src/datamodel/cabledesign/base.jl +++ /dev/null @@ -1,116 +0,0 @@ - - -Base.eltype(::CableDesign{T}) where {T} = T -Base.eltype(::Type{CableDesign{T}}) where {T} = T - - -""" -$(TYPEDSIGNATURES) - -Defines the display representation of a [`CableDesign`](@ref) object for REPL or text output. - -# Arguments - -- `io`: Output stream. -- `::MIME"text/plain"`: MIME type for plain text output. -- `design`: The [`CableDesign`](@ref) object to be displayed. - -# Returns - -- Nothing. Modifies `io` by writing text representation of the object. -""" -function Base.show(io::IO, ::MIME"text/plain", design::CableDesign) - # Print header with cable ID and count of components - print(io, "$(length(design.components))-element CableDesign \"$(design.cable_id)\"") - - # Add nominal values if available - nominal_values = [] - if design.nominal_data.resistance !== nothing - push!( - nominal_values, - "resistance=$(round(design.nominal_data.resistance, sigdigits=4))", - ) - end - if design.nominal_data.inductance !== nothing - push!( - nominal_values, - "inductance=$(round(design.nominal_data.inductance, sigdigits=4))", - ) - end - if design.nominal_data.capacitance !== nothing - push!( - nominal_values, - "capacitance=$(round(design.nominal_data.capacitance, sigdigits=4))", - ) - end - - if !isempty(nominal_values) - print(io, ", with nominal values: [", join(nominal_values, ", "), "]") - end - println(io) - - # For each component, display its properties in a tree structure - for (i, component) in enumerate(design.components) - # Determine if this is the last component - is_last_component = i == length(design.components) - - # Determine component prefix and continuation line - component_prefix = is_last_component ? "└─" : "├─" - continuation_line = is_last_component ? " " : "│ " - - # Print component name and header - println(io, component_prefix, " Component \"", component.id, "\":") - - # Display conductor group with combined properties - print(io, continuation_line, "├─ ConductorGroup: [") - - # Combine properties from conductor_group and conductor_props - conductor_props = [ - "r_in" => component.conductor_group.r_in, - "r_ex" => component.conductor_group.r_ex, - "rho" => component.conductor_props.rho, - "eps_r" => component.conductor_props.eps_r, - "mu_r" => component.conductor_props.mu_r, - "alpha" => component.conductor_props.alpha, - ] - - # Display combined conductor properties - displayed_fields = 0 - for (field, value) in conductor_props - if !(value isa Number && isnan(value)) - if displayed_fields > 0 - print(io, ", ") - end - print(io, "$field=$(round(value, sigdigits=4))") - displayed_fields += 1 - end - end - println(io, "]") - - # Display insulator group with combined properties - print(io, continuation_line, "└─ InsulatorGroup: [") - - # Combine properties from insulator_group and insulator_props - insulator_props = [ - "r_in" => component.insulator_group.r_in, - "r_ex" => component.insulator_group.r_ex, - "rho" => component.insulator_props.rho, - "eps_r" => component.insulator_props.eps_r, - "mu_r" => component.insulator_props.mu_r, - "alpha" => component.insulator_props.alpha, - ] - - # Display combined insulator properties - displayed_fields = 0 - for (field, value) in insulator_props - if !(value isa Number && isnan(value)) - if displayed_fields > 0 - print(io, ", ") - end - print(io, "$field=$(round(value, sigdigits=4))") - displayed_fields += 1 - end - end - println(io, "]") - end -end diff --git a/src/datamodel/cabledesign/dataframe.jl b/src/datamodel/cabledesign/dataframe.jl deleted file mode 100644 index e4ad42ad0..000000000 --- a/src/datamodel/cabledesign/dataframe.jl +++ /dev/null @@ -1,336 +0,0 @@ -import DataFrames: DataFrame - -""" -$(TYPEDSIGNATURES) - -Extracts and displays data from a [`CableDesign`](@ref). - -# Arguments - -- `design`: A [`CableDesign`](@ref) object to extract data from. -- `format`: Symbol indicating the level of detail: - - `:baseparams`: Basic RLC parameters with nominal value comparison (default). - - `:components`: Component-level equivalent properties. - - `:detailed`: Individual cable part properties with layer-by-layer breakdown. -- `S`: Separation distance between cables \\[m\\] (only used for `:baseparams` format). Default: outermost cable diameter. -- `rho_e`: Resistivity of the earth \\[Ω·m\\] (only used for `:baseparams` format). Default: 100. - -# Returns - -- A `DataFrame` containing the requested cable data in the specified format. - -# Examples - -```julia -# Get basic RLC parameters -data = DataFrame(design) # Default is :baseparams format - -# Get component-level data -comp_data = DataFrame(design, :components) - -# Get detailed part-by-part breakdown -detailed_data = DataFrame(design, :detailed) - -# Specify earth parameters for core calculations -core_data = DataFrame(design, :baseparams, S=0.5, rho_e=150) -``` - -# See also - -- [`CableDesign`](@ref) -- [`calc_tubular_resistance`](@ref) -- [`calc_inductance_trifoil`](@ref) -- [`calc_shunt_capacitance`](@ref) -""" -function DataFrame( - design::CableDesign, - format::Symbol = :baseparams; - S::Union{Nothing, Number} = nothing, - rho_e::Number = 100.0, -)::DataFrame - - - - if format == :baseparams - # Core parameters calculation - # Get components from the vector - if length(design.components) < 2 - throw( - ArgumentError( - "At least two components are required for :baseparams format.", - ), - ) - end - - cable_core = design.components[1] - cable_shield = design.components[2] - cable_outer = design.components[end] - - # Determine separation distance if not provided - S = - S === nothing ? - ( - # Check if we need to use insulator or conductor radius - isnan(cable_outer.insulator_group.r_ex) ? - 2 * cable_outer.conductor_group.r_ex : - 2 * cable_outer.insulator_group.r_ex - ) : S - - # Compute R, L, and C using given formulas - mapped to new data structure - # Cable core resistance - R = - calc_tubular_resistance( - cable_core.conductor_group.r_in, - cable_core.conductor_group.r_ex, - cable_core.conductor_props.rho, - 0.0, 20.0, 20.0, - ) * 1e3 - - # Inductance calculation - L = - calc_inductance_trifoil( - cable_core.conductor_group.r_in, - cable_core.conductor_group.r_ex, - cable_core.conductor_props.rho, - cable_core.conductor_props.mu_r, - cable_shield.conductor_group.r_in, - cable_shield.conductor_group.r_ex, - cable_shield.conductor_props.rho, - cable_shield.conductor_props.mu_r, - S, - rho_e = rho_e, - ) * 1e6 - - # Capacitance calculation - C = - calc_shunt_capacitance( - cable_core.conductor_group.r_ex, - cable_core.insulator_group.r_ex, - cable_core.insulator_props.eps_r, - ) * 1e6 * 1e3 - - # Prepare nominal values from CableDesign - nominals = [ - design.nominal_data.resistance, - design.nominal_data.inductance, - design.nominal_data.capacitance, - ] - - # Calculate differences - diffs = map(zip([R, L, C], nominals)) do (computed, nominal) - if isnothing(nominal) - return missing - else - return to_nominal(abs(nominal - computed) / nominal * 100) - end - end - - # Compute the comparison DataFrame - data = DataFrame( - parameter = ["R [Ω/km]", "L [mH/km]", "C [μF/km]"], - computed = [R, L, C], - nominal = to_nominal.(nominals), - ) - - # Add percent_diff column only for rows with non-nothing nominal values - data[!, "percent_diff"] = diffs - - # Handle measurement bounds if present - has_error_bounds = !(isnan(to_lower(R)) || isnan(to_upper(R))) - if has_error_bounds - data[!, "lower"] = [to_lower(R), to_lower(L), to_lower(C)] - data[!, "upper"] = [to_upper(R), to_upper(L), to_upper(C)] - - # Add compliance column only for rows with non-nothing nominal values - data[!, "in_range?"] = - map(zip(data.nominal, data.lower, data.upper)) do (nom, low, up) - isnothing(nom) ? missing : (nom >= low && nom <= up) - end - end - - elseif format == :components - # Component-level properties - properties = [ - :radius_in_con, - :radius_ext_con, - :rho_con, - :alpha_con, - :mu_con, - :radius_ext_ins, - :eps_ins, - :mu_ins, - :loss_factor_ins, - ] - - # Initialize the DataFrame - data = DataFrame(property = properties) - - # Process each component - now using vector - for component in design.components - # Use component ID as column name - col = component.id - - # For each component, we need to map new structure to old column names - # Calculate loss factor from resistivity - ω = 2 * π * f₀ # Using default frequency - C_eq = component.insulator_group.shunt_capacitance - G_eq = component.insulator_group.shunt_conductance - loss_factor = G_eq / (ω * C_eq) - - # Collect values for each property - mapping from new structure to old property names - new_col = [ - component.conductor_group.r_in, # radius_in_con - component.conductor_group.r_ex, # radius_ext_con - component.conductor_props.rho, # rho_con - component.conductor_props.alpha, # alpha_con - component.conductor_props.mu_r, # mu_con - component.insulator_group.r_ex, # radius_ext_ins - component.insulator_props.eps_r, # eps_ins - component.insulator_props.mu_r, # mu_ins - loss_factor, # loss_factor_ins - ] - - # Add to DataFrame - data[!, col] = new_col - end - - elseif format == :detailed - # Detailed part-by-part breakdown - properties = [ - "type", - "r_in", - "r_ex", - "diam_in", - "diam_ext", - "thickness", - "cross_section", - "num_wires", - "resistance", - "alpha", - "gmr", - "gmr/radius", - "shunt_capacitance", - "shunt_conductance", - ] - - # Initialize the DataFrame - data = DataFrame(property = properties) - - # Process each component - for component in design.components - # Handle conductor group layers - for (i, part) in enumerate(component.conductor_group.layers) - # Column name with component ID and layer number - col = lowercase(component.id) * ", cond. layer " * string(i) - - # Collect values for each property - new_col = _extract_part_properties(part, properties) - - # Add to DataFrame - data[!, col] = new_col - end - - # Handle insulator group layers - for (i, part) in enumerate(component.insulator_group.layers) - # Column name with component ID and layer number - col = lowercase(component.id) * ", ins. layer " * string(i) - - # Collect values for each property - new_col = _extract_part_properties(part, properties) - - # Add to DataFrame - data[!, col] = new_col - end - end - else - Base.error( - "Unsupported format: $format. Use :baseparams, :components, or :detailed", - ) - end - - return data -end - -""" -$(TYPEDSIGNATURES) - -Helper function to extract properties from a part for detailed format. - -# Arguments - -- `part`: An instance of [`AbstractCablePart`](@ref) from which to extract properties. -- `properties`: A vector of symbols indicating which properties to extract (not used in the current implementation). - -# Returns - -- A vector containing the extracted properties in the following order: - - `type`: The lowercase string representation of the part's type. - - `r_in`: The inner radius of the part, if it exists, otherwise `missing`. - - `r_ex`: The outer radius of the part, if it exists, otherwise `missing`. - - `diameter_in`: The inner diameter of the part (2 * r_in), if `r_in` exists, otherwise `missing`. - - `diameter_ext`: The outer diameter of the part (2 * r_ex), if `r_ex` exists, otherwise `missing`. - - `thickness`: The difference between `r_ex` and `r_in`, if both exist, otherwise `missing`. - - `cross_section`: The cross-sectional area of the part, if it exists, otherwise `missing`. - - `num_wires`: The number of wires in the part, if it exists, otherwise `missing`. - - `resistance`: The resistance of the part, if it exists, otherwise `missing`. - - `alpha`: The temperature coefficient of resistivity of the part or its material, if it exists, otherwise `missing`. - - `gmr`: The geometric mean radius of the part, if it exists, otherwise `missing`. - - `gmr_ratio`: The ratio of `gmr` to `r_ex`, if both exist, otherwise `missing`. - - `shunt_capacitance`: The shunt capacitance of the part, if it exists, otherwise `missing`. - - `shunt_conductance`: The shunt conductance of the part, if it exists, otherwise `missing`. - -# Notes - -This function is used to create a standardized format for displaying detailed information about cable parts. - -# Examples - -```julia -part = Conductor(...) -properties = [:r_in, :r_ex, :resistance] # Example of properties to extract -extracted_properties = _extract_part_properties(part, properties) -println(extracted_properties) -``` -""" -function _extract_part_properties(part, properties) - return [ - lowercase(string(typeof(part))), # type - hasfield(typeof(part), :r_in) ? - getproperty(part, :r_in) : missing, - hasfield(typeof(part), :r_ex) ? - getproperty(part, :r_ex) : missing, - hasfield(typeof(part), :r_in) ? - 2 * getproperty(part, :r_in) : missing, - hasfield(typeof(part), :r_ex) ? - 2 * getproperty(part, :r_ex) : missing, - hasfield(typeof(part), :r_ex) && - hasfield(typeof(part), :r_in) ? - (getproperty(part, :r_ex) - getproperty(part, :r_in)) : - missing, - hasfield(typeof(part), :cross_section) ? - getproperty(part, :cross_section) : missing, - hasfield(typeof(part), :num_wires) ? - getproperty(part, :num_wires) : missing, - hasfield(typeof(part), :resistance) ? - getproperty(part, :resistance) : missing, - hasfield(typeof(part), :alpha) || - ( - hasfield(typeof(part), :material_props) && - hasfield(typeof(getproperty(part, :material_props)), :alpha) - ) ? - ( - hasfield(typeof(part), :alpha) ? - getproperty(part, :alpha) : - getproperty(getproperty(part, :material_props), :alpha) - ) : missing, - hasfield(typeof(part), :gmr) ? - getproperty(part, :gmr) : missing, - hasfield(typeof(part), :gmr) && - hasfield(typeof(part), :r_ex) ? - (getproperty(part, :gmr) / getproperty(part, :r_ex)) : missing, - hasfield(typeof(part), :shunt_capacitance) ? - getproperty(part, :shunt_capacitance) : missing, - hasfield(typeof(part), :shunt_conductance) ? - getproperty(part, :shunt_conductance) : missing, - ] -end diff --git a/src/datamodel/cableslibrary.jl b/src/datamodel/cableslibrary.jl deleted file mode 100644 index 82bd566e3..000000000 --- a/src/datamodel/cableslibrary.jl +++ /dev/null @@ -1,83 +0,0 @@ -""" -$(TYPEDEF) - -Represents a library of cable designs stored as a dictionary. - -$(TYPEDFIELDS) -""" -mutable struct CablesLibrary - "Dictionary mapping cable IDs to the respective CableDesign objects." - data::Dict{String, CableDesign} - - @doc """ - $(TYPEDSIGNATURES) - - Constructs an empty [`CablesLibrary`](@ref) instance. - - # Arguments - - - None. - - # Returns - - - A [`CablesLibrary`](@ref) object with an empty dictionary of cable designs. - - # Examples - - ```julia - # Create a new, empty library - library = $(FUNCTIONNAME)() - ``` - - # See also - - - [`CableDesign`](@ref) - - [`add!`](@ref) - - [`delete!`](@ref) - - [`LineCableModels.ImportExport.save`](@ref) - - [`DataFrame`](@ref) - """ - function CablesLibrary()::CablesLibrary - library = new(Dict{String, CableDesign}()) - @info "Initializing empty cables database..." - return library - end -end - - -""" -Stores a cable design in a [`CablesLibrary`](@ref) object. - -# Arguments - -- `library`: An instance of [`CablesLibrary`](@ref) to which the cable design will be added. -- `design`: A [`CableDesign`](@ref) object representing the cable design to be added. This object must have a `cable_id` field to uniquely identify it. - -# Returns - -- None. Modifies the `data` field of the [`CablesLibrary`](@ref) object in-place by adding the new cable design. - -# Examples -```julia -library = CablesLibrary() -design = CableDesign("example", ...) # Initialize CableDesign with required fields -add!(library, design) -println(library) # Prints the updated dictionary containing the new cable design -``` -# See also - -- [`CablesLibrary`](@ref) -- [`CableDesign`](@ref) -- [`delete!`](@ref) -""" -function add!(library::CablesLibrary, design::CableDesign) - library.data[design.cable_id] = design - @info "Cable design with ID `$(design.cable_id)` added to the library." - library -end - -include("cableslibrary/base.jl") -include("cableslibrary/dataframe.jl") -include("cableslibrary/vdeparse.jl") - - diff --git a/src/datamodel/cableslibrary/base.jl b/src/datamodel/cableslibrary/base.jl index 02341d1d5..0d3b3c907 100644 --- a/src/datamodel/cableslibrary/base.jl +++ b/src/datamodel/cableslibrary/base.jl @@ -1,97 +1,90 @@ # Implement the AbstractDict interface Base.length(lib::CablesLibrary) = length(lib.data) -Base.setindex!(lib::CablesLibrary, value::CableDesign, key::String) = (lib.data[key] = value) +function Base.setindex!(lib::CablesLibrary, design::CableDesign, key) + cable_id = convert(String, key) + cable_id == design.cable_id || throw(ArgumentError( + "cable key '$cable_id' differs from cable_id '$(design.cable_id)'", + )) + validate(design) + record = DatasheetInfo(design.nominal_data) + lib.data[cable_id] = design + lib.datasheets[cable_id] = record + return lib +end Base.iterate(lib::CablesLibrary, state...) = iterate(lib.data, state...) Base.keys(lib::CablesLibrary) = keys(lib.data) Base.values(lib::CablesLibrary) = values(lib.data) -Base.haskey(lib::CablesLibrary, key::String) = haskey(lib.data, key) -Base.getindex(lib::CablesLibrary, key::String) = getindex(lib.data, key) +Base.haskey(lib::CablesLibrary, key) = haskey(lib.data, key) +Base.getindex(lib::CablesLibrary, key) = getindex(lib.data, key) """ $(TYPEDSIGNATURES) -Retrieves a cable design from a [`CablesLibrary`](@ref) object by its ID. +Return the cable design stored under `cable_id`, or `default` when absent. # Arguments -- `library`: An instance of [`CablesLibrary`](@ref) from which the cable design will be retrieved. -- `cable_id`: The ID of the cable design to retrieve. +- `library`: cable-design library. +- `cable_id`: stored cable identifier. +- `default`: value returned when `cable_id` is absent. # Returns -- A [`CableDesign`](@ref) object corresponding to the given `cable_id` if found, otherwise `nothing`. - -# Examples - -```julia -library = CablesLibrary() -design = CableDesign("example", ...) # Initialize a CableDesign -add!(library, design) - -# Retrieve the cable design -retrieved_design = $(FUNCTIONNAME)(library, "cable1") -println(retrieved_design.id) # Prints "example" - -# Attempt to retrieve a non-existent design -missing_design = $(FUNCTIONNAME)(library, "nonexistent_id") -println(missing_design === nothing) # Prints true -``` +- The stored [`CableDesign`](@ref), or `default`. -# See also - -- [`CablesLibrary`](@ref) -- [`CableDesign`](@ref) -- [`add!`](@ref) -- [`delete!`](@ref) """ -function Base.get(library::CablesLibrary, cable_id::String, default=nothing) - if haskey(library, cable_id) - @info "Cable design with ID `$cable_id` loaded from the library." - return library[cable_id] - else - @warn "Cable design with ID `$cable_id` not found in the library; returning default." - return default - end +function Base.get(library::CablesLibrary, cable_id, default) + return get(library.data, cable_id, default) +end + +function Base.get( + default::Union{Function, Type}, library::CablesLibrary, cable_id +) + return get(default, library.data, cable_id) end """ $(TYPEDSIGNATURES) -Removes a cable design from a [`CablesLibrary`](@ref) object by its ID. +Remove the cable design stored under `cable_id`. # Arguments -- `library`: An instance of [`CablesLibrary`](@ref) from which the cable design will be removed. -- `cable_id`: The ID of the cable design to remove. +- `library`: cable-design library. +- `cable_id`: stored cable identifier. # Returns -- Nothing. Modifies the `data` field of the [`CablesLibrary`](@ref) object in-place by removing the specified cable design if it exists. - -# Examples +- The modified `library`. -```julia -library = CablesLibrary() -design = CableDesign("example", ...) # Initialize a CableDesign -add!(library, design) +""" +function Base.delete!(library::CablesLibrary, cable_id) + delete!(library.data, cable_id) + delete!(library.datasheets, cable_id) + return library +end -# Remove the cable design -$(FUNCTIONNAME)(library, "example") -haskey(library, "example") # Returns false -``` +""" +Return an empty cable library. +""" +Base.empty(::CablesLibrary) = CablesLibrary() -# See also +""" +Remove every cable design and catalog record and return `library`. +""" +function Base.empty!(library::CablesLibrary) + empty!(library.data) + empty!(library.datasheets) + return library +end -- [`CablesLibrary`](@ref) -- [`add!`](@ref) """ -function Base.delete!(library::CablesLibrary, cable_id::String) - if haskey(library, cable_id) - delete!(library.data, cable_id) - @info "Cable design with ID `$cable_id` removed from the library." - else - @error "Cable design with ID `$cable_id` not found in the library; cannot delete." - throw(KeyError(cable_id)) - end -end \ No newline at end of file +Return a shallow cable-library copy with independent dictionary storage. +""" +function Base.copy(library::CablesLibrary) + copied = CablesLibrary() + copied.data = copy(library.data) + copied.datasheets = copy(library.datasheets) + return copied +end diff --git a/src/datamodel/cableslibrary/cableslibrary.jl b/src/datamodel/cableslibrary/cableslibrary.jl new file mode 100644 index 000000000..9f539777a --- /dev/null +++ b/src/datamodel/cableslibrary/cableslibrary.jl @@ -0,0 +1,87 @@ +""" +$(TYPEDEF) + +Store cable designs by `cable_id` as an `AbstractDict{String, CableDesign}`. + +Ordinary indexed assignment inserts or replaces a design and resets its +catalog record from `design.nominal_data`. Use [`add!`](@ref) to reject an +existing identifier or to supply an explicit catalog record. + +$(TYPEDFIELDS) +""" +mutable struct CablesLibrary <: AbstractDict{String, CableDesign} + "Cable designs indexed by `cable_id`." + data::Dict{String, CableDesign} + "Catalog records indexed by `cable_id`." + datasheets::Dict{String, DatasheetInfo} + + @doc """ + $(TYPEDSIGNATURES) + + Construct an empty cable-design library. + + # Returns + + - An empty [`CablesLibrary`](@ref). + + # Examples + + ```jldoctest + library = $(FUNCTIONNAME)() + isempty(library) + # output + true + ``` + + """ + function CablesLibrary()::CablesLibrary + return new(Dict{String, CableDesign}(), Dict{String, DatasheetInfo}()) + end +end + +""" +$(TYPEDSIGNATURES) + +Add a cable design under its `cable_id`. + +# Arguments + +- `library`: destination library. +- `design`: validated cable design. + +# Keywords + +- `datasheet=design.nominal_data`: datasheet information stored beside the + design. A named tuple is normalized to [`DatasheetInfo`](@ref). + +# Returns + +- The modified `library`. + +# Errors + +- Throws `ArgumentError` when `library` already contains `design.cable_id`. +""" +function add!( + library::CablesLibrary, + design::CableDesign; + datasheet::Union{DatasheetInfo, NamedTuple} = design.nominal_data +) + haskey(library, design.cable_id) && throw(ArgumentError( + "cable design '$(design.cable_id)' already exists", + )) + validate(design) + record = datasheet isa DatasheetInfo ? datasheet : DatasheetInfo(datasheet) + library.data[design.cable_id] = design + library.datasheets[design.cable_id] = record + return library +end + +""" +Return the catalog record associated with `cable_id`. +""" +datasheet(library::CablesLibrary, cable_id::AbstractString) = + library.datasheets[String(cable_id)] + +include("base.jl") +include("vdeparse.jl") diff --git a/src/datamodel/cableslibrary/dataframe.jl b/src/datamodel/cableslibrary/dataframe.jl deleted file mode 100644 index 8f755020a..000000000 --- a/src/datamodel/cableslibrary/dataframe.jl +++ /dev/null @@ -1,52 +0,0 @@ -import DataFrames: DataFrame - -""" -$(TYPEDSIGNATURES) - -Lists the cable designs in a [`CablesLibrary`](@ref) object as a `DataFrame`. - -# Arguments - -- `library`: An instance of [`CablesLibrary`](@ref) whose cable designs are to be displayed. - -# Returns - -- A `DataFrame` object with the following columns: - - `cable_id`: The unique identifier for each cable design. - - `nominal_data`: A string representation of the nominal data for each cable design. - - `components`: A comma-separated string listing the components of each cable design. - -# Examples - -```julia -library = CablesLibrary() -design1 = CableDesign("example1", nominal_data=NominalData(...), components=Dict("A"=>...)) -design2 = CableDesign("example2", nominal_data=NominalData(...), components=Dict("C"=>...)) -add!(library, design1) -add!(library, design2) - -# Display the library as a DataFrame -df = $(FUNCTIONNAME)(library) -first(df, 5) # Show the first 5 rows of the DataFrame -``` - -# See also - -- [`CablesLibrary`](@ref) -- [`CableDesign`](@ref) -- [`add!`](@ref) -""" -function DataFrame(library::CablesLibrary)::DataFrame - ids = keys(library) - nominal_data = [string(design.nominal_data) for design in values(library)] - components = [ - join([comp.id for comp in design.components], ", ") for - design in values(library) - ] - df = DataFrame( - cable_id=collect(ids), - nominal_data=nominal_data, - components=components, - ) - return (df) -end \ No newline at end of file diff --git a/src/datamodel/cableslibrary/datasheetinfo.jl b/src/datamodel/cableslibrary/datasheetinfo.jl new file mode 100644 index 000000000..30ecc4a01 --- /dev/null +++ b/src/datamodel/cableslibrary/datasheetinfo.jl @@ -0,0 +1,51 @@ +""" +$(TYPEDEF) + +Store the values reported by one cable datasheet without imposing a fixed +catalog schema. + +Field names and values are retained in the supplied `NamedTuple`. Property +access delegates to that record, so `info.resistance` returns the value stored +under `:resistance`. + +$(TYPEDFIELDS) +""" +struct DatasheetInfo{D <: NamedTuple} + "Named datasheet values in their stated engineering units." + data::D + + function DatasheetInfo(data::D) where {D <: NamedTuple} + :data in keys(data) && throw(ArgumentError( + "datasheet field :data is reserved" + )) + return new{D}(data) + end +end + +""" +$(TYPEDSIGNATURES) + +Construct cable datasheet information from named values. + +# Keywords + +- `values...`: datasheet fields. Names and value types are retained exactly. + +# Returns + +- A [`DatasheetInfo`](@ref) record. +""" +DatasheetInfo(; values...) = DatasheetInfo((; values...)) + +Base.propertynames(info::DatasheetInfo, private::Bool = false) = + private ? (:data, keys(getfield(info, :data))...) : keys(getfield(info, :data)) + +function Base.getproperty(info::DatasheetInfo, name::Symbol) + name === :data && return getfield(info, :data) + return getproperty(getfield(info, :data), name) +end + +Base.keys(info::DatasheetInfo) = keys(getfield(info, :data)) +Base.pairs(info::DatasheetInfo) = pairs(getfield(info, :data)) +Base.length(info::DatasheetInfo) = length(getfield(info, :data)) +Base.getindex(info::DatasheetInfo, name::Symbol) = getindex(getfield(info, :data), name) diff --git a/src/datamodel/cableslibrary/vdeparse.jl b/src/datamodel/cableslibrary/vdeparse.jl index 43e7446bf..fbdf03d5d 100644 --- a/src/datamodel/cableslibrary/vdeparse.jl +++ b/src/datamodel/cableslibrary/vdeparse.jl @@ -2,238 +2,238 @@ # ─────────────────────────── Mappings ─────────────────────────── const MAP = Dict{Symbol, Dict{String, String}}( - :designation => Dict( - "N" => "DIN VDE standard", - "(N)" => "similar to DIN VDE standard", - ), - - # Conductor material (omitted ⇒ copper) - :conductor => Dict( - "A" => "aluminium conductor", - "-" => "copper conductor", - ), - - # Insulation (omitted ⇒ paper) - :insulation => Dict( - "2X" => "cross-linked PE (XLPE)", - "Y" => "PVC", - "H" => "LSOH compound", - "-" => "impregnated paper", - ), - - # Screen / concentric conductor - :metallic_screen => Dict( - "CW" => "concentric conductor of copper in waveconal formation", - "CE" => "concentric conductor of copper over each individual core", - "SE" => "screen of copper wires over each individual core", - "C" => "concentric conductor of copper", - "S" => "screen of copper wires", - "H" => "conductive layers", - ), - - # Water blocking right after screen, inside parentheses - :waterblocking => Dict( - "FL" => "longitudinally and radially water-proof protection", - "F" => "longitudinally water-proof protection", - "L" => "radially water-proof protection", - ), - - # Inner sheath (e.g., …XSH… : H before outer sheath) - :inner_sheath => Dict( - "H" => "LSOH compound inner sheath", - ), - - # Armouring - :armouring => Dict( - "B" => "steel tape armouring", - "F" => "armour of galvanised flat steel wires", - "G" => "counter helix of galvanised steel tape", - "R" => "armour of galvanised round steel wires", - ), - - # Sheath - :sheath => Dict( - "KL" => "aluminium sheath", - "K" => "lead sheath", - ), - - # Outer sheath - :outer_sheath => Dict( - "A" => "outer sheath made of fibrous material", - "2Y" => "PE outer sheath", - "Y" => "PVC outer sheath", - ), - - # Grounding / protective conductor suffix - :grounding => Dict( - "I" => "with grounding (protective) conductor", - "J" => "with grounding (protective) conductor", - "O" => "without grounding (protective) conductor", - ), + :designation => Dict( + "N" => "DIN VDE standard", + "(N)" => "similar to DIN VDE standard" + ), + + # Conductor material (omitted ⇒ copper) + :conductor => Dict( + "A" => "aluminium conductor", + "-" => "copper conductor" + ), + + # Insulation (omitted ⇒ paper) + :insulation => Dict( + "2X" => "cross-linked PE (XLPE)", + "Y" => "PVC", + "H" => "LSOH compound", + "-" => "impregnated paper" + ), + + # Screen / concentric conductor + :metallic_screen => Dict( + "CW" => "concentric conductor of copper in waveconal formation", + "CE" => "concentric conductor of copper over each individual core", + "SE" => "screen of copper wires over each individual core", + "C" => "concentric conductor of copper", + "S" => "screen of copper wires", + "H" => "conductive layers" + ), + + # Water blocking right after screen, inside parentheses + :waterblocking => Dict( + "FL" => "longitudinally and radially water-proof protection", + "F" => "longitudinally water-proof protection", + "L" => "radially water-proof protection" + ), + + # Inner sheath (such as `…XSH…`, with `H` before the outer sheath) + :inner_sheath => Dict( + "H" => "LSOH compound inner sheath", + ), + + # Armouring + :armouring => Dict( + "B" => "steel tape armouring", + "F" => "armour of galvanised flat steel wires", + "G" => "counter helix of galvanised steel tape", + "R" => "armour of galvanised round steel wires" + ), + + # Sheath + :sheath => Dict( + "KL" => "aluminium sheath", + "K" => "lead sheath" + ), + + # Outer sheath + :outer_sheath => Dict( + "A" => "outer sheath made of fibrous material", + "2Y" => "PE outer sheath", + "Y" => "PVC outer sheath" + ), + + # Grounding / protective conductor suffix + :grounding => Dict( + "I" => "with grounding (protective) conductor", + "J" => "with grounding (protective) conductor", + "O" => "without grounding (protective) conductor" + ) ) -# Canonical order of appearance within the stub +# Expected order of appearance within the stub const ORDER = [ - :designation, - :conductor, - :insulation, - :metallic_screen, - :waterblocking, # immediately after :metallic_screen, parenthesized - :inner_sheath, # H before metallic sheath (e.g., …XSH…) - :sheath, - :armouring, - :outer_sheath, - :grounding, + :designation, + :conductor, + :insulation, + :metallic_screen, + :waterblocking, # immediately after :metallic_screen, parenthesized + :inner_sheath, # H before metallic sheath (such as …XSH…) + :sheath, + :armouring, + :outer_sheath, + :grounding ] # ─────────────────────── Regex builders ──────────────────────── escape_for_rx(s) = replace(s, r"([.^$|?*+\[\]{}\\])" => "\\\\\1") -# Return the non-capturing alternation **as a string**, e.g. "(?:FL|F|L)" +# Return the non-capturing alternation **as a string**, such as "(?:FL|F|L)" function union_pat_str(tokens::Vector{String}) - ts = sort(tokens; by = length, rev = true) # longest first - "(?:" * join(escape_for_rx.(ts), "|") * ")" + ts = sort(tokens; by = length, rev = true) # longest first + "(?:" * join(escape_for_rx.(ts), "|") * ")" end -# If you really want a Regex anchored at start, wrap union_pat_str +# Anchor the alternation at the start of the input. union_pat(tokens::Vector{String}) = Regex("^" * union_pat_str(tokens)) const RXS = let rxs = Dict{Symbol, Regex}() - for fld in ORDER - fld_keys = collect(keys(MAP[fld])) - if fld == :waterblocking - # parenthesized immediately after :metallic_screen (e.g., "(FL)"), anchored - core = union_pat_str(fld_keys) # "(?:FL|F|L)" - rxs[fld] = Regex("^\\(" * core * "\\)") # "^\((?:FL|F|L)\)" - else - rxs[fld] = union_pat(fld_keys) # e.g. "^(?:CE|SE|CW|S|C|H)" - end - end - rxs + for fld in ORDER + fld_keys = collect(keys(MAP[fld])) + if fld == :waterblocking + # parenthesized immediately after :metallic_screen (such as "(FL)"), anchored + core = union_pat_str(fld_keys) # "(?:FL|F|L)" + rxs[fld] = Regex("^\\(" * core * "\\)") # "^\((?:FL|F|L)\)" + else + rxs[fld] = union_pat(fld_keys) # such as "^(?:CE|SE|CW|S|C|H)" + end + end + rxs end # Trailing specs (anchored at start of tail) const RX_CORES_X_CSA = r"^(\d+)\s*[x×]\s*(\d+(?:\.\d+)?)(?:\s*/\s*(\d+(?:\.\d+)?))?" -const RX_VOLTAGE = r"^(\d+(?:\.\d+)?)\s*/\s*(\d+(?:\.\d+)?)\s*(?:kV|KV|kv)\b" -const RX_TYPE = r"^([RSEMOH]{1,3})(?:\s*/\s*V)?\b" # RM, SE, OH, … + optional /V +const RX_VOLTAGE = r"^(\d+(?:\.\d+)?)\s*/\s*(\d+(?:\.\d+)?)\s*(?:kV|KV|kv)\b" +const RX_TYPE = r"^([REMOH]{1,3})(?:\s*/\s*V)?\b" # RM, OH, … + optional /V # KISS conductor type mapping const TYPE_MAP = Dict( - 'R' => "round", 'S' => "sector", 'O' => "oval", - 'E' => "solid", 'M' => "stranded", 'H' => "hollow", - 'V' => "compact", + 'R' => "round", 'O' => "oval", + 'E' => "solid", 'M' => "stranded", 'H' => "hollow", + 'V' => "compact" ) function decode_type(code::AbstractString; has_compact::Bool = false) - words = String[] - for c in code - if haskey(TYPE_MAP, c) - push!(words, TYPE_MAP[c]) - else - @warn "Unknown conductor type letter ignored." letter=String(c) - end - end - if has_compact - push!(words, "compact") - end - # de-dup preserving order - seen = Set{String}(); - uniq = String[] - for w in words - if !(w in seen) - ; - push!(uniq, w); - push!(seen, w); - end - end - return join(uniq, ", ") + words = String[] + for c in code + if haskey(TYPE_MAP, c) + push!(words, TYPE_MAP[c]) + else + @warn "Unknown conductor type letter ignored." letter=string(c) + end + end + if has_compact + push!(words, "compact") + end + # de-dup preserving order + seen = Set{String}() + uniq = String[] + for w in words + if !(w in seen) + push!(uniq, w) + push!(seen, w) + end + end + return join(uniq, ", ") end # ─────────────────────────── Parser ──────────────────────────── """ - vdeparse(code::AbstractString) -> Dict{Symbol,String} +$(TYPEDSIGNATURES) -Parses VDE/DIN 0271/0276 cable codes: -- **stub** (first non-space token): designation → conductor_material (default copper) → insulation (default paper) → screen → waterblocking → inner_sheath → armouring → sheath → grounding -- **tail**: cores × cross-section (optional screen csa), voltage, conductor type (R/S/O + E/M/H, optional `/V` ⇒ compact) +Parse a VDE/DIN 0271 or 0276 cable designation. -Only parsed keys are returned; defaults are materialized when omitted. +# Arguments + +- `code`: cable designation containing the compact type token followed by + optional conductor count, cross-sections, voltage, and conductor form. + +# Returns + +- A dictionary of parsed designation fields. Omitted conductor and insulation + tokens produce copper and impregnated-paper defaults. Unparsed compact-token + text is returned under `:unparsed_stub`. """ function vdeparse(code::AbstractString)::Dict{Symbol, String} - # normalize spaces (NBSP -> space) and trim - s = replace(code, '\u00A0' => ' ') - s = strip(s) - - # split into stub token (first non-space chunk) + tail - m = match(r"^\S+", s) - if m === nothing - return Dict{Symbol, String}() # empty / whitespace line - end - stub = m.match # e.g., "2XS(F)2Y" - tail = strip(s[(length(stub)+1):end]) - - # parse stub left-to-right - out = Dict{Symbol, String}() - rest = stub - for fld in ORDER - rx = RXS[fld] - mm = match(rx, rest) - if mm !== nothing - tok = mm.match - key = (fld == :waterblocking) ? tok[2:(end-1)] : tok - out[fld] = MAP[fld][key] - rest = rest[(length(tok)+1):end] # consume - else - # materialize normative omissions - if fld == :conductor - out[fld] = "copper conductor" - elseif fld == :insulation - out[fld] = "impregnated paper" - end - end - end - # Any non-empty rest means unknown extra within the stub itself - if !isempty(rest) - out[:unparsed_stub] = rest - @warn "Unparsed stub residue." residue=rest - end - - # parse tail in anchored passes - if !isempty(tail) - if (mm = match(RX_CORES_X_CSA, tail)) !== nothing - out[:cores] = mm.captures[1] - out[:conductor_cross_section] = mm.captures[2] - if mm.captures[3] !== nothing - out[:metallic_screen_cross_section] = mm.captures[3] - end - tail = strip(tail[(length(mm.match)+1):end]) - end - end - - if !isempty(tail) - if (mm = match(RX_VOLTAGE, tail)) !== nothing - out[:voltage] = "$(mm.captures[1])/$(mm.captures[2]) kV" - tail = strip(tail[(length(mm.match)+1):end]) - end - end - - if !isempty(tail) - if (mm = match(RX_TYPE, tail)) !== nothing - raw = mm.captures[1] - has_compact = occursin(r"/\s*V\b", mm.match) - out[:conductor_type] = decode_type(raw; has_compact = has_compact) - tail = strip(tail[(length(mm.match)+1):end]) - end - end - - if !isempty(tail) - out[:unparsed] = tail - @warn "Unparsed trailing token(s)." residue=tail - end - - return out + # Normalize spaces (NBSP to space) and trim the result. + s = replace(code, '\u00A0' => ' ') + s = strip(s) + + # split into stub token (first non-space chunk) + tail + m = match(r"^\S+", s) + if m === nothing + return Dict{Symbol, String}() # empty / whitespace line + end + stub = m.match # such as "2XS(F)2Y" + tail = strip(s[(length(stub) + 1):end]) + + # parse stub left-to-right + out = Dict{Symbol, String}() + rest = stub + for fld in ORDER + rx = RXS[fld] + mm = match(rx, rest) + if mm !== nothing + tok = mm.match + key = (fld == :waterblocking) ? tok[2:(end - 1)] : tok + out[fld] = MAP[fld][key] + rest = rest[(length(tok) + 1):end] # consume + else + # Materialize values omitted by the source format. + if fld == :conductor + out[fld] = "copper conductor" + elseif fld == :insulation + out[fld] = "impregnated paper" + end + end + end + # Any non-empty rest means unknown extra within the stub itself + if !isempty(rest) + out[:unparsed_stub] = rest + @warn "Unparsed stub residue." residue=rest + end + + # parse tail in anchored passes + if !isempty(tail) && (mm = match(RX_CORES_X_CSA, tail)) !== nothing + out[:cores] = mm.captures[1] + out[:conductor_cross_section] = mm.captures[2] + if mm.captures[3] !== nothing + out[:metallic_screen_cross_section] = mm.captures[3] + end + tail = strip(tail[(length(mm.match) + 1):end]) + end + + if !isempty(tail) && (mm = match(RX_VOLTAGE, tail)) !== nothing + out[:voltage] = "$(mm.captures[1])/$(mm.captures[2]) kV" + tail = strip(tail[(length(mm.match) + 1):end]) + end + + if !isempty(tail) && (mm = match(RX_TYPE, tail)) !== nothing + raw = mm.captures[1] + has_compact = occursin(r"/\s*V\b", mm.match) + out[:conductor_type] = decode_type(raw; has_compact = has_compact) + tail = strip(tail[(length(mm.match) + 1):end]) + end + + if !isempty(tail) + out[:unparsed] = tail + @warn "Unparsed trailing token(s)." residue=tail + end + + return out end # # ───────────────────────── Smoke tests ───────────────────────── @@ -248,5 +248,3 @@ end # for ex in exs # println(ex, " → ", vdeparse(ex)) # end - - diff --git a/src/datamodel/circstrands.jl b/src/datamodel/circstrands.jl deleted file mode 100644 index e58380ceb..000000000 --- a/src/datamodel/circstrands.jl +++ /dev/null @@ -1,171 +0,0 @@ -""" -$(TYPEDEF) - -Represents an array of wires equally spaced around a circumference of arbitrary radius, with attributes: - -$(TYPEDFIELDS) -""" -struct CircStrands{T <: REALSCALAR, U <: Int} <: AbstractStrandsLayer{T} - "Internal radius of the wire array \\[m\\]." - r_in::T - "External radius of the wire array \\[m\\]." - r_ex::T - "Radius of each individual wire \\[m\\]." - radius_wire::T - "Number of wires in the array \\[dimensionless\\]." - num_wires::U - "Ratio defining the lay length of the wires (twisting factor) \\[dimensionless\\]." - lay_ratio::T - "Mean diameter of the wire array \\[m\\]." - mean_diameter::T - "Pitch length of the wire array \\[m\\]." - pitch_length::T - "Twisting direction of the strands (1 = unilay, -1 = contralay) \\[dimensionless\\]." - lay_direction::U - "Material object representing the physical properties of the wire material." - material_props::Material{T} - "Temperature at which the properties are evaluated \\[°C\\]." - temperature::T - "Cross-sectional area of all wires in the array \\[m²\\]." - cross_section::T - "Electrical resistance per wire in the array \\[Ω/m\\]." - resistance::T - "Geometric mean radius of the wire array \\[m\\]." - gmr::T -end - -""" -$(TYPEDSIGNATURES) - -Constructs a [`CircStrands`](@ref) instance based on specified geometric and material parameters. - -# Arguments - -- `r_in`: Internal radius of the wire array \\[m\\]. -- `radius_wire`: Radius of each individual wire \\[m\\]. -- `num_wires`: Number of wires in the array \\[dimensionless\\]. -- `lay_ratio`: Ratio defining the lay length of the wires (twisting factor) \\[dimensionless\\]. -- `material_props`: A [`Material`](@ref) object representing the material properties. -- `temperature`: Temperature at which the properties are evaluated \\[°C\\]. -- `lay_direction`: Twisting direction of the strands (1 = unilay, -1 = contralay) \\[dimensionless\\]. - -# Returns - -- A [`CircStrands`](@ref) object with calculated geometric and electrical properties. - -# Examples - -```julia -material_props = Material(1.7241e-8, 1.0, 0.999994, 20.0, 0.00393) -circstrands = $(FUNCTIONNAME)(0.01, Diameter(0.002), 7, 10, material_props, temperature=25) -println(circstrands.mean_diameter) # Outputs mean diameter in m -println(circstrands.resistance) # Outputs resistance in Ω/m -``` - -# See also - -- [`Material`](@ref) -- [`ConductorGroup`](@ref) -- [`calc_tubular_resistance`](@ref) -- [`calc_circstrands_gmr`](@ref) -- [`calc_helical_params`](@ref) -""" -function CircStrands( - r_in::T, - radius_wire::T, - num_wires::U, - lay_ratio::T, - material_props::Material{T}, - temperature::T, - lay_direction::U, -) where {T <: REALSCALAR, U <: Int} - - rho = material_props.rho - T0 = material_props.T0 - alpha = material_props.alpha - r_ex = num_wires == 1 ? radius_wire : r_in + 2 * radius_wire # TODO: The resolved outer radius for stranded cores should account for compression. See rectstrands.jl for an example of area-preserving expansion. - # Issue URL: https://github.com/Electa-Git/LineCableModels.jl/issues/32 - - mean_diameter, pitch_length, overlength = calc_helical_params( - r_in, - r_ex, - lay_ratio, - ) - - cross_section = num_wires * (π * radius_wire^2) - - R_wire = - calc_tubular_resistance(0.0, radius_wire, rho, alpha, T0, temperature) * - overlength - R_all_wires = R_wire / num_wires - - gmr = calc_circstrands_gmr( - r_in + radius_wire, - num_wires, - radius_wire, - material_props.mu_r, - ) - - # Initialize object - return CircStrands( - r_in, - r_ex, - radius_wire, - num_wires, - lay_ratio, - mean_diameter, - pitch_length, - lay_direction, - material_props, - temperature, - cross_section, - R_all_wires, - gmr, - ) -end - -const _REQ_CIRCSTRANDS = (:r_in, :radius_wire, :num_wires, :lay_ratio, :material_props) -const _OPT_CIRCSTRANDS = (:temperature, :lay_direction) -const _DEFS_CIRCSTRANDS = (T₀, 1) - -Validation.has_radii(::Type{CircStrands}) = false -Validation.has_temperature(::Type{CircStrands}) = true -Validation.required_fields(::Type{CircStrands}) = _REQ_CIRCSTRANDS -Validation.keyword_fields(::Type{CircStrands}) = _OPT_CIRCSTRANDS -Validation.keyword_defaults(::Type{CircStrands}) = _DEFS_CIRCSTRANDS - -Validation.coercive_fields(::Type{CircStrands}) = - (:r_in, :radius_wire, :lay_ratio, :material_props, :temperature) # not :num_wires, :lay_direction -# accept proxies for radii -Validation.is_radius_input(::Type{CircStrands}, ::Val{:r_in}, - x::AbstractCablePart) = true -Validation.is_radius_input(::Type{CircStrands}, ::Val{:r_ex}, - x::Diameter) = true - -Validation.extra_rules(::Type{CircStrands}) = ( - # radii (post-parse they must be numeric) - Normalized(:r_in), Finite(:r_in), Nonneg(:r_in), - Normalized(:radius_wire), Finite(:radius_wire), Positive(:radius_wire), - - # counts and geometry params - IntegerField(:num_wires), Positive(:num_wires), - Finite(:lay_ratio), Nonneg(:lay_ratio), - - # material type - IsA{Material}(:material_props), - - # lay direction constraint (pin to -1 or +1) - OneOf(:lay_direction, (-1, 1)), -) - -maxfill(::Type{CircStrands}, rin::Real, rw::Real) = - rin == 0 ? 1 : floor(Int, π / asin(rw / (rin + rw))) - -# normalize proxies -> numbers -Validation.parse(::Type{CircStrands}, nt) = begin - rin, rw = _normalize_radii(CircStrands, nt.r_in, nt.radius_wire) - (; nt..., r_in = rin, radius_wire = rw) -end - -# This macro expands to a weakly-typed constructor for CircStrands -@construct CircStrands _REQ_CIRCSTRANDS _OPT_CIRCSTRANDS _DEFS_CIRCSTRANDS diff --git a/src/datamodel/conductorgroup.jl b/src/datamodel/conductorgroup.jl deleted file mode 100644 index b18332cc0..000000000 --- a/src/datamodel/conductorgroup.jl +++ /dev/null @@ -1,238 +0,0 @@ -""" -$(TYPEDEF) - -Represents a composite conductor group assembled from multiple conductive layers or stranded wires. - -This structure serves as a container for different [`AbstractConductorPart`](@ref) elements -(such as wire arrays, strips, and tubular conductors) arranged in concentric layers. -The `ConductorGroup` aggregates these individual parts and provides equivalent electrical -properties that represent the composite behavior of the entire assembly. - -# Attributes - -$(TYPEDFIELDS) -""" - -# mutable struct ConductorGroup{T <: REALSCALAR, L <: AbstractLayout} <: -# AbstractConductorPart{T} -# # MultiCoreGroup...... under CableDesign? -# # L<: Concentric, SectorShaped -# # Concentric -> stacks over radii -# # SectorShaped -> stacks over angles - -# end - -mutable struct ConductorGroup{T <: REALSCALAR} <: AbstractConductorPart{T} - "Inner radius of the conductor group \\[m\\]." - r_in::T - "Outer radius of the conductor group \\[m\\]." - r_ex::T - "Cross-sectional area of the entire conductor group \\[m²\\]." - cross_section::T - "Number of individual wires in the conductor group \\[dimensionless\\]." - num_wires::Int - "Number of turns per meter of each wire strand \\[1/m\\]." - num_turns::T - "DC resistance of the conductor group \\[Ω\\]." - resistance::T - "Temperature coefficient of resistance \\[1/°C\\]." - alpha::T - "Geometric mean radius of the conductor group \\[m\\]." - gmr::T - "Vector of conductor layer components." - layers::Vector{AbstractConductorPart{T}} - - @doc """ - $(TYPEDSIGNATURES) - - Constructs a [`ConductorGroup`](@ref) instance initializing with the central conductor part. - - # Arguments - - - `central_conductor`: An [`AbstractConductorPart`](@ref) object located at the center of the conductor group. - - # Returns - - - A [`ConductorGroup`](@ref) object initialized with geometric and electrical properties derived from the central conductor. - """ - function ConductorGroup{T}( - r_in::T, - r_ex::T, - cross_section::T, - num_wires::Int, - num_turns::T, - resistance::T, - alpha::T, - gmr::T, - layers::Vector{AbstractConductorPart{T}}, - ) where {T} - return new{T}(r_in, r_ex, cross_section, num_wires, num_turns, - resistance, alpha, gmr, layers) - end - - function ConductorGroup{T}(central::AbstractConductorPart{T}) where {T} - num_wires::Int = 0 - num_turns::T = zero(T) - - # only touch fields that exist inside the guarded branches - if central isa CircStrands{T} - num_wires = central.num_wires - num_turns = - central.pitch_length > zero(T) ? one(T) / central.pitch_length : zero(T) - elseif central isa Strip{T} - num_wires = 1 - num_turns = - central.pitch_length > zero(T) ? one(T) / central.pitch_length : zero(T) - end - - return new{T}( - central.r_in, - central.r_ex, - central.cross_section, - num_wires, - num_turns, - central.resistance, - central.material_props.alpha, - central.gmr, - AbstractConductorPart{T}[central], - ) - end -end - - - -# Outer helper that infers T from the central part -ConductorGroup(con::AbstractConductorPart{T}) where {T} = ConductorGroup{T}(con) - -""" -$(TYPEDSIGNATURES) - -Add a new conductor part to a [`ConductorGroup`](@ref), validating raw inputs, -normalizing proxies, and **promoting** the group’s numeric type if required. - -# Behavior: - -1. Apply part-level keyword defaults. -2. Default `r_in` to `group.r_ex` if absent. -3. Compute `Tnew = resolve_T(group, r_in, args..., values(kwargs)...)`. -4. If `Tnew === T`, mutate in place; else `coerce_to_T(group, Tnew)` then mutate and **return the promoted group**. - -# Arguments - -- `group`: [`ConductorGroup`](@ref) object to which the new part will be added. -- `part_type`: Type of conductor part to add ([`AbstractConductorPart`](@ref)). -- `args...`: Positional arguments specific to the constructor of the `part_type` ([`AbstractConductorPart`](@ref)) \\[various\\]. -- `kwargs...`: Named arguments for the constructor including optional values specific to the constructor of the `part_type` ([`AbstractConductorPart`](@ref)) \\[various\\]. - -# Returns - -- The function modifies the [`ConductorGroup`](@ref) instance in place and does not return a value. - -# Notes - -- Updates `gmr`, `resistance`, `alpha`, `r_ex`, `cross_section`, and `num_wires` to account for the new part. -- The `temperature` of the new part defaults to the temperature of the first layer if not specified. -- The `r_in` of the new part defaults to the external radius of the existing conductor if not specified. - -!!! warning "Note" - - When an [`AbstractCablePart`](@ref) is provided as `r_in`, the constructor retrieves its `r_ex` value, allowing the new cable part to be placed directly over the existing part in a layered cable design. - - For uncertain geometries, the preceding part's outer-radius derivative graph - is retained. Adjacent layers therefore share one physical boundary and - cumulative-radius covariance is preserved across different part types. - -# Examples - -```julia -material_props = Material(1.7241e-8, 1.0, 0.999994, 20.0, 0.00393) -conductor = ConductorGroup(Strip(0.01, 0.002, 0.05, 10, material_props)) -$(FUNCTIONNAME)(conductor, CircStrands, 0.02, 0.002, 7, 15, material_props, temperature = 25) -``` - -# See also - -- [`ConductorGroup`](@ref) -- [`CircStrands`](@ref) -- [`Strip`](@ref) -- [`Tubular`](@ref) -- [`calc_equivalent_gmr`](@ref) -- [`calc_parallel_equivalent`](@ref) -- [`calc_equivalent_alpha`](@ref) -""" -function add!( - group::ConductorGroup{T}, - part_type::Type{C}, - args...; - kwargs..., -) where {T, C <: AbstractConductorPart} - - # 1) Merge declared keyword defaults for this part type - kwv = _with_kwdefaults(C, (; kwargs...)) - - # 2) Default stacking: inner radius = current outer radius unless overridden - rin = get(kwv, :r_in, group.r_ex) - kwv = haskey(kwv, :r_in) ? kwv : merge(kwv, (; r_in = rin)) - - # 3) Decide target numeric type using *current group + raw inputs* - Tnew = resolve_T(group, rin, args..., values(kwv)...) - - if Tnew === T - # 4a) Fast path: mutate in place - return _do_add!(group, C, args...; kwv...) - else - @warn """ - Adding a `$Tnew` part to a `ConductorGroup{$T}` returns a **promoted** group. - Capture the result: group = add!(group, $C, …) - """ - promoted = coerce_to_T(group, Tnew) - return _do_add!(promoted, C, args...; kwv...) - end -end - -""" -$(TYPEDSIGNATURES) - -Internal, in-place insertion (no promotion logic). Assumes `:r_in` was materialized. -Runs Validation → parsing, then coerces fields to the group’s `T` and updates -equivalent properties and book-keeping. -""" -function _do_add!( - group::ConductorGroup{Tg}, - C::Type{<:AbstractConductorPart}, - args...; - kwargs..., -) where {Tg} - # Materialize keyword args into a NamedTuple (never poke Base.Pairs internals) - kw = (; kwargs...) - - # Validate + parse with the part’s own pipeline (proxies resolved here) - ntv = Validation.validate!(C, kw.r_in, args...; kw...) - - # Coerce validated values to group’s T and call strict numeric core - order = (Validation.required_fields(C)..., Validation.keyword_fields(C)...) - coerced = _coerced_args(C, ntv, Tg, order) # respects coercive_fields(C) - new_part = C(coerced...) - - # Update equivalent properties - group.gmr = calc_equivalent_gmr(group, new_part) - group.alpha = calc_equivalent_alpha(group.alpha, group.resistance, - new_part.material_props.alpha, - new_part.resistance) - group.resistance = calc_parallel_equivalent(group.resistance, new_part.resistance) - group.r_ex += (new_part.r_ex - new_part.r_in) - group.cross_section += new_part.cross_section - - # CircStrands / Strip bookkeeping - if new_part isa CircStrands || new_part isa Strip - old_wires = group.num_wires - old_turns = group.num_turns - nw = new_part isa CircStrands ? new_part.num_wires : 1 - nt = new_part.pitch_length > 0 ? inv(new_part.pitch_length) : zero(Tg) - group.num_wires += nw - group.num_turns = (old_wires * old_turns + nw * nt) / group.num_wires - end - - push!(group.layers, new_part) - return group -end - -include("conductorgroup/base.jl") diff --git a/src/datamodel/conductorgroup/base.jl b/src/datamodel/conductorgroup/base.jl deleted file mode 100644 index 3d4998d3d..000000000 --- a/src/datamodel/conductorgroup/base.jl +++ /dev/null @@ -1,4 +0,0 @@ -import Base: eltype - -eltype(::ConductorGroup{T}) where {T} = T -eltype(::Type{ConductorGroup{T}}) where {T} = T \ No newline at end of file diff --git a/src/datamodel/design/assembly.jl b/src/datamodel/design/assembly.jl new file mode 100644 index 000000000..9b859e74a --- /dev/null +++ b/src/datamodel/design/assembly.jl @@ -0,0 +1,252 @@ +""" +Store one explicitly placed member of an `Assembly`. +""" +struct AssemblyMember{E <: AbstractCablePart, P <: Pose2} + item::E + at::P +end + +""" +Store the resolved, potentially disconnected geometric boundary of an `Assembly`. + +The member geometric boundaries remain exact. An assembly of sector +cores is not replaced by a circular envelope. Consumers that require one +containing domain must request an explicit `Enclosure`. +""" +struct AssemblyShape{T <: Real, S <: Tuple} <: AbstractShape{T} + members::S + + function AssemblyShape(members::S) where {S <: Tuple} + isempty(members) && throw(ArgumentError( + "an assembly boundary requires at least one member" + )) + all(member -> member isa AbstractShape, members) || throw(ArgumentError( + "assembly boundary members must be resolved shapes" + )) + T = promote_type(map(eltype, members)...) + return new{T, S}(members) + end +end + +boundary(shape::AssemblyShape) = shape +area(shape::AssemblyShape) = sum(area, shape.members) +perimeter(shape::AssemblyShape) = sum(perimeter, shape.members) +function support(shape::AssemblyShape, angle::Real) + maximum(member -> support(member, angle), shape.members) +end +support(shape::AssemblyShape) = maximum(support, shape.members) + +function centroid(shape::AssemblyShape) + areas = map(area, shape.members) + total = sum(areas) + centers = map(centroid, shape.members) + return ( + sum(index -> areas[index] * centers[index][1], eachindex(areas)) / total, + sum(index -> areas[index] * centers[index][2], eachindex(areas)) / total + ) +end + +function resolve(at::Pose2, shape::AssemblyShape) + AssemblyShape(map(member -> resolve(at, member), shape.members)) +end + +AssemblyMember(item::AbstractCablePart) = AssemblyMember(item, Pose2(0, 0, 0)) + +function Base.:(==)(left::AssemblyMember, right::AssemblyMember) + left.item == right.item && left.at == right.at +end + +""" +$(TYPEDEF) + +Arrange physical members while retaining their independent terminal +identities. + +An assembly stores either one prototype with a repetition pattern or an +explicit tuple of independently placed members. Both forms resolve through +the same terminal-preserving operation. + +$(TYPEDFIELDS) +""" +struct Assembly{A, E, P, H, C, N} <: AbstractCablePart + "Pose relative to the containing frame." + at::A + "Repeated prototype or tuple of explicit members." + item::E + "Cross-sectional placement definition for a repeated prototype." + pattern::P + "Longitudinal path definition or `nothing`." + path::H + "Compaction definition or `nothing`." + compact::C + "Exact repeated-member terminal names, or `nothing` for explicit members." + names::N + + function Assembly( + at::A, + item::E, + pattern::P, + path::H, + compact::C, + names::N + ) where {A, E, P, H, C, N} + at isa Pose2 || throw(ArgumentError("assembly pose must resolve to Pose2")) + repeated = item isa AbstractCablePart + explicit = item isa Tuple && !isempty(item) && + all(member -> member isa AssemblyMember, item) + repeated || explicit || + throw(ArgumentError( + "assembly item must be one prototype or explicit placed members" + )) + if repeated + pattern === nothing && throw(ArgumentError( + "a repeated assembly requires a placement pattern" + )) + names isa Union{Nothing, AbstractVector{Symbol}, Tuple{Vararg{Symbol}}} || + throw(ArgumentError( + "assembly names must be exact symbols or nothing" + )) + names === nothing || + all(name -> !isempty(String(name)), names) || + throw(ArgumentError( + "assembly names cannot be empty" + )) + else + pattern === nothing && path === nothing && compact === nothing && + names === nothing || throw(ArgumentError( + "explicit assembly members own their poses and terminal identities" + )) + end + return new{A, E, P, H, C, N}(at, item, pattern, path, compact, names) + end +end + +function Base.:(==)(left::Assembly, right::Assembly) + left.at == right.at && left.item == right.item && + left.pattern == right.pattern && left.path == right.path && + left.compact == right.compact && left.names == right.names +end + +function _append_assembly_member!( + regions::Vector{PlacedRegion}, + child::CableGeometry, + assembly_at::Pose2, + member_at::Pose2; + terminal_map = identity, + pattern = nothing, + member::Int = 1, + path = nothing +) + radius = hypot(member_at.x, member_at.y) + extent = zero(support(boundary(child))) + for source in child.regions + placed = resolve(assembly_at * member_at, source) + terminal = source.terminal === nothing ? nothing : terminal_map(source.terminal) + local_primitive = resolve(member_at, source.primitive) + extent = max(extent, support(local_primitive)) + patterns = pattern === nothing ? placed.placement.patterns : + (placed.placement.patterns..., + (owner = Assembly, pattern = pattern, member = member, pose = member_at)) + paths = path === nothing ? source.paths : + (source.paths..., (path = path, radius = radius)) + push!(regions, + PlacedRegion( + source.source, + placed.primitive, + terminal, + (patterns = patterns,), + paths + )) + end + return extent +end + +function _resolve_repeated(assembly::Assembly) + child = resolve(EmptyBoundary(), assembly.item) + child_terminals = unique(Symbol[source.terminal + for source in child.regions + if source.terminal !== nothing]) + length(child_terminals) <= 1 || throw(ArgumentError( + "a repeated assembly prototype may resolve at most one terminal" + )) + poses = placements(assembly.pattern, child, assembly.compact) + count = length(poses) + count > 0 || throw(ArgumentError("assembly placement cannot be empty")) + member_names = if isempty(child_terminals) + assembly.names === nothing || throw(ArgumentError( + "a zero-terminal repeated assembly does not accept terminal names" + )) + fill(nothing, count) + else + assembly.names === nothing && throw(ArgumentError( + "a terminal-bearing repeated assembly requires exact names" + )) + values = collect(assembly.names) + length(values) == count || throw(DimensionMismatch( + "assembly requires $count names; got $(length(values))" + )) + allunique(values) || throw(ArgumentError("assembly names must be unique")) + values + end + + regions = PlacedRegion[] + boundaries = AbstractShape[] + for (member, (name, placement)) in enumerate(zip(member_names, poses)) + pose = _placement_pose(placement) + mapping = isempty(child_terminals) ? identity : + terminal -> terminal === only(child_terminals) ? name : terminal + _append_assembly_member!( + regions, + child, + assembly.at, + pose; + terminal_map = mapping, + pattern = assembly.pattern, + member, + path = assembly.path + ) + push!(boundaries, resolve(assembly.at * pose, boundary(child))) + end + return CableGeometry(regions, AssemblyShape(Tuple(boundaries))) +end + +function _resolve_explicit(assembly::Assembly) + regions = PlacedRegion[] + terminals = Symbol[] + boundaries = AbstractShape[] + for member in assembly.item + child = resolve(EmptyBoundary(), member.item) + child_terminals = unique(Symbol[source.terminal + for source in child.regions + if source.terminal !== nothing]) + for terminal in child_terminals + terminal in terminals && throw(ArgumentError( + "explicit assembly terminal :$terminal is not unique" + )) + push!(terminals, terminal) + end + _append_assembly_member!( + regions, child, assembly.at, member.at + ) + push!(boundaries, resolve(assembly.at * member.at, boundary(child))) + end + return CableGeometry(regions, AssemblyShape(Tuple(boundaries))) +end + +function resolve( + ::EmptyBoundary, + assembly::Assembly{<:Any, <:AbstractCablePart} +) + _resolve_repeated(assembly) +end + +resolve( + ::EmptyBoundary, + assembly::Assembly{<:Any, <:Tuple} +) = _resolve_explicit(assembly) + +function resolve(::AbstractShape, ::Assembly) + throw(ArgumentError( + "an Assembly requires explicit placement inside a Stack or Enclosure" + )) +end diff --git a/src/datamodel/design/cabledesign.jl b/src/datamodel/design/cabledesign.jl new file mode 100644 index 000000000..07ba6ccaf --- /dev/null +++ b/src/datamodel/design/cabledesign.jl @@ -0,0 +1,321 @@ +""" +$(TYPEDEF) + +Store one completed cable design built from an authoritative physical declaration. + +`origin` is the serialized declaration. Geometry and terminal indexing are +derived together by [`build`](@ref) and cannot be supplied independently. + +$(TYPEDFIELDS) +""" +struct CableDesign{ + T <: Real, + O <: AbstractCablePart, + G <: CableGeometry, + N <: NamedTuple +} + "Stable cable identifier." + cable_id::String + "Authoritative physical declaration." + origin::O + "Descriptive catalog data supplied with the physical declaration." + nominal_data::N + "Resolved physical geometry." + geometry::G + "Retained terminals in physical order." + terminal_order::Vector{Symbol} + "Terminal index for every resolved region. Zero denotes no terminal." + terminal_map::Vector{Int} + + function CableDesign{T, O, G, N}( + cable_id::String, + origin::O, + nominal_data::N, + geometry::G, + terminal_order::Vector{Symbol}, + terminal_map::Vector{Int} + ) where { + T <: Real, O <: AbstractCablePart, G <: CableGeometry, N <: NamedTuple + } + return validate(new{T, O, G, N}( + cable_id, + origin, + nominal_data, + geometry, + terminal_order, + terminal_map + )) + end +end + +Base.eltype(::CableDesign{T}) where {T} = T +Base.eltype(::Type{<:CableDesign{T}}) where {T} = T + +function validate(design::CableDesign) + isempty(design.cable_id) && throw(ArgumentError( + "CableDesign.cable_id cannot be empty" + )) + isempty(design.geometry.regions) && throw(ArgumentError( + "CableDesign.geometry.regions must contain at least one resolved region" + )) + isempty(design.terminal_order) && throw(ArgumentError( + "CableDesign.terminal_order must contain at least one retained terminal" + )) + all(name -> !isempty(String(name)), design.terminal_order) || throw(ArgumentError( + "CableDesign.terminal_order cannot contain an empty terminal name" + )) + allunique(design.terminal_order) || throw(ArgumentError( + "CableDesign.terminal_order must contain unique terminal names; " * + "received $(repr(design.terminal_order))" + )) + length(design.terminal_map) == length(design.geometry.regions) || + throw(DimensionMismatch( + "CableDesign.terminal_map must contain one entry per resolved region; " * + "received $(length(design.terminal_map)) entries for " * + "$(length(design.geometry.regions)) regions" + )) + reference = first(design.geometry.regions).source.material.T0 + for (index, placed) in pairs(design.geometry.regions) + validate(placed.source.material) + expected = if placed.terminal === nothing + 0 + else + something(findfirst(==(placed.terminal), design.terminal_order), 0) + end + design.terminal_map[index] == expected || throw(DimensionMismatch( + "CableDesign.terminal_map[$index] must be $expected for resolved " * + "terminal $(repr(placed.terminal)); received $(design.terminal_map[index])" + )) + placed.source.material.kind === :conductor && placed.terminal === nothing && + throw(ArgumentError( + "CableDesign.geometry.regions[$index] is conductive but has no terminal" + )) + placed.source.material.kind !== :conductor && placed.terminal !== nothing && + throw(ArgumentError( + "CableDesign.geometry.regions[$index] is nonconductive but carries " * + "terminal $(repr(placed.terminal))" + )) + isapprox(placed.source.material.T0, reference) || throw(ArgumentError( + "CableDesign.geometry.regions[$index].source.material.T0 must match " * + "the design reference temperature $reference °C; received " * + "$(placed.source.material.T0) °C" + )) + bounded_index = findall( + entry -> entry.pattern isa BoundedPlacement, + placed.placement.patterns + ) + length(bounded_index) <= 1 || throw(ArgumentError( + "CableDesign.geometry.regions[$index] belongs to more than one " * + "bounded formation" + )) + isempty(bounded_index) && continue + placed.source.material.kind === :conductor || throw(ArgumentError( + "CableDesign.geometry.regions[$index] is a bounded strand but its " * + "material is not conductive" + )) + placed.source.primitive isa Union{Disk, Rectangle} || throw(ArgumentError( + "CableDesign.geometry.regions[$index] has an unsupported bounded " * + "strand declaration $(nameof(typeof(placed.source.primitive)))" + )) + (placed.primitive isa Union{Disk, Rectangle, Polygon, BentStrip} || + (placed.primitive isa Annulus && placed.source.primitive isa Rectangle)) || + throw(ArgumentError( + "CableDesign.geometry.regions[$index] has unsupported resolved " * + "bounded geometry $(nameof(typeof(placed.primitive)))" + )) + declared_area = area(placed.source.primitive) + resolved_area = area(placed.primitive) + isapprox( + resolved_area, + declared_area; + rtol = 5.0e-6, + atol = 0 + ) || throw(ArgumentError( + "CableDesign.geometry.regions[$index] does not preserve its " * + "declared strand area" + )) + entry_index = only(bounded_index) + entry = placed.placement.patterns[entry_index] + entry.member == 1 || continue + enclosing_placements = placed.placement.patterns[(entry_index + 1):end] + formation_indices = [peer_index + for (peer_index, peer) in pairs(design.geometry.regions) + if begin + peer_entries = findall( + member -> member.pattern isa BoundedPlacement, + peer.placement.patterns + ) + if length(peer_entries) != 1 + false + else + peer_entry_index = only(peer_entries) + peer_entry = peer.placement.patterns[peer_entry_index] + isequal( + peer_entry.pattern.boundary, + entry.pattern.boundary + ) && peer.terminal === placed.terminal && + isequal( + peer.placement.patterns[ + (peer_entry_index + 1):end + ], + enclosing_placements + ) + end + end] + members = sort([ + begin + peer = design.geometry.regions[peer_index] + peer_entry = findfirst( + member -> member.pattern isa BoundedPlacement, + peer.placement.patterns + ) + peer.placement.patterns[peer_entry].member + end for peer_index in formation_indices + ]) + members == collect(1:length(members)) || + throw(ArgumentError( + "bounded-formation member identities must be contiguous from one" + )) + source_area = sum( + peer_index -> area( + design.geometry.regions[peer_index].source.primitive + ), + formation_indices + ) + formation_area = sum( + peer_index -> area(design.geometry.regions[peer_index].primitive), + formation_indices + ) + isapprox( + source_area, + formation_area; + rtol = 5.0e-6, + atol = 0 + ) || throw(ArgumentError( + "a bounded formation must preserve its total declared strand area" + )) + boundary = entry.pattern.boundary + boundary isa Union{Disk, SectorShape} || + throw(ArgumentError( + "a bounded formation has an unsupported authoritative boundary" + )) + boundary_area = area(boundary) + formation_area <= boundary_area * (1 + 5.0e-6) || + throw(ArgumentError( + "bounded strand area cannot exceed its authoritative boundary" + )) + end + return design +end + +function Base.:(==)(left::CableDesign, right::CableDesign) + left.cable_id == right.cable_id && left.origin == right.origin && + left.nominal_data == right.nominal_data && left.geometry == right.geometry && + left.terminal_order == right.terminal_order && + left.terminal_map == right.terminal_map +end + +""" +$(TYPEDSIGNATURES) + +Build a completed cable design from v1 physical declarations. + +The method validates physical invariants before resolving contextual geometry and assigning retained terminals, and freezes the resulting [`CableGeometry`](@ref). +It does not perform a formulation computation. + +# Arguments + +- `CableDesign`: completed target type. +- `cable_id`: stable cable identifier. +- `parts`: physical `Region`, `Stack`, `Group`, `Assembly`, or `Enclosure` declarations. +- `nominal_data`: descriptive nominal_data data, or `nothing`. + +# Keywords + +- `combine`: gridspace composition mode. It is validated here for a common + scalar and parametric API. Scalar construction uses one value. + +# Returns + +- One completed `CableDesign`. +""" +function build( + ::Type{CableDesign}, + cable_id::AbstractString, + parts::Tuple{Vararg{AbstractCablePart}}, + nominal_data::Union{Nothing, NamedTuple}; + combine::Symbol = :product +) + isempty(parts) && throw(ArgumentError("a cable design requires one physical part")) + origin = length(parts) == 1 ? only(parts) : Stack(parts...) + # 1. Public conveniences have already lowered into the physical grammar. + # A conductive Region does not acquire a terminal from its material class. + normalized = origin + + # 2. Validate formulation-independent declarations. + combine in (:product, :zip) || throw(ArgumentError( + "combine must be :product or :zip" + )) + identifier = String(cable_id) + isempty(identifier) && throw(ArgumentError("cable_id cannot be empty")) + nominal_data = nominal_data === nothing ? (;) : nominal_data + nominal_data isa NamedTuple || throw(ArgumentError( + "nominal_data must be a named tuple or nothing" + )) + + # 3. Resolve intrinsic primitives against their contextual geometric boundaries. + # 4. Resolve pattern placements and compaction through `placements` dispatch. + # 5. Resolve longitudinal path radii while traversing the same physical tree. + # 6. Collect the resolved primitives as one CableGeometry in physical order. + geometry = resolve(EmptyBoundary(), normalized) + isempty(geometry.regions) && throw(ArgumentError( + "a cable design requires one region" + )) + + # 7. Assign conductive primitives to the terminal declared by their Group. + # 8. Establish terminal order and the region-to-terminal map. + terminal_order = Symbol[] + for placed in geometry.regions + if placed.source.material.kind === :conductor && placed.terminal === nothing + throw(ArgumentError( + "conductive region :$(placed.source.tag) is not owned by a Group terminal" + )) + end + placed.terminal === nothing || placed.terminal in terminal_order || + push!(terminal_order, placed.terminal) + end + isempty(terminal_order) && throw(ArgumentError( + "a cable design requires at least one retained terminal" + )) + terminal_map = Int[placed.terminal === nothing ? 0 : + something(findfirst(==(placed.terminal), terminal_order), 0) + for placed in geometry.regions] + + reference = first(geometry.regions).source.material.T0 + all(placed -> isapprox(placed.source.material.T0, reference), geometry.regions) || + throw(ArgumentError("all cable materials must share one reference temperature")) + + # 9. Freeze the authoritative origin and its completed geometry together. + T = promote_type( + (eltype(placed.primitive) for placed in geometry.regions)..., + (eltype(placed.source.material) for placed in geometry.regions)... + ) + return CableDesign{T, typeof(normalized), typeof(geometry), typeof(nominal_data)}( + identifier, + normalized, + nominal_data, + geometry, + terminal_order, + terminal_map + ) +end + +function build( + ::Type{CableDesign}, + cable_id::AbstractString, + parts::AbstractCablePart...; + nominal_data = nothing, + combine::Symbol = :product +) + return build(CableDesign, cable_id, parts, nominal_data; combine) +end diff --git a/src/datamodel/design/enclosure.jl b/src/datamodel/design/enclosure.jl new file mode 100644 index 000000000..3254543ca --- /dev/null +++ b/src/datamodel/design/enclosure.jl @@ -0,0 +1,369 @@ +""" +$(TYPEDEF) + +Mark a resolved fill or wall region that establishes an enclosure boundary. + +Geometry methods use this marker to identify an enclosure boundary. +""" +struct EnclosureBoundary end + +""" +$(TYPEDEF) + +Contain a physical cable object in an explicit cross-section, fill, and wall. + +$(TYPEDFIELDS) +""" +struct Enclosure{ + A, + S <: AbstractPrimitive, + E <: AbstractCablePart, + F, + W +} <: AbstractCablePart + "Physical enclosure identity." + tag::Symbol + "Pose relative to the containing frame." + at::A + "Containing cross-sectional primitive." + primitive::S + "Enclosed physical object." + item::E + "Filling material or filling Region." + fill::F + "Optional physical wall." + wall::W + + function Enclosure( + tag::Symbol, + at::A, + primitive::S, + item::E, + fill::F, + wall::W + ) where {A, S <: AbstractPrimitive, E <: AbstractCablePart, F, W} + isempty(String(tag)) && throw(ArgumentError("enclosure tag cannot be empty")) + at isa Pose2 || throw(ArgumentError("enclosure pose must resolve to Pose2")) + fill isa Union{Material, Region} || + throw(ArgumentError("enclosure fill must be Material or Region")) + wall isa Union{Nothing, AbstractCablePart} || + throw(ArgumentError("enclosure wall must be a cable part or nothing")) + return new{A, S, E, F, W}(tag, at, primitive, item, fill, wall) + end +end + +function Base.:(==)(left::Enclosure, right::Enclosure) + left.tag == right.tag && left.at == right.at && + left.primitive == right.primitive && left.item == right.item && + left.fill == right.fill && left.wall == right.wall +end + +function resolve( + container::Disk, + holes::Tuple, + material::Material, + tag::Symbol +) + length(holes) == 1 && return _disk_fill(container, only(holes), material, tag) + # A coaxial terminal may supply a disk plus several annuli as its occupied + # shapes. Their union is one disk only when the radial intervals are complete. + # Preserve that exact annular fill instead of manufacturing a Boolean region + # that computational flattening would classify as non-radial interstitial fill. + if all(holes) do shape + shape isa Union{Disk, Annulus} && + iszero(shape.at.x) && iszero(shape.at.y) && + iszero(container.at.x) && iszero(container.at.y) + end + intervals = sort!([(r_in(shape), r_ex(shape)) for shape in holes]; by = first) + outer = zero(container.r) + tolerance = 100 * eps(typeof(float(_geometry_scalar(container.r)))) * container.r + for (inner, radius) in intervals + inner <= outer + tolerance || return _difference_fill(container, holes, material, tag) + outer = max(outer, radius) + end + return _disk_fill(container, Disk(outer), material, tag) + end + return _difference_fill(container, holes, material, tag) +end + +function _disk_fill( + container::Disk, + contents::Disk, + material::Material, + tag::Symbol +) + fill_region = Region( + Symbol(tag, :_fill), + Annulus(support(contents), container.r), + material + ) + return resolve(EmptyBoundary(), fill_region) +end + +function _disk_fill(container::Disk, contents, material::Material, tag::Symbol) + _difference_fill(container, (contents,), material, tag) +end + +function _difference_fill(container, holes, material, tag) + source = Region(Symbol(tag, :_fill), _definition(container), material) + primitive = DifferenceShape(container, holes) + area(primitive) > zero(eltype(primitive)) || throw(DomainError( + area(primitive), "enclosure fill must have positive area" + )) + return CableGeometry(PlacedRegion[PlacedRegion(source, primitive)], boundary(container)) +end + +function resolve(container::AbstractShape, holes::Tuple, material::Material, tag::Symbol) + _difference_fill(container, holes, material, tag) +end + +_definition(primitive::Disk) = Disk(primitive.r) +_definition(primitive::Rectangle) = Rectangle(primitive.w, primitive.h) +_definition(primitive::Ellipse) = Ellipse(primitive.a, primitive.b) +_definition(shape::SectorShape) = shape.primitive +_definition(primitive::Annulus) = Annulus(primitive.ri, primitive.ro) +_definition(primitive::Polygon) = Polygon(primitive.points) + +function resolve( + ::AbstractPrimitive, + holes::Tuple, + fill::Region, + ::Symbol +) + length(holes) == 1 ? resolve(only(holes), fill) : + throw(ArgumentError( + "an explicit fill Region requires one occupied boundary" + )) +end + +function _contained(container::Disk, child::AbstractShape) + extent = support(child) + tolerance = 5.0e-6 * container.r + return extent <= container.r + tolerance +end + +function _contained(container::Rectangle, child::AbstractShape) + return support(child, 0) <= container.w / 2 && + support(child, pi) <= container.w / 2 && + support(child, pi / 2) <= container.h / 2 && + support(child, -pi / 2) <= container.h / 2 +end + +function _contained(container::Ellipse, child::AbstractShape) + tolerance = sqrt(eps(float(max(container.a, container.b)))) + angles = range(zero(container.a), oftype(container.a, 2pi); length = 4097) + return all(Iterators.take(angles, 4096)) do angle + support(child, angle) <= support(container, angle) + tolerance + end +end + +function _contained(container::Annulus, child::Disk) + offset = hypot( + child.at.x - container.at.x, + child.at.y - container.at.y + ) + return (offset + child.r < container.ro || + isapprox(offset + child.r, container.ro)) && + (offset - child.r > container.ri || + isapprox(offset - child.r, container.ri)) +end + +function _contained(container::Annulus, child::Annulus) + concentric = isapprox(child.at.x, container.at.x) && + isapprox(child.at.y, container.at.y) + return concentric && + (child.ro < container.ro || isapprox(child.ro, container.ro)) && + (child.ri > container.ri || isapprox(child.ri, container.ri)) +end + +function _contained(container::Annulus, child::BentStrip) + concentric = isapprox(child.at.x, container.at.x) && + isapprox(child.at.y, container.at.y) + return concentric && + (child.ro < container.ro || isapprox(child.ro, container.ro)) && + (child.ri > container.ri || isapprox(child.ri, container.ri)) +end + +function _contained(container::SectorShape, child::Disk) + accommodates(container, centroid(child), child.r) +end + +function _contained(container::SectorShape, child::Polygon) + cosine = cos(child.at.φ) + sine = sin(child.at.φ) + tolerance = 5.0e-6 * container.primitive.r_back + return all(child.points) do point + resolved = ( + child.at.x + cosine * point[1] - sine * point[2], + child.at.y + sine * point[1] + cosine * point[2] + ) + _geometry_scalar(clearance(container, resolved) + tolerance) >= 0 + end +end + +function _contained(::AbstractShape, ::AbstractShape) + throw(ArgumentError( + "enclosure containment is not implemented for this resolved shape pair" + )) +end + +function fill_holes(::AbstractCablePart, contents::CableGeometry) + return Tuple(source.primitive for source in contents.regions) +end + +fill_holes(::Enclosure, contents::CableGeometry) = (boundary(contents),) + +function fill_holes(group::Group, contents::CableGeometry) + if group.boundary isa Disk && last(contents.regions).source.primitive isa Rectangle + outer = boundary(contents) + center = first(contents.regions).primitive + if center isa Disk && center.at.x == outer.at.x && center.at.y == outer.at.y + # The formation already resolved its exact occupied disk. + return (outer,) + end + end + group.item isa Assembly || return Tuple(source.primitive for source in contents.regions) + outer = boundary(contents) + outer isa AssemblyShape || throw(ArgumentError( + "a grouped assembly must retain its member boundaries" + )) + return outer.members +end + +function fill_holes(assembly::Assembly, contents::CableGeometry) + outer = boundary(contents) + outer isa AssemblyShape || throw(ArgumentError( + "an enclosed assembly must retain its member boundaries" + )) + return outer.members +end + +function resolve(context::EmptyBoundary, enclosure::Enclosure) + container = resolve(EmptyBoundary(), enclosure.primitive) + contents = resolve(EmptyBoundary(), enclosure.item) + return _resolve_enclosure(enclosure, container, contents) +end + +function _resolve_enclosure(enclosure::Enclosure, container, contents::CableGeometry) + holes = enclosure.fill isa Material ? + fill_holes(enclosure.item, contents) : (boundary(contents),) + all(hole -> _contained(container, hole), holes) || throw(DomainError( + holes, + "enclosed geometry must fit inside the enclosure boundary" + )) + # A complete circular partition is one occupied disk even through terminal + # and stack wrappers. Incomplete formations retain their individual holes. + occupied = boundary(contents) + if container isa Disk && occupied isa Disk && + occupied.at.x == container.at.x && occupied.at.y == container.at.y && + isapprox(sum(area, holes), area(occupied); + rtol=0, atol=geometry_tolerance(area(occupied))) + holes = (occupied,) + end + + regions = PlacedRegion[] + for source in contents.regions + placed = resolve(enclosure.at, source) + push!(regions, + PlacedRegion( + source.source, + placed.primitive, + source.terminal, + placed.placement, + source.paths + )) + end + + remaining_area = area(container) - sum(area, holes; init = zero(area(container))) + tolerance = geometry_tolerance(area(container)) + remaining_area >= -tolerance || throw(DomainError( + remaining_area, "enclosure contents exceed the containing boundary area" + )) + if enclosure.fill isa Region || remaining_area > tolerance + fill_result = resolve( + container, + holes, + enclosure.fill, + enclosure.tag + ) + outer_extent = support(container) + isapprox(support(boundary(fill_result)), outer_extent) || throw(DomainError( + support(boundary(fill_result)), + "enclosure fill must reach the containing boundary" + )) + for source in fill_result.regions + placed = resolve(enclosure.at, source) + push!(regions, + PlacedRegion( + source.source, + placed.primitive, + source.terminal, + ( + patterns = ( + placed.placement.patterns..., + ( + owner = Enclosure, + pattern = EnclosureBoundary(), + member = 1, + pose = enclosure.at + ) + ), + ), + source.paths + )) + end + end + + outer = container + if enclosure.wall !== nothing + wall_result = resolve(container, enclosure.wall) + for source in wall_result.regions + placed = resolve(enclosure.at, source) + push!(regions, + PlacedRegion( + source.source, + placed.primitive, + source.terminal, + ( + patterns = ( + placed.placement.patterns..., + ( + owner = Enclosure, + pattern = EnclosureBoundary(), + member = 1, + pose = enclosure.at + ) + ), + ), + source.paths + )) + end + outer = boundary(wall_result) + end + return CableGeometry(regions, resolve(enclosure.at, boundary(outer))) +end + +function resolve(context::AbstractShape, enclosure::Enclosure) + container = resolve(EmptyBoundary(), enclosure.primitive) + container isa Annulus || throw(ArgumentError( + "a contextual Enclosure requires an annular containing primitive" + )) + placed = resolve(enclosure.at, container) + context isa Disk && + isapprox(context.at.x, placed.at.x) && + isapprox(context.at.y, placed.at.y) && + context.r <= placed.ri + geometry_tolerance(placed.ri) || throw(DomainError( + context, + "an annular Enclosure must be concentric with and outside the preceding circular boundary" + )) + # A filled course defines its complement down to the preceding physical + # boundary. Explicit wire radii remain fixed. Contextual rings use this disk. + # An explicit fill Region has its own geometry and is not extended. + enclosure.fill isa Material || isapprox(context.r, placed.ri; + rtol=0, atol=geometry_tolerance(placed.ri)) || throw(DomainError( + context, "an explicit fill Region must continue the preceding boundary")) + inner = Disk(context.r, container.at) + contents = resolve(inner, enclosure.item) + effective = Annulus(context.r, container.ro, container.at) + return _resolve_enclosure(enclosure, effective, contents) +end diff --git a/src/datamodel/design/group.jl b/src/datamodel/design/group.jl new file mode 100644 index 000000000..f793ebfe0 --- /dev/null +++ b/src/datamodel/design/group.jl @@ -0,0 +1,507 @@ +""" +$(TYPEDEF) + +Represent one repeated-member coalescing scope. + +Each conductive descendant resolves to terminal `name`. A group containing no +conductive descendant is a valid physical group and contributes no terminal. + +$(TYPEDFIELDS) +""" +struct Group{A, E <: AbstractCablePart, P, H, C, B} <: AbstractCablePart + "Coalesced terminal name when the group contains conductive descendants." + name::Symbol + "Pose relative to the containing frame." + at::A + "Repeated physical member." + item::E + "Cross-sectional placement definition." + pattern::P + "Longitudinal path definition or `nothing`." + path::H + "Compaction definition or `nothing`." + compact::C + "Formation boundary, or rectangular-course packing limit. `nothing` for unbounded groups." + boundary::B + + function Group( + name::Symbol, + at::A, + item::E, + pattern::P, + path::H, + compact::C, + boundary::B + ) where {A, E <: AbstractCablePart, P, H, C, B} + isempty(String(name)) && throw(ArgumentError("group name cannot be empty")) + at isa Pose2 || throw(ArgumentError("group pose must resolve to Pose2")) + boundary === nothing || boundary isa Union{Disk, Sector} || throw(ArgumentError( + "group boundary must be a Disk, Sector, or nothing" + )) + boundary === nothing || pattern === nothing || throw(ArgumentError( + "a bounded formation owns its member placements and cannot carry a group pattern" + )) + boundary === nothing || path === nothing || throw(ArgumentError( + "a bounded formation cannot carry a group-level longitudinal path" + )) + boundary isa Sector && compact !== nothing && throw(ArgumentError( + "sector bounded formations are intrinsically compacted" + )) + return new{A, E, P, H, C, B}( + name, at, item, pattern, path, compact, boundary + ) + end +end + +Group(name::Symbol, at, item, pattern, path, compact) = + Group(name, at, item, pattern, path, compact, nothing) + +function Base.:(==)(left::Group, right::Group) + left.name == right.name && left.at == right.at && left.item == right.item && + left.pattern == right.pattern && left.path == right.path && + left.compact == right.compact && left.boundary == right.boundary +end + +function _path_radius(pattern::Ring, pose::Pose2, primitive::Annulus) + return iszero(pattern.r) ? (r_in(primitive) + r_ex(primitive)) / 2 : pattern.r +end +_path_radius(pattern::Ring, pose::Pose2, primitive::AbstractShape) = pattern.r +function _path_radius(::Nothing, pose::Pose2, primitive::Union{Annulus, BentStrip}) + (r_in(primitive) + r_ex(primitive)) / 2 +end +_path_radius(::Nothing, pose::Pose2, primitive::AbstractShape) = hypot(pose.x, pose.y) +_path_radius(pattern, pose::Pose2, primitive::AbstractShape) = hypot(pose.x, pose.y) + +function _resolved_path_radius(compact, pattern, pose, primitive) + _path_radius(pattern, pose, primitive) +end +function _resolved_path_radius( + compact, pattern, pose, primitive::Union{Polygon, BentStrip} +) + center = centroid(primitive) + return hypot(center...) +end +function _resolved_path_radius(::FillFactor, pattern, pose, primitive::Annulus) + (r_in(primitive) + r_ex(primitive)) / 2 +end + +_member_definition(region::Region) = region.primitive +_member_definition(::AbstractCablePart) = nothing + +function bounded_declarations(group::Group) + group.item isa Stack || throw(ArgumentError( + "a bounded formation must own an ordered Stack of strand declarations" + )) + parts = collect(group.item.items) + for part in parts + part isa Group || throw(ArgumentError( + "a bounded formation Stack may contain only strand Groups" + )) + part.item isa Region || throw(ArgumentError( + "each bounded strand declaration must own one Region" + )) + part.at == Pose2(0, 0, 0) || throw(ArgumentError( + "bounded strand declarations must share the formation origin" + )) + part.boundary === nothing || throw(ArgumentError( + "nested bounded formations are not supported" + )) + part.pattern === nothing || throw(ArgumentError( + "a bounded strand inventory cannot declare a Ring or other placement" + )) + part.compact === nothing || throw(ArgumentError( + "bounded compaction belongs to the containing formation" + )) + end + return parts +end + +function strand_path(paths, course::Int, maximum_course::Int) + iszero(course) && return nothing + paths === nothing && return nothing + paths isa Tuple || return paths + length(paths) == maximum_course || throw(DimensionMismatch( + "lay schedule requires exactly $maximum_course inferred courses; got $(length(paths))" + )) + return paths[course] +end + +function circular_members(boundary_shape::Disk, parts, compact::Bool) + length(parts) == 2 || throw(ArgumentError( + "a circular stranded core requires center and outer strand declarations" + )) + center_part, strand_part = parts + center = center_part.item.primitive + strand = strand_part.item.primitive + center isa Disk || throw(ArgumentError( + "a circular stranded core requires a Disk center wire" + )) + strand isa Disk || throw(ArgumentError( + "circular strand packing requires Disk source wires" + )) + center.r <= boundary_shape.r || throw(DomainError( + center.r, "the center wire exceeds the circular core boundary" + )) + + outer = circular_courses(boundary_shape, center, strand, compact) + sites = [( + site = (boundary_shape.at.x, boundary_shape.at.y), + course = 0, + member = 1, + angle = zero(boundary_shape.r) + ); outer] + maximum_course = maximum(getproperty.(sites, :course)) + members = NamedTuple[] + for (member, placement) in enumerate(sites) + source = member == 1 ? center_part.item : strand_part.item + path = member == 1 ? nothing : + strand_path(strand_part.path, placement.course, maximum_course) + angle = placement.angle + push!(members, (; + source, + placement.course, + member, + path, + angle, + placement.site + )) + end + primitives = compact ? deform_disk_members(boundary_shape, members) : + [resolve(Pose2(member.site..., member.angle), member.source.primitive) + for member in members] + return members, primitives +end + +function sector_members(boundary_shape::SectorShape, parts) + length(parts) == 1 || throw(ArgumentError( + "a sector stranded core uses one declaration for its center and course wires" + )) + part = only(parts) + wire = part.item.primitive + wire isa Disk || throw(ArgumentError( + "a sector stranded core requires Disk source wires" + )) + courses, primitives, maximum_course = sector_courses(boundary_shape, wire) + members = [( + source = part.item, + placement.course, + member, + path = strand_path(part.path, placement.course, maximum_course), + placement.angle, + placement.site + ) for (member, placement) in enumerate(courses)] + return members, primitives +end + +function rectangular_members(boundary_shape::Disk, parts, compact::Bool) + compact || throw(ArgumentError( + "rectangular strands require area-preserving bending" + )) + length(parts) == 2 || throw(ArgumentError( + "a rectangular stranded core requires center and strand declarations" + )) + center_part, strand_part = parts + center = center_part.item.primitive + strand = strand_part.item.primitive + center isa Disk || throw(ArgumentError( + "a rectangular stranded core requires a Disk center wire" + )) + strand isa Rectangle || throw(ArgumentError( + "rectangular strand packing requires Rectangle source strands" + )) + strips = rectangular_strands(boundary_shape, center, strand) + maximum_course = maximum(getproperty.(strips, :course)) + members = NamedTuple[( + source = center_part.item, + course = 0, + member = 1, + path = nothing, + angle = zero(boundary_shape.r), + site = (boundary_shape.at.x, boundary_shape.at.y) + )] + primitives = AbstractShape[ + resolve(Pose2(boundary_shape.at.x, boundary_shape.at.y, boundary_shape.at.φ), center) + ] + for strip in strips + push!(members, ( + source = strand_part.item, + strip.course, + member = length(members) + 1, + path = strand_path(strand_part.path, strip.course, maximum_course), + angle = strip.primitive.at.φ, + strip.site + )) + push!(primitives, strip.primitive) + end + return members, primitives +end + +""" +$(TYPEDSIGNATURES) + +Construct the members and placed primitives of a bounded wire formation from +its physical boundary, ordered part declarations, and compaction selection. +Coordinates and dimensions are in meters. Return `(members, primitives)` in +construction order. +""" +function bounded_members(boundary_shape::Disk, parts, compact::Bool) + outer = last(parts).item.primitive + return outer isa Disk ? + circular_members(boundary_shape, parts, compact) : + rectangular_members(boundary_shape, parts, compact) +end + +bounded_members(boundary_shape::SectorShape, parts, ::Nothing) = + sector_members(boundary_shape, parts) + +""" +$(TYPEDSIGNATURES) + +Resolve a bounded wire group into `(boundary_shape, members, primitives)`. +The returned geometric boundary follows the group's pose and the occupied rectangular +courses when present. Coordinates and dimensions are in meters. Member and +primitive order follows construction. An empty formation raises `ArgumentError`. +""" +function bounded_members(group::Group) + boundary_shape = resolve(group.at, group.boundary) + parts = bounded_declarations(group) + members, primitives = bounded_members(boundary_shape, parts, group.compact) + isempty(members) && throw(ArgumentError( + "a bounded formation requires at least one strand" + )) + if boundary_shape isa Disk && last(parts).item.primitive isa Rectangle + # Complete bent courses occupy exactly this disk. The requested + # packing limit is not an additional physical layer. + boundary_shape = Disk(r_ex(last(primitives)), boundary_shape.at) + end + return boundary_shape, members, primitives +end + +function bounded_pose(absolute, origin::Pose2, angle) + dx = absolute[1] - origin.x + dy = absolute[2] - origin.y + cosine = cos(origin.φ) + sine = sin(origin.φ) + return Pose2( + cosine * dx + sine * dy, + -sine * dx + cosine * dy, + angle + ) +end + +function resolve_bounded(group::Group) + outer, members, primitives = bounded_members(group) + length(primitives) == length(members) || throw(DimensionMismatch( + "bounded resolution must preserve the inferred strand count" + )) + formation_center = centroid(outer) + regions = PlacedRegion[] + for (formation_member, (member, primitive)) in enumerate(zip(members, primitives)) + absolute_center = centroid(primitive) + pose = bounded_pose(member.site, group.at, member.angle) + patterns = (( + owner = Group, + pattern = BoundedPlacement(outer, member.course), + member = formation_member, + pose = pose + ),) + paths = member.path === nothing ? () : + (( + path = member.path, + radius = _path_radius( + nothing, + Pose2(absolute_center[1] - formation_center[1], + absolute_center[2] - formation_center[2]), + primitive + ) + ),) + terminal = member.source.material.kind === :conductor ? group.name : nothing + push!(regions, + PlacedRegion( + member.source, + primitive, + terminal, + (patterns = patterns,), + paths + )) + end + return CableGeometry(regions, outer) +end + +_radial_half_extent(definition::Disk) = definition.r +_radial_half_extent(definition::Rectangle) = definition.h / 2 +function _radial_half_extent(definition::AbstractPrimitive) + support(resolve(EmptyBoundary(), definition)) +end + +_contextual_pattern(pattern, item, child, compact, context) = pattern +function _contextual_pattern( + pattern::Ring, + item, + child, + compact, + context +) + definition = _member_definition(item) + inner = context isa EmptyBoundary ? zero(support(boundary(child))) : support(context) + radial = definition === nothing ? support(boundary(child)) : + _radial_half_extent(definition) + radius = something(pattern.r, inner + radial) + count = if pattern.n isa Int + pattern.n + elseif definition === nothing + capacity(Ring, radius, support(boundary(child)); gap_frac = pattern.gap_frac) + else + capacity( + Ring(pattern.n; r = radius, φ0 = pattern.φ0, + span = pattern.span, gap_frac = pattern.gap_frac), + definition, + compact + ) + end + count > 0 || throw(ArgumentError( + "the group geometry cannot admit one member" + )) + return Ring( + count; + r = radius, + φ0 = pattern.φ0, + span = pattern.span, + gap_frac = pattern.gap_frac + ) +end + +function _group_placements(pattern, item, child, compact, context) + concrete = _contextual_pattern(pattern, item, child, compact, context) + subject = _member_definition(item) + subject === nothing && (subject = child) + return concrete, placements(concrete, subject, compact) +end + +function _minimum_radius(primitive::Union{Annulus, BentStrip}) + iszero(primitive.at.x) && iszero(primitive.at.y) && return r_in(primitive) + return invoke(_minimum_radius, Tuple{AbstractShape}, primitive) +end +function _minimum_radius(primitive::AbstractShape) + center = centroid(primitive) + center_radius = hypot(center...) + iszero(center_radius) && return zero(eltype(primitive)) + φ = atan(center[2], center[1]) + return -support(primitive, φ + pi) +end + +function _resolve_group( + context::Union{EmptyBoundary, AbstractShape}, + group::Group +) + if group.boundary !== nothing + context isa EmptyBoundary || throw(ArgumentError( + "a bounded stranded formation must be the innermost physical formation" + )) + return resolve_bounded(group) + end + if group.pattern === nothing + child = resolve(context, group.item) + regions = PlacedRegion[] + for source in child.regions + placed = resolve(group.at, source) + terminal = source.source.material.kind === :conductor ? group.name : + source.terminal + # This scope coalesces even terminal-preserving child assemblies. + patterns = map(placed.placement.patterns) do entry + entry.owner === Assembly ? merge(entry, (owner = Group,)) : entry + end + center = centroid(source.primitive) + paths = group.path === nothing ? source.paths : + (source.paths..., + ( + path = group.path, + radius = _path_radius( + nothing, + Pose2(center[1], center[2], 0), + source.primitive + ) + )) + push!(regions, PlacedRegion( + source.source, + placed.primitive, + terminal, + (patterns = patterns,), + paths + )) + end + outer = group.path === nothing ? boundary(child) : Disk(support(boundary(child))) + return CableGeometry(regions, resolve(group.at, outer)) + end + + child = resolve(EmptyBoundary(), group.item) + pattern, members = _group_placements( + group.pattern, group.item, child, group.compact, context + ) + isempty(members) && throw(ArgumentError("group placement cannot be empty")) + + regions = PlacedRegion[] + local_extent = nothing + for (member, placement) in enumerate(members) + pose = _placement_pose(placement) + placed_at = group.at * pose + for source in child.regions + terminal = source.source.material.kind === :conductor ? group.name : + source.terminal + definition = _placement_definition( + placement, + source.source.primitive + ) + local_primitive = placement isa _ResolvedPlacement ? + resolve(pose, definition) : + resolve(pose, source.primitive) + primitive = resolve(group.at, local_primitive) + placed = resolve(placed_at, source) + extent = support(local_primitive) + local_extent = local_extent === nothing ? extent : max(local_extent, extent) + patterns = map(placed.placement.patterns) do entry + entry.owner === Assembly ? merge(entry, (owner = Group,)) : entry + end + patterns = (patterns..., + (owner = Group, pattern = pattern, member = member, pose = pose)) + paths = group.path === nothing ? source.paths : + (source.paths..., + ( + path = group.path, + radius = _resolved_path_radius( + group.compact, + pattern, + pose, + local_primitive + ) + )) + push!(regions, PlacedRegion( + source.source, + primitive, + terminal, + (patterns = patterns,), + paths + )) + end + end + local_extent === nothing && throw(ArgumentError("group placement cannot be empty")) + local_boundary = Disk(local_extent) + return CableGeometry(regions, resolve(group.at, local_boundary)) +end + +resolve(context::EmptyBoundary, group::Group) = _resolve_group(context, group) + +function resolve(context::AbstractShape, group::Group) + result = _resolve_group(context, group) + current_radius = support(context) + tolerance = sqrt(eps(typeof(float(current_radius)))) * + max(one(current_radius), current_radius) + for source in result.regions + minimum_radius = _minimum_radius(source.primitive) + minimum_radius + tolerance >= current_radius || throw(DomainError( + minimum_radius, + "group geometry overlaps the current stack boundary at radius $current_radius" + )) + end + return result +end diff --git a/src/datamodel/design/region.jl b/src/datamodel/design/region.jl new file mode 100644 index 000000000..b735cae00 --- /dev/null +++ b/src/datamodel/design/region.jl @@ -0,0 +1,151 @@ +""" +$(TYPEDEF) + +Represent one homogeneous physical domain with intrinsic geometry. + +$(TYPEDFIELDS) +""" +struct Region{P, M} <: AbstractCablePart + "Physical identity within its containing cable object." + tag::Symbol + "Intrinsic cross-sectional geometry." + primitive::P + "Constitutive material data." + material::M + + function Region(tag::Symbol, primitive::P, material::M) where { + P, M + } + isempty(String(tag)) && throw(ArgumentError("region tag cannot be empty")) + primitive isa Union{AbstractPrimitive, Shell} || throw(ArgumentError( + "region geometry must be an intrinsic primitive or contextual Shell" + )) + material isa AbstractMaterial || + throw(ArgumentError("region material must resolve to AbstractMaterial")) + return new{P, M}(tag, primitive, material) + end +end + +# A material for the cable part of `role` in a region, restricted to the `allowed` kinds. +function validate(material, ::Type{Region}, role::Symbol, allowed::Tuple) + material isa AbstractMaterial || throw(ArgumentError( + "$role material must resolve to AbstractMaterial" + )) + material.kind in allowed || throw(ArgumentError( + "$role material must have kind $(join(string.(allowed), " or ")); " * + "got :$(material.kind)" + )) + return material +end + +function Base.:(==)(left::Region, right::Region) + left.tag == right.tag && + left.primitive == right.primitive && + left.material == right.material +end + +function Base.isequal(left::Region, right::Region) + isequal(left.tag, right.tag) && + isequal(left.primitive, right.primitive) && + isequal(left.material, right.material) +end + +function Base.hash(region::Region, h::UInt) + hash((:Region, region.tag, region.primitive, region.material), h) +end + +""" +$(TYPEDEF) + +Pair one physical region with its resolved primitive and retained terminal, with resolved declarations for placement and paths. + +Each placement retains its physical scope through the existing owner type. +`Assembly` placements preserve independent terminals and are not strand courses. +An enclosing `Group` coalesces those placements into its own terminal scope. +Member indices, placement poses and longitudinal paths remain unchanged. + +$(TYPEDFIELDS) +""" +struct PlacedRegion{ + R <: Region, + S <: AbstractShape, + P <: NamedTuple, + H <: Tuple +} + "Authoritative homogeneous physical region." + source::R + "Resolved cross-sectional primitive with its absolute pose." + primitive::S + "Retained electrical terminal, or `nothing` for a nonconductive region." + terminal::Union{Nothing, Symbol} + "Ordered `(owner, pattern, member, pose)` placements. `Group` coalesces terminals, `Assembly` preserves them." + placement::P + "Ordered `(path, radius)` declarations traversed by the region." + paths::H + + function PlacedRegion( + source::R, + primitive::S, + terminal::Union{Nothing, Symbol}, + placement::P, + paths::H + ) where { + R <: Region, + S <: AbstractShape, + P <: NamedTuple, + H <: Tuple + } + terminal === nothing || !isempty(String(terminal)) || + throw( + ArgumentError("placed-region terminal cannot be empty") + ) + keys(placement) == (:patterns,) || throw(ArgumentError( + "placed-region placement must contain patterns" + )) + placement.patterns isa Tuple || throw(ArgumentError( + "placed-region patterns must be a tuple" + )) + all(placement.patterns) do entry + entry isa NamedTuple && keys(entry) == (:owner, :pattern, :member, :pose) && + entry.owner isa Type && entry.owner <: AbstractCablePart && + entry.member isa Integer && entry.member > 0 && + entry.pose isa Pose2 + end || throw(ArgumentError( + "placed-region patterns must contain a physical owner type, positive member indices and poses" + )) + all(paths) do entry + entry isa NamedTuple && keys(entry) == (:path, :radius) && + entry.radius isa Real && isfinite(entry.radius) && entry.radius >= 0 + end || throw(ArgumentError( + "placed-region paths must contain finite nonnegative radii" + )) + return new{R, S, P, H}(source, primitive, terminal, placement, paths) + end +end + +function Base.:(==)(left::PlacedRegion, right::PlacedRegion) + left.source == right.source && left.primitive == right.primitive && + left.terminal == right.terminal && left.placement == right.placement && + left.paths == right.paths +end + +function PlacedRegion(source::Region, primitive::AbstractShape) + return PlacedRegion( + source, + primitive, + nothing, + (patterns = (),), + () + ) +end + +boundary(region::PlacedRegion) = boundary(region.primitive) +area(region::PlacedRegion) = area(region.primitive) +centroid(region::PlacedRegion) = centroid(region.primitive) +support(region::PlacedRegion, φ::Real) = support(region.primitive, φ) + +function resolve(context::Union{EmptyBoundary, AbstractShape}, region::Region) + primitive = resolve(context, region.primitive) + placed = PlacedRegion(region, primitive) + return CableGeometry(PlacedRegion[placed], boundary(placed)) +end diff --git a/src/datamodel/design/stack.jl b/src/datamodel/design/stack.jl new file mode 100644 index 000000000..3fb4eb6c4 --- /dev/null +++ b/src/datamodel/design/stack.jl @@ -0,0 +1,78 @@ +""" +$(TYPEDEF) + +Store an ordered outward composition of cable parts. + +$(TYPEDFIELDS) +""" +struct Stack{V <: AbstractVector} <: AbstractCablePart + "Physical members in inner-to-outer order." + items::V + + function Stack(items::V) where {V <: AbstractVector} + isempty(items) && throw(ArgumentError("a stack requires at least one item")) + all(item -> item isa AbstractCablePart, items) || + throw(ArgumentError("stack items must be cable parts")) + return new{V}(copy(items)) + end +end + +Stack(item::AbstractCablePart) = Stack(AbstractCablePart[item]) +Stack(items::AbstractCablePart...) = Stack(AbstractCablePart[items...]) + +Base.:(==)(left::Stack, right::Stack) = left.items == right.items + +""" +$(TYPEDEF) + +Store the resolved physical regions and outer boundary of one cable root. + +$(TYPEDFIELDS) +""" +struct CableGeometry{V <: AbstractVector, B <: AbstractShape} + "Resolved regions in physical traversal order." + regions::V + "Resolved outer boundary of the complete root." + outer::B + + function CableGeometry(regions::V, outer::B) where { + V <: AbstractVector, + B <: AbstractShape + } + isempty(regions) && throw(ArgumentError( + "cable geometry requires at least one placed region" + )) + all(region -> region isa PlacedRegion, regions) || throw(ArgumentError( + "cable geometry regions must be PlacedRegion values" + )) + all(regions) do region + region_area = area(region) + center = centroid(region) + region_area isa Real && isfinite(region_area) && region_area > 0 && + all(isfinite, center) + end || throw(ArgumentError( + "cable geometry requires finite, positive-area placed regions" + )) + extent = support(outer) + extent isa Real && isfinite(extent) && extent > 0 || throw(ArgumentError( + "cable geometry requires a finite positive outer extent" + )) + return new{V, B}(regions, outer) + end +end + +boundary(geometry::CableGeometry) = geometry.outer + +Base.:(==)(left::CableGeometry, right::CableGeometry) = + left.regions == right.regions && left.outer == right.outer + +function resolve(context::Union{EmptyBoundary, AbstractShape}, stack::Stack) + regions = PlacedRegion[] + state = context + for item in stack.items + result = resolve(state, item) + append!(regions, result.regions) + state = boundary(result) + end + return CableGeometry(regions, state) +end diff --git a/src/datamodel/flatten.jl b/src/datamodel/flatten.jl new file mode 100644 index 000000000..699a2b467 --- /dev/null +++ b/src/datamodel/flatten.jl @@ -0,0 +1,1648 @@ +""" +$(TYPEDSIGNATURES) + +Apply the multiplicative overlength of every longitudinal path to conductor +resistance \\[Ω/m\\]. An empty path tuple leaves the resistance unchanged. +""" +function path_corrected_resistance(resistance::T, ::Tuple{}) where {T} + resistance +end + +function path_corrected_resistance(resistance::T, paths::Tuple) where {T} + inner = path_corrected_resistance(resistance, Base.front(paths)) + entry = last(paths) + return inner * convert(T, overlength(entry.path, entry.radius)) +end + +""" +$(TYPEDSIGNATURES) + +Add the turns per unit length contributed by a tuple of longitudinal paths +\\[1/m\\]. +""" +function turns_per_length(paths::Tuple, ::Type{T}) where {T <: Real} + return sum( + entry -> inv(pitch(entry.path, entry.radius)), + paths; + init = zero(T) + ) +end + +""" +$(TYPEDSIGNATURES) + +Calculate the geometric mean distance between two conductor-zone element sets +\\[m\\]. Coincident element centers use the greater element radius as their +finite self-distance. Element areas weight heterogeneous zones by their +cross-sectional participation. + +# Arguments + +- `left_coordinates`: element centers of the accumulated zone \\[m\\]. +- `left_radius`: element radius of the accumulated zone \\[m\\]. +- `right_coordinates`: element centers of the added zone \\[m\\]. +- `right_radius`: element radius of the added zone \\[m\\]. +- `left_element_area`: area represented by each accumulated element \\[m²\\]. +- `right_element_area`: area represented by each added element \\[m²\\]. + +# Returns + +- Area-weighted geometric mean distance \\[m\\]. +""" +function geometric_mean_distance( + left_coordinates, + left_radius::Real, + right_coordinates, + right_radius::Real, + left_element_area::Real = one(float(left_radius)), + right_element_area::Real = one(float(right_radius)) +) + isempty(left_coordinates) && throw(ArgumentError( + "the first conductor zone requires at least one element coordinate" + )) + isempty(right_coordinates) && throw(ArgumentError( + "the second conductor zone requires at least one element coordinate" + )) + logarithmic_sum, weights = promote( + zero(float(left_radius)), + zero(float(right_radius)) + ) + weight = left_element_area * right_element_area + weight > zero(weight) || throw(DomainError( + weight, "conductor-zone element areas must be positive" + )) + for left in left_coordinates, right in right_coordinates + + distance = hypot(left[1] - right[1], left[2] - right[2]) + effective_distance = iszero(distance) ? max(left_radius, right_radius) : distance + effective_distance > zero(effective_distance) || throw(DomainError( + effective_distance, "conductor-zone distance must be positive" + )) + logarithmic_sum += weight * log(effective_distance) + weights += weight + end + return exp(logarithmic_sum / weights) +end + +function geometric_mean_distance( + left_coordinates, + left_radii::AbstractVector, + right_coordinates, + right_radii::AbstractVector, + left_element_areas::AbstractVector, + right_element_areas::AbstractVector +) + isempty(left_coordinates) && throw(ArgumentError( + "the first conductor zone requires at least one element coordinate" + )) + isempty(right_coordinates) && throw(ArgumentError( + "the second conductor zone requires at least one element coordinate" + )) + length(left_coordinates) == length(left_radii) == length(left_element_areas) || + throw(DimensionMismatch( + "first conductor coordinates, radii, and areas must have equal lengths" + )) + length(right_coordinates) == length(right_radii) == length(right_element_areas) || + throw(DimensionMismatch( + "second conductor coordinates, radii, and areas must have equal lengths" + )) + logarithmic_sum, + weights = promote( + zero(float(first(left_radii))), + zero(float(first(right_radii))) + ) + for left in eachindex(left_coordinates), right in eachindex(right_coordinates) + + weight = left_element_areas[left] * right_element_areas[right] + weight > zero(weight) || throw(DomainError( + weight, "conductor-zone element areas must be positive" + )) + distance = hypot( + left_coordinates[left][1] - right_coordinates[right][1], + left_coordinates[left][2] - right_coordinates[right][2] + ) + effective_distance = iszero(distance) ? + max(left_radii[left], right_radii[right]) : distance + effective_distance > zero(effective_distance) || throw(DomainError( + effective_distance, "conductor-zone distance must be positive" + )) + logarithmic_sum += weight * log(effective_distance) + weights += weight + end + return exp(logarithmic_sum / weights) +end + +""" +$(TYPEDSIGNATURES) + +Calculate the equivalent radial permeability of dielectric layers and apply +the helical-solenoid correction associated with the enclosed conductor. + +# Arguments + +- `layers`: ordered dielectric layers with radii \\[m\\] and material + permeability \\[dimensionless\\]. +- `turns`: equivalent conductor turns per unit length \\[1/m\\]. +- `conductor_radius`: outer conductor radius \\[m\\]. +- `dielectric_radius`: outer dielectric radius \\[m\\]. + +# Returns + +- Corrected relative permeability \\[dimensionless\\]. +""" +function equivalent_dielectric_permeability( + layers, + turns::Real, + conductor_radius::Real, + dielectric_radius::Real +) + isempty(layers) && throw(ArgumentError( + "equivalent dielectric permeability requires at least one layer" + )) + weighted = sum(layers) do layer + layer.material.mu_r * log(layer.r_ex / layer.r_in) + end + radial = weighted / log(dielectric_radius / conductor_radius) + return radial * solenoid_factor( + turns, + conductor_radius, + dielectric_radius + ) +end + +""" +$(TYPEDSIGNATURES) + +Reduce one geometrically uniform conductor zone to resistance, GMR, path, and +cross-sectional values. Primitive-specific methods preserve the implemented +disk, annulus, and equivalent-area cable-sector assumptions. +""" + +function conductor_zone( + definition::Disk, + primitive::Disk, + zone, + ::Type{T}, + expected_inner +) where {T <: Real} + source = first(zone) + material = convert(Material{T}, source.source.material) + radius = convert(T, primitive.r) + all(item -> isapprox( + area(item.primitive), + (one(T) * pi) * radius^2 + ), zone) || throw(ArgumentError( + "resolved disk strands do not preserve their declared area" + )) + coordinates = Tuple{T, T}[convert.(T, centroid(item.primitive)) + for item in zone] + center = conductor_zone_position(zone) + center_radius = hypot( + first(coordinates)[1] - center[1], + first(coordinates)[2] - center[2] + ) + circular_locus = all( + point -> isapprox( + hypot(point[1] - center[1], point[2] - center[2]), + center_radius + ), coordinates) + element_area = (one(T) * pi) * radius^2 + zone_area = length(zone) * element_area + zone_r_in, + zone_r_ex = if length(zone) == 1 && iszero(center_radius) + zero(T), radius + elseif !circular_locus + outer_radius = maximum(coordinates) do point + hypot(point[1] - center[1], point[2] - center[2]) + radius + end + expected_outer = expected_inner + 2radius + isapprox(outer_radius, expected_outer) || throw(ArgumentError( + "noncircular disk strand course must extend one strand diameter " * + "beyond the preceding course" + )) + expected_inner, outer_radius + else + declared_inner = center_radius - radius + isapprox(declared_inner, expected_inner) || throw(ArgumentError( + "disk strand layer does not begin at the preceding radial boundary" + )) + expected_inner, expected_inner + 2radius + end + patterned = any(entry -> entry.owner === Group, source.placement.patterns) + uniform_paths = all(item -> item.paths == source.paths, zone) + turns = uniform_paths ? turns_per_length(source.paths, T) : + sum(item -> turns_per_length(item.paths, T), zone) / length(zone) + base_resistance = tubular_resistance( + zero(T), + radius, + material.rho + ) + resistance = if uniform_paths + path_corrected_resistance(base_resistance, source.paths) / length(zone) + else + resistances = map(zone) do item + path_corrected_resistance(base_resistance, item.paths) + end + reduce(parallel, resistances) + end + gmr = patterned ? + (circular_locus ? + strand_gmr( + center_radius, + length(zone), + radius, + material.mu_r + ) : + strand_gmr(coordinates, radius, material.mu_r)) : + tubular_gmr(zone_r_ex, zone_r_in, material.mu_r) + return ( + r_in = zone_r_in, + r_ex = zone_r_ex, + area = zone_area, + wires = patterned ? length(zone) : 0, + turns = convert(T, turns), + resistance, + gmr, + element_radius = radius, + element_area, + coordinates, + position = center, + material, + pairwise_gmd = patterned + ) +end + +function conductor_zone( + definition::Annulus, + primitive::Annulus, + zone, + ::Type{T}, + expected_inner +) where {T <: Real} + length(zone) == 1 || throw(ArgumentError( + "flatten requires one region per annular conductor layer" + )) + source = only(zone) + material = convert(Material{T}, source.source.material) + center = (convert(T, primitive.at.x), convert(T, primitive.at.y)) + zone_r_in = convert(T, primitive.ri) + zone_r_ex = convert(T, primitive.ro) + zone_area = (one(T) * pi) * (zone_r_ex^2 - zone_r_in^2) + isapprox(area(source.primitive), zone_area) || throw(ArgumentError( + "resolved annular conductor does not preserve its declared area" + )) + turns = turns_per_length(source.paths, T) + resistance = path_corrected_resistance( + material.rho / zone_area, + source.paths + ) + return ( + r_in = zone_r_in, + r_ex = zone_r_ex, + area = zone_area, + wires = isempty(source.paths) ? 0 : 1, + turns = convert(T, turns), + resistance, + gmr = tubular_gmr(zone_r_ex, zone_r_in, material.mu_r), + element_radius = zone_r_ex, + element_area = zone_area, + coordinates = Tuple{T, T}[center], + position = center, + material, + pairwise_gmd = false + ) +end + +function conductor_zone( + definition::Shell, + primitive::Annulus, + zone, + ::Type{T}, + expected_inner +) where {T <: Real} + length(zone) == 1 || throw(ArgumentError( + "flatten requires one region per contextual conductor shell" + )) + source = only(zone) + material = convert(Material{T}, source.source.material) + center = (convert(T, primitive.at.x), convert(T, primitive.at.y)) + zone_r_in = convert(T, primitive.ri) + zone_r_ex = convert(T, primitive.ro) + isapprox(zone_r_in, expected_inner) || throw(ArgumentError( + "resolved conductor shell does not begin at the preceding radial boundary" + )) + isapprox(zone_r_ex - zone_r_in, definition.t) || throw(ArgumentError( + "resolved conductor shell does not preserve its declared thickness" + )) + zone_area = (one(T) * pi) * (zone_r_ex^2 - zone_r_in^2) + turns = turns_per_length(source.paths, T) + resistance = path_corrected_resistance( + material.rho / zone_area, + source.paths + ) + return ( + r_in = zone_r_in, + r_ex = zone_r_ex, + area = zone_area, + wires = isempty(source.paths) ? 0 : 1, + turns = convert(T, turns), + resistance, + gmr = tubular_gmr(zone_r_ex, zone_r_in, material.mu_r), + element_radius = zone_r_ex, + element_area = zone_area, + coordinates = Tuple{T, T}[center], + position = center, + material, + pairwise_gmd = false + ) +end + +function conductor_zone( + definition::Sector, + primitive::SectorShape, + zone, + ::Type{T}, + expected_inner +) where {T <: Real} + length(zone) == 1 || throw(ArgumentError( + "flatten requires one region per sector conductor" + )) + source = only(zone) + isempty(source.paths) || throw(ArgumentError( + "flatten does not support a helical sector conductor" + )) + material = convert(Material{T}, source.source.material) + zone_area = convert(T, area(primitive)) + equivalent_radius = sqrt(zone_area / (one(T) * pi)) + center = convert.(T, centroid(primitive)) + return ( + r_in = zero(T), + r_ex = equivalent_radius, + area = zone_area, + wires = 0, + turns = zero(T), + resistance = material.rho / zone_area, + gmr = equivalent_radius * exp(-material.mu_r / 4), + element_radius = equivalent_radius, + element_area = zone_area, + coordinates = Tuple{T, T}[center], + position = center, + material, + pairwise_gmd = false + ) +end + +function conductor_zone( + definition::AbstractPrimitive, + primitive::AbstractShape, + zone, + ::Type, + expected_inner +) + throw(ArgumentError( + "flatten does not support conductor source/resolved pair " * + "$(nameof(typeof(definition))) → $(nameof(typeof(primitive)))" + )) +end + +""" +$(TYPEDSIGNATURES) + +Reduce one homogeneous dielectric region to concentric radii and material +properties. Primitive-specific methods retain circular and equivalent-area +cable-sector assumptions. +""" +function dielectric_layer( + primitive::Annulus, + source::PlacedRegion, + ::Type{T} +) where {T <: Real} + isempty(source.paths) || throw(ArgumentError( + "flatten does not support helical dielectric layers" + )) + center = (convert(T, primitive.at.x), convert(T, primitive.at.y)) + material = source.source.material + material.kind === :conductor && throw(ArgumentError( + "a dielectric interval cannot contain a conductor material" + )) + return ( + r_in = convert(T, r_in(source.primitive)), + r_ex = convert(T, r_ex(source.primitive)), + position = center, + material + ) +end + +function dielectric_layer( + primitive::ShellShape{ + <:Any, + <:SectorShape, + <:SectorShape + }, + source::PlacedRegion, + ::Type{T} +) where {T <: Real} + isempty(source.paths) || throw(ArgumentError( + "flatten does not support a helical sector dielectric" + )) + material = source.source.material + material.kind === :conductor && throw(ArgumentError( + "a dielectric interval cannot contain a conductor material" + )) + inner_area = convert(T, area(primitive.inner)) + outer_area = convert(T, area(primitive.outer)) + inner_radius = sqrt(inner_area / (one(T) * pi)) + outer_radius = sqrt(outer_area / (one(T) * pi)) + center = convert.(T, centroid(primitive.inner)) + return ( + r_in = inner_radius, + r_ex = outer_radius, + position = center, + material + ) +end + +function dielectric_layer( + primitive::AbstractShape, + source::PlacedRegion, + ::Type +) + throw(ArgumentError( + "flatten does not support dielectric primitive " * + "$(nameof(typeof(primitive)))" + )) +end + +""" +$(TYPEDSIGNATURES) + +Reduce a nested repeated-wire conductor to parallel resistance, +current-share-weighted GMR, equivalent temperature coefficient, turns, and +radial extent. +""" +function member_resistance(definition::Disk, material) + return tubular_resistance(zero(definition.r), definition.r, material.rho) +end + +function member_resistance(definition::Rectangle, material) + return strip_resistance(definition.h, definition.w, material.rho) +end + +function radial_extent(shape::Disk, center) + return hypot(shape.at.x - center[1], shape.at.y - center[2]) + shape.r +end + +function radial_extent(shape::Annulus, center) + return hypot(shape.at.x - center[1], shape.at.y - center[2]) + shape.ro +end + +function radial_extent(shape::Polygon, center) + cosine = cos(shape.at.φ) + sine = sin(shape.at.φ) + return maximum(shape.points) do point + x = shape.at.x + cosine * point[1] - sine * point[2] + y = shape.at.y + sine * point[1] + cosine * point[2] + hypot(x - center[1], y - center[2]) + end +end + +function radial_extent(shape::BentStrip, center) + dx = shape.at.x - center[1] + dy = shape.at.y - center[2] + offset = hypot(dx, dy) + iszero(offset) && return shape.ro + direction = atan(dy, dx) + period = oftype(direction, 2pi) + relative = mod(direction - shape.at.φ + pi, period) - pi + nearest = clamp(relative, -shape.span / 2, shape.span / 2) + cosine = cos(relative - nearest) + return sqrt(max( + offset^2 + shape.ri^2 + 2offset * shape.ri * cosine, + offset^2 + shape.ro^2 + 2offset * shape.ro * cosine + )) +end + +function radial_extent(shape::Rectangle, center) + cosine = cos(shape.at.φ) + sine = sin(shape.at.φ) + half_width = shape.w / 2 + half_height = shape.h / 2 + return maximum( + ((-half_width, -half_height), (half_width, -half_height), + (half_width, half_height), (-half_width, half_height)) + ) do point + x = shape.at.x + cosine * point[1] - sine * point[2] + y = shape.at.y + sine * point[1] + cosine * point[2] + hypot(x - center[1], y - center[2]) + end +end + +""" +$(TYPEDSIGNATURES) + +Calculate the mutual GMD of two resolved strand sections \\[m\\]. +General shapes retain the centroid-distance approximation. An annulus +surrounding the other section uses the uniform-current area average from +Rosa [rosa1908](@cite), Eq. (57): + +```math +\\log D = \\frac{r_o^2\\log r_o-r_i^2\\log r_i}{r_o^2-r_i^2}-\\frac12. +``` + +# Arguments + +- `left`, `right`: resolved strand cross-sections, with coordinates in \\[m\\]. + +# Returns + +- Mutual geometric mean distance \\[m\\]. + +# Notes + +The annular expression is independent of the enclosed section's shape or +eccentricity while that section remains entirely inside the annular hole. +It does not replace the separate annular self-GMR or imply an exact integral +for arbitrary deformed wire strands. + +# Errors + +- `ArgumentError`: coincident centroids without an enclosing annular section. +""" +function geometric_mean_distance(left::AbstractShape, right::AbstractShape) + left_center, right_center = centroid(left), centroid(right) + distance = hypot(left_center[1] - right_center[1], left_center[2] - right_center[2]) + distance > zero(distance) || throw(ArgumentError( + "nested conductor members must have distinct centroids unless one is an enclosing annulus" + )) + return distance +end + +function geometric_mean_distance(left::Annulus, right::AbstractShape) + tolerance = 64eps(float(nominal(left.ro))) + if left.ri > zero(left.ri) && radial_extent(right, centroid(left)) <= left.ri + tolerance + # A dimensionless ratio of radii avoids subtraction of nearly equal + # squared radii in Rosa's area average for a thin annulus. + ratio = (left.ro - left.ri) / left.ri + return left.ri * exp(log1p(ratio) * (1 + ratio)^2 / (ratio * (2 + ratio)) - one(ratio) / 2) + end + return invoke(geometric_mean_distance, Tuple{AbstractShape, AbstractShape}, left, right) +end + +geometric_mean_distance(left::AbstractShape, right::Annulus) = + geometric_mean_distance(right, left) + +function geometric_mean_distance(left::Annulus, right::Annulus) + outer, inner = left.ro >= right.ro ? (left, right) : (right, left) + return invoke(geometric_mean_distance, Tuple{Annulus, AbstractShape}, outer, inner) +end + +function nested_conductor_zone( + sources, + ::Type{T}, + expected_inner::T +) where {T <: Real} + isempty(sources) && throw(ArgumentError( + "a nested conductor requires at least one resolved primitive" + )) + all(sources) do source + source.source.primitive isa Union{Disk, Rectangle} && + source.primitive isa Union{Disk, Rectangle, Polygon, BentStrip, Annulus} + end || throw(ArgumentError( + "nested conductor flattening requires Disk or Rectangle source members " * + "resolved as Disk, Rectangle, Polygon, BentStrip, or Annulus geometry" + )) + + materials = Material{T}[convert(Material{T}, source.source.material) + for source in sources] + reference = first(materials).T0 + all(material -> isapprox(material.T0, reference), materials) || throw( + ArgumentError("all nested conductor materials must share one reference temperature") + ) + + areas = T[convert(T, area(source.source.primitive)) for source in sources] + all(eachindex(sources)) do index + isapprox( + convert(T, area(sources[index].primitive)), + areas[index]; + rtol = 5.0e-6, + atol = zero(T) + ) + end || throw(ArgumentError( + "resolved compacted strands must preserve every declared source area" + )) + equivalent_radii = sqrt.(areas ./ (one(T) * π)) + total_area = sum(areas) + center = convert.(T, conductor_zone_position(sources)) + resistances = T[path_corrected_resistance( + member_resistance( + sources[index].source.primitive, + materials[index] + ), + sources[index].paths + ) for index in eachindex(sources)] + + resistance = first(resistances) + alpha = first(materials).alpha + for index in Iterators.drop(eachindex(sources), 1) + alpha = equivalent_alpha( + alpha, + resistance, + materials[index].alpha, + resistances[index] + ) + resistance = parallel(resistance, resistances[index]) + end + + conductances = inv.(resistances) + weights = conductances ./ sum(conductances) + log_gmr = zero(T) + for left in eachindex(sources) + primitive = sources[left].primitive + self_gmr = primitive isa Annulus ? + tubular_gmr(primitive.ro, primitive.ri, materials[left].mu_r) : + equivalent_radii[left] * exp(-materials[left].mu_r / 4) + log_gmr += weights[left]^2 * log(self_gmr) + for right in (left + 1):length(sources) + distance = geometric_mean_distance(sources[left].primitive, sources[right].primitive) + log_gmr += 2weights[left] * weights[right] * log(distance) + end + end + + turn_values = T[turns_per_length(source.paths, T) for source in sources] + turns = sum(weights .* turn_values) + bounded = map(sources) do source + index = findfirst( + entry -> entry.pattern isa BoundedPlacement, + source.placement.patterns + ) + index === nothing && return nothing + any(entry -> entry.owner === Group, source.placement.patterns[(index + 1):end]) && + return nothing + return source.placement.patterns[index].pattern.boundary + end + outer = if all(!isnothing, bounded) + envelope = first(bounded) + all(==(envelope), bounded) || throw(ArgumentError( + "one nested conductor cannot combine different bounded formations" + )) + iszero(expected_inner) || throw(ArgumentError( + "a bounded stranded formation must be the innermost conductor" + )) + sqrt(convert(T, area(envelope)) / (one(T) * π)) + else + maximum( + index -> radial_extent(sources[index].primitive, center), + eachindex(sources) + ) + end + outer > expected_inner || throw(ArgumentError( + "nested conductor envelope must exceed its preceding radial boundary" + )) + return ( + r_in = expected_inner, + r_ex = outer, + cross_section = total_area, + num_wires = length(sources), + turns_per_length = turns, + resistance, + alpha, + gmr = exp(log_gmr), + reference_temperature = reference, + position = center + ) +end + +""" +$(TYPEDSIGNATURES) + +Return the transverse position \\[m\\] used for a radial shape or placed region. +Disks and annuli use their stored center. Sectors use their centroid, and sector +shells use the inner sector's centroid. A placed region uses its primitive. +""" +radial_position(shape::Union{Disk, Annulus}) = (shape.at.x, shape.at.y) +radial_position(shape::SectorShape) = centroid(shape) +function radial_position( + shape::ShellShape{ + <:Any, + <:SectorShape, + <:SectorShape +} +) + centroid(shape.inner) +end +radial_position(region::PlacedRegion) = radial_position(region.primitive) + +""" +$(TYPEDSIGNATURES) + +Return the transverse center \\[m\\] of a nonempty collection of resolved +conductor regions. Use a shared bounded-placement or group center when present. +Otherwise, use the primitive centroid for one region or the area-weighted +centroid for several regions. +""" +function conductor_zone_position(sources) + isempty(sources) && throw(ArgumentError( + "a conductor zone requires at least one resolved region" + )) + bounded = map(sources) do source + index = findfirst( + entry -> entry.pattern isa BoundedPlacement, + source.placement.patterns + ) + index === nothing && return nothing + shape = source.placement.patterns[index].pattern.boundary + return shape isa Disk ? (shape.at.x, shape.at.y) : centroid(shape) + end + if all(!isnothing, bounded) + reference = first(bounded) + all(position -> same_radial_position(position, reference), bounded) && + return reference + end + patterned = map(sources) do source + entries = filter(entry -> entry.owner === Group, source.placement.patterns) + length(entries) == 1 || return nothing + primitive = source.primitive + hasproperty(primitive, :at) || return nothing + intrinsic = source.source.primitive + hasproperty(intrinsic, :at) || return nothing + local_pose = only(entries).pose * intrinsic.at + absolute = primitive.at + parent_angle = absolute.φ - local_pose.φ + return ( + absolute.x - cos(parent_angle) * local_pose.x + + sin(parent_angle) * local_pose.y, + absolute.y - sin(parent_angle) * local_pose.x - + cos(parent_angle) * local_pose.y + ) + end + if all(!isnothing, patterned) + reference = first(patterned) + all(position -> same_radial_position(position, reference), patterned) && + return reference + end + length(sources) == 1 && return centroid(only(sources).primitive) + areas = map(source -> area(source.primitive), sources) + centers = map(source -> centroid(source.primitive), sources) + total = sum(areas) + return ( + sum(index -> areas[index] * centers[index][1], eachindex(sources)) / total, + sum(index -> areas[index] * centers[index][2], eachindex(sources)) / total + ) +end + +""" +$(TYPEDSIGNATURES) + +Compare two transverse coordinate pairs \\[m\\] using the existing radial +position tolerance. After promotion to floating type `T`, the Euclidean distance +must not exceed `sqrt(eps(T))` times the larger of one meter and the greatest +absolute coordinate. Return whether the positions satisfy this tolerance. +""" +function same_radial_position(left, right) + coordinates = promote( + float(left[1]), + float(left[2]), + float(right[1]), + float(right[2]) + ) + T = typeof(first(coordinates)) + scale = max(one(T), maximum(abs, coordinates)) + tolerance = sqrt(eps(T)) * scale + return hypot( + coordinates[1] - coordinates[3], + coordinates[2] - coordinates[4] + ) <= tolerance +end + +function same_radial_chain(left::PlacedRegion, right_position) + return same_radial_position(radial_position(left), right_position) +end + +""" +$(TYPEDSIGNATURES) + +Initialize the scalar conductor reduction from its innermost zone. +""" +function initialize_conductor(zone) + return ( + r_in = zone.r_in, + r_ex = zone.r_ex, + cross_section = zone.area, + num_wires = zone.wires, + turns_per_length = zone.turns, + resistance = zone.resistance, + alpha = zone.material.alpha, + gmr = zone.gmr, + reference_temperature = zone.material.T0, + position = zone.position, + coordinates = copy(zone.coordinates), + element_radii = fill(zone.element_radius, length(zone.coordinates)), + element_areas = fill(zone.element_area, length(zone.coordinates)), + previous_coordinates = zone.coordinates, + previous_radius = zone.element_radius, + previous_element_area = zone.element_area, + pairwise_gmd = zone.pairwise_gmd + ) +end + +""" +$(TYPEDSIGNATURES) + +Combine the strand-count-weighted turns per unit length of two conductor +zones \\[1/m\\]. A zone without explicit wires leaves the accumulated value +unchanged. +""" +function equivalent_turns_per_length( + num_wires::Integer, + turns_per_length::Real, + added_wires::Integer, + added_turns::Real +) + iszero(added_wires) && return turns_per_length + total_wires = num_wires + added_wires + return (num_wires * turns_per_length + added_wires * added_turns) / total_wires +end + +""" +$(TYPEDSIGNATURES) + +Add one concentric conductor zone to an accumulated scalar reduction. The +resistances combine in parallel, while GMR and temperature coefficient are +updated from the properties of the two zones. +""" +function add_conductor_zone(conductor, zone) + same_radial_position(zone.position, conductor.position) || throw(ArgumentError( + "conductor zones must share one radial center" + )) + isapprox(zone.r_in, conductor.r_ex) || throw(ArgumentError( + "conductor zones must be radially contiguous" + )) + isapprox(zone.material.T0, conductor.reference_temperature) || throw( + ArgumentError("all cable materials must share one reference temperature") + ) + pairwise_gmd = conductor.pairwise_gmd || zone.pairwise_gmd + distance = if pairwise_gmd + geometric_mean_distance( + conductor.coordinates, + conductor.element_radii, + zone.coordinates, + fill(zone.element_radius, length(zone.coordinates)), + conductor.element_areas, + fill(zone.element_area, length(zone.coordinates)) + ) + else + geometric_mean_distance( + conductor.previous_coordinates, + conductor.previous_radius, + zone.coordinates, + zone.element_radius, + conductor.previous_element_area, + zone.element_area + ) + end + return ( + r_in = conductor.r_in, + r_ex = zone.r_ex, + cross_section = conductor.cross_section + zone.area, + num_wires = conductor.num_wires + zone.wires, + turns_per_length = equivalent_turns_per_length( + conductor.num_wires, + conductor.turns_per_length, + zone.wires, + zone.turns + ), + resistance = parallel( + conductor.resistance, + zone.resistance + ), + alpha = equivalent_alpha( + conductor.alpha, + conductor.resistance, + zone.material.alpha, + zone.resistance + ), + gmr = equivalent_gmr( + conductor.gmr, + conductor.cross_section, + zone.gmr, + zone.area, + distance + ), + reference_temperature = conductor.reference_temperature, + position = conductor.position, + coordinates = append!(copy(conductor.coordinates), zone.coordinates), + element_radii = append!( + copy(conductor.element_radii), + fill(zone.element_radius, length(zone.coordinates)) + ), + element_areas = append!( + copy(conductor.element_areas), + fill(zone.element_area, length(zone.coordinates)) + ), + previous_coordinates = zone.coordinates, + previous_radius = zone.element_radius, + previous_element_area = zone.element_area, + pairwise_gmd + ) +end + +""" +$(TYPEDSIGNATURES) + +Convert a completed scalar conductor reduction into the homogeneous material +that reproduces its DC resistance and GMR. +""" +function equivalent_conductor_material(conductor) + return Material( + :conductor, + equivalent_rho( + conductor.resistance, + conductor.r_ex, + conductor.r_in + ), + zero(conductor.r_in), + equivalent_mu( + conductor.gmr, + conductor.r_ex, + conductor.r_in + ), + conductor.reference_temperature, + conductor.alpha + ) +end + +""" +$(TYPEDSIGNATURES) + +Calculate the shunt capacitance \\[F/m\\] and conductance \\[S/m\\] of one radial +dielectric layer at `angular_frequency` \\[rad/s\\]. Polarization loss tangent +excludes the conduction already represented by material resistivity. +""" +function dielectric_circuit(layer, angular_frequency::Real) + capacitance = shunt_capacitance(layer.r_in, layer.r_ex, layer.material.eps_r) + return ( + capacitance = capacitance, + conductance = shunt_conductance( + layer.r_in, + layer.r_ex, + layer.material.rho + ) + angular_frequency * capacitance * layer.material.tan_delta + ) +end + +function dielectric_circuit(layers, angular_frequency::Real, ::Type{T}) where {T <: Real} + isempty(layers) && return ( + capacitance = zero(T), + conductance = zero(T) + ) + combined = dielectric_circuit(first(layers), angular_frequency) + @inbounds for layer in Iterators.drop(layers, 1) + circuit = dielectric_circuit(layer, angular_frequency) + combined = series_shunt_admittance( + combined.conductance, + combined.capacitance, + circuit.conductance, + circuit.capacitance, + angular_frequency + ) + end + return combined +end + +""" +$(TYPEDSIGNATURES) + +Initialize the frequency-independent radial dielectric description from its +innermost layer. +""" +function initialize_dielectric(layer, conductor) + same_radial_position(layer.position, conductor.position) || throw(ArgumentError( + "dielectric layers must share the conductor radial center" + )) + isapprox(layer.r_in, conductor.r_ex) || throw(ArgumentError( + "the first dielectric layer must begin at the conductor boundary" + )) + isapprox(layer.material.T0, conductor.reference_temperature) || throw( + ArgumentError("all cable materials must share one reference temperature") + ) + return ( + r_in = layer.r_in, + r_ex = layer.r_ex, + cross_section = (one(layer.r_in) * pi) * + (layer.r_ex^2 - layer.r_in^2), + position = layer.position, + reference_temperature = layer.material.T0, + layers = dielectric_layer(layer.material, layer.r_in, layer.r_ex) + ) +end + +""" +$(TYPEDSIGNATURES) + +Add one radial dielectric layer to a frequency-independent radial description. +""" +function add_dielectric_layer(dielectric, layer) + same_radial_position(layer.position, dielectric.position) || throw(ArgumentError( + "dielectric layers must share the conductor radial center" + )) + isapprox(layer.r_in, dielectric.r_ex) || throw(ArgumentError( + "dielectric layers must be radially contiguous" + )) + isapprox(layer.material.T0, dielectric.reference_temperature) || throw( + ArgumentError("all cable materials must share one reference temperature") + ) + layers = copy(dielectric.layers) + append!(layers, dielectric_layer(layer.material, layer.r_in, layer.r_ex)) + return ( + r_in = dielectric.r_in, + r_ex = layer.r_ex, + cross_section = dielectric.cross_section + + (one(layer.r_in) * pi) * (layer.r_ex^2 - layer.r_in^2), + position = dielectric.position, + reference_temperature = dielectric.reference_temperature, + layers + ) +end + +function dielectric_layer(material::Material, inner::T, outer::T) where {T <: Real} + return [(r_in = inner, r_ex = outer, material = convert(Material{T}, material))] +end + +function dielectric_layer(material::RadialDielectric, inner::T, outer::T) where {T <: Real} + # Reconstruct virtual radial intervals only for computation. The homogeneous + # design still contains one physical shell, not these constituent surfaces. + total = convert(T, sum(material.weights)) + width = log(outer / inner) + radial_mu = sum(w * m.mu_r for (w, m) in zip(material.weights, material.materials)) / total + mu_scale = material.mu_r / radial_mu + accumulated = zero(T) + previous = inner + layers = NamedTuple{(:r_in, :r_ex, :material), Tuple{T, T, Material{T}}}[] + for index in eachindex(material.materials) + accumulated += convert(T, material.weights[index]) + next = index == lastindex(material.materials) ? outer : + inner * exp(width * accumulated / total) + source = material.materials[index] + physical = Material(source.kind, source.rho, source.eps_r, source.mu_r * mu_scale, + source.T0, source.alpha; rho_thermal=source.rho_thermal, + theta_max=source.theta_max, tan_delta=source.tan_delta, sigma_solar=source.sigma_solar) + push!(layers, (r_in = previous, r_ex = next, + material = convert(Material{T}, physical))) + previous = next + end + return layers +end + +function empty_dielectric(conductor, ::Type{T}) where {T <: Real} + Layer = NamedTuple{ + (:r_in, :r_ex, :material), + Tuple{T, T, Material{T}} + } + return ( + r_in = conductor.r_ex, + r_ex = conductor.r_ex, + cross_section = zero(T), + position = conductor.position, + reference_temperature = conductor.reference_temperature, + layers = Layer[] + ) +end + +""" +$(TYPEDSIGNATURES) + +Convert a completed scalar dielectric reduction into a homogeneous material +that matches its capacitance and conductance at `angular_frequency` \\[rad/s\\]. +Resistivity retains the series DC conduction. The remaining conductance is +represented by polarization loss tangent, including interfacial polarization +from heterogeneous layers. The equivalent is a reference-frequency export, +not a broadband replacement for the original physical layers. + +Relative permeability retains the radial material average and the conductor's +helical-solenoid correction. +""" +function equivalent_dielectric_material(dielectric, conductor, terminal::Symbol, + angular_frequency::Real) + isempty(dielectric.layers) && return Material( + :insulator, + oftype(dielectric.r_in, Inf), + zero(dielectric.r_in), + one(dielectric.r_in), + conductor.reference_temperature, + zero(dielectric.r_in) + ) + relative_mu = equivalent_dielectric_permeability( + dielectric.layers, + conductor.turns_per_length, + conductor.r_ex, + dielectric.r_ex + ) + isfinite(relative_mu) || throw(ArgumentError( + "dielectric reduction produced a non-finite permeability " * + "for terminal :$terminal (turns_per_length=$(conductor.turns_per_length), " * + "r_con=$(conductor.r_ex), r_ins=$(dielectric.r_ex))" + )) + dc_conductance = inv(sum(dielectric.layers) do layer + inv(shunt_conductance(layer.r_in, layer.r_ex, layer.material.rho)) + end) + polarization_conductance = dielectric.shunt_conductance - dc_conductance + tolerance = 100 * eps(typeof(float(nominal(dc_conductance)))) * + max(abs(dielectric.shunt_conductance), abs(dc_conductance)) + polarization_conductance >= -tolerance || throw(DomainError( + polarization_conductance, + "equivalent dielectric conductance must not be smaller than its DC limit")) + polarization_conductance = max(zero(polarization_conductance), polarization_conductance) + loss_tangent = iszero(polarization_conductance) ? zero(polarization_conductance) : + polarization_conductance / (angular_frequency * dielectric.shunt_capacitance) + return Material( + :insulator, + inv(equivalent_conductivity( + dc_conductance, + dielectric.r_in, + dielectric.r_ex + )), + equivalent_eps( + dielectric.shunt_capacitance, + dielectric.r_ex, + dielectric.r_in + ), + relative_mu, + conductor.reference_temperature, + zero(dielectric.r_in); + tan_delta = loss_tangent + ) +end + +function _radial_regions(regions) + radial = PlacedRegion[] + sizehint!(radial, length(regions)) + for source in regions + source.primitive isa DifferenceShape || begin + push!(radial, source) + continue + end + material = source.source.material + source.terminal === nothing || throw(ArgumentError( + "a non-radial enclosure fill cannot own a terminal" + )) + material.kind === :insulator || throw(ArgumentError( + "a non-radial enclosure fill must use an insulator material" + )) + isapprox(material.mu_r, one(material.mu_r)) || throw(ArgumentError( + "the coaxial reduction requires a nonmagnetic non-radial enclosure fill" + )) + end + return radial +end + +function same_path_definitions(left, right) + length(left) == length(right) || return false + return all(eachindex(left)) do index + left[index].path == right[index].path + end +end + +""" +$(TYPEDSIGNATURES) + +Reduce each radial terminal section of a completed cable design to equivalent +conductor geometry and its ordered physical dielectric layers. + +The reduction combines the resistance of conductors and GMR and retains each physical +dielectric layer for subsequent material evaluation. + +# Arguments + +- `design`: completed physical cable design. +# Returns + +An ordered vector of named tuples containing equivalent conductor fields and +the uncombined physical dielectric layers for each retained terminal. +""" +function radial_components(design::CableDesign, ::Type{T}) where {T <: Real} + regions = _radial_regions(design.geometry.regions) + terminals = design.terminal_order + isempty(terminals) && throw(ArgumentError( + "flatten requires at least one retained terminal" + )) + terminal_starts = Int[] + for terminal in terminals + index = findfirst(region -> region.terminal === terminal, regions) + index === nothing && throw(ArgumentError( + "terminal :$terminal has no resolved conductive region" + )) + push!(terminal_starts, index) + end + issorted(terminal_starts) && allunique(terminal_starts) || throw(ArgumentError( + "flatten requires nonreappearing radial terminal blocks" + )) + + Layer = NamedTuple{ + (:r_in, :r_ex, :material), + Tuple{T, T, Material{T}} + } + ConductorInput = NamedTuple{ + (:r_in, :r_ex, :cross_section, :num_wires, :turns_per_length, + :resistance, :alpha, :gmr, :position, :material), + Tuple{T, T, T, Int, T, T, T, T, Tuple{T, T}, Material{T}} + } + DielectricInput = NamedTuple{ + (:r_in, :r_ex, :cross_section, :layers), + Tuple{T, T, T, Vector{Layer}} + } + ComponentInput = NamedTuple{ + (:name, :conductor, :dielectric), + Tuple{Symbol, ConductorInput, DielectricInput} + } + effective = ComponentInput[] + + for (terminal_index, terminal) in enumerate(terminals) + first_index = terminal_starts[terminal_index] + last_index = terminal_index == length(terminals) ? length(regions) : + terminal_starts[terminal_index + 1] - 1 + block = @view regions[first_index:last_index] + + zone_ranges = UnitRange{Int}[] + cursor = firstindex(block) + while cursor <= lastindex(block) && block[cursor].terminal === terminal + start = cursor + source = block[cursor] + signature = map(entry -> entry.pattern, source.placement.patterns) + while cursor < lastindex(block) + next = block[cursor + 1] + next_signature = map(entry -> entry.pattern, next.placement.patterns) + same_zone = next.terminal === terminal && + next.source == source.source && + same_path_definitions(next.paths, source.paths) && + next_signature == signature + same_zone || break + cursor += 1 + end + push!(zone_ranges, start:cursor) + cursor += 1 + end + isempty(zone_ranges) && throw(ArgumentError( + "terminal :$terminal has no conductive zone" + )) + any(index -> block[index].terminal !== nothing, cursor:lastindex(block)) && + throw(ArgumentError( + "terminal :$terminal reappears after its dielectric interval" + )) + + conductor_sources = @view block[firstindex(block):(cursor - 1)] + nested = any(conductor_sources) do source + source.primitive isa Union{Polygon, BentStrip} || + source.source.primitive isa Rectangle || + any( + entry -> entry.pattern isa BoundedPlacement, + source.placement.patterns + ) || + count(entry -> entry.owner === Group, source.placement.patterns) > 1 || + length(source.paths) > 1 + end + conductor = if nested + zone_position = conductor_zone_position(conductor_sources) + expected_inner = if first_index > firstindex(regions) && + same_radial_chain(regions[first_index - 1], zone_position) + convert(T, r_ex(regions[first_index - 1].primitive)) + else + zero(T) + end + nested_conductor_zone( + conductor_sources, + T, + expected_inner + ) + else + accumulated = nothing + for (zone_index, indices) in enumerate(zone_ranges) + zone = @view block[indices] + source = first(zone) + source.source.material.kind === :conductor || throw(ArgumentError( + "terminal :$terminal contains a nonconductor physical region" + )) + all(item -> item.source.material == source.source.material, zone) || + throw(ArgumentError("one conductor zone must use one material")) + all(item -> item.source.primitive == source.source.primitive, zone) || + throw(ArgumentError("one conductor zone must use one primitive")) + zone_position = conductor_zone_position(zone) + expected_inner = if zone_index > 1 + accumulated.r_ex + elseif first_index > firstindex(regions) && + same_radial_chain(regions[first_index - 1], zone_position) + convert(T, r_ex(regions[first_index - 1].primitive)) + else + zero(T) + end + values = conductor_zone( + source.source.primitive, + source.primitive, + zone, + T, + expected_inner + ) + accumulated = zone_index == 1 ? + initialize_conductor(values) : + add_conductor_zone(accumulated, values) + end + accumulated + end + + dielectric_sources = cursor > lastindex(block) ? + @view(block[1:0]) : @view(block[cursor:lastindex(block)]) + if first_index > firstindex(regions) && + same_radial_chain(regions[first_index - 1], conductor.position) + preceding = regions[first_index - 1] + preceding.terminal === nothing || throw(ArgumentError( + "a radial terminal must be preceded by a dielectric region" + )) + preceding_layer = dielectric_layer( + preceding.primitive, + preceding, + T + ) + conductor = merge(conductor, (r_in = preceding_layer.r_ex,)) + end + if !isempty(dielectric_sources) + first_dielectric = first(dielectric_sources) + first_layer = dielectric_layer( + first_dielectric.primitive, + first_dielectric, + T + ) + conductor = merge(conductor, (r_ex = first_layer.r_in,)) + end + + dielectric = empty_dielectric(conductor, T) + for (layer_index, source) in enumerate(dielectric_sources) + source.terminal === nothing || throw(ArgumentError( + "a dielectric region cannot own a retained terminal" + )) + layer = dielectric_layer( + source.primitive, + source, + T + ) + dielectric = layer_index == 1 ? + initialize_dielectric(layer, conductor) : + add_dielectric_layer(dielectric, layer) + end + + conductor_material = equivalent_conductor_material(conductor) + push!(effective, + ( + name = terminal, + conductor = ( + r_in = conductor.r_in, + r_ex = conductor.r_ex, + cross_section = conductor.cross_section, + num_wires = conductor.num_wires, + turns_per_length = conductor.turns_per_length, + resistance = conductor.resistance, + alpha = conductor.alpha, + gmr = conductor.gmr, + position = conductor.position, + material = conductor_material + ), + dielectric = ( + r_in = dielectric.r_in, + r_ex = dielectric.r_ex, + cross_section = dielectric.cross_section, + layers = dielectric.layers + ) + )) + end + return effective +end + +function radial_components(design::CableDesign) + T = promote_type( + (eltype(region.primitive) for region in design.geometry.regions)..., + (eltype(region.source.material) for region in design.geometry.regions)... + ) + return radial_components(design, T) +end + +""" +$(TYPEDSIGNATURES) + +Reduce a completed cable design to equivalent radial conductor and dielectric +components at one dielectric reference frequency. + +This reference-frequency operation serves the scalar ATP and TRALIN export +adapters. It includes the supplied physical material losses at their reference +temperature and combines radial layers in series. The resulting scalar +material reproduces capacitance and conductance at the requested frequency. +Its validity is restricted to that frequency. Mutual coupling and earth +return are outside this reduction. Reusable [`homogenize`](@ref) designs retain their +constituents without calling this frequency-dependent operation. + +# Arguments + +- `design`: completed physical cable design. +- `dielectric_frequency`: reference frequency used to match the equivalent + dielectric [Hz]. + +# Returns + +- An ordered vector of equivalent radial component records. +""" +function flatten( + design::CableDesign, + dielectric_frequency::Real +) + T = promote_type( + typeof(float(dielectric_frequency)), + (eltype(region.primitive) for region in design.geometry.regions)..., + (eltype(region.source.material) for region in design.geometry.regions)... + ) + return flatten( + design, + convert(T, float(dielectric_frequency)), + T + ) +end + +function flatten( + design::CableDesign, + frequency::T, + ::Type{T} +) where {T <: Real} + frequency > zero(frequency) || throw(DomainError( + frequency, + "dielectric reference frequency must be positive" + )) + components = radial_components(design, T) + angular_frequency = 2 * (one(T) * pi) * frequency + Layer = NamedTuple{ + (:r_in, :r_ex, :material), + Tuple{T, T, Material{T}} + } + ConductorInput = typeof(first(components).conductor) + DielectricInput = NamedTuple{ + (:r_in, :r_ex, :cross_section, :shunt_capacitance, + :shunt_conductance, :frequency, :layers, :material), + Tuple{T, T, T, T, T, T, Vector{Layer}, Material{T}} + } + ComponentInput = NamedTuple{ + (:name, :conductor, :dielectric), + Tuple{Symbol, ConductorInput, DielectricInput} + } + effective = ComponentInput[] + sizehint!(effective, length(components)) + for component in components + circuit = dielectric_circuit( + component.dielectric.layers, + angular_frequency, + T + ) + dielectric = merge(component.dielectric, + ( + shunt_capacitance = circuit.capacitance, + shunt_conductance = circuit.conductance + )) + material = equivalent_dielectric_material( + dielectric, + merge(component.conductor, ( + reference_temperature = component.conductor.material.T0, + )), + component.name, + angular_frequency + ) + push!(effective, + ( + name = component.name, + conductor = component.conductor, + dielectric = ( + r_in = dielectric.r_in, + r_ex = dielectric.r_ex, + cross_section = dielectric.cross_section, + shunt_capacitance = circuit.capacitance, + shunt_conductance = circuit.conductance, + frequency, + layers = dielectric.layers, + material + ) + )) + end + return effective +end + +""" +$(TYPEDSIGNATURES) + +Build a homogeneous cable design from locally reduced radial components. + +Each retained terminal becomes one solid or annular conductor followed by one +homogeneous dielectric interval. The conductor material reproduces the +terminal's parallel resistance and geometric-mean radius. The dielectric +material retains the physical constituents and radial weights in a +[`RadialDielectric`](@ref). Constitutive laws are selected only during computation +or explicit reference-frequency export. The source design is not modified. + +# Arguments + +- `original`: completed physical cable design. + +# Keywords + +- `new_id`: identifier for the returned design. An empty value appends + `"_equivalent"` to the source identifier. + +# Returns + +- A completed homogeneous [`CableDesign`](@ref). +""" +function homogenize( + original::CableDesign; + new_id::AbstractString = "" +) + target_id = isempty(new_id) ? original.cable_id * "_equivalent" : String(new_id) + components = radial_components(original) + chains = Vector{typeof(components)}() + for component in components + if isempty(chains) || + !same_radial_position( + first(last(chains)).conductor.position, + component.conductor.position + ) + any( + chain -> same_radial_position( + first(chain).conductor.position, + component.conductor.position + ), + chains) && throw(ArgumentError( + "a radial assembly member cannot reappear after another member" + )) + push!(chains, eltype(components)[]) + end + push!(last(chains), component) + end + + flattened_members = AbstractCablePart[] + member_positions = Tuple[] + for chain in chains + parts = AbstractCablePart[] + for component in chain + terminal = component.name + conductor = component.conductor + conductor_primitive = iszero(conductor.r_in) ? + Disk(conductor.r_ex) : + Annulus(conductor.r_in, conductor.r_ex) + conductor_region = Region( + Symbol(terminal, :_equivalent_conductor), + conductor_primitive, + conductor.material + ) + push!(parts, + Group( + terminal, + Pose2(0, 0, 0), + conductor_region, + nothing, + nothing, + nothing + )) + + dielectric = component.dielectric + if dielectric.r_ex > dielectric.r_in + push!(parts, + Region( + Symbol(terminal, :_equivalent_dielectric), + Annulus(dielectric.r_in, dielectric.r_ex), + RadialDielectric( + [layer.material for layer in dielectric.layers], + [log(layer.r_ex / layer.r_in) for layer in dielectric.layers]; + mu_r = equivalent_dielectric_permeability(dielectric.layers, + conductor.turns_per_length, conductor.r_ex, dielectric.r_ex)) + )) + end + end + push!(flattened_members, Stack(parts)) + push!(member_positions, first(chain).conductor.position) + end + + origin = (zero(first(member_positions)[1]), zero(first(member_positions)[2])) + singleton = length(flattened_members) == 1 && + same_radial_position(only(member_positions), origin) + root = if singleton + only(flattened_members) + else + members = Tuple( + AssemblyMember( + member, + Pose2(position[1], position[2], 0) + ) + for (member, position) in zip(flattened_members, member_positions) + ) + Assembly( + Pose2(0, 0, 0), + members, + nothing, + nothing, + nothing, + nothing + ) + end + return build(CableDesign, target_id, root; nominal_data = original.nominal_data) +end diff --git a/src/datamodel/geometry.jl b/src/datamodel/geometry.jl new file mode 100644 index 000000000..fd2754b06 --- /dev/null +++ b/src/datamodel/geometry.jl @@ -0,0 +1,35 @@ +""" +Return three cable-center coordinates in a trefoil formation. +""" +function trefoil_formation(x0::Real, y0::Real, radius::Real) + radius > zero(radius) || throw(DomainError(radius, "radius must be positive")) + x, y, r = promote(float(x0), float(y0), float(radius)) + distance = r / cosd(30) + return ( + x, + y + distance, + x + distance * cosd(210), + y + distance * sind(210), + x + distance * cosd(330), + y + distance * sind(330) + ) +end + +""" +Return three cable-center coordinates in a horizontal or vertical flat formation. +""" +function flat_formation(x0::Real, y0::Real, spacing::Real; vertical::Bool = false) + spacing > zero(spacing) || + throw(DomainError(spacing, "spacing must be positive")) + x, y, distance = promote(float(x0), float(y0), float(spacing)) + return vertical ? + (x, y, x, y - distance, x, y - 2 * distance) : + (x, y, x + distance, y, x + 2 * distance, y) +end + +""" +Return the outer radius of a materialized cable design. +""" +function outer_radius(design::CableDesign) + return support(boundary(design.geometry)) +end diff --git a/src/datamodel/geometry/ellipse.jl b/src/datamodel/geometry/ellipse.jl new file mode 100644 index 000000000..0f3c5c710 --- /dev/null +++ b/src/datamodel/geometry/ellipse.jl @@ -0,0 +1,115 @@ +""" +$(TYPEDEF) + +Represent the exact outward parallel boundary of an ellipse. + +For ellipse support ``h(\\phi)`` and normal offset ``t``, the parallel boundary +has support + +```math +h_t(\\phi) = h(\\phi) + t. +``` + +`EllipseOffset` is resolved geometry produced by applying [`Shell`](@ref). + +$(TYPEDFIELDS) +""" +struct EllipseOffset{T <: Real, P <: Pose2{T}} <: AbstractShape{T} + "Original ellipse semi-axis along the local x-axis \\[m\\]." + a::T + "Original ellipse semi-axis along the local y-axis \\[m\\]." + b::T + "Outward normal offset \\[m\\]." + t::T + "Pose in the completed cable coordinate system." + at::P + + function EllipseOffset{T, P}(a::T, b::T, t::T, at::P) where { + T <: Real, P <: Pose2{T} + } + isfinite(a) && a > zero(a) || throw(DomainError( + a, "ellipse-offset semi-axis a must be positive and finite" + )) + isfinite(b) && b > zero(b) || throw(DomainError( + b, "ellipse-offset semi-axis b must be positive and finite" + )) + isfinite(t) && t > zero(t) || throw(DomainError( + t, "ellipse offset must be positive and finite" + )) + return new{T, P}(a, b, t, at) + end +end + +function EllipseOffset(ellipse::Ellipse, t::Real) + T = promote_type(eltype(ellipse), typeof(t)) + return EllipseOffset{T, Pose2{T}}( + convert(T, ellipse.a), + convert(T, ellipse.b), + convert(T, t), + convert(Pose2{T}, ellipse.at) + ) +end + +boundary(shape::EllipseOffset) = shape +area(shape::EllipseOffset) = + π * shape.a * shape.b + _ellipse_perimeter(shape.a, shape.b) * shape.t + + π * shape.t^2 +perimeter(shape::EllipseOffset) = + _ellipse_perimeter(shape.a, shape.b) + 2π * shape.t +centroid(shape::EllipseOffset) = (shape.at.x, shape.at.y) + +function support(shape::EllipseOffset, angle::Real) + local_angle = angle - shape.at.φ + return shape.at.x * cos(angle) + shape.at.y * sin(angle) + + hypot(shape.a * cos(local_angle), shape.b * sin(local_angle)) + shape.t +end + +support(shape::EllipseOffset) = + hypot(shape.at.x, shape.at.y) + max(shape.a, shape.b) + shape.t + +function resolve(at::Pose2, shape::EllipseOffset) + return EllipseOffset( + Ellipse(shape.a, shape.b, at * shape.at), + shape.t + ) +end + +function resolve(inner::Ellipse, layer::Shell) + return ShellShape(inner, EllipseOffset(inner, layer.t)) +end + +function resolve(inner::EllipseOffset, layer::Shell) + outer = EllipseOffset( + Ellipse(inner.a, inner.b, inner.at), + inner.t + layer.t + ) + return ShellShape(inner, outer) +end + +function tessellate(shape::Ellipse; points_per_arc::Integer = 128) + points_per_arc >= 8 || throw(ArgumentError( + "an ellipse requires at least eight boundary points" + )) + angles = range(zero(shape.a), oftype(shape.a, 2π); length = Int(points_per_arc)) + local_points = [ + (shape.a * cos(angle), shape.b * sin(angle)) for angle in angles + ] + return [shape.at(point) for point in local_points] +end + +function tessellate(shape::EllipseOffset; points_per_arc::Integer = 128) + points_per_arc >= 8 || throw(ArgumentError( + "an ellipse offset requires at least eight boundary points" + )) + angles = range(zero(shape.a), oftype(shape.a, 2π); length = Int(points_per_arc)) + local_points = map(angles) do angle + cosine = cos(angle) + sine = sin(angle) + normalizer = hypot(shape.b * cosine, shape.a * sine) + ( + shape.a * cosine + shape.t * shape.b * cosine / normalizer, + shape.b * sine + shape.t * shape.a * sine / normalizer + ) + end + return [shape.at(point) for point in local_points] +end diff --git a/src/datamodel/geometry/pose.jl b/src/datamodel/geometry/pose.jl new file mode 100644 index 000000000..af799b42d --- /dev/null +++ b/src/datamodel/geometry/pose.jl @@ -0,0 +1,71 @@ +""" +$(TYPEDEF) + +Represent planar translation and orientation relative to a parent frame. + +$(TYPEDFIELDS) +""" +struct Pose2{T <: Real} + "Horizontal translation \\[m\\]." + x::T + "Vertical translation \\[m\\]." + y::T + "Counter-clockwise orientation \\[rad\\]." + φ::T + + function Pose2{T}(x::T, y::T, φ::T) where {T <: Real} + all(isfinite, (x, y, φ)) || + throw(ArgumentError("pose coordinates and orientation must be finite")) + return new{T}(x, y, φ) + end +end + +function _pose2(x, y, φ) + values = map(float, promote(x, y, φ)) + return Pose2{typeof(first(values))}(values...) +end + +function Pose2(x, y, φ; combine::Symbol = :product) + return parameterize(Pose2, _pose2, (x, y, φ); combine) +end + +Pose2(x, y; combine::Symbol = :product) = Pose2(x, y, 0; combine) + +Base.eltype(::Pose2{T}) where {T} = T +Base.eltype(::Type{<:Pose2{T}}) where {T} = T + +function Base.convert(::Type{<:Pose2{T}}, pose::Pose2) where {T <: Real} + return Pose2{T}(convert(T, pose.x), convert(T, pose.y), convert(T, pose.φ)) +end + +""" +Compose `child` relative to `parent`. +""" +function Base.:*(parent::Pose2, child::Pose2) + values = map(float, promote( + parent.x, parent.y, parent.φ, child.x, child.y, child.φ + )) + px, py, pφ, cx, cy, cφ = values + return Pose2( + px + cos(pφ) * cx - sin(pφ) * cy, + py + sin(pφ) * cx + cos(pφ) * cy, + pφ + cφ + ) +end + +""" +$(TYPEDSIGNATURES) + +Rotate `point` by the orientation of `pose`, then translate it by its position. +""" +function (pose::Pose2)(point) + cosine = cos(pose.φ) + sine = sin(pose.φ) + return ( + pose.x + cosine * point[1] - sine * point[2], + pose.y + sine * point[1] + cosine * point[2] + ) +end + +Commons.input_fields(::Type{<:Pose2}) = (x=(name="horizontal position",unit="m"), + y=(name="vertical position",unit="m"),φ=(name="rotation",unit="rad")) diff --git a/src/datamodel/geometry/primitives.jl b/src/datamodel/geometry/primitives.jl new file mode 100644 index 000000000..73c55c3a6 --- /dev/null +++ b/src/datamodel/geometry/primitives.jl @@ -0,0 +1,474 @@ +""" +$(TYPEDEF) + +Supertype for exact cross-sectional geometry. + +Intrinsic primitives and derived domains such as conformal shells share this +interface. A shape is independent of any plotting tessellation. +""" +abstract type AbstractShape{T <: Real} end + +Base.eltype(::AbstractShape{T}) where {T} = T +Base.eltype(::Type{<:AbstractShape{T}}) where {T} = T + +""" +$(TYPEDEF) + +Supertype for intrinsic cross-sectional primitives. + +A primitive states material-neutral dimensions. During construction, simple +primitives also include the `Pose2` produced by coordinate composition. Shapes +that require additional exact contact geometry use a separate resolved +`AbstractShape` implementation. +""" +abstract type AbstractPrimitive{T <: Real} <: AbstractShape{T} end + +Base.eltype(::AbstractPrimitive{T}) where {T} = T +Base.eltype(::Type{<:AbstractPrimitive{T}}) where {T} = T + +""" +Explicit initial state for resolving the first member of an outward stack. +""" +struct EmptyBoundary end + +""" +$(TYPEDEF) + +Represent a circular cross-section and its composed pose. + +$(TYPEDFIELDS) +""" +struct Disk{T <: Real, P <: Pose2{T}} <: AbstractPrimitive{T} + "Radius \\[m\\]." + r::T + "Pose in the completed cable coordinate system." + at::P + + function Disk{T, P}(r::T, at::P) where {T <: Real, P <: Pose2{T}} + isfinite(r) && r > zero(r) || + throw(DomainError(r, "disk radius must be positive and finite")) + return new{T, P}(r, at) + end +end + +""" +$(TYPEDEF) + +Represent a rectangular cross-section and its composed pose. + +$(TYPEDFIELDS) +""" +struct Rectangle{T <: Real, P <: Pose2{T}} <: AbstractPrimitive{T} + "Width along the local x-axis \\[m\\]." + w::T + "Height along the local y-axis \\[m\\]." + h::T + "Pose in the completed cable coordinate system." + at::P + + function Rectangle{T, P}(w::T, h::T, at::P) where {T <: Real, P <: Pose2{T}} + isfinite(w) && w > zero(w) || + throw(DomainError(w, "rectangle width must be positive and finite")) + isfinite(h) && h > zero(h) || + throw(DomainError(h, "rectangle height must be positive and finite")) + return new{T, P}(w, h, at) + end +end + +""" +$(TYPEDEF) + +Represent an elliptical cross-section and its composed pose. + +$(TYPEDFIELDS) +""" +struct Ellipse{T <: Real, P <: Pose2{T}} <: AbstractPrimitive{T} + "Semi-axis along the local x-axis \\[m\\]." + a::T + "Semi-axis along the local y-axis \\[m\\]." + b::T + "Pose in the completed cable coordinate system." + at::P + + function Ellipse{T, P}(a::T, b::T, at::P) where {T <: Real, P <: Pose2{T}} + isfinite(a) && a > zero(a) || + throw(DomainError(a, "ellipse semi-axis a must be positive and finite")) + isfinite(b) && b > zero(b) || + throw(DomainError(b, "ellipse semi-axis b must be positive and finite")) + return new{T, P}(a, b, at) + end +end + +""" +$(TYPEDEF) + +Represent an annular cross-section and its composed pose. + +$(TYPEDFIELDS) +""" +struct Annulus{T <: Real, P <: Pose2{T}} <: AbstractPrimitive{T} + "Inner radius \\[m\\]." + ri::T + "Outer radius \\[m\\]." + ro::T + "Pose in the completed cable coordinate system." + at::P + + function Annulus{T, P}(ri::T, ro::T, at::P) where {T <: Real, P <: Pose2{T}} + isfinite(ri) && ri >= zero(ri) || throw(DomainError( + ri, "annulus inner radius must be nonnegative and finite" + )) + isfinite(ro) && ro > ri || throw(DomainError( + ro, "annulus outer radius must exceed its inner radius" + )) + return new{T, P}(ri, ro, at) + end +end + +""" +$(TYPEDEF) + +Represent a polygonal cross-section and its composed pose. + +$(TYPEDFIELDS) +""" +struct Polygon{T <: Real, P <: Pose2{T}} <: AbstractPrimitive{T} + "Ordered local `(x, y)` vertices \\[m\\]." + points::Vector{Tuple{T, T}} + "Pose in the completed cable coordinate system." + at::P + + function Polygon{T, P}( + points::Vector{Tuple{T, T}}, at::P + ) where {T <: Real, P <: Pose2{T}} + length(points) >= 3 || + throw(ArgumentError("a polygon requires at least three vertices")) + all(point -> length(point) == 2 && all(isfinite, point), points) || + throw(ArgumentError("polygon vertices must be finite coordinate pairs")) + twice_area = sum(eachindex(points)) do index + next = mod1(index + 1, length(points)) + points[index][1] * points[next][2] - + points[next][1] * points[index][2] + end + iszero(twice_area) && throw(DomainError( + twice_area, "polygon area must be nonzero" + )) + return new{T, P}(copy(points), at) + end +end + +Base.:(==)(left::Polygon, right::Polygon) = + left.points == right.points && left.at == right.at +Base.isequal(left::Polygon, right::Polygon) = + isequal(left.points, right.points) && isequal(left.at, right.at) +Base.hash(value::Polygon, seed::UInt) = hash(value.at, hash(value.points, seed)) + +""" +Store one exact outer shape with exact material-free holes removed from it. +""" +struct DifferenceShape{ + T <: Real, + O <: AbstractShape, + H <: Tuple, + P <: Pose2{T} +} <: AbstractShape{T} + outer::O + holes::H + at::P + + function DifferenceShape( + outer::O, + holes::H + ) where {O <: AbstractShape, H <: Tuple} + all(hole -> hole isa AbstractShape, holes) || throw(ArgumentError( + "difference holes must be exact resolved shapes" + )) + T = promote_type(eltype(outer), map(eltype, holes)...) + at = convert(Pose2{T}, outer.at) + return new{T, O, H, typeof(at)}(outer, holes, at) + end +end + +Base.:(==)(left::DifferenceShape, right::DifferenceShape) = + left.outer == right.outer && left.holes == right.holes && left.at == right.at +Base.isequal(left::DifferenceShape, right::DifferenceShape) = + isequal(left.outer, right.outer) && isequal(left.holes, right.holes) && + isequal(left.at, right.at) +Base.hash(value::DifferenceShape, seed::UInt) = + hash(value.at, hash(value.holes, hash(value.outer, seed))) + +function Disk(r::Real, at::Pose2) + T = promote_type(typeof(r), eltype(at)) + return Disk{T, Pose2{T}}(convert(T, r), convert(Pose2{T}, at)) +end +function Rectangle(w::Real, h::Real, at::Pose2) + T = promote_type(typeof(w), typeof(h), eltype(at)) + return Rectangle{T, Pose2{T}}( + convert(T, w), convert(T, h), convert(Pose2{T}, at)) +end +function Ellipse(a::Real, b::Real, at::Pose2) + T = promote_type(typeof(a), typeof(b), eltype(at)) + return Ellipse{T, Pose2{T}}( + convert(T, a), convert(T, b), convert(Pose2{T}, at)) +end +function Annulus(ri::Real, ro::Real, at::Pose2) + T = promote_type(typeof(ri), typeof(ro), eltype(at)) + return Annulus{T, Pose2{T}}( + convert(T, ri), convert(T, ro), convert(Pose2{T}, at)) +end +function _polygon(points, at::Pose2) + Base.@nospecialize points + isempty(points) && throw(ArgumentError("a polygon requires at least three vertices")) + T = foldl(points; init = eltype(at)) do current, point + promote_type(current, typeof(point[1]), typeof(point[2])) + end + vertices = Tuple{T, T}[ + (convert(T, point[1]), convert(T, point[2])) for point in points + ] + return Polygon{T, Pose2{T}}( + vertices, convert(Pose2{T}, at)) +end + +_origin(::Type{T}) where {T <: Real} = Pose2(zero(T), zero(T), zero(T)) + +Disk(r::T) where {T <: Real} = Disk(r, _origin(T)) +function Rectangle(w::Real, h::Real) + T = promote_type(typeof(w), typeof(h)) + return Rectangle(convert(T, w), convert(T, h), _origin(T)) +end +function Ellipse(a::Real, b::Real) + T = promote_type(typeof(a), typeof(b)) + return Ellipse(convert(T, a), convert(T, b), _origin(T)) +end +function Annulus(ri::Real, ro::Real) + T = promote_type(typeof(ri), typeof(ro)) + return Annulus(convert(T, ri), convert(T, ro), _origin(T)) +end +function Polygon(points::Union{Tuple, AbstractVector}) + Base.@nospecialize points + return _polygon(points, _origin(typeof(float(first(points)[1])))) +end + +function Base.convert( + ::Type{<:AbstractPrimitive{T}}, value::Disk +) where {T <: Real} + return Disk(convert(T, value.r), convert(Pose2{T}, value.at)) +end +function Base.convert( + ::Type{<:AbstractPrimitive{T}}, value::Rectangle +) where {T <: Real} + return Rectangle( + convert(T, value.w), convert(T, value.h), convert(Pose2{T}, value.at) + ) +end +function Base.convert( + ::Type{<:AbstractPrimitive{T}}, value::Ellipse +) where {T <: Real} + return Ellipse( + convert(T, value.a), convert(T, value.b), convert(Pose2{T}, value.at) + ) +end +function Base.convert( + ::Type{<:AbstractPrimitive{T}}, value::Annulus +) where {T <: Real} + return Annulus( + convert(T, value.ri), convert(T, value.ro), convert(Pose2{T}, value.at) + ) +end +function Base.convert( + ::Type{<:AbstractPrimitive{T}}, value::Polygon +) where {T <: Real} + points = map(point -> (convert(T, point[1]), convert(T, point[2])), value.points) + return _polygon(points, convert(Pose2{T}, value.at)) +end + +""" +Return the outer resolved geometric boundary of `primitive`. +""" +function boundary end + +""" +Return cross-sectional area \\[m²\\]. +""" +function area end + +""" +Return exact boundary perimeter in meters. +""" +function perimeter end + +""" +Return the absolute cross-sectional centroid as `(x, y)` \\[m\\]. +""" +function centroid end + +""" +Return the absolute directional support coordinate at angle `φ` \\[m\\]. +""" +function support end + +""" +Return the inner radius of radial geometry \\[m\\]. +""" +function r_in end + +""" +Return the outer radius of radial geometry \\[m\\]. +""" +function r_ex end + +""" +Return the radial thickness of radial geometry \\[m\\]. +""" +function thickness end + +@inline _local_centroid(::Union{Disk, Rectangle, Ellipse, Annulus}) = (0, 0) +function _local_centroid(primitive::Polygon) + signed_twice_area = zero(eltype(primitive)) + xmoment = zero(eltype(primitive)) + ymoment = zero(eltype(primitive)) + for index in eachindex(primitive.points) + next = mod1(index + 1, length(primitive.points)) + left = primitive.points[index] + right = primitive.points[next] + cross = left[1] * right[2] - right[1] * left[2] + signed_twice_area += cross + xmoment += (left[1] + right[1]) * cross + ymoment += (left[2] + right[2]) * cross + end + return ( + xmoment / (3 * signed_twice_area), + ymoment / (3 * signed_twice_area) + ) +end + +function centroid(primitive::AbstractPrimitive) + x, y = _local_centroid(primitive) + at = primitive.at + return ( + at.x + cos(at.φ) * x - sin(at.φ) * y, + at.y + sin(at.φ) * x + cos(at.φ) * y + ) +end + +_local_support(primitive::Disk, φ::Real) = primitive.r +_local_support(primitive::Annulus, φ::Real) = primitive.ro +function _local_support(primitive::Rectangle, φ::Real) + abs(cos(φ)) * primitive.w / 2 + abs(sin(φ)) * primitive.h / 2 +end +function _local_support(primitive::Ellipse, φ::Real) + hypot(primitive.a * cos(φ), primitive.b * sin(φ)) +end +function _local_support(primitive::Polygon, φ::Real) + maximum( + point[1] * cos(φ) + point[2] * sin(φ) for point in primitive.points + ) +end + +function support(primitive::AbstractPrimitive, φ::Real) + at = primitive.at + return at.x * cos(φ) + at.y * sin(φ) + + _local_support(primitive, φ - at.φ) +end + +_local_extent(primitive::Disk) = primitive.r +_local_extent(primitive::Annulus) = primitive.ro +_local_extent(primitive::Rectangle) = hypot(primitive.w, primitive.h) / 2 +_local_extent(primitive::Ellipse) = max(primitive.a, primitive.b) +_local_extent(primitive::Polygon) = maximum(hypot(point...) for point in primitive.points) +function support(primitive::AbstractPrimitive) + hypot(primitive.at.x, primitive.at.y) + + _local_extent(primitive) +end +function support(primitive::Polygon) + cosine = cos(primitive.at.φ) + sine = sin(primitive.at.φ) + return maximum(primitive.points) do point + x = primitive.at.x + cosine * point[1] - sine * point[2] + y = primitive.at.y + sine * point[1] + cosine * point[2] + hypot(x, y) + end +end + +boundary(primitive::Union{Disk, Rectangle, Ellipse, Polygon}) = primitive +boundary(primitive::Annulus) = Disk(primitive.ro, primitive.at) + +area(primitive::Disk) = π * primitive.r^2 +area(primitive::Rectangle) = primitive.w * primitive.h +area(primitive::Ellipse) = π * primitive.a * primitive.b +area(primitive::Annulus) = π * (primitive.ro^2 - primitive.ri^2) +function area(primitive::Polygon) + twice = sum(eachindex(primitive.points)) do index + next = mod1(index + 1, length(primitive.points)) + primitive.points[index][1] * primitive.points[next][2] - + primitive.points[next][1] * primitive.points[index][2] + end + return abs(twice) / 2 +end + +perimeter(primitive::Disk) = 2π * primitive.r +perimeter(primitive::Rectangle) = 2 * (primitive.w + primitive.h) +function _ellipse_perimeter(a, b) + major = max(a, b) + minor = min(a, b) + parameter = one(major) - (minor / major)^2 + return 4major * ellipe(parameter) +end +perimeter(primitive::Ellipse) = _ellipse_perimeter(primitive.a, primitive.b) +perimeter(primitive::Annulus) = 2π * (primitive.ro + primitive.ri) +function perimeter(primitive::Polygon) + return sum(eachindex(primitive.points)) do index + next = mod1(index + 1, length(primitive.points)) + hypot( + primitive.points[next][1] - primitive.points[index][1], + primitive.points[next][2] - primitive.points[index][2] + ) + end +end + +r_in(primitive::Disk) = zero(primitive.r) +r_in(primitive::Annulus) = primitive.ri +r_ex(primitive::Disk) = primitive.r +r_ex(primitive::Annulus) = primitive.ro +thickness(primitive::Disk) = primitive.r +thickness(primitive::Annulus) = primitive.ro - primitive.ri + +_with_pose(primitive::Disk, at::Pose2) = Disk(primitive.r, at) +_with_pose(primitive::Rectangle, at::Pose2) = Rectangle(primitive.w, primitive.h, at) +_with_pose(primitive::Ellipse, at::Pose2) = Ellipse(primitive.a, primitive.b, at) +_with_pose(primitive::Annulus, at::Pose2) = Annulus(primitive.ri, primitive.ro, at) +_with_pose(primitive::Polygon, at::Pose2) = _polygon(primitive.points, at) + +boundary(primitive::DifferenceShape) = boundary(primitive.outer) +support(primitive::DifferenceShape, φ::Real) = support(primitive.outer, φ) +support(primitive::DifferenceShape) = support(primitive.outer) +function area(primitive::DifferenceShape) + area(primitive.outer) - sum(area, primitive.holes; init = zero(eltype(primitive))) +end +function perimeter(primitive::DifferenceShape) + perimeter(primitive.outer) + + sum(perimeter, primitive.holes; init = zero(eltype(primitive))) +end +function centroid(primitive::DifferenceShape) + total_area = area(primitive) + total_area > zero(total_area) || throw(DomainError( + total_area, "difference primitive must have positive area" + )) + outer_area = area(primitive.outer) + outer_centroid = centroid(primitive.outer) + xmoment = outer_area * outer_centroid[1] + ymoment = outer_area * outer_centroid[2] + for hole in primitive.holes + hole_area = area(hole) + hole_centroid = centroid(hole) + xmoment -= hole_area * hole_centroid[1] + ymoment -= hole_area * hole_centroid[2] + end + return (xmoment / total_area, ymoment / total_area) +end + +Commons.input_fields(::Type{<:Disk}) = (r=(name="radius",unit="m"),) +Commons.input_fields(::Type{<:Rectangle}) = (w=(name="width",unit="m"),h=(name="height",unit="m")) +Commons.input_fields(::Type{<:Ellipse}) = (a=(name="semi-axis a",unit="m"),b=(name="semi-axis b",unit="m")) +Commons.input_fields(::Type{<:Annulus}) = (ri=(name="inner radius",unit="m"),ro=(name="outer radius",unit="m")) +Commons.input_fields(::Type{<:Polygon}) = (points=(name="vertex",unit="m"),) diff --git a/src/datamodel/geometry/resolve.jl b/src/datamodel/geometry/resolve.jl new file mode 100644 index 000000000..82e42faed --- /dev/null +++ b/src/datamodel/geometry/resolve.jl @@ -0,0 +1,43 @@ +""" +Resolve one primitive or contextual layer against a geometric context. +""" +function resolve end + +resolve(::EmptyBoundary, primitive::AbstractPrimitive) = primitive + +function resolve(context::Disk, definition::Shell) + values = map(float, promote(context.r, definition.t)) + inner, layer = values + return Annulus(inner, inner + layer, context.at) +end + +function resolve(context::Annulus, definition::Shell) + values = map(float, promote(context.ro, definition.t)) + inner, layer = values + return Annulus(inner, inner + layer, context.at) +end + +function resolve( + context::Union{Disk, Annulus}, + definition::Annulus +) + inner = r_ex(context) + tolerance = sqrt(eps(float(inner))) * max(one(inner), inner) + definition.ri + tolerance >= inner || throw(DomainError( + definition.ri, + "annulus inner radius must not overlap the current outer radius $inner" + )) + return Annulus(definition.ri, definition.ro, context.at) +end + +""" +Compose `at` with the existing absolute primitive pose. +""" +resolve(at::Pose2, primitive::AbstractPrimitive) = _with_pose(primitive, at * primitive.at) + +function resolve(at::Pose2, primitive::DifferenceShape) + return DifferenceShape( + resolve(at, primitive.outer), + map(hole -> resolve(at, hole), primitive.holes) + ) +end diff --git a/src/datamodel/geometry/sector.jl b/src/datamodel/geometry/sector.jl new file mode 100644 index 000000000..ec3556aa0 --- /dev/null +++ b/src/datamodel/geometry/sector.jl @@ -0,0 +1,438 @@ +""" +$(TYPEDEF) + +Define one material-neutral cable-sector primitive with optional fillets. + +The symmetry axis is local `+x`. Translation and rotation belong to +[`Pose2`](@ref), while material and terminal identity belong to `Region` and +its containing physical tree. + +$(TYPEDFIELDS) +""" +struct Sector{T <: Real} <: AbstractPrimitive{T} + "Angular opening \\[rad\\]." + span::T + "Radial coordinate of the innermost midpoint \\[m\\]." + r_base::T + "Radius of the circular outer back \\[m\\]." + r_back::T + "Common base and side fillet radius \\[m\\]." + fillet::T + + function Sector{T}( + span::T, + r_base::T, + r_back::T, + fillet::T + ) where {T <: Real} + all(isfinite, (span, r_base, r_back, fillet)) || throw(ArgumentError( + "sector dimensions must be finite" + )) + zero(span) < span < oftype(span, π) || throw(DomainError( + span, "sector span must lie in (0, π)" + )) + zero(r_base) <= r_base < r_back || throw(DomainError( + r_base, "sector base radius must lie in [0, r_back)" + )) + zero(fillet) <= fillet < r_back || throw(DomainError( + fillet, "sector fillet must lie in [0, r_back)" + )) + value = new{T}(span, r_base, r_back, fillet) + _sector_contacts(value) + return value + end +end + +function Sector( + span::Real, + r_base::Real, + r_back::Real, + fillet::Real = 0 +) + values = map(float, promote(span, r_base, r_back, fillet)) + return Sector{typeof(first(values))}(values...) +end + +function Base.convert( + ::Type{<:AbstractPrimitive{T}}, + value::Sector +) where {T <: Real} + return Sector{T}( + convert(T, value.span), + convert(T, value.r_base), + convert(T, value.r_back), + convert(T, value.fillet) + ) +end + +""" +$(TYPEDEF) + +Store the exact contact geometry of one placed [`Sector`](@ref). + +`contacts` contains passive derived arc and segment data. It is rebuilt from +the authoritative primitive and is never serialized. +""" +struct SectorShape{ + T <: Real, + P <: Sector{T}, + C <: NamedTuple, + A <: Pose2{T} +} <: AbstractShape{T} + "Authoritative intrinsic cable-sector primitive." + primitive::P + "Exact derived arc contacts and straight boundary segments." + contacts::C + "Pose in the completed cable coordinate system." + at::A +end + +""" +$(TYPEDEF) + +Represent the exact material domain between two resolved geometric boundaries. +""" +struct ShellShape{ + T <: Real, + I <: AbstractShape{T}, + O <: AbstractShape{T} +} <: AbstractShape{T} + "Resolved inner boundary." + inner::I + "Resolved parallel outer boundary." + outer::O + + function ShellShape(inner::I, outer::O) where { + T <: Real, I <: AbstractShape{T}, O <: AbstractShape{T} + } + shell_area = area(outer) - area(inner) + shell_area > zero(shell_area) || throw(DomainError( + shell_area, "a conformal shell must have positive area" + )) + return new{T, I, O}(inner, outer) + end +end + +_geometry_scalar(value) = float(nominal(value)) + +""" +$(TYPEDSIGNATURES) + +Return the absolute floating-point roundoff allowance used by geometric +predicates: 64 spacings at the magnitude of the nominal input, with unit scale +when that magnitude is zero. + +# Arguments + +- `value`: geometric scale in the SI units of the quantity being compared + (such as length \\[m\\] or area \\[m²\\]). + +# Returns + +- A nonnegative numerical allowance in the same units. Physical uncertainty + is not included in this roundoff allowance. +""" +function geometry_tolerance(value) + scale = abs(_geometry_scalar(value)) + return 64 * eps(iszero(scale) ? one(scale) : scale) +end + +function _positive_arc(start, stop) + period = oftype(start + stop, 2π) + while stop < start + stop += period + end + return (start = start, stop = stop) +end + +function _arc(center, radius, first_point, last_point) + start = atan(first_point[2] - center[2], first_point[1] - center[1]) + stop = atan(last_point[2] - center[2], last_point[1] - center[1]) + interval = _positive_arc(start, stop) + return (; center, radius, interval.start, interval.stop) +end + +function _sector_contacts(primitive::Sector) + α = primitive.span + b = primitive.r_base + radius = primitive.r_back + fillet = primitive.fillet + half_complement = π / 2 - α / 2 + cosine = cos(half_complement) + sine = sin(half_complement) + tangent = tan(half_complement) + + base_offset = b - fillet * (inv(cosine) - one(cosine)) + line_offset = b + fillet + qa = one(tangent) + tangent^2 + qb = 2 * line_offset * tangent + qc = line_offset^2 - (radius - fillet)^2 + discriminant = qb^2 - 4 * qa * qc + tolerance = geometry_tolerance(max(abs(qb^2), abs(4 * qa * qc))) + discriminant_value = _geometry_scalar(discriminant) + discriminant_value < -tolerance && throw(DomainError( + discriminant, + "sector side fillet has no valid tangent contact" + )) + discriminant_value < 0 && (discriminant = zero(discriminant)) + + old_x = (-qb + sqrt(discriminant)) / (2 * qa) + old_y = old_x * tangent + line_offset + side_distance = hypot(old_x, old_y) + distance_tolerance = geometry_tolerance(radius) + abs(_geometry_scalar(side_distance - (radius - fillet))) <= + distance_tolerance || throw(DomainError( + side_distance, + "sector side contact is inconsistent with its outer back" + )) + + base_center = (b + fillet, zero(b)) + base_x = base_offset + fillet * sine * tangent + base_lower = (base_x, -fillet * sine) + base_upper = (base_x, fillet * sine) + lower_center = (old_y, -old_x) + upper_center = (old_y, old_x) + side_lower = (old_y - fillet * cosine, -(old_x + fillet * sine)) + side_upper = (side_lower[1], -side_lower[2]) + back_lower = (old_y * radius / side_distance, -old_x * radius / side_distance) + back_upper = (back_lower[1], -back_lower[2]) + + lower_direction = ( + side_lower[1] - base_lower[1], + side_lower[2] - base_lower[2] + ) + lower_direction[1] * cosine - lower_direction[2] * sine >= + -distance_tolerance || throw(DomainError( + lower_direction, "sector straight side has reversed contact order" + )) + + arcs = ( + base = _arc(base_center, fillet, base_upper, base_lower), + lower = _arc(lower_center, fillet, side_lower, back_lower), + back = _arc((zero(radius), zero(radius)), radius, back_lower, back_upper), + upper = _arc(upper_center, fillet, back_upper, side_upper) + ) + all(arc -> begin + iszero(arc.radius) && return true + extent = _geometry_scalar(arc.stop - arc.start) + -distance_tolerance <= extent <= π + distance_tolerance + end, values(arcs)) || throw(DomainError( + arcs, "sector arc contacts do not form a convex boundary" + )) + segments = ( + lower = (base_lower, side_lower), + upper = (side_upper, base_upper) + ) + points = (; + base_upper, + base_lower, + side_lower, + back_lower, + back_upper, + side_upper + ) + return (; points, arcs, segments) +end + +function _sector_side_clearance(primitive::Sector) + normal_projection = sin(primitive.span / 2) + return 2 * ( + primitive.r_base * normal_projection - + primitive.fillet * (one(normal_projection) - normal_projection) + ) +end + +function SectorShape(primitive::Sector, at::Pose2 = _origin(eltype(primitive))) + T = promote_type(eltype(primitive), eltype(at)) + converted = convert(AbstractPrimitive{T}, primitive) + pose = convert(Pose2{T}, at) + contacts = _sector_contacts(converted) + return SectorShape{T, typeof(converted), typeof(contacts), typeof(pose)}( + converted, contacts, pose + ) +end + +function _line_moments(first_point, last_point) + x0, y0 = first_point + x1, y1 = last_point + return ( + area = (x0 * y1 - x1 * y0) / 2, + xmoment = (y1 - y0) * (x0^2 + x0 * x1 + x1^2) / 6, + ymoment = -(x1 - x0) * (y0^2 + y0 * y1 + y1^2) / 6 + ) +end + +function _arc_moments(arc) + cx, cy = arc.center + radius = arc.radius + start = arc.start + stop = arc.stop + span = stop - start + cosine = sin(stop) - sin(start) + sine = -cos(stop) + cos(start) + cosine_squared = span / 2 + (sin(2 * stop) - sin(2 * start)) / 4 + sine_squared = span / 2 - (sin(2 * stop) - sin(2 * start)) / 4 + cosine_cubed = + sin(stop) - sin(stop)^3 / 3 - sin(start) + sin(start)^3 / 3 + sine_cubed = + -cos(stop) + cos(stop)^3 / 3 + cos(start) - cos(start)^3 / 3 + return ( + area = ( + radius * cx * cosine + radius * cy * sine + radius^2 * span + ) / 2, + xmoment = radius * ( + cx^2 * cosine + 2 * cx * radius * cosine_squared + + radius^2 * cosine_cubed + ) / 2, + ymoment = radius * ( + cy^2 * sine + 2 * cy * radius * sine_squared + + radius^2 * sine_cubed + ) / 2 + ) +end + +function _sector_moments(shape::SectorShape) + moments = ( + _arc_moments(shape.contacts.arcs.base), + _line_moments(shape.contacts.segments.lower...), + _arc_moments(shape.contacts.arcs.lower), + _arc_moments(shape.contacts.arcs.back), + _arc_moments(shape.contacts.arcs.upper), + _line_moments(shape.contacts.segments.upper...) + ) + return ( + area = sum(getproperty.(moments, :area)), + xmoment = sum(getproperty.(moments, :xmoment)), + ymoment = sum(getproperty.(moments, :ymoment)) + ) +end + +area(shape::SectorShape) = _sector_moments(shape).area +function perimeter(shape::SectorShape) + arc_length = sum(values(shape.contacts.arcs)) do arc + arc.radius * (arc.stop - arc.start) + end + line_length = sum(values(shape.contacts.segments)) do segment + hypot( + segment[2][1] - segment[1][1], + segment[2][2] - segment[1][2] + ) + end + return arc_length + line_length +end + +function centroid(shape::SectorShape) + moments = _sector_moments(shape) + local_centroid = ( + moments.xmoment / moments.area, + moments.ymoment / moments.area + ) + return shape.at(local_centroid) +end + +function _angle_in_arc(angle, arc) + period = oftype(angle, 2π) + shifted_angle = angle + ceil((arc.start - angle) / period) * period + tolerance = geometry_tolerance(arc.stop - arc.start) + return _geometry_scalar(shifted_angle - arc.stop) <= tolerance +end + +function _local_support(shape::SectorShape, angle::Real) + cosine = cos(angle) + sine = sin(angle) + projection(point) = point[1] * cosine + point[2] * sine + support_values = [projection(point) for point in values(shape.contacts.points)] + for arc in values(shape.contacts.arcs) + _angle_in_arc(angle, arc) || continue + push!(support_values, projection(arc.center) + arc.radius) + end + return maximum(support_values) +end + +function support(shape::SectorShape, angle::Real) + return shape.at.x * cos(angle) + shape.at.y * sin(angle) + + _local_support(shape, angle - shape.at.φ) +end + +support(shape::SectorShape) = hypot(shape.at.x, shape.at.y) + shape.primitive.r_back +boundary(shape::SectorShape) = shape +r_in(shape::SectorShape) = shape.primitive.r_base +r_ex(shape::SectorShape) = shape.primitive.r_back +thickness(shape::SectorShape) = r_ex(shape) - r_in(shape) + +resolve(::EmptyBoundary, primitive::Sector) = SectorShape(primitive) +resolve(at::Pose2, primitive::Sector) = SectorShape(primitive, at) +function resolve(at::Pose2, shape::SectorShape) + return SectorShape(shape.primitive, at * shape.at) +end + +function resolve(inner::SectorShape, layer::Shell) + primitive = inner.primitive + outer = Sector( + primitive.span, + primitive.r_base - layer.t, + primitive.r_back + layer.t, + primitive.fillet + layer.t + ) + return ShellShape(inner, SectorShape(outer, inner.at)) +end + +boundary(shape::ShellShape) = shape.outer +area(shape::ShellShape) = area(shape.outer) - area(shape.inner) +perimeter(shape::ShellShape) = perimeter(shape.inner) + perimeter(shape.outer) +support(shape::ShellShape, angle::Real) = support(shape.outer, angle) +support(shape::ShellShape) = support(shape.outer) +function centroid(shape::ShellShape) + inner_area = area(shape.inner) + outer_area = area(shape.outer) + shell_area = outer_area - inner_area + inner_centroid = centroid(shape.inner) + outer_centroid = centroid(shape.outer) + return ( + (outer_area * outer_centroid[1] - inner_area * inner_centroid[1]) / + shell_area, + (outer_area * outer_centroid[2] - inner_area * inner_centroid[2]) / + shell_area + ) +end + +function resolve(at::Pose2, shape::ShellShape) + return ShellShape(resolve(at, shape.inner), resolve(at, shape.outer)) +end + +function _arc_points(arc, count::Int) + iszero(arc.radius) && return [arc.center] + return [ + ( + arc.center[1] + arc.radius * cos(angle), + arc.center[2] + arc.radius * sin(angle) + ) + for angle in range(arc.start, arc.stop; length = count) + ] +end + +""" + tessellate(shape::SectorShape; points_per_arc=32) + +Approximate the exact geometric boundary with coordinate tuples for rendering or meshing. +Geometric properties are computed from the exact sector boundary. +""" +function tessellate(shape::SectorShape; points_per_arc::Integer = 32) + points_per_arc >= 2 || throw(ArgumentError( + "points_per_arc must be at least two" + )) + contacts = shape.contacts + points = collect(_arc_points(contacts.arcs.base, Int(points_per_arc))) + push!(points, contacts.points.side_lower) + append!(points, _arc_points(contacts.arcs.lower, Int(points_per_arc))[2:end]) + append!(points, _arc_points(contacts.arcs.back, Int(points_per_arc))[2:end]) + append!(points, _arc_points(contacts.arcs.upper, Int(points_per_arc))[2:end]) + return [shape.at(point) for point in points] +end + +function tessellate(shape::ShellShape; points_per_arc::Integer = 128) + return ( + outer = tessellate(shape.outer; points_per_arc), + inner = tessellate(shape.inner; points_per_arc) + ) +end diff --git a/src/datamodel/geometry/shell.jl b/src/datamodel/geometry/shell.jl new file mode 100644 index 000000000..64f60ad07 --- /dev/null +++ b/src/datamodel/geometry/shell.jl @@ -0,0 +1,29 @@ +""" +$(TYPEDEF) + +Declare a conformal layer extending outward from an exact resolved geometric boundary. + +Calling [`resolve`](@ref) against the preceding geometric boundary produces the exact +material domain occupied by the layer. + +$(TYPEDFIELDS) +""" +struct Shell{T <: Real} + "Normal layer thickness \\[m\\]." + t::T + + function Shell{T}(t::T) where {T <: Real} + isfinite(t) && t > zero(t) || + throw(DomainError(t, "shell thickness must be positive and finite")) + return new{T}(t) + end +end + +Shell(t::Real) = Shell{typeof(float(t))}(float(t)) + +Base.eltype(::Shell{T}) where {T} = T +Base.eltype(::Type{<:Shell{T}}) where {T} = T + +function Base.convert(::Type{<:Shell{T}}, value::Shell) where {T <: Real} + return Shell{T}(convert(T, value.t)) +end diff --git a/src/datamodel/helpers.jl b/src/datamodel/helpers.jl deleted file mode 100644 index ab6e88027..000000000 --- a/src/datamodel/helpers.jl +++ /dev/null @@ -1,98 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Calculates the coordinates of three cables arranged in a trifoil pattern. - -# Arguments - -- `x0`: X-coordinate of the center point \\[m\\]. -- `y0`: Y-coordinate of the center point \\[m\\]. -- `r_ext`: External radius of the circular layout \\[m\\]. - -# Returns - -- A tuple containing: - - `xa`, `ya`: Coordinates of the top cable \\[m\\]. - - `xb`, `yb`: Coordinates of the bottom-left cable \\[m\\]. - - `xc`, `yc`: Coordinates of the bottom-right cable \\[m\\]. - -# Examples - -```julia -xa, ya, xb, yb, xc, yc = $(FUNCTIONNAME)(0.0, 0.0, 0.035) -println((xa, ya)) # Coordinates of top cable -println((xb, yb)) # Coordinates of bottom-left cable -println((xc, yc)) # Coordinates of bottom-right cable -``` -""" -function trifoil_formation(x0::T, y0::T, r_ext::T) where {T <: REALSCALAR} - @assert r_ext > 0 "External radius must be positive" - - d = r_ext / cos(deg2rad(30)) - xa = x0 - ya = y0 + d * sin(deg2rad(90)) - - xb = x0 + d * cos(deg2rad(210)) - yb = y0 + d * sin(deg2rad(210)) - - xc = x0 + d * cos(deg2rad(330)) - yc = y0 + d * sin(deg2rad(330)) - - return xa, ya, xb, yb, xc, yc -end - -""" -$(TYPEDSIGNATURES) - -Calculates the coordinates of three conductors arranged in a flat (horizontal or vertical) formation. - -# Arguments - -- `xc`: X-coordinate of the reference point \\[m\\]. -- `yc`: Y-coordinate of the reference point \\[m\\]. -- `s`: Spacing between adjacent conductors \\[m\\]. -- `vertical`: Boolean flag indicating whether the formation is vertical. - -# Returns - -- A tuple containing: - - `xa`, `ya`: Coordinates of the first conductor \\[m\\]. - - `xb`, `yb`: Coordinates of the second conductor \\[m\\]. - - `xc`, `yc`: Coordinates of the third conductor \\[m\\]. - -# Examples - -```julia -# Horizontal formation -xa, ya, xb, yb, xc, yc = $(FUNCTIONNAME)(0.0, 0.0, 0.1) -println((xa, ya)) # First conductor coordinates -println((xb, yb)) # Second conductor coordinates -println((xc, yc)) # Third conductor coordinates - -# Vertical formation -xa, ya, xb, yb, xc, yc = $(FUNCTIONNAME)(0.0, 0.0, 0.1, vertical=true) -``` -""" -function flat_formation(xc::T, yc::T, s::T; vertical = false) where {T <: REALSCALAR} - if vertical - # Layout is vertical; adjust only y-coordinates - xa, ya = xc, yc - xb, yb = xc, yc - s - xc, yc = xc, yc - 2s - else - # Layout is horizontal; adjust only x-coordinates - xa, ya = xc, yc - xb, yb = xc + s, yc - xc, yc = xc + 2s, yc - end - - return xa, ya, xb, yb, xc, yc -end - -# Outermost radius of a fully-built cable design -@inline function get_outer_radius(des::CableDesign) - last_comp = des.components[end] - r_c = to_nominal(last_comp.conductor_group.r_ex) - r_i = to_nominal(last_comp.insulator_group.r_ex) - return max(r_c, r_i) -end \ No newline at end of file diff --git a/src/datamodel/insulator.jl b/src/datamodel/insulator.jl deleted file mode 100644 index fdaf822b9..000000000 --- a/src/datamodel/insulator.jl +++ /dev/null @@ -1,112 +0,0 @@ -""" -$(TYPEDEF) - -Represents an insulating layer with defined geometric, material, and electrical properties given by the attributes: - -$(TYPEDFIELDS) -""" -struct Insulator{T <: REALSCALAR} <: AbstractInsulatorPart{T} - "Internal radius of the insulating layer \\[m\\]." - r_in::T - "External radius of the insulating layer \\[m\\]." - r_ex::T - "Material properties of the insulator." - material_props::Material{T} - "Operating temperature of the insulator \\[°C\\]." - temperature::T - "Cross-sectional area of the insulating layer \\[m²\\]." - cross_section::T - "Electrical resistance of the insulating layer \\[Ω/m\\]." - resistance::T - "Geometric mean radius of the insulator \\[m\\]." - gmr::T - "Shunt capacitance per unit length of the insulating layer \\[F/m\\]." - shunt_capacitance::T - "Shunt conductance per unit length of the insulating layer \\[S·m\\]." - shunt_conductance::T -end - -""" -$(TYPEDSIGNATURES) - -Constructs an [`Insulator`](@ref) object with specified geometric and material parameters. - -# Arguments - -- `r_in`: Internal radius of the insulating layer \\[m\\]. -- `r_ex`: External radius or thickness of the layer \\[m\\]. -- `material_props`: Material properties of the insulating material. -- `temperature`: Operating temperature of the insulator \\[°C\\]. - -# Returns - -- An [`Insulator`](@ref) object with calculated electrical properties. - -# Examples - -```julia -material_props = Material(1e10, 3.0, 1.0, 20.0, 0.0) -insulator_layer = $(FUNCTIONNAME)(0.01, 0.015, material_props, temperature=25) -``` -""" -function Insulator( - r_in::T, - r_ex::T, - material_props::Material{T}, - temperature::T, -) where {T <: REALSCALAR} - - rho = material_props.rho - T0 = material_props.T0 - alpha = material_props.alpha - epsr_r = material_props.eps_r - - cross_section = π * (r_ex^2 - r_in^2) - - resistance = - calc_tubular_resistance(r_in, r_ex, rho, alpha, T0, temperature) - gmr = calc_tubular_gmr(r_ex, r_in, material_props.mu_r) - shunt_capacitance = calc_shunt_capacitance(r_in, r_ex, epsr_r) - shunt_conductance = calc_shunt_conductance(r_in, r_ex, rho) - - # Initialize object - return Insulator( - r_in, - r_ex, - material_props, - temperature, - cross_section, - resistance, - gmr, - shunt_capacitance, - shunt_conductance, - ) -end - -const _REQ_INSULATOR = (:r_in, :r_ex, :material_props) -const _OPT_INSULATOR = (:temperature,) -const _DEFS_INSULATOR = (T₀,) - -Validation.has_radii(::Type{Insulator}) = true -Validation.has_temperature(::Type{Insulator}) = true -Validation.required_fields(::Type{Insulator}) = _REQ_INSULATOR -Validation.keyword_fields(::Type{Insulator}) = _OPT_INSULATOR -Validation.keyword_defaults(::Type{Insulator}) = _DEFS_INSULATOR - -# accept proxies for radii -Validation.is_radius_input(::Type{Insulator}, ::Val{:r_in}, x::AbstractCablePart) = - true -Validation.is_radius_input(::Type{Insulator}, ::Val{:r_in}, x::Thickness) = true -Validation.is_radius_input(::Type{Insulator}, ::Val{:r_ex}, x::Thickness) = true -Validation.is_radius_input(::Type{Insulator}, ::Val{:r_ex}, x::Diameter) = true - -Validation.extra_rules(::Type{Insulator}) = (IsA{Material}(:material_props),) - -# normalize proxies -> numbers -Validation.parse(::Type{Insulator}, nt) = begin - rin, rex = _normalize_radii(Insulator, nt.r_in, nt.r_ex) - (; nt..., r_in = rin, r_ex = rex) -end - -# This macro expands to a weakly-typed constructor for Insulator -@construct Insulator _REQ_INSULATOR _OPT_INSULATOR _DEFS_INSULATOR diff --git a/src/datamodel/insulatorgroup.jl b/src/datamodel/insulatorgroup.jl deleted file mode 100644 index aa57d36b1..000000000 --- a/src/datamodel/insulatorgroup.jl +++ /dev/null @@ -1,203 +0,0 @@ - -""" -$(TYPEDEF) - -Represents a composite coaxial insulator group assembled from multiple insulating layers. - -This structure serves as a container for different [`AbstractInsulatorPart`](@ref) elements -(such as insulators and semiconductors) arranged in concentric layers. -The `InsulatorGroup` aggregates these individual parts and provides equivalent electrical -properties that represent the composite behavior of the entire assembly, stored in the attributes: - -$(TYPEDFIELDS) -""" -mutable struct InsulatorGroup{T <: REALSCALAR} <: AbstractInsulatorPart{T} - "Inner radius of the insulator group \\[m\\]." - r_in::T - "Outer radius of the insulator group \\[m\\]." - r_ex::T - "Cross-sectional area of the entire insulator group \\[m²\\]." - cross_section::T - "Shunt capacitance per unit length of the insulator group \\[F/m\\]." - shunt_capacitance::T - "Shunt conductance per unit length of the insulator group \\[S·m\\]." - shunt_conductance::T - "Vector of insulator layer components." - layers::Vector{AbstractInsulatorPart{T}} - - @doc """ - $(TYPEDSIGNATURES) - - Constructs an [`InsulatorGroup`](@ref) instance initializing with the initial insulator part. - - # Arguments - - - `initial_insulator`: An [`AbstractInsulatorPart`](@ref) object located at the innermost position of the insulator group. - - # Returns - - - An [`InsulatorGroup`](@ref) object initialized with geometric and electrical properties derived from the initial insulator. - - # Examples - - ```julia - material_props = Material(1e10, 3.0, 1.0, 20.0, 0.0) - initial_insulator = Insulator(0.01, 0.015, material_props) - insulator_group = $(FUNCTIONNAME)(initial_insulator) - println(insulator_group.layers) # Output: [initial_insulator] - println(insulator_group.shunt_capacitance) # Output: Capacitance in [F/m] - ``` - """ - function InsulatorGroup{T}( - r_in::T, - r_ex::T, - cross_section::T, - shunt_capacitance::T, - shunt_conductance::T, - layers::Vector{AbstractInsulatorPart{T}}, - ) where {T} - return new{T}(r_in, r_ex, cross_section, - shunt_capacitance, shunt_conductance, layers) - end - - function InsulatorGroup{T}(initial_insulator::AbstractInsulatorPart{T}) where {T} - return new{T}( - initial_insulator.r_in, - initial_insulator.r_ex, - initial_insulator.cross_section, - initial_insulator.shunt_capacitance, - initial_insulator.shunt_conductance, - AbstractInsulatorPart{T}[initial_insulator], - ) - end -end - -# Convenience outer -InsulatorGroup(ins::AbstractInsulatorPart{T}) where {T} = InsulatorGroup{T}(ins) - -""" -$(TYPEDSIGNATURES) - -Adds a new part to an existing [`InsulatorGroup`](@ref) object and updates its equivalent electrical parameters. - -# Behavior: - -1. Apply part-level keyword defaults (from `Validation.keyword_defaults`). -2. Default `r_in` to `group.r_ex` if absent. -3. Compute `Tnew = resolve_T(group, r_in, args..., values(kwargs)..., f)`. -4. If `Tnew === T`, mutate in place; else `coerce_to_T(group, Tnew)` then mutate and **return the promoted group**. - -# Arguments - -- `group`: [`InsulatorGroup`](@ref) object to which the new part will be added. -- `part_type`: Type of insulator part to add ([`AbstractInsulatorPart`](@ref)). -- `args...`: Positional arguments specific to the constructor of the `part_type` ([`AbstractInsulatorPart`](@ref)) \\[various\\]. -- `kwargs...`: Named arguments for the constructor including optional values specific to the constructor of the `part_type` ([`AbstractInsulatorPart`](@ref)) \\[various\\]. - -# Returns - -- The function modifies the [`InsulatorGroup`](@ref) instance in place and does not return a value. - -# Notes - -- Updates `shunt_capacitance`, `shunt_conductance`, `r_ex`, and `cross_section` to account for the new part. -- The `r_in` of the new part defaults to the external radius of the existing insulator group if not specified. - -!!! warning "Note" - - When an [`AbstractCablePart`](@ref) is provided as `r_in`, the constructor retrieves its `r_ex` value, allowing the new cable part to be placed directly over the existing part in a layered cable design. - - For uncertain geometries, the preceding part's outer-radius derivative graph - is retained. Adjacent layers therefore share one physical boundary and - cumulative-radius covariance is preserved across different part types. - -# Examples - -```julia -material_props = Material(1e10, 3.0, 1.0, 20.0, 0.0) -insulator_group = InsulatorGroup(Insulator(0.01, 0.015, material_props)) -$(FUNCTIONNAME)(insulator_group, Semicon, 0.015, 0.018, material_props) -``` - -# See also - -- [`InsulatorGroup`](@ref) -- [`Insulator`](@ref) -- [`Semicon`](@ref) -- [`calc_parallel_equivalent`](@ref) -""" -function add!( - group::InsulatorGroup{T}, - part_type::Type{C}, - args...; - f::Number = f₀, - kwargs..., -) where {T, C <: AbstractInsulatorPart} - - # 1) Merge declared keyword defaults for this part type - kwv = _with_kwdefaults(C, (; kwargs...)) - - # 2) Default stacking: inner radius = current outer radius unless overridden - rin = get(kwv, :r_in, group.r_ex) - kwv = haskey(kwv, :r_in) ? kwv : merge(kwv, (; r_in = rin)) - - # 3) Decide target numeric type using *current group + raw inputs + f* - Tnew = resolve_T(group, rin, args..., values(kwv)..., f) - - if Tnew === T - # 4a) Fast path: mutate in place - return _do_add!(group, C, args...; f, kwv...) - else - @warn """ - Adding a `$Tnew` part to an `InsulatorGroup{$T}` returns a **promoted** group. - Capture the result: group = add!(group, $C, …) - """ - promoted = coerce_to_T(group, Tnew) - return _do_add!(promoted, C, args...; f, kwv...) - end -end - -""" -$(TYPEDSIGNATURES) - -Do the actual insertion for `InsulatorGroup` with the group already at the -correct scalar type. Validates/parses the part, coerces to the group’s `T`, -constructs the strict numeric core, and updates geometry and admittances at the -provided frequency. - -Returns the mutated group (same object). -""" -function _do_add!( - group::InsulatorGroup{Tg}, - C::Type{<:AbstractInsulatorPart}, - args...; - f::Number = f₀, - kwargs..., -) where {Tg} - - # Materialize keyword args into a NamedTuple - kw = (; kwargs...) - - # Validate + parse with the part’s own pipeline (proxies resolved here) - ntv = Validation.validate!(C, kw.r_in, args...; kw...) - - # Build argument order and coerce validated values to group’s T - order = (Validation.required_fields(C)..., Validation.keyword_fields(C)...) - coerced = _coerced_args(C, ntv, Tg, order) # respects coercive_fields(C) - new_part = C(coerced...) # call strict numeric core - - # Parallel admittances at frequency f - ω = Tg(2π) * coerce_to_T(f, Tg) - Yg = Complex(group.shunt_conductance, ω * group.shunt_capacitance) - Yp = Complex(new_part.shunt_conductance, ω * new_part.shunt_capacitance) - Ye = calc_parallel_equivalent(Yg, Yp) - group.shunt_conductance = real(Ye) - group.shunt_capacitance = imag(Ye) / ω - - # Update geometry - group.r_ex += new_part.r_ex - new_part.r_in - group.cross_section += new_part.cross_section - - push!(group.layers, new_part) - return group -end - -include("insulatorgroup/base.jl") diff --git a/src/datamodel/insulatorgroup/base.jl b/src/datamodel/insulatorgroup/base.jl deleted file mode 100644 index f4bf7185e..000000000 --- a/src/datamodel/insulatorgroup/base.jl +++ /dev/null @@ -1,3 +0,0 @@ - -Base.eltype(::InsulatorGroup{T}) where {T} = T -Base.eltype(::Type{InsulatorGroup{T}}) where {T} = T \ No newline at end of file diff --git a/src/datamodel/interfaces.jl b/src/datamodel/interfaces.jl new file mode 100644 index 000000000..8ee7dbe6e --- /dev/null +++ b/src/datamodel/interfaces.jl @@ -0,0 +1,19 @@ +""" +Return the number of cable positions in a line-cable system. +""" +function ncables end + +""" +Return the number of distinct active phases in a line-cable system. +""" +function nphases end + +""" +Return detached preview geometry for one owned cable layer. +""" +function preview_shapes end + +""" +Return the materials represented by one owned cable layer. +""" +function preview_materials end diff --git a/src/datamodel/io.jl b/src/datamodel/io.jl deleted file mode 100644 index 7b45456a0..000000000 --- a/src/datamodel/io.jl +++ /dev/null @@ -1,193 +0,0 @@ -# Full inheritance over composition madness -function Base.getproperty(part::AbstractCablePart, sym::Symbol) - # Fast path: Is it a real field on the top-level struct? (r_in, gmr, shape) - if hasfield(typeof(part), sym) - return getfield(part, sym) # MUST use getfield here to prevent infinite recursion - end - - # Fallback path: Route it to the shape payload (if the struct has a shape) - if hasfield(typeof(part), :shape) - shape_payload = getfield(part, :shape) - # We use hasproperty on the shape just in case the shape itself has custom routing - if hasproperty(shape_payload, sym) - return getproperty(shape_payload, sym) - end - end - - # If it doesn't exist anywhere, fallback to standard getfield to throw the normal error - return getfield(part, sym) -end - -function Base.hasproperty(part::AbstractCablePart, sym::Symbol) - # 1. Does it exist at the top level? - if hasfield(typeof(part), sym) - return true - end - - # 2. Does it exist in the shape payload? - if hasfield(typeof(part), :shape) - return hasproperty(getfield(part, :shape), sym) - end - - return false -end - -function Base.propertynames(part::AbstractCablePart, private::Bool = false) - top_fields = fieldnames(typeof(part)) - - if hasfield(typeof(part), :shape) - shape_payload = getfield(part, :shape) - shape_fields = propertynames(shape_payload, private) - - # Merge and deduplicate the fields - return Tuple(unique((top_fields..., shape_fields...))) - end - - return top_fields -end - -""" -$(TYPEDSIGNATURES) - -Defines the display representation of an [`AbstractCablePart`](@ref) object for REPL or text output. - -# Arguments - -- `io`: Output stream. -- `::MIME"text/plain"`: MIME type for plain text output. -- `part`: The [`AbstractCablePart`](@ref) instance to be displayed. - -# Returns - -- Nothing. Modifies `io` by writing text representation of the object. -""" -function Base.show(io::IO, ::MIME"text/plain", part::T) where {T <: AbstractCablePart} - # Start output with type name - print(io, "$(nameof(T)): [") - - # Use _print_fields to display all relevant fields - _print_fields( - io, - part, - [ - :r_in, - :r_ex, - :cross_section, - :resistance, - :gmr, - :shunt_capacitance, - :shunt_conductance, - ], - ) - - println(io, "]") - - # Display material properties if available - if hasproperty(part, :material_props) - print(io, "└─ Material properties: [") - _print_fields( - io, - part.material_props, - [:rho, :eps_r, :mu_r, :alpha], - ) - println(io, "]") - end -end - - -""" -$(TYPEDSIGNATURES) - -Defines the display representation of a [`ConductorGroup`](@ref) or [`InsulatorGroup`](@ref)objects for REPL or text output. - -# Arguments - -- `io`: Output stream. -- `::MIME"text/plain"`: MIME type for plain text output. -- `group`: The [`ConductorGroup`](@ref) or [`InsulatorGroup`](@ref) instance to be displayed. - -# Returns - -- Nothing. Modifies `io` by writing text representation of the object. -""" -function Base.show(io::IO, ::MIME"text/plain", group::Union{ConductorGroup, InsulatorGroup}) - - print(io, "$(length(group.layers))-element $(nameof(typeof(group))): [") - _print_fields( - io, - group, - [ - :r_in, - :r_ex, - :cross_section, - :resistance, - :gmr, - :shunt_capacitance, - :shunt_conductance, - ], - ) - println(io, "]") - - # Tree-like layer representation - - for (i, layer) in enumerate(group.layers) - # Determine prefix based on whether it's the last layer - prefix = i == length(group.layers) ? "└─" : "├─" - # Print layer information with only selected fields - print(io, prefix, "$(nameof(typeof(layer))): [") - _print_fields( - io, - layer, - [ - :r_in, - :r_ex, - :cross_section, - :resistance, - :gmr, - :shunt_capacitance, - :shunt_conductance, - ], - ) - println(io, "]") - end -end - -""" -$(TYPEDSIGNATURES) - -Print the specified fields of an object in a compact format. - -# Arguments -- `io`: The output stream. -- `obj`: The object whose fields will be displayed. -- `fields_to_show`: Vector of field names (as Symbols) to display. -- `sigdigits`: Number of significant digits for rounding numeric values. - -# Returns - -- Number of fields that were actually displayed. -""" -function _print_fields(io::IO, obj, fields_to_show::Vector{Symbol}; sigdigits::Int = 4) - displayed_fields = 0 - for field in fields_to_show - if hasproperty(obj, field) - value = getproperty(obj, field) - # Skip NaN values - if value isa Number && isnan(value) - continue - end - # Add comma if not the first item - if displayed_fields > 0 - print(io, ", ") - end - # Format numbers with rounding - if value isa Number - print(io, "$field=$(round(value, sigdigits=sigdigits))") - else - print(io, "$field=$value") - end - displayed_fields += 1 - end - end - return displayed_fields -end diff --git a/src/datamodel/linecablesystem.jl b/src/datamodel/linecablesystem.jl deleted file mode 100644 index 481a0aa88..000000000 --- a/src/datamodel/linecablesystem.jl +++ /dev/null @@ -1,372 +0,0 @@ - -""" -$(TYPEDEF) - -Represents a physically defined cable with position and phase mapping within a system. - -$(TYPEDFIELDS) -""" -struct CablePosition{T <: REALSCALAR} - "The [`CableDesign`](@ref) object assigned to this cable position." - design_data::CableDesign{T} - "Horizontal coordinate \\[m\\]." - horz::T - "Vertical coordinate \\[m\\]." - vert::T - "Phase mapping vector (aligned with design_data.components)." - conn::Vector{Int} - - @doc """ - $(TYPEDSIGNATURES) - - Constructs a [`CablePosition`](@ref) instance with specified cable design, coordinates, and phase mapping. - - # Arguments - - - `cable`: A [`CableDesign`](@ref) object defining the cable structure. - - `horz`: Horizontal coordinate \\[m\\]. - - `vert`: Vertical coordinate \\[m\\]. - - `conn`: A dictionary mapping component names to phase indices, or `nothing` for default mapping. - - # Returns - - - A [`CablePosition`](@ref) object with the assigned cable design, coordinates, and phase mapping. - - !!! note "Phase mapping" - The `conn` argument is a `Dict` that maps the cable components to their respective phases. The values (1, 2, 3) represent the phase numbers (A, B, C) in a three-phase system. Components mapped to phase 0 will be Kron-eliminated (grounded). Components set to the same phase will be bundled into an equivalent phase. - - # Examples - - ```julia - cable_design = CableDesign("example", nominal_data, components_dict) - xa, ya = 0.0, -1.0 # Coordinates in meters - - # With explicit phase mapping - cablepos1 = $(FUNCTIONNAME)(cable_design, xa, ya, Dict("core" => 1)) - - # With default phase mapping (first component to phase 1, others to 0) - default_cablepos = $(FUNCTIONNAME)(cable_design, xa, ya) - ``` - - # See also - - - [`CableDesign`](@ref) - """ - function CablePosition{T}( - cable::CableDesign{T}, - horz::T, - vert::T, - conn::Vector{Int}, - ) where {T <: REALSCALAR} - # Validate: cable not empty - @assert !isempty(cable.components) "CableDesign must contain at least one component" - - # Find outermost radius (last component) - last_comp = cable.components[end] - r_cond = last_comp.conductor_group.r_ex - r_ins = last_comp.insulator_group.r_ex - r_max = max(r_cond, r_ins) - - # Validate vertical position - if iszero(vert) - throw( - ArgumentError( - "Vertical position cannot be exactly at the air/earth interface (z=0)", - ), - ) - end - if abs(vert) < r_max - throw( - ArgumentError( - "Vertical position |$vert| must be ≥ cable's outer radius $r_max to avoid crossing z=0", - ), - ) - end - - return new{T}(cable, horz, vert, conn) - end -end - -""" -$(TYPEDSIGNATURES) - -**Weakly-typed constructor** that infers `T` from the `cable` and coordinates, builds/validates the phase mapping, coerces inputs to `T`, and calls the typed kernel. -""" -function CablePosition( - cable::Union{CableDesign, Nothing}, - horz::Number, - vert::Number, - conn::Union{Dict{String, Int}, Nothing} = nothing, -) - @assert !isnothing(cable) "A valid CableDesign must be provided" - @assert !isempty(cable.components) "CableDesign must contain at least one component" - - # Build phase mapping vector aligned to component order - names = [comp.id for comp in cable.components] - conn_vector = if isnothing(conn) - [i == 1 ? 1 : 0 for i in 1:length(names)] # default: first component → phase 1, others grounded - else - [get(conn, name, 0) for name in names] - end - - # Validate provided mapping keys exist (only when conn was given) - if conn !== nothing - for component_id in keys(conn) - if !(component_id in names) - throw( - ArgumentError( - "Component ID '$component_id' not found in the cable design.", - ), - ) - end - end - end - - # Warn if all grounded - !all(iszero, conn_vector) || - @warn("At least one component should be assigned to a non-zero phase.") - - # Resolve scalar type and coerce — with identity-preserving pass-through - T = resolve_T(cable, horz, vert) - cableT = coerce_to_T(cable, T) - horzT = (horz isa T) ? horz : coerce_to_T(horz, T) - vertT = (vert isa T) ? vert : coerce_to_T(vert, T) - - return CablePosition{T}(cableT, horzT, vertT, conn_vector) -end - -""" -$(TYPEDEF) - -Represents a cable system configuration, defining the physical structure, cables, and their positions. - -$(TYPEDFIELDS) -""" -mutable struct LineCableSystem{T <: REALSCALAR} - "Unique identifier for the system." - system_id::String - "Length of the cable system \\[m\\]." - line_length::T - "Number of cables in the system." - num_cables::Int - "Number of actual phases in the system." - num_phases::Int - "Cross-section cable positions." - cables::Vector{CablePosition{T}} - - @doc """ - $(TYPEDSIGNATURES) - - Constructs a [`LineCableSystem`](@ref) with an initial cable position and system parameters. - - # Arguments - - - `system_id`: Identifier for the cable system. - - `line_length`: Length of the cable system \\[m\\]. - - `cable`: Initial [`CablePosition`](@ref) object defining a cable position and phase mapping. - - # Returns - - - A [`LineCableSystem`](@ref) object initialized with a single cable position. - - # Examples - - ```julia - cable_design = CableDesign("example", nominal_data, components_dict) - cablepos1 = CablePosition(cable_design, 0.0, 0.0, Dict("core" => 1)) - - cable_system = $(FUNCTIONNAME)("test_case_1", 1000.0, cablepos1) - println(cable_system.num_phases) # Prints number of unique phase assignments - ``` - - # See also - - - [`CablePosition`](@ref) - - [`CableDesign`](@ref) - """ - @inline function LineCableSystem{T}( - system_id::String, - line_length::T, - cable::CablePosition{T}, - ) where {T <: REALSCALAR} - # phase accounting from this single position - conn = cable.conn - # count unique non-zero phases - nph = count(x -> x > 0, unique(conn)) - return new{T}(system_id, line_length, 1, nph, CablePosition{T}[cable]) - end - - @doc """ - $(TYPEDSIGNATURES) - - **Strict numeric kernel**. Builds a typed `LineCableSystem{T}` from a vector of `CablePosition{T}`. - """ - @inline function LineCableSystem{T}( - system_id::String, - line_length::T, - cables::Vector{CablePosition{T}}, - ) where {T <: REALSCALAR} - @assert !isempty(cables) "At least one CablePosition must be provided" - # flatten & count phases - assigned = unique(vcat((cp.conn for cp in cables)...)) - nph = count(x -> x > 0, assigned) - return new{T}(system_id, line_length, length(cables), nph, cables) - end -end - -""" -$(TYPEDSIGNATURES) - -Weakly-typed constructor. Infers scalar type `T` from `line_length` and the `cable` (or its design), coerces as needed, and calls the strict kernel. -""" -function LineCableSystem( - system_id::String, - line_length::Number, - cable::CablePosition, -) - T = resolve_T(line_length, cable) - return LineCableSystem{T}( - system_id, - coerce_to_T(line_length, T), - coerce_to_T(cable, T), - ) -end - -""" -$(TYPEDSIGNATURES) - -Weakly-typed convenience constructor. Builds a `CablePosition` from a `CableDesign` and coordinates, then constructs the system. -""" -function LineCableSystem( - system_id::String, - line_length::Number, - cable::CableDesign, - horz::Number, - vert::Number, - conn::Union{Dict{String, Int}, Nothing} = nothing, -) - pos = CablePosition(cable, horz, vert, conn) - return LineCableSystem(system_id, line_length, pos) -end - -# # Outer (maximum) radius of the last component of a position's design -# @inline function _outer_radius(cp::CablePosition) -# comp = cp.design_data.components[end] -# return max(comp.conductor_group.r_ex, comp.insulator_group.r_ex) -# end - -# True if two cable disks overlap (strictly), evaluated in a common scalar type T -@inline function _overlaps(a::CablePosition, b::CablePosition, ::Type{T}) where {T} - x1 = coerce_to_T(a.horz, T) - y1 = coerce_to_T(a.vert, T) - r1 = coerce_to_T(get_outer_radius(a.design_data), T) - x2 = coerce_to_T(b.horz, T) - y2 = coerce_to_T(b.vert, T) - r2 = coerce_to_T(get_outer_radius(b.design_data), T) - d = hypot(x1 - x2, y1 - y2) - tol = 1e-8 * max(r1 + r2, 1.0) - overlaps = d+tol < (r1 + r2) - if overlaps - @warn "Cable positions overlap: distance $d < sum of radii $(r1 + r2)" - end - return overlaps # strict overlap; grazing contact (within tolerance) allowed -end - -""" -$(TYPEDSIGNATURES) - -Adds a new cable position to an existing [`LineCableSystem`](@ref), updating its phase mapping and cable count. If adding the position introduces a different numeric scalar type, the system is **promoted** and the promoted system is returned. Otherwise, mutation happens in place. - -# Arguments - -- `system`: Instance of [`LineCableSystem`](@ref) to which the cable will be added. -- `cable`: A [`CableDesign`](@ref) object defining the cable structure. -- `horz`: Horizontal coordinate \\[m\\]. -- `vert`: Vertical coordinate \\[m\\]. -- `conn`: Dictionary mapping component names to phase indices, or `nothing` for automatic assignment. - -# Returns - -- The modified [`LineCableSystem`](@ref) object with the new cable added. - -# Examples - -```julia -cable_design = CableDesign("example", nominal_data, components_dict) - -# Define coordinates for two cables -xa, ya = 0.0, -1.0 -xb, yb = 1.0, -2.0 - -# Create initial system with one cable -cablepos1 = CablePosition(cable_design, xa, ya, Dict("core" => 1)) -cable_system = LineCableSystem("test_case_1", 1000.0, cablepos1) - -# Add second cable to system -$(FUNCTIONNAME)(cable_system, cable_design, xb, yb, Dict("core" => 2)) - -println(cable_system.num_cables) # Prints: 2 -``` - -# See also - -- [`LineCableSystem`](@ref) -- [`CablePosition`](@ref) -- [`CableDesign`](@ref) -""" -function add!(system::LineCableSystem{T}, pos::CablePosition) where {T} - # Decide the common numeric type first - Tnew = resolve_T(system, pos) - - # Geometric guard once, in a common type (no mutation, no allocation) - for cp in system.cables - if _overlaps(cp, pos, Tnew) - throw( - ArgumentError( - "Cable position overlaps an existing cable (disks intersect).", - ), - ) - end - end - - if Tnew === T - posT = coerce_to_T(pos, T) # identity if already T - push!(system.cables, posT) - system.num_cables += 1 - assigned = unique(vcat((cp.conn for cp in system.cables)...)) - system.num_phases = count(x -> x > 0, assigned) - return system - else - @warn """ - Adding a `$Tnew` position to a `LineCableSystem{$T}` returns a **promoted** system. - Capture the result: system = add!(system, position) - """ - sysT = coerce_to_T(system, Tnew) - posT = coerce_to_T(pos, Tnew) - push!(sysT.cables, posT) - sysT.num_cables += 1 - assigned = unique(vcat((cp.conn for cp in sysT.cables)...)) - sysT.num_phases = count(x -> x > 0, assigned) - return sysT - end -end - -""" -$(TYPEDSIGNATURES) - -Convenience `add!` that accepts a cable design and coordinates (and optional mapping). -Builds a [`CablePosition`](@ref) and forwards to `add!(system, pos)`. -""" -function add!( - system::LineCableSystem{T}, - cable::CableDesign, - horz::Number, - vert::Number, - conn::Union{Dict{String, Int}, Nothing} = nothing, -) where {T} - pos = CablePosition(cable, horz, vert, conn) - return add!(system, pos) # may mutate or return a promoted system -end - -include("linecablesystem/dataframe.jl") -include("linecablesystem/base.jl") diff --git a/src/datamodel/linecablesystem/base.jl b/src/datamodel/linecablesystem/base.jl deleted file mode 100644 index 327c4d387..000000000 --- a/src/datamodel/linecablesystem/base.jl +++ /dev/null @@ -1,35 +0,0 @@ - -Base.eltype(::CablePosition{T}) where {T} = T -Base.eltype(::Type{CablePosition{T}}) where {T} = T -Base.eltype(::LineCableSystem{T}) where {T} = T -Base.eltype(::Type{LineCableSystem{T}}) where {T} = T - -function Base.show(io::IO, ::MIME"text/plain", system::LineCableSystem) - # Print top level info - println( - io, - "LineCableSystem \"$(system.system_id)\": [line_length=$(system.line_length), num_cables=$(system.num_cables), num_phases=$(system.num_phases)]", - ) - - # Print cable definitions - println(io, "└─ $(length(system.cables))-element CablePosition:") - - # Display each cable definition - for (i, cable_position) in enumerate(system.cables) - # Cable prefix - prefix = i == length(system.cables) ? " └─" : " ├─" - - # Format connections as a string - components = [comp.id for comp in cable_position.design_data.components] - conn_str = join( - ["$(comp)→$(phase)" for (comp, phase) in zip(components, cable_position.conn)], - ", ", - ) - - # Print cable info - println( - io, - "$(prefix) CableDesign \"$(cable_position.design_data.cable_id)\": [horz=$(round(cable_position.horz, sigdigits=4)), vert=$(round(cable_position.vert, sigdigits=4)), conn=($(conn_str))]", - ) - end -end \ No newline at end of file diff --git a/src/datamodel/linecablesystem/clearance.jl b/src/datamodel/linecablesystem/clearance.jl new file mode 100644 index 000000000..a98975ee1 --- /dev/null +++ b/src/datamodel/linecablesystem/clearance.jl @@ -0,0 +1,323 @@ +"Minimum engineering separation of cable exteriors \\[m\\]." +const _CABLE_CLEARANCE = 1.0e-6 + +# A scoped value, not process-global mutable state: nested user builders can +# participate without acquiring an extra public argument. Each sampling task +# maintains its records and counters. +const _CLEARANCE_CONTEXT = Base.ScopedValues.ScopedValue{Any}(nothing) + +function _clearance_exterior(design, pose) + shape = boundary(design.geometry) + if shape isa Disk + # Keep eccentric or uncertain local @at coordinates in the center, not + # inside a norm about the declaration origin (undefined derivative at 0). + reserve = uncertainty(outer_radius(design)) + if !isfinite(reserve) + # At the origin, `norm(x,y)` has no derivative. Its RMS offset + # bounds the standard deviation. Retain that conservative reserve + # without manufacturing an independent Measurement. + reserve = uncertainty(shape.r) + hypot(uncertainty(shape.at.x), uncertainty(shape.at.y)) + end + reserve = max(reserve, uncertainty(shape.r)) + return (radius = shape.r, center = pose * shape.at, reserve = reserve) + end + radius = outer_radius(design) + return (radius = radius, center = pose, reserve = uncertainty(radius)) +end + +""" +Floating-point allowance for subtraction of placement coordinates \\[m\\]. +""" +function _clearance_roundoff(radii, poses) + scale = maximum(abs ∘ nominal, radii) + for pose in poses + scale = max(scale, abs(nominal(pose.x)), abs(nominal(pose.y))) + end + return 8eps(float(scale)) +end + +""" +$(TYPEDSIGNATURES) + +Resolve exterior-circle clearance by expanding relative cable placements, +without changing cable dimensions or rotations. Shared translations and +formation symmetry are retained. If `interface` is true, each half-space +group is translated away from the air-earth interface when necessary. + +The required pair gap is: + +```math +c_{ij} = 10^{-6} + \\max\\left(u_{R,\\max}, u(d_{ij}-r_i-r_j)\\right) +``` + +where all lengths and standard uncertainties are in \\[m\\]. Diagonal entries +retain the corresponding interface clearance. Supplied `required` values +retain the original uncertainty budget after stochastic sampling. + +Circular boundaries use their composed centers and physical radii. Other +geometric boundaries use their containing circles. A nominally zero uncertain circular +offset uses its RMS displacement as a conservative radius-uncertainty reserve. + +Nominal overlaps are rejected. `sampling=true` permits construction of a +feasible realization from an already checked declaration. With `adjust=false`, +only validate the supplied geometry and clearance requirements. + +# Arguments + +- `designs`: completed cable designs in placement order. +- `poses`: cable translations \\[m\\] and rotations \\[rad\\] in the system frame. + +# Keywords + +- `required=nothing`: retained pairwise and interface clearance matrix \\[m\\]. +- `reference=poses`: declared poses used to distinguish nominal overlaps from sampled ones. +- `interface=false`: enforce clearance from the air-earth interface. +- `sampling=false`: permit correction of overlaps produced by a sampled declaration. +- `adjust=true`: resolve placements. `false` performs validation only. +- `reference_centers=nothing`: composed exterior-center poses retained before sampling. + +# Returns + +- Resolved poses, retained clearance matrix \\[m\\], and maximum translation + magnitude \\[m\\]. Input arrays are not modified. +""" +function clearance_geometry(designs, poses; + required = nothing, reference = poses, interface::Bool = false, + sampling::Bool = false, adjust::Bool = true, reference_centers = nothing) + exteriors = _clearance_exterior.(designs, poses) + radii = getproperty.(exteriors, :radius) + centers = getproperty.(exteriors, :center) + reference_centers === nothing && + (reference_centers = getproperty.(_clearance_exterior.(designs, reference), :center)) + n = length(radii) + length(poses) == length(reference) == n || throw(DimensionMismatch( + "clearance requires one pose per cable")) + T = eltype(eltype(poses)) + roundoff = _clearance_roundoff(radii, centers) + if required === nothing + minimum_gap = oftype(nominal(zero(T)), _CABLE_CLEARANCE) + minimum_gap < _CABLE_CLEARANCE && (minimum_gap = nextfloat(minimum_gap)) + radius_uncertainty = maximum(exterior -> exterior.reserve, exteriors) + required = zeros(T, n, n) + for i in 1:n + required[i, i] = minimum_gap + max(radius_uncertainty, + uncertainty(abs(centers[i].y) - radii[i])) + for j in 1:(i - 1) + distance = hypot(centers[i].x - centers[j].x, centers[i].y - centers[j].y) + clearance = minimum_gap + max(radius_uncertainty, + uncertainty(distance - radii[i] - radii[j])) + required[i, j] = required[j, i] = clearance + end + end + end + size(required) == (n, n) || throw(DimensionMismatch( + "clearance matrix must match the cable count")) + all(value -> isfinite(value) && nominal(value) >= oftype(nominal(value), _CABLE_CLEARANCE) && + iszero(uncertainty(value)), required) || throw(ArgumentError( + "retained clearances must be finite deterministic lengths of at least 1 μm")) + required == transpose(required) || throw(ArgumentError( + "retained cable clearances must be symmetric")) + + result = copy(centers) + if sampling && any(((i, j),) -> + iszero(nominal(result[i].x - result[j].x)) && + iszero(nominal(result[i].y - result[j].y)), + ((i, j) for i in 1:n for j in 1:(i - 1))) + # A coincident draw has no direction. Reuse the declared relative + # layout at the sampled center, then apply the same clearance rule. + cx = sum(p -> p.x, result) / n + cy = sum(p -> p.y, result) / n + rx = sum(p -> nominal(p.x), reference_centers) / n + ry = sum(p -> nominal(p.y), reference_centers) / n + result = [Pose2(cx + nominal(reference_centers[i].x) - rx, + cy + nominal(reference_centers[i].y) - ry, centers[i].φ) for i in 1:n] + end + factor = one(T) + for i in 1:n, j in 1:(i - 1) + distance = hypot(result[i].x - result[j].x, result[i].y - result[j].y) + gap = distance - radii[i] - radii[j] + if !adjust + nominal(gap) > 0 && nominal(gap) + roundoff >= nominal(required[i, j]) || + throw(DomainError((i, j), "cable exterior clearance is below its retained minimum")) + continue + end + !sampling && nominal(gap) < -roundoff && throw(DomainError((i, j), + "cable cross-sections overlap; only touching or insufficient positive clearance can be adjusted")) + nominal(distance) > 0 || throw(DomainError((i, j), + "coincident cable centers do not define a separation direction")) + if nominal(gap) + roundoff < nominal(required[i, j]) + required_scale = (radii[i] + radii[j] + required[i, j] + 2roundoff) / distance + nominal(required_scale) > nominal(factor) && (factor = required_scale) + end + end + if nominal(factor) > 1 + xcenter = sum(pose -> pose.x, result) / n + ycenter = sum(pose -> pose.y, result) / n + result = [Pose2(xcenter + factor * (pose.x - xcenter), + ycenter + factor * (pose.y - ycenter), pose.φ) for pose in result] + end + + if interface + # A common translation within each half-space leaves distances unchanged + # within that group and increases separation from the other group. + for side in (-1, 1) + indices = findall(pose -> sign(nominal(pose.y)) == side, reference_centers) + shift = zero(T) + for i in indices + original_gap = side * reference_centers[i].y - radii[i] + gap = side * result[i].y - radii[i] + adjust && !sampling && nominal(gap) + roundoff < nominal(required[i, i]) && + nominal(original_gap) < -roundoff && throw(DomainError(i, + "cable cross-section crosses the air-earth interface")) + if !adjust + nominal(gap) > 0 && nominal(gap) + roundoff >= nominal(required[i, i]) || + throw(DomainError(i, "cable clearance from the air-earth interface is below its retained minimum")) + elseif nominal(gap) + roundoff < nominal(required[i, i]) + required_shift = required[i, i] + 2roundoff - gap + nominal(required_shift) > nominal(shift) && (shift = required_shift) + end + end + if nominal(shift) > 0 + for i in indices + pose = result[i] + result[i] = Pose2(pose.x, pose.y + side * shift, pose.φ) + end + end + end + any(pose -> iszero(nominal(pose.y)), reference_centers) && throw(DomainError( + reference, "a declared cable center cannot lie on the air-earth interface")) + if adjust + # Extreme draws can exchange half-spaces before projection. If + # restoring their declared sides shortened a cross-interface + # distance, expansion about y=0 preserves the restored sides. + factor = one(T) + for i in 1:n, j in 1:(i - 1) + distance = hypot(result[i].x - result[j].x, result[i].y - result[j].y) + limit = radii[i] + radii[j] + required[i, j] + if nominal(distance) + roundoff < nominal(limit) + required_scale = (limit + 2roundoff) / distance + nominal(required_scale) > nominal(factor) && (factor = required_scale) + end + end + if nominal(factor) > 1 + xcenter = sum(p -> p.x, result) / n + result = [Pose2(xcenter + factor * (pose.x - xcenter), + factor * pose.y, pose.φ) for pose in result] + end + end + end + displacement = maximum(eachindex(result)) do i + hypot(nominal(result[i].x - centers[i].x), nominal(result[i].y - centers[i].y)) + end + resolved = iszero(displacement) ? copy(poses) : + [Pose2(poses[i].x + (result[i].x - centers[i].x), + poses[i].y + (result[i].y - centers[i].y), poses[i].φ) for i in 1:n] + if adjust && displacement > 0 + clearance_geometry(designs, resolved; required, reference, reference_centers, + interface, sampling, adjust = false) + end + return resolved, Matrix{T}(required), displacement +end + +function _record_clearance_adjustment(system_id, displacement) + displacement > 0 || return nothing + clearance = _CLEARANCE_CONTEXT[] + if clearance === nothing + @warn "Cable placements adjusted to preserve exterior clearance" system_id max_displacement_m=displacement + else + clearance.adjustments[] += 1 + clearance.max_displacement[] = max(clearance.max_displacement[], displacement) + end + return nothing +end + +""" +$(TYPEDSIGNATURES) + +Materialize one unresolved line-system point to retain its uncertainty-bearing +clearance requirements before sampling. Return task-scoped construction records +and adjustment counters. Do not consume the sampling RNG. +""" +function collect_clearance_requirements(point) + clearance = (records = Any[], references = IdDict{Any, Any}(), + cursor = Ref(0), sampling = Ref(false), + adjustments = Ref(0), max_displacement = Ref(0.0), declaration=Ref{Any}(nothing)) + clearance.declaration[] = Base.ScopedValues.with(_CLEARANCE_CONTEXT => clearance) do + if Base.get_extension(parentmodule(@__MODULE__), :LineCableModelsMeasurementsExt) === nothing + # Monte Carlo remains usable without Measurements. The geometric + # constraint still applies to every draw. Propagated uncertainty + # reserves are available when Measurements is loaded. + realize(point, realize_arguments(Random.Xoshiro(0), point, + (_rng, mean, _sigma) -> mean)) + else + materialize(point) + end + end + clearance.sampling[] = true + clearance.adjustments[] = 0 + clearance.max_displacement[] = 0.0 + return clearance +end + +""" +$(TYPEDSIGNATURES) + +Execute `f` while reconstructing one realization with recorded clearance +requirements. Return the result of `f` and restore the previous scope on exit. +""" +function with_clearance(f, clearance) + clearance === nothing && return f() + return Base.ScopedValues.with(_CLEARANCE_CONTEXT => clearance) do + clearance.cursor[] = 0 + empty!(clearance.references) + value = f() + clearance.cursor[] == length(clearance.records) || throw(ArgumentError( + "sampled construction produced fewer cable systems than its declaration")) + return value + end +end + +""" +$(TYPEDSIGNATURES) + +Return the number of adjusted builds and maximum translation \\[m\\] recorded +in the sampling record. A missing clearance returns zero counts and displacement. +""" +function clearance_summary(clearance) + clearance === nothing && return (adjustments = 0, max_displacement_m = 0.0) + return (adjustments = clearance.adjustments[], + max_displacement_m = clearance.max_displacement[]) +end + +""" +$(TYPEDSIGNATURES) + +Emit one aggregate warning if any sampled placements required adjustment. +Return `nothing`. No warning is emitted for an unchanged geometry. +""" +function warn_clearance_summary(clearance) + summary = clearance_summary(clearance) + summary.adjustments > 0 && + @warn "Sampled cable placements adjusted to preserve exterior clearance" summary... + return nothing +end + +""" +$(TYPEDSIGNATURES) + +Sample and build one line-system point with its retained clearance requirements. +Reuse enclosing clearance requirements or collect them for a standalone draw. +Return the completed system or problem. +""" +function realize_clearance(rng, point, distribution) + _CLEARANCE_CONTEXT[] === nothing || + return realize(point, realize_arguments(rng, point, distribution)) + clearance = collect_clearance_requirements(point) + try + return with_clearance(clearance) do + realize(point, realize_arguments(rng, point, distribution)) + end + finally + warn_clearance_summary(clearance) + end +end diff --git a/src/datamodel/linecablesystem/dataframe.jl b/src/datamodel/linecablesystem/dataframe.jl deleted file mode 100644 index c51cef710..000000000 --- a/src/datamodel/linecablesystem/dataframe.jl +++ /dev/null @@ -1,62 +0,0 @@ -import DataFrames: DataFrame - -""" -$(TYPEDSIGNATURES) - -Generates a summary DataFrame for cable positions and phase mappings within a [`LineCableSystem`](@ref). - -# Arguments - -- `system`: A [`LineCableSystem`](@ref) object containing the cable definitions and their configurations. - -# Returns - -- A `DataFrame` containing: - - `cable_id`: Identifier of each cable design. - - `horz`: Horizontal coordinate of each cable \\[m\\]. - - `vert`: Vertical coordinate of each cable \\[m\\]. - - `phase_mapping`: Human-readable string representation mapping each cable component to its assigned phase. - -# Examples - -```julia -df = $(FUNCTIONNAME)(cable_system) -println(df) -# Output: -# │ cable_id │ horz │ vert │ phase_mapping │ -# │------------│------│-------│-------------------------│ -# │ "Cable1" │ 0.0 │ -0.5 │ core: 1, sheath: 0 │ -# │ "Cable2" │ 0.35 │ -1.25 │ core: 2, sheath: 0 │ -``` - -# See also - -- [`LineCableSystem`](@ref) -- [`CablePosition`](@ref) -""" -function DataFrame(system::LineCableSystem)::DataFrame - cable_ids = String[] - horz_coords = Number[] - vert_coords = Number[] - mappings = String[] - - for cable_position in system.cables - push!(cable_ids, cable_position.design_data.cable_id) - push!(horz_coords, cable_position.horz) - push!(vert_coords, cable_position.vert) - - component_names = [comp.id for comp in cable_position.design_data.components] - mapping_str = join( - ["$(name): $(phase)" for (name, phase) in zip(component_names, cable_position.conn)], - ", ", - ) - push!(mappings, mapping_str) - end - data = DataFrame( - cable_id=cable_ids, - horz=horz_coords, - vert=vert_coords, - phase_mapping=mappings - ) - return data -end \ No newline at end of file diff --git a/src/datamodel/linecablesystem/linecablesystem.jl b/src/datamodel/linecablesystem/linecablesystem.jl new file mode 100644 index 000000000..59113cec6 --- /dev/null +++ b/src/datamodel/linecablesystem/linecablesystem.jl @@ -0,0 +1,459 @@ +""" +$(TYPEDEF) + +Store completed cable placements and their global terminal state. + +`designs`, `input_positions`, `connections`, and `environment` are declarations. +`positions` contains the resolved poses after automatic exterior-clearance +adjustment. Touching cables are separated by at least 1 μm plus the propagated +uncertainty reserve. Nominal designs overlapping beyond the roundoff allowance are rejected. +Global geometry, terminal order, terminal indices, and the flattened +connection order are derived by the constructor. + +$(TYPEDFIELDS) +""" +struct LineCableSystem{ + T <: Real, + D <: AbstractVector, + P <: AbstractVector{<:Pose2{T}}, + C <: AbstractVector, + E, + G <: AbstractVector +} + "Stable system identifier." + system_id::String + "Physical line length [m]." + line_length::T + "Completed cable designs in placement order." + designs::D + "Cable poses in the system frame." + positions::P + "Cable poses before automatic clearance adjustment \\[m, m, rad\\]." + input_positions::P + "Retained pairwise clearance requirements. Diagonal entries concern the interface \\[m\\]." + clearances::Matrix{T} + "Per-cable terminal connection declarations in terminal order." + connections::C + "Optional physical environment declaration." + environment::E + "Resolved cable regions placed in the system frame." + geometry::G + "Global `(cable, terminal)` records in deterministic order." + terminal_order::Vector{NamedTuple{(:cable, :terminal), Tuple{Int, Symbol}}} + "Global terminal index for every resolved system region." + terminal_map::Vector{Int} + "Active phase IDs aligned with `terminal_order`. Zero selects a conductor for elimination." + connection_order::Vector{Int} + + function LineCableSystem{T, D, P, C, E, G}( + system_id::String, + line_length::T, + designs::D, + positions::P, + connections::C, + environment::E, + geometry::G, + terminal_order::Vector{ + NamedTuple{(:cable, :terminal), Tuple{Int, Symbol}} + }, + terminal_map::Vector{Int}, + connection_order::Vector{Int}, + input_positions::P, + clearances::Matrix{T} + ) where { + T <: Real, + D <: AbstractVector, + P <: AbstractVector{<:Pose2{T}}, + C <: AbstractVector, + E, + G <: AbstractVector + } + return validate(new{T, D, P, C, E, G}( + system_id, + line_length, + designs, + positions, + input_positions, + clearances, + connections, + environment, + geometry, + terminal_order, + terminal_map, + connection_order + )) + end +end + +line_length(system::LineCableSystem) = system.line_length + +""" +$(TYPEDSIGNATURES) + +Return a system whose exterior circles satisfy their retained clearance from +the air-earth interface. Reuse the input when no adjustment is necessary. +This construction step is called when a line problem supplies the earth model. +""" +function interface_clearance(system::LineCableSystem) + reference = system.input_positions + context = _CLEARANCE_CONTEXT[] + sampling = context !== nothing && context.sampling[] + reference_centers = sampling ? get(context.references, system.clearances, nothing) : + nothing + positions, _, displacement = clearance_geometry(system.designs, system.positions; + required = system.clearances, reference, reference_centers, interface = true, sampling) + iszero(displacement) && return system + result = build(LineCableSystem, system.designs, positions, system.connections, + system.environment, system.system_id, system.line_length; + _clearances = system.clearances, _input_positions = system.input_positions, + _interface = true, _rebuild = true) + sampling && reference_centers !== nothing && + (context.references[result.clearances] = reference_centers) + _record_clearance_adjustment(system.system_id, displacement) + return result +end + +Base.eltype(::LineCableSystem{T}) where {T} = T +Base.eltype(::Type{<:LineCableSystem{T}}) where {T} = T + +function realize(rng::Random.AbstractRNG, point::Gridpoint{LineCableSystem}, distribution) + return realize_clearance(rng, point, distribution) +end + +ncables(system::LineCableSystem) = length(system.designs) +nphases(system::LineCableSystem) = length(unique(filter(>(0), system.connection_order))) + +function validate(system::LineCableSystem) + isempty(system.system_id) && throw(ArgumentError( + "LineCableSystem.system_id cannot be empty" + )) + isfinite(system.line_length) && system.line_length > zero(system.line_length) || + throw(DomainError( + system.line_length, + "LineCableSystem.line_length must be positive and finite" + )) + isempty(system.designs) && throw(ArgumentError( + "LineCableSystem.designs must contain at least one CableDesign" + )) + length(system.positions) == length(system.designs) || throw(DimensionMismatch( + "LineCableSystem.positions must contain one Pose2 per design; received " * + "$(length(system.positions)) positions for $(length(system.designs)) designs" + )) + length(system.input_positions) == length(system.designs) || throw(DimensionMismatch( + "LineCableSystem.input_positions must contain one pose per cable")) + length(system.connections) == length(system.designs) || throw(DimensionMismatch( + "LineCableSystem.connections must contain one declaration per design; " * + "received $(length(system.connections)) for $(length(system.designs)) designs" + )) + expected_terminals = sum(length(design.terminal_order) for design in system.designs) + length(system.terminal_order) == expected_terminals || throw(DimensionMismatch( + "LineCableSystem.terminal_order must contain $expected_terminals entries; " * + "received $(length(system.terminal_order))" + )) + length(system.connection_order) == expected_terminals || throw(DimensionMismatch( + "LineCableSystem.connection_order must contain $expected_terminals entries; " * + "received $(length(system.connection_order))" + )) + expected_regions = sum(length(design.geometry.regions) for design in system.designs) + length(system.geometry) == expected_regions || throw(DimensionMismatch( + "LineCableSystem.geometry must contain $expected_regions resolved regions; " * + "received $(length(system.geometry))" + )) + length(system.terminal_map) == expected_regions || throw(DimensionMismatch( + "LineCableSystem.terminal_map must contain $expected_regions entries; " * + "received $(length(system.terminal_map))" + )) + + terminal_index = 0 + region_index = 0 + terminal_offset = 0 + for (cable_index, design) in pairs(system.designs) + validate(design) + pose = system.positions[cable_index] + all(isfinite, (pose.x, pose.y, pose.φ)) || throw(DomainError( + (pose.x, pose.y, pose.φ), + "LineCableSystem.positions[$cable_index] must contain finite coordinates" + )) + connection = system.connections[cable_index] + connection isa AbstractVector{<:Integer} || throw(ArgumentError( + "LineCableSystem.connections[$cable_index] must be an integer vector; " * + "received $(typeof(connection))" + )) + length(connection) == length(design.terminal_order) || + throw(DimensionMismatch( + "LineCableSystem.connections[$cable_index] must contain " * + "$(length(design.terminal_order)) terminal assignments; received " * + "$(length(connection))" + )) + all(>=(0), connection) || throw(DomainError( + connection, + "LineCableSystem.connections[$cable_index] must contain nonnegative " * + "active phase IDs or zero" + )) + for (local_terminal, name) in pairs(design.terminal_order) + terminal_index += 1 + expected = (cable = cable_index, terminal = name) + system.terminal_order[terminal_index] == expected || + throw(DimensionMismatch( + "LineCableSystem.terminal_order[$terminal_index] must be " * + "$(repr(expected)); received " * + "$(repr(system.terminal_order[terminal_index]))" + )) + system.connection_order[terminal_index] == connection[local_terminal] || + throw(DimensionMismatch( + "LineCableSystem.connection_order[$terminal_index] must match " * + "connections[$cable_index][$local_terminal]" + )) + end + for (local_region, placed) in pairs(design.geometry.regions) + region_index += 1 + resolved = system.geometry[region_index] + resolved.source == placed.source || throw(DimensionMismatch( + "LineCableSystem.geometry[$region_index].source does not match " * + "designs[$cable_index].geometry.regions[$local_region].source" + )) + resolved.terminal === placed.terminal || throw(DimensionMismatch( + "LineCableSystem.geometry[$region_index].terminal does not match " * + "designs[$cable_index].geometry.regions[$local_region].terminal" + )) + local_index = design.terminal_map[local_region] + expected = iszero(local_index) ? 0 : terminal_offset + local_index + system.terminal_map[region_index] == expected || throw(DimensionMismatch( + "LineCableSystem.terminal_map[$region_index] must be $expected; " * + "received $(system.terminal_map[region_index])" + )) + end + terminal_offset += length(design.terminal_order) + end + + clearance_geometry(system.designs, system.positions; + required = system.clearances, interface = system.environment !== nothing, + adjust = false) + return system +end + +function build( + ::Type{LineCableSystem}, + designs, + placements, + connections, + environment, + system_id::AbstractString, + line_length::Real; + combine::Symbol = :product, + _clearances = nothing, + _input_positions = nothing, + _interface::Bool = environment !== nothing, + _rebuild::Bool = false +) + combine in (:product, :zip) || throw(ArgumentError( + "combine must be :product or :zip" + )) + declared_input = if designs isa CableDesign + typeof(designs)[designs] + elseif designs isa AbstractVector || designs isa Tuple + all(design -> design isa CableDesign, designs) || throw(ArgumentError( + "designs must contain completed CableDesign objects" + )) + collect(designs) + else + throw(ArgumentError("designs must be a CableDesign or a design collection")) + end + + # 1. Place every completed design in the system frame. Coordinate tuples + # are construction shorthand. The stored declaration is always Pose2. + isempty(declared_input) && throw(ArgumentError("a line system requires one cable")) + identifier = String(system_id) + isempty(identifier) && throw(ArgumentError("system_id cannot be empty")) + isfinite(line_length) && line_length > zero(line_length) || throw(DomainError( + line_length, + "line length must be positive" + )) + position_values = if placements isa Pose2 + Pose2[placements] + elseif placements isa Tuple && length(placements) in (2, 3) && + all(value -> value isa Real, placements) + Pose2[Pose2( + placements[1], + placements[2], + length(placements) == 3 ? placements[3] : 0 + )] + elseif placements isa AbstractVector || placements isa Tuple + Pose2[position isa Pose2 ? position : + position isa Tuple && length(position) in (2, 3) && + all(value -> value isa Real, position) ? + Pose2( + position[1], + position[2], + length(position) == 3 ? position[3] : 0 + ) : + throw(ArgumentError( + "placements must contain Pose2 values or `(x, y[, φ])` tuples" + )) + for position in placements] + else + throw(ArgumentError( + "placements must be a Pose2, coordinate tuple, or placement collection" + )) + end + declared_designs = if length(declared_input) == 1 && length(position_values) > 1 + fill(only(declared_input), length(position_values)) + else + declared_input + end + length(position_values) == length(declared_designs) || throw(DimensionMismatch( + "placement count must match the cable-design count" + )) + + T = promote_type( + typeof(float(line_length)), + (eltype(design) for design in declared_designs)..., + (eltype(position) for position in position_values)... + ) + poses = Pose2{T}[convert(Pose2{T}, position) for position in position_values] + original_poses = _input_positions === nothing ? copy(poses) : + Pose2{T}[convert(Pose2{T}, position) for position in _input_positions] + context = _CLEARANCE_CONTEXT[] + sampling = context !== nothing && context.sampling[] + reference = _rebuild ? poses : original_poses + reference_centers = nothing + if context !== nothing && !_rebuild && sampling + context.cursor[] += 1 + context.cursor[] <= length(context.records) || throw(ArgumentError( + "sampled construction produced more cable systems than its declaration")) + record = context.records[context.cursor[]] + record.system_id == identifier && + record.cables == getproperty.(declared_designs, :cable_id) || + throw(ArgumentError("sampled cable-system layout differs from its declaration")) + _clearances = record.clearances + reference = record.positions + reference_centers = record.centers + end + poses, clearances, displacement = clearance_geometry(declared_designs, poses; + required = _clearances, reference, reference_centers, interface = _interface, sampling) + if context !== nothing && !_rebuild && !sampling + push!(context.records, + (system_id = identifier, + cables = getproperty.(declared_designs, :cable_id), + clearances = nominal.(clearances), positions = original_poses, + centers = getproperty.(_clearance_exterior.(declared_designs, original_poses), :center))) + end + + # 2. Establish global primitive and terminal order while retaining cable + # and local terminal order verbatim. + terminal_order = NamedTuple{(:cable, :terminal), Tuple{Int, Symbol}}[] + terminal_offsets = Int[] + offset = 0 + for (cable_index, design) in enumerate(declared_designs) + push!(terminal_offsets, offset) + for terminal in design.terminal_order + push!(terminal_order, (cable = cable_index, terminal = terminal)) + offset += 1 + end + end + + # 3. Resolve connection definitions independently for every design. + declarations = if connections === nothing + fill(nothing, length(declared_designs)) + elseif length(declared_designs) == 1 && + (connections isa AbstractDict || connections isa NamedTuple || + connections isa AbstractVector{<:Integer}) + Any[connections] + elseif connections isa AbstractVector || connections isa Tuple + length(connections) == length(declared_designs) || throw(DimensionMismatch( + "connection declaration count must match the cable-design count" + )) + collect(connections) + else + throw(ArgumentError( + "connections must be one declaration per cable design" + )) + end + normalized_connections = Vector{Int}[] + for (cable_index, (design, declaration)) in enumerate(zip(declared_designs, declarations)) + names = design.terminal_order + values = if declaration === nothing + [index == 1 ? cable_index : 0 for index in eachindex(names)] + elseif declaration isa AbstractVector{<:Integer} + length(declaration) == length(names) || throw(DimensionMismatch( + "connection vector for cable $cable_index must match its terminal count" + )) + Int[declaration...] + elseif declaration isa NamedTuple + unknown = setdiff(collect(keys(declaration)), names) + isempty(unknown) || throw(KeyError(first(unknown))) + Int[Int(get(declaration, name, 0)) for name in names] + elseif declaration isa AbstractDict + normalized = Dict{Symbol, Int}() + for (name, value) in declaration + key = Symbol(name) + key in names || throw(KeyError(key)) + value isa Integer || throw(ArgumentError( + "connection assignments must be integers" + )) + normalized[key] = Int(value) + end + Int[get(normalized, name, 0) for name in names] + else + throw(ArgumentError( + "each connection declaration must be a mapping or integer vector" + )) + end + all(>=(0), values) || throw(DomainError( + values, + "connection assignments must contain nonnegative active phase IDs or zero" + )) + push!(normalized_connections, values) + end + connection_order = collect(Iterators.flatten(normalized_connections)) + + # 4. Place the resolved cable geometry. An explicit environment owns any + # interface constraint. Formulation-specific media checks occur in the + # problem that consumes the system. + global_geometry = PlacedRegion[] + terminal_map = Int[] + for (cable_index, (design, pose)) in enumerate(zip(declared_designs, poses)) + for (source, local_terminal) in zip( + design.geometry.regions, + design.terminal_map + ) + push!(global_geometry, resolve(pose, source)) + push!( + terminal_map, + iszero(local_terminal) ? 0 : + terminal_offsets[cable_index] + local_terminal + ) + end + end + # 5. Freeze declarations and their completed ordering together. + system = LineCableSystem{ + T, + typeof(declared_designs), + typeof(poses), + typeof(normalized_connections), + typeof(environment), + typeof(global_geometry) + }( + identifier, + convert(T, float(line_length)), + declared_designs, + poses, + normalized_connections, + environment, + global_geometry, + terminal_order, + terminal_map, + connection_order, + original_poses, + clearances + ) + if sampling && !_rebuild + context.references[system.clearances] = reference_centers + end + _record_clearance_adjustment(identifier, displacement) + return system +end + +Commons.input_fields(::Type{<:LineCableSystem}) = (line_length=(name="line length",unit="m"), + positions=(name="position",unit="m"),input_positions=(name="input position",unit="m"), + clearances=(name="clearance",unit="m")) diff --git a/src/datamodel/macros.jl b/src/datamodel/macros.jl deleted file mode 100644 index 6e90425c0..000000000 --- a/src/datamodel/macros.jl +++ /dev/null @@ -1,184 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Determines the promoted numeric element type for convenience constructors of component `C`. The promotion is computed across the values of `coercive_fields(C)`, extracted from the normalized `NamedTuple` `ntv` produced by [`validate!`](@ref). This ensures all numeric fields that participate in calculations share a common element type (e.g., `Float64`, `Measurement{Float64}`). - -# Arguments - -- `::Type{C}`: Component type \\[dimensionless\\]. -- `ntv`: Normalized `NamedTuple` returned by `validate!` \\[dimensionless\\]. -- `_order::Tuple`: Ignored by this method; present for arity symmetry with `_coerced_args` \\[dimensionless\\]. - -# Returns - -- The promoted numeric element type \\[dimensionless\\]. - -# Examples - -```julia -Tp = $(FUNCTIONNAME)(Tubular, (r_in=0.01, r_ex=0.02, material_props=mat, temperature=20.0), ()) -``` -""" -@inline _promotion_T(::Type{C}, ntv, _order::Tuple) where {C} = - resolve_T((getfield(ntv, k) for k in coercive_fields(C))...) - -""" -$(TYPEDSIGNATURES) - -Builds the positional argument tuple to feed the **typed core** constructor, coercing only the fields returned by `coercive_fields(C)` to type `Tp`. Non‑coercive fields (e.g., integer flags) are passed through unchanged. Field order is controlled by `order` (a tuple of symbols), typically `(required_fields(C)..., keyword_fields(C)...)`. - -# Arguments - -- `::Type{C}`: Component type \\[dimensionless\\]. -- `ntv`: Normalized `NamedTuple` returned by `validate!` \\[dimensionless\\]. -- `Tp`: Target element type for numeric coercion \\[dimensionless\\]. -- `order::Tuple`: Field order used to assemble the positional tuple \\[dimensionless\\]. - -# Returns - -- A `Tuple` of arguments in the requested order, with coercions applied where configured. - -# Examples - -```julia -args = $(FUNCTIONNAME)(Tubular, ntv, Float64, (:r_in, :r_ex, :material_props, :temperature)) -``` -""" -@inline _coerced_args(::Type{C}, ntv, Tp, order::Tuple) where {C} = - tuple(( - let k = s, v = getfield(ntv, s) - (s in coercive_fields(C)) ? coerce_to_T(v, Tp) : v - end - for s in order - )...) - -""" -$(TYPEDSIGNATURES) - -Utility for the constructor macro to *materialize* input tuples from either: - -- A tuple literal expression (e.g., `(:a, :b, :c)`), or -- A bound constant tuple name (e.g., `_REQ_TUBULAR`). - -Used to keep macro call sites short while allowing both styles. - -# Arguments - -- `mod`: Module where constants are resolved \\[dimensionless\\]. -- `x`: Expression or symbol representing a tuple \\[dimensionless\\]. - -# Returns - -- A standard Julia `Tuple` (of symbols or defaults). - -# Errors - -- `ErrorException` if `x` is neither a tuple literal nor a bound constant name. - -# Examples - -```julia -syms = $(FUNCTIONNAME)(@__MODULE__, :( :a, :b )) -syms = $(FUNCTIONNAME)(@__MODULE__, :_REQ_TUBULAR) -``` -""" -_ctor_materialize(mod, x) = - x === :(()) ? () : - x isa Expr && x.head === :tuple ? x.args : - x isa Symbol ? Base.eval(mod, x) : - Base.error("@construct: expected tuple literal or const tuple, got $(x)") - -using MacroTools: postwalk -""" -$(TYPEDSIGNATURES) - -Generates a weakly‑typed convenience constructor for a component `T`. The generated method: - -1. Accepts exactly the positional fields listed in `REQ`. -2. Accepts keyword arguments listed in `OPT` with defaults `DEFS`. -3. Calls `validate!(T, ...)` forwarding **variables** (not defaults), -4. Computes the promotion type via `_promotion_T(T, ntv, order)`, -5. Coerces only `coercive_fields(T)` via `_coerced_args(T, ntv, Tp, order)`, -6. Delegates to the numeric core `T(...)` with the coerced positional tuple. - -`REQ`, `OPT`, and `DEFS` can be provided as tuple literals or as names of bound constant tuples. `order` is implicitly `(REQ..., OPT...)`. - -# Arguments - -- `T`: Component type (bare name) \\[dimensionless\\]. -- `REQ`: Tuple of required positional field names \\[dimensionless\\]. -- `OPT`: Tuple of optional keyword field names \\[dimensionless\\]. Defaults to `()`. -- `DEFS`: Tuple of default values matching `OPT` \\[dimensionless\\]. Defaults to `()`. - -# Returns - -- A method definition for the weakly‑typed constructor. - -# Examples - -```julia -const _REQ_TUBULAR = (:r_in, :r_ex, :material_props) -const _OPT_TUBULAR = (:temperature,) -const _DEFS_TUBULAR = (T₀,) - -@construct Tubular _REQ_TUBULAR _OPT_TUBULAR _DEFS_TUBULAR - -# Expands roughly to: -# function Tubular(r_in, r_ex, material_props; temperature=T₀) -# ntv = validate!(Tubular, r_in, r_ex, material_props; temperature=temperature) -# Tp = _promotion_T(Tubular, ntv, (:r_in, :r_ex, :material_props, :temperature)) -# args = _coerced_args(Tubular, ntv, Tp, (:r_in, :r_ex, :material_props, :temperature)) -# return Tubular(args...) -# end -``` - -# Notes - -- Defaults supplied in `DEFS` are **escaped** into the method signature (evaluated at macro expansion time). -- Forwarding into `validate!` always uses *variables* (e.g., `temperature=temperature`), never literal defaults. -- The macro is hygiene‑aware; identifiers `validate!`, `_promotion_T`, `_coerced_args`, and the type name are properly escaped. - -# Errors - -- `ErrorException` if `length(OPT) != length(DEFS)`. -""" -macro construct(T, REQ, OPT = :(()), DEFS = :(())) - mod = __module__ - req = Symbol.(_ctor_materialize(mod, REQ)) - opt = Symbol.(_ctor_materialize(mod, OPT)) - dfx = _ctor_materialize(mod, DEFS) - length(opt) == length(dfx) || Base.error("@construct: OPT and DEFS length mismatch") - - # A) signature defaults (escape defaults) - sig_kws = [Expr(:kw, opt[i], esc(dfx[i])) for i in eachindex(opt)] - # forwarding kwargs (variables, not defaults) - pass_kws = [Expr(:kw, s, s) for s in opt] - - # B) flat order tuple - order_syms = (req..., opt...) - order = Expr(:tuple, (QuoteNode.(order_syms))...) - - ex = - isempty(sig_kws) ? quote - function $(T)($(req...)) - ntv = validate!($(T), $(req...)) - Tp = _promotion_T($(T), ntv, $order) - local __args__ = _coerced_args($(T), ntv, Tp, $order) - return $(T)(__args__...) - end - end : quote - function $(T)($(req...); $(sig_kws...)) - ntv = validate!($(T), $(req...); $(pass_kws...)) # C) pass vars - Tp = _promotion_T($(T), ntv, $order) - local __args__ = _coerced_args($(T), ntv, Tp, $order) - return $(T)(__args__...) - end - end - - # hygiene stays as you had it - free = Set{Symbol}([:validate!, :_promotion_T, :_coerced_args, T]) - ex2 = postwalk(ex) do node - node isa Symbol && (node in free) ? esc(node) : node - end - return ex2 -end diff --git a/src/datamodel/nominaldata.jl b/src/datamodel/nominaldata.jl deleted file mode 100644 index 4e75ece08..000000000 --- a/src/datamodel/nominaldata.jl +++ /dev/null @@ -1,96 +0,0 @@ - -""" -$(TYPEDEF) - -Stores nominal electrical and geometric parameters for a cable design. - -$(TYPEDFIELDS) -""" -struct NominalData{T<:REALSCALAR} - "Cable designation as per DIN VDE 0271/0276." - designation_code::Union{Nothing,String} - "Rated phase-to-earth voltage \\[kV\\]." - U0::Union{Nothing,T} - "Rated phase-to-phase voltage \\[kV\\]." - U::Union{Nothing,T} - "Cross-sectional area of the conductor \\[mm²\\]." - conductor_cross_section::Union{Nothing,T} - "Cross-sectional area of the screen \\[mm²\\]." - screen_cross_section::Union{Nothing,T} - "Cross-sectional area of the armor \\[mm²\\]." - armor_cross_section::Union{Nothing,T} - "Base (DC) resistance of the cable core \\[Ω/km\\]." - resistance::Union{Nothing,T} - "Capacitance of the main insulation \\[μF/km\\]." - capacitance::Union{Nothing,T} - "Inductance of the cable (trifoil formation) \\[mH/km\\]." - inductance::Union{Nothing,T} - - # --- Tight / typed kernel: assumes values already coerced to T (or nothing) - @inline function NominalData{T}(; - designation_code::Union{Nothing,String}=nothing, - U0::Union{Nothing,T}=nothing, - U::Union{Nothing,T}=nothing, - conductor_cross_section::Union{Nothing,T}=nothing, - screen_cross_section::Union{Nothing,T}=nothing, - armor_cross_section::Union{Nothing,T}=nothing, - resistance::Union{Nothing,T}=nothing, - capacitance::Union{Nothing,T}=nothing, - inductance::Union{Nothing,T}=nothing, - ) where {T<:REALSCALAR} - new{T}( - designation_code, - U0, - U, - conductor_cross_section, - screen_cross_section, - armor_cross_section, - resistance, - capacitance, - inductance, - ) - end -end - -""" -$(TYPEDSIGNATURES) - -Weakly-typed constructor that infers the target scalar type `T` from the **provided numeric kwargs** (ignoring `nothing` and the string designation), coerces numerics to `T`, and calls the strict kernel. - -If no numeric kwargs are provided, it defaults to `Float64`. -""" -@inline function NominalData(; - designation_code::Union{Nothing,String}=nothing, - U0::Union{Nothing,Number}=nothing, - U::Union{Nothing,Number}=nothing, - conductor_cross_section::Union{Nothing,Number}=nothing, - screen_cross_section::Union{Nothing,Number}=nothing, - armor_cross_section::Union{Nothing,Number}=nothing, - resistance::Union{Nothing,Number}=nothing, - capacitance::Union{Nothing,Number}=nothing, - inductance::Union{Nothing,Number}=nothing, -) - # collect provided numerics (skip `nothing`) - nums = Tuple(x for x in - (U0, U, conductor_cross_section, screen_cross_section, armor_cross_section, - resistance, capacitance, inductance) if x !== nothing) - - # infer T from numerics, fallback to Float64 if none - T = isempty(nums) ? Float64 : resolve_T(nums...) - - return NominalData{T}(; - designation_code=designation_code, - U0=(U0 === nothing ? nothing : coerce_to_T(U0, T)), - U=(U === nothing ? nothing : coerce_to_T(U, T)), - conductor_cross_section=(conductor_cross_section === nothing ? nothing : coerce_to_T(conductor_cross_section, T)), - screen_cross_section=(screen_cross_section === nothing ? nothing : coerce_to_T(screen_cross_section, T)), - armor_cross_section=(armor_cross_section === nothing ? nothing : coerce_to_T(armor_cross_section, T)), - resistance=(resistance === nothing ? nothing : coerce_to_T(resistance, T)), - capacitance=(capacitance === nothing ? nothing : coerce_to_T(capacitance, T)), - inductance=(inductance === nothing ? nothing : coerce_to_T(inductance, T)), - ) -end - -include("nominaldata/base.jl") - - diff --git a/src/datamodel/nominaldata/base.jl b/src/datamodel/nominaldata/base.jl deleted file mode 100644 index 035a933e6..000000000 --- a/src/datamodel/nominaldata/base.jl +++ /dev/null @@ -1,4 +0,0 @@ - -# Scalar-type query -Base.eltype(::NominalData{T}) where {T} = T -Base.eltype(::Type{NominalData{T}}) where {T} = T \ No newline at end of file diff --git a/src/datamodel/placement/bounded.jl b/src/datamodel/placement/bounded.jl new file mode 100644 index 000000000..6b9a94bc2 --- /dev/null +++ b/src/datamodel/placement/bounded.jl @@ -0,0 +1,853 @@ +""" +$(TYPEDEF) + +Record the formation boundary and radial course of one resolved member. + +The geometric boundary identifies the complete formation while `course` records the +inferred radial course. Course zero identifies the center strand of a circular +bundle or of one sector segment. + +$(TYPEDFIELDS) +""" +struct BoundedPlacement{B <: AbstractShape} + "Resolved geometric boundary shared by every member of the formation." + boundary::B + "Inferred radial course. Zero denotes a center member." + course::Int + + function BoundedPlacement(boundary::B, course::Integer) where { + B <: AbstractShape + } + course >= 0 || throw(DomainError( + course, "bounded-placement course must be nonnegative" + )) + return new{B}(boundary, Int(course)) + end +end + +function resolve(at::Pose2, region::PlacedRegion) + patterns = map(region.placement.patterns) do entry + pattern = entry.pattern isa BoundedPlacement ? + BoundedPlacement( + resolve(at, entry.pattern.boundary), entry.pattern.course + ) : entry.pattern + merge(entry, (pattern = pattern,)) + end + return PlacedRegion( + region.source, + resolve(at, region.primitive), + region.terminal, + (patterns = patterns,), + region.paths + ) +end + +""" +$(TYPEDEF) + +Store the exact annular deformation of one rectangular strip. + +Resolve a [`Rectangle`](@ref) onto a circular course, preserving its area and +bending its radial faces to follow that course. + +For angular coverage ``\\Delta\\phi`` and radial limits ``r_i`` and ``r_o``, + +```math +A = \\frac{\\Delta\\phi}{2}\\left(r_o^2-r_i^2\\right) = w h, +``` + +where ``w h`` is the area of the source rectangle. + +$(TYPEDFIELDS) +""" +struct BentStrip{T <: Real, P <: Pose2{T}} <: AbstractShape{T} + "Inner course radius \\[m\\]." + ri::T + "Outer course radius \\[m\\]." + ro::T + "Angular coverage \\[rad\\]." + span::T + "Resolved pose of the strip symmetry axis." + at::P + + function BentStrip{T, P}(ri::T, ro::T, span::T, at::P) where { + T <: Real, P <: Pose2{T} + } + isfinite(ri) && ri >= zero(ri) || throw(DomainError( + ri, "bent-strip inner radius must be nonnegative and finite" + )) + isfinite(ro) && ro > ri || throw(DomainError( + ro, "bent-strip outer radius must exceed its inner radius" + )) + isfinite(span) && zero(span) < span <= oftype(span, 2pi) || + throw(DomainError(span, "bent-strip span must lie in (0, 2pi]")) + return new{T, P}(ri, ro, span, at) + end +end + +function BentStrip(ri::Real, ro::Real, span::Real, at::Pose2 = Pose2(0, 0, 0)) + values = map(float, promote(ri, ro, span, at.x, at.y, at.φ)) + inner, outer, angle, x, y, rotation = values + pose = Pose2(x, y, rotation) + return BentStrip{typeof(inner), typeof(pose)}(inner, outer, angle, pose) +end + +area(shape::BentStrip) = shape.span * (shape.ro^2 - shape.ri^2) / 2 +perimeter(shape::BentStrip) = + shape.span * (shape.ro + shape.ri) + 2(shape.ro - shape.ri) +r_in(shape::BentStrip) = shape.ri +r_ex(shape::BentStrip) = shape.ro +thickness(shape::BentStrip) = shape.ro - shape.ri +boundary(shape::BentStrip) = shape + +function centroid(shape::BentStrip) + radius = isapprox(shape.span, oftype(shape.span, 2pi)) ? zero(shape.span) : + 4sin(shape.span / 2) * (shape.ro^3 - shape.ri^3) / + (3shape.span * (shape.ro^2 - shape.ri^2)) + return ( + shape.at.x + radius * cos(shape.at.φ), + shape.at.y + radius * sin(shape.at.φ) + ) +end + +function support(shape::BentStrip, φ::Real) + direction = φ - shape.at.φ + half = shape.span / 2 + endpoint_projections = ( + shape.ro * cos(direction - half), + shape.ro * cos(direction + half), + shape.ri * cos(direction - half), + shape.ri * cos(direction + half) + ) + period = oftype(direction, 2pi) + wrapped = mod(direction + pi, period) - pi + radial = abs(wrapped) <= half ? shape.ro : maximum(endpoint_projections) + return shape.at.x * cos(φ) + shape.at.y * sin(φ) + radial +end + +support(shape::BentStrip) = hypot(shape.at.x, shape.at.y) + shape.ro + +function resolve(at::Pose2, shape::BentStrip) + return BentStrip(shape.ri, shape.ro, shape.span, at * shape.at) +end + +function tessellate(shape::BentStrip; points_per_arc::Integer = 32) + points_per_arc >= 2 || throw(ArgumentError( + "points_per_arc must be at least two" + )) + outer = range(-shape.span / 2, shape.span / 2; length = Int(points_per_arc)) + inner = reverse(outer) + local_points = [ + (shape.ro * cos(angle), shape.ro * sin(angle)) for angle in outer + ] + append!(local_points, + [(shape.ri * cos(angle), shape.ri * sin(angle)) for angle in inner]) + cosine = cos(shape.at.φ) + sine = sin(shape.at.φ) + return [ + ( + shape.at.x + cosine * point[1] - sine * point[2], + shape.at.y + sine * point[1] + cosine * point[2] + ) for point in local_points + ] +end + +""" +Return a counter-clockwise polygonal approximation of a resolved geometric boundary. +""" +function boundary_polygon(shape::Disk; points::Integer = 512) + points >= 16 || throw(ArgumentError( + "a disk boundary polygon requires at least 16 points" + )) + center = (shape.at.x, shape.at.y) + angles = range(zero(shape.r), oftype(shape.r, 2π); length = Int(points) + 1) + polygon = [(center[1] + shape.r * cos(angle), center[2] + shape.r * sin(angle)) + for angle in Iterators.drop(angles, 1)] + return polygon +end + +function boundary_polygon(shape::SectorShape; points::Integer = 64) + polygon = collect(tessellate(shape; points_per_arc = points)) + length(polygon) > 1 && + isapprox(first(polygon)[1], last(polygon)[1]) && + isapprox(first(polygon)[2], last(polygon)[2]) && pop!(polygon) + return signed_polygon_area(polygon) < 0 ? reverse(polygon) : polygon +end + +function signed_polygon_area(points) + return sum(eachindex(points); init = zero(first(points)[1])) do index + next = mod1(index + 1, length(points)) + points[index][1] * points[next][2] - points[next][1] * points[index][2] + end / 2 +end + +function polygon_centroid(points) + signed_area = signed_polygon_area(points) + abs(signed_area) > eps(float(max(abs(signed_area), one(signed_area)))) || + throw(ArgumentError("a compaction polygon must have positive area")) + xmoment = zero(signed_area) + ymoment = zero(signed_area) + for index in eachindex(points) + next = mod1(index + 1, length(points)) + cross = points[index][1] * points[next][2] - + points[next][1] * points[index][2] + xmoment += (points[index][1] + points[next][1]) * cross + ymoment += (points[index][2] + points[next][2]) * cross + end + return (xmoment / (6signed_area), ymoment / (6signed_area)) +end + +function normalize_polygon_area(points, target_area::Real, center) + polygon = signed_polygon_area(points) < 0 ? reverse(points) : points + current_area = signed_polygon_area(polygon) + current_area > zero(current_area) || throw(ArgumentError( + "a bounded formation requires a counter-clockwise positive-area boundary" + )) + factor = sqrt(target_area / current_area) + return [ + ( + center[1] + factor * (point[1] - center[1]), + center[2] + factor * (point[2] - center[2]) + ) for point in polygon + ] +end + +function clip_halfplane(points, normal, offset) + isempty(points) && return copy(points) + T = promote_type(typeof(first(points)[1]), typeof(first(points)[2]), + typeof(normal[1]), typeof(normal[2]), typeof(offset)) + return clip_halfplane!(Tuple{T,T}[], points, normal, offset) +end + +function clip_halfplane!(output, points, normal, offset) + empty!(output) + isempty(points) && return output + scale = max(abs(offset), one(offset)) + tolerance = 128eps(float(scale)) * scale + previous = last(points) + previous_value = normal[1] * previous[1] + normal[2] * previous[2] - offset + previous_inside = previous_value <= tolerance + for current in points + current_value = normal[1] * current[1] + normal[2] * current[2] - offset + current_inside = current_value <= tolerance + if current_inside != previous_inside + denominator = previous_value - current_value + fraction = iszero(denominator) ? zero(denominator) : + clamp(previous_value / denominator, + zero(denominator), one(denominator)) + push!(output, + ( + previous[1] + fraction * (current[1] - previous[1]), + previous[2] + fraction * (current[2] - previous[2]) + )) + end + current_inside && push!(output, current) + previous = current + previous_value = current_value + previous_inside = current_inside + end + return output +end + +function power_cell(boundary, sites, weights, index::Int) + cell = copy(boundary) + site = sites[index] + for neighbour in eachindex(sites) + neighbour == index && continue + other = sites[neighbour] + dx = other[1] - site[1] + dy = other[2] - site[2] + iszero(dx) && iszero(dy) && throw(ArgumentError( + "bounded compaction requires distinct initial sites" + )) + offset = other[1]^2 + other[2]^2 - site[1]^2 - site[2]^2 + + weights[index] - weights[neighbour] + cell = clip_halfplane(cell, (2dx, 2dy), offset) + isempty(cell) && break + end + return cell +end + +power_cells(boundary, sites, weights) = + [power_cell(boundary, sites, weights, index) for index in eachindex(sites)] + +function shared_face_length(cell, left, right, left_weight, right_weight, scale) + dx = right[1] - left[1] + dy = right[2] - left[2] + distance = hypot(dx, dy) + iszero(distance) && return zero(distance) + offset = right[1]^2 + right[2]^2 - left[1]^2 - left[2]^2 + + left_weight - right_weight + tolerance = 2048eps(float(scale)) * max(scale, one(scale)) + projections = typeof(float(scale))[] + for point in cell + residual = abs(2dx * point[1] + 2dy * point[2] - offset) / (2distance) + residual <= tolerance || continue + push!(projections, (-dy * point[1] + dx * point[2]) / distance) + end + length(projections) >= 2 || return zero(distance) + return maximum(projections) - minimum(projections) +end + +function weight_hessian(cells, sites, weights, scale) + count = length(sites) + matrix = zeros(typeof(float(scale)), count, count) + for left in 1:(count - 1), right in (left + 1):count + distance = hypot( + sites[right][1] - sites[left][1], + sites[right][2] - sites[left][2] + ) + face = shared_face_length( + cells[left], sites[left], sites[right], + weights[left], weights[right], scale + ) + iszero(face) && continue + derivative = face / (2distance) + matrix[left, left] += derivative + matrix[right, right] += derivative + matrix[left, right] -= derivative + matrix[right, left] -= derivative + end + return matrix +end + +function balance_power_cells(boundary, sites, targets; maxiter::Integer = 50, + rtol::Real = 1.0e-11) + count = length(sites) + count == length(targets) || throw(DimensionMismatch( + "power-cell sites and target areas must have equal lengths" + )) + count == 1 && return ([copy(boundary)], zeros(typeof(first(targets)), 1)) + origin = polygon_centroid(boundary) + length_scale = sqrt(sum(targets)) + boundary = [((point[1] - origin[1]) / length_scale, + (point[2] - origin[2]) / length_scale) for point in boundary] + sites = [((point[1] - origin[1]) / length_scale, + (point[2] - origin[2]) / length_scale) for point in sites] + targets = targets ./ length_scale^2 + nominal_boundary = [nominal.(point) for point in boundary] + nominal_sites = [nominal.(point) for point in sites] + nominal_targets = nominal.(targets) + cells, weights = _balance_power_cells_nominal( + nominal_boundary, nominal_sites, nominal_targets; maxiter, rtol + ) + uncertain = any(point -> any(x -> !iszero(uncertainty(x)), point), boundary) || + any(point -> any(x -> !iszero(uncertainty(x)), point), sites) || + any(x -> !iszero(uncertainty(x)), targets) + if uncertain + # The area constraint, not Newton's iteration history, defines the + # sensitivity. Fix the last weight to zero and use the true Jacobian. + scale = maximum(point -> hypot(point...), nominal_boundary) + hessian = weight_hessian(cells, nominal_sites, weights, scale) + fixed_cells = power_cells(boundary, sites, weights) + residual = targets .- signed_polygon_area.(fixed_cells) + correction = LinearAlgebra.lu(hessian[1:(end - 1), 1:(end - 1)]) \ + (residual[1:(end - 1)] .- nominal.(residual[1:(end - 1)])) + weights = weights .+ [correction; zero(first(correction))] + cells = power_cells(boundary, sites, weights) + end + resolved = [[(origin[1] + length_scale * point[1], + origin[2] + length_scale * point[2]) for point in cell] + for cell in cells] + return (resolved, weights .* length_scale^2) +end + +function _balance_power_cells_nominal(boundary, sites, targets; + maxiter::Integer = 50, rtol::Real = 1.0e-11) + count = length(sites) + scale = maximum(point -> hypot(point...), boundary) + weights = zeros(typeof(float(first(targets))), count) + best_error = oftype(first(targets), Inf) + for _ in 1:Int(maxiter) + cells = power_cells(boundary, sites, weights) + any(cell -> length(cell) < 3, cells) && throw(ArgumentError( + "bounded compaction produced an empty power cell" + )) + areas = signed_polygon_area.(cells) + residual = targets .- areas + relative_error = maximum(abs.(residual) ./ targets) + if relative_error <= rtol + return (cells, weights) + end + if relative_error < best_error + best_error = relative_error + end + + hessian = weight_hessian(cells, sites, weights, scale) + reduced = hessian[1:(end - 1), 1:(end - 1)] + regularization = eps(float(scale^2)) / max(scale^2, eps(float(scale^2))) + for index in axes(reduced, 1) + reduced[index, index] += regularization + end + step = try + LinearAlgebra.qr(reduced) \ residual[1:(end - 1)] + catch + (scale^2 / sum(targets)) .* residual[1:(end - 1)] + end + + accepted = false + damping = one(eltype(weights)) + for _ in 1:10 + candidate = copy(weights) + candidate[1:(end - 1)] .+= damping .* step + candidate .-= candidate[end] + candidate_cells = power_cells(boundary, sites, candidate) + if all(cell -> length(cell) >= 3, candidate_cells) + candidate_areas = signed_polygon_area.(candidate_cells) + candidate_error = maximum(abs.((targets .- candidate_areas) ./ targets)) + if candidate_error < relative_error + weights = candidate + accepted = true + break + end + end + damping /= 2 + end + if !accepted + weights .+= (scale^2 / sum(targets)) .* residual ./ 4 + weights .-= weights[end] + end + end + throw(ArgumentError( + "bounded compaction did not converge; maximum relative cell-area error " * + "was $(best_error), target is $rtol" + )) +end + +function compact_power_cells(boundary, sites, targets; relaxations::Integer = 1) + current_sites = copy(sites) + cells = Vector{eltype(boundary)}[] + for pass in 1:Int(relaxations) + cells, _ = balance_power_cells(boundary, current_sites, targets) + pass == relaxations && break + current_sites = polygon_centroid.(cells) + end + return cells +end + +function course_count(available_area, strand_area) + ratio = nominal(available_area / strand_area) + capacity = ratio + 64eps(float(ratio)) + capacity >= 6 || throw(DomainError( + available_area, + "a stranded formation requires enough area for its first six-wire course" + )) + courses = floor(Int, (sqrt(1 + 4ratio / 3) - 1) / 2) + # The inverse can round below an integer at exact full occupancy. Check + # the actual inventory in both directions, using the same area tolerance. + while 3(courses + 1) * (courses + 2) <= capacity + courses += 1 + end + while 3courses * (courses + 1) > capacity + courses -= 1 + end + return courses +end + +function circular_courses(shape::Disk, center::Disk, wire::Disk, compact::Bool) + courses = if compact + course_count(area(shape) - area(center), area(wire)) + else + ratio = nominal((shape.r - center.r) / (2wire.r)) + floor(Int, ratio + 64eps(float(ratio))) + end + courses >= 1 || throw(DomainError( + wire.r, "the disk boundary admits no complete six-wire course" + )) + source_area = area(center) + 3courses * (courses + 1) * area(wire) + sites = NamedTuple[] + for course in 1:courses + count = 6course + radius = if compact + area_before = area(center) + 3(course - 1) * course * area(wire) + area_middle = area_before + 3course * area(wire) + shape.r * sqrt(area_middle / source_area) + else + center.r + (2course - 1) * wire.r + end + phase = isodd(course) ? zero(radius) : π / count + for member in 1:count + angle = phase + 2π * (member - 1) / count + push!(sites, ( + site = ( + shape.at.x + radius * cos(angle), + shape.at.y + radius * sin(angle) + ), + course, + member, + angle + )) + end + end + return sites +end + +function rectangular_strands(shape::Disk, center::Disk, strand::Rectangle) + remaining_area = area(shape) - area(center) + remaining_area > zero(remaining_area) || throw(DomainError( + center.r, "the center wire leaves no area for rectangular strands" + )) + count = floor(Int, nominal(remaining_area / area(strand)) + + 64eps(float(nominal(remaining_area / area(strand))))) + count > 0 || throw(DomainError( + area(strand), "one rectangular strand does not fit outside the center wire" + )) + result = NamedTuple[] + inner = center.r + remaining = count + course = 0 + while remaining > 0 + course += 1 + mean_radius = inner + strand.h / 2 + nominal_count = max(1, floor(Int, nominal(2pi * mean_radius / strand.w))) + member_count = min(remaining, nominal_count) + outer = sqrt(inner^2 + member_count * area(strand) / pi) + if outer > shape.r + member_count = min( + remaining, + floor(Int, nominal(pi * (shape.r^2 - inner^2) / area(strand))) + ) + member_count > 0 || break + outer = sqrt(inner^2 + member_count * area(strand) / pi) + end + span = 2pi / member_count + for member in 1:member_count + angle = (member - 1) * span + pose = Pose2(shape.at.x, shape.at.y, shape.at.φ + angle) + primitive = member_count == 1 ? Annulus(inner, outer, pose) : + BentStrip(inner, outer, span, pose) + push!(result, ( + primitive, + site = centroid(primitive), + course, + member + )) + end + inner = outer + remaining -= member_count + end + return result +end + +function clearance(shape::SectorShape, center) + cosine = cos(shape.at.φ) + sine = sin(shape.at.φ) + dx = center[1] - shape.at.x + dy = center[2] - shape.at.y + point = (cosine * dx + sine * dy, -sine * dx + cosine * dy) + distance = oftype(shape.primitive.r_back, Inf) + + for segment in values(shape.contacts.segments) + first_point, last_point = segment + edge = ( + last_point[1] - first_point[1], + last_point[2] - first_point[2] + ) + edge_length = hypot(edge...) + outward = (edge[2] / edge_length, -edge[1] / edge_length) + clearance = + first_point[1] * outward[1] + first_point[2] * outward[2] - + point[1] * outward[1] - point[2] * outward[2] + distance = min(distance, clearance) + end + + for arc in values(shape.contacts.arcs) + iszero(arc.radius) && continue + offset = (point[1] - arc.center[1], point[2] - arc.center[2]) + direction = atan(offset[2], offset[1]) + projection = if _angle_in_arc(direction, arc) + hypot(offset...) + else + max( + offset[1] * cos(arc.start) + offset[2] * sin(arc.start), + offset[1] * cos(arc.stop) + offset[2] * sin(arc.stop) + ) + end + distance = min(distance, arc.radius - projection) + end + return distance +end + +function accommodates(shape::SectorShape, center, radius::Real) + tolerance = 256 * geometry_tolerance( + max(shape.primitive.r_back, radius) + ) + return _geometry_scalar(clearance(shape, center) + tolerance - radius) >= 0 +end + +function sector_polygon(shape::SectorShape; points::Integer = 17) + polygon = boundary_polygon(shape; points) + cosine = cos(shape.at.φ) + sine = sin(shape.at.φ) + first_index = findmin(polygon) do point + dx = point[1] - shape.at.x + dy = point[2] - shape.at.y + cosine * dx + sine * dy + end[2] + return [polygon[mod1(first_index + index - 1, length(polygon))] + for index in eachindex(polygon)] +end + +function fan_measure(polygon, center) + areas = map(eachindex(polygon)) do index + next = mod1(index + 1, length(polygon)) + first_point = polygon[index] + last_point = polygon[next] + abs( + (first_point[1] - center[1]) * (last_point[2] - center[2]) - + (last_point[1] - center[1]) * (first_point[2] - center[2]) + ) / 2 + end + total = sum(areas) + total > zero(total) || throw(ArgumentError( + "a sector course requires a positive-area boundary" + )) + return [zero(total); cumsum(areas) ./ total] +end + +function fan_point(polygon, measure, fraction) + wrapped = mod(fraction, one(fraction)) + index = min(searchsortedlast(measure, wrapped), length(polygon)) + width = measure[index + 1] - measure[index] + local_fraction = iszero(width) ? zero(width) : + (wrapped - measure[index]) / width + next = mod1(index + 1, length(polygon)) + return ( + polygon[index][1] + local_fraction * + (polygon[next][1] - polygon[index][1]), + polygon[index][2] + local_fraction * + (polygon[next][2] - polygon[index][2]) + ) +end +""" +$(TYPEDSIGNATURES) + +Map one center strand and complete circular `6k` courses into a sector. +The mapped sites retain their course identities while a prescribed-area power +diagram allocates space. Clipped disks supply the actual conductor shapes. + +For `L` courses and `N = 1 + 3L(L+1)` wire strands, the course sites are + +```math +p_0=c,\\qquad +p_{k,m}=c+\\sqrt{\\frac{1+3k^2}{N}}\\,[q(t_{k,m})-c], +\\qquad t_{k,m}=\\frac{m+\\delta_k}{6k}. +``` + +Here `c` is the polygonal sector centroid and `q` traverses the geometric boundary +by normalized swept area. Sites remain fixed during power-cell balancing. + +# Arguments + +- `shape`: resolved sector boundary, with coordinates in meters. +- `wire`: circular source strand. Its area is preserved in every member. + +# Returns + +- Mapped members, resolved conductor polygons, and the number of outer courses. +""" +function sector_courses(shape::SectorShape, wire::Disk) + source_area = area(wire) + courses = course_count(area(shape) - source_area, source_area) + total_count = 1 + 3courses * (courses + 1) + points = 17 + polygon = sector_polygon(shape; points) + while total_count * nominal(source_area) > + signed_polygon_area([nominal.(point) for point in polygon]) * (1 + 2.0e-6) + points = 2points - 1 + points <= 16_385 || throw(ArgumentError( + "sector course tessellation could not preserve its source inventory" + )) + polygon = sector_polygon(shape; points) + end + center = polygon_centroid(polygon) + measure = fan_measure(polygon, center) + members = [(site = center, course = 0, member = 1, angle = zero(shape.at.φ))] + for course in 1:courses + count = 6course + scale = sqrt((1 + 3course^2) / total_count) + phase = isodd(course) ? 0.0 : 0.5 + for member in 1:count + point = fan_point(polygon, measure, (member - 1 + phase) / count) + site = ( + center[1] + scale * (point[1] - center[1]), + center[2] + scale * (point[2] - center[2]) + ) + push!(members, ( + site, + course, + member, + angle = atan(site[2] - center[2], site[1] - center[1]) + )) + end + end + targets = fill(signed_polygon_area(polygon) / total_count, total_count) + cells, _ = balance_power_cells(polygon, getproperty.(members, :site), targets) + primitives = [ + resolved_polygon( + area_preserving_strand(cell, source_area; angle = shape.at.φ), + zero(shape.at.φ) + ) for cell in cells + ] + return members, primitives, courses +end + +function clipped_disk!(points, clipped, cell, radius, directions) + empty!(points) + for point in directions + push!(points, (radius * point[1], radius * point[2])) + end + for index in eachindex(cell) + next = mod1(index + 1, length(cell)) + first_point, last_point = cell[index], cell[next] + normal = (last_point[2] - first_point[2], first_point[1] - last_point[1]) + offset = normal[1] * first_point[1] + normal[2] * first_point[2] + clip_halfplane!(clipped, points, normal, offset) + points, clipped = clipped, points + end + return points +end + +""" +$(TYPEDSIGNATURES) + +Grow a disk about a convex cell's centroid until its intersection with the cell +has the prescribed strand area: + +```math +W_i=P_i\\cap D(c_i,\\rho_i),\\qquad |W_i|=A_i, +\\qquad c_i=\\operatorname{centroid}(P_i). +``` + +The disk is approximated by a regular polygon. Bisection solves its radius +against the area of the clipped polygon, so the area tolerance is independent +of the arc tessellation. Cell faces bound contact regions. Free geometric boundaries +follow the disk. Full occupancy returns the cell within the geometric boundary's +geometric tolerance. + +# Arguments + +- `cell`: counter-clockwise vertices of an allocated convex cell \\[m\\]. +- `source_area`: prescribed transverse conductor area \\[m²\\]. + +# Keywords + +- `angle=0`: orientation of the disk tessellation \\[rad\\]. +- `points=128`: vertices in the disk approximation. + +# Returns + +- Vertices of the strand polygon, contained in its cell to numerical tolerance. +""" +function area_preserving_strand(cell, source_area; angle = 0, points::Integer = 128) + points >= 16 || throw(ArgumentError("a clipped disk requires at least 16 vertices")) + target = nominal(source_area) + cell_area = nominal(signed_polygon_area(cell)) + target > 0 || throw(DomainError(source_area, "strand area must be positive")) + target <= cell_area * (1 + 4.0e-6) || throw(DomainError( + source_area, "a source strand does not fit inside its allocated cell" + )) + center = polygon_centroid(cell) + if target >= cell_area * (1 - 64eps(float(cell_area))) + return normalize_polygon_area(cell, source_area, center) + end + + # Solve at unit area to avoid coordinate-scale tolerances and cancellation. + scale = sqrt(source_area) + local_cell = [((point[1] - center[1]) / scale, + (point[2] - center[2]) / scale) for point in cell] + directions = [(cos(angle + 2pi * index / points), + sin(angle + 2pi * index / points)) for index in 0:(points - 1)] + lower = inv(sqrt(pi)) + nominal_cell = [nominal.(point) for point in local_cell] + nominal_directions = [nominal.(point) for point in directions] + upper = maximum(point -> hypot(point...), nominal_cell) / cos(pi / points) + radius = lower + buffer = similar(nominal_directions, 0) + clipped = similar(buffer) + sizehint!(buffer, points + length(cell)) + sizehint!(clipped, points + length(cell)) + strand = buffer + for _ in 1:64 + radius = (lower + upper) / 2 + strand = clipped_disk!(buffer, clipped, nominal_cell, radius, nominal_directions) + value = signed_polygon_area(strand) + abs(value - 1) <= 64eps(float(value)) && break + value < 1 ? (lower = radius) : (upper = radius) + end + + # Differentiate the scalar area constraint for uncertainty-bearing inputs. + # The nominal cell topology is fixed, as in the existing compaction model. + if any(point -> any(x -> !iszero(uncertainty(x)), point), local_cell) || + !iszero(uncertainty(angle)) + step = cbrt(eps(float(radius))) * radius + area_plus = signed_polygon_area( + clipped_disk!(buffer, clipped, nominal_cell, radius + step, nominal_directions) + ) + area_minus = signed_polygon_area( + clipped_disk!(buffer, clipped, nominal_cell, radius - step, nominal_directions) + ) + derivative = (area_plus - area_minus) / (2step) + isfinite(derivative) && derivative > 0 || throw(DomainError( + derivative, "clipped-strand area has no positive local radius derivative" + )) + T = promote_type(typeof(first(local_cell)[1]), typeof(first(local_cell)[2]), + typeof(angle), typeof(radius)) + buffer_t, clipped_t = Tuple{T,T}[], Tuple{T,T}[] + fixed = clipped_disk!(buffer_t, clipped_t, local_cell, radius, directions) + residual = 1 - signed_polygon_area(fixed) + radius += (residual - nominal(residual)) / derivative + strand = clipped_disk!(buffer_t, clipped_t, local_cell, radius, directions) + end + return [(center[1] + scale * point[1], center[2] + scale * point[2]) + for point in strand] +end + +function resolved_polygon(points, angle) + center = polygon_centroid(points) + cosine = cos(angle) + sine = sin(angle) + local_points = map(points) do point + dx = point[1] - center[1] + dy = point[2] - center[2] + return (cosine * dx + sine * dy, -sine * dx + cosine * dy) + end + return _polygon(local_points, Pose2(center[1], center[2], angle)) +end + +function deform_disk_members(boundary_shape, members) + all(member -> member.source.primitive isa Disk, members) || throw(ArgumentError( + "boundary-constrained compaction currently requires circular source strands" + )) + source_areas = [area(member.source.primitive) for member in members] + total_source_area = sum(source_areas) + nominal_source_areas = nominal.(source_areas) + nominal_total_source_area = sum(nominal_source_areas) + nominal_boundary_area = nominal(area(boundary_shape)) + nominal_total_source_area <= nominal_boundary_area * (1 + 2.0e-6) || + throw(DomainError( + total_source_area, + "the compacted strand inventory exceeds its authoritative boundary area" + )) + points = boundary_shape isa Disk ? 256 : 64 + polygon = boundary_polygon(boundary_shape; points) + while nominal_total_source_area > nominal(signed_polygon_area(polygon)) * (1 + 2.0e-6) + points *= 2 + points <= 16_384 || throw(ArgumentError( + "bounded compaction could not resolve the declared fill factor " * + "within its geometric tolerance" + )) + polygon = boundary_polygon(boundary_shape; points) + end + targets = signed_polygon_area(polygon) .* source_areas ./ total_source_area + sites = [member.site for member in members] + cells = compact_power_cells(polygon, sites, targets) + return [ + resolved_polygon( + area_preserving_strand( + cells[index], source_areas[index]; angle = boundary_shape.at.φ + ), + members[index].angle + ) for index in eachindex(members) + ] +end diff --git a/src/datamodel/placement/compaction.jl b/src/datamodel/placement/compaction.jl new file mode 100644 index 000000000..b25a5116f --- /dev/null +++ b/src/datamodel/placement/compaction.jl @@ -0,0 +1,105 @@ +""" +$(TYPEDEF) + +Set the ratio of member material area to the resolved course-envelope area. + +For member areas ``A_i`` inside an envelope of area ``A_B``, the declared +dimensionless factor is + +```math +\\eta = \\frac{\\sum_i A_i}{A_B}. +``` + +$(TYPEDFIELDS) +""" +struct FillFactor{T <: Real} + "Material-area fraction \\[dimensionless\\]." + η::T + function FillFactor{T}(η::T) where {T <: Real} + isfinite(η) && zero(η) < η <= one(η) || throw(DomainError( + η, "fill factor must lie in (0, 1]" + )) + return new{T}(η) + end +end + +_fill_factor(η) = FillFactor{typeof(float(η))}(float(η)) + +""" +$(TYPEDSIGNATURES) + +Declare one material-area fill factor or a homogeneous schedule of fill +factors. + +# Arguments + +- `η`: one material-area fraction \\[dimensionless\\]. +- `values`: two or more factors, or one tuple or vector of factors. Specify one factor per repeated course. + +# Keywords + +- `combine=:product`: gridspace composition rule. + +# Returns + +- A `FillFactor`, a tuple of `FillFactor` values, or the corresponding + `Gridspace` when an explicit finite source is supplied. +""" +function FillFactor(η; combine::Symbol = :product) + parameterize(FillFactor, _fill_factor, (η,); combine) +end +function FillFactor(values::Union{Tuple, AbstractVector}; combine::Symbol = :product) + _normalize_schedule(FillFactor, values; combine) +end +function FillFactor(first, second, remaining...; combine::Symbol = :product) + _normalize_schedule(FillFactor, (first, second, remaining...); combine) +end + +function placements( + pattern::Ring, + item::Rectangle, + factor::FillFactor +) + pattern.n isa Int || throw(ArgumentError( + "capacity() requires contextual placement" + )) + pattern.r === nothing && throw(ArgumentError( + "FillFactor requires contextual placement or a resolved inner radius" + )) + inner = max(zero(pattern.r), pattern.r - item.h / 2) + outer = sqrt( + inner^2 + 2pattern.n * area(item) / (pattern.span * factor.η) + ) + angular_width = factor.η * pattern.span / pattern.n + definition = BentStrip(inner, outer, angular_width) + step = pattern.n == 1 ? zero(pattern.span) : pattern.span / pattern.n + return _ResolvedPlacement[ + _ResolvedPlacement( + Pose2(0, 0, pattern.φ0 + index * step), + definition + ) for index in 0:(pattern.n - 1) + ] +end + +function _fillfactor_capacity( + pattern::Ring, + member_area::Real, + radial_half_extent::Real, + factor::FillFactor +) + radius = something(pattern.r, radial_half_extent) + radius > zero(radius) || return 0 + factor.η <= inv(one(pattern.gap_frac) + pattern.gap_frac) || throw(DomainError( + (factor.η, pattern.gap_frac), + "fill factor cannot provide the requested member gap" + )) + inner = max(zero(radius), radius - radial_half_extent) + outer = radius + radial_half_extent + envelope_area = pattern.span * (outer^2 - inner^2) / 2 + count = factor.η * envelope_area / member_area + return max(0, floor(Int, count + 8eps(float(count)))) +end + +function capacity(pattern::Ring, item::Rectangle, factor::FillFactor) + _fillfactor_capacity(pattern, item.w * item.h, item.h / 2, factor) +end diff --git a/src/datamodel/placement/paths.jl b/src/datamodel/placement/paths.jl new file mode 100644 index 000000000..cc0b8241b --- /dev/null +++ b/src/datamodel/placement/paths.jl @@ -0,0 +1,202 @@ +""" +Store a helical lay-length to mean-diameter ratio. +""" +struct LayRatio{T <: Real} + q::T + function LayRatio{T}(q::T) where {T <: Real} + isfinite(q) && q > zero(q) || + throw(DomainError(q, "lay ratio must be positive and finite")) + return new{T}(q) + end +end + +_lay_ratio(q) = LayRatio{typeof(float(q))}(float(q)) + +function _normalize_schedule(wrapper, values; combine::Symbol) + return map(Tuple(values)) do value + value isa wrapper ? value : wrapper(value; combine) + end +end + +""" +$(TYPEDSIGNATURES) + +Declare one lay-length ratio or a homogeneous schedule of lay-length ratios. + +# Arguments + +- `q`: one positive lay-length to mean-diameter ratio \\[dimensionless\\]. +- `values`: two or more ratios, or one tuple or vector of ratios. Specify one ratio per repeated course. + +# Keywords + +- `combine=:product`: gridspace composition rule. + +# Returns + +- A `LayRatio`, a tuple of `LayRatio` values, or the corresponding `Gridspace` + when an explicit finite source is supplied. +""" +LayRatio(q; combine::Symbol = :product) = parameterize(LayRatio, _lay_ratio, (q,); combine) +function LayRatio(values::Union{Tuple, AbstractVector}; combine::Symbol = :product) + _normalize_schedule(LayRatio, values; combine) +end +function LayRatio(first, second, remaining...; combine::Symbol = :product) + _normalize_schedule(LayRatio, (first, second, remaining...); combine) +end + +""" +Store an authoritative helical pitch length \\[m\\]. +""" +struct Pitch{T <: Real} + p::T + function Pitch{T}(p::T) where {T <: Real} + isfinite(p) && p > zero(p) || + throw(DomainError(p, "pitch must be positive and finite")) + return new{T}(p) + end +end + +_pitch(p) = Pitch{typeof(float(p))}(float(p)) + +""" +$(TYPEDSIGNATURES) + +Declare one helical pitch or a homogeneous schedule of helical pitches. + +# Arguments + +- `p`: one positive helical pitch \\[m\\]. +- `values`: two or more pitches, or one tuple or vector of pitches. Specify one pitch per repeated course \\[m\\]. + +# Keywords + +- `combine=:product`: gridspace composition rule. + +# Returns + +- A `Pitch`, a tuple of `Pitch` values, or the corresponding `Gridspace` when + an explicit finite source is supplied. +""" +Pitch(p; combine::Symbol = :product) = parameterize(Pitch, _pitch, (p,); combine) +function Pitch(values::Union{Tuple, AbstractVector}; combine::Symbol = :product) + _normalize_schedule(Pitch, values; combine) +end +function Pitch(first, second, remaining...; combine::Symbol = :product) + _normalize_schedule(Pitch, (first, second, remaining...); combine) +end + +""" +Store an authoritative helical lay angle relative to the cable axis \\[rad\\]. +""" +struct LayAngle{T <: Real} + α::T + function LayAngle{T}(α::T) where {T <: Real} + isfinite(α) && zero(α) < α < oftype(α, π / 2) || + throw(DomainError(α, "lay angle must lie in (0, π/2)")) + return new{T}(α) + end +end + +_lay_angle(α) = LayAngle{typeof(float(α))}(float(α)) + +""" +$(TYPEDSIGNATURES) + +Declare one helical lay angle or a homogeneous schedule of lay angles. + +# Arguments + +- `α`: one lay angle relative to the cable axis \\[rad\\]. +- `values`: two or more angles, or one tuple or vector of angles. Specify one angle per repeated course \\[rad\\]. + +# Keywords + +- `combine=:product`: gridspace composition rule. + +# Returns + +- A `LayAngle`, a tuple of `LayAngle` values, or the corresponding `Gridspace` + when an explicit finite source is supplied. +""" +LayAngle(α; combine::Symbol = :product) = parameterize(LayAngle, _lay_angle, (α,); combine) +function LayAngle(values::Union{Tuple, AbstractVector}; combine::Symbol = :product) + _normalize_schedule(LayAngle, values; combine) +end +function LayAngle(first, second, remaining...; combine::Symbol = :product) + _normalize_schedule(LayAngle, (first, second, remaining...); combine) +end + +""" +$(TYPEDEF) + +Represent one helical path from one authoritative lay definition. + +$(TYPEDFIELDS) +""" +struct Helix{L, T <: Real} + "Lay definition." + lay::L + "Handedness: `1` or `-1` \\[dimensionless\\]." + dir::Int + "Initial angular position \\[rad\\]." + φ0::T + + function Helix{L, T}(lay::L, dir::Int, φ0::T) where {L, T <: Real} + lay isa Union{LayRatio, Pitch, LayAngle} || + throw(ArgumentError("helix lay must be LayRatio, Pitch, or LayAngle")) + dir in (-1, 1) || throw(ArgumentError("helix direction must be -1 or 1")) + isfinite(φ0) || throw(DomainError(φ0, "helix initial angle must be finite")) + return new{L, T}(lay, dir, φ0) + end +end + +function _helix(lay, dir, φ0) + dir isa Integer && !(dir isa Bool) || throw(ArgumentError( + "helix direction must be an integer" + )) + angle = float(φ0) + return Helix{typeof(lay), typeof(angle)}(lay, Int(dir), angle) +end + +function Helix( + lay; + dir = 1, + φ0 = 0, + combine::Symbol = :product +) + return parameterize(Helix, _helix, (lay, dir, φ0); combine) +end + +""" +Return helical pitch at local radius `radius` \\[m\\]. +""" +function pitch end +pitch(path::Helix{<:LayRatio}, radius::Real) = path.lay.q * (2 * radius) +pitch(path::Helix{<:Pitch}, radius::Real) = path.lay.p +pitch(path::Helix{<:LayAngle}, radius::Real) = 2π * radius / tan(path.lay.α) + +""" +Return helical lay angle at local radius `radius` \\[rad\\]. +""" +function angle(path::Helix, radius::Real) + radius >= zero(radius) || throw(DomainError(radius, "helix radius must be nonnegative")) + return atan(2π * radius, pitch(path, radius)) +end + +""" +Return the helical path-length ratio at local radius `radius`. +""" +function overlength(path::Helix, radius::Real) + value = angle(path, radius) + return inv(cos(value)) +end + +function overlength(path::Helix{<:LayRatio}, radius::Real) + radius >= zero(radius) || throw(DomainError( + radius, + "helix radius must be nonnegative" + )) + ratio = path.lay.q + return sqrt(one(ratio) + ((one(ratio) * pi) / ratio)^2) +end diff --git a/src/datamodel/placement/patterns.jl b/src/datamodel/placement/patterns.jl new file mode 100644 index 000000000..9264369eb --- /dev/null +++ b/src/datamodel/placement/patterns.jl @@ -0,0 +1,580 @@ +""" +$(TYPEDEF) + +Place `n` members on one circular locus. + +$(TYPEDFIELDS) +""" +struct _DeferredCardinality end + +""" +Return the deferred maximum wire count used by placement patterns. +""" +capacity() = _DeferredCardinality() + +""" +Return the maximum admissible member count for a placement declaration. +""" +function capacity end + +struct Ring{N, T <: Real, R} + "Number of members \\[dimensionless\\]." + n::N + "Radius of the member-center locus \\[m\\]." + r::R + "Starting angle \\[rad\\]." + φ0::T + "Counter-clockwise angular span \\[rad\\]." + span::T + "Fractional clearance between adjacent members \\[dimensionless\\]." + gap_frac::T + + function Ring{N, T, R}( + n::N, r::R, φ0::T, span::T, gap_frac::T + ) where {N, T <: Real, R} + n isa _DeferredCardinality || + (n isa Int && n > 0) || + throw(ArgumentError("ring cardinality must be positive or capacity()")) + r === nothing || + (r isa Real && isfinite(r) && r >= zero(r)) || + throw(DomainError(r, "ring radius must be nonnegative, finite, or contextual")) + isfinite(φ0) || throw(DomainError(φ0, "ring start angle must be finite")) + isfinite(span) && zero(span) < span <= oftype(span, 2π) || + throw(DomainError(span, "ring span must lie in (0, 2π]")) + isfinite(gap_frac) && gap_frac >= zero(gap_frac) || throw(DomainError( + gap_frac, "ring gap fraction must be nonnegative and finite" + )) + return new{N, T, R}(n, r, φ0, span, gap_frac) + end +end + +""" +Maximum circular-wire count on a ring with fractional adjacent clearance. +""" +function capacity( + ::Type{Ring}, + lay_radius::Real, + item_radius::Real; + gap_frac::Real = 0 +) + lay_radius > zero(lay_radius) || throw(DomainError( + lay_radius, "lay radius must be positive" + )) + item_radius > zero(item_radius) || throw(DomainError( + item_radius, "item radius must be positive" + )) + gap_frac >= zero(gap_frac) || throw(DomainError( + gap_frac, "gap fraction must be nonnegative" + )) + ratio = item_radius * (one(gap_frac) + gap_frac) / lay_radius + zero(ratio) < ratio < one(ratio) || return 0 + count = pi / asin(ratio) + return max(0, floor(Int, count + 8eps(float(count)))) +end + +function _ring(n, r, φ0, span, gap_frac) + n isa Union{Integer, _DeferredCardinality} || throw(ArgumentError( + "ring cardinality must be a positive integer or capacity()" + )) + numeric = r === nothing ? promote(φ0, span, gap_frac) : + promote(r, φ0, span, gap_frac) + values = map(float, numeric) + if r === nothing + angle, angular_span, gap = values + return Ring{typeof(n), typeof(angle), Nothing}( + n isa Integer ? Int(n) : n, + nothing, + angle, + angular_span, + gap + ) + end + radius, angle, angular_span, gap = values + return Ring{typeof(n isa Integer ? Int(n) : n), typeof(radius), typeof(radius)}( + n isa Integer ? Int(n) : n, + radius, + angle, + angular_span, + gap + ) +end + +function Ring( + n; + r = nothing, + φ0 = 0, + span = 2π, + gap_frac = 0, + combine::Symbol = :product +) + return parameterize( + Ring, _ring, (n, r, φ0, span, gap_frac); combine + ) +end + +""" +$(TYPEDEF) + +Place members on `nr` concentric loci with `nφ` angular positions per locus. + +$(TYPEDFIELDS) +""" +struct Polar{T <: Real} + "Number of radial loci \\[dimensionless\\]." + nr::Int + "Angular positions per nonzero locus \\[dimensionless\\]." + nφ::Int + "First locus radius \\[m\\]." + r0::T + "Radial increment \\[m\\]." + dr::T + "Starting angle \\[rad\\]." + φ0::T + "Counter-clockwise angular span \\[rad\\]." + span::T + + function Polar{T}( + nr::Int, nφ::Int, r0::T, dr::T, φ0::T, span::T + ) where {T <: Real} + nr > 0 || throw(ArgumentError("polar radial count must be positive")) + nφ > 0 || throw(ArgumentError("polar angular count must be positive")) + isfinite(r0) && r0 >= zero(r0) || + throw(DomainError(r0, "polar initial radius must be nonnegative and finite")) + isfinite(dr) && dr >= zero(dr) || + throw(DomainError(dr, "polar radial increment must be nonnegative and finite")) + isfinite(φ0) || throw(DomainError(φ0, "polar start angle must be finite")) + isfinite(span) && zero(span) < span <= oftype(span, 2π) || + throw(DomainError(span, "polar span must lie in (0, 2π]")) + return new{T}(nr, nφ, r0, dr, φ0, span) + end +end + +function _polar(nr, nφ, r0, dr, φ0, span) + nr isa Integer && !(nr isa Bool) || throw(ArgumentError( + "polar radial count must be an integer" + )) + nφ isa Integer && !(nφ isa Bool) || throw(ArgumentError( + "polar angular count must be an integer" + )) + values = map(float, promote(r0, dr, φ0, span)) + return Polar{typeof(first(values))}(Int(nr), Int(nφ), values...) +end + +function Polar(; + nr, + nφ, + r0 = 0, + dr, + φ0 = 0, + span = 2π, + combine::Symbol = :product +) + return parameterize(Polar, _polar, (nr, nφ, r0, dr, φ0, span); combine) +end + +""" +$(TYPEDEF) + +Fill a circular domain with one central member and concentric tangent courses. + +$(TYPEDFIELDS) +""" +struct Fill{T <: Real} + "Outer radius available to the filled members \\[m\\]." + r::T + "Angular offset applied between consecutive courses \\[rad\\]." + φ::T + "Starting angle of the first outer course \\[rad\\]." + φ0::T + "Counter-clockwise angular span of each outer course \\[rad\\]." + span::T + + function Fill{T}(r::T, φ::T, φ0::T, span::T) where {T <: Real} + isfinite(r) && r > zero(r) || throw(DomainError( + r, "fill radius must be positive and finite" + )) + isfinite(φ) || throw(DomainError(φ, "fill course offset must be finite")) + isfinite(φ0) || throw(DomainError(φ0, "fill start angle must be finite")) + isfinite(span) && zero(span) < span <= oftype(span, 2π) || throw( + DomainError(span, "fill span must lie in (0, 2π]") + ) + return new{T}(r, φ, φ0, span) + end +end + +function _fill(r, φ, φ0, span) + values = map(float, promote(r, φ, φ0, span)) + return Fill{typeof(first(values))}(values...) +end + +function Fill(; + r, + φ, + φ0 = 0, + span = 2π, + combine::Symbol = :product +) + return parameterize(Fill, _fill, (r, φ, φ0, span); combine) +end + +""" +$(TYPEDEF) + +Place members on a centered rectangular lattice. + +$(TYPEDFIELDS) +""" +struct Lattice{T <: Real} + "Number of columns \\[dimensionless\\]." + nx::Int + "Number of rows \\[dimensionless\\]." + ny::Int + "Column spacing \\[m\\]." + dx::T + "Row spacing \\[m\\]." + dy::T + + function Lattice{T}(nx::Int, ny::Int, dx::T, dy::T) where {T <: Real} + nx > 0 || throw(ArgumentError("lattice column count must be positive")) + ny > 0 || throw(ArgumentError("lattice row count must be positive")) + isfinite(dx) && (nx == 1 ? dx >= zero(dx) : dx > zero(dx)) || + throw(DomainError(dx, "lattice column spacing is invalid")) + isfinite(dy) && (ny == 1 ? dy >= zero(dy) : dy > zero(dy)) || + throw(DomainError(dy, "lattice row spacing is invalid")) + return new{T}(nx, ny, dx, dy) + end +end + +function _lattice(nx, ny, dx, dy) + nx isa Integer && !(nx isa Bool) || throw(ArgumentError( + "lattice column count must be an integer" + )) + ny isa Integer && !(ny isa Bool) || throw(ArgumentError( + "lattice row count must be an integer" + )) + values = map(float, promote(dx, dy)) + return Lattice{typeof(first(values))}(Int(nx), Int(ny), values...) +end + +function Lattice(; + nx, + ny, + dx, + dy, + combine::Symbol = :product +) + return parameterize(Lattice, _lattice, (nx, ny, dx, dy); combine) +end + +""" +Return local member poses prescribed by a placement pattern. +""" +function placements end + +struct _ResolvedPlacement{P <: Pose2, D <: AbstractShape} + pose::P + primitive::D +end + +_placement_pose(value::Pose2) = value +_placement_pose(value::_ResolvedPlacement) = value.pose +_placement_definition(value::Pose2, definition::AbstractPrimitive) = definition +_placement_definition(value::_ResolvedPlacement, ::AbstractPrimitive) = value.primitive + +function _ring_step(pattern::Ring, count::Int) + return count == 1 ? zero(pattern.span) : + isapprox(pattern.span, oftype(pattern.span, 2π)) ? + pattern.span / count : pattern.span / (count - 1) +end + +function _ring_poses(pattern::Ring, count::Int, radius::Real) + step = _ring_step(pattern, count) + return Pose2[Pose2( + radius * cos(pattern.φ0 + index * step), + radius * sin(pattern.φ0 + index * step), + pattern.φ0 + index * step + ) + for index in 0:(count - 1)] +end + +placements(::Nothing, item, ::Nothing) = Pose2[Pose2(0, 0, 0)] + +# Ring members of this tangential width keep the requested gap along the ring. +function validate(pattern::Ring, tangential_width::Real) + pattern.n == 1 && return pattern + interval_count = isapprox(pattern.span, oftype(pattern.span, 2π)) ? + pattern.n : pattern.n - 1 + chord = 2pattern.r * sin(pattern.span / interval_count / 2) + required = tangential_width * (one(pattern.gap_frac) + pattern.gap_frac) + tolerance = sqrt(eps(float(chord))) * max(one(chord), chord) + chord + tolerance >= required || throw(DomainError( + pattern.n, + "ring members overlap or violate the requested gap: " * + "count=$(pattern.n), radius=$(nominal(pattern.r)) m, " * + "member width=$(nominal(tangential_width)) m, " * + "gap fraction=$(nominal(pattern.gap_frac)), " * + "required chord=$(nominal(required)) m, " * + "available chord=$(nominal(chord)) m, " * + "deficit=$(nominal(required - chord)) m" + )) + return pattern +end + +function _tangential_width(definition::AbstractPrimitive) + primitive = resolve(EmptyBoundary(), definition) + return support(primitive, pi / 2) + support(primitive, -pi / 2) +end + +function placements( + pattern::Ring, + item::AbstractPrimitive, + ::Nothing +) + pattern.n isa Int || throw(ArgumentError( + "capacity() requires contextual placement" + )) + pattern.r === nothing && throw(ArgumentError( + "a contextual ring radius requires placement inside a physical tree" + )) + validate(pattern, _tangential_width(item)) + return _ring_poses(pattern, pattern.n, pattern.r) +end + +function placements(pattern::Ring, item::Disk, ::Nothing) + pattern.n isa Int || throw(ArgumentError( + "capacity() requires contextual placement" + )) + pattern.r === nothing && throw(ArgumentError( + "a contextual ring radius requires placement inside a physical tree" + )) + validate(pattern, 2item.r) + return _ring_poses(pattern, pattern.n, pattern.r) +end + +function placements(pattern::Ring, item::Rectangle, ::Nothing) + pattern.n isa Int || throw(ArgumentError( + "capacity() requires contextual placement" + )) + pattern.r === nothing && throw(ArgumentError( + "a contextual ring radius requires placement inside a physical tree" + )) + occupied = pattern.n * item.w * (one(pattern.gap_frac) + pattern.gap_frac) + available = pattern.r * pattern.span + tolerance = sqrt(eps(float(available))) * max(one(available), available) + occupied <= available + tolerance || throw(DomainError( + pattern.n, + "rectangular members exceed the available angular span" + )) + inner = max(zero(pattern.r), pattern.r - item.h / 2) + outer = inner + item.h + angular_width = 2area(item) / (outer^2 - inner^2) + definition = BentStrip(inner, outer, angular_width) + step = pattern.n == 1 ? zero(pattern.span) : pattern.span / pattern.n + return _ResolvedPlacement[ + _ResolvedPlacement( + Pose2(0, 0, pattern.φ0 + index * step), + definition + ) for index in 0:(pattern.n - 1) + ] +end + +function placements(pattern::Ring, item::Sector, ::Nothing) + pattern.n isa Int || throw(ArgumentError( + "capacity() requires contextual placement" + )) + pattern.r === nothing && throw(ArgumentError( + "a contextual ring radius requires placement inside a physical tree" + )) + if iszero(pattern.r) + return _origin_ring_poses( + pattern, + resolve(EmptyBoundary(), item) + ) + end + validate(pattern, _tangential_width(item)) + return _ring_poses(pattern, pattern.n, pattern.r) +end + +function placements(pattern::Ring, item::CableGeometry, ::Nothing) + pattern.n isa Int || throw(ArgumentError( + "capacity() requires contextual placement" + )) + pattern.r === nothing && throw(ArgumentError( + "a contextual ring radius requires placement inside a physical tree" + )) + iszero(pattern.r) && return _origin_ring_poses(pattern, item) + validate(pattern, 2support(boundary(item))) + return _ring_poses(pattern, pattern.n, pattern.r) +end + +function _sector_ring_poses(pattern::Ring, shape::SectorShape, allow_contact::Bool) + pattern.n == 1 && return _ring_poses(pattern, pattern.n, pattern.r) + iszero(pattern.gap_frac) || throw(DomainError( + pattern.gap_frac, + "origin-centered sector spacing is set by its physical side clearance, " * + "not Ring gap_frac" + )) + pitch = _ring_step(pattern, pattern.n) + angle_tolerance = sqrt(eps(float(_geometry_scalar(pitch)))) * + max(one(_geometry_scalar(pitch)), abs(_geometry_scalar(pitch))) + isapprox( + _geometry_scalar(shape.primitive.span), + _geometry_scalar(pitch); + atol = angle_tolerance, + rtol = sqrt(eps(float(_geometry_scalar(pitch)))) + ) || throw(DomainError( + shape.primitive.span, + "an origin-centered sector span must equal its angular pitch $pitch so " * + "neighboring wedge sides remain parallel" + )) + clearance = _sector_side_clearance(shape.primitive) + clearance_tolerance = sqrt(eps(float(_geometry_scalar(shape.primitive.r_back)))) * + max( + one(_geometry_scalar(shape.primitive.r_back)), + abs(_geometry_scalar(shape.primitive.r_back)) + ) + valid_clearance = allow_contact ? + _geometry_scalar(clearance) >= -clearance_tolerance : + _geometry_scalar(clearance) > clearance_tolerance + valid_clearance || throw(DomainError( + clearance, + allow_contact ? + "sector insulation boundaries overlap" : + "bare origin-centered sectors require a positive physical side clearance" + )) + return _ring_poses(pattern, pattern.n, pattern.r) +end + +_origin_ring_poses(pattern::Ring, shape::SectorShape) = + _sector_ring_poses(pattern, shape, false) + +function _origin_ring_poses(pattern::Ring, item::CableGeometry) + shape = boundary(item) + shape isa SectorShape || throw(ArgumentError( + "only sector-shaped members can share an origin" + )) + outer_region = findlast( + region -> boundary(region.primitive) === shape, + item.regions + ) + allow_contact = outer_region !== nothing && + item.regions[outer_region].source.material.kind === :insulator + return _sector_ring_poses(pattern, shape, allow_contact) +end + +function placements(pattern::Polar, item, ::Nothing) + poses = Pose2[] + for radial_index in 0:(pattern.nr - 1) + radius = pattern.r0 + radial_index * pattern.dr + if iszero(radius) + push!(poses, Pose2(0, 0, pattern.φ0)) + continue + end + append!(poses, + placements( + Ring(pattern.nφ; r = radius, φ0 = pattern.φ0, span = pattern.span), + item, + nothing + )) + end + return poses +end + +function _ring_capacity(pattern::Ring, ratio::Real) + zero(ratio) < ratio < one(ratio) || return 0 + half_angle = asin(ratio) + if isapprox(pattern.span, oftype(pattern.span, 2π)) + count = pi / half_angle + return max(0, floor(Int, count + 8eps(float(count)))) + end + count = pattern.span / (2half_angle) + return max(0, floor(Int, count + 8eps(float(count))) + 1) +end + +function capacity(pattern::Ring, item::Disk, ::Nothing) + radius = something( + pattern.r, + item.r * (one(item.r) + pattern.gap_frac) + ) + ratio = item.r * (one(pattern.gap_frac) + pattern.gap_frac) / radius + return _ring_capacity(pattern, ratio) +end + +function capacity(pattern::Ring, item::Rectangle, ::Nothing) + radius = something(pattern.r, item.h / 2) + radius > zero(radius) || return 0 + ratio = item.w * (one(pattern.gap_frac) + pattern.gap_frac) / (2radius) + return _ring_capacity(pattern, ratio) +end + +function capacity(pattern::Ring, item::Sector, ::Nothing) + if pattern.r !== nothing && !iszero(pattern.r) + width = _tangential_width(item) + ratio = width * (one(pattern.gap_frac) + pattern.gap_frac) / + (2pattern.r) + return _ring_capacity(pattern, ratio) + end + occupied = item.span * (one(pattern.gap_frac) + pattern.gap_frac) + return max(0, floor(Int, pattern.span / occupied + 8eps(float(pattern.span)))) +end + +function capacity(pattern::Ring, item::CableGeometry, ::Nothing) + member_radius = support(boundary(item)) + radius = something( + pattern.r, + member_radius * (one(member_radius) + pattern.gap_frac) + ) + ratio = member_radius * (one(pattern.gap_frac) + pattern.gap_frac) / radius + return _ring_capacity(pattern, ratio) +end + +function placements(pattern::Lattice, item, ::Nothing) + x0 = -(pattern.nx - 1) * pattern.dx / 2 + y0 = -(pattern.ny - 1) * pattern.dy / 2 + return Pose2[Pose2(x0 + ix * pattern.dx, y0 + iy * pattern.dy, 0) + for iy in 0:(pattern.ny - 1) for ix in 0:(pattern.nx - 1)] +end + +_fill_member_extent(item::AbstractPrimitive) = support(resolve(EmptyBoundary(), item)) +_fill_member_extent(item::CableGeometry) = support(boundary(item)) + +function _fill_placements(pattern::Fill, item, compact) + extent = _fill_member_extent(item) + extent > zero(extent) || throw(DomainError( + extent, "fill members must have positive radial extent" + )) + tolerance = sqrt(eps(float(pattern.r))) * max(one(pattern.r), pattern.r) + extent <= pattern.r + tolerance || throw(DomainError( + extent, "one member does not fit inside the fill radius" + )) + + poses = Pose2[Pose2(0, 0, pattern.φ0)] + course = 1 + radius = 2extent + while radius + extent <= pattern.r + tolerance + ring = Ring( + capacity(); + r = radius, + φ0 = pattern.φ0 + (course - 1) * pattern.φ, + span = pattern.span + ) + count = capacity(ring, item, compact) + count > 0 || break + append!(poses, placements( + Ring(count; r = ring.r, φ0 = ring.φ0, span = ring.span), + item, + compact + )) + course += 1 + radius = 2course * extent + end + return poses +end + +placements(pattern::Fill, item, ::Nothing) = _fill_placements(pattern, item, nothing) +capacity(pattern::Fill, item, ::Nothing) = length(_fill_placements(pattern, item, nothing)) + +function placements(poses::AbstractVector{<:Pose2}, item, ::Nothing) + isempty(poses) && throw(ArgumentError("explicit placements cannot be empty")) + return copy(poses) +end diff --git a/src/datamodel/preview.jl b/src/datamodel/preview.jl deleted file mode 100644 index b722b03e0..000000000 --- a/src/datamodel/preview.jl +++ /dev/null @@ -1,1284 +0,0 @@ -using Makie, Colors -using Printf -using Dates -using Statistics - - -# _is_interactive_backend() = nameof(Makie.current_backend()) in (:GLMakie, :WGLMakie) -_is_interactive_backend() = current_backend_symbol() in (:gl, :wgl) -_is_static_backend() = current_backend_symbol() == :cairo -_is_gl_backend() = current_backend_symbol() == :gl - - -# finite & nonnegative -_valid_finite(x, y) = isfinite(x) && isfinite(y) - - -# Tunables (bands & palettes) -# ---------------------------- -const RHO_MIN = 1e-9 # for legend floor -const RHO_METAL_MAX = 1e-6 -const RHO_SEMIMETAL = 1e-4 -const RHO_SEMI_MAX = 1e3 -const RHO_LEAKY_MAX = 1e8 -const RHO_MAX = 1e10 # for legend ceiling - -const METAL_GRADIENT = [ - RGB(0.92, 0.90, 0.86), # warm-silver (copper-ish) - RGB(0.89, 0.89, 0.89), # neutral silver - RGB(0.86, 0.89, 0.92), # cool-silver (aluminium-ish) - RGB(0.70, 0.72, 0.75), # slightly darker metal -] - -const SEMIMETAL_GRADIENT = [ - RGB(0.70, 0.72, 0.75), # gray - RGB(0.80, 0.75, 0.65), # sand/bronze hint -] - -const SEMICON_GRADIENT = [ - RGB(1.00, 0.83, 0.40), # light amber - RGB(0.85, 0.55, 0.18), # dark amber-brown -] - -const LEAKY_GRADIENT = [ - RGB(0.42, 0.55, 0.15), # olive/earthy - RGB(0.13, 0.13, 0.13), # charcoal -] - -const INSULATOR_GRADIENT = [ - RGB(0.07, 0.07, 0.07), # near-black (keep >0 so overlays remain visible) - RGB(0.00, 0.00, 0.00), -] - -# Overlays -const MU_OVERLAY_GRADIENT = [RGB(0.20, 0.50, 0.95), RGB(0.56, 0.00, 0.91)] # blue → indigo -const EPS_OVERLAY_GRADIENT = [RGB(0.00, 0.85, 0.70), RGB(0.00, 0.55, 0.90)] # teal → cyan - - -# Linear interpolation across a list of colors in [0,1] -# robust gradient (no reinterpret) -_interpolate_gradient(colors::Vector{<:Colorant}, t::Real) = begin - n = length(colors); - n >= 2 || throw(ArgumentError("Need ≥ 2 colors")) - tc = clamp(Float64(t), 0, 1) - x = tc * (n - 1) - i = clamp(floor(Int, x) + 1, 1, n - 1) - f = x - (i - 1) - c1 = RGB(colors[i]); - c2 = RGB(colors[i+1]) - RGB( - (1 - f) * red(c1) + f * red(c2), - (1 - f) * green(c1) + f * green(c2), - (1 - f) * blue(c1) + f * blue(c2), - ) -end - -# Log normalization helper: map v∈[a,b] (log10) → t∈[0,1] -_lognorm(v, a, b) = begin - va = clamp(v, min(a, b), max(a, b)) - (log10(va) - log10(a)) / (log10(b) - log10(a)) -end - -_overlay(a::Colors.RGBA, b::Colors.RGBA) = begin - a1, a2 = alpha(a), alpha(b) - out_a = a2 + a1*(1 - a2) - out_a == 0 && return Colors.RGBA(0, 0, 0, 0) - r = (red(b)*a2 + red(a)*a1*(1 - a2)) / out_a - g = (green(b)*a2 + green(a)*a1*(1 - a2)) / out_a - b_ = (blue(b)*a2 + blue(a)*a1*(1 - a2)) / out_a - Colors.RGBA(r, g, b_, out_a) -end - -# Clamp lightness to keep overlays visible on "black" -function _ensure_min_lightness(c::RGB, Lmin::Float64 = 0.07) - hsl = HSL(c) - L = max(hsl.l, Lmin) - rgb = RGB(HSL(hsl.h, hsl.s, L)) - return rgb -end - -# ---------------------------- -# Base color controlled by ρ -# ---------------------------- -function _base_color_from_rho(ρ::Real)::RGB - if !isfinite(ρ) - return INSULATOR_GRADIENT[end] - elseif ρ ≤ RHO_METAL_MAX - t = _lognorm(ρ, 1e-8, RHO_METAL_MAX) - return _interpolate_gradient(METAL_GRADIENT, t) - elseif ρ ≤ RHO_SEMIMETAL - t = _lognorm(ρ, RHO_METAL_MAX, RHO_SEMIMETAL) - return _interpolate_gradient(SEMIMETAL_GRADIENT, t) - elseif ρ ≤ RHO_SEMI_MAX - t = _lognorm(ρ, RHO_SEMIMETAL, RHO_SEMI_MAX) - return _interpolate_gradient(SEMICON_GRADIENT, t) - elseif ρ ≤ RHO_LEAKY_MAX - t = _lognorm(ρ, RHO_SEMI_MAX, RHO_LEAKY_MAX) - return _interpolate_gradient(LEAKY_GRADIENT, t) - else - t = _lognorm(min(ρ, RHO_MAX), RHO_LEAKY_MAX, RHO_MAX) - return _ensure_min_lightness(_interpolate_gradient(INSULATOR_GRADIENT, t), 0.07) - end -end - -# ---------------------------- -# Overlays (μr & εr) -# ---------------------------- -# μr in [1, 300] → alpha up to ~0.5, stronger on dark bases -function _mu_overlay(base::RGB, μr::Real)::Colors.RGBA - μn = clamp((_lognorm(max(μr, 1.0), 1.0, 300.0)), 0, 1) - tint = _interpolate_gradient(MU_OVERLAY_GRADIENT, μn) - L = HSL(base).l - α = 0.50 * μn * (0.6 + 0.4*(1 - L)) # reduce on bright silver, boost on dark - Colors.RGBA(tint.r, tint.g, tint.b, α) -end - -# εr in [1, 1000] → alpha up to ~0.6 on insulators, ~0.2 on metals -function _eps_overlay(base::RGB, εr::Real, ρ::Real)::Colors.RGBA - εn = clamp((_lognorm(max(εr, 1.0), 1.0, 1000.0)), 0, 1) - tint = _interpolate_gradient(EPS_OVERLAY_GRADIENT, εn) - # weight more if it's an insulator/leaky (so it shows on dark) - band_weight = ρ > RHO_SEMI_MAX ? 1.0 : (ρ > RHO_METAL_MAX ? 0.6 : 0.35) - L = HSL(base).l - α = (0.20 + 0.40*band_weight) * εn * (0.55 + 0.45*(1 - L)) - Colors.RGBA(tint.r, tint.g, tint.b, α) -end - -""" - get_material_color_makie(material_props; mu_scale=1.0, eps_scale=1.0) - -Piecewise ρ→base color (metals→silver, semiconductors→amber, etc.) with -blue/purple magnetic overlay (μr) and teal/cyan permittivity overlay (εr). -`mu_scale` and `eps_scale` scale overlay strength (1.0 = default). -""" -function get_material_color_makie(material_props; mu_scale = 1.0, eps_scale = 1.0) - ρ = to_nominal(material_props.rho) - εr = to_nominal(material_props.eps_r) - μr = to_nominal(material_props.mu_r) - - base = _base_color_from_rho(ρ) |> c -> _ensure_min_lightness(c, 0.07) - - # Compose overlays - mu = _mu_overlay(base, μr); - mu = Colors.RGBA(mu.r, mu.g, mu.b, clamp(alpha(mu)*mu_scale, 0, 1)) - eps = _eps_overlay(base, εr, ρ); - eps = Colors.RGBA(eps.r, eps.g, eps.b, clamp(alpha(eps)*eps_scale, 0, 1)) - - out = _overlay(Colors.RGBA(base.r, base.g, base.b, 1.0), mu) - out = _overlay(out, eps) - return out -end - -function show_material_scale(; size = (800, 400), backend = nothing) - # if backend !== nothing - # _use_makie_backend(backend) - # end - ensure_backend!(backend === nothing ? :cairo : backend) - - fig = Figure(size = size) - - # sampling density for smooth bars - N = 1024 - - # --- ρ colorbar (log scale by ticks/limits) ------------------------------- - ρmin_log, ρmax_log = log10(RHO_MIN), log10(RHO_MAX) - # sample uniformly in log(ρ) so the bar matches your piecewise mapping - cm_ρ = begin - cols = Vector{RGBA}(undef, N) - for i in 1:N - t = (i - 1) / (N - 1) - ρ = 10^(ρmin_log + t * (ρmax_log - ρmin_log)) - c = _base_color_from_rho(ρ) - cols[i] = RGBA(c.r, c.g, c.b, 1.0) - end - cols - end - - cb_ρ = Colorbar(fig[1, 1]; - colormap = cm_ρ, - limits = (ρmin_log, ρmax_log), # we encode log(ρ) in limits/ticks - vertical = false, - label = "Base color by resistivity ρ [Ω·m] (log scale)", - ) - - # label ticks at meaningful boundaries - edges = [RHO_MIN, 1e-8, 1e-7, RHO_METAL_MAX, RHO_SEMIMETAL, RHO_SEMI_MAX, - 1e4, 1e6, RHO_LEAKY_MAX, RHO_MAX] - cb_ρ.ticks = (log10.(edges), string.(edges)) - - # --- μr overlay colorbar (blue→indigo on mid-gray) ------------------------ - μmin, μmax = 1.0, 300.0 - base_mid = RGB(0.5, 0.5, 0.5) - cm_μ = begin - cols = Vector{RGBA}(undef, N) - for i in 1:N - t = (i - 1) / (N - 1) - μ = 10^(log10(μmin) + t * (log10(μmax) - log10(μmin))) - o = _mu_overlay(base_mid, μ) - out = _overlay(RGBA(base_mid.r, base_mid.g, base_mid.b, 1.0), o) - cols[i] = out - end - cols - end - - cb_μ = Colorbar(fig[2, 1]; - colormap = cm_μ, - limits = (μmin, μmax), - vertical = false, - label = "Magnetic overlay μᵣ (blue→indigo)", - ) - cb_μ.ticks = ( - [1, 2, 5, 10, 20, 50, 100, 200, 300], - string.([1, 2, 5, 10, 20, 50, 100, 200, 300]), - ) - - # --- εr overlay colorbar (teal→cyan on dark base) ------------------------- - εmin, εmax = 1.0, 1000.0 - base_dark = RGB(0.10, 0.10, 0.10) - cm_ε = begin - cols = Vector{RGBA}(undef, N) - for i in 1:N - t = (i - 1) / (N - 1) - ε = 10^(log10(εmin) + t * (log10(εmax) - log10(εmin))) - o = _eps_overlay(base_dark, ε, RHO_MAX + 1) # treat as strong insulator - out = _overlay(RGBA(base_dark.r, base_dark.g, base_dark.b, 1.0), o) - cols[i] = out - end - cols - end - - cb_ε = Colorbar(fig[3, 1]; - colormap = cm_ε, - limits = (εmin, εmax), - vertical = false, - label = "Permittivity overlay εᵣ (teal→cyan)", - ) - cb_ε.ticks = ([1, 2, 5, 10, 20, 50, 100, 200, 500, 1000], - string.([1, 2, 5, 10, 20, 50, 100, 200, 500, 1000])) - - renderfig(fig) - return fig -end - - -################################# -# Geometry helpers (polygons) # -################################# -# polygons (Float32 points; filter non-finite) -function _annulus_poly(rin::Real, rex::Real, x0::Real, y0::Real; N::Int = 256) - N ≥ 32 || throw(ArgumentError("N too small for a smooth annulus")) - θo = range(0, 2π; length = N); - θi = reverse(θo) - xo = x0 .+ rex .* cos.(θo); - yo = y0 .+ rex .* sin.(θo) - xi = x0 .+ rin .* cos.(θi); - yi = y0 .+ rin .* sin.(θi) - px = vcat(xo, xi, xo[1]); - py = vcat(yo, yi, yo[1]) - pts = Makie.Point2f.(px, py) - filter(p -> _valid_finite(p[1], p[2]), pts) -end - -function _circle_poly(r::Real, x0::Real, y0::Real; N::Int = 128) - θ = range(0, 2π; length = N) - x = x0 .+ r .* cos.(θ); - y = y0 .+ r .* sin.(θ) - pts = Makie.Point2f.(vcat(x, x[1]), vcat(y, y[1])) - filter(p -> _valid_finite(p[1], p[2]), pts) -end - -function _annular_sector_poly( - rin::Real, - rex::Real, - θ_start::Real, - θ_end::Real, - x0::Real, - y0::Real; - N_pts::Int = 32, -) - # Generate points along the outer arc, then reverse back along the inner arc - θ_out = range(θ_start, θ_end; length = N_pts) - θ_in = reverse(θ_out) - - xo = x0 .+ rex .* cos.(θ_out) - yo = y0 .+ rex .* sin.(θ_out) - - xi = x0 .+ rin .* cos.(θ_in) - yi = y0 .+ rin .* sin.(θ_in) - - # Close the polygon - px = vcat(xo, xi, xo[1]) - py = vcat(yo, yi, yo[1]) - - pts = Makie.Point2f.(px, py) - filter(p -> _valid_finite(p[1], p[2]), pts) -end - -function _bent_rect_poly( - rin::Real, - rex::Real, - w::Real, - θ_c::Real, - x0::Real, - y0::Real; - N_pts::Int = 32, -) - # Outer arc: arc length is exactly w, so the angular span is w / rex - dθ_out = rex > 0 ? (w / rex) : 0.0 - θ_out = range(θ_c - dθ_out/2, θ_c + dθ_out/2; length = N_pts) - xo = x0 .+ rex .* cos.(θ_out) - yo = y0 .+ rex .* sin.(θ_out) - - # Inner arc: arc length is exactly w, so the angular span is w / rin - dθ_in = rin > 0 ? (w / rin) : 0.0 - θ_in = reverse(range(θ_c - dθ_in/2, θ_c + dθ_in/2; length = N_pts)) - xi = x0 .+ rin .* cos.(θ_in) - yi = y0 .+ rin .* sin.(θ_in) - - # Connect the arcs. The straight side walls will now perfectly - # preserve the Cartesian width of the strand. - px = vcat(xo, xi, xo[1]) - py = vcat(yo, yi, yo[1]) - - pts = Makie.Point2f.(px, py) - filter(p -> _valid_finite(p[1], p[2]), pts) -end - -function _radial_wedge_poly( - rin::Real, - rex::Real, - w::Real, - θ_c::Real, - x0::Real, - y0::Real; - N_pts::Int = 32, -) - # The true angular width is dictated entirely by the inner arc length (w) - dθ = rin > 0 ? (w / rin) : 0.0 - - # Both inner and outer arcs share this exact same angular range. - # This guarantees perfectly radial side walls (a true sector). - θ_out = range(θ_c - dθ/2, θ_c + dθ/2; length = N_pts) - θ_in = reverse(θ_out) - - xo = x0 .+ rex .* cos.(θ_out) - yo = y0 .+ rex .* sin.(θ_out) - - xi = x0 .+ rin .* cos.(θ_in) - yi = y0 .+ rin .* sin.(θ_in) - - # Close the polygon - px = vcat(xo, xi, xo[1]) - py = vcat(yo, yi, yo[1]) - - pts = Makie.Point2f.(px, py) - filter(p -> _valid_finite(p[1], p[2]), pts) -end - -############################# -# Layer -> Makie primitives # -############################# -function _plot_layer_makie!(ax, layer, label::String; - x0::Real = 0.0, y0::Real = 0.0, display_legend::Bool = true, - legend_sink::Union{Nothing, Tuple} = nothing, -) - - if layer isa CircStrands - rwire = to_nominal(layer.radius_wire) - nW = layer.num_wires - lay_r = nW == 1 ? 0.0 : to_nominal(layer.r_in) - color = get_material_color_makie(layer.material_props) - - coords = calc_circstrands_coords(nW, rwire, to_nominal(lay_r), C = (x0, y0)) - - plots = Any[] - handle = nothing - for (i, (x, y)) in enumerate(coords) - poly = Makie.poly!(ax, _circle_poly(rwire, x, y); - color = color, - strokecolor = :black, - strokewidth = 0.5, - label = (i==1 && display_legend) ? label : "") - push!(plots, poly) - if i==1 && display_legend - handle = poly - end - end - - # Legend sink: push one entry per layer. If sink has 3rd slot, store the group. - if legend_sink !== nothing && display_legend && handle !== nothing - push!(legend_sink[1], handle) - push!(legend_sink[2], label) - if length(legend_sink) >= 3 - push!(legend_sink[3], plots) # group = all wires in this layer - end - if length(legend_sink) >= 4 - push!(legend_sink[4], to_nominal(layer.material_props.rho)) # <-- rho key - end - end - return plots - end - - if layer isa RectStrands - rin = to_nominal(layer.r_in) - rex = to_nominal(layer.r_ex) - w = to_nominal(layer.width) - nW = layer.num_wires - color = get_material_color_makie(layer.material_props) - - plots = Any[] - handle = nothing - for i in 1:nW - # Distribute the center points symmetrically around the circle - θ_c = (i - 1) * 2π / nW - - # Draw the constant-width bent rectangle - poly = Makie.poly!(ax, _radial_wedge_poly(rin, rex, w, θ_c, x0, y0); - color = color, - strokecolor = :black, - strokewidth = 0.5, - label = (i == 1 && display_legend) ? label : "") - - push!(plots, poly) - if i == 1 && display_legend - handle = poly - end - end - - # Legend sink: push one entry per layer. If sink has 3rd slot, store the group. - if legend_sink !== nothing && display_legend && handle !== nothing - push!(legend_sink[1], handle) - push!(legend_sink[2], label) - if length(legend_sink) >= 3 - push!(legend_sink[3], plots) # group = all wires in this layer - end - if length(legend_sink) >= 4 - push!(legend_sink[4], to_nominal(layer.material_props.rho)) # <-- rho key - end - end - return plots - end - - if layer isa Strip || layer isa Tubular || - layer isa Semicon || layer isa Insulator - rin = to_nominal(layer.r_in) - rex = to_nominal(layer.r_ex) - color = get_material_color_makie(layer.material_props) - - poly = Makie.poly!(ax, _annulus_poly(rin, rex, x0, y0); - color = color, - label = display_legend ? label : "") - - if legend_sink !== nothing && display_legend - push!(legend_sink[1], poly) - push!(legend_sink[2], label) - if length(legend_sink) >= 3 - push!(legend_sink[3], [poly]) - end - if length(legend_sink) >= 4 - push!(legend_sink[4], NaN) # not a circstrands - end - end - return (poly,) - end - - if layer isa ConductorGroup - plots = Any[] - first_label = true - for sub in layer.layers - append!( - plots, - _plot_layer_makie!(ax, sub, - first_label ? lowercase(string(nameof(typeof(layer)))) : ""; - x0 = x0, y0 = y0, display_legend = display_legend, - legend_sink = legend_sink), - ) - first_label = false - end - return plots - end - - if layer isa Sector - vertices = layer.vertices - # Convert vertices to Makie.Point2f format with offset - makie_points = [Makie.Point2f(v[1] + x0, v[2] + y0) for v in vertices] - # Ensure polygon is closed by adding first point at the end if needed - if length(makie_points) > 0 && makie_points[1] != makie_points[end] - push!(makie_points, makie_points[1]) - end - - color = get_material_color_makie(layer.material_props) - - poly = Makie.poly!(ax, makie_points; - color = color, - strokecolor = :black, - strokewidth = 0.5, - label = display_legend ? label : "") - - if legend_sink !== nothing && display_legend - push!(legend_sink[1], poly) - push!(legend_sink[2], label) - if length(legend_sink) >= 3 - push!(legend_sink[3], [poly]) - end - if length(legend_sink) >= 4 - push!(legend_sink[4], NaN) # not a wirearray - end - end - return (poly,) - end - - if layer isa SectorInsulator - outer_vertices = [(v[1] + x0, v[2] + y0) for v in layer.outer_vertices] - # Convert to Makie.Point2f format - outer_points = [Makie.Point2f(v[1], v[2]) for v in outer_vertices] - # Ensure polygon is closed - if length(outer_points) > 0 && outer_points[1] != outer_points[end] - push!(outer_points, outer_points[1]) - end - - # (Not used for now) The inner boundary is the conductor's vertices. It must be reversed for the hole to be drawn correctly. - inner_vertices = [(v[1] + x0, v[2] + y0) for v in layer.inner_sector.vertices] - inner_points = [Makie.Point2f(v[1], v[2]) for v in inner_vertices] - # Ensure inner polygon is closed - if length(inner_points) > 0 && inner_points[1] != inner_points[end] - push!(inner_points, inner_points[1]) - end - color = get_material_color_makie(layer.material_props) - # Create a shape with a hole by passing the outer boundary and holes as a vector of vectors - polygon_with_hole = Makie.Polygon(outer_points, [inner_points]) - poly = Makie.poly!(ax, polygon_with_hole; - color = color, - strokecolor = :black, - strokewidth = 0.5, - label = display_legend ? label : "") - - if legend_sink !== nothing && display_legend - push!(legend_sink[1], poly) - push!(legend_sink[2], label) - if length(legend_sink) >= 3 - push!(legend_sink[3], [poly]) - end - if length(legend_sink) >= 4 - push!(legend_sink[4], NaN) # not a wirearray - end - end - return (poly,) - end - - @warn "Unknown layer type $(typeof(layer)); skipping" - return () -end - -function apply_default_theme!() - bg = _is_static_backend() ? :white : :gray90 - set_theme!(backgroundcolor = bg, fonts = (; icons = ICON_TTF)) -end - - - -############################################### -# CableDesign cross-section (Makie version) # -############################################### -function preview(design::CableDesign; - x_offset::Real = 0.0, - y_offset::Real = 0.0, - backend::Union{Nothing, Symbol} = nothing, - size::Tuple{Int, Int} = (800, 600), - display_plot::Bool = true, - display_legend::Bool = true, - display_id::Bool = false, - axis = nothing, - legend_sink::Union{Nothing, Tuple{Vector{Any}, Vector{String}}} = nothing, - display_colorbars::Bool = true, - side_frac::Real = 0.26, # ~26% right column -) - - ensure_backend!(backend) - - # backgroundcolor = (_is_static_backend() ? :white : :gray90) - # set_theme!(backgroundcolor = backgroundcolor) - apply_default_theme!() - - fig = - isnothing(axis) ? Makie.Figure(size = size, figure_padding = (10, 10, 10, 10)) : - nothing - - # ── 2 columns: left = main axis, right = container (button + legend + bars) - local ax - local side - if isnothing(axis) - ax = Makie.Axis(fig[1, 1], aspect = Makie.DataAspect()) - side = fig[1, 2] = Makie.GridLayout() # single container on the right - Makie.colsize!(fig.layout, 1, Makie.Relative(1 - side_frac)) - Makie.colsize!(fig.layout, 2, Makie.Relative(side_frac)) - Makie.rowsize!(fig.layout, 1, Makie.Relative(1.0)) - - ax.xlabel = "y [m]" - ax.ylabel = "z [m]" - - ax.title = - display_id ? "Cable design preview: $(design.cable_id)" : - "Cable design preview" - - avail_w = size[1] * (1 - side_frac) - avail_h = size[2] - s = floor(Int, min(avail_w, avail_h)*0.9) - Makie.colsize!(fig.layout, 1, Makie.Fixed(s)) - Makie.rowsize!(fig.layout, 1, Makie.Fixed(s)) - else - ax = axis - side = nothing - end - - # legend sink - local own_legend = false - local sink = legend_sink - if sink === nothing && display_legend - sink = (Any[], String[], Vector{Vector{Any}}(), Float64[]) # handles, labels, groups, rho_keys - own_legend = true - end - - let r = try - to_nominal(design.components[end].insulator_group.r_ex) - catch - NaN - end - if isfinite(r) && r > 0 - Makie.poly!(ax, _circle_poly(r, x_offset, y_offset); - color = :white, - strokecolor = :transparent) - end - end - - # draw layers - for comp in design.components - for layer in comp.conductor_group.layers - _plot_layer_makie!(ax, layer, lowercase(string(nameof(typeof(layer)))); - x0 = x_offset, y0 = y_offset, - display_legend = display_legend, legend_sink = sink) - end - for layer in comp.insulator_group.layers - _plot_layer_makie!(ax, layer, lowercase(string(nameof(typeof(layer)))); - x0 = x_offset, y0 = y_offset, - display_legend = display_legend, legend_sink = sink) - end - end - - - # Right column: stack button, legend, colorbars - if isnothing(axis) - row_idx = 1 - - if _is_interactive_backend() - # Reset button at top - _add_reset_button!(side[row_idx, 1], ax, fig) - row_idx += 1 - _add_save_svg_button!( - side[row_idx, 1], design; - display_id = display_id, - display_legend = display_legend, - display_colorbars = display_colorbars, - side_frac = side_frac, - size = size, - base = design.cable_id, - ) - row_idx += 1 - end - - # Legend (optional) - if display_legend && own_legend - handles = sink[1] - labels = sink[2] - groups = length(sink) >= 3 ? sink[3] : [[h] for h in handles] - rhos = length(sink) >= 4 ? sink[4] : fill(NaN, length(handles)) - - # Merge consecutive entries that share the exact same label and material rho - merged_handles = Any[] - merged_labels = String[] - merged_groups = Vector{Any}[] # Vector{Vector{Any}} - - i = 1 - while i <= length(handles) - h = handles[i] - l = labels[i] - g = groups[i] - ρ = rhos[i] - - # We merge if the rho is finite (i.e., it's a conductor) - if isfinite(ρ) - j = i + 1 - merged_g = Vector{Any}(g) - - # Look ahead: merge as long as the label and rho match the current group - while j <= length(handles) && - labels[j] == l && - isfinite(rhos[j]) && - isapprox(ρ, rhos[j]; rtol = 1e-6, atol = 0.0) - append!(merged_g, groups[j]) - j += 1 - end - - push!(merged_handles, h) # keep first handle for the group - push!(merged_labels, l) # keep the shared label - push!(merged_groups, merged_g) # all sub-elements across merged layers - i = j - else - # Non-finite rho (e.g., insulators where you pushed NaN) do not merge - push!(merged_handles, h) - push!(merged_labels, l) - push!(merged_groups, g) - i += 1 - end - end - - # Build legend with merged entries - leg = Makie.Legend( - side[row_idx, 1], - merged_handles, - merged_labels, - padding = (6, 6, 6, 6), - halign = :center, - valign = :top, - ) - - # Clicking one entry toggles its whole merged group - for (h, grp) in zip(merged_handles, merged_groups) - Makie.on(h.visible) do v - for p in grp - p === h && continue - p.visible[] = v - end - end - end - - row_idx += 1 - end - - # Colorbars (optional) - if display_colorbars - # read actual ranges (helper you already have) - ρmin, ρmax, μmin, μmax, εmin, εmax = _collect_material_ranges(design) - - cbgrid = side[row_idx, 1] = Makie.GridLayout() - _build_colorbars!(cbgrid; ρmin, ρmax, μmin, μmax, εmin, εmax) - - end - end - - if display_plot && isnothing(axis) && !is_in_testset() - resize_to_layout!(fig) - n = next_fignum() - scr = - _is_gl_backend() ? - gl_screen("Fig. $(n) – CableDesign preview: $(design.cable_id)") : - nothing - if scr === nothing - renderfig(fig) - else - display(scr, fig) - end - - end - return fig, ax -end - -function preview(system::LineCableSystem; - earth_model = nothing, - zoom_factor = nothing, - backend::Union{Nothing, Symbol} = nothing, - size::Tuple{Int, Int} = (800, 600), - display_plot::Bool = true, - display_id::Bool = false, - axis = nothing, - display_legend::Bool = true, - display_colorbars::Bool = true, - side_frac::Real = 0.26, -) - - ensure_backend!(backend) - # backgroundcolor = (_is_static_backend() ? :white : :gray90) - - # set_theme!(backgroundcolor = backgroundcolor) - apply_default_theme!() - - fig = - isnothing(axis) ? Makie.Figure(size = size, figure_padding = (10, 10, 10, 10)) : - nothing - - # Layout: left = main axis, right = legend/colorbars (only if we own the axis) - local ax - local side - if isnothing(axis) - ax = Makie.Axis(fig[1, 1], aspect = Makie.DataAspect()) - side = fig[1, 2] = Makie.GridLayout() - Makie.colsize!(fig.layout, 1, Makie.Relative(1 - side_frac)) - Makie.colsize!(fig.layout, 2, Makie.Relative(side_frac)) - Makie.rowsize!(fig.layout, 1, Makie.Relative(1.0)) - - ax.xlabel = "y [m]" - ax.ylabel = "z [m]" - - ax.title = - display_id ? "Cable system cross-section: $(system.system_id)" : - "Cable system cross-section" - - # Make the plotting canvas square if we own the axis - avail_w = size[1] * (1 - side_frac) - avail_h = size[2] - s = floor(Int, min(avail_w, avail_h)*0.9) - Makie.colsize!(fig.layout, 1, Makie.Fixed(s)) - Makie.rowsize!(fig.layout, 1, Makie.Fixed(s)) - else - ax = axis - side = nothing - end - - # Air/earth interface - Makie.hlines!(ax, [0.0], color = :black, linewidth = 1.5) - - # Compute barycentered, square view from cable bounding box - x0s = Float64[to_nominal(c.horz) for c in system.cables] - y0s = Float64[to_nominal(c.vert) for c in system.cables] - radii = Float64[ - (comp = last(c.design_data.components); - max(to_nominal(comp.conductor_group.r_ex), - to_nominal(comp.insulator_group.r_ex))) - for c in system.cables - ] - cx = isempty(x0s) ? 0.0 : mean(x0s) - cy = isempty(y0s) ? -1.0 : mean(y0s) - x_min = isempty(x0s) ? -1.0 : minimum(x0s .- radii) - x_max = isempty(x0s) ? 1.0 : maximum(x0s .+ radii) - y_min = isempty(y0s) ? -1.0 : minimum(y0s .- radii) - y_max = isempty(y0s) ? 1.0 : maximum(y0s .+ radii) - half_x = max(x_max - cx, cx - x_min) - half_y = max(y_max - cy, cy - y_min) - base_halfspan = max(half_x, half_y) - base_halfspan = base_halfspan > 0 ? base_halfspan : 1.0 - pad_factor = 1.05 - zf = zoom_factor === nothing ? 1.5 : Float64(zoom_factor) - halfspan = base_halfspan * pad_factor * zf - x_limits = (cx - halfspan, cx + halfspan) - y_limits = (cy - halfspan, cy + halfspan) - - # Expanded fill extents beyond visible region - BUFFER_FILL = 5.0 - x_fill = - (x_limits[1] - 0.5*halfspan - BUFFER_FILL, x_limits[2] + 0.5*halfspan + BUFFER_FILL) - y_fill_min = y_limits[1] - 0.5*halfspan - BUFFER_FILL - - # Build legend entries only for earth layers - earth_handles = Any[] - earth_labels = String[] - - # Plot earth layers if provided and horizontal (vertical_layers == false) - if !isnothing(earth_model) && getproperty(earth_model, :vertical_layers) == false - cumulative_depth = 0.0 - # Skip air layer (index 1). Iterate finite-thickness layers; stop on Inf. - for (i, layer) in enumerate(earth_model.layers[2:end]) - # Compute color using the same material convention - # Adapt EarthLayer base_* fields to material_props (rho, eps_r, mu_r) - mat = (; - rho = layer.base_rho_g, - eps_r = layer.base_epsr_g, - mu_r = layer.base_mur_g, - ) - fillcol = get_material_color_makie(mat) - # Slight transparency for fill - fillcol = Makie.RGBA(fillcol.r, fillcol.g, fillcol.b, 0.25) - - if isinf(layer.t) - # Semi-infinite: fill from current depth down to far below visible - ytop = cumulative_depth # bottom of previous finite layer - ybot = y_fill_min # push well below visible range - else - # Finite thickness: update cumulative and compute band extents - t = to_nominal(layer.t) - ytop = cumulative_depth - ybot = cumulative_depth - t - cumulative_depth = ybot - end - - xs = (x_fill[1], x_fill[2], x_fill[2], x_fill[1]) - ys = (ytop, ytop, ybot, ybot) - - # Filled band and a colored interface line - poly = Makie.poly!( - ax, - collect(Makie.Point2f.(xs, ys)), # ensure a Vector, not a Tuple - color = fillcol, - strokecolor = :transparent, - label = "", - ) - Makie.hlines!(ax, [ybot], color = fillcol, linewidth = 1.0) - - if display_legend && isnothing(axis) - push!(earth_handles, poly) - push!(earth_labels, "Earth layer $(i)") - end - end - end - - # Draw each cable onto the same axis (no legend for cable components) - for cable in system.cables - x0 = to_nominal(cable.horz) - y0 = to_nominal(cable.vert) - # Reuse the design-level preview on our axis - preview( - cable.design_data; - x_offset = x0, - y_offset = y0, - backend = backend, - size = size, - display_plot = false, - display_legend = false, - axis = ax, - ) - end - - # Set limits only when we own the axis (square extents) - if isnothing(axis) - Makie.xlims!(ax, x_limits...) - Makie.ylims!(ax, y_limits...) - end - - # Right-column: buttons, earth-only legend and optional colorbars - if isnothing(axis) - row_idx = 1 - if _is_interactive_backend() - _add_reset_button!(side[row_idx, 1], ax, fig) - row_idx += 1 - _add_save_svg_button!( - side[row_idx, 1], system; - earth_model = earth_model, - zoom_factor = zoom_factor, - display_legend = display_legend, - display_colorbars = display_colorbars, - side_frac = side_frac, - display_id = display_id, - size = size, - base = system.system_id, - ) - row_idx += 1 - end - - if display_legend && !isempty(earth_handles) - Makie.Legend( - side[row_idx, 1], - earth_handles, - earth_labels, - padding = (6, 6, 6, 6), - halign = :center, - valign = :top, - ) - row_idx += 1 - end - - if display_colorbars - ρmin, ρmax, μmin, μmax, εmin, εmax = _collect_earth_ranges(earth_model) - cbgrid = side[row_idx, 1] = Makie.GridLayout() - _build_colorbars!( - cbgrid; - ρmin, - ρmax, - μmin, - μmax, - εmin, - εmax, - alpha_global = 0.25, - showμminmax = false, - showεminmax = false, - ) - end - end - - - if display_plot && isnothing(axis) && !is_in_testset() - resize_to_layout!(fig) - n = next_fignum() - scr = - _is_gl_backend() ? - gl_screen("Fig. $(n) – LineCableSystem preview: $(system.system_id)") : - nothing - if scr === nothing - renderfig(fig) - else - display(scr, fig) - end - - end - - return fig, ax -end - -# Add a save-to-SVG button to a grid cell, re-rendering with Cairo backend -function _add_save_svg_button!(parent_cell, system; - earth_model = nothing, - zoom_factor = nothing, - display_id::Bool, - display_legend::Bool, - display_colorbars::Bool, - side_frac::Real, - size::Tuple{Int, Int}, - base::String = "preview", - save_dir::AbstractString = pwd(), -) - btn = Makie.Button( - parent_cell, - label = with_icon(MI_SAVE; text = "Save SVG"), - halign = :center, - valign = :top, - width = Makie.Auto(), - ) - Makie.on(btn.clicks) do _ - @async begin - orig_label = btn.label[] - btn.label[] = "Saving…" - orig_color = hasproperty(btn, :buttoncolor) ? btn.buttoncolor[] : nothing - try - # _use_makie_backend(:cairo) - ensure_backend!(:cairo) - if system isa CableDesign - fig, _ = preview( - system; - display_legend = display_legend, - display_colorbars = display_colorbars, - display_plot = false, - size = size, - display_id = display_id, - backend = :cairo, - side_frac = side_frac, - ) - elseif system isa LineCableSystem - fig, _ = preview( - system; - earth_model = earth_model, - zoom_factor = zoom_factor, - display_id = display_id, - display_legend = display_legend, - display_colorbars = display_colorbars, - display_plot = false, - size = size, - backend = :cairo, - side_frac = side_frac, - ) - end - ts = Dates.format(Dates.now(), "yyyymmdd-HHMMSS") - file = joinpath(save_dir, "$(base)_$ts.svg") - Makie.save(file, fig) - btn.label[] = "Saved ✓" - @info "Saved figure to $(file)" - hasproperty(btn, :buttoncolor) && - (btn.buttoncolor[] = Makie.RGBA(0.15, 0.65, 0.25, 1.0)) - sleep(1.2) - catch e - @error "Save failed: $(typeof(e)): $(e)" - btn.label[] = "Failed ✗" - hasproperty(btn, :buttoncolor) && - (btn.buttoncolor[] = Makie.RGBA(0.80, 0.20, 0.20, 1.0)) - sleep(1.6) - finally - if orig_color !== nothing - btn.buttoncolor[] = orig_color - end - btn.label[] = orig_label - end - end - end - return btn -end - -# Add a reset button to a grid cell, wired to reset axis limits -function _add_reset_button!(parent_cell, ax, fig) - btn = Makie.Button( - parent_cell, - label = with_icon(MI_REFRESH; text = "Reset view"), - halign = :center, - valign = :top, - width = Makie.Relative(1.0), - ) - Makie.on(btn.clicks) do _ - reset_limits!(ax) - # resize_to_layout!(fig) - - end - return btn -end - -function _build_colorbars!(cbgrid::Makie.GridLayout; - ρmin::Real, ρmax::Real, μmin::Real, μmax::Real, εmin::Real, εmax::Real, - cb_bar_h::Int = 12, alpha_global::Real = 1.0, showρminmax::Bool = true, - showμminmax::Bool = true, showεminmax::Bool = true, -) - Makie.colsize!(cbgrid, 1, Makie.Fixed(2)) - - function _nice(x) - axv = abs(x) - axv == 0 && return "0" - (axv ≥ 1e-3 && axv < 1e4) ? @sprintf("%.4g", x) : @sprintf("%.1e", x) - end - - N = 256 - idx=1 - - # ρ bar sampled in log-space between actual min/max - if showρminmax - cm_ρ = let cols = Vector{Makie.RGBA}(undef, N) - lo, hi = log10(ρmin), log10(ρmax) - for i in 1:N - t = (i-1)/(N-1) - ρ = 10^(lo + t*(hi - lo)) - c = _base_color_from_rho(ρ) - cols[i] = Makie.RGBA(c.r, c.g, c.b, 1*alpha_global) - end; - cols - end - Makie.Label(cbgrid[idx, 1], L"\rho"; halign = :left, fontsize = 16) - Makie.Colorbar(cbgrid[idx, 2]; colormap = cm_ρ, limits = (0.0, 1.0), - vertical = false, - ticks = ([0.0, 1.0], [_nice(ρmin), _nice(ρmax)]), - labelvisible = false, height = cb_bar_h) - idx += 1 - end - - # μr bar overlay on mid-gray - if showμminmax - base_mid = Makie.RGB(0.5, 0.5, 0.5) - cm_μ = let cols = Vector{Makie.RGBA}(undef, N) - lo, hi = log10(μmin), log10(μmax) - for i in 1:N - t = (i-1)/(N-1) - μ = 10^(lo + t*(hi - lo)) - o = _mu_overlay(base_mid, μ) - cols[i] = _overlay( - Makie.RGBA(base_mid.r, base_mid.g, base_mid.b, 1), - o*alpha_global, - ) - end; - cols - end - Makie.Label(cbgrid[idx, 1], L"\mu_{r}"; halign = :left, fontsize = 16) - Makie.Colorbar(cbgrid[idx, 2]; colormap = cm_μ, limits = (0.0, 1.0), - vertical = false, - ticks = ([0.0, 1.0], [_nice(μmin), _nice(μmax)]), - labelvisible = false, height = cb_bar_h) - idx += 1 - end - - if showεminmax - # εr bar overlay on dark - base_dark = Makie.RGB(0.10, 0.10, 0.10) - cm_ε = let cols = Vector{Makie.RGBA}(undef, N) - lo, hi = log10(εmin), log10(εmax) - for i in 1:N - t = (i-1)/(N-1) - ε = 10^(lo + t*(hi - lo)) - o = _eps_overlay(base_dark, ε, RHO_MAX + 1) - cols[i] = _overlay( - Makie.RGBA(base_dark.r, base_dark.g, base_dark.b, 1), - o*alpha_global, - ) - end; - cols - end - Makie.Label(cbgrid[idx, 1], L"\varepsilon_{r}"; halign = :left, fontsize = 16) - Makie.Colorbar(cbgrid[idx, 2]; colormap = cm_ε, limits = (0.0, 1.0), - vertical = false, - ticks = ([0.0, 1.0], [_nice(εmin), _nice(εmax)]), - labelvisible = false, height = cb_bar_h) - end - - return cbgrid -end - -# collect actual property ranges from the design (finite values only) -function _collect_material_ranges(design::CableDesign) - rhos = Float64[] - mus = Float64[] - epses = Float64[] - - _push_props!(layer) = begin - ρ = try - to_nominal(layer.material_props.rho) - catch - NaN - end - μr = try - to_nominal(layer.material_props.mu_r) - catch - NaN - end - εr = try - to_nominal(layer.material_props.eps_r) - catch - NaN - end - isfinite(ρ) && push!(rhos, ρ) - isfinite(μr) && push!(mus, μr) - isfinite(εr) && push!(epses, εr) - nothing - end - - for comp in design.components - for L in comp.conductor_group.layers - if L isa ConductorGroup - for s in L.layers - _push_props!(s); - end - else - _push_props!(L) - end - end - for L in comp.insulator_group.layers - _push_props!(L) - end - end - - ρmin = isempty(rhos) ? RHO_MIN : minimum(rhos) - ρmax = isempty(rhos) ? RHO_MAX : maximum(rhos) - μmin = isempty(mus) ? 1.0 : max(1.0, minimum(mus)) - μmax = isempty(mus) ? 300.0 : maximum(mus) - εmin = isempty(epses) ? 1.0 : max(1.0, minimum(epses)) - εmax = isempty(epses) ? 1000.0 : maximum(epses) - ρmax == ρmin && (ρmax = nextfloat(ρmax)) - μmax == μmin && (μmax += 1e-6) - εmax == εmin && (εmax += 1e-6) - - return ρmin, ρmax, μmin, μmax, εmin, εmax -end - -function _collect_earth_ranges(earth_model) - rhos = Float64[]; - mus = Float64[]; - epses = Float64[] - if !isnothing(earth_model) - for layer in earth_model.layers[2:end] - ρ = try - to_nominal(layer.base_rho_g) - catch - NaN - end - μr = try - to_nominal(layer.base_mur_g) - catch - NaN - end - εr = try - to_nominal(layer.base_epsr_g) - catch - NaN - end - isfinite(ρ) && push!(rhos, ρ) - isfinite(μr) && push!(mus, μr) - isfinite(εr) && push!(epses, εr) - end - end - ρmin = isempty(rhos) ? RHO_MIN : minimum(rhos) - ρmax = isempty(rhos) ? RHO_MAX : maximum(rhos) - μmin = isempty(mus) ? 1.0 : max(1.0, minimum(mus)) - μmax = isempty(mus) ? 300.0 : maximum(mus) - εmin = isempty(epses) ? 1.0 : max(1.0, minimum(epses)) - εmax = isempty(epses) ? 1000.0 : maximum(epses) - return ρmin, ρmax, μmin, μmax, εmin, εmax -end diff --git a/src/datamodel/preview/geometry.jl b/src/datamodel/preview/geometry.jl new file mode 100644 index 000000000..c158b97e6 --- /dev/null +++ b/src/datamodel/preview/geometry.jl @@ -0,0 +1,44 @@ +""" +$(TYPEDEF) + +Detached geometry and material identity for one physical cable region. + +Renderers choose colors, strokes, labels, and legend grouping for each region. + +$(TYPEDFIELDS) +""" +struct PreviewShape{G, M} + "Closed two-dimensional region geometry in meters." + geometry::G + "Physical material represented by the region." + material::M + "Stable construction tag carried by the source region." + tag::Symbol + function PreviewShape(geometry::G, material::M, tag::Symbol) where {G, M} + return validate(new{G, M}(geometry, material, tag)) + end +end + +function validate(shape::PreviewShape) + points = shape.geometry isa GeometryBasics.Polygon ? + Iterators.flatten((shape.geometry.exterior, shape.geometry.interiors...)) : + shape.geometry + applicable(iterate, points) || throw(ArgumentError( + "PreviewShape.geometry must be a collection of points", + )) + found = false + for point in points + found = true + coordinates = collect(point) + length(coordinates) >= 2 || throw(ArgumentError( + "PreviewShape.geometry points require at least two coordinates", + )) + all(value -> value isa Real && isfinite(value), coordinates) || + throw(ArgumentError( + "PreviewShape.geometry coordinates must be finite real numbers", + )) + end + found || throw(ArgumentError("PreviewShape.geometry cannot be empty")) + isempty(String(shape.tag)) && throw(ArgumentError("PreviewShape.tag cannot be empty")) + return shape +end diff --git a/src/datamodel/preview/materials.jl b/src/datamodel/preview/materials.jl new file mode 100644 index 000000000..f668934a7 --- /dev/null +++ b/src/datamodel/preview/materials.jl @@ -0,0 +1,156 @@ +const _RHO_MIN = 1.0e-9 +const _RHO_MAX = 1.0e10 + +_finite_point(point) = isfinite(point[1]) && isfinite(point[2]) + +function _circle_points(radius, xcenter, ycenter; count::Int = 128) + angles = range(0, 2π; length = count) + return filter( + _finite_point, + Point2f.(xcenter .+ radius .* cos.(angles), ycenter .+ radius .* sin.(angles)) + ) +end + +function _annulus_polygon(inner_radius, outer_radius, xcenter, ycenter; count::Int = 256) + outer = _circle_points(outer_radius, xcenter, ycenter; count) + iszero(inner_radius) && return GeometryBasics.Polygon(outer) + inner = reverse(_circle_points(inner_radius, xcenter, ycenter; count)) + return GeometryBasics.Polygon(outer, [inner]) +end + +function _transform_preview_points(points, pose::Pose2) + cosine = cos(nominal(pose.φ)) + sine = sin(nominal(pose.φ)) + x = nominal(pose.x) + y = nominal(pose.y) + return Point2f[( + x + cosine * nominal(point[1]) - sine * nominal(point[2]), + y + sine * nominal(point[1]) + cosine * nominal(point[2]) + ) for point in points] +end + +function _primitive_geometry(primitive::Disk) + return GeometryBasics.Polygon(_circle_points(nominal(primitive.r), 0.0, 0.0)) +end + +function _primitive_geometry(primitive::Annulus) + return _annulus_polygon( + nominal(primitive.ri), nominal(primitive.ro), 0.0, 0.0 + ) +end + +function _primitive_geometry(primitive::Rectangle) + x = nominal(primitive.w) / 2 + y = nominal(primitive.h) / 2 + return GeometryBasics.Polygon(Point2f[(-x, -y), (x, -y), (x, y), (-x, y)]) +end + +function _primitive_geometry(primitive::Ellipse) + angles = range(0, 2pi; length = 128) + return GeometryBasics.Polygon(Point2f[(nominal(primitive.a) * cos(angle), + nominal(primitive.b) * sin(angle)) + for angle in angles]) +end + +function _primitive_geometry(primitive::Polygon) + return GeometryBasics.Polygon(Point2f[point for point in primitive.points]) +end + +function _shape_geometry(primitive::AbstractPrimitive) + geometry = _primitive_geometry(primitive) + exterior = _transform_preview_points(geometry.exterior, primitive.at) + interiors = [_transform_preview_points(interior, primitive.at) + for interior in geometry.interiors] + return GeometryBasics.Polygon(exterior, interiors) +end + +function _shape_geometry(primitive::DifferenceShape) + outer = _shape_geometry(primitive.outer) + interiors = copy(outer.interiors) + for hole in primitive.holes + push!(interiors, reverse(_shape_geometry(hole).exterior)) + end + return GeometryBasics.Polygon(outer.exterior, interiors) +end + +function _shape_geometry(shape::SectorShape) + return GeometryBasics.Polygon(Point2f.(tessellate(shape))) +end + +function _shape_geometry(shape::EllipseOffset) + return GeometryBasics.Polygon(Point2f.(tessellate(shape))) +end + +function _shape_geometry(shape::BentStrip) + return GeometryBasics.Polygon(Point2f.(tessellate(shape))) +end + +function _shape_geometry(shape::ShellShape) + coordinates = tessellate(shape) + outer = Point2f.(coordinates.outer) + inner = reverse(Point2f.(coordinates.inner)) + return GeometryBasics.Polygon(outer, [inner]) +end + +preview_materials(region::PlacedRegion) = (region.source.material,) + +function preview_shapes(region::PlacedRegion) + return PreviewShape[ + PreviewShape( + _shape_geometry(region.primitive), + region.source.material, + region.source.tag + ), + ] +end + +function _each_material(callback, design::CableDesign) + for source in design.geometry.regions + foreach(callback, preview_materials(source)) + end + return nothing +end + +function _each_material(callback, designs::AbstractVector{<:CableDesign}) + for design in designs + _each_material(callback, design) + end + return nothing +end + +function _property_ranges(design) + resistivities = Float64[] + permeabilities = Float64[] + permittivities = Float64[] + _each_material(design) do material + resistivity = nominal(material.rho) + permeability = nominal(material.mu_r) + permittivity = nominal(material.eps_r) + isfinite(resistivity) && push!(resistivities, resistivity) + isfinite(permeability) && push!(permeabilities, permeability) + isfinite(permittivity) && push!(permittivities, permittivity) + end + return (; + rho = isempty(resistivities) ? + (_RHO_MIN, _RHO_MAX) : extrema(resistivities), + mu_r = isempty(permeabilities) ? + (1.0, 300.0) : extrema(permeabilities), + eps_r = isempty(permittivities) ? + (1.0, 1000.0) : extrema(permittivities) + ) +end + +""" + material_property_ranges() + material_property_ranges(design::CableDesign) + material_property_ranges(designs::AbstractVector{<:CableDesign}) + +Return physical resistivity, relative permeability, and relative permittivity +ranges without assigning any presentation color scheme. +""" +function material_property_ranges() + return (; rho = (_RHO_MIN, _RHO_MAX), mu_r = (1.0, 300.0), eps_r = (1.0, 1000.0)) +end + +material_property_ranges(design::CableDesign) = _property_ranges(design) +material_property_ranges(designs::AbstractVector{<:CableDesign}) = _property_ranges(designs) diff --git a/src/datamodel/radii.jl b/src/datamodel/radii.jl deleted file mode 100644 index 963e7c414..000000000 --- a/src/datamodel/radii.jl +++ /dev/null @@ -1,130 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Resolves radius parameters for cable components, converting from various input formats to standardized inner radius, outer radius, and thickness values. - -This function serves as a high-level interface to the radius resolution system. It processes inputs through a two-stage pipeline: -1. First normalizes input parameters to consistent forms using [`_parse_radius_operand`](@ref). -2. Then delegates to specialized implementations via [`_do_resolve_radius`](@ref) based on the component type. - -# Arguments - -- `param_in`: Inner boundary parameter (defaults to radius) \\[m\\]. - Can be a number, a [`Diameter`](@ref) , a [`Thickness`](@ref), or an [`AbstractCablePart`](@ref). -- `param_ext`: Outer boundary parameter (defaults to radius) \\[m\\]. - Can be a number, a [`Diameter`](@ref) , a [`Thickness`](@ref), or an [`AbstractCablePart`](@ref). -- `object_type`: Type associated to the constructor of the new [`AbstractCablePart`](@ref). - -# Returns - -- `r_in`: Normalized inner radius \\[m\\]. -- `r_ex`: Normalized outer radius \\[m\\]. -- `thickness`: Computed thickness or specialized dimension depending on the method \\[m\\]. - For [`CircStrands`](@ref) components, this value represents the wire radius instead of thickness. - -# See also - -- [`Diameter`](@ref) -- [`Thickness`](@ref) -- [`AbstractCablePart`](@ref) -""" -@inline _normalize_radii(::Type{T}, rin, rex) where {T} = - _do_normalize_radii(_parse_radius_operand(rin, T), _parse_radius_operand(rex, T), T) - -""" -$(TYPEDSIGNATURES) - -Parses input values into radius representation based on object type and input type. - -# Arguments - -- `x`: Input value that can be a raw number, a [`Diameter`](@ref), a [`Thickness`](@ref), or other convertible type \\[m\\]. -- `object_type`: Type parameter used for dispatch. - -# Returns - -- Parsed radius value in appropriate units \\[m\\]. - -# Examples - -```julia -radius = $(FUNCTIONNAME)(10.0, ...) # Direct radius value -radius = $(FUNCTIONNAME)(Diameter(20.0), ...) # From diameter object -radius = $(FUNCTIONNAME)(Thickness(5.0), ...) # From thickness object -``` - -# Methods - -$(METHODLIST) - -# See also - -- [`Diameter`](@ref) -- [`Thickness`](@ref) -""" -function _parse_radius_operand end - -# ------------ Input parsing -@inline _parse_radius_operand(x::Number, ::Type{T}) where {T} = x -@inline _parse_radius_operand(d::Diameter, ::Type{T}) where {T} = d.value / 2 -@inline _parse_radius_operand(p::Thickness, ::Type{T}) where {T} = p -# A part proxy denotes the exact physical interface shared by two adjacent -# layers. Preserve the complete Measurement derivative graph of that boundary; -# stripping it here makes the same radius statistically different on each side -# of the interface and breaks cumulative-geometry covariance. -@inline _parse_radius_operand(p::AbstractCablePart, ::Type{T}) where {T} = - getproperty(p, :r_ex) -@inline _parse_radius_operand(x::AbstractString, ::Type{T}) where {T} = - throw( - ArgumentError( - "[$(nameof(T))] radius parameter must be numeric, not String: $(repr(x))", - ), - ) -@inline _parse_radius_operand(x, ::Type{T}) where {T} = - throw( - ArgumentError( - "[$(nameof(T))] unsupported radius parameter $(typeof(x)): $(repr(x))", - ), - ) - -# ------------ Input parsing -@inline function _do_normalize_radii( - r_in::Number, - r_ex::Number, - ::Type{T}, -) where {T} - return r_in, r_ex -end - -@inline function _do_normalize_radii( - r_in::Number, - thickness::Thickness, - ::Type{T}, -) where {T} - return r_in, (r_in + thickness.value) -end - -@inline function _do_normalize_radii( - r_in::Number, - radius_wire::Number, - ::Type{AbstractStrandsLayer}, -) - return r_in, r_in + (2 * radius_wire) -end - -@inline function _do_normalize_radii(t::Thickness, rex::Number, ::Type{T}) where {T} - rin = rex - t.value - rin >= 0 || throw( - ArgumentError("[$(nameof(T))] thickness $(t.value) exceeds outer radius $(rex)."), - ) - return rin, rex -end - -# NEW: reject thickness on BOTH ends -@inline function _do_normalize_radii(::Thickness, ::Thickness, ::Type{T}) where {T} - throw( - ArgumentError( - "[$(nameof(T))] cannot specify thickness for both inner and outer radii.", - ), - ) -end diff --git a/src/datamodel/rectstrands.jl b/src/datamodel/rectstrands.jl deleted file mode 100644 index 2f3a373aa..000000000 --- a/src/datamodel/rectstrands.jl +++ /dev/null @@ -1,211 +0,0 @@ -""" -$(TYPEDEF) - -Holds the pure geometric layout for a concentric layer of rectangular/flat strands. -""" -struct RectStrandsShape{T <: REALSCALAR, U <: Int} <: AbstractShapeGeometry - "Thickness of the individual rectangular strip \\[m\\]." - thickness::T - "Width of the individual rectangular strip \\[m\\]." - width::T - "Number of wires in the layer \\[dimensionless\\]." - num_wires::U - "Ratio defining the lay length of the wires \\[dimensionless\\]." - lay_ratio::T - "Twisting direction of the strands (1 = unilay, -1 = contralay) \\[dimensionless\\]." - lay_direction::U - "Mean diameter of the wire layer \\[m\\]." - mean_diameter::T - "Pitch length of the wire layer \\[m\\]." - pitch_length::T - "Total cross-sectional area of the conductive metal in this layer \\[m²\\]." - cross_section::T -end - -""" -$(TYPEDEF) - -Represents a concentric layer of rectangular strands with defined geometric, material, and electrical properties: - -$(TYPEDFIELDS) -""" -struct RectStrands{T <: REALSCALAR, S <: RectStrandsShape} <: AbstractStrandsLayer{T} - "Internal radial boundary \\[m\\]." - r_in::T - "External radial boundary \\[m\\]." - r_ex::T - "Material properties of the conductive strands." - material_props::Material{T} - "Operating temperature of the layer \\[°C\\]." - temperature::T - "Equivalent electrical resistance of the layer \\[Ω/m\\]." - resistance::T - "Geometric mean radius (GMR) of the layer \\[m\\]." - gmr::T - "Shape payload defining the internal geometric layout." - shape::S -end - -# struct SectorCore{T <: REALSCALAR, S <: SectorShape} <: AbstractStrandsLayer{T} -# "Internal radial boundary \\[m\\]." -# r_in::T -# "External radial boundary \\[m\\]." -# r_ex::T -# "Material properties of the conductive strands." -# material_props::Material{T} -# "Operating temperature of the layer \\[°C\\]." -# temperature::T -# "Equivalent electrical resistance of the layer \\[Ω/m\\]." -# resistance::T -# "Geometric mean radius (GMR) of the layer \\[m\\]." -# gmr::T -# "Shape payload defining the internal geometric layout." -# shape::S -# end - -# struct CircCore{T <: REALSCALAR, S <: Concentric} <: AbstractStrandsLayer{T} -# "Internal radial boundary \\[m\\]." -# r_in::T -# "External radial boundary \\[m\\]." -# r_ex::T -# "Material properties of the conductive strands." -# material_props::Material{T} -# "Operating temperature of the layer \\[°C\\]." -# temperature::T -# "Equivalent electrical resistance of the layer \\[Ω/m\\]." -# resistance::T -# "Geometric mean radius (GMR) of the layer \\[m\\]." -# gmr::T -# "Shape payload defining the internal geometric layout." -# shape::S -# end - -""" -$(TYPEDSIGNATURES) - -Constructs a [`RectStrands`](@ref) instance. - -# Arguments - -- `r_in`: Internal radius of the layer \\[m\\]. -- `thickness`: Radial thickness of the strands \\[m\\]. -- `width`: Width of the individual rectangular strip \\[m\\]. -- `num_wires`: Number of strands in the layer \\[dimensionless\\]. -- `lay_ratio`: Ratio defining the lay length of the strands \\[dimensionless\\]. -- `material_props`: A [`Material`](@ref) object containing physical properties. -- `temperature`: Operating temperature \\[°C\\]. -- `lay_direction`: Twisting direction (1 = unilay, -1 = contralay) \\[dimensionless\\]. - -# Returns - -- A [`RectStrands`](@ref) object with calculated geometric and electrical properties. - -# Examples - -```julia -material = Material(1.724e-8, 1.0, 1.0, 20.0, 0.00393) -layer = $(FUNCTIONNAME)(0.01, 0.002, 0.005, 10, 12.0, material, 25.0, 1) -``` -""" -function RectStrands( - r_in::T, - thickness::T, - width::T, - num_wires::U, - lay_ratio::T, - material_props::Material{T}, - temperature::T, - lay_direction::U, -) where {T <: REALSCALAR, U <: Int} - - - # Target Area (the 'invariant' property) - A0 = width * thickness - - # Area-Preserving Radial Expansion - # This solves A_total = pi * (r_ext^2 - r_in^2) - r_ex = - num_wires == 1 ? Base.error("num_wires must be > 1") : - sqrt(r_in^2 + (num_wires * A0) / T(π)) - thickness_effective=r_ex-r_in - @info "Calculating outer radius to preserve total cross-sectional area of strands." r_ex radius_ext_without_correction=r_in+thickness thickness_effective thickness num_wires - mean_diameter, pitch_length, overlength = - calc_helical_params(r_in, r_ex, lay_ratio) - - cross_section = num_wires * A0 - - shape_payload = RectStrandsShape( - thickness_effective, width, num_wires, lay_ratio, lay_direction, - mean_diameter, pitch_length, cross_section, - ) - - # Electrical properties - rho = material_props.rho - T0 = material_props.T0 - alpha = material_props.alpha - - R_wire = - calc_strip_resistance(thickness, width, rho, alpha, T0, temperature) * overlength - R_layer = R_wire / num_wires - - gmr_layer = calc_tubular_gmr(r_ex, r_in, material_props.mu_r) - - # 3. Instantiate the concrete struct - return RectStrands( - r_in, - r_ex, - material_props, - temperature, - R_layer, - gmr_layer, - shape_payload, - ) -end - -const _REQ_RECTSTRANDS = - (:r_in, :thickness, :width, :num_wires, :lay_ratio, :material_props) -const _OPT_RECTSTRANDS = (:temperature, :lay_direction) -const _DEFS_RECTSTRANDS = (T₀, 1) - -Validation.has_radii(::Type{RectStrands}) = false -Validation.has_temperature(::Type{RectStrands}) = true -Validation.required_fields(::Type{RectStrands}) = _REQ_RECTSTRANDS -Validation.keyword_fields(::Type{RectStrands}) = _OPT_RECTSTRANDS -Validation.keyword_defaults(::Type{RectStrands}) = _DEFS_RECTSTRANDS - -Validation.coercive_fields(::Type{RectStrands}) = - (:r_in, :thickness, :width, :lay_ratio, :material_props, :temperature) - -Validation.is_radius_input( - ::Type{RectStrands}, - ::Val{:r_in}, - x::AbstractCablePart, -) = true - -Validation.is_radius_input(::Type{RectStrands}, ::Val{:r_in}, x::Thickness) = true - -# Specific for rectangular strands -Validation.maxfill(::Type{RectStrands}, rin::Real, w::Real) = - floor(Int, 2 * π * rin / w) - -Validation.extra_rules(::Type{RectStrands}) = ( - Normalized(:r_in), Finite(:r_in), Nonneg(:r_in), - Normalized(:thickness), Finite(:thickness), Positive(:thickness), - IntegerField(:num_wires), Positive(:num_wires), - Finite(:lay_ratio), Nonneg(:lay_ratio), - IsA{Material}(:material_props), - OneOf(:lay_direction, (-1, 1)), Finite(:width), - Positive(:width), - PhysicalFillLimit(:num_wires, (:r_in, :width)), # THE BOUNCER -) - -Validation.parse(::Type{RectStrands}, nt) = begin - rin, rw = _normalize_radii(RectStrands, nt.r_in, nt.thickness) - - # Resolves to Int using the generic interface - n_wires = _resolve_strands(nt.num_wires, RectStrands, rin, nt.width) - - return (; nt..., r_in = rin, thickness = rw, num_wires = n_wires) -end - -@construct RectStrands _REQ_RECTSTRANDS _OPT_RECTSTRANDS _DEFS_RECTSTRANDS \ No newline at end of file diff --git a/src/datamodel/sector.jl b/src/datamodel/sector.jl deleted file mode 100644 index c76255dba..000000000 --- a/src/datamodel/sector.jl +++ /dev/null @@ -1,397 +0,0 @@ -""" -$(TYPEDEF) - -Holds the geometric parameters that define the shape of a sector conductor. - -$(TYPEDFIELDS) -""" -struct SectorParams{T<:REALSCALAR} - "Number of sectors in the full cable (e.g., 3 or 4)." - n_sectors::Int - "Back radius of the sector (outermost curve) \\[m\\]." - r_back::T - "Depth of the sector from the back to the base \\[m\\]." - d_sector::T - "Corner radius for rounding sharp edges \\[m\\]." - r_corner::T - "Angular width of the conductor's flat base/sides [degrees]." - theta_cond_deg::T - "Insulation thickness \\[m\\]." - d_insulation::T # needed to correct for the offset - - function SectorParams(n_sectors::Int, r_back::T, d_sector::T, r_corner::T, theta_cond_deg::T, d_insulation::T) where {T<:REALSCALAR} - # validation for the sector geometry constraints (angle limits, no overlap, discriminant check) - _validate_sector_params(n_sectors, r_back, d_sector, r_corner, theta_cond_deg, d_insulation) - new{T}(n_sectors, r_back, d_sector, r_corner, theta_cond_deg, d_insulation) - end -end - -const _REQ_SECTOR_PARAMS = (:n_sectors, :r_back, :d_sector, :r_corner, :theta_cond_deg, :d_insulation) - -Validation.has_radii(::Type{SectorParams}) = false -Validation.required_fields(::Type{SectorParams}) = _REQ_SECTOR_PARAMS -Validation.coercive_fields(::Type{SectorParams}) = (:r_back, :d_sector, :r_corner, :theta_cond_deg, :d_insulation) - -# --- Geometry Logic Storage --- -function _validate_sector_params(n_sectors, r_back, d_sector, r_corner, theta_cond_deg, d_insulation) - # 1. Basic Non-negativity (redundant with rules but kept for Core safety) - if r_back < 0 || d_sector < 0 || r_corner < 0 || theta_cond_deg < 0 || d_insulation < 0 - throw(ArgumentError("Sector geometry parameters must be non-negative.")) - end - - # 2. Basic constraints - if theta_cond_deg >= 360.0 - throw(ArgumentError("[SectorParams] theta_cond_deg ($theta_cond_deg) must be < 360")) - end - allowed_angle = 360.0 / n_sectors - if theta_cond_deg > allowed_angle + 1e-4 - throw(ArgumentError("[SectorParams] theta_cond_deg ($theta_cond_deg) exceeds allowed 360/n ($allowed_angle)")) - end - - if d_sector >= r_back - throw(ArgumentError("[SectorParams] d_sector ($d_sector) must be less than r_back ($r_back)")) - end - - # 3. Geometric feasibility (Discriminant check) - phi_deg = 90.0 - theta_cond_deg / 2.0 - phi_rad = deg2rad(phi_deg) - - if abs(cos(phi_rad)) < 1e-9 - throw(ArgumentError("[SectorParams] theta_cond_deg too close to 180 (phi ~ 90), valid sector cannot be formed.")) - end - - d_base_corner = r_corner * (1.0 / cos(phi_rad) - 1.0) - d_offset = r_back - d_sector - d_base_corner - - k = r_corner / cos(phi_rad) + d_offset - qa = 1.0 + tan(phi_rad)^2 - qb = 2.0 * k * tan(phi_rad) - qc = k^2 - (r_back - r_corner)^2 - - discriminant = qb^2 - 4.0 * qa * qc - if discriminant < 0 - throw(ArgumentError("[SectorParams] Geometric constraints violated (negative discriminant). The combination of radii, depth and angle does not form a closed sector.")) - end -end - -""" -$(TYPEDEF) - -Rule that enforces specific Sector geometry constraints (discriminant, angle limits, no overlap). -""" -struct SectorGeometryValid <: Validation.Rule - name::Symbol -end - -function Validation._apply(r::SectorGeometryValid, nt, ::Type{SectorParams}) - # Delegate to the shared validation function - # Note: Non-neg rules from Validation framework handle the basic checks, - # but _validate_sector_params repeats them safely. - _validate_sector_params(nt.n_sectors, nt.r_back, nt.d_sector, nt.r_corner, nt.theta_cond_deg, nt.d_insulation) -end - -Validation.extra_rules(::Type{SectorParams}) = ( - Validation.IntegerField(:n_sectors), - Validation.Positive(:n_sectors), - Validation.Nonneg(:r_back), - Validation.Nonneg(:d_sector), - Validation.Nonneg(:r_corner), - Validation.Nonneg(:theta_cond_deg), - Validation.Nonneg(:d_insulation), - Validation.Less(:d_sector, :r_back), - SectorGeometryValid(:sector_geometry), -) - -@construct SectorParams _REQ_SECTOR_PARAMS - -""" -$(TYPEDEF) - -Represents a single sector-shaped conductor with defined geometric and material properties. - -$(TYPEDFIELDS) -""" -struct Sector{T<:REALSCALAR} <: AbstractConductorPart{T} - "Inner radius (not applicable, typically 0 for the central point) \\[m\\]." - r_in::T - "Outer radius (equivalent back radius) \\[m\\]." - r_ex::T - "Geometric parameters defining the sector's shape." - params::SectorParams{T} - "Rotation angle of this specific sector around the cable's center [degrees]." - rotation_angle_deg::T - "Material properties of the conductor." - material_props::Material{T} - "Operating temperature of the conductor \\[°C\\]." - temperature::T - "Cross-sectional area of the sector \\[m²\\]." - cross_section::T - "Electrical resistance of the sector \\[Ω/m\\]." - resistance::T - "Geometric mean radius (GMR) of the sector (approximated) \\[m\\]." - gmr::T - "Calculated vertices defining the polygon shape." - vertices::Vector{Point{2,T}} - "Geometric centroid of the sector shape." - centroid::Point{2,T} -end - - -function Sector( - params::SectorParams{T}, - rotation_angle_deg::T, - material_props::Material{T}; - temperature::T=T₀, -) where {T<:REALSCALAR} - - # 1. Calculate the geometry and vertices for a base (unrotated) sector - base_vertices = _calculate_sector_polygon_points(params) - - # 2. Rotate the vertices to the specified angle - rotation_angle_rad = deg2rad(rotation_angle_deg) - rotated_vertices = [_rotate_point(p, rotation_angle_rad) for p in base_vertices] - - # Ensure the polygon is closed: first point == last point (within tolerance) - # tol = 1e-9 - # if !isempty(rotated_vertices) - # firstp = rotated_vertices[1] - # lastp = rotated_vertices[end] - # if !(isapprox(firstp[1], lastp[1]; atol=tol, rtol=0.0) && - # isapprox(firstp[2], lastp[2]; atol=tol, rtol=0.0)) - # push!(rotated_vertices, firstp) - # @debug "(Sector) rotated_vertices not closed — appended first point to close polygon." - # end - # end - # 3. Calculate cross-sectional area using the Shoelace formula - #cross_section = PolygonOps.area(rotated_vertices) - cross_section = _shoelace_area(rotated_vertices) - #@debug "Sector cross-sectional area: $(cross_section*1e6) mm²" - @debug "Sector cross-sectional area (Shoelace): $(cross_section*1e6) mm²" - - # Calculate centroid - centroid = _calculate_polygon_centroid(rotated_vertices) - @debug "Sector numerically calculated centroid point is: $(centroid)" - # 4. Calculate DC resistance - rho_eff = calc_temperature_correction(material_props.alpha, temperature, material_props.T0) * material_props.rho - resistance = rho_eff / cross_section - - # 5. Approximate GMR based on a circle of equivalent area - r_equiv = sqrt(cross_section / π) - gmr = r_equiv * exp(-0.25 * material_props.mu_r) # GMR of an equivalent solid round conductor - - return Sector{T}( - zero(T), - params.r_back, - params, - rotation_angle_deg, - material_props, - temperature, - cross_section, - resistance, - gmr, - rotated_vertices, - centroid, - ) -end - - - -# --- Geometric Helper Functions (internal) --- - -function _calculate_sector_geometry(p::SectorParams) - phi_deg = 90.0 - p.theta_cond_deg / 2.0 - phi_rad = deg2rad(phi_deg) # ϕ - @debug "(Sector) phi_deg: $phi_deg" - @debug "(Sector) phi_rad: $phi_rad rad" - - if abs(cos(phi_rad)) < 1e-9 - error("theta_cond_deg is too close to 180, leading to division by zero. Check parameters.") - end - - d_base_corner = p.r_corner * (1.0 / cos(phi_rad) - 1.0) # D_B - @debug "(Sector) d_base_corner (D_B) : $d_base_corner m" - d_offset = p.r_back - p.d_sector - d_base_corner # D_O - @debug "(Sector) d_offset (D_O) : $d_offset m" - d_insulation_offset = (p.d_insulation / (cos((pi - ((2 * pi) / p.n_sectors)) / 2.0))) - d_offset # D_I - @debug "(Sector) d_insulation_offset (D_I) : $d_insulation_offset m" - - # trick test (works though different that Urquhart's) - #d_offset = (p.d_insulation / (cos((pi - ((2 * pi) / p.n_sectors)) / 2.0))) # D_I - #d_insulation_offset = 0.0 - - x_base_corner = p.r_corner * sin(phi_rad) # X_A = -X_F - y_base_corner = x_base_corner * tan(phi_rad) + d_offset # Y_A = Y_F - node_A = Point2f(x_base_corner, y_base_corner + d_insulation_offset) - - k = p.r_corner / cos(phi_rad) + d_offset - qa = 1.0 + tan(phi_rad)^2 - qb = 2.0 * k * tan(phi_rad) - qc = k^2 - (p.r_back - p.r_corner)^2 - - discriminant = qb^2 - 4.0 * qa * qc - if discriminant < 0 - error("Cannot calculate side corner center: negative discriminant ($discriminant). Check parameters.") - end - - x_side_center = (-qb + sqrt(discriminant)) / (2.0 * qa) # X_N - @debug "(Sector) x_side_center (X_N): $(x_side_center*1e3) mm" - @debug "(Sector) r_corner: $(p.r_corner*1e3) mm" - y_side_center = x_side_center * tan(phi_rad) + k # Y_N - - # Sector width (for checks): - # w = 2 * X_N + 2 * r_corner - w_sector = 2.0 * x_side_center + 2.0 * p.r_corner - @debug "(Sector) sector width (computed): $(w_sector) m ($(w_sector*1e3) mm)" - - x_side_lower = x_side_center + p.r_corner * sin(phi_rad) # X_B - y_side_lower = y_side_center - p.r_corner * cos(phi_rad) # Y_B - node_B = Point2f(x_side_lower, y_side_lower + d_insulation_offset) - - dist_origin_side_center = sqrt(x_side_center^2 + y_side_center^2) - if dist_origin_side_center < 1e-9 - error("Side corner center is at the origin, cannot determine upper point direction.") - end - x_side_upper = x_side_center * p.r_back / dist_origin_side_center # X_C - y_side_upper = y_side_center * p.r_back / dist_origin_side_center # Y_C - node_C = Point2f(x_side_upper, y_side_upper + d_insulation_offset) - - nodes = ( - A=node_A, - B=node_B, - C=node_C, - D=Point2f(-node_C[1], node_C[2]), - E=Point2f(-node_B[1], node_B[2]), - F=Point2f(-node_A[1], node_A[2]) - ) - @debug "(Sector) Nodes: $nodes" - centers = ( - Back=Point2f(0, 0+ d_insulation_offset), - Base=Point2f(0, d_offset + p.r_corner / cos(phi_rad) + d_insulation_offset), - RightSide=Point2f(x_side_center, y_side_center+ d_insulation_offset), - LeftSide=Point2f(-x_side_center, y_side_center+ d_insulation_offset) - ) - @debug "(Sector) Centers: $centers" - return (Nodes=nodes, Centers=centers, Params=p) -end - -function _generate_arc_points(center, radius, start_angle, end_angle, num_points) - while end_angle < start_angle - @debug "(Sector) end_angle < start_angle: $(rad2deg(end_angle)) < $(rad2deg(start_angle))" - end_angle += 2pi - @debug "(Sector) Adjusted end_angle to be greater than start_angle: $(rad2deg(end_angle)) > $(rad2deg(start_angle))" - end - while end_angle - start_angle > pi - @debug "(Sector) overshoot! end_angle - start_angle > π: $(rad2deg(end_angle - start_angle)) > 180" - end_angle -= 2pi - @debug "(Sector) Adjusted end_angle to be less than start_angle: $(rad2deg(end_angle)) < $(rad2deg(start_angle))" - end - angle_range = range(start_angle, stop=end_angle, length=num_points) - @debug "(Sector) ______________________________ ∠ $(rad2deg(start_angle-end_angle))." - return [Point2f(center[1] + radius * cos(a), center[2] + radius * sin(a)) for a in angle_range] -end - -function _calculate_sector_polygon_points(params; num_arc_points=30) # increase `num_arc_points` for higher accuracy - geom = _calculate_sector_geometry(params) - nodes, centers = geom.Nodes, geom.Centers - - poly_points = Point2f[] - get_angle(p1, p2) = atan((p1[2] - p2[2]), (p1[1] - p2[1])) - # Start at F, go to A (Base) - push!(poly_points, nodes.F) - if params.r_corner > 1e-9 - start_angle = get_angle(nodes.F, centers.Base) - @debug "Arc from F to A: start_angle=$(rad2deg(start_angle))" - end_angle = get_angle(nodes.A, centers.Base) - @debug "Arc from F to A: end_angle=$(rad2deg(end_angle))" - append!(poly_points, - _generate_arc_points(centers.Base, params.r_corner, start_angle, end_angle, num_arc_points)[2:end]) - else - push!(poly_points, Point2f(0, params.r_back - params.d_sector), nodes.A) - end - - # Line A to B - push!(poly_points, nodes.B) - - # Arc B to C (Right Side) - if params.r_corner > 1e-9 - start_angle = get_angle(nodes.B, centers.RightSide) - @debug "Arc from B to C: start_angle=$(rad2deg(start_angle))" - end_angle = get_angle(nodes.C, centers.RightSide) - @debug "Arc from B to C: end_angle=$(rad2deg(end_angle))" - append!(poly_points, _generate_arc_points(centers.RightSide, params.r_corner, start_angle, end_angle, num_arc_points)[2:end]) - else - push!(poly_points, nodes.C) - end - - # Arc C to D (Back) - start_angle = get_angle(nodes.C, centers.Back) - @debug "Arc from C to D: start_angle=$(rad2deg(start_angle))" - end_angle = get_angle(nodes.D, centers.Back) - @debug "Arc from C to D: end_angle=$(rad2deg(end_angle))" - append!(poly_points, _generate_arc_points(centers.Back, params.r_back, start_angle, end_angle, num_arc_points)[2:end]) - - # Arc D to E (Left Side) - if params.r_corner > 1e-9 - start_angle = get_angle(nodes.D, centers.LeftSide) - @debug "Arc from D to E: start_angle=$(rad2deg(start_angle))" - end_angle = get_angle(nodes.E, centers.LeftSide) - @debug "Arc from D to E: end_angle=$(rad2deg(end_angle))" - append!(poly_points, _generate_arc_points(centers.LeftSide, params.r_corner, start_angle, end_angle, num_arc_points)[2:end]) - else - push!(poly_points, nodes.E) - end - - return poly_points -end - -function _rotate_point(p::Point2f, angle_rad::Real) - cos_a, sin_a = cos(angle_rad), sin(angle_rad) - return Point2f(p[1] * cos_a - p[2] * sin_a, p[1] * sin_a + p[2] * cos_a) -end - - -function _shoelace_area(vertices::AbstractVector{Point{2,T}}) where {T<:Real} - n::Int = length(vertices) - if n < 3 - @warn "Polygon must have at least 3 vertices to compute area. Returning 0 area" - return zero(T) - end - area::T = zero(T) - for i in 1:n - p1 = vertices[i] - p2 = vertices[mod1(i + 1, n)] - area += (p1[1] * p2[2] - p2[1] * p1[2]) - end - return abs(area) / T(2) -end - -function _calculate_polygon_centroid(vertices::AbstractVector{Point{2,T}}) where {T<:Real} - n = length(vertices) - if n < 3 - return Point2f(0, 0) - end - - - Cx = zero(T) - Cy = zero(T) - signed_area = zero(T) - - for i in 1:n - p1 = vertices[i] - p2 = vertices[mod1(i + 1, n)] - cross_prod = (p1[1] * p2[2] - p2[1] * p1[2]) - signed_area += (p1[1] * p2[2] - p2[1] * p1[2]) - Cx += (p1[1] + p2[1]) * cross_prod - Cy += (p1[2] + p2[2]) * cross_prod - end - signed_area /= 2 - - if abs(signed_area) < 1e-12 - return Point2f(0, 0) - end - - Cx /= (6 * signed_area) - Cy /= (6 * signed_area) - - return Point2f(Cx, Cy) -end \ No newline at end of file diff --git a/src/datamodel/sectorinsulator.jl b/src/datamodel/sectorinsulator.jl deleted file mode 100644 index 8f13ea6be..000000000 --- a/src/datamodel/sectorinsulator.jl +++ /dev/null @@ -1,131 +0,0 @@ -""" -$(TYPEDEF) - -Represents an insulating layer surrounding a sector-shaped conductor. - -$(TYPEDFIELDS) -""" -struct SectorInsulator{T<:REALSCALAR} <: AbstractInsulatorPart{T} - "Inner radius (not applicable, defined by inner sector) \\[m\\]." - r_in::T - "Outer radius (equivalent back radius of outer boundary) \\[m\\]." - r_ex::T - "The inner sector conductor that this insulator surrounds." - inner_sector::Sector{T} - "The thickness of the insulating layer \\[m\\]." - thickness::T - "Material properties of the insulator." - material_props::Material{T} - "Operating temperature of the insulator \\[°C\\]." - temperature::T - "Cross-sectional area of the insulating layer \\[m²\\]." - cross_section::T - "Shunt capacitance (approximated) \\[F/m\\]." - shunt_capacitance::T - "Shunt conductance (approximated) \\[S·m\\]." - shunt_conductance::T - "Calculated vertices of the outer boundary polygon." - outer_vertices::Vector{Point{2, T}} -end - - -function SectorInsulator( - inner_sector::Sector{T}, - thickness::T, - material_props::Material{T}; - temperature::T=T₀, -) where {T<:REALSCALAR} - - # 1. Calculate the outer vertices by offsetting the inner sector's geometry - outer_vertices = _calculate_offset_polygon(inner_sector.vertices, thickness) - - # 2. Calculate areas - inner_area = inner_sector.cross_section - - # tol = 1e-9 - # if !isempty(outer_vertices) - # firstp = outer_vertices[1] - # lastp = outer_vertices[end] - # if !(isapprox(firstp[1], lastp[1]; atol=tol, rtol=0.0) && - # isapprox(firstp[2], lastp[2]; atol=tol, rtol=0.0)) - # push!(outer_vertices, firstp) - # @debug "(Sector) outer_vertices not closed — appended first point to close polygon." - # end - # end - - - - #outer_area = PolygonOps.area(outer_vertices) - outer_area = _shoelace_area(outer_vertices) - @debug "SectorInsulator inner area: $(inner_area*1e6) mm²" - @debug "SectorInsulator outer area: $(outer_area*1e6) mm²" - cross_section = outer_area - inner_area - - # 3. Approximate capacitance and conductance using equivalent coaxial circles - # This is more consistent with the package's approach than a parallel plate model. - r_eq_in = sqrt(inner_area / π) - r_eq_ext = sqrt(outer_area / π) - shunt_capacitance = calc_shunt_capacitance(r_eq_in, r_eq_ext, material_props.eps_r) - shunt_conductance = calc_shunt_conductance(r_eq_in, r_eq_ext, material_props.rho) - - # 4. Determine outer radius from the new params - outer_r_back = inner_sector.params.r_back + thickness - - return SectorInsulator{T}( - inner_sector.r_ex, - outer_r_back, - inner_sector, - thickness, - material_props, - temperature, - cross_section, - shunt_capacitance, - shunt_conductance, - outer_vertices - ) -end - -# --- Geometric Helper Functions (internal) --- -# REVISED: This function now takes vertices and thickness to compute a geometric offset. -function _calculate_offset_polygon(vertices::Vector{Point{2, T}}, thickness::T) where {T<:REALSCALAR} - num_vertices = length(vertices) - if num_vertices < 3 - error("Polygon must have at least 3 vertices.") - end - - new_vertices = similar(vertices) - - for i in 1:num_vertices - p_prev = vertices[i == 1 ? num_vertices : i - 1] - p_curr = vertices[i] - p_next = vertices[i == num_vertices ? 1 : i + 1] - - v1 = p_curr - p_prev - v2 = p_next - p_curr - - # Normalize the vectors - v1_norm = v1 / norm(v1) - v2_norm = v2 / norm(v2) - - # Normal vectors (rotated 90 degrees clockwise for outward direction) - n1 = Point(v1_norm[2], -v1_norm[1]) - n2 = Point(v2_norm[2], -v2_norm[1]) - - # Bisector of the normals - bisector = (n1 + n2) / norm(n1 + n2) - - # Angle between the two vectors to calculate the correct offset distance - angle = acos(clamp(v1_norm ⋅ v2_norm, -1.0, 1.0)) - - # Miter length - offset_distance = thickness / sin((π - angle) / 2) - - if isinf(offset_distance) # The vectors are parallel - new_vertices[i] = p_curr + n1 * thickness - else - new_vertices[i] = p_curr + bisector * offset_distance - end - end - - return new_vertices -end \ No newline at end of file diff --git a/src/datamodel/semicon.jl b/src/datamodel/semicon.jl deleted file mode 100644 index bc1380f86..000000000 --- a/src/datamodel/semicon.jl +++ /dev/null @@ -1,116 +0,0 @@ -""" -$(TYPEDEF) - -Represents a semiconducting layer with defined geometric, material, and electrical properties given by the attributes: - -$(TYPEDFIELDS) -""" -struct Semicon{T <: REALSCALAR} <: AbstractInsulatorPart{T} - "Internal radius of the semiconducting layer \\[m\\]." - r_in::T - "External radius of the semiconducting layer \\[m\\]." - r_ex::T - "Material properties of the semiconductor." - material_props::Material{T} - "Operating temperature of the semiconductor \\[°C\\]." - temperature::T - "Cross-sectional area of the semiconducting layer \\[m²\\]." - cross_section::T - "Electrical resistance of the semiconducting layer \\[Ω/m\\]." - resistance::T - "Geometric mean radius of the semiconducting layer \\[m\\]." - gmr::T - "Shunt capacitance per unit length of the semiconducting layer \\[F/m\\]." - shunt_capacitance::T - "Shunt conductance per unit length of the semiconducting layer \\[S·m\\]." - shunt_conductance::T -end - -""" -$(TYPEDSIGNATURES) - -Constructs a [`Semicon`](@ref) instance with calculated electrical and geometric properties. - -# Arguments - -- `r_in`: Internal radius of the semiconducting layer \\[m\\]. -- `r_ex`: External radius or thickness of the layer \\[m\\]. -- `material_props`: Material properties of the semiconducting material. -- `temperature`: Operating temperature of the layer \\[°C\\] (default: T₀). - -# Returns - -- A [`Semicon`](@ref) object with initialized properties. - -# Examples - -```julia -material_props = Material(1e6, 2.3, 1.0, 20.0, 0.00393) -semicon_layer = $(FUNCTIONNAME)(0.01, Thickness(0.002), material_props, temperature=25) -println(semicon_layer.cross_section) # Expected output: ~6.28e-5 [m²] -println(semicon_layer.resistance) # Expected output: Resistance in [Ω/m] -println(semicon_layer.gmr) # Expected output: GMR in [m] -println(semicon_layer.shunt_capacitance) # Expected output: Capacitance in [F/m] -println(semicon_layer.shunt_conductance) # Expected output: Conductance in [S·m] -``` -""" -function Semicon( - r_in::T, - r_ex::T, - material_props::Material{T}, - temperature::T, -) where {T <: REALSCALAR} - - rho = material_props.rho - T0 = material_props.T0 - alpha = material_props.alpha - epsr_r = material_props.eps_r - - cross_section = π * (r_ex^2 - r_in^2) - - resistance = - calc_tubular_resistance(r_in, r_ex, rho, alpha, T0, temperature) - gmr = calc_tubular_gmr(r_ex, r_in, material_props.mu_r) - shunt_capacitance = calc_shunt_capacitance(r_in, r_ex, epsr_r) - shunt_conductance = calc_shunt_conductance(r_in, r_ex, rho) - - # Initialize object - return Semicon( - r_in, - r_ex, - material_props, - temperature, - cross_section, - resistance, - gmr, - shunt_capacitance, - shunt_conductance, - ) -end - -const _REQ_SEMICON = (:r_in, :r_ex, :material_props) -const _OPT_SEMICON = (:temperature,) -const _DEFS_SEMICON = (T₀,) - -Validation.has_radii(::Type{Semicon}) = true -Validation.has_temperature(::Type{Semicon}) = true -Validation.required_fields(::Type{Semicon}) = _REQ_SEMICON -Validation.keyword_fields(::Type{Semicon}) = _OPT_SEMICON -Validation.keyword_defaults(::Type{Semicon}) = _DEFS_SEMICON - -# accept proxies for radii -Validation.is_radius_input(::Type{Semicon}, ::Val{:r_in}, x::AbstractCablePart) = true -Validation.is_radius_input(::Type{Semicon}, ::Val{:r_in}, x::Thickness) = true -Validation.is_radius_input(::Type{Semicon}, ::Val{:r_ex}, x::Thickness) = true -Validation.is_radius_input(::Type{Semicon}, ::Val{:r_ex}, x::Diameter) = true - -Validation.extra_rules(::Type{Semicon}) = (IsA{Material}(:material_props),) - -# normalize proxies -> numbers -Validation.parse(::Type{Semicon}, nt) = begin - rin, rex = _normalize_radii(Semicon, nt.r_in, nt.r_ex) - (; nt..., r_in = rin, r_ex = rex) -end - -# This macro expands to a weakly-typed constructor for Semicon -@construct Semicon _REQ_SEMICON _OPT_SEMICON _DEFS_SEMICON diff --git a/src/datamodel/strands_handler.jl b/src/datamodel/strands_handler.jl deleted file mode 100644 index e9be77de7..000000000 --- a/src/datamodel/strands_handler.jl +++ /dev/null @@ -1,30 +0,0 @@ -struct MaxFill end # The promise proxy - -import ..Validation: maxfill - -""" -$(TYPEDSIGNATURES) - -Fallback method for the [`maxfill`](@ref) interface. Throws an explicit error indicating that the component type `T` has not implemented its physical capacity limit. - -# Arguments - -- `::Type{T}`: Component type \\[dimensionless\\]. -- `args...`: Geometric parameters required for the calculation \\[dimensionless\\]. - -# Returns - -- Nothing. Always throws an `ArgumentError`. -""" -@noinline function maxfill(::Type{T}, args...) where {T} - throw( - ArgumentError( - "[$(_typename(T))] `maxfill` is not implemented. Any component using `MaxFill()` " * - "or the `PhysicalFillLimit` rule must overload `maxfill(::Type{$(_typename(T))}, args...)`.", - ), - ) -end - -# The plumbing (Never needs to be touched again) -@inline _resolve_strands(x::Int, ::Type{T}, args...) where {T} = x -@inline _resolve_strands(::MaxFill, ::Type{T}, args...) where {T} = maxfill(T, args...) diff --git a/src/datamodel/strip.jl b/src/datamodel/strip.jl deleted file mode 100644 index 70e475b7b..000000000 --- a/src/datamodel/strip.jl +++ /dev/null @@ -1,155 +0,0 @@ -""" -$(TYPEDEF) - -Represents a flat conductive strip with defined geometric and material properties given by the attributes: - -$(TYPEDFIELDS) -""" -struct Strip{T <: REALSCALAR} <: AbstractConductorPart{T} - "Internal radius of the strip \\[m\\]." - r_in::T - "External radius of the strip \\[m\\]." - r_ex::T - "Thickness of the strip \\[m\\]." - thickness::T - "Width of the strip \\[m\\]." - width::T - "Ratio defining the lay length of the strip (twisting factor) \\[dimensionless\\]." - lay_ratio::T - "Mean diameter of the strip's helical path \\[m\\]." - mean_diameter::T - "Pitch length of the strip's helical path \\[m\\]." - pitch_length::T - "Twisting direction of the strip (1 = unilay, -1 = contralay) \\[dimensionless\\]." - lay_direction::Int - "Material properties of the strip." - material_props::Material{T} - "Temperature at which the properties are evaluated \\[°C\\]." - temperature::T - "Cross-sectional area of the strip \\[m²\\]." - cross_section::T - "Electrical resistance of the strip \\[Ω/m\\]." - resistance::T - "Geometric mean radius of the strip \\[m\\]." - gmr::T -end - -""" -$(TYPEDSIGNATURES) - -Constructs a [`Strip`](@ref) object with specified geometric and material parameters. - -# Arguments - -- `r_in`: Internal radius of the strip \\[m\\]. -- `r_ex`: External radius or thickness of the strip \\[m\\]. -- `width`: Width of the strip \\[m\\]. -- `lay_ratio`: Ratio defining the lay length of the strip \\[dimensionless\\]. -- `material_props`: Material properties of the strip. -- `temperature`: Temperature at which the properties are evaluated \\[°C\\]. Defaults to [`T₀`](@ref). -- `lay_direction`: Twisting direction of the strip (1 = unilay, -1 = contralay) \\[dimensionless\\]. Defaults to 1. - -# Returns - -- A [`Strip`](@ref) object with calculated geometric and electrical properties. - -# Examples - -```julia -material_props = Material(1.7241e-8, 1.0, 0.999994, 20.0, 0.00393) -strip = $(FUNCTIONNAME)(0.01, Thickness(0.002), 0.05, 10, material_props, temperature=25) -println(strip.cross_section) # Output: 0.0001 [m²] -println(strip.resistance) # Output: Resistance value [Ω/m] -``` - -# See also - -- [`Material`](@ref) -- [`ConductorGroup`](@ref) -- [`calc_strip_resistance`](@ref) -- [`calc_tubular_gmr`](@ref) -- [`calc_helical_params`](@ref) -""" -function Strip( - r_in::T, - r_ex::T, - width::T, - lay_ratio::T, - material_props::Material{T}, - temperature::T, - lay_direction::Int, -) where {T <: REALSCALAR} - - thickness = r_ex - r_in - rho = material_props.rho - T0 = material_props.T0 - alpha = material_props.alpha - - mean_diameter, pitch_length, overlength = calc_helical_params( - r_in, - r_ex, - lay_ratio, - ) - - cross_section = thickness * width - - R_strip = - calc_strip_resistance(thickness, width, rho, alpha, T0, temperature) * - overlength - - gmr = calc_tubular_gmr(r_ex, r_in, material_props.mu_r) - - # Initialize object - return Strip( - r_in, - r_ex, - thickness, - width, - lay_ratio, - mean_diameter, - pitch_length, - lay_direction, - material_props, - temperature, - cross_section, - R_strip, - gmr, - ) -end - -const _REQ_STRIP = (:r_in, :r_ex, :width, :lay_ratio, :material_props) -const _OPT_STRIP = (:temperature, :lay_direction) -const _DEFS_STRIP = (T₀, 1) - -Validation.has_radii(::Type{Strip}) = true -Validation.has_temperature(::Type{Strip}) = true -Validation.required_fields(::Type{Strip}) = _REQ_STRIP -Validation.keyword_fields(::Type{Strip}) = _OPT_STRIP -Validation.keyword_defaults(::Type{Strip}) = _DEFS_STRIP - -Validation.coercive_fields(::Type{Strip}) = - (:r_in, :r_ex, :width, :lay_ratio, :material_props, :temperature) # not :lay_direction -# accept proxies for radii - -Validation.is_radius_input(::Type{Strip}, ::Val{:r_in}, x::AbstractCablePart) = true -Validation.is_radius_input(::Type{Strip}, ::Val{:r_in}, x::Thickness) = true -Validation.is_radius_input(::Type{Strip}, ::Val{:r_ex}, x::Thickness) = true -Validation.is_radius_input(::Type{Strip}, ::Val{:r_ex}, x::Diameter) = true - -Validation.extra_rules(::Type{Strip}) = ( - IsA{Material}(:material_props), - OneOf(:lay_direction, (-1, 1)), - Finite(:lay_ratio), - Nonneg(:lay_ratio), - Finite(:width), - Positive(:width), -) - -# normalize proxies -> numbers -Validation.parse(::Type{Strip}, nt) = begin - rin, rex = _normalize_radii(Strip, nt.r_in, nt.r_ex) - (; nt..., r_in = rin, r_ex = rex) -end - -# This macro expands to a weakly-typed constructor for Strip -@construct Strip _REQ_STRIP _OPT_STRIP _DEFS_STRIP diff --git a/src/datamodel/textdisplay.jl b/src/datamodel/textdisplay.jl new file mode 100644 index 000000000..5b2a865c0 --- /dev/null +++ b/src/datamodel/textdisplay.jl @@ -0,0 +1,575 @@ +function _identity_pose(pose::Pose2) + return iszero(pose.x) && iszero(pose.y) && iszero(pose.φ) +end + +function _display_pose(pose::Pose2) + _identity_pose(pose) && return nothing + return sprint(show, pose; context = :compact => true) +end + +function _bounded_collection(value) + value isa AbstractArray && return "$(length(value)) values" + value isa Tuple && return "$(length(value)) values" + return sprint(show, value; context = :compact => true) +end + +TextDisplay.@showfields Pose2 "Pose" pose -> ( + x = TextDisplay.engineering(pose.x, :meter), + y = TextDisplay.engineering(pose.y, :meter), + φ = TextDisplay.angle(pose.φ) +) + +TextDisplay.@showfields Disk "Disk" primitive -> ( + r = TextDisplay.engineering(primitive.r, :meter), + at = _display_pose(primitive.at) +) + +TextDisplay.@showfields Rectangle "Rectangle" primitive -> ( + w = TextDisplay.engineering(primitive.w, :meter), + h = TextDisplay.engineering(primitive.h, :meter), + at = _display_pose(primitive.at) +) + +TextDisplay.@showfields Ellipse "Ellipse" primitive -> ( + a = TextDisplay.engineering(primitive.a, :meter), + b = TextDisplay.engineering(primitive.b, :meter), + at = _display_pose(primitive.at) +) + +TextDisplay.@showfields EllipseOffset "EllipseOffset" shape -> ( + a = TextDisplay.engineering(shape.a, :meter), + b = TextDisplay.engineering(shape.b, :meter), + t = TextDisplay.engineering(shape.t, :meter), + at = _display_pose(shape.at) +) + +TextDisplay.@showfields Sector "Sector" primitive -> ( + Δφ = TextDisplay.angle(primitive.span), + rᵢ = TextDisplay.engineering(primitive.r_base, :meter), + rₒ = TextDisplay.engineering(primitive.r_back, :meter), + fillet = iszero(primitive.fillet) ? nothing : + TextDisplay.engineering(primitive.fillet, :meter) +) + +TextDisplay.@showfields Annulus "Annulus" primitive -> ( + rᵢ = TextDisplay.engineering(primitive.ri, :meter), + rₒ = TextDisplay.engineering(primitive.ro, :meter), + at = _display_pose(primitive.at) +) + +TextDisplay.@showfields Polygon "Polygon" primitive -> ( + vertices = length(primitive.points), + at = _display_pose(primitive.at) +) + +TextDisplay.@showfields BentStrip "BentStrip" shape -> ( + rᵢ = TextDisplay.engineering(shape.ri, :meter), + rₒ = TextDisplay.engineering(shape.ro, :meter), + Δφ = TextDisplay.angle(shape.span), + at = _display_pose(shape.at) +) + +TextDisplay.@showfields BoundedPlacement "BoundedPlacement" placement -> ( + boundary = string(nameof(typeof(placement.boundary))), + course = placement.course, +) + +TextDisplay.@showfields Shell "Shell" layer -> ( + t = TextDisplay.engineering(layer.t, :meter), +) + +TextDisplay.@showfields DifferenceShape "DifferenceShape" shape -> ( + outer = string(nameof(typeof(shape.outer))), + holes = length(shape.holes) +) + +TextDisplay.@showfields AssemblyShape "AssemblyShape" shape -> ( + members = length(shape.members), +) + +TextDisplay.@showfields AssemblyMember "AssemblyMember" member -> ( + item = string(nameof(typeof(member.item))), + at = _display_pose(member.at) +) + +TextDisplay.name(::Type{<:EmptyBoundary}) = "Empty boundary" +Base.summary(io::IO, ::EmptyBoundary) = print(io, "Empty boundary") +Base.show(io::IO, ::EmptyBoundary) = print(io, "EmptyBoundary()") +Base.show(io::IO, ::MIME"text/plain", value::EmptyBoundary) = show(io, value) + +TextDisplay.name(::Type{<:EnclosureBoundary}) = "Enclosure boundary" +Base.summary(io::IO, ::EnclosureBoundary) = print(io, "Enclosure boundary") +Base.show(io::IO, ::EnclosureBoundary) = print(io, "EnclosureBoundary()") +Base.show(io::IO, ::MIME"text/plain", value::EnclosureBoundary) = show(io, value) + +function _ring_count(pattern::Ring) + pattern.n isa _DeferredCardinality && return "capacity()" + return pattern.n +end + +TextDisplay.@showfields Ring "Ring" pattern -> ( + n = _ring_count(pattern), + r = pattern.r === nothing ? nothing : TextDisplay.engineering(pattern.r, :meter), + φ₀ = iszero(pattern.φ0) ? nothing : TextDisplay.angle(pattern.φ0), + Δφ = isapprox(pattern.span, oftype(pattern.span, 2π)) ? nothing : + TextDisplay.angle(pattern.span), + gap = iszero(pattern.gap_frac) ? nothing : TextDisplay.value(pattern.gap_frac) +) + +TextDisplay.@showfields Polar "Polar" pattern -> ( + nᵣ = pattern.nr, + nφ = pattern.nφ, + r₀ = TextDisplay.engineering(pattern.r0, :meter), + Δr = TextDisplay.engineering(pattern.dr, :meter), + φ₀ = iszero(pattern.φ0) ? nothing : TextDisplay.angle(pattern.φ0), + Δφ = isapprox(pattern.span, oftype(pattern.span, 2π)) ? nothing : + TextDisplay.angle(pattern.span) +) + +TextDisplay.@showfields Fill "Fill" pattern -> ( + r = TextDisplay.engineering(pattern.r, :meter), + φ = iszero(pattern.φ) ? nothing : TextDisplay.angle(pattern.φ), + φ₀ = iszero(pattern.φ0) ? nothing : TextDisplay.angle(pattern.φ0), + Δφ = isapprox(pattern.span, oftype(pattern.span, 2π)) ? nothing : + TextDisplay.angle(pattern.span) +) + +TextDisplay.@showfields Lattice "Lattice" pattern -> ( + nₓ = pattern.nx, + nᵧ = pattern.ny, + Δx = TextDisplay.engineering(pattern.dx, :meter), + Δy = TextDisplay.engineering(pattern.dy, :meter) +) + +TextDisplay.@showfields FillFactor "FillFactor" factor -> ( + η = TextDisplay.value(factor.η), +) + +TextDisplay.@showfields LayRatio "LayRatio" lay -> ( + q = TextDisplay.value(lay.q), +) + +TextDisplay.@showfields Pitch "Pitch" lay -> ( + p = TextDisplay.engineering(lay.p, :meter), +) + +TextDisplay.@showfields LayAngle "LayAngle" lay -> ( + α = TextDisplay.angle(lay.α), +) + +TextDisplay.@showfields Helix{<:LayRatio} "Helix" path -> ( + q = TextDisplay.value(path.lay.q), + dir = path.dir > 0 ? "+1" : "−1", + φ₀ = iszero(path.φ0) ? nothing : TextDisplay.angle(path.φ0) +) + +TextDisplay.@showfields Helix{<:Pitch} "Helix" path -> ( + p = TextDisplay.engineering(path.lay.p, :meter), + dir = path.dir > 0 ? "+1" : "−1", + φ₀ = iszero(path.φ0) ? nothing : TextDisplay.angle(path.φ0) +) + +TextDisplay.@showfields Helix{<:LayAngle} "Helix" path -> ( + α = TextDisplay.angle(path.lay.α), + dir = path.dir > 0 ? "+1" : "−1", + φ₀ = iszero(path.φ0) ? nothing : TextDisplay.angle(path.φ0) +) + +TextDisplay.name(::Type{<:Helix}) = "Helix" +Base.summary(io::IO, ::Helix) = print(io, "Helix") +function Base.show(io::IO, path::Helix) + return TextDisplay.fields(io, + "Helix", + ( + lay = sprint(show, path.lay; context = :compact => true), + dir = path.dir > 0 ? "+1" : "−1", + φ₀ = iszero(path.φ0) ? nothing : TextDisplay.angle(path.φ0) + )) +end +function Base.show(io::IO, ::MIME"text/plain", path::Helix) + get(io, :compact, false) && return show(io, path) + return TextDisplay.fields(io, + "Helix", + ( + lay = sprint(show, path.lay; context = :compact => true), + dir = path.dir > 0 ? "+1" : "−1", + φ₀ = iszero(path.φ0) ? nothing : TextDisplay.angle(path.φ0) + ); + multiline = true) +end + +_datasheet_unit(::Val{:U0}) = "kV" +_datasheet_unit(::Val{:U}) = "kV" +_datasheet_unit(::Val{:conductor_cross_section}) = "mm²" +_datasheet_unit(::Val{:screen_cross_section}) = "mm²" +_datasheet_unit(::Val{:armor_cross_section}) = "mm²" +_datasheet_unit(::Val{:resistance}) = "Ω/km" +_datasheet_unit(::Val{:capacitance}) = "μF/km" +_datasheet_unit(::Val{:inductance}) = "mH/km" +_datasheet_unit(::Val) = nothing + +function _datasheet_value(name::Symbol, item) + item === nothing && return nothing + text = item isa Real ? TextDisplay.value(item) : + sprint(show, item; context = :compact => true) + unit = _datasheet_unit(Val(name)) + return unit === nothing ? text : "$text $unit" +end + +function _datasheet_fields(info::DatasheetInfo) + names = keys(info) + values = Tuple(_datasheet_value(name, info[name]) for name in names) + return NamedTuple{names}(values) +end + +TextDisplay.@showfields DatasheetInfo "DatasheetInfo" info -> _datasheet_fields(info) + +_compact_text(value) = sprint(show, value; context = :compact => true) + +function _part_label(region::Region) + primitive = _compact_text(region.primitive) + return "Region :$(region.tag) · $primitive · $(region.material.kind)" +end + +function _part_label(stack::Stack) + count = length(stack.items) + return "Stack · $count $(count == 1 ? "part" : "parts")" +end + +function _part_label(group::Group) + attributes = String[] + !_identity_pose(group.at) && push!(attributes, _compact_text(group.at)) + one_member = group.pattern === nothing || + group.pattern isa Ring && group.pattern.n == 1 && + (group.pattern.r === nothing || iszero(group.pattern.r)) + push!(attributes, one_member ? "one member" : _compact_text(group.pattern)) + group.path === nothing || push!(attributes, _compact_text(group.path)) + group.compact === nothing || push!(attributes, _compact_text(group.compact)) + return "Group :$(group.name) · $(join(attributes, " · "))" +end + +function _part_label(assembly::Assembly) + attributes = String[] + !_identity_pose(assembly.at) && push!(attributes, _compact_text(assembly.at)) + if assembly.item isa Tuple + count = length(assembly.item) + push!(attributes, "$count explicit $(count == 1 ? "member" : "members")") + else + push!(attributes, _compact_text(assembly.pattern)) + assembly.path === nothing || push!(attributes, _compact_text(assembly.path)) + assembly.compact === nothing || push!(attributes, _compact_text(assembly.compact)) + end + return "Assembly · $(join(attributes, " · "))" +end + +function _part_label(enclosure::Enclosure) + attributes = String[_compact_text(enclosure.primitive)] + !_identity_pose(enclosure.at) && push!(attributes, _compact_text(enclosure.at)) + return "Enclosure :$(enclosure.tag) · $(join(attributes, " · "))" +end + +_part_children(::Region) = () +_part_children(stack::Stack) = Tuple(_part_node(item) for item in stack.items) +_part_children(group::Group) = (_part_node(group.item),) +function _part_children(assembly::Assembly) + if assembly.item isa Tuple + return Tuple(( + label = _identity_pose(member.at) ? _part_label(member.item) : + string(_part_label(member.item), " · ", _compact_text(member.at)), + children = _part_children(member.item), + noun = "parts" + ) for member in assembly.item) + end + return (_part_node(assembly.item),) +end +function _part_children(enclosure::Enclosure) + fill = enclosure.fill isa Material ? + "fill · Material · $(enclosure.fill.kind)" : + string("fill · ", _part_label(enclosure.fill)) + children = Any[_part_node(enclosure.item), ( + label = fill, noun = "parts")] + enclosure.wall === nothing || push!(children, + ( + label = string("wall · ", _part_label(enclosure.wall)), + children = _part_children(enclosure.wall), + noun = "parts" + )) + return Tuple(children) +end + +function _part_node(part::AbstractCablePart) + ( + label = _part_label(part), + children = _part_children(part), + noun = "parts" + ) +end + +TextDisplay.name(::Type{<:Region}) = "Region" +Base.summary(io::IO, region::Region) = print(io, "Region :", region.tag) +Base.show(io::IO, region::Region) = print(io, _part_label(region)) +Base.show(io::IO, ::MIME"text/plain", region::Region) = show(io, region) + +TextDisplay.name(::Type{<:Stack}) = "Stack" +function Base.summary(io::IO, stack::Stack) + count = length(stack.items) + print(io, "Stack with $count $(count == 1 ? "part" : "parts")") +end +function Base.show(io::IO, stack::Stack) + count = length(stack.items) + print(io, "Stack($count $(count == 1 ? "part" : "parts"))") +end +function Base.show(io::IO, ::MIME"text/plain", stack::Stack) + get(io, :compact, false) && return show(io, stack) + return TextDisplay.tree(io, _part_label(stack), _part_children(stack); noun = "parts") +end + +TextDisplay.name(::Type{<:Group}) = "Group" +Base.summary(io::IO, group::Group) = print(io, "Group :", group.name) +function Base.show(io::IO, group::Group) + print(io, "Group(:", group.name, "; ") + if group.pattern === nothing + print(io, "one member") + else + show(IOContext(io, :compact => true), group.pattern) + end + group.path === nothing || + (print(io, ", path="); show(IOContext(io, :compact => true), group.path)) + group.compact === nothing || + (print(io, ", compact="); show(IOContext(io, :compact => true), group.compact)) + group.boundary === nothing || + (print(io, ", boundary="); show(IOContext(io, :compact => true), group.boundary)) + print(io, ")") +end +function Base.show(io::IO, ::MIME"text/plain", group::Group) + get(io, :compact, false) && return show(io, group) + return TextDisplay.tree(io, _part_label(group), _part_children(group); noun = "parts") +end + +TextDisplay.name(::Type{<:Assembly}) = "Assembly" +function Base.summary(io::IO, assembly::Assembly) + assembly.item isa Tuple ? + print(io, "Assembly with $(length(assembly.item)) members") : + print(io, "Assembly") +end +function Base.show(io::IO, assembly::Assembly) + if assembly.item isa Tuple + print(io, "Assembly($(length(assembly.item)) members)") + else + print(io, "Assembly(") + show(IOContext(io, :compact => true), assembly.pattern) + print(io, ")") + end +end +function Base.show(io::IO, ::MIME"text/plain", assembly::Assembly) + get(io, :compact, false) && return show(io, assembly) + return TextDisplay.tree(io, _part_label(assembly), _part_children(assembly); noun = "members") +end + +TextDisplay.name(::Type{<:Enclosure}) = "Enclosure" +Base.summary(io::IO, enclosure::Enclosure) = print(io, "Enclosure :", enclosure.tag) +function Base.show(io::IO, enclosure::Enclosure) + print(io, "Enclosure(:", enclosure.tag, "; ") + show(IOContext(io, :compact => true), enclosure.primitive) + print(io, ")") +end +function Base.show(io::IO, ::MIME"text/plain", enclosure::Enclosure) + get(io, :compact, false) && return show(io, enclosure) + return TextDisplay.tree( + io, _part_label(enclosure), _part_children(enclosure); noun = "parts" + ) +end + +TextDisplay.name(::Type{<:PlacedRegion}) = "PlacedRegion" +Base.summary(io::IO, region::PlacedRegion) = print(io, "Placed region :", region.source.tag) +function Base.show(io::IO, region::PlacedRegion) + terminal = region.terminal === nothing ? "" : "; terminal=:$(region.terminal)" + print(io, "PlacedRegion(:", region.source.tag, terminal, "; ") + show(IOContext(io, :compact => true), region.primitive) + print(io, ")") +end +function Base.show(io::IO, ::MIME"text/plain", region::PlacedRegion) + get(io, :compact, false) && return show(io, region) + attributes = String[ + "source $(_part_label(region.source))", + "primitive $(_compact_text(region.primitive))" +] + region.terminal === nothing || push!(attributes, "terminal :$(region.terminal)") + isempty(region.placement.patterns) || push!(attributes, + "patterns $(length(region.placement.patterns))") + isempty(region.paths) || push!(attributes, "paths $(length(region.paths))") + return TextDisplay.tree(io, "PlacedRegion :$(region.source.tag)", attributes) +end + +TextDisplay.name(::Type{<:CableGeometry}) = "CableGeometry" +function Base.summary(io::IO, geometry::CableGeometry) + print(io, "Cable geometry with $(length(geometry.regions)) regions") +end +function Base.show(io::IO, geometry::CableGeometry) + print(io, "CableGeometry(regions=$(length(geometry.regions)); outer=") + show(IOContext(io, :compact => true), geometry.outer) + print(io, ")") +end +function Base.show(io::IO, ::MIME"text/plain", geometry::CableGeometry) + get(io, :compact, false) && return show(io, geometry) + children = Any[( + label = "outer $(_compact_text(geometry.outer))", noun = "regions")] + append!(children, + ( + label = string( + "region :", region.source.tag, + region.terminal === nothing ? "" : " · terminal :$(region.terminal)", + " · ", _compact_text(region.primitive) + ), + noun = "regions" + ) for region in geometry.regions) + count = length(geometry.regions) + return TextDisplay.tree( + io, + "CableGeometry · $count $(count == 1 ? "region" : "regions")", + Tuple(children); + noun = "regions" + ) +end + +TextDisplay.name(::Type{<:CableDesign}) = "CableDesign" +function Base.summary(io::IO, design::CableDesign) + print(io, "CableDesign \"", design.cable_id, "\"") +end +function Base.show(io::IO, design::CableDesign) + print( + io, + "CableDesign(\"", design.cable_id, "\"; terminals=", + length(design.terminal_order), ", regions=", length(design.geometry.regions), ")" + ) +end +function Base.show(io::IO, ::MIME"text/plain", design::CableDesign) + get(io, :compact, false) && return show(io, design) + terminals = isempty(design.terminal_order) ? "none" : join(design.terminal_order, ", ") + origin = _part_node(design.origin) + children = ( + (label = "terminals $terminals", noun = "parts"), + (label = "regions $(length(design.geometry.regions))", noun = "parts"), + (label = "diameter $(TextDisplay.engineering(2outer_radius(design), :meter))", + noun = "parts"), + (label = "origin $(origin.label)", children = origin.children, noun = "parts") + ) + return TextDisplay.tree(io, "CableDesign \"$(design.cable_id)\"", children) +end + +TextDisplay.name(::Type{<:LineCableSystem}) = "LineCableSystem" +function Base.summary(io::IO, system::LineCableSystem) + print(io, "LineCableSystem \"", system.system_id, "\"") +end +function Base.show(io::IO, system::LineCableSystem) + print( + io, + "LineCableSystem(\"", system.system_id, "\"; cables=", ncables(system), + ", terminals=", length(system.terminal_order), ")" + ) +end +function Base.show(io::IO, ::MIME"text/plain", system::LineCableSystem) + get(io, :compact, false) && return show(io, system) + cables = Tuple(( + label = string( + design.cable_id, + " · ", _compact_text(position), + " · ", join( + ("$terminal→$phase" + for (terminal, phase) in + zip(design.terminal_order, connections)), + ", " + ) + ), + noun = "cables" + ) + for (design, position, connections) in zip( + system.designs, system.positions, system.connections + )) + children = ( + (label = "length $(TextDisplay.engineering(system.line_length, :meter))", + noun = "cables"), + (label = "terminals $(length(system.terminal_order))", noun = "cables"), + (label = "cables", children = cables, noun = "cables") + ) + return TextDisplay.tree(io, "LineCableSystem \"$(system.system_id)\"", children) +end + +TextDisplay.name(::Type{<:CablesLibrary}) = "CablesLibrary" +function Base.summary(io::IO, library::CablesLibrary) + count = length(library) + print(io, "CablesLibrary with $count $(count == 1 ? "design" : "designs")") +end +function Base.show(io::IO, library::CablesLibrary) + count = length(library) + print(io, "CablesLibrary($count $(count == 1 ? "design" : "designs"))") +end +function Base.show(io::IO, ::MIME"text/plain", library::CablesLibrary) + get(io, :compact, false) && return show(io, library) + ids = sort!(collect(keys(library))) + children = Tuple(begin + design = library[cable_id] + ( + label = string( + cable_id, " · ", length(design.terminal_order), " terminals · ", + length(design.geometry.regions), " regions · D=", + TextDisplay.engineering(2outer_radius(design), :meter) + ), + noun = "designs" + ) + end + for cable_id in ids) + count = length(library) + return TextDisplay.tree( + io, + "CablesLibrary · $count $(count == 1 ? "design" : "designs")", + children; + noun = "designs" + ) +end + +TextDisplay.name(::Type{<:SectorShape}) = "Sector shape" +Base.summary(io::IO, ::SectorShape) = print(io, "Resolved sector") +function Base.show(io::IO, shape::SectorShape) + print(io, "ResolvedSector(") + show(IOContext(io, :compact => true), shape.primitive) + _identity_pose(shape.at) || + (print(io, "; at="); show(IOContext(io, :compact => true), shape.at)) + print(io, ")") +end +Base.show(io::IO, ::MIME"text/plain", shape::SectorShape) = show(io, shape) + +TextDisplay.name(::Type{<:ShellShape}) = "Shell shape" +Base.summary(io::IO, ::ShellShape) = print(io, "Resolved shell") +function Base.show(io::IO, shape::ShellShape) + print(io, "ResolvedShell(inner=") + show(IOContext(io, :compact => true), shape.inner) + print(io, ", outer=") + show(IOContext(io, :compact => true), shape.outer) + print(io, ")") +end +function Base.show(io::IO, ::MIME"text/plain", shape::ShellShape) + get(io, :compact, false) && return show(io, shape) + return TextDisplay.tree(io, + "Resolved shell", + ( + (label = "inner $(_compact_text(shape.inner))", noun = "boundaries"), + (label = "outer $(_compact_text(shape.outer))", noun = "boundaries") + )) +end + +function _preview_geometry_count(geometry) + points = geometry isa GeometryBasics.Polygon ? geometry.exterior : geometry + applicable(length, points) && return length(points) + return count(_ -> true, points) +end + +TextDisplay.name(::Type{<:PreviewShape}) = "Preview shape" +function Base.summary(io::IO, shape::PreviewShape) + print(io, "Preview shape :", shape.tag) +end +function Base.show(io::IO, shape::PreviewShape) + print(io, "PreviewShape(:", shape.tag, "; vertices=", + _preview_geometry_count(shape.geometry), ")") +end +Base.show(io::IO, ::MIME"text/plain", shape::PreviewShape) = show(io, shape) diff --git a/src/datamodel/tubular.jl b/src/datamodel/tubular.jl deleted file mode 100644 index ee7b29fdd..000000000 --- a/src/datamodel/tubular.jl +++ /dev/null @@ -1,112 +0,0 @@ -""" -$(TYPEDEF) - -Represents a tubular or solid (`r_in=0`) conductor with geometric and material properties defined as: - -$(TYPEDFIELDS) -""" -struct Tubular{T <: REALSCALAR} <: AbstractConductorPart{T} - "Internal radius of the tubular conductor \\[m\\]." - r_in::T - "External radius of the tubular conductor \\[m\\]." - r_ex::T - "A [`Material`](@ref) object representing the physical properties of the conductor material." - material_props::Material{T} - "Temperature at which the properties are evaluated \\[°C\\]." - temperature::T - "Cross-sectional area of the tubular conductor \\[m²\\]." - cross_section::T - "Electrical resistance (DC) of the tubular conductor \\[Ω/m\\]." - resistance::T - "Geometric mean radius of the tubular conductor \\[m\\]." - gmr::T -end - -""" -$(TYPEDSIGNATURES) - -Initializes a [`Tubular`](@ref) object with specified geometric and material parameters. - -# Arguments - -- `r_in`: Internal radius of the tubular conductor \\[m\\]. -- `r_ex`: External radius of the tubular conductor \\[m\\]. -- `material_props`: A [`Material`](@ref) object representing the physical properties of the conductor material. -- `temperature`: Temperature at which the properties are evaluated \\[°C\\]. Defaults to [`T₀`](@ref). - -# Returns - -- An instance of [`Tubular`](@ref) initialized with calculated geometric and electrical properties. - -# Examples - -```julia -material_props = Material(1.7241e-8, 1.0, 0.999994, 20.0, 0.00393) -tubular = $(FUNCTIONNAME)(0.01, 0.02, material_props, temperature=25) -println(tubular.cross_section) # Output: 0.000942 [m²] -println(tubular.resistance) # Output: Resistance value [Ω/m] -``` - -# See also - -- [`Material`](@ref) -- [`calc_tubular_resistance`](@ref) -- [`calc_tubular_gmr`](@ref) -""" -function Tubular( - r_in::T, - r_ex::T, - material_props::Material{T}, - temperature::T, -) where {T <: REALSCALAR} - - rho = material_props.rho - T0 = material_props.T0 - alpha = material_props.alpha - - cross_section = π * (r_ex^2 - r_in^2) - - R0 = calc_tubular_resistance(r_in, r_ex, rho, alpha, T0, temperature) - - gmr = calc_tubular_gmr(r_ex, r_in, material_props.mu_r) - - # Initialize object - return Tubular( - r_in, - r_ex, - material_props, - temperature, - cross_section, - R0, - gmr, - ) -end - -const _REQ_TUBULAR = (:r_in, :r_ex, :material_props) -const _OPT_TUBULAR = (:temperature,) -const _DEFS_TUBULAR = (T₀,) - -Validation.has_radii(::Type{Tubular}) = true -Validation.has_temperature(::Type{Tubular}) = true -Validation.required_fields(::Type{Tubular}) = _REQ_TUBULAR -Validation.keyword_fields(::Type{Tubular}) = _OPT_TUBULAR -Validation.keyword_defaults(::Type{Tubular}) = _DEFS_TUBULAR - -# accept proxies for radii -Validation.is_radius_input(::Type{Tubular}, ::Val{:r_in}, x::AbstractCablePart) = true -Validation.is_radius_input(::Type{Tubular}, ::Val{:r_in}, x::Thickness) = true -Validation.is_radius_input(::Type{Tubular}, ::Val{:r_ex}, x::Thickness) = true -Validation.is_radius_input(::Type{Tubular}, ::Val{:r_ex}, x::Diameter) = true - -Validation.extra_rules(::Type{Tubular}) = (IsA{Material}(:material_props),) - -# normalize proxies -> numbers -Validation.parse(::Type{Tubular}, nt) = begin - rin, rex = _normalize_radii(Tubular, nt.r_in, nt.r_ex) - (; nt..., r_in = rin, r_ex = rex) -end - -# This macro expands to a weakly-typed constructor for Tubular -@construct Tubular _REQ_TUBULAR _OPT_TUBULAR _DEFS_TUBULAR - - diff --git a/src/datamodel/typecoercion.jl b/src/datamodel/typecoercion.jl deleted file mode 100644 index 144a57d3a..000000000 --- a/src/datamodel/typecoercion.jl +++ /dev/null @@ -1,111 +0,0 @@ -@inline function _rebuild_part_typed_core(p, ::Type{T}) where {T} - C0 = typeof(p).name.wrapper # concrete parametric type (e.g., CircStrands) - order = (required_fields(C0)..., keyword_fields(C0)...) # positional order for tight kernel - coer = coercive_fields(C0) # only these get coerced to T - - argsT = ntuple(i -> begin - s = order[i] - v = getfield(p, s) - (s in coer) ? coerce_to_T(v, T) : v # preserve Int/categorical fields - end, length(order)) - - return C0(argsT...) # call the tight numeric constructor -end - -# Identity when already at T (no rebuild, preserves ===) -coerce_to_T(p::AbstractConductorPart{T}, ::Type{T}) where {T} = p -# Cross-T rebuild via your existing tight numeric-core helper -coerce_to_T(p::AbstractConductorPart{S}, ::Type{T}) where {S, T} = - _rebuild_part_typed_core(p, T) -coerce_to_T(g::ConductorGroup{T}, ::Type{T}) where {T} = g -# Cross-T: fieldwise coerce + layer coercion (no recompute) -@inline function coerce_to_T(g::ConductorGroup{S}, ::Type{T}) where {S, T} - n = length(g.layers) - layersT = Vector{AbstractConductorPart{T}}(undef, n) - @inbounds for i in 1:n - layersT[i] = coerce_to_T(g.layers[i], T) # uses your part-level coercers - end - return ConductorGroup{T}( - coerce_to_T(g.r_in, T), - coerce_to_T(g.r_ex, T), - coerce_to_T(g.cross_section, T), - g.num_wires, # keep Int as-is - coerce_to_T(g.num_turns, T), - coerce_to_T(g.resistance, T), - coerce_to_T(g.alpha, T), - coerce_to_T(g.gmr, T), - layersT, - ) -end - -@inline coerce_to_T(p::AbstractInsulatorPart{T}, ::Type{T}) where {T} = p -@inline coerce_to_T(p::AbstractInsulatorPart{S}, ::Type{T}) where {S, T} = - _rebuild_part_typed_core(p, T) -@inline coerce_to_T(g::InsulatorGroup{T}, ::Type{T}) where {T} = g -@inline function coerce_to_T(g::InsulatorGroup{S}, ::Type{T}) where {S, T} - n = length(g.layers) - layersT = Vector{AbstractInsulatorPart{T}}(undef, n) - @inbounds for i in 1:n - layersT[i] = coerce_to_T(g.layers[i], T) # uses the part-level coercers above - end - return InsulatorGroup{T}( - coerce_to_T(g.r_in, T), - coerce_to_T(g.r_ex, T), - coerce_to_T(g.cross_section, T), - coerce_to_T(g.shunt_capacitance, T), - coerce_to_T(g.shunt_conductance, T), - layersT, - ) -end - -@inline coerce_to_T(c::CableComponent{T}, ::Type{T}) where {T} = c -@inline function coerce_to_T(c::CableComponent{S}, ::Type{T}) where {S, T} - CableComponent{T}( - c.id, - coerce_to_T(c.conductor_group, T), - coerce_to_T(c.insulator_group, T), - ) -end - -"Identity: no allocation when already at `T`." -@inline coerce_to_T(n::NominalData{T}, ::Type{T}) where {T} = n -# Cross-T rebuild: fieldwise coercion, preserving `nothing` -@inline function coerce_to_T(n::NominalData{S}, ::Type{T}) where {S, T} - names = fieldnames(typeof(n)) # e.g. (:designation_code, :U0, :U, ...) - vals = map(names) do k # map over tuple of names → returns a tuple - v = getfield(n, k) - v === nothing ? nothing : coerce_to_T(v, T) - end - NT = NamedTuple{names}(vals) # correct: pass a SINGLE tuple, not varargs - return NominalData{T}(; NT...) # call typed kernel via keyword splat -end - -@inline coerce_to_T(d::CableDesign{T}, ::Type{T}) where {T} = d -@inline function coerce_to_T(d::CableDesign{S}, ::Type{T}) where {S, T} - compsT = Vector{CableComponent{T}}(undef, length(d.components)) - @inbounds for i in eachindex(d.components) - compsT[i] = coerce_to_T(d.components[i], T) - end - ndT = isnothing(d.nominal_data) ? nothing : coerce_to_T(d.nominal_data, T) - CableDesign{T}(d.cable_id, compsT; nominal_data = ndT) -end - -@inline coerce_to_T(p::CablePosition{T}, ::Type{T}) where {T} = p -@inline function coerce_to_T(p::CablePosition{S}, ::Type{T}) where {S, T} - CablePosition{T}( - coerce_to_T(p.design_data, T), - coerce_to_T(p.horz, T), - coerce_to_T(p.vert, T), - p.conn, # keep Int mapping as-is - ) -end - -@inline coerce_to_T(sys::LineCableSystem{T}, ::Type{T}) where {T} = sys -@inline function coerce_to_T(sys::LineCableSystem{S}, ::Type{T}) where {S, T} - cablesT = Vector{CablePosition{T}}(undef, length(sys.cables)) - @inbounds for i in eachindex(sys.cables) - cablesT[i] = coerce_to_T(sys.cables[i], T) - end - # counts will be recomputed once positions are populated; preserve them now - LineCableSystem{T}(sys.system_id, coerce_to_T(sys.line_length, T), cablesT) -end diff --git a/src/datamodel/types.jl b/src/datamodel/types.jl index a3de41e38..192ace250 100644 --- a/src/datamodel/types.jl +++ b/src/datamodel/types.jl @@ -1,89 +1,6 @@ -# To handle radius-related operations -abstract type AbstractRadius <: Number end - -""" -$(TYPEDEF) - -Represents the thickness of a cable component. - -$(TYPEDFIELDS) -""" -struct Thickness{T <: Real} <: AbstractRadius - "Numerical value of the thickness \\[m\\]." - value::T - function Thickness(value::T) where {T <: Real} - value >= 0 || throw(ArgumentError("Thickness must be a non-negative number.")) - new{T}(value) - end -end - -""" -$(TYPEDEF) - -Represents the diameter of a cable component. - -$(TYPEDFIELDS) -""" -struct Diameter{T <: Real} <: AbstractRadius - "Numerical value of the diameter \\[m\\]." - value::T - function Diameter(value::T) where {T <: Real} - value > 0 || throw(ArgumentError("Diameter must be a positive number.")) - new{T}(value) - end -end - -""" -$(TYPEDEF) - -Abstract type representing a generic cable part. -""" -abstract type AbstractCablePart{T} end - """ $(TYPEDEF) -Abstract type representing a conductive part of a cable. - -Subtypes implement specific configurations: -- [`Tubular`](@ref) -- [`Strip`](@ref) -""" -abstract type AbstractConductorPart{T} <: AbstractCablePart{T} end - -""" -$(TYPEDEF) - -Abstract type representing all stranded configurations composed of grouped discrete geometric shapes. - -Subtypes implement specific configurations: -- [`CircStrands`](@ref) -- [`RectStrands`](@ref) -""" -abstract type AbstractStrandsLayer{T} <: AbstractConductorPart{T} end - - -""" -$(TYPEDEF) - -Abstract type representing an insulating part of a cable. - -Subtypes implement specific configurations: -- [`Insulator`](@ref) -- [`Semicon`](@ref) +Supertype for materialized cable parts. """ -abstract type AbstractInsulatorPart{T} <: AbstractCablePart{T} end - - -# If a correct ctor exists, Julia will pick it; this runs only when arity is wrong. -function (::Type{T})(args::Vararg{Any, N}; kwargs...) where {T <: AbstractCablePart, N} - throw( - ArgumentError( - "[$(nameof(T))] constructor: invalid number of positional args ($N).", - ), - ) -end - - -### Provisions for the new types currently under development: RectStrandsShape and SectorShape -abstract type AbstractShapeGeometry end \ No newline at end of file +abstract type AbstractCablePart end diff --git a/src/datamodel/validation.jl b/src/datamodel/validation.jl deleted file mode 100644 index 0e6304236..000000000 --- a/src/datamodel/validation.jl +++ /dev/null @@ -1,86 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Default policy for **inner** radius raw inputs: accept proxies that expose an outer radius. This permits stacking by hijacking `p.r_ex` during parsing. - -# Arguments - -- `::Type{T}`: Component type \\[dimensionless\\]. -- `::Val{:r_in}`: Field tag for the inner radius \\[dimensionless\\]. -- `p::AbstractCablePart`: Proxy object \\[dimensionless\\]. - -# Returns - -- `Bool` indicating acceptance (`true` if `hasproperty(p, :r_ex)`). - -# Examples - -```julia -Validation.is_radius_input(Tubular, Val(:r_in), prev_layer) # true if prev_layer has :r_ex -``` -""" -is_radius_input(::Type{T}, ::Val{:r_in}, p::AbstractCablePart) where {T} = - hasproperty(p, :r_ex) - -""" -$(TYPEDSIGNATURES) - -Default policy for **outer** radius raw inputs (annular shells): reject `AbstractCablePart` proxies. Outer radius must be numeric or a `Thickness` wrapper to avoid creating zero‑thickness layers. - -# Arguments - -- `::Type{T}`: Component type \\[dimensionless\\]. -- `::Val{:r_ex}`: Field tag for the outer radius \\[dimensionless\\]. -- `::AbstractCablePart`: Proxy object \\[dimensionless\\]. - -# Returns - -- `false` always. - -# Examples - -```julia -Validation.is_radius_input(Tubular, Val(:r_ex), prev_layer) # false -``` -""" -is_radius_input(::Type{T}, ::Val{:r_ex}, ::AbstractCablePart) where {T} = false - -""" -$(TYPEDSIGNATURES) - -Default policy for **outer** radius raw inputs (annular shells): accept `Thickness` as a convenience wrapper. The thickness is expanded to an outer radius during parsing. - -# Arguments - -- `::Type{T}`: Component type \\[dimensionless\\]. -- `::Val{:r_ex}`: Field tag for the outer radius \\[dimensionless\\]. -- `::Thickness`: Thickness wrapper \\[dimensionless\\]. - -# Returns - -- `Bool` indicating acceptance (`true`). - -# Examples - -```julia -Validation.is_radius_input(Tubular, Val(:r_ex), Thickness(1e-3)) # true -``` -""" -is_radius_input(::Type{T}, ::Val{:r_ex}, ::Thickness) where {T} = true - -""" -$(TYPEDSIGNATURES) - -Merge per-part keyword defaults declared via `Validation.keyword_defaults` with -user-provided kwargs and return a **NamedTuple** suitable for forwarding. - -Defaults may be a `NamedTuple` or a `Tuple` zipped against `Validation.keyword_fields(::Type{C})`. -User keys always win. -""" -@inline function _with_kwdefaults(::Type{C}, kwargs::NamedTuple) where {C} - defs = Validation.keyword_defaults(C) - defs === () && return kwargs - nt = defs isa NamedTuple ? defs : - NamedTuple{Validation.keyword_fields(C)}(defs) - return merge(nt, kwargs) -end diff --git a/src/docstrings.jl b/src/docstrings.jl new file mode 100644 index 000000000..797688d51 --- /dev/null +++ b/src/docstrings.jl @@ -0,0 +1,65 @@ +using DocStringExtensions: DocStringExtensions, SIGNATURES, TYPEDSIGNATURES, TYPEDEF, + TYPEDFIELDS, FIELDS, FUNCTIONNAME, IMPORTS, EXPORTS + +export SIGNATURES, TYPEDSIGNATURES, TYPEDEF, TYPEDFIELDS, FIELDS, FUNCTIONNAME, + METHODLIST, IMPORTS, EXPORTS + +#! explicit-imports: off +# METHODLIST adapts dependency-internal DocStringExtensions machinery. Keep this +# exception local to the adapter so the rest of the package remains strict. +struct _CleanMethodList <: DocStringExtensions.Abbreviation end + +"`METHODLIST` abbreviation with package-local, CI-safe source paths." +const METHODLIST = _CleanMethodList() +const _PACKAGE_ROOT = normpath(dirname(@__DIR__)) + +function _method_path(method) + file = string(method.file) + isempty(file) && return "unknown" + normalized = normpath(file) + return startswith(normalized, _PACKAGE_ROOT) ? + relpath(normalized, _PACKAGE_ROOT) : basename(normalized) +end + +function DocStringExtensions.format(::_CleanMethodList, buffer, doc) + binding = doc.data[:binding] + typesig = doc.data[:typesig] + module_name = doc.data[:module] + function_value = Docs.resolve(binding) + groups = DocStringExtensions.methodgroups( + function_value, + typesig, + module_name; + exact = false + ) + isempty(groups) && return nothing + + println(buffer) + for group in groups + println(buffer, "```julia") + for method in group + DocStringExtensions.printmethod(buffer, binding, function_value, method) + println(buffer) + end + println(buffer, "```\n") + if !isempty(group) + method = first(group) + url = DocStringExtensions.url(method) + if isempty(url) || startswith(url, "file:") + println( + buffer, + "defined at `$(_method_path(method)):$(method.line)`." + ) + else + println( + buffer, + "defined at [`$(_method_path(method)):$(method.line)`]($url)." + ) + end + end + println(buffer) + end + println(buffer) + return nothing +end +#! explicit-imports: on diff --git a/src/earth/Earth.jl b/src/earth/Earth.jl new file mode 100644 index 000000000..e7463aaf3 --- /dev/null +++ b/src/earth/Earth.jl @@ -0,0 +1,62 @@ +""" + LineCableModels.Earth + +Define static homogeneous and layered-earth descriptions, measured +frequency-dependent material relations, and equivalent homogeneous-earth +reductions required by line-parameter formulations. + +# Public actions + +- Declare earth descriptions with [`layer`](@ref) and [`homogeneous`](@ref), + represented by [`EarthLayer`](@ref) and [`EarthModel`](@ref). +- Construct the ephemeral [`EarthMaterial`](@ref) used by the engine. +- Select measured frequency dependence through [`FrequencyDependent`](@ref). +- Select equivalent homogeneous-earth reductions through [`EquivalentHomogeneous`](@ref). +- Build immutable earth models from complete ordered layer declarations. +- Present earth data through the Base display protocol. +""" +module Earth + +export AbstractEarthModel, AbstractEarthLayer, AbstractEarthMaterial, EarthMaterial, + EarthLayer, EarthModel +export layer, homogeneous +export build +export FrequencyDependent, EquivalentHomogeneous + +using DocStringExtensions: TYPEDEF, TYPEDFIELDS, TYPEDSIGNATURES +using RequiredInterfaces: @required +import ..LineCableModels: build, validate +import ..LineCableModels: parameterize +using ..Materials: AbstractMaterial +import ..Commons +import ..TextDisplay + +""" +Supertype for materialized static earth-layer and earth-model descriptions. +""" +abstract type AbstractEarthModel end + +""" +Supertype for a static earth layer. +""" +abstract type AbstractEarthLayer <: AbstractEarthModel end + +""" +Supertype for frequency-evaluated earth constitutive properties. +""" +abstract type AbstractEarthMaterial <: AbstractMaterial end + +@required AbstractEarthModel begin + validate(::AbstractEarthModel) +end + +include("earthmaterial.jl") +include("earthlayer.jl") +include("earthmodel.jl") + +include("frequencydependent/FrequencyDependent.jl") +include("equivalenthomogeneous/EquivalentHomogeneous.jl") + +include("base.jl") + +end # module Earth diff --git a/src/earth/base.jl b/src/earth/base.jl new file mode 100644 index 000000000..02f849830 --- /dev/null +++ b/src/earth/base.jl @@ -0,0 +1,81 @@ +TextDisplay.@showfields EarthMaterial "EarthMaterial" material -> ( + ρ = TextDisplay.engineering(material.rho, :ohm_meter), + εᵣ = TextDisplay.value(material.eps_r), + μᵣ = TextDisplay.value(material.mu_r) +) + +TextDisplay.@showfields EarthLayer "EarthLayer" layer -> ( + ρ = TextDisplay.engineering(layer.rho, :ohm_meter), + εᵣ = TextDisplay.value(layer.eps_r), + μᵣ = TextDisplay.value(layer.mu_r), + h = isinf(layer.thickness) ? "half-space" : + TextDisplay.engineering(layer.thickness, :meter) +) + +TextDisplay.name(::Type{<:EarthModel}) = "EarthModel" + +function _earth_description(model::EarthModel) + earth_count = length(model.layers) - 1 + earth_count == 1 && return "homogeneous" + orientation = model.vertical_layers ? "vertical " : "" + return "$earth_count $(orientation)earth layers" +end + +function Base.summary(io::IO, model::EarthModel) + print(io, "EarthModel · ", _earth_description(model)) +end + +function Base.show(io::IO, model::EarthModel) + print(io, "EarthModel(", _earth_description(model), ")") +end + +function _earth_layer_name(index::Int, count::Int) + index == 1 && return "air" + count == 2 && return "earth" + index == count && return "basement" + return "layer $(index - 1)" +end + +function _earth_resistivity(layer::EarthLayer) + return isinf(layer.rho) ? "∞" : TextDisplay.engineering(layer.rho, :ohm_meter) +end + +function _earth_layer_text( + name::AbstractString, + layer::EarthLayer; + show_thickness::Bool +) + thickness = if !show_thickness + "" + elseif isinf(layer.thickness) + "half-space" + else + "h=$(TextDisplay.engineering(layer.thickness, :meter))" + end + separator = show_thickness ? string(" ", rpad(thickness, 12)) : "" + return string( + rpad(name, 9), separator, + " ρ=", _earth_resistivity(layer), + " εᵣ=", TextDisplay.value(layer.eps_r), + " μᵣ=", TextDisplay.value(layer.mu_r) + ) +end + +function Base.show(io::IO, ::MIME"text/plain", model::EarthModel) + get(io, :compact, false) && return show(io, model) + show_thickness = length(model.layers) > 2 + children = [ + _earth_layer_text( + _earth_layer_name(index, length(model.layers)), + layer; + show_thickness + ) + for (index, layer) in enumerate(model.layers) + ] + return TextDisplay.tree( + io, + "EarthModel · $(_earth_description(model))", + children; + noun = "layers" + ) +end diff --git a/src/earth/earthlayer.jl b/src/earth/earthlayer.jl new file mode 100644 index 000000000..5b0db313f --- /dev/null +++ b/src/earth/earthlayer.jl @@ -0,0 +1,123 @@ +""" +$(TYPEDEF) + +Store the static physical properties of one earth or air layer. + +$(TYPEDFIELDS) +""" +struct EarthLayer{T <: Real} <: AbstractEarthLayer + "Electrical resistivity \\[Ω·m\\]." + rho::T + "Relative permittivity \\[dimensionless\\]." + eps_r::T + "Relative permeability \\[dimensionless\\]." + mu_r::T + "Layer thickness \\[m\\]." + thickness::T + + function EarthLayer{T}(rho::T, eps_r::T, mu_r::T, thickness::T) where {T <: Real} + return validate(new{T}(rho, eps_r, mu_r, thickness)) + end +end + +Base.eltype(::EarthLayer{T}) where {T} = T +Base.eltype(::Type{EarthLayer{T}}) where {T} = T + +function validate(layer::EarthLayer) + isnan(layer.rho) && throw(DomainError( + layer.rho, + "EarthLayer.rho must not be NaN" + )) + layer.rho > zero(layer.rho) || + throw(DomainError(layer.rho, "EarthLayer.rho must be positive")) + isfinite(layer.eps_r) && layer.eps_r > zero(layer.eps_r) || + throw(DomainError( + layer.eps_r, + "EarthLayer.eps_r must be positive and finite" + )) + isfinite(layer.mu_r) && layer.mu_r > zero(layer.mu_r) || + throw(DomainError( + layer.mu_r, + "EarthLayer.mu_r must be positive and finite" + )) + layer.thickness > zero(layer.thickness) || + throw(DomainError(layer.thickness, "EarthLayer.thickness must be positive")) + return layer +end + +""" +$(TYPEDSIGNATURES) + +Construct one static earth layer after floating and promoting its properties. +""" +function EarthLayer(rho::Real, eps_r::Real, mu_r::Real, thickness::Real) + values = promote(float(rho), float(eps_r), float(mu_r), float(thickness)) + T = typeof(first(values)) + return EarthLayer{T}(values...) +end + +function EarthLayer(rho::Real, eps_r::Real, mu_r::Real) + values = promote(float(rho), float(eps_r), float(mu_r)) + T = typeof(first(values)) + return EarthLayer{T}(values..., T(Inf)) +end + +function _earth_layer(rho, eps_r, mu_r, thickness) + permittivity = eps_r === nothing ? one(float(rho)) : eps_r + permeability = mu_r === nothing ? one(float(rho)) : mu_r + return thickness === nothing ? + EarthLayer(rho, permittivity, permeability) : + EarthLayer(rho, permittivity, permeability, thickness) +end + +""" +$(TYPEDSIGNATURES) + +Declare one static earth layer directly or as an explicit finite space. + +# Keywords + +- `rho`: electrical resistivity \\[Ω·m\\]. +- `eps_r=nothing`: relative permittivity \\[dimensionless\\]. `nothing` + selects unity in the resistivity scalar type. +- `mu_r=nothing`: relative permeability \\[dimensionless\\]. `nothing` + selects unity in the resistivity scalar type. +- `thickness=nothing`: layer thickness \\[m\\]. `nothing` selects a + semi-infinite layer. +- `combine=:product`: gridspace composition rule. + +# Returns + +- An `EarthLayer`, or a `Gridspace{EarthLayer}` when an explicit finite source + is supplied. +""" +function layer(; + rho, + eps_r = nothing, + mu_r = nothing, + thickness = nothing, + combine::Symbol = :product +) + values = (rho, eps_r, mu_r, thickness) + return parameterize(EarthLayer, _earth_layer, values; combine) +end + +function Base.convert(::Type{EarthLayer{T}}, layer::EarthLayer) where {T <: Real} + return EarthLayer{T}( + convert(T, layer.rho), convert(T, layer.eps_r), + convert(T, layer.mu_r), convert(T, layer.thickness) + ) +end + +Base.convert(::Type{EarthLayer{T}}, layer::EarthLayer{T}) where {T <: Real} = layer + +""" +Construct the ephemeral electromagnetic material represented by an earth layer. +""" +function EarthMaterial(layer::EarthLayer{T}) where {T <: Real} + EarthMaterial{T}(layer.rho, layer.eps_r, layer.mu_r) +end + +Commons.input_fields(::Type{<:EarthLayer}) = (rho=(name="electrical resistivity",unit="Ω·m"), + eps_r=(name="relative permittivity",unit=""),mu_r=(name="relative permeability",unit=""), + thickness=(name="layer thickness",unit="m")) diff --git a/src/earth/earthmaterial.jl b/src/earth/earthmaterial.jl new file mode 100644 index 000000000..c27113222 --- /dev/null +++ b/src/earth/earthmaterial.jl @@ -0,0 +1,83 @@ +""" +$(TYPEDEF) + +Store the electromagnetic properties of one earth material at one frequency. + +`EarthMaterial` is evaluated constitutive state produced from an +[`EarthLayer`](@ref) for analytical or finite-element computations. It is not stored in an +[`EarthModel`](@ref) or in a materials library. + +$(TYPEDFIELDS) +""" +struct EarthMaterial{T <: Real} <: AbstractEarthMaterial + "Electrical resistivity \\[Ω·m\\]." + rho::T + "Relative permittivity \\[dimensionless\\]. May be negative for an artificial EquivalentHomogeneous." + eps_r::T + "Relative permeability \\[dimensionless\\]." + mu_r::T + + @inline function EarthMaterial{T}(rho::T, eps_r::T, mu_r::T) where {T <: Real} + return validate(new{T}(rho, eps_r, mu_r)) + end +end + +Base.eltype(::EarthMaterial{T}) where {T} = T +Base.eltype(::Type{EarthMaterial{T}}) where {T} = T + +function validate(material::EarthMaterial) + isnan(material.rho) && throw(DomainError( + material.rho, + "EarthMaterial.rho must not be NaN" + )) + material.rho > zero(material.rho) || throw(DomainError( + material.rho, + "EarthMaterial.rho must be positive" + )) + isfinite(material.eps_r) && !iszero(material.eps_r) || throw(DomainError( + material.eps_r, + "EarthMaterial.eps_r must be nonzero and finite" + )) + isfinite(material.mu_r) && material.mu_r > zero(material.mu_r) || + throw(DomainError( + material.mu_r, + "EarthMaterial.mu_r must be positive and finite" + )) + return material +end + +""" +$(TYPEDSIGNATURES) + +Construct one ephemeral earth material after floating and promoting its +electromagnetic properties. + +# Arguments + +- `rho`: electrical resistivity \\[Ω·m\\]. +- `eps_r`: relative permittivity \\[dimensionless\\]. Artificial equivalent + earth models may produce a negative value. +- `mu_r`: relative permeability \\[dimensionless\\]. +""" +@inline function EarthMaterial(rho::Real, eps_r::Real, mu_r::Real) + values = promote(float(rho), float(eps_r), float(mu_r)) + T = typeof(first(values)) + return EarthMaterial{T}(values...) +end + +function Base.convert(::Type{EarthMaterial{T}}, material::EarthMaterial) where {T <: Real} + return EarthMaterial{T}( + convert(T, material.rho), + convert(T, material.eps_r), + convert(T, material.mu_r) + ) +end + +function Base.convert( + ::Type{EarthMaterial{T}}, material::EarthMaterial{T} +) where {T <: Real} + material +end + +Commons.input_fields(::Type{<:EarthMaterial}) = (rho=(name="electrical resistivity",unit="Ω·m"), + eps_r=(name="relative permittivity",unit=""),mu_r=(name="relative permeability",unit="")) diff --git a/src/earth/earthmodel.jl b/src/earth/earthmodel.jl new file mode 100644 index 000000000..910379514 --- /dev/null +++ b/src/earth/earthmodel.jl @@ -0,0 +1,210 @@ +""" +$(TYPEDEF) + +Store an immutable static layered-earth description. The first layer is always +air, and `layers` is a read-only ordered tuple. + +$(TYPEDFIELDS) +""" +struct EarthModel{T <: Real, N} <: AbstractEarthModel + "Whether earth interfaces are vertical rather than horizontal." + vertical_layers::Bool + "Read-only static layers beginning with semi-infinite air." + layers::NTuple{N, EarthLayer{T}} + + function EarthModel{T, N}( + vertical_layers::Bool, + layers::NTuple{N, EarthLayer{T}} + ) where {T <: Real, N} + return validate(new{T, N}(vertical_layers, layers)) + end +end + +function EarthModel{T}( + vertical_layers::Bool, + layers::NTuple{N, EarthLayer{T}} +) where {T <: Real, N} + return EarthModel{T, N}(vertical_layers, layers) +end + +Base.eltype(::EarthModel{T}) where {T} = T +Base.eltype(::Type{<:EarthModel{T}}) where {T} = T + +function validate(model::EarthModel) + for layer in model.layers + validate(layer) + end + length(model.layers) >= 2 || throw(ArgumentError( + "EarthModel.layers must contain air and at least one earth layer; " * + "received $(length(model.layers)) layers" + )) + air = first(model.layers) + isinf(air.rho) && isinf(air.thickness) || throw(ArgumentError( + "EarthModel.layers[1] must be semi-infinite air", + )) + model.vertical_layers && !isinf(model.layers[2].thickness) && + throw(ArgumentError( + "EarthModel.layers[2].thickness must be infinite for vertical layers", + )) + for index in 2:(length(model.layers) - 1) + if isinf(model.layers[index].thickness) + model.vertical_layers && index == 2 && continue + throw(ArgumentError( + "EarthModel.layers[$index].thickness must be finite because " * + "only the final horizontal layer may be semi-infinite" + )) + end + end + return model +end + +""" +$(TYPEDSIGNATURES) + +Construct a complete immutable static earth model. Frequencies are supplied +later by a `LineParametersProblem`. +""" +function EarthModel( + rho::Real, + eps_r::Real = one(float(rho)), + mu_r::Real = one(float(rho)); + thickness::Real = oftype(float(rho), Inf), + vertical_layers::Bool = false, + air_layer::Union{Nothing, EarthLayer} = nothing +) + T = promote_type( + typeof(float(rho)), typeof(float(eps_r)), typeof(float(mu_r)), + typeof(float(thickness)), + air_layer === nothing ? typeof(float(rho)) : eltype(air_layer) + ) + air = air_layer === nothing ? + EarthLayer{T}(T(Inf), convert(T, 1), convert(T, 1), T(Inf)) : + convert(EarthLayer{T}, air_layer) + earth = EarthLayer( + convert(T, rho), convert(T, eps_r), convert(T, mu_r), + convert(T, thickness) + ) + return EarthModel{T}( + vertical_layers, (air, convert(EarthLayer{T}, earth)) + ) +end + +function _earth_model(layers, vertical_layers, air_layer) + vertical_layers isa Bool || throw(ArgumentError( + "vertical_layers must be true or false" + )) + declared = if layers isa EarthLayer + (layers,) + elseif layers isa Union{Tuple, AbstractVector} + all(layer -> layer isa EarthLayer, layers) || throw(ArgumentError( + "earth layers must contain completed EarthLayer objects" + )) + Tuple(layers) + else + throw(ArgumentError( + "earth layers must be an EarthLayer or a nonempty layer collection" + )) + end + isempty(declared) && throw(ArgumentError( + "an earth model requires at least one earth layer" + )) + air_layer isa Union{Nothing, EarthLayer} || throw(ArgumentError( + "air_layer must be nothing or a completed EarthLayer" + )) + T = promote_type( + eltype.(declared)..., + air_layer === nothing ? eltype(first(declared)) : eltype(air_layer) + ) + air = air_layer === nothing ? + EarthLayer{T}(T(Inf), convert(T, 1), convert(T, 1), T(Inf)) : + convert(EarthLayer{T}, air_layer) + earth = map(layer -> convert(EarthLayer{T}, layer), declared) + return EarthModel{T}(vertical_layers, (air, earth...)) +end + +""" +$(TYPEDSIGNATURES) + +Build a complete immutable static earth model from one or more completed earth +layers. A semi-infinite air layer is prepended unless `air_layer` is supplied. + +# Arguments + +- `layers`: one `EarthLayer` or an ordered collection. Horizontal models are + ordered from the earth surface downward. + +# Keywords + +- `vertical_layers=false`: whether earth interfaces are vertical. +- `air_layer=nothing`: optional explicit semi-infinite air layer. +- `combine=:product`: gridspace composition rule. + +# Returns + +- An `EarthModel`, or a `Gridspace{EarthModel}` when an explicit finite source + is supplied. +""" +function build( + ::Type{EarthModel}, + layers; + vertical_layers = false, + air_layer = nothing, + combine::Symbol = :product +) + values = (layers, vertical_layers, air_layer) + return parameterize(EarthModel, _earth_model, values; combine) +end + +function Base.convert( + ::Type{EarthModel{T}}, model::EarthModel{U, N} +) where {T <: Real, U <: Real, N} + return EarthModel{T}( + model.vertical_layers, + ntuple(index -> convert(EarthLayer{T}, model.layers[index]), N) + ) +end + +Base.convert(::Type{EarthModel{T}}, model::EarthModel{T}) where {T <: Real} = model + +""" +$(TYPEDSIGNATURES) + +Declare a homogeneous earth model through [`layer`](@ref) and +`build(EarthModel, ...)`. Semi-infinite air is implicit. + +# Keywords + +- `rho`: electrical resistivity \\[Ω·m\\]. +- `eps_r=nothing`: relative permittivity \\[dimensionless\\]. `nothing` + selects unity in the resistivity scalar type. +- `mu_r=nothing`: relative permeability \\[dimensionless\\]. `nothing` + selects unity in the resistivity scalar type. +- `thickness=nothing`: earth-layer thickness \\[m\\]. `nothing` selects a + semi-infinite earth layer. +- `vertical_layers=false`: whether earth interfaces are vertical. +- `air_layer=nothing`: optional explicit semi-infinite air layer. +- `combine=:product`: gridspace composition rule. + +# Returns + +- An `EarthModel`, or a `Gridspace{EarthModel}` when an explicit finite source + is supplied. +""" +function homogeneous(; + rho, + eps_r = nothing, + mu_r = nothing, + thickness = nothing, + vertical_layers = false, + air_layer = nothing, + combine::Symbol = :product +) + earth_layer = layer(; rho, eps_r, mu_r, thickness, combine) + return build( + EarthModel, + earth_layer; + vertical_layers, + air_layer, + combine + ) +end diff --git a/src/earth/equivalenthomogeneous/EquivalentHomogeneous.jl b/src/earth/equivalenthomogeneous/EquivalentHomogeneous.jl new file mode 100644 index 000000000..2dff242ba --- /dev/null +++ b/src/earth/equivalenthomogeneous/EquivalentHomogeneous.jl @@ -0,0 +1,49 @@ +""" + LineCableModels.Earth.EquivalentHomogeneous + +Define equivalent homogeneous-earth rules used when a homogeneous earth-return +formulation consumes a horizontally layered [`EarthModel`](@ref). + +Material frequency dependence and equivalent-earth reduction remain separate +operations. [`AfterFD`](@ref) and [`BeforeFD`](@ref) select their composition +order through dispatch. + +# Dependencies + +$(IMPORTS) +""" +module EquivalentHomogeneous +import ...Commons: FormulationOptions +import ...Commons: formulation_options, initialize_buffers, formulas +import ...LineCableModels: validate + +export Formula, AfterFD, BeforeFD +export formula_id, formulas, rule + +#! explicit-imports: off +using DocStringExtensions: IMPORTS, TYPEDEF, TYPEDFIELDS, TYPEDSIGNATURES +#! explicit-imports: on +using ..Earth: EarthMaterial, EarthModel +import ...Commons: AbstractFormulation, Functor +import ...LineCableModels: FormulaDefinition, Expression, formula_id +#! explicit-imports: off +import ...LineCableModels: description +#! explicit-imports: on + +include("interface.jl") + +public equivalent_material, AbstractRule, AbstractSequence + +#! explicit-imports: off +const FORMULAS = ( + include("formulas/bottommost.jl"), + include("formulas/default.jl"), +) +#! explicit-imports: on + +""" +Return the built-in equivalent homogeneous-earth formula identifiers. +""" +formulas(::Type{<:Formula}) = FORMULAS + +end # module EquivalentHomogeneous diff --git a/src/earth/equivalenthomogeneous/formulas/bottommost.jl b/src/earth/equivalenthomogeneous/formulas/bottommost.jl new file mode 100644 index 000000000..114b4ae6e --- /dev/null +++ b/src/earth/equivalenthomogeneous/formulas/bottommost.jl @@ -0,0 +1,47 @@ + +""" +$(TYPEDSIGNATURES) + +**Identification.** Homogeneous earth represented by the deepest soil layer. + +**Expression.** Select the deepest layer's resistivity \\[Ω·m\\], relative +permittivity, and relative permeability (both dimensionless). Property +vectors use index 1 for air and indices 2 through N for soil. This default +selects index N for every conductor-pair layout. + +**Scope.** Martins-Britto et al. found that deep-layer conductivity predominated +in the magnetic ground-return impedance of the multilayer soil cases they +studied. This supports using deep-layer resistivity as a default, subject to +the soil structure and frequency range. Large conductivity contrasts and +high-frequency effects limit that approximation. Selecting one layer does +not implement their equivalent-conductivity formula, and their result does +not establish the accuracy of selecting its permittivity or permeability. + +Select a different rule to change the equivalent material. See [`Formula`](@ref). + +**Reference.** A. G. Martins-Britto, F. V. Lopes, and S. R. M. J. Rondineau, +“Multilayer Earth Structure Approximation by a Homogeneous Conductivity Soil +for Ground Return Impedance Calculations,” *IEEE Transactions on Power +Delivery*, 35(2), 881–891, 2020. +[DOI: 10.1109/TPWRD.2019.2930406](https://doi.org/10.1109/TPWRD.2019.2930406). +""" +description(::Type{<:Formula{:bottommost}}; compact::Bool=false) = compact ? "Bottommost" : "Bottommost earth layer" + +function Formula{:bottommost}(; parameters::NamedTuple=(;), options::Union{NamedTuple, FormulationOptions} = FormulationOptions()) + options = options isa NamedTuple ? FormulationOptions(options) : options + isempty(parameters) || throw(ArgumentError("bottommost earth has no configurable model parameters")) + return Formula{:bottommost, typeof(parameters), typeof(options)}(parameters, options) +end + +function equivalent_material( + ::Formula{:bottommost}, ::Union{Val{:self}, Val{:mutual}}, ::Val{S}, ::Val{T}, + functor, workspace +) where {S, T} + S >= 1 && T >= 1 || throw(ArgumentError("physical layer indices must be positive")) + (; rho, eps_r, mu_r) = functor.input + return EarthMaterial(rho[end], eps_r[end], mu_r[end]) +end + +formulation_options(::Expression{<:Formula{:bottommost}, typeof(equivalent_material)}) = FormulationOptions() + +:bottommost diff --git a/src/earth/equivalenthomogeneous/formulas/default.jl b/src/earth/equivalenthomogeneous/formulas/default.jl new file mode 100644 index 000000000..34d685810 --- /dev/null +++ b/src/earth/equivalenthomogeneous/formulas/default.jl @@ -0,0 +1,12 @@ +""" +$(TYPEDSIGNATURES) + +Route the default selection to the explicit `:bottommost` implementation. +No numerical equation is owned by `:default`. +""" +description(::Type{<:Formula{:default}}; compact::Bool=false) = + compact ? "Default" : "Default routing to :bottommost" + +Formula{:default}(; kwargs...) = Formula{:bottommost}(; kwargs...) + +:default diff --git a/src/earth/equivalenthomogeneous/interface.jl b/src/earth/equivalenthomogeneous/interface.jl new file mode 100644 index 000000000..35bb34167 --- /dev/null +++ b/src/earth/equivalenthomogeneous/interface.jl @@ -0,0 +1,213 @@ +""" +Abstract equivalent homogeneous-earth rule. +""" +abstract type AbstractRule <: AbstractFormulation end + +""" +Abstract ordering of material frequency dependence and EquivalentHomogeneous reduction. +""" +abstract type AbstractSequence <: AbstractFormulation end + +""" +$(TYPEDEF) + +Select one equivalent homogeneous-earth rule by its stable formula +identifier. + +Each rule implements +`equivalent_material(selected, Val(kind), Val(source), Val(target), functor, workspace)` for +the conductor pairs it reduces. The input of `functor` holds the evaluated layer properties +`rho`, `eps_r` and `mu_r`, the earth `model`, the physical conductor `pair`, the `frequency` +and the `options`. The method returns one artificial homogeneous [`EarthMaterial`](@ref). +Formula parameters participate in the concrete Julia type. + +The `:default` formula uses the bottommost soil layer as the equivalent +homogeneous material. + +$(TYPEDFIELDS) +""" +struct Formula{ID, A <: NamedTuple, O <: FormulationOptions} <: AbstractRule + "Explicit model parameters." + parameters::A + "Explicit numerical sections owned by the reduction." + options::O +end + +""" +$(TYPEDEF) + +Apply material frequency dependence to every physical layer before the EquivalentHomogeneous +rule constructs an equivalent material. + +$(TYPEDFIELDS) +""" +struct AfterFD{R <: AbstractRule} <: AbstractSequence + "Equivalent homogeneous-earth rule." + rule::R +end + +""" +$(TYPEDEF) + +Construct an equivalent material from static layers before applying the +selected material frequency dependence to that artificial material. + +$(TYPEDFIELDS) +""" +struct BeforeFD{R <: AbstractRule} <: AbstractSequence + "Equivalent homogeneous-earth rule." + rule::R +end + +""" +Return the rule stored by an EquivalentHomogeneous composition. +""" +rule(sequence::AbstractSequence) = sequence.rule + +function initialize_buffers(sequence::AbstractSequence, ::Type{T}, input, plan, + buffers) where {T} + return initialize_buffers(rule(sequence), T, input, plan, buffers) +end + +""" +Construct one formula-owned equivalent homogeneous-earth material. +""" +function equivalent_material end + +""" +$(TYPEDSIGNATURES) + +Construct an equivalent-earth rule with model parameters and numerical controls. +Custom rules subtype `AbstractRule` and extend `equivalent_material` on their +own concrete type. The selected sequence defines its position relative to the +frequency-dependent material law. Each registered rule defines its own identity +constructor. Any other identifier is unknown. +""" +Formula{ID}(; kwargs...) where {ID} = throw(ArgumentError("unknown equivalent-earth rule :$ID")) + +AfterFD(identifier::Symbol; kwargs...) = AfterFD(Formula(identifier; kwargs...)) +BeforeFD(identifier::Symbol; kwargs...) = BeforeFD(Formula(identifier; kwargs...)) + +function description(sequence::AfterFD;compact::Bool=false) + "$(description(sequence.rule;compact)) after layerwise FrequencyDependent" +end +function description(sequence::BeforeFD;compact::Bool=false) + "$(description(sequence.rule;compact)) before layerwise FrequencyDependent" +end + +""" +$(TYPEDSIGNATURES) + +Return the `equivalent_material` expression that the reduction `rule` declares for the +conductor pair `pair`: self or mutual by its indices, from the physical layer of its source to +that of its target. +""" +function Expression(rule::AbstractRule, pair) + kind = pair.row == pair.column ? :self : :mutual + return Expression(rule, equivalent_material, Val(kind), Val.(pair.layers)...) +end + +""" +$(TYPEDSIGNATURES) + +Build the Functor of the reduction `rule` for one physical conductor pair at one frequency. It +checks that the layer properties align with the layers of the earth model, that the frequency +is positive and finite, and that the pair's layers belong to the model. The state is empty. +""" +function Functor(rule::AbstractRule, input::NamedTuple; workspace = nothing) + (; rho, eps_r, mu_r, model, pair, frequency) = input + length(rho) == length(eps_r) == length(mu_r) == length(model.layers) || + throw(DimensionMismatch("EquivalentHomogeneous properties must align with the complete physical model")) + isfinite(frequency) && frequency > zero(frequency) || throw(DomainError( + frequency, + "EquivalentHomogeneous evaluation frequency must be positive and finite" + )) + validate(pair, getproperty.(model.layers, :thickness)) + return Functor(rule, input, (;)) +end + +""" +$(TYPEDSIGNATURES) + +Check that the reduction `rule` returned an [`EarthMaterial`](@ref). Return `material`. +""" +function validate(material, rule::AbstractRule) + material isa EarthMaterial || + throw(ArgumentError("an EquivalentHomogeneous contribution must return EarthMaterial")) + return material +end + +@inline function (formula::AbstractRule)( + rho::AbstractVector, + eps_r::AbstractVector, + mu_r::AbstractVector, + model::EarthModel, + pair, + frequency::Real; workspace = nothing +) + expression = validate(Expression(formula, pair)) + options = only(formulation_options(formula, (expression,)).options) + functor = Functor(formula, (; rho, eps_r, mu_r, model, pair, frequency, options); + workspace) + return validate(expression(functor, workspace), formula) +end + +function AbstractSequence(selection::FormulaDefinition{ID, Order}) where {ID, Order} + selection.equivalent_earth === nothing || + throw(ArgumentError("a reduction cannot contain another reduction")) + rule = Formula{ID}(; parameters = selection.parameters, + options = selection.options) + return Order === :before ? BeforeFD(rule) : AfterFD(rule) +end + +"""Expose the reduction rule, model parameters and numerical options as a native record.""" +function Base.NamedTuple(value::Formula) + return (identifier=formula_id(value), parameters=value.parameters, + options=value.options.data) +end + +"""Expose the order of material evaluation and the selected equivalent-earth rule.""" +Base.NamedTuple(value::AfterFD) = (order = :after, rule = NamedTuple(value.rule)) +Base.NamedTuple(value::BeforeFD) = (order = :before, rule = NamedTuple(value.rule)) + +Formula(identifier::Symbol; kwargs...) = Formula(Val(identifier); kwargs...) +Formula(::Val{ID}; kwargs...) where {ID} = Formula{ID}(; kwargs...) +Formula(selected::AbstractRule) = selected + +function Formula(selection::FormulaDefinition{ID, Order}) where {ID, Order} + Order === :default || throw(ArgumentError("order applies only to equivalent_earth")) + selection.equivalent_earth === nothing || + throw(ArgumentError("a reduction cannot contain another reduction")) + return Formula{ID}(; parameters = selection.parameters, + options = selection.options) +end + +""" +Return the stable formula identifier of an EquivalentHomogeneous formula. +""" +formula_id(::Formula{ID}) where {ID} = ID +formula_id(::Type{<:Formula{ID}}) where {ID} = ID +# Identity-only dispatch also describes retained selections without constructors. +description(value::Formula; compact::Bool = false) = description(typeof(value); compact) +formulation_options(value::Formula) = value.options + +"""Iterate the independently selectable child slots admitted by this formula family.""" +Base.pairs(::Type{<:Formula}; quantity = nothing) = pairs((;)) + +"""Describe an explicit equivalent-earth rule and its requested material-law order.""" +description(owner::Type{FormulaDefinition},slot::Val{:equivalent_earth},value::FormulaDefinition; + compact::Bool=true) = description(owner,slot,NamedTuple(value);compact) +function description(::Type{FormulaDefinition},::Val{:equivalent_earth},value::NamedTuple; + compact::Bool=true) + record=get(value,:rule,value) + text=record.identifier in formulas(Formula) ? description(Formula{record.identifier};compact) : + string(record.identifier) + order=get(value,:order,:default) + order===:default || (text *= " "*string(order)*" FrequencyDependent") + controls=(; (key => record[key] for key in (:parameters, :options) + if haskey(record,key) && !isempty(record[key]))...) + isempty(controls) || (text *= " "*description(FormulaDefinition,controls;compact)) + return text +end +description(::Type{FormulaDefinition},::Val{:equivalent_earth},value::AbstractSequence; + compact::Bool=true) = description(value;compact) diff --git a/src/earth/frequencydependent/FrequencyDependent.jl b/src/earth/frequencydependent/FrequencyDependent.jl new file mode 100644 index 000000000..a7eff5d24 --- /dev/null +++ b/src/earth/frequencydependent/FrequencyDependent.jl @@ -0,0 +1,57 @@ +""" + LineCableModels.Earth.FrequencyDependent + +Define measured and material-physics relations that map one soil material and +one frequency to its frequency-dependent electromagnetic properties. + +The explicit `:constant` formula is the frequency-independent pass-through. +The `:default` formula is a routing alias for `:constant`. The remaining +registered identifiers implement literature-based frequency-dispersive laws. + +# Dependencies + +$(IMPORTS) +""" +module FrequencyDependent +import ...Commons: FormulationOptions, formulas +import ...Commons: formulation_options +import ...LineCableModels: FormulaDefinition + +export Formula, formula_id, formulas +public FrequencyDependentFormulation, earth_material + +#! explicit-imports: off +using DocStringExtensions: IMPORTS, TYPEDEF, TYPEDFIELDS, TYPEDSIGNATURES +#! explicit-imports: on +using ..Earth: EarthMaterial +import ...Commons: AbstractFormulation, Functor +import ...LineCableModels: Expression, constitutive, formula_id, validate +#! explicit-imports: off +import ...LineCableModels: description +using ...Commons: vacuum_permittivity +#! explicit-imports: on + +include("interface.jl") + +#! explicit-imports: off +const FORMULAS = ( + include("formulas/alipio2014.jl"), + include("formulas/cigre2019.jl"), + include("formulas/constant.jl"), + include("formulas/datsios2019.jl"), + include("formulas/default.jl"), + include("formulas/longmire1975.jl"), + include("formulas/messier1985.jl"), + include("formulas/portela1999.jl"), + include("formulas/scott1967.jl"), + include("formulas/visacro1987.jl"), + include("formulas/visacro2012.jl"), +) +#! explicit-imports: on + +""" +Return the built-in frequency-dependent earth-material formula identifiers. +""" +formulas(::Type{<:Formula}) = FORMULAS + +end # module FrequencyDependent diff --git a/src/earth/frequencydependent/formulas/alipio2014.jl b/src/earth/frequencydependent/formulas/alipio2014.jl new file mode 100644 index 000000000..3c6b270eb --- /dev/null +++ b/src/earth/frequencydependent/formulas/alipio2014.jl @@ -0,0 +1,69 @@ +# Construct `:alipio2014` with the fitted parameters of the Alipio-Visacro causal soil +# model as parameter defaults. +function Formula{:alipio2014}(; parameters::NamedTuple = (;), + options::Union{NamedTuple, FormulationOptions} = FormulationOptions()) + defaults = (exponent = 0.54, epsilon_infinity = 12.0, scale = 1.26, + conductivity_exponent = -0.73) + return Formula{:alipio2014}(defaults, parameters, options) +end + +function validate(selected::Formula{:alipio2014}) + exponent = selected.parameters.exponent + isinteger(exponent) && isodd(exponent) && throw(ArgumentError( + "Alipio–Visacro exponent must not be an odd integer (tangent pole)")) + return selected +end + +""" +$(TYPEDSIGNATURES) + +Causal power law fitted by Alipio and Visacro to measured soil dispersion. +The relation evaluates conductivity and relative permittivity from the static +reference resistivity. Ametani is not involved in this model. + +**Expression.** With ``\\widehat\\sigma_0=1000/\\rho_0`` in mS/m, +``\\gamma=0.54``, and ``D=1.26\\widehat\\sigma_0^{-0.73}``, + +```math +\\widehat\\sigma(f)=\\widehat\\sigma_0 +\\left[1+D\\left(\\frac{f}{10^6}\\right)^\\gamma\\right], +``` + +```math +\\varepsilon_r(f)=12+ +\\tan\\left(\\frac{\\pi\\gamma}{2}\\right) +\\frac{10^{-3}\\widehat\\sigma_0D f^{\\gamma-1}} +{2\\pi\\varepsilon_0\\cdot10^{6\\gamma}}. +``` +""" +function description(::Type{<:Formula{:alipio2014}}; compact::Bool=false) + compact ? "Alipio" : "Alipio–Visacro causal soil dispersion (2014)" +end + +function earth_material(formula::Formula{:alipio2014}, functor, workspace) + (; material, frequency) = functor.input + T = typeof(frequency) + parameters = formula.parameters + gamma = convert(T, parameters.exponent) + epsilon_infinity = convert(T, parameters.epsilon_infinity) + scale = convert(T, parameters.scale) + conductivity_exponent = convert(T, parameters.conductivity_exponent) + thousand = convert(T, 1000) + million = convert(T, 1e6) + conductivity_reference = thousand / material.rho + dispersion = scale * conductivity_reference^conductivity_exponent + epsilon0 = vacuum_permittivity(typeof(frequency)) + relative_permittivity = epsilon_infinity + + tan((one(frequency) * π) * gamma / 2) * + convert(T, 1e-3) * conductivity_reference * dispersion * + frequency^(gamma - one(gamma)) / + (2 * (one(frequency) * π) * epsilon0 * + convert(T, 10)^(6 * gamma)) + conductivity = conductivity_reference * + (one(frequency) + dispersion * (frequency / million)^gamma) + return EarthMaterial{T}(thousand / conductivity, relative_permittivity, material.mu_r) +end + +formulation_options(::Expression{<:Formula{:alipio2014}, typeof(earth_material)}) = FormulationOptions() + +:alipio2014 diff --git a/src/earth/frequencydependent/formulas/cigre2019.jl b/src/earth/frequencydependent/formulas/cigre2019.jl new file mode 100644 index 000000000..ad1d32216 --- /dev/null +++ b/src/earth/frequencydependent/formulas/cigre2019.jl @@ -0,0 +1,51 @@ +# Construct `:cigre2019` with the fitted parameters recommended by CIGRE Technical +# Brochure 781 as parameter defaults. +function Formula{:cigre2019}(; parameters::NamedTuple = (;), + options::Union{NamedTuple, FormulationOptions} = FormulationOptions()) + defaults = (epsilon_infinity = 12.0, epsilon_scale = 9.5e4, + epsilon_conductivity_exponent = 0.27, epsilon_frequency_exponent = -0.46, + conductivity_scale = 4.7e-6, conductivity_frequency_exponent = 0.54) + return Formula{:cigre2019}(defaults, parameters, options) +end + +""" +$(TYPEDSIGNATURES) + +CIGRE WG C4.33 recommended empirical soil-dispersion relation. + +**Expression.** With ``\\sigma_0=1/\\rho_0``, + +```math +\\varepsilon_r(f)=12+9.5\\times10^4\\sigma_0^{0.27}f^{-0.46}, +\\qquad +\\sigma(f)=\\sigma_0+4.7\\times10^{-6}\\sigma_0^{0.27}f^{0.54}. +``` + +The fitted constants are exposed through the formula parameters. + +**Reference.** CIGRE WG C4.33, Technical Brochure 781 (2019). +""" +function description(::Type{<:Formula{:cigre2019}}; compact::Bool=false) + compact ? "CIGRE" : "CIGRE WG C4.33 recommended soil dispersion (2019)" +end + +function earth_material(formula::Formula{:cigre2019}, functor, workspace) + (; material, frequency) = functor.input + T = typeof(frequency) + parameters = formula.parameters + conductivity_reference = inv(material.rho) + conductivity_exponent = convert(T, parameters.epsilon_conductivity_exponent) + relative_permittivity = convert(T, parameters.epsilon_infinity) + + convert(T, parameters.epsilon_scale) * + conductivity_reference^conductivity_exponent * + frequency^convert(T, parameters.epsilon_frequency_exponent) + conductivity = conductivity_reference + + convert(T, parameters.conductivity_scale) * + conductivity_reference^conductivity_exponent * + frequency^convert(T, parameters.conductivity_frequency_exponent) + return EarthMaterial{T}(inv(conductivity), relative_permittivity, material.mu_r) +end + +formulation_options(::Expression{<:Formula{:cigre2019}, typeof(earth_material)}) = FormulationOptions() + +:cigre2019 diff --git a/src/earth/frequencydependent/formulas/constant.jl b/src/earth/frequencydependent/formulas/constant.jl new file mode 100644 index 000000000..3cca1dde5 --- /dev/null +++ b/src/earth/frequencydependent/formulas/constant.jl @@ -0,0 +1,44 @@ +""" +$(TYPEDSIGNATURES) + +**Identification.** Frequency-independent earth-material pass-through. + +**Expression.** + +```math +\\rho(f)=\\rho_0,\\qquad +\\varepsilon_r(f)=\\varepsilon_{r,0},\\qquad +\\mu_r(f)=\\mu_{r,0}. +``` + +This relation preserves the static material at every positive evaluation +frequency. It is the explicit equation selected by `:default`. +""" +function description(::Type{<:Formula{:constant}}; compact::Bool=false) + compact ? "Constant" : "Constant frequency-independent earth material" +end + +""" +$(TYPEDSIGNATURES) + +Preserve the supplied static earth properties at the requested frequency. + +# Arguments + +- `functor`: the Functor of the evaluation point. Its input holds: + - `material`: static earth material. + - `frequency`: evaluation frequency \\[Hz\\]. + - `options`: the normalized formulation options of the formula. +- `workspace`: optional execution resources. + +# Returns + +- The unchanged `EarthMaterial`. +""" +function earth_material(::Formula{:constant}, functor, workspace) + return functor.input.material +end + +formulation_options(::Expression{<:Formula{:constant}, typeof(earth_material)}) = FormulationOptions() + +:constant diff --git a/src/earth/frequencydependent/formulas/datsios2019.jl b/src/earth/frequencydependent/formulas/datsios2019.jl new file mode 100644 index 000000000..0cb2e2f50 --- /dev/null +++ b/src/earth/frequencydependent/formulas/datsios2019.jl @@ -0,0 +1,70 @@ +# Construct `:datsios2019` with the dry-soil parameter of the Datsios-Mikropoulos relation +# as parameter defaults. +function Formula{:datsios2019}(; parameters::NamedTuple = (;), + options::Union{NamedTuple, FormulationOptions} = FormulationOptions()) + defaults = (dry_permittivity = 3.5,) + return Formula{:datsios2019}(defaults, parameters, options) +end + +""" +$(TYPEDSIGNATURES) + +Datsios-Mikropoulos two-limit fit for sandy soil, with relative permittivity +held at its 3 kHz value below the fitted frequency limit. + +**Expression.** With ``\\widehat\\sigma_{42}=10^4/\\rho_0`` in μS/cm and +dry permittivity ``\\varepsilon_d=3.5``, + +```math +p=0.537\\widehat\\sigma_{42}^{0.16},\\quad +\\varepsilon_\\infty=1.24\\widehat\\sigma_{42}^{0.415}\\varepsilon_d, +\\quad +\\varepsilon_{3k}=4\\widehat\\sigma_{42}^{0.463}(2.9\\varepsilon_d-3.8). +``` + +The conductivity interpolation is evaluated between the measured 42 Hz and +high-frequency limits. + +**Reference.** Z. G. Datsios and P. N. Mikropoulos, *IEEE Transactions on +Dielectrics and Electrical Insulation*, 26(3), 2019. +""" +function description(::Type{<:Formula{:datsios2019}}; compact::Bool=false) + compact ? "Datsios" : "Datsios–Mikropoulos two-limit soil fit (2019)" +end + +function earth_material(formula::Formula{:datsios2019}, functor, workspace) + (; material, frequency) = functor.input + T = typeof(frequency) + parameters = formula.parameters + conductivity_low = convert(T, 1e4) / material.rho + frequency_low = convert(T, 42) + frequency_boundary = convert(T, 3000) + dry_permittivity = convert(T, parameters.dry_permittivity) + + permittivity_exponent = convert(T, 0.537) * conductivity_low^convert(T, 0.16) + dry_permittivity_3khz = convert(T, 2.9) * dry_permittivity - convert(T, 3.8) + permittivity_high = convert(T, 1.24) * + conductivity_low^convert(T, 0.415) * dry_permittivity + permittivity_3khz = convert(T, 4) * + conductivity_low^convert(T, 0.463) * dry_permittivity_3khz + permittivity_frequency = frequency < frequency_boundary ? + frequency_boundary : frequency + relative_permittivity = permittivity_high + + (frequency_boundary / permittivity_frequency)^permittivity_exponent * + (permittivity_3khz - permittivity_high) + + conductivity_high = conductivity_low * ( + one(frequency) + convert(T, 0.65) / conductivity_low^convert(T, 0.57) + ) + micro = convert(T, 1e-6) + conductivity_micro_siemens_per_centimetre = + conductivity_high * frequency * micro + + (frequency_low - frequency_low * (frequency - frequency_low) * micro) * + (conductivity_low / frequency_low - conductivity_high * micro) + conductivity = convert(T, 1e-4) * conductivity_micro_siemens_per_centimetre + return EarthMaterial{T}(inv(conductivity), relative_permittivity, material.mu_r) +end + +formulation_options(::Expression{<:Formula{:datsios2019}, typeof(earth_material)}) = FormulationOptions() + +:datsios2019 diff --git a/src/earth/frequencydependent/formulas/default.jl b/src/earth/frequencydependent/formulas/default.jl new file mode 100644 index 000000000..033312676 --- /dev/null +++ b/src/earth/frequencydependent/formulas/default.jl @@ -0,0 +1,12 @@ +""" +$(TYPEDSIGNATURES) + +Route the default selection to the explicit `:constant` implementation. +No numerical equation is owned by `:default`. +""" +description(::Type{<:Formula{:default}}; compact::Bool=false) = + compact ? "Default" : "Default routing to :constant" + +Formula{:default}(; kwargs...) = Formula{:constant}(; kwargs...) + +:default diff --git a/src/earth/frequencydependent/formulas/longmire1975.jl b/src/earth/frequencydependent/formulas/longmire1975.jl new file mode 100644 index 000000000..b9f3d5fa0 --- /dev/null +++ b/src/earth/frequencydependent/formulas/longmire1975.jl @@ -0,0 +1,74 @@ +const LONGMIRE_SMITH_COEFFICIENTS = ( + 3.4e6, 2.74e5, 2.58e4, 3.38e3, 5.26e2, 1.33e2, 2.72e1, + 1.25e1, 4.8, 2.17, 0.98, 0.392, 0.173 +) + +# Construct `:longmire1975` with the high-frequency and relaxation parameters of +# Longmire-Smith as parameter defaults. +function Formula{:longmire1975}(; parameters::NamedTuple = (;), + options::Union{NamedTuple, FormulationOptions} = FormulationOptions()) + defaults = (epsilon_infinity = 5.0, corner_scale = 125.0, corner_exponent = 0.8312) + return Formula{:longmire1975}(defaults, parameters, options) +end + +function validate(selected::Formula{:longmire1975}) + selected.parameters.corner_scale > 0 || throw(ArgumentError( + "Longmire–Smith corner_scale must be positive")) + return selected +end + +""" +$(TYPEDSIGNATURES) + +Longmire-Smith thirteen-term dielectric-relaxation soil model. + +**Expression.** With ``\\sigma_0=1/\\rho_0``, base corner +``f_c=(125\\sigma_0)^{0.8312}``, tabulated coefficients ``a_n``, and +``f_n=10^{n-1}f_c``, + +```math +\\varepsilon_r(f)=5+\\sum_{n=1}^{13} +\\frac{a_n}{1+(f/f_n)^2}, +``` + +```math +\\sigma(f)=\\sigma_0+2\\pi f\\varepsilon_0 +\\sum_{n=1}^{13}\\frac{a_n(f/f_n)}{1+(f/f_n)^2}. +``` + +**Reference.** C. L. Longmire and K. S. Smith, *A Universal Impedance for +Soils*, Defense Nuclear Agency, 1975. +""" +function description(::Type{<:Formula{:longmire1975}}; compact::Bool=false) + compact ? "Longmire" : "Longmire–Smith 13-term dielectric relaxation (1975)" +end + +function earth_material(formula::Formula{:longmire1975}, functor, workspace) + (; material, frequency) = functor.input + T = typeof(frequency) + parameters = formula.parameters + conductivity_reference = inv(material.rho) + corner = (convert(T, parameters.corner_scale) * conductivity_reference)^convert(T, parameters.corner_exponent) + permittivity_sum = zero(frequency) + conductivity_sum = zero(frequency) + decade = one(frequency) + ten = convert(T, 10) + @inbounds for coefficient in LONGMIRE_SMITH_COEFFICIENTS + corner_frequency = corner * decade + ratio = frequency / corner_frequency + denominator = one(frequency) + ratio^2 + typed_coefficient = convert(T, coefficient) + permittivity_sum += typed_coefficient / denominator + conductivity_sum += typed_coefficient * ratio / denominator + decade *= ten + end + relative_permittivity = convert(T, parameters.epsilon_infinity) + permittivity_sum + conductivity = conductivity_reference + + 2 * (one(frequency) * π) * frequency * + vacuum_permittivity(typeof(frequency)) * conductivity_sum + return EarthMaterial{T}(inv(conductivity), relative_permittivity, material.mu_r) +end + +formulation_options(::Expression{<:Formula{:longmire1975}, typeof(earth_material)}) = FormulationOptions() + +:longmire1975 diff --git a/src/earth/frequencydependent/formulas/messier1985.jl b/src/earth/frequencydependent/formulas/messier1985.jl new file mode 100644 index 000000000..eb4040e1a --- /dev/null +++ b/src/earth/frequencydependent/formulas/messier1985.jl @@ -0,0 +1,57 @@ +# Construct `:messier1985` with the high-frequency permittivity parameter of the Messier +# relation as parameter defaults. +function Formula{:messier1985}(; parameters::NamedTuple = (;), + options::Union{NamedTuple, FormulationOptions} = FormulationOptions()) + defaults = (epsilon_infinity = 8.0,) + return Formula{:messier1985}(defaults, parameters, options) +end + +function validate(selected::Formula{:messier1985}) + selected.parameters.epsilon_infinity >= 0 || throw(ArgumentError( + "Messier epsilon_infinity must be nonnegative for the real square roots")) + return selected +end + +""" +$(TYPEDSIGNATURES) + +Messier square-root dispersive soil model. + +**Expression.** With ``\\sigma_0=1/\\rho_0`` and +``\\varepsilon_\\infty=8``, + +```math +\\varepsilon_r(f)=\\varepsilon_\\infty+ +\\sqrt{\\frac{\\sigma_0\\varepsilon_\\infty}{\\pi f\\varepsilon_0}}, +\\qquad +\\sigma(f)=\\sigma_0+ +\\sqrt{4\\pi f\\sigma_0\\varepsilon_0\\varepsilon_\\infty}. +``` + +**Reference.** M. Messier, *Another Soil Conductivity Model*, JAYCOR, +Santa Barbara, 1985. +""" +function description(::Type{<:Formula{:messier1985}}; compact::Bool=false) + compact ? "Messier" : "Messier square-root soil dispersion (1985)" +end + +function earth_material(formula::Formula{:messier1985}, functor, workspace) + (; material, frequency) = functor.input + T = typeof(frequency) + parameters = formula.parameters + conductivity_reference = inv(material.rho) + epsilon_infinity = convert(T, parameters.epsilon_infinity) + epsilon0 = vacuum_permittivity(typeof(frequency)) + pi_typed = one(frequency) * π + relative_permittivity = epsilon_infinity + sqrt( + conductivity_reference * epsilon_infinity / (pi_typed * frequency * epsilon0) + ) + conductivity = conductivity_reference + sqrt( + 4 * pi_typed * frequency * conductivity_reference * epsilon0 * epsilon_infinity + ) + return EarthMaterial{T}(inv(conductivity), relative_permittivity, material.mu_r) +end + +formulation_options(::Expression{<:Formula{:messier1985}, typeof(earth_material)}) = FormulationOptions() + +:messier1985 diff --git a/src/earth/frequencydependent/formulas/portela1999.jl b/src/earth/frequencydependent/formulas/portela1999.jl new file mode 100644 index 000000000..4af64dd90 --- /dev/null +++ b/src/earth/frequencydependent/formulas/portela1999.jl @@ -0,0 +1,57 @@ +# Construct `:portela1999` with the fitted parameters of the Portela soil-dispersion +# relation as parameter defaults. +function Formula{:portela1999}(; parameters::NamedTuple = (;), + options::Union{NamedTuple, FormulationOptions} = FormulationOptions()) + defaults = (beta = 0.1, exponent = 0.72) + return Formula{:portela1999}(defaults, parameters, options) +end + +function validate(selected::Formula{:portela1999}) + exponent = selected.parameters.exponent + isinteger(exponent) && isodd(exponent) && throw(ArgumentError( + "Portela exponent must not be an odd integer (tangent pole)")) + return selected +end + +""" +$(TYPEDSIGNATURES) + +Portela causal power-law soil-dispersion model. + +**Expression.** For ``\\omega=2\\pi f``, ``\\beta=0.1``, and +``\\alpha=0.72``, + +```math +\\sigma(f)=\\sigma_0+\\beta10^{-6}\\omega^\\alpha, +\\qquad +\\varepsilon_r(f)=\\frac{\\beta10^{-6}\\tan(\\pi\\alpha/2) +\\omega^{\\alpha-1}}{\\varepsilon_0}. +``` + +**Reference.** C. M. Portela, “Measurement and Modeling of Soil +Electromagnetic Behavior,” *IEEE International Symposium on Electromagnetic +Compatibility*, 1004–1009, 1999. +""" +function description(::Type{<:Formula{:portela1999}}; compact::Bool=false) + compact ? "Portela" : "Portela power-law soil dispersion (1999)" +end + +function earth_material(formula::Formula{:portela1999}, functor, workspace) + (; material, frequency) = functor.input + T = typeof(frequency) + parameters = formula.parameters + beta = convert(T, parameters.beta) + exponent = convert(T, parameters.exponent) + angular_frequency = 2 * (one(frequency) * π) * frequency + fitted_scale = beta * convert(T, 1e-6) + conductivity = inv(material.rho) + fitted_scale * angular_frequency^exponent + relative_permittivity = fitted_scale * + tan((one(frequency) * π) * exponent / 2) * + angular_frequency^(exponent - one(exponent)) / + vacuum_permittivity(typeof(frequency)) + return EarthMaterial{T}(inv(conductivity), relative_permittivity, material.mu_r) +end + +formulation_options(::Expression{<:Formula{:portela1999}, typeof(earth_material)}) = FormulationOptions() + +:portela1999 diff --git a/src/earth/frequencydependent/formulas/scott1967.jl b/src/earth/frequencydependent/formulas/scott1967.jl new file mode 100644 index 000000000..c1206f7ae --- /dev/null +++ b/src/earth/frequencydependent/formulas/scott1967.jl @@ -0,0 +1,52 @@ +""" +$(TYPEDSIGNATURES) + +Scott-Carroll-Cunningham empirical moist-rock soil fit over its measured range +of 100 Hz to 1 MHz. + +**Expression.** Let ``s=\\log_{10}(1000/\\rho_0)`` and ``x=\\log_{10}f``. + +```math +\\log_{10}\\varepsilon_r=5.491+0.946s-1.097x+0.069s^2-0.114sx+0.067x^2, +``` + +```math +\\log_{10}\\widehat\\sigma=0.028+1.098s-0.068x+0.036s^2-0.046sx+0.018x^2, +\\qquad \\sigma=10^{-3}\\widehat\\sigma. +``` + +**Reference.** J. H. Scott, R. D. Carroll, and D. R. Cunningham, +*Journal of Geophysical Research*, 72(20), 1967. +""" +function description(::Type{<:Formula{:scott1967}}; compact::Bool=false) + compact ? "Scott" : "Scott–Carroll–Cunningham empirical moist-soil fit (1967)" +end + +function earth_material(::Formula{:scott1967}, functor, workspace) + (; material, frequency) = functor.input + T = typeof(frequency) + thousand = convert(T, 1000) + conductivity_100hz = thousand / material.rho + conductivity_log = log10(conductivity_100hz) + frequency_log = log10(frequency) + permittivity_log = convert(T, 5.491) + + convert(T, 0.946) * conductivity_log - + convert(T, 1.097) * frequency_log + + convert(T, 0.069) * conductivity_log^2 - + convert(T, 0.114) * conductivity_log * frequency_log + + convert(T, 0.067) * frequency_log^2 + output_conductivity_log = convert(T, 0.028) + + convert(T, 1.098) * conductivity_log - + convert(T, 0.068) * frequency_log + + convert(T, 0.036) * conductivity_log^2 - + convert(T, 0.046) * conductivity_log * frequency_log + + convert(T, 0.018) * frequency_log^2 + ten = convert(T, 10) + relative_permittivity = ten^permittivity_log + conductivity = ten^output_conductivity_log + return EarthMaterial{T}(thousand / conductivity, relative_permittivity, material.mu_r) +end + +formulation_options(::Expression{<:Formula{:scott1967}, typeof(earth_material)}) = FormulationOptions() + +:scott1967 diff --git a/src/earth/frequencydependent/formulas/visacro1987.jl b/src/earth/frequencydependent/formulas/visacro1987.jl new file mode 100644 index 000000000..f22cd2595 --- /dev/null +++ b/src/earth/frequencydependent/formulas/visacro1987.jl @@ -0,0 +1,50 @@ +# Construct `:visacro1987` with the normalization parameter of the Visacro-Portela soil +# relation as parameter defaults. +function Formula{:visacro1987}(; parameters::NamedTuple = (;), + options::Union{NamedTuple, FormulationOptions} = FormulationOptions()) + defaults = (normalization_frequency = 100.0,) + return Formula{:visacro1987}(defaults, parameters, options) +end + +function validate(selected::Formula{:visacro1987}) + selected.parameters.normalization_frequency > 0 || throw(ArgumentError( + "Visacro–Portela normalization_frequency must be positive [Hz]")) + return selected +end + +""" +$(TYPEDSIGNATURES) + +Visacro-Portela empirical power laws for soil conductivity and permittivity. + +**Expression.** With ``\\sigma_0=1/\\rho_0``, + +```math +\\varepsilon_r(f)=2.34\\times10^6\\sigma_0^{0.535}f^{-0.597}, +\\qquad +\\sigma(f)=\\sigma_0\\left(\\frac{f}{100}\\right)^{0.072}. +``` + +**Reference.** S. Visacro and C. M. Portela, *International Symposium on +High Voltage Engineering*, 1987. +""" +function description(::Type{<:Formula{:visacro1987}}; compact::Bool=false) + compact ? "Visacro" : "Visacro–Portela empirical soil dispersion (1987)" +end + +function earth_material(formula::Formula{:visacro1987}, functor, workspace) + (; material, frequency) = functor.input + T = typeof(frequency) + parameters = formula.parameters + conductivity_reference = inv(material.rho) + relative_permittivity = convert(T, 2.34e6) * + conductivity_reference^convert(T, 0.535) * + frequency^convert(T, -0.597) + conductivity = conductivity_reference * + (frequency / convert(T, parameters.normalization_frequency))^convert(T, 0.072) + return EarthMaterial{T}(inv(conductivity), relative_permittivity, material.mu_r) +end + +formulation_options(::Expression{<:Formula{:visacro1987}, typeof(earth_material)}) = FormulationOptions() + +:visacro1987 diff --git a/src/earth/frequencydependent/formulas/visacro2012.jl b/src/earth/frequencydependent/formulas/visacro2012.jl new file mode 100644 index 000000000..b0c6d36e2 --- /dev/null +++ b/src/earth/frequencydependent/formulas/visacro2012.jl @@ -0,0 +1,54 @@ +# Construct `:visacro2012` with the lower frequency limit of the Visacro-Alipio soil +# relation as parameter defaults. +function Formula{:visacro2012}(; parameters::NamedTuple = (;), + options::Union{NamedTuple, FormulationOptions} = FormulationOptions()) + defaults = (frequency_boundary = 100.0,) + return Formula{:visacro2012}(defaults, parameters, options) +end + +function validate(selected::Formula{:visacro2012}) + selected.parameters.frequency_boundary > 0 || throw(ArgumentError( + "Visacro–Alipio frequency_boundary must be positive [Hz]")) + return selected +end + +""" +$(TYPEDSIGNATURES) + +Visacro-Alipio empirical causal soil-dispersion fit with a 100 Hz lower +frequency limit. + +**Expression.** With ``f_e=\\max(f,100)`` and ``\\sigma_0=1/\\rho_0``, + +```math +\\varepsilon_r(f)=1.3+7.6\\times10^3 f_e^{-0.4}, +\\qquad +\\sigma(f)=\\sigma_0+1.2\\times10^{-6}\\sigma_0^{0.27}(f_e-100)^{0.65}. +``` + +**Reference.** S. Visacro and R. Alipio, *IEEE Transactions on Power +Delivery*, 27(2), 2012. +""" +function description(::Type{<:Formula{:visacro2012}}; compact::Bool=false) + compact ? "Visacro" : "Visacro–Alipio empirical soil dispersion (2012)" +end + +function earth_material(formula::Formula{:visacro2012}, functor, workspace) + (; material, frequency) = functor.input + T = typeof(frequency) + parameters = formula.parameters + frequency_boundary = convert(T, parameters.frequency_boundary) + evaluated_frequency = frequency < frequency_boundary ? + frequency_boundary : frequency + conductivity_reference = inv(material.rho) + relative_permittivity = convert(T, 1.3) + + convert(T, 7.6e3) * evaluated_frequency^convert(T, -0.4) + conductivity = conductivity_reference + + convert(T, 1.2e-6) * conductivity_reference^convert(T, 0.27) * + (evaluated_frequency - frequency_boundary)^convert(T, 0.65) + return EarthMaterial{T}(inv(conductivity), relative_permittivity, material.mu_r) +end + +formulation_options(::Expression{<:Formula{:visacro2012}, typeof(earth_material)}) = FormulationOptions() + +:visacro2012 diff --git a/src/earth/frequencydependent/interface.jl b/src/earth/frequencydependent/interface.jl new file mode 100644 index 000000000..2becac5ac --- /dev/null +++ b/src/earth/frequencydependent/interface.jl @@ -0,0 +1,152 @@ +""" +Interface for a selected frequency-dependent earth-material relation. +Concrete selections expose model `parameters` and numerical `options` records. +""" +abstract type FrequencyDependentFormulation <: AbstractFormulation end + +""" +$(TYPEDEF) + +Select one frequency-dependent earth-material relation by its stable formula +identifier. + +Each formula implements `earth_material(selected, functor, workspace) -> EarthMaterial` on +its concrete selection type. The input of `functor` holds the earth material, the frequency +and the options. `:default` is a routing alias for the explicit `:constant` pass-through. + +$(TYPEDFIELDS) +""" +struct Formula{ID, P <: NamedTuple, O <: FormulationOptions} <: + FrequencyDependentFormulation + "Resolved physical model parameters." + parameters::P + "Normalized numerical sections for this equation." + options::O +end + +""" +Evaluate one formula-owned frequency-dependent earth material relation. +""" +function earth_material end + +""" +$(TYPEDSIGNATURES) + +Construct a selected formulation with model parameters and numerical controls. +Custom formulations extend `earth_material` on their own concrete selection type. +Unknown controls fail before numerical evaluation. A formula with model parameters +defines its own identity constructor, which holds their defaults. +""" +function Formula{ID}(; parameters::NamedTuple = (;), + options::Union{NamedTuple, FormulationOptions} = FormulationOptions()) where {ID} + return Formula{ID}((;), parameters, options) +end + +""" +$(TYPEDSIGNATURES) + +Complete the supplied model `parameters` with a formula's `defaults`, check every +coefficient and the formula's coefficient domains, and normalize its numerical controls. +""" +function Formula{ID}(defaults::NamedTuple, parameters::NamedTuple, + options::Union{NamedTuple, FormulationOptions}) where {ID} + options = options isa NamedTuple ? FormulationOptions(options) : options + unknown = setdiff(keys(parameters), keys(defaults)) + isempty(unknown) || throw(ArgumentError( + "unknown parameters for earth-property formula :$ID: $(collect(unknown))")) + parameters = merge(defaults, parameters) + for (name, value) in pairs(parameters) + value isa Real && !(value isa Bool) && isfinite(value) || + throw(ArgumentError("earth-property parameter :$name must be a finite real coefficient")) + end + selected = Formula{ID, typeof(parameters), typeof(options)}(parameters, options) + validate(selected) + expression = Expression(selected, earth_material) + normalized = formulation_options(expression, options) + return Formula{ID, typeof(parameters), typeof(normalized)}(parameters, normalized) +end + +"""Check model-specific coefficient domains before evaluating a material.""" +validate(selected::Formula) = selected + +""" +$(TYPEDSIGNATURES) + +Build the Functor of a frequency-dependent earth relation for one earth material at one +frequency, after checking the frequency. The state is empty. +""" +function Functor(formula::FrequencyDependentFormulation, input::NamedTuple; + workspace = nothing) + (; frequency) = input + isfinite(frequency) && frequency > zero(frequency) || throw(DomainError( + frequency, "earth-property evaluation frequency must be positive and finite")) + return Functor(formula, input, (;)) +end + +@inline function (formula::FrequencyDependentFormulation)( + material::EarthMaterial{T}, frequency::T; workspace = nothing +) where {T <: Real} + functor = Functor(formula, (; material, frequency, options = formula.options); workspace) + evaluated = Expression(formula, earth_material)(functor, workspace) + evaluated isa EarthMaterial || + throw(ArgumentError("a frequency-dependent earth relation must return EarthMaterial")) + return evaluated +end + +function (formula::FrequencyDependentFormulation)( + material::EarthMaterial{T}, frequency::Real; workspace = nothing) where {T <: + Real} + U = promote_type(T, typeof(float(frequency))) + return formula( + convert(EarthMaterial{U}, material), + convert(U, float(frequency)); workspace + ) +end + +""" +Pass static earth properties through when no constitutive relation is selected. +""" +constitutive(::Nothing, material::EarthMaterial, ::Real; workspace = nothing) = material + +""" +Evaluate one registered frequency-dependent earth constitutive relation. +""" +function constitutive( + formula::FrequencyDependentFormulation, material::EarthMaterial, frequency::Real; + workspace = nothing) + formula(material, frequency; workspace) +end + +Formula(identifier::Symbol; kwargs...) = Formula(Val(identifier); kwargs...) +Formula(::Val{ID}; kwargs...) where {ID} = Formula{ID}(; kwargs...) +Formula(selected::FrequencyDependentFormulation) = selected + +function Formula(selection::FormulaDefinition{ID, Order}) where {ID, Order} + Order === :default || throw(ArgumentError("order applies only to equivalent_earth")) + selection.equivalent_earth === nothing || throw(ArgumentError( + "equivalent_earth applies only to external earth formulas")) + return Formula{ID}(; parameters = selection.parameters, + options = selection.options) +end + +""" +Return the stable formula identifier of an earth-property formula. +""" +formula_id(::Formula{ID}) where {ID} = ID +formula_id(::Type{<:Formula{ID}}) where {ID} = ID +# Identity-only dispatch also describes retained selections without constructors. +description(value::Formula; compact::Bool = false) = description(typeof(value); compact) +formulation_options(value::Formula) = value.options + +""" +$(TYPEDSIGNATURES) + +Expose the selected identity, model parameters, and numerical options as a native record. +""" +function Base.NamedTuple(value::Formula) + return (identifier = formula_id(value), + parameters = value.parameters, options = value.options.data) +end + +"""Iterate the independently selectable child slots admitted by this formula family.""" +Base.pairs(::Type{<:Formula}; quantity = nothing) = pairs((;)) diff --git a/src/earthprops/EarthProps.jl b/src/earthprops/EarthProps.jl deleted file mode 100644 index bb9e75ca1..000000000 --- a/src/earthprops/EarthProps.jl +++ /dev/null @@ -1,447 +0,0 @@ -""" - LineCableModels.EarthProps - -The [`EarthProps`](@ref) module provides functionality for modeling and computing earth properties within the [`LineCableModels.jl`](index.md) package. This module includes definitions for homogeneous and layered earth models, and formulations for frequency-dependent earth properties, to be used in impedance/admittance calculations. - -# Overview - -- Defines the [`EarthModel`](@ref) object for representing horizontally or vertically multi-layered earth models with frequency-dependent properties (ρ, ε, μ). -- Provides the [`EarthLayer`](@ref) type for representing individual soil layers with electromagnetic properties. -- Implements a multi-dispatch framework to allow different formulations of frequency-dependent earth models with [`AbstractFDEMFormulation`](@ref). -- Contains utility functions for building complex multi-layered earth models and generating data summaries. - -# Dependencies - -$(IMPORTS) - -# Exports - -$(EXPORTS) -""" -module EarthProps - -# Export public API -export CPEarth, - EarthLayer, - EarthModel - -# Module-specific dependencies -using ..Commons -using ..Utils: resolve_T -import ..Commons: get_description, add! -import ..Utils: coerce_to_T -using Measurements - -include("fdprops.jl") - -""" -$(TYPEDEF) - -Represents one single earth layer in an [`EarthModel`](@ref) object, with base and frequency-dependent properties, and attributes: - -$(TYPEDFIELDS) -""" -struct EarthLayer{T <: REALSCALAR} - "Base (DC) electrical resistivity \\[Ω·m\\]." - base_rho_g::T - "Base (DC) relative permittivity \\[dimensionless\\]." - base_epsr_g::T - "Base (DC) relative permeability \\[dimensionless\\]." - base_mur_g::T - "Thickness of the layer \\[m\\]." - t::T - "Computed resistivity values \\[Ω·m\\] at given frequencies." - rho_g::Vector{T} - "Computed permittivity values \\[F/m\\] at given frequencies." - eps_g::Vector{T} - "Computed permeability values \\[H/m\\] at given frequencies." - mu_g::Vector{T} - - @doc """ - Constructs an [`EarthLayer`](@ref) instance with specified base and frequency-dependent properties. - """ - function EarthLayer{T}(base_rho_g::T, base_epsr_g::T, base_mur_g::T, t::T, - rho_g::Vector{T}, eps_g::Vector{T}, mu_g::Vector{T}) where {T <: REALSCALAR} - new{T}(base_rho_g, base_epsr_g, base_mur_g, t, rho_g, eps_g, mu_g) - end -end - -""" -$(TYPEDSIGNATURES) - -Constructs an [`EarthLayer`](@ref) instance with specified base properties and computes its frequency-dependent values. - -# Arguments - -- `frequencies`: Vector of frequency values \\[Hz\\]. -- `base_rho_g`: Base (DC) electrical resistivity of the layer \\[Ω·m\\]. -- `base_epsr_g`: Base (DC) relative permittivity of the layer \\[dimensionless\\]. -- `base_mur_g`: Base (DC) relative permeability of the layer \\[dimensionless\\]. -- `t`: Thickness of the layer \\[m\\]. -- `freq_dependence`: Instance of a subtype of [`AbstractFDEMFormulation`](@ref) defining the computation method for frequency-dependent properties. - -# Returns - -- An [`EarthLayer`](@ref) instance with computed frequency-dependent properties. - -# Examples - -```julia -frequencies = [1e3, 1e4, 1e5] -layer = $(FUNCTIONNAME)(frequencies, 100, 10, 1, 5, CPEarth()) -println(layer.rho_g) # Output: [100, 100, 100] -println(layer.eps_g) # Output: [8.854e-11, 8.854e-11, 8.854e-11] -println(layer.mu_g) # Output: [1.2566e-6, 1.2566e-6, 1.2566e-6] -``` - -# See also - -- [`CPEarth`](@ref) -""" -function EarthLayer( - frequencies::Vector{T}, - base_rho_g::T, - base_epsr_g::T, - base_mur_g::T, - t::T, - freq_dependence::AbstractFDEMFormulation, -) where {T <: REALSCALAR} - - rho_g, eps_g, mu_g = freq_dependence(frequencies, base_rho_g, base_epsr_g, base_mur_g) - return EarthLayer{T}( - base_rho_g, - base_epsr_g, - base_mur_g, - t, - rho_g, - eps_g, - mu_g, - ) -end - -function EarthLayer( - frequencies::AbstractVector, - base_rho_g, - base_epsr_g, - base_mur_g, - t, - freq_dependence, -) - T = resolve_T(frequencies, base_rho_g, base_epsr_g, base_mur_g, t) - return EarthLayer( - coerce_to_T(frequencies, T), - coerce_to_T(base_rho_g, T), - coerce_to_T(base_epsr_g, T), - coerce_to_T(base_mur_g, T), - coerce_to_T(t, T), - freq_dependence, - ) -end - -""" -$(TYPEDEF) - -Represents a multi-layered earth model with frequency-dependent properties, and attributes: - -$(TYPEDFIELDS) -""" -struct EarthModel{T <: REALSCALAR} - "Selected frequency-dependent formulation for earth properties." - freq_dependence::AbstractFDEMFormulation - "Boolean flag indicating whether the model is treated as vertically layered." - vertical_layers::Bool - "Vector of [`EarthLayer`](@ref) objects, starting with an air layer and the specified first earth layer." - layers::Vector{EarthLayer{T}} - - @doc """ - Constructs an [`EarthModel`](@ref) instance with specified attributes. - """ - function EarthModel{T}(freq_dependence::AbstractFDEMFormulation, - vertical_layers::Bool, - layers::Vector{EarthLayer{T}}) where {T <: REALSCALAR} - new{T}(freq_dependence, vertical_layers, layers) - end -end - -""" -$(TYPEDSIGNATURES) - -Constructs an [`EarthModel`](@ref) instance with a specified first earth layer. A semi-infinite air layer is always added before the first earth layer. - -# Arguments - -- `frequencies`: Vector of frequency values \\[Hz\\]. -- `rho_g`: Base (DC) electrical resistivity of the first earth layer \\[Ω·m\\]. -- `epsr_g`: Base (DC) relative permittivity of the first earth layer \\[dimensionless\\]. -- `mur_g`: Base (DC) relative permeability of the first earth layer \\[dimensionless\\]. -- `t`: Thickness of the first earth layer \\[m\\]. For homogeneous earth models (or the bottommost layer), set `t = Inf`. -- `freq_dependence`: Instance of a subtype of [`AbstractFDEMFormulation`](@ref) defining the computation method for frequency-dependent properties (default: [`CPEarth`](@ref)). -- `vertical_layers`: Boolean flag indicating whether the model should be treated as vertically-layered (default: `false`). -- `air_layer`: optional [`EarthLayer`](@ref) object representing the semi-infinite air layer (default: `EarthLayer(frequencies, Inf, 1.0, 1.0, Inf, freq_dependence)`). - -# Returns - -- An [`EarthModel`](@ref) instance with the specified attributes and computed frequency-dependent properties. - -# Examples - -```julia -frequencies = [1e3, 1e4, 1e5] -earth_model = $(FUNCTIONNAME)(frequencies, 100, 10, 1, t=Inf) -println(length(earth_model.layers)) # Output: 2 (air + top layer) -println(earth_model.rho_eff) # Output: missing -``` - -# See also - -- [`EarthLayer`](@ref) -- [`add!`](@ref) -""" -function EarthModel( - frequencies::Vector{T}, - rho_g::T, - epsr_g::T, - mur_g::T; - t::T = T(Inf), - freq_dependence::AbstractFDEMFormulation = CPEarth(), - vertical_layers::Bool = false, - air_layer::Union{EarthLayer{T}, Nothing} = nothing, -) where {T <: REALSCALAR} - - # Validate inputs - @assert all(f -> f > 0, frequencies) "Frequencies must be positive" - @assert rho_g > 0 "Resistivity must be positive" - @assert epsr_g > 0 "Relative permittivity must be positive" - @assert mur_g > 0 "Relative permeability must be positive" - @assert t > 0 || isinf(t) "Layer thickness must be positive or infinite" - - # Enforce rule for vertical model initialization - if vertical_layers && !isinf(t) - Base.error( - "A vertically-layered model must be initialized with an infinite thickness (t=Inf).", - ) - end - - # Create air layer if not provided - if air_layer === nothing - air_layer = EarthLayer(frequencies, T(Inf), T(1.0), T(1.0), T(Inf), freq_dependence) - end - - # Create top earth layer - top_layer = EarthLayer(frequencies, rho_g, epsr_g, mur_g, t, freq_dependence) - - return EarthModel{T}( - freq_dependence, - vertical_layers, - [air_layer, top_layer], - ) -end - -function EarthModel( - frequencies::AbstractVector, - rho_g, - epsr_g, - mur_g; - t = Inf, - freq_dependence = CPEarth(), - vertical_layers = false, - air_layer = nothing, -) - T = resolve_T( - frequencies, - rho_g, - epsr_g, - mur_g, - t, - freq_dependence, - vertical_layers, - air_layer, - ) - return EarthModel( - coerce_to_T(frequencies, T), - coerce_to_T(rho_g, T), - coerce_to_T(epsr_g, T), - coerce_to_T(mur_g, T); - t = coerce_to_T(t, T), - freq_dependence = freq_dependence, - vertical_layers = vertical_layers, - air_layer = air_layer === nothing ? nothing : coerce_to_T(air_layer, T), - ) -end - -""" -$(TYPEDSIGNATURES) - -Adds a new earth layer to an existing [`EarthModel`](@ref). - -# Arguments - -- `model`: Instance of [`EarthModel`](@ref) to which the new layer will be added. -- `frequencies`: Vector of frequency values \\[Hz\\]. -- `base_rho_g`: Base electrical resistivity of the new earth layer \\[Ω·m\\]. -- `base_epsr_g`: Base relative permittivity of the new earth layer \\[dimensionless\\]. -- `base_mur_g`: Base relative permeability of the new earth layer \\[dimensionless\\]. -- `t`: Thickness of the new earth layer \\[m\\] (default: `Inf`). - -# Returns - -- Modifies `model` in place by appending a new [`EarthLayer`](@ref). - -# Notes - -For **horizontal layering** (`vertical_layers = false`): - -- Layer 1 (air) is always infinite (`t = Inf`). -- Layer 2 (first earth layer) can be infinite if modeling a homogeneous half-space. -- If adding a third layer (`length(EarthModel.layers) == 3`), it can be infinite **only if the previous layer is finite**. -- No two successive earth layers (`length(EarthModel.layers) > 2`) can have infinite thickness. - -For **vertical layering** (`vertical_layers = true`): - -- Layer 1 (air) is always **horizontal** and infinite at `z > 0`. -- Layer 2 (first vertical layer) is always **infinite** in `z < 0` **and** `y < 0`. The first vertical layer is assumed to always end at `y = 0`. -- Layer 3 (second vertical layer) **can be infinite** (establishing a vertical interface at `y = 0`). -- Subsequent layers **can be infinite only if the previous is finite**. -- No two successive vertical layers (`length(EarthModel.layers) > 3`) can both be infinite. - -# Examples - -```julia -frequencies = [1e3, 1e4, 1e5] - -# Define a horizontal model with finite thickness for the first earth layer -horz_earth_model = EarthModel(frequencies, 100, 10, 1, t=5) - -# Add a second horizontal earth layer -$(FUNCTIONNAME)(horz_earth_model, frequencies, 200, 15, 1, t=10) -println(length(horz_earth_model.layers)) # Output: 3 - -# The bottom layer should be set to infinite thickness -$(FUNCTIONNAME)(horz_earth_model, frequencies, 300, 15, 1, t=Inf) -println(length(horz_earth_model.layers)) # Output: 4 - -# Initialize a vertical-layered model with first interface at y = 0. -vert_earth_model = EarthModel(frequencies, 100, 10, 1, t=Inf, vertical_layers=true) - -# Add a second vertical layer at y = 0 (this can also be infinite) -$(FUNCTIONNAME)(vert_earth_model, frequencies, 150, 12, 1, t=Inf) -println(length(vert_earth_model.layers)) # Output: 3 - -# Attempt to add a third infinite layer (invalid case) -try - $(FUNCTIONNAME)(vert_earth_model, frequencies, 120, 12, 1, t=Inf) -catch e - println(e) # Error: Cannot add consecutive vertical layers with infinite thickness. -end - -# Fix: Set a finite thickness to the currently rightmost layer -vert_earth_model.layers[end].t = 3 - -# Add the third layer with infinite thickness now -$(FUNCTIONNAME)(vert_earth_model, frequencies, 120, 12, 1, t=Inf) -println(length(vert_earth_model.layers)) # Output: 4 -``` - -# See also - -- [`EarthLayer`](@ref) -""" -function add!( - model::EarthModel{T}, - frequencies::Vector{T}, - base_rho_g::T, - base_epsr_g::T, - base_mur_g::T; - t::T = T(Inf), -) where {T <: REALSCALAR} - - num_layers = length(model.layers) - - # Validate inputs following established pattern - @assert all(f -> f > 0, frequencies) "Frequencies must be positive" - @assert base_rho_g > 0 "Resistivity must be positive" - @assert base_epsr_g > 0 "Relative permittivity must be positive" - @assert base_mur_g > 0 "Relative permeability must be positive" - @assert t > 0 || isinf(t) "Layer thickness must be positive or infinite" - @assert eltype(frequencies) === T "frequencies eltype must match model T" - @assert all(x -> x isa T, (base_rho_g, base_epsr_g, base_mur_g)) "scalars must match model T" - - # Enforce thickness rules - if isinf(last(model.layers).t) - # The current last layer is infinite. - if model.vertical_layers && num_layers == 2 - # This is the special case: adding the second earth layer to a vertical model. - # The new layer can be finite or infinite. No error. - else - # For all other cases (horizontal, or vertical with >2 earth layers), - # it's an error to add anything after an infinite layer. - model_type = model.vertical_layers ? "vertical" : "horizontal" - Base.error("Cannot add a $(model_type) layer after an infinite one.") - end - end - - # Create the new earth layer - new_layer = EarthLayer( - frequencies, - base_rho_g, - base_epsr_g, - base_mur_g, - t, - model.freq_dependence, - ) - push!(model.layers, new_layer) - - model -end - -function add!( - model::EarthModel, - frequencies::AbstractVector, - base_rho_g, - base_epsr_g, - base_mur_g; - t = Inf, -) - - # Resolve the required type from ALL inputs (the model + the new layer) - T_new = resolve_T(model, frequencies, base_rho_g, base_epsr_g, base_mur_g, t) - T_old = eltype(model) - - if T_new == T_old - # CASE 1: No promotion needed. The model already has the correct type. - # This is the fast path that mutates the existing model. - return add!( - model, # Pass the original model - coerce_to_T(frequencies, T_new), - coerce_to_T(base_rho_g, T_new), - coerce_to_T(base_epsr_g, T_new), - coerce_to_T(base_mur_g, T_new); - t = coerce_to_T(t, T_new), - ) - else - # CASE 2: Promotion is required (e.g., from Float64 to Measurement). - @warn """ - Adding a `$T_new` layer to a `$T_old` EarthModel created a new object and did NOT modify the original in-place. - You MUST capture the returned value to avoid losing changes, e.g. `earth_model = add!(earth_model, ...)` - """ - - # 1. Create a new model by coercing the original one to the new type. - promoted_model = coerce_to_T(model, T_new) - - # 2. Call the inner add! method on the NEWLY CREATED model. - return add!( - promoted_model, - coerce_to_T(frequencies, T_new), - coerce_to_T(base_rho_g, T_new), - coerce_to_T(base_epsr_g, T_new), - coerce_to_T(base_mur_g, T_new); - t = coerce_to_T(t, T_new), - ) - end -end - -include("typecoercion.jl") -include("dataframe.jl") -include("base.jl") - -end # module EarthProps \ No newline at end of file diff --git a/src/earthprops/base.jl b/src/earthprops/base.jl deleted file mode 100644 index 6f8a165fd..000000000 --- a/src/earthprops/base.jl +++ /dev/null @@ -1,82 +0,0 @@ - -Base.eltype(::EarthModel{T}) where {T} = T -Base.eltype(::EarthLayer{T}) where {T} = T - -function Base.convert(::Type{EarthModel{T}}, model::EarthModel) where {T} - # If the model is already the target type, return it without modification. - model isa EarthModel{T} && return model - - # Delegate the actual conversion logic to the existing coerce_to_T function. - return coerce_to_T(model, T) -end - -function Base.convert(::Type{EarthLayer{T}}, layer::EarthLayer) where {T} - # Avoid unnecessary work if the layer is already the correct type. - layer isa EarthLayer{T} && return layer - - # Delegate the conversion logic to the specialized coerce_to_T function. - return coerce_to_T(layer, T) -end - -""" -$(TYPEDSIGNATURES) - -Defines the display representation of a [`EarthModel`](@ref) object for REPL or text output. - -# Arguments - -- `io`: The output stream to write the representation to \\[IO\\]. -- `mime`: The MIME type for plain text output \\[MIME"text/plain"\\]. -- `model`: The [`EarthModel`](@ref) instance to be displayed. - - -# Returns - -- Nothing. Modifies `io` to format the output. -""" -function Base.show(io::IO, ::MIME"text/plain", model::EarthModel) - # Determine model type based on num_layers and vertical_layers flag - num_layers = length(model.layers) - model_type = num_layers == 2 ? "homogeneous" : "multilayer" - orientation = model.vertical_layers ? "vertical" : "horizontal" - layer_word = (num_layers - 1) == 1 ? "layer" : "layers" - - # Count frequency samples from the first layer's property arrays - num_freq_samples = length(model.layers[1].rho_g) - freq_word = (num_freq_samples) == 1 ? "sample" : "samples" - - # Print header with key information - println( - io, - "EarthModel with $(num_layers-1) $(orientation) earth $(layer_word) ($(model_type)) and $(num_freq_samples) frequency $(freq_word)", - ) - - # Print layers in treeview style - for i in 1:num_layers - layer = model.layers[i] - # Determine prefix based on whether it's the last layer - prefix = i == num_layers ? "└─" : "├─" - - # Format thickness value - thickness_str = isinf(layer.t) ? "Inf" : "$(round(layer.t, sigdigits=4))" - - # Format layer name - layer_name = i == 1 ? "Layer $i (air)" : "Layer $i" - - # Print layer properties with proper formatting - println( - io, - "$prefix $layer_name: [rho_g=$(round(layer.base_rho_g, sigdigits=4)), " * - "epsr_g=$(round(layer.base_epsr_g, sigdigits=4)), " * - "mur_g=$(round(layer.base_mur_g, sigdigits=4)), " * - "t=$thickness_str]", - ) - end - - # Add formulation information as child nodes - if !isnothing(model.freq_dependence) - formulation_tag = get_description(model.freq_dependence) - println(io, " Frequency-dependent model: $(formulation_tag)") - end - -end \ No newline at end of file diff --git a/src/earthprops/dataframe.jl b/src/earthprops/dataframe.jl deleted file mode 100644 index 1b2e3a036..000000000 --- a/src/earthprops/dataframe.jl +++ /dev/null @@ -1,41 +0,0 @@ -import DataFrames: DataFrame - -""" -$(TYPEDSIGNATURES) - -Generates a `DataFrame` summarizing basic properties of earth layers from an [`EarthModel`](@ref). - -# Arguments - -- `earth_model`: Instance of [`EarthModel`](@ref) containing earth layers. - -# Returns - -- A `DataFrame` with columns: - - `rho_g`: Base (DC) resistivity of each layer \\[Ω·m\\]. - - `epsr_g`: Base (DC) relative permittivity of each layer \\[dimensionless\\]. - - `mur_g`: Base (DC) relative permeability of each layer \\[dimensionless\\]. - - `thickness`: Thickness of each layer \\[m\\]. - -# Examples - -```julia -df = $(FUNCTIONNAME)(earth_model) -println(df) -``` -""" -function DataFrame(earth_model::EarthModel) - layers = earth_model.layers - - base_rho_g = [layer.base_rho_g for layer in layers] - base_epsr_g = [layer.base_epsr_g for layer in layers] - base_mur_g = [layer.base_mur_g for layer in layers] - thickness = [layer.t for layer in layers] - - return DataFrame( - rho_g=base_rho_g, - epsr_g=base_epsr_g, - mur_g=base_mur_g, - thickness=thickness, - ) -end \ No newline at end of file diff --git a/src/earthprops/fdprops.jl b/src/earthprops/fdprops.jl deleted file mode 100644 index 61521303b..000000000 --- a/src/earthprops/fdprops.jl +++ /dev/null @@ -1,83 +0,0 @@ -""" -$(TYPEDEF) - -Abstract type representing different frequency-dependent earth models (FDEM). Used in the multi-dispatch implementations in modules [`LineCableModels.EarthProps`](@ref) and [`LineCableModels.Engine`](@ref). - -# Currently available formulations - -- [`CPEarth`](@ref): Constant properties (CP) model. -""" -abstract type AbstractFDEMFormulation end - -""" -$(TYPEDEF) - -Represents an earth model with constant properties (CP), i.e. frequency-invariant electromagnetic properties. -""" -struct CPEarth <: AbstractFDEMFormulation end -get_description(::CPEarth) = "Constant properties (CP)" - - -""" -$(TYPEDSIGNATURES) - -Functor implementation for `CPEarth`. - -Computes frequency-dependent earth properties using the [`CPEarth`](@ref) formulation, which assumes frequency-invariant values for resistivity, permittivity, and permeability. - -# Arguments - -- `frequencies`: Vector of frequency values \\[Hz\\]. -- `base_rho_g`: Base (DC) electrical resistivity of the soil \\[Ω·m\\]. -- `base_epsr_g`: Base (DC) relative permittivity of the soil \\[dimensionless\\]. -- `base_mur_g`: Base (DC) relative permeability of the soil \\[dimensionless\\]. -- `formulation`: Instance of a subtype of [`AbstractFDEMFormulation`](@ref) defining the computation method. - -# Returns - -- `rho`: Vector of resistivity values \\[Ω·m\\] at the given frequencies. -- `epsilon`: Vector of permittivity values \\[F/m\\] at the given frequencies. -- `mu`: Vector of permeability values \\[H/m\\] at the given frequencies. - -# Examples - -```julia -frequencies = [1e3, 1e4, 1e5] - -# Using the CP model -rho, epsilon, mu = $(FUNCTIONNAME)(frequencies, 100, 10, 1, CPEarth()) -println(rho) # Output: [100, 100, 100] -println(epsilon) # Output: [8.854e-11, 8.854e-11, 8.854e-11] -println(mu) # Output: [1.2566e-6, 1.2566e-6, 1.2566e-6] -``` - -# See also - -- [`EarthLayer`](@ref) -""" -function (f::CPEarth)(frequencies::Vector{T}, base_rho_g::T, base_epsr_g::T, - base_mur_g::T) where {T<:REALSCALAR} - - # Preallocate for performance - n_freq = length(frequencies) - rho = Vector{T}(undef, n_freq) - epsilon = Vector{typeof(ε₀ * base_epsr_g)}(undef, n_freq) - mu = Vector{typeof(μ₀ * base_mur_g)}(undef, n_freq) - - # Vectorized assignment - fill!(rho, base_rho_g) - fill!(epsilon, ε₀ * base_epsr_g) - fill!(mu, μ₀ * base_mur_g) - - return rho, epsilon, mu -end - -function (f::CPEarth)(frequencies::AbstractVector, base_rho_g, base_epsr_g, base_mur_g) - T = resolve_T(frequencies, base_rho_g, base_epsr_g, base_mur_g) - return f( - coerce_to_T(frequencies, T), - coerce_to_T(base_rho_g, T), - coerce_to_T(base_epsr_g, T), - coerce_to_T(base_mur_g, T), - ) -end diff --git a/src/earthprops/typecoercion.jl b/src/earthprops/typecoercion.jl deleted file mode 100644 index 3e54f3c5c..000000000 --- a/src/earthprops/typecoercion.jl +++ /dev/null @@ -1,85 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Converts an `EarthModel{S}` to `EarthModel{T}` by reconstructing the model with -all layers coerced to the new scalar type `T`. Layer conversion is delegated to -[`coerce_to_T(::EarthLayer, ::Type)`](@ref), and non-numeric metadata are -forwarded unchanged. - -# Arguments - -- `model`: Source Earth model \\[dimensionless\\]. -- `::Type{T}`: Target element type for numeric fields \\[dimensionless\\]. - -# Returns - -- `EarthModel{T}` rebuilt with each layer and numeric payload converted to `T`. - -# Examples - -```julia -m64 = $(FUNCTIONNAME)(model, Float64) -mM = $(FUNCTIONNAME)(model, Measurement{Float64}) -``` - -# See also - -- [`coerce_to_T`](@ref) -- [`resolve_T`](@ref) -""" -function coerce_to_T(model::EarthModel, ::Type{T}) where {T} - # 1. Coerce all existing layers recursively - new_layers = [coerce_to_T(layer, T) for layer in model.layers] - - # 2. Use the inner constructor to build the new, promoted model - return EarthModel{T}( - model.freq_dependence, - model.vertical_layers, - new_layers - ) -end - -""" -$(TYPEDSIGNATURES) - -Converts an `EarthLayer{S}` to `EarthLayer{T}` by coercing each stored field to -the target element type `T` and rebuilding the layer via its inner constructor. -Scalar and array fields are converted using the generic [`coerce_to_T`](@ref) -machinery. - -# Arguments - -- `layer`: Source Earth layer \\[dimensionless\\]. -- `::Type{T}`: Target element type for numeric fields \\[dimensionless\\]. - -# Returns - -- `EarthLayer{T}` with all numeric state converted to `T`. - -# Examples - -```julia -ℓ64 = $(FUNCTIONNAME)(layer, Float64) -ℓM = $(FUNCTIONNAME)(layer, Measurement{Float64}) -``` - -# See also - -- [`coerce_to_T`](@ref) -- [`resolve_T`](@ref) -""" -function coerce_to_T(layer::EarthLayer, ::Type{T}) where {T} - # Reconstruct the layer using the correct internal constructor. - # The existing coerce_to_T methods for scalars and arrays will be - # dispatched automatically for each field. - return EarthLayer{T}( - coerce_to_T(layer.base_rho_g, T), - coerce_to_T(layer.base_epsr_g, T), - coerce_to_T(layer.base_mur_g, T), - coerce_to_T(layer.t, T), - coerce_to_T(layer.rho_g, T), - coerce_to_T(layer.eps_g, T), - coerce_to_T(layer.mu_g, T) - ) -end - diff --git a/src/engine/Engine.jl b/src/engine/Engine.jl index 9931cf323..4d58775cc 100644 --- a/src/engine/Engine.jl +++ b/src/engine/Engine.jl @@ -1,62 +1,114 @@ """ - LineCableModels.Engine + LineCableModels.Engine -The [`Engine`](@ref) module provides the main functionalities of the [`LineCableModels.jl`](index.md) package. This module implements data structures, methods and functions for calculating frequency-dependent electrical parameters (Z/Y matrices) of line and cable systems with uncertainty quantification. +Calculate cable constants and frequency-dependent line-parameter matrices from +completed cable declarations and Engine-owned numerical blueprints. # Overview -- Calculation of frequency-dependent series impedance (Z) and shunt admittance (Y) matrices. -- Uncertainty propagation for geometric and material parameters using `Measurements.jl`. -- Internal impedance computation for solid, tubular and multi-layered coaxial conductors. -- Earth return impedances/admittances for overhead lines and underground cables (valid up to 10 MHz). -- Support for frequency-dependent soil properties. -- Handling of arbitrary polyphase systems with multiple conductors per phase. -- Phase and sequence domain calculations with uncertainty quantification. -- Novel N-layer concentric cable formulation with semiconductor modeling. +- Define scalar problems, formulations, and core results. +- Calculate conductor, insulation, and earth-return impedance and admittance. +- Assemble phase-domain series-impedance and shunt-admittance matrices. +- Apply bundle reduction, Kron elimination, and ideal transposition. +- Compare, tabulate, and describe plots of completed line-parameter results. # Dependencies $(IMPORTS) -# Exports - -$(EXPORTS) """ module Engine +import ..LineCableModels # Export public API -export LineParametersProblem, - LineParameters, SeriesImpedance, ShuntAdmittance, per_km, - per_m, kronify -export EMTFormulation, FormulationSet, LineParamOptions - -export compute!, plot +export LineParametersProblem, CableConstantsProblem, + LineParameters, CableConstants, SeriesImpedance, ShuntAdmittance, + RMSError, LineParametersBenchmark, compare, + absolute_error, relative_error, + Z, Y, R, X, L, G, B, C, + series_impedance, shunt_admittance, + resistance, reactance, inductance, + conductance, susceptance, capacitance, + frequencies, nconductors, nfrequencies, basis +export AbstractFormulation, LineParametersFormulation, CableConstantsFormulation, + Formulation +export LineCableModelsCoaxial, LineCableModelsFEM, + LineCableModelsFEMError, LineParametersWorkspace +export constitutive, formula_id, EarthPair +export verbosity +export InternalImpedance, InsulationImpedance, EarthImpedance, PipeImpedance +export InsulationAdmittance, SemiconAdmittance, EarthAdmittance +export ShuntModel, BoundarySolveError +export ModalAnalysis + +export compute # Module-specific dependencies -using Reexport, ForceImport -using Measurements -using LinearAlgebra -using ..Commons -import ..Commons: get_description, LineParamsDomain, PhaseDomain, ModalDomain, domain - -using ..Utils +using LinearAlgebra: diag +import LinearAlgebra: norm +using DocStringExtensions: IMPORTS, TYPEDEF, TYPEDFIELDS, TYPEDSIGNATURES +import ..LineCableModels: basis, line_length, build, R, L, C, + resistance, inductance, capacitance +import ..LineCableModels: nominal +import ..LineCableModels: constitutive, formula, formula_id, + Expression, FormulaDefinition +import ..LineCableModels: parameterize +import ..LineCableModels: verbosity, VerbosityLogger +#! explicit-imports: off +import ..LineCableModels: description +#! explicit-imports: on +import ..Commons: AbstractProblemDefinition, AbstractFormulation, + AbstractProblemResult, AbstractCoreResult, + FormulationOptions, ComputationOptions, + ComputationDetails, + formulation_options, computation_options, computation_details, details, + compute, observe, observables, + observation_indices, observation_resolution, + uncertainty, + request_identity, request_indices + +using ..Units +import ..Commons +using ..Commons: vacuum_permittivity, vacuum_permeability +using ..Commons: kron_reduce!, ReductionPlan, reduce_line_matrices! +import ..Commons: Functor +import ..Commons: initialize_buffers using ..Materials -using ..EarthProps: EarthModel -using ..DataModel: LineCableSystem -using ..Utils: levelfrom, TimestampLogger -using Logging, LoggingExtras - -include("types.jl") - -# Problem definitions -include("lineparamopts.jl") -include("problemdefs.jl") -include("lineparams.jl") +using ..Materials: TemperatureDependent +import ..Earth +using ..Earth: EarthMaterial, EarthModel, EquivalentHomogeneous +using ..DataModel: CableDesign, LineCableSystem, ncables, nphases +import ..DataModel +import ..TextDisplay +import ..LineCableModels: validate +import Logging +using Logging: with_logger +import SpecialFunctions +using QuadGK: alloc_segbuf, quadgk + +include("interfaces.jl") +include("formulations.jl") +include("earthplan.jl") +include("specialfunctions.jl") + +# Problem and coaxial formulation definitions +include("problems.jl") +include("options.jl") +include("integration.jl") +include("earthkernels.jl") + +# Line-parameter results and their protocols +include("lineparameters/lineparameters.jl") +include("lineparameters/quantities.jl") +include("lineparameters/resolution.jl") +include("lineparameters/benchmark.jl") # Submodule `InternalImpedance` include("internalimpedance/InternalImpedance.jl") using .InternalImpedance: InternalImpedance +include("pipeimpedance/PipeImpedance.jl") + # Submodule `InsulationImpedance` include("insulationimpedance/InsulationImpedance.jl") using .InsulationImpedance: InsulationImpedance @@ -69,41 +121,57 @@ using .EarthImpedance: EarthImpedance include("insulationadmittance/InsulationAdmittance.jl") using .InsulationAdmittance: InsulationAdmittance +# Submodule `SemiconAdmittance` +include("semiconadmittance/SemiconAdmittance.jl") +using .SemiconAdmittance: SemiconAdmittance + # Submodule `EarthAdmittance` include("earthadmittance/EarthAdmittance.jl") using .EarthAdmittance: EarthAdmittance -# Submodule `Transforms` -include("transforms/Transforms.jl") -using .Transforms - -# Submodule `EHEM` -include("ehem/EHEM.jl") -using .EHEM - -# Helpers -include("helpers.jl") - -# Workspace definition -include("workspace.jl") - -# Computation methods -include("solver.jl") -include("reduction.jl") -include("plot.jl") - -# Override I/O methods -include("base.jl") -include("dataframe.jl") - -# Submodule `FEM` -include("fem/FEM.jl") - -@reexport using .InternalImpedance: InternalImpedance -@reexport using .InsulationImpedance: InsulationImpedance -@reexport using .EarthImpedance: EarthImpedance -@reexport using .InsulationAdmittance: InsulationAdmittance -@reexport using .EarthAdmittance: EarthAdmittance -@reexport using .EHEM, .Transforms +# Native workspace and numerical action +include("blueprint.jl") +include("shuntmodel/ShuntModel.jl") +using .ShuntModel: BoundarySolveError +include("blueprint_shunt.jl") +include("input.jl") +include("earthreturn.jl") +include("impedance.jl") +include("admittance.jl") +include("lineparameters.jl") +include("cableconstants.jl") +include("observed_inputs.jl") +include("lineparameters/observations.jl") + +# Line-parameter protocols and observation publication +include("lineparameters/base.jl") +include("textdisplay.jl") + +# Submodule `ModalAnalysis` +include("modalanalysis/ModalAnalysis.jl") +using .ModalAnalysis: ModalAnalysis + +public completion_details, completed_inputs, completed_formulation, retain_gridpoint +public selectdetails +public AbstractModalOperators +public SpectralIntegral, integrate +public AirVoltageSpectrum, earth_spectral_term, earth_spectral_value, + earth_spectral_points!, earth_contour_angle, earth_direct, + outgoing_root, bessel_i0m1, bessel_current_ratio, special_besselix +public earth!, materials!, homogenize!, + same_physical_state, layer_index, computation_type +public has_uncertainty_type, numerical_magnitude +public resolution_available +public observation_assumptions +public domain, LineParamsDomain, PhaseDomain, ModalDomain, line_coordinates +public internal_shunt_response, blueprint_dependencies +public InternalImpedanceFormulation, InsulationImpedanceFormulation, + PipeImpedanceFormulation, + EarthImpedanceFormulation, InsulationAdmittanceFormulation, + SemiconAdmittanceFormulation, + EarthAdmittanceFormulation, ShuntModelFormulation +public layer_admittance +public CableBlueprint, BlueprintConductor, BlueprintDielectric, flatten, lineinput, + earth_pairs end # module Engine diff --git a/src/engine/admittance.jl b/src/engine/admittance.jl new file mode 100644 index 000000000..4ee947e02 --- /dev/null +++ b/src/engine/admittance.jl @@ -0,0 +1,285 @@ +""" +$(TYPEDSIGNATURES) + +Calculate the shunt admittance of one homogeneous coaxial annulus: + +```math +y=\\frac{2\\pi\\kappa}{\\log(r_{ex}/r_{in})}. +``` + +# Returns + +- Complex shunt admittance per unit length ``y`` \\[S/m\\]. +""" +@inline function layer_admittance(r_in::T, r_ex::T, κ::Complex{T}) where {T <: Real} + return 2 * (one(T) * π) * κ / log(r_ex / r_in) +end + +""" +$(TYPEDSIGNATURES) + +Calculate one homogeneous coaxial annulus's potential coefficient +``p=s/y`` from its registered constitutive relation's complex admittivity. A +zero inner radius or zero-thickness annulus contributes zero. + +# Returns + +- Complex charge-based potential coefficient ``p`` \\[m/F\\]. + +Here `r_in` and `r_ex` are the dielectric radii in meters, `κ` is complex +admittivity in S/m, and `s` is complex frequency in s⁻¹. For sinusoidal +evaluation, ``s=jω``. This definition gives ``y=s/p``. The FEM backend extracts +the inverse-admittance coefficient ``P=1/y`` in m/S and passes ``p=sP`` to +[`reduce_line_matrices!`](@ref LineCableModels.Commons.reduce_line_matrices!). +""" +@inline function potential_coefficient( + r_in::T, + r_ex::T, + κ::Complex{T}, + s::Complex{T} +) where {T <: Real} + if isapprox(r_in, zero(T); atol = eps(T)) || + isapprox(r_in, r_ex; atol = eps(T)) + return zero(Complex{T}) + end + y = layer_admittance(r_in, r_ex, κ) + return s / y +end + +""" +$(TYPEDSIGNATURES) + +Evaluate the selected dielectric law at frequency in Hz and temperature in °C. +The selected `temperature_dependence` law evaluates resistivity exactly once +before the dielectric equation. Its default is the linear resistivity law. +Use `nothing` to retain reference resistivity. The temporary material records the +operating temperature when resistivity changes. The source material remains unchanged. +The lossless dielectric law ignores resistivity and loss tangent. +Returns complex admittivity in S/m. +""" +function constitutive( + formula::Union{InsulationAdmittanceFormulation, SemiconAdmittanceFormulation}, + material::Material, frequency::Real, temperature::Real; + temperature_dependence = TemperatureDependent.Formula(:default), workspace = nothing) + rho = constitutive(temperature_dependence, material, temperature; workspace) + if rho != material.rho + material = Material(material.kind, rho, material.eps_r, material.mu_r, + temperature, material.alpha; rho_thermal = material.rho_thermal, + theta_max = material.theta_max, tan_delta = material.tan_delta, + sigma_solar = material.sigma_solar) + end + return formula(material, frequency, temperature; workspace) +end + +@inline function radial_coefficient(coefficients, layers::UnitRange{Int}) + coefficient = zero(eltype(coefficients)) + @inbounds for layer in layers + coefficient += coefficients[layer] + end + return coefficient +end + +""" +$(TYPEDSIGNATURES) + +Evaluate a homogeneous radial dielectric from its original constituents. +`relations` contains the selected insulation and semicon material callables, +in that order. Each callable returns complex admittivity in S/m at the supplied +frequency in Hz and temperature in °C. Each constituent loss is added only once. + +The effective admittivity is the logarithmically weighted harmonic mean of +the selected constituent responses. This is radial circuit equivalence, not +an assertion of full-field equivalence for an arbitrary heterogeneous domain. +""" +function constitutive(relations::Tuple{I, S}, material::RadialDielectric, + frequency::Real, temperature::Real) where {I, S} + T = promote_type(eltype(material), typeof(float(frequency)), typeof(float(temperature))) + impedance = zero(Complex{T}) + for (source, weight) in zip(material.materials, material.weights) + response = source.kind === :semicon ? + relations[2](source, frequency, temperature) : + relations[1](source, frequency, temperature) + iszero(response) && return zero(Complex{T}) + impedance += weight / response + end + return sum(material.weights) / impedance +end + +function constitutive(relations::Tuple{I, S}, material::RadialDielectric, + frequency::Real, temperature::Real; + temperature_dependence = TemperatureDependent.Formula(:default), workspace = nothing +) where {I <: InsulationAdmittanceFormulation, S <: SemiconAdmittanceFormulation} + evaluated = map(relations) do selected + (source, f, + t) -> constitutive(selected, source, f, t; temperature_dependence, workspace) + end + return constitutive(evaluated, material, frequency, temperature) +end + +""" +$(TYPEDSIGNATURES) + +Evaluate the registered insulation and semicon constitutive relations for +every physical dielectric layer, without applying radial geometry. + +# Returns + +- `admittivity`, overwritten in physical radial-layer order \\[S/m\\]. +""" +function dielectric!( + admittivity::AbstractVector{Complex{T}}, + input::LocalCableData{T}, + methods::NamedTuple, + frequency::T, + temperature::T; workspace = nothing +) where {T <: Real} + @inbounds for layer in input.insulation_indices + κ = constitutive( + methods.insulation_admittance, + input.dielectric_materials[layer], + frequency, + temperature; temperature_dependence = methods.temperature_dependence, workspace + ) + admittivity[layer] = κ + end + @inbounds for layer in input.semicon_indices + κ = constitutive( + methods.semicon_admittance, + input.dielectric_materials[layer], + frequency, + temperature; temperature_dependence = methods.temperature_dependence, workspace + ) + admittivity[layer] = κ + end + return admittivity +end + +""" +$(TYPEDSIGNATURES) + +Assemble the unreduced cable-internal potential-coefficient matrix from the +series sum of physical radial dielectric layers. + +# Returns + +- `destination`, overwritten with the local potential coefficients \\[m/F\\]. +""" +function cable_potential!( + destination::AbstractMatrix{Complex{T}}, + input::LocalCableData{T}, + admittivity::AbstractVector{Complex{T}}, + s::Complex{T}, + layer_coefficients::AbstractVector{Complex{T}}, + coefficients::AbstractVector{Complex{T}}, + tails::AbstractVector{Complex{T}} +) where {T <: Real} + fill!(destination, zero(Complex{T})) + @. layer_coefficients = potential_coefficient( + input.r_layer_in, input.r_layer_ext, admittivity, s) + @inbounds for conductors in input.assemblies + count = length(conductors) + for component in 1:count + coefficients[component] = input.shunt_covered[conductors[component]] ? zero(s) : + radial_coefficient( + layer_coefficients, + input.dielectric_ranges[conductors[component]] + ) + end + tails[count] = coefficients[count] + for gap in (count - 1):-1:1 + tails[gap] = coefficients[gap] + tails[gap + 1] + end + for row in 1:count, column in 1:count + + destination[conductors[row], conductors[column]] += tails[max(row, column)] + end + end + return _shunt_potential!(destination, input.shunt) +end + +""" +$(TYPEDSIGNATURES) + +Assemble the earth-free nodal shunt-admittance matrix of independent +concentric assemblies. + +For each conductor-owned radial interval, the physical layer coefficients +combine in series as ``p=\\sum_k p_k`` and contribute the branch admittance +``y=s/p``. An interval outside the last retained conductor terminates at the +external reference. + +# Returns + +- `destination`, overwritten with the local shunt admittance [S/m]. +""" +function cable_admittance!( + destination::AbstractMatrix{Complex{T}}, + input::LocalCableData{T}, + admittivity::AbstractVector{Complex{T}}, + s::Complex{T}, + layer_coefficients::AbstractVector{Complex{T}} +) where {T <: Real} + fill!(destination, zero(Complex{T})) + @. layer_coefficients = potential_coefficient( + input.r_layer_in, input.r_layer_ext, admittivity, s) + @inbounds for conductors in input.assemblies + count = length(conductors) + for position in 1:count + index = conductors[position] + input.shunt_covered[index] && continue + coefficient = radial_coefficient( + layer_coefficients, + input.dielectric_ranges[index] + ) + iszero(coefficient) && continue + admittance = s / coefficient + destination[index, index] += admittance + if position < count + outside = conductors[position + 1] + destination[outside, outside] += admittance + destination[index, outside] -= admittance + destination[outside, index] -= admittance + end + end + end + return _shunt_admittance!(destination, input.shunt, s) +end + +""" +$(TYPEDSIGNATURES) + +Add the already calculated exterior potential coefficients to the cable-local +primitive potential matrix \\[m/F\\] and retain its optional trace at `frequency`. +The caller then forms admittance from this matrix. No equation or +material law is evaluated here. Return the mutated `destination`. +""" +function admittance!( + destination::AbstractMatrix{Complex{T}}, + workspace::LineParametersWorkspace{T}, + frequency::Int +) where {T <: Real} + input = workspace.input + indices = workspace.plan.cable_indices + earth_matrix = workspace.buffers.Pearth + trace = workspace.trace + + @inbounds for cable in 1:input.n_cables + self = earth_matrix[cable, cable] + for row in indices[cable], column in indices[cable] + + destination[row, column] += self + end + end + @inbounds for left in 1:(input.n_cables - 1) + for right in (left + 1):input.n_cables + mutual = earth_matrix[left, right] + for row in indices[left], column in indices[right] + + destination[row, column] += mutual + destination[column, row] += earth_matrix[right, left] + end + end + end + _stash!(trace, :P, frequency, destination) + return destination +end diff --git a/src/engine/base.jl b/src/engine/base.jl deleted file mode 100644 index 59e4d5823..000000000 --- a/src/engine/base.jl +++ /dev/null @@ -1,394 +0,0 @@ - -Base.eltype(::LineParametersProblem{T}) where {T} = T -Base.eltype(::Type{LineParametersProblem{T}}) where {T} = T - -Base.eltype(::LineParameters{T}) where {T} = T -Base.eltype(::Type{LineParameters{T}}) where {T} = T - -Base.eltype(::EMTWorkspace{T}) where {T} = T -Base.eltype(::Type{EMTWorkspace{T}}) where {T} = T - -abstract type UnitLen end -struct PerMeter <: UnitLen end -struct PerKilometer <: UnitLen end -_len_scale(::PerMeter) = 1.0 -_len_scale(::PerKilometer) = 1_000.0 -_len_label(::PerMeter) = "m" -_len_label(::PerKilometer) = "km" - -abstract type DisplayMode end -struct AsZY <: DisplayMode end -struct AsRLCG <: DisplayMode end - -using Printf -using Measurements: value, uncertainty - -""" -ResultsView: pretty, non-mutating renderer with zero-clipping and units. - -- `mode = AsZY()` prints Z [Ω/len], Y [S/len]. -- `mode = AsRLCG()` prints R [Ω/len], L [mH/len], G [S/len], C [µF/len]; - frequency is taken from the `LineParameters.f` vector of the view. -- `tol` clips tiny magnitudes to 0.0 in display only (value & uncertainty). -""" -struct ResultsView{LP <: LineParameters, U <: UnitLen, M <: DisplayMode} - lp::LP - unit::U - mode::M - tol::Float64 -end - -# Builders -resultsview( - lp::LineParameters; - per::Symbol = :km, - mode::Symbol = :ZY, - tol::Real = sqrt(eps(Float64)), -) = ResultsView( - lp, - per === :km ? PerKilometer() : PerMeter(), - mode === :ZY ? AsZY() : AsRLCG(), - float(tol), -) - -# --- Scalar formatting with zero-clipping ------------------------------------- - -# Zero-clip helpers (display only) -_clip(x::Real, tol) = (abs(x) < tol ? 0.0 : x) - -_format_real(io, x::Real, tol) = @printf(io, "%.6g", _clip(x, tol)) - -_format_meas(io, m, tol) = begin - v = _clip(value(m), tol) - u = _clip(uncertainty(m), tol) - @printf(io, "%.6g±%.6g", v, u) -end - -_format_complex(io, z, tol) = begin - # z may be Complex{<:Real} or Complex{<:Measurement} - print(io, "") - if z.re isa Real - _format_real(io, real(z), tol) - else - _format_meas(io, real(z), tol) - end - print(io, "+") - if z.im isa Real - _format_real(io, imag(z), tol) - else - _format_meas(io, imag(z), tol) - end - print(io, "im") -end - -_format_any(io, x, tol) = - x isa Complex ? _format_complex(io, x, tol) : - x isa Measurements.Measurement ? _format_meas(io, x, tol) : - _format_real(io, x, tol) - -# String versions (for aligned, copy-pastable matrix literals) -_repr_real(x::Real, tol) = @sprintf("%.6g", _clip(x, tol)) -_repr_meas(m, tol) = begin - v = _clip(value(m), tol) - u = _clip(uncertainty(m), tol) - @sprintf("%.6g±%.6g", v, u) -end -function _repr_complex(z, tol) - if z.re isa Real - rs = _repr_real(real(z), tol) - else - rs = _repr_meas(real(z), tol) - end - if z.im isa Real - is = _repr_real(imag(z), tol) - else - is = _repr_meas(imag(z), tol) - end - return string(rs, "+", is, "im") -end -_repr_any(x, tol) = - x isa Complex ? _repr_complex(x, tol) : - x isa Measurements.Measurement ? _repr_meas(x, tol) : _repr_real(x, tol) - -# Detect if any element would be clipped by tolerance after mapping -_would_clip(x::Real, tol) = (x != 0 && abs(x) < tol) -_would_clip_meas(m, tol) = _would_clip(value(m), tol) || _would_clip(uncertainty(m), tol) -function _would_clip_complex(z, tol) - (z.re isa Real ? _would_clip(real(z), tol) : _would_clip_meas(real(z), tol)) || - (z.im isa Real ? _would_clip(imag(z), tol) : _would_clip_meas(imag(z), tol)) -end -_would_clip_any(x, tol) = - x isa Complex ? _would_clip_complex(x, tol) : - x isa Measurements.Measurement ? _would_clip_meas(x, tol) : _would_clip(x, tol) - -function _any_clipped(A::AbstractMatrix; tol::Float64, map::Function = identity) - n1, n2 = size(A, 1), size(A, 2) - @inbounds for i in 1:n1, j in 1:n2 - x = map(A[i, j]) - _would_clip_any(x, tol) && return true - end - return false -end - - - -# --- Show methods -------------------------------------------------------------- - -function _show_matrix(io::IO, A::AbstractArray; tol::Float64, map::Function = identity) - n1, n2 = size(A, 1), size(A, 2) - for i in 1:n1 - for j in 1:n2 - j > 1 && print(io, " ") - _format_any(io, map(A[i, j]), tol) - end - i < n1 && print(io, '\n') - end -end - -# Copy-pastable Julia matrix literal with column alignment -function _show_matrix_literal( - io::IO, - A::AbstractMatrix; - tol::Float64, - map::Function = identity, -) - n1, n2 = size(A, 1), size(A, 2) - # Build string table - S = [_repr_any(map(A[i, j]), tol) for i in 1:n1, j in 1:n2] - # Column widths - widths = [maximum(length(S[i, j]) for i in 1:n1) for j in 1:n2] - # Print rows - for i in 1:n1 - print(io, i == 1 ? "[" : " ") - for j in 1:n2 - s = S[i, j] - pad = widths[j] - length(s) - # right align - print(io, " "^pad, s) - if j < n2 - print(io, " ") - end - end - if i < n1 - print(io, ";\n") - else - print(io, "]") - end - end -end - -function Base.show(io::IO, ::MIME"text/plain", rv::ResultsView) - lp = rv.lp - unit = rv.unit - tol = rv.tol - scale = _len_scale(unit) - ulabel = _len_label(unit) - _, _, nf = size(lp.Z) - - # Determine if any value would be clipped across displayed content - any_clipped = false - if rv.mode isa AsZY - @inbounds for k in 1:nf - Zk = lp.Z.values[:, :, k] - Yk = lp.Y.values[:, :, k] - any_clipped |= _any_clipped(Zk; tol = tol, map = x -> scale * x) - any_clipped && break - any_clipped |= _any_clipped(Yk; tol = tol, map = x -> scale * x) - any_clipped && break - end - else - @inbounds for k in 1:nf - Zk = lp.Z.values[:, :, k] - Yk = lp.Y.values[:, :, k] - fk = lp.f[k] - ω = 2 * pi * float(fk) - any_clipped |= - _any_clipped(Zk; tol = tol, map = x -> scale * real(x)) || - _any_clipped(Zk; tol = tol, map = x -> (scale * 1e3 / ω) * imag(x)) || - _any_clipped(Yk; tol = tol, map = x -> scale * real(x)) || - _any_clipped(Yk; tol = tol, map = x -> (scale * 1e6 / ω) * imag(x)) - any_clipped && break - end - end - - # Styled header similar to DataFrame-like formatting - n, _, _ = size(lp.Z) - mode_label = rv.mode isa AsZY ? "ZY" : "RLCG" - tol_str = @sprintf("%.1e", tol) - header_plain = - @sprintf("%dx%dx%d LineParameters | mode = %s | units per %s | tol = %s%s", - n, n, nf, mode_label, ulabel, tol_str, any_clipped ? " (!)" : "") - printstyled(io, @sprintf("%dx%dx%d LineParameters", n, n, nf); bold = true) - print(io, " | mode = ") - printstyled(io, mode_label; bold = true, color = :cyan) - print(io, " | units per ", ulabel, " | tol = ", tol_str) - if any_clipped - print(io, " ") - printstyled(io, "(!)"; bold = true, color = :yellow) - end - print(io, "\n") - print(io, repeat("─", length(header_plain))) - print(io, "\n\n") - - @views for k in 1:nf - # Slice header with frequency of the slice - fk = lp.f[k] - print(io, "\n[:, :, ", k, "] @ f=") - print(io, @sprintf("%.6g", float(fk))) - print(io, " Hz\n") - Zk = lp.Z.values[:, :, k] - Yk = lp.Y.values[:, :, k] - - if rv.mode isa AsZY - println(io, "Z [Ω/", ulabel, "] =") - _show_matrix_literal(io, Zk; tol = tol, map = x -> scale * x) - - print(io, "\n\nY [S/", ulabel, "] =\n") - _show_matrix_literal(io, Yk; tol = tol, map = x -> scale * x) - else - # derive ω from frequency vector for this slice - ω = 2 * pi * float(fk) - - println(io, "R [Ω/", ulabel, "] =") - _show_matrix_literal(io, Zk; tol = tol, map = x -> scale * real(x)) - - print(io, "\n\nL [mH/", ulabel, "] =\n") - _show_matrix_literal(io, Zk; tol = tol, map = x -> (scale * 1e3 / ω) * imag(x)) - - print(io, "\n\nG [S/", ulabel, "] =\n") - _show_matrix_literal(io, Yk; tol = tol, map = x -> scale * real(x)) - - print(io, "\n\nC [µF/", ulabel, "] =\n") - _show_matrix_literal(io, Yk; tol = tol, map = x -> (scale * 1e6 / ω) * imag(x)) - end - - k < nf && print(io, "\n", "---"^10, "\n") - end -end - -function Base.show(io::IO, ::MIME"text/plain", Z::SeriesImpedance) - n, _, nf = size(Z.values) - header_plain = @sprintf("%dx%dx%d SeriesImpedance [Ω/m]", n, n, nf) - printstyled(io, header_plain; bold = true) - print(io, "\n") - print(io, repeat("─", length(header_plain))) - print(io, "\n") - @views _show_matrix(io, Z.values[:, :, 1]; tol = sqrt(eps(Float64))) - size(Z, 3) > 1 && print( - io, - "\n… (", - size(Z, 3) - 1, - " more slice", - size(Z, 3) - 1 == 1 ? "" : "s", - ")", - ) -end - -function Base.show(io::IO, ::MIME"text/plain", Y::ShuntAdmittance) - n, _, nf = size(Y.values) - header_plain = @sprintf("%dx%dx%d ShuntAdmittance [S/m]", n, n, nf) - printstyled(io, header_plain; bold = true) - print(io, "\n") - print(io, repeat("─", length(header_plain))) - print(io, "\n") - @views _show_matrix(io, Y.values[:, :, 1]; tol = sqrt(eps(Float64))) - size(Y, 3) > 1 && print( - io, - "\n… (", - size(Y, 3) - 1, - " more slice", - size(Y, 3) - 1 == 1 ? "" : "s", - ")", - ) -end - - - -# ---- SeriesImpedance array-ish interface ---- -Base.size(Z::SeriesImpedance) = size(Z.values) -Base.size(Z::SeriesImpedance, d::Int) = size(Z.values, d) -Base.axes(Z::SeriesImpedance) = axes(Z.values) -Base.ndims(::Type{SeriesImpedance{T}}) where {T} = 3 -Base.eltype(::Type{SeriesImpedance{T}}) where {T} = T -Base.getindex(Z::SeriesImpedance, I...) = @inbounds Z.values[I...] - -# ---- ShuntAdmittance array-ish interface ---- -Base.size(Y::ShuntAdmittance) = size(Y.values) -Base.size(Y::ShuntAdmittance, d::Int) = size(Y.values, d) -Base.axes(Y::ShuntAdmittance) = axes(Y.values) -Base.ndims(::Type{ShuntAdmittance{T}}) where {T} = 3 -Base.eltype(::Type{ShuntAdmittance{T}}) where {T} = T -Base.getindex(Y::ShuntAdmittance, I...) = @inbounds Y.values[I...] - -# --- Frequency-slice sugar ---------------------------------------------------- -@inline Base.getindex(lp::LineParameters, k::Integer) = LineParameters( - SeriesImpedance(@view lp.Z.values[:, :, k:k]), - ShuntAdmittance(@view lp.Y.values[:, :, k:k]), - lp.f[k:k], -) - -# --- One-argument k, derive ω from freq (or accept ω directly) --------------- -function per_km(lp::LineParameters, k::Integer = 1; - mode::Symbol = :ZY, - tol::Real = sqrt(eps(Float64))) - lpk = lp[k] - return resultsview(lpk; per = :km, mode = mode, tol = tol) -end - -function per_m(lp::LineParameters, k::Integer = 1; - mode::Symbol = :ZY, - tol::Real = sqrt(eps(Float64))) - lpk = lp[k] - return resultsview(lpk; per = :m, mode = mode, tol = tol) -end - -# Helper: detect uncertainties in element type -_has_uncertainty_type(::Type{Complex{S}}) where {S} = S <: Measurement -_has_uncertainty_type(::Type) = false - -# Terse summary (used inside collections) -function Base.show(io::IO, lp::LineParameters) - n, _, nf = size(lp.Z) - T = eltype(lp.Z) - print(io, "LineParameters{$(T)} ", n, "×", n, "×", nf, " [Z:Ω/m, Y:S/m]") - _has_uncertainty_type(T) && print(io, " (±)") -end - -function Base.show(io::IO, ::MIME"text/plain", lp::LineParameters) - n, _, nf = size(lp.Z) - T = eltype(lp.Z) - tol = sqrt(eps(Float64)) - scale = 1_000.0 # per km preview - ulabel = "km" - - # Styled header similar to ResultsView - header_plain = string( - n, "x", n, "x", nf, " LineParameters | eltype = ", T, - _has_uncertainty_type(T) ? " | uncertainties: yes" : "", - ) - - printstyled(io, string(n, "x", n, "x", nf, " LineParameters"); bold = true) - print(io, " | eltype = ", T) - _has_uncertainty_type(T) && print(io, " | uncertainties: yes") - print(io, "\n") - print(io, repeat("─", length(header_plain))) - print(io, "\n\n") - - # Preview: slice 1, per km, Z then Y - @views begin - Z1 = view(lp.Z.values,:,:,1) - Y1 = view(lp.Y.values,:,:,1) - - println(io, "Preview (slice 1/", nf, ") per ", ulabel) - println(io, "Z [Ω/", ulabel, "] =") - _show_matrix(io, Z1; tol = tol, map = x -> scale * x) - - print(io, "\n\nY [S/", ulabel, "] =\n") - _show_matrix(io, Y1; tol = tol, map = x -> scale * x) - end - - if nf > 1 - print(io, "\n\n… (", nf - 1, " more frequency slice", nf - 1 == 1 ? "" : "s", ")") - end -end - diff --git a/src/engine/blueprint.jl b/src/engine/blueprint.jl new file mode 100644 index 000000000..29c792acd --- /dev/null +++ b/src/engine/blueprint.jl @@ -0,0 +1,570 @@ +""" +$(TYPEDEF) + +Store one frequency-independent equivalent coaxial conductor row. + +$(TYPEDFIELDS) +""" +struct BlueprintConductor{T <: Real} + "Retained terminal name." + terminal::Symbol + "Concentric assembly containing the terminal." + assembly::Int + "Equivalent inner radius [m]." + r_in::T + "Equivalent outer radius [m]." + r_ex::T + "Physical conductor cross-section [m²]." + cross_section::T + "Number of explicitly represented wires." + num_wires::Int + "Equivalent helical turns per unit length [1/m]." + turns_per_length::T + "Equivalent resistance at the material reference temperature [Ω/m]." + resistance::T + "Equivalent temperature coefficient [1/°C]." + alpha::T + "Equivalent geometric-mean radius [m]." + gmr::T + "Assembly-local center in the design frame [m]." + position::Tuple{T, T} + "Artificial homogeneous conductor material." + material::Material{T} +end + +""" +$(TYPEDEF) + +Store one physical dielectric layer owned by a coaxial conductor interval. + +$(TYPEDFIELDS) +""" +struct BlueprintDielectric{T <: Real} + "Index of the conductor immediately inside this radial interval." + conductor::Int + "Layer inner radius [m]." + r_in::T + "Layer outer radius [m]." + r_ex::T + "Unmodified physical material." + material::Material{T} +end + +""" +$(TYPEDEF) + +Store one lossless terminal-capacitance block in its owner's conductor indices. +Blueprint indices are cable-local. Local assembly data remap them to the system. +`C` is shield-referenced capacitance \\[F/m\\]. `P` is its charge-potential +inverse \\[m/F\\]. + +$(TYPEDFIELDS) +""" +struct InternalShuntBlock{T <: Real} + "Conductor range of the containing concentric assembly." + assembly::UnitRange{Int} + "Domain terminals including the reference shield." + terminals::UnitRange{Int} + "Terminal capacitance \\[F/m\\]." + C::Matrix{T} + "Terminal charge-potential coefficients \\[m/F\\]." + P::Matrix{T} +end + +function Base.convert(::Type{InternalShuntBlock{T}}, block::InternalShuntBlock) where {T <: Real} + InternalShuntBlock{T}(block.assembly, block.terminals, block.C, block.P) +end +Base.convert(::Type{InternalShuntBlock{T}}, block::InternalShuntBlock{T}) where {T <: Real} = block + +""" +$(TYPEDEF) + +Store the frequency-independent, unreduced numerical description of one cable +design and its selected local shunt formulation. + +The blueprint is the result of computational flattening. It retains equivalent +conductor annuli and every physical dielectric layer. An explicitly selected +boundary shunt model supplies completed lossless terminal coefficients during +construction. Frequency-dependent constitutive evaluation, conductor temperature +correction, earth return and matrix reductions remain calculation work. + +$(TYPEDFIELDS) +""" +struct CableBlueprint{T <: Real} + "Source cable identifier." + cable_id::String + "Equivalent conductors in DataModel terminal order." + conductors::Vector{BlueprintConductor{T}} + "Physical dielectric layers in radial order." + dielectrics::Vector{BlueprintDielectric{T}} + "Dielectric-layer range owned by every conductor row." + dielectric_ranges::Vector{UnitRange{Int}} + "Contiguous conductor ranges for independent concentric assemblies." + assembly_ranges::Vector{UnitRange{Int}} + "Completed boundary shunt blocks. Empty for the annular model." + shunt::Vector{InternalShuntBlock{T}} + "Requested/effective local model, domain outcomes and numerical diagnostics." + shunt_details::NamedTuple + + function CableBlueprint{T}( + cable_id::String, + conductors::Vector{BlueprintConductor{T}}, + dielectrics::Vector{BlueprintDielectric{T}}, + dielectric_ranges::Vector{UnitRange{Int}}, + assembly_ranges::Vector{UnitRange{Int}}, + shunt::Vector{InternalShuntBlock{T}}, + shunt_details::NamedTuple + ) where {T <: Real} + return validate(new{T}( + cable_id, + conductors, + dielectrics, + dielectric_ranges, + assembly_ranges, + shunt, + shunt_details + )) + end +end + +Base.eltype(::CableBlueprint{T}) where {T} = T +Base.eltype(::Type{<:CableBlueprint{T}}) where {T} = T +Base.length(blueprint::CableBlueprint) = length(blueprint.conductors) + +function _assembly_ranges(components) + isempty(components) && throw(ArgumentError( + "a coaxial blueprint requires at least one retained terminal", + )) + starts = Int[1] + positions = [first(components).conductor.position] + @inbounds for index in 2:length(components) + position = components[index].conductor.position + if !DataModel.same_radial_position(position, last(positions)) + any(reference -> DataModel.same_radial_position(position, reference), positions) && + throw(ArgumentError( + "a concentric assembly cannot reappear after another assembly", + )) + push!(starts, index) + push!(positions, position) + end + end + return UnitRange{Int}[start:(index == length(starts) ? length(components) : + starts[index + 1] - 1) + for (index, start) in pairs(starts)] +end + +function validate(blueprint::CableBlueprint) + isempty(blueprint.cable_id) && throw(ArgumentError( + "CableBlueprint.cable_id cannot be empty" + )) + count = length(blueprint.conductors) + count > 0 || throw(ArgumentError( + "CableBlueprint.conductors must contain at least one conductor" + )) + length(blueprint.dielectric_ranges) == count || throw(DimensionMismatch( + "CableBlueprint.dielectric_ranges must contain one range per conductor; " * + "received $(length(blueprint.dielectric_ranges)) ranges for $count conductors", + )) + isempty(blueprint.assembly_ranges) && throw(ArgumentError( + "CableBlueprint.assembly_ranges must contain at least one assembly", + )) + collect(Iterators.flatten(blueprint.assembly_ranges)) == collect(1:count) || + throw(DimensionMismatch( + "CableBlueprint.assembly_ranges must partition conductor indices 1:$count in order", + )) + layer_count = length(blueprint.dielectrics) + collected_layers = collect(Iterators.flatten(blueprint.dielectric_ranges)) + collected_layers == collect(1:layer_count) || throw(DimensionMismatch( + "CableBlueprint.dielectric_ranges must partition dielectric indices " * + "1:$layer_count in order", + )) + @inbounds for (index, conductor) in pairs(blueprint.conductors) + isempty(String(conductor.terminal)) && throw(ArgumentError( + "CableBlueprint.conductors[$index].terminal cannot be empty" + )) + conductor.assembly in eachindex(blueprint.assembly_ranges) || throw(DomainError( + conductor.assembly, + "CableBlueprint.conductors[$index].assembly must index assembly_ranges" + )) + index in blueprint.assembly_ranges[conductor.assembly] || throw(DimensionMismatch( + "CableBlueprint.conductors[$index].assembly does not own conductor $index", + )) + isfinite(conductor.r_in) && conductor.r_in >= zero(conductor.r_in) || + throw(DomainError( + conductor.r_in, + "CableBlueprint.conductors[$index].r_in must be nonnegative and finite" + )) + isfinite(conductor.r_ex) && conductor.r_ex > conductor.r_in || + throw(DomainError( + conductor.r_ex, + "CableBlueprint.conductors[$index].r_ex must be finite and greater than r_in" + )) + isfinite(conductor.cross_section) && + conductor.cross_section > zero(conductor.cross_section) || throw(DomainError( + conductor.cross_section, + "CableBlueprint.conductors[$index].cross_section must be positive and finite" + )) + conductor.num_wires >= 0 || throw(DomainError( + conductor.num_wires, + "CableBlueprint.conductors[$index].num_wires must be nonnegative" + )) + isfinite(conductor.turns_per_length) || throw(DomainError( + conductor.turns_per_length, + "CableBlueprint.conductors[$index].turns_per_length must be finite" + )) + isfinite(conductor.resistance) && + conductor.resistance > zero(conductor.resistance) || throw(DomainError( + conductor.resistance, + "CableBlueprint.conductors[$index].resistance must be positive and finite" + )) + isfinite(conductor.alpha) || throw(DomainError( + conductor.alpha, + "CableBlueprint.conductors[$index].alpha must be finite" + )) + isfinite(conductor.gmr) && conductor.gmr > zero(conductor.gmr) || + throw(DomainError( + conductor.gmr, + "CableBlueprint.conductors[$index].gmr must be positive and finite" + )) + all(isfinite, conductor.position) || throw(DomainError( + conductor.position, + "CableBlueprint.conductors[$index].position must be finite" + )) + validate(conductor.material) + conductor.material.kind === :conductor || throw(ArgumentError( + "CableBlueprint.conductors[$index].material.kind must be :conductor; " * + "received $(repr(conductor.material.kind))", + )) + for layer_index in blueprint.dielectric_ranges[index] + layer = blueprint.dielectrics[layer_index] + layer.conductor == index || throw(DimensionMismatch( + "CableBlueprint.dielectrics[$layer_index].conductor must be $index; " * + "received $(layer.conductor)", + )) + isfinite(layer.r_in) && layer.r_in >= zero(layer.r_in) || + throw(DomainError( + layer.r_in, + "CableBlueprint.dielectrics[$layer_index].r_in must be nonnegative and finite" + )) + isfinite(layer.r_ex) && layer.r_ex > layer.r_in || + throw(DomainError( + layer.r_ex, + "CableBlueprint.dielectrics[$layer_index].r_ex must be finite and greater than r_in" + )) + validate(layer.material) + layer.material.kind in (:insulator, :semicon) || throw(ArgumentError( + "CableBlueprint.dielectrics[$layer_index].material.kind must be " * + ":insulator or :semicon; received $(repr(layer.material.kind))", + )) + end + end + for block in blueprint.shunt + block.assembly in blueprint.assembly_ranges || throw(ArgumentError( + "blueprint shunt block must belong to a conductor assembly")) + first(block.terminals) in block.assembly && + last(block.terminals) in block.assembly || + throw(ArgumentError("blueprint shunt terminals must lie in their assembly")) + n = length(block.terminals) - 1 + size(block.C) == size(block.P) == (n, n) || throw(DimensionMismatch( + "blueprint shunt coefficients must match the nonreference terminals")) + end + return blueprint +end + +""" +$(TYPEDSIGNATURES) + +Flatten one completed cable design into the frequency-independent numerical +description consumed by the coaxial backend. + +# Arguments + +- `engine`: coaxial backend identity. +- `design`: completed physical cable design. +- `T`: scalar type used by the numerical payload. +- `formulation`: selected local formulas. Defaults to [`Formulation`](@ref). + +# Returns + +- A validated [`CableBlueprint`](@ref) with conductor annuli, physical + dielectric layers, assembly partitions and any selected boundary coefficients. +""" +function flatten( + engine::LineCableModelsCoaxial, + design::CableDesign, + ::Type{T}, + formulation::AbstractFormulation = Formulation() +) where {T <: Real} + return only(only(flatten(engine, [design], T, [formulation]))) +end + +function flatten( + ::LineCableModelsCoaxial, + design::CableDesign, + ::Type{T}, + methods::NamedTuple, + solutions::Vector, + design_index::Int +) where {T <: Real} + components = DataModel.radial_components(design, T) + ranges = _assembly_ranges(components) + assembly_by_conductor = Vector{Int}(undef, length(components)) + @inbounds for (assembly, indices) in pairs(ranges), index in indices + + assembly_by_conductor[index] = assembly + end + + conductors = Vector{BlueprintConductor{T}}(undef, length(components)) + layer_count = sum(component -> length(component.dielectric.layers), components) + dielectrics = Vector{BlueprintDielectric{T}}(undef, layer_count) + dielectric_ranges = Vector{UnitRange{Int}}(undef, length(components)) + layer_index = 0 + @inbounds for (index, component) in pairs(components) + conductor = component.conductor + conductors[index] = BlueprintConductor{T}( + component.name, + assembly_by_conductor[index], + conductor.r_in, + conductor.r_ex, + conductor.cross_section, + conductor.num_wires, + conductor.turns_per_length, + conductor.resistance, + conductor.alpha, + conductor.gmr, + conductor.position, + conductor.material + ) + first_layer = layer_index + 1 + for layer in component.dielectric.layers + layer_index += 1 + dielectrics[layer_index] = BlueprintDielectric{T}( + index, + layer.r_in, + layer.r_ex, + layer.material + ) + end + dielectric_ranges[index] = first_layer:layer_index + end + geometry = (; conductors, assembly_ranges = ranges) + response = internal_shunt_response(methods.shunt_model, design, geometry, T, + methods, solutions, design_index) + return CableBlueprint{T}( + design.cable_id, + conductors, + dielectrics, + dielectric_ranges, + ranges, + response.blocks, + response.details + ) +end + +function flatten( + engine::LineCableModelsCoaxial, + design::CableDesign, + formulation::AbstractFormulation = Formulation() +) + T = eltype(design) + return flatten(engine, design, T, formulation) +end + +""" +$(TYPEDEF) + +Store the concrete array representation shared by the coaxial local primitive +impedance and potential-coefficient assemblers. + +$(TYPEDFIELDS) +""" +struct LocalCableData{T <: Real} + "Terminal names in DataModel order." + terminals::Vector{Symbol} + "Contiguous conductor-index ranges for concentric assemblies." + assemblies::Vector{UnitRange{Int}} + "Source-design index for each concentric assembly." + assembly_designs::Vector{Int} + "Assembly-local conductor centers [m]." + positions::Vector{Tuple{T, T}} + "Equivalent conductor inner radii [m]." + r_in::Vector{T} + "Equivalent conductor outer radii [m]." + r_ext::Vector{T} + "Inner radii of conductor-owned dielectric intervals [m]." + r_ins_in::Vector{T} + "Outer radii of conductor-owned dielectric intervals [m]." + r_ins_ext::Vector{T} + "Reference conductor materials, including their resistivity calibration." + conductor_materials::Vector{Material{T}} + "Conductor relative permeabilities." + mu_r_cond::Vector{T} + "Equivalent relative permeabilities of conductor-owned dielectric intervals." + mu_r_ins::Vector{T} + "Physical dielectric-layer range owned by each conductor." + dielectric_ranges::Vector{UnitRange{Int}} + "Physical dielectric-layer inner radii [m]." + r_layer_in::Vector{T} + "Physical dielectric-layer outer radii [m]." + r_layer_ext::Vector{T} + "Physical dielectric materials in radial order." + dielectric_materials::Vector{Material{T}} + "Indices of dielectric layers classified as insulation." + insulation_indices::Vector{Int} + "Indices of dielectric layers classified as semiconducting material." + semicon_indices::Vector{Int} + "Boundary coefficients remapped from cable-local to system conductor indices." + shunt::Vector{InternalShuntBlock{T}} + "Radial intervals replaced by boundary coefficients." + shunt_covered::BitVector + "Local-model outcomes in system terminal order." + shunt_details::NamedTuple +end + +# Completed blueprint coefficients do not depend on external formula arguments. +# Widen their representation without recalculating the blueprint or detaching +# correlated material values. Native field conversion retains unchanged indices. +function Base.convert(::Type{LocalCableData{T}}, cable::LocalCableData) where {T <: Real} + LocalCableData{T}(map(name -> getfield(cable, name), fieldnames(typeof(cable)))...) +end +Base.convert(::Type{LocalCableData{T}}, cable::LocalCableData{T}) where {T <: Real} = cable + +function LocalCableData(blueprints::AbstractVector{<:CableBlueprint{T}}) where {T <: Real} + isempty(blueprints) && throw(ArgumentError( + "local cable data require at least one blueprint", + )) + conductor_count = sum(length, blueprints) + layer_count = sum(blueprint -> length(blueprint.dielectrics), blueprints) + assembly_count = sum(blueprint -> length(blueprint.assembly_ranges), blueprints) + + terminals = Vector{Symbol}(undef, conductor_count) + assemblies = Vector{UnitRange{Int}}(undef, assembly_count) + assembly_designs = Vector{Int}(undef, assembly_count) + positions = Vector{Tuple{T, T}}(undef, conductor_count) + r_in_values = Vector{T}(undef, conductor_count) + r_ext_values = Vector{T}(undef, conductor_count) + r_ins_in = Vector{T}(undef, conductor_count) + r_ins_ext = Vector{T}(undef, conductor_count) + conductor_materials = Vector{Material{T}}(undef, conductor_count) + mu_r_cond = Vector{T}(undef, conductor_count) + mu_r_ins = Vector{T}(undef, conductor_count) + dielectric_ranges = Vector{UnitRange{Int}}(undef, conductor_count) + r_layer_in = Vector{T}(undef, layer_count) + r_layer_ext = Vector{T}(undef, layer_count) + dielectric_materials = Vector{Material{T}}(undef, layer_count) + insulation_indices = Int[] + semicon_indices = Int[] + sizehint!(insulation_indices, layer_count) + sizehint!(semicon_indices, layer_count) + shunt = InternalShuntBlock{T}[] + shunt_covered = falses(conductor_count) + reports = similar(first(blueprints).shunt_details.domains, 0) + diagnostics = similar(first(blueprints).shunt_details.diagnostics, 0) + solved = Base.IdSet{Matrix{T}}() + requested = first(blueprints).shunt_details.requested + + conductor_offset = 0 + layer_offset = 0 + assembly_offset = 0 + @inbounds for (design_index, blueprint) in pairs(blueprints) + blueprint.shunt_details.requested === requested || throw(ArgumentError( + "local cable blueprints must use the same shunt model")) + for block in blueprint.shunt + assembly_range = (first(block.assembly) + conductor_offset):(last(block.assembly) + conductor_offset) + terminal_range = (first(block.terminals) + conductor_offset):(last(block.terminals) + conductor_offset) + push!(shunt, InternalShuntBlock(assembly_range, terminal_range, block.C, block.P)) + shunt_covered[first(terminal_range):(last(terminal_range) - 1)] .= true + push!(solved, block.C) + end + append!(diagnostics, blueprint.shunt_details.diagnostics) + for report in blueprint.shunt_details.domains + terminal_range = (first(report.terminals) + conductor_offset):(last(report.terminals) + conductor_offset) + push!(reports, merge(report, (; + design = design_index, terminals = terminal_range))) + end + for local_range in blueprint.assembly_ranges + assembly_offset += 1 + assemblies[assembly_offset] = ( + first(local_range) + conductor_offset + ):(last(local_range) + conductor_offset) + assembly_designs[assembly_offset] = design_index + end + for local_index in eachindex(blueprint.conductors) + index = conductor_offset + local_index + conductor = blueprint.conductors[local_index] + terminals[index] = conductor.terminal + positions[index] = conductor.position + r_in_values[index] = conductor.r_in + r_ext_values[index] = conductor.r_ex + conductor_materials[index] = conductor.material + mu_r_cond[index] = conductor.material.mu_r + + local_layers = blueprint.dielectric_ranges[local_index] + first_layer = layer_offset + 1 + for local_layer in local_layers + layer_offset += 1 + layer = blueprint.dielectrics[local_layer] + r_layer_in[layer_offset] = layer.r_in + r_layer_ext[layer_offset] = layer.r_ex + dielectric_materials[layer_offset] = layer.material + if layer.material.kind === :insulator + push!(insulation_indices, layer_offset) + elseif layer.material.kind === :semicon + push!(semicon_indices, layer_offset) + else + throw(ArgumentError( + "unsupported coaxial dielectric kind :$(layer.material.kind)", + )) + end + end + dielectric_ranges[index] = first_layer:layer_offset + if isempty(local_layers) + r_ins_in[index] = conductor.r_ex + r_ins_ext[index] = conductor.r_ex + mu_r_ins[index] = one(T) + else + r_ins_in[index] = blueprint.dielectrics[first(local_layers)].r_in + r_ins_ext[index] = blueprint.dielectrics[last(local_layers)].r_ex + layers = @view blueprint.dielectrics[local_layers] + mu_r_ins[index] = DataModel.equivalent_dielectric_permeability( + layers, + conductor.turns_per_length, + conductor.r_ex, + r_ins_ext[index] + ) + end + end + conductor_offset += length(blueprint.conductors) + end + + return LocalCableData{T}( + terminals, + assemblies, + assembly_designs, + positions, + r_in_values, + r_ext_values, + r_ins_in, + r_ins_ext, + conductor_materials, + mu_r_cond, + mu_r_ins, + dielectric_ranges, + r_layer_in, + r_layer_ext, + dielectric_materials, + insulation_indices, + semicon_indices, + shunt, + shunt_covered, + (requested, + effective = isempty(reports) ? :equivalent : + all(r -> r.effective === :boundary, reports) ? :boundary : + all(r -> r.effective === :equivalent, reports) ? :equivalent : :mixed, + solves = length(solved), domains = reports, diagnostics) + ) +end + +function LocalCableData(blueprint::CableBlueprint{T}) where {T <: Real} + LocalCableData(CableBlueprint{T}[blueprint]) +end diff --git a/src/engine/blueprint_shunt.jl b/src/engine/blueprint_shunt.jl new file mode 100644 index 000000000..47c89ce70 --- /dev/null +++ b/src/engine/blueprint_shunt.jl @@ -0,0 +1,57 @@ +""" +$(TYPEDSIGNATURES) + +Construct cable blueprints for each selected formulation. Identical local +selections share the completed blueprints. Equivalent lossless domains share +their coefficient matrices. +The returned outer vector follows formulation order, and each inner vector +follows design order. Sharing is confined to this construction call. +""" +function flatten(engine::LineCableModelsCoaxial, designs::AbstractVector, + ::Type{T}, formulations::AbstractVector{<:AbstractFormulation}) where {T <: Real} + solutions = NamedTuple[] + selections = NamedTuple[] + blueprints = Vector{CableBlueprint{T}}[] + for formulation in formulations + methods = formulation.methods + selected = blueprint_dependencies(methods.shunt_model, methods) + previous = findfirst(value -> isequal(value, selected), selections) + current = previous === nothing ? + CableBlueprint{T}[flatten(engine, design, T, selected, solutions, index) + for (index, design) in pairs(designs)] : + blueprints[previous] + push!(selections, selected) + push!(blueprints, current) + end + return blueprints +end + +# Install completed terminal coefficients. No boundary equation is evaluated. +function _shunt_potential!(destination, blocks::AbstractVector{<:InternalShuntBlock}) + for block in blocks + inner, reference = first(block.terminals), last(block.terminals) + # Charge on the inner anchor includes all conductors shielded inside it. + @inbounds for j in first(block.assembly):(reference - 1), + i in first(block.assembly):(reference - 1) + + destination[i, j] += block.P[max(1, i-inner+1), max(1, j-inner+1)] + end + end + return destination +end + +function _shunt_admittance!(destination, blocks::AbstractVector{<:InternalShuntBlock}, s) + for block in blocks + first_index, reference = first(block.terminals), last(block.terminals) + @inbounds for j in axes(block.C, 2), i in axes(block.C, 1) + + value = s*block.C[i, j] + row, column = first_index+i-1, first_index+j-1 + destination[row, column] += value + destination[row, reference] -= value + destination[reference, column] -= value + destination[reference, reference] += value + end + end + return destination +end diff --git a/src/engine/cableconstants.jl b/src/engine/cableconstants.jl new file mode 100644 index 000000000..596a06759 --- /dev/null +++ b/src/engine/cableconstants.jl @@ -0,0 +1,660 @@ +""" +$(TYPEDEF) + +Store earth-free cable constants per unit length for one or more independent +concentric assemblies. + +Every entry of `cores`, `R`, `L`, `C`, and `G` describes one assembly. Values +are evaluated at `frequency`. `R`, `L`, `C`, and `G` use Ω/m, H/m, F/m, and +S/m respectively. + +$(TYPEDFIELDS) +""" +struct CableConstants{T <: Real, D <: ComputationDetails} <: AbstractCoreResult + "Innermost active terminal of each concentric assembly." + cores::Vector{Symbol} + "Series resistance per unit length [Ω/m]." + R::Vector{T} + "Series inductance per unit length [H/m]." + L::Vector{T} + "Shunt capacitance per unit length [F/m]." + C::Vector{T} + "Shunt conductance per unit length [S/m]." + G::Vector{T} + "Evaluation frequency [Hz]." + frequency::T + + "Requested/resolved formulas and blueprint construction diagnostics." + details::D + + function CableConstants{T}( + cores::Vector{Symbol}, + R::Vector{T}, + L::Vector{T}, + C::Vector{T}, + G::Vector{T}, + frequency::T, + details::ComputationDetails = ComputationDetails() + ) where {T <: Real} + count = length(cores) + iszero(count) && throw(ArgumentError( + "cable constants require at least one concentric assembly", + )) + all(length(values) == count for values in (R, L, C, G)) || + throw(DimensionMismatch( + "cores, R, L, C, and G must contain the same number of entries", + )) + allunique(cores) || throw(ArgumentError( + "cable-constant core names must be unique", + )) + isfinite(frequency) && frequency > zero(frequency) || throw(DomainError( + frequency, + "cable-constant frequency must be positive and finite" + )) + all(isfinite, Iterators.flatten((R, L, C, G))) || throw(DomainError( + (R, L, C, G), + "cable constants must be finite" + )) + retained=haskey(details.data,:gridpoint) ? details : + completion_details(merge(details.data,(gridpoint=Commons.gridpoint_id(),))) + return new{T, typeof(retained)}(cores, R, L, C, G, frequency, retained) + end +end + +function CableConstants( + cores::AbstractVector{Symbol}, + R::AbstractVector{<:Real}, + L::AbstractVector{<:Real}, + C::AbstractVector{<:Real}, + G::AbstractVector{<:Real}, + frequency::Real, + details::ComputationDetails = ComputationDetails() +) + T = promote_type( + eltype(R), eltype(L), eltype(C), eltype(G), typeof(float(frequency)) + ) + return CableConstants{T}( + collect(Symbol, cores), + T.(R), + T.(L), + T.(C), + T.(G), + convert(T, float(frequency)), + details + ) +end + +function CableConstants( + R::Real, + L::Real, + C::Real, + G::Real = 0; + core::Symbol = :core, + frequency::Real = 50 +) + values = promote(R, L, C, G, frequency) + T = typeof(first(values)) + return CableConstants{T}( + Symbol[core], + T[values[1]], + T[values[2]], + T[values[3]], + T[values[4]], + values[5] + ) +end + +function Base.:(==)(left::CableConstants, right::CableConstants) + left.cores == right.cores && left.R == right.R && left.L == right.L && + left.C == right.C && left.G == right.G && left.frequency == right.frequency +end + +Base.length(constants::CableConstants) = length(constants.cores) +details(constants::CableConstants) = constants.details +Base.size(constants::CableConstants) = (length(constants),) +function Base.eltype(::Type{CableConstants{T}}) where {T} + NamedTuple{ + (:core, :R, :L, :C, :G), + Tuple{Symbol, T, T, T, T} + } +end +Base.firstindex(constants::CableConstants) = firstindex(constants.cores) +Base.lastindex(constants::CableConstants) = lastindex(constants.cores) + +function Base.getindex(constants::CableConstants, index::Integer) + return ( + core = constants.cores[index], + R = constants.R[index], + L = constants.L[index], + C = constants.C[index], + G = constants.G[index] + ) +end + +function Base.iterate(constants::CableConstants, state::Int = 1) + state > length(constants) && return nothing + return constants[state], state + 1 +end + +observe(constants::CableConstants, ::typeof(R)) = constants.R +observe(constants::CableConstants, ::typeof(L)) = constants.L +observe(constants::CableConstants, ::typeof(C)) = constants.C +observe(constants::CableConstants, ::typeof(G)) = constants.G + +R(constants::CableConstants) = observe(constants, R) +L(constants::CableConstants) = observe(constants, L) +C(constants::CableConstants) = observe(constants, C) +G(constants::CableConstants) = observe(constants, G) +basis(::CableConstants) = :pul +resistance(constants::CableConstants) = observe(constants, R) +inductance(constants::CableConstants) = observe(constants, L) +capacitance(constants::CableConstants) = observe(constants, C) +conductance(constants::CableConstants) = observe(constants, G) +observables(::Type{<:CableConstants}) = (R, L, C, G) + + +""" +$(TYPEDEF) + +Define one earth-free cable-constant computation for a completed cable design. + +The innermost terminal of every concentric assembly is active and every +additional outward terminal, when present, is grounded. The computation is +restricted to the 50 Hz or 60 Hz base frequency used by cable datasheets. + +$(TYPEDFIELDS) +""" +struct CableConstantsProblem{ + T <: Real, + D <: CableDesign +} <: AbstractProblemDefinition + "Completed physical cable design." + design::D + "Operating temperature [°C]." + temperature::T + "Evaluation frequency [Hz]." + frequency::T + + function CableConstantsProblem{T, D}( + design::D, + temperature::T, + frequency::T + ) where {T <: Real, D <: CableDesign} + return validate(new{T, D}(design, temperature, frequency)) + end +end + +Base.eltype(::CableConstantsProblem{T}) where {T} = T +Base.eltype(::Type{<:CableConstantsProblem{T}}) where {T} = T + +function validate(problem::CableConstantsProblem) + validate(problem.design) + isfinite(problem.temperature) || throw(DomainError( + problem.temperature, + "cable-constant temperature must be finite" + )) + problem.frequency in (oftype(problem.frequency, 50), oftype(problem.frequency, 60)) || + throw(DomainError( + problem.frequency, + "cable-constant base frequency must be 50 Hz or 60 Hz" + )) + return problem +end + +""" +$(TYPEDSIGNATURES) + +Construct an earth-free cable-constant problem. + +# Keywords + +- `temperature=20`: operating temperature [°C]. +- `frequency=50`: base frequency, either 50 Hz or 60 Hz. + +# Returns + +- A validated [`CableConstantsProblem`](@ref). +""" +function CableConstantsProblem( + design::CableDesign; + temperature::Real = 20, + frequency::Real = 50 +) + T = promote_type( + eltype(design), typeof(float(temperature)), typeof(float(frequency)) + ) + value = convert(T, float(frequency)) + return CableConstantsProblem{T, typeof(design)}( + design, + convert(T, float(temperature)), + value + ) +end + +""" +$(TYPEDEF) + +Select the conductor and dielectric formulas used by a cable-constant +computation. + +$(TYPEDFIELDS) +""" +struct CableConstantsFormulation{ + M <: NamedTuple, + O <: FormulationOptions, + D <: NamedTuple +} <: AbstractFormulation + "Registered physical formula selections." + methods::M + "Cable-constant formulation options." + options::O + "Requested declarations retained before formula-owner resolution." + definitions::D +end + +function formulation_options( + ::Type{CableConstantsFormulation}, + options::FormulationOptions +)::FormulationOptions + isempty(options.data) || throw(ArgumentError( + "unknown cable-constant formulation options: $(keys(options.data))")) + return FormulationOptions() +end + +description(::Type{<:CableConstantsFormulation}; compact::Bool = false) = "Cable constants" +function description(::CableConstantsFormulation; compact::Bool = false) + description(CableConstantsFormulation; compact) +end +formula_id(::Type{<:CableConstantsFormulation}) = :cable_constants +formula_id(::CableConstantsFormulation) = :cable_constants +formulation_options(value::CableConstantsFormulation) = value.options +function Base.pairs(::Type{CableConstantsFormulation}; quantity = nothing) + return pairs((; + (key=>family + for (key, family) in pairs(LineParametersFormulation; quantity) + if key ∉ (:earth_impedance, :earth_admittance, :earth_properties))...)) +end +function description(::Type{CableConstantsFormulation}, slot::Val; compact::Bool=false) + description(LineParametersFormulation, slot; compact) +end +function Base.pairs(value::CableConstantsFormulation; quantity = nothing) + pairs(CableConstantsFormulation, + (methods = value.methods, requested = value.definitions, + options = value.options.data); quantity) +end +function Base.pairs(::Type{CableConstantsFormulation}, retained::NamedTuple; quantity = nothing) + pairs(LineParametersFormulation, retained; quantity, owner = CableConstantsFormulation) +end + +"""Expose requested and resolved local formulas for passive scientific records.""" +function Base.NamedTuple(value::CableConstantsFormulation) + record = function (selected) + selected === nothing && return nothing + selected isa Symbol && return NamedTuple(formula(selected)) + selected isa NamedTuple && return map(record, selected) + return NamedTuple(selected) + end + Record = NamedTuple{(:backend, :requested, :methods, :options), + Tuple{Symbol, NamedTuple, NamedTuple, NamedTuple}} + return Record((:cable_constants, map(record, value.definitions), + map(record, value.methods), value.options.data)) +end + +function _constants_formulation( + internal_impedance, + insulation_impedance, + shunt_model, + insulation_admittance, + semicon_admittance, + pipe_impedance, + temperature_dependence, + options::FormulationOptions +) + methods = ( + internal_impedance = InternalImpedance.Formula(internal_impedance), + insulation_impedance = InsulationImpedance.Formula(insulation_impedance), + shunt_model = ShuntModel.Formula(shunt_model), + insulation_admittance = InsulationAdmittance.Formula(insulation_admittance), + semicon_admittance = SemiconAdmittance.Formula(semicon_admittance), + pipe_impedance = PipeImpedance.Formula(pipe_impedance), + temperature_dependence = temperature_dependence === nothing ? nothing : + TemperatureDependent.Formula(temperature_dependence) + ) + return CableConstantsFormulation( + methods, + formulation_options(CableConstantsFormulation, options), + (; internal_impedance, insulation_impedance, shunt_model, insulation_admittance, + semicon_admittance, pipe_impedance, temperature_dependence) + ) +end + +""" +$(TYPEDSIGNATURES) + +Construct the cable-constant formula bundle. + +Each formula slot and the complete `options` tuple accepts a scalar selection +or an explicit [`Grid`](@ref LineCableModels.ParametricBuilder.Grid)/ +[`Gridspace`](@ref LineCableModels.ParametricBuilder.Gridspace) source. Scalar +inputs return one [`CableConstantsFormulation`](@ref). Varying inputs return a +`Gridspace{CableConstantsFormulation}` of completed formulations. + +# Keywords + +- `internal_impedance`: conductor surface-impedance recipe. +- `insulation_impedance`: longitudinal insulation-impedance recipe. +- `shunt_model`: local geometry model. `:default`/`:equivalent` uses the equivalent + annular layer, `:boundary` explicitly computes lossless open-screen coupling. +- `insulation_admittance`: insulation constitutive relation. +- `semicon_admittance`: semiconducting-layer constitutive relation. +- `pipe_impedance`: pipe-type selection. The coaxial pipe implementation is not + yet available. Ordinary concentric assemblies have no additional pipe term. +- `temperature_dependence`: resistivity law. `:default` is linear and `nothing` + retains reference resistivity. +- `options`: formulation controls. Currently empty. +- `combine`: `:product` or `:zip` composition among varying fields. +""" +function CableConstantsFormulation(; + internal_impedance = formula(:default), + insulation_impedance = formula(:default), + shunt_model = formula(:default), + insulation_admittance = formula(:default), + semicon_admittance = formula(:default), + pipe_impedance = formula(:default), + temperature_dependence = formula(:default), + options = FormulationOptions(), + combine::Symbol = :product +) + values = ( + internal_impedance, + insulation_impedance, + shunt_model, + insulation_admittance, + semicon_admittance, + pipe_impedance, + temperature_dependence, + options + ) + return parameterize( + CableConstantsFormulation, + (inputs...) -> _constants_formulation(inputs[1:(end - 1)]..., + last(inputs) isa NamedTuple ? FormulationOptions(last(inputs)) : last(inputs)), + values; + combine + ) +end + +Formulation(::Val{:cable_constants}; kwargs...) = CableConstantsFormulation(; kwargs...) + +function computation_options( + ::Type{<:CableConstantsFormulation}, + options::ComputationOptions +)::ComputationOptions + isempty(options.data) || throw(ArgumentError( + "CableConstants compute does not accept computation options", + )) + return ComputationOptions() +end + +""" +$(TYPEDEF) + +Own the local cable arrays, corrected resistivities, and reusable matrices for +one cable-constant computation. The constructor consumes the completed +blueprint without retaining a duplicate representation. + +$(TYPEDFIELDS) +""" +struct CableConstantsWorkspace{T <: Real, L, B} + "Concrete array payload used by local primitive assemblers." + cable::L + "Temperature-corrected conductor resistivities [Ω·m]." + rho::Vector{T} + "Reusable primitive, reduction, and result storage." + buffers::B +end + +function CableConstantsWorkspace( + problem::CableConstantsProblem{T}, + formulation::CableConstantsFormulation, + blueprint::CableBlueprint{T} +) where {T <: Real} + return CableConstantsWorkspace( + problem, + formulation, + LocalCableData(blueprint) + ) +end + +function CableConstantsWorkspace( + problem::CableConstantsProblem{T}, + formulation::CableConstantsFormulation, + cable::LocalCableData{T} +) where {T <: Real} + @inbounds for assembly in cable.assemblies + isempty(cable.dielectric_ranges[first(assembly)]) && throw(ArgumentError( + "assembly core :$(cable.terminals[first(assembly)]) has no radial dielectric path", + )) + end + count = length(cable.terminals) + rho = Vector{T}(undef, length(cable.conductor_materials)) + + maximum_size = maximum(length, cable.assemblies) + removed = maximum_size - 1 + buffers = ( + Z = Matrix{Complex{T}}(undef, count, count), + Y = Matrix{Complex{T}}(undef, count, count), + reduced = Matrix{Complex{T}}(undef, 1, 1), + factor = Matrix{Complex{T}}(undef, removed, removed), + coupling = Matrix{Complex{T}}(undef, 1, removed), + right_hand_side = Matrix{Complex{T}}(undef, removed, 1), + indices = collect(1:maximum_size), + layer_coefficients = Vector{Complex{T}}( + undef, length(cable.dielectric_materials) + ), + dielectric_admittivity = Vector{Complex{T}}(undef, length(cable.dielectric_materials)), + R = Vector{T}(undef, length(cable.assemblies)), + L = Vector{T}(undef, length(cable.assemblies)), + C = Vector{T}(undef, length(cable.assemblies)), + G = Vector{T}(undef, length(cable.assemblies)), + observations = nothing + ) + # The internal formula has an expression for each surface impedance that the geometry + # needs. + for kind in (any(>(0), cable.r_in) ? (:inner, :outer, :transfer) : (:outer,)) + validate(Expression(formulation.methods.internal_impedance, + InternalImpedance.internal_impedance, Val(kind))) + end + buffers = initialize_buffers(formulation.methods, T, cable, (;), buffers) + return CableConstantsWorkspace{T, typeof(cable), typeof(buffers)}( + cable, rho, buffers + ) +end + +function _solve!( + workspace::CableConstantsWorkspace{T}, + problem::CableConstantsProblem{T}, + formulation::CableConstantsFormulation; + physical_inputs=completed_inputs(problem), gridpoint=Commons.gridpoint_id() +) where {T <: Real} + buffers = workspace.buffers + ω = 2 * (one(T) * π) * problem.frequency + s = complex(zero(T), ω) + for (index, material) in pairs(workspace.cable.conductor_materials) + workspace.rho[index] = constitutive(formulation.methods.temperature_dependence, + material, problem.temperature; workspace) + end + dielectric!(buffers.dielectric_admittivity, workspace.cable, formulation.methods, + problem.frequency, problem.temperature; workspace) + cable_impedance!( + buffers.Z, + workspace.cable, + workspace.rho, + formulation.methods, + s; workspace + ) + cable_admittance!( + buffers.Y, + workspace.cable, + buffers.dielectric_admittivity, + s, + buffers.layer_coefficients + ) + keep = @view buffers.indices[1:1] + @inbounds for (assembly, chain) in pairs(workspace.cable.assemblies) + count = length(chain) + Z = @view buffers.Z[chain, chain] + eliminate = @view buffers.indices[2:count] + factor = @view buffers.factor[1:(count - 1), 1:(count - 1)] + coupling = @view buffers.coupling[:, 1:(count - 1)] + right_hand_side = @view buffers.right_hand_side[1:(count - 1), :] + kron_reduce!( + Z, + keep, + eliminate, + buffers.reduced, + factor, + coupling, + right_hand_side + ) + equivalent = buffers.reduced[1, 1] + buffers.R[assembly] = real(equivalent) + buffers.L[assembly] = imag(equivalent) / ω + + Y = buffers.Y[first(chain), first(chain)] + iszero(Y) && throw(ArgumentError( + "assembly core :$(workspace.cable.terminals[first(chain)]) has no finite radial dielectric path", + )) + buffers.G[assembly] = real(Y) + buffers.C[assembly] = imag(Y) / ω + end + return CableConstants( + Symbol[workspace.cable.terminals[first(chain)] + for chain in workspace.cable.assemblies], + buffers.R, + buffers.L, + buffers.C, + buffers.G, + problem.frequency, + ComputationDetails(NamedTuple{ + (:formulations,:selections,:formulation_fields,:shunt_model,:inputs,:gridpoint), + NTuple{6,NamedTuple}}((values(completed_formulation(formulation))..., + workspace.cable.shunt_details,physical_inputs,gridpoint))) + ) +end + +""" +$(TYPEDSIGNATURES) + +Compute earth-free cable constants with the default coaxial formulation. +""" +function compute( + problem::CableConstantsProblem; + options::Union{NamedTuple, ComputationOptions} = ComputationOptions() +) + options = options isa NamedTuple ? ComputationOptions(options) : options + return compute( + LineCableModelsCoaxial(), + problem, + CableConstantsFormulation(); + options + ) +end + +""" +$(TYPEDSIGNATURES) + +Compute earth-free cable constants with an explicit formula bundle. +""" +function compute( + problem::CableConstantsProblem, + formulation::CableConstantsFormulation; + options::Union{NamedTuple, ComputationOptions} = ComputationOptions() +) + options = options isa NamedTuple ? ComputationOptions(options) : options + return compute(LineCableModelsCoaxial(), problem, formulation; options) +end + +function compute( + problem::CableConstantsProblem, + formulations::AbstractVector{<:CableConstantsFormulation}; + options::Union{NamedTuple, ComputationOptions} = ComputationOptions() +) + options = options isa NamedTuple ? ComputationOptions(options) : options + return compute(LineCableModelsCoaxial(), problem, formulations; options) +end + +""" +$(TYPEDSIGNATURES) + +Compute earth-free cable constants through the coaxial backend tag. +""" +function compute( + engine::LineCableModelsCoaxial, + problem::CableConstantsProblem, + formulation::CableConstantsFormulation = CableConstantsFormulation(); + options::Union{NamedTuple, ComputationOptions} = ComputationOptions() +) + options = options isa NamedTuple ? ComputationOptions(options) : options + values = compute( + engine, + problem, + typeof(formulation)[formulation]; + options + ) + return first(values) +end + +function compute( + engine::LineCableModelsCoaxial, + problem::CableConstantsProblem, + formulations::AbstractVector{<:CableConstantsFormulation}; + options::Union{NamedTuple, ComputationOptions} = ComputationOptions() +) + options = options isa NamedTuple ? ComputationOptions(options) : options + computation_options(CableConstantsFormulation, options) + isempty(formulations) && throw(ArgumentError( + "cable-constant formulation collections cannot be empty", + )) + validate(problem) + for formulation in formulations + validate(problem.design, formulation.methods.pipe_impedance, engine) + end + blueprints = flatten(engine, [problem.design], eltype(problem), formulations) + cables = [LocalCableData(first(blueprints))] + for index in 2:length(blueprints) + previous = findfirst(other -> other === blueprints[index], blueprints) + push!(cables, previous < index ? cables[previous] : + LocalCableData(blueprints[index])) + end + physical_inputs = completed_inputs(problem) + source_id = Commons.gridpoint_id().source_id + return map(formulations, cables, eachindex(formulations)) do formulation, cable, index + workspace = CableConstantsWorkspace(problem, formulation, cable) + _solve!(workspace, problem, formulation; physical_inputs, + gridpoint=Commons.gridpoint_id(;source_id,formulation_index=index)) + end +end + +""" +$(TYPEDSIGNATURES) + +Calculate earth-free constants directly from one completed cable design. +""" +function CableConstants( + design::CableDesign; + temperature::Real = 20, + frequency::Real = 50, + formulation::CableConstantsFormulation = CableConstantsFormulation(), + options::Union{NamedTuple, ComputationOptions} = ComputationOptions() +) + problem = CableConstantsProblem(design; temperature, frequency) + return compute(problem, formulation; options) +end + +function computation_details( + ::Type{<:CableConstantsFormulation}, + result::CableConstants +)::ComputationDetails + return details(result) +end diff --git a/src/engine/dataframe.jl b/src/engine/dataframe.jl deleted file mode 100644 index af5579479..000000000 --- a/src/engine/dataframe.jl +++ /dev/null @@ -1,292 +0,0 @@ -import DataFrames: DataFrame, metadata! - -const _LP_FREQ_COL = :frequency - -const _SERIES_DUMMY = SeriesImpedance(zeros(ComplexF64, 1, 1, 1)) -const _SHUNT_DUMMY = ShuntAdmittance(zeros(ComplexF64, 1, 1, 1)) - -_freq_units_label(unit::Symbol) = unit_text(unit, "Hz") - -_length_unit(per::Symbol) = per - -function _column_name(meta::ComponentMetadata) - component = meta.component - if component in (:resistance, :inductance, :conductance, :capacitance) - return Symbol(meta.symbol) - else - return Symbol(component) - end -end - -function _normalize_quantity_units(units) - return normalize_quantity_units(units) -end - -function _frequency_vector(obj, freqs) - if freqs === nothing - return float.(collect(axes(obj, 3))) - else - f = collect(freqs) - length(f) == size(obj, 3) || - Base.error("Frequency vector length does not match object samples") - return float.(f) - end -end - -function _frequency_vector(slice::AbstractVector, freqs::AbstractVector) - f = collect(freqs) - length(f) == length(slice) || - Base.error("Frequency vector length must match slice length") - return float.(f) -end - -function _build_dataframe( - slice, - freq_raw::Vector{<:Real}, - comps::Vector{ComponentMetadata}, - units::Dict{Symbol, Symbol}, - length_unit::Symbol, - freq_unit::Symbol, - tol::Real, -) - freq_scale = frequency_scale(freq_unit) - freq_values = freq_raw .* freq_scale - unit_map = Dict{Symbol, String}( - _LP_FREQ_COL => _freq_units_label(freq_unit), - ) - df = DataFrame(_LP_FREQ_COL => freq_values) - for meta in comps - q_prefix = resolve_quantity_prefix(meta.quantity, units) - scale = quantity_scale(q_prefix) - l_scale = meta.unit.per_length ? length_scale(length_unit) : 1.0 - raw_vals = component_values(meta.component, slice, freq_raw) - col_data = map(raw_vals) do x - _clip_field(x * (scale * l_scale), tol) - end - col_name = _column_name(meta) - df[!, col_name] = col_data - unit_map[col_name] = - composite_unit(q_prefix, meta.unit.symbol, meta.unit.per_length, length_unit) - end - metadata!(df, "units", unit_map, style = :note) - return df -end - -function _matrix_dataframes( - obj, - freq_raw::Vector{<:Real}, - comps::Vector{ComponentMetadata}, - units::Dict{Symbol, Symbol}, - length_unit::Symbol, - freq_unit::Symbol, - tol::Real, -) - nx, ny, _ = size(obj.values) - result = Matrix{DataFrame}(undef, nx, ny) - for i in 1:nx, j in 1:ny - slice = @view obj.values[i, j, :] - result[i, j] = _build_dataframe( - slice, - freq_raw, - comps, - units, - length_unit, - freq_unit, - tol, - ) - end - return result -end - -# function _slice_dataframe( -# slice::AbstractVector, -# kind::Symbol, -# freq_raw::Vector{<:Real}, -# mode::Symbol, -# coord::Symbol, -# units::Dict{Symbol, Symbol}, -# length_unit::Symbol, -# freq_unit::Symbol, -# tol::Real, -# ) -# resolved_kind = _resolve_kind(slice, kind, tol) -# comps = -# resolved_kind == :series_impedance ? -# components_for(_SERIES_DUMMY, mode, coord) : -# components_for(_SHUNT_DUMMY, mode, coord) -# return _build_dataframe( -# slice, -# freq_raw, -# comps, -# units, -# length_unit, -# freq_unit, -# tol, -# ) -# end - -""" - DataFrame(Z::SeriesImpedance; freqs=nothing, mode=:RLCG, coord=:cart, - freq_unit=:base, length_unit=:kilo, quantity_units=nothing, - tol=sqrt(eps(Float64))) - -Convert the entries of a `SeriesImpedance` object into per-element `DataFrame`s -indexed by frequency. Returns an `n×n` matrix of `DataFrame`s whose rows -correspond to conductor indices. - -- `freqs`: explicit frequency vector in Hz. Defaults to `1:length(freq axis)`. -- `mode`: `:RLCG` (default) or `:ZY`. For `:ZY`, `coord` may be `:cart` or `:polar`. -- `length_unit`: metric prefix for per-length units (e.g. `:kilo` ⇒ per km). -- `quantity_units`: optional overrides for the quantity metric prefixes used in each column. -- `tol`: absolute tolerance used to zero-out tiny numerical noise. -""" -function DataFrame( - Z::SeriesImpedance; - freqs = nothing, - mode::Symbol = :RLCG, - coord::Symbol = :cart, - freq_unit::Symbol = :base, - length_unit::Symbol = :kilo, - quantity_units = nothing, - tol::Real = sqrt(eps(Float64)), - per_length::Bool = true, -) - freq_raw = _frequency_vector(Z, freqs) - units = _normalize_quantity_units(quantity_units) - comps = components_for(Z, mode, coord; per_length = per_length) - return _matrix_dataframes( - Z, - freq_raw, - comps, - units, - length_unit, - freq_unit, - float(tol), - ) -end - -""" - DataFrame(Y::ShuntAdmittance; freqs=nothing, mode=:RLCG, coord=:cart, - freq_unit=:base, length_unit=:kilo, quantity_units=nothing, - tol=sqrt(eps(Float64))) - -Convert the entries of a `ShuntAdmittance` object into per-element `DataFrame`s -indexed by frequency. Returns an `n×n` matrix of `DataFrame`s. - -Keyword arguments mirror those of `DataFrame(::SeriesImpedance)`. -""" -function DataFrame( - Y::ShuntAdmittance; - freqs = nothing, - mode::Symbol = :RLCG, - coord::Symbol = :cart, - freq_unit::Symbol = :base, - length_unit::Symbol = :kilo, - quantity_units = nothing, - tol::Real = sqrt(eps(Float64)), - per_length::Bool = true, -) - freq_raw = _frequency_vector(Y, freqs) - units = _normalize_quantity_units(quantity_units) - comps = components_for(Y, mode, coord; per_length = per_length) - return _matrix_dataframes( - Y, - freq_raw, - comps, - units, - length_unit, - freq_unit, - float(tol), - ) -end - -function _clip_field(x::Real, tol) - isfinite(x) || return x - return _clip(x, tol) -end - -function _clip_field(m::Measurements.Measurement, tol) - v = _clip(value(m), tol) - u = _clip(uncertainty(m), tol) - return Measurements.measurement(v, u) -end - -_clip_field(x, _) = x - -function _resolve_kind(slice, kind::Symbol, tol::Real) - kind != :auto && return kind - max_real = 0.0 - max_imag = 0.0 - for z in slice - r = real(z) - i = imag(z) - val_r = _scalar_abs(r) - val_i = _scalar_abs(i) - isfinite(val_r) && val_r > max_real && (max_real = val_r) - isfinite(val_i) && val_i > max_imag && (max_imag = val_i) - end - if max_real <= tol && max_imag > tol - return :shunt_admittance - else - return :series_impedance - end -end - -_scalar_abs(x::Real) = abs(x) -_scalar_abs(m::Measurements.Measurement) = abs(value(m)) - -""" - DataFrame(LP::LineParameters; mode=:RLCG, coord=:cart, - freq_unit=:base, length_unit=:kilo, quantity_units=nothing, - tol=sqrt(eps(Float64))) -Convert `LP.Z` and `LP.Y` to per-element, frequency-indexed `DataFrame`s -using `LP.f` as the authoritative frequency vector. Returns `(df_z, df_y)`, -each an `n×n` `Matrix{DataFrame}`. -""" -function DataFrame( - LP::LineParameters; - mode::Symbol = :RLCG, - coord::Symbol = :cart, - freq_unit::Symbol = :base, - length_unit::Symbol = :kilo, - quantity_units = nothing, - tol::Real = sqrt(eps(Float64)), - per_length::Bool = true, -) - # --- validations: LP is the source of truth for frequency samples ---- - @assert eltype(LP.f) <: Real "LP.f must be real-valued frequencies." - nzx, nzy, nfZ = size(LP.Z.values) - nyx, nyy, nfY = size(LP.Y.values) - nfZ == nfY || - Base.error("Z and Y have different number of frequency samples: $nfZ ≠ $nfY.") - length(LP.f) == nfZ || Base.error( - "Length of LP.f ($(length(LP.f))) does not match samples in Z/Y ($nfZ).", - ) - - # --- delegate with LP.f explicitly (no guessing, no manual input) ---- - df_z = DataFrame( - LP.Z; - freqs = LP.f, - mode = mode, - coord = coord, - freq_unit = freq_unit, - length_unit = length_unit, - quantity_units = quantity_units, - tol = tol, - per_length = per_length, - ) - - df_y = DataFrame( - LP.Y; - freqs = LP.f, - mode = mode, - coord = coord, - freq_unit = freq_unit, - length_unit = length_unit, - quantity_units = quantity_units, - tol = tol, - per_length = per_length, - ) - - return df_z, df_y -end diff --git a/src/engine/earthadmittance/EarthAdmittance.jl b/src/engine/earthadmittance/EarthAdmittance.jl index 8905cb2b2..7b536d316 100644 --- a/src/engine/earthadmittance/EarthAdmittance.jl +++ b/src/engine/earthadmittance/EarthAdmittance.jl @@ -1,28 +1,66 @@ """ - LineCableModels.Engine.EarthAdmittance + LineCableModels.Engine.EarthAdmittance + +Define earth-return admittance recipes, numerical primitives, and +formula-owned frequency functors. # Dependencies $(IMPORTS) -# Exports - -$(EXPORTS) """ module EarthAdmittance +import ...Commons: FormulationOptions, formulas # Export public API -export Papadopoulos +export Formula, formula_id, earth_potential_coefficient, formulas # Module-specific dependencies -using ...Commons -import ...Commons: get_description -import ..Engine: EarthAdmittanceFormulation -using Measurements: Measurement, value -using QuadGK: quadgk -using ...Utils: _to_σ, _bessel_diff, to_nominal - -include("homogeneous.jl") -include("base.jl") +#! explicit-imports: off +# These abbreviations are expanded in this module docstring and included files. +using DocStringExtensions: IMPORTS, TYPEDEF, TYPEDFIELDS, TYPEDSIGNATURES +#! explicit-imports: on +import ...Earth: EquivalentHomogeneous +import ..Engine: EarthAdmittanceFormulation, formula_id +#! explicit-imports: off +# Explicitly included equations share these physical and numerical operations. +import ...LineCableModels: validate +import ...Commons: Functor +import ..Engine: EarthPair +import ..Engine: EarthPlan, earth!, same_physical_state, layer_index +using ...Earth: EarthModel +import ...Commons: initialize_buffers +import ..Engine: computation_type, EarthImpedanceFormulation, special_besselix, + SpectralIntegral, integrate +using LinearAlgebra: lu!, ldiv! +import ..EarthImpedance +import ...LineCableModels: FormulaDefinition, Expression, nominal +import ..Engine: description, conductivity +import ..Engine: formulation_options +import ..Engine: AirVoltageSpectrum, earth_spectral_term, earth_spectral_value, + earth_spectral_points!, earth_contour_angle, earth_direct, + outgoing_root, bessel_i0m1, bessel_current_ratio +using ...Commons: vacuum_permittivity +#! explicit-imports: on + +public source_coefficients, earth! + +include("interface.jl") + +#! explicit-imports: off +const FORMULAS = ( + include("formulas/unified.jl"), + include("formulas/default.jl"), + include("formulas/ideal.jl"), + include("formulas/pollaczek1926.jl"), + include("formulas/wise1948.jl"), + include("formulas/xue2018.jl") +) +#! explicit-imports: on + +""" +Return registered earth-admittance identities, including unimplemented equations. +""" +formulas(::Type{<:Formula}) = FORMULAS end # module EarthAdmittance diff --git a/src/engine/earthadmittance/base.jl b/src/engine/earthadmittance/base.jl deleted file mode 100644 index 69adbf0a5..000000000 --- a/src/engine/earthadmittance/base.jl +++ /dev/null @@ -1,7 +0,0 @@ -@inline function Base.getproperty(f::Homogeneous, name::Symbol) - if name === :s || name === :t || name === :Γx || name === :γ1 || name === :γ2 || - name === :μ2 - return getproperty(from_kernel(f), name) - end - return getfield(f, name) # subtype-specific fields (if any) -end \ No newline at end of file diff --git a/src/engine/earthadmittance/formulas/default.jl b/src/engine/earthadmittance/formulas/default.jl new file mode 100644 index 000000000..dea72e577 --- /dev/null +++ b/src/engine/earthadmittance/formulas/default.jl @@ -0,0 +1,12 @@ +""" +$(TYPEDSIGNATURES) + +Route the default selection to the explicit `:unified` implementation. +No numerical equation is owned by `:default`. +""" +description(::Type{<:Formula{:default}}; compact::Bool=false) = + compact ? description(Formula{:unified};compact=true) : "Default routing to :unified" + +Formula{:default}(; kwargs...) = Formula{:unified}(; kwargs...) + +:default diff --git a/src/engine/earthadmittance/formulas/ideal.jl b/src/engine/earthadmittance/formulas/ideal.jl new file mode 100644 index 000000000..feded847b --- /dev/null +++ b/src/engine/earthadmittance/formulas/ideal.jl @@ -0,0 +1,88 @@ +function description(::Type{<:Formula{:ideal}}; compact::Bool = false) + compact ? "Ideal earth" : "Ideal-earth electrostatic image potential coefficients" +end + +""" +$(TYPEDSIGNATURES) + +Evaluate Maxwell's electrostatic image potential coefficients above an +ideal conducting earth plane, in m/F. + +# Assumptions + +Aerial conductors in layer 1 above an ideal conducting plane, with air +permittivity ``\\varepsilon_0`` [F/m]. The external potential coefficient is +zero whenever either conductor is buried, including buried self and mutual +interactions. Insulation potential coefficients remain separate. + +# Expression + +```math +P_{ii}=\\frac{1}{2\\pi\\varepsilon_0}\\ln\\frac{2h_i}{r_i},\\qquad +P_{ij}=\\frac{1}{2\\pi\\varepsilon_0}\\ln\\frac{D_{ij}}{d_{ij}}, +``` + +```math +d_{ij}=\\sqrt{x_{ij}^2+(h_i-h_j)^2},\\qquad +D_{ij}=\\sqrt{x_{ij}^2+(h_i+h_j)^2}. +``` + +Here ``h_i`` is aerial height, ``r_i`` is the exterior radius, and ``x_{ij}`` +is horizontal separation. ``d_{ij}`` and ``D_{ij}`` are distances to the real +conductor and its image, respectively, all in meters. +For lossless materials, the complete potential matrix gives purely imaginary +admittance ``Y=j\\omega P^{-1}``. Capacitance itself is real. + +The coaxial backend evaluates these equations directly. PSCAD maps this +selection to its native potential model. Its direct-integration setting can +produce nonzero aerial conductance even with lossless insulation. The adapter +preserves that native deviation rather than changing these equations. + +# Reference + +PSCAD 5.1 help, *Deriving System Y and Z Matrices*, Eq. (8-25), and +*Mutual Impedance with Earth Return*, Eq. (8-35). +""" +function earth_potential_coefficient( + ::Formula{:ideal}, ::Val{:self}, ::Val{1}, ::Val{1}, + functor, workspace) + pair = functor.input.pair + ε0 = vacuum_permittivity(typeof(real(functor.input.jω))) + return log(2 * pair.heights[1] / pair.radius) / (2 * (one(ε0) * π) * ε0) +end + +function earth_potential_coefficient( + ::Formula{:ideal}, ::Val{:mutual}, ::Val{1}, ::Val{1}, + functor, workspace) + pair = functor.input.pair + ε0 = vacuum_permittivity(typeof(real(functor.input.jω))) + hi, hj = pair.heights + D = hypot(pair.separation, hi + hj) + d = hypot(pair.separation, hi - hj) + return log(D / d) / (2 * (one(ε0) * π) * ε0) +end + +function earth_potential_coefficient( + ::Formula{:ideal}, ::Union{Val{:self}, Val{:mutual}}, ::Val{2}, ::Val{2}, + functor, workspace) + return zero(functor.input.jω) +end + +function earth_potential_coefficient( + ::Formula{:ideal}, ::Val{:mutual}, ::Val{1}, ::Val{2}, + functor, workspace) + return zero(functor.input.jω) +end + +function earth_potential_coefficient( + ::Formula{:ideal}, ::Val{:mutual}, ::Val{2}, ::Val{1}, + functor, workspace) + return zero(functor.input.jω) +end + +function formulation_options(::Expression{ + <:Formula{:ideal}, typeof(earth_potential_coefficient)}) + FormulationOptions() +end + +:ideal diff --git a/src/engine/earthadmittance/formulas/pollaczek1926.jl b/src/engine/earthadmittance/formulas/pollaczek1926.jl new file mode 100644 index 000000000..a3355de6d --- /dev/null +++ b/src/engine/earthadmittance/formulas/pollaczek1926.jl @@ -0,0 +1,35 @@ +""" +$(TYPEDSIGNATURES) + +**Identification.** Classical homogeneous-earth underground potential coefficient. + +**Availability.** Registered scientific identity. The coaxial implementation is +not yet implemented. The method fails without a numerical fallback. + +**Expression.** + +```math +P_{e,ij}^{11}=\\frac{j\\omega}{2\\pi(\\sigma_1+j\\omega\\varepsilon_1)} +[K_0(\\gamma_0d_{ij})-K_0(\\gamma_0D_{ij})]. +``` + +**Reference.** F. Pollaczek, “Über das Feld einer unendlich langen +wechselstromdurchflossenen Einfachleitung,” *Elektrische Nachrichtentechnik*, +3, 339–360, 1926. Potential-coefficient transcription follows Ametani et al., +IET, 2021. +""" +function description(::Type{<:Formula{:pollaczek1926}}; compact::Bool = false) + compact ? "Pollaczek" : "Pollaczek underground potential coefficients (1926) — not yet implemented" +end + +function earth_potential_coefficient( + ::Formula{:pollaczek1926}, kind::Union{Val{:self}, Val{:mutual}}, ::Val{2}, ::Val{2}, + functor, workspace +) + throw(ArgumentError("earth_potential_coefficient :pollaczek1926 ($kind), source layer 2, target layer 2: not yet implemented for the coaxial backend")) +end + +formulation_options(::Expression{<:Formula{:pollaczek1926}, typeof(earth_potential_coefficient)}) = + FormulationOptions() + +:pollaczek1926 diff --git a/src/engine/earthadmittance/formulas/unified.jl b/src/engine/earthadmittance/formulas/unified.jl new file mode 100644 index 000000000..8acbc40a7 --- /dev/null +++ b/src/engine/earthadmittance/formulas/unified.jl @@ -0,0 +1,361 @@ +""" +$(TYPEDSIGNATURES) + +Validate Unified's prescribed longitudinal wavenumber Γ \\[1/m\\]. A scalar +applies at every frequency. A nonempty vector follows the frequency order. Return `Γ`. +""" +function validate(Γ, ::Type{<:Union{EarthImpedance.Formula{:unified}, Formula{:unified}}}) + Γ isa Union{Number, AbstractVector} || throw(ArgumentError( + "unified Γ must be a scalar or frequency-aligned vector [1/m]")) + values = Γ isa Number ? (Γ,) : Γ + !isempty(values) && + all(value -> value isa Number && !(value isa Bool) && isfinite(value), values) || + throw(ArgumentError("unified Γ must be a finite scalar or nonempty finite vector [1/m]")) + return Γ +end + +function formulation_options( + owner::Type{<:Union{EarthImpedance.Formula{:unified}, Formula{:unified}}}, + options::FormulationOptions) + argument = validate(get(options.data, :Γ, 0), owner) + return FormulationOptions(merge(options.data, + (; Γ = argument isa AbstractVector ? copy(argument) : argument))) +end + +# The formula's construction validated Γ. Its expressions take the value as supplied. +function formulation_options( + expression::Expression{<:Union{ + EarthImpedance.Formula{:unified}, Formula{:unified}}}, + ::Val{:Γ}, default, supplied) + return supplied +end + +function description(::Type{<:Formula{:unified}}; compact::Bool = false) + compact ? "Unified" : + "Unified circumferential earth potential with complete enclosed-current normalization" +end + +function description(::Type{<:Formula{:unified}}, ::Val{:Γ}, value::Number; compact::Bool = false) + "Γ="*string(value)*" m⁻¹" +end +function description(::Type{<:Formula{:unified}}, ::Val{:Γ}, value::AbstractVector; compact::Bool = false) + "Γ=["*join(value, ", ")*"] m⁻¹ (frequency order)" +end + +function computation_type(::Type{T}, + selected::Union{EarthImpedance.Formula{:unified}, Formula{:unified}}, + frequencies) where {T <: Real} + argument = selected.options.data.Γ + argument isa AbstractVector && length(argument) != length(frequencies) && + throw(DimensionMismatch("unified Γ must contain one value per frequency sample")) + argument isa Number && + return promote_type(T, typeof(real(argument)), typeof(imag(argument))) + return foldl(argument; init = T) do scalar, value + promote_type(scalar, typeof(real(value)), typeof(imag(value))) + end +end + +""" +$(TYPEDSIGNATURES) + +Calculate both source coefficients of a conductor pair for circumferentially averaged fields +in two homogeneous half-spaces: the axial-field coefficient per source current \\[Ω/m\\] and +the source-charge coefficient of conductor voltage \\[m/F\\]. It takes the Unified formula of +either earth family. The exp(jωt) convention and caller-prescribed Γ \\[1/m\\] +give + +```math +\\widetilde K_{ij}=\\mathcal Z_{ij}-\\Gamma^2\\mathcal P_{\\phi,ij}/(j\\omega). +``` + +The electric scalar-potential contribution is retained when Γ is nonzero. Air targets use +the interface z=0 as voltage reference. Buried targets use deep earth. With source +amplitudes q̃ and conductor voltages U, + +```math +U=\\widetilde P\\widetilde q,\\qquad P_e T_I=\\widetilde P. +``` + +The source-potential coefficient includes the complete-field voltage relation, not just +scalar potential. The air endpoint and conductor field are combined before quadrature to +preserve cancellation. Source columns include exp(abs(real(κⱼrⱼ))) scaling, shared by both +coefficients and the enclosed-current matrices, and it cancels in the physical matrix solve. +`functor.input.pair` retains source-target geometry \\[m\\]. `functor.state` contains the +evaluated media of the system, and the workspace buffers store the circumferential factors. +The complete current map converts the axial-field coefficients to physical series impedance. +The direct-image term is evaluated once for both coefficients. Quadrature estimates remain +diagnostic warnings. + +# Returns + +- `(axial, potential)`: the scaled axial-field coefficient \\[Ω/m\\] and the scaled + source-potential coefficient \\[m/F\\]. + +# Reference + +User-supplied manuscript, *Unified circumferentially averaged framework for overhead, +buried, and mixed conductor systems*: complete-field current relation, voltage and +source-charge maps. +""" +function source_coefficients( + formula::Union{EarthImpedance.Formula{:unified}, Formula{:unified}}, + kind::Union{Val{:self}, Val{:mutual}}, source::Union{Val{1}, Val{2}}, + target::Union{Val{1}, Val{2}}, functor, workspace) + u=functor.state + pair=functor.input.pair + buffers=workspace.buffers + row, column=pair.row, pair.column + hp, hq=abs(pair.heights[2]), abs(pair.heights[1]) + y=pair.separation + r=workspace.plan.geometry.radius[row] + average=buffers.circumference_average[row] + argument=buffers.radial_argument[row] + sp, sq=buffers.source_logscale[row], buffers.source_logscale[column] + πT=one(u.jω)*π + integration=functor.input.options.data.integration + context=( + formula = :unified, frequency = imag(u.jω)/(2π), receiver = row, source = column) + direct=earth_direct(formula, kind, source, target, u, pair, r, average, argument, sp, sq) + # The axial field, before the source potential: the integral records keep this order. + medium=target === Val(1) ? 1 : 2 + z=u.jω/πT*average*earth_spectral_term(formula, Val(:Z), target, source, u, + hp, hq, y, zero(r), sp+sq, + integration.method, integration.options, buffers; context) + z+=u.jω*u.mu[medium]/(2πT)*direct + phi=zero(z) + if !iszero(u.Γ) + phi=u.jω/πT*average*earth_spectral_term(formula, Val(:phi), target, source, u, + hp, hq, y, zero(r), sp+sq, + integration.method, integration.options, buffers; context) + phi+=u.jω/(2πT*u.sh[medium])*direct + end + axial=z-u.Γ^2/u.jω*phi + # A buried target takes its voltage from deep earth. + if target === Val(2) + value=u.jω/(one(u.jω)*π)*average*earth_spectral_term(formula, Val(:voltage), Val(2), + source, u, hp, hq, y, zero(r), sp+sq, integration.method, + integration.options, buffers; context) + return (axial, value+u.jω/(2*(one(u.jω)*π)*u.sh[2])*direct) + end + # An air target takes its voltage from the interface. + if abs(real(nominal(argument)))<300 + R=typeof(float(nominal(real(u.jω)))) + padding=hq/2 + angle=min(R(π)/6, atan(R(nominal(hq))/(4max(R(nominal(y+r)), eps(R))))) + angle=earth_contour_angle(u, angle) + points=earth_spectral_points!(buffers.earth_spectrum, u, hq-padding, y, r, angle) + g=(hp, hq, radius = r, padding, logscale = sq, i0minus = bessel_i0m1(argument)) + S=source === Val(1) ? 1 : 2 + kernel=AirVoltageSpectrum{S, typeof(u), typeof(g)}(u, g) + scale=max(R(abs(nominal(u.k[2]))), inv(R(nominal(hq)))) + contour=scale*cis(angle) + integral=SpectralIntegral(t->contour*earth_spectral_value( + kernel, contour*t, hq-padding, y, zero(r))) + points ./= scale + push!(points, one(scale)) + value, + _=integrate(integral, integration.method, integration.options, + buffers; points, coordinate_type = R, + context = merge(context, (term = :air_voltage,)), observations = buffers.observations) + return (axial, u.jω/πT*value+u.jω/(2πT*u.sh[1])*direct) + end + value=u.jω/πT*average*earth_spectral_term(formula, Val(:voltage), Val(1), source, u, + hp, hq, y, zero(r), sp+sq, integration.method, integration.options, buffers; context) + value+=u.jω/(2πT*u.sh[1])*direct + return (axial, value+u.jω/πT*earth_spectral_term(formula, Val(:air_reference), Val(1), + source, u, zero(r), hq, y, r, sq, integration.method, integration.options, buffers; + context)) +end + +function Expression(formula::Union{EarthImpedance.Formula{:unified}, Formula{:unified}}, + pair::EarthPair) + return Expression(formula, source_coefficients, + Val(pair.row == pair.column ? :self : :mutual), Val.(layer_index(pair))...) +end + +function formulation_options(::Expression{ + <:Union{EarthImpedance.Formula{:unified}, Formula{:unified}}, + typeof(source_coefficients), + A}) where { + A <: Tuple{ + Union{Val{:self}, Val{:mutual}}, Union{Val{1}, Val{2}}, Union{Val{1}, Val{2}}}} + return FormulationOptions((Γ = 0, integration = (method = :quad, options = (;)))) +end + +function validate(reduction::EquivalentHomogeneous.Formula{:bottommost}, + ::Expression{<:Union{EarthImpedance.Formula{:unified}, Formula{:unified}}, + typeof(source_coefficients)}) + return reduction +end + +# The unified calculation computes the whole system, every conductor pair, even when its slot +# publishes some of them, and both coefficients of each pair: either output needs both. Its +# formulation options are common to all pairs, and the exterior circumferences must lie in one +# half-space and must not overlap. +function EarthPlan(formula::Union{EarthImpedance.Formula{:unified}, Formula{:unified}}, + earth, model::EarthModel, physical::AbstractVector{<:EarthPair}, indices, + geometry::NamedTuple) + whole=invoke(EarthPlan, + Tuple{Union{EarthImpedanceFormulation, EarthAdmittanceFormulation}, + Any, EarthModel, AbstractVector{<:EarthPair}, Any, NamedTuple}, + formula, earth, model, physical, eachindex(physical), geometry) + calculation=only(whole.calculations) + options=first(calculation.parts).options + all(part->isequal(part.options, options), calculation.parts) || + throw(ArgumentError("the unified earth-return calculation requires common formulation options for all conductor pairs")) + for entry in calculation.pairs + pair=entry.pair + target_radius=geometry.radius[pair.row] + source_radius=geometry.radius[pair.column] + # Physical shapes can be disjoint while their equivalent circles overlap. + # The field coefficients integrate these circles, so their applicability + # is checked here using the engine's geometry. + if pair.row==pair.column + target_radius + target_radius+source_radius || throw(DomainError((pair.row, pair.column), + "exterior circumferences must not overlap")) + end + end + published=(; formula, pairs = collect(indices)) + impedance=formula isa EarthImpedanceFormulation ? published : nothing + admittance=formula isa EarthAdmittanceFormulation ? published : nothing + return EarthPlan((merge(calculation, (; impedance, admittance)),)) +end + +# Unified's arithmetic reads the pair's geometry and the conductor radii, not its indices. +function same_physical_state(::Union{EarthImpedance.Formula{:unified}, Formula{:unified}}, + a::EarthPair, b::EarthPair, geometry::NamedTuple) + inputs(pair)=(pair.row==pair.column, pair.layers, pair.heights, pair.separation, + geometry.radius[pair.row], geometry.radius[pair.column]) + return same_physical_state(inputs(a), inputs(b)) +end + +# An impedance and an admittance formula of Unified share one calculation when their model +# parameters and requested equivalent earths agree: they solve the same system. +function same_physical_state(z::EarthImpedance.Formula{:unified}, p::Formula{:unified}) + return same_physical_state(z.parameters, p.parameters) && + same_physical_state(z.equivalent_earth, p.equivalent_earth) +end + +function initialize_buffers( + selected::Union{EarthImpedance.Formula{:unified}, Formula{:unified}}, + ::Type{T}, input, plan, buffers) where {T} + buffers=initialize_buffers(selected.equivalent_earth, T, input, plan, buffers) + buffers=initialize_buffers(SpectralIntegral, Val(:quad), T, input, plan, buffers) + haskey(buffers, :current_map) && return buffers + R=typeof(float(nominal(one(T)))) + n=length(plan.geometry.radius) + axial_field=Matrix{Complex{T}}(undef, n, n) + return merge(buffers, + ( + axial_field, source_potential = similar(axial_field), current_map = similar(axial_field), + enclosed_impedance = similar(axial_field), enclosed_potential = similar(axial_field), + current_factor = similar(axial_field), current_rhs = similar(axial_field), + radial_argument = Vector{Complex{T}}(undef, n), source_logscale = Vector{T}(undef, n), + circumference_average = Vector{Complex{T}}(undef, n), radial_current = Vector{Complex{T}}(undef, n), + earth_spectrum = (points = sizehint!(R[], 128), seeds = sizehint!(R[], 128), + scales = sizehint!(R[], 16)))) +end + +# The Functor of the calculation at one frequency. Its state is the system that every pair +# shares, as plain values. The per-conductor arrays go to Unified's buffers, and the parts +# write the source coefficients into Unified's own two matrices. +function Functor(formula::Union{EarthImpedance.Formula{:unified}, Formula{:unified}}, + input::NamedTuple; workspace) + s=input.jω + isfinite(s) && !iszero(s) || throw(DomainError(s, "jω must be finite and nonzero")) + prescribed=formula.options.data.Γ + longitudinal=prescribed isa Number ? prescribed : prescribed[input.frequency] + for column in axes(input.rho, 2) + validate(@view(input.rho[:, column]), formula, + @view(input.epsilon[:, column]), @view(input.mu[:, column]), + input.thickness) + end + for values in (input.rho, input.epsilon, input.mu) + for column in axes(values, 2), row in axes(values, 1) + + same_physical_state(values[row, column], values[row, 1]) || + throw(ArgumentError("the unified earth-return calculation requires the same equivalent media for all conductor pairs")) + end + end + Γ=oftype(s, longitudinal) + sh=ntuple(m->conductivity(input.rho[m, 1])+s*input.epsilon[m, 1], 2) + mu=ntuple(m->input.mu[m, 1], 2) + gamma=ntuple(m->sqrt(s*mu[m]*sh[m]), 2) + k2=ntuple(m->gamma[m]^2-Γ^2, 2) + k=map(outgoing_root, k2) + buffers=workspace.buffers + geometry=workspace.plan.geometry + for i in eachindex(buffers.radial_argument) + medium=input.media[i] + buffers.radial_argument[i]=k[medium]*geometry.radius[i] + buffers.source_logscale[i]=abs(real(buffers.radial_argument[i])) + buffers.circumference_average[i]=special_besselix(0, buffers.radial_argument[i]) + buffers.radial_current[i]=2 * (one(s)*π) * sh[medium] * geometry.radius[i]^2 * + bessel_current_ratio(buffers.radial_argument[i]) + end + state=(jω = s, Γ, sh, mu, k2, k) + destinations=(buffers.axial_field, buffers.source_potential) + return Functor(formula, merge(input, (; destinations)), state) +end + +""" +$(TYPEDSIGNATURES) + +Convert the completed source coefficients to physical exterior impedance +\\[Ω/m\\] and potential coefficients \\[m/F\\] using the complete enclosed-current +map. With s=jω \\[1/s\\], prescribed Γ \\[1/m\\], and radial-current factors Fᵣ +\\[m/Ω\\], + +```math +T_I=A_r^{-1}-F_r\\widetilde K,\\qquad +P_e T_I=\\widetilde P,\\qquad +Z_e T_I=\\widetilde K+\\Gamma^2\\widetilde P/s. +``` + +The work arrays contain a common exponential source-column scaling. Specifically, `current_map` stores TᵢD, not the unscaled Tᵢ. `circumference_average` +contains scaled I₀ values. The same D multiplies both source-coefficient arrays +and cancels from these right solves. The factorization is reused without +conjugating transposes. All conductors participate before selected physical +entries are copied by Engine. + +This complete-field conversion includes both scalar- and vector-potential +contributions. The package then assembles cable contributions and +reductions before calculating total Y=jωP⁻¹ \\[S/m\\]. + +# Returns + +- Named tuple of the physical `impedance` and `admittance` matrices: the impedance \\[Ω/m\\] + and the potential coefficients \\[m/F\\]. + +# Reference + +User-supplied manuscript, *Unified circumferentially averaged framework for +overhead, buried, and mixed conductor systems*, current and charge maps. +""" +function earth!(::Union{EarthImpedance.Formula{:unified}, Formula{:unified}}, functor::Functor, + workspace) + buffers=workspace.buffers + u=functor.state + for column in axes(buffers.axial_field, 2), row in axes(buffers.axial_field, 1) + + buffers.current_map[row, column]=(row==column ? inv(buffers.circumference_average[row]) : + zero(u.jω))- + buffers.radial_current[row]*buffers.axial_field[row, column] + end + copyto!(buffers.current_factor, transpose(buffers.current_map)) + factor=lu!(buffers.current_factor) + copyto!(buffers.current_rhs, transpose(buffers.source_potential)) + ldiv!(factor, buffers.current_rhs) + copyto!(buffers.enclosed_potential, transpose(buffers.current_rhs)) + @. buffers.enclosed_impedance=buffers.axial_field+u.Γ^2/u.jω*buffers.source_potential + copyto!(buffers.current_rhs, transpose(buffers.enclosed_impedance)) + ldiv!(factor, buffers.current_rhs) + copyto!(buffers.enclosed_impedance, transpose(buffers.current_rhs)) + return (impedance = buffers.enclosed_impedance, admittance = buffers.enclosed_potential) +end + +:unified diff --git a/src/engine/earthadmittance/formulas/wise1948.jl b/src/engine/earthadmittance/formulas/wise1948.jl new file mode 100644 index 000000000..115798e07 --- /dev/null +++ b/src/engine/earthadmittance/formulas/wise1948.jl @@ -0,0 +1,41 @@ +""" +$(TYPEDSIGNATURES) + +**Identification.** Wideband homogeneous-earth overhead potential +coefficient. + +**Availability.** Registered scientific identity. The coaxial implementation is +not yet implemented. The method fails without a numerical fallback. + +**Expression.** + +```math +P_{e,ij}=\\frac{P_{0,ij}+M_{ij}+jN_{ij}}{2\\pi\\varepsilon_0}, +``` + +```math +M_{ij}+jN_{ij}=2\\int_0^\\infty +\\frac{e^{-H\\lambda}\\cos(y_{ij}\\lambda)} +{(\\gamma_1^2/\\gamma_0^2)\\lambda+ +\\sqrt{\\lambda^2+\\gamma_1^2-\\gamma_0^2}}d\\lambda, +\\quad P_{0,ij}=\\ln(D_{ij}/d_{ij}). +``` + +**Reference.** W. H. Wise, “Potential Coefficients for Ground Return +Circuits,” *Bell System Technical Journal*, 27, 365–371, 1948. +""" +function description(::Type{<:Formula{:wise1948}}; compact::Bool = false) + compact ? "Wise" : "Wise homogeneous-earth overhead potential coefficient (1948) — not yet implemented" +end + +function earth_potential_coefficient( + ::Formula{:wise1948}, kind::Union{Val{:self}, Val{:mutual}}, ::Val{1}, ::Val{1}, + functor, workspace +) + throw(ArgumentError("earth_potential_coefficient :wise1948 ($kind), source layer 1, target layer 1: not yet implemented for the coaxial backend")) +end + +formulation_options(::Expression{<:Formula{:wise1948}, typeof(earth_potential_coefficient)}) = + FormulationOptions() + +:wise1948 diff --git a/src/engine/earthadmittance/formulas/xue2018.jl b/src/engine/earthadmittance/formulas/xue2018.jl new file mode 100644 index 000000000..c56c86059 --- /dev/null +++ b/src/engine/earthadmittance/formulas/xue2018.jl @@ -0,0 +1,45 @@ +""" +$(TYPEDSIGNATURES) + +**Identification.** Generalized underground potential coefficient. The +registered expression is referenced to infinite earth depth. + +**Availability.** Registered scientific identity. The coaxial implementation is +not yet implemented. The method fails without a numerical fallback. + +**Expression.** + +```math +P_{e,ij}^{\\infty}=\\frac{j\\omega}{2\\pi(\\sigma_1+j\\omega\\varepsilon_1)} +\\left[K_0(\\gamma_1d_{ij})-K_0(\\gamma_1D_{ij})+2S_{12}^c+ +2\\gamma_1^2S_{13}^c\\right], +``` + +```math +S_{12}^c=\\int_0^\\infty\\frac{e^{-Hu_1}\\lambda^2\\cos(y\\lambda)} +{(\\lambda^2+\\gamma_1^2)[u_0+(\\gamma_0^2/\\gamma_1^2)u_1]}d\\lambda, +\\quad +S_{13}^c=\\int_0^\\infty\\frac{e^{-Hu_1}\\cos(y\\lambda)} +{(\\lambda^2+\\gamma_1^2)(u_0+u_1)}d\\lambda. +``` + +**Reference.** Haoyan Xue, *General Formulation and Accurate Evaluation of +Earth-Return Parameters for Overhead / Underground Cables*, doctoral thesis, +Polytechnique Montréal, 2018. +[Primary source](https://publications.polymtl.ca/3190/1/2018_HaoyanXue.pdf). +""" +function description(::Type{<:Formula{:xue2018}}; compact::Bool = false) + compact ? "Xue" : "Xue homogeneous-earth underground potential coefficient (2018) — not yet implemented" +end + +function earth_potential_coefficient( + ::Formula{:xue2018}, kind::Union{Val{:self}, Val{:mutual}}, ::Val{2}, ::Val{2}, + functor, workspace +) + throw(ArgumentError("earth_potential_coefficient :xue2018 ($kind), source layer 2, target layer 2: not yet implemented for the coaxial backend")) +end + +formulation_options(::Expression{<:Formula{:xue2018}, typeof(earth_potential_coefficient)}) = + FormulationOptions() + +:xue2018 diff --git a/src/engine/earthadmittance/homogeneous.jl b/src/engine/earthadmittance/homogeneous.jl deleted file mode 100644 index 2fbd7b08a..000000000 --- a/src/engine/earthadmittance/homogeneous.jl +++ /dev/null @@ -1,228 +0,0 @@ -abstract type Homogeneous <: EarthAdmittanceFormulation end - -struct Kernel{Tγ1, Tγ2, Tμ2} - "Layer where the source conductor is placed." - s::Int - "Layer where the target conductor is placed." - t::Int - "Primary field propagation constant (0 = lossless, 1 = air, 2 = earth)." - Γx::Int - "Air propagation constant γ₁(jω, μ, σ, ε)." - γ1::Tγ1 - "Earth propagation constant γ₂(jω, μ, σ, ε)." - γ2::Tγ2 - "Earth magnetic-constant assumption μ₂(μ)." - μ2::Tμ2 -end - -struct Papadopoulos{Tγ1, Tγ2, Tμ2} <: Homogeneous - kernel::Kernel{Tγ1, Tγ2, Tμ2} -end - -Papadopoulos(; s::Int = 2, t::Int = 2, Γx::Int = 2, - γ1 = (jω, μ, σ, ε) -> sqrt(jω * μ * (σ + jω*ε)), - γ2 = (jω, μ, σ, ε) -> sqrt(jω * μ * (σ + jω*ε)), - μ2 = μ -> μ) = - Papadopoulos( - Kernel{typeof(γ1), typeof(γ2), typeof(μ2)}(s, t, Γx, γ1, γ2, μ2), - ) - -get_description(::Papadopoulos) = "Papadopoulos" -from_kernel(f::Papadopoulos) = f.kernel - - -struct Pollaczek{Tγ1, Tγ2, Tμ2} <: Homogeneous - kernel::Kernel{Tγ1, Tγ2, Tμ2} -end - -Pollaczek(; s::Int = 2, t::Int = 2, Γx::Int = 0, - γ1 = (jω, μ, σ, ε) -> jω * sqrt(μ * ε₀), - γ2 = (jω, μ, σ, ε) -> jω * sqrt(μ * ε₀), - μ2 = μ -> oftype(μ, μ₀)) = - Pollaczek( - Kernel{typeof(γ1), typeof(γ2), typeof(μ2)}(s, t, Γx, γ1, γ2, μ2), - ) - -get_description(::Pollaczek) = "Pollaczek" -from_kernel(f::Pollaczek) = f.kernel - -struct Images{Tγ1, Tγ2, Tμ2} <: Homogeneous - kernel::Kernel{Tγ1, Tγ2, Tμ2} -end - -Images(; s::Int = 1, t::Int = 1, Γx::Int = 0, - γ1 = (jω, μ, σ, ε) -> jω * sqrt(μ * ε₀), - γ2 = (jω, μ, σ, ε) -> jω * sqrt(μ * ε₀), - μ2 = μ -> oftype(μ, μ₀)) = - Images( - Kernel{typeof(γ1), typeof(γ2), typeof(μ2)}(s, t, Γx, γ1, γ2, μ2), - ) - -get_description(::Images) = "Electrostatic images" -from_kernel(f::Images) = f.kernel - -# ρ, ε, μ = ws.rho_g, ws.eps_g, ws.mu_g -# f(h, d, @view(ρ[:,k]), @view(ε[:,k]), @view(μ[:,k]), ws.jω[k]) - -# Functor implementation for all homogeneous earth impedance formulations. -function (f::Homogeneous)( - form::Symbol, - h::AbstractVector{T}, - yij::T, - rho_g::AbstractVector{T}, - eps_g::AbstractVector{T}, - mu_g::AbstractVector{T}, - jω::Complex{T}, -) where {T <: REALSCALAR} - Base.@nospecialize form - return form === :self ? f(Val(:self), h, yij, rho_g, eps_g, mu_g, jω) : - form === :mutual ? f(Val(:mutual), h, yij, rho_g, eps_g, mu_g, jω) : - throw(ArgumentError("Unknown earth admittance form: $form")) -end - -# function (f::Homogeneous)( -# h::AbstractVector{T}, -# yij::T, -# rho_g::AbstractVector{T}, -# eps_g::AbstractVector{T}, -# mu_g::AbstractVector{T}, -# jω::Complex{T}, -# ) where {T <: REALSCALAR} -# return f(Val(:mutual), h, yij, rho_g, eps_g, mu_g, jω) -# end - -function (f::Homogeneous)( - ::Val{:self}, - h::AbstractVector{T}, - yij::T, - rho_g::AbstractVector{T}, - eps_g::AbstractVector{T}, - mu_g::AbstractVector{T}, - jω::Complex{T}, -) where {T <: REALSCALAR} - return f(Val(:mutual), h, yij, rho_g, eps_g, mu_g, jω) -end - -@inline _not(s::Int) = - (s == 1 || s == 2) ? (3 - s) : - throw(ArgumentError("s must be 1 or 2")) - -@inline _get_layer(z) = - z > 0 ? 1 : - (z < 0 ? 2 : throw(ArgumentError("Conductor at interface (h=0) is invalid"))) - -@noinline function _layer_mismatch(which::AbstractString, got::Int, expected::Int) - throw( - ArgumentError( - "conductor $which is in layer $got but formulation expects layer $expected", - ), - ) -end - -@inline function validate_layers!(f::Homogeneous, h) - @boundscheck length(h) == 2 || throw(ArgumentError("h must have length 2")) - ℓ1 = _get_layer(h[1]) - ℓ2 = _get_layer(h[2]) - (ℓ1 == f.s) || _layer_mismatch("i (h[1])", ℓ1, f.s) - (ℓ2 == f.t) || _layer_mismatch("j (h[2])", ℓ2, f.t) - return nothing -end - -@inline function (f::Homogeneous)( - ::Val{:mutual}, - h::AbstractVector{T}, - yij::T, - rho_g::AbstractVector{T}, - eps_g::AbstractVector{T}, - mu_g::AbstractVector{T}, - jω::Complex{T}, -) where {T <: REALSCALAR} - - validate_layers!(f, h) - - s = f.s # index of source layer - o = _not(s) # the other layer - nL = length(rho_g) - μ = similar(mu_g); - σ = similar(rho_g); - @inbounds for i in 1:nL - μ[i] = (i == 1) ? mu_g[i] : f.μ2(mu_g[i]) # μ₂ for earth layers - σ[i] = _to_σ(rho_g[i]) - end - - # construct propagation constants according to formulation assumptions - γ = Vector{Complex{T}}(undef, nL) - @inbounds for i in 1:nL - γ[i] = (i == 1 ? f.γ1 : f.γ2)(jω, μ[i], σ[i], eps_g[i]) - end - γ_s = γ[s]; - γ_o = γ[o] - γs_2 = γ_s^2 - γo_2 = γ_o^2 - - # kx from struct: 0:none, 1:air, 2:source layer - kx_2 = if f.Γx == 0 # precalc squared - zero(γs_2) - else - ℓ = (f.Γx == 1) ? 1 : s - oftype(γs_2, (-jω^2) * μ[ℓ] * eps_g[ℓ]) - end - - σ̃ = σ[s] + jω*eps_g[s] # complex conductivity of source layer - - # unpack geometry - @inbounds hi, hj = abs(h[1]), abs(h[2]) - dij = hypot(yij, hi - hj) # √(y^2 + (hi - hj)^2) - conductor-conductor - Dij = hypot(yij, hi + hj) # √(y^2 + (hi + hj)^2) - conductor-image - - # perfectly conducting earth term in Bessel form - Λij = _bessel_diff(γ_s, dij, Dij) - - # --- Overhead special case --- - # physics: source in AIR (s=t=1), kx = 0, σ_air ≈ 0, - # earth propagation constant negligible γ_earth ≈ 0 - # ⇒ Sij = Tij = 0, Pe = (jω)/(2π(σ_air+jωε_air)) * Λ ≡ (1/(2π ε0)) * Λ - if f.s == 1 && f.Γx == 0 && isapprox(to_nominal(real(γ_o)), 0.0, atol = TOL) - return (jω/(2π*σ̃)) * Λij #(1/(2π*ε₀)) * Λij - end - - # --- Underground,"no displacement currents" --- - # physics: source in EARTH (s=t=2), kx = 0, γ_earth ≈ 0 - # ⇒ Pe = 0 - if f.s == 2 && f.t == 2 && isapprox(to_nominal(real(γ_s)), 0.0, atol = TOL) - return (jω/(2π*σ̃)) * Λij - end - - # precompute scalars for integrand - μ_s = μ[s] - μ_o = μ[o] - H = hi + hj - - # S_ij + T_ij in one go: 2∫₀^∞ (Fij+Gij) cos(yij λ) dλ - # integrand = (λ) -> (Fij(λ) + Gij(λ)) * cos(yij * λ) - @inline function integrand(λ::Float64)::Complex{T} - as = sqrt(λ*λ + γs_2 + kx_2) - ao = sqrt(λ*λ + γo_2 + kx_2) - - F = μ_o * exp(-as*H) / (as*μ_o + ao*μ_s) - - num = μ_o*μ_s*as*(γs_2 - γo_2)*exp(-as*H) - den = (as*μ_o + ao*μ_s) * (as*γo_2*μ_s + ao*γs_2*μ_o) - G = num/den - - (F + G) * cos(yij*λ) - end - - Iij, _ = quadgk( - integrand, - 0.0, - Inf; - rtol = 1e-8, - norm = z -> abs(complex(value(real(z)), value(imag(z)))), - ) - Iij *= 2 - - - return (jω / (2π * σ̃)) * (Λij + Iij) - -end diff --git a/src/engine/earthadmittance/interface.jl b/src/engine/earthadmittance/interface.jl new file mode 100644 index 000000000..a38fe71c5 --- /dev/null +++ b/src/engine/earthadmittance/interface.jl @@ -0,0 +1,103 @@ +""" +$(TYPEDEF) + +Own one earth potential coefficient formulation, its indexed equation declarations and explicit +controls. Each interaction is selected by its explicit kind and source-layer and +target-layer equation signature. The layers in the signatures of its +`earth_potential_coefficient` methods declare the media it handles: methods up to layer 2 treat air +and one homogeneous earth. `parameters` stores model data. `options` stores formulation +choices and numerical controls. + +$(TYPEDFIELDS) +""" +struct Formula{ID, P <: NamedTuple, O <: FormulationOptions, E} <: + EarthAdmittanceFormulation + "Explicit physical model parameters." + parameters::P + "Formulation options. Projected onto required indexed equations during initialization." + options::O + "Independent equivalent homogeneous-earth reduction, or nothing." + equivalent_earth::E +end + +""" +Evaluate one source-owned earth potential coefficient equation. +""" +function earth_potential_coefficient end + +""" +$(TYPEDSIGNATURES) + +Resolve a formulation's model parameters and numerical controls. Its concrete +selection type defines the indexed equation. Material properties are evaluated +before equation execution. +Formula-specific arguments are normalized by their selected owner. +A missing physical case is unsupported. +""" +function Formula{ID}(; parameters::NamedTuple = (;), + options::Union{NamedTuple, FormulationOptions} = FormulationOptions(), equivalent_earth = nothing) where {ID} + options = options isa NamedTuple ? FormulationOptions(options) : options + options = formulation_options(Formula{ID}, options) + isempty(parameters) || + throw(ArgumentError("earth formula :$ID has no configurable physical parameters")) + ID in formulas(Formula) || throw(ArgumentError("unknown earthadmittance formula :$ID")) + reduction = if equivalent_earth === nothing + nothing + elseif equivalent_earth isa EquivalentHomogeneous.AbstractSequence + equivalent_earth + elseif equivalent_earth isa FormulaDefinition + EquivalentHomogeneous.AbstractSequence(equivalent_earth) + else + throw(ArgumentError("equivalent_earth must be a formula definition or explicit reduction sequence")) + end + return Formula{ID, typeof(parameters), typeof(options), typeof(reduction)}( + parameters, options, reduction) +end + +Formula(identifier::Symbol; kwargs...) = Formula(Val(identifier); kwargs...) +Formula(::Val{ID}; kwargs...) where {ID} = Formula{ID}(; kwargs...) +Formula(selected::EarthAdmittanceFormulation) = selected +Formula(::Nothing) = Formula(:default) + +# A recipe selects one formula per earth route. Within it, a `nothing` route supplies no +# expression. Only omitting the whole slot selects the default. +function Formula(recipe::NamedTuple) + routes = (; pairs(Formula)...) + all(in(keys(routes)), keys(recipe)) || throw(ArgumentError( + "$Formula selections admit only $(join(keys(routes), ", "))")) + names = filter(in(keys(recipe)), keys(routes)) + return NamedTuple{names}(map(name -> recipe[name] === nothing ? nothing : + Formula(recipe[name]), names)) +end + +function Formula(selection::FormulaDefinition{ID, Order}) where {ID, Order} + Order === :default || throw(ArgumentError("order applies only to equivalent_earth")) + return Formula{ID}(; parameters = selection.parameters, + options = selection.options, equivalent_earth = selection.equivalent_earth) +end + +formula_id(::Formula{ID}) where {ID} = ID +formula_id(::Type{<:Formula{ID}}) where {ID} = ID +# Identity-only dispatch also describes retained selections without constructors. +description(value::Formula; compact::Bool = false) = description(typeof(value); compact) +formulation_options(value::Formula) = value.options +# Indexed numerical controls remain deferred to their consuming equations. +formulation_options(::Type{<:Formula}, options::FormulationOptions) = options + +""" +$(TYPEDSIGNATURES) + +Expose the selected identity, model and numerical controls, and explicit +equivalent-earth reduction as a native record. +""" +function Base.NamedTuple(value::Formula) + return (identifier = formula_id(value), + parameters = value.parameters, options = value.options.data, + equivalent_earth = value.equivalent_earth === nothing ? nothing : + NamedTuple(value.equivalent_earth)) +end + +"""Iterate the independently selectable child slots admitted by this formula family.""" +function Base.pairs(::Type{<:Formula}; quantity = nothing) + pairs((air = Formula, earth = Formula, mixed = Formula)) +end diff --git a/src/engine/earthimpedance/EarthImpedance.jl b/src/engine/earthimpedance/EarthImpedance.jl index 098ca9e8d..7a8b6d5d1 100644 --- a/src/engine/earthimpedance/EarthImpedance.jl +++ b/src/engine/earthimpedance/EarthImpedance.jl @@ -1,28 +1,56 @@ """ - LineCableModels.Engine.EarthImpedance + LineCableModels.Engine.EarthImpedance + +Define earth-return impedance recipes, numerical primitives, and formula-owned +frequency functors. # Dependencies $(IMPORTS) -# Exports - -$(EXPORTS) """ module EarthImpedance +import ...Commons: FormulationOptions, formulas # Export public API -export Papadopoulos +export Formula, formula_id, earth_impedance, formulas # Module-specific dependencies -using ...Commons -import ...Commons: get_description -import ..Engine: EarthImpedanceFormulation -using Measurements: Measurement, value -using QuadGK: quadgk -using ...Utils: _to_σ, _bessel_diff, to_nominal - -include("homogeneous.jl") -include("base.jl") +#! explicit-imports: off +# These abbreviations are expanded in this module docstring and included files. +using DocStringExtensions: IMPORTS, TYPEDEF, TYPEDFIELDS, TYPEDSIGNATURES +#! explicit-imports: on +import ...Earth: EquivalentHomogeneous +import ..Engine: EarthImpedanceFormulation, formula_id +#! explicit-imports: off +# Explicitly included equations share these physical and numerical operations. +import ...LineCableModels: FormulaDefinition, Expression +import ..Engine: description, conductivity, special_besselk +import ..Engine: formulation_options +using ...Commons: vacuum_permeability +#! explicit-imports: on + +include("interface.jl") + +#! explicit-imports: off +const FORMULAS = ( + include("formulas/unified.jl"), + include("formulas/ametani2009.jl"), + include("formulas/carson1926.jl"), + include("formulas/default.jl"), + include("formulas/gary1976.jl"), + include("formulas/lucca1994.jl"), + include("formulas/pollaczek1926.jl"), + include("formulas/saad1996.jl"), + include("formulas/wedepohl1973.jl"), + include("formulas/wise1934.jl"), + include("formulas/xue2018.jl") +) +#! explicit-imports: on + +""" +Return registered earth-impedance identities, including unimplemented equations. +""" +formulas(::Type{<:Formula}) = FORMULAS end # module EarthImpedance diff --git a/src/engine/earthimpedance/base.jl b/src/engine/earthimpedance/base.jl deleted file mode 100644 index 3df1a4b43..000000000 --- a/src/engine/earthimpedance/base.jl +++ /dev/null @@ -1,8 +0,0 @@ - -@inline function Base.getproperty(f::Homogeneous, name::Symbol) - if name === :s || name === :t || name === :Γx || name === :γ1 || name === :γ2 || - name === :μ2 - return getproperty(from_kernel(f), name) - end - return getfield(f, name) # subtype-specific fields (if any) -end \ No newline at end of file diff --git a/src/engine/earthimpedance/formulas/ametani2009.jl b/src/engine/earthimpedance/formulas/ametani2009.jl new file mode 100644 index 000000000..32f816648 --- /dev/null +++ b/src/engine/earthimpedance/formulas/ametani2009.jl @@ -0,0 +1,50 @@ +""" +$(TYPEDSIGNATURES) + +**Identification.** Homogeneous-earth approximation for mixed overhead-underground pairs. + +**Availability.** Registered scientific identity. The coaxial implementation is +not yet implemented. The method fails without a numerical fallback. + +**Expression.** Its distinctive mixed term is + +```math +Z_{e,ij}^{01}=\\frac{j\\omega\\mu_0}{2\\pi}e^{-h_g/h_e}\\ln\\frac{S}{D}, +\\quad h_e=(j\\omega\\mu_0\\sigma_g)^{-1/2}, +``` + +```math +S=\\sqrt{(h_a+h_g+2h_e)^2+y_{ij}^2},\\qquad +D=\\sqrt{(h_a+h_g)^2+y_{ij}^2}. +``` + +**Reference.** A. Ametani, T. Yoneda, Y. Baba, and N. Nagaoka, “An +Investigation of Earth-Return Impedance Between Overhead and Underground +Conductors and Its Approximation,” *IEEE Transactions on Electromagnetic +Compatibility*, 51, 860–867, 2009. +DOI: 10.1109/TEMC.2009.2019953. +PSCAD's help lists this journal article with a 2005 date. Its journal volume +and DOI identify the 2009 publication used by this registration. +""" +function description(::Type{<:Formula{:ametani2009}}; compact::Bool = false) + compact ? "Ametani" : "Ametani mixed-pair homogeneous-earth impedance (2009)" +end + +function earth_impedance( + ::Formula{:ametani2009}, kind::Val{:mutual}, ::Val{1}, ::Val{2}, + functor, workspace +) + throw(ArgumentError("earth_impedance :ametani2009 ($kind), source layer 1, target layer 2: not yet implemented for the coaxial backend")) +end + +function earth_impedance( + ::Formula{:ametani2009}, kind::Val{:mutual}, ::Val{2}, ::Val{1}, + functor, workspace +) + throw(ArgumentError("earth_impedance :ametani2009 ($kind), source layer 2, target layer 1: not yet implemented for the coaxial backend")) +end + +formulation_options(::Expression{<:Formula{:ametani2009}, typeof(earth_impedance)}) = + FormulationOptions() + +:ametani2009 diff --git a/src/engine/earthimpedance/formulas/carson1926.jl b/src/engine/earthimpedance/formulas/carson1926.jl new file mode 100644 index 000000000..8de140da7 --- /dev/null +++ b/src/engine/earthimpedance/formulas/carson1926.jl @@ -0,0 +1,37 @@ +""" +$(TYPEDSIGNATURES) + +**Identification.** Classical homogeneous, conductive-earth overhead +impedance. Displacement currents and longitudinal propagation are neglected. + +**Availability.** Registered scientific identity. The coaxial implementation is +not yet implemented. The method fails without a numerical fallback. + +**Expression.** + +```math +Z_{e,ij}=\\frac{j\\omega\\mu_0}{2\\pi}\\left[ +\\ln\\frac{D_{ij}}{d_{ij}}+2\\int_0^\\infty +\\frac{e^{-H\\lambda}\\cos(y_{ij}\\lambda)} +{\\lambda+\\sqrt{\\lambda^2+\\gamma_g^2}}d\\lambda\\right], +\\qquad \\gamma_g^2=j\\omega\\mu_0\\sigma_g. +``` + +**Reference.** J. R. Carson, “Wave Propagation in Overhead Wires with Ground +Return,” *Bell System Technical Journal*, 5, 539–554, 1926. +""" +function description(::Type{<:Formula{:carson1926}}; compact::Bool = false) + compact ? "Carson" : "Carson homogeneous-earth overhead impedance (1926)" +end + +function earth_impedance( + ::Formula{:carson1926}, kind::Union{Val{:self}, Val{:mutual}}, ::Val{1}, ::Val{1}, + functor, workspace +) + throw(ArgumentError("earth_impedance :carson1926 ($kind), source layer 1, target layer 1: not yet implemented for the coaxial backend")) +end + +formulation_options(::Expression{<:Formula{:carson1926}, typeof(earth_impedance)}) = + FormulationOptions() + +:carson1926 diff --git a/src/engine/earthimpedance/formulas/default.jl b/src/engine/earthimpedance/formulas/default.jl new file mode 100644 index 000000000..dea72e577 --- /dev/null +++ b/src/engine/earthimpedance/formulas/default.jl @@ -0,0 +1,12 @@ +""" +$(TYPEDSIGNATURES) + +Route the default selection to the explicit `:unified` implementation. +No numerical equation is owned by `:default`. +""" +description(::Type{<:Formula{:default}}; compact::Bool=false) = + compact ? description(Formula{:unified};compact=true) : "Default routing to :unified" + +Formula{:default}(; kwargs...) = Formula{:unified}(; kwargs...) + +:default diff --git a/src/engine/earthimpedance/formulas/gary1976.jl b/src/engine/earthimpedance/formulas/gary1976.jl new file mode 100644 index 000000000..ef84ba016 --- /dev/null +++ b/src/engine/earthimpedance/formulas/gary1976.jl @@ -0,0 +1,91 @@ +function description(::Type{<:Formula{:gary1976}}; compact::Bool = false) + compact ? "Gary" : "Gary complex-depth approximation (1976)" +end + +""" +$(TYPEDSIGNATURES) + +Evaluate Gary's complex-depth approximation for overhead self and mutual +exterior impedance, in Ω/m. + +# Assumptions + +Aerial conductors above homogeneous, nonmagnetic conductive earth, with an +image plane at a complex depth. Displacement current is neglected. + +# Expression + +For the positive-time convention ``e^{j\\omega t}``, define + +```math +h_e=(j\\omega\\mu_0\\sigma_g)^{-1/2},\\qquad +\\mu_0=4\\pi\\,10^{-7}\\ \\mathrm{H/m}. +``` + +Soil conductivity ``\\sigma_g`` is in S/m. The principal square root gives +complex depth ``h_e`` in m. With aerial heights ``h_i,h_j`` and horizontal +separation ``x``, the direct and complex-image distances are + +```math +d=\\sqrt{x^2+(h_i-h_j)^2},\\qquad +S=\\sqrt{x^2+(h_i+h_j+2h_e)^2}. +``` + +The complete exterior mutual impedance is + +```math +Z_{e,ij}=\\frac{j\\omega\\mu_0}{2\\pi}\\ln\\frac{S}{d}. +``` + +For self impedance, the image distance is ``2(h_i+h_e)`` and the direct distance +is the exterior radius ``r_i``: + +```math +Z_{e,ii}=\\frac{j\\omega\\mu_0}{2\\pi}\\ln\\frac{2(h_i+h_e)}{r_i}. +``` + +All geometric lengths are in m. The expression includes the ideal-earth exterior +term. Returning only the lossy-ground correction ``\\ln(S/D)`` with real-image +distance ``D`` would omit that term from the package's assembly. + +# Reference + +C. Gary, “Approche complète de la propagation multifilaire en haute fréquence +par utilisation des matrices complexes,” *EDF Bulletin de la Direction des +Études et Recherches*, série B (1976). The implementation follows the +complex-image expression reproduced in +[PSCAD's earth-return impedance documentation](https://www.pscad.com/webhelp-v5-ol/EMTDC/Transmission_Lines/Mutual_Impedance_with_Earth_Return.htm), +Eq. (8-28) and its complex-depth diagram. PSCAD calls its native implementation +Deri-Semlyen. +""" +function earth_impedance( + ::Formula{:gary1976}, ::Val{:self}, ::Val{1}, ::Val{1}, + functor, workspace +) + pair = functor.input.pair + s = functor.input.jω + μ0 = vacuum_permeability(typeof(real(s))) + he = inv(sqrt(s * μ0 * conductivity(functor.input.rho[2]))) + return s * μ0 / (2 * (one(real(s)) * π)) * log(2 * (pair.heights[1] + he) / pair.radius) +end + +function earth_impedance( + ::Formula{:gary1976}, ::Val{:mutual}, ::Val{1}, ::Val{1}, + functor, workspace +) + pair = functor.input.pair + s = functor.input.jω + μ0 = vacuum_permeability(typeof(real(s))) + he = inv(sqrt(s * μ0 * conductivity(functor.input.rho[2]))) + hi, hj = pair.heights + x = pair.separation + S = sqrt((hi + hj + 2he)^2 + x^2) + d = hypot(x, hi - hj) + return s * μ0 / (2 * (one(real(s)) * π)) * log(S / d) +end + +function formulation_options(::Expression{<:Formula{:gary1976}, typeof(earth_impedance)}) + FormulationOptions() +end + +:gary1976 diff --git a/src/engine/earthimpedance/formulas/lucca1994.jl b/src/engine/earthimpedance/formulas/lucca1994.jl new file mode 100644 index 000000000..4bcc0c868 --- /dev/null +++ b/src/engine/earthimpedance/formulas/lucca1994.jl @@ -0,0 +1,83 @@ +function description(::Type{<:Formula{:lucca1994}}; compact::Bool = false) + compact ? "Lucca" : "Lucca mixed-pair homogeneous-earth impedance (1994)" +end + +""" +$(TYPEDSIGNATURES) + +Evaluate Lucca's mutual exterior impedance between an aerial and a buried +conductor, in Ω/m. + +# Assumptions + +Homogeneous, nonmagnetic conductive earth, neglecting displacement current. +Both ordered interactions between an aerial conductor and a buried conductor +use the same reciprocal expression. +The formula supplies no self or same-half-space interactions. Those remain +separate selections. + +# Expression + +With the positive-time convention ``e^{j\\omega t}``, let ``h_a>0`` be aerial +height, ``h_g>0`` burial depth, and ``x`` horizontal separation, all in m. +Signed package heights are converted to these positive physical distances. +With soil conductivity ``\\sigma_g`` [S/m], define + +```math +h_e=(j\\omega\\mu_0\\sigma_g)^{-1/2},\\qquad +H=h_a+h_g+2h_e,\\qquad +S=\\sqrt{H^2+x^2},\\qquad +D=\\sqrt{(h_a+h_g)^2+x^2}, +``` + +where ``\\mu_0=4\\pi\\,10^{-7}`` H/m and square roots use their principal value. +The complete mixed coefficient is + +```math +Z_{e,ag}=\\frac{j\\omega\\mu_0}{2\\pi} +\\left[\\ln\\frac{S}{D} +-\\frac23\\left(\\frac{h_e}{S^2}\\right)^3 H(H^2-3x^2)\\right]. +``` + +The correction uses ``(h_e/S^2)^3``, not ``h_e^3/S^3``. + +# Reference + +G. Lucca, “Mutual Impedance Between an Overhead and a Buried Line with Earth +Return,” *9th International Conference on Electromagnetic Compatibility* (1994), +[doi:10.1049/cp:19940679](https://doi.org/10.1049/cp:19940679). +The implemented expression is reproduced in +[PSCAD's earth-return impedance documentation](https://www.pscad.com/webhelp-v5-ol/EMTDC/Transmission_Lines/Mutual_Impedance_with_Earth_Return.htm), +Eq. (8-34), with the geometric definitions immediately below it. +""" +function earth_impedance( + ::Formula{:lucca1994}, ::Val{:mutual}, ::Val{1}, ::Val{2}, + functor, workspace +) + pair = functor.input.pair + s = functor.input.jω + μ0 = vacuum_permeability(typeof(real(s))) + he = inv(sqrt(s * μ0 * conductivity(functor.input.rho[2]))) + vertical = abs(pair.heights[1]) + abs(pair.heights[2]) + x = pair.separation + H = vertical + 2he + S2 = H^2 + x^2 + D = hypot(vertical, x) + return s * μ0 / (2 * (one(real(s)) * π)) * + (log(sqrt(S2) / D) - 2 * (he / S2)^3 * H * (H^2 - 3x^2) / 3) +end + +# The reciprocal mixed coefficient has the same physical distances in either direction. +function earth_impedance( + selected::Formula{:lucca1994}, kind::Val{:mutual}, ::Val{2}, ::Val{1}, + functor, workspace +) + return earth_impedance(selected, kind, Val(1), Val(2), functor, workspace) +end + +function formulation_options(::Expression{ + <:Formula{:lucca1994}, typeof(earth_impedance)}) + FormulationOptions() +end + +:lucca1994 diff --git a/src/engine/earthimpedance/formulas/pollaczek1926.jl b/src/engine/earthimpedance/formulas/pollaczek1926.jl new file mode 100644 index 000000000..c7a376066 --- /dev/null +++ b/src/engine/earthimpedance/formulas/pollaczek1926.jl @@ -0,0 +1,37 @@ +""" +$(TYPEDSIGNATURES) + +**Identification.** Classical homogeneous-earth underground integral. + +**Availability.** Registered scientific identity. The coaxial implementation is +not yet implemented. The method fails without a numerical fallback. + +**Expression.** The underground term is + +```math +Z_{e,ij}^{11}=\\frac{j\\omega\\mu_0}{2\\pi}\\left[ +K_0(\\gamma_1d_{ij})-K_0(\\gamma_1D_{ij})+2\\int_0^\\infty +\\frac{e^{-H\\sqrt{\\lambda^2+\\gamma_1^2}}} +{\\lambda+\\sqrt{\\lambda^2+\\gamma_1^2}} +\\cos(y_{ij}\\lambda)d\\lambda\\right], +``` + +**Reference.** F. Pollaczek, “Über das Feld einer unendlich langen +wechselstromdurchflossenen Einfachleitung,” *Elektrische Nachrichtentechnik*, +3, 339–360, 1926. +""" +function description(::Type{<:Formula{:pollaczek1926}}; compact::Bool = false) + compact ? "Pollaczek" : "Pollaczek homogeneous-earth underground impedance (1926)" +end + +function earth_impedance( + ::Formula{:pollaczek1926}, kind::Union{Val{:self}, Val{:mutual}}, ::Val{2}, ::Val{2}, + functor, workspace +) + throw(ArgumentError("earth_impedance :pollaczek1926 ($kind), source layer 2, target layer 2: not yet implemented for the coaxial backend")) +end + +formulation_options(::Expression{<:Formula{:pollaczek1926}, typeof(earth_impedance)}) = + FormulationOptions() + +:pollaczek1926 diff --git a/src/engine/earthimpedance/formulas/saad1996.jl b/src/engine/earthimpedance/formulas/saad1996.jl new file mode 100644 index 000000000..a7a6660bf --- /dev/null +++ b/src/engine/earthimpedance/formulas/saad1996.jl @@ -0,0 +1,136 @@ +function description(::Type{<:Formula{:saad1996}}; compact::Bool = false) + compact ? "Saad" : "Saad underground closed form (1996)" +end + +""" +$(TYPEDSIGNATURES) + +Evaluate Saad, Gaba, and Giroux's closed-form approximation for underground +self and mutual earth-return impedance, in Ω/m. + +# Assumptions + +Parallel horizontal cables in homogeneous, linear, isotropic, nonmagnetic +conductive earth below air. Displacement current is neglected, and the +wavelength is long compared with the transverse dimensions. Neither air +propagation nor a nonzero imposed longitudinal propagation constant is retained. +The earth-return calculation treats cables as filaments, substituting the +exterior cable radius in the self term. Internal conductor and insulation +contributions are separate. + +# Expression + +With the package's positive-time convention ``e^{j\\omega t}``, define + +```math +m=\\sqrt{\\frac{j\\omega\\mu_0}{\\rho_g}},\\qquad +\\ell=h_i+h_j,\\qquad +d=\\sqrt{x^2+(h_i-h_j)^2},\\qquad +D=\\sqrt{x^2+(h_i+h_j)^2}. +``` + +Here ``\\rho_g`` is earth resistivity [Ω·m], ``\\mu_0=4\\pi\\,10^{-7}`` H/m, +and ``m`` has units of inverse meters. Depths ``h_i,h_j`` are positive downward. +``x`` is horizontal separation, ``d`` is axis distance, and ``D`` is image +distance, all in meters. The implementation uses the principal square root. +Its branch and a separate time factor remain unspecified in the source. + +The proposed mutual and self expressions, Eqs. (5)-(6), repeated as (26)-(27), are + +```math +Z_{e,ij}=\\frac{j\\omega\\mu_0}{2\\pi} +\\left[K_0(md)+\\frac{2e^{-m\\ell}}{4+m^2x^2}\\right], +``` + +```math +Z_{e,ii}=\\frac{j\\omega\\mu_0}{2\\pi} +\\left[K_0(mr_i)+\\frac{2e^{-2mh_i}}{4+m^2r_i^2}\\right]. +``` + +``K_0`` is the modified Bessel function of the second kind, order zero. +``r_i`` is the exterior cable radius [m], written ``R`` in the paper. +The source prefactor ``\\rho_g m^2/(2\\pi)`` equals ``j\\omega\\mu_0/(2\\pi)``. +The self expression follows the mutual geometry with ``x=r_i`` and +``h_i=h_j``. The mutual correction denominator uses horizontal separation, +not axis distance. + +# Approximation and limitations + +The source starts from the Pollaczek/Wedepohl representations, Eqs. (1)-(4): + +```math +Z_m=\\frac{\\rho_gm^2}{2\\pi} +\\left[K_0(md)-K_0(mD)+J_m\\right],\\qquad +J_m=\\int_{-\\infty}^{\\infty} +\\frac{e^{-\\ell\\sqrt{\\gamma^2+m^2}}} +{|\\gamma|+\\sqrt{\\gamma^2+m^2}}e^{j\\gamma x}\\,d\\gamma, +``` + +```math +Z_s=\\frac{\\rho_gm^2}{2\\pi} +\\left[K_0(mR)-K_0\\!\\left(m\\sqrt{R^2+4h^2}\\right)+J_s\\right],\\qquad +J_s=\\int_{-\\infty}^{\\infty} +\\frac{e^{-2h\\sqrt{\\gamma^2+m^2}}} +{|\\gamma|+\\sqrt{\\gamma^2+m^2}}e^{j\\gamma R}\\,d\\gamma. +``` + +Here ``\\gamma`` is the Fourier integration variable [1/m], not ``m`` or a +longitudinal line propagation constant. The source declares these parent +expressions without attributing new kernels to Saad et al. The paper cites +Pollaczek's 1931 French publication separately from the 1926 overhead result. + +After contour deformation, Eq. (15) approximates +``\\sqrt{\\delta^2+1}/(\\delta+\\sqrt{\\delta^2+1})`` by +``(1+e^{-2\\delta})/2``. Equations (20)-(21) then use +``\\sqrt{\\delta^2+1}\\simeq1`` in the rapidly decaying part. The approximated +interface integral cancels the separate image Bessel term. The further +small-argument reduction discussed in the paper is not this implementation. + +The contour proof is restricted to ``x/\\ell<1``. The paper reports about 3% +maximum relative error for the first kernel approximation, errors below about +1.5% for typical ``x/\\ell<1`` geometries, and negligible error through 10 kHz +for its ``x/\\ell=5`` examples. These reported test cases do not establish universal +bounds or runtime acceptance conditions. + +# Reference + +O. Saad, G. Gaba, and M. Giroux, “A Closed-Form Approximation for Ground Return +Impedance of Underground Cables,” *IEEE Transactions on Power Delivery*, +11(3), 1536–1545, 1996. Model and parent expressions: pp. 1536–1537, +Eqs. (1)-(4). Proposed equations: p. 1537, Eqs. (5)-(6). Derivation: +pp. 1537–1539, Eqs. (7)-(27). Error discussion: p. 1540 and Figs. 5–6. +The transcription was checked against the original publication's page images. +""" +function earth_impedance( + ::Formula{:saad1996}, ::Val{:self}, ::Val{2}, ::Val{2}, + functor, workspace +) + pair = functor.input.pair + s = functor.input.jω + μ0 = vacuum_permeability(typeof(real(s))) + m = sqrt(s * μ0 / functor.input.rho[2]) + h, r = abs(pair.heights[1]), pair.radius + return s * μ0 / (2 * (one(real(s)) * π)) * + (special_besselk(0, m * r) + 2exp(-2m * h) / (4 + (m * r)^2)) +end + +function earth_impedance( + ::Formula{:saad1996}, ::Val{:mutual}, ::Val{2}, ::Val{2}, + functor, workspace +) + pair = functor.input.pair + s = functor.input.jω + μ0 = vacuum_permeability(typeof(real(s))) + m = sqrt(s * μ0 / functor.input.rho[2]) + hi, hj = abs.(pair.heights) + x = pair.separation + d = hypot(x, hi - hj) + return s * μ0 / (2 * (one(real(s)) * π)) * + (special_besselk(0, m * d) + 2exp(-m * (hi + hj)) / (4 + (m * x)^2)) +end + +function formulation_options(::Expression{<:Formula{:saad1996}, typeof(earth_impedance)}) + FormulationOptions() +end + +:saad1996 diff --git a/src/engine/earthimpedance/formulas/unified.jl b/src/engine/earthimpedance/formulas/unified.jl new file mode 100644 index 000000000..fc7c7e28a --- /dev/null +++ b/src/engine/earthimpedance/formulas/unified.jl @@ -0,0 +1,35 @@ +""" +$(TYPEDSIGNATURES) + +**Identification.** Unified circumferentially averaged earth impedance for overhead, buried +and mixed conductor systems in two homogeneous half-spaces, with complete enclosed-current +normalization. + +**Expression.** `EarthAdmittance.source_coefficients` computes the axial-field coefficient of +each conductor pair, together with its source-potential coefficient, for the Unified formula +of either earth family. + +**Validity.** The formula holds within two ranges. +- A nonzero prescribed Γ lies between the propagation constants of air and earth. It is in + range at a frequency where Im γ_air ≤ Im Γ ≤ Im γ_earth and Re Γ ≤ Re γ_earth. Γ = 0, the + default, always lies in range. +- Each receiving conductor is represented by the mean field on its exterior circumference, + which holds while |κ_m r_p| ≤ 0.1. Here κ_m is the outgoing root of γ_m² − Γ² in the + medium of conductor p, and r_p is its exterior radius, the jacket for an insulated cable. + Above the range, the voltage that a thick conductor receives departs from the full-field + value, with an error that grows as (κ_m r_p)². Emission from a thick conductor is + accurate. +""" +function description(::Type{<:Formula{:unified}}; compact::Bool = false) + compact ? "Unified" : + "Unified circumferential earth impedance with complete enclosed-current normalization" +end + +function description(::Type{<:Formula{:unified}}, ::Val{:Γ}, value::Number; compact::Bool = false) + "Γ="*string(value)*" m⁻¹" +end +function description(::Type{<:Formula{:unified}}, ::Val{:Γ}, value::AbstractVector; compact::Bool = false) + "Γ=["*join(value, ", ")*"] m⁻¹ (frequency order)" +end + +:unified diff --git a/src/engine/earthimpedance/formulas/wedepohl1973.jl b/src/engine/earthimpedance/formulas/wedepohl1973.jl new file mode 100644 index 000000000..e89cffe51 --- /dev/null +++ b/src/engine/earthimpedance/formulas/wedepohl1973.jl @@ -0,0 +1,88 @@ +function description(::Type{<:Formula{:wedepohl1973}}; compact::Bool = false) + compact ? "Wedepohl" : "Wedepohl-Wilcox low-frequency underground approximation (1973)" +end + +""" +$(TYPEDSIGNATURES) + +Evaluate the Wedepohl-Wilcox low-order approximation for underground +self and mutual exterior impedance, in Ω/m. + +# Assumptions + +Buried conductors in homogeneous conductive earth, neglecting displacement +current. The expansion assumes small penetration-depth products and uses the +supplied absolute soil permeability. It is distinct from the internal-impedance +formulation with the same `:wedepohl1973` identifier. + +# Expression + +For the positive-time convention ``e^{j\\omega t}``, define + +```math +m=\\sqrt{j\\omega\\mu_g/\\rho_g},\\qquad e_c=1.7811. +``` + +Here ``\\rho_g`` is resistivity [Ω·m], ``\\mu_g`` is permeability [H/m], and +``m`` is inverse penetration depth [1/m], using the principal square root. +The published rounded logarithmic constant ``e_c`` approximates the exponential +of Euler's constant. + +For positive burial depths ``h_i,h_j``, horizontal separation ``x``, exterior +radius ``r_i``, and axis distance ``d=\\sqrt{x^2+(h_i-h_j)^2}`` (all lengths in m), + +```math +Z_{e,ii}=\\frac{j\\omega\\mu_g}{2\\pi} +\\left[-\\ln\\left(\\frac{e_cmr_i}{2}\\right)+\\frac12-\\frac43mh_i\\right], +``` + +```math +Z_{e,ij}=\\frac{j\\omega\\mu_g}{2\\pi} +\\left[-\\ln\\left(\\frac{e_cmd}{2}\\right)+\\frac12-\\frac23m(h_i+h_j)\\right]. +``` + +The implementation evaluates this approximation at every requested frequency. Coincident mutual axes are invalid +physical geometry. A vertical pair at distinct depths has ``d>0`` and uses the same equation. + +# Reference + +L. M. Wedepohl and D. J. Wilcox, “Transient Analysis of Underground +Power-Transmission Systems: System-Model and Wave-Propagation Characteristics,” +*Proceedings of the IEE* 120, 253–260 (1973), p. 255, Eqs. (7)-(8), +[doi:10.1049/piee.1973.0056](https://doi.org/10.1049/piee.1973.0056). +The equations and rounded constant are also reproduced in +[PSCAD's earth-return impedance documentation](https://www.pscad.com/webhelp-v5-ol/EMTDC/Transmission_Lines/Mutual_Impedance_with_Earth_Return.htm), +Eqs. (8-31)-(8-32). +""" +function earth_impedance( + ::Formula{:wedepohl1973}, ::Val{:self}, ::Val{2}, ::Val{2}, + functor, workspace +) + pair = functor.input.pair + s, μ = functor.input.jω, functor.input.mu[2] + m = sqrt(s * μ / functor.input.rho[2]) + ec = one(real(m)) * 17811 / 10000 + return s * μ / (2 * (one(real(s)) * π)) * + (-log(ec * m * pair.radius / 2) + one(m) / 2 - 4m * abs(pair.heights[1]) / 3) +end + +function earth_impedance( + ::Formula{:wedepohl1973}, ::Val{:mutual}, ::Val{2}, ::Val{2}, + functor, workspace +) + pair = functor.input.pair + s, μ = functor.input.jω, functor.input.mu[2] + m = sqrt(s * μ / functor.input.rho[2]) + ec = one(real(m)) * 17811 / 10000 + hi, hj = abs.(pair.heights) + d = hypot(pair.separation, hi - hj) + return s * μ / (2 * (one(real(s)) * π)) * + (-log(ec * m * d / 2) + one(m) / 2 - 2m * (hi + hj) / 3) +end + +function formulation_options(::Expression{ + <:Formula{:wedepohl1973}, typeof(earth_impedance)}) + FormulationOptions() +end + +:wedepohl1973 diff --git a/src/engine/earthimpedance/formulas/wise1934.jl b/src/engine/earthimpedance/formulas/wise1934.jl new file mode 100644 index 000000000..a6e731eba --- /dev/null +++ b/src/engine/earthimpedance/formulas/wise1934.jl @@ -0,0 +1,38 @@ +""" +$(TYPEDSIGNATURES) + +**Identification.** Homogeneous-earth wideband overhead integral retaining +earth displacement current and magnetic permeability. + +**Availability.** Registered scientific identity. The coaxial implementation is +not yet implemented. The method fails without a numerical fallback. + +**Expression.** + +```math +Z_{e,ij}=\\frac{j\\omega\\mu_0}{2\\pi}\\left[ +\\ln\\frac{D_{ij}}{d_{ij}}+2\\int_0^\\infty +\\frac{\\mu_1e^{-\\lambda H}} +{\\lambda\\mu_1+a_1\\mu_0}\\cos(y_{ij}\\lambda)d\\lambda\\right], +\\quad a_1=\\sqrt{\\lambda^2+\\gamma_1^2-\\gamma_0^2}. +``` + +**Reference.** W. H. Wise, “Propagation of High-Frequency Currents in Ground +Return Circuits,” *Proceedings of the Institute of Radio Engineers*, 22, +522–527, 1934. +""" +function description(::Type{<:Formula{:wise1934}}; compact::Bool = false) + compact ? "Wise" : "Wise homogeneous-earth overhead impedance (1934) — not yet implemented" +end + +function earth_impedance( + ::Formula{:wise1934}, kind::Union{Val{:self}, Val{:mutual}}, ::Val{1}, ::Val{1}, + functor, workspace +) + throw(ArgumentError("earth_impedance :wise1934 ($kind), source layer 1, target layer 1: not yet implemented for the coaxial backend")) +end + +formulation_options(::Expression{<:Formula{:wise1934}, typeof(earth_impedance)}) = + FormulationOptions() + +:wise1934 diff --git a/src/engine/earthimpedance/formulas/xue2018.jl b/src/engine/earthimpedance/formulas/xue2018.jl new file mode 100644 index 000000000..fda967fb3 --- /dev/null +++ b/src/engine/earthimpedance/formulas/xue2018.jl @@ -0,0 +1,44 @@ +""" +$(TYPEDSIGNATURES) + +**Identification.** Generalized homogeneous-earth underground wideband +impedance. + +**Availability.** Registered scientific identity. The coaxial implementation is +not yet implemented. The method fails without a numerical fallback. + +**Expression.** + +```math +Z_{e,ij}=\\frac{j\\omega\\mu_0}{2\\pi}\\left[ +K_0(\\gamma_1d_{ij})-K_0(\\gamma_1D_{ij})+2S_{11}^c+ +2\\gamma_1^2S_{13}^c\\right], +``` + +```math +S_{11}^c=\\int_0^\\infty\\frac{e^{-Hu_1}\\lambda^2\\cos(y\\lambda)} +{(\\lambda^2+\\gamma_1^2)(u_0+u_1)}d\\lambda,\\qquad +S_{13}^c=\\int_0^\\infty\\frac{e^{-Hu_1}\\cos(y\\lambda)} +{(\\lambda^2+\\gamma_1^2)(u_0+u_1)}d\\lambda. +``` + +**Reference.** Haoyan Xue, *General Formulation and Accurate Evaluation of +Earth-Return Parameters for Overhead / Underground Cables*, doctoral thesis, +Polytechnique Montréal, 2018. +[Primary source](https://publications.polymtl.ca/3190/1/2018_HaoyanXue.pdf). +""" +function description(::Type{<:Formula{:xue2018}}; compact::Bool = false) + compact ? "Xue" : "Xue homogeneous-earth underground impedance (2018) — not yet implemented" +end + +function earth_impedance( + ::Formula{:xue2018}, kind::Union{Val{:self}, Val{:mutual}}, ::Val{2}, ::Val{2}, + functor, workspace +) + throw(ArgumentError("earth_impedance :xue2018 ($kind), source layer 2, target layer 2: not yet implemented for the coaxial backend")) +end + +formulation_options(::Expression{<:Formula{:xue2018}, typeof(earth_impedance)}) = + FormulationOptions() + +:xue2018 diff --git a/src/engine/earthimpedance/homogeneous.jl b/src/engine/earthimpedance/homogeneous.jl deleted file mode 100644 index c4caa6624..000000000 --- a/src/engine/earthimpedance/homogeneous.jl +++ /dev/null @@ -1,206 +0,0 @@ -abstract type Homogeneous <: EarthImpedanceFormulation end - -struct Kernel{Tγ1, Tγ2, Tμ2} - "Layer where the source conductor is placed." - s::Int - "Layer where the target conductor is placed." - t::Int - "Primary field propagation constant (0 = lossless, 1 = air, 2 = earth)." - Γx::Int - "Air propagation constant γ₁(jω, μ, σ, ε)." - γ1::Tγ1 - "Earth propagation constant γ₂(jω, μ, σ, ε)." - γ2::Tγ2 - "Earth magnetic-constant assumption μ₂(μ)." - μ2::Tμ2 -end - -struct Papadopoulos{Tγ1, Tγ2, Tμ2} <: Homogeneous - kernel::Kernel{Tγ1, Tγ2, Tμ2} -end - -Papadopoulos(; s::Int = 2, t::Int = 2, Γx::Int = 2, - γ1 = (jω, μ, σ, ε) -> sqrt(jω * μ * (σ + jω*ε)), - γ2 = (jω, μ, σ, ε) -> sqrt(jω * μ * (σ + jω*ε)), - μ2 = μ -> μ) = - Papadopoulos( - Kernel{typeof(γ1), typeof(γ2), typeof(μ2)}(s, t, Γx, γ1, γ2, μ2), - ) - -get_description(::Papadopoulos) = "Papadopoulos" -from_kernel(f::Papadopoulos) = f.kernel - - -struct Pollaczek{Tγ1, Tγ2, Tμ2} <: Homogeneous - kernel::Kernel{Tγ1, Tγ2, Tμ2} -end - -Pollaczek(; s::Int = 2, t::Int = 2, Γx::Int = 0, - γ1 = (jω, μ, σ, ε) -> jω * sqrt(μ * ε), - γ2 = (jω, μ, σ, ε) -> sqrt(jω * μ * σ), - μ2 = μ -> oftype(μ, μ₀)) = - Pollaczek( - Kernel{typeof(γ1), typeof(γ2), typeof(μ2)}(s, t, Γx, γ1, γ2, μ2), - ) - -get_description(::Pollaczek) = "Pollaczek" -from_kernel(f::Pollaczek) = f.kernel - -struct Carson{Tγ1, Tγ2, Tμ2} <: Homogeneous - kernel::Kernel{Tγ1, Tγ2, Tμ2} -end - -Carson(; s::Int = 1, t::Int = 1, Γx::Int = 0, - γ1 = (jω, μ, σ, ε) -> jω * sqrt(μ * ε), - γ2 = (jω, μ, σ, ε) -> sqrt(jω * μ * σ), - μ2 = μ -> oftype(μ, μ₀)) = - Carson( - Kernel{typeof(γ1), typeof(γ2), typeof(μ2)}(s, t, Γx, γ1, γ2, μ2), - ) - -get_description(::Carson) = "Carson" -from_kernel(f::Carson) = f.kernel - - -# ρ, ε, μ = ws.rho_g, ws.eps_g, ws.mu_g -# f(h, d, @view(ρ[:,k]), @view(ε[:,k]), @view(μ[:,k]), ws.freq[k]) - -# Functor implementation for all homogeneous earth impedance formulations. -function (f::Homogeneous)( - form::Symbol, - h::AbstractVector{T}, - yij::T, - rho_g::AbstractVector{T}, - eps_g::AbstractVector{T}, - mu_g::AbstractVector{T}, - jω::Complex{T}, -) where {T <: REALSCALAR} - Base.@nospecialize form - return form === :self ? f(Val(:self), h, yij, rho_g, eps_g, mu_g, jω) : - form === :mutual ? f(Val(:mutual), h, yij, rho_g, eps_g, mu_g, jω) : - throw(ArgumentError("Unknown earth impedance form: $form")) -end - -# function (f::Homogeneous)( -# h::AbstractVector{T}, -# yij::T, -# rho_g::AbstractVector{T}, -# eps_g::AbstractVector{T}, -# mu_g::AbstractVector{T}, -# jω::Complex{T}, -# ) where {T <: REALSCALAR} -# return f(Val(:mutual), h, yij, rho_g, eps_g, mu_g, jω) -# end - -function (f::Homogeneous)( - ::Val{:self}, - h::AbstractVector{T}, - yij::T, - rho_g::AbstractVector{T}, - eps_g::AbstractVector{T}, - mu_g::AbstractVector{T}, - jω::Complex{T}, -) where {T <: REALSCALAR} - return f(Val(:mutual), h, yij, rho_g, eps_g, mu_g, jω) -end - -@inline _not(s::Int) = - (s == 1 || s == 2) ? (3 - s) : - throw(ArgumentError("s must be 1 or 2")) - -@inline _get_layer(z) = - z > 0 ? 1 : - (z < 0 ? 2 : throw(ArgumentError("Conductor at interface (h=0) is invalid"))) - -@noinline function _layer_mismatch(which::AbstractString, got::Int, expected::Int) - throw( - ArgumentError( - "conductor $which is in layer $got but formulation expects layer $expected", - ), - ) -end - -@inline function validate_layers!(f::Homogeneous, h) - @boundscheck length(h) == 2 || throw(ArgumentError("h must have length 2")) - ℓ1 = _get_layer(h[1]) - ℓ2 = _get_layer(h[2]) - (ℓ1 == f.s) || _layer_mismatch("i (h[1])", ℓ1, f.s) - (ℓ2 == f.t) || _layer_mismatch("j (h[2])", ℓ2, f.t) - return nothing -end - -@inline function (f::Homogeneous)( - ::Val{:mutual}, - h::AbstractVector{T}, - yij::T, - rho_g::AbstractVector{T}, - eps_g::AbstractVector{T}, - mu_g::AbstractVector{T}, - jω::Complex{T}, -) where {T <: REALSCALAR} - - validate_layers!(f, h) - - s = f.s # index of source layer - o = _not(s) # the other layer - nL = length(rho_g) - μ = similar(mu_g); - σ = similar(rho_g); - @inbounds for i in 1:nL - μ[i] = (i == 1) ? mu_g[i] : f.μ2(mu_g[i]) # μ₂ for earth layers - σ[i] = _to_σ(rho_g[i]) - end - - # construct propagation constants according to formulation assumptions - γ = Vector{Complex{T}}(undef, nL) - @inbounds for i in 1:nL - γ[i] = (i == 1 ? f.γ1 : f.γ2)(jω, μ[i], σ[i], eps_g[i]) - end - γ_s = γ[s]; - γ_o = γ[o] - γs_2 = γ_s^2 - γo_2 = γ_o^2 - - # kx from struct: 0:none, 1:air, 2:source layer - kx_2 = if f.Γx == 0 # precalc squared - zero(γs_2) - else - ℓ = (f.Γx == 1) ? 1 : s - oftype(γs_2, (-jω^2) * μ[ℓ] * eps_g[ℓ]) - end - - # unpack geometry - @inbounds hi, hj = abs(h[1]), abs(h[2]) - dij = hypot(yij, hi - hj) # √(y^2 + (hi - hj)^2) - conductor-conductor - Dij = hypot(yij, hi + hj) # √(y^2 + (hi + hj)^2) - conductor-image - - # perfectly conducting earth term in Bessel form - Λij = _bessel_diff(γ_s, dij, Dij) - - # precompute scalars for integrand - μ_s = μ[s] - μ_o = μ[o] - H = hi + hj - - # Sij = 2 ∫_0^∞ Fij(λ) cos(yij λ) dλ - # integrand = (λ) -> Fij(λ) * cos(yij * λ) - @inline function integrand(λ::Float64)::Complex{T} - as = sqrt(λ*λ + γs_2 + kx_2) - ao = sqrt(λ*λ + γo_2 + kx_2) - - F = μ_o * exp(-as*H) / (as*μ_o + ao*μ_s) - - F * cos(yij*λ) - end - - Sij, _ = quadgk( - integrand, - 0.0, - 1.0; - rtol = 1e-8, - norm = z -> abs(complex(value(real(z)), value(imag(z)))), - ) - Sij *= 2 - - return (jω * μ_s / (2π)) * (Λij + Sij) -end diff --git a/src/engine/earthimpedance/interface.jl b/src/engine/earthimpedance/interface.jl new file mode 100644 index 000000000..86c99196f --- /dev/null +++ b/src/engine/earthimpedance/interface.jl @@ -0,0 +1,103 @@ +""" +$(TYPEDEF) + +Own one earth impedance formulation, its indexed equation declarations and explicit +controls. Each interaction is selected by its explicit kind and source-layer and +target-layer equation signature. The layers in the signatures of its +`earth_impedance` methods declare the media it handles: methods up to layer 2 treat air +and one homogeneous earth. `parameters` stores model data. `options` stores formulation +choices and numerical controls. + +$(TYPEDFIELDS) +""" +struct Formula{ID, P <: NamedTuple, O <: FormulationOptions, E} <: + EarthImpedanceFormulation + "Explicit physical model parameters." + parameters::P + "Formulation options. Projected onto required indexed equations during initialization." + options::O + "Independent equivalent homogeneous-earth reduction, or nothing." + equivalent_earth::E +end + +""" +Evaluate one source-owned earth impedance equation. +""" +function earth_impedance end + +""" +$(TYPEDSIGNATURES) + +Resolve a formulation's model parameters and numerical controls. Its concrete +selection type defines the indexed equation. Material properties are evaluated +before equation execution. +Formula-specific arguments are normalized by their selected owner. +A missing physical case is unsupported. +""" +function Formula{ID}(; parameters::NamedTuple = (;), + options::Union{NamedTuple, FormulationOptions} = FormulationOptions(), equivalent_earth = nothing) where {ID} + options = options isa NamedTuple ? FormulationOptions(options) : options + options = formulation_options(Formula{ID}, options) + isempty(parameters) || + throw(ArgumentError("earth formula :$ID has no configurable physical parameters")) + ID in formulas(Formula) || throw(ArgumentError("unknown earthimpedance formula :$ID")) + reduction = if equivalent_earth === nothing + nothing + elseif equivalent_earth isa EquivalentHomogeneous.AbstractSequence + equivalent_earth + elseif equivalent_earth isa FormulaDefinition + EquivalentHomogeneous.AbstractSequence(equivalent_earth) + else + throw(ArgumentError("equivalent_earth must be a formula definition or explicit reduction sequence")) + end + return Formula{ID, typeof(parameters), typeof(options), typeof(reduction)}( + parameters, options, reduction) +end + +Formula(identifier::Symbol; kwargs...) = Formula(Val(identifier); kwargs...) +Formula(::Val{ID}; kwargs...) where {ID} = Formula{ID}(; kwargs...) +Formula(selected::EarthImpedanceFormulation) = selected +Formula(::Nothing) = Formula(:default) + +# A recipe selects one formula per earth route. Within it, a `nothing` route supplies no +# expression. Only omitting the whole slot selects the default. +function Formula(recipe::NamedTuple) + routes = (; pairs(Formula)...) + all(in(keys(routes)), keys(recipe)) || throw(ArgumentError( + "$Formula selections admit only $(join(keys(routes), ", "))")) + names = filter(in(keys(recipe)), keys(routes)) + return NamedTuple{names}(map(name -> recipe[name] === nothing ? nothing : + Formula(recipe[name]), names)) +end + +function Formula(selection::FormulaDefinition{ID, Order}) where {ID, Order} + Order === :default || throw(ArgumentError("order applies only to equivalent_earth")) + return Formula{ID}(; parameters = selection.parameters, + options = selection.options, equivalent_earth = selection.equivalent_earth) +end + +formula_id(::Formula{ID}) where {ID} = ID +formula_id(::Type{<:Formula{ID}}) where {ID} = ID +# Identity-only dispatch also describes retained selections without constructors. +description(value::Formula; compact::Bool = false) = description(typeof(value); compact) +formulation_options(value::Formula) = value.options +# Indexed numerical controls remain deferred to their consuming equations. +formulation_options(::Type{<:Formula}, options::FormulationOptions) = options + +""" +$(TYPEDSIGNATURES) + +Expose the selected identity, model and numerical controls, and explicit +equivalent-earth reduction as a native record. +""" +function Base.NamedTuple(value::Formula) + return (identifier = formula_id(value), + parameters = value.parameters, options = value.options.data, + equivalent_earth = value.equivalent_earth === nothing ? nothing : + NamedTuple(value.equivalent_earth)) +end + +"""Iterate the independently selectable child slots admitted by this formula family.""" +function Base.pairs(::Type{<:Formula}; quantity = nothing) + pairs((air = Formula, earth = Formula, mixed = Formula)) +end diff --git a/src/engine/earthkernels.jl b/src/engine/earthkernels.jl new file mode 100644 index 000000000..ee9ba7c0f --- /dev/null +++ b/src/engine/earthkernels.jl @@ -0,0 +1,354 @@ +""" +Select the outgoing square root for the exp(jωt) convention. +""" +@inline function outgoing_root(z) + root = sqrt(complex(z)) + return real(root) < 0 || (iszero(real(root)) && imag(root) < 0) ? -root : root +end + +@inline root_difference(k2, a, λ) = iszero(k2) ? zero(a) : k2/(a+λ) + +# Keep the decay and oscillation in the same exponential on a complex ray. +@inline earth_cosine(height, separation, λ) = (exp(-(height+im*separation)*λ)+exp(-(height-im*separation)*λ))/2 + +function earth_spectral_value(kernel, λ, height, separation, radius) + if iszero(radius) + return earth_combined_weight(kernel) ? + (earth_weighted_spectrum(kernel, λ, -(height+im*separation)*λ) + + earth_weighted_spectrum(kernel, λ, -(height-im*separation)*λ))/2 : + kernel(λ)*earth_cosine(height, separation, λ) + end + growth=abs(imag(radius*λ)) + weighted=if earth_combined_weight(kernel) + (earth_weighted_spectrum(kernel, λ, -(height+im*separation)*λ+growth) + + earth_weighted_spectrum(kernel, λ, -(height-im*separation)*λ+growth))/2 + else + kernel(λ)*(exp(-(height+im*separation)*λ+growth) + + exp(-(height-im*separation)*λ+growth))/2 + end + isfinite(weighted) || + throw(DomainError((λ, weighted), "nonfinite weighted earth kernel")) + iszero(numerical_magnitude(weighted)) && return weighted + return weighted*special_besseljx(0, radius*λ) +end + +""" +Evaluate I₀(z)−1 without subtracting two nearly equal numbers. +""" +function bessel_i0m1(z) + if abs(nominal(z)) > 0.5 + return special_besselix(0, z)*exp(abs(real(z)))-one(z) + end + term=z*z/4 + result=term + for n in 2:1000 + term *= (z/(2n))^2 + next=result+term + next == result && return next + result=next + end + return result +end + +@inline bessel_j0m1(z) = bessel_i0m1(im*z) + +# Combine the air receiver and its interface endpoint before integration. +# The kernel is ag*(I0*exp(-hp*a0)-J0)/(a0*ds). +struct AirVoltageSpectrum{Q, S, G} + state::S + geometry::G +end + +function earth_weighted_spectrum(kernel::AirVoltageSpectrum{Q}, + λ, logweight) where {Q} + u=kernel.state + g=kernel.geometry + a0=outgoing_root(λ^2+u.k2[1]) + ag=outgoing_root(λ^2+u.k2[2]) + ap=a0 + aq=(a0, ag)[Q] + decay=exp(g.logscale-g.hq*root_difference(u.k2[Q], aq, λ)+logweight) + difference=if iszero(ap) + argument=g.radius*λ + j0=exp(-g.padding*λ+abs(imag(argument)))*special_besseljx(0, argument) + -g.hp*j0 + elseif abs(nominal(g.radius*λ))<0.5 + jminus=bessel_j0m1(g.radius*λ) + envelope=exp(-g.padding*λ) + envelope*(g.i0minus*exp(-g.hp*ap)+expm1(-g.hp*ap)-jminus)/ap + else + argument=g.radius*λ + envelope=exp(-g.padding*λ+abs(imag(argument))) + scaled=iszero(numerical_magnitude(envelope)) ? zero(envelope) : + envelope*special_besseljx(0, argument) + ((one(g.i0minus)+g.i0minus)*exp(-g.padding*λ-g.hp*ap)-scaled)/ap + end + return decay*ag*difference/(u.sh[2]*a0+u.sh[1]*ag) +end +(kernel::AirVoltageSpectrum)(λ) = earth_weighted_spectrum(kernel, λ, zero(λ)) +function earth_combined_weight(kernel::AirVoltageSpectrum) + g=kernel.geometry + return nominal(g.logscale+g.hq*maximum(abs, kernel.state.k))>300 +end + +""" +Evaluate the removable I₁(z)/(z I₀(z)) limit. +""" +@inline function bessel_current_ratio(z) + if abs(nominal(z)) < 1e-3 + z2=z*z + return one(z)/2-z2/16+z2*z2/96-11z2^3/6144 + end + return special_besselix(1, z)/(z*special_besselix(0, z)) +end + +# Each term excludes exp(-height*λ), cos(yλ), and optional J₀(rλ). +# Kind and the two ordered media dispatch outside the spectral loop. +struct EarthSpectrum{Kind, Receiver, Source, S, G} + state::S + geometry::G +end + +function EarthSpectrum{Kind, P, Q}(state::S, geometry::G) where {Kind, P, Q, S, G} + EarthSpectrum{Kind, P, Q, S, G}(state, geometry) +end + +@inline function earth_weighted_spectrum(kernel::EarthSpectrum{:Z, P, Q}, λ, logweight) where { + P, Q} + u=kernel.state + g=kernel.geometry + a0=outgoing_root(λ^2+u.k2[1]) + ag=outgoing_root(λ^2+u.k2[2]) + a=(a0, ag) + dm=u.mu[2]*a0+u.mu[1]*ag + decay=exp(g.logscale-g.hp*root_difference(u.k2[P], a[P], λ) - + g.hq*root_difference(u.k2[Q], a[Q], λ)+logweight) + return decay*u.mu[1]*u.mu[2]/dm +end + +@inline function earth_weighted_spectrum(kernel::EarthSpectrum{:phi, P, Q}, λ, logweight) where { + P, Q} + u=kernel.state + g=kernel.geometry + a0=outgoing_root(λ^2+u.k2[1]) + ag=outgoing_root(λ^2+u.k2[2]) + a=(a0, ag) + dm=u.mu[2]*a0+u.mu[1]*ag + ds=u.sh[2]*a0+u.sh[1]*ag + decay=exp(g.logscale-g.hp*root_difference(u.k2[P], a[P], λ) - + g.hq*root_difference(u.k2[Q], a[Q], λ)+logweight) + return decay*(u.mu[1]*a0+u.mu[2]*ag)/(dm*ds) +end + +@inline function earth_weighted_spectrum(kernel::EarthSpectrum{:voltage, 1, Q}, λ, logweight) where {Q} + u=kernel.state + g=kernel.geometry + a0=outgoing_root(λ^2+u.k2[1]) + ag=outgoing_root(λ^2+u.k2[2]) + a=(a0, ag) + ds=u.sh[2]*a0+u.sh[1]*ag + decay=exp(g.logscale-g.hp*root_difference(u.k2[1], a[1], λ) - + g.hq*root_difference(u.k2[Q], a[Q], λ)+logweight) + return decay*(ag/a0)/ds +end + +@inline function earth_weighted_spectrum(kernel::EarthSpectrum{:voltage, 2, Q}, λ, logweight) where {Q} + u=kernel.state + g=kernel.geometry + a0=outgoing_root(λ^2+u.k2[1]) + ag=outgoing_root(λ^2+u.k2[2]) + a=(a0, ag) + ds=u.sh[2]*a0+u.sh[1]*ag + decay=exp(g.logscale-g.hp*root_difference(u.k2[2], a[2], λ) - + g.hq*root_difference(u.k2[Q], a[Q], λ)+logweight) + return decay*(a0/ag)/ds +end + +@inline function earth_weighted_spectrum( + kernel::EarthSpectrum{ + :air_reference, 1, Q}, λ, logweight) where {Q} + u=kernel.state + g=kernel.geometry + a0=outgoing_root(λ^2+u.k2[1]) + ag=outgoing_root(λ^2+u.k2[2]) + a=(a0, ag) + ds=u.sh[2]*a0+u.sh[1]*ag + decay=exp(g.logscale-g.hp*root_difference(u.k2[1], a[1], λ) - + g.hq*root_difference(u.k2[Q], a[Q], λ)+logweight) + return -decay*ag/(a0*ds) +end + +@inline (kernel::EarthSpectrum)(λ) = earth_weighted_spectrum(kernel, λ, zero(λ)) +function earth_combined_weight(kernel::EarthSpectrum) + g=kernel.geometry + return nominal(g.logscale+(g.hp+g.hq)*maximum(abs, kernel.state.k))>300 +end + +# Evaluate the averaged direct-image term of the Unified formula `formula` with the +# source-column scaling. Both Unified families call it. +function earth_direct(::Union{EarthImpedanceFormulation, EarthAdmittanceFormulation}, + ::Union{Val{:self}, Val{:mutual}}, ::Val{1}, ::Val{2}, state, + pair, radius, average, argument, target_scale, source_scale) + zero(state.jω) +end +function earth_direct(::Union{EarthImpedanceFormulation, EarthAdmittanceFormulation}, + ::Union{Val{:self}, Val{:mutual}}, ::Val{2}, ::Val{1}, state, + pair, radius, average, argument, target_scale, source_scale) + zero(state.jω) +end + +function earth_direct(::Union{EarthImpedanceFormulation, EarthAdmittanceFormulation}, + ::Val{:self}, ::Val{M}, ::Val{M}, u, + pair::EarthPair, r, average, xp, sp, sq) where {M} + h=abs(pair.heights[2])+abs(pair.heights[1]) + d=hypot(pair.separation, h) + D=r + k=u.k[M] + if abs(nominal(k*d)) < 0.5 + difference=iszero(k) ? log(d/D) : special_besselk(0, k*D)-special_besselk(0, k*d) + correction=iszero(k) ? zero(k) : bessel_i0m1(xp)*special_besselk(0, k*d) + return exp(sq)*(difference-correction) + end + direct=special_besselkx(0, k*r)*exp(sq-k*r) + image=average*special_besselkx(0, k*d)*exp(sp+sq-k*d) + return direct-image +end + +function earth_direct(::Union{EarthImpedanceFormulation, EarthAdmittanceFormulation}, + ::Val{:mutual}, ::Val{M}, ::Val{M}, u, + pair::EarthPair, r, average, xp, sp, sq) where {M} + h=abs(pair.heights[2])+abs(pair.heights[1]) + d=hypot(pair.separation, h) + D=hypot(pair.separation, pair.heights[2]-pair.heights[1]) + k=u.k[M] + if abs(nominal(k*d)) < 0.5 + difference=iszero(k) ? log(d/D) : special_besselk(0, k*D)-special_besselk(0, k*d) + return average*exp(sp+sq)*difference + end + return average*(special_besselkx(0, k*D)*exp(sp+sq-k*D) - + special_besselkx(0, k*d)*exp(sp+sq-k*d)) +end + +# Subdivision decisions use nominal branch positions, including degenerate +# equal-media denominators. Physical kernel evaluations retain uncertainty. +function earth_denominator_pole(state) + sh=map(nominal, state.sh) + k2=map(nominal, state.k2) + denominator=sh[2]^2-sh[1]^2 + iszero(denominator) && return nothing + return outgoing_root((sh[1]^2*k2[2]-sh[2]^2*k2[1])/denominator) +end +function earth_valid_pole(state, candidate, ::Type{R}) where {R} + sh=map(nominal, state.sh) + k2=map(nominal, state.k2) + a=sh[2]*outgoing_root(candidate^2+k2[1]) + b=sh[1]*outgoing_root(candidate^2+k2[2]) + return abs(a+b)<=sqrt(eps(R))*(abs(a)+abs(b)) +end + +# The medium and denominator scales are independent. Retain both, even when +# their ratio spans many decades. Bridge them without prescribing a fixed grid. +function earth_spectral_points!(arrays, state, height, separation, radius, angle) + R=typeof(float(nominal(real(state.jω)))) + scales=empty!(arrays.scales) + for k in state.k + iszero(nominal(k)) || push!(scales, R(abs(nominal(k)))) + end + for (p, q) in ((1, 2), (2, 1)) + !iszero(nominal(state.sh[q])) && + push!(scales, R(abs(nominal(state.k[q]*state.sh[p]/state.sh[q])))) + end + decay=R(nominal(height))*cos(angle)-R(nominal(separation+radius))*sin(angle) + decay>0 || throw(DomainError(angle, "spectral contour has a growing geometric weight")) + push!(scales, inv(decay)) + filter!(x->isfinite(x)&&x>zero(R), scales) + unique!(sort!(scales; alg = Base.Sort.QuickSort)) + points=empty!(arrays.points) + push!(points, zero(R)) + for scale in scales + append!(points, (scale/2, scale, 2scale)) + end + rotation=cis(angle) + for k in state.k, sign in (-1, 1) + + earth_spectral_neighbourhood!(points, nominal(sign*im*k)/rotation) + end + pole=earth_denominator_pole(state) + if pole!==nothing + for candidate in (pole, -pole) + earth_valid_pole(state, candidate, R) || continue + earth_spectral_neighbourhood!(points, nominal(candidate)/rotation) + end + end + unique!(sort!(points; alg = Base.Sort.QuickSort)) + # Extra intervals only bridge gaps between physically declared scales. + seeds=empty!(arrays.seeds) + append!(seeds, points) + for i in 2:(length(seeds) - 1) + x=4seeds[i] + while x0 || return points + width=max(width, 64eps(R)*center) + for offset in (-4, -1, 0, 1, 4) + point=center+offset*width + point>0&&isfinite(point) && push!(points, point) + end + return points +end + +# A prescribed Γ can move a branch point into the first quadrant. Keep the +# contour below it. The usual passive Γ=0 roots do not restrict the upper ray. +function earth_contour_angle(state, proposed) + result=proposed + for k in state.k, sign in (-1, 1) + + branch=nominal(sign*im*k) + real(branch)>0&&imag(branch)>0 || continue + result=min(result, atan(imag(branch), real(branch))/2) + end + # Squaring the electric denominator supplies pole candidates. Only a + # candidate satisfying the original, unsquared denominator is a pole. + pole=earth_denominator_pole(state) + if pole!==nothing + for candidate in (pole, -pole) + real(candidate)>0&&imag(candidate)>0 || continue + earth_valid_pole(state, candidate, typeof(proposed)) || continue + result=min(result, atan(imag(candidate), real(candidate))/2) + end + end + return typeof(proposed)(result) +end + +function earth_spectral_term(::Union{EarthImpedanceFormulation, EarthAdmittanceFormulation}, + kind::Val{Kind}, ::Val{P}, ::Val{Q}, state, hp, hq, y, radius, logscale, + method, controls, numerical; context = (;)) where {Kind, P, Q} + R=typeof(float(nominal(real(state.jω)))) + h=hp+hq + angle=min(R(π)/6, atan(R(nominal(h))/(2max(R(nominal(y+radius)), eps(R))))) + angle=earth_contour_angle(state, angle) + points=earth_spectral_points!(numerical.earth_spectrum, state, h, y, radius, angle) + scale=max(R(abs(nominal(state.k[1]))), R(abs(nominal(state.k[2]))), inv(R(nominal(h)))) + g=(; hp, hq, logscale) + kernel=EarthSpectrum{Kind, P, Q, typeof(state), typeof(g)}(state, g) + contour=scale*cis(angle) + integral=SpectralIntegral(t->contour*earth_spectral_value( + kernel, contour*t, h, y, radius)) + points ./= scale + push!(points, one(scale)) + value, + _ = integrate(integral, method, controls, + numerical; + points = points, coordinate_type = R, context = merge(context, (term = Kind,)), + observations = numerical.observations) + return value +end diff --git a/src/engine/earthplan.jl b/src/engine/earthplan.jl new file mode 100644 index 000000000..0866b1c62 --- /dev/null +++ b/src/engine/earthplan.jl @@ -0,0 +1,260 @@ +""" +$(TYPEDEF) + +Hold the earth calculations of a line-parameter computation, fixed before the frequency loop. +The workspace stores it as `plan.earth`, beside its arrays in `buffers.earth`. An earth +calculation is a record with these fields. + +- `impedance` and `admittance`: each is `(formula, pairs)`, the formula of that quantity and + the conductor pairs whose values it publishes. It is `nothing` when the calculation does + not serve the quantity. +- `earth`: the layered earth model or the reduction that its expressions consume. +- `reductions`: on a reduced earth, each is `(expression, options, pairs)`, one expression of + the reduction, its normalized options and the conductor pairs it serves. It is `nothing` on + the layered earth. +- `parts`: each is `(expression, options, pairs)`, one expression of the formula, its + normalized options and the conductor pairs it serves. The expression returns one + coefficient per destination of the calculation, a number for one destination and a tuple + for several. +- `pairs`: each is `(pair, physical, reuse_from)`. It holds the pair on the decided earth and + the physical pair. `reuse_from` is the earlier pair with the same inputs whose computed value + this pair takes when their media agree, or its own index when the pair is computed. +- `media`: the medium of each conductor on the decided earth. + +The plan of one slot serves only that slot's quantity. The merge of the impedance and the +admittance plans gives one calculation both quantities when it can serve them. + +$(TYPEDFIELDS) +""" +struct EarthPlan + "Earth calculations. Their record types differ, and the frequency loop dispatches on them once." + calculations::Tuple +end + +""" +$(TYPEDSIGNATURES) + +Build the calculation of the earth formula `formula` for the physical conductor pairs +`physical[indices]`, on the decided `earth`: the layered earth `model`, or a reduction of it. +The constructor checks each pair and the geometric restrictions of each expression. It checks +that `formula` defines its expressions for the layers of `model`. On a reduction, it checks +that the reduction admits each expression and defines its own expression for each physical +pair. The distinct expressions, with the options that `formula` declares for each, become the +parts. +`geometry` holds the conductor radii and the physical layer of each conductor. + +A formula that computes the whole system adds a method on its own type. +""" +function EarthPlan(formula::Union{EarthImpedanceFormulation, EarthAdmittanceFormulation}, + earth, model::EarthModel, physical::AbstractVector{<:EarthPair}, indices, + geometry::NamedTuple) + earth === model && validate(model, formula) + decided = [EarthPair(physical[index], earth) for index in indices] + expressions = map(decided) do pair + validate(pair) + expression = Expression(formula, pair) + validate(pair, expression) + expression + end + projected = formulation_options(formula, expressions) + for expression in projected.expressions + validate(expression, model.layers) + end + # The reduction's own expressions, their options and the physical pairs each serves. + reductions = if earth === model + nothing + else + rule = EquivalentHomogeneous.rule(earth) + foreach(expression -> validate(rule, expression), expressions) + reduced = [Expression(rule, physical[index]) for index in indices] + normalized = formulation_options(rule, reduced) + foreach(validate, normalized.expressions) + Tuple(map(normalized.expressions, normalized.options) do expression, options + (; expression, options, pairs = findall(==(expression), reduced)) + end) + end + # The pairs that each distinct expression serves. + served = [findall(==(expression), expressions) for expression in projected.expressions] + # A pair takes the value of the nearest earlier pair of its part with the same inputs. + reuse_from = collect(eachindex(decided)) + for members in served, ordinal in eachindex(members) + position = members[ordinal] + for earlier in (ordinal - 1):-1:1 + candidate = members[earlier] + if same_physical_state(formula, decided[position], decided[candidate], geometry) + reuse_from[position] = candidate + break + end + end + end + parts = map(projected.expressions, projected.options, served) do expression, options, pairs + (; expression, options, pairs) + end + pairs = [(pair = decided[position], physical = physical[index], + reuse_from = reuse_from[position]) for (position, index) in enumerate(indices)] + media = earth === model ? geometry.layers : + [layer == 1 ? 1 : 2 for layer in geometry.layers] + published = (; formula, pairs = collect(eachindex(decided))) + impedance = formula isa EarthImpedanceFormulation ? published : nothing + admittance = formula isa EarthAdmittanceFormulation ? published : nothing + calculation = (; impedance, admittance, earth, reductions, parts = Tuple(parts), pairs, + media) + return EarthPlan((calculation,)) +end + +""" +$(TYPEDSIGNATURES) + +Build the earth slot that holds the formula `formula`, for the physical conductor pairs +`physical` of an earth `model`. The slot decides its earth once: an explicit +`equivalent_earth` applies. Without one, a formula that does not admit any layer from 3 to N +consumes the `:default` reduction of a model with N > 2 layers, and every other formula sees +the layered earth. +""" +function EarthPlan(formula::Union{EarthImpedanceFormulation, EarthAdmittanceFormulation}, + model::EarthModel, physical::AbstractVector{<:EarthPair}, geometry::NamedTuple) + layers = length(model.layers) + earth = if formula.equivalent_earth !== nothing + formula.equivalent_earth + elseif layers > 2 && + !any(k -> _admits_layer(Expression(formula, first(physical)), k), 3:layers) + EquivalentHomogeneous.AbstractSequence(LineCableModels.formula(:default)) + else + model + end + return EarthPlan(formula, earth, model, physical, eachindex(physical), geometry) +end + +""" +$(TYPEDSIGNATURES) + +Build the earth slot that holds a recipe of homogeneous formulas, for the physical conductor +pairs `physical` of an earth `model`. The slot decides its earth once: the explicit +reduction that its formulas agree on, or else the `:default` reduction on more than two +layers. Each pair then takes the recipe's formula for its layers on that earth, and each +distinct formula builds one calculation. + +# Errors + +- Throws `ArgumentError` when a formula of the recipe admits a layer above 2, or when the + formulas give different equivalent earths. +""" +function EarthPlan(recipe::NamedTuple, model::EarthModel, + physical::AbstractVector{<:EarthPair}, geometry::NamedTuple) + selected = Tuple(leaf for leaf in recipe if leaf !== nothing) + layers = length(model.layers) + for leaf in selected + expression = Expression(leaf, first(physical)) + any(k -> _admits_layer(expression, k), 3:max(3, layers)) && throw(ArgumentError( + "formula :$(formula_id(leaf)) is multilayer and is used alone")) + end + explicit = unique(leaf.equivalent_earth for leaf in selected + if leaf.equivalent_earth !== nothing) + length(explicit) > 1 && throw(ArgumentError( + "the formulas of an earth recipe give different equivalent earths")) + earth = !isempty(explicit) ? only(explicit) : + layers > 2 ? EquivalentHomogeneous.AbstractSequence(LineCableModels.formula(:default)) : + model + leaves = [Expression(recipe, EarthPair(pair, earth)).selection for pair in physical] + calculations = NamedTuple[] + for leaf in unique(leaves) + indices = findall(value -> value === leaf, leaves) + append!(calculations, + EarthPlan(leaf, earth, model, physical, indices, geometry).calculations) + end + return EarthPlan(Tuple(calculations)) +end + +""" +$(TYPEDSIGNATURES) + +Merge the earth plans of the impedance and the admittance slot. The first plan's calculations +compute the impedance and the second plan's the admittance. An impedance and an admittance +calculation become one calculation serving both when their formulas have the same physical +state and their parts the same options. +""" +function EarthPlan(impedance::EarthPlan, admittance::EarthPlan) + calculations = NamedTuple[] + remaining = collect(NamedTuple, admittance.calculations) + for calculation in impedance.calculations + index = findfirst(remaining) do candidate + same_physical_state(calculation.impedance.formula, candidate.admittance.formula) && + same_physical_state(first(calculation.parts).options.data, + first(candidate.parts).options.data) + end + if index === nothing + push!(calculations, calculation) + else + push!(calculations, + merge(calculation, (admittance = remaining[index].admittance,))) + deleteat!(remaining, index) + end + end + append!(calculations, remaining) + return EarthPlan(Tuple(calculations)) +end + +""" +$(TYPEDSIGNATURES) + +Whether `formula` gives the conductor pairs `a` and `b` the same value when their media +agree. `geometry` holds the conductor radii. By default the pairs' destination indices take +part, so distinct pairs are computed separately. A formula whose arithmetic does not read +them compares the inputs it reads instead. +""" +function same_physical_state( + ::Union{EarthImpedanceFormulation, EarthAdmittanceFormulation}, + a::EarthPair, b::EarthPair, geometry::NamedTuple) + return same_physical_state((a.row, a.column), (b.row, b.column)) +end + +""" +$(TYPEDSIGNATURES) + +Extend `buffers` with `earth`, the arrays of the earth calculations of `earth`. + +- `static` and `evaluated` hold the earth's layer properties, static and at each frequency. + `evaluated` is `nothing` unless a calculation consumes the evaluated layers. +- `calculations` holds, for each calculation, the `rho`, `epsilon` and `mu` of the media that + each of its conductor pairs sees, one column per pair, and the layer thicknesses of a + layered earth. +- `pairs` holds, for the conductor pairs, the pair whose computed value each one took at the + current frequency, the ranges of its integrals and warnings, and the warning records. + +The integration warnings, when a formula integrates, share the warning records: the +formulas of the calculations provision their own arrays first, through the formulation's +methods. +""" +function initialize_buffers(earth::EarthPlan, ::Type{T}, input, plan, buffers) where {T} + layers = input.earth.layers + static = (rho = collect(getproperty.(layers, :rho)), + eps_r = collect(getproperty.(layers, :eps_r)), + mu_r = collect(getproperty.(layers, :mu_r))) + needed = any(calculation -> !(calculation.earth isa EquivalentHomogeneous.BeforeFD), + earth.calculations) + evaluated = needed ? + map(_ -> Matrix{eltype(input.freq)}(undef, length(layers), input.n_frequencies), + static) : nothing + Evaluated = NamedTuple{(:rho, :eps_r, :mu_r), NTuple{3, Matrix{eltype(input.freq)}}} + # The layered earth keeps every physical layer, and its thicknesses when it has interior + # layers. A reduction keeps air and one equivalent medium. + calculations = map(earth.calculations) do calculation + count = calculation.earth isa EarthModel ? length(layers) : 2 + columns = length(calculation.pairs) + thickness = count > 2 ? T[layer.thickness for layer in layers] : nothing + (rho = Matrix{T}(undef, count, columns), epsilon = Matrix{T}(undef, count, columns), + mu = Matrix{T}(undef, count, columns), thickness) + end + pairs = (representatives = zeros(Int, input.n_cables^2), + integral_ranges = Vector{UnitRange{Int}}(undef, input.n_cables^2), + warning_ranges = Vector{UnitRange{Int}}(undef, input.n_cables^2), + warnings = NamedTuple[]) + # The record type does not depend on the calculations, nor on whether the evaluated + # layers are needed. + Record = NamedTuple{(:static, :evaluated, :calculations, :pairs), + Tuple{typeof(static), Union{Nothing, Evaluated}, Tuple, typeof(pairs)}} + buffers = merge(buffers, (earth = Record((static, evaluated, calculations, pairs)),)) + haskey(buffers, :quadrature) || return buffers + return merge(buffers, (quadrature = merge(buffers.quadrature, + (warnings = buffers.earth.pairs.warnings,)),)) +end diff --git a/src/engine/earthreturn.jl b/src/engine/earthreturn.jl new file mode 100644 index 000000000..c310bb5f1 --- /dev/null +++ b/src/engine/earthreturn.jl @@ -0,0 +1,136 @@ +function earth!(workspace::LineParametersWorkspace, frequency::Int, + calculations::Tuple = workspace.plan.earth.calculations, + materials::Tuple = workspace.buffers.earth.calculations) + foreach(calculations, materials) do calculation, material + earth!(calculation, material, workspace, frequency) + end + return workspace +end + +# Build the calculation's Functor at the frequency, evaluate its parts into the destinations, +# convert them when the formula requires it, then publish each served quantity's pairs. +function earth!(calculation::NamedTuple, materials::NamedTuple, workspace, frequency::Int) + formula = something(calculation.impedance, calculation.admittance).formula + functor = Functor(formula, (; jω = workspace.input.jω[frequency], frequency, + materials.rho, materials.epsilon, materials.mu, materials.thickness, + calculation.media); workspace) + earth!(functor.input.destinations, calculation, functor, workspace) + physical = earth!(formula, functor, workspace) + if calculation.impedance !== nothing && haskey(physical, :impedance) + destination = workspace.buffers.Zearth + for index in calculation.impedance.pairs + pair = calculation.pairs[index].pair + destination[pair.row, pair.column] = physical.impedance[pair.row, pair.column] + end + end + if calculation.admittance !== nothing && haskey(physical, :admittance) + destination = workspace.buffers.Pearth + for index in calculation.admittance.pairs + pair = calculation.pairs[index].pair + destination[pair.row, pair.column] = physical.admittance[pair.row, pair.column] + end + end + return workspace +end + +# The parts of a formula that does not solve a whole system wrote the physical coefficients +# into `Zearth` or `Pearth`, so the conversion does not return any matrix. +earth!(::Union{EarthImpedanceFormulation, EarthAdmittanceFormulation}, ::Functor, workspace) = (;) + +""" +$(TYPEDSIGNATURES) + +Evaluate the parts of an earth calculation at the frequency of `functor` and distribute their +scalar coefficients into the aligned matrices `destinations`. A part gives its expression and +options. The expression returns one coefficient per destination, a number for one destination +and a tuple for several. The calculation's pairs give each pair's source-target geometry and +the earlier pair with the same inputs, `reuse_from`. + +A pair takes the value of that earlier pair only when their current media agree under +`same_physical_state`. `buffers.earth.pairs` records, at each frequency, the pair whose +computed value each pair took. Every logical integral and warning retains its receiving row +and source column. +""" +function earth!(destinations::Tuple{Vararg{AbstractMatrix}}, calculation::NamedTuple, + functor::Functor, workspace) + computed = workspace.buffers.earth.pairs + length(computed.representatives) >= length(calculation.pairs) || + throw(DimensionMismatch("earth interaction scratch is too small")) + empty!(computed.warnings) + foreach(calculation.parts) do part + earth!(destinations, part, calculation, functor, workspace) + end + empty!(computed.warnings) + return destinations +end + +function earth!(destinations, part, calculation, functor, workspace) + computed = workspace.buffers.earth.pairs + observations = workspace.buffers.observations + materials = (functor.input.rho, functor.input.epsilon, functor.input.mu) + for index in part.pairs + entry = calculation.pairs[index] + pair = entry.pair + # Follow the earlier pairs with the same inputs until one whose media agree. + current = index + source = entry.reuse_from + while source != current + same = let source = source + all( + values -> same_physical_state(@view(values[:, index]), + @view(values[:, source])), + materials) + end + same && break + current = source + source = calculation.pairs[source].reuse_from + end + if source == current + computed.representatives[index] = index + first_integral = observations === nothing ? 1 : length(observations)+1 + first_warning = length(computed.warnings)+1 + point = Functor(functor, (; pair, entry.physical, + rho = @view(functor.input.rho[:, index]), + epsilon = @view(functor.input.epsilon[:, index]), + mu = @view(functor.input.mu[:, index]), part.options)) + validate(point.input, functor.formula) + coefficients = part.expression(point, workspace) + values = coefficients isa Tuple ? coefficients : (coefficients,) + all(value -> value isa Number && isfinite(value), values) || throw(DomainError( + values, "earth coefficients must be finite scalars")) + converted = map(value -> oftype(functor.input.jω, value), values) + length(converted) == length(destinations) || + throw(DimensionMismatch("earth coefficients must match their destinations")) + foreach(destinations, converted) do destination, value + destination[pair.row, pair.column] = value + end + computed.integral_ranges[index] = first_integral:(observations === nothing ? 0 : + length(observations)) + computed.warning_ranges[index] = first_warning:length(computed.warnings) + else + representative = computed.representatives[source] + computed.representatives[index] = representative + previous_pair = calculation.pairs[representative].pair + for destination in destinations + destination[pair.row, pair.column] = destination[previous_pair.row, previous_pair.column] + end + context = (receiver = pair.row, source = pair.column) + if observations !== nothing + for position in computed.integral_ranges[representative] + record = observations[position] + integral_context = record.context isa NamedTuple ? + merge(record.context, context) : record.context + push!(observations, merge(record, (context = integral_context,))) + end + end + for position in computed.warning_ranges[representative] + record = computed.warnings[position] + integral_context = record.context isa NamedTuple ? + merge(record.context, context) : record.context + record_integral!(nothing, nothing, record.value, record.estimated_error, + record.controls, integral_context) + end + end + end + return destinations +end diff --git a/src/engine/ehem/EHEM.jl b/src/engine/ehem/EHEM.jl deleted file mode 100644 index bf63e9c83..000000000 --- a/src/engine/ehem/EHEM.jl +++ /dev/null @@ -1,26 +0,0 @@ -""" - LineCableModels.Engine.EHEM - -# Dependencies - -$(IMPORTS) - -# Exports - -$(EXPORTS) -""" -module EHEM - -# Export public API -export EnforceLayer - -# Module-specific dependencies -using ...Commons -using ...EarthProps: EarthModel -import ...Commons: get_description -import ..Engine: AbstractEHEMFormulation -using Measurements - -include("enforcelayer.jl") - -end # module EHEM \ No newline at end of file diff --git a/src/engine/ehem/enforcelayer.jl b/src/engine/ehem/enforcelayer.jl deleted file mode 100644 index 86da5f684..000000000 --- a/src/engine/ehem/enforcelayer.jl +++ /dev/null @@ -1,136 +0,0 @@ -""" -$(TYPEDEF) - -An EHEM formulation that creates a homogeneous earth model by enforcing the properties of a single, specified layer from a multi-layer model. - -# Attributes -$(TYPEDFIELDS) -""" -struct EnforceLayer <: AbstractEHEMFormulation - "Index of the earth layer to enforce. `-1` selects the bottommost layer." - layer::Int - - @doc """ - $(TYPEDSIGNATURES) - - Constructs an `EnforceLayer` instance. - - # Arguments - - `layer::Int`: The index of the layer to enforce. - - `-1` (default): Enforces the properties of the bottommost earth layer. - - `2`: Enforces the properties of the topmost earth layer (the one directly below the air). - - `> 2`: Enforces the properties of a specific layer by its index. - """ - function EnforceLayer(; layer::Int = -1) - @assert (layer == -1 || layer >= 2) "Invalid layer index. Must be -1 (bottommost) or >= 2." - new(layer) - end -end - -function get_description(f::EnforceLayer) - if f.layer == -1 - return "Assume bottom layer" - elseif f.layer == 2 - return "Assume top earth layer" - else - return "Assume layer $(f.layer)" - end -end - -""" -$(TYPEDSIGNATURES) - -Functor implementation for `EnforceLayer`. - -Builds a 2-layer (air + one enforced earth layer) data pack as three matrices -ρ, ε, μ of size (2 × n_freq), already converted to `T`. - -# Returns -- `(ρ, ε, μ) :: (Matrix{T}, Matrix{T}, Matrix{T})` - with row 1 = air, row 2 = enforced earth layer. -""" -function (f::EnforceLayer)( - model::EarthModel, - freq::AbstractVector{<:REALSCALAR}, - ::Type{T}, -) where {T <: REALSCALAR} - - nL = length(model.layers) - nF = length(freq) - - layer_idx = f.layer == -1 ? nL : f.layer - (2 <= layer_idx <= nL) || error( - "Invalid layer index: $layer_idx. Model has $nL layers (including air). " * - "Valid earth layer indices are 2:$nL.", - ) - - Lair = model.layers[1] - Lsel = model.layers[layer_idx] - - ρ = Matrix{T}(undef, 2, nF) - ε = similar(ρ) - μ = similar(ρ) - - @inbounds for j in 1:nF - ρ[1, j] = T(Lair.rho_g[j]) - ε[1, j] = T(Lair.eps_g[j]) - μ[1, j] = T(Lair.mu_g[j]) - - ρ[2, j] = T(Lsel.rho_g[j]) - ε[2, j] = T(Lsel.eps_g[j]) - μ[2, j] = T(Lsel.mu_g[j]) - end - - return ρ, ε, μ -end - -# """ -# $(TYPEDSIGNATURES) - -# Functor implementation for `EnforceLayer`. - -# Takes a multi-layer `EarthModel` and returns a new two-layer model (air + one effective earth layer) based on the properties of the layer specified in the `EnforceLayer` instance. - -# # Returns -# - A `Vector{EarthLayer}` containing two layers: the original air layer and the selected earth layer. -# """ -# function (f::EnforceLayer)( -# model::EarthModel, -# freq::Vector{<:REALSCALAR}, -# T::DataType, -# ) -# num_layers = length(model.layers) - -# # Determine the index of the layer to select -# layer_idx = f.layer == -1 ? num_layers : f.layer - -# # Validate the chosen index -# if !(2 <= layer_idx <= num_layers) -# Base.error( -# "Invalid layer index: $layer_idx. The model only has $num_layers layers (including air). Valid earth layer indices are from 2 to $num_layers.", -# ) -# end - -# # The air layer is always the first layer in the original model -# air_layer = model.layers[1] - -# # The enforced earth layer is the one at the selected index -# enforced_layer = model.layers[layer_idx] - -# # Create a NamedTuple for the air layer with type-promoted property vectors -# air_data = ( -# rho_g = T.(air_layer.rho_g), -# eps_g = T.(air_layer.eps_g), -# mu_g = T.(air_layer.mu_g), -# ) - -# # Create a NamedTuple for the enforced earth layer -# earth_data = ( -# rho_g = T.(enforced_layer.rho_g), -# eps_g = T.(enforced_layer.eps_g), -# mu_g = T.(enforced_layer.mu_g), -# ) - -# # Return a new vector containing only these two layers -# return [air_data, earth_data] -# end \ No newline at end of file diff --git a/src/engine/fem/FEM.jl b/src/engine/fem/FEM.jl deleted file mode 100644 index 8b1b18052..000000000 --- a/src/engine/fem/FEM.jl +++ /dev/null @@ -1,73 +0,0 @@ -""" - LineCableModels.Engine.FEM - -The [`FEM`](@ref) module provides functionality for generating geometric meshes for cable cross-sections, assigning physical properties, and preparing the system for electromagnetic simulation within the [`LineCableModels.jl`](index.md) package. - -# Overview - -- Defines core types [`FEMFormulation`](@ref), and [`FEMWorkspace`](@ref) for managing simulation parameters and state. -- Implements a physical tag encoding system (CCOGYYYYY scheme for cable components, EPFXXXXX for domain regions). -- Provides primitive drawing functions for geometric elements. -- Creates a two-phase workflow: creation → fragmentation → identification. -- Maintains all state in a structured [`FEMWorkspace`](@ref) object. - -# Dependencies - -$(IMPORTS) - -# Exports - -$(EXPORTS) -""" -module FEM - -# Export public API -export MeshTransition, calc_domain_size -export compute!, preview_results -export FormulationSet, Electrodynamics, Darwin - -# Module-specific dependencies -using ...Commons -using ...Materials -using ...EarthProps -using ...DataModel -using ...Engine -import ...Engine: kronify, reorder_M, reorder_indices, merge_bundles!, FormulationSet, - AbstractFormulationSet, AbstractImpedanceFormulation, AbstractAdmittanceFormulation, - compute! -import ...Commons: PhaseDomain, ModalDomain, LineParamsDomain, domain -import ...Engine: AbstractFormulationOptions, LineParamOptions, build_options, _COMMON_SYMS -import ...DataModel: AbstractCablePart, AbstractConductorPart, AbstractInsulatorPart -using ...Utils: - display_path, set_verbosity!, is_headless, to_nominal, symtrans!, symtrans, - line_transpose! -using Measurements -using LinearAlgebra -using Colors -using Makie: Point, Point2f # otherwise will require adding GeometryBasics as a dependency -# FEM specific dependencies -using Gmsh -using GetDP -using GetDP: Problem, get_getdp_executable, add! - - -include("types.jl") -include("lineparamopts.jl") # Line parameter options - -# Include auxiliary files -include("meshtransitions.jl") # Mesh transition objects -include("problemdefs.jl") # Problem definitions -include("workspace.jl") # Workspace functions -include("encoding.jl") # Tag encoding schemes -include("drawing.jl") # Primitive drawing functions -include("identification.jl") # Entity identification -include("mesh.jl") # Mesh generation -include("materialprops.jl") # Material handling -include("helpers.jl") # Various utilities -include("visualization.jl") # Visualization functions -include("space.jl") # Domain creation functions -include("cable.jl") # Cable geometry creation functions -include("solver.jl") # Solver functions -include("base.jl") # Base namespace extensions - -end # module FEM diff --git a/src/engine/fem/base.jl b/src/engine/fem/base.jl deleted file mode 100644 index 47b415fd7..000000000 --- a/src/engine/fem/base.jl +++ /dev/null @@ -1,2 +0,0 @@ -Base.eltype(::FEMWorkspace{T}) where {T} = T -Base.eltype(::Type{FEMWorkspace{T}}) where {T} = T diff --git a/src/engine/fem/cable.jl b/src/engine/fem/cable.jl deleted file mode 100644 index d540f4307..000000000 --- a/src/engine/fem/cable.jl +++ /dev/null @@ -1,699 +0,0 @@ -""" -Cable geometry creation functions for the FEMTools.jl module. -These functions handle the creation of cable components. -""" - -""" -$(TYPEDSIGNATURES) - -Create the cable geometry for all cables in the system. - -# Arguments - -- `workspace`: The [`FEMWorkspace`](@ref) containing the model parameters. - -# Returns - -- Nothing. Updates the conductors and insulators vectors in the workspace. - -# Examples - -```julia -$(FUNCTIONNAME)(workspace) -``` -""" -function make_cable_geometry(workspace::FEMWorkspace) - - # Get the cable system - cable_system = workspace.problem_def.system - - # Process each cable in the system - for (cable_idx, cable_position) in enumerate(cable_system.cables) - @info "Processing cable $(cable_idx) at position ($(cable_position.horz), $(cable_position.vert))" - - # Get the cable design - cable_design = cable_position.design_data - - # Get the phase assignments - phase_assignments = cable_position.conn - - # Process each component in the cable - for (comp_idx, component) in enumerate(cable_design.components) - # Get the component ID - comp_id = component.id - - # Get the phase assignment for this component - phase = comp_idx <= length(phase_assignments) ? phase_assignments[comp_idx] : 0 - - @debug "Processing component $(comp_id) (phase $(phase))" - - # Process conductor group - if !isnothing(component.conductor_group) - @debug "Processing conductor group for component $(comp_id)" - - # Process each layer in the conductor group - for (layer_idx, layer) in enumerate(component.conductor_group.layers) - @debug "Processing conductor layer $(layer_idx)" - - # Create the cable part - _make_cablepart!( - workspace, - layer, - cable_idx, - comp_idx, - comp_id, - phase, - layer_idx, - ) - end - end - - # Process insulator group - if !isnothing(component.insulator_group) - @debug "Processing insulator group for component $(comp_id)" - - # Process each layer in the insulator group - for (layer_idx, layer) in enumerate(component.insulator_group.layers) - @debug "Processing insulator layer $(layer_idx)" - - # Create the cable part - _make_cablepart!( - workspace, - layer, - cable_idx, - comp_idx, - comp_id, - phase, - layer_idx, - ) - end - end - end - end - - @info "Cable geometry created" -end - -""" -$(TYPEDSIGNATURES) - -Create a cable part entity for all tubular shapes. - -# Arguments - -- `workspace`: The [`FEMWorkspace`](@ref) containing the model parameters. -- `part`: The [`AbstractCablePart`](@ref) to create. -- `cable_idx`: The index of the cable. -- `comp_idx`: The index of the component. -- `comp_id`: The ID of the component. -- `phase`: The phase assignment. -- `layer_idx`: The index of the layer. - -# Returns - -- Nothing. Updates the conductors or insulators vector in the workspace. - -# Examples - -```julia -$(FUNCTIONNAME)(workspace, part, 1, 1, "core", 1, 1) -``` -""" -function _make_cablepart!(workspace::FEMWorkspace, part::AbstractCablePart, - cable_idx::Int, comp_idx::Int, comp_id::String, - phase::Int, layer_idx::Int) - - # Get the cable definition - cable_position = workspace.problem_def.system.cables[cable_idx] - - # Get the center coordinates - x_center = to_nominal(cable_position.horz) - y_center = to_nominal(cable_position.vert) - - # Determine material group directly from part type - material_group = get_material_group(part) - - # Get or register material ID - material_id = get_or_register_material_id(workspace, part.material_props) - - # Create physical tag with new encoding scheme - physical_group_tag = encode_physical_group_tag( - 1, # Surface type 1 = cable component - cable_idx, # Cable number - comp_idx, # Component number - material_group, # Material group from part type - material_id, # Material ID from registry - ) - - # Create physical name - part_type = lowercase(string(nameof(typeof(part)))) - elementary_name = create_cable_elementary_name( - cable_idx = cable_idx, - component_id = comp_id, - group_type = material_group, - part_type = part_type, - layer_idx = layer_idx, - phase = phase, - ) - - # Extract parameters - r_in = to_nominal(part.r_in) - r_ex = to_nominal(part.r_ex) - - # Calculate mesh size for this part - if part isa AbstractConductorPart - num_elements = workspace.formulation.elements_per_length_conductor - elseif part isa Insulator - num_elements = workspace.formulation.elements_per_length_insulator - elseif part isa Semicon - num_elements = workspace.formulation.elements_per_length_semicon - end - - mesh_size_current = - _calc_mesh_size(r_in, r_ex, part.material_props, num_elements, workspace) - - # Calculate mesh size for the next part - num_layers = - length(cable_position.design_data.components[comp_idx].conductor_group.layers) - next_part = - layer_idx < num_layers ? - cable_position.design_data.components[comp_idx].conductor_group.layers[layer_idx+1] : - nothing - - if !isnothing(next_part) - next_radius_in = to_nominal(next_part.r_in) - next_radius_ext = to_nominal(next_part.r_ex) - mesh_size_next = _calc_mesh_size( - next_radius_in, - next_radius_ext, - next_part.material_props, - num_elements, - workspace, - ) - if next_part isa Insulator - mesh_size = min(mesh_size_current, mesh_size_next) - else - mesh_size = max(mesh_size_current, mesh_size_next) - end - else - mesh_size = mesh_size_current - end - - num_points_circumference = workspace.formulation.points_per_circumference - - # Create annular shape and assign marker - if r_in ≈ 0 - # Solid disk - _, _, marker, _ = - draw_disk(x_center, y_center, r_ex, mesh_size, num_points_circumference) - else - # Annular shape - _, _, marker, _ = draw_annular( - x_center, - y_center, - r_in, - r_ex, - mesh_size, - num_points_circumference, - ) - end - - # Create entity data - core_data = CoreEntityData(physical_group_tag, elementary_name, mesh_size) - entity_data = CablePartEntity(core_data, part) - - # Add to workspace in the unassigned container for subsequent processing - workspace.unassigned_entities[marker] = entity_data - - # Add physical groups to the workspace - register_physical_group!(workspace, physical_group_tag, part.material_props) - - - -end - -""" -$(TYPEDSIGNATURES) - -Specialized method to create individual wire entities for `CircStrands` parts. - -# Arguments - -- `workspace`: The [`FEMWorkspace`](@ref) containing the model parameters. -- `part`: The [`CircStrands`](@ref) to create. -- `cable_idx`: The index of the cable. -- `comp_idx`: The index of the component. -- `comp_id`: The ID of the component. -- `phase`: The phase assignment. -- `layer_idx`: The index of the layer. - -# Returns - -- Nothing. Updates the conductors vector in the workspace. - -# Examples - -```julia -$(FUNCTIONNAME)(workspace, part, 1, 1, "core", 1, 1) -``` -""" -function _make_cablepart!(workspace::FEMWorkspace, part::CircStrands, - cable_idx::Int, comp_idx::Int, comp_id::String, - phase::Int, layer_idx::Int) - - # Get the cable definition - cable_position = workspace.problem_def.system.cables[cable_idx] - - # Get the center coordinates - x_center = to_nominal(cable_position.horz) - y_center = to_nominal(cable_position.vert) - - # Determine material group directly from part type - material_group = get_material_group(part) - - # Get or register material ID - material_id = get_or_register_material_id(workspace, part.material_props) - - # Create physical tag with new encoding scheme - physical_group_tag = encode_physical_group_tag( - 1, # Surface type 1 = cable component - cable_idx, # Cable number - comp_idx, # Component number - material_group, # Material group from part type - material_id, # Material ID from registry - ) - - # -------- First handle the wires - - # Create physical name - part_type = lowercase(string(nameof(typeof(part)))) - - # Extract parameters - r_in = to_nominal(part.r_in) - r_ex = to_nominal(part.r_ex) - - radius_wire = to_nominal(part.radius_wire) - num_wires = part.num_wires - - - # Calculate mesh size for this part - num_elements = workspace.formulation.elements_per_length_conductor - mesh_size_current = - _calc_mesh_size(r_in, r_ex, part.material_props, num_elements, workspace) - - # Calculate mesh size for the next part - num_layers = - length(cable_position.design_data.components[comp_idx].conductor_group.layers) - next_part = - layer_idx < num_layers ? - cable_position.design_data.components[comp_idx].conductor_group.layers[layer_idx+1] : - nothing - - if !isnothing(next_part) - next_radius_in = to_nominal(next_part.r_in) - next_radius_ext = to_nominal(next_part.r_ex) - mesh_size_next = _calc_mesh_size( - next_radius_in, - next_radius_ext, - next_part.material_props, - num_elements, - workspace, - ) - mesh_size = max(mesh_size_current, mesh_size_next) - else - mesh_size = mesh_size_current - end - - # A single wire without air gaps - is_single_wire = - (num_wires == 1) && (isnothing(next_part) || !(next_part isa CircStrands)) - - - - num_points_circumference = workspace.formulation.points_per_circumference - - # Calculate wire positions - function _calc_circstrands_coords( - num_wires::Number, - # radius_wire::Number, - r_in::Number, - r_ex::Number; - C = (0.0, 0.0), - ) - wire_coords = [] # Global coordinates of all wires - - lay_radius = num_wires == 1 ? 0 : (r_in + r_ex) / 2 - - # Calculate the angle between each wire - angle_step = 2 * π / num_wires - for i in 0:(num_wires-1) - angle = i * angle_step - x = C[1] + lay_radius * cos(angle) - y = C[2] + lay_radius * sin(angle) - push!(wire_coords, (x, y)) # Add wire center - end - return wire_coords - end - - wire_positions = - _calc_circstrands_coords(num_wires, r_in, r_ex, C = (x_center, y_center)) - - # Create wires - TOL = is_single_wire ? 0 : 5e-6 # Shrink the radius to avoid overlapping boundaries, this must be greater than Gmsh geometry tolerance - for (wire_idx, (wx, wy)) in enumerate(wire_positions) - - _, _, marker, _ = - draw_disk(wx, wy, radius_wire - TOL, mesh_size, num_points_circumference) - - # Create wire name - elementary_name = create_cable_elementary_name( - cable_idx = cable_idx, - component_id = comp_id, - group_type = material_group, - part_type = part_type, - layer_idx = layer_idx, - phase = phase, - wire_idx = wire_idx, - ) - - # Create entity data - core_data = CoreEntityData(physical_group_tag, elementary_name, mesh_size) - entity_data = CablePartEntity(core_data, part) - - # Add to workspace - workspace.unassigned_entities[marker] = entity_data - end - # Add physical groups to the workspace - register_physical_group!(workspace, physical_group_tag, part.material_props) - - # Handle CircStrands outermost boundary - mesh_size = (r_ex - r_in) - if !(next_part isa CircStrands) && !isnothing(next_part) - # step_angle = 2 * pi / num_wires - add_mesh_points( - r_in = r_ex, - r_ex = r_ex, - theta_0 = 0, - theta_1 = 2 * pi, - mesh_size = mesh_size, - num_points_ang = num_points_circumference, - num_points_rad = 0, - C = (x_center, y_center), - theta_offset = 0, #step_angle / 2 - ) - end - - # Create air gaps for: - # - Multiple wires (always) - # - Single wire IF next part is a CircStrands - # Skip ONLY for single wire when next part is not a CircStrands - if !is_single_wire - # Air gaps will be determined from the boolean fragmentation operation and do not need to be drawn. Only the markers are needed. - markers_air_gap = get_air_gap_markers(num_wires, radius_wire, r_in) - - # Adjust air gap markers to cable center - for marker in markers_air_gap - marker[1] += x_center - marker[2] += y_center - end - - # Determine material group - air gaps map to insulators - material_group = 2 - - # Get air material - air_material = get_air_material(workspace) - - # Get or register material ID - material_id = get_or_register_material_id(workspace, air_material) - - # Create physical tag with new encoding scheme - physical_group_tag_air_gap = encode_physical_group_tag( - 1, # Surface type 1 = cable component - cable_idx, # Cable number - comp_idx, # Component number - material_group, # Material group from part type - material_id, # Material ID from registry - ) - - for marker in markers_air_gap - # elementary names are not assigned to the air gaps because they are not drawn and appear as a result of the boolean operation - core_data = CoreEntityData(physical_group_tag_air_gap, "", mesh_size) - entity_data = SurfaceEntity(core_data, air_material) - - # Add to unassigned entities with type information - workspace.unassigned_entities[marker] = entity_data - end - - # Add physical groups to the workspace - register_physical_group!(workspace, physical_group_tag_air_gap, air_material) - end -end - -""" -$(TYPEDSIGNATURES) - -Specialized method to create the geometry for a `Sector` conductor. -""" -function _make_cablepart!(workspace::FEMWorkspace, part::Sector, - cable_idx::Int, comp_idx::Int, comp_id::String, - phase::Int, layer_idx::Int) - - # Get the cable's center coordinates from the workspace - cable_position = workspace.problem_def.system.cables[cable_idx] - x_center = to_nominal(cable_position.horz) - y_center = to_nominal(cable_position.vert) - - # The vertices in the Sector object are already rotated and relative to the cable's origin. - # We just need to translate them to the cable's position in the system. - translated_vertices = [Point(v[1] + x_center, v[2] + y_center) for v in part.vertices] - - # Calculate mesh size for this part - num_elements = workspace.formulation.elements_per_length_conductor - mesh_size = _calc_mesh_size(part.r_in, part.r_ex, part.material_props, num_elements, workspace) - - # Create the polygon in Gmsh - @debug "the translated vertices are $translated_vertices \n they are of type $(typeof(translated_vertices))" - surface_tag, marker = draw_polygon(translated_vertices, mesh_size) - - # --- The rest of this function is similar to the other _make_cablepart! methods --- - - # Get material group (1 for conductor) - material_group = get_material_group(part) - material_id = get_or_register_material_id(workspace, part.material_props) - - # Create physical tag - physical_group_tag = encode_physical_group_tag(1, cable_idx, comp_idx, material_group, material_id) - - # Create a descriptive name for the entity - elementary_name = create_cable_elementary_name( - cable_idx=cable_idx, component_id=comp_id, group_type=material_group, - part_type="sector", layer_idx=layer_idx, phase=phase - ) - - # Create the entity data and add it to the workspace's unassigned entities - core_data = CoreEntityData(physical_group_tag, elementary_name, mesh_size) - entity_data = CablePartEntity(core_data, part) - workspace.unassigned_entities[marker] = entity_data - - # Register the physical group - register_physical_group!(workspace, physical_group_tag, part.material_props) -end - - -""" -$(TYPEDSIGNATURES) - -Specialized method to create the geometry for a `SectorInsulator`. -""" -function _make_cablepart!(workspace::FEMWorkspace, part::SectorInsulator, - cable_idx::Int, comp_idx::Int, comp_id::String, - phase::Int, layer_idx::Int) - - cable_position = workspace.problem_def.system.cables[cable_idx] - x_center = to_nominal(cable_position.horz) - y_center = to_nominal(cable_position.vert) - - # Translate the vertices for both outer and inner boundaries - outer_vertices_translated = [Point(v[1] + x_center, v[2] + y_center) for v in part.outer_vertices] - inner_vertices_translated = [Point(v[1] + x_center, v[2] + y_center) for v in part.inner_sector.vertices] - - # Calculate mesh size for this part - num_elements = workspace.formulation.elements_per_length_insulator - mesh_size = _calc_mesh_size(part.r_in, part.r_ex, part.material_props, num_elements, workspace) - - # Create the polygon with a hole using our new drawing primitive - surface_tag, marker = draw_polygon_with_hole(outer_vertices_translated, inner_vertices_translated, mesh_size) - - # --- The rest is similar to the Sector method --- - - material_group = get_material_group(part) - material_id = get_or_register_material_id(workspace, part.material_props) - physical_group_tag = encode_physical_group_tag(1, cable_idx, comp_idx, material_group, material_id) - - elementary_name = create_cable_elementary_name( - cable_idx=cable_idx, component_id=comp_id, group_type=material_group, - part_type="sector_insulator", layer_idx=layer_idx, phase=phase - ) - - core_data = CoreEntityData(physical_group_tag, elementary_name, mesh_size) - entity_data = CablePartEntity(core_data, part) - workspace.unassigned_entities[marker] = entity_data - - register_physical_group!(workspace, physical_group_tag, part.material_props) -end - -""" -Create a cable part entity for all tubular shapes. This function is specialized for `Tubular` parts. - -# Arguments - -- `workspace`: The [`FEMWorkspace`](@ref) containing the model parameters. -- `part`: The [`Tubular`](@ref) to create. -- `cable_idx`: The index of the cable. -- `comp_idx`: The index of the component. -- `comp_id`: The ID of the component. -- `phase`: The phase assignment. -- `layer_idx`: The index of the layer. - -# Returns - -- Nothing. Updates the conductors or insulators vector in the workspace. - -# Examples - -```julia -$(FUNCTIONNAME)(workspace, part, 1, 1, "core", 1, 1) -``` -""" -function _make_cablepart!(workspace::FEMWorkspace, part::Tubular, - cable_idx::Int, comp_idx::Int, comp_id::String, - phase::Int, layer_idx::Int) - - # Get the cable definition - cable_position = workspace.problem_def.system.cables[cable_idx] - - # Get the center coordinates - x_center = to_nominal(cable_position.horz) - y_center = to_nominal(cable_position.vert) - - # Determine material group directly from part type - material_group = get_material_group(part) - - # Get or register material ID - material_id = get_or_register_material_id(workspace, part.material_props) - - # Create physical tag with new encoding scheme - physical_group_tag = encode_physical_group_tag( - 1, # Surface type 1 = cable component - cable_idx, # Cable number - comp_idx, # Component number - material_group, # Material group from part type - material_id, # Material ID from registry - ) - - # Create physical name - part_type = lowercase(string(nameof(typeof(part)))) - elementary_name = create_cable_elementary_name( - cable_idx = cable_idx, - component_id = comp_id, - group_type = material_group, - part_type = part_type, - layer_idx = layer_idx, - phase = phase, - ) - - # Extract parameters - r_in = to_nominal(part.r_in) - r_ex = to_nominal(part.r_ex) - - # Calculate mesh size for this part - if part isa AbstractConductorPart - num_elements = workspace.formulation.elements_per_length_conductor - elseif part isa Insulator - num_elements = workspace.formulation.elements_per_length_insulator - elseif part isa Semicon - num_elements = workspace.formulation.elements_per_length_semicon - end - - mesh_size_current = - _calc_mesh_size(r_in, r_ex, part.material_props, num_elements, workspace) - - # Calculate mesh size for the next part - num_layers = - length(cable_position.design_data.components[comp_idx].conductor_group.layers) - next_part = - layer_idx < num_layers ? - cable_position.design_data.components[comp_idx].conductor_group.layers[layer_idx+1] : - nothing - - if !isnothing(next_part) - next_radius_in = to_nominal(next_part.r_in) - next_radius_ext = to_nominal(next_part.r_ex) - mesh_size_next = _calc_mesh_size( - next_radius_in, - next_radius_ext, - next_part.material_props, - num_elements, - workspace, - ) - if next_part isa Insulator - mesh_size = min(mesh_size_current, mesh_size_next) - else - mesh_size = max(mesh_size_current, mesh_size_next) - end - else - mesh_size = mesh_size_current - end - - num_points_circumference = workspace.formulation.points_per_circumference - - # Create annular shape and assign marker - if r_in ≈ 0 - # Solid disk - _, _, marker, _ = - draw_disk(x_center, y_center, r_ex, mesh_size, num_points_circumference) - else - # Annular shape - _, _, marker, _ = draw_annular( - x_center, - y_center, - r_in, - r_ex, - mesh_size, - num_points_circumference, - ) - - # Define the inner region as an insulator (air) - air_material = get_air_material(workspace) - air_material_id = get_or_register_material_id(workspace, air_material) - air_physical_group_tag = encode_physical_group_tag(1, cable_idx, comp_idx, 2, air_material_id) - air_elementary_name = create_cable_elementary_name( - cable_idx=cable_idx, component_id=comp_id, group_type=2, - part_type="tubular_inner_air", layer_idx=layer_idx, phase=phase - ) - - # Place a marker in the inner region - inner_marker_x = x_center - inner_marker_y = y_center - inner_marker = [inner_marker_x, inner_marker_y, 0.0] - - core_data_air = CoreEntityData(air_physical_group_tag, air_elementary_name, mesh_size) - entity_data_air = SurfaceEntity(core_data_air, air_material) - workspace.unassigned_entities[inner_marker] = entity_data_air - register_physical_group!(workspace, air_physical_group_tag, air_material) - - end - - # Create entity data - core_data = CoreEntityData(physical_group_tag, elementary_name, mesh_size) - entity_data = CablePartEntity(core_data, part) - - # Add to workspace in the unassigned container for subsequent processing - workspace.unassigned_entities[marker] = entity_data - - # Add physical groups to the workspace - register_physical_group!(workspace, physical_group_tag, part.material_props) - - - -end diff --git a/src/engine/fem/drawing.jl b/src/engine/fem/drawing.jl deleted file mode 100644 index 654d85a46..000000000 --- a/src/engine/fem/drawing.jl +++ /dev/null @@ -1,863 +0,0 @@ -""" -Primitive drawing functions for the FEMTools.jl module. -These functions handle the creation of geometric entities in Gmsh. -""" - -function add_mesh_points(; - r_ex::Number, - theta_0::Number, - theta_1::Number, - mesh_size::Number, - r_in::Number = 0.0, - num_points_ang::Integer = 8, - num_points_rad::Integer = 0, - C::Tuple{Number, Number} = (0.0, 0.0), - theta_offset::Number = 0.0) - - point_tags = Vector{Int}() - center_x, center_y = C - - # Handle special cases - if num_points_ang <= 0 && num_points_rad <= 0 - # Single point at center C - point_tag = gmsh.model.occ.add_point(center_x, center_y, 0.0, mesh_size) - gmsh.model.set_entity_name( - 0, - point_tag, - "mesh_size_$(round(mesh_size, sigdigits=6))", - ) - return [point_tag] - end - - # Circular arc (default case or when num_points_rad=0) - if num_points_rad == 0 - r = r_ex # Use external radius as default - np_ang = max(2, num_points_ang) # At least 2 points for an arc - - for i in 0:(np_ang-1) - t_ang = i / (np_ang - 1) - theta = theta_0 + t_ang * (theta_1 - theta_0) + theta_offset - - x = center_x + r * cos(theta) - y = center_y + r * sin(theta) - - point_tag = gmsh.model.occ.add_point(x, y, 0.0, mesh_size) - gmsh.model.set_entity_name( - 0, - point_tag, - "mesh_size_$(round(mesh_size, sigdigits=6))", - ) - push!(point_tags, point_tag) - end - - return point_tags - end - - # Radial line (when theta_0 == theta_1) - if theta_0 == theta_1 - theta = theta_0 + theta_offset - np_rad = max(2, num_points_rad) - - for j in 0:(np_rad-1) - t_rad = j / (np_rad - 1) - r = r_in + t_rad * (r_ex - r_in) - - x = center_x + r * cos(theta) - y = center_y + r * sin(theta) - - point_tag = gmsh.model.occ.add_point(x, y, 0.0, mesh_size) - gmsh.model.set_entity_name( - 0, - point_tag, - "mesh_size_$(round(mesh_size, sigdigits=6))", - ) - push!(point_tags, point_tag) - end - - return point_tags - end - - # 2D array of points (both radial and angular) - np_rad = max(2, num_points_rad) - np_ang = max(2, num_points_ang) - - for j in 0:(np_rad-1) - t_rad = j / (np_rad - 1) - r = r_in + t_rad * (r_ex - r_in) - - for i in 0:(np_ang-1) - t_ang = i / (np_ang - 1) - theta = theta_0 + t_ang * (theta_1 - theta_0) + theta_offset - * - x = center_x + r * cos(theta) - y = center_y + r * sin(theta) - - point_tag = gmsh.model.occ.add_point(x, y, 0.0, mesh_size) - gmsh.model.set_entity_name( - 0, - point_tag, - "mesh_size_$(round(mesh_size, sigdigits=6))", - ) - push!(point_tags, point_tag) - end - end - - return point_tags -end - - -""" -$(TYPEDSIGNATURES) - -Draw a point with specified coordinates and mesh size. - -# Arguments - -- `x`: X-coordinate \\[m\\]. -- `y`: Y-coordinate \\[m\\]. -- `z`: Z-coordinate \\[m\\]. - -# Returns - -- Gmsh point tag \\[dimensionless\\]. - -# Examples - -```julia -point_tag = $(FUNCTIONNAME)(0.0, 0.0, 0.0, 0.01) -``` -""" -function draw_point(x::Number, y::Number, z::Number) - return gmsh.model.occ.add_point(x, y, z) -end - -""" -$(TYPEDSIGNATURES) - -Draw a line between two points. - -# Arguments - -- `x1`: X-coordinate of the first point \\[m\\]. -- `y1`: Y-coordinate of the first point \\[m\\]. -- `x2`: X-coordinate of the second point \\[m\\]. -- `y2`: Y-coordinate of the second point \\[m\\]. - -# Returns - -- Gmsh line tag \\[dimensionless\\]. - -# Examples - -```julia -line_tag = $(FUNCTIONNAME)(0.0, 0.0, 1.0, 0.0, 0.01) -``` -""" -function draw_line( - x1::Number, - y1::Number, - x2::Number, - y2::Number, - mesh_size::Number, - num_points::Number, -) - - # Calculate line parameters - line_length = sqrt((x2 - x1)^2 + (y2 - y1)^2) - x_center = (x1 + x2) / 2 - y_center = (y1 + y2) / 2 - - # Calculate angle in polar coordinates (in radians) - theta = atan(y2 - y1, x2 - x1) - - # Use the distance as a "domain radius" for placing mesh points - radius = line_length / 2 - - # Create a unique marker for this line - marker = [x_center, y_center, 0.0] # Center of the line - - marker_tag = gmsh.model.occ.add_point(marker[1], marker[2], marker[3], mesh_size) - gmsh.model.set_entity_name(0, marker_tag, "marker_$(round(mesh_size, sigdigits=6))") - - mesh_points = add_mesh_points( - r_in = -radius, - r_ex = radius, - theta_0 = theta, - theta_1 = theta, - mesh_size = mesh_size, - num_points_ang = 0, - num_points_rad = num_points, # Not strictly a circumference, but the trick works - C = (x_center, y_center), - ) - - tag = gmsh.model.occ.add_line(mesh_points[1], mesh_points[end]) - - # Add midpoint markers between each pair of mesh points - segment_markers = Vector{Vector{Float64}}() - push!(segment_markers, marker) - - if length(mesh_points) >= 2 - # Iterate through adjacent pairs of mesh points - for i in 1:(num_points-1) - t = (i - 0.5) / (num_points - 1) # Parametric coordinate (0.5 between points) - mid_x = x1 + t * (x2 - x1) - mid_y = y1 + t * (y2 - y1) - # Create marker at midpoint - mid_marker = [mid_x, mid_y, 0.0] - push!(segment_markers, mid_marker) - end - end - - return tag, mesh_points, segment_markers -end - -""" -$(TYPEDSIGNATURES) - -Draw a circular disk with specified center and radius. - -# Arguments - -- `x`: X-coordinate of the center \\[m\\]. -- `y`: Y-coordinate of the center \\[m\\]. -- `radius`: Radius of the disk \\[m\\]. - -# Returns - -- Gmsh surface tag \\[dimensionless\\]. - -# Examples - -```julia -disk_tag = $(FUNCTIONNAME)(0.0, 0.0, 0.5, 0.01) -``` -""" -function draw_disk( - x::Number, - y::Number, - radius::Number, - mesh_size::Number, - num_points::Number, -) - - tag = gmsh.model.occ.add_disk(x, y, 0.0, radius, radius) - - mesh_points = add_mesh_points( - r_in = radius, - r_ex = radius, - theta_0 = 0, - theta_1 = 2 * pi, - mesh_size = mesh_size, - num_points_ang = num_points, - C = (x, y), - theta_offset = 0, #pi / 15 - ) - - - marker = [x, y + 0.99 * radius, 0.0] # A very small offset inwards the circle - marker_tag = gmsh.model.occ.add_point(marker[1], marker[2], marker[3], mesh_size) - gmsh.model.set_entity_name(0, marker_tag, "marker_$(round(mesh_size, sigdigits=6))") - - # Add midpoint markers between each pair of mesh points - arc_markers = Vector{Vector{Float64}}() - - if num_points >= 2 - # Calculate the angular step between mesh points - theta_step = 2 * pi / num_points - - # Add a midpoint marker for each arc segment - for i in 1:num_points - # Calculate midpoint theta (angle) - theta_mid = (i - 0.5) * theta_step - - # Calculate midpoint coordinates - mid_x = x + radius * cos(theta_mid) - mid_y = y + radius * sin(theta_mid) - - # Create marker at the midpoint - mid_marker = [mid_x, mid_y, 0.0] - - push!(arc_markers, mid_marker) - end - end - - return tag, mesh_points, marker, arc_markers -end - - -""" -$(TYPEDSIGNATURES) - -Draw an annular (ring) shape with specified center, inner radius, and outer radius. - -# Arguments - -- `x`: X-coordinate of the center \\[m\\]. -- `y`: Y-coordinate of the center \\[m\\]. -- `r_in`: Inner radius of the annular shape \\[m\\]. -- `r_ex`: Outer radius of the annular shape \\[m\\]. - -# Returns - -- Gmsh surface tag \\[dimensionless\\]. - -# Examples - -```julia -annular_tag = $(FUNCTIONNAME)(0.0, 0.0, 0.3, 0.5, 0.01) -``` -""" -function draw_annular( - x::Number, - y::Number, - r_in::Number, - r_ex::Number, - mesh_size::Number, - num_points::Number; - inner_points::Bool = false, -) - # Create outer disk - outer_disk = gmsh.model.occ.add_disk(x, y, 0.0, r_ex, r_ex) - - # Create inner disk - inner_disk = gmsh.model.occ.add_disk(x, y, 0.0, r_in, r_in) - - # Cut inner disk from outer disk to create annular shape - annular_obj, _ = gmsh.model.occ.cut([(2, outer_disk)], [(2, inner_disk)]) - - # Return the tag of the resulting surface - if length(annular_obj) > 0 - tag = annular_obj[1][2] - else - Base.error("Failed to create annular shape.") - end - - mesh_points = add_mesh_points( - r_in = r_ex, - r_ex = r_ex, - theta_0 = 0, - theta_1 = 2 * pi, - mesh_size = mesh_size, - num_points_ang = num_points, - C = (x, y), - theta_offset = 0, #pi / 15 - ) - - if inner_points - mesh_points = add_mesh_points( - r_in = r_in, - r_ex = r_in, - theta_0 = 0, - theta_1 = 2 * pi, - mesh_size = mesh_size, - num_points_ang = num_points, - C = (x, y), - theta_offset = pi / 3, - ) - end - - marker = [x, y + (r_in + 0.99 * (r_ex - r_in)), 0.0] - marker_tag = gmsh.model.occ.add_point(marker[1], marker[2], marker[3], mesh_size) - gmsh.model.set_entity_name(0, marker_tag, "marker_$(round(mesh_size, sigdigits=6))") - - # Add midpoint markers between each pair of mesh points - arc_markers = Vector{Vector{Float64}}() - - if num_points >= 2 - # Calculate the angular step between mesh points - theta_step = 2 * pi / num_points - - # Add a midpoint marker for each arc segment - for i in 1:num_points - # Calculate midpoint theta (angle) - theta_mid = (i - 0.5) * theta_step - - # Calculate midpoint coordinates - mid_x = x + r_ex * cos(theta_mid) - mid_y = y + r_ex * sin(theta_mid) - - # Create marker at the midpoint - mid_marker = [mid_x, mid_y, 0.0] - - push!(arc_markers, mid_marker) - end - end - - return tag, mesh_points, marker, arc_markers -end - - -""" -$(TYPEDSIGNATURES) - -Draw a rectangle with specified center, width, and height. - -# Arguments - -- `x`: X-coordinate of the center \\[m\\]. -- `y`: Y-coordinate of the center \\[m\\]. -- `width`: Width of the rectangle \\[m\\]. -- `height`: Height of the rectangle \\[m\\]. - -# Returns - -- Gmsh surface tag \\[dimensionless\\]. - -# Examples - -```julia -rect_tag = $(FUNCTIONNAME)(0.0, 0.0, 1.0, 0.5, 0.01) -``` -""" -function draw_rectangle(x::Number, y::Number, width::Number, height::Number) - # Calculate corner coordinates - x1 = x - width / 2 - y1 = y - height / 2 - x2 = x + width / 2 - y2 = y + height / 2 - - # Create rectangle - return gmsh.model.occ.add_rectangle(x1, y1, 0.0, width, height) -end - -""" -$(TYPEDSIGNATURES) - -Draw a circular arc between two points with a specified center. - -# Arguments - -- `x1`: X-coordinate of the first point \\[m\\]. -- `y1`: Y-coordinate of the first point \\[m\\]. -- `x2`: X-coordinate of the second point \\[m\\]. -- `y2`: Y-coordinate of the second point \\[m\\]. -- `xc`: X-coordinate of the center \\[m\\]. -- `yc`: Y-coordinate of the center \\[m\\]. - -# Returns - -- Gmsh curve tag \\[dimensionless\\]. - -# Examples - -```julia -arc_tag = $(FUNCTIONNAME)(1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.01) -``` -""" -function draw_arc(x1::Number, y1::Number, x2::Number, y2::Number, xc::Number, yc::Number) - p1 = gmsh.model.occ.add_point(x1, y1, 0.0) - p2 = gmsh.model.occ.add_point(x2, y2, 0.0) - pc = gmsh.model.occ.add_point(xc, yc, 0.0) - - return gmsh.model.occ.add_circle_arc(p1, pc, p2) -end - -""" -$(TYPEDSIGNATURES) - -Draw a circle with specified center and radius. - -# Arguments - -- `x`: X-coordinate of the center \\[m\\]. -- `y`: Y-coordinate of the center \\[m\\]. -- `radius`: Radius of the circle \\[m\\]. - -# Returns - -- Gmsh curve tag \\[dimensionless\\]. - -# Examples - -```julia -circle_tag = $(FUNCTIONNAME)(0.0, 0.0, 0.5, 0.01) -``` -""" -function draw_circle(x::Number, y::Number, radius::Number) - return gmsh.model.occ.add_circle(x, y, 0.0, radius) -end - -""" -$(TYPEDSIGNATURES) - -Draw a polygon with specified vertices. - -# Arguments - -- `vertices`: Array of (x,y) coordinates for the vertices \\[m\\]. - -# Returns - -- Gmsh surface tag \\[dimensionless\\]. - -# Examples - -```julia -vertices = [(0.0, 0.0), (1.0, 0.0), (0.5, 1.0)] -polygon_tag = $(FUNCTIONNAME)(vertices, 0.01) -``` -""" -function draw_polygon(vertices::Vector{<:Tuple{<:Number, <:Number}}) - # Create points - points = Vector{Int}() - for (x, y) in vertices - push!(points, gmsh.model.occ.add_point(x, y, 0.0)) - end - - # Create lines - lines = Vector{Int}() - for i in 1:length(points) - next_i = i % length(points) + 1 - push!(lines, gmsh.model.occ.add_line(points[i], points[next_i])) - end - - # Create curve loop - curve_loop = gmsh.model.occ.add_curve_loop(lines) - - # Create surface - return gmsh.model.occ.add_plane_surface([curve_loop]) -end - -function draw_transition_region( - x::Number, - y::Number, - radii::Vector{<:Number}, - mesh_sizes::Vector{<:Number}, - num_points::Number, -) - # Validate inputs - if length(radii) != length(mesh_sizes) - Base.error("Radii and mesh_sizes vectors must have the same length") - end - - n_regions = length(radii) - if n_regions < 1 - Base.error("At least one radius must be provided") - end - - # Sort radii in ascending order if not already sorted - if !issorted(radii) - p = sortperm(radii) - radii = radii[p] - mesh_sizes = mesh_sizes[p] - end - - # Note to future self: the reason why I did this is because in the edge case when the bounding box coincides with the cable outermost radius (i.e. when you want the transition region around 1 single cable and not several), the earth marker ends up inside the cable region, which present me does not need to explain to future me why it's bad. - - rad_buffer = 0.001 - radii[1] += rad_buffer # Ensure the innermost radius is slightly larger than zero to avoid ambiguous regions - tags = Int[] - all_mesh_points = Int[] - markers = Vector{Vector{Float64}}() - - # Create all disks - disk_tags = Int[] - for i in 1:n_regions - disk_tag = gmsh.model.occ.add_disk(x, y, 0.0, radii[i], radii[i]) - gmsh.model.occ.synchronize() - push!(disk_tags, disk_tag) - end - - # Add the innermost disk to output - push!(tags, disk_tags[1]) - - # Add mesh points for innermost disk - inner_mesh_points = add_mesh_points( - r_in = radii[1], - r_ex = radii[1], - theta_0 = 0, - theta_1 = 2 * pi, - mesh_size = mesh_sizes[1], - num_points_ang = num_points, - C = (x, y), - theta_offset = 0, - ) - append!(all_mesh_points, inner_mesh_points) - - # Create marker at the midpoint between the real radius and the added buffer - # Feel free to implement a less dumb way to do this - radius_inner_marker = (radii[1] + radii[1] - rad_buffer) / 2 - inner_marker = [x, y + radius_inner_marker, 0.0] - - marker_tag = gmsh.model.occ.add_point( - inner_marker[1], - inner_marker[2], - inner_marker[3], - mesh_sizes[1], - ) - gmsh.model.set_entity_name(0, marker_tag, "marker_$(round(mesh_sizes[1], sigdigits=6))") - push!(markers, inner_marker) - - # Synchronize the model - gmsh.model.occ.synchronize() - - # Create annular regions for the rest - for i in 2:n_regions - # Cut the inner disk from the outer disk - annular_obj, _ = - gmsh.model.occ.cut([(2, disk_tags[i])], [(2, disk_tags[i-1])], false, false) - - # Get the resulting surface tag - if length(annular_obj) > 0 - annular_tag = annular_obj[1][2] - push!(tags, annular_tag) - - # Add mesh points on the boundary - boundary_points = add_mesh_points( - r_in = radii[i], - r_ex = radii[i], - theta_0 = 0, - theta_1 = 2 * pi, - mesh_size = mesh_sizes[i], - num_points_ang = num_points, - C = (x, y), - theta_offset = 0, - ) - append!(all_mesh_points, boundary_points) - - # Create marker at 99% of the way from inner to outer radius - radius_marker = radii[i-1] + 0.99 * (radii[i] - radii[i-1]) - annular_marker = [x, y + radius_marker, 0.0] - marker_tag = gmsh.model.occ.add_point( - annular_marker[1], - annular_marker[2], - annular_marker[3], - mesh_sizes[i], - ) - gmsh.model.set_entity_name( - 0, - marker_tag, - "marker_$(round(mesh_sizes[i], sigdigits=6))", - ) - push!(markers, annular_marker) - else - Base.error( - "Failed to create annular region for radii $(radii[i-1]) and $(radii[i])", - ) - end - end - - return tags, all_mesh_points, markers -end - -function draw_polygon(vertices::Vector{<:Point}) - @debug "Drawing a polygon for a Point vertices" - # Create points - points = [gmsh.model.occ.add_point(v[1], v[2], 0.0) for v in vertices] - - # Create lines - lines = [gmsh.model.occ.add_line(points[i], points[i % length(points) + 1]) for i in 1:length(points)] - - # Create curve loop and surface - curve_loop = gmsh.model.occ.add_curve_loop(lines) - surface_tag = gmsh.model.occ.add_plane_surface([curve_loop]) - - # Synchronize to make the new entity available for calculations - gmsh.model.occ.synchronize() - - # calculate the centroid of the vertices as a marker - if isempty(vertices) - error("Cannot calculate centroid of empty vertex list.") - end - avg_x = sum(v[1] for v in vertices) / length(vertices) - avg_y = sum(v[2] for v in vertices) / length(vertices) - marker = [avg_x, avg_y, 0.0] - - return surface_tag, marker -end - -""" -$(TYPEDSIGNATURES) - -Draw a polygon with a hole. - -# Arguments -- `outer_vertices`: A vector of (x,y) coordinates for the outer boundary. -- `inner_vertices`: A vector of (x,y) coordinates for the inner boundary (the hole). - -# Returns -- A tuple containing the Gmsh surface tag and a marker point `[x, y, z]`. -""" -function _densify_vertices(vertices::Vector{<:Point}, max_len::Number) - new_vertices = Point[] - if isempty(vertices) - return new_vertices - end - for i in 1:length(vertices) - p1 = vertices[i] - p2 = vertices[i % length(vertices) + 1] - - push!(new_vertices, p1) - - edge_vec = p2 - p1 - edge_len = norm(edge_vec) - - if edge_len > max_len - num_segments = ceil(Int, edge_len / max_len) - for j in 1:(num_segments - 1) - intermediate_point = p1 + (j / num_segments) * edge_vec - push!(new_vertices, intermediate_point) - end - end - end - return new_vertices -end - -function draw_polygon_with_hole(outer_vertices::Vector{<:Point}, inner_vertices::Vector{<:Point}, max_edge_length::Number) - - new_outer_vertices = _densify_vertices(outer_vertices, max_edge_length) - new_inner_vertices = _densify_vertices(inner_vertices, max_edge_length) - - # Create outer boundary - outer_points = [gmsh.model.occ.add_point(v[1], v[2], 0.0) for v in new_outer_vertices] - outer_lines = [gmsh.model.occ.add_line(outer_points[i], outer_points[i % length(outer_points) + 1]) for i in 1:length(outer_points)] - outer_loop = gmsh.model.occ.add_curve_loop(outer_lines) - - # Create inner boundary (hole) - inner_points = [gmsh.model.occ.add_point(v[1], v[2], 0.0) for v in new_inner_vertices] - inner_lines = [gmsh.model.occ.add_line(inner_points[i], inner_points[i % length(inner_points) + 1]) for i in 1:length(inner_points)] - inner_loop = gmsh.model.occ.add_curve_loop(inner_lines) - - # Create surface with hole - surface_tag = gmsh.model.occ.add_plane_surface([outer_loop, inner_loop]) - - # Synchronize to make the new entity available for calculations - gmsh.model.occ.synchronize() - - # A marker point must be inside the insulator, but outside the conductor. - # A point halfway between the inner and outer boundaries along one of the vertices should work. - marker_point = (outer_vertices[1] + inner_vertices[1]) / 2 - marker = [marker_point[1], marker_point[2], 0.0] - - return surface_tag, marker -end - -function get_system_centroid(cable_system::LineCableSystem, cable_idx::Vector{<:Integer}) - # Check if cable_idx is empty - if isempty(cable_idx) - Base.error("Cable index vector cannot be empty") - end - - # Check if any index is out of bounds - if any(idx -> idx < 1 || idx > length(cable_system.cables), cable_idx) - Base.error("Cable index out of bounds") - end - - # Extract coordinates - horz_coords = [cable_system.cables[idx].horz for idx in cable_idx] - vert_coords = [cable_system.cables[idx].vert for idx in cable_idx] - - # Calculate centroid - centroid_x = sum(horz_coords) / length(horz_coords) - centroid_y = sum(vert_coords) / length(vert_coords) - - # Find the maximum distance from centroid to any cable's edge - max_distance = 0.0 - characteristic_len = Inf - - for idx in cable_idx - cable_position = cable_system.cables[idx] - - # Calculate distance from centroid to cable center - distance_to_center = sqrt( - (cable_position.horz - centroid_x)^2 + (cable_position.vert - centroid_y)^2, - ) - - # Get the outermost component (last component in the vector) - if !isempty(cable_position.design_data.components) - last_component = cable_position.design_data.components[end] - - outer_radius = last_component.insulator_group.r_ex - - insulator_radius_in = last_component.insulator_group.layers[end].r_in - last_layer_thickness = outer_radius - insulator_radius_in - - - # Add cable radius to get distance to edge - total_distance = distance_to_center + outer_radius - max_distance = max(max_distance, total_distance) - characteristic_len = min(characteristic_len, last_layer_thickness) - end - end - - return (centroid_x, centroid_y, max_distance, characteristic_len) -end - - -""" -$(TYPEDSIGNATURES) - -Calculate the coordinates of air gaps in a wire array. - -# Arguments - -- `num_wires`: Number of wires in the array \\[dimensionless\\]. -- `radius_wire`: Radius of each wire \\[m\\]. -- `r_in`: Inner radius of the wire array \\[m\\]. - -# Returns - -- Vector of marker positions (3D coordinates) for air gaps \\[m\\]. - -# Notes - -This function calculates positions for markers that are guaranteed to be in the air gaps -between wires in a wire array. These markers are used to identify the air regions after -boolean fragmentation operations. - -# Examples - -```julia -markers = $(FUNCTIONNAME)(7, 0.002, 0.01) -``` -""" -function get_air_gap_markers(num_wires::Int, radius_wire::Number, r_in::Number) - markers = Vector{Vector{Float64}}() - - lay_radius = r_in + radius_wire - - num_angular_markers = num_wires == 1 ? 6 : num_wires - # For multiple wires, place markers between adjacent wires - angle_step = 2π / num_angular_markers - for i in 0:(num_angular_markers-1) - angle = i * angle_step + (angle_step / 2) # Midway between wires - r = lay_radius + (radius_wire / 2) # Slightly outward - x = r * cos(angle) - y = r * sin(angle) - push!(markers, [x, y, 0.0]) - end - return markers -end - -function draw_polygon(vertices::Vector{<:Point}, max_edge_length::Number) - if isempty(vertices) - error("Cannot draw a polygon with no vertices.") - end - - new_vertices = _densify_vertices(vertices, max_edge_length) - - # Create points - points = [gmsh.model.occ.add_point(v[1], v[2], 0.0) for v in new_vertices] - - # Create lines - lines = [gmsh.model.occ.add_line(points[i], points[i % length(points) + 1]) for i in 1:length(points)] - - # Create curve loop and surface - curve_loop = gmsh.model.occ.add_curve_loop(lines) - surface_tag = gmsh.model.occ.add_plane_surface([curve_loop]) - - # Synchronize to make the new entity available for calculations - gmsh.model.occ.synchronize() - - # calculate the centroid of the original vertices as a marker - if isempty(vertices) - error("Cannot calculate centroid of empty vertex list.") - end - avg_x = sum(v[1] for v in vertices) / length(vertices) - avg_y = sum(v[2] for v in vertices) / length(vertices) - marker = [avg_x, avg_y, 0.0] - - return surface_tag, marker -end - - diff --git a/src/engine/fem/encoding.jl b/src/engine/fem/encoding.jl deleted file mode 100644 index 93da41c08..000000000 --- a/src/engine/fem/encoding.jl +++ /dev/null @@ -1,485 +0,0 @@ -""" -Functions for physical group tag encoding and decoding in the FEMTools.jl module. -Implements the unified SCCCOOGMMM scheme for all entity types. -""" - -""" - encode_physical_group_tag(surface_type, entity_num, component_num, material_group, material_id) - -Encode entity information into a single integer ID using the unified SCCCOOGMMM scheme. - -# Arguments - -- `surface_type`: Surface type (1=cable, 2=physical space, 3=infinite shell) \\[dimensionless\\]. -- `entity_num`: Cable number or layer number (1-999) \\[dimensionless\\]. -- `component_num`: Component number (0-99, 0 for spatial regions) \\[dimensionless\\]. -- `material_group`: Material group (1=conductor, 2=insulator) \\[dimensionless\\]. -- `material_id`: Material identifier (0-999) \\[dimensionless\\]. - -# Returns - -- Encoded tag as an integer \\[dimensionless\\]. - -# Examples - -```julia -# Cable core conductor with material ID 5 -tag = encode_physical_group_tag(1, 1, 1, 1, 5) - -# Air region with material ID 1 -air_tag = encode_physical_group_tag(2, 1, 0, 2, 1) - -# Earth layer with material ID 3 -earth_tag = encode_physical_group_tag(2, 2, 0, 1, 3) -``` -""" -function encode_physical_group_tag( - surface_type::Int, - entity_num::Int, - component_num::Int, - material_group::Int, - material_id::Int, -) - # Input validation with detailed error messages - if !(1 <= surface_type <= 9) - Base.error("Invalid surface type: $surface_type. Must be between 1 and 9") - end - - if !(0 <= entity_num <= 999) - Base.error("Invalid entity number: $entity_num. Must be between 0 and 999") - end - - if !(0 <= component_num <= 99) - Base.error("Invalid component number: $component_num. Must be between 0 and 99") - end - - if !(1 <= material_group <= 2) - Base.error(""" - Invalid material group: $material_group - Material group must be either: - - 1: Conductor (accounts for eddy currents) - - 2: Insulator (no eddy currents) - """) - end - - if !(0 <= material_id <= 99) - Base.error("Invalid material ID: $material_id. Must be between 0 and 99") - end - - # SCCCOOGMM encoding - tag = ( - surface_type * 100_000_000 + - entity_num * 100_000 + - component_num * 1_000 + - material_group * 100 + - material_id - ) - - # Validate the generated tag - tag_str = string(tag) - expected_length = 9 # SCCCOOGMM = 9 digits - - if length(tag_str) != expected_length - Base.error( - "Generated tag $tag has invalid length ($(length(tag_str))) for inputs: surface_type=$surface_type, entity_num=$entity_num, component_num=$component_num, material_group=$material_group, material_id=$material_id", - ) - end - - return tag -end - -""" - decode_physical_group_tag(tag) - -Decode a physical group tag into its component parts. - -# Arguments - -- `tag`: Encoded tag as an integer \\[dimensionless\\]. - -# Returns - -- Tuple of (surface_type, entity_num, component_num, material_group, material_id) \\[dimensionless\\]. - -# Examples - -```julia -surface_type, entity_num, component_num, material_group, material_id = decode_physical_group_tag(1001010005) -println((surface_type, entity_num, component_num, material_group, material_id)) -# Output: (1, 1, 1, 1, 5) -``` -""" -function decode_physical_group_tag(tag::Int) - tag_str = string(tag) - - # Validate format - expected_length = 9 # SCCCOOGMM = 9 digits - if length(tag_str) != expected_length - Base.error("Invalid tag format: $tag. Expected a $expected_length-digit number") - end - - # Extract parts - surface_type = parse(Int, tag_str[1:1]) - entity_num = parse(Int, tag_str[2:4]) - component_num = parse(Int, tag_str[5:6]) - material_group = parse(Int, tag_str[7:7]) - material_id = parse(Int, tag_str[8:9]) - - return (surface_type, entity_num, component_num, material_group, material_id) -end - -function encode_boundary_tag( - curve_type::Int, - layer_idx::Int, - sequence_num::Int = 1, -) - # Input validation - if !(1 <= curve_type <= 3) - Base.error("Invalid curve type: $curve_type. Must be between 1 and 3: - 1 = domain boundary - 2 = domain -> infinity - 3 = layer interface") - end - - if !(1 <= layer_idx <= 999) - Base.error("Invalid layer index: $layer_idx. Must be between 1 and 999") - end - - if !(1 <= sequence_num <= 99) - Base.error("Invalid sequence number: $sequence_num. Must be between 1 and 99") - end - - # Use entity_num format consistent with the cable parts encoding - # Format: 1CCCLSS - # 1: Fixed prefix for boundaries/interfaces - # CCC: Layer index (1-999) - # L: Curve type (1-3) - # SS: Sequence number (1-99) - tag = 1_000_000 + - layer_idx * 1_000 + - curve_type * 100 + - sequence_num - - return tag -end - -function decode_boundary_tag(tag::Int) - tag_str = string(tag) - - # Validate format (should start with 1) - if length(tag_str) != 7 || tag_str[1] != '1' - Base.error( - "Invalid boundary tag format: $tag. Expected a 7-digit number starting with 1", - ) - end - - # Extract parts - layer_idx = parse(Int, tag_str[2:4]) - curve_type = parse(Int, tag_str[5]) - sequence_num = parse(Int, tag_str[6:7]) - - return (curve_type, layer_idx, sequence_num) -end - -""" - get_material_group(part) - -Get the material group (conductor or insulator) for a cable part based on its type. - -# Arguments - -- `part`: An AbstractCablePart instance. - -# Returns - -- Material group (1=conductor, 2=insulator) \\[dimensionless\\]. - -# Examples - -```julia -group = get_material_group(circstrands) # Returns 1 (conductor) -``` -""" -function get_material_group(part::AbstractCablePart) - if part isa AbstractConductorPart - return 1 # Conductor - elseif part isa AbstractInsulatorPart - return 2 # Insulator - else - Base.error("Unknown part type: $(typeof(part))") - end -end - -""" - get_material_group(earth_model, layer_idx) - -Get the material group (conductor or insulator) for an earth layer. - -# Arguments - -- `earth_model`: The EarthModel containing layer information. -- `layer_idx`: The layer index to check. - -# Returns - -- Material group (1=conductor, 2=insulator) \\[dimensionless\\]. - -# Examples - -```julia -group = get_material_group(earth_model, 1) # Layer 1 is air -> Returns 2 (insulator) -group = get_material_group(earth_model, 2) # Layer 2 is earth -> Returns 1 (conductor) -``` -""" -function get_material_group(earth_model::EarthModel, layer_idx::Int) - # Layer 1 is always air (insulator) - if layer_idx == 1 - return 2 # Insulator - else - # All other layers are earth (conductor) - return 1 # Conductor - end -end - -""" - get_or_register_material_id(workspace, material) - -Find or create a unique ID for a material within the current workspace. - -# Arguments - -- `workspace`: The FEMWorkspace containing the material registry. -- `material`: The Material object to register. - -# Returns - -- A unique material ID (1-99) \\[dimensionless\\]. - -# Examples - -```julia -material_id = get_or_register_material_id(workspace, copper_material) -``` -""" -function get_or_register_material_id(workspace::FEMWorkspace, material::Material) - # Create material_registry if it doesn't exist - if !isdefined(workspace, :material_registry) - workspace.material_registry = Dict{String, Int}() - end - - # Get material name using existing function that checks library first - material_name = get_material_name(material, workspace.formulation.materials) - - # Find or create the ID - if !haskey(workspace.material_registry, material_name) - # New material - assign next available ID - material_id = length(workspace.material_registry) + 1 - if material_id > 99 - Base.error("Material registry full: Maximum of 99 unique materials supported") - end - workspace.material_registry[material_name] = material_id - else - material_id = workspace.material_registry[material_name] - end - - return material_id -end - -function register_physical_group!( - workspace::FEMWorkspace, - physical_group_tag::Int, - material::Material, -) - - # Create physical_groups if it doesn't exist - if !isdefined(workspace, :physical_groups) - workspace.physical_groups = Dict{Int, Material}() - end - - # Find or create the ID - if !haskey(workspace.physical_groups, physical_group_tag) - # New material - assign next available ID - workspace.physical_groups[physical_group_tag] = Material( - to_nominal(material.rho), - to_nominal(material.eps_r), - to_nominal(material.mu_r), - to_nominal(material.T0), - to_nominal(material.alpha), - ) - end - -end -""" -$(TYPEDSIGNATURES) - -Generate a readable elementary name for a cable component. -Format: cable_X___layer__[_wire_N][_phase_M] - -# Arguments - -- `cable_idx`: Cable index \\[dimensionless\\]. -- `component_id`: Component ID (e.g., "core", "sheath") \\[dimensionless\\]. -- `group_type`: Group type (1=conductor, 2=insulator, 3=empty) \\[dimensionless\\]. -- `part_type`: Part type (e.g., "wire", "strip", "tubular") \\[dimensionless\\]. -- `layer_idx`: Layer index \\[dimensionless\\]. -- `wire_idx`: Optional wire index \\[dimensionless\\]. -- `phase`: Optional phase index \\[dimensionless\\]. - -# Returns - -- Human-readable physical name as a string. - -# Examples - -```julia -name = $(FUNCTIONNAME)( - cable_idx=1, - component_id="core", - group_type=1, - part_type="wire", - layer_idx=2, - wire_idx=3, - phase=1 -) -println(name) # Output: "cable_1_core_con_layer_2_wire_wire_3_phase_1" -``` -""" -function create_cable_elementary_name(; - cable_idx::Int, - component_id::String, - group_type::Int, # 1=conductor, 2=insulator, 3=air gap - part_type::String, - layer_idx::Union{Int, Nothing} = nothing, - wire_idx::Union{Int, Nothing} = nothing, - phase::Union{Int, Nothing} = nothing, -) - # Convert group_type to string - group_str = if group_type == 1 - "con" - elseif group_type == 2 - "ins" - else - Base.error("Invalid group_type: $group_type") - end - - # Base name without optional parts - name = "cable_$(cable_idx)_$(component_id)_$(group_str)" - - # Add layer index if provided - if !isnothing(layer_idx) - name *= "_layer_$(layer_idx)" - end - - name *= "_$(part_type)" - - # Add wire index if provided - if !isnothing(wire_idx) - name *= "_wire_$(wire_idx)" - end - - # Add phase if provided - if !isnothing(phase) && phase > 0 - name *= "_phase_$(phase)" - elseif !isnothing(phase) && phase == 0 - name *= "_ground" - end - - return name -end - -function create_physical_group_name(workspace::FEMWorkspace, tag::Int) - # Determine tag type by length - tag_str = string(tag) - - if length(tag_str) == 9 - # This is a physical group tag (SCCCOOGMM format) - return _create_surface_physical_name(workspace, tag) - elseif length(tag_str) == 7 && tag_str[1] == '1' - # This is a boundary tag (1CCCLSS format) - return _create_boundary_physical_name(workspace, tag) - else - # Unknown format - return generic name - return "group_$(tag)" - end -end - -function _create_surface_physical_name(workspace::FEMWorkspace, tag::Int) - # Decode the tag - surface_type, entity_num, component_num, material_group, material_id = - decode_physical_group_tag(tag) - - # Get material name if available - material_name = "unknown" - for (name, id) in workspace.material_registry - if id == material_id - material_name = name - break - end - end - - # Create base string based on surface type - base_str = if surface_type == 1 - # Cable component - # Try to get component name - component_name = "unknown" - if 1 <= entity_num <= length(workspace.problem_def.system.cables) - cable = workspace.problem_def.system.cables[entity_num] - - # Validate component_num is within range - if 1 <= component_num <= length(cable.design_data.components) - component = cable.design_data.components[component_num] - component_name = component.id - end - end - - group_str = material_group == 1 ? "con" : "ins" - "cable_$(entity_num)_$(component_name)_$(group_str)" - elseif surface_type == 2 - # Physical domain - layer_str = entity_num == 1 ? "air" : "earth" - group_str = material_group == 1 ? "con" : "ins" - "layer_$(entity_num)_$(layer_str)_$(group_str)" - elseif surface_type == 3 - # Infinite shell - layer_str = entity_num == 1 ? "air" : "earth" - group_str = material_group == 1 ? "con" : "ins" - "infshell_$(entity_num)_$(layer_str)_$(group_str)" - else - "surf_$(surface_type)" - end - - # Add material information - return "$(base_str)_$(material_name)" -end - -function _create_boundary_physical_name(workspace::FEMWorkspace, tag::Int) - # Decode the boundary tag - curve_type, layer_idx, sequence_num = decode_boundary_tag(tag) - - # Create boundary name based on curve type - base_str = if curve_type == 1 - # Domain boundary - layer_str = layer_idx == 1 ? "air" : "earth" - "boundary_domain_$(layer_str)" - elseif curve_type == 2 - # Domain to infinity - layer_str = layer_idx == 1 ? "air" : "earth" - "boundary_infinity_$(layer_str)" - elseif curve_type == 3 - # Layer interface - if layer_idx == 1 - "interface_air_earth" - else - "interface_earth_layers_$(layer_idx)_$(layer_idx+1)" - end - else - "boundary_unknown" - end - - # Add sequence number if more than one of the same type - if sequence_num > 1 - base_str *= "_$(sequence_num)" - end - - return base_str -end diff --git a/src/engine/fem/helpers.jl b/src/engine/fem/helpers.jl deleted file mode 100644 index 0de6a980f..000000000 --- a/src/engine/fem/helpers.jl +++ /dev/null @@ -1,315 +0,0 @@ -""" -Utility functions for the FEMTools.jl module. -These functions provide various utilities for file management, logging, etc. -""" - - -""" -$(TYPEDSIGNATURES) - -Set up directory structure and file paths for a FEM simulation. - -# Arguments - -- `solver`: The [`FEMSolver`](@ref) containing the base path. -- `cable_system`: The [`LineCableSystem`](@ref) containing the case ID. - -# Returns - -- A dictionary of paths for the simulation. - -# Examples - -```julia -paths = $(FUNCTIONNAME)(solver, cable_system) -``` -""" -function setup_paths(cable_system::LineCableSystem, formulation::FEMFormulation) - - opts = formulation.options - # Create base output directory if it doesn't exist - if !isdir(opts.save_path) - mkpath(opts.save_path) - @info "Created base output directory: $(display_path(opts.save_path))" - end - - # Set up case-specific paths - case_id = cable_system.system_id - case_dir = joinpath(opts.save_path, case_id) - - # Create case directory if needed - if !isdir(case_dir) && (opts.force_remesh || opts.mesh_only) - mkpath(case_dir) - @info "Created case directory: $(display_path(case_dir))" - end - - # Create results directory path - results_dir = joinpath(case_dir, "results") - - # Define key file paths - mesh_file = joinpath(case_dir, "$(case_id).msh") - geo_file = joinpath(case_dir, "$(case_id).geo_unrolled") - # data_file = joinpath(case_dir, "$(case_id)_data.geo") - - impedance_res = lowercase(formulation.analysis_type[1].resolution_name) - impedance_file = joinpath(case_dir, "$(case_id)_$(impedance_res).pro") - - admittance_res = lowercase(formulation.analysis_type[2].resolution_name) - admittance_file = joinpath(case_dir, "$(case_id)_$(admittance_res).pro") - - # Return compiled dictionary of paths - paths = Dict{Symbol,String}( - :base_dir => opts.save_path, - :case_dir => case_dir, - :results_dir => results_dir, - :mesh_file => mesh_file, - :geo_file => geo_file, - :impedance_file => impedance_file, - :admittance_file => admittance_file, - ) - - @debug "Paths configured: $(join(["$(k): $(v)" for (k,v) in paths], ", "))" - - return paths -end - -""" -$(TYPEDSIGNATURES) - -Clean up files based on configuration flags. - -# Arguments - -- `paths`: Dictionary of paths for the simulation. -- `solver`: The [`FEMSolver`](@ref) containing the configuration flags. - -# Returns - -- Nothing. Deletes files as specified by the configuration. - -# Examples - -```julia -$(FUNCTIONNAME)(paths, solver) -``` -""" -function cleanup_files(paths::Dict{Symbol,String}, opts::NamedTuple) - if opts.force_remesh - # If force_remesh is true, delete mesh-related files - if isfile(paths[:mesh_file]) - rm(paths[:mesh_file], force=true) - @info "Removed existing mesh file: $(display_path(paths[:mesh_file]))" - end - - if isfile(paths[:geo_file]) - rm(paths[:geo_file], force=true) - @info "Removed existing geometry file: $(display_path(paths[:geo_file]))" - end - end - - if opts.overwrite_results && opts.run_solver - - # Add cleanup for .pro files in case_dir - for file in readdir(paths[:case_dir]) - if endswith(file, ".pro") - filepath = joinpath(paths[:case_dir], file) - rm(filepath, force=true) - @info "Removed existing problem file: $(display_path(filepath))" - end - end - - # If overwriting results and running solver, clear the results directory - if isdir(paths[:results_dir]) - for file in readdir(paths[:results_dir]) - filepath = joinpath(paths[:results_dir], file) - if isfile(filepath) - rm(filepath, force=true) - end - end - @info "Cleared existing results in: $(display_path(paths[:results_dir]))" - end - end -end - -function read_results_file( - fem_formulation::Union{AbstractImpedanceFormulation,AbstractAdmittanceFormulation}, - workspace::FEMWorkspace; - file::Union{String,Nothing}=nothing, -) - - results_path = - joinpath(workspace.paths[:results_dir], lowercase(fem_formulation.resolution_name)) - - if isnothing(file) - file = - fem_formulation isa AbstractImpedanceFormulation ? "Z.dat" : - fem_formulation isa AbstractAdmittanceFormulation ? "Y.dat" : - throw(ArgumentError("Invalid formulation type: $(typeof(fem_formulation))")) - end - - filepath = joinpath(results_path, file) - - isfile(filepath) || Base.error("File not found: $filepath") - - # Read all lines from file - lines = readlines(filepath) - n_rows = - sum([length(c.design_data.components) for c in workspace.problem_def.system.cables]) - - # Pre-allocate result matrix - matrix = zeros(ComplexF64, n_rows, n_rows) - - # Process each line (matrix row) - for (i, line) in enumerate(lines) - # Parse all numbers, dropping the initial 0 - values = parse.(Float64, split(line))[2:end] - - # Fill matrix row with complex values - for j in 1:n_rows - idx = 2j - 1 # Index for real part - matrix[i, j] = Complex(values[idx], values[idx+1]) - end - end - - return matrix -end - - -# Verbosity Levels in GetDP -# Level Output Description -# 0 Silent (no output) -# 1 Errors only -# 2 Errors + warnings -# 3 Errors + warnings + basic info -# 4 Detailed debugging -# 5 Full internal tracing -function map_verbosity_to_getdp(verbosity::Int) - if is_headless() # Prevent huge logs in CI/CD deploys - @info "Running in headless mode, suppressing GetDP output" - return 0 # Gmsh Silent level - elseif verbosity >= 2 # Debug - return 4 # GetDP Debug level - elseif verbosity == 1 # Info - return 3 # GetDP Info level - else # Warn - return 1 # GetDP Errors level - end -end - -# Verbosity Levels in Gmsh -# Level Output Description -# 0 Silent (no output) -# 1 Errors only -# 2 Warnings -# 3 Direct/Important info -# 4 Information -# 5 Status messages -# 99 Debug -function map_verbosity_to_gmsh(verbosity::Int) - if is_headless() # Prevent huge logs in CI/CD deploys - @info "Running in headless mode, suppressing Gmsh output" - return 0 # Gmsh Silent level - elseif verbosity >= 2 # Debug - return 99 # Gmsh Debug level - elseif verbosity == 1 # Info - return 4 # Gmsh Information level - else # Warn - return 1 # Gmsh Errors level - end -end - -function calc_domain_size( - earth_params::EarthModel, - f::Vector{<:Float64}; - min_radius=5.0, - max_radius=5000.0, -) - # Find the earth layer with the highest resistivity to determine the domain size - if isempty(earth_params.layers) - Base.error("EarthModel has no layers defined.") - end - - inds = 2:length(earth_params.layers) - max_rho_idx = inds[argmax([earth_params.layers[i].rho_g[1] for i in inds])] - - target_layer = earth_params.layers[max_rho_idx] - - rho_g = target_layer.rho_g[1] - mu_g = target_layer.mu_g[1] - freq = first(f) # Use the first frequency for the calculation - skin_depth_earth = abs(sqrt(rho_g / (1im * 2 * pi * freq * mu_g))) - return clamp(skin_depth_earth, min_radius, max_radius) -end - -function archive_frequency_results(workspace::FEMWorkspace, frequency::Float64) - try - results_dir = workspace.paths[:results_dir] - freq_dir = - joinpath(dirname(results_dir), "results_f=$(round(frequency, sigdigits=6))") - - if isdir(results_dir) - mv(results_dir, freq_dir, force=true) - @debug "Archived results for f=$frequency Hz" - end - - # Move solver files - for ext in [".res", ".pre"] - case_files = filter(f -> endswith(f, ext), - readdir(workspace.paths[:case_dir], join=true)) - for f in case_files - mv(f, joinpath(freq_dir, basename(f)), force=true) - end - end - catch e - @warn "Failed to archive results for frequency $frequency Hz" exception = e - end -end - -# Run a command quietly; return true if it starts and exits with code 0. -_run_ok(cmd::Cmd) = - try - success(pipeline(cmd; stdout=devnull, stderr=devnull)) - catch - false # covers "file not found", spawn failures, etc. - end - -# Does this path behave like a GetDP executable? -_is_valid_getdp_exe(path::AbstractString) = begin - @debug "Probing GetDP via -info" path = path - _run_ok(`$path -info`) -end - -# Resolve the GetDP path: -# 1) If user provided :getdp_executable and it runs with -info, use it. -# 2) Else ask GetDP.jl for its executable and use it if it runs with -info. -# 3) Else, error. -function _resolve_getdp_path(opts::NamedTuple) - user_path = get(opts, :getdp_executable, nothing) - @debug "Resolving GetDP path (simple probe)" user_path = user_path - - if user_path isa AbstractString - if _is_valid_getdp_exe(user_path) - @debug "Using user-specified GetDP executable" path = user_path - return user_path - else - @warn "User-specified GetDP executable failed when invoked with -info" path = user_path - end - else - @debug "No user-specified GetDP path" - end - - fallback = try - GetDP.get_getdp_executable() - catch - nothing - end - @debug "GetDP.get_getdp_executable() returned" path = fallback - - if fallback isa AbstractString && _is_valid_getdp_exe(fallback) - @debug "Using dependency-provided GetDP executable" path = fallback - return fallback - end - - Base.error("GetDP executable not found or not working (invocation with -info failed). " * - "Provide :getdp_executable in opts or ensure GetDP.jl is properly deployed.") -end \ No newline at end of file diff --git a/src/engine/fem/identification.jl b/src/engine/fem/identification.jl deleted file mode 100644 index b5c61b060..000000000 --- a/src/engine/fem/identification.jl +++ /dev/null @@ -1,215 +0,0 @@ -""" -Entity identification functions for the FEMTools.jl module. -These functions handle the identification of entities after boolean operations. -""" - -""" -$(TYPEDSIGNATURES) - -Perform boolean fragmentation on all entities in the model. - -# Arguments - -- `workspace`: The [`FEMWorkspace`](@ref) containing the entities to fragment. - -# Returns - -- Nothing. Modifies the Gmsh model in place. - -# Examples - -```julia -$(FUNCTIONNAME)(workspace) -``` - -# Notes - -This function performs boolean fragmentation on all surfaces and curves in the model. -After fragmentation, the original entities are replaced with new entities that respect -the intersections between them. The original entity tags are no longer valid after -this operation. -""" -function process_fragments(workspace::FEMWorkspace) - - # Get all entities - surfaces = gmsh.model.get_entities(2) - curves = gmsh.model.get_entities(1) - points = gmsh.model.get_entities(0) - - @debug "Initial counts: $(length(surfaces)) surfaces, $(length(curves)) curves, $(length(points)) points" - - # Fragment points onto curves - if !isempty(curves) && !isempty(points) - @debug "Fragmenting points onto curves..." - gmsh.model.occ.fragment(curves, points) - gmsh.model.occ.synchronize() - end - - # Get updated entities after first fragmentation - updated_curves = gmsh.model.get_entities(1) - updated_points = gmsh.model.get_entities(0) - - @debug "After fragmenting points onto curves: $(length(updated_curves)) curves, $(length(updated_points)) points" - - # Fragment curves onto surfaces - if !isempty(surfaces) && !isempty(updated_curves) - @debug "Fragmenting curves onto surfaces..." - gmsh.model.occ.fragment(surfaces, updated_curves) - gmsh.model.occ.synchronize() - end - - # Remove duplicates - @debug "Removing duplicate entities..." - gmsh.model.occ.remove_all_duplicates() - gmsh.model.occ.synchronize() - - # Final counts - final_surfaces = gmsh.model.get_entities(2) - final_curves = gmsh.model.get_entities(1) - final_points = gmsh.model.get_entities(0) - - @info "Boolean fragmentation completed" - @debug "Before: $(length(surfaces)) surfaces, $(length(curves)) curves, $(length(points)) points" - @debug "After: $(length(final_surfaces)) surfaces, $(length(final_curves)) curves, $(length(final_points)) points" - @debug "Unique markers in workspace: $(length(workspace.unassigned_entities)) markers" - -end - -function identify_by_marker(workspace::FEMWorkspace) - - # Get all surfaces after fragmentation - all_surfaces = gmsh.model.get_entities(2) - - # Track statistics - total_entities = length(workspace.unassigned_entities) - identified_count = 0 - - # Copy keys to avoid modifying dict during iteration - markers = collect(keys(workspace.unassigned_entities)) - - # For each marker, find which surface contains it - for marker in markers - entity_data = workspace.unassigned_entities[marker] - physical_group_tag = entity_data.core.physical_group_tag - elementary_name = entity_data.core.elementary_name - - for (dim, tag) in all_surfaces - if !(entity_data isa CurveEntity) - # Check if marker is inside this surface - if gmsh.model.is_inside(dim, tag, marker) == 1 - fem_entity = GmshObject(tag, entity_data) - - # Place in appropriate container - if entity_data isa CablePartEntity - if entity_data.cable_part isa AbstractConductorPart - push!(workspace.conductors, fem_entity) - elseif entity_data.cable_part isa AbstractInsulatorPart - push!(workspace.insulators, fem_entity) - end - elseif entity_data isa SurfaceEntity - push!(workspace.space_regions, fem_entity) - end - - delete!(workspace.unassigned_entities, marker) - identified_count += 1 - @debug "Marker at $(marker) identified entity $(tag) as $(elementary_name) (tag: $(physical_group_tag))" - break - end - end - end - end - - # Get all remaining curves after fragmentation - all_curves = gmsh.model.get_entities(1) - - # Update keys to avoid modifying dict during iteration - markers = collect(keys(workspace.unassigned_entities)) - - # For each marker, find which surface contains it - for marker in markers - entity_data = workspace.unassigned_entities[marker] - physical_group_tag = entity_data.core.physical_group_tag - elementary_name = entity_data.core.elementary_name - - for (dim, tag) in all_curves - # Check if marker is inside this curve - if gmsh.model.is_inside(dim, tag, marker) == 1 - # Found match - create GmshObject and add to appropriate container - fem_entity = GmshObject(tag, entity_data) - - # Place in appropriate container - if entity_data isa CurveEntity - push!(workspace.boundaries, fem_entity) - end - - delete!(workspace.unassigned_entities, marker) - identified_count += 1 - @debug "Marker at $(marker) identified entity $(tag) as $(elementary_name) (tag: $(physical_group_tag))" - break - end - end - end - - # Report identification stats - @info "Entity identification completed: $(identified_count)/$(total_entities) entities identified" - - if !isempty(workspace.unassigned_entities) - @warn "$(length(workspace.unassigned_entities))/$(total_entities) markers could not be matched to entities" - end -end -function assign_physical_groups(workspace::FEMWorkspace) - # Group entities by physical tag and dimension - entities_by_physical_group_tag = Dict{Tuple{Int,Int},Vector{Int}}() - - # Process all entity containers - for container in [workspace.conductors, workspace.insulators, workspace.space_regions, workspace.boundaries] - for entity in container - physical_group_tag = entity.data.core.physical_group_tag - elementary_name = entity.data.core.elementary_name - dim = entity.data isa CurveEntity ? 1 : 2 - - # Key is now a tuple of (physical_group_tag, dimension) - group_key = (physical_group_tag, dim) - - if !haskey(entities_by_physical_group_tag, group_key) - entities_by_physical_group_tag[group_key] = Int[] - end - - # Add this entity to the collection for this physical tag - current_physical_group = gmsh.model.get_physical_groups_for_entity(dim, entity.tag) - if !isempty(current_physical_group) - @debug "Entity $(entity.tag) already has physical group: $(current_physical_group)" - end - push!(entities_by_physical_group_tag[group_key], entity.tag) - - if !isempty(elementary_name) - # Append the complete name to the shape - current_name = gmsh.model.get_entity_name(dim, entity.tag) - if !isempty(current_name) - @debug "Entity $(entity.tag) already has elementary name: $(current_name)" - end - gmsh.model.set_entity_name(dim, entity.tag, elementary_name) - end - end - end - - # Create physical groups for each physical tag - successful_groups = 0 - failed_groups = 0 - - for ((physical_group_tag, dim), entity_tags) in entities_by_physical_group_tag - try - physical_group_name = create_physical_group_name(workspace, physical_group_tag) - @debug "Creating physical group $(physical_group_name) (tag: $(physical_group_tag), dim: $(dim)) with $(length(entity_tags)) entities" - - # Use the correct dimension when creating the physical group - gmsh.model.add_physical_group(dim, entity_tags, physical_group_tag, physical_group_name) - successful_groups += 1 - catch e - @warn "Failed to create physical group tag: $(physical_group_tag), dim: $(dim): $(e)" - failed_groups += 1 - end - end - - @info "Physical groups assigned: $(successful_groups) successful, $(failed_groups) failed out of $(length(entities_by_physical_group_tag)) total" -end diff --git a/src/engine/fem/lineparamopts.jl b/src/engine/fem/lineparamopts.jl deleted file mode 100644 index e8eba978f..000000000 --- a/src/engine/fem/lineparamopts.jl +++ /dev/null @@ -1,34 +0,0 @@ -Base.@kwdef struct FEMOptions <: AbstractFormulationOptions - common::LineParamOptions = LineParamOptions() - - "Build mesh only and preview (no solving)" - mesh_only::Bool = false - "Force mesh regeneration even if file exists" - force_remesh::Bool = false - "Generate field visualization outputs" - plot_field_maps::Bool = true - "Archive temporary files after each frequency run" - keep_run_files::Bool = false - - "Base path for output files" - save_path::String = joinpath(".", "fem_output") - "Path to GetDP executable" - getdp_executable::Union{String, Nothing} = nothing -end - -const _FEM_OWN = Tuple(s for s in fieldnames(FEMOptions) if s != :common) -@inline Base.hasproperty(::FEMOptions, s::Symbol) = - (s in _FEM_OWN) || (s in _COMMON_SYMS) || s === :common - -@inline function Base.getproperty(o::FEMOptions, s::Symbol) - s === :common && return getfield(o, :common) - (s in _FEM_OWN) && return getfield(o, s) # FEM-specific - (s in _COMMON_SYMS) && return getfield(o.common, s) # forwarded common - throw(ArgumentError("Unknown option $(s) for $(typeof(o))")) -end - -Base.propertynames(::FEMOptions, ::Bool = false) = (_COMMON_SYMS..., _FEM_OWN..., :common) -Base.get(o::FEMOptions, s::Symbol, default) = - hasproperty(o, s) ? getproperty(o, s) : default -asnamedtuple(o::FEMOptions) = (; (k=>getproperty(o, k) for k in propertynames(o))...) -# asnamedtuple(o::FEMOptions) = (; (k=>getproperty(o,k) for k in propertynames(o) if k != :common)...) diff --git a/src/engine/fem/materialprops.jl b/src/engine/fem/materialprops.jl deleted file mode 100644 index 9599143bc..000000000 --- a/src/engine/fem/materialprops.jl +++ /dev/null @@ -1,127 +0,0 @@ -""" -Material handling functions for the FEMTools.jl module. -These functions handle the management of material properties. -""" - -""" -$(TYPEDSIGNATURES) - -Get the name of a material from a materials library. - -# Arguments - -- `material`: The [`Material`](@ref) object to find. -- `library`: The [`MaterialsLibrary`](@ref) to search in. -- `tol`: Tolerance for floating-point comparisons \\[dimensionless\\]. Default: 1e-6. - -# Returns - -- The name of the material if found, or a hash-based name if not found. - -# Examples - -```julia -name = $(FUNCTIONNAME)(material, materials) -``` -""" -function get_material_name(material::Material, library::MaterialsLibrary; tol = 1e-6) - # If material has infinite resistivity, it's air - if isinf(to_nominal(material.rho)) - return "air" - end - - # Convert values to nominal (remove uncertainties) - rho = to_nominal(material.rho) - eps_r = to_nominal(material.eps_r) - mu_r = to_nominal(material.mu_r) - alpha = to_nominal(material.alpha) - - # Try to find an exact match - for (name, lib_material) in library - # Check if all properties match within tolerance - if isapprox(rho, to_nominal(lib_material.rho), rtol = tol) && - isapprox(eps_r, to_nominal(lib_material.eps_r), rtol = tol) && - isapprox(mu_r, to_nominal(lib_material.mu_r), rtol = tol) && - isapprox(alpha, to_nominal(lib_material.alpha), rtol = tol) - return name - end - end - - # If no match, create a unique hash-based name - return "material_" * hash_material_properties(material) -end - -""" -$(TYPEDSIGNATURES) - -Create a hash string based on material properties. - -# Arguments - -- `material`: The [`Material`](@ref) object to hash. - -# Returns - -- A string hash of the material properties. - -# Examples - -```julia -hash = $(FUNCTIONNAME)(material) -``` -""" -function hash_material_properties(material::Material) - # Create a deterministic hash based on material properties - rho = to_nominal(material.rho) - eps_r = to_nominal(material.eps_r) - mu_r = to_nominal(material.mu_r) - - rho_str = isinf(rho) ? "inf" : "$(round(rho, sigdigits=6))" - eps_str = "$(round(eps_r, sigdigits=6))" - mu_str = "$(round(mu_r, sigdigits=6))" - - return "rho=$(rho_str)_epsr=$(eps_str)_mu=$(mu_str)" -end - - -function get_earth_model_material(workspace::FEMWorkspace, layer_idx::Int) - - earth_props = workspace.problem_def.earth_props - num_layers = length(earth_props.layers) - - if layer_idx <= num_layers - - # Create a material with the earth properties - rho = to_nominal(earth_props.layers[layer_idx].base_rho_g) # Layer 1 is air, Layer 2 is first earth layer - eps_r = to_nominal(earth_props.layers[layer_idx].base_epsr_g) - mu_r = to_nominal(earth_props.layers[layer_idx].base_mur_g) - - return Material(rho, eps_r, mu_r, 20.0, 0.0) - else - # Default to bottom earth layer if layer_idx is out of bounds - - # Create a material with the earth properties - rho = to_nominal(earth_props.layers[end].base_rho_g) # Layer 1 is air, Layer 2 is first earth layer - eps_r = to_nominal(earth_props.layers[end].base_epsr_g) - mu_r = to_nominal(earth_props.layers[end].base_mur_g) - - return Material(rho, eps_r, mu_r, 20.0, 0.0) - end -end - -function get_air_material(workspace::FEMWorkspace) - if !isnothing(workspace.formulation.materials) - airm = get(workspace.formulation.materials, "air") - - if isnothing(airm) - @warn("Air material not found in database. Overriding with default properties.") - air_material = Material(Inf, 1.0, 1.0, 20.0, 0.0) - else - rho = to_nominal(airm.rho) - eps_r = to_nominal(airm.eps_r) - mu_r = to_nominal(airm.mu_r) - air_material = Material(rho, eps_r, mu_r, 20.0, 0.0) - end - end - return air_material -end diff --git a/src/engine/fem/mesh.jl b/src/engine/fem/mesh.jl deleted file mode 100644 index e621fec3e..000000000 --- a/src/engine/fem/mesh.jl +++ /dev/null @@ -1,351 +0,0 @@ -""" -Mesh generation functions for the FEMTools.jl module. -These functions handle the configuration and generation of the mesh. -""" - -""" -$(TYPEDSIGNATURES) - -Calculate the skin depth for a conductive material. - -# Arguments - -- `rho`: Electrical resistivity \\[Ω·m\\]. -- `mu_r`: Relative permeability \\[dimensionless\\]. -- `freq`: Frequency \\[Hz\\]. - -# Returns - -- Skin depth \\[m\\]. - -# Examples - -```julia -depth = $(FUNCTIONNAME)(1.7241e-8, 1.0, 50.0) -``` - -# Notes - -```math -\\delta = \\sqrt{\\frac{\\rho}{\\pi \\cdot f \\cdot \\mu_0 \\cdot \\mu_r}} -``` - -where \\(\\mu_0 = 4\\pi \\times 10^{-7}\\) H/m is the vacuum permeability. -""" -function calc_skin_depth(rho::Number, mu_r::Number, freq::Number) - # Convert to nominal values in case of Measurement types - rho = to_nominal(rho) - mu_r = to_nominal(mu_r) - - # Constants - mu_0 = 4e-7 * π # Vacuum permeability - - # Calculate skin depth - # δ = sqrt(ρ / (π * f * μ_0 * μ_r)) - return sqrt(rho / (π * freq * mu_0 * mu_r)) -end - -function _calc_mesh_size(part::AbstractCablePart, workspace::FEMWorkspace) - - # Extract geometric properties - r_in = to_nominal(part.r_in) - r_ex = to_nominal(part.r_ex) - thickness = r_ex - r_in - - # Extract formulation parameters - formulation = workspace.formulation - - # Calculate mesh size based on part type and properties - scale_length = thickness - if part isa CircStrands - # For wire arrays, consider the wire radius - scale_length = to_nominal(part.radius_wire) * 2 - num_elements = formulation.elements_per_length_conductor - elseif part isa AbstractConductorPart - num_elements = formulation.elements_per_length_conductor - elseif part isa Insulator - num_elements = formulation.elements_per_length_insulator - elseif part isa Semicon - num_elements = formulation.elements_per_length_semicon - end - - # Apply bounds from configuration - mesh_size = scale_length / num_elements - mesh_size = max(mesh_size, formulation.mesh_size_min) - mesh_size = min(mesh_size, formulation.mesh_size_max) - - return mesh_size -end - -function _calc_mesh_size( - r_in::Number, - r_ex::Number, - material::Material, - num_elements::Int, - workspace::FEMWorkspace, -) - # Extract geometric properties - thickness = r_ex - r_in - - # Extract problem_def parameters - formulation = workspace.formulation - mesh_size = thickness / num_elements - - # Apply bounds from configuration - mesh_size = max(mesh_size, formulation.mesh_size_min) - mesh_size = min(mesh_size, formulation.mesh_size_max) - - return mesh_size -end - -""" -$(TYPEDSIGNATURES) - -Configure mesh sizes for all entities in the model. - -# Arguments - -- `workspace`: The [`FEMWorkspace`](@ref) containing the entities. - -# Returns - -- Nothing. Updates the mesh size map in the workspace. - -# Examples - -```julia -$(FUNCTIONNAME)(workspace) -``` -""" -function config_mesh_options(workspace::FEMWorkspace) - - - gmsh.option.set_number("General.InitialModule", 2) - - # Set mesh algorithm - gmsh.option.set_number("Mesh.Algorithm", workspace.formulation.mesh_algorithm) - gmsh.option.set_number("Mesh.AlgorithmSwitchOnFailure", 1) - # Set mesh optimization parameters - gmsh.option.set_number("Mesh.Optimize", 0) - gmsh.option.set_number("Mesh.OptimizeNetgen", 0) - - # Set mesh globals - gmsh.option.set_number("Mesh.SaveAll", 1) # Mesh all regions - gmsh.option.set_number("Mesh.MaxRetries", workspace.formulation.mesh_max_retries) - gmsh.option.set_number("Mesh.MeshSizeMin", workspace.formulation.mesh_size_min) - gmsh.option.set_number("Mesh.MeshSizeMax", workspace.formulation.mesh_size_max) - gmsh.option.set_number("Mesh.MeshSizeFromPoints", 1) - gmsh.option.set_number("Mesh.MeshSizeFromParametricPoints", 0) - - gmsh.option.set_number("Mesh.MeshSizeExtendFromBoundary", 1) - gmsh.option.set_number( - "Mesh.MeshSizeFromCurvature", - workspace.formulation.points_per_circumference, - ) - - - @debug "Mesh algorithm: $(workspace.formulation.mesh_algorithm)" - @debug "Mesh size range: [$(workspace.formulation.mesh_size_min), $(workspace.formulation.mesh_size_max)]" -end - -""" -$(TYPEDSIGNATURES) - -Generate the mesh. - -# Arguments - -- `workspace`: The [`FEMWorkspace`](@ref) containing the model. - -# Returns - -- Nothing. Generates the mesh in the Gmsh model. - -# Examples - -```julia -$(FUNCTIONNAME)(workspace) -``` -""" -function generate_mesh(workspace::FEMWorkspace) - # Generate 2D mesh - gmsh.model.mesh.generate(2) - - # Get mesh statistics - nodes = gmsh.model.mesh.get_nodes() - elements = gmsh.model.mesh.get_elements() - - num_nodes = length(nodes[1]) - num_elements = sum(length.(elements[2])) - - @info "Mesh generation completed" - @info "Created mesh with $(num_nodes) nodes and $(num_elements) elements" -end - -""" -$(TYPEDSIGNATURES) - -Initialize a Gmsh model with appropriate settings. - -# Arguments - -- `case_id`: Identifier for the model. -- `problem_def`: The [`FEMFormulation`](@ref) containing mesh parameters. -- `solver`: The [`FEMSolver`](@ref) containing visualization parameters. - -# Returns - -- Nothing. Initializes the Gmsh model. - -# Examples - -```julia -$(FUNCTIONNAME)("test_case", problem_def, solver) -``` -""" -function initialize_gmsh(workspace::FEMWorkspace) - # Create a new model - system_id = workspace.problem_def.system.system_id - gmsh.model.add(system_id) - - # Module launched on startup (0: automatic, 1: geometry, 2: mesh, 3: solver, 4: post-processing) - gmsh.option.set_number("General.InitialModule", 0) - gmsh.option.set_string("General.DefaultFileName", system_id * ".geo") - - # Define verbosity level - gmsh_verbosity = map_verbosity_to_gmsh(workspace.opts.verbosity) - gmsh.option.set_number("General.Verbosity", gmsh_verbosity) - - # Set OCC model healing options - gmsh.option.set_number("Geometry.AutoCoherence", 1) - gmsh.option.set_number("Geometry.OCCFixDegenerated", 1) - gmsh.option.set_number("Geometry.OCCFixSmallEdges", 1) - gmsh.option.set_number("Geometry.OCCFixSmallFaces", 1) - gmsh.option.set_number("Geometry.OCCSewFaces", 1) - gmsh.option.set_number("Geometry.OCCMakeSolids", 1) - - # Log settings based on verbosity - @info "Initialized Gmsh model: $system_id" - -end - -function _do_make_mesh!(workspace::FEMWorkspace) - - # Initialize Gmsh model and set parameters - initialize_gmsh(workspace) - - # Create geometry - @info "Creating domain boundaries..." - make_space_geometry(workspace) - - @info "Creating cable geometry..." - make_cable_geometry(workspace) - - # Synchronize the model - gmsh.model.occ.synchronize() - - # Boolean operations - @info "Performing boolean operations..." - process_fragments(workspace) - - # Entity identification and entity assignment - @info "Identifying entities after fragmentation..." - identify_by_marker(workspace) - - # Physical group assignment - @info "Assigning physical groups..." - assign_physical_groups(workspace) - - # Mesh sizing - @info "Setting up mesh sizing..." - config_mesh_options(workspace) - - # Mesh generation - @info "Generating mesh..." - generate_mesh(workspace) - - # Save mesh - @info "Saving mesh to file: $(display_path(workspace.paths[:mesh_file]))" - gmsh.write(workspace.paths[:mesh_file]) - - # Save geometry - @info "Saving geometry to file: $(display_path(workspace.paths[:geo_file]))" - gmsh.write(workspace.paths[:geo_file]) -end - -function mesh_exists(workspace::FEMWorkspace) - mesh_file = workspace.paths[:mesh_file] - - # Force remesh overrides everything - if workspace.opts.force_remesh - @debug "Force remesh requested" - return false - end - - # If workspace is empty (no entities), force remesh regardless of file existence - if isempty(workspace.conductors) && isempty(workspace.insulators) && - isempty(workspace.space_regions) && isempty(workspace.boundaries) && - isempty(workspace.physical_groups) && isempty(workspace.material_registry) - @warn "Empty workspace detected - forcing remesh" - return false - end - - # Check if mesh file exists - if !isfile(mesh_file) - @debug "No existing mesh file found" - return false - end - - # Mesh exists - can reuse - @debug "Existing mesh found and will be reused" - return true -end - -function make_mesh!(workspace::FEMWorkspace) - # If mesh exists and we are not forcing a remesh, do nothing and continue. - if mesh_exists(workspace) - @info "Using existing mesh" - return false # Signal to continue to solver - end - - # --- Mesh generation is required from this point on --- - @info "Building mesh for system: $(workspace.problem_def.system.system_id)" - - try - # Ensure Gmsh is initialized - if gmsh.is_initialized() == 0 - gmsh.initialize() - end - - # Perform the actual meshing - _do_make_mesh!(workspace) - @info "Mesh generation completed" - - # Handle mesh-only mode: preview the mesh and stop. - # The Gmsh session is still active here. - if workspace.opts.mesh_only - @info "Mesh-only mode: Opening preview. Close the preview window to continue." - preview_mesh(workspace) - @info "Preview closed. Halting computation as per mesh_only=true." - return true # Signal to stop computation - end - - catch e - @error "An error occurred during mesh generation or preview" exception = e - rethrow(e) - finally - # CRITICAL: Finalize Gmsh only after all operations, including the - # potential preview, are complete. This ensures the session is - # always closed cleanly. - if gmsh.is_initialized() == 1 - try - gmsh.finalize() - catch fin_err - @warn "Gmsh finalization error" exception = fin_err - end - end - end - - # If we are not in mesh_only mode, signal to continue to the solver. - return false -end diff --git a/src/engine/fem/meshtransitions.jl b/src/engine/fem/meshtransitions.jl deleted file mode 100644 index 1ef9bed8c..000000000 --- a/src/engine/fem/meshtransitions.jl +++ /dev/null @@ -1,108 +0,0 @@ -""" -$(TYPEDEF) - -Defines a mesh transition region for improved mesh quality in earth/air regions around cable systems. - -$(TYPEDFIELDS) -""" -struct MeshTransition - "Center coordinates (x, y) [m]" - center::Tuple{Float64, Float64} - "Minimum radius (must be ≥ bounding radius of cables) [m]" - r_min::Float64 - "Maximum radius [m]" - r_max::Float64 - "Minimum mesh size factor at r_min [m]" - mesh_factor_min::Float64 - "Maximum mesh size factor at r_max [m]" - mesh_factor_max::Float64 - "Number of transition regions [dimensionless]" - n_regions::Int - "Earth layer index (1=air, 2+=earth layers from top to bottom, nothing=auto-detect)" - earth_layer::Union{Int, Nothing} - - function MeshTransition( - center, - r_min, - r_max, - mesh_factor_min, - mesh_factor_max, - n_regions, - earth_layer, - ) - # Basic validation - r_min >= 0 || Base.error("r_min must be greater than or equal to 0") - r_max > r_min || Base.error("r_max must be greater than r_min") - mesh_factor_min > 0 || Base.error("mesh_factor_min must be positive") - mesh_factor_max <= 1 || - Base.error("mesh_factor_max must be smaller than or equal to 1") - mesh_factor_max > mesh_factor_min || - Base.error("mesh_factor_max must be > mesh_factor_min") - n_regions >= 1 || Base.error("n_regions must be at least 1") - - # Validate earth_layer if provided - if !isnothing(earth_layer) - earth_layer >= 1 || - Base.error("earth_layer must be >= 1 (1=air, 2+=earth layers)") - end - - new(center, r_min, r_max, mesh_factor_min, mesh_factor_max, n_regions, earth_layer) - end -end - -# Convenience constructor -function MeshTransition( - cable_system::LineCableSystem, - cable_indices::Vector{Int}; - r_min::Number, - r_length::Number, - mesh_factor_min::Number, - mesh_factor_max::Number, - n_regions::Int = 3, - earth_layer::Union{Int, Nothing} = nothing, -) - (r_min, r_length, mesh_factor_min, mesh_factor_max) = - to_nominal.((r_min, r_length, mesh_factor_min, mesh_factor_max)) - - # Validate cable indices - all(1 <= idx <= length(cable_system.cables) for idx in cable_indices) || - Base.error("Cable indices out of bounds") - - isempty(cable_indices) && Base.error("Cable indices cannot be empty") - - # Get centroid and bounding radius - cx, cy, bounding_radius, _ = - to_nominal.(get_system_centroid(cable_system, cable_indices)) - - # Calculate parameters - if r_min < bounding_radius - @warn "r_min ($r_min m) is smaller than bounding radius ($bounding_radius m). Adjusting r_min to match." - r_min = bounding_radius - end - - r_max = r_min + r_length - - # Auto-detect layer if not specified - if isnothing(earth_layer) - # Simple detection: y >= 0 is air (layer 1), y < 0 is first earth layer (layer 2) - earth_layer = cy >= 0 ? 1 : 2 - @debug "Auto-detected earth_layer=$earth_layer for transition at ($cx, $cy)" - end - - # Validate no surface crossing for underground transitions - if earth_layer > 1 && cy + r_max > 0 - Base.error( - "Transition region would cross earth surface (y=0). Reduce r_length or use separate transition regions.", - ) - end - - return MeshTransition( - (cx, cy), - r_min, - r_max, - mesh_factor_min, - mesh_factor_max, - n_regions, - earth_layer, - ) -end diff --git a/src/engine/fem/problemdefs.jl b/src/engine/fem/problemdefs.jl deleted file mode 100644 index 70b6022f3..000000000 --- a/src/engine/fem/problemdefs.jl +++ /dev/null @@ -1,190 +0,0 @@ - -# @kwdef struct FEMOptions <: AbstractFormulationOptions -# "Build mesh only and preview (no solving)" -# mesh_only::Bool = false -# "Force mesh regeneration even if file exists" -# force_remesh::Bool = false -# "Skip user confirmation for overwriting results" -# force_overwrite::Bool = false -# "Generate field visualization outputs" -# plot_field_maps::Bool = true -# "Archive temporary files after each frequency run" -# keep_run_files::Bool = false -# "Reduce bundle conductors to equivalent single conductor" -# reduce_bundle::Bool = true -# "Eliminate grounded conductors from the system (Kron reduction)" -# kron_reduction::Bool = true -# "Enforce ideal transposition transposition/snaking" -# ideal_transposition::Bool = true -# "Temperature correction" -# temperature_correction::Bool = true -# "Base path for output files" -# save_path::String = joinpath(".", "fem_output") -# "Path to GetDP executable" -# getdp_executable::Union{String, Nothing} = nothing -# "Verbosity level" -# verbosity::Int = 0 -# "Log file path" -# logfile::Union{String, Nothing} = nothing -# end - -# # The one-line constructor to "promote" a NamedTuple -# FEMOptions(opts::NamedTuple) = FEMOptions(; opts...) - - - -""" -$(TYPEDEF) - -Abstract problem definition type for FEM simulation parameters. -This contains the physics-related parameters of the simulation. - -$(TYPEDFIELDS) -""" -struct FEMFormulation <: AbstractFormulationSet - "Radius of the physical domain \\[m\\]." - domain_radius::Float64 - "Outermost radius to apply the infinity transform \\[m\\]." - domain_radius_inf::Float64 - "Elements per characteristic length for conductors \\[dimensionless\\]." - elements_per_length_conductor::Int - "Elements per characteristic length for insulators \\[dimensionless\\]." - elements_per_length_insulator::Int - "Elements per characteristic length for semiconductors \\[dimensionless\\]." - elements_per_length_semicon::Int - "Elements per characteristic length for interfaces \\[dimensionless\\]." - elements_per_length_interfaces::Int - "Points per circumference length (2π radians) \\[dimensionless\\]." - points_per_circumference::Int - "Analysis types to perform \\[dimensionless\\]." - analysis_type::Tuple{AbstractImpedanceFormulation, AbstractAdmittanceFormulation} - "Minimum mesh size \\[m\\]." - mesh_size_min::Float64 - "Maximum mesh size \\[m\\]." - mesh_size_max::Float64 - "Default mesh size \\[m\\]." - mesh_size_default::Float64 - "Mesh transition regions for improved mesh quality" - mesh_transitions::Vector{MeshTransition} - "Mesh algorithm to use \\[dimensionless\\]." - mesh_algorithm::Int - "Maximum meshing retries and number of recursive subdivisions \\[dimensionless\\]." - mesh_max_retries::Int - "Materials database." - materials::MaterialsLibrary - "Solver options for FEM simulations." - options::FEMOptions - """ - $(TYPEDSIGNATURES) - - Constructs a [`FEMFormulation`](@ref) instance with default values. - - # Arguments - - - `domain_radius`: Domain radius for the simulation \\[m\\]. Default: 5.0. - - `elements_per_length_conductor`: Elements per scale length for conductors \\[dimensionless\\]. Default: 3.0. - - `elements_per_length_insulator`: Elements per scale length for insulators \\[dimensionless\\]. Default: 2.0. - - `elements_per_length_semicon`: Elements per scale length for semiconductors \\[dimensionless\\]. Default: 4.0. - - `elements_per_length_interfaces`: Elements per scale length for interfaces \\[dimensionless\\]. Default: 0.1. - - `analysis_type`: - - `mesh_size_min`: Minimum mesh size \\[m\\]. Default: 1e-4. - - `mesh_size_max`: Maximum mesh size \\[m\\]. Default: 1.0. - - `mesh_size_default`: Default mesh size \\[m\\]. Default: `domain_radius/10`. - - `mesh_algorithm`: Mesh algorithm to use \\[dimensionless\\]. Default: 6. - - `materials`: Materials database. Default: MaterialsLibrary(). - - # Returns - - - A [`FEMFormulation`](@ref) instance with the specified parameters. - - # Examples - - ```julia - # Create a problem definition with default parameters - formulation = $(FUNCTIONNAME)() - - # Create a problem definition with custom parameters - formulation = $(FUNCTIONNAME)( - domain_radius=10.0, - elements_per_length_conductor=5.0, - mesh_algorithm=2 - ) - ``` - """ - function FEMFormulation(; - impedance::AbstractImpedanceFormulation, - admittance::AbstractAdmittanceFormulation, - domain_radius::Float64, - domain_radius_inf::Float64, - elements_per_length_conductor::Int, - elements_per_length_insulator::Int, - elements_per_length_semicon::Int, - elements_per_length_interfaces::Int, - points_per_circumference::Int, - mesh_size_min::Float64, - mesh_size_max::Float64, - mesh_size_default::Float64, - mesh_transitions::Vector{MeshTransition}, - mesh_algorithm::Int, - mesh_max_retries::Int, - materials::MaterialsLibrary, - options::FEMOptions, - ) - - return new( - domain_radius, domain_radius_inf, - elements_per_length_conductor, elements_per_length_insulator, - elements_per_length_semicon, elements_per_length_interfaces, - points_per_circumference, (impedance, admittance), - mesh_size_min, mesh_size_max, mesh_size_default, - mesh_transitions, mesh_algorithm, mesh_max_retries, materials, - options, - ) - end -end - -# Wrapper function to create a FEMFormulation -function FormulationSet(::Val{:FEM}; impedance::AbstractImpedanceFormulation = Darwin(), - admittance::AbstractAdmittanceFormulation = Electrodynamics(), - domain_radius::Float64 = 5.0, - domain_radius_inf::Float64 = 6.25, - elements_per_length_conductor::Int = 3, - elements_per_length_insulator::Int = 2, - elements_per_length_semicon::Int = 4, - elements_per_length_interfaces::Int = 3, - points_per_circumference::Int = 16, - mesh_size_min::Float64 = 1e-4, - mesh_size_max::Float64 = 1.0, - mesh_size_default::Float64 = domain_radius / 10, - mesh_transitions::Vector{MeshTransition} = MeshTransition[], - mesh_algorithm::Int = 5, - mesh_max_retries::Int = 20, - materials::MaterialsLibrary = MaterialsLibrary(), - options = (;), -) - # Resolve solver path - validated_path = _resolve_getdp_path(options) - - # Create a new NamedTuple with the validated path overwriting any user value - final_opts = merge(options, (getdp_executable = validated_path,)) - fem_opts = build_options(FEMOptions, final_opts; strict = true) - - return FEMFormulation(; impedance = impedance, - admittance = admittance, - domain_radius = domain_radius, - domain_radius_inf = domain_radius_inf, - elements_per_length_conductor = elements_per_length_conductor, - elements_per_length_insulator = elements_per_length_insulator, - elements_per_length_semicon = elements_per_length_semicon, - elements_per_length_interfaces = elements_per_length_interfaces, - points_per_circumference = points_per_circumference, - mesh_size_min = mesh_size_min, - mesh_size_max = mesh_size_max, - mesh_size_default = mesh_size_default, - mesh_transitions = mesh_transitions, - mesh_algorithm = mesh_algorithm, - mesh_max_retries = mesh_max_retries, - materials = materials, - options = fem_opts, - ) -end diff --git a/src/engine/fem/solver.jl b/src/engine/fem/solver.jl deleted file mode 100644 index baed15ab4..000000000 --- a/src/engine/fem/solver.jl +++ /dev/null @@ -1,872 +0,0 @@ - -function make_fem_problem!( - fem_formulation::Union{AbstractImpedanceFormulation, AbstractAdmittanceFormulation}, - frequency::Float64, - workspace::FEMWorkspace, -) - - fem_formulation.problem = GetDP.Problem() - define_jacobian!(fem_formulation.problem, workspace) - define_integration!(fem_formulation.problem) - define_material_props!(fem_formulation.problem, workspace) - define_constants!(fem_formulation.problem, fem_formulation, frequency) - define_domain_groups!(fem_formulation.problem, fem_formulation, workspace) - define_constraint!(fem_formulation.problem, fem_formulation, workspace) - define_resolution!(fem_formulation.problem, fem_formulation, workspace) - - make_problem!(fem_formulation.problem) - fem_formulation.problem.filename = - fem_formulation isa AbstractImpedanceFormulation ? - workspace.paths[:impedance_file] : workspace.paths[:admittance_file] - write_file(fem_formulation.problem) -end - -function define_jacobian!(problem::GetDP.Problem, workspace::FEMWorkspace) - # Initialize Jacobian - jac = Jacobian() - - Rint = workspace.formulation.domain_radius - Rext = workspace.formulation.domain_radius_inf - - # Add Vol Jacobian - vol = add!(jac, "Vol") - add!(vol; - Region = "DomainInf", - Jacobian = VolSphShell( - Rint = Rint, - Rext = Rext, - center_X = 0.0, - center_Y = 0.0, - center_Z = 0.0, - ), - ) - add!(vol; Region = "All", Jacobian = "Vol") - - # Add Sur Jacobian - sur = add!(jac, "Sur") - add!(sur; - Region = "All", - Jacobian = "Sur", - ) - - # Add Jacobian to problem - problem.jacobian = jac -end - -function define_integration!(problem::GetDP.Problem) - # Initialize Integration - integ = Integration() - i1 = add!(integ, "I1") - case = add!(i1) - geo_case = add_nested_case!(case; type = "Gauss") - add!(geo_case; GeoElement = "Point", NumberOfPoints = 1) - add!(geo_case; GeoElement = "Line", NumberOfPoints = 4) - add!(geo_case; GeoElement = "Triangle", NumberOfPoints = 4) - add!(geo_case; GeoElement = "Quadrangle", NumberOfPoints = 4) - problem.integration = integ - -end - -function define_material_props!(problem::GetDP.Problem, workspace::FEMWorkspace) - # Create material properties function - func = GetDP.Function() - - for (tag, mat) in workspace.physical_groups - if tag > 10^8 - # Add material properties for this region - add_comment!( - func, - "Material properties for region $(tag): $(create_physical_group_name(workspace, tag))", - false, - ) - add_space!(func) - add!(func, "nu", expression = 1 / (mat.mu_r * μ₀), region = [tag]) - add!( - func, - "sigma", - expression = isinf(mat.rho) ? 0.0 : 1 / mat.rho, - region = [tag], - ) - add!(func, "epsilon", expression = mat.eps_r * ε₀, region = [tag]) - end - end - - push!(problem.function_obj, func) -end - -function define_constants!( - problem::GetDP.Problem, - fem_formulation::Union{AbstractImpedanceFormulation, AbstractAdmittanceFormulation}, - frequency::Float64, -) - func = GetDP.Function() - - add_constant!(func, "Freq", frequency) - add_constant!(func, "UnitAmplitude", 1.0) - push!(problem.function_obj, func) -end - -function define_domain_groups!( - problem::GetDP.Problem, - fem_formulation::Union{AbstractImpedanceFormulation, AbstractAdmittanceFormulation}, - workspace::FEMWorkspace, -) - - material_reg = Dict{Symbol, Vector{Int}}( - :DomainC => Int[], - :DomainCC => Int[], - :DomainInf => Int[], - ) - inds_reg = Int[] - cables_reg = Dict{Int, Vector{Int}}() - boundary_reg = Int[] - add_raw_code!(problem, - """ - DefineConstant[ - active_con = {1, Choices{1,9999}, Name "Input/Active conductor", Visible 1}]; - """) - for tag in keys(workspace.physical_groups) - if tag > 10^8 - # Decode tag information - surface_type, entity_num, component_num, material_group, _ = - decode_physical_group_tag(tag) - - # Categorize regions - if surface_type == 1 - push!(get!(cables_reg, entity_num, Int[]), tag) - if material_group == 1 - push!(inds_reg, tag) - end - end - if material_group == 1 - push!(material_reg[:DomainC], tag) - elseif material_group == 2 - push!(material_reg[:DomainCC], tag) - end - - surface_type == 3 && push!(material_reg[:DomainInf], tag) - - else - decode_boundary_tag(tag)[1] == 2 && push!(boundary_reg, tag) - end - end - inds_reg = sort(inds_reg) - material_reg[:DomainC] = sort(material_reg[:DomainC]) - material_reg[:DomainCC] = sort(material_reg[:DomainCC]) - - # Create and configure groups - group = GetDP.Group() - - # Add common domains - add!( - group, - "DomainInf", - material_reg[:DomainInf], - "Region", - comment = "Domain transformation to infinity", - ) - - for (key, tag) in enumerate(inds_reg) - add!(group, "Con_$key", [tag], "Region"; - comment = "$(create_physical_group_name(workspace, tag))") - end - - add!(group, "Conductors", inds_reg, "Region") - - # Add standard FEM domains - domain_configs = [ - ("DomainC", Int[], "All conductor materials"), - ("DomainCC", Int[], "All non-conductor materials"), - ("DomainActive", ["Con~{active_con}"], "Sources"), - ( - "DomainInactive", - ["Conductors - Con~{active_con}"], - "Conductors set to zero energization", - ), - ] - - for (name, regions, comment) in domain_configs - add!(group, name, regions, "Region"; comment = comment) - end - - for tag in material_reg[:DomainC] - add!(group, "DomainC", [tag], "Region"; - operation = "+=", - comment = "$(create_physical_group_name(workspace, tag))") - end - - for tag in material_reg[:DomainCC] - add!(group, "DomainCC", [tag], "Region"; - operation = "+=", - comment = "$(create_physical_group_name(workspace, tag))") - end - - if fem_formulation isa AbstractAdmittanceFormulation - add!(group, "Domain_Ele", ["DomainCC", "DomainC"], "Region") - add!(group, "Sur_Dirichlet_Ele", boundary_reg, "Region") - else - # Add domain groups - add!(group, "Domain_Mag", ["DomainCC", "DomainC"], "Region") - add!(group, "Sur_Dirichlet_Mag", boundary_reg, "Region") - end - - problem.group = group -end - -function define_constraint!( - problem::GetDP.Problem, - fem_formulation::Union{AbstractImpedanceFormulation, AbstractAdmittanceFormulation}, - workspace::FEMWorkspace, -) - constraint = GetDP.Constraint() - - # num_cores = workspace.problem_def.system.num_cables - - if fem_formulation isa AbstractAdmittanceFormulation - # ScalarPotential_2D - esp = assign!(constraint, "ScalarPotential_2D") - case!(esp, "DomainInactive", value = "0.0") - case!(esp, "Con~{active_con}", value = "UnitAmplitude") - case!(esp, "Sur_Dirichlet_Ele", value = "0.0") - - charge = assign!(constraint, "Charge_2D") - else - # MagneticVectorPotential_2D - mvp = assign!(constraint, "MagneticVectorPotential_2D") - case!(mvp, "Sur_Dirichlet_Mag", value = "0.0") - - # Voltage_2D (placeholder) - voltage = assign!(constraint, "Voltage_2D") - case!(voltage, "") - - # Current_2D - current = assign!(constraint, "Current_2D") - - case!(current, "DomainInactive", value = "0.0") - case!(current, "Con~{active_con}", value = "UnitAmplitude") - end - - problem.constraint = constraint - -end - -function define_resolution!( - problem::GetDP.Problem, - formulation::Electrodynamics, - workspace::FEMWorkspace, -) - resolution_name = formulation.resolution_name - num_sources = workspace.problem_def.system.num_cables - - # FunctionSpace section - functionspace = FunctionSpace() - fs1 = add!(functionspace, "Hgrad_v_Ele", nothing, nothing, Type = "Form0") - add_basis_function!( - functionspace, - "sn", - "vn", - "BF_Node"; - Support = "Domain_Ele", - Entity = "NodesOf[ All, Not Conductors ]", - ) - add_basis_function!( - functionspace, - "sf", - "vf", - "BF_GroupOfNodes"; - Support = "Domain_Ele", - Entity = "GroupsOfNodesOf[ Conductors ]", - ) - add_global_quantity!(functionspace, "U", "AliasOf"; NameOfCoef = "vf") - add_global_quantity!(functionspace, "Q", "AssociatedWith"; NameOfCoef = "vf") - add_constraint!(functionspace, "U", "Region", "ScalarPotential_2D") - add_constraint!(functionspace, "Q", "Region", "Charge_2D") - add_constraint!(functionspace, "vn", "NodesOf", "ScalarPotential_2D") - - problem.functionspace = functionspace - - # Formulation section - formulation = Formulation() - form = add!(formulation, "Electrodynamics_v", "FemEquation") - add_quantity!(form, "v", Type = "Local", NameOfSpace = "Hgrad_v_Ele") - add_quantity!(form, "U", Type = "Global", NameOfSpace = "Hgrad_v_Ele [U]") - add_quantity!(form, "Q", Type = "Global", NameOfSpace = "Hgrad_v_Ele [Q]") - - eq = add_equation!(form) - add!( - eq, - "Galerkin", - "[ sigma[] * Dof{d v} , {d v} ]", - In = "Domain_Ele", - Jacobian = "Vol", - Integration = "I1", - ) - add!( - eq, - "Galerkin", - "DtDof[ epsilon[] * Dof{d v} , {d v} ]", - In = "DomainCC", - Jacobian = "Vol", - Integration = "I1", - ) #CHECKME - add!(eq, "GlobalTerm", "[ Dof{Q} , {U} ]", In = "Conductors") - - problem.formulation = formulation - - # Resolution section - output_dir = joinpath("results", lowercase(resolution_name)) - output_dir = replace(output_dir, "\\" => "/") # for compatibility with Windows paths - resolution = Resolution() - add!(resolution, resolution_name, "Sys_Ele", - NameOfFormulation = "Electrodynamics_v", - Type = "Complex", - Frequency = "Freq", - Operation = [ - "CreateDir[\"$(output_dir)\"]", - "Generate[Sys_Ele]", - "Solve[Sys_Ele]", - "SaveSolution[Sys_Ele]", - "PostOperation[LineParams]", - ]) - - problem.resolution = resolution - - # PostProcessing section - postprocessing = PostProcessing() - pp = add!(postprocessing, "EleDyn_v", "Electrodynamics_v") - - # Add field maps quantities - for (name, expr, options) in [ - ("v", "{v}", Dict()), - ("e", "-{d v}", Dict()), - ("em", "Norm[-{d v}]", Dict()), - ("d", "-epsilon[] * {d v}", Dict()), - ("dm", "Norm[-epsilon[] * {d v}]", Dict()), - ("j", "-sigma[] * {d v}", Dict()), - ("jm", "Norm[-sigma[] * {d v}]", Dict()), - ] - q = add!(pp, name) - add!(q, "Term", expr; In = "Domain_Ele", Jacobian = "Vol", options...) - end - - # Add jtot (combination of j and d) - q = add!(pp, "jtot") - add!( - q, - "Term", - "-sigma[] * {d v}"; - Type = "Global", - In = "Domain_Ele", - Jacobian = "Vol", - ) - add!( - q, - "Term", - "-epsilon[] * Dt[{d v}]"; - Type = "Global", - In = "Domain_Ele", - Jacobian = "Vol", - ) - - q = add!(pp, "U") - add!(q, "Term", "{U}"; In = "Domain_Ele") - - q = add!(pp, "Q") - add!(q, "Term", "{Q}"; In = "Domain_Ele") - - q = add!(pp, "Y") - add!(q, "Term", "-{Q}"; In = "Domain_Ele") - - problem.postprocessing = postprocessing - - # PostOperation section - postoperation = PostOperation() - - # Field_Maps - po1 = add!(postoperation, "Field_Maps", "EleDyn_v") - op1 = add_operation!(po1) - add_operation!( - op1, - "Print[ v, OnElementsOf Domain_Ele, File StrCat[ \"$(joinpath(output_dir,"v_"))\", Sprintf(\"%g\",active_con), \".pos\" ] ];", - ) - add_operation!( - op1, - "Print[ em, OnElementsOf Domain_Ele, Name \"|E| [V/m]\", File StrCat[ \"$(joinpath(output_dir,"em_"))\", Sprintf(\"%g\",active_con), \".pos\" ] ];", - ) - add_operation!( - op1, - "Print[ dm, OnElementsOf Domain_Ele, Name \"|D| [A/m²]\", File StrCat[ \"$(joinpath(output_dir,"dm_"))\", Sprintf(\"%g\",active_con), \".pos\" ] ];", - ) - add_operation!( - op1, - "Print[ e, OnElementsOf Domain_Ele, Name \"E [V/m]\", File StrCat[ \"$(joinpath(output_dir,"e_"))\", Sprintf(\"%g\",active_con), \".pos\" ] ];", - ) - - # LineParams - po2 = add!(postoperation, "LineParams", "EleDyn_v") - op2 = add_operation!(po2) - add_operation!( - op2, - "Print[ Y, OnRegion Conductors, Format Table, File \"$(joinpath(output_dir,"Y.dat"))\", AppendToExistingFile (active_con > 1 ? 1 : 0) ];", - ) - - problem.postoperation = postoperation - -end - -function define_resolution!( - problem::GetDP.Problem, - formulation::Darwin, - workspace::FEMWorkspace, -) - - resolution_name = formulation.resolution_name - - # Create a new Problem instance - functionspace = FunctionSpace() - - # FunctionSpace section - fs1 = add!(functionspace, "Hcurl_a_Mag_2D", nothing, nothing, Type = "Form1P") - add_basis_function!( - functionspace, - "se", - "ae", - "BF_PerpendicularEdge"; - Support = "Domain_Mag", - Entity = "NodesOf[ All ]", - ) - - add_constraint!(functionspace, "ae", "NodesOf", "MagneticVectorPotential_2D") - - fs3 = add!(functionspace, "Hregion_u_Mag_2D", nothing, nothing, Type = "Form1P") - add_basis_function!( - functionspace, - "sr", - "ur", - "BF_RegionZ"; - Support = "DomainC", - Entity = "DomainC", - ) - add_global_quantity!(functionspace, "U", "AliasOf"; NameOfCoef = "ur") - add_global_quantity!(functionspace, "I", "AssociatedWith"; NameOfCoef = "ur") - add_constraint!(functionspace, "U", "Region", "Voltage_2D") - add_constraint!(functionspace, "I", "Region", "Current_2D") - - problem.functionspace = functionspace - - # Define Formulation - formulation = GetDP.Formulation() - - form = add!(formulation, "Darwin_a_2D", "FemEquation") - add_quantity!(form, "a", Type = "Local", NameOfSpace = "Hcurl_a_Mag_2D") - add_quantity!(form, "ur", Type = "Local", NameOfSpace = "Hregion_u_Mag_2D") - add_quantity!(form, "I", Type = "Global", NameOfSpace = "Hregion_u_Mag_2D [I]") - add_quantity!(form, "U", Type = "Global", NameOfSpace = "Hregion_u_Mag_2D [U]") - - eq = add_equation!(form) - - add!( - eq, - "Galerkin", - "[ nu[] * Dof{d a} , {d a} ]", - In = "Domain_Mag", - Jacobian = "Vol", - Integration = "I1", - ) - add!( - eq, - "Galerkin", - "DtDof [ sigma[] * Dof{a} , {a} ]", - In = "DomainC", - Jacobian = "Vol", - Integration = "I1", - ) - add!( - eq, - "Galerkin", - "[ sigma[] * Dof{ur}, {a} ]", - In = "DomainC", - Jacobian = "Vol", - Integration = "I1", - ) - add!( - eq, - "Galerkin", - "DtDof [ sigma[] * Dof{a} , {ur} ]", - In = "DomainC", - Jacobian = "Vol", - Integration = "I1", - ) - add!( - eq, - "Galerkin", - "[ sigma[] * Dof{ur}, {ur}]", - In = "DomainC", - Jacobian = "Vol", - Integration = "I1", - ) - add!( - eq, - "Galerkin", - "DtDtDof [ epsilon[] * Dof{a} , {a}]", - In = "DomainC", - Jacobian = "Vol", - Integration = "I1", - comment = " Darwin approximation term", - ) - add!( - eq, - "Galerkin", - "DtDof[ epsilon[] * Dof{ur}, {a} ]", - In = "DomainC", - Jacobian = "Vol", - Integration = "I1", - ) - add!( - eq, - "Galerkin", - "DtDtDof [ epsilon[] * Dof{a} , {ur}]", - In = "DomainC", - Jacobian = "Vol", - Integration = "I1", - ) - add!( - eq, - "Galerkin", - "DtDof[ epsilon[] * Dof{ur}, {ur} ]", - In = "DomainC", - Jacobian = "Vol", - Integration = "I1", - ) - add!(eq, "GlobalTerm", "[ Dof{I} , {U} ]", In = "Conductors") #DomainActive - - # Add the formulation to the problem - problem.formulation = formulation - - # Define Resolution - resolution = Resolution() - - # Add a resolution - output_dir = joinpath("results", lowercase(resolution_name)) - output_dir = replace(output_dir, "\\" => "/") # for compatibility with Windows paths - add!(resolution, resolution_name, "Sys_Mag", - NameOfFormulation = "Darwin_a_2D", - Type = "Complex", Frequency = "Freq", - Operation = [ - "CreateDir[\"$(output_dir)\"]", - "InitSolution[Sys_Mag]", - "Generate[Sys_Mag]", - "Solve[Sys_Mag]", - "SaveSolution[Sys_Mag]", - "PostOperation[LineParams]", - ]) - - # Add the resolution to the problem - problem.resolution = resolution - - # PostProcessing section - postprocessing = PostProcessing() - - pp = add!(postprocessing, "Darwin_a_2D", "Darwin_a_2D") - q = add!(pp, "a") - add!(q, "Term", "{a}"; In = "Domain_Mag", Jacobian = "Vol") - q = add!(pp, "az") - add!(q, "Term", "CompZ[{a}]"; In = "Domain_Mag", Jacobian = "Vol") - q = add!(pp, "b") - add!(q, "Term", "{d a}"; In = "Domain_Mag", Jacobian = "Vol") - q = add!(pp, "bm") - add!(q, "Term", "Norm[{d a}]"; In = "Domain_Mag", Jacobian = "Vol") - q = add!(pp, "j") - add!(q, "Term", "-sigma[]*(Dt[{a}]+{ur})"; In = "DomainC", Jacobian = "Vol") - q = add!(pp, "jz") - add!(q, "Term", "CompZ[-sigma[]*(Dt[{a}]+{ur})]"; In = "DomainC", Jacobian = "Vol") - q = add!(pp, "jm") - add!(q, "Term", "Norm[-sigma[]*(Dt[{a}]+{ur})]"; In = "DomainC", Jacobian = "Vol") - q = add!(pp, "d") - add!(q, "Term", "epsilon[] * Dt[Dt[{a}]+{ur}]"; In = "DomainC", Jacobian = "Vol") - q = add!(pp, "dz") - add!(q, "Term", "CompZ[epsilon[] * Dt[Dt[{a}]+{ur}]]"; In = "DomainC", Jacobian = "Vol") - q = add!(pp, "dm") - add!(q, "Term", "Norm[epsilon[] * Dt[Dt[{a}]+{ur}]]"; In = "DomainC", Jacobian = "Vol") - q = add!(pp, "rhoj2") - add!(q, "Term", "0.5*sigma[]*SquNorm[Dt[{a}]+{ur}]"; In = "DomainC", Jacobian = "Vol") - - q = add!(pp, "U") - add!(q, "Term", "{U}"; In = "DomainC") - q = add!(pp, "I") - add!(q, "Term", "{I}"; In = "DomainC") - q = add!(pp, "Z") - add!(q, "Term", "-{U}"; In = "DomainC") - - problem.postprocessing = postprocessing - - # PostOperation section - postoperation = PostOperation() - - # Add post-operation items - po1 = add!(postoperation, "Field_Maps", "Darwin_a_2D") - op1 = add_operation!(po1) - - add_operation!( - op1, - "Print[ az, OnElementsOf Domain_Mag, Smoothing 1, Name \"flux lines: Az [T m]\", File StrCat[ \"$(joinpath(output_dir,"az_"))\", Sprintf(\"%g\",active_con), \".pos\" ] ];", - ) - add_operation!( - op1, - "Print[ b, OnElementsOf Domain_Mag, Smoothing 1, Name \"B [T]\", File StrCat[ \"$(joinpath(output_dir,"b_"))\", Sprintf(\"%g\",active_con), \".pos\" ] ];", - ) - add_operation!( - op1, - "Print[ bm, OnElementsOf Domain_Mag, Smoothing 1, Name \"|B| [T]\", File StrCat[ \"$(joinpath(output_dir,"bm_"))\", Sprintf(\"%g\",active_con), \".pos\" ] ];", - ) - add_operation!( - op1, - "Print[ jz, OnElementsOf Region[{DomainC}], Smoothing 1, Name \"jz [A/m²] Conducting domain\", File StrCat[ \"$(joinpath(output_dir,"jz_"))\", Sprintf(\"%g\",active_con), \".pos\" ] ];", - ) - add_operation!( - op1, - "Print[ rhoj2, OnElementsOf Region[{DomainC}], Smoothing 1, Name \"Power density\", File StrCat[ \"$(joinpath(output_dir,"rhoj2_"))\", Sprintf(\"%g\",active_con), \".pos\" ] ];", - ) - add_operation!( - op1, - "Print[ jm, OnElementsOf DomainC, Smoothing 1, Name \"|j| [A/m²] Conducting domain\", File StrCat[ \"$(joinpath(output_dir,"jm_"))\", Sprintf(\"%g\",active_con), \".pos\" ] ];", - ) - add_operation!( - op1, - "Print[ dm, OnElementsOf DomainC, Smoothing 1, Name \"|D| [A/m²]\", File StrCat[ \"$(joinpath(output_dir,"dm_"))\", Sprintf(\"%g\",active_con), \".pos\" ] ];", - ) - - po2 = add!(postoperation, "LineParams", "Darwin_a_2D") - op2 = add_operation!(po2) - add_operation!( - op2, - "Print[ Z, OnRegion Conductors, Format Table, File \"$(joinpath(output_dir,"Z.dat"))\", AppendToExistingFile (active_con > 1 ? 1 : 0) ];", - ) - - # Add the post-operation to the problem - problem.postoperation = postoperation - -end - - -function run_getdp(workspace::FEMWorkspace, fem_formulation::AbstractFormulationSet) - # Initialize Gmsh if not already initialized - if gmsh.is_initialized() == 0 - gmsh.initialize() - end - - # Number of iterations (from the original function) - n_phases = - sum([length(c.design_data.components) for c in workspace.problem_def.system.cables]) - - # Flag to track if all solves are successful - all_success = true - - # Map verbosity to Gmsh/GetDP level - gmsh_verbosity = map_verbosity_to_gmsh(workspace.opts.verbosity) - gmsh.option.set_number("General.Verbosity", gmsh_verbosity) - - - getdp_verbosity = map_verbosity_to_getdp(workspace.opts.verbosity) - - # Loop over each active_ind from 1 to n_phases - for i in 1:n_phases - # Construct solver command with -setnumber active_ind i - solve_cmd = "$(workspace.opts.getdp_executable) $(fem_formulation.problem.filename) -msh $(workspace.paths[:mesh_file]) -solve $(fem_formulation.resolution_name) -setnumber active_con $i -v2 -verbose $(getdp_verbosity)" - - # Log the current solve attempt - @info "Solving for source conductor $i... (Resolution = $(fem_formulation.resolution_name))" - - # Attempt to run the solver - try - gmsh.onelab.run("GetDP", solve_cmd) - - if workspace.opts.plot_field_maps - @info "Building field maps for source conductor $i... (Resolution = $(fem_formulation.resolution_name))" - - post_cmd = "$(workspace.opts.getdp_executable) $(fem_formulation.problem.filename) -msh $(workspace.paths[:mesh_file]) -pos Field_Maps -setnumber active_con $i -v2 -verbose $(getdp_verbosity)" - - gmsh.onelab.run("GetDP", post_cmd) - end - - @info "Solve successful for source conductor $(i)!" - catch e - # Log the error and update the success flag - @error "Solver failed for source conductor $i: $e" - all_success = false - # Continue to the next iteration even if this one fails - end - end - - # Return true only if all solves were successful - return all_success -end - -using LinearAlgebra: BLAS, BlasFloat - -function run_solver!(workspace::FEMWorkspace) - problem = workspace.problem_def - formulation = workspace.formulation - - n_phases = workspace.n_phases - n_frequencies = workspace.n_frequencies - phase_map = workspace.phase_map - - # --- index plan (once) --- - perm = reorder_indices(phase_map) # encounter-ordered: first of each phase, then tails, then zeros - map_r = phase_map[perm] # reordered map (constant across k) - - - # --- outputs: size decided by kron_map (here: map_r after merge_bundles! zeros tails) - - # Probe the keep-size once using a scratch (no heavy cost). - _probe = Matrix{ComplexF64}(I, n_phases, n_phases) - _, reduced_map = merge_bundles!(copy(_probe), map_r) - n_keep = count(!=(0), reduced_map) - - Zr = zeros(ComplexF64, n_keep, n_keep, n_frequencies) - Yr = zeros(ComplexF64, n_keep, n_keep, n_frequencies) - - # --- scratch buffers (reused every k) --- - Zbuf = Matrix{ComplexF64}(undef, n_phases, n_phases) # reordered + merged target - Ybuf = Matrix{ComplexF64}(undef, n_phases, n_phases) - Pf = Matrix{ComplexF64}(undef, n_phases, n_phases) # potentials (for Y path) - - # tiny gather helper: reorder src[:,:,k] into dest without temp allocs - @inline function _reorder_into!(dest::StridedMatrix{ComplexF64}, - src::Array{ComplexF64, 3}, - perm::Vector{Int}, k::Int) - n = length(perm) - @inbounds for j in 1:n, i in 1:n - dest[i, j] = src[perm[i], perm[j], k] - end - return dest - end - - # --- big loop --- - for (k, frequency) in enumerate(workspace.freq) - @info "Solving frequency $k/$n_frequencies: $frequency Hz" - - # Fill Z,Y (original ordering) for this slice - _do_run_solver!(k, workspace) - - # REORDER → Z - _reorder_into!(Zbuf, workspace.Z, perm, k) - # symtrans!(Zbuf) - - # MERGE bundles (in-place on Zbuf) and get reduced map (tails → 0) - Zm, reduced_map = merge_bundles!(Zbuf, map_r) - - # KRON on Z - Zred = kronify(Zm, reduced_map) - symtrans!(Zred) - formulation.options.ideal_transposition || line_transpose!(Zred) - @inbounds Zr[:, :, k] .= Zred - - # Y path goes via potentials: Pf = inv(Y/(jω)) - w = 2π * frequency - # REORDER → Y - _reorder_into!(Ybuf, workspace.Y, perm, k) - # symtrans!(Ybuf) - - # Pf = inv(Ybuf / (jω)) without extra temps - @inbounds @views begin - Pf .= Ybuf - Pf ./= (1im*w) - end - Pf .= inv(Pf) - - # MERGE bundles for Pf (same reduced_map semantics) - Pfm, reduced_map = merge_bundles!(Pf, map_r) - - # KRON on Pf, then invert back to Y - Pr = kronify(Pfm, reduced_map) - Yrk = (1im*w) * inv(Pr) - symtrans!(Yrk) - formulation.options.ideal_transposition || line_transpose!(Yrk) - @inbounds Yr[:, :, k] .= Yrk - - # Archive if requested - if workspace.opts.keep_run_files - archive_frequency_results(workspace, frequency) - end - end - - lp = LineParameters(PhaseDomain, Zr, Yr, workspace.freq) - - return lp -end - - -function _do_run_solver!(freq_idx::Int, - workspace::FEMWorkspace) # Z::Array{ComplexF64, 3}, Y::Array{ComplexF64, 3}) - - # Get formulation from workspace - formulation = workspace.formulation - # Z, Y = workspace.Z, workspace.Y - frequency = workspace.freq[freq_idx] - - # Build and solve both formulations - for fem_formulation in formulation.analysis_type - @debug "Processing $(fem_formulation.resolution_name) formulation" - - make_fem_problem!(fem_formulation, frequency, workspace) - - if !run_getdp(workspace, fem_formulation) - Base.error("$(fem_formulation.resolution_name) solver failed") - end - end - - # Extract results into preallocated arrays - workspace.Z[:, :, freq_idx] = - read_results_file(formulation.analysis_type[1], workspace) - workspace.Y[:, :, freq_idx] = - read_results_file(formulation.analysis_type[2], workspace) -end - -""" -$(TYPEDSIGNATURES) - -Main function to run the FEM simulation workflow for a cable system. - -# Arguments - -- `cable_system`: Cable system to simulate. -- `formulation`: Problem definition parameters. -- `solver`: Solver parameters. -- `frequency`: Simulation frequency \\[Hz\\]. Default: 50.0. - -# Returns - -- A [`FEMWorkspace`](@ref) instance with the simulation results. - -# Examples - -```julia -# Run a FEM simulation -workspace = $(FUNCTIONNAME)(cable_system, formulation, solver) -``` -""" -function compute!(problem::LineParametersProblem, - formulation::FEMFormulation, - workspace::Union{FEMWorkspace, Nothing} = nothing) - - opts = formulation.options - - # Initialize workspace - workspace = init_workspace(problem, formulation, workspace) - - # Meshing phase: make_mesh! decides if it needs to run. - # It returns true if the process should stop (e.g., mesh_only=true). - if make_mesh!(workspace) - return workspace, nothing - end - - # Solving phase - always runs unless mesh_only - @info "Starting FEM solver" - ZY = run_solver!(workspace) - - @info "FEM computation completed successfully" - return workspace, ZY -end - diff --git a/src/engine/fem/space.jl b/src/engine/fem/space.jl deleted file mode 100644 index 51b771ea9..000000000 --- a/src/engine/fem/space.jl +++ /dev/null @@ -1,367 +0,0 @@ -""" -Domain creation functions for the FEMTools.jl module. -These functions handle the creation of domain boundaries and earth interfaces. -""" - -""" -$(TYPEDSIGNATURES) - -Create the domain boundaries (inner solid disk and outer annular region) for the simulation. - -# Arguments - -- `workspace`: The [`FEMWorkspace`](@ref) containing the model parameters. - -# Returns - -- Nothing. Updates the boundaries vector in the workspace. - -# Examples - -```julia -$(FUNCTIONNAME)(workspace) -``` -""" -function make_space_geometry(workspace::FEMWorkspace) - @info "Creating domain boundaries..." - - # Extract parameters - formulation = workspace.formulation - domain_radius = formulation.domain_radius - domain_radius_inf = formulation.domain_radius_inf # External radius for boundary transform - mesh_size_default = formulation.mesh_size_default - mesh_size_domain = formulation.mesh_size_max - mesh_size_inf = 1.25 * formulation.mesh_size_max - - # Center coordinates - x_center = 0.0 - y_center = 0.0 - - # Create inner domain disk - num_points_circumference = formulation.points_per_circumference - @debug "Creating inner domain disk with radius $(domain_radius) m" - _, _, air_region_marker, domain_boundary_markers = draw_disk( - x_center, - y_center, - domain_radius, - mesh_size_domain, - num_points_circumference, - ) - - # Create outer domain annular region - @debug "Creating outer domain annular region with radius $(domain_radius_inf) m" - _, _, air_infshell_marker, domain_infty_markers = draw_annular( - x_center, - y_center, - domain_radius, - domain_radius_inf, - mesh_size_inf, - num_points_circumference, - ) - - # Get earth model from workspace - earth_props = workspace.problem_def.earth_props - air_layer_idx = 1 # air layer is 1 by default - num_earth_layers = length(earth_props.layers) # Number of earth layers - earth_layer_idx = num_earth_layers - - # Air layer (Layer 1) - air_material = get_earth_model_material(workspace, air_layer_idx) - air_material_id = get_or_register_material_id(workspace, air_material) - air_material_group = get_material_group(earth_props, air_layer_idx) # Will return 2 (insulator) - - # Physical domain air tag - air_region_tag = encode_physical_group_tag( - 2, # Surface type 2 = physical domain - air_layer_idx, # Layer 1 = air - 0, # Component 0 (not a cable component) - air_material_group, # Material group 2 (insulator) - air_material_id, # Material ID - ) - air_region_name = create_physical_group_name(workspace, air_region_tag) - - # Infinite shell air tag - air_infshell_tag = encode_physical_group_tag( - 3, # Surface type 3 = infinite shell - air_layer_idx, # Layer 1 = air - 0, # Component 0 (not a cable component) - air_material_group, # Material group 2 (insulator) - air_material_id, # Material ID - ) - air_infshell_name = create_physical_group_name(workspace, air_infshell_tag) - - - # Earth layer (Layer 2+) - earth_material = get_earth_model_material(workspace, earth_layer_idx) - earth_material_id = get_or_register_material_id(workspace, earth_material) - earth_material_group = get_material_group(earth_props, earth_layer_idx) # Will return 1 (conductor) - - # Physical domain earth tag - earth_region_tag = encode_physical_group_tag( - 2, # Surface type 2 = physical domain - earth_layer_idx, # Layer 2 = first earth layer - 0, # Component 0 (not a cable component) - earth_material_group, # Material group 1 (conductor) - earth_material_id, # Material ID - ) - earth_region_name = create_physical_group_name(workspace, earth_region_tag) - - # Infinite shell earth tag - earth_infshell_tag = encode_physical_group_tag( - 3, # Surface type 3 = infinite shell - earth_layer_idx, # Layer 2 = first earth layer - 0, # Component 0 (not a cable component) - earth_material_group, # Material group 1 (conductor) - earth_material_id, # Material ID - ) - earth_infshell_name = create_physical_group_name(workspace, earth_infshell_tag) - - - # Create group tags for boundary curves - above ground (air) - inner domain - air_boundary_tag = encode_boundary_tag(1, air_layer_idx, 1) - air_boundary_name = create_physical_group_name(workspace, air_boundary_tag) - air_boundary_marker = [0.0, domain_radius, 0.0] - - # Below ground (earth) - inner domain - earth_boundary_tag = encode_boundary_tag(1, earth_layer_idx, 1) - earth_boundary_name = create_physical_group_name(workspace, earth_boundary_tag) - earth_boundary_marker = [0.0, -domain_radius, 0.0] - - # Above ground (air) - domain -> infinity - air_infty_tag = encode_boundary_tag(2, air_layer_idx, 1) - air_infty_name = create_physical_group_name(workspace, air_infty_tag) - air_infty_marker = [0.0, domain_radius_inf, 0.0] - - # Below ground (earth) - domain -> infinity - earth_infty_tag = encode_boundary_tag(2, earth_layer_idx, 1) - earth_infty_name = create_physical_group_name(workspace, earth_infty_tag) - earth_infty_marker = [0.0, -domain_radius_inf, 0.0] - - # Create markers for the domain surfaces - earth_region_marker = [0.0, -domain_radius * 0.99, 0.0] - marker_tag = gmsh.model.occ.add_point( - earth_region_marker[1], - earth_region_marker[2], - earth_region_marker[3], - mesh_size_domain, - ) - gmsh.model.set_entity_name( - 0, - marker_tag, - "marker_$(round(mesh_size_domain, sigdigits=6))", - ) - - earth_infshell_marker = - [0.0, -(domain_radius + 0.99 * (domain_radius_inf - domain_radius)), 0.0] - marker_tag = gmsh.model.occ.add_point( - earth_infshell_marker[1], - earth_infshell_marker[2], - earth_infshell_marker[3], - mesh_size_inf, - ) - gmsh.model.set_entity_name(0, marker_tag, "marker_$(round(mesh_size_inf, sigdigits=6))") - - # Create boundary curves - air_boundary_entity = CurveEntity( - CoreEntityData(air_boundary_tag, air_boundary_name, mesh_size_domain), - air_material, - ) - - earth_boundary_entity = CurveEntity( - CoreEntityData(earth_boundary_tag, earth_boundary_name, mesh_size_domain), - earth_material, - ) - - air_infty_entity = CurveEntity( - CoreEntityData(air_infty_tag, air_infty_name, mesh_size_inf), - air_material, - ) - - earth_infty_entity = CurveEntity( - CoreEntityData(earth_infty_tag, earth_infty_name, mesh_size_inf), - earth_material, - ) - - # Add curves to the workspace - workspace.unassigned_entities[air_boundary_marker] = air_boundary_entity - workspace.unassigned_entities[air_infty_marker] = air_infty_entity - workspace.unassigned_entities[earth_boundary_marker] = earth_boundary_entity - workspace.unassigned_entities[earth_infty_marker] = earth_infty_entity - - @debug "Domain boundary markers:" - for point_marker in domain_boundary_markers - target_entity = point_marker[2] > 0 ? air_boundary_entity : earth_boundary_entity - workspace.unassigned_entities[point_marker] = target_entity - @debug " Point $point_marker: ($(point_marker[1]), $(point_marker[2]), $(point_marker[3]))" - end - - @debug "Domain -> infinity markers:" - for point_marker in domain_infty_markers - target_entity = point_marker[2] > 0 ? air_infty_entity : earth_infty_entity - workspace.unassigned_entities[point_marker] = target_entity - @debug " Point $point_marker: ($(point_marker[1]), $(point_marker[2]), $(point_marker[3]))" - end - - # Add physical groups to the workspace - register_physical_group!(workspace, air_region_tag, air_material) - register_physical_group!(workspace, earth_region_tag, earth_material) - register_physical_group!(workspace, air_infshell_tag, air_material) - register_physical_group!(workspace, earth_infshell_tag, earth_material) - - # Physical groups for Dirichlet boundary - register_physical_group!(workspace, air_infty_tag, air_material) - register_physical_group!(workspace, earth_infty_tag, earth_material) - - # Create domain surfaces - air_region_entity = SurfaceEntity( - CoreEntityData(air_region_tag, air_region_name, mesh_size_default), - air_material, - ) - - air_infshell_entity = SurfaceEntity( - CoreEntityData(air_infshell_tag, air_infshell_name, mesh_size_default), - air_material, - ) - - # Earth regions will be created after boolean fragmentation - earth_region_entity = SurfaceEntity( - CoreEntityData(earth_region_tag, earth_region_name, mesh_size_default), - earth_material, - ) - - earth_infshell_entity = SurfaceEntity( - CoreEntityData(earth_infshell_tag, earth_infshell_name, mesh_size_default), - earth_material, - ) - - # Add surfaces to the workspace - workspace.unassigned_entities[air_region_marker] = air_region_entity - workspace.unassigned_entities[air_infshell_marker] = air_infshell_entity - workspace.unassigned_entities[earth_region_marker] = earth_region_entity - workspace.unassigned_entities[earth_infshell_marker] = earth_infshell_entity - - @info "Domain boundaries created" - - # Create earth interface line (y=0) - @debug "Creating earth interface line at y=0" - - # Create line from -domain_radius to +domain_radius at y=0 - num_elements = formulation.elements_per_length_interfaces - earth_interface_mesh_size = - _calc_mesh_size(0, domain_radius, earth_material, num_elements, workspace) - - _, _, earth_interface_markers = draw_line( - -domain_radius_inf, - 0.0, - domain_radius_inf, - 0.0, - earth_interface_mesh_size, - round(Int, domain_radius), - ) - - # Create physical tag for the earth interface - interface_idx = 1 # Earth interface index - earth_interface_tag = encode_boundary_tag(3, interface_idx, 1) - earth_interface_name = create_physical_group_name(workspace, earth_interface_tag) - - # Create domain entity - earth_interface_entity = CurveEntity( - CoreEntityData( - earth_interface_tag, - earth_interface_name, - earth_interface_mesh_size, - ), - get_earth_model_material(workspace, earth_layer_idx), # Earth material - ) - - # Create mesh transitions if specified - if !isempty(workspace.formulation.mesh_transitions) - @info "Creating $(length(workspace.formulation.mesh_transitions)) mesh transition regions" - - for (idx, transition) in enumerate(workspace.formulation.mesh_transitions) - cx, cy = transition.center - - # Use provided layer or auto-detect - layer_idx = if !isnothing(transition.earth_layer) - transition.earth_layer - else - # Fallback auto-detection (should rarely happen due to constructor) - cy >= 0 ? 1 : 2 - end - - # Validate layer index exists in earth model - if layer_idx > num_earth_layers - Base.error( - "Earth layer $layer_idx does not exist in earth model (max: $(num_earth_layers))", - ) - end - - # Get material for this earth layer - transition_material = get_earth_model_material(workspace, layer_idx) - material_id = get_or_register_material_id(workspace, transition_material) - material_group = get_material_group(earth_props, layer_idx) - - # Create physical tag for this transition - transition_tag = encode_physical_group_tag( - 2, # Surface type 2 = physical domain - layer_idx, # Earth layer index - 0, # Component 0 (not a cable component) - material_group, # Material group (1=conductor for earth, 2=insulator for air) - material_id, # Material ID - ) - - layer_name = layer_idx == 1 ? "air" : "earth_$(layer_idx-1)" - transition_name = "mesh_transition_$(idx)_$(layer_name)" - - # Calculate radii and mesh sizes - mesh_size_min = transition.mesh_factor_min * earth_interface_mesh_size - mesh_size_max = transition.mesh_factor_max * earth_interface_mesh_size - - transition_radii = - collect(LinRange(transition.r_min, transition.r_max, transition.n_regions)) - transition_mesh = - collect(LinRange(mesh_size_min, mesh_size_max, transition.n_regions)) - @debug "Transition $(idx): radii=$(transition_radii), mesh sizes=$(transition_mesh)" - - # Draw the transition regions - _, _, transition_markers = draw_transition_region( - cx, cy, - transition_radii, - transition_mesh, - num_points_circumference, - ) - - # Register each transition region - for k in 1:transition.n_regions - transition_region = SurfaceEntity( - CoreEntityData( - transition_tag, - "$(transition_name)_region_$(k)", - transition_mesh[k], - ), - transition_material, - ) - workspace.unassigned_entities[transition_markers[k]] = transition_region - - @debug "Created transition region $k at ($(cx), $(cy)) with radius $(transition_radii[k]) m in layer $layer_idx" - end - - # Register physical group - register_physical_group!(workspace, transition_tag, transition_material) - end - - @info "Mesh transition regions created" - else - @debug "No mesh transitions specified" - end - - # Add interface to the workspace - @debug "Domain -> infinity markers:" - for point_marker in earth_interface_markers - workspace.unassigned_entities[point_marker] = earth_interface_entity - @debug " Point $point_marker: ($(point_marker[1]), $(point_marker[2]), $(point_marker[3]))" - end - - @info "Earth interfaces created" - -end diff --git a/src/engine/fem/types.jl b/src/engine/fem/types.jl deleted file mode 100644 index 1710f6e3c..000000000 --- a/src/engine/fem/types.jl +++ /dev/null @@ -1,141 +0,0 @@ - -""" -$(TYPEDEF) - -Abstract base type for workspace containers in the FEM simulation framework. -Workspace containers maintain the complete state of a simulation, including -intermediate data structures, identification mappings, and results. - -Concrete implementations should provide state tracking for all phases of the -simulation process from geometry creation through results analysis. -""" -abstract type AbstractWorkspace end - -""" -$(TYPEDEF) - -Abstract type for entity data to be stored within the FEMWorkspace. -""" -abstract type AbstractEntityData end - -""" -$(TYPEDEF) - -Core entity data structure containing common properties for all entity types. - -$(TYPEDFIELDS) -""" -struct CoreEntityData - "Encoded physical tag \\[dimensionless\\]." - physical_group_tag::Int - "Name of the elementary surface." - elementary_name::String - "Target mesh size \\[m\\]." - mesh_size::Float64 -end - -""" -$(TYPEDEF) - -Entity data structure for cable parts. - -$(TYPEDFIELDS) -""" -struct CablePartEntity{T <: AbstractCablePart} <: AbstractEntityData - "Core entity data." - core::CoreEntityData - "Reference to original cable part." - cable_part::T -end - -""" -$(TYPEDEF) - -Entity data structure for domain surfaces external to cable parts. - -$(TYPEDFIELDS) -""" -struct SurfaceEntity <: AbstractEntityData - "Core entity data." - core::CoreEntityData - "Material properties of the domain." - material::Material -end - -""" -$(TYPEDEF) - -Entity data structure for domain curves (boundaries and layer interfaces). - -$(TYPEDFIELDS) -""" -struct CurveEntity <: AbstractEntityData - "Core entity data." - core::CoreEntityData - "Material properties of the domain." - material::Material -end - -""" -$(TYPEDEF) - -Entity container that associates Gmsh entity with metadata. - -$(TYPEDFIELDS) -""" -struct GmshObject{T <: AbstractEntityData} - "Gmsh entity tag (will be defined after boolean fragmentation)." - tag::Int32 - "Entity-specific data." - data::T -end - -""" -$(TYPEDSIGNATURES) - -Constructs a [`GmshObject`](@ref) instance with automatic type conversion. - -# Arguments - -- `tag`: Gmsh entity tag (will be converted to Int32) -- `data`: Entity-specific data conforming to [`AbstractEntityData`](@ref) - -# Returns - -- A [`GmshObject`](@ref) instance with the specified tag and data. - -# Notes - -This constructor automatically converts any integer tag to Int32 for compatibility with the Gmsh C API, which uses 32-bit integers for entity tags. - -# Examples - -```julia -# Create domain entity with tag and data -core_data = CoreEntityData([0.0, 0.0, 0.0]) -domain_data = SurfaceEntity(core_data, material) -entity = $(FUNCTIONNAME)(1, domain_data) -``` -""" -function GmshObject(tag::Integer, data::T) where {T <: AbstractEntityData} - return GmshObject{T}(Int32(tag), data) -end - -mutable struct Darwin <: AbstractImpedanceFormulation - problem::GetDP.Problem - resolution_name::String - - function Darwin() - return new(GetDP.Problem(), "Darwin") - end - -end - -mutable struct Electrodynamics <: AbstractAdmittanceFormulation - problem::GetDP.Problem - resolution_name::String - - function Electrodynamics() - return new(GetDP.Problem(), "Electrodynamics") - end -end \ No newline at end of file diff --git a/src/engine/fem/visualization.jl b/src/engine/fem/visualization.jl deleted file mode 100644 index b884e4030..000000000 --- a/src/engine/fem/visualization.jl +++ /dev/null @@ -1,160 +0,0 @@ -""" -Visualization functions for the FEMTools.jl module. -These functions handle the visualization of the mesh and results. -""" - -""" -$(TYPEDSIGNATURES) - -Preview the mesh in the Gmsh GUI. - -# Arguments - -- `workspace`: The [`FEMWorkspace`](@ref) containing the model. - -# Returns - -- Nothing. Launches the Gmsh GUI. - -# Examples - -```julia -$(FUNCTIONNAME)(workspace) -``` -""" -function preview_mesh(workspace::FEMWorkspace) - - if gmsh.is_initialized() == 0 - gmsh.initialize() - @debug "Initialized Gmsh for mesh preview" - else - @debug "Gmsh already initialized" - end - - try - # Set visualization options - gmsh.option.set_number("Geometry.SurfaceLabels", 0) # Show surface labels - gmsh.option.set_number("Geometry.PointNumbers", 0) - gmsh.option.set_number("Geometry.CurveNumbers", 0) - gmsh.option.set_number("Geometry.SurfaceNumbers", 0) - gmsh.option.set_number("Geometry.NumSubEdges", 160) - gmsh.option.set_number("Geometry.Points", 1) - gmsh.option.set_number("Geometry.Curves", 1) - gmsh.option.set_number("Geometry.Surfaces", 0) - gmsh.option.set_number("Mesh.ColorCarousel", 2) # Colors by physical group - gmsh.option.set_number("Mesh.LineWidth", 1) - gmsh.option.set_number("Mesh.SurfaceFaces", 1) - - # Initialize FLTK GUI - gmsh.fltk.initialize() - - @info "Launching Gmsh GUI for mesh preview" - @info "Close the Gmsh window to continue..." - - # Define event check function - function check_for_event() - action = gmsh.onelab.get_string("ONELAB/Action") - if length(action) > 0 && action[1] == "check" - gmsh.onelab.set_string("ONELAB/Action", [""]) - @debug "UI interaction detected" - gmsh.graphics.draw() - end - return true - end - - # Wait for user to close the window - while gmsh.fltk.is_available() == 1 && check_for_event() - gmsh.fltk.wait() - end - - @info "Mesh preview closed" - - catch e - @warn "Error during mesh preview: $e" - end -end - -""" -$(TYPEDSIGNATURES) - -Preview a single electromagnetic field result file in Gmsh GUI. - -# Arguments -- `workspace`: The [`FEMWorkspace`](@ref) containing the model. -- `pos_file`: Path to the .pos file to visualize. - -# Examples -```julia -$(FUNCTIONNAME)(workspace, "/path/to/result.pos") -``` -""" -function preview_results(workspace::FEMWorkspace, pos_file::String) - # Validate inputs - if !isfile(pos_file) - @error "Result file not found: $pos_file" - return - end - - if !endswith(pos_file, ".pos") - @error "File must be a .pos file: $pos_file" - return - end - - # Initialize Gmsh - gmsh.initialize() - - try - # Add single model - gmsh.model.add("field_view") - - # Merge mesh file - mesh_file = workspace.paths[:mesh_file] - if isfile(mesh_file) - gmsh.merge(abspath(mesh_file)) - else - @error "Mesh file not found: $mesh_file" - return - end - - # Merge the single result file - @info "Loading field data: $(display_path(pos_file))" - gmsh.merge(abspath(pos_file)) - - # Set mesh color to light gray - gmsh.option.set_color("Mesh.Color.Lines", 240, 240, 240) - gmsh.option.set_number("Mesh.ColorCarousel", 0) - gmsh.option.set_number("Mesh.LineWidth", 1) - gmsh.option.set_number("Mesh.SurfaceFaces", 0) - gmsh.option.set_number("Mesh.Lines", 1) - gmsh.option.set_number("Geometry.Points", 0) - gmsh.option.set_number("General.InitialModule", 4) - - # Get view tags and configure - view_tags = gmsh.view.getTags() - - if isempty(view_tags) - @warn "No field views found in file" - return - end - - # Configure field visualization - for view_tag in view_tags - gmsh.view.option.set_number(view_tag, "IntervalsType", 2) - gmsh.view.option.set_number(view_tag, "RangeType", 3) - gmsh.view.option.set_number(view_tag, "ShowTime", 0) - end - - @info "Launching Gmsh GUI with $(length(view_tags)) field view(s)" - @info "Close the Gmsh window to continue..." - - # Launch GUI - gmsh.fltk.run() - - @info "Field visualization closed" - - catch e - @error "Error during field visualization" exception = e - finally - gmsh.finalize() - end -end \ No newline at end of file diff --git a/src/engine/fem/workspace.jl b/src/engine/fem/workspace.jl deleted file mode 100644 index eaf54bd4e..000000000 --- a/src/engine/fem/workspace.jl +++ /dev/null @@ -1,282 +0,0 @@ -import ..Engine: _get_earth_data - -""" -$(TYPEDEF) - -FEMWorkspace - The central workspace for FEM simulations. -This is the main container that maintains all state during the simulation process. - -$(TYPEDFIELDS) -""" -struct FEMWorkspace{T <: AbstractFloat} - "Line parameters problem definition." - problem_def::LineParametersProblem - "Formulation parameters." - formulation::FEMFormulation - "Computation options." - opts::FEMOptions - - "Path information." - paths::Dict{Symbol, String} - - "Conductor surfaces within cables." - conductors::Vector{GmshObject{<:AbstractEntityData}} - "Insulator surfaces within cables." - insulators::Vector{GmshObject{<:AbstractEntityData}} - "Domain-space physical surfaces (air and earth layers)." - space_regions::Vector{GmshObject{<:AbstractEntityData}} - "Domain boundary curves." - boundaries::Vector{GmshObject{<:AbstractEntityData}} - "Container for all pre-fragmentation entities." - unassigned_entities::Dict{Vector{Float64}, AbstractEntityData} - "Container for all material names used in the model." - material_registry::Dict{String, Int} - "Container for unique physical groups." - physical_groups::Dict{Int, Material} - - "Vector of frequency values [Hz]." - freq::Vector{T} - "Vector of horizontal positions [m]." - horz::Vector{T} - "Vector of vertical positions [m]." - vert::Vector{T} - "Vector of internal conductor radii [m]." - r_in::Vector{T} - "Vector of external conductor radii [m]." - r_ext::Vector{T} - "Vector of internal insulator radii [m]." - r_ins_in::Vector{T} - "Vector of external insulator radii [m]." - r_ins_ext::Vector{T} - "Vector of conductor resistivities [Ω·m]." - rho_cond::Vector{T} - "Vector of conductor temperature coefficients [1/°C]." - alpha_cond::Vector{T} - "Vector of conductor relative permeabilities." - mu_cond::Vector{T} - "Vector of conductor relative permittivities." - eps_cond::Vector{T} - "Vector of insulator resistivities [Ω·m]." - rho_ins::Vector{T} - "Vector of insulator relative permeabilities." - mu_ins::Vector{T} - "Vector of insulator relative permittivities." - eps_ins::Vector{T} - "Vector of insulator loss tangents." - tan_ins::Vector{T} - "Vector of phase mapping indices." - phase_map::Vector{Int} - "Vector of cable mapping indices." - cable_map::Vector{Int} - "Effective earth resistivity (layers × freq)." - rho_g::Matrix{T} - "Effective earth permittivity (layers × freq)." - eps_g::Matrix{T} - "Effective earth permeability (layers × freq)." - mu_g::Matrix{T} - "Operating temperature [°C]." - temp::T - "Number of frequency samples." - n_frequencies::Int - "Number of phases in the system." - n_phases::Int - "Number of cables in the system." - n_cables::Int - "Full component-based Z matrix (before bundling/reduction)." - Z::Array{Complex{T}, 3} - "Full component-based Y matrix (before bundling/reduction)." - Y::Array{Complex{T}, 3} - - - """ - $(TYPEDSIGNATURES) - - Constructs a [`FEMWorkspace`](@ref) instance. - - # Arguments - - - `cable_system`: Cable system being simulated. - - `formulation`: Problem definition parameters. - - `solver`: Solver parameters. - - `frequency`: Simulation frequency \\[Hz\\]. Default: 50.0. - - # Returns - - - A [`FEMWorkspace`](@ref) instance with the specified parameters. - - # Examples - - ```julia - # Create a workspace - workspace = $(FUNCTIONNAME)(cable_system, formulation, solver) - ``` - """ - function FEMWorkspace( - problem::LineParametersProblem{U}, - formulation::FEMFormulation, - ) where {U <: REALSCALAR} - - # Initialize empty workspace - opts = formulation.options - - system = problem.system - n_frequencies = length(problem.frequencies) - n_phases = sum(length(cable.design_data.components) for cable in system.cables) - - # Pre-allocate 1D arrays - T = BASE_FLOAT - freq = Vector{T}(undef, n_frequencies) - horz = Vector{T}(undef, n_phases) - vert = Vector{T}(undef, n_phases) - r_in = Vector{T}(undef, n_phases) - r_ext = Vector{T}(undef, n_phases) - r_ins_in = Vector{T}(undef, n_phases) - r_ins_ext = Vector{T}(undef, n_phases) - rho_cond = Vector{T}(undef, n_phases) - alpha_cond = Vector{T}(undef, n_phases) - mu_cond = Vector{T}(undef, n_phases) - eps_cond = Vector{T}(undef, n_phases) - rho_ins = Vector{T}(undef, n_phases) - mu_ins = Vector{T}(undef, n_phases) - eps_ins = Vector{T}(undef, n_phases) - tan_ins = Vector{T}(undef, n_phases) # Loss tangent for insulator - phase_map = Vector{Int}(undef, n_phases) - cable_map = Vector{Int}(undef, n_phases) - Z = zeros(Complex{T}, n_phases, n_phases, n_frequencies) - Y = zeros(Complex{T}, n_phases, n_phases, n_frequencies) - - # Fill arrays, ensuring type promotion - freq .= to_nominal.(problem.frequencies) - - idx = 0 - for (cable_idx, cable) in enumerate(system.cables) - for (comp_idx, component) in enumerate(cable.design_data.components) - idx += 1 - # Geometric properties - horz[idx] = to_nominal(cable.horz) - vert[idx] = to_nominal(cable.vert) - r_in[idx] = to_nominal(component.conductor_group.r_in) - r_ext[idx] = to_nominal(component.conductor_group.r_ex) - r_ins_in[idx] = to_nominal(component.insulator_group.r_in) - r_ins_ext[idx] = to_nominal(component.insulator_group.r_ex) - - # Material properties - rho_cond[idx] = to_nominal(component.conductor_props.rho) - alpha_cond[idx] = to_nominal(component.conductor_props.alpha) - mu_cond[idx] = to_nominal(component.conductor_props.mu_r) - eps_cond[idx] = to_nominal(component.conductor_props.eps_r) - rho_ins[idx] = to_nominal(component.insulator_props.rho) - mu_ins[idx] = to_nominal(component.insulator_props.mu_r) - eps_ins[idx] = to_nominal(component.insulator_props.eps_r) - - # Calculate loss factor from resistivity - ω = 2 * π * f₀ # Using default frequency - C_eq = to_nominal(component.insulator_group.shunt_capacitance) - G_eq = to_nominal(component.insulator_group.shunt_conductance) - tan_ins[idx] = G_eq / (ω * C_eq) - - # Mapping - phase_map[idx] = cable.conn[comp_idx] - cable_map[idx] = cable_idx - end - end - - (rho_g, eps_g, mu_g) = _get_earth_data( - nothing, - problem.earth_props, - freq, - T, - ) - - - temp = to_nominal(problem.temperature) - - - workspace = new{T}( - problem, formulation, opts, - setup_paths(problem.system, formulation), - # Dict{Symbol,String}(), # Path information. - Vector{GmshObject{<:AbstractEntityData}}(), #conductors - Vector{GmshObject{<:AbstractEntityData}}(), #insulators - Vector{GmshObject{<:AbstractEntityData}}(), #space_regions - Vector{GmshObject{<:AbstractEntityData}}(), #boundaries - Dict{Vector{Float64}, AbstractEntityData}(), #unassigned_entities - Dict{String, Int}(), # Initialize empty material registry - Dict{Int, Material}(), # Maps physical group tags to materials, - freq, - horz, vert, - r_in, r_ext, - r_ins_in, r_ins_ext, - rho_cond, alpha_cond, mu_cond, eps_cond, - rho_ins, mu_ins, eps_ins, tan_ins, - phase_map, cable_map, rho_g, - eps_g, mu_g, - temp, n_frequencies, n_phases, - system.num_cables, Z, Y, - ) - - # Set up paths - # workspace.paths = setup_paths(problem.system, formulation) - - return workspace - end -end - -function init_workspace(problem, formulation, workspace) - if isnothing(workspace) - @debug "Creating new workspace" - workspace = FEMWorkspace(problem, formulation) - else - @debug "Reusing existing workspace" - end - - opts = formulation.options - - # set_verbosity!(opts.verbosity, opts.logfile) - - # Handle existing results - check both current and archived - results_dir = workspace.paths[:results_dir] - base_dir = dirname(results_dir) - - # Check current results directory - current_results_exist = isdir(results_dir) && !isempty(readdir(results_dir)) - - # Check for archived frequency results (results_f* pattern) - archived_results_exist = false - if isdir(base_dir) - archived_dirs = - filter(d -> startswith(d, "results_f") && isdir(joinpath(base_dir, d)), - readdir(base_dir)) - archived_results_exist = !isempty(archived_dirs) - end - - # Handle existing results if any are found - if current_results_exist || archived_results_exist - if opts.force_overwrite - # Remove both current and archived results - if current_results_exist - rm(results_dir, recursive = true, force = true) - end - if archived_results_exist - for archived_dir in archived_dirs - rm(joinpath(base_dir, archived_dir), recursive = true, force = true) - end - @debug "Removed $(length(archived_dirs)) archived result directories" - end - else - # Build informative error message - error_msg = "Existing results found:\n" - if current_results_exist - error_msg *= " - Current results: $results_dir\n" - end - if archived_results_exist - error_msg *= " - Archived results: $(length(archived_dirs)) frequency directories\n" - end - error_msg *= "Set force_overwrite=true to automatically delete existing results." - - Base.error(error_msg) - end - end - - return workspace -end diff --git a/src/engine/formulations.jl b/src/engine/formulations.jl new file mode 100644 index 000000000..526bce7ce --- /dev/null +++ b/src/engine/formulations.jl @@ -0,0 +1,630 @@ +# Engine-owned formulation hierarchy. +""" +$(TYPEDEF) + +Select the LineCableModels backend for concentric coaxial cable assemblies. + +Series impedance uses the equivalent concentric representation supplied by +DataModel. The default local shunt model uses the equivalent annular layer. Explicit +`shunt_model=:boundary` resolves eligible open wires and finite tapes in a +lossless, radially layered circular shielded domain during blueprint +construction. Other geometry and material selections retain their +equivalent-coaxial treatment. +""" +struct LineCableModelsCoaxial end + +"""Identify the coaxial backend without executing or configuring a computation.""" +description(::Type{LineCableModelsCoaxial}; compact::Bool = false) = "coaxial" +function description(::LineCableModelsCoaxial; compact::Bool = false) + description(LineCableModelsCoaxial; compact) +end +formula_id(::Type{LineCableModelsCoaxial}) = :coaxial +formula_id(::LineCableModelsCoaxial) = :coaxial + +""" +$(TYPEDEF) + +Select the Julia-native Gmsh/GetDP finite-element backend. + +`options` stores the field-model choice and matrix reductions. Set +`options=(physics=:quasi_tem,)` (default) or `(physics=:quasi_fw,)`. +Execution controls belong to `compute(...; options=(...))` and are validated +by [`computation_options`](@ref) for `LineCableModelsFEM`. + +The quasi-TEM model solves independent axial ``A_z/u`` and scalar electric +Helmholtz blocks in one factorization. The magnetic excitation is one ampere. +The electric excitation is one ampere per meter. Both models retain conduction +and displacement through ``κ=σ+jωε`` \\[S/m\\], where ``ω`` is angular +frequency \\[rad/s\\], ``σ`` conductivity \\[S/m\\], and ``ε`` permittivity \\[F/m\\]. + +The quasi-full-wave model uses phasors ``e^{jωt-Γz}`` and expands +``A_z=a``, ``A_t=Γb``, ``φ=Γv`` before taking ``Γ→0``. Here ``Γ`` is +the longitudinal propagation constant \\[1/m\\], ``a`` has units \\[T m\\], +``b`` \\[T m²\\], and ``v`` \\[V m\\]. In the media outside the equipotential +metal terminals, the retained equations are + +```math +-\\nabla_t\\!\\cdot(\\nu\\nabla_t a)+j\\omega\\kappa a=0, +\\qquad +C^*(\\nu Cb)+j\\omega\\kappa b+\\kappa\\nabla_t v-\\nu\\nabla_t a=0, +\\qquad +-\\nabla_t\\!\\cdot[\\kappa(j\\omega b+\\nabla_t v)]+j\\omega\\kappa a=0. +``` + +Here ``\\nu=1/\\mu`` \\[m/H\\], ``μ`` is permeability \\[H/m\\], +``Cb=∂_x b_y-∂_y b_x`` and ``C^*h=(∂_y h,-∂_x h)``. +The finite-conductivity axial ``a/u`` block supplies both the series voltage +drop and the normalized transverse-current source. There is no independent +electric excitation. A tree gauge removes the gradient freedom of ``b``. +Its tangential trace vanishes on every terminal contour and the entire outer +boundary. ``v=0`` on the earth-side outer reference, with natural electric +conditions on the air side. + +Terms of order ``Γ^2`` in the axial equation are omitted. The transverse +Ampère equation remains at the order used to extract shunt response: continuity +alone constrains a divergence and cannot supply that vector balance across a +material interface. Choosing a smaller numerical ``Γ`` cannot restore it. + +Terminal voltage includes the vector potential. For a path ``ℓ_i`` oriented +from the earth reference to terminal ``i``, the inverse-admittance matrix is + +```math +P_{ij}=\\frac{v_i-v_{\\rm ref}+j\\omega\\int_{\\ell_i}b\\cdot d\\ell}{I_j}, +\\qquad Y=P^{-1},\\qquad P_e=j\\omega P. +``` + +``I_j`` is the imposed axial current \\[A\\], ``P`` has units \\[Ω m\\], +``Y`` \\[S/m\\], and the analytical potential coefficient ``P_e`` \\[m/F\\]. +The backend uses physical vertical paths to the lowest mesh node of each +terminal contour, with zero transverse field inside equipotential metal. +It integrates the pulled-back edge field through the infinite shell. +The scalar trace ``v_i/I_j`` alone is gauge dependent and is saved separately +as `Pscalar.tsv` diagnostics. Series impedance is ``Z_{ij}=-U_i/I_j`` \\[Ω/m\\], +where ``U_i`` is the axial electric unknown \\[V/m\\]. + +This is a two-dimensional first-order reduction of Maxwell's potential +equations, retaining transverse induction and displacement. It does not solve +for a finite propagation constant or constitute the Darwin approximation. +The full-vector potential equations and the role of terminal conditions and +gauging are described by G. Ciuprina and R. V. Sabriego, *Electric circuit +element boundary conditions for electromagneto-quasistatic and full wave +models in A, φ potentials and their finite element implementation*, Journal +of Mathematics in Industry **14**, 27 (2024), +[doi:10.1186/s13362-024-00165-6](https://doi.org/10.1186/s13362-024-00165-6), +Sect. 4 and Appendix B. The longitudinal reduction and vertical voltage-path +convention above are specific to this backend. The paper validates 3D models. + +$(TYPEDFIELDS) +""" +struct LineCableModelsFEM{M <: NamedTuple, O <: FormulationOptions, D <: NamedTuple} <: + AbstractFormulation + "Shared scientific formula selections, independent of FEM execution controls." + methods::M + "Field model and line-parameter matrix reductions." + options::O + "Requested formula definitions." + definitions::D +end + +"""Identify the FEM backend without loading meshing or solver packages.""" +description(::Type{<:LineCableModelsFEM}; compact::Bool = false) = "FEM" +function description(::LineCableModelsFEM; compact::Bool = false) + description(LineCableModelsFEM; compact) +end +formula_id(::Type{<:LineCableModelsFEM}) = :fem +formula_id(::LineCableModelsFEM) = :fem +formulation_options(value::LineCableModelsFEM) = value.options +function Base.pairs(value::LineCableModelsFEM; quantity = nothing) + pairs(LineCableModelsFEM, + (methods = value.methods, + requested = value.definitions, + options = value.options.data); + quantity) +end + +"""FEM's coupled field equations retain all four constitutive selections.""" +function Base.pairs(::Type{LineCableModelsFEM}; quantity = nothing) + return pairs((insulation_admittance = InsulationAdmittance.Formula, + semicon_admittance = SemiconAdmittance.Formula, + earth_properties = Earth.FrequencyDependent.Formula, + temperature_dependence = TemperatureDependent.Formula)) +end +function description(::Type{LineCableModelsFEM}, slot::Val; compact::Bool=false) + description(LineParametersFormulation, slot; compact) +end +function Base.pairs(::Type{LineCableModelsFEM}, retained::NamedTuple; quantity = nothing) + pairs(LineParametersFormulation, retained; quantity, owner = LineCableModelsFEM) +end + +""" +$(TYPEDEF) + +Report a failure in finite-element model construction, meshing, solving, or result validation. + +$(TYPEDFIELDS) +""" +struct LineCableModelsFEMError <: Exception + "Failure category." + category::Symbol + "Stable identifier of the object associated with the failure." + object_id::String + "Field or derived datum that failed validation." + field::Symbol + "Human-readable failure description." + message::String + "Retained run directory, or `nothing` before run creation." + run_directory::Union{Nothing, String} +end + +function LineCableModelsFEMError( + category::Symbol, + object_id, + field::Symbol, + message::AbstractString; + run_directory::Union{Nothing, AbstractString} = nothing +) + path = run_directory === nothing ? nothing : String(run_directory) + return LineCableModelsFEMError( + category, String(object_id), field, String(message), path + ) +end + +function Base.showerror(io::IO, error::LineCableModelsFEMError) + print( + io, + "LineCableModelsFEMError(", + error.category, + ", object=", + repr(error.object_id), + ", field=:", + error.field, + "): ", + error.message + ) + error.run_directory === nothing || print( + io, "; retained run directory: ", error.run_directory + ) +end + +""" +$(TYPEDEF) + +Supertype for Engine impedance formulations. +""" +abstract type AbstractImpedanceFormulation <: AbstractFormulation end +""" +Select equations for the surface impedance of conductors [Ω/m]. Concrete subtypes build, in a +`Functor` method, the values that inner, outer and transfer surface impedances share, from +the conductor dimensions and material properties. They implement +`InternalImpedance.internal_impedance` for their supported `inner`, `outer`, and `transfer` +cases. +""" +abstract type InternalImpedanceFormulation <: AbstractImpedanceFormulation end +""" +Select the pipe contribution and its backend and topology applicability. Concrete +subtypes extend `validate(design, selected, backend)`, which admits the topology of +`design` or throws. An impedance equation must be supplied separately from admission. +""" +abstract type PipeImpedanceFormulation <: AbstractImpedanceFormulation end +""" +Select magnetic impedance across an insulation annulus [Ω/m]. Concrete subtypes +implement `InsulationImpedance.insulation_impedance` on their selected type. +""" +abstract type InsulationImpedanceFormulation <: AbstractImpedanceFormulation end +""" +Select earth-return impedance equations [Ω/m]. Concrete subtypes implement +indexed `EarthImpedance.earth_impedance` methods on evaluated material inputs. +Air, earth, and mixed selections refer to conductor locations. +""" +abstract type EarthImpedanceFormulation <: AbstractImpedanceFormulation end + +"""Supertype for local shunt geometry and dielectric and earth admittance selections.""" +abstract type AbstractAdmittanceFormulation <: AbstractFormulation end +""" +Select cable-local shunt geometry, independently of material admittivity. +Concrete subtypes implement `internal_shunt_response` during blueprint construction. +""" +abstract type ShuntModelFormulation <: AbstractAdmittanceFormulation end +""" +Select insulation admittivity [S/m]. Concrete subtypes implement +`InsulationAdmittance.insulation_material`. Radial geometry is applied separately. +""" +abstract type InsulationAdmittanceFormulation <: AbstractAdmittanceFormulation end +""" +Select semiconducting-layer admittivity [S/m]. Concrete subtypes implement +`SemiconAdmittance.semicon_material`. Radial geometry is applied separately. +""" +abstract type SemiconAdmittanceFormulation <: AbstractAdmittanceFormulation end +""" +Select earth potential-coefficient equations [m/F]. Concrete subtypes implement +indexed `EarthAdmittance.earth_potential_coefficient` methods on evaluated +material inputs. Matrix assembly converts +the potential coefficients to shunt admittance [S/m]. +""" +abstract type EarthAdmittanceFormulation <: AbstractAdmittanceFormulation end + +""" +Validate the admittivity [S/m] that a dielectric law returns, before radial aggregation +represents it as `Complex{T}`. A result requiring a wider scalar type than `T` is rejected +instead of silently discarding precision or uncertainty. Return `value`. +""" +function validate(value, ::Union{InsulationAdmittanceFormulation, SemiconAdmittanceFormulation}, + ::Type{T}) where {T <: Real} + value isa Number && !(value isa Bool) && isfinite(value) || throw(DomainError( + value, "a dielectric material law must return a finite scalar admittivity [S/m]")) + promote_type(T, typeof(real(value))) === T || throw(ArgumentError( + "dielectric admittivity requires scalar type $(typeof(real(value))); " * + "use a material and problem scalar type that preserves its precision and uncertainty")) + return value +end + +# The expression of an earth formula for one interaction: its kind and its source and target +# layers. +function Expression(formula::EarthImpedanceFormulation, pair::EarthPair) + return Expression(formula, EarthImpedance.earth_impedance, + Val(pair.row == pair.column ? :self : :mutual), Val.(layer_index(pair))...) +end +function Expression(formula::EarthAdmittanceFormulation, pair::EarthPair) + return Expression(formula, EarthAdmittance.earth_potential_coefficient, + Val(pair.row == pair.column ? :self : :mutual), Val.(layer_index(pair))...) +end + +""" +$(TYPEDSIGNATURES) + +Build the expression of an earth recipe for `pair`: the `air` formula for layers (1, 1), the +`earth` formula for (2, 2) and the `mixed` formula for (1, 2) and (2, 1). The pair's layers +are the decided ones: a recipe describes a homogeneous earth. + +# Errors + +- Throws `ArgumentError` when the recipe has no formula for the pair's route, or for another + layer pair. +""" +function Expression(recipe::NamedTuple, pair::EarthPair) + source, target = layer_index(pair) + route = (source, target) == (1, 1) ? :air : (source, target) == (2, 2) ? :earth : + (source, target) in ((1, 2), (2, 1)) ? :mixed : + throw(ArgumentError("homogeneous selection is not defined for source in layer " * + "$source and target in layer $target")) + selected = get(recipe, route, nothing) + selected === nothing && + throw(ArgumentError("explicit equation recipe has no requested :$route case")) + return Expression(selected, pair) +end + +# Equation-specific geometric restrictions extend the existing validation protocol. +validate(pair::EarthPair, ::Expression) = pair + +# Whether the formula of `expression` admits earth layer `k`. It does when a method of its +# operation accepts `Val{k}` in the source or the target position, with the `Functor` and the +# workspace that the evaluation passes, whatever the types of the other arguments. A layer +# left generic, such as `::Val{S}`, admits every layer. The earth decision, the error message +# of `validate(expression, layers)` and the standalone formula call read the layers here. +function _admits_layer(expression::Expression, k::Int) + F = typeof(expression.selection) + return !isempty(methods(expression.method, Tuple{F, Any, Val{k}, Any, Functor, Any})) || + !isempty(methods(expression.method, Tuple{F, Any, Any, Val{k}, Functor, Any})) +end + +""" +$(TYPEDSIGNATURES) + +Check that the formula of `expression` defines it for an earth with one entry of `layers` per +medium, before evaluating it: the layers of an earth model, or the resistivities of the +standalone formula call. The signatures of a formula's methods declare the earth layers it +handles, whatever the types of their runtime arguments. In layers that the formula admits, +`validate(expression)` checks the expression. Return `expression`. + +# Errors + +- Throws `ArgumentError` with the layer count N and the highest layer up to N that the + formula admits, when the formula does not admit the source or target layer. +- Throws the `ArgumentError` of `validate(expression)` for a missing expression in layers + that the formula admits. + +When the formula admits layer N + 1, the message reads "defined beyond layer N" instead. +""" +function validate(expression::Expression{<:Union{EarthImpedanceFormulation, EarthAdmittanceFormulation}}, + layers::Union{Tuple, AbstractVector}) + selected = expression.selection + signature = Tuple{typeof(selected), map(typeof, expression.arguments)..., Functor, Any} + hasmethod(expression.method, signature) && return expression + interaction(::Val{K}, ::Val{S}, ::Val{T}) where {K, S, T} = (K, S, T) + kind, source, target = interaction(expression.arguments...) + if _admits_layer(expression, source) && _admits_layer(expression, target) + validate(expression) + return expression + end + N = length(layers) + defined = _admits_layer(expression, N + 1) ? "defined beyond layer $N" : + "defined up to layer $(something(findlast(k -> _admits_layer(expression, k), 1:N), 0))" + throw(ArgumentError( + "the earth model has $N layers and formula " * + ":$(formula_id(selected)) is $defined; it has no expression " * + "for a $kind interaction from layer $source to layer $target")) +end + +function validate(earth::EarthModel, formula::Union{EarthImpedanceFormulation, EarthAdmittanceFormulation}) + validate(earth) + earth.vertical_layers && + throw(ArgumentError("earth-return equations require horizontal interfaces or an explicit EquivalentHomogeneous reduction")) + return earth +end + +function validate(rho::AbstractVector, + formula::Union{EarthImpedanceFormulation, EarthAdmittanceFormulation}, + epsilon::AbstractVector, mu::AbstractVector, thickness) + length(rho) == length(epsilon) == length(mu) || + throw(DimensionMismatch("material vectors must align")) + all(x -> x > 0 && !isnan(x), rho) || + throw(DomainError(rho, "resistivities must be positive, including infinite air resistivity")) + all(x -> isfinite(x) && !iszero(x), epsilon) && all(x -> isfinite(x) && x > 0, mu) || + throw(DomainError((epsilon, mu), + "permittivities must be nonzero and finite; permeabilities positive and finite")) + epsilon[1] > 0 || throw(DomainError(epsilon[1], "air permittivity must be positive")) + all(>(0), epsilon) || + throw(DomainError(epsilon, + "formula :$(formula_id(formula)) requires positive permittivity; the earth-material data type permits artificial negative values")) + if thickness === nothing + length(rho) == 2 || throw(DimensionMismatch( + "material vectors without layer thicknesses describe air and one earth medium")) + else + length(thickness) == length(rho) || + throw(DimensionMismatch("layer thicknesses must align with materials")) + isinf(first(thickness)) && isinf(last(thickness)) && + all(x -> isfinite(x) && x > 0, @view(thickness[2:(end - 1)])) || + throw(DomainError(thickness, + "air and bottom half-spaces must be infinite; internal layers positive and finite")) + end + return rho +end + +""" +$(TYPEDSIGNATURES) + +Return the normalized options of the surface impedance `kind` of the internal-impedance +formula `formula`: the section of its options for this kind, or empty options. +""" +function formulation_options(formula::InternalImpedanceFormulation, ::Val{Kind}) where {Kind} + return FormulationOptions(get(formula.options.data, Kind, (;))) +end + +""" +$(TYPEDSIGNATURES) + +Build the Functor of an insulation-impedance formula for one insulation annulus at one +frequency, after checking its radii, its relative permeability and `jω`. The state is empty. +""" +function Functor(formula::InsulationImpedanceFormulation, input::NamedTuple; + workspace = nothing) + (; r_in, r_ex, mu_r, jω) = input + T = typeof(r_in) + isfinite(r_in) && isfinite(r_ex) && zero(T) <= r_in <= r_ex || + throw(DomainError((r_in, r_ex), "insulation radii must satisfy 0 ≤ r_in ≤ r_ex [m]")) + isfinite(mu_r) && mu_r > zero(T) || throw(DomainError(mu_r, + "relative insulation permeability must be positive and finite")) + isfinite(jω) || throw(DomainError(jω, "jω must be finite")) + return Functor(formula, input, (;)) +end + +""" +$(TYPEDSIGNATURES) + +Build the Functor of an insulation admittivity law for one material at one frequency and +temperature, after checking both. The state is empty. +""" +function Functor(formula::InsulationAdmittanceFormulation, input::NamedTuple; + workspace = nothing) + (; frequency, temperature) = input + isfinite(frequency) && frequency > zero(frequency) || throw(DomainError( + frequency, "insulation constitutive frequency must be positive and finite")) + isfinite(temperature) || throw(DomainError( + temperature, "insulation constitutive temperature must be finite")) + return Functor(formula, input, (;)) +end + +""" +$(TYPEDSIGNATURES) + +Build the Functor of a semiconductor admittivity law for one material at one frequency and +temperature, after checking both. The state is empty. +""" +function Functor(formula::SemiconAdmittanceFormulation, input::NamedTuple; + workspace = nothing) + (; frequency, temperature) = input + isfinite(frequency) && frequency > zero(frequency) || throw(DomainError( + frequency, "semicon constitutive frequency must be positive and finite")) + isfinite(temperature) || throw(DomainError( + temperature, "semicon constitutive temperature must be finite")) + return Functor(formula, input, (;)) +end + +""" +$(TYPEDSIGNATURES) + +Build the Functor of an earth calculation of the impedance formula `formula` at one +frequency, whose parts write the impedance coefficients of each conductor pair into `Zearth` +of the workspace buffers, with an empty state. +""" +function Functor(formula::EarthImpedanceFormulation, input::NamedTuple; workspace) + return Functor(formula, merge(input, (destinations = (workspace.buffers.Zearth,),)), (;)) +end + +""" +$(TYPEDSIGNATURES) + +Build the Functor of an earth calculation of the admittance formula `formula` at one +frequency, whose parts write the potential coefficients of each conductor pair into `Pearth` +of the workspace buffers, with an empty state. +""" +function Functor(formula::EarthAdmittanceFormulation, input::NamedTuple; workspace) + return Functor(formula, merge(input, (destinations = (workspace.buffers.Pearth,),)), (;)) +end + +""" +$(TYPEDSIGNATURES) + +Check the input of one conductor pair before an earth formula evaluates it: a finite nonzero +`jω`, the aligned material vectors of the pair, and, on a layered earth, the pair's heights +against the layer thicknesses. Return `input`. +""" +function validate(input::NamedTuple, + formula::Union{EarthImpedanceFormulation, EarthAdmittanceFormulation}) + isfinite(input.jω) && !iszero(input.jω) || + throw(DomainError(input.jω, "jω must be finite and nonzero")) + validate(input.rho, formula, input.epsilon, input.mu, input.thickness) + input.thickness === nothing || validate(input.pair, input.thickness) + return input +end + +""" +$(TYPEDSIGNATURES) + +Evaluate the earth coefficient of one indexed conductor pair at fixed angular frequency. +Material vectors list air and every physical earth layer when layer thicknesses are given, +and exactly `(air,soil)` otherwise. The call runs the checks of a computation: the pair and +its geometry, the expression for its kind and layers on an earth with these media, the +formula's options for that expression, and the pair's input. Return the coefficient as +`Complex{T}`. + +# Arguments + +- `resistivity`: aligned resistivities [Ω·m]. +- `permittivity`: absolute permittivities [F/m]. +- `permeability`: absolute permeabilities [H/m]. +- `jω`: imaginary angular frequency [1/s]. +- `pair`: indexed conductor interaction. Lengths [m]. + +# Keywords + +- `thickness`: aligned layer thicknesses [m] for a layered earth. +- `physical_pair`: the physical pair before an equivalent-earth reduction. +- `workspace`: the computation workspace, when the expression reads its buffers. +""" +function (formula::Union{EarthImpedanceFormulation, EarthAdmittanceFormulation})( + resistivity::AbstractVector{T}, permittivity::AbstractVector{T}, + permeability::AbstractVector{T}, jω::Complex{T}, pair::EarthPair; + thickness = nothing, physical_pair = pair, workspace = nothing +) where {T <: Real} + validate(pair) + expression = Expression(formula, pair) + validate(pair, expression) + validate(expression, resistivity) + options = only(formulation_options(formula, (expression,)).options) + # One pair does not need destinations. The plain constructor gives the empty state of a + # formula that does not share values. Unified, whose state is its whole system, runs only in + # a computation. + functor = Functor(formula, (; jω, thickness, pair, physical = physical_pair, + rho = resistivity, epsilon = permittivity, mu = permeability, options), (;)) + validate(functor.input, formula) + value = expression(functor, workspace) + value isa Number && isfinite(value) || + throw(DomainError((value,), "earth coefficients must be finite scalars")) + return oftype(jω, value) +end + +""" +$(TYPEDSIGNATURES) + +Build the backend formulation whose identity is `tag`: `:coaxial`, `:cable_constants`, +`:fem` or `:pscad`. `Formulation(::Val{tag}; kwargs...)` forwards `kwargs` to the +backend's constructor. PSCAD defines its own method. + +# Errors + +- Throws `ArgumentError` for an unknown `tag`. +""" +Formulation(tag::Symbol; kwargs...) = Formulation(Val(tag); kwargs...) + +Formulation(::Val{tag}; kwargs...) where {tag} = + throw(ArgumentError("unknown backend formulation :$tag")) + +function _fem_formulation( + insulation_admittance, semicon_admittance, earth_properties, temperature_dependence, + options::FormulationOptions +) + methods = ( + insulation_admittance = InsulationAdmittance.Formula(insulation_admittance), + semicon_admittance = SemiconAdmittance.Formula(semicon_admittance), + earth_properties = earth_properties === nothing ? nothing : + Earth.FrequencyDependent.Formula(earth_properties), + temperature_dependence = temperature_dependence === nothing ? nothing : + TemperatureDependent.Formula(temperature_dependence) + ) + definitions = (; insulation_admittance, semicon_admittance, earth_properties, + temperature_dependence) + return LineCableModelsFEM(methods, formulation_options(LineCableModelsFEM, options), + definitions) +end + +""" +$(TYPEDSIGNATURES) + +Construct the Gmsh/GetDP finite-element formulation. FEM defines its field equations +and selects four material laws. Each law and `options` accepts a +scalar or an explicit `Grid`/`Gridspace`. Varying inputs return a +`Gridspace{LineCableModelsFEM}`. + +# Keywords + +- `insulation_admittance`: insulation admittivity law. `:default` routes to + `:lossless`. +- `semicon_admittance`: semicon admittivity law. `:default` routes to + `:lossless`. +- `earth_properties`: soil frequency-dependent constitutive law. `:default` + routes to the explicit `:constant` pass-through, while `nothing` preserves + the declared static soil. Equivalent-earth reductions are unsupported. Air + uses its declared static properties. +- `temperature_dependence`: cable-material resistivity law. `:default` selects + the linear law and `nothing` retains reference resistivity. Operating + temperature belongs to `LineParametersProblem`. +- `options=(;)`: field model (`physics=:quasi_tem` or `:quasi_fw`) and bundle, + Kron, and ideal-transposition reductions. Hyphenated strings and symbols + are also accepted for `physics`. Julia parses `:quasi-fw` as subtraction. + use `:quasi_fw` or `Symbol("quasi-fw")`. + Pass execution controls to `compute(...; options=(...))`. +- `combine=:product`: product or zip composition among varying inputs. + +Analytical impedance and admittance kernel keywords are rejected. Supported enclosure +geometry is represented directly in the FEM domain. +""" +function LineCableModelsFEM(; + insulation_admittance = formula(:default), + semicon_admittance = formula(:default), + earth_properties = formula(:default), + temperature_dependence = formula(:default), + options = FormulationOptions(), + combine::Symbol = :product +) + return parameterize( + LineCableModelsFEM, + (inputs...) -> _fem_formulation(inputs[1:(end - 1)]..., + last(inputs) isa NamedTuple ? FormulationOptions(last(inputs)) : last(inputs)), + (insulation_admittance, semicon_admittance, earth_properties, + temperature_dependence, options); + combine + ) +end + +Formulation(::Val{:fem}; kwargs...) = LineCableModelsFEM(; kwargs...) + +# An earth equation admits an equivalent-earth reduction only through its own method. +function validate(reduction::EquivalentHomogeneous.AbstractRule, + expression::Expression{<:Union{EarthImpedanceFormulation, EarthAdmittanceFormulation}}) + throw(ArgumentError("$expression does not admit equivalent-earth reduction :$(formula_id(reduction))")) +end + +"""Expose FEM constitutive and admittance selections, field model, and reductions.""" +function Base.NamedTuple(value::LineCableModelsFEM) + record = function (selected) + selected === nothing && return nothing + selected isa Symbol && return NamedTuple(formula(selected)) + selected isa NamedTuple && return map(record, selected) + return NamedTuple(selected) + end + Record=NamedTuple{(:backend, :requested, :methods, :options), + Tuple{Symbol, NamedTuple, NamedTuple, NamedTuple}} + return Record(( + :fem, map(record, value.definitions), map(record, value.methods), value.options.data)) +end diff --git a/src/engine/helpers.jl b/src/engine/helpers.jl deleted file mode 100644 index 586fc5b20..000000000 --- a/src/engine/helpers.jl +++ /dev/null @@ -1,182 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Inspects all numerical data within a `LineParametersProblem` and determines the -common floating-point type. If any value (frequencies, geometric properties, -material properties, or earth properties) is a `Measurement`, the function -returns `Measurement{Float64}`. Otherwise, it returns `Float64`. -""" -function _find_common_type(problem::LineParametersProblem) - # Check frequencies - any(x -> x isa Measurement, problem.frequencies) && return Measurement{Float64} - - # Check cable system properties - for cable in problem.system.cables - (cable.horz isa Measurement || cable.vert isa Measurement) && - return Measurement{Float64} - for component in cable.design_data.components - if any( - x -> x isa Measurement, - ( - component.conductor_group.r_in, - component.conductor_group.r_ex, - component.insulator_group.r_in, - component.insulator_group.r_ex, - component.conductor_props.rho, component.conductor_props.mu_r, - component.conductor_props.eps_r, - component.insulator_props.rho, component.insulator_props.mu_r, - component.insulator_props.eps_r, - component.insulator_group.shunt_capacitance, - component.insulator_group.shunt_conductance, - ), - ) - return Measurement{Float64} - end - end - end - - # Check earth model properties - if !isnothing(problem.earth_props) - for layer in problem.earth_props.layers - if any(x -> x isa Measurement, (layer.rho_g, layer.mu_g, layer.eps_g)) - return Measurement{Float64} - end - end - end - - if !isnothing(problem.temperature) - if problem.temperature isa Measurement - return Measurement{Float64} - end - - end - - return Float64 -end - -function _get_earth_data( - functor::AbstractEHEMFormulation, - earth_model::EarthModel, - freq::Vector{<:REALSCALAR}, - T::DataType, -) - return functor(earth_model, freq, T) -end - -""" -Default method for when no EHEM formulation is provided. -""" -function _get_earth_data(::Nothing, - earth_model::EarthModel, - freq::AbstractVector{<:REALSCALAR}, - ::Type{T}) where {T <: REALSCALAR} - - nL = length(earth_model.layers) - nF = length(freq) - - ρ = Matrix{T}(undef, nL, nF) - ε = Matrix{T}(undef, nL, nF) - μ = Matrix{T}(undef, nL, nF) - - @inbounds for i in 1:nL - L = earth_model.layers[i] - @assert length(L.rho_g) == nF && length(L.eps_g) == nF && length(L.mu_g) == nF - # Fill elementwise to avoid temp vectors - for j in 1:nF - ρ[i, j] = T(to_nominal(L.rho_g[j])) - ε[i, j] = T(to_nominal(L.eps_g[j])) - μ[i, j] = T(to_nominal(L.mu_g[j])) - end - end - - return (rho_g = ρ, eps_g = ε, mu_g = μ) -end - -@inline function _get_outer_radii(cable_map::AbstractVector{Int}, - r_ext::AbstractVector{T}, - r_ins_ext::AbstractVector{T}) where {T <: Real} - @assert length(cable_map) == length(r_ext) == length(r_ins_ext) - n = length(cable_map) - G = maximum(cable_map) - gmax = fill(zero(T), G) - @inbounds for i in 1:n - g = cable_map[i] - r = max(r_ext[i], r_ins_ext[i]) - if r > gmax[g] - ; - gmax[g] = r; - end - end - return gmax -end - -@inline function _calc_horz_sep!(dest::AbstractMatrix{T}, - horz::AbstractVector{T}, - r_ext::AbstractVector{T}, - r_ins_ext::AbstractVector{T}, - cable_map::AbstractVector{Int}) where {T <: Real} - @assert size(dest, 1) == size(dest, 2) == length(horz) == - length(r_ext) == length(r_ins_ext) == length(cable_map) - n = length(horz) - gmax = _get_outer_radii(cable_map, r_ext, r_ins_ext) - @inbounds for j in 1:n, i in 1:n - if cable_map[i] == cable_map[j] - dest[i, j] = gmax[cable_map[i]] - else - dest[i, j] = abs(horz[i] - horz[j]) - end - end - return dest -end - -@inline function _get_cable_indices(ws) - Nc = ws.n_cables - idxs_by_cable = [Int[] for _ in 1:Nc] - @inbounds for i in 1:ws.n_phases - push!(idxs_by_cable[ws.cable_map[i]], i) - end - heads = similar(collect(1:Nc)) - @inbounds for c in 1:Nc - heads[c] = idxs_by_cable[c][1] # representative (any member) per cable - end - return idxs_by_cable, heads -end - -@inline function _to_phase!(A::AbstractMatrix{Complex{T}}) where {T <: REALSCALAR} - m, n = size(A) - - # Right-multiply by T_I (lower-triangular ones): cumulative sum of columns, right→left - @inbounds for j in (n-1):-1:1 - @views A[:, j] .+= A[:, j+1] - end - - # Left-multiply by T_V^{-1} (bidiagonal solve): cumulative sum of rows, bottom→top - @inbounds for i in (m-1):-1:1 - @views A[i, :] .+= A[i+1, :] - end - - return A -end - -# function _to_phase!( -# M::Matrix{Complex{T}}, -# ) where {T <: REALSCALAR} -# # Check the size of the M matrix (assuming M is NxN) -# N = size(M, 1) - -# # Build the voltage transformation matrix T_V -# T_V = Matrix{T}(I, N, N + 1) # Start with an identity matrix -# for i ∈ 1:N -# T_V[i, i+1] = -1 # Set the -1 in the next column -# end -# T_V = T_V[:, 1:N] # Remove the last column - -# # Build the current transformation matrix T_I -# T_I = tril(ones(T, N, N)) # Lower triangular matrix of ones - -# # Compute the new impedance matrix M_prime -# M = T_V \ M * T_I - -# return M -# end - diff --git a/src/engine/impedance.jl b/src/engine/impedance.jl new file mode 100644 index 000000000..32b76ebb7 --- /dev/null +++ b/src/engine/impedance.jl @@ -0,0 +1,109 @@ +""" +$(TYPEDSIGNATURES) + +Assemble the earth-free primitive series-impedance matrix of independent +concentric cable assemblies. + +The selected internal- and insulation-impedance formulas contribute their +outer, inner, transfer, and longitudinal-insulation terms directly to +`destination`. The matrix remains unreduced. + +# Arguments + +- `destination`: reusable primitive series-impedance matrix [Ω/m]. +- `input`: concrete local cable arrays. +- `rho_cond`: temperature-corrected conductor resistivities [Ω·m]. +- `methods`: resolved formulation methods. +- `s`: complex angular frequency ``jω`` [1/s]. + +# Returns + +- `destination`, overwritten with the assembled local matrix. +""" +function cable_impedance!( + destination::AbstractMatrix{Complex{T}}, + input::LocalCableData{T}, + rho_cond::AbstractVector{T}, + methods::NamedTuple, + s::Complex{T}; workspace = nothing +) where {T <: Real} + fill!(destination, zero(Complex{T})) + @inbounds for conductors in input.assemblies + count = length(conductors) + inside = zero(eltype(destination)) + for position in count:-1:1 + index = conductors[position] + surfaces = InternalImpedance.surface_impedances(methods.internal_impedance, + input.r_in[index] > 0 ? Val((:outer, :transfer, :inner)) : Val((:outer,)), + input.r_in[index], + input.r_ext[index], + rho_cond[index], + input.mu_r_cond[index], + s; workspace + ) + outside = surfaces.outer + transfer = position > 1 ? surfaces.transfer : zero(outside) + insulation = methods.insulation_impedance( + input.r_ext[index], + input.r_ins_ext[index], + input.mu_r_ins[index], + s; workspace + ) + loop = outside + inside + insulation + if position > 1 + for row in 1:(position - 1), column in 1:(position - 1) + + destination[conductors[row], conductors[column]] += loop - 2 * transfer + end + for row in 1:(position - 1) + destination[index, conductors[row]] += loop - transfer + destination[conductors[row], index] += loop - transfer + end + end + destination[index, index] += loop + # Reuse this wall's evaluated state when its inner surface is needed + # by the next contained conductor. + position > 1 && (inside = surfaces.inner) + end + end + return destination +end + +""" +$(TYPEDSIGNATURES) + +Add the already calculated exterior impedance to the cable-local primitive +matrix \\[Ω/m\\] and retain its optional trace at `frequency`. No equation or +material law is evaluated here. Return the mutated `destination`. +""" +function impedance!( + destination::AbstractMatrix{Complex{T}}, + workspace::LineParametersWorkspace{T}, + frequency::Int +) where {T <: Real} + input = workspace.input + indices = workspace.plan.cable_indices + earth_matrix = workspace.buffers.Zearth + trace = workspace.trace + + @inbounds for cable in 1:input.n_cables + conductors = indices[cable] + self = earth_matrix[cable, cable] + for row in conductors, column in conductors + + destination[row, column] += self + end + end + @inbounds for left in 1:(input.n_cables - 1) + for right in (left + 1):input.n_cables + mutual = earth_matrix[left, right] + for row in indices[left], column in indices[right] + + destination[row, column] += mutual + destination[column, row] += earth_matrix[right, left] + end + end + end + _stash!(trace, :Z, frequency, destination) + return destination +end diff --git a/src/engine/input.jl b/src/engine/input.jl new file mode 100644 index 000000000..f42f0ba8c --- /dev/null +++ b/src/engine/input.jl @@ -0,0 +1,454 @@ +""" +$(TYPEDEF) + +Own the numerical input and reusable storage for one coaxial line-parameter +computation. + +The constructor adapts a completed physical system once and validates the aligned numerical representation. It constructs cable and reduction indices, and +allocates every matrix used by the frequency loop. Each `compute` call uses an independent workspace. Constant fields fix +its input and buffer bindings. Reference identity avoids copying this large +record when dispatching heterogeneous equation groups. + +Bound earth calculations and their material arrays are stored as tuples. +The complete scan specializes on their concrete types once, before frequency +traversal. Conductor layout is stored as runtime data. + +$(TYPEDFIELDS) +""" +mutable struct LineParametersWorkspace{ + T <: Real, + N <: NamedTuple, + P <: NamedTuple, + B <: NamedTuple, + C +} + # Immutable numerical input derived from the problem and formulation. + const input::N + # Physical values and index maps invariant across the frequency loop. + const plan::P + # Mutable numerical storage allocated once for the computation. + const buffers::B + # Optional retained diagnostic arrays, or `nothing`. + const trace::C + + function LineParametersWorkspace{T, N, P, B, C}( + input::N, + plan::P, + buffers::B, + trace::C + ) where {T <: Real, N <: NamedTuple, P <: NamedTuple, B <: NamedTuple, C} + return validate(new{T, N, P, B, C}( + input, + plan, + buffers, + trace + )) + end +end + +Base.eltype(::LineParametersWorkspace{T}) where {T} = T +Base.eltype(::Type{<:LineParametersWorkspace{T}}) where {T} = T + +function validate(workspace::LineParametersWorkspace) + input = workspace.input + cable = input.cable + n = input.n_phases + input.n_frequencies == length(input.freq) || throw(DimensionMismatch( + "frequency count differs from the frequency vector" + )) + input.n_cables == maximum(input.cable_map) || throw(DimensionMismatch( + "cable count differs from the cable map" + )) + for values in ( + input.horz, input.vert, input.phase_map, input.cable_map, + input.design_map, cable.terminals, cable.positions, cable.r_in, + cable.r_ext, cable.r_ins_in, cable.r_ins_ext, cable.conductor_materials, + cable.mu_r_cond, cable.mu_r_ins, + cable.dielectric_ranges + ) + length(values) == n || throw(DimensionMismatch( + "engine input arrays must have $n component entries" + )) + end + n_layers = length(cable.dielectric_materials) + for values in ( + cable.r_layer_in, cable.r_layer_ext, + workspace.buffers.layer_coefficients + ) + length(values) == n_layers || throw(DimensionMismatch( + "dielectric-layer arrays must contain $n_layers entries" + )) + end + sort(vcat( + cable.insulation_indices, + cable.semicon_indices + )) == collect(1:n_layers) || throw(DimensionMismatch( + "insulation and semicon indices must partition the dielectric layers" + )) + size(input.horz_sep) == (n, n) || throw(DimensionMismatch( + "horizontal separation matrix must be $n×$n" + )) + length(workspace.plan.cable_indices) == input.n_cables || throw( + DimensionMismatch("cable indices must align with the cable count") + ) + all(!isempty, workspace.plan.cable_indices) || throw(ArgumentError( + "every cable must contain one retained primitive conductor" + )) + size(workspace.buffers.Zprimitive) == (n, n) || throw(DimensionMismatch( + "primitive impedance storage must be $n×$n" + )) + size(workspace.buffers.Pprimitive) == (n, n) || throw(DimensionMismatch( + "primitive potential-coefficient storage must be $n×$n" + )) + return workspace +end + +""" +$(TYPEDSIGNATURES) + +Construct the formulation-independent coaxial input for one validated +line-parameter problem. + +The selected designs have already been flattened into frequency-independent +blueprints. This step constructs local cable arrays, physical geometry, +terminal indices, and frequency coordinates once. + +# Arguments + +- `problem`: completed line-parameter problem. +- `blueprints`: one frequency-independent blueprint per selected design. + +# Returns + +- A read-only named tuple shared by independent formulation workspaces. +""" +function lineinput( + problem::LineParametersProblem{T}, + blueprints::Vector{CableBlueprint{T}} +) where {T <: Real} + system = problem.system + length(blueprints) == length(system.designs) || throw(DimensionMismatch( + "line-parameter blueprints must align with the selected system designs", + )) + cable = LocalCableData(blueprints) + n_frequencies = length(problem.frequencies) + n_phases = length(system.terminal_order) + length(cable.terminals) == n_phases || throw(DimensionMismatch( + "DataModel terminal order differs from the cable blueprint count" + )) + n_cables = length(cable.assemblies) + + freq = copy(problem.frequencies) + jω = Complex{T}.(im .* (2 * (one(first(freq)) * π) .* freq)) + horz = Vector{T}(undef, n_phases) + horz_sep = Matrix{T}(undef, n_phases, n_phases) + vert = Vector{T}(undef, n_phases) + phase_map = copy(system.connection_order) + design_map = Int[entry.cable for entry in system.terminal_order] + cable_map = Vector{Int}(undef, n_phases) + @inbounds for (assembly, indices) in pairs(cable.assemblies), index in indices + + cable_map[index] = assembly + end + + @inbounds for index in eachindex(cable.terminals) + terminal = system.terminal_order[index] + terminal.terminal === cable.terminals[index] || throw(DimensionMismatch( + "DataModel terminal order is not aligned with the cable blueprint" + )) + design_index = design_map[index] + cable.assembly_designs[cable_map[index]] == design_index || + throw(DimensionMismatch( + "blueprint assembly ownership differs from system terminal order" + )) + position = system.positions[design_index] + local_x, local_y = cable.positions[index] + horz[index] = position.x + cos(position.φ) * local_x - + sin(position.φ) * local_y + vert[index] = position.y + sin(position.φ) * local_x + + cos(position.φ) * local_y + end + horizontal_separation!( + horz_sep, + horz, + cable.r_ext, + cable.r_ins_ext, + cable_map + ) + return ( + freq, + jω, + horz, + horz_sep, + vert, + cable, + phase_map, + cable_map, + design_map, + earth = problem.earth_props, + temperature = problem.temperature, + line_length = system.line_length, + n_frequencies, + n_phases, + n_cables + ) +end + +function lineinput(::Type{T}, input::NamedTuple) where {T <: Real} + T === eltype(input.freq) && return input + return merge(input, + ( + freq = T.(input.freq), jω = Complex{T}.(input.jω), + horz = T.(input.horz), vert = T.(input.vert), horz_sep = T.(input.horz_sep), + cable = convert(LocalCableData{T}, input.cable), + earth = convert(EarthModel{T}, input.earth), + temperature = convert(T, input.temperature), + line_length = convert(T, input.line_length))) +end + +function LineParametersWorkspace( + problem::LineParametersProblem{T}, + formulation::LineParametersFormulation, + execution::ComputationOptions, + blueprints::Vector{CableBlueprint{T}} +) where {T <: Real} + return LineParametersWorkspace( + problem, + formulation, + execution, + lineinput(problem, blueprints) + ) +end + +function LineParametersWorkspace( + problem::LineParametersProblem{T}, + formulation::LineParametersFormulation, + execution::ComputationOptions, + input::NamedTuple +) where {T <: Real} + cable = input.cable + horz = input.horz + horz_sep = input.horz_sep + vert = input.vert + phase_map = input.phase_map + cable_indices = [collect(indices) for indices in cable.assemblies] + cable_representatives = first.(cable_indices) + physical_pairs = earth_pairs( + cable_representatives, + horz, + vert, + horz_sep, + problem.earth_props + ) + geometry = (radius = _outer_radii(input.cable_map, cable.r_ext, cable.r_ins_ext), + layers = [layer_index(pair)[1] for pair in physical_pairs if pair.row == pair.column]) + # Each slot decides its earth once. A recipe then picks each pair's formula on the decided + # layers. Impedance and admittance calculations that solve the same system merge. + earth = EarthPlan( + EarthPlan(formulation.methods.earth_impedance, problem.earth_props, physical_pairs, + geometry), + EarthPlan(formulation.methods.earth_admittance, problem.earth_props, physical_pairs, + geometry)) + options = formulation.options.data + reduction = ReductionPlan(phase_map; options.reduce_bundle, options.kron_reduction, + options.ideal_transposition) + plan = (; cable_indices, reduction, earth, geometry) + # The earth formulas that the computation uses: each slot's formula, or the formulas of a + # recipe that a calculation uses, the others `nothing`. They widen the scalar type and + # provision their arrays. + formulas = map(formulation.methods[(:earth_impedance, :earth_admittance)]) do selected + selected isa NamedTuple || return selected + map(selected) do leaf + any(calculation -> any(entry -> entry !== nothing && entry.formula === leaf, + (calculation.impedance, calculation.admittance)), earth.calculations) ? + leaf : nothing + end + end + scalar = computation_type(T, formulas, input.freq) + return LineParametersWorkspace{scalar}(problem, formulation, execution, + lineinput(scalar, input), plan, formulas) +end + +function LineParametersWorkspace{T}( + problem::LineParametersProblem, + formulation::LineParametersFormulation, + execution::ComputationOptions, + input::NamedTuple, + plan::NamedTuple, + formulas::NamedTuple +) where {T <: Real} + cable = input.cable + n_phases, n_cables, n_frequencies = input.n_phases, input.n_cables, input.n_frequencies + n_layers = length(cable.dielectric_materials) + cable_indices = plan.cable_indices + nkeep = length(plan.reduction.keep) + # The plan keeps the conductor radii in the computation's scalar type. + plan = merge(plan, (geometry = (radius = convert(Vector{T}, plan.geometry.radius), + layers = plan.geometry.layers),)) + rho_cond = Vector{T}(undef, length(cable.conductor_materials)) + + Zprimitive = Matrix{Complex{T}}(undef, n_phases, n_phases) + Pprimitive = similar(Zprimitive) + Zout = Array{Complex{T}, 3}(undef, nkeep, nkeep, n_frequencies) + Yout = similar(Zout) + Zearth = Matrix{Complex{T}}(undef, n_cables, n_cables) + Pearth = similar(Zearth) + trace = if execution.data.trace isa Val{true} + (Zin = Array{Complex{T}, 3}(undef, n_phases, n_phases, n_frequencies), + Pin = Array{Complex{T}, 3}(undef, n_phases, n_phases, n_frequencies), + Zg = Array{Complex{T}, 3}(undef, n_cables, n_cables, n_frequencies), + Pg = Array{Complex{T}, 3}(undef, n_cables, n_cables, n_frequencies), + Z = Array{Complex{T}, 3}(undef, n_phases, n_phases, n_frequencies), + P = Array{Complex{T}, 3}(undef, n_phases, n_phases, n_frequencies), + integrals = NamedTuple[]) + else + nothing + end + observations = trace === nothing ? nothing : trace.integrals + largest_cable = maximum(length, cable_indices) + coefficients = Vector{Complex{T}}(undef, largest_cable) + tails = similar(coefficients) + layer_coefficients = Vector{Complex{T}}(undef, n_layers) + dielectric_admittivity = similar(layer_coefficients) + buffers = (; + rho_cond, + dielectric_admittivity, + Zprimitive, + Pprimitive, + Zout, + Yout, + Zearth, + Pearth, + observations, + layer_coefficients, + coefficients, + tails + ) + buffers = initialize_buffers(plan.reduction, Complex{T}, input, plan, buffers) + # The internal formula has an expression for each surface impedance that the geometry + # needs. + for kind in (any(>(0), cable.r_in) ? (:inner, :outer, :transfer) : (:outer,)) + validate(Expression(formulation.methods.internal_impedance, + InternalImpedance.internal_impedance, Val(kind))) + end + # Concrete formulas provision numerical storage, never option-key inspection. A recipe + # formula provisions arrays only when a calculation uses it. The earth plan then + # provisions its calculations. + buffers = initialize_buffers(merge(formulation.methods, formulas), T, input, plan, buffers) + buffers = initialize_buffers(plan.earth, T, input, plan, buffers) + workspace = LineParametersWorkspace{ + T, + typeof(input), + typeof(plan), + typeof(buffers), + typeof(trace) + }(input, plan, buffers, trace) + return workspace +end + +function initialize_buffers( + selected::Union{EarthImpedanceFormulation, EarthAdmittanceFormulation}, + ::Type{T}, input, plan, buffers) where {T} + return initialize_buffers(selected.equivalent_earth, T, input, plan, buffers) +end + +function layer_index(problem::LineParametersProblem, horizontal, vertical) + layer_index(problem.earth_props, horizontal, vertical) +end +layer_index(pair::EarthPair) = pair.layers + +function layer_index(model::EarthModel, horizontal, vertical) + vertical > zero(vertical) && return 1 + iszero(vertical) && throw(ArgumentError( + "a conductor on the air-earth interface has no physical layer" + )) + model.vertical_layers && length(model.layers) > 2 && + throw(ArgumentError( + "physical source/target indexing for vertical earth interfaces is not implemented")) + depth = -vertical + boundary = zero(depth) + @inbounds for layer in 2:length(model.layers) + thickness = model.layers[layer].thickness + isinf(thickness) && return layer + boundary += thickness + depth <= boundary && return layer + end + throw(ArgumentError( + "conductor depth $depth m is outside the earth-layer model" + )) +end + +""" +$(TYPEDSIGNATURES) + +Construct every ordered external interaction from resolved conductor geometry. +Source columns and target rows retain physical earth-layer indices. Air is 1. + +`cables` contains representative conductor indices. `horizontal`, `vertical`, +and `separation` are aligned coordinates and distances in meters. Diagonal +separations supply the external self radius. Layer assignment uses `earth`'s +physical interfaces and rejects conductors on the interface between air and earth. + +Return ordered geometry payloads for indexed formula dispatch. Formula +selection and equivalent-earth reduction occur separately. +""" +function earth_pairs( + cables::AbstractVector{Int}, + horizontal, + vertical, + separation, + earth::EarthModel +) + T = eltype(vertical) + pairs = EarthPair{T}[] + sizehint!(pairs, length(cables)^2) + placed_layers = [layer_index(earth, horizontal[index], vertical[index]) + for index in cables] + @inbounds for column in eachindex(cables), row in eachindex(cables) + + source = cables[column] + target = cables[row] + layers = (placed_layers[column], placed_layers[row]) + push!(pairs, + EarthPair( + row, + column, + (vertical[source], vertical[target]), + row == column ? zero(T) : separation[target, source], + layers; radius = row == column ? separation[target, source] : nothing + )) + end + return pairs +end + +@inline function _outer_radii(cable_map, r_ext_values, r_ins_ext) + length(cable_map) == length(r_ext_values) == length(r_ins_ext) || + throw(DimensionMismatch("cable maps and radius vectors must align")) + outer = fill(zero(eltype(r_ext_values)), maximum(cable_map)) + @inbounds for index in eachindex(cable_map) + cable = cable_map[index] + outer[cable] = max(outer[cable], r_ext_values[index], r_ins_ext[index]) + end + return outer +end + +function horizontal_separation!( + destination, + horizontal, + r_ext_values, + r_ins_ext, + cable_map +) + n = length(horizontal) + size(destination) == (n, n) || throw(DimensionMismatch( + "horizontal separation matrix must be $n×$n" + )) + outer = _outer_radii(cable_map, r_ext_values, r_ins_ext) + @inbounds for column in 1:n, row in 1:n + + destination[row, column] = cable_map[row] == cable_map[column] ? + outer[cable_map[row]] : + abs(horizontal[row] - horizontal[column]) + end + return destination +end diff --git a/src/engine/insulationadmittance/InsulationAdmittance.jl b/src/engine/insulationadmittance/InsulationAdmittance.jl index f99ea7593..6a08a872d 100644 --- a/src/engine/insulationadmittance/InsulationAdmittance.jl +++ b/src/engine/insulationadmittance/InsulationAdmittance.jl @@ -1,27 +1,52 @@ """ - LineCableModels.Engine.InsulationAdmittance + LineCableModels.Engine.InsulationAdmittance + +Define registered constitutive relations for cable-insulation admittance. +`:lossy` retains material conduction and displacement current. `:lossless` +explicitly selects the lossless approximation. `:default` routes to +`:lossless`. # Dependencies $(IMPORTS) -# Exports - -$(EXPORTS) """ module InsulationAdmittance +import ...Commons: FormulationOptions, formulas +using ...Commons: Functor +import ...Commons: formulation_options # Export public API -export Lossless, ParallelRC +export Formula, formula_id, formulas # Module-specific dependencies -using ...Commons -import ...Commons: get_description -using ...Utils: _to_σ -import ..Engine: InsulationAdmittanceFormulation -using Measurements - -include("lossless.jl") -include("parallelrc.jl") +#! explicit-imports: off +# IMPORTS is expanded in this module docstring rather than called as Julia code. +using DocStringExtensions: IMPORTS, TYPEDEF, TYPEDFIELDS, TYPEDSIGNATURES +#! explicit-imports: on +import ..Engine: InsulationAdmittanceFormulation, formula_id, validate +import ...LineCableModels: FormulaDefinition, Expression +using ...Materials: Material +#! explicit-imports: off +import ..Engine: description, conductivity +using ...Commons: vacuum_permittivity +#! explicit-imports: on + +include("interface.jl") + +public insulation_material + +#! explicit-imports: off +const FORMULAS = ( + include("formulas/default.jl"), + include("formulas/lossless.jl"), + include("formulas/lossy.jl"), +) +#! explicit-imports: on + +""" +Return the built-in insulation-admittance formula identifiers. +""" +formulas(::Type{<:Formula}) = FORMULAS end # module InsulationAdmittance diff --git a/src/engine/insulationadmittance/formulas/default.jl b/src/engine/insulationadmittance/formulas/default.jl new file mode 100644 index 000000000..c8a84fb5c --- /dev/null +++ b/src/engine/insulationadmittance/formulas/default.jl @@ -0,0 +1,12 @@ +""" +$(TYPEDSIGNATURES) + +Route the default selection to the explicit `:lossless` implementation. +No numerical equation is owned by `:default`. +""" +description(::Type{<:Formula{:default}}; compact::Bool=false) = + compact ? "Default" : "Default routing to :lossless" + +Formula{:default}(; kwargs...) = Formula{:lossless}(; kwargs...) + +:default diff --git a/src/engine/insulationadmittance/formulas/lossless.jl b/src/engine/insulationadmittance/formulas/lossless.jl new file mode 100644 index 000000000..9800f0c69 --- /dev/null +++ b/src/engine/insulationadmittance/formulas/lossless.jl @@ -0,0 +1,52 @@ +""" +$(TYPEDSIGNATURES) + +**Identification.** Lossless cable-insulation approximation retaining +displacement current while suppressing conduction and polarization loss. + +**Expression.** + +```math +\\kappa=j\\omega\\varepsilon_0\\varepsilon_r. +``` + +This is the lossless specialization of the standard frequency-domain +constitutive relation. +""" +function description(::Type{<:Formula{:lossless}}; compact::Bool=false) + compact ? "Lossless" : "Lossless cable-insulation admittivity" +end + +""" +$(TYPEDSIGNATURES) + +Evaluate lossless cable-insulation admittivity: + +```math +\\kappa=j\\omega\\varepsilon_0\\varepsilon_r. +``` + +# Arguments + +- `functor`: the Functor of the evaluation point. Its input holds: + - `material`: insulation material and relative permittivity. + - `frequency`: evaluation frequency \\[Hz\\]. + - `temperature`: operating temperature \\[°C\\]. + - `options`: the normalized formulation options of the formula. +- `workspace`: optional computation workspace supplying reusable numerical buffers. + +# Returns + +- Complex lossless admittivity \\[S/m\\]. +""" +@inline function insulation_material(::Formula{:lossless}, functor, workspace) + (; material, frequency) = functor.input + T = typeof(frequency) + ε₀ = vacuum_permittivity(T) + ω = 2 * (one(T) * π) * frequency + return complex(zero(T), ω) * ε₀ * material.eps_r +end + +formulation_options(::Expression{<:Formula{:lossless}, typeof(insulation_material)}) = FormulationOptions() + +:lossless diff --git a/src/engine/insulationadmittance/formulas/lossy.jl b/src/engine/insulationadmittance/formulas/lossy.jl new file mode 100644 index 000000000..2e10438cd --- /dev/null +++ b/src/engine/insulationadmittance/formulas/lossy.jl @@ -0,0 +1,73 @@ +""" +$(TYPEDSIGNATURES) + +**Identification.** Generic lossy complex-admittivity representation of a +homogeneous cable-insulation layer. Ohmic conduction, dielectric displacement, +and an optional polarization-loss contribution are evaluated together. + +**Expression.** For resistivity ``\\rho``, real relative permittivity +``\\varepsilon_r``, and polarization loss tangent ``\\tan\\delta_p``, + +```math +\\kappa=\\frac{1}{\\rho}+\\omega\\varepsilon_0\\varepsilon_r\\tan\\delta_p+ +j\\omega\\varepsilon_0\\varepsilon_r. +``` + +The annular layer operator converts this material admittivity to +``Y=2\\pi\\kappa/\\ln(b/a)``. This is the standard frequency-domain +constitutive relation, not an author-specific empirical law. Ametani, +Miyamoto, and Nagaoka (2004), Eqs. (14)-(15), remain a useful cable-layer +application reference. The paper's main-insulation term is the lossless +specialization. +""" +function description(::Type{<:Formula{:lossy}}; compact::Bool=false) + compact ? "Lossy" : "Lossy complex-admittivity cable-insulation model" +end + +""" +$(TYPEDSIGNATURES) + +Evaluate the standard lossy material admittivity for a cable-insulation layer: + +```math +\\kappa=\\frac{1}{\\rho}+\\omega\\varepsilon_0\\varepsilon_r\\tan\\delta_p+ +j\\omega\\varepsilon_0\\varepsilon_r. +``` + +`material.tan_delta` represents polarization loss only. Conduction is supplied +by `material.rho`. The common coaxial operator applies the annular geometry. + +# Arguments + +- `functor`: the Functor of the evaluation point. Its input holds: + - `material`: insulation material properties, including resistivity and + relative permittivity. + - `frequency`: evaluation frequency \\[Hz\\]. + - `temperature`: operating temperature \\[°C\\]. + - `options`: the normalized formulation options of the formula. +- `workspace`: optional computation workspace supplying reusable numerical buffers. + +# Returns + +- Complex material admittivity \\[S/m\\]. + +# Notes + +Ametani, Miyamoto, and Nagaoka (2004), DOI +10.1109/TPWRD.2003.822502, is retained as a cable-layer application +reference. The constitutive relation itself is standard frequency-domain +electromagnetism. +""" +@inline function insulation_material(::Formula{:lossy}, functor, workspace) + (; material, frequency) = functor.input + T = typeof(frequency) + ε₀ = vacuum_permittivity(T) + ω = 2 * (one(T) * π) * frequency + displacement = complex(zero(T), ω) * ε₀ * material.eps_r + return conductivity(material.rho) + imag(displacement) * material.tan_delta + + displacement +end + +formulation_options(::Expression{<:Formula{:lossy}, typeof(insulation_material)}) = FormulationOptions() + +:lossy diff --git a/src/engine/insulationadmittance/interface.jl b/src/engine/insulationadmittance/interface.jl new file mode 100644 index 000000000..658b51d89 --- /dev/null +++ b/src/engine/insulationadmittance/interface.jl @@ -0,0 +1,100 @@ +""" +$(TYPEDEF) + +Select one insulation constitutive relation by its stable literature identifier. + +Each formula implements `insulation_material(selected, functor, workspace)` on its concrete +selection type. The input of `functor` holds the material, the frequency, the temperature +and the options. The method returns the material's frequency-evaluated admittivity [S/m]. Geometry and +radial series aggregation remain common Engine operations. + +$(TYPEDFIELDS) +""" +struct Formula{ID, P <: NamedTuple, O <: FormulationOptions} <: InsulationAdmittanceFormulation + "Resolved physical model parameters." + parameters::P + "Normalized numerical sections for this equation." + options::O +end + +""" +Evaluate one formula-owned insulation-material constitutive relation. +""" +function insulation_material end + +""" +$(TYPEDSIGNATURES) + +Construct a selected formulation with model parameters and numerical controls. +Custom formulations extend `insulation_material` on their own concrete selection type. +Unknown controls fail before numerical evaluation. +""" +function Formula{ID}(; parameters::NamedTuple=(;), options::Union{NamedTuple, FormulationOptions} = FormulationOptions()) where {ID} + options = options isa NamedTuple ? FormulationOptions(options) : options + isempty(parameters) || throw(ArgumentError("formula :$ID has no configurable model parameters")) + selected = Formula{ID, typeof(parameters), typeof(options)}(parameters, options) + expression = Expression(selected, insulation_material) + normalized = formulation_options(expression, options) + return Formula{ID, typeof(parameters), typeof(normalized)}(parameters, normalized) +end + +@inline function (formula::InsulationAdmittanceFormulation)( + material::Material{T}, + frequency::T, + temperature::T; workspace = nothing +) where {T <: Real} + functor = Functor(formula, + (; material, frequency, temperature, options = formula.options); workspace) + value = Expression(formula, insulation_material)(functor, workspace) + return convert(Complex{T}, validate(value, formula, T)) +end + +function (formula::InsulationAdmittanceFormulation)( + material::Material{T}, + frequency::Real, + temperature::Real; workspace = nothing +) where {T <: Real} + U = promote_type( + T, + typeof(float(frequency)), + typeof(float(temperature)) + ) + return formula( + convert(Material{U}, material), + convert(U, float(frequency)), + convert(U, float(temperature)); workspace + ) +end + +Formula(identifier::Symbol; kwargs...) = Formula(Val(identifier); kwargs...) +Formula(::Val{ID}; kwargs...) where {ID} = Formula{ID}(; kwargs...) +Formula(selected::InsulationAdmittanceFormulation) = selected + +function Formula(selection::FormulaDefinition{ID, Order}) where {ID, Order} + Order === :default || throw(ArgumentError("order applies only to equivalent_earth")) + selection.equivalent_earth === nothing || throw(ArgumentError( + "equivalent_earth applies only to external earth formulas")) + return Formula{ID}(; parameters = selection.parameters, + options = selection.options) +end + +""" +Return the stable identifier of an insulation-admittance formula. +""" +formula_id(::Formula{ID}) where {ID} = ID +formula_id(::Type{<:Formula{ID}}) where {ID} = ID +# Identity-only dispatch also describes retained selections without constructors. +description(value::Formula; compact::Bool = false) = description(typeof(value); compact) +formulation_options(value::Formula) = value.options + +""" +$(TYPEDSIGNATURES) + +Expose the selected identity, model parameters, and numerical options as a native record. +""" +function Base.NamedTuple(value::Formula) + return (identifier=formula_id(value), parameters=value.parameters, options=value.options.data) +end + +"""Iterate the independently selectable child slots admitted by this formula family.""" +Base.pairs(::Type{<:Formula}; quantity=nothing) = pairs((;)) diff --git a/src/engine/insulationadmittance/lossless.jl b/src/engine/insulationadmittance/lossless.jl deleted file mode 100644 index 79f3369af..000000000 --- a/src/engine/insulationadmittance/lossless.jl +++ /dev/null @@ -1,38 +0,0 @@ -struct Lossless <: InsulationAdmittanceFormulation end -get_description(::Lossless) = "Lossless insulation (ideal dielectric)" - -@inline function (f::Lossless)( - r_in::T, - r_ex::T, - epsr_i::T, - jω::Complex{T}, - loss_factor::T, -) where {T <: REALSCALAR} - - if isapprox(r_in, 0.0, atol = eps(T)) || isapprox(r_in, r_ex, atol = eps(T)) - # TODO: Implement consistent handling of admittance for bare conductors - # Issue URL: https://github.com/Electa-Git/LineCableModels.jl/issues/17 - return zero(Complex{T}) - end - - # Constants - eps_i = T(ε₀) * epsr_i - - - return Complex{T}(log(r_ex / r_in) / (2π * eps_i)) -end - -@inline function potential_coefficient( - f::Lossless, - ws, - component_idx::Int, - jω::Complex{T}, -) where {T <: REALSCALAR} - return f( - ws.r_ins_in[component_idx], - ws.r_ins_ext[component_idx], - ws.eps_ins[component_idx], - jω, - ws.tan_ins[component_idx], - ) -end diff --git a/src/engine/insulationadmittance/parallelrc.jl b/src/engine/insulationadmittance/parallelrc.jl deleted file mode 100644 index f5f3ba244..000000000 --- a/src/engine/insulationadmittance/parallelrc.jl +++ /dev/null @@ -1,98 +0,0 @@ -""" - ParallelRC - -Represent each concentric insulation layer as a frequency-independent shunt -conductance in parallel with its capacitance. - -# Notes - -For a layer with inner radius ``r_i`` \\[m\\], outer radius ``r_o`` \\[m\\], -resistivity ``\\rho`` \\[Ω·m\\], and relative permittivity -``\\varepsilon_r`` \\[dimensionless\\], the per-unit-length layer admittance is - -```math -y(s) = G + sC, -\\qquad -C = \\frac{2\\pi\\varepsilon_0\\varepsilon_r}{\\ln(r_o/r_i)}, -\\qquad -G = \\frac{2\\pi}{\\rho\\ln(r_o/r_i)}. -``` - -The EMT solver assembles potential coefficients, so this formulation returns -``p(s)=s/y(s)`` for every layer and adds series layer coefficients before matrix -assembly. Material uncertainties therefore propagate through the complete -admittance calculation. -""" -struct ParallelRC <: InsulationAdmittanceFormulation end - -get_description(::ParallelRC) = - "Parallel-RC insulation (constant conductivity and permittivity)" - -""" - (formulation::ParallelRC)(r_in, r_ex, rho, eps_r, s) - -Calculate the potential coefficient of one concentric lossy dielectric layer. - -# Arguments - -- `r_in`: Inner layer radius \\[m\\]. -- `r_ex`: Outer layer radius \\[m\\]. -- `rho`: Dielectric resistivity \\[Ω·m\\]. -- `eps_r`: Relative dielectric permittivity \\[dimensionless\\]. -- `s`: Complex angular frequency \\[rad/s\\]. - -# Returns - -- Complex potential coefficient per unit length \\[m/F\\]. - -# Notes - -This method implements - -```math -p(s) = \\frac{s}{G+sC} - = \\frac{\\ln(r_o/r_i)}{2\\pi} - \\frac{s}{1/\\rho+s\\varepsilon_0\\varepsilon_r}. -``` - -At infinite resistivity, the result reduces exactly to the lossless potential -coefficient ``1/C``. -""" -@inline function (f::ParallelRC)( - r_in::T, - r_ex::T, - rho::T, - eps_r::T, - s::Complex{T}, -) where {T <: REALSCALAR} - if isapprox(r_in, 0.0, atol = eps(T)) || isapprox(r_in, r_ex, atol = eps(T)) - # Keep bare-conductor handling consistent with Lossless. - return zero(Complex{T}) - end - - log_ratio = log(r_ex / r_in) - capacitance = T(2π * ε₀) * eps_r / log_ratio - conductivity = _to_σ(rho) - conductance = T(2π) * conductivity / log_ratio - - return s / (conductance + s * capacitance) -end - -@inline function potential_coefficient( - f::ParallelRC, - ws, - component_idx::Int, - s::Complex{T}, -) where {T <: REALSCALAR} - p = zero(Complex{T}) - @inbounds for layer_idx in ws.insulator_layer_ranges[component_idx] - p += f( - ws.r_ins_layer_in[layer_idx], - ws.r_ins_layer_ext[layer_idx], - ws.rho_ins_layer[layer_idx], - ws.eps_ins_layer[layer_idx], - s, - ) - end - return p -end diff --git a/src/engine/insulationimpedance/InsulationImpedance.jl b/src/engine/insulationimpedance/InsulationImpedance.jl index 60854f40d..29e44e070 100644 --- a/src/engine/insulationimpedance/InsulationImpedance.jl +++ b/src/engine/insulationimpedance/InsulationImpedance.jl @@ -1,26 +1,47 @@ """ - LineCableModels.Engine.InsulationImpedance + LineCableModels.Engine.InsulationImpedance + +Define registered series-impedance formulas for cable insulation. # Dependencies $(IMPORTS) -# Exports - -$(EXPORTS) """ module InsulationImpedance +import ...Commons: FormulationOptions, formulas +using ...Commons: Functor +import ...Commons: formulation_options # Export public API -export Lossless - +export Formula, formula_id, formulas # Module-specific dependencies -using ...Commons -import ...Commons: get_description -import ..Engine: InsulationImpedanceFormulation -using Measurements +#! explicit-imports: off +# These abbreviations are expanded in this module docstring and included files. +using DocStringExtensions: IMPORTS, TYPEDEF, TYPEDFIELDS, TYPEDSIGNATURES +#! explicit-imports: on +import ..Engine: InsulationImpedanceFormulation, formula_id +import ...LineCableModels: FormulaDefinition, Expression +#! explicit-imports: off +import ..Engine: description +using ...Commons: vacuum_permeability +#! explicit-imports: on + +include("interface.jl") + +public insulation_impedance + +#! explicit-imports: off +const FORMULAS = ( + include("formulas/ametani1980.jl"), + include("formulas/default.jl"), +) +#! explicit-imports: on -include("lossless.jl") +""" +Return the built-in insulation-impedance formula identifiers. +""" +formulas(::Type{<:Formula}) = FORMULAS end # module InsulationImpedance diff --git a/src/engine/insulationimpedance/formulas/ametani1980.jl b/src/engine/insulationimpedance/formulas/ametani1980.jl new file mode 100644 index 000000000..16405bd00 --- /dev/null +++ b/src/engine/insulationimpedance/formulas/ametani1980.jl @@ -0,0 +1,69 @@ + +""" +$(TYPEDSIGNATURES) + +**Identification.** Longitudinal magnetic impedance of one concentric +insulation region in Ametani's single-core cable formulation. + +**Expression.** + +```math +Z_{ins}=\\frac{j\\omega\\mu_0\\mu_r}{2\\pi}\\ln\\frac{b}{a}. +``` + +The term vanishes when the annular region has zero thickness. It is assembled +with the conductor surface impedances to form the cable series-impedance +matrix. + +**Reference.** A. Ametani, “A General Formulation of Impedance and Admittance +of Cables,” *IEEE Transactions on Power Apparatus and Systems*, PAS-99(3), +902–910, 1980. DOI: 10.1109/TPAS.1980.319718. + +""" +function description(::Type{<:Formula{:ametani1980}}; compact::Bool=false) + compact ? "Ametani" : "Ametani coaxial-insulation magnetic impedance (1980)" +end + +""" +$(TYPEDSIGNATURES) + +Calculate the longitudinal magnetic impedance of one concentric insulation +region as used in Ametani's single-core cable assembly: + +```math +z_{ab}=\\frac{s\\mu_0\\mu_i}{2\\pi}\\ln\\frac{b}{a}. +``` + +# Arguments + +- `functor`: the Functor of the evaluation point. Its input holds: + - `r_in`: inner insulation radius ``a`` \\[m\\]. + - `r_ex`: outer insulation radius ``b`` \\[m\\]. + - `mu_r`: relative insulation permeability ``\\mu_i`` \\[dimensionless\\]. + - `jω`: complex angular frequency ``j\\omega`` \\[rad/s\\]. + - `options`: the normalized formulation options of the formula. +- `workspace`: optional computation workspace supplying reusable numerical buffers. + +# Returns + +- Longitudinal insulation impedance ``z_{ab}`` \\[Ω/m\\]. + +# Notes + +Implements Ametani (1980) as reproduced in Ametani, Ohno, and Nagaoka +(2015), Eqs. 2.6–2.13, and Ametani et al. (2021), Appendix A1.1.1. +""" +@inline function insulation_impedance(::Formula{:ametani1980}, functor, workspace) + (; r_in, r_ex, mu_r, jω) = functor.input + T = typeof(r_in) + if isapprox(r_in, zero(T); atol = eps(T)) || + isapprox(r_in, r_ex; atol = eps(T)) + return zero(Complex{T}) + end + μ0 = vacuum_permeability(typeof(r_in)) + return jω * μ0 * mu_r / (2 * (one(r_in) * π)) * log(r_ex / r_in) +end + +formulation_options(::Expression{<:Formula{:ametani1980}, typeof(insulation_impedance)}) = FormulationOptions() + +:ametani1980 diff --git a/src/engine/insulationimpedance/formulas/default.jl b/src/engine/insulationimpedance/formulas/default.jl new file mode 100644 index 000000000..921e5f26f --- /dev/null +++ b/src/engine/insulationimpedance/formulas/default.jl @@ -0,0 +1,12 @@ +""" +$(TYPEDSIGNATURES) + +Route the default selection to the explicit `:ametani1980` implementation. +No numerical equation is owned by `:default`. +""" +description(::Type{<:Formula{:default}}; compact::Bool=false) = + compact ? "Default" : "Default routing to :ametani1980" + +Formula{:default}(; kwargs...) = Formula{:ametani1980}(; kwargs...) + +:default diff --git a/src/engine/insulationimpedance/interface.jl b/src/engine/insulationimpedance/interface.jl new file mode 100644 index 000000000..47f8c7600 --- /dev/null +++ b/src/engine/insulationimpedance/interface.jl @@ -0,0 +1,81 @@ +""" +$(TYPEDEF) + +Select one insulation-impedance formula by its stable identifier. + +$(TYPEDFIELDS) +""" +struct Formula{ID, P <: NamedTuple, O <: FormulationOptions} <: InsulationImpedanceFormulation + "Resolved physical model parameters." + parameters::P + "Normalized numerical sections for this equation." + options::O +end + +""" +Evaluate one formula-owned insulation-impedance route. +""" +function insulation_impedance end + +""" +$(TYPEDSIGNATURES) + +Construct a selected formulation with model parameters and numerical controls. +Custom formulations extend `insulation_impedance` on their own concrete selection type. +Unknown controls fail before numerical evaluation. +""" +function Formula{ID}(; parameters::NamedTuple=(;), options::Union{NamedTuple, FormulationOptions} = FormulationOptions()) where {ID} + options = options isa NamedTuple ? FormulationOptions(options) : options + isempty(parameters) || throw(ArgumentError("formula :$ID has no configurable model parameters")) + selected = Formula{ID, typeof(parameters), typeof(options)}(parameters, options) + expression = Expression(selected, insulation_impedance) + normalized = formulation_options(expression, options) + return Formula{ID, typeof(parameters), typeof(normalized)}(parameters, normalized) +end + +@inline function (formula::InsulationImpedanceFormulation)( + r_in::T, + r_ex::T, + mu_r::T, + s::Complex{T}; workspace = nothing +) where {T <: Real} + functor = Functor(formula, (; r_in, r_ex, mu_r, jω = s, options = formula.options); + workspace) + value = Expression(formula, insulation_impedance)(functor, workspace) + value isa Number && isfinite(value) || throw(DomainError(value, + "insulation_impedance must return a finite scalar")) + return value +end + +Formula(identifier::Symbol; kwargs...) = Formula(Val(identifier); kwargs...) +Formula(::Val{ID}; kwargs...) where {ID} = Formula{ID}(; kwargs...) +Formula(selected::InsulationImpedanceFormulation) = selected + +function Formula(selection::FormulaDefinition{ID, Order}) where {ID, Order} + Order === :default || throw(ArgumentError("order applies only to equivalent_earth")) + selection.equivalent_earth === nothing || throw(ArgumentError( + "equivalent_earth applies only to external earth formulas")) + return Formula{ID}(; parameters = selection.parameters, + options = selection.options) +end + +""" +Return the stable identifier of an insulation-impedance formula. +""" +formula_id(::Formula{ID}) where {ID} = ID +formula_id(::Type{<:Formula{ID}}) where {ID} = ID +# Identity-only dispatch also describes retained selections without constructors. +description(value::Formula; compact::Bool = false) = description(typeof(value); compact) +formulation_options(value::Formula) = value.options + +""" +$(TYPEDSIGNATURES) + +Expose the selected identity, model parameters, and numerical options as a native record. +""" +function Base.NamedTuple(value::Formula) + return (identifier=formula_id(value), parameters=value.parameters, options=value.options.data) +end + +"""Iterate the independently selectable child slots admitted by this formula family.""" +Base.pairs(::Type{<:Formula}; quantity=nothing) = pairs((;)) diff --git a/src/engine/insulationimpedance/lossless.jl b/src/engine/insulationimpedance/lossless.jl deleted file mode 100644 index b70e3a8ff..000000000 --- a/src/engine/insulationimpedance/lossless.jl +++ /dev/null @@ -1,22 +0,0 @@ - -struct Lossless <: InsulationImpedanceFormulation end -get_description(::Lossless) = "Lossless insulation (ideal dielectric)" - -@inline function (f::Lossless)( - r_in::T, - r_ex::T, - mur_i::T, - jω::Complex{T}, -) where {T <: REALSCALAR} - - if isapprox(r_in, 0.0, atol = eps(T)) || isapprox(r_in, r_ex, atol = eps(T)) - # TODO: Implement consistent handling of admittance for bare conductors - # Issue URL: https://github.com/Electa-Git/LineCableModels.jl/issues/18 - return zero(Complex{T}) - end - - # Constants - mu_i = T(μ₀) * mur_i - - return Complex{T}(jω * mu_i * log(r_ex / r_in) / 2π) -end \ No newline at end of file diff --git a/src/engine/integration.jl b/src/engine/integration.jl new file mode 100644 index 000000000..5c0acaf58 --- /dev/null +++ b/src/engine/integration.jl @@ -0,0 +1,166 @@ +""" +$(TYPEDEF) + +Select a Sommerfeld-type integral over the spatial Fourier variable on `[0, Inf)`. +The formula supplies the complete scalar integrand, including any weights and Jacobians. + +$(TYPEDFIELDS) +""" +struct SpectralIntegral{F} <: AbstractFormulation + "Complete integrand, evaluated at nonnegative real coordinates." + f::F +end + +""" +$(TYPEDSIGNATURES) + +Extend `buffers` with `quadrature`, the reusable QuadGK segments of the `:quad` solution. +Coordinates have the real type of `T`, and integrand values have the type `Complex{T}`. +`buffers` returns unchanged when it holds `quadrature` already. `integrate` reads the +segments from this record. +""" +function initialize_buffers(::Type{SpectralIntegral}, ::Val{:quad}, ::Type{T}, input, plan, + buffers) where {T} + haskey(buffers, :quadrature) && return buffers + R = typeof(float(nominal(one(T)))) + V = Complex{T} + size = 128 + return merge(buffers, (quadrature = ( + segments = alloc_segbuf(R, V, R; size), + seeds = alloc_segbuf(R, V, R; size), + seed = alloc_segbuf(R, V, R; size = 1), + prototype = zero(V), + points = sizehint!(R[], size), + mapped = sizehint!(R[], size), + warnings = nothing),)) +end + +""" +$(TYPEDSIGNATURES) + +Evaluate the complete callable over `[0, Inf)` with QuadGK. QuadGK takes its storage from +`buffers.quadrature` when the caller passes `buffers`. Return +`(value, estimated_error)`, where the error is the estimated absolute +quadrature error in the same units as the integral. + +# Keywords + +- `points`: finite nonnegative subdivisions in the callable's coordinate. + The supplied collection is not mutated. +- `coordinate_type`: real coordinate type when no buffers are supplied. + Defaults to `Float64`. Buffers supply their own coordinate type. +- `context`: optional caller-provided description included in numerical warnings. +- `observations`: optional trace vector receiving the native result and context. + +An unmet requested target produces a warning and returns the finite result. +There is no outer retry or error-budget controller. Nonfinite integral values +and actual quadrature failures remain errors. +""" +@inline function integrate( + integral::SpectralIntegral, ::Val{:quad}, controls, buffers = nothing; + points = (), coordinate_type::Type{C} = Float64, context = nothing, + observations = nothing) where {C <: AbstractFloat} + quadrature=buffers===nothing ? nothing : buffers.quadrature + R=quadrature===nothing ? coordinate_type : eltype(quadrature.points) + subdivisions=quadrature===nothing ? R[] : empty!(quadrature.points) + push!(subdivisions, zero(R)) + for point in points + point isa Real && isfinite(point) && point>=0 || + throw(DomainError(point, "integration points must be finite nonnegative real coordinates")) + push!(subdivisions, R(point)) + end + all(isfinite, subdivisions) || + throw(DomainError(subdivisions, "integration points must be finite in the coordinate type")) + unique!(sort!(subdivisions; alg = Base.Sort.QuickSort)) + mapped=quadrature===nothing ? R[] : empty!(quadrature.mapped) + for point in subdivisions + push!(mapped, point/(one(R)+point)) + end + push!(mapped, one(R)) + unique!(sort!(mapped; alg = Base.Sort.QuickSort)) + f=t->begin + den=inv(one(R)-t) + integral.f(t*den)*den^2 + end + value, + error=if quadrature===nothing + quadgk(f, mapped; rtol = controls.rtol, atol = controls.atol, + maxevals = controls.maxevals, norm = numerical_magnitude) + else + # Public QuadGK evaluation creates reusable seeds in the same mapped + # coordinate as f. No dependency-private segment constructors are used. + seeds=empty!(quadrature.seeds) + sample=quadrature.prototype + for i in 1:(length(mapped) - 1) + quadgk(_->sample, mapped[i], mapped[i + 1]; + segbuf = quadrature.seed, norm = numerical_magnitude) + append!(seeds, quadrature.seed) + end + quadgk(f, zero(R), one(R); rtol = controls.rtol, atol = controls.atol, + maxevals = controls.maxevals, segbuf = quadrature.segments, + eval_segbuf = seeds, norm = numerical_magnitude) + end + isfinite(value) || throw(DomainError(value, "QuadGK returned a nonfinite integral")) + record_integral!(observations, quadrature === nothing ? nothing : quadrature.warnings, + value, error, controls, context) + return value, error +end + +# Actual and reused integrals report the same estimates and requested controls. +function record_integral!(observations, warnings, value, error, controls, context) + observations === nothing || + push!(observations, (; context, value, estimated_error = error)) + target=max(controls.atol, controls.rtol*numerical_magnitude(value)) + if !isfinite(error) || error>target + warnings === nothing || + push!(warnings, (; context, value, estimated_error = error, controls)) + @warn "QuadGK returned an estimated error above the requested target" value estimated_error=error target rtol=controls.rtol atol=controls.atol maxevals=controls.maxevals context + end + return nothing +end + +""" +$(TYPEDSIGNATURES) + +Normalize adaptive Gauss–Kronrod integration controls for `method=:quad`. +Accepted controls are `rtol`, `atol`, and `maxevals`. +""" +function formulation_options(::Type{SpectralIntegral}, values::NamedTuple) + isempty(setdiff(keys(values), (:method, :options))) || + throw(ArgumentError("integration accepts only method and options")) + method = get(values, :method, :quad) + method === :quad || + throw(ArgumentError("integration method must be :quad")) + supplied = get(values, :options, (;)) + supplied isa NamedTuple || + throw(ArgumentError("integration options must be a named tuple")) + defaults = (rtol = 1e-8, atol = 0.0, maxevals = 10^7) + unknown_controls = setdiff(keys(supplied), keys(defaults)) + isempty(unknown_controls) || + throw(ArgumentError("unknown :$method integration controls: $(collect(unknown_controls))")) + controls = merge(defaults, supplied) + for key in (:rtol, :atol) + value = getproperty(controls, key) + value isa Real && isfinite(value) && value >= 0 || + throw(ArgumentError("$key must be finite and nonnegative")) + end + controls.rtol > 0 || controls.atol > 0 || + throw(ArgumentError("at least one integration tolerance must be positive")) + for key in setdiff(keys(controls), (:rtol, :atol)) + value = getproperty(controls, key) + value isa Integer && !(value isa Bool) && value > 0 || + throw(ArgumentError("$key must be a positive integer")) + end + return (method = Val(method), options = controls) +end + +function formulation_options(::Expression, ::Val{:integration}, defaults::NamedTuple, + supplied::NamedTuple) + isempty(setdiff(keys(supplied), (:method, :options))) || + throw(ArgumentError("integration accepts only method and options")) + get(supplied, :options, (;)) isa NamedTuple || + throw(ArgumentError("integration options must be a NamedTuple")) + method = get(supplied, :method, defaults.method) + controls = merge(defaults.options, get(supplied, :options, (;))) + return formulation_options(SpectralIntegral, (; method, options = controls)) +end diff --git a/src/engine/interfaces.jl b/src/engine/interfaces.jl new file mode 100644 index 000000000..fa8a65390 --- /dev/null +++ b/src/engine/interfaces.jl @@ -0,0 +1,308 @@ +""" +Return the series-impedance values of a line-parameter result. +""" +function Z end + +""" +Return the shunt-admittance values of a line-parameter result. +""" +function Y end + +function X end +function G end +function B end +function series_impedance end +function shunt_admittance end +function reactance end +function conductance end +function susceptance end +function frequencies end +function nconductors end +function nfrequencies end +""" +Return absolute numerical errors from an owned comparison result. +""" +function absolute_error end +""" +Return reference-normalized numerical errors from an owned comparison result. +""" +function relative_error end + +""" +Return electrical conductivity \\[S/m\\] from resistivity \\[Ω·m\\], including +the open-circuit limit. +""" +@inline conductivity(rho) = isinf(rho) ? zero(rho) : inv(rho) + +""" +Return a real scalar magnitude for numerical error estimates and physical-state +comparisons. Deterministic values use absolute magnitude. Uncertainty extensions +also account for uncertain contributions with zero nominal value. Evaluated +physical quantities retain their correlations. +""" +@inline numerical_magnitude(z) = abs(complex(nominal(real(z)), nominal(imag(z)))) + +# Equality of physical state includes correlation, not just nominal values. +same_physical_state(a, b) = isequal(a, b) +same_physical_state(a::BigFloat, b::BigFloat) = isequal(a, b) +function same_physical_state(a::Number, b::Number) + a === b || (isequal(a, b) && iszero(numerical_magnitude(a-b))) +end +function same_physical_state(a::Tuple, b::Tuple) + # Tuple map preserves each field's type in heterogeneous physical records. + length(a) == length(b) && all(map(same_physical_state, a, b)) +end +function same_physical_state(a::NamedTuple, b::NamedTuple) + keys(a) == keys(b) && same_physical_state(values(a), values(b)) +end +function same_physical_state(a::AbstractArray, b::AbstractArray) + axes(a) == axes(b) && all(pair -> same_physical_state(pair...), zip(a, b)) +end + +""" +Resolve a placed conductor's earth-layer index from the problem and coordinates +[m], or read `(source, target)` indices from an initialized `EarthPair`. Air is +layer 1. Subsequent indices retain the physical earth model's layer order. +""" +function layer_index end + +""" +Resolve the real scalar representation needed by an active formulation before +allocating numerical storage. Arguments of active formulations may widen `T`. +Frequency-aligned arguments are +validated by their scientific owner against `frequencies`. +""" +computation_type(::Type{T}, ::AbstractFormulation, frequencies) where {T <: Real} = T +computation_type(::Type{T}, ::Nothing, frequencies) where {T <: Real} = T +# Each selection of a record, and of a recipe within it, can widen `T`. +function computation_type(::Type{T}, selections::NamedTuple, frequencies) where {T <: Real} + return foldl(values(selections); init = T) do scalar, selected + computation_type(scalar, selected, frequencies) + end +end + +"""Identify the local formula selections that determine blueprint coefficients.""" +function blueprint_dependencies end + +"""Calculate the selected local shunt response while constructing a blueprint.""" +function internal_shunt_response end +""" +$(TYPEDSIGNATURES) + +Calculate the selected earth contributions at one frequency from completed material +inputs: for every calculation of the earth plan, or for one calculation. Given a formula and +the Functor of its calculation, return the physical matrices of the calculation after its +parts, by quantity. A formula whose parts write the physical coefficients returns none. A +coupled formula converts its parts' coefficients for both quantities in one calculation. +Matrix rows are receivers and columns are sources. Impedance contributions are \\[Ω/m\\]. +Potential-coefficient contributions are \\[m/F\\]. +""" +function earth! end +function homogenize! end + +""" +$(TYPEDSIGNATURES) + +Evaluate the material quantities required by a coaxial computation into its +allocated buffers. Material-law methods receive the original material values. +Field equations consume these completed quantities without reevaluating them. +""" +function materials! end + +""" +Abstract tag for the physical domain represented by line-parameter matrices. +""" +abstract type LineParamsDomain end + +""" +$(TYPEDEF) + +Store one earth-return matrix interaction and its resolved physical layers. + +$(TYPEDFIELDS) +""" +struct EarthPair{T <: Real} + "Destination row." + row::Int + "Destination column." + column::Int + "Source and target heights relative to the air-earth interface, in that order \\[m\\]." + heights::Tuple{T, T} + "Horizontal distance between conductor centers \\[m\\]." + separation::T + "Physical layer indices of the source and target conductors." + layers::Tuple{Int, Int} + "Conductor outer radius for a self interaction [m]. Nothing for a mutual. " + radius::Union{Nothing, T} +end + +function EarthPair(row::Integer, column::Integer, heights::Tuple{T, T}, separation::T, + layers::Tuple{Int, Int}; radius = nothing) where {T <: Real} + return EarthPair{T}(row, column, heights, separation, layers, radius) +end + +""" +$(TYPEDSIGNATURES) + +Return `pair` on the earth that its calculation consumes. The layered earth model keeps the +physical layers. +""" +EarthPair(pair::EarthPair, ::EarthModel) = pair + +""" +$(TYPEDSIGNATURES) + +Return `pair` on an equivalent homogeneous earth: air stays layer 1, and every earth layer +becomes layer 2. +""" +function EarthPair(pair::EarthPair, ::EquivalentHomogeneous.AbstractSequence) + return EarthPair(pair.row, pair.column, pair.heights, pair.separation, + map(layer -> layer == 1 ? 1 : 2, pair.layers); radius = pair.radius) +end + +""" +$(TYPEDSIGNATURES) + +Check the resolved geometry of an earth-return interaction. + +# Arguments + +- `pair`: matrix indices and physical layer indices, with signed heights and + horizontal separation. An explicit self radius completes the geometry. + Lengths are in \\[m\\]. Layer 1 is air. + +# Returns + +- The same `pair`, without altering its geometry. + +# Errors + +- Throws `ArgumentError` for inconsistent indices or layer assignments. +- Throws `DomainError` for nonfinite lengths, nonpositive self radius, + coincident distinct conductors, or conductors on the air-earth interface. +""" +function validate(pair::EarthPair) + pair.row > 0 && pair.column > 0 || throw(ArgumentError( + "earth-pair matrix indices must be positive")) + all(>(0), pair.layers) || throw(ArgumentError( + "earth-pair physical layer indices must be positive; layer 1 is air")) + all(isfinite, pair.heights) && isfinite(pair.separation) || throw(DomainError( + (pair.heights, pair.separation), "earth-pair lengths must be finite")) + pair.separation >= zero(pair.separation) || throw(DomainError( + pair.separation, "earth-pair horizontal separation must be nonnegative")) + for index in eachindex(pair.heights) + height = pair.heights[index] + iszero(height) && throw(DomainError( + height, "a conductor on the air-earth interface has no physical layer")) + (height > zero(height)) == (pair.layers[index] == 1) || throw(ArgumentError( + "earth-pair conductor $index height and physical layer disagree; layer 1 is air")) + end + if pair.row == pair.column + pair.heights[1] == pair.heights[2] && pair.layers[1] == pair.layers[2] || + throw(ArgumentError("an earth self interaction must repeat the same conductor")) + iszero(pair.separation) || + throw(ArgumentError("a self interaction has zero horizontal separation")) + pair.radius !== nothing && isfinite(pair.radius) && pair.radius > 0 || + throw(DomainError(pair.radius, "a self interaction requires an explicit positive conductor radius")) + else + pair.radius === nothing || + throw(ArgumentError("a mutual interaction has no self radius")) + iszero(pair.separation) && pair.heights[1] == pair.heights[2] && + throw(DomainError((pair.heights, pair.separation), + "distinct earth-return conductors cannot have coincident centers")) + end + return pair +end + +""" +$(TYPEDSIGNATURES) + +Check conductor depths against their resolved horizontal earth layers. + +# Arguments + +- `pair`: resolved earth-return geometry. +- `thickness`: layer thicknesses \\[m\\], including semi-infinite air first. + +# Returns + +- The same `pair`. + +# Errors + +- Throws `ArgumentError` when a layer index or conductor depth disagrees with + the supplied horizontal layer inventory. +""" +function validate(pair::EarthPair, thickness::Union{Tuple, AbstractVector}) + validate(pair) + for position in eachindex(pair.layers) + layer = pair.layers[position] + layer <= length(thickness) || throw(ArgumentError( + "earth-pair conductor $position refers to absent physical layer $layer")) + layer == 1 && continue + depth = -pair.heights[position] + top = sum((thickness[index] for index in 2:(layer - 1)); init = zero(depth)) + local_depth = depth - top + local_depth >= zero(depth) && + (!isfinite(thickness[layer]) || local_depth <= thickness[layer]) || + throw(ArgumentError( + "earth-pair conductor $position depth $depth m is outside its resolved earth layer $layer")) + end + return pair +end + +""" +Tag line parameters expressed in the physical phase domain. +""" +struct PhaseDomain <: LineParamsDomain end +""" +Supertype of the modal-to-phase voltage and current bases of a modal transformation. +A concrete subtype implements `Base.size`, which returns the +`(modes, modes, frequencies)` dimensions of its operator tensors. +""" +abstract type AbstractModalOperators end + +""" +Store the coordinate system of a calculated modal transformation. + +The operator tensor type parameterizes the domain because inverse transforms +consume it numerically. The defining transform module may use one formula-family +parameter to record the selected formula, so different concrete formula identities can +share one concrete element type for the result space. +""" +struct ModalDomain{O, G} <: LineParamsDomain + "Frequency-dependent modal-to-phase voltage and current bases." + operators::O + "Aligned propagation roots in the coefficient basis." + gamma::G + + function ModalDomain(operators::O, gamma::G) where {O, G} + return validate(new{O, G}(operators, gamma)) + end +end + +function validate(domain::ModalDomain) + domain.gamma isa AbstractMatrix || throw(DimensionMismatch( + "modal roots must be a mode×frequency matrix")) + domain.operators isa AbstractModalOperators || throw(ArgumentError( + "modal domain requires a validated ModalOperators value")) + dimensions=size(domain.operators) + size(domain.gamma)==(dimensions[2], dimensions[3]) || throw(DimensionMismatch( + "modal roots must align with operator modes and frequency samples")) + return domain +end + +""" +Return a domain value restricted to selected frequency samples. +""" +selectdomain(domain::LineParamsDomain, _) = domain +selectdetails(details, ::LineParamsDomain, _) = details + +@inline domain(::Type{PhaseDomain}) = PhaseDomain +@inline domain(::Type{<:ModalDomain}) = ModalDomain + +""" +Return the domain tag type of a value, or `nothing` when it has no domain. +""" +@inline domain(::Type) = nothing +@inline domain(value) = domain(typeof(value)) diff --git a/src/engine/internalimpedance/InternalImpedance.jl b/src/engine/internalimpedance/InternalImpedance.jl index 6d839dd86..4b49552e7 100644 --- a/src/engine/internalimpedance/InternalImpedance.jl +++ b/src/engine/internalimpedance/InternalImpedance.jl @@ -1,29 +1,46 @@ """ - LineCableModels.Engine.InternalImpedance + LineCableModels.Engine.InternalImpedance + +Define conductor internal-impedance recipes and their electromagnetic +interaction formulas. # Dependencies $(IMPORTS) -# Exports - -$(EXPORTS) """ module InternalImpedance +import ...Commons: FormulationOptions, Functor, formulas # Export public API -export ScaledBessel +export Formula, formula_id, formulas, internal_impedance, surface_impedances # Module-specific dependencies -using ...Commons -import ...Commons: get_description -import ..Engine: InternalImpedanceFormulation -using Measurements -using LinearAlgebra -using ...UncertainBessels: besselix, besselkx -using ...Utils: _to_σ +#! explicit-imports: off +# These abbreviations are expanded in this module docstring and included files. +using DocStringExtensions: IMPORTS, TYPEDEF, TYPEDFIELDS, TYPEDSIGNATURES +#! explicit-imports: on +import ..Engine: InternalImpedanceFormulation, formula_id, formulation_options +#! explicit-imports: off +import ...LineCableModels: FormulaDefinition, Expression +import ..Engine: description, conductivity +import ..Engine: special_besselix, special_besselkx +using ...Commons: vacuum_permeability +#! explicit-imports: on + +include("interface.jl") + +#! explicit-imports: off +const FORMULAS = ( + include("formulas/default.jl"), + include("formulas/schelkunoff1934.jl"), + include("formulas/wedepohl1973.jl"), +) +#! explicit-imports: on -include("scaledbessel.jl") +""" +Return the built-in internal-impedance formula identifiers. +""" +formulas(::Type{<:Formula}) = FORMULAS end # module InternalImpedance - diff --git a/src/engine/internalimpedance/formulas/default.jl b/src/engine/internalimpedance/formulas/default.jl new file mode 100644 index 000000000..87ebd8229 --- /dev/null +++ b/src/engine/internalimpedance/formulas/default.jl @@ -0,0 +1,12 @@ +""" +$(TYPEDSIGNATURES) + +Route the default selection to the explicit `:schelkunoff1934` implementation. +No numerical equation is owned by `:default`. +""" +description(::Type{<:Formula{:default}}; compact::Bool=false) = + compact ? "Default" : "Default routing to :schelkunoff1934" + +Formula{:default}(; kwargs...) = Formula{:schelkunoff1934}(; kwargs...) + +:default diff --git a/src/engine/internalimpedance/formulas/schelkunoff1934.jl b/src/engine/internalimpedance/formulas/schelkunoff1934.jl new file mode 100644 index 000000000..48a04bcc0 --- /dev/null +++ b/src/engine/internalimpedance/formulas/schelkunoff1934.jl @@ -0,0 +1,187 @@ + +""" +$(TYPEDSIGNATURES) + +**Identification.** Exact cylindrical surface impedances for a solid or +hollow round conductor. + +**Expression.** + +```math +\\begin{aligned} +Z_{is}&=\\frac{\\rho m}{2\\pi aD} +[I_0(ma)K_1(mb)+K_0(ma)I_1(mb)],\\\\ +Z_{os}&=\\frac{\\rho m}{2\\pi bD} +[I_0(mb)K_1(ma)+K_0(mb)I_1(ma)],\\\\ +Z_{ms}&=\\frac{\\rho m}{2\\pi abD},\\\\ +D&=I_1(mb)K_1(ma)-K_1(mb)I_1(ma). +\\end{aligned} +``` + +Here ``a`` and ``b`` are the inner and outer conductor radii in meters, +``ρ`` is resistivity in Ω·m, and ``m=\\sqrt{jωμ/ρ}`` is in m⁻¹, with +``μ=μ_0μ_r`` in H/m. ``I_ν`` and ``K_ν`` are modified Bessel functions. +Each surface impedance is in Ω/m. + +For ``a=0``, ``Z_{int}=\\rho mI_0(mb)/(2\\pi bI_1(mb))``. + +Schelkunoff's surface terms were later recovered by Ametani to assemble the +complete core-sheath-armor impedance matrix. The Engine applies that outward +assembly recursively to any number of concentric conductive terminals. + +**Reference.** S. A. Schelkunoff, “The Electromagnetic Theory of Coaxial +Transmission Lines and Cylindrical Shields,” *Bell System Technical Journal*, +13, 532–579, 1934. A. Ametani, “A General Formulation of Impedance and +Admittance of Cables,” *IEEE Transactions on Power Apparatus and Systems*, +PAS-99(3), 902–910, 1980. DOI: 10.1109/TPAS.1980.319718. + +""" +function description(::Type{<:Formula{:schelkunoff1934}}; compact::Bool=false) + compact ? "Schelkunoff" : "Schelkunoff exact round-conductor surface impedances (1934)" +end + +""" +$(TYPEDSIGNATURES) + +Build the Functor of one solid or hollow circular conductor at one frequency for the exact +Schelkunoff surface impedances: + +```math +Z_{is}=\\frac{\\rho m}{2\\pi aD} +\\left[I_0(ma)K_1(mb)+K_0(ma)I_1(mb)\\right], +\\qquad +Z_{ms}=\\frac{\\rho m}{2\\pi abD}, +``` + +```math +Z_{os}=\\frac{\\rho m}{2\\pi bD} +\\left[I_0(mb)K_1(ma)+K_0(mb)I_1(ma)\\right], +``` + +where + +```math +D=I_1(mb)K_1(ma)-K_1(mb)I_1(ma), +\\qquad +m=\\sqrt{j\\omega\\mu/\\rho}. +``` + +Here ``μ=μ_0μ_r`` is absolute permeability in H/m, and ``I_ν`` and ``K_ν`` +are modified Bessel functions. For ``a=0``, the outer term is evaluated from the solid-cylinder limit +``Z_{int}=\\rho m I_0(mb)/(2\\pi b I_1(mb))``. + +# Input + +- `r_in`: inner conductor radius ``a`` \\[m\\]. +- `r_ex`: outer conductor radius ``b`` \\[m\\]. +- `rho`: conductor resistivity ``\\rho`` \\[Ω·m\\]. +- `mu_r`: relative conductor permeability \\[dimensionless\\]. +- `jω`: complex angular frequency ``j\\omega`` \\[rad/s\\]. + +# Returns + +- The Functor of the conductor at that frequency. Its state stores the scaled modified + Bessel functions ``I_0``, ``I_1``, ``K_0`` and ``K_1`` at ``ma`` and ``mb``, evaluated once + for the inner, outer and transfer surfaces. A solid conductor evaluates only ``I_0(mb)`` + and ``I_1(mb)``. + +# Notes + +Implements Schelkunoff's cylindrical surface terms. Ametani (1980) recovered +these terms for the complete core-sheath-armor impedance assembly performed +recursively by the Engine. +""" +function Functor(formula::Formula{:schelkunoff1934}, input::NamedTuple; workspace = nothing) + (; r_in, r_ex, rho, mu_r, jω) = input + T = typeof(r_in) + isfinite(r_in) && isfinite(r_ex) && zero(T) <= r_in < r_ex || + throw(DomainError((r_in, r_ex), "conductor radii must satisfy 0 ≤ r_in < r_ex [m]")) + isfinite(rho) && rho > zero(T) && isfinite(mu_r) && mu_r > zero(T) || + throw(DomainError((rho, mu_r), "conductor resistivity and permeability must be positive and finite")) + isfinite(jω) && !iszero(jω) || throw(DomainError(jω, "jω must be finite and nonzero")) + mu_c = vacuum_permeability(T) * mu_r + sigma_c = conductivity(rho) + m = sqrt(jω * mu_c * sigma_c) + w_ex = m * r_ex + w_in = m * r_in + i0_ex = special_besselix(0, w_ex) + i1_ex = special_besselix(1, w_ex) + # A solid conductor has no inner surface, and K at zero would be infinite. Its values + # that the outer surface does not read are zero, so the state type does not depend on + # the geometry. + absent = zero(i0_ex) + sc_ex, sc, i0_in, i1_in, k0_in, k1_in, k0_ex, k1_ex = + if isapprox(r_in, zero(T); atol = eps(T)) + ntuple(_ -> absent, 8) + else + sc_in = exp(abs(real(w_in)) - w_ex) + sc_out = exp(abs(real(w_ex)) - w_in) + (sc_out, sc_in / sc_out, special_besselix(0, w_in), special_besselix(1, w_in), + special_besselkx(0, w_in), special_besselkx(1, w_in), + special_besselkx(0, w_ex), special_besselkx(1, w_ex)) + end + state = (; mu_c, sigma_c, m, w_in, w_ex, sc_ex, sc, + i0_in, i1_in, k0_in, k1_in, i0_ex, i1_ex, k0_ex, k1_ex) + return Functor(formula, input, state) +end + +@inline function internal_impedance( + ::Formula{:schelkunoff1934}, + ::Val{:inner}, + functor, workspace +) + input, state = functor.input, functor.state + T = typeof(input.r_in) + if isapprox(input.r_in, zero(T); atol = eps(T)) + return zero(Complex{T}) + end + + numerator = state.k0_in * state.i1_ex + state.sc * state.i0_in * state.k1_ex + denominator = state.k1_in * state.i1_ex - state.sc * state.i1_in * state.k1_ex + return Complex{T}( + (input.jω * state.mu_c / 2π) * (1 / state.w_in) * (numerator / denominator) + ) +end + +@inline function internal_impedance( + ::Formula{:schelkunoff1934}, + ::Val{:outer}, + functor, workspace +) + input, state = functor.input, functor.state + T = typeof(input.r_in) + if isapprox(input.r_in, zero(T); atol = eps(T)) + numerator = state.i0_ex + denominator = state.i1_ex + else + numerator = state.i0_ex * state.k1_in + state.sc * state.k0_ex * state.i1_in + denominator = state.i1_ex * state.k1_in - state.sc * state.k1_ex * state.i1_in + end + return Complex{T}( + (input.jω * state.mu_c / 2π) * (1 / state.w_ex) * + (numerator / denominator) + ) +end + +@inline function internal_impedance( + ::Formula{:schelkunoff1934}, + ::Val{:transfer}, + functor, workspace +) + input, state = functor.input, functor.state + T = typeof(input.r_in) + if isapprox(input.r_in, zero(T); atol = eps(T)) + return zero(Complex{T}) + end + + numerator = one(state.sc_ex) / state.sc_ex + denominator = state.i1_ex * state.k1_in - state.sc * state.i1_in * state.k1_ex + return Complex{T}( + (1 / (2π * input.r_in * input.r_ex * state.sigma_c)) * + (numerator / denominator) + ) +end + +formulation_options(::Expression{<:Formula{:schelkunoff1934}, typeof(internal_impedance)}) = FormulationOptions() + +:schelkunoff1934 diff --git a/src/engine/internalimpedance/formulas/wedepohl1973.jl b/src/engine/internalimpedance/formulas/wedepohl1973.jl new file mode 100644 index 000000000..0f6181771 --- /dev/null +++ b/src/engine/internalimpedance/formulas/wedepohl1973.jl @@ -0,0 +1,25 @@ +""" +$(TYPEDSIGNATURES) + +Identify the Wedepohl-Wilcox analytical approximations for the inner, outer, +and transfer surface impedances of a round conductor, in Ω/m. A solid cylinder +requires only the outer coefficient. An annulus uses all three coefficients. + +This scientific identity is registered for backend dispatch. The owned coaxial backend has +no expression for it, so selecting it there fails before evaluation. +PSCAD's line-constants program uses these approximations for conductor surfaces. + +Reference: L. M. Wedepohl and D. J. Wilcox, “Transient Analysis of Underground +Power-Transmission Systems: System-Model and Wave-Propagation Characteristics,” +*Proceedings of the IEE*, 120, 253–260, 1973. DOI: 10.1049/piee.1973.0056. +PSCAD 5.1 help reproduces the surface approximations in *Deriving System Y and Z +Matrices*, Eqs. (8-8), (8-9), and (8-11)-(8-13). +""" +function description(::Type{<:Formula{:wedepohl1973}}; compact::Bool = false) + compact ? "Wedepohl" : "Wedepohl-Wilcox round-conductor surface impedances (1973)" +end + +formulation_options(::Expression{<:Formula{:wedepohl1973}, typeof(internal_impedance)}) = + FormulationOptions() + +:wedepohl1973 diff --git a/src/engine/internalimpedance/interface.jl b/src/engine/internalimpedance/interface.jl new file mode 100644 index 000000000..03590e9a1 --- /dev/null +++ b/src/engine/internalimpedance/interface.jl @@ -0,0 +1,117 @@ +""" +$(TYPEDEF) + +Select cylindrical surface equations and their model and numerical controls. +The actual conductor geometry determines which surfaces are required. + +$(TYPEDFIELDS) +""" +struct Formula{ID, P <: NamedTuple, O <: FormulationOptions} <: InternalImpedanceFormulation + "Resolved physical model parameters." + parameters::P + "Normalized numerical sections indexed by surface kind." + options::O +end + +"""Evaluate a selected cylindrical surface coefficient in Ω/m.""" +function internal_impedance end + +"""Evaluate required cylindrical surface impedances in Ω/m.""" +function surface_impedances end + +""" +$(TYPEDSIGNATURES) + +Construct an internal-impedance formulation. Numerical controls are projected +onto its surface equations. Unknown numerical sections are rejected. +A custom formulation subtypes `InternalImpedanceFormulation` and extends +`internal_impedance` on its own type. Its `Functor` method builds the values shared by its +surface impedances. +""" +function Formula{ID}(; parameters::NamedTuple=(;), options::Union{NamedTuple, FormulationOptions} = FormulationOptions()) where {ID} + ID in formulas(Formula) || throw(ArgumentError("unknown internal-impedance formula :$ID")) + options = options isa NamedTuple ? FormulationOptions(options) : options + isempty(parameters) || throw(ArgumentError("internal impedance :$ID has no model parameters")) + kinds = (:inner, :outer, :transfer) + selected = Formula{ID, typeof(parameters), typeof(options)}(parameters, options) + expressions = map(kind -> Expression(selected, internal_impedance, Val(kind)), kinds) + projected = formulation_options(selected, expressions) + normalized = FormulationOptions(NamedTuple{kinds}(map(expressions) do expression + projected.options[findfirst(==(expression), projected.expressions)].data + end)) + return Formula{ID, typeof(parameters), typeof(normalized)}(parameters, normalized) +end + +""" +$(TYPEDSIGNATURES) + +Evaluate cylindrical surface coefficients in Ω/m. The wall operator is +`[inner transfer; transfer outer]` in the surface-current basis +`(-enclosed axial current, total axial current including the wall)`. +Assemblers own basis transformation and matrix placement. + +# Arguments + +- `formula`: the selected formulation. +- `r_in`, `r_ex`: inner and outer conductor radii \\[m\\]. +- `rho`: conductor resistivity \\[Ω·m\\]. +- `mu_r`: relative permeability \\[dimensionless\\]. +- `jω`: imaginary angular frequency \\[1/s\\]. +- `workspace`: optional computation workspace passed to each selected equation. + +# Returns + +- NamedTuple of `outer` for a solid primitive, or `inner`, `outer`, and + `transfer` for a tubular primitive \\[Ω/m\\]. +""" +function surface_impedances(formula::InternalImpedanceFormulation, r_in, r_ex, rho, mu_r, jω; + workspace=nothing) + return surface_impedances(formula, + r_in > 0 ? Val((:inner,:outer,:transfer)) : Val((:outer,)), + r_in, r_ex, rho, mu_r, jω; workspace) +end + +""" +Evaluate the surface impedances that `Kinds` names. The formula's `Functor` stores the values +shared by its surface impedances, built once per conductor and frequency. Each surface +impedance evaluates with the options of its own section. +""" +@inline function surface_impedances(formula::InternalImpedanceFormulation, ::Val{Kinds}, + r_in, r_ex, rho, mu_r, jω; workspace=nothing) where {Kinds} + length(Kinds) in (1, 3) || + throw(ArgumentError("internal surfaces require one or three kinds")) + functor = Functor(formula, (; r_in, r_ex, rho, mu_r, jω); workspace) + values = map(map(Val, Kinds)) do kind + options = formulation_options(formula, kind) + value = Expression(formula, internal_impedance, kind)( + Functor(functor, (; options)), workspace) + value isa Number && isfinite(value) || throw(DomainError(value, + "internal_impedance must return a finite surface coefficient [Ω/m]")) + value + end + return NamedTuple{Kinds}(values) +end + +Formula(identifier::Symbol; kwargs...) = Formula(Val(identifier); kwargs...) +Formula(::Val{ID}; kwargs...) where {ID} = Formula{ID}(; kwargs...) +Formula(selected::InternalImpedanceFormulation) = selected +Formula(::Nothing) = Formula(:default) + +function Formula(selection::FormulaDefinition{ID, Order}) where {ID, Order} + Order === :default || throw(ArgumentError("order applies only to equivalent_earth")) + selection.equivalent_earth === nothing || throw(ArgumentError( + "equivalent_earth applies only to external earth formulas")) + return Formula{ID}(; parameters=selection.parameters, options=selection.options) +end + +"""Return the stable identifier of a selected internal-impedance formulation.""" +formula_id(::Formula{ID}) where {ID} = ID +formula_id(::Type{<:Formula{ID}}) where {ID} = ID +description(value::Formula; compact::Bool=false) = description(typeof(value); compact) +formulation_options(value::Formula) = value.options + +"""Expose the selected identity, model parameters and numerical controls.""" +Base.NamedTuple(value::Formula) = (identifier=formula_id(value), + parameters=value.parameters, options=value.options.data) + +Base.pairs(::Type{<:Formula}; quantity=nothing) = pairs((;)) diff --git a/src/engine/internalimpedance/scaledbessel.jl b/src/engine/internalimpedance/scaledbessel.jl deleted file mode 100644 index 8dafd7171..000000000 --- a/src/engine/internalimpedance/scaledbessel.jl +++ /dev/null @@ -1,157 +0,0 @@ - -struct ScaledBessel <: InternalImpedanceFormulation end -get_description(::ScaledBessel) = "Scaled Bessel (Schelkunoff)" - - -@inline function (f::ScaledBessel)( - form::Symbol, - r_in::T, - r_ex::T, - rho_c::T, - mur_c::T, - jω::Complex{T}, -) where {T <: REALSCALAR} - Base.@nospecialize form - return form === :inner ? f(Val(:inner), r_in, r_ex, rho_c, mur_c, jω) : - form === :outer ? f(Val(:outer), r_in, r_ex, rho_c, mur_c, jω) : - form === :mutual ? f(Val(:mutual), r_in, r_ex, rho_c, mur_c, jω) : - throw(ArgumentError("Unknown ScaledBessel form: $form")) -end - -@inline function (f::ScaledBessel)( - ::Val{:inner}, - r_in::T, - r_ex::T, - rho_c::T, - mur_c::T, - jω::Complex{T}, -) where {T <: REALSCALAR} - # Constants - mu_c = T(μ₀) * mur_c - sigma_c = _to_σ(rho_c) - - # Calculate the reciprocal of the skin depth - m = sqrt(jω * mu_c * sigma_c) - w_ex = m * r_ex - - if isapprox(r_in, 0.0, atol = eps(T)) - return zero(Complex{T}) # not physical, but consistent with :outer - algorithmic shortcut for solids/tubular blending - else - - w_in = m * r_in - - sc_in = exp(abs(real(w_in)) - w_ex) - sc_ex = exp(abs(real(w_ex)) - w_in) - sc = sc_in / sc_ex - - # Bessel function terms with uncertainty handling - N = - (besselkx(0, w_in)) * - (besselix(1, w_ex)) + - sc * - (besselix(0, w_in)) * - (besselkx(1, w_ex)) - - D = - (besselkx(1, w_in)) * - (besselix(1, w_ex)) - - sc * - (besselix(1, w_in)) * - (besselkx(1, w_ex)) - - return Complex{T}((jω * mu_c / 2π) * (1 / w_in) * (N / D)) - end - -end - -@inline function (f::ScaledBessel)( - ::Val{:outer}, - r_in::T, - r_ex::T, - rho_c::T, - mur_c::T, - jω::Complex{T}, -) where {T <: REALSCALAR} - # Constants - mu_c = T(μ₀) * mur_c - sigma_c = _to_σ(rho_c) - - # Calculate the reciprocal of the skin depth - m = sqrt(jω * mu_c * sigma_c) - w_ex = m * r_ex - - if isapprox(r_in, 0.0, atol = eps(T)) # solid conductor - @debug "Using closed form for solid conductor" - N = besselix(0, w_ex) - D = besselix(1, w_ex) - - else - w_in = m * r_in - - sc_in = exp(abs(real(w_in)) - w_ex) - sc_ex = exp(abs(real(w_ex)) - w_in) - sc = sc_in / sc_ex - - # Bessel function terms with uncertainty handling - N = - (besselix(0, w_ex)) * - (besselkx(1, w_in)) + - sc * - (besselkx(0, w_ex)) * - (besselix(1, w_in)) - - D = - (besselix(1, w_ex)) * - (besselkx(1, w_in)) - - sc * - (besselkx(1, w_ex)) * - (besselix(1, w_in)) - end - - return Complex{T}((jω * mu_c / 2π) * (1 / w_ex) * (N / D)) -end - -@inline function (f::ScaledBessel)( - ::Val{:mutual}, - r_in::T, - r_ex::T, - rho_c::T, - mur_c::T, - jω::Complex{T}, -) where {T <: REALSCALAR} - # Constants - mu_c = T(μ₀) * mur_c - sigma_c = _to_σ(rho_c) - - # Calculate the reciprocal of the skin depth - m = sqrt(jω * mu_c * sigma_c) - w_ex = m * r_ex - - if isapprox(r_in, 0.0, atol = eps(T)) - - return zero(Complex{T}) # not physical, but consistent with :outer - algorithmic shortcut for solids/tubular blending - # return f(Val(:outer), r_in, r_ex, rho_c, mur_c, freq) - - else - - w_in = m * r_in - - sc_in = exp(abs(real(w_in)) - w_ex) - sc_ex = exp(abs(real(w_ex)) - w_in) - sc = sc_in / sc_ex - - # Bessel function terms with uncertainty handling - N = 1.0 / sc_ex - - D = - (besselix(1, w_ex)) * - (besselkx(1, w_in)) - - sc * - (besselix(1, w_in)) * - (besselkx(1, w_ex)) - - return Complex{T}((1 / (2π * r_in * r_ex * sigma_c)) * (N / D)) - - end - -end diff --git a/src/engine/lineparameters.jl b/src/engine/lineparameters.jl new file mode 100644 index 000000000..9884af0ce --- /dev/null +++ b/src/engine/lineparameters.jl @@ -0,0 +1,524 @@ +# LineParameters computation remains independent from CableConstants. + +@inline _stash!(::Nothing, ::Symbol, ::Int, ::AbstractMatrix) = nothing + +@inline function _stash!(trace::NamedTuple, name::Symbol, frequency::Int, source::AbstractMatrix) + destination = getproperty(trace, name) + @views copyto!(destination[:, :, frequency], source) + return nothing +end + +function _solve!( + workspace::LineParametersWorkspace{T}, + formulation::LineParametersFormulation, + calculations::Tuple = workspace.plan.earth.calculations, + materials::Tuple = workspace.buffers.earth.calculations +) where {T <: Real} + input = workspace.input + plan = workspace.plan + buffers = workspace.buffers + workspace.trace===nothing || empty!(workspace.trace.integrals) + Zprimitive = buffers.Zprimitive + Pprimitive = buffers.Pprimitive + Zout = buffers.Zout + Yout = buffers.Yout + + materials!(workspace, formulation) + @debug "Starting line parameters computation" + for frequency in 1:input.n_frequencies + materials!(workspace, formulation, frequency, calculations, materials) + cable_impedance!(Zprimitive, input.cable, buffers.rho_cond, + formulation.methods, input.jω[frequency]; workspace) + cable_potential!(Pprimitive, input.cable, buffers.dielectric_admittivity, + input.jω[frequency], buffers.layer_coefficients, buffers.coefficients, buffers.tails) + _stash!(workspace.trace, :Zin, frequency, Zprimitive) + _stash!(workspace.trace, :Pin, frequency, Pprimitive) + earth!(workspace, frequency, calculations, materials) + _stash!(workspace.trace, :Zg, frequency, buffers.Zearth) + _stash!(workspace.trace, :Pg, frequency, buffers.Pearth) + impedance!(Zprimitive, workspace, frequency) + admittance!(Pprimitive, workspace, frequency) + reduce_line_matrices!(view(Zout, :, :, frequency), view(Yout, :, :, frequency), + Zprimitive, Pprimitive, input.jω[frequency], plan.reduction, buffers.reduction) + end + + return workspace +end + +function _retained_details(workspace::LineParametersWorkspace{ + <:Real, <:NamedTuple, <:NamedTuple, + <:NamedTuple, Nothing}) + # Local model diagnostics vary with geometry and selection, not the result type. + ComputationDetails(NamedTuple{(:shunt_model,), Tuple{NamedTuple}}((workspace.input.cable.shunt_details,))) +end + +function _retained_details(workspace::LineParametersWorkspace) + trace = workspace.trace + shunt = NamedTuple{(:shunt_model,), Tuple{NamedTuple}}((workspace.input.cable.shunt_details,)) + trace === nothing && return ComputationDetails(shunt) + input = workspace.input + return ComputationDetails(merge(shunt, + ( + trace = ( + phase_map = copy(input.phase_map), + cable_map = copy(input.cable_map), + Zin = copy(trace.Zin), + Pin = copy(trace.Pin), + Zg = copy(trace.Zg), + Pg = copy(trace.Pg), + Z = copy(trace.Z), + P = copy(trace.P), + integrals = copy(trace.integrals) + ), + ))) +end + +function _finish( + workspace::LineParametersWorkspace, + problem::LineParametersProblem, + formulation::LineParametersFormulation, + ::Val{Basis}; + physical_inputs=completed_inputs(problem), + gridpoint=Commons.gridpoint_id() +) where {Basis} + impedance = copy(workspace.buffers.Zout) + admittance = copy(workspace.buffers.Yout) + if Basis === :total + impedance .*= workspace.input.line_length + admittance .*= workspace.input.line_length + end + all(isfinite, impedance) || throw(DomainError(impedance, + "completed series impedance must contain only finite entries")) + all(isfinite, admittance) || throw(DomainError(admittance, + "completed shunt admittance must contain only finite entries")) + retained = _retained_details(workspace) + names=["cable:$(terminal.cable):$(terminal.terminal)" + for terminal in problem.system.terminal_order] + coordinates=map(workspace.plan.reduction.indices) do index + phase=problem.system.connection_order[index] + members=findall(==(phase), problem.system.connection_order) + formulation.options.data.reduce_bundle && phase > 0 && length(members) > 1 ? + "bundle:[" * join(names[members], ",") * "]" : names[index] + end + result = LineParameters(PhaseDomain, + SeriesImpedance{eltype(impedance), Basis}(impedance), + ShuntAdmittance{eltype(admittance), Basis}(admittance), + workspace.input.freq, + completion_details(merge(retained.data, + completed_formulation(formulation, workspace), + NamedTuple{(:inputs,:gridpoint,:coordinates),Tuple{NamedTuple,NamedTuple,Vector{String}}}( + (physical_inputs,gridpoint,coordinates))))) + return result +end + +function _compute( + engine::LineCableModelsCoaxial, + problem::LineParametersProblem, + formulation::LineParametersFormulation, + execution::ComputationOptions, + input::NamedTuple, + physical_inputs::NamedTuple, + gridpoint::NamedTuple +) + workspace = LineParametersWorkspace(problem, formulation, execution, input) + _solve!(workspace, formulation) + return _finish(workspace, problem, formulation, execution.data.output_basis; + physical_inputs, gridpoint) +end + +function _compute( + engine::LineCableModelsCoaxial, + problem::LineParametersProblem, + formulation::LineParametersFormulation, + execution::ComputationOptions, + timing::Val +) + values = _compute( + engine, + problem, + typeof(formulation)[formulation], + execution, + timing + ) + return first(values) +end + +function _compute( + engine::LineCableModelsCoaxial, + problem::LineParametersProblem, + formulations::AbstractVector{<:LineParametersFormulation}, + execution::ComputationOptions, + timing::Val +) + isempty(formulations) && throw(ArgumentError( + "line-parameter formulation collections cannot be empty", + )) + progress = verbosity(execution, :progress) > 0 + started = progress ? time_ns() : UInt64(0) + last_log = started + previous_completion = started + average_seconds = 0.0 + progress && @info "Line parameters computation started" _group=:progress total=length(formulations) + validate(problem) + for design in problem.system.designs, formulation in formulations + + validate(design, formulation.methods.pipe_impedance, engine) + end + maximum(problem.frequencies) > oftype(first(problem.frequencies), 1e8) && + @warn("Frequencies above 100 MHz exceed the quasi-TEM validity range.", + max_frequency=maximum(problem.frequencies),) + physical_inputs = completed_inputs(problem) + source_id = Commons.gridpoint_id().source_id + T = eltype(problem) + blueprints = flatten(engine, problem.system.designs, T, formulations) + inputs = [lineinput(problem, first(blueprints))] + for index in 2:length(blueprints) + previous = findfirst(other -> other === blueprints[index], blueprints) + push!(inputs, previous < index ? inputs[previous] : + lineinput(problem, blueprints[index])) + end + values = map(formulations, inputs, eachindex(formulations)) do formulation, input, index + gridpoint = Commons.gridpoint_id(; source_id, formulation_index=index) + # Measure a full scan from workspace creation through the solve and result validation. + # Shared input and workspace construction, attachment, callbacks and progress are excluded. + value = if timing isa Val{true} + measured = Base.@timed _compute(engine, problem, formulation, execution, + input, physical_inputs, gridpoint) + retain_gridpoint(measured.value, gridpoint; fields=(timing=( + wall_seconds=measured.time, bytes=measured.bytes, + gc_seconds=measured.gctime, compile_seconds=measured.compile_time, + recompile_seconds=measured.recompile_time),)) + else + _compute( + engine, + problem, + formulation, + execution, + input, + physical_inputs, + gridpoint + ) + end + execution.data.on_result === nothing || + execution.data.on_result(problem, index, value) + if progress + now = time_ns() + interval = (now - previous_completion) * 1e-9 + average_seconds = index == 1 ? interval : 0.2 * interval + 0.8 * average_seconds + previous_completion = now + if now - last_log >= 5_000_000_000 + @info "Line parameters progress" _group=:progress completed=index total=length(formulations) elapsed_seconds=(now-started)*1e-9 eta_hours=(length(formulations)-index)*average_seconds/3600 + last_log = now + end + end + value + end + progress && @info "Line parameters computation completed successfully" _group=:progress completed=length(values) total=length(formulations) elapsed_seconds=(time_ns()-started)*1e-9 + return values +end + +""" +$(TYPEDSIGNATURES) + +Compute line parameters with the coaxial backend and default formulation. + +# Arguments + +- `problem`: completed line-parameter problem. + +# Keywords + +- `options`: coaxial-backend computation options. + +# Returns + +- One [`LineParameters`](@ref) result. +""" +Base.@constprop :aggressive function compute( + problem::LineParametersProblem; + options::Union{NamedTuple, ComputationOptions} = ComputationOptions(), + modal=nothing, modal_options::Union{NamedTuple, ComputationOptions}=ComputationOptions() +) + options = options isa NamedTuple ? ComputationOptions(options) : options + modal===nothing || return compute(problem,Formulation(), + ModalAnalysis.ModalAnalysisFormulation(modal);options,modal_options) + isempty(modal_options isa NamedTuple ? modal_options : modal_options.data) || + throw(ArgumentError("modal_options require a modal formulation")) + return compute(LineCableModelsCoaxial(), problem, Formulation(); options) +end + +""" +$(TYPEDSIGNATURES) + +Compute frequency-dependent line parameters with the coaxial backend. + +The completed data model supplies the equivalent concentric representation +used for series impedance and ordinary radial dielectric intervals. Eligible +open wire and tape domains retain their physical geometry for the explicitly selected +`shunt_model=:boundary` calculation. The default uses annular geometry. The +physical system is normalized once into a backend-owned +workspace, and all reusable numerical storage is allocated before the frequency +loop. `trace=true` copies `Zin`, `Pin`, `Zg`, `Pg`, `Z`, `P`, `phase_map`, +`cable_map` and the integration records into `details(result).data.trace`. +These arrays remain available after the workspace is reused. The result type +remains unchanged. + +# Arguments + +- `problem`: completed line-parameter problem. +- `formulation`: selected line-parameter physical methods. + +# Keywords + +- `options`: named tuple containing `verbosity`, `output_basis`, `trace`, and + `on_result`. The optional callable `on_result(problem, index, result)` runs + synchronously after each completed formulation and + before the next computation. `index` is local to the formulation collection + (`1` for a scalar call). Its return value is ignored. Exceptions propagate. + The callback must not mutate the problem or result. The default is `nothing`. + The compute call constructs the selected local shunt coefficients in the + cable blueprints without an additional execution option. + +# Returns + +- One [`LineParameters`](@ref) result. +""" +Base.@constprop :aggressive function compute( + problem::LineParametersProblem, + formulation::LineParametersFormulation; + options::Union{NamedTuple, ComputationOptions} = ComputationOptions(), + modal=nothing, modal_options::Union{NamedTuple, ComputationOptions}=ComputationOptions() +) + options = options isa NamedTuple ? ComputationOptions(options) : options + modal===nothing || return compute(problem,formulation, + ModalAnalysis.ModalAnalysisFormulation(modal);options,modal_options) + isempty(modal_options isa NamedTuple ? modal_options : modal_options.data) || + throw(ArgumentError("modal_options require a modal formulation")) + return compute(LineCableModelsCoaxial(), problem, formulation; options) +end + +function compute( + problem::LineParametersProblem, + formulations::AbstractVector{<:LineParametersFormulation}; + options::Union{NamedTuple, ComputationOptions} = ComputationOptions(), + modal=nothing, modal_options::Union{NamedTuple, ComputationOptions}=ComputationOptions() +) + options = options isa NamedTuple ? ComputationOptions(options) : options + modal===nothing || return compute(problem,formulations, + ModalAnalysis.ModalAnalysisFormulation(modal);options,modal_options) + isempty(modal_options isa NamedTuple ? modal_options : modal_options.data) || + throw(ArgumentError("modal_options require a modal formulation")) + return compute(LineCableModelsCoaxial(), problem, formulations; options) +end + +""" +$(TYPEDSIGNATURES) + +Compute line parameters through an explicit coaxial backend tag. + +The tag owns execution dispatch while `formulation.methods` retains the +selected physical recipes. Ordinary callers can omit the tag and use the +two-argument `compute` method. +""" +Base.@constprop :aggressive function compute( + engine::LineCableModelsCoaxial, + problem::LineParametersProblem, + formulation::LineParametersFormulation = Formulation(); + options::Union{NamedTuple, ComputationOptions} = ComputationOptions() +) + options = options isa NamedTuple ? ComputationOptions(options) : options + execution = computation_options(LineCableModelsCoaxial, options) + # Timing changes the retained detail schema. Carry this finite choice through + # the caller's dynamically typed logger without widening the default result. + timing = execution.data.timing ? Val(true) : Val(false) + logger = VerbosityLogger(Logging.current_logger(), execution.data.verbosity) + return with_logger(logger) do + _compute(engine, problem, formulation, execution, timing) + end +end + +Base.@constprop :aggressive function compute( + engine::LineCableModelsCoaxial, + problem::LineParametersProblem, + formulations::AbstractVector{<:LineParametersFormulation}; + options::Union{NamedTuple, ComputationOptions} = ComputationOptions() +) + options = options isa NamedTuple ? ComputationOptions(options) : options + execution = computation_options(LineCableModelsCoaxial, options) + timing = execution.data.timing ? Val(true) : Val(false) + logger = VerbosityLogger(Logging.current_logger(), execution.data.verbosity) + return with_logger(logger) do + _compute(engine, problem, formulations, execution, timing) + end +end + +function computation_details( + ::Type{<:LineParametersFormulation}, + result::LineParameters +)::ComputationDetails + return details(result) +end + +function computation_details( + ::Type{<:LineCableModelsFEM}, + result::LineParameters +)::ComputationDetails + return details(result) +end + +function materials!( + destination::NamedTuple, + relation, + model::EarthModel{T}, + frequencies::AbstractVector{T}; workspace = nothing +) where {T <: Real} + rho, eps_r, mu_r = destination.rho, destination.eps_r, destination.mu_r + @inbounds for row in eachindex(model.layers) + static = EarthMaterial(model.layers[row]) + for column in eachindex(frequencies) + material = row == firstindex(model.layers) ? + static : + constitutive(relation, static, frequencies[column]; workspace) + rho[row, column] = material.rho + eps_r[row, column] = material.eps_r + mu_r[row, column] = material.mu_r + end + end + return destination +end + +function materials!(workspace::LineParametersWorkspace, formulation::LineParametersFormulation) + input, buffers = workspace.input, workspace.buffers + for (index, material) in pairs(input.cable.conductor_materials) + buffers.rho_cond[index] = constitutive(formulation.methods.temperature_dependence, + material, input.temperature; workspace) + end + buffers.earth.evaluated === nothing || materials!(buffers.earth.evaluated, + formulation.methods.earth_properties, input.earth, input.freq; workspace) + return workspace +end + +function materials!( + workspace::LineParametersWorkspace, formulation::LineParametersFormulation, + frequency::Int, + calculations::Tuple = workspace.plan.earth.calculations, + materials::Tuple = workspace.buffers.earth.calculations) + homogenize!(workspace, frequency, formulation, calculations, materials) + dielectric!(workspace.buffers.dielectric_admittivity, workspace.input.cable, + formulation.methods, workspace.input.freq[frequency], workspace.input.temperature; workspace) + return workspace +end + +@inline function _media!(destination, column::Int, air, earth) + epsilon0 = vacuum_permittivity(typeof(earth.rho)) + mu0 = vacuum_permeability(typeof(earth.rho)) + destination.rho[1, column] = air.rho + destination.rho[2, column] = earth.rho + destination.epsilon[1, column] = epsilon0 * air.eps_r + destination.epsilon[2, column] = epsilon0 * earth.eps_r + destination.mu[1, column] = mu0 * air.mu_r + destination.mu[2, column] = mu0 * earth.mu_r + return nothing +end + +function homogenize!( + workspace::LineParametersWorkspace, + frequency::Int, + formulation::LineParametersFormulation, + calculations::Tuple = workspace.plan.earth.calculations, + materials::Tuple = workspace.buffers.earth.calculations +) + foreach(calculations, materials) do calculation, destination + homogenize!(destination, calculation, workspace, frequency, + formulation.methods.earth_properties) + end + return materials +end + +# The plan decided once which earth the formula's expressions see: the layered earth or +# a reduction. The loop dispatches on that decision. +function homogenize!(destination, + calculation::NamedTuple, + workspace::LineParametersWorkspace, frequency_index::Int, relation) + earth = workspace.buffers.earth + model = workspace.input.earth + frequency = workspace.input.freq[frequency_index] + data = calculation.earth isa EquivalentHomogeneous.BeforeFD ? earth.static : earth.evaluated + homogenize!(destination, calculation.earth, relation, data, model, + calculation, frequency, frequency_index; workspace) + return destination +end + +# The layered earth reuses the layerwise FrequencyDependent values already evaluated +# during input construction. Its thicknesses were set when the buffers were allocated. +function homogenize!( + destination, + ::EarthModel, + relation, + evaluated, + model::EarthModel, + calculation, + frequency, + frequency_index::Int; workspace = nothing +) + epsilon0=vacuum_permittivity(eltype(destination.rho)) + mu0=vacuum_permeability(eltype(destination.rho)) + for row in axes(destination.rho, 1) + for column in eachindex(calculation.pairs) + destination.rho[row, column]=evaluated.rho[row, frequency_index] + destination.epsilon[row, column]=epsilon0*evaluated.eps_r[row, frequency_index] + destination.mu[row, column]=mu0*evaluated.mu_r[row, frequency_index] + end + end + return destination +end + +function homogenize!( + destination, + sequence::EquivalentHomogeneous.AfterFD, + relation, + evaluated, + model::EarthModel, + calculation, + frequency, + frequency_index::Int; workspace = nothing +) + rho = @view evaluated.rho[:, frequency_index] + eps_r = @view evaluated.eps_r[:, frequency_index] + mu_r = @view evaluated.mu_r[:, frequency_index] + air = EarthMaterial(rho[1], eps_r[1], mu_r[1]) + foreach(calculation.reductions) do reduction + @inbounds for column in reduction.pairs + functor = Functor(sequence.rule, (; rho, eps_r, mu_r, model, + pair = calculation.pairs[column].physical, frequency, reduction.options); + workspace) + earth = validate(reduction.expression(functor, workspace), sequence.rule) + _media!(destination, column, air, earth) + end + end + return destination +end + +function homogenize!( + destination, + sequence::EquivalentHomogeneous.BeforeFD, + relation, + static, + model::EarthModel, + calculation, + frequency, + frequency_index::Int; workspace = nothing +) + air = EarthMaterial(static.rho[1], static.eps_r[1], static.mu_r[1]) + foreach(calculation.reductions) do reduction + @inbounds for column in reduction.pairs + functor = Functor(sequence.rule, (; static.rho, static.eps_r, static.mu_r, model, + pair = calculation.pairs[column].physical, frequency, reduction.options); + workspace) + reconstructed = validate(reduction.expression(functor, workspace), sequence.rule) + earth = constitutive(relation, reconstructed, frequency; workspace) + _media!(destination, column, air, earth) + end + end + return destination +end diff --git a/src/engine/lineparameters/base.jl b/src/engine/lineparameters/base.jl new file mode 100644 index 000000000..ffa5e6823 --- /dev/null +++ b/src/engine/lineparameters/base.jl @@ -0,0 +1,45 @@ +Base.eltype(::LineParameters{T}) where {T} = T +Base.eltype(::Type{LineParameters{T}}) where {T} = T + +Base.size(value::SeriesImpedance) = size(value.values) +Base.size(value::SeriesImpedance, dimension::Int) = size(value.values, dimension) +Base.axes(value::SeriesImpedance) = axes(value.values) +Base.ndims(::Type{<:SeriesImpedance}) = 3 +Base.eltype(::Type{SeriesImpedance{T, Basis}}) where {T, Basis} = T +Base.getindex(value::SeriesImpedance, indices...) = getindex(value.values, indices...) +Base.IndexStyle(::Type{<:SeriesImpedance}) = IndexCartesian() + +Base.size(value::ShuntAdmittance) = size(value.values) +Base.size(value::ShuntAdmittance, dimension::Int) = size(value.values, dimension) +Base.axes(value::ShuntAdmittance) = axes(value.values) +Base.ndims(::Type{<:ShuntAdmittance}) = 3 +Base.eltype(::Type{ShuntAdmittance{T, Basis}}) where {T, Basis} = T +Base.getindex(value::ShuntAdmittance, indices...) = getindex(value.values, indices...) +Base.IndexStyle(::Type{<:ShuntAdmittance}) = IndexCartesian() + +function Base.getindex( + lp::LineParameters{T, U, D, Basis}, + selector::Union{ + Integer, AbstractRange{<:Integer}, AbstractVector{<:Integer}, Colon} +) where {T, U, D, Basis} + selected = selector isa Integer ? (selector:selector) : selector + checkbounds(lp.f, selected) + selected_frequencies = selector isa Integer ? lp.f[selector:selector] : lp.f[selected] + return LineParameters( + selectdomain(lp.domain, selected), + SeriesImpedance{T, Basis}(Array(view(lp.Z.values,:,:,selected))), + ShuntAdmittance{T, Basis}(Array(view(lp.Y.values,:,:,selected))), + selected_frequencies, + selectdetails(lp.details,lp.domain,selected) + ) +end + +""" +Return whether a scalar type encodes explicit numerical uncertainty. +""" +has_uncertainty_type(::Type) = false + +function _result_unit(value, selector) + quantity = Units.quantity(selector) + return Units.native_unit(quantity, basis(value)) +end diff --git a/src/engine/lineparameters/benchmark.jl b/src/engine/lineparameters/benchmark.jl new file mode 100644 index 000000000..6cdb9a33a --- /dev/null +++ b/src/engine/lineparameters/benchmark.jl @@ -0,0 +1,537 @@ +""" +$(TYPEDEF) + +Store element-wise absolute and reference-normalized root-mean-square benchmark errors. + +Each matrix entry contains the error for the corresponding line-parameter +term over the selected frequency samples. Missing values represent explicit +non-applicability, an empty band, or ineligible operands, with the explanation +retained in `details`. An ineligible sample makes both metrics missing for that +term and band. Eligible small and zero errors remain unchanged. + +$(TYPEDFIELDS) +""" +struct RMSError{T <: Real, D <: ComputationDetails} + "Absolute error in the units of the compared quantity." + absolute::Matrix{Union{Missing, T}} + "Reference-normalized error [dimensionless]." + relative::Matrix{Union{Missing, T}} + "Requested and selected frequency band, tolerance, applicability, and per-term classification." + details::D +end + +function RMSError{T}(absolute::AbstractMatrix, relative::AbstractMatrix; + details::ComputationDetails = ComputationDetails()) where {T <: Real} + size(absolute) == size(relative) || + throw(DimensionMismatch("RMS error matrices must match")) + return RMSError{T, typeof(details)}(absolute, relative, details) +end + +function RMSError(absolute::AbstractMatrix{T}, relative::AbstractMatrix{S}; + details::ComputationDetails = ComputationDetails()) where {T <: Real, S <: Real} + return RMSError{promote_type(T, S)}(absolute, relative; details) +end + +"""Return RMS comparison metadata without exposing result storage to consumers.""" +details(error::RMSError) = error.details +observe(error::RMSError, ::typeof(absolute_error)) = error.absolute +observe(error::RMSError, ::typeof(relative_error)) = error.relative +observe(error::RMSError, ::typeof(absolute_error), indices...) = getindex(error.absolute, indices...) +observe(error::RMSError, ::typeof(relative_error), indices...) = getindex(error.relative, indices...) + +""" +$(TYPEDEF) + +Store per-term frequency-domain errors for the series impedance and shunt +admittance of two [`LineParameters`](@ref) objects. + +$(TYPEDFIELDS) +""" +struct LineParametersBenchmark{T <: Real, Basis, ZE <: RMSError{T}, YE <: RMSError{T}} <: + AbstractProblemResult + "Series-impedance error." + Z::ZE + "Shunt-admittance error." + Y::YE +end + +function LineParametersBenchmark( + impedance::RMSError{T}, + admittance::RMSError{T}; + basis::Symbol = :pul +) where {T <: Real} + validate(basis, LineParameters) + return LineParametersBenchmark{T, basis, typeof(impedance), typeof(admittance)}(impedance, admittance) +end + +basis(::LineParametersBenchmark{T, Basis}) where {T, Basis} = Basis + +function observe(benchmark::LineParametersBenchmark, ::typeof(Z), ::typeof(absolute_error), indices...) + getindex(benchmark.Z.absolute, indices...) +end +function observe(benchmark::LineParametersBenchmark, ::typeof(Z), ::typeof(relative_error), indices...) + getindex(benchmark.Z.relative, indices...) +end +function observe(benchmark::LineParametersBenchmark, ::typeof(Y), ::typeof(absolute_error), indices...) + getindex(benchmark.Y.absolute, indices...) +end +function observe(benchmark::LineParametersBenchmark, ::typeof(Y), ::typeof(relative_error), indices...) + getindex(benchmark.Y.relative, indices...) +end + +function observe(benchmark::LineParametersBenchmark, ::typeof(Z), ::typeof(absolute_error)) + benchmark.Z.absolute +end +function observe(benchmark::LineParametersBenchmark, ::typeof(Z), ::typeof(relative_error)) + benchmark.Z.relative +end +function observe(benchmark::LineParametersBenchmark, ::typeof(Y), ::typeof(absolute_error)) + benchmark.Y.absolute +end +function observe(benchmark::LineParametersBenchmark, ::typeof(Y), ::typeof(relative_error)) + benchmark.Y.relative +end + +function observables(::Type{<:LineParametersBenchmark}) + ( + (Z, absolute_error), + (Z, relative_error), + (Y, absolute_error), + (Y, relative_error) + ) +end + +function _rms_series(reference::AbstractVector, result::AbstractVector, + normalization::Symbol, tolerance, result_tolerance; + reference_unresolved=nothing, result_unresolved=nothing) + unresolved_reference = reference_unresolved===nothing ? count(_resolution_unresolved.(reference,tolerance)) : count(reference_unresolved) + unresolved_result = result_unresolved===nothing ? count(_resolution_unresolved.(result,result_tolerance)) : count(result_unresolved) + counts = (; reference=unresolved_reference, result=unresolved_result) + for (operand,values) in ((:reference,reference),(:result,result)) + if any(value -> !resolution_available(value),values) + return (absolute=missing,relative=missing,status=Symbol(operand,:_unavailable), + reason="$(operand) contains unavailable or nonfinite samples; no samples were omitted",counts) + end + end + # Eligibility is two-sided and independent of normalization. Never reduce a + # band's sample population to hide an undefined pairwise relative comparison. + for (operand, count) in pairs(counts) + if count > 0 + entire_trace = count == length(reference) + status = if operand === :reference + entire_trace ? :reference_below_tolerance : :reference_sample_below_tolerance + else + entire_trace ? :result_below_tolerance : :result_sample_below_tolerance + end + reason = "Samples at or below declared resolution: reference " * + "$(counts.reference)/$(length(reference)), result $(counts.result)/$(length(reference)); " * + "both RMS metrics require nominal operands above tolerance at every selected sample; no samples were omitted" + return (; absolute=missing, relative = missing, status, reason, counts) + end + end + difference_norm = norm(reference .- result) + sample_normalizer = sqrt(oftype(difference_norm, length(reference))) + absolute = difference_norm / sample_normalizer + relative = normalization === :pointwise ? + norm((result .- reference) ./ reference) / sample_normalizer : + difference_norm / norm(reference) + return (; absolute, relative, status = :compared, reason = nothing, counts) +end + +""" + compare(reference::AbstractArray{<:Number,3}, result; normalization=:reference_rms, atol=0) + +Measure per-entry absolute and relative RMS differences across the third axis. +The caller must establish equal physical coordinates, units and terminal order. +`atol` is a nonnegative scalar or one tolerance per sample, in the input units. +Both RMS metrics are `missing` if either operand's nominal magnitude is at or +below `atol` at any selected sample, for either normalization. `details` explains +unavailable comparisons. Eligible small and zero errors are retained unchanged. +""" +function compare( + reference::AbstractArray{<:Number, 3}, result::AbstractArray{<:Number, 3}; + normalization::Symbol = :reference_rms, atol = 0) + axes(reference) == axes(result) || + throw(DimensionMismatch("RMS tensor axes must match")) + isempty(reference) && throw(ArgumentError("RMS tensors cannot be empty")) + normalization in (:reference_rms, :pointwise) || + throw(ArgumentError("unknown RMS normalization")) + tolerance=atol isa Real ? fill(atol, size(reference, 3)) : collect(atol) + length(tolerance) == size(reference, 3) || + throw(DimensionMismatch("one tolerance per sample is required")) + all(value -> value isa Real && isfinite(value) && value >= 0, tolerance) || + throw(ArgumentError("RMS tolerances must be finite and nonnegative")) + return _rms_arrays(reference, result, normalization, tolerance, tolerance) +end + +function _rms_arrays(reference, result, normalization, tolerance, result_tolerance; + reference_unresolved=nothing,result_unresolved=nothing) + T=promote_type(typeof(float(real(zero(Base.nonmissingtype(eltype(reference)))))), + typeof(float(real(zero(Base.nonmissingtype(eltype(result))))))) + errors=[_rms_series(view(reference, row, column, :), + view(result, row, column, :), normalization, tolerance, result_tolerance; + reference_unresolved=reference_unresolved===nothing ? nothing : view(reference_unresolved,row,column,:), + result_unresolved=result_unresolved===nothing ? nothing : view(result_unresolved,row,column,:)) + for row in axes(reference, 1), column in axes(reference, 2)] + return RMSError{T}(getproperty.(errors, :absolute), getproperty.(errors, :relative); + details = ComputationDetails(; normalization, atol = tolerance, sample_count = size(reference, 3), + status = getproperty.(errors, :status), normalization_reason = getproperty.(errors, :reason), + unresolved_samples = getproperty.(errors, :counts), + resolution=(kind=:explicit_floor, unit=nothing))) +end + +""" +$(TYPEDSIGNATURES) + +Compare two line-parameter results using absolute and reference-normalized +root-mean-square errors for each Z and Y matrix term across frequency. + +For the reference series ``A_{ij}`` and result series ``B_{ij}`` at one +matrix term, the default `normalization=:reference_rms` uses: + +```math +\\mathrm{RMS}_{\\mathrm{abs},ij} = +\\sqrt{\\frac{1}{N_f}\\sum_{k=1}^{N_f}\\left|A_{ij,k}-B_{ij,k}\\right|^2} +``` + +```math +\\mathrm{RMS}_{\\mathrm{rel},ij} = +\\sqrt{\\frac{\\sum_{k=1}^{N_f}\\left|A_{ij,k}-B_{ij,k}\\right|^2} +{\\sum_{k=1}^{N_f}\\left|A_{ij,k}\\right|^2}} +``` + +The operands must have identical frequency samples, tensor dimensions, basis, +and domain. Comparison does not reorder conductors, interpolate frequency +samples, convert basis, or apply a reduction. + +Every original operand sample must pass the shared engineering-zero classifier. +Complex zero requires both Cartesian components to satisfy their own cutoffs. +Unavailable samples are separately ineligible. Otherwise both error metrics are +`missing`, including for identical ineligible traces. Eligible identical traces +retain errors equal to zero. + +Keyword arguments are shared with the single-observable `compare` method: +`normalization`, `band`, `fundamental`, `harmonics`, `atol`, and `unsupported`. Full-band error +is the default. Optional sub-bands only slice the stored results. Each Z/Y +error retains its selected samples and numerical-zero classification. + +# Arguments + +- `reference`: reference line parameters. +- `result`: result line parameters on the same frequency samples. + +# Returns + +- A [`LineParametersBenchmark`](@ref). Absolute Z errors use the impedance + units selected by the result basis, and absolute Y errors use the corresponding + admittance units. Relative errors are dimensionless. + +# Errors + +- `DimensionMismatch` when Z/Y tensor dimensions differ. +- `ArgumentError` when frequencies, basis, or domain differ. +""" +function compare(reference::LineParameters, result::LineParameters; kwargs...) + return LineParametersBenchmark(compare(reference, result, Z; kwargs...), + compare(reference, result, Y; kwargs...); basis = basis(reference)) +end + +""" +$(TYPEDSIGNATURES) + +Compare a selected line-parameter observable over stored frequency samples. +The first operand sets the relative-error normalization, not scientific truth. + +# Arguments + +- `reference`, `result`: results with identical frequency coordinates, basis, + domain, and matrix dimensions. +- `quantity`: `Z`, `Y`, `R`, `X`, `L`, `G`, `B`, or `C`. Comparing `G` + separately prevents displacement current from hiding dielectric-loss differences. + +# Keywords + +- `normalization`: `:reference_rms` (default) divides the absolute RMS error + by the reference RMS. `:pointwise` instead computes the RMS of the + sample-wise relative errors. Both return dimensionless fractions, not percentages. +- `band`: `:all` (default), explicit closed `(lower, upper)` Hz bounds, or + `:dc` (0.1–100 Hz), `:harmonic` (`fundamental` to `harmonics*fundamental`), + `:narrow` (1000–1000000 Hz), or `:wide` (strictly above 1000000 Hz). +- `fundamental`: fundamental frequency in Hz. Default 50. +- `harmonics`: upper harmonic order. Default 50. Every stored sample in the + harmonic band is used, not only samples at integer harmonics. +- `atol`: declared absolute reporting resolution in the observable's native basis + units, not a certified floating-point error bound. + Use a scalar for the requested quantity or a NamedTuple to select tolerances + by component symbol. Defaults per meter are 1e-10 Ω/m for R, 1e-12 S/m for G, + 1e-15 H/m for L, and 1e-16 F/m for C. X and B thresholds are linked through + 2πf. Total-basis defaults scale by retained physical length. Otherwise explicit + total-unit cutoffs are required. Complex requests require component-keyed cutoffs. + Unless overridden directly, X/B use `2πf*atol_L`/`2πf*atol_C`, + Z uses `atol_R + 2πf*atol_L` and Y uses + `atol_G + 2πf*atol_C` at each sample. A fixed admittance threshold would + otherwise treat the same small capacitance differently across the spectrum. +- `unsupported`: NamedTuple of quantity symbols and explanatory strings for + explicitly unavailable comparisons. Backend declarations may also supply + this map as `details.comparison_unsupported` in either result. + +Closed endpoints snap to the nearest stored frequency by absolute Hz distance. +Ties select the lower frequency. Partial overlap uses the available portion. +Disjoint bands and an empty `:wide` band return `missing` errors with +`:no_samples`. No interpolation, extrapolation, weighting, or computation runs +are introduced. A one-sample band is valid. + +Both RMS metrics require every original operand sample to pass +`observation_resolution`, for either normalization. Real quantities use absolute +nominal magnitude. Complex zero requires both components to satisfy their own +cutoffs. An entire trace within tolerance gives `missing` for both metrics with status +`:reference_below_tolerance` or `:result_below_tolerance`. A partially +negligible trace gives `:reference_sample_below_tolerance` or +`:result_sample_below_tolerance`. The check is local to the selected band. +The denominator is used without a floor. Source arrays are never modified. + +For `normalization=:pointwise`, the relative error is + +```math +\\sqrt{\\frac{1}{N_f}\\sum_{k=1}^{N_f} +\\left|\\frac{B_{ij,k}-A_{ij,k}}{A_{ij,k}}\\right|^2}. +``` + +Samples are never omitted to obtain an eligible subset. Eligibility is independent +of normalization. Eligible errors are never clipped. Each cell retains its explanation +in `details.normalization_reason`. + +# Returns + +- [`RMSError`](@ref), including actual bounds, sample indices and count, tolerance, + reason, and a per-term status matrix in `details`. +""" +function compare(reference::AbstractCoreResult, result::AbstractCoreResult, + quantity::Union{typeof(Z), typeof(Y), typeof(R), typeof(X), typeof(L), typeof(G), typeof(B), typeof(C)}; + normalization::Symbol = :reference_rms, + band = :all, fundamental::Real = 50.0, harmonics::Integer = 50, + atol = nothing, unsupported::NamedTuple = (;)) + validate((; normalization, band, fundamental, harmonics, atol, unsupported), compare) + left_coordinates=get(details(reference).data, :coordinates, nothing) + right_coordinates=get(details(result).data, :coordinates, nothing) + if left_coordinates !== nothing && right_coordinates !== nothing + left_coordinates == right_coordinates || throw(ArgumentError("reference and result output terminal identities differ")) + end + f = frequencies(reference) + isempty(f) && throw(ArgumentError("reference frequencies cannot be empty")) + f == frequencies(result) || throw(ArgumentError( + "reference and result frequencies must match exactly and in order")) + basis(reference) === basis(result) || throw(ArgumentError("reference and result basis must match")) + domain(reference) === domain(result) || throw(ArgumentError("reference and result domains must match")) + issorted(f) || throw(ArgumentError("frequency-band comparison requires ascending stored frequencies")) + left, right = _line_observation_values(reference,quantity), _line_observation_values(result,quantity) + declared = merge(get(details(result).data, :comparison_unsupported, (;)), + get(details(reference).data, :comparison_unsupported, (;)), unsupported) + return compare(left, right, quantity; frequencies=f, result_basis=basis(reference), + reference_resolution=observation_resolution(reference, quantity; atol, frequencies=f), + result_resolution=observation_resolution(result, quantity; atol, frequencies=f), + normalization, band, fundamental, harmonics, atol, unsupported=declared) +end + +""" +$(TYPEDSIGNATURES) + +Compare physical tensors on an explicit frequency axis using the same band and +two-sided resolution rules as core results. Arrays use native units in +`result_basis` (`:pul` or `:total`). Frequencies use Hz. Owner-supplied resolution +records preserve operand-specific precision. The operation preserves each sample without a physical calculation. +""" +function compare(left::AbstractArray{<:Union{Missing,Number},3}, right::AbstractArray{<:Union{Missing,Number},3}, + quantity::Function; frequencies::AbstractVector, result_basis::Symbol, + reference_resolution=nothing, result_resolution=nothing, + normalization::Symbol=:reference_rms, band=:all, fundamental::Real=50.0, + harmonics::Integer=50, atol=nothing, unsupported::NamedTuple=(;)) + validate((; normalization, band, fundamental, harmonics, atol, unsupported), compare) + validate(result_basis, LineParameters) + quantity in _LINE_RESOLUTION_QUANTITIES || throw(ArgumentError("unsupported physical RMS quantity")) + f = frequencies + !isempty(f) && issorted(f) && all(value -> isfinite(value) && value >= 0, f) || + throw(ArgumentError("frequency-band comparison requires finite ascending nonnegative frequencies")) + size(left) == size(right) || throw(DimensionMismatch("reference and result quantity dimensions must match")) + !isempty(left) && size(left, 3) == length(f) || + throw(DimensionMismatch("quantity dimensions must match the stored frequencies")) + requested = if band === :all + (first(f), last(f)) + elseif band === :dc + (0.1, 100.0) + elseif band === :harmonic + (fundamental, fundamental * harmonics) + elseif band === :narrow + (1e3, 1e6) + elseif band === :wide + (1e6, Inf) + elseif band isa Tuple{Real, Real} + band + else + throw(ArgumentError("band must be :all, :dc, :harmonic, :narrow, :wide, or (lower, upper) in Hz")) + end + lower, upper = requested + isfinite(lower) && lower >= 0 && !isnan(upper) && upper >= lower || + throw(ArgumentError("frequency bounds must satisfy 0 ≤ lower ≤ upper with finite lower")) + indices = if band === :wide + (searchsortedlast(f, 1e6) + 1):length(f) + elseif upper < first(f) || lower > last(f) + 1:0 + elseif band === :all + 1:length(f) + else + first_index = argmin(abs.(f .- lower)) + last_index = isinf(upper) ? length(f) : argmin(abs.(f .- upper)) + first_index:last_index + end + T = promote_type(typeof(float(real(zero(Base.nonmissingtype(eltype(left)))))), + typeof(float(real(zero(Base.nonmissingtype(eltype(right))))))) + name = Symbol(nameof(quantity)) + resolution = reference_resolution === nothing ? + observation_resolution(left, quantity; atol, frequencies=f, result_basis) : reference_resolution + result_resolution = result_resolution === nothing ? + observation_resolution(right, quantity; atol, frequencies=f, result_basis) : result_resolution + tolerance = _comparison_cutoffs(resolution.atol,indices) + result_tolerance = _comparison_cutoffs(result_resolution.atol,indices) + reason = get(unsupported, name, nothing) + reason === nothing || reason isa AbstractString && !isempty(reason) || + throw(ArgumentError("unsupported comparisons require a nonempty explanatory string for $name")) + status = reason !== nothing ? :unsupported : isempty(indices) ? :no_samples : :compared + reason === nothing && isempty(indices) && + (reason = "No stored samples in the requested frequency band") + absolute = Matrix{Union{Missing, T}}(missing, size(left, 1), size(left, 2)) + relative = similar(absolute) + fill!(relative, missing) + classifications = fill(status, size(absolute)) + normalization_reasons = Matrix{Union{Nothing, String}}(nothing, size(absolute)) + unresolved_samples = fill((reference=0, result=0), size(absolute)) + if status === :compared + error=_rms_arrays(left[:, :, indices], right[:, :, indices], normalization, + tolerance, result_tolerance; + reference_unresolved=resolution.unresolved[:,:,indices], + result_unresolved=result_resolution.unresolved[:,:,indices]) + absolute .= error.absolute + relative .= error.relative + classifications .= error.details.data.status + normalization_reasons .= error.details.data.normalization_reason + unresolved_samples .= error.details.data.unresolved_samples + end + bounds = isempty(indices) ? (missing, missing) : + (f[first(indices)], f[last(indices)]) + comparison_details = (; quantity = name, normalization, band, + requested_bounds = requested, actual_bounds = bounds, + indices, sample_count = length(indices), fundamental, harmonics, atol = tolerance, + result_atol = result_tolerance, + status = classifications, reason, normalization_reason = normalization_reasons, + unresolved_samples, + resolution=(; resolution.kind, resolution.unit)) + # Empty bands and supported bands have the same result type on a Gridspace. + # Preserve the frequency scalar type while admitting an absent bound or reason. + detail_types=map(keys(comparison_details)) do key + key === :actual_bounds ? NTuple{2,Union{Missing,eltype(f)}} : + key === :reason ? Union{Nothing,String} : typeof(getproperty(comparison_details,key)) + end + stable_details=NamedTuple{keys(comparison_details),Tuple{detail_types...}}(values(comparison_details)) + return RMSError{T}(absolute, relative; details=ComputationDetails(stable_details)) +end + +_comparison_cutoffs(value::Real,indices) = fill(value,length(indices)) +_comparison_cutoffs(value::AbstractVector,indices) = value[indices] +_comparison_cutoffs(value::NamedTuple,indices) = map(item -> _comparison_cutoffs(item,indices),value) +_comparison_cutoffs(::Nothing,indices) = throw(ArgumentError("comparison requires explicit applicable operand cutoffs")) + +_comparison_primary(source::Commons.AbstractResultSpace,index) = source[index] +_comparison_primary(source::AbstractVector,index) = source[index] +_comparison_primary(source,index) = index==1 ? source : throw(BoundsError(source,index)) + +function compare(reference::AbstractCoreResult,results::AbstractVector,request::Union{Function,Tuple};kwargs...) + return [compare(reference,result,request;kwargs...) for result in results] +end + +function _comparison_maximum(values) + eligible=findall(!ismissing,values) + isempty(eligible) && return (value=missing,index=missing) + selected=eligible[argmax(values[eligible])] + return (value=values[selected],index=Tuple(selected)) +end + +""" +$(TYPEDSIGNATURES) + +Complete explicitly requested comparisons through the existing scalar or UQ +comparison methods, then associate each product with its original result and +reference identities. The request vector distinguishes this operation from one +statistical request tuple. No observation constructor performs this operation. +""" +function compare(reference,result,requests::AbstractVector; + bands=(:all,),normalizations=(:reference_rms,),pairing=nothing,kwargs...) + isempty(requests) && throw(ArgumentError("comparison requests must be nonempty")) + allunique(requests) && allunique(bands) && allunique(normalizations) || + throw(ArgumentError("comparison selections must be distinct")) + if reference isa AbstractCoreResult && pairing!==nothing + count=result isa Union{AbstractVector,Commons.AbstractResultSpace} ? length(result) : 1 + all(pair -> first(pair)==1,pairing) && sort(last.(collect(pairing)))==collect(1:count) || + throw(ArgumentError("a scalar reference requires reference index 1 for every result")) + pairing=nothing + end + completed=NamedTuple[] + for request in requests,band in bands,normalization in normalizations + errors=pairing===nothing ? compare(reference,result,request;band,normalization,kwargs...) : + compare(reference,result,request;band,normalization,pairing,kwargs...) + for (result_index,error) in enumerate(errors isa RMSError ? (errors,) : errors) + reference_index=pairing===nothing ? 1 : first(only(filter(pair -> last(pair)==result_index,pairing))) + left=_comparison_primary(reference,reference_index) + right=_comparison_primary(result,result_index) + reference_id=Commons.observation_gridpoint(left).id + result_id=Commons.observation_gridpoint(right).id + reference_id===nothing && throw(ArgumentError("the reference needs an explicit retained gridpoint identity")) + result_id===nothing && throw(ArgumentError("the result needs an explicit retained gridpoint identity")) + absolute=observe(error,absolute_error) + relative=observe(error,relative_error) + information=merge(details(error).data,(basis=basis(right),requested_atol=get(kwargs,:atol,nothing),unsupported=get(kwargs,:unsupported,(;)))) + identity=request_identity(request) + statistic=identity isa Tuple && first(identity)!==Z && first(identity)!==Y ? + (last(identity) isa Base.Fix2 ? Symbol("quantile_",last(identity).x) : nameof(last(identity))) : :value + unit=Units.native_unit(Commons.request_quantity(request),basis(right)) + push!(completed,Commons.detach((result_id,reference_id,request, + quantity=Commons.request_quantity(request),statistic,band,normalization, + absolute,relative,absolute_unit=unit,relative_unit=Units.units(:base,:dimensionless), + coordinates=get(details(right).data,:coordinates,string.(1:size(absolute,1))), + assumptions=observation_assumptions(right,identity isa Function ? identity : identity[2]), + settings=information,maxima=(absolute=_comparison_maximum(absolute),relative=_comparison_maximum(relative))))) + end + end + return completed +end + +""" +$(TYPEDSIGNATURES) + +Validate the RMS comparison settings of `compare` before accessing results or +starting a computation. `settings` holds `normalization`, `band`, `fundamental`, +`harmonics`, `atol` and `unsupported`. Frequency bounds and `fundamental` use Hz. Absolute +tolerances use the units of the selected quantities. Return `settings`. +""" +function validate(settings::NamedTuple, ::typeof(compare)) + (; normalization, band, fundamental, harmonics, atol, unsupported) = settings + normalization in (:reference_rms, :pointwise) || throw(ArgumentError( + "normalization must be :reference_rms or :pointwise")) + fundamental isa Real && isfinite(fundamental) && fundamental > 0 || + throw(ArgumentError("fundamental must be finite and positive Hz")) + harmonics isa Integer && !(harmonics isa Bool) && harmonics > 0 || + throw(ArgumentError("harmonics must be a positive integer")) + if band isa Tuple{Real, Real} + lower, upper = band + isfinite(lower) && lower >= 0 && !isnan(upper) && upper >= lower || + throw(ArgumentError("frequency bounds must satisfy 0 ≤ lower ≤ upper with finite lower")) + else + band in (:all, :dc, :harmonic, :narrow, :wide) || throw(ArgumentError( + "band must be :all, :dc, :harmonic, :narrow, :wide, or (lower, upper) in Hz")) + end + _validate_resolution_atol(atol) + unsupported isa NamedTuple && isempty(setdiff(keys(unsupported), (:Z, :Y, :R, :X, :L, :G, :B, :C))) || + throw(ArgumentError("unsupported must name Z, Y, R, X, L, G, B, or C")) + all(reason -> reason isa AbstractString && !isempty(reason), unsupported) || + throw(ArgumentError("unsupported comparisons require nonempty explanatory strings")) + return settings +end diff --git a/src/engine/lineparameters/lineparameters.jl b/src/engine/lineparameters/lineparameters.jl new file mode 100644 index 000000000..e83d6c33e --- /dev/null +++ b/src/engine/lineparameters/lineparameters.jl @@ -0,0 +1,556 @@ +const LINE_PARAMETER_BASES = (:pul, :total) + +""" + SeriesImpedance{T, Basis} + +Store a square series-impedance matrix over frequency. Values use \\[Ω/m\\] +when `Basis` is `:pul` and \\[Ω\\] when it is `:total`. +""" +struct SeriesImpedance{T, Basis} <: AbstractArray{T, 3} + "Complex series-impedance tensor with dimensions conductor × conductor × frequency." + values::Array{T, 3} + + function SeriesImpedance{T, Basis}(values::Array{T, 3}) where {T, Basis} + Basis isa Symbol || throw( + ArgumentError("basis must be :pul or :total; got $(repr(Basis))"), + ) + validate(Basis, LineParameters) + return new{T, Basis}(values) + end +end + +""" + ShuntAdmittance{T, Basis} + +Store a square shunt-admittance matrix over frequency. Values use \\[S/m\\] +when `Basis` is `:pul` and \\[S\\] when it is `:total`. +""" +struct ShuntAdmittance{T, Basis} <: AbstractArray{T, 3} + "Complex shunt-admittance tensor with dimensions conductor × conductor × frequency." + values::Array{T, 3} + + function ShuntAdmittance{T, Basis}(values::Array{T, 3}) where {T, Basis} + Basis isa Symbol || throw( + ArgumentError("basis must be :pul or :total; got $(repr(Basis))"), + ) + validate(Basis, LineParameters) + return new{T, Basis}(values) + end +end + +function SeriesImpedance(A::AbstractArray{T, 3}; basis::Symbol = :pul) where {T} + validate(basis, LineParameters) + return SeriesImpedance{T, basis}(Array(A)) +end + +function ShuntAdmittance(A::AbstractArray{T, 3}; basis::Symbol = :pul) where {T} + validate(basis, LineParameters) + return ShuntAdmittance{T, basis}(Array(A)) +end + +@inline basis(::Type{<:SeriesImpedance{T, Basis}}) where {T, Basis} = Basis +@inline basis(::SeriesImpedance{T, Basis}) where {T, Basis} = Basis +@inline basis(::Type{<:ShuntAdmittance{T, Basis}}) where {T, Basis} = Basis +@inline basis(::ShuntAdmittance{T, Basis}) where {T, Basis} = Basis + +""" + LineParameters{T, U, D, Basis, Q} + +Frequency-dependent series-impedance and shunt-admittance matrices. + + `Basis` is either `:pul` or `:total`. Per-length values are stored in +Ω/m and S/m. Total values are stored in Ω and S. Frequencies are stored in +Hz. `D` is the concrete physical coordinate-domain value. `Q` is the concrete +named-tuple type of optional computation output. +""" +struct LineParameters{ + T <: Complex, + U <: Real, + D <: LineParamsDomain, + Basis, + Q <: ComputationDetails +} <: AbstractCoreResult + "Frequency-dependent series impedance \\[Ω/m\\] or \\[Ω\\]." + Z::SeriesImpedance{T, Basis} + "Frequency-dependent shunt admittance \\[S/m\\] or \\[S\\]." + Y::ShuntAdmittance{T, Basis} + "Frequency samples \\[Hz\\]." + f::Vector{U} + "Physical coordinate domain, including any required transformation state." + domain::D + "Typed supplemental output retained by the computation." + details::Q + + function LineParameters( + domain::D, + Z::SeriesImpedance{T, Basis}, + Y::ShuntAdmittance{T, Basis}, + f::AbstractVector{U}, + details::Q = ComputationDetails() + ) where { + D <: LineParamsDomain, + T <: Complex, + U <: Real, + Basis, + Q <: ComputationDetails + } + retained=haskey(details.data,:gridpoint) ? details : + completion_details(merge(details.data,(gridpoint=Commons.gridpoint_id(),))) + return validate(new{T, U, D, Basis, typeof(retained)}( + Z, + Y, + Vector{U}(f), + domain, + retained + )) + end +end + +# Line-parameter quantities are per unit length (`:pul`) or for the total line length. +@inline function validate(basis::Symbol, ::Type{LineParameters}) + basis in LINE_PARAMETER_BASES || throw( + ArgumentError("basis must be :pul or :total; got :$basis"), + ) + return basis +end + +# The phase domain does not restrict the coefficients. +validate(domain::LineParamsDomain, ::LineParameters) = domain + +function validate(domain::ModalDomain, parameters::LineParameters) + size(domain.operators)==size(parameters.Z.values) || throw(DimensionMismatch( + "modal operators must align with line-parameter coefficients")) + return domain +end + +function validate(parameters::LineParameters{T, U, D, Basis}) where {T, U, D, Basis} + Basis in LINE_PARAMETER_BASES || throw(ArgumentError( + "LineParameters basis must be :pul or :total; received $(repr(Basis))" + )) + size(parameters.Z, 1) == size(parameters.Z, 2) || throw(DimensionMismatch( + "LineParameters.Z must be square; received size $(size(parameters.Z))" + )) + size(parameters.Y, 1) == size(parameters.Y, 2) || throw(DimensionMismatch( + "LineParameters.Y must be square; received size $(size(parameters.Y))" + )) + size(parameters.Z) == size(parameters.Y) || throw(DimensionMismatch( + "LineParameters.Z and LineParameters.Y must have equal n×n×nfreq " * + "dimensions; received $(size(parameters.Z)) and $(size(parameters.Y))" + )) + size(parameters.Z, 3) == length(parameters.f) || throw(DimensionMismatch( + "LineParameters.f must contain one value per matrix frequency plane; " * + "received $(length(parameters.f)) values for $(size(parameters.Z, 3)) planes" + )) + validate(parameters.domain, parameters) + all(isfinite, parameters.f) || throw(ArgumentError( + "LineParameters.f must contain only finite frequencies; received " * + repr(parameters.f) + )) + return parameters +end + +function LineParameters( + ::Type{PhaseDomain}, + Z::SeriesImpedance{T, Basis}, + Y::ShuntAdmittance{T, Basis}, + f::AbstractVector{U}, + details::Q = ComputationDetails() +) where {T <: Complex, U <: Real, Basis, Q <: ComputationDetails} + return LineParameters(PhaseDomain(), Z, Y, f, details) +end + +function LineParameters( + Z::SeriesImpedance{T, Basis}, + Y::ShuntAdmittance{T, Basis}, + f::AbstractVector{U} +) where {T <: Complex, U <: Real, Basis} + return LineParameters(PhaseDomain, Z, Y, f) +end + +function LineParameters( + Z::SeriesImpedance{TZ, ZBasis}, + Y::ShuntAdmittance{TY, YBasis}, + f::AbstractVector{U} +) where { + TZ <: Complex, + TY <: Complex, + U <: Real, + ZBasis, + YBasis +} + ZBasis === YBasis || throw( + ArgumentError("Z and Y must have the same basis; got :$ZBasis and :$YBasis"), + ) + element_type = promote_type(TZ, TY) + return LineParameters( + PhaseDomain, + SeriesImpedance(convert(Array{element_type, 3}, Z.values); basis = ZBasis), + ShuntAdmittance(convert(Array{element_type, 3}, Y.values); basis = YBasis), + f + ) +end + +function LineParameters( + domain::D, + Z::AbstractArray{TZ, 3}, + Y::AbstractArray{TY, 3}, + f::AbstractVector{U}; + basis::Symbol = :pul, + details::ComputationDetails = ComputationDetails() +) where { + D <: LineParamsDomain, + TZ <: Complex, + TY <: Complex, + U <: Real +} + validate(basis, LineParameters) + element_type = promote_type(TZ, TY) + return LineParameters( + domain, + SeriesImpedance(convert(Array{element_type, 3}, Z); basis), + ShuntAdmittance(convert(Array{element_type, 3}, Y); basis), + f, + details + ) +end + +function LineParameters( + ::Type{PhaseDomain}, + Z::AbstractArray{TZ, 3}, + Y::AbstractArray{TY, 3}, + f::AbstractVector{U}; + basis::Symbol = :pul, + details::ComputationDetails = ComputationDetails() +) where {TZ <: Complex, TY <: Complex, U <: Real} + return LineParameters(PhaseDomain(), Z, Y, f; basis, details) +end + +function LineParameters( + Z::AbstractArray{TZ, 3}, + Y::AbstractArray{TY, 3}, + f::AbstractVector{U}; + basis::Symbol = :pul, + details::ComputationDetails = ComputationDetails() +) where { + TZ <: Complex, + TY <: Complex, + U <: Real +} + return LineParameters(PhaseDomain, Z, Y, f; basis, details) +end + +@inline domain(::Type{<:LineParameters{ + T, U, D}}) where {T, U, D <: LineParamsDomain} = domain(D) +@inline domain(lp::LineParameters) = domain(typeof(lp)) +@inline basis(::Type{<:LineParameters{T, U, D, Basis}}) where {T, U, D, Basis} = Basis +@inline basis(::LineParameters{T, U, D, Basis}) where {T, U, D, Basis} = Basis +details(parameters::LineParameters) = parameters.details + +observe(lp::LineParameters, ::typeof(frequencies)) = lp.f +observe(lp::LineParameters, ::typeof(frequencies), indices...) = getindex(lp.f, indices...) +frequencies(lp::LineParameters, indices...) = observe(lp, frequencies, indices...) +nconductors(lp::LineParameters) = size(lp.Z, 1) +nfrequencies(lp::LineParameters) = length(lp.f) + +function observables(::Type{<:LineParameters}) + ( + frequencies, + Z, + Y, + R, + X, + L, + G, + B, + C, + (Z, abs), + (Z, angle), + (Y, abs), + (Y, angle), + (Z, diag), + (Y, diag), + (R, diag), + (X, diag), + (L, diag), + (G, diag), + (B, diag), + (C, diag) + ) +end + +series_impedance(value::Union{LineParameters, SeriesImpedance}) = observe(value, Z) +shunt_admittance(value::Union{LineParameters, ShuntAdmittance}) = observe(value, Y) +""" + Z(parameters[, i, j[, k]]) + Y(parameters[, i, j[, k]]) + +Return series impedance or shunt admittance. With `(i, j)`, return the complete +frequency response at that matrix position. Use one index, a range or `:` for `k`. Stored units follow [`basis`](@ref): \\[Ω/m\\] and \\[S/m\\] for +`:pul` or \\[Ω\\] and \\[S\\] for `:total`. +""" +@inline _observe_array(values::AbstractArray) = values +@inline _observe_array(values::AbstractArray, i, j) = view(values, i, j, :) +@inline _observe_array(values::AbstractArray, indices...) = getindex(values, indices...) + +function observe(impedance::SeriesImpedance, ::typeof(Z), indices...) + _observe_array(impedance.values, indices...) +end +function observe(admittance::ShuntAdmittance, ::typeof(Y), indices...) + _observe_array(admittance.values, indices...) +end +function observe(lp::LineParameters, ::typeof(Z), indices...) + _observe_array(lp.Z.values, indices...) +end +function observe(lp::LineParameters, ::typeof(Y), indices...) + _observe_array(lp.Y.values, indices...) +end + +function observe(value::Union{LineParameters, SeriesImpedance}, ::typeof(R), indices...) + real.(observe(value, Z, indices...)) +end +function observe(value::Union{LineParameters, SeriesImpedance}, ::typeof(X), indices...) + imag.(observe(value, Z, indices...)) +end +function observe(value::Union{LineParameters, ShuntAdmittance}, ::typeof(G), indices...) + real.(observe(value, Y, indices...)) +end +function observe(value::Union{LineParameters, ShuntAdmittance}, ::typeof(B), indices...) + imag.(observe(value, Y, indices...)) +end + +function observe(value::LineParameters, ::typeof(Z), ::typeof(abs), indices...) + abs.(observe(value, Z, indices...)) +end +function observe(value::SeriesImpedance, ::typeof(Z), ::typeof(abs), indices...) + abs.(observe(value, Z, indices...)) +end +function observe(value::LineParameters, ::typeof(Z), ::typeof(angle), indices...) + angle.(observe(value, Z, indices...)) +end +function observe(value::SeriesImpedance, ::typeof(Z), ::typeof(angle), indices...) + angle.(observe(value, Z, indices...)) +end +function observe(value::LineParameters, ::typeof(Y), ::typeof(abs), indices...) + abs.(observe(value, Y, indices...)) +end +function observe(value::ShuntAdmittance, ::typeof(Y), ::typeof(abs), indices...) + abs.(observe(value, Y, indices...)) +end +function observe(value::LineParameters, ::typeof(Y), ::typeof(angle), indices...) + angle.(observe(value, Y, indices...)) +end +function observe(value::ShuntAdmittance, ::typeof(Y), ::typeof(angle), indices...) + angle.(observe(value, Y, indices...)) +end + +function _observe_diagonal(values::AbstractArray{T, 3}, indices...) where {T} + size(values, 1) == size(values, 2) || throw(DimensionMismatch( + "diagonal observation requires square matrix slices" + )) + diagonal = Matrix{T}(undef, size(values, 1), size(values, 3)) + for sample in axes(values, 3), mode in axes(values, 1) + + diagonal[mode, sample] = values[mode, mode, sample] + end + return isempty(indices) ? diagonal : getindex(diagonal, indices...) +end + +const _SeriesResult = Union{LineParameters, SeriesImpedance} +const _ShuntResult = Union{LineParameters, ShuntAdmittance} + +function observe(value::LineParameters, ::typeof(Z), ::typeof(diag), indices...) + _observe_diagonal(observe(value, Z), indices...) +end +function observe(value::SeriesImpedance, ::typeof(Z), ::typeof(diag), indices...) + _observe_diagonal(observe(value, Z), indices...) +end +function observe(value::_SeriesResult, ::typeof(R), ::typeof(diag), indices...) + _observe_diagonal(observe(value, R), indices...) +end +function observe(value::_SeriesResult, ::typeof(X), ::typeof(diag), indices...) + _observe_diagonal(observe(value, X), indices...) +end +function observe(value::LineParameters, ::typeof(L), ::typeof(diag), indices...) + _observe_diagonal(observe(value, L), indices...) +end +""" +$(TYPEDSIGNATURES) + +Return diagonal inductances from a standalone impedance tensor and its explicit +frequency vector \\[Hz\\]. The output axes are conductor × frequency. Optional +indices select those axes. Values use \\[H/m\\] for `:pul` and \\[H\\] for `:total`. +""" +function observe(value::SeriesImpedance, ::typeof(L), ::typeof(diag), + frequencies::AbstractVector, indices...) + _observe_diagonal(observe(value, L, frequencies), indices...) +end + +function observe(value::LineParameters, ::typeof(Y), ::typeof(diag), indices...) + _observe_diagonal(observe(value, Y), indices...) +end +function observe(value::ShuntAdmittance, ::typeof(Y), ::typeof(diag), indices...) + _observe_diagonal(observe(value, Y), indices...) +end +function observe(value::_ShuntResult, ::typeof(G), ::typeof(diag), indices...) + _observe_diagonal(observe(value, G), indices...) +end +function observe(value::_ShuntResult, ::typeof(B), ::typeof(diag), indices...) + _observe_diagonal(observe(value, B), indices...) +end +function observe(value::LineParameters, ::typeof(C), ::typeof(diag), indices...) + _observe_diagonal(observe(value, C), indices...) +end +""" +$(TYPEDSIGNATURES) + +Return diagonal capacitances from a standalone admittance tensor and its explicit +frequency vector \\[Hz\\]. The output axes are conductor × frequency. Optional +indices select those axes. Values use \\[F/m\\] for `:pul` and \\[F\\] for `:total`. +""" +function observe(value::ShuntAdmittance, ::typeof(C), ::typeof(diag), + frequencies::AbstractVector, indices...) + _observe_diagonal(observe(value, C, frequencies), indices...) +end + +function observables(::Type{<:SeriesImpedance}) + (Z, R, X, L, (Z, abs), (Z, angle), (Z, diag), (R, diag), (X, diag), (L, diag)) +end +function observables(::Type{<:ShuntAdmittance}) + (Y, G, B, C, (Y, abs), (Y, angle), (Y, diag), (G, diag), (B, diag), (C, diag)) +end + +Z(value::Union{LineParameters, SeriesImpedance}, indices...) = observe(value, Z, indices...) +Y(value::Union{LineParameters, ShuntAdmittance}, indices...) = observe(value, Y, indices...) +R(value::Union{LineParameters, SeriesImpedance}, indices...) = observe(value, R, indices...) +X(value::Union{LineParameters, SeriesImpedance}, indices...) = observe(value, X, indices...) +G(value::Union{LineParameters, ShuntAdmittance}, indices...) = observe(value, G, indices...) +B(value::Union{LineParameters, ShuntAdmittance}, indices...) = observe(value, B, indices...) + +resistance(value::Union{LineParameters, SeriesImpedance}, args...) = observe(value, R, args...) +reactance(value::Union{LineParameters, SeriesImpedance}, args...) = observe(value, X, args...) +conductance(value::Union{LineParameters, ShuntAdmittance}, args...) = observe(value, G, args...) +susceptance(value::Union{LineParameters, ShuntAdmittance}, args...) = observe(value, B, args...) + +@inline function _angular_frequencies(lp::LineParameters, k) + selected = lp.f[k] + any(iszero, selected isa Number ? (selected,) : selected) && throw( + DomainError(selected, "L and C are undefined at zero frequency"), + ) + return 2π .* selected +end + +function _angular_frequencies(frequencies::AbstractVector) + any(iszero, frequencies) && throw( + DomainError(frequencies, "L and C are undefined at zero frequency"), + ) + return reshape(2π .* frequencies, 1, 1, :) +end + +function observe( + impedance::SeriesImpedance, + ::typeof(L), + frequencies::AbstractVector +) + size(impedance, 3) == length(frequencies) || throw( + DimensionMismatch("frequency count must match the impedance third dimension"), + ) + return imag.(impedance.values) ./ _angular_frequencies(frequencies) +end + +function observe( + impedance::SeriesImpedance, + ::typeof(L), + frequencies::AbstractVector, + indices... +) + return getindex(observe(impedance, L, frequencies), indices...) +end + +function observe( + admittance::ShuntAdmittance, + ::typeof(C), + frequencies::AbstractVector +) + size(admittance, 3) == length(frequencies) || throw( + DimensionMismatch("frequency count must match the admittance third dimension"), + ) + return imag.(admittance.values) ./ _angular_frequencies(frequencies) +end + +function observe( + admittance::ShuntAdmittance, + ::typeof(C), + frequencies::AbstractVector, + indices... +) + return getindex(observe(admittance, C, frequencies), indices...) +end + +""" +$(TYPEDSIGNATURES) + +Return series inductance using the same frequency-selection grammar as +[`Z`](@ref), evaluated as: + +```math +L(f) = \\frac{\\operatorname{Im} Z(f)}{2\\pi f}. +``` + +Units are \\[H/m\\] for `:pul` and \\[H\\] for `:total`. + +# Errors + +Throws `DomainError` when a selected frequency is zero. +""" +function observe(lp::LineParameters, ::typeof(L)) + any(iszero, lp.f) && throw(DomainError(lp.f, "L is undefined at zero frequency")) + return imag.(lp.Z.values) ./ reshape(2π .* lp.f, 1, 1, :) +end + +""" +$(TYPEDSIGNATURES) + +Return shunt capacitance using the same frequency-selection grammar as +[`Y`](@ref), evaluated as: + +```math +C(f) = \\frac{\\operatorname{Im} Y(f)}{2\\pi f}. +``` + +Units are \\[F/m\\] for `:pul` and \\[F\\] for `:total`. + +# Errors + +Throws `DomainError` when a selected frequency is zero. +""" +function observe(lp::LineParameters, ::typeof(C)) + any(iszero, lp.f) && throw(DomainError(lp.f, "C is undefined at zero frequency")) + return imag.(lp.Y.values) ./ reshape(2π .* lp.f, 1, 1, :) +end + +observe(lp::LineParameters, ::typeof(L), i, j) = observe(lp, L, i, j, :) +observe(lp::LineParameters, ::typeof(C), i, j) = observe(lp, C, i, j, :) +function _divide_by_angular_frequency(component, angular_frequency) + angular_frequency isa Number && return component ./ angular_frequency + ndims(component) == 1 && return component ./ angular_frequency + dimensions = (ntuple(_ -> 1, ndims(component) - 1)..., length(angular_frequency)) + return component ./ reshape(angular_frequency, dimensions) +end + +function observe(lp::LineParameters, ::typeof(L), i, j, k) + return _divide_by_angular_frequency( + observe(lp, X, i, j, k), + _angular_frequencies(lp, k) + ) +end +function observe(lp::LineParameters, ::typeof(C), i, j, k) + return _divide_by_angular_frequency( + observe(lp, B, i, j, k), + _angular_frequencies(lp, k) + ) +end + +L(lp::LineParameters, args...) = observe(lp, L, args...) +C(lp::LineParameters, args...) = observe(lp, C, args...) +inductance(lp::LineParameters, args...) = observe(lp, L, args...) +capacitance(lp::LineParameters, args...) = observe(lp, C, args...) diff --git a/src/engine/lineparameters/observations.jl b/src/engine/lineparameters/observations.jl new file mode 100644 index 000000000..27c5c4ffb --- /dev/null +++ b/src/engine/lineparameters/observations.jl @@ -0,0 +1,234 @@ +const _ObservedLineSource = Union{LineParameters,SeriesImpedance,ShuntAdmittance} +_line_families(::LineParameters) = (Z,Y) +_line_families(::SeriesImpedance) = (Z,) +_line_families(::ShuntAdmittance) = (Y,) +_primary_family(selector) = selector in (Z,R,X,L) ? Z : selector in (Y,G,B,C) ? Y : nothing +observation_assumptions(::Union{SeriesImpedance,ShuntAdmittance},selector) = nothing +function observation_assumptions(source::AbstractCoreResult,selector) + family=_primary_family(selector) + family===nothing && return nothing + return get(get(details(source).data,:selections,(;)),Symbol(nameof(family)),nothing) +end +_primary_key(identity) = identity isa Function ? nameof(identity) : + first(identity) in (Z,Y) && any(in((abs,angle)),Base.tail(identity)) ? + Symbol(nameof(first(identity)),last(filter(in((abs,angle)),Base.tail(identity)))===abs ? :_abs : :_angle) : nameof(first(identity)) +_pair_keys(::typeof(Z)) = ((:R,:X),(:Z_abs,:Z_angle),(:R,:L)) +_pair_keys(::typeof(Y)) = ((:G,:B),(:Y_abs,:Y_angle),(:G,:C)) +_pair_selector(key::Symbol) = key===:Z_abs ? (Z,abs) : key===:Z_angle ? (Z,angle) : + key===:Y_abs ? (Y,abs) : key===:Y_angle ? (Y,angle) : (getfield(@__MODULE__,key),) + +function _line_request(source,request) + identity=request_identity(request) + prefix=identity isa Tuple ? identity : (identity,) + indices=request_indices(request) + first(prefix) in (real,imag,abs,angle) && throw(ArgumentError("use a physical quantity or a family-qualified transform")) + if length(prefix)>=2 && prefix[2] in (real,imag) + component=first(prefix)===Z ? (prefix[2]===real ? R : X) : + first(prefix)===Y ? (prefix[2]===real ? G : B) : + throw(ArgumentError("real/imag transforms require Z or Y")) + prefix=(component,prefix[3:end]...) + end + family=_primary_family(first(prefix)) + family in _line_families(source) || throw(ArgumentError("quantity does not belong to an available primary family")) + diagonal=diag in prefix + rank=diagonal ? 2 : 3 + isempty(indices) && (indices=ntuple(_ -> Colon(),rank)) + !diagonal && length(indices)==2 && (indices=(indices...,Colon())) + length(indices)==rank || throw(DimensionMismatch("a primary request requires $rank coordinate selectors")) + all(x -> x isa Colon || x isa Integer && !(x isa Bool) || x isa AbstractVector{<:Integer},indices) || + throw(ArgumentError("coordinates must be integer indices, ranges, vectors, or :")) + # Polar diagonal access is obtained from the original complex diagonal. + supported=Tuple(filter(!=(diag),prefix)) + identity_checked=length(supported)==1 ? only(supported) : supported + identity_checked in observables(typeof(source)) || throw(ArgumentError("unsupported line representation")) + return (prefix...,indices...) +end + +function _line_observation_requests(source::_ObservedLineSource,requests::Tuple;complete_pairs::Bool=false) + selected=isempty(requests) ? Tuple(_line_families(source)) : requests + expanded=Tuple[] + for item in selected + if item in (real,imag,abs,angle) + append!(expanded,[_line_request(source,(family,item)) for family in _line_families(source)]) + continue + end + normalized=_line_request(source,item) + identity=request_identity(normalized) + prefix=identity isa Tuple ? identity : (identity,) + if first(prefix) in (Z,Y) && all(==(diag),Base.tail(prefix)) + for key in first(_pair_keys(first(prefix))) + push!(expanded,(_pair_selector(key)...,Base.tail(prefix)...,request_indices(normalized)...)) + end + else + push!(expanded,normalized) + end + end + allunique(expanded) || throw(ArgumentError("observation requests must be distinct")) + retained=Tuple[] + for family in _line_families(source) + entries=filter(request -> _primary_family(first(request))===family,expanded) + if isempty(entries) + template=first(expanded) + diagonal=diag in (request_identity(template) isa Tuple ? request_identity(template) : ()) + entries=[(_pair_selector(key)...,(diagonal ? (diag,) : ())...,request_indices(template)...) + for key in first(_pair_keys(family))] + end + keys=Tuple(_primary_key(request_identity(request)) for request in entries) + choices=filter(pair -> all(in(pair),keys),_pair_keys(family)) + if length(entries)==1 && complete_pairs + chosen=first(choices) + diagonal=diag in (request_identity(first(entries)) isa Tuple ? request_identity(first(entries)) : ()) + entries=[(_pair_selector(key)...,(diagonal ? (diag,) : ())...,request_indices(first(entries))...) + for key in chosen] + elseif length(entries)!=2 || length(unique(keys))!=2 || length(choices)!=1 + throw(ArgumentError("$(nameof(family)) requires one complete pair: $(_pair_keys(family)); received $keys")) + end + dimensions=size(source isa LineParameters ? source.Z : source) + coordinate(request) = begin + identity=request_identity(request) + diagonal=identity isa Tuple && diag in identity + dims=diagonal ? (dimensions[1],dimensions[3]) : dimensions + map(observation_indices,request_indices(request),dims) + end + coordinate(first(entries))==coordinate(last(entries)) || + throw(DimensionMismatch("both quantities of a primary pair must select identical coordinates and rank")) + chosen=only(filter(pair -> Set(pair)==Set(_primary_key(request_identity(q)) for q in entries),_pair_keys(family))) + append!(retained,[only(filter(q -> _primary_key(request_identity(q))===key,entries)) for key in chosen]) + end + return (retained=Tuple(retained),displayed=Tuple(expanded)) +end +Commons.observation_requests(source::LineParameters,requests::Tuple;complete_pairs::Bool=false) = + _line_observation_requests(source,requests;complete_pairs) +Commons.observation_requests(source::Union{SeriesImpedance,ShuntAdmittance},requests::Tuple;complete_pairs::Bool=false) = + _line_observation_requests(source,requests;complete_pairs) + +""" +$(TYPEDSIGNATURES) + +Select physical matrix coordinates for an indexed line-parameter observation. + +# Arguments + +- `source`: line parameters, series impedance or shunt admittance tensor. +- `request`: normalized observable request with row and column and sample indices, or + diagonal and sample indices for a diagonal request. +- `frequencies`: supplied frequency samples \\[Hz\\] or `nothing`. Line parameters + use their stored frequencies and reject a differing supplied vector. + +# Returns + +- A named tuple retaining the original indices, selected row and column and sample + positions, frequencies \\[Hz\\], coordinate labels, full tensor extent, domain, + and `:matrix` or `:diagonal` representation. Selection order is preserved. +""" +function line_coordinates(source,request,frequencies) + identity=request_identity(request) + diagonal=identity isa Tuple && diag in identity + dimensions=size(source isa LineParameters ? source.Z : source) + indices=request_indices(request) + dims=diagonal ? (dimensions[1],dimensions[3]) : dimensions + selected=map(observation_indices,indices,dims) + rows=selected[1] + columns=diagonal ? copy(rows) : selected[2] + samples=last(selected) + f=_resolution_frequencies(source,frequencies) + f===nothing || length(f)==dimensions[3] || throw(DimensionMismatch("frequency count differs from tensor depth")) + labels=source isa LineParameters ? get(details(source).data,:coordinates,nothing) : nothing + labels=labels===nothing ? string.(1:dimensions[1]) : copy(labels) + return (kind=diagonal ? :diagonal : :matrix,indices,rows,columns,samples, + frequencies=f===nothing ? nothing : copy(f[samples]),frequency_unit=Units.units(:base,:hertz), + extent=dimensions,labels,domain=source isa LineParameters ? nameof(domain(source)) : :unspecified) +end + +function _line_observation_quantity(source::_ObservedLineSource,request; + unit=nothing,clip=true,atol=nothing,frequencies=nothing) + identity=request_identity(request) + prefix=identity isa Tuple ? identity : (identity,) + selector=first(prefix) + indices=request_indices(request) + coordinates=line_coordinates(source,request,frequencies) + f=_resolution_frequencies(source,frequencies) + sampled_f=f===nothing ? nothing : f[last(indices)] + polar=any(in((abs,angle)),prefix) + phase=angle in prefix + diagonal=diag in prefix + original=polar ? (diagonal ? observe(source,selector,diag,indices...) : observe(source,selector,indices...)) : + _line_observation_values(source,request;frequencies=f) + resolution=observation_resolution(original,selector;atol,frequencies=sampled_f, + result_basis=basis(source),line_length=line_length(source)) + values=polar ? (phase ? angle.(original) : abs.(original)) : original + available=resolution.available .& resolution_available.(values) + exact_origin=polar ? iszero.(nominal.(original)) : false + phase && (available=available .& .!exact_origin) + mask=resolution.unresolved===nothing ? false : resolution.unresolved + resolved=broadcast(values,mask,available) do value,unresolved,valid + !valid && return missing + clip && phase && unresolved && return missing + clip && unresolved ? zero(value) : value + end + reasons=broadcast(available,mask,exact_origin) do valid,unresolved,origin + !valid ? (polar && !phase && origin ? :undefined_first_order_magnitude : phase && origin ? :undefined_phase : + selector in (L,C) ? :unavailable_proxy : :nonfinite_value) : + clip && phase && unresolved ? :engineering_zero_phase : nothing + end + q=Commons.request_quantity(request) + native=Units.native_unit(q,basis(source)) + target=unit===nothing ? Units.display_unit(q,basis(source)) : unit + T=typeof(float(real(nominal(zero(Base.nonmissingtype(eltype(original))))))) + factor=Units.scale_factor(native,target,T) + threshold_unit=resolution.unit + threshold_factor=phase ? one(T) : factor + undefined=polar && !phase && any(x -> x===:undefined_first_order_magnitude, + reasons isa AbstractArray ? reasons : (reasons,)) + # Components are retained only for the undefined first-order polar value. + # They are scientific values, never a reference back to the source tensor. + components=undefined ? (nominal_magnitude=Commons.detach(abs.(nominal.(original))),real=Commons.detach(real.(original)),imaginary=Commons.detach(imag.(original)), + unit=Units.native_unit(selector,basis(source))) : nothing + return (request,quantity=q,family=Symbol(nameof(_primary_family(selector))),statistic=:value, + values=Commons.detach(resolved isa AbstractArray && ndims(resolved)==0 ? only(resolved) : resolved,factor),unit=target,basis=basis(source),coordinates, + assumptions=observation_assumptions(source,selector), + thresholds=(kind=resolution.kind,values=Commons.detach(resolution.atol,threshold_factor), + unit=phase ? threshold_unit : target),available,engineering_zero=mask,clipped=clip, + missing_reason=reasons,unavailable_components=components) +end +Commons.observation_quantity(source::LineParameters,request;kwargs...) = + _line_observation_quantity(source,request;kwargs...) +Commons.observation_quantity(source::Union{SeriesImpedance,ShuntAdmittance},request;kwargs...) = + _line_observation_quantity(source,request;kwargs...) + +function Commons.observation_requests(source::CableConstants,requests::Tuple;complete_pairs::Bool=false) + selected=isempty(requests) ? (R,L,G,C) : requests + Commons.validate_observables(source,selected,()) + return (retained=selected,displayed=selected) +end + +function Commons.observation_quantity(source::CableConstants,request; + unit=nothing,clip=true,atol=nothing,frequencies=nothing) + frequencies===nothing || frequencies==[source.frequency] || throw(ArgumentError("frequency differs from cable constants")) + indices=request_indices(request) + length(indices)<=1 || throw(ArgumentError("cable constants select assembly indices")) + index=isempty(indices) ? Colon() : only(indices) + selected=observation_indices(index,length(source)) + selector=request_identity(request) + values=getindex(observe(source,selector),index) + resolution=observation_resolution(values,selector;atol,result_basis=:pul) + resolved = if !clip + values + elseif resolution.unresolved === nothing + resolution.available === false ? missing : values + else + broadcast(values, resolution.unresolved, resolution.available) do value, unresolved, available + available || return missing + return unresolved ? zero(value) : value + end + end + q=Units.quantity(selector) + target=unit===nothing ? Units.display_unit(q,:pul) : unit + T=typeof(float(nominal(zero(eltype(values))))) + factor=Units.scale_factor(Units.native_unit(q,:pul),target,T) + return (request,quantity=q,family=:constants,statistic=:value,values=Commons.detach(resolved,factor), + unit=target,basis=:pul,coordinates=(kind=:assemblies,indices=(index,),assemblies=selected, + labels=copy(source.cores),frequencies=[source.frequency],frequency_unit=Units.units(:base,:hertz),extent=(length(source),1)), + thresholds=(kind=resolution.kind,values=Commons.detach(resolution.atol,factor),unit=target), + available=resolution.available,engineering_zero=resolution.unresolved,clipped=clip,missing_reason=nothing) +end diff --git a/src/engine/lineparameters/quantities.jl b/src/engine/lineparameters/quantities.jl new file mode 100644 index 000000000..f913ffaa3 --- /dev/null +++ b/src/engine/lineparameters/quantities.jl @@ -0,0 +1,34 @@ +# Engine-owned line-parameter quantity and unit accessors. +Units.quantity(::typeof(frequencies)) = Units.Quantity{:frequency}() +Units.quantity(::typeof(Z)) = Units.Quantity{:series_impedance}() +Units.quantity(::typeof(Y)) = Units.Quantity{:shunt_admittance}() +Units.quantity(::typeof(X)) = Units.Quantity{:series_reactance}() +Units.quantity(::typeof(G)) = Units.Quantity{:shunt_conductance}() +Units.quantity(::typeof(B)) = Units.Quantity{:shunt_susceptance}() + +Units.quantity(::typeof(Z), ::typeof(abs)) = + Units.Quantity{(:series_impedance, :magnitude)}() +Units.quantity(::typeof(Z), ::typeof(angle)) = + Units.Quantity{(:series_impedance, :phase_angle)}() +Units.quantity(::typeof(Y), ::typeof(abs)) = + Units.Quantity{(:shunt_admittance, :magnitude)}() +Units.quantity(::typeof(Y), ::typeof(angle)) = + Units.Quantity{(:shunt_admittance, :phase_angle)}() + +Units.quantity(::typeof(Z), ::typeof(diag)) = Units.quantity(Z) +Units.quantity(::typeof(Y), ::typeof(diag)) = Units.quantity(Y) +Units.quantity(::typeof(R), ::typeof(diag)) = Units.quantity(R) +Units.quantity(::typeof(X), ::typeof(diag)) = Units.quantity(X) +Units.quantity(::typeof(L), ::typeof(diag)) = Units.quantity(L) +Units.quantity(::typeof(G), ::typeof(diag)) = Units.quantity(G) +Units.quantity(::typeof(B), ::typeof(diag)) = Units.quantity(B) +Units.quantity(::typeof(C), ::typeof(diag)) = Units.quantity(C) + +Units.quantity(::typeof(Z), ::typeof(absolute_error)) = + Units.Quantity{:series_impedance_absolute_error}() +Units.quantity(::typeof(Z), ::typeof(relative_error)) = + Units.Quantity{:series_impedance_relative_error}() +Units.quantity(::typeof(Y), ::typeof(absolute_error)) = + Units.Quantity{:shunt_admittance_absolute_error}() +Units.quantity(::typeof(Y), ::typeof(relative_error)) = + Units.Quantity{:shunt_admittance_relative_error}() diff --git a/src/engine/lineparameters/resolution.jl b/src/engine/lineparameters/resolution.jl new file mode 100644 index 000000000..056499e62 --- /dev/null +++ b/src/engine/lineparameters/resolution.jl @@ -0,0 +1,209 @@ +# Exact decimal engineering floors in native per-metre units. Convert before +# frequency or length scaling. These are not solver-error estimates. +const _LINE_RESOLUTION_DEFAULTS = (R=1//10^10, L=1//10^15, G=1//10^12, C=1//10^16) +const _LINE_RESOLUTION_QUANTITIES = (Z, Y, R, X, L, G, B, C) + +function _validate_resolution_atol(atol) + atol === nothing && return nothing + limits = atol isa NamedTuple ? values(atol) : (atol,) + if atol isa NamedTuple + all(name -> name in (:R,:X,:L,:G,:B,:C), keys(atol)) || + throw(ArgumentError("atol keys must be R, X, L, G, B, or C; complex zero uses component cutoffs")) + for pair in ((:X,:L),(:B,:C)) + all(key -> haskey(atol,key),pair) && throw(ArgumentError( + "supply only one cutoff for $(join(pair, '/')); their thresholds are linked by 2πf")) + end + end + all(value -> value isa Real && !(value isa Bool) && isfinite(value) && value >= 0, limits) || + throw(ArgumentError("atol must be finite and nonnegative")) + return nothing +end + +_resolution_frequencies(source, supplied) = supplied +function _resolution_frequencies(source::LineParameters, supplied) + supplied === nothing || supplied == source.f || throw(ArgumentError( + "supplied frequencies must match the stored LineParameters frequency axis")) + return source.f +end + +_cutoff_number(::Type{T}, value::Rational) where {T} = T(value) +_cutoff_number(::Type{T}, value::Real) where {T} = convert(promote_type(T,typeof(float(value))),value) + +function _line_resolution_tolerance(quantity, ::Type{T}, f, atol; + result_basis=:pul, line_length=nothing) where {T} + name = nameof(quantity) + atol isa Real && name in (:Z,:Y) && throw(ArgumentError( + "complex zero requires component cutoffs, for example atol=(R=0, X=0)")) + overrides = atol === nothing ? (;) : atol isa NamedTuple ? atol : NamedTuple{(name,)}((atol,)) + component = function (selector) + key=nameof(selector) + haskey(overrides,key) && return _cutoff_number(T,overrides[key]) + partner = key === :X ? :L : key === :L ? :X : key === :B ? :C : key === :C ? :B : nothing + linked = key in (:X,:B) || (partner !== nothing && haskey(overrides,partner)) + if linked + f === nothing && return nothing + angular = T(2)*T(π) .* T.(nominal.(f)) + other = _line_resolution_tolerance(getfield(@__MODULE__,partner),T,f,overrides; + result_basis,line_length) + other === nothing && return nothing + # L/C at zero frequency are unavailable. Never divide by zero. + return key in (:X,:B) ? angular .* other : + broadcast((cutoff,w) -> iszero(w) ? zero(cutoff) : cutoff/w,other,angular) + end + cutoff=T(getproperty(_LINE_RESOLUTION_DEFAULTS,key)) + result_basis === :pul && return cutoff + line_length === nothing && throw(ArgumentError( + "total $key reporting requires retained line length or an explicit total-unit cutoff")) + length_value=nominal(line_length) + isfinite(length_value) && length_value>0 || throw(ArgumentError("line length must be finite and positive")) + return cutoff * length_value + end + name === :Z && return (R=component(R),X=component(X)) + name === :Y && return (G=component(G),B=component(B)) + return component(quantity) +end + +"""Return whether a scalar has finite nominal and first-order uncertainty values.""" +resolution_available(value) = false +resolution_available(value::Real) = isfinite(nominal(value)) && isfinite(uncertainty(value)) +resolution_available(value::Complex) = resolution_available(real(value)) && resolution_available(imag(value)) +_resolution_unresolved(value::Number, tolerance::Real) = + resolution_available(value) && abs(nominal(value)) <= tolerance +_resolution_unresolved(value, tolerance) = false + +_aligned_cutoff(tolerance::Real, values) = tolerance +_aligned_cutoff(tolerance::AbstractArray, values::Number) = only(tolerance) +_aligned_cutoff(tolerance::AbstractArray, values::AbstractArray) = + reshape(tolerance, (ntuple(_ -> 1,ndims(values)-1)...,length(tolerance))) +_cutoff_available(value::Real) = isfinite(value) && value>=0 +_cutoff_available(value::AbstractArray) = all(_cutoff_available,value) +_cutoff_available(value::NamedTuple) = all(_cutoff_available,values(value)) +_cutoff_available(::Nothing) = false + +""" +$(TYPEDSIGNATURES) + +Classify original quantities using `abs(nominal(value)) ≤ cutoff`. Complex zero +requires both Cartesian components to satisfy their cutoffs. Availability is +separate. Physical standard uncertainty never contributes to a reporting floor. + +# Keywords + +- `atol`: component cutoffs in native basis units. +- `frequencies`: aligned frequencies \\[Hz\\] for linked X/L and B/C cutoffs. +- `result_basis=:pul`: per-metre or `:total` quantities. +- `line_length`: physical length \\[m\\] for scaling default total-unit floors. + +# Returns + +- Applied thresholds, native units, and aligned availability and nominal-zero + masks. An unknown frequency-dependent threshold is explicitly unassessed. +""" +function observation_resolution(values::Union{Number,AbstractArray}, selector::Function; + atol=nothing, frequencies=nothing, result_basis::Symbol=:pul, line_length=nothing) + _validate_resolution_atol(atol) + validate(result_basis, LineParameters) + selector in _LINE_RESOLUTION_QUANTITIES || + return observation_resolution(nothing,selector;atol,frequencies) + if frequencies isa Real + isfinite(frequencies) && frequencies>=0 || throw(ArgumentError("frequency must be finite and nonnegative in Hz")) + elseif frequencies !== nothing + frequencies isa AbstractVector && all(f -> f isa Real && isfinite(f) && f>=0,frequencies) || + throw(ArgumentError("frequencies must be finite and nonnegative in Hz")) + (values isa Number ? 1 : size(values,ndims(values))) == length(frequencies) || + throw(DimensionMismatch("frequencies must match the quantity depth")) + end + T=typeof(float(real(nominal(zero(Base.nonmissingtype(eltype(values))))))) + tolerance=_line_resolution_tolerance(selector,T,frequencies,atol;result_basis,line_length) + assessed = tolerance !== nothing && (!(tolerance isa NamedTuple) || all(!isnothing,Base.values(tolerance))) + available=resolution_available.(values) + if selector in (L,C) && frequencies!==nothing + available = available .& (.!iszero.(_aligned_cutoff(frequencies,values))) + end + assessed || return (kind=:unassessed,atol=nothing,unit=Units.native_unit(selector,result_basis), + unresolved=nothing,available) + _cutoff_available(tolerance) || throw(ArgumentError("cutoffs must remain finite and nonnegative")) + unresolved = if selector in (Z,Y) + first_limit,last_limit=Base.values(tolerance) + _resolution_unresolved.(real.(values),_aligned_cutoff(first_limit,values)) .& + _resolution_unresolved.(imag.(values),_aligned_cutoff(last_limit,values)) + else + _resolution_unresolved.(values,_aligned_cutoff(tolerance,values)) + end + return (kind=:declared_floor,atol=tolerance,unit=Units.native_unit(selector,result_basis), + unresolved=unresolved .& available,available) +end + +observation_resolution(source::Union{SeriesImpedance,ShuntAdmittance},request::Function; + atol=nothing,frequencies=nothing) = observation_resolution(source,(request,);atol,frequencies) + +# Observation and comparison admit unavailable DC proxies without changing the +# strict raw L/C accessors used by numerical algorithms. +function _line_observation_values(source, request; frequencies=nothing) + identity=request_identity(request) + selector=identity isa Tuple ? first(identity) : identity + indices=request_indices(request) + if selector in (L,C) && source isa Union{LineParameters,SeriesImpedance,ShuntAdmittance} + f=_resolution_frequencies(source,frequencies) + if source isa Union{SeriesImpedance,ShuntAdmittance} && !isempty(indices) && length(indices)>3 && first(indices) isa AbstractVector{<:Real} + f=first(indices) + indices=Base.tail(indices) + end + f===nothing && throw(ArgumentError("L/C observations require frequencies in Hz")) + transforms=identity isa Tuple ? Base.tail(identity) : () + diagonal=diag in transforms + proxy=selector===L ? X : B + values=diagonal ? observe(source,proxy,diag,indices...) : observe(source,proxy,indices...) + sample=length(indices)==(diagonal ? 2 : 3) ? last(indices) : Colon() + selected=f[sample] + aligned=_aligned_cutoff(selected,values) + T=typeof(float(real(nominal(zero(eltype(values)))))) + V=typeof(zero(eltype(values))/(T(2)*T(π)*one(eltype(f)))) + output=Array{Union{Missing,V}}(undef,values isa Number ? () : size(values)) + broadcast!(output,values,aligned) do value,frequency + iszero(frequency) && return missing + value/(T(2)*T(π)*frequency) + end + return output + end + return request isa Function ? observe(source,request) : observe(source,request...) +end + +""" +$(TYPEDSIGNATURES) + +Classify an owned line observation in native units, using original complex +components for polar requests. +""" +function observation_resolution(source::Union{AbstractCoreResult,SeriesImpedance,ShuntAdmittance}, + request;atol=nothing,frequencies=nothing) + _validate_resolution_atol(atol) + identity=request_identity(request) + selector=identity isa Tuple ? first(identity) : identity + selector in _LINE_RESOLUTION_QUANTITIES || return observation_resolution(nothing,request;atol,frequencies) + f=_resolution_frequencies(source,frequencies) + indices=request_indices(request) + if source isa Union{SeriesImpedance,ShuntAdmittance} && selector in (L,C) && !isempty(indices) + embedded=first(indices) + f===nothing || f==embedded || throw(ArgumentError("request and supplied frequencies differ")) + f=embedded + indices=Base.tail(indices) + end + if f!==nothing + f isa AbstractVector && all(x -> x isa Real && isfinite(x) && x>=0,f) || + throw(ArgumentError("frequencies must be finite and nonnegative in Hz")) + parent=selector in (Z,R,X,L) ? Z : Y + size(observe(source,parent),3)==length(f) || throw(DimensionMismatch("frequencies must match tensor depth")) + end + transforms=identity isa Tuple ? Base.tail(identity) : () + polar=any(transform -> transform in (abs,angle),transforms) + diagonal=diag in transforms + values = if polar + diagonal ? observe(source,selector,diag,indices...) : observe(source,selector,indices...) + else + _line_observation_values(source,request;frequencies=f) + end + sample=length(indices)==(diagonal ? 2 : 3) ? last(indices) : Colon() + return observation_resolution(values,selector;atol,frequencies=f===nothing ? nothing : f[sample], + result_basis=basis(source),line_length=line_length(source)) +end diff --git a/src/engine/lineparamopts.jl b/src/engine/lineparamopts.jl deleted file mode 100644 index d3e03c044..000000000 --- a/src/engine/lineparamopts.jl +++ /dev/null @@ -1,88 +0,0 @@ -Base.@kwdef struct LineParamOptions - "Skip user confirmation for overwriting results" - force_overwrite::Bool = false - "Reduce bundle conductors to equivalent single conductor" - reduce_bundle::Bool = true - "Eliminate grounded conductors from the system (Kron reduction)" - kron_reduction::Bool = true - "Enforce ideal transposition/snaking" - ideal_transposition::Bool = true - "Temperature correction" - temperature_correction::Bool = true - "Store primitive matrices" - store_primitive_matrices::Bool = true - "Verbosity level" - verbosity::Int = 0 - "Log file path" - logfile::Union{String, Nothing} = nothing -end - - -# --- Helpers to turn anything into a NamedTuple ---------------------------- - -_to_nt(nt::NamedTuple) = nt -_to_nt(p::Base.Pairs) = (; p...) -_to_nt(d::AbstractDict) = (; d...) -_to_nt(::Nothing) = (;) - -# --- Generic key splitter + builder --------------------------------------- - -const _COMMON_KEYS = Set(fieldnames(LineParamOptions)) - -_select_keys(nt::NamedTuple, allowed::Set{Symbol}) = - (; (k => v for (k, v) in pairs(nt) if k in allowed)...) - -function build_options(::Type{O}, opts; - strict::Bool = true, -) where {O <: AbstractFormulationOptions} - - nt = _to_nt(opts) - - own_allowed = Set(filter(!=(:common), fieldnames(O))) - common_nt = _select_keys(nt, _COMMON_KEYS) - own_nt = _select_keys(nt, own_allowed) - - unknown = setdiff(Set(keys(nt)), union(_COMMON_KEYS, own_allowed)) - if strict && !isempty(unknown) - throw(ArgumentError("Unknown option keys for $(O): $(collect(unknown))")) - end - - return O(; common = LineParamOptions(; common_nt...), own_nt...) -end - -# Convenience overloads (accept already-built things) -build_options(::Type{O}, o::O; kwargs...) where {O <: AbstractFormulationOptions} = o -build_options( - ::Type{O}, - c::LineParamOptions; - kwargs..., -) where {O <: AbstractFormulationOptions} = O(; common = c) - -# save_path stays solver-specific (different sensible defaults). -Base.@kwdef struct EMTOptions <: AbstractFormulationOptions - common::LineParamOptions = LineParamOptions() - "Save path for output files" - save_path::String = joinpath(".", "lineparams_output") -end - -const _COMMON_SYMS = Tuple(fieldnames(LineParamOptions)) -const _EMT_OWN = Tuple(s for s in fieldnames(EMTOptions) if s != :common) -@inline Base.hasproperty(::EMTOptions, s::Symbol) = - (s in _EMT_OWN) || (s in _COMMON_SYMS) || s === :common - -@inline function Base.getproperty(o::EMTOptions, s::Symbol) - s === :common && return getfield(o, :common) - (s in _EMT_OWN) && return getfield(o, s) # EMT-specific - (s in _COMMON_SYMS) && return getfield(o.common, s) # forwarded common - throw(ArgumentError("Unknown option $(s) for $(typeof(o))")) -end - -Base.propertynames(::EMTOptions, ::Bool = false) = (_COMMON_SYMS..., _EMT_OWN..., :common) -Base.get(o::EMTOptions, s::Symbol, default) = - hasproperty(o, s) ? getproperty(o, s) : default -asnamedtuple(o::EMTOptions) = (; (k=>getproperty(o, k) for k in propertynames(o))...) - - - - - diff --git a/src/engine/lineparams.jl b/src/engine/lineparams.jl deleted file mode 100644 index 5c09a92e0..000000000 --- a/src/engine/lineparams.jl +++ /dev/null @@ -1,118 +0,0 @@ -struct SeriesImpedance{T} <: AbstractArray{T, 3} - values::Array{T, 3} # n×n×nfreq, units: Ω/m -end - -struct ShuntAdmittance{T} <: AbstractArray{T, 3} - values::Array{T, 3} # n×n×nfreq, units: S/m -end - -""" -$(TYPEDEF) - -Represents the frequency-dependent line parameters (series impedance and shunt admittance matrices) for a cable or line system. - -$(TYPEDFIELDS) -""" -struct LineParameters{T <: COMPLEXSCALAR, U <: REALSCALAR, D <: LineParamsDomain} - "Series impedance matrices \\[Ω/m\\]." - Z::SeriesImpedance{T} - "Shunt admittance matrices \\[S/m\\]." - Y::ShuntAdmittance{T} - "Frequencies \\[Hz\\]." - f::Vector{U} - - @doc """ - $(TYPEDSIGNATURES) - - Constructs a [`LineParameters`](@ref) instance. - - # Arguments - - - `Z`: Series impedance matrices \\[Ω/m\\]. - - `Y`: Shunt admittance matrices \\[S/m\\]. - - `f`: Frequencies \\[Hz\\]. - - # Returns - - - A [`LineParameters`](@ref) object with prelocated impedance and admittance matrices for a given frequency range. - - # Examples - - ```julia - params = $(FUNCTIONNAME)(Z, Y, f) - ``` - """ - function LineParameters( - ::Type{D}, - Z::SeriesImpedance{T}, - Y::ShuntAdmittance{T}, - f::AbstractVector{U}, - ) where {D <: LineParamsDomain, T <: COMPLEXSCALAR, U <: REALSCALAR} - size(Z, 1) == size(Z, 2) || throw(DimensionMismatch("Z must be square")) - size(Y, 1) == size(Y, 2) || throw(DimensionMismatch("Y must be square")) - size(Z, 3) == size(Y, 3) == length(f) || - throw(DimensionMismatch("Z and Y must have same dimensions (n×n×nfreq)")) - new{T, U, D}(Z, Y, Vector{U}(f)) - end - - # Backward-compatible constructor: defaults to PhaseDomain - LineParameters( - Z::SeriesImpedance{T}, - Y::ShuntAdmittance{T}, - f::AbstractVector{U}, - ) where {T <: COMPLEXSCALAR, U <: REALSCALAR} = - LineParameters(PhaseDomain, Z, Y, f) -end - -SeriesImpedance(A::AbstractArray{T, 3}) where {T} = SeriesImpedance{T}(Array(A)) -ShuntAdmittance(A::AbstractArray{T, 3}) where {T} = ShuntAdmittance{T}(Array(A)) - -# --- Outer convenience constructors ------------------------------------------- - -""" -$(TYPEDSIGNATURES) - -Construct from 3D arrays and frequency vector. Arrays are wrapped -into `SeriesImpedance` and `ShuntAdmittance` automatically. -""" -LineParameters( - ::Type{D}, - Z::AbstractArray{Tc, 3}, - Y::AbstractArray{Tc, 3}, - f::AbstractVector{U}, -) where {D <: LineParamsDomain, Tc <: COMPLEXSCALAR, U <: REALSCALAR} = - LineParameters(D, SeriesImpedance(Z), ShuntAdmittance(Y), f) - - -# Backward-compatible constructor: defaults to PhaseDomain -LineParameters( - Z::AbstractArray{Tc, 3}, - Y::AbstractArray{Tc, 3}, - f::AbstractVector{U}, -) where {Tc <: COMPLEXSCALAR, U <: REALSCALAR} = - LineParameters(PhaseDomain, Z, Y, f) - - -# """ -# $(TYPEDSIGNATURES) - -# Backward-compatible constructor without frequencies. A dummy equally-spaced -# `Vector{BASE_FLOAT}` is used with length `size(Z,3)`. -# """ -# function LineParameters( -# Z::AbstractArray{Tc, 3}, -# Y::AbstractArray{Tc, 3}, -# ) where {Tc <: COMPLEXSCALAR} -# nfreq = size(Z, 3) -# (size(Y, 3) == nfreq) || throw(DimensionMismatch("Z and Y must have same nfreq")) -# # Provide a placeholder frequency vector to preserve legacy call sites -# f = collect(BASE_FLOAT.(1:nfreq)) -# return LineParameters(SeriesImpedance(Z), ShuntAdmittance(Y), f) -# end - -# --- Tiny domain extractors --------------------------------------------------- -""" -Return the domain tag type for `LineParameters` objects. -""" -@inline domain(::Type{<:LineParameters{T, U, D}}) where {T, U, D <: LineParamsDomain} = D -@inline domain(lp::LineParameters) = domain(typeof(lp)) diff --git a/src/engine/modalanalysis/ModalAnalysis.jl b/src/engine/modalanalysis/ModalAnalysis.jl new file mode 100644 index 000000000..0550f5091 --- /dev/null +++ b/src/engine/modalanalysis/ModalAnalysis.jl @@ -0,0 +1,71 @@ +""" + LineCableModels.Engine.ModalAnalysis + +Transform fully coupled line-parameter matrices between phase and modal +coordinate domains independently of the backend that calculated them. + +# Dependencies + +$(IMPORTS) +""" +module ModalAnalysis + +export ModalAnalysisProblem, ModalAnalysisFormulation +export LineCableModelsModal, ModalOperators, Formula +export operators, Tv, Ti, gamma, alpha, beta, velocity, Zc, Yc, H, PropagationParameters, transform +export formula_id, formulas + +#! explicit-imports: off +using DocStringExtensions: IMPORTS, TYPEDEF, TYPEDFIELDS, TYPEDSIGNATURES +import ...LineCableModels: Expression, nominal, FormulaDefinition, formula, parameterize, validate +import ...LineCableModels: line_length +import ...LineCableModels: Grid, Gridpoint, Gridspace +import ...LineCableModels +import ...Commons: AbstractProblemDefinition, AbstractFormulation, + FormulationOptions, ComputationOptions, ComputationDetails, + compute, computation_options, computation_details, formulation_options, details, + initialize_buffers, formulas +import ...Commons: AbstractResultSpace +import ...Commons: observe, observables, request_identity, request_indices, observation_indices +import ..Engine: LineParameters, LineParametersFormulation, LineParametersProblem, + PhaseDomain, ModalDomain, SeriesImpedance, ShuntAdmittance, basis, frequencies, + description, formula_id, selectdomain, selectdetails +using LinearAlgebra: Diagonal, I, checksquare, cond, diag, dot, eigen, + eigen!, issuccess, ldiv!, lu!, mul!, norm, rdiv!, svd!, svdvals! +import ...Commons: AbstractCoreResult +import ..Engine +import ...Commons +using ...Commons: vacuum_permittivity, vacuum_permeability +import ...Units +import ...TextDisplay +#! explicit-imports: on + +include("interfaces.jl") +include("problems.jl") + +include("formulations.jl") +public decompose! +include("eigensystems.jl") +include("compute.jl") +include("composition.jl") + +#! explicit-imports: off +const FORMULAS = ( + include("formulas/chrysochos2014.jl"), + include("formulas/vieira2026.jl"), + include("formulas/wedepohl1996.jl"), + include("formulas/default.jl"), +) +#! explicit-imports: on + +""" +Return the built-in modal-transformation formula identifiers. +""" +formulas(::Type{<:Formula}) = FORMULAS + +include("quantities.jl") +include("propagation.jl") +include("observations.jl") +include("textdisplay.jl") + +end # module ModalAnalysis diff --git a/src/engine/modalanalysis/composition.jl b/src/engine/modalanalysis/composition.jl new file mode 100644 index 000000000..60b3978b9 --- /dev/null +++ b/src/engine/modalanalysis/composition.jl @@ -0,0 +1,25 @@ +# A third positional argument composes a line-parameter computation with modal +# analysis of its result, whatever formulation computed it. Each upstream wrapper +# keeps its original no-modal execution path. +const _ModalSelection=Union{ModalAnalysisFormulation, + Gridspace{<:ModalAnalysisFormulation}} + +function compute(problem::Union{LineParametersProblem, + Gridspace{<:LineParametersProblem},Gridpoint{<:LineParametersProblem}}, + formulation,modal::_ModalSelection; + options::Union{NamedTuple,ComputationOptions}=ComputationOptions(), + modal_options::Union{NamedTuple,ComputationOptions}=ComputationOptions()) + phase=compute(problem,formulation;options) + return _modal_results(phase,modal,modal_options) +end + +function _modal_results(phase::LineParameters,modal,options) + return compute(ModalAnalysisProblem(phase),modal;options) +end +function _modal_results(phase::AbstractResultSpace,modal,options) + return compute(Gridspace{ModalAnalysisProblem}(phase),modal;options) +end +function _modal_results(phase::AbstractVector{<:LineParameters},modal,options) + problems=Gridspace{ModalAnalysisProblem}(ModalAnalysisProblem,(Grid(phase),)) + return compute(problems,modal;options).values +end diff --git a/src/engine/modalanalysis/compute.jl b/src/engine/modalanalysis/compute.jl new file mode 100644 index 000000000..8d843040c --- /dev/null +++ b/src/engine/modalanalysis/compute.jl @@ -0,0 +1,258 @@ +function offdiagonal_ratio(matrix::AbstractMatrix) + n = checksquare(matrix) + n > 0 || throw(ArgumentError("modal matrix must be nonempty")) + first_value = nominal(matrix[1, 1]) + matrix_norm_squared = zero(real(abs2(first_value))) + offdiagonal_norm_squared = zero(matrix_norm_squared) + @inbounds for column in 1:n, row in 1:n + squared = abs2(nominal(matrix[row, column])) + matrix_norm_squared += squared + row == column || (offdiagonal_norm_squared += squared) + end + matrix_norm = sqrt(matrix_norm_squared) + return sqrt(offdiagonal_norm_squared) / + max(matrix_norm, eps(float(real(matrix_norm)))) +end + +# The operators transform the coefficients of `parameters` between modal and phase domains. +function validate(maps::ModalOperators, parameters::LineParameters) + expected = size(parameters.Z.values) + size(maps.Tv) == expected || throw(DimensionMismatch("Tv must have size $expected")) + size(maps.Ti) == expected || throw(DimensionMismatch("Ti must have size $expected")) + all(isfinite, maps.Tv) || throw(DomainError(maps.Tv, "Tv must be finite")) + all(isfinite, maps.Ti) || throw(DomainError(maps.Ti, "Ti must be finite")) + return maps +end + +function computation_options(::Type{LineCableModelsModal}, record::ComputationOptions)::ComputationOptions + options = record.data + isempty(setdiff(keys(options), (:rotate, :offdiagonal_tolerance, :on_result, :timing, :verbosity))) || + throw(ArgumentError("unknown modal action options: $(Tuple(keys(options)))")) + tolerance = get(options, :offdiagonal_tolerance, 1e-6) + tolerance isa Real && isfinite(tolerance) && tolerance >= 0 || + throw(ArgumentError("offdiagonal_tolerance must be finite and nonnegative")) + callback = get(options, :on_result, nothing) + (callback === nothing || callback isa Function) || + throw(ArgumentError("on_result must be a callable function or nothing")) + timing = get(options, :timing, false) + timing isa Bool || throw(ArgumentError("timing must be Bool")) + rotate = get(options, :rotate, true) + rotate isa Bool || throw(ArgumentError("rotate must be Bool")) + return ComputationOptions(; offdiagonal_tolerance=tolerance, on_result=callback, + timing, rotate, verbosity=get(options, :verbosity, 0)) +end + +"""Store one scalar modal computation's results, common arrays, selected work, and diagnostics.""" +struct ModalAnalysisWorkspace{P,I,V,R,Z,Y,B,D} + source::P + input::I + Tv::V + Ti::V + roots::R + Zm::Z + Ym::Y + buffers::B + diagnostics::D +end + +formula_parameters(selected::Formula) = selected.parameters +formula_parameters(::AbstractFormulation) = (;) + +function ModalAnalysisWorkspace(source::LineParameters, selected::AbstractFormulation) + Zp = nominal(source.Z.values) + Yp = nominal(source.Y.values) + n, _, nf = size(Zp) + ell0 = line_length(source) + factor = one(real(zero(eltype(Zp)))) + if basis(source) === :total && ell0 !== nothing + ell0 isa Real && !(ell0 isa Bool) && isfinite(nominal(ell0)) && nominal(ell0) > 0 || + throw(ArgumentError("total coefficients require a positive source normalization length")) + factor = nominal(ell0) + end + input = (Z=Zp, Y=Yp, f=nominal(source.f), + root_scale=factor, source_basis=basis(source)) + T = float(promote_type(eltype(input.Z), eltype(input.Y))) + Tv = Array{T,3}(undef, n, n, nf) + Ti = similar(Tv) + roots = Array{T,2}(undef, n, nf) + S = promote_type(eltype(source.Z.values), eltype(Tv)) + Zm = Array{S,3}(undef, n, n, nf) + Ym = similar(Zm) + plan = (; n, nf) + common = (Zslice=Matrix{T}(undef,n,n),Yslice=Matrix{T}(undef,n,n), + coordinate_product=Matrix{S}(undef,n,n), + coordinate_factor=Matrix{T}(undef,n,n), + admittance_impedance_product=Matrix{T}(undef,n,n), + previous_eigenvalues=Vector{T}(undef,n),eigenvalues=Vector{T}(undef,n), + previous_eigenvectors=Matrix{T}(undef,n,n),eigenvectors=Matrix{T}(undef,n,n), + propagation_eigenvalues=Vector{T}(undef,n),voltage_vector=Vector{T}(undef,n)) + buffers = initialize_buffers(selected, T, input, plan, common) + R=typeof(real(zero(T))) + residual=Matrix{Union{Nothing,R}}(undef,n,nf) + iteration_counts=Matrix{Union{Nothing,Int}}(undef,n,nf) + convergence=Matrix{Union{Nothing,Bool}}(undef,n,nf) + fill!(residual,nothing) + fill!(iteration_counts,nothing) + fill!(convergence,nothing) + diagnostics = (fallback_frequencies=Int[], missed_frequencies=Int[], + z_coupling=Vector{typeof(real(zero(T)))}(undef, nf), + y_coupling=Vector{typeof(real(zero(T)))}(undef, nf), + eigen_residual=residual,iterations=iteration_counts,converged=convergence) + return ModalAnalysisWorkspace(source, input, Tv, Ti, roots, Zm, Ym, buffers, diagnostics) +end + +function decompose! end + +function _coordinate_algebra!(workspace::ModalAnalysisWorkspace, execution) + source = workspace.source + nfreq = size(workspace.Zm, 3) + tolerance = execution.data.offdiagonal_tolerance + product=workspace.buffers.coordinate_product + factor=workspace.buffers.coordinate_factor + for frequency in 1:nfreq + Tvf = @view workspace.Tv[:, :, frequency] + Tif = @view workspace.Ti[:, :, frequency] + Zp = @view source.Z.values[:, :, frequency] + Yp = @view source.Y.values[:, :, frequency] + mul!(product,Zp,Tif) + copyto!(factor,Tvf) + ldiv!(lu!(factor),product) + copyto!(@view(workspace.Zm[:,:,frequency]),product) + mul!(product,Yp,Tvf) + copyto!(factor,Tif) + ldiv!(lu!(factor),product) + copyto!(@view(workspace.Ym[:,:,frequency]),product) + zr = offdiagonal_ratio(@view(workspace.Zm[:, :, frequency])) + yr = offdiagonal_ratio(@view(workspace.Ym[:, :, frequency])) + workspace.diagnostics.z_coupling[frequency] = zr + workspace.diagnostics.y_coupling[frequency] = yr + if zr > tolerance || yr > tolerance + @warn "modal coefficients exceed the requested off-diagonal tolerance" frequency zr yr tolerance + end + end + return workspace +end + +# Differentiate the diagonal product at the retained nominal branch. The maps +# stay nominal. Uncertainty in the transformed coefficients remains connected +# to its original scalar sources. +function _dependent_roots(workspace::ModalAnalysisWorkspace) + nominal_roots=workspace.roots + eltype(workspace.Zm)===eltype(nominal_roots) && return copy(nominal_roots) + S=promote_type(eltype(workspace.Zm),eltype(nominal_roots)) + roots=Array{S}(undef,size(nominal_roots)) + for frequency in axes(roots,2), mode in axes(roots,1) + root=nominal_roots[mode,frequency] + iszero(root) && throw(DomainError(root, + "first-order modal root is undefined at a zero nominal root")) + product=workspace.Zm[mode,mode,frequency]*workspace.Ym[mode,mode,frequency] + roots[mode,frequency]=root+(product-nominal(product))/(2root) + end + return roots +end + +# Keep the completed record's outer key and type layout inferable even when an +# upstream gridpoint or formula description is intentionally type-erased. +@generated function _modal_detail_merge(record::NamedTuple{Names,Types}, + extra::NamedTuple{ExtraNames,ExtraTypes}) where {Names,Types,ExtraNames,ExtraTypes} + retained=filter(name -> name ∉ ExtraNames && + name ∉ (:timing,:comparison_unsupported),Names) + names=(retained...,ExtraNames...) + types=(map(name -> fieldtype(Types,findfirst(==(name),Names)),retained)..., + fieldtypes(ExtraTypes)...) + compact=map(type -> type<:NamedTuple ? NamedTuple : type,types) + output=NamedTuple{names,Tuple{compact...}} + entries=[:(getproperty(record,$(QuoteNode(name)))) for name in retained] + append!(entries,[:(getproperty(extra,$(QuoteNode(name)))) for name in ExtraNames]) + return :(ComputationDetails($output(($(entries...),)))) +end + +Base.@constprop :aggressive function _modal_completion(parameters, formulation, selected, diagnostics, execution) + source_record=parameters.details.data + source_fields=get(source_record,:formulation_fields,(;)) + modal_descriptions=Engine.completed_formulation(formulation).formulation_fields.all + modal_descriptions=filter(field -> !isempty(field.meaning),modal_descriptions) + descriptions=vcat(get(source_fields,:all,NamedTuple[]),modal_descriptions) + fields=merge(source_fields, + (all=descriptions,Z=copy(descriptions),Y=copy(descriptions))) + added=NamedTuple{(:source_gridpoint,:phase_coordinates,:coordinates,:modal,:formulation_fields), + Tuple{Any,Any,Vector{String},NamedTuple,NamedTuple}}(( + get(parameters.details.data, :gridpoint, nothing), + get(parameters.details.data, :coordinates, nothing), + string.(1:size(parameters.Z,1)), + (identifier=formula_id(selected), requested=NamedTuple(formulation.definition), + effective=NamedTuple(selected), rotate=execution.data.rotate, diagnostics=diagnostics), + fields)) + return _modal_detail_merge(source_record,added) +end + +function compute(::LineCableModelsModal, + problem::ModalAnalysisProblem{<:LineParameters{T,U,PhaseDomain,Basis}}, + formulation::ModalAnalysisFormulation, execution::ComputationOptions) where {T,U,Basis} + parameters=problem.parameters + selected = formulation.formula + workspace = ModalAnalysisWorkspace(parameters, selected) + decompose!(selected, workspace, formula_parameters(selected), + formulation_options(selected)) + validate(ModalOperators(workspace.Tv, workspace.Ti), parameters) + orient_modes!(workspace.Tv, workspace.Ti, execution.data.rotate) + maps = ModalOperators(copy(workspace.Tv), copy(workspace.Ti)) + _coordinate_algebra!(workspace, execution) + roots = _dependent_roots(workspace) + all(isfinite, roots) || throw(DomainError(roots, "modal roots must be finite")) + modal = ModalDomain(maps, roots) + diagnostics = map(copy,workspace.diagnostics) + retained = _modal_completion(parameters, formulation, selected, diagnostics, execution) + return LineParameters(modal, + SeriesImpedance{eltype(workspace.Zm),Basis}(copy(workspace.Zm)), + ShuntAdmittance{eltype(workspace.Ym),Basis}(copy(workspace.Ym)), + parameters.f, retained) +end + +"""Restore phase coefficients using the retained modal-to-phase bases.""" +function transform(::Type{PhaseDomain}, parameters::LineParameters{T,U,D,Basis}) where {T,U,D<:ModalDomain,Basis} + maps = validate(parameters.domain.operators, parameters) + dimensions = size(parameters.Z.values) + S = promote_type(T, eltype(maps.Tv), eltype(maps.Ti)) + impedance = Array{S,3}(undef, dimensions) + admittance = similar(impedance) + for frequency in axes(impedance, 3) + Tvf = @view maps.Tv[:, :, frequency] + Tif = @view maps.Ti[:, :, frequency] + @views impedance[:, :, frequency] .= (Tvf * parameters.Z.values[:, :, frequency]) / Tif + @views admittance[:, :, frequency] .= (Tif * parameters.Y.values[:, :, frequency]) / Tvf + end + retained = ComputationDetails(merge(parameters.details.data, + (coordinates=get(parameters.details.data,:phase_coordinates,nothing),))) + return LineParameters(PhaseDomain, + SeriesImpedance{S,Basis}(impedance), + ShuntAdmittance{S,Basis}(admittance), parameters.f, + retained) +end + +function compute(problem::ModalAnalysisProblem{P}, formulation::ModalAnalysisFormulation; + options::Union{NamedTuple,ComputationOptions}=ComputationOptions()) where {P} + return compute(LineCableModelsModal(), problem, formulation; options) +end + +function compute(::LineCableModelsModal, problem::ModalAnalysisProblem{P}, + formulation::ModalAnalysisFormulation; + options::Union{NamedTuple,ComputationOptions}=ComputationOptions()) where {P} + options = options isa NamedTuple ? ComputationOptions(options) : options + execution = computation_options(LineCableModelsModal, options) + result = if execution.data.timing + measured = Base.@timed compute(LineCableModelsModal(),problem,formulation,execution) + Engine.retain_gridpoint(measured.value, details(measured.value).data.gridpoint; + fields=(timing=(wall_seconds=measured.time, bytes=measured.bytes, + gc_seconds=measured.gctime, compile_seconds=measured.compile_time, + recompile_seconds=measured.recompile_time),)) + else + compute(LineCableModelsModal(),problem,formulation,execution) + end + execution.data.on_result === nothing || execution.data.on_result(problem, 1, result) + return result +end + +function computation_details(::Type{<:ModalAnalysisFormulation}, result::LineParameters)::ComputationDetails + return details(result) +end diff --git a/src/engine/modalanalysis/eigensystems.jl b/src/engine/modalanalysis/eigensystems.jl new file mode 100644 index 000000000..e8a0f9091 --- /dev/null +++ b/src/engine/modalanalysis/eigensystems.jl @@ -0,0 +1,281 @@ +# Shared numerical operations for frequency-tracked eigensystems. + +# Minimize the current column's imaginary norm, preserve the voltage and current +# pairing, then resolve the remaining sign against the previous frequency. +function orient_modes!(voltage::AbstractArray{T, 3}, current::AbstractArray{T, 3}, + rotate::Bool) where {T <: Complex} + R=typeof(real(zero(T))) + for frequency in axes(current, 3), mode in axes(current, 2) + + vector=@view current[:, mode, frequency] + phase=one(T) + scale=maximum(value -> max(abs(real(value)), abs(imag(value))), vector) + if rotate && !iszero(scale) + real_sum=zero(R) + imaginary_sum=zero(R) + for value in vector + scaled=value/scale + real_sum += real(scaled)^2-imag(scaled)^2 + imaginary_sum += 2real(scaled)*imag(scaled) + end + phase=cis(-atan(imaginary_sum, real_sum)/2) + end + if frequency>firstindex(current, 3) && !iszero(scale) + previous=@view current[:, mode, frequency - 1] + previous_scale=maximum(value -> max(abs(real(value)), abs(imag(value))), previous) + if !iszero(previous_scale) + overlap=zero(T) + for row in eachindex(vector) + overlap += conj(previous[row]/previous_scale)*(vector[row]/scale) + end + real(overlap*phase) magnitude + pivot = index + magnitude = component_magnitude + end + end + iszero(magnitude) && return false + vector .*= conj(vector[pivot]) / magnitude + return true +end + +function _align!(vector::AbstractVector, reference::AbstractVector) + _unit!(vector) || return false + overlap = dot(reference, vector) + scale = abs(overlap) + if scale > sqrt(eps(typeof(real(scale)))) + vector .*= conj(overlap) / scale + return true + end + return _orient!(vector) +end + +function normalize_bilinear!(vector::AbstractVector{T}) where {T <: Complex} + squared = zero(T) + magnitude = zero(typeof(real(zero(T)))) + @inbounds for value in vector + squared += value * value + magnitude += abs2(value) + end + abs(squared) > sqrt(eps(typeof(magnitude))) * max(magnitude, one(magnitude)) || + return false + vector ./= sqrt(squared) + return true +end + +function _seed(matrix::AbstractMatrix{T}) where {T <: Complex} + decomposition = eigen(matrix) + values = Vector{T}(decomposition.values) + vectors = Matrix{T}(decomposition.vectors) + @inbounds for mode in axes(vectors, 2) + _orient!(@view(vectors[:, mode])) || throw(ArgumentError( + "eigendecomposition produced a zero eigenvector" + )) + end + return values, vectors +end + +# Minimum-cost square assignment by the O(n³) Hungarian algorithm. +function hungarian_assignment!(cost::AbstractMatrix{R},buffers) where {R <: Real} + n = checksquare(cost) + n == 0 && return Int[] + u,v,matching,way,minimums,used=buffers.u,buffers.v,buffers.matching, + buffers.way,buffers.minimums,buffers.used + fill!(u,zero(R));fill!(v,zero(R)) + fill!(matching,0);fill!(way,0) + + @inbounds for row in 1:n + matching[1] = row + fill!(minimums, R(Inf)) + fill!(used, false) + column = 1 + while true + used[column] = true + matched_row = matching[column] + delta = R(Inf) + next_column = 0 + for column_slot in 2:(n + 1) + used[column_slot] && continue + reduced = cost[matched_row, column_slot - 1] - + u[matched_row + 1] - v[column_slot] + if reduced < minimums[column_slot] + minimums[column_slot] = reduced + way[column_slot] = column + end + if minimums[column_slot] < delta + delta = minimums[column_slot] + next_column = column_slot + end + end + isfinite(delta) || throw(ArgumentError( + "modal assignment cost must be finite" + )) + for column_slot in 1:(n + 1) + if used[column_slot] + u[matching[column_slot] + 1] += delta + v[column_slot] -= delta + elseif column_slot > 1 + minimums[column_slot] -= delta + end + end + column = next_column + iszero(matching[column]) && break + end + while true + previous = way[column] + matching[column] = matching[previous] + column = previous + column == 1 && break + end + end + + assignment = buffers.assignment + @inbounds for column in 1:n + assignment[matching[column + 1]] = column + end + return assignment +end +function _match!( + values::AbstractVector{T}, + vectors::AbstractMatrix{T}, + previous_values::AbstractVector{T}, + previous_vectors::AbstractMatrix{T},buffers +) where {T <: Complex} + n = length(values) + length(previous_values) == n || throw(DimensionMismatch( + "eigenvalue sequences must have equal length" + )) + size(vectors) == size(previous_vectors) == (n, n) || throw( + DimensionMismatch("eigenvector matrices must be n×n") + ) + R = typeof(real(zero(T))) + cost = buffers.cost + @inbounds for previous in 1:n, current in 1:n + denominator = norm(@view(previous_vectors[:, previous])) * + norm(@view(vectors[:, current])) + overlap = iszero(denominator) ? zero(R) : + abs(dot( + @view(previous_vectors[:, previous]), + @view(vectors[:, current]) + )) / denominator + cost[previous, current] = one(R) - overlap + end + assignment = hungarian_assignment!(cost,buffers) + ordered_values = buffers.ordered_values + ordered_vectors = buffers.ordered_vectors + copyto!(ordered_values,values) + copyto!(ordered_vectors,vectors) + @inbounds for mode in 1:n + source = assignment[mode] + values[mode] = ordered_values[source] + copyto!(@view(vectors[:, mode]), @view(ordered_vectors[:, source])) + end + return assignment +end +""" +$(TYPEDSIGNATURES) + +Accept the tracked modes of one frequency: return whether the columns `t` of `Ti` and +the values `γ²` diagonalize `YZ`. The test returns `true` when: + +- every value and every entry of `Ti` is finite, +- `Ti` is invertible within √eps, with a condition number of at most `1/√eps`, +- each pair satisfies `YZ·t = γ²·t` within `tolerance`, relative to `‖YZ‖∞` and never + tighter than √eps. + +`residual` is the work vector of the last condition. When the test fails, the caller +records the frequency as a missed numerical target. With `fallback = :matched` it then +replaces the tracked modes by directly decomposed eigenpairs matched to the previous +frequency. +""" +function diagonalizes!( + Ti::AbstractMatrix{T}, + γ²::AbstractVector{T}, + YZ::AbstractMatrix{T}, + tolerance::Real,residual::AbstractVector{T} +) where {T <: Complex} + all(isfinite, γ²) && all(isfinite, Ti) || return false + R = typeof(real(zero(T))) + condition = cond(Ti) + isfinite(condition) && condition <= inv(sqrt(eps(R))) || return false + scale = max(norm(YZ, Inf), eps(R)) + limit = max(convert(R, tolerance), sqrt(eps(R))) * scale + @inbounds for mode in eachindex(γ²) + mul!(residual, YZ, @view(Ti[:, mode])) + residual .-= γ²[mode] .* @view(Ti[:, mode]) + norm(residual, Inf) <= limit || return false + end + return true +end +function recompute_matched_eigenpairs!( + matrix::AbstractMatrix{T}, + previous_values::AbstractVector{T}, + previous_vectors::AbstractMatrix{T},buffers +) where {T <: Complex} + values, vectors = _seed(matrix) + _match!(values, vectors, previous_values, previous_vectors,buffers) + @inbounds for mode in eachindex(values) + _align!(@view(vectors[:, mode]), @view(previous_vectors[:, mode])) + end + return values, vectors +end + +# Complex eigenpair equations with the bilinear normalization tᵀt = 1. +function eigenpair_residual!(residual, x, matrix) + n = size(matrix, 1) + vector = @view x[1:n] + mul!(@view(residual[1:n]), matrix, vector) + constraint = -one(eltype(x)) + @inbounds for row in 1:n + residual[row] -= x[end] * vector[row] + constraint += vector[row] * vector[row] + end + residual[end] = constraint + return residual +end + +function eigenpair_jacobian!(jacobian, x, matrix) + n = size(matrix, 1) + copyto!(@view(jacobian[1:n, 1:n]), matrix) + @inbounds for row in 1:n + jacobian[row, row] -= x[end] + jacobian[row, end] = -x[row] + jacobian[end, row] = 2x[row] + end + jacobian[end, end] = zero(eltype(jacobian)) + return jacobian +end + +# Greedy square assignment: choose the smallest remaining entry. +function greedy_assignment!(assignment, cost) + for _ in eachindex(assignment) + entry = argmin(cost) + assignment[entry[1]] = entry[2] + @views cost[entry[1], :] .= Inf + @views cost[:, entry[2]] .= Inf + end + return assignment +end diff --git a/src/engine/modalanalysis/formulas/chrysochos2014.jl b/src/engine/modalanalysis/formulas/chrysochos2014.jl new file mode 100644 index 000000000..4393ed6cd --- /dev/null +++ b/src/engine/modalanalysis/formulas/chrysochos2014.jl @@ -0,0 +1,414 @@ + +""" +$(TYPEDSIGNATURES) + +**Identification.** Levenberg–Marquardt tracking of each complex eigenpair, +initialized from the preceding frequency. + +**Expression.** + +```math +\\widetilde{\\mathbf S}=\\frac{\\mathbf Y\\mathbf Z} +{-\\omega^2\\mu_0\\varepsilon_0}-\\mathbf I,\\qquad +\\widetilde{\\mathbf S}\\mathbf t=\\lambda\\mathbf t,\\qquad +\\mathbf t^T\\mathbf t=1. +``` + +The complex residual is represented as a real least-squares system and solved +with a damped normal-equation step. + +**Reference.** A. I. Chrysochos, T. A. Papadopoulos, and G. K. Papagiannis, +“Robust Calculation of Frequency-Dependent Transmission-Line Transformation +Matrices Using the Levenberg–Marquardt Method,” *IEEE Transactions on Power +Delivery*, 29(4), 1621–1629, 2014. DOI: 10.1109/TPWRD.2013.2284504. + +""" +function description(::Type{<:Formula{:chrysochos2014}}; compact::Bool=false) + compact ? "Chrysochos" : "Chrysochos et al. Levenberg–Marquardt modal transformation (2014)" +end + +function levenberg_marquardt_residual!( + ::Formula{:chrysochos2014}, + residual::AbstractVector{R}, + x::AbstractVector{R}, + real_matrix::AbstractMatrix{R}, + imaginary_matrix::AbstractMatrix{R} +) where {R <: Real} + n = size(real_matrix, 1) + real_vector = @view x[1:n] + imaginary_vector = @view x[(n + 1):(2n)] + real_value = x[2n + 1] + imaginary_value = x[2n + 2] + real_constraint = -one(R) + imaginary_constraint = zero(R) + @inbounds for row in 1:n + real_result = -real_value * real_vector[row] + + imaginary_value * imaginary_vector[row] + imaginary_result = -imaginary_value * real_vector[row] - + real_value * imaginary_vector[row] + for column in 1:n + real_result += real_matrix[row, column] * real_vector[column] - + imaginary_matrix[row, column] * imaginary_vector[column] + imaginary_result += imaginary_matrix[row, column] * real_vector[column] + + real_matrix[row, column] * imaginary_vector[column] + end + residual[row] = real_result + residual[n + row] = imaginary_result + real_constraint += real_vector[row]^2 - imaginary_vector[row]^2 + imaginary_constraint += 2 * real_vector[row] * imaginary_vector[row] + end + residual[2n + 1] = real_constraint + residual[2n + 2] = imaginary_constraint + return residual +end + +function levenberg_marquardt_jacobian!( + ::Formula{:chrysochos2014}, + jacobian::AbstractMatrix{R}, + x::AbstractVector{R}, + real_matrix::AbstractMatrix{R}, + imaginary_matrix::AbstractMatrix{R} +) where {R <: Real} + n = size(real_matrix, 1) + real_vector = @view x[1:n] + imaginary_vector = @view x[(n + 1):(2n)] + real_value = x[2n + 1] + imaginary_value = x[2n + 2] + fill!(jacobian, zero(R)) + @inbounds for row in 1:n + for column in 1:n + diagonal = row == column + jacobian[row, column] = real_matrix[row, column] - + (diagonal ? real_value : zero(R)) + jacobian[row, n + column] = -imaginary_matrix[row, column] + + (diagonal ? imaginary_value : zero(R)) + jacobian[n + row, column] = imaginary_matrix[row, column] - + (diagonal ? imaginary_value : zero(R)) + jacobian[n + row, n + column] = real_matrix[row, column] - + (diagonal ? real_value : zero(R)) + end + jacobian[row, 2n + 1] = -real_vector[row] + jacobian[row, 2n + 2] = imaginary_vector[row] + jacobian[n + row, 2n + 1] = -imaginary_vector[row] + jacobian[n + row, 2n + 2] = -real_vector[row] + jacobian[2n + 1, row] = 2 * real_vector[row] + jacobian[2n + 1, n + row] = -2 * imaginary_vector[row] + jacobian[2n + 2, row] = 2 * imaginary_vector[row] + jacobian[2n + 2, n + row] = 2 * real_vector[row] + end + return jacobian +end + +function levenberg_marquardt_step!( + formula::Formula{:chrysochos2014}, + vector::AbstractVector{T}, + value::T, + iteration_options::NamedTuple, + buffers +) where {T <: Complex} + n = length(vector) + R = typeof(real(zero(T))) + requested_tolerance = convert(R, iteration_options.convergence) + tolerance = max(R(100) * eps(R), requested_tolerance^2) + iterations = iteration_options.max_iterations + iterations isa Integer && iterations > 0 || throw(DomainError( + iterations, + "max_iterations must be a positive integer" + )) + damping = convert(R, iteration_options.damping) + isfinite(damping) && damping > zero(R) || throw(DomainError( + damping, + "damping must be finite and positive" + )) + + normalize_bilinear!(vector) || _unit!(vector) || return value, false, 0 + @inbounds for index in 1:n + buffers.x[index] = real(vector[index]) + buffers.x[n + index] = imag(vector[index]) + end + buffers.x[2n + 1] = real(value) + buffers.x[2n + 2] = imag(value) + + converged = false + performed = 0 + maximum_damping = inv(eps(R)) + for iteration in 1:iterations + performed=iteration + levenberg_marquardt_residual!( + formula, + buffers.residual, + buffers.x, + buffers.real_matrix, + buffers.imaginary_matrix + ) + residual_norm = norm(buffers.residual, Inf) + if residual_norm <= tolerance + converged = true + break + end + levenberg_marquardt_jacobian!( + formula, + buffers.jacobian, + buffers.x, + buffers.real_matrix, + buffers.imaginary_matrix + ) + mul!(buffers.gradient, transpose(buffers.jacobian), buffers.residual) + norm(buffers.gradient, Inf) <= tolerance && (converged = true; break) + mul!(buffers.hessian, transpose(buffers.jacobian), buffers.jacobian) + copyto!(buffers.system, buffers.hessian) + diagonal_scale = one(R) + @inbounds for index in axes(buffers.hessian, 1) + diagonal_scale = max( + diagonal_scale, + abs(buffers.hessian[index, index]) + ) + end + floor = eps(R) * diagonal_scale + @inbounds for index in axes(buffers.system, 1) + buffers.system[index, index] += damping * + max(buffers.hessian[index, index], floor) + end + copyto!(buffers.step, buffers.gradient) + buffers.step .*= -one(R) + factorization = lu!(buffers.system; check = false) + issuccess(factorization) || return value, false, performed + ldiv!(factorization, buffers.step) + all(isfinite, buffers.step) || return value, false, performed + buffers.candidate .= buffers.x .+ buffers.step + levenberg_marquardt_residual!( + formula, + buffers.candidate_residual, + buffers.candidate, + buffers.real_matrix, + buffers.imaginary_matrix + ) + old_cost = dot(buffers.residual, buffers.residual) + new_cost = dot(buffers.candidate_residual, buffers.candidate_residual) + if isfinite(new_cost) && new_cost < old_cost + copyto!(buffers.x, buffers.candidate) + damping = max(damping / R(3), eps(R)) + if norm(buffers.step, Inf) <= tolerance * + (tolerance + norm(buffers.x, Inf)) + converged = true + break + end + else + damping *= R(10) + damping <= maximum_damping || return value, false, performed + end + end + + @inbounds for index in 1:n + vector[index] = complex(buffers.x[index], buffers.x[n + index]) + end + _unit!(vector) || return value, false, performed + return complex(buffers.x[2n + 1], buffers.x[2n + 2]), converged, performed +end + +""" +$(TYPEDSIGNATURES) + +Calculate current modal eigenvectors with the Chrysochos-Papadopoulos, +Papagiannis Levenberg–Marquardt formulation. At each frequency, the current +eigenproblem is scaled as + +```math +\\widetilde{S} = \\frac{YZ}{-\\omega^2\\mu_0\\epsilon_0} - I, \\qquad +\\widetilde{S}t = \\lambda t, +``` + +then each complex eigenpair is solved independently through its equivalent +real residual with the constraint ``t^Tt=1``. The preceding frequency supplies +the initial eigenpair. + +# Arguments + +- `lp`: fully coupled phase-domain line parameters, with `Z` in \\[Ω/m\\], `Y` + in \\[S/m\\], and frequency in \\[Hz\\]. +- `values`: modal controls containing the modal-residue `tolerance`, LM + `convergence`, `max_iterations`, and initial `damping` coefficient. + +# Returns + +- Frequency-dependent phase-to-modal voltage and current operators. + +# Notes + +The implementation uses an analytic real Jacobian and a monotone damped +normal-equation step. A conventional eigensolution matched to the preceding +frequency is retained if an iterative slice is singular or does not converge. + +# Reference + +A. I. Chrysochos, T. A. Papadopoulos, and G. K. Papagiannis, *Robust +Calculation of Frequency-Dependent Transmission-Line Transformation Matrices +Using the Levenberg–Marquardt Method*, IEEE Transactions on Power Delivery, +29(4), 2014. DOI: 10.1109/TPWRD.2013.2284504. +""" +function initialize_buffers(::Formula{:chrysochos2014}, ::Type{T}, input, + plan, buffers) where {T <: Complex} + n = plan.n + R = typeof(real(zero(T))) + order = 2n + 2 + return merge(buffers,( + normalized_shifted_eigenproblem=Matrix{T}(undef,n,n), + least_squares=( + x = Vector{R}(undef, order), + candidate = Vector{R}(undef, order), + residual = Vector{R}(undef, order), + candidate_residual = Vector{R}(undef, order), + jacobian = Matrix{R}(undef, order, order), + gradient = Vector{R}(undef, order), + hessian = Matrix{R}(undef, order, order), + system = Matrix{R}(undef, order, order), + step = Vector{R}(undef, order), + real_matrix = Matrix{R}(undef, n, n), + imaginary_matrix = Matrix{R}(undef, n, n)), + eigenpair_assignment=(cost=Matrix{R}(undef,n,n),u=zeros(R,n+1),v=zeros(R,n+1), + matching=zeros(Int,n+1),way=zeros(Int,n+1),minimums=Vector{R}(undef,n+1), + used=falses(n+1),assignment=Vector{Int}(undef,n), + ordered_values=Vector{T}(undef,n),ordered_vectors=Matrix{T}(undef,n,n), + residual=Vector{T}(undef,n)))) +end + +function decompose!(formula::Formula{:chrysochos2014}, workspace::ModalAnalysisWorkspace, + parameters::NamedTuple, options::FormulationOptions) + iteration_options = options.data.iteration + impedance = workspace.input.Z + admittance = workspace.input.Y + frequencies = workspace.input.f + n, _, nfrequencies = size(impedance) + T = eltype(workspace.Ti) + R = typeof(real(zero(T))) + buffers = workspace.buffers + admittance_impedance_product = buffers.admittance_impedance_product + normalized_shifted_eigenproblem = buffers.normalized_shifted_eigenproblem + least_squares = buffers.least_squares + previous_eigenvalues = buffers.previous_eigenvalues + previous_eigenvectors = buffers.previous_eigenvectors + eigenvalues = buffers.eigenvalues + eigenvectors = buffers.eigenvectors + propagation_eigenvalues = buffers.propagation_eigenvalues + convergence = convert(R, iteration_options.convergence) + validation_tolerance = max(R(100)*eps(R), convergence^2) + missed = workspace.diagnostics.missed_frequencies + fallback = workspace.diagnostics.fallback_frequencies + + @inbounds for frequency_index in 1:nfrequencies + copyto!(workspace.buffers.Zslice,@view(impedance[:,:,frequency_index])) + copyto!(workspace.buffers.Yslice,@view(admittance[:,:,frequency_index])) + workspace.buffers.Zslice ./= workspace.input.root_scale + workspace.buffers.Yslice ./= workspace.input.root_scale + Zslice=workspace.buffers.Zslice + Yslice=workspace.buffers.Yslice + frequency = convert(R, frequencies[frequency_index]) + frequency > zero(R) || throw(DomainError(frequency, + "Chrysochos modal analysis requires positive frequencies")) + mul!(admittance_impedance_product,Yslice,Zslice) + unit = one(frequency) + omega = 2*(unit*π)*frequency + epsilon0 = vacuum_permittivity(typeof(frequency)) + mu0 = vacuum_permeability(typeof(frequency)) + scale = -(omega^2)*epsilon0*mu0 + normalized_shifted_eigenproblem .= admittance_impedance_product ./ scale + for mode in 1:n + normalized_shifted_eigenproblem[mode,mode] -= one(T) + end + for index in eachindex(normalized_shifted_eigenproblem) + least_squares.real_matrix[index] = real(normalized_shifted_eigenproblem[index]) + least_squares.imaginary_matrix[index] = imag(normalized_shifted_eigenproblem[index]) + end + if frequency_index == 1 + seed_values, seed_vectors = _seed(normalized_shifted_eigenproblem) + copyto!(previous_eigenvalues, seed_values) + copyto!(previous_eigenvectors, seed_vectors) + else + copyto!(eigenvalues, previous_eigenvalues) + copyto!(eigenvectors, previous_eigenvectors) + failed_mode = 0 + for mode in 1:n + reference = @view previous_eigenvectors[:,mode] + value, converged, iterations = levenberg_marquardt_step!( + formula, @view(eigenvectors[:,mode]), + previous_eigenvalues[mode], iteration_options, least_squares) + workspace.diagnostics.iterations[mode,frequency_index]=iterations + workspace.diagnostics.converged[mode,frequency_index]=converged + eigenvalues[mode] = value + _align!(@view(eigenvectors[:,mode]), reference) + if !converged + failed_mode = mode + break + end + end + for mode in 1:n + propagation_eigenvalues[mode] = (eigenvalues[mode]+one(T))*scale + end + if failed_mode != 0 || !diagonalizes!(eigenvectors, + propagation_eigenvalues,admittance_impedance_product,validation_tolerance, + buffers.eigenpair_assignment.residual) + push!(missed, frequency_index) + if iteration_options.fallback === :matched + push!(fallback, frequency_index) + fallback_values, fallback_vectors = recompute_matched_eigenpairs!( + admittance_impedance_product,previous_eigenvalues, + previous_eigenvectors,buffers.eigenpair_assignment) + for mode in 1:n + eigenvalues[mode] = fallback_values[mode]/scale-one(T) + end + copyto!(eigenvectors, fallback_vectors) + end + end + copyto!(previous_eigenvalues, eigenvalues) + copyto!(previous_eigenvectors, eigenvectors) + end + copyto!(@view(workspace.Ti[:,:,frequency_index]),previous_eigenvectors) + for mode in 1:n + vector = @view workspace.Ti[:,mode,frequency_index] + _unit!(vector) || throw(ArgumentError("current eigenvector has zero norm")) + mul!(buffers.voltage_vector,Zslice,vector) + divisor = norm(buffers.voltage_vector) + isfinite(divisor) && !iszero(divisor) || + throw(ArgumentError("voltage eigenvector has zero or undefined norm")) + @views workspace.Tv[:,mode,frequency_index] .= buffers.voltage_vector ./ divisor + root = sqrt((previous_eigenvalues[mode]+one(T))*scale) + (real(root)<0 || (iszero(real(root)) && imag(root)<0)) && (root=-root) + workspace.roots[mode,frequency_index] = root*workspace.input.root_scale + eigenvalue=(previous_eigenvalues[mode]+one(T))*scale + mul!(buffers.eigenpair_assignment.residual,admittance_impedance_product,vector) + buffers.eigenpair_assignment.residual .-= eigenvalue .* vector + denominator=(norm(admittance_impedance_product,Inf)+abs(eigenvalue))*norm(vector,Inf) + numerator=norm(buffers.eigenpair_assignment.residual,Inf) + workspace.diagnostics.eigen_residual[mode,frequency_index]= + iszero(denominator) ? (iszero(numerator) ? zero(R) : R(Inf)) : + numerator/denominator + end + end + isempty(missed) || @warn ":chrysochos2014 missed numerical targets" count=length(missed) frequencies=copy(missed) fallback_count=length(fallback) + return workspace +end + +function formulation_options(::Expression{<:Formula{:chrysochos2014}, typeof(decompose!)}) + return FormulationOptions((iteration = ( + convergence = 1e-8, max_iterations = 100, damping = 1e-3, fallback = :matched),)) +end + +function formulation_options(::Expression{<:Formula{:chrysochos2014}, typeof(decompose!)}, + ::Val{:iteration}, defaults::NamedTuple, supplied::NamedTuple) + isempty(setdiff(keys(supplied), keys(defaults))) || + throw(ArgumentError("unknown modal iteration controls")) + options = merge(defaults, supplied) + for key in (:convergence, :damping) + value = getproperty(options, key) + value isa Real && isfinite(value) && value > 0 || + throw(ArgumentError("$key must be finite and positive")) + end + options.max_iterations isa Integer && !(options.max_iterations isa Bool) && + options.max_iterations > 0 || + throw(ArgumentError("max_iterations must be a positive integer")) + options.fallback in (:matched, :none) || + throw(ArgumentError("fallback must be :matched or :none")) + return options +end + +:chrysochos2014 diff --git a/src/engine/modalanalysis/formulas/default.jl b/src/engine/modalanalysis/formulas/default.jl new file mode 100644 index 000000000..a30388453 --- /dev/null +++ b/src/engine/modalanalysis/formulas/default.jl @@ -0,0 +1,12 @@ +""" +$(TYPEDSIGNATURES) + +Route the default selection to the explicit `:chrysochos2014` implementation. +No numerical equation is owned by `:default`. +""" +description(::Type{<:Formula{:default}}; compact::Bool=false) = + compact ? "Default" : "Default routing to :chrysochos2014" + +Formula{:default}(; kwargs...) = Formula{:chrysochos2014}(; kwargs...) + +:default diff --git a/src/engine/modalanalysis/formulas/vieira2026.jl b/src/engine/modalanalysis/formulas/vieira2026.jl new file mode 100644 index 000000000..b1a6e49f7 --- /dev/null +++ b/src/engine/modalanalysis/formulas/vieira2026.jl @@ -0,0 +1,416 @@ +""" +$(TYPEDSIGNATURES) + +Track current eigenpairs with Vieira's complex implementation of the +Chrysochos-Papadopoulos-Papagiannis Levenberg–Marquardt method: + +```math +S = \\frac{YZ}{-(2\\pi f)^2\\mu_0\\epsilon_0}-I,\\qquad +r(t,\\lambda) = \\begin{bmatrix}(S-\\lambda I)t\\\\t^Tt-1\\end{bmatrix},\\qquad +J = \\begin{bmatrix}S-\\lambda I&-t\\\\2t^T&0\\end{bmatrix}. +``` + +Here `Z` and `Y` are per-length matrices in \\[Ω/m\\] and \\[S/m\\], and `f` +is frequency in \\[Hz\\]. Complex SVD least-squares steps minimize the Euclidean +residual norm. A two-sample predictor, greedy eigenvalue matching and clustered +eigenvector refinement track the supplied samples. Failed tracking uses a +correlation-matched eigensolution at the same frequency. No samples are inserted. +Internal vectors use the bilinear constraint. Returned `Ti` and paired `Tv` +columns have unit Euclidean norm and map modal quantities to phase quantities. + +`iteration` controls `convergence=1e-13` and `max_iterations=100`. `tracking` +controls `predictor_tolerance=0.25`, `eigenvalue_tolerance=1e-5`, +`cluster_tolerance=1e-3` and `order_by_velocity=true`. The latter orders all +samples by decreasing phase constant at the final frequency. Numerical misses +are diagnostics, not rejection of finite results. + +Adapted from P. H. N. Vieira's `eig_levenberg_marquardt` implementation in +[Parametric-Eigenvalues-and-Vectors-of-Transmission-Lines](https://github.com/pedrohnv/Parametric-Eigenvalues-and-Vectors-of-Transmission-Lines). +The method is from A. I. Chrysochos, T. A. Papadopoulos and G. K. Papagiannis, *Robust Calculation of Frequency-Dependent Transmission-Line Transformation Matrices Using the +Levenberg–Marquardt Method*, IEEE Transactions on Power Delivery 29(4), +1621–1629 (2014), DOI: 10.1109/TPWRD.2013.2284504. The implementation uses the +package vacuum permittivity, 8.8541878128e-12 F/m, and evaluates `s = j2πf`. +""" +function description(::Type{<:Formula{:vieira2026}}; compact::Bool = false) + return compact ? "Vieira" : + "Vieira complex Levenberg–Marquardt modal transformation (2026)" +end + +function formulation_options(::Expression{<:Formula{:vieira2026}, typeof(decompose!)}) + return FormulationOptions(( + iteration = (convergence = 1e-13, max_iterations = 100), + tracking = (predictor_tolerance = 0.25, eigenvalue_tolerance = 1e-5, + cluster_tolerance = 1e-3, order_by_velocity = true))) +end + +function formulation_options(::Expression{<:Formula{:vieira2026}, typeof(decompose!)}, + ::Val{:iteration}, defaults::NamedTuple, supplied::NamedTuple) + isempty(setdiff(keys(supplied), keys(defaults))) || + throw(ArgumentError("unknown modal iteration controls")) + options = merge(defaults, supplied) + options.convergence isa Real && isfinite(options.convergence) && + options.convergence > 0 || + throw(ArgumentError("convergence must be finite and positive")) + options.max_iterations isa Integer && !(options.max_iterations isa Bool) && + options.max_iterations > 0 || + throw(ArgumentError("max_iterations must be a positive integer")) + return options +end + +function formulation_options(::Expression{<:Formula{:vieira2026}, typeof(decompose!)}, + ::Val{:tracking}, defaults::NamedTuple, supplied::NamedTuple) + isempty(setdiff(keys(supplied), keys(defaults))) || + throw(ArgumentError("unknown modal tracking controls")) + options = merge(defaults, supplied) + for key in (:predictor_tolerance, :eigenvalue_tolerance, :cluster_tolerance) + value = getproperty(options, key) + value isa Real && isfinite(value) && value > 0 || + throw(ArgumentError("$key must be finite and positive")) + end + options.order_by_velocity isa Bool || + throw(ArgumentError("order_by_velocity must be Bool")) + return options +end + +function initialize_buffers( + ::Formula{:vieira2026}, ::Type{T}, input, plan, buffers) where {T <: Complex} + n = plan.n + R = typeof(real(zero(T))) + order = n + 1 + return merge(buffers, + ( + normalized_shifted_eigenproblem = Matrix{T}(undef, n, n), + spectral_factor = Matrix{T}(undef, n, n), + prediction = (older_vectors = Matrix{T}(undef, n, n), + older_values = Vector{T}(undef, n), vectors = Matrix{T}(undef, n, n), + values = Vector{T}(undef, n)), + least_squares = ( + x = Vector{T}(undef, order), candidate = Vector{T}(undef, order), + residual = Vector{T}(undef, order), + candidate_residual = Vector{T}(undef, order), + jacobian = Matrix{T}(undef, order, order), + system = Matrix{T}(undef, 2order, order), rhs = Vector{T}(undef, 2order), + column_norms = Vector{R}(undef, order), step = Vector{T}(undef, order), + projection = Vector{T}(undef, order)), + eigenpair_assignment = ( + cost = Matrix{R}(undef, n, n), assignment = Vector{Int}(undef, n), + labels = Vector{Int}(undef, n), stack = Vector{Int}(undef, n), + columns = Vector{Int}(undef, n), tracks = Vector{Int}(undef, n), + cluster_assignment = Vector{Int}(undef, n), isolated = trues(n), + system = Matrix{T}(undef, n, n), rhs = Matrix{T}(undef, n, n), + coefficients = Matrix{T}(undef, n, n), projection = Matrix{T}(undef, n, n)), + eigen_residual = Vector{T}(undef, n), + mode_order = (indices = Vector{Int}(undef, n), + residual = Vector{Union{Nothing, R}}(undef, n), + iterations = Vector{Union{Nothing, Int}}(undef, n), + converged = Vector{Union{Nothing, Bool}}(undef, n)))) +end + +# Truncated SVD minimum-norm solve. Matrix is disposable factorization storage. +function minimum_norm!(solution, matrix::AbstractMatrix{T}, rhs, projection) where {T <: + Complex} + factor = svd!(matrix) + R = typeof(real(zero(T))) + cutoff = eps(R) * max(size(matrix)...) * first(factor.S) + mul!(projection, adjoint(factor.U), rhs) + @inbounds for column in axes(projection, 2), row in axes(projection, 1) + + singular = factor.S[row] + projection[row, column] = singular > cutoff ? projection[row, column] / singular : + zero(T) + end + mul!(solution, adjoint(factor.Vt), projection) + return solution +end + +function levenberg_marquardt_step!(::Formula{:vieira2026}, vector::AbstractVector{T}, + value::T, matrix, options, buffers) where {T <: Complex} + n = length(vector) + order = n + 1 + R = typeof(real(zero(T))) + tolerance = convert(R, options.convergence) + copyto!(@view(buffers.x[1:n]), vector) + buffers.x[end] = value + eigenpair_residual!(buffers.residual, buffers.x, matrix) + cost = real(dot(buffers.residual, buffers.residual)) + damping = zero(R) + iterations = 0 + for iteration in 1:options.max_iterations + sqrt(cost) < tolerance && break + iterations = iteration + eigenpair_jacobian!(buffers.jacobian, buffers.x, matrix) + for column in 1:order + buffers.column_norms[column] = norm(@view(buffers.jacobian[:, column])) + end + candidate_cost = cost + while true + rows = iszero(damping) ? order : 2order + copyto!(@view(buffers.system[1:order, :]), buffers.jacobian) + @views buffers.rhs[1:order] .= -buffers.residual + if !iszero(damping) + fill!(@view(buffers.system[(order + 1):end, :]), zero(T)) + fill!(@view(buffers.rhs[(order + 1):end]), zero(T)) + for column in 1:order + buffers.system[order + column, column] = sqrt(damping) * + buffers.column_norms[column] + end + end + minimum_norm!(buffers.step, @view(buffers.system[1:rows, :]), + @view(buffers.rhs[1:rows]), buffers.projection) + buffers.candidate .= buffers.x .+ buffers.step + eigenpair_residual!(buffers.candidate_residual, buffers.candidate, matrix) + candidate_cost = real(dot(buffers.candidate_residual, buffers.candidate_residual)) + (candidate_cost < cost || damping > R(1e8)) && break + damping = iszero(damping) ? R(1e-6) : 10damping + end + candidate_cost < cost || break + copyto!(buffers.x, buffers.candidate) + copyto!(buffers.residual, buffers.candidate_residual) + cost = candidate_cost + damping = damping <= R(1e-6) ? zero(R) : damping / 10 + end + copyto!(vector, @view(buffers.x[1:n])) + return buffers.x[end], sqrt(cost) < tolerance, iterations +end + +function refine_eigenpairs!(::Formula{:vieira2026}, values, vectors, previous_vectors, + prediction, eigensystem, options, buffers) + n = length(values) + R = eltype(buffers.cost) + @inbounds for column in 1:n, row in 1:n + + buffers.cost[row, column] = abs(values[row] - eigensystem.values[column]) + end + greedy_assignment!(buffers.assignment, buffers.cost) + eigenvalue_limit = R(options.eigenvalue_tolerance) * + max(one(R), maximum(abs, eigensystem.values)) + any( + row -> abs(values[row] - eigensystem.values[buffers.assignment[row]]) > + eigenvalue_limit, 1:n) && + return false + + fill!(buffers.labels, 0) + clusters = 0 + for seed in 1:n + buffers.labels[seed] == 0 || continue + clusters += 1 + buffers.labels[seed] = clusters + pending = 1 + buffers.stack[pending] = seed + while pending > 0 + row = buffers.stack[pending] + pending -= 1 + for column in 1:n + buffers.labels[column] == 0 || continue + gap = abs(eigensystem.values[row] - eigensystem.values[column]) + limit = R(options.cluster_tolerance) * + max(abs(1 + eigensystem.values[row]), + abs(1 + eigensystem.values[column]), R(1e-3)) + if gap < limit + buffers.labels[column] = clusters + pending += 1 + buffers.stack[pending] = column + end + end + end + end + @inbounds for mode in 1:n + values[mode] = eigensystem.values[buffers.assignment[mode]] + end + fill!(buffers.isolated, true) + for cluster in 1:clusters + count = 0 + tracks = 0 + for mode in 1:n + if buffers.labels[mode] == cluster + count += 1 + buffers.columns[count] = mode + end + if buffers.labels[buffers.assignment[mode]] == cluster + tracks += 1 + buffers.tracks[tracks] = mode + end + end + count == 1 && continue + for column in 1:count + track = buffers.tracks[column] + buffers.isolated[track] = false + copyto!(@view(buffers.system[:, column]), @view(previous_vectors[:, track])) + copyto!(@view(buffers.rhs[:, column]), @view(eigensystem.vectors[:, buffers.columns[column]])) + end + coefficients = @view buffers.coefficients[1:count, 1:count] + minimum_norm!(coefficients, @view(buffers.system[:, 1:count]), + @view(buffers.rhs[:, 1:count]), @view(buffers.projection[1:count, 1:count])) + cost = @view buffers.cost[1:count, 1:count] + cost .= .-abs.(coefficients) + assignment = @view buffers.cluster_assignment[1:count] + greedy_assignment!(assignment, cost) + for column in 1:count + track = buffers.tracks[column] + source = buffers.columns[assignment[column]] + copyto!(@view(vectors[:, track]), @view(eigensystem.vectors[:, source])) + values[track] = eigensystem.values[source] + end + end + largest_change = zero(R) + for mode in 1:n + value_change = abs(values[mode] - prediction.values[mode]) / + max(abs(prediction.values[mode]), R(1e-3)) + largest_change = max(largest_change, value_change) + if buffers.isolated[mode] + change = zero(R) + magnitude = zero(R) + for row in 1:n + change = max(change, abs(vectors[row, mode] - + prediction.vectors[row, mode])) + magnitude = max(magnitude, abs(prediction.vectors[row, mode])) + end + largest_change = max(largest_change, change / magnitude) + end + end + return largest_change <= R(options.predictor_tolerance) +end + +function decompose!(formula::Formula{:vieira2026}, workspace::ModalAnalysisWorkspace, + parameters::NamedTuple, options::FormulationOptions) + buffers = workspace.buffers + input = workspace.input + diagnostics = workspace.diagnostics + iteration = options.data.iteration + tracking = options.data.tracking + prediction = buffers.prediction + assignment = buffers.eigenpair_assignment + n, _, nf = size(workspace.Ti) + T = eltype(workspace.Ti) + R = typeof(real(zero(T))) + unit = one(R) + epsilon0 = vacuum_permittivity(R) + mu0 = vacuum_permeability(R) + for frequency in 1:nf + copyto!(buffers.Zslice, @view(input.Z[:, :, frequency])) + copyto!(buffers.Yslice, @view(input.Y[:, :, frequency])) + buffers.Zslice ./= input.root_scale + buffers.Yslice ./= input.root_scale + mul!(buffers.admittance_impedance_product, buffers.Yslice, buffers.Zslice) + omega = 2 * (unit * π) * R(input.f[frequency]) + scale = -(omega^2) * epsilon0 * mu0 + matrix = buffers.normalized_shifted_eigenproblem + matrix .= buffers.admittance_impedance_product ./ scale + for mode in 1:n + matrix[mode, mode] -= one(T) + end + copyto!(buffers.spectral_factor, matrix) + eigensystem = eigen!(buffers.spectral_factor) + for mode in 1:n + vector = @view eigensystem.vectors[:, mode] + vector ./= sqrt(sum(value -> value * value, vector)) + end + if frequency == 1 + copyto!(buffers.eigenvalues, eigensystem.values) + copyto!(buffers.eigenvectors, eigensystem.vectors) + else + coefficient = frequency == 2 ? zero(R) : + clamp( + R(input.f[frequency] - input.f[frequency - 1]) / + R(input.f[frequency - 1] - input.f[frequency - 2]), -one(R), one(R)) + if frequency == 2 + copyto!(prediction.vectors, buffers.previous_eigenvectors) + copyto!(prediction.values, buffers.previous_eigenvalues) + else + prediction.vectors .= buffers.previous_eigenvectors .+ + coefficient .* (buffers.previous_eigenvectors .- + prediction.older_vectors) + prediction.values .= buffers.previous_eigenvalues .+ + coefficient .* + (buffers.previous_eigenvalues .- prediction.older_values) + end + copyto!(buffers.eigenvectors, prediction.vectors) + for mode in 1:n + value, converged, iterations = levenberg_marquardt_step!(formula, + @view(buffers.eigenvectors[:, mode]), prediction.values[mode], matrix, + iteration, buffers.least_squares) + buffers.eigenvalues[mode] = value + diagnostics.iterations[mode, frequency] = iterations + diagnostics.converged[mode, frequency] = converged + end + matched = refine_eigenpairs!(formula, buffers.eigenvalues, + buffers.eigenvectors, buffers.previous_eigenvectors, prediction, eigensystem, tracking, assignment) + if !matched + # Preserve the reference's square correlation solve and greedy assignment. + copyto!(assignment.system, buffers.previous_eigenvectors) + copyto!(assignment.coefficients, eigensystem.vectors) + ldiv!(lu!(assignment.system), assignment.coefficients) + assignment.cost .= .-abs.(assignment.coefficients) + greedy_assignment!(assignment.assignment, assignment.cost) + for mode in 1:n + source = assignment.assignment[mode] + copyto!(@view(buffers.eigenvectors[:, mode]), @view(eigensystem.vectors[:, source])) + buffers.eigenvalues[mode] = eigensystem.values[source] + end + push!(diagnostics.fallback_frequencies, frequency) + end + if !matched || any(!, @view(diagnostics.converged[:, frequency])) + push!(diagnostics.missed_frequencies, frequency) + end + for mode in 1:n + real(dot(@view(buffers.previous_eigenvectors[:, mode]), + @view(buffers.eigenvectors[:, mode]))) < 0 && + (@views buffers.eigenvectors[:, mode] .*= -one(T)) + end + copyto!(prediction.older_vectors, buffers.previous_eigenvectors) + copyto!(prediction.older_values, buffers.previous_eigenvalues) + end + copyto!(buffers.previous_eigenvectors, buffers.eigenvectors) + copyto!(buffers.previous_eigenvalues, buffers.eigenvalues) + buffers.propagation_eigenvalues .= (buffers.eigenvalues .+ one(T)) .* scale + for mode in 1:n + vector = @view workspace.Ti[:, mode, frequency] + copyto!(vector, @view(buffers.eigenvectors[:, mode])) + _unit!(vector) || + throw(ArgumentError("current eigenvector has zero or undefined norm")) + mul!(buffers.voltage_vector, buffers.Zslice, vector) + divisor = norm(buffers.voltage_vector) + isfinite(divisor) && !iszero(divisor) || + throw(ArgumentError("voltage eigenvector has zero or undefined norm")) + @views workspace.Tv[:, mode, frequency] .= buffers.voltage_vector ./ divisor + eigenvalue = buffers.propagation_eigenvalues[mode] + root = sqrt(eigenvalue) + (real(root) < 0 || (iszero(real(root)) && imag(root) < 0)) && (root = -root) + workspace.roots[mode, frequency] = root * input.root_scale + mul!(buffers.eigen_residual, buffers.admittance_impedance_product, vector) + buffers.eigen_residual .-= eigenvalue .* vector + denominator = (norm(buffers.admittance_impedance_product, Inf) + abs(eigenvalue)) * + norm(vector, Inf) + numerator = norm(buffers.eigen_residual, Inf) + diagnostics.eigen_residual[mode, frequency] = iszero(denominator) ? + (iszero(numerator) ? zero(R) : + R(Inf)) : numerator / denominator + end + end + if tracking.order_by_velocity + order = buffers.mode_order + sortperm!(order.indices, @view(workspace.roots[:, end]); by = value -> -imag(value)) + for frequency in 1:nf + copyto!(buffers.eigenvectors, @view(workspace.Ti[:, :, frequency])) + copyto!(buffers.previous_eigenvectors, @view(workspace.Tv[:, :, frequency])) + copyto!(buffers.eigenvalues, @view(workspace.roots[:, frequency])) + copyto!(order.residual, @view(diagnostics.eigen_residual[:, frequency])) + copyto!(order.iterations, @view(diagnostics.iterations[:, frequency])) + copyto!(order.converged, @view(diagnostics.converged[:, frequency])) + for mode in 1:n + source = order.indices[mode] + copyto!(@view(workspace.Ti[:, mode, frequency]), @view(buffers.eigenvectors[:, source])) + copyto!(@view(workspace.Tv[:, mode, frequency]), @view(buffers.previous_eigenvectors[:, source])) + workspace.roots[mode, frequency] = buffers.eigenvalues[source] + diagnostics.eigen_residual[mode, frequency] = order.residual[source] + diagnostics.iterations[mode, frequency] = order.iterations[source] + diagnostics.converged[mode, frequency] = order.converged[source] + end + end + end + isempty(diagnostics.missed_frequencies) || + @warn ":vieira2026 missed numerical targets" frequencies=copy(diagnostics.missed_frequencies) fallback_count=length(diagnostics.fallback_frequencies) + return workspace +end + +:vieira2026 diff --git a/src/engine/modalanalysis/formulas/wedepohl1996.jl b/src/engine/modalanalysis/formulas/wedepohl1996.jl new file mode 100644 index 000000000..2cd228af2 --- /dev/null +++ b/src/engine/modalanalysis/formulas/wedepohl1996.jl @@ -0,0 +1,194 @@ +""" +$(TYPEDSIGNATURES) + +Track current eigenpairs by Newton–Raphson refinement of the normalized +admittance-impedance product: + +```math +A = YZ/\\|YZ\\|_2,\\qquad +r(t,\\lambda) = \\begin{bmatrix}(A-\\lambda I)t\\\\t^Tt-1\\end{bmatrix},\\qquad +J = \\begin{bmatrix}A-\\lambda I&-t\\\\2t^T&0\\end{bmatrix}. +``` + +Here `Z` and `Y` are per-length matrices in \\[Ω/m\\] and \\[S/m\\]. Each +eigenpair starts from the previous stored frequency. The seed is ordered by +decreasing attenuation. Failed iterations or duplicate eigenpairs use a direct +eigendecomposition at the same frequency, greedily matched by column overlap. +The calculation uses the existing physical evaluation and frequency samples. + +`iteration.convergence=1e-9` bounds the largest absolute Newton correction in +the normalized problem. `iteration.max_iterations=60` limits each eigenpair. +Misses are recorded and warned about, without discarding the fallback result. + +Adapted from `eig_newton` in the supplied UniversalLineModel `modal.jl`, based on +L. M. Wedepohl, H. V. Nguyen and G. D. Irwin, *Frequency-dependent transformation +matrices for untransposed transmission lines using Newton-Raphson method*, +IEEE Transactions on Power Systems 11(3), 1538–1546 (1996), +DOI: 10.1109/59.535695. The selection identifier is `:wedepohl1996`. +""" +function description(::Type{<:Formula{:wedepohl1996}}; compact::Bool = false) + compact ? "Wedepohl" : + "Wedepohl–Nguyen–Irwin Newton–Raphson modal transformation (1996)" +end + +function formulation_options(::Expression{<:Formula{:wedepohl1996}, typeof(decompose!)}) + FormulationOptions((iteration = (convergence = 1e-9, max_iterations = 60),)) +end + +function formulation_options(::Expression{<:Formula{:wedepohl1996}, typeof(decompose!)}, + ::Val{:iteration}, defaults::NamedTuple, supplied::NamedTuple) + isempty(setdiff(keys(supplied), keys(defaults))) || + throw(ArgumentError("unknown modal iteration controls")) + options=merge(defaults, supplied) + options.convergence isa Real && isfinite(options.convergence) && + options.convergence>0 || + throw(ArgumentError("convergence must be finite and positive")) + options.max_iterations isa Integer && !(options.max_iterations isa Bool) && + options.max_iterations>0 || + throw(ArgumentError("max_iterations must be a positive integer")) + return options +end + +function initialize_buffers( + ::Formula{:wedepohl1996}, ::Type{T}, input, plan, buffers) where {T <: Complex} + n=plan.n + R=typeof(real(zero(T))) + return merge(buffers, + ( + normalized_eigenproblem = Matrix{T}(undef, n, n), + spectral_factor = Matrix{T}(undef, n, n), + newton = (x = Vector{T}(undef, n+1), residual = Vector{T}(undef, n+1), + jacobian = Matrix{T}(undef, n+1, n+1)), + eigenpair_assignment = ( + cost = Matrix{R}(undef, n, n), indices = Vector{Int}(undef, n)), + eigen_residual = Vector{T}(undef, n))) +end + +function newton_eigenpair!( + vector::AbstractVector{T}, value::T, matrix, options, buffers) where {T <: Complex} + n=length(vector) + R=typeof(real(zero(T))) + _unit!(vector) || return value, false, 0 + copyto!(@view(buffers.x[1:n]), vector) + buffers.x[end]=value + correction=R(Inf) + iterations=0 + for iteration in 1:options.max_iterations + iterations=iteration + eigenpair_jacobian!(buffers.jacobian, buffers.x, matrix) + eigenpair_residual!(buffers.residual, buffers.x, matrix) + factor=lu!(buffers.jacobian; check = false) + issuccess(factor) || break + ldiv!(factor, buffers.residual) + correction=maximum(abs, buffers.residual) + buffers.x .-= buffers.residual + all(isfinite, buffers.x) || break + correction<=R(options.convergence) && break + end + copyto!(vector, @view(buffers.x[1:n])) + valid=all(isfinite, buffers.x) && _unit!(vector) + return buffers.x[end], valid && correction<=R(options.convergence), iterations +end + +function decompose!(::Formula{:wedepohl1996}, workspace::ModalAnalysisWorkspace, + parameters::NamedTuple, options::FormulationOptions) + buffers=workspace.buffers + input=workspace.input + diagnostics=workspace.diagnostics + n, _, nf=size(workspace.Ti) + T=eltype(workspace.Ti) + R=typeof(real(zero(T))) + for frequency in 1:nf + copyto!(buffers.Zslice, @view(input.Z[:, :, frequency])) + copyto!(buffers.Yslice, @view(input.Y[:, :, frequency])) + buffers.Zslice ./= input.root_scale + buffers.Yslice ./= input.root_scale + matrix=buffers.admittance_impedance_product + mul!(matrix, buffers.Yslice, buffers.Zslice) + use_newton_result=frequency>1 + if frequency>1 + copyto!(buffers.spectral_factor, matrix) + scale=first(svdvals!(buffers.spectral_factor)) + iszero(scale) && (scale=one(R)) + buffers.normalized_eigenproblem .= matrix ./ scale + copyto!(buffers.eigenvectors, buffers.previous_eigenvectors) + for mode in 1:n + value, converged, iterations=newton_eigenpair!( + @view(buffers.eigenvectors[:, mode]), buffers.previous_eigenvalues[mode]/scale, + buffers.normalized_eigenproblem, options.data.iteration, buffers.newton) + buffers.eigenvalues[mode]=value*scale + diagnostics.iterations[mode, frequency]=iterations + diagnostics.converged[mode, frequency]=converged + use_newton_result &= converged + end + if use_newton_result + for first_mode in 1:n, second_mode in (first_mode + 1):n + + overlap=abs(dot(@view(buffers.eigenvectors[:, first_mode]), + @view(buffers.eigenvectors[:, second_mode]))) + gap=abs(buffers.eigenvalues[first_mode]-buffers.eigenvalues[second_mode]) + if one(R)-overlap-real(sqrt(value))) + else + for column in 1:n, row in 1:n + + assignment.cost[row, column]=-abs(dot( + @view(buffers.previous_eigenvectors[:, row]), + @view(eigensystem.vectors[:, column]))) + end + greedy_assignment!(assignment.indices, assignment.cost) + push!(diagnostics.missed_frequencies, frequency) + push!(diagnostics.fallback_frequencies, frequency) + end + for mode in 1:n + source=assignment.indices[mode] + buffers.eigenvalues[mode]=eigensystem.values[source] + copyto!(@view(buffers.eigenvectors[:, mode]), @view(eigensystem.vectors[:, source])) + end + end + for mode in 1:n + vector=@view(buffers.eigenvectors[:, mode]) + _unit!(vector) || + throw(ArgumentError("current eigenvector has zero or undefined norm")) + if frequency>1 && + real(dot(@view(buffers.previous_eigenvectors[:, mode]), vector))<0 + vector .*= -one(T) + end + copyto!(@view(workspace.Ti[:, mode, frequency]), vector) + mul!(buffers.voltage_vector, buffers.Zslice, vector) + divisor=norm(buffers.voltage_vector) + isfinite(divisor) && !iszero(divisor) || + throw(ArgumentError("voltage eigenvector has zero or undefined norm")) + @views workspace.Tv[:, mode, frequency] .= buffers.voltage_vector ./ divisor + value=buffers.eigenvalues[mode] + root=sqrt(value) + (real(root)<0 || (iszero(real(root)) && imag(root)<0)) && (root=-root) + workspace.roots[mode, frequency]=root*input.root_scale + mul!(buffers.eigen_residual, matrix, vector) + buffers.eigen_residual .-= value .* vector + denominator=(norm(matrix, Inf)+abs(value))*norm(vector, Inf) + numerator=norm(buffers.eigen_residual, Inf) + diagnostics.eigen_residual[mode, frequency]=iszero(denominator) ? + (iszero(numerator) ? zero(R) : + R(Inf)) : numerator/denominator + end + copyto!(buffers.previous_eigenvectors, buffers.eigenvectors) + copyto!(buffers.previous_eigenvalues, buffers.eigenvalues) + end + isempty(diagnostics.missed_frequencies) || + @warn ":wedepohl1996 missed numerical targets" frequencies=copy(diagnostics.missed_frequencies) fallback_count=length(diagnostics.fallback_frequencies) + return workspace +end + +:wedepohl1996 diff --git a/src/engine/modalanalysis/formulations.jl b/src/engine/modalanalysis/formulations.jl new file mode 100644 index 000000000..dcb523d58 --- /dev/null +++ b/src/engine/modalanalysis/formulations.jl @@ -0,0 +1,133 @@ +""" +$(TYPEDEF) + +Select a modal decomposition with model parameters and normalized numerical +controls. The modal workspace supplies common numerical storage. The selected +formula implements `decompose!` and allocates additional operation-specific +work through `initialize_buffers`. + +$(TYPEDFIELDS) +""" +struct Formula{ID, P <: NamedTuple, O <: FormulationOptions} <: AbstractFormulation + "Resolved model parameters." + parameters::P + "Normalized numerical sections for the modal algorithm." + options::O +end + +""" +$(TYPEDSIGNATURES) + +Construct a modal decomposition with model parameters and numerical controls. +Custom formulations extend `initialize_buffers` and `decompose!`. +""" +function Formula{ID}(; parameters::NamedTuple=(;), options::Union{NamedTuple, FormulationOptions} = FormulationOptions()) where {ID} + options = options isa NamedTuple ? FormulationOptions(options) : options + isempty(parameters) || throw(ArgumentError("modal :$ID has no physical parameters")) + selected = Formula{ID, typeof(parameters), typeof(options)}(parameters, options) + normalized = formulation_options(Expression(selected, decompose!), options) + return Formula{ID, typeof(parameters), typeof(normalized)}(parameters, normalized) +end + +Formula(identifier::Symbol; kwargs...) = Formula(Val(identifier); kwargs...) +Formula(::Val{ID}; kwargs...) where {ID} = Formula{ID}(; kwargs...) +Formula(selected::Formula) = selected + +function Formula(selection::FormulaDefinition{ID, Order}) where {ID, Order} + Order === :default || throw(ArgumentError("order applies only to equivalent_earth")) + selection.equivalent_earth === nothing || throw(ArgumentError( + "modal formulas cannot consume equivalent_earth")) + return Formula{ID}(; parameters=selection.parameters, options=selection.options) +end + +formula_id(::Formula{ID}) where {ID} = ID +formula_id(::Type{<:Formula{ID}}) where {ID} = ID +description(value::Formula; compact::Bool=false) = description(typeof(value); compact) +formulation_options(value::Formula) = value.options + +"""Expose a selected modal equation and its model and numerical controls.""" +Base.NamedTuple(value::Formula) = (identifier=formula_id(value), + parameters=value.parameters, options=value.options.data) + +Base.pairs(::Type{<:Formula}; quantity=nothing) = pairs((;)) + +""" +$(TYPEDEF) + +Own a modal computation's requested declaration and resolved equation. +The requested formula and controls are retained before default resolution and +normalization. The resolved formula and effective controls are stored separately. + +$(TYPEDFIELDS) +""" +struct ModalAnalysisFormulation{F <: AbstractFormulation, D} <: AbstractFormulation + "Resolved modal decomposition." + formula::F + "Requested selection before default resolution and control normalization." + definition::D +end + +ModalAnalysisFormulation() = ModalAnalysisFormulation(:default) + +function _modal_formulation(identifier::Symbol, controls::NamedTuple) + selection = formula(identifier; controls...) + return ModalAnalysisFormulation(Formula(selection), selection) +end + +function _modal_formulation(selection::FormulaDefinition, controls::NamedTuple) + isempty(controls) || throw(ArgumentError( + "formula(...) already contains its modal parameters and numerical controls")) + return ModalAnalysisFormulation(Formula(selection), selection) +end + +function _modal_formulation(selected::AbstractFormulation, controls::NamedTuple) + isempty(controls) || throw(ArgumentError( + "a completed modal formulation cannot receive additional controls")) + return ModalAnalysisFormulation(selected, selected) +end + +function _modal_formulation(selected::ModalAnalysisFormulation, controls::NamedTuple) + isempty(controls) || throw(ArgumentError( + "a completed modal formulation cannot receive additional controls")) + return selected +end + +""" +$(TYPEDSIGNATURES) + +Select one or more modal computations. A symbol, formula declaration, or +completed user-owned formulation selects one equation. `Grid` and `Gridspace` +vary complete selections with product or zip composition. +""" +function ModalAnalysisFormulation(selection; combine::Symbol=:product, kwargs...) + return parameterize(ModalAnalysisFormulation, _modal_formulation, + (selection, (; kwargs...)); combine) +end + +formula_id(::Type{<:ModalAnalysisFormulation}) = :modal +formula_id(::ModalAnalysisFormulation) = :modal +description(::Type{<:ModalAnalysisFormulation}; compact::Bool=false) = "modal" +description(::ModalAnalysisFormulation; compact::Bool=false) = "modal" +description(::Type{ModalAnalysisFormulation}, ::Val{:transformation}; compact::Bool=false) = "modal operators" +description(::Type{ModalAnalysisFormulation},selected::AbstractFormulation; + compact::Bool=false,quantity=nothing) = description(selected;compact) +description(::Type{ModalAnalysisFormulation},selected::Pair{<:AbstractFormulation,<:NamedTuple}; + compact::Bool=false,quantity=nothing) = + description(ModalAnalysisFormulation,first(selected);compact,quantity) +Base.pairs(::Type{ModalAnalysisFormulation}; quantity=nothing) = pairs((transformation=Formula,)) +formulation_options(::ModalAnalysisFormulation) = FormulationOptions() + +function Base.pairs(value::ModalAnalysisFormulation; quantity=nothing) + return pairs(ModalAnalysisFormulation, + (methods=(transformation=value.formula,), + requested=(transformation=value.definition,), options=(;)); quantity) +end + +function Base.pairs(::Type{ModalAnalysisFormulation}, retained::NamedTuple; quantity=nothing) + return pairs(LineParametersFormulation, retained; quantity, owner=ModalAnalysisFormulation) +end + +function Base.NamedTuple(value::ModalAnalysisFormulation) + return (backend=:modal, requested=(transformation=NamedTuple(value.definition),), + methods=(transformation=NamedTuple(value.formula),), options=(;)) +end diff --git a/src/engine/modalanalysis/interfaces.jl b/src/engine/modalanalysis/interfaces.jl new file mode 100644 index 000000000..59362743a --- /dev/null +++ b/src/engine/modalanalysis/interfaces.jl @@ -0,0 +1,79 @@ +""" +$(TYPEDEF) + +Select the LineCableModels modal-transformation backend. +""" +struct LineCableModelsModal end + +""" +$(TYPEDEF) + +Store frequency-dependent modal-to-phase voltage and current bases. + +For every frequency sample, `Tv` and `Ti` satisfy `Vₚ = Tv Vₘ` and +`Iₚ = Ti Iₘ`. + +$(TYPEDFIELDS) +""" +struct ModalOperators{V <: AbstractArray, I <: AbstractArray} <: Engine.AbstractModalOperators + "Modal-to-phase voltage tensor." + Tv::V + "Modal-to-phase current tensor." + Ti::I + + function ModalOperators(Tv::V, Ti::I) where { + V <: AbstractArray, + I <: AbstractArray + } + ndims(Tv) == 3 || throw( + DimensionMismatch("voltage operators must be an n×n×nfreq tensor") + ) + ndims(Ti) == 3 || throw( + DimensionMismatch("current operators must be an n×n×nfreq tensor") + ) + size(Tv) == size(Ti) || throw(DimensionMismatch( + "voltage and current operators must have equal n×n×nfreq dimensions" + )) + size(Tv, 1) == size(Tv, 2) || throw( + DimensionMismatch("modal operators must be square") + ) + return new{V, I}(Tv, Ti) + end +end + +Base.size(maps::ModalOperators) = size(maps.Tv) + +""" +Return the modal operators stored in modal-domain line parameters. +""" +function operators(parameters::LineParameters{T, U, D}) where {T, U, D <: ModalDomain} + parameters.domain.operators +end + +function selectdomain(domain::ModalDomain, selected) + maps = domain.operators + Tv = Array(view(maps.Tv, :, :, selected)) + Ti = Array(view(maps.Ti, :, :, selected)) + selected_maps = ModalOperators(Tv, Ti) + roots = Array(view(domain.gamma, :, selected)) + return ModalDomain(selected_maps, roots) +end + +function selectdetails(retained::ComputationDetails, ::ModalDomain, selected) + record=retained.data + haskey(record,:modal) || return retained + modal=record.modal + haskey(modal,:diagnostics) || return retained + diagnostics=modal.diagnostics + original=selected isa Colon ? collect(eachindex(diagnostics.z_coupling)) : collect(selected) + fallback=findall(in(diagnostics.fallback_frequencies),original) + missed=findall(in(diagnostics.missed_frequencies),original) + sliced=merge(diagnostics,(fallback_frequencies=fallback, + missed_frequencies=missed, + z_coupling=diagnostics.z_coupling[selected], + y_coupling=diagnostics.y_coupling[selected], + eigen_residual=diagnostics.eigen_residual[:,selected], + iterations=diagnostics.iterations[:,selected], + converged=diagnostics.converged[:,selected])) + return ComputationDetails(merge(record,(modal=merge(modal,(diagnostics=sliced,)),))) +end diff --git a/src/engine/modalanalysis/observations.jl b/src/engine/modalanalysis/observations.jl new file mode 100644 index 000000000..280270e0f --- /dev/null +++ b/src/engine/modalanalysis/observations.jl @@ -0,0 +1,403 @@ +# Scientific units belong to the scalar quantity, independently of presentation. +Units.quantity(::typeof(gamma)) = Units.Quantity{:propagation_constant}() +Units.quantity(::typeof(alpha)) = Units.Quantity{:attenuation_constant}() +Units.quantity(::typeof(beta)) = Units.Quantity{:phase_constant}() +Units.quantity(::typeof(velocity)) = Units.Quantity{:phase_velocity}() +Units.quantity(::typeof(Zc)) = Units.Quantity{:characteristic_impedance}() +Units.quantity(::typeof(Yc)) = Units.Quantity{:characteristic_admittance}() +Units.quantity(::typeof(H)) = Units.Quantity{:forward_response}() +Units.quantity(::typeof(Tv)) = Units.Quantity{:voltage_basis}() +Units.quantity(::typeof(Ti)) = Units.Quantity{:current_basis}() +Units.quantity(value::Base.Fix2{typeof(H)}) = value.x.field===:voltage ? + Units.Quantity{:voltage_propagation_function}() : + Units.Quantity{:current_propagation_function}() +Units.quantity(value::Base.Fix2{typeof(Zc)}) = Units.Quantity{:phase_characteristic_impedance}() +Units.quantity(value::Base.Fix2{typeof(Yc)}) = Units.Quantity{:phase_characteristic_admittance}() + +function Units.quantity(value::Base.Fix2{typeof(H)},transform::Function) + name=value.x.field===:voltage ? :voltage_propagation_function : + :current_propagation_function + return _modal_bound_quantity(name,transform) +end +Units.quantity(::Base.Fix2{typeof(Zc)},transform::Function) = + _modal_bound_quantity(:phase_characteristic_impedance,transform) +Units.quantity(::Base.Fix2{typeof(Yc)},transform::Function) = + _modal_bound_quantity(:phase_characteristic_admittance,transform) + +_modal_bound_quantity(name::Symbol,::typeof(abs)) = Units.Quantity{(name,:magnitude)}() +_modal_bound_quantity(name::Symbol,::typeof(angle)) = Units.Quantity{(name,:phase_angle)}() +_modal_bound_quantity(name::Symbol,::typeof(real)) = Units.Quantity{(name,:real)}() +_modal_bound_quantity(name::Symbol,::typeof(imag)) = Units.Quantity{(name,:imag)}() + +for (selector, name) in ((gamma,:propagation_constant),(Zc,:characteristic_impedance), + (Yc,:characteristic_admittance),(H,:forward_response),(Tv,:voltage_basis), + (Ti,:current_basis)) + @eval begin + Units.quantity(::typeof($selector),::typeof(abs)) = Units.Quantity{($(QuoteNode(name)),:magnitude)}() + Units.quantity(::typeof($selector),::typeof(angle)) = Units.Quantity{($(QuoteNode(name)),:phase_angle)}() + Units.native_unit(::Units.Quantity{($(QuoteNode(name)),:magnitude)}) = Units.native_unit(Units.Quantity{$(QuoteNode(name))}()) + Units.display_unit(::Units.Quantity{($(QuoteNode(name)),:magnitude)}) = Units.display_unit(Units.Quantity{$(QuoteNode(name))}()) + Units.native_unit(::Units.Quantity{($(QuoteNode(name)),:phase_angle)}) = Units.units(:base,:radian) + Units.display_unit(::Units.Quantity{($(QuoteNode(name)),:phase_angle)}) = Units.units(:base,:degree) + Units.label(::Units.Quantity{($(QuoteNode(name)),:magnitude)}) = Units.label(Units.Quantity{$(QuoteNode(name))}()) * " magnitude" + Units.label(::Units.Quantity{($(QuoteNode(name)),:phase_angle)}) = Units.label(Units.Quantity{$(QuoteNode(name))}()) * " angle" + Units.symbol(::Units.Quantity{($(QuoteNode(name)),:magnitude)}) = "|" * Units.symbol(Units.Quantity{$(QuoteNode(name))}()) * "|" + Units.symbol(::Units.Quantity{($(QuoteNode(name)),:phase_angle)}) = "∠" * Units.symbol(Units.Quantity{$(QuoteNode(name))}()) + end +end +Units.quantity(::typeof(gamma),::typeof(real)) = Units.quantity(alpha) +Units.quantity(::typeof(gamma),::typeof(imag)) = Units.quantity(beta) +Units.quantity(::typeof(Zc),::typeof(real)) = Units.Quantity{:characteristic_resistance}() +Units.quantity(::typeof(Zc),::typeof(imag)) = Units.Quantity{:characteristic_reactance}() +Units.quantity(::typeof(Yc),::typeof(real)) = Units.Quantity{:characteristic_conductance}() +Units.quantity(::typeof(Yc),::typeof(imag)) = Units.Quantity{:characteristic_susceptance}() +Units.quantity(::typeof(H),::typeof(real)) = Units.Quantity{:forward_response_real}() +Units.quantity(::typeof(H),::typeof(imag)) = Units.Quantity{:forward_response_imag}() +Units.quantity(::typeof(Tv),::typeof(real)) = Units.Quantity{:voltage_basis_real}() +Units.quantity(::typeof(Tv),::typeof(imag)) = Units.Quantity{:voltage_basis_imag}() +Units.quantity(::typeof(Ti),::typeof(real)) = Units.Quantity{:current_basis_real}() +Units.quantity(::typeof(Ti),::typeof(imag)) = Units.Quantity{:current_basis_imag}() + +Units.native_unit(::Units.Quantity{:attenuation_constant}) = Units.units(:base,:neper;per=(:base,:meter)) +Units.display_unit(::Units.Quantity{:attenuation_constant}) = Units.units(:base,:neper;per=(:kilo,:meter)) +Units.label(::Units.Quantity{:attenuation_constant}) = "Attenuation constant" +Units.symbol(::Units.Quantity{:attenuation_constant}) = "α" +Units.native_unit(::Units.Quantity{:phase_constant}) = Units.units(:base,:radian;per=(:base,:meter)) +Units.display_unit(::Units.Quantity{:phase_constant}) = Units.units(:base,:radian;per=(:kilo,:meter)) +Units.label(::Units.Quantity{:phase_constant}) = "Phase constant" +Units.symbol(::Units.Quantity{:phase_constant}) = "β" +Units.native_unit(::Units.Quantity{:phase_velocity}) = Units.units(:base,:meter;per=(:base,:second)) +Units.display_unit(::Units.Quantity{:phase_velocity}) = Units.native_unit(Units.Quantity{:phase_velocity}()) +Units.label(::Units.Quantity{:phase_velocity}) = "Phase velocity" +Units.symbol(::Units.Quantity{:phase_velocity}) = "vₚ" +for (name,base,scientific,sym) in ( + (:characteristic_resistance,:characteristic_impedance,"Characteristic resistance","R꜀"), + (:characteristic_reactance,:characteristic_impedance,"Characteristic reactance","X꜀"), + (:characteristic_conductance,:characteristic_admittance,"Characteristic conductance","G꜀"), + (:characteristic_susceptance,:characteristic_admittance,"Characteristic susceptance","B꜀"), + (:forward_response_real,:forward_response,"Real propagation function","Re(H)"), + (:forward_response_imag,:forward_response,"Imaginary propagation function","Im(H)"), + (:voltage_basis_real,:voltage_basis,"Real modal-to-phase voltage transformation","Re(Tv)"), + (:voltage_basis_imag,:voltage_basis,"Imaginary modal-to-phase voltage transformation","Im(Tv)"), + (:current_basis_real,:current_basis,"Real modal-to-phase current transformation","Re(Ti)"), + (:current_basis_imag,:current_basis,"Imaginary modal-to-phase current transformation","Im(Ti)")) + @eval begin + Units.native_unit(::Units.Quantity{$(QuoteNode(name))}) = Units.native_unit(Units.Quantity{$(QuoteNode(base))}()) + Units.display_unit(::Units.Quantity{$(QuoteNode(name))}) = Units.display_unit(Units.Quantity{$(QuoteNode(base))}()) + Units.label(::Units.Quantity{$(QuoteNode(name))}) = $scientific + Units.symbol(::Units.Quantity{$(QuoteNode(name))}) = $sym + end +end +Units.native_unit(::Units.Quantity{:propagation_constant}) = + Units.units(:base,:dimensionless;per=(:base,:meter)) +Units.display_unit(::Units.Quantity{:propagation_constant}) = + Units.units(:base,:dimensionless;per=(:kilo,:meter)) +Units.native_unit(::Units.Quantity{:characteristic_impedance}) = Units.units(:base,:ohm) +Units.display_unit(::Units.Quantity{:characteristic_impedance}) = Units.units(:base,:ohm) +Units.native_unit(::Units.Quantity{:characteristic_admittance}) = Units.units(:base,:siemens) +Units.display_unit(::Units.Quantity{:characteristic_admittance}) = Units.units(:base,:siemens) +for name in (:forward_response,:voltage_basis,:current_basis) + @eval begin + Units.native_unit(::Units.Quantity{$(QuoteNode(name))}) = Units.units(:base,:dimensionless) + Units.display_unit(::Units.Quantity{$(QuoteNode(name))}) = Units.units(:base,:dimensionless) + end +end +Units.label(::Units.Quantity{:propagation_constant}) = "Propagation constant" +Units.label(::Units.Quantity{:characteristic_impedance}) = "Characteristic impedance" +Units.label(::Units.Quantity{:characteristic_admittance}) = "Characteristic admittance" +Units.label(::Units.Quantity{:forward_response}) = "Propagation function" +Units.label(::Units.Quantity{:voltage_basis}) = "Modal-to-phase voltage transformation" +Units.label(::Units.Quantity{:current_basis}) = "Modal-to-phase current transformation" +Units.symbol(::Units.Quantity{:propagation_constant}) = "γ" +Units.symbol(::Units.Quantity{:characteristic_impedance}) = "Zc" +Units.symbol(::Units.Quantity{:characteristic_admittance}) = "Yc" +Units.symbol(::Units.Quantity{:forward_response}) = "H" +Units.symbol(::Units.Quantity{:voltage_basis}) = "Tv" +Units.symbol(::Units.Quantity{:current_basis}) = "Ti" + +for (name,parent,scientific,sym) in ( + (:phase_characteristic_impedance,:characteristic_impedance,"Phase-domain characteristic impedance","Zc,phase"), + (:phase_characteristic_admittance,:characteristic_admittance,"Phase-domain characteristic admittance","Yc,phase"), + (:voltage_propagation_function,:forward_response,"Voltage propagation function","Hᵥ"), + (:current_propagation_function,:forward_response,"Current propagation function","Hᵢ")) + @eval begin + Units.native_unit(::Units.Quantity{$(QuoteNode(name))}) = Units.native_unit(Units.Quantity{$(QuoteNode(parent))}()) + Units.display_unit(::Units.Quantity{$(QuoteNode(name))}) = Units.display_unit(Units.Quantity{$(QuoteNode(parent))}()) + Units.label(::Units.Quantity{$(QuoteNode(name))}) = $scientific + Units.symbol(::Units.Quantity{$(QuoteNode(name))}) = $sym + Units.native_unit(::Units.Quantity{($(QuoteNode(name)),:magnitude)}) = Units.native_unit(Units.Quantity{$(QuoteNode(name))}()) + Units.display_unit(::Units.Quantity{($(QuoteNode(name)),:magnitude)}) = Units.display_unit(Units.Quantity{$(QuoteNode(name))}()) + Units.label(::Units.Quantity{($(QuoteNode(name)),:magnitude)}) = $scientific * " magnitude" + Units.symbol(::Units.Quantity{($(QuoteNode(name)),:magnitude)}) = "|" * $sym * "|" + Units.native_unit(::Units.Quantity{($(QuoteNode(name)),:phase_angle)}) = Units.units(:base,:radian) + Units.display_unit(::Units.Quantity{($(QuoteNode(name)),:phase_angle)}) = Units.units(:base,:degree) + Units.label(::Units.Quantity{($(QuoteNode(name)),:phase_angle)}) = $scientific * " angle" + Units.symbol(::Units.Quantity{($(QuoteNode(name)),:phase_angle)}) = "∠" * $sym + Units.native_unit(::Units.Quantity{($(QuoteNode(name)),:real)}) = Units.native_unit(Units.Quantity{$(QuoteNode(name))}()) + Units.display_unit(::Units.Quantity{($(QuoteNode(name)),:real)}) = Units.display_unit(Units.Quantity{$(QuoteNode(name))}()) + Units.label(::Units.Quantity{($(QuoteNode(name)),:real)}) = "Real " * lowercase($scientific) + Units.symbol(::Units.Quantity{($(QuoteNode(name)),:real)}) = "Re(" * $sym * ")" + Units.native_unit(::Units.Quantity{($(QuoteNode(name)),:imag)}) = Units.native_unit(Units.Quantity{$(QuoteNode(name))}()) + Units.display_unit(::Units.Quantity{($(QuoteNode(name)),:imag)}) = Units.display_unit(Units.Quantity{$(QuoteNode(name))}()) + Units.label(::Units.Quantity{($(QuoteNode(name)),:imag)}) = "Imaginary " * lowercase($scientific) + Units.symbol(::Units.Quantity{($(QuoteNode(name)),:imag)}) = "Im(" * $sym * ")" + end +end + +const _phase_Zc = Base.Fix2(Zc,(domain=PhaseDomain,)) +const _phase_Yc = Base.Fix2(Yc,(domain=PhaseDomain,)) +const _voltage_H = Base.Fix2(H,(domain=PhaseDomain,field=:voltage)) +const _current_H = Base.Fix2(H,(domain=PhaseDomain,field=:current)) + +# Finite representation bindings include only domain and, for H, field. +function _validate_phase_selector(selector::Base.Fix2) + selector.f in (H,Zc,Yc) || throw(ArgumentError("unsupported finite representation selector")) + value=selector.x + value isa NamedTuple || throw(ArgumentError("representation binding must be a NamedTuple")) + if selector.f===H + keys(value)==(:domain,:field) && value.domain===PhaseDomain && + value.field in (:voltage,:current) || throw(ArgumentError("invalid phase H binding")) + else + keys(value)==(:domain,) && value.domain===PhaseDomain || + throw(ArgumentError("invalid phase characteristic binding")) + end + return selector +end + +const _ModalLineParameters = LineParameters{T,U,D} where {T<:Complex,U<:Real,D<:ModalDomain} +for selector in (gamma,Zc,Yc,Tv,Ti) + @eval function observe(source::_ModalLineParameters,::typeof($selector),indices...) + values=$selector(source) + return isempty(indices) ? values : getindex(values,indices...) + end +end +for selector in (gamma,alpha,beta,velocity,Zc,Yc,H,Tv,Ti) + @eval function observe(source::PropagationParameters,::typeof($selector),indices...) + values=$selector(source) + return isempty(indices) ? values : getindex(values,indices...) + end +end +function observe(source::Union{_ModalLineParameters,PropagationParameters}, + selector::Base.Fix2{typeof(Zc)},indices...) + _validate_phase_selector(selector) + values=Zc(source,PhaseDomain) + return isempty(indices) ? values : getindex(values,indices...) +end +function observe(source::Union{_ModalLineParameters,PropagationParameters}, + selector::Base.Fix2{typeof(Yc)},indices...) + _validate_phase_selector(selector) + values=Yc(source,PhaseDomain) + return isempty(indices) ? values : getindex(values,indices...) +end +function observe(source::PropagationParameters, + selector::Base.Fix2{typeof(H)},indices...) + _validate_phase_selector(selector) + values=H(source,PhaseDomain;field=selector.x.field) + return isempty(indices) ? values : getindex(values,indices...) +end +const _ModalTransform=Union{typeof(abs),typeof(angle),typeof(real),typeof(imag)} +for selector in (gamma,Zc,Yc,Tv,Ti), source_type in (:_ModalLineParameters,:PropagationParameters) + @eval observe(source::$source_type,::typeof($selector), + transform::_ModalTransform,indices...) = transform.(observe(source,$selector,indices...)) +end +@eval observe(source::PropagationParameters,::typeof(H), + transform::_ModalTransform,indices...) = transform.(observe(source,H,indices...)) +for selector in (Zc,Yc), source_type in (:_ModalLineParameters,:PropagationParameters) + @eval observe(source::$source_type,bound::Base.Fix2{typeof($selector)}, + transform::_ModalTransform,indices...) = transform.(observe(source,bound,indices...)) +end +observe(source::PropagationParameters,bound::Base.Fix2{typeof(H)}, + transform::_ModalTransform,indices...) = transform.(observe(source,bound,indices...)) + +_modal_transform_requests(selectors) = Tuple((selector,transform) for selector in selectors + for transform in (abs,angle,real,imag)) + +function observables(::Type{<:LineParameters{T,U,D}}) where {T<:Complex,U<:Real,D<:ModalDomain} + selectors=(gamma,Zc,Yc,Tv,Ti,_phase_Zc,_phase_Yc) + return (observables(LineParameters)...,selectors..., + _modal_transform_requests(selectors)...) +end +function observables(::Type{<:PropagationParameters}) + selectors=(gamma,Zc,Yc,H,Tv,Ti,_phase_Zc,_phase_Yc,_voltage_H,_current_H) + return (selectors...,alpha,beta,velocity,_modal_transform_requests(selectors)...) +end + +Commons.normalize_observation_selector(::typeof(alpha)) = (gamma,real) +Commons.normalize_observation_selector(::typeof(beta)) = (gamma,imag) +Commons.normalize_observation_selector(::Val{:alpha}) = Commons.normalize_observation_selector(alpha) +Commons.normalize_observation_selector(::Val{:beta}) = Commons.normalize_observation_selector(beta) + +function _normalize_modal_request(source,request) + identity=request_identity(request) + prefix=identity isa Tuple ? identity : (identity,) + selector=first(prefix) + selector in observables(typeof(source)) || + selector in (gamma,alpha,beta,velocity,Zc,Yc,H,Tv,Ti) || + throw(ArgumentError("unsupported modal quantity")) + selector isa Base.Fix2 && _validate_phase_selector(selector) + length(prefix)<=2 && (length(prefix)==1 || prefix[2] in (abs,angle,real,imag)) || + throw(ArgumentError("unsupported modal transform")) + rank=selector in (Tv,Ti,_phase_Zc,_phase_Yc,_voltage_H,_current_H) ? 3 : 2 + indices=request_indices(request) + isempty(indices) && (indices=ntuple(_->Colon(),rank)) + length(indices)==rank || throw(DimensionMismatch("modal request requires $rank selectors")) + dimensions=rank==2 ? size(gamma(source)) : size(Tv(source)) + map(observation_indices,indices,dimensions) + return (prefix...,indices...) +end + +function Commons.observation_requests(source::_ModalLineParameters,requests::Tuple;complete_pairs::Bool=false) + selected=isempty(requests) ? (Engine.Z,Engine.Y,gamma,Zc,Yc,Tv,Ti) : requests + primary=Tuple(filter(request -> begin + identity=request_identity(request) + first(identity isa Tuple ? identity : (identity,)) in + (Engine.Z,Engine.Y,Engine.R,Engine.X,Engine.L,Engine.G,Engine.B,Engine.C,real,imag,abs,angle) + end,selected)) + derived=Tuple(filter(request -> request ∉ primary,selected)) + normalized_primary=isempty(primary) ? () : + invoke(Commons.observation_requests, + Tuple{LineParameters,Tuple}, + source,primary;complete_pairs).retained + normalized_derived=_modal_component_requests(source,derived;complete_pairs) + retained=(normalized_primary...,normalized_derived...) + allunique(retained) || throw(ArgumentError("observation requests must be distinct")) + return (retained=retained,displayed=selected) +end + +function Commons.observation_requests(source::PropagationParameters,requests::Tuple;complete_pairs::Bool=false) + selected=isempty(requests) ? (gamma,Zc,Yc,H,Tv,Ti,velocity) : requests + normalized=_modal_component_requests(source,selected;complete_pairs) + allunique(normalized) || throw(ArgumentError("observation requests must be distinct")) + return (retained=normalized,displayed=selected) +end + +function _modal_component_requests(source,selected;complete_pairs) + normalized=Tuple[] + complex_selectors=(gamma,Zc,Yc,H,Tv,Ti,_phase_Zc,_phase_Yc,_voltage_H,_current_H) + for request in selected + item=_normalize_modal_request(source,request) + identity=Commons.normalize_observation_selector(request_identity(item)) + prefix=identity isa Tuple ? identity : (identity,) + item=(prefix...,request_indices(item)...) + if identity in complex_selectors + append!(normalized,((identity,real,request_indices(item)...), + (identity,imag,request_indices(item)...))) + else + push!(normalized,item) + end + end + allunique(normalized) || throw(ArgumentError("observation requests must be distinct")) + for item in copy(normalized) + identity=request_identity(item) + identity isa Tuple && length(identity)==2 && last(identity) in (real,imag,abs,angle) || continue + selector=first(identity) + selector in complex_selectors || continue + other=last(identity)===real ? imag : + last(identity)===imag ? real : last(identity)===abs ? angle : abs + companion=(selector,other,request_indices(item)...) + companion in normalized && continue + complete_pairs || throw(ArgumentError("$(nameof(selector isa Base.Fix2 ? selector.f : selector)) requires a complete component pair")) + push!(normalized,companion) + end + return Tuple(normalized) +end + +function _modal_coordinates(source,request,values) + identity=request_identity(request) + selector=first(identity isa Tuple ? identity : (identity,)) + dims=size(values) + indices=request_indices(request) + selected=map(observation_indices,indices,dims) + n=size(Tv(source),1) + modes=string.(1:n) + phase=get(details(source isa PropagationParameters ? source.parameters : source).data, + :phase_coordinates,nothing) + phase=phase===nothing ? string.(1:n) : string.(phase) + mixed=selector in (Tv,Ti) + phase_matrix=selector in (_phase_Zc,_phase_Yc,_voltage_H,_current_H) + labels=mixed || phase_matrix ? phase : modes + if length(dims)==2 + return (kind=:vector,indices,axis=:mode,axis_label="Mode", + positions=selected[1], + samples=selected[2],frequencies=copy(frequencies(source)[selected[2]]), + frequency_unit=Units.units(:base,:hertz),extent=(n,length(frequencies(source))), + labels,domain=:ModalDomain) + end + record=(kind=:matrix,indices,rows=selected[1],columns=selected[2],samples=selected[3], + frequencies=copy(frequencies(source)[selected[3]]), + frequency_unit=Units.units(:base,:hertz),extent=(n,n,length(frequencies(source))), + labels,domain=mixed || phase_matrix ? :PhaseDomain : :ModalDomain) + return mixed ? merge(record,(column_labels=modes,column_domain=:ModalDomain)) : record +end + +function _modal_observation_quantity(source,request;unit=nothing,clip=true,atol=nothing,frequencies=nothing) + frequencies===nothing || frequencies==LineCableModels.frequencies(source) || + throw(ArgumentError("derived modal observation uses stored frequencies")) + atol===nothing || throw(ArgumentError("derived modal quantities have no engineering cutoff")) + identity=request_identity(request) + prefix=identity isa Tuple ? identity : (identity,) + selector=first(prefix) + transform=length(prefix)==2 ? prefix[2] : identity + raw=observe(source,selector) + indices=request_indices(request) + original=getindex(raw,indices...) + values=length(prefix)==2 ? transform.(original) : original + available=Engine.resolution_available.(original) .& Engine.resolution_available.(values) + origin=length(prefix)==2 && transform===angle ? iszero.(nominal.(original)) : false + magnitude_origin=length(prefix)==2 && transform===abs ? + iszero.(nominal.(original)) : false + available=available .& .!origin + reasons=broadcast(available,origin,magnitude_origin) do valid,at_zero,at_magnitude_origin + valid ? nothing : at_zero ? :undefined_phase : + at_magnitude_origin ? :undefined_first_order_magnitude : :nonfinite_value + end + resolved=broadcast((value,valid)->valid ? value : missing,values,available) + q=Commons.request_quantity(request) + native=Units.native_unit(q,basis(source)) + target=unit===nothing ? Units.display_unit(q,basis(source)) : unit + T=typeof(float(real(nominal(zero(eltype(raw)))))) + factor=Units.scale_factor(native,target,T) + assumptions=Engine.observation_assumptions(source,selector) + undefined=any(x -> x===:undefined_first_order_magnitude, + reasons isa AbstractArray ? reasons : (reasons,)) + components=undefined ? (nominal_magnitude=Commons.detach(abs.(nominal.(original))), + real=Commons.detach(real.(original)),imaginary=Commons.detach(imag.(original)), + unit=Units.native_unit(selector isa Base.Fix2 ? selector.f : selector,basis(source))) : nothing + return (request,quantity=q,family=Symbol(nameof(selector isa Base.Fix2 ? selector.f : selector)), + statistic=:value,values=Commons.detach(resolved,factor),unit=target, + basis=basis(source),coordinates=_modal_coordinates(source,request,raw), + assumptions,thresholds=nothing,available,engineering_zero=false,clipped=false, + missing_reason=reasons,unavailable_components=components) +end + +function Engine.observation_assumptions( + source::Union{_ModalLineParameters,PropagationParameters},selector) + retained=details(source).data + upstream=get(retained,:selections,nothing) + modal=get(retained,:modal,nothing) + return upstream===nothing || modal===nothing ? nothing : + (upstream=(Z=get(upstream,:Z,nothing),Y=get(upstream,:Y,nothing)), + modal=get(modal,:effective,nothing),rotate=get(modal,:rotate,nothing)) +end + +function Commons.observation_quantity(source::_ModalLineParameters,request;kwargs...) + identity=request_identity(request) + selector=first(identity isa Tuple ? identity : (identity,)) + if selector in (Engine.Z,Engine.Y,Engine.R,Engine.X,Engine.L,Engine.G,Engine.B,Engine.C) + return invoke(Commons.observation_quantity, + Tuple{LineParameters,Any}, + source,request;kwargs...) + end + return _modal_observation_quantity(source,request;kwargs...) +end +Commons.observation_quantity(source::PropagationParameters,request;kwargs...) = + _modal_observation_quantity(source,request;kwargs...) + +function Commons.observation_gridpoint(source::PropagationParameters) + original=Commons.observation_gridpoint(source.parameters) + inputs=original.inputs===nothing ? (segment=(line_length=source.line_length,),) : + merge(original.inputs,(segment=(line_length=source.line_length,),)) + return merge(original,(id=get(source.details.data,:gridpoint,nothing), + source_gridpoint=get(source.details.data,:source_gridpoint,nothing),inputs, + transformation=get(source.details.data,:modal,nothing))) +end diff --git a/src/engine/modalanalysis/problems.jl b/src/engine/modalanalysis/problems.jl new file mode 100644 index 000000000..6b4aa7f1d --- /dev/null +++ b/src/engine/modalanalysis/problems.jl @@ -0,0 +1,40 @@ +""" +$(TYPEDEF) + +Define modal analysis of one completed phase-domain frequency scan. + +$(TYPEDFIELDS) +""" +struct ModalAnalysisProblem{P <: LineParameters} <: AbstractProblemDefinition + "Source line parameters." + parameters::P + + function ModalAnalysisProblem{P}(parameters::P) where {P <: LineParameters} + return validate(new{P}(parameters)) + end +end + +function validate(problem::ModalAnalysisProblem) + validate(problem.parameters) + parameters = problem.parameters + size(parameters.Z, 1) > 0 || throw(ArgumentError("modal analysis requires at least one mode")) + isempty(parameters.f) && throw(ArgumentError("modal analysis requires frequency samples")) + all(>(zero(eltype(parameters.f))), parameters.f) || + throw(DomainError(parameters.f, "modal analysis requires positive frequencies")) + all(>(zero(eltype(parameters.f))), diff(parameters.f)) || + throw(ArgumentError("modal analysis requires strictly increasing frequencies")) + all(isfinite, parameters.Z.values) || + throw(DomainError(parameters.Z.values, "series impedance must be finite")) + all(isfinite, parameters.Y.values) || + throw(DomainError(parameters.Y.values, "shunt admittance must be finite")) + return problem +end + +function ModalAnalysisProblem( + parameters::LineParameters{T, U, PhaseDomain} +) where {T, U} + return ModalAnalysisProblem{typeof(parameters)}(parameters) +end + +Base.eltype(problem::ModalAnalysisProblem) = eltype(problem.parameters) +Base.eltype(::Type{ModalAnalysisProblem{P}}) where {P} = eltype(P) diff --git a/src/engine/modalanalysis/propagation.jl b/src/engine/modalanalysis/propagation.jl new file mode 100644 index 000000000..e8731bac3 --- /dev/null +++ b/src/engine/modalanalysis/propagation.jl @@ -0,0 +1,177 @@ +""" + PropagationParameters(modal; line_length=line_length(modal)) + +Bind per-unit-length modal parameters to one line segment. +The source normalization length and active segment length are independent. +""" +struct PropagationParameters{P <: LineParameters, L <: Real, D <: ComputationDetails} <: + AbstractCoreResult + parameters::P + line_length::L + details::D + + function PropagationParameters(parameters::P, line_length::L, + details::D) where + {P <: LineParameters, L <: Real, D <: ComputationDetails} + Engine.domain(parameters)===ModalDomain || throw(ArgumentError( + "PropagationParameters require modal-domain coefficients")) + basis(parameters)===:pul || throw(ArgumentError( + "PropagationParameters require per-unit-length coefficients")) + _segment_length(line_length) + haskey(details.data, :gridpoint) && haskey(details.data, :source_gridpoint) || + throw(ArgumentError("segment details must retain gridpoint ancestry")) + retained=get(details.data, :segment, nothing) + retained isa NamedTuple && haskey(retained, :normalization_length) && + haskey(retained, :line_length) || + throw(ArgumentError("segment details must retain the completed length description")) + isequal(retained.line_length, line_length) || + throw(ArgumentError("segment details must describe the stored line_length")) + return new{P, L, D}(parameters, line_length, details) + end +end + +function _segment_length(value) + value isa Real && !(value isa Bool) && isfinite(nominal(value)) && + nominal(value) >= 0 || + throw(ArgumentError("segment line_length must be finite and nonnegative")) + return value +end + +function per_unit_length(parameters::LineParameters{ + T, U, D, :pul}) where {T, U, D <: ModalDomain} + return parameters +end + +function per_unit_length(parameters::LineParameters{ + T, U, D, :total}) where {T, U, D <: ModalDomain} + ell0 = line_length(parameters) + ell0 isa Real && !(ell0 isa Bool) && isfinite(nominal(ell0)) && + nominal(ell0) > 0 || throw(ArgumentError( + "total modal coefficients require a known positive source normalization length")) + modal = ModalDomain(parameters.domain.operators, gamma(parameters) ./ ell0) + return LineParameters(modal, + SeriesImpedance(parameters.Z.values ./ ell0; basis = :pul), + ShuntAdmittance(parameters.Y.values ./ ell0; basis = :pul), + parameters.f, parameters.details) +end + +function PropagationParameters(parameters::LineParameters{T, U, D}; + line_length = LineCableModels.line_length(parameters), combine::Symbol = :product) where { + T, U, D <: ModalDomain} + return parameterize(PropagationParameters, _bind_segment, (parameters, line_length); combine) +end + +function _bind_segment(parameters::LineParameters{T, U, D}, line_length) where { + T, U, D <: ModalDomain} + line_length === nothing && throw(ArgumentError( + "segment line_length is required when the source has no declared length")) + segment_length = _segment_length(line_length) + per_unit_length_parameters = per_unit_length(parameters) + source_length = LineCableModels.line_length(parameters) + source_gridpoint = get(parameters.details.data, :gridpoint, nothing) + segment_gridpoint = isequal(segment_length, source_length) ? source_gridpoint : + Commons.gridpoint_id() + source_record=(; + (key=>value + for (key, value) in pairs(parameters.details.data) + if key ∉ (:timing, :comparison_unsupported))...) + segment_details = merge(source_record, + (gridpoint = segment_gridpoint, source_gridpoint, + segment = (line_length = segment_length, normalization_length = source_length))) + completed_details = ComputationDetails(segment_details) + return PropagationParameters(per_unit_length_parameters, segment_length, completed_details) +end + +function PropagationParameters(source::PropagationParameters; + line_length = source.line_length, combine::Symbol = :product) + return parameterize(PropagationParameters, _bind_segment, (source, line_length); combine) +end + +function _bind_segment(source::PropagationParameters, line_length) + segment_length = _segment_length(line_length) + if isequal(segment_length, source.line_length) + return source + end + source_record=(; + (key=>value + for (key, value) in pairs(source.details.data) + if key ∉ (:timing, :comparison_unsupported))...) + segment_details = merge(source_record, + (gridpoint = Commons.gridpoint_id(), + source_gridpoint = get(source.details.data, :gridpoint, nothing), + segment = merge(source.details.data.segment, (line_length = segment_length,)))) + return PropagationParameters(source.parameters, segment_length, ComputationDetails(segment_details)) +end + +line_length(source::PropagationParameters) = source.line_length +basis(::PropagationParameters) = :pul +frequencies(source::PropagationParameters) = frequencies(source.parameters) +details(source::PropagationParameters) = source.details +Tv(source::PropagationParameters) = Tv(source.parameters) +Ti(source::PropagationParameters) = Ti(source.parameters) +gamma(source::PropagationParameters) = gamma(source.parameters) + +"""Return modal attenuation constants in inverse metres, ordered mode × frequency.""" +alpha(source::PropagationParameters) = real.(gamma(source)) + +"""Return modal phase constants in radians per metre, ordered mode × frequency.""" +beta(source::PropagationParameters) = imag.(gamma(source)) + +"""Return phase velocities in metres per second from `2πf / beta`, ordered mode × frequency.""" +function velocity(source::PropagationParameters) + constants=beta(source) + f=frequencies(source) + return map(CartesianIndices(constants)) do index + value=constants[index] + T=promote_type(typeof(float(real(nominal(value)))), typeof(float(nominal(f[index[2]])))) + (T(2)*T(π)*f[index[2]])/value + end +end +function Zc(source::PropagationParameters, domain::Type{<:Engine.LineParamsDomain} = ModalDomain) + Zc(source.parameters, domain) +end +function Yc(source::PropagationParameters, domain::Type{<:Engine.LineParamsDomain} = ModalDomain) + Yc(source.parameters, domain) +end + +function Base.getindex(source::PropagationParameters, + selected::Union{ + Integer, AbstractRange{<:Integer}, AbstractVector{<:Integer}, Colon}) + parameters=source.parameters[selected] + retained=ComputationDetails(merge(source.details.data, + (modal = get(parameters.details.data, :modal, nothing),))) + return PropagationParameters(parameters, source.line_length, retained) +end + +"""Forward-wave factors over the segment's bound physical length.""" +H(source::PropagationParameters) = H(source, ModalDomain) + +function H(source::PropagationParameters, ::Type{ModalDomain}; field = nothing) + field === nothing || throw(ArgumentError("modal H does not accept a field")) + return exp.(-gamma(source) .* source.line_length) +end + +function H(source::PropagationParameters, ::Type{PhaseDomain}; field) + field in (:voltage, :current) || + throw(ArgumentError("field must be :voltage or :current")) + return _phase_H(source, Val(field)) +end + +function _phase_H(source::PropagationParameters, ::Val{:voltage}) + return _similarity_H(H(source), Tv(source)) +end +function _phase_H(source::PropagationParameters, ::Val{:current}) + return _similarity_H(H(source), Ti(source)) +end + +function _similarity_H(factors, maps) + n, nf = size(factors) + S=promote_type(eltype(factors), eltype(maps)) + result=Array{S, 3}(undef, n, n, nf) + for frequency in 1:nf + basis_matrix=@view maps[:, :, frequency] + @views result[:, :, frequency] .= (basis_matrix * Diagonal(factors[:, frequency])) / + basis_matrix + end + return result +end diff --git a/src/engine/modalanalysis/quantities.jl b/src/engine/modalanalysis/quantities.jl new file mode 100644 index 000000000..49f3378be --- /dev/null +++ b/src/engine/modalanalysis/quantities.jl @@ -0,0 +1,52 @@ +"""Return the retained modal-to-phase voltage basis (phase × mode × frequency).""" +Tv(parameters::LineParameters{T,U,D}) where {T,U,D<:ModalDomain} = + parameters.domain.operators.Tv + +"""Return the retained modal-to-phase current basis (phase × mode × frequency).""" +Ti(parameters::LineParameters{T,U,D}) where {T,U,D<:ModalDomain} = + parameters.domain.operators.Ti + +"""Return the retained propagation roots (mode × frequency).""" +gamma(parameters::LineParameters{T,U,D}) where {T,U,D<:ModalDomain} = + parameters.domain.gamma + +function _characteristic(parameters::LineParameters{T,U,D}, quantity::Val, + ::Type{ModalDomain}) where {T,U,D<:ModalDomain} + coefficients = quantity isa Val{:Zc} ? parameters.Z.values : parameters.Y.values + roots = gamma(parameters) + n, _, nf = size(coefficients) + S = promote_type(T,eltype(roots)) + result = Array{S,2}(undef,n,nf) + for frequency in 1:nf, mode in 1:n + result[mode,frequency] = coefficients[mode,mode,frequency] / roots[mode,frequency] + end + return result +end + +function _characteristic(parameters::LineParameters{T,U,D}, quantity::Val, + ::Type{PhaseDomain}) where {T,U,D<:ModalDomain} + diagonal = _characteristic(parameters,quantity,ModalDomain) + maps = operators(parameters) + n,nf = size(diagonal) + S = promote_type(eltype(diagonal),eltype(maps.Tv),eltype(maps.Ti)) + result = Array{S,3}(undef,n,n,nf) + for frequency in 1:nf + if quantity isa Val{:Zc} + left = @view maps.Tv[:,:,frequency] + right = @view maps.Ti[:,:,frequency] + else + left = @view maps.Ti[:,:,frequency] + right = @view maps.Tv[:,:,frequency] + end + @views result[:,:,frequency] .= (left * Diagonal(diagonal[:,frequency])) / right + end + return result +end + +"""Characteristic impedance of the selected diagonal modal approximation.""" +Zc(parameters::LineParameters{T,U,D}, domain::Type{<:Engine.LineParamsDomain}=ModalDomain) where {T,U,D<:ModalDomain} = + _characteristic(parameters,Val(:Zc),domain) + +"""Characteristic admittance of the selected diagonal modal approximation.""" +Yc(parameters::LineParameters{T,U,D}, domain::Type{<:Engine.LineParamsDomain}=ModalDomain) where {T,U,D<:ModalDomain} = + _characteristic(parameters,Val(:Yc),domain) diff --git a/src/engine/modalanalysis/textdisplay.jl b/src/engine/modalanalysis/textdisplay.jl new file mode 100644 index 000000000..998f18e99 --- /dev/null +++ b/src/engine/modalanalysis/textdisplay.jl @@ -0,0 +1,96 @@ +TextDisplay.name(::Type{<:ModalAnalysisProblem}) = "ModalAnalysisProblem" +function Base.summary(io::IO, problem::ModalAnalysisProblem) + print(io, "Modal analysis problem, ", size(problem.parameters.Z, 1), " modes") +end +function Base.show(io::IO, problem::ModalAnalysisProblem) + print(io, "ModalAnalysisProblem(", size(problem.parameters.Z, 1), " modes, ", + length(problem.parameters.f), " frequencies)") +end +function Base.show(io::IO, ::MIME"text/plain", problem::ModalAnalysisProblem) + get(io, :compact, false) && return show(io, problem) + return TextDisplay.tree(io, + "Modal analysis problem", + ( + (label = "source phase-domain LineParameters", noun = "fields"), + (label = "modes $(size(problem.parameters.Z,1))", noun = "fields"), + ( + label = "frequency $(first(problem.parameters.f))–$(last(problem.parameters.f)) Hz ($(length(problem.parameters.f)) samples)", + noun = "fields") + )) +end + +TextDisplay.name(::Type{<:ModalAnalysisFormulation}) = "ModalAnalysisFormulation" +function Base.summary(io::IO, formulation::ModalAnalysisFormulation) + print(io, "Modal analysis formulation, :", formula_id(formulation.formula)) +end +function Base.show(io::IO, formulation::ModalAnalysisFormulation) + print(io, "ModalAnalysisFormulation(:", formula_id(formulation.formula), ")") +end +function Base.show(io::IO, ::MIME"text/plain", formulation::ModalAnalysisFormulation) + get(io, :compact, false) && return show(io, formulation) + return TextDisplay.tree(io, + "Modal analysis formulation", + ( + (label = "formula :$(formula_id(formulation.formula))", noun = "fields"), + ( + label = "controls $(length(formulation_options(formulation.formula).data)) sections", + noun = "fields") + )) +end + +TextDisplay.name(::Type{<:ModalAnalysisWorkspace}) = "ModalAnalysisWorkspace" +function Base.summary(io::IO, workspace::ModalAnalysisWorkspace) + print(io, "Modal analysis workspace, ", size(workspace.Ti, 1), " modes") +end +function Base.show(io::IO, workspace::ModalAnalysisWorkspace) + print(io, "ModalAnalysisWorkspace(", join(size(workspace.Ti), '×'), ")") +end +function Base.show(io::IO, ::MIME"text/plain", workspace::ModalAnalysisWorkspace) + get(io, :compact, false) && return show(io, workspace) + return TextDisplay.tree(io, + "Modal analysis workspace", + ( + (label = "bases $(join(size(workspace.Ti),'×'))", noun = "fields"), + (label = "roots $(join(size(workspace.roots),'×'))", noun = "fields") + )) +end + +TextDisplay.name(::Type{<:ModalOperators}) = "ModalOperators" +function Base.summary(io::IO, maps::ModalOperators) + print(io, "Modal operators, ", join(size(maps.Tv), '×')) +end +function Base.show(io::IO, maps::ModalOperators) + print(io, "ModalOperators(Tv=", join(size(maps.Tv), '×'), + ", Ti=", join(size(maps.Ti), '×'), ")") +end +function Base.show(io::IO, ::MIME"text/plain", maps::ModalOperators) + get(io, :compact, false) && return show(io, maps) + return TextDisplay.tree(io, + "Modal operators", + ( + (label = "Tv $(join(size(maps.Tv),'×'))", noun = "fields"), + (label = "Ti $(join(size(maps.Ti),'×'))", noun = "fields") + )) +end + +TextDisplay.name(::Type{<:PropagationParameters}) = "PropagationParameters" +function Base.summary(io::IO, source::PropagationParameters) + print(io, "Propagation parameters over ", source.line_length, " m") +end +function Base.show(io::IO, source::PropagationParameters) + print(io, "PropagationParameters(length=", source.line_length, ", modes=", + size(Tv(source), 1), ")") +end +function Base.show(io::IO, ::MIME"text/plain", source::PropagationParameters) + get(io, :compact, false) && return show(io, source) + return TextDisplay.tree(io, + "Propagation parameters · line segment", + ( + (label = "length $(source.line_length) m", noun = "fields"), + (label = "modes $(size(Tv(source),1))", noun = "fields"), + ( + label = "frequency $(first(frequencies(source)))–$(last(frequencies(source))) Hz ($(length(frequencies(source))) samples)", + noun = "fields"), + (label = "basis :pul", noun = "fields") + )) +end diff --git a/src/engine/observed_inputs.jl b/src/engine/observed_inputs.jl new file mode 100644 index 000000000..fb79f14ef --- /dev/null +++ b/src/engine/observed_inputs.jl @@ -0,0 +1,196 @@ +# This projection is restricted to physical declarations owned by the package. +# It retains their named values, not constructed problems or geometry caches. +_input_record(value::Union{Number,Symbol,AbstractString,Nothing,Missing,Type}) = value +_input_record(value::NamedTuple) = map(_input_record, value) +_input_record(value::Tuple) = map(_input_record, value) +_input_record(value::AbstractArray) = map(_input_record, value) +_input_record(value::Pair) = _input_record(first(value)) => _input_record(last(value)) +_input_record(value::AbstractDict) = Dict(_input_record(k) => _input_record(v) for (k,v) in value) + +function _input_record(value::Union{DataModel.AbstractCablePart,DataModel.AbstractShape, + DataModel.Pose2,DataModel.Shell,DataModel.Ring,DataModel.Polar,DataModel.Fill, + DataModel.Lattice,DataModel.Helix,DataModel.LayRatio,DataModel.Pitch, + DataModel.LayAngle,DataModel.FillFactor,DataModel.AssemblyMember, + Materials.AbstractMaterial,Earth.AbstractEarthModel,Earth.AbstractEarthLayer, + Earth.AbstractEarthMaterial}) + fields = fieldnames(typeof(value)) + return merge((kind=nameof(typeof(value)),field_descriptions=Commons.input_fields(typeof(value))), + NamedTuple{fields}(map(name -> _input_record(getfield(value,name)), fields))) +end + +_input_record(design::CableDesign) = (cable_id=design.cable_id, + origin=_input_record(design.origin), + terminal_order=copy(design.terminal_order)) + +function _input_record(system::LineCableSystem) + return (system_id=system.system_id, field_descriptions=Commons.input_fields(typeof(system)), line_length=system.line_length, + designs=_input_record(system.designs), positions=_input_record(system.positions), + input_positions=_input_record(system.input_positions), clearances=copy(system.clearances), + connections=_input_record(system.connections), environment=_input_record(system.environment), + terminal_order=copy(system.terminal_order), connection_order=copy(system.connection_order)) +end + +""" +$(TYPEDSIGNATURES) + +Capture physical declarations from a completed package-owned problem. Store the +returned record in result details at completion, once per physical point. Later +observation reads that record without evaluating problem builders. +""" +function completed_inputs(problem::LineParametersProblem) + Record=NamedTuple{(:system,:temperature,:earth_props,:frequencies,:field_descriptions), + Tuple{NamedTuple,typeof(problem.temperature),NamedTuple,typeof(problem.frequencies),NamedTuple}} + return Record((_input_record(problem.system),problem.temperature,_input_record(problem.earth_props), + copy(problem.frequencies),(temperature=(name="temperature",unit="°C"),))) +end +function completed_inputs(problem::CableConstantsProblem) + Record=NamedTuple{(:design,:temperature,:frequency,:field_descriptions), + Tuple{NamedTuple,typeof(problem.temperature),typeof(problem.frequency),NamedTuple}} + return Record((_input_record(problem.design),problem.temperature,problem.frequency, + (temperature=(name="temperature",unit="°C"),frequency=(name="frequency",unit="Hz")))) +end + +# Capture owner-scoped meanings and names while the selected objects exist. +# Common-field omission later compares selections, never punctuation. +""" +$(TYPEDSIGNATURES) + +Capture actual formulation selections, controls, and structured description +fields when a result completes. +""" +function completed_formulation(formulation, declaration::NamedTuple=NamedTuple(formulation)) + function control_fields(owner, controls, path=()) + captured=NamedTuple[] + for (key,value) in pairs(controls) + route=(path...,key) + if value isa NamedTuple && key !== :equivalent_earth + append!(captured,control_fields(owner,value,route)) + else + text=key === :equivalent_earth ? + description(FormulaDefinition,Val(key),value;compact=true) : + description(owner,Val(key),value;compact=true) + push!(captured,(scope=route,value=Commons.detach(value),text)) + end + end + return captured + end + function fields(quantity) + selections=pairs(formulation;quantity) + map(collect(selections)) do (scope,selected) + owner,route=scope + name=isempty(route) ? "" : description(owner,Val(first(route));compact=true) + length(route)>1 && (name *= "("*join(string.(Base.tail(route)),",")*")") + controls=selected isa Pair ? Commons.detach(last(selected)) : (;) + # The description compares applied controls, including defaults. + # Keep the original selection identity used by scientific grouping. + effective=get(declaration,:methods,nothing) + for key in route + effective=effective isa NamedTuple ? get(effective,key,nothing) : nothing + end + applied=isempty(route) || !(effective isa NamedTuple) ? controls : merge(controls, + (; (key=>effective[key] for key in (:parameters,:options,:equivalent_earth) + if haskey(effective,key) && effective[key]!==nothing)...)) + leaf=selected isa Pair ? first(selected) : selected + control_owner=isempty(route) ? owner : typeof(leaf) + # Routes are the scientific slots declared by pairs(owner), shared + # by backends using the same constitutive meaning. + (meaning=route, + selection=(identifier=formula_id(selected),controls),name, + value=isempty(route) ? description(selected;compact=true) : description(owner,selected;compact=true), + summary=isempty(route) ? description(owner,formulation;quantity,compact=true) : nothing, + control_fields=control_fields(control_owner,applied)) + end + end + return NamedTuple{(:formulations,:selections,:formulation_fields), + Tuple{NamedTuple,NamedTuple,NamedTuple}}((declaration, + (Z=formula_id(formulation,Z),Y=formula_id(formulation,Y)), + (all=fields(nothing),Z=fields(Z),Y=fields(Y)))) +end + +""" +$(TYPEDSIGNATURES) + +Complete `formulation` as the line-parameter computation of `workspace` applied it. Each +earth formula records the equivalent earth that its calculation in the plan consumed. A +reduction that the plan resolved through `:default` appears as that formula's +`equivalent_earth`, as an explicit one does. The requested selections remain as declared. +""" +function completed_formulation(formulation::LineParametersFormulation, + workspace::LineParametersWorkspace) + declaration = NamedTuple(formulation) + length(workspace.input.earth.layers) > 2 || + return completed_formulation(formulation, declaration) + slots = (:earth_impedance, :earth_admittance) + # Each formula without an explicit `equivalent_earth` that the plan reduced, with the + # reduction its calculation consumed. + reduced = Pair[] + for calculation in workspace.plan.earth.calculations + calculation.earth isa EarthModel && continue + for entry in (calculation.impedance, calculation.admittance) + entry === nothing || entry.formula.equivalent_earth !== nothing || + push!(reduced, entry.formula => calculation.earth) + end + end + function record(selected, retained) + selected isa NamedTuple && return map(record, selected, retained) + index = findfirst(entry -> first(entry) === selected, reduced) + index === nothing && return retained + return merge(retained, (equivalent_earth = NamedTuple(last(reduced[index])),)) + end + recorded = merge(declaration.methods, + map(record, formulation.methods[slots], declaration.methods[slots])) + return completed_formulation(formulation, + typeof(declaration)((declaration.backend, declaration.requested, recorded, declaration.options))) +end + +# Description contents and runtime geometry do not specialize the result type. +""" +$(TYPEDSIGNATURES) + +Store captured completion records without specializing the result type on runtime +geometry or nested description contents. Persistence uses this same operation. +""" +function completion_details(record::NamedTuple{names,T}) where {names,T} + types=map(value -> value isa NamedTuple ? NamedTuple : typeof(value),values(record)) + return ComputationDetails(NamedTuple{names,Tuple{types...}}(values(record))) +end + +function Commons.observation_gridpoint(source::Union{LineParameters,CableConstants}) + retained=details(source).data + inputs=get(retained,:inputs,nothing) + return Commons.detach((id=get(retained,:gridpoint,nothing), inputs, + source_gridpoint=get(retained,:source_gridpoint,nothing), + formulations=get(retained,:formulations,nothing),formulation_fields=get(retained,:formulation_fields,(;)), + coordinates=get(retained,:coordinates,source isa CableConstants ? source.cores : nothing), + uncertainty=get(retained,:uncertainty,nothing), + uncertainty_descriptions=get(retained,:uncertainty_descriptions,nothing), + transformation=get(retained,:modal,nothing), + missing_reason=inputs===nothing ? :physical_inputs_not_supplied : nothing)) +end + +# Updating an association reuses numerical storage. It does not recompute or +# copy a physical problem. External result owners can supply their own method. +""" +$(TYPEDSIGNATURES) + +Associate a completed result with its original gridpoint identity and additional +captured fields. Built-in results retain their numerical arrays and replace only +completion details. External result owners may extend this completion operation. +""" +retain_gridpoint(source, id; fields=(;)) = source +function retain_gridpoint(source::LineParameters, id; fields=(;)) + retained=completion_details(merge(details(source).data,fields,(gridpoint=id,))) + return LineParameters(source.domain,source.Z,source.Y,source.f,retained) +end +function retain_gridpoint(source::CableConstants, id; fields=(;)) + retained=completion_details(merge(details(source).data,fields,(gridpoint=id,))) + return CableConstants(source.cores,source.R,source.L,source.C,source.G,source.frequency,retained) +end + +function line_length(source::AbstractCoreResult) + inputs=get(details(source).data,:inputs,nothing) + inputs===nothing && return nothing + system=get(inputs,:system,nothing) + return system===nothing ? nothing : system.line_length +end +line_length(source) = nothing diff --git a/src/engine/options.jl b/src/engine/options.jl new file mode 100644 index 000000000..102dc8208 --- /dev/null +++ b/src/engine/options.jl @@ -0,0 +1,200 @@ +""" +$(TYPEDSIGNATURES) + +Normalize the line-parameter reductions. `reduce_bundle=true` merges the +conductors of each active phase, `kron_reduction=true` eliminates conductors with +phase zero, and `ideal_transposition=false` leaves the retained matrices +untransposed. With `ideal_transposition=true`, the retained ``Z`` and +potential-coefficient matrix ``P`` are averaged over cyclic transposition before +``P`` is inverted to ``Y``. `LineCableModelsFEM` shares these options. +""" +function formulation_options( + ::Type{LineParametersFormulation}, + record::FormulationOptions +)::FormulationOptions + options = record.data + allowed = ( + :reduce_bundle, + :kron_reduction, + :ideal_transposition + ) + unknown = filter(key -> key ∉ allowed, keys(options)) + isempty(unknown) || throw(ArgumentError( + "unknown line-parameter formulation options: $(sort!(collect(unknown)))", + )) + normalized = merge( + ( + reduce_bundle = true, + kron_reduction = true, + ideal_transposition = false + ), + options + ) + all(name -> getproperty(normalized, name) isa Bool, + (:reduce_bundle, :kron_reduction, :ideal_transposition)) || throw(ArgumentError( + "reduction and transposition options must be Bool", + )) + return FormulationOptions(; + reduce_bundle = normalized.reduce_bundle, + kron_reduction = normalized.kron_reduction, + ideal_transposition = normalized.ideal_transposition + ) +end + +function computation_options( + ::Type{LineCableModelsCoaxial}, + record::ComputationOptions +)::ComputationOptions + options = record.data + allowed = (:verbosity, :output_basis, :trace, :on_result, :timing) + unknown = filter(key -> key ∉ allowed, keys(options)) + isempty(unknown) || throw(ArgumentError( + "unknown LineCableModelsCoaxial computation options: $(sort!(collect(unknown)))", + )) + normalized = merge( + (verbosity = (default = 0,), output_basis = :pul, + trace = false, on_result = nothing, timing = false), + options + ) + levels = verbosity(normalized.verbosity) + basis_value = normalized.output_basis + basis_value in (:pul, :total) || throw(ArgumentError( + "output_basis must be :pul or :total; got $(repr(basis_value))", + )) + normalized.trace isa Bool || throw(ArgumentError("trace must be Bool")) + normalized.timing isa Bool || throw(ArgumentError("timing must be Bool")) + basis = basis_value === :pul ? (output_basis = Val(:pul),) : + (output_basis = Val(:total),) + execution = merge((verbosity = levels,), basis, + (trace = Val(false), on_result = normalized.on_result, timing = normalized.timing)) + normalized.trace && return ComputationOptions(merge(execution, (trace = Val(true),))) + return ComputationOptions(execution) +end + + +description(::Type{<:Union{LineParametersFormulation,LineCableModelsFEM}}, + ::Val{:reduce_bundle},value::Bool;compact::Bool=false) = "bundle reduction="*string(value) +description(::Type{<:Union{LineParametersFormulation,LineCableModelsFEM}}, + ::Val{:kron_reduction},value::Bool;compact::Bool=false) = "Kron reduction="*string(value) +description(::Type{<:Union{LineParametersFormulation,LineCableModelsFEM}}, + ::Val{:ideal_transposition},value::Bool;compact::Bool=false) = "ideal transposition="*string(value) + +"""Describe the active FEM field equations without executing a field solve.""" +description(::Type{LineCableModelsFEM},::Val{:physics},value::Symbol;compact::Bool=false) = + description(LineCableModelsFEM,Val(:physics),Val(value);compact) +description(::Type{LineCableModelsFEM},::Val{:physics},::Val{Symbol("quasi-tem")};compact::Bool=false) = + "quasi-TEM" +description(::Type{LineCableModelsFEM},::Val{:physics},::Val{Symbol("quasi-fw")};compact::Bool=false) = + "quasi-full-wave" + +function formulation_options(::Type{LineCableModelsFEM}, record::FormulationOptions)::FormulationOptions + options = record.data + physics = get(options, :physics, :quasi_tem) + physics isa Union{Symbol, AbstractString} || throw(ArgumentError( + "physics must be :quasi_tem or :quasi_fw")) + physics = Symbol(replace(String(physics), '_' => '-')) + physics in (Symbol("quasi-tem"), Symbol("quasi-fw")) || throw(ArgumentError( + "physics must be :quasi_tem or :quasi_fw (also accepted as hyphenated strings or Symbols)")) + reductions = formulation_options(LineParametersFormulation, + FormulationOptions(; (key => value for (key, value) in pairs(options) if key !== :physics)...)) + return FormulationOptions(; reductions.data..., physics) +end + +""" +$(TYPEDSIGNATURES) + +Validate Gmsh/GetDP execution controls supplied to `compute(...; options)`. +The field model is selected separately by `formulation_options(LineCableModelsFEM, ...)`. + +# Arguments + +- `owner`: the `LineCableModelsFEM` computation owner. +- `options`: caller-supplied named tuple. Supported keys and defaults are: + - `ui=false`: open the Gmsh graphical interface. + - `plot_field_maps=false`: emit spatial field maps for every solve. + - `mesh_mode=:reuse`: reuse compatible meshes. `:remesh` regenerates them. + - `mesh_path=nothing`: optional existing `.msh` path. + - `domain_skin_depths=2.0`: minimum finite earth-domain radius in conductive + skin depths \\[dimensionless\\]. The layout can require a larger radius. + Must be finite and positive. Changing it retains local mesh-size targets. + - `keep_run_directory=false`: retain successful run artifacts. + - `getdp_executable=nothing`: executable override. Otherwise resolve the + environment override, package artifact, or unsupported-platform `PATH` fallback. + - `gmsh_verbosity=2`, `getdp_verbosity=2`: native message levels from 0 through 5. + - `frequency_workers=2`: maximum concurrent frequency solver processes. + - `solver_threads=1`: BLAS and OpenMP threads per solver process. + - `verbosity=(default=0,)`: logging in Julia levels from 0 through 2. + - `output_basis=:pul`: per-unit-length matrices. `:total` scales by line length. + - `trace=false`: retain `Z_primitive`, `P_primitive` and a copied `phase_map` + under `details(result).data.fem.primitive`. The primitive arrays refer to the + completed FEM scan arrays. + - `timing=false`: retain fresh complete-scan measurements in result details. + - `on_result=nothing`: optional callback `(problem, index, result)`. + - `log_file=nothing`: optional Julia log path. + - `resume_run_directory=nothing`: resume a compatible path or `:latest`. + +# Returns + +- A fixed-key [`ComputationOptions`](@ref) named tuple. Paths and integer controls + are normalized. `output_basis` and `trace` are lowered to `Val` values. + +# Errors + +- `ArgumentError`: unknown keys, invalid controls or empty paths. +""" +function computation_options(::Type{LineCableModelsFEM}, record::ComputationOptions)::ComputationOptions + options = record.data + defaults = (ui=false, plot_field_maps=false, mesh_mode=:reuse, + mesh_path=nothing, domain_skin_depths=2.0, + keep_run_directory=false, getdp_executable=nothing, + gmsh_verbosity=2, getdp_verbosity=2, frequency_workers=2, solver_threads=1, + log_file=nothing, resume_run_directory=nothing) + standard_keys = (:verbosity, :output_basis, :trace, :on_result, :timing) + unknown = setdiff(keys(options), (keys(defaults)..., standard_keys...)) + isempty(unknown) || throw(ArgumentError( + "unknown LineCableModelsFEM computation options: $(Tuple(unknown))")) + standard = computation_options(LineCableModelsCoaxial, + ComputationOptions(; (key => value for (key, value) in pairs(options) if key in standard_keys)...)) + normalized = merge(defaults, + (; (key => value for (key, value) in pairs(options) if key in keys(defaults))...)) + for name in (:ui, :plot_field_maps, :keep_run_directory) + getproperty(normalized, name) isa Bool || throw(ArgumentError("$name must be Bool")) + end + normalized.mesh_mode in (:reuse, :remesh) || throw(ArgumentError( + "mesh_mode must be :reuse or :remesh")) + radius_factor = normalized.domain_skin_depths + radius_factor isa Real && !(radius_factor isa Bool) && + isfinite(radius_factor) && 0 < radius_factor <= floatmax(Float64) && + Float64(radius_factor) > 0 || throw(ArgumentError( + "domain_skin_depths must be a finite positive Float64-representable number")) + for name in (:mesh_path, :getdp_executable, :log_file) + value = getproperty(normalized, name) + value isa Union{Nothing, AbstractString} || throw(ArgumentError( + "$name must be a path string or nothing")) + value === nothing || !isempty(value) || throw(ArgumentError("$name cannot be empty")) + end + for name in (:gmsh_verbosity, :getdp_verbosity) + value = getproperty(normalized, name) + value isa Integer && !(value isa Bool) && value in 0:5 || throw(ArgumentError( + "$name must be an integer from 0 through 5")) + end + for name in (:frequency_workers, :solver_threads) + value = getproperty(normalized, name) + value isa Integer && !(value isa Bool) && 1 <= value <= typemax(Int) || + throw(ArgumentError("$name must be a positive integer")) + end + resume = normalized.resume_run_directory + resume === nothing || resume === :latest || resume isa AbstractString && !isempty(resume) || + throw(ArgumentError("resume_run_directory must be nothing, :latest, or a nonempty path string")) + return ComputationOptions(; standard.data..., + ui=normalized.ui, plot_field_maps=normalized.plot_field_maps, + mesh_mode=normalized.mesh_mode, + mesh_path=normalized.mesh_path === nothing ? nothing : String(normalized.mesh_path), + domain_skin_depths=Float64(radius_factor), + keep_run_directory=normalized.keep_run_directory, + getdp_executable=normalized.getdp_executable === nothing ? nothing : String(normalized.getdp_executable), + gmsh_verbosity=Int(normalized.gmsh_verbosity), getdp_verbosity=Int(normalized.getdp_verbosity), + frequency_workers=Int(normalized.frequency_workers), solver_threads=Int(normalized.solver_threads), + log_file=normalized.log_file === nothing ? nothing : String(normalized.log_file), + resume_run_directory=resume isa AbstractString ? String(resume) : resume) +end diff --git a/src/engine/pipeimpedance/PipeImpedance.jl b/src/engine/pipeimpedance/PipeImpedance.jl new file mode 100644 index 000000000..8d6cfbd34 --- /dev/null +++ b/src/engine/pipeimpedance/PipeImpedance.jl @@ -0,0 +1,41 @@ +""" + LineCableModels.Engine.PipeImpedance + +Own pipe-type formula selections and backend applicability. No analytical +pipe-type implementation is supplied yet. +""" +module PipeImpedance +import ...Commons: FormulationOptions, formulas, formulation_options, validate +import ...LineCableModels: FormulaDefinition + +export Formula, formula_id, formulas + +using DocStringExtensions: TYPEDEF +#! explicit-imports: off +# Expanded in the docstrings of the included formula files. +using DocStringExtensions: TYPEDSIGNATURES +#! explicit-imports: on +import ..Engine: PipeImpedanceFormulation +import ...LineCableModels: formula_id +#! explicit-imports: off +# These bindings are consumed by the formula definitions below. +import ...LineCableModels: description +#! explicit-imports: on +import ...DataModel +using ...DataModel: CableDesign + +include("interface.jl") + +#! explicit-imports: off +const FORMULAS = ( + include("formulas/none.jl"), + include("formulas/default.jl"), +) +#! explicit-imports: on + +""" +Return registered pipe selections, including the explicit default selection. +""" +formulas(::Type{<:Formula}) = FORMULAS + +end diff --git a/src/engine/pipeimpedance/formulas/default.jl b/src/engine/pipeimpedance/formulas/default.jl new file mode 100644 index 000000000..b497536de --- /dev/null +++ b/src/engine/pipeimpedance/formulas/default.jl @@ -0,0 +1,12 @@ +""" +$(TYPEDSIGNATURES) + +Route the default selection to the explicit `:none` implementation. +No numerical equation is owned by `:default`. +""" +description(::Type{<:Formula{:default}}; compact::Bool=false) = + compact ? "Default" : "Default routing to :none" + +Formula{:default}(; kwargs...) = Formula{:none}(; kwargs...) + +:default diff --git a/src/engine/pipeimpedance/formulas/none.jl b/src/engine/pipeimpedance/formulas/none.jl new file mode 100644 index 000000000..d11dab695 --- /dev/null +++ b/src/engine/pipeimpedance/formulas/none.jl @@ -0,0 +1,14 @@ +""" +$(TYPEDSIGNATURES) + +**Identification.** No additional pipe impedance for coaxial geometry. + +**Expression.** Ordinary coaxial assemblies do not require an additional pipe term. +An eccentric or multicore conductive enclosure requires a pipe formulation: +the coaxial formulation supplies none. + +**Applicability.** This selector does not add an author-labeled pipe equation or numerical approximation. +""" +description(::Type{<:Formula{:none}}; compact::Bool=false) = compact ? "No additional pipe term" : "No additional pipe impedance for coaxial geometry" + +:none diff --git a/src/engine/pipeimpedance/interface.jl b/src/engine/pipeimpedance/interface.jl new file mode 100644 index 000000000..976e25aa0 --- /dev/null +++ b/src/engine/pipeimpedance/interface.jl @@ -0,0 +1,81 @@ +""" +$(TYPEDEF) + +Store a pipe-type impedance selection until the backend checks the topology. +Unsupported pipes require a separate numerical formula. `Formula{ID}()` constructs a +registered pipe-type selection. Unknown identifiers or controls raise `ArgumentError`. +Backend applicability is checked against the design. +""" +struct Formula{ID} <: PipeImpedanceFormulation + function Formula{ID}(; parameters::NamedTuple = (;), + options::Union{NamedTuple, FormulationOptions} = FormulationOptions()) where {ID} + ID in formulas(Formula) || throw(ArgumentError("unknown pipe-impedance formula :$ID")) + options = options isa NamedTuple ? FormulationOptions(options) : options + isempty(parameters) && isempty(options.data) || + throw(ArgumentError("pipe-impedance :$ID accepts no model parameters or numerical controls")) + return new{ID}() + end +end + +""" +$(TYPEDSIGNATURES) + +Check that `backend` can compute `design` with the pipe-impedance formula `selected`. +The check compares the conductor axes with each conductive enclosure: a design whose +enclosure is eccentric or contains several cores has pipe topology. Because the built-in +formulas do not add a pipe term, they admit coaxial topology only. A backend or formula +that implements pipe topology adds its own method. Return `design`. + +# Errors + +- Throws `ArgumentError` for a pipe topology. +""" +function validate(design::CableDesign, selected::Formula, backend) + pipe() = throw(ArgumentError("Pipe-type cable formulation is not yet implemented for " * + "the $(description(backend)) backend. No pipe formulation is available.")) + # Compare conductor axes, not wire positions. A concentric sheath remains + # ordinary coaxial geometry even when declared through pipe(...). + for wall in design.geometry.regions + wall.source.material.kind === :conductor || continue + annular_wall = wall.primitive isa DataModel.Annulus && wall.primitive.ri > 0 + enclosed_wall = any(entry -> entry.pattern isa DataModel.EnclosureBoundary, + wall.placement.patterns) + annular_wall || enclosed_wall || continue + wall.primitive isa DataModel.Annulus || pipe() + axis = DataModel.radial_position(wall) + for terminal in design.terminal_order + terminal === wall.terminal && continue + sources = filter(region -> region.terminal === terminal, design.geometry.regions) + center = DataModel.conductor_zone_position(sources) + distance = hypot(center[1] - axis[1], center[2] - axis[2]) + distance < wall.primitive.ri && !DataModel.same_radial_position(center, axis) && + pipe() + end + end + return design +end + +Formula(identifier::Symbol; kwargs...) = Formula(Val(identifier); kwargs...) +Formula(::Val{ID}; kwargs...) where {ID} = Formula{ID}(; kwargs...) +Formula(selected::PipeImpedanceFormulation) = selected + +function Formula(selection::FormulaDefinition{ID, Order}) where {ID, Order} + Order === :default || throw(ArgumentError("order applies only to equivalent_earth")) + isempty(selection.options.data) || + throw(ArgumentError("deferred pipe contribution has no numerical options")) + selection.equivalent_earth === nothing || + throw(ArgumentError("pipe contribution cannot consume equivalent_earth")) + return Formula{ID}(; parameters = selection.parameters) +end + +formula_id(::Formula{ID}) where {ID} = ID +formula_id(::Type{<:Formula{ID}}) where {ID} = ID +# Identity-only dispatch also describes retained selections without constructors. +description(value::Formula; compact::Bool = false) = description(typeof(value); compact) +formulation_options(::Formula) = FormulationOptions() + +"""Expose the selected pipe equation as a native record.""" +Base.NamedTuple(value::Formula) = (identifier=formula_id(value),parameters=(;),options=(;)) + +"""Iterate the independently selectable child slots admitted by this formula family.""" +Base.pairs(::Type{<:Formula}; quantity=nothing) = pairs((;)) diff --git a/src/engine/plot.jl b/src/engine/plot.jl deleted file mode 100644 index b9fb3eeca..000000000 --- a/src/engine/plot.jl +++ /dev/null @@ -1,1179 +0,0 @@ - -using Makie -import Makie: plot - -include("../plotbuilder/plothelpers.jl") - -using Measurements: Measurements - -const _ICON_FN = - (icon; text = nothing, kwargs...) -> - with_icon(icon; text = text === nothing ? "" : text, kwargs...) - -const LP_FIG_SIZE = (800, 400) - -const METRIC_PREFIX_EXPONENT = Dict( - :yocto => -24, - :zepto => -21, - :atto => -18, - :femto => -15, - :pico => -12, - :nano => -9, - :micro => -6, - :milli => -3, - :centi => -2, - :deci => -1, - :base => 0, - :deca => 1, - :hecto => 2, - :kilo => 3, - :mega => 6, - :giga => 9, - :tera => 12, - :peta => 15, - :exa => 18, - :zetta => 21, - :yotta => 24, -) - -const METRIC_PREFIX_SYMBOL = Dict( - :yocto => "y", - :zepto => "z", - :atto => "a", - :femto => "f", - :pico => "p", - :nano => "n", - :micro => "μ", - :milli => "m", - :centi => "c", - :deci => "d", - :base => "", - :deca => "da", - :hecto => "h", - :kilo => "k", - :mega => "M", - :giga => "G", - :tera => "T", - :peta => "P", - :exa => "E", - :zetta => "Z", - :yotta => "Y", -) - -const DEFAULT_QUANTITY_UNITS = Dict( - :impedance => :base, - :admittance => :base, - :resistance => :base, - :inductance => :milli, - :conductance => :base, - :capacitance => :micro, - :angle => :base, -) - -struct UnitSpec - symbol::String - per_length::Bool -end - -struct ComponentMetadata - component::Symbol - quantity::Symbol - symbol::String - title::String - axis_label::String - unit::UnitSpec -end - -struct LineParametersPlotSpec <: AbstractPlotSpec - parent_kind::Symbol - component::Symbol - symbol::String - title::String - xlabel::String - ylabel::String - freqs::Vector{<:Real} - raw_freqs::Vector{<:Real} - curves::Vector{Vector{<:Real}} - raw_curves::Vector{Vector{<:Real}} - labels::Vector{String} - x_exp::Int - y_exp::Int - fig_size::Union{Nothing, Tuple{Int, Int}} - xscale::Base.RefValue{Function} - yscale::Base.RefValue{Function} -end - -get_description(::SeriesImpedance) = ( - impedance = "Series impedance", - resistance = "Series resistance", - inductance = "Series inductance", -) - -get_symbol(::SeriesImpedance) = ( - impedance = "Z", - resistance = "R", - inductance = "L", -) - -get_unit_symbol(::SeriesImpedance) = ( - impedance = "Ω", - resistance = "Ω", - inductance = "H", -) - -get_description(::ShuntAdmittance) = ( - admittance = "Shunt admittance", - conductance = "Shunt conductance", - capacitance = "Shunt capacitance", -) - -get_symbol(::ShuntAdmittance) = ( - admittance = "Y", - conductance = "G", - capacitance = "C", -) - -get_unit_symbol(::ShuntAdmittance) = ( - admittance = "S", - conductance = "S", - capacitance = "F", -) - -parent_kind(::SeriesImpedance) = :series_impedance -parent_kind(::ShuntAdmittance) = :shunt_admittance - -metric_exponent(prefix::Symbol) = - get(METRIC_PREFIX_EXPONENT, prefix) do - Base.error("Unsupported metric prefix :$(prefix)") - end - -prefix_symbol(prefix::Symbol) = - get(METRIC_PREFIX_SYMBOL, prefix) do - Base.error("Unsupported metric prefix :$(prefix)") - end - - -quantity_scale(prefix::Symbol) = 10.0 ^ (-metric_exponent(prefix)) -length_scale(prefix::Symbol) = 10.0 ^ (metric_exponent(prefix)) -frequency_scale(prefix::Symbol) = quantity_scale(prefix) - -function unit_text(quantity_prefix::Symbol, base_unit::String) - ps = prefix_symbol(quantity_prefix) - return isempty(ps) ? base_unit : string(ps, base_unit) -end - -function length_unit_text(prefix::Symbol) - ps = prefix_symbol(prefix) - return isempty(ps) ? "m" : string(ps, "m") -end - -function composite_unit( - quantity_prefix::Symbol, - base_unit::String, - per_length::Bool, - length_prefix::Symbol, -) - numerator = unit_text(quantity_prefix, base_unit) - if per_length - denominator = length_unit_text(length_prefix) - return string(numerator, "/", denominator) - else - return numerator - end -end - -function frequency_axis_label(prefix::Symbol) - unit = unit_text(prefix, "Hz") - return string("frequency [", unit, "]") -end - -function normalize_quantity_units(units) - table = Dict(DEFAULT_QUANTITY_UNITS) - if units isa Symbol - for key in keys(table) - table[key] = units - end - elseif units isa NamedTuple - for (key, val) in pairs(units) - table[key] = val - end - elseif units isa AbstractDict - for (key, val) in units - table[key] = val - end - elseif units === nothing - return table - else - Base.error("Unsupported quantity unit specification $(typeof(units))") - end - return table -end - -function resolve_quantity_prefix(quantity::Symbol, units::AbstractDict{Symbol, Symbol}) - return get(units, quantity, get(DEFAULT_QUANTITY_UNITS, quantity, :base)) -end - -function resolve_conductors(data_dims::NTuple{3, Int}, con) - nrows, ncols, _ = data_dims - if con === nothing - return collect(1:nrows), collect(1:ncols) - elseif con isa Tuple && length(con) == 2 - isel = collect_indices(con[1], nrows) - jsel = collect_indices(con[2], ncols) - return isel, jsel - else - Base.error("Conductor selector must be a tuple (i_sel, j_sel)") - end -end - -function collect_indices(sel, n) - if sel === nothing - return collect(1:n) - elseif sel isa Integer - (1 <= sel <= n) || - Base.error("Index $(sel) out of bounds for dimension of size $(n)") - return [sel] - elseif sel isa AbstractVector - indices = collect(Int, sel) - for idx in indices - (1 <= idx <= n) || - Base.error("Index $(idx) out of bounds for dimension of size $(n)") - end - return indices - elseif sel isa AbstractRange - indices = collect(sel) - for idx in indices - (1 <= idx <= n) || - Base.error("Index $(idx) out of bounds for dimension of size $(n)") - end - return indices - elseif sel isa Colon - return collect(1:n) - else - Base.error("Unsupported selector $(sel)") - end -end - -function components_for( - obj::SeriesImpedance, - mode::Symbol, - coord::Symbol; - per_length::Bool = true, -) - desc = get_description(obj) - sym = get_symbol(obj) - units = get_unit_symbol(obj) - if mode == :ZY - coord in (:cart, :polar) || Base.error("Unsupported coordinate system $(coord)") - if coord == :cart - return ComponentMetadata[ - ComponentMetadata(:real, :impedance, sym.impedance, - string(desc.impedance, " – real part"), - string("real(", sym.impedance, ")"), - UnitSpec(units.impedance, per_length)), - ComponentMetadata(:imag, :impedance, sym.impedance, - string(desc.impedance, " – imaginary part"), - string("imag(", sym.impedance, ")"), - UnitSpec(units.impedance, per_length)), - ] - else - return ComponentMetadata[ - ComponentMetadata(:magnitude, :impedance, sym.impedance, - string(desc.impedance, " – magnitude"), - string("|", sym.impedance, "|"), - UnitSpec(units.impedance, per_length)), - ComponentMetadata(:angle, :angle, sym.impedance, - string(desc.impedance, " – angle"), - string("angle(", sym.impedance, ")"), - UnitSpec("deg", false)), - ] - end - elseif mode == :RLCG - return ComponentMetadata[ - ComponentMetadata(:resistance, :resistance, sym.resistance, - desc.resistance, - sym.resistance, - UnitSpec(units.resistance, per_length)), - ComponentMetadata(:inductance, :inductance, sym.inductance, - desc.inductance, - sym.inductance, - UnitSpec(units.inductance, per_length)), - ] - else - Base.error("Unsupported mode $(mode)") - end -end - -function components_for( - obj::ShuntAdmittance, - mode::Symbol, - coord::Symbol; - per_length::Bool = true, -) - desc = get_description(obj) - sym = get_symbol(obj) - units = get_unit_symbol(obj) - if mode == :ZY - coord in (:cart, :polar) || Base.error("Unsupported coordinate system $(coord)") - if coord == :cart - return ComponentMetadata[ - ComponentMetadata(:real, :admittance, sym.admittance, - string(desc.admittance, " – real part"), - string("real(", sym.admittance, ")"), - UnitSpec(units.admittance, per_length)), - ComponentMetadata(:imag, :admittance, sym.admittance, - string(desc.admittance, " – imaginary part"), - string("imag(", sym.admittance, ")"), - UnitSpec(units.admittance, per_length)), - ] - else - return ComponentMetadata[ - ComponentMetadata(:magnitude, :admittance, sym.admittance, - string(desc.admittance, " – magnitude"), - string("|", sym.admittance, "|"), - UnitSpec(units.admittance, per_length)), - ComponentMetadata(:angle, :angle, sym.admittance, - string(desc.admittance, " – angle"), - string("angle(", sym.admittance, ")"), - UnitSpec("deg", false)), - ] - end - elseif mode == :RLCG - if (coord == :cart || coord == :polar) - @warn "Ignoring argument :$(coord) for RLCG parameters" - end - return ComponentMetadata[ - ComponentMetadata(:conductance, :conductance, sym.conductance, - desc.conductance, - sym.conductance, - UnitSpec(units.conductance, per_length)), - ComponentMetadata(:capacitance, :capacitance, sym.capacitance, - desc.capacitance, - sym.capacitance, - UnitSpec(units.capacitance, per_length)), - ] - else - Base.error("Unsupported mode $(mode)") - end -end - -function component_values(component::Symbol, slice, freqs::Vector{<:Real}) - data = collect(slice) - if component === :real - return (real.(data)) - elseif component === :imag - return (imag.(data)) - elseif component === :magnitude - return (abs.(data)) - elseif component === :angle - return rad2deg.((angle.(data))) - elseif component === :resistance || component === :conductance - return (real.(data)) - elseif component === :inductance - imag_part = (imag.(data)) - return reactance_to_l(imag_part, freqs) - elseif component === :capacitance - imag_part = (imag.(data)) - return reactance_to_c(imag_part, freqs) - else - Base.error("Unsupported component $(component)") - end -end - -function reactance_to_l(imag_part::Vector{<:Real}, freqs::Vector{<:Real}) - result = similar(freqs, promote_type(eltype(imag_part), eltype(freqs))) - two_pi = 2π - for idx in eachindex(freqs) - f = freqs[idx] - if iszero(f) - result[idx] = NaN - else - result[idx] = imag_part[idx] / (two_pi * f) - end - end - return result -end - -function reactance_to_c(imag_part::Vector{<:Real}, freqs::Vector{<:Real}) - result = similar(freqs, promote_type(eltype(imag_part), eltype(freqs))) - two_pi = 2π - for idx in eachindex(freqs) - f = freqs[idx] - if iszero(f) - result[idx] = NaN - else - result[idx] = imag_part[idx] / (two_pi * f) - end - end - return result -end - -function legend_label(symbol::String, i::Int, j::Int) - return string(symbol, "(", i, ",", j, ")") -end - -function _axis_label(base::AbstractString, exp::Int) - exp == 0 && return base - return Makie.rich( - base, - Makie.rich(" × 10"; font = :regular, fontsize = AXIS_LABEL_FONT_SIZE), - Makie.rich( - superscript(string(exp)); - font = :regular, - fontsize = AXIS_LABEL_FONT_SIZE - 2, - # baseline_shift = 0.6, - ), - ) -end - -# Return scaled data and the exponent factored out for the axis badge. -function autoscale_axis(values::AbstractVector{<:Real}; _threshold = 1e4) - isempty(values) && return values, 0 - maxval = 0.0 - has_value = false - for val in values - if isnan(val) - continue - end - absval = abs(val) - if !has_value || absval > maxval - maxval = absval - has_value = true - end - end - !has_value && return values, 0 - exp = floor(Int, log10(maxval)) - abs(exp) < 3 && return values, 0 - scale = 10.0 ^ exp - # return values ./ scale, exp - return values ./ scale, exp -end - -function autoscale_axis_stacked( - curves::AbstractVector{<:AbstractVector{<:Real}}; - _threshold = 1e4, -) - isempty(curves) && return curves, 0 - maxval = 0.0 - has_value = false - for curve in curves - for val in curve - if isnan(val) - continue - end - absval = abs(val) - if !has_value || absval > maxval - maxval = absval - has_value = true - end - end - end - !has_value && return curves, 0 - exp = floor(Int, log10(maxval)) - abs(exp) < 3 && return curves, 0 - scale = 10.0 ^ exp - scaled_curves = [curve ./ scale for curve in curves] - return scaled_curves, exp -end - -function lineparameter_plot_specs( - obj::SeriesImpedance, - freqs::AbstractVector; - mode::Symbol = :ZY, - coord::Symbol = :cart, - freq_unit::Symbol = :base, - length_unit::Symbol = :base, - quantity_units = nothing, - con = nothing, - fig_size::Union{Nothing, Tuple{Int, Int}} = LP_FIG_SIZE, - xscale::Function = Makie.identity, - yscale::Function = Makie.identity, - per_length::Bool = true, -) - freq_vec = collect(freqs) - nfreq = length(freq_vec) - if nfreq <= 1 - @warn "Frequency vector has $(nfreq) sample(s); nothing to plot." - return LineParametersPlotSpec[] - end - size(obj.values, 3) == nfreq || - Base.error("Frequency vector length does not match impedance samples") - comps = components_for(obj, mode, coord; per_length = per_length) - units = normalize_quantity_units(quantity_units) - freq_scale = frequency_scale(freq_unit) - raw_freq_axis = freq_vec .* freq_scale - freq_axis, freq_exp = autoscale_axis(raw_freq_axis) - xlabel_base = frequency_axis_label(freq_unit) - (isel, jsel) = resolve_conductors(size(obj.values), con) - specs = LineParametersPlotSpec[] - for meta in comps - q_prefix = resolve_quantity_prefix(meta.quantity, units) - y_scale = quantity_scale(q_prefix) - l_scale = meta.unit.per_length ? length_scale(length_unit) : 1.0 - ylabel_unit = - composite_unit(q_prefix, meta.unit.symbol, meta.unit.per_length, length_unit) - ylabel_base = string(meta.axis_label, " [", ylabel_unit, "]") - - # collect raw curves and labels - raw_curves = Vector{Vector{<:Real}}() - labels = String[] - for i in isel, j in jsel - slice = @view obj.values[i, j, :] - raw_vals = component_values(meta.component, slice, freq_vec) - push!(raw_curves, (raw_vals .* y_scale .* l_scale)) - push!(labels, legend_label(meta.symbol, i, j)) - end - curves, y_exp = autoscale_axis_stacked(raw_curves) - push!( - specs, - LineParametersPlotSpec( - parent_kind(obj), - meta.component, - meta.symbol, - meta.title, - xlabel_base, - ylabel_base, - freq_axis, - raw_freq_axis, - curves, - raw_curves, - labels, - freq_exp, - y_exp, - fig_size, - Ref{Function}(xscale), - Ref{Function}(yscale), - ), - ) - end - return specs -end - -function lineparameter_plot_specs( - obj::ShuntAdmittance, - freqs::AbstractVector; - mode::Symbol = :ZY, - coord::Symbol = :cart, - freq_unit::Symbol = :base, - length_unit::Symbol = :base, - quantity_units = nothing, - con = nothing, - fig_size::Union{Nothing, Tuple{Int, Int}} = LP_FIG_SIZE, - xscale::Function = Makie.identity, - yscale::Function = Makie.identity, - per_length::Bool = true, -) - freq_vec = collect(freqs) - nfreq = length(freq_vec) - if nfreq <= 1 - @warn "Frequency vector has $(nfreq) sample(s); nothing to plot." - return LineParametersPlotSpec[] - end - size(obj.values, 3) == nfreq || - Base.error("Frequency vector length does not match admittance samples") - comps = components_for(obj, mode, coord; per_length = per_length) - units = normalize_quantity_units(quantity_units) - freq_scale = frequency_scale(freq_unit) - raw_freq_axis = freq_vec .* freq_scale - freq_axis, freq_exp = autoscale_axis(raw_freq_axis) - xlabel_base = frequency_axis_label(freq_unit) - (isel, jsel) = resolve_conductors(size(obj.values), con) - specs = LineParametersPlotSpec[] - for meta in comps - q_prefix = resolve_quantity_prefix(meta.quantity, units) - y_scale = quantity_scale(q_prefix) - l_scale = meta.unit.per_length ? length_scale(length_unit) : 1.0 - ylabel_unit = - composite_unit(q_prefix, meta.unit.symbol, meta.unit.per_length, length_unit) - ylabel_base = string(meta.axis_label, " [", ylabel_unit, "]") - - raw_curves = Vector{Vector{<:Real}}() - labels = String[] - for i in isel, j in jsel - slice = @view obj.values[i, j, :] - raw_vals = component_values(meta.component, slice, freq_vec) - push!(raw_curves, (raw_vals .* y_scale .* l_scale)) - push!(labels, legend_label(meta.symbol, i, j)) - end - - curves, y_exp = autoscale_axis_stacked(raw_curves) - push!( - specs, - LineParametersPlotSpec( - parent_kind(obj), - meta.component, - meta.symbol, - meta.title, - xlabel_base, - ylabel_base, - freq_axis, - raw_freq_axis, - curves, - raw_curves, - labels, - freq_exp, - y_exp, - fig_size, - Ref{Function}(xscale), - Ref{Function}(yscale), - ), - ) - end - return specs -end - -function lineparameter_plot_specs( - lp::LineParameters; - mode::Symbol = :ZY, - coord::Symbol = :cart, - freq_unit::Symbol = :base, - length_unit::Symbol = :base, - quantity_units = nothing, - con = nothing, - fig_size::Union{Nothing, Tuple{Int, Int}} = LP_FIG_SIZE, - xscale::Function = Makie.identity, - yscale::Function = Makie.identity, - per_length::Bool = true, -) - specs = LineParametersPlotSpec[] - append!( - specs, - lineparameter_plot_specs(lp.Z, lp.f; - mode = mode, - coord = coord, - freq_unit = freq_unit, - length_unit = length_unit, - quantity_units = quantity_units, - con = con, - fig_size = fig_size, - xscale = xscale, - yscale = yscale, - per_length = per_length, - ), - ) - append!( - specs, - lineparameter_plot_specs(lp.Y, lp.f; - mode = mode, - coord = coord, - freq_unit = freq_unit, - length_unit = length_unit, - quantity_units = quantity_units, - con = con, - fig_size = fig_size, - xscale = xscale, - yscale = yscale, - per_length = per_length, - ), - ) - return specs -end - -function render_plot_specs( - specs::Vector{LineParametersPlotSpec}; - backend = nothing, - display_plot::Bool = true, -) - assemblies = Dict{Tuple{Symbol, Symbol}, PlotAssembly}() - for spec in specs - assembly = _render_spec(spec; backend = backend, display_plot = display_plot) - assemblies[(spec.parent_kind, spec.component)] = assembly - end - return assemblies -end - -function plot( - obj::SeriesImpedance, - freqs::AbstractVector; - backend = nothing, - display_plot::Bool = true, - per_length::Bool = true, - kwargs..., -) - specs = lineparameter_plot_specs(obj, freqs; per_length = per_length, kwargs...) - return render_plot_specs(specs; backend = backend, display_plot = display_plot) -end - -function plot( - obj::ShuntAdmittance, - freqs::AbstractVector; - backend = nothing, - display_plot::Bool = true, - per_length::Bool = true, - kwargs..., -) - specs = lineparameter_plot_specs(obj, freqs; per_length = per_length, kwargs...) - return render_plot_specs(specs; backend = backend, display_plot = display_plot) -end - -function plot( - lp::LineParameters; - backend = nothing, - display_plot::Bool = true, - per_length::Bool = true, - kwargs..., -) - specs = lineparameter_plot_specs(lp; per_length = per_length, kwargs...) - return render_plot_specs(specs; backend = backend, display_plot = display_plot) -end - -function build_export_figure(spec::LineParametersPlotSpec) - backend_ctx = _make_window( - BackendHandler, - :cairo; - icons = _ICON_FN, - icons_font = ICON_TTF, - interactive_override = false, - use_latex_fonts = true, - ) - pipeline_kwargs = - spec.fig_size === nothing ? - (; initial_status = "") : - (; fig_size = spec.fig_size, initial_status = "") - assembly = with_plot_theme(backend_ctx; mode = :export) do - _run_plot_pipeline( - backend_ctx, - (fig_ctx, ctx, axis) -> _build_plot!(fig_ctx, ctx, axis, spec); - pipeline_kwargs..., - ) - end - ensure_export_background!(assembly.figure) - return assembly.figure -end - -function build_export_figure( - obj, - key::Tuple{Symbol, Symbol}; - kwargs..., -) - specs = - obj isa LineParametersPlotSpec ? [obj] : lineparameter_plot_specs(obj; kwargs...) - idx = findfirst(s -> (s.parent_kind, s.component) == key, specs) - idx === nothing && Base.error("No plot specification found for key $(key)") - return build_export_figure(specs[idx]) -end - -function _render_spec( - spec::LineParametersPlotSpec; - backend = nothing, - display_plot::Bool = true, -) - n = next_fignum() - backend_ctx = _make_window( - BackendHandler, - backend; - title = "Fig. $(n) – $(spec.title)", - icons = _ICON_FN, - icons_font = ICON_TTF, - ) - pipeline_kwargs = - spec.fig_size === nothing ? - (; initial_status = " ") : - (; fig_size = spec.fig_size, initial_status = " ") - assembly = with_plot_theme(backend_ctx) do - _run_plot_pipeline( - backend_ctx, - (fig_ctx, ctx, axis) -> _build_plot!(fig_ctx, ctx, axis, spec); - pipeline_kwargs..., - ) - end - if display_plot - _display!(backend_ctx, assembly.figure; title = spec.title) - end - return assembly -end - -function _get_axis_data( - raw_data::Vector{<:Real}, - scaled_data::Vector{<:Real}, - scale_func::Function, -) - data = scale_func == Makie.log10 ? raw_data : scaled_data - values = float(Measurements.value.(data)) - errors = if eltype(data) <: Measurements.Measurement - float(Measurements.uncertainty.(data)) - else - nothing - end - return (; values, errors) -end - -function _get_axis_label(base_label::String, exponent::Int, scale_func::Function) - if scale_func == Makie.log10 - return base_label - else - return _axis_label(base_label, exponent) - end -end - -function _build_plot!(fig_ctx, ctx, axis, spec::LineParametersPlotSpec) - # ---- Axis title & initial labels ---------------------------------------- - axis.title = spec.title - axis.xlabel = _get_axis_label(spec.xlabel, spec.x_exp, spec.xscale[]) - axis.ylabel = _get_axis_label(spec.ylabel, spec.y_exp, spec.yscale[]) - - # ---- Override global tick formatter for this specialized plot ---------- - axis.xtickformat[] = Makie.automatic - axis.ytickformat[] = Makie.automatic - - # ---- Helpers ------------------------------------------------------------ - sanitize_log!(v::AbstractVector, is_log::Bool) = - (is_log && !isempty(v)) ? (v[v .<= 0] .= NaN; v) : v - - _x_data_for(scale) = begin - xd = _get_axis_data(spec.raw_freqs, spec.freqs, scale) - sanitize_log!(xd.values, scale == Makie.log10) - xd - end - - _y_data_for(i::Int, scale) = begin - yd = _get_axis_data(spec.raw_curves[i], spec.curves[i], scale) - sanitize_log!(yd.values, scale == Makie.log10) - yd - end - - function _link_visibility!(plot_obj, controller) - # plot_obj is the Errorbars plot object. - # controller is the master Lines plot. - # React to the controller's visibility changes. - on(controller.visible) do is_visible - # A. Manually control the visibility of the stem plot directly. - plot_obj.visible = is_visible - - # B. Manually control the special attribute for the whiskers. - plot_obj.whisker_visible[] = is_visible - end - nothing - end - - # safe max(abs(.)) ignoring non-finite - _finite_max_abs(v) = begin - buf = (x -> abs(x)).(value.(v)) - any(isfinite, buf) ? maximum(x for x in buf if isfinite(x)) : 0.0 - end - - # ---- Select active (non-noise) curves by EPS ------------------------------- - ncurves = length(spec.curves) - active_idx = Int[] - - @inbounds for i in 1:ncurves - # max magnitude of raw curve; works for Real, Complex, and Measurement types - maxmag = maximum(value.(abs.(spec.raw_curves[i]))) - if maxmag > eps(Float64) # keep only if anything rises above machine eps - push!(active_idx, i) - end - end - - any_real_curve = !isempty(active_idx) - - # ---- Initial data (x) --------------------------------------------------- - x_init = _x_data_for(spec.xscale[]) - x_vals_obs = Observable(copy(x_init.values)) - x_errs_obs = x_init.errors === nothing ? nothing : Observable(copy(x_init.errors)) - - # ---- Per-curve allocs only for active curves --------------------------- - palette = Makie.wong_colors() - ncolors = length(palette) - nact = length(active_idx) - - y_vals_obs = Vector{Observable}(undef, nact) - y_errs_obs = Vector{Union{Nothing, Observable}}(undef, nact) - line_plots = Vector{Any}(undef, nact) - yerr_plots = Vector{Any}(undef, nact) - xerr_plots = Vector{Any}(undef, nact) - - # ---- Draw active curves ------------------------------------------------- - for k in 1:nact - i = active_idx[k] - color = palette[mod1(k, ncolors)] # color by active order - label = spec.labels[i] - - yd = _y_data_for(i, spec.yscale[]) - - y_vals_obs[k] = Observable(copy(yd.values)) - y_errs_obs[k] = yd.errors === nothing ? nothing : Observable(copy(yd.errors)) - - # line - ln = lines!( - axis, - x_vals_obs, - y_vals_obs[k]; - color = color, - label = label, - linewidth = 2, - ) - line_plots[k] = ln - - # Y errorbars: stems + caps; fully follow the line’s visibility - if y_errs_obs[k] !== nothing - eb = errorbars!( - axis, x_vals_obs, y_vals_obs[k], y_errs_obs[k]; - color = :black, direction = :y, whiskerwidth = 3, linewidth = 1, - ) - _link_visibility!(eb, ln) - yerr_plots[k] = eb - else - yerr_plots[k] = nothing - end - - # X errorbars: stems + caps; fully follow the line’s visibility - if x_errs_obs !== nothing - ebx = errorbars!( - axis, x_vals_obs, y_vals_obs[k], x_errs_obs; - color = :black, direction = :x, whiskerwidth = 3, linewidth = 1, - ) - _link_visibility!(ebx, ln) - xerr_plots[k] = ebx - else - xerr_plots[k] = nothing - end - - end - - # If nothing to draw, add transparent dummy without legend entry - if !any_real_curve - lines!(axis, x_vals_obs, [0]; color = :transparent, label = "No data") - end - - # ---- Apply initial scales safely --------------------------------------- - try - axis.xscale[] = spec.xscale[] - axis.yscale[] = spec.yscale[] - catch - axis.xscale[] = Makie.identity - axis.yscale[] = Makie.identity - @warn "Failed to set axis scale; reverted to linear scale." - end - - # Enforce reasonable limits (avoid microscopic ranges when curves are flat) - # Helper to compute finite extents - _finite_extents(v::AbstractVector) = begin - fv = filter(isfinite, v) - isempty(fv) && return (NaN, NaN, false) - return (minimum(fv), maximum(fv), true) - end - - function _apply_limits!() - # Helper: smallest positive finite value in a vector - _min_positive(v::AbstractVector) = begin - m = Inf - @inbounds for a in v - if isfinite(a) && a > 0 && a < m - m = a - end - end - return m - end - - # X limits - x = x_vals_obs[] - xmin, xmax, okx = _finite_extents(x) - if okx - Δx = xmax - xmin - if Δx <= 0 - xc = (xmax + xmin) / 2 - # minimal span based on magnitude - Δx = max(1e-12, 1e-3 * max(abs(xc), abs(xmax), abs(xmin), 1.0)) - xmin = xc - Δx / 2 - xmax = xc + Δx / 2 - else - pad = 0.05 * Δx - xmin -= pad; - xmax += pad - end - # Guard for log x-axis: lower bound must stay > 0 - if axis.xscale[] == Makie.log10 - posmin = _min_positive(x) - floor_pos = isfinite(posmin) ? 0.9 * posmin : nextfloat(0.0) - xmin = max(xmin, floor_pos) - xmin <= 0 && (xmin = nextfloat(0.0)) # absolute safety - end - Makie.xlims!(axis, xmin, xmax) - end - - # Y limits (consider error bars too) - ymins = Float64[] - ymaxs = Float64[] - @inbounds for k in 1:nact - y = y_vals_obs[k][] - ymin, ymax, ok = _finite_extents(y) - if ok - if y_errs_obs[k] !== nothing - e = y_errs_obs[k][] - eymin, _, okm = _finite_extents(y .- e) - _, eymax, okp = _finite_extents(y .+ e) - okm && (ymin = min(ymin, eymin)) - okp && (ymax = max(ymax, eymax)) - end - push!(ymins, ymin); - push!(ymaxs, ymax) - end - end - - if !isempty(ymins) - ymin = minimum(ymins) - ymax = maximum(ymaxs) - Δy = ymax - ymin - yc = (ymax + ymin) / 2 - - # Minimal span to avoid "micro-zoom" when the curve is essentially flat. - # - relative floor: 0.1% of magnitude (>= 1.0 to avoid collapsing near zero) - # - absolute floor: 1e-12 - min_span = max(1e-12, 1e-3 * max(abs(yc), abs(ymax), abs(ymin), 1.0)) - - if !(Δy > min_span) - Δy = min_span - ymin = yc - Δy / 2 - ymax = yc + Δy / 2 - else - pad = 0.05 * Δy - ymin -= pad; - ymax += pad - end - - # Guard for log y-axis: lower bound must stay > 0 - if axis.yscale[] == Makie.log10 - # find smallest positive among all active curves (and their lower error bars) - posmin = Inf - @inbounds for k in 1:nact - y = y_vals_obs[k][] - m = _min_positive(y) - if isfinite(m) && m < posmin - posmin = m - end - if y_errs_obs[k] !== nothing - e = y_errs_obs[k][] - # consider lower whiskers - @inbounds for (yy, ee) in zip(y, e) - l = yy - ee - if isfinite(l) && l > 0 && l < posmin - posmin = l - end - end - end - end - floor_pos = isfinite(posmin) ? 0.9 * posmin : nextfloat(0.0) - ymin = max(ymin, floor_pos) - ymin <= 0 && (ymin = nextfloat(0.0)) # absolute safety - end - - Makie.ylims!(axis, ymin, ymax) - end - return nothing - end - Makie.autolimits!(axis) - _apply_limits!() - - # ---- Refreshers (update Observables only) ------------------------------ - function _refresh_x!(scale) - Makie.autolimits!(axis) - spec.xscale[] = scale - axis.xscale[] = scale - axis.xlabel = _get_axis_label(spec.xlabel, spec.x_exp, scale) - - xd = _x_data_for(scale) - x_vals_obs[] = xd.values - if x_errs_obs !== nothing - x_errs_obs[] = xd.errors - end - - _apply_limits!() - nothing - end - - function _refresh_y!(scale) - Makie.autolimits!(axis) - spec.yscale[] = scale - axis.yscale[] = scale - axis.ylabel = _get_axis_label(spec.ylabel, spec.y_exp, scale) - - @inbounds for k in 1:nact - i = active_idx[k] - yd = _y_data_for(i, scale) - y_vals_obs[k][] = yd.values - if y_errs_obs[k] !== nothing - y_errs_obs[k][] = yd.errors - end - end - _apply_limits!() - nothing - end - - # ---- Buttons ------------------------------------------------------------ - buttons = - any_real_curve ? - [ - ControlButtonSpec( - (_ctx, _btn) -> (Makie.reset_limits!(axis); nothing); - icon = MI_REFRESH, - on_success = ControlReaction(status_string = "Axis limits reset"), - ), - ControlButtonSpec( - (_ctx, _btn) -> _save_plot_export(spec, axis); - icon = MI_SAVE, - on_success = ControlReaction( - status_string = path -> string("Saved SVG to ", basename(path)), - ), - ), - ] : Any[] - - # ---- Toggles ------------------------------------------------------------ - toggles = - any_real_curve ? - [ - ControlToggleSpec( - (_ctx, _t) -> _refresh_x!(Makie.log10), - (_ctx, _t) -> _refresh_x!(Makie.identity); - label = "log x-axis", - start_active = spec.xscale[] == Makie.log10, - on_success_on = ControlReaction(status_string = "x-axis scale set to log"), - on_success_off = ControlReaction( - status_string = "x-axis scale set to linear", - ), - on_failure = ControlReaction(status_string = err -> err), - ), - ControlToggleSpec( - (_ctx, _t) -> _refresh_y!(Makie.log10), - (_ctx, _t) -> _refresh_y!(Makie.identity); - label = "log y-axis", - start_active = spec.yscale[] == Makie.log10, - on_success_on = ControlReaction(status_string = "y-axis scale set to log"), - on_success_off = ControlReaction( - status_string = "y-axis scale set to linear", - ), - on_failure = ControlReaction(status_string = err -> err), - ), - ] : Any[] - - # ---- Legend ------------------------------------------------------------- - legend_builder = - parent -> - Makie.Legend( - parent, - axis; - orientation = :vertical, - ) - - return PlotBuildArtifacts( - axis = axis, - legends = legend_builder, - colorbars = Any[], - control_buttons = buttons, - control_toggles = toggles, - status_message = nothing, - ) -end - - - -function _display!(backend_ctx, fig::Makie.Figure; title::AbstractString = "") - if backend_ctx.interactive && backend_ctx.window !== nothing - display(backend_ctx.window, fig) - if !isempty(title) && hasproperty(backend_ctx.window, :title) - backend_ctx.window.title[] = title - end - else - BackendHandler.renderfig(fig) - end - return nothing -end diff --git a/src/engine/problemdefs.jl b/src/engine/problemdefs.jl deleted file mode 100644 index cf1279d8e..000000000 --- a/src/engine/problemdefs.jl +++ /dev/null @@ -1,244 +0,0 @@ - -""" -$(TYPEDEF) - -Represents a line parameters computation problem for a given physical cable system. - -$(TYPEDFIELDS) -""" -struct LineParametersProblem{T <: REALSCALAR} <: ProblemDefinition - "The physical cable system to analyze." - system::LineCableSystem{T} - "Operating temperature \\[°C\\]." - temperature::T - "Earth properties model." - earth_props::EarthModel{T} - "Frequencies at which to perform the analysis \\[Hz\\]." - frequencies::Vector{T} - - @doc """ - $(TYPEDSIGNATURES) - - Constructs a [`LineParametersProblem`](@ref) instance. - - # Arguments - - - `system`: The cable system to analyze ([`LineCableSystem`](@ref)). - - `temperature`: Operating temperature \\[°C\\]. Default: `T₀`. - - `earth_props`: Earth properties model ([`EarthModel`](@ref)). - - `frequencies`: Frequencies for analysis \\[Hz\\]. Default: [`f₀`](@ref). - - # Returns - - - A [`LineParametersProblem`](@ref) object with validated cable system, temperature, earth model, and frequency vector. - - # Examples - - ```julia - prob = $(FUNCTIONNAME)(system; temperature=25.0, earth_props=earth, frequencies=[50.0, 60.0, 100.0]) - ``` - """ - function LineParametersProblem( - system::LineCableSystem; - temperature::REALSCALAR = (T₀), - earth_props::EarthModel, - frequencies::Vector{<:Number} = [f₀], - ) - - # 1. System structure validation - @assert !isempty(system.cables) "LineCableSystem must contain at least one cable" - - # 2. Phase assignment validation - phase_numbers = unique(vcat([cable.conn for cable in system.cables]...)) - @assert !isempty(filter(x -> x > 0, phase_numbers)) "At least one conductor must be assigned to a phase (>0)" - @assert maximum(phase_numbers) <= system.num_phases "Invalid phase number detected" - - # 3. Cable components validation - for (i, cable) in enumerate(system.cables) - @assert !isempty(cable.design_data.components) "Cable $i has no components defined" - - # Validate conductor-insulator pairs - for (j, comp) in enumerate(cable.design_data.components) - @assert !isempty(comp.conductor_group.layers) "Component $j in cable $i has no conductor layers" - @assert !isempty(comp.insulator_group.layers) "Component $j in cable $i has no insulator layers" - - # Validate monotonic increase of radii - @assert comp.conductor_group.r_ex > comp.conductor_group.r_in "Component $j in cable $i: conductor outer radius must be larger than inner radius" - @assert comp.insulator_group.r_ex > comp.insulator_group.r_in "Component $j in cable $i: insulator outer radius must be larger than inner radius" - - # Validate geometric continuity between conductor and insulator - r_ext_cond = comp.conductor_group.r_ex - r_in_ins = comp.insulator_group.r_in - @assert abs(r_ext_cond - r_in_ins) < 1e-10 "Geometric mismatch in cable $i component $j: conductor outer radius ≠ insulator inner radius" - - # Validate electromagnetic properties - # Conductor properties - @assert comp.conductor_props.rho > 0 "Component $j in cable $i: conductor resistivity must be positive" - @assert comp.conductor_props.mu_r > 0 "Component $j in cable $i: conductor relative permeability must be positive" - @assert comp.conductor_props.eps_r >= 0 "Component $j in cable $i: conductor relative permittivity grater than or equal to zero" - - # Insulator properties - @assert comp.insulator_props.rho > 0 "Component $j in cable $i: insulator resistivity must be positive" - @assert comp.insulator_props.mu_r > 0 "Component $j in cable $i: insulator relative permeability must be positive" - @assert comp.insulator_props.eps_r > 0 "Component $j in cable $i: insulator relative permittivity must be positive" - end - end - - # 4. Temperature range validation - @assert abs(temperature - T₀) < ΔTmax """ -Temperature is outside the valid range for linear resistivity model: -T = $temperature -T₀ = $T₀ -ΔTmax = $ΔTmax -|T - T₀| = $(abs(temperature - T₀))""" - - # 5. Frequency range validation - @assert !isempty(frequencies) "Frequency vector cannot be empty" - @assert all(f -> f > 0, frequencies) "All frequencies must be positive" - @assert issorted(frequencies) "Frequency vector must be monotonically increasing" - if maximum(frequencies) > 1e8 - @warn "Frequencies above 100 MHz exceed quasi-TEM validity limit. High-frequency results should be interpreted with caution." maxfreq = - maximum(frequencies) - end - - # 6. Earth model validation - @assert length(earth_props.layers[end].rho_g) == length(frequencies) """Earth model frequencies must match analysis frequencies - Earth model frequencies = $(length(earth_props.layers[end].rho_g)) - Analysis frequencies = $(length(frequencies)) - """ - - # 7. Geometric validation - positions = [ - ( - cable.horz, - cable.vert, - maximum( - comp.insulator_group.r_ex - for comp in cable.design_data.components - ), - ) - for cable in system.cables - ] - - for i in eachindex(positions) - for j in (i+1):lastindex(positions) - # Calculate center-to-center distance - dist = sqrt( - (positions[i][1] - positions[j][1])^2 + - (positions[i][2] - positions[j][2])^2, - ) - - # Get outermost radii for both cables - r_outer_i = positions[i][3] - r_outer_j = positions[j][3] - - # Check if cables overlap - min_allowed = r_outer_i + r_outer_j - tol = 1e-8 * max(min_allowed, 1.0) - - - @assert dist + tol >= min_allowed """ - Cables $i and $j overlap! - Center-to-center distance: $(dist + tol) m - Minimum required distance: $(min_allowed) m - Cable $i outer radius: $(r_outer_i) m - Cable $j outer radius: $(r_outer_j) m""" - end - end - - T = resolve_T(system, temperature, earth_props, frequencies) - return new{T}( - coerce_to_T(system, T), - coerce_to_T(temperature, T), - coerce_to_T(earth_props, T), - coerce_to_T(frequencies, T), - ) - end -end - -""" -$(TYPEDEF) - -Represents the electromagnetic transient (EMT) formulation set for cable or line systems, containing all required impedance and admittance models for internal and earth effects. - -$(TYPEDFIELDS) -""" -struct EMTFormulation <: AbstractFormulationSet - "Internal impedance formulation." - internal_impedance::InternalImpedanceFormulation - "Insulation impedance formulation." - insulation_impedance::InsulationImpedanceFormulation - "Earth impedance formulation." - earth_impedance::EarthImpedanceFormulation - "Insulation admittance formulation." - insulation_admittance::InsulationAdmittanceFormulation - "Earth admittance formulation." - earth_admittance::EarthAdmittanceFormulation - "Modal transformation method." - modal_transform::Union{AbstractTransformFormulation, Nothing} - "Equivalent homogeneous earth model (EHEM) formulation." - equivalent_earth::Union{AbstractEHEMFormulation, Nothing} - "Solver options for EMT-type computations." - options::EMTOptions - - @doc """ - $(TYPEDSIGNATURES) - - Constructs an [`EMTFormulation`](@ref) instance. - - # Arguments - - - `internal_impedance`: Internal impedance formulation. - - `insulation_impedance`: Insulation impedance formulation. - - `earth_impedance`: Earth impedance formulation. - - `insulation_admittance`: Insulation admittance formulation. - - `earth_admittance`: Earth admittance formulation. - - `modal_transform`: Modal transformation method. - - `equivalent_earth`: Equivalent homogeneous earth model (EHEM) formulation. - - `options`: Solver options for EMT-type computations. - - # Returns - - - An [`EMTFormulation`](@ref) object containing the specified methods. - - # Examples - - ```julia - emt = $(FUNCTIONNAME)(...) - ``` - """ - function EMTFormulation(; - internal_impedance::InternalImpedanceFormulation, - insulation_impedance::InsulationImpedanceFormulation, - earth_impedance::EarthImpedanceFormulation, - insulation_admittance::InsulationAdmittanceFormulation, - earth_admittance::EarthAdmittanceFormulation, - modal_transform::Union{AbstractTransformFormulation, Nothing}, - equivalent_earth::Union{AbstractEHEMFormulation, Nothing}, - options::EMTOptions, - ) - return new( - internal_impedance, insulation_impedance, earth_impedance, - insulation_admittance, earth_admittance, modal_transform, equivalent_earth, - options, - ) - end -end - -function FormulationSet(::Val{:EMT}; - internal_impedance::InternalImpedanceFormulation = InternalImpedance.ScaledBessel(), - insulation_impedance::InsulationImpedanceFormulation = InsulationImpedance.Lossless(), - earth_impedance::EarthImpedanceFormulation = EarthImpedance.Papadopoulos(), - insulation_admittance::InsulationAdmittanceFormulation = InsulationAdmittance.Lossless(), - earth_admittance::EarthAdmittanceFormulation = EarthAdmittance.Papadopoulos(), - modal_transform::Union{AbstractTransformFormulation, Nothing} = nothing, - equivalent_earth::Union{AbstractEHEMFormulation, Nothing} = nothing, - options = (;), -) - emt_opts = build_options(EMTOptions, options; strict = true) - return EMTFormulation(; internal_impedance, insulation_impedance, earth_impedance, - insulation_admittance, earth_admittance, modal_transform, equivalent_earth, - options = emt_opts, - ) -end - diff --git a/src/engine/problems.jl b/src/engine/problems.jl new file mode 100644 index 000000000..0a162578b --- /dev/null +++ b/src/engine/problems.jl @@ -0,0 +1,449 @@ +""" +$(TYPEDEF) + +Define one scalar line-parameter computation over a completed cable system. +Operating temperature and analysis frequencies are fields of the problem. + +$(TYPEDFIELDS) +""" +struct LineParametersProblem{ + T <: Real, + S <: LineCableSystem{T}, + E <: EarthModel{T} +} <: AbstractProblemDefinition + "Physical cable system." + system::S + "Operating temperature \\[°C\\]." + temperature::T + "Static earth model." + earth_props::E + "Strictly positive, sorted analysis frequencies \\[Hz\\]." + frequencies::Vector{T} + + function LineParametersProblem{T, S, E}( + system::S, + temperature::T, + earth_props::E, + frequencies::Vector{T} + ) where { + T <: Real, + S <: LineCableSystem{T}, + E <: EarthModel{T} + } + return validate(new{T, S, E}( + system, + temperature, + earth_props, + frequencies + )) + end +end + +Base.eltype(::LineParametersProblem{T}) where {T} = T +Base.eltype(::Type{LineParametersProblem{T}}) where {T} = T +line_length(problem::LineParametersProblem) = line_length(problem.system) + +function validate(problem::LineParametersProblem) + validate(problem.system) + validate(problem.earth_props) + DataModel.clearance_geometry(problem.system.designs, problem.system.positions; + required = problem.system.clearances, interface = true, adjust = false) + phases = unique(problem.system.connection_order) + active = filter(>(0), phases) + isempty(active) && throw(ArgumentError( + "at least one conductor must be assigned to an active phase", + )) + maximum(active) <= nphases(problem.system) || throw(DomainError( + active, + "an active-phase assignment exceeds the number of distinct active phases" + )) + isfinite(problem.temperature) || throw(DomainError(problem.temperature, + "operating temperature must be finite")) + isempty(problem.frequencies) && throw(ArgumentError("frequencies cannot be empty")) + all(value -> isfinite(value) && value > zero(value), problem.frequencies) || + throw(DomainError( + problem.frequencies, "frequencies must be positive and finite" + )) + issorted(problem.frequencies) || throw(ArgumentError("frequencies must be sorted")) + return problem +end + +""" +$(TYPEDSIGNATURES) + +Construct a problem after promoting the system, operating temperature, static +earth model and frequencies to one real scalar +type. + +# Keywords + +- `temperature`: operating temperature \\[°C\\]. +- `earth_props`: static earth model. +- `frequencies`: positive sorted analysis frequencies \\[Hz\\]. +""" +function LineParametersProblem( + system::LineCableSystem; + temperature::Real = oftype(float(system.line_length), 20), + earth_props::EarthModel, + frequencies::AbstractVector{<:Real} = [oftype(float(system.line_length), 50)] +) + isempty(frequencies) && throw(ArgumentError("frequencies cannot be empty")) + T = promote_type( + eltype(system), typeof(float(temperature)), eltype(earth_props), + typeof(float(first(frequencies))) + ) + converted_system = DataModel.interface_clearance(convert(LineCableSystem{T}, system)) + converted_earth = convert(EarthModel{T}, earth_props) + return LineParametersProblem{ + T, + typeof(converted_system), + typeof(converted_earth) + }( + converted_system, + convert(T, float(temperature)), + converted_earth, + T[convert(T, float(value)) for value in frequencies] + ) +end + +""" +$(TYPEDSIGNATURES) + +Construct a line-parameter problem from completed cable designs and physical +placements. + +# Arguments + +- `designs`: one completed design, or a collection aligned with `placements`. +- `placements`: one placement or a collection of physical placements. + +# Keywords + +- `connections`: terminal-to-active-phase declarations. Use one-based active + phase IDs and `0` for grounded or eliminated conductors. +- `environment`: optional physical environment declaration. +- `system_id`: stable system identifier. +- `line_length`: physical line length in meters. +- `temperature`: operating temperature in °C. +- `earth_props`: static earth model. +- `frequencies`: positive sorted analysis frequencies in Hz. +- `combine`: rule used to combine designs and placements. + +# Returns + +One validated [`LineParametersProblem`](@ref) containing a completed +[`LineCableSystem`](@ref). +""" +function LineParametersProblem( + designs::Union{CableDesign, AbstractVector{<:CableDesign}, Tuple}, + placements, + connections, + environment, + system_id::AbstractString, + line_length::Real, + temperature::Real, + earth_props::EarthModel, + frequencies::AbstractVector{<:Real}; + combine::Symbol = :product +) + system = build( + LineCableSystem, + designs, + placements; + connections, + environment, + system_id, + line_length, + combine + ) + return LineParametersProblem(system; temperature, earth_props, frequencies) +end + +""" +$(TYPEDEF) + +Store the physical methods selected for a line-parameter computation. + +$(TYPEDFIELDS) +""" +struct LineParametersFormulation{M <: NamedTuple, O <: FormulationOptions, D <: NamedTuple} <: + AbstractFormulation + "Owner-resolved physical methods. Context-dependent defaults remain deferred." + methods::M + "Shared physical computation options." + options::O + "Requested selections retained before owner and problem-context resolution." + definitions::D +end + +"""Identify the owned coaxial computation without report numbering.""" +description(::Type{<:LineParametersFormulation}; compact::Bool=false) = description(LineCableModelsCoaxial;compact) +description(::LineParametersFormulation; compact::Bool=false) = description(LineParametersFormulation;compact) +formula_id(::Type{<:LineParametersFormulation}) = :coaxial +formula_id(::LineParametersFormulation) = :coaxial + +""" +$(TYPEDSIGNATURES) + +Describe the selected earth-return methods relevant to `quantity`. This compact +summary identifies the analytical computation when compared with another +backend. Individual constitutive selections remain separately described. +""" +function description(::Type{LineParametersFormulation}, source::LineParametersFormulation; + quantity=nothing, compact::Bool=false) + entries=pairs(source;quantity) + selected=[(route,value) for ((_,route),value) in entries + if !isempty(route) && first(route) in (:earth_impedance,:earth_admittance) && + value !== nothing && !ismissing(value)] + isempty(selected) && return description(source;compact) + length(unique(formula_id(value) for (_,value) in selected))==1 && + return description(last(first(selected));compact) + return join(map(selected) do (route,value) + name=description(LineParametersFormulation,Val(first(route));compact) + length(route)>1 && (name *= "("*join(string.(Base.tail(route)),",")*")") + name*"="*description(value;compact) + end," / ") +end + +""" +$(TYPEDSIGNATURES) + +Iterate ordered formula-slot owners relevant to a physical quantity. With +`quantity=nothing`, retain every declared slot. This is a declaration read, +not problem-dependent formula resolution. +""" +function Base.pairs(::Type{LineParametersFormulation}; quantity=nothing) + selected=(internal_impedance=InternalImpedance.Formula, + insulation_impedance=InsulationImpedance.Formula, + earth_impedance=EarthImpedance.Formula, + shunt_model=ShuntModel.Formula, + insulation_admittance=InsulationAdmittance.Formula, + semicon_admittance=SemiconAdmittance.Formula, + earth_admittance=EarthAdmittance.Formula, + earth_properties=Earth.FrequencyDependent.Formula, + pipe_impedance=PipeImpedance.Formula, + temperature_dependence=TemperatureDependent.Formula) + quantity===nothing && return pairs(selected) + q=quantity isa Units.Quantity ? quantity : Commons.request_quantity(quantity) + series=q in (Units.quantity(Z),Units.quantity(R),Units.quantity(X),Units.quantity(L), + Units.quantity(Z,abs),Units.quantity(Z,angle)) + shunt=q in (Units.quantity(Y),Units.quantity(G),Units.quantity(B),Units.quantity(C), + Units.quantity(Y,abs),Units.quantity(Y,angle)) + series || shunt || throw(ArgumentError("no line-formulation selections for $q")) + omitted=series ? (:shunt_model,:insulation_admittance,:semicon_admittance,:earth_admittance) : + (:internal_impedance,:insulation_impedance,:earth_impedance,:pipe_impedance) + return pairs((; (key=>value for (key,value) in pairs(selected) if key ∉ omitted)...)) +end + +description(::Type{LineParametersFormulation},::Val{:internal_impedance}; compact::Bool=false) = "internal Z" +description(::Type{LineParametersFormulation},::Val{:insulation_impedance}; compact::Bool=false) = "insulation Z" +description(::Type{LineParametersFormulation},::Val{:earth_impedance}; compact::Bool=false) = "earth Z" +description(::Type{LineParametersFormulation},::Val{:shunt_model}; compact::Bool=false) = "shunt geometry" +description(::Type{LineParametersFormulation},::Val{:insulation_admittance}; compact::Bool=false) = "insulation Y" +description(::Type{LineParametersFormulation},::Val{:semicon_admittance}; compact::Bool=false) = "semicon Y" +description(::Type{LineParametersFormulation},::Val{:earth_admittance}; compact::Bool=false) = "earth Y" +description(::Type{LineParametersFormulation},::Val{:earth_properties}; compact::Bool=false) = "soil law" +description(::Type{LineParametersFormulation},::Val{:pipe_impedance}; compact::Bool=false) = "pipe Z" +description(::Type{LineParametersFormulation},::Val{:temperature_dependence}; compact::Bool=false) = "temperature law" + +"""Expose typed children and controls without serializing the formulation.""" +Base.pairs(value::LineParametersFormulation; quantity=nothing) = + pairs(LineParametersFormulation,(methods=value.methods,requested=value.definitions,options=value.options.data);quantity) +formulation_options(value::LineParametersFormulation) = value.options + +""" +$(TYPEDSIGNATURES) + +Iterate owner-scoped selections from a formulation's declared child interface. +`retained.methods` contains typed children. +`retained.requested` contains their explicit controls. `pairs(owner; quantity)` +owns child order and relevance. The empty route identifies the owner itself. +""" +function Base.pairs(::Type{LineParametersFormulation}, retained::NamedTuple; + quantity=nothing,owner=LineParametersFormulation) + # A selection pair contains passive explicit controls and excludes option inputs. + explicit = function (value) + record = value isa Union{FormulaDefinition, AbstractFormulation} ? NamedTuple(value) : value + record isa NamedTuple || return (;) + return (; (key => record[key] for key in (:parameters, :options, :equivalent_earth) + if haskey(record, key) && record[key] !== nothing && + !(record[key] isa NamedTuple && isempty(record[key])))...) + end + entries=Pair{Tuple,Any}[(owner,()) => (owner => retained.options)] + for (slot,family) in pairs(owner;quantity) + selected=retained.methods[slot] + requested=retained.requested[slot] + if selected isa NamedTuple + children = pairs(family) + issubset(keys(selected), (key for (key,_) in children)) || throw(ArgumentError( + "retained $slot selections contain unknown owning formula slots")) + for (route,_) in children + haskey(selected, route) || continue + value=selected[route] + controls=explicit(requested isa NamedTuple ? requested[route] : requested) + push!(entries,(owner,(slot,route)) => (value===nothing || ismissing(value) ? value : value => controls)) + end + else + controls=explicit(requested) + push!(entries,(owner,(slot,)) => (selected===nothing || ismissing(selected) ? selected : selected => controls)) + end + end + return entries +end + +function LineParametersFormulation(methods::NamedTuple, options::FormulationOptions) + LineParametersFormulation(methods, options, methods) +end + +function _line_formulation( + internal_impedance, + insulation_impedance, + earth_impedance, + shunt_model, + insulation_admittance, + semicon_admittance, + earth_admittance, + earth_properties, + pipe_impedance, + temperature_dependence, + options::FormulationOptions +) + selected = LineParametersFormulation((; + internal_impedance = InternalImpedance.Formula(internal_impedance), + insulation_impedance = InsulationImpedance.Formula(insulation_impedance), + earth_impedance = EarthImpedance.Formula(earth_impedance), + shunt_model = ShuntModel.Formula(shunt_model), + insulation_admittance = InsulationAdmittance.Formula(insulation_admittance), + semicon_admittance = SemiconAdmittance.Formula(semicon_admittance), + earth_admittance = EarthAdmittance.Formula(earth_admittance), + earth_properties = earth_properties === nothing ? nothing : + Earth.FrequencyDependent.Formula(earth_properties), + pipe_impedance = PipeImpedance.Formula(pipe_impedance), + temperature_dependence = temperature_dependence === nothing ? nothing : + TemperatureDependent.Formula(temperature_dependence)), + formulation_options(LineParametersFormulation, options)) + definitions = (; internal_impedance, insulation_impedance, + earth_impedance = earth_impedance isa NamedTuple ? + NamedTuple{keys(selected.methods.earth_impedance)}(earth_impedance) : earth_impedance, + shunt_model, insulation_admittance, semicon_admittance, + earth_admittance = earth_admittance isa NamedTuple ? + NamedTuple{keys(selected.methods.earth_admittance)}(earth_admittance) : earth_admittance, + earth_properties, pipe_impedance, temperature_dependence) + return LineParametersFormulation(selected.methods, selected.options, definitions) +end + +""" +$(TYPEDSIGNATURES) + +Select the complete physical-method bundle for a line-parameter computation. +`Formulation(; kwargs...)` and `Formulation(:coaxial; kwargs...)` call it. + +`shunt_model=:default` (or `:equivalent`) selects the equivalent annular layer as local shunt +geometry. +`:boundary` explicitly computes a lossless wire and tape boundary correction. +This choice is independent of `insulation_admittance` and `semicon_admittance`, +which select material constitutive laws. Geometric boundary numerical controls and an +explicit fallback belong to `formula(:boundary; options, parameters)`. + +`internal_impedance` accepts one formula. A solid primitive requests only its `outer` +surface, and a tubular primitive all three. Before the frequency loop, the computation +checks that the formula has an expression for the surface impedances that it requests. + +`earth_impedance` and `earth_admittance` each accept one formula or a NamedTuple +with the required subset of `air`, `earth`, and `mixed` selections. On air and one +homogeneous earth, these select `(s,t)=(1,1)`, `(2,2)`, and the two cross-layer mutual +directions. Actual kind and source and target method dispatch governs equation +applicability. Missing cases have no implicit fallback. A recipe describes a homogeneous +earth, so each of its formulas stops at layer 2, and a multilayer formula is used alone. On +an earth with more layers, the slot takes one equivalent earth: the explicit reduction that +its formulas agree on, or the `:default` reduction. Its routes then resolve on layers 1 and +2. Model parameters and numerical options remain local to each selected entry. + +`temperature_dependence=formula(:default)` selects the Materials-owned linear +resistivity law. `nothing` retains reference resistivity. Operating temperature +belongs to the problem. Reference temperature and coefficients belong to each +material. + +Each formula slot and the complete `options` tuple accepts either one scalar +selection or an explicit +[`Grid`](@ref LineCableModels.ParametricBuilder.Grid)/ +[`Gridspace`](@ref LineCableModels.ParametricBuilder.Gridspace) source. Scalar +inputs return one [`LineParametersFormulation`](@ref). Varying inputs return a +`Gridspace{LineParametersFormulation}` whose points contain only completed, +owner-resolved formula values. `:default` routes to an explicit implementation. +The scalar problem supplies its geometry and earth context for validation. + +`combine=:product` forms the Cartesian product among varying fields in this +formulation. `combine=:zip` aligns equally sized fields and broadcasts +singletons. This composition is independent of the Cartesian product between +problem points and formulation points performed by +[`Combinatorial`](@ref LineCableModels.ParametricBuilder.Combinatorial). +""" +function LineParametersFormulation(; + internal_impedance = formula(:default), + insulation_impedance = formula(:default), + earth_impedance = formula(:default), + shunt_model = formula(:default), + insulation_admittance = formula(:default), + semicon_admittance = formula(:default), + earth_admittance = formula(:default), + earth_properties = formula(:default), + pipe_impedance = formula(:default), + temperature_dependence = formula(:default), + options = FormulationOptions(), + combine::Symbol = :product +) + values = ( + internal_impedance, + insulation_impedance, + earth_impedance, + shunt_model, + insulation_admittance, + semicon_admittance, + earth_admittance, + earth_properties, + pipe_impedance, + temperature_dependence, + options + ) + return parameterize( + LineParametersFormulation, + (inputs...) -> _line_formulation(inputs[1:end-1]..., last(inputs) isa NamedTuple ? FormulationOptions(last(inputs)) : last(inputs)), + values; + combine + ) +end + +""" +$(TYPEDSIGNATURES) + +Build the coaxial formulation, [`LineParametersFormulation`](@ref), from the same keywords. +""" +Formulation(; kwargs...) = LineParametersFormulation(; kwargs...) + +Formulation(::Val{:coaxial}; kwargs...) = LineParametersFormulation(; kwargs...) + +""" +$(TYPEDSIGNATURES) + +Expose complete requested and resolved formula selections and reduction options. +Selections retain scientific identity and data. Serialization belongs to the writer. +""" +function Base.NamedTuple(value::LineParametersFormulation) + # Copy record containers, not their scalar leaves: measurement sources and + # other immutable scientific values keep their identities and correlations. + copy_containers(value) = value isa Union{NamedTuple, Tuple, AbstractArray} ? + map(copy_containers, value) : value + record = function (selected) + selected === nothing && return nothing + selected isa Symbol && return NamedTuple(formula(selected)) + selected isa NamedTuple && return map(record,selected) + return NamedTuple(selected) + end + # Retain complete records without specializing result containers on each + # parameter tuple or concrete selection type. The stored values remain unchanged. + Record=NamedTuple{(:backend,:requested,:methods,:options), + Tuple{Symbol,NamedTuple,NamedTuple,NamedTuple}} + return Record((:coaxial,copy_containers(map(record,value.definitions)), + copy_containers(map(record,value.methods)),copy_containers(value.options.data))) +end diff --git a/src/engine/reduction.jl b/src/engine/reduction.jl deleted file mode 100644 index 8ceed9531..000000000 --- a/src/engine/reduction.jl +++ /dev/null @@ -1,150 +0,0 @@ -using LinearAlgebra: BLAS, BlasFloat - -function reorder_indices(map::AbstractVector{<:Integer}) - n = length(map) - phases = Int[] # encounter order of phases > 0 - firsts = Int[] - sizehint!(firsts, n) - zeros = Int[] - sizehint!(zeros, n) - tails = Dict{Int, Vector{Int}}() # phase => remaining indices - - seen = Set{Int}() - @inbounds for (i, p) in pairs(map) - if p > 0 - if !(p in seen) - push!(seen, p) - push!(phases, p) - push!(firsts, i) - else - push!(get!(tails, p, Int[]), i) - end - else - push!(zeros, i) - end - end - - perm = Vector{Int}(undef, n) - k = 1 - @inbounds begin - for i in firsts - perm[k] = i - k += 1 - end - for p in phases - if haskey(tails, p) - for i in tails[p] - perm[k] = i - k += 1 - end - end - end - for i in zeros - perm[k] = i - k += 1 - end - end - return perm -end - -# Non-mutating reorder (2D) -function reorder_M(M::AbstractMatrix, map::AbstractVector{<:Integer}) - n = size(M, 1) - n == size(M, 2) == length(map) || throw(ArgumentError("shape mismatch")) - perm = reorder_indices(map) - return M[perm, perm], map[perm] -end - - -""" - kronify(M, phase_map) - Kron elimination -""" -function kronify( - M::Matrix{Complex{T}}, - phase_map::Vector{Int}, -) where {T <: REALSCALAR} - keep = findall(!=(0), phase_map) - eliminate = findall(==(0), phase_map) - - M11 = M[keep, keep] - M12 = M[keep, eliminate] - M21 = M[eliminate, keep] - M22 = M[eliminate, eliminate] - - return M11 - (M12 * inv(M22)) * M21 -end - -""" - kronify!(M, phase_map, Mred) - Kron's angry little brother (in-place) -""" -function kronify!( - M::Matrix{Complex{T}}, - phase_map::Vector{Int}, - Mred::Matrix{Complex{T}}, -) where {T <: REALSCALAR} - keep = findall(!=(0), phase_map) - eliminate = findall(==(0), phase_map) - - M11 = M[keep, keep] - M12 = M[keep, eliminate] - M21 = M[eliminate, keep] - M22 = M[eliminate, eliminate] - @views @inbounds Mred .= M11 - (M12 * inv(M22)) * M21 - return nothing -end - -# In-place: columns tail -= first (from original), then rows tail -= first (after col pass). -function merge_bundles!(M::AbstractMatrix{T}, ph::AbstractVector{<:Integer}) where {T} - n = size(M, 1) - (size(M, 2) == n && length(ph) == n) || throw(ArgumentError("shape mismatch")) - - # Encounter-ordered groups (include phase 0) - groups = Vector{Vector{Int}}() - index_of = Dict{Int, Int}() - @inbounds for (i, p) in pairs(ph) - gi = get(index_of, p, 0) - if gi == 0 - push!(groups, Int[]) - gi = length(groups) - index_of[p] = gi - end - push!(groups[gi], i) - end - - # -------- Pass 1: columns -------- - @inbounds for grp in groups - length(grp) > 1 || continue - i1 = grp[1] - base_col = @view M[:, i1] # original column kept intact in this pass - for t in Iterators.drop(eachindex(grp), 1) - j = grp[t] - col = @view M[:, j] - if (M isa StridedMatrix{T}) && (T <: BlasFloat) - BLAS.axpy!(-one(T), base_col, col) # col -= base_col - else - col .-= base_col - end - end - end - - # -------- Pass 2: rows -------- - newmap = copy(ph) - @inbounds for grp in groups - length(grp) > 1 || continue - i1 = grp[1] - base_row = @view M[i1, :] # uses row after pass 1 (matches Z1→Z2) - for t in Iterators.drop(eachindex(grp), 1) - i = grp[t] - row = @view M[i, :] - if (M isa StridedMatrix{T}) && (T <: BlasFloat) - BLAS.axpy!(-one(T), base_row, row) # row -= base_row - else - row .-= base_row - end - newmap[i] = 0 - end - end - return M, newmap -end diff --git a/src/engine/semiconadmittance/SemiconAdmittance.jl b/src/engine/semiconadmittance/SemiconAdmittance.jl new file mode 100644 index 000000000..264d4a33f --- /dev/null +++ b/src/engine/semiconadmittance/SemiconAdmittance.jl @@ -0,0 +1,46 @@ +""" + LineCableModels.Engine.SemiconAdmittance + +Define registered constitutive relations for semiconducting-screen admittance. + +# Dependencies + +$(IMPORTS) + +""" +module SemiconAdmittance +import ...Commons: FormulationOptions, formulas +using ...Commons: Functor +import ...Commons: formulation_options + +export Formula, formula_id, formulas + +#! explicit-imports: off +using DocStringExtensions: IMPORTS, TYPEDEF, TYPEDFIELDS, TYPEDSIGNATURES +#! explicit-imports: on +import ..Engine: SemiconAdmittanceFormulation, formula_id, validate +import ...LineCableModels: FormulaDefinition, Expression +using ...Materials: Material +#! explicit-imports: off +import ..Engine: description, conductivity +using ...Commons: vacuum_permittivity +#! explicit-imports: on + +include("interface.jl") + +public semicon_material + +#! explicit-imports: off +const FORMULAS = ( + include("formulas/default.jl"), + include("formulas/lossless.jl"), + include("formulas/lossy.jl"), +) +#! explicit-imports: on + +""" +Return the built-in semicon-admittance formula identifiers. +""" +formulas(::Type{<:Formula}) = FORMULAS + +end # module SemiconAdmittance diff --git a/src/engine/semiconadmittance/formulas/default.jl b/src/engine/semiconadmittance/formulas/default.jl new file mode 100644 index 000000000..c8a84fb5c --- /dev/null +++ b/src/engine/semiconadmittance/formulas/default.jl @@ -0,0 +1,12 @@ +""" +$(TYPEDSIGNATURES) + +Route the default selection to the explicit `:lossless` implementation. +No numerical equation is owned by `:default`. +""" +description(::Type{<:Formula{:default}}; compact::Bool=false) = + compact ? "Default" : "Default routing to :lossless" + +Formula{:default}(; kwargs...) = Formula{:lossless}(; kwargs...) + +:default diff --git a/src/engine/semiconadmittance/formulas/lossless.jl b/src/engine/semiconadmittance/formulas/lossless.jl new file mode 100644 index 000000000..dd0870d61 --- /dev/null +++ b/src/engine/semiconadmittance/formulas/lossless.jl @@ -0,0 +1,52 @@ +""" +$(TYPEDSIGNATURES) + +**Identification.** Lossless semiconducting-screen approximation retaining +displacement current while suppressing conduction and polarization loss. + +**Expression.** + +```math +\\kappa=j\\omega\\varepsilon_0\\varepsilon_r. +``` + +This is the lossless specialization of the standard frequency-domain +constitutive relation. +""" +function description(::Type{<:Formula{:lossless}}; compact::Bool=false) + compact ? "Lossless" : "Lossless semiconducting-screen admittivity" +end + +""" +$(TYPEDSIGNATURES) + +Evaluate lossless semiconducting-screen admittivity: + +```math +\\kappa=j\\omega\\varepsilon_0\\varepsilon_r. +``` + +# Arguments + +- `functor`: the Functor of the evaluation point. Its input holds: + - `material`: semiconducting material and relative permittivity. + - `frequency`: evaluation frequency \\[Hz\\]. + - `temperature`: operating temperature \\[°C\\]. + - `options`: the normalized formulation options of the formula. +- `workspace`: optional computation workspace supplying reusable numerical buffers. + +# Returns + +- Complex lossless admittivity \\[S/m\\]. +""" +@inline function semicon_material(::Formula{:lossless}, functor, workspace) + (; material, frequency) = functor.input + T = typeof(frequency) + ε₀ = vacuum_permittivity(T) + ω = 2 * (one(T) * π) * frequency + return complex(zero(T), ω) * ε₀ * material.eps_r +end + +formulation_options(::Expression{<:Formula{:lossless}, typeof(semicon_material)}) = FormulationOptions() + +:lossless diff --git a/src/engine/semiconadmittance/formulas/lossy.jl b/src/engine/semiconadmittance/formulas/lossy.jl new file mode 100644 index 000000000..872a42bb7 --- /dev/null +++ b/src/engine/semiconadmittance/formulas/lossy.jl @@ -0,0 +1,73 @@ +""" +$(TYPEDSIGNATURES) + +**Identification.** Generic lossy complex-admittivity representation of a +homogeneous semiconducting screen. Ohmic conduction, dielectric displacement, +and an optional polarization-loss contribution are evaluated together. + +**Expression.** For resistivity ``\\rho``, real relative permittivity +``\\varepsilon_r``, and polarization loss tangent ``\\tan\\delta_p``, + +```math +\\kappa=\\frac{1}{\\rho}+\\omega\\varepsilon_0\\varepsilon_r\\tan\\delta_p+ +j\\omega\\varepsilon_0\\varepsilon_r. +``` + +The annular layer operator converts this material admittivity to +``Y=2\\pi\\kappa/\\ln(b/a)``. This is the standard frequency-domain +constitutive relation, not an author-specific empirical law. Ametani, +Miyamoto, and Nagaoka (2004), Eqs. (14)-(15), remain a useful application +reference for its zero-``\\tan\\delta_p`` semiconducting-screen specialization +and radial series assembly. +""" +function description(::Type{<:Formula{:lossy}}; compact::Bool=false) + compact ? "Lossy" : "Lossy complex-admittivity semiconducting-screen model" +end + +""" +$(TYPEDSIGNATURES) + +Evaluate the standard lossy material admittivity for a semiconducting layer: + +```math +\\kappa=\\frac{1}{\\rho}+\\omega\\varepsilon_0\\varepsilon_r\\tan\\delta_p+ +j\\omega\\varepsilon_0\\varepsilon_r. +``` + +`material.tan_delta` represents polarization loss only. Conduction is supplied +by `material.rho`. The common coaxial operator applies the annular geometry. + +# Arguments + +- `functor`: the Functor of the evaluation point. Its input holds: + - `material`: semiconducting material properties, including resistivity and + relative permittivity. + - `frequency`: evaluation frequency \\[Hz\\]. + - `temperature`: operating temperature \\[°C\\]. + - `options`: the normalized formulation options of the formula. +- `workspace`: optional computation workspace supplying reusable numerical buffers. + +# Returns + +- Complex screen admittivity \\[S/m\\]. + +# Notes + +Ametani, Miyamoto, and Nagaoka (2004), DOI +10.1109/TPWRD.2003.822502, is retained as an application reference for the +semiconducting-screen specialization. The constitutive relation itself is +standard frequency-domain electromagnetism. +""" +@inline function semicon_material(::Formula{:lossy}, functor, workspace) + (; material, frequency) = functor.input + T = typeof(frequency) + ε₀ = vacuum_permittivity(T) + ω = 2 * (one(T) * π) * frequency + displacement = complex(zero(T), ω) * ε₀ * material.eps_r + return conductivity(material.rho) + imag(displacement) * material.tan_delta + + displacement +end + +formulation_options(::Expression{<:Formula{:lossy}, typeof(semicon_material)}) = FormulationOptions() + +:lossy diff --git a/src/engine/semiconadmittance/interface.jl b/src/engine/semiconadmittance/interface.jl new file mode 100644 index 000000000..818e047e8 --- /dev/null +++ b/src/engine/semiconadmittance/interface.jl @@ -0,0 +1,101 @@ +""" +$(TYPEDEF) + +Select one semiconducting-screen constitutive relation by its stable literature +identifier. + +Each formula implements `semicon_material(selected, functor, workspace)` on its concrete +selection type. The input of `functor` holds the material, the frequency, the temperature +and the options. The method returns the material's frequency-evaluated admittivity [S/m]. Geometry and +radial series aggregation remain common Engine operations. + +$(TYPEDFIELDS) +""" +struct Formula{ID, P <: NamedTuple, O <: FormulationOptions} <: SemiconAdmittanceFormulation + "Resolved physical model parameters." + parameters::P + "Normalized numerical sections for this equation." + options::O +end + +""" +Evaluate one formula-owned semiconducting-material constitutive relation. +""" +function semicon_material end + +""" +$(TYPEDSIGNATURES) + +Construct a selected formulation with model parameters and numerical controls. +Custom formulations extend `semicon_material` on their own concrete selection type. +Unknown controls fail before numerical evaluation. +""" +function Formula{ID}(; parameters::NamedTuple=(;), options::Union{NamedTuple, FormulationOptions} = FormulationOptions()) where {ID} + options = options isa NamedTuple ? FormulationOptions(options) : options + isempty(parameters) || throw(ArgumentError("formula :$ID has no configurable model parameters")) + selected = Formula{ID, typeof(parameters), typeof(options)}(parameters, options) + expression = Expression(selected, semicon_material) + normalized = formulation_options(expression, options) + return Formula{ID, typeof(parameters), typeof(normalized)}(parameters, normalized) +end + +@inline function (formula::SemiconAdmittanceFormulation)( + material::Material{T}, + frequency::T, + temperature::T; workspace = nothing +) where {T <: Real} + functor = Functor(formula, + (; material, frequency, temperature, options = formula.options); workspace) + value = Expression(formula, semicon_material)(functor, workspace) + return convert(Complex{T}, validate(value, formula, T)) +end + +function (formula::SemiconAdmittanceFormulation)( + material::Material{T}, + frequency::Real, + temperature::Real; workspace = nothing +) where {T <: Real} + U = promote_type( + T, + typeof(float(frequency)), + typeof(float(temperature)) + ) + return formula( + convert(Material{U}, material), + convert(U, float(frequency)), + convert(U, float(temperature)); workspace + ) +end + +Formula(identifier::Symbol; kwargs...) = Formula(Val(identifier); kwargs...) +Formula(::Val{ID}; kwargs...) where {ID} = Formula{ID}(; kwargs...) +Formula(selected::SemiconAdmittanceFormulation) = selected + +function Formula(selection::FormulaDefinition{ID, Order}) where {ID, Order} + Order === :default || throw(ArgumentError("order applies only to equivalent_earth")) + selection.equivalent_earth === nothing || throw(ArgumentError( + "equivalent_earth applies only to external earth formulas")) + return Formula{ID}(; parameters = selection.parameters, + options = selection.options) +end + +""" +Return the stable identifier of a semicon-admittance formula. +""" +formula_id(::Formula{ID}) where {ID} = ID +formula_id(::Type{<:Formula{ID}}) where {ID} = ID +# Identity-only dispatch also describes retained selections without constructors. +description(value::Formula; compact::Bool = false) = description(typeof(value); compact) +formulation_options(value::Formula) = value.options + +""" +$(TYPEDSIGNATURES) + +Expose the selected identity, model parameters, and numerical options as a native record. +""" +function Base.NamedTuple(value::Formula) + return (identifier=formula_id(value), parameters=value.parameters, options=value.options.data) +end + +"""Iterate the independently selectable child slots admitted by this formula family.""" +Base.pairs(::Type{<:Formula}; quantity=nothing) = pairs((;)) diff --git a/src/engine/shuntmodel/ShuntModel.jl b/src/engine/shuntmodel/ShuntModel.jl new file mode 100644 index 000000000..e6a929b81 --- /dev/null +++ b/src/engine/shuntmodel/ShuntModel.jl @@ -0,0 +1,44 @@ +""" + LineCableModels.Engine.ShuntModel + +Select the cable-local shunt geometry model independently of dielectric +constitutive laws. The equivalent annular layer is the default. The geometric boundary +approximation resolves eligible open wires and tapes inside a closed circular shield. + +# Dependencies + +$(IMPORTS) +""" +module ShuntModel +import ...Commons: FormulationOptions, formulas +using DocStringExtensions: TYPEDEF, TYPEDFIELDS, TYPEDSIGNATURES +#! explicit-imports: off +# Expanded in the module docstring, outside this module's analyzed expressions. +using DocStringExtensions: IMPORTS +#! explicit-imports: on +import ..Engine: ShuntModelFormulation +import ...LineCableModels: FormulaDefinition, formula_id, description +import ...Commons: formulation_options, initialize_buffers +using ...Commons: vacuum_permittivity +import ...Materials: Material +import ...DataModel: CableDesign +import ...DataModel +import ...TextDisplay +import ..Engine: CableBlueprint, InternalShuntBlock, internal_shunt_response, + blueprint_dependencies, same_physical_state, numerical_magnitude +import ..Engine: InsulationAdmittance, SemiconAdmittance +import ...LineCableModels: nominal +using LinearAlgebra: eigvals!, norm, I, lu, mul!, qr!, ColumnNorm, eigen, + SymTridiagonal, UpperTriangular +using QuadGK: alloc_segbuf, quadgk! +import SpecialFunctions + +export Formula, formulas + +include("interface.jl") +include("geometry.jl") +include("charge_collocation.jl") +include("blueprint.jl") + +public BoundarySolveError +end diff --git a/src/engine/shuntmodel/blueprint.jl b/src/engine/shuntmodel/blueprint.jl new file mode 100644 index 000000000..b4807b9e0 --- /dev/null +++ b/src/engine/shuntmodel/blueprint.jl @@ -0,0 +1,85 @@ +# Boundary coefficients are completed while constructing coaxial blueprints. +# The solved-domain inventory lives only for this flattening call. +const ShuntDomainReport = NamedTuple{ + (:design, :terminals, :requested, :effective, :reason, :message), + Tuple{Int, UnitRange{Int}, Symbol, Symbol, Symbol, String}} + +# Each shunt model selects the formulations needed to construct its blueprint. +# The equivalent annular layer model omits the dielectric selections handled during the +# solve. +function blueprint_dependencies(::ShuntModelFormulation, methods) + methods[(:shunt_model, :insulation_admittance, :semicon_admittance)] +end +blueprint_dependencies(::Formula{:equivalent}, methods) = methods[(:shunt_model,)] + +function internal_shunt_response(selected::ShuntModelFormulation, design, + geometry, T, methods, solutions, design_index) + domains=internal_shunt_domains(design, geometry, T; design_index) + return internal_shunt_response(selected, domains, methods, solutions) +end + +function internal_shunt_response( + selected::Formula{:equivalent}, + design::CableDesign, geometry, ::Type{T}, methods, solutions, design_index) where {T} + requested = formula_id(selected) + reports = ShuntDomainReport[(design_index, indices, requested, + :equivalent, :selected, + "Equivalent annular layers; no boundary extraction or audit") + for indices in geometry.assembly_ranges] + return (blocks = InternalShuntBlock{T}[], + details = + (requested, effective = :equivalent, solves = 0, domains = reports, + diagnostics = InternalShuntDiagnostic[])) +end + +function internal_shunt_response(selected::Formula{:boundary}, + domains::Vector{InternalShuntDomain{T}}, methods, solutions) where {T} + blocks = InternalShuntBlock{T}[] + diagnostics = InternalShuntDiagnostic[] + reports = ShuntDomainReport[] + for domain in domains + try + _shunt_lossless(methods) || throw(BoundarySolveError(:unsupported, + (; design = domain.design, terminals = domain.terminals), + "boundary shunt requires the built-in lossless dielectric laws")) + previous = findfirst( + value -> isequal(value.methods, methods) && + _shunt_domain_equal(value.domain, domain), + solutions) + if previous === nothing + # The admitted lossless laws are independent of frequency and + # temperature. Their dielectric descriptors retain UQ sources. + result = internal_shunt_response( + selected, _shunt_values(domain, methods), domain; + level = selected.options.data.resolution, + integration = selected.options.data.integration, audit = selected.options.data.audit) + C = Matrix{T}(result.C) + P = lu(C) \ Matrix{T}(I, size(C, 1), size(C, 1)) + diagnostic = result.diagnostic + push!(solutions, (; domain, methods, C, P, diagnostic)) + else + source = solutions[previous] + C, P, diagnostic = source.C, source.P, source.diagnostic + end + push!(blocks, InternalShuntBlock(domain.assembly, domain.terminals, C, P)) + push!(diagnostics, diagnostic) + push!(reports, ( + domain.design, domain.terminals, :boundary, :boundary, :resolved, "")) + catch exception + exception isa BoundarySolveError || rethrow() + selected.parameters.fallback === :equivalent || rethrow() + @warn "Boundary shunt replaced by the equivalent annular layer" design=domain.design terminals=domain.terminals reason=exception.category + push!(reports, + (domain.design, domain.terminals, :boundary, + :equivalent, exception.category, sprint(showerror, exception))) + end + end + solved = Base.IdSet{Matrix{T}}() + foreach(block -> push!(solved, block.C), blocks) + effective = isempty(reports) ? :equivalent : + all(r -> r.effective === :boundary, reports) ? :boundary : + all(r -> r.effective === :equivalent, reports) ? :equivalent : :mixed + return (blocks, + details = (requested = :boundary, effective, + solves = length(solved), domains = reports, diagnostics)) +end diff --git a/src/engine/shuntmodel/charge_collocation.jl b/src/engine/shuntmodel/charge_collocation.jl new file mode 100644 index 000000000..5221ccbfd --- /dev/null +++ b/src/engine/shuntmodel/charge_collocation.jl @@ -0,0 +1,1075 @@ +# Integrated finite-face charge element for the explicit boundary shunt model. +# Coordinates are in m. Green functions use charge/(2pi*epsilon0). +# Dense numerical arrays are local to a boundary solve, never a global cache. + +""" +$(TYPEDEF) + +Report a recognized failure of the local geometric boundary approximation. Numerical +failure is distinct from invalid physical input and must not trigger Monte +Carlo rejection and resampling. + +$(TYPEDFIELDS) +""" +struct BoundarySolveError <: Exception + "Failure stage or unsupported model assumption." + category::Symbol + "Physical location and available numerical diagnostics." + context::NamedTuple + "Explanation of the failure." + message::String +end + +function Base.showerror(io::IO, error::BoundarySolveError) + print(io, "BoundarySolveError(", error.category, "): ", error.message) + isempty(error.context) || print(io, "; ", error.context) +end +Base.show(io::IO, error::BoundarySolveError) = showerror(io, error) +function Base.summary(io::IO, error::BoundarySolveError) + print(io, "BoundarySolveError(", error.category, ")") +end +function Base.show(io::IO, ::MIME"text/plain", error::BoundarySolveError) + get(io, :compact, false) && return summary(io, error) + TextDisplay.tree(io, + sprint(summary, error), + ( + (label = error.message, noun = "details"), + (label = sprint(show, error.context; context = :limit => true), + noun = "details") + ); + noun = "details") +end + +""" +$(TYPEDSIGNATURES) + +Propagate the radial Dirichlet-to-Neumann load of one positive angular mode +through concentric dielectric layers. In log-radius coordinates, the load +transformation is the admittance form of the uniform-line input-impedance +relation [Sunde1968](@cite), Section 1.6, Eqs. (1.39)-(1.42), p. 15: + +```math +Y_{\\mathrm{in}}=Y_c\\frac{Y_L+Y_c\\tanh(m\\ell)} +{Y_c+Y_L\\tanh(m\\ell)},\\qquad +Y_c=\\varepsilon_r m,\\qquad\\ell=\\log(r_o/r_i). +``` + +Here the loads ``Y_L,Y_c,Y_{\\mathrm{in}}`` are dimensionless modal loads, +not electrical admittances. The recursion starts with ``Y_L=\\infty`` at +grounded metal. Applying the line relation to cylindrical Laplace modes is +the log-radius construction used here, not a cable propagation calculation. + +# Arguments + +- `layers`: radially ordered layers with inner and outer radii \\[m\\] and + positive relative permittivities \\[dimensionless\\]. +- `m`: positive angular mode order. + +# Keywords + +- `reverse_layers=false`: reverse the layer order to propagate from the + outer grounded shield toward the host instead of from the inner conductor. + +# Returns + +- Modal load seen at the host boundary \\[dimensionless\\]. `Inf` when the + layer sequence is empty, representing an immediately adjacent metal boundary. +""" +function _shunt_load(layers, m; reverse_layers = false) + load = Inf + for layer in (reverse_layers ? Iterators.reverse(layers) : layers) + t = tanh(m * log(layer.ro/layer.ri)) + characteristic = layer.epsilon * m + load = isinf(load) ? characteristic/t : + characteristic * (load + characteristic*t)/(characteristic + load*t) + end + load +end + +""" +$(TYPEDSIGNATURES) + +Evaluate the reflected Fourier modes of the local electrostatic Green function +in a concentric layered annulus. The radial basis ``r^{\\pm m}``, with +``1,\\log r`` for the zero mode, is the classical cylindrical Laplace basis. +See [Schelkunoff1934](@cite), Eqs. (122)-(124), p. 573, in the small-radius-to- +wavelength limit of cylindrical fields. Here it is used for the electrostatic +potential. The layered-kernel strategy is also supported by [Bernal1997](@cite), +Sections II-III, whose planar Prony/matrix-pencil kernel is not used here. + +With ``t=\\log(r/a)``, each source-free dielectric layer satisfies +``d^2u_m/dt^2-m^2u_m=0``. Matching potential and normal displacement gives +the radial load recursion in `_shunt_load`, terminated at grounded metal. +The zero Fourier mode is evaluated separately in `_shunt_kernel`: + +```math +G_0(r,s)=\\frac{R_< (R_{\\mathrm{total}}-R_>)}{R_{\\mathrm{total}}}, +\\qquad R(r)=R_{\\mathrm{left}}+\\frac{\\log(r/a)}{\\varepsilon_h}. +``` + +Here ``R_<`` and ``R_>`` are the smaller and larger of ``R(r),R(s)``. +The dielectric log-radius sums ``R`` and host relative permittivity +``\\varepsilon_h`` are dimensionless. The log-radius normalization follows +the coaxial capacitance ``C=2\\pi\\varepsilon/\\log(r_o/r_i)`` \\[F/m\\], with +absolute permittivity ``\\varepsilon`` \\[F/m\\]. See [Sunde1968](@cite), +Section 1.5, Eq. (1.20), p. 11. Direct and nearest-interface logarithms +are extracted analytically. Only the remaining reflections are truncated. +For an adjacent dielectric, the extracted contrast is +``(\\varepsilon_h-\\varepsilon_j)/(\\varepsilon_h+\\varepsilon_j)``, also used +in [Campione2018](@cite), Section 2, Eq. (3). That paper uses a local planar +approximation to the braid geometry. It supports these dielectric image +factors, not this concentric annular Green function or its radial load recursion. + +# Arguments + +- `g`: host radii and layer boundaries \\[m\\], with positive relative + permittivities \\[dimensionless\\] and dielectric log-radius sums. +- `modes`: number of retained positive Fourier modes. + +# Returns + +- `A`, `B`, `D`: smooth Fourier reflection coefficients \\[dimensionless\\]. +- `ra0`, `rb0`: reflection coefficients at the nearest inner and outer interfaces + \\[dimensionless\\]. The resulting kernel multiplies charge per unit length + divided by ``2\\pi\\varepsilon_0`` \\[V\\] to give potential \\[V\\]. +""" +function _shunt_kernel_coefficients(g, modes) + A, B, D = zeros(modes), zeros(modes), zeros(modes) + ra0 = isempty(g.left) ? -1.0 : + (g.epsilon-last(g.left).epsilon)/(g.epsilon+last(g.left).epsilon) + rb0 = isempty(g.right) ? -1.0 : + (g.epsilon-first(g.right).epsilon)/(g.epsilon+first(g.right).epsilon) + for m in 1:modes + l = _shunt_load(g.left, m)/g.epsilon + r = _shunt_load(g.right, m; reverse_layers = true)/g.epsilon + ra = isinf(l) ? -1.0 : (m-l)/(m+l) + rb = isinf(r) ? -1.0 : (m-r)/(m+r) + denominator = g.epsilon*m*(1-ra*rb*(g.a/g.b)^(2m)) + A[m] = ra/denominator-ra0/(g.epsilon*m) + B[m] = rb/denominator-rb0/(g.epsilon*m) + D[m] = ra*rb/denominator + end + (; A, B, D, ra0, rb0) +end + +function _shunt_kernel(z, source, g, k; regular = false, split_images = false) + r, s = abs(z), abs(source) + Rr = g.Rleft + log(r/g.a)/g.epsilon + Rs = g.Rleft + log(s/g.a)/g.epsilon + result = min(Rr, Rs)*(g.Rtotal-max(Rr, Rs))/g.Rtotal + if split_images + result += (log(max(r, s))+k.ra0*log(s)+k.rb0*(2log(g.b)-log(r)))/g.epsilon + else + result += (regular ? log(max(r, s)) : log(max(r, s)/abs(z-source)))/g.epsilon + result -= k.ra0/g.epsilon*log(abs(1-g.a^2/(conj(z)*source))) + result -= k.rb0/g.epsilon*log(abs(1-z*conj(source)/g.b^2)) + end + qa, qb = g.a^2/(r*s), r*s/g.b^2 + qd = (g.a/g.b)^2 + qc, qe = qd*r/s, qd*s/r + pa, pb, pc, pe = qa, qb, qc, qe + cosine = real(z*conj(source))/(r*s) + previous, current = 1.0, cosine + @inbounds for m in eachindex(k.A) + result += current*(k.A[m]*pa + k.B[m]*pb + k.D[m]*(pc+pe)) + previous, current = current, 2cosine*current-previous + pa *= qa + pb *= qb + pc *= qc + pe *= qe + end + result +end + +""" +$(TYPEDSIGNATURES) + +Extend `buffers` with the kernel storage of one shunt assembly. It holds `plan.rows` +targets, `plan.columns` sources, Fourier blocks of at most 64 of the `plan.modes` modes, +and tape kernels with `plan.quadrature` nodes per face. +""" +function initialize_buffers(::Formula{:boundary}, ::Val{:kernel}, ::Type{T}, input, plan, + buffers) where {T} + (; rows, columns, modes, quadrature) = plan + return merge(buffers, + (ta = Vector{Complex{T}}(undef, rows), tb = Vector{Complex{T}}(undef, rows), + sa = Vector{Complex{T}}(undef, columns), sb = Vector{Complex{T}}(undef, columns), + tpa = Vector{Complex{T}}(undef, rows), tpb = Vector{Complex{T}}(undef, rows), + spa = Vector{Complex{T}}(undef, columns), + spb = Vector{Complex{T}}(undef, columns), + U = Matrix{T}(undef, rows, 4min(64, modes)), + V = Matrix{T}(undef, columns, 4min(64, modes)), + tape = Matrix{T}(undef, rows, quadrature))) +end + +function _shunt_kernel_matrix!(matrix, targets, sources, g, k; regular = false, + split_images = false, buffers) + # Same Green function, batched into small Fourier blocks for BLAS. No dense + # N-by-modes cache: working storage is only 4*64 columns per geometric boundary set. + zero_modes = merge(k, (; A = (), B = (), D = ())) + @inbounds for j in eachindex(sources), i in eachindex(targets) + + matrix[i, j] = _shunt_kernel( + targets[i], sources[j], g, zero_modes; regular, split_images) + end + nr, nc = length(targets), length(sources) + ta, tb = @view(buffers.ta[1:nr]), @view(buffers.tb[1:nr]) + sa, sb = @view(buffers.sa[1:nc]), @view(buffers.sb[1:nc]) + tpa, tpb = @view(buffers.tpa[1:nr]), @view(buffers.tpb[1:nr]) + spa, spb = @view(buffers.spa[1:nc]), @view(buffers.spb[1:nc]) + ta .= g.a ./ conj.(targets); + tb .= targets ./ g.b + sa .= g.a ./ conj.(sources); + sb .= sources ./ g.b + fill!(tpa, 1); + fill!(tpb, 1); + fill!(spa, 1); + fill!(spb, 1) + U, V = @view(buffers.U[1:nr, :]), @view(buffers.V[1:nc, :]) + for first_mode in 1:64:length(k.A) + modes = first_mode:min(first_mode + 63, length(k.A)) + for (column, m) in enumerate(modes) + tpa .*= ta + tpb .*= tb + spa .*= sa + spb .*= sb + cross = k.D[m]*(g.a/g.b)^m + @inbounds for i in eachindex(targets) + U[i, 4column - 3]=real(tpa[i]) + U[i, 4column - 2]=imag(tpa[i]) + U[i, 4column - 1]=real(tpb[i]) + U[i, 4column]=imag(tpb[i]) + end + @inbounds for j in eachindex(sources) + a=k.A[m]*spa[j]+cross*spb[j] + b=k.B[m]*spb[j]+cross*spa[j] + V[j, 4column - 3]=real(a) + V[j, 4column - 2]=imag(a) + V[j, 4column - 1]=real(b) + V[j, 4column]=imag(b) + end + end + @views mul!( + matrix, U[:, 1:4length(modes)], transpose(V[:, 1:4length(modes)]), 1.0, 1.0) + end + matrix +end + +function _shunt_kernel_matrix(selected::Formula{:boundary}, targets, sources, g, k; + kwargs...) + buffers = initialize_buffers(selected, Val(:kernel), Float64, nothing, + (rows = length(targets), columns = length(sources), modes = length(k.A), + quadrature = 0), (;)) + return _shunt_kernel_matrix!(Matrix{Float64}(undef, length(targets), length(sources)), + targets, sources, g, k; buffers, kwargs...) +end + +_shunt_core_voltage(z, g) = 1-(g.Rleft+log(abs(z)/g.a)/g.epsilon)/g.Rtotal + +# Round wires retain their existing auxiliary-source treatment. The tape has +# no source contour or inset: its physical faces are integrated below. +function _shunt_points(g, nw, fraction; shift = 0.0) + targets, sources = ComplexF64[], ComplexF64[] + for wire in g.wires + center = complex(wire.x, wire.y) + for j in 0:(nw - 1) + push!(targets, center + wire.r*cis(2pi*(j+shift)/nw)) + push!(sources, center + fraction*wire.r*cis(2pi*j/nw)) + end + end + (; targets, sources) +end + +function _shunt_jacobi!(values, t, alpha, beta) + p = length(values)-1 + values[1] = 1 + p == 0 && return values + values[2] = ((alpha+beta+2)*t+alpha-beta)/2 + s = alpha+beta + for n in 1:(p - 1) + A = (2n+s+1)*(2n+s+2)/(2(n+1)*(n+s+1)) + B = (alpha^2-beta^2)*(2n+s+1)/(2(n+1)*(n+s+1)*(2n+s)) + C = (n+alpha)*(n+beta)*(2n+s+2)/((n+1)*(n+s+1)*(2n+s)) + values[n + 2] = (A*t+B)*values[n + 1]-C*values[n] + end + values +end + +function _shunt_jacobi(t, alpha, beta, p) + _shunt_jacobi!(Vector{Float64}(undef, p+1), t, alpha, beta) +end + +function _shunt_gauss_jacobi(n, alpha, beta) + alpha > -1 && beta > -1 && n > 1 || error("Invalid Jacobi rule.") + s = alpha+beta + diagonal = [(beta-alpha)/(s+2); + [(beta^2-alpha^2)/((2k+s)*(2k+s+2)) for k in 1:(n - 1)]] + off = [2/(s+2)*sqrt((1+alpha)*(1+beta)/(s+3)); + [2/(2k+s)*sqrt(k*(k+alpha)*(k+beta)*(k+s) / + ((2k+s-1)*(2k+s+1))) for k in 2:(n - 1)]] + decomposition = eigen(SymTridiagonal(diagonal, off)) + # Normalized measure w(t)dt / integral(w). No arclength factor belongs here. + decomposition.values, vec(decomposition.vectors[1, :] .^ 2) +end + +""" +$(TYPEDSIGNATURES) + +Find the leading electrostatic exponent at a right-angle metal corner on a +dielectric interface. The host occupies angle ``\\pi/2`` and the adjacent +dielectric angle ``\\pi``. Continuity of potential and normal displacement, +with constant potential on the two metal faces, gives this specialization: + +```math +\\varepsilon_h\\cos(\\nu\\pi/2)\\sin(\\nu\\pi) ++\\varepsilon_o\\sin(\\nu\\pi/2)\\cos(\\nu\\pi)=0. +``` + +The smallest root ``0<\\nu<1`` gives potential relative to the metal proportional +to ``\\rho^\\nu`` and surface charge density proportional to ``\\rho^{\\nu-1}``, +where ``\\rho`` is distance from the corner \\[m\\]. The admissible branch +obeys Meixner's finite-energy edge condition [Meixner1972](@cite). +Material-dependent metal-dielectric wedge singularities, rather than a +universal homogeneous exponent, are treated by [VanBladel1985](@cite). +For equal permittivities this equation recovers ``\\nu=2/3``. + +# Arguments + +- `epsilon_host`: host relative permittivity \\[dimensionless\\]. +- `epsilon_outer`: adjacent dielectric relative permittivity \\[dimensionless\\]. + +# Returns + +- Leading exponent ``\\nu`` \\[dimensionless\\], found by bisection excluding + the trivial zero root. This is the specified two-dielectric corner, not a + general wedge solver. + +# Errors + +Nonpositive permittivities or an unbracketed root raise `ErrorException`. +""" +function _shunt_junction_exponent(epsilon_host, epsilon_outer) + epsilon_host > 0 && epsilon_outer > 0 || + error("Positive dielectric permittivities required.") + # Host sector pi/2, outer dielectric sector pi. Determinant has no cotangent + # poles. Exclude the trivial nu=0 root and bracket the first positive root. + determinant(nu) = epsilon_host*cospi(nu/2)*sinpi(nu) + + epsilon_outer*sinpi(nu/2)*cospi(nu) + left, right = 1e-10, 1.0 + determinant(left) > 0 && determinant(right) < 0 || + error("internal shunt: dielectric corner root is not bracketed") + for _ in 1:60 + mid = (left+right)/2 + if determinant(mid) > 0 + left = mid + else + right = mid + end + end + (left+right)/2 +end + +function _shunt_face_point(face, t) + face.kind == :arc ? + face.radius*cis(face.phi+face.span*t/2) : + (face.mid+face.half*t)*cis(face.phi) +end + +""" +$(TYPEDSIGNATURES) + +Build whole-face charge expansions on the tape's two circular arcs and two +radial ends, retaining its physical thickness. The finite-face precedent is +[Bernal1997](@cite), Section IV, Eq. (10), which uses Maxwell-weighted +Chebyshev functions. This implementation instead uses material-dependent +Jacobi weights, with exponents from `_shunt_junction_exponent` at dielectric +interfaces and ``\\nu=2/3`` at homogeneous right-angle corners: + +```math +d\\widehat q(t)=\\sum_{n=0}^{p} a_n P_n^{(\\alpha,\\beta)}(t)\\,d\\mu(t), +\\qquad +d\\mu(t)=\\frac{(1-t)^\\alpha(1+t)^\\beta\\,dt} +{2^{\\alpha+\\beta+1}\\mathrm{B}(\\alpha+1,\\beta+1)}, +\\qquad \\alpha=\\nu_+-1,\\quad\\beta=\\nu_--1. +``` + +Here ``t\\in[-1,1]`` is the face parameter, ``\\nu_\\pm`` are its endpoint +exponents, and ``P_n`` are Jacobi polynomials. The normalized charge measure +``d\\widehat q=dq/(2\\pi\\varepsilon_0)`` and coefficients ``a_n`` have units +\\[V\\]. ``dq`` is charge per cable length \\[C/m\\]. Each face's total charge +is ``2\\pi\\varepsilon_0 a_0``. Do not multiply this measure by a second +arclength Jacobian. Encoding known singularities in the approximation space +is also supported by [Classen2011](@cite), Sections 2 and 4. Their FIT/DG +discretizations are not the boundary-integral element implemented here. + +# Arguments + +- `g`: host and layer radii \\[m\\] and relative permittivities \\[dimensionless\\]. +- `s`: tape geometry and terminal index. The inner and outer radii are in + \\[m\\], and the center angle and angular span are in \\[rad\\]. +- `p`: highest Jacobi degree on each face. +- `quadrature`: number of normalized Gauss–Jacobi nodes per face. + +# Returns + +- 4 face records with physical coordinates \\[m\\], endpoint exponents, + quadrature weights and weighted polynomial values. + +# Errors + +Insufficient quadrature order or tape contact with a bounding conductor +raises `ErrorException`. +""" +function _shunt_tape_faces(g, s, p, quadrature) + p >= 0 && quadrature >= max(2, p+1) || error("Quadrature needs at least order+1 nodes.") + at_interface = isapprox(s.ro, g.b; atol = 64eps(Float64)*g.b, rtol = 0) + at_inner = isapprox(s.ri, g.a; atol = 64eps(Float64)*g.b, rtol = 0) + at_inner && isempty(g.left) && + error("internal shunt: tape contacts the inner conductor") + nu_inner = at_inner ? _shunt_junction_exponent(g.epsilon, last(g.left).epsilon) : 2/3 + # A metal face coincident with the reference shield is not an open tape. + at_interface && isempty(g.right) && + error("internal shunt: tape contacts the reference shield") + nu_outer = at_interface ? _shunt_junction_exponent(g.epsilon, first(g.right).epsilon) : + 2/3 + descriptions = [ + (; kind = :arc, radius = s.ri, phi = s.phi, span = s.span, mid = 0.0, half = 0.0, + alpha = nu_inner-1, beta = nu_inner-1), + (; kind = :arc, radius = s.ro, phi = s.phi, span = s.span, mid = 0.0, half = 0.0, + alpha = nu_outer-1, beta = nu_outer-1), + [(; kind = :end, radius = 0.0, phi = s.phi+sign*s.span/2, span = 0.0, + mid = (s.ri+s.ro)/2, half = (s.ro-s.ri)/2, + alpha = nu_outer-1, beta = nu_inner-1) for sign in (-1, 1)]...] + map(descriptions) do face + nodes, weights = _shunt_gauss_jacobi(quadrature, face.alpha, face.beta) + polys = transpose(reduce(hcat, [_shunt_jacobi(t, face.alpha, face.beta, p) + for t in nodes])) + weighted = weights .* polys + loggamma = SpecialFunctions.loggamma + beta_norm = exp(loggamma(face.alpha+1)+loggamma(face.beta+1) - + loggamma(face.alpha+face.beta+2)) + merge(face, + (; p, nodes, weights, weighted, beta_norm, terminal = s.terminal, + points = _shunt_face_point.(Ref(face), nodes))) + end +end + +function _shunt_face_projection(z, face) + if face.kind == :arc + radius = abs(z) + angle_difference = atan(sin(angle(z)-face.phi), cos(angle(z)-face.phi)) + u = 2angle_difference/face.span + delta = abs(radius-face.radius)/(face.radius*face.span/2) + else + rotated = z*cis(-face.phi) + u = (real(rotated)-face.mid)/face.half + delta = abs(imag(rotated))/face.half + end + u, delta +end + +""" +$(TYPEDSIGNATURES) + +Integrate the logarithmic kernel against the normalized face charge modes: + +```math +M_n(z)=\\int_{-1}^{1} P_n^{(\\alpha,\\beta)}(t) +\\log\\!\\left(\\frac{|z-\\zeta(t)|}{1\\,\\mathrm{m}}\\right)d\\mu(t). +``` + +The face position is ``\\zeta(t)`` \\[m\\]. ``d\\mu`` is the normalized measure +in `_shunt_tape_faces`. Ordinary quadrature can lose accuracy close to a +source boundary [HelsingOjala2008](@cite), Section 1. Here direct and image +logarithms are integrated separately from the smooth Green remainder. +Nearby targets use ``t=\\cos\\theta`` and adaptive Gauss–Kronrod integration, +split at the projected singularity or near peak. Distant targets use the +face's Gauss–Jacobi rule. This is not Helsing-Ojala's rational-quadrature +algorithm, and that reference does not prescribe the corner basis. + +# Arguments + +- `z`: target coordinate in the complex cross-sectional plane \\[m\\]. +- `face`: physical face geometry, normalized weights and Jacobi degree. + +# Keywords + +- `rtol=1e-10`: relative adaptive quadrature tolerance. The absolute tolerance + is `0.01rtol` for these dimensionless moments. +- `atol=0.01rtol`: absolute tolerance for the dimensionless moment vector. +- `maxevals=100_000`: adaptive quadrature evaluation budget. +- `result=zeros(face.p+1)`: in-place moment workspace, also returned. +- `segments=nothing`: optional reusable QuadGK segment buffer. + +# Returns + +- Logarithmic moments \\[dimensionless\\]. +- Adaptive error estimate. The far-field fixed rule returns zero because it + supplies no estimate, not because its error is known to vanish. + +# Errors + +An unresolved zero-distance evaluation or failed adaptive quadrature raises +`BoundarySolveError`. The logarithm is not replaced by an arbitrary finite floor. +""" +function _shunt_log_moments(z, face; rtol = 1e-10, atol = 0.01rtol, + maxevals = 100_000, result = zeros(face.p+1), segments = nothing) + u, delta = _shunt_face_projection(z, face) + if hypot(max(abs(u)-1, 0), delta) > 0.2 + fill!(result, 0) + @inbounds for j in eachindex(face.points) + value = log(abs(face.points[j]-z)) + for n in eachindex(result) + result[n] += face.weighted[j, n]*value + end + end + return result, 0.0 + end + # Weighted logarithmic product integration: compute the log moments of the + # Jacobi modes, independently of the smooth Green remainder. t=cos(theta) + # absorbs endpoint weights. Split at the logarithmic singularity or near peak. + theta0 = acos(clamp(u, -1, 1)) + splits = [0.0, theta0, pi] + if delta > 0 + width = min(sqrt(delta), delta/max(sin(theta0), eps(Float64))) + for sign in (-1, 1), factor in (1, 4) + + push!(splits, clamp(theta0+sign*factor*width, 0, pi)) + end + end + sort!(unique!(splits)) + evaluations = Ref(0) + function integrand!(output, theta) + evaluations[] += 1 + t = cos(theta) + difference = abs(u) <= 1 ? + -2sin((theta+theta0)/2)*sin((theta-theta0)/2) : t-u + distance = if face.kind == :arc + # Exact opposed-arc distance, also valid in the self limit. + hypot(abs(z)-face.radius, + 2sqrt(abs(z)*face.radius)*sin(face.span*difference/4)) + else + hypot(face.half*difference, imag(z*cis(-face.phi))) + end + # Roundoff may identify the endpoint only after its contribution is + # below integration accuracy. Never manufacture a finite log(0) floor. + distance > 0 || throw(BoundarySolveError(:quadrature, + (; point = z, theta, face = face.kind), "unresolved logarithmic integration point")) + weight = sin(theta/2)^(2face.alpha+1)*cos(theta/2)^(2face.beta+1)/face.beta_norm + _shunt_jacobi!(output, t, face.alpha, face.beta) + output .*= log(distance)*weight + end + # Reuse quadrature work vectors. Modal integration must not allocate a new + # polynomial vector at each of its thousands of function evaluations. + integral, + estimate = quadgk!(integrand!, result, splits; + segbuf = segments, rtol, atol, order = max(7, cld(face.p+1, 2)), maxevals) + all(isfinite, integral) || throw(BoundarySolveError(:nonfinite, + (; point = z, face = face.kind), "nonfinite logarithmic integral")) + tolerance = max(rtol*norm(integral), atol) + if !(estimate <= tolerance) + @warn "Boundary logarithmic quadrature target was not met; returning the computed integral" context=(; point = z, face = face.kind, radius = face.radius, + phi = face.phi, degree = face.p, + span = face.span, mid = face.mid, half = face.half, alpha = face.alpha, beta = face.beta, + quadrature = length(face.points), + estimate, tolerance, evaluations = evaluations[], maxevals) + end + return integral, estimate +end + +function _shunt_tape_columns!(columns, targets, faces, g, k; log_rtol = 1e-10, + integration = (rtol = log_rtol, atol = 0.01log_rtol, maxevals = 100_000), buffers) + estimated_error = 0.0 + offset = 0 + for face in faces + block = @view columns[:, (offset + 1):(offset + face.p + 1)] + kernel = @view buffers.tape[1:length(targets), 1:length(face.points)] + _shunt_kernel_matrix!( + kernel, targets, face.points, g, k; split_images = true, buffers) + mul!(block, kernel, face.weighted) + result = zeros(face.p+1) + segments = alloc_segbuf(Float64, Vector{Float64}, Float64; size = 32) + for (i, z) in pairs(targets) + inner, outer = g.a^2/conj(z), g.b^2/conj(z) + direct_coefficient = 1.0 + inner_same = abs(inner-z) <= 8eps(Float64)*g.b + outer_same = abs(outer-z) <= 8eps(Float64)*g.b + direct_coefficient += (inner_same ? k.ra0 : 0.0) + (outer_same ? k.rb0 : 0.0) + for (point, coefficient) in ((inner, inner_same ? 0.0 : k.ra0), + (outer, outer_same ? 0.0 : k.rb0), (z, direct_coefficient)) + coefficient == 0 && continue + moments, + err = _shunt_log_moments(point, face; integration..., result, segments) + @inbounds for n in eachindex(moments) + block[i, n] -= (coefficient/g.epsilon)*moments[n] + end + estimated_error = max(estimated_error, abs(coefficient/g.epsilon)*err) + end + end + offset += face.p+1 + end + (; columns, log_moment_error = estimated_error) +end + +function _shunt_tape_columns(selected::Formula{:boundary}, targets, faces, g, k; kwargs...) + nodes = maximum(face -> length(face.points), faces; init = 0) + buffers = initialize_buffers(selected, Val(:kernel), Float64, nothing, + (rows = length(targets), columns = nodes, modes = length(k.A), quadrature = nodes), + (;)) + columns = Matrix{Float64}(undef, length(targets), sum(f.p+1 for f in faces; init = 0)) + return _shunt_tape_columns!(columns, targets, faces, g, k; buffers, kwargs...) +end + +function _shunt_face_targets(faces, n; validation = false) + parameters = -cos.(pi .* ((1:n) .- 0.5) ./ n) + if validation + # Independent uniform targets plus progressively closer corner targets. + parameters = sort!(unique!([parameters; collect(range(-1, 1; length = n+1)); + [-1+10.0^-k for k in 2:10]; [1-10.0^-k for k in 2:10]])) + end + [_shunt_face_point(face, t) for face in faces for t in parameters] +end + +"Maximum dense collocation storage per local solve [bytes]." +const INTERNAL_SHUNT_MATRIX_BYTES = 384 * 1024^2 + +const ShuntTapeFace = NamedTuple{ + (:kind, :radius, :phi, :span, :mid, :half, :alpha, :beta, :p, :nodes, :weights, + :weighted, :beta_norm, :terminal, :points), + Tuple{Symbol, Float64, Float64, Float64, Float64, Float64, Float64, Float64, + Int, Vector{Float64}, Vector{Float64}, Matrix{Float64}, + Float64, Int, Vector{ComplexF64}}} + +const InternalShuntDiagnostic = NamedTuple{ + (:boundary_residual, :wire_residual, :tape_residual, :common_residual, + :penetration_indicator, :reciprocity, :log_moment_error, :unknowns, + :equations, :matrix_bytes, :level), + Tuple{Union{Nothing, Float64}, Union{Nothing, Float64}, Union{Nothing, Float64}, + Union{Nothing, Float64}, Union{Nothing, Float64}, Float64, Float64, + Int, Int, Int, typeof(DEFAULT_RESOLUTION)}} + +function _shunt_values(domain::InternalShuntDomain{T}, methods) where {T} + # The admitted lossless laws depend only on relative permittivity. Resistivity + # temperature corrections do not belong to these blueprint coefficients. + reference_frequency = one(T) + epsilon(material) = begin + law = material.kind === :semicon ? methods.semicon_admittance : + methods.insulation_admittance + kappa = law(material, reference_frequency, material.T0) + imag(kappa)/(2pi*reference_frequency*vacuum_permittivity(T)) + end + values = T[domain.a, domain.b, epsilon(domain.material)] + for layers in (domain.left, domain.right), layer in layers + + append!(values, (layer.ri, layer.ro, epsilon(layer.material))) + end + for wire in domain.wires + append!(values, (wire.x, wire.y, wire.r)) + end + for tape in domain.tapes + append!(values, (tape.ri, tape.ro, tape.phi, tape.span)) + end + return values +end + +# The flat values vector is also the differentiation input. It keeps +# correlated scalar graphs out of dense factorizations without discarding them. +function _shunt_data(values::AbstractVector{T}, domain) where {T} + a, b, epsilon = values[1:3] + Layer = NamedTuple{(:ri, :ro, :epsilon), Tuple{T, T, T}} + left, right = Layer[], Layer[] + cursor = 4 + for (destination, layers) in ((left, domain.left), (right, domain.right)) + for _ in layers + push!(destination, ( + ri = values[cursor], ro = values[cursor + 1], epsilon = values[cursor + 2])) + cursor += 3 + end + end + wires, tapes = ShuntWire{T}[], ShuntTape{T}[] + for wire in domain.wires + push!(wires, + (x = values[cursor], y = values[cursor + 1], + r = values[cursor + 2], terminal = wire.terminal)) + cursor += 3 + end + for tape in domain.tapes + push!(tapes, + (ri = values[cursor], ro = values[cursor + 1], phi = values[cursor + 2], + span = values[cursor + 3], terminal = tape.terminal)) + cursor += 4 + end + Rleft = sum(l->log(l.ro/l.ri)/l.epsilon, left; init = zero(T)) + Rright = sum(l->log(l.ro/l.ri)/l.epsilon, right; init = zero(T)) + return (; a, b, epsilon, left, right, wires, tapes, Rleft, Rright, + Rtotal = Rleft+log(b/a)/epsilon+Rright, ports = length(domain.terminals)-1) +end + +function _shunt_discretization(g, level; validation = false) + faces = ShuntTapeFace[] + for tape in g.tapes + append!(faces, _shunt_tape_faces(g, tape, level.order, level.quadrature)) + end + count = validation ? 3level.wire : 2level.wire + points = _shunt_points(g, count, 0.75; shift = validation ? 0.37 : 0.0).targets + terminals = Int[w.terminal for w in g.wires for _ in 1:count] + nface = validation ? max(41, 12(level.order+1)) : max(24, 4(level.order+1)) + for face in faces + targets = _shunt_face_targets([face], nface; validation) + append!(points, targets) + append!(terminals, fill(face.terminal, length(targets))) + end + return (; faces, points, terminals, wire_targets = count*length(g.wires)) +end + +function _shunt_rhs!(rhs, points, terminals, g) + fill!(rhs, 0) + @inbounds for i in eachindex(points) + rhs[i, 1] = -_shunt_core_voltage(points[i], g) + rhs[i, terminals[i]] = 1 + end + return rhs +end + +function _shunt_matrix!(matrix, points, sources, faces, g, k; + integration = DEFAULT_INTEGRATION, buffers, stage = :assembly) + count = length(sources) + try + _shunt_kernel_matrix!(@view(matrix[:, 1:count]), points, sources, g, k; buffers) + tape = _shunt_tape_columns!( + @view(matrix[:, (count + 1):end]), points, faces, g, k; integration, buffers) + return tape.log_moment_error + catch exception + exception isa BoundarySolveError || rethrow() + throw(BoundarySolveError(exception.category, merge(exception.context, (; stage)), exception.message)) + end +end + +function _shunt_charge_map(g, sources, faces, level) + count = length(sources)+sum(f->f.p+1, faces; init = 0) + charge = zeros(g.ports, count) + @inbounds for (index, wire) in pairs(g.wires) + for j in ((index - 1) * level.wire + 1):(index * level.wire) + charge[1, j] = -_shunt_core_voltage(sources[j], g) + charge[wire.terminal, j] = 1 + end + end + offset = length(sources) + for face in faces + charge[face.terminal, offset + 1] = 1 + if face.kind === :arc + charge[1, offset + 1] = -_shunt_core_voltage(first(face.points), g) + else + @views mul!(charge[1:1, (offset + 1):(offset + face.p + 1)], + transpose(_shunt_core_voltage.(face.points, Ref(g))), face.weighted, -1.0, 0.0) + end + offset += face.p+1 + end + return charge +end + +""" +$(TYPEDSIGNATURES) + +Compute the lossless terminal capacitance of exposed wires and finite tapes +inside a layered circular reference shield. Round-wire auxiliary sources and +corner-weighted integrated tape charges share a column-equilibrated least- +squares solve. Charge is extracted by exact total-charge and core-potential +moments, not by symmetrizing the answer. + +# Arguments + +- `selected`: the boundary shunt formula, which builds the kernel buffers. +- `g`: local radii and positions \\[m\\] and relative dielectric permittivities. + +# Keywords + +- `level`: fixed numerical resolution. The reference settings are not an + accuracy guarantee for arbitrary geometry or weak terminal couplings. +- `retain=false`: retain the factorization only for local differentiation. +- `integration`: dimensionless logarithmic-moment quadrature controls. +- `audit=false`: evaluate independent boundary-grid residuals. When disabled, + those diagnostic fields are `nothing`. The terminal solve is unchanged. + +# Returns + +- Local capacitance `C` \\[F/m\\], sampled numerical diagnostics, and optional + differentiation state. Sampled residuals are not certified error bounds. + +# Notes + +The layered Green function and whole-face charge approach has precedent in +[Bernal1997](@cite), but this solve uses oversampled boundary collocation and +pivoted QR, not that paper's Galerkin system. The circular kernel, wire-source +placement at `0.75` times each wire radius, Jacobi adaptation, resolution and +acceptance checks are implementation choices. An accuracy bound for this combined discretization +is not established by the cited methods. + +# Errors + +Memory-budget failures, degenerate charge columns and nonfinite capacitance are +`BoundarySolveError`, not geometry `DomainError`. Monte Carlo must not condition +its samples on them. Estimated rank, reciprocity, passivity and sampled geometric boundary +quality are reported as warnings without substituting a different calculation. +""" +Base.@constprop :aggressive function _shunt_capacitance(selected::Formula{:boundary}, g; + level = DEFAULT_RESOLUTION, retain = false, integration = DEFAULT_INTEGRATION, + audit = false) + return _shunt_capacitance(selected, g, Val(retain); level, integration, audit) +end + +function _shunt_capacitance(selected::Formula{:boundary}, g, ::Val{retain}; + level = DEFAULT_RESOLUTION, + integration = DEFAULT_INTEGRATION, audit = false) where {retain} + estimated_unknowns = length(g.wires)*level.wire + 4length(g.tapes)*(level.order+1) + estimated_rows = 2length(g.wires)*level.wire + + 4length(g.tapes)*max(24, 4(level.order+1)) + estimated_unknowns > 0 && estimated_rows >= estimated_unknowns || + error("internal shunt: insufficient boundary equations") + estimated_unknowns <= div(INTERNAL_SHUNT_MATRIX_BYTES, 8estimated_rows) || throw( + BoundarySolveError(:budget, (; estimated_unknowns, estimated_rows), + "requested resolution exceeds the dense storage budget")) + discretization = _shunt_discretization(g, level) + (; faces, points, terminals) = discretization + sources = _shunt_points(g, level.wire, 0.75).sources + n = length(sources)+sum(f->f.p+1, faces; init = 0) + m = length(points) + n > 0 && m >= n || error("internal shunt: insufficient boundary equations") + n <= div(INTERNAL_SHUNT_MATRIX_BYTES, 8m) || + throw(BoundarySolveError(:budget, (; rows = m, columns = n), + "boundary matrix exceeds the $(INTERNAL_SHUNT_MATRIX_BYTES÷1024^2) MiB storage budget")) + k = _shunt_kernel_coefficients(g, level.modes) + matrix = Matrix{Float64}(undef, m, n) + buffers = initialize_buffers(selected, Val(:kernel), Float64, nothing, + (rows = max(m, 192), columns = max(length(sources), level.quadrature), + modes = level.modes, quadrature = isempty(faces) ? 0 : level.quadrature), (;)) + moment_error = _shunt_matrix!( + matrix, points, sources, faces, g, k; integration, buffers) + rhs = _shunt_rhs!(zeros(m, g.ports), points, terminals, g) + scales = Vector{Float64}(undef, n) + @inbounds for j in 1:n + scales[j] = norm(@view matrix[:, j]) + isfinite(scales[j]) && scales[j] > 0 || throw(BoundarySolveError( + :rank, (; column = j), "degenerate charge column")) + @views matrix[:, j] ./= scales[j] + end + # In-place pivoted QR avoids retaining K, scaled K, U and V simultaneously. + factor = qr!(matrix, ColumnNorm()) + diagonal = [abs(factor.factors[i, i]) for i in 1:n] + cutoff = max(m, n)*eps(Float64)*maximum(diagonal) + estimated_rank = count(>(cutoff), diagonal) + estimated_rank == n || @warn "Boundary charge basis has unresolved numerical rank; retaining the selected QR solve" estimated_rank columns=n cutoff + scaled_coefficients = factor\rhs + coefficients = scaled_coefficients ./ scales + charge = _shunt_charge_map(g, sources, faces, level) + C = (2pi*vacuum_permittivity(Float64)) .* (charge*coefficients) + C[1, 1] += 2pi*vacuum_permittivity(Float64)/g.Rtotal + all(isfinite, C) || + throw(BoundarySolveError(:nonfinite, (;), "nonfinite terminal capacitance")) + audit_result = audit ? + _shunt_audit( + g, level, sources, k, coefficients, n, buffers, integration) : nothing + boundary = audit_result === nothing ? nothing : audit_result.boundary + common = audit_result === nothing ? nothing : audit_result.common + wire_residual = audit_result === nothing ? nothing : audit_result.wire_residual + tape_residual = audit_result === nothing ? nothing : audit_result.tape_residual + audit_result === nothing || (moment_error=max(moment_error, audit_result.moment_error)) + reciprocity = norm(C-transpose(C))/norm(C) + reciprocity <= 0.01 || @warn "Boundary terminal reciprocity target was not met" reciprocity tolerance=0.01 + minimum_eigenvalue = minimum(eigvals!(copy((C+transpose(C))/2))) + minimum_eigenvalue > 0 || @warn "Boundary capacitance has a nonpositive symmetric-part eigenvalue" minimum_eigenvalue + leakage = abs(sum(@view C[1, :])) + coupling = sum(abs, @view C[1, 2:end]) + indicator = common === nothing ? nothing : + iszero(leakage) ? Inf : common*coupling/leakage + diagnostic = InternalShuntDiagnostic((boundary, wire_residual, tape_residual, + common, indicator, reciprocity, moment_error, n, m, 8m*n, level)) + state = if retain + residual = factor.Q' * rhs + residual[1:n, :] .= 0 + residual = factor.Q * residual + (; factor, scales, scaled_coefficients, coefficients, charge, residual, + points, terminals, sources, faces, k, level, integration) + else + nothing + end + return (; C, diagnostic, state) +end + +function _shunt_audit(g, level, sources, k, coefficients, n, buffers, integration) + checks = _shunt_discretization(g, level; validation = true) + matrix = Matrix{Float64}(undef, min(192, length(checks.points)), n) + errors = zeros(size(matrix, 1), g.ports) + boundary = common = wire_residual = tape_residual = 0.0 + moment_error = 0.0 + for start in 1:192:length(checks.points) + stop = min(start+191, length(checks.points)) + rows = 1:(stop - start + 1) + block = @view matrix[rows, :] + selected = @view checks.points[start:stop] + moment_error = max(moment_error, + _shunt_matrix!(block, selected, sources, checks.faces, g, k; + integration, buffers, stage = :audit)) + residual = @view errors[rows, :] + _shunt_rhs!(residual, selected, @view(checks.terminals[start:stop]), g) + mul!(residual, block, coefficients, 1.0, -1.0) + for i in rows + value = maximum(abs, @view residual[i, :]) + boundary = max(boundary, value) + common = max(common, abs(sum(@view residual[i, :]))) + if start+i-1 <= checks.wire_targets + wire_residual = max(wire_residual, value) + else + tape_residual = max(tape_residual, value) + end + end + end + boundary <= 0.06 || @warn "Sampled boundary residual exceeds 0.06 V per unit excitation" boundary tolerance=0.06 + return (; boundary, common, wire_residual, tape_residual, moment_error) +end + +""" +$(TYPEDSIGNATURES) + +Differentiate the fixed-resolution local least-squares problem in one physical +parameter direction. Centered differences evaluate only kernel and charge-map +derivatives. The nominal pivoted QR is reused. Include the nonzero collocation +residual in the differentiated stationarity equation: + +```math +A^T A\\,dx=A^T(db-dA\\,x)+dA^T(b-Ax). +``` + +Triangular QR solves evaluate this equation without forming normal equations. +Geometric boundary derivatives are streamed in bounded row blocks, not stored as a dense +matrix of uncertain scalars. Input directions retain their original physical +units. The direction parameter and `step` are dimensionless. + +# Returns + +- Directional derivative of terminal capacitance \\[F/m\\]. +""" +function _shunt_tangent(selected::Formula{:boundary}, values, direction, domain, state, step) + plus = _shunt_data(values .+ step .* direction, domain) + minus = _shunt_data(values .- step .* direction, domain) + dp, + dm = _shunt_discretization(plus, state.level), _shunt_discretization(minus, state.level) + sp = _shunt_points(plus, state.level.wire, 0.75).sources + sm = _shunt_points(minus, state.level.wire, 0.75).sources + kp, + km = _shunt_kernel_coefficients(plus, state.level.modes), + _shunt_kernel_coefficients(minus, state.level.modes) + m, n = size(state.factor) + count = min(192, m) + matrix_p, matrix_m = Matrix{Float64}(undef, count, n), Matrix{Float64}(undef, count, n) + bp, bm = zeros(count, plus.ports), zeros(count, plus.ports) + drive, stationarity = zeros(m, plus.ports), zeros(n, plus.ports) + buffers = initialize_buffers(selected, Val(:kernel), Float64, nothing, + (rows = count, columns = max(length(sp), state.level.quadrature), + modes = state.level.modes, + quadrature = isempty(dp.faces) ? 0 : state.level.quadrature), (;)) + for start in 1:192:m + stop = min(start+191, m) + rows = 1:(stop - start + 1) + ap, am = @view(matrix_p[rows, :]), @view(matrix_m[rows, :]) + xp, xm = @view(dp.points[start:stop]), @view(dm.points[start:stop]) + _shunt_matrix!(ap, xp, sp, dp.faces, plus, kp; + integration = state.integration, buffers, stage = :derivative) + _shunt_matrix!(am, xm, sm, dm.faces, minus, km; + integration = state.integration, buffers, stage = :derivative) + ap .-= am + ap ./= 2step + rp, rm = @view(bp[rows, :]), @view(bm[rows, :]) + _shunt_rhs!(rp, xp, @view(dp.terminals[start:stop]), plus) + _shunt_rhs!(rm, xm, @view(dm.terminals[start:stop]), minus) + target = @view drive[start:stop, :] + target .= (rp .- rm) ./ (2step) + mul!(target, ap, state.coefficients, -1.0, 1.0) + mul!(stationarity, transpose(ap), @view(state.residual[start:stop, :]), 1.0, 1.0) + end + stationarity ./= state.scales + delta = state.factor\drive + R = UpperTriangular(@view state.factor.factors[1:n, 1:n]) + correction = R\(transpose(R)\stationarity[state.factor.p, :]) + @inbounds for i in 1:n, j in axes(delta, 2) + + delta[state.factor.p[i], j] += correction[i, j] + end + delta ./= state.scales + charge_delta = (_shunt_charge_map(plus, sp, dp.faces, state.level) - + _shunt_charge_map(minus, sm, dm.faces, state.level)) ./ (2step) + result = (2pi*vacuum_permittivity(Float64)) .* + (charge_delta*state.coefficients + state.charge*delta) + result[1, 1] += 2pi*vacuum_permittivity(Float64)*(inv(plus.Rtotal)-inv(minus.Rtotal))/(2step) + return result +end + +""" +$(TYPEDSIGNATURES) + +Evaluate a local shunt domain from its physical scalar descriptors. +This non-exported extension protocol lets Measurements preserve correlations +without introducing uncertain scalars into dense numerical workspaces. + +# Arguments + +- `values`: host and layer radii, wire coordinates and radii and tape dimensions + \\[m\\], relative permittivities and tape angles \\[rad\\], in the order + supplied during blueprint construction. +- `domain`: extracted local geometry and terminal ownership. + +# Keywords + +- `level`: wire, tape, and Fourier discretization controls. +- `integration`: dimensionless logarithmic-moment quadrature controls. +- `audit=false`: enable independent boundary-grid and derivative step checks. +- `retain=false`: retain the nominal factorization for differentiation tests. +- `directions=nothing`: optional columns of physical input perturbations per + unit dimensionless direction parameter. + +# Returns + +- Capacitance `C` \\[F/m\\], numerical diagnostics and optional retained state. + With `directions`, also return checked implicit capacitance derivatives. +""" +function internal_shunt_response( + selected::Formula{:boundary}, values::AbstractVector{<:Real}, domain; + level = DEFAULT_RESOLUTION, retain = false, directions = nothing, + integration = DEFAULT_INTEGRATION, audit = false) + numerical = Float64.(values) + result = try + _shunt_capacitance(selected, _shunt_data(numerical, domain); level, integration, audit, + retain = retain || directions !== nothing) + catch exception + exception isa BoundarySolveError || rethrow() + throw(BoundarySolveError(exception.category, + merge(exception.context, + (; design = domain.design, terminals = domain.terminals)), + exception.message)) + end + directions === nothing && return result + size(directions, 1) == length(numerical) || throw(DimensionMismatch( + "internal shunt directions must align with physical descriptors")) + derivatives = Matrix{Float64}(undef, size(directions, 2), length(result.C)) + for column in axes(directions, 2) + direction = @view directions[:, column] + relative = maximum(eachindex(direction)) do i + abs(direction[i])/(iszero(numerical[i]) ? abs(numerical[2]) : abs(numerical[i])) + end + iszero(relative) && (derivatives[column, :] .= 0; continue) + step = cbrt(eps(Float64))/relative + fine = _shunt_tangent(selected, numerical, direction, domain, result.state, step/2) + all(isfinite, fine) || throw(BoundarySolveError(:derivative, + (; design = domain.design, terminals = domain.terminals, column), "nonfinite sensitivity")) + if audit + coarse = _shunt_tangent(selected, numerical, direction, domain, result.state, step) + discrepancy = norm(fine-coarse) + tolerance = 0.02max(norm(fine), norm(coarse)) + + 256eps(Float64)*norm(result.C)/step + discrepancy <= tolerance || @warn "Boundary uncertainty sensitivity did not resolve on step refinement" design=domain.design terminals=domain.terminals column discrepancy tolerance + end + derivatives[column, :] .= vec(fine) + end + return (; C = result.C, diagnostic = result.diagnostic, + state = retain ? result.state : nothing, tangents = derivatives) +end diff --git a/src/engine/shuntmodel/geometry.jl b/src/engine/shuntmodel/geometry.jl new file mode 100644 index 000000000..5921fce89 --- /dev/null +++ b/src/engine/shuntmodel/geometry.jl @@ -0,0 +1,268 @@ +# Numerical boundary-domain records owned by the local shunt formulations. +const ShuntWire{T} = NamedTuple{(:x, :y, :r, :terminal), Tuple{T, T, T, Int}} +const ShuntTape{T} = NamedTuple{(:ri, :ro, :phi, :span, :terminal), Tuple{T, T, T, T, Int}} +const ShuntLayer{T} = NamedTuple{(:ri, :ro, :material), Tuple{T, T, Material{T}}} + +""" +$(TYPEDEF) + +Describe an open-conductor domain inside a circular layered shield. The first +terminal is the equivalent inner conductor. The last is the closed reference +shield. Intermediate terminals retain their exposed circular wires and tapes. +Coordinates and radii are in \\[m\\]. Materials are not homogenized. + +$(TYPEDFIELDS) +""" +struct InternalShuntDomain{T <: Real} + "Source design index." + design::Int + "Global conductor range of the containing concentric assembly." + assembly::UnitRange{Int} + "Global terminal range, including inner conductor and reference shield." + terminals::UnitRange{Int} + "Host dielectric inner radius \\[m\\]." + a::T + "Host dielectric outer radius \\[m\\]." + b::T + "Unmodified physical host dielectric." + material::Material{T} + "Ordered layers between inner conductor and host." + left::Vector{ShuntLayer{T}} + "Ordered layers between host and reference shield." + right::Vector{ShuntLayer{T}} + "Exposed circular wires in the host frame." + wires::Vector{ShuntWire{T}} + "Exposed finite-thickness tapes in the host frame." + tapes::Vector{ShuntTape{T}} +end + +_shunt_scalar(x) = float(nominal(x)) +_shunt_tol(x) = 128eps(typeof(_shunt_scalar(x))) * abs(_shunt_scalar(x)) +_shunt_same(a, b, scale) = numerical_magnitude(a - b) <= _shunt_tol(scale) +function _shunt_concentric(shape, x, y, scale) + _shunt_same(shape.at.x, x, scale) && _shunt_same(shape.at.y, y, scale) +end + +# Preserve ordering and dependency information. Nominal values decide ordering. +# Coincident interfaces and centers must also have matching uncertain dependencies. +function _shunt_layers(regions, inner, outer, x, y, ::Type{T}) where {T} + layers = ShuntLayer{T}[] + _shunt_same(inner, outer, outer) && return layers + for placed in regions + shape = placed.primitive + placed.terminal === nothing && shape isa DataModel.Annulus || continue + _shunt_concentric(shape, x, y, outer) || continue + _shunt_scalar(shape.ri - inner) >= -_shunt_tol(outer) && + _shunt_scalar(shape.ro - outer) <= _shunt_tol(outer) || continue + push!(layers, + (ri = T(shape.ri), ro = T(shape.ro), + material = convert(Material{T}, placed.source.material))) + end + sort!(layers; by = layer -> _shunt_scalar(layer.ri)) + cursor = inner + for layer in layers + _shunt_same(cursor, layer.ri, outer) || return nothing + cursor = layer.ro + end + _shunt_same(cursor, outer, outer) || return nothing + return layers +end + +function _shunt_closed(design, index, x, y, scale) + indices = findall(==(index), design.terminal_map) + length(indices) == 1 || return nothing + shape = design.geometry.regions[only(indices)].primitive + shape isa DataModel.Annulus && _shunt_concentric(shape, x, y, scale) || + return nothing + return shape +end + +function _shunt_host_domain( + design, blueprint, host_region, design_index, offset, ::Type{T}) where {T} + host = host_region.primitive.outer + x, y, phi = host.at.x, host.at.y, host.at.φ + scale = host.ro + members = Int[] + for (i, placed) in pairs(design.geometry.regions) + placed.terminal === nothing && continue + shape = placed.primitive + inside = if shape isa DataModel.Disk + radius = hypot(shape.at.x - x, shape.at.y - y) + _shunt_scalar(radius - shape.r - host.ri) >= -_shunt_tol(scale) && + _shunt_scalar(radius + shape.r - host.ro) <= _shunt_tol(scale) + elseif shape isa DataModel.BentStrip + _shunt_concentric(shape, x, y, scale) && + _shunt_scalar(shape.ri - host.ri) >= -_shunt_tol(scale) && + _shunt_scalar(shape.ro - host.ro) <= _shunt_tol(scale) + else + false + end + if inside + inner, outer = if shape isa DataModel.Disk + radius = hypot(shape.at.x-x, shape.at.y-y) + radius-shape.r, radius+shape.r + else + shape.ri, shape.ro + end + # A nominal contact with an independent uncertain displacement + # crosses a dielectric interface, outside a fixed-topology tangent. + for (edge, boundary) in ((inner, host.ri), (outer, host.ro)) + abs(_shunt_scalar(edge-boundary)) <= _shunt_tol(scale) && + !_shunt_same(edge, boundary, scale) && return nothing + end + push!(members, i) + end + end + isempty(members) && return nothing + # A coating, void, or another dielectric island inside the host invalidates + # the homogeneous-host Green function even if the metal itself fits radially. + holes = host_region.primitive.holes + length(holes) == length(members) && all( + index -> any(hole -> isequal(design.geometry.regions[index].primitive, hole), holes), members) || + return nothing + terminals = sort!(unique(design.terminal_map[members])) + first(terminals) > 1 && last(terminals) < length(blueprint.conductors) || return nothing + terminals == collect(first(terminals):last(terminals)) || return nothing + # Never resolve only some pieces of a retained terminal. + all(i -> design.terminal_map[i] ∉ terminals || i in members, + eachindex(design.terminal_map)) || return nothing + first_index, reference = first(terminals) - 1, last(terminals) + 1 + assembly_index = blueprint.conductors[first_index].assembly + assembly = blueprint.assembly_ranges[assembly_index] + reference in assembly || return nothing + inner = blueprint.conductors[first_index] + _shunt_same(inner.position[1], x, scale) && + _shunt_same(inner.position[2], y, scale) || return nothing + # Inner cores keep the existing equivalent-core assumption. Elsewhere an + # actual closed conductor is needed to separate independently solved gaps. + first_index == first(assembly) || + _shunt_closed(design, first_index, x, y, scale) !== nothing || return nothing + shield = _shunt_closed(design, reference, x, y, scale) + shield === nothing && return nothing + left = _shunt_layers(design.geometry.regions, inner.r_ex, host.ri, x, y, T) + right = _shunt_layers(design.geometry.regions, host.ro, shield.ri, x, y, T) + (left === nothing || right === nothing) && return nothing + wires, tapes = ShuntWire{T}[], ShuntTape{T}[] + for index in members + shape = design.geometry.regions[index].primitive + terminal = design.terminal_map[index] - first_index + 1 + if shape isa DataModel.Disk + dx, dy = shape.at.x - x, shape.at.y - y + push!(wires, + (x = T(cos(phi)*dx + sin(phi)*dy), + y = T(-sin(phi)*dx + cos(phi)*dy), r = T(shape.r), terminal)) + else + zero(shape.span) < shape.span < 2pi || return nothing + push!(tapes, + (ri = T(shape.ri), ro = T(shape.ro), + phi = T(shape.at.φ - phi), span = T(shape.span), terminal)) + end + end + # The whole-face element admits exposed faces only. Overlapping/touching + # faces require an exposed-union adapter. Never impose buried geometric boundaries. + for i in eachindex(wires), j in 1:(i - 1) + + l, r = wires[i], wires[j] + gap = hypot(l.x-r.x, l.y-r.y)-l.r-r.r + if _shunt_scalar(gap) <= _shunt_tol(scale) + l.terminal == r.terminal || throw(ArgumentError( + "internal shunt: touching wire terminals in $(design.cable_id)")) + return nothing + end + end + for tape in tapes, wire in wires + + gap = tape.ri - hypot(wire.x, wire.y) - wire.r + _shunt_scalar(gap) >= -_shunt_tol(scale) || return nothing + abs(_shunt_scalar(gap)) <= _shunt_tol(scale) && + !_shunt_same(gap, zero(gap), scale) && return nothing + if abs(_shunt_scalar(gap)) <= _shunt_tol(scale) && wire.terminal != tape.terminal + theta = atan(sin(atan(wire.y, wire.x)-tape.phi), + cos(atan(wire.y, wire.x)-tape.phi)) + abs(_shunt_scalar(theta)) <= _shunt_scalar(tape.span)/2 && + throw(ArgumentError("internal shunt: touching wire and tape terminals in $(design.cable_id)")) + end + end + for i in eachindex(tapes), j in 1:(i - 1) + + l, r = tapes[i], tapes[j] + separation = abs(atan(sin(l.phi-r.phi), cos(l.phi-r.phi))) + radial = _shunt_scalar(max(l.ri, r.ri)-min(l.ro, r.ro)) + angular = _shunt_scalar(separation - (l.span+r.span)/2) + radial > _shunt_tol(scale) || angular > _shunt_tol(scale)/_shunt_scalar(scale) || + return nothing + end + return InternalShuntDomain{T}(design_index, + (first(assembly) + offset):(last(assembly) + offset), + (first_index + offset):(reference + offset), T(host.ri), T(host.ro), + convert(Material{T}, host_region.source.material), left, right, wires, tapes) +end + +""" +$(TYPEDSIGNATURES) + +Extract eligible local shunt domains from completed physical designs. Concentric +dielectric continuity, closed reference shields and exposed faces are checked +before any numerical allocation. Unsupported geometries keep their existing +equivalent-coaxial treatment. No shapes, materials or terminals are changed. + +# Returns + +- Concrete vector of internal domains, with global terminal indices. +""" +function internal_shunt_domains(designs, blueprints::AbstractVector{<:CableBlueprint{T}}) where {T} + domains = InternalShuntDomain{T}[] + offset = 0 + for (design_index, (design, blueprint)) in enumerate(zip(designs, blueprints)) + append!(domains, internal_shunt_domains(design, blueprint, T; design_index, offset)) + offset += length(blueprint) + end + return domains +end + +function internal_shunt_domains( + design::CableDesign, geometry, ::Type{T}; design_index = 1, offset = 0) where {T} + domains = InternalShuntDomain{T}[] + for placed in design.geometry.regions + shape = placed.primitive + placed.terminal === nothing && shape isa DataModel.DifferenceShape && + shape.outer isa DataModel.Annulus || continue + domain = _shunt_host_domain(design, geometry, placed, design_index, offset, T) + domain === nothing && continue + any( + old -> max(first(old.terminals), first(domain.terminals)) < + min(last(old.terminals), last(domain.terminals)), + domains) && continue + push!(domains, domain) + end + return domains +end + +function _shunt_domain_equal(a, b) + length(a.terminals) == length(b.terminals) || return false + for name in (:a, :b) + same_physical_state(getproperty(a, name), getproperty(b, name)) || return false + end + for name in (:left, :right, :wires, :tapes) + l, r = getproperty(a, name), getproperty(b, name) + length(l) == length(r) || return false + if name in (:left, :right) + all(same_physical_state((x.ri, x.ro, x.material.eps_r), ( + y.ri, y.ro, y.material.eps_r)) + for (x, y) in zip(l, r)) || return false + else + all(same_physical_state(x, y) for (x, y) in zip(l, r)) || return false + end + end + return same_physical_state(a.material.eps_r, b.material.eps_r) +end + +_shunt_lossless(::Any) = false +function _shunt_lossless(::Union{InsulationAdmittance.Formula{:lossless}, + SemiconAdmittance.Formula{:lossless}}) + true +end +function _shunt_lossless(methods::NamedTuple) + _shunt_lossless(methods.insulation_admittance) && + _shunt_lossless(methods.semicon_admittance) +end diff --git a/src/engine/shuntmodel/interface.jl b/src/engine/shuntmodel/interface.jl new file mode 100644 index 000000000..24c929852 --- /dev/null +++ b/src/engine/shuntmodel/interface.jl @@ -0,0 +1,130 @@ +""" +$(TYPEDEF) + +Select a local shunt geometry approximation. Material admittivity remains owned +by the insulation and semiconductor constitutive selections. + +$(TYPEDFIELDS) +""" +struct Formula{ID, P <: NamedTuple, O <: FormulationOptions} <: ShuntModelFormulation + "Requested fallback model." + parameters::P + "Boundary discretization, quadrature, and optional audit controls." + options::O +end + +"Reference boundary discretization. Accuracy depends on geometry and the requested terminal quantity." +const DEFAULT_RESOLUTION = (wire = 64, order = 32, quadrature = 256, modes = 1024) +"Default controls for dimensionless logarithmic-moment integration." +const DEFAULT_INTEGRATION = (rtol = 1e-8, atol = 1e-10, maxevals = 100_000) + +"Return the available local shunt model identifiers." +formulas(::Type{<:Formula}) = (:default, :equivalent, :boundary) + +""" +$(TYPEDSIGNATURES) + +Construct a cable-local shunt model. `:default` and `:equivalent` use the equivalent annular +layer of each dielectric interval. `:boundary` resolves eligible lossless open-screen +domains before frequency evaluation. + +# Keywords + +- `parameters=(;)`: geometric boundary `fallback=:error` (default) or explicitly + `:equivalent` after an unsupported geometric boundary assumption or numerical failure. + A finite result's quality warning never triggers this fallback. +- `options=(;)`: geometric boundary `resolution=(wire=64, order=32, quadrature=256, + modes=1024)`, `integration=(rtol=1e-8, atol=1e-10, maxevals=100_000)`, and + `audit=false`, where the audit recomputes an independent boundary grid to check + derivative step refinement. The equivalent model does not accept numerical controls. + +# Returns + +- A concrete shunt model selection. +""" +function Formula{ID}(; parameters::NamedTuple = (;), + options::Union{NamedTuple, FormulationOptions} = FormulationOptions()) where {ID} + options = options isa NamedTuple ? FormulationOptions(options) : options + ID === :equivalent || throw(ArgumentError("unknown shunt model :$ID")) + isempty(parameters) && isempty(options.data) || throw(ArgumentError( + "equivalent shunt models accept no parameters or numerical controls")) + return Formula{ID, typeof(parameters), typeof(options)}(parameters, options) +end + +Formula{:default}(; kwargs...) = Formula{:equivalent}(; kwargs...) + +function Formula{:boundary}(; parameters::NamedTuple = (;), + options::Union{NamedTuple, FormulationOptions} = FormulationOptions()) + options = options isa NamedTuple ? FormulationOptions(options) : options + isempty(setdiff(keys(parameters), (:fallback,))) || throw(ArgumentError( + "boundary shunt parameters accept only fallback")) + fallback = get(parameters, :fallback, :error) + fallback in (:error, :equivalent) || + throw(ArgumentError("boundary fallback must be :error or :equivalent")) + isempty(setdiff(keys(options.data), (:resolution, :integration, :audit))) || + throw(ArgumentError( + "boundary shunt options accept resolution, integration, and audit")) + resolution = get(options.data, :resolution, (;)) + resolution isa NamedTuple && + isempty(setdiff(keys(resolution), keys(DEFAULT_RESOLUTION))) || + throw(ArgumentError("unknown boundary resolution controls")) + resolution = merge(DEFAULT_RESOLUTION, resolution) + all(x -> x isa Integer && !(x isa Bool) && x > 0, values(resolution)) || + throw(ArgumentError("boundary resolution controls must be positive integers")) + resolution.quadrature >= resolution.order+1 || throw(ArgumentError( + "boundary quadrature must contain at least order+1 nodes")) + integration = get(options.data, :integration, (;)) + integration isa NamedTuple && + isempty(setdiff(keys(integration), keys(DEFAULT_INTEGRATION))) || + throw(ArgumentError("unknown boundary integration controls")) + integration = merge(DEFAULT_INTEGRATION, integration) + all(x -> x isa Real && isfinite(x) && x >= 0, (integration.rtol, integration.atol)) && + max(integration.rtol, integration.atol) > 0 || throw(ArgumentError( + "boundary integration requires nonnegative finite tolerances, at least one positive")) + integration.maxevals isa Integer && !(integration.maxevals isa Bool) && + integration.maxevals > 0 || + throw(ArgumentError("boundary maxevals must be a positive integer")) + audit = get(options.data, :audit, false) + audit isa Bool || throw(ArgumentError("boundary audit must be Bool")) + normalized = FormulationOptions(; resolution, + integration = (rtol = Float64(integration.rtol), + atol = Float64(integration.atol), maxevals = Int(integration.maxevals)), + audit) + parameters = (; fallback) + return Formula{:boundary, typeof(parameters), typeof(normalized)}(parameters, normalized) +end + +"""Describe the equivalent annular layer local shunt approximation.""" +function description(::Type{<:Formula{:equivalent}}; compact::Bool = false) + compact ? "equivalent annular layer" : + "Equivalent annular layer shunt geometry" +end +"""Describe the default equivalent annular layer local shunt approximation.""" +function description(::Type{<:Formula{:default}}; compact::Bool = false) + description(Formula{:equivalent}; compact) +end +"""Describe the lossless wire and tape geometric boundary approximation.""" +function description(::Type{<:Formula{:boundary}}; compact::Bool = false) + compact ? "boundary" : + "Lossless wire/tape boundary shunt geometry" +end + +Formula(identifier::Symbol; kwargs...) = Formula(Val(identifier); kwargs...) +Formula(::Val{ID}; kwargs...) where {ID} = Formula{ID}(; kwargs...) +Formula(value::ShuntModelFormulation) = value + +function Formula(value::FormulaDefinition{ID, Order}) where {ID, Order} + Order === :default && value.equivalent_earth === nothing || throw(ArgumentError( + "shunt_model does not accept equivalent-earth reductions or ordering")) + return Formula{ID}(; parameters = value.parameters, options = value.options) +end + +formula_id(::Formula{ID}) where {ID} = ID +formula_id(::Type{<:Formula{ID}}) where {ID} = ID +description(value::Formula; compact::Bool = false) = description(typeof(value); compact) +formulation_options(value::Formula) = value.options +function Base.NamedTuple(value::Formula) + (identifier = formula_id(value), + parameters = value.parameters, options = value.options.data) +end +Base.pairs(::Type{<:Formula}; quantity = nothing) = pairs((;)) diff --git a/src/engine/solver.jl b/src/engine/solver.jl deleted file mode 100644 index 6a029bcec..000000000 --- a/src/engine/solver.jl +++ /dev/null @@ -1,370 +0,0 @@ -function compute!( - problem::LineParametersProblem{T}, - formulation::EMTFormulation, -) where {T <: REALSCALAR} - - lvl = levelfrom(formulation.options.common.verbosity) - sink = - isnothing(formulation.options.logfile) ? - ConsoleLogger(stderr, lvl) : - TeeLogger(ConsoleLogger(stderr, lvl), - FileLogger(formulation.options.logfile, lvl)) - with_logger(TimestampLogger(sink)) do - - @info "Preallocating arrays" - - ws = init_workspace(problem, formulation) - nph, nfreq = ws.n_phases, ws.n_frequencies - - # --- full matrices are built per slice (no 3D alloc) ---------------------- - Zbuf = Matrix{Complex{T}}(undef, nph, nph) # reordered scratch (mutated by merge_bundles!) - Pbuf = Matrix{Complex{T}}(undef, nph, nph) - inv_Pbuf = similar(Pbuf) # buffer to hold inv(Pbuf) - - Ztmp = Matrix{Complex{T}}(undef, nph, nph) # raw slice coming from builders - Ptmp = Matrix{Complex{T}}(undef, nph, nph) - - # --- index plan (constant across k) --------------------------------------- - phase_map = ws.phase_map::Vector{Int} - perm = reorder_indices(phase_map) - map_r = phase_map[perm] # reordered map - - # bundle tails mask (same logic as merge_bundles!, but map-only) - reduced_map = let m = copy(map_r), seen = Set{Int}() - @inbounds for (i, p) in pairs(map_r) - if p > 0 && (p in seen) - m[i]=0 - else - p>0 && push!(seen, p) - end - end - m - end - - # decide what Kron shall smite upon - kron_map = if formulation.options.reduce_bundle - if formulation.options.kron_reduction - reduced_map # kill tails and keep nonzero labels - else - km = copy(reduced_map) # kill only tails; keep phase-0 explicit - @inbounds for i in eachindex(km) - if map_r[i] == 0 - km[i] = -1 - end - end - km - end - else - formulation.options.kron_reduction ? map_r : nothing - end - - nkeep = kron_map === nothing ? nph : count(!=(0), kron_map) - Zout = Array{Complex{T}, 3}(undef, nkeep, nkeep, nfreq) - Yout = Array{Complex{T}, 3}(undef, nkeep, nkeep, nfreq) - Mred = Matrix{Complex{T}}(undef, nkeep, nkeep) # buffer to hold Mred - inv_Mred = similar(Mred) # buffer to hold inv(Mred) - - # tiny gather helper to avoid per-slice allocs - @inline function _reorder_into!(dest::AbstractMatrix{Complex{T}}, - src::AbstractMatrix{Complex{T}}, - perm::AbstractVector{Int}) - n = length(perm) - @inbounds for j in 1:n, i in 1:n - dest[i, j] = src[perm[i], perm[j]] - end - return dest - end - - # apply temperature correction if needed - if formulation.options.temperature_correction - ΔT = ws.temp - T₀ - @. ws.rho_cond *= 1 + ws.alpha_cond * ΔT - end - - # Pre-allocate identities for potential-coefficient matrix inversion. - # P is complex symmetric, not Hermitian, whenever dielectric or earth - # losses are present. LU is therefore required; wrapping P in Hermitian - # changes the matrix that is being solved. - I_nph = Matrix{Complex{T}}(I, nph, nph) # identity for full size - I_nkeep = Matrix{Complex{T}}(I, nkeep, nkeep) # identity for reduced size - - # --- per-frequency pipeline ------------------------------------------------ - @info "Starting line parameters computation" - for k in 1:nfreq - - compute_impedance_matrix!(Ztmp, ws, k, formulation) - compute_admittance_matrix!(Ptmp, ws, k, formulation) - - # 1) reorder - _reorder_into!(Zbuf, Ztmp, perm) - _reorder_into!(Pbuf, Ptmp, perm) - - # 2) bundle reduction (in-place) - if formulation.options.reduce_bundle - merge_bundles!(Zbuf, map_r) - merge_bundles!(Pbuf, map_r) - end - - # 3) kron - if kron_map === nothing - symtrans!(Zbuf) - formulation.options.ideal_transposition || line_transpose!(Zbuf) - @views @inbounds Zout[:, :, k] .= Zbuf - - F = lu!(Pbuf) - ldiv!(inv_Pbuf, F, I_nph) # inv_Pbuf := P^{-1} - # inv_Pbuf = pBuf - inv_Pbuf .*= ws.jω[k] - symtrans!(inv_Pbuf) - formulation.options.ideal_transposition || line_transpose!(inv_Pbuf) - @views @inbounds Yout[:, :, k] .= inv_Pbuf - else - kronify!(Zbuf, kron_map, Mred) - symtrans!(Mred) - formulation.options.ideal_transposition || line_transpose!(Mred) - @views @inbounds Zout[:, :, k] .= Mred - - kronify!(Pbuf, kron_map, Mred) - F = lu!(Mred) - ldiv!(inv_Mred, F, I_nkeep) - # inv_Mred = Mred - inv_Mred .*= ws.jω[k] - symtrans!(inv_Mred) - formulation.options.ideal_transposition && line_transpose!(inv_Mred) - - @views @inbounds Yout[:, :, k] .= inv_Mred - end - end - - if !isnothing(formulation.modal_transform) - # apply modal transformation - _, lp = formulation.modal_transform( - LineParameters(PhaseDomain, Zout, Yout, ws.freq), - ) - else - lp = LineParameters(PhaseDomain, Zout, Yout, ws.freq) - end - - @info "Line parameters computation completed successfully" - return ws, lp - end -end - -@inline function stash!(slice_or_nothing, k::Int, src::AbstractMatrix) - slice_or_nothing === nothing && return nothing - @views copyto!(slice_or_nothing[:, :, k], src) - nothing -end - -# Builds an Nc×Nc earth matrix using the functors f(h, y, ρ[:,k], ε[:,k], μ[:,k], jω) -@inline function compute_earth_return_matrix!( - E::AbstractMatrix{Complex{T}}, - cables::AbstractVector{Int}, - ws, - k::Int, - functor, # formulation.earth_impedance or .earth_admittance -) where {T} - ρ = @view ws.rho_g[:, k] - ε = @view ws.eps_g[:, k] - μ = @view ws.mu_g[:, k] - jω = ws.jω[k] - - Nc = length(cables) - - @inbounds for cj in 1:Nc - i = cables[cj] - for ck in 1:Nc - j = cables[ck] - # y: diagonal blocks use cable outer radius; off-diagonals use center distance - yij = ws.horz_sep[i, j] - hij = @view ws.vert[[i, j]] - E[cj, ck] = - cj == ck ? functor(Val(:self), hij, yij, ρ, ε, μ, jω) : - functor(Val(:mutual), hij, yij, ρ, ε, μ, jω) - end - end - - return nothing -end - - -function compute_impedance_matrix!( - Ztmp::AbstractMatrix{Complex{T}}, - ws, - k::Int, - formulation, -) where {T <: REALSCALAR} - - @inbounds fill!(Ztmp, zero(Complex{T})) - @assert length(ws.r_ins_ext) == ws.n_phases "ws.r_ins_ext length mismatch" - @assert length(ws.mu_ins) == ws.n_phases "ws.mu_ins length mismatch" - - Nc = ws.n_cables - jω = ws.jω[k] - - cons_in_cable, cables = _get_cable_indices(ws) - - # Earth return impedance (Nc×Nc) - Zext = Matrix{Complex{T}}(undef, Nc, Nc) - compute_earth_return_matrix!(Zext, cables, ws, k, formulation.earth_impedance) - stash!(ws.Zg, k, Zext) - - # ws.Zg[:, :, k] .= Zext # store in workspace for later use - - zinfunctor = formulation.internal_impedance - zinsfunctor = formulation.insulation_impedance - - @inbounds for c in 1:Nc - cons = cons_in_cable[c]; - n = length(cons) - - for p ∈ n:-1:1 - i = cons[p] - rin = ws.r_in[i] - rex = ws.r_ext[i] - ρc = ws.rho_cond[i] - μrc = ws.mu_cond[i] - - z_outer = zinfunctor(:outer, rin, rex, ρc, μrc, jω) - z_inner = (p < n) ? zinfunctor(:inner, - ws.r_in[cons[p+1]], - ws.r_ext[cons[p+1]], - ws.rho_cond[cons[p+1]], - ws.mu_cond[cons[p+1]], jω) : zero(z_outer) - z_mutual = zinfunctor(:mutual, rin, rex, ρc, μrc, jω) - - # insulation series - r_ins_ext = ws.r_ins_ext[i] - μr_ins = ws.mu_ins[i] - z_ins = zinsfunctor(rex, r_ins_ext, μr_ins, jω) - - z_loop = z_outer + z_inner + z_ins - - if p > 1 - for a in 1:(p-1), b in 1:(p-1) - Ztmp[cons[a], cons[b]] += (z_loop - 2*z_mutual) - end - for a in 1:(p-1) - Ztmp[cons[p], cons[a]] += (z_loop - z_mutual) - Ztmp[cons[a], cons[p]] += (z_loop - z_mutual) - end - end - Ztmp[cons[p], cons[p]] += z_loop - end - - stash!(ws.Zin, k, Ztmp) - - # self earth-return on intra-cable block - zgself = Zext[c, c] - for a in 1:n, b in 1:n - Ztmp[cons[a], cons[b]] += zgself - end - end - - # mutual earth-return off-blocks - @inbounds for cj in 1:(Nc-1) - cons_j = cons_in_cable[cj]; - nj = length(cons_j) - for ck in (cj+1):Nc - zgmut = Zext[cj, ck] - cons_k = cons_in_cable[ck]; - nk = length(cons_k) - for a in 1:nj, b in 1:nk - Ztmp[cons_j[a], cons_k[b]] += zgmut - Ztmp[cons_k[b], cons_j[a]] += zgmut - end - end - end - - stash!(ws.Z, k, Ztmp) - return nothing -end - -function compute_admittance_matrix!( - Ptmp::AbstractMatrix{Complex{T}}, - ws, - k::Int, - formulation, -) where {T <: REALSCALAR} - - # Earth return (Nc×Nc) - @inbounds fill!(Ptmp, zero(Complex{T})) - @assert length(ws.r_ins_ext) == ws.n_phases "ws.r_ins_ext length mismatch" - @assert length(ws.mu_ins) == ws.n_phases "ws.mu_ins length mismatch" - - Nc = ws.n_cables - jω = ws.jω[k] - - cons_in_cable, cables = _get_cable_indices(ws) - - # Earth return admittance (Nc×Nc) - Pext = Matrix{Complex{T}}(undef, Nc, Nc) - compute_earth_return_matrix!(Pext, cables, ws, k, formulation.earth_admittance) - ws.Pg[:, :, k] .= Pext # store in workspace for later use - - # --- internal Maxwell coefficients (Ametani tail-sum) ------------------------- - pinsfunctor = formulation.insulation_admittance - @inbounds for c in 1:Nc - cons = cons_in_cable[c] - n = length(cons) - if n <= 1 - continue - end - - # gap coefficients p_g for gaps g = 1..n-1 (between cons[g] and cons[g+1]) - p = Vector{Complex{T}}(undef, n-1) - @inbounds for g in 1:(n-1) - i = cons[g] - p[g] = InsulationAdmittance.potential_coefficient( - pinsfunctor, - ws, - i, - jω, - ) - end - - # tail sums S[k] = sum_{g=k}^{n-1} p_g, with S[n] = 0 - S = Vector{Complex{T}}(undef, n) - S[n] = zero(Complex{T}) - @inbounds for k in (n-1):-1:1 - S[k] = p[k] + S[k+1] - end - - # P_in[a,b] = S[max(a,b)] - @inbounds for a in 1:n - ia = cons[a] - for b in 1:n - Ptmp[ia, cons[b]] += S[max(a, b)] - end - end - end - stash!(ws.Pin, k, Ptmp) - - # stamp earth terms - @inbounds for c in 1:Nc - cons = cons_in_cable[c]; - n = length(cons) - pgself = Pext[c, c] - for a in 1:n, b in 1:n - Ptmp[cons[a], cons[b]] += pgself - end - end - - @inbounds for cj in 1:(Nc-1) - cons_j = cons_in_cable[cj]; - nj = length(cons_j) - for ck in (cj+1):Nc - pgmut = Pext[cj, ck] - cons_k = cons_in_cable[ck]; - nk = length(cons_k) - for a in 1:nj, b in 1:nk - Ptmp[cons_j[a], cons_k[b]] += pgmut - Ptmp[cons_k[b], cons_j[a]] += pgmut - end - end - end - - stash!(ws.P, k, Ptmp) - - return nothing -end diff --git a/src/engine/specialfunctions.jl b/src/engine/specialfunctions.jl new file mode 100644 index 000000000..2dd4e625b --- /dev/null +++ b/src/engine/specialfunctions.jl @@ -0,0 +1,87 @@ +@inline special_besselix(order::Integer, value) = SpecialFunctions.besselix(order, value) +@inline special_besselkx(order::Integer, value) = SpecialFunctions.besselkx(order, value) +@inline special_besselk(order::Integer, value) = SpecialFunctions.besselk(order, value) +@inline special_besseljx(order::Integer, value) = SpecialFunctions.besseljx(order, value) +@inline special_besselix(order::Integer, + value::Complex{Float32}) = ComplexF32(SpecialFunctions.besselix(Float32(order), value)) +@inline special_besselkx(order::Integer, + value::Complex{Float32}) = ComplexF32(SpecialFunctions.besselkx(Float32(order), value)) +@inline special_besselk(order::Integer, + value::Complex{Float32}) = ComplexF32(SpecialFunctions.besselk(Float32(order), value)) +@inline special_besseljx(order::Integer, + value::Complex{Float32}) = ComplexF32(SpecialFunctions.besseljx(Float32(order), value)) +function special_besseljx(order::Integer, value::Complex{BigFloat}) + order>=0 || throw(DomainError(order, "Bessel order must be nonnegative")) + abs(value)>max(128, precision(BigFloat), order^2) || + return (-im)^order*special_besselix(order, im*value) + real(value)<0 && return (-1)^order*special_besseljx(order, -value) + # DLMF 10.17.1, 10.17.5–6: combine the two scaled Hankel expansions. + # This avoids an angular grid proportional to a large complex argument. + plus=one(value) + minus=one(value) + tp=one(value) + tm=one(value) + for n in 1:100_000 + factor=(4BigFloat(order)^2-BigFloat(2n-1)^2)/(8n*value) + tp*=im*factor + tm*=-im*factor + plus+=tp + minus+=tm + max(abs(tp), abs(tm))<=eps(BigFloat)*max(abs(plus), abs(minus)) && break + n==100_000 && + throw(ErrorException("scaled BigFloat Bessel asymptotic did not converge")) + end + phase=cis(BigFloat(π)*(BigFloat(order)/2+BigFloat(1)/4)) + growth=abs(imag(value)) + return (exp(im*value-growth)*conj(phase)*plus + + exp(-im*value-growth)*phase*minus)/sqrt(2BigFloat(π)*value) +end + +# SpecialFunctions omits complex BigFloat Bessel functions. The local methods +# retain the caller's working precision for unsupported argument types. +function special_besselix(order::Integer, value::Complex{BigFloat}) + order >= 0 || throw(DomainError(order, "Bessel order must be nonnegative")) + abs(value)>max(128, precision(BigFloat), order^2) && + return (-im)^order*special_besseljx(order, im*value) + if abs(value)>8 + # DLMF 10.32.3, with the scaling inside the exponential. Seed the + # angular phase scale rather than relying on two aliased rules. + count=max(8, ceil(Int, abs(imag(value))+order)) + points=collect(range(zero(BigFloat), BigFloat(π); length = count+1)) + f=θ->exp(value*cos(θ)-abs(real(value)))*cos(order*θ) + result, _=quadgk(f, points; rtol = sqrt(eps(BigFloat))) + return result/BigFloat(π) + end + half = value / BigFloat(2) + term = half^order / BigFloat(factorial(big(order))) + result = term + for index in 1:100_000 + term *= half^2 / (BigFloat(index) * BigFloat(index + order)) + next = result + term + if next == result || abs(term) <= eps(BigFloat) * max(abs(next), one(BigFloat)) + return exp(-abs(real(value))) * next + end + result = next + end + throw(ErrorException("complex BigFloat besseli series did not converge")) +end + +function special_besselk(order::Integer, value::Complex{BigFloat}) + return exp(-value)*special_besselkx(order, value) +end + +function special_besselkx(order::Integer, value::Complex{BigFloat}) + order>=0 || throw(DomainError(order, "Bessel order must be nonnegative")) + !iszero(value)&&real(value)>=0 || throw(DomainError(value, + "complex BigFloat K requires a nonzero argument on the outgoing right half-plane")) + # DLMF 10.32.8, w=z(t−1), rotate the w contour to the positive real + # axis, then w=u². This remains exponentially decaying in the lossless + # imaginary-argument limit. Exp(-z*cosh(t)) does not. + exponent=BigFloat(order)-BigFloat(1)/2 + f=u->exp(-u*u)*u^(2order)*(1+u*u/(2value))^exponent + feature=min(sqrt(abs(value)), BigFloat(1)/2) + result, + _=quadgk(f, zero(BigFloat), feature, one(BigFloat), BigFloat(Inf); + rtol = sqrt(eps(BigFloat))) + return 2sqrt(BigFloat(π)/(2value))/SpecialFunctions.gamma(BigFloat(order)+BigFloat(1)/2)*result +end diff --git a/src/engine/textdisplay.jl b/src/engine/textdisplay.jl new file mode 100644 index 000000000..c3e383441 --- /dev/null +++ b/src/engine/textdisplay.jl @@ -0,0 +1,333 @@ +_engine_type_name(value) = String(nameof(typeof(value))) +_engine_unit(unit) = replace(Units.label(unit), "." => "·") + +_domain_name(::Type{PhaseDomain}) = "phase domain" +_domain_name(::Type{ModalDomain}) = "modal domain" +_domain_name(::Type{D}) where {D <: LineParamsDomain} = lowercase(String(nameof(D))) + +TextDisplay.@showfields EarthPair "EarthPair" pair -> ( + row = pair.row, + column = pair.column, + heights = TextDisplay.engineering.(pair.heights, Ref(:meter)), + separation = TextDisplay.engineering(pair.separation, :meter), + layers = pair.layers +) + +TextDisplay.@showfields SpectralIntegral "SpectralIntegral" integral -> ( + callable = typeof(integral.f), +) + +TextDisplay.@showfields AirVoltageSpectrum "AirVoltageSpectrum" kernel -> ( + target_interface_distance = TextDisplay.engineering(kernel.geometry.hp, :meter), + source_interface_distance = TextDisplay.engineering(kernel.geometry.hq, :meter), + radius = TextDisplay.engineering(kernel.geometry.radius, :meter) +) + +TextDisplay.@showfields BlueprintConductor "BlueprintConductor" row -> ( + terminal = row.terminal, + assembly = row.assembly, + r_in = TextDisplay.engineering(row.r_in, :meter), + r_ex = TextDisplay.engineering(row.r_ex, :meter), + kind = row.material.kind +) + +TextDisplay.@showfields BlueprintDielectric "BlueprintDielectric" row -> ( + conductor = row.conductor, + r_in = TextDisplay.engineering(row.r_in, :meter), + r_ex = TextDisplay.engineering(row.r_ex, :meter), + kind = row.material.kind +) + +TextDisplay.@showfields CableBlueprint "CableBlueprint" blueprint -> ( + cable_id = blueprint.cable_id, + conductors = length(blueprint.conductors), + dielectrics = length(blueprint.dielectrics), + assemblies = length(blueprint.assembly_ranges), + shunt_model = blueprint.shunt_details.requested, + boundary_blocks = length(blueprint.shunt) +) + +TextDisplay.@showfields LineCableModelsFEMError "LineCableModelsFEMError" error -> ( + category = error.category, + object = error.object_id, + field = error.field, + message = error.message, + run_directory = error.run_directory +) + +function _frequency_span(values) + isempty(values) && return "no points" + count = length(values) + first_value = TextDisplay.engineering(first(values), :hertz) + last_value = TextDisplay.engineering(last(values), :hertz) + return count == 1 ? "1 point · $first_value" : + "$count points · $first_value … $last_value" +end + +TextDisplay.name(::Type{LineCableModelsCoaxial}) = "LineCableModelsCoaxial" +Base.summary(io::IO, ::LineCableModelsCoaxial) = + print(io, "LineCableModels coaxial backend") +Base.show(io::IO, ::LineCableModelsCoaxial) = print(io, "LineCableModelsCoaxial()") +Base.show(io::IO, ::MIME"text/plain", backend::LineCableModelsCoaxial) = + show(io, backend) + +TextDisplay.name(::Type{<:LineCableModelsFEM}) = "LineCableModelsFEM" +Base.summary(io::IO, ::LineCableModelsFEM) = print(io, "LineCableModels FEM backend") +function Base.show(io::IO, backend::LineCableModelsFEM) + print(io, "LineCableModelsFEM(", length(backend.methods), " material laws)") +end +function Base.show(io::IO, ::MIME"text/plain", backend::LineCableModelsFEM) + get(io, :compact, false) && return show(io, backend) + selections = map(backend.methods) do selected + selected === nothing && return nothing + description(selected;compact=true) + end + return TextDisplay.fields( + io, + "LineCableModels FEM backend", + (; selections..., options = backend.options); + multiline = true + ) +end + +TextDisplay.name(::Type{PhaseDomain}) = "Phase domain" +Base.summary(io::IO, ::PhaseDomain) = print(io, "Phase domain") +Base.show(io::IO, ::PhaseDomain) = print(io, "PhaseDomain()") +Base.show(io::IO, ::MIME"text/plain", value::PhaseDomain) = show(io, value) + +TextDisplay.name(::Type{<:ModalDomain}) = "Modal domain" +Base.summary(io::IO, value::ModalDomain) = + print(io, "Modal domain, ", size(value.gamma,1), " modes") +Base.show(io::IO, value::ModalDomain) = + print(io, "ModalDomain(", join(size(value.gamma),'×'), " roots)") +function Base.show(io::IO, ::MIME"text/plain", value::ModalDomain) + get(io,:compact,false) && return show(io,value) + return TextDisplay.tree(io,"Modal domain",( + (label="Tv $(join(size(value.operators.Tv),'×'))",noun="fields"), + (label="Ti $(join(size(value.operators.Ti),'×'))",noun="fields"), + (label="roots $(join(size(value.gamma),'×'))",noun="fields"), + )) +end + +function Base.summary(io::IO, formulation::AbstractFormulation) + print(io, _engine_type_name(formulation), " formulation") +end +function Base.show(io::IO, formulation::AbstractFormulation) + print(io, _engine_type_name(formulation), "()") +end +Base.show(io::IO, ::MIME"text/plain", formulation::AbstractFormulation) = + show(io, formulation) + +TextDisplay.@showfields Union{ + InternalImpedance.Formula, InsulationImpedance.Formula, + EarthImpedance.Formula, InsulationAdmittance.Formula, + SemiconAdmittance.Formula, EarthAdmittance.Formula, PipeImpedance.Formula, + ShuntModel.Formula, +} "Formula" method -> (id = formula_id(method),) + +TextDisplay.name(::Type{<:LineParametersFormulation}) = "LineParametersFormulation" +Base.summary(io::IO, ::LineParametersFormulation) = print(io, "Line-parameters formulation") +function Base.show(io::IO, formulation::LineParametersFormulation) + print(io, "LineParametersFormulation(", length(formulation.methods), " methods)") +end + +TextDisplay.name(::Type{<:CableConstantsFormulation}) = "CableConstantsFormulation" +Base.summary(io::IO, ::CableConstantsFormulation) = + print(io, "Cable-constants formulation") +function Base.show(io::IO, formulation::CableConstantsFormulation) + print(io, "CableConstantsFormulation(", length(formulation.methods), " methods)") +end +function Base.show(io::IO, ::MIME"text/plain", formulation::CableConstantsFormulation) + get(io, :compact, false) && return show(io, formulation) + children = Tuple(( + label = string(key, " ", sprint(show, method; context = :compact => true)), + noun = "methods", + ) for (key, method) in pairs(formulation.methods)) + return TextDisplay.tree(io, "Cable-constants formulation", children; noun = "methods") +end + +TextDisplay.name(::Type{<:CableConstantsProblem}) = "CableConstantsProblem" +Base.summary(io::IO, ::CableConstantsProblem) = print(io, "Cable-constants problem") +function Base.show(io::IO, problem::CableConstantsProblem) + print(io, "CableConstantsProblem(temperature=", problem.temperature, + ", frequency=", problem.frequency, ")") +end +function Base.show(io::IO, ::MIME"text/plain", problem::CableConstantsProblem) + get(io, :compact, false) && return show(io, problem) + return TextDisplay.tree(io, "CableConstantsProblem", ( + (label = "design $(problem.design.cable_id)", noun = "fields"), + (label = "temperature $(TextDisplay.engineering(problem.temperature, :celsius))", noun = "fields"), + (label = "frequency $(TextDisplay.engineering(problem.frequency, :hertz))", noun = "fields"), + )) +end + +TextDisplay.name(::Type{<:CableConstants}) = "CableConstants" +Base.summary(io::IO, constants::CableConstants) = + print(io, "CableConstants, ", length(constants), " assemblies") +function Base.show(io::IO, constants::CableConstants) + print(io, "CableConstants(assemblies=", length(constants), + ", frequency=", constants.frequency, ")") +end +function Base.show(io::IO, ::MIME"text/plain", constants::CableConstants) + get(io, :compact, false) && return show(io, constants) + rows = Tuple(( + label = string( + constants.cores[index], " R=", constants.R[index], + " Ω/m L=", constants.L[index], " H/m C=", constants.C[index], + " F/m G=", constants.G[index], " S/m" + ), + noun = "assemblies", + ) for index in eachindex(constants.cores)) + return TextDisplay.tree(io, + "Cable constants · $(constants.frequency) Hz", rows; noun = "assemblies") +end +function Base.show(io::IO, ::MIME"text/plain", formulation::LineParametersFormulation) + get(io, :compact, false) && return show(io, formulation) + children = Any[( + label = string(key, " ", sprint(show, method; context = :compact => true)), + noun = "methods", + ) for (key, method) in pairs(formulation.methods)] + isempty(formulation.options.data) || push!(children, ( + label = "options $(length(formulation.options.data)) entries", + noun = "methods", + )) + return TextDisplay.tree(io, "Line-parameters formulation", Tuple(children); noun = "methods") +end + +TextDisplay.name(::Type{<:LineParametersProblem}) = "LineParametersProblem" +Base.summary(io::IO, problem::LineParametersProblem) = + print(io, "Line-parameters problem over $(length(problem.frequencies)) frequencies") +function Base.show(io::IO, problem::LineParametersProblem) + print(io, "LineParametersProblem(cables=", ncables(problem.system), + ", frequencies=", length(problem.frequencies), ")") +end +function Base.show(io::IO, ::MIME"text/plain", problem::LineParametersProblem) + get(io, :compact, false) && return show(io, problem) + children = ( + (label = "system $(sprint(show, problem.system; context = :compact => true))", noun = "fields"), + (label = "temperature $(TextDisplay.engineering(problem.temperature, :celsius))", noun = "fields"), + (label = "earth $(sprint(show, problem.earth_props; context = :compact => true))", noun = "fields"), + (label = "f $(_frequency_span(problem.frequencies))", noun = "fields"), + ) + return TextDisplay.tree(io, "LineParametersProblem", children) +end + +function _array_summary(name, value, selector) + return string( + name, "(", join(size(value), '×'), "; unit=", + _engine_unit(_result_unit(value, selector)), ")" + ) +end + +TextDisplay.name(::Type{<:SeriesImpedance}) = "SeriesImpedance" +Base.summary(io::IO, value::SeriesImpedance) = + print(io, "Series impedance, ", join(size(value), '×')) +Base.show(io::IO, value::SeriesImpedance) = + print(io, _array_summary("SeriesImpedance", value, Z)) +function Base.show(io::IO, ::MIME"text/plain", value::SeriesImpedance) + get(io, :compact, false) && return show(io, value) + TextDisplay.tree(io, "Series impedance · $(join(size(value), '×'))", ( + (label = "unit $(_engine_unit(_result_unit(value, Z)))", noun = "fields"), + (label = "values summarized; use observables for extraction", noun = "fields"), + )) +end + +TextDisplay.name(::Type{<:ShuntAdmittance}) = "ShuntAdmittance" +Base.summary(io::IO, value::ShuntAdmittance) = + print(io, "Shunt admittance, ", join(size(value), '×')) +Base.show(io::IO, value::ShuntAdmittance) = + print(io, _array_summary("ShuntAdmittance", value, Y)) +function Base.show(io::IO, ::MIME"text/plain", value::ShuntAdmittance) + get(io, :compact, false) && return show(io, value) + TextDisplay.tree(io, "Shunt admittance · $(join(size(value), '×'))", ( + (label = "unit $(_engine_unit(_result_unit(value, Y)))", noun = "fields"), + (label = "values summarized; use observables for extraction", noun = "fields"), + )) +end + +TextDisplay.name(::Type{<:LineParameters}) = "LineParameters" +function Base.summary(io::IO, parameters::LineParameters) + print(io, "LineParameters, ", nconductors(parameters), '×', nconductors(parameters), + " over ", nfrequencies(parameters), " frequencies") +end +function Base.show(io::IO, parameters::LineParameters) + print(io, "LineParameters(", _domain_name(domain(parameters)), "; ", + nconductors(parameters), '×', nconductors(parameters), '×', + nfrequencies(parameters), ", basis=:", basis(parameters), ")") + has_uncertainty_type(eltype(parameters)) && print(io, " ±") +end +function Base.show(io::IO, ::MIME"text/plain", parameters::LineParameters) + get(io, :compact, false) && return show(io, parameters) + zunit = _engine_unit(_result_unit(parameters, Z)) + yunit = _engine_unit(_result_unit(parameters, Y)) + shape = join(size(parameters.Z), '×') + children = ( + (label = "f $(_frequency_span(parameters.f))", noun = "fields"), + (label = "Z $shape · $zunit", noun = "fields"), + (label = "Y $shape · $yunit", noun = "fields"), + ) + if parameters.domain isa ModalDomain + state=parameters.domain + children=(children..., + (label="Tv/Ti $(join(size(state.operators.Tv),'×'))",noun="fields"), + (label="roots $(join(size(state.gamma),'×'))",noun="fields")) + if haskey(parameters.details.data,:modal) && + haskey(parameters.details.data.modal,:diagnostics) + diagnostics=parameters.details.data.modal.diagnostics + children=(children..., + (label="numerical targets $(length(diagnostics.missed_frequencies)) missed, $(length(diagnostics.fallback_frequencies)) matched fallback",noun="fields")) + end + end + return TextDisplay.tree(io, "LineParameters · $(_domain_name(domain(parameters)))", children) +end + +TextDisplay.name(::Type{<:RMSError}) = "RMSError" +Base.summary(io::IO, error::RMSError) = + print(io, "RMS error, ", join(size(error.absolute), '×')) +function Base.show(io::IO, error::RMSError) + print(io, "RMSError(", join(size(error.absolute), '×'), ")") +end +function Base.show(io::IO, ::MIME"text/plain", error::RMSError) + get(io, :compact, false) && return show(io, error) + return TextDisplay.tree(io, "RMS error · $(join(size(error.absolute), '×'))", ( + (label = "absolute matrix summarized", noun = "fields"), + (label = "relative matrix summarized", noun = "fields"), + )) +end + +TextDisplay.name(::Type{<:LineParametersBenchmark}) = "LineParametersBenchmark" +Base.summary(io::IO, benchmark::LineParametersBenchmark) = + print(io, "Line-parameters benchmark, ", join(size(benchmark.Z.absolute), '×')) +function Base.show(io::IO, benchmark::LineParametersBenchmark) + print(io, "LineParametersBenchmark(", join(size(benchmark.Z.absolute), '×'), + "; basis=:", basis(benchmark), ")") +end +function Base.show(io::IO, ::MIME"text/plain", benchmark::LineParametersBenchmark) + get(io, :compact, false) && return show(io, benchmark) + shape = join(size(benchmark.Z.absolute), '×') + return TextDisplay.tree(io, "Line-parameters benchmark", ( + (label = "Z $shape RMS errors", noun = "fields"), + (label = "Y $shape RMS errors", noun = "fields"), + (label = "basis :$(basis(benchmark))", noun = "fields"), + )) +end + +TextDisplay.name(::Type{<:LineParametersWorkspace}) = "LineParametersWorkspace" +Base.summary(io::IO, workspace::LineParametersWorkspace) = + print(io, "Line-parameters workspace") +function Base.show(io::IO, workspace::LineParametersWorkspace) + input = workspace.input + print(io, "LineParametersWorkspace(phases=", input.n_phases, + ", cables=", input.n_cables, + ", frequencies=", input.n_frequencies, ")") +end +function Base.show(io::IO, ::MIME"text/plain", workspace::LineParametersWorkspace) + get(io, :compact, false) && return show(io, workspace) + input = workspace.input + return TextDisplay.tree(io, "Line-parameters workspace", ( + (label = "phases $(input.n_phases)", noun = "fields"), + (label = "cables $(input.n_cables)", noun = "fields"), + (label = "frequencies $(input.n_frequencies)", noun = "fields"), + (label = "trace $(workspace.trace === nothing ? "disabled" : "enabled")", noun = "fields"), + )) +end diff --git a/src/engine/transforms/Transforms.jl b/src/engine/transforms/Transforms.jl deleted file mode 100644 index f2540a63d..000000000 --- a/src/engine/transforms/Transforms.jl +++ /dev/null @@ -1,44 +0,0 @@ -""" - LineCableModels.Engine.Transforms - -# Dependencies - -$(IMPORTS) - -# Exports - -$(EXPORTS) -""" -module Transforms - -# Export public API -export Fortescue - -# Module-specific dependencies -using ...Commons -import ...Commons: get_description, PhaseDomain, ModalDomain -import ...Utils: symtrans, symtrans!, offdiag_ratio, to_nominal -import ..Engine: - AbstractTransformFormulation, LineParameters, SeriesImpedance, ShuntAdmittance - -# -using Measurements -using LinearAlgebra -# using GenericLinearAlgebra -using NLsolve - - -include("fortescue.jl") -include("eiglevenberg.jl") - -function (F::AbstractTransformFormulation)( - lp::LineParameters{Tc, U, ModalDomain}, -) where {Tc <: COMPLEXSCALAR, U <: REALSCALAR} - throw( - ErrorException( - "Not yet implemented: inverse $(nameof(typeof(F)))( ::LineParameters{<:COMPLEXSCALAR,<:REALSCALAR,ModalDomain} )", - ), - ) -end - -end # module Transforms diff --git a/src/engine/transforms/eiglevenberg.jl b/src/engine/transforms/eiglevenberg.jl deleted file mode 100644 index d20050b14..000000000 --- a/src/engine/transforms/eiglevenberg.jl +++ /dev/null @@ -1,367 +0,0 @@ -struct Levenberg <: AbstractTransformFormulation - tol::BASE_FLOAT -end - -# Convenient ctor -Levenberg(; tol::BASE_FLOAT = BASE_FLOAT(1e-8)) = Levenberg(tol) - -get_description( - ::Levenberg, -) = "Levenberg–Marquardt (frequency-tracked eigen decomposition)" - -""" -$(TYPEDSIGNATURES) - -Apply Levenberg–Marquardt modal decomposition to a frequency-dependent -[`LineParameters`](@ref) object. Returns the (frequency-tracked) modal -transformation matrices and a **modal-domain** `LineParameters` holding the -**modal per-unit-length** impedance/admittance (diagonal per frequency). - -# Arguments - -- `lp`: Phase-domain line parameters (series `Z`, shunt `Y`, and `f`). -- `f::Levenberg`: Functor with solver tolerance. - -# Returns - -- `Ti`: Transformation matrices `T(•)` as a 3-tensor `n×n×nfreq` (columns are modes). -- `LineParameters`: Modal-domain per-unit-length parameters: - - Series impedance `Zm` (diagonal per frequency) \\[Ω/m\\]. - - Shunt admittance `Ym` (diagonal per frequency) \\[S/m\\]. - -""" -function (f::Levenberg)( - lp::LineParameters{Tc, U, PhaseDomain}, -) where {Tc <: COMPLEXSCALAR, U <: REALSCALAR} - n, n2, nfreq = size(lp.Z.values) - n == n2 || throw(DimensionMismatch("Z must be square")) - size(lp.Y.values) == (n, n, nfreq) || throw(DimensionMismatch("Y must be n×n×nfreq")) - - # 1) Deterministic eigen/LM on nominal arrays - Z_nom = to_nominal(lp.Z.values) - Y_nom = to_nominal(lp.Y.values) - f_nom = to_nominal(lp.f) - Ti, _g_nom = _calc_transformation_matrix_LM(n, Z_nom, Y_nom, f_nom; tol = f.tol) - _rot_min_imag!(Ti) - - Zm = similar(lp.Z.values) - Ym = similar(lp.Y.values) - - Tk = zeros(Tc, n, n) - Zk = zeros(Tc, n, n) - Yk = zeros(Tc, n, n) - invT = zeros(Tc, n, n) - - @inbounds for k in 1:nfreq - Tk .= @view Ti[:, :, k] - invT .= inv(Tk) - @views begin # enforce reciprocity - copyto!(Zk, lp.Z.values[:, :, k]); - symtrans!(Zk) - copyto!(Yk, lp.Y.values[:, :, k]); - symtrans!(Yk) - end - # Modal matrices (carry uncertainties) - @views Zm[:, :, k] .= transpose(Tk) * Zk * Tk - @views Ym[:, :, k] .= invT * Yk * transpose(invT) - - fname = String(nameof(typeof(f))) - offdiagZ = offdiag_ratio(Zm[:, :, k]) - if offdiagZ > f.tol - @warn "$fname: transformed Z not diagonal within tolerance, check your results" ratio = - offdiagZ - else - @views Zm[:, :, k] .= Diagonal(diag(Zm[:, :, k])) # enforce exact diagonal - end - offdiagY = offdiag_ratio(Ym[:, :, k]) - if offdiagY > f.tol - @warn "$fname: transformed Y not diagonal within tolerance, check your results" ratio = - offdiagY - else - @views Ym[:, :, k] .= Diagonal(diag(Ym[:, :, k])) # enforce exact diagonal - end - end - # 2) Apply deterministic T to uncertain (or plain) inputs for *physical* outputs - # Zm, Ym, Zc_mod, Yc_mod, Zch, Ych = - # _calc_modal_quantities(Ti, lp.Z.values, lp.Y.values) - # Gdiag = _calc_gamma(Ti, lp.Z.values, lp.Y.values) - - return Ti, LineParameters(ModalDomain, SeriesImpedance(Zm), ShuntAdmittance(Ym), lp.f) - # Keep original return (Ti, modal characteristic) for compatibility, - # but you now also have Zm, Ym, Zch, Ych, Gdiag available for downstream use. - # return Ti, LineParameters(SeriesImpedance(Zc_mod), ShuntAdmittance(Yc_mod), lp.f), - # LineParameters(SeriesImpedance(Zm), ShuntAdmittance(Ym), lp.f), - # LineParameters(SeriesImpedance(Zch), ShuntAdmittance(Ych), lp.f), Gdiag -end - -#= --------------------------------------------------------------------------- -Internals ------------------------------------------------------------------------------=# - -# Propagate γ with uncertainty WITHOUT eigen(): -# γ̂_k = sqrt.( diag( inv(T_k) * (Y_k*Z_k) * T_k ) ) -function _calc_gamma( - Ti::AbstractArray{Tc, 3}, - Z::AbstractArray{Tu, 3}, - Y::AbstractArray{Tu, 3}, -) where {Tc <: Complex, Tu <: COMPLEXSCALAR} - n, n2, nfreq = size(Ti) - n == n2 || throw(DimensionMismatch("Ti must be n×n×nfreq")) - size(Z) == size(Y) == (n, n, nfreq) || throw(DimensionMismatch("Z,Y must be n×n×nfreq")) - - # Element type follows uncertain inputs - Tγ = promote_type(eltype(Z), eltype(Y)) - Gdiag = zeros(Tγ, n, n, nfreq) # store as diagonal matrices for consistency - - Tk = zeros(Tc, n, n) - invT = zeros(Tc, n, n) - - @inbounds for k in 1:nfreq - Tk .= @view Ti[:, :, k] - invT .= inv(Tk) - - S_k = @view(Y[:, :, k]) * @view(Z[:, :, k]) # Complex{Measurement} ok - λdiag = diag(invT * S_k * Tk) - γdiag = sqrt.(λdiag) - @views Gdiag[:, :, k] .= Diagonal(γdiag) - end - return Gdiag -end - -# Frequency-tracked Levenberg–Marquardt eigen solution -function _calc_transformation_matrix_LM( - n::Int, - Z::AbstractArray{T, 3}, - Y::AbstractArray{T, 3}, - f::AbstractVector{U}; - tol::U = LMTOL, -) where {T <: Complex, U <: Real} - - # Constants - ε0 = U(ε₀) # [F/m] - μ0 = U(μ₀) - - nfreq = size(Z, 3) - Ti = zeros(T, n, n, nfreq) - g = zeros(T, n, n, nfreq) # store as diagonalized in n×n×nfreq for convenience - - Zk = zeros(T, n, n) - Yk = zeros(T, n, n) - - # k = 1 → plain eigen-decomposition seed - Zk .= @view Z[:, :, 1] - Yk .= @view Y[:, :, 1] - S = Yk * Zk - E = eigen(S) # S*v = λ*v - Ti[:, :, 1] .= E.vectors - g[:, :, 1] .= Diagonal(sqrt.(E.values)) # γ = sqrt(λ) - - # k ≥ 2 → LM tracking - ord_sq = n^2 - for k in 2:nfreq - Zk .= @view Z[:, :, k] - Yk .= @view Y[:, :, k] - - S = Yk * Zk - - # Normalize as in legacy: (S / norm_val) - I - ω = 2π * f[k] - nrm = -(ω^2) * ε0 * μ0 - S̃ = (S ./ nrm) - I - - # Seed from previous step - Tseed = @view Ti[:, :, k-1] - gseed = @view g[:, :, k-1] - λseed = (diag(gseed) .^ 2 ./ nrm) .- 1 # since S̃*T = T*Λ with Λ = λ̃ = (λ/nrm)-1 - - # Build real-valued unknown vector: [Re(T); Im(T); Re(λ); Im(λ)] - x0 = [ - vec(real(Tseed)); - vec(imag(Tseed)); - real(λseed); - imag(λseed) - ] - - function _residual!( - F::AbstractVector{<:R}, - x::AbstractVector{<:R}, - ) where {R <: Real} - # Unpack - Tr = reshape(@view(x[1:ord_sq]), n, n) - Ti_ = reshape(@view(x[(ord_sq+1):(2*ord_sq)]), n, n) - - λr = @view x[(2*ord_sq+1):(2*ord_sq+n)] - λi = @view x[(2*ord_sq+n+1):(2*ord_sq+2n)] - - Λr = Diagonal(λr) - Λi = Diagonal(λi) - - Sr = real(S̃); - Si = imag(S̃) - - # Residual of S̃*T - T*Λ = 0, split into real/imag - Rr = (Sr*Tr - Si*Ti_) - (Tr*Λr - Ti_*Λi) - Ri = (Sr*Ti_ + Si*Tr) - (Tr*Λi + Ti_*Λr) - - F[1:ord_sq] .= vec(Rr) - F[(ord_sq+1):(2*ord_sq)] .= vec(Ri) - - # Column normalization constraints - # For each column j: ||t_r||^2 - ||t_i||^2 = 1 and t_r ⋅ t_i = 0 - c1 = sum(abs2.(Tr), dims = 1) .- sum(abs2.(Ti_), dims = 1) .- 1 - c2 = sum(Tr .* Ti_, dims = 1) - idx = 2*ord_sq - @inbounds for j in 1:n - F[idx+2j-1] = c1[j] - F[idx+2j] = c2[j] - end - return nothing - end - - sol = nlsolve( - _residual!, - x0; - method = :trust_region, - autodiff = :forward, - xtol = tol, - ftol = tol, - ) - - if !converged(sol) - @warn "LM solver did not converge at k=$k, using seed eigen-decomposition fallback" - E = eigen(S) - Ti[:, :, k] .= E.vectors - g[:, :, k] .= Diagonal(sqrt.(E.values)) - continue - end - - x = sol.zero - Tr = reshape(@view(x[1:ord_sq]), n, n) - Ti_ = reshape(@view(x[(ord_sq+1):(2*ord_sq)]), n, n) - T̂ = Tr .+ im .* Ti_ - - λr = @view x[(2*ord_sq+1):(2*ord_sq+n)] - λi = @view x[(2*ord_sq+n+1):(2*ord_sq+2n)] - λ̃ = λr .+ im .* λi # normalized eigenvalues - - # Undo normalization: λ = (λ̃ + 1) * nrm ; γ = sqrt(λ) - λ = (λ̃ .+ one(eltype(λ̃))) .* nrm - γ = sqrt.(λ) - - Ti[:, :, k] .= T̂ - g[:, :, k] .= Diagonal(γ) - end - - return Ti, g -end - -# In-place rotation to minimize imag part column-wise (per frequency slice) -function _rot_min_imag!(Ti::AbstractArray{T, 3}) where {T <: Complex} - n, n2, nfreq = size(Ti) - n == n2 || throw(DimensionMismatch("Ti must be n×n×nfreq")) - tmp = zeros(T, n, n) - @inbounds for k in 1:nfreq - tmp .= @view Ti[:, :, k] - rot!(tmp) # column-wise rotation in-place - Ti[:, :, k] .= tmp - end - return Ti -end - -# Full modal + characteristic + phase back-projection -# Returns: -# Zm, Ym :: n×n×nfreq (modal-domain series/shunt matrices) -# Zc_mod,Yc_mod :: n×n×nfreq (diagonal: per-mode characteristic) -# Zch, Ych :: n×n×nfreq (phase-domain characteristic back-projected) -function _calc_modal_quantities( - Ti::AbstractArray{Tc, 3}, - Z::AbstractArray{Tu, 3}, - Y::AbstractArray{Tu, 3}, -) where {Tc <: Complex, Tu <: COMPLEXSCALAR} - - n, n2, nfreq = size(Ti) - n == n2 || throw(DimensionMismatch("Ti must be n×n×nfreq")) - size(Z) == size(Y) == (n, n, nfreq) || throw(DimensionMismatch("Z,Y must be n×n×nfreq")) - - Tz = promote_type(eltype(Z), eltype(Y)) # keep uncertainties - Zm = zeros(Tz, n, n, nfreq) - Ym = zeros(Tz, n, n, nfreq) - Zc_mod = zeros(Tz, n, n, nfreq) - Yc_mod = zeros(Tz, n, n, nfreq) - Zch = zeros(Tz, n, n, nfreq) - Ych = zeros(Tz, n, n, nfreq) - - Tk = zeros(Tc, n, n) - Zk = zeros(Tz, n, n) - Yk = zeros(Tz, n, n) - invT = zeros(Tc, n, n) - - @inbounds for k in 1:nfreq - Tk .= @view Ti[:, :, k] - invT .= inv(Tk) - Zk .= @view Z[:, :, k] - Yk .= @view Y[:, :, k] - - # Modal matrices (carry uncertainties) - @views Zm[:, :, k] .= transpose(Tk) * Zk * Tk - @views Ym[:, :, k] .= invT * Yk * transpose(invT) - - # Characteristic per-mode (diagonal) in modal domain - zc = sqrt.(diag(@view Zm[:, :, k])) ./ sqrt.(diag(@view Ym[:, :, k])) - @views Zc_mod[:, :, k] .= Diagonal(zc) - @views Yc_mod[:, :, k] .= Diagonal(inv.(zc)) - - # Phase-domain characteristic back-projection - @views Zch[:, :, k] .= transpose(invT) * Zc_mod[:, :, k] * invT - @views Ych[:, :, k] .= Tk * Yc_mod[:, :, k] * transpose(Tk) - end - return Zm, Ym, Zc_mod, Yc_mod, Zch, Ych -end - -# column rotation to minimize imag parts -function rot!(S::AbstractMatrix{T}) where {T <: COMPLEXSCALAR} - n, m = size(S) - n == m || throw(DimensionMismatch("Input must be square")) - @inbounds for j in 1:n - col = @view S[:, j] - - # optimal angle - num = -2 * sum(real.(col) .* imag.(col)) # real - den = sum(real.(col) .^ 2 .- imag.(col) .^ 2) # real - ang = BASE_FLOAT(0.5) * atan(num, den) # real - - s1 = cis(ang) - s2 = cis(ang + BASE_FLOAT(pi/2)) - - A = col .* s1 - B = col .* s2 - - # all-real quadratic metrics - Ar = real.(A); - Ai = imag.(A) - Br = real.(B); - Bi = imag.(B) - - aaa1 = sum(Ai .^ 2) - bbb1 = sum(Ar .* Ai) - ccc1 = sum(Ar .^ 2) - err1 = aaa1 * cos(ang)^2 + bbb1 * sin(2*ang) + ccc1 * sin(ang)^2 # real - - aaa2 = sum(Bi .^ 2) - bbb2 = sum(Br .* Bi) - ccc2 = sum(Br .^ 2) - err2 = aaa2 * cos(ang)^2 + bbb2 * sin(2*ang) + ccc2 * sin(ang)^2 # real - - col .*= (err1 < err2 ? s1 : s2) - end - return S -end - - -# tiny helper: in-place imag (for metric term; avoids repeated allocations) -@inline function imag!(x::AbstractVector{<:Complex}) - @inbounds for i in eachindex(x) - x[i] = imag(x[i]) - end - return x -end diff --git a/src/engine/transforms/fortescue.jl b/src/engine/transforms/fortescue.jl deleted file mode 100644 index 2d8ecf9f1..000000000 --- a/src/engine/transforms/fortescue.jl +++ /dev/null @@ -1,54 +0,0 @@ -struct Fortescue <: AbstractTransformFormulation - tol::BASE_FLOAT -end -# Convenient constructor with default tolerance -Fortescue(; tol::BASE_FLOAT = BASE_FLOAT(1e-4)) = Fortescue(tol) -get_description(::Fortescue) = "Fortescue (symmetrical components)" - -""" -$(TYPEDSIGNATURES) - -Functor implementation for `Fortescue`. -""" -function (f::Fortescue)( - lp::LineParameters{Tc, U, PhaseDomain}, -) where {Tc <: COMPLEXSCALAR, U <: REALSCALAR} - _, nph, nfreq = size(lp.Z.values) - Tr = typeof(real(zero(Tc))) - Tv = fortescue_F(nph, Tr) # unitary; inverse is F' - Z012 = similar(lp.Z.values) - Y012 = similar(lp.Y.values) - - @inbounds for k in 1:nfreq - Zs = symtrans(lp.Z.values[:, :, k]) # enforce reciprocity - Ys = symtrans(lp.Y.values[:, :, k]) - - Zseq = Tv * Zs * Tv' - Yseq = Tv * Ys * Tv' - - fname = String(nameof(typeof(f))) - offdiagZ = offdiag_ratio(Zseq) - if offdiagZ > f.tol - @warn "$fname: transformed Z not diagonal within tolerance, check your results" ratio = - offdiagZ - end - offdiagY = offdiag_ratio(Yseq) - if offdiagY > f.tol - @warn "$fname: transformed Y not diagonal within tolerance, check your results" ratio = - offdiagY - end - - Z012[:, :, k] = Matrix(Diagonal(diag(Zseq))) - Y012[:, :, k] = Matrix(Diagonal(diag(Yseq))) - end - return Tv, LineParameters(ModalDomain, Z012, Y012, lp.f) -end - -# Unitary N-point DFT (Fortescue) matrix -function fortescue_F(N::Integer, ::Type{T} = BASE_FLOAT) where {T <: REALSCALAR} - N ≥ 1 || throw(ArgumentError("N ≥ 1")) - θ = T(2π) / T(N) - s = one(T) / sqrt(T(N)) - a = cis(θ) - return s .* [a^(k * m) for k in 0:(N-1), m in 0:(N-1)] # F; inverse is F' -end diff --git a/src/engine/types.jl b/src/engine/types.jl deleted file mode 100644 index ec6582dfb..000000000 --- a/src/engine/types.jl +++ /dev/null @@ -1,44 +0,0 @@ -""" -$(TYPEDEF) - -Abstract base type for all problem definitions in the [`LineCableModels.jl`](index.md) computation framework. -""" -abstract type ProblemDefinition end - -# Formulation abstract types -abstract type AbstractFormulationSet end - -abstract type AbstractImpedanceFormulation <: AbstractFormulationSet end -abstract type InternalImpedanceFormulation <: AbstractImpedanceFormulation end -abstract type InsulationImpedanceFormulation <: AbstractImpedanceFormulation end -abstract type EarthImpedanceFormulation <: AbstractImpedanceFormulation end - -abstract type AbstractAdmittanceFormulation <: AbstractFormulationSet end -abstract type InsulationAdmittanceFormulation <: AbstractAdmittanceFormulation end -abstract type EarthAdmittanceFormulation <: AbstractAdmittanceFormulation end - -abstract type AbstractTransformFormulation <: AbstractFormulationSet end - -""" - FormulationSet(...) - -Constructs a specific formulation object based on the provided keyword arguments. -The system will infer the correct formulation type. -""" -FormulationSet(engine::Symbol; kwargs...) = FormulationSet(Val(engine); kwargs...) - - -""" -$(TYPEDEF) - -Abstract type representing different equivalent homogeneous earth models (EHEM). Used in the multi-dispatch implementation of [`_calc_ehem_properties!`](@ref). - -# Currently available formulations - -- [`EnforceLayer`](@ref): Effective parameters defined according to a specific earth layer. -""" -abstract type AbstractEHEMFormulation <: AbstractFormulationSet end - -abstract type AbstractFormulationOptions end - - diff --git a/src/engine/workspace.jl b/src/engine/workspace.jl deleted file mode 100644 index 2e4ecb49c..000000000 --- a/src/engine/workspace.jl +++ /dev/null @@ -1,239 +0,0 @@ -""" -$(TYPEDEF) - -A container for the flattened, type-stable data arrays derived from a -[`LineParametersProblem`](@ref). This struct serves as the primary data source -for all subsequent computational steps. - -# Fields -$(TYPEDFIELDS) -""" -@kwdef struct EMTWorkspace{T <: REALSCALAR} - "Vector of frequency values [Hz]." - freq::Vector{T} - "Vector of complex frequency values cast as `σ + jω` [rad/s]." - jω::Vector{Complex{T}} - "Vector of horizontal positions [m]." - horz::Vector{T} - "Vector of horizontal separations [m]." - horz_sep::Matrix{T} - "Vector of vertical positions [m]." - vert::Vector{T} - "Vector of internal conductor radii [m]." - r_in::Vector{T} - "Vector of external conductor radii [m]." - r_ext::Vector{T} - "Vector of internal insulator radii [m]." - r_ins_in::Vector{T} - "Vector of external insulator radii [m]." - r_ins_ext::Vector{T} - "Vector of conductor resistivities [Ω·m]." - rho_cond::Vector{T} - "Vector of conductor temperature coefficients [1/°C]." - alpha_cond::Vector{T} - "Vector of conductor relative permeabilities." - mu_cond::Vector{T} - "Vector of conductor relative permittivities." - eps_cond::Vector{T} - "Vector of insulator resistivities [Ω·m]." - rho_ins::Vector{T} - "Vector of insulator relative permeabilities." - mu_ins::Vector{T} - "Vector of insulator relative permittivities." - eps_ins::Vector{T} - "Vector of insulator loss tangents." - tan_ins::Vector{T} - "Physical insulation-layer indices for each cable component." - insulator_layer_ranges::Vector{UnitRange{Int}} - "Vector of physical insulation-layer inner radii \\[m\\]." - r_ins_layer_in::Vector{T} - "Vector of physical insulation-layer outer radii \\[m\\]." - r_ins_layer_ext::Vector{T} - "Vector of physical insulation-layer resistivities \\[Ω·m\\]." - rho_ins_layer::Vector{T} - "Vector of physical insulation-layer relative permittivities \\[dimensionless\\]." - eps_ins_layer::Vector{T} - "Vector of phase mapping indices." - phase_map::Vector{Int} - "Vector of cable mapping indices." - cable_map::Vector{Int} - "Effective earth resistivity (layers × freq)." - rho_g::Matrix{T} - "Effective earth permittivity (layers × freq)." - eps_g::Matrix{T} - "Effective earth permeability (layers × freq)." - mu_g::Matrix{T} - "Operating temperature [°C]." - temp::T - "Line length [m]." - line_length::T - "Number of frequency samples." - n_frequencies::Int - "Number of phases in the system." - n_phases::Int - "Number of cables in the system." - n_cables::Int - "Full component-based Z matrix (before bundling/reduction)." - Z::Array{Complex{T}, 3} - "Full component-based P matrix (before bundling/reduction)." - P::Array{Complex{T}, 3} - "Full internal impedance matrix (before bundling/reduction)." - Zin::Array{Complex{T}, 3} - "Full internal potential coefficient matrix (before bundling/reduction)." - Pin::Array{Complex{T}, 3} - "Earth impedance matrix (n_cables x n_cables)." - Zg::Array{Complex{T}, 3} - "Earth potential coefficient matrix (n_cables x n_cables)." - Pg::Array{Complex{T}, 3} -end - - - -""" -$(TYPEDSIGNATURES) - -Initializes and populates the [`EMTWorkspace`](@ref) by normalizing a -[`LineParametersProblem`](@ref) into flat, type-stable arrays. -""" -function init_workspace( - problem::LineParametersProblem{T}, - formulation::EMTFormulation, -) where {T} - - opts = formulation.options - - system = problem.system - n_frequencies = length(problem.frequencies) - n_phases = sum(length(cable.design_data.components) for cable in system.cables) - n_insulator_layers = sum( - length(component.insulator_group.layers) - for cable in system.cables - for component in cable.design_data.components - ) - n_cables = system.num_cables - - # Pre-allocate 1D arrays - freq = Vector{T}(undef, n_frequencies) - jω = Vector{Complex{T}}(undef, n_frequencies) - horz = Vector{T}(undef, n_phases) - horz_sep = Matrix{T}(undef, n_phases, n_phases) - vert = Vector{T}(undef, n_phases) - r_in = Vector{T}(undef, n_phases) - r_ext = Vector{T}(undef, n_phases) - r_ins_in = Vector{T}(undef, n_phases) - r_ins_ext = Vector{T}(undef, n_phases) - rho_cond = Vector{T}(undef, n_phases) - alpha_cond = Vector{T}(undef, n_phases) - mu_cond = Vector{T}(undef, n_phases) - eps_cond = Vector{T}(undef, n_phases) - rho_ins = Vector{T}(undef, n_phases) - mu_ins = Vector{T}(undef, n_phases) - eps_ins = Vector{T}(undef, n_phases) - tan_ins = Vector{T}(undef, n_phases) # Loss tangent for insulator - insulator_layer_ranges = Vector{UnitRange{Int}}(undef, n_phases) - r_ins_layer_in = Vector{T}(undef, n_insulator_layers) - r_ins_layer_ext = Vector{T}(undef, n_insulator_layers) - rho_ins_layer = Vector{T}(undef, n_insulator_layers) - eps_ins_layer = Vector{T}(undef, n_insulator_layers) - phase_map = Vector{Int}(undef, n_phases) - cable_map = Vector{Int}(undef, n_phases) - Z = - opts.store_primitive_matrices ? - zeros(Complex{T}, n_phases, n_phases, n_frequencies) : nothing - P = - opts.store_primitive_matrices ? - zeros(Complex{T}, n_phases, n_phases, n_frequencies) : nothing - Zin = - opts.store_primitive_matrices ? - zeros(Complex{T}, n_phases, n_phases, n_frequencies) : nothing - Pin = - opts.store_primitive_matrices ? - zeros(Complex{T}, n_phases, n_phases, n_frequencies) : nothing - Zg = - opts.store_primitive_matrices ? - zeros(Complex{T}, n_cables, n_cables, n_frequencies) : nothing - Pg = - opts.store_primitive_matrices ? - zeros(Complex{T}, n_cables, n_cables, n_frequencies) : nothing - - # Fill arrays, ensuring type promotion - freq .= problem.frequencies - jω .= 1im * 2π * freq - - idx = 0 - layer_idx = 0 - for (cable_idx, cable) in enumerate(system.cables) - for (comp_idx, component) in enumerate(cable.design_data.components) - idx += 1 - # Geometric properties - horz[idx] = T(cable.horz) - vert[idx] = T(cable.vert) - r_in[idx] = T(component.conductor_group.r_in) - r_ext[idx] = T(component.conductor_group.r_ex) - r_ins_in[idx] = T(component.insulator_group.r_in) - r_ins_ext[idx] = T(component.insulator_group.r_ex) - - # Material properties - rho_cond[idx] = T(component.conductor_props.rho) - alpha_cond[idx] = T(component.conductor_props.alpha) - mu_cond[idx] = T(component.conductor_props.mu_r) - eps_cond[idx] = T(component.conductor_props.eps_r) - rho_ins[idx] = T(component.insulator_props.rho) - mu_ins[idx] = T(component.insulator_props.mu_r) - eps_ins[idx] = T(component.insulator_props.eps_r) - - # Calculate loss factor from resistivity - ω = 2 * π * f₀ # Using default frequency - C_eq = T(component.insulator_group.shunt_capacitance) - G_eq = T(component.insulator_group.shunt_conductance) - tan_ins[idx] = G_eq / (ω * C_eq) - - # Preserve the physical dielectric stack for broadband lossy models. - first_layer_idx = layer_idx + 1 - for layer in component.insulator_group.layers - layer_idx += 1 - r_ins_layer_in[layer_idx] = T(layer.r_in) - r_ins_layer_ext[layer_idx] = T(layer.r_ex) - rho_ins_layer[layer_idx] = T(layer.material_props.rho) - eps_ins_layer[layer_idx] = T(layer.material_props.eps_r) - end - insulator_layer_ranges[idx] = first_layer_idx:layer_idx - - # Mapping - phase_map[idx] = cable.conn[comp_idx] - cable_map[idx] = cable_idx - end - end - - # Precompute Euclidean distances, use max radius for self-distances - _calc_horz_sep!(horz_sep, horz, r_ext, r_ins_ext, cable_map) - - (rho_g, eps_g, mu_g) = _get_earth_data( - formulation.equivalent_earth, - problem.earth_props, - freq, - T, - ) - - temp = T(problem.temperature) - line_length = T(problem.system.line_length) - - # Construct and return the EMTWorkspace struct - return EMTWorkspace{T}( - freq = freq, jω = jω, - horz = horz, horz_sep = horz_sep, vert = vert, - r_in = r_in, r_ext = r_ext, - r_ins_in = r_ins_in, r_ins_ext = r_ins_ext, - rho_cond = rho_cond, alpha_cond = alpha_cond, mu_cond = mu_cond, - eps_cond = eps_cond, rho_ins = rho_ins, mu_ins = mu_ins, eps_ins = eps_ins, - tan_ins = tan_ins, insulator_layer_ranges = insulator_layer_ranges, - r_ins_layer_in = r_ins_layer_in, r_ins_layer_ext = r_ins_layer_ext, - rho_ins_layer = rho_ins_layer, eps_ins_layer = eps_ins_layer, - phase_map = phase_map, cable_map = cable_map, rho_g = rho_g, - eps_g = eps_g, mu_g = mu_g, - temp = temp, line_length = line_length, n_frequencies = n_frequencies, - n_phases = n_phases, - n_cables = n_cables, Z = Z, P = P, Zin = Zin, Pin = Pin, Zg = Zg, - Pg = Pg, - ) -end diff --git a/src/formulas.jl b/src/formulas.jl new file mode 100644 index 000000000..575151b64 --- /dev/null +++ b/src/formulas.jl @@ -0,0 +1,169 @@ +""" +$(TYPEDSIGNATURES) + +Select a registered formula. The receiving formulation determines its family from the +keyword slot in which the selection appears. + +# Arguments + +- `identifier`: stable formula identifier. + `:default` routes to the defining family's explicit default implementation for + the chosen backend. The resulting selection is checked against the problem + before computation. + Cable-insulation and semicon-admittance `:default` selections route to the + explicit `:lossless` dielectric relation. Unsupported contexts fail before + frequency evaluation. + +# Keywords + +- `order`: position of an equivalent homogeneous-earth reduction relative to + material frequency dependence. `:before` applies EquivalentHomogeneous before FrequencyDependent, `:after` + applies EquivalentHomogeneous after FrequencyDependent, and `:default` selects the receiving formulation's + default. Non-EquivalentHomogeneous formula slots accept only `:default`. +- `parameters=(;)`: explicit model parameters accepted by the defining formula. +- `options=(;)`: formulation-owned physical choices and numerical controls, such + as Unified's `Γ` \\[1/m\\] and `integration=(method=:quad, options=(;))`. +- `equivalent_earth=nothing`: explicit reduction for a compatible external formula. + +# Returns + +- A concrete declarative selection resolved before computation. + +# Examples + +```julia +earth = formula(:carson1926) +soil = formula(:default) +equivalent = formula(:default; order=:before) +``` +""" +function formula(identifier::Symbol; order::Symbol = :default, + parameters::NamedTuple = (;), + options::Union{NamedTuple, FormulationOptions} = FormulationOptions(), equivalent_earth = nothing) + options = options isa NamedTuple ? FormulationOptions(options) : options + order in (:default, :before, :after) || throw(ArgumentError( + "formula order must be :default, :before, or :after" + )) + return FormulaDefinition{identifier, order, typeof(parameters), + typeof(options), typeof(equivalent_earth)}( + parameters, options, equivalent_earth) +end + +"Return the selected formulation's identifier." +formula_id(expression::Expression) = formula_id(expression.selection) + +formula_id(::FormulaDefinition{ID}) where {ID} = ID + +"""Expose a requested formula identifier and its explicit model and numerical controls.""" +function Base.NamedTuple(value::FormulaDefinition{ID,Order}) where {ID,Order} + return (identifier=ID, order=Order, parameters=value.parameters, + options=value.options.data, equivalent_earth=value.equivalent_earth === nothing ? nothing : NamedTuple(value.equivalent_earth)) +end + +""" +$(TYPEDSIGNATURES) + +Describe a retained selection through its defining type. +""" +description(source::Pair{<:Type,<:NamedTuple}; compact::Bool=false) = description(first(source); compact) +formula_id(source::Pair{<:Type,<:NamedTuple}) = formula_id(first(source)) + +description(::Missing; compact::Bool=false) = "method unavailable" +description(::Nothing; compact::Bool=false) = "none" + +"""Describe a scalar, explicitly selected control at its defining formula.""" +description(::Type, ::Val{key}, value::Union{Number,Symbol,AbstractString}; + compact::Bool=false) where {key} = string(key,"=",value) +formula_id(::Missing) = missing +formula_id(::Nothing) = :none + +""" +$(TYPEDSIGNATURES) + +Label ordered formulation selections using their owner-supplied descriptions. + +# Arguments + +- `sources`: native formulations or owner-bound retained declarations. + +# Keywords + +- `roles`: one `:reference`, `:result`, or `:none` role per source. +- `quantity`: physical quantity selected through the observation grammar, or + `nothing` for all equation choices. +- `compact=true`: use the short owned names for both root and child selections. + +# Returns + +- One text label per source, in input order, without result numbering. + Consumers select unique quantity-relevant formulations before presentation. + +# Notes + +Owners expose native children through `pairs(source; quantity)` and retained +children through `pairs(owner, record; quantity)`, scoped +identity through `formula_id`, explicit controls in those selection pairs, and +text through `description`. This formatter only decides common-field omission +and reference prefixes. Unsupported owner methods are not caught. +""" +function description(sources::AbstractVector; + roles=fill(:result,length(sources)), + quantity=nothing, compact::Bool=true) + length(roles)==length(sources) || + throw(DimensionMismatch("one role is required per formulation")) + all(in((:reference,:result,:none)),roles) || + throw(ArgumentError("description roles must be reference, result, or none")) + isempty(sources) && return String[] + retained(value) = value isa Pair ? last(value) : (;) + selections=[ismissing(source) ? Pair[] : collect(pairs((source isa Pair ? Tuple(source) : (source,))...; quantity)) for source in sources] + complete=[ismissing(source) ? Pair[] : collect(pairs((source isa Pair ? Tuple(source) : (source,))...)) for source in sources] + formulation_indices=findall(!=(:reference),roles) + # Scientific comparisons use owner-scoped identifiers, never display text. + scopes=unique([scope for index in formulation_indices for (scope,_) in complete[index] + if !isempty(last(scope))]) + varying=filter(scopes) do scope + values=[[(formula_id(value),retained(value)) for (key,value) in complete[index] + if key==scope] for index in formulation_indices] + !all(value -> isequal(value,first(values)),values) + end + identities=[[scope for (scope,_) in entries if isempty(last(scope))] for entries in complete] + formulation_owners=unique(first(ids) for ids in identities[formulation_indices] if !isempty(ids)) + inner_owners=unique(last(ids) for ids in identities if length(ids)>1) + return map(eachindex(sources)) do index + prefix=roles[index]===:reference ? "Reference" : "" + parts=String[] + for (scope,value) in selections[index] + if isempty(last(scope)) + position=findfirst(==(scope),identities[index]) + show_identity=position==1 ? + (roles[index]!==:result || length(identities[index])>1 || length(formulation_owners)>1) : + length(inner_owners)>1 + show_identity && push!(parts,description(value;compact)) + controls=retained(value) + peer=[other for entries in selections for (key,other) in entries if key==scope] + if !isempty(controls) && any(other -> !isequal(retained(other),controls),peer) + push!(parts,description(scope,value;compact,settings=true)) + end + else + peer=[other for entries in selections for (key,other) in entries if key==scope] + changed=any(other -> !isequal((formula_id(other),retained(other)), + (formula_id(value),retained(value))),peer) + standalone=length(sources)==1 && !ismissing(formula_id(value)) && + formula_id(value)!==:none + # Common scalar choices are omitted independently of their IDs. + # Branch structure and controls remain visible in comparisons. + # A standalone description shows its concrete selections. + (length(last(scope))>1 || !isempty(retained(value)) || + scope in varying || changed || standalone) && + push!(parts,description(scope,value;compact)) + end + end + if isempty(parts) + unavailable=ismissing(sources[index]) || + any(entry -> ismissing(formula_id(last(entry))),selections[index]) + push!(parts,unavailable ? description(missing;compact) : description(sources[index];compact)) + end + text=join(parts,"; ") + isempty(prefix) ? text : isempty(text) ? prefix : prefix*" · "*text + end +end diff --git a/src/grid.jl b/src/grid.jl new file mode 100644 index 000000000..d06fbe059 --- /dev/null +++ b/src/grid.jl @@ -0,0 +1,310 @@ +import Base: eltype, extrema, getindex, iterate, length, rand, size +import Random + +""" +$(TYPEDEF) + +Supertype for the deterministic and uncertainty-bearing finite sources created +by [`Grid`](@ref). +""" +abstract type AbstractGrid end + +""" +$(TYPEDEF) + +Supertype for finite sources whose points retain an uncertainty descriptor. +""" +abstract type AbstractUncertainGrid <: AbstractGrid end + +""" +$(TYPEDEF) + +Store a nominal value and its absolute standard uncertainty. + +$(TYPEDFIELDS) +""" +struct UncertainValue{T, E} + "Nominal parameter value." + nominal::T + + "Absolute standard uncertainty in the same unit as `nominal`." + sigma::E + + function UncertainValue(nominal::T, sigma::E) where {T, E} + if nominal isa Real && sigma isa Real + isfinite(nominal) || throw(ArgumentError( + "uncertain nominal values must be finite; got $nominal", + )) + isfinite(sigma) || throw(ArgumentError( + "uncertainty must be finite; got $sigma", + )) + sigma >= zero(sigma) || throw(ArgumentError( + "uncertainty must be nonnegative; got $sigma", + )) + end + return new{T, E}(nominal, sigma) + end +end + +""" +$(TYPEDSIGNATURES) + +Return the nominal value stored in an [`UncertainValue`](@ref). +""" +nominal(value::UncertainValue) = value.nominal + +""" +$(TYPEDSIGNATURES) + +Return the absolute standard uncertainty stored in an +[`UncertainValue`](@ref), in the same unit as its nominal value. +""" +uncertainty(value::UncertainValue) = value.sigma + +""" +$(TYPEDEF) + +Represent one finite source of deterministic values. + +$(TYPEDFIELDS) +""" +struct DeterministicGrid{V <: Tuple} <: AbstractGrid + "Values admitted by the source." + vals::V +end + +""" +$(TYPEDEF) + +Represent the Cartesian product of nominal values and relative standard +uncertainties. + +$(TYPEDFIELDS) +""" +struct RelativeGrid{V <: Tuple, P <: Tuple} <: AbstractUncertainGrid + "Nominal values admitted by the source." + vals::V + + "Relative standard uncertainties expressed as percentages." + rel_err::P + + function RelativeGrid(vals::V, rel_err::P) where {V <: Tuple, P <: Tuple} + return validate(new{V, P}(vals, rel_err)) + end +end + +""" +$(TYPEDEF) + +Represent the Cartesian product of nominal values and absolute standard +uncertainties. + +$(TYPEDFIELDS) +""" +struct AbsoluteGrid{V <: Tuple, P <: Tuple} <: AbstractUncertainGrid + "Nominal values admitted by the source." + vals::V + + "Absolute standard uncertainties in the same unit as `vals`." + abs_err::P + + function AbsoluteGrid(vals::V, abs_err::P) where {V <: Tuple, P <: Tuple} + return validate(new{V, P}(vals, abs_err)) + end +end + +""" +$(TYPEDEF) + +Mark values as absolute standard uncertainties for +`Grid(values, AbsoluteError(...))`. + +$(TYPEDFIELDS) +""" +struct AbsoluteError{T <: Tuple} + "Absolute standard uncertainties." + vals::T + + function AbsoluteError(vals::T) where {T <: Tuple} + validate(vals, AbsoluteError) + return new{T}(vals) + end +end + +_grid_values(value::Tuple) = value +_grid_values(value::AbstractArray) = Tuple(value) +_grid_values(value) = (value,) + +# Standard uncertainties of the record being built. Relative uncertainties are percentages. +function validate(errors::Tuple, + ::Type{R}) where {R <: Union{AbsoluteError, AbsoluteGrid, RelativeGrid}} + kind = R <: RelativeGrid ? "relative" : "absolute" + isempty(errors) && throw(ArgumentError("$kind uncertainty cannot be empty")) + for error in errors + error isa Real || + throw(ArgumentError("$kind errors must be real; got $(typeof(error))")) + isfinite(error) || throw(ArgumentError("$kind errors must be finite; got $error")) + error >= zero(error) || + throw(ArgumentError("$kind errors must be nonnegative; got $error")) + end + return errors +end + +function validate(grid::Union{RelativeGrid, AbsoluteGrid}) + kind, errors = grid isa RelativeGrid ? ("relative", grid.rel_err) : + ("absolute", grid.abs_err) + values = grid.vals + isempty(values) && + throw(ArgumentError("$kind uncertainty cannot have an empty nominal source")) + validate(errors, typeof(grid)) + for value in values + value isa Real || throw(ArgumentError( + "$kind uncertainty requires real nominal values; got $(typeof(value))", + )) + isfinite(value) || + throw(ArgumentError("$kind nominal values must be finite; got $value")) + end + return grid +end + +AbsoluteError(value) = AbsoluteError(_grid_values(value)) + +""" +$(TYPEDSIGNATURES) + +Create an explicit finite source. Collections vary only when passed to +`Grid`. Ordinary builder arguments remain atomic domain values. +""" +Grid(grid::AbstractGrid) = grid +Grid(value) = DeterministicGrid(_grid_values(value)) +Grid(value, error::AbsoluteError) = AbsoluteGrid(_grid_values(value), error.vals) +function Grid(value, relative_error) + RelativeGrid( + _grid_values(value), + _grid_values(relative_error) + ) +end + +iterate(grid::DeterministicGrid, state...) = iterate(grid.vals, state...) +length(grid::DeterministicGrid) = length(grid.vals) +size(grid::DeterministicGrid) = (length(grid),) +getindex(grid::DeterministicGrid, index::Integer) = grid.vals[index] +eltype(::Type{<:DeterministicGrid{V}}) where {V} = eltype(V) +#! explicit-imports: off +# Base's iterator trait protocol exposes this value without a public binding. +Base.IteratorSize(::Type{<:DeterministicGrid}) = Base.HasShape{1}() +#! explicit-imports: on + +function iterate(grid::RelativeGrid, state...) + item = iterate(Iterators.product(grid.vals, grid.rel_err), state...) + item === nothing && return nothing + (value, percent), next_state = item + return UncertainValue(value, abs(value) * percent / 100), next_state +end + +function iterate(grid::AbsoluteGrid, state...) + item = iterate(Iterators.product(grid.vals, grid.abs_err), state...) + item === nothing && return nothing + (value, error), next_state = item + return UncertainValue(value, error), next_state +end + +length(grid::RelativeGrid) = length(grid.vals) * length(grid.rel_err) +length(grid::AbsoluteGrid) = length(grid.vals) * length(grid.abs_err) +size(grid::AbstractUncertainGrid) = (length(grid),) +#! explicit-imports: off +# Base's iterator trait protocol exposes this value without a public binding. +Base.IteratorSize(::Type{<:AbstractUncertainGrid}) = Base.HasShape{1}() +#! explicit-imports: on + +function getindex(grid::RelativeGrid, index::Integer) + 1 <= index <= length(grid) || throw(BoundsError(grid, index)) + value_index = mod1(index, length(grid.vals)) + error_index = cld(index, length(grid.vals)) + value = grid.vals[value_index] + return UncertainValue(value, abs(value) * grid.rel_err[error_index] / 100) +end + +function getindex(grid::AbsoluteGrid, index::Integer) + 1 <= index <= length(grid) || throw(BoundsError(grid, index)) + value_index = mod1(index, length(grid.vals)) + error_index = cld(index, length(grid.vals)) + return UncertainValue(grid.vals[value_index], grid.abs_err[error_index]) +end + +extrema(grid::DeterministicGrid) = extrema(grid.vals) + +function extrema(grid::RelativeGrid) + bounds = map(Iterators.product(grid.vals, grid.rel_err)) do (value, percent) + delta = abs(value) * percent / 100 + (value - delta, value + delta) + end + return minimum(first, bounds), maximum(last, bounds) +end + +function extrema(grid::AbsoluteGrid) + bounds = map(Iterators.product(grid.vals, grid.abs_err)) do (value, error) + (value - error, value + error) + end + return minimum(first, bounds), maximum(last, bounds) +end + +""" +$(TYPEDSIGNATURES) + +Draw one scalar realization from an uncertainty descriptor using a caller-owned +random number generator and the selected sampling family. +""" +function sample_uncertainty( + rng::Random.AbstractRNG, + value::UncertainValue{<:Real, <:Real}, + distribution::Symbol +) + distribution === :normal && return value.nominal + value.sigma * randn(rng) + distribution === :uniform && + return value.nominal + sqrt(3) * value.sigma * (2 * rand(rng) - 1) + throw(ArgumentError( + "unsupported distribution :$distribution; expected :normal or :uniform", + )) +end + +function sample_uncertainty( + rng::Random.AbstractRNG, + value::UncertainValue{<:Real}, + sampler::Function +) + sampler(rng, value.nominal, value.sigma) +end + +function sample_uncertainty(::Random.AbstractRNG, ::UncertainValue, distribution) + throw(ArgumentError( + "unsupported distribution $(typeof(distribution)); load its package extension or pass a sampler function", + )) +end + +function rand( + rng::Random.AbstractRNG, + value::UncertainValue{<:Real}; + distribution = :normal +) + iszero(value.sigma) && return float(value.nominal) + return sample_uncertainty(rng, value, distribution) +end + +function rand(value::UncertainValue{<:Real}; distribution = :normal) + return rand(Random.default_rng(), value; distribution) +end + +function rand( + rng::Random.AbstractRNG, + grid::AbstractGrid; + distribution = :normal +) + isempty(grid) && throw(ArgumentError("cannot sample an empty Grid")) + value = grid[rand(rng, 1:length(grid))] + return value isa UncertainValue ? rand(rng, value; distribution) : value +end + +function rand(grid::AbstractGrid; distribution = :normal) + return rand(Random.default_rng(), grid; distribution) +end diff --git a/src/gridspace.jl b/src/gridspace.jl new file mode 100644 index 000000000..ed02975c5 --- /dev/null +++ b/src/gridspace.jl @@ -0,0 +1,397 @@ +""" +Concrete callable representation of a type constructor used by a Gridspace. +""" +struct _TypeConstructor{Target} end + +(::_TypeConstructor{Target})(arguments...) where {Target} = Target(arguments...) + +_gridspace_callable(build) = build +_gridspace_callable(::Type{Target}) where {Target} = _TypeConstructor{Target}() + +""" +$(TYPEDEF) + +Represent a typed finite space assembled from explicit [`Grid`](@ref), nested +`Gridspace` or admitted completed result-space sources. `combine` is encoded +in the type and may be `:product` or `:zip`. `Target` identifies the semantic +result family. A nonempty deterministic space records the concrete type +returned by its callable when Julia can prove it without evaluating a point. +Otherwise the space declares its iterator element type unknown until values +are materialized. + +$(TYPEDFIELDS) +""" +struct Gridspace{Target, F, G <: Tuple, C, R} + "Callable that constructs `Target` from one selected argument tuple." + build::F + + "Explicit Grid or nested Gridspace sources." + grids::G +end + +""" +Sentinel result type for a Gridspace whose materialized element type is unknown. +""" +struct _UnknownGridspaceEltype end + +const _GridspaceSource = Union{AbstractGrid, Gridspace, AbstractResultSpace} + +function _gridspace_eltype(::Type{Tuple}, ::typeof(tuple), grids::Tuple) + (any(has_uncertainty, grids) || any(grid -> iszero(length(grid)), grids)) && + return _UnknownGridspaceEltype + argument_types = map(grid -> eltype(typeof(grid)), grids) + all(isconcretetype, argument_types) || return _UnknownGridspaceEltype + return Tuple{argument_types...} +end + +function _gridspace_eltype(::Type{Target}, build, grids::Tuple) where {Target} + (any(has_uncertainty, grids) || any(grid -> iszero(length(grid)), grids)) && + return _UnknownGridspaceEltype + + argument_types = map(grid -> eltype(typeof(grid)), grids) + inferred = Base.code_typed( + build, + Tuple{argument_types...}; + optimize = false + ) + result_type = isempty(inferred) ? Any : + foldl(typejoin, (last(entry) for entry in inferred)) + isconcretetype(result_type) || return _UnknownGridspaceEltype + result_type <: Target || throw(ArgumentError( + "Gridspace callable result $result_type is not a subtype of target $Target", + )) + return result_type +end + +""" +$(TYPEDSIGNATURES) + +Construct a lazy space from explicit finite sources. Product composition uses +Julia's product order, with the first source varying fastest. Zip composition +pairs equal-length sources and broadcasts singleton sources. A completed +result space is admitted only through a result-owner transport method of the +form `Gridspace{Target}(source)`. + +# Errors + +- Throws `ArgumentError` when `combine` is unsupported or an input is not a + `Grid` or nested `Gridspace`. +- Throws `ArgumentError` when an inferred concrete callable result does not + belong to `Target`. +- Throws `DimensionMismatch` when zip source lengths are incompatible. +""" +function Gridspace{Target}( + build, + grids::G; + combine::Symbol = :product +) where {Target, G <: Tuple} + combine in (:product, :zip) || + throw(ArgumentError("combine must be :product or :zip; got :$combine")) + all(grid -> grid isa _GridspaceSource, grids) || throw( + ArgumentError( + "Gridspace sources must be Grid, Gridspace, or completed result-space values", + ), + ) + callable = _gridspace_callable(build) + result_type = _gridspace_eltype(Target, callable, grids) + space = Gridspace{ + Target, typeof(callable), G, Val{combine}, result_type + }(callable, grids) + combine === :zip && _zip_length(space) + return space +end + +function Gridspace{Target}(grids::Tuple; combine::Symbol = :product) where {Target} + Gridspace{Target}(Target, grids; combine) +end + +Grid(space::Gridspace) = space + +const _FiniteSource = Union{AbstractGrid, Gridspace} +const _FiniteCollection = Union{Tuple, AbstractVector} + +_contains_grid(::_FiniteSource) = true +_contains_grid(values::_FiniteCollection) = any(_contains_grid, values) +_contains_grid(::Any) = false + +_collect_arguments(values...) = collect(values) + +_grid_source(source::_FiniteSource; combine::Symbol) = source +function _grid_source(values::Tuple; combine::Symbol) + sources = map(values) do value + _contains_grid(value) ? _grid_source(value; combine) : Grid((value,)) + end + return Gridspace{Tuple}(tuple, sources; combine) +end +function _grid_source(values::AbstractVector; combine::Symbol) + sources = map(values) do value + _contains_grid(value) ? _grid_source(value; combine) : Grid((value,)) + end + return Gridspace{Vector}(_collect_arguments, Tuple(sources); combine) +end + +""" + parameterize(Target, caller, values; combine=:product) + +Invoke `caller` directly when `values` are scalar. If an explicit `Grid` or +`Gridspace` occurs at any admitted argument position, return a +`Gridspace{Target}` that reconstructs the same call point by point. +""" +function parameterize( + ::Type{Target}, caller, values::Tuple; combine::Symbol = :product +) where {Target} + combine in (:product, :zip) || throw(ArgumentError( + "combine must be :product or :zip" + )) + any(_contains_grid, values) || return caller(values...) + sources = map(values) do value + _contains_grid(value) ? _grid_source(value; combine) : Grid((value,)) + end + return Gridspace{Target}(caller, sources; combine) +end + +points(grid::AbstractGrid) = grid +points(result::AbstractResultSpace) = result + +_zip_length(::Gridspace{<:Any, <:Any, Tuple{}, Val{:zip}, <:Any}) = 1 +function _zip_length(space::Gridspace{<:Any, <:Any, <:Tuple, Val{:zip}, <:Any}) + counts = map(length, space.grids) + target_count = maximum(counts) + all(count -> count == 1 || count == target_count, counts) || throw( + DimensionMismatch( + "zip sources must have equal cardinality or be singletons; got $(Tuple(counts))", + ), + ) + return target_count +end + +function _zip_source(source, target_count::Int) + length(source) == 1 && + return Iterators.repeated(only(points(source)), target_count) + return points(source) +end + +_combinations(::Gridspace{<:Any, <:Any, Tuple{}, Val{:zip}, <:Any}) = ((),) +function _combinations( + space::Gridspace{<:Any, <:Any, <:Tuple, Val{:zip}, <:Any} +) + target_count = _zip_length(space) + iterators = map(source -> _zip_source(source, target_count), space.grids) + return Iterators.zip(iterators...) +end + +_combinations(::Gridspace{<:Any, <:Any, Tuple{}, Val{:product}, <:Any}) = ((),) +function _combinations( + space::Gridspace{<:Any, <:Any, <:Any, Val{:product}, <:Any} +) + Iterators.product(map(points, space.grids)...) +end + +_indexed_source(::AbstractGrid) = Val(false) +_indexed_source(::AbstractResultSpace) = Val(true) +_indexed_source(::DeterministicGrid) = Val(true) +_indexed_source(::RelativeGrid) = Val(true) +_indexed_source(::AbsoluteGrid) = Val(true) +_indexed_source(space::Gridspace) = _indexed_sources(space.grids) + +_indexed_sources(::Tuple{}) = Val(true) +function _indexed_sources(sources::Tuple) + return _indexed_sources(_indexed_source(first(sources)), Base.tail(sources)) +end +_indexed_sources(::Val{false}, ::Tuple) = Val(false) +_indexed_sources(::Val{true}, sources::Tuple) = _indexed_sources(sources) + +_source_point(source::AbstractGrid, index::Int) = source[index] +_source_point(source::AbstractResultSpace, index::Int) = source[index] +function _source_point(space::Gridspace{Target}, index::Int) where {Target} + return Gridpoint{Target}(space.build, _arguments_at(space, index)) +end + +function _arguments_at( + space::Gridspace{<:Any, <:Any, <:Any, Val{:product}, <:Any}, + index::Int +) + 1 <= index <= length(space) || throw(BoundsError(space, index)) + arguments = Vector{Any}(undef, length(space.grids)) + offset = index - 1 + for (source_index, source) in pairs(space.grids) + count = length(source) + arguments[source_index] = _source_point(source, mod(offset, count) + 1) + offset = div(offset, count) + end + return Tuple(arguments) +end + +function _arguments_at( + space::Gridspace{<:Any, <:Any, <:Any, Val{:zip}, <:Any}, + index::Int +) + 1 <= index <= length(space) || throw(BoundsError(space, index)) + arguments = Vector{Any}(undef, length(space.grids)) + for (source_index, source) in pairs(space.grids) + arguments[source_index] = _source_point( + source, length(source) == 1 ? 1 : index + ) + end + return Tuple(arguments) +end + +function _indexed_points(space::Gridspace{Target}) where {Target} + return (Gridpoint{Target}(space.build, _arguments_at(space, index)) + for index in 1:length(space)) +end +function _iterated_points(space::Gridspace{Target}) where {Target} + return (Gridpoint{Target}(space.build, args) for args in _combinations(space)) +end +_zip_points(space::Gridspace, ::Val{true}) = _indexed_points(space) +_zip_points(space::Gridspace, ::Val{false}) = _iterated_points(space) + +""" +Return the lazy unresolved points of a Gridspace. +""" +function points( + space::Gridspace{<:Any, <:Any, <:Any, Val{:product}, <:Any} +) + return _iterated_points(space) +end +function points(space::Gridspace{<:Any, <:Any, <:Any, Val{:zip}, <:Any}) + return _zip_points(space, _indexed_sources(space.grids)) +end + +""" +Return a value unchanged during deterministic point materialization. +""" +materialize(value) = value + +function materialize(value::UncertainValue) + throw(ArgumentError( + "direct materialization of an uncertainty-bearing Gridspace requires " * + "Measurements.jl; load it with `using Measurements` before " * + "iteration or MonteCarlo computation", + )) +end + +""" +Recursively materialize a selected Gridspace point. +""" +function materialize(point::Gridpoint) + point.build(map(materialize, point.args)...) +end + +function materialize( + point::Gridpoint{Target} +) where {Target <: AbstractProblemDefinition} + return validate(point.build(map(materialize, point.args)...))::Target +end + +""" +Return a deterministic value unchanged during stochastic realization. +""" +realize(::Random.AbstractRNG, value, _) = value + +function realize(rng::Random.AbstractRNG, value::UncertainValue, distribution) + rand(rng, value; distribution) +end + +""" +Draw the arguments of a selected Gridspace point without invoking its builder. +""" +function realize_arguments(rng::Random.AbstractRNG, point::Gridpoint, distribution) + return map(value -> realize(rng, value, distribution), point.args) +end + +""" +Build a selected Gridspace point from an already realized argument tuple. +""" +realize(point::Gridpoint, arguments::Tuple) = point.build(arguments...) + +function realize( + point::Gridpoint{Target}, + arguments::Tuple +) where {Target <: AbstractProblemDefinition} + return validate(point.build(arguments...))::Target +end + +""" +Recursively realize a selected Gridspace point using the caller's RNG. +""" +function realize(rng::Random.AbstractRNG, point::Gridpoint, distribution) + return realize(point, realize_arguments(rng, point, distribution)) +end + +function _materialized_result( + ::Type{_UnknownGridspaceEltype}, ::Type{Target}, point +) where {Target} + return materialize(point)::Target +end +function _materialized_result(::Type{Result}, ::Type, point) where {Result} + return materialize(point)::Result +end + +function Base.iterate( + space::Gridspace{Target, <:Any, <:Any, <:Any, Result}, state... +) where {Target, Result} + item = iterate(points(space), state...) + item === nothing && return nothing + point, next_state = item + return _materialized_result(Result, Target, point), next_state +end + +_gridspace_iterator_eltype(::Type{_UnknownGridspaceEltype}) = Base.IteratorEltype(Any) +_gridspace_iterator_eltype(::Type) = Base.IteratorEltype(Vector{Int}) + +_gridspace_eltype_trait(::Type{_UnknownGridspaceEltype}) = Any +_gridspace_eltype_trait(::Type{Result}) where {Result} = Result + +#! explicit-imports: off +# Base's iterator trait protocol exposes these values without public bindings. +Base.IteratorSize(::Type{<:Gridspace}) = Base.HasShape{1}() +function Base.IteratorEltype( + ::Type{<:Gridspace{<:Any, <:Any, <:Any, <:Any, Result}} +) where {Result} + return _gridspace_iterator_eltype(Result) +end +#! explicit-imports: on +function Base.eltype( + ::Type{<:Gridspace{<:Any, <:Any, <:Any, <:Any, Result}} +) where {Result} + _gridspace_eltype_trait(Result) +end +function Base.length( + space::Gridspace{<:Any, <:Any, <:Any, Val{:product}, <:Any} +) + prod(length, space.grids; init = 1) +end +Base.length(space::Gridspace{<:Any, <:Any, <:Any, Val{:zip}, <:Any}) = _zip_length(space) +Base.size(space::Gridspace) = (length(space),) + +""" +Draw one unresolved point, then realize its arguments through the owned construction path. +""" +function Base.rand( + rng::Random.AbstractRNG, + space::Gridspace{<:Any, <:Any, <:Any, <:Any, Result}; + distribution = :normal +) where {Result} + iszero(length(space)) && throw(ArgumentError("cannot sample an empty Gridspace")) + offset = rand(rng, 0:(length(space) - 1)) + point = _indexed_sources(space.grids) === Val(true) ? + _source_point(space, offset + 1) : first(Iterators.drop(points(space), offset)) + value = realize(rng, point, distribution) + Result === _UnknownGridspaceEltype && return value + return value::Result +end + +function Base.rand(space::Gridspace; distribution = :normal) + return rand(Random.default_rng(), space; distribution) +end + +""" +Return whether a value structurally contains an uncertainty descriptor. +""" +has_uncertainty(::UncertainValue) = true +has_uncertainty(grid::AbstractUncertainGrid) = true +has_uncertainty(grid::DeterministicGrid) = any(has_uncertainty, grid.vals) +has_uncertainty(point::Gridpoint) = any(has_uncertainty, point.args) +has_uncertainty(space::Gridspace) = any(has_uncertainty, space.grids) +has_uncertainty(::Any) = false diff --git a/src/importexport/ImportExport.jl b/src/importexport/ImportExport.jl index 75bcaa6ad..0a7bb8ffe 100644 --- a/src/importexport/ImportExport.jl +++ b/src/importexport/ImportExport.jl @@ -1,75 +1,80 @@ """ - LineCableModels.ImportExport + LineCableModels.ImportExport -The [`ImportExport`](@ref) module provides methods for serializing and deserializing data structures in [`LineCableModels.jl`](index.md), and data exchange with external programs. +Read and write LineCableModels data. # Overview -This module provides functionality for: - -- Saving and loading cable designs and material libraries to/from JSON and other formats. -- Exporting cable system models to PSCAD and ATP formats. -- Serializing custom types with special handling for measurements and complex numbers. - -The module implements a generic serialization framework with automatic type reconstruction -and proper handling of Julia-specific types like `Measurement` objects and `Inf`/`NaN` values. +- Save and load material and cable libraries. +- Encode package-owned model types in the versioned JSON schema. +- Export cable systems and calculated matrices to PSCAD, ATPDraw, and TRALIN + formats. +- Import supported PSCAD and TRALIN results. # Dependencies $(IMPORTS) -# Exports - -$(EXPORTS) """ module ImportExport -# Export public API export export_data -export read_data +export import_data export save export load! -# Module-specific dependencies -using ..Commons -using ..Utils: display_path, to_nominal, resolve_T, coerce_to_T, isdiag_approx -using ..Materials: Material, MaterialsLibrary -using ..EarthProps: EarthModel -using ..DataModel: CablesLibrary, CableDesign, CableComponent, ConductorGroup, - InsulatorGroup, CircStrands, RectStrands, Strip, Tubular, Semicon, Insulator, - LineCableSystem, NominalData -import ..Engine: LineParameters, SeriesImpedance, ShuntAdmittance -using Measurements -using EzXML -using Dates -using Printf # For ATP export -using JSON3 -using Serialization # For .jls format -using LinearAlgebra -using XLSX -using Tables -using DataFrames - -""" -$(TYPEDSIGNATURES) - -Export [`LineCableModels`](@ref) data for use in different EMT-type programs. - -# Methods - -$(METHODLIST) -""" -# function export_data end -export_data(backend::Symbol, args...; kwargs...) = - export_data(Val(backend), args...; kwargs...) - +#! explicit-imports: off +# IMPORTS is expanded in the module docstring rather than called as Julia code. +using DocStringExtensions: IMPORTS +#! explicit-imports: on +using DocStringExtensions: TYPEDSIGNATURES, METHODLIST +import ..LineCableModels: build, validate, nominal, parameterize +import ..LineCableModels +import ..Commons +import ..Units +import UUIDs +import ..Commons: observe, FormulationOptions, ComputationOptions, ComputationDetails +import ..ReportBuilder +using ..Materials: AbstractMaterial, Material, RadialDielectric, MaterialsLibrary +using ..Earth: EarthLayer, EarthModel +import ..DataModel +using ..DataModel: CablesLibrary, DatasheetInfo, CableDesign, LineCableSystem, + AbstractCablePart, Region, Stack, Group, Assembly, Enclosure, + Disk, Rectangle, Ellipse, + Sector, Annulus, Shell, + Polygon, Pose2, + Ring, Polar, Fill, Lattice, capacity, + FillFactor, + LayRatio, Pitch, LayAngle, Helix +using ..ParametricBuilder: AbstractGrid, DeterministicGrid, RelativeGrid, + AbsoluteGrid, Grid, Gridspace, AbsoluteError +import ..Engine +import ..Engine: ModalAnalysis +import ..Engine: LineParameters, SeriesImpedance, ShuntAdmittance, + frequencies, Z, Y, C +import EzXML +using EzXML: ElementNode, XMLDocument, addelement!, prettyprint, setroot! +using Printf: @printf, @sprintf +import JSON3 +import Serialization +import Statistics +import ..UQ +import LinearAlgebra +using LinearAlgebra: tril + +include("interfaces.jl") +include("paths.jl") include("serialize.jl") include("deserialize.jl") +include("uncertainty.jl") +include("observed.jl") +include("problem.jl") include("cableslibrary.jl") include("materialslibrary.jl") -include("pscad.jl") include("atp.jl") -include("xlsx.jl") include("tralin.jl") +public encode_observation, decode_observation_measurement, observation_sources +public serialize_value, deserialize_value, deserialize_extension + end # module ImportExport diff --git a/src/importexport/atp.jl b/src/importexport/atp.jl index fa21e80ab..814ead739 100644 --- a/src/importexport/atp.jl +++ b/src/importexport/atp.jl @@ -1,45 +1,27 @@ -"""$(TYPEDSIGNATURES) +""" +$(TYPEDSIGNATURES) -Export a [`LineCableSystem`](@ref) to an **ATPDraw‑compatible** XML file (LCC component with input data). +Write an ATPDraw LCC project for a materialized line-cable system. -This routine serializes the cable system geometry (positions and outer radii) and the -already‑computed, frequency‑specific equivalent parameters of each cable component to the -ATPDraw XML schema. The result is written to disk and the absolute file path is returned -on success. +The XML contains cable positions, conductor and insulation properties, line +length, system frequency, and the resistivity of the last earth layer. # Arguments -- `::Val{:atp}`: Backend selector for the ATP/ATPDraw exporter. -- `cable_system::LineCableSystem`: The system to export. Each entry in `cable_system.cables` provides one phase position and its associated [`CableDesign`](@ref). The number of phases exported equals `length(cable_system.cables)`. -- `earth_props::EarthModel`: Ground model used to populate ATP soil parameters. The exporter -uses the **last** layer’s base resistivity as *Grnd resis*. -- `base_freq::Number = f₀` \\[Hz\\]: System frequency written to ATP (`SysFreq`) and stored in component metadata. *This exporter does not recompute R/L/C/G; it writes the values as -present in the groups/components at the time of export.* -- `file_name::String = "*_export.xml"`: Output file name or path. If a relative path is given, it is resolved against the exporter’s source directory. The absolute path of the saved file is returned. +- `::Val{:atp}`: ATP format selector. +- `cable_system`: positioned cable designs and line length. +- `earth_props`: earth model. The last layer supplies `Grnd resis`. -# Behavior +# Keywords -1. Create the ATPDraw `` root and header and insert a single **LCC** component with - `NumPhases = length(cable_system.cables)`. -2. For each [`CablePosition`](@ref) in `cable_system.cables`: +- `base_freq`: ATP system frequency \\[Hz\\]. Default: `50.0`. +- `file_name`: output path. Relative paths are resolved beside this exporter. + a supplied basename is prefixed with `cable_system.system_id`. Default: + `nothing`, which writes `_export.xml`. - * Write a `` element with: +# Returns - * `NumCond` = number of [`CableComponent`](@ref)s in the design, - * `Rout` = outermost radius of the design (m), - * `PosX`, `PosY` = cable coordinates (m). -3. For each [`CableComponent`](@ref) inside a cable: - - * Write one `` element with fields (all per unit length): - - * `Rin`, `Rout` — from the component’s conductor group, - * `rho` — conductor equivalence via [`calc_equivalent_rho`](@ref), - * `muC` — conductor relative permeability via [`calc_equivalent_mu`](@ref), - * `muI` — insulator relative permeability (taken from the first insulating layer’s material), - * `epsI` — insulation relative permittivity via [`calc_equivalent_eps`](@ref), - * `Cext`, `Gext` — shunt capacitance and conductance from the component’s insulator group. -4. Soil resistivity is written as *Grnd resis* using `earth_props.layers[end].base_rho_g`. -5. The XML is pretty‑printed and written to `file_name`. On I/O error, the function logs an error and returns `nothing`. +- The absolute output path. # Units @@ -56,499 +38,334 @@ Units are printed in the XML file according to the ATPDraw specifications: # Notes -* The exporter assumes each component’s equivalent parameters (R/G/C and derived ρ/ε/μ) were - already computed by the design/group constructors at the operating conditions of interest. -* Mixed numeric types are supported; values are stringified for XML output. When using - uncertainty types (e.g., `Measurements.Measurement`), the uncertainty is removed. -* Overlap checks between cables are enforced when building the system, not during export. - -# Examples - -```julia -# Build or load a system `sys` and an earth model `earth` -file = $(FUNCTIONNAME)(Val(:atp), sys, earth; base_freq = 50.0, - file_name = "system_id_export.xml") -println("Exported to: ", file) -``` - -# See also +- The exporter writes explicitly adapted equivalent properties at the common material + reference state. Operating-temperature correction is excluded from the export. +- [`nominal`](@ref) removes uncertainty before numeric values are written. +- LineCableSystem construction, rather than export, checks cable overlap. -* [`LineCableSystem`](@ref), [`CablePosition`](@ref), [`CableComponent`](@ref) -* [`EarthModel`](@ref) -* [`calc_equivalent_rho`](@ref), [`calc_equivalent_mu`](@ref), [`calc_equivalent_eps`](@ref) """ function export_data(::Val{:atp}, - cable_system::LineCableSystem, - earth_props::EarthModel; - base_freq = f₀, - file_name::Union{String, Nothing} = nothing, -)::Union{String, Nothing} - - function _set_attributes!(element::EzXML.Node, attrs::Dict) - for (k, v) in attrs - element[k] = string(v) - end - end - # --- 1. Setup Constants and Variables --- - if isnothing(file_name) - # caller didn't supply a name -> derive from cable_system if present - file_name = joinpath(@__DIR__, "$(cable_system.system_id)_export.xml") - else - # caller supplied a path/name -> respect directory, but prepend system_id to basename - requested = isabspath(file_name) ? file_name : joinpath(@__DIR__, file_name) - if isnothing(cable_system) - file_name = requested - else - dir = dirname(requested) - base = basename(requested) - file_name = joinpath(dir, "$(cable_system.system_id)_$base") - end - end - - num_phases = length(cable_system.cables) - - # Create XML Structure and LCC Component - doc = XMLDocument() - project = ElementNode("project") - setroot!(doc, project) - _set_attributes!( - project, - Dict("Application" => "ATPDraw", "Version" => "7.3", "VersionXML" => "1"), - ) - header = addelement!(project, "header") - _set_attributes!( - header, - Dict( - "Timestep" => 1e-6, - "Tmax" => 0.1, - "XOPT" => 0, - "COPT" => 0, - "SysFreq" => base_freq, - "TopLeftX" => 200, - "TopLeftY" => 0, - ), - ) - objects = addelement!(project, "objects") - variables = addelement!(project, "variables") - comp = addelement!(objects, "comp") - _set_attributes!( - comp, - Dict( - "Name" => "LCC", - "Id" => "$(cable_system.system_id)_1", - "Capangl" => 90, - "CapPosX" => -10, - "CapPosY" => -25, - "Caption" => "", - ), - ) - comp_content = addelement!(comp, "comp_content") - _set_attributes!( - comp_content, - Dict( - "PosX" => 280, - "PosY" => 360, - "NumPhases" => num_phases, - "Icon" => "default", - "SinglePhaseIcon" => "true", - ), - ) - for side in ["IN", "OUT"] - y0 = -20 - for k in 1:num_phases - y0 += 10 - node = addelement!(comp_content, "node") - _set_attributes!( - node, - Dict( - "Name" => "$side$k", - "Value" => "C$(k)$(side=="IN" ? "SND" : "RCV")", - "UserNamed" => "true", - "Kind" => k, - "PosX" => side == "IN" ? -20 : 20, - "PosY" => y0, - "NamePosX" => 0, - "NamePosY" => 0, - ), - ) - end - end - - line_length = to_nominal(cable_system.line_length) - soil_rho = to_nominal(earth_props.layers[end].base_rho_g) - for (name, value) in - [("Length", line_length), ("Freq", base_freq), ("Grnd resis", soil_rho)] - data_node = addelement!(comp_content, "data") - _set_attributes!(data_node, Dict("Name" => name, "Value" => value)) - end - - # Populate the LCC Sub-structure with CORRECTLY Structured Cable Data - lcc_node = addelement!(comp, "LCC") - _set_attributes!( - lcc_node, - Dict( - "NumPhases" => num_phases, - "IconLength" => "true", - "LineCablePipe" => 2, - "ModelType" => 1, - ), - ) - cable_header = addelement!(lcc_node, "cable_header") - _set_attributes!( - cable_header, - Dict("InAirGrnd" => 1, "MatrixOutput" => "true", "ExtraCG" => "$(num_phases)"), - ) - - for (k, cable) in enumerate(cable_system.cables) - cable_node = addelement!(cable_header, "cable") - - num_components = length(cable.design_data.components) - outermost_radius = - to_nominal(cable.design_data.components[end].insulator_group.r_ex) - - _set_attributes!( - cable_node, - Dict( - "NumCond" => num_components, - "Rout" => outermost_radius, - "PosX" => to_nominal(cable.horz), - "PosY" => to_nominal(cable.vert), - ), - ) - - for component in cable.design_data.components - conductor_node = addelement!(cable_node, "conductor") - - cond_group = component.conductor_group - cond_props = component.conductor_props - ins_group = component.insulator_group - ins_props = component.insulator_props - - rho_eq = (cond_props.rho) - mu_r_cond = (cond_props.mu_r) - mu_r_ins = (ins_props.mu_r) - eps_eq = (ins_props.eps_r) - - _set_attributes!( - conductor_node, - Dict( - "Rin" => to_nominal(cond_group.r_in), - "Rout" => to_nominal(cond_group.r_ex), - "rho" => to_nominal(rho_eq), - "muC" => to_nominal(mu_r_cond), - "muI" => to_nominal(mu_r_ins), - "epsI" => to_nominal(eps_eq), - "Cext" => to_nominal(ins_group.shunt_capacitance), - "Gext" => to_nominal(ins_group.shunt_conductance), - ), - ) - end - end - - # Finalize and Write to File - _set_attributes!(variables, Dict("NumSim" => 1, "IOPCVP" => 0, "UseParser" => "false")) - - try - open(file_name, "w") do fid - prettyprint(fid, doc) - end - @info "XML file saved to: $(display_path(file_name))" - return file_name - catch e - @error "Failed to write XML file '$(display_path(file_name))'" exception = - (e, catch_backtrace()) - return nothing - end + cable_system::LineCableSystem, + earth_props::EarthModel; + base_freq = 50.0, + file_name::Union{String, Nothing} = nothing +)::String + # ATP defines this explicit homogenization choice. The physical design remains + # authoritative. DataModel reduces only the local concentric parts required + # by ATPDraw's LCC record. + atp_components(design) = DataModel.flatten(design, base_freq) + function _set_attributes!(element, attrs::Dict) + for (k, v) in attrs + element[k] = string(v) + end + end + # 1. Setup Constants and Variables + if isnothing(file_name) + # Derive the name from cable_system when the caller omits it. + file_name = joinpath(@__DIR__, "$(cable_system.system_id)_export.xml") + else + # caller supplied a path and name -> respect directory, but prepend system_id to basename + requested = isabspath(file_name) ? file_name : joinpath(@__DIR__, file_name) + if isnothing(cable_system) + file_name = requested + else + dir = dirname(requested) + base = basename(requested) + file_name = joinpath(dir, "$(cable_system.system_id)_$base") + end + end + + num_phases = length(cable_system.designs) + + # Create XML Structure and LCC Component + doc = XMLDocument() + project = ElementNode("project") + setroot!(doc, project) + _set_attributes!( + project, + Dict("Application" => "ATPDraw", "Version" => "7.3", "VersionXML" => "1") + ) + header = addelement!(project, "header") + _set_attributes!( + header, + Dict( + "Timestep" => 1e-6, + "Tmax" => 0.1, + "XOPT" => 0, + "COPT" => 0, + "SysFreq" => base_freq, + "TopLeftX" => 200, + "TopLeftY" => 0 + ) + ) + objects = addelement!(project, "objects") + variables = addelement!(project, "variables") + comp = addelement!(objects, "comp") + _set_attributes!( + comp, + Dict( + "Name" => "LCC", + "Id" => "$(cable_system.system_id)_1", + "Capangl" => 90, + "CapPosX" => -10, + "CapPosY" => -25, + "Caption" => "" + ) + ) + comp_content = addelement!(comp, "comp_content") + _set_attributes!( + comp_content, + Dict( + "PosX" => 280, + "PosY" => 360, + "NumPhases" => num_phases, + "Icon" => "default", + "SinglePhaseIcon" => "true" + ) + ) + for side in ["IN", "OUT"] + y0 = -20 + for k in 1:num_phases + y0 += 10 + node = addelement!(comp_content, "node") + _set_attributes!( + node, + Dict( + "Name" => "$side$k", + "Value" => "C$(k)$(side=="IN" ? "SND" : "RCV")", + "UserNamed" => "true", + "Kind" => k, + "PosX" => side == "IN" ? -20 : 20, + "PosY" => y0, + "NamePosX" => 0, + "NamePosY" => 0 + ) + ) + end + end + + line_length = nominal(cable_system.line_length) + soil_rho = nominal(earth_props.layers[end].rho) + for (name, value) in [ + ("Length", line_length), ("Freq", base_freq), ("Grnd resis", soil_rho)] + data_node = addelement!(comp_content, "data") + _set_attributes!(data_node, Dict("Name" => name, "Value" => value)) + end + + # Populate the LCC Sub-structure with CORRECTLY Structured Cable Data + lcc_node = addelement!(comp, "LCC") + _set_attributes!( + lcc_node, + Dict( + "NumPhases" => num_phases, + "IconLength" => "true", + "LineCablePipe" => 2, + "ModelType" => 1 + ) + ) + cable_header = addelement!(lcc_node, "cable_header") + _set_attributes!( + cable_header, + Dict("InAirGrnd" => 1, "MatrixOutput" => "true", "ExtraCG" => "$(num_phases)") + ) + + for (k, (design, position)) in enumerate(zip( + cable_system.designs, + cable_system.positions + )) + cable_node = addelement!(cable_header, "cable") + + components = atp_components(design) + num_components = length(components) + outermost = last(components) + outermost_radius = nominal(max( + outermost.conductor.r_ex, + outermost.dielectric.r_ex + )) + + _set_attributes!( + cable_node, + Dict( + "NumCond" => num_components, + "Rout" => outermost_radius, + "PosX" => nominal(position.x), + "PosY" => nominal(position.y) + ) + ) + + for component in components + conductor_node = addelement!(cable_node, "conductor") + + conductor = component.conductor + dielectric = component.dielectric + + rho_eq = conductor.material.rho + mu_r_cond = conductor.material.mu_r + mu_r_ins = dielectric.material.mu_r + eps_eq = dielectric.material.eps_r + + _set_attributes!( + conductor_node, + Dict( + "Rin" => nominal(conductor.r_in), + "Rout" => nominal(conductor.r_ex), + "rho" => nominal(rho_eq), + "muC" => nominal(mu_r_cond), + "muI" => nominal(mu_r_ins), + "epsI" => nominal(eps_eq), + "Cext" => nominal(dielectric.shunt_capacitance), + "Gext" => nominal(dielectric.shunt_conductance) + ) + ) + end + end + + # Write the completed XML document. + _set_attributes!(variables, Dict("NumSim" => 1, "IOPCVP" => 0, "UseParser" => "false")) + + open(file_name, "w") do fid + prettyprint(fid, doc) + end + @info "XML file saved to: $(_display_path(file_name))" + return file_name end +""" +$(TYPEDSIGNATURES) +Write frequency-indexed series-impedance and shunt-admittance matrices to an +ATPDraw `ZY` XML file. -# TODO: Develop `.lis` import and tests -# Issue URL: https://github.com/Electa-Git/LineCableModels.jl/issues/12 -function read_data end -# I TEST THEREFORE I EXIST -# I DON´T TEST THEREFORE GO TO THE GARBAGE -# """ -# read_atp_data(file_name::String, cable_system::LineCableSystem) - -# Reads an ATP `.lis` output file, extracts the Ze and Zi matrices, and dynamically -# reorders them to a grouped-by-phase format based on the provided `cable_system` -# structure. It correctly handles systems with a variable number of components per cable. - -# # Arguments -# - `file_name`: The path to the `.lis` file. -# - `cable_system`: The `LineCableSystem` object corresponding to the data in the file. - -# # Returns -# - `Array{T, 2}`: A 2D complex matrix representing the total reordered series -# impedance `Z = Ze + Zi` for a single frequency. -# - `nothing`: If the file cannot be found, parsed, or if the matrix dimensions in the -# file do not match the provided `cable_system` structure. -# """ -# function read_data(::Val{:atp}, -# cable_system::LineCableSystem, -# freq::AbstractFloat; -# file_name::String="$(cable_system.system_id)_1.lis" -# )::Union{Array{COMPLEXSCALAR,2},Nothing} -# # --- Inner helper function to parse a matrix block from text lines --- -# function parse_block(block_lines::Vector{String}) -# data_lines = filter(line -> !isempty(strip(line)), block_lines) -# if isempty(data_lines) -# return Matrix{ComplexF64}(undef, 0, 0) -# end -# matrix_size = length(split(data_lines[1])) -# real_parts = zeros(Float64, matrix_size, matrix_size) -# imag_parts = zeros(Float64, matrix_size, matrix_size) -# row_counter = 1 -# for i in 1:2:length(data_lines) -# if i + 1 > length(data_lines) -# break -# end -# real_line, imag_line = data_lines[i], data_lines[i+1] -# try -# real_parts[row_counter, :] = [parse(Float64, s) for s in split(real_line)[1:matrix_size]] -# imag_parts[row_counter, :] = [parse(Float64, s) for s in split(imag_line)[1:matrix_size]] -# catch e -# @error "Parsing failed" exception = (e, catch_backtrace()) -# return nothing -# end -# row_counter += 1 -# if row_counter > matrix_size -# break -# end -# end -# return real_parts + im * imag_parts -# end - -# # --- Main Function Logic --- -# if !isfile(file_name) -# @error "File not found: $file_name" -# return nothing -# end -# lines = readlines(file_name) -# ze_start_idx = findfirst(occursin.("Earth impedance [Ze]", lines)) -# zi_start_idx = findfirst(occursin.("Conductor internal impedance [Zi]", lines)) -# if isnothing(ze_start_idx) || isnothing(zi_start_idx) -# @error "Could not find Ze/Zi headers." -# return nothing -# end - -# Ze = parse_block(lines[ze_start_idx+1:zi_start_idx-1]) -# Zi = parse_block(lines[zi_start_idx+1:end]) -# if isnothing(Ze) || isnothing(Zi) -# return nothing -# end - -# # --- DYNAMICALLY GENERATE PERMUTATION INDICES (Numerical Method) --- -# component_counts = [length(c.design_data.components) for c in cable_system.cables] -# total_conductors = sum(component_counts) -# num_phases = length(component_counts) -# max_components = isempty(component_counts) ? 0 : maximum(component_counts) - -# if size(Ze, 1) != total_conductors -# @error "Matrix size from file ($(size(Ze,1))x$(size(Ze,1))) does not match total components in cable_system ($total_conductors)." -# return nothing -# end - -# num_conductors_per_type = [sum(c >= i for c in component_counts) for i in 1:max_components] -# type_offsets = cumsum([0; num_conductors_per_type[1:end-1]]) - -# permutation_indices = Int[] -# sizehint!(permutation_indices, total_conductors) -# instance_counters = ones(Int, max_components) -# for phase_idx in 1:num_phases -# for comp_type_idx in 1:component_counts[phase_idx] -# instance = instance_counters[comp_type_idx] -# original_idx = type_offsets[comp_type_idx] + instance -# push!(permutation_indices, original_idx) -# instance_counters[comp_type_idx] += 1 -# end -# end - -# Ze_reordered = Ze[permutation_indices, permutation_indices] -# Zi_reordered = Zi[permutation_indices, permutation_indices] - -# return Ze_reordered + Zi_reordered -# end - - -"""$(TYPEDSIGNATURES) - -Export calculated [`LineParameters`](@ref) (series impedance **Z** and shunt admittance **Y**) to an **compliant** `ZY` XML file. - -This routine writes the complex **Z** and **Y** matrices versus frequency into a compact XML -structure understood by external tools. Rows are emitted as comma‑separated complex entries -(`R+Xi` / `G+Bi`) with one ``/`` block per frequency sample. +Each frequency produces one `` and one `` block. Matrix rows use +comma-separated `R+Xi` and `G+Bi` entries. # Arguments -- `::Val{:atp}`: Backend selector for the ATP/ATPDraw ZY exporter. -- `line_params::LineParameters`: Object holding the frequency‑dependent matrices `Z[:,:,k]`, `Y[:,:,k]`, and `f[k]` in `line_params.f`. -- `file_name::String = "ZY_export.xml"`: Output file name or path. If relative, it is resolved against the exporter’s source directory. The absolute path of the saved file is returned. -- `cable_system::Union{LineCableSystem,Nothing} = nothing`: Optional system used only to derive a default name. When provided and `file_name` is not overridden, the exporter uses `"\$(cable_system.system_id)_ZY_export.xml"`. +- `::Val{:atp}`: ATP format selector. +- `line_params`: frequency-dependent matrices and frequency vector. -# Behavior +# Keywords -1. The root tag `` includes `NumPhases`, `Length` (fixed to `1.0`), and format attributes `ZFmt="R+Xi"`, `YFmt="G+Bi"`. -2. For each frequency `fᵏ = line_params.f[k]`: +- `file_name`: output path. Default: `ZY_export.xml` beside this exporter. +- `cable_system`: optional system supplying the line length and output-name + prefix. Default: `nothing`. - * Emit a `` block with `num_phases` lines, each line the `k`‑th slice of row `i` formatted as `real(Z[i,j,k]) + imag(Z[i,j,k])i`. - * Emit a `` block in the same fashion (default `G+Bi`). -3. Close the `` element and write to disk. On I/O error the function logs and returns `nothing`. +# Returns + +- The absolute output path. # Units Units are printed in the XML file according to the ATPDraw specifications: - `freq` (XML `Freq` attribute): \\[Hz\\] -- `Z` entries: \\[Ω/km\\] (per unit length) -- `Y` entries: \\[S/km\\] (per unit length) when `YFmt = "G+Bi"` -- XML `Length` attribute: \\[m\\] +- `Z` and `Y` entries retain the numerical basis stored by `line_params`. +- XML `Length`: cable-system length \\[m\\], or `1.0` when `cable_system` is + absent. # Notes -- The exporter assumes `size(line_params.Z, 1) == size(line_params.Z, 2) == size(line_params.Y, 1) == size(line_params.Y, 2)` and `length(line_params.f) == size(Z,3) == size(Y,3)`. -- Numeric types are stringified; mixed numeric backends (e.g., with uncertainties) are acceptable as long as they can be printed via `@sprintf`. -- This exporter **does not** modify or recompute matrices; it serializes exactly what is in `line_params`. - -# Examples - -```julia -# Z, Y, f have already been computed into `lp::LineParameters` -file = $(FUNCTIONNAME)(:atp, lp; file_name = "ZY_export.xml") -println("Exported ZY to: ", file) - -# Naming based on a cable system -file2 = $(FUNCTIONNAME)(:atp, lp; cable_system = sys) -println("Exported ZY to: ", file2) # => "\$(sys.system_id)_ZY_export.xml" -``` - -# See also +- [`nominal`](@ref) removes uncertainty before numeric values are written. -* [`LineParameters`](@ref) -* [`LineCableSystem`](@ref) -* [`export_data(::Val{:atp}, cable_system, ...)`](@ref) — exporter that writes full LCC input data """ function export_data(::Val{:atp}, - line_params::LineParameters; - file_name::Union{String, Nothing} = nothing, - cable_system::Union{LineCableSystem, Nothing} = nothing, -)::Union{String, Nothing} - - # Resolve final file_name while preserving any user-supplied path. - if isnothing(file_name) - # caller didn't supply a name -> derive from cable_system if present - if isnothing(cable_system) - file_name = joinpath(@__DIR__, "ZY_export.xml") - else - file_name = joinpath(@__DIR__, "$(cable_system.system_id)_ZY_export.xml") - end - else - # caller supplied a path/name -> respect directory, but prepend system_id to basename if cable_system provided - requested = isabspath(file_name) ? file_name : joinpath(@__DIR__, file_name) - if isnothing(cable_system) - file_name = requested - else - dir = dirname(requested) - base = basename(requested) - file_name = joinpath(dir, "$(cable_system.system_id)_$base") - end - end - - freq = line_params.f - - @debug ("ZY export called", - :method => "ZY", - :cable_system_isnothing => isnothing(cable_system), - :cable_system_type => (isnothing(cable_system) ? :nothing : typeof(cable_system)), - :file_name_in => file_name) - - cable_length = isnothing(cable_system) ? 1.0 : to_nominal(cable_system.line_length) - atp_format = "G+Bi" - # file_name = isabspath(file_name) ? file_name : joinpath(@__DIR__, file_name) - - open(file_name, "w") do fid - num_phases = size(line_params.Z, 1) - y_fmt = (atp_format == "C") ? "C" : "G+Bi" - - @printf( - fid, - "\n", - num_phases, - cable_length, - y_fmt - ) - - # --- Z Matrix Printing --- - for (k, freq_val) in enumerate(freq) - @printf(fid, " \n", to_nominal(freq_val)) - for i in 1:num_phases - row_str = join( - [ - @sprintf( - "%.16E%+.16Ei", - to_nominal(real(line_params.Z[i, j, k])), - to_nominal(imag(line_params.Z[i, j, k])) - ) for j in 1:num_phases - ], - ",", - ) - println(fid, row_str) - end - @printf(fid, " \n") - end - - # --- Y Matrix Printing --- - if atp_format == "C" - freq1 = to_nominal(freq[1]) - @printf(fid, " \n", freq1) - for i in 1:num_phases - row_str = join( - [ - @sprintf( - "%.16E", - to_nominal(imag(line_params.Y[i, j, 1]) / (2 * pi * freq1)) - ) for j in 1:num_phases - ], - ",", - ) - println(fid, row_str) - end - @printf(fid, " \n") - else # Case for "G+Bi" - for (k, freq_val) in enumerate(freq) - @printf(fid, " \n", to_nominal(freq_val)) - for i in 1:num_phases - row_str = join( - [ - @sprintf( - "%.16E%+.16Ei", - to_nominal(real(line_params.Y[i, j, k])), - to_nominal(imag(line_params.Y[i, j, k])) - ) for j in 1:num_phases - ], - ",", - ) - println(fid, row_str) - end - @printf(fid, " \n") - end - end - - # --- Footer --- - println(fid, "") - end - try - # Use pretty print option for debugging comparisons if needed - # open(filename, "w") do io; prettyprint(io, doc); end - if isfile(file_name) - @info "XML file saved to: $(display_path(file_name))" - end - return file_name - catch e - @error "Failed to write XML file '$(display_path(file_name))': $(e)" - isa(e, SystemError) && println("SystemError details: ", e.extrainfo) - return nothing - rethrow(e) # Rethrow to indicate failure clearly - end + line_params::LineParameters; + file_name::Union{String, Nothing} = nothing, + cable_system::Union{LineCableSystem, Nothing} = nothing +)::String + + # Resolve final file_name while preserving any user-supplied path. + if isnothing(file_name) + # Derive the name from cable_system when the caller omits it. + if isnothing(cable_system) + file_name = joinpath(@__DIR__, "ZY_export.xml") + else + file_name = joinpath(@__DIR__, "$(cable_system.system_id)_ZY_export.xml") + end + else + # caller supplied a path and name -> respect directory, but prepend system_id to basename if cable_system provided + requested = isabspath(file_name) ? file_name : joinpath(@__DIR__, file_name) + if isnothing(cable_system) + file_name = requested + else + dir = dirname(requested) + base = basename(requested) + file_name = joinpath(dir, "$(cable_system.system_id)_$base") + end + end + + freq = observe(line_params, frequencies) + + @debug ("ZY export called", + :method => "ZY", + :cable_system_isnothing => isnothing(cable_system), + :cable_system_type => (isnothing(cable_system) ? :nothing : typeof(cable_system)), + :file_name_in => file_name) + + cable_length = isnothing(cable_system) ? 1.0 : nominal(cable_system.line_length) + atp_format = "G+Bi" + # file_name = isabspath(file_name) ? file_name : joinpath(@__DIR__, file_name) + + open(file_name, "w") do fid + num_phases = size(observe(line_params, Z), 1) + y_fmt = (atp_format == "C") ? "C" : "G+Bi" + + @printf(fid, + "\n", + num_phases, + cable_length, + y_fmt) + + # Z Matrix Printing + for (k, freq_val) in enumerate(freq) + @printf(fid, " \n", nominal(freq_val)) + for i in 1:num_phases + row_str = join( + [@sprintf("%.16E%+.16Ei", + nominal(real(observe(line_params, Z, i, j, k))), + nominal(imag(observe(line_params, Z, i, j, k)))) + for j in 1:num_phases], + "," + ) + println(fid, row_str) + end + @printf(fid, " \n") + end + + # Y Matrix Printing + if atp_format == "C" + freq1 = nominal(freq[1]) + @printf(fid, " \n", freq1) + for i in 1:num_phases + row_str = join( + [@sprintf("%.16E", + nominal(observe(line_params, C, i, j, 1))) + for j in 1:num_phases], + "," + ) + println(fid, row_str) + end + @printf(fid, " \n") + else # Case for "G+Bi" + for (k, freq_val) in enumerate(freq) + @printf(fid, " \n", nominal(freq_val)) + for i in 1:num_phases + row_str = join( + [@sprintf("%.16E%+.16Ei", + nominal(real(observe(line_params, Y, i, j, k))), + nominal(imag(observe(line_params, Y, i, j, k)))) + for j in 1:num_phases], + "," + ) + println(fid, row_str) + end + @printf(fid, " \n") + end + end + + # Footer + println(fid, "") + end + @info "XML file saved to: $(_display_path(file_name))" + return file_name end diff --git a/src/importexport/cableslibrary.jl b/src/importexport/cableslibrary.jl index 421cbc528..1cf0d7849 100644 --- a/src/importexport/cableslibrary.jl +++ b/src/importexport/cableslibrary.jl @@ -1,680 +1,132 @@ """ $(TYPEDSIGNATURES) -Saves a [`CablesLibrary`](@ref) to a file. -The format is determined by the file extension: -- `.json`: Saves using the custom JSON serialization. -- `.jls`: Saves using Julia native binary serialization. - -# Arguments -- `library`: The [`CablesLibrary`](@ref) instance to save. -- `file_name`: The path to the output file (default: "cables_library.json"). - -# Returns -- The absolute path of the saved file, or `nothing` on failure. +Save a cable library as versioned JSON or trusted Julia serialization (`.jls`). +JLS input must come from a trusted source and use matching package types. """ function save( - library::CablesLibrary; - file_name::String = "cables_library.json", -)::Union{String, Nothing} - - file_name = isabspath(file_name) ? file_name : joinpath(@__DIR__, file_name) - - _, ext = splitext(file_name) - ext = lowercase(ext) - - try - if ext == ".jls" - return _save_cableslibrary_jls(library, file_name) - elseif ext == ".json" - return _save_cableslibrary_json(library, file_name) - else - @warn "Unrecognized file extension '$ext' for CablesLibrary. Defaulting to .json format." - # Ensure filename has .json extension if defaulting - if ext != ".json" - file_name = file_name * ".json" - end - return _save_cableslibrary_json(library, file_name) - end - catch e - @error "Error saving CablesLibrary to '$(display_path(file_name))': $e" - showerror(stderr, e, catch_backtrace()) - println(stderr) - return nothing - end -end - -""" -$(TYPEDSIGNATURES) - -Saves the [`CablesLibrary`](@ref) using Julia native binary serialization. -This format is generally not portable across Julia versions or machine architectures -but can be faster and preserves exact types. - -# Arguments -- `library`: The [`CablesLibrary`](@ref) instance. -- `file_name`: The output file path (should end in `.jls`). - -# Returns -- The absolute path of the saved file. -""" -function _save_cableslibrary_jls(library::CablesLibrary, file_name::String)::String - # Note: Serializing the whole library object directly might be problematic - # if the library struct itself changes. Serializing the core data (designs) is safer. - serialize(file_name, library.data) - @info "Cables library saved using Julia serialization to: $(display_path(file_name))" - return abspath(file_name) + library::CablesLibrary; + file_name::String = "cables_library.json" +) + extension = lowercase(splitext(file_name)[2]) + if extension == ".jls" + Serialization.serialize(file_name, ( + designs = library.data, + datasheets = library.datasheets + )) + return abspath(file_name) + end + path = _json_path(file_name) + open(path, "w") do io + #! explicit-imports: off + # JSON3 exposes this established writer without a public marker. + JSON3.pretty(io, _json_document(library); allow_inf = true) + #! explicit-imports: on + end + return abspath(path) end """ $(TYPEDSIGNATURES) -Saves the [`CablesLibrary`](@ref) to a JSON file using the custom serialization logic. - -# Arguments -- `library`: The [`CablesLibrary`](@ref) instance. -- `file_name`: The output file path (should end in `.json`). - -# Returns -- The absolute path of the saved file. -""" -function _save_cableslibrary_json(library::CablesLibrary, file_name::String)::String - # Use the generic _serialize_value, which will delegate to _serialize_obj - # for the library object, which in turn uses _serializable_fields(::CablesLibrary) - serialized_library = _serialize_value(library) - - open(file_name, "w") do io - # Use JSON3.pretty for human-readable output - # allow_inf=true is needed if Measurements or other fields might contain Inf - JSON3.pretty(io, serialized_library, allow_inf = true) - end - if isfile(file_name) - @info "Cables library saved to: $(display_path(file_name))" - end - return abspath(file_name) -end - -""" -$(TYPEDSIGNATURES) - -Loads cable designs from a file into an existing [`CablesLibrary`](@ref) object. -Modifies the library in-place. -The format is determined by the file extension: -- `.json`: Loads using the custom JSON deserialization and reconstruction. -- `.jls`: Loads using Julia's native binary deserialization. - -# Arguments -- `library`: The [`CablesLibrary`](@ref) instance to populate (modified in-place). -- `file_name`: Path to the file to load (default: "cables_library.json"). - -# Returns -- The modified [`CablesLibrary`](@ref) instance. +Atomically replace a cable library from supported JSON or trusted JLS data. """ function load!( - library::CablesLibrary; # Type annotation ensures it's the correct object - file_name::String = "cables_library.json", -)::CablesLibrary # Return the modified library - if !isfile(file_name) - throw(ErrorException("Cables library file not found: '$(display_path(file_name))'")) # make caller receive an Exception - end - - _, ext = splitext(file_name) - ext = lowercase(ext) - - try - if ext == ".jls" - _load_cableslibrary_jls!(library, file_name) - elseif ext == ".json" - _load_cableslibrary_json!(library, file_name) - else - @warn "Unrecognized file extension '$ext' for CablesLibrary. Attempting to load as .json." - _load_cableslibrary_json!(library, file_name) - end - catch e - @error "Error loading CablesLibrary from '$(display_path(file_name))': $e" - showerror(stderr, e, catch_backtrace()) - println(stderr) - # Optionally clear the library or leave it partially loaded depending on desired robustness - # empty!(library.data) - end - return library # Return the modified library + library::CablesLibrary; + file_name::String = "cables_library.json" +) + isfile(file_name) || throw(ArgumentError( + "cables library file not found: '$(_display_path(file_name))'", + )) + extension = lowercase(splitext(file_name)[2]) + decoded_library = if extension == ".jls" + _trusted_cable_data(Serialization.deserialize(file_name)) + elseif extension == ".json" + document = _read_document(file_name, CABLES_SCHEMA) + materials = _document_materials(document) + root = _required(document, "root", CABLES_SCHEMA) + get(root, "kind", nothing) == "cable_library" || throw(ArgumentError( + "cable document root must have kind 'cable_library'" + )) + raw_cables = _required(root, "cables", "cable_library") + raw_cables isa AbstractDict || throw(ArgumentError( + "cable_library cables must be an object" + )) + _decoded_cable_library(raw_cables, materials) + else + throw(ArgumentError("CablesLibrary loading requires a .json or .jls file")) + end + library.data = decoded_library.designs + library.datasheets = decoded_library.datasheets + return library end -""" -$(TYPEDSIGNATURES) - -Loads cable designs from a Julia binary serialization file (`.jls`) -into the provided library object. - -# Arguments -- `library`: The [`CablesLibrary`](@ref) instance to modify. -- `file_name`: The path to the `.jls` file. - -# Returns -- Nothing. Modifies `library` in-place. -""" -function _load_cableslibrary_jls!(library::CablesLibrary, file_name::String) - loaded_data = deserialize(file_name) - - if isa(loaded_data, Dict{String, CableDesign}) - # Replace the existing designs - library.data = loaded_data - println( - "Cables library successfully loaded via Julia deserialization from: ", - display_path(file_name), - ) - else - # This indicates the .jls file did not contain the expected dictionary structure - @error "Invalid data format in '$(display_path(file_name))'. Expected Dict{String, CableDesign}, got $(typeof(loaded_data)). Library not loaded." - # Ensure library.data exists if it was potentially wiped before load attempt - if !isdefined(library, :data) || !(library.data isa AbstractDict) - library.data = Dict{String, CableDesign}() - end - end - return nothing +function _decode_datasheet(value) + value isa AbstractDict || throw(ArgumentError( + "a cable datasheet record must be an object" + )) + names = sort!(Symbol.(collect(keys(value))); by = String) + entries = Tuple(deserialize_value(value[String(name)]) for name in names) + return DatasheetInfo(NamedTuple{Tuple(names)}(entries)) end -""" -$(TYPEDSIGNATURES) - -Loads cable designs from a JSON file into the provided library object -using the detailed, sequential reconstruction logic. - -# Arguments -- `library`: The [`CablesLibrary`](@ref) instance to modify. -- `file_name`: The path to the `.json` file. - -# Returns -- Nothing. Modifies `library` in-place. -""" -function _load_cableslibrary_json!(library::CablesLibrary, file_name::String) - # Ensure library structure is initialized - if !isdefined(library, :data) || !(library.data isa AbstractDict) - @warn "Library 'data' field was not initialized or not a Dict. Initializing." - library.data = Dict{String, CableDesign}() - else - # Clear existing designs before loading (common behavior) - empty!(library.data) - end - - # Load the entire JSON structure - json_data = open(file_name, "r") do io - JSON3.read(io, Dict{String, Any}) # Read the top level as a Dict - end - - # The JSON might store designs directly under "data" key, - # or the top level might be the dictionary of designs itself. - local designs_to_process::Dict - if haskey(json_data, "data") && json_data["data"] isa AbstractDict - # Standard case: designs are under the "data" key - designs_to_process = json_data["data"] - elseif haskey(json_data, "__julia_type__") && - occursin("CablesLibrary", json_data["__julia_type__"]) && - haskey(json_data, "data") - # Case where the entire library object was serialized - designs_to_process = json_data["data"] - elseif all( - v -> - v isa AbstractDict && haskey(v, "__julia_type__") && - occursin("CableDesign", v["__julia_type__"]), - values(json_data), - ) - # Fallback: Assume the top-level dict *is* the designs dict - @info "Assuming top-level JSON object in '$(display_path(file_name))' is the dictionary of cable designs." - designs_to_process = json_data - else - @error "JSON file '$(display_path(file_name))' does not contain a recognizable 'data' dictionary or structure." - return nothing # Exit loading process - end - - @info "Loading cable designs from JSON: '$(display_path(file_name))'..." - num_loaded = 0 - num_failed = 0 - - # Process each cable design entry using manual reconstruction - for (cable_id, design_data) in designs_to_process - if !(design_data isa AbstractDict) - @warn "Skipping entry '$cable_id': Invalid data format (expected Dictionary, got $(typeof(design_data)))." - num_failed += 1 - continue - end - try - # Reconstruct the design using the dedicated function - reconstructed_design = - _reconstruct_cabledesign(string(cable_id), design_data) - # Store the fully reconstructed design in the library - library.data[string(cable_id)] = reconstructed_design - num_loaded += 1 - catch e - num_failed += 1 - @error "Failed to reconstruct cable design '$cable_id': $e" - # Show stacktrace for detailed debugging, especially for MethodErrors during construction - showerror(stderr, e, catch_backtrace()) - println(stderr) # Add newline for clarity - end - end - - @info "Finished loading from '$(display_path(file_name))'. Successfully loaded $num_loaded cable designs, failed to load $num_failed." - return nothing +function _decoded_cable_library(raw_cables, materials) + designs = Dict{String, CableDesign}() + datasheets = Dict{String, DatasheetInfo}() + for (name, value) in raw_cables + cable_id = String(name) + design = _decode_design(value, materials) + design isa CableDesign || throw(ArgumentError( + "cable '$cable_id' decoded as $(typeof(design)), not CableDesign" + )) + cable_id == design.cable_id || throw(ArgumentError( + "cable key '$cable_id' differs from cable_id '$(design.cable_id)'" + )) + designs[cable_id] = validate(design) + datasheets[cable_id] = _decode_datasheet( + _required(value, "datasheet", "cable_design") + ) + end + return (; designs, datasheets) end -""" -$(TYPEDSIGNATURES) - -Helper function to reconstruct a [`ConductorGroup`](@ref) or [`InsulatorGroup`](@ref) object with the first layer of the respective [`AbstractCablePart`](@ref). Subsequent layers are added using `add!` methods. - -# Arguments -- `layer_data`: Dictionary containing the data for the first layer, parsed from JSON. - -# Returns -- A reconstructed [`ConductorGroup`](@ref) object with the initial [`AbstractCablePart`](@ref). - -# Throws -- Error if essential data is missing or the layer type is unsupported. -""" -function _reconstruct_partsgroup(layer_data::Dict) - - if !haskey(layer_data, "__julia_type__") - Base.error("Layer data missing '__julia_type__' key: $layer_data") - end - type_str = layer_data["__julia_type__"] - LayerType = _resolve_type(type_str) - - # Use generic deserialization for the whole layer data first. - # _deserialize_value now returns Dict{Symbol, Any} for plain dicts - local deserialized_layer_dict::Dict{Symbol, Any} - try - # Temporarily remove type key to avoid recursive loop in _deserialize_value -> _deserialize_obj - temp_data = filter(p -> p.first != "__julia_type__", layer_data) - deserialized_layer_dict = _deserialize_value(temp_data) # Should return Dict{Symbol, Any} - catch e - # This fallback might not be strictly needed anymore if _deserialize_value is robust, - # but kept for safety. It also needs to produce Dict{Symbol, Any}. - @error "Initial deserialization failed for first layer data ($type_str): $e. Trying manual field extraction." - deserialized_layer_dict = Dict{Symbol, Any}() - for (k_str, v) in layer_data # k_str is String from JSON parsing - if k_str != "__julia_type__" - deserialized_layer_dict[Symbol(k_str)] = _deserialize_value(v) # Deserialize value, use Symbol key - end - end - end - - # Ensure the result is Dict{Symbol, Any} - if !(deserialized_layer_dict isa Dict{Symbol, Any}) - error( - "Internal error: deserialized_layer_dict is not Dict{Symbol, Any}, but $(typeof(deserialized_layer_dict))", - ) - end - - # Extract necessary fields using get with Symbol keys - r_in = get_as(deserialized_layer_dict, :r_in, missing, BASE_FLOAT) - material_props = get_as(deserialized_layer_dict, :material_props, missing, BASE_FLOAT) - temperature = get_as(deserialized_layer_dict, :temperature, T₀, BASE_FLOAT) - - # Check for essential properties common to most first layers - ismissing(r_in) && - Base.error( - "Missing 'r_in' for first layer type $LayerType in data: $layer_data", - ) - ismissing(material_props) && error( - "Missing 'material_props' for first layer type $LayerType in data: $layer_data", - ) - !(material_props isa Material) && error( - "'material_props' did not deserialize to a Material object for first layer type $LayerType. Got: $(typeof(material_props))", - ) - - - # Type-specific reconstruction using POSITIONAL constructors + Keywords - # This requires knowing the exact constructor signatures. - try - - if LayerType == CircStrands - radius_wire = get_as(deserialized_layer_dict, :radius_wire, missing, BASE_FLOAT) - num_wires = get_as(deserialized_layer_dict, :num_wires, missing, Int) - lay_ratio = get_as(deserialized_layer_dict, :lay_ratio, missing, BASE_FLOAT) - lay_direction = get_as(deserialized_layer_dict, :lay_direction, 1, Int) # Default lay_direction - # Validate required fields - any(ismissing, (radius_wire, num_wires, lay_ratio)) && error( - "Missing required field(s) (radius_wire, num_wires, lay_ratio) for CircStrands first layer.", - ) - # Ensure num_wires is Int - num_wires_int = isa(num_wires, Int) ? num_wires : Int(num_wires) - lay_direction_int = isa(lay_direction, Int) ? lay_direction : Int(lay_direction) - return CircStrands( - r_in, - radius_wire, - num_wires_int, - lay_ratio, - material_props; - temperature = temperature, - lay_direction = lay_direction_int, - ) - elseif LayerType == Tubular - r_ex = get_as(deserialized_layer_dict, :r_ex, missing, BASE_FLOAT) - ismissing(r_ex) && - Base.error("Missing 'r_ex' for Tubular first layer.") - return Tubular( - r_in, r_ex, material_props; temperature = temperature) - elseif LayerType == Strip - r_ex = get_as(deserialized_layer_dict, :r_ex, missing, BASE_FLOAT) - width = get_as(deserialized_layer_dict, :width, missing, BASE_FLOAT) - lay_ratio = get_as(deserialized_layer_dict, :lay_ratio, missing, BASE_FLOAT) - lay_direction = get(deserialized_layer_dict, :lay_direction, 1) - any(ismissing, (r_ex, width, lay_ratio)) && error( - "Missing required field(s) (r_ex, width, lay_ratio) for Strip first layer.", - ) - lay_direction_int = isa(lay_direction, Int) ? lay_direction : Int(lay_direction) - - return Strip( - r_in, - r_ex, - width, - lay_ratio, - material_props; - temperature = temperature, - lay_direction = lay_direction_int, - ) - elseif LayerType == Insulator - r_ex = get_as(deserialized_layer_dict, :r_ex, missing, BASE_FLOAT) - ismissing(r_ex) && - Base.error("Missing 'r_ex' for Insulator first layer.") - return Insulator( - r_in, - r_ex, - material_props; - temperature = temperature, - ) - elseif LayerType == Semicon - r_ex = get_as(deserialized_layer_dict, :r_ex, missing, BASE_FLOAT) - ismissing(r_ex) && - Base.error("Missing 'r_ex' for Semicon first layer.") - return Semicon(r_in, r_ex, material_props; temperature = temperature) - elseif LayerType == Sector - params = get_as(deserialized_layer_dict, :params, missing, BASE_FLOAT) - rotation_angle_deg = get_as(deserialized_layer_dict, :rotation_angle_deg, missing, BASE_FLOAT) - - ismissing(params) && Base.error("Missing 'params' for Sector in data: $layer_data") - !(params isa SectorParams) && error("'params' did not deserialize to a SectorParams object. Got: $(typeof(params))") - ismissing(rotation_angle_deg) && Base.error("Missing 'rotation_angle_deg' for Sector in data: $layer_data") - - return Sector(params, rotation_angle_deg, material_props; temperature=temperature) - elseif LayerType == SectorInsulator - inner_sector = get_as(deserialized_layer_dict, :inner_sector, missing, BASE_FLOAT) - thickness = get_as(deserialized_layer_dict, :thickness, missing, BASE_FLOAT) - - ismissing(inner_sector) && Base.error("Missing 'inner_sector' for SectorInsulator in data: $layer_data") - !(inner_sector isa Sector) && error("'inner_sector' did not deserialize to a Sector object. Got: $(typeof(inner_sector))") - ismissing(thickness) && Base.error("Missing 'thickness' for SectorInsulator in data: $layer_data") - - return SectorInsulator(inner_sector, thickness, material_props; temperature=temperature) - else - Base.error("Unsupported layer type for first layer reconstruction: $LayerType") - end - catch e - @error "Construction failed for first layer of type $LayerType with data: $deserialized_layer_dict. Error: $e" - rethrow(e) - end +function _decoded_cable_data(decoded) + decoded isa AbstractDict || throw(ArgumentError( + "the cables field must be a JSON object", + )) + designs = Dict{String, CableDesign}() + for (name, design) in decoded + design isa CableDesign || throw(ArgumentError( + "cable '$name' decoded as $(typeof(design)), not CableDesign", + )) + String(name) == design.cable_id || throw(ArgumentError( + "cable key '$name' differs from cable_id '$(design.cable_id)'", + )) + designs[String(name)] = validate(design) + end + return designs end -""" -$(TYPEDSIGNATURES) - -Reconstructs a complete [`CableDesign`](@ref) object from its dictionary representation (parsed from JSON). -This function handles the sequential process of building cable designs: - 1. Deserialize [`NominalData`](@ref). - 2. Iterate through components. - 3. For each component: -    a. Reconstruct the first layer of the conductor group. -    b. Create the [`ConductorGroup`](@ref) with the first layer. -    c. Add subsequent conductor layers using [`add!`](@ref). -    d. Repeat a-c for the [`InsulatorGroup`](@ref). -    e. Create the [`CableComponent`](@ref). - 4. Create the [`CableDesign`](@ref) with the first component. - 5. Add subsequent components using [`add!`](@ref). - -# Arguments -- `cable_id`: The identifier string for the cable design. -- `design_data`: Dictionary containing the data for the cable design. - -# Returns -- A fully reconstructed [`CableDesign`](@ref) object. - -# Throws -- Error if reconstruction fails at any step. -""" -function _reconstruct_cabledesign( - cable_id::String, - design_data::Dict, -)::CableDesign - @info "Reconstructing CableDesign: $cable_id" - - # 1. Reconstruct NominalData using generic deserialization - local nominal_data::NominalData - if haskey(design_data, "nominal_data") - # Ensure the input to _deserialize_value is the Dict for NominalData - nominal_data_dict = design_data["nominal_data"] - if !(nominal_data_dict isa AbstractDict) - error( - "Invalid format for 'nominal_data' in $cable_id: Expected Dictionary, got $(typeof(nominal_data_dict))", - ) - end - nominal_data_val = _deserialize_value(nominal_data_dict) - if !(nominal_data_val isa NominalData) - # This error check relies on _deserialize_value returning the original dict on failure - error( - "Field 'nominal_data' did not deserialize to a NominalData object for $cable_id. Got: $(typeof(nominal_data_val))", - ) - end - nominal_data = nominal_data_val - @info " Reconstructed NominalData" - else - @warn "Missing 'nominal_data' for $cable_id. Using default NominalData()." - nominal_data = NominalData() # Use default if missing - end - - # 2. Process Components Sequentially - components_data = get(design_data, "components", []) - if isempty(components_data) || !(components_data isa AbstractVector) - Base.error("Missing or invalid 'components' array in design data for $cable_id") - end - - reconstructed_components = CableComponent[] # Store fully built components - - - for (idx, comp_data) in enumerate(components_data) - if !(comp_data isa AbstractDict) - @warn "Component data at index $idx for $cable_id is not a dictionary. Skipping." - continue - end - comp_id = get(comp_data, "id", "UNKNOWN_COMPONENT_ID_$idx") - @info " Processing Component $idx: $comp_id" - - # --- 2.1 Build Conductor Group --- - local conductor_group::ConductorGroup - conductor_group_data = get(comp_data, "conductor_group", Dict()) - cond_layers_data = get(conductor_group_data, "layers", []) - - if isempty(cond_layers_data) || !(cond_layers_data isa AbstractVector) - Base.error( - "Component '$comp_id' has missing or invalid conductor group layers.", - ) - end - - # - Create the FIRST layer object - # Ensure the input to _reconstruct_partsgroup is the Dict for the layer - first_layer_dict = cond_layers_data[1] - if !(first_layer_dict isa AbstractDict) - error( - "Invalid format for first conductor layer in component '$comp_id': Expected Dictionary, got $(typeof(first_layer_dict))", - ) - end - first_cond_layer = _reconstruct_partsgroup(first_layer_dict) - - # - Initialize ConductorGroup using its constructor with the first layer - conductor_group = ConductorGroup(first_cond_layer) - @info " Created ConductorGroup with first layer: $(typeof(first_cond_layer))" - - # - Add remaining layers using add! - for i in 2:lastindex(cond_layers_data) - layer_data = cond_layers_data[i] - if !(layer_data isa AbstractDict) - @warn "Conductor layer data at index $i for component $comp_id is not a dictionary. Skipping." - continue - end - - # Extract Type and necessary arguments for add! - LayerType = _resolve_type(layer_data["__julia_type__"]) - material_props = get_as(layer_data, "material_props", missing, BASE_FLOAT) - material_props isa Material || Base.error( - "'material_props' must deserialize to Material, got $(typeof(material_props))", - ) - - # Prepare args and kwargs based on LayerType for add! - args = [] - kwargs = Dict{Symbol, Any}() - kwargs[:temperature] = get_as(layer_data, "temperature", T₀, BASE_FLOAT) - if haskey(layer_data, "lay_direction") # Only add if present - kwargs[:lay_direction] = get_as(layer_data, "lay_direction", 1, Int) - end - - # Extract type-specific arguments needed by add! - try - if LayerType == CircStrands - radius_wire = get_as(layer_data, "radius_wire", missing, BASE_FLOAT) - num_wires = get_as(layer_data, "num_wires", missing, Int) - lay_ratio = get_as(layer_data, "lay_ratio", missing, BASE_FLOAT) - any(ismissing, (radius_wire, num_wires, lay_ratio)) && error( - "Missing required field(s) for CircStrands layer $i in $comp_id", - ) - args = [radius_wire, num_wires, lay_ratio, material_props] - elseif LayerType == Tubular - r_ex = get_as(layer_data, "r_ex", missing, BASE_FLOAT) - ismissing(r_ex) && - Base.error("Missing 'r_ex' for Tubular layer $i in $comp_id") - args = [r_ex, material_props] - elseif LayerType == Strip - r_ex = get_as(layer_data, "r_ex", missing, BASE_FLOAT) - width = get_as(layer_data, "width", missing, BASE_FLOAT) - lay_ratio = get_as(layer_data, "lay_ratio", missing, BASE_FLOAT) - any(ismissing, (r_ex, width, lay_ratio)) && - Base.error( - "Missing required field(s) for Strip layer $i in $comp_id", - ) - args = [r_ex, width, lay_ratio, material_props] - else - Base.error("Unsupported layer type '$LayerType' for add!") - end - - # Call add! with Type, args..., and kwargs... - add!(conductor_group, LayerType, args...; kwargs...) - @info " Added conductor layer $i: $LayerType" - catch e - @error "Failed to add conductor layer $i ($LayerType) to component $comp_id: $e" - println(stderr, " Layer Data: $layer_data") - println(stderr, " Args: $args") - println(stderr, " Kwargs: $kwargs") - rethrow(e) - end - end # End loop for conductor layers - - # --- 2.2 Build Insulator Group (Analogous logic) --- - local insulator_group::InsulatorGroup - insulator_group_data = get(comp_data, "insulator_group", Dict()) - insu_layers_data = get(insulator_group_data, "layers", []) - - if isempty(insu_layers_data) || !(insu_layers_data isa AbstractVector) - Base.error( - "Component '$comp_id' has missing or invalid insulator group layers.", - ) - end - - # - Create the FIRST layer object - first_layer_dict_insu = insu_layers_data[1] - if !(first_layer_dict_insu isa AbstractDict) - error( - "Invalid format for first insulator layer in component '$comp_id': Expected Dictionary, got $(typeof(first_layer_dict_insu))", - ) - end - first_insu_layer = _reconstruct_partsgroup(first_layer_dict_insu) - - # - Initialize InsulatorGroup - insulator_group = InsulatorGroup(first_insu_layer) - @info " Created InsulatorGroup with first layer: $(typeof(first_insu_layer))" - - # - Add remaining layers using add! - for i in 2:lastindex(insu_layers_data) - layer_data = insu_layers_data[i] - if !(layer_data isa AbstractDict) - @warn "Insulator layer data at index $i for component $comp_id is not a dictionary. Skipping." - continue - end - - LayerType = _resolve_type(layer_data["__julia_type__"]) - material_props = get_as(layer_data, "material_props", missing, BASE_FLOAT) - material_props isa Material || Base.error( - "'material_props' must deserialize to Material, got $(typeof(material_props))", - ) - - - args = [] - kwargs = Dict{Symbol, Any}() - kwargs[:temperature] = get_as(layer_data, "temperature", T₀, BASE_FLOAT) - - try - # All insulator types (Semicon, Insulator) take r_ex, material_props - # for the add! method. - if LayerType in [Semicon, Insulator] - r_ex = get_as(layer_data, "r_ex", missing, BASE_FLOAT) - ismissing(r_ex) && - Base.error( - "Missing 'r_ex' for $LayerType layer $i in $comp_id", - ) - args = [r_ex, material_props] - else - Base.error("Unsupported layer type '$LayerType' for add!") - end - - # Call add! with Type, args..., and kwargs... - add!(insulator_group, LayerType, args...; kwargs...) - @info " Added insulator layer $i: $LayerType" - catch e - @error "Failed to add insulator layer $i ($LayerType) to component $comp_id: $e" - println(stderr, " Layer Data: $layer_data") - println(stderr, " Args: $args") - println(stderr, " Kwargs: $kwargs") - rethrow(e) - end - end # End loop for insulator layers - - # --- 2.3 Create the CableComponent object --- - component = CableComponent(comp_id, conductor_group, insulator_group) - push!(reconstructed_components, component) - @info " Created CableComponent: $comp_id" - - end # End loop through components_data - - # 3. Create the final CableDesign object using the first component - if isempty(reconstructed_components) - Base.error( - "Failed to reconstruct any valid components for cable design '$cable_id'", - ) - end - # Use the CableDesign constructor which takes the first component - cable_design = - CableDesign(cable_id, reconstructed_components[1]; nominal_data = nominal_data) - @info " Created initial CableDesign with component: $(reconstructed_components[1].id)" - - # 4. Add remaining components to the design sequentially using add! - for i in 2:lastindex(reconstructed_components) - try - add!(cable_design, reconstructed_components[i]) - @info " Added component $(reconstructed_components[i].id) to CableDesign '$cable_id'" - catch e - @error "Failed to add component '$(reconstructed_components[i].id)' to CableDesign '$cable_id': $e" - rethrow(e) - end - end - - @info "Finished Reconstructing CableDesign: $cable_id" - return cable_design +function _trusted_cable_data(decoded) + decoded isa NamedTuple && keys(decoded) == (:designs, :datasheets) || throw( + ArgumentError( + "trusted JLS cable data must contain designs and datasheets" + ) + ) + designs = _decoded_cable_data(decoded.designs) + decoded.datasheets isa AbstractDict || throw(ArgumentError( + "trusted JLS datasheet data must be a dictionary", + )) + datasheets = Dict{String, DatasheetInfo}() + for cable_id in keys(designs) + record = get(decoded.datasheets, cable_id) do + throw(KeyError(cable_id)) + end + record isa Union{DatasheetInfo, NamedTuple} || throw(ArgumentError( + "datasheet '$cable_id' must be DatasheetInfo or a named tuple" + )) + datasheets[cable_id] = + record isa DatasheetInfo ? record : DatasheetInfo(record) + end + return (; designs, datasheets) end diff --git a/src/importexport/deserialize.jl b/src/importexport/deserialize.jl index 88551db22..a461a13ea 100644 --- a/src/importexport/deserialize.jl +++ b/src/importexport/deserialize.jl @@ -1,328 +1,510 @@ -@inline function _resolve_dotted_in(path::String, root::Module) - cur = root - for p in split(path, '.') - s = Symbol(p) - if isdefined(cur, s) - cur = getfield(cur, s) - else - return nothing - end - end - return cur isa Type ? cur : nothing -end - -function _module_candidates() - pkg = parentmodule(@__MODULE__) # e.g., LineCableModels - cands = Module[@__MODULE__] - pkg !== nothing && push!(cands, pkg) - push!(cands, Main) - if pkg !== nothing - for name in (:DataModel, :Materials, :Engine, :EarthProps, :ImportExport) - if isdefined(pkg, name) - mod = getfield(pkg, name) - mod isa Module && push!(cands, mod) - end - end - end - return cands -end - - """ -$(TYPEDSIGNATURES) - -Resolves a fully qualified type name string (e.g., \"Module.Type\") -into a Julia `Type` object. - -Resolution order: -1) If fully-qualified (contains '.') and not parametric, walk modules (no eval). -2) If bare name (no '.' and not parametric), search candidate modules. -3) Fallback: parse + eval in `Main` (handles parametric types like `Vector{Float64}`). - -# Arguments - -- `type_str`: The string representation of the type. - -# Returns - -- The corresponding Julia `Type` object. - -# Throws - -- `Error` if the type cannot be resolved. +Decode an extension-owned tagged value from the v1 JSON format. """ -function _resolve_type(type_str::String) - pkg = parentmodule(@__MODULE__) - try - # 1) Fully-qualified, non-parametric: try walking - if occursin('.', type_str) && !occursin('{', type_str) - if (T = _resolve_dotted_in(type_str, Main)) !== nothing - return T - end - if pkg !== nothing - if (T = _resolve_dotted_in(type_str, pkg)) !== nothing - return T - end - end - end - - # 2) Bare name, non-parametric: search candidate modules - if !occursin('.', type_str) && !occursin('{', type_str) - sym = Symbol(type_str) - for m in _module_candidates() - if isdefined(m, sym) - val = getfield(m, sym) - if val isa Type - return val - end - end - end - end - - # 3) General case: parse + eval in package root (or Main as fallback) - return Base.eval(pkg === nothing ? Main : pkg, Meta.parse(type_str)) - catch e - @error "Could not resolve type '$type_str'" exception = (e, catch_backtrace()) - rethrow(e) - end -end -# function _resolve_type(type_str::String) -# try -# return Core.eval(@__MODULE__, Meta.parse(type_str)) -# catch e -# @error "Could not resolve type '$type_str'. Ensure module structure is correct and type is loaded in Main." -# rethrow(e) -# end -# end - -""" -$(TYPEDSIGNATURES) - -Deserializes a value from its JSON representation back into a Julia value. -Handles special type markers for `Measurements`, `Inf`/`NaN`, and custom structs -identified by `__julia_type__`. Ensures plain dictionaries use Symbol keys. - -# Arguments -- `value`: The JSON-parsed value (Dict, Vector, Number, String, Bool, Nothing). - -# Returns -- The deserialized Julia value. -""" -function _deserialize_value(value) - if value isa Dict - # Check for special type markers first - if haskey(value, "__type__") - type_marker = value["__type__"] - if type_marker == "Measurement" - # Reconstruct Measurement - uncval = get_as(value, "uncertainty", nothing, Measurement) - if isa(uncval, Measurement) - return uncval - else - @warn "Could not reconstruct Measurement from input: value=$(typeof(get_as(value, "value", nothing, BASE_FLOAT))), uncertainty=$(typeof(get_as(value, "uncertainty", nothing, BASE_FLOAT))). Returning original Dict." - return value # Return original dict if parts are invalid - end - - elseif type_marker == "SpecialFloat" - # Reconstruct Inf/NaN - val_str = get(value, "value", "") - if val_str == "Inf" - return Inf - end - if val_str == "-Inf" - return -Inf - end - if val_str == "NaN" - return NaN - end - @warn "Unknown SpecialFloat value: '$val_str'. Returning original Dict." - return value - - elseif type_marker == "Float" - return get_as(value, "value", nothing, BASE_FLOAT) - - elseif type_marker == "Int" - return get_as(value, "value", nothing, Int) - - elseif type_marker == "Complex" - return get_as(value, "value", nothing, Complex) - - else - @warn "Unknown __type__ marker: '$type_marker'. Processing as regular dictionary." - # Fall through to regular dictionary processing - end - end - - # Check for Julia object marker - if haskey(value, "__julia_type__") - type_str = value["__julia_type__"] - try - T = _resolve_type(type_str) - # Delegate object construction to _deserialize_obj - return _deserialize_obj(value, T) - catch e - # Catch errors specifically from _deserialize_obj or _resolve_type - @error "Failed to resolve or deserialize type '$type_str': $e. Returning original Dict." - showerror(stderr, e, catch_backtrace()) - println(stderr) - return value # Return original dict on error - end - end - return Dict(Symbol(k) => _deserialize_value(v) for (k, v) in value) - - elseif value isa Vector - # Recursively deserialize array elements - return [_deserialize_value(v) for v in value] - - else - # Basic JSON types (Number, String, Bool, Nothing) pass through - return value - - end +function deserialize_extension end + +_float_type(::Val{:Float16}) = Float16 +_float_type(::Val{:Float32}) = Float32 +_float_type(::Val{:Float64}) = Float64 +_float_type(::Val{:BigFloat}) = BigFloat + +function _required(value, name::AbstractString, owner) + haskey(value, name) || throw(ArgumentError( + "$owner requires a '$name' field" + )) + return value[name] end -""" -$(TYPEDSIGNATURES) - -Retrieves a value from a dictionary by key, deserializes it, and coerces it to the specified type `T`. Returns a default value if the key is missing or the value is `missing`. - -# Arguments +function _field(value, name::AbstractString) + deserialize_value( + _required(value, name, get(value, "kind", get(value, "type", "object"))) + ) +end +function _optional(value, name::AbstractString) + haskey(value, name) ? deserialize_value(value[name]) : nothing +end -- `d`: The dictionary to query. -- `key`: The key to look up (symbol or string). -- `default`: The value to return if the key is not present. -- `T`: The target type for coercion. +function _decode_named_tuple(value) + value isa AbstractVector || throw(ArgumentError( + "named-tuple data must be an ordered array" + )) + names = Tuple(Symbol(_required(entry, "name", "named-tuple entry")) + for entry in value) + allunique(names) || throw(ArgumentError("named-tuple names must be unique")) + values = Tuple(deserialize_value( + _required(entry, "value", "named-tuple entry") + ) for entry in value) + return NamedTuple{names}(values) +end -# Returns +function _decode_float(type_name::AbstractString, value::AbstractDict) + T = _float_type(Val(Symbol(type_name))) + if haskey(value, "special") + special = value["special"] + special == "Inf" && return T(Inf) + special == "-Inf" && return T(-Inf) + special == "NaN" && return T(NaN) + throw(ArgumentError("unknown special floating-point value '$special'")) + end + stored_value = _required(value, "value", type_name) + return T === BigFloat ? parse(BigFloat, String(stored_value);precision=get(value,"precision",precision(BigFloat))) : convert(T, stored_value) +end -- The value associated with `key` in `d`, deserialized and coerced to type `T`, or `default` if the key is missing or the value is `missing`. +function _decode_material_record(value) + if get(value, "kind", nothing) == "radial_dielectric" + materials = [_decode_material_record(m) + for m in _required(value, "materials", "radial_dielectric")] + return RadialDielectric(materials, _field(value, "weights"); + mu_r = _field(value, "mu_r")) + end + raw_kind = deserialize_value(_required(value, "kind", "material")) + kind = raw_kind isa AbstractString ? Symbol(raw_kind) : raw_kind + fields = ( + kind, + _field(value, "rho"), + _field(value, "eps_r"), + _field(value, "mu_r"), + _field(value, "T0"), + _field(value, "alpha"), + _field(value, "rho_thermal"), + _field(value, "theta_max"), + _field(value, "tan_delta"), + _field(value, "sigma_solar") + ) + build = (selected_kind, + properties...) -> Material( + selected_kind isa Symbol ? selected_kind : Symbol(selected_kind), + properties... + ) + return _decoded_target(Material, build, fields) +end -# Examples +_decoded_source(value) = value isa Union{AbstractGrid, Gridspace} +function _decoded_target(::Type{Target}, build, values::Tuple) where {Target} + any(_decoded_source, values) || return build(values...) + sources = map(values) do value + _decoded_source(value) ? value : Grid((value,)) + end + return Gridspace{Target}(build, sources) +end -```julia -result = $(FUNCTIONNAME)(Dict(:a => 1), :a, 0, Int) # Returns 1 -result = $(FUNCTIONNAME)(Dict(), :b, 42, Int) # Returns 42 -``` """ -get_as(d::AbstractDict, key::Union{Symbol, AbstractString}, default, ::Type{T}) where {T} = - begin - v = get(d, key, default) - v === missing ? missing : coerce_to_T(_deserialize_value(v), T) - end - +Decode a supported scalar, collection, Grid, or v1 declaration. """ -$(TYPEDSIGNATURES) - -Deserializes a dictionary (parsed from JSON) into a Julia object of type `T`. -Attempts keyword constructor first, then falls back to positional constructor -if the keyword attempt fails with a specific `MethodError`. - -# Arguments -- `dict`: Dictionary containing the serialized object data. Keys should match field names. -- `T`: The target Julia `Type` to instantiate. +function deserialize_value(value) + value isa AbstractVector && return [deserialize_value(item) for item in value] + value isa AbstractDict || return value + if haskey(value, "__type__") + marker = String(value["__type__"]) + marker in ("Float16", "Float32", "Float64", "BigFloat") && + return _decode_float(marker, value) + marker == "Symbol" && return Symbol(_required(value, "value", marker)) + marker == "Val" && return Val(deserialize_value(_required(value, "value", marker))) + marker == "Complex" && return complex( + deserialize_value(_required(value, "re", marker)), + deserialize_value(_required(value, "im", marker)) + ) + marker == "Missing" && return missing + marker == "Tuple" && return Tuple(deserialize_value(item) for item in value["values"]) + if marker == "Array" + decoded=map(deserialize_value,value["values"]) + if !isempty(decoded) + T=typeof(first(decoded)) + all(item -> item isa T,decoded) && (decoded=collect(T,decoded)) + end + return reshape(decoded,Tuple(Int.(value["size"]))) + end + marker == "NamedTuple" && return NamedTuple{Tuple(Symbol.(value["names"]))}( + Tuple(deserialize_value(item) for item in value["values"])) + marker in ("UInt64", "Observable", "Quantile", "ModalRepresentation", "LineParameters", "CableConstants", "MonteCarloResult", + "LinearErrorResult", "SampleSummary", "HistogramDensity", "Distribution", + "FormulationOptions", "ComputationOptions", "ComputationDetails", "ObservedArchive", + "UUID", "Colon", "Quantity", "Unit", "UnitExpr", "ScientificType") && + return deserialize_extension(Val(Symbol(marker)),value) + if marker in ("Measurement", "MeasurementLinearErrorResult") + applicable(deserialize_extension, Val(Symbol(marker)), value) || throw( + ArgumentError("deserializing Measurement values requires Measurements.jl") + ) + return deserialize_extension(Val(Symbol(marker)), value) + end + throw(ArgumentError("unsupported serialized scalar tag '$marker'")) + end + if haskey(value, "grid") + values = deserialize_value(value["grid"]) + haskey(value, "rel") && return Grid(values, deserialize_value(value["rel"])) + haskey(value, "abs") && return Grid( + values, + AbsoluteError(deserialize_value(value["abs"])) + ) + return Grid(values) + end + if get(value, "type", nothing) == "material" + return _decode_material_record(_required(value, "value", "material")) + end + haskey(value, "type") && throw(ArgumentError( + "unsupported serialized object type '$(value["type"])'" + )) + haskey(value, "kind") && return _decode_node(Val(Symbol(value["kind"])), value) + return Dict(String(key) => deserialize_value(item) for (key, item) in value) +end -# Returns -- An instance of type `T`. +_decode_node(::Val{:disk}, value) = _decoded_target(Disk, Disk, (_field(value, "r"),)) +function _decode_node(::Val{:rectangle}, value) + _decoded_target( + Rectangle, + Rectangle, + (_field(value, "w"), _field(value, "h")) + ) +end +function _decode_node(::Val{:ellipse}, value) + _decoded_target( + Ellipse, + Ellipse, + (_field(value, "a"), _field(value, "b")) + ) +end +function _decode_node(::Val{:sector}, value) + _decoded_target( + Sector, + Sector, + ( + _field(value, "span"), + _field(value, "r_base"), + _field(value, "r_back"), + _field(value, "fillet") + ) + ) +end +function _decode_node(::Val{:annulus}, value) + _decoded_target( + Annulus, + Annulus, + (_field(value, "ri"), _field(value, "ro")) + ) +end +_decode_node(::Val{:shell}, value) = _decoded_target(Shell, Shell, (_field(value, "t"),)) +function _decode_node(::Val{:polygon}, value) + _decoded_target(Polygon, Polygon, (_field(value, "points"),)) +end +function _decode_node(::Val{:pose2}, value) + _decoded_target( + Pose2, + Pose2, + (_field(value, "x"), _field(value, "y"), _field(value, "φ")) + ) +end +function _decode_node(::Val{:earth_layer}, value) + EarthLayer( + _field(value, "rho"), + _field(value, "eps_r"), + _field(value, "mu_r"), + _field(value, "thickness") + ) +end +function _decode_node(::Val{:earth_model}, value) + raw_layers = _required(value, "layers", "earth_model") + raw_layers isa AbstractVector || throw(ArgumentError( + "earth_model layers must be an array" + )) + layers = EarthLayer[deserialize_value(layer) for layer in raw_layers] + isempty(layers) && throw(ArgumentError("earth_model layers cannot be empty")) + T = promote_type(map(eltype, layers)...) + converted = Tuple(convert(EarthLayer{T}, layer) for layer in layers) + length(converted) >= 2 || throw(ArgumentError( + "earth_model requires air and at least one earth layer" + )) + return build( + EarthModel, + Base.tail(converted); + vertical_layers = Bool(_required(value, "vertical_layers", "earth_model")), + air_layer = first(converted) + ) +end -# Throws -- `Error` if construction fails by both methods. -""" -function _deserialize_obj(dict::Dict, ::Type{T}) where {T} - # Prepare a dictionary mapping field symbols to deserialized values - deserialized_fields = Dict{Symbol, Any}() - for (key_str, val) in dict - # Skip metadata keys - if key_str == "__julia_type__" || key_str == "__type__" - continue - end - key_sym = Symbol(key_str) - # Ensure value is deserialized before storing - deserialized_fields[key_sym] = _deserialize_value(val) +function _decode_node(::Val{:ring}, value) + values = ( + _field(value, "n"), _field(value, "r"), + _field(value, "φ0"), _field(value, "span"), + haskey(value, "gap_frac") ? _field(value, "gap_frac") : 0 + ) + build = (n, r, φ0, span, gap_frac) -> Ring(n; r, φ0, span, gap_frac) + return _decoded_target(Ring, build, values) +end +function _decode_node(::Val{:polar}, value) + values = ( + _field(value, "nr"), _field(value, "nφ"), + _field(value, "r0"), _field(value, "dr"), + _field(value, "φ0"), _field(value, "span") + ) + build = (nr, nφ, r0, dr, φ0, span) -> Polar(; nr, nφ, r0, dr, φ0, span) + return _decoded_target(Polar, build, values) +end +function _decode_node(::Val{:fill}, value) + values = ( + _field(value, "r"), _field(value, "φ"), + _field(value, "φ0"), _field(value, "span") + ) + build = (r, φ, φ0, span) -> Fill(; r, φ, φ0, span) + return _decoded_target(Fill, build, values) +end +function _decode_node(::Val{:lattice}, value) + values = ( + _field(value, "nx"), _field(value, "ny"), + _field(value, "dx"), _field(value, "dy") + ) + build = (nx, ny, dx, dy) -> Lattice(; nx, ny, dx, dy) + return _decoded_target(Lattice, build, values) +end +function _decode_node(::Val{:fill_factor}, value) + _decoded_target( + FillFactor, FillFactor, (_field(value, "η"),) + ) +end +_decode_node(::Val{:capacity}, value) = capacity() +function _decode_node(::Val{:lay_ratio}, value) + _decoded_target(LayRatio, LayRatio, (_field(value, "q"),)) +end +_decode_node(::Val{:pitch}, value) = _decoded_target(Pitch, Pitch, (_field(value, "p"),)) +function _decode_node(::Val{:lay_angle}, value) + _decoded_target(LayAngle, LayAngle, (_field(value, "α"),)) +end +function _decode_node(::Val{:helix}, value) + values = ( + _field(value, "lay"), _field(value, "dir"), _field(value, "φ0") + ) + build = (lay, dir, φ0) -> Helix(lay; dir, φ0) + return _decoded_target(Helix, build, values) +end - end +function _material_reference(value, materials) + value isa AbstractString && return get(materials, String(value)) do + throw(KeyError(String(value))) + end + decoded = deserialize_value(value) + decoded isa Union{AbstractMaterial, Gridspace{<:AbstractMaterial}} || + throw(ArgumentError( + "region material must decode as AbstractMaterial" + )) + return decoded +end - # --- Attempt 1: Keyword Constructor --- - try - # Convert Dict{Symbol, Any} to pairs for keyword constructor T(; pairs...) - # Ensure kwargs only contain keys that are valid fieldnames for T - # This prevents errors if extra keys were present in JSON - valid_keys = fieldnames(T) - kwargs = pairs(filter(p -> p.first in valid_keys, deserialized_fields)) +function _decode_part(value, materials) + value isa AbstractDict || throw(ArgumentError("physical nodes must be objects")) + kind = Symbol(_required(value, "kind", "physical node")) + return _decode_part(Val(kind), value, materials) +end - # @info "Attempting keyword construction for $T with kwargs: $(collect(kwargs))" # Debug logging - if !isempty(kwargs) || hasmethod(T, Tuple{}, Symbol[]) # Check if kw constructor exists or if kwargs are empty - return T(; kwargs...) - else - # If no kwargs and no zero-arg kw constructor, trigger fallback - error( - "No keyword arguments provided and no zero-argument keyword constructor found for $T.", - ) - end - catch e - # Check if the error is specifically a MethodError for the keyword call - is_kw_meth_error = - e isa MethodError && (e.f === Core.kwcall || (e.f === T && isempty(e.args))) # Check for kwcall or zero-arg method error +function _decode_part(::Val{:region}, value, materials) + values = ( + Symbol(_required(value, "tag", "region")), + deserialize_value(_required(value, "primitive", "region")), + _material_reference(_required(value, "material", "region"), materials) + ) + return _decoded_target(Region, Region, values) +end +function _decode_part(::Val{:stack}, value, materials) + items = _required(value, "items", "stack") + items isa AbstractVector && !isempty(items) || throw(ArgumentError( + "stack items must be a nonempty array" + )) + decoded = Tuple(_decode_part(item, materials) for item in items) + return _decoded_target(Stack, Stack, decoded) +end +function _decode_part(::Val{:group}, value, materials) + path = deserialize_value(_required(value, "path", "group")) + path isa AbstractVector && (path = Tuple(path)) + values = ( + Symbol(_required(value, "name", "group")), + deserialize_value(_required(value, "at", "group")), + _decode_part(_required(value, "item", "group"), materials), + deserialize_value(_required(value, "pattern", "group")), + path, + deserialize_value(_required(value, "compact", "group")), + deserialize_value(_required(value, "boundary", "group")) + ) + return _decoded_target(Group, Group, values) +end +function _decode_part(::Val{:assembly}, value, materials) + if haskey(value, "members") + raw_members = _required(value, "members", "assembly") + raw_members isa AbstractVector && !isempty(raw_members) || throw( + ArgumentError("explicit assembly members must be a nonempty array") + ) + members = Tuple(map(raw_members) do member + member isa AbstractDict || throw(ArgumentError( + "explicit assembly members must be objects" + )) + DataModel.AssemblyMember( + _decode_part(_required(member, "item", "assembly member"), materials), + deserialize_value(_required(member, "at", "assembly member")) + ) + end) + return Assembly( + deserialize_value(_required(value, "at", "assembly")), + members, + nothing, + nothing, + nothing, + nothing + ) + end + raw_names = _required(value, "names", "assembly") + names = raw_names === nothing ? nothing : Symbol.(raw_names) + values = ( + deserialize_value(_required(value, "at", "assembly")), + _decode_part(_required(value, "item", "assembly"), materials), + deserialize_value(_required(value, "pattern", "assembly")), + deserialize_value(_required(value, "path", "assembly")), + deserialize_value(_required(value, "compact", "assembly")), + names + ) + return _decoded_target(Assembly, Assembly, values) +end +function _decode_part(::Val{:enclosure}, value, materials) + raw_fill = _required(value, "fill", "enclosure") + fill = raw_fill isa AbstractString ? _material_reference(raw_fill, materials) : + get(raw_fill, "kind", nothing) == "region" ? + _decode_part(raw_fill, materials) : deserialize_value(raw_fill) + raw_wall = _required(value, "wall", "enclosure") + wall = raw_wall === nothing ? nothing : _decode_part(raw_wall, materials) + values = ( + Symbol(_required(value, "tag", "enclosure")), + deserialize_value(_required(value, "at", "enclosure")), + deserialize_value(_required(value, "primitive", "enclosure")), + _decode_part(_required(value, "item", "enclosure"), materials), + fill, + wall + ) + return _decoded_target(Enclosure, Enclosure, values) +end +function _decode_part(::Val{kind}, value, materials) where {kind} + throw(ArgumentError("unsupported physical node kind '$kind'")) +end - if is_kw_meth_error - # @info "Keyword construction failed for $T (as expected for types without kw constructor). Trying positional." # Debug logging - # Fall through to positional attempt - else - # Different error during keyword construction (e.g., type mismatch inside constructor) - @error "Keyword construction failed for type $T with unexpected error: $e" - println(stderr, "Input dictionary: $dict") - println(stderr, "Deserialized fields (kwargs used): $(deserialized_fields)") - rethrow(e) # Rethrow unexpected errors - end - end +_decode_node(::Val{:region}, value) = _decode_part(value, Dict{String, Material}()) +_decode_node(::Val{:stack}, value) = _decode_part(value, Dict{String, Material}()) +_decode_node(::Val{:group}, value) = _decode_part(value, Dict{String, Material}()) +_decode_node(::Val{:assembly}, value) = _decode_part(value, Dict{String, Material}()) +_decode_node(::Val{:enclosure}, value) = _decode_part(value, Dict{String, Material}()) + +function _decode_design_resolved(value, materials) + get(value, "kind", nothing) == "cable_design" || throw(ArgumentError( + "cable declaration must have kind 'cable_design'" + )) + # Historical records used "root". Only this decoder accepts that key. + # Never silently choose between two competing physical declarations. + haskey(value, "origin") && haskey(value, "root") && throw(ArgumentError( + "cable_design must not contain both 'origin' and legacy 'root'" + )) + origin = haskey(value, "root") ? value["root"] : + _required(value, "origin", "cable_design") + values = ( + String(_required(value, "cable_id", "cable_design")), + _decode_part(origin, materials), + haskey(value, "nominal_data") ? + _decode_named_tuple(_required(value, "nominal_data", "cable_design")) : (;) + ) + caller = ( + cable_id, origin, nominal_data) -> build( + CableDesign, cable_id, origin; nominal_data + ) + return _decoded_target(CableDesign, caller, values) +end - # --- Attempt 2: Positional Constructor (Fallback) --- - # @info "Attempting positional construction for $T" # Debug logging - fields_in_order = fieldnames(T) - positional_args = [] +function _decode_design(value, materials = Dict{String, Material}()) + varying = Tuple( + (name, material) + for (name, material) in sort!(collect(materials); by = first) + if material isa Gridspace{Material} + ) + isempty(varying) && return _decode_design_resolved(value, materials) + names = first.(varying) + sources = last.(varying) + build = function (selected...) + resolved = Dict{String, Any}(materials) + for (name, material) in zip(names, selected) + resolved[name] = material + end + design = _decode_design_resolved(value, resolved) + design isa CableDesign || throw(ArgumentError( + "a named-material parameter space cannot be combined with another " * + "unresolved design field in the same JSON declaration" + )) + return design + end + return Gridspace{CableDesign}(build, sources) +end +_decode_node(::Val{:cable_design}, value) = _decode_design(value) + +function _decode_node(::Val{:line_cable_system}, value) + designs = CableDesign[deserialize_value(item) + for item in _required(value, "designs", "line_cable_system")] + clearance_rows = _optional(value, "clearances") + clearances = clearance_rows === nothing ? nothing : + reduce(vcat, permutedims.(clearance_rows)) + inputs = ( + designs, + deserialize_value(_required(value, "positions", "line_cable_system")), + deserialize_value(_required(value, "connections", "line_cable_system")), + _optional(value, "environment"), + String(_required(value, "system_id", "line_cable_system")), + _field(value, "line_length") + ) + input_positions = _optional(value, "input_positions") + caller = (selected...) -> build(LineCableSystem, selected...; + _input_positions = input_positions, _clearances = clearances) + return parameterize(LineCableSystem, caller, inputs) +end - try - # Check if the number of deserialized fields matches the number of struct fields - # This is a basic check for suitability of positional constructor - # It might be too strict if optional fields were omitted in JSON for keyword constructor types - # but for true positional types, all fields should generally be present. - # if length(deserialized_fields) != length(fields_in_order) - # Base.error("Number of fields in JSON ($(length(deserialized_fields))) does not match number of fields in struct $T ($(length(fields_in_order))). Cannot use positional constructor.") - # end +function _decode_node(::Val{:line_parameters_problem}, value) + haskey(value, "Gamma") && throw(ArgumentError( + "obsolete problem-level Gamma input; prescribe Γ in unified formula options")) + system = _field(value, "system") + system isa LineCableSystem || throw(ArgumentError( + "line_parameters_problem system must decode as LineCableSystem" + )) + earth = _field(value, "earth_props") + earth isa EarthModel || throw(ArgumentError( + "line_parameters_problem earth_props must decode as EarthModel" + )) + return Engine.LineParametersProblem( + system; + temperature = _field(value, "temperature"), + earth_props = earth, + frequencies = _field(value, "frequencies") + ) +end - for field_sym in fields_in_order - if haskey(deserialized_fields, field_sym) - push!(positional_args, deserialized_fields[field_sym]) - else - # If a field is missing, positional construction will fail. - error( - "Cannot attempt positional construction for $T: Missing required field '$field_sym' in input data.", - ) - end - end +function _decode_node(::Val{kind}, value) where {kind} + throw(ArgumentError("unsupported declaration kind '$kind'")) +end - # @info "Positional args for $T: $positional_args" # Debug logging - return T(positional_args...) - catch e - # Catch errors during positional construction (e.g., MethodError, TypeError) - @error "Positional construction failed for type $T with args: $positional_args. Error: $e" - println(stderr, "Input dictionary: $dict") - println( - stderr, - "Deserialized fields used for positional args: $(deserialized_fields)", - ) - # Check argument count mismatch again, although the loop above should ensure it if no error occurred there - if length(positional_args) != length(fields_in_order) - println( - stderr, - "Mismatch between number of args provided ($(length(positional_args))) and fields expected ($(length(fields_in_order))).", - ) - end - # Rethrow the error after providing context. This indicates neither method worked. - rethrow(e) - end +function _read_document(file_name::AbstractString, expected_format::AbstractString) + document = open(file_name, "r") do io + #! explicit-imports: off + JSON3.read(io, Dict{String, Any}) + #! explicit-imports: on + end + _required(document, "\$schema", "LineCableModels document") == + JSON_SCHEMA_DIALECT || throw(ArgumentError( + "unsupported JSON Schema dialect" + )) + format = _required(document, "format", "LineCableModels document") + format == expected_format || throw(ArgumentError( + "expected format '$expected_format', found '$format'" + )) + version = _required(document, "version", expected_format) + version == JSON_SCHEMA_VERSION || throw(ArgumentError( + "unsupported $expected_format version $version; supported version is " * + JSON_SCHEMA_VERSION + )) + return document +end - # This line should ideally not be reached - error( - "Failed to construct object of type $T using both keyword and positional methods.", - ) +function _document_materials(document) + raw = _required(document, "materials", "LineCableModels document") + raw isa AbstractDict || throw(ArgumentError("materials must be an object")) + return Dict{String, Any}( + String(name) => _decode_material_record(value) for (name, value) in raw + ) end diff --git a/src/importexport/interfaces.jl b/src/importexport/interfaces.jl new file mode 100644 index 000000000..0f450f85e --- /dev/null +++ b/src/importexport/interfaces.jl @@ -0,0 +1,65 @@ +""" +$(TYPEDSIGNATURES) + +Export LineCableModels data in the format selected by `format`. + +# Arguments + +- `format`: format selector. +- `args`: inputs required by the selected format. + +# Keywords + +- Format-specific output options. + +# Returns + +- The output path or value defined by the selected format. + +# Methods + +$(METHODLIST) +""" +function export_data(format::Symbol, args...; kwargs...) + return export_data(Val(format), args...; kwargs...) +end + +function export_data( + ::Val{:xlsx}, + line_parameters::Union{LineParameters,Commons.ObservedResult,AbstractVector{<:Commons.ObservedResult}}; + file_name::Union{String, Nothing} = nothing, + cable_system::Union{LineCableSystem, Nothing} = nothing, + overwrite::Bool = false +) + artifact = ReportBuilder.report( + ReportBuilder.XLSXReportDefinition(; file_name, cable_system, overwrite), + line_parameters + ) + return artifact.output +end + +""" +$(TYPEDSIGNATURES) + +Import data in the format selected by `format`. + +# Arguments + +- `format`: format selector. +- `args`: inputs required by the selected format. + +# Keywords + +- Format-specific input options. + +# Returns + +- Materialized objects defined by the selected format. + +# Methods + +$(METHODLIST) +""" +function import_data(format::Symbol, args...; kwargs...) + return import_data(Val(format), args...; kwargs...) +end diff --git a/src/importexport/materialslibrary.jl b/src/importexport/materialslibrary.jl index 64241049b..a7f09b442 100644 --- a/src/importexport/materialslibrary.jl +++ b/src/importexport/materialslibrary.jl @@ -1,193 +1,68 @@ """ $(TYPEDSIGNATURES) -Saves a [`MaterialsLibrary`](@ref) to a JSON file. - -# Arguments -- `library`: The [`MaterialsLibrary`](@ref) instance to save. -- `file_name`: The path to the output JSON file (default: "materials_library.json"). - -# Returns -- The absolute path of the saved file, or `nothing` on failure. +Save a material library as versioned JSON or trusted Julia serialization (`.jls`). +JLS input must come from a trusted source and use matching package types. """ function save( - library::MaterialsLibrary; - file_name::String = "materials_library.json", -)::Union{String, Nothing} - # TODO: Add jls serialization to materials library. - # Issue URL: https://github.com/Electa-Git/LineCableModels.jl/issues/3 - file_name = isabspath(file_name) ? file_name : joinpath(@__DIR__, file_name) - - - _, ext = splitext(file_name) - ext = lowercase(ext) - if ext != ".json" - @warn "MaterialsLibrary only supports .json saving. Forcing extension for file '$file_name'." - file_name = first(splitext(file_name)) * ".json" - end - - try - - return _save_materialslibrary_json(library, file_name) - - catch e - @error "Error saving MaterialsLibrary to '$(display_path(file_name))': $e" - showerror(stderr, e, catch_backtrace()) - println(stderr) - return nothing - end + library::MaterialsLibrary; + file_name::String = "materials_library.json" +) + extension = lowercase(splitext(file_name)[2]) + if extension == ".jls" + Serialization.serialize(file_name, library.data) + return abspath(file_name) + end + path = _json_path(file_name) + open(path, "w") do io + #! explicit-imports: off + # JSON3 exposes this established writer without a public marker. + JSON3.pretty(io, _json_document(library); allow_inf = true) + #! explicit-imports: on + end + return abspath(path) end """ $(TYPEDSIGNATURES) -Internal function to save the [`MaterialsLibrary`](@ref) to JSON. - -# Arguments -- `library`: The [`MaterialsLibrary`](@ref) instance. -- `file_name`: The output file path. - -# Returns -- The absolute path of the saved file. -""" -function _save_materialslibrary_json(library::MaterialsLibrary, file_name::String)::String - # Check if the library has the data field initialized correctly - if !isdefined(library, :data) || !(library.data isa AbstractDict) - Base.error("MaterialsLibrary does not have a valid 'data' dictionary. Cannot save.") - end - - # Use the generic _serialize_value, which handles the dictionary and its Material contents - serialized_library_data = _serialize_value(library) # Serialize the dict directly - - open(file_name, "w") do io - JSON3.pretty(io, serialized_library_data, allow_inf = true) - end - if isfile(file_name) - @info "Materials library saved to: $(display_path(file_name))" - end - - return abspath(file_name) -end - -""" -$(TYPEDSIGNATURES) - -Loads materials from a JSON file into an existing [`MaterialsLibrary`](@ref) object. -Modifies the library in-place. - -# Arguments -- `library`: The [`MaterialsLibrary`](@ref) instance to populate (modified in-place). -- `file_name`: Path to the JSON file to load (default: \"materials_library.json\"). - -# Returns -- The modified [`MaterialsLibrary`](@ref) instance. - -# See also -- [`MaterialsLibrary`](@ref) +Atomically replace a material library from supported JSON or trusted JLS data. +The original library remains unchanged if parsing or validation fails. """ function load!( - library::MaterialsLibrary; - file_name::String = "materials_library.json", -)::MaterialsLibrary - - if !isfile(file_name) - throw( - ErrorException( - "Materials library file not found: '$(display_path(file_name))'", - ), - ) # make caller receive an Exception - - end - - # Only JSON format is supported now - _, ext = splitext(file_name) - ext = lowercase(ext) - if ext != ".json" - @error "MaterialsLibrary loading only supports .json files. Cannot load '$(display_path(file_name))'." - return library - end - - try - _load_materialslibrary_json!(library, file_name) - catch e - @error "Error loading MaterialsLibrary from '$(display_path(file_name))': $e" - showerror(stderr, e, catch_backtrace()) - println(stderr) - # Optionally clear or leave partially loaded - # empty!(library) - end - return library + library::MaterialsLibrary; + file_name::String = "materials_library.json" +) + isfile(file_name) || throw(ArgumentError( + "materials library file not found: '$(_display_path(file_name))'", + )) + extension = lowercase(splitext(file_name)[2]) + materials = if extension == ".jls" + _trusted_material_data(Serialization.deserialize(file_name)) + elseif extension == ".json" + document = _read_document(file_name, MATERIALS_SCHEMA) + Dict( + name => validate(material) for (name, material) in _document_materials(document) + ) + else + throw(ArgumentError( + "MaterialsLibrary loading requires a .json or .jls file", + )) + end + library.data = materials + return library end -""" -$(TYPEDSIGNATURES) - -Internal function to load materials from JSON into the library. - -# Arguments -- `library`: The [`MaterialsLibrary`](@ref) instance to modify. -- `file_name`: The path to the JSON file. - -# Returns -- Nothing. Modifies `library` in-place. - -# See also -- [`MaterialsLibrary`](@ref) -- [`Material`](@ref) -- [`add!`](@ref) -- [`_deserialize_value`](@ref) -""" -function _load_materialslibrary_json!(library::MaterialsLibrary, file_name::String) - # Ensure library structure is initialized - if !isdefined(library, :data) || !(library.data isa AbstractDict) - @warn "Library 'data' field was not initialized or not a Dict. Initializing." - library.data = Dict{String, Material}() - else - # Clear existing materials before loading - empty!(library.data) - end - - # Load and parse the JSON data (expecting a Dict of material_name => material_data) - json_data = open(file_name, "r") do io - JSON3.read(io, Dict{String, Any}) - end - - - @info "Loading materials from JSON: '$(display_path(file_name))'..." - num_loaded = 0 - num_failed = 0 - - # Process each material entry - for (name::String, material_data::Any) in json_data - if !(material_data isa AbstractDict) - @warn "Skipping material '$name': Invalid data format (expected Dictionary, got $(typeof(material_data)))." - num_failed += 1 - continue - end - try - # Use the generic _deserialize_value function. - # It will detect __julia_type__ and call _deserialize_obj for Material. - deserialized_material = _deserialize_value(material_data) - - # **Crucial Check:** Verify the deserialized object is actually a Material - if deserialized_material isa Material - add!(library, name, deserialized_material) # Assumes this function exists - num_loaded += 1 - else - # This path is taken if _deserialize_obj failed and returned the original Dict - @warn "Skipping material '$name': Failed to deserialize into Material object. Data received: $material_data" - # The error from _deserialize_obj inside _deserialize_value would have already been logged. - num_failed += 1 - end - catch e - # Catch errors that might occur outside _deserialize_value (e.g., in add!) - num_failed += 1 - @error "Error processing material entry '$name': $e" - showerror(stderr, e, catch_backtrace()) - println(stderr) - end - end - - @info "Finished loading materials from '$(display_path(file_name))'. Successfully loaded $num_loaded materials, failed to load $num_failed." - return nothing +function _trusted_material_data(decoded) + decoded isa AbstractDict || throw(ArgumentError( + "trusted JLS material data must be a dictionary", + )) + materials = Dict{String, Material}() + for (name, material) in decoded + material isa Material || throw(ArgumentError( + "material '$name' must be Material, not $(typeof(material))", + )) + materials[String(name)] = validate(material) + end + return materials end diff --git a/src/importexport/observed.jl b/src/importexport/observed.jl new file mode 100644 index 000000000..36e856ffe --- /dev/null +++ b/src/importexport/observed.jl @@ -0,0 +1,150 @@ +# Each archive defines one uncertainty-source table spanning all points and the +# reference. The archive is independent of process-global source registries and behavior-generation tags. +_observed_encoding() = (indices=Dict{Any,Int}(),sources=Any[]) +""" +$(TYPEDSIGNATURES) + +Encode detached values using one archive-wide uncertainty-source table. +Extensions register independent sources in `source_table.sources` and reuse their +indices in `source_table.indices`, preserving dependencies across all observations. +""" +encode_observation(value,source_table) = serialize_value(value,Val(:scientific)) +function encode_observation(value::NamedTuple,source_table) + Dict("__type__"=>"NamedTuple","names"=>string.(collect(keys(value))), + "values"=>[encode_observation(item,source_table) for item in values(value)]) +end +encode_observation(value::Tuple,source_table) = Dict("__type__"=>"Tuple", + "values"=>[encode_observation(item,source_table) for item in value]) +encode_observation(value::AbstractArray,source_table) = Dict("__type__"=>"Array","size"=>collect(size(value)), + "values"=>[encode_observation(item,source_table) for item in vec(value)]) +encode_observation(value::AbstractDict,source_table) = Dict("__type__"=>"Dictionary", + "entries"=>[encode_observation((key,item),source_table) for (key,item) in value]) +encode_observation(value::Complex,source_table) = Dict("__type__"=>"Complex", + "re"=>encode_observation(real(value),source_table),"im"=>encode_observation(imag(value),source_table)) +encode_observation(value::Commons.ObservedResult,source_table) = Dict("__type__"=>"ObservedResult", + "fields"=>encode_observation((value.gridpoint,value.quantities,value.errors,value.timings),source_table)) + +function serialize_value(value::Union{Commons.ObservedResult,AbstractVector{<:Commons.ObservedResult}}) + return serialize_value(value,Val(:observed)) +end +function serialize_value(value,::Val{:observed}) + source_table=_observed_encoding() + payload=encode_observation(value,source_table) + return Dict("__type__"=>"ObservedArchive","payload"=>payload, + "sources"=>serialize_value(source_table.sources,Val(:scientific))) +end +function serialize_value(artifact::ReportBuilder.ReportArtifact) + return serialize_value((observed=artifact.observed,reference=artifact.reference),Val(:observed)) +end + +_decode_observed(value,sources) = deserialize_value(value) +function _decode_observed(value::AbstractDict,sources) + marker=get(value,"__type__",nothing) + if marker=="ObservedResult" + return Commons.ObservedResult(_decode_observed(value["fields"],sources)...) + elseif marker=="ObservedMeasurement" + return decode_observation_measurement(value,sources) + elseif marker=="NamedTuple" + return NamedTuple{Tuple(Symbol.(value["names"]))}(Tuple(_decode_observed(item,sources) for item in value["values"])) + elseif marker=="Tuple" + return Tuple(_decode_observed(item,sources) for item in value["values"]) + elseif marker=="Array" + elements=map(item -> _decode_observed(item,sources),value["values"]) + if !isempty(elements) + T=typeof(first(elements)) + if all(item -> item isa T,elements) + elements=collect(T,elements) + elseif all(item -> item isa Number || ismissing(item),elements) + scalar_type=foldl((left,right) -> Union{left,right},typeof.(elements)) + elements=collect(scalar_type,elements) + end + end + return reshape(elements,Tuple(Int.(value["size"]))) + elseif marker=="Dictionary" + return Dict(_decode_observed(entry,sources) for entry in value["entries"]) + elseif marker=="Complex" + return complex(_decode_observed(value["re"],sources),_decode_observed(value["im"],sources)) + end + return deserialize_value(value) +end + +"""Restore one uncertain scalar from its archived sensitivities and shared sources.""" +function decode_observation_measurement end +function deserialize_extension(::Val{:ObservedArchive},record) + records=deserialize_value(record["sources"]) + sources=isempty(records) ? () : observation_sources(records,Val(:measurements)) + return _decode_observed(record["payload"],sources) +end +"""Restore the archive's independent uncertainty sources once, before its values.""" +function observation_sources(records,::Val{:measurements}) + throw(ArgumentError("restoring uncertain observations requires using Measurements")) +end + +serialize_value(value::UUIDs.UUID) = Dict("__type__"=>"UUID","value"=>string(value)) +deserialize_extension(::Val{:UUID},record) = UUIDs.UUID(record["value"]) +serialize_value(::Colon) = Dict("__type__"=>"Colon") +deserialize_extension(::Val{:Colon},record) = Colon() +serialize_value(value::Units.Quantity{Q}) where {Q} = Q isa Tuple ? + Dict("__type__"=>"Quantity","parts"=>string.(collect(Q))) : + Dict("__type__"=>"Quantity","name"=>string(Q)) +deserialize_extension(::Val{:Quantity},record) = haskey(record,"parts") ? + Units.Quantity{Tuple(Symbol.(record["parts"]))}() : + Units.Quantity{Symbol(record["name"])}() +serialize_value(value::Units.Unit) = Dict("__type__"=>"Unit","name"=>string(value.name),"prefix"=>string(value.prefix)) +deserialize_extension(::Val{:Unit},record) = Units.Unit(Symbol(record["name"]),Symbol(record["prefix"])) +serialize_value(value::Units.UnitExpr) = Dict("__type__"=>"UnitExpr", + "numerator"=>serialize_value(value.numerator,Val(:scientific)),"denominator"=>serialize_value(value.denominator,Val(:scientific))) +deserialize_extension(::Val{:UnitExpr},record) = Units.UnitExpr(deserialize_value(record["numerator"]),deserialize_value(record["denominator"])) +function serialize_value(value::Type) + owner=parentmodule(value) + path=Base.fullname(owner) + first(path) in (:LineCableModels,:Base,:Core) || throw(ArgumentError("no portable type identity for $value")) + getfield(owner,nameof(value))===value || throw(ArgumentError("parametric runtime types are not portable formulation identities")) + return Dict("__type__"=>"ScientificType","path"=>string.(path),"name"=>string(nameof(value))) +end +function deserialize_extension(::Val{:ScientificType},record) + path=Symbol.(record["path"]) + root=first(path) + owner=root===:LineCableModels ? LineCableModels : root===:Base ? Base : root===:Core ? Core : + throw(ArgumentError("unknown scientific type owner")) + for name in path[2:end] + owner=getfield(owner,name) + owner isa Module || throw(ArgumentError("scientific type owner must be a module")) + end + value=getfield(owner,Symbol(record["name"])) + value isa Type || throw(ArgumentError("saved scientific identity does not name a type")) + return value +end + +""" +$(TYPEDSIGNATURES) + +Save current observed data as JSON or a native Julia archive. One shared source table +preserves shared uncertainties across quantities, points, and a report reference. +BigFloat values retain their precision. Report tables and figures are rebuilt +from these observations after loading. +""" +function save(value::Union{Commons.ObservedResult,AbstractVector{<:Commons.ObservedResult},ReportBuilder.ReportArtifact},path::AbstractString) + encoded=serialize_value(value) + extension=lowercase(splitext(path)[2]) + extension in (".json",".jls") || throw(ArgumentError("observed archives require .json or .jls")) + destination=abspath(path) + temporary=tempname(dirname(destination)) + try + open(temporary,"w") do io + extension==".json" ? JSON3.write(io,encoded) : Serialization.serialize(io,encoded) + end + mv(temporary,destination;force=true) + finally + isfile(temporary) && rm(temporary) + end + return destination +end + +function import_data(::Val{:observed},path::AbstractString) + extension=lowercase(splitext(path)[2]) + encoded=extension==".json" ? JSON3.read(read(path,String)) : extension==".jls" ? + open(Serialization.deserialize,path) : throw(ArgumentError("observed archives require .json or .jls")) + get(encoded,"__type__",nothing)=="ObservedArchive" || throw(ArgumentError("file is not a current observed archive")) + return deserialize_value(encoded) +end diff --git a/src/importexport/paths.jl b/src/importexport/paths.jl new file mode 100644 index 000000000..4ef111f0c --- /dev/null +++ b/src/importexport/paths.jl @@ -0,0 +1,16 @@ +_display_path(path::AbstractString) = + try + relpath(abspath(path), pwd()) + catch + basename(path) + end + +function _json_path(file_name::AbstractString) + path = isabspath(file_name) ? String(file_name) : abspath(file_name) + extension = lowercase(splitext(path)[2]) + isempty(extension) && return path * ".json" + extension == ".json" || throw(ArgumentError( + "JSON output requires a .json extension; got '$extension'", + )) + return path +end diff --git a/src/importexport/problem.jl b/src/importexport/problem.jl new file mode 100644 index 000000000..8cf8c7780 --- /dev/null +++ b/src/importexport/problem.jl @@ -0,0 +1,63 @@ +""" +$(TYPEDSIGNATURES) + +Write one fully materialized line-parameter problem as versioned JSON. + +# Arguments + +- `problem`: completed scalar line-parameter problem. + +# Keywords + +- `file_name`: destination JSON file. + +# Returns + +- Absolute path of the written file. +""" +function export_data( + ::Val{:json}, + problem::Engine.LineParametersProblem; + file_name::AbstractString +) + path = _json_path(String(file_name)) + open(path, "w") do io + #! explicit-imports: off + JSON3.pretty(io, _json_document(problem); allow_inf = true) + #! explicit-imports: on + end + return abspath(path) +end + +""" +$(TYPEDSIGNATURES) + +Read one fully materialized line-parameter problem from versioned JSON. + +# Arguments + +- `LineParametersProblem`: requested result type. + +# Keywords + +- `file_name`: source JSON file. + +# Returns + +- A validated scalar [`Engine.LineParametersProblem`](@ref). +""" +function import_data( + ::Val{:json}, + ::Type{Engine.LineParametersProblem}; + file_name::AbstractString +) + isfile(file_name) || throw(ArgumentError( + "line-parameter problem file not found: '$(_display_path(file_name))'", + )) + document = _read_document(file_name, PROBLEM_SCHEMA) + problem = deserialize_value(_required(document, "root", PROBLEM_SCHEMA)) + problem isa Engine.LineParametersProblem || throw(ArgumentError( + "line-parameter problem document did not decode as LineParametersProblem", + )) + return problem +end diff --git a/src/importexport/pscad.jl b/src/importexport/pscad.jl deleted file mode 100644 index c1ab4bc70..000000000 --- a/src/importexport/pscad.jl +++ /dev/null @@ -1,868 +0,0 @@ -#= -Generates sequential IDs, used for simulation element identification (e.g., PSCAD). -Starts from 100,000,000 and increments. -=# -let current_id = 100000000 - global _next_id = () -> (id = current_id; current_id += 1; string(id)) -end - -""" -$(TYPEDSIGNATURES) - -Exports a [`LineCableSystem`](@ref) to a PSCAD-compatible file format. - -# Arguments - -- `cable_system`: A [`LineCableSystem`](@ref) object representing the cable system to be exported. -- `earth_props`: An [`EarthModel`](@ref) object containing the earth properties. -- `base_freq`: The base frequency \\[Hz\\] used for the PSCAD export. -- `file_name`: The path to the output file (default: "*_export.pscx") - -# Returns - -- The absolute path of the saved file, or `nothing` on failure. - -# Examples - -```julia -cable_system = LineCableSystem(...) -earth_model = EarthModel(...) -$(FUNCTIONNAME)(cable_system, earth_model, base_freq=50) -``` - -# See also - -- [`LineCableSystem`](@ref) -""" -function export_data(::Val{:pscad}, - cable_system::LineCableSystem, - earth_props::EarthModel; - base_freq = f₀, - file_name::Union{String, Nothing} = nothing, -)::Union{String, Nothing} - - if isnothing(file_name) - # caller didn't supply a name -> derive from cable_system if present - file_name = joinpath(@__DIR__, "$(cable_system.system_id)_export.pscx") - else - # caller supplied a path/name -> respect directory, but prepend system_id to basename - requested = isabspath(file_name) ? file_name : joinpath(@__DIR__, file_name) - if isnothing(cable_system) - file_name = requested - else - dir = dirname(requested) - base = basename(requested) - file_name = joinpath(dir, "$(cable_system.system_id)_$base") - end - end - - # Sets attributes on an existing EzXML.Node from a dictionary. - function _set_attributes!(element::EzXML.Node, attrs::Dict{String, String}) - # Loop through the dictionary and set each attribute on the element - for (k, v) in attrs - element[k] = v - end - end - - # Adds child elements to an existing EzXML.Node - # from a vector of ("name", "value") tuples. - function _add_params_to_list!( - list_element::EzXML.Node, - params::Vector{Tuple{String, String}}, - ) - # Ensure the target element is actually a paramlist for clarity, though not strictly necessary for EzXML - # if nodename(list_element) != "paramlist" - # @warn "Attempting to add params to a non-paramlist node: $(nodename(list_element))" - # end - # Loop through the vector and add each parameter as a child element - for (name, value) in params - param = addelement!(list_element, "param") - param["name"] = name - param["value"] = value - end - end - - # --- Initial Setup (Identical to original) --- - # Local Ref for ID generation ensures it's unique to this function call if nested - current_id = Ref(100000000) - _next_id() = string(current_id[] += 1) - - # Formatting function (ensure to_nominal is defined or handle types appropriately) - format_nominal = - (X; sigdigits = 4, minval = -1e30, maxval = 1e30) -> begin - - local_value = round(to_nominal(X), sigdigits = sigdigits) - - local_value = max(min(local_value, maxval), minval) - if abs(local_value) < eps(Float64) - local_value = 0.0 - end - return string(local_value) - end - - id_map = Dict{String, String}() # Stores IDs needed for linking (Instance IDs in this case) - doc = XMLDocument() - project = ElementNode("project") - setroot!(doc, project) - project_id = cable_system.system_id - - # --- Project Attributes (Identical) --- - project["name"] = project_id - project["version"] = "5.0.2" - project["schema"] = "" - project["Target"] = "EMTDC" - - # --- Settings (Use Helper for Params) --- - settings = addelement!(project, "paramlist") - settings["name"] = "Settings" # Set name attribute directly as in original - timestamp = string(round(Int, datetime2unix(now()))) - settings_params = [ - ("creator", "LineCableModels.jl,$timestamp"), ("time_duration", "0.5"), - ("time_step", "5"), ("sample_step", "250"), ("chatter_threshold", ".001"), - ("branch_threshold", ".0005"), ("StartType", "0"), - ("startup_filename", "\$(Namespace).snp"), ("PlotType", "0"), - ("output_filename", "\$(Namespace).out"), ("SnapType", "0"), - ("SnapTime", "0.3"), ("snapshot_filename", "\$(Namespace).snp"), - ("MrunType", "0"), ("Mruns", "1"), ("Scenario", ""), ("Advanced", "14335"), - ("sparsity_threshold", "200"), ("Options", "16"), ("Build", "18"), - ("Warn", "0"), ("Check", "0"), - ( - "description", - "Created with LineCableModels.jl (https://github.com/Electa-Git/LineCableModels.jl)", - ), - ("Debug", "0"), - ] - _add_params_to_list!(settings, settings_params) # Use helper to add children - - # --- Empty Elements (Identical) --- - addelement!(project, "Layers") - addelement!(project, "List")["classid"] = "Settings" - addelement!(project, "bookmarks") - - # --- GlobalSubstitutions (Identical Structure) --- - global_subs = addelement!(project, "GlobalSubstitutions") - global_subs["name"] = "Default" - addelement!(global_subs, "List")["classid"] = "Sub" - addelement!(global_subs, "List")["classid"] = "ValueSet" - global_pl = addelement!(global_subs, "paramlist") # No name attribute - # Add the single parameter directly as in original - global_param = addelement!(global_pl, "param") - global_param["name"] = "Current" - global_param["value"] = "" - - # --- Definitions Section (Identical Start) --- - definitions = addelement!(project, "definitions") - - # --- StationDefn (Use Helpers for Attrs/Params) --- - station = addelement!(definitions, "Definition") - station_id = _next_id() - id_map["DS_Defn"] = station_id # Map Definition ID - station_attrs = Dict( - "classid" => "StationDefn", "name" => "DS", "id" => station_id, - "group" => "", "url" => "", "version" => "", "build" => "", - "crc" => "-1", "view" => "false", - ) - _set_attributes!(station, station_attrs) # Use helper - - station_pl = addelement!(station, "paramlist") - station_pl["name"] = "" # Keep empty name attribute exactly as original - # Add Description param directly as original - desc_param_st = addelement!(station_pl, "param") - desc_param_st["name"] = "Description" - desc_param_st["value"] = "" - - schematic = addelement!(station, "schematic") - schematic["classid"] = "StationCanvas" - schematic_pl = addelement!(schematic, "paramlist") # No name attribute - schematic_params = [ - ("show_grid", "0"), ("size", "0"), ("orient", "1"), ("show_border", "0"), - ("monitor_bus_voltage", "0"), ("show_signal", "0"), ("show_virtual", "0"), - ("show_sequence", "0"), ("auto_sequence", "1"), ("bus_expand_x", "8"), - ("bus_expand_y", "8"), ("bus_length", "4"), - ] - _add_params_to_list!(schematic_pl, schematic_params) # Use helper - - addelement!(schematic, "grouping") # Identical - - # --- Station Schematic: Wire/User Instance for "Main" (Use Helpers) --- - wire = addelement!(schematic, "Wire") - wire_id = _next_id() - wire_attrs = Dict( - "classid" => "Branch", "id" => wire_id, "name" => "Main", "x" => "180", - "y" => "180", - "w" => "66", "h" => "82", "orient" => "0", "disable" => "false", - "defn" => "Main", - "recv" => "-1", "send" => "-1", "back" => "-1", - ) - _set_attributes!(wire, wire_attrs) # Use helper - - # Keep vertex loop identical - for (x, y) in [(0, 0), (0, 18), (54, 54), (54, 72)] - vertex = addelement!(wire, "vertex") - vertex["x"] = string(x) - vertex["y"] = string(y) - end - - user = addelement!(wire, "User") # User instance nested in Wire - user_id = _next_id() - id_map["Main"] = user_id # Original maps the *instance* ID here for hierarchy link - user_attrs = Dict( - "classid" => "UserCmp", "id" => user_id, "name" => "$project_id:Main", - "x" => "0", "y" => "0", "w" => "0", "h" => "0", "z" => "-1", "orient" => "0", - "defn" => "$project_id:Main", # Links to definition named "Main" (implicitly in same project) - "link" => "-1", "q" => "4", "disable" => "false", - ) - _set_attributes!(user, user_attrs) # Use helper - - user_pl = addelement!(user, "paramlist") - # Original sets attributes directly on paramlist and adds no children - replicate exactly: - user_pl["name"] = "" - user_pl["link"] = "-1" - user_pl["crc"] = "-1" - - # --- UserCmpDefn "Main" (Use Helpers) --- - user_cmp = addelement!(definitions, "Definition") - user_cmp_id = _next_id() # This is the definition ID - id_map["Main_Defn"] = user_cmp_id # Map Definition ID separately - user_cmp_attrs = Dict( - "classid" => "UserCmpDefn", "name" => "Main", "id" => user_cmp_id, - "group" => "", - "url" => "", "version" => "", "build" => "", "crc" => "-1", "view" => "false", - "date" => timestamp, - ) - _set_attributes!(user_cmp, user_cmp_attrs) # Use helper - - user_cmp_pl = addelement!(user_cmp, "paramlist") - user_cmp_pl["name"] = "" # Empty name attribute - # Add Description param directly - desc_param_ucmp = addelement!(user_cmp_pl, "param") - desc_param_ucmp["name"] = "Description" - desc_param_ucmp["value"] = "" - - # Form (Identical) - form = addelement!(user_cmp, "form") - form["name"] = "" - form["w"] = "320" - form["h"] = "400" - form["splitter"] = "60" - - # Graphics (Identical Structure) - graphics = addelement!(user_cmp, "graphics") - graphics["viewBox"] = "-200 -200 200 200" - graphics["size"] = "2" - - # Graphics Rectangle (Use Helpers) - rect = addelement!(graphics, "Gfx") - rect_id = _next_id() - rect_attrs = Dict( - "classid" => "Graphics.Rectangle", "id" => rect_id, "x" => "-36", "y" => "-36", - "w" => "72", "h" => "72", - ) - _set_attributes!(rect, rect_attrs) # Use helper - rect_pl = addelement!(rect, "paramlist") # No name attribute - rect_params = [ - ("color", "Black"), ("dasharray", "0"), ("thickness", "0"), ("port", ""), - ("fill_style", "0"), ("fill_fg", "Black"), ("fill_bg", "Black"), - ("cond", "true"), - ] - _add_params_to_list!(rect_pl, rect_params) # Use helper - - # Graphics Text (Use Helpers) - text = addelement!(graphics, "Gfx") - text_id = _next_id() - text_attrs = Dict("classid" => "Graphics.Text", "id" => text_id, "x" => "0", "y" => "0") - _set_attributes!(text, text_attrs) # Use helper - text_pl = addelement!(text, "paramlist") # No name attribute - text_params = [ - ("text", "%:Name"), ("anchor", "0"), ("full_font", "Tahoma, 13world"), - ("angle", "0"), ("color", "Black"), ("cond", "true"), - ] - _add_params_to_list!(text_pl, text_params) # Use helper - - # --- UserCmpDefn "Main" Schematic (Use Helpers) --- - user_schematic = addelement!(user_cmp, "schematic") - user_schematic["classid"] = "UserCanvas" - user_sch_pl = addelement!(user_schematic, "paramlist") # No name attribute - user_sch_params = [ - ("show_grid", "0"), ("size", "0"), ("orient", "1"), ("show_border", "0"), - ("monitor_bus_voltage", "0"), ("show_signal", "0"), ("show_virtual", "0"), - ("show_sequence", "0"), ("auto_sequence", "1"), ("bus_expand_x", "8"), - ("bus_expand_y", "8"), ("bus_length", "4"), ("show_terminals", "0"), - ("virtual_filter", ""), ("animation_freq", "500"), - ] - _add_params_to_list!(user_sch_pl, user_sch_params) # Use helper - - addelement!(user_schematic, "grouping") # Identical - - # --- UserCmpDefn "Main" Schematic: CableSystem Instance (Use Helpers) --- - cable = addelement!(user_schematic, "Wire") # Wire instance - cable_id = _next_id() - cable_attrs = Dict( - "classid" => "Cable", "id" => cable_id, "name" => "$project_id:CableSystem", - "x" => "72", "y" => "36", "w" => "107", "h" => "128", "orient" => "0", - "disable" => "false", "defn" => "$project_id:CableSystem", # Links to definition named "CableSystem" - "recv" => "-1", "send" => "-1", "back" => "-1", "crc" => "-1", - ) - _set_attributes!(cable, cable_attrs) # Use helper - - # Keep vertex loop identical - for (x, y) in [(0, 0), (0, 18), (54, 54), (54, 72)] - vertex = addelement!(cable, "vertex") - vertex["x"] = string(x) - vertex["y"] = string(y) - end - - cable_user = addelement!(cable, "User") # User instance nested in Wire - cable_user_id = _next_id() - id_map["CableSystem"] = cable_user_id # Original maps this *instance* ID for hierarchy link - cable_user_attrs = Dict( - "classid" => "UserCmp", "id" => cable_user_id, - "name" => "$project_id:CableSystem", - "x" => "0", "y" => "0", "w" => "0", "h" => "0", "z" => "-1", "orient" => "0", - "defn" => "$project_id:CableSystem", # Links to definition named "CableSystem" - "link" => "-1", "q" => "4", "disable" => "false", - ) - _set_attributes!(cable_user, cable_user_attrs) # Use helper - - cable_pl = addelement!(cable_user, "paramlist") - # Original sets attributes on paramlist AND adds params - replicate exactly - cable_pl["name"] = "" - cable_pl["link"] = "-1" - cable_pl["crc"] = "-1" - cable_params = [ # Instance parameters - ("Name", "LineCableSystem_1"), ("R", "#NaN"), ("X", "#NaN"), ("B", "#NaN"), - ("Freq", format_nominal(base_freq)), - ("Length", format_nominal(cable_system.line_length / 1000)), # Assumes field exists - ("Dim", "0"), ("Mode", "0"), ("CoupleEnab", "0"), ("CoupleName", "row"), - ("CoupleOffset", "0.0 [m]"), ("CoupleRef", "0"), ("tname", "tandem_segment"), - ("sfault", "0"), ("linc", "10.0 [km]"), ("steps", "3"), ("gen_cnst", "1"), - ("const_path", "%TEMP%\\my_constants_file.tlo"), ("Date", timestamp), - ] - _add_params_to_list!(cable_pl, cable_params) # Use helper - - # --- RowDefn "CableSystem" (Use Helpers) --- - row = addelement!(definitions, "Definition") - row_id = _next_id() - id_map["CableSystem_Defn"] = row_id # Map definition ID separately - row_attrs = Dict( - "id" => row_id, "classid" => "RowDefn", "name" => "CableSystem", "group" => "", - "url" => "", "version" => "RowDefn", "build" => "RowDefn", "crc" => "-1", - "key" => "", "view" => "false", "date" => timestamp, - ) - _set_attributes!(row, row_attrs) # Use helper - - row_pl = addelement!(row, "paramlist") # No name attribute - row_params = [("Description", ""), ("type", "Cable")] - _add_params_to_list!(row_pl, row_params) # Use helper - - row_schematic = addelement!(row, "schematic") - row_schematic["classid"] = "RowCanvas" - row_sch_pl = addelement!(row_schematic, "paramlist") # No name attribute - row_sch_params = - [("show_grid", "0"), ("size", "0"), ("orient", "1"), ("show_border", "0")] - _add_params_to_list!(row_sch_pl, row_sch_params) # Use helper - - # --- Components in RowDefn "CableSystem" Schematic --- - - # FrePhase Component (Use Helpers) - fre_phase = addelement!(row_schematic, "User") - fre_phase_id = _next_id() - fre_phase_attrs = Dict( - "id" => fre_phase_id, "name" => "master:Line_FrePhase_Options", - "classid" => "UserCmp", - "x" => "576", "y" => "180", "w" => "460", "h" => "236", "z" => "-1", - "orient" => "0", - "defn" => "master:Line_FrePhase_Options", "link" => "-1", "q" => "4", - "disable" => "false", - ) - _set_attributes!(fre_phase, fre_phase_attrs) # Use helper - - fre_pl = addelement!(fre_phase, "paramlist") - # Original sets crc attribute only on paramlist, replicate exactly - fre_pl["crc"] = "-1" - fre_params = [ # Actual params - ("Interp1", "1"), ("Output", "0"), ("Inflen", "0"), ("FS", "0.5"), - ("FE", "1.0E6"), - ("Numf", "100"), ("YMaxP", "20"), ("YMaxE", "0.2"), ("AMaxP", "20"), - ("AMaxE", "0.2"), - ("MaxRPtol", "2.0e6"), ("W1", "1.0"), ("W2", "1000.0"), ("W3", "1.0"), - ("CPASS", "0"), - ("NFP", "1000"), ("FSP", "0.001"), ("FEP", "1000.0"), ("DCenab", "0"), - ("DCCOR", "1"), - ("ECLS", "1"), ("shntcab", "1.0E-9"), ("ET_PE", "1E-10"), ("MER_PE", "2"), - ("MIT_PE", "5"), ("FDIS", "3"), ("enablf", "1"), - ] - _add_params_to_list!(fre_pl, fre_params) # Use helper - - addelement!(row_schematic, "grouping") # Identical - - # --- Coaxial Cables Loop (Use Helpers, keep logic identical) --- - num_cables = cable_system.num_cables - dx = 400 - for i in 1:num_cables - cable_position = cable_system.cables[i] - coax1 = addelement!(row_schematic, "User") - coax1_id = _next_id() - coax1_attrs = Dict( - "classid" => "UserCmp", "name" => "master:Cable_Coax", "id" => coax1_id, - "x" => "$(234+(i-1)*dx)", "y" => "612", "w" => "311", "h" => "493", - "z" => "-1", - "orient" => "0", "defn" => "master:Cable_Coax", "link" => "-1", "q" => "4", - "disable" => "false", - ) - _set_attributes!(coax1, coax1_attrs) # Use helper - - coax1_pl = addelement!(coax1, "paramlist") - # Original sets attributes on paramlist AND adds params - replicate exactly - coax1_pl["link"] = "-1" - coax1_pl["name"] = "" - coax1_pl["crc"] = "-1" - - # --- Parameter Calculation (Identical Logic from Original) --- - component_ids = collect(keys(cable_position.design_data.components)) - num_cable_parts = length(component_ids) - if num_cable_parts > 4 - error( - "Cable $(cable_position.design_data.cable_id) has $num_cable_parts parts, exceeding the limit of 4 (core/sheath/armor/outer).", - ) - end - conn = cable_position.conn - elim1 = length(conn) >= 2 && conn[2] == 0 ? "1" : "0" - elim2 = length(conn) >= 3 && conn[3] == 0 ? "1" : "0" - elim3 = length(conn) >= 4 && conn[4] == 0 ? "1" : "0" - cable_x = cable_position.horz - cable_y = cable_position.vert - - # Build the parameter list exactly as in the original's logic - coax1_params_vector = Vector{Tuple{String, String}}() # Renamed variable - # Base parameters - push!(coax1_params_vector, ("CABNUM", "$i")) - push!(coax1_params_vector, ("Name", "$(cable_position.design_data.cable_id)")) - push!(coax1_params_vector, ("X", format_nominal(cable_x))) - push!(coax1_params_vector, ("OHC", "$(cable_y < 0 ? 0 : 1)")) - push!( - coax1_params_vector, - ("Y", (cable_y < 0 ? format_nominal(abs(cable_y)) : "0.0")), - ) - push!(coax1_params_vector, ("Y2", (cable_y > 0 ? format_nominal(cable_y) : "0.0"))) - push!(coax1_params_vector, ("ShuntA", "1.0e-11 [mho/m]")) - push!(coax1_params_vector, ("FLT", format_nominal(base_freq))) - push!(coax1_params_vector, ("RorT", "0")) - push!(coax1_params_vector, ("LL", "$(2*num_cable_parts-1)")) - push!(coax1_params_vector, ("CROSSBOND", "0")) - push!(coax1_params_vector, ("GROUPNO", "1")) - push!( - coax1_params_vector, - ("CBC1", "1"), - ("CBC2", "0"), - ("CBC3", "0"), - ("CBC4", "0"), - ) - push!(coax1_params_vector, ("SHRad", "1")) - push!(coax1_params_vector, ("LC", "3")) - - # Component parameters (Keep identical logic) - ω = 2 * π * base_freq - for (idx, component_id) in enumerate(component_ids) - component = cable_position.design_data.components[component_id] - C_eq = component.insulator_group.shunt_capacitance - G_eq = component.insulator_group.shunt_conductance - loss_factor = C_eq > 1e-18 ? G_eq / (ω * C_eq) : 0.0 # Avoid NaN/Inf - - sig_digits_props = 6 - max_loss_tangent = 10.0 - - if idx == 1 # Core - push!(coax1_params_vector, ("CONNAM1", uppercasefirst(component.id))) - push!( - coax1_params_vector, - ("R1", format_nominal(component.conductor_group.r_in)), - ) - push!( - coax1_params_vector, - ("R2", format_nominal(component.conductor_group.r_ex)), - ) - push!( - coax1_params_vector, - ( - "RHOC", - format_nominal( - component.conductor_props.rho, - sigdigits = sig_digits_props, - ), - ), - ) - push!( - coax1_params_vector, - ( - "PERMC", - format_nominal( - component.conductor_props.mu_r, - sigdigits = sig_digits_props, - ), - ), - ) - push!( - coax1_params_vector, - ("R3", format_nominal(component.insulator_group.r_ex)), - ) - push!(coax1_params_vector, ("T3", "0.0000")) - push!(coax1_params_vector, ("SemiCL", "0")) - push!(coax1_params_vector, ("SL2", "0.0000")) - push!(coax1_params_vector, ("SL1", "0.0000")) - push!( - coax1_params_vector, - ( - "EPS1", - format_nominal( - component.insulator_props.eps_r, - sigdigits = sig_digits_props, - ), - ), - ) - push!( - coax1_params_vector, - ( - "PERM1", - format_nominal( - component.insulator_props.mu_r, - sigdigits = sig_digits_props, - ), - ), - ) - push!( - coax1_params_vector, - ( - "LT1", - format_nominal( - loss_factor, - sigdigits = sig_digits_props, - maxval = max_loss_tangent, - ), - ), - ) - elseif idx == 2 # Sheath - push!(coax1_params_vector, ("CONNAM2", uppercasefirst(component.id))) - push!( - coax1_params_vector, - ("R4", format_nominal(component.conductor_group.r_ex)), - ) - push!(coax1_params_vector, ("T4", "0.0000")) - push!( - coax1_params_vector, - ( - "RHOS", - format_nominal( - component.conductor_props.rho, - sigdigits = sig_digits_props, - ), - ), - ) - push!( - coax1_params_vector, - ( - "PERMS", - format_nominal( - component.conductor_props.mu_r, - sigdigits = sig_digits_props, - ), - ), - ) - push!(coax1_params_vector, ("elim1", elim1)) - push!( - coax1_params_vector, - ("R5", format_nominal(component.insulator_group.r_ex)), - ) - push!(coax1_params_vector, ("T5", "0.0000")) - push!( - coax1_params_vector, - ( - "EPS2", - format_nominal( - component.insulator_props.eps_r, - sigdigits = sig_digits_props, - ), - ), - ) - push!( - coax1_params_vector, - ( - "PERM2", - format_nominal( - component.insulator_props.mu_r, - sigdigits = sig_digits_props, - ), - ), - ) - push!( - coax1_params_vector, - ( - "LT2", - format_nominal( - loss_factor, - sigdigits = sig_digits_props, - maxval = max_loss_tangent, - ), - ), - ) - elseif idx == 3 # Armor - push!(coax1_params_vector, ("CONNAM3", uppercasefirst(component.id))) - push!( - coax1_params_vector, - ("R6", format_nominal(component.conductor_group.r_ex)), - ) - push!(coax1_params_vector, ("T6", "0.0000")) - push!( - coax1_params_vector, - ( - "RHOA", - format_nominal( - component.conductor_props.rho, - sigdigits = sig_digits_props, - ), - ), - ) - push!( - coax1_params_vector, - ( - "PERMA", - format_nominal( - component.conductor_props.mu_r, - sigdigits = sig_digits_props, - ), - ), - ) - push!(coax1_params_vector, ("elim2", elim2)) - push!( - coax1_params_vector, - ("R7", format_nominal(component.insulator_group.r_ex)), - ) - push!(coax1_params_vector, ("T7", "0.0000")) - push!( - coax1_params_vector, - ( - "EPS3", - format_nominal( - component.insulator_props.eps_r, - sigdigits = sig_digits_props, - ), - ), - ) - push!( - coax1_params_vector, - ( - "PERM3", - format_nominal( - component.insulator_props.mu_r, - sigdigits = sig_digits_props, - ), - ), - ) - push!( - coax1_params_vector, - ( - "LT3", - format_nominal( - loss_factor, - sigdigits = sig_digits_props, - maxval = max_loss_tangent, - ), - ), - ) - elseif idx == 4 # Outer - push!(coax1_params_vector, ("CONNAM4", uppercasefirst(component.id))) - push!( - coax1_params_vector, - ("R8", format_nominal(component.conductor_group.r_ex)), - ) - push!(coax1_params_vector, ("T8", "0.0000")) - push!( - coax1_params_vector, - ( - "RHOO", - format_nominal( - component.conductor_props.rho, - sigdigits = sig_digits_props, - ), - ), - ) - push!( - coax1_params_vector, - ( - "PERMO", - format_nominal( - component.conductor_props.mu_r, - sigdigits = sig_digits_props, - ), - ), - ) - push!(coax1_params_vector, ("elim3", elim3)) - push!( - coax1_params_vector, - ("R9", format_nominal(component.insulator_group.r_ex)), - ) - push!(coax1_params_vector, ("T9", "0.0000")) - push!( - coax1_params_vector, - ( - "EPS4", - format_nominal( - component.insulator_props.eps_r, - sigdigits = sig_digits_props, - ), - ), - ) - push!( - coax1_params_vector, - ( - "PERM4", - format_nominal( - component.insulator_props.mu_r, - sigdigits = sig_digits_props, - ), - ), - ) - push!( - coax1_params_vector, - ( - "LT4", - format_nominal( - loss_factor, - sigdigits = sig_digits_props, - maxval = max_loss_tangent, - ), - ), - ) - end - end - - # Default empty values (Keep identical logic) - if num_cable_parts < 2 - append!( - coax1_params_vector, - [ - ("CONNAM2", "none"), - ("R4", "0.0"), - ("T4", "0.0000"), - ("RHOS", "0.0"), - ("PERMS", "0.0"), - ("elim1", "0"), - ("R5", "0.0"), - ("T5", "0.0000"), - ("EPS2", "0.0"), - ("PERM2", "0.0"), - ("LT2", "0.0000"), - ], - ) - end - if num_cable_parts < 3 - append!( - coax1_params_vector, - [ - ("CONNAM3", "none"), - ("R6", "0.0"), - ("T6", "0.0000"), - ("RHOA", "0.0"), - ("PERMA", "0.0"), - ("elim2", "0"), - ("R7", "0.0"), - ("T7", "0.0000"), - ("EPS3", "0.0"), - ("PERM3", "0.0"), - ("LT3", "0.0000"), - ], - ) - end - if num_cable_parts < 4 - append!( - coax1_params_vector, - [ - ("CONNAM4", "none"), - ("R8", "0.0"), - ("T8", "0.0000"), - ("RHOO", "0.0"), - ("PERMO", "0.0"), - ("elim3", "0"), - ("R9", "0.0"), - ("T9", "0.0000"), - ("EPS4", "0.0"), - ("PERM4", "0.0"), - ("LT4", "0.0000"), - ], - ) - end - - # Add all collected parameters to the paramlist created earlier - _add_params_to_list!(coax1_pl, coax1_params_vector) # Use helper - - end # End Coax cable loop - - # --- Line_Ground Component (Use Helpers) --- - ground = addelement!(row_schematic, "User") - ground_id = _next_id() - ground_attrs = Dict( - "classid" => "UserCmp", "name" => "master:Line_Ground", "id" => ground_id, - "x" => "504", "y" => "288", "w" => "793", "h" => "88", "z" => "-1", - "orient" => "0", - "defn" => "master:Line_Ground", "link" => "-1", "q" => "4", "disable" => "false", - ) - _set_attributes!(ground, ground_attrs) # Use helper - - ground_pl = addelement!(ground, "paramlist") - # Original sets attributes on paramlist AND adds params - replicate exactly - ground_pl["link"] = "-1" - ground_pl["name"] = "" - ground_pl["crc"] = "-1" - - earth_layer = earth_props.layers[end] - ground_params_vector = [ # Renamed variable - ("EarthForm2", "0"), ("EarthForm", "3"), ("EarthForm3", "2"), ("GrRho", "0"), - ("GRRES", format_nominal(earth_layer.base_rho_g)), - ("GPERM", format_nominal(earth_layer.base_mur_g)), - ("K0", "0.001"), ("K1", "0.01"), ("alpha", "0.7"), - ("GRP", format_nominal(earth_layer.base_epsr_g)), - ] - _add_params_to_list!(ground_pl, ground_params_vector) # Use helper - - # --- Resource List and Hierarchy (Identical Nested Structure from Original) --- - addelement!(project, "List")["classid"] = "Resource" - - hierarchy = addelement!(project, "hierarchy") - # Nested calls exactly as in the original, linking to INSTANCE IDs from id_map - call1 = addelement!(hierarchy, "call") - # The link should be to the Station Definition ID, not an instance - call1["link"] = id_map["DS_Defn"] # Corrected link - call1["name"] = "$project_id:DS" - call1["z"] = "-1" - call1["view"] = "false" - call1["instance"] = "0" - - call2 = addelement!(call1, "call") - call2["link"] = id_map["Main"] # Links to Main User INSTANCE ID (as per original id_map usage) - call2["name"] = "$project_id:Main" - call2["z"] = "-1" - call2["view"] = "false" - call2["instance"] = "0" - - call3 = addelement!(call2, "call") - call3["link"] = id_map["CableSystem"] # Links to CableSystem User INSTANCE ID (as per original id_map usage) - call3["name"] = "$project_id:CableSystem" - call3["z"] = "-1" - call3["view"] = "true" - call3["instance"] = "0" - - try - # Use pretty print option for debugging comparisons if needed - # open(filename, "w") do io; prettyprint(io, doc); end - write(file_name, doc) # Standard write - if isfile(file_name) - @info "PSCAD file saved to: $(display_path(file_name))" - end - return file_name - catch e - @error "Failed to write PSCAD file '$(display_path(file_name))': $(e)" - isa(e, SystemError) && println("SystemError details: ", e.extrainfo) - return nothing - rethrow(e) # Rethrow to indicate failure clearly - end -end diff --git a/src/importexport/serialize.jl b/src/importexport/serialize.jl index aae900d3b..41d4d68cc 100644 --- a/src/importexport/serialize.jl +++ b/src/importexport/serialize.jl @@ -1,227 +1,357 @@ -""" -$(TYPEDSIGNATURES) +const JSON_SCHEMA_VERSION = "1.0.0" +const JSON_SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema" +const MATERIALS_SCHEMA = "linecablemodels.materials" +const CABLES_SCHEMA = "linecablemodels.cable" +const PROBLEM_SCHEMA = "linecablemodels.line_parameters_problem" + +_scalar_tag(::Type{Float16}) = "Float16" +_scalar_tag(::Type{Float32}) = "Float32" +_scalar_tag(::Type{Float64}) = "Float64" +_scalar_tag(::Type{BigFloat}) = "BigFloat" + +"""Encode a supported scalar, collection, Grid, or v1 declaration.""" +function serialize_value(value::AbstractFloat) + tag = _scalar_tag(typeof(value)) + encoded_value = value isa BigFloat ? string(value) : value + if isfinite(value) + return value isa BigFloat ? Dict("__type__"=>tag,"value"=>encoded_value,"precision"=>precision(value)) : + Dict("__type__" => tag, "value" => encoded_value) + end + special = isnan(value) ? "NaN" : signbit(value) ? "-Inf" : "Inf" + return Dict("__type__" => tag, "special" => special) +end -Defines which fields of a given object should be serialized to JSON. -This function acts as a trait. Specific types should overload this method -to customize which fields are needed for reconstruction. +serialize_value(value::Integer) = value +serialize_value(value::Union{Nothing, String, Bool}) = value +serialize_value(value::Symbol) = Dict("__type__" => "Symbol", "value" => String(value)) +function serialize_value(value::Complex) + return Dict( + "__type__" => "Complex", + "re" => serialize_value(real(value)), + "im" => serialize_value(imag(value)) + ) +end +function serialize_value(value::AbstractDict) + Dict(string(key) => serialize_value(item) for (key, item) in value) +end +function serialize_value(value::NamedTuple) + Dict(string(key) => serialize_value(item) for (key, item) in pairs(value)) +end +function serialize_value(value::Union{AbstractVector, Tuple}) + [serialize_value(item) for item in value] +end -# Arguments +function _node(kind::AbstractString; fields...) + Dict{String, Any}( + "kind" => String(kind), + (String(name) => serialize_value(value) for (name, value) in pairs(fields))... + ) +end -- `obj`: The object whose serializable fields are to be determined. +function _material_record(value::Material) + return Dict{String, Any}( + "kind" => String(value.kind), + "rho" => serialize_value(value.rho), + "eps_r" => serialize_value(value.eps_r), + "mu_r" => serialize_value(value.mu_r), + "T0" => serialize_value(value.T0), + "alpha" => serialize_value(value.alpha), + "rho_thermal" => serialize_value(value.rho_thermal), + "theta_max" => serialize_value(value.theta_max), + "tan_delta" => serialize_value(value.tan_delta), + "sigma_solar" => serialize_value(value.sigma_solar) + ) +end -# Returns +function _material_record(value::RadialDielectric) + return Dict{String, Any}( + "kind" => "radial_dielectric", + "materials" => [_material_record(m) for m in value.materials], + "weights" => serialize_value(value.weights), + "mu_r" => serialize_value(value.mu_r)) +end -- A tuple of symbols representing the fields of `obj` that should be serialized. +function serialize_value(value::AbstractMaterial) + Dict( + "type" => "material", + "value" => _material_record(value) + ) +end -# Methods +serialize_value(value::Disk) = _node("disk"; r = value.r) +serialize_value(value::Rectangle) = _node("rectangle"; w = value.w, h = value.h) +serialize_value(value::Ellipse) = _node("ellipse"; a = value.a, b = value.b) +function serialize_value(value::Sector) + return _node( + "sector"; + span = value.span, + r_base = value.r_base, + r_back = value.r_back, + fillet = value.fillet + ) +end +serialize_value(value::Annulus) = _node("annulus"; ri = value.ri, ro = value.ro) +serialize_value(value::Shell) = _node("shell"; t = value.t) +serialize_value(value::Polygon) = _node("polygon"; points = value.points) +serialize_value(value::Pose2) = _node("pose2"; x = value.x, y = value.y, φ = value.φ) + +function serialize_value(value::EarthLayer) + return _node( + "earth_layer"; + rho = value.rho, + eps_r = value.eps_r, + mu_r = value.mu_r, + thickness = value.thickness + ) +end +function serialize_value(value::EarthModel) + return _node( + "earth_model"; + vertical_layers = value.vertical_layers, + layers = value.layers + ) +end -$(METHODLIST) -""" -function _serializable_fields end +function serialize_value(value::Ring) + return _node("ring"; n = value.n, r = value.r, φ0 = value.φ0, + span = value.span, gap_frac = value.gap_frac) +end +function serialize_value(value::Polar) + return _node("polar"; nr = value.nr, nφ = value.nφ, r0 = value.r0, + dr = value.dr, φ0 = value.φ0, span = value.span) +end +function serialize_value(value::Fill) + _node("fill"; r = value.r, φ = value.φ, φ0 = value.φ0, span = value.span) +end +function serialize_value(value::Lattice) + _node("lattice"; nx = value.nx, ny = value.ny, dx = value.dx, dy = value.dy) +end +serialize_value(value::FillFactor) = _node("fill_factor"; η = value.η) +serialize_value(::typeof(capacity())) = _node("capacity") +serialize_value(value::LayRatio) = _node("lay_ratio"; q = value.q) +serialize_value(value::Pitch) = _node("pitch"; p = value.p) +serialize_value(value::LayAngle) = _node("lay_angle"; α = value.α) +function serialize_value(value::Helix) + _node("helix"; lay = value.lay, dir = value.dir, φ0 = value.φ0) +end -# Default fallback: Serialize all fields. This might include computed fields -# that are not needed for reconstruction. Overload for specific types. -_serializable_fields(obj::T) where {T} = fieldnames(T) +function serialize_value(value::Region; material_name = nothing) + material = material_name === nothing ? serialize_value(value.material) : + String(material_name(value.material)) + return Dict( + "kind" => "region", + "tag" => String(value.tag), + "primitive" => serialize_value(value.primitive), + "material" => material + ) +end +function serialize_value(value::Stack; material_name = nothing) + return Dict( + "kind" => "stack", + "items" => [serialize_value(item; material_name) for item in value.items] + ) +end +function serialize_value(value::Group; material_name = nothing) + return Dict( + "kind" => "group", + "name" => String(value.name), + "at" => serialize_value(value.at), + "item" => serialize_value(value.item; material_name), + "pattern" => serialize_value(value.pattern), + "path" => serialize_value(value.path), + "compact" => serialize_value(value.compact), + "boundary" => serialize_value(value.boundary) + ) +end +function serialize_value( + value::Assembly{<:Any, <:AbstractCablePart}; + material_name = nothing +) + names = value.names === nothing ? nothing : String.(value.names) + return Dict( + "kind" => "assembly", + "at" => serialize_value(value.at), + "item" => serialize_value(value.item; material_name), + "pattern" => serialize_value(value.pattern), + "path" => serialize_value(value.path), + "compact" => serialize_value(value.compact), + "names" => names + ) +end +function serialize_value(value::Assembly{<:Any, <:Tuple}; material_name = nothing) + return Dict( + "kind" => "assembly", + "at" => serialize_value(value.at), + "members" => [Dict( + "at" => serialize_value(member.at), + "item" => serialize_value(member.item; material_name) + ) for member in value.item] + ) +end +function serialize_value(value::Enclosure; material_name = nothing) + fill = value.fill isa Material ? + (material_name === nothing ? serialize_value(value.fill) : + String(material_name(value.fill))) : + serialize_value(value.fill; material_name) + wall = value.wall === nothing ? nothing : serialize_value(value.wall; material_name) + return Dict( + "kind" => "enclosure", + "tag" => String(value.tag), + "at" => serialize_value(value.at), + "primitive" => serialize_value(value.primitive), + "item" => serialize_value(value.item; material_name), + "fill" => fill, + "wall" => wall + ) +end -# Define exactly which fields are needed to reconstruct each object. -# These typically match the constructor arguments or the minimal set -# required by the reconstruction logic (e.g., for groups). +function serialize_value(value::CableDesign; material_name = nothing) + return Dict( + "kind" => "cable_design", + "cable_id" => value.cable_id, + "nominal_data" => [Dict( + "name" => String(name), + "value" => serialize_value(item) + ) for (name, item) in pairs(value.nominal_data)], + "origin" => serialize_value(value.origin; material_name) + ) +end -# Core Data Types -_serializable_fields(::Material) = (:rho, :eps_r, :mu_r, :T0, :alpha) -_serializable_fields(::NominalData) = ( - :designation_code, - :U0, - :U, - :conductor_cross_section, - :screen_cross_section, - :armor_cross_section, - :resistance, - :capacitance, - :inductance, -) +function serialize_value(value::LineCableSystem) + return Dict( + "kind" => "line_cable_system", + "system_id" => value.system_id, + "line_length" => serialize_value(value.line_length), + "designs" => [serialize_value(design) for design in value.designs], + "positions" => serialize_value(value.positions), + "input_positions" => serialize_value(value.input_positions), + "clearances" => + [serialize_value(collect(row)) for row in eachrow(value.clearances)], + "connections" => serialize_value(value.connections), + "environment" => serialize_value(value.environment) + ) +end -# Layer Types (Conductor Parts) -_serializable_fields(::CircStrands) = ( - :r_in, # Needed for first layer reconstruction - :radius_wire, - :num_wires, - :lay_ratio, - :material_props, - :temperature, - :lay_direction, -) +function serialize_value(value::Engine.LineParametersProblem) + return Dict( + "kind" => "line_parameters_problem", + "system" => serialize_value(value.system), + "temperature" => serialize_value(value.temperature), + "earth_props" => serialize_value(value.earth_props), + "frequencies" => serialize_value(value.frequencies) + ) +end -# Layer Types (Conductor Parts) -_serializable_fields(::RectStrands) = ( - :r_in, # Needed for first layer reconstruction - :thickness, - :width, - :num_wires, - :lay_ratio, - :material_props, - :temperature, - :lay_direction, -) +function serialize_value(grid::DeterministicGrid) + return Dict("grid" => serialize_value(collect(grid.vals))) +end +function serialize_value(grid::RelativeGrid) + return Dict( + "grid" => serialize_value(collect(grid.vals)), + "rel" => serialize_value(collect(grid.rel_err)) + ) +end +function serialize_value(grid::AbsoluteGrid) + return Dict( + "grid" => serialize_value(collect(grid.vals)), + "abs" => serialize_value(collect(grid.abs_err)) + ) +end -_serializable_fields(::Tubular) = ( - :r_in, # Needed for first layer reconstruction - :r_ex, - :material_props, - :temperature, +function _collect_materials!(materials::Vector{AbstractMaterial}, part::Region) + any(item -> isequal(item, part.material), materials) || push!(materials, part.material) + return materials +end +function _collect_materials!(materials::Vector{AbstractMaterial}, part::Stack) + foreach(item -> _collect_materials!(materials, item), part.items) + return materials +end +function _collect_materials!(materials::Vector{AbstractMaterial}, part::Group) + return _collect_materials!(materials, part.item) +end +function _collect_materials!( + materials::Vector{AbstractMaterial}, + part::Assembly{<:Any, <:AbstractCablePart} ) -_serializable_fields(::Strip) = ( - :r_in, # Needed for first layer reconstruction - :r_ex, - :width, - :lay_ratio, - :material_props, - :temperature, - :lay_direction, + return _collect_materials!(materials, part.item) +end +function _collect_materials!( + materials::Vector{AbstractMaterial}, + part::Assembly{<:Any, <:Tuple} ) + foreach(member -> _collect_materials!(materials, member.item), part.item) + return materials +end +function _collect_materials!(materials::Vector{AbstractMaterial}, part::Enclosure) + _collect_materials!(materials, part.item) + part.fill isa Material ? + (any(item -> isequal(item, part.fill), materials) || push!(materials, part.fill)) : + _collect_materials!(materials, part.fill) + part.wall === nothing || _collect_materials!(materials, part.wall) + return materials +end -# Layer Types (Insulator Parts) -_serializable_fields(::Insulator) = ( - :r_in, # Needed for first layer reconstruction - :r_ex, - :material_props, - :temperature, -) -_serializable_fields(::Semicon) = ( - :r_in, # Needed for first layer reconstruction - :r_ex, - :material_props, - :temperature, -) +function _json_document(library::MaterialsLibrary) + return Dict( + "\$schema" => JSON_SCHEMA_DIALECT, + "format" => MATERIALS_SCHEMA, + "version" => JSON_SCHEMA_VERSION, + "materials" => Dict( + name => _material_record(material) + for (name, material) in sort!(collect(library.data); by = first) + ) + ) +end + +function _json_document(library::CablesLibrary) + materials = AbstractMaterial[] + cable_ids = sort!(collect(keys(library.data))) + for cable_id in cable_ids + _collect_materials!(materials, library.data[cable_id].origin) + end + names = Dict{Int, String}( + index => "$(material.kind)_$index" for (index, material) in enumerate(materials) + ) + material_name(material) = names[only(findall(item -> isequal(item, material), materials))] + return Dict( + "\$schema" => JSON_SCHEMA_DIALECT, + "format" => CABLES_SCHEMA, + "version" => JSON_SCHEMA_VERSION, + "materials" => Dict( + names[index] => _material_record(material) + for (index, material) in enumerate(materials) + ), + "root" => Dict( + "kind" => "cable_library", + "cables" => Dict( + cable_id => merge( + serialize_value(library.data[cable_id]; material_name), + Dict("datasheet" => Dict( + String(name) => serialize_value(item) + for (name, item) in pairs(library.datasheets[cable_id]) + )) + ) + for cable_id in cable_ids + ) + ) + ) +end + +function _json_document(system::LineCableSystem) + return Dict( + "\$schema" => JSON_SCHEMA_DIALECT, + "format" => CABLES_SCHEMA, + "version" => JSON_SCHEMA_VERSION, + "materials" => Dict{String, Any}(), + "root" => serialize_value(system) + ) +end -# Group Types - Only serialize the layers needed for reconstruction. -_serializable_fields(::ConductorGroup) = (:layers,) -_serializable_fields(::InsulatorGroup) = (:layers,) - -# Component & Design Types -_serializable_fields(::CableComponent) = (:id, :conductor_group, :insulator_group) -# For CableDesign, we need components and nominal data. ID is handled as the key. -_serializable_fields(::CableDesign) = (:cable_id, :nominal_data, :components) - -# Library Types -_serializable_fields(::CablesLibrary) = (:data,) -_serializable_fields(::MaterialsLibrary) = (:data,) - - -#= -Serializes a Julia value into a JSON-compatible representation. -Handles special types like Measurements, Inf/NaN, Symbols, and custom structs -using the `_serializable_fields` trait. - -# Arguments -- `value`: The Julia value to serialize. - -# Returns -- A JSON-compatible representation (Dict, Vector, Number, String, Bool, Nothing). -=# -# Helper: only used in serialization, never leaks to core math. -function _serialize_value(value) - - if isnothing(value) - return nothing - - elseif value isa Measurements.Measurement - v = Measurements.value(value) - u = Measurements.uncertainty(value) - return Dict( - "__type__" => "Measurement", - "value" => _serialize_value(v), - "uncertainty" => _serialize_value(u), - ) - - elseif value isa Number && !isfinite(value) - # Inf / -Inf / NaN stay tagged - local val_str - if isinf(value) - val_str = value > 0 ? "Inf" : "-Inf" - else - # NaN - val_str = "NaN" - end - return Dict("__type__" => "SpecialFloat", "value" => val_str) - - elseif value isa AbstractFloat - return Dict("__type__" => "Float", "value" => value) - - elseif value isa Integer - return Dict("__type__" => "Int", "value" => value) - - elseif value isa Complex - return Dict("__type__" => "Complex", - "re" => _serialize_value(real(value)), - "im" => _serialize_value(imag(value)), - ) - - elseif value isa Number || value isa String || value isa Bool - return value - - elseif value isa Symbol - return string(value) - - elseif value isa AbstractDict - return Dict(string(k) => _serialize_value(v) for (k, v) in value) - - elseif value isa Union{AbstractVector, Tuple} - return [_serialize_value(v) for v in value] - else - !isprimitivetype(typeof(value)) && fieldcount(typeof(value)) > 0 - # Custom structs - return _serialize_obj(value) - end -end - - -""" -$(TYPEDSIGNATURES) - -Serializes a Julia value into a JSON-compatible representation. -Handles special types like Measurements, Inf/NaN, Symbols, and custom structs -using the [`_serializable_fields`](@ref) trait. - -# Arguments -- `value`: The Julia value to serialize. - -# Returns -- A JSON-compatible representation (Dict, Vector, Number, String, Bool, Nothing). -""" -function _serialize_obj(obj) - T = typeof(obj) - # Get fully qualified type name (e.g., Main.MyModule.MyType) - try - mod = parentmodule(T) - typeName = nameof(T) - type_str = string(mod, ".", typeName) - - result = Dict{String, Any}() - result["__julia_type__"] = type_str - - # Get the fields to serialize using the trait function - fields_to_include = _serializable_fields(obj) - - # Iterate only through the fields specified by the trait - for field in fields_to_include - if hasproperty(obj, field) - value = getproperty(obj, field) - result[string(field)] = _serialize_value(value) # Recursively serialize - else - # This indicates an issue with the _serializable_fields definition for T - @warn "Field :$field specified by _serializable_fields(::$T) not found in object. Skipping." - end - end - return result - catch e - Base.error( - "Error determining module or type name for object of type $T: $e. Cannot serialize.", - ) - # Return a representation indicating the error - return Dict( - "__error__" => "Serialization failed for type $T", - "__details__" => string(e), - ) - end +function _json_document(problem::Engine.LineParametersProblem) + return Dict( + "\$schema" => JSON_SCHEMA_DIALECT, + "format" => PROBLEM_SCHEMA, + "version" => JSON_SCHEMA_VERSION, + "root" => serialize_value(problem) + ) end diff --git a/src/importexport/tralin.jl b/src/importexport/tralin.jl index 05ddf3ec0..ca8b1f27e 100644 --- a/src/importexport/tralin.jl +++ b/src/importexport/tralin.jl @@ -1,471 +1,486 @@ const _TRALIN_COMP = ("CORE", "SHEATH", "ARMOUR") - function export_data(::Val{:tralin}, - cable_system::LineCableSystem, - earth_props::EarthModel; - freq = f₀, - file_name::Union{String, Nothing} = nothing, -)::Union{String, Nothing} - - # -- helpers --------------------------------------------------------------- - _freqs(x) = x isa AbstractVector ? collect(x) : [x] - _fmt(x) = string(round(Float64(to_nominal(x)); digits = 6)) - _maybe(x) = (x === nothing) ? "" : _fmt(x) - - # Resolve output file name (prefix "tr_"; mirror XML semantics) - if isnothing(file_name) - file_name = joinpath(@__DIR__, "tr_$(cable_system.system_id).f05") - else - dir = dirname(file_name) - fname = basename(file_name) - # Ensure filename has "tr_" prefix, but preserve user's name - prefixed_fname = startswith(fname, "tr_") ? fname : "tr_" * fname - # Rejoin with original path, handling relative vs absolute - file_name = - isabspath(file_name) ? joinpath(dir, prefixed_fname) : - joinpath(@__DIR__, dir, prefixed_fname) - end - - num_phases = length(cable_system.cables) - freqs = map(f -> to_nominal(f), _freqs(freq)) - - # -- build TRALIN lines ---------------------------------------------------- - lines = String[] - - push!(lines, "TRALIN") - push!(lines, "TEXT,MODULE,LineCableModels run") - push!(lines, "OPTIONS") - push!(lines, "UNITS,METRIC") - push!(lines, "RUN-IDENTIFICATION,$(cable_system.system_id)") - push!(lines, "SEQUENCE,ON") - push!(lines, "MULTILAYER,ON") - push!(lines, "CONDUCTANCE,ON") - push!(lines, "!KEEP_CIRCUIT_MODE") - - push!(lines, "PARAMETERS") - push!(lines, "BASE-VALUES") - push!(lines, "ACCURACY,1e-7") - push!(lines, "BESSEL") - push!(lines, "TERMS,300") - for f in freqs - push!(lines, "FREQUENCY,$(_fmt(f))") - end - push!(lines, "INTEGRATION,AUTO-ADJUST,9") - push!(lines, "STEP,1e-6") - push!(lines, "UPPER-LIMIT,5.") - push!(lines, "SERIES-TERMS,300") - - nlayers = length(earth_props.layers) - - if nlayers == 2 - # [AIR, SOIL] => uniform semi-infinite earth - soil = earth_props.layers[end] - rho = _fmt(getproperty(soil, :base_rho_g)) - mu_r = hasfield(typeof(soil), :mu_r) ? _fmt(getproperty(soil, :mu_r)) : "1" - eps_r = hasfield(typeof(soil), :eps_r) ? _fmt(getproperty(soil, :eps_r)) : "1" - push!(lines, "SOIL-TYPE") - push!(lines, "UNIFORM,$rho,$mu_r,$eps_r") - else - # [AIR, TOP, (CENTRAL...), BOTTOM] => HORIZONTAL - push!(lines, "SOIL-TYPE") - push!(lines, "HORIZONTAL") - - # AIR: no thickness -> explicit empty field `,,` - push!(lines, " LAYER,AIR,1e+18,,1,1") - - n_earth = nlayers - 1 - names = - n_earth == 1 ? ["TOP"] : - n_earth == 2 ? ["TOP", "BOTTOM"] : - vcat("TOP", fill("CENTRAL", n_earth - 2), "BOTTOM") - - for (eidx, (lname, layer)) in enumerate(zip(names, earth_props.layers[2:end])) - rho = _fmt(getproperty(layer, :base_rho_g)) - mu_r = hasfield(typeof(layer), :base_mur_g) ? _fmt(getproperty(layer, :base_mur_g)) : "1" - eps_r = hasfield(typeof(layer), :base_epsr_g) ? _fmt(getproperty(layer, :base_epsr_g)) : "1" - - if eidx == n_earth - # BOTTOM: no thickness -> explicit empty field `,,` - push!(lines, " LAYER,$lname,$rho,,$mu_r,$eps_r") - else - # TOP/CENTRAL: include thickness if available; otherwise leave it empty to keep the slot - thk = - ( - hasfield(typeof(layer), :t) && - getproperty(layer, :t) !== nothing - ) ? - _fmt(getproperty(layer, :t)) : "" - push!(lines, " LAYER,$lname,$rho,$thk,$mu_r,$eps_r") - end - end - end - - push!(lines, "SYSTEM") - - for (pidx, cable) in enumerate(cable_system.cables) - # Phase group position - push!(lines, "GROUP,PH-$(pidx),$(_fmt(cable.horz)),$(_fmt(cable.vert))") - - comps_vec = cable.design_data.components # assumed Vector in your corrected model - ncomp = length(comps_vec) - if ncomp > 3 - throw( - ArgumentError( - "TRALIN supports at most 3 concentric components (CORE/SHEATH/ARMOR); got $ncomp for cable index $pidx.", - ), - ) - end - # Outer radius for CABLE line - outer_R = to_nominal(comps_vec[end].insulator_group.r_ex) - push!(lines, "CABLE,CA-$(pidx),$(_fmt(outer_R))") - - # Strict connection vector - conn = getproperty(cable, :conn) - if !(conn isa AbstractVector) - throw( - ArgumentError( - "cable.conn must be a Vector of Int mappings (0 or 1..$num_phases) for cable index $pidx.", - ), - ) - end - if length(conn) < ncomp - throw( - ArgumentError( - "cable.conn length $(length(conn)) < number of components $ncomp for cable index $pidx.", - ), - ) - end - - # Emit COMPONENT lines (same syntax for CORE/SHEATH/ARMOR) - for i in 1:ncomp - label = _TRALIN_COMP[i] - comp = comps_vec[i] - comp_id = String(getproperty(comp, :id)) # <-- component name from your datamodel - - conn_val = Int(conn[i]) # 0 or 1..N phases - - cond_group = comp.conductor_group - ins_group = comp.insulator_group - cond_props = comp.conductor_props - ins_props = comp.insulator_props - - rin = _fmt(cond_group.r_in) - rex = _fmt(cond_group.r_ex) - rho = _fmt(cond_props.rho/ρ₀) # values in TRALIN are normalized to match the annealed copper - muC = _fmt(cond_props.mu_r) - epsI = _fmt(ins_props.eps_r) # coating εr - - # COMPONENT,,,,,,,0, - push!(lines, "$label,$comp_id,$conn_val,$rex,$rin,$rho,$muC,0,$epsI") - end - end - - push!(lines, "ENDPROGRAM") - - try - open(file_name, "w") do fid - for ln in lines - write(fid, ln); - write(fid, '\n') - end - end - @info "TRALIN file saved to: $(display_path(file_name))" - return file_name - catch e - @error "Failed to write TRALIN file '$(display_path(file_name))'" exception = - (e, catch_backtrace()) - return nothing - end + cable_system::LineCableSystem, + earth_props::EarthModel; + freq = 50.0, + file_name::Union{String, Nothing} = nothing +)::String + + # helpers + _freqs(x) = x isa AbstractVector ? collect(x) : [x] + _fmt(x) = string(round(Float64(nominal(x)); digits = 6)) + _maybe(x) = (x === nothing) ? "" : _fmt(x) + + # Prefix the output filename with "tr_", matching the XML exporter. + if isnothing(file_name) + file_name = joinpath(@__DIR__, "tr_$(cable_system.system_id).f05") + else + dir = dirname(file_name) + fname = basename(file_name) + # Add the prefix while preserving the supplied filename. + prefixed_fname = startswith(fname, "tr_") ? fname : "tr_" * fname + # Resolve relative paths beside this exporter. + file_name = isabspath(file_name) ? joinpath(dir, prefixed_fname) : + joinpath(@__DIR__, dir, prefixed_fname) + end + + freqs = map(f -> nominal(f), _freqs(freq)) + # TRALIN defines this explicit radial homogenization choice. The choice is not stored + # on CableDesign. Unsupported physical geometry fails at the local + # homogenization step. + tralin_components(design) = DataModel.flatten( + design, + first(freqs) + ) + + # build TRALIN lines + lines = String[] + + push!(lines, "TRALIN") + push!(lines, "TEXT,MODULE,LineCableModels run") + push!(lines, "OPTIONS") + push!(lines, "UNITS,METRIC") + push!(lines, "RUN-IDENTIFICATION,$(cable_system.system_id)") + push!(lines, "SEQUENCE,ON") + push!(lines, "MULTILAYER,ON") + push!(lines, "CONDUCTANCE,ON") + push!(lines, "!KEEP_CIRCUIT_MODE") + + push!(lines, "PARAMETERS") + push!(lines, "BASE-VALUES") + push!(lines, "ACCURACY,1e-7") + push!(lines, "BESSEL") + push!(lines, "TERMS,300") + for f in freqs + push!(lines, "FREQUENCY,$(_fmt(f))") + end + push!(lines, "INTEGRATION,AUTO-ADJUST,9") + push!(lines, "STEP,1e-6") + push!(lines, "UPPER-LIMIT,5.") + push!(lines, "SERIES-TERMS,300") + + nlayers = length(earth_props.layers) + + if nlayers == 2 + # [AIR, SOIL] => uniform semi-infinite earth + soil = earth_props.layers[end] + rho = _fmt(soil.rho) + mu_r = _fmt(soil.mu_r) + eps_r = _fmt(soil.eps_r) + push!(lines, "SOIL-TYPE") + push!(lines, "UNIFORM,$rho,$mu_r,$eps_r") + else + # [AIR, TOP, (CENTRAL...), BOTTOM] => HORIZONTAL + push!(lines, "SOIL-TYPE") + push!(lines, "HORIZONTAL") + + # AIR: no thickness -> explicit empty field `,,` + push!(lines, " LAYER,AIR,1e+18,,1,1") + + n_earth = nlayers - 1 + names = n_earth == 1 ? ["TOP"] : + n_earth == 2 ? ["TOP", "BOTTOM"] : + vcat("TOP", fill("CENTRAL", n_earth - 2), "BOTTOM") + + for (eidx, (lname, layer)) in enumerate(zip(names, earth_props.layers[2:end])) + rho = _fmt(layer.rho) + mu_r = _fmt(layer.mu_r) + eps_r = _fmt(layer.eps_r) + + if eidx == n_earth + # BOTTOM: no thickness -> explicit empty field `,,` + push!(lines, " LAYER,$lname,$rho,,$mu_r,$eps_r") + else + # TOP/CENTRAL: include thickness if available. Otherwise leave it empty to keep the slot + thk = ( + isfinite(layer.thickness) + ) ? + _fmt(layer.thickness) : "" + push!(lines, " LAYER,$lname,$rho,$thk,$mu_r,$eps_r") + end + end + end + + push!(lines, "SYSTEM") + + for (pidx, (design, position, connections)) in enumerate(zip( + cable_system.designs, + cable_system.positions, + cable_system.connections + )) + # Phase group position + push!(lines, "GROUP,PH-$(pidx),$(_fmt(position.x)),$(_fmt(position.y))") + + comps_vec = tralin_components(design) + ncomp = length(comps_vec) + if ncomp > 3 + throw( + ArgumentError( + "TRALIN supports at most 3 concentric components (CORE/SHEATH/ARMOR); got $ncomp for cable index $pidx.", + ), + ) + end + # Outer radius for CABLE line + outer_R = nominal(max( + comps_vec[end].conductor.r_ex, + comps_vec[end].dielectric.r_ex + )) + push!(lines, "CABLE,CA-$(pidx),$(_fmt(outer_R))") + + # Strict connection vector + conn = connections + if length(conn) < ncomp + throw( + ArgumentError( + "connection-vector length $(length(conn)) < number of components $ncomp for cable index $pidx.", + ), + ) + end + + # Emit COMPONENT lines (same syntax for CORE/SHEATH/ARMOR) + for i in 1:ncomp + label = _TRALIN_COMP[i] + comp = comps_vec[i] + comp_id = String(comp.name) + + conn_val = Int(conn[i]) # 0 or 1..N phases + + conductor = comp.conductor + dielectric = comp.dielectric + + rin = _fmt(conductor.r_in) + rex = _fmt(conductor.r_ex) + rho = _fmt(conductor.material.rho / 1.724e-8) + muC = _fmt(conductor.material.mu_r) + epsI = _fmt(dielectric.material.eps_r) + + # COMPONENT,,,,,,,0, + push!(lines, "$label,$comp_id,$conn_val,$rex,$rin,$rho,$muC,0,$epsI") + end + end + + push!(lines, "ENDPROGRAM") + + open(file_name, "w") do fid + for ln in lines + write(fid, ln) + write(fid, '\n') + end + end + @info "TRALIN file saved to: $(_display_path(file_name))" + return file_name end - -# --- internal utility: slice a block between an anchor and the next page header --- +# internal utility: slice a block between an anchor and the next page header # Finds the first line that contains `anchor` and returns the lines up to (but not including) # the next "TRALIN package - PAGE" header. Throws if not found. function _block_after_anchor(fileLines::Vector{String}, anchor::AbstractString) - start_idx = findfirst(l -> occursin(anchor, l), fileLines) - start_idx === nothing && throw(ArgumentError("Anchor not found: $anchor")) + start_idx = findfirst(l -> occursin(anchor, l), fileLines) + start_idx === nothing && throw(ArgumentError("Anchor not found: $anchor")) - # page header appears after each page break; we stop before it - page_hdr = "TRALIN package - PAGE" - stop_idx = findnext(l -> occursin(page_hdr, l), fileLines, start_idx + 1) - stop_idx === nothing && (stop_idx = length(fileLines) + 1) + # Stop before the next page header. + page_hdr = "TRALIN package - PAGE" + stop_idx = findnext(l -> occursin(page_hdr, l), fileLines, start_idx + 1) + stop_idx === nothing && (stop_idx = length(fileLines) + 1) - # drop the anchor line itself and the terminating page header (if any) - return fileLines[(start_idx+1):(stop_idx-1)] + # drop the anchor line itself and the terminating page header (if any) + return fileLines[(start_idx + 1):(stop_idx - 1)] end function _infer_tralin_order(file_or_lines)::Int - fileLines = - file_or_lines isa AbstractString ? readlines(String(file_or_lines)) : file_or_lines - - block = _block_after_anchor( - fileLines, - "CHARACTERISTICS OF ALL CONDUCTORS", - ) - - # Table rows look like: - # 1 1 1 1 1 core 0.00000 0.01885 ... - # Columns (first 5 numbers): CONDUCTOR, GROUP, CABLE, COAX, PHASE - # We capture the 5th integer (PHASE) and keep nonzero uniques. - phase_set = Set{Int}() - row_re = r"^\s*\d+\s+\d+\s+\d+\s+\d+\s+(\d+)\s+\S+" - - for ln in block - m = match(row_re, ln) - if m !== nothing - ph = parse(Int, m.captures[1]) - if ph != 0 - push!(phase_set, ph) - end - end - end - - isempty(phase_set) && throw( - ArgumentError( - "Could not infer phase count from the 'CHARACTERISTICS OF ALL CONDUCTORS' table.", - ), - ) - return length(phase_set) + fileLines = file_or_lines isa AbstractString ? readlines(String(file_or_lines)) : + file_or_lines + + block = _block_after_anchor( + fileLines, + "CHARACTERISTICS OF ALL CONDUCTORS" + ) + + # Table rows look like: + # 1 1 1 1 1 core 0.00000 0.01885 ... + # Columns (first 5 numbers): CONDUCTOR, GROUP, CABLE, COAX, PHASE + # Read the fifth integer (PHASE) and retain distinct nonzero values. + phase_set = Set{Int}() + row_re = r"^\s*\d+\s+\d+\s+\d+\s+\d+\s+(\d+)\s+\S+" + + for ln in block + m = match(row_re, ln) + if m !== nothing + ph = parse(Int, m.captures[1]) + if ph != 0 + push!(phase_set, ph) + end + end + end + + isempty(phase_set) && throw( + ArgumentError( + "Could not infer phase count from the 'CHARACTERISTICS OF ALL CONDUCTORS' table.", + ), + ) + return length(phase_set) end -# --- public: extract the frequency vector from the "FREQUENCY OF HARMONIC CURRENT" section --- +# public: extract the frequency vector from the "FREQUENCY OF HARMONIC CURRENT" section """ - extract_tralin_frequencies(file_or_lines) -> Vector{Float64} +$(TYPEDSIGNATURES) + +Read operating frequencies from a TRALIN output section \\[Hz\\]. + +# Arguments + +- `file_or_lines`: TRALIN output path or preloaded file lines. -Parses the list of operating frequencies from the `FREQUENCY OF HARMONIC CURRENT:` section -up to the next page header. Returns a `Vector{Float64}` in \\[Hz\\]. +# Returns -Accepts either a filename (`AbstractString`) or a preloaded `Vector{String}` with file lines. +- Operating frequencies through the next page header \\[Hz\\]. + +# Errors + +- Throws `ArgumentError` when the frequency section is absent or empty. """ function _extract_tralin_frequencies(file_or_lines)::Vector{Float64} - fileLines = - file_or_lines isa AbstractString ? readlines(String(file_or_lines)) : file_or_lines - - block = _block_after_anchor( - fileLines, - "FREQUENCY OF HARMONIC CURRENT:", - ) - - # Data lines look like: - # 1 1.00 - # 6 0.215E+04 - # We capture the second column as a float (supports E-notation). - freqs = Float64[] - row_re = r"^\s*\d+\s+([+-]?(?:\d+\.?\d*|\.\d+)(?:[Ee][+-]?\d+)?)\s*$" - - for ln in block - m = match(row_re, ln) - if m !== nothing - push!(freqs, parse(Float64, m.captures[1])) - end - end - - isempty(freqs) && throw( - ArgumentError("No frequency lines found under 'FREQUENCY OF HARMONIC CURRENT:'."), - ) - - return freqs + fileLines = file_or_lines isa AbstractString ? readlines(String(file_or_lines)) : + file_or_lines + + block = _block_after_anchor( + fileLines, + "FREQUENCY OF HARMONIC CURRENT:" + ) + + # Data lines look like: + # 1 1.00 + # 6 0.215E+04 + # Read the second column as a floating-point value with optional E notation. + freqs = Float64[] + row_re = r"^\s*\d+\s+([+-]?(?:\d+\.?\d*|\.\d+)(?:[Ee][+-]?\d+)?)\s*$" + + for ln in block + m = match(row_re, ln) + if m !== nothing + push!(freqs, parse(Float64, m.captures[1])) + end + end + + isempty(freqs) && throw( + ArgumentError("No frequency lines found under 'FREQUENCY OF HARMONIC CURRENT:'."), + ) + + return freqs end """ - parse_tralin_file(filename) +$(TYPEDSIGNATURES) + +Parse frequency-indexed impedance, admittance, and potential-coefficient +matrices from a TRALIN output file. + +# Arguments -Parse a TRALIN file and extract impedance, admittance, and potential coefficient matrices -for multiple frequency samples. +- `filename`: TRALIN output path. + +# Returns + +- A tuple containing frequencies \\[Hz\\], series impedance \\[Ω/m\\], shunt + admittance \\[S/m\\], and potential coefficients \\[Ω·m\\]. """ function parse_tralin_file(filename) - fileLines = readlines(filename) - - ord = _infer_tralin_order(fileLines) - freqs = _extract_tralin_frequencies(fileLines) - - # Get all occurrences of "GROUND WIRES ELIMINATED" - limited_str = "GROUND WIRES ELIMINATED" - all_idx = findall(row -> occursin(limited_str, row), fileLines) - - # Initialize arrays to store matrices for all frequency samples - Z_matrices = Vector{Matrix{ComplexF64}}(undef, length(all_idx)) - Y_matrices = Vector{Matrix{ComplexF64}}(undef, length(all_idx)) - P_matrices = Vector{Matrix{ComplexF64}}(undef, length(all_idx)) - - # Loop through each frequency block - for (k, start_idx) in enumerate(all_idx) - - # Slice the file from the current "GROUND WIRES ELIMINATED" position to end - block_lines = fileLines[start_idx:end] - - # Extract matrices for this frequency sample, ensuring output is ComplexF64 - Z_matrices[k] = Complex{Float64}.( - extract_tralin_variable( - block_lines, - ord, - "SERIES IMPEDANCES - (ohms/kilometer)", - "SHUNT ADMITTANCES (microsiemens/kilometer)", - ), - ) - Y_matrices[k] = Complex{Float64}.( - extract_tralin_variable( - block_lines, - ord, - "SHUNT ADMITTANCES (microsiemens/kilometer)", - "SERIES ADMITTANCES (siemens.kilometer)", - ), - ) - P_matrices[k] = Complex{Float64}.( - extract_tralin_variable( - block_lines, - ord, - "POTENTIAL COEFFICIENTS (meghoms.kilometer)", - "SERIES IMPEDANCES - (ohms/kilometer)", - ), - ) - end - - # Convert lists of matrices into 3D arrays for each matrix type - Z_stack = reshape(hcat(Z_matrices...), ord, ord, length(Z_matrices)) - Y_stack = reshape(hcat(Y_matrices...), ord, ord, length(Y_matrices)) - P_stack = reshape(hcat(P_matrices...), ord, ord, length(P_matrices)) - - Z_stack = Z_stack ./ 1000 - Y_stack = Y_stack .* 1e-6 ./ 1000 - P_stack = P_stack .* 1e6 .* 1000 - - return freqs, Z_stack, Y_stack, P_stack + fileLines = readlines(filename) + + ord = _infer_tralin_order(fileLines) + freqs = _extract_tralin_frequencies(fileLines) + + # Get all occurrences of "GROUND WIRES ELIMINATED" + limited_str = "GROUND WIRES ELIMINATED" + all_idx = findall(row -> occursin(limited_str, row), fileLines) + + # Initialize matrices for all frequency samples. + Z_matrices = Vector{Matrix{ComplexF64}}(undef, length(all_idx)) + Y_matrices = Vector{Matrix{ComplexF64}}(undef, length(all_idx)) + P_matrices = Vector{Matrix{ComplexF64}}(undef, length(all_idx)) + + # Loop through each frequency block + for (k, start_idx) in enumerate(all_idx) + + # Slice the file from the current "GROUND WIRES ELIMINATED" position to end + block_lines = fileLines[start_idx:end] + + # Extract matrices for this frequency sample, ensuring output is ComplexF64 + Z_matrices[k] = Complex{Float64}.( + extract_tralin_variable( + block_lines, + ord, + "SERIES IMPEDANCES - (ohms/kilometer)", + "SHUNT ADMITTANCES (microsiemens/kilometer)" + ), + ) + Y_matrices[k] = Complex{Float64}.( + extract_tralin_variable( + block_lines, + ord, + "SHUNT ADMITTANCES (microsiemens/kilometer)", + "SERIES ADMITTANCES (siemens.kilometer)" + ), + ) + P_matrices[k] = Complex{Float64}.( + extract_tralin_variable( + block_lines, + ord, + "POTENTIAL COEFFICIENTS (meghoms.kilometer)", + "SERIES IMPEDANCES - (ohms/kilometer)" + ), + ) + end + + # Convert lists of matrices into 3D arrays for each matrix type + Z_stack = reshape(hcat(Z_matrices...), ord, ord, length(Z_matrices)) + Y_stack = reshape(hcat(Y_matrices...), ord, ord, length(Y_matrices)) + P_stack = reshape(hcat(P_matrices...), ord, ord, length(P_matrices)) + + Z_stack = Z_stack ./ 1000 + Y_stack = Y_stack .* 1e-6 ./ 1000 + P_stack = P_stack .* 1e6 .* 1000 + + return freqs, Z_stack, Y_stack, P_stack end """ - extract_tralin_variable(fileLines, order, str_init, str_final) +$(TYPEDSIGNATURES) + +Parse one upper-triangular complex matrix between two TRALIN section headers. + +# Arguments + +- `fileLines`: TRALIN output lines beginning before `str_init`. +- `order`: matrix order. +- `str_init`: header that begins the matrix section. +- `str_final`: header that ends the matrix section. -Extracts matrix data between specified headers in `fileLines`, handling complex formatting. +# Returns + +- A symmetric `order × order` complex matrix. Missing headers produce a zero + matrix after writing a diagnostic to standard output. """ function extract_tralin_variable(fileLines, order, str_init, str_final) - # Locate header and footer lines - variable_init = findfirst(line -> occursin(str_init, line), fileLines) - variable_final = findfirst(line -> occursin(str_final, line), fileLines) - - if isnothing(variable_init) || isnothing(variable_final) - println("Could not locate start or end of the block.") - return zeros(ComplexF64, order, order) - end - - # Parse the relevant lines into a list of complex numbers - variable_list_number = [] - for line in fileLines[(variable_init+15):(variable_final-1)] - numbers = take_complex_list(line) - if !isempty(numbers) - push!(variable_list_number, numbers) - end - end - - # Process, clean, and arrange data into matrix form - variable_list_number = clean_variable_list(variable_list_number, order) - - # Initialize matrix and fill, with padding if necessary - matrix = zeros(ComplexF64, order, order) - for (i, row) in enumerate(variable_list_number) - matrix[i, 1:length(row)] = row - end - - # Make symmetric by filling lower triangle - matrix += tril(matrix, -1)' - - return matrix + # Locate header and footer lines + variable_init = findfirst(line -> occursin(str_init, line), fileLines) + variable_final = findfirst(line -> occursin(str_final, line), fileLines) + + if isnothing(variable_init) || isnothing(variable_final) + println("Could not locate start or end of the block.") + return zeros(ComplexF64, order, order) + end + + # Parse the relevant lines into a list of complex numbers + variable_list_number = [] + for line in fileLines[(variable_init + 15):(variable_final - 1)] + numbers = take_complex_list(line) + if !isempty(numbers) + push!(variable_list_number, numbers) + end + end + + # Process, clean, and arrange data into matrix form + variable_list_number = clean_variable_list(variable_list_number, order) + + # Initialize and fill the matrix, padding incomplete rows when necessary. + matrix = zeros(ComplexF64, order, order) + for (i, row) in enumerate(variable_list_number) + matrix[i, 1:length(row)] = row + end + + # Make symmetric by filling lower triangle + matrix += tril(matrix, -1)' + + return matrix end - """ - take_complex_list(s) +$(TYPEDSIGNATURES) -Parses a string to identify real and complex numbers, with conditional scaling for scientific notation. +Parse the leading row index and `a + j b` values from one TRALIN matrix row. """ function take_complex_list(s) - numbers = [] - - # Match the first real number (decimal, integer, or scientific notation) - first_real_pattern = r"([-+]?\d*\.?\d+(?:[Ee][-+]?\d+)?|\d+)" - first_real_match = match(first_real_pattern, s) - if !isnothing(first_real_match) - real_part_str = strip(first_real_match.match) - real_value = - occursin(r"[Ee]", real_part_str) ? parse(Float64, real_part_str) : - parse(Float64, real_part_str) * 1 - push!(numbers, real_value) - end - - # Match complex numbers (handles scientific notation or regular float, allowing extra whitespace before 'j') - complex_pattern = - r"([-+]?\d*\.?\d+(?:[Ee][-+]?\d+)?|\d+)\s*\+\s*j\s*([-+]?\d*\.?\d+(?:[Ee][-+]?\d+)?|\d+)" - for m in eachmatch(complex_pattern, s) - real_part_str, imag_part_str = m.captures - real_value = - occursin(r"[Ee]", real_part_str) ? parse(Float64, real_part_str) : - parse(Float64, real_part_str) * 1 - imag_value = - occursin(r"[Ee]", imag_part_str) ? parse(Float64, imag_part_str) : - parse(Float64, imag_part_str) * 1 - push!(numbers, Complex(real_value, imag_value)) - end - - return numbers + numbers = [] + + # Match the first real number (decimal, integer, or scientific notation) + first_real_pattern = r"([-+]?\d*\.?\d+(?:[Ee][-+]?\d+)?|\d+)" + first_real_match = match(first_real_pattern, s) + if !isnothing(first_real_match) + real_part_str = strip(first_real_match.match) + real_value = occursin(r"[Ee]", real_part_str) ? parse(Float64, real_part_str) : + parse(Float64, real_part_str) * 1 + push!(numbers, real_value) + end + + # Match complex numbers (handles scientific notation or regular float, allowing extra whitespace before 'j') + complex_pattern = r"([-+]?\d*\.?\d+(?:[Ee][-+]?\d+)?|\d+)\s*\+\s*j\s*([-+]?\d*\.?\d+(?:[Ee][-+]?\d+)?|\d+)" + for m in eachmatch(complex_pattern, s) + real_part_str, imag_part_str = m.captures + real_value = occursin(r"[Ee]", real_part_str) ? parse(Float64, real_part_str) : + parse(Float64, real_part_str) * 1 + imag_value = occursin(r"[Ee]", imag_part_str) ? parse(Float64, imag_part_str) : + parse(Float64, imag_part_str) * 1 + push!(numbers, Complex(real_value, imag_value)) + end + + return numbers end - """ - clean_variable_list(variable_list_number, order) +$(TYPEDSIGNATURES) -Cleans and arranges extracted list into a proper matrix format. +Remove row labels and pad parsed TRALIN rows to `order × order`. """ function clean_variable_list(data, order) - # Remove entries that lack values, filter short lists - filter!(lst -> length(lst) > 1, data) + # Remove entries that lack values, filter short lists + filter!(lst -> length(lst) > 1, data) - # Trim row label elements and only keep the actual data - data = [lst[2:end] for lst in data] + # Remove row labels. + data = [lst[2:end] for lst in data] - # Apply padding to each row as needed to align with specified order - data_padded = [vcat(lst, fill(0.0 + 0.0im, order - length(lst))) for lst in data] + # Pad each row to the requested order. + data_padded = [vcat(lst, fill(0.0 + 0.0im, order - length(lst))) for lst in data] - # Ensure `data_padded` has `order` rows; add extra rows of zeros if required - if length(data_padded) < order - for _ in 1:(order-length(data_padded)) - push!(data_padded, fill(0.0 + 0.0im, order)) - end - end + # Append rows of zeros to reach the requested order. + if length(data_padded) < order + for _ in 1:(order - length(data_padded)) + push!(data_padded, fill(0.0 + 0.0im, order)) + end + end - return data_padded + return data_padded end -# -- Direct TRALIN constructor +# Direct TRALIN constructor function LineParameters(::Val{:tralin}, file_name::AbstractString) - f, Z_tralin, Y_tralin, _ = parse_tralin_file(file_name) + f, Z_tralin, Y_tralin, _ = parse_tralin_file(file_name) - # Normalize types (ComplexF64 / Float64 by default; tweak if you need Measurements etc.) - Z = ComplexF64.(Z_tralin) - Y = ComplexF64.(Y_tralin) - fv = Float64.(f) + # Convert parsed values to the requested numeric types. + Z = ComplexF64.(Z_tralin) + Y = ComplexF64.(Y_tralin) + fv = Float64.(f) - return LineParameters(SeriesImpedance(Z), ShuntAdmittance(Y), fv) + return LineParameters(SeriesImpedance(Z), ShuntAdmittance(Y), fv) end -# -- Format-auto convenience (add branches as you implement other parsers) +# Select the parser from the requested format. function LineParameters(file_name::AbstractString; format::Symbol = :auto) - fmt = - format === :auto ? (endswith(lowercase(file_name), ".f09") ? :tralin : :unknown) : - format - if fmt === :tralin - return LineParameters(Val(:tralin), file_name) - else - throw( - ArgumentError("Unknown/unsupported format for '$file_name' (format=$format)."), - ) - end + fmt = format === :auto ? (endswith(lowercase(file_name), ".f09") ? :tralin : :unknown) : + format + if fmt === :tralin + return LineParameters(Val(:tralin), file_name) + else + throw( + ArgumentError("Unknown/unsupported format for '$file_name' (format=$format)."), + ) + end end -# helpful fallback for unknown symbols (better than a MethodError) -LineParameters(::Val{fmt}, args...; kwargs...) where {fmt} = - throw(ArgumentError("Unsupported format: $(fmt)")) +# Report unsupported format symbols as argument errors. +function LineParameters(::Val{fmt}, args...; kwargs...) where {fmt} + throw(ArgumentError("Unsupported format: $(fmt)")) +end -@inline LineParameters(fmt::Symbol, args...; kwargs...) = - LineParameters(Val(fmt), args...; kwargs...) +@inline LineParameters(fmt::Symbol, args...; kwargs...) = LineParameters(Val(fmt), args...; kwargs...) diff --git a/src/importexport/uncertainty.jl b/src/importexport/uncertainty.jl new file mode 100644 index 000000000..192f88616 --- /dev/null +++ b/src/importexport/uncertainty.jl @@ -0,0 +1,246 @@ +# Scientific result records are distinct from executable computation checkpoints. +serialize_value(value, ::Val{:scientific}) = serialize_value(value) +# JSON number readers need not preserve UInt64 values above typemax(Int64). +# Scientific seeds must retain all 64 bits, including within formulation records. +serialize_value(value::UInt64, ::Val{:scientific}) = + Dict("__type__"=>"UInt64", "value"=>string(value)) +deserialize_extension(::Val{:UInt64},record) = parse(UInt64,record["value"]) +function serialize_value(value::FormulationOptions) + return Dict("__type__"=>"FormulationOptions", "value"=>serialize_value(value.data, Val(:scientific))) +end +function serialize_value(value::ComputationOptions) + return Dict("__type__"=>"ComputationOptions", "value"=>serialize_value(value.data, Val(:scientific))) +end +function serialize_value(value::ComputationDetails) + return Dict("__type__"=>"ComputationDetails", "value"=>serialize_value(value.data, Val(:scientific))) +end +deserialize_extension(::Val{:FormulationOptions}, record) = + FormulationOptions(deserialize_value(record["value"])) +deserialize_extension(::Val{:ComputationOptions}, record) = + ComputationOptions(deserialize_value(record["value"])) +deserialize_extension(::Val{:ComputationDetails}, record) = + ComputationDetails(deserialize_value(record["value"])) +function serialize_value(::Val{Value}, ::Val{:scientific}) where {Value} + return Dict("__type__"=>"Val", "value"=>serialize_value(Value, Val(:scientific))) +end +function serialize_value(value::NamedTuple, ::Val{:scientific}) + return Dict("__type__"=>"NamedTuple", "names"=>string.(collect(keys(value))), + "values"=>[serialize_value(item, Val(:scientific)) for item in values(value)]) +end +function serialize_value(value::Tuple, ::Val{:scientific}) + return Dict("__type__"=>"Tuple", "values"=>[serialize_value(item, Val(:scientific)) + for item in value]) +end +function serialize_value(value::AbstractArray, ::Val{:scientific}) + return Dict("__type__"=>"Array", "size"=>collect(size(value)), + "values"=>[serialize_value(item, Val(:scientific)) for item in vec(value)]) +end +serialize_value(::Missing) = Dict("__type__"=>"Missing") +function serialize_value(value::AbstractArray{T, N}) where {T, N} + return Dict("__type__"=>"Array", "size"=>collect(size(value)), + "values"=>map(serialize_value, vec(value))) +end + +function serialize_value(selector::Function) + selector in ( + Engine.Z, Engine.Y, Engine.R, Engine.X, Engine.L, Engine.G, Engine.B, Engine.C, + ModalAnalysis.gamma, ModalAnalysis.alpha, ModalAnalysis.beta, ModalAnalysis.velocity, + ModalAnalysis.Zc, ModalAnalysis.Yc, ModalAnalysis.H, + ModalAnalysis.Tv, ModalAnalysis.Ti, + frequencies, UQ.statistics, UQ.samples, UQ.histograms, Statistics.mean, Statistics.std, + Statistics.median, minimum, maximum, abs, angle, real, imag, + LinearAlgebra.diag) || throw(ArgumentError( + "no portable scientific selector codec for $selector")) + return Dict("__type__"=>"Observable", "name"=>string(nameof(selector))) +end +function serialize_value(selector::Base.Fix2{typeof(Statistics.quantile)}) + Dict("__type__"=>"Quantile", "probability"=>serialize_value(selector.x)) +end +function serialize_value(selector::Base.Fix2{F}) where {F<:Union{typeof(ModalAnalysis.Zc), + typeof(ModalAnalysis.Yc),typeof(ModalAnalysis.H)}} + selector in Commons.observables(ModalAnalysis.PropagationParameters) || + throw(ArgumentError("unsupported modal representation selector")) + return Dict("__type__"=>"ModalRepresentation", + "selector"=>string(nameof(selector.f)), + "field"=>selector.f===ModalAnalysis.H ? string(selector.x.field) : nothing) +end + +function deserialize_extension(::Val{:Observable}, record) + selectors=( + Engine.Z, Engine.Y, Engine.R, Engine.X, Engine.L, Engine.G, Engine.B, Engine.C, + ModalAnalysis.gamma, ModalAnalysis.alpha, ModalAnalysis.beta, ModalAnalysis.velocity, + ModalAnalysis.Zc, ModalAnalysis.Yc, ModalAnalysis.H, + ModalAnalysis.Tv, ModalAnalysis.Ti, + frequencies, UQ.statistics, UQ.samples, UQ.histograms, Statistics.mean, Statistics.std, + Statistics.median, minimum, maximum, abs, angle, real, imag, + LinearAlgebra.diag) + selected=filter(selector -> string(nameof(selector)) == record["name"], selectors) + length(selected)==1 || throw(ArgumentError("unknown saved scientific selector")) + return only(selected) +end +function deserialize_extension(::Val{:Quantile}, record) + Base.Fix2(Statistics.quantile, deserialize_value(record["probability"])) +end +function deserialize_extension(::Val{:ModalRepresentation},record) + name=record["selector"] + field=get(record,"field",nothing) + if name=="H" && field in ("voltage","current") + return Base.Fix2(ModalAnalysis.H,(domain=Engine.PhaseDomain,field=Symbol(field))) + elseif name in ("Zc","Yc") && field===nothing + selector=name=="Zc" ? ModalAnalysis.Zc : ModalAnalysis.Yc + return Base.Fix2(selector,(domain=Engine.PhaseDomain,)) + end + throw(ArgumentError("unknown saved modal representation")) +end + +function serialize_value(value::UQ.SampleSummary) + return Dict("__type__"=>"SampleSummary", "values"=>serialize_value(Tuple(NamedTuple(value)))) +end +function deserialize_extension(::Val{:SampleSummary}, record) + UQ.SampleSummary(deserialize_value(record["values"])...) +end + +function serialize_value(value::UQ.HistogramDensity) + record=NamedTuple(value) + return Dict("__type__"=>"HistogramDensity", "edges"=>serialize_value(record.edges), + "density"=>serialize_value(record.density)) +end +function deserialize_extension(::Val{:HistogramDensity}, record) + UQ.HistogramDensity(deserialize_value(record["edges"]), deserialize_value(record["density"])) +end + +function serialize_value(value::LineParameters) + Engine.domain(value) === Engine.ModalDomain && throw(ArgumentError( + "modal LineParameters require Julia Serialization for numerical state")) + return Dict("__type__"=>"LineParameters", "Z"=>serialize_value(observe(value, Z)), + "Y"=>serialize_value(observe(value, Y)), "frequencies"=>serialize_value(frequencies(value)), + "basis"=>string(LineCableModels.basis(value)), "domain"=>string(nameof(Engine.domain(value))), + "details"=>serialize_value(LineCableModels.details(value).data,Val(:scientific))) +end +function deserialize_extension(::Val{:LineParameters}, record) + record["domain"] in ("PhaseDomain","ModalDomain") || + throw(ArgumentError("unsupported saved result domain")) + retained=deserialize_value(record["details"]) + return LineParameters( + getfield(Engine,Symbol(record["domain"])), deserialize_value(record["Z"]), deserialize_value(record["Y"]), + deserialize_value(record["frequencies"]); basis = Symbol(record["basis"]), details = Engine.completion_details(retained)) +end + +function serialize_value(value::Engine.CableConstants) + return Dict("__type__"=>"CableConstants", "cores"=>serialize_value(value.cores), + "R"=>serialize_value(value.R),"L"=>serialize_value(value.L), + "C"=>serialize_value(value.C),"G"=>serialize_value(value.G), + "frequency"=>serialize_value(value.frequency), + "details"=>serialize_value(value.details.data,Val(:scientific))) +end +function deserialize_extension(::Val{:CableConstants},record) + retained=deserialize_value(record["details"]) + return Engine.CableConstants(Symbol.(deserialize_value(record["cores"])), + (deserialize_value(record[key]) for key in ("R","L","C","G","frequency"))..., + Engine.completion_details(retained)) +end + +""" +$(TYPEDSIGNATURES) + +Encode retained MC products or first-order results without solving a model. +The versioned record retains scientific data, not executable formulations. +Measurement-bearing MC and LEP results use the extension's shared-source codec. +""" +function serialize_value(value::Union{UQ.MonteCarloResult, UQ.LinearErrorResult}) + return serialize_value(value, map(serialize_value, value.values), nothing) +end + +""" +$(TYPEDSIGNATURES) + +Encode a UQ result envelope with already encoded core `points`. Optional +`sources` are shared Measurement source records supplied by the Measurements +extension. Their point records retain signed sensitivities. This codec keeps +empirical products, source identities and details under the scientific result codec. +""" +function serialize_value(value::Union{UQ.MonteCarloResult,UQ.LinearErrorResult}, + points::AbstractVector, sources) + record=NamedTuple(value) + formulation=record.formulation isa NamedTuple ? record.formulation : + NamedTuple(record.formulation) + retained = record.details.data + portable_details = if isempty(retained) + retained + elseif value isa UQ.LinearErrorResult + (points=map(point -> point.data, retained.points),) + elseif haskey(retained, :trials) + merge(retained, (trials=map(trials -> map(trial -> trial.data, trials), retained.trials),)) + else + retained + end + return Dict( + "__type__"=>value isa UQ.MonteCarloResult ? "MonteCarloResult" : + "LinearErrorResult", + "version"=>2, "formulation"=>serialize_value(formulation, Val(:scientific)), + "points"=>points, "sources"=>serialize_value(sources,Val(:scientific)), "details"=>serialize_value( + portable_details, Val(:scientific)), + "statistics"=>serialize_value(get(record, :statistics, nothing), Val(:scientific)), + "samples"=>serialize_value(get(record, :samples, nothing), Val(:scientific)), + "histograms"=>serialize_value(get(record, :histograms, nothing), Val(:scientific)), + "root_seed"=>serialize_value(get(record, :root_seed, nothing),Val(:scientific)), + "point_seeds"=>serialize_value(get(record, :point_seeds, nothing),Val(:scientific)), + "trial_counts"=>get(record, :trial_counts, nothing)) +end + +function deserialize_extension(kind::Union{Val{:MonteCarloResult}, Val{:LinearErrorResult}}, record) + record["version"] in (1,2) || + throw(ArgumentError("unsupported scientific UQ record version")) + decoded=deserialize_value(record["formulation"]) + formulation=(; (Symbol(k)=>v for (k, v) in pairs(decoded))...) + options=(; (Symbol(k)=>v for (k, v) in pairs(formulation.options))...) + formulation=merge(formulation, (; options)) + points=if get(record,"sources",nothing) === nothing + map(deserialize_value, record["points"]) + else + Base.get_extension(LineCableModels,:LineCableModelsMeasurementsExt) === nothing && + throw(ArgumentError("restoring uncertainty-bearing results requires `using Measurements`")) + deserialize_extension(Val(:MeasurementPoints),record) + end + details=deserialize_value(record["details"]) + retained=(; (Symbol(k)=>v for (k, v) in pairs(details))...) + if !isempty(retained) + if kind isa Val{:LinearErrorResult} + records=map(retained.points,points) do detail,point + point isa Union{Engine.CableConstants,LineParameters} ? + Engine.completion_details(detail) : ComputationDetails(detail) + end + retained=(points=records,) + elseif haskey(retained, :trials) + records=map(retained.trials,points) do trials,point + map(trials) do detail + point isa Union{Engine.CableConstants,LineParameters} ? + Engine.completion_details(detail) : ComputationDetails(detail) + end + end + retained=merge(retained,(trials=records,)) + end + end + details=ComputationDetails(retained) + kind isa Val{:LinearErrorResult} && + return UQ.LinearErrorResult(formulation, points, details) + products=map(("statistics", "samples", "histograms")) do key + values=deserialize_value(record[key]) + values === nothing && return nothing + [(; (Symbol(k)=>v for (k, v) in pairs(point))...) for point in values] + end + root_seed=deserialize_value(record["root_seed"]) + point_seeds=deserialize_value(record["point_seeds"]) + root_seed isa Integer && !(root_seed isa Bool) && + all(seed -> seed isa Integer && !(seed isa Bool),point_seeds) || + throw(ArgumentError("scientific Monte Carlo seeds require exact integers; floating-point JSON seeds cannot preserve seeded replay")) + if record["version"] == 1 + # Supported portable MC v1 records retained full empirical summaries + # but only mean-valued cores. Restore their documented marginal result + # through the UQ-owned materialization, never through native checkpoints. + points=map(LineCableModels.materialize,points,first(products)) + end + return UQ.MonteCarloResult( + formulation, points, products..., UInt64(root_seed), + UInt64.(point_seeds), Int.(record["trial_counts"]), details) +end diff --git a/src/importexport/xlsx.jl b/src/importexport/xlsx.jl deleted file mode 100644 index 6e50767fa..000000000 --- a/src/importexport/xlsx.jl +++ /dev/null @@ -1,161 +0,0 @@ -# import DataFrames: DataFrame -# using DataFrames - -# --------------------------------------------------------------------------- -# Stringification helpers (keeps uncertainties visible in Excel) -# --------------------------------------------------------------------------- -stringify(x) = string(x) # fallback (rarely reached) -stringify(::Missing) = "" -stringify(x::Real) = @sprintf("%.12g", float(x)) -stringify(x::Measurements.Measurement) = - @sprintf("%.12g ± %.6g", Measurements.value(x), Measurements.uncertainty(x)) - -function df_to_strings(df::DataFrame) - DataFrame((name => stringify.(df[!, name]) for name in names(df))...; copycols = false) -end - -# helper to fetch the units dict from df.metadata -_get_units(df::DataFrame) = - try - DataFrames.metadata(df, "units", style = :note) - catch - try - DataFrames.metadata(df, "units") - catch - nothing - end - end - -# --------------------------------------------------------------------------- -# XLSX sheet writer: reuses/renames Sheet1 for the first write to avoid blanks -# --------------------------------------------------------------------------- -function _write_sheet!(xf, sheetname::String, df::DataFrame; use_first_sheet::Bool) - units = _get_units(df) - df_str = df_to_strings(df) - - ws = nothing - if use_first_sheet - ws = try - xf["Sheet1"] # reuse default first sheet - catch - nothing - end - ws = ws === nothing ? XLSX.addsheet!(xf, sheetname) : ws - # If rename! exists, great; if not, we still write so Sheet1 isn't blank. - try - XLSX.rename!(ws, sheetname) - catch - end - else - ws = XLSX.addsheet!(xf, sheetname) - end - - # Start row for writing - start_row = 1 - - # Optional UNITS block (Column | Unit) from DataFrame metadata - if units isa AbstractDict - for name in names(df) - ws[start_row, 1] = String(name) - u = get(units, name, get(units, Symbol(name), "")) - ws[start_row, 2] = String(u) - start_row += 1 - end - start_row += 1 # spacer line - end - - # IMPORTANT: anchor_cell must be a CellRef, not a String - XLSX.writetable!( - ws, - Tables.columntable(df_str); - anchor_cell = XLSX.CellRef(start_row, 1), - ) - return nothing -end - - -# --------------------------------------------------------------------------- -# Main export -# --------------------------------------------------------------------------- -function export_data( - ::Val{:xlsx}, - line_params::LineParameters; - file_name::Union{String, Nothing} = nothing, - cable_system::Union{LineCableSystem, Nothing} = nothing, -)::Union{String, Nothing} - - # ---- Resolve final file_name (exactly as requested) -------------------- - if isnothing(file_name) - if isnothing(cable_system) - file_name = joinpath(@__DIR__, "ZY_export.xlsx") - else - file_name = joinpath(@__DIR__, "$(cable_system.system_id)_ZY_export.xlsx") - end - else - requested = isabspath(file_name) ? file_name : joinpath(@__DIR__, file_name) - if isnothing(cable_system) - file_name = requested - else - dir = dirname(requested) - base = basename(requested) - file_name = joinpath(dir, "$(cable_system.system_id)_$base") - end - end - - # ---- Build the DataFrames once (uses LP.f internally) ------------------ - df_z, df_y = DataFrame(line_params) # each is Matrix{DataFrame} - - # Shapes - nzx, nzy = size(df_z) - nyx, nyy = size(df_y) - - # Diagonal-only logic (modal parameters) - Z_isdiag = isdiag_approx(line_params.Z[:, :, 1]) - Y_isdiag = isdiag_approx(line_params.Y[:, :, 1]) - - if Z_isdiag - @warn "Z appears modal/diagonal (isdiag_approx=true). Exporting ONLY diagonal elements Z[i,i]; off-diagonals are intentionally omitted." - end - if Y_isdiag - @warn "Y appears modal/diagonal (isdiag_approx=true). Exporting ONLY diagonal elements Y[i,i]; off-diagonals are intentionally omitted." - end - - # ---- Write XLSX -------------------------------------------------------- - try - first_sheet = true - XLSX.openxlsx(file_name, mode = "w") do xf - # Z sheets - if Z_isdiag - for i in 1:min(nzx, nzy) - _write_sheet!(xf, "Z($i,$i)", df_z[i, i]; use_first_sheet = first_sheet) - first_sheet = false - end - else - for i in 1:nzx, j in 1:nzy - _write_sheet!(xf, "Z($i,$j)", df_z[i, j]; use_first_sheet = first_sheet) - first_sheet = false - end - end - - # Y sheets - if Y_isdiag - for i in 1:min(nyx, nyy) - _write_sheet!(xf, "Y($i,$i)", df_y[i, i]; use_first_sheet = first_sheet) - first_sheet = false - end - else - for i in 1:nyx, j in 1:nyy - _write_sheet!(xf, "Y($i,$j)", df_y[i, j]; use_first_sheet = first_sheet) - first_sheet = false - end - end - end - - return file_name - catch err - # If anything explodes (e.g., filesystem perms), return nothing. - # Let the caller decide whether to rethrow. - @error "Failed to export XLSX: $(err)" - return nothing - end -end diff --git a/src/interfaces.jl b/src/interfaces.jl new file mode 100644 index 000000000..f26dee5d1 --- /dev/null +++ b/src/interfaces.jl @@ -0,0 +1,105 @@ +""" +Add one owned value to a mutable collection. +""" +function add! end + +""" + build(Target, declarations...; kwargs...) + +Construct one completed domain object from complete physical declarations. + +`build(CableDesign, ...)` resolves a physical cable root and terminal state. +`build(LineCableSystem, ...)` places completed designs and resolves global +connections. When an argument is a `Grid` or `Gridspace`, or an admitted tuple +or vector contains one, the same call returns a `Gridspace{Target}` whose +callable invokes scalar `build` after selecting and reconstructing one complete +point. + +# Arguments + +- `Target`: completed domain type to construct. +- `declarations`: complete physical declarations owned by `Target`. + +# Returns + +One completed `Target`, or `Gridspace{Target}` for explicit finite inputs. +""" +function build end + +""" + Gridpoint{Target}(build, args) + +Store one selected but unresolved argument tuple from a finite parameter space. +`Target` preserves the semantic object or problem family that the point will +materialize, allowing computation dispatch to consume a scalar point without +first discarding its target identity. +""" +struct Gridpoint{Target, F, A <: Tuple} + "Scalar constructor or lowering function selected by the finite space." + build::F + "Selected argument tuple, with any nested target-bearing points retained." + args::A +end + +function Gridpoint{Target}(build, args::A) where {Target, A <: Tuple} + return Gridpoint{Target, typeof(build), A}(build, args) +end + +""" +homogenize(design. New_id="") + +Build a homogeneous cable design that preserves each radial assembly member +and matches the effective conductor and dielectric properties of `design`. + +The physical source design remains unchanged. The reduction uses only scalar +series and parallel circuit calculations. It does not calculate line-parameter +matrices, mutual coupling, or earth return. + +# Arguments + +- `design`: completed physical cable design. + +# Keywords + +- `new_id`: identifier for the returned design. An empty value appends + `"_equivalent"` to the source identifier. + +# Returns + +- A completed homogeneous `CableDesign`. +""" +function homogenize end + +""" +Evaluate the constitutive relation selected for a material value. +""" +function constitutive end + +""" +Return the physical storage basis of a result. +""" +function basis end +function line_length end + +function R end +function L end +function C end +function resistance end +function inductance end +function capacitance end + +""" +Return owned scientific text for a formulation. `compact=true` selects its +short display name. `formula_id` remains its scientific identity. +""" +function description end + +""" +Declare a formula selection for resolution by its receiving formulation owner. +""" +function formula end + +""" +Return the stable scientific identifier of a formula selection. +""" +function formula_id end diff --git a/src/logging.jl b/src/logging.jl new file mode 100644 index 000000000..6ecbe2e1d --- /dev/null +++ b/src/logging.jl @@ -0,0 +1,66 @@ +""" +$(TYPEDSIGNATURES) + +Validate verbosity levels, or select a level from execution options. Levels +0, 1, and 2 permit warnings, information, and debug messages respectively. +The `progress` group uses its explicit level or `default`. Other messages use +the nearest explicitly configured module ancestor before falling back to `default`. +""" +function verbosity(levels::NamedTuple) + haskey(levels, :default) || + throw(ArgumentError("verbosity must define a default level")) + all(value -> value isa Integer && value in 0:2, values(levels)) || + throw(ArgumentError("verbosity levels must be integers from 0 to 2")) + return NamedTuple{keys(levels)}(Int.(values(levels))) +end +verbosity(levels) = throw(ArgumentError("verbosity must be a named tuple")) +function verbosity(record::ComputationOptions, key::Symbol) + levels = get(record.data, :verbosity, (default = 0,)) + return get(levels, key, levels.default) +end +function verbosity(levels::NamedTuple, source::Union{Module, Nothing}, group) + group === :progress && return get(levels, :progress, levels.default) + source === nothing && return levels.default + while true + haskey(levels, nameof(source)) && return levels[nameof(source)] + ancestor = parentmodule(source) + ancestor === source && return levels.default + source = ancestor + end +end + +""" +$(TYPEDEF) + +Filter ordinary Julia log records by execution verbosity and forward accepted +records to the caller's logger. The parent logger retains its filtering and +exception handling. Progress counters and output resources remain outside this filter. + +$(TYPEDFIELDS) +""" +struct VerbosityLogger{L <: Logging.AbstractLogger, V <: NamedTuple} <: + Logging.AbstractLogger + "Caller-owned destination." + parent::L + "Validated verbosity levels." + levels::V +end + +# Julia's logger protocol methods are not public stdlib bindings. +function Logging.min_enabled_level(logger::VerbosityLogger) + Logging.min_enabled_level(logger.parent) +end +Logging.catch_exceptions(logger::VerbosityLogger) = Logging.catch_exceptions(logger.parent) +function Logging.shouldlog(logger::VerbosityLogger, level, source, group, id) + selected = verbosity(logger.levels, source, group) + threshold = selected == 0 ? Logging.Warn : selected == 1 ? Logging.Info : Logging.Debug + return level >= threshold && level >= Logging.min_enabled_level(logger.parent) && + Logging.shouldlog(logger.parent, level, source, group, id) +end +function Logging.handle_message(logger::VerbosityLogger, level, message, source, group, + id, file, line; kwargs...) + return Logging.handle_message(logger.parent, level, message, source, group, id, + file, line; kwargs...) +end + +public verbosity, VerbosityLogger diff --git a/src/materials/Materials.jl b/src/materials/Materials.jl index 3bf599633..209952bbc 100644 --- a/src/materials/Materials.jl +++ b/src/materials/Materials.jl @@ -1,97 +1,43 @@ """ - LineCableModels.Materials + LineCableModels.Materials -The [`Materials`](@ref) module provides functionality for managing and utilizing material properties within the [`LineCableModels.jl`](index.md) package. This module includes definitions for material properties, a library for storing and retrieving materials, and functions for manipulating material data. +Define electromagnetic material records and an in-memory material library. -# Overview +# Public actions -- Defines the [`Material`](@ref) struct representing fundamental physical properties of materials. -- Provides the [`MaterialsLibrary`](@ref) mutable struct for storing a collection of materials. -- Includes functions for adding, removing, and retrieving materials from the library. -- Supports loading and saving material data from/to JSON files. -- Contains utility functions for displaying material data. +- Construct and validate [`Material`](@ref) values. +- Add, remove, and retrieve materials in a [`MaterialsLibrary`](@ref). +- Present material data through the Base display protocol. + +JSON persistence belongs to `LineCableModels.ImportExport`. # Dependencies $(IMPORTS) - -# Exports - -$(EXPORTS) """ module Materials -# Export public API -export Material, MaterialsLibrary - -# Module-specific dependencies -using ..Commons -using ..Utils: resolve_T -using Measurements -import ..Commons: add! -import ..Utils: coerce_to_T - -""" -$(TYPEDEF) - -Defines electromagnetic and thermal properties of a material used in cable modeling: - -$(TYPEDFIELDS) -""" -struct Material{T <: REALSCALAR} - "Electrical resistivity of the material \\[Ω·m\\]." - rho::T - "Relative permittivity \\[dimensionless\\]." - eps_r::T - "Relative permeability \\[dimensionless\\]." - mu_r::T - "Reference temperature for property evaluations \\[°C\\]." - T0::T - "Temperature coefficient of resistivity \\[1/°C\\]." - alpha::T - - @inline function Material{T}( - rho::T, - eps_r::T, - mu_r::T, - T0::T, - alpha::T, - ) where {T <: REALSCALAR} - return new{T}(rho, eps_r, mu_r, T0, alpha) - end - -end - -""" -$(TYPEDSIGNATURES) - -Weakly-typed constructor that infers the target scalar type `T` from the arguments, -coerces values to `T`, and calls the strict numeric kernel. - -# Arguments -- `rho`: Resistivity \\[Ω·m\\]. -- `eps_r`: Relative permittivity \\[1\\]. -- `mu_r`: Relative permeability \\[1\\]. -- `T0`: Reference temperature \\[°C\\]. -- `alpha`: Temperature coefficient of resistivity \\[1/°C\\]. - -# Returns -- `Material{T}` where `T = resolve_T(rho, eps_r, mu_r, T0, alpha)`. -""" -@inline function Material(rho, eps_r, mu_r, T0, alpha) - T = resolve_T(rho, eps_r, mu_r, T0, alpha) - return Material{T}( - coerce_to_T(rho, T), - coerce_to_T(eps_r, T), - coerce_to_T(mu_r, T), - coerce_to_T(T0, T), - coerce_to_T(alpha, T), - ) -end - +export AbstractMaterial, Material, RadialDielectric, MaterialsLibrary, add! + +#! explicit-imports: off +# IMPORTS is expanded in the module docstring rather than called as Julia code. +using DocStringExtensions: IMPORTS +#! explicit-imports: on +using DocStringExtensions: TYPEDEF, TYPEDFIELDS, TYPEDSIGNATURES, FUNCTIONNAME +using RequiredInterfaces: @required +import ..LineCableModels: add!, validate +import ..Commons +import ..TextDisplay + +include("material.jl") +include("radialdielectric.jl") +include("temperaturedependent/TemperatureDependent.jl") +public TemperatureDependent include("materialslibrary.jl") -include("dataframe.jl") include("base.jl") -include("typecoercion.jl") + +@required AbstractMaterial begin + validate(::AbstractMaterial) +end end # module Materials diff --git a/src/materials/base.jl b/src/materials/base.jl index fdc7d1b9e..fb005c87f 100644 --- a/src/materials/base.jl +++ b/src/materials/base.jl @@ -3,196 +3,163 @@ Base.eltype(::Type{Material{T}}) where {T} = T # Implement the AbstractDict interface Base.length(lib::MaterialsLibrary) = length(lib.data) -Base.setindex!(lib::MaterialsLibrary, value::Material, key::String) = - (lib.data[key] = value) +function Base.setindex!(lib::MaterialsLibrary, value::Material, key) + setindex!(lib.data, validate(value), key) + return lib +end Base.iterate(lib::MaterialsLibrary, state...) = iterate(lib.data, state...) Base.keys(lib::MaterialsLibrary) = keys(lib.data) Base.values(lib::MaterialsLibrary) = values(lib.data) -Base.haskey(lib::MaterialsLibrary, key::String) = haskey(lib.data, key) -Base.getindex(lib::MaterialsLibrary, key::String) = getindex(lib.data, key) - +Base.haskey(lib::MaterialsLibrary, key) = haskey(lib.data, key) +Base.getindex(lib::MaterialsLibrary, key) = getindex(lib.data, key) """ $(TYPEDSIGNATURES) -Removes a material from a [`MaterialsLibrary`](@ref). +Remove the material stored under `name`. # Arguments -- `library`: Instance of [`MaterialsLibrary`](@ref) from which the material will be removed. -- `name`: Name of the material to be removed. +- `library`: material library. +- `name`: stored material name. # Returns -- The modified instance of [`MaterialsLibrary`](@ref) without the specified material. - -# Errors - -Throws an error if the material does not exist in the library. - -# Examples - -```julia -library = MaterialsLibrary() -$(FUNCTIONNAME)(library, "copper") -``` +- The modified `library`. -# See also - -- [`add!`](@ref) """ -function Base.delete!(library::MaterialsLibrary, name::String) - if !haskey(library, name) - @error "Material '$name' not found in the library; cannot delete." - throw(KeyError(name)) - - end - delete!(library.data, name) - @info "Material '$name' removed from the library." +function Base.delete!(library::MaterialsLibrary, name) + delete!(library.data, name) + return library end - - """ $(TYPEDSIGNATURES) -Retrieves a material from a [`MaterialsLibrary`](@ref) by name. +Return the material stored under `name`, or `default` when absent. # Arguments -- `library`: Instance of [`MaterialsLibrary`](@ref) containing the materials. -- `name`: Name of the material to retrieve. +- `library`: material library. +- `name`: stored material name. +- `default`: value returned when `name` is absent. # Returns -- The requested [`Material`](@ref) if found, otherwise `nothing`. +- The stored [`Material`](@ref), or `default`. -# Examples +""" +function Base.get(library::MaterialsLibrary, name, default) + return get(library.data, name, default) +end -```julia -library = MaterialsLibrary() -material = $(FUNCTIONNAME)(library, "copper") -``` +function Base.get( + default::Union{Function, Type}, library::MaterialsLibrary, name +) + return get(default, library.data, name) +end -# See also +""" +Return an empty material library without repopulating built-in records. +""" +Base.empty(::MaterialsLibrary) = MaterialsLibrary(; add_defaults = false) -- [`add!`](@ref) -- [`delete!`](@ref) """ -function Base.get(library::MaterialsLibrary, name::String, default = nothing) - material = get(library.data, name, default) - if material === nothing - @warn "Material '$name' not found in the library; returning default." - end - return material +Remove every stored material and return `library`. +""" +function Base.empty!(library::MaterialsLibrary) + empty!(library.data) + return library end """ -$(TYPEDSIGNATURES) - -Defines the display representation of a [`Material`](@ref) object for REPL or text output. - -# Arguments - -- `io`: Output stream. -- `::MIME"text/plain"`: MIME type for plain text output. -- `material`: The [`Material`](@ref) instance to be displayed. - -# Returns - -- Nothing. Modifies `io` by writing text representation of the material. +Return a shallow material-library copy with independent dictionary storage. """ -function Base.show(io::IO, ::MIME"text/plain", material::Material) - print(io, "Material with properties: [") +Base.copy(library::MaterialsLibrary) = MaterialsLibrary(copy(library.data)) - # Define fields to display - fields = [:rho, :eps_r, :mu_r, :T0, :alpha] +TextDisplay.name(::Type{<:Material}) = "Material" - # Print each field with proper formatting - for (i, field) in enumerate(fields) - value = getproperty(material, field) - # Add comma only between items, not after the last one - delimiter = i < length(fields) ? ", " : "" - print(io, "$field=$(round(value, sigdigits=4))$delimiter") - end +function Base.summary(io::IO, material::Material) + print(io, "Material · ", material.kind) +end - print(io, "]") +function _material_fields(material::Material) + return ( + ρ = TextDisplay.engineering(material.rho, :ohm_meter), + εᵣ = TextDisplay.value(material.eps_r), + μᵣ = TextDisplay.value(material.mu_r), + T₀ = TextDisplay.engineering(material.T0, :celsius), + α = iszero(material.alpha) ? nothing : + TextDisplay.engineering(material.alpha, :kelvin_inverse), + ρₜₕ = iszero(material.rho_thermal) ? nothing : + string(TextDisplay.value(material.rho_thermal), " K·m/W"), + θₘₐₓ = material.theta_max == oftype(material.theta_max, 90) ? nothing : + TextDisplay.engineering(material.theta_max, :celsius), + tanδ = iszero(material.tan_delta) ? nothing : + TextDisplay.value(material.tan_delta), + σₛ = iszero(material.sigma_solar) ? nothing : + TextDisplay.value(material.sigma_solar) + ) end -""" -$(TYPEDSIGNATURES) +function Base.show(io::IO, material::Material) + fields = _material_fields(material) + print(io, "Material(:", material.kind, "; ρ=", fields.ρ, + ", εᵣ=", fields.εᵣ, ", μᵣ=", fields.μᵣ) + for key in (:ρₜₕ, :θₘₐₓ, :tanδ, :σₛ) + displayed = getproperty(fields, key) + displayed === nothing && continue + print(io, ", ", key, "=", displayed) + end + print(io, ")") +end -Defines the display representation of a [`MaterialsLibrary`](@ref) object for REPL or text output. +function Base.show(io::IO, ::MIME"text/plain", material::Material) + get(io, :compact, false) && return show(io, material) + return TextDisplay.fields( + io, + "Material · $(material.kind)", + _material_fields(material); + multiline = true + ) +end -# Arguments +TextDisplay.name(::Type{<:MaterialsLibrary}) = "MaterialsLibrary" -- `io`: Output stream. -- `::MIME"text/plain"`: MIME type for plain text output. -- `library`: The [`MaterialsLibrary`](@ref) instance to be displayed. +function Base.summary(io::IO, library::MaterialsLibrary) + count = length(library) + print(io, "MaterialsLibrary with ", count, count == 1 ? " material" : " materials") +end -# Returns +function Base.show(io::IO, library::MaterialsLibrary) + count = length(library) + print(io, "MaterialsLibrary(", count, count == 1 ? " material)" : " materials)") +end -- Nothing. Modifies `io` by writing text representation of the library. -""" function Base.show(io::IO, ::MIME"text/plain", library::MaterialsLibrary) - num_materials = length(library) - material_word = num_materials == 1 ? "material" : "materials" - print(io, "MaterialsLibrary with $num_materials $material_word") - - if num_materials > 0 - print(io, ":") - # Optional: list the first few materials - shown_materials = min(5, num_materials) - material_names = collect(keys(library))[1:shown_materials] - - for (i, name) in enumerate(material_names) - print(io, "\n$(i == shown_materials ? "└─" : "├─") $name") - end - - # If there are more materials than we're showing - if num_materials > shown_materials - print(io, "\n└─ ... and $(num_materials - shown_materials) more") - end - end + get(io, :compact, false) && return show(io, library) + count = length(library) + header = "MaterialsLibrary · $count $(count == 1 ? "material" : "materials")" + children = [ + string(name, " · ", sprint(summary, library[name])) + for name in sort!(collect(keys(library))) + ] + return TextDisplay.tree(io, header, children; noun = "materials") end -""" -$(TYPEDSIGNATURES) - -Defines the display representation of a [`MaterialsLibrary`](@ref) object for REPL or text output. - -# Arguments - -- `io`: Output stream. -- `::MIME"text/plain"`: MIME type for plain text output. -- `dict`: The [`MaterialsLibrary`](@ref) contents to be displayed. - -# Returns - -- Nothing. Modifies `io` by writing text representation of the library. -""" -function Base.show(io::IO, ::MIME"text/plain", dict::Dict{String, Material}) - num_materials = length(dict) - material_word = num_materials == 1 ? "material" : "materials" - print(io, "Dict{String, Material} with $num_materials $material_word") - - if num_materials > 0 - print(io, ":") - # List the first few materials - shown_materials = min(5, num_materials) - material_names = collect(keys(dict))[1:shown_materials] - - for (i, name) in enumerate(material_names) - print(io, "\n$(i == shown_materials ? "└─" : "├─") $name") - end - - # If there are more materials than we're showing - if num_materials > shown_materials - print(io, "\n└─ ... and $(num_materials - shown_materials) more") - end - end +function Base.convert(::Type{Material{T}}, m::Material) where {T <: Real} + Material{T}( + m.kind, + convert(T, m.rho), + convert(T, m.eps_r), + convert(T, m.mu_r), + convert(T, m.T0), + convert(T, m.alpha), + convert(T, m.rho_thermal), + convert(T, m.theta_max), + convert(T, m.tan_delta), + convert(T, m.sigma_solar) + ) end -Base.convert(::Type{Material{T}}, m::Material) where {T <: REALSCALAR} = - Material{T}(convert(T, m.rho), convert(T, m.eps_r), convert(T, m.mu_r), - convert(T, m.T0), convert(T, m.alpha)) +Base.convert(::Type{Material{T}}, material::Material{T}) where {T <: Real} = material diff --git a/src/materials/dataframe.jl b/src/materials/dataframe.jl deleted file mode 100644 index b3fbf645a..000000000 --- a/src/materials/dataframe.jl +++ /dev/null @@ -1,41 +0,0 @@ -import DataFrames: DataFrame - -""" -$(TYPEDSIGNATURES) - -Lists the contents of a [`MaterialsLibrary`](@ref) as a `DataFrame`. - -# Arguments - -- `library`: Instance of [`MaterialsLibrary`](@ref) to be displayed. - -# Returns - -- A `DataFrame` containing the material properties. - -# Examples - -```julia -library = MaterialsLibrary() -df = $(FUNCTIONNAME)(library) -``` - -# See also - -- [`LineCableModels.ImportExport.save`](@ref) -""" -function DataFrame(library::MaterialsLibrary)::DataFrame - rows = [ - ( - name=name, - rho=m.rho, - eps_r=m.eps_r, - mu_r=m.mu_r, - T0=m.T0, - alpha=m.alpha, - ) - for (name, m) in library - ] - data = DataFrame(rows) - return data -end \ No newline at end of file diff --git a/src/materials/material.jl b/src/materials/material.jl new file mode 100644 index 000000000..61311be9f --- /dev/null +++ b/src/materials/material.jl @@ -0,0 +1,172 @@ +""" +Supertype for package-owned electromagnetic material values. +""" +abstract type AbstractMaterial end + +""" +$(TYPEDEF) + +Store one broad material class and its electromagnetic and thermal properties. + +$(TYPEDFIELDS) +""" +struct Material{T <: Real} <: AbstractMaterial + "Broad physical class used by formulation dispatch." + kind::Symbol + "Electrical resistivity at reference temperature T0 \\[Ω·m\\]." + rho::T + "Relative permittivity \\[dimensionless\\]." + eps_r::T + "Relative permeability \\[dimensionless\\]." + mu_r::T + "Reference temperature for property evaluations \\[°C\\]." + T0::T + "Temperature coefficient of resistivity \\[1/°C\\]." + alpha::T + "Thermal resistivity \\[K·m/W\\]." + rho_thermal::T + "Maximum continuous operating temperature \\[°C\\]." + theta_max::T + "Polarization loss tangent, excluding conduction represented by rho \\[dimensionless\\]." + tan_delta::T + "Solar-absorption coefficient \\[dimensionless\\]." + sigma_solar::T + + @inline function Material{T}( + kind::Symbol, + rho::T, + eps_r::T, + mu_r::T, + T0::T, + alpha::T, + rho_thermal::T, + theta_max::T, + tan_delta::T, + sigma_solar::T + ) where {T <: Real} + return validate(new{T}( + kind, rho, eps_r, mu_r, T0, alpha, rho_thermal, + theta_max, tan_delta, sigma_solar + )) + end +end + +""" +$(TYPEDSIGNATURES) + +Construct a material after floating and promoting its real-valued properties. + +# Arguments +- `kind`: broad physical class, such as `:conductor`, `:insulator`, or + `:semicon`. +- `rho`: resistivity \\[Ω·m\\]. +- `eps_r`: relative permittivity \\[dimensionless\\]. +- `mu_r`: relative permeability \\[dimensionless\\]. +- `T0`: reference temperature \\[°C\\]. +- `alpha`: temperature coefficient of resistivity \\[1/°C\\]. + +# Keywords + +- `rho_thermal=0`: thermal resistivity \\[K·m/W\\]. +- `theta_max=90`: maximum continuous operating temperature \\[°C\\]. +- `tan_delta=0`: polarization loss tangent \\[dimensionless\\], excluding the + conduction contribution already represented by `rho`. A measured total loss + tangent must be separated into those contributions before supplying both. + Lossless constitutive selections explicitly suppress this input and conductivity. +- `sigma_solar=0`: solar-absorption coefficient. + +# Returns +- A validated `Material` whose scalar type is the promoted floating type of + its inputs. + +# Errors + +- Throws `DomainError` when a property is outside its accepted physical + domain or is not finite, except that infinite resistivity is accepted. +""" +@inline function Material( + kind::Symbol, + rho, + eps_r, + mu_r, + T0, + alpha, + rho_thermal, + theta_max, + tan_delta, + sigma_solar +) + values = map(float, promote( + rho, eps_r, mu_r, T0, alpha, rho_thermal, theta_max, + tan_delta, sigma_solar + )) + return Material{typeof(first(values))}(kind, values...) +end + +@inline function Material( + kind::Symbol, + rho, + eps_r = 1, + mu_r = 1, + T0 = 20, + alpha = 0; + rho_thermal = 0, + theta_max = 90, + tan_delta = 0, + sigma_solar = 0 +) + return Material( + kind, rho, eps_r, mu_r, T0, alpha, rho_thermal, + theta_max, tan_delta, sigma_solar + ) +end + +function validate(material::Material) + isempty(string(material.kind)) && throw(ArgumentError( + "Material.kind cannot be empty; received $(repr(material.kind))" + )) + isnan(material.rho) && throw(DomainError( + material.rho, + "Material.rho must not be NaN" + )) + material.rho > zero(material.rho) || + throw(DomainError(material.rho, "Material.rho must be positive")) + isfinite(material.eps_r) && material.eps_r >= zero(material.eps_r) || + throw(DomainError( + material.eps_r, + "Material.eps_r must be nonnegative and finite" + )) + isfinite(material.mu_r) && material.mu_r > zero(material.mu_r) || + throw(DomainError( + material.mu_r, + "Material.mu_r must be positive and finite" + )) + isfinite(material.T0) || + throw(DomainError(material.T0, "Material.T0 must be finite")) + isfinite(material.alpha) || + throw(DomainError(material.alpha, "Material.alpha must be finite")) + isfinite(material.rho_thermal) && material.rho_thermal >= zero(material.rho_thermal) || + throw(DomainError( + material.rho_thermal, + "Material.rho_thermal must be nonnegative and finite" + )) + isfinite(material.theta_max) || + throw(DomainError(material.theta_max, "Material.theta_max must be finite")) + isfinite(material.tan_delta) && material.tan_delta >= zero(material.tan_delta) || + throw(DomainError( + material.tan_delta, + "Material.tan_delta must be nonnegative and finite" + )) + isfinite(material.sigma_solar) && material.sigma_solar >= zero(material.sigma_solar) || + throw(DomainError( + material.sigma_solar, + "Material.sigma_solar must be nonnegative and finite" + )) + return material +end + +Commons.input_fields(::Type{<:Material}) = (rho=(name="electrical resistivity",unit="Ω·m"), + eps_r=(name="relative permittivity",unit=""),mu_r=(name="relative permeability",unit=""), + T0=(name="reference temperature",unit="°C"),alpha=(name="temperature coefficient",unit="1/°C"), + rho_thermal=(name="thermal resistivity",unit="K·m/W"),theta_max=(name="maximum temperature",unit="°C"), + tan_delta=(name="loss tangent",unit=""),sigma_solar=(name="solar absorption coefficient",unit="")) diff --git a/src/materials/materialslibrary.jl b/src/materials/materialslibrary.jl index c165b9d49..68db059b3 100644 --- a/src/materials/materialslibrary.jl +++ b/src/materials/materialslibrary.jl @@ -1,147 +1,128 @@ """ $(TYPEDEF) -Stores a collection of predefined materials for cable modeling, indexed by material name: +Store materials by name as an `AbstractDict{String, Material}`. + +Ordinary indexed assignment inserts or replaces a material. Use [`add!`](@ref) +when an existing name must be rejected. $(TYPEDFIELDS) """ mutable struct MaterialsLibrary <: AbstractDict{String, Material} - "Dictionary mapping material names to [`Material`](@ref) objects." - data::Dict{String, Material} # Key: Material name, Value: Material object + "Materials indexed by name." + data::Dict{String, Material} end """ $(TYPEDSIGNATURES) -Constructs an empty [`MaterialsLibrary`](@ref) instance and initializes with default materials. +Construct a material library. -# Arguments +# Keywords -- None. +- `add_defaults`: add the package's built-in material records. Default: `true`. # Returns -- A [`MaterialsLibrary`](@ref) object populated with default materials. +- A [`MaterialsLibrary`](@ref), populated with built-in records when + `add_defaults` is `true`. # Examples -```julia -# Create a new, empty library -library = $(FUNCTIONNAME)() +```jldoctest +library = $(FUNCTIONNAME)(; add_defaults=false) +isempty(library) +# output +true ``` -# See also - -- [`Material`](@ref) -- [`_add_default_materials!`](@ref) """ function MaterialsLibrary(; add_defaults::Bool = true)::MaterialsLibrary - library = MaterialsLibrary(Dict{String, Material}()) + library = MaterialsLibrary(Dict{String, Material}()) - if add_defaults - @info "Initializing default materials database..." - _add_default_materials!(library) - end + if add_defaults + _add_default_materials!(library) + end - return library + return library end """ $(TYPEDSIGNATURES) -Populates a [`MaterialsLibrary`](@ref) with commonly used materials, assigning predefined electrical and thermal properties. +Add the built-in material records to `library`. # Arguments -- `library`: Instance of [`MaterialsLibrary`](@ref) to be populated. +- `library`: destination material library. # Returns -- The modified instance of [`MaterialsLibrary`](@ref) containing the predefined materials. - -# Examples - -```julia -library = MaterialsLibrary() -$(FUNCTIONNAME)(library) -``` - -# See also +- The modified `library`. -- [`add!`](@ref) """ function _add_default_materials!(library::MaterialsLibrary) - add!(library, "air", Material(Inf, 1.0, 1.0, 20.0, 0.0)) - add!(library, "pec", Material(eps(), 1.0, 1.0, 20.0, 0.0)) - add!( - library, - "copper", - Material(1.7241e-8, 1.0, 0.999994, 20.0, 0.00393), - ) - add!( - library, - "aluminum", - Material(2.8264e-8, 1.0, 1.000022, 20.0, 0.00429), - ) - add!(library, "xlpe", Material(1.97e14, 2.5, 1.0, 20.0, 0.0)) - add!(library, "pe", Material(1.97e14, 2.3, 1.0, 20.0, 0.0)) - add!( - library, - "semicon1", - Material(1000.0, 1000.0, 1.0, 20.0, 0.0), - ) - add!( - library, - "semicon2", - Material(500.0, 1000.0, 1.0, 20.0, 0.0), - ) - add!( - library, - "polyacrylate", - Material(5.3e3, 32.3, 1.0, 20.0, 0.0), - ) - add!(library, "lead", Material(21.4e-8, 1.0, 0.999983, 20.0, 0.00400)) # Lead or lead alloy - add!(library, "steel", Material(13.8e-8, 1.0, 300.0, 20.0, 0.00450)) # Steel - add!(library, "pp", Material(1e15, 2.8, 1.0, 20.0, 0.0)) # Laminated paper propylene + add!(library, "air", Material(:insulator, Inf, 1.0, 1.0, 20.0, 0.0)) + add!(library, "pec", Material(:conductor, eps(), 1.0, 1.0, 20.0, 0.0)) + add!( + library, + "copper", + Material(:conductor, 1.7241e-8, 1.0, 0.999994, 20.0, 0.00393) + ) + add!( + library, + "aluminum", + Material(:conductor, 2.8264e-8, 1.0, 1.000022, 20.0, 0.00429) + ) + add!(library, "xlpe", Material(:insulator, 1.97e14, 2.5, 1.0, 20.0, 0.0)) + add!(library, "pe", Material(:insulator, 1.97e14, 2.3, 1.0, 20.0, 0.0)) + add!( + library, + "semicon1", + Material(:semicon, 1000.0, 1000.0, 1.0, 20.0, 0.0) + ) + add!( + library, + "semicon2", + Material(:semicon, 500.0, 1000.0, 1.0, 20.0, 0.0) + ) + add!( + library, + "polyacrylate", + Material(:semicon, 5.3e3, 32.3, 1.0, 20.0, 0.0) + ) + add!(library, "lead", Material(:conductor, 21.4e-8, 1.0, 0.999983, 20.0, 0.00400)) # Lead or lead alloy + add!(library, "steel", Material(:conductor, 13.8e-8, 1.0, 300.0, 20.0, 0.00450)) # Steel + add!(library, "pp", Material(:insulator, 1e15, 2.8, 1.0, 20.0, 0.0)) # Laminated paper propylene end - """ $(TYPEDSIGNATURES) -Adds a new material to a [`MaterialsLibrary`](@ref). +Add `material` under `name`. # Arguments -- `library`: Instance of [`MaterialsLibrary`](@ref) where the material will be added. -- `name`: Name of the material. -- `material`: Instance of [`Material`](@ref) containing its properties. +- `library`: destination material library. +- `name`: name of the material. +- `material`: validated material record. # Returns -- The modified instance of [`MaterialsLibrary`](@ref) with the new material added. +- The modified `library`. # Errors -Throws an error if a material with the same name already exists in the library. - -# Examples - -```julia -library = MaterialsLibrary() -material = Material(1.7241e-8, 1.0, 0.999994, 20.0, 0.00393) -$(FUNCTIONNAME)(library, "copper", material) -``` +- Throws `ArgumentError` when `name` already exists. """ function add!( - library::MaterialsLibrary, - name::AbstractString, - material::Material, + library::MaterialsLibrary, + name::Union{AbstractString, Symbol}, + material::Material ) - if haskey(library, name) - Base.error("Material $name already exists in the library.") - end - library[String(name)] = material - library + key = String(name) + haskey(library, key) && throw(ArgumentError("material '$key' already exists")) + validate(material) + library[key] = material + return library end - diff --git a/src/materials/radialdielectric.jl b/src/materials/radialdielectric.jl new file mode 100644 index 000000000..568a51df3 --- /dev/null +++ b/src/materials/radialdielectric.jl @@ -0,0 +1,113 @@ +""" +$(TYPEDEF) + +Frequency-independent description of dielectric materials combined radially +in series. Constituent kinds retain independent insulation and semicon +formulation selection. No constitutive law or reference frequency is selected. + +For logarithmic radial weights ``q_i = \\log(r_{i+1}/r_i)``, the effective +admittivity is ``\\kappa_{eq} = \\sum_i q_i / \\sum_i(q_i/\\kappa_i)``. +The engine evaluates each ``\\kappa_i`` before combining it. + +The displayed `rho` is the DC radial resistivity and `eps_r` is the lossless +radial permittivity, not a lossy fit. There is no frequency-independent +equivalent loss tangent in general. + +$(TYPEDFIELDS) +""" +struct RadialDielectric{T <: Real} <: AbstractMaterial + "Original insulation and semicon materials." + materials::Vector{Material{T}} + "Positive logarithmic radial weights, dimensionless." + weights::Vector{T} + "Passive region classification. Constituent kinds remain authoritative." + kind::Symbol + "Series DC resistivity, in Ω·m." + rho::T + "Lossless relative permittivity, dimensionless." + eps_r::T + "Equivalent relative permeability, dimensionless." + mu_r::T + "Shared constituent reference temperature, in °C." + T0::T + function RadialDielectric(materials::Vector{Material{T}}, weights::Vector{T}, + mu_r::T) where {T <: Real} + isempty(materials) && throw(ArgumentError("RadialDielectric requires at least one constituent")) + length(materials) == length(weights) || throw(DimensionMismatch( + "RadialDielectric requires one radial weight per constituent")) + total = sum(weights) + rho = sum(w * m.rho for (w, m) in zip(weights, materials)) / total + eps_r = total / sum(w / m.eps_r for (w, m) in zip(weights, materials)) + return validate(new{T}(copy(materials), copy(weights), :insulator, + rho, eps_r, mu_r, first(materials).T0)) + end +end + +""" +$(TYPEDSIGNATURES) + +Describe a radial series composition without selecting its dielectric law. + +# Arguments + +- `materials`: physical [`Material`](@ref) constituents, each classified as + `:insulator` or `:semicon`. +- `weights`: positive logarithmic radius ratios, dimensionless. + +# Keywords + +- `mu_r`: equivalent relative permeability. Defaults to the radial weighted + mean. Homogenization may supply its helical-solenoid correction. + +# Returns + +- A [`RadialDielectric`](@ref) with promoted concrete scalar storage. +""" +function RadialDielectric(materials, weights; + mu_r = sum(w * m.mu_r for (w, m) in zip(weights, materials)) / sum(weights)) + T = promote_type(typeof(float(mu_r)), map(eltype, materials)..., + map(x -> typeof(float(x)), weights)...) + return RadialDielectric(Material{T}[convert(Material{T}, m) for m in materials], + T[weights...], convert(T, mu_r)) +end + +function validate(material::RadialDielectric) + all(w -> isfinite(w) && w > zero(w), material.weights) || throw(ArgumentError( + "RadialDielectric.weights must be finite and positive logarithmic radius ratios")) + for (index, constituent) in enumerate(material.materials) + validate(constituent) + constituent.kind in (:insulator, :semicon) || throw(ArgumentError( + "RadialDielectric.materials[$index] must be insulation or semicon, not :$(constituent.kind)")) + isapprox(constituent.T0, material.T0) || throw(ArgumentError( + "RadialDielectric constituents must share one reference temperature")) + end + isfinite(material.mu_r) && material.mu_r > zero(material.mu_r) || + throw(ArgumentError("RadialDielectric.mu_r must be finite and positive")) + return material +end + +Base.eltype(::RadialDielectric{T}) where {T} = T +Base.eltype(::Type{RadialDielectric{T}}) where {T} = T +function Base.convert(::Type{RadialDielectric{T}}, material::RadialDielectric) where {T <: Real} + return RadialDielectric(Material{T}[convert(Material{T}, m) for m in material.materials], + T[material.weights...], convert(T, material.mu_r)) +end +Base.convert(::Type{RadialDielectric{T}}, material::RadialDielectric{T}) where {T <: Real} = material +Base.:(==)(a::RadialDielectric, b::RadialDielectric) = + a.materials == b.materials && a.weights == b.weights && a.mu_r == b.mu_r +Base.isequal(a::RadialDielectric, b::RadialDielectric) = + isequal(a.materials, b.materials) && isequal(a.weights, b.weights) && isequal(a.mu_r, b.mu_r) +Base.hash(material::RadialDielectric, h::UInt) = + hash(material.mu_r, hash(material.weights, hash(material.materials, hash(:RadialDielectric, h)))) +TextDisplay.name(::Type{<:RadialDielectric}) = "RadialDielectric" +Base.summary(io::IO, material::RadialDielectric) = + print(io, "RadialDielectric · ", length(material.materials), " constituents") +Base.show(io::IO, material::RadialDielectric) = summary(io, material) +function Base.show(io::IO, ::MIME"text/plain", material::RadialDielectric) + get(io, :compact, false) && return show(io, material) + return TextDisplay.fields(io, "RadialDielectric", ( + constituents = length(material.materials), + ρ_DC = TextDisplay.engineering(material.rho, :ohm_meter), + εᵣ_lossless = TextDisplay.value(material.eps_r), + μᵣ = TextDisplay.value(material.mu_r)); multiline = true) +end diff --git a/src/materials/temperaturedependent/TemperatureDependent.jl b/src/materials/temperaturedependent/TemperatureDependent.jl new file mode 100644 index 000000000..86c76b796 --- /dev/null +++ b/src/materials/temperaturedependent/TemperatureDependent.jl @@ -0,0 +1,44 @@ +""" + LineCableModels.Materials.TemperatureDependent + +Evaluate electrical resistivity at a prescribed temperature from a material's +reference calibration and a selected constitutive equation. + +# Dependencies + +$(IMPORTS) +""" +module TemperatureDependent +import ...Commons: FormulationOptions, formulas + +export Formula, formula_id, formulas +public temperature_resistivity + +#! explicit-imports: off +using DocStringExtensions: IMPORTS, TYPEDEF, TYPEDFIELDS, TYPEDSIGNATURES +#! explicit-imports: on +using ..Materials: Material +import ...TextDisplay +import ...Commons: AbstractFormulation, Functor, formulation_options +import ...LineCableModels: FormulaDefinition, Expression, constitutive, + formula_id, validate +#! explicit-imports: off +# Used by the formula files included below. +import ...LineCableModels: description +#! explicit-imports: on + +include("interface.jl") + +public TemperatureDependentFormulation + +#! explicit-imports: off +const FORMULAS = ( + include("formulas/linear.jl"), + include("formulas/default.jl"), +) +#! explicit-imports: on + +"""Return the registered electrical-resistivity temperature laws.""" +formulas(::Type{<:Formula}) = FORMULAS + +end # module TemperatureDependent diff --git a/src/materials/temperaturedependent/formulas/default.jl b/src/materials/temperaturedependent/formulas/default.jl new file mode 100644 index 000000000..821e0315c --- /dev/null +++ b/src/materials/temperaturedependent/formulas/default.jl @@ -0,0 +1,12 @@ +""" +$(TYPEDSIGNATURES) + +Route the default selection to the explicit `:linear` implementation. +No numerical equation is owned by `:default`. +""" +description(::Type{<:Formula{:default}}; compact::Bool=false) = + compact ? "Default" : "Default routing to :linear" + +Formula{:default}(; kwargs...) = Formula{:linear}(; kwargs...) + +:default diff --git a/src/materials/temperaturedependent/formulas/linear.jl b/src/materials/temperaturedependent/formulas/linear.jl new file mode 100644 index 000000000..f986468e5 --- /dev/null +++ b/src/materials/temperaturedependent/formulas/linear.jl @@ -0,0 +1,31 @@ +""" +$(TYPEDSIGNATURES) + +**Identification.** Linear electrical-resistivity temperature dependence. + +**Expression.** ``\\rho(T)=\\rho_0[1+\\alpha(T-T_0)]``, with resistivity in Ω·m, +temperature in °C and ``\\alpha`` in K⁻¹. Each reference material supplies its +own calibration. Infinite passive resistivity remains infinite. + +**Applicability.** The applicability limit is ``|T-T_0|<150`` K and a +finite positive correction factor. This is a restriction of this approximation, +not a thermal-rating or material operating-temperature limit. + +**Reference.** Package default linear resistivity approximation. +""" +description(::Type{<:Formula{:linear}}; compact::Bool=false) = compact ? "Linear" : "Linear electrical-resistivity temperature dependence" + +function temperature_resistivity(::Formula{:linear}, functor, workspace) + (; material, temperature) = functor.input + difference = temperature - material.T0 + abs(difference) < oftype(difference, 150) || throw(DomainError(temperature, + "temperature is outside the linear resistivity model range relative to $(material.T0) °C")) + factor = one(difference) + material.alpha * difference + isfinite(factor) && factor > zero(factor) || throw(DomainError(factor, + "linear resistivity correction factor must be positive and finite")) + return isinf(material.rho) ? material.rho : material.rho * factor +end + +formulation_options(::Expression{<:Formula{:linear}, typeof(temperature_resistivity)}) = FormulationOptions() + +:linear diff --git a/src/materials/temperaturedependent/interface.jl b/src/materials/temperaturedependent/interface.jl new file mode 100644 index 000000000..8d71704ab --- /dev/null +++ b/src/materials/temperaturedependent/interface.jl @@ -0,0 +1,138 @@ +""" +Interface for a selected temperature-dependent resistivity law. +Concrete selections expose model `parameters` and numerical `options` records. +""" +abstract type TemperatureDependentFormulation <: AbstractFormulation end + +""" +$(TYPEDEF) + +Select a scalar electrical-resistivity law evaluated from reference material +properties and prescribed temperature. Reference material values are immutable. + +$(TYPEDFIELDS) +""" +struct Formula{ID, P <: NamedTuple, O <: FormulationOptions} <: + TemperatureDependentFormulation + "Resolved physical model parameters." + parameters::P + "Normalized numerical sections for this equation." + options::O +end + +TextDisplay.@showfields Formula "Formula" selected -> ( + id = formula_id(selected),) + +"""Evaluate a temperature-dependent electrical resistivity in ohm meters.""" +function temperature_resistivity end + +""" +$(TYPEDSIGNATURES) + +Construct a temperature-dependent resistivity law with model parameters and +numerical controls. Custom formulations extend +`temperature_resistivity(selected, functor, workspace)` on their own concrete selection type. +The input of `functor` holds the reference material, the temperature and the options. +""" +function Formula{ID}(; parameters::NamedTuple = (;), + options::Union{NamedTuple, FormulationOptions} = FormulationOptions()) where {ID} + options = options isa NamedTuple ? FormulationOptions(options) : options + isempty(parameters) || + throw(ArgumentError("formula :$ID has no configurable model parameters")) + selected = Formula{ID, typeof(parameters), typeof(options)}(parameters, options) + expression = Expression(selected, temperature_resistivity) + normalized = formulation_options(expression, options) + return Formula{ID, typeof(parameters), typeof(normalized)}(parameters, normalized) +end + +""" +$(TYPEDSIGNATURES) + +Validate evaluated resistivity \\[Ω·m\\] for a reference material and prescribed +temperature \\[°C\\]. Resistivity must be real and positive. Conductors require a +finite value. Passive materials admit infinite resistivity. Return `rho`. +""" +function validate(rho, ::Union{Nothing, TemperatureDependentFormulation}, material::Material, temperature::Real) + isfinite(temperature) || throw(DomainError(temperature, + "constitutive temperature must be finite")) + rho isa Real && !isnan(rho) && rho > zero(rho) || throw(DomainError(rho, + "temperature law must return positive real resistivity for $(material.kind) at $temperature °C")) + material.kind === :conductor && !isfinite(rho) && + throw(DomainError(rho, + "conductor resistivity must be finite at $temperature °C")) + return rho +end + +""" +$(TYPEDSIGNATURES) + +Build the Functor of a temperature-dependent resistivity law for one reference material at +one temperature, after checking the temperature. The state is empty. +""" +function Functor(formula::TemperatureDependentFormulation, input::NamedTuple; + workspace = nothing) + (; temperature) = input + isfinite(temperature) || throw(DomainError(temperature, + "constitutive temperature must be finite")) + return Functor(formula, input, (;)) +end + +@inline function (formula::TemperatureDependentFormulation)( + material::Material{T}, temperature::T; + workspace = nothing) where {T <: Real} + functor = Functor(formula, (; material, temperature, options = formula.options); + workspace) + rho = Expression(formula, temperature_resistivity)(functor, workspace) + return validate(rho, formula, material, temperature) +end + +function (formula::TemperatureDependentFormulation)( + material::Material{T}, temperature::Real; + workspace = nothing) where {T <: Real} + U = promote_type(T, typeof(float(temperature))) + return formula(convert(Material{U}, material), convert(U, temperature); workspace) +end + +"""Evaluate a selected resistivity law at prescribed temperature in °C. Return Ω·m.""" +function constitutive( + formula::TemperatureDependentFormulation, material::Material, temperature::Real; + workspace = nothing) + formula(material, temperature; workspace) +end + +"""Retain reference resistivity in Ω·m when no temperature law is selected.""" +function constitutive(::Nothing, material::Material, temperature::Real; workspace = nothing) + validate(material.rho, nothing, material, temperature) +end + +Formula(identifier::Symbol; kwargs...) = Formula(Val(identifier); kwargs...) +Formula(::Val{ID}; kwargs...) where {ID} = Formula{ID}(; kwargs...) +Formula(selected::TemperatureDependentFormulation) = selected + +function Formula(selection::FormulaDefinition{ID, Order}) where {ID, Order} + Order === :default || throw(ArgumentError("order applies only to equivalent_earth")) + selection.equivalent_earth === nothing || throw(ArgumentError( + "equivalent_earth applies only to external earth formulas")) + return Formula{ID}(; parameters = selection.parameters, + options = selection.options) +end + +"""Return the stable identifier of a temperature-dependent resistivity law.""" +formula_id(::Formula{ID}) where {ID} = ID +formula_id(::Type{<:Formula{ID}}) where {ID} = ID +# Identity-only dispatch also describes retained selections without constructors. +description(value::Formula; compact::Bool = false) = description(typeof(value); compact) +formulation_options(value::Formula) = value.options + +""" +$(TYPEDSIGNATURES) + +Expose the selected identity, model parameters, and numerical options as a native record. +""" +function Base.NamedTuple(value::Formula) + return (identifier = formula_id(value), + parameters = value.parameters, options = value.options.data) +end + +"""Iterate the independently selectable child slots admitted by this formula family.""" +Base.pairs(::Type{<:Formula}; quantity = nothing) = pairs((;)) diff --git a/src/materials/typecoercion.jl b/src/materials/typecoercion.jl deleted file mode 100644 index 9cd1b075a..000000000 --- a/src/materials/typecoercion.jl +++ /dev/null @@ -1,11 +0,0 @@ -# Identity: no allocation if already at T -@inline coerce_to_T(m::Material{T}, ::Type{T}) where {T} = m - -# Cross-T rebuild: use the TYPED constructor to avoid surprise promotion -@inline coerce_to_T(m::Material{S}, ::Type{T}) where {S,T} = Material{T}( - coerce_to_T(m.rho, T), - coerce_to_T(m.eps_r, T), - coerce_to_T(m.mu_r, T), - coerce_to_T(m.T0, T), - coerce_to_T(m.alpha, T), -) \ No newline at end of file diff --git a/src/parametricbuilder/ParametricBuilder.jl b/src/parametricbuilder/ParametricBuilder.jl index a9a87fd8e..5031514a1 100644 --- a/src/parametricbuilder/ParametricBuilder.jl +++ b/src/parametricbuilder/ParametricBuilder.jl @@ -1,92 +1,100 @@ +""" + LineCableModels.ParametricBuilder + +Construct finite parameter spaces and materialize cable problems from explicit +`Grid` inputs. + +# Overview + +- Define deterministic and uncertainty-bearing finite sources. +- Compose sources with product or zip semantics. +- Materialize materials, cable parts, cable designs, positions, earth models, + and line-parameter problems. +- Evaluate every materialized problem with `Combinatorial`. +- Transport completed result spaces into target-bearing downstream problem + spaces. +- Estimate stranded-conductor and wire-screen patterns. +""" module ParametricBuilder +import ..LineCableModels + +export Grid, AbsoluteError, DeterministicGrid, RelativeGrid, AbsoluteGrid +export AbstractGrid, AbstractUncertainGrid, UncertainValue +export Gridspace +export has_uncertainty, nominal, uncertainty +export @gridspace +export build + +export Combinatorial, ParametricProblem, ParametricResult + +export Material +export CableDesign, LineCableSystem +export Disk, Rectangle, Ellipse, Sector, Annulus, Polygon, Shell, Pose2 +export Region, Stack +export Group, Assembly +export Enclosure +export terminal, core, stranded, milliken, rope, cores, tape, insulation, screen, sheath +export armor, bedding, jacket, filler, pipe, duct +export solid, shell, wires, layers, assembly +export capacity +export FillFactor +export at, trefoil, hflat, vflat, layer, homogeneous, EarthLayer, EarthModel +export @cable, @system, @earth, @terminal, @assembly, @pipe, @duct +export @at, @hflat, @vflat, @trefoil +export @distribute +export WireEstimate, estimate_stranding, estimate_screen + +using DocStringExtensions: SIGNATURES, TYPEDSIGNATURES, TYPEDEF, TYPEDFIELDS +import Random +import ..LineCableModels: add!, build, Gridpoint, validate +import ..LineCableModels: Grid, AbsoluteError, DeterministicGrid, RelativeGrid +import ..LineCableModels: AbsoluteGrid, AbstractGrid, AbstractUncertainGrid +import ..LineCableModels: UncertainValue, Gridspace, has_uncertainty +import ..LineCableModels: parameterize, materialize, points +import ..LineCableModels: verbosity, VerbosityLogger +import Logging +import ..Commons +import ..Commons: compute, computation_options, computation_details, details, + nominal, uncertainty +using ..Commons: + AbstractProblemDefinition, AbstractFormulation, AbstractResultSpace, + AbstractParametricResult, + ComputationOptions, ComputationDetails +import ..Materials +import ..Materials: Material +import ..DataModel +import ..DataModel: Disk, Rectangle, Ellipse, Sector, Annulus, Polygon +import ..DataModel: Shell, Pose2 +import ..DataModel: Region, Stack +import ..DataModel: Group, Assembly +import ..DataModel: Enclosure +import ..DataModel: capacity, FillFactor +import ..DataModel: CableDesign, LineCableSystem +import ..Earth +using ..Earth: EarthLayer, EarthModel, layer, homogeneous +import ..Engine +import ..Engine: compare +import ..TextDisplay + +include("macros.jl") +include("results.jl") + +include("material.jl") +include("geometry.jl") +include("physicaltree.jl") +include("positions.jl") +include("construction_macros.jl") +include("system.jl") + +include("engine/cableconstants.jl") +include("traversal.jl") +include("comparisons.jl") -# Export public API -export make_stranded, make_screened -export Conductor, Insulator, Material, CableBuilder -export at, trifoil, Earth, SystemBuilder -export determinize - -# Module-specific dependencies -using ..Commons -import ..Commons: add! -using ..Materials: Materials -using ..DataModel: DataModel, trifoil_formation, CableDesign, get_outer_radius -using ..EarthProps: EarthModel -using ..Engine: LineParametersProblem -using ..Utils: to_nominal -using Measurements -using Base.Iterators: product - - - -# normalize input to (spec, pct) -function _spec(x) - if x isa Tuple && length(x) == 2 - spec, pct = x - _validate_valuespec(spec) - _validate_valuespec(pct) - return (spec, pct) - else - _validate_valuespec(x) - return (x, nothing) - end -end - -# THIS is the minimal validator -@inline function _validate_valuespec(x) - if x isa Tuple && length(x) == 3 && x[1] isa Number && x[2] isa Number && - x[3] isa Integer - lo, hi, n = x - n >= 2 || error("Range (lo,hi,n) must have n ≥ 2, got $x") - end - return nothing -end - - -_values(x::Number) = (x,) -_values(v::AbstractVector) = collect(v) -_values(t::Tuple{<:Number, <:Number, <:Integer}) = range(t[1], t[2]; length = t[3]) - - -_pcts(::Nothing) = (0.0,) -_pcts(p::Number) = (float(p),) -_pcts(v::AbstractVector) = map(float, collect(v)) -_pcts(t::Tuple{<:Number, <:Number, <:Integer}) = range(t[1], t[2]; length = t[3]) - - -function _make_range(spec; pct = nothing) +include("wirepatterns/WirePatterns.jl") +using .WirePatterns: WireEstimate, estimate_stranding, estimate_screen - if spec isa Tuple && length(spec)==3 - lo, hi, n = spec - n < 2 && Base.error("Invalid (lo,hi,n) values range: n=$n must be ≥2") - end +include("textdisplay.jl") - vs, ps = collect(_values(spec)), collect(_pcts(pct)) - if all(p->p==0.0, ps) - return vs - end - out = Any[] - for v in vs, p in ps - push!(out, measurement(v, abs(v)*(p/100))) - end - out -end +public traverse -# expand positional args tuple → iterator of resolved tuples -function _expand_args(args::Tuple) - spaces = - map(a -> (a isa Tuple && length(a)==2 ? _make_range(a[1]; pct = a[2]) : (a,)), args) - return (tuple(vals...) for vals in Iterators.product(spaces...)) end - -include("materialspec.jl") -include("cablebuilderspec.jl") -include("systembuilderspec.jl") -include("determinize.jl") -include("base.jl") - -# Submodule `WirePatterns` -include("wirepatterns/WirePatterns.jl") -using .WirePatterns - -end # module ParametricBuilder diff --git a/src/parametricbuilder/base.jl b/src/parametricbuilder/base.jl deleted file mode 100644 index 0a0bfcdf0..000000000 --- a/src/parametricbuilder/base.jl +++ /dev/null @@ -1,318 +0,0 @@ -Base.IteratorEltype(::Type{CableBuilderSpec}) = Base.HasEltype() -Base.eltype(::Type{CableBuilderSpec}) = DataModel.CableDesign -Base.IteratorSize(::Type{CableBuilderSpec}) = Base.SizeUnknown() - -function Base.iterate(cbs::CableBuilderSpec) - ch = iterate(cbs) - try - d = take!(ch) - return (d, ch) - catch - return nothing - end -end - -function Base.iterate(::CableBuilderSpec, ch::Channel) - try - d = take!(ch) - return (d, ch) - catch - return nothing - end -end - - -# how many choices are in a "range-like" thing -_choice_count(x) = - x === nothing ? 1 : - (x isa Tuple && length(x) == 2) ? _choice_count(x[1]) * _choice_count(x[2]) : - (x isa AbstractVector) ? length(x) : - (x isa Tuple && length(x) == 3) ? last(x) : 1 - -# count choices for a MaterialSpec (rho/eps/mu/T/α product) -_choice_count(ms::MaterialSpec) = length(_make_range(ms)) - -# args: each entry can be scalar | vector | (lo,hi,n) | (value_spec, pct_spec) -_arg_choice_count(a) = - (a isa Tuple && length(a) == 2) ? (_choice_count(a[1]) * _choice_count(a[2])) : - _choice_count(a) - -_args_choice_count(args::Tuple) = - isempty(args) ? 1 : prod(_arg_choice_count(a) for a in args) - -function cardinality(cbs::CableBuilderSpec) - comp_names = unique(p.component for p in cbs.parts) - by_comp = Dict{Symbol, Vector{PartSpec}}() - for p in cbs.parts - get!(by_comp, p.component, PartSpec[]) |> v -> push!(v, p) - end - - total = 1 - for cname in comp_names - ps = by_comp[cname] - cond = [p for p in ps if p.part_type <: DataModel.AbstractConductorPart] - insu = [p for p in ps if p.part_type <: DataModel.AbstractInsulatorPart] - isempty(cond) && Base.error("component '$cname' has no conductors") - isempty(insu) && Base.error("component '$cname' has no insulators") - - # first conductor axes - p1c = cond[1] - c_dim = _choice_count(p1c.dim[1]) * _choice_count(p1c.dim[2]) - c_args = _args_choice_count(p1c.args) - c_mat = _choice_count(p1c.material) - - # uncoupled extras from later conductors (couple when tuples compare equal) - for pc in cond[2:end] - pc_dim_same = (pc.dim == p1c.dim) - pc_args_same = (pc.args == p1c.args) - pc_mat_same = (pc.material == p1c.material) - - c_dim *= pc_dim_same ? 1 : (_choice_count(pc.dim[1]) * _choice_count(pc.dim[2])) - c_args *= pc_args_same ? 1 : _args_choice_count(pc.args) - c_mat *= pc_mat_same ? 1 : _choice_count(pc.material) - end - cond_factor = c_dim * c_args * c_mat - - # first insulator axes - p1i = insu[1] - i_dim = _choice_count(p1i.dim[1]) * _choice_count(p1i.dim[2]) - i_args = _args_choice_count(p1i.args) - i_mat = _choice_count(p1i.material) - - for pi in insu[2:end] - pi_dim_same = (pi.dim == p1i.dim) - pi_args_same = (pi.args == p1i.args) - pi_mat_same = (pi.material == p1i.material) - - i_dim *= pi_dim_same ? 1 : (_choice_count(pi.dim[1]) * _choice_count(pi.dim[2])) - i_args *= pi_args_same ? 1 : _args_choice_count(pi.args) - i_mat *= pi_mat_same ? 1 : _choice_count(pi.material) - end - insu_factor = i_dim * i_args * i_mat - - total *= cond_factor * insu_factor - end - return total -end - -Base.length(cbs::CableBuilderSpec) = cardinality(cbs) - -function Base.show(io::IO, ::MIME"text/plain", cbs::CableBuilderSpec) - comp_names = unique(p.component for p in cbs.parts) - by_comp = Dict{Symbol, Vector{PartSpec}}() - for p in cbs.parts - get!(by_comp, p.component, PartSpec[]) |> v -> push!(v, p) - end - - println(io, "CableBuilderSpec(\"", cbs.cable_id, "\")") - println(io, " components: ", join(string.(comp_names), ", ")) - - total = 1 - for cname in comp_names - ps = by_comp[cname] - cond = [p for p in ps if p.part_type <: DataModel.AbstractConductorPart] - insu = [p for p in ps if p.part_type <: DataModel.AbstractInsulatorPart] - isempty(cond) && Base.error("component '$cname' has no conductors") - isempty(insu) && Base.error("component '$cname' has no insulators") - - # conductors: couple to first when tuples compare equal - p1c = cond[1] - c_dim = _choice_count(p1c.dim[1]) * _choice_count(p1c.dim[2]) - c_args = _args_choice_count(p1c.args) - c_mat = _choice_count(p1c.material) - for pc in cond[2:end] - c_dim *= (pc.dim == p1c.dim) ? 1 : (_choice_count(pc.dim[1]) * _choice_count(pc.dim[2])) - c_args *= (pc.args == p1c.args) ? 1 : _args_choice_count(pc.args) - c_mat *= (pc.material == p1c.material) ? 1 : _choice_count(pc.material) - end - cond_factor = c_dim * c_args * c_mat - - # insulators: same coupling rule vs first insulator - p1i = insu[1] - i_dim = _choice_count(p1i.dim[1]) * _choice_count(p1i.dim[2]) - i_args = _args_choice_count(p1i.args) - i_mat = _choice_count(p1i.material) - for pi in insu[2:end] - i_dim *= (pi.dim == p1i.dim) ? 1 : (_choice_count(pi.dim[1]) * _choice_count(pi.dim[2])) - i_args *= (pi.args == p1i.args) ? 1 : _args_choice_count(pi.args) - i_mat *= (pi.material == p1i.material) ? 1 : _choice_count(pi.material) - end - insu_factor = i_dim * i_args * i_mat - - fac = cond_factor * insu_factor - total *= fac - print(io, " • ", cname, ": ") - print(io, "cond(dim=", c_dim, ", args=", c_args, ", mat=", c_mat, "); ") - println(io, "insu(dim=", i_dim, ", args=", i_args, ", mat=", i_mat, ") ⇒ ×", fac) - end - - println(io, " cardinality: ", total) - if cbs.nominal !== nothing - println(io, " nominal: ", typeof(cbs.nominal)) - end -end - - -function show_trace(tr::DesignTrace) - println("Design: ", tr.cable_id) - for comp in tr.components - println(" Component: ", comp.name) - for c in comp.choices - mat = c.mat - println(" [", c.role, "] ", c.T, - " layers=", c.layers, - " dim=", c.dim, - " args=", c.args, - " ρ=", mat.rho, " εr=", mat.eps_r, " μr=", mat.mu_r) - end - end -end - -Base.show(io::IO, ::MIME"text/plain", tr::DesignTrace) = show_trace(tr) - -_earth_choice_count(e::EarthSpec) = - _choice_count(e.rho) * _choice_count(e.eps_r) * _choice_count(e.mu_r) * - _choice_count(e.t) - -# == Public cardinality API == -function cardinality(s::SystemBuilderSpec) - # designs from CableBuilderSpec (uses existing cardinality(cbs::CableBuilderSpec)) - n_builder = cardinality(s.builder) - - # length / temperature / earth choices via existing expanders - n_len = length(collect(_expand_pair(s.length))) - n_temp = length(collect(_expand_pair(s.temperature))) - n_earth = length(collect(_expand_earth(s.earth))) - - # positions: product over singles and groups - n_pos = isempty(s.positions) ? 1 : prod(_position_choice_count(p) for p in s.positions) - - return n_builder * n_len * n_temp * n_earth * n_pos -end - -Base.length(spec::SystemBuilderSpec) = cardinality(spec) - -# == Iterator over fully formed LineParametersProblem (skips overlaps silently) == -function Base.iterate(spec::SystemBuilderSpec) - ch = iterate(spec) - try - x = take!(ch); - return (x, ch) - catch e - @error "SystemBuilderSpec iteration failed before first yield" exception=( - e, - catch_backtrace(), - ) - - rethrow() - end -end - -function Base.iterate(::SystemBuilderSpec, ch::Channel{LineParametersProblem}) - try - x = take!(ch); - return (x, ch) - catch - return nothing - end -end - -Base.IteratorEltype(::Type{SystemBuilderSpec}) = Base.HasEltype() -Base.eltype(::Type{SystemBuilderSpec}) = LineParametersProblem -Base.IteratorSize(::Type{SystemBuilderSpec}) = Base.SizeUnknown() - -# == Terse pretty printer (because why the hell not?) == -# show at most `limit` values: "v1, v2, ..., vN (N=total)" -_fmt_vals(vals; limit = 8) = begin - v = collect(vals) - n = length(v) - if n == 0 - "∅" - elseif n <= limit - string(join(v, ", ")) - else - string(join(v[1:limit], ", "), ", … (N=", n, ")") - end -end - -# expand one knob (your (valuespec,pct) grammar) into concrete values -_vals_pair(p) = collect(_expand_pair(p)) -# axis around anchor (handles (nothing, pct) → uncertain anchor) -_vals_axis(anchor, dspec) = collect(_axis(anchor, dspec)) - -# deterministic freq summary: list if tiny, else min..max (N) -_fmt_freqs(f::AbstractVector) = - length(f) ≤ 8 ? join(f, ", ") : - string(first(f), " … ", last(f), " (N=", length(f), ")") - -# stable, human order for phases: core,sheath,jacket first if present, then alphabetical -function _fmt_map(conn::Dict{String, Int}) - prio = Dict("core"=>1, "sheath"=>2, "jacket"=>3) - ks = collect(keys(conn)) - sort!(ks, by = k -> (get(prio, k, 1000), k)) - # FIX: use getindex, not get - vs = getindex.(Ref(conn), ks) # or: map(k -> conn[k], ks) - return join(string.(ks, "=>", vs), ", ") -end - -# helper for printing a single arbitrary position (old behaviour) -function _show_position(io::IO, i::Int, p::PositionSpec) - dxvals = _vals_axis(p.x0, p.dx) - dyvals = _vals_axis(p.y0, p.dy) - println( - io, - " • p", i, - " x: ", _fmt_vals(dxvals), - ", y: ", _fmt_vals(dyvals), - ", phases: {", _fmt_map(p.conn), "}", - ) -end - -# helper for printing a grouped formation -function _show_position(io::IO, i::Int, g::PositionGroupSpec) - x0, y0 = g.anchor - dvals = _vals_pair(g.d) - - # phases: show one map per leg, reusing _fmt_map - phase_str = "[" * join((_fmt_map(c) for c in g.conn), "; ") * "]" - - println( - io, - " • p", i, - " group(", g.arrangement, ", n=", g.n, ")", - " anchor=(", x0, ", ", y0, ")", - ", d: ", _fmt_vals(dvals), - ", phases: ", phase_str, - ) -end - -function Base.show(io::IO, ::MIME"text/plain", spec::SystemBuilderSpec) - println(io, "SystemBuilder(\"", spec.system_id, "\")") - println(io, " designs × = ", cardinality(spec.builder)) - - # positions block - println(io, " positions = ", length(spec.positions)) - for (i, p) in enumerate(spec.positions) - _show_position(io, i, p) # dispatches on PositionSpec vs PositionGroupSpec - end - - # system scalars - println(io, " length = ", _fmt_vals(_vals_pair(spec.length))) - println(io, " temp = ", _fmt_vals(_vals_pair(spec.temperature))) - - # earth knobs (each axis separately) - println(io, " earth:") - println(io, " ρ = ", _fmt_vals(_vals_pair(spec.earth.rho))) - println(io, " εr = ", _fmt_vals(_vals_pair(spec.earth.eps_r))) - println(io, " μr = ", _fmt_vals(_vals_pair(spec.earth.mu_r))) - println(io, " t = ", _fmt_vals(_vals_pair(spec.earth.t))) - - # frequencies (deterministic vector coming from the user/spec) - if hasfield(SystemBuilderSpec, :frequencies) && - !isempty(getproperty(spec, :frequencies)) - f = getproperty(spec, :frequencies) - println(io, " f = ", _fmt_freqs(f)) - end - - println(io, " cardinality (upper bound): ", cardinality(spec)) -end diff --git a/src/parametricbuilder/cablebuilderspec.jl b/src/parametricbuilder/cablebuilderspec.jl deleted file mode 100644 index b09bd61ff..000000000 --- a/src/parametricbuilder/cablebuilderspec.jl +++ /dev/null @@ -1,523 +0,0 @@ -# spec: (value_spec, pct_spec) — pct_spec can be `nothing | number | vector | (lo,hi,n)` -""" -PartSpec: -- component::Symbol # e.g. :core, :sheath, :jacket -- part_type::Type # CircStrands, Tubular, Strip, Insulator, Semicon, … -- n_layers::Int # how many stacked layers of this part_type -- dim::Tuple # diameter OR thickness OR radius (spec, pct) -- args::Tuple # specialized ctor positional args -- material::MaterialSpec -""" -struct PartSpec - component::Symbol - part_type::Type - n_layers::Int - dim::Tuple # (spec, pct) - args::Tuple # positional args; each entry is either a number or (spec, pct) - material::MaterialSpec -end - -PartSpec(component::Symbol, part_type::Type, n_layers::Int; - dim, args = (), material::MaterialSpec) = - PartSpec(component, part_type, n_layers, dim, args, material) - -""" -CableBuilderSpec: -- cable_id::String -- parts::Vector{PartSpec} # may interleave conductor/insulator arbitrarily -- nominal::Union{Nothing,DataModel.NominalData} -""" -struct CableBuilderSpec - cable_id::String - parts::Vector{PartSpec} - nominal::Union{Nothing, DataModel.NominalData} -end -CableBuilder(id::AbstractString, parts::Vector{PartSpec}; nominal = nothing) = - CableBuilderSpec(String(id), parts, nominal) - -# --- minimal flattening helpers (accept PartSpec or collections of them) ----- -function _collect_parts!(acc::Vector{PartSpec}, x) - if x isa PartSpec - push!(acc, x) - elseif x isa AbstractVector - @inbounds for y in x - _collect_parts!(acc, y) - end - elseif isnothing(x) - @warn "Ignoring `nothing` in parts collection." - else - Base.error("Expected PartSpec or a collection of PartSpec; got $(typeof(x))") - end - return acc -end - -# ctor that accepts a vector with possible nested vectors (no splat needed) -function CableBuilder(id::AbstractString, parts_any::AbstractVector; nominal = nothing) - acc = PartSpec[] - _collect_parts!(acc, parts_any) - return CableBuilderSpec(String(id), acc, nominal) # calls your primary ctor -end - -# ctor that accepts varargs (mixed PartSpec and vectors), plus nominal kw -function CableBuilder(id::AbstractString, parts...; nominal = nothing) - acc = PartSpec[] - @inbounds for p in parts - _collect_parts!(acc, p) - end - return CableBuilderSpec(String(id), acc, nominal) -end - -struct PartChoice - idx::Int # index in ps vector (1-based) - role::Symbol # :conductor or :insulator - T::Type - dim::Any # chosen scalar (Diameter/Thickness proxy input) - args::Tuple # chosen positional args (scalars) - mat::Materials.Material # concrete material used - layers::Int # n_layers replicated with that choice -end - -struct ComponentTrace - name::String - choices::Vector{PartChoice} -end - -struct DesignTrace - cable_id::String - components::Vector{ComponentTrace} -end - - - -# ----- anchor: last physical layer, not the container ----- -@inline _anchor(x::Real) = x -@inline _anchor(x::DataModel.AbstractConductorPart) = x -@inline _anchor(x::DataModel.AbstractInsulatorPart) = x - -@inline function _anchor(g::DataModel.ConductorGroup) - L = getproperty(g, :layers) - @assert !isempty(L) "ConductorGroup has no layers to anchor on." - return L[end] -end -@inline function _anchor(g::DataModel.InsulatorGroup) - L = getproperty(g, :layers) - @assert !isempty(L) "InsulatorGroup has no layers to anchor on." - return L[end] -end - -# ----- proxy for r_ex by CONTRACT ----- -@inline function _resolve_dim(T::Type, is_abs_first::Bool) - return T <: DataModel.AbstractStrandsLayer ? :diameter : - (is_abs_first && T === DataModel.Tubular ? :diameter : :thickness) -end - -@inline _make_dim(::Val{:diameter}, d) = DataModel.Diameter(d) -@inline _make_dim(::Val{:thickness}, d) = DataModel.Thickness(d) -@inline _make_dim(::Val{:radius}, r) = r # if direct radius -@inline _make_dim(sym::Symbol, d) = _make_dim(Val(sym), d) - - -function _init_cg(T::Type, base, dim_val, args_pos::Tuple, mat; abs_first::Bool) - - r_in = _anchor(base) - sym = _resolve_dim(T, abs_first) - _ = _make_dim(sym, dim_val) # keeps intent (CircStrands ignores this) - - if T <: DataModel.AbstractStrandsLayer - @assert length(args_pos) ≥ 1 "CircStrands needs (n, [lay])." - n = args_pos[1] - lay = length(args_pos) ≥ 2 ? args_pos[2] : 0.0 - return DataModel.ConductorGroup( - DataModel.CircStrands(r_in, DataModel.Diameter(dim_val), n, lay, mat), - ) - else - return DataModel.ConductorGroup(T(r_in, _make_dim(sym, dim_val), args_pos..., mat)) - end -end - - -function _add_conductor!( - cg::DataModel.ConductorGroup, - T::Type, - dim_val, - args_pos::Tuple, - mat; - layer::Int, -) - if T <: DataModel.AbstractStrandsLayer - @assert length(args_pos) ≥ 1 "CircStrands needs (n, [lay])." - n = args_pos[1] - lay = length(args_pos) ≥ 2 ? args_pos[2] : 0.0 - add!(cg, DataModel.CircStrands, DataModel.Diameter(dim_val), layer*n, lay, mat) - else - # thickness by contract for all non-wire additions - add!(cg, T, _make_dim(:thickness, dim_val), args_pos..., mat) - end -end - - -function _init_ig(T::Type, cg::DataModel.ConductorGroup, dim_val, args_pos::Tuple, mat) - c_last = _anchor(cg) - obj = T(c_last, DataModel.Thickness(dim_val), args_pos..., mat) # insulators use THICKNESS - return DataModel.InsulatorGroup(obj) -end - - -function _add_insulator!( - ig::DataModel.InsulatorGroup, - T::Type, - dim_val, - args_pos::Tuple, - mat, -) - add!(ig, T, DataModel.Thickness(dim_val), args_pos..., mat) -end - -# Build all variants of ONE component, anchored at `base` (0.0 for the very first) -function _make_variants(ps::Vector{PartSpec}, base) - cond = [p for p in ps if p.part_type <: DataModel.AbstractConductorPart] - insu = [p for p in ps if p.part_type <: DataModel.AbstractInsulatorPart] - isempty(cond) && error("component has no conductors") - isempty(insu) && error("component has no insulators") - - variants = Tuple{DataModel.CableComponent, DataModel.InsulatorGroup, ComponentTrace}[] - - # ---------------- first conductor choice spaces ---------------- - p1c = cond[1] - mats1 = _make_range(p1c.material) - dims1 = _make_range(p1c.dim[1]; pct = p1c.dim[2]) - args1s = collect(_expand_args(p1c.args)) # Vector{<:Tuple} - - # remaining conductors — spaces, with COUPLING flags to p1c - # Tuple layout: (pc, mcs_or_nothing, dcs_or_nothing, acs_or_nothing) - rest_cond_spaces = Tuple{PartSpec, Any, Union{Nothing, Any}, Union{Nothing, Any}}[] - for pc in cond[2:end] - same_mat = (pc.material == p1c.material) - same_dim = (pc.dim == p1c.dim) - same_args = (pc.args == p1c.args) - - mcs = same_mat ? nothing : _make_range(pc.material) - dcs = same_dim ? nothing : _make_range(pc.dim[1]; pct = pc.dim[2]) - acs = same_args ? nothing : collect(_expand_args(pc.args)) - - push!(rest_cond_spaces, (pc, mcs, dcs, acs)) - end - - # ---------------- first insulator choice spaces ---------------- - p1i = insu[1] - matsi = _make_range(p1i.material) - dimsi = _make_range(p1i.dim[1]; pct = p1i.dim[2]) - args1i = collect(_expand_args(p1i.args)) - - # remaining insulators — spaces, with COUPLING flags to p1i - rest_ins_spaces = Tuple{PartSpec, Any, Union{Nothing, Any}, Union{Nothing, Any}}[] - for pi in insu[2:end] - same_mat = (pi.material == p1i.material) - same_dim = (pi.dim == p1i.dim) - same_args = (pi.args == p1i.args) - - m2 = same_mat ? nothing : _make_range(pi.material) - d2 = same_dim ? nothing : _make_range(pi.dim[1]; pct = pi.dim[2]) - a2 = same_args ? nothing : collect(_expand_args(pi.args)) - - push!(rest_ins_spaces, (pi, m2, d2, a2)) - end - - # ---------------- selection stacks (resolved tuples) ------------- - chosen_c = Vector{NTuple{4, Any}}() - chosen_i = Vector{NTuple{4, Any}}() - - # ---------------- build with current resolved choices ------------ - function build_with_current_selection(mat1, d1, a1, mi, di, ai) - # 1) conductors - cg = _init_cg(p1c.part_type, base, d1, a1, mat1; abs_first = base == 0.0) - for k in 2:p1c.n_layers - _add_conductor!(cg, p1c.part_type, d1, a1, mat1; layer = k) - end - for (pc, mc, dc, ac) in chosen_c - for k in 1:pc.n_layers - _add_conductor!(cg, pc.part_type, dc, ac, mc; layer = k) - end - end - - # 2) insulators - ig = _init_ig(p1i.part_type, cg, di, ai, mi) - for (pi, m2i, d2i, a2i) in chosen_i - for k in 1:pi.n_layers - _add_insulator!(ig, pi.part_type, d2i, a2i, m2i) - end - end - - # assemble trace - choices = PartChoice[] - # first conductor spec - push!(choices, PartChoice(1, :conductor, p1c.part_type, d1, a1, mat1, p1c.n_layers)) - # remaining conductors - for (j, (pc, mc, dc, ac)) in enumerate(chosen_c) - push!( - choices, - PartChoice(1 + j, :conductor, pc.part_type, dc, ac, mc, pc.n_layers), - ) - end - # first insulator spec - push!( - choices, - PartChoice( - length(choices)+1, - :insulator, - p1i.part_type, - di, - ai, - mi, - p1i.n_layers, - ), - ) - # remaining insulators - for (pi, m2i, d2i, a2i) in chosen_i - push!( - choices, - PartChoice( - length(choices)+1, - :insulator, - pi.part_type, - d2i, - a2i, - m2i, - pi.n_layers, - ), - ) - end - ctrace = ComponentTrace(String(ps[1].component), choices) - - push!( - variants, - (DataModel.CableComponent(String(ps[1].component), cg, ig), ig, ctrace), - ) - - # push!(variants, (DataModel.CableComponent(String(ps[1].component), cg, ig), ig)) - end - - # ---------------- enumerate insulators with coupling ------------- - function choose_ins(idx::Int, mi, di, ai, mat1, d1, a1) - if idx > length(rest_ins_spaces) - build_with_current_selection(mat1, d1, a1, mi, di, ai) - return - end - pi, m2, d2, a2 = rest_ins_spaces[idx] - - Ms = (m2 === nothing) ? (mi,) : m2 - Ds = (d2 === nothing) ? (di,) : d2 - As = (a2 === nothing) ? (ai,) : a2 - - for m2i in Ms, d2i in Ds, a2i in As - push!(chosen_i, (pi, m2i, d2i, a2i)) - choose_ins(idx + 1, mi, di, ai, mat1, d1, a1) - pop!(chosen_i) - end - end - - # ---------------- enumerate conductors with coupling ------------- - function choose_cond(idx::Int, mat1, d1, a1, mi, di, ai) - if idx > length(rest_cond_spaces) - empty!(chosen_i) - choose_ins(1, mi, di, ai, mat1, d1, a1) - return - end - pc, mcs, dcs, acs = rest_cond_spaces[idx] - - Ms = (mcs === nothing) ? (mat1,) : mcs - Ds = (dcs === nothing) ? (d1,) : dcs - As = (acs === nothing) ? (a1,) : acs - - for mc in Ms, dc in Ds, ac in As - push!(chosen_c, (pc, mc, dc, ac)) - choose_cond(idx + 1, mat1, d1, a1, mi, di, ai) - pop!(chosen_c) - end - end - - # ---------------- top-level selection loops ---------------------- - for mat1 in mats1, d1 in dims1, a1 in args1s - for mi in matsi, di in dimsi, ai in args1i - empty!(chosen_c) - empty!(chosen_i) - choose_cond(1, mat1, d1, a1, mi, di, ai) - end - end - - return variants -end - - -function build(cbs::CableBuilderSpec; trace::Bool = false) - comp_names = unique(p.component for p in cbs.parts) - by_comp = Dict{Symbol, Vector{PartSpec}}() - for p in cbs.parts - get!(by_comp, p.component, PartSpec[]) |> v -> push!(v, p) - end - - # partials: (built_components, last_ig_or_nothing) - partials = Tuple{ - Vector{DataModel.CableComponent}, - Union{Nothing, DataModel.InsulatorGroup}, - Vector{ComponentTrace}, - }[(DataModel.CableComponent[], nothing, ComponentTrace[])] - - for cname in comp_names - ps = by_comp[cname] - new_partials = Tuple{ - Vector{DataModel.CableComponent}, - Union{Nothing, DataModel.InsulatorGroup}, - Vector{ComponentTrace}, - }[] - for (built, last_ig, tr) in partials - base = last_ig === nothing ? 0.0 : last_ig - for (comp, ig, ctrace) in _make_variants(ps, base) - push!(new_partials, (vcat(built, comp), ig, [tr...; ctrace])) - end - end - partials = new_partials - end - - - - if !trace - designs = DataModel.CableDesign[] - for (comps, _) in ((x[1], x[2]) for x in partials) - des = DataModel.CableDesign(cbs.cable_id, comps[1]; nominal_data = cbs.nominal) - for k in Iterators.drop(eachindex(comps), 1) - add!(des, comps[k]) - end - push!(designs, des) - end - return designs - else - designs = DataModel.CableDesign[] - traces = DesignTrace[] - for (comps, _, ctraces) in partials - des = DataModel.CableDesign(cbs.cable_id, comps[1]; nominal_data = cbs.nominal) - for k in 2:length(comps) - ; - add!(des, comps[k]); - end - push!(designs, des) - push!(traces, DesignTrace(cbs.cable_id, ctraces)) - end - return designs, traces - end -end - -""" - iterate(cbs) -> Channel{DataModel.CableDesign} - -Lazy stream of `CableDesign`s built from `CableBuilderSpec` without allocating all of them. -Works with `for d in iterate(cbs)`. -""" -function iterate(cbs::CableBuilderSpec) - # group by component - comp_names = unique(p.component for p in cbs.parts) - by_comp = Dict{Symbol, Vector{PartSpec}}() - for p in cbs.parts - get!(by_comp, p.component, PartSpec[]) |> v -> push!(v, p) - end - - return Channel{DataModel.CableDesign}(32) do ch - built = DataModel.CableComponent[] - lastig = Ref{Union{Nothing, DataModel.InsulatorGroup}}(nothing) - - function dfs(i::Int) - if i > length(comp_names) - des = DataModel.CableDesign( - cbs.cable_id, - built[1]; - nominal_data = cbs.nominal, - ) - for k in 2:length(built) - add!(des, built[k]) - end - put!(ch, des) - return - end - cname = comp_names[i] - ps = by_comp[cname] - base = (lastig[] === nothing) ? 0.0 : lastig[] - - for (comp, ig) in _make_variants(ps, base) - push!(built, comp) - prev = lastig[]; - lastig[] = ig - dfs(i + 1) - lastig[] = prev - pop!(built) - end - end - - dfs(1) - end -end - -module Conductor - -using ..ParametricBuilder: PartSpec, _spec -using ...DataModel: DataModel - -# wire: args are (n, lay) -Wires(component::Symbol; layers::Int, d, n::Int, lay = 11.0, m) = - PartSpec(component, DataModel.CircStrands, layers; - dim = _spec(d), args = (n, _spec(lay)), material = m) - -# tube: no extra args -Tubular(component::Symbol; layers::Int, t, m) = - PartSpec(component, DataModel.Tubular, layers; - dim = _spec(t), args = (), material = m) - -# strip: args are (width, lay) -Strip(component::Symbol; layers::Int, t, w, lay = 0.0, m) = - PartSpec(component, DataModel.Strip, layers; - dim = _spec(t), args = (_spec(w), _spec(lay)), material = m) - -# solid: inherits inner radius = 0.0, builds from diameter -Solid(component::Symbol; d, m) = - PartSpec(component, DataModel.Tubular, 1; - dim = _spec(d), args = (), material = m) - -# central + hex rings sugar -function Stranded(component::Symbol; layers::Int, d, n::Int, lay = 11.0, m) - @assert layers >= 1 "stranded: layers must be ≥ 1 (includes the central wire)." - specs = PartSpec[] - dspec = _spec(d) - - # 1) central wire: 1 layer, n=1, lay=0.0 - push!( - specs, - PartSpec(component, DataModel.CircStrands, 1; - dim = dspec, args = (1, (0.0, nothing)), material = m), - ) - - # 2) rings: (layers-1) layers, base n, common lay - if layers > 1 - push!( - specs, - PartSpec(component, DataModel.CircStrands, layers - 1; - dim = dspec, args = (n, _spec(lay)), material = m), - ) - end - - return specs -end - -end - -module Insulator - -using ..ParametricBuilder: PartSpec, _spec -using ...DataModel: DataModel - -Tubular(component::Symbol; layers::Int, t, m) = - PartSpec(component, DataModel.Insulator, layers; - dim = _spec(t), args = (), material = m) - -Semicon(component::Symbol; layers::Int, t, m) = - PartSpec(component, DataModel.Semicon, layers; - dim = _spec(t), args = (), material = m) -end diff --git a/src/parametricbuilder/comparisons.jl b/src/parametricbuilder/comparisons.jl new file mode 100644 index 000000000..e060ed833 --- /dev/null +++ b/src/parametricbuilder/comparisons.jl @@ -0,0 +1,35 @@ +""" +$(TYPEDSIGNATURES) + +Compare one reference with every result while retaining the problem and +formulation axes. The returned ParametricResult contains per-term RMSError +values in the same order as the input results. No formulation is evaluated. +""" +function compare(reference::Commons.AbstractCoreResult, result::ParametricResult, + quantity::Union{Function,Tuple}; kwargs...) + isempty(result.axes) && throw(ArgumentError("comparison requires retained problem/formulation axes")) + errors=[compare(reference, value, quantity::Union{Function,Tuple}; kwargs...) for value in result] + return ParametricResult(result.formulation, errors, result.axes, ComputationDetails()) +end + +""" +$(TYPEDSIGNATURES) + +Compare two retained result spaces with an explicit reference index for every +result point. `pairing` is a vector of `(reference_index, result_index)` +pairs. Each result must occur exactly once. Use the declared pairs even when the lengths are equal. +""" +function compare(reference::ParametricResult, result::ParametricResult, + quantity::Union{Function,Tuple}; pairing=nothing, kwargs...) + pairing === nothing && throw(ArgumentError("two result spaces require explicit pairing")) + length(pairing) == length(result) && + sort(last.(pairing)) == collect(eachindex(result.values)) || + throw(ArgumentError("pairing must select every result exactly once")) + all(pair -> pair isa Tuple{Integer,Integer} && + !(first(pair) isa Bool) && !(last(pair) isa Bool) && + 1 <= first(pair) <= length(reference), pairing) || + throw(ArgumentError("pairing contains an invalid reference/result index")) + ordered=sort(collect(pairing); by=last) + errors=[compare(reference[i], result[j], quantity::Union{Function,Tuple}; kwargs...) for (i,j) in ordered] + return ParametricResult(result.formulation, errors, result.axes, ComputationDetails()) +end diff --git a/src/parametricbuilder/construction_macros.jl b/src/parametricbuilder/construction_macros.jl new file mode 100644 index 000000000..22349663a --- /dev/null +++ b/src/parametricbuilder/construction_macros.jl @@ -0,0 +1,485 @@ +function _macro_block(value, name::Symbol) + value isa Expr && value.head === :block || throw(ArgumentError( + "@$name requires a begin/end block" + )) + parts = Any[] + for item in value.args + item isa LineNumberNode && continue + if item isa Expr && item.head === :macrocall && + first(item.args) in (GlobalRef(Core, Symbol("@doc")), Symbol("@doc")) + item = last(item.args) + end + item isa AbstractString || item === nothing || item === :nothing || + push!(parts, item) + end + return parts +end + +_macro_call(name::Symbol, args...) = Expr( + :call, GlobalRef(@__MODULE__, name), args... +) + +function _macro_keywords(values, name::Symbol) + keywords = Pair{Symbol, Any}[] + for value in values + value isa Expr && value.head === :(=) && value.args[1] isa Symbol || + throw(ArgumentError("@$name accepts keyword assignments")) + push!(keywords, value.args[1] => value.args[2]) + end + allunique(first.(keywords)) || throw(ArgumentError( + "@$name does not accept duplicate keyword assignments" + )) + return keywords +end + +function _call_with_keywords(name::Symbol, arguments, keywords) + parameters = Expr(:parameters, [Expr(:kw, key, value) for (key, value) in keywords]...) + return Expr(:call, GlobalRef(@__MODULE__, name), parameters, arguments...) +end + +""" + @cable identifier [nominal_data=value] [combine=:product] begin + parts... + end + +Build a completed cable from physical declarations ordered from the center +outward. + +# Arguments + +- `identifier`: stable cable identifier. +- `parts`: ordered physical cable declarations. + +# Keywords + +- `nominal_data=nothing`: descriptive catalog data stored with the design. +- `combine=:product`: gridspace composition rule. + +# Returns + +- A completed `CableDesign`, or a `Gridspace{CableDesign}` when a direct input + varies. +""" +macro cable(identifier, arguments...) + isempty(arguments) && throw(ArgumentError( + "@cable requires a begin/end block" + )) + block = last(arguments) + parts = _macro_block(block, :cable) + isempty(parts) && throw(ArgumentError("@cable requires at least one part")) + keywords = _macro_keywords(arguments[1:(end - 1)], :cable) + allowed = (:nominal_data, :combine) + unsupported = filter(pair -> !(first(pair) in allowed), keywords) + isempty(unsupported) || throw(ArgumentError( + "@cable accepts only nominal_data and combine" + )) + call = if isempty(keywords) + _macro_call( + :build, + GlobalRef(DataModel, :CableDesign), + identifier, + parts... + ) + else + _call_with_keywords( + :build, + (GlobalRef(DataModel, :CableDesign), identifier, parts...), + keywords + ) + end + return esc(call) +end + +""" + @system identifier [keyword=value ...] begin + placements... + end + +Build a completed line-cable system from placed cable declarations. + +# Arguments + +- `identifier`: stable system identifier. +- `placements`: ordered expressions that produce placed cable declarations. + +# Keywords + +- `environment=nothing`: optional physical environment declaration. +- `line_length=1`: physical line length \\[m\\]. +- `combine=:product`: gridspace composition rule. + +# Returns + +- A completed `LineCableSystem`, or a `Gridspace{LineCableSystem}` when a + placement or keyword value varies. +""" +macro system(identifier, arguments...) + isempty(arguments) && throw(ArgumentError( + "@system requires a begin/end block" + )) + block = last(arguments) + placements = _macro_block(block, :system) + isempty(placements) && throw(ArgumentError( + "@system requires at least one placed cable" + )) + keywords = _macro_keywords(arguments[1:(end - 1)], :system) + allowed = (:environment, :line_length, :combine) + unsupported = filter(pair -> !(first(pair) in allowed), keywords) + isempty(unsupported) || throw(ArgumentError( + "@system accepts only environment, line_length, and combine" + )) + pushfirst!(keywords, :system_id => identifier) + declarations = Expr(:tuple, placements...) + return esc(_call_with_keywords( + :build, + (GlobalRef(DataModel, :LineCableSystem), declarations), + keywords + )) +end + +""" + @earth [keyword=value ...] begin + layers... + end + +Build a completed earth model from ordered `layer(...)` declarations. For +horizontal interfaces, the block runs from the earth surface downward. +Semi-infinite air is implicit. + +# Arguments + +- `layers`: ordered expressions that produce completed earth layers. + +# Keywords + +- `vertical_layers=false`: whether earth interfaces are vertical. +- `air_layer=nothing`: optional explicit semi-infinite air layer. +- `combine=:product`: gridspace composition rule. + +# Returns + +- An `EarthModel`, or a `Gridspace{EarthModel}` when a layer or keyword value + varies. +""" +macro earth(arguments...) + isempty(arguments) && throw(ArgumentError( + "@earth requires a begin/end block" + )) + block = last(arguments) + layers = _macro_block(block, :earth) + isempty(layers) && throw(ArgumentError( + "@earth requires at least one earth layer" + )) + keywords = _macro_keywords(arguments[1:(end - 1)], :earth) + allowed = (:vertical_layers, :air_layer, :combine) + unsupported = filter(pair -> !(first(pair) in allowed), keywords) + isempty(unsupported) || throw(ArgumentError( + "@earth accepts only vertical_layers, air_layer, and combine" + )) + declarations = Expr(:tuple, layers...) + return esc(_call_with_keywords( + :build, + (GlobalRef(Earth, :EarthModel), declarations), + keywords + )) +end + +""" + @terminal name [combine=:product] begin + parts... + end + +Coalesce the conductive descendants in one ordered block as a terminal. + +# Arguments + +- `name`: retained electrical terminal name. +- `parts`: ordered physical cable declarations. + +# Keywords + +- `combine=:product`: gridspace composition rule. + +# Returns + +- A terminal-owning `Group`, or a `Gridspace{Group}` when a direct input + varies. +""" +macro terminal(name, arguments...) + isempty(arguments) && throw(ArgumentError( + "@terminal requires a begin/end block" + )) + block = last(arguments) + parts = _macro_block(block, :terminal) + isempty(parts) && throw(ArgumentError("@terminal requires at least one part")) + keywords = _macro_keywords(arguments[1:(end - 1)], :terminal) + all(pair -> first(pair) === :combine, keywords) || throw(ArgumentError( + "@terminal accepts only combine" + )) + call = if isempty(keywords) + _macro_call(:terminal, name, parts...) + else + _call_with_keywords(:terminal, (name, parts...), keywords) + end + return esc(call) +end + +""" + @assembly [combine=:product] begin + members... + end + +Assemble independent physical members while preserving their terminal +identities. + +# Arguments + +- `members`: independent physical declarations, optionally placed with `@at`. + +# Keywords + +- `combine=:product`: gridspace composition rule. + +# Returns + +- An `Assembly`, or a `Gridspace{Assembly}` when a direct input varies. +""" +macro assembly(arguments...) + isempty(arguments) && throw(ArgumentError( + "@assembly requires a begin/end block" + )) + block = last(arguments) + members = _macro_block(block, :assembly) + isempty(members) && throw(ArgumentError("@assembly requires at least one member")) + keywords = _macro_keywords(arguments[1:(end - 1)], :assembly) + all(pair -> first(pair) === :combine, keywords) || throw(ArgumentError( + "@assembly accepts only combine" + )) + call = if isempty(keywords) + _macro_call(:assembly, members...) + else + _call_with_keywords(:assembly, members, keywords) + end + return esc(call) +end + +function _enclosure_macro(name::Symbol, arguments) + isempty(arguments) && throw(ArgumentError( + "@$name requires keywords and a begin/end block" + )) + block = last(arguments) + members = _macro_block(block, name) + isempty(members) && throw(ArgumentError("@$name requires enclosed content")) + keywords = _macro_keywords(arguments[1:(end - 1)], name) + keys = first.(keywords) + :shape in keys || throw(ArgumentError("@$name requires shape")) + :fill in keys || throw(ArgumentError("@$name requires fill")) + allowed = name === :pipe ? (:shape, :fill, :wall, :at, :combine) : + (:shape, :fill, :wall, :formation, :at, :combine) + unsupported = filter(pair -> !(first(pair) in allowed), keywords) + isempty(unsupported) || throw(ArgumentError( + "@$name accepts only $(join(allowed, ", "))" + )) + return _call_with_keywords(name, members, keywords) +end + +""" + @pipe shape=value fill=value [keyword=value ...] begin + members... + end + +Contain one or more physical members inside a pipe cross-section. + +# Arguments + +- `members`: enclosed physical declarations. + +# Keywords + +- `shape`: intrinsic containing primitive. +- `fill`: filling material or explicit filling region. +- `wall=nothing`: optional outward wall declaration. +- `at=nothing`: pipe pose relative to its parent frame. +- `combine=:product`: gridspace composition rule. + +# Returns + +- An `Enclosure`, or a `Gridspace{Enclosure}` when a direct input varies. +""" +macro pipe(arguments...) + return esc(_enclosure_macro(:pipe, arguments)) +end + +""" + @duct shape=value fill=value [keyword=value ...] begin + members... + end + +Contain one or more physical members inside a duct cross-section. + +# Arguments + +- `members`: enclosed physical declarations. + +# Keywords + +- `shape`: intrinsic containing primitive. +- `fill`: filling material or explicit filling region. +- `wall=nothing`: optional outward wall declaration. +- `formation=nothing`: placement pattern for one repeated prototype. +- `at=nothing`: duct pose relative to its parent frame. +- `combine=:product`: gridspace composition rule. + +# Returns + +- An `Enclosure`, or a `Gridspace{Enclosure}` when a direct input varies. +""" +macro duct(arguments...) + return esc(_enclosure_macro(:duct, arguments)) +end + +function _coordinate_tuple(value, name::Symbol) + value isa Expr && value.head === :tuple && length(value.args) in (2, 3) || + throw(ArgumentError("@$name requires a two- or three-coordinate tuple")) + return copy(value.args) +end + +""" +Place a physical subject using coordinate-tuple notation. +""" +macro at(subject, coordinates, assignments...) + values = _coordinate_tuple(coordinates, :at) + keywords = _macro_keywords(assignments, :at) + reserved = (:φ, :connections, :combine) + explicit_connections = findall(pair -> first(pair) === :connections, keywords) + shorthand = filter(pair -> !(first(pair) in reserved), keywords) + isempty(explicit_connections) || isempty(shorthand) || + throw(ArgumentError( + "@at cannot mix connections with connection shorthand" + )) + if isempty(explicit_connections) && !isempty(shorthand) + named = Expr(:tuple, Expr( + :parameters, + [Expr(:kw, key, value) for (key, value) in shorthand]... + )) + keywords = filter(pair -> first(pair) in reserved, keywords) + push!(keywords, :connections => named) + end + keys = first.(keywords) + tuple_has_angle = length(values) == 3 + tuple_has_angle && :φ in keys && + throw(ArgumentError( + "@at cannot receive φ both in the coordinate tuple and as a keyword" + )) + angle = tuple_has_angle ? pop!(values) : nothing + angle === nothing || pushfirst!(keywords, :φ => angle) + return esc(_call_with_keywords(:at, (subject, values...), keywords)) +end + +function _formation_macro(name::Symbol, design, assignments) + keywords = _macro_keywords(assignments, name) + reserved = name === :trefoil ? + (:spacing, :center, :φ0, :connections, :combine) : + (:spacing, :center, :connections, :combine) + explicit_connections = findall(pair -> first(pair) === :connections, keywords) + length(explicit_connections) <= 1 || throw(ArgumentError( + "@$name accepts one connections assignment" + )) + shorthand = filter(pair -> !(first(pair) in reserved), keywords) + isempty(explicit_connections) || isempty(shorthand) || + throw(ArgumentError( + "@$name cannot mix connections with connection shorthand" + )) + if isempty(explicit_connections) && !isempty(shorthand) + named = Expr(:tuple, Expr( + :parameters, + [Expr(:kw, key, value) for (key, value) in shorthand]... + )) + keywords = filter(pair -> first(pair) in reserved, keywords) + push!(keywords, :connections => named) + end + for index in eachindex(keywords) + key, value = keywords[index] + if key === :center && value isa Expr && value.head === :tuple + coordinates = _coordinate_tuple(value, name) + center = length(coordinates) == 2 ? + _macro_call(:at, coordinates...) : + _call_with_keywords( + :at, + coordinates[1:2], + [:φ => coordinates[3]] + ) + keywords[index] = key => center + end + end + return _call_with_keywords(name, (design,), keywords) +end + +""" + @trefoil design spacing=value [keyword=value ...] + +Place three copies of `design` in a trefoil formation. `center`, `φ0`, +`connections`, and `combine` are forwarded to [`trefoil`](@ref). Every other +keyword is terminal-connection shorthand. +""" +macro trefoil(design, assignments...) + return esc(_formation_macro(:trefoil, design, assignments)) +end + +""" + @hflat design spacing=value [keyword=value ...] + +Place three copies of `design` in a horizontal flat formation. `center`, +`connections`, and `combine` are forwarded to [`hflat`](@ref). Every other +keyword is terminal-connection shorthand. +""" +macro hflat(design, assignments...) + return esc(_formation_macro(:hflat, design, assignments)) +end + +""" + @vflat design spacing=value [keyword=value ...] + +Place three copies of `design` in a vertical flat formation. `center`, +`connections`, and `combine` are forwarded to [`vflat`](@ref). Every other +keyword is terminal-connection shorthand. +""" +macro vflat(design, assignments...) + return esc(_formation_macro(:vflat, design, assignments)) +end + +""" +Insert the deferred `n = capacity()` count into a repeated-member call. +""" +macro distribute(call) + call isa Expr && call.head === :call || throw(ArgumentError( + "@distribute requires a constructor call" + )) + rewritten = copy(call) + rewritten.args = copy(call.args) + callee = first(rewritten.args) + callee isa Symbol && callee in (:wires, :rope, :tape) || throw( + ArgumentError("@distribute supports wires, rope, and tape") + ) + parameters = findfirst( + argument -> argument isa Expr && argument.head === :parameters, + rewritten.args + ) + if parameters === nothing + insert!(rewritten.args, 2, Expr(:parameters, Expr( + :kw, :n, _macro_call(:capacity) + ))) + else + original_parameters = rewritten.args[parameters] + rewritten.args[parameters] = copy(original_parameters) + kwargs = copy(original_parameters.args) + rewritten.args[parameters].args = kwargs + any( + keyword -> keyword isa Expr && keyword.head === :kw && + keyword.args[1] === :n, kwargs) && throw(ArgumentError( + "@distribute cannot overwrite an explicit n" + )) + push!(kwargs, Expr(:kw, :n, _macro_call(:capacity))) + end + return esc(rewritten) +end diff --git a/src/parametricbuilder/determinize.jl b/src/parametricbuilder/determinize.jl deleted file mode 100644 index da2b55c83..000000000 --- a/src/parametricbuilder/determinize.jl +++ /dev/null @@ -1,150 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Deterministic collapse (Monte Carlo on deterministic ranges only) -# Policy: transform (valuespec, pctspec) → (merged_valuespec, nothing) -# ───────────────────────────────────────────────────────────────────────────── - -# Percent helpers -@inline _pct(u) = float(u) / 100 -@inline _expand_nom(nom::Number, u::Number) = (nom*(1 - _pct(u)), nom*(1 + _pct(u))) -@inline _expand_bounds(lo::Number, hi::Number, u1::Number, u2::Number) = - (lo*(1 - _pct(u1)), hi*(1 + _pct(u2))) - -# Deterministic collapse with pct interpreted as percent (not absolute) -@inline function _det_pair(spec, pct) - pct === nothing && return (spec, nothing) - - # helper: largest percent magnitude in the tuple - _umax(u1, u2) = max(abs(float(u1)), abs(float(u2))) - - # A) spec = (lo,hi,N1), pct = (u1,u2,N2) - if (spec isa Tuple && length(spec)==3 && all(x->x isa Number, spec)) && - (pct isa Tuple && length(pct) == 3 && all(x->x isa Number, pct)) - lo, hi, N1 = float(spec[1]), float(spec[2]), Int(spec[3]) - u1, u2, N2 = float(pct[1]), float(pct[2]), Int(pct[3]) - u = _umax(u1, u2) - lo_det = lo * (1 - _pct(u)) - hi_det = hi * (1 + _pct(u)) - return ((lo_det, hi_det, N1 * N2), nothing) - end - - # B) spec = (lo,hi,N1), pct = u - if (spec isa Tuple && length(spec)==3 && all(x->x isa Number, spec)) && (pct isa Number) - lo, hi, N1 = float(spec[1]), float(spec[2]), Int(spec[3]) - u = abs(float(pct)) - lo_det = lo * (1 - _pct(u)) - hi_det = hi * (1 + _pct(u)) - return ((lo_det, hi_det, N1), nothing) - end - - # C) spec = nom, pct = (u1,u2,N2) - if (spec isa Number) && (pct isa Tuple && length(pct)==3 && all(x->x isa Number, pct)) - nom = float(spec) - u1, u2, N2 = float(pct[1]), float(pct[2]), Int(pct[3]) - u = _umax(u1, u2) - lo_det = nom * (1 - _pct(u)) - hi_det = nom * (1 + _pct(u)) - return ((lo_det, hi_det, max(N2, 2)), nothing) - end - - # D) spec = nom, pct = u - if (spec isa Number) && (pct isa Number) - nom = float(spec); - u = abs(float(pct)) - lo_det = nom * (1 - _pct(u)) - hi_det = nom * (1 + _pct(u)) - return ((lo_det, hi_det, 2), nothing) - end - - # E) fallback - return (spec, nothing) -end - -# Normalizer: accept a field already in (spec,pct) or as a scalar → return (spec’, nothing) -@inline _det_field(x) = (x isa Tuple && length(x)==2) ? _det_pair(x[1], x[2]) : (x, nothing) - -# ---- MaterialSpec ---- -function determinize(ms::MaterialSpec) - MaterialSpec( - rho = _det_field(ms.rho), - eps_r = _det_field(ms.eps_r), - mu_r = _det_field(ms.mu_r), - T0 = _det_field(ms.T0), - alpha = _det_field(ms.alpha), - ) -end - -# ---- PartSpec (dim, args, material) ---- -function determinize(ps::PartSpec) - dim_det = _det_field(ps.dim) - # each arg can be scalar or (spec,pct) - args_det = map(a -> (a isa Tuple && length(a)==2) ? _det_field(a) : a, ps.args) |> Tuple - mat_det = determinize(ps.material) - return PartSpec( - ps.component, - ps.part_type, - ps.n_layers; - dim = dim_det, - args = args_det, - material = mat_det, - ) -end - -# ---- CableBuilderSpec (vector/nested parts) ---- -function determinize(cbs::CableBuilderSpec) - parts_det = PartSpec[determinize(p) for p in cbs.parts] - return CableBuilderSpec(cbs.cable_id, parts_det, cbs.nominal) -end - -# ───────────────────────────────────────────────────────────────────────────── -# Deterministic collapse for SystemBuilderSpec (non-materializing) -# ───────────────────────────────────────────────────────────────────────────── - -@inline _det_axis(a) = (a isa Tuple && length(a)==2) ? _det_pair(a[1], a[2]) : a -# determinize EarthSpec -function determinize(e::EarthSpec) - EarthSpec( - rho = _det_field(e.rho), - eps_r = _det_field(e.eps_r), - mu_r = _det_field(e.mu_r), - t = _det_field(e.t), - ) -end - -# determinize PositionSpec (keep anchors; just collapse dx/dy specs) -function determinize(p::PositionSpec) - dx_det = _det_axis(p.dx) - dy_det = _det_axis(p.dy) - return PositionSpec( - p.x0, - p.y0, - dx_det, - dy_det, - p.conn, - ) -end - -# determinize PositionGroupSpec: collapse (valuespec,pctspec) for spacing, -# keep the rest as-is; still materialized lazily later. -function determinize(p::PositionGroupSpec) - dspec_det = _det_field(p.d) - return PositionGroupSpec( - p.arrangement, - p.n, - p.anchor, - dspec_det, - p.conn, - ) -end - -# determinize SystemBuilderSpec -function determinize(s::SystemBuilderSpec) - SystemBuilderSpec( - s.system_id, - determinize(s.builder), - [determinize(p) for p in s.positions]; - length = _det_field(s.length), - temperature = _det_field(s.temperature), - earth = determinize(s.earth), - f = s.frequencies, - ) -end \ No newline at end of file diff --git a/src/parametricbuilder/engine/cableconstants.jl b/src/parametricbuilder/engine/cableconstants.jl new file mode 100644 index 000000000..f0b525365 --- /dev/null +++ b/src/parametricbuilder/engine/cableconstants.jl @@ -0,0 +1,43 @@ +""" +$(TYPEDSIGNATURES) + +Map a finite space of completed cable designs to cable constants. + +Each deterministic configuration or stochastic realization calls the scalar +`CableConstants(design; kwargs...)` call exactly once. +""" +function Engine.CableConstants( + designs::Gridspace{<:DataModel.CableDesign}; + kwargs... +) + caller = design -> Engine.CableConstants(design; kwargs...) + return Gridspace{Engine.CableConstants}(caller, (designs,)) +end + +""" +$(TYPEDSIGNATURES) + +Map a finite space of completed cable designs to earth-free cable-constant +problems. +""" +function Engine.CableConstantsProblem( + designs::Gridspace{<:DataModel.CableDesign}; + temperature = 20, + frequency = 50, + combine::Symbol = :product +) + sources = ( + designs, + temperature isa Union{AbstractGrid, Gridspace} ? temperature : Grid((temperature,)), + frequency isa Union{AbstractGrid, Gridspace} ? frequency : Grid((frequency,)) + ) + return Gridspace{Engine.CableConstantsProblem}( + _cable_constants_problem, + sources; + combine + ) +end + +function _cable_constants_problem(design, temperature, frequency) + return Engine.CableConstantsProblem(design; temperature, frequency) +end diff --git a/src/parametricbuilder/geometry.jl b/src/parametricbuilder/geometry.jl new file mode 100644 index 000000000..2136e1af3 --- /dev/null +++ b/src/parametricbuilder/geometry.jl @@ -0,0 +1,104 @@ +""" +Construct one physical pose, or a finite space of poses. +""" +function Pose2(; + x = 0, + y = 0, + φ = 0, + combine::Symbol = :product +) + return parameterize(DataModel.Pose2, DataModel.Pose2, (x, y, φ); combine) +end + +""" +Declare a circular primitive, or a finite space of circular primitives. +""" +function Disk(r; combine::Symbol = :product) + return parameterize( + DataModel.Disk, + DataModel.Disk, + (r,); + combine + ) +end + +""" +Declare a rectangular primitive, or a finite space of rectangular primitives. +""" +function Rectangle(w, h; combine::Symbol = :product) + return parameterize( + DataModel.Rectangle, + DataModel.Rectangle, + (w, h); + combine + ) +end + +""" +Declare an elliptical primitive, or a finite space of elliptical primitives. +""" +function Ellipse(a, b; combine::Symbol = :product) + return parameterize( + DataModel.Ellipse, + DataModel.Ellipse, + (a, b); + combine + ) +end + +""" +Declare a filleted cable-sector primitive, or a finite space of sectors. +""" +function Sector(; + span, + r_base, + r_back, + fillet = 0, + combine::Symbol = :product +) + return parameterize( + DataModel.Sector, + DataModel.Sector, + (span, r_base, r_back, fillet); + combine + ) +end + +""" +Declare an annular primitive, or a finite space of annular primitives. +""" +function Annulus(ri, ro; combine::Symbol = :product) + return parameterize( + DataModel.Annulus, + DataModel.Annulus, + (ri, ro); + combine + ) +end + +""" +Declare a contextual shell, or a finite space of contextual shells. +""" +function Shell(t; combine::Symbol = :product) + return parameterize( + DataModel.Shell, + DataModel.Shell, + (t,); + combine + ) +end + +""" +Declare a polygon primitive, or a finite space of polygon primitives. +""" +function Polygon( + points::Union{AbstractGrid, Gridspace}; + combine::Symbol = :product +) + return parameterize( + DataModel.Polygon, + DataModel.Polygon, + (points,); + combine + ) +end diff --git a/src/parametricbuilder/groupspec.jl b/src/parametricbuilder/groupspec.jl deleted file mode 100644 index 0b72d62db..000000000 --- a/src/parametricbuilder/groupspec.jl +++ /dev/null @@ -1,214 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Grouped formations: PositionGroupSpec -# -# These are *lazy* group specs. They do NOT carry concrete coordinates; they -# carry: -# - an arrangement symbol (:trifoil, :hflat, :vflat, …) -# - the number of legs n -# - an anchor (x0,y0) -# - a spacing spec (values,pct) via the same grammar as everything else -# - per-leg connection maps (Dict{String,Int}) -# -# They are materialized to concrete (x,y,conn) tuples *after* the CableDesign is -# known, so we can enforce a min spacing of 2 * outer_radius. -# ───────────────────────────────────────────────────────────────────────────── -struct PositionGroupSpec <: AbstractPositionSpec - arrangement::Symbol # :trifoil, :hflat, :vflat, … - n::Int # number of cables in the group - anchor::Tuple{Float64, Float64} # (x0,y0) - d::Tuple{Any, Any} # (valuespec, pctspec) - conn::Vector{Dict{String, Int}} # per-leg connection maps -end - -# ───────────────────────────────────────────────────────────────────────────── -# Public sugar constructors -# ───────────────────────────────────────────────────────────────────────────── - -""" - trifoil(; x0 = 0.0, y0, d, phases) - -Lazily describes a 3-cable trifoil formation. The anchor `(x0,y0)` is passed to -`trifoil_formation(x0,y0,d)` when the group is materialized. - -The spacing `d` follows the usual `(valuespec, pctspec)` grammar; it will be -expanded lazily and clamped at runtime to avoid overlaps. -""" -function trifoil(; x0::Real = 0.0, y0::Real, d, phases) - conn = make_phase_maps(phases, 3) - return PositionGroupSpec( - :trifoil, - 3, - (float(x0), float(y0)), - _spec(d), - conn, - ) -end - -""" - hflat(; x0 = 0.0, y0 = 0.0, d, n = 3, phases) - -Horizontal flat formation: first cable at `(x0, y0)`, remaining `n-1` cables at -`(x0 + k*d, y0)` for `k = 1, …, n-1`. - -`d` accepts the `(valuespec, pctspec)` grammar. -""" -function hflat(; x0::Real = 0.0, y0::Real = 0.0, d, n::Integer = 3, phases) - n < 1 && error("hflat requires n ≥ 1") - conn = make_phase_maps(phases, n) - return PositionGroupSpec( - :hflat, - n, - (float(x0), float(y0)), - _spec(d), - conn, - ) -end - -""" - vflat(; x0 = 0.0, y0 = 0.0, d, n = 3, phases) - -Vertical flat formation: first cable at `(x0, y0)`, remaining `n-1` cables at -`(x0, y0 - k*d)` for `k = 1, …, n-1`. - -`d` accepts the `(valuespec, pctspec)` grammar. -""" -function vflat(; x0::Real = 0.0, y0::Real = 0.0, d, n::Integer = 3, phases) - n < 1 && error("vflat requires n ≥ 1") - conn = make_phase_maps(phases, n) - return PositionGroupSpec( - :vflat, - n, - (float(x0), float(y0)), - _spec(d), - conn, - ) -end - -# ───────────────────────────────────────────────────────────────────────────── -# Group materialization (PositionGroupSpec) -# ───────────────────────────────────────────────────────────────────────────── - -# Expand spacing spec and clamp out overlapping choices, based on radius -function _get_valid_spacings(g::PositionGroupSpec, rout) - min_spacing = to_nominal(rout) + eps() # tiny epsilon to avoid overlap issues - - # Full grid of values × pct → Measurement or plain Real - raw = collect(_make_range(g.d[1]; pct = g.d[2])) - - # Nothing at all? auto-min with same pct grammar. - if isempty(raw) - return collect(_make_range(min_spacing; pct = g.d[2])) - end - - valid = Any[] - discarded = 0 - - # Filter by geometry, but KEEP the original object (Measurement or Real) - for s in raw - ds = to_nominal(s) - if ds >= min_spacing - push!(valid, s) # don't strip uncertainty - else - discarded += 1 - end - end - - # CASE 1: all invalid → pure AUTO: min_spacing with all pcts - if isempty(valid) - @debug "Spacing spec produced only overlapping layouts; clamping to minimum with % uncertainty grid." min_spacing=min_spacing - return collect(_make_range(min_spacing; pct = g.d[2])) - end - - # CASE 2: some valid, some discarded → inject ONE batch at min_spacing, - # but only for spacing+uncertainty combos that are not already present. - if discarded > 0 - @debug "Dropped $discarded spacing samples below minimum center-to-center distance; including one batch at the minimum feasible spacing." min_spacing=min_spacing - - autos_all = collect(_make_range(min_spacing; pct = g.d[2])) - - # Use a Set to avoid injecting exact duplicates (same Measurement). - valid_set = Set(valid) - autos = Any[] - for a in autos_all - if !(a in valid_set) - push!(autos, a) - end - end - - # Prepend autos so min_spacing layouts come first, but WITHOUT - # multiplying cardinality by cloning identical points. - valid = vcat(autos, valid) - end - - return valid -end - - -""" - _materialize(g::PositionGroupSpec, des::CableDesign) - -Lazily expands a grouped formation into concrete `(x, y, conn)` blocks after the -external radius is known (`des` is the fully materialized design). - -Returns a generator of `Vector{Tuple{Float64,Float64,Dict{String,Int}}}`, one -vector per valid spacing choice. -""" -function _materialize(g::PositionGroupSpec, des::CableDesign) - r = get_outer_radius(des) - spacings = _get_valid_spacings(g, r) - - # Build one concrete layout (vector of (x,y,conn)) for a given spacing d - function _make_layout(g::PositionGroupSpec, d::Real) - x0, y0 = g.anchor - - coords = - g.arrangement == :trifoil ? - begin - g.n == 3 || error("trifoil formation expects n = 3, got $(g.n)") - x0p, y0p, dp = promote(x0, y0, d) - xa, ya, xb, yb, xc, yc = DataModel.trifoil_formation(x0p, y0p, dp) - [(xa, ya), (xb, yb), (xc, yc)] - end : - g.arrangement == :hflat ? begin - [(x0 + d * (i - 1), y0) for i in 1:g.n] - end : - g.arrangement == :vflat ? begin - [(x0, y0 - d * (i - 1)) for i in 1:g.n] - end : - error("Unknown position group arrangement $(g.arrangement)") - - return [(x, y, g.conn[i]) for (i, (x, y)) in enumerate(coords)] - end - - return (_make_layout(g, d) for d in spacings) -end - - -""" - _expand_position(position_defs, des) - -Top-level helper that yields flattened `Vector{(x,y,conn)}` for every allowed -combination of positions/groups. -""" -function _expand_position(position_defs::Vector{AbstractPositionSpec}, des::CableDesign) - spaces = Vector{Any}(undef, length(position_defs)) - - for (i, p) in pairs(position_defs) - if p isa PositionGroupSpec - # group: generator of Vector{(x,y,conn)} - spaces[i] = _materialize(p, des) - elseif p isa PositionSpec - # single: wrap each (x,y,conn) into a 1-element vector so the - # outer logic can always `vcat` vectors. - spaces[i] = ( - [(x, y, p.conn)] - for x in _axis(p.x0, p.dx), - y in _axis(p.y0, p.dy) - ) - else - error("Unsupported position spec type: $(typeof(p))") - end - end - - return (reduce(vcat, combo) for combo in product(spaces...)) -end diff --git a/src/parametricbuilder/macros.jl b/src/parametricbuilder/macros.jl new file mode 100644 index 000000000..362a4643e --- /dev/null +++ b/src/parametricbuilder/macros.jl @@ -0,0 +1,149 @@ +# These AST helpers are core composition machinery for `@gridspace`. They +# operate on exactly one struct and reject ambiguous inputs. +function _strip_escapes(expression) + if expression isa Expr && expression.head === :escape + return _strip_escapes(expression.args[1]) + elseif expression isa Expr + return Expr(expression.head, map(_strip_escapes, expression.args)...) + end + return expression +end + +function _struct_nodes(expression, nodes = Expr[]) + expression isa Expr || return nodes + expression.head === :struct && push!(nodes, expression) + for argument in expression.args + _struct_nodes(argument, nodes) + end + return nodes +end + +function _get_struct_node(expression) + nodes = _struct_nodes(expression) + length(nodes) == 1 || throw(ArgumentError( + "macro input must contain exactly one struct definition; found $(length(nodes))", + )) + return only(nodes) +end + +function _replace_struct(expression, old_struct, new_struct) + expression === old_struct && return new_struct + expression isa Expr || return expression + return Expr( + expression.head, + map( + argument -> _replace_struct(argument, old_struct, new_struct), + expression.args + )... + ) +end + +function _parse_fields(struct_body) + fields = NamedTuple[] + clean_body = Any[] + for argument in struct_body.args + if argument isa LineNumberNode || argument isa String + push!(clean_body, argument) + continue + end + has_default = argument isa Expr && argument.head === :(=) + field_expression = has_default ? argument.args[1] : argument + default = has_default ? argument.args[2] : nothing + if field_expression isa Symbol + name = field_expression + declared_type = nothing + elseif field_expression isa Expr && field_expression.head === :(::) + name = field_expression.args[1] + declared_type = field_expression.args[2] + else + push!(clean_body, argument) + continue + end + push!(clean_body, field_expression) + push!(fields, (; name, declared_type, has_default, default)) + end + isempty(fields) && + throw(ArgumentError("macro struct must declare at least one field")) + return Tuple(fields), clean_body +end + +function _extract_struct_name(struct_node) + signature = struct_node.args[2] + signature isa Expr && signature.head === :(<:) && + (signature = signature.args[1]) + signature isa Symbol && return signature + signature isa Expr && signature.head === :curly && + return signature.args[1] + throw(ArgumentError("malformed struct signature")) +end + +function _rebuild_ast(expression, old_struct, new_struct, generated) + replaced = _replace_struct(expression, old_struct, new_struct) + return Expr(:block, replaced, generated...) +end + +""" +$(SIGNATURES) + +Retain the strict positional struct and add a keyword constructor that lifts +only explicit Grid or Gridspace fields into a `Gridspace`. With `Target`, the +generated space materializes that target instead of the declared struct itself. +""" +macro gridspace(arguments...) + length(arguments) in (1, 2) || + throw(ArgumentError("@gridspace accepts a struct and optional target")) + target_expression, + expression = length(arguments) == 1 ? (nothing, arguments[1]) : + arguments + raw = _strip_escapes(expression) + struct_node = _get_struct_node(raw) + struct_name = _extract_struct_name(struct_node) + fields, clean_body = _parse_fields(struct_node.args[3]) + target = target_expression === nothing ? struct_name : target_expression + + clean_struct = Expr( + :struct, + struct_node.args[1], + struct_node.args[2], + Expr(:block, clean_body...) + ) + keywords = Any[] + for field in fields + push!( + keywords, + field.has_default ? + Expr(:kw, field.name, field.default) : field.name + ) + end + push!(keywords, Expr(:kw, :combine, QuoteNode(:product))) + signature = Expr(:call, struct_name, Expr(:parameters, keywords...)) + values = Expr(:tuple, map(field -> field.name, fields)...) + constructor = Expr(:function, + signature, + quote + combine in (:product, :zip) || + throw(ArgumentError("combine must be :product or :zip")) + values = $values + if any( + value -> value isa Union{ + $(GlobalRef(@__MODULE__, :AbstractGrid)), + $(GlobalRef(@__MODULE__, :Gridspace)) + }, + values + ) + grids = map(values) do value + value isa Union{ + $(GlobalRef(@__MODULE__, :AbstractGrid)), + $(GlobalRef(@__MODULE__, :Gridspace)) + } ? value : $(GlobalRef(@__MODULE__, :Grid))((value,)) + end + return $(GlobalRef(@__MODULE__, :Gridspace)){$target}( + $target, + grids; + combine + ) + end + return $target(values...) + end) + return esc(_rebuild_ast(raw, struct_node, clean_struct, (constructor,))) +end diff --git a/src/parametricbuilder/material.jl b/src/parametricbuilder/material.jl new file mode 100644 index 000000000..47f0b2aae --- /dev/null +++ b/src/parametricbuilder/material.jl @@ -0,0 +1,111 @@ +""" +$(TYPEDSIGNATURES) + +Construct electromagnetic and thermal material properties. Scalar property +inputs return a [`Material`](@ref) directly. An explicit [`Grid`](@ref) or +nested [`Gridspace`](@ref) lifts the construction to a finite space. + +# Keywords + +- `rho`: electrical resistivity \\[Ω·m\\]. +- `kind`: broad physical class. Deterministic symbol grids are accepted. +- `eps_r=1`: relative permittivity \\[dimensionless\\]. +- `mu_r=1`: relative permeability \\[dimensionless\\]. +- `T0=20`: reference temperature \\[°C\\]. +- `alpha=0`: temperature coefficient of resistivity \\[1/°C\\]. +- `rho_thermal=0`: thermal resistivity \\[K·m/W\\]. +- `theta_max=90`: maximum continuous operating temperature \\[°C\\]. +- `tan_delta=0`: dielectric loss tangent. +- `sigma_solar=0`: solar-absorption coefficient. +- `combine=:product`: local composition rule when an input varies. + +# Returns + +- A [`Material`](@ref) for scalar inputs, or a `Gridspace{Material}` when at + least one direct input is a `Grid` or nested `Gridspace`. +""" +function Material(; + kind, + rho, + eps_r = 1, + mu_r = 1, + T0 = 20, + alpha = 0, + rho_thermal = 0, + theta_max = 90, + tan_delta = 0, + sigma_solar = 0, + combine::Symbol = :product +) + values = ( + kind, rho, eps_r, mu_r, T0, alpha, rho_thermal, + theta_max, tan_delta, sigma_solar + ) + return parameterize( + Materials.Material, Materials.Material, values; combine + ) +end + +function Material( + material::Materials.Material; + kind = material.kind, + rho = material.rho, + eps_r = material.eps_r, + mu_r = material.mu_r, + T0 = material.T0, + alpha = material.alpha, + rho_thermal = material.rho_thermal, + theta_max = material.theta_max, + tan_delta = material.tan_delta, + sigma_solar = material.sigma_solar, + combine::Symbol = :product +) + return Material(; + kind, rho, eps_r, mu_r, T0, alpha, rho_thermal, + theta_max, tan_delta, sigma_solar, combine + ) +end + +function Material( + library::Materials.MaterialsLibrary, + name::Union{AbstractString, Symbol}; + kwargs... +) + material = get(library, String(name), nothing) + material === nothing && throw(KeyError(String(name))) + return Material(material; kwargs...) +end + +function Material(unexpected; kwargs...) + throw(ArgumentError( + "keyword Material construction accepts no positional arguments; " * + "got $(repr(unexpected)). To declare uncertainty, keep the error " * + "inside Grid: `property = Grid(values, relative_error)` or " * + "`property = Grid(values, AbsoluteError(error))`.", + )) +end + +""" +$(TYPEDSIGNATURES) + +Add the one deterministic material represented by `space` to a material +library. + +# Errors + +- Throws `ArgumentError` when `space` contains uncertainty or describes more + than one material. +""" +function add!( + library::Materials.MaterialsLibrary, + name::Union{AbstractString, Symbol}, + space::Gridspace{Materials.Material} +) + has_uncertainty(space) && throw(ArgumentError( + "a reusable material-library entry must be deterministic", + )) + length(space) == 1 || throw(ArgumentError( + "a reusable material-library entry must describe exactly one material; got $(length(space)) points", + )) + return add!(library, String(name), only(space)) +end diff --git a/src/parametricbuilder/materialspec.jl b/src/parametricbuilder/materialspec.jl deleted file mode 100644 index f18ab2a0b..000000000 --- a/src/parametricbuilder/materialspec.jl +++ /dev/null @@ -1,70 +0,0 @@ -# Use lib/material nominal; kw is either percent-only or (value,pct) -_pair_from_nominal(nom, x) = - x === nothing ? (nom, nothing) : - (x isa Tuple && length(x)==2) ? x : - (nom, x) - -# -------------------- material spec -------------------- - -""" -MaterialSpec: pass specs for fields (value spec + optional %unc) - -Example: - MaterialSpec(; rho=(2.826e-8, nothing), - eps_r=(1.0, nothing), - mu_r=(1.0, nothing), - T0=(20.0, nothing), - alpha=(4.0e-3, nothing)) -""" -struct MaterialSpec - rho::Any; - eps_r::Any; - mu_r::Any; - T0::Any; - alpha::Any -end -MaterialSpec(; rho, eps_r, mu_r, T0, alpha) = MaterialSpec(rho, eps_r, mu_r, T0, alpha) - -# --- 1) Ad-hoc numeric: values (or (value,pct)) --- -Material(; rho, eps_r = 1.0, mu_r = 1.0, T0 = 20.0, alpha = 0.0) = - MaterialSpec( - rho = _spec(rho), - eps_r = _spec(eps_r), - mu_r = _spec(mu_r), - T0 = _spec(T0), - alpha = _spec(alpha), - ) - -# --- 2) From an existing Material: append %unc by default, or override with (value,pct) --- -function Material( - m::Materials.Material; - rho = nothing, - eps_r = nothing, - mu_r = nothing, - T0 = nothing, - alpha = nothing, -) - MaterialSpec( - rho = _pair_from_nominal(m.rho, rho), - eps_r = _pair_from_nominal(m.eps_r, eps_r), - mu_r = _pair_from_nominal(m.mu_r, mu_r), - T0 = _pair_from_nominal(m.T0, T0), - alpha = _pair_from_nominal(m.alpha, alpha), - ) -end - -# --- 3) From a MaterialsLibrary + name --- -Material(lib::Materials.MaterialsLibrary, name::AbstractString; kwargs...) = - Material(get(lib, name); kwargs...) -Material(lib::Materials.MaterialsLibrary, name::Symbol; kwargs...) = - Material(lib, String(name); kwargs...) - - -function _make_range(ms::MaterialSpec) - ρs = _make_range(ms.rho[1]; pct = ms.rho[2]) - εs = _make_range(ms.eps_r[1]; pct = ms.eps_r[2]) - μs = _make_range(ms.mu_r[1]; pct = ms.mu_r[2]) - Ts = _make_range(ms.T0[1]; pct = ms.T0[2]) - αs = _make_range(ms.alpha[1]; pct = ms.alpha[2]) - [Materials.Material(ρ, ε, μ, T, α) for (ρ, ε, μ, T, α) in product(ρs, εs, μs, Ts, αs)] -end \ No newline at end of file diff --git a/src/parametricbuilder/physicaltree.jl b/src/parametricbuilder/physicaltree.jl new file mode 100644 index 000000000..13f2ddaf2 --- /dev/null +++ b/src/parametricbuilder/physicaltree.jl @@ -0,0 +1,1359 @@ +function Region(tag, primitive, material; combine::Symbol = :product) + return parameterize( + DataModel.Region, DataModel.Region, (tag, primitive, material); combine + ) +end + +# `DataModel.Region(::Symbol, primitive, material)` is deliberately permissive +# for scalar declaration types, so explicit finite inputs need narrower methods +# to use the collection constructor instead of that scalar constructor. +const _FiniteRegionInput = Union{AbstractGrid, Gridspace} + +function Region(tag::_FiniteRegionInput, primitive, material; combine::Symbol = :product) + parameterize(DataModel.Region, DataModel.Region, (tag, primitive, material); combine) +end +function Region(tag, primitive::_FiniteRegionInput, material; combine::Symbol = :product) + parameterize(DataModel.Region, DataModel.Region, (tag, primitive, material); combine) +end +function Region(tag, primitive, material::_FiniteRegionInput; combine::Symbol = :product) + parameterize(DataModel.Region, DataModel.Region, (tag, primitive, material); combine) +end +function Region( + tag::Symbol, + primitive::_FiniteRegionInput, + material; + combine::Symbol = :product +) + parameterize(DataModel.Region, DataModel.Region, (tag, primitive, material); combine) +end +function Region( + tag::Symbol, + primitive, + material::_FiniteRegionInput; + combine::Symbol = :product +) + parameterize(DataModel.Region, DataModel.Region, (tag, primitive, material); combine) +end +function Region( + tag::Symbol, + primitive::_FiniteRegionInput, + material::_FiniteRegionInput; + combine::Symbol = :product +) + parameterize(DataModel.Region, DataModel.Region, (tag, primitive, material); combine) +end +function Region( + tag::_FiniteRegionInput, + primitive::_FiniteRegionInput, + material; + combine::Symbol = :product +) + parameterize(DataModel.Region, DataModel.Region, (tag, primitive, material); combine) +end +function Region( + tag::_FiniteRegionInput, + primitive, + material::_FiniteRegionInput; + combine::Symbol = :product +) + parameterize(DataModel.Region, DataModel.Region, (tag, primitive, material); combine) +end +function Region( + tag, + primitive::_FiniteRegionInput, + material::_FiniteRegionInput; + combine::Symbol = :product +) + parameterize(DataModel.Region, DataModel.Region, (tag, primitive, material); combine) +end +function Region( + tag::_FiniteRegionInput, + primitive::_FiniteRegionInput, + material::_FiniteRegionInput; + combine::Symbol = :product +) + parameterize(DataModel.Region, DataModel.Region, (tag, primitive, material); combine) +end + +function Stack(items...; combine::Symbol = :product) + isempty(items) && throw(ArgumentError("layers require at least one part")) + return parameterize(DataModel.Stack, DataModel.Stack, items; combine) +end + +""" +$(TYPEDSIGNATURES) + +Compose physical parts in outward order. + +# Arguments + +- `parts`: physical declarations ordered from the center outward. + +# Keywords + +- `combine=:product`: gridspace composition rule. + +# Returns + +- A `Stack`, or a `Gridspace{Stack}` when a direct argument varies. +""" +layers(parts...; combine::Symbol = :product) = Stack(parts...; combine) + +function Group( + name, + item; + at = DataModel.Pose2(0, 0, 0), + pattern = nothing, + path = nothing, + compact = nothing, + boundary = nothing, + combine::Symbol = :product +) + values = (name, at, item, pattern, path, compact, boundary) + return parameterize(DataModel.Group, DataModel.Group, values; combine) +end + +function Assembly( + item; + at = DataModel.Pose2(0, 0, 0), + pattern, + names = nothing, + path = nothing, + compact = nothing, + combine::Symbol = :product +) + values = (at, item, pattern, path, compact, names) + return parameterize(DataModel.Assembly, DataModel.Assembly, values; combine) +end + +function _explicit_assembly(members...) + placed = map(members) do member + member isa DataModel.AssemblyMember ? member : + member isa DataModel.AbstractCablePart ? DataModel.AssemblyMember(member) : + throw(ArgumentError("assembly members must be physical cable parts")) + end + return DataModel.Assembly( + DataModel.Pose2(0, 0, 0), Tuple(placed), nothing, nothing, nothing, nothing + ) +end + +""" +$(TYPEDSIGNATURES) + +Preserve explicit physical members and their independent terminal identities. + +With `pattern`, retain one prototype and one placement pattern. Without a +pattern, retain one or more heterogeneous members and their local poses. + +# Arguments + +- `members`: physical members, optionally placed with [`at`](@ref). + +# Keywords + +- `pattern=nothing`: placement pattern for one repeated prototype, or explicit members. +- `names=nothing`: exact terminal names for repeated terminal-bearing members. +- `path=nothing`: shared longitudinal path declaration. +- `compact=nothing`: explicit compaction law. +- `combine=:product`: gridspace composition rule. + +# Returns + +- An `Assembly`, or a `Gridspace{Assembly}` when a direct argument varies. +""" +function assembly( + members...; + pattern = nothing, + names = nothing, + path = nothing, + compact = nothing, + combine::Symbol = :product +) + isempty(members) && throw(ArgumentError("assembly requires at least one member")) + if pattern === nothing + names === nothing && path === nothing && compact === nothing || + throw(ArgumentError( + "explicit assembly members own their names, paths and compaction; " * + "provide pattern to repeat one prototype")) + return parameterize(DataModel.Assembly, _explicit_assembly, members; combine) + end + length(members) == 1 || throw(ArgumentError( + "a repeated assembly requires exactly one prototype with pattern")) + return Assembly( + first(members); + pattern, + names, + path, + compact, + combine + ) +end + +function Enclosure( + tag, + item; + at = DataModel.Pose2(0, 0, 0), + primitive, + fill, + wall = nothing, + combine::Symbol = :product +) + values = (tag, at, primitive, item, fill, wall) + return parameterize(DataModel.Enclosure, DataModel.Enclosure, values; combine) +end + +_terminal_eligible(region::DataModel.Region) = region.material.kind === :conductor ? 1 : 0 +_terminal_eligible(stack::DataModel.Stack) = sum(_terminal_eligible, stack.items; init = 0) +_terminal_eligible(group::DataModel.Group) = _terminal_eligible(group.item) +function _terminal_eligible( + assembly::DataModel.Assembly{<:Any, <:DataModel.AbstractCablePart} +) + _terminal_eligible(assembly.item) +end +function _terminal_eligible(assembly::DataModel.Assembly{<:Any, <:Tuple}) + return sum(member -> _terminal_eligible(member.item), assembly.item; init = 0) +end +function _terminal_eligible(enclosure::DataModel.Enclosure) + count = _terminal_eligible(enclosure.item) + enclosure.fill isa DataModel.Region && (count += _terminal_eligible(enclosure.fill)) + enclosure.wall === nothing || (count += _terminal_eligible(enclosure.wall)) + return count +end + +function _terminal(name, parts...) + root = DataModel.Stack(parts...) + _terminal_eligible(root) > 0 || throw(ArgumentError( + "terminal :$name requires a conductive descendant" + )) + return DataModel.Group( + name, DataModel.Pose2(0, 0, 0), root, nothing, nothing, nothing + ) +end + +""" +$(TYPEDSIGNATURES) + +Coalesce every conductive descendant of an ordered physical subtree into one +retained terminal. + +# Arguments + +- `name`: retained electrical terminal name. +- `parts`: physical declarations ordered from the center outward. + +# Keywords + +- `combine=:product`: gridspace composition rule. + +# Returns + +- A terminal-owning `Group`, or a `Gridspace{Group}` when a direct argument + varies. + +# Errors + +- Throws `ArgumentError` when the realized subtree contains no conductive + descendant. +""" +function terminal(name, parts...; combine::Symbol = :product) + isempty(parts) && throw(ArgumentError("terminal requires at least one part")) + return parameterize(DataModel.Group, _terminal, (name, parts...); combine) +end + +""" +$(TYPEDSIGNATURES) + +Bind an intrinsic primitive definition to one material and local physical tag. + +# Arguments + +- `material`: constitutive material record. +- `primitive`: intrinsic cross-sectional primitive definition. + +# Keywords + +- `tag=:solid`: local physical identity. +- `combine=:product`: gridspace composition rule. + +# Returns + +- A `Region`, or a `Gridspace{Region}` when a direct argument varies. +""" +function solid(material, primitive; tag = :solid, combine::Symbol = :product) + Region(tag, primitive, material; combine) +end + +""" +$(TYPEDSIGNATURES) + +Declare one outward material layer of thickness `t` \\[m\\]. + +# Arguments + +- `material`: constitutive material record. + +# Keywords + +- `t`: normal layer thickness \\[m\\]. +- `tag=:shell`: local physical identity. +- `combine=:product`: gridspace composition rule. + +# Returns + +- A contextual `Region`, or a `Gridspace{Region}` when a direct argument + varies. +""" +function shell(material; t, tag = :shell, combine::Symbol = :product) + Region(tag, Shell(t), material; combine) +end + +""" +$(TYPEDSIGNATURES) + +Declare a conductive core region. + +# Arguments + +- `material`: material with `kind == :conductor`. +- `primitive`: intrinsic core geometry. The keyword form constructs a disk of + radius `r` \\[m\\]. + +# Keywords + +- `r`: disk radius \\[m\\]. +- `tag=:core`: local physical identity. +- `combine=:product`: gridspace composition rule. + +# Returns + +- A conductive `Region`, or a `Gridspace{Region}` when a direct argument + varies. +""" +function core(material, primitive; tag = :core, combine::Symbol = :product) + caller = (resolved_material, + resolved_primitive, + resolved_tag) -> DataModel.Region( + resolved_tag, + resolved_primitive, + validate(resolved_material, DataModel.Region, :core, (:conductor,)) + ) + return parameterize( + DataModel.Region, caller, (material, primitive, tag); combine + ) +end + +function core(material; r, tag = :core, combine::Symbol = :product) + core(material, Disk(r); tag, combine) +end + +function _role_shell(role::Symbol, allowed, material, t, tag) + return DataModel.Region( + tag, + DataModel.Shell(t), + validate(material, DataModel.Region, role, allowed) + ) +end + +function _shell_role( + role::Symbol, allowed::Tuple, material, t, tag; + combine::Symbol +) + caller = (resolved_material, + resolved_t, + resolved_tag) -> _role_shell( + role, allowed, resolved_material, resolved_t, resolved_tag) + return parameterize(DataModel.Region, caller, (material, t, tag); combine) +end + +"""Declare an insulating layer of thickness `t` \\[m\\].""" +function insulation(material; t, tag = :insulation, combine::Symbol = :product) + _shell_role(:insulation, (:insulator,), material, t, tag; combine) +end +"""Declare a semiconductive or conductive screen layer of thickness `t` \\[m\\].""" +function screen(material; t, tag = :screen, combine::Symbol = :product) + _shell_role(:screen, (:semicon, :conductor), material, t, tag; combine) +end +"""Declare a conductive sheath layer of thickness `t` \\[m\\].""" +function sheath(material; t, tag = :sheath, combine::Symbol = :product) + _shell_role(:sheath, (:conductor,), material, t, tag; combine) +end +"""Declare a nonconducting bedding layer of thickness `t` \\[m\\].""" +function bedding(material; t, tag = :bedding, combine::Symbol = :product) + _shell_role(:bedding, (:insulator,), material, t, tag; combine) +end +"""Declare an insulating jacket layer of thickness `t` \\[m\\].""" +function jacket(material; t, tag = :jacket, combine::Symbol = :product) + _shell_role(:jacket, (:insulator,), material, t, tag; combine) +end + +""" +$(TYPEDSIGNATURES) + +Bind a nonconducting filler material to an intrinsic primitive definition. + +# Arguments + +- `material`: material with `kind == :insulator`. +- `primitive`: intrinsic filler geometry. + +# Keywords + +- `tag=:filler`: local physical identity. +- `combine=:product`: gridspace composition rule. + +# Returns + +- A filler `Region`, or a `Gridspace{Region}` when a direct argument varies. +""" +function filler(material, primitive; tag = :filler, combine::Symbol = :product) + caller = (resolved_material, + resolved_primitive, + resolved_tag) -> DataModel.Region( + resolved_tag, + resolved_primitive, + validate(resolved_material, DataModel.Region, :filler, (:insulator,)) + ) + return parameterize( + DataModel.Region, caller, (material, primitive, tag); combine + ) +end + +_path(::Nothing, dir, φ0) = nothing +_path(path::DataModel.Helix, dir, φ0) = path +_path(lay, dir, φ0) = DataModel.Helix(lay; dir, φ0) + +function _wires( + material, shape, pattern, path, compact, tag +) + source = DataModel.Region( + tag, + shape, + validate(material, DataModel.Region, :wires, (:conductor,)) + ) + return DataModel.Group( + tag, DataModel.Pose2(0, 0, 0), source, pattern, path, compact + ) +end + +""" +$(TYPEDSIGNATURES) + +Declare one repeated course of conductive members. + +Supply `pattern` and `path` directly for general placement, or supply `n`, `r`, +and `lay` for the practical ring-course form. + +# Arguments + +- `material`: material with `kind == :conductor`. + +# Keywords + +- `shape`: intrinsic member primitive. +- `pattern=nothing`: explicit member placement pattern. +- `path=nothing`: explicit longitudinal path. +- `n=nothing`: exact ring cardinality or `capacity()`. +- `r=nothing`: member-center ring radius \\[m\\]. +- `gap_frac=0`: fractional adjacent clearance \\[dimensionless\\]. +- `lay=nothing`: one lay law used to construct a `Helix`. +- `dir=1`: helix handedness, `1` or `-1` \\[dimensionless\\]. +- `φ0=0`: initial angular position \\[rad\\]. +- `compact=nothing`: explicit compaction law. +- `tag=:wire`: local member and group identity. +- `combine=:product`: gridspace composition rule. + +# Returns + +- A repeated-member `Group`, or a `Gridspace{Group}` when a direct argument + varies. +""" +function wires( + material; + shape, + pattern = nothing, + path = nothing, + n = nothing, + r = nothing, + gap_frac = 0, + lay = nothing, + dir = 1, + φ0 = 0, + compact = nothing, + tag = :wire, + combine::Symbol = :product +) + caller = function ( + resolved_material, resolved_shape, resolved_pattern, resolved_path, + resolved_n, resolved_r, resolved_gap, resolved_lay, resolved_dir, + resolved_φ0, resolved_compact, resolved_tag + ) + if resolved_pattern !== nothing + resolved_n === nothing && resolved_r === nothing || throw(ArgumentError( + "pattern cannot be combined with n or r" + )) + resolved_lay === nothing || throw(ArgumentError( + "pattern-oriented wires use path rather than lay" + )) + return _wires( + resolved_material, + resolved_shape, + resolved_pattern, + resolved_path, + resolved_compact, + resolved_tag + ) + end + resolved_n === nothing && throw(ArgumentError( + "ring-course wires require n" + )) + resolved_r === nothing && throw(ArgumentError( + "ring-course wires require r" + )) + resolved_path === nothing || throw(ArgumentError( + "ring-course wires use lay rather than path" + )) + return _wires( + resolved_material, + resolved_shape, + DataModel.Ring( + resolved_n; + r = resolved_r, + φ0 = resolved_φ0, + gap_frac = resolved_gap + ), + _path(resolved_lay, resolved_dir, resolved_φ0), + resolved_compact, + resolved_tag + ) + end + values = ( + material, shape, pattern, path, n, r, gap_frac, lay, dir, φ0, + compact, tag + ) + return parameterize(DataModel.Group, caller, values; combine) +end + +function _course_schedule(value, count::Int, name::Symbol) + if value isa Union{Tuple, AbstractVector} + length(value) == count || throw(DimensionMismatch( + "$name requires exactly $count course values" + )) + return Tuple(value) + end + return ntuple(_ -> value, count) +end + +function _count_schedule(value, count::Int) + if value isa Union{Tuple, AbstractVector} + length(value) == count || throw(DimensionMismatch( + "n requires exactly $count course values" + )) + return Tuple(value) + end + value isa Integer && !(value isa Bool) && value > 0 && + return ntuple(index -> index * Int(value), count) + value === DataModel.capacity() && return ntuple(_ -> value, count) + throw(ArgumentError( + "n must be a positive base count, exact course schedule, or capacity()" + )) +end + +function _stranding_paths(lay, dir, φ0) + lay === nothing && return nothing + if lay isa Union{Tuple, AbstractVector} + count = length(lay) + count > 0 || throw(ArgumentError("lay schedule cannot be empty")) + directions = _course_schedule(dir, count, :dir) + angles = _course_schedule(φ0, count, :φ0) + return ntuple(index -> _path(lay[index], directions[index], angles[index]), count) + end + dir isa Union{Tuple, AbstractVector} && throw(ArgumentError( + "a dir schedule requires a lay schedule of the same length" + )) + φ0 isa Union{Tuple, AbstractVector} && throw(ArgumentError( + "a φ0 schedule requires a lay schedule of the same length" + )) + return _path(lay, dir, φ0) +end + +function _stranded( + material, + center, + shape, + lay, + dir, + φ0, + compact, + prescribed_boundary, + fill +) + material = validate(material, DataModel.Region, :stranded, (:conductor,)) + fill = validate(fill, DataModel.Region, :stranded_fill, (:insulator, :semicon)) + prescribed_boundary isa Union{DataModel.Disk, DataModel.Sector} || + throw(ArgumentError( + "stranded requires a nonhollow Disk or Sector boundary" + )) + shape isa Union{DataModel.Disk, DataModel.Rectangle} || throw(ArgumentError( + "stranded members must be Disk wires or Rectangle strands" + )) + if prescribed_boundary isa DataModel.Sector + shape isa DataModel.Disk || throw(ArgumentError( + "a sector stranded core requires circular Disk wires" + )) + center === nothing || throw(ArgumentError( + "a sector bundle infers its center strand from shape; omit center" + )) + compact === nothing || throw(ArgumentError( + "sector stranded cores are intrinsically compacted; omit compact" + )) + compaction = nothing + else + if shape isa DataModel.Disk + center === nothing && (center = shape) + compaction = compact === nothing ? false : compact + compaction isa Bool || throw(ArgumentError( + "circular stranded compaction is selected with compact=true or false" + )) + else + center isa DataModel.Disk || throw(ArgumentError( + "a rectangular stranded core requires center=Disk(...)" + )) + compact === nothing || throw(ArgumentError( + "rectangular strands are intrinsically bent; omit compact" + )) + compaction = true + end + center isa DataModel.Disk || throw(ArgumentError( + "a disk-bounded stranded core requires one circular center wire" + )) + end + + paths = _stranding_paths(lay, dir, φ0) + parts = DataModel.AbstractCablePart[] + center === nothing || push!(parts, + DataModel.Group( + :strand, + DataModel.Pose2(0, 0, 0), + DataModel.Region(:wire, center, material), + nothing, + nothing, + nothing + )) + push!(parts, + DataModel.Group( + :strand, + DataModel.Pose2(0, 0, 0), + DataModel.Region(:wire, shape, material), + nothing, + paths, + nothing + )) + bounded = DataModel.Group( + :core, + DataModel.Pose2(0, 0, 0), + DataModel.Stack(parts), + nothing, + nothing, + compaction, + prescribed_boundary + ) + shape isa DataModel.Rectangle && return bounded + return DataModel.Enclosure( + :stranded, + DataModel.Pose2(0, 0, 0), + prescribed_boundary, + bounded, + fill, + nothing + ) +end + +""" +$(TYPEDSIGNATURES) + +Fill one authoritative core boundary with the maximum admissible inventory of +equal source strands. + +Circular source bundles have one center strand and `6k` wire strands in course `k`. +A sector admits the largest complete inventory satisfying +``[1 + 3L(L+1)]\\,\\pi a^2 \\leq A_{\\mathrm{sector}}``. Its mapped sites define +a prescribed-area power diagram. Disks clipped to those cells retain each +source area. This geometric reconstruction preserves the declared copper +fill and does not model mechanical forming or plasticity. + +Rectangular strands occupy complete, area-preserving annular courses, including +a full annulus when a course contains one strand. Their requested geometric boundary is +a packing limit. The resolved geometric boundary is the occupied metal disk. Subsequent +layers start there, without an automatically generated outer filler film. + +# Arguments + +- `material`: material with `kind == :conductor`. + +# Keywords + +- `center=nothing`: circular center member for a disk-bounded core. Circular + strands default to one center wire equal to `shape`. Rectangular strands + require an explicit `Disk`. A sector bundle infers its center strand from + `shape` and does not admit a separate center declaration. +- `shape`: circular wire or rectangular strand primitive. +- `boundary`: nonhollow `Disk` or `Sector` core boundary. For rectangular + strands, a `Disk` packing limit rather than an imposed finished radius. +- `fill=air`: interstitial insulating or semiconducting material, unused by + contiguous rectangular courses. The default + is lossless air (``\\rho=\\infty``, ``\\epsilon_r=\\mu_r=1``). +- `lay=nothing`: one lay law or a schedule matching the inferred radial courses. +- `dir=1`: helix handedness or a schedule matching `lay`. +- `φ0=0`: helix initial angle or a schedule matching `lay` \\[rad\\]. +- `compact=nothing`: circular disk-bounded strands remain circular by default. + `true` requests area-preserving deformation. Sector and rectangular + wire stranding are intrinsically deformed and do not admit this keyword. +- `combine=:product`: gridspace composition rule. + +# Returns + +- A bounded `Group` for rectangular strands, or a material-complete + `Enclosure` for circular strands. Gridded arguments return a `Gridspace` + of the corresponding part type, or `AbstractCablePart` for mixed shapes. +""" +function stranded( + material; + center = nothing, + shape, + lay = nothing, + dir = 1, + φ0 = 0, + compact = nothing, + boundary, + fill = Materials.Material(:insulator, Inf), + combine::Symbol = :product +) + caller = function ( + resolved_material, resolved_center, resolved_shape, resolved_lay, + resolved_dir, resolved_φ0, resolved_compact, resolved_boundary, + resolved_fill + ) + return _stranded( + resolved_material, + resolved_center, + resolved_shape, + resolved_lay, + resolved_dir, + resolved_φ0, + resolved_compact, + resolved_boundary, + resolved_fill + ) + end + values = ( + material, center, shape, lay, dir, φ0, compact, boundary, fill + ) + shape_type = shape isa AbstractGrid ? eltype(shape) : typeof(shape) + target = if shape_type <: DataModel.Rectangle || shape isa Gridspace{<:DataModel.Rectangle} + DataModel.Group + elseif shape_type <: DataModel.Disk || shape isa Gridspace{<:DataModel.Disk} + DataModel.Enclosure + else + DataModel.AbstractCablePart + end + return parameterize(target, caller, values; combine) +end + +function _inner_radius(primitive::DataModel.Disk) + return hypot(primitive.at.x, primitive.at.y) - primitive.r +end + +function _inner_radius(primitive::DataModel.Polygon) + cosine = cos(primitive.at.φ) + sine = sin(primitive.at.φ) + points = map(primitive.points) do point + return ( + primitive.at.x + cosine * point[1] - sine * point[2], + primitive.at.y + sine * point[1] + cosine * point[2] + ) + end + return minimum(eachindex(points)) do index + next = mod1(index + 1, length(points)) + first_point = points[index] + last_point = points[next] + edge = ( + last_point[1] - first_point[1], + last_point[2] - first_point[2] + ) + length_squared = edge[1]^2 + edge[2]^2 + fraction = iszero(length_squared) ? zero(length_squared) : clamp( + -(first_point[1] * edge[1] + first_point[2] * edge[2]) / + length_squared, + zero(length_squared), + one(length_squared) + ) + hypot( + first_point[1] + fraction * edge[1], + first_point[2] + fraction * edge[2] + ) + end +end + +function _milliken( + material, + shape, + segment, + segments, + lay, + dir, + φ0, + fill +) + material = validate(material, DataModel.Region, :milliken, (:conductor,)) + shape isa DataModel.Disk || throw(ArgumentError( + "milliken segment strands must be circular Disk wires" + )) + segment isa DataModel.Sector || throw(ArgumentError( + "milliken segment must be one Sector boundary" + )) + segments isa Integer && !(segments isa Bool) && segments >= 2 || + throw(ArgumentError("milliken segments must be an integer of at least two")) + pitch = 2pi / segments + isapprox(segment.span, pitch) || throw(DomainError( + segment.span, + "milliken segment span must equal its angular pitch $pitch" + )) + fill = validate(fill, DataModel.Region, :milliken_fill, (:insulator, :semicon)) + + segment_part = _stranded( + material, + nothing, + shape, + lay, + dir, + φ0, + nothing, + segment, + fill + ) + _, _, segment_primitives = DataModel.bounded_members(segment_part.item) + center_radius = minimum(_inner_radius, segment_primitives) + center_radius > zero(center_radius) || throw(DomainError( + center_radius, + "milliken segment strands leave no positive radius for the center wire" + )) + center = DataModel.Disk(center_radius) + members = DataModel.AssemblyMember[] + center_terminal = DataModel.Group( + :milliken_center, + DataModel.Pose2(0, 0, 0), + DataModel.Region(:wire, center, material), + nothing, + nothing, + nothing + ) + push!(members, DataModel.AssemblyMember(center_terminal)) + for index in 1:Int(segments) + name = Symbol(:milliken_segment_, index) + terminal = DataModel.Group( + name, + DataModel.Pose2(0, 0, 0), + segment_part.item, + nothing, + nothing, + nothing + ) + push!(members, DataModel.AssemblyMember( + terminal, + DataModel.Pose2(0, 0, (index - 1) * pitch) + )) + end + assembly = DataModel.Assembly( + DataModel.Pose2(0, 0, 0), + Tuple(members), + nothing, + nothing, + nothing, + nothing + ) + core = DataModel.Group( + :core, + DataModel.Pose2(0, 0, 0), + DataModel.Stack(assembly), + nothing, + nothing, + nothing + ) + return DataModel.Enclosure( + :milliken, + DataModel.Pose2(0, 0, 0), + DataModel.Disk(segment.r_back), + core, + fill, + nothing + ) +end + +""" +$(TYPEDSIGNATURES) + +Declare one Milliken conductor as a cable-center wire surrounded by equal +stranded sector segments. Each segment contains its own bundle-center strand +and complete `6k` courses. Each conductive descendant resolves to one `:core` +terminal. + +# Arguments + +- `material`: conductor material shared by the center and segment strands. + +# Keywords + +- `shape`: circular source wire repeated inside every segment. +- `segment`: authoritative geometric boundary of one sector segment. Its span must equal + ``2\\pi/N`` for `segments=N`. +- `segments=6`: number of equal sector segments \\[dimensionless\\]. +- `lay=nothing`: common strand lay law. +- `dir=1`: helix handedness \\[dimensionless\\]. +- `φ0=0`: helix initial angle \\[rad\\]. +- `fill=air`: interstitial material around the center and every segment strand. +- `combine=:product`: gridspace composition rule. + +The center-wire radius is inferred from the resolved segment packing so that +the center wire is tangent to the innermost strand of every equal segment. + +# Returns + +- A material-complete `Enclosure`, or a `Gridspace{Enclosure}` when a direct + argument varies. +""" +function milliken( + material; + shape, + segment, + segments = 6, + lay = nothing, + dir = 1, + φ0 = 0, + fill = Materials.Material(:insulator, Inf), + combine::Symbol = :product +) + values = ( + material, shape, segment, segments, lay, dir, φ0, fill + ) + return parameterize(DataModel.Enclosure, _milliken, values; combine) +end + +function _rope( + item, + course_count, + counts, + lays, + directions, + angles, + compactions, + gaps +) + item isa DataModel.AbstractCablePart || throw(ArgumentError( + "rope item must be a physical cable part" + )) + central = DataModel.Group( + :rope, DataModel.Pose2(0, 0, 0), item, nothing, nothing, nothing + ) + parts = DataModel.AbstractCablePart[central] + for course in 1:course_count + push!(parts, + DataModel.Group( + :rope, + DataModel.Pose2(0, 0, 0), + item, + DataModel.Ring( + counts[course]; + r = nothing, + φ0 = angles[course], + gap_frac = gaps[course] + ), + _path(lays[course], directions[course], angles[course]), + compactions[course] + )) + end + return DataModel.Stack(parts) +end + +""" +$(TYPEDSIGNATURES) + +Repeat one physical item as a central child and concentric outer courses. + +# Arguments + +- `item`: physical cable part repeated by the rope. + +# Keywords + +- `layers`: number of outer courses \\[dimensionless\\]. +- `n=6`: base count, exact course schedule or deferred maximum count `capacity()`. +- `lay=nothing`: one lay law or one law per outer course. Homogeneous schedules + may be declared as `LayRatio(q...)`, `Pitch(p...)` or `LayAngle(α...)`. +- `dir=1`: one handedness or one value per outer course. +- `φ0=0`: one initial angle or one value per outer course \\[rad\\]. +- `compact=nothing`: one compaction law or one law per outer course. + Homogeneous scalar schedules may be declared as `FillFactor(η...)`. +- `gap_frac=0`: one clearance fraction or one value per outer course + \\[dimensionless\\]. +- `combine=:product`: gridspace composition rule. + +# Returns + +- A nested `Stack`, or a `Gridspace{Stack}` when a direct argument varies. +""" +function rope( + item; + layers, + n = 6, + lay = nothing, + dir = 1, + φ0 = 0, + compact = nothing, + gap_frac = 0, + combine::Symbol = :product +) + caller = function ( + resolved_item, resolved_layers, resolved_n, resolved_lay, + resolved_dir, resolved_φ0, resolved_compact, resolved_gap + ) + resolved_layers isa Integer && !(resolved_layers isa Bool) && + resolved_layers >= 0 || throw(ArgumentError( + "layers must be a nonnegative integer" + )) + count = Int(resolved_layers) + return _rope( + resolved_item, + count, + _count_schedule(resolved_n, count), + _course_schedule(resolved_lay, count, :lay), + _course_schedule(resolved_dir, count, :dir), + _course_schedule(resolved_φ0, count, :φ0), + _course_schedule(resolved_compact, count, :compact), + _course_schedule(resolved_gap, count, :gap_frac) + ) + end + values = (item, layers, n, lay, dir, φ0, compact, gap_frac) + return parameterize(DataModel.Stack, caller, values; combine) +end + +""" +$(TYPEDSIGNATURES) + +Declare a repeated armor-wire course whose radius is resolved from the current +outer boundary. + +# Arguments + +- `material`: material with `kind == :conductor`. + +# Keywords + +- `shape`: intrinsic armor-member primitive. +- `n`: exact cardinality or deferred maximum count `capacity()`. +- `lay=nothing`: one helical lay law. +- `dir=1`: helix handedness, `1` or `-1` \\[dimensionless\\]. +- `φ0=0`: initial angular position \\[rad\\]. +- `compact=nothing`: explicit compaction law. +- `gap_frac=0`: fractional adjacent clearance \\[dimensionless\\]. +- `tag=:armor`: local member and group identity. +- `combine=:product`: gridspace composition rule. + +# Returns + +- An armor `Group`, or a `Gridspace{Group}` when a direct argument varies. +""" +function armor( + material; + shape, + n, + lay = nothing, + dir = 1, + φ0 = 0, + compact = nothing, + gap_frac = 0, + tag = :armor, + combine::Symbol = :product +) + caller = function ( + resolved_material, resolved_shape, resolved_n, resolved_lay, + resolved_dir, resolved_φ0, resolved_compact, resolved_gap, + resolved_tag + ) + source = DataModel.Region( + resolved_tag, + resolved_shape, + validate(resolved_material, DataModel.Region, :armor, (:conductor,)) + ) + return DataModel.Group( + resolved_tag, + DataModel.Pose2(0, 0, 0), + source, + DataModel.Ring( + resolved_n; + r = nothing, + φ0 = resolved_φ0, + gap_frac = resolved_gap + ), + _path(resolved_lay, resolved_dir, resolved_φ0), + resolved_compact + ) + end + values = (material, shape, n, lay, dir, φ0, compact, gap_frac, tag) + return parameterize(DataModel.Group, caller, values; combine) +end + +function _tape(material, section, n, lay, gap, compact, tag) + section isa DataModel.Rectangle || throw(ArgumentError( + "tape section must be a Rectangle(width, thickness)" + )) + source = DataModel.Region(tag, section, material) + return DataModel.Group( + :tapes, + DataModel.Pose2(0, 0, 0), + source, + DataModel.Ring(n; r = nothing, gap_frac = gap), + _path(lay, 1, 0), + compact + ) +end + +""" +$(TYPEDSIGNATURES) + +Declare a repeated conductive, semiconductive, or insulating tape system. + +# Arguments + +- `material`: tape material. + +# Keywords + +- `section`: intrinsic tape cross-section. +- `n`: exact angular cardinality or deferred maximum count `capacity()`. +- `lay=nothing`: one helical lay law. +- `gap_frac=0`: fractional angular clearance \\[dimensionless\\]. +- `compact=nothing`: explicit compaction law. +- `tag=:tape`: local tape identity. +- `combine=:product`: gridspace composition rule. + +# Returns + +- A tape `Group`, or a `Gridspace{Group}` when a direct argument varies. +""" +function tape( + material; + section, + n, + lay = nothing, + gap_frac = 0, + compact = nothing, + tag = :tape, + combine::Symbol = :product +) + values = (material, section, n, lay, gap_frac, compact, tag) + return parameterize(DataModel.Group, _tape, values; combine) +end + +""" +$(TYPEDSIGNATURES) + +Arrange independent cable parts as repeated or explicit cores. + +The pattern-backed form retains one prototype. The variadic form preserves +heterogeneous members and their local poses. + +# Arguments + +- `members`: explicit core members. + +# Keywords + +- `n=nothing`: repeated cardinality. Omit for explicit members. +- `r=nothing`: member-center ring radius \\[m\\], required when `n` is supplied. +- `names=nothing`: exact terminal names required for repeated terminal-bearing members. +- `φ0=0`: starting angle \\[rad\\]. +- `span=2π`: angular span \\[rad\\]. +- `path=nothing`: shared longitudinal path. +- `compact=nothing`: explicit compaction law. +- `combine=:product`: gridspace composition rule. + +# Returns + +- An `Assembly`, or a `Gridspace{Assembly}` when a direct argument varies. + +# Notes + +Origin-centered repeated sectors require the sector span to equal their angular +pitch. Their resolved sides must not overlap. An outer insulating layer may +close the clearance to zero. Bare sectors require positive side clearance. +""" +function cores( + members...; + n = nothing, + r = nothing, + names = nothing, + φ0 = 0, + span = 2π, + path = nothing, + compact = nothing, + combine::Symbol = :product +) + if n === nothing + r === nothing && φ0 == 0 && span == 2π || throw(ArgumentError( + "cores requires n when specifying a repeated radius or angular placement")) + return assembly(members...; names, path, compact, combine) + end + r === nothing && throw(ArgumentError("repeated cores require an explicit ring radius r")) + return assembly(members...; + pattern = DataModel.Ring(n; r, φ0, span, combine), + names, path, compact, combine) +end + +function _enclosure_item(items::Tuple, formation) + if formation === nothing + return length(items) == 1 && only(items) isa DataModel.AbstractCablePart ? + only(items) : _explicit_assembly(items...) + end + length(items) == 1 || throw(ArgumentError( + "a pattern-backed enclosure requires one repeated prototype" + )) + only(items) isa DataModel.AbstractCablePart || throw(ArgumentError( + "a pattern-backed enclosure requires an unplaced physical prototype" + )) + return DataModel.Assembly( + DataModel.Pose2(0, 0, 0), + only(items), + formation, + nothing, + nothing, + nothing + ) +end + +function _enclose(tag, items, shape, fill, wall, pose, formation) + item = _enclosure_item(Tuple(items), formation) + resolved_pose = pose === nothing ? DataModel.Pose2(0, 0, 0) : pose + return DataModel.Enclosure(tag, resolved_pose, shape, item, fill, wall) +end + +""" +$(TYPEDSIGNATURES) + +Contain one or more physical members inside a pipe cross-section. + +# Arguments + +- `items`: enclosed physical members. Several members form an explicit + assembly. + +# Keywords + +- `shape`: intrinsic containing primitive. +- `fill`: filling material or explicit filling region. +- `wall=nothing`: optional outward wall declaration. +- `at=nothing`: pipe pose relative to its parent frame. +- `combine=:product`: gridspace composition rule. + +# Returns + +- An `Enclosure`, or a `Gridspace{Enclosure}` when a direct argument varies. +""" +function pipe( + items...; + shape, + fill, + wall = nothing, + at = nothing, + combine::Symbol = :product +) + isempty(items) && throw(ArgumentError("pipe requires enclosed content")) + caller = (selected...) -> begin + count = length(items) + physical = selected[1:count] + _enclose(:pipe, physical, selected[(count + 1):end]..., nothing) + end + values = (items..., shape, fill, wall, at) + return parameterize(DataModel.Enclosure, caller, values; combine) +end + +""" +$(TYPEDSIGNATURES) + +Contain one or more physical members inside a duct cross-section. + +A `formation` repeats one prototype without expanding it. Explicitly placed +members may differ in geometry and terminal structure. + +# Arguments + +- `items`: enclosed physical members. + +# Keywords + +- `shape`: intrinsic containing primitive. +- `fill`: filling material or explicit filling region. +- `wall=nothing`: optional outward wall declaration. +- `formation=nothing`: placement pattern for one repeated prototype. +- `at=nothing`: duct pose relative to its parent frame. +- `combine=:product`: gridspace composition rule. + +# Returns + +- An `Enclosure`, or a `Gridspace{Enclosure}` when a direct argument varies. +""" +function duct( + items...; + shape, + fill, + wall = nothing, + formation = nothing, + at = nothing, + combine::Symbol = :product +) + isempty(items) && throw(ArgumentError("duct requires enclosed content")) + caller = (selected...) -> begin + count = length(items) + physical = selected[1:count] + _enclose(:duct, physical, selected[(count + 1):end]...) + end + values = (items..., shape, fill, wall, at, formation) + return parameterize(DataModel.Enclosure, caller, values; combine) +end + +function build( + ::Type{DataModel.CableDesign}, + cable_id, + parts::Tuple, + nominal_data; + combine::Symbol = :product +) + isempty(parts) && throw(ArgumentError("a cable design requires one physical part")) + caller = (selected...) -> begin + id = first(selected) + data = last(selected) + physical = selected[2:(end - 1)] + build(DataModel.CableDesign, id, Tuple(physical), data) + end + values = (cable_id, parts..., nominal_data) + return parameterize(DataModel.CableDesign, caller, values; combine) +end + +function build( + ::Type{DataModel.CableDesign}, + cable_id, + parts...; + nominal_data = nothing, + combine::Symbol = :product +) + values = (cable_id, parts..., nominal_data) + any(value -> value isa Union{AbstractGrid, Gridspace}, values) || throw(MethodError( + build, (DataModel.CableDesign, cable_id, parts...) + )) + return build( + DataModel.CableDesign, + cable_id, + Tuple(parts), + nominal_data; + combine + ) +end diff --git a/src/parametricbuilder/positions.jl b/src/parametricbuilder/positions.jl new file mode 100644 index 000000000..fc8efa198 --- /dev/null +++ b/src/parametricbuilder/positions.jl @@ -0,0 +1,286 @@ +struct _ConnectionsAbsent end + +const _CONNECTIONS_ABSENT = _ConnectionsAbsent() + +_parameter_type(value) = typeof(value) +_parameter_type(grid::DeterministicGrid) = eltype(grid) +_parameter_type(::Union{RelativeGrid, AbsoluteGrid}) = Real +_parameter_type(::Gridspace{Target}) where {Target} = Target + +at(; x = 0, y = 0, φ = 0, combine::Symbol = :product) = at(x, y; φ, combine) + +at(pose::DataModel.Pose2) = pose + +function _placed_design(design, pose, connections) + ( + design = design, + pose = pose, + connections = connections + ) +end + +function _compose_member(outer, member::DataModel.AssemblyMember) + DataModel.AssemblyMember(member.item, outer * member.at) +end +_compose_member(outer, pose::DataModel.Pose2) = outer * pose +function _compose_member(outer, placement::NamedTuple) + merge( + placement, + (pose = outer * placement.pose,) + ) +end + +function _compose_collection(placements, pose) + placements isa Union{Tuple, AbstractVector} || throw(ArgumentError( + "outer placement requires a tuple or vector" + )) + return map(member -> _compose_member(pose, member), placements) +end + +_collection_target(::Tuple) = Tuple +_collection_target(::AbstractVector) = Vector +_collection_target(::Gridspace{Target}) where {Target} = Target <: Tuple ? Tuple : Vector + +function _at_pair( + ::Type{<:Real}, ::Type{<:Real}, + x, y, φ, ::_ConnectionsAbsent, combine::Symbol +) + return parameterize(DataModel.Pose2, DataModel.Pose2, (x, y, φ); combine) +end + +function _at_pair( + ::Type{<:DataModel.AbstractCablePart}, ::Type{<:DataModel.Pose2}, + part, pose, φ, ::_ConnectionsAbsent, combine::Symbol +) + iszero(φ) || throw(ArgumentError( + "at(part, pose) does not accept a second rotation" + )) + return parameterize( + DataModel.AssemblyMember, DataModel.AssemblyMember, (part, pose); combine + ) +end + +function _at_pair( + ::Type{<:DataModel.CableDesign}, ::Type{<:DataModel.Pose2}, + design, pose, φ, connections, combine::Symbol +) + connections isa _ConnectionsAbsent && throw(ArgumentError( + "at(design, pose) requires connections" + )) + iszero(φ) || throw(ArgumentError( + "at(design, pose) does not accept a second rotation" + )) + return parameterize( + NamedTuple, _placed_design, (design, pose, connections); combine + ) +end + +function _at_pair( + ::Type{<:Union{Tuple, AbstractVector}}, ::Type{<:DataModel.Pose2}, + placements, pose, φ, ::_ConnectionsAbsent, + combine::Symbol +) + iszero(φ) || throw(ArgumentError( + "at(placements, pose) does not accept a second rotation" + )) + return parameterize( + _collection_target(placements), + _compose_collection, + (placements, pose); + combine + ) +end + +""" +$(TYPEDSIGNATURES) + +Construct a pose, place a local cable part, place a completed cable with its +connections, or compose an outer transform onto a placement collection. + +# Arguments + +- `x`: horizontal translation \\[m\\]. +- `y`: vertical translation \\[m\\]. +- `subject`: physical part, completed design or placement collection. +- `pose`: existing `Pose2` declaration. + +# Keywords + +- `φ=0`: counter-clockwise rotation \\[rad\\]. +- `connections`: terminal-to-active-phase declaration required for a completed + cable design. Use one-based active phase IDs and `0` for a grounded or eliminated + conductor. +- `combine=:product`: gridspace composition rule. + +# Returns + +- A `Pose2`, placed member, placed-design record, transformed collection, or + the corresponding `Gridspace` when a direct argument varies. +""" +function at( + left, + right; + φ = 0, + connections = _CONNECTIONS_ABSENT, + combine::Symbol = :product +) + left_type = _parameter_type(left) + right_type = _parameter_type(right) + (left_type === Union{} || right_type === Union{}) && throw(ArgumentError( + "at does not accept an empty finite source" + )) + return _at_pair(left_type, right_type, left, right, φ, connections, combine) +end + +function at( + subject, + x, + y; + φ = 0, + connections = _CONNECTIONS_ABSENT, + combine::Symbol = :product +) + pose = at(x, y; φ, combine) + return at(subject, pose; connections, combine) +end + +function _connection_for_member(connections::NamedTuple, member::Int, count::Int) + resolved = map(Base.values(connections)) do value + if value isa Union{Tuple, AbstractVector} + length(value) == count || throw(DimensionMismatch( + "connection schedules must contain $count values" + )) + return value[member] + end + return value + end + return NamedTuple{keys(connections)}(resolved) +end + +function _connection_for_member(connections::AbstractDict, member::Int, count::Int) + return Dict(key => begin + if value isa Union{Tuple, AbstractVector} + length(value) == count || throw(DimensionMismatch( + "connection schedules must contain $count values" + )) + value[member] + else + value + end + end for (key, value) in connections) +end + +function _connection_for_member(connections::Union{Tuple, AbstractVector}, member, count) + length(connections) == count || throw(DimensionMismatch( + "formation connections must contain $count declarations" + )) + return connections[member] +end + +function _formation(design, local_poses, center, connections) + count = length(local_poses) + return [_placed_design( + design, + center * pose, + _connection_for_member(connections, member, count) + ) + for (member, pose) in enumerate(local_poses)] +end + +function _trefoil(design, center, spacing, connections, φ0) + spacing > zero(spacing) || throw(DomainError( + spacing, "trefoil spacing must be positive" + )) + coordinates = DataModel.trefoil_formation(0, 0, spacing / 2) + poses = DataModel.Pose2[DataModel.Pose2(coordinates[index], coordinates[index + 1], 0) + for index in 1:2:6] + origin = center * DataModel.Pose2(0, 0, φ0) + return _formation(design, poses, origin, connections) +end + +""" +$(TYPEDSIGNATURES) + +Place three copies of one completed cable in an equilateral trefoil formation. + +# Arguments + +- `design`: completed cable design reused by all three members. + +# Keywords + +- `center=at(0, 0)`: formation-center pose \\[m, m, rad\\]. +- `spacing`: cable center-to-center distance \\[m\\]. +- `connections`: scalar or three-member terminal connection schedules. +- `φ0=0`: formation rotation \\[rad\\]. +- `combine=:product`: gridspace composition rule. + +# Returns + +- Three placed-design records, or a `Gridspace{Vector}` when a direct argument + varies. +""" +function trefoil( + design; + center = at(0, 0), + spacing, + connections, + φ0 = 0, + combine::Symbol = :product +) + values = (design, center, spacing, connections, φ0) + return parameterize(Vector, _trefoil, values; combine) +end + +function _flat(design, center, spacing, connections, vertical::Bool) + spacing > zero(spacing) || throw(DomainError( + spacing, "flat-formation spacing must be positive" + )) + offsets = (-spacing, zero(spacing), spacing) + poses = DataModel.Pose2[vertical ? DataModel.Pose2(0, -offset, 0) : + DataModel.Pose2(offset, 0, 0) + for offset in offsets] + return _formation(design, poses, center, connections) +end + +""" +$(TYPEDSIGNATURES) + +Place three copies of one completed cable in a horizontal flat formation. + +`spacing` is the adjacent center-to-center distance \\[m\\]. Scalar connection +entries apply to every cable. Three-element entries distribute by member. +""" +function hflat( + design; + center = at(0, 0), + spacing, + connections, + combine::Symbol = :product +) + caller = (resolved...) -> _flat(resolved..., false) + return parameterize( + Vector, caller, (design, center, spacing, connections); combine + ) +end + +""" +$(TYPEDSIGNATURES) + +Place three copies of one completed cable in a vertical flat formation. + +`spacing` is the adjacent center-to-center distance \\[m\\]. Scalar connection +entries apply to every cable. Three-element entries distribute by member. +""" +function vflat( + design; + center = at(0, 0), + spacing, + connections, + combine::Symbol = :product +) + caller = (resolved...) -> _flat(resolved..., true) + return parameterize( + Vector, caller, (design, center, spacing, connections); combine + ) +end diff --git a/src/parametricbuilder/positionspec.jl b/src/parametricbuilder/positionspec.jl deleted file mode 100644 index 8037af380..000000000 --- a/src/parametricbuilder/positionspec.jl +++ /dev/null @@ -1,79 +0,0 @@ -struct PositionSpec <: AbstractPositionSpec - x0::Real - y0::Real - dx::Any - dy::Any - conn::Dict{String, Int} -end - -# ───────────────────────────────────────────────────────────────────────────── -# Phase mapping helpers -# Accepts: -# :core => 1 -# ("core", 1) -# (:core, 1) -# [ :core => 1, :sheath => 0 ] -# etc. -# ───────────────────────────────────────────────────────────────────────────── -const _PhaseMapInputs = Union{ - Tuple{Symbol, Any}, - Tuple{String, Any}, - Pair{Symbol, Any}, - Pair{String, Any}, -} - -# normalize phases input to a splattable tuple of _PhaseMapInputs -_normalize_phase_map(p::_PhaseMapInputs) = (p,) -_normalize_phase_map(p::Tuple) = p -_normalize_phase_map(v::AbstractVector) = Tuple(v) -_normalize_phase_map(::Nothing) = () - -""" - make_phase_maps(phases, n::Int) - -Unified helper to process phase DSL inputs. -- If `n=1`, returns a vector with one Dict (used by `at`). -- If `n>1`, distributes values: - - Scalars (e.g. `1`) are broadcast to all `n` legs. - - Tuples/Vectors (e.g. `(1,2,3)`) are distributed to respective legs. -""" -function make_phase_maps(phases, n::Int) - items = _normalize_phase_map(phases) - out = [Dict{String, Int}() for _ in 1:n] - - for item in items - # Extract key/value - key_raw, val_raw = item isa Pair ? (first(item), last(item)) : (item[1], item[2]) - key = string(key_raw) - - # Distribute - if val_raw isa Integer - # Scalar broadcast - v = Int(val_raw) - for i in 1:n - out[i][key] = v - end - elseif (val_raw isa Tuple || val_raw isa AbstractVector) - # Vector distribution - if length(val_raw) != n - error( - "Dimension mismatch for phase '$key': expected $n elements, got $(length(val_raw))", - ) - end - for i in 1:n - out[i][key] = Int(val_raw[i]) - end - else - error( - "Invalid phase value for '$key': expected Integer or collection of length $n, got $(typeof(val_raw))", - ) - end - end - return out -end - -function at(; x, y, dx = 0.0, dy = 0.0, phases = nothing) - # n=1 for single position - maps = make_phase_maps(phases, 1) - return PositionSpec(x, y, dx, dy, maps[1]) -end diff --git a/src/parametricbuilder/results.jl b/src/parametricbuilder/results.jl new file mode 100644 index 000000000..a9a818c16 --- /dev/null +++ b/src/parametricbuilder/results.jl @@ -0,0 +1,256 @@ +""" +$(TYPEDEF) + +Evaluate every point in a [`ParametricProblem`](@ref) with `inner`. + +`inner` may be one completed formulation, a deterministic target-bearing +formulation [`Gridspace`](@ref) or a deterministic [`Grid`](@ref) containing +completed formulations. Problem and formulation points always form a +Cartesian product. Uncertainty belongs to the problem space and is rejected +from the formulation source. + +$(TYPEDFIELDS) +""" +struct Combinatorial{F, O <: ComputationOptions} <: AbstractFormulation + "Formulation used for each core problem." + inner::F + "Supplemental-output retention options owned by this traversal." + options::O + + function Combinatorial(inner, options::ComputationOptions) + source = _combinatorial_source(inner) + normalized = computation_options(Combinatorial, options) + return new{typeof(source), typeof(normalized)}(source, normalized) + end +end + +function computation_options( + ::Type{Combinatorial}, + record::ComputationOptions +)::ComputationOptions + options = record.data + unknown = filter(key -> key !== :retain_details, keys(options)) + isempty(unknown) || throw(ArgumentError( + "unknown Combinatorial computation options: $(sort!(collect(unknown)))", + )) + normalized = merge((retain_details = false,), options) + normalized.retain_details isa Bool || throw(ArgumentError( + "Combinatorial retain_details must be Bool", + )) + return ComputationOptions(; retain_details = normalized.retain_details) +end + +function _combinatorial_source(inner::AbstractFormulation) + return inner +end + +function _combinatorial_source(inner::Gridspace{Target}) where {Target} + Target <: AbstractFormulation || throw(ArgumentError( + "Combinatorial Gridspace target must subtype AbstractFormulation; got $Target", + )) + has_uncertainty(inner) && throw(ArgumentError( + "Combinatorial formulation spaces must be deterministic", + )) + return inner +end + +function _combinatorial_source(inner::AbstractGrid) + has_uncertainty(inner) && throw(ArgumentError( + "Combinatorial formulation grids must be deterministic", + )) + all(value -> value isa AbstractFormulation, inner) || throw(ArgumentError( + "Combinatorial formulation grids must contain completed AbstractFormulation values", + )) + return inner +end + +function _combinatorial_source(inner) + throw(ArgumentError( + "Combinatorial requires a formulation, formulation Grid, or target-bearing formulation Gridspace; got $(typeof(inner))", + )) +end + +function Combinatorial(inner; options::Union{NamedTuple,ComputationOptions} = ComputationOptions()) + options = options isa NamedTuple ? ComputationOptions(options) : options + return Combinatorial(inner, options) +end + +""" +$(TYPEDEF) + +Pair a lazy parameter space with computation options for a higher-order +computation. A completed scalar problem is normalized to a singleton +target-bearing `Gridspace`. The problem itself need not implement iteration. + +$(TYPEDFIELDS) +""" +struct ParametricProblem{S, O <: ComputationOptions} <: AbstractProblemDefinition + "Lazy space of complete core problems." + space::S + "Options forwarded to each core computation for owner-specific normalization." + options::O + + function ParametricProblem(space::S, options::O) where {S, O <: ComputationOptions} + return validate(new{S, O}(space, options)) + end +end + +function ParametricProblem(problem::AbstractProblemDefinition, options::ComputationOptions) + space = Gridspace{typeof(problem)}(identity, (Grid((problem,)),)) + return ParametricProblem(space, options) +end + +ParametricProblem(space) = ParametricProblem(space, ComputationOptions()) + +function validate(problem::ParametricProblem) + problem.space isa Union{ + AbstractProblemDefinition, + Gridspace{<:AbstractProblemDefinition} + } || throw(ArgumentError( + "ParametricProblem.space must be a completed problem or a Gridspace " * + "targeting AbstractProblemDefinition; received $(typeof(problem.space))" + )) + if problem.space isa AbstractProblemDefinition + validate(problem.space) + else + length(problem.space) > 0 || throw(ArgumentError( + "ParametricProblem.space must contain at least one problem point" + )) + end + return problem +end + +""" +$(TYPEDEF) + +Store core results over resolved problem and formulation axes. + +Linear indexing remains compatible with ordinary result-space iteration. +Values use column-major `(problem, formulation)` order: the problem index +varies fastest. Two-dimensional indexing accepts those indices directly. + +$(TYPEDFIELDS) +""" +struct ParametricResult{T, F, A <: NamedTuple, D <: ComputationDetails} <: + AbstractParametricResult{T} + "Higher-order formulation used for the computation." + formulation::F + "Core results in problem-index-fastest formulation order." + values::Vector{T} + "Problem and resolved-formulation sources aligned with `values`." + axes::A + "Typed supplemental output retained by the traversal." + details::D + + function ParametricResult( + formulation::F, + values::Vector{T}, + axes::A, + details::D + ) where {T, F, A <: NamedTuple, D <: ComputationDetails} + validate(T, ParametricResult) + if !isempty(axes) + keys(axes) == (:problems, :formulations) || throw(ArgumentError( + "ParametricResult axes must contain problems and formulations", + )) + length(axes.problems) * length(axes.formulations) == length(values) || + throw(DimensionMismatch( + "ParametricResult axes must span every stored core result", + )) + end + isempty(details.data) || keys(details.data) == (:points,) || + throw(ArgumentError( + "ParametricResult details must be empty or contain only points", + )) + isempty(details.data) || length(details.data.points) == length(values) || + throw(DimensionMismatch( + "retained details must contain one entry per core result", + )) + return new{T, F, A, D}(formulation, values, axes, details) + end +end + +function ParametricResult(formulation, values, details::ComputationDetails) + ParametricResult(formulation, values, (;), details) +end +ParametricResult(formulation, values) = ParametricResult(formulation, values, (;), ComputationDetails()) + +Base.length(value::ParametricResult) = length(value.values) +Base.size(value::ParametricResult) = (length(value),) +Base.getindex(value::ParametricResult, index::Integer) = value.values[index] +Base.iterate(value::ParametricResult, state...) = iterate(value.values, state...) +Base.firstindex(value::ParametricResult) = firstindex(value.values) +Base.lastindex(value::ParametricResult) = lastindex(value.values) + +""" +Return one result selected by its problem and formulation indices. +""" +function Base.getindex( + value::ParametricResult, + problem_index::Integer, + formulation_index::Integer +) + isempty(value.axes) && throw(ArgumentError( + "this ParametricResult does not retain problem/formulation axes", + )) + problem_count = length(value.axes.problems) + formulation_count = length(value.axes.formulations) + 1 <= problem_index <= problem_count || throw(BoundsError( + value, + (problem_index, formulation_index) + )) + 1 <= formulation_index <= formulation_count || throw(BoundsError( + value, + (problem_index, formulation_index) + )) + index = problem_index + (formulation_index - 1) * problem_count + return value.values[index] +end + +details(value::ParametricResult) = value.details + +const _GRIDSPACE_TRANSPORT_DOCUMENTATION = "https://electa-git.github.io/LineCableModels.jl/dev/gridspace/" * + "#Transporting-completed-result-spaces" + +function _transport_error(::Type{Target}, source::AbstractResultSpace) where {Target} + target_name = nameof(Target) + source_name = nameof(typeof(source)) + throw(ArgumentError( + "Gridspace transport from $source_name to problem $target_name " * + "is not defined. Define Gridspace{$target_name}(::$source_name) " * + "for that result family. See `?Gridspace` and " * + _GRIDSPACE_TRANSPORT_DOCUMENTATION, + )) +end + +""" +$(TYPEDSIGNATURES) + +Transport every completed combinatorial result into one target-bearing +downstream problem point while preserving source cardinality and order. +""" +function Gridspace{Target}( + source::ParametricResult{T, F} +) where {Target, T, F <: Combinatorial} + return Gridspace{Target}(Target, (source,)) +end + +""" +$(TYPEDSIGNATURES) + +Reject result-to-problem transports whose result owner has not defined how to +construct the requested target-bearing `Gridspace`. +""" +function Gridspace{Target}(source::AbstractResultSpace) where {Target} + return _transport_error(Target, source) +end + +""" +$(TYPEDSIGNATURES) + +Expose the formulation, ordered values, resolved axes and computation details as +a native record. Arrays and axes retain their identity. The method returns these retained objects without calculating or copying them. +""" +function Base.NamedTuple(value::ParametricResult) + return (formulation=value.formulation, values=value.values, axes=value.axes, details=value.details) +end diff --git a/src/parametricbuilder/system.jl b/src/parametricbuilder/system.jl new file mode 100644 index 000000000..de7d1606b --- /dev/null +++ b/src/parametricbuilder/system.jl @@ -0,0 +1,224 @@ +function build( + ::Type{DataModel.LineCableSystem}, + designs, + placements; + connections = nothing, + environment = nothing, + system_id = "line-cable-system", + line_length = 1, + combine::Symbol = :product +) + values = ( + designs, + placements, + connections, + environment, + system_id, + line_length + ) + caller = (selected...) -> build(DataModel.LineCableSystem, selected...) + return parameterize(DataModel.LineCableSystem, caller, values; combine) +end + +function _placed_system( + placements, + environment, + system_id, + line_length +) + placements isa Union{Tuple, AbstractVector} && !isempty(placements) || throw( + ArgumentError("placed cable declarations must be a nonempty collection") + ) + declarations = Any[] + for placement in placements + if placement isa Union{Tuple, AbstractVector} + append!(declarations, placement) + else + push!(declarations, placement) + end + end + isempty(declarations) && throw(ArgumentError( + "placed cable declarations must be a nonempty collection" + )) + all(declarations) do placement + placement isa NamedTuple && + keys(placement) == (:design, :pose, :connections) && + placement.design isa DataModel.CableDesign && + placement.pose isa DataModel.Pose2 + end || throw(ArgumentError( + "each placed cable requires design, pose, and connections" + )) + return build( + DataModel.LineCableSystem, + getproperty.(declarations, :design), + getproperty.(declarations, :pose), + getproperty.(declarations, :connections), + environment, + system_id, + line_length + ) +end + +function build( + ::Type{DataModel.LineCableSystem}, + placements; + environment = nothing, + system_id = "line-cable-system", + line_length = 1, + combine::Symbol = :product +) + values = (placements, environment, system_id, line_length) + return parameterize( + DataModel.LineCableSystem, _placed_system, values; combine + ) +end + +function _line_problem(system, temperature, earth, frequencies) + return Engine.LineParametersProblem( + system; + temperature, + earth_props = earth, + frequencies + ) +end + +function Engine.LineParametersProblem( + system::Gridspace{DataModel.LineCableSystem}; + temperature = 20, + earth_props, + frequencies = [50], + combine::Symbol = :product +) + values = (system, temperature, earth_props, frequencies) + grids = map(values) do value + value isa Union{AbstractGrid, Gridspace} ? value : Grid((value,)) + end + return Gridspace{Engine.LineParametersProblem}(_line_problem, grids; combine) +end + +function Engine.LineParametersProblem( + system::Gridspace{DataModel.LineCableSystem}, + earth_props::Union{AbstractGrid, Gridspace}; + temperature = 20, + frequencies = [50], + combine::Symbol = :product +) + return Engine.LineParametersProblem( + system; + temperature, + earth_props, + frequencies, + combine + ) +end + +function _placed_line_problem( + placements, + environment, + system_id, + line_length, + temperature, + earth_props, + frequencies +) + system = _placed_system(placements, environment, system_id, line_length) + return Engine.LineParametersProblem( + system; + temperature, + earth_props, + frequencies + ) +end + +function Engine.LineParametersProblem( + placements::Union{Tuple, AbstractVector, Gridspace}; + environment = nothing, + system_id = "line-cable-system", + line_length = 1, + temperature = 20, + earth_props, + frequencies = [50], + combine::Symbol = :product +) + values = ( + placements, environment, system_id, line_length, temperature, + earth_props, frequencies + ) + return parameterize( + Engine.LineParametersProblem, _placed_line_problem, values; combine + ) +end + +function Engine.LineParametersProblem( + system::DataModel.LineCableSystem, + earth_props::Union{AbstractGrid, Gridspace}; + temperature = 20, + frequencies = [50], + combine::Symbol = :product +) + values = (system, temperature, earth_props, frequencies) + grids = map(values) do value + value isa Union{AbstractGrid, Gridspace} ? value : Grid((value,)) + end + return Gridspace{Engine.LineParametersProblem}(_line_problem, grids; combine) +end + +function Engine.LineParametersProblem( + designs, + placements, + connections, + environment, + system_id, + line_length, + temperature, + earth_props, + frequencies; + combine::Symbol = :product +) + values = ( + designs, + placements, + connections, + environment, + system_id, + line_length, + temperature, + earth_props, + frequencies + ) + any(value -> value isa Union{AbstractGrid, Gridspace}, values) || throw( + MethodError(Engine.LineParametersProblem, values) + ) + sources = map(values) do value + value isa Union{AbstractGrid, Gridspace} ? value : Grid((value,)) + end + return Gridspace{Engine.LineParametersProblem}( + Engine.LineParametersProblem, sources; combine + ) +end + +function Engine.LineParametersProblem( + designs, + placements; + connections = nothing, + environment = nothing, + system_id = "line-cable-system", + line_length = 1, + temperature = 20, + earth_props, + frequencies = [50], + combine::Symbol = :product +) + return Engine.LineParametersProblem( + designs, + placements, + connections, + environment, + system_id, + line_length, + temperature, + earth_props, + frequencies; + combine + ) +end diff --git a/src/parametricbuilder/systembuilderspec.jl b/src/parametricbuilder/systembuilderspec.jl deleted file mode 100644 index 8329437bd..000000000 --- a/src/parametricbuilder/systembuilderspec.jl +++ /dev/null @@ -1,166 +0,0 @@ -# Positions in the parametric system builder. -# -# Two flavours: -# - PositionSpec : single anchor with dx/dy ranges (arbitrary layouts) -# - PositionGroupSpec : defined in `trifoil.jl`, grouped formations -# -# Both subtype AbstractPositionSpec so SystemBuilderSpec can store a mixed vector. -abstract type AbstractPositionSpec end - -include("positionspec.jl") -include("groupspec.jl") - -# ───────────────────────────────────────────────────────────────────────────── -# Earth and system specs -# ───────────────────────────────────────────────────────────────────────────── -struct EarthSpec - rho::Any; - eps_r::Any; - mu_r::Any; - t::Any -end -EarthSpec(; rho, eps_r = 1.0, mu_r = 1.0, t = Inf) = - EarthSpec(_spec(rho), _spec(eps_r), _spec(mu_r), _spec(t)) - -Earth(; rho, eps_r = 1.0, mu_r = 1.0, t = Inf) = - EarthSpec(_spec(rho), _spec(eps_r), _spec(mu_r), _spec(t)) - -struct SystemBuilderSpec - system_id::String - builder::CableBuilderSpec - positions::Vector{AbstractPositionSpec} - length::Any # (valuespec, pctspec) or scalar - temperature::Any # (valuespec, pctspec) or scalar - earth::EarthSpec - frequencies::Vector{Float64} -end - -function SystemBuilderSpec(id::AbstractString, cbs::CableBuilderSpec, - positions::Vector{<:AbstractPositionSpec}; - length = 1000.0, temperature = 20.0, earth::EarthSpec, f::AbstractVector{<:Real}) - return SystemBuilderSpec( - String(id), - cbs, - positions, - _spec(length), - _spec(temperature), - earth, - collect(float.(f)), - ) -end - -SystemBuilder(id::AbstractString, cbs::CableBuilderSpec, - positions::AbstractVector{<:AbstractPositionSpec}; - length = 1000.0, temperature = 20.0, earth::EarthSpec, f::AbstractVector{<:Real}) = SystemBuilderSpec(id, cbs, positions; length, temperature, earth, f) - -SystemBuilder(id::AbstractString, cbs::CableBuilderSpec, - positions::AbstractPositionSpec; - length = 1000.0, temperature = 20.0, earth::EarthSpec, f::AbstractVector{<:Real}) = SystemBuilderSpec(id, cbs, [positions]; length, temperature, earth, f) - -# ───────────────────────────────────────────────────────────────────────────── -# Internals: expand range/% grammar via ParametricBuilder helpers -# ───────────────────────────────────────────────────────────────────────────── -@inline _expand_pair(specpair) = _make_range(specpair[1]; pct = specpair[2]) - -# (nothing, pct) on dx/dy ⇒ attach % to the anchor itself (no displacement sweep) -@inline function _axis(anchor::Number, dspec) - spec, pct = _spec(dspec) - if spec === nothing - return _make_range(anchor; pct = pct) # uncertain anchor - else - return (anchor .+ v for v in _make_range(spec; pct = pct)) # displaced anchor - end -end - -_expand_earth(e::EarthSpec) = ( - (ρ, ε, μ, t) - for ρ in _expand_pair(e.rho), - ε in _expand_pair(e.eps_r), - μ in _expand_pair(e.mu_r), - t in _expand_pair(e.t) -) - -# Choice count for single positions: size of the dx × dy grid -function _position_choice_count(p::PositionSpec) - nx = length(collect(_axis(p.x0, p.dx))) - ny = length(collect(_axis(p.y0, p.dy))) - return nx * ny -end - -# Choice count for grouped positions: number of spacing samples -function _position_choice_count(g::PositionGroupSpec) - spec, pct = g.d - return length(collect(_make_range(spec; pct = pct))) -end - -# ───────────────────────────────────────────────────────────────────────────── -# Main iterator: yields fully-formed LineParametersProblem objects -# Overlaps are *not* emitted (skipped with warning by catching the geometry error). -# Designs are identical per system realization (no cross-mixing). -# ───────────────────────────────────────────────────────────────────────────── -function iterate(spec::SystemBuilderSpec) - return Channel{LineParametersProblem}(32) do ch - produced = 0 - try - for des in spec.builder - for L in _expand_pair(spec.length) - # NB: _expand_position keeps grouped spacings atomic and - # materializes after `des` (and its outer radius) are known. - for choice in _expand_position(spec.positions, des) - try - x1, y1, c1 = choice[1] - sys = DataModel.LineCableSystem( - spec.system_id, - L, - DataModel.CablePosition(des, x1, y1, c1), - ) - - for k in Iterators.drop(eachindex(choice), 1) - xk, yk, ck = choice[k] - sys = add!(sys, des, xk, yk, ck) - end - - for T in _expand_pair(spec.temperature) - for (ρ, ε, μ, t) in _expand_earth(spec.earth) - em = EarthModel(spec.frequencies, ρ, ε, μ; t = t) - prob = LineParametersProblem( - sys; - temperature = T, - earth_props = em, - frequencies = spec.frequencies, - ) - put!(ch, prob) - produced += 1 - end - end - catch e - if occursin("overlap", sprint(showerror, e)) || occursin( - "conductor resistivity must be positive", - sprint(showerror, e), - ) - @warn sprint(showerror, e) - @warn "Skipping..." - continue - else - rethrow() - end - end - end - end - end - catch e - @error "iterate SystemBuilderSpec failed" exception = (e, catch_backtrace()) - finally - @debug "iterate SystemBuilderSpec finished" produced = produced upper_bound = - cardinality(spec) - end - end -end - -function build(spec::SystemBuilderSpec) - problems = LineParametersProblem[] - for prob in spec # uses Base.iterate(spec::SystemBuilderSpec) - push!(problems, prob) - end - return problems -end diff --git a/src/parametricbuilder/textdisplay.jl b/src/parametricbuilder/textdisplay.jl new file mode 100644 index 000000000..80548f0b2 --- /dev/null +++ b/src/parametricbuilder/textdisplay.jl @@ -0,0 +1,200 @@ +function _grid_item(value) + value isa Real && return TextDisplay.value(value) + return sprint(show, value; context = :compact => true, sizehint = 64) +end + +function _display_grid_values(values) + count = length(values) + count == 0 && return "empty" + visible = min(count, 4) + items = join((_grid_item(values[index]) for index in 1:visible), ", ") + return count > visible ? "$items, … ($count values)" : items +end + +TextDisplay.name(::Type{<:UncertainValue}) = "UncertainValue" +function Base.summary(io::IO, ::UncertainValue) + print(io, "Uncertain value") +end +function Base.show(io::IO, value::UncertainValue) + print(io, "UncertainValue(", _grid_item(value.nominal), " ± ", _grid_item(value.sigma), ")") +end +function Base.show(io::IO, ::MIME"text/plain", value::UncertainValue) + show(io, value) +end + +TextDisplay.name(::Type{<:AbsoluteError}) = "AbsoluteError" +function Base.summary(io::IO, error::AbsoluteError) + print(io, "Absolute error with $(length(error.vals)) values") +end +function Base.show(io::IO, error::AbsoluteError) + print(io, "AbsoluteError(", _display_grid_values(error.vals), ")") +end +Base.show(io::IO, ::MIME"text/plain", error::AbsoluteError) = show(io, error) + +function _grid_kind(::DeterministicGrid) + return "Grid" +end +function _grid_kind(::RelativeGrid) + return "Relative-error Grid" +end +function _grid_kind(::AbsoluteGrid) + return "Absolute-error Grid" +end + +function _grid_nominals(grid::DeterministicGrid) + return grid.vals +end +function _grid_nominals(grid::Union{RelativeGrid, AbsoluteGrid}) + return grid.vals +end +function _grid_errors(grid::RelativeGrid) + return "relative error $(_display_grid_values(grid.rel_err)) %" +end +function _grid_errors(grid::AbsoluteGrid) + return "absolute error $(_display_grid_values(grid.abs_err))" +end + +TextDisplay.name(::Type{<:DeterministicGrid}) = "Grid" +TextDisplay.name(::Type{<:RelativeGrid}) = "Relative-error Grid" +TextDisplay.name(::Type{<:AbsoluteGrid}) = "Absolute-error Grid" +function Base.summary(io::IO, grid::AbstractGrid) + print(io, _grid_kind(grid), " with ", length(grid), " points") +end +function Base.show(io::IO, grid::AbstractGrid) + print(io, _grid_kind(grid), "(", _display_grid_values(_grid_nominals(grid))) + grid isa DeterministicGrid || print(io, "; ", _grid_errors(grid)) + print(io, ")") +end +function Base.show(io::IO, ::MIME"text/plain", grid::AbstractGrid) + get(io, :compact, false) && return show(io, grid) + children = Any[(label = "values $(_display_grid_values(_grid_nominals(grid)))", noun = "values")] + grid isa DeterministicGrid || push!(children, (label = _grid_errors(grid), noun = "values")) + return TextDisplay.tree(io, "$(_grid_kind(grid)) · $(length(grid)) points", Tuple(children)) +end + +function _semantic_target(::Type{Target}) where {Target} + applicable(TextDisplay.name, Target) && return TextDisplay.name(Target) + return String(nameof(Target)) +end + +function _space_axis(source::Gridspace) + return sprint(show, source; context = :compact => true, sizehint = 96) +end +function _space_axis(source::AbstractGrid) + return _display_grid_values(_grid_nominals(source)) +end + +TextDisplay.name(::Type{<:Gridspace{Target}}) where {Target} = + string(_semantic_target(Target), " parameter space") +function Base.summary(io::IO, space::Gridspace{Target}) where {Target} + print(io, _semantic_target(Target), " parameter space with ", length(space), " points") +end +function Base.show(io::IO, space::Gridspace{Target}) where {Target} + print(io, _semantic_target(Target), " parameter space · ", length(space), " points") +end +function Base.show(io::IO, ::MIME"text/plain", space::Gridspace{Target}) where {Target} + get(io, :compact, false) && return show(io, space) + varying = Tuple((index, source) for (index, source) in enumerate(space.grids) + if length(source) > 1) + fixed = length(space.grids) - length(varying) + children = Any[ + (label = "axis $index $(_space_axis(source))", noun = "axes") + for (index, source) in varying + ] + fixed == 0 || push!(children, ( + label = "$fixed fixed $(fixed == 1 ? "input" : "inputs")", + noun = "axes", + )) + target = _semantic_target(Target) + return TextDisplay.tree( + io, + "$target parameter space · $(length(varying)) varying axes · $(length(space)) points", + Tuple(children); + noun = "axes" + ) +end + +TextDisplay.name(::Type{<:Combinatorial}) = "Combinatorial" +Base.summary(io::IO, ::Combinatorial) = print(io, "Combinatorial formulation") +function Base.show(io::IO, formulation::Combinatorial) + print(io, "Combinatorial(") + show(IOContext(io, :compact => true), formulation.inner) + print(io, ")") +end +Base.show(io::IO, ::MIME"text/plain", formulation::Combinatorial) = show(io, formulation) + +TextDisplay.name(::Type{<:ParametricProblem}) = "ParametricProblem" +Base.summary(io::IO, ::ParametricProblem) = print(io, "Parametric problem") +function Base.show(io::IO, problem::ParametricProblem) + print(io, "ParametricProblem(") + show(IOContext(io, :compact => true), problem.space) + print(io, ")") +end +function Base.show(io::IO, ::MIME"text/plain", problem::ParametricProblem) + get(io, :compact, false) && return show(io, problem) + return TextDisplay.tree(io, "ParametricProblem", ( + (label = "space $(sprint(show, problem.space; context = :compact => true))", noun = "fields"), + (label = "options $(length(problem.options.data)) entries", noun = "fields"), + )) +end + +TextDisplay.name(::Type{<:ParametricResult}) = "ParametricResult" +function Base.summary(io::IO, result::ParametricResult) + print(io, "Parametric result with $(length(result)) values") +end +function Base.show(io::IO, result::ParametricResult) + print(io, "ParametricResult($(length(result)) values)") +end +function Base.show(io::IO, ::MIME"text/plain", result::ParametricResult) + get(io, :compact, false) && return show(io, result) + value_type = isempty(result.values) ? "none" : _semantic_target(eltype(result.values)) + return TextDisplay.tree(io, "ParametricResult · $(length(result)) values", ( + (label = "result type $value_type", noun = "fields"), + (label = "details $(length(result.details.data)) entries", noun = "fields"), + )) +end + +TextDisplay.name(::Type{<:WireEstimate}) = "WireEstimate" +function Base.summary(io::IO, estimate::WireEstimate) + print(io, "Wire estimate with $(length(estimate)) patterns") +end +function Base.show(io::IO, estimate::WireEstimate) + print(io, "WireEstimate(", estimate.status, "; patterns=", length(estimate), ")") +end +function Base.show(io::IO, ::MIME"text/plain", estimate::WireEstimate) + get(io, :compact, false) && return show(io, estimate) + patterns = Tuple(( + label = sprint(show, pattern; context = :compact => true, sizehint = 96), + noun = "patterns", + ) for pattern in estimate.patterns) + children = Any[ + (label = "target $(TextDisplay.value(estimate.target_area))", noun = "fields"), + (label = "status $(estimate.status)", noun = "fields"), + (label = "patterns", children = patterns, noun = "patterns"), + ] + isempty(estimate.reasons) || push!(children, ( + label = "reasons", + children = Tuple((label = reason, noun = "reasons") for reason in estimate.reasons), + noun = "reasons", + )) + return TextDisplay.tree(io, "Wire estimate", Tuple(children)) +end + +function Base.summary(io::IO, pattern::WirePatterns.HexaPattern) + print(io, "Strand pattern with $(pattern.wires) wires") +end +function Base.show(io::IO, pattern::WirePatterns.HexaPattern) + print(io, "StrandPattern(layers=", pattern.layers, ", wires=", pattern.wires, + ", d=", TextDisplay.engineering(pattern.wire_diameter, :meter), ")") +end +Base.show(io::IO, ::MIME"text/plain", pattern::WirePatterns.HexaPattern) = show(io, pattern) + +function Base.summary(io::IO, pattern::WirePatterns.ScreenPattern) + print(io, "Screen pattern with $(pattern.wires) wires") +end +function Base.show(io::IO, pattern::WirePatterns.ScreenPattern) + print(io, "ScreenPattern(wires=", pattern.wires, + ", d=", TextDisplay.engineering(pattern.wire_diameter, :meter), + ", coverage=", TextDisplay.value(pattern.coverage), " %)") +end +Base.show(io::IO, ::MIME"text/plain", pattern::WirePatterns.ScreenPattern) = show(io, pattern) diff --git a/src/parametricbuilder/traversal.jl b/src/parametricbuilder/traversal.jl new file mode 100644 index 000000000..408430610 --- /dev/null +++ b/src/parametricbuilder/traversal.jl @@ -0,0 +1,324 @@ +function _formulations(inner::AbstractFormulation) + return typeof(inner)[inner] +end + +function _formulations(source::Gridspace{Target}) where {Target} + formulations = Vector{Target}(undef, length(source)) + for (index, point) in enumerate(points(source)) + formulation = materialize(point) + formulation isa AbstractFormulation || throw(ArgumentError( + "formulation Gridspace produced $(typeof(formulation))", + )) + formulations[index] = formulation + end + return formulations +end + +function _formulations(source::AbstractGrid) + isempty(source) && return AbstractFormulation[] + collected = collect(source) + formulation_type = foldl( + typejoin, + (typeof(formulation) for formulation in collected) + ) + formulation_type <: AbstractFormulation || throw(ArgumentError( + "formulation Grid must contain completed AbstractFormulation values", + )) + formulations = Vector{formulation_type}(undef, length(collected)) + for (index, formulation) in enumerate(collected) + formulation isa AbstractFormulation || throw(ArgumentError( + "formulation Grid produced $(typeof(formulation))", + )) + formulations[index] = formulation + end + return formulations +end + +""" +$(TYPEDSIGNATURES) + +Evaluate established scalar problem-formulation dispatch for every completed +formulation in a collection. Owners may provide a more specific method to +share immutable lowering work. Every result must have one consistent concrete +type. +""" +function compute( + problem::AbstractProblemDefinition, + formulations::AbstractVector{<:AbstractFormulation}; + options::Union{NamedTuple,ComputationOptions} = ComputationOptions() +) + options = options isa NamedTuple ? ComputationOptions(options) : options + isempty(formulations) && throw(ArgumentError( + "a formulation collection must contain at least one formulation", + )) + first_result = compute(problem, first(formulations); options) + validate(typeof(first_result), AbstractResultSpace) + values = Vector{typeof(first_result)}(undef, length(formulations)) + values[1] = first_result + for index in 2:length(formulations) + value = compute(problem, formulations[index]; options) + typeof(value) === eltype(values) || throw(ArgumentError( + "batched computation produced inconsistent core result types", + )) + values[index] = value + end + return values +end + +""" +$(TYPEDSIGNATURES) + +Resolve the formulation source once, materialize every problem point once, +and evaluate their Cartesian product. Optional details are aligned with the +same problem-index-fastest storage order. + +# Returns + +- A named tuple containing `values`, `(problems, formulations)` axes, and + `ComputationDetails`, empty or containing `(points=records,)`. +""" +function traverse(problem::ParametricProblem, formulation) + progress = problem.space isa Gridspace{<:Engine.LineParametersProblem} && verbosity(problem.options, :progress) > 0 + started = progress ? time_ns() : UInt64(0) + previous = started + last_log = started + average_seconds = 0.0 + child_options = progress ? ComputationOptions(merge(problem.options.data, + (verbosity=merge(problem.options.data.verbosity, (progress=0,)),))) : problem.options + source_id = Commons.gridpoint_id().source_id + point_count = length(problem.space) + point_count > 0 || throw(ArgumentError( + "higher-order problem space must contain at least one core problem", + )) + + formulations = _formulations(formulation.inner) + formulation_count = length(formulations) + formulation_count > 0 || throw(ArgumentError( + "higher-order formulation space must contain at least one formulation", + )) + + point_source = points(problem.space) + first_item = iterate(point_source) + first_item === nothing && throw(DimensionMismatch( + "problem-space iteration ended before its declared cardinality", + )) + first_point, state = first_item + first_problem = materialize(first_point) + first_batch = [Engine.retain_gridpoint(value, + Commons.gridpoint_id(;source_id,problem_index=1,formulation_index=index)) + for (index,value) in enumerate(compute(first_problem, formulations; options = child_options))] + length(first_batch) == formulation_count || throw(DimensionMismatch( + "batched computation did not return one result per formulation", + )) + first_result = first(first_batch) + validate(typeof(first_result), ParametricResult) + values = Vector{typeof(first_result)}( + undef, + point_count * formulation_count + ) + @inbounds for formulation_index in 1:formulation_count + value = first_batch[formulation_index] + typeof(value) === eltype(values) || throw(ArgumentError( + "batched computation produced inconsistent core result types", + )) + values[1 + (formulation_index - 1) * point_count] = value + end + + retained = if formulation.options.data.retain_details + first_record = computation_details( + typeof(first(formulations)), + first_result + ) + records = Vector{typeof(first_record)}( + undef, + point_count * formulation_count + ) + @inbounds for formulation_index in 1:formulation_count + record = computation_details( + typeof(formulations[formulation_index]), + first_batch[formulation_index] + ) + typeof(record) === eltype(records) || throw(ArgumentError( + "batched computation produced inconsistent details record types", + )) + records[1 + (formulation_index - 1) * point_count] = record + end + records + else + nothing + end + + if progress + now = time_ns() + average_seconds = (now - previous) * 1e-9 / formulation_count + previous = now + if now - last_log >= 5_000_000_000 + @info "Parametric progress" _group=:progress problems_completed=1 problems=point_count completed=formulation_count total=point_count*formulation_count elapsed_seconds=(now-started)*1e-9 eta_hours=(point_count-1)*formulation_count*average_seconds/3600 + last_log = now + end + end + + for index in 2:point_count + item = iterate(point_source, state) + item === nothing && throw(DimensionMismatch( + "problem-space iteration ended before its declared cardinality", + )) + point, state = item + resolved_problem = materialize(point) + batch = [Engine.retain_gridpoint(value, + Commons.gridpoint_id(;source_id,problem_index=index,formulation_index=fi)) + for (fi,value) in enumerate(compute(resolved_problem, formulations; options = child_options))] + length(batch) == formulation_count || throw(DimensionMismatch( + "batched computation did not return one result per formulation", + )) + @inbounds for formulation_index in 1:formulation_count + core_result = batch[formulation_index] + typeof(core_result) === eltype(values) || throw(ArgumentError( + "higher-order computation produced inconsistent core result types", + )) + result_index = index + (formulation_index - 1) * point_count + values[result_index] = core_result + + if retained !== nothing + record = computation_details( + typeof(formulations[formulation_index]), + core_result + ) + typeof(record) === eltype(retained) || throw(ArgumentError( + "higher-order computation produced inconsistent details record types", + )) + retained[result_index] = record + end + end + if progress + now = time_ns() + interval = (now - previous) * 1e-9 / formulation_count + average_seconds = 0.2 * interval + 0.8 * average_seconds + previous = now + if now - last_log >= 5_000_000_000 + @info "Parametric progress" _group=:progress problems_completed=index problems=point_count completed=index*formulation_count total=point_count*formulation_count elapsed_seconds=(now-started)*1e-9 eta_hours=(point_count-index)*formulation_count*average_seconds/3600 + last_log = now + end + end + end + iterate(point_source, state) === nothing || throw(DimensionMismatch( + "problem-space iteration exceeded its declared cardinality", + )) + + retained_details = ComputationDetails(retained === nothing ? (;) : (points = retained,)) + axes = ( + problems = problem.space, + formulations = formulations + ) + return (; values, details = retained_details, axes) +end + +""" +$(TYPEDSIGNATURES) + +Compute one target-bearing scalar grid point. Core workflows may add a more +specific lowering method. The default method materializes the selected point +and computes its result. +""" +function compute( + point::Gridpoint{Target}, + formulation; + options::Union{NamedTuple,ComputationOptions} = ComputationOptions() +) where {Target <: AbstractProblemDefinition} + options = options isa NamedTuple ? ComputationOptions(options) : options + problem = materialize(point)::Target + return compute(problem, formulation; options) +end + +function compute(problem::ParametricProblem, formulation::Combinatorial; + modal=nothing, modal_options::Union{NamedTuple,ComputationOptions}=ComputationOptions()) + modal===nothing || return compute(problem,formulation, + LineCableModels.ModalAnalysisFormulation(modal);modal_options) + isempty(modal_options isa NamedTuple ? modal_options : modal_options.data) || + throw(ArgumentError("modal_options require a modal formulation")) + levels = verbosity(get(problem.options.data, :verbosity, (default=0,))) + progress = problem.space isa Gridspace{<:Engine.LineParametersProblem} && get(levels, :progress, levels.default) > 0 + logger = haskey(problem.options.data, :verbosity) ? VerbosityLogger(Logging.current_logger(), levels) : Logging.current_logger() + return Logging.with_logger(logger) do + started = progress ? time_ns() : UInt64(0) + progress && @info "Parametric computation started" _group=:progress problems=length(problem.space) + traversed = traverse(problem, formulation) + result = ParametricResult(formulation, traversed.values, traversed.axes, traversed.details) + progress && @info "Parametric computation completed successfully" _group=:progress completed=length(result) total=length(result) elapsed_seconds=(time_ns()-started)*1e-9 + result + end +end + +""" +$(TYPEDSIGNATURES) + +Evaluate a deterministic formulation `Gridspace` with default +[`Combinatorial`](@ref) settings. A scalar problem forms a singleton problem +axis. A problem `Gridspace` forms a Cartesian product with the formulations. + +# Keywords + +- `options=(;)`: options supplied to each core computation. + +# Returns + +- A [`ParametricResult`](@ref) indexed by problem and formulation, with no + retained traversal details. Select `Combinatorial` explicitly to customize + its settings. +""" +function compute( + problem::Union{AbstractProblemDefinition, Gridspace{<:AbstractProblemDefinition}}, + formulations::Gridspace{<:AbstractFormulation}; + options::Union{NamedTuple,ComputationOptions} = ComputationOptions(), + modal=nothing, modal_options::Union{NamedTuple,ComputationOptions}=ComputationOptions() +) + options = options isa NamedTuple ? ComputationOptions(options) : options + modal===nothing || return compute(problem,formulations, + LineCableModels.ModalAnalysisFormulation(modal);options,modal_options) + isempty(modal_options isa NamedTuple ? modal_options : modal_options.data) || + throw(ArgumentError("modal_options require a modal formulation")) + return compute(ParametricProblem(problem, options), Combinatorial(formulations)) +end + +function compute(problem::Gridspace{<:AbstractProblemDefinition}, + formulation::AbstractFormulation; + options::Union{NamedTuple,ComputationOptions}=ComputationOptions(), + modal=nothing, modal_options::Union{NamedTuple,ComputationOptions}=ComputationOptions()) + options = options isa NamedTuple ? ComputationOptions(options) : options + modal===nothing || return compute(problem,formulation, + LineCableModels.ModalAnalysisFormulation(modal);options,modal_options) + isempty(modal_options isa NamedTuple ? modal_options : modal_options.data) || + throw(ArgumentError("modal_options require a modal formulation")) + return compute(ParametricProblem(problem, options), Combinatorial(formulation)) +end + +""" +$(TYPEDSIGNATURES) + +Evaluate a deterministic formulation `Gridspace` with default +[`Combinatorial`](@ref) settings, retaining the existing `ParametricProblem` +and its core computation options. Return a [`ParametricResult`](@ref). +""" +function compute( + problem::ParametricProblem, + formulations::Gridspace{<:AbstractFormulation}; + modal=nothing, modal_options::Union{NamedTuple,ComputationOptions}=ComputationOptions() +) + modal===nothing || return compute(problem,formulations, + LineCableModels.ModalAnalysisFormulation(modal);modal_options) + isempty(modal_options isa NamedTuple ? modal_options : modal_options.data) || + throw(ArgumentError("modal_options require a modal formulation")) + return compute(problem, Combinatorial(formulations)) +end + +# A third positional argument composes a parametric study with modal analysis of +# each completed line-parameter result. +function compute(problem::ParametricProblem, + formulation::Union{Combinatorial,Gridspace{<:AbstractFormulation}}, + modal::Union{LineCableModels.ModalAnalysisFormulation, + Gridspace{<:LineCableModels.ModalAnalysisFormulation}}; + modal_options::Union{NamedTuple,ComputationOptions}=ComputationOptions()) + phase=compute(problem,formulation) + return compute(Gridspace{LineCableModels.ModalAnalysisProblem}(phase),modal; + options=modal_options) +end diff --git a/src/parametricbuilder/wirepatterns/WirePatterns.jl b/src/parametricbuilder/wirepatterns/WirePatterns.jl index b9f935017..ead83adc3 100644 --- a/src/parametricbuilder/wirepatterns/WirePatterns.jl +++ b/src/parametricbuilder/wirepatterns/WirePatterns.jl @@ -1,344 +1,26 @@ -module WirePatterns - -# ──────────────────────────────────────────────────────────────────────────── -# Public API -# ──────────────────────────────────────────────────────────────────────────── - -# export ScreenPattern, HexaPattern -export make_stranded, make_screened - -# ──────────────────────────────────────────────────────────────────────────── -# Types -# ──────────────────────────────────────────────────────────────────────────── - -""" - struct HexaPattern - -Result for a single design choice. - -Fields: -- `layers::Int` — number of concentric layers (1 = center only). -- `wires::Int` — total number of wires, N(L) = 1 + 3L(L-1). -- `wire_diameter_m::Float64` — strand diameter [m]. -- `total_area_m2::Float64` — summed metallic area [m²]. -- `awg::String` — AWG label from the table (informative). -""" -struct HexaPattern - layers::Int - wires::Int - wire_diameter_m::Float64 - total_area_m2::Float64 - awg::String -end - -""" - struct ScreenPattern - -Screen wires design. - -Fields: -- `wires::Int` — number of wires on the wire array (N). -- `wire_diameter_m::Float64` — strand diameter [m]. -- `lay_diameter_m::Float64` — laying diameter Dm [m]. -- `radius_m::Float64` — wire array centerline radius = (Dm + d)/2 [m]. -- `total_area_m2::Float64` — N * π/4 * d^2 [m²]. -- `coverage_pct::Float64` — 100 * N*d / (π*Dm*sinα) [%]. -- `awg::String` — AWG label from the table (informative). -""" -struct ScreenPattern - wires::Int - wire_diameter_m::Float64 - lay_diameter_m::Float64 - radius_m::Float64 - total_area_m2::Float64 - coverage_pct::Float64 - awg::String -end - -# ──────────────────────────────────────────────────────────────────────────── -# Utils -# ──────────────────────────────────────────────────────────────────────────── - -_wire_area(dw::Real) = (pi/4) * (dw^2) # area of one wire - -# ---- AWG exact formulas (solid wire) ---- -const _AWG_BASE = 92.0 -const _D0_MM = 0.127 # 0.005 in in mm -const _AREA0_MM2 = 0.012668 # (π/4)*0.127^2 -const _LN_BASE = log(_AWG_BASE) - -awg_to_d_mm(n::Real) = _D0_MM * (_AWG_BASE ^ ((36 - n)/39)) -awg_to_area_mm2(n::Real) = _AREA0_MM2 * (_AWG_BASE ^ ((36 - n)/19.5)) - -d_mm_to_awg(d_mm::Real) = 36 - 39 * (log(d_mm/_D0_MM) / _LN_BASE) -area_mm2_to_awg(A_mm2::Real) = 36 - 19.5 * (log(A_mm2/_AREA0_MM2) / _LN_BASE) - -function awg_label(n::Integer) - n == -3 && return "0000 (4/0)" - n == -2 && return "000 (3/0)" - n == -1 && return "00 (2/0)" - n == 0 && return "0 (1/0)" - return string(n) -end - -"Generate (label, diameter_m) for AWG n in [nmin, nmax]." -function awg_sizes(nmin::Integer = -3, nmax::Integer = 40) - out = Tuple{String, Float64}[] - @inbounds for n in nmin:nmax - d_m = awg_to_d_mm(n) / 1000.0 - push!(out, (awg_label(n), d_m)) - end - return out -end - -"Apply a compaction/fill factor to solid area to approximate stranded metallic CSA." -stranded_area_mm2(n::Real; fill_factor::Real = 0.94) = fill_factor * awg_to_area_mm2(n) - -# ──────────────────────────────────────────────────────────────────────────── -# Hexagonal strand patterns -# ──────────────────────────────────────────────────────────────────────────── - -# ---- wire-count constraints per target area (mm²) ---- -const _WIRE_RULES = Tuple{Int, Int, Union{Int, Nothing}}[ - (10, 6, 7), - (16, 6, 7), - (25, 6, 7), - (35, 6, 7), - (50, 6, 19), - (70, 12, 19), - (95, 15, 19), - (120, 15, 37), - (150, 15, 37), - (185, 30, 37), - (240, 30, 37), - (300, 30, 61), - (400, 53, 61), - (500, 53, 61), - (630, 53, 91), - (800, 53, 91), - (1000, 53, 91), -] - """ - make_stranded(target_area_m2::Real; nmin::Integer=-3, nmax::Integer=40) + LineCableModels.ParametricBuilder.WirePatterns -Compute hexagonal-pattern strand layouts that approximate or meet the target metallic cross-section, imposing allowed total-wire ranges by target area. - -Inputs: -- `target_mm2` — target metallic area [mm²]. -- `nmin`,`nmax` — AWG range to consider (default 4/0 … 40). - -Returns: -- `best_match` — within allowed N(L), minimize |A − target|. -- `min_layers` — within allowed N(L) and A ≥ target, minimize layers (tie: smallest excess, then smaller diameter). - Fallback: within allowed, pick largest A < target (tie: smaller L, then smaller diameter). -- `min_diam` — within allowed N(L) and A ≥ target, minimize diameter, then layers, then excess. - Fallback: within allowed, pick smallest diameter with largest A < target (then smallest L). +Estimate deterministic wire patterns and retain ranked patterns with their +geometric packing limits. """ -function make_stranded(target_mm2::Real; nmin::Integer = -3, nmax::Integer = 40) - @assert target_mm2 > 0 "Target cross-section must be positive." - @assert nmin <= nmax "nmin must be ≤ nmax." - - target_area_m2 = target_mm2 * 1e-6 # m² - # ---- hex geometry ---- - _hex_N(L::Int) = 1 + 3L*(L - 1) # total wires after L layers - _to_choice((dw, L, N, A, awg)) = HexaPattern(L, N, dw, A, awg) - - # Return (minN, maxN::Union{Int,Nothing}) for target in mm² - function _allowed_wires(target_mm2::Real) - for (thr, minN, maxN) in _WIRE_RULES - if target_mm2 <= thr - return (minN, maxN) - end - end - return (53, nothing) # > 1000 mm² -> min 53, no maximum - end - - @inline function _allowed_N(N::Int, minN::Int, maxN::Union{Int, Nothing}) - maxN === nothing ? (N >= minN) : (N >= minN && N <= maxN) - end - - # Allowed wire-count range from target (mm²) - minN, maxN = _allowed_wires(target_mm2) - - # AWG sizes (label, d_m) - sizes = awg_sizes(nmin, nmax) - @assert !isempty(sizes) "AWG range produced no sizes." - - # Build allowed candidates: (dw, L, N, A, awg) - candidates = Vector{Tuple{Float64, Int, Int, Float64, String}}() - for (awg, dw) in sizes - a1 = _wire_area(dw) - @inbounds for L in 1:300 - N = _hex_N(L) - if _allowed_N(N, minN, maxN) - A = N * a1 - push!(candidates, (dw, L, N, A, awg)) - end - if maxN !== nothing && N > maxN - break - end - end - end - @assert !isempty(candidates) "No allowed candidates under the imposed wire-count span." - - # ---- best_match: minimize |A - target| (tie: smaller dw, then smaller L) ---- - rank_keys = [(abs(A - target_area_m2), dw, L) for (dw, L, N, A, _) in candidates] - best_match = _to_choice(candidates[argmin(rank_keys)]) - if best_match.wires > 271 - @warn "Best match stranded pattern with a very high wire count ($(best_match.wires) wires). Consider revising the target cross-section or choosing a different wire configuration." - end - - # Split feasible/infeasible for next selectors - feas = filter(((dw, L, N, A, awg),)->A >= target_area_m2, candidates) - infeas = filter(((dw, L, N, A, awg),)->A < target_area_m2, candidates) - - # ---- min_layers ---- - if !isempty(feas) - # minimal layers, then minimal excess, then smaller diameter - keys_L = [(L, A - target_area_m2, dw) for (dw, L, N, A, _) in feas] - min_layers = _to_choice(feas[argmin(keys_L)]) - else - # fallback: closest from below (largest A), then minimal L, then smaller dw - keys_fb = [(-A, L, dw) for (dw, L, N, A, _) in infeas] - min_layers = _to_choice(infeas[argmin(keys_fb)]) - end - - # ---- min_diam ---- - if !isempty(feas) - # smallest diameter; for it, minimal layers; then smallest excess - sort!(feas, by = x -> (x[1], x[2], x[4] - target_area_m2)) # (dw asc, L asc, excess asc) - min_diam = _to_choice(first(feas)) - else - # fallback: smallest diameter with best undershoot; then minimal layers - sort!(infeas, by = x -> (x[1], -(x[4]), x[2])) # (dw asc, A desc, L asc) - min_diam = _to_choice(first(infeas)) - end +module WirePatterns - return (; best_match, min_layers, min_diam) -end +using DocStringExtensions: TYPEDEF, TYPEDFIELDS, TYPEDSIGNATURES -# ──────────────────────────────────────────────────────────────────────────── -# Screen (single wire array) patterns -# ──────────────────────────────────────────────────────────────────────────── +import ...LineCableModels: nominal """ - make_screened(A_req_m2::Real, Dm_m::Real; - alpha_deg::Real=15.0, coverage_min_pct::Real=85.0, - gap_frac::Real=0.0, min_wires::Int=3, extra_span::Int=8, - nmin::Integer=-3, nmax::Integer=40) - -Compute screen wire layouts that approximate or meet the target metallic cross-section, imposing: - - 1) CSA: N * (π/4) * d^2 ≥ A_req_m2 - 2) Cover: (N*d)/(π*Dm*sinα) * 100 ≥ coverage_min_pct - -while enforcing no-overlap wire array geometry (with optional clearance `gap_frac`). - -Arguments: -- `A_req_mm2` — required metallic cross-section [mm²]. -- `Dm_mm` — laying diameter (screen centerline) [mm]. -- `alpha_deg` — lay angle α in degrees (default 20°). -- `coverage_min_pct` — required geometric coverage (default 85%). -- `gap_frac` — extra clearance fraction in the no-overlap check (default 0). -- `min_wires` — lower bound on N to avoid degenerate “non-wire-array" cases (default 3; set 6 for stronger symmetry). -- `extra_span` — consider up to this many extra wires above the minimal requirement for better best_match search. -- `nmin`,`nmax` — AWG range to consider (default 4/0 … 40). - -Returns: -- `min_wires` — minimal N (≥ min_wires) that satisfies both CSA & coverage & geometry; - tie-break: smaller d, then smaller excess area. -- `min_diam` — smallest d that can satisfy both constraints; for it, minimal feasible N; - tie-break: smaller excess area. -- `best_match`— among all feasible combos, area closest to A_req_m2; tie: smaller N, then smaller d. +Return the maximum wire count admitted by one estimate geometry. """ -function make_screened(A_req_mm2::Real, Dm_mm::Real; - alpha_deg::Real = 15.0, coverage_min_pct::Real = 85.0, - gap_frac::Real = 0.0, min_wires::Int = 6, extra_span::Int = 8, - nmin::Integer = -3, nmax::Integer = 40, - coverage_max_pct::Real = 100.0, # NEW: cap coverage to a single layer - max_overshoot_pct::Real = 10.0, # NEW: optional cap on A overshoot (∞ to disable) - custom_diameters_mm::AbstractVector{<:Real} = Float64[]) - - @assert 0.0 < coverage_min_pct <= 100.0 - @assert coverage_max_pct >= coverage_min_pct - @assert max_overshoot_pct ≥ 0 - @assert A_req_mm2 > 0 - @assert Dm_mm > 0 - - A_req_m2 = A_req_mm2 * 1e-6 # m² - Dm_m = Dm_mm * 1e-3 # m - - # --- helpers --- - function _max_wires_single_layer(Dm::Real, d::Real; gap_frac::Real = 0.0) - s = d*(1 + gap_frac) / (Dm + d) - if !(0.0 < s < 1.0) - ; - return 0; - end - return max(0, floor(Int, pi / asin(s))) - end - _to_choice((N, d, Dm, A, cov, awg)) = ScreenPattern(N, d, Dm, 0.5*(Dm + d), A, cov, awg) - - α = deg2rad(alpha_deg) - sα = sin(α); - @assert sα > 0 - - # AWG sizes + optional customs - sizes = awg_sizes(nmin, nmax) - for d in custom_diameters_mm - push!(sizes, ("custom($(round(d; digits=3)) mm)", Float64(d))) - end - @assert !isempty(sizes) - - # Build candidates that satisfy BOTH constraints + geometry + coverage upper bound - candidates = Tuple{Int, Float64, Float64, Float64, Float64, String}[] # (N,d,Dm,A,cov,awg) - - for (awg, d) in sizes - a1 = _wire_area(d) - N_csa = ceil(Int, A_req_m2 / a1) - N_cov = ceil(Int, (coverage_min_pct/100.0) * (pi*Dm_m*sα) / d) - N_min = max(min_wires, N_csa, N_cov) - N_max = _max_wires_single_layer(Dm_m, d; gap_frac = gap_frac) - if N_max <= 0 || N_min > N_max - continue - end - - # Try N from N_min upward but reject coverage > coverage_max_pct and overshoot > max_overshoot_pct - upper = min(N_min + extra_span, N_max) - @inbounds for N in N_min:upper - A = N * a1 - cov = 100.0 * (N*d) / (pi*Dm_m*sα) - if cov > coverage_max_pct - break # for fixed d, cov grows linearly with N; larger N will also violate - end - if isfinite(max_overshoot_pct) - if A > A_req_m2 * (1 + max_overshoot_pct/100) - continue - end - end - push!(candidates, (N, d, Dm_m, A, cov, awg)) - end - end - - @assert !isempty(candidates) "No feasible screen with given CSA, Dm, α, coverage bounds, and geometry." - - # --- selectors (tweaked) --- - - # 1) min_wires: minimize N; tie → minimize |A−Areq|; then smaller d - keys_minN = [(N, abs(A - A_req_m2), d) for (N, d, _, A, _, _) in candidates] - min_wires = _to_choice(candidates[argmin(keys_minN)]) - - # 2) min_diam: smallest d; for it, minimal |A−Areq|; then minimal N - sort!(candidates, by = x -> (x[2], abs(x[4] - A_req_m2), x[1])) # (d asc, |ΔA| asc, N asc) - min_diam = _to_choice(first(candidates)) - - # 3) best_match: closest area to A_req; tie → smaller N, then smaller d - keys_best = [(abs(A - A_req_m2), N, d) for (N, d, _, A, _, _) in candidates] - best_match = _to_choice(candidates[argmin(keys_best)]) +function maxfill end - return (; min_wires, min_diam, best_match) -end +export WireEstimate, estimate_stranding, estimate_screen +public HexaPattern, ScreenPattern +include("types.jl") +include("gauges.jl") +include("stranded.jl") +include("screened.jl") -end # module +end # module WirePatterns diff --git a/src/parametricbuilder/wirepatterns/gauges.jl b/src/parametricbuilder/wirepatterns/gauges.jl new file mode 100644 index 000000000..37c7da88c --- /dev/null +++ b/src/parametricbuilder/wirepatterns/gauges.jl @@ -0,0 +1,77 @@ +_wire_area(diameter::Real) = (one(diameter) * pi / 4) * diameter^2 + +const _AWG_BASE = 92 +const _D0_MM = 0.127 +const _AREA0_MM2 = 0.012668 + +function awg_to_d_mm(number::Real) + oftype(float(number), _D0_MM) * + oftype(float(number), _AWG_BASE) ^ + ((oftype(float(number), 36) - number) / oftype(float(number), 39)) +end +function awg_to_area_mm2(number::Real) + oftype(float(number), _AREA0_MM2) * + oftype(float(number), _AWG_BASE) ^ + ((oftype(float(number), 36) - number) / oftype(float(number), 19.5)) +end + +"""Return the AWG number corresponding to `diameter` \\[mm\\].""" +function d_mm_to_awg(diameter::Real) + oftype(float(diameter), 36) - + oftype(float(diameter), 39) * + log(diameter / oftype(float(diameter), _D0_MM)) / + log(oftype(float(diameter), _AWG_BASE)) +end +"""Return the AWG number corresponding to solid metal `area` \\[mm²\\].""" +function area_mm2_to_awg(area::Real) + oftype(float(area), 36) - + oftype(float(area), 19.5) * + log(area / oftype(float(area), _AREA0_MM2)) / + log(oftype(float(area), _AWG_BASE)) +end + +function awg_label(number::Integer) + number == -3 && return "0000 (4/0)" + number == -2 && return "000 (3/0)" + number == -1 && return "00 (2/0)" + number == 0 && return "0 (1/0)" + return string(number) +end + +function awg_sizes(::Type{T}, awg_min::Integer = -3, awg_max::Integer = 40) where {T <: Real} + awg_min <= awg_max || throw(ArgumentError("awg_min must not exceed awg_max")) + return [(awg_label(number), convert(T, awg_to_d_mm(number)) / T(1000)) + for number in awg_min:awg_max] +end + +awg_sizes(awg_min::Integer = -3, awg_max::Integer = 40) = awg_sizes(Float64, awg_min, awg_max) + +""" +Apply a fill factor to solid area to approximate stranded metallic area. +""" +function stranded_area_mm2(number::Real; fill_factor::Real = 0.94) + factor, area = promote(float(fill_factor), float(awg_to_area_mm2(number))) + return factor * area +end + +const _WIRE_RULES = Tuple{Int, Int, Union{Int, Nothing}}[ + (10, 6, 7), (16, 6, 7), (25, 6, 7), (35, 6, 7), + (50, 6, 19), (70, 12, 19), (95, 15, 19), (120, 15, 37), + (150, 15, 37), (185, 30, 37), (240, 30, 37), (300, 30, 61), + (400, 53, 61), (500, 53, 61), (630, 53, 91), (800, 53, 91), + (1000, 53, 91) +] + +_hex_wires(layers::Int) = 1 + 3layers * (layers - 1) + +"""Return wire-count bounds for the requested metal `target_area` \\[mm²\\].""" +function _allowed_wires(target_area::Real) + for (threshold, minimum, maximum) in _WIRE_RULES + target_area <= threshold && return minimum, maximum + end + return 53, nothing +end + +function _allowed_wires(wires::Int, minimum::Int, maximum::Union{Int, Nothing}) + return maximum === nothing ? wires >= minimum : minimum <= wires <= maximum +end diff --git a/src/parametricbuilder/wirepatterns/screened.jl b/src/parametricbuilder/wirepatterns/screened.jl new file mode 100644 index 000000000..94e94246f --- /dev/null +++ b/src/parametricbuilder/wirepatterns/screened.jl @@ -0,0 +1,183 @@ +function ScreenPattern( + wires::Int, + diameter::T, + lay_diameter::T, + sine_angle::T, + awg::String +) where {T <: Real} + area = wires * _wire_area(diameter) + coverage = T(100) * wires * diameter / + (T(pi) * lay_diameter * sine_angle) + return ScreenPattern( + wires, diameter, lay_diameter, (lay_diameter + diameter) / T(2), + area, coverage, awg + ) +end + +function _screen_feasible( + pattern::ScreenPattern, + target_area, + coverage_min, + coverage_max, + overshoot_max +) + enough_area = pattern.total_area >= target_area + coverage_ok = coverage_min <= pattern.coverage <= coverage_max + T = typeof(target_area) + overshoot = T(100) * (pattern.total_area / target_area - one(T)) + overshoot_ok = !isfinite(overshoot_max) || overshoot <= overshoot_max + return enough_area && coverage_ok && overshoot_ok +end + +""" +$(TYPEDSIGNATURES) + +Estimate single-layer wire-screen patterns. + +# Arguments + +- `target_area`: required metallic cross-section \\[mm²\\]. +- `lay_diameter`: diameter beneath the wire layer \\[mm\\]. + +# Keywords + +- `lay_angle=15`: wire lay angle \\[degrees\\], with positive sine. +- `coverage_min=85`, `coverage_max=100`: circumferential coverage bounds \\[%\\]. +- `gap_frac=0`: additional wire clearance as a fraction of wire diameter. +- `min_wires=6`: minimum wire count. +- `extra_span=8`: additional wire counts examined above the required count. +- `awg_min=-3`, `awg_max=40`: inclusive AWG-number limits. +- `max_area_overshoot=10`: maximum excess metallic area relative to the target \\[%\\]. +- `wire_diameters=[]`: additional wire diameters \\[mm\\]. + +# Returns + +A [`WireEstimate`](@ref) whose stored lengths and areas use \\[m\\] and \\[m²\\]. + +Geometrically valid patterns must meet the requested area, coverage, and +overshoot bounds to make the result feasible. Otherwise the returned +[`WireEstimate`](@ref) contains ranked best-effort patterns and reasons. +""" +function estimate_screen( + target_area::Real, + lay_diameter::Real; + lay_angle::Real = 15, + coverage_min::Real = 85, + gap_frac::Real = 0, + min_wires::Integer = 6, + extra_span::Integer = 8, + awg_min::Integer = -3, + awg_max::Integer = 40, + coverage_max::Real = 100, + max_area_overshoot::Real = 10, + wire_diameters::AbstractVector{<:Real} = Float64[] +) + target_area > zero(target_area) || throw(DomainError( + target_area, "required cross-section must be positive" + )) + lay_diameter > zero(lay_diameter) || throw(DomainError( + lay_diameter, "laying diameter must be positive" + )) + zero(coverage_min) < coverage_min <= 100 || throw(DomainError( + coverage_min, "minimum coverage must be in (0, 100]" + )) + coverage_max >= coverage_min || throw(DomainError( + coverage_max, "maximum coverage must not be below its minimum" + )) + max_area_overshoot >= zero(max_area_overshoot) || throw(DomainError( + max_area_overshoot, "maximum overshoot must be nonnegative" + )) + gap_frac >= zero(gap_frac) || throw(DomainError( + gap_frac, "gap fraction must be nonnegative" + )) + min_wires >= 1 || throw(DomainError(min_wires, "minimum wires must be positive")) + extra_span >= 0 || throw(DomainError(extra_span, "extra span must be nonnegative")) + awg_min <= awg_max || throw(ArgumentError("awg_min must not exceed awg_max")) + all(>(0), wire_diameters) || throw(DomainError( + wire_diameters, "custom wire diameters must be positive" + )) + + custom_types = isempty(wire_diameters) ? () : (eltype(wire_diameters),) + T = promote_type( + typeof(float(target_area)), typeof(float(lay_diameter)), + typeof(float(lay_angle)), typeof(float(coverage_min)), + typeof(float(coverage_max)), typeof(float(max_area_overshoot)), + typeof(float(gap_frac)), custom_types... + ) + target_area = convert(T, target_area) * T(1e-6) + lay_diameter = convert(T, lay_diameter) * T(1e-3) + angle = deg2rad(convert(T, lay_angle)) + sine_angle = sin(angle) + sine_angle > zero(T) || throw(DomainError( + lay_angle, "lay angle must have a positive sine" + )) + coverage_min = convert(T, coverage_min) + coverage_max = convert(T, coverage_max) + overshoot_max = convert(T, max_area_overshoot) + gap = convert(T, gap_frac) + + sizes = awg_sizes(T, awg_min, awg_max) + append!(sizes, + [("custom($(round(diameter; digits=3)) mm)", convert(T, diameter) / T(1000)) + for diameter in wire_diameters]) + + geometric = ScreenPattern{T}[] + for (awg, diameter) in sizes + area = _wire_area(diameter) + required_by_area = ceil(Int, target_area / area) + required_by_coverage = ceil( + Int, coverage_min * T(pi) * lay_diameter * sine_angle / + (T(100) * diameter) + ) + required = max(Int(min_wires), required_by_area, required_by_coverage) + lay_radius = (lay_diameter + diameter) / T(2) + maximum_wires = maxfill( + ScreenPattern, lay_radius, diameter / T(2); gap_frac = gap + ) + upper = min(maximum_wires, required + Int(extra_span)) + if upper >= min_wires + append!(geometric, + [ScreenPattern(wires, diameter, lay_diameter, sine_angle, awg) + for wires in Int(min_wires):upper]) + elseif maximum_wires > 0 + push!(geometric, ScreenPattern( + maximum_wires, diameter, lay_diameter, sine_angle, awg + )) + end + end + isempty(geometric) && throw(ArgumentError( + "the supplied diameters cannot form even one wire on this laying radius", + )) + + feasible_patterns = filter( + pattern -> _screen_feasible( + pattern, target_area, coverage_min, coverage_max, overshoot_max + ), + geometric + ) + feasible = !isempty(feasible_patterns) + patterns = feasible ? feasible_patterns : geometric + sort!(patterns; + by = pattern -> ( + abs(pattern.total_area - target_area), pattern.wires, + pattern.wire_diameter + )) + + reasons = String[] + if !feasible + maximum_area = Base.maximum(pattern.total_area for pattern in geometric) + maximum_coverage = Base.maximum(pattern.coverage for pattern in geometric) + maximum_area < target_area && push!( + reasons, "available single-layer patterns do not reach the requested area" + ) + maximum_coverage < coverage_min && push!( + reasons, "available single-layer patterns do not reach minimum coverage" + ) + isempty(reasons) && push!( + reasons, "coverage or overshoot limits reject every geometric pattern" + ) + end + return WireEstimate( + target_area, patterns, feasible, feasible ? :feasible : :infeasible, reasons + ) +end diff --git a/src/parametricbuilder/wirepatterns/stranded.jl b/src/parametricbuilder/wirepatterns/stranded.jl new file mode 100644 index 000000000..6ed4bb4c9 --- /dev/null +++ b/src/parametricbuilder/wirepatterns/stranded.jl @@ -0,0 +1,94 @@ +""" +$(TYPEDSIGNATURES) + +Estimate hexagonally packed strand patterns for a metallic cross-section. + +# Arguments + +- `target_area`: required cross-sectional area of metal \\[mm²\\]. Stored areas use \\[m²\\]. + +# Keywords + +- `awg_min=-3`, `awg_max=40`: inclusive AWG-number limits. + +# Returns + +The returned [`WireEstimate`](@ref) retains ranked patterns, including those +below the target area. When no pattern reaches that area, the result has +`status == :infeasible`. +""" +function estimate_stranding(target_area::Real; awg_min::Integer = -3, awg_max::Integer = 40) + target_area > zero(target_area) || throw(DomainError( + target_area, "target cross-section must be positive" + )) + awg_min <= awg_max || throw(ArgumentError("awg_min must not exceed awg_max")) + + target_value = float(target_area) + T = typeof(target_value) + target_area = target_value * T(1e-6) + minimum, maximum_wires = _allowed_wires(target_value) + patterns = HexaPattern{T}[] + + for (awg, diameter) in awg_sizes(T, awg_min, awg_max) + area = _wire_area(diameter) + for layers in 1:300 + wires = _hex_wires(layers) + _allowed_wires(wires, minimum, maximum_wires) && push!( + patterns, + HexaPattern(layers, wires, diameter, wires * area, awg) + ) + maximum_wires !== nothing && wires > maximum_wires && break + end + end + isempty(patterns) && throw(ArgumentError( + "the AWG range and permitted wire counts produced no patterns", + )) + + sort!(patterns; + by = pattern -> ( + abs(pattern.total_area - target_area), pattern.wire_diameter, + pattern.layers + )) + feasible = any(pattern -> pattern.total_area >= target_area, patterns) + reasons = feasible ? String[] : + [ + "no permitted strand pattern reaches the requested metallic area", + ] + estimate = WireEstimate( + target_area, patterns, feasible, feasible ? :feasible : :infeasible, reasons + ) + estimate[:closest_area].wires > 271 && + @warn("The closest stranded pattern exceeds 271 wires.", + wires=estimate[:closest_area].wires,) + return estimate +end + +""" +$(TYPEDSIGNATURES) + +Return the maximum number of screen wires that fit on `lay_radius`. + +`wire_radius` and `lay_radius` are center-to-center geometric radii in the same +unit. `gap_frac` adds a fractional clearance between adjacent wires. +""" +function maxfill( + ::Type{ScreenPattern}, + lay_radius::Real, + wire_radius::Real; + gap_frac::Real = 0 +) + lay_radius > zero(lay_radius) || throw(DomainError( + lay_radius, "lay radius must be positive" + )) + wire_radius > zero(wire_radius) || throw(DomainError( + wire_radius, "wire radius must be positive" + )) + gap_frac >= zero(gap_frac) || throw(DomainError( + gap_frac, "gap fraction must be nonnegative" + )) + ratio = wire_radius * (one(gap_frac) + gap_frac) / lay_radius + nominal_ratio = float(nominal(ratio)) + zero(nominal_ratio) < nominal_ratio < one(nominal_ratio) || return 0 + count = pi / asin(nominal_ratio) + return max(0, floor(Int, count + 8eps(count))) +end diff --git a/src/parametricbuilder/wirepatterns/types.jl b/src/parametricbuilder/wirepatterns/types.jl new file mode 100644 index 000000000..e3347985e --- /dev/null +++ b/src/parametricbuilder/wirepatterns/types.jl @@ -0,0 +1,123 @@ +""" +$(TYPEDEF) + +A concentric, hexagonally packed strand pattern. + +$(TYPEDFIELDS) +""" +struct HexaPattern{T <: Real} + "Number of concentric wire layers, including the central wire." + layers::Int + "Total wire count." + wires::Int + "Diameter of each wire \\[m\\]." + wire_diameter::T + "Total metallic cross-section \\[m²\\]." + total_area::T + "AWG size label." + awg::String +end + +""" +$(TYPEDEF) + +A single-layer wire-screen pattern. + +$(TYPEDFIELDS) +""" +struct ScreenPattern{T <: Real} + "Wire count." + wires::Int + "Diameter of each wire \\[m\\]." + wire_diameter::T + "Diameter beneath the wire layer \\[m\\]." + lay_diameter::T + "Radius to the wire centers \\[m\\]." + radius::T + "Total metallic cross-section \\[m²\\]." + total_area::T + "Circumferential coverage \\[%\\]." + coverage::T + "AWG size or supplied-diameter label." + awg::String +end + +""" +$(TYPEDEF) + +Store ranked patterns from a wire-pattern search. + +For wire stranding, `feasible` means at least one pattern reaches the target metal +area. The ranked list can also contain undersized patterns. For screening, it +means at least one pattern satisfies the area, coverage and overshoot bounds, +and only those feasible patterns are retained. When none is feasible, the +geometrically valid alternatives remain ranked and `reasons` describes the limits. +Use `estimate[:closest_area]`, `estimate[:fewest_layers]`, `estimate[:fewest_wires]`, or +`estimate[:smallest_diameter]` to select a pattern. + +$(TYPEDFIELDS) +""" +struct WireEstimate{T <: Real, P} + "Requested metallic cross-section \\[m²\\]." + target_area::T + "Ranked wire patterns." + patterns::Vector{P} + "Whether at least one pattern meets the estimator's feasibility conditions." + feasible::Bool + "Feasibility label, either `:feasible` or `:infeasible`." + status::Symbol + "Explanations for unmet constraints." + reasons::Vector{String} + + function WireEstimate( + target_area::T, + patterns::Vector{P}, + feasible::Bool, + status::Symbol, + reasons::Vector{String} + ) where {T <: Real, P} + status in (:feasible, :infeasible) || throw(ArgumentError( + "wire-estimate status must be :feasible or :infeasible", + )) + feasible == (status === :feasible) || throw(ArgumentError( + "wire-estimate feasibility and status disagree", + )) + isempty(patterns) && throw(ArgumentError( + "a wire estimate must retain at least one pattern", + )) + return new{T, P}(target_area, patterns, feasible, status, reasons) + end +end + +Base.length(estimate::WireEstimate) = length(estimate.patterns) +Base.iterate(estimate::WireEstimate, state...) = iterate(estimate.patterns, state...) + +_area(pattern::Union{HexaPattern, ScreenPattern}) = pattern.total_area +_diameter(pattern::Union{HexaPattern, ScreenPattern}) = pattern.wire_diameter +_selected(estimate::WireEstimate, key) = argmin(key, estimate.patterns) + +Base.getindex(estimate::WireEstimate, selector::Symbol) = estimate[Val(selector)] + +function Base.getindex(estimate::WireEstimate, ::Val{:closest_area}) + _selected(estimate, pattern -> (abs(_area(pattern) - estimate.target_area), + _diameter(pattern))) +end +function Base.getindex(estimate::WireEstimate{<:Real, <:HexaPattern}, ::Val{:fewest_layers}) + _selected(estimate, pattern -> (pattern.layers, + abs(_area(pattern) - estimate.target_area), _diameter(pattern))) +end +function Base.getindex(estimate::WireEstimate, ::Val{:fewest_wires}) + _selected(estimate, pattern -> (pattern.wires, + abs(_area(pattern) - estimate.target_area), _diameter(pattern))) +end +function Base.getindex(estimate::WireEstimate, ::Val{:smallest_diameter}) + _selected(estimate, pattern -> (_diameter(pattern), + abs(_area(pattern) - estimate.target_area), pattern.wires)) +end + +function Base.getindex(::WireEstimate, ::Val{selector}) where {selector} + throw(ArgumentError( + "unknown wire-estimate selector :$selector; use :closest_area, :fewest_layers, " * + ":fewest_wires, or :smallest_diameter", + )) +end diff --git a/src/performance.jl b/src/performance.jl new file mode 100644 index 000000000..76e05b746 --- /dev/null +++ b/src/performance.jl @@ -0,0 +1,83 @@ +""" +$(TYPEDSIGNATURES) + +Repeat a caller-supplied computation and collect its retained scan measurements. +The callable must request `timing=true` through ordinary computation options. +No additional stopwatch surrounds the callable. + +# Arguments + +- `f`: zero-argument callable returning line parameters, an ordinary batch, + a parametric or linear-error result, or a Monte Carlo result. + +# Keywords + +- `samples=1`: positive number of measured repetitions. Boolean values are rejected. +- `warmup=0`: nonnegative number of discarded repetitions. Boolean values are rejected. + +# Returns + +- `(; result, timings)`: the last scientific result and detached timing payloads + for each measured repetition. Batches and parametric and linear-error results keep + their value order. Monte Carlo measurements keep population and trial order. + Reused results retain empty timing records. + +# Notes + +Owned scans retain wall, GC, compilation, and recompilation durations in seconds, +and Julia allocation volume in bytes. Native scans retain caller wall time and +their backend measurements in seconds. These describe complete frequency scans, +excluding shared input construction, measurement attachment, and callbacks. +The callable controls verbosity, seeds, callbacks, and reuse. Options remain unchanged. + +# Errors + +- `ArgumentError`: invalid counts, unsupported results or absent requested timing. +- Exceptions from `f` propagate immediately without retry. + +# Examples + +```julia +measurement = LineCableModels.benchmark(; samples=3, warmup=1) do + compute(problem, formulation; options=(timing=true, verbosity=(default=0,))) +end +``` +""" +function benchmark(f; samples = 1, warmup = 0) + samples isa Integer && !(samples isa Bool) && 0 < samples <= typemax(Int) || + throw(ArgumentError("samples must be a positive integer representable as Int")) + warmup isa Integer && !(warmup isa Bool) && 0 <= warmup <= typemax(Int) || + throw(ArgumentError("warmup must be a nonnegative integer representable as Int")) + for _ in 1:warmup + f() + end + # Projection consumes completed outputs only. It never visits input spaces. + function measurement(value) + if value isa Union{LineParameters, MonteCarloResult} + haskey(details(value).data, :timing) || throw(ArgumentError( + "benchmark requires timing=true in the computation options")) + recorded = details(value).data.timing + return value isa MonteCarloResult ? + [NamedTuple[deepcopy(record) for record in population] + for population in recorded] : deepcopy(recorded) + elseif value isa + Union{AbstractVector{<:LineParameters}, ParametricResult, LinearErrorResult} + isempty(value) && + throw(ArgumentError("benchmark requires nonempty timing=true results")) + return NamedTuple[measurement(core) for core in value] + end + throw(ArgumentError("benchmark requires supported line-parameter results with timing=true; got $(typeof(value))")) + end + result = f() + first_timing = measurement(result) + timings = first_timing isa NamedTuple ? Vector{NamedTuple}(undef, samples) : + Vector{typeof(first_timing)}(undef, samples) + timings[1] = first_timing + for index in 2:samples + # Drop the previous large result before starting another repetition. + result = nothing + result = f() + timings[index] = measurement(result) + end + return (; result, timings) +end diff --git a/src/plotbuilder/PlotBuilder.jl b/src/plotbuilder/PlotBuilder.jl index f09b89755..118b8a8f0 100644 --- a/src/plotbuilder/PlotBuilder.jl +++ b/src/plotbuilder/PlotBuilder.jl @@ -1,62 +1,20 @@ -module PlotBuilder - -using Base: @kwdef - -import ..UnitHandler: Units, QuantityTag, get_label, get_symbol, display_unit, scale_factor -import ..Commons: PhaseDomain, ModalDomain, domain - -export make_render, RenderSpec - -# Submodule `BackendHandler` -include("backendhandler/BackendHandler.jl") -using .BackendHandler - -# Submodule `PlotUIComponents` - WILL BE DEPRECATED SOON -include("plotuicomponents/PlotUIComponents.jl") -using .PlotUIComponents - -include("types.jl") -include("traits.jl") -include("axisspec.jl") -include("parse.jl") -include("seriesspec.jl") -include("viewspec.jl") -include("pagespec.jl") - -# Submodule `UIComponents` -include("uicomponents/UIComponents.jl") - -include("plotspecs.jl") - - - """ - make_render(::Type{S}, obj; kwargs...) where {S<:AbstractPlotSpec} + PlotBuilder -High-level API: from domain object + keyword arguments to a RenderSpec. - -Checks that the object type is compatible with `dispatch_on(S)` and then -runs: - - parse_kwargs(S, obj; kwargs...) → raw - resolve_input(S, raw) → nt - make_pages(S, nt) → figs - -`make_pages` returns a vector of PageSpec values; `make_render` -wraps them into a RenderSpec that the UI layer will later assemble into -actual windows/layouts. +Provide plotting functions and live figure handles for the Makie extension. +Load a Makie backend to draw figures. Returned handles expose the Makie objects +for further editing. """ +module PlotBuilder -function make_render(::Type{S}, obj; kwargs...) where {S <: AbstractPlotSpec} - Tdispatch = dispatch_on(S) - obj isa Tdispatch || - Base.error("Spec $(S) cannot dispatch on $(typeof(obj)); expected $(Tdispatch)") +using DocStringExtensions: TYPEDEF, TYPEDFIELDS - raw = parse_kwargs(S, obj; kwargs...) - norm = resolve_input(S, raw) - pags = make_pages(S, norm) # ::Vector{PageSpec} +export UIPlot, plot, preview, show_material_scale, export_svg +export figurelegend!, panellegend!, figuretitle!, paneltitle! +export figurecolorbars!, axisscale!, resetview!, addwidget!, removewidget! +export plotwindow, materialcolors, materialscale! - return RenderSpec(S, pags) -end +include("handle.jl") +include("interfaces.jl") end # module PlotBuilder diff --git a/src/plotbuilder/axisspec.jl b/src/plotbuilder/axisspec.jl deleted file mode 100644 index ef507132f..000000000 --- a/src/plotbuilder/axisspec.jl +++ /dev/null @@ -1,83 +0,0 @@ - -""" - make_axes(::Type{S}, nt::NamedTuple) where {S<:AbstractPlotSpec} - -Build axes for spec `S` using quantity tags stored in `nt` as -fields `x_quantity`, `y_quantity`, `z_quantity` (when applicable). - -Returns: - (xaxis = AxisSpec or nothing, - yaxis = AxisSpec or nothing, - zaxis = AxisSpec or nothing) -""" -function make_axes(::Type{S}, nt::NamedTuple) where {S <: AbstractPlotSpec} - dims = geom_axes(S) - - xaxis = nothing - yaxis = nothing - zaxis = nothing - - for dim in dims - qfield = Symbol(dim, :_quantity) # :x_quantity, :y_quantity, :z_quantity - q = getproperty(nt, qfield) # expected to be a QuantityTag - - u = axis_unit(S, q, dim) - ax = make_axis(S, dim, q, u) - - if dim === :x - xaxis = ax - elseif dim === :y - yaxis = ax - elseif dim === :z - zaxis = ax - else - Base.error("Unsupported axis dim $(dim) in geom_axes for $(S)") - end - end - - return (xaxis = xaxis, yaxis = yaxis, zaxis = zaxis) -end - -""" -Build the final axis label from quantity + units. - -Default pattern: "Label [symbol]" if units are non-empty. -Uses `get_label(q::QuantityTag)` and `get_label(u::Units)` from UnitHandler. -""" -function axis_label(q::QuantityTag, u::Units) - base = get_label(q) # human-readable quantity name - usym = get_label(u) # unit symbol string, e.g. "Ω/km" - return isempty(usym) ? base : string(base, " [", usym, "]") -end - -""" -Build a AxisSpec for spec `S`, axis dim `dim`, quantity `q`, units `u`. -""" -function make_axis(::Type{S}, - dim::Symbol, - q::QuantityTag, - u::Units) where {S <: AbstractPlotSpec} - - lab = axis_label(q, u) - dims = enable_logscale(S) - sc = dim in dims ? :log10 : :linear - return AxisSpec(dim, q, u, lab, sc) -end - -# High-level builder: dim only (static semantics) -function make_axis(::Type{S}, ::Val{dim}) where {S <: AbstractPlotSpec, dim} - q = axis_quantity(S, Val(dim)) - u = axis_unit(S, q, dim) - return make_axis(S, dim, q, u) -end - -# High-level builder: dim + semantic code (e.g. :R/:L/:C/:G) -function make_axis( - ::Type{S}, - ::Val{dim}, - ::Val{qty}, -) where {S <: AbstractPlotSpec, dim, qty} - q = axis_quantity(S, Val(dim), Val(qty)) - u = axis_unit(S, q, dim) - return make_axis(S, dim, q, u) -end diff --git a/src/plotbuilder/backendhandler/BackendHandler.jl b/src/plotbuilder/backendhandler/BackendHandler.jl deleted file mode 100644 index 3d9765f7e..000000000 --- a/src/plotbuilder/backendhandler/BackendHandler.jl +++ /dev/null @@ -1,241 +0,0 @@ -""" -Makie backend handler for LineCableModels. - -Design goals: -- Precompile in any environment (never touch GL/WGL at load-time). -- Default to CairoMakie as a safe backend. -- Offer a single `set_backend!` API (no user `using` needed). -- In headless (e.g., Literate → Documenter), display PNG inline. - -How to wire it (minimal integration): - -1) Add this helper to your package module once: - - include(joinpath(@__DIR__, "..", "@INPROGRESS", "makie_backend_alt.jl")) - using .BackendHandler - -2) In Makie-based preview entrypoints, ensure a backend is active: - - # Use caller keyword `backend::Union{Nothing,Symbol}` if you keep it - BackendHandler.ensure_backend!(backend === nothing ? :cairo : backend) - -3) For GL interactive windows without directly referencing GLMakie: - - if BackendHandler.current_backend_symbol() == :gl - if (scr = BackendHandler.gl_screen("Title")) !== nothing - display(scr, fig) - else - display(fig) - end - else - display(fig) - end - -4) For docs/headless builds (Literate/Documenter) PNG inline display: - - # in your final display branch - BackendHandler.renderfig(fig) - -5) Let users select backends interactively (no extra imports): - - BackendHandler.set_backend!(:gl) # or :wgl, :cairo - -Notes: -- No `@eval import` anywhere; backends are loaded via `Base.require` using PkgId. -- Calls into newly loaded modules go through `Base.invokelatest` to avoid - world-age issues. -""" -module BackendHandler - -using Makie -using UUIDs -using ...Utils: is_headless - -export set_backend!, - ensure_backend!, current_backend_symbol, renderfig, with_backend, make_screen - -# --------------------------------------------------------------------------- -# Backend registry -# --------------------------------------------------------------------------- - -const _BACKENDS = Dict{Symbol, Tuple{UUID, String}}( - :cairo => (UUID("13f3f980-e62b-5c42-98c6-ff1f3baf88f0"), "CairoMakie"), - :gl => (UUID("e9467ef8-e4e7-5192-8a1a-b1aee30e663a"), "GLMakie"), - :wgl => (UUID("276b4fcb-3e11-5398-bf8b-a0c2d153d008"), "WGLMakie"), -) - -_pkgid(sym::Symbol) = begin - tup = get(_BACKENDS, sym, nothing) - tup === nothing && throw( - ArgumentError("Unknown backend: $(sym). Valid: $(collect(keys(_BACKENDS)))"), - ) - Base.PkgId(tup[1], tup[2]) -end - -"""Return true if a backend package exists in the environment.""" -backend_available(backend::Symbol) = Base.find_package(last(_BACKENDS[backend])) !== nothing - -# Track the last activated backend symbol (separate from Makie.internal state) -const _active_backend = Base.RefValue{Symbol}(:none) - -# --------------------------------------------------------------------------- -# Activation core (lazy, world-age safe) -# --------------------------------------------------------------------------- - -function _activate_backend!(backend::Symbol; allow_interactive_in_headless::Bool = false) - if is_headless() && backend != :cairo && !allow_interactive_in_headless - @warn "Headless environment: forcing :cairo instead of $(backend)." - return _activate_backend!(:cairo; allow_interactive_in_headless) - end - - pid = _pkgid(backend) - # Load the backend module into Julia's module world; idempotent if already loaded - mod = Base.require(pid) - # Call `activate!` safely with world-age correctness - Base.invokelatest(getproperty(mod, :activate!)) - _active_backend[] = backend - return backend -end - -"""Ensure a backend is active. Defaults to :cairo the first time.""" -function ensure_backend!(backend::Union{Nothing, Symbol} = nothing) - if backend === nothing - return _active_backend[] == :none ? _activate_backend!(:cairo) : _active_backend[] - else - return set_backend!(backend) - end -end - -"""Activate a specific backend (:cairo, :gl, :wgl). - -In headless, :gl/:wgl requests fall back to :cairo unless `force=true`. -No `using` required by callers. -""" -function set_backend!(backend::Symbol; force::Bool = false) - haskey(_BACKENDS, backend) || throw( - ArgumentError("Unknown backend: $(backend). Valid: $(collect(keys(_BACKENDS)))"), - ) - if backend != :cairo && !backend_available(backend) - if !is_headless() - @warn "Backend $(last(_BACKENDS[backend])) not in environment; using :cairo." - end - - return _activate_backend!(:cairo) - end - return _activate_backend!(backend; allow_interactive_in_headless = force) -end - -"""Symbol of the current Makie backend (:cairo, :gl, :wgl, :unknown, :none).""" -function current_backend_symbol() - try - nb = nameof(Makie.current_backend()) - nb === :CairoMakie && return :cairo - nb === :GLMakie && return :gl - nb === :WGLMakie && return :wgl - return :unknown - catch - return :none - end -end - -""" - with_backend(f, backend; force=false) - -Temporarily activate `backend`, run `f()`, then restore the previous backend. - -Intended for export workflows (switch to CairoMakie, render/save, restore). -Restoration is best-effort: if the previous backend can’t be determined, it’s skipped. -""" -function with_backend(f::Function, backend::Symbol; force::Bool = false) - prev = current_backend_symbol() - prev_ok = - prev in (:cairo, :gl, :wgl) ? prev : - (_active_backend[] in (:cairo, :gl, :wgl) ? _active_backend[] : :none) - - ensure_backend!(backend) - - try - return f() - finally - if prev_ok != :none && prev_ok != current_backend_symbol() - try - set_backend!(prev_ok; force = force) - catch e - @warn "Failed to restore backend $(prev_ok)" exception=( - e, - catch_backtrace(), - ) - end - end - end -end - -""" - make_screen(title; backend=current_backend_symbol(), kwargs...) -> screen | nothing - -Create a backend-specific screen/window handle for interactive display. - -Currently: -- `:gl` returns `GLMakie.Screen(; title=...)` (loaded lazily). -- other backends return `nothing`. -""" -function make_screen(title::AbstractString; - backend::Symbol = current_backend_symbol(), - kwargs..., -) - backend == :gl || return nothing - mod = Base.require(_pkgid(:gl)) - ctor = getproperty(mod, :Screen) - return Base.invokelatest(ctor; title = String(title), kwargs...) -end - -""" - make_screen(backend, title; kwargs...) -> screen | nothing - -Convenience overload. -""" -make_screen(backend::Symbol, title::AbstractString; kwargs...) = - make_screen(title; backend = backend, kwargs...) - - - -"""Display a figure appropriately in headless docs or interactive sessions. - -- Headless: returns `DisplayAs.Text(DisplayAs.PNG(fig))` if `DisplayAs` exists; - otherwise attempts to rasterize via CairoMakie and returns nothing. -- Interactive: calls `display(fig)` and returns its result. -""" -function renderfig(fig) - if is_headless() - try - D = Base.require( - Base.PkgId(UUID("0b91fe84-8a4c-11e9-3e1d-67c38462b6d6"), "DisplayAs"), - ) - return D.Text(D.PNG(fig)) - catch - try - ensure_backend!(:cairo) - cm = Base.require(_pkgid(:cairo)) - savef = getproperty(cm, :save) - io = IOBuffer() - Base.invokelatest(savef, io, fig) - return nothing - catch - return nothing - end - end - else - return display(fig) - end -end - -const FIG_NO = Base.Threads.Atomic{Int}(1) -next_fignum() = Base.Threads.atomic_add!(FIG_NO, 1) -reset_fignum!(n::Int = 1) = (FIG_NO[] = n) - -# Backend activation is deliberately lazy. Activating CairoMakie from `__init__` -# evaluates into Makie's module while packages are restoring on Julia 1.12, -# which breaks incremental compilation. Public rendering entry points already -# call `ensure_backend!` before using a backend. - -end # module diff --git a/src/plotbuilder/handle.jl b/src/plotbuilder/handle.jl new file mode 100644 index 000000000..0874df98d --- /dev/null +++ b/src/plotbuilder/handle.jl @@ -0,0 +1,81 @@ +""" +$(TYPEDEF) + +Return the small handle assembled around a native Makie figure by a +LineCableModels plotting call. + +`figure`, `title`, every element of `axes`, every value in `controls`, `legend`, +every value in `panel_legends`, and every element of `colorbars` are the live +Makie objects used for display. They remain owned by the caller: mutating them +or adding native Makie plots changes the displayed figure and the state saved +by [`export_svg`](@ref). + +The remaining fields are private addon state and export defaults. + +$(TYPEDFIELDS) +""" +mutable struct UIPlot{F, C} + "Native Makie figure owned by the caller." + figure::F + "Figure-wide native Makie title label, or `nothing`." + title::Any + "Native Makie axes in display order." + axes::Vector{Any} + "Native Makie controls keyed by their public purpose." + controls::C + "Native Makie legend, or `nothing`." + legend::Any + "Panel-scoped native Makie legends keyed by logical panel identity." + panel_legends::Dict{Any, Any} + "Native Makie colorbars in display order." + colorbars::Vector{Any} + "Status observable shared with the native shell and managed callbacks." + status::Any + "Private Makie objects and callback subscriptions, with presentation state." + plot_state::Any + "Default base filename used by [`export_svg`](@ref)." + export_name::String + "Default SVG theme." + export_theme::Symbol + "Whether toolbar exports should be opened after writing." + open_export::Bool +end + +function UIPlot( + figure, + axes; + title = nothing, + controls = Dict{Symbol, Any}(), + legend = nothing, + panel_legends = Dict{Any, Any}(), + colorbars = (), + status = nothing, + plot_state = nothing, + export_name::AbstractString = "linecablemodels_plot", + export_theme::Symbol = :default, + open_export::Bool = true +) + export_theme in (:default, :publication) || throw(ArgumentError( + "export_theme must be :default or :publication", + )) + isempty(strip(export_name)) && throw(ArgumentError("export_name cannot be empty")) + panel_legends isa AbstractDict || throw(ArgumentError( + "panel_legends must be a dictionary keyed by logical panel identity", + )) + return UIPlot( + figure, + title, + Any[axes...], + controls, + legend, + Dict{Any, Any}(panel_legends), + Any[colorbars...], + status, + plot_state, + String(export_name), + export_theme, + open_export + ) +end + +Base.summary(io::IO, ::UIPlot) = print(io, "Native Makie plot") diff --git a/src/plotbuilder/interfaces.jl b/src/plotbuilder/interfaces.jl new file mode 100644 index 000000000..f6c942733 --- /dev/null +++ b/src/plotbuilder/interfaces.jl @@ -0,0 +1,402 @@ +""" + plot(observed::ObservedResult, selection=nothing; ydata=nothing, kwargs...) + plot(observed::AbstractVector{<:ObservedResult}, selection=nothing; ydata=nothing, kwargs...) + plot(completed_result, selection=nothing; ydata=nothing, kwargs...) + +Present retained scientific quantities with a loaded Makie backend. Completed +numerical results are conveniences: they construct `ObservedResult` objects and +call the same public observed-input method. `PlotBuilder.plot` and +`LineCableModels.plot` are the same function. + +`selection` and `ydata` are alternative spellings of the same request. Supplying +both is an error. Requests retain `@observe` syntax and original coordinates. +An existing observation supplies values, units, descriptions, uncertainty, and +scientific groups. + +# Selection and acquisition + +Raw conveniences accept line, series, shunt, cable-constant, parametric, and UQ +results, ordinary supported tuples or vectors, named collections, and report +artifacts. Standalone series and shunt inputs also accept a frequency vector before +the selection. Raw references become separate atomic observations. Report +artifacts forward their observed results and their observed reference. + +Raw-only acquisition keywords are `clip`, `atol`, and `frequencies`. Single +primary requests use the observation owner's pair completion. Statistical +products do not trigger it. Observed inputs reject new clipping decisions, +thresholds, or replacement sample coordinates. + +`units`, `length_unit`, `quantity_units`, and `frequency_unit` also apply to +retained inputs, including reports. The existing `ObservedResult(existing; ...)` +operation re-expresses compatible units once before drawing. Omitted options +preserve recorded units, masks, errors, timings, and uncertainty dependencies. +`freq_unit` is a spelling of `frequency_unit`. Supplying both is an error. +`plot(report; ydata=(R,), length_unit=:base)` displays its retained +result and reference curves per meter without rebuilding the report. + +`problem` and `formulations` select original recorded identities. `band` selects +saved comparison samples through the observation owner, retaining each trace's +own coordinates and reference association. + +`overlay=:auto` chooses the overlaid dimension after filtering and before +equivalent-result grouping. One modal vector uses selected modes as curves +when `layout` is omitted or `(1,1)`. A larger explicit layout or several +gridpoints uses mode panels and gridpoint curves. Matrices use coefficient +panels and gridpoint curves. `overlay=:gridpoints` always uses selected +coordinates as panels. `overlay=:coordinates` uses one panel per selected +gridpoint, with physical coordinates as curves. `overlay=:rows` requires matrix +quantities: selected columns become panels and selected rows become curves. +For Tv/Ti, panel titles identify modes and legends identify conductors. Each +gridpoint, including a reference or an equivalent result, gets separate figures. +Columns flow left to right, then top to bottom, restarting pagination for each +point. Figure titles use compact gridpoint descriptions. A reference counts as a +selected gridpoint. Positional `series_labels` and `series_attributes` address +the overlaid dimension. Mixed figure families with incompatible positional +lengths require separate calls. + +# Layout and native presentation + +- `layout=nothing` resolves nominal panel capacity for each figure family: + its selected matrix row and column span, or a near-square flow arrangement. + An explicit `(rows, columns)` supplies a positive nominal capacity. With matrix + gridpoint overlays, automatic pages start at their selected minimum coordinate. + Explicit layouts anchor block membership at original coordinate `(1,1)`. + Empty exterior tracks are removed. Internal selection holes and original + coefficient identities remain. +- Each quantity and statistical meaning has separate figure families. With matrix + gridpoint overlays, a full 3×3 matrix at `layout=(2,2)` has four pages with + extents `(2,2)`, `(2,1)`, `(1,2)`, `(1,1)` per quantity. `layout=(1,1)` produces + nine pages per quantity. + Explicit diagonal products paginate compactly with original `(i,i)` identities. + With `overlay=:rows`, automatic capacity uses the selected column count per + gridpoint. An explicit layout is the shared block capacity for every point. + Each page contains columns from one point. Panel addresses + are `(original_point_position, original_column)`, with a reference appended + after the input points. Row styles retain original row identities across pages + and points. Positional labels and attributes follow the selected row order. +- `fig_size` is the initial reference size of the complete nominal capacity. + `figure=(size=...,)` takes precedence. Managed figures fit their decorated + occupied content. Residual pages retain the same initial data-frame dimensions, + including when no full-capacity page is emitted. Native resizing remains local. +- Assembly products use categorical points and uncertainty intervals, with a + first-seen union of recorded assembly names. Scalars/vectors without physical + coordinates use honest element-index views. Full modal matrices retain every + selected coefficient, including zero and residual off-diagonals. +- `series_labels`, `reference`, and `series_attributes` control the overlaid + trace identity and native appearance. Attributes accept one NamedTuple or an + aligned tuple or vector. + Result slots are assigned before filtering and adding a reference. References default to black solid curves and hollow circles. +- `errorbar_sampling` defaults to `:staggered` for multiple displayed series and + `:all` for one. Full uncertainty support controls limits. Explicit curve + markers and categorical or scalar points use every original sample. +- `xscale`, `yscale`, `xlabel`, `ylabel`, native limits, ticks and formatters, `axis=(;)`, + and `figure=(;)` configure native objects. Constructor groups beat shared + attributes. Per-series overrides beat shared series settings. Later native + edits retain authority. Unknown attributes fail with a diagnostic. +- Frequency X defaults to adaptive `:log10`. Other numeric dimensions default + to linear. Adaptive log uses stable signed log when visible support or bounds + contain zero or negative values. Its reference magnitude is the smallest finite + nonzero magnitude in eligible samples, enabled uncertainty endpoints and + explicit bounds, in displayed units (1 if none exist). The reference remains fixed during zooming. Reapplying `:log10` selects it from current data. + The native `log10` function remains strict. + Categorical axes retain native conversion and have no X-log toggle. +- Titles use `title`, `title_prefix`, `figure_title`, `title_attributes`, and + `panel_titles`. Positional panel titles bind before pagination. Dictionary and + function selections retain original panel identities. +- Legends use `legend_position`, `legend_title`, `legend_attributes`, + `legend_cap`, and `panel_legends`. Multiple/explicitly labelled result + series default to a bottom legend. One unlabelled series has none. + `legend_cap=0.5` accepts a finite real number excluding Boolean values in `(0,1]`. + It caps top and bottom legend height or side legend width relative to the + associated data area. The other dimension also fits within that area. + An inside legend uses the height cap. Excess entries are replaced by `(...)` + and restored when space returns. Every curve remains plotted. If even the + ellipsis and title cannot fit, the legend is hidden until space returns. +- `colorbar_position`, `colorbar_group_attributes`, `colorbar_attributes`, and + `guide_gap=8` place existing scale content. + Native `halign`/`valign` supply symbolic or fractional alignment. +- `guide_spacing=12` sets minimum spacing between neighboring complete guides + in logical pixels. A `(rowgap=..., colgap=...)` NamedTuple controls each + direction. Omitted constructor components default to 12. This is independent + of plot-to-guide `guide_gap`, figure padding, and legend-entry spacing. +- `colorbar_group_attributes=(layout=nothing, rowgap=nothing, colgap=nothing)` + controls the scale group's cells and minimum internal gaps. Explicit positive + integer capacity fills row-major. Unused tracks are omitted. Automatic layout + is one row at top and bottom and one column at left and right, explicit guide slots, + or standalone main content. Gaps inherit `guide_spacing` when omitted or + `nothing`. `colorbar_attributes.vertical` orients each bar independently. +- `backend`, `display_plot=true`, `controls=true`, `widgets=()`, + `export_theme=:default`, and `open_export=true` control display and UI behavior. + Widget callables receive the final live handle once per figure. + [`axisscale!`](@ref) and [`resetview!`](@ref) are also available with controls hidden. + +Numerical axes share size-aware ticks, engineering multipliers, and relative +near-constant padding. Scale changes preflight the complete page and preserve +orthogonal views and configured bounds. Rendering uses the values, uncertainty, +and availability recorded by the observation owner. +Changes to guides, titles or widgets refit the affected outer window around its current +frames. Closing a display window leaves its retained handle reusable. + +# Returns + +One [`UIPlot`](@ref), or an ordinary `Vector{UIPlot}` for multiple figures. Its +figure, axes, guides, controls, and status remain live native objects. + +# Examples + +```julia +using LineCableModels +using LineCableModels: plot +using CairoMakie +frequency = [1.0, 10.0, 100.0] +impedance = reshape(complex.([1.0, 2.0, 3.0], [2.0, 3.0, 4.0]), 1, 1, :) +raw = LineParameters(impedance, impedance .* 1e-6, frequency) +r_request = @observe R[:, :, :] +pair = (@observe(R[:, :, :]), @observe(L[:, :, :])) +a = plot(raw; ydata=(r_request,), clip=true, length_unit=:kilo) +o = ObservedResult(raw, (r_request,); complete_pairs=true, + clip=true, length_unit=:kilo) +b = plot(o; ydata=(r_request,), length_unit=:base) +c = plot(raw; ydata=pair, layout=(1,1)) +d = plot(ObservedResult(raw, pair); ydata=pair, layout=(1,1)) +``` + +`c` and `d` each contain separate R and L figures. +""" +function plot end + +function plot(args...; kwargs...) + throw(ArgumentError( + "Plotting is optional. Load CairoMakie, GLMakie, or WGLMakie before calling plot.", + )) +end + +""" + preview(source; kwargs...) + +Preview a cable design, a collection of cable designs, or a cable system with +a loaded Makie backend. + +# Keywords + +- `display_dielectric_pattern=true`: fill insulating regions with sparse + diagonal marks over their material color. Applies to all three preview routes. +- `earth_model=nothing`: static earth model for a system preview. Horizontal + strata retain their physical depths while their visible coverage follows the + axis view. Vertical strata are not rendered. +- `display_surface_gradient=true`: for a system with horizontal earth, add a light blue sky. Its color is strongest at the upper axis limit and fades toward the white or transparent background at the earth surface `z=0` \\[m\\]. The fade stretches + with the view and is hidden when the view lies entirely underground. This decoration is independent of material properties. +- `zoom_factor=nothing`: initial system-view span multiplier. The reset + control restores that initial view. +- `colorbar_position=:bottom`: place the material scales in one horizontal + strip below the preview. Horizontal bars keep their property labels on the + left, independently of group placement. +- `guide_gap=(8,8,24,8)`: clearance in logical pixels between plot decorations + and guides, ordered left, right, bottom, top. A scalar sets all four sides. +- `colorbar_group_attributes`: group `layout`, `rowgap`, and `colgap`, with + native group alignment and explicit outer `margin`. Bar orientation and + dimensions belong to `colorbar_attributes`. See [`plot`](@ref) for the shared + placement rules and sibling spacing through `guide_spacing=12`. + +# Returns + +- One [`UIPlot`](@ref), or an ordinary vector when a collection exceeds `layout` + capacity. Collection panels retain original integer indices. Material ranges + are shared across pages. `size` supplies the initial reference dimensions. + `figure.size` takes precedence. The finished window fits the decorated panels + while preserving physical aspect, limits, and each panel's established frame. + Heterogeneous panels may retain necessary internal row and column space. + +# Notes + +Material colors retain nominal physical properties. Magnetic tint progresses +from indigo to magenta on a logarithmic relative-permeability range. Earth uses +its own logarithmic resistivity palette. Dielectric marks use native Makie +pattern tiles, including in SVG/PDF exports. +""" +function preview end + +function preview(args...; kwargs...) + throw(ArgumentError( + "Plotting is optional. Load CairoMakie, GLMakie, or WGLMakie before calling preview.", + )) +end + +""" + show_material_scale(; kwargs...) + +Display the three independently defined material color schemes as a compact +reference figure. Use [`materialscale!`](@ref) to place any selected scheme in a +caller-owned Makie layout. Main-content placement defaults to one column, +independently of bar orientation. The shared `colorbar_group_attributes` and +`guide_spacing` options also apply here. +""" +function show_material_scale end + +function show_material_scale(args...; kwargs...) + throw(ArgumentError( + "Plotting is optional. Load CairoMakie, GLMakie, or WGLMakie before calling show_material_scale.", + )) +end + +""" + export_svg(plot::UIPlot; path=nothing, theme=nothing, open_file=nothing) + +Save the current live Makie figure in `plot` as SVG through CairoMakie and +return the absolute output path. `theme` may be `:default` or `:publication`. +The SVG retains the current zoom and pan without resetting the interactive view. + +Load CairoMakie explicitly before exporting. For an interactive GLMakie window +with SVG export, import both backends and select `backend=:gl` when plotting. +The SVG button is created only when CairoMakie is already loaded. Loading it +later enables this function on existing plots. Recreate a plot to add its button. +Export preserves the active backend and restores the live figure state. The toolbar displays file errors in the window's status row. Direct calls throw the +corresponding exception. An unloaded CairoMakie renderer raises `ArgumentError` +before filesystem or figure changes. +""" +function export_svg end + +""" + figurelegend!(plot::UIPlot; position, title, max_fraction, legend_labels, kwargs...) + +Update the figure legend from the shell's native series groups. Omitted options +preserve current state. `position=nothing` detaches and hides the guide. Native +`halign`, `valign`, margins, and style attributes remain editable. Removal and +restoration retain native styles and series visibility. `max_fraction` uses +the same `(0,1]` size limit as `plot(...; legend_cap=0.5)` and defaults +to `0.5` when creating a guide. Figure legends use the combined panel footprint. + +`guide_spacing` updates the shared figure-wide minimum sibling spacing. A scalar +sets both directions. A partial `(rowgap=..., colgap=...)` update preserves the +other current component. Values are finite, nonnegative, non-Boolean real +numbers in logical pixels. Existing panel legends inherit this setting. +""" +function figurelegend! end + +""" + panellegend!(plot::UIPlot, panel; kwargs...) + +Create or replace a native Makie legend scoped to one logical plot panel. +`panel` may be the stable panel identity returned by a recipe or its compatible +grid position. `max_fraction` bounds this legend against its own panel data +area. It inherits figure-wide `guide_spacing`. No per-panel spacing +override is accepted. +""" +function panellegend! end + +""" + figuretitle!(plot::UIPlot, title; kwargs...) + +Create, replace, or remove the figure-wide native Makie title. Pass `nothing` +to remove it. +""" +function figuretitle! end + +""" + paneltitle!(plot::UIPlot, panel, title) + +Set the native axis title for one logical plot panel. Pass `nothing` to clear +it. +""" +function paneltitle! end + +""" + plotwindow(callback; title, figure_title=nothing, size=(800, 400), kwargs...) + +Build the standard Makie shell, pass its caller-owned content `GridLayout` to +`callback`, and return a [`UIPlot`](@ref). The callback uses ordinary Makie and +is not constrained by a renderer-independent plot specification. +Numeric axes share scale, reset and export controls. Native `axis=(...)` +attributes explicitly override callback-created axes. Otherwise their native +construction settings are retained. Shared series and `figure=(...)` attributes +follow the same rules as [`plot`](@ref). +""" +function plotwindow end + +""" + materialcolors(property, [range]; alpha=1.0) + +Construct one reusable material color scheme. Palette selection is separate +from [`materialscale!`](@ref), which only renders a supplied scheme. +""" +function materialcolors end + +""" + materialscale!(position, scheme; kwargs...) + +Place one native Makie color scale at `position`. `scheme` supplies one label, +colormap, limits, and tick definition. +""" +function materialscale! end + +""" + axisscale!(p::UIPlot, dimension::Symbol, scale; panel=nothing) + +Set one displayed coordinate scale on all eligible numeric axes, or on the +original identity selected by `panel`. `:linear` selects the identity scale. +`:log10` adapts to signed support, while the native `log10` function requires +strictly positive support. `:pseudolog10` explicitly selects stable signed log. +Adaptive signed log maps `v` to `sign(v)*log10(1+abs(v)/s)`, where `s` is the +smallest finite nonzero magnitude in eligible visible samples, enabled +uncertainty endpoints and explicit bounds, in displayed units. `s=1` when +none exist. Explicit `:pseudolog10` retains `s=1`. Tick labels remain physical +values. Reapplying `:log10` updates `s`. Zooming and resetting limits retain it. +Selecting `:linear` restores the identity mapping without changing data or +observation clipping. +Preflight covers the complete selection before mutation and preserves the +orthogonal view and configured limits. Return `p`. +""" +function axisscale! end + +""" + resetview!(p::UIPlot; panel=nothing, x::Bool=true, y::Bool=true) + +Refit selected automatic view dimensions to current visible support, including +full enabled uncertainty intervals. Preserve configured full or partial limits. +`panel=nothing` selects every panel. Another value selects its original identity. +The Boolean keywords `x=true` and `y=true` select dimensions. Return `p`. +""" +function resetview! end + +""" + addwidget!(builder, p::UIPlot, key::Symbol; event=nothing, callback=nothing, success=nothing) + +Construct one custom native control with `builder(p, slot)` and register it +under a unique nonstandard Symbol `key`. Supply both `event` and `callback`, or +neither. `event(control)` returns the event observable. Actual notifications call +`callback(p, value)` once and may set `success` on `p.status`. Return the native +control. Figures constructed with `controls=false` reject widget additions. +""" +function addwidget! end + +""" + removewidget!(p::UIPlot, key::Symbol) + +Remove a custom widget, its native subtree, and its owned event subscriptions. +Release and repack its toolbar slot without changing other controls. Standard +and unknown keys are rejected. Return `p`. +""" +function removewidget! end + +""" + figurecolorbars!(p::UIPlot; position, group_attributes, native_bar_attributes...) + +Update the placement and native attributes of the figure's retained color scales. +Omitted arguments preserve current state. `position=nothing` removes displayed +scales while retaining their configuration. `group_attributes` merges by field +with current settings: `layout=(rows,columns)` fills complete scale items +row-major, and `layout=nothing` restores placement-based arrangement. Explicit +layout survives side and bar-orientation changes. `rowgap`/`colgap` are minimum +separations between complete item rows and columns. `nothing` restores inheritance +from the shared `guide_spacing`. Alignment and explicit outer margins retain +their native meanings. One item has zero exterior spacing. + +`guide_spacing` updates the same figure-wide setting as [`figurelegend!`](@ref). +Native `vertical`, `width`, `height`, labels and ticks configure individual bars. +Layout changes retain the native Colorbar objects and edits. Hidden items retain +their handles and supplied order without reserving empty group tracks. Invalid +prospective settings fail before changing the displayed arrangement. + +Return the current native colorbar collection. +""" +function figurecolorbars! end diff --git a/src/plotbuilder/pagespec.jl b/src/plotbuilder/pagespec.jl deleted file mode 100644 index 29f95dc8d..000000000 --- a/src/plotbuilder/pagespec.jl +++ /dev/null @@ -1,459 +0,0 @@ -""" - make_pages(::Type{S}, nt, views) where {S<:AbstractPlotSpec} - -Packs ViewSpec values into PageSpec payloads. - -Default behavior: -- a single PageSpec is created for this spec call, -- `layout` is chosen based on `figure_layout(S)` trait. - -Specs that want multiple OS windows or more complex layout policies may -override this method. -""" -function make_pages( - ::Type{S}, - nt::NamedTuple, - views::Vector{ViewSpec}, -) where {S <: AbstractPlotSpec} - layout = figure_layout(S) # :windows, :grid, ... - - # Do not materialize plots if length == 1. - # Upstream grouping/materialization stays intact, but the final PageSpec[] is empty. - if !isempty(views) - p1 = first(views) - if !isempty(p1.series) - ds1 = first(p1.series) - - ds1.xdata === nothing && Base.error( - "Broken plot payload for spec $(S): xdata is `nothing`.", - ) - - n = length(ds1.xdata) - if n <= 1 - @warn "Skipping plot for spec $(S): sample length is $(n) (need ≥ 2)." - return PageSpec[] - end - end - end - - fig_kwargs = nt.renderer - figsize = default_figsize(S) - fig_title = default_title(S, nt) - - fig = PageSpec(fig_title, figsize, layout, views, fig_kwargs) - return PageSpec[fig] -end - -# Determine if, for spec S and resolved input nt, the leaf seen by axis_slice / -# make_series behaves as "scalar" (numeric/complex) or as a NamedTuple that -# should be exploded at the figure / view level. -function is_leaf(::Type{S}, nt::NamedTuple) where {S <: AbstractPlotSpec} - dims_raw = geom_axes(S) - dims = dims_raw isa Tuple ? dims_raw : (dims_raw,) - - for dim in dims - kfield = select_field(S, Val(dim)) - kfield === nothing && continue - - if haskey(nt, kfield) - # A concrete field Symbol is pinned in the resolved input; axis_slice - # will unwrap the NamedTuple and return numeric data. - return true - else - # Spec uses select_field on this axis but no field was chosen yet; - # axis_slice will return a vector of NamedTuples. - return false - end - end - - # No axis uses select_field at all → grammar never unwraps fields. - return true -end - -# Decide high-level figure mode given spec S and resolved input nt. -function resolve_group_mode(::Type{S}, nt::NamedTuple) where {S <: AbstractPlotSpec} - - trait_mode = grouping_mode(S) # Symbol - if trait_mode !== :auto - return trait_mode - end - - idx_raw = index_keys(S) - idx = idx_raw isa Tuple ? idx_raw : (idx_raw,) - - # Matrix-like selector indices (pure selectors, never ranged) - has_i = :i in idx - has_j = :j in idx - has_matrix = has_i || has_j - - i_defined = has_i && haskey(nt, :i) - j_defined = has_j && haskey(nt, :j) - - - if is_leaf(S, nt) - if !has_matrix - return :single - end - - all_pinned = (!has_i || i_defined) && (!has_j || j_defined) - if all_pinned - return :single - else - return :overlay_ij - end - else - if !has_matrix - return :overlay_fields - end - - all_pinned = (!has_i || i_defined) && (!has_j || j_defined) - if all_pinned - return :overlay_fields - else - return :per_ij_overlay_fields - end - end -end - -# Infer matrix dimensions (Ni, Nj) from any axis container that carries the -# i/j selector structure. Falls back to (1,1) if no matrix indices are used. -function matrix_size(::Type{S}, nt::NamedTuple) where {S <: AbstractPlotSpec} - idx_raw = index_keys(S) - idx = idx_raw isa Tuple ? idx_raw : (idx_raw,) - - has_i = :i in idx - has_j = :j in idx - - Ni = 1 - Nj = 1 - - (!has_i && !has_j) && return Ni, Nj - - dims_raw = geom_axes(S) - dims = dims_raw isa Tuple ? dims_raw : (dims_raw,) - obj = nt.obj - - for dim in dims - # skip if this axis has no selector in nt (e.g. z unused) - !haskey(nt, dim) && continue - - datakey = getfield(nt, dim) - datakey isa Symbol || continue - - arr = container_array(S, obj, dim, datakey) - - arr isa AbstractArray || continue - - if has_i && size(arr, 1) > 1 - Ni = size(arr, 1) - end - if has_j && ndims(arr) >= 2 && size(arr, 2) > 1 - Nj = size(arr, 2) - end - - if (!has_i || Ni > 1) && (!has_j || Nj > 1) - break - end - end - - return Ni, Nj -end - -# For specs where source data is a NamedTuple (select_field used but no field -# chosen yet), infer which axis carries the NamedTuple leaf and what its -# field keys are, using axis_slice to get a vector of NamedTuples. -function get_fields( - ::Type{S}, - nt::NamedTuple, - axes::NamedTuple, -) where {S <: AbstractPlotSpec} - dims_raw = geom_axes(S) - dims = dims_raw isa Tuple ? dims_raw : (dims_raw,) - - # Find the first axis that uses select_field - dim_field = nothing - kfield = nothing - for dim in dims - kf = select_field(S, Val(dim)) - kf === nothing && continue - dim_field = dim - kfield = kf - break - end - - if dim_field === nothing || kfield === nothing - Base.error("get_fields: no axis uses select_field for spec $(S).") - end - - # axis descriptor - axis = - dim_field === :x ? axes.xaxis : - dim_field === :y ? axes.yaxis : - axes.zaxis - - axis === nothing && - Base.error( - "get_fields: axis $(dim_field) has no AxisSpec for spec $(S).", - ) - - # axis_slice will return a vector of NamedTuples in the "NamedTuple leaf" modes - vec = axis_slice(S, nt, axis, Val(dim_field)) - isempty(vec) && - Base.error( - "get_fields: empty data along axis $(dim_field) for spec $(S); cannot infer NamedTuple fields.", - ) - - leaf = first(vec) - leaf isa NamedTuple || - Base.error( - "get_fields: expected NamedTuple leaf for spec $(S) axis $(dim_field), got $(typeof(leaf)).", - ) - - field_keys = collect(keys(leaf)) - return dim_field, kfield, field_keys -end - -""" - is_modal(obj) - -Return `true` iff `domain(obj)` is a modal-like tag. -""" -@inline is_modal(x) = (D = domain(x); D !== nothing && D <: ModalDomain) - -# -------------------------------------------------------------------------- -# Index pair iterator: PhaseDomain vs ModalDomain -# -------------------------------------------------------------------------- - -""" - index_pairs(::Type{S}, nt) where {S<:AbstractPlotSpec} - -Return the list of \\((i,j)\\) index pairs that this spec should materialize -for the given resolved input `nt`. - -Semantics: - -- Phase-like domain (default, or `domain(nt.obj) === nothing`): - * If :i is in `index_keys(S)`: - - If `nt` pins `i`, use only that value. - - Otherwise, use `1:Ni` where `Ni` is inferred from `matrix_size(S, nt)`. - * If :j is in `index_keys(S)`: - - Same, using `nj` / `Nj`. - - Result: full rectangular coverage over the active ranges. - -- ModalDomain AND both :i and :j are in `index_keys(S)` AND neither is pinned in `nt`: - * Let `(Ni, Nj) = matrix_size(S, nt)` and `N = min(Ni, Nj)`. - * Return only diagonal pairs: `(1,1), (2,2), ..., (N,N)`. - -- If the user pins `i` and/or `j`, user intent takes precedence regardless - of domain tag. -""" -function index_pairs( - ::Type{S}, - nt::NamedTuple, -) where {S <: AbstractPlotSpec} - idx_raw = index_keys(S) - idx = idx_raw isa Tuple ? idx_raw : (idx_raw,) - - has_i = :i in idx - has_j = :j in idx - - Ni, Nj = matrix_size(S, nt) - - i_defined = has_i && haskey(nt, :i) - j_defined = has_j && haskey(nt, :j) - - obj = nt.obj - modal = is_modal(obj) - - # Modal diagonal semantics: - # - both :i and :j are matrix selectors, - # - neither is pinned by the user or defaults, - # - domain is ModalDomain (or subtype). - if modal && has_i && has_j && !i_defined && !j_defined - N = min(Ni, Nj) - pairs = Vector{Tuple{Int, Int}}(undef, N) - @inbounds for k in 1:N - pairs[k] = (k, k) - end - return pairs - end - - # Phase-like / generic semantics (or user-pinned case in modal) - I_range = - if has_i - i_defined ? (nt.i:nt.i) : (1:Ni) - else - 1:1 - end - - J_range = - if has_j - j_defined ? (nt.j:nt.j) : (1:Nj) - else - 1:1 - end - - pairs = Tuple{Int, Int}[] - @inbounds for i in I_range - for j in J_range - push!(pairs, (i, j)) - end - end - - return pairs -end - - -""" - make_pages(::Type{S}, nt) where {S<:AbstractPlotSpec} - -Top-level figure builder for spec `S`. - -Decides a high-level figure mode based on: -- whether the leaf behaves as scalar or NamedTuple (via select_field traits), -- whether index selectors :i and :j are present in `index_keys(S)`, -- whether :i and :j are defined or free in the resolved input `nt`. - -It then delegates to `make_pages(::Type{S}, ::Val{mode}, nt, axes)` where -`mode` is one of: -- :single → single atom, no i/j or field expansion -- :overlay_ij → scalar leaf, some of :i/:j free → overlay all (i,j) -- :overlay_fields → NamedTuple leaf, fixed (i,j) → overlay all fields -- :per_ij_overlay_fields → NamedTuple leaf, some of :i/:j free → one ViewSpec - per (i,j), overlaying all fields in each view. -""" -function make_pages( - ::Type{S}, - nt::NamedTuple, -) where {S <: AbstractPlotSpec} - axes = make_axes(S, nt) - mode = resolve_group_mode(S, nt) - return make_pages(S, Val(mode), nt, axes) -end - -function make_pages( - ::Type{S}, - ::Val{:single}, - nt::NamedTuple, - axes::NamedTuple, -) where {S <: AbstractPlotSpec} - series = make_series(S, nt, axes) - views = make_views(S, nt, axes, series) - return make_pages(S, nt, views) -end - -function make_pages( - ::Type{S}, - ::Val{:overlay_ij}, - nt::NamedTuple, - axes::NamedTuple, -) where {S <: AbstractPlotSpec} - idx_raw = index_keys(S) - idx = idx_raw isa Tuple ? idx_raw : (idx_raw,) - - has_i = :i in idx - has_j = :j in idx - - all_series = SeriesSpec[] - - for (i, j) in index_pairs(S, nt) - nt_ij = nt - if has_i - nt_ij = merge(nt_ij, (; i = i)) - end - if has_j - nt_ij = merge(nt_ij, (; j = j)) - end - - series_ij = make_series(S, nt_ij, axes) - append!(all_series, series_ij) - end - - views = make_views(S, nt, axes, all_series) - return make_pages(S, nt, views) -end - -function make_pages( - ::Type{S}, - ::Val{:overlay_fields}, - nt::NamedTuple, - axes::NamedTuple, -) where {S <: AbstractPlotSpec} - _, kfield, field_keys = get_fields(S, nt, axes) - - all_series = SeriesSpec[] - - for fk in field_keys - nt_fk = merge(nt, (; kfield => fk)) - series_fk = make_series(S, nt_fk, axes) - append!(all_series, series_fk) - end - - views = make_views(S, nt, axes, all_series) - return make_pages(S, nt, views) -end - -function make_pages( - ::Type{S}, - ::Val{:per_ij_overlay_fields}, - nt::NamedTuple, - axes::NamedTuple, -) where {S <: AbstractPlotSpec} - idx_raw = index_keys(S) - idx = idx_raw isa Tuple ? idx_raw : (idx_raw,) - - has_i = :i in idx - has_j = :j in idx - - _, kfield, field_keys = get_fields(S, nt, axes) - - views = ViewSpec[] - - for (i, j) in index_pairs(S, nt) - nt_ij = nt - if has_i - nt_ij = merge(nt_ij, (; i = i)) - end - if has_j - nt_ij = merge(nt_ij, (; j = j)) - end - - series_ij = SeriesSpec[] - - for fk in field_keys - nt_ij_fk = merge(nt_ij, (; kfield => fk)) - series_fk = make_series(S, nt_ij_fk, axes) - append!(series_ij, series_fk) - end - - # Populate view key with whatever matrix indices this spec actually uses. - key = - if has_i && has_j - (; i = i, j = j) - elseif has_i - (; i = i) - elseif has_j - (; j = j) - else - (;) - end - - title = default_title(S, nt_ij) - - view = ViewSpec( - axes.xaxis, - axes.yaxis, - axes.zaxis, - title, - series_ij, - key, - ) - - push!(views, view) - end - - return make_pages(S, nt, views) -end - - diff --git a/src/plotbuilder/parse.jl b/src/plotbuilder/parse.jl deleted file mode 100644 index ca11e1218..000000000 --- a/src/plotbuilder/parse.jl +++ /dev/null @@ -1,537 +0,0 @@ - -# split_kwargs – purely “what did the user say?” -function split_kwargs( - ::Type{S}, - kwargs::NamedTuple, - input_keys::Tuple, - renderer_keys::Tuple, - idx::Tuple, - dims::Tuple, -) where {S <: AbstractPlotSpec} - - # AxisSpec selector keys: (:x, :y, :z) - select_fields = dims - - semantic_keys = (input_keys..., idx..., select_fields...) - allowed = (semantic_keys..., renderer_keys...) - - spec_pairs = Tuple(filter(((k, _),) -> k in semantic_keys, pairs(kwargs))) - renderer_pairs = Tuple(filter(((k, _),) -> k in renderer_keys, pairs(kwargs))) - for k in keys(kwargs) - k in allowed || @warn "Unknown plot keyword for $(S): :$(k)" - end - - spec = NamedTuple(spec_pairs) - renderer = NamedTuple(renderer_pairs) - - return spec, renderer -end - -# merge_defaults – “how does this spec fill in the blanks?” -function merge_defaults( - ::Type{S}, - obj, - spec::NamedTuple, - renderer::NamedTuple, -) where {S <: AbstractPlotSpec} - - idefault = input_defaults(S, obj) - bdefault = renderer_defaults(S, obj) - - spec_merged = merge(idefault, spec) - renderer_merged = merge(bdefault, renderer) - - return spec_merged, renderer_merged -end - -# normalize_indices – enforce Int vs range-capable -function normalize_indices( - ::Type{S}, - spec::NamedTuple, - idx_keys::Tuple{Vararg{Symbol}}, - ranged_keys::Tuple{Vararg{Symbol}}, -) where {S <: AbstractPlotSpec} - - # No index keys → nothing to normalize - isempty(idx_keys) && return spec - - out = spec - - for k in idx_keys - is_ranged = k in ranged_keys - - if is_ranged - # Sample-like index (typically :k, optionally :l) - # Default to full range when not provided at all. - v = get(out, k, Colon()) - - (v isa Int || - v isa AbstractUnitRange{<:Int} || - v isa Colon) || - Base.error( - "Index $(k) for spec $(S) must be Int, AbstractUnitRange{<:Int} or `:`; " * - "got $(typeof(v)).", - ) - - out = merge(out, NamedTuple{(k,)}((v,))) - else - # Selector indices (:i, :j, ...) – only normalized if explicitly present. - # No defaults are invented for these. - if haskey(out, k) - v = getfield(out, k) - v isa Int || Base.error( - "Index $(k) for spec $(S) must be Int when provided; got $(typeof(v)).", - ) - out = merge(out, NamedTuple{(k,)}((v,))) - end - end - end - - return out -end - - -# sanity check selectors of datasources – ensure sources for xdata/ydata/... exist and are Symbols -function verify_selectors( - ::Type{S}, - spec::NamedTuple, - dims::Tuple, -) where {S <: AbstractPlotSpec} - - for d in dims - val = get(spec, d, nothing) - val === nothing && - Base.error("Missing axis selector $(d) for spec $(S) after defaults") - val isa Symbol || - Base.error( - "AxisSpec selector $(d) must be a Symbol, got $(typeof(val)) for spec $(S)", - ) - end - - return -end - -function container_array( - ::Type{S}, - obj, - dim::Symbol, - datakey::Symbol, -) where {S <: AbstractPlotSpec} - - container = data_container(S, Val(dim)) - - if container === nothing - hasproperty(obj, datakey) || - Base.error( - "For spec $(S), axis $(dim) expects obj.$(datakey), " * - "but $(typeof(obj)) has no such field.", - ) - return getproperty(obj, datakey) - else - container isa Symbol || - Base.error( - "data_container(::Type{$(S)}, Val($(dim))) must be Symbol or nothing; got $(typeof(container))", - ) - - hasproperty(obj, container) || - Base.error( - "data_container(::Type{$(S)}, Val($(dim))) = :$(container), " * - "but $(typeof(obj)) has no field :$(container).", - ) - - parent = getproperty(obj, container) - - if parent isa AbstractDict - haskey(parent, datakey) || - Base.error( - "Container field :$(container) for $(S) has no key :$(datakey) for axis $(dim).", - ) - return parent[datakey] - elseif parent isa NamedTuple && haskey(parent, datakey) - return parent[datakey] - elseif hasproperty(parent, datakey) - return getproperty(parent, datakey) - else - try - return parent[datakey] - catch - Base.error( - "Container field :$(container) of type $(typeof(parent)) " * - "does not provide data for key :$(datakey) for axis $(dim) in $(S).", - ) - end - end - end -end - -# normalize_shapes – centralized structural sanity -# container resolution, -# i/j bounds, -# sample length alignment. -# adjusted to respect the axis-level data_container(::Type{S}, ::Val{dim}) contract: -function verify_shapes( - ::Type{S}, - obj, - spec::NamedTuple, - dims::Tuple, - idx_keys::Tuple, -) where {S <: AbstractPlotSpec} - - # AxisSpec → datakey mapping (:x → :f, :y → :R, etc.) - datakeys = Dict{Symbol, Symbol}() - for d in dims - datakeys[d] = getfield(spec, d) # verified by verify_selectors - end - - # Index presence semantics: - # - i/j are selector indices: present iff field exists in spec NT - # - k is sample-like only if it is in ranged_keys(S) - rk = ranged_keys(S) - - has_i = (:i in idx_keys) && haskey(spec, :i) - has_j = (:j in idx_keys) && haskey(spec, :j) - has_k = (:k in rk) # sample-like dimension iff ranged_keys(S) contains :k - - i_val = has_i ? spec.i : nothing - j_val = has_j ? spec.j : nothing - k_val = has_k ? spec.k : Colon() # normalized in normalize_indices - - local function _check_index(name::Symbol, v, n::Int) - v isa Int || Base.error( - "Index $(name) must be Int, got $(typeof(v)) for spec $(S)", - ) - (1 <= v <= n) || - error("Index $(name) = $(v) out of bounds 1:$(n) for spec $(S)") - v - end - - lengths = Dict{Symbol, Int}() - - for d in dims - datakey = datakeys[d] - arr = container_array(S, obj, d, datakey) - - nd = ndims(arr) - nd == 0 && - Base.error("AxisSpec $(d) data for $(S) is scalar; expected an array.") - - # Enforce the same storage contract as axis_slice, - # except that :x may be a global 1D vector. - if has_i && has_j && !(d === :x && nd == 1) && nd < 3 - Base.error( - "Invalid axis storage for $(d): spec uses indices :i and :j, " * - "but container_array($(S), $(d)) returned an array with $(nd) dimension(s). " * - "When both :i and :j are active, the underlying array must be at least 3D " * - "(Ni, Nj, Nk...).", - ) - end - - # Check i/j bounds using first/second dims when present. - # Skip for global 1D :x vectors. - if !(d === :x && nd == 1) - if has_i && nd >= 1 - _check_index(:i, i_val, size(arr, 1)) - end - if has_j && nd >= 2 - _check_index(:j, j_val, size(arr, 2)) - end - end - - # Determine sample length along k or last dimension - n_samp = if nd == 1 - length(arr) - else - size(arr, nd) - end - - len = if has_k - kv = k_val - if kv isa Int - _check_index(:k, kv, n_samp) - 1 - elseif kv isa AbstractUnitRange{<:Int} - first(kv) >= 1 && last(kv) <= n_samp || - error( - "Range k = $(kv) out of bounds 1:$(n_samp) for spec $(S) on axis $(d).", - ) - length(kv) - elseif kv isa Colon - n_samp - else - Base.error( - "Index :k must be Int, Int range, or `:` after normalization; " * - "got $(typeof(kv)) for spec $(S).", - ) - end - else - # No ranged k for this spec → sample length is the 1D length (nd == 1) - # or the last dimension if nd ≥ 2; caller already ensured alignment. - nd == 1 ? length(arr) : n_samp - end - - lengths[d] = len - - # guard select_field semantics - kfield = select_field(S, Val(d)) - if kfield !== nothing - # select_field is interpreted strictly as a spec field name. - # If that field is provided in the spec NT, then it must be a Symbol - # and the data must be NamedTuple with that key. - # If not provided, we only enforce that elements are NamedTuple; - # the grammar decides how to use the keys later. - isempty(arr) && continue - - first_el = first(arr) - - if kfield in keys(spec) - v = spec[kfield] - v isa Symbol || Base.error( - "select_field($(S), Val($(d))) = :$(kfield) but spec.$(kfield) " * - "is not a Symbol; got $(typeof(v)).", - ) - sym = v - - first_el isa NamedTuple || Base.error( - "Data for axis $(d) in $(S) must be NamedTuple when a leaf " * - "field is selected via select_field; got $(typeof(first_el)).", - ) - haskey(first_el, sym) || Base.error( - "NamedTuple data for axis $(d) in $(S) has no key $(sym).", - ) - else - # select_field is defined but no concrete field has been bound yet. - # Enforce that the data are NamedTuple; actual key usage is left - # to the generic make_series/make_views logic. - first_el isa NamedTuple || Base.error( - "Data for axis $(d) in $(S) must be NamedTuple when " * - "select_field($(S), Val($(d))) is defined; got $(typeof(first_el)).", - ) - end - end - end - - vals = collect(values(lengths)) - isempty(vals) && return - - ref = first(vals) - for (d, len) in lengths - len == ref || Base.error( - "Mismatched sample lengths for spec $(S): axis $(d) has length $(len), " * - "expected $(ref). Containers must align along their sample dimension.", - ) - end - - return -end - - - - - -@inline function trait_to_tuple(::Type{S}, raw, name) where {S <: AbstractPlotSpec} - raw === () && return () - raw isa Tuple && return raw - @warn "Trait $(name) for $(S) should be a Tuple; got $(typeof(raw)). Coercing to 1-tuple." - return (raw,) -end - -""" - parse_kwargs(::Type{S}, obj, kwargs::NamedTuple) where {S<:AbstractPlotSpec} - -Grammar-level normalization phase. - -Responsibilities: - - 1. Decide which kwargs matter for this spec (`input_kwargs`, `renderer_kwargs`, - `index_keys`, `geom_axes`) and partition user kwargs into semantic vs - renderer. - 2. Merge user kwargs with `input_defaults(S, obj)` and - `renderer_defaults(S, obj)`. - 3. Normalize indices (`index_keys(S)` / `ranged_keys(S)`) to the allowed - types and fill in defaults. - 4. Ensure axis data keys (`x`, `y`, ...) exist and are `Symbol`s. - 5. Run grammar-level structural checks: - - data sources exist under `obj` according to `data_container`, - - indices are in bounds, - - all active axes have compatible sample lengths. - -Returns a canonical NamedTuple: - - (; obj = obj, spec = spec_nt, backend = backend_nt) - -to be consumed by `resolve_input`. -""" -function parse_kwargs(::Type{S}, obj, kwargs::NamedTuple) where {S <: AbstractPlotSpec} - # Raw traits - ik_raw = input_kwargs(S) - bk_raw = renderer_kwargs(S) - idx_raw = index_keys(S) - dims_raw = geom_axes(S) - rk_raw = ranged_keys(S) - - # Coerce to tuples with warnings if someone was lazy - ik = trait_to_tuple(S, ik_raw, "input_kwargs") - bk = trait_to_tuple(S, bk_raw, "renderer_kwargs") - idx = trait_to_tuple(S, idx_raw, "index_keys") - dims = trait_to_tuple(S, dims_raw, "geom_axes") - rk = trait_to_tuple(S, rk_raw, "ranged_keys") - - # Basic trait sanity: they should all be Symbols, and axes only from :x,:y,:z - for (name, tup) in (("input_kwargs", ik), ("renderer_kwargs", bk), - ("index_keys", idx), ("ranged_keys", rk)) - all(k -> k isa Symbol, tup) || - @warn "$(name)(::Type{$(S)}) should be a Tuple of Symbols, got $(tup)." - end - - for d in dims - d in (:x, :y, :z) || - Base.error( - "geom_axes(::Type{$(S)}) returned unsupported axis $(d). " * - "Valid axes are :x, :y, :z.", - ) - end - - # Additional trait sanity for ranged_keys: - # - ranged_keys ⊆ index_keys - # - only :k and :l are allowed to be ranged (sample-like dims) - if !isempty(rk) - # ranged_keys ⊆ index_keys - for key in rk - key in idx || Base.error( - "ranged_keys(::Type{$(S)}) includes $(key), which is not in " * - "index_keys(::Type{$(S)}) = $(idx).", - ) - end - - # Only :k and :l allowed as rangeable indices (sample dimensions) - for key in rk - (key === :k || key === :l) || Base.error( - "ranged_keys(::Type{$(S)}) may only contain :k and/or :l. " * - "Got $(key). Allowing :i or :j here would break the axis " * - "semantics (sample dimension must remain unique).", - ) - end - end - - # 1) Split user kwargs into semantic vs backend - spec_inputs, renderer_inputs = split_kwargs(S, kwargs, ik, bk, idx, dims) - - # 2) Merge with defaults - spec_nt, renderer_nt = merge_defaults(S, obj, spec_inputs, renderer_inputs) - - # 3) Normalize indices (i,j,k,...) according to index/ranged traits - spec_nt = normalize_indices(S, spec_nt, idx, rk) - - # 4) Ensure axis data keys exist and are Symbols - verify_selectors(S, spec_nt, dims) - - # 5) Grammar-level structural sanity: containers, bounds, lengths - verify_shapes(S, obj, spec_nt, dims, idx) - - return (; obj = obj, spec = spec_nt, renderer = renderer_nt) -end - -# Convenience varargs wrapper -parse_kwargs(::Type{S}, obj; kwargs...) where {S <: AbstractPlotSpec} = - parse_kwargs(S, obj, (; kwargs...)) - -""" -Resolve raw inputs into a normalized NamedTuple understood by `make_pages`. - -This is where a spec implements its own mini-grammar: - -- parse `values_expr` / `ijk`, -- pick matrix indices/slices, -- decide which quantities (R/L/C/G etc.) and which kind (:hist, :heatmap, ...). - -Default is identity; spec types are expected to override. -""" -function resolve_input(::Type{S}, nt::NamedTuple) where {S <: AbstractPlotSpec} - obj = nt.obj - spec = nt.spec - renderer_nt = nt.renderer - - dims = geom_axes(S) - dims = dims isa Tuple ? dims : (dims,) - - xsel = :x in dims ? spec.x : nothing - ysel = :y in dims ? spec.y : nothing - zsel = :z in dims && haskey(spec, :z) ? spec.z : nothing - - # raw user knob (global, for now) - has_as = haskey(spec, :as) - raw_as = has_as ? spec.as : nothing - - xq = yq = zq = nothing - xas = yas = zas = nothing - - any_complex = false - - if :x in dims - if has_complex_qty(S, Val(:x), Val(xsel)) - any_complex = true - xas = raw_as === nothing ? complex_as_default(S, Val(:x), Val(xsel)) : raw_as - - allowed = complex_as(S, Val(:x), Val(xsel)) - xas in allowed || - Base.error("Invalid as=$(xas) for x=$(xsel). Allowed: $(allowed).") - - xq = axis_quantity(S, Val(:x), Val(xsel), Val(xas)) - else - xq = axis_quantity(S, Val(:x), Val(xsel)) - end - end - - if :y in dims - if has_complex_qty(S, Val(:y), Val(ysel)) - any_complex = true - yas = raw_as === nothing ? complex_as_default(S, Val(:y), Val(ysel)) : raw_as - allowed = complex_as(S, Val(:y), Val(ysel)) - yas in allowed || - Base.error("Invalid as=$(yas) for y=$(ysel). Allowed: $(allowed).") - - yq = axis_quantity(S, Val(:y), Val(ysel), Val(yas)) - else - yq = axis_quantity(S, Val(:y), Val(ysel)) - end - end - - if :z in dims && zsel !== nothing - if has_complex_qty(S, Val(:z), Val(zsel)) - any_complex = true - zas = raw_as === nothing ? complex_as_default(S, Val(:z), Val(zsel)) : raw_as - - allowed = complex_as(S, Val(:z), Val(zsel)) - zas in allowed || - Base.error("Invalid as=$(zas) for z=$(zsel). Allowed: $(allowed).") - - zq = axis_quantity(S, Val(:z), Val(zsel), Val(zas)) - else - zq = axis_quantity(S, Val(:z), Val(zsel)) - end - end - - # Tight API: if user asked for as= but nothing is complex, that's nonsense. - if has_as && !any_complex - Base.error( - "Keyword as= is only valid for complex selectors with trait has_complex_qty == true.", - ) - end - - out = spec - - if :x in dims - out = merge(out, (; x = xsel, x_quantity = xq)) - xas === nothing || (out = merge(out, (; x_as = xas))) - end - if :y in dims - out = merge(out, (; y = ysel, y_quantity = yq)) - yas === nothing || (out = merge(out, (; y_as = yas))) - end - if :z in dims && zsel !== nothing - out = merge(out, (; z = zsel, z_quantity = zq)) - zas === nothing || (out = merge(out, (; z_as = zas))) - end - - return merge(out, (; obj = obj, renderer = renderer_nt)) -end diff --git a/src/plotbuilder/plothelpers.jl b/src/plotbuilder/plothelpers.jl deleted file mode 100644 index 802f71ff2..000000000 --- a/src/plotbuilder/plothelpers.jl +++ /dev/null @@ -1,36 +0,0 @@ -using Makie - -using Base: basename, mod1 -using Dates: format, now -using Printf: @sprintf - -import LineCableModels.PlotBuilder.BackendHandler: BackendHandler, next_fignum - -using LineCableModels.PlotBuilder.PlotUIComponents: - PlotAssembly, - PlotBuildArtifacts, - ControlButtonSpec, - ControlToggleSpec, - ControlReaction, - _make_window, - _run_plot_pipeline, - with_plot_theme, - ensure_export_background!, - with_icon, - MI_REFRESH, - MI_SAVE, - ICON_TTF, - AXIS_LABEL_FONT_SIZE, - clear_status!, - TICKFORMATTER, - EXPORT_EXTENSION, - EXPORT_TIMESTAMP_FORMAT - -using LineCableModels.PlotBuilder: AbstractPlotSpec, - _sanitize_filename_plot, - _default_export_path, - _save_plot_export, - _parse_values_expr, - _autoscale_axis, - _render_plot_specs, - _build_common_plot_controls diff --git a/src/plotbuilder/plotspecs.jl b/src/plotbuilder/plotspecs.jl deleted file mode 100644 index 4c8f0ca4d..000000000 --- a/src/plotbuilder/plotspecs.jl +++ /dev/null @@ -1,167 +0,0 @@ -function _sanitize_filename_plot(str::AbstractString) - sanitized = lowercase(strip(str)) - sanitized = replace(sanitized, r"[^0-9a-z]+" => "_") - sanitized = strip(sanitized, '_') - return isempty(sanitized) ? "linecablemodels_plot" : sanitized -end - -function _default_export_path( - spec::AbstractPlotSpec; - extension::AbstractString = EXPORT_EXTENSION, -) - base_title = strip(spec.title) - base = isempty(base_title) ? string(spec.parent_kind, "_", spec.component) : base_title - name = _sanitize_filename_plot(base) - timestamp = format(now(), EXPORT_TIMESTAMP_FORMAT) - filename = string(name, "_", timestamp, ".", extension) - return joinpath(pwd(), filename) -end - -function _save_plot_export(spec::AbstractPlotSpec, axis) - # Capture current axis scales before building the export figure - spec.xscale[] = axis.xscale[] - spec.yscale[] = axis.yscale[] - fig = build_export_figure(spec) - trim!(fig.layout) - path = _default_export_path(spec) - Makie.save(path, fig) - return path -end - -""" -Parses a values expression like :X[1,1,:] or :X[1,1,1:5]. -""" -function _parse_values_expr(values_expr, ijk) - - # --- input is :R[1,1,:] --- - if ijk === nothing - if values_expr isa Expr && values_expr.head === :ref && - length(values_expr.args) == 4 - q = values_expr.args[1] - q isa Symbol || - Base.error("Expected values symbol as first argument, got $(q)") - - local i::Int - local j::Int - local k::Union{Int, Colon, AbstractRange} - - # Eval the indices to resolve them from Expr. - # It will correctly resolve 1, :, and 1:5. - try - i = eval(values_expr.args[2]) - j = eval(values_expr.args[3]) - k = eval(values_expr.args[4]) - catch e - Base.error( - "Failed to parse indices from $(values_expr). Ensure they are valid literals (1, :, 1:5, etc.). Error: $e", - ) - end - - # Type checking after eval - i isa Int || Base.error("i index '$i' is not an Int") - j isa Int || Base.error("j index '$j' is not an Int") - (k isa Int || k == (:) || k isa AbstractRange) || - Base.error("k index '$k' must be Int, ':', or AbstractRange") - - return q, (i, j, k) - else - Base.error( - "Provide values as Expr like :X[1,1,:] or :X[1,1,1:5], or pass symbol and `ijk`", - ) - end - - # --- input is :X, ijk=(1,1,:) --- - else - # This branch already supports non-Int types, it just needs a type assertion. - ijk isa NTuple{3, Any} || - Base.error("ijk must be NTuple{3,Any}, got $(typeof(ijk))") - values_expr isa Symbol || - Base.error( - "values must be Symbol when ijk is provided; got $(typeof(values_expr))", - ) - - i, j, k = ijk - - # Check types - i isa Int || Base.error("i in ijk must be Int") - j isa Int || Base.error("j in ijk must be Int") - (k isa Int || k == (:) || k isa AbstractRange) || - Base.error("k in ijk must be Int, ':', or AbstractRange") - - return values_expr, (i, j, k) - end -end - -function _autoscale_axis(values::AbstractVector{<:Real}; _threshold = 1e4) - isempty(values) && return values, 0 - maxval = 0.0 - has_value = false - for val in values - if isnan(val) - continue - end - absval = abs(val) - if !has_value || absval > maxval - maxval = absval - has_value = true - end - end - !has_value && return values, 0 - exp = floor(Int, log10(maxval)) - _threshold_exp = floor(Int, log10(_threshold)) - abs(exp) < abs(_threshold_exp) && return values, 0 - scale = 10.0 ^ exp - return values ./ scale, exp -end - -function _render_plot_specs( - spec::AbstractPlotSpec; - backend = nothing, - display_plot::Bool = true, -) - n = next_fignum() - backend_ctx = _make_window( - BackendHandler, - backend; - title = "Fig. $(n) – $(spec.title)", - icons = _ICON_FN, - icons_font = ICON_TTF, - ) - pipeline_kwargs = - spec.fig_size === nothing ? - (; initial_status = " ") : - (; fig_size = spec.fig_size, initial_status = " ") - - assembly = with_plot_theme(backend_ctx) do - _run_plot_pipeline( - backend_ctx, - # This closure dispatches to the correct _build_plot! method - # based on the *concrete type* of 'spec'. - (fig_ctx, ctx, axis) -> _build_plot!(fig_ctx, ctx, axis, spec); - pipeline_kwargs..., - ) - end - if display_plot - _display!(backend_ctx, assembly.figure; title = spec.title) - end - return assembly -end - -# --- De-duplicated Button Logic --- -function _build_common_plot_controls(spec::AbstractPlotSpec, axis) - buttons = [ - ControlButtonSpec( - (_ctx, _btn) -> (Makie.autolimits!(axis); nothing), - icon = MI_REFRESH, - on_success = ControlReaction(status_string = "Axis limits reset"), - ), - ControlButtonSpec( - (_ctx, _btn) -> _save_plot_export(spec, axis), # Generic save - icon = MI_SAVE, - on_success = ControlReaction( - status_string = path -> string("Saved SVG to ", basename(path)), - ), - ), - ] - return buttons -end diff --git a/src/plotbuilder/plotuicomponents/PlotUIComponents.jl b/src/plotbuilder/plotuicomponents/PlotUIComponents.jl deleted file mode 100644 index ed2e8510d..000000000 --- a/src/plotbuilder/plotuicomponents/PlotUIComponents.jl +++ /dev/null @@ -1,818 +0,0 @@ -module PlotUIComponents - -using Makie - -# Makie now supplies `toggle_visibility!`. Mutating Makie's module from this -# package's `__init__` breaks incremental compilation on Julia 1.12. - -using Printf: @sprintf -import ..PlotBuilder.BackendHandler: current_backend_symbol, _pkgid - -# ----------------------------------------------------------------------------- -# Constants -# ----------------------------------------------------------------------------- - -const FIG_SIZE = (800, 600) -const FIG_PADDING = (80, 60, 40, 40) # left, right, bottom, top -const CTLBAR_HEIGHT = 36 -const STATUSBAR_HEIGHT = 20 -const GRID_ROW_GAP = 6 -const GRID_COL_GAP = 6 -const LEGEND_GAP = 4 -const LEGEND_WIDTH = 140 -const COLORBAR_GAP = 4 -const CTLBAR_GAP = 2 -const BUTTON_MIN_WIDTH = 32 -const BUTTON_ICON_SIZE = 18 -const BUTTON_TEXT_FONT_SIZE = 15 -const AXIS_TITLE_FONT_SIZE = 15 -const AXIS_LABEL_FONT_SIZE = 14 -const AXIS_TICK_FONT_SIZE = 14 -const STATUS_FONT_SIZE = 10 -const BG_COLOR_INTERACTIVE = :grey90 -const BG_COLOR_EXPORT = :white -const ICON_COLOR_ACTIVE = Makie.RGBAf(0.15, 0.15, 0.15, 1.0) -const ICON_COLOR_DISABLED = Makie.RGBAf(0.55, 0.55, 0.55, 1.0) -const TICK_FMT = x -> @sprintf("%.4g", x) -const TICKFORMATTER = values -> [TICK_FMT(v) for v in values] -const EXPORT_TIMESTAMP_FORMAT = "yyyymmdd_HHMMSS" -const EXPORT_EXTENSION = "svg" - -# ----------------------------------------------------------------------------- -# Material UI icons -# ----------------------------------------------------------------------------- -const MI_REFRESH = "\uE5D5" # Material Icons: 'refresh' -const MI_SAVE = "\uE161" # Material Icons: 'save' -const ICON_TTF = joinpath(pkgdir(@__MODULE__), "assets", "fonts", "material-icons", "MaterialIcons-Regular.ttf") - - -# ----------------------------------------------------------------------------- -# Data structures -# ----------------------------------------------------------------------------- - -mutable struct PlotBackendContext - backend::Symbol - interactive::Bool - window::Union{Nothing, Any} - screen::Union{Nothing, Any} - use_latex_fonts::Bool - icons::Function - icons_font::Union{Nothing, String} - statusbar::Union{Nothing, Makie.Observable{String}} -end - -struct PlotFigureContext - figure::Makie.Figure - canvas_node::Any - legend_grid::Makie.GridLayout - legend_slot::Any - colorbar_slot::Any - ctlbar_node::Makie.GridLayout - placeholder_node::Makie.GridLayout - statusbar_node::Makie.GridLayout -end - -struct ControlReaction - status_string::Union{Nothing, String, Function} - button_color::Union{Nothing, Any} - button_label::Union{Nothing, String} - timeout::Union{Nothing, AbstractFloat} -end - -ControlReaction(; - status_string = nothing, - button_color = nothing, - button_label = nothing, - timeout = 1.5, -) = - ControlReaction(status_string, button_color, button_label, timeout) - -struct ControlButtonSpec - label::Union{Nothing, String} - icon::Union{Nothing, String} - action::Function - on_success::Union{Nothing, ControlReaction} - on_failure::Union{Nothing, ControlReaction} -end - -ControlButtonSpec( - action::Function; - label::Union{Nothing, String} = nothing, - icon::Union{Nothing, String} = nothing, - on_success::Union{Nothing, ControlReaction} = nothing, - on_failure::Union{Nothing, ControlReaction} = nothing, -) = ControlButtonSpec(label, icon, action, on_success, on_failure) - -struct ControlToggleSpec - label::Union{Nothing, String} - action_on::Function - action_off::Function - on_success_on::Union{Nothing, ControlReaction} - on_success_off::Union{Nothing, ControlReaction} - on_failure::Union{Nothing, ControlReaction} - start_active::Bool -end - -ControlToggleSpec( - action_on::Function, - action_off::Function; - label::Union{Nothing, String} = nothing, - on_success_on::Union{Nothing, ControlReaction} = nothing, - on_success_off::Union{Nothing, ControlReaction} = nothing, - on_failure::Union{Nothing, ControlReaction} = nothing, - start_active::Bool = false, -) = ControlToggleSpec( - label, - action_on, - action_off, - on_success_on, - on_success_off, - on_failure, - start_active, -) - -struct PlotBuildArtifacts - axis::Union{Nothing, Makie.Axis} - legends::Union{Nothing, Any} - colorbars::Union{Nothing, Vector{Any}} - control_buttons::Vector{ControlButtonSpec} - control_toggles::Vector{ControlToggleSpec} - status_message::Union{Nothing, String} -end - -PlotBuildArtifacts(; axis = nothing, legends = nothing, colorbars = nothing, - control_buttons = ControlButtonSpec[], control_toggles = ControlToggleSpec[], - status_message = nothing) = - PlotBuildArtifacts( - axis, - legends, - colorbars, - control_buttons, - control_toggles, - status_message, - ) - -struct PlotAssembly - backend_ctx::PlotBackendContext - figure_ctx::PlotFigureContext - figure::Makie.Figure - axis::Any - buttons::Vector{Makie.Button} - legend::Any - colorbars::Vector{Any} - status_label::Any - artifacts::PlotBuildArtifacts -end - -# ----------------------------------------------------------------------------- -# Backend helpers -# ----------------------------------------------------------------------------- - -"""Create a GLMakie screen if GL backend is active; otherwise return nothing.""" -function gl_screen(title::AbstractString) - if current_backend_symbol() == :gl - mod = Base.require(_pkgid(:gl)) - ctor = getproperty(mod, :Screen) - return Base.invokelatest(ctor; title = String(title)) - end - return nothing -end - -# tiny helper to build "icon + text" labels ergonomically --- -""" -with_icon(icon; text="", isize=14, tsize=12, color=:black, gap=4, - dy_icon=-0.18, dy_text=0.0) - -- `dy_icon`, `dy_text`: vertical tweaks in *em* units (fraction of that part's fontsize). - Negative moves down, positive moves up. -""" -with_icon(icon::AbstractString; text::AbstractString = "", - isize::Int = BUTTON_ICON_SIZE, tsize::Int = BUTTON_TEXT_FONT_SIZE, color = :black, - gap::Int = 2, - dy_icon::Float64 = -0.18, dy_text::Float64 = 0.0) = - text == "" ? - rich(icon; font = :icons, fontsize = isize, color = color, offset = (0, dy_icon)) : - rich( - rich(icon; font = :icons, fontsize = isize, color = color, offset = (0, dy_icon)), - rich(" "^gap; font = :regular, fontsize = tsize, color = color), - rich(text; font = :regular, fontsize = tsize, color = color, offset = (0, dy_text)), - ) - -function build_backend_context( - backend::Symbol; - interactive::Union{Nothing, Bool} = nothing, - window = nothing, - screen = nothing, - icons::Function = (icon; text = nothing, kwargs...) -> - (text === nothing ? string(icon) : string(text)), - use_latex_fonts::Bool = false, - icons_font::Union{Nothing, String} = nothing, - statusbar::Union{Nothing, Makie.Observable{String}} = nothing, -) - is_interactive = - interactive === nothing ? backend in (:gl, :wgl, :wglmakie) : interactive - chan = statusbar - if chan === nothing && is_interactive - chan = Makie.Observable("") - end - return PlotBackendContext( - backend, - is_interactive, - window, - screen, - use_latex_fonts, - icons, - icons_font, - chan, - ) -end - -function attach_window!(ctx::PlotBackendContext; window = nothing, screen = nothing) - ctx.window = window - ctx.screen = screen - return ctx -end - -function _make_window( - backend_handler::Module, - backend::Union{Nothing, Symbol} = nothing; - title::AbstractString = "LineCableModels Plot", - icons::Function = (icon; text = nothing, kwargs...) -> - (text === nothing ? string(icon) : string(text)), - use_latex_fonts::Bool = false, - icons_font::Union{Nothing, String} = nothing, - statusbar::Union{Nothing, Makie.Observable{String}} = nothing, - interactive_override::Union{Nothing, Bool} = nothing, -) - actual_backend = backend_handler.ensure_backend!(backend) - is_interactive = - interactive_override === nothing ? actual_backend in (:gl, :wgl) : - interactive_override - ctx = build_backend_context( - actual_backend; - interactive = is_interactive, - icons = icons, - use_latex_fonts = use_latex_fonts, - icons_font = icons_font, - statusbar = statusbar, - ) - if is_interactive && actual_backend == :gl - scr = gl_screen(title) - if scr !== nothing - attach_window!(ctx; window = scr, screen = scr) - end - end - return ctx -end - -function theme_for( - ctx::PlotBackendContext; - mode::Symbol = ctx.interactive ? :interactive : :export, -) - background = mode === :interactive ? BG_COLOR_INTERACTIVE : BG_COLOR_EXPORT - base = Makie.Theme() - if ctx.use_latex_fonts && mode == :export - base = merge(base, Makie.theme_latexfonts()) - end - icon_font = ctx.icons_font - # Optional keyword: empty if no icon font, otherwise sets fonts = (; icons = icon_font) - fonts_kw = icon_font === nothing ? NamedTuple() : (fonts = (; icons = icon_font),) - # one single theme with conditional fonts because we are civilized barbarians - custom = Makie.Theme(; - backgroundcolor = background, - Axis = ( - titlesize = AXIS_TITLE_FONT_SIZE, - xlabelsize = AXIS_LABEL_FONT_SIZE, - ylabelsize = AXIS_LABEL_FONT_SIZE, - xticklabelsize = AXIS_TICK_FONT_SIZE, - yticklabelsize = AXIS_TICK_FONT_SIZE, - xtickformat = TICKFORMATTER, - ytickformat = TICKFORMATTER, - ), - Legend = ( - fontsize = AXIS_LABEL_FONT_SIZE, - labelsize = AXIS_LABEL_FONT_SIZE, - ), - Colorbar = ( - labelsize = AXIS_LABEL_FONT_SIZE, - ticklabelsize = AXIS_TICK_FONT_SIZE, - ), - fonts_kw..., # <- conditionally adds `fonts` only when icon_font ≠ nothing - ) - return merge(base, custom) -end - -_configure_theme!( - ctx::PlotBackendContext; - mode::Symbol = ctx.interactive ? :interactive : :export, -) = - theme_for(ctx; mode = mode) - -function with_plot_theme( - f::Function, - ctx::PlotBackendContext; - mode::Union{Nothing, Symbol} = nothing, -) - chosen_mode = mode === nothing ? (ctx.interactive ? :interactive : :export) : mode - theme = _configure_theme!(ctx; mode = chosen_mode) - return Makie.with_theme(theme) do - f() - end -end - -# ----------------------------------------------------------------------------- -# Figure helpers -# ----------------------------------------------------------------------------- - -function _make_figure( - ctx::PlotBackendContext; - fig_size::Tuple{Int, Int} = FIG_SIZE, - figure_padding::NTuple{4, Int} = FIG_PADDING, - legend_panel_width::Int = LEGEND_WIDTH, -) - fig = Makie.Figure(; size = fig_size, figure_padding = figure_padding) - - ctlbar_node = fig[1, 1:2] = Makie.GridLayout() - ctlbar_node.halign = :left - ctlbar_node.valign = :bottom - placeholder_node = fig[2, 1:2] = Makie.GridLayout() - canvas_node = fig[3, 1] - legend_grid = fig[3, 2] = Makie.GridLayout() - statusbar_node = fig[4, 1:2] = Makie.GridLayout() - statusbar_node.halign = :left - - legend_slot = legend_grid[1, 1] - legend_slot[] = Makie.GridLayout() - colorbar_slot = legend_grid[2, 1] - colorbar_slot[] = Makie.GridLayout() - - fig_ctx = PlotFigureContext( - fig, - canvas_node, - legend_grid, - legend_slot, - colorbar_slot, - ctlbar_node, - placeholder_node, - statusbar_node, - ) - - _configure_layout!( - fig_ctx; - interactive = ctx.interactive, - legend_panel_width = legend_panel_width, - ) - return fig_ctx -end - -function _configure_layout!( - fig_ctx::PlotFigureContext; - interactive::Bool = true, - legend_panel_width::Int = LEGEND_WIDTH, -) - layout = fig_ctx.figure.layout - - Makie.rowgap!(layout, GRID_ROW_GAP) - Makie.colgap!(layout, GRID_COL_GAP) - - Makie.rowsize!(layout, 1, Makie.Fixed(interactive ? CTLBAR_HEIGHT : 0)) - Makie.rowsize!(layout, 2, Makie.Fixed(0)) - Makie.rowsize!(layout, 3, Makie.Relative(1.0)) - Makie.rowsize!(layout, 4, Makie.Fixed(interactive ? STATUSBAR_HEIGHT : 0)) - - Makie.colsize!(layout, 1, Makie.Relative(1.0)) - Makie.colsize!(layout, 2, Makie.Fixed(legend_panel_width)) - - Makie.rowgap!(fig_ctx.legend_grid, LEGEND_GAP) - Makie.colgap!(fig_ctx.legend_grid, 0) - - - Makie.rowsize!(fig_ctx.legend_grid, 1, Makie.Auto()) - Makie.rowsize!(fig_ctx.legend_grid, 2, Makie.Auto()) - - return fig_ctx -end - -function _make_canvas!( - fig_ctx::PlotFigureContext; - axis_ctor = Makie.Axis, - axis_options::NamedTuple = NamedTuple(), -) - axis = axis_ctor(fig_ctx.canvas_node; axis_options...) - return axis -end - -# ----------------------------------------------------------------------------- -# Control bar helpers -# ----------------------------------------------------------------------------- - -function _make_ctlbar!( - fig_ctx::PlotFigureContext, - ctx::PlotBackendContext, - button_specs::AbstractVector{ControlButtonSpec}, - toggle_specs::AbstractVector{ControlToggleSpec}; - button_height::Int = max(CTLBAR_HEIGHT - 12, 32), - button_gap::Int = CTLBAR_GAP, -) - if !ctx.interactive || (isempty(button_specs) && isempty(toggle_specs)) - Makie.rowsize!(fig_ctx.figure.layout, 1, Makie.Fixed(0)) - return [], [] - end - - layout = fig_ctx.ctlbar_node - Makie.rowgap!(layout, 0) - Makie.colgap!(layout, button_gap) - Makie.rowsize!(layout, 1, Makie.Fixed(button_height)) - - buttons = Makie.Button[] - toggles = Makie.Toggle[] - col_idx = 1 - - for spec in button_specs - label = _build_button_label(ctx, spec) - button_kwargs = ( - ; label = label, - fontsize = BUTTON_TEXT_FONT_SIZE, - height = button_height, - halign = :left, - ) - if spec.icon !== nothing - width = _preferred_button_width(spec) - if width !== nothing - button_kwargs = (; button_kwargs..., width = width) - end - end - button = Makie.Button(layout[1, col_idx]; button_kwargs...) - push!(buttons, button) - _wire_button_callback!(button, spec, ctx) - col_idx += 1 - end - - for spec in toggle_specs - gl = layout[1, col_idx] = Makie.GridLayout() - gl.halign = :left - gl.valign = :center - - toggle = Makie.Toggle(gl[1, 2]; active = spec.start_active) - if spec.label !== nothing - Makie.Label(gl[1, 1], spec.label, halign = :right) - end - - push!(toggles, toggle) - _wire_toggle_callback!(toggle, spec, ctx) - col_idx += 1 - end - - return buttons, toggles -end - -function _build_button_label(ctx::PlotBackendContext, spec::ControlButtonSpec) - icon_fn = ctx.icons - label_text = spec.label === nothing ? "" : spec.label - if spec.icon === nothing - return label_text - end - try - return icon_fn(spec.icon; text = label_text, gap = 6) - catch err - if err isa MethodError - return isempty(label_text) ? string(spec.icon) : label_text - else - rethrow(err) - end - end -end - -function _preferred_button_width(spec::ControlButtonSpec) - if spec.icon !== nothing && spec.label === nothing - return BUTTON_MIN_WIDTH - end - return nothing -end - -function _wire_button_callback!(button, spec::ControlButtonSpec, ctx::PlotBackendContext) - ensure_statusbar!(ctx) - - Makie.on(button.clicks) do _ - Base.@async begin - try - result = _invoke_button_action(spec.action, ctx, button) - _apply_reaction!(ctx, button, spec.on_success, result) - catch err - _apply_reaction!(ctx, button, spec.on_failure, sprint(showerror, err)) - end - end - end - return button -end - -function _wire_toggle_callback!(toggle, spec::ControlToggleSpec, ctx::PlotBackendContext) - ensure_statusbar!(ctx) - - Makie.on(toggle.active) do is_active - Base.@async begin - original_state = !is_active - try - if is_active - result = _invoke_button_action(spec.action_on, ctx, toggle) - _apply_reaction!(ctx, toggle, spec.on_success_on, result) - else - result = _invoke_button_action(spec.action_off, ctx, toggle) - _apply_reaction!(ctx, toggle, spec.on_success_off, result) - end - catch err - _apply_reaction!(ctx, toggle, spec.on_failure, sprint(showerror, err)) - end - end - end - return toggle -end - -function _invoke_button_action(action::Function, ctx::PlotBackendContext, button) - try - return Base.invokelatest(action, ctx, button) - catch err - if err isa MethodError && err.f === action - try - return Base.invokelatest(action, ctx) - catch err2 - if err2 isa MethodError && err2.f === action - return Base.invokelatest(action) - else - throw(err2) - end - end - else - throw(err) - end - end -end - - -function _apply_reaction!( - ctx::PlotBackendContext, - button, - reaction::Union{Nothing, ControlReaction}, - result, -) - has_color = hasproperty(button, :buttoncolor) - original_color = has_color ? button.buttoncolor[] : nothing - has_label = hasproperty(button, :label) - original_label = has_label ? button.label[] : nothing - - # Determine the status message - status_msg = nothing - if reaction !== nothing && reaction.status_string !== nothing - if reaction.status_string isa Function - status_msg = reaction.status_string(result) - else - status_msg = reaction.status_string - end - elseif result isa AbstractString && !isempty(result) - status_msg = result - end - - # Apply reaction and status update - if status_msg !== nothing - update_status!(ctx, status_msg) - end - - if reaction !== nothing - if reaction.button_color !== nothing && has_color - button.buttoncolor[] = Makie.to_color(reaction.button_color) - end - if reaction.button_label !== nothing - button.label[] = reaction.button_label - end - end - - # Handle timeout and UI restoration - timeout = reaction !== nothing ? reaction.timeout : 1.6 - if timeout !== nothing && isfinite(timeout) - sleep(timeout) - if status_msg !== nothing - clear_status!(ctx) - end - if reaction !== nothing - if reaction.button_color !== nothing && has_color - button.buttoncolor[] = original_color - end - if reaction.button_label !== nothing && has_label - button.label[] = original_label - end - end - end - - return nothing -end - -clear_status!(ctx) = begin - # non-breaking space keeps the row height while looking empty - update_status!(ctx, "\u00A0") - -end - -# ----------------------------------------------------------------------------- -# Legend & colorbar helpers -# ----------------------------------------------------------------------------- - -"""Populate the legend area. Accepts `nothing`, a builder function, or a Makie plot object.""" -function _make_legend!(fig_ctx::PlotFigureContext, content; kwargs...) - slot = fig_ctx.legend_slot - if content === nothing - slot[] = Makie.GridLayout() - Makie.rowsize!(fig_ctx.legend_grid, 1, Makie.Fixed(0)) - return nothing - end - - Makie.rowsize!(fig_ctx.legend_grid, 1, Makie.Auto()) - container = Makie.GridLayout() - slot[] = container - built = _materialize_component!(container[1, 1], content; kwargs...) - if built !== nothing - if hasproperty(built, :valign) - built.valign[] = :top - end - if hasproperty(built, :halign) - built.halign[] = :left - end - end - return built -end - -"""Populate the colorbar stack with zero or more builder specs.""" -function _make_colorbars!( - fig_ctx::PlotFigureContext, - specs::Union{Nothing, AbstractVector}; - kwargs..., -) - slot = fig_ctx.colorbar_slot - if specs === nothing || isempty(specs) - slot[] = Makie.GridLayout() - Makie.rowsize!(fig_ctx.legend_grid, 2, Makie.Fixed(0)) - return Any[] - end - - Makie.rowsize!(fig_ctx.legend_grid, 2, Makie.Auto()) - container = Makie.GridLayout() - slot[] = container - Makie.rowgap!(container, COLORBAR_GAP) - - built = Any[] - row = 1 - for spec in specs - spec === nothing && continue - node = container[row, 1] - push!(built, _materialize_component!(node, spec; kwargs...)) - row += 1 - end - return built -end - -function _materialize_component!(parent, spec; kwargs...) - if spec isa Function - return spec(parent; kwargs...) - elseif Makie.isplot(spec) - parent[] = spec - return spec - else - try - parent[] = spec - return spec - catch err - if err isa MethodError - error("Unsupported component specification $(typeof(spec))") - else - rethrow(err) - end - end - end -end - - -# ----------------------------------------------------------------------------- -# Status helpers -# ----------------------------------------------------------------------------- - -function _make_statusbar!( - fig_ctx::PlotFigureContext, - ctx::PlotBackendContext; - initial_message::AbstractString = "", -) - if !ctx.interactive - Makie.rowsize!(fig_ctx.figure.layout, 4, Makie.Fixed(0)) - return nothing - end - - status_obs = ensure_statusbar!(ctx) - if !isempty(initial_message) - status_obs[] = String(initial_message) - end - - label = Makie.Label(fig_ctx.statusbar_node[1, 1]; - text = status_obs, - fontsize = STATUS_FONT_SIZE, - halign = :left, - tellwidth = false, - tellheight = false, - ) - return label -end - -function ensure_statusbar!(ctx::PlotBackendContext) - if ctx.statusbar === nothing - ctx.statusbar = Makie.Observable("") - end - return ctx.statusbar -end - -function update_status!(ctx::PlotBackendContext, message::AbstractString) - chan = ensure_statusbar!(ctx) - chan[] = String(message) - return chan -end - -# ----------------------------------------------------------------------------- -# Orchestration helpers -# ----------------------------------------------------------------------------- - -function _run_plot_pipeline( - backend_ctx::PlotBackendContext, - plot_fn::Function; - fig_size::Tuple{Int, Int} = FIG_SIZE, - figure_padding::NTuple{4, Int} = FIG_PADDING, - legend_panel_width::Int = LEGEND_WIDTH, - axis_ctor = Makie.Axis, - axis_kwargs::NamedTuple = NamedTuple(), - extra_buttons::AbstractVector{ControlButtonSpec} = ControlButtonSpec[], - initial_status::Union{Nothing, String} = nothing, -) - fig_ctx = _make_figure( - backend_ctx; - fig_size = fig_size, - figure_padding = figure_padding, - legend_panel_width = legend_panel_width, - ) - - axis = - isempty(axis_kwargs) ? - _make_canvas!(fig_ctx; axis_ctor = axis_ctor) : - _make_canvas!(fig_ctx; axis_ctor = axis_ctor, axis_options = axis_kwargs) - - artifacts = plot_fn(fig_ctx, backend_ctx, axis) - artifacts = artifacts === nothing ? PlotBuildArtifacts(axis = axis) : artifacts - - axis = artifacts.axis === nothing ? axis : artifacts.axis - - button_specs = ControlButtonSpec[] - isempty(extra_buttons) || append!(button_specs, extra_buttons) - isempty(artifacts.control_buttons) || append!(button_specs, artifacts.control_buttons) - - buttons, toggles = - _make_ctlbar!(fig_ctx, backend_ctx, button_specs, artifacts.control_toggles) - - legend_obj = _make_legend!(fig_ctx, artifacts.legends) - colorbar_objs = _make_colorbars!(fig_ctx, artifacts.colorbars) - - status_message = artifacts.status_message - if status_message === nothing - status_message = initial_status - end - status_message = status_message === nothing ? "" : status_message - - status_label = _make_statusbar!(fig_ctx, backend_ctx; initial_message = status_message) - if !isempty(status_message) - update_status!(backend_ctx, status_message) - end - - return PlotAssembly( - backend_ctx, - fig_ctx, - fig_ctx.figure, - axis, - buttons, - legend_obj, - colorbar_objs, - status_label, - artifacts, - ) -end - -make_window_context(args...; kwargs...) = _make_window(args...; kwargs...) -make_standard_figure(args...; kwargs...) = _make_figure(args...; kwargs...) -configure_layout!(args...; kwargs...) = _configure_layout!(args...; kwargs...) -make_canvas!(args...; kwargs...) = _make_canvas!(args...; kwargs...) -make_ctlbar!(args...; kwargs...) = _make_ctlbar!(args...; kwargs...) -make_legend!(args...; kwargs...) = _make_legend!(args...; kwargs...) -make_colorbars!(args...; kwargs...) = _make_colorbars!(args...; kwargs...) -make_statusbar!(args...; kwargs...) = _make_statusbar!(args...; kwargs...) -run_plot_pipeline(args...; kwargs...) = _run_plot_pipeline(args...; kwargs...) - -function ensure_export_background!(fig) - if fig !== nothing && hasproperty(fig, :scene) - fig.scene.backgroundcolor[] = Makie.to_color(BG_COLOR_EXPORT) - end - return fig -end - -end # module PlotUIComponents diff --git a/src/plotbuilder/plotuicomponents/callbacks.jl b/src/plotbuilder/plotuicomponents/callbacks.jl deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/plotbuilder/seriesspec.jl b/src/plotbuilder/seriesspec.jl deleted file mode 100644 index 767fa2679..000000000 --- a/src/plotbuilder/seriesspec.jl +++ /dev/null @@ -1,289 +0,0 @@ - -# -------------------------------------------------------------------------- -# AxisSpec-level transform hook -# -------------------------------------------------------------------------- - -""" - axis_transform(::Type{S}, ::Val{dim}, ::Val{datakey}, nt, axis::AxisSpec, data) where {S,dim,datakey} - -Per-spec hook to post-process the sliced axis data *before* unit scaling. - -- `dim` : :x, :y, or :z -- `datakey` : axis selector symbol (e.g. :f, :R, :Z, ...) -- `nt` : resolved input NamedTuple from `resolve_input` -- `axis` : AxisSpec for this dimension (quantity + units + label + scale) -- `data` : 1D numeric array returned by `axis_slice` - -Default is identity; in case of complex quantities defined by the trait `has_complex_qty`, the selector `as` defines what to extract: real, imaginary, magnitude, or phase components. -""" -axis_transform( - ::Type{S}, - ::Val{dim}, - ::Val{datakey}, - nt::NamedTuple, - axis::AxisSpec, - data, -) where {S <: AbstractPlotSpec, dim, datakey} = begin - has_complex_qty(S, Val(dim), Val(datakey)) || return data - (data isa AbstractArray && eltype(data) <: Number) || return data - - ask = Symbol(dim, :_as) - haskey(nt, ask) || return data - as = getfield(nt, ask) - - # Warn if a "complex view" is requested but the materialized slice is real. - # This is mathematically valid (imag(real)=0, abs(real)=|real|, angle(real)=0/π), - # but it usually indicates the pipeline expected complex data and got real instead. - if as !== :re && !(eltype(data) <: Complex) - @warn "Complex view requested on real-valued data; did you materialize a real quantity where complex was expected?" spec=S dim=dim datakey=datakey as=as eltype=eltype( - data, - ) - end - - as === :re && return real.(data) - as === :im && return imag.(data) - as === :abs && return abs.(data) - as === :angle && return angle.(data) .* (180 / pi) - - Base.error( - "Unsupported as=$(as) for $(datakey) on axis $(dim). Valid options: :re, :im, :abs, :angle.", - ) -end - - -""" - axis_slice(::Type{S}, nt, axis::AxisSpec, ::Val{dim}) where {S<:AbstractPlotSpec} - -Return a 1D slice for axis `dim` using the grammar: - - * Use `data_container(S, Val(dim))` and the axis selector `nt.` - (e.g. `nt.x`, `nt.y`) to locate the raw storage in `nt.obj`. - * Apply indices `i, j` if present in `nt`, assuming the sample dimension - is the last array dimension. - * Optionally unwrap child fields using `select_field(S, Val(dim))` if it is - non-`nothing` and elements are NamedTuples. - -No unit scaling and no numeric check happen here; those are handled by -`axis_transform` and `make_series`. -""" -function axis_slice( - ::Type{S}, - nt::NamedTuple, - axis::AxisSpec, - ::Val{dim}, -) where {S <: AbstractPlotSpec, dim} - - obj = nt.obj - - # AxisSpec selector: what the user (or defaults) chose for this axis, e.g. :f, :R, ... - selector = getfield(nt, dim)::Symbol - - # --- Fetch raw array via centralized container logic --- - raw_arr = container_array(S, obj, dim, selector) - - # --- Apply indices (i,j,k) → 1D slice along sample dimension --- - arr = raw_arr - nd = ndims(arr) - - has_i = haskey(nt, :i) - has_j = haskey(nt, :j) - has_k = haskey(nt, :k) - - # First slice in i,j where applicable. - # Exception: allow :x to be a global 1D vector shared across all (i,j). - if has_i && has_j - if dim === :x && nd == 1 - # global x; do nothing - elseif nd < 3 - Base.error( - "Invalid axis storage for $(dim): spec uses indices :i and :j, " * - "but container_array($(S), $(dim)) returned an array with $(nd) dimension(s). " * - "When both :i and :j are active, the underlying array must be at least 3D " * - "(Ni, Nj, Nk...). Check index_keys($(S)) and container_array($(S), $(dim)).", - ) - else - # canonical case: Ni×Nj×Nk... - arr = view(arr, nt.i, nt.j, :) - end - elseif has_i && !has_j - if dim === :x && nd == 1 - # global x; do nothing - elseif nd >= 2 - arr = view(arr, nt.i, :) - end - elseif has_j && !has_i - if dim === :x && nd == 1 - # global x; do nothing - elseif nd >= 2 - arr = view(arr, :, nt.j) - end - end - - # Then slice in k along last dimension (sample dim) - if has_k - k = nt.k - nd2 = ndims(arr) - - if nd2 == 0 - Base.error( - "AxisSpec $(dim) for $(S) has scalar data after i/j slicing; cannot apply k index.", - ) - end - - if nd2 == 1 - if k isa Int - arr = view(arr, k:k) - elseif k isa AbstractUnitRange{<:Int} || k isa Colon - arr = view(arr, k) - else - Base.error( - "Index :k must be Int, Int range, or `:` after normalization; " * - "got $(typeof(k)) for spec $(S) on axis $(dim).", - ) - end - else - # nd2 ≥ 2, index last dimension - lastdim = nd2 - if k isa Int - inds = ntuple(d -> d == lastdim ? (k:k) : Colon(), lastdim) - elseif k isa AbstractUnitRange{<:Int} || k isa Colon - inds = ntuple(d -> d == lastdim ? k : Colon(), lastdim) - else - Base.error( - "Index :k must be Int, Int range, or `:` after normalization; " * - "got $(typeof(k)) for spec $(S) on axis $(dim).", - ) - end - arr = view(arr, inds...) - end - end - - ndims(arr) == 1 || - Base.error( - "AxisSpec $(dim) for $(S) expected to resolve to a 1D slice after indexing; " * - "got $(ndims(arr))-dimensional array.", - ) - - vec_arr = arr - - # --- NamedTuple unwrapping via select_field --- - kfield = select_field(S, Val(dim)) - - if kfield === nothing - return collect(vec_arr) - else - # select_field is interpreted strictly as a spec field name that, when present - # in the resolved input `nt`, holds the Symbol of the NamedTuple field to - # extract. If the field is not present, we *do not* guess: we simply - # return the NamedTuple vector and let higher-level grammar decide what - # to do (overlay all fields, facet, etc.). - if haskey(nt, kfield) - v = getfield(nt, kfield) - v isa Symbol || Base.error( - "Field $(kfield) in resolved input for $(S) on axis $(dim) " * - "must be a Symbol; got $(typeof(v)).", - ) - ksym = v - - first_el = first(vec_arr) - first_el isa NamedTuple || - Base.error( - "select_field($(S), Val($(dim))) expects NamedTuple elements; " * - "got $(typeof(first_el)).", - ) - - haskey(first_el, ksym) || Base.error( - "NamedTuple elements on axis $(dim) for $(S) have no key $(ksym). " * - "Available keys: $(collect(keys(first_el))).", - ) - - return [el[ksym] for el in vec_arr] - else - # No leaf field bound yet; just enforce NamedTuple contract and return as-is. - first_el = first(vec_arr) - first_el isa NamedTuple || - Base.error( - "select_field($(S), Val($(dim))) is defined but resolved input has no " * - "field $(kfield); data elements on axis $(dim) for $(S) must be " * - "NamedTuple; got $(typeof(first_el)).", - ) - return collect(vec_arr) - end - end -end - - -# Process one axis if present -@inline function axis_data( - ::Type{S}, - dim::Symbol, - nt::NamedTuple, - axis::Union{AxisSpec, Nothing}, -) where {S <: AbstractPlotSpec} - axis === nothing && return nothing - - # axis selector: nt.x / nt.y / nt.z - selector = getfield(nt, dim)::Symbol - - # 1) slice + select_field unwrapping (no scaling) - raw_vec = axis_slice(S, nt, axis, Val(dim)) - - # 2) spec-level transform - transformed = axis_transform(S, Val(dim), Val(selector), nt, axis, raw_vec) - - # 3) numeric check - transformed isa AbstractArray || - Base.error( - "AxisSpec $(dim) for $(S) did not resolve to an array; got $(typeof(transformed)).", - ) - - eltype(transformed) <: Number || - Base.error( - "AxisSpec $(dim) for $(S) did not resolve to numeric data; got eltype $(eltype(transformed)).", - ) - - # 4) unit scaling - sf = scale_factor(axis.quantity, axis.units) - return sf .* transformed -end - -""" - make_series(::Type{S}, nt, axes) where {S<:AbstractPlotSpec} - -Builds the vector of SeriesSpec for the given spec and resolved input `nt`. - -Defaults to a single SeriesSpec corresponding to the primary primitive of -this spec, using the axis data computed by `axis_data`. Specs that need -multiple traces (overlays, histogram + CDF, etc.) should override this -method and typically still call `axis_data` under the hood. -""" -function make_series( - ::Type{S}, - nt::NamedTuple, - axes::NamedTuple, -) where {S <: AbstractPlotSpec} - dims = geom_axes(S) - dims = dims isa Tuple ? dims : (dims,) - - xaxis = axes.xaxis - yaxis = axes.yaxis - zaxis = axes.zaxis - - xdata = :x in dims ? axis_data(S, :x, nt, xaxis) : nothing - ydata = :y in dims ? axis_data(S, :y, nt, yaxis) : nothing - zdata = :z in dims ? axis_data(S, :z, nt, zaxis) : nothing - - kind = plot_kind(S) - labels = legend_labels(S, nt) - label = isempty(labels) ? nothing : first(labels) - - series = SeriesSpec( - kind, - xdata, - ydata, - zdata, - label, - ) - - return SeriesSpec[series] -end \ No newline at end of file diff --git a/src/plotbuilder/traits.jl b/src/plotbuilder/traits.jl deleted file mode 100644 index bcff4a498..000000000 --- a/src/plotbuilder/traits.jl +++ /dev/null @@ -1,302 +0,0 @@ - - -# ----------------------------------------------------------------------------- -# Spec-level traits (configuration surface) -# ----------------------------------------------------------------------------- - -""" -Kind of plotting primitive this spec corresponds to. - -Typical values: :line, :heatmap, :hist, :bar, :surface, ... -""" -plot_kind(::Type{S}) where {S <: AbstractPlotSpec} = :unknown - -""" -Axes that can be toggled to log-scale at the UI level. - -Returns a tuple of axis dims, e.g.: - - enable_logscale(::Type{MySpec}) = (:x,) # only x can log - enable_logscale(::Type{OtherSpec}) = (:x,:y) # x and y -""" -enable_logscale(::Type{S}) where {S <: AbstractPlotSpec} = () - -""" -Domain/container type this spec expects to dispatch on. - -Example: - - dispatch_on(::Type{MyRPlotSpec}) = LineParameters -""" -dispatch_on(::Type{S}) where {S <: AbstractPlotSpec} = Any - -""" -Default figure size for this spec type, in pixels. - -This removes hard-coded consts in PlotUIComponents and pushes that -decision into the grammar layer. -""" -default_figsize(::Type{S}) where {S <: AbstractPlotSpec} = (800, 400) - -""" - axis_quantity(::Type{S}, ::Val{dim}) where {S<:AbstractPlotSpec, dim} - -Return the default semantic quantity for axis `dim` in spec `S`, when it -does not depend on which data source is selected. - - axis_quantity(::Type{S}, ::Val{dim}, ::Val{datakey}) - -Higher-ranked variant: given a data selector `datakey` (e.g. :f, :R, :L, :Z), -return the semantic quantity for axis `dim`. - -The `datakey` is a symbol describing *where* data comes from in the container; -it is not necessarily equal to the quantity name used in `QuantityTag{Q}`. -""" -axis_quantity(::Type{S}, dim::Symbol) where {S <: AbstractPlotSpec} = - QuantityTag{:unknown}() - -axis_quantity(::Type{S}, ::Val{dim}) where {S <: AbstractPlotSpec, dim} = - axis_quantity(S, dim) - -axis_quantity( - ::Type{S}, - ::Val{dim}, - ::Val{datakey}, -) where {S <: AbstractPlotSpec, dim, datakey} = - axis_quantity(S, dim) - -""" - index_keys(::Type{S}) where {S<:AbstractPlotSpec} - -Semantic index parameters this spec uses to address elements of its underlying -tensors (e.g. (:i, :j) for matrix-like data, (:i, :j, :k) for 3D, etc.). - -By default, no index keys are assumed. Specs that work on per-element data -over frequencies should typically override this to `(:i, :j)`. -""" -index_keys(::Type{S}) where {S <: AbstractPlotSpec} = () - -""" - ranged_keys(::Type{S}) where {S<:AbstractPlotSpec} - -Index keys among `index_keys(S)` that may also be specified as ranges. - -A ranged index may be: - - • `Int` → single position (e.g. `k = 5`) - • `AbstractUnitRange{<:Int}` → slice (e.g. `k = 1:10`, `k = 3:2:99`) - • `:` → full range - -Default: empty tuple (no ranged indices). -""" -ranged_keys(::Type{S}) where {S <: AbstractPlotSpec} = () - -""" - geom_axes(::Type{S}) where {S<:AbstractPlotSpec} - -Geometric axes used by this spec, in order. -Default is 2D (:x, :y). If your spec is 3D, override to return (:x, :y, :z). -""" -geom_axes(::Type{S}) where {S <: AbstractPlotSpec} = (:x, :y) - - -# -------------------------------------------------------------------------- -# Title / legend grammar traits -# -------------------------------------------------------------------------- - -""" - default_title(::Type{S}, nt) where {S<:AbstractPlotSpec} - -Return the default plot title for this spec, given the resolved input `nt`. -`nt` is the output of `resolve_input(S, ...)`, so its structure is spec-defined. -""" -default_title(::Type{S}, nt::NamedTuple) where {S <: AbstractPlotSpec} = "" - -""" - legend_labels(::Type{S}, nt) where {S<:AbstractPlotSpec} - -Return the legend entry labels for this spec, given the resolved input `nt`. -Length of the returned vector must match the number of primitives produced -by `make_series(S, nt)`. -""" -legend_labels(::Type{S}, nt::NamedTuple) where {S <: AbstractPlotSpec} = String[] - - -# ----------------------------------------------------------------------------- -# Quantity-level unit and label hooks (using UnitHandler) -# ----------------------------------------------------------------------------- - -""" -Spec-level hook: unit for specific spec + axis dim. - -By default, delegates to `display_unit(quantity)`, since plotting is a -display concern. Specs can override for special cases if needed or to -honour user overrides. -""" -axis_unit(::Type{S}, q::QuantityTag, dim::Symbol) where {S <: AbstractPlotSpec} = - display_unit(q) - - -# -------------------------------------------------------------------------- -# Input / backend grammar traits -# -------------------------------------------------------------------------- - -""" - input_kwargs(::Type{S}) where {S<:AbstractPlotSpec} - -Plot-level *semantic* kwargs understood by this spec. - -These describe what is plotted or how the data is selected/sliced -(e.g. :quantity, :stat, :i, :j, :k, :values_expr, ...). -""" -input_kwargs(::Type{S}) where {S <: AbstractPlotSpec} = () - -""" - renderer_kwargs(::Type{S}) where {S<:AbstractPlotSpec} - -Figure kwargs that are simply forwarded to the renderer that will be processed by the backend (Makie today, whatever tomorrow). -""" -renderer_kwargs(::Type{S}) where {S <: AbstractPlotSpec} = () - -""" -Root container inside `obj` for the data of axis `dim`. - -Default: `nothing` → use `obj` itself. - -For example, if `obj.stats` is a NamedTuple of tensors and y-axis data -comes from there, define: - - data_container(::Type{MySpec}, ::Val{:y}) = :stats -""" -data_container(::Type{S}, ::Val{dim}) where {S <: AbstractPlotSpec, dim} = nothing - -# Child key for axis `dim` inside the container returned by `data_container`. -# child_key = getfield(nt, select_field(S, Val(dim))) # e.g. :mean -# data = obj.container.datakey[i,j,k].(child_key) -select_field(::Type{S}, ::Val{dim}) where {S <: AbstractPlotSpec, dim} = nothing - -""" - input_defaults(::Type{S}, obj) where {S<:AbstractPlotSpec} - -Defaults for semantic kwargs declared in `input_kwargs(S)`. - -May depend on the dispatched object `obj` (e.g. pick default `:quantity` -from `obj` contents). -""" -input_defaults(::Type{S}, obj) where {S <: AbstractPlotSpec} = NamedTuple() - -""" - renderer_defaults(::Type{S}, obj) where {S<:AbstractPlotSpec} - -Defaults for figure kwargs declared in `renderer_kwargs(S)`. - -This is where a spec declares its default color/linestyle/whatever, -possibly depending on `obj` (e.g. per-phase colors). -""" -renderer_defaults(::Type{S}, obj) where {S <: AbstractPlotSpec} = NamedTuple() - -""" -How to group dataseries into figures. -Options: :auto -> let the machinery decide; - :single -> one dataseries in one plot area (view), same axis; - :overlay_ij -> one plot area (view), overlay all (i,j) on the same axis - target/leaf resolved to one field; - :overlay_fields -> one plot area (view), overlay all fields on the same axis - data container resolved to one pair (i,j). - :per_ij_overlay_fields -> multiple plot areas (views), one per (i,j), overlay all fields. -""" -grouping_mode(::Type{S}) where {S <: AbstractPlotSpec} = :auto - -""" -How to render figures into Makie windows. -Options: :single -> one view per window; - :grid -> all views in a single window, arranged in a grid; - :tabs -> TBD: all views in a single window, arranged in tabs. -""" -figure_layout(::Type{S}) where {S <: AbstractPlotSpec} = :single # default - -# -------------------------------------------------------------------------- -# Complex quantity / "as" traits (default: disabled) -# -------------------------------------------------------------------------- - -has_complex_qty( - ::Type{S}, - ::Val{dim}, - ::Val{datakey}, -) where {S <: AbstractPlotSpec, dim, datakey} = - false - -complex_as( - ::Type{S}, - ::Val{dim}, - ::Val{datakey}, -) where {S <: AbstractPlotSpec, dim, datakey} = - (:re, :im, :abs, :angle) - -complex_as_default( - ::Type{S}, - ::Val{dim}, - ::Val{datakey}, -) where {S <: AbstractPlotSpec, dim, datakey} = - :re - -# View-aware axis_quantity: fallback keeps existing grammar intact -axis_quantity( - ::Type{S}, - ::Val{dim}, - ::Val{datakey}, - ::Val{as}, -) where {S <: AbstractPlotSpec, dim, datakey, as} = - axis_quantity(S, Val(dim), Val(datakey)) - -# Z: re/im correspond to R/X -axis_quantity( - ::Type{S}, - ::Val{dim}, - ::Val{:Z}, - ::Val{:re}, -) where {S <: AbstractPlotSpec, dim} = QuantityTag{:resistance}() -axis_quantity( - ::Type{S}, - ::Val{dim}, - ::Val{:Z}, - ::Val{:im}, -) where {S <: AbstractPlotSpec, dim} = QuantityTag{:reactance}() - -# Y: re/im correspond to G/B -axis_quantity( - ::Type{S}, - ::Val{dim}, - ::Val{:Y}, - ::Val{:re}, -) where {S <: AbstractPlotSpec, dim} = QuantityTag{:conductance}() -axis_quantity( - ::Type{S}, - ::Val{dim}, - ::Val{:Y}, - ::Val{:im}, -) where {S <: AbstractPlotSpec, dim} = QuantityTag{:susceptance}() - -axis_quantity( - ::Type{S}, - ::Val{dim}, - ::Val{:Z}, - ::Val{:abs}, -) where {S <: AbstractPlotSpec, dim} = QuantityTag{(:impedance, :abs)}() -axis_quantity( - ::Type{S}, - ::Val{dim}, - ::Val{:Z}, - ::Val{:angle}, -) where {S <: AbstractPlotSpec, dim} = QuantityTag{(:impedance, :angle)}() - -axis_quantity( - ::Type{S}, - ::Val{dim}, - ::Val{:Y}, - ::Val{:abs}, -) where {S <: AbstractPlotSpec, dim} = QuantityTag{(:admittance, :abs)}() -axis_quantity( - ::Type{S}, - ::Val{dim}, - ::Val{:Y}, - ::Val{:angle}, -) where {S <: AbstractPlotSpec, dim} = QuantityTag{(:admittance, :angle)}() diff --git a/src/plotbuilder/types.jl b/src/plotbuilder/types.jl deleted file mode 100644 index 8e3d1f743..000000000 --- a/src/plotbuilder/types.jl +++ /dev/null @@ -1,104 +0,0 @@ -abstract type AbstractPlotSpec end - -""" -AxisSpec is the fully decided axis descriptor used by plot areas (views). - -- `dim` : axis dimension (e.g. :x, :y, :z) -- `quantity` : semantic quantity tag (from UnitHandler) -- `units` : unit system for this axis -- `label` : full label text, including unit symbol -- `scale` : :linear or :log10 -""" -struct AxisSpec - dim::Symbol - quantity::QuantityTag - units::Units - label::String - scale::Symbol -end - -# -------------------------------------------------------------------------- -# Payload hierarchy: series → view → figure → renderer -# -------------------------------------------------------------------------- -""" - SeriesSpec - -Single plot primitive (one Makie call). - -Fields: -- `kind` : plotting primitive kind (:line, :scatter, :hist, :heatmap, ...) -- `xdata` : x values \\[dimensionless or scaled\\] -- `ydata` : y values \\[dimensionless or scaled\\] -- `zdata` : z values if applicable, otherwise `nothing` -- `label` : legend entry for this series, or `nothing` for no legend -""" -struct SeriesSpec - kind :: Symbol - xdata :: Union{Nothing, AbstractVector{<:Number}} - ydata :: Union{Nothing, AbstractArray{<:Number}} - zdata :: Union{Nothing, AbstractArray{<:Number}} - label :: Union{Nothing, String} -end - -""" - ViewSpec - -One plot view / axis system. - -All SeriesSpec inside a ViewSpec share the same x/y(/z) axes. The `key` -field encodes the semantic facet this area represents (e.g. \\(i,j\\), -quantity, frequency). - -Fields: -- `xaxis` : x AxisSpec or `nothing` if unused -- `yaxis` : y AxisSpec or `nothing` if unused -- `zaxis` : z AxisSpec or `nothing` if unused -- `title` : view title -- `series` : vector of SeriesSpec -- `key` : NamedTuple identifying the facet, or empty `NamedTuple` if none -""" -struct ViewSpec - xaxis :: Union{Nothing, AxisSpec} - yaxis :: Union{Nothing, AxisSpec} - zaxis :: Union{Nothing, AxisSpec} - title :: String - series :: Vector{SeriesSpec} - key :: NamedTuple -end - -""" - PageSpec - -One logical figure / window. - -Fields: -- `title` : optional figure-level title (may be empty) -- `size` : (width, height) in pixels -- `layout` : layout spec (:windows, :grid, :tabbed, ...) -- `views` : vector of ViewSpec values contained in this figure -- `kwargs` : figure-level backend options (e.g. figsize) -""" -struct PageSpec - title :: String - size :: Tuple{Int, Int} - layout :: Symbol - views :: Vector{ViewSpec} - kwargs :: NamedTuple -end - -""" - RenderSpec{S} - -Final product of the grammar pipeline for spec type `S`. - -Carries only: -- the spec type (the grammar definition), -- a vector of PageSpec payloads. - -The rendering backend (Makie) should only see RenderSpec values and must -never touch domain objects or grammar logic. -""" -struct RenderSpec{S <: AbstractPlotSpec} - spec :: Type{S} - figures :: Vector{PageSpec} -end diff --git a/src/plotbuilder/uicomponents/UIComponents.jl b/src/plotbuilder/uicomponents/UIComponents.jl deleted file mode 100644 index 8570c07b2..000000000 --- a/src/plotbuilder/uicomponents/UIComponents.jl +++ /dev/null @@ -1,23 +0,0 @@ -module UIComponents - -using Makie - -import ..BackendHandler - -import ..PlotBuilder: AbstractPlotSpec, RenderSpec, PageSpec, ViewSpec, SeriesSpec, AxisSpec - -export build, export_svg!, - UIContext, UILayoutSpec, UIContainerSpec, UISlotSpec, - UIFigure, UIPanel, PlotAssembly - -export build_context, display! - -include("themes.jl") -include("types.jl") -include("layoutspecs.jl") -include("actions.jl") -include("draw.jl") -include("widgets.jl") -include("pipeline.jl") - -end # module UIComponents diff --git a/src/plotbuilder/uicomponents/actions.jl b/src/plotbuilder/uicomponents/actions.jl deleted file mode 100644 index 9b0ef4a46..000000000 --- a/src/plotbuilder/uicomponents/actions.jl +++ /dev/null @@ -1,67 +0,0 @@ -function action_set_status!(ctx::UIContext, msg::AbstractString) - if ctx.status !== nothing - ctx.status[] = String(msg) - end - return nothing -end - -# Adapting signature to match build! call (ctx, uifig, btn) -> needs mapping to Assembly? -# Ideally, we pass the Assembly. -# We can rely on the fact that UIPlot is the container. -# Let's assume the action receives (ctx, assem::UIPlot, widget) -# The build! loop in pipeline.jl needs to wrap this. -# Correction: In pipeline.jl, we can't fully bind UIPlot because it's being built. -# However, `uifig` contains everything graphical. -# `action_refresh` needs access to panels. `uifig` does NOT store panels directly (UIPlot does). -# We should store panels in uifig or return to pipeline to bind actions AFTER UIPlot creation. -# Let's fix pipeline.jl logic in the next iteration or use a workaround here. -# Workaround: action_refresh takes `uifig` and assumes it can find axes? -# No, `uifig` has `containers`. We can iterate `uifig.slots[:canvas].content`. - -function action_refresh(uifig::UIFigure) - # Iterate over axes in the canvas slot - canvas = uifig.slots[:canvas] - for content in canvas.content - if content.content isa Makie.Axis - Makie.autolimits!(content.content) - end - end - return nothing -end - -# Signature overload for compatibility if called with UIPlot -action_refresh(assem::UIPlot) = action_refresh(assem.uifig) - -function action_export_svg!(ctx::UIContext, assem_or_fig, path::AbstractString) - # We need the spec and page to re-render. - # If we only have uifig, we are stuck. - # The widgets need the UIPlot. - # FIX: The widgets must be wired up AFTER UIPlot is created in pipeline.jl. - # See note in pipeline.jl. - error("Export requires full UIPlot assembly context.") -end - -function action_export_svg!(ctx::UIContext, assem::UIPlot, path::AbstractString) - action_set_status!(ctx, "Exporting to $path...") - - BackendHandler.with_backend(:cairo) do - # THEME SWITCHING: Force interactive=false for export style - export_theme = make_theme(ctx; interactive = false) - - Makie.with_theme(export_theme) do - # Reconstruct RenderSpec from the UIPlot data - r = RenderSpec(assem.spec, PageSpec[assem.page]) - - # Render headless - new_assems = render(r; backend = :cairo, display = false) - - if !isempty(new_assems) - target_fig = new_assems[1].uifig.figure - Makie.save(path, target_fig) - end - end - end - - action_set_status!(ctx, "Saved SVG to $path") - return nothing -end diff --git a/src/plotbuilder/uicomponents/draw.jl b/src/plotbuilder/uicomponents/draw.jl deleted file mode 100644 index 3ec32ee75..000000000 --- a/src/plotbuilder/uicomponents/draw.jl +++ /dev/null @@ -1,54 +0,0 @@ -# ------------------------- -# The Artist: draw! -# ------------------------- - -function draw!(axis, s::SeriesSpec; kwargs...) - draw!(Val(s.kind), axis, s; kwargs...) -end - -function draw!(::Val{kind}, axis, s::SeriesSpec; kwargs...) where {kind} - @warn "Unsupported plot kind :$kind" - return Any[] -end - -function draw!(::Val{:line}, axis, s::SeriesSpec; kwargs...) - (s.xdata === nothing || s.ydata === nothing) && return Any[] - - plots = Any[] - if s.ydata isa AbstractMatrix - for k in 1:size(s.ydata, 2) - # PBSeries/SeriesSpec only supports one label. - # We label the first trace for the legend. - lbl = (k==1) ? s.label : nothing - p = Makie.lines!(axis, s.xdata, view(s.ydata, :, k); label = lbl, kwargs...) - push!(plots, p) - end - else - p = Makie.lines!(axis, s.xdata, s.ydata; label = s.label, kwargs...) - push!(plots, p) - end - return plots -end - -function draw!(::Val{:scatter}, axis, s::SeriesSpec; kwargs...) - (s.xdata === nothing || s.ydata === nothing) && return Any[] - - plots = Any[] - if s.ydata isa AbstractMatrix - for k in 1:size(s.ydata, 2) - lbl = (k==1) ? s.label : nothing - p = Makie.scatter!(axis, s.xdata, view(s.ydata, :, k); label = lbl, kwargs...) - push!(plots, p) - end - else - p = Makie.scatter!(axis, s.xdata, s.ydata; label = s.label, kwargs...) - push!(plots, p) - end - return plots -end - -function draw!(::Val{:heatmap}, axis, s::SeriesSpec; kwargs...) - (s.xdata === nothing || s.ydata === nothing || s.zdata === nothing) && return Any[] - p = Makie.heatmap!(axis, s.xdata, s.ydata, s.zdata; kwargs...) - return Any[p] -end \ No newline at end of file diff --git a/src/plotbuilder/uicomponents/layoutspecs.jl b/src/plotbuilder/uicomponents/layoutspecs.jl deleted file mode 100644 index 3a3f9f6c9..000000000 --- a/src/plotbuilder/uicomponents/layoutspecs.jl +++ /dev/null @@ -1,75 +0,0 @@ -# ------------------------- -# The Architect: make_layout -# ------------------------- - -function make_layout(::Val{layout}) where {layout} - error("make_layout(Val(:$layout)) not implemented") -end - -function make_layout(::Val{:single}) - # 1. ROOT CONFIGURATION (The global visual effect) - # We define a container for :root to apply gaps and padding. - root = UIContainerSpec( - :root, nothing, (1, 1); # Position ignored for root - layout = (; - rowgap = GRID_ROW_GAP, - colgap = LEGEND_GAP, # The gap between Canvas and Legend - alignmode = Makie.Outside(FIG_PADDING...), # (L, R, B, T) - ), - ) - - # 2. SLOTS - slots = [ - # Toolbar: Rigid height, internal spacing for buttons - UISlotSpec(:toolbar, :root, (1, 1); - layout = (; - height = CTLBAR_HEIGHT, - tellheight = true, - colgap = CTLBAR_GAP # Spacing between buttons - ), - ), - - # Canvas: Takes available space - UISlotSpec(:canvas, :root, (2, 1); - layout = (; - alignmode = Makie.Inside() # Standard plot behavior - ), - ), - - # Status: Rigid height - UISlotSpec(:status, :root, (3, 1); - layout = (; - height = STATUSBAR_HEIGHT, - tellheight = true, - ), - ), - - # Legend: Fixed width column - UISlotSpec(:legend, :root, (1:3, 2); - layout = (; - width = LEGEND_WIDTH, # Enforce width at slot level too for safety - tellwidth = true, - alignmode = Makie.Inside(), - ), - attrs = (; valign = :top), - ), - ] - - # 3. ROOT SIZING (Structural constraints) - rs = Dict( - :root => Any[ - Makie.Fixed(CTLBAR_HEIGHT), - Makie.Relative(1.0), - Makie.Fixed(STATUSBAR_HEIGHT), - ], - ) - - cs = Dict(:root => Any[ - Makie.Relative(1.0), - Makie.Fixed(LEGEND_WIDTH), - ]) - - return UILayoutSpec(:single, [root], slots, rs, cs) -end - -make_layout(::Val{:grid}) = make_layout(Val(:single)) \ No newline at end of file diff --git a/src/plotbuilder/uicomponents/pipeline.jl b/src/plotbuilder/uicomponents/pipeline.jl deleted file mode 100644 index 276631779..000000000 --- a/src/plotbuilder/uicomponents/pipeline.jl +++ /dev/null @@ -1,310 +0,0 @@ -# ------------------------- -# The Architect: make_context -# ------------------------- - -function make_context(; - backend::Union{Nothing, Symbol} = nothing, - display::Bool = true, - title::AbstractString = "LineCableModels Plot", - theme::Union{Nothing, Makie.Theme} = nothing, - use_latex_fonts::Bool = false, - kwargs..., -) - active_backend = BackendHandler.ensure_backend!(backend) - interactive = (display && active_backend in (:gl, :wgl)) - - stat = interactive ? Makie.Observable("Ready.") : nothing - - win = nothing - scr = nothing - if interactive && active_backend == :gl - scr = BackendHandler.make_screen(title; backend = :gl) - win = scr - end - - # Default theme - default_theme = make_theme(; interactive, use_latex_fonts) - - # 2. Build official theme (uses ctx.interactive by default) - built_theme = theme === nothing ? default_theme : merge(default_theme, theme) - - return UIContext( - active_backend, - interactive, - use_latex_fonts, - win, - scr, - stat, - built_theme, - ) -end - -# ------------------------- -# The Boss: render -# ------------------------- - -function build( - r::RenderSpec{S}; - backend = nothing, - display::Bool = true, - kwargs..., -) where {S} - ctx = make_context(; backend = backend, display = display, kwargs...) - assemblies = UIPlot[] - - Makie.with_theme(ctx.theme) do - for page in r.figures # page is PageSpec - - # A. Architect - layout_s = make_layout(Val(page.layout)) - - # B. Constructor (Shell) - uifig = build_figure(ctx, page, layout_s) - - # C. Constructor (Panels) - panels = UIPanel[] - for view in page.views # view is ViewSpec - push!(panels, build_panel!(uifig, view)) - end - - # D. Constructor (Decorations) - widgets_s = make_widgets(S, ctx, page, panels) - widgets_dict = build_toolbar!(uifig, widgets_s, ctx) - - build_statusbar!(uifig, Val(:status), ctx) - build_legend!(uifig, Val(:legend), panels) - - # E. Assembly - assem = UIPlot(S, ctx, page, uifig, panels, widgets_dict) - push!(assemblies, assem) - - if display - display!(ctx, assem) - end - end - end - return assemblies -end - -# ------------------------- -# The Constructors: build_ / build_! -# ------------------------- - -# Helper to find spec for a slot name (to retrieve attrs) -function get_slot_spec(uifig::UIFigure, name::Symbol) - idx = findfirst(s -> s.name == name, uifig.layoutspec.slots) - return idx === nothing ? nothing : uifig.layoutspec.slots[idx] -end - -function build_figure(ctx::UIContext, page::PageSpec, ls::UILayoutSpec) - kw = page.kwargs - safe_kw = (; (k=>v for (k, v) in pairs(kw) if k != :size && k != :resolution)...) - - fig = Makie.Figure(; size = page.size, safe_kw...) - - containers = Dict{Symbol, Makie.GridLayout}() - slots = Dict{Symbol, Any}() - - containers[:root] = fig.layout - - # --- PHASE 1: Materialize Slots/Containers --- - # Uses `s.layout` for Grid properties - - # 1a. Intermediate Containers (and Root configuration) - for c in ls.containers - if c.name == :root - # SPECIAL CASE: Configuration for the main Figure layout - # Apply gaps, alignmode (padding), etc. - gl = containers[:root] - for (k, v) in pairs(c.layout) - # We use setproperty! or specific Makie functions for gaps - if k == :rowgap - Makie.rowgap!(gl, v) - elseif k == :colgap - Makie.colgap!(gl, v) - else - # alignmode, etc. - setproperty!(gl, k, v) - end - end - else - # Standard nested container creation - parent_gl = containers[c.parent] - subgl = Makie.GridLayout(; c.layout...) - parent_gl[c.at...] = subgl - containers[c.name] = subgl - end - end - - # 1b. Slots (Terminals) - for s in ls.slots - parent_gl = containers[s.parent] - subgl = Makie.GridLayout(; s.layout...) - parent_gl[s.at...] = subgl - slots[s.name] = subgl - - # --- DEBUG: VISUALIZE SLOTS --- - # Makie.Box(parent_gl[s.at...], color = (:red, 0.2), strokewidth = 0) - end - - # --- PHASE 2: Apply Sizing --- - - for (name, sizes) in ls.rowsizes - gl = get(containers, name, nothing) - gl === nothing && continue - for (i, s) in enumerate(sizes) - Makie.rowsize!(gl, i, s) - end - end - - for (name, sizes) in ls.colsizes - gl = get(containers, name, nothing) - gl === nothing && continue - for (i, s) in enumerate(sizes) - Makie.colsize!(gl, i, s) - end - end - - # Grid Shape Logic - n = length(page.views) - panel_shape = (1, 1) - if n > 1 - nr = ceil(Int, sqrt(n)) - nc = ceil(Int, n / nr) - panel_shape = (nr, nc) - end - - return UIFigure(fig, ls, containers, slots, Ref(0), panel_shape) -end - -function build_panel!(uifig::UIFigure, view::ViewSpec) - target_gl = uifig.slots[:canvas] - nr, nc = uifig.panelshape - - ax_pos = if nr > 1 || nc > 1 - uifig.cursor[] += 1 - k = uifig.cursor[] - row = (k - 1) ÷ nc + 1 - col = (k - 1) % nc + 1 - target_gl[row, col] - else - target_gl[1, 1] - end - - # Retrieve 'attrs' for content - slot_spec = get_slot_spec(uifig, :canvas) - slot_attrs = slot_spec !== nothing ? slot_spec.attrs : (;) - - ax = Makie.Axis(ax_pos; - xlabel = something(view.xaxis.label, ""), - ylabel = something(view.yaxis.label, ""), - title = view.title, - xscale = (view.xaxis.scale == :log10) ? Makie.log10 : Makie.identity, - yscale = (view.yaxis.scale == :log10) ? Makie.log10 : Makie.identity, - slot_attrs..., - ) - - plots = Any[] - for s in view.series # s is SeriesSpec - append!(plots, draw!(ax, s)) - end - - return UIPanel(view, ax, plots) -end - -function build_toolbar!(uifig::UIFigure, specs::Vector{UIWidgetSpec}, ctx::UIContext) - dict = Dict{Symbol, Any}() - haskey(uifig.slots, :toolbar) || return dict - - gl = uifig.slots[:toolbar] - gl.halign = :left - - for (i, s) in enumerate(specs) - # --- BUTTON --- - if s isa UIButtonSpec - lbl = (s.icon !== nothing) ? with_icon(s.icon; text = s.label) : s.label - - btn = Makie.Button(gl[1, i]; label = lbl, s.attrs...) - dict[Symbol(:btn_, i)] = btn - - Makie.on(btn.clicks) do _ - Base.@async begin - try - s.action(ctx, uifig, btn) - catch e - @error "Widget error" exception=(e, catch_backtrace()) - action_set_status!(ctx, "Error: $(e)") - end - end - end - - # --- TOGGLE --- - elseif s isa UIToggleSpec - # Container for [Label | Toggle] to keep them grouped in the toolbar slot - sub = Makie.GridLayout(gl[1, i]) - - # 1. Label - Makie.Label(sub[1, 1], s.label, halign = :right) - - # 2. Toggle - tgl = Makie.Toggle(sub[1, 2]; active = s.active, s.attrs...) - dict[Symbol(:tgl_, i)] = tgl - - Makie.on(tgl.active) do val - Base.@async begin - try - s.action(ctx, uifig, val) - catch e - @error "Widget error" exception=(e, catch_backtrace()) - action_set_status!(ctx, "Error: $(e)") - end - end - end - - # Tweak subgrid spacing - Makie.colgap!(sub, 4) - end - end - return dict -end - -function build_statusbar!(uifig::UIFigure, ::Val{:status}, ctx::UIContext) - haskey(uifig.slots, :status) || return - gl = uifig.slots[:status] - txt = (ctx.status !== nothing) ? ctx.status : Makie.Observable("") - Makie.Label(gl[1, 1], txt, halign = :left, fontsize = 12) -end - -function build_legend!(uifig::UIFigure, ::Val{:legend}, panels::Vector{UIPanel}) - haskey(uifig.slots, :legend) || return - - seen = Set{String}() - elements = Any[] - labels = String[] - - for p in panels, plt in p.plots - if hasproperty(plt, :label) - lbl = plt.label[] - if lbl !== nothing && !isempty(lbl) && !(lbl in seen) - push!(elements, plt) - push!(labels, lbl) - push!(seen, lbl) - end - end - end - - if !isempty(elements) - slot_spec = get_slot_spec(uifig, :legend) - slot_attrs = slot_spec !== nothing ? slot_spec.attrs : (;) - - Makie.Legend(uifig.slots[:legend][1, 1], elements, labels; slot_attrs...) - end -end - -function display!(ctx::UIContext, assem::UIPlot) - if ctx.interactive && ctx.window !== nothing - display(ctx.window, assem.uifig.figure) - else - BackendHandler.renderfig(assem.uifig.figure) - end -end \ No newline at end of file diff --git a/src/plotbuilder/uicomponents/themes.jl b/src/plotbuilder/uicomponents/themes.jl deleted file mode 100644 index 53f554d3b..000000000 --- a/src/plotbuilder/uicomponents/themes.jl +++ /dev/null @@ -1,108 +0,0 @@ -using Printf: @sprintf - -# ----------------------------------------------------------------------------- -# Constants -# ----------------------------------------------------------------------------- - -const FIG_SIZE = (800, 600) -const FIG_PADDING = (80, 60, 40, 40) # left, right, bottom, top -const CTLBAR_HEIGHT = 36 -const STATUSBAR_HEIGHT = 20 -const GRID_ROW_GAP = 6 -const GRID_COL_GAP = 6 -const LEGEND_GAP = 4 -const LEGEND_WIDTH = 140 -const COLORBAR_GAP = 4 -const CTLBAR_GAP = 2 -const BUTTON_MIN_WIDTH = 32 -const BUTTON_ICON_SIZE = 18 -const BUTTON_TEXT_FONT_SIZE = 15 -const AXIS_TITLE_FONT_SIZE = 15 -const AXIS_LABEL_FONT_SIZE = 14 -const AXIS_TICK_FONT_SIZE = 14 -const STATUS_FONT_SIZE = 10 -const BG_COLOR_INTERACTIVE = :grey90 -const BG_COLOR_EXPORT = :white -const ICON_COLOR_ACTIVE = Makie.RGBAf(0.15, 0.15, 0.15, 1.0) -const ICON_COLOR_DISABLED = Makie.RGBAf(0.55, 0.55, 0.55, 1.0) -const TICK_FMT = x -> @sprintf("%.4g", x) -const TICKFORMATTER = values -> [TICK_FMT(v) for v in values] -const EXPORT_TIMESTAMP_FORMAT = "yyyymmdd_HHMMSS" -const EXPORT_EXTENSION = "svg" - -# ----------------------------------------------------------------------------- -# Material UI icons -# ----------------------------------------------------------------------------- -const MI_REFRESH = "\uE5D5" # Material Icons: 'refresh' -const MI_SAVE = "\uE161" # Material Icons: 'save' - -# ----------------------------------------------------------------------------- -# Helpers -# ----------------------------------------------------------------------------- - -with_icon(icon::AbstractString; text::AbstractString = "", - isize::Int = BUTTON_ICON_SIZE, tsize::Int = BUTTON_TEXT_FONT_SIZE, color = :black, - gap::Int = 2, - dy_icon::Float64 = -0.18, dy_text::Float64 = 0.0) = - text == "" ? - rich(icon; font = :icons, fontsize = isize, color = color, offset = (0, dy_icon)) : - rich( - rich(icon; font = :icons, fontsize = isize, color = color, offset = (0, dy_icon)), - rich(" "^gap; font = :regular, fontsize = tsize, color = color), - rich(text; font = :regular, fontsize = tsize, color = color, offset = (0, dy_text)), - ) - -""" - make_theme(; interactive::Bool, use_latex_fonts::Bool) - -Returns the package-specific Theme delta. -""" -function make_theme(; interactive::Bool, use_latex_fonts::Bool) - background = interactive ? BG_COLOR_INTERACTIVE : BG_COLOR_EXPORT - - # Base configuration - config = Dict{Symbol, Any}( - :backgroundcolor => background, - :Axis => ( - titlesize = AXIS_TITLE_FONT_SIZE, - xlabelsize = AXIS_LABEL_FONT_SIZE, - ylabelsize = AXIS_LABEL_FONT_SIZE, - xticklabelsize = AXIS_TICK_FONT_SIZE, - yticklabelsize = AXIS_TICK_FONT_SIZE, - xtickformat = TICKFORMATTER, - ytickformat = TICKFORMATTER, - ), - :Legend => ( - fontsize = AXIS_LABEL_FONT_SIZE, - labelsize = AXIS_LABEL_FONT_SIZE, - ), - :Colorbar => ( - labelsize = AXIS_LABEL_FONT_SIZE, - ticklabelsize = AXIS_TICK_FONT_SIZE, - ), - ) - - # Conditional logic: Fonts - # 1. Latex fonts (Export only) - if use_latex_fonts && !interactive - # merge! is safe on Dicts - merge!(config, Makie.theme_latexfonts().attributes) - end - - # 2. Icon fonts (Always try to load) - font_path = joinpath( - pkgdir(@__MODULE__), - "assets", - "fonts", - "material-icons", - "MaterialIcons-Regular.ttf", - ) - if isfile(font_path) - current_fonts = get(config, :fonts, (;)) - # Convert to NamedTuple to simple merge - new_fonts = merge(current_fonts, (; icons = font_path)) - config[:fonts] = new_fonts - end - - return Makie.Theme(; config...) -end diff --git a/src/plotbuilder/uicomponents/types.jl b/src/plotbuilder/uicomponents/types.jl deleted file mode 100644 index bc0f70db2..000000000 --- a/src/plotbuilder/uicomponents/types.jl +++ /dev/null @@ -1,95 +0,0 @@ -# ------------------------- -# UI Logic Specs (Recipes) -# ------------------------- - -struct UIContainerSpec - name::Symbol - parent::Union{Nothing, Symbol} - at::Tuple # Relaxed from strict Union types to generic Tuple - layout::NamedTuple -end - -UIContainerSpec(name, parent, at; layout = (;)) = - UIContainerSpec(name, parent, at, layout) - -struct UISlotSpec - name::Symbol - parent::Symbol - at::Tuple # Relaxed from strict Union types - layout::NamedTuple # Grid properties (e.g. alignmode, height) - attrs::NamedTuple # Content properties (e.g. Axis background, Legend align) -end - -# Robust helper constructor -UISlotSpec(name, parent, at; layout = (;), attrs = (;)) = - UISlotSpec(name, parent, at, layout, attrs) - -struct UILayoutSpec - name::Symbol - containers::Vector{UIContainerSpec} - slots::Vector{UISlotSpec} - rowsizes::Dict{Symbol, Vector{Any}} - colsizes::Dict{Symbol, Vector{Any}} -end - -abstract type UIWidgetSpec end - -struct UIButtonSpec <: UIWidgetSpec - label::String - icon::Union{Nothing, String} - action::Function # (ctx, uifig, button) -> nothing - attrs::NamedTuple # Passed to Makie.Button -end - -# Constructor -UIButtonSpec(label, icon, action; attrs = (;)) = - UIButtonSpec(label, icon, action, attrs) - -struct UIToggleSpec <: UIWidgetSpec - label::String - active::Bool - action::Function # (ctx, uifig, active::Bool) -> nothing - attrs::NamedTuple # Passed to Makie.Toggle -end - -# Constructor -UIToggleSpec(label, active, action; attrs = (;)) = - UIToggleSpec(label, active, action, attrs) - -# ------------------------- -# UI Instances (Objects) -# ------------------------- - -mutable struct UIContext - backend::Symbol - interactive::Bool - use_latex_fonts::Bool - window::Union{Nothing, Any} - screen::Union{Nothing, Any} - status::Union{Nothing, Makie.Observable{String}} - theme::Makie.Theme -end - -struct UIFigure - figure::Makie.Figure - layoutspec::UILayoutSpec - containers::Dict{Symbol, Makie.GridLayout} - slots::Dict{Symbol, Any} - cursor::Base.RefValue{Int} - panelshape::Tuple{Int, Int} -end - -struct UIPanel - view::ViewSpec - axis::Any - plots::Vector{Any} -end - -struct UIPlot - spec::DataType - ctx::UIContext - page::PageSpec - uifig::UIFigure - panels::Vector{UIPanel} - widgets::Dict{Symbol, Any} -end \ No newline at end of file diff --git a/src/plotbuilder/uicomponents/widgets.jl b/src/plotbuilder/uicomponents/widgets.jl deleted file mode 100644 index df2664aff..000000000 --- a/src/plotbuilder/uicomponents/widgets.jl +++ /dev/null @@ -1,34 +0,0 @@ -# ------------------------- -# Traits (Declarative) -# ------------------------- - -function controls_default(::Type{S}, ctx::UIContext, page, panels) where {S} - !ctx.interactive && return UIWidgetSpec[] - - return UIWidgetSpec[ - UIButtonSpec( - "", - MI_REFRESH, - (c, a, o) -> action_refresh(a), - ), - UIButtonSpec( - "", - MI_SAVE, - (c, a, o) -> action_export_svg!(c, a, "plot_export.svg"), - ), - ] -end - -function controls_custom(::Type{S}, ctx, page, panels) where {S} - return UIWidgetSpec[] -end - -# ------------------------- -# The Architect: make_widgets -# ------------------------- - -function make_widgets(::Type{S}, ctx::UIContext, page, panels) where {S} - w = controls_default(S, ctx, page, panels) - append!(w, controls_custom(S, ctx, page, panels)) - return w -end \ No newline at end of file diff --git a/src/plotbuilder/viewspec.jl b/src/plotbuilder/viewspec.jl deleted file mode 100644 index 65f5d46f7..000000000 --- a/src/plotbuilder/viewspec.jl +++ /dev/null @@ -1,31 +0,0 @@ -""" - make_views(::Type{S}, nt, axes, series) where {S<:AbstractPlotSpec} - -Groups SeriesSpec into ViewSpec values. - -Default behavior: -- all series share the same axes `axes.xaxis`, `axes.yaxis`, `axes.zaxis`, -- one ViewSpec is created, -- `title` is taken from `default_title(S, nt)`, -- `key` is the empty NamedTuple `(; )` (no faceting semantics). - -Specs that require multiple views (e.g. grids over indices or frequencies) -should override this method and partition `series` accordingly, setting -a meaningful `key` for each ViewSpec. -""" -function make_views( - ::Type{S}, - nt::NamedTuple, - axes::NamedTuple, - series::Vector{SeriesSpec}, -) where {S <: AbstractPlotSpec} - title = default_title(S, nt) - key = (;) - - xaxis = axes.xaxis - yaxis = axes.yaxis - zaxis = axes.zaxis - - view = ViewSpec(xaxis, yaxis, zaxis, title, series, key) - return ViewSpec[view] -end \ No newline at end of file diff --git a/src/pscad/PSCAD.jl b/src/pscad/PSCAD.jl new file mode 100644 index 000000000..839f54741 --- /dev/null +++ b/src/pscad/PSCAD.jl @@ -0,0 +1,52 @@ +""" + PSCAD + +PSCAD model exchange and native line-parameter computation. Loading this module +defines the backend without contacting a station or launching PSCAD. +""" +module PSCAD + +using Base64: base64encode +using SHA: sha256 +import TOML +import Logging +using LineCableModels +using LineCableModels.DataModel: LineCableSystem +using LineCableModels.Earth: EarthModel +using LineCableModels.Commons: vacuum_permittivity +using LineCableModels.Engine +using LineCableModels.ImportExport +import LineCableModels: description, parameterize, computation_details, validate, + Expression, verbosity +import LineCableModels.Engine.EarthImpedance: earth_impedance +import LineCableModels.Engine.EarthAdmittance: earth_potential_coefficient +import LineCableModels.Engine.InternalImpedance: internal_impedance +import LineCableModels.Engine.InsulationImpedance: insulation_impedance +import LineCableModels.Engine: Formulation, + LineParametersProblem +import LineCableModels.Commons: AbstractFormulation, ComputationOptions, ComputationDetails, + FormulationOptions, computation_options, compute, + formulation_options, gridpoint_id +using DocStringExtensions: TYPEDSIGNATURES, TYPEDEF, TYPEDFIELDS +import LineCableModels: constitutive +import LineCableModels.DataModel +import LineCableModels.Engine +import LineCableModels.ImportExport: import_data, export_data +import EzXML +using EzXML: ElementNode, XMLDocument, addelement!, nodename, + readxml, root, setroot! + +export PSCADFormulation, RemoteConfig, + read_pscad_result, remote_command, run_remote_pscad, identify +public pscad_setting + +include("formulations.jl") +include("importexport/pscad.jl") +include("results.jl") +include("validate.jl") +include("remote/configuration.jl") +include("remote/files.jl") +include("remote/remote.jl") +include("compute.jl") + +end diff --git a/src/pscad/compute.jl b/src/pscad/compute.jl new file mode 100644 index 000000000..1a1d487ff --- /dev/null +++ b/src/pscad/compute.jl @@ -0,0 +1,420 @@ +function computation_options( + ::Type{PSCADFormulation}, + record::ComputationOptions +)::ComputationOptions + options = record.data + allowed = (:output_stem, :remote, :verbosity, :output_basis, :on_result, + :resume_run_directory, :solver_identity, :work_root, :timing) + unknown = filter(key -> key ∉ allowed, keys(options)) + isempty(unknown) || throw(ArgumentError( + "unknown PSCAD computation options: $(sort!(collect(unknown)))", + )) + haskey(options, :remote) || throw(ArgumentError( + "PSCAD computation options must define remote::RemoteConfig", + )) + options.remote isa RemoteConfig || throw(ArgumentError( + "PSCAD computation option remote must be a RemoteConfig", + )) + normalized = merge( + ( + output_stem = "lcm", + work_root = options.remote.local_root, + verbosity = (default = 0,), + output_basis = :pul, + on_result = nothing, + resume_run_directory = nothing, + solver_identity = nothing, + timing = false + ), + options + ) + normalized.output_stem isa AbstractString || throw(ArgumentError( + "PSCAD output_stem must be a string", + )) + output_stem = String(normalized.output_stem) + occursin(r"^[A-Za-z0-9][A-Za-z0-9_]{0,19}$", output_stem) || + throw(ArgumentError( + "PSCAD output_stem must contain 1–20 ASCII letters, digits, or underscores", + )) + levels = verbosity(normalized.verbosity) + normalized.timing isa Bool || throw(ArgumentError("timing must be Bool")) + basis_value = normalized.output_basis + basis_value in (:pul, :total) || throw(ArgumentError( + "output_basis must be :pul or :total; got $(repr(basis_value))", + )) + resume = normalized.resume_run_directory + (resume === nothing || resume === :latest || resume isa AbstractString) || + throw(ArgumentError("PSCAD resume_run_directory must be nothing, :latest, or a completed run path")) + resume isa AbstractString && isempty(resume) && + throw(ArgumentError( + "PSCAD resume_run_directory cannot be empty")) + identity = normalized.solver_identity + (identity === nothing || identity isa AbstractDict{String, String}) || + throw(ArgumentError("PSCAD solver_identity must be the record returned by identify(remote)")) + identity = identity === nothing ? nothing : + Dict{String, String}(validate(identity, options.remote)) + options.remote.transport === :local && !Sys.iswindows() && throw(ArgumentError( + "PSCAD local transport requires a Windows caller")) + work_root = abspath(normalized.work_root) + first(splitpath(relpath(work_root, options.remote.local_root))) == ".." && + throw(ArgumentError("PSCAD work_root must be inside remote.local_root")) + return ComputationOptions(; + work_root, + output_stem, + remote = options.remote, + verbosity = levels, + timing = normalized.timing, + output_basis = Val(basis_value), + on_result = normalized.on_result, + resume_run_directory = resume isa AbstractString ? abspath(resume) : resume, + solver_identity = identity + ) +end + +function _pscad_size(problem::LineParametersProblem) + assignments = problem.system.connection_order + isempty(assignments) && throw(ArgumentError( + "PSCAD computation requires at least one explicit terminal", + )) + any(iszero, assignments) && throw(ArgumentError( + "PSCAD computation does not permit conductor elimination", + )) + all(>(0), assignments) || throw(ArgumentError( + "PSCAD computation phase assignments must identify active phases", + )) + length(unique(assignments)) == length(assignments) || throw(ArgumentError( + "PSCAD computation does not permit bundled terminals", + )) + sort(assignments) == collect(1:length(assignments)) || throw(ArgumentError( + "PSCAD computation active-phase assignments must be contiguous from 1", + )) + return (length(assignments), length(assignments), length(problem.frequencies)) +end + +function _pscad_basis(parameters, ::LineParametersProblem, ::Val{:pul}) + parameters +end + +function _pscad_basis( + parameters::LineParameters, + problem::LineParametersProblem, + ::Val{:total} +) + return LineParameters( + PhaseDomain, + Z(parameters) .* problem.system.line_length, + Y(parameters) .* problem.system.line_length, + frequencies(parameters); + basis = :total, details = details(parameters) + ) +end + +function _pscad_inputs(problem::LineParametersProblem, formulation::PSCADFormulation, blueprints) + _pscad_deterministic(eltype(problem), typeof(formulation.options.data.base_frequency)) + setting = pscad_setting(formulation, problem, blueprints) + frequency = formulation.options.data.base_frequency + components = [_pscad_components(blueprint, frequency, formulation, problem.temperature) + for blueprint in blueprints] + native_order = [(cable = cable, terminal = component.name) + for (cable, values) in enumerate(components) for component in values] + allunique(native_order) && Set(native_order) == Set(problem.system.terminal_order) || + throw(ArgumentError("PSCAD equivalent components must represent every terminal exactly once")) + requested_order = problem.system.terminal_order[sortperm(problem.system.connection_order)] + permutation = [only(findall(==(terminal), native_order)) for terminal in requested_order] + coordinates = ["cable:$(terminal.cable):$(terminal.terminal)" for terminal in requested_order] + document = _pscad_project(problem.system, problem.earth_props, frequency, components; + native_settings = setting[(:ground, :frequency)]) + project = sprint(print, document) + dielectric_losses = map(enumerate(components)) do (cable, values) + map(enumerate(values)) do (layer, component) + dielectric = component.dielectric + requested = iszero(dielectric.shunt_capacitance) ? 0.0 : + dielectric.shunt_conductance / (2pi * frequency * dielectric.shunt_capacitance) + exported = parse(Float64, _pscad_value(requested; maximum = 10)) + (; cable, layer, requested, exported, capped = requested > 10) + end + end + return (; setting, project, native_order, permutation, coordinates, dielectric_losses) +end + +function _stage_pscad_project(inputs, work_root::AbstractString) + root = mktempdir(mkpath(abspath(work_root)); prefix = "run-", cleanup = false) + @debug "Exporting PSCAD computation project" + staged = joinpath(root, "generated.pscx") + write(staged, inputs.project) + return (; root, staged) +end + +const PSCAD_COMPLETED_OUTPUTS = ("result_zm.out", "result_zp.out", "result_ym.out", + "result_yp.out", "solver.toml", "native-settings.toml", "timing.txt") + +_pscad_digest(record::AbstractDict) = + bytes2hex(sha256(sprint(io -> TOML.print(io, record; sorted = true)))) + +function _compute_pscad(problem::LineParametersProblem, formulation::PSCADFormulation, + execution_options, inputs) + config = execution_options.data.remote + setting = inputs.setting + input = Dict{String, Any}( + "schema_version" => 4, + "project_sha256" => bytes2hex(sha256(inputs.project)), + "frequencies" => Float64.(problem.frequencies), + "matrix_size" => collect(_pscad_size(problem)), + "native_settings" => Dict(string(component) => Dict(string(field) => Dict( + "value" => control.value, "readback" => control.readback) + for (field, control) in pairs(getproperty(setting, component))) + for component in (:ground, :frequency, :configuration)), + "toolkit" => Dict(name => bytes2hex(sha256(source)) for (name, source) in PSCAD_REMOTE_SOURCES)) + # Expected identity constrains execution, not numerical compatibility. The + # completion record independently authenticates the complete request. + signature = _pscad_digest(input) + expected = execution_options.data.solver_identity + expected === nothing || (input["expected_solver"] = expected) + resume = execution_options.data.resume_run_directory + parent = execution_options.data.work_root + candidates = resume === nothing ? String[] : resume === :latest ? + (isdir(parent) ? sort!(filter(path -> isdir(path) && isfile(joinpath(path, "complete.toml")), + readdir(parent; join = true)); by = path -> mtime(joinpath(path, "complete.toml")), rev = true) : String[]) : [resume] + source_root = nothing + station_identity = nothing + actual = nothing + for candidate in candidates + record_path = joinpath(candidate, "complete.toml") + isfile(record_path) || throw(ArgumentError("PSCAD run has no completion record: $candidate")) + record = TOML.parsefile(record_path) + if get(record, "schema_version", nothing) != 4 + resume === :latest && continue + throw(ArgumentError("PSCAD completed run requires a fresh version-4 computation: $candidate")) + end + if get(record, "input_sha256", nothing) != signature + resume === :latest && continue + throw(ArgumentError("PSCAD completed run has different numerical inputs or solver implementation: $candidate")) + end + stored = TOML.parsefile(joinpath(candidate, "computation.toml")) + _pscad_digest(stored) == get(record, "request_sha256", nothing) || throw(ArgumentError( + "PSCAD completed-run input integrity check failed: $candidate")) + numerical = copy(stored) + pop!(numerical, "expected_solver", nothing) + _pscad_digest(numerical) == signature || throw(ArgumentError( + "PSCAD completed-run numerical input integrity check failed: $candidate")) + bytes2hex(open(sha256, joinpath(candidate, "generated.pscx"))) == stored["project_sha256"] || + throw(ArgumentError("PSCAD completed-run exported project changed: $candidate")) + for (name, digest) in stored["toolkit"] + bytes2hex(open(sha256, joinpath(candidate, "toolkit", name))) == digest || + throw(ArgumentError("PSCAD completed-run solver source changed: $candidate/toolkit/$name")) + end + for name in PSCAD_COMPLETED_OUTPUTS + path = joinpath(candidate, "outputs", name) + isfile(path) && bytes2hex(open(sha256, path)) == get(record["outputs"], name, nothing) || + throw(ArgumentError("PSCAD completed-run output integrity check failed: $path")) + end + stored_solver_identity = Dict{String, String}(validate( + TOML.parsefile(joinpath(candidate, "outputs", "solver.toml")), config)) + stored_solver_identity == get(record, "observed_solver", nothing) || throw(ArgumentError( + "PSCAD completed run has no matching solver attestation: $candidate")) + station_identity === nothing && (station_identity = identify(config)) + expected === nothing || expected == station_identity || throw(ArgumentError( + "PSCAD station does not match the expected solver identity")) + if stored_solver_identity != station_identity + resume === :latest && continue + throw(ArgumentError("PSCAD completed run belongs to a different solver installation: $candidate")) + end + source_root, actual = candidate, stored_solver_identity + break + end + reused = source_root !== nothing + if reused + @debug "PSCAD reuses a verified completed run" source_run=source_root + output = joinpath(source_root, "outputs") + execution = (exit_code = 0, + stdout_path = joinpath(output, "stdout.txt"), stderr_path = joinpath(output, "stderr.txt"), + console_path = joinpath(output, "pscad-console.txt"), output_dir = output) + else + root, staged = _stage_pscad_project(inputs, parent) + source_root = root + open(joinpath(root, "computation.toml"), "w") do io + TOML.print(io, input; sorted = true) + end + @debug "Computing PSCAD line parameters" system=problem.system.system_id + execution = run_remote_pscad(config, staged, joinpath(root, "outputs"), + formulation, problem.frequencies; output_stem = execution_options.data.output_stem, + verbosity = verbosity(execution_options, :PSCAD)) + actual = Dict{String, String}(validate( + TOML.parsefile(joinpath(execution.output_dir, "solver.toml")), config)) + expected === nothing || actual == expected || throw(ArgumentError( + "PSCAD result does not match the expected solver identity: $root")) + end + native_readback = TOML.parsefile(joinpath(execution.output_dir, "native-settings.toml")) + expected_readback = Dict(component => Dict(field => control["readback"] + for (field, control) in controls) for (component, controls) in input["native_settings"]) + native_readback == expected_readback || throw(ArgumentError( + "PSCAD result has no matching native-settings readback: $(execution.output_dir)")) + parameters = try + read_pscad_result(execution.output_dir, problem.frequencies, _pscad_size(problem)) + catch error + verbosity(execution_options, :progress) > 0 && @info "PSCAD result validation failed" _group=:progress exception=error + throw(ErrorException("PSCAD result validation failed: $(sprint(showerror, error))" * + "\nLast PSCAD diagnostics:\n$(_diagnostic_tail(execution.console_path))" * + "\nFull PSCAD diagnostics: $(execution.console_path)")) + end + source_elapsed = parse(Float64, strip(read(joinpath(execution.output_dir, "timing.txt"), String))) + isfinite(source_elapsed) && source_elapsed >= 0 || throw(ArgumentError("invalid PSCAD execution timing")) + if !reused + completion = Dict("schema_version" => 4, "input_sha256" => signature, + "request_sha256" => _pscad_digest(input), "observed_solver" => actual, + "outputs" => Dict(name => bytes2hex(open(sha256, joinpath(execution.output_dir, name))) + for name in PSCAD_COMPLETED_OUTPUTS)) + temporary = tempname(source_root) + try + open(temporary, "w") do io + TOML.print(io, completion; sorted = true) + end + mv(temporary, joinpath(source_root, "complete.toml"); force = false) + finally + isfile(temporary) && rm(temporary) + end + end + files = [(path = relpath(joinpath(directory, name), source_root), + source = joinpath(directory, name), sha256 = bytes2hex(open(sha256, joinpath(directory, name)))) + for (directory, _, names) in walkdir(source_root) for name in sort(names)] + # Reused and fresh native records have the same execution-fact schema. + # The public remote call's duration is transient. The scan projects it once. + execution = (; (key => value for (key, value) in pairs(execution) + if key ∉ (:elapsed_seconds, :elapsed_scope))...) + execution = merge(execution, (backend = :pscad, pscad_version = config.pscad_version, + reused, source_run = source_root, input_sha256 = signature, solver_identity = actual)) + return (; parameters, native_readback, files, execution, compile_call_seconds=source_elapsed) +end + +function _pscad_result(problem, formulation, options, inputs, native; batch_reuse = false) + permutation = inputs.permutation + parameters = LineParameters(PhaseDomain, + Z(native.parameters)[permutation, permutation, :], Y(native.parameters)[permutation, permutation, :], + copy(frequencies(native.parameters)); details = details(native.parameters)) + parameters = _pscad_basis(parameters, problem, options.data.output_basis) + all(isfinite, Z(parameters)) || throw(DomainError(Z(parameters), + "completed PSCAD series impedance must contain only finite entries")) + all(isfinite, Y(parameters)) || throw(DomainError(Y(parameters), + "completed PSCAD shunt admittance must contain only finite entries")) + execution = native.execution + batch_reuse && (execution=merge(execution, (reused=true,))) + retained = (files = deepcopy(native.files), coordinates = copy(inputs.coordinates), + native_terminal_order = copy(inputs.native_order), requested_frequencies = copy(problem.frequencies), + formulations = computation_details(formulation).data, native_setting = inputs.setting, + base_frequency = formulation.options.data.base_frequency, loss_tangent_limit = 10.0, + aerial_shunt_conductance = 1e-38, native_readback = deepcopy(native.native_readback), + native_frequencies = copy(details(native.parameters).data.native_frequencies), + dielectric_losses = deepcopy(inputs.dielectric_losses), exported_project = inputs.project, + execution = deepcopy(execution)) + if options.data.timing + timing = execution.reused ? (;) : (compile_call_seconds=native.compile_call_seconds,) + retained = merge(retained, (; timing)) + end + return LineParameters(parameters.domain, parameters.Z, parameters.Y, parameters.f, Engine.completion_details(retained)) +end + +function compute(problem::LineParametersProblem, formulation::PSCADFormulation; + options::Union{NamedTuple, ComputationOptions} = ComputationOptions(), + modal=nothing, modal_options::Union{NamedTuple, ComputationOptions}=ComputationOptions()) + modal===nothing || return compute(problem,formulation, + LineCableModels.ModalAnalysisFormulation(modal);options,modal_options) + isempty(modal_options isa NamedTuple ? modal_options : modal_options.data) || + throw(ArgumentError("modal_options require a modal formulation")) + return first(compute(problem, [formulation]; options)) +end + +function compute(problem::LineParametersProblem, formulations::AbstractVector{<:PSCADFormulation}; + options::Union{NamedTuple, ComputationOptions} = ComputationOptions(), + modal=nothing, modal_options::Union{NamedTuple, ComputationOptions}=ComputationOptions()) + isempty(formulations) && throw(ArgumentError("PSCAD formulation collections cannot be empty")) + options = options isa NamedTuple ? ComputationOptions(options) : options + modal===nothing || return compute(problem,formulations, + LineCableModels.ModalAnalysisFormulation(modal);options,modal_options) + isempty(modal_options isa NamedTuple ? modal_options : modal_options.data) || + throw(ArgumentError("modal_options require a modal formulation")) + execution = computation_options(PSCADFormulation, options) + _pscad_deterministic(eltype(problem)) + validate(problem.frequencies, PSCADFormulation) + _pscad_size(problem) + blueprints = _pscad_blueprints(problem.system) + inputs = [_pscad_inputs(problem, formulation, blueprints) for formulation in formulations] + logger = LineCableModels.VerbosityLogger(Logging.current_logger(), execution.data.verbosity) + return Logging.with_logger(logger) do + _compute_pscad(problem, formulations, execution, inputs) + end +end + +function _compute_pscad(problem::LineParametersProblem, + formulations::AbstractVector{<:PSCADFormulation}, execution::ComputationOptions, inputs) + progress = verbosity(execution, :progress) > 0 + started = progress ? time_ns() : UInt64(0) + last_log = started + previous = started + average_seconds = 0.0 + progress && @info "PSCAD computation started" _group=:progress total=length(formulations) + physical_inputs = Engine.completed_inputs(problem) + source_id = gridpoint_id().source_id + keys = [(project = value.project, setting = value.setting[(:ground, :frequency, :configuration)]) + for value in inputs] + # Include native execution, readback, and final result construction, but + # exclude batch input construction, measurement attachment, and callbacks. + scan_started = execution.data.timing ? time_ns() : UInt64(0) + native = _compute_pscad(problem, first(formulations), execution, first(inputs)) + execution = ComputationOptions(merge(execution.data, (solver_identity = native.execution.solver_identity,))) + first_result = Engine.retain_gridpoint(_pscad_result(problem, first(formulations), execution, + first(inputs), native), gridpoint_id(; source_id); + fields = merge(Engine.completed_formulation(first(formulations)), (inputs = physical_inputs,))) + if execution.data.timing && !isempty(first_result.details.data.timing) + wall_seconds = (time_ns() - scan_started) * 1e-9 + first_result = Engine.retain_gridpoint(first_result, first_result.details.data.gridpoint; + fields=(timing=merge((; wall_seconds), first_result.details.data.timing),)) + end + values = Vector{typeof(first_result)}(undef, length(formulations)) + values[1] = first_result + execution.data.on_result === nothing || execution.data.on_result(problem, 1, first_result) + if progress + now = time_ns() + average_seconds = (now - previous) * 1e-9 + previous = now + if now - last_log >= 5_000_000_000 + @info "PSCAD progress" _group=:progress completed=1 total=length(formulations) elapsed_seconds=(now-started)*1e-9 eta_hours=(length(formulations)-1)*average_seconds/3600 + last_log = now + end + end + completed = Dict(first(keys) => native) + for index in 2:length(formulations) + shared = haskey(completed, keys[index]) + scan_started = execution.data.timing ? time_ns() : UInt64(0) + native = if shared + @debug "PSCAD reuses identical exported inputs" formulation=index + completed[keys[index]] + else + _compute_pscad(problem, formulations[index], execution, inputs[index]) + end + value = _pscad_result(problem, formulations[index], execution, inputs[index], native; batch_reuse = shared) + value = Engine.retain_gridpoint(value, + gridpoint_id(; source_id, formulation_index = index); + fields = merge(Engine.completed_formulation(formulations[index]), (inputs = physical_inputs,))) + if execution.data.timing && !isempty(value.details.data.timing) + wall_seconds = (time_ns() - scan_started) * 1e-9 + value = Engine.retain_gridpoint(value, value.details.data.gridpoint; + fields=(timing=merge((; wall_seconds), value.details.data.timing),)) + end + typeof(value) === eltype(values) || throw(ArgumentError("PSCAD formulations produced inconsistent result types")) + values[index] = value + completed[keys[index]] = native + execution.data.on_result === nothing || execution.data.on_result(problem, index, values[index]) + if progress + now = time_ns() + interval = (now - previous) * 1e-9 + average_seconds = 0.2 * interval + 0.8 * average_seconds + previous = now + if now - last_log >= 5_000_000_000 + @info "PSCAD progress" _group=:progress completed=index total=length(formulations) elapsed_seconds=(now-started)*1e-9 eta_hours=(length(formulations)-index)*average_seconds/3600 + last_log = now + end + end + end + progress && @info "PSCAD computation completed successfully" _group=:progress completed=length(values) total=length(formulations) elapsed_seconds=(time_ns()-started)*1e-9 + return values +end diff --git a/src/pscad/formulations.jl b/src/pscad/formulations.jl new file mode 100644 index 000000000..55eedc3e0 --- /dev/null +++ b/src/pscad/formulations.jl @@ -0,0 +1,350 @@ +import LineCableModels: formula_id + +""" + PSCADFormulation + +Store shared formula selections, requested definitions, and physical options +for PSCAD. Backend execution settings remain computation options. +""" +struct PSCADFormulation{M <: NamedTuple, O <: FormulationOptions, D <: NamedTuple} <: + AbstractFormulation + methods::M + options::O + definitions::D +end + +"""Identify PSCAD without server access or native setting compilation.""" +description(::Type{<:PSCADFormulation}; compact::Bool=false) = "PSCAD" +description(::PSCADFormulation; compact::Bool=false) = description(PSCADFormulation;compact) +formula_id(::Type{<:PSCADFormulation}) = :pscad +formula_id(::PSCADFormulation) = :pscad +formulation_options(value::PSCADFormulation) = value.options +function Base.pairs(::Type{PSCADFormulation}; quantity=nothing) + return pairs((; (name => owner + for (name, owner) in pairs(LineParametersFormulation; quantity) + if name !== :shunt_model)...)) +end +Base.pairs(value::PSCADFormulation;quantity=nothing) = + pairs(PSCADFormulation,(methods=value.methods,requested=value.definitions,options=value.options.data);quantity) +description(::Type{PSCADFormulation},slot::Val;compact::Bool=false) = + description(LineParametersFormulation,slot;compact) +description(::Type{PSCADFormulation},slot::Union{Val{:reduce_bundle},Val{:kron_reduction},Val{:ideal_transposition}}, + value::Bool;compact::Bool=false) = description(LineParametersFormulation,slot,value;compact) +description(::Type{PSCADFormulation},::Val{:base_frequency},value::Real;compact::Bool=false) = + "base frequency="*string(value)*" Hz" +Base.pairs(::Type{PSCADFormulation},retained::NamedTuple;quantity=nothing) = + pairs(LineParametersFormulation,retained;quantity,owner=PSCADFormulation) + +function formulation_options(::Type{PSCADFormulation}, record::FormulationOptions)::FormulationOptions + options = record.data + base_frequency = get(options, :base_frequency, 50.0) + _pscad_deterministic(typeof(base_frequency)) + isfinite(base_frequency) && base_frequency >= 0.1 || throw(DomainError( + base_frequency, "PSCAD base frequency must be finite and at least 0.1 Hz")) + physical = (; + (key => value for (key, value) in pairs(options) if key !== :base_frequency)...) + normalized = formulation_options(LineParametersFormulation, + FormulationOptions(merge((reduce_bundle = false, kron_reduction = false, ideal_transposition = false), physical))) + any((normalized.data.reduce_bundle, normalized.data.kron_reduction, + normalized.data.ideal_transposition)) && + throw(ArgumentError("PSCAD currently requires unreduced, untransposed terminal matrices")) + return FormulationOptions(merge(normalized.data, (; base_frequency))) +end + +# Resolve the backend's explicit defaults before the Engine constructs selections. +# Partial recipes retain their original branches and are validated by the owner. +_pscad_default(selected, default) = selected +_pscad_default(selected::Symbol, default) = _pscad_default(LineCableModels.formula(selected), default) +function _pscad_default(selected::LineCableModels.FormulaDefinition{:default, Order}, default) where {Order} + Order === :default && selected.equivalent_earth === nothing || throw(ArgumentError( + "PSCAD equations do not execute equivalent-earth reductions")) + isempty(selected.parameters) && isempty(selected.options.data) || throw(ArgumentError( + "PSCAD equations do not accept analytical parameters or numerical controls")) + return default +end +function _pscad_default(selected::NamedTuple, default) + return NamedTuple{keys(selected)}(map(keys(selected)) do name + _pscad_default(selected[name], default isa NamedTuple ? get(default, name, selected[name]) : default) + end) +end + +function validate(selected::Union{InternalImpedance.Formula, InsulationImpedance.Formula, + EarthImpedance.Formula, EarthAdmittance.Formula}, ::Type{<:PSCADFormulation}) + empty_controls = selected isa InternalImpedance.Formula ? + all(isempty, values(selected.options.data)) : isempty(selected.options.data) + isempty(selected.parameters) && empty_controls || throw(ArgumentError( + "PSCAD equations do not accept analytical parameters or numerical controls")) + if selected isa Union{EarthImpedance.Formula, EarthAdmittance.Formula} + selected.equivalent_earth === nothing || throw(ArgumentError( + "PSCAD equations do not execute equivalent-earth reductions")) + end + return selected +end + +function _pscad_formulation(internal_impedance, insulation_impedance, earth_impedance, + insulation_admittance, semicon_admittance, earth_admittance, earth_properties, + pipe_impedance, temperature_dependence, options::FormulationOptions) + selections = (; internal_impedance, insulation_impedance, earth_impedance, + insulation_admittance, semicon_admittance, earth_admittance, earth_properties, + pipe_impedance, temperature_dependence) + normalized = formulation_options(PSCADFormulation, options) + defaults = (internal_impedance = :wedepohl1973, insulation_impedance = :ametani1980, + earth_impedance = (air = :carson1926, earth = :pollaczek1926, mixed = :lucca1994), + earth_admittance = :ideal) + methods = (; (name => begin + requested = selections[name] + if requested === nothing && name in (:earth_properties, :temperature_dependence) + nothing + else + selected = owner(haskey(defaults, name) ? + _pscad_default(requested, defaults[name]) : requested) + if haskey(defaults, name) + for leaf in (selected isa NamedTuple ? values(selected) : (selected,)) + leaf === nothing || validate(leaf, PSCADFormulation) + end + end + selected + end + end for (name, owner) in pairs(PSCADFormulation))...) + definitions = map(selections, methods) do requested, selected + requested isa NamedTuple ? NamedTuple{keys(selected)}(requested) : requested + end + return PSCADFormulation(methods, normalized, definitions) +end + +""" + PSCADFormulation(; kwargs...) + +Select PSCAD through the shared formula grammar. `Formulation(:pscad; kwargs...)` calls +it. All formula slots and options accept Grid inputs with product or zip composition. Earth-impedance +`:default` resolves to `(air=:carson1926, earth=:pollaczek1926, mixed=:lucca1994)`. +Internal impedance selects `:wedepohl1973` for inner, outer, and transfer surfaces. +Magnetic insulation impedance selects `:ametani1980`. Earth potential coefficients +select `:ideal`: electrostatic images in air, zero for buried and mixed pairs. +Each scientific selection is registered by its Engine owner. Dielectric +`:default` routes to `:lossless`. An explicit `:lossy` selection is represented +by the equivalent capacitance and loss tangent at the export reference +frequency. PSCAD's native frequency law and loss-tangent cap of ten still apply. +External selections accept the same `(air, earth, mixed)` shorthand as LCM. +Each required native field is compiled from the indexed Engine selections. +PSCAD has no separate potential-model selector. Native direct integration can +produce nonzero aerial conductance under the `:ideal` selection. Raw native +matrices are preserved. Unsupported equations and analytical integration settings +fail before export. +""" +function PSCADFormulation(; + internal_impedance = formula(:default), + insulation_impedance = formula(:default), + earth_impedance = formula(:default), + insulation_admittance = formula(:default), + semicon_admittance = formula(:default), + earth_admittance = formula(:default), + earth_properties = formula(:default), + pipe_impedance = formula(:default), + temperature_dependence = formula(:default), + options = FormulationOptions(), combine::Symbol = :product) + selections = (internal_impedance, insulation_impedance, earth_impedance, + insulation_admittance, semicon_admittance, earth_admittance, earth_properties, + pipe_impedance, temperature_dependence) + return parameterize(PSCADFormulation, (inputs...) -> _pscad_formulation(inputs[1:end-1]..., + last(inputs) isa NamedTuple ? FormulationOptions(last(inputs)) : last(inputs)), + (selections..., options); combine) +end + +Formulation(::Val{:pscad}; kwargs...) = PSCADFormulation(; kwargs...) + +# The Engine defines each selected formula and its kind/s/t expression. These methods +# translate that expression to PSCAD settings without evaluating it in Julia. +function earth_impedance( + ::EarthImpedance.Formula{ID}, ::Val{Kind}, ::Val{S}, ::Val{T}, ::PSCADFormulation) where {ID, Kind, S, T} + throw(ArgumentError("PSCAD earth_impedance :$ID, kind :$Kind: formula not implemented for source in layer $S and target in layer $T")) +end +function earth_potential_coefficient( + ::EarthAdmittance.Formula{ID}, ::Val{Kind}, ::Val{S}, ::Val{T}, ::PSCADFormulation) where {ID, Kind, S, T} + throw(ArgumentError("PSCAD earth_potential_coefficient :$ID, kind :$Kind: formula not implemented for source in layer $S and target in layer $T")) +end +function internal_impedance(::InternalImpedance.Formula{ID}, ::Val{Kind}, ::PSCADFormulation) where {ID, Kind} + throw(ArgumentError("PSCAD internal_impedance :$ID: formula not implemented for kind :$Kind")) +end +function insulation_impedance(::InsulationImpedance.Formula{ID}, ::PSCADFormulation) where {ID} + throw(ArgumentError("PSCAD insulation_impedance :$ID: formula not implemented")) +end + +function earth_impedance(::EarthImpedance.Formula{:gary1976}, ::Union{Val{:self}, Val{:mutual}}, + ::Val{1}, ::Val{1}, ::PSCADFormulation) + (EarthForm2 = (value = 0, readback = "DERISEMLYEN"),) +end +function earth_impedance(::EarthImpedance.Formula{:carson1926}, ::Union{Val{:self}, Val{:mutual}}, + ::Val{1}, ::Val{1}, ::PSCADFormulation) + (EarthForm2 = (value = 2, readback = "DIRECT_NUMERICAL_INTEGRATION"),) +end +function earth_impedance(::EarthImpedance.Formula{:pollaczek1926}, ::Union{Val{:self}, Val{:mutual}}, + ::Val{2}, ::Val{2}, ::PSCADFormulation) + (EarthForm = (value = 2, readback = "DIRECT_NUMERICAL_INTEGRATION"),) +end +function earth_impedance(::EarthImpedance.Formula{:wedepohl1973}, ::Union{Val{:self}, Val{:mutual}}, + ::Val{2}, ::Val{2}, ::PSCADFormulation) + (EarthForm = (value = 0, readback = "WEDEPOHL"),) +end +function earth_impedance(::EarthImpedance.Formula{:saad1996}, ::Union{Val{:self}, Val{:mutual}}, + ::Val{2}, ::Val{2}, ::PSCADFormulation) + (EarthForm = (value = 3, readback = "SAAD"),) +end +function earth_impedance( + ::EarthImpedance.Formula{:ametani2009}, ::Val{:mutual}, ::Val{1}, ::Val{2}, ::PSCADFormulation) + (EarthForm3 = (value = 0, readback = "AMETANIL"),) +end +function earth_impedance( + ::EarthImpedance.Formula{:ametani2009}, ::Val{:mutual}, ::Val{2}, ::Val{1}, ::PSCADFormulation) + (EarthForm3 = (value = 0, readback = "AMETANIL"),) +end +function earth_impedance( + ::EarthImpedance.Formula{:lucca1994}, ::Val{:mutual}, ::Val{1}, ::Val{2}, ::PSCADFormulation) + (EarthForm3 = (value = 2, readback = "LUCCA"),) +end +function earth_impedance( + ::EarthImpedance.Formula{:lucca1994}, ::Val{:mutual}, ::Val{2}, ::Val{1}, ::PSCADFormulation) + (EarthForm3 = (value = 2, readback = "LUCCA"),) +end + +# The documented external potential law uses ideal images in air and zero +# coefficients for buried and mixed pairs. There is no independent native selector. +# Direct integration can deviate from strict ideal behavior in the aerial block. +function earth_potential_coefficient(::EarthAdmittance.Formula{:ideal}, ::Union{Val{:self}, Val{:mutual}}, + ::Val{1}, ::Val{1}, ::PSCADFormulation) + (;) +end +function earth_potential_coefficient(::EarthAdmittance.Formula{:ideal}, ::Union{Val{:self}, Val{:mutual}}, + ::Val{2}, ::Val{2}, ::PSCADFormulation) + (;) +end +function earth_potential_coefficient( + ::EarthAdmittance.Formula{:ideal}, ::Val{:mutual}, ::Val{1}, ::Val{2}, ::PSCADFormulation) + (;) +end +function earth_potential_coefficient( + ::EarthAdmittance.Formula{:ideal}, ::Val{:mutual}, ::Val{2}, ::Val{1}, ::PSCADFormulation) + (;) +end +function internal_impedance( + ::InternalImpedance.Formula{:wedepohl1973}, ::Union{ + Val{:inner}, Val{:outer}, Val{:transfer}}, ::PSCADFormulation) + (;) # PSCAD fixes the conductor surfaces to the Wedepohl-Wilcox approximation. +end +insulation_impedance(::InsulationImpedance.Formula{:ametani1980}, ::PSCADFormulation) = (;) + +""" +Compile every required native setting from the physical indexed interactions. +""" +function pscad_setting(formulation::PSCADFormulation, problem::LineParametersProblem) + _pscad_deterministic(eltype(problem), typeof(formulation.options.data.base_frequency)) + return pscad_setting(formulation, problem, _pscad_blueprints(problem.system)) +end + +function pscad_setting(formulation::PSCADFormulation, problem::LineParametersProblem, blueprints) + validate(problem) + model = problem.earth_props + validate(model, PSCADFormulation) + relation = formulation.methods.earth_properties + (relation === nothing || + relation === LineCableModels.Earth.FrequencyDependent.Formula(:default)) || + throw(ArgumentError("PSCAD does not implement the selected frequency-dependent soil relation")) + for name in (:insulation_admittance, :semicon_admittance) + selected = getproperty(formulation.methods, name) + selected isa Union{InsulationAdmittance.Formula{:lossless},InsulationAdmittance.Formula{:lossy}, + SemiconAdmittance.Formula{:lossless},SemiconAdmittance.Formula{:lossy}} || throw(ArgumentError( + "PSCAD does not implement $name :$(formula_id(selected))")) + end + for design in problem.system.designs + validate(design, formulation.methods.pipe_impedance, formulation) + end + input = Engine.lineinput(problem, blueprints) + pairs = Engine.earth_pairs(first.(input.cable.assemblies), input.horz, input.vert, + input.horz_sep, problem.earth_props) + settings = Dict{Symbol, NamedTuple}() + interactions = map(formulation.methods[(:earth_impedance, :earth_admittance)]) do selection + selection isa NamedTuple && validate(problem.earth_props) + records = NamedTuple{ + (:formula, :kind, :source, :target), Tuple{Symbol, Symbol, Int, Int}}[] + for pair in pairs + validate(pair) + expression = Expression(selection, pair) + selected = expression.selection + # Registration and physical validity belong to the equation owner. + # Execution availability is selected by the native expression. + validate(pair, expression) + formulation_options(selected, (expression,)) + record = (formula = formula_id(selected), + kind = pair.row == pair.column ? :self : :mutual, + source = pair.layers[1], target = pair.layers[2]) + record in records && continue + push!(records, record) + for (field, setting) in Base.pairs(expression(formulation)) + haskey(settings, field) && settings[field] != setting && + throw(ArgumentError( + "PSCAD field $field has conflicting formula selections")) + settings[field] = setting + end + end + records + end + kinds = any(indices -> length(indices) > 1, input.cable.assemblies) ? + (:inner, :outer, :transfer) : (:outer,) + for kind in kinds + Expression(formulation.methods.internal_impedance, + internal_impedance, Val(kind))(formulation) + end + Expression(formulation.methods.insulation_impedance, insulation_impedance)(formulation) + # Unused native slots are set deterministically and retained too. They do not + # authorize any additional physical case. + ground = ( + EarthForm2 = get(settings, :EarthForm2, ( + value = 2, readback = "DIRECT_NUMERICAL_INTEGRATION")), + EarthForm = get(settings, :EarthForm, ( + value = 2, readback = "DIRECT_NUMERICAL_INTEGRATION")), + EarthForm3 = get(settings, :EarthForm3, (value = 2, readback = "LUCCA"))) + return (ground = ground, + frequency = (enablf = (value = 1, readback = "YES"), + FS = (value = Float64(first(problem.frequencies)), + readback = Float64(first(problem.frequencies))), + FE = (value = Float64(last(problem.frequencies)), + readback = Float64(last(problem.frequencies))), + Numf = (value = length(problem.frequencies)-1, + readback = length(problem.frequencies)-1)), + configuration = (Freq = (value = Float64(formulation.options.data.base_frequency), + readback = Float64(formulation.options.data.base_frequency)),), interactions = interactions) +end + +"""Record consumed PSCAD identifiers and fixed native assumptions with bounded field types.""" +function computation_details(formulation::PSCADFormulation)::ComputationDetails + methods = formulation.methods + return ComputationDetails(merge(( + schema_version = 4, + assumptions = ( + internal_impedance = "Wedepohl-Wilcox (1973) inner, outer, and transfer conductor surface approximations", + insulation_impedance = "Ametani (1980) concentric-insulation magnetic impedance", + earth_admittance = "Ideal-earth images in air; zero external potential coefficients for buried and mixed pairs. Native direct integration can add aerial conductance; native matrices are preserved", + insulation_admittance = description(methods.insulation_admittance), + semicon_admittance = description(methods.semicon_admittance), + dielectric_equivalence = "Reference-frequency equivalent capacitance and loss tangent; native PSCAD frequency law; loss tangent capped at 10", + earth = "One homogeneous earth layer; no FrequencyDependent relation", + pipe_impedance = "Cable_Coax only; shared eccentric metallic enclosure unsupported"), + ),NamedTuple(formulation))) +end + +function computation_details(::Type{<:PSCADFormulation}, result::LineParameters)::ComputationDetails + return details(result) +end + +"""Expose complete requested PSCAD formula choices and native configuration options.""" +function Base.NamedTuple(value::PSCADFormulation) + record = function (selected) + selected === nothing && return nothing + selected isa Symbol && return NamedTuple(formula(selected)) + selected isa NamedTuple && return map(record,selected) + return NamedTuple(selected) + end + Record=NamedTuple{(:backend,:requested,:methods,:options), + Tuple{Symbol,NamedTuple,NamedTuple,NamedTuple}} + return Record((:pscad,map(record,value.definitions),map(record,value.methods),value.options.data)) +end diff --git a/src/pscad/importexport/import.jl b/src/pscad/importexport/import.jl new file mode 100644 index 000000000..6abba2669 --- /dev/null +++ b/src/pscad/importexport/import.jl @@ -0,0 +1,529 @@ + +const _PSCAD_PART_FIELDS = ( + ( + inner = "R1", outer = "R2", insulation = "R3", + rho = "RHOC", conductor_mu = "PERMC", + eps = "EPS1", insulation_mu = "PERM1", loss = "LT1" + ), + ( + inner = nothing, outer = "R4", insulation = "R5", + rho = "RHOS", conductor_mu = "PERMS", + eps = "EPS2", insulation_mu = "PERM2", loss = "LT2" + ), + ( + inner = nothing, outer = "R6", insulation = "R7", + rho = "RHOA", conductor_mu = "PERMA", + eps = "EPS3", insulation_mu = "PERM3", loss = "LT3" + ), + ( + inner = nothing, outer = "R8", insulation = "R9", + rho = "RHOO", conductor_mu = "PERMO", + eps = "EPS4", insulation_mu = "PERM4", loss = "LT4" + ) +) + +function _pscad_parameters(node) + values = Dict{String, String}() + for parameter in findall("./paramlist/param", node) + name = parameter["name"] + haskey(values, name) && throw(ArgumentError( + "duplicate PSCAD parameter '$name'", + )) + values[name] = parameter["value"] + end + return values +end + +function _pscad_parameter(values, name::AbstractString) + haskey(values, name) || throw(KeyError(name)) + return values[name] +end + +function _pscad_number(values, name::AbstractString) + raw = _pscad_parameter(values, name) + scalar = strip(first(split(raw, '['; limit = 2))) + scalar = replace(scalar, 'D' => 'E', 'd' => 'e') + value = tryparse(Float64, scalar) + value === nothing && throw(ArgumentError( + "PSCAD parameter '$name' is not numeric: '$raw'", + )) + return value +end + +function _pscad_integer(values, name::AbstractString) + value = _pscad_number(values, name) + isinteger(value) || throw(ArgumentError( + "PSCAD parameter '$name' must be an integer; got $value", + )) + return round(Int, value) +end + +function _pscad_binding(node) + return haskey(node, "defn") ? node["defn"] : "" +end + +function _pscad_output_enabled(node) + values = _pscad_parameters(node) + haskey(values, "Output") || return nothing + return uppercase(strip(values["Output"])) in ("1", "YES", "ENABLED", "TRUE") +end + +function _pscad_row_definition( + project, + requested::Union{Nothing, AbstractString} = nothing +) + row_definitions = NamedTuple[] + for definition in findall("./definitions/Definition", project) + users = findall("./schematic/User", definition) + frequency = filter( + node -> _pscad_binding(node) == _PSCAD_FREQUENCY_BINDING, + users + ) + ground = filter( + node -> _pscad_binding(node) == _PSCAD_GROUND_BINDING, + users + ) + isempty(frequency) && continue + isempty(ground) && continue + length(frequency) == 1 || throw(ArgumentError( + "PSCAD definition '$(definition["name"])' has multiple frequency options", + )) + length(ground) == 1 || throw(ArgumentError( + "PSCAD definition '$(definition["name"])' has multiple ground definitions", + )) + push!(row_definitions, (; + definition, + users, + frequency = only(frequency), + ground = only(ground) + )) + end + isempty(row_definitions) && throw(ArgumentError( + "PSCAD project contains no supported frequency-dependent row definition", + )) + if requested !== nothing + qualified = String(requested) + name = last(split(qualified, ':'; limit = 2)) + selected = filter(row_definition -> row_definition.definition["name"] == name, row_definitions) + length(selected) == 1 || throw(ArgumentError( + isempty(selected) ? + "PSCAD project contains no supported row definition '$qualified'" : + "PSCAD project contains multiple supported row definitions named '$name'", + )) + return only(selected) + end + enabled = filter(row_definition -> _pscad_output_enabled(row_definition.frequency) === true, + row_definitions) + if length(enabled) == 1 + return only(enabled) + end + isempty(enabled) && length(row_definitions) == 1 && return only(row_definitions) + throw(ArgumentError( + isempty(enabled) ? + "PSCAD project has multiple row definitions and none is selected for output" : + "PSCAD project has multiple row definitions selected for output", + )) +end + +function _pscad_active_cables(row) + unsupported = String[] + cables = typeof(row.definition)[] + for node in row.users + binding = _pscad_binding(node) + if binding in (_PSCAD_CABLE_BINDING, "master:Cable_CoaxSimpl") + values = _pscad_parameters(node) + _pscad_integer(values, "LL") >= 0 && push!(cables, node) + elseif startswith(binding, "master:Line_Tower_") || + binding == "master:Cable_PipeType" + push!(unsupported, binding) + end + end + isempty(unsupported) || throw(ArgumentError( + "PSCAD row definition uses unsupported physical components: " * + join(unique(unsupported), ", "), + )) + isempty(cables) && throw(ArgumentError( + "PSCAD row definition contains no active master:Cable_Coax component", + )) + sort!(cables; by = node -> _pscad_integer(_pscad_parameters(node), "CABNUM")) + return cables +end + +function _pscad_instance(document, project, row) + namespace = project["name"] + target = "$namespace:$(row.definition["name"])" + matching_nodes = filter(findall("//User", document)) do node + _pscad_binding(node) == target || return false + values = _pscad_parameters(node) + return haskey(values, "Length") && haskey(values, "Name") + end + length(matching_nodes) == 1 || throw(ArgumentError( + "PSCAD project must contain one instance of '$target' with line parameters", + )) + return only(matching_nodes) +end + +function _pscad_material(kind::Symbol, rho, eps_r, mu_r) + return Material(kind, rho, eps_r, mu_r, 20.0, 0.0) +end + +function _pscad_dielectric(values, fields, frequency; eps_r = _pscad_number(values, fields.eps)) + mu_r = _pscad_number(values, fields.insulation_mu) + loss = _pscad_number(values, fields.loss) + 0 <= loss <= 10 || throw(DomainError( + loss, "PSCAD loss tangent must be between zero and ten" + )) + conductivity = 2π * frequency * vacuum_permittivity(typeof(eps_r)) * eps_r * loss + rho = iszero(conductivity) ? Inf : inv(conductivity) + return _pscad_material(:insulator, rho, eps_r, mu_r) +end + +function _pscad_radial_design( + cable_name::AbstractString, + components +) + parts = AbstractCablePart[] + for component in components + terminal = Symbol(component.name) + push!(parts, + Group( + terminal, + Region( + Symbol(terminal, :_conductor), + Annulus(component.conductor_inner, component.conductor_outer), + component.conductor_material + ) + )) + if component.insulation_outer > component.conductor_outer + push!(parts, + Region( + Symbol(terminal, :_insulation), + Annulus(component.conductor_outer, component.insulation_outer), + component.dielectric_material + )) + end + end + return build(CableDesign, cable_name, Stack(parts)) +end + +function _pscad_design(values, cable_number::Int, frequency) + line_layers = _pscad_integer(values, "LL") + if iszero(line_layers) + conductor_inner = _pscad_number(values, "R1") + conductor_outer = _pscad_number(values, "R2") + cable_name = strip(get(values, "Name", "")) + isempty(cable_name) && (cable_name = "cable$cable_number") + component = (; + name = "conductor", + conductor_inner, + conductor_outer, + insulation_outer = conductor_outer, + conductor_material = _pscad_material( + :conductor, + _pscad_number(values, "RHOC"), + 0.0, + _pscad_number(values, "PERMC") + ), + dielectric_material = _pscad_material(:insulator, Inf, 1.0, 1.0) + ) + return _pscad_radial_design(cable_name, (component,)) + end + isodd(line_layers) || throw(ArgumentError( + "PSCAD LL must describe alternating conductor and insulation layers", + )) + component_count = (line_layers + 1) ÷ 2 + component_count in eachindex(_PSCAD_PART_FIELDS) || throw(ArgumentError( + "PSCAD Cable_Coax supports one to four concentric components", + )) + + components = NamedTuple[] + previous_outer = 0.0 + for index in 1:component_count + fields = _PSCAD_PART_FIELDS[index] + conductor_inner = fields.inner === nothing ? previous_outer : + _pscad_number(values, fields.inner) + conductor_outer = _pscad_number(values, fields.outer) + insulation_outer = _pscad_number(values, fields.insulation) + name = strip(_pscad_parameter(values, "CONNAM$index")) + isempty(name) && (name = "component$index") + lowercase(name) == "none" && throw(ArgumentError( + "PSCAD active component $index cannot be named 'none'", + )) + push!(components, + (; + name = lowercasefirst(name), + conductor_inner, + conductor_outer, + insulation_outer, + conductor_material = _pscad_material( + :conductor, + _pscad_number(values, fields.rho), + 0.0, + _pscad_number(values, fields.conductor_mu) + ), + dielectric_material = _pscad_dielectric(values, fields, frequency) + )) + previous_outer = insulation_outer + end + cable_name = strip(get(values, "Name", "")) + isempty(cable_name) && (cable_name = "cable$cable_number") + return _pscad_radial_design(cable_name, components) +end + +const _PSCAD_SIMPLIFIED_FIELDS = ( + ( + inner = "R1", outer = "R2", insulation = "R3", + rho = "RHOC", resistance = "DCRC", material_mode = "DTC", + mu = "PERMC", eps = "EPS1", capacitance = "CI1", + dielectric_mode = "DTI1", insulation_mu = "mu_r1", loss = "LT1", + name = "core" + ), + ( + inner = "R3", outer = "R4", insulation = "R5", + rho = "RHOS", resistance = "DCRS", material_mode = "DTS", + mu = "PERMS", eps = "EPS2", capacitance = "CI2", + dielectric_mode = "DTI2", insulation_mu = "mu_r2", loss = "LT2", + name = "sheath" + ), + ( + inner = "R5", outer = "R6", insulation = "R7", + rho = "RHOA", resistance = "DCRA", material_mode = "DTA", + mu = "PERMA", eps = "EPS3", capacitance = "CI3", + dielectric_mode = "DTI3", insulation_mu = "mu_r3", loss = "LT3", + name = "armor" + ) +) + +function _pscad_simplified_rho(values, fields, r_in, r_ex) + mode = _pscad_integer(values, fields.material_mode) + mode in (0, 1) || throw(ArgumentError( + "PSCAD simplified-cable material selector must be zero or one", + )) + mode == 1 && return _pscad_number(values, fields.rho) + resistance = _pscad_number(values, fields.resistance) / 1000 + return resistance * π * (r_ex^2 - r_in^2) +end + +function _pscad_simplified_eps(values, fields, r_in, r_ex) + mode = _pscad_integer(values, fields.dielectric_mode) + mode in (0, 1) || throw(ArgumentError( + "PSCAD simplified-cable dielectric selector must be zero or one", + )) + mode == 1 && return _pscad_number(values, fields.eps) + capacitance = _pscad_number(values, fields.capacitance) * 1e-9 + return capacitance * log(r_ex / r_in) / (2π * vacuum_permittivity(typeof(capacitance))) +end + +function _pscad_simplified_design(values, cable_number::Int, frequency) + _pscad_integer(values, "RorT") == 0 || throw(ArgumentError( + "PSCAD simplified-cable import currently requires radius input", + )) + _pscad_integer(values, "SemiCL") == 0 || throw(ArgumentError( + "PSCAD simplified-cable import does not support semiconductive layers", + )) + line_layers = _pscad_integer(values, "LL") + isodd(line_layers) || throw(ArgumentError( + "PSCAD LL must describe alternating conductor and insulation layers", + )) + component_count = (line_layers + 1) ÷ 2 + component_count in eachindex(_PSCAD_SIMPLIFIED_FIELDS) || throw(ArgumentError( + "PSCAD Cable_CoaxSimpl supports one to three concentric components", + )) + components = NamedTuple[] + for index in 1:component_count + fields = _PSCAD_SIMPLIFIED_FIELDS[index] + conductor_inner = _pscad_number(values, fields.inner) + conductor_outer = _pscad_number(values, fields.outer) + insulation_outer = _pscad_number(values, fields.insulation) + eps_r = _pscad_simplified_eps( + values, fields, conductor_outer, insulation_outer + ) + push!(components, + (; + name = fields.name, + conductor_inner, + conductor_outer, + insulation_outer, + conductor_material = _pscad_material( + :conductor, + _pscad_simplified_rho( + values, fields, conductor_inner, conductor_outer + ), + 0.0, + _pscad_number(values, fields.mu) + ), + dielectric_material = _pscad_dielectric(values, fields, frequency; eps_r) + )) + end + cable_name = strip(get(values, "Name", "")) + isempty(cable_name) && (cable_name = "cable$cable_number") + return _pscad_radial_design(cable_name, components) +end + +function _pscad_position(values, cable_number::Int, next_phase::Ref{Int}) + frequency = _pscad_number(values, "FLT") + isfinite(frequency) && frequency > 0 || throw(DomainError( + frequency, "PSCAD cable reference frequency must be positive and finite" + )) + design = _pscad_design(values, cable_number, frequency) + connections = Dict{Symbol, Int}() + for (index, terminal) in enumerate(design.terminal_order) + eliminated = index > 1 && _pscad_integer(values, "elim$(index - 1)") != 0 + connections[terminal] = eliminated ? 0 : next_phase[] + eliminated || (next_phase[] += 1) + end + + horizontal = _pscad_number(values, "X") + overhead = _pscad_integer(values, "OHC") != 0 + vertical = overhead ? _pscad_number(values, "Y2") : + -abs(_pscad_number(values, "Y")) + return (; + design, + position = Pose2(horizontal, vertical, 0), + connections + ) +end + +function _pscad_simplified_positions(values, next_phase::Ref{Int}) + circuit_count = _pscad_integer(values, "NC") + circuit_count > 0 || throw(DomainError( + circuit_count, "PSCAD simplified-cable circuit count must be positive" + )) + first_number = _pscad_integer(values, "CABNUM") + spacing = _pscad_number(values, "D") + horizontal = _pscad_number(values, "X") + vertical = -abs(_pscad_number(values, "Y")) + frequency = _pscad_number(values, "FLT") + isfinite(frequency) && frequency > 0 || throw(DomainError( + frequency, "PSCAD cable reference frequency must be positive and finite" + )) + positions = NamedTuple[] + for offset in 0:(3circuit_count - 1) + cable_number = first_number + offset + design = _pscad_simplified_design(values, cable_number, frequency) + connections = Dict( + terminal => (next_phase[] += 1; next_phase[] - 1) + for terminal in design.terminal_order + ) + push!(positions, + (; + design, + position = Pose2(horizontal + offset * spacing, vertical, 0), + connections + )) + end + return positions +end + +function _pscad_earth(values) + return EarthModel( + _pscad_number(values, "GRRES"), + _pscad_number(values, "GRP"), + _pscad_number(values, "GPERM") + ) +end + +""" +$(TYPEDSIGNATURES) + +Import a self-contained PSCAD project that follows the supported +frequency-dependent coaxial-cable schema. + +# Arguments + +- `file_name`: existing PSCAD project with a `.pscx` extension. + +# Keywords + +- `definition`: qualified or local name of the frequency-dependent row to import. Leave it unset when the project contains one eligible row or one row selected for output. + +# Returns + +- `earth`: materialized homogeneous [`EarthModel`](@ref). +- `system`: materialized [`LineCableSystem`](@ref). + +# Notes + +The PSCAD schema stores equivalent concentric layers but does not retain the +source cable's detailed strand geometry, material reference temperature, or +temperature coefficient. Imported layers use the emitted equivalent +properties at 20 °C with zero temperature coefficient. Non-eliminated PSCAD +conductors receive consecutive phase indices in cable and layer order. + +PSCAD bounds dielectric loss tangent at ten. A project produced from a more +conductive equivalent dielectric contains the bounded PSCAD value, +and import materializes that value rather than the pre-export resistivity. + +The importer accepts detailed and simplified coaxial cables. Simplified cables +must use radius input and may not contain semiconductive layers. Tower and +pipe-type definitions are rejected. + +# Errors + +Throws `ArgumentError`, `DomainError`, `DimensionMismatch`, or `KeyError` when +the file, schema, geometry, or physical parameters are invalid. +""" +function import_data( + ::Val{:pscad}, + file_name::AbstractString; + definition::Union{Nothing, AbstractString} = nothing +) + extension = lowercase(splitext(file_name)[2]) + extension == ".pscx" || throw(ArgumentError( + "PSCAD import requires a .pscx file", + )) + isfile(file_name) || throw(ArgumentError( + "PSCAD project does not exist: $(abspath(file_name))", + )) + document = readxml(file_name) + project = root(document) + nodename(project) == "project" || throw(ArgumentError( + "PSCAD input root must be ", + )) + haskey(project, "name") || throw(ArgumentError( + "PSCAD project has no name", + )) + + row = _pscad_row_definition(project, definition) + cables = _pscad_active_cables(row) + instance = _pscad_parameters(_pscad_instance(document, project, row)) + system_id = strip(_pscad_parameter(instance, "Name")) + isempty(system_id) && throw(ArgumentError("PSCAD line instance has no name")) + line_length = 1000 * _pscad_number(instance, "Length") + + next_phase = Ref(1) + numbered_positions = Pair{Int, NamedTuple}[] + for cable in cables + values = _pscad_parameters(cable) + first_number = _pscad_integer(values, "CABNUM") + if _pscad_binding(cable) == "master:Cable_CoaxSimpl" + append!(numbered_positions, + (first_number + offset - 1) => position + for (offset, position) in enumerate( + _pscad_simplified_positions(values, next_phase) + )) + else + push!(numbered_positions, first_number => _pscad_position( + values, + first_number, + next_phase + )) + end + end + sort!(numbered_positions; by = first) + first.(numbered_positions) == collect(eachindex(numbered_positions)) || + throw(ArgumentError( + "PSCAD cable numbers must be consecutive and start at one", + )) + declarations = last.(numbered_positions) + earth = _pscad_earth(_pscad_parameters(row.ground)) + system = build( + LineCableSystem, + CableDesign[item.design for item in declarations], + Pose2[item.position for item in declarations]; + system_id, + line_length, + connections = [item.connections for item in declarations] + ) + return earth, system +end diff --git a/src/pscad/importexport/project.jl b/src/pscad/importexport/project.jl new file mode 100644 index 000000000..ab0e9d5be --- /dev/null +++ b/src/pscad/importexport/project.jl @@ -0,0 +1,511 @@ +function _pscad_output_path(system::LineCableSystem, file_name) + if file_name === nothing + return joinpath(@__DIR__, "$(system.system_id)_export.pscx") + end + requested = isabspath(file_name) ? String(file_name) : abspath(file_name) + directory, name = splitdir(requested) + return joinpath(directory, "$(system.system_id)_$name") +end + +const _PSCAD_DEFINITION_DATE = "0" +const _PSCAD_IDS = ( + station = 100000001, + station_wire = 100000002, + main_instance = 100000003, + main = 100000004, + main_rectangle = 100000005, + main_text = 100000006, + cable_wire = 100000007, + cable_instance = 100000008, + cable_system = 100000009, + first_physical_component = 100000010 +) + +function _pscad_project_settings!(project) + settings = Pair{String, String}[ + "creator" => "LineCableModels.jl", + "time_duration" => "0.5", + "time_step" => "5", + "sample_step" => "250", + "chatter_threshold" => ".001", + "branch_threshold" => ".0005", + "StartType" => "0", + "startup_filename" => "\$(Namespace).snp", + "PlotType" => "0", + "output_filename" => "\$(Namespace).out", + "SnapType" => "0", + "SnapTime" => "0.3", + "snapshot_filename" => "\$(Namespace).snp", + "MrunType" => "0", + "Mruns" => "1", + "Scenario" => "", + "Advanced" => "14335", + "sparsity_threshold" => "200", + "Options" => "16", + "Build" => "18", + "Warn" => "0", + "Check" => "0", + "description" => "Generated by LineCableModels.jl", + "Debug" => "0" +] + return _pscad_paramlist!(project, settings; name = "Settings") +end + +function _pscad_project_scaffolding!(project) + addelement!(project, "Layers") + addelement!(project, "List")["classid"] = "Settings" + addelement!(project, "bookmarks") + substitutions = addelement!(project, "GlobalSubstitutions") + substitutions["name"] = "Default" + addelement!(substitutions, "List")["classid"] = "Sub" + addelement!(substitutions, "List")["classid"] = "ValueSet" + _pscad_paramlist!(substitutions, ["Current" => ""]) + return project +end + +function _pscad_station_definition!(definitions, namespace::AbstractString) + station = addelement!(definitions, "Definition") + _pscad_attributes!(station, + ( + id = _PSCAD_IDS.station, + classid = "StationDefn", + name = "DS", + group = "", + url = "", + version = "", + build = "", + crc = -1, + view = "false" + )) + _pscad_paramlist!(station, ["Description" => ""]; name = "") + schematic = addelement!(station, "schematic") + schematic["classid"] = "StationCanvas" + _pscad_canvas_params!(schematic) + addelement!(schematic, "grouping") + _pscad_wire!( + schematic, + _PSCAD_IDS.station_wire, + _PSCAD_IDS.main_instance, + "Branch", + "Main", + "Main", + "$namespace:Main", + Pair{String, String}[]; + x = 180, + y = 180, + width = 66, + height = 82 + ) + return station +end + +function _pscad_main_instance_parameters(system::LineCableSystem, base_frequency) + length_km = system.line_length / 1000 + return Pair{String, String}[ + "Name" => system.system_id, + "R" => "#NaN", + "X" => "#NaN", + "B" => "#NaN", + "Freq" => _pscad_value(base_frequency), + "Length" => _pscad_value(length_km), + "Dim" => "0", + "Mode" => "0", + "CoupleEnab" => "0", + "CoupleName" => "row", + "CoupleOffset" => "0.0 [m]", + "CoupleRef" => "0", + "tname" => "tandem_segment", + "sfault" => "0", + "linc" => "$(_pscad_value(length_km)) [km]", + "steps" => "3", + "gen_cnst" => "1", + "const_path" => "%TEMP%\\my_constants_file.tlo", + "Date" => _PSCAD_DEFINITION_DATE + ] +end + +function _pscad_main_definition!( + definitions, + system::LineCableSystem, + base_frequency +) + namespace = system.system_id + main = addelement!(definitions, "Definition") + _pscad_attributes!(main, + ( + id = _PSCAD_IDS.main, + classid = "UserCmpDefn", + name = "Main", + group = "", + url = "", + version = "", + build = "", + crc = -1, + view = "false", + date = _PSCAD_DEFINITION_DATE + )) + _pscad_paramlist!(main, ["Description" => ""]; name = "") + form = addelement!(main, "form") + _pscad_attributes!(form, (name = "", w = 320, h = 400, splitter = 60)) + + graphics = addelement!(main, "graphics") + _pscad_attributes!(graphics, (viewBox = "-200 -200 200 200", size = 2)) + rectangle = addelement!(graphics, "Gfx") + _pscad_attributes!(rectangle, + ( + id = _PSCAD_IDS.main_rectangle, + classid = "Graphics.Rectangle", + x = -36, + y = -36, + w = 72, + h = 72 + )) + _pscad_paramlist!(rectangle, + [ + "color" => "Black", + "dasharray" => "0", + "thickness" => "0", + "port" => "", + "fill_style" => "0", + "fill_fg" => "Black", + "fill_bg" => "Black", + "cond" => "true" + ]) + text = addelement!(graphics, "Gfx") + _pscad_attributes!(text, ( + id = _PSCAD_IDS.main_text, + classid = "Graphics.Text", + x = 0, + y = 0 + )) + _pscad_paramlist!(text, + [ + "text" => "%:Name", + "anchor" => "0", + "full_font" => "Tahoma, 13world", + "angle" => "0", + "color" => "Black", + "cond" => "true" + ]) + + schematic = addelement!(main, "schematic") + schematic["classid"] = "UserCanvas" + _pscad_canvas_params!(schematic; user = true) + addelement!(schematic, "grouping") + _pscad_wire!( + schematic, + _PSCAD_IDS.cable_wire, + _PSCAD_IDS.cable_instance, + "Cable", + "$namespace:CableSystem", + "$namespace:CableSystem", + "$namespace:CableSystem", + _pscad_main_instance_parameters(system, base_frequency); + x = 72, + y = 36, + width = 107, + height = 128, + crc = -1 + ) + return main +end + +function _pscad_hierarchy!(project, namespace::AbstractString) + addelement!(project, "List")["classid"] = "Resource" + hierarchy = addelement!(project, "hierarchy") + station = addelement!(hierarchy, "call") + _pscad_attributes!(station, + ( + link = _PSCAD_IDS.station, + name = "$namespace:DS", + z = -1, + view = "false", + instance = 0 + )) + main = addelement!(station, "call") + _pscad_attributes!(main, + ( + link = _PSCAD_IDS.main_instance, + name = "$namespace:Main", + z = -1, + view = "false", + instance = 0 + )) + cable = addelement!(main, "call") + _pscad_attributes!(cable, + ( + link = _PSCAD_IDS.cable_instance, + name = "$namespace:CableSystem", + z = -1, + view = "true", + instance = 0 + )) + return hierarchy +end + +function _pscad_part_parameters(component, index::Int, angular_frequency) + radii = (("R1", "R2", "R3"), ("R4", "R4", "R5"), + ("R6", "R6", "R7"), ("R8", "R8", "R9")) + resistivities = ("RHOC", "RHOS", "RHOA", "RHOO") + permeabilities = ("PERMC", "PERMS", "PERMA", "PERMO") + dielectric_permittivities = ("EPS1", "EPS2", "EPS3", "EPS4") + dielectric_permeabilities = ("PERM1", "PERM2", "PERM3", "PERM4") + loss_tangents = ("LT1", "LT2", "LT3", "LT4") + conductor_name = "CONNAM$index" + inner_name, outer_name, insulation_name = radii[index] + conductor = component.conductor + dielectric = component.dielectric + capacitance = dielectric.shunt_capacitance + conductance = dielectric.shunt_conductance + loss = iszero(capacitance) ? zero(capacitance) : + conductance / (angular_frequency * capacitance) + + parameters = Pair{String, String}[ + conductor_name => uppercasefirst(String(component.name)), + outer_name => _pscad_value(conductor.r_ex), + resistivities[index] => _pscad_value(conductor.material.rho), + permeabilities[index] => _pscad_value(conductor.material.mu_r), + insulation_name => _pscad_value(dielectric.r_ex), + dielectric_permittivities[index] => _pscad_value(dielectric.material.eps_r), + dielectric_permeabilities[index] => _pscad_value(dielectric.material.mu_r), + loss_tangents[index] => _pscad_value( + loss; maximum = 10) +] + index == 1 && pushfirst!( + parameters, + inner_name => _pscad_value(conductor.r_in) + ) + index > 1 && push!(parameters, "elim$(index - 1)" => "0") + push!(parameters, "T$(2index + 1)" => "0.0000") + index == 1 && append!(parameters, [ + "SemiCL" => "0", "SL2" => "0.0000", "SL1" => "0.0000" + ]) + return parameters +end + +function _empty_pscad_part(index::Int) + radii = (("R1", "R2", "R3"), ("R4", "R4", "R5"), + ("R6", "R6", "R7"), ("R8", "R8", "R9")) + resistivities = ("RHOC", "RHOS", "RHOA", "RHOO") + permeabilities = ("PERMC", "PERMS", "PERMA", "PERMO") + epsilons = ("EPS1", "EPS2", "EPS3", "EPS4") + mus = ("PERM1", "PERM2", "PERM3", "PERM4") + losses = ("LT1", "LT2", "LT3", "LT4") + _, outer_name, insulation_name = radii[index] + parameters = Pair{String, String}[ + "CONNAM$index" => "none", + outer_name => "0.0", + resistivities[index] => "0.0", + permeabilities[index] => "0.0", + insulation_name => "0.0", + epsilons[index] => "0.0", + mus[index] => "0.0", + losses[index] => "0.0000", + "T$(2index + 1)" => "0.0000" +] + index > 1 && push!(parameters, "elim$(index - 1)" => "0") + return parameters +end + +function _pscad_components(blueprint::Engine.CableBlueprint, frequency, formulation, temperature) + T = eltype(blueprint) + omega = 2 * (one(T) * pi) * convert(T, frequency) + epsilon0 = vacuum_permittivity(T) + return map(eachindex(blueprint.conductors)) do index + conductor = blueprint.conductors[index] + material = conductor.material + operating = temperature === nothing ? material.T0 : convert(T, temperature) + rho = constitutive(formulation.methods.temperature_dependence, material, operating) + metal = Material(material.kind, rho, material.eps_r, material.mu_r, + material.T0, material.alpha; rho_thermal = material.rho_thermal, + theta_max = material.theta_max, tan_delta = material.tan_delta, + sigma_solar = material.sigma_solar) + layers = @view blueprint.dielectrics[blueprint.dielectric_ranges[index]] + impedance = zero(Complex{T}) + for layer in layers + source = layer.material + operating = temperature === nothing ? source.T0 : convert(T, temperature) + relation = source.kind === :semicon ? formulation.methods.semicon_admittance : + formulation.methods.insulation_admittance + kappa = constitutive(relation, source, frequency, operating; + temperature_dependence = formulation.methods.temperature_dependence) + impedance += inv(Engine.layer_admittance(layer.r_in, layer.r_ex, kappa)) + end + admittance = isempty(layers) ? zero(Complex{T}) : inv(impedance) + capacitance = imag(admittance) / omega + outer = isempty(layers) ? conductor.r_ex : last(layers).r_ex + epsilon = isempty(layers) ? zero(T) : + capacitance * log(outer / conductor.r_ex) / + (2 * (one(T) * pi) * epsilon0) + permeability = isempty(layers) ? one(T) : + DataModel.equivalent_dielectric_permeability( + layers, conductor.turns_per_length, conductor.r_ex, outer) + dielectric = Material(:insulator, oftype(epsilon, Inf), epsilon, permeability) + return (name = conductor.terminal, + conductor = (r_in = conductor.r_in, r_ex = conductor.r_ex, material = metal), + dielectric = (r_ex = outer, material = dielectric, + shunt_capacitance = capacitance, shunt_conductance = real(admittance))) + end +end + +function _pscad_cable_parameters( + design, + components, + position, + connections, + index::Int, + base_frequency +) + length(components) <= 4 || throw(ArgumentError( + "PSCAD Cable_Coax supports at most four concentric components", + )) + length(connections) == length(components) || throw(DimensionMismatch( + "PSCAD phase mapping must match the cable component count", + )) + # LL=0 is PSCAD's bare-conductor configuration. LL=1 includes insulation. + bare = length(components) == 1 && + only(components).dielectric.r_ex == only(components).conductor.r_ex + parameters = Pair{String, String}[ + "CABNUM" => string(index), + "Name" => design.cable_id, + "X" => _pscad_value(position.x), + "OHC" => position.y < 0 ? + "0" : + "1", + "Y" => position.y < 0 ? + _pscad_value(abs(position.y)) : + "0.0", + "Y2" => position.y > 0 ? + _pscad_value(position.y) : + "0.0", + "ShuntA" => "1.0e-38 [mho/m]", + "FLT" => _pscad_value(base_frequency), + "RorT" => "0", + "LL" => bare ? + "0" : + string(2length(components) - 1), + "CROSSBOND" => "0", + "GROUPNO" => "1", + "CBC1" => "1", + "CBC2" => "0", + "CBC3" => "0", + "CBC4" => "0", + "SHRad" => "1", + "LC" => "3" +] + angular_frequency = 2 * pi * base_frequency + for (component_index, component) in enumerate(components) + append!(parameters, _pscad_part_parameters( + component, component_index, angular_frequency + )) + if component_index > 1 + elimination = "elim$(component_index - 1)" + parameter_index = findlast(pair -> first(pair) == elimination, parameters) + terminal_index = only(findall(==(component.name), design.terminal_order)) + parameters[parameter_index] = elimination => (connections[terminal_index] == + 0 ? "1" : "0") + end + end + for component_index in (length(components) + 1):4 + append!(parameters, _empty_pscad_part(component_index)) + end + return parameters +end + +function _pscad_project(system::LineCableSystem, earth::EarthModel, base_frequency, components; + native_settings::NamedTuple = (;)) + isempty(setdiff(keys(native_settings), (:ground, :frequency))) || throw(ArgumentError( + "PSCAD native settings require ground and frequency fields")) + document = XMLDocument() + project = ElementNode("project") + setroot!(document, project) + _pscad_attributes!(project, ( + name = system.system_id, + version = "5.0.2", + schema = "", + Target = "EMTDC" + )) + _pscad_project_settings!(project) + _pscad_project_scaffolding!(project) + + definitions = addelement!(project, "definitions") + _pscad_station_definition!(definitions, system.system_id) + _pscad_main_definition!(definitions, system, base_frequency) + + definition = addelement!(definitions, "Definition") + _pscad_attributes!(definition, + ( + id = _PSCAD_IDS.cable_system, + classid = "RowDefn", + name = "CableSystem", + group = "", + url = "", + version = "RowDefn", + build = "RowDefn", + crc = -1, + key = "", + view = "false", + date = _PSCAD_DEFINITION_DATE + )) + _pscad_paramlist!(definition, ["Description" => "", "type" => "Cable"]) + schematic = addelement!(definition, "schematic") + schematic["classid"] = "RowCanvas" + _pscad_paramlist!(schematic, [ + "show_grid" => "0", + "size" => "0", + "orient" => "1", + "show_border" => "0" + ]) + + identifier = _PSCAD_IDS.first_physical_component + frequency_parameters = [ + "FS" => "0.5", "FE" => "1.0E6", "Numf" => "100", + "DCCOR" => "1", "CPASS" => "0", "enablf" => "1", "shntcab" => "0.0"] + for (field, setting) in pairs(get(native_settings, :frequency, (;))) + index = findfirst(pair -> first(pair) == string(field), frequency_parameters) + index === nothing && + throw(ArgumentError("unsupported PSCAD frequency field $field")) + frequency_parameters[index] = string(field) => _pscad_value(setting.value) + end + _pscad_user!(schematic, identifier, _PSCAD_FREQUENCY_BINDING, + frequency_parameters; x = 576, y = 180) + addelement!(schematic, "grouping") + identifier += 1 + for (index, (design, position, connections)) in enumerate(zip( + system.designs, + system.positions, + system.connections + )) + _pscad_user!( + schematic, + identifier, + _PSCAD_CABLE_BINDING, + _pscad_cable_parameters( + design, + components[index], + position, + connections, + index, + base_frequency + ); + x = 234 + (index - 1) * 400, + y = 612 + ) + identifier += 1 + end + ground = last(earth.layers) + ground_parameters = [ + "EarthForm2" => "0", "EarthForm" => "3", "EarthForm3" => "2", + "GrRho" => "0", "GRRES" => _pscad_value(ground.rho), + "GPERM" => _pscad_value(ground.mu_r), "K0" => "0.001", + "K1" => "0.01", "alpha" => "0.7", "GRP" => _pscad_value(ground.eps_r)] + for (field, setting) in pairs(get(native_settings, :ground, (;))) + index = findfirst(pair -> first(pair) == string(field), ground_parameters) + index === nothing && throw(ArgumentError("unsupported PSCAD ground field $field")) + ground_parameters[index] = string(field) => _pscad_value(setting.value) + end + _pscad_user!(schematic, identifier, _PSCAD_GROUND_BINDING, + ground_parameters; x = 504, y = 288) + _pscad_hierarchy!(project, system.system_id) + return document +end diff --git a/src/pscad/importexport/pscad.jl b/src/pscad/importexport/pscad.jl new file mode 100644 index 000000000..d27593217 --- /dev/null +++ b/src/pscad/importexport/pscad.jl @@ -0,0 +1,79 @@ +include("schema.jl") +include("project.jl") +include("import.jl") + +""" +$(TYPEDSIGNATURES) + +Export a [`LineCableSystem`](@ref) as a minimal PSCAD project. + +The generated project preserves PSCAD's `master:Line_FrePhase_Options`, +`master:Cable_Coax`, and `master:Line_Ground` component bindings. Cable +geometry, material properties, dielectric losses, phase eliminations, line +length, base frequency, and static earth properties are emitted as component +parameters. PSCAD may normalize the deterministic placeholder identifiers when +it opens the project. + +# Arguments + +- `system`: materialized line and cable geometry. +- `earth`: physical air and one infinite homogeneous soil half-space. +- `base_freq`: base frequency in hertz. +- `file_name`: destination `.pscx` file. The system identifier is prepended to + an explicitly supplied basename. +- `formulation`: selected line-parameter or cable-constant formulation. + The default routes to the lossless dielectric relation. Request `:lossy` + explicitly to include the supplied material losses. +- `native_settings=(;)`: optional validated native `ground` and `frequency` + field records supplied by the PSCAD formula adapter. Each field includes its + `value` and expected `readback`. The same record accompanies native execution. +- `temperature=nothing`: optional operating temperature \\[°C\\]. Correction is + applied here during export, not in geometric flattening. `nothing` retains + the material reference temperatures. + +!!! note + PSCAD uses a reference-frequency equivalent loss tangent, bounded at ten. + It does not reproduce an arbitrary broadband constitutive law. Exporting + the selected relation matches its radial admittance at `base_freq` before + that cap, not necessarily at every frequency in a later PSCAD scan. + +# Returns + +The written path. Filesystem errors are propagated to the caller. +""" +function export_data( + ::Val{:pscad}, + system::LineCableSystem, + earth::EarthModel; + formulation::Union{Engine.LineParametersFormulation, + Engine.CableConstantsFormulation, PSCADFormulation} = Engine.Formulation(), + base_freq::Real = formulation isa PSCADFormulation ? + formulation.options.data.base_frequency : 50.0, + temperature::Union{Nothing, Real} = nothing, + file_name::Union{AbstractString, Nothing} = nothing, + native_settings::NamedTuple = (;) +) + _pscad_deterministic(eltype(system), eltype(earth), typeof(base_freq), typeof(temperature)) + isfinite(base_freq) && base_freq > zero(base_freq) || throw(DomainError( + base_freq, "PSCAD base frequency must be positive and finite" + )) + validate(earth, PSCADFormulation) + path = _pscad_output_path(system, file_name) + #! explicit-imports: off + # EzXML does not mark XMLError public, but this exporter preserves the + # established IOError behavior for invalid XML output destinations. + isdir(path) && throw(EzXML.XMLError( + 8, + 0, + "PSCAD output path is a directory: $path", + 2, + 0 + )) + #! explicit-imports: on + blueprints = _pscad_blueprints(system) + components = [_pscad_components(blueprint, base_freq, formulation, temperature) + for blueprint in blueprints] + document = _pscad_project(system, earth, base_freq, components; native_settings) + write(path, document) + return path +end diff --git a/src/pscad/importexport/schema.jl b/src/pscad/importexport/schema.jl new file mode 100644 index 000000000..55f9ece92 --- /dev/null +++ b/src/pscad/importexport/schema.jl @@ -0,0 +1,133 @@ +const _PSCAD_FREQUENCY_BINDING = "master:Line_FrePhase_Options" +const _PSCAD_CABLE_BINDING = "master:Cable_Coax" +const _PSCAD_GROUND_BINDING = "master:Line_Ground" + +function _pscad_attributes!(node, attributes) + for (name, value) in pairs(attributes) + node[string(name)] = string(value) + end + return node +end + +function _pscad_paramlist!( + parent, + parameters; + name = nothing, + link = nothing, + crc = nothing +) + list = addelement!(parent, "paramlist") + name === nothing || (list["name"] = string(name)) + link === nothing || (list["link"] = string(link)) + crc === nothing || (list["crc"] = string(crc)) + for (name, value) in parameters + parameter = addelement!(list, "param") + parameter["name"] = string(name) + parameter["value"] = string(value) + end + return list +end + +function _pscad_params!(parent, parameters) + return _pscad_paramlist!(parent, parameters; name = "", link = -1, crc = -1) +end + +function _pscad_user!( + parent, + id::Integer, + binding::AbstractString, + parameters; + x::Integer = 0, + y::Integer = 0 +) + user = addelement!(parent, "User") + _pscad_attributes!(user, + ( + id = id, + name = binding, + classid = "UserCmp", + x = x, + y = y, + w = 0, + h = 0, + z = -1, + orient = 0, + defn = binding, + link = -1, + q = 4, + disable = "false" + )) + _pscad_params!(user, parameters) + return user +end + +function _pscad_wire!( + parent, + wire_id::Integer, + user_id::Integer, + kind::AbstractString, + name::AbstractString, + definition::AbstractString, + user_binding::AbstractString, + parameters; + x::Integer, + y::Integer, + width::Integer, + height::Integer, + crc = nothing +) + wire = addelement!(parent, "Wire") + attributes = ( + id = wire_id, + name, + classid = kind, + x, + y, + w = width, + h = height, + orient = 0, + defn = definition, + recv = -1, + send = -1, + back = -1, + disable = "false" + ) + _pscad_attributes!(wire, attributes) + crc === nothing || (wire["crc"] = string(crc)) + for (vertex_x, vertex_y) in ((0, 0), (0, 18), (54, 54), (54, 72)) + vertex = addelement!(wire, "vertex") + _pscad_attributes!(vertex, (x = vertex_x, y = vertex_y)) + end + _pscad_user!(wire, user_id, user_binding, parameters) + return wire +end + +function _pscad_canvas_params!(canvas; user::Bool = false) + parameters = Pair{String, String}[ + "show_grid" => "0", + "size" => "0", + "orient" => "1", + "show_border" => "0", + "monitor_bus_voltage" => "0", + "show_signal" => "0", + "show_virtual" => "0", + "show_sequence" => "0", + "auto_sequence" => "1", + "bus_expand_x" => "8", + "bus_expand_y" => "8", + "bus_length" => "4" +] + user && append!(parameters, [ + "show_terminals" => "0", + "virtual_filter" => "", + "animation_freq" => "500" + ]) + return _pscad_paramlist!(canvas, parameters) +end + +function _pscad_value(value; sigdigits::Integer = 6, minimum = -Inf, maximum = Inf) + _pscad_deterministic(typeof(value)) + scalar = clamp(round(value; sigdigits), minimum, maximum) + iszero(scalar) && return "0.0" + return string(scalar) +end diff --git a/src/pscad/remote/Manifest.toml b/src/pscad/remote/Manifest.toml new file mode 100644 index 000000000..532198ade --- /dev/null +++ b/src/pscad/remote/Manifest.toml @@ -0,0 +1,318 @@ +# This file is machine-generated - editing it directly is not advised + +julia_version = "1.12.7" +manifest_format = "2.0" +project_hash = "140fc71a07663969a7fa7c65059b100c6443e145" + +[[deps.ArgTools]] +uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" +version = "1.1.2" + +[[deps.Artifacts]] +uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" +version = "1.11.0" + +[[deps.Base64]] +uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" +version = "1.11.0" + +[[deps.CompilerSupportLibraries_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" +version = "1.3.1+2" + +[[deps.CondaPkg]] +deps = ["JSON", "Markdown", "MicroMamba", "Pidfile", "Pkg", "Preferences", "Scratch", "TOML", "pixi_jll"] +git-tree-sha1 = "2b1afb8ae65a0758795b00adafb37f97e67ef0e9" +uuid = "992eb4ea-22a4-4c89-a5bb-47a3300528ab" +version = "0.2.36" + +[[deps.DataAPI]] +git-tree-sha1 = "abe83f3a2f1b857aac70ef8b269080af17764bbe" +uuid = "9a962f9c-6df0-11e9-0e5d-c546b8b5ee8a" +version = "1.16.0" + +[[deps.DataValueInterfaces]] +git-tree-sha1 = "bfc1187b79289637fa0ef6d4436ebdfe6905cbd6" +uuid = "e2d170a0-9d28-54be-80f0-106bbe20a464" +version = "1.0.0" + +[[deps.Dates]] +deps = ["Printf"] +uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" +version = "1.11.0" + +[[deps.Downloads]] +deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] +uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" +version = "1.7.0" + +[[deps.FileWatching]] +uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" +version = "1.11.0" + +[[deps.InteractiveUtils]] +deps = ["Markdown"] +uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" +version = "1.11.0" + +[[deps.IteratorInterfaceExtensions]] +git-tree-sha1 = "a3f24677c21f5bbe9d2a714f95dcd58337fb2856" +uuid = "82899510-4779-5014-852e-03e436cf321d" +version = "1.0.0" + +[[deps.JLLWrappers]] +deps = ["Artifacts", "Preferences"] +git-tree-sha1 = "7204148362dafe5fe6a273f855b8ccbe4df8173e" +uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" +version = "1.8.0" + +[[deps.JSON]] +deps = ["Dates", "Logging", "Parsers", "PrecompileTools", "StructUtils", "UUIDs", "Unicode"] +git-tree-sha1 = "c7345ab1a7ca4dc8a02c9f6510da0d9857bbe513" +uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +version = "1.7.1" + + [deps.JSON.extensions] + JSONArrowExt = ["ArrowTypes"] + + [deps.JSON.weakdeps] + ArrowTypes = "31f734f8-188a-4ce0-8406-c8a06bd891cd" + +[[deps.JuliaSyntaxHighlighting]] +deps = ["StyledStrings"] +uuid = "ac6e5ff7-fb65-4e79-a425-ec3bc9c03011" +version = "1.12.0" + +[[deps.LazyArtifacts]] +deps = ["Artifacts", "Pkg"] +uuid = "4af54fe1-eca0-43a8-85a7-787d91b784e3" +version = "1.11.0" + +[[deps.LibCURL]] +deps = ["LibCURL_jll", "MozillaCACerts_jll"] +uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" +version = "0.6.4" + +[[deps.LibCURL_jll]] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll", "Zlib_jll", "nghttp2_jll"] +uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" +version = "8.15.0+0" + +[[deps.LibGit2]] +deps = ["LibGit2_jll", "NetworkOptions", "Printf", "SHA"] +uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" +version = "1.11.0" + +[[deps.LibGit2_jll]] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll"] +uuid = "e37daf67-58a4-590a-8e99-b0245dd2ffc5" +version = "1.9.0+0" + +[[deps.LibSSH2_jll]] +deps = ["Artifacts", "Libdl", "OpenSSL_jll"] +uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" +version = "1.11.3+1" + +[[deps.Libdl]] +uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" +version = "1.11.0" + +[[deps.Logging]] +uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" +version = "1.11.0" + +[[deps.MacroTools]] +git-tree-sha1 = "1e0228a030642014fe5cfe68c2c0a818f9e3f522" +uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" +version = "0.5.16" + +[[deps.Markdown]] +deps = ["Base64", "JuliaSyntaxHighlighting", "StyledStrings"] +uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" +version = "1.11.0" + +[[deps.MicroMamba]] +deps = ["Pkg", "Scratch", "micromamba_jll"] +git-tree-sha1 = "535656ce55266bfed0575cd051acc4f36dc869a0" +uuid = "0b3b1443-0f03-428d-bdfb-f27f9c1191ea" +version = "0.1.15" + +[[deps.MozillaCACerts_jll]] +uuid = "14a3606d-f60d-562e-9121-12d972cd8159" +version = "2025.11.4" + +[[deps.NetworkOptions]] +uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" +version = "1.3.0" + +[[deps.OpenSSL_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" +version = "3.5.6+0" + +[[deps.OrderedCollections]] +git-tree-sha1 = "05f45c2e0de6259db764adbfd2f1dc6d3f8de13c" +uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +version = "2.0.1" + +[[deps.Parsers]] +deps = ["Dates", "PrecompileTools", "UUIDs"] +git-tree-sha1 = "3de8f5e6e90ebfa8d6d1f86997d6cdcd6a912ff3" +uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" +version = "2.8.7" + +[[deps.Pidfile]] +deps = ["FileWatching", "Test"] +git-tree-sha1 = "2d8aaf8ee10df53d0dfb9b8ee44ae7c04ced2b03" +uuid = "fa939f87-e72e-5be4-a000-7fc836dbe307" +version = "1.3.0" + +[[deps.Pkg]] +deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"] +uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +version = "1.12.1" + + [deps.Pkg.extensions] + REPLExt = "REPL" + + [deps.Pkg.weakdeps] + REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" + +[[deps.PrecompileTools]] +deps = ["Preferences"] +git-tree-sha1 = "edbeefc7a4889f528644251bdb5fc9ab5348bc2c" +uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a" +version = "1.3.4" + +[[deps.Preferences]] +deps = ["TOML"] +git-tree-sha1 = "8b770b60760d4451834fe79dd483e318eee709c4" +uuid = "21216c6a-2e73-6563-6e65-726566657250" +version = "1.5.2" + +[[deps.Printf]] +deps = ["Unicode"] +uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" +version = "1.11.0" + +[[deps.PythonCall]] +deps = ["CondaPkg", "Dates", "Libdl", "MacroTools", "Markdown", "Preferences", "Serialization", "Tables", "UnsafePointers"] +git-tree-sha1 = "2b67e030054dd9438a00e3d7f59927e839b00569" +uuid = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d" +version = "0.9.35" + + [deps.PythonCall.extensions] + CategoricalArraysExt = "CategoricalArrays" + PyCallExt = "PyCall" + + [deps.PythonCall.weakdeps] + CategoricalArrays = "324d7699-5711-5eae-9e2f-1d82baa6b597" + PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0" + +[[deps.Random]] +deps = ["SHA"] +uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +version = "1.11.0" + +[[deps.SHA]] +uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" +version = "0.7.0" + +[[deps.Scratch]] +deps = ["Dates"] +git-tree-sha1 = "9b81b8393e50b7d4e6d0a9f14e192294d3b7c109" +uuid = "6c6a2e73-6563-6170-7368-637461726353" +version = "1.3.0" + +[[deps.Serialization]] +uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" +version = "1.11.0" + +[[deps.StructUtils]] +deps = ["Dates", "UUIDs"] +git-tree-sha1 = "2d0fc55c61321ba245c47be599570d11bac50303" +uuid = "ec057cc2-7a8d-4b58-b3b3-92acb9f63b42" +version = "2.8.5" + + [deps.StructUtils.extensions] + StructUtilsMeasurementsExt = ["Measurements"] + StructUtilsStaticArraysCoreExt = ["StaticArraysCore"] + StructUtilsTablesExt = ["Tables"] + + [deps.StructUtils.weakdeps] + Measurements = "eff96d63-e80a-5855-80a2-b1b0885c5ab7" + StaticArraysCore = "1e83bf80-4336-4d27-bf5d-d5a4f845583c" + Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" + +[[deps.StyledStrings]] +uuid = "f489334b-da3d-4c2e-b8f0-e476e12c162b" +version = "1.11.0" + +[[deps.TOML]] +deps = ["Dates"] +uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" +version = "1.0.3" + +[[deps.TableTraits]] +deps = ["IteratorInterfaceExtensions"] +git-tree-sha1 = "c06b2f539df1c6efa794486abfb6ed2022561a39" +uuid = "3783bdb8-4a98-5b6b-af9a-565f29a5fe9c" +version = "1.0.1" + +[[deps.Tables]] +deps = ["DataAPI", "DataValueInterfaces", "IteratorInterfaceExtensions", "OrderedCollections", "TableTraits"] +git-tree-sha1 = "0f38a06c83f0007bbab3cf911262841c9a0f07e0" +uuid = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" +version = "1.13.0" + +[[deps.Tar]] +deps = ["ArgTools", "SHA"] +uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" +version = "1.10.0" + +[[deps.Test]] +deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] +uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +version = "1.11.0" + +[[deps.UUIDs]] +deps = ["Random", "SHA"] +uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" +version = "1.11.0" + +[[deps.Unicode]] +uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" +version = "1.11.0" + +[[deps.UnsafePointers]] +git-tree-sha1 = "c81331b3b2e60a982be57c046ec91f599ede674a" +uuid = "e17b2a0c-0bdf-430a-bd0c-3a23cae4ff39" +version = "1.0.0" + +[[deps.Zlib_jll]] +deps = ["Libdl"] +uuid = "83775a58-1f1d-513f-b197-d71354ab007a" +version = "1.3.1+2" + +[[deps.micromamba_jll]] +deps = ["Artifacts", "JLLWrappers", "LazyArtifacts", "Libdl"] +git-tree-sha1 = "717df6f6892af4ee13279a73aa58474e58a88667" +uuid = "f8abcde7-e9b7-5caa-b8af-a437887ae8e4" +version = "2.3.1+0" + +[[deps.nghttp2_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" +version = "1.64.0+1" + +[[deps.p7zip_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] +uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" +version = "17.7.0+0" + +[[deps.pixi_jll]] +deps = ["Artifacts", "JLLWrappers", "LazyArtifacts", "Libdl"] +git-tree-sha1 = "56c56fede8f01e1be7e2fdf1eb911487640619a0" +uuid = "4d7b5844-a134-5dcd-ac86-c8f19cd51bed" +version = "0.76.2+0" diff --git a/src/pscad/remote/Project.toml b/src/pscad/remote/Project.toml new file mode 100644 index 000000000..339654085 --- /dev/null +++ b/src/pscad/remote/Project.toml @@ -0,0 +1,7 @@ +[deps] +PythonCall = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d" +TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" + +[compat] +PythonCall = "0.9" +julia = "1.12" diff --git a/src/pscad/remote/configuration.jl b/src/pscad/remote/configuration.jl new file mode 100644 index 000000000..fcb6d828b --- /dev/null +++ b/src/pscad/remote/configuration.jl @@ -0,0 +1,135 @@ +const PSCAD_TIMING_SCOPE = "PSCAD compile call; excludes output-readiness wait and transfer" + +""" +$(TYPEDEF) + +Station connection and filesystem mapping for native PSCAD execution. +`local_root` and `shared_root` name the same directory on the caller and station. +`remote_root` is scratch space on the station. Construction from field values +does not perform I/O. Construction from a TOML filename only reads that file. +`timeout` is the remote execution limit \\[s\\], defaulting to 1800. + +$(TYPEDFIELDS) +""" +struct RemoteConfig + local_root::String + host::String + shared_root::String + remote_root::String + julia_executable::String + python_executable::String + pscad_version::String + transport::Symbol + "Remote execution timeout \\[s\\]." + timeout::Int + "Argument array for `:command` transport. Exact `{host}` arguments are replaced." + command::Vector{String} +end + +function RemoteConfig( + host::AbstractString, + shared_root::AbstractString, + remote_root::AbstractString, + julia_executable::AbstractString, + python_executable::AbstractString; + local_root::AbstractString, + pscad_version::AbstractString = "5.1.0", + transport::Symbol = :ssh, + verbosity = nothing, + timeout::Integer = 1800, + command::AbstractVector{<:AbstractString} = String[] +) + verbosity === nothing || throw(ArgumentError( + "RemoteConfig no longer owns verbosity; set it with " * + "options=(verbosity=(default=0, PSCAD=2),) in compute", + )) + isempty(strip(host)) && throw(ArgumentError("PSCAD host cannot be empty")) + isempty(strip(local_root)) && throw(ArgumentError("PSCAD local root cannot be empty")) + isempty(strip(shared_root)) && throw(ArgumentError( + "PSCAD shared root cannot be empty", + )) + isempty(strip(remote_root)) && throw(ArgumentError("PSCAD remote root cannot be empty")) + isempty(strip(julia_executable)) && throw(ArgumentError( + "PSCAD-host Julia executable cannot be empty", + )) + isempty(strip(python_executable)) && throw(ArgumentError( + "PSCAD-host Python executable cannot be empty", + )) + pscad_version == "5.1.0" || throw(ArgumentError( + "this PSCAD adapter supports version 5.1.0 only", + )) + timeout > 0 || throw(ArgumentError( + "PSCAD timeout must be positive", + )) + if transport === :command + isempty(command) && throw(ArgumentError("PSCAD command transport requires an argument array")) + isempty(strip(first(command))) && throw(ArgumentError("PSCAD command executable cannot be empty")) + else + isempty(command) || throw(ArgumentError("PSCAD command arguments require transport=:command")) + end + return RemoteConfig( + abspath(local_root), + String(host), + String(shared_root), + String(remote_root), + String(julia_executable), + String(python_executable), + String(pscad_version), + transport, + Int(timeout), + String.(command) + ) +end + +""" +$(TYPEDSIGNATURES) + +Read a user-selected TOML file into a station configuration. + +# Arguments + +- `path`: TOML filename. Required fields are `host`, `local_root`, `shared_root`, + `remote_root`, `julia_executable`, and `python_executable`. Optional fields are + `pscad_version`, `transport`, `timeout`, and `command`. + +# Returns + +- A `RemoteConfig`. A relative `local_root` is resolved against the file's + directory. Station-side paths are retained verbatim. + +# Notes + +`transport` is a string identifying a transport method. For `"command"`, `command` +is an argument array, with exact `{host}` arguments replaced by `host`. +Encoded PowerShell arguments are appended without shell evaluation. +""" +function RemoteConfig(path::AbstractString) + values = TOML.parsefile(path) + required = ("host", "local_root", "shared_root", "remote_root", + "julia_executable", "python_executable") + optional = ("pscad_version", "transport", "timeout", "command") + unknown = setdiff(keys(values), (required..., optional...)) + isempty(unknown) || throw(ArgumentError("unknown PSCAD configuration fields: $(sort!(collect(unknown)))")) + for name in required + get(values, name, nothing) isa AbstractString || throw(ArgumentError( + "PSCAD configuration requires string field $name")) + end + for name in ("pscad_version", "transport") + !haskey(values, name) || values[name] isa AbstractString || throw(ArgumentError( + "PSCAD configuration field $name must be a string")) + end + timeout = get(values, "timeout", 1800) + timeout isa Integer && !(timeout isa Bool) || throw(ArgumentError( + "PSCAD timeout must be an integer")) + command = get(values, "command", String[]) + command isa AbstractVector && all(value -> value isa AbstractString, command) || + throw(ArgumentError("PSCAD command must be an array of strings")) + local_root = values["local_root"] + isempty(strip(local_root)) && throw(ArgumentError("PSCAD local root cannot be empty")) + return RemoteConfig(values["host"], values["shared_root"], values["remote_root"], + values["julia_executable"], values["python_executable"]; + local_root = isabspath(local_root) ? local_root : joinpath(dirname(abspath(path)), local_root), + pscad_version = get(values, "pscad_version", "5.1.0"), + transport = Symbol(get(values, "transport", "ssh")), + timeout = timeout, command = String[value for value in command]) +end diff --git a/src/pscad/remote/files.jl b/src/pscad/remote/files.jl new file mode 100644 index 000000000..8bc945550 --- /dev/null +++ b/src/pscad/remote/files.jl @@ -0,0 +1,62 @@ +function _output_matches(roots, suffix) + matches=String[] + for root in unique(filter(isdir, roots)) + for (directory, _, files) in walkdir(root), file in files + + endswith(lowercase(file), suffix) && + push!(matches, realpath(joinpath(directory, file))) + end + end + unique!(matches) + return matches +end + +function _data_rows(path::AbstractString) + return count(eachline(path)) do line + fields=split(strip(line)) + length(fields) >= 2 || return false + return tryparse(Float64, fields[1]) !== nothing && + tryparse(Float64, fields[2]) !== nothing + end +end + +""" +Wait for complete output rows for at most `timeout` \\[s\\], checking every +`poll_interval` \\[s\\]. Require the requested row count before returning a path. +""" +function _wait_output( + roots, + suffix::AbstractString, + expected_rows::Integer; + timeout::Real = 30, + poll_interval::Real = 0.1 +) + expected_rows > 0 || throw(ArgumentError("expected_rows must be positive")) + timeout > 0 || throw(ArgumentError("timeout must be positive")) + poll_interval > 0 || throw(ArgumentError("poll_interval must be positive")) + deadline=time() + timeout + previous=nothing + observed="no matching file" + while time() < deadline + matches=_output_matches(roots, suffix) + length(matches) <= 1 || throw(ArgumentError( + "PSCAD emitted $(length(matches)) files ending in $suffix", + )) + if length(matches) == 1 + path=only(matches) + rows=_data_rows(path) + rows <= expected_rows || throw(ArgumentError( + "PSCAD output $path contains $rows rows; expected $expected_rows", + )) + state=(path, filesize(path), rows) + observed="$rows of $expected_rows rows in $path" + state == previous && rows == expected_rows && return path + previous=state + end + sleep(poll_interval) + end + throw(ArgumentError( + "PSCAD output $suffix did not become complete within $timeout seconds; " * + "last observed $observed", + )) +end diff --git a/src/pscad/remote/identity.py b/src/pscad/remote/identity.py new file mode 100644 index 000000000..2515cb5bd --- /dev/null +++ b/src/pscad/remote/identity.py @@ -0,0 +1,57 @@ +"""Identify PSCAD's selected line-constants implementation without solving a model.""" + +import hashlib +import platform +from importlib import metadata +from pathlib import Path + +from mhi.common import process +import mhi.pscad + + +def identify(version, app=None): + if version != "5.1.0": + raise ValueError("The LCM PSCAD adapter supports version 5.1.0") + executable = Path(process.find_exe("PSCAD", version=version, x64=True)).resolve() + root = executable.parents[2] + automation_version = metadata.version("mhi.pscad") + if automation_version != "3.1.2": + raise ValueError("The LCM PSCAD adapter requires mhi.pscad 3.1.2") + owned = app is None + try: + if owned: + app = mhi.pscad.launch(version=version, x64=True, minimize=True, + splash=False, silence=True, timeout=60) + if str(app.version) != version: + raise ValueError("PSCAD launched an unexpected application version") + # Version-bound workaround: mhi.pscad 3.1.2 settings() initializes its + # Fortran codec even for a read, raising "Unable to retrieve detected + # software" on this 5.1 station. This is its own underlying read-only + # settings call; bypass only that unrelated decoder, not solver settings. + # Revisit when the required automation version fixes that reader. + selected = app._settings({})["file_lcp"] + expanded = str(selected).replace("$(HomeDir)", str(root)) + solver = Path(expanded) + if "$(" in expanded or not solver.is_absolute(): + raise ValueError("Cannot identify PSCAD's selected LCP executable: " + str(selected)) + files = { + "pscad": executable, + "line_constants": solver.resolve(), + "master_library": root / "master.pslx", + } + result = { + "schema": "1", + "version": version, + "python_version": platform.python_version(), + "automation_version": automation_version, + "common_version": metadata.version("mhi.common"), + "profile": "saved profile; numerical inputs supplied explicitly", + } + for name, path in files.items(): + result[name + "_path"] = str(path) + with path.open("rb") as stream: + result[name + "_sha256"] = hashlib.file_digest(stream, "sha256").hexdigest() + return result + finally: + if owned and app is not None: + app.quit() diff --git a/src/pscad/remote/remote.jl b/src/pscad/remote/remote.jl new file mode 100644 index 000000000..2b9f003d7 --- /dev/null +++ b/src/pscad/remote/remote.jl @@ -0,0 +1,414 @@ +function _powershell_argv(powershell::AbstractString) + command="\$ProgressPreference='SilentlyContinue'; $(String(powershell))" + utf16 = htol.(transcode(UInt16, command)) + encoded = base64encode(reinterpret(UInt8, utf16)) + return [ + "powershell.exe", "-NoProfile", "-NonInteractive", + "-EncodedCommand", encoded + ] +end + +function remote_command(::Val{:ssh}, config::RemoteConfig, powershell::AbstractString) + return Cmd(vcat(["ssh", config.host], _powershell_argv(powershell))) +end + +function remote_command(::Val{:local}, ::RemoteConfig, powershell::AbstractString) + return Cmd(_powershell_argv(powershell)) +end + +function remote_command(::Val{:command}, config::RemoteConfig, powershell::AbstractString) + arguments = [value == "{host}" ? config.host : value for value in config.command] + return Cmd(vcat(arguments, _powershell_argv(powershell))) +end + +function remote_command(config::RemoteConfig, powershell::AbstractString) + # A caller may supply a transport method after the package was compiled. + return Base.invokelatest( + remote_command, + Val(config.transport), + config, + powershell + ) +end + +function _ps_quote(value::AbstractString) + return "'" * replace(String(value), "'" => "''") * "'" +end + +function _remote_path(root::AbstractString, parts::AbstractString...) + clean = rstrip(String(root), ('\\', '/')) + return join((clean, parts...), '\\') +end + +function _remote_project_name(path::AbstractString) + return splitext(basename(replace(String(path), '\\' => '/')))[1] +end + +function _run_remote( + config::RemoteConfig, + powershell::AbstractString; + stdout_path::Union{Nothing, AbstractString} = nothing, + stderr_path::Union{Nothing, AbstractString} = nothing, + stream::Bool = false, + on_interrupt::Function = () -> nothing, + timeout::Real = config.timeout +) + isfinite(timeout) && timeout > 0 || throw(ArgumentError( + "PSCAD transport timeout must be positive and finite")) + stdout_path === nothing || mkpath(dirname(stdout_path)) + stderr_path === nothing || mkpath(dirname(stderr_path)) + output = Pipe() + errors = Pipe() + process = run( + pipeline(remote_command(config, powershell); stdout = output, stderr = errors); + wait = false + ) + close(output.in) + close(errors.in) + output_buffer = IOBuffer() + error_buffer = IOBuffer() + output_file = stdout_path === nothing ? nothing : open(stdout_path, "w") + error_file = stderr_path === nothing ? nothing : open(stderr_path, "w") + output_task = @async for line in eachline(output) + println(output_buffer, line) + output_file === nothing || println(output_file, line) + output_file === nothing || flush(output_file) + if stream + println(stdout, line) + flush(stdout) + end + end + error_task = @async for line in eachline(errors) + println(error_buffer, line) + error_file === nothing || println(error_file, line) + error_file === nothing || flush(error_file) + if stream + println(stderr, line) + flush(stderr) + end + end + interrupted = nothing + try + finished = timedwait(() -> istaskfailed(output_task) || istaskfailed(error_task) || + (process_exited(process) && istaskdone(output_task) && istaskdone(error_task)), + timeout; pollint = min(0.1, timeout / 10)) + finished === :ok || throw(ErrorException( + "PSCAD transport exceeded its timeout of $timeout seconds")) + istaskfailed(output_task) && wait(output_task) + istaskfailed(error_task) && wait(error_task) + wait(process) + catch error + interrupted = error + # SIGKILL (9): a transport must not keep the caller waiting in its own + # signal handler after timeout or interruption. + process_running(process) && kill(process, 9) + if error isa InterruptException + try + on_interrupt() + catch cancellation_error + @warn "Remote PSCAD cancellation could not be confirmed" exception = ( + cancellation_error, catch_backtrace()) + end + end + try + wait(process) + catch wait_error + @warn "PSCAD transport wait failed during cleanup" exception = ( + wait_error, catch_backtrace()) + end + finally + if interrupted !== nothing + close(output) + close(errors) + end + for (stream_name, reader) in ((:stdout, output_task), (:stderr, error_task)) + try + wait(reader) + catch reader_error + if interrupted === nothing + interrupted = reader_error + elseif !(interrupted isa TaskFailedException && interrupted.task === reader) + @warn "PSCAD output reader failed during cleanup" stream = stream_name exception = ( + reader_error, catch_backtrace()) + end + end + end + for (stream_name, file) in ((:stdout, output_file), (:stderr, error_file)) + file === nothing && continue + try + close(file) + catch close_error + if interrupted === nothing + interrupted = close_error + else + @warn "PSCAD output log could not be closed" stream = stream_name exception = ( + close_error, catch_backtrace()) + end + end + end + end + stdout_value = String(take!(output_buffer)) + stderr_value = String(take!(error_buffer)) + interrupted === nothing || throw(interrupted) + success(process) || throw(ErrorException( + "remote PSCAD command failed with exit code $(process.exitcode)\n" * + "stdout:\n$stdout_value\nstderr:\n$stderr_value", + )) + return stdout_value +end + +function _supervisor_command( + config::RemoteConfig, + shared_case::AbstractString, + remote_case::AbstractString, + project_name::AbstractString, + formulation::PSCADFormulation, + frequencies_value::AbstractVector; + output_stem::AbstractString, + verbosity::Integer = 0 +) + validate(frequencies_value, PSCADFormulation) + label = only(description([formulation];roles=[:none])) + increments = length(frequencies_value) - 1 + shared_supervisor = _remote_path(shared_case, "toolkit", "supervisor.ps1") + return join( + ( + "& $(_ps_quote(shared_supervisor))", + "-SharedCase $(_ps_quote(shared_case))", + "-LocalCase $(_ps_quote(remote_case))", + "-Julia $(_ps_quote(config.julia_executable))", + "-Python $(_ps_quote(config.python_executable))", + "-ProjectName $(_ps_quote(project_name))", + "-OutputStem $(_ps_quote(output_stem))", + "-Formulation $(_ps_quote(label))", + "-FrequencyStart $(_ps_quote(string(first(frequencies_value))))", + "-FrequencyEnd $(_ps_quote(string(last(frequencies_value))))", + "-FrequencyIncrements $(_ps_quote(string(increments)))", + "-PSCADVersion $(_ps_quote(config.pscad_version))", + "-Verbosity $(_ps_quote(string(verbosity)))", + "-TimeoutSeconds $(_ps_quote(string(config.timeout)))" + ), + ' ') +end + +function _cancel_command(remote_case::AbstractString) + owner_path = _remote_path(remote_case, "owner.txt") + return "\$ownerPath=$(_ps_quote(owner_path)); " * + "if (-not (Test-Path -LiteralPath \$ownerPath -PathType Leaf)) { exit 0 }; " * + "\$owner=@(Get-Content -LiteralPath \$ownerPath); " * + "if (\$owner.Count -ne 2) { throw 'invalid PSCAD runner owner file' }; " * + "\$runner=[IO.Path]::GetFullPath(\$owner[1]); " * + "\$root=[IO.Path]::GetFullPath($(_ps_quote(remote_case))).TrimEnd('\\')+'\\'; " * + "if (-not \$runner.StartsWith(\$root,[StringComparison]::OrdinalIgnoreCase)) " * + "{ throw 'refusing to stop a process outside the PSCAD case directory' }; " * + "\$runnerPid=0; if (-not [int]::TryParse(\$owner[0],[ref]\$runnerPid)) " * + "{ throw 'invalid PSCAD runner PID' }; " * + "\$process=Get-CimInstance Win32_Process -Filter \"ProcessId = \$runnerPid\"; " * + "if (\$null -eq \$process) { Remove-Item -LiteralPath \$ownerPath -Force; exit 0 }; " * + "if (\$null -eq \$process.CommandLine -or " * + "\$process.CommandLine.IndexOf(\$runner,[StringComparison]::OrdinalIgnoreCase) -lt 0) " * + "{ throw 'recorded PID no longer belongs to the PSCAD runner' }; " * + "& taskkill.exe /PID \$runnerPid /T /F | Out-Null; " * + "if (\$LASTEXITCODE -ne 0) { throw 'could not stop PSCAD runner process tree' }; " * + "Remove-Item -LiteralPath \$ownerPath -Force -ErrorAction SilentlyContinue" +end + +function _cancel_remote( + config::RemoteConfig, + remote_case::AbstractString; + verbosity::Integer = 0 +) + _run_remote(config, _cancel_command(remote_case); stream = verbosity >= 2, + timeout = min(config.timeout, 30)) + return nothing +end + +# Keep remotely executed code paired with the loaded Julia adapter. Later +# invocations must not pick up working-tree edits mid-run. +const PSCAD_REMOTE_SOURCES = Dict(name => let + path = joinpath(@__DIR__, name) + Base.include_dependency(path) + read(path, String) + end +for name in ("Project.toml", "Manifest.toml", "files.jl", + "runner.jl", "supervisor.ps1", "identity.py")) + +""" +$(TYPEDSIGNATURES) + +Read the remote PSCAD installation identity without launching a simulation. + +# Arguments + +- `config`: station connection and selected PSCAD installation. + +# Returns + +- A string dictionary containing application, line-constants executable and + master-library paths and SHA-256 digests, automation versions, and the + selected line-constants implementation. A temporary application instance + reads the station settings and closes without loading a project. + +# Notes + +Passing this record as the `solver_identity` computation option requires that +installation. Fresh computations verify identity inside their own application +instance. Completed-run reuse verifies the current station before acceptance. +""" +function identify(config::RemoteConfig) + code = PSCAD_REMOTE_SOURCES["identity.py"] * "\nimport json\n" * + "for key, value in identify(" * repr(config.pscad_version) * ").items():\n" * + " print(json.dumps(key) + ' = ' + json.dumps(value))\n" + # Use the existing shared work directory. Embedding a whole script inside + # an encoded PowerShell command exceeds Windows' command-line limit. + directory = mktempdir(mkpath(config.local_root); prefix = "pscad-identity-") + result = try + write(joinpath(directory, "identify.py"), code) + remote = _remote_path(config.shared_root, basename(directory), "identify.py") + command = "& " * _ps_quote(config.python_executable) * " " * _ps_quote(remote) * + "; if (\$LASTEXITCODE -ne 0) { exit \$LASTEXITCODE }" + TOML.parse(_run_remote(config, command)) + finally + rm(directory; recursive = true) + end + return Dict{String, String}(validate(result, config)) +end + +# The solver identity that a PSCAD station reports for the requested configuration. +function validate(result::AbstractDict, config::RemoteConfig) + all(pair -> first(pair) isa AbstractString && last(pair) isa AbstractString, result) || + throw(ArgumentError("PSCAD solver identity must contain string fields")) + get(result, "version", nothing) == config.pscad_version || throw(ArgumentError( + "PSCAD station did not return the requested solver identity")) + get(result, "schema", nothing) == "1" && + all(name -> occursin(r"^[0-9a-f]{64}$", get(result, name * "_sha256", "")), + ("pscad", "line_constants", "master_library")) || throw(ArgumentError( + "PSCAD station returned an incomplete solver identity")) + return result +end + +function _stage_toolkit(local_project::AbstractString, local_output::AbstractString) + isfile(local_project) || throw(ArgumentError( + "local PSCAD input is missing: $local_project", + )) + run_directory = dirname(local_output) + expected_project = joinpath(run_directory, "generated.pscx") + abspath(local_project) == abspath(expected_project) || throw(ArgumentError( + "PSCAD project must be staged as $expected_project", + )) + toolkit_stage = joinpath(run_directory, "toolkit") + isdir(toolkit_stage) && rm(toolkit_stage; recursive = true) + mkpath(toolkit_stage) + for (name, source) in PSCAD_REMOTE_SOURCES + write(joinpath(toolkit_stage, name), source) + end + return toolkit_stage +end + +function _diagnostic_tail(path::AbstractString; count::Integer = 12) + isfile(path) || return "PSCAD produced no diagnostic log." + lines=filter(!isempty, strip.(readlines(path))) + isempty(lines) && return "PSCAD diagnostic log is empty." + return join(last(lines, min(count, length(lines))), '\n') +end + +function run_remote_pscad( + config::RemoteConfig, + local_project::AbstractString, + local_output::AbstractString, + formulation::PSCADFormulation, + frequencies_value::AbstractVector; + output_stem::AbstractString, + verbosity::Integer = 0 +) + verbosity in 0:2 || throw(ArgumentError("PSCAD verbosity must be 0, 1, or 2")) + validate(frequencies_value, PSCADFormulation) + isdir(local_output) && !isempty(readdir(local_output)) && + throw(ArgumentError( + "PSCAD output directory is not empty; select a new run directory: $local_output")) + mkpath(local_output) + relative = relpath(dirname(abspath(local_output)), config.local_root) + work_parts = splitpath(relative) + first(work_parts) == ".." && throw(ArgumentError( + "PSCAD native run directory must be inside remote.local_root")) + run_id = last(work_parts) + _stage_toolkit(local_project, local_output) + shared_case = _remote_path(config.shared_root, work_parts...) + remote_case = _remote_path(config.remote_root, work_parts..., output_stem) + stdout_path = joinpath(local_output, "stdout.txt") + stderr_path = joinpath(local_output, "stderr.txt") + transport_stdout = joinpath(local_output, "transport-stdout.txt") + transport_stderr = joinpath(local_output, "transport-stderr.txt") + command = _supervisor_command( + config, + shared_case, + remote_case, + _remote_project_name(local_project), + formulation, + frequencies_value; + output_stem, + verbosity + ) + verbosity >= 2 && @debug "Executing PSCAD frequency scan" host=config.host run_id formulation=only(description([formulation];roles=[:none])) frequencies=length(frequencies_value) timeout=config.timeout + execution_error = try + _run_remote( + config, + command; + stdout_path = transport_stdout, + stderr_path = transport_stderr, + stream = verbosity >= 2, + timeout = config.timeout + 60, + on_interrupt = () -> _cancel_remote(config, remote_case; verbosity) + ) + nothing + catch error + if !(error isa InterruptException) + try + _cancel_remote(config, remote_case; verbosity) + catch cancellation_error + @warn "Remote PSCAD cancellation could not be confirmed" host=config.host run_id exception=( + cancellation_error, catch_backtrace()) + end + end + error isa InterruptException && rethrow() + error + end + if execution_error !== nothing + console_path = joinpath(local_output, "pscad-console.txt") + if !isfile(console_path) + write( + console_path, + "PSCAD did not produce a console log before the remote failure.\n" + ) + end + summary=first(split(sprint(showerror, execution_error), '\n')) + throw(ErrorException( + "$summary\nLast PSCAD diagnostics:\n$(_diagnostic_tail(console_path))" * + "\nFull PSCAD diagnostics: $console_path" * + "\nTransport stdout: $transport_stdout" * + "\nTransport stderr: $transport_stderr" * + "\nRemote scratch: $remote_case", + )) + end + verbosity >= 2 && @debug "Checking PSCAD outputs" host=config.host run_id destination=local_output + required = ( + "pscad-console.txt", "timing.txt", "result_zm.out", "result_zp.out", + "result_ym.out", "result_yp.out" + ) + for name in required + path = joinpath(local_output, name) + isfile(path) || throw(ArgumentError("required PSCAD output is missing: $path")) + filesize(path) > 0 || throw(ArgumentError("required PSCAD output is empty: $path")) + end + elapsed = parse(Float64, strip(read(joinpath(local_output, "timing.txt"), String))) + verbosity >= 2 && @debug "PSCAD frequency scan completed" host=config.host run_id compile_call_seconds=elapsed timing_scope=PSCAD_TIMING_SCOPE + return ( + elapsed_seconds = elapsed, + elapsed_scope = PSCAD_TIMING_SCOPE, + exit_code = 0, + stdout_path, + stderr_path, + console_path = joinpath(local_output, "pscad-console.txt"), + output_dir = String(local_output) + ) +end diff --git a/src/pscad/remote/runner.jl b/src/pscad/remote/runner.jl new file mode 100644 index 000000000..4aa681deb --- /dev/null +++ b/src/pscad/remote/runner.jl @@ -0,0 +1,312 @@ +using PythonCall +using TOML + +include(joinpath(@__DIR__, "files.jl")) + +const MHI_PSCAD_VERSION = "3.1.2" +const OUTPUT_SUFFIXES = ( + "_zm.out" => "result_zm.out", + "_zp.out" => "result_zp.out", + "_ym.out" => "result_ym.out", + "_yp.out" => "result_yp.out" +) +_string(value) = pyconvert(String, pybuiltins.str(value)) +_components(value) = pyconvert(Vector{Py}, pybuiltins.list(value)) + +function _definition(component) + return lowercase(_string(component.defn_name)) +end + +function _single_component(components, definition) + matches = filter(component -> _definition(component) == lowercase(definition), components) + length(matches) == 1 || throw(ArgumentError( + "PSCAD project exposes $(length(matches)) components named $definition", + )) + return only(matches) +end + +function _parameters(component) + raw = pyconvert(Dict, component.parameters()) + return Dict(string(key) => value for (key, value) in raw) +end + +_same_value(observed::Number, requested::Number) = observed == requested +_same_value(observed, requested) = string(observed) == string(requested) + +function _set!(component, name::AbstractString, value; readback = value) + haskey(_parameters(component), name) || throw(KeyError( + "PSCAD component $(_string(component.defn_name)) has no field $name", + )) + component.parameters(; Dict(Symbol(name) => value)...) + observed = _parameters(component)[name] + _same_value(observed, readback) || throw(ArgumentError( + "PSCAD field $name rejected $(repr(value)); expected readback " * + "$(repr(readback)), found $(repr(observed))", + )) + return value +end + +function _disabled(value) + value isa Number && return iszero(value) + return uppercase(strip(string(value))) in ( + "0", "NO", "FALSE", "DISABLE", "DISABLED", "RETAIN", "NONE") +end + +# The generated PSCAD project retains every requested cable port. +function validate(components) + cables = filter( + component -> _definition(component) == "master:cable_coax", + components + ) + isempty(cables) && throw(ArgumentError( + "generated PSCAD project exposes no master:Cable_Coax components", + )) + for cable in cables + parameters = _parameters(cable) + elimination_fields = sort!(filter( + field -> occursin(r"^elim\d+$", field), + collect(keys(parameters)) + )) + for field in elimination_fields + _disabled(parameters[field]) || throw(ArgumentError( + "PSCAD field $field requests conductor elimination; requested ports must be retained", + )) + end + end + return components +end + +function _messages(project) + messages = try + _components(project.messages()) + catch error + return "Unable to read PSCAD messages: $(sprint(showerror, error))\n" + end + return join((_string(message) for message in messages), '\n') * "\n" +end + +function _report(console::IO, verbosity::Int, level::Int, message::AbstractString) + line = "[$(round(time(); digits = 3))] $(String(message))" + println(console, line) + flush(console) + if verbosity >= level + println(stdout, line) + flush(stdout) + end + return nothing +end + +function _project_diagnostics(project) + parts=String["PSCAD messages:\n$(strip(_messages(project)))"] + try + output=strip(_string(project.output())) + isempty(output) || push!(parts, "PSCAD project output:\n$output") + catch error + push!(parts, "Unable to read PSCAD project output: $(sprint(showerror, error))") + end + return join(parts, '\n') +end + +function _record_diagnostics( + console::IO, + project, + verbosity::Int, + heading::AbstractString; + error_stream::Bool = false +) + text="$(String(heading))\n$(_project_diagnostics(project))" + println(console, text) + flush(console) + if verbosity >= 2 + stream=error_stream ? stderr : stdout + println(stream, text) + flush(stream) + end + return text +end + +function main(arguments) + length(arguments) == 10 || throw(ArgumentError( + "runner expects project, output, project name, output stem, formulation, " * + "FS, FE, Numf, PSCAD version, " * + "and verbosity", + )) + project_path, output, project_name, output_stem, formulation, + fs, fe, numf, pscad_version, + verbosity_text = arguments + verbosity = parse(Int, verbosity_text) + verbosity in 0:2 || throw(ArgumentError("verbosity must be 0, 1, or 2")) + pscad_version == "5.1.0" || throw(ArgumentError("PSCAD 5.1.0 is required")) + occursin(r"^[A-Za-z0-9][A-Za-z0-9_]{0,19}$", output_stem) || + throw(ArgumentError("invalid PSCAD output stem $output_stem")) + mkpath(output) + app = nothing + project = nothing + phase = "initialization" + input_path = joinpath(dirname(project_path), "computation.toml") + input = TOML.parsefile(input_path) + get(input, "schema_version", nothing) == 4 || throw(ArgumentError( + "PSCAD runner requires the complete version-4 numerical input record")) + identify = pyimport("runpy").run_path(joinpath(@__DIR__, "identity.py"))["identify"] + console = open(joinpath(output, "pscad-console.txt"), "w") + try + metadata = pyimport("importlib.metadata") + observed = _string(metadata.version("mhi.pscad")) + observed == MHI_PSCAD_VERSION || throw(ArgumentError( + "mhi.pscad $MHI_PSCAD_VERSION is required; found $observed", + )) + pscad = pyimport("mhi.pscad") + phase = "PSCAD launch" + _report(console, verbosity, 1, "Launching PSCAD 5.1.0") + app = cd(abspath(output)) do + pscad.launch(; + version = pscad_version, + x64 = true, + minimize = true, + splash = false, + silence = true, + timeout = 60 + ) + end + pyconvert(Bool, app.licensed()) || error("PSCAD refused the configured license") + _string(app.version) == pscad_version || + error("PSCAD launched an unexpected version") + initial_identity = pyconvert(Dict{String, String}, identify(pscad_version, app)) + expected_identity = get(input, "expected_solver", nothing) + expected_identity === nothing || initial_identity == expected_identity || + error("PSCAD installation does not match the expected solver identity") + phase = "project load" + _report(console, verbosity, 1, "Loading generated project $project_name") + app.load(abspath(project_path)) + project = app.project(project_name) + lines = vcat( + _components(project.find_all("TLine")), + _components(project.find_all("Cable")) + ) + length(lines) == 1 || throw(ArgumentError( + "generated PSCAD project must expose exactly one line-data row; found $(length(lines))", + )) + line = only(lines) + _set!(line, "Name", output_stem) + _report( + console, + verbosity, + 2, + "Found line-data row $(_string(line.defn_name)); output stem is $output_stem" + ) + canvas = try + project.canvas(last(split(_string(line.defn_name), ':'; limit = 2))) + catch + line.canvas() + end + components = _components(canvas.components()) + phase = "input configuration" + frequency = _single_component(components, "master:line_frephase_options") + ground = _single_component(components, "master:line_ground") + _set!(frequency, "FS", parse(Float64, fs)) + _set!(frequency, "FE", parse(Float64, fe)) + _set!(frequency, "Numf", parse(Int, numf)) + _set!(frequency, "Output", 1; readback = "YES") + readbacks = Dict{String, Any}() + for (name, component) in + (("ground", ground), ("frequency", frequency), ("configuration", line)) + controls = input["native_settings"][name] + observed = Dict{String, Any}() + for (field, setting) in controls + _set!(component, field, setting["value"]; readback = setting["readback"]) + observed[field] = _parameters(component)[field] + end + readbacks[name] = observed + end + open(joinpath(output, "native-settings.toml"), "w") do io + TOML.print(io, readbacks; sorted = true) + end + _report( + console, + verbosity, + 2, + "Applied frequency range $fs Hz to $fe Hz and formulation $formulation" + ) + validate(components) + _report(console, verbosity, 2, "Verified that all cable terminals are retained") + project.save() + _record_diagnostics(console, project, verbosity, "PSCAD diagnostics before computation") + phase = "line-constants computation" + _report(console, verbosity, 1, "Starting PSCAD line-constants computation") + started = time_ns() + line.compile() + elapsed = (time_ns() - started)*1e-9 + _report( + console, + verbosity, + 1, + "PSCAD compile call returned in $(round(elapsed; digits = 3)) seconds" + ) + _record_diagnostics(console, project, verbosity, "PSCAD diagnostics after compile call") + write(joinpath(output, "timing.txt"), string(elapsed)) + roots = [ + abspath(output), + abspath(dirname(project_path)), + abspath(_string(project.temp_folder)) + ] + phase = "detailed-output completion" + expected_rows=parse(Int, numf) + 1 + _report( + console, + verbosity, + 1, + "Waiting for $expected_rows rows in each detailed PSCAD matrix file" + ) + for (suffix, name) in OUTPUT_SUFFIXES + source=_wait_output(roots, suffix, expected_rows) + destination=joinpath(output, name) + cp(source, destination; force = true) + _data_rows(destination) == expected_rows || throw(ArgumentError( + "copied PSCAD output $destination is incomplete", + )) + _report(console, verbosity, 2, "Validated $name with $expected_rows rows") + end + _report(console, verbosity, 1, "Collected detailed PSCAD Z and Y outputs") + observed = pyconvert(Dict{String, String}, identify(pscad_version, app)) + observed == initial_identity || + error("PSCAD installation changed during computation") + open(joinpath(output, "solver.toml"), "w") do io + TOML.print(io, observed; sorted = true) + end + catch error + project === nothing || _record_diagnostics( + console, + project, + verbosity, + "PSCAD diagnostics at failure"; + error_stream = true + ) + message="PSCAD runner failed during $phase: $(sprint(showerror, error))" + println(console, message) + flush(console) + throw(ErrorException(message)) + finally + project === nothing || try + project.unload() + catch error + println(console, "PSCAD project unload failed: ", sprint(showerror, error)) + end + app === nothing || try + app.quit() + catch error + println(console, "PSCAD application close failed: ", sprint(showerror, error)) + end + flush(console) + close(console) + end + return nothing +end + +if abspath(PROGRAM_FILE) == @__FILE__ + try + main(ARGS) + catch error + println(stderr, sprint(showerror, error)) + exit(1) + end +end diff --git a/src/pscad/remote/supervisor.ps1 b/src/pscad/remote/supervisor.ps1 new file mode 100644 index 000000000..339734859 --- /dev/null +++ b/src/pscad/remote/supervisor.ps1 @@ -0,0 +1,313 @@ +param( + [Parameter(Mandatory = $true)] + [string] $SharedCase, + [Parameter(Mandatory = $true)] + [string] $LocalCase, + [Parameter(Mandatory = $true)] + [string] $Julia, + [Parameter(Mandatory = $true)] + [string] $Python, + [Parameter(Mandatory = $true)] + [string] $ProjectName, + [Parameter(Mandatory = $true)] + [string] $OutputStem, + [Parameter(Mandatory = $true)] + [string] $Formulation, + [Parameter(Mandatory = $true)] + [double] $FrequencyStart, + [Parameter(Mandatory = $true)] + [double] $FrequencyEnd, + [Parameter(Mandatory = $true)] + [int] $FrequencyIncrements, + [Parameter(Mandatory = $true)] + [string] $PSCADVersion, + [Parameter(Mandatory = $true)] + [ValidateRange(0, 2)] + [int] $Verbosity, + [Parameter(Mandatory = $true)] + [ValidateRange(1, 2147483647)] + [int] $TimeoutSeconds +) + +$ErrorActionPreference = "Stop" +$ProgressPreference = "SilentlyContinue" + +function Stop-OwnedRunner { + param( + [Parameter(Mandatory = $true)] + [string] $OwnerPath, + [Parameter(Mandatory = $true)] + [string] $OwnedRoot + ) + + if (-not (Test-Path -LiteralPath $OwnerPath -PathType Leaf)) { + return + } + + $owner = @(Get-Content -LiteralPath $OwnerPath) + if ($owner.Count -ne 2) { + throw "Invalid PSCAD runner owner file: $OwnerPath" + } + + $processId = 0 + if (-not [int]::TryParse($owner[0], [ref] $processId)) { + throw "Invalid PSCAD runner PID in $OwnerPath" + } + + $runner = [IO.Path]::GetFullPath($owner[1]) + $root = [IO.Path]::GetFullPath($OwnedRoot).TrimEnd('\') + '\' + if (-not $runner.StartsWith($root, [StringComparison]::OrdinalIgnoreCase)) { + throw "Refusing to stop a process outside the PSCAD case directory: $runner" + } + + $process = Get-CimInstance Win32_Process -Filter "ProcessId = $processId" + if ($null -eq $process) { + Remove-Item -LiteralPath $OwnerPath -Force + return + } + + if ($null -eq $process.CommandLine -or + $process.CommandLine.IndexOf($runner, [StringComparison]::OrdinalIgnoreCase) -lt 0) { + throw "PID $processId no longer belongs to the recorded PSCAD runner" + } + + & taskkill.exe /PID $processId /T /F | Out-Null + if ($LASTEXITCODE -ne 0) { + throw "Could not stop PSCAD runner process tree $processId" + } + Remove-Item -LiteralPath $OwnerPath -Force -ErrorAction SilentlyContinue +} + +function Remove-DirectoryWithRetry { + param( + [Parameter(Mandatory = $true)] + [string] $Path, + [int] $Attempts = 30, + [int] $DelayMilliseconds = 500, + [switch] $BestEffort + ) + + for ($attempt = 1; $attempt -le $Attempts; $attempt++) { + if (-not (Test-Path -LiteralPath $Path)) { + return + } + try { + Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction Stop + return + } catch { + if ($attempt -eq $Attempts) { + if ($BestEffort) { + return + } + throw + } + Start-Sleep -Milliseconds $DelayMilliseconds + } + } +} + +function Copy-Result { + param( + [Parameter(Mandatory = $true)] + [string] $Source, + [Parameter(Mandatory = $true)] + [string] $Destination + ) + + New-Item -ItemType Directory -Path $Destination -Force | Out-Null + if (Test-Path -LiteralPath $Source -PathType Container) { + Get-ChildItem -LiteralPath $Source -Force | + Copy-Item -Destination $Destination -Recurse -Force + } +} + +function Write-NewLines { + param( + [Parameter(Mandatory = $true)] + [string] $Path, + [Parameter(Mandatory = $true)] + [ref] $Count, + [Parameter(Mandatory = $true)] + [bool] $ErrorStream + ) + + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { + return + } + $lines = @(Get-Content -LiteralPath $Path) + for ($index = $Count.Value; $index -lt $lines.Count; $index++) { + if ($ErrorStream) { + [Console]::Error.WriteLine($lines[$index]) + } else { + [Console]::Out.WriteLine($lines[$index]) + } + } + $Count.Value = $lines.Count +} + +if (-not (Test-Path -LiteralPath $SharedCase -PathType Container)) { + throw "PSCAD exchange directory is unavailable: $SharedCase" +} +if (-not (Test-Path -LiteralPath $Julia -PathType Leaf)) { + throw "Julia executable is unavailable: $Julia" +} +if (-not (Test-Path -LiteralPath $Python -PathType Leaf)) { + throw "Python executable is unavailable: $Python" +} + +$sharedFull = [IO.Path]::GetFullPath($SharedCase).TrimEnd('\') + '\' +$localFull = [IO.Path]::GetFullPath($LocalCase).TrimEnd('\') + '\' +if ($sharedFull.StartsWith($localFull, [StringComparison]::OrdinalIgnoreCase) -or + $localFull.StartsWith($sharedFull, [StringComparison]::OrdinalIgnoreCase)) { + throw "PSCAD exchange and scratch directories must not overlap" +} +$ownerPath = Join-Path $LocalCase "owner.txt" +Stop-OwnedRunner -OwnerPath $ownerPath -OwnedRoot $LocalCase +if (Test-Path -LiteralPath $LocalCase) { + Remove-DirectoryWithRetry -Path $LocalCase +} +New-Item -ItemType Directory -Path $LocalCase -Force | Out-Null +Get-ChildItem -LiteralPath $SharedCase -Force | + Where-Object Name -ne "outputs" | + Copy-Item -Destination $LocalCase -Recurse -Force + +$toolkit = Join-Path $LocalCase "toolkit" +$runner = Join-Path $toolkit "runner.jl" +$project = Join-Path $toolkit "Project.toml" +$projectFile = Join-Path $LocalCase "generated.pscx" +$result = Join-Path $LocalCase "result" +$sharedOutput = Join-Path $SharedCase "outputs" +$stdoutPath = Join-Path $result "stdout.txt" +$stderrPath = Join-Path $result "stderr.txt" +$exitPath = Join-Path $result "exit-code.txt" +$invokePath = Join-Path $LocalCase "invoke-runner.cmd" + +foreach ($required in @($runner, $project, $projectFile)) { + if (-not (Test-Path -LiteralPath $required -PathType Leaf)) { + throw "Staged PSCAD input is missing: $required" + } +} + +New-Item -ItemType Directory -Path $result -Force | Out-Null +New-Item -ItemType Directory -Path $sharedOutput -Force | Out-Null +$commandValues = @( + $Julia, + $Python, + $toolkit, + $runner, + $projectFile, + $result, + $ProjectName, + $OutputStem, + $Formulation, + $stdoutPath, + $stderrPath, + $exitPath +) +foreach ($value in $commandValues) { + if ($value.IndexOfAny(@([char] '"', [char] '%', [char] 10, [char] 13)) -ge 0) { + throw "PSCAD runner command value contains an unsupported character: $value" + } +} +$runnerArguments = @( + "`"--project=$toolkit`"", + "--startup-file=no", + "`"$runner`"", + "`"$projectFile`"", + "`"$result`"", + "`"$ProjectName`"", + "`"$OutputStem`"", + "`"$Formulation`"", + "`"$FrequencyStart`"", + "`"$FrequencyEnd`"", + "`"$FrequencyIncrements`"", + "`"$PSCADVersion`"", + "`"$Verbosity`"" +) +$invokeLines = @( + "@echo off", + "setlocal", + "set `"JULIA_PYTHONCALL_EXE=$Python`"", + "`"$Julia`" $($runnerArguments -join ' ') 1>`"$stdoutPath`" 2>`"$stderrPath`"", + "set `"PSCAD_EXIT=%ERRORLEVEL%`"", + ">`"$exitPath`" echo %PSCAD_EXIT%", + "exit /b %PSCAD_EXIT%" +) +[IO.File]::WriteAllLines($invokePath, $invokeLines, [Text.Encoding]::ASCII) + +$process = $null +$exitCode = 1 +$timedOut = $false +$stdoutCount = 0 +$stderrCount = 0 +try { + $process = Start-Process ` + -FilePath $env:ComSpec ` + -ArgumentList @("/d", "/s", "/c", "`"$invokePath`"") ` + -PassThru ` + -WindowStyle Hidden + Set-Content -LiteralPath $ownerPath -Value @($process.Id, $invokePath) + if ($Verbosity -ge 2) { + [Console]::Out.WriteLine("Started PSCAD runner process $($process.Id)") + } + + $stopwatch = [Diagnostics.Stopwatch]::StartNew() + while (-not $process.HasExited) { + if ($Verbosity -ge 2) { + Write-NewLines -Path $stdoutPath -Count ([ref] $stdoutCount) -ErrorStream $false + Write-NewLines -Path $stderrPath -Count ([ref] $stderrCount) -ErrorStream $true + } + if ($stopwatch.Elapsed.TotalSeconds -ge $TimeoutSeconds) { + $timedOut = $true + break + } + Start-Sleep -Milliseconds 500 + $process.Refresh() + } + + if ($timedOut) { + [Console]::Error.WriteLine( + "PSCAD runner exceeded the configured timeout of $TimeoutSeconds seconds" + ) + Stop-OwnedRunner -OwnerPath $ownerPath -OwnedRoot $LocalCase + $exitCode = 124 + } else { + $process.WaitForExit() + if (Test-Path -LiteralPath $exitPath -PathType Leaf) { + $recordedExit = (Get-Content -LiteralPath $exitPath -Raw).Trim() + if (-not [int]::TryParse($recordedExit, [ref] $exitCode)) { + [Console]::Error.WriteLine( + "PSCAD runner wrote an invalid exit code: $recordedExit" + ) + $exitCode = 1 + } + } else { + [Console]::Error.WriteLine("PSCAD runner did not write an exit code") + $exitCode = 1 + } + if ($Verbosity -ge 2) { + [Console]::Out.WriteLine( + "PSCAD runner process $($process.Id) exited with code $exitCode" + ) + } + } + + if ($Verbosity -ge 2) { + Write-NewLines -Path $stdoutPath -Count ([ref] $stdoutCount) -ErrorStream $false + Write-NewLines -Path $stderrPath -Count ([ref] $stderrCount) -ErrorStream $true + } +} catch { + [Console]::Error.WriteLine($_.Exception.ToString()) + if ($null -ne $process -and -not $process.HasExited) { + Stop-OwnedRunner -OwnerPath $ownerPath -OwnedRoot $LocalCase + } + $exitCode = 1 +} finally { + Copy-Result -Source $result -Destination $sharedOutput + Remove-Item -LiteralPath $ownerPath -Force -ErrorAction SilentlyContinue +} + +if ($exitCode -eq 0) { + Remove-DirectoryWithRetry -Path $LocalCase -BestEffort +} +exit $exitCode diff --git a/src/pscad/results.jl b/src/pscad/results.jl new file mode 100644 index 000000000..03c42bf2a --- /dev/null +++ b/src/pscad/results.jl @@ -0,0 +1,122 @@ +struct DetailedTable + frequency::Vector{Float64} + values::Matrix{Float64} +end + +const PSCAD_FREQUENCY_RTOL = 5.0e-8 + +function _pscad_number(text::AbstractString) + value = replace(strip(text), 'D' => 'E', 'd' => 'e') + value = replace(value, r"^([+-])\." => s"\g<1>0.") + value = replace(value, r"^\." => "0.") + value = replace( + value, + r"^([+-]?(?:\d+(?:\.\d*)?|\.\d+))([+-]\d+)$" => s"\g<1>E\g<2>" + ) + parsed = tryparse(Float64, value) + parsed === nothing && throw(ArgumentError("invalid PSCAD numeric field $(repr(text))")) + return parsed +end + +function _float_row(line::AbstractString) + return _pscad_number.(split(strip(line))) +end + +function _read_detailed(path::AbstractString) + lines = collect(eachline(path)) + header = findfirst(line -> occursin("LOG10(FN)", line), lines) + header === nothing && throw(ArgumentError( + "missing PSCAD detailed-output header in $path", + )) + rows = [_float_row(line) + for line in @view(lines[(header + 1):end]) if !isempty(strip(line))] + isempty(rows) && throw(ArgumentError("PSCAD detailed output contains no data: $path")) + width = length(first(rows)) + width >= 3 || throw(DimensionMismatch("PSCAD detailed output needs data columns")) + all(row -> length(row) == width, rows) || throw(DimensionMismatch( + "PSCAD detailed output has inconsistent row widths: $path", + )) + table = reduce(vcat, permutedims.(rows)) + all(isfinite, table) || throw(DomainError( + path, + "PSCAD detailed output contains nonfinite data" + )) + return DetailedTable(table[:, 2], table[:, 3:end]) +end + +function _combine_polar(magnitude::DetailedTable, phase::DetailedTable) + magnitude.frequency == phase.frequency || throw(ArgumentError( + "PSCAD magnitude and phase frequencies differ", + )) + size(magnitude.values) == size(phase.values) || throw(DimensionMismatch( + "PSCAD magnitude and phase table shapes differ", + )) + flat = magnitude.values .* cispi.(phase.values ./ 180) + dimension = isqrt(size(flat, 2)) + dimension^2 == size(flat, 2) || throw(DimensionMismatch( + "PSCAD detailed table does not contain a square matrix", + )) + result = Array{ComplexF64, 3}(undef, dimension, dimension, size(flat, 1)) + for frequency in axes(flat, 1), row in 1:dimension, column in 1:dimension + result[row, column, frequency] = flat[frequency, (row - 1) * dimension + column] + end + return result +end + +function _result_path(directory::AbstractString, name::AbstractString) + path = joinpath(directory, name) + isfile(path) || throw(ArgumentError("required PSCAD result is missing: $path")) + return path +end + +function _case_frequencies(observed::AbstractVector, expected::AbstractVector) + length(observed) == length(expected) || throw(DimensionMismatch( + "PSCAD emitted $(length(observed)) frequencies; expected $(length(expected))", + )) + for index in eachindex(observed, expected) + isapprox( + observed[index], expected[index]; + rtol = max(PSCAD_FREQUENCY_RTOL, 8Float64(eps(float(one(eltype(expected)))))), + atol = 0.0 + ) || throw(ArgumentError( + "PSCAD frequency at row $index is $(observed[index]); " * + "expected $(expected[index]) within the PSCAD text-output resolution", + )) + end + return Float64.(expected) +end + +function read_pscad_result( + output_dir::AbstractString, + expected_frequencies::AbstractVector, + expected_size::NTuple{3, Int} +) + isdir(output_dir) || + throw(ArgumentError("PSCAD output directory is missing: $output_dir")) + zm = _read_detailed(_result_path(output_dir, "result_zm.out")) + zp = _read_detailed(_result_path(output_dir, "result_zp.out")) + ym = _read_detailed(_result_path(output_dir, "result_ym.out")) + yp = _read_detailed(_result_path(output_dir, "result_yp.out")) + observed_frequencies = zm.frequency + zp.frequency == observed_frequencies && ym.frequency == observed_frequencies && + yp.frequency == observed_frequencies || throw(ArgumentError( + "PSCAD detailed outputs use different frequency samples", + )) + frequencies_value = _case_frequencies(observed_frequencies, expected_frequencies) + impedance = _combine_polar(zm, zp) + admittance = _combine_polar(ym, yp) + size(impedance) == expected_size || throw(DimensionMismatch( + "PSCAD impedance has size $(size(impedance)); expected $expected_size", + )) + size(admittance) == expected_size || throw(DimensionMismatch( + "PSCAD admittance has size $(size(admittance)); expected $expected_size", + )) + return LineParameters( + PhaseDomain, + impedance, + admittance, + frequencies_value; + basis = :pul, + details = ComputationDetails(; native_frequencies = copy(observed_frequencies)) + ) +end diff --git a/src/pscad/validate.jl b/src/pscad/validate.jl new file mode 100644 index 000000000..3b148096c --- /dev/null +++ b/src/pscad/validate.jl @@ -0,0 +1,54 @@ +""" +$(TYPEDSIGNATURES) + +Check the physical earth inventory accepted by PSCAD model export and execution. +The supported model consists of air and one infinite horizontal soil half-space. +""" +function validate(model::EarthModel, ::Type{<:PSCADFormulation}) + _pscad_deterministic(eltype(model)) + validate(model) + !model.vertical_layers && length(model.layers) == 2 && + all(layer -> isinf(layer.thickness), model.layers) || throw(ArgumentError( + "PSCAD supports physical air and one homogeneous horizontal soil half-space")) + return model +end + +function _pscad_deterministic(types::Type...) + any(Engine.has_uncertainty_type, types) && throw(ArgumentError( + "PSCAD does not support Measurement values. Supply deterministic inputs.")) + return nothing +end + +function _pscad_blueprints(system::LineCableSystem) + _pscad_deterministic(eltype(system)) + T = eltype(system) + return Engine.CableBlueprint{T}[ + Engine.flatten(LineCableModelsCoaxial(), design, T) for design in system.designs] +end + +# Computation frequencies of the PSCAD phase-scan adapter. +function validate(values::AbstractVector, ::Type{<:PSCADFormulation}) + _pscad_deterministic(eltype(values)) + isempty(values) && throw(ArgumentError("PSCAD requires computation frequencies")) + invalid = findall(value -> !isfinite(value) || value < 0.1, values) + isempty(invalid) || throw(DomainError(values[invalid], + "PSCAD computation frequencies must be finite and at least 0.1 Hz; invalid indices: $invalid")) + issorted(values) && allunique(values) || throw(ArgumentError( + "PSCAD frequencies must be strictly increasing")) + length(values) - 1 in (100, 200, 500, 1000) || throw(ArgumentError( + "PSCAD phase-scan adapter supports 101, 201, 501 or 1001 logarithmic samples; requested $(length(values))")) + expected = 10.0 .^ range(log10(Float64(first(values))), + log10(Float64(last(values))); length = length(values)) + tolerance = max(32eps(Float64), 8Float64(eps(float(one(eltype(values)))))) + all(isapprox(a, b; rtol = tolerance, atol = 0) for (a, b) in zip(values, expected)) || + throw(ArgumentError("PSCAD phase-scan adapter requires logarithmic samples; arbitrary-grid execution is not implemented")) + return values +end + +function validate(problem::LineParametersProblem, formulation::PSCADFormulation) + _pscad_deterministic(eltype(problem), typeof(formulation.options.data.base_frequency)) + validate(problem.frequencies, PSCADFormulation) + _pscad_size(problem) + _pscad_inputs(problem, formulation, _pscad_blueprints(problem.system)) + return problem +end diff --git a/src/reportbuilder/ReportBuilder.jl b/src/reportbuilder/ReportBuilder.jl new file mode 100644 index 000000000..361cc648d --- /dev/null +++ b/src/reportbuilder/ReportBuilder.jl @@ -0,0 +1,43 @@ +""" + LineCableModels.ReportBuilder + +Build human-facing tables and optional plot artifacts from published scientific +observations. +""" +module ReportBuilder + +export AbstractReportDefinition, ReportArtifact +export TableReportDefinition, CableConstantsTableDefinition +export LineParametersTableDefinition, BenchmarkTableDefinition +export MonteCarloTableDefinition, XLSXReportDefinition, report +export select, tabulate, illustrate, encode, write + +using DocStringExtensions: TYPEDEF, TYPEDFIELDS, TYPEDSIGNATURES +import Statistics +import DataFrames +import DataFrames: DataFrame, metadata, metadata!, Not +import ..Commons: observables +import ..Commons: ObservedResult +import ..Commons +import ..Units +import ..DataModel +import ..Engine +import ..Commons: AbstractUncertaintyResult, request_identity, request_quantity, request_indices +import ..LineCableModels: validate, description +import ..LineCableModels +import ..PlotBuilder +import ..UQ +import ..TextDisplay +import ..LineCableModels: Z, Y, R, X, L, G, B, C + +include("grammar.jl") +include("tables.jl") +include("comparisons.jl") +include("montecarlo.jl") +include("performance.jl") +include("xlsx.jl") +include("textdisplay.jl") + +public observation_columns, encode_cell, XLSXSheet, XLSXWorkbook + +end diff --git a/src/reportbuilder/comparisons.jl b/src/reportbuilder/comparisons.jl new file mode 100644 index 000000000..cd70ffa01 --- /dev/null +++ b/src/reportbuilder/comparisons.jl @@ -0,0 +1,173 @@ +function _selected_errors(definition,point,reference) + rows=point.errors + isempty(rows) && throw(ArgumentError("this observation has no completed comparisons")) + if reference!==nothing + rows=filter(row -> row.reference_id==reference.gridpoint.id,rows) + end + settings=definition.settings + for key in definition.requested + if key===:requests + rows=filter(row -> row.request in settings.requests,rows) + elseif key===:bands + rows=filter(row -> row.band in settings.bands,rows) + elseif key===:normalizations + rows=filter(row -> row.normalization in settings.normalizations,rows) + elseif key!==:pairing + all(row -> isequal(get(row.settings,key===:atol ? :requested_atol : key,nothing),getproperty(settings,key)),rows) || + throw(ArgumentError("requested $key differs from the retained comparison; run compare explicitly")) + end + end + isempty(rows) && throw(ArgumentError("requested comparison was not retained")) + requests=:requests in definition.requested ? settings.requests : unique(row.request for row in rows) + bands=:bands in definition.requested ? settings.bands : unique(row.band for row in rows) + normalizations=:normalizations in definition.requested ? settings.normalizations : unique(row.normalization for row in rows) + for request in requests,band in bands,normalization in normalizations + any(row -> row.request==request && isequal(row.band,band) && row.normalization==normalization,rows) || + throw(ArgumentError("some requested comparison products were not retained")) + end + return rows +end + +function _point_information(points,role,labels=Commons.observation_labels(points)) + computations=NamedTuple[] + formulations=NamedTuple[] + formula_details=NamedTuple[] + for (index,point) in enumerate(points) + id=point.gridpoint.id + push!(computations,(role,identity=id,label=labels[index],physical_inputs=point.gridpoint.inputs)) + push!(formulations,(role,identity=id,label=labels[index],selections=point.gridpoint.formulations)) + for (key,value) in pairs(something(point.gridpoint.formulations,(;))) + push!(formula_details,(role,identity=id,selection=key,value)) + end + end + return (;computations,formulations,formula_details) +end + +""" +$(TYPEDSIGNATURES) + +Tabulate completed comparisons, independent maxima, sampling information, and +recorded performance evidence. Every result remains represented. Display +features consume the same observation-side groups as plots. No source access, +comparison, sampling estimate, or timing measurement occurs here. +""" +select(definition::BenchmarkTableDefinition,observed::ObservedResult;reference=nothing) = + _selected_errors(definition,observed,reference) + +function tabulate(definition::BenchmarkTableDefinition, + observed::Union{ObservedResult,AbstractVector{<:ObservedResult}},selected;reference=nothing) + points=collect(_observed_points(observed)) + population=reference===nothing ? points : [points;reference] + labels=Commons.observation_labels(population) + rows=NamedTuple[];terms=NamedTuple[];maxima=NamedTuple[] + selected_errors=observed isa ObservedResult ? (selected,) : selected + for (index,point) in enumerate(points),error in selected_errors[index] + id=point.gridpoint.id + settings=error.settings + identity=(result_id=error.result_id,reference_id=error.reference_id, + problem_index=id.problem_index,formulation_index=id.formulation_index, + result_point=index,method=labels[index],request=error.request, + quantity=Symbol(Units.symbol(error.quantity)),statistic=error.statistic, + band=error.band,normalization=error.normalization, + absolute_unit=Units.label(error.absolute_unit),samples=settings.sample_count, + requested_bounds_Hz=get(settings,:requested_bounds,nothing),actual_bounds_Hz=settings.actual_bounds) + absolute=error.absolute + relative=error.relative.*100 + status=get(settings,:status,fill(:not_recorded,size(absolute))) + reasons=get(settings,:normalization_reason,fill(get(settings,:reason,nothing),size(absolute))) + push!(rows,merge(identity,(absolute_rms=Commons.detach(absolute),relative_rms_percent=relative, + status=Commons.detach(status),reason=Commons.detach(reasons),settings))) + for i in axes(absolute,1),j in axes(absolute,2) + push!(terms,merge(identity,(row=i,column=j,response=error.coordinates[i],excitation=error.coordinates[j], + absolute_rms=absolute[i,j],relative_rms_percent=relative[i,j],status=status[i,j],reason=reasons[i,j]))) + end + push!(maxima,merge(identity,(maximum_absolute_rms=error.maxima.absolute.value, + absolute_term=error.maxima.absolute.index,maximum_relative_rms_percent=error.maxima.relative.value*100, + relative_term=error.maxima.relative.index, + absolute_term_response=ismissing(error.maxima.absolute.index) ? missing : error.coordinates[error.maxima.absolute.index[1]], + absolute_term_excitation=ismissing(error.maxima.absolute.index) ? missing : error.coordinates[error.maxima.absolute.index[2]], + relative_term_response=ismissing(error.maxima.relative.index) ? missing : error.coordinates[error.maxima.relative.index[1]], + relative_term_excitation=ismissing(error.maxima.relative.index) ? missing : error.coordinates[error.maxima.relative.index[2]], + unavailable=count(ismissing,relative), + term_count=length(relative),compared=count(!ismissing,relative), + reasons=unique(filter(!isnothing,vec(reasons)))))) + end + features=NamedTuple[] + groups=NamedTuple[] + partitions=unique((row.request,row.normalization,row.reference_id,row.problem_index) for row in rows) + for (request,normalization,reference_id,problem_index) in partitions + selected=filter(row -> row.request==request && row.normalization==normalization && row.reference_id==reference_id && row.problem_index==problem_index,maxima) + bands=unique(row.band for row in selected) + active=unique(row.result_point for row in selected) + display_groups=[Commons.observation_groups(points[active];request,band,normalization,reference=reference_id) for band in bands] + representatives=sort(unique(vcat(([active[group.representative] for group in entries] for entries in display_groups)...))) + push!(groups,(request,normalization,reference_id,bands,groups=display_groups,points=active)) + quantity_labels=Commons.observation_labels(population;request) + absolute=DataFrame(formula=quantity_labels[representatives]);relative=copy(absolute) + for band in bands + name=band isa Symbol ? band : Symbol(string(band)) + for (frame,field) in ((absolute,:maximum_absolute_rms),(relative,:maximum_relative_rms_percent)) + frame[!,name]=[begin + matches=filter(row -> row.result_point==index && isequal(row.band,band),selected) + isempty(matches) ? missing : getproperty(only(matches),field) + end for index in representatives] + end + end + push!(features,(request,quantity=first(selected).quantity,statistic=first(selected).statistic, + normalization,reference_id,problem_index,absolute_unit=first(selected).absolute_unit,absolute,relative)) + end + result_info=_point_information(points,:result,labels) + reference_info=reference===nothing ? (computations=NamedTuple[],formulations=NamedTuple[],formula_details=NamedTuple[]) : + _point_information([reference],:reference,[last(labels)]) + info=map((a,b) -> DataFrame(vcat(a,b)),result_info,reference_info) + timing=_timing_tables(points,reference) + sampling=_sampling_tables(points,reference) + coverage=DataFrame([(result_source=string(row.result_id.source_id),reference_source=string(row.reference_id.source_id), + point=row.problem_index,formulation_index=row.formulation_index,band=row.band isa Tuple ? string(row.band) : row.band, + frequency_count=row.samples,first_Hz=row.actual_bounds_Hz===nothing ? missing : Commons.nominal(first(row.actual_bounds_Hz)), + last_Hz=row.actual_bounds_Hz===nothing ? missing : Commons.nominal(last(row.actual_bounds_Hz))) for row in maxima]) + coverage=unique(coverage) + overview=(coverage,execution=timing.execution,performance=timing.performance, + timing_ratio=timing.performance_comparison,source_timings=timing.source_timings, + sampling=sampling.sampling,cdf_precision=isempty(sampling.sampling) ? DataFrame() : DataFrames.select(sampling.sampling, + :role,:point,:method,:trials,:confidence,:marginal_count,:target_cdf,:cdf_bound,:target_supported,:scope)) + maxima_table=DataFrame(maxima) + metadata!(maxima_table,"comparison_records",Commons.detach(maxima);style=:note) + return merge(info,(quantities=map(tabulate,points),comparisons=DataFrame(rows),terms=DataFrame(terms), + maxima=maxima_table,summary=copy(maxima_table),features,groups),timing,sampling,(;overview)) +end + +function illustrate(definition::BenchmarkTableDefinition,observed,tables;reference=nothing) + illustration=definition.illustration + (illustration===nothing || illustration===false) && return nothing + callable=illustration===true ? PlotBuilder.plot : illustration + return callable(observed;reference,definition.plot_options...) +end + +""" +$(TYPEDSIGNATURES) + +Convenience for raw benchmark operands. Explicit comparison completes before +observation construction. Already completed products supplied as `comparisons` +are joined by original identities. The reference remains a separate observation. +""" +function report(definition::BenchmarkTableDefinition,source::NamedTuple;requests::Tuple=(),observation_options::NamedTuple=(;)) + all(key -> haskey(source,key),(:reference,:result)) || throw(ArgumentError("benchmark operands require reference and result")) + reference=source.reference isa NamedTuple && haskey(source.reference,:result) ? source.reference.result : source.reference + result=source.result isa NamedTuple && haskey(source.result,:result) ? source.result.result : source.result + result isa Union{ObservedResult,AbstractVector{<:ObservedResult}} && return report(definition,result;reference) + settings=definition.settings + completed=haskey(source,:comparisons) ? source.comparisons : Engine.compare(reference,result,collect(settings.requests); + bands=settings.bands,normalizations=settings.normalizations,pairing=settings.pairing, + atol=settings.atol,fundamental=settings.fundamental,harmonics=settings.harmonics,unsupported=settings.unsupported) + timings=(measurements=get(source,:measurements,nothing),context=get(source,:context,(;))) + result isa AbstractUncertaintyResult && isempty(requests) && (requests=settings.requests) + observed=observables(result,requests;comparisons=completed,timings,clip=definition.clip,atol=settings.atol,complete_pairs=true,observation_options...) + observed_reference=reference isa AbstractUncertaintyResult ? + (length(reference)==1 ? ObservedResult(reference,1,requests;clip=definition.clip,atol=settings.atol,complete_pairs=true,observation_options...) : + throw(ArgumentError("a benchmark report retains one separate reference point"))) : + ObservedResult(reference,requests;clip=definition.clip,atol=settings.atol,complete_pairs=true,observation_options...) + return report(definition,observed;reference=observed_reference) +end + +description(::BenchmarkTableDefinition,band) = band isa Symbol ? string(band) : string(first(band),"–",last(band)," Hz") diff --git a/src/reportbuilder/grammar.jl b/src/reportbuilder/grammar.jl new file mode 100644 index 000000000..a4405e3d6 --- /dev/null +++ b/src/reportbuilder/grammar.jl @@ -0,0 +1,376 @@ +""" +$(TYPEDEF) + +Supertype for definitions consumed by [`report`](@ref). +""" +abstract type AbstractReportDefinition end + +""" +$(TYPEDEF) + +Retain the observed inputs, separate observed reference, tables, illustration, +and written destinations of one completed report. + +Plain and HTML display show the completed quantity tables. `artifact[R]` +retrieves the reported resistance DataFrame, or an ordered vector of DataFrames +for a collection. `artifact[i, R]` retrieves the table for result position `i`. +Lookup and display use completed tables without further observation or computation. + +$(TYPEDFIELDS) +""" +struct ReportArtifact{T,I,O} + "One atomic observation or an ordinary vector of observations." + observed::Union{ObservedResult,Vector{ObservedResult}} + "Separate observed reference, when supplied." + reference::Union{Nothing,ObservedResult} + "Quantity-wise tables and other retained scientific summaries." + tables::T + "Optional rendered illustration." + illustration::I + "Written destinations, or nothing for an in-memory report." + output::O +end + +# Only ordinary report containers are traversed. DataFrame cells are leaves. +_reported_leaves(table::DataFrame) = (table,) +_reported_leaves(tables::Union{NamedTuple,Tuple,AbstractVector}) = + Iterators.flatten(_reported_leaves(child) for child in tables) +_reported_leaves(_) = () + +function _reported_tables(artifact::ReportArtifact) + output=artifact.tables + benchmark=output isa NamedTuple && haskey(output,:features) + benchmark && (output=output.quantities) + collection=artifact.observed isa AbstractVector + # A specialized definition may return one aggregate table. Its display is + # still native, but a collection does not associate it with one gridpoint. + output isa DataFrame && return [(point_index=collection ? nothing : 1,tables=[output])] + return map(eachindex(_observed_points(artifact.observed))) do index + tables=benchmark || collection ? output[index] : output + (point_index=index,tables=collect(DataFrame,_reported_leaves(tables))) + end +end + +function _reported_table(artifact,groups,request,index) + identity=Commons.normalize_observation_selector(request_identity(request)) + indices=request_indices(request) + tables=collect(DataFrame,Iterators.flatten(group.tables for group in groups + if group.point_index==index)) + matches=filter(tables) do table + retained=metadata(table,"request",nothing) + retained===nothing && return false + isequal(Commons.normalize_observation_selector(request_identity(retained)),identity) && + (isempty(indices) || isequal(request_indices(retained),indices)) + end + length(matches)==1 && return only(matches) + unassociated=any(group -> group.point_index===nothing,groups) + available_tables=unassociated ? Iterators.flatten(group.tables for group in groups) : tables + available=join((repr(metadata(table,"request",nothing)) for table in available_tables),", ") + problem=isempty(matches) ? "absent" : "ambiguous" + unassociated && (problem="not associated with a gridpoint") + id=_observed_points(artifact.observed)[index].gridpoint.id + location="reported result $index"*(id===nothing ? "" : " (gridpoint $(repr(id)))") + throw(ArgumentError("reported request $(repr(request)) is $problem for $location; " * + "available reported requests: [$available] (nothing means absent quantity descriptors). " * + "Use report(...; values=...) for a different selection.")) +end + +""" +$(TYPEDSIGNATURES) + +Retrieve an already-produced quantity DataFrame using an observation request. +For an atomic report, the result is one DataFrame. Collections return an ordered vector, +including for one result. `artifact[i, request]` returns the table for result +position `i`, independently of its recorded scientific gridpoint identifier. + +An unindexed request returns the product with its reported selection intact. +An indexed request must match that original selection exactly. Complete +transformation and statistical identities remain distinct. + +# Arguments + +- `artifact`: a completed report. +- `request`: a quantity selector or complete observation request. + +# Returns + +The stored DataFrame itself, or a vector of those DataFrames. Use `copy` for an +independent table. Editing a returned table does not edit retained observations. + +# Errors + +Absent or ambiguous products, missing descriptors, and Boolean point indices +raise `ArgumentError`. Invalid result positions raise `BoundsError`. + +# Examples + +```julia +resistance = constants_report[R] +resistance_tables = collection_report[R] +second_resistance = collection_report[2, R] +``` +""" +function Base.getindex(artifact::ReportArtifact,request) + groups=_reported_tables(artifact) + tables=map(eachindex(_observed_points(artifact.observed))) do index + _reported_table(artifact,groups,request,index) + end + return artifact.observed isa ObservedResult ? only(tables) : tables +end + +function Base.getindex(artifact::ReportArtifact,index::Integer,request) + index isa Bool && throw(ArgumentError("result position must be an integer, not Bool")) + points=_observed_points(artifact.observed) + index in eachindex(points) || throw(BoundsError(artifact,(index,request))) + return _reported_table(artifact,_reported_tables(artifact),request,index) +end + +""" +$(TYPEDEF) + +Select retained quantities for separate tables. Raw-input conveniences first +construct [`ObservedResult`](@ref). Illustrations consume those same observations. + +$(TYPEDFIELDS) +""" +struct TableReportDefinition{R<:Tuple,U<:Tuple,P,O<:NamedTuple} <: AbstractReportDefinition + "Quantity requests. An empty tuple selects all retained quantities." + requests::R + "Display units used only when constructing a raw-input observation." + units::U + "True, a plotting callable, or nothing." + illustration::P + "Options passed to the illustration call." + plot_options::O + "Set unresolved observed values to exact zero, including zero uncertainty." + clip::Bool +end +TableReportDefinition(requests::Tuple=();units::Tuple=(),illustration=nothing, + plot_options::NamedTuple=(;),clip::Bool=true) = + TableReportDefinition(requests,units,illustration,plot_options,clip) + +"""Select retained products without extracting or recomputing numerical values.""" +function select end +"""Build quantity-wise tables from detached observations.""" +function tabulate end +"""Render an optional illustration from detached observations.""" +function illustrate end +"""Encode observed tables for a requested output format.""" +function encode end +"""Write already encoded report output.""" +function write end + +_observed_points(observed::ObservedResult) = (observed,) +_observed_points(observed::AbstractVector{<:ObservedResult}) = observed + +select(definition::AbstractReportDefinition,observed::AbstractVector{<:ObservedResult};reference=nothing) = + map(point -> select(definition,point;reference),observed) + +function select(definition::TableReportDefinition,observed::ObservedResult;reference=nothing) + isempty(definition.requests) && return observed.quantities + return [Commons.observation_product(observed,request) + for request in Commons.observation_requests(observed,definition.requests).retained] +end + +tabulate(definition::AbstractReportDefinition,observed;reference=nothing) = + tabulate(definition,observed,select(definition,observed;reference);reference) + +illustrate(::AbstractReportDefinition,observed,tables;reference=nothing) = nothing +encode(::AbstractReportDefinition,observed,tables,illustration;reference=nothing) = nothing +write(::AbstractReportDefinition,::Nothing) = nothing + +function illustrate(definition::TableReportDefinition,observed,tables;reference=nothing) + illustration=definition.illustration + (illustration===nothing || illustration===false) && return nothing + options=merge(definition.plot_options,isempty(definition.requests) ? (;) : (ydata=definition.requests,)) + callable=illustration===true ? PlotBuilder.plot : illustration + return reference===nothing ? callable(observed;options...) : callable(observed;reference,options...) +end + +""" +$(TYPEDSIGNATURES) + +Select retained products, build tables, render an optional illustration, encode +output, and write artifacts, in that order. Only observations enter these report +stages. `reference` is an atomic +observation kept outside the reported-result collection. +""" +function report(definition::AbstractReportDefinition, + observed::Union{ObservedResult,AbstractVector{<:ObservedResult}}; + reference::Union{Nothing,ObservedResult}=nothing) + selected=select(definition,observed;reference) + tables=tabulate(definition,observed,selected;reference) + illustration=illustrate(definition,observed,tables;reference) + encoded=encode(definition,observed,tables,illustration;reference) + written=write(definition,encoded) + points=observed isa ObservedResult ? observed : collect(ObservedResult,observed) + return ReportArtifact(points,reference,tables,illustration,written) +end + +function report(definition::TableReportDefinition,source;kwargs...) + return report(source;values=definition.requests,units=definition.units,clip=definition.clip, + illustration=definition.illustration,plot_options=definition.plot_options,kwargs...) +end + +"""Return the quantity and unit metadata of an observed table's columns.""" +observation_columns(table::DataFrame) = metadata(table,"observation_columns") + +# Resolve the Julia dispatch intersection between a generic raw convenience and +# the common observed workflow without adding a second execution path. +function report(definition::TableReportDefinition, + observed::Union{ObservedResult,AbstractVector{<:ObservedResult}};reference=nothing) + return invoke(report,Tuple{AbstractReportDefinition,typeof(observed)},definition,observed;reference) +end + +# These keywords control acquisition. Observation defaults remain with the source. +function _report_observation_options(options; retained = false) + for key in keys(options) + key in (:ydata, :rdata, :requests, :quantities) && throw(ArgumentError( + "use values to select report quantities; $key is not a reporting keyword")) + key in (:units, :length_unit, :quantity_units, :frequency_unit, :freq_unit, + :clip, :atol, :frequencies) || throw(ArgumentError( + "unknown reporting keyword $key; illustration options belong in plot_options")) + retained && key in (:clip, :atol, :frequencies) && + throw(ArgumentError( + "$key requires a raw result; retained reports only select or re-express recorded values")) + end + haskey(options, :frequency_unit) && haskey(options, :freq_unit) && + throw(ArgumentError( + "use frequency_unit or freq_unit, not both")) + return (; + (key===:freq_unit ? :frequency_unit=>value : key=>value + for (key, value) in options)...) +end + +function _report_plot_options(source, requests, illustration, options::NamedTuple) + isempty(options) || !(illustration===nothing || illustration===false) || + throw(ArgumentError( + "plot_options requires an explicit illustration")) + for key in (:values, :rdata, :requests, :quantities) + haskey(options, key) && + throw(ArgumentError("select report and illustration quantities with values")) + end + if haskey(options, :ydata) + selected=Commons.observation_selection(source, options.ydata) + expected=Commons.observation_requests(source, requests; complete_pairs = true).displayed + Commons.observation_requests(source, selected; complete_pairs = true).displayed==expected || + throw(ArgumentError("plot_options.ydata conflicts with the report's values selection")) + end + return (; (key=>value for (key, value) in pairs(options) if key!==:ydata)...) +end + +""" +$(TYPEDSIGNATURES) + +Build separate in-memory quantity tables from a completed result or collection. +The same scientific selection used by plotting's `ydata` is named `values` here. +Raw results are observed first. Retained observations preserve their recorded +units and numerical eligibility unless compatible display units are requested. + +# Arguments + +- `source`: completed data supplied as a primary result or standalone Z/Y tensor. + A result space, ordinary collection or retained observation is also accepted. +- `selection`: optional positional alternative to `values`. Do not supply both. + +# Keywords + +- `values=nothing`: a selector, one `@observe` request or a tuple of requests. + `nothing` and `()` use the source owner's defaults or all retained products. +- `units`, `length_unit`, `quantity_units`, `frequency_unit`: observation unit + options. Raw defaults belong to the observation owner. Omitted retained + options preserve recorded units. `freq_unit` is an alternative spelling of + `frequency_unit`. Supplying both is an error. +- `clip`, `atol`, `frequencies`: raw observation options. Cutoffs use native + units. Frequencies for a standalone tensor are in \\[Hz\\]. These keywords + cannot be supplied for retained inputs. +- `reference=nothing`: a separate atomic raw or observed reference, stored in + `artifact.reference`. Numerical comparisons are supplied as completed records + in the observed inputs. +- `illustration=nothing`: `true` or a plotting callable requests an illustration + of the retained observations with the matching `ydata` selection. +- `plot_options=(;)`: options for an explicitly requested illustration. + +# Returns + +- A [`ReportArtifact`](@ref) containing observed inputs and separate quantity + DataFrames in memory. + +# Errors + +Unknown options, competing selection keywords, conflicting illustration +selections, and acquisition options on retained inputs raise `ArgumentError`. +Specialized definitions remain available through `report(definition, observed)`. + +# Examples + +```julia +report(constants) +report(constants; values=(R, L, G, C), length_unit=:kilo, + quantity_units=(R=:base, L=:milli, G=:micro, C=:micro)) +report(line_parameters; values=@observe(R[1, 1, 1:12])) +``` +""" +function report( + source::Union{Commons.AbstractCoreResult, Commons.AbstractResultSpace, + Engine.SeriesImpedance, Engine.ShuntAdmittance, AbstractVector, Tuple}; + values = nothing, reference = nothing, illustration = nothing, + plot_options::NamedTuple = (;), kwargs...) + collection=source isa Union{AbstractVector, Tuple, Commons.AbstractParametricResult} + collection && isempty(source) && + throw(ArgumentError("report requires at least one result")) + acquisition=_report_observation_options(kwargs; + retained = collection && any(point -> point isa ObservedResult, source)) + point=collection ? first(source) : source + requests=Commons.observation_selection(point, values) + displayed=isempty(requests) ? () : + Commons.observation_requests(point, requests; complete_pairs = true).displayed + options=_report_plot_options(point, displayed, illustration, plot_options) + reference===nothing || + reference isa Union{ObservedResult, Commons.AbstractCoreResult, + Engine.SeriesImpedance, Engine.ShuntAdmittance} || + throw(ArgumentError( + "reference must be an atomic result or ObservedResult")) + observed=observables(source, requests; complete_pairs = true, acquisition...) + if reference isa ObservedResult + display_units=(; + (key=>value + for (key, value) in pairs(acquisition) + if key in (:units, :length_unit, :quantity_units, :frequency_unit))...) + isempty(display_units) || (reference=ObservedResult(reference; display_units...)) + elseif reference!==nothing + reference=ObservedResult(reference, requests; complete_pairs = true, acquisition...) + end + return report( + observed; values = displayed, reference, illustration, plot_options = options) +end + +function report(observed::Union{ObservedResult, AbstractVector{<:ObservedResult}}; + values = nothing, reference::Union{Nothing, ObservedResult} = nothing, + illustration = nothing, plot_options::NamedTuple = (;), kwargs...) + display_units=_report_observation_options(kwargs; retained = true) + observed isa AbstractVector && isempty(observed) && + throw(ArgumentError("report requires at least one observation")) + point=observed isa ObservedResult ? observed : first(observed) + requests=Commons.observation_selection(point, values) + displayed=Commons.observation_requests(point, requests).displayed + options=_report_plot_options(point, displayed, illustration, plot_options) + if !isempty(display_units) + observed=observables(observed, requests; display_units...) + reference===nothing || + (reference=ObservedResult(reference, requests; display_units...)) + end + definition=TableReportDefinition(requests; illustration, + plot_options = illustration===nothing || illustration===false ? options : + merge(options, (ydata = displayed,))) + return report(definition, observed; reference) +end + +function report( + source::Union{Commons.AbstractCoreResult, Commons.AbstractResultSpace, + Engine.SeriesImpedance, Engine.ShuntAdmittance, ObservedResult, AbstractVector, Tuple}, + selection; kwargs...) + haskey(kwargs, :values) && + throw(ArgumentError("use positional selection or values, not both")) + return report(source; values = selection, kwargs...) +end diff --git a/src/reportbuilder/montecarlo.jl b/src/reportbuilder/montecarlo.jl new file mode 100644 index 000000000..abb36b571 --- /dev/null +++ b/src/reportbuilder/montecarlo.jl @@ -0,0 +1,61 @@ +""" +$(TYPEDEF) + +Select retained UQ statistical products for quantity-wise tables. + +$(TYPEDFIELDS) +""" +struct MonteCarloTableDefinition{U} <: AbstractReportDefinition + "Length prefix used during raw-input observation construction." + length_unit::Symbol + "Optional quantity-unit overrides." + quantity_units::U + "Set unresolved primary quantities to exact zero, including zero uncertainty." + clip::Bool +end +MonteCarloTableDefinition(length_unit::Symbol=:kilo,quantity_units=nothing) = + MonteCarloTableDefinition(length_unit,quantity_units,true) + +function report(definition::MonteCarloTableDefinition,source::AbstractUncertaintyResult) + requests=Tuple((UQ.statistics,selector,statistic) for selector in (R,L,G,C) for statistic in (Statistics.mean,Statistics.std)) + observed=observables(source,requests;length_unit=definition.length_unit, + quantity_units=definition.quantity_units,clip=definition.clip) + return report(definition,observed) +end +select(::MonteCarloTableDefinition,observed::ObservedResult;reference=nothing) = observed.quantities +function tabulate(::MonteCarloTableDefinition,observed,selected;reference=nothing) + observed isa ObservedResult && return _quantity_tables(selected;gridpoint_id=observed.gridpoint.id) + return map((point,products) -> _quantity_tables(products;gridpoint_id=point.gridpoint.id), + observed,selected) +end + +function _sampling_tables(points,reference) + sampling=NamedTuple[];precision=NamedTuple[];statistics=NamedTuple[] + operands=reference===nothing ? ((:result,points),) : ((:result,points),(:reference,[reference])) + for (role,observations) in operands,point in observations + id=point.gridpoint.id + for quantity in point.quantities + quantity.family===:statistics || continue + push!(statistics,(role,identity=id,request=quantity.request,statistic=quantity.statistic, + unit=quantity.unit,values=Commons.detach(quantity.values))) + end + record=get(point.gridpoint,:sampling,nothing) + record===nothing && continue + scalar=(; (key=>(value===nothing ? missing : value isa Union{Number,Bool,Symbol,AbstractString,Missing} ? value : string(value)) + for (key,value) in pairs(record) if !(key in (:mean_standard_error,:frequencies,:basis)))...) + push!(sampling,merge(scalar,(role,point=id===nothing ? record.point : id.problem_index, + formulation_index=id===nothing ? missing : id.formulation_index, + method=only(Commons.observation_labels([point])),std_sampling_precision=missing))) + for (quantity,values) in pairs(record.mean_standard_error),index in CartesianIndices(values) + coordinates=Tuple(index) + unit=Units.native_unit(Units.quantity(getfield(Engine,quantity)),record.basis) + push!(precision,(role,identity=id,point=id===nothing ? record.point : id.problem_index, + formulation_index=id===nothing ? missing : id.formulation_index, + method=only(Commons.observation_labels([point])),quantity,index=coordinates, + row=first(coordinates),column=length(coordinates)==3 ? coordinates[2] : missing, + frequency_Hz=Commons.nominal(record.frequencies[length(coordinates)==3 ? last(coordinates) : 1]), + standard_error=values[index],unit=Units.label(unit))) + end + end + return (statistics=DataFrame(statistics),sampling=DataFrame(sampling),mean_sampling_precision=DataFrame(precision)) +end diff --git a/src/reportbuilder/performance.jl b/src/reportbuilder/performance.jl new file mode 100644 index 000000000..66745882c --- /dev/null +++ b/src/reportbuilder/performance.jl @@ -0,0 +1,116 @@ +""" +$(TYPEDSIGNATURES) + +Tabulate completed timing records as scalar columns. Allocated bytes measure +Julia allocation volume. Each record keeps its backend's timing scope. + +The optional `labels` are supplied by the same formulation descriptions used +by the comparison report. Full workload and session records remain in the observation. +""" +function _timing_tables(measurements::Union{Nothing, NamedTuple}; + labels=(reference="Reference",result="Result")) + execution=DataFrame() + source_timings=DataFrame() + performance=DataFrame() + performance_samples=DataFrame() + performance_environment=DataFrame() + performance_comparison=DataFrame() + if measurements !== nothing + for (role, record) in pairs(get(measurements,:execution,(;))) + method=getproperty(labels,role) + timing=get(record,:timing,(;)) + session=get(record,:session,nothing) + push!(execution,(;role,method,point=missing,scope=get(timing,:scope,missing), + seconds=get(timing,:seconds,missing),reused=get(record,:reused,missing), + session_id=session===nothing ? missing : get(session,:id,missing), + execution_wall_seconds=get(record,:execution_wall_seconds,missing), + reused_points=get(timing,:reused_points,missing));cols=:union) + native=get(timing,:source_timings,(;)) + sources=haskey(native,:points) ? enumerate(native.points) : ((1,native),) + for (point,measured) in sources + isempty(measured) && continue + # Source timing records are already scoped by their measurement + # owner. Non-scalar annotations are text, never hidden arrays. + scalar=(; (key => (value===nothing ? missing : + value isa Union{Number,Bool,Symbol,AbstractString,Missing} ? value : string(value)) + for (key,value) in pairs(measured))...) + push!(source_timings,merge((;role,method,point),scalar);cols=:union) + end + for (point,measured) in enumerate(get(timing,:points,())) + push!(execution,(;role,method,point,scope=get(measured,:scope,missing), + seconds=get(measured,:seconds,missing),reused=get(measured,:reused,missing));cols=:union) + end + end + recorded=get(measurements,:performance,nothing) + if recorded !== nothing + session=get(measurements,:session,nothing) + for role in (:reference,:result) + method=getproperty(labels,role) + measured=getproperty(recorded,role) + observations=get(measured,:observations,()) + push!(performance,(;role,method,scope=measured.scope, + median_seconds=measured.median_seconds,allocated_bytes=measured.bytes, + allocated_MiB=measured.bytes/2.0^20, + samples=measured.samples,requested_samples=recorded.settings.samples, + reused=any(row -> get(row,:reused,false),observations), + session_id=session===nothing ? missing : get(session,:id,missing), + checksum_verified=get(measurements,:checksum_verified,missing), + workload_verified=get(measurements,:workload_verified,missing));cols=:union) + for (sample,observation) in enumerate(observations) + push!(performance_samples,(;role,method,sample,scope=measured.scope, + seconds=get(observation,:seconds,missing), + allocated_bytes=get(observation,:bytes,missing), + reused=get(observation,:reused,missing));cols=:union) + end + for (key,value) in pairs(measured.environment) + # Full calculation and workload bindings remain retained, + # not printed as one opaque cell in the default table. + key in (:calculation,:workload,:settings) && continue + value isa NamedTuple && continue + push!(performance_environment,(;role,method,property=string(key), + value=value===nothing ? missing : + value isa Union{Number,Bool,Symbol,AbstractString,Missing} ? value : string(value));cols=:union) + end + end + push!(performance_comparison,( + reference_over_result=recorded.speedup,comparable=recorded.comparable, + requested_samples=recorded.settings.samples,time_budget_seconds=recorded.settings.seconds)) + end + end + return (;execution,source_timings,performance,performance_samples, + performance_environment,performance_comparison) +end + +# Attach each retained measurement to the results it describes. A batch-scoped +# measurement keeps that scope even when several observations refer to it. +# Equal numbers never establish that two measurements are the same event. +function _timing_tables(points::AbstractVector,reference) + reference_id=reference===nothing ? nothing : reference.gridpoint.id + labels=Commons.observation_labels(reference===nothing ? points : [points;reference]) + reference_label=reference===nothing ? "Reference" : last(labels) + combined=_timing_tables(nothing) + for (index,point) in enumerate(points) + record=point.timings + tables=_timing_tables(get(record,:measurements,nothing); + labels=(reference=reference_label,result=labels[index])) + if haskey(record,:seconds) + push!(tables.execution,(role=:result,method=labels[index], + point=point.gridpoint.id.problem_index,scope=get(record,:scope,missing), + seconds=record.seconds);cols=:union) + end + for (destination,table) in zip(combined,tables) + isempty(table) && continue + result_id=point.gridpoint.id + bindings=(result_source=string(result_id.source_id), + result_point=result_id.problem_index,result_formulation=result_id.formulation_index, + reference_source=reference_id===nothing ? missing : string(reference_id.source_id), + reference_point=reference_id===nothing ? missing : reference_id.problem_index, + reference_formulation=reference_id===nothing ? missing : reference_id.formulation_index) + for (name,value) in pairs(bindings) + table[!,name]=fill(value,size(table,1)) + end + append!(destination,table;cols=:union) + end + end + return combined +end diff --git a/src/reportbuilder/tables.jl b/src/reportbuilder/tables.jl new file mode 100644 index 000000000..963d861d8 --- /dev/null +++ b/src/reportbuilder/tables.jl @@ -0,0 +1,301 @@ +""" +$(TYPEDEF) + +Publish an [`Engine.CableConstants`](@ref) result as separate R/L/C/G tables, +each with one operating-frequency row and a named column per assembly. + +$(TYPEDFIELDS) +""" +struct CableConstantsTableDefinition <: AbstractReportDefinition + "Whether detached display residue is replaced with exact zero." + clip::Bool +end +CableConstantsTableDefinition() = CableConstantsTableDefinition(true) + +""" +$(TYPEDEF) + +Define requests and display units for separate line-parameter quantity tables. +Each full matrix table has one frequency column and every ordered coefficient. + +$(TYPEDFIELDS) +""" +struct LineParametersTableDefinition{Q <: Tuple, U} <: AbstractReportDefinition + "Explicit observable requests in quantity-table order." + requests::Q + "SI prefix used to display frequency." + frequency_unit::Symbol + "Length prefix used for per-length quantities." + length_unit::Symbol + "Optional display-unit overrides resolved from the requests." + quantity_units::U + "Whether detached display residue is replaced with exact zero." + clip::Bool +end + +""" +$(TYPEDEF) + +Select and publish per-term comparisons of scalar or formulation-space results. +Retained products supply detailed tables and a compact summary. + +$(TYPEDFIELDS) +""" +struct BenchmarkTableDefinition{S <: NamedTuple, I, O <: NamedTuple} <: AbstractReportDefinition + "Normalized quantities, frequency bands and numerical comparison controls." + settings::S + "Explicitly supplied comparison controls, used when selecting retained products." + requested::Tuple{Vararg{Symbol}} + "Whether detached display residue is replaced with exact zero." + clip::Bool + "Optional explicit illustration request. Use `nothing` to omit the figure." + illustration::I + "Options for the explicitly requested illustration." + plot_options::O +end + +""" +$(TYPEDSIGNATURES) + +Request per-term comparisons grouped by formulation and frequency band. +Quantities use the observation grammar. The default bands are the entire range, +near DC, harmonic, narrowband and wideband. `fundamental` is in Hz. Creating a figure requires an explicit `illustration`. A benchmark report retains one scalar reference separately from its results. +The explicit comparison operation also supports declared collection pairings. +""" +function BenchmarkTableDefinition(; clip::Bool=false, illustration=nothing, plot_options=(;), kwargs...) + haskey(kwargs,:requests) && any(key -> haskey(kwargs,key),(:quantities,:statistics)) && + throw(ArgumentError("use requests or quantities/statistics, not both")) + statistics = Tuple(get(kwargs,:statistics,(:value,))) + quantities = Tuple(get(kwargs,:quantities,statistics == (:value,) ? (Z,Y,R,L,G,C) : (R,L,C,G))) + requests = get(kwargs,:requests,nothing) + if requests === nothing + selectors = map(quantities) do value + value isa Function && return value + matches = filter(selector -> nameof(selector) == value, (Z,Y,R,X,L,G,B,C)) + length(matches) == 1 || throw(ArgumentError("unknown physical quantity $value")) + only(matches) + end + transforms = map(statistics) do entry + entry === :value && return nothing + entry isa Function && return entry + matches = filter(selector -> nameof(selector) == entry, + (Statistics.mean,Statistics.std,Statistics.median,minimum,maximum)) + length(matches) == 1 || throw(ArgumentError("unknown statistic $entry")) + only(matches) + end + requests = Tuple(transform === nothing ? selector : (UQ.statistics,selector,transform) + for selector in selectors for transform in transforms) + end + controls = (; (key=>value for (key,value) in kwargs if !(key in (:requests,:quantities,:statistics)))...) + result = BenchmarkTableDefinition(Tuple(requests);clip,illustration,plot_options,controls...) + requested = Tuple(unique(key in (:quantities,:statistics) ? :requests : key for key in keys(kwargs))) + return BenchmarkTableDefinition(result.settings,requested,clip,illustration,plot_options) +end + +""" +$(TYPEDSIGNATURES) + +Select scientific requests for per-term RMS comparisons. Statistical requests +use `(statistics, quantity, statistic)` function tuples. Bands apply equally to +deterministic and statistical products. Absolute limits use native physical +units. Relative errors are dimensionless. Plotting remains explicitly requested. +""" +function BenchmarkTableDefinition(requests::Tuple; clip::Bool=false, illustration=nothing, + plot_options=(;), kwargs...) + defaults=(bands=(:all,:dc,:harmonic,:narrow,:wide), + normalizations=(:reference_rms,), atol=nothing, fundamental=50.0, harmonics=50, + unsupported=(;), pairing=nothing) + isempty(setdiff(keys(kwargs),keys(defaults))) || throw(ArgumentError("unknown benchmark comparison controls")) + supplied=merge(defaults,(;kwargs...)) + settings=merge((;requests),supplied,( + bands=Tuple(supplied.bands), + normalizations=Tuple(supplied.normalizations))) + return validate(BenchmarkTableDefinition(settings,(:requests,keys(kwargs)...),clip,illustration,plot_options)) +end + +function BenchmarkTableDefinition(clip::Bool; kwargs...) + return BenchmarkTableDefinition(; clip, kwargs...) +end + +"""Validate comparison requests before numerical execution.""" +function validate(definition::BenchmarkTableDefinition) + settings=definition.settings + !isempty(settings.requests) && allunique(settings.requests) || + throw(ArgumentError("benchmark requests must be nonempty and distinct")) + for request in settings.requests + request_quantity(request) + isempty(request_indices(request)) || throw(ArgumentError("benchmark point selection uses pairing")) + end + if settings.pairing !== nothing + settings.pairing isa Union{Tuple,AbstractVector} && !isempty(settings.pairing) && + all(pair -> pair isa Tuple{Integer,Integer} && all(index -> !(index isa Bool) && index>0,pair),settings.pairing) && + sort(last.(collect(settings.pairing))) == collect(1:length(settings.pairing)) || + throw(ArgumentError("pairing must list positive reference/result indices with each result exactly once")) + end + !isempty(settings.bands) && allunique(settings.bands) || + throw(ArgumentError("benchmark needs distinct frequency bands")) + !isempty(settings.normalizations) && allunique(settings.normalizations) || + throw(ArgumentError("benchmark needs distinct RMS normalizations")) + for band in settings.bands, normalization in settings.normalizations + validate((; normalization, band, fundamental=settings.fundamental, + harmonics=settings.harmonics, atol=settings.atol, + unsupported=settings.unsupported), Engine.compare) + end + return definition +end + + +LineParametersTableDefinition(requests::Tuple=();frequency_unit::Symbol=:base, + length_unit::Symbol=:kilo,quantity_units=nothing,clip::Bool=true) = + LineParametersTableDefinition(requests,frequency_unit,length_unit,quantity_units,clip) + +_bound_name(selector::Base.Fix2,statistic) = + selector.f in (Engine.ModalAnalysis.H,Engine.ModalAnalysis.Zc, + Engine.ModalAnalysis.Yc) ? + string(nameof(selector.f),"_phase", + selector.f===Engine.ModalAnalysis.H ? "_$(selector.x.field)" : "") : + string(statistic) + +_quantity_name(product) = begin + identity=request_identity(product.request) + identity isa Function ? (identity isa Base.Fix2 ? Symbol(_bound_name(identity,product.statistic)) : nameof(identity)) : + Symbol(join([entry isa Base.Fix2 ? _bound_name(entry,product.statistic) : string(nameof(entry)) for entry in identity],"_")) +end + +function _quantity_table(product; gridpoint_id=nothing) + coordinates=product.coordinates + values=product.values + scalar=values isa Number || ismissing(values) + if coordinates.kind in (:matrix,:diagonal,:vector) + diagonal=coordinates.kind===:diagonal + vector=coordinates.kind===:vector + dimensions=vector ? (length(coordinates.positions),length(coordinates.samples)) : + diagonal ? (length(coordinates.rows),length(coordinates.samples)) : + (length(coordinates.rows),length(coordinates.columns),length(coordinates.samples)) + shaped=reshape(scalar ? [values] : values,dimensions...) + f=coordinates.frequencies + table=f===nothing ? DataFrame(sample=coordinates.samples) : DataFrame(frequency=f) + columns=Pair{Symbol,Any}[] + if vector + for (i,position) in enumerate(coordinates.positions) + name=Symbol(coordinates.axis_label," ",coordinates.labels[position]) + table[!,name]=copy(shaped[i,:]) + push!(columns,name=>(quantity=product.quantity,unit=product.unit, + axis=coordinates.axis,position,label=coordinates.labels[position])) + end + elseif diagonal + for (i,row) in enumerate(coordinates.rows) + name=Symbol("[",row,",",row,"]") + table[!,name]=copy(shaped[i,:]) + push!(columns,name=>(quantity=product.quantity,unit=product.unit,row,column=row)) + end + else + # Full matrices are deliberately row-major, including both off-diagonals. + for (i,row) in enumerate(coordinates.rows), (j,column) in enumerate(coordinates.columns) + name=get(coordinates,:column_domain,nothing)===:ModalDomain ? + Symbol("Conductor ",coordinates.labels[row],", Mode ",coordinates.column_labels[column]) : + Symbol("[",row,",",column,"]") + table[!,name]=copy(shaped[i,j,:]) + push!(columns,name=>(quantity=product.quantity,unit=product.unit,row,column, + row_label=coordinates.labels[row], + column_label=get(coordinates,:column_labels,coordinates.labels)[column])) + end + end + first_column=f===nothing ? (:sample=>(quantity=nothing,unit=nothing)) : + (:frequency=>(quantity=Units.Quantity{:frequency}(),unit=coordinates.frequency_unit)) + metadata!(table,"observation_columns",(; (first_column,columns...)...);style=:note) + elseif coordinates.kind===:assemblies + table=DataFrame(frequency=coordinates.frequencies) + columns=Pair{Symbol,Any}[:frequency=>(quantity=Units.Quantity{:frequency}(),unit=coordinates.frequency_unit)] + for (index,assembly) in enumerate(coordinates.assemblies) + name=Symbol(coordinates.labels[assembly]) + name===:frequency && throw(ArgumentError("assembly name conflicts with the frequency column")) + table[!,name]=[scalar ? values : values[index]] + push!(columns,name=>(quantity=product.quantity,unit=product.unit,assembly)) + end + metadata!(table,"observation_columns",(;columns...);style=:note) + elseif coordinates.kind===:samples + if haskey(coordinates,:rows) + dims=(length(coordinates.rows),length(coordinates.columns),length(coordinates.samples),length(coordinates.trials)) + shaped=reshape(scalar ? [values] : values,dims...) + table=DataFrame([(frequency=coordinates.frequencies[k],row=coordinates.rows[i],column=coordinates.columns[j], + trial=coordinates.trials[t],value=shaped[i,j,k,t]) for t in 1:dims[4] for k in 1:dims[3] for i in 1:dims[1] for j in 1:dims[2]]) + else + shaped=reshape(scalar ? [values] : values,length(coordinates.assemblies),length(coordinates.trials)) + table=DataFrame([(assembly=coordinates.assemblies[i],trial=coordinates.trials[t],value=shaped[i,t]) + for t in eachindex(coordinates.trials) for i in eachindex(coordinates.assemblies)]) + end + metadata!(table,"observation_columns",(value=(quantity=product.quantity,unit=product.unit),);style=:note) + elseif values isa NamedTuple + table=DataFrame(values) + metadata!(table,"observation_columns",(;);style=:note) + else + vector=vec(scalar ? [values] : values) + table=DataFrame(index=collect(eachindex(vector)),value=copy(vector)) + metadata!(table,"observation_columns",(value=(quantity=product.quantity,unit=product.unit),);style=:note) + end + coordinate_columns=coordinates.kind===:samples ? Tuple(filter(!=(:value),propertynames(table))) : + coordinates.kind in (:matrix,:diagonal,:vector,:assemblies,:array) ? (first(propertynames(table)),) : () + metadata!(table,"coordinate_columns",coordinate_columns;style=:note) + metadata!(table,"coordinates",Commons.detach(coordinates);style=:note) + metadata!(table,"quantity",product.quantity;style=:note) + metadata!(table,"request",Commons.detach(product.request);style=:note) + metadata!(table,"statistic",Commons.detach(product.statistic);style=:note) + metadata!(table,"family",product.family;style=:note) + metadata!(table,"unit",product.unit;style=:note) + metadata!(table,"basis",product.basis;style=:note) + metadata!(table,"missing_reason",Commons.detach(product.missing_reason);style=:note) + gridpoint_id===nothing || metadata!(table,"gridpoint_id",gridpoint_id;style=:note) + return table +end + +""" +$(TYPEDSIGNATURES) + +Build one table per retained quantity, grouped by the defining physical family. +Full matrices retain every coefficient in row-major order. Each row represents +one retained frequency or sample coordinate. +""" +function _quantity_tables(products; gridpoint_id=nothing) + allunique((q.family,_quantity_name(q)) for q in products) || throw(ArgumentError( + "multiple retained products share a quantity name; select a complete request with tabulate(observed, request)")) + families=unique(q.family for q in products) + return (;(family=>(;(_quantity_name(q)=>_quantity_table(q;gridpoint_id) for q in products if q.family==family)...) + for family in families)...) +end +tabulate(observed::ObservedResult) = _quantity_tables(observed.quantities;gridpoint_id=observed.gridpoint.id) + +"""Build one table from a retained quantity request.""" +tabulate(observed::ObservedResult,request) = _quantity_table(Commons.observation_product(observed,request);gridpoint_id=observed.gridpoint.id) +tabulate(observed::AbstractVector{<:ObservedResult}) = map(tabulate,observed) + +select(::CableConstantsTableDefinition,observed::ObservedResult;reference=nothing) = observed.quantities +function select(definition::LineParametersTableDefinition,observed::ObservedResult;reference=nothing) + return [Commons.observation_product(observed,request) + for request in Commons.observation_requests(observed,definition.requests).retained] +end +function tabulate(::Union{TableReportDefinition,CableConstantsTableDefinition,LineParametersTableDefinition}, + observed,selected;reference=nothing) + observed isa ObservedResult && return _quantity_tables(selected;gridpoint_id=observed.gridpoint.id) + return map((point,products) -> _quantity_tables(products;gridpoint_id=point.gridpoint.id), + observed,selected) +end +function report(definition::CableConstantsTableDefinition,source::Engine.CableConstants;kwargs...) + return report(definition,ObservedResult(source;clip=definition.clip,kwargs...)) +end +function report(definition::LineParametersTableDefinition,source::Engine.LineParameters;kwargs...) + return report(definition,ObservedResult(source,definition.requests;complete_pairs=true,clip=definition.clip, + frequency_unit=definition.frequency_unit,length_unit=definition.length_unit, + quantity_units=definition.quantity_units,kwargs...)) +end + +""" +$(TYPEDSIGNATURES) + +Reject aggregate conversion because an observation contains separate physical +quantities. Select a quantity table with `ReportBuilder.tabulate(observed, R)` +or a leaf such as `ReportBuilder.tabulate(observed).Z.R`. +""" +function DataFrame(::ObservedResult) + throw(ArgumentError("ObservedResult contains separate quantity tables; use ReportBuilder.tabulate(observed, R) or ReportBuilder.tabulate(observed).Z.R")) +end diff --git a/src/reportbuilder/textdisplay.jl b/src/reportbuilder/textdisplay.jl new file mode 100644 index 000000000..739e3615f --- /dev/null +++ b/src/reportbuilder/textdisplay.jl @@ -0,0 +1,294 @@ +_report_name(value) = String(nameof(typeof(value))) + +function Base.summary(io::IO, definition::AbstractReportDefinition) + print(io, _report_name(definition), " report definition") +end +function Base.show(io::IO, definition::AbstractReportDefinition) + print(io, _report_name(definition), "()") +end +Base.show(io::IO, ::MIME"text/plain", definition::AbstractReportDefinition) = + show(io, definition) + +Base.summary(io::IO, definition::TableReportDefinition) = + print(io, "Table report with $(length(definition.requests)) observations") +function Base.show(io::IO, definition::TableReportDefinition) + print(io, "TableReportDefinition(requests=", length(definition.requests), ")") +end +function Base.show(io::IO, ::MIME"text/plain", definition::TableReportDefinition) + get(io, :compact, false) && return show(io, definition) + children = Any[ + (label = "requests $(length(definition.requests))", noun = "fields"), + (label = "clip $(definition.clip)", noun = "fields"), + ] + definition.illustration === nothing || push!(children, + (label = "illustration $(definition.illustration)", noun = "fields")) + return TextDisplay.tree(io, "Table report definition", Tuple(children)) +end + +Base.summary(io::IO, ::CableConstantsTableDefinition) = + print(io, "Cable-constants table definition") +Base.show(io::IO, definition::CableConstantsTableDefinition) = + print(io, "CableConstantsTableDefinition(clip=", definition.clip, ")") +Base.show(io::IO, ::MIME"text/plain", definition::CableConstantsTableDefinition) = + show(io, definition) + +Base.summary(io::IO, definition::LineParametersTableDefinition) = + print(io, "Line-parameters table with $(length(definition.requests)) observations") +function Base.show(io::IO, definition::LineParametersTableDefinition) + print(io, "LineParametersTableDefinition(requests=", length(definition.requests), + ", length_unit=:", definition.length_unit, ")") +end +Base.show(io::IO, ::MIME"text/plain", definition::LineParametersTableDefinition) = + show(io, definition) + +Base.summary(io::IO, ::BenchmarkTableDefinition) = print(io, "Benchmark table definition") +Base.show(io::IO, definition::BenchmarkTableDefinition) = + print(io, "BenchmarkTableDefinition(clip=", definition.clip, ")") +Base.show(io::IO, ::MIME"text/plain", definition::BenchmarkTableDefinition) = + show(io, definition) + +Base.summary(io::IO, ::MonteCarloTableDefinition) = print(io, "Monte Carlo table definition") +function Base.show(io::IO, definition::MonteCarloTableDefinition) + print(io, "MonteCarloTableDefinition(length_unit=:", definition.length_unit, + ", clip=", definition.clip, ")") +end +Base.show(io::IO, ::MIME"text/plain", definition::MonteCarloTableDefinition) = + show(io, definition) + +Base.summary(io::IO, ::XLSXReportDefinition) = print(io, "XLSX report definition") +function Base.show(io::IO, definition::XLSXReportDefinition) + destination = definition.file_name === nothing ? "default path" : repr(definition.file_name) + print(io, "XLSXReportDefinition(", destination, "; clip=", definition.clip, ", overwrite=", definition.overwrite, ")") +end +Base.show(io::IO, ::MIME"text/plain", definition::XLSXReportDefinition) = + show(io, definition) + +function Base.summary(io::IO, artifact::ReportArtifact) + groups=_reported_tables(artifact) + point_count=length(_observed_points(artifact.observed)) + table_count=sum(group -> length(group.tables),groups;init=0) + print(io,"Report") + point_count>1 && print(io," · ",point_count," gridpoints") + print(io," · ",table_count,table_count==1 ? " table" : " tables") +end + +function Base.show(io::IO, artifact::ReportArtifact) + groups=_reported_tables(artifact) + point_count=length(_observed_points(artifact.observed)) + print(io,"ReportArtifact(") + point_count>1 && print(io,"gridpoints=",point_count,", ") + print(io,"tables=",sum(group -> length(group.tables),groups;init=0)) + artifact.illustration===nothing || print(io,", illustration=present") + artifact.output===nothing || print(io,", output=present") + print(io,")") +end + +_report_escape(value)=replace(string(value),'&'=>"&",'<'=>"<",'>'=>">",'"'=>""",'\''=>"'") + +function _report_line(io,::MIME"text/plain",text;level=0,first=false) + first || print(io,'\n') + line=replace(string(text),'\n'=>' ','\r'=>' ') + print(io,get(io,:limit,true) ? TextDisplay.truncate_text(line,max(displaysize(io)[2],0)) : line) +end +function _report_line(io,::MIME"text/html",text;level=0,first=false) + tag=level==0 ? "p" : "h$(level)" + print(io,"<",tag,">",_report_escape(text),"") +end + +function _reported_heading(table;families=false,limited=false) + quantity=metadata(table,"quantity",nothing) + unit=metadata(table,"unit",nothing) + title=quantity===nothing ? "Table" : unit===nothing ? Units.label(quantity) : Units.label(quantity,unit) + statistic=metadata(table,"statistic",:value) + statistic===:value || (title*=" · "*string(statistic)) + coordinates=metadata(table,"coordinates",(;)) + get(coordinates,:kind,nothing)===:diagonal && (title*=" · diagonal") + family=metadata(table,"family",nothing) + families && family in (:Z,:Y) && (title=string(family," · ",title)) + columns=metadata(table,"observation_columns",(;)) + for name in metadata(table,"coordinate_columns",()) + descriptor=get(columns,name,nothing) + text=descriptor===nothing || descriptor.quantity===nothing ? string(name) : + Units.label(descriptor.quantity,descriptor.unit) + title*=" · "*text + end + if limited + rows,columns=size(table) + title*=" · $rows row$(rows==1 ? "" : "s") × $columns column$(columns==1 ? "" : "s")" + end + return title +end + +# Native DataFrames writers own cell formatting and whole-row and column omission. +# A limited table is buffered only within its allotted display area, to account +# for its actual line use. Unlimited inspection streams directly to the caller. +function _report_table(io,mime::MIME"text/plain",table,height,width,limited) + if !limited + print(io,'\n') + show(IOContext(io,:limit=>false),mime,table;summary=false,eltypes=false,truncate=0) + return 0 + end + # Native horizontal cropping can cut a numeric token. Measure a bounded + # native preview without cell clipping, dropping whole columns until it fits. + columns=min(DataFrames.ncol(table),max(width÷3,1)) + while columns>0 || DataFrames.ncol(table)==0 + buffer=IOBuffer() + context=IOContext(IOContext(buffer,io),:limit=>true,:displaysize=>(height,width)) + show(context,mime,table;summary=false,eltypes=false,truncate=0, + reserved_display_lines=0,maximum_number_of_rows=max(height-3,1), + maximum_number_of_columns=columns,allrows=true,allcols=true, + show_omitted_cell_summary=false) + rendered=String(take!(buffer)) + lines=split(rendered,'\n') + # Ignore terminal color escapes for width measurement only. + if all(line -> textwidth(replace(line,r"\e\[[0-9;:]*m"=>""))<=width,lines) + print(io,'\n',rendered) + return length(lines) + end + columns-=1 + DataFrames.ncol(table)==0 && break + end + _report_line(io,mime,"… no complete column fits") + return 1 +end +function _report_table(io,mime::MIME"text/html",table,height,width,limited) + show(IOContext(io,:limit=>limited),mime,table;summary=false,eltypes=false, + maximum_number_of_rows=limited ? max(height-3,1) : -1, + maximum_number_of_columns=limited ? max(width÷12,1) : -1, + show_omitted_cell_summary=!limited,new_line_at_end=false) + return limited ? min(height,DataFrames.nrow(table)+2) : 0 +end + +function _show_quantity_report(io,mime,artifact) + groups=_reported_tables(artifact) + total=sum(group -> length(group.tables),groups;init=0) + limited=get(io,:limit,true) + rows,width=displaysize(io) + point_count=length(_observed_points(artifact.observed)) + table_noun=total==1 ? "table" : "tables" + context=point_count>1 ? "Report · $point_count gridpoints" : "Report" + _report_line(io,mime,"$context · $total $table_noun";level=2,first=true) + limited && rows<=1 && return nothing + # One final line remains available for honest omission counts. + remaining=limited ? max(rows-2,0) : typemax(Int) + points=collect(_observed_points(artifact.observed)) + labels=Commons.observation_labels(artifact.reference===nothing ? points : [points;artifact.reference];fallback="") + if artifact.reference!==nothing && remaining>0 + _report_line(io,mime,isempty(last(labels)) ? "Reference" : "Reference · $(last(labels))") + remaining-=1 + end + shown=0 + shown_points=0 + for group in groups + index=group.point_index + tables=group.tables + isempty(tables) && continue + context=index===nothing ? join(unique(filter(!isempty,labels[1:point_count])),"; ") : labels[index] + if index!==nothing && point_count>1 && (isempty(context) || count(==(context),labels[1:point_count])>1) + context="[$index]"*(isempty(context) ? "" : " · $context") + end + context_lines=isempty(context) ? 0 : 1 + # Heading, column labels, separator, at least one data row, and native + # omission information when needed. No gridpoint consumes the next one's + # place unless its own selected quantities have received a fair preview. + costs=[3+min(DataFrames.nrow(table),1)+(DataFrames.nrow(table)>1) for table in tables] + visible=limited ? count(<=(remaining-context_lines),cumsum(costs)) : length(tables) + (visible==0 || limited && width<12) && break + isempty(context) || _report_line(io,mime,context;level=3) + remaining-=context_lines + shown_points+=index===nothing ? point_count : 1 + families=length(unique(metadata(table,"family",nothing) for table in tables))>1 + for position in 1:visible + table=tables[position] + remaining-=1 + height=limited ? max(costs[position]-1,remaining÷(visible-position+1)-1) : 0 + _report_line(io,mime,_reported_heading(table;families,limited);level=4) + used=_report_table(io,mime,table,height,width,limited) + remaining-=used + shown+=1 + end + visible1 ? "; $shown_points/$point_count gridpoints shown" : "" + _report_line(io,mime,"… $(total-shown) tables omitted$progress") + elseif remaining>0 && artifact.illustration!==nothing + _report_line(io,mime,"Illustration retained") + remaining-=1 + end + if remaining>0 && artifact.output!==nothing + output=artifact.output isa AbstractString ? "Written output: $(artifact.output)" : + artifact.output isa AbstractVector ? "$(length(artifact.output)) written destinations" : "Written output retained" + _report_line(io,mime,output) + end + return nothing +end + +function Base.show(io::IO, mime::MIME"text/plain", artifact::ReportArtifact;kwargs...) + get(io,:compact,false) && return show(io,artifact) + artifact.tables isa NamedTuple && haskey(artifact.tables,:features) && + return _show_observed_benchmark(io,mime,artifact;kwargs...) + return _show_quantity_report(io,mime,artifact) +end + +Base.summary(io::IO, sheet::XLSXSheet) = print(io, "XLSX sheet \"", sheet.name, "\"") +function Base.show(io::IO, sheet::XLSXSheet) + print(io, "XLSXSheet(\"", sheet.name, "\"; cells=", join(size(sheet.cells), '×'), ")") +end +Base.show(io::IO, ::MIME"text/plain", sheet::XLSXSheet) = show(io, sheet) + +Base.summary(io::IO, workbook::XLSXWorkbook) = + print(io, "XLSX workbook with $(length(workbook.sheets)) sheets") +function Base.show(io::IO, workbook::XLSXWorkbook) + print(io, "XLSXWorkbook(", repr(workbook.destination), "; sheets=", + length(workbook.sheets), ")") +end +function Base.show(io::IO, ::MIME"text/plain", workbook::XLSXWorkbook) + get(io, :compact, false) && return show(io, workbook) + sheets = Tuple((label = "$(sheet.name) · $(join(size(sheet.cells), '×')) cells", + noun = "sheets") for sheet in workbook.sheets) + return TextDisplay.tree(io, "XLSX workbook · $(length(workbook.sheets)) sheets", sheets; + noun = "sheets") +end + + +function _show_observed_benchmark(io,mime,artifact;metric=:relative,problem=nothing,native_timings=false) + metric in (:relative,:absolute) || throw(ArgumentError("metric must be :relative or :absolute")) + features=filter(row -> problem===nothing || row.problem_index==problem,artifact.tables.features) + isempty(features) && throw(ArgumentError("the selected point has no retained comparison features")) + html=mime isa MIME"text/html" + table_options=html ? (;) : (;truncate=0) + heading(title)=html ? println(io,"

",_report_escape(title),"

") : println(io,title) + note(value)=html ? println(io,"

",_report_escape(value),"

") : println(io,value) + for feature in features + label=metric===:relative ? "Relative RMS [%]" : "Absolute RMS [$(feature.absolute_unit)]" + heading("$(feature.quantity) — $label · $(feature.statistic) · point $(feature.problem_index)") + show(IOContext(io,:limit=>false),mime,getproperty(feature,metric);summary=false,eltypes=false,table_options...) + println(io) + end + note("Maxima use eligible terms only; unavailable coefficients remain missing.") + overview=artifact.tables.overview + if isempty(overview.performance) + note("No controlled performance measurements were recorded.") + else + heading("Controlled computation measurements") + note("Allocated bytes are cumulative Julia allocations, not peak memory.") + any(==(1),overview.performance.samples) && note("At least one measurement has only one timed call; variability is unavailable.") + any(!,overview.timing_ratio.comparable) && note("The recorded ratio is not a validated speedup.") + end + for (name,table) in pairs(overview) + name===:source_timings && !native_timings && continue + isempty(table) && continue + heading(replace(string(name),'_'=>' ')) + shown=DataFrames.select(table,Not(intersect(propertynames(table),[:session_id,:checksum_verified,:workload_verified,:result_source,:reference_source]))) + show(IOContext(io,:limit=>false),mime,shown;summary=false,eltypes=false,table_options...) + println(io) + end + return nothing +end +function Base.show(io::IO,mime::MIME"text/html",artifact::ReportArtifact;kwargs...) + get(io,:compact,false) && return show(io,artifact) + artifact.tables isa NamedTuple && haskey(artifact.tables,:features) && + return _show_observed_benchmark(io,mime,artifact;kwargs...) + return _show_quantity_report(io,mime,artifact) +end diff --git a/src/reportbuilder/xlsx.jl b/src/reportbuilder/xlsx.jl new file mode 100644 index 000000000..e94736847 --- /dev/null +++ b/src/reportbuilder/xlsx.jl @@ -0,0 +1,135 @@ +""" +$(TYPEDEF) + +Write one workbook per gridpoint and retained quantity. `file_name` supplies a +path prefix. Numeric values and standard deviations occupy separate sheets. + +$(TYPEDFIELDS) +""" +struct XLSXReportDefinition <: AbstractReportDefinition + "Requested output path prefix, or nothing for observed.xlsx." + file_name::Union{Nothing,String} + "Optional system name used only when constructing output filenames." + system_id::Union{Nothing,String} + "Set unresolved observed values to exact zero, including zero uncertainty." + clip::Bool + "Allow replacing existing destination files after complete preflight." + overwrite::Bool +end +function XLSXReportDefinition(;file_name=nothing,cable_system=nothing,clip::Bool=true,overwrite::Bool=false) + return XLSXReportDefinition(file_name===nothing ? nothing : String(file_name), + cable_system===nothing ? nothing : String(cable_system.system_id),clip,overwrite) +end + +""" +$(TYPEDEF) + +An encoded sheet with numeric cells and textual headings. + +$(TYPEDFIELDS) +""" +struct XLSXSheet + "Worksheet name." + name::String + "Numeric cells or textual metadata. Cells may also be missing." + cells::Matrix{Any} +end +""" +$(TYPEDEF) + +A workbook ready for the XLSX writer. + +$(TYPEDFIELDS) +""" +struct XLSXWorkbook + "Absolute output path." + destination::String + "Sheets in output order." + sheets::Vector{XLSXSheet} +end + +""" +$(TYPEDSIGNATURES) + +Convert a spreadsheet number to Float64. Reject nonfinite values, overflow, and +nonzero underflow. Native persistence retains original precision and uncertainty +dependencies. Spreadsheets contain nominal values and standard deviations. +""" +function encode_cell(::XLSXReportDefinition,value::Real) + number=Float64(value) + isfinite(number) && (iszero(number) ? iszero(value) : true) || + throw(ArgumentError("value cannot be represented as a finite non-underflowing XLSX number")) + return number +end +encode_cell(::XLSXReportDefinition,value::Complex) = throw(ArgumentError("XLSX quantities require retained real components; select real-valued primary or statistical products")) +encode_cell(::XLSXReportDefinition,::Missing) = missing +encode_cell(::XLSXReportDefinition,value::AbstractString) = String(value) +encode_cell(::XLSXReportDefinition,value) = string(value) + +function _numeric_sheet(definition,table,name,transform) + _xlsx_sheet_size(size(table,1)+1,size(table,2)) + cells=Matrix{Any}(missing,size(table,1)+1,size(table,2)) + cells[1,:]=names(table) + coordinate_columns=DataFrames.metadata(table,"coordinate_columns",()) + for j in 1:size(table,2),i in 1:size(table,1) + value=table[i,j] + cells[i+1,j]=ismissing(value) ? missing : encode_cell(definition,propertynames(table)[j] in coordinate_columns ? Commons.nominal(value) : transform(value)) + end + return XLSXSheet(name,cells) +end + +select(::XLSXReportDefinition,observed::ObservedResult;reference=nothing) = observed.quantities +function tabulate(::XLSXReportDefinition,observed,selected;reference=nothing) + observed isa ObservedResult && return _quantity_tables(selected;gridpoint_id=observed.gridpoint.id) + return map((point,products) -> _quantity_tables(products;gridpoint_id=point.gridpoint.id), + observed,selected) +end +function report(definition::XLSXReportDefinition,source::Engine.LineParameters;kwargs...) + return report(definition,ObservedResult(source;clip=definition.clip,kwargs...)) +end +function encode(definition::XLSXReportDefinition,observed,tables,illustration;reference=nothing) + workbooks=XLSXWorkbook[] + requested=abspath(something(definition.file_name,"observed.xlsx")) + stem=splitext(basename(requested))[1] + definition.system_id===nothing || (stem=definition.system_id*"_"*stem) + for (point_index,point) in enumerate(_observed_points(observed)), product in point.quantities + point_tables=observed isa ObservedResult ? tables : tables[point_index] + table=getproperty(getproperty(point_tables,product.family),_quantity_name(product)) + name=replace(string(_quantity_name(product)),r"[^A-Za-z0-9_-]"=>"_") + id=get(point.gridpoint,:id,nothing) + index=id===nothing ? string(point_index) : string(id.source_id,"_",id.problem_index,"_",id.formulation_index) + destination=joinpath(dirname(requested),stem*"_"*index*"_"*name*".xlsx") + any(book -> book.destination==destination,workbooks) && throw(ArgumentError("duplicate workbook destination")) + entries=(quantity=string(product.quantity),unit=Units.label(product.unit),basis=string(product.basis), + coordinates=repr(product.coordinates),gridpoint=repr(point.gridpoint), + cutoffs=repr(product.thresholds),missing_reason=repr(product.missing_reason)) + cells=Matrix{Any}(undef,length(entries),2) + for (i,(key,value)) in enumerate(pairs(entries)) + cells[i,1]=string(key); cells[i,2]=value + end + sheets=[_numeric_sheet(definition,table,"values",Commons.nominal), + _numeric_sheet(definition,table,"std",Commons.uncertainty),XLSXSheet("metadata",cells)] + push!(workbooks,XLSXWorkbook(destination,sheets)) + end + return validate(workbooks,definition) +end + +function _xlsx_sheet_size(rows,columns) + rows<=1_048_576 && columns<=16_384 || throw(DimensionMismatch( + "XLSX worksheets support at most 1048576 rows and 16384 columns, including headings")) + return nothing +end + +function validate(workbooks::AbstractVector{<:XLSXWorkbook},definition::XLSXReportDefinition) + destinations=[abspath(book.destination) for book in workbooks] + allunique(destinations) || throw(ArgumentError("duplicate workbook destination")) + for book in workbooks + ispath(book.destination) && (!definition.overwrite || !isfile(book.destination)) && + throw(ArgumentError("XLSX destination already exists: $(book.destination); use overwrite=true to replace files")) + isdir(dirname(book.destination)) || throw(ArgumentError("XLSX destination directory does not exist")) + for sheet in book.sheets + _xlsx_sheet_size(size(sheet.cells)...) + end + end + return workbooks +end diff --git a/src/textdisplay/TextDisplay.jl b/src/textdisplay/TextDisplay.jl new file mode 100644 index 000000000..2b361bbbf --- /dev/null +++ b/src/textdisplay/TextDisplay.jl @@ -0,0 +1,404 @@ +# Package-local formatting support. Domain owners select semantic names, fields, +# units, and child relationships. This module supplies bounded writers only. +module TextDisplay + +import ..Units +import ..LineCableModels: FormulaDefinition + +""" +Return the explicitly declared semantic display name for an owned type. +""" +function name end + +function _fallback(value) + return sprint(show, value; context = :compact => true) +end + +""" +Format one numeric value with at most `sigdigits` significant digits. +""" +function value(number::Real; sigdigits::Integer = 6) + sigdigits > 0 || throw(ArgumentError("sigdigits must be positive")) + isnan(number) && return "NaN" + isinf(number) && return signbit(number) ? "−∞" : "∞" + iszero(number) && return "0" + rounded = try + round(number; sigdigits) + catch + return _fallback(number) + end + if rounded isa AbstractFloat && isinteger(rounded) && + abs(rounded) <= typemax(Int) + return string(round(Int, rounded)) + end + return replace(string(rounded), "e+" => "e") +end + +function value(number::Complex; sigdigits::Integer = 6) + real_text = value(real(number); sigdigits) + imag_text = value(abs(imag(number)); sigdigits) + sign = signbit(imag(number)) ? " − " : " + " + return string(real_text, sign, imag_text, "im") +end + +value(item; sigdigits::Integer = 6) = _fallback(item) + +const _ENGINEERING_PREFIX = ( + -24 => :yocto, + -21 => :zepto, + -18 => :atto, + -15 => :femto, + -12 => :pico, + -9 => :nano, + -6 => :micro, + -3 => :milli, + 0 => :base, + 3 => :kilo, + 6 => :mega, + 9 => :giga, + 12 => :tera, + 15 => :peta, + 18 => :exa, + 21 => :zetta, + 24 => :yotta +) + +function _engineering_exponent(number::Real) + iszero(number) && return 0 + magnitude = try + abs(Float64(number)) + catch exception + exception isa InexactError || + (exception isa MethodError && exception.f === Float64 && + length(exception.args) == 1 && only(exception.args) === number) || + rethrow() + return 0 + end + iszero(magnitude) && return 0 + isfinite(magnitude) || return 0 + exponent = 3 * floor(Int, log10(magnitude) / 3) + return clamp(exponent, first(first(_ENGINEERING_PREFIX)), first(last(_ENGINEERING_PREFIX))) +end + +function _prefix(exponent::Integer) + only(last(pair) for pair in _ENGINEERING_PREFIX + if first(pair) == exponent) +end + +function _physical_unit(::Val{:meter}, prefix::Symbol) + return Units.label(Units.Unit(:meter, prefix)) +end +function _physical_unit(::Val{:ohm_meter}, prefix::Symbol) + return string(Units.label(Units.Unit(:ohm, prefix)), "·m") +end +function _physical_unit(::Val{:hertz}, prefix::Symbol) + return Units.label(Units.Unit(:hertz, prefix)) +end +_physical_unit(::Val{:celsius}, ::Symbol) = "°C" +_physical_unit(::Val{:kelvin_inverse}, ::Symbol) = "K⁻¹" +_physical_unit(::Val{:degree}, ::Symbol) = "°" +_physical_unit(::Val{:dimensionless}, ::Symbol) = "" + +_adaptive_prefix(::Val{:meter}) = true +_adaptive_prefix(::Val{:ohm_meter}) = true +_adaptive_prefix(::Val{:hertz}) = true +_adaptive_prefix(::Val{:celsius}) = false +_adaptive_prefix(::Val{:kelvin_inverse}) = false +_adaptive_prefix(::Val{:degree}) = false +_adaptive_prefix(::Val{:dimensionless}) = false + +function _append_unit(number::AbstractString, unit::AbstractString) + isempty(unit) && return String(number) + unit == "°" && return string(number, unit) + return string(number, " ", unit) +end + +""" +Format a physical value using an adaptive engineering prefix. +""" +function engineering(number::Real, unit::Symbol; sigdigits::Integer = 6) + if !isfinite(number) + return _append_unit(value(number; sigdigits), _physical_unit(Val(unit), :base)) + end + exponent = _adaptive_prefix(Val(unit)) ? _engineering_exponent(number) : 0 + prefix = _prefix(exponent) + scaled = number / (10.0^exponent) + return _append_unit( + value(scaled; sigdigits), + _physical_unit(Val(unit), prefix) + ) +end + +""" +Format an angle stored in radians. +""" +function angle(radians::Real; sigdigits::Integer = 6) + if isfinite(radians) && !iszero(radians) + turns = radians / π + nearest = round(turns) + if abs(turns) >= 1 && isapprox(turns, nearest; rtol = 0, atol = 16eps(float(turns))) + nearest == 1 && return "π" + nearest == -1 && return "−π" + return string(value(nearest; sigdigits), "π") + end + end + return _append_unit(value(rad2deg(radians); sigdigits), "°") +end + +""" + truncate_text(text::AbstractString, width::Integer) + +Return text occupying at most `width` display columns, appending an ellipsis +when truncated. Nonpositive widths return an empty string. Character widths +follow `textwidth`. The ellipsis occupies one column. +""" +function truncate_text(text::AbstractString, width::Integer) + width <= 0 && return "" + textwidth(text) <= width && return String(text) + width == 1 && return "…" + characters = collect(text) + while !isempty(characters) && textwidth(join(characters)) > width - 1 + pop!(characters) + end + return string(join(characters), "…") +end + +function _display_width(io::IO) + _, columns = displaysize(io) + return max(columns, 20) +end + +""" +Write an explicitly named flat field record in compact or aligned form. +""" +function fields( + io::IO, + semantic_name::AbstractString, + displayed::NamedTuple; + multiline::Bool = false +) + retained = Pair{Symbol, Any}[key => item + for (key, item) in pairs(displayed) if item !== nothing] + if !multiline || get(io, :compact, false) + print(io, semantic_name, "(") + for (index, pair) in enumerate(retained) + index > 1 && print(io, ", ") + print(io, first(pair), "=", last(pair)) + end + print(io, ")") + return nothing + end + print(io, semantic_name) + isempty(retained) && return nothing + key_width = maximum(textwidth(String(first(pair))) for pair in retained) + available = max(_display_width(io) - key_width - 4, 1) + limited = get(io, :limit, true) + rows, _ = displaysize(io) + visible_count = limited ? min(length(retained), max(rows - 2, 0)) : length(retained) + for pair in Iterators.take(retained, visible_count) + key = String(first(pair)) + print( + io, '\n', " ", rpad(key, key_width), " ", truncate_text(string(last(pair)), available)) + end + omitted = length(retained) - visible_count + omitted > 0 && print(io, '\n', " ⋮ $omitted more fields") + return nothing +end + +function _tree_state(io::IO) + limited = get(io, :limit, true) + rows, _ = displaysize(io) + return ( + # Reserve one row for an explicit truncation summary. This keeps the + # complete output within `displaysize(io)` even when the row budget is + # exhausted in a nested branch. + remaining = Ref(limited ? max(rows - 2, 0) : typemax(Int)), + depth = limited ? clamp(rows ÷ 4, 1, 4) : typemax(Int), + children = limited ? 8 : typemax(Int), + truncation = Ref("") + ) +end + +_tree_children(node::NamedTuple) = get(node, :children, ()) +_tree_children(node) = () +_tree_noun(node::NamedTuple) = String(get(node, :noun, "items")) +_tree_noun(node) = "items" + +function _write_tree_line(io::IO, prefix, connector, label) + available = max(_display_width(io) - textwidth(prefix) - 3, 1) + print(io, '\n', prefix, connector, truncate_text(label, available)) + return nothing +end + +function _write_tree_children!(io::IO, nodes, prefix, level, state) + entries = collect(nodes) + isempty(entries) && return nothing + + visible_count = min(length(entries), state.children) + omitted_by_count = length(entries) - visible_count + for index in 1:visible_count + if state.remaining[] <= 0 + isempty(state.truncation[]) && + (state.truncation[] = "$(length(entries) - index + 1) more $(_tree_noun(first(entries)))") + return nothing + end + node = entries[index] + children = collect(_tree_children(node)) + has_following = index < visible_count || omitted_by_count > 0 + connector = has_following ? "├─ " : "└─ " + label = node isa NamedTuple ? String(node.label) : string(node) + _write_tree_line(io, prefix, connector, label) + state.remaining[] -= 1 + + isempty(children) && continue + continuation = string(prefix, has_following ? "│ " : " ") + if level >= state.depth + if state.remaining[] <= 0 + isempty(state.truncation[]) && + (state.truncation[] = "$(length(children)) nested $(_tree_noun(node))") + return nothing + end + noun = _tree_noun(node) + _write_tree_line( + io, + continuation, + "└─ ", + "⋮ $(length(children)) nested $noun" + ) + state.remaining[] -= 1 + else + _write_tree_children!(io, children, continuation, level + 1, state) + end + end + + if omitted_by_count > 0 + message = "$omitted_by_count more $(_tree_noun(first(entries)))" + if state.remaining[] > 0 + _write_tree_line(io, prefix, "└─ ", "⋮ $message") + state.remaining[] -= 1 + else + isempty(state.truncation[]) && (state.truncation[] = message) + end + end + return nothing +end + +""" +Write a deterministic, depth-, row-, and width-bounded tree. + +Each child is either a string or a named tuple with `label`, optional +`children`, and optional `noun` fields. The formatter consumes these detached +display records. +""" +function tree( + io::IO, + header::AbstractString, + children; + noun::AbstractString = "items" +) + print(io, truncate_text(header, _display_width(io))) + get(io, :compact, false) && return nothing + entries = collect(children) + isempty(entries) && return nothing + normalized = map(entries) do entry + entry isa NamedTuple ? entry : (label = string(entry), noun = noun) + end + state = _tree_state(io) + _write_tree_children!(io, normalized, "", 1, state) + isempty(state.truncation[]) || _write_tree_line( + io, + "", + "└─ ", + "⋮ $(state.truncation[])" + ) + return nothing +end + +""" +Generate the three Base display methods for one explicitly described flat record. +""" +macro showfields(type_expression, semantic_name, mapping) + mapping isa Expr && mapping.head === :-> || throw(ArgumentError( + "@showfields expects `value -> (field = formatted, ...)`" + )) + binding = mapping.args[1] + binding isa Symbol || throw(ArgumentError( + "@showfields requires one untyped value binding" + )) + body = mapping.args[2] + name_ref = GlobalRef(@__MODULE__, :name) + fields_ref = GlobalRef(@__MODULE__, :fields) + # The generated methods record the caller's file and line, so `@which` and stack + # traces point to the call. Line numbers inside escaped caller code are unchanged. + located(x) = x isa LineNumberNode ? __source__ : + x isa Expr && x.head !== :escape ? Expr(x.head, map(located, x.args)...) : x + return located(quote + $name_ref(::Type{<:$(esc(type_expression))}) = $(esc(semantic_name)) + function Base.summary(io::IO, value::$(esc(type_expression))) + print(io, $name_ref(typeof(value))) + end + function Base.show(io::IO, value::$(esc(type_expression))) + displayed = let $(esc(binding)) = value + $(esc(body)) + end + $fields_ref(io, $name_ref(typeof(value)), displayed; multiline = false) + end + function Base.show( + io::IO, + ::MIME"text/plain", + value::$(esc(type_expression)) + ) + displayed = let $(esc(binding)) = value + $(esc(body)) + end + $fields_ref(io, $name_ref(typeof(value)), displayed; multiline = true) + end + end) +end + +public name, value, engineering, angle, fields, tree +public truncate_text, @showfields + +Base.summary(io::IO, unit::Units.Unit) = print(io, "Physical unit ", Units.label(unit)) +Base.show(io::IO, unit::Units.Unit) = print(io, Units.label(unit)) +Base.show(io::IO, ::MIME"text/plain", unit::Units.Unit) = show(io, unit) + +function Base.summary(io::IO, unit::Units.UnitExpr) + print(io, "Unit expression ", Units.label(unit)) +end +Base.show(io::IO, unit::Units.UnitExpr) = print(io, Units.label(unit)) +Base.show(io::IO, ::MIME"text/plain", unit::Units.UnitExpr) = show(io, unit) + +function Base.summary(io::IO, quantity::Units.Quantity) + print(io, Units.label(quantity)) +end +function Base.show(io::IO, quantity::Units.Quantity) + scientific_symbol = Units.symbol(quantity) + isempty(scientific_symbol) ? print(io, Units.label(quantity)) : + print(io, scientific_symbol, " · ", Units.label(quantity)) +end +Base.show(io::IO, ::MIME"text/plain", quantity::Units.Quantity) = show(io, quantity) + +name(::Type{<:FormulaDefinition}) = "FormulaDefinition" +function Base.summary(io::IO, ::FormulaDefinition{ID}) where {ID} + print(io, "Formula definition :", ID) +end +function Base.show(io::IO, definition::FormulaDefinition{ID, Order}) where {ID, Order} + fields(io, + "FormulaDefinition", + (id = ID, order = Order, parameters = definition.parameters, + options = definition.options, + equivalent_earth = definition.equivalent_earth); + multiline = false) +end +function Base.show(io::IO, ::MIME"text/plain", definition::FormulaDefinition{ + ID, Order}) where {ID, Order} + fields(io, + "FormulaDefinition", + (id = ID, order = Order, parameters = definition.parameters, + options = definition.options, + equivalent_earth = definition.equivalent_earth); + multiline = true) +end + +end diff --git a/src/uncertainbessels/UncertainBessels.jl b/src/uncertainbessels/UncertainBessels.jl deleted file mode 100644 index 435dfd49d..000000000 --- a/src/uncertainbessels/UncertainBessels.jl +++ /dev/null @@ -1,173 +0,0 @@ -""" - LineCableModels.UncertainBessels - -Uncertainty-aware wrappers for Bessel functions. - -[`UncertainBessels`](@ref) lifts selected functions from `SpecialFunctions` so they accept -`Measurement` and `Complex{Measurement}` inputs. The wrapper evaluates the -underlying function at the nominal complex argument and propagates uncertainty -via first-order finite differences using the four partial derivatives ``\\frac{\\partial \\mathrm{Re} \\, f}{\\partial x}, \\frac{\\partial \\mathrm{Re} \\, f}{\\partial y}, \\frac{\\partial \\mathrm{Im} \\, f}{\\partial x}, \\frac{\\partial \\mathrm{Im} \\, f}{\\partial y}`` with ``x = \\mathrm{Re}(z)`` and ``y = \\mathrm{Im}(z)``. No new Bessel algorithms are implemented: for plain numeric inputs, results and numerical behaviour are those of -`SpecialFunctions`. - -Numerical scaling (as defined by `SpecialFunctions`) is supported for the -“x” variants (e.g. `besselix`, `besselkx`, `besseljx`, …) to improve stability -for large or complex arguments. In particular, the modified functions use -exponential factors to temper growth along ``\\mathrm{Re}(z)`` (e.g. ``I_\\nu`` and ``K_\\nu``); -other scaled variants follow conventions in `SpecialFunctions` and DLMF guidance -for complex arguments. See [NIST:DLMF](@cite) and [6897971](@cite). - -# Overview - -- Thin, uncertainty-aware wrappers around `SpecialFunctions` (`besselj`, `bessely`, - `besseli`, `besselk`, `besselh`) and their scaled counterparts (`…x`). -- For `Complex{Measurement}` inputs, uncertainty is propagated using the 4-component - gradient with respect to ``\\mathrm{Re}(z)`` and ``\\mathrm{Im}(z)``. -- For `Measurement` (real) inputs, a 1-D finite-difference derivative is used. -- No change in semantics for `Real`/`Complex` inputs: calls delegate to `SpecialFunctions`. - -# Dependencies - -$(IMPORTS) - -# Exports - -$(EXPORTS) - -# Usage - -```julia -# do not import SpecialFunctions directly -using LineCableModels.UncertainBessels -z = complex(1.0, 1.0 ± 0.5) -J0_cpl = besselj(0, z) # Complex{Measurement} -J0_nom = besselj(0, value(z)) # nominal comparison -I1 = besselix(1, z) # scaled I1 with uncertainty -``` - -# Numerical notes - -- Scaled modified Bessels remove large exponential factors along ``\\mathrm{Re}(z)`` (e.g., ``I_\\nu`` and ``K_\\nu`` are scaled by opposite signs of ``|\\mathrm{Re}(z)|``), improving conditioning. Scaled forms for the other families follow the definitions in `SpecialFunctions` and DLMF. -- Uncertainty propagation is first order (linearization at the nominal point). - Large uncertainties or strong nonlinearity may reduce accuracy. - -# See also - -- [`LineCableModels.Engine.InternalImpedance`](@ref) -- [`LineCableModels.Engine.EarthImpedance`](@ref) -""" -module UncertainBessels - -# Module-specific dependencies -using ..Commons -using Calculus: Calculus -using SpecialFunctions: SpecialFunctions -using Measurements: Measurements, Measurement - -export besselix, besselkx, besseljx, besselyx, besselhx -export besseli, besselk, besselj, bessely, besselh - -# Complex argument with measurement parts -@inline function _lift_complex_measurement(f, ν, ẑ::Complex{<:Measurement}) - return Measurements.result( - f(ν, Measurements.value(ẑ)), - vcat( - Calculus.gradient( - x -> real(f(ν, complex(x[1], x[2]))), - [reim(Measurements.value(ẑ))...], - ), - Calculus.gradient( - x -> imag(f(ν, complex(x[1], x[2]))), - [reim(Measurements.value(ẑ))...], - ), - ), - ẑ, - ) -end - -# Real argument with measurement -@inline function _lift_real_measurement(f, ν, x::Measurements.Measurement) - x0 = Measurements.value(x) - y0 = f(ν, x0) - dy = Calculus.derivative(t -> f(ν, t), x0) - return Measurements.result(y0, (dy,), x) -end - - -# Complex inputs with uncertainty -@inline besselix(ν, z::Complex{<:Measurements.Measurement{T}}) where {T <: AbstractFloat} = - _lift_complex_measurement(SpecialFunctions.besselix, ν, z) -@inline besselkx(ν, z::Complex{<:Measurements.Measurement{T}}) where {T <: AbstractFloat} = - _lift_complex_measurement(SpecialFunctions.besselkx, ν, z) -@inline besseljx(ν, z::Complex{<:Measurements.Measurement{T}}) where {T <: AbstractFloat} = - _lift_complex_measurement(SpecialFunctions.besseljx, ν, z) -@inline besselyx(ν, z::Complex{<:Measurements.Measurement{T}}) where {T <: AbstractFloat} = - _lift_complex_measurement(SpecialFunctions.besselyx, ν, z) -@inline besselhx(ν, z::Complex{<:Measurements.Measurement{T}}) where {T <: AbstractFloat} = - _lift_complex_measurement(SpecialFunctions.besselhx, ν, z) -@inline besselj(ν, z::Complex{<:Measurements.Measurement{T}}) where {T <: AbstractFloat} = - _lift_complex_measurement(SpecialFunctions.besselj, ν, z) -@inline bessely(ν, z::Complex{<:Measurements.Measurement{T}}) where {T <: AbstractFloat} = - _lift_complex_measurement(SpecialFunctions.bessely, ν, z) -@inline besseli(ν, z::Complex{<:Measurements.Measurement{T}}) where {T <: AbstractFloat} = - _lift_complex_measurement(SpecialFunctions.besseli, ν, z) -@inline besselk(ν, z::Complex{<:Measurements.Measurement{T}}) where {T <: AbstractFloat} = - _lift_complex_measurement(SpecialFunctions.besselk, ν, z) -@inline besselh(ν, z::Complex{<:Measurements.Measurement{T}}) where {T <: AbstractFloat} = - _lift_complex_measurement(SpecialFunctions.besselh, ν, z) - -# Real inputs with uncertainty -@inline besselix(ν, x::Measurements.Measurement{T}) where {T <: AbstractFloat} = - _lift_real_measurement(SpecialFunctions.besselix, ν, x) -@inline besselkx(ν, x::Measurements.Measurement{T}) where {T <: AbstractFloat} = - _lift_real_measurement(SpecialFunctions.besselkx, ν, x) -@inline besseljx(ν, x::Measurements.Measurement{T}) where {T <: AbstractFloat} = - _lift_real_measurement(SpecialFunctions.besseljx, ν, x) -@inline besselyx(ν, x::Measurements.Measurement{T}) where {T <: AbstractFloat} = - _lift_real_measurement(SpecialFunctions.besselyx, ν, x) -@inline besselhx(ν, x::Measurements.Measurement{T}) where {T <: AbstractFloat} = - _lift_real_measurement(SpecialFunctions.besselhx, ν, x) -@inline besselj(ν, x::Measurements.Measurement{T}) where {T <: AbstractFloat} = - _lift_real_measurement(SpecialFunctions.besselj, ν, x) -@inline bessely(ν, x::Measurements.Measurement{T}) where {T <: AbstractFloat} = - _lift_real_measurement(SpecialFunctions.bessely, ν, x) -@inline besseli(ν, x::Measurements.Measurement{T}) where {T <: AbstractFloat} = - _lift_real_measurement(SpecialFunctions.besseli, ν, x) -@inline besselk(ν, x::Measurements.Measurement{T}) where {T <: AbstractFloat} = - _lift_real_measurement(SpecialFunctions.besselk, ν, x) -@inline besselh(ν, x::Measurements.Measurement{T}) where {T <: AbstractFloat} = - _lift_real_measurement(SpecialFunctions.besselh, ν, x) - -# Plain Float/Complex fallbacks -@inline besselix(ν, z::T) where {T <: AbstractFloat} = SpecialFunctions.besselix(ν, z) -@inline besselkx(ν, z::T) where {T <: AbstractFloat} = SpecialFunctions.besselkx(ν, z) -@inline besseljx(ν, z::T) where {T <: AbstractFloat} = SpecialFunctions.besseljx(ν, z) -@inline besselyx(ν, z::T) where {T <: AbstractFloat} = SpecialFunctions.besselyx(ν, z) -@inline besselhx(ν, z::T) where {T <: AbstractFloat} = SpecialFunctions.besselhx(ν, z) -@inline besselj(ν, z::T) where {T <: AbstractFloat} = SpecialFunctions.besselj(ν, z) -@inline bessely(ν, z::T) where {T <: AbstractFloat} = SpecialFunctions.bessely(ν, z) -@inline besseli(ν, z::T) where {T <: AbstractFloat} = SpecialFunctions.besseli(ν, z) -@inline besselk(ν, z::T) where {T <: AbstractFloat} = SpecialFunctions.besselk(ν, z) -@inline besselh(ν, z::T) where {T <: AbstractFloat} = SpecialFunctions.besselh(ν, z) - -@inline besselix(ν, z::Complex{T}) where {T <: AbstractFloat} = - SpecialFunctions.besselix(ν, z) -@inline besselkx(ν, z::Complex{T}) where {T <: AbstractFloat} = - SpecialFunctions.besselkx(ν, z) -@inline besseljx(ν, z::Complex{T}) where {T <: AbstractFloat} = - SpecialFunctions.besseljx(ν, z) -@inline besselyx(ν, z::Complex{T}) where {T <: AbstractFloat} = - SpecialFunctions.besselyx(ν, z) -@inline besselhx(ν, z::Complex{T}) where {T <: AbstractFloat} = - SpecialFunctions.besselhx(ν, z) -@inline besselj(ν, z::Complex{T}) where {T <: AbstractFloat} = - SpecialFunctions.besselj(ν, z) -@inline bessely(ν, z::Complex{T}) where {T <: AbstractFloat} = - SpecialFunctions.bessely(ν, z) -@inline besseli(ν, z::Complex{T}) where {T <: AbstractFloat} = - SpecialFunctions.besseli(ν, z) -@inline besselk(ν, z::Complex{T}) where {T <: AbstractFloat} = - SpecialFunctions.besselk(ν, z) -@inline besselh(ν, z::Complex{T}) where {T <: AbstractFloat} = - SpecialFunctions.besselh(ν, z) - -end # module UncertainBessels diff --git a/src/unithandler/UnitHandler.jl b/src/unithandler/UnitHandler.jl deleted file mode 100644 index c8c44dc95..000000000 --- a/src/unithandler/UnitHandler.jl +++ /dev/null @@ -1,416 +0,0 @@ -module UnitHandler - -using Base: @kwdef - -export Unit, Units, units, get_label, get_symbol, get_exp, - METRIC_PREFIX_EXPONENT, METRIC_PREFIX_SYMBOL, UNIT_SYMBOL, - QuantityTag, default_unit, display_unit, scale_factor - -# -------------------------------------------------------------------------- -# Metric prefixes -# -------------------------------------------------------------------------- - -const METRIC_PREFIX_EXPONENT = Dict( - :yocto => -24, - :zepto => -21, - :atto => -18, - :femto => -15, - :pico => -12, - :nano => -9, - :micro => -6, - :milli => -3, - :centi => -2, - :deci => -1, - :base => 0, - :deca => 1, - :hecto => 2, - :kilo => 3, - :mega => 6, - :giga => 9, - :tera => 12, - :peta => 15, - :exa => 18, - :zetta => 21, - :yotta => 24, -) - -const METRIC_PREFIX_SYMBOL = Dict( - :yocto => "y", - :zepto => "z", - :atto => "a", - :femto => "f", - :pico => "p", - :nano => "n", - :micro => "μ", - :milli => "m", - :centi => "c", - :deci => "d", - :base => "", - :deca => "da", - :hecto => "h", - :kilo => "k", - :mega => "M", - :giga => "G", - :tera => "T", - :peta => "P", - :exa => "E", - :zetta => "Z", - :yotta => "Y", -) - -const UNIT_SYMBOL = Dict( - :ohm => "Ω", - :henry => "H", - :farad => "F", - :siemens => "S", - :meter => "m", - :hertz => "Hz", - :degree => "°", - :dimensionless => "", - # extend this as your sadism requires -) - -@inline _prefix_exp(p::Symbol) = get(METRIC_PREFIX_EXPONENT, p, 0) -@inline _prefix_symbol(p::Symbol) = get(METRIC_PREFIX_SYMBOL, p, String(p)) - -# -------------------------------------------------------------------------- -# Core types -# -------------------------------------------------------------------------- - -""" -A single physical unit with a metric prefix. - -- `name` : symbolic unit name, e.g. :ohm, :henry, :farad, :siemens, :meter, :hertz, :degree, :dimensionless -- `prefix` : one of the metric prefix symbols (:base, :kilo, :milli, ...). -""" -@kwdef struct Unit - name::Symbol = :dimensionless - prefix::Symbol = :base -end - -""" -A composite unit, e.g., "Ω/km". - -- `base` : numerator units. -- `per` : denominator units. -""" -@kwdef struct Units - base::Vector{Unit} = [Unit()] # dimensionless by default - per::Vector{Unit} = Unit[] -end - -# -------------------------------------------------------------------------- -# Convenience constructor -# -------------------------------------------------------------------------- - -""" -Convenience constructor for `Units`. - -Examples: - - u1 = units(:base, :ohm) # Ω - u2 = units(:base, :ohm; per = (:kilo, :meter)) # Ω/km -""" -function units( - prefix::Symbol, - name::Symbol; - per::Union{Nothing, Tuple{Symbol, Symbol}} = nothing, -) - b = Unit(name = name, prefix = prefix) - if per === nothing - return Units(base = [b], per = Unit[]) - else - pfx2, name2 = per - d = Unit(name = name2, prefix = pfx2) - return Units(base = [b], per = [d]) - end -end - -# -------------------------------------------------------------------------- -# Formatting -# -------------------------------------------------------------------------- - -""" -Render a `Unit` as a readable string, e.g. "kΩ", "mH", "μF", "Hz". -""" -function get_label(u::Unit) - prefix_str = _prefix_symbol(u.prefix) - unit_str = get(UNIT_SYMBOL, u.name, String(u.name)) - return string(prefix_str, unit_str) -end - -""" -Render composite `Units` as a readable string: - -- base units concatenated with "." -- per units concatenated with "." -- if more than one per, denominator is shown as "(...)" -- :dimensionless units disappear; all-dimensionless → "" -""" -function get_label(u::Units) - base_units = [x for x in u.base if x.name != :dimensionless] - per_units = [x for x in u.per if x.name != :dimensionless] - - base_strs = [get_label(b) for b in base_units if get_label(b) != ""] - per_strs = [get_label(p) for p in per_units if get_label(p) != ""] - - if isempty(base_strs) && isempty(per_strs) - return "" - end - - base_str = isempty(base_strs) ? "" : join(base_strs, ".") - - if isempty(per_strs) - return base_str - else - if isempty(base_str) - base_str = "1" - end - if length(per_strs) == 1 - return string(base_str, "/", per_strs[1]) - else - return string(base_str, "/(", join(per_strs, "."), ")") - end - end -end - -# -------------------------------------------------------------------------- -# Quantity semantics -# -------------------------------------------------------------------------- - -""" -QuantityTag{Q} is a semantic tag for a physical quantity. - -Examples of Q (to be extended as needed): - -- :freq, :Z, :R, :L, :C, :G, :X, :B -- :angle, :index, :distance, :magnitude... - -These tags are used across plotting, exporting, reporting, etc. -""" -struct QuantityTag{Q} end - -QuantityTag(::Val{Q}) where {Q} = QuantityTag{Q}() -QuantityTag(::Type{QuantityTag{Q}}) where {Q} = QuantityTag{Q}() - -""" -Native (storage/computational) unit for a quantity. - -Default: dimensionless, must be overridden in domain code where appropriate. -""" -default_unit(::QuantityTag{Q}) where {Q} = Units() - -""" -Default display unit for a quantity. - -By default, equal to `default_unit(q)`, but you override this when -you want prettier axes/reports. - -Example override (outside this module): - - default_unit(::QuantityTag{:Z}) = units(:base, :ohm; per = (:base, :meter)) # Ω/m - display_unit(::QuantityTag{:Z}) = - units(:base, :ohm; per = (:kilo, :meter)) # Ω/km -""" -display_unit(q::QuantityTag{Q}) where {Q} = default_unit(q) - -""" -Human-readable label for the quantity, without units. - -Default: `String(Q)`, i.e., :freq → "freq". You override this with -more civilized labels. -""" -get_label(::QuantityTag{Q}) where {Q} = String(Q) - -""" -SI symbol label for the quantity. - -Default: fall back to the quantity tag name `Q` as a string. -There is no way to guess, so you must override it with meaningful labels. -""" -get_symbol(::QuantityTag{Q}) where {Q} = - Q isa Tuple ? get_symbol(QuantityTag{first(Q)}()) : String(Q) - - -# -------------------------------------------------------------------------- -# Unit scaling between arbitrary composite units -# -------------------------------------------------------------------------- - -""" -Numeric scale factor to convert values from `from_unit` to `to_unit`. - -If `v_raw` is expressed in `from_unit`, then: - - v_to = v_raw * scale_factor(from_unit, to_unit) - -because 1[to] = 10^(exp_to - exp_from) [from] ⇒ v_to = v_from * 10^(exp_from - exp_to). -""" -function scale_factor(from_unit::Units, to_unit::Units) - ex_from = get_exp(from_unit) - ex_to = get_exp(to_unit) - return 10.0^(ex_from - ex_to) -end - -""" -Scale factor to convert from the native unit of quantity `q` to a -target display unit `to_unit`: - - v_display = v_native * scale_factor(q, to_unit) -""" -function scale_factor(q::QuantityTag, to_unit::Units) - return scale_factor(default_unit(q), to_unit) -end - -# -------------------------------------------------------------------------- -# Prefix exponent aggregation for composite units -# -------------------------------------------------------------------------- - -""" -Return the net base-10 exponent from the prefixes in a composite unit: - - exp = (sum prefix exponents over `base`) - (sum over `per`) -""" -function get_exp(u::Units)::Int - num = 0 - for b in u.base - num += _prefix_exp(b.prefix) - end - den = 0 - for p in u.per - den += _prefix_exp(p.prefix) - end - return num - den -end - -""" -Scale factor to convert a raw value expressed in base-prefixed units -(e.g. Ω/m, S/m, Hz) to the given `Units`. - -i.e. q_disp = q_raw * scale_factor(u) -""" -scale_factor(u::Units) = 10.0^(-get_exp(u)) - -# Convenience: call with Val{:freq} etc. -default_unit(::Val{Q}) where {Q} = - default_unit(QuantityTag{Q}()) - -display_unit(::Val{Q}) where {Q} = - display_unit(QuantityTag{Q}()) - -get_label(::Val{Q}) where {Q} = - get_label(QuantityTag{Q}()) - -# Convenience: call with a Symbol. This just wraps to Val. -default_unit(q::Symbol) = - default_unit(Val(q)) - -display_unit(q::Symbol) = - display_unit(Val(q)) - -get_label(q::Symbol) = - get_label(Val(q)) - - -# -------------------------------------------------------------------------- -# Fundamental quantities -# -------------------------------------------------------------------------- - -# Frequency -default_unit(::QuantityTag{:freq}) = - units(:base, :hertz) -display_unit(::QuantityTag{:freq}) = - units(:base, :hertz) -get_label(::QuantityTag{:freq}) = "Frequency" -get_symbol(::QuantityTag{:freq}) = "f" - -# Series resistance -default_unit(::QuantityTag{:resistance}) = - units(:base, :ohm; per = (:base, :meter)) # Ω/m -display_unit(::QuantityTag{:resistance}) = - units(:base, :ohm; per = (:kilo, :meter)) # Ω/km -get_label(::QuantityTag{:resistance}) = "Series resistance" -get_symbol(::QuantityTag{:resistance}) = "R" - -# Series inductance -default_unit(::QuantityTag{:inductance}) = - units(:base, :henry; per = (:base, :meter)) -display_unit(::QuantityTag{:inductance}) = - units(:milli, :henry; per = (:kilo, :meter)) -get_label(::QuantityTag{:inductance}) = "Series inductance" -get_symbol(::QuantityTag{:inductance}) = "L" - -# Shunt capacitance -default_unit(::QuantityTag{:capacitance}) = - units(:base, :farad; per = (:base, :meter)) -display_unit(::QuantityTag{:capacitance}) = - units(:micro, :farad; per = (:kilo, :meter)) -get_label(::QuantityTag{:capacitance}) = "Shunt capacitance" -get_symbol(::QuantityTag{:capacitance}) = "C" - -# Shunt conductance -default_unit(::QuantityTag{:conductance}) = - units(:base, :siemens; per = (:base, :meter)) -display_unit(::QuantityTag{:conductance}) = - units(:base, :siemens; per = (:kilo, :meter)) -get_label(::QuantityTag{:conductance}) = "Shunt conductance" -get_symbol(::QuantityTag{:conductance}) = "G" - -# Series impedance -default_unit(::QuantityTag{:impedance}) = - units(:base, :ohm; per = (:base, :meter)) -display_unit(::QuantityTag{:impedance}) = - units(:base, :ohm; per = (:kilo, :meter)) -get_label(::QuantityTag{:impedance}) = "Series impedance" -get_symbol(::QuantityTag{:impedance}) = "Z" - -# Shunt admittance -default_unit(::QuantityTag{:admittance}) = - units(:base, :siemens; per = (:base, :meter)) -display_unit(::QuantityTag{:admittance}) = - units(:base, :siemens; per = (:kilo, :meter)) -get_label(::QuantityTag{:admittance}) = "Shunt admittance" -get_symbol(::QuantityTag{:admittance}) = "Y" - -# Inductive reactance -default_unit(::QuantityTag{:reactance}) = - units(:base, :ohm; per = (:base, :meter)) -display_unit(::QuantityTag{:reactance}) = - units(:base, :ohm; per = (:kilo, :meter)) -get_label(::QuantityTag{:reactance}) = "Inductive reactance" -get_symbol(::QuantityTag{:reactance}) = "X" - -# Capacitive susceptance -default_unit(::QuantityTag{:susceptance}) = - units(:base, :siemens; per = (:base, :meter)) -display_unit(::QuantityTag{:susceptance}) = - units(:base, :siemens; per = (:kilo, :meter)) -get_label(::QuantityTag{:susceptance}) = "Capacitive susceptance" -get_symbol(::QuantityTag{:susceptance}) = "B" - -# Angle -default_unit(::QuantityTag{:angle}) = - units(:base, :degree) -display_unit(::QuantityTag{:angle}) = - units(:base, :degree) -get_label(::QuantityTag{:angle}) = "Angle" -get_symbol(::QuantityTag{:angle}) = "∠" - -# magnitude uses same unit as base quantity -default_unit(::QuantityTag{(:impedance, :re)}) = default_unit(QuantityTag{:resistance}()) -default_unit(::QuantityTag{(:impedance, :im)}) = default_unit(QuantityTag{:reactance}()) -default_unit(::QuantityTag{(:impedance, :abs)}) = default_unit(QuantityTag{:impedance}()) -default_unit(::QuantityTag{(:admittance, :re)}) = default_unit(QuantityTag{:conductance}()) -default_unit(::QuantityTag{(:admittance, :im)}) = default_unit(QuantityTag{:susceptance}()) -default_unit(::QuantityTag{(:admittance, :abs)}) = default_unit(QuantityTag{:admittance}()) - -# angle unit -default_unit(::QuantityTag{(:impedance, :angle)}) = default_unit(QuantityTag{:angle}()) -default_unit(::QuantityTag{(:admittance, :angle)}) = default_unit(QuantityTag{:angle}()) - -# labels -get_label(::QuantityTag{(:impedance, :abs)}) = "Series impedance magnitude" -get_label(::QuantityTag{(:impedance, :angle)}) = "Series impedance angle" -get_label(::QuantityTag{(:admittance, :abs)}) = "Shunt admittance magnitude" -get_label(::QuantityTag{(:admittance, :angle)}) = "Shunt admittance angle" - -end # module UnitHandler diff --git a/src/units/Units.jl b/src/units/Units.jl new file mode 100644 index 000000000..0384930d5 --- /dev/null +++ b/src/units/Units.jl @@ -0,0 +1,35 @@ +""" + LineCableModels.Units + +Define the physical-quantity identities and units used by computation results, +plots, and reports. + +# Public actions + +- `quantity` maps a scientific selector to its physical meaning. +- `native_unit` and `display_unit` select computation and presentation units. +- `scale_factor` converts compatible unit expressions. +- `label` and `symbol` format unit and quantity metadata. +""" +module Units + +using DocStringExtensions: TYPEDEF, TYPEDFIELDS, TYPEDSIGNATURES +import ..LineCableModels: R, L, C + +export Unit, UnitExpr, Quantity, units +export quantity, native_unit, display_unit, scale_factor, label, symbol + +""" +Return the owner-defined family of a physical quantity. +""" +function family end + +public family + +include("units.jl") +include("quantities.jl") +include("accessors.jl") +include("scaling.jl") +include("definitions.jl") + +end # module Units diff --git a/src/units/accessors.jl b/src/units/accessors.jl new file mode 100644 index 000000000..2063da369 --- /dev/null +++ b/src/units/accessors.jl @@ -0,0 +1,112 @@ +# Shared physical selectors extended independently by DataModel and Engine. +quantity(::typeof(R)) = Quantity{:series_resistance}() +quantity(::typeof(L)) = Quantity{:series_inductance}() +quantity(::typeof(C)) = Quantity{:shunt_capacitance}() + +""" +Return the scientific label registered for `selector`. +""" +label(selector::Function) = label(quantity(selector)) + +""" +Return the scientific label registered for `selector` and `transform`. +""" +label(selector::Function, transform::Function) = label(quantity(selector, transform)) + +""" +Return the scientific symbol registered for `selector`. +""" +symbol(selector::Function) = symbol(quantity(selector)) + +""" +Return the scientific symbol registered for `selector` and `transform`. +""" +symbol(selector::Function, transform::Function) = symbol(quantity(selector, transform)) + +""" +Return the native unit registered for `selector`. +""" +native_unit(selector::Function) = native_unit(quantity(selector)) + +""" +Return the native unit registered for `selector` at `basis`. +""" +native_unit(selector::Function, basis::Symbol) = native_unit(quantity(selector), basis) + +""" +Return the native unit registered for `selector` and `transform`. +""" +function native_unit(selector::Function, transform::Function) + native_unit(quantity(selector, transform)) +end + +""" +Return the native unit registered for `selector` and `transform` at `basis`. +""" +function native_unit(selector::Function, transform::Function, basis::Symbol) + native_unit(quantity(selector, transform), basis) +end + +""" +Return the display unit registered for `selector`. +""" +display_unit(selector::Function) = display_unit(quantity(selector)) + +""" +Return the display unit registered for `selector` at `basis`. +""" +function display_unit(selector::Function, basis::Symbol; kwargs...) + return display_unit(quantity(selector), basis; kwargs...) +end + +""" +Return the display unit registered for `selector` and `transform`. +""" +function display_unit(selector::Function, transform::Function) + display_unit(quantity(selector, transform)) +end + +""" +Return the display unit registered for `selector` and `transform` at `basis`. +""" +function display_unit( + selector::Function, + transform::Function, + basis::Symbol; + kwargs... +) + return display_unit(quantity(selector, transform), basis; kwargs...) +end + +""" +Return the factor from the native unit of `selector` to `target`. +""" +function scale_factor(selector::Function, target::UnitExpr) + scale_factor(quantity(selector), target) +end + +""" +Return the factor from the native unit of `selector` at `basis` to `target`. +""" +function scale_factor(selector::Function, basis::Symbol, target::UnitExpr) + scale_factor(quantity(selector), basis, target) +end + +""" +Return the factor from the native unit of `selector` and `transform` to `target`. +""" +function scale_factor(selector::Function, transform::Function, target::UnitExpr) + scale_factor(quantity(selector, transform), target) +end + +""" +Return the factor from the native unit of `selector` and `transform` at `basis` to `target`. +""" +function scale_factor( + selector::Function, + transform::Function, + basis::Symbol, + target::UnitExpr +) + return scale_factor(quantity(selector, transform), basis, target) +end diff --git a/src/units/definitions.jl b/src/units/definitions.jl new file mode 100644 index 000000000..d1ad69ab1 --- /dev/null +++ b/src/units/definitions.jl @@ -0,0 +1,139 @@ +native_unit(::Quantity{:frequency}) = units(:base, :hertz) +display_unit(::Quantity{:frequency}) = units(:base, :hertz) +label(::Quantity{:frequency}) = "Frequency" +symbol(::Quantity{:frequency}) = "f" + +native_unit(::Quantity{:series_resistance}) = units(:base, :ohm; per = (:base, :meter)) +display_unit(::Quantity{:series_resistance}) = units(:base, :ohm; per = (:kilo, :meter)) +label(::Quantity{:series_resistance}) = "Series resistance" +symbol(::Quantity{:series_resistance}) = "R" + +native_unit(::Quantity{:series_reactance}) = units(:base, :ohm; per = (:base, :meter)) +display_unit(::Quantity{:series_reactance}) = units(:base, :ohm; per = (:kilo, :meter)) +label(::Quantity{:series_reactance}) = "Series reactance" +symbol(::Quantity{:series_reactance}) = "X" + +native_unit(::Quantity{:series_inductance}) = units(:base, :henry; per = (:base, :meter)) +display_unit(::Quantity{:series_inductance}) = units(:milli, :henry; per = (:kilo, :meter)) +label(::Quantity{:series_inductance}) = "Series inductance" +symbol(::Quantity{:series_inductance}) = "L" + +native_unit(::Quantity{:series_impedance}) = units(:base, :ohm; per = (:base, :meter)) +display_unit(::Quantity{:series_impedance}) = units(:base, :ohm; per = (:kilo, :meter)) +label(::Quantity{:series_impedance}) = "Series impedance" +symbol(::Quantity{:series_impedance}) = "Z" + +native_unit(::Quantity{:shunt_conductance}) = units(:base, :siemens; per = (:base, :meter)) +display_unit(::Quantity{:shunt_conductance}) = units(:base, :siemens; per = (:kilo, :meter)) +label(::Quantity{:shunt_conductance}) = "Shunt conductance" +symbol(::Quantity{:shunt_conductance}) = "G" + +native_unit(::Quantity{:shunt_susceptance}) = units(:base, :siemens; per = (:base, :meter)) +display_unit(::Quantity{:shunt_susceptance}) = units(:base, :siemens; per = (:kilo, :meter)) +label(::Quantity{:shunt_susceptance}) = "Shunt susceptance" +symbol(::Quantity{:shunt_susceptance}) = "B" + +native_unit(::Quantity{:shunt_capacitance}) = units(:base, :farad; per = (:base, :meter)) +display_unit(::Quantity{:shunt_capacitance}) = units(:micro, :farad; per = (:kilo, :meter)) +label(::Quantity{:shunt_capacitance}) = "Shunt capacitance" +symbol(::Quantity{:shunt_capacitance}) = "C" + +native_unit(::Quantity{:shunt_admittance}) = units(:base, :siemens; per = (:base, :meter)) +display_unit(::Quantity{:shunt_admittance}) = units(:base, :siemens; per = (:kilo, :meter)) +label(::Quantity{:shunt_admittance}) = "Shunt admittance" +symbol(::Quantity{:shunt_admittance}) = "Y" + +native_unit(::Quantity{:phase_angle}) = units(:base, :radian) +display_unit(::Quantity{:phase_angle}) = units(:base, :degree) +label(::Quantity{:phase_angle}) = "Phase angle" +symbol(::Quantity{:phase_angle}) = "∠" + +native_unit(::Quantity{:distance}) = units(:base, :meter) +display_unit(::Quantity{:distance}) = units(:base, :meter) +label(::Quantity{:distance}) = "Distance" +symbol(::Quantity{:distance}) = "d" + +native_unit(::Quantity{:dimensionless}) = units(:base, :dimensionless) +display_unit(::Quantity{:dimensionless}) = units(:base, :dimensionless) +label(::Quantity{:dimensionless}) = "Dimensionless" +symbol(::Quantity{:dimensionless}) = "" + +native_unit(::Quantity{(:series_impedance, :magnitude)}) = + native_unit(Quantity{:series_impedance}()) +display_unit(::Quantity{(:series_impedance, :magnitude)}) = + display_unit(Quantity{:series_impedance}()) +native_unit(::Quantity{(:shunt_admittance, :magnitude)}) = + native_unit(Quantity{:shunt_admittance}()) +display_unit(::Quantity{(:shunt_admittance, :magnitude)}) = + display_unit(Quantity{:shunt_admittance}()) +native_unit(::Quantity{(:series_impedance, :phase_angle)}) = + native_unit(Quantity{:phase_angle}()) +display_unit(::Quantity{(:series_impedance, :phase_angle)}) = + display_unit(Quantity{:phase_angle}()) +native_unit(::Quantity{(:shunt_admittance, :phase_angle)}) = + native_unit(Quantity{:phase_angle}()) +display_unit(::Quantity{(:shunt_admittance, :phase_angle)}) = + display_unit(Quantity{:phase_angle}()) + +label(::Quantity{(:series_impedance, :magnitude)}) = "Series impedance magnitude" +symbol(::Quantity{(:series_impedance, :magnitude)}) = "|Z|" +label(::Quantity{(:series_impedance, :phase_angle)}) = "Series impedance angle" +symbol(::Quantity{(:series_impedance, :phase_angle)}) = "∠Z" +label(::Quantity{(:shunt_admittance, :magnitude)}) = "Shunt admittance magnitude" +symbol(::Quantity{(:shunt_admittance, :magnitude)}) = "|Y|" +label(::Quantity{(:shunt_admittance, :phase_angle)}) = "Shunt admittance angle" +symbol(::Quantity{(:shunt_admittance, :phase_angle)}) = "∠Y" + +const _SeriesQuantity = Union{ + Quantity{:series_resistance}, + Quantity{:series_reactance}, + Quantity{:series_inductance}, + Quantity{:series_impedance}, + Quantity{(:series_impedance, :magnitude)}, + Quantity{(:series_impedance, :phase_angle)}, + Quantity{:series_impedance_absolute_error}, + Quantity{:series_impedance_relative_error} +} + +const _ShuntQuantity = Union{ + Quantity{:shunt_conductance}, + Quantity{:shunt_susceptance}, + Quantity{:shunt_capacitance}, + Quantity{:shunt_admittance}, + Quantity{(:shunt_admittance, :magnitude)}, + Quantity{(:shunt_admittance, :phase_angle)}, + Quantity{:shunt_admittance_absolute_error}, + Quantity{:shunt_admittance_relative_error} +} + +family(::_SeriesQuantity) = Val(:series) +family(::_ShuntQuantity) = Val(:shunt) + +native_unit(::Quantity{:series_impedance_absolute_error}) = + native_unit(Quantity{:series_impedance}()) +display_unit(::Quantity{:series_impedance_absolute_error}) = + display_unit(Quantity{:series_impedance}()) +label(::Quantity{:series_impedance_absolute_error}) = "Series impedance absolute error" +symbol(::Quantity{:series_impedance_absolute_error}) = "ΔZ" + +native_unit(::Quantity{:shunt_admittance_absolute_error}) = + native_unit(Quantity{:shunt_admittance}()) +display_unit(::Quantity{:shunt_admittance_absolute_error}) = + display_unit(Quantity{:shunt_admittance}()) +label(::Quantity{:shunt_admittance_absolute_error}) = "Shunt admittance absolute error" +symbol(::Quantity{:shunt_admittance_absolute_error}) = "ΔY" + +native_unit(::Quantity{:series_impedance_relative_error}) = units(:base, :dimensionless) +display_unit(::Quantity{:series_impedance_relative_error}) = units(:base, :dimensionless) +label(::Quantity{:series_impedance_relative_error}) = "Series impedance relative error" +symbol(::Quantity{:series_impedance_relative_error}) = "εZ" + +native_unit(::Quantity{:shunt_admittance_relative_error}) = units(:base, :dimensionless) +display_unit(::Quantity{:shunt_admittance_relative_error}) = units(:base, :dimensionless) +label(::Quantity{:shunt_admittance_relative_error}) = "Shunt admittance relative error" +symbol(::Quantity{:shunt_admittance_relative_error}) = "εY" + +function label(quantity::Quantity, unit::UnitExpr) + unit_text = label(unit) + return isempty(unit_text) ? label(quantity) : "$(label(quantity)) [$unit_text]" +end diff --git a/src/units/quantities.jl b/src/units/quantities.jl new file mode 100644 index 000000000..3d7780ffc --- /dev/null +++ b/src/units/quantities.jl @@ -0,0 +1,18 @@ +""" +$(TYPEDEF) + +Represent the scientific meaning selected by `Q` without storing numerical +values or presentation metadata. +""" +struct Quantity{Q} end + +""" + quantity(selector::Function) + quantity(selector::Function, transform::Function) + +Return the typed [`Quantity`](@ref) identity registered for a scientific +selector or selector-transform pair. + +Unsupported selectors have no fallback method. +""" +function quantity end diff --git a/src/units/scaling.jl b/src/units/scaling.jl new file mode 100644 index 000000000..84b13d1a9 --- /dev/null +++ b/src/units/scaling.jl @@ -0,0 +1,105 @@ +@inline _dimension(name::Symbol) = name in (:radian, :degree) ? :angle : name +function _dimensions(expression::UnitExpr) + return ( + map(unit -> _dimension(unit.name), expression.numerator), + map(unit -> _dimension(unit.name), expression.denominator) + ) +end + +""" +$(TYPEDSIGNATURES) + +Return the numeric factor that converts values between compatible unit +expressions. + +# Errors + +- Throws `ArgumentError` when the expressions represent different dimensions. +""" +function scale_factor(from::UnitExpr, to::UnitExpr) + return scale_factor(from,to,Float64) +end + +""" +$(TYPEDSIGNATURES) + +Convert compatible units using constants evaluated in scalar precision `T`. +Conversion between degrees and radians preserves this precision instead of +rounding π through `Float64`. +""" +function scale_factor(from::UnitExpr,to::UnitExpr,::Type{T}) where {T<:AbstractFloat} + _dimensions(from) == _dimensions(to) || throw( + ArgumentError("cannot convert $(label(from)) to incompatible unit $(label(to))"), + ) + exponent(expression)=sum(unit -> _prefix_exponent(unit.prefix),expression.numerator;init=0) - + sum(unit -> _prefix_exponent(unit.prefix),expression.denominator;init=0) + degrees(expression)=count(unit -> unit.name===:degree,expression.numerator) - + count(unit -> unit.name===:degree,expression.denominator) + return T(10)^(exponent(from)-exponent(to)) * + (T(π)/T(180))^(degrees(from)-degrees(to)) +end + +function _with_basis(expression::UnitExpr, basis::Symbol; length_prefix::Symbol = :base) + basis in (:pul, :total) || throw( + ArgumentError("basis must be :pul or :total"), + ) + has_length = any(unit -> unit.name === :meter, expression.denominator) + retained = filter(unit -> unit.name !== :meter, expression.denominator) + denominator = basis === :pul && has_length ? + (retained..., Unit(:meter, length_prefix)) : retained + return UnitExpr(expression.numerator, denominator) +end + +function native_unit(quantity::Quantity, basis::Symbol) + _with_basis(native_unit(quantity), basis) +end +function display_unit( + quantity::Quantity, + basis::Symbol; + length_prefix::Symbol = :kilo, + prefix::Union{Nothing, Symbol} = nothing +) + expression = _with_basis(display_unit(quantity), basis; length_prefix) + prefix === nothing && return expression + isempty(expression.numerator) && return expression + numerator = ( + Unit(first(expression.numerator).name, prefix), + Base.tail(expression.numerator)... + ) + return UnitExpr(numerator, expression.denominator) +end + +""" +$(TYPEDSIGNATURES) + +Resolve a display unit for a quantity and basis from one optional override. + +The value `nothing` retains the registered display unit, a `Symbol` replaces +its numerator metric prefix, and a `UnitExpr` is returned unchanged. + +# Errors + +- Throws `ArgumentError` for unsupported override values or invalid metric + prefixes. +""" +function display_unit( + quantity::Quantity, + basis::Symbol, + override; + length_prefix::Symbol = :kilo +) + override === nothing && + return display_unit(quantity, basis; length_prefix) + override isa UnitExpr && return override + override isa Symbol || throw(ArgumentError( + "display-unit overrides must be a metric prefix, UnitExpr, or nothing", + )) + return display_unit(quantity, basis; length_prefix, prefix = override) +end + +function scale_factor(quantity::Quantity, target::UnitExpr) + scale_factor(native_unit(quantity), target) +end +function scale_factor(quantity::Quantity, basis::Symbol, target::UnitExpr) + scale_factor(native_unit(quantity, basis), target) +end diff --git a/src/units/units.jl b/src/units/units.jl new file mode 100644 index 000000000..eab269d4a --- /dev/null +++ b/src/units/units.jl @@ -0,0 +1,147 @@ +const _METRIC_PREFIX_EXPONENT = ( + yocto = -24, zepto = -21, atto = -18, femto = -15, pico = -12, + nano = -9, micro = -6, milli = -3, centi = -2, deci = -1, + base = 0, deca = 1, hecto = 2, kilo = 3, mega = 6, giga = 9, + tera = 12, peta = 15, exa = 18, zetta = 21, yotta = 24 +) + +const _METRIC_PREFIX_SYMBOL = ( + yocto = "y", zepto = "z", atto = "a", femto = "f", pico = "p", + nano = "n", micro = "μ", milli = "m", centi = "c", deci = "d", + base = "", deca = "da", hecto = "h", kilo = "k", mega = "M", + giga = "G", tera = "T", peta = "P", exa = "E", zetta = "Z", yotta = "Y" +) + +const _UNIT_SYMBOL = ( + dimensionless = "", ohm = "Ω", henry = "H", farad = "F", + siemens = "S", meter = "m", hertz = "Hz", second = "s", + radian = "rad", degree = "°", neper = "Np" +) + +@inline function _prefix_exponent(prefix::Symbol) + hasproperty(_METRIC_PREFIX_EXPONENT, prefix) || throw( + ArgumentError("unsupported metric prefix :$prefix"), + ) + return getproperty(_METRIC_PREFIX_EXPONENT, prefix) +end + +@inline function _prefix_symbol(prefix::Symbol) + hasproperty(_METRIC_PREFIX_SYMBOL, prefix) || throw( + ArgumentError("unsupported metric prefix :$prefix"), + ) + return getproperty(_METRIC_PREFIX_SYMBOL, prefix) +end + +@inline function _unit_symbol(name::Symbol) + hasproperty(_UNIT_SYMBOL, name) || throw(ArgumentError("unsupported unit :$name")) + return getproperty(_UNIT_SYMBOL, name) +end + +""" +$(TYPEDEF) + +Describe one supported physical unit and its metric prefix. + +$(TYPEDFIELDS) +""" +struct Unit + "Unit name." + name::Symbol + "Metric prefix name." + prefix::Symbol + + function Unit(name::Symbol, prefix::Symbol) + _unit_symbol(name) + _prefix_exponent(prefix) + name in (:dimensionless, :radian, :degree, :neper) && prefix !== :base && throw( + ArgumentError("unit :$name does not accept metric prefix :$prefix"), + ) + return new(name, prefix) + end +end + +Unit(name::Symbol) = Unit(name, :base) +Unit(; name::Symbol = :dimensionless, prefix::Symbol = :base) = Unit(name, prefix) + +""" +$(TYPEDEF) + +Describe numerator and denominator units, such as Ω/km. + +$(TYPEDFIELDS) +""" +struct UnitExpr{N <: Tuple, D <: Tuple} + "Numerator units." + numerator::N + "Denominator units." + denominator::D + + function UnitExpr(numerator::N, denominator::D) where {N <: Tuple, D <: Tuple} + all(unit -> unit isa Unit, numerator) || throw( + ArgumentError("unit-expression numerators must contain Unit values"), + ) + all(unit -> unit isa Unit, denominator) || throw( + ArgumentError("unit-expression denominators must contain Unit values"), + ) + return new{N, D}(numerator, denominator) + end +end + +function UnitExpr(; numerator::Tuple = (Unit(),), denominator::Tuple = ()) + UnitExpr(numerator, denominator) +end + +""" +$(TYPEDSIGNATURES) + +Construct a unit expression with one numerator and an optional denominator. + +# Arguments + +- `prefix`: numerator metric prefix. +- `name`: numerator unit name. + +# Keywords + +- `per`: optional `(prefix, name)` denominator. + +# Returns + +- A validated [`UnitExpr`](@ref). +""" +function units( + prefix::Symbol, + name::Symbol; + per::Union{Nothing, Tuple{Symbol, Symbol}} = nothing +) + numerator = (Unit(name, prefix),) + denominator = per === nothing ? () : (Unit(last(per), first(per)),) + return UnitExpr(numerator, denominator) +end + +""" +Return the SI symbol, including its metric prefix, for `unit`. +""" +label(unit::Unit) = string(_prefix_symbol(unit.prefix), _unit_symbol(unit.name)) + +function _unit_side_label(side::Tuple) + labels = map(label, filter(unit -> unit.name !== :dimensionless, side)) + return join(labels, ".") +end + +""" +$(TYPEDSIGNATURES) + +Format a unit expression using SI symbols. +""" +function label(expression::UnitExpr) + numerator = _unit_side_label(expression.numerator) + denominator = _unit_side_label(expression.denominator) + isempty(denominator) && return numerator + isempty(numerator) && (numerator = "1") + denominator = length(filter(unit -> unit.name !== :dimensionless, + expression.denominator)) > 1 ? "($denominator)" : denominator + return "$numerator/$denominator" +end + +Base.inv(expression::UnitExpr) = UnitExpr(expression.denominator, expression.numerator) diff --git a/src/uq/UQ.jl b/src/uq/UQ.jl index 25676c488..1b49bea1f 100644 --- a/src/uq/UQ.jl +++ b/src/uq/UQ.jl @@ -1,359 +1,55 @@ -module UQ - -# Export public API -export sample, trial, mc, hist - -# Module-specific dependencies -using ..Commons: BASE_FLOAT -import ..Commons: domain, PhaseDomain, ModalDomain, - LineParamsDomain -using ..ParametricBuilder: - MaterialSpec, PartSpec, CableBuilderSpec, SystemBuilderSpec, AbstractPositionSpec, - PositionSpec, PositionGroupSpec, build, iterate, _spec, determinize -using ..Engine: - EMTFormulation, compute!, LineParameters -using ..DataModel: get_outer_radius -using Measurements: Measurement, measurement, value, uncertainty -using Random, Statistics, DataFrames -using Distributions: - Distributions, ContinuousUnivariateDistribution, Normal, Uniform, cdf, sampler -using StatsBase: fit, Histogram, normalize, quantile, ecdf -using LinearAlgebra - - -# Draw once from a "range-like" spec -# spec :: Number → return as-is -# spec :: AbstractVector → random element (uniform over indices) -# spec :: (lo::Number, hi::Number, n::Int) → given [lo, hi], interpret as ±1σ around μ = (lo+hi)/2, σ = (hi-lo)/2. -# anything iterable → pick a random element -@inline function _rand_in(spec, distribution::Symbol) - if spec isa Number - return spec - - elseif spec isa AbstractVector - @inbounds return spec[rand(1:length(spec))] - - elseif spec isa Tuple && length(spec) == 3 && - spec[1] isa Number && spec[2] isa Number && spec[3] isa Integer - lo, hi = spec[1], spec[2] - # - Given [lo, hi], interpret as ±1σ around μ = (lo+hi)/2, σ = (hi-lo)/2. - lo_f = float(lo) - hi_f = float(hi) - # TODO: handle edge case lo == hi when the nominal value is 0 - # Issue URL: https://github.com/Electa-Git/LineCableModels.jl/issues/31 - @assert hi_f > lo_f "hi must be greater than lo" - μ = (lo_f + hi_f) / 2 - σ = (hi_f - lo_f) / 2 - - if distribution === :normal - # - :normal => Normal(μ, σ). - return rand(Distributions.Normal(μ, σ)) - elseif distribution === :uniform - # - :uniform => Uniform(μ ± √3 σ) so std matches σ. - d = √3 * σ - return rand(Distributions.Uniform(μ - d, μ + d)) - else - throw( - ArgumentError( - "unsupported distribution: $(distribution). Use :uniform or :normal", - ), - ) - end - - else - if Base.iterable(spec) - vals = collect(spec) - @inbounds return vals[rand(1:length(vals))] - end - return spec - end -end - -# Collapse a (spec, pct) pair → (value::Number, pct::Union{Nothing,Number}) -""" - _collapse_pair(sp::Tuple, distribution::Symbol; domain=nothing, max_tries::Int=10_000) - -Collapse a (spec, pct) pair into `(value, pct_value)` by drawing once from the -"range-like" `spec` and `pct` using `_rand_in`. - -If `domain !== nothing`, it must be a tuple `(lo, hi)` where each bound can be -`Real` or `nothing`. The value draw `v` is accepted only if: - - (lo === nothing || v ≥ lo) && (hi === nothing || v ≤ hi) - -Otherwise a new draw is attempted, up to `max_tries`. If no feasible value is -found, an `error` is thrown. - -This gives you generic rejection-sampling with minimal code, suitable for -enforcing physical domains like `(0, Inf)` for resistivity, spacing, thickness, etc. -""" -@inline function _collapse_pair( - sp::Tuple, - distribution::Symbol; - domain::Union{Nothing, Tuple} = nothing, - max_tries::Int = 10_000, -) - spec, pct = sp - - # 1) VALUE SIDE (v): domain only for randomizable specs - v = - if domain === nothing || spec isa Number - # No domain guardrail for scalars: if the user hard-codes nonsense, - # let geometry/physics code blow up later. - _rand_in(spec, distribution) - else - lo, hi = domain - tries = 0 - accepted = nothing - while true - tries += 1 - tries > max_tries && error( - "Unable to draw value in domain $domain from spec=$spec " * - "after $max_tries attempts. Check your range and distribution.", - ) - val = _rand_in(spec, distribution) - if (lo === nothing || val >= lo) && (hi === nothing || val <= hi) - accepted = val - break - end - end - accepted - end - - # 2) PCT SIDE (u): domain only for randomizable pct-specs - u = - pct === nothing ? nothing : - begin - if pct isa Number - # Scalar pct: pass through. If it's garbage, some other validator - # or the physics will scream, not the domain sampler. - _rand_in(pct, distribution) - else - # Range-like pct: enforce 0–100 on the *random draws*. - lo, hi = 0.0, 100.0 - tries = 0 - accepted_pct = nothing - while true - tries += 1 - tries > max_tries && error( - "Unable to draw pct ∈ [0,100] from pct-spec=$pct after $max_tries attempts.", - ) - val = _rand_in(pct, distribution) - if lo <= val <= hi - accepted_pct = val - break - end - end - accepted_pct - end - end - - return (v, u) -end - - -# Collapse PartSpec.args: -# each entry can be: -# - scalar → keep as-is -# - (spec, pct) → collapse to (rand_val, rand_pct) -# Treat an args entry as (spec, pct) only if first element is *not* Integer -@inline function _collapse_args(args::Tuple, distribution::Symbol) - isempty(args) && return () - return tuple( - ( - begin - a = args[i] - if (a isa Tuple) && (length(a) == 2) && !(a[1] isa Integer) - _collapse_pair(a, distribution) - elseif a isa AbstractVector - @inbounds a[rand(1:length(a))] - else - a - end - end for i in eachindex(args) - )..., - ) -end - -# Collapse an entire MaterialSpec by collapsing each (spec, pct) field -@inline function _collapse_material( - ms::MaterialSpec, - distribution::Symbol, -) - return MaterialSpec(; - rho = _collapse_pair(ms.rho, distribution), - eps_r = _collapse_pair(ms.eps_r, distribution), - mu_r = _collapse_pair(ms.mu_r, distribution), - T0 = _collapse_pair(ms.T0, distribution), - alpha = _collapse_pair(ms.alpha, distribution), - ) -end - -# Collapse one PartSpec → singleton PartSpec (no enumerations left) -@inline function _collapse_part(p::PartSpec, distribution::Symbol) - new_dim = _collapse_pair(p.dim, distribution) - new_args = _collapse_args(p.args, distribution) - new_mat = _collapse_material(p.material, distribution) - return PartSpec(p.component, p.part_type, p.n_layers; - dim = new_dim, args = new_args, material = new_mat) -end - -""" - collapse(cbs::CableBuilderSpec; distribution::Symbol = :uniform) -> CableBuilderSpec - -Return a **singleton** `CableBuilderSpec` by collapsing every range-like item -(dims, args, and material fields) into one random draw using the chosen distribution. - -""" -function collapse( - cbs::CableBuilderSpec; - distribution::Symbol = :normal, -) - parts = PartSpec[_collapse_part(p, distribution) for p in cbs.parts] - return CableBuilderSpec(cbs.cable_id, parts, cbs.nominal) -end - -""" - sample(cbs::CableBuilderSpec; distribution::Symbol = :uniform) -> DataModel.CableDesign - -Collapse ranges in `cbs` using `collapse` and build **one** cable design. -Useful for Monte Carlo style sampling where each call yields a new realization. -""" -function sample( - cbs::CableBuilderSpec; - distribution::Symbol = :normal, -) - scbs = collapse(cbs; distribution = distribution) - designs = build(scbs) # with singleton choices, this yields length == 1 - @assert length(designs) == 1 - return designs[1] -end - -""" - _collapse_position(p::AbstractPositionSpec, distribution) -> PositionSpec or PositionGroupSpec - -Collapse the uncertainty-bearing fields of a position specification. -No geometry is touched — grouped formations remain lazy, but their spacing -is collapsed to a concrete `(value, pct)` pair. -""" -# --- collapse for single positions ------------------------------------------------- -function _collapse_position(p::PositionSpec, distribution::Symbol) - dxc = _collapse_pair(_spec(p.dx), distribution) - dyc = _collapse_pair(_spec(p.dy), distribution) - return PositionSpec( - p.x0, - p.y0, - dxc, - dyc, - p.conn, - ) -end -# --- collapse for grouped formations ---------------------------------------------- -function _collapse_position( - g::PositionGroupSpec, - distribution::Symbol, - d_min::Real; - max_tries::Int = 10_000, -) - # Physical constraint: d ≥ 2*R_out - dspec_collapsed = _collapse_pair( - g.d, - distribution; - domain = (d_min, nothing), # (lo, hi), hi unconstrained - max_tries = max_tries, - ) - # value is guaranteed ≥ d_min here - - return PositionGroupSpec( - g.arrangement, - g.n, - g.anchor, - dspec_collapsed, - g.conn, - ) -end - """ - collapse(sbs::SystemBuilderSpec; distribution::Symbol = :uniform) -> SystemBuilderSpec - -Collapse ranges in a `SystemBuilderSpec` using existing helpers. + LineCableModels.UQ -Rules: -- Anchors `x, y` are numbers → pass through unchanged. -- `pos` are ((nom_range), (unc_range)) → `_collapse_position(..., distribution)`. -- `length`, `temperature` → `_collapse_pair(_spec(...), distribution)`. -- Earth fields (`rho`, `eps_r`, `mu_r`, `t`) → `_collapse_pair(_spec(...), distribution)`. -- Inner `builder` → `collapse(builder; distribution)`. +Define direct linear propagation, conditional Monte Carlo sampling, retained +statistics, and uncertainty-result presentation. """ -function collapse( - sbs::SystemBuilderSpec; - distribution::Symbol = :normal, -) - # 1) collapse cable builder (dims, mats, etc.) - scbs = collapse(sbs.builder; distribution = distribution) - - # 2) build the *single* cable design and get its outer radius - designs = build(scbs) - @assert length(designs) == 1 "Collapsed CableBuilderSpec should yield exactly one design" - des = designs[1] - r_out = get_outer_radius(des) - - # 3) collapse positions: singles are collapsed generically, - # grouped formations are collapsed with geometry-aware rejection. - pos = Vector{AbstractPositionSpec}(undef, length(sbs.positions)) - for (i, p) in enumerate(sbs.positions) - if p isa PositionSpec - pos[i] = _collapse_position(p, distribution) - elseif p isa PositionGroupSpec - pos[i] = _collapse_position(p, distribution, 2*r_out) - else - error("Unsupported position type in SystemBuilderSpec: $(typeof(p))") - end - end - - # 4) system-level scalars as before - L = _collapse_pair(_spec(sbs.length), distribution) - T = _collapse_pair(_spec(sbs.temperature), distribution) - - er = sbs.earth - ρ = _collapse_pair(_spec(er.rho), distribution) - ε = _collapse_pair(_spec(er.eps_r), distribution) - μ = _collapse_pair(_spec(er.mu_r), distribution) - t = _collapse_pair(_spec(er.t), distribution) - earth = typeof(er)(; rho = ρ, eps_r = ε, mu_r = μ, t = t) - - return typeof(sbs)( - sbs.system_id, - scbs, - pos; - length = L, - temperature = T, - earth = earth, - f = sbs.frequencies, - ) -end - +module UQ +import ..LineCableModels: description, formula_id + +export LinearError, MonteCarlo, LinearErrorResult, MonteCarloResult +export SampleSummary, HistogramDensity +export statistics, samples, histograms, uncertain +export root_seed, point_seed, trial_count +export confidence, cdf_tolerance, sampling_distribution +export cumulative_probability, quantile_pairs + +import Random +import Statistics +import ..LineCableModels +using DocStringExtensions: TYPEDEF, TYPEDFIELDS, TYPEDSIGNATURES +import ..LineCableModels: basis, frequencies, R, L, C, nominal, uncertainty +import ..LineCableModels: points, realize, realize_arguments, Gridpoint, materialize, UncertainValue +import ..LineCableModels: verbosity, VerbosityLogger +import Logging +import ..DataModel +import ..Engine +import ..Commons +import ..Commons: compute, computation_options, computation_details, details, + observe, observables, validate, + detach, request_identity, request_indices, + observation_indices, observation_resolution, observation_request, + initialize_buffers +using ..Commons: request_quantity +import ..ParametricBuilder +import ..ParametricBuilder: traverse +import ..Units +import ..TextDisplay +using ..Commons: + AbstractFormulation, AbstractUncertaintyResult, + ComputationOptions, ComputationDetails +using ..ParametricBuilder: + ParametricProblem -""" - sample(sbs::SystemBuilderSpec; distribution::Symbol = :uniform) +include("formulations.jl") +include("statistics.jl") +include("results.jl") +include("base.jl") +include("observations.jl") +include("observedresult.jl") +include("comparisons.jl") +include("linearerror.jl") +include("montecarlo/compute.jl") +include("textdisplay.jl") -Collapse ranges in `sbs` and produce one `LineParametersProblem`. -""" -function sample( - sbs::SystemBuilderSpec; - distribution::Symbol = :normal, -) - ss = collapse(sbs; distribution = distribution) - ch = iterate(ss) - return take!(ch) end - -include("types.jl") -include("distributions.jl") -include("montecarlo.jl") -include("dataframe.jl") -include("plot.jl") - -include("plotspecs/mcstatsplotspec.jl") - -end # module UQ diff --git a/src/uq/base.jl b/src/uq/base.jl new file mode 100644 index 000000000..48f830ce6 --- /dev/null +++ b/src/uq/base.jl @@ -0,0 +1,14 @@ +Base.length(value::LinearErrorResult) = length(value.values) +Base.size(value::LinearErrorResult) = (length(value),) +Base.getindex(value::LinearErrorResult, index::Integer) = value.values[index] +Base.iterate(value::LinearErrorResult, state...) = iterate(value.values, state...) +Base.firstindex(value::LinearErrorResult) = firstindex(value.values) +Base.lastindex(value::LinearErrorResult) = lastindex(value.values) + +Base.length(value::MonteCarloResult) = length(value.values) +Base.size(value::MonteCarloResult) = (length(value),) +Base.getindex(value::MonteCarloResult, index::Integer) = value.values[index] +Base.iterate(value::MonteCarloResult, state...) = iterate(value.values, state...) +Base.firstindex(value::MonteCarloResult) = firstindex(value.values) +Base.lastindex(value::MonteCarloResult) = lastindex(value.values) +Base.eachindex(value::Union{MonteCarloResult,LinearErrorResult}) = Base.OneTo(length(value)) diff --git a/src/uq/comparisons.jl b/src/uq/comparisons.jl new file mode 100644 index 000000000..d4a2a4b25 --- /dev/null +++ b/src/uq/comparisons.jl @@ -0,0 +1,56 @@ +""" +$(TYPEDSIGNATURES) + +Compare the selected statistic of two UQ result spaces without pooling points +or trials. `pairing` lists one reference-result pair per result, in any +order. A singleton reference can be broadcast. Multiple references require +explicit pairing. Return RMS results in result order. Physical cutoffs, +frequency bands and normalization are owned by Engine. +""" +function Engine.compare(reference::Union{MonteCarloResult, LinearErrorResult}, + result::Union{MonteCarloResult, LinearErrorResult}, request::Tuple; + pairing = nothing, kwargs...) + identity = request_identity(request) + identity isa Tuple && length(identity) == 3 && first(identity) === statistics || + throw(ArgumentError("UQ comparison requires (statistics, quantity, statistic)")) + isempty(request_indices(request)) || throw(ArgumentError( + "comparison point selection uses pairing; tensor entries are compared individually")) + observation_request(reference, request) + observation_request(result, request) + selected = pairing === nothing ? + (length(reference) == 1 ? [(1, index) for index in eachindex(result)] : + throw(ArgumentError("multiple reference points require explicit pairing"))) : + collect(pairing) + length(selected) == length(result) && + all( + pair -> pair isa Tuple{Integer, Integer} && !(first(pair) isa Bool) && + !(last(pair) isa Bool) && 1 <= first(pair) <= length(reference), + selected) && + sort(last.(selected)) == collect(eachindex(result)) || + throw(ArgumentError("pairing must reference valid points and select each result exactly once")) + sort!(selected; by = last) + for (left_index, right_index) in selected + left, right = reference[left_index], result[right_index] + basis(left) === basis(right) && Engine.domain(left) === Engine.domain(right) || + throw(ArgumentError("UQ point basis or domain differs")) + frequencies(left) == frequencies(right) || + throw(ArgumentError("UQ point frequencies differ")) + left_ports = get(details(left).data, :coordinates, nothing) + right_ports = get(details(right).data, :coordinates, nothing) + left_ports === nothing || right_ports === nothing || left_ports == right_ports || + throw(ArgumentError("UQ point output terminals differ")) + end + return map(selected) do (left_index, right_index) + left=reference[left_index] + a = observe(reference, identity..., left_index) + b = observe(result, identity..., right_index) + error = Engine.compare(a, b, identity[2]; frequencies = frequencies(left), + result_basis = basis(left), kwargs...) + return Engine.RMSError{Base.nonmissingtype(eltype(observe(error, Engine.absolute_error)))}( + observe(error, Engine.absolute_error), observe(error, Engine.relative_error); + details = ComputationDetails(merge(details(error).data, (; request = identity, + estimators=( + reference=reference isa MonteCarloResult ? :empirical : :first_order, + result=result isa MonteCarloResult ? :empirical : :first_order))))) + end +end diff --git a/src/uq/dataframe.jl b/src/uq/dataframe.jl deleted file mode 100644 index bf3423ecf..000000000 --- a/src/uq/dataframe.jl +++ /dev/null @@ -1,45 +0,0 @@ -import DataFrames: DataFrame - -function DataFrame(res::LineParametersMC) - nph, _, nfreq = size(res.stats.R) - dfs = Array{DataFrame, 3}(undef, nph, nph, nfreq) - @inbounds for i in 1:nph, j in 1:nph, k in 1:nfreq - r = res.stats.R[i, j, k] - l = res.stats.L[i, j, k] - c = res.stats.C[i, j, k] - g = res.stats.G[i, j, k] - dfs[i, j, k] = DataFrame( - quantity = ["R", "L", "C", "G"], - mean = [r.mean, l.mean, c.mean, g.mean], - std = [r.std, l.std, c.std, g.std], - min = [r.min, l.min, c.min, g.min], - q05 = [r.q05, l.q05, c.q05, g.q05], - q50 = [r.q50, l.q50, c.q50, g.q50], - q95 = [r.q95, l.q95, c.q95, g.q95], - max = [r.max, l.max, c.max, g.max], - n = [r.n, l.n, c.n, g.n], - conf = [r.conf, l.conf, c.conf, g.conf], - z = [r.z, l.z, c.z, g.z], - ci_half = [r.ci_half, l.ci_half, c.ci_half, g.ci_half], - ci_rel = [r.ci_rel, l.ci_rel, c.ci_rel, g.ci_rel], - ) - end - return dfs -end - -function DataFrame(res::CableDesignMC) - sR, sL, sC = res.stats.R, res.stats.L, res.stats.C - DataFrame( - variable = ["R", "L", "C"], - mean = [sR.mean, sL.mean, sC.mean], - std = [sR.std, sL.std, sC.std], - min = [sR.min, sL.min, sC.min], - q05 = [sR.q05, sL.q05, sC.q05], - q50 = [sR.q50, sL.q50, sC.q50], - q95 = [sR.q95, sL.q95, sC.q95], - max = [sR.max, sL.max, sC.max], - ntrials = fill(sR.n, 3), - ci_half = [sR.ci_half, sL.ci_half, sC.ci_half], - ci_rel = [sR.ci_rel, sL.ci_rel, sC.ci_rel], - ) -end diff --git a/src/uq/distributions.jl b/src/uq/distributions.jl deleted file mode 100644 index 3f532e162..000000000 --- a/src/uq/distributions.jl +++ /dev/null @@ -1,638 +0,0 @@ -@inline function _stored_lineparameter_samples(res::LineParametersMC) - samples = res.samples - samples === nothing && throw( - ArgumentError( - "whole-trial sampling requires stored samples; " * - "rerun mc(...; return_samples=true)", - ), - ) - return samples -end - - -""" - trial(res::LineParametersMC, t::Integer) - -Reconstruct one complete empirical Monte Carlo realization. - -# Arguments - -- `res`: Monte Carlo line-parameter result created with `return_samples=true`. -- `t`: Stored one-based trial index. - -# Returns - -- A `LineParameters` object containing the jointly observed series impedance and - shunt admittance at every matrix entry and frequency. Values are in \\[Ω/m\\] - and \\[S/m\\] when `res` was computed with `per_length=true`, or \\[Ω\\] and \\[S\\] - when it was computed with `per_length=false`. - -# Notes - -The same index `t` is used for every R, L, G, and C coordinate. The result is -therefore one member of the original discrete empirical joint distribution, -without independent marginal resampling or covariance approximation. - -# Errors - -- Throws `ArgumentError` when `res` does not contain raw samples. -- Throws `BoundsError` when `t` is not a stored trial index. -- Throws `DimensionMismatch` when stored sample tensors or frequencies have - inconsistent dimensions. - -# Examples - -```julia -result = mc(spec, formulation; trials=100, return_samples=true) -lp = trial(result, 7) -``` -""" -function trial(res::LineParametersMC, t::Integer) - samples = _stored_lineparameter_samples(res) - sample_size = size(samples.R) - all(size(values) == sample_size for values in (samples.L, samples.G, samples.C)) || - throw(DimensionMismatch("stored R, L, G, and C sample tensors must have equal dimensions")) - sample_size[1] == sample_size[2] || - throw(DimensionMismatch("stored line-parameter sample matrices must be square")) - sample_size[3] == length(res.f) || - throw(DimensionMismatch("stored sample and frequency dimensions must agree")) - t in axes(samples.R, 4) || throw(BoundsError(res, t)) - - nph, _, nfreq, _ = sample_size - U = eltype(samples.R) - Z = Array{Complex{U}, 3}(undef, nph, nph, nfreq) - Y = Array{Complex{U}, 3}(undef, nph, nph, nfreq) - @inbounds for j1 in 1:nph, j2 in 1:nph, k in 1:nfreq - ω = 2π * res.f[k] - Z[j1, j2, k] = samples.R[j1, j2, k, t] + im * ω * samples.L[j1, j2, k, t] - Y[j1, j2, k] = samples.G[j1, j2, k, t] + im * ω * samples.C[j1, j2, k, t] - end - return LineParameters(domain(res), Z, Y, res.f) -end - - -""" - rand(rng::AbstractRNG, res::LineParametersMC) - rand(res::LineParametersMC) - -Draw one complete empirical Monte Carlo realization uniformly. - -# Arguments - -- `rng`: Random-number generator used to select the stored trial index. -- `res`: Monte Carlo line-parameter result created with `return_samples=true`. - -# Returns - -- A `LineParameters` object reconstructed from one uniformly selected stored - trial. - -# Notes - -Exactly one trial index is drawn per returned `LineParameters`; that index is -shared across all matrix entries, R/L/G/C components, and frequencies. The -method without `rng` uses `Random.default_rng()`. - -# Errors - -- Throws `ArgumentError` when `res` does not contain raw samples or contains no - stored trials. - -# Examples - -```julia -rng = Random.MersenneTwister(42) -lp = rand(rng, result) -``` - -# See also - -- [`trial`](@ref) -""" -function Base.rand(rng::AbstractRNG, res::LineParametersMC) - samples = _stored_lineparameter_samples(res) - trial_indices = axes(samples.R, 4) - isempty(trial_indices) && throw(ArgumentError("whole-trial sampling requires at least one stored trial")) - return trial(res, rand(rng, trial_indices)) -end - -Base.rand(res::LineParametersMC) = rand(Random.default_rng(), res) - - -""" -Freedman–Diaconis rule to guesstimate number of bins for histogram - -For samples x: - -IQR = interquartile range = q75 - q25 - -bin width -h = 2 * IQR / N^(1/3) -number of bins ~ (max(x) - min(x)) / h -""" -function _auto_nbins(x::AbstractVector{<:Real}; - nbins_min::Int = 10, - nbins_max::Int = 200, -) - n = length(x) - n == 0 && error("Empty sample set.") - - xs = sort(float.(x)) - xmin, xmax = xs[1], xs[end] - span = xmax - xmin - - # degenerate span: all samples equal (or numerically so) - if span <= 0 || !isfinite(span) - return nbins_min - end - - q25 = quantile(xs, 0.25) - q75 = quantile(xs, 0.75) - iqr = q75 - q25 - - # iqr ~ 0 → data essentially degenerate → fallback - if iqr <= 0 || !isfinite(iqr) - return clamp(ceil(Int, sqrt(n)), nbins_min, nbins_max) - end - - h = 2 * iqr / n^(1/3) - - # h tiny or broken → fallback - if h <= 0 || !isfinite(h) - return clamp(ceil(Int, sqrt(n)), nbins_min, nbins_max) - end - - raw = span / h - - # If raw bin count is insane, just clamp **before** converting to Int - if !isfinite(raw) || raw <= nbins_min - return nbins_min - elseif raw >= nbins_max - return nbins_max - else - return ceil(Int, raw) - end -end - -# Build a piecewise-constant PDF from samples -function _pdf_from_hist(x::AbstractVector{<:Real}; nbins::Union{Int, Nothing} = nothing) - n = length(x) - n == 0 && error("Empty sample set.") - - nb = isnothing(nbins) ? _auto_nbins(x) : nbins - - h = fit(Histogram, float.(x); nbins = nb, closed = :left) - edges = collect(h.edges[1]) - widths = diff(edges) - dens = h.weights ./ (n .* widths) - - return LineParametersPDF(edges, dens) # ctor re-normalizes area -end - -# Density at x0 -@inline function (hp::LineParametersPDF)(x0::Real) - i = searchsortedlast(hp.edges, float(x0)) - (i < 1 || i >= length(hp.edges)) && return 0.0 - return hp.dens[i] -end - -""" -Finds the index `i` of the bin `[edges[i], edges[i+1])` that `x` falls into. -Returns 0 if `x` is out of bounds. -Handles the right-most edge `x == edges[end]` correctly. -""" -function _binsearch(d::LineParametersPDF, x::Real) - if x < d.edges[1] || x > d.edges[end] - return 0 # Out of bounds - end - - # Handle the maximum edge case, which searchsortedlast fucks up - if x == d.edges[end] - return length(d.dens) # Belongs to the last bin - end - - # searchsortedlast finds the largest index i s.t. edges[i] <= x - # This is exactly the bin index we need. - i = searchsortedlast(d.edges, x) - - # This should be redundant given the initial check, but belt and suspenders. - (i < 1 || i > length(d.dens)) && return 0 - - return i -end - -""" -Computes the stable integral of x^k over [a, b] -Returns: (b^(k+1) - a^(k+1)) / (k+1) -""" -function _stable_pow_integral(a::T, b::T, k::Int) where {T <: Real} - n = k + 1 - h = b - a # width - - # If width is effectively zero, integral is zero - if h == 0 - return zero(T) - end - - # Use the stable factored form: (b-a)/n * sum(a^j * b^(n-1-j) for j=0..n-1) - # n-1 = k - s = zero(T) - @inbounds for j in 0:k - s += a^j * b^(k - j) - end - - return s * h / n -end - -""" -Computes raw moments m_1...m_K in a *single pass*. -Returns a Vector m where m[k] = E[X^k]. -""" -function _raw_moments(d::LineParametersPDF{T}, K::Int) where {T <: Real} - e = d.edges - dens = d.dens - - # m[k] will hold the k-th raw moment - m = zeros(T, K) - - @inbounds for i in 1:length(dens) - # Skip bins with zero density. - d_i = dens[i] - d_i == 0 && continue - - a = e[i] - b = e[i+1] - - # Calculate all moments 1..K for this bin - for k in 1:K - # This is the numerically stable integral of x^k from a to b, - # which is (b^(k+1) - a^(k+1)) / (k+1). - integral_term = _stable_pow_integral(a, b, k) - - # Add this bin's contribution to the k-th moment - m[k] += d_i * integral_term - end - end - return m -end - -# ───────────────────────────────────────────────────────────────────────────── -# Distributions.jl API -# ───────────────────────────────────────────────────────────────────────────── - -# --- Basic properties --- - -Distributions.minimum(d::LineParametersPDF) = d.edges[1] -Distributions.maximum(d::LineParametersPDF) = d.edges[end] - -function Distributions.insupport(d::LineParametersPDF, x::Real) - # Is it in the bounds? This isn't rocket science. - return d.edges[1] <= x <= d.edges[end] -end - -# --- PDF / LOGPDF --- - -function Distributions.pdf(d::LineParametersPDF{T}, x::Real) where {T} - i = _binsearch(d, x) - # If index is 0 (out of bounds), density is 0. Otherwise, look it up. - return i == 0 ? zero(T) : d.dens[i] -end - -function Distributions.logpdf(d::LineParametersPDF{T}, x::Real) where {T} - p = Distributions.pdf(d, x) - # Don't try to log(0). It's -Inf. - return p > 0 ? log(p) : -Inf -end - -# --- CDF (Cumulative Distribution Function) --- - -function Distributions.cdf(d::LineParametersPDF{T}, x::Real) where {T} - if x < Distributions.minimum(d) - return zero(T) - end - if x >= Distributions.maximum(d) - return one(T) - end - - i_x = _binsearch(d, x) # The bin that x is currently in - widths = diff(d.edges) - - # 1. Sum the area of all *full* bins before the current one - area_full_bins = sum( - (d.dens[j] * widths[j] for j in 1:(i_x-1)); - init = zero(T), - ) - - # 2. Add the partial area of the current bin - area_partial_bin = d.dens[i_x] * (x - d.edges[i_x]) - - return area_full_bins + area_partial_bin -end - -# --- Quantile (Inverse CDF) --- - -""" -Pre-calculates cumulative probabilities for efficient sampling. -This is what `sampler` should actually be doing. -""" -struct LineParametersPDFSampler{T <: Real} <: - Distributions.Sampleable{Distributions.Univariate, Distributions.Continuous} - d::LineParametersPDF{T} - cum_probs::Vector{T} # Cumulative probability at the *end* of each bin -end - -function Distributions.sampler(d::LineParametersPDF) - widths = diff(d.edges) - bin_probs = d.dens .* widths - cum_probs = cumsum(bin_probs) - - # Ensure the last value is exactly 1.0 to avoid float rounding - # errors when sampling u=1.0 - cum_probs[end] = 1.0 - - return LineParametersPDFSampler(d, cum_probs) -end - -function Distributions.quantile(s::LineParametersPDFSampler{T}, q::Real) where {T} - # This is the actual inverse-CDF logic. - d = s.d - - if q <= 0 - return Distributions.minimum(d) - end - if q >= 1 - return Distributions.maximum(d) - end - - # Find the first bin `i` where the cumulative probability >= q - i = findfirst(p -> p >= q, s.cum_probs) - # This should never be nothing thanks to the q >= 1 check, but... - if i === nothing - return Distributions.maximum(d) - end - - # Get probability accumulated *before* this bin - q_prev = (i == 1) ? zero(T) : s.cum_probs[i-1] - - # How much more probability do we need *from this bin*? - q_needed = q - q_prev - - # If density is zero, any width is fine, just return the start edge. - # Avoids a 0/0 NaN. - if d.dens[i] <= 0 - return d.edges[i] - end - - # Calculate the partial width into this bin - # width = probability / density - width_needed = q_needed / d.dens[i] - - return d.edges[i] + width_needed -end - -# `quantile(d, q)` will be slow as it builds the sampler *every time*. -# This is the price you pay for a stateless distribution object. -function Distributions.quantile(d::LineParametersPDF, q::Real) - return Distributions.quantile(Distributions.sampler(d), q) -end - -# --- RAND (Random Sampling) --- - -# Use the efficient sampler-based method -function Base.rand(rng::AbstractRNG, s::LineParametersPDFSampler) - # 1. Draw a uniform random number between 0 and 1 - u = rand(rng) - - # 2. Find which bin 'u' falls into using binary search - idx = searchsortedfirst(s.cum_probs, u) - - # 3. Get the cumulative probability up to the start of this bin - prev_cum_prob = idx == 1 ? zero(eltype(s.cum_probs)) : s.cum_probs[idx-1] - - # 4. Calculate how far 'u' is into this specific bin (as a fraction from 0 to 1) - prob_in_bin = s.cum_probs[idx] - prev_cum_prob - fraction = (u - prev_cum_prob) / prob_in_bin - - # 5. Interpolate between the bin edges to get the continuous value - left_edge = s.d.edges[idx] - right_edge = s.d.edges[idx+1] - - return left_edge + fraction * (right_edge - left_edge) -end - -# This one will be called if you just do `rand(d)` -function Base.rand(rng::AbstractRNG, d::LineParametersPDF) - # This is inefficient as fuck. It builds the sampler on every. single. draw. - # But it's what the Distributions.jl API expects as a fallback. - # Use `rand(rng, sampler(d))` for batch sampling. - s = Distributions.sampler(d) - return Base.rand(rng, s) -end - -# Get all moments up to k, then return the k-th -Distributions.moment(d::LineParametersPDF, k::Integer) = _raw_moments(d, k)[k] - -function Distributions.mean(d::LineParametersPDF{T}) where {T} - # E[X] = ∫ x * p(x) dx - return Distributions.moment(d, 1) -end - -function Distributions.var(d::LineParametersPDF{T}) where {T} - # Var(X) = E[X^2] - (E[X])^2 - # E[X^2] = ∫ x^2 * p(x) dx - # For bin i, integral is d.dens[i] * ∫(from e_i to e_{i+1}) x^2 dx - m = _raw_moments(d, 2) - m1 = m[1] - m2 = m[2] - - v = m2 - m1^2 - # Handle floating point noise. Variance cannot be negative. - return v < 0 ? zero(T) : v -end - -Distributions.std(d::LineParametersPDF) = sqrt(Distributions.var(d)) - -function Distributions.skewness(d::LineParametersPDF{T}) where {T} - m = _raw_moments(d, 3) - m1, m2, m3 = m[1], m[2], m[3] - - μ = m1 - μ2 = m2 - μ^2 # Variance - - # Degenerate case: variance is zero. Return NaN. - if μ2 <= eps(T) # Use machine epsilon for float comparison - return T(NaN) - end - - μ3 = m3 - 3*μ*m2 + 2*μ^3 - return μ3 / μ2^(3/2) -end - -function Distributions.kurtosis(d::LineParametersPDF{T}) where {T} - # Pass `true` for excess kurtosis (subtracts 3) - return Distributions.kurtosis(d, true) -end - -function Distributions.kurtosis(d::LineParametersPDF{T}, excess::Bool) where {T} - m = _raw_moments(d, 4) - m1, m2, m3, m4 = m[1], m[2], m[3], m[4] - - μ = m1 - μ2 = m2 - μ^2 # Variance - - # Degenerate case: variance is zero. Return NaN. - if μ2 <= eps(T) - return T(NaN) - end - - μ4 = m4 - 4*μ*m3 + 6*μ^2*m2 - 3*μ^4 - - kurt = μ4 / μ2^2 - return excess ? (kurt - 3) : kurt -end - -function Distributions.mode(d::LineParametersPDF) - # Returns *a* mode. The distribution is multi-modal - # if the max density spans multiple (or disjoint) bins. - # We'll just return the midpoint of the *first* bin with max density. - - max_dens, i = findmax(d.dens) - return (d.edges[i] + d.edges[i+1]) / 2 -end - -function Distributions.modes(d::LineParametersPDF{T}) where {T} - # The "modes" are technically *intervals*, not points. - # This is a pain in the ass. - # We'll just return the midpoints of all bins with max density. - - max_dens = maximum(d.dens) - # Find all bins that are numerically close to the max - indices = findall(p -> p ≈ max_dens, d.dens) - - return [(d.edges[i] + d.edges[i+1]) / 2 for i in indices] -end - -""" -entropy(d::LineParametersPDF) - -Calculate the differential entropy (base e). -H(X) = - ∫ f(x) * log(f(x)) dx - = - Σ ∫_{e_i}^{e_{i+1}} [d_i * log(d_i)] dx - = - Σ [d_i * log(d_i) * w_i] - = - Σ [p_i * log(d_i)] -where p_i = d_i * w_i is the probability mass of bin i. -""" -function Distributions.entropy(d::LineParametersPDF{T}) where {T} - acc = zero(T) - widths = diff(d.edges) - - @inbounds for i in 1:length(d.dens) - dens_i = d.dens[i] - - # If density is 0, (f(x) * log(f(x))) -> 0. - # So we just skip the bin. - if dens_i > 0 - width_i = widths[i] - prob_mass_i = dens_i * width_i - - # This is base e (natural log) - acc -= prob_mass_i * log(dens_i) - end - end - return acc -end - -""" -entropy(d::LineParametersPDF, b::Real) - -Calculate the differential entropy with a specified base b. -H_b(X) = H_e(X) / log(b) -""" -function Distributions.entropy(d::LineParametersPDF, b::Real) - (b > 0 && b != 1) || - throw(ArgumentError("Entropy base must satisfy b > 0 and b ≠ 1, got b = $b")) - - # Just convert the base e entropy. - return Distributions.entropy(d) / log(b) -end - - -# ───────────────────────────────────────────────────────────────────────────── -# Moment Generating & Characteristic Functions -# ───────────────────────────────────────────────────────────────────────────── - -""" -mgf(d::LineParametersPDF, t::Real) - -Moment Generating Function -M(t) = E[e^(tX)] = ∫ e^(tx) * f(x) dx - = Σ ∫_{e_i}^{e_{i+1}} [d_i * e^(tx)] dx - = Σ d_i * [e^(tx) / t]_{e_i}^{e_{i+1}} - = Σ d_i/t * (e^(t*e_{i+1}) - e^(t*e_i)) - -This is numerically catastrophic for t -> 0. -We rewrite: -Term_i = d_i * e^(t*e_i) * (e^(t*(e_{i+1}-e_i)) - 1) / t -Let w_i = e_{i+1} - e_i. -Term_i = d_i * e^(t*e_i) * w_i * (e^(t*w_i) - 1) / (t*w_i) -Term_i = d_i * e^(t*e_i) * w_i * exprel(t*w_i) - -`Base.Math.exprel(x)` is the numerically stable (e^x - 1) / x. -""" -function Distributions.mgf(d::LineParametersPDF{T}, t::Real) where {T} - t == 0 && return one(T) # MGF(0) = 1 - - acc = zero(T) - - @inbounds for i in 1:length(d.dens) - dens_i = d.dens[i] - dens_i == 0 && continue - - a = d.edges[i] - b = d.edges[i+1] - w = b - a - - # This is the argument to exprel: z = t*w - z = t * w - - # Calculate (e^z - 1) / z, handling z=0 - # This is the stable implementation - exprel_z = iszero(z) ? one(z) : expm1(z) / z - - # Term_i = d_i * e^(t*a) * w_i * exprel(t*w_i) - acc += dens_i * exp(t * a) * w * exprel_z - end - return acc -end - - -""" -cf(d::LineParametersPDF, t::Real) - -Characteristic Function -ϕ(t) = E[e^(itX)] = MGF(it) - -This is the exact same derivation as the MGF, just -substituting `t` with `it`. -""" -function Distributions.cf(d::LineParametersPDF{T}, t::Real) where {T} - t == 0 && return one(Complex{T}) - - acc = zero(Complex{T}) - - @inbounds for i in 1:length(d.dens) - dens_i = d.dens[i] - dens_i == 0 && continue - - a = d.edges[i] - b = d.edges[i+1] - w = b - a - - z = im * t * w - - exprel_z = iszero(z) ? one(z) : expm1(z) / z - - acc += dens_i * exp(im * t * a) * w * exprel_z - end - return acc -end diff --git a/src/uq/formulations.jl b/src/uq/formulations.jl new file mode 100644 index 000000000..3bfd48a98 --- /dev/null +++ b/src/uq/formulations.jl @@ -0,0 +1,271 @@ +""" +$(TYPEDEF) + +Select direct linear uncertainty propagation with `inner`. + +Shared inputs must be supplied once to a joint `Gridspace` builder, which +derives the dependent geometry. Propagation retains those correlations and +differentiates continuous geometry at the nominal design, including bounded +power-cell compaction. Strand counts and clipping topology are nominal discrete +choices. A smooth linear model cannot describe a study across such a transition. +First-order moments need not equal nonlinear Monte Carlo moments. + +$(TYPEDFIELDS) +""" +struct LinearError{F <: AbstractFormulation, O <: ComputationOptions} <: AbstractFormulation + "Formulation used for each materialized problem." + inner::F + "Supplemental-output retention options owned by this propagation." + options::O + + function LinearError(inner::AbstractFormulation, options::ComputationOptions) + validate(inner, LinearError) + normalized = computation_options(LinearError, options) + return new{typeof(inner), typeof(normalized)}(inner, normalized) + end +end + +function computation_options( + ::Type{LinearError}, + record::ComputationOptions +)::ComputationOptions + options = record.data + unknown = filter(key -> key !== :retain_details, keys(options)) + isempty(unknown) || throw(ArgumentError( + "unknown LinearError computation options: $(sort!(collect(unknown)))", + )) + normalized = merge((retain_details = false,), options) + normalized.retain_details isa Bool || throw(ArgumentError( + "LinearError retain_details must be Bool", + )) + return ComputationOptions(; retain_details = normalized.retain_details) +end + +function LinearError( + inner::F; + options::Union{NamedTuple,ComputationOptions} = ComputationOptions() +) where {F <: AbstractFormulation} + options = options isa NamedTuple ? ComputationOptions(options) : options + return LinearError(inner, options) +end + +function Base.NamedTuple(value::LinearError) + return (kind = :linear_error, inner = NamedTuple(value.inner), options = value.options.data) +end + +"""Identify first-order uncertainty propagation without inspecting its inner owner.""" +description(::Type{<:LinearError}; compact::Bool=false) = "LEP" +description(::LinearError; compact::Bool=false) = description(LinearError;compact) +formula_id(::Type{<:LinearError}) = :LinearError +formula_id(::LinearError) = :LinearError +computation_options(value::LinearError) = value.options +function Base.pairs(value::LinearError; quantity = nothing) + pairs(LinearError, (inner = value.inner, options = value.options.data); quantity) +end + +""" +$(TYPEDEF) + +Select conditional Monte Carlo propagation over a +[`ParametricProblem`](@ref). Randomness is local and reproducible when `seed` +is supplied. Computation option `on_error=:fail` propagates every exception. +`on_error=:resample` rejects only realizations that raise `DomainError` and retains +their sampled arguments and error summaries. Sampling continues until the requested +number of successful trials is obtained or `max_failures` is reached. Resampling +mode requires `retain_details=true` and estimates the output distribution +conditional on successful problem construction and computation. + +Load `Measurements` before computation. Aggregation stores marginal +uncertainty-bearing cores from accepted sample means and sample standard +deviations, not histogram bins or standard errors of the mean. This payload is independent of sampling and histogram retention. Joint output correlations are +not retained by the marginal surrogate. Access and transport reuse the stored +uncertainty-source identities. + +Line-system construction enforces exterior clearance on every realization. +When Measurements is loaded, the propagated clearance reserve is computed +before sampling and retained for every draw. Adjusted placements +describe a clearance-constrained system. Adjustments emit one summary per +parameter point, not one warning per trial. Retained details include their +count and maximum displacement \\[m\\]. Invalid dimensions and infeasible +internal cable constructions remain errors subject to `on_error`. + +Use one joint `Gridspace` builder for dependent internal dimensions. Derive +stack geometric boundaries and wire placements from its shared inputs. Repeating a Grid +as separate source arguments creates independent draws. The default normal +law has unbounded support: marginal means and standard uncertainties cannot +guarantee feasible geometry. `distribution=:uniform` instead has support +`nominal ± sqrt(3)*sigma` for each primitive descriptor. The builder must map +the complete joint support to feasible designs. Select this law only when the +uniform distribution is a justified statistical assumption. Changing the +marginal law neither repairs the geometry nor supplies a correlation. + +$(TYPEDFIELDS) +""" +struct MonteCarlo{F <: AbstractFormulation, O <: ComputationOptions} <: + AbstractFormulation + "Formulation used for each sampled problem." + inner::F + "Normalized sampling, error-handling and supplemental-output computation options." + options::O + + function MonteCarlo(inner::AbstractFormulation, options::ComputationOptions) + validate(inner, MonteCarlo) + normalized = computation_options(MonteCarlo, options) + return new{typeof(inner), typeof(normalized)}(inner, normalized) + end +end + +# Uncertainty propagation accepts an inner formulation whose shunt model is fixed. +validate(inner::AbstractFormulation, ::Type{<:Union{LinearError, MonteCarlo}}) = inner +function validate(inner::Union{Engine.LineParametersFormulation, Engine.CableConstantsFormulation}, + ::Type{<:Union{LinearError, MonteCarlo}}) + selected = inner.methods.shunt_model + if selected isa Engine.ShuntModel.Formula{:boundary} && selected.parameters.fallback !== :error + throw(ArgumentError("uncertainty propagation requires a fixed shunt model; select strict :boundary or :equivalent, without automatic fallback")) + end + return inner +end + +"""Identify Monte Carlo propagation without sampling or configuring a solver.""" +description(::Type{<:MonteCarlo}; compact::Bool=false) = "Monte Carlo" +description(::MonteCarlo; compact::Bool=false) = description(MonteCarlo;compact) +description(::Type{<:LinearError},::Val{:representation};compact::Bool=false) = + "dependency-preserving uncertainty" +description(::Type{<:MonteCarlo},::Val{:representation};compact::Bool=false) = + "marginal mean ± std" + +# Capture display annotations separately from the estimator and representation IDs +# used by scientific grouping. Both names remain supplied by their UQ owner. +function _uncertainty_descriptions(owner) + return (estimator=(name="uncertainty estimator",unit="",text=description(owner;compact=true)), + representation=(name="uncertainty representation",unit="", + text=description(owner,Val(:representation);compact=true))) +end +formula_id(::Type{<:MonteCarlo}) = :MonteCarlo +formula_id(::MonteCarlo) = :MonteCarlo +computation_options(value::MonteCarlo) = value.options +function Base.pairs(value::MonteCarlo; quantity = nothing) + pairs(MonteCarlo, (inner = value.inner, options = value.options.data); quantity) +end +function Base.pairs(owner::Type{<:Union{MonteCarlo, LinearError}}, retained::NamedTuple; quantity = nothing) + entries=Pair{Tuple, Any}[(owner, ()) => (owner => retained.options)] + append!(entries, pairs(retained.inner; quantity)) + return entries +end +description(::Type{<:Union{MonteCarlo, LinearError}}, ::Val{:inner}; compact::Bool=false) = "inner method" + +function computation_options( + ::Type{MonteCarlo}, + record::ComputationOptions +)::ComputationOptions + options = record.data + defaults = ( + trials = nothing, confidence = 0.95, cdf_tol = 0.02, + distribution = :normal, seed = nothing, + return_samples = false, return_histograms = false, bins = nothing, + retain_details = false, on_error = :fail, max_failures = 100 + ) + unknown = filter(key -> key ∉ keys(defaults), keys(options)) + isempty(unknown) || throw(ArgumentError( + "unknown MonteCarlo computation options: $(sort!(collect(unknown)))", + )) + normalized = merge(defaults, options) + for key in (:trials, :bins, :max_failures) + value = getproperty(normalized, key) + key !== :max_failures && value === nothing && continue + value isa Integer && !(value isa Bool) && 0 < value <= typemax(Int) || + throw(ArgumentError("MonteCarlo $key must be a positive integer representable as Int")) + end + for key in (:confidence, :cdf_tol) + value = getproperty(normalized, key) + value isa Real && !(value isa Bool) && 0 < value < 1 && + 0 < Float64(value) < 1 || throw(ArgumentError( + "MonteCarlo $key must lie strictly between zero and one as Float64", + )) + end + for key in (:return_samples, :return_histograms, :retain_details) + getproperty(normalized, key) isa Bool || throw(ArgumentError( + "MonteCarlo $key must be Bool", + )) + end + seed = normalized.seed + seed === nothing || + (seed isa Integer && !(seed isa Bool) && 0 <= seed <= typemax(UInt64)) || + throw(ArgumentError("MonteCarlo seed must be a nonnegative integer representable as UInt64")) + distribution = normalized.distribution + distribution isa Symbol && distribution ∉ (:normal, :uniform) && + throw(ArgumentError( + "unsupported distribution $(repr(distribution)); expected :normal, :uniform, a sampler function, or an extension-supported distribution", + )) + normalized.on_error === :fail || normalized.on_error === :resample || + throw(ArgumentError( + "MonteCarlo on_error must be :fail or :resample", + )) + normalized.on_error === :resample && !normalized.retain_details && + throw( + ArgumentError( + "MonteCarlo on_error=:resample requires retain_details=true", + ), + ) + return ComputationOptions(; + trials = normalized.trials === nothing ? nothing : Int(normalized.trials), + confidence = Float64(normalized.confidence), + cdf_tol = Float64(normalized.cdf_tol), + distribution = distribution, + seed = seed === nothing ? nothing : UInt64(seed), + return_samples = normalized.return_samples, + return_histograms = normalized.return_histograms, + bins = normalized.bins === nothing ? nothing : Int(normalized.bins), + retain_details = normalized.retain_details, + on_error = normalized.on_error, + max_failures = Int(normalized.max_failures) + ) +end + +""" +$(TYPEDSIGNATURES) + +Construct a Monte Carlo formulation with execution controls supplied as an +ordinary `options` named tuple or as keyword shorthand. Both forms are +normalized by `computation_options(MonteCarlo, options)`. A key supplied in +both places is an error. + +# Arguments + +- `inner`: formulation used for each sampled problem. + +# Keywords + +`options=(;)` accepts these controls. Additional keywords use +the same names and are merged into `options` before normalization. + +- `trials=nothing`: positive accepted-trial count, or DKW sizing when omitted. +- `confidence=0.95`: simultaneous empirical-CDF confidence [dimensionless], in `(0, 1)`. +- `cdf_tol=0.02`: maximum empirical-CDF deviation for DKW sizing [dimensionless], in `(0, 1)`. +- `distribution=:normal`: `:normal`, `:uniform`, a sampler function, or an extension-supported distribution. +- `seed=nothing`: nonnegative root seed representable as `UInt64`, or fresh randomness. +- `return_samples=false`: retain joint samples. +- `return_histograms=false`: retain marginal histogram densities. +- `bins=nothing`: positive histogram bin count, or automatic binning. +- `retain_details=false`: retain accepted-trial details and failure diagnostics. +- `on_error=:fail`: propagate exceptions. `:resample` rejects `DomainError` realizations and requires `retain_details=true`. +- `max_failures=100`: positive maximum rejected-trial count per parameter point. + +# Returns + +- A `MonteCarlo` formulation storing normalized `ComputationOptions` in `options`. + The concrete payload type retains the sampler type. +""" +function MonteCarlo(inner::AbstractFormulation; options::Union{NamedTuple,ComputationOptions} = ComputationOptions(), kwargs...) + options = options isa NamedTuple ? ComputationOptions(options) : options + supplied = (; kwargs...) + duplicates = filter(key -> haskey(options.data, key), keys(supplied)) + isempty(duplicates) || throw(ArgumentError( + "MonteCarlo computation options supplied both as keywords and in options: $(collect(duplicates))", + )) + return MonteCarlo(inner, isempty(supplied) ? options : ComputationOptions(merge(options.data, supplied))) +end + +function Base.NamedTuple(value::MonteCarlo) + return (kind = :monte_carlo, inner = NamedTuple(value.inner), options = value.options.data) +end diff --git a/src/uq/linearerror.jl b/src/uq/linearerror.jl new file mode 100644 index 000000000..eb9ffd356 --- /dev/null +++ b/src/uq/linearerror.jl @@ -0,0 +1,26 @@ +function compute(problem::ParametricProblem, formulation::LinearError) + levels = verbosity(get(problem.options.data, :verbosity, (default = 0,))) + progress = problem.space isa + LineCableModels.Gridspace{<:Engine.LineParametersProblem} && + get(levels, :progress, levels.default) > 0 + logger = haskey(problem.options.data, :verbosity) ? + VerbosityLogger(Logging.current_logger(), levels) : Logging.current_logger() + return Logging.with_logger(logger) do + started = progress ? time_ns() : UInt64(0) + progress && + @info "Linear error computation started" _group=:progress problems=length(problem.space) + traversed = traverse(problem, formulation) + values=map(traversed.values) do value + id=get(Commons.observation_gridpoint(value), :id, nothing) + Engine.retain_gridpoint(value, id; + fields = ( + uncertainty = ( + estimator = :first_order, representation = :dependency_preserving), + uncertainty_descriptions = _uncertainty_descriptions(LinearError))) + end + result = LinearErrorResult(formulation, values, traversed.details) + progress && + @info "Linear error computation completed successfully" _group=:progress completed=length(result) total=length(result) elapsed_seconds=(time_ns()-started)*1e-9 + result + end +end diff --git a/src/uq/montecarlo.jl b/src/uq/montecarlo.jl deleted file mode 100644 index fef1cbdac..000000000 --- a/src/uq/montecarlo.jl +++ /dev/null @@ -1,466 +0,0 @@ -""" - mc(cbs::CableBuilderSpec; trials=nothing, distribution=:normal, seed=nothing, - trial_sampler=nothing, conf=0.95, tol=0.02, print_step=1000, - return_samples=false, return_pdf=false, nbins=nothing) - -Propagate cable-design uncertainty by Monte Carlo sampling. - -# Arguments - -- `cbs`: Cable-builder specification containing the uncertain primitives. -- `trials`: Number of trials, or `nothing` to size the run with the DKW bound. -- `distribution`: Primitive sampling law, either `:normal` or `:uniform`. -- `seed`: Random-number seed, or `nothing` to preserve the active RNG state. -- `trial_sampler`: Optional callback called as - `trial_sampler(deterministic_spec, trial_index, distribution)`. It must return - a sampled cable design accepted by `DataFrame(design, :baseparams)`. -- `conf`: Confidence level used for DKW sizing and mean confidence intervals. -- `tol`: DKW absolute CDF tolerance when `trials === nothing`. -- `print_step`: Number of trials between progress messages. -- `return_samples`: Retain the scalar R, L, and C trial arrays when `true`. -- `return_pdf`: Construct histogram-based PDFs when `true`. -- `nbins`: Histogram bin count, or `nothing` for automatic selection. - -# Returns - -- A `CableDesignMC` containing R, L, and C statistics and Measurements.jl values. - -# Notes - -`trial_sampler` permits callers to impose shared or correlated primitive draws. -The callback receives one-based trial indices and is invoked exactly once per -trial. Its result bypasses the package's default independent primitive sampler. -""" -function mc(cbs::CableBuilderSpec; - trials::Union{Int, Nothing} = nothing, - distribution::Symbol = :normal, - seed::Union{Int, Nothing} = nothing, - trial_sampler::Union{Nothing, Function} = nothing, - conf::Float64 = 0.95, - tol::Float64 = 0.02, # used only if trials === nothing (DKW sizing) - print_step::Int = 1000, - return_samples::Bool = false, - return_pdf::Bool = false, - nbins::Union{Int, Nothing} = nothing, -) - seed !== nothing && Random.seed!(seed) - z = quantile(Distributions.Normal(), 0.5 + conf/2) - - # 3 scalar observables: R, L, C - M = 3 - ntrials = if trials === nothing - α = 1 - conf - ceil(Int, log(2 * M / α) / (2 * tol^2)) - else - trials - end - - if trials === nothing - @info "mc: estimate number of trials using DKW inequality" scalars = M conf = conf tol = - tol trials = ntrials - end - - @info "mc: starting draws" draws = ntrials conf = conf tol = tol distribution = - (distribution === :uniform ? "Uniform(μ ± √3·σ)" : "Normal(μ, σ)") - - # Base float type — enforced upstream - T = BASE_FLOAT - - μR = Vector{T}(undef, ntrials) - μL = Vector{T}(undef, ntrials) - μC = Vector{T}(undef, ntrials) - cbs_det = determinize(cbs) - - @inline function _draw!(i::Int) - des = if trial_sampler === nothing - sample(cbs_det; distribution = distribution) - else - trial_sampler(cbs_det, i, distribution) - end - params = DataFrame(des, :baseparams).computed # invariant ordering: R, L, C - r = params[1] - l = params[2] - c = params[3] - @inbounds begin - μR[i] = T(r) / 1e3 # ohm/km to ohm/m - μL[i] = T(l) / 1e6 # mH/km to H/m - μC[i] = T(c) / 1e9 # μF/km to F/m - end - return nothing - end - - for i in 1:ntrials - _draw!(i) - (i % print_step == 0) && @info "mc: progress" done = i - end - @info "mc: done" total = ntrials - - # stats kernel (scalar real vector → NamedTuple) - _stats = function (arr::AbstractVector{<:Real}) - m = mean(arr) - s = std(arr) - N = length(arr) - ci = z * s / sqrt(N) - return (mean = m, std = s, min = minimum(arr), - q05 = quantile(arr, 0.05), - q50 = quantile(arr, 0.50), - q95 = quantile(arr, 0.95), - max = maximum(arr), - n = N, - ci_half = ci, - ci_rel = ci / max(abs(m), eps())) - end - - sR = _stats(μR) - sL = _stats(μL) - sC = _stats(μC) - - meas = Measurement{T}[ - measurement(sR.mean, sR.std), - measurement(sL.mean, sL.std), - measurement(sC.mean, sC.std), - ] - - # PDFs (optional) - pdf_nt = nothing - if return_pdf - pdfR = _pdf_from_hist(μR; nbins = nbins) - pdfL = _pdf_from_hist(μL; nbins = nbins) - pdfC = _pdf_from_hist(μC; nbins = nbins) - pdf_nt = (R = pdfR, L = pdfL, C = pdfC) - end - - # Samples as NamedTuple of vectors (R,L,C) or nothing - samples_nt = return_samples ? (R = μR, L = μL, C = μC) : nothing - - return CableDesignMC{T}( - (R = sR, L = sL, C = sC), - pdf_nt, - samples_nt, - meas, - ) -end - - -""" - mc(sbs::SystemBuilderSpec, F::EMTFormulation; trials=nothing, - distribution=:normal, seed=nothing, trial_sampler=nothing, conf=0.95, - tol=0.02, print_step=1000, return_samples=false, return_pdf=false, - per_length=true, nbins=nothing) - -Propagate system uncertainty through a frequency-domain EMT formulation. - -# Arguments - -- `sbs`: Cable-system specification containing the uncertain primitives and - frequency vector \\[Hz\\]. -- `F`: EMT formulation used to compute the series-impedance and shunt-admittance - matrices. -- `trials`: Number of trials, or `nothing` to size the run with the DKW bound. -- `distribution`: Primitive sampling law, either `:normal` or `:uniform`. -- `seed`: Random-number seed, or `nothing` to preserve the active RNG state. -- `trial_sampler`: Optional callback called as - `trial_sampler(deterministic_spec, trial_index, distribution)`. It must return - a sampled system specification accepted by `compute!`. -- `conf`: Confidence level used for DKW sizing and mean confidence intervals. -- `tol`: DKW absolute CDF tolerance when `trials === nothing`. -- `print_step`: Number of trials between progress messages. -- `return_samples`: Retain R, L, C, and G trial tensors when `true`. Stored - samples are required by [`trial`](@ref) and `rand(::LineParametersMC)` for - exact empirical joint resampling. -- `return_pdf`: Construct histogram-based PDFs when `true`. -- `per_length`: Return per-unit-length quantities when `true`; otherwise scale - impedance and admittance by the physical line length \\[m\\]. -- `nbins`: Histogram bin count, or `nothing` for automatic selection. - -# Returns - -- A `LineParametersMC` containing entrywise R, L, C, and G statistics, optional - samples and PDFs, and a Measurements.jl-valued `LineParameters` object. - -# Notes - -The first sampled problem is solved before tensor allocation, so matrix size is -taken from the actual solver result after bundling and reduction. When supplied, -`trial_sampler` is invoked exactly once for every one-based trial index and its -result bypasses the package's default independent primitive sampler. - -The `measurements` field is a joint, moment-matched covariance surrogate. All -R, L, G, and C coordinates use one shared set of latent standard measurements, -so their empirical Monte Carlo means and covariances are retained across matrix -entries, complex components, impedance and admittance, and frequencies. Sampling -those latent variables normally produces a Gaussian surrogate. Sampling them -from variance-equivalent uniform laws preserves the same mean and covariance, -but neither surrogate reproduces a nonlinear or non-Gaussian Monte Carlo -distribution. - -For `n > 1` trials, the surrogate uses the complete centered sample factor: - -```math -x_i = \\mu_i + \\sum_{t=1}^{n} A_{it}\\xi_t, \\qquad -A_{it} = \\frac{X_{it} - \\mu_i}{\\sqrt{n-1}}, -``` - -where the ``\\xi_t`` are shared independent zero-mean, unit-variance primitives. -For one trial, every output has zero uncertainty. -""" -function mc( - sbs::SystemBuilderSpec, - F::EMTFormulation; - trials::Union{Int, Nothing} = nothing, - distribution::Symbol = :normal, # :uniform => Uniform(μ ± √3·σ), :normal => Normal(μ,σ) - seed::Union{Int, Nothing} = nothing, - trial_sampler::Union{Nothing, Function} = nothing, - conf::Float64 = 0.95, - tol::Float64 = 0.02, - print_step::Int = 1000, - return_samples::Bool = false, # returns Vector{LineParameters} (one per trial) - return_pdf::Bool = false, # hist-based LineParametersPDF per R/L/C/G & freq - per_length::Bool = true, # scale results per length - nbins::Union{Int, Nothing} = nothing, -) - - seed !== nothing && Random.seed!(seed) - z = quantile(Distributions.Normal(), 0.5 + conf/2) - - fvec = sbs.frequencies - nfreq = length(fvec) - - trials === nothing || trials > 0 || - throw(ArgumentError("mc: trials must be greater than zero")) - - # Materialize and solve the first draw before allocating result tensors. The - # output dimension depends on the complete solver reduction policy (bundling, - # Kron reduction, retained grounded terminals, and modal transformation), so - # it cannot be inferred reliably from position dictionaries alone. - sys_det = determinize(sbs) - first_problem = if trial_sampler === nothing - sample(sys_det; distribution = distribution) - else - trial_sampler(sys_det, 1, distribution) - end - first_ws, first_lp = compute!(first_problem, F) - nph = size(first_lp.Z, 1) - size(first_lp.Z) == size(first_lp.Y) || - throw(DimensionMismatch("mc: first-trial Z and Y dimensions differ")) - size(first_lp.Z, 3) == nfreq || - throw(DimensionMismatch("mc: first-trial frequency dimension differs from the specification")) - - # Total scalar observables under DKW: Z & Y, Real & Imag, upper-triangular (incl. diag) per freq - M = 2 * nph * (nph + 1) * nfreq - - ntrials = if trials === nothing - α = 1 - conf - ceil(Int, log(2 * M / α) / (2 * tol^2)) - else - trials - end - - if trials === nothing - @info "mc: estimate number of trials using DKW inequality" scalars = M conf = conf tol = - tol trials = ntrials - end - - @info "mc[Z,Y]: starting" draws = ntrials conf = conf tol = tol distribution = - (distribution === :uniform ? "Uniform(μ ± √3·σ)" : "Normal(μ, σ)") - - # Stats kernel on reals - _stats = function (arr::AbstractVector{<:Real}) - m = mean(arr) - N = length(arr) - s = N == 1 ? zero(eltype(arr)) : std(arr) - ci = z * s / sqrt(N) - return (mean = m, std = s, min = minimum(arr), - q05 = quantile(arr, 0.05), q50 = quantile(arr, 0.50), - q95 = quantile(arr, 0.95), - max = maximum(arr), n = N, conf = conf, z = z, - ci_half = ci, ci_rel = ci / max(abs(m), eps())) - end - - U = eltype(fvec) - - # Concrete vectors of RLCG samples - Rsamp = Array{U, 4}(undef, nph, nph, nfreq, ntrials) - Lsamp = Array{U, 4}(undef, nph, nph, nfreq, ntrials) - Gsamp = Array{U, 4}(undef, nph, nph, nfreq, ntrials) - Csamp = Array{U, 4}(undef, nph, nph, nfreq, ntrials) - - # ───────────────────────────────────────────────────────────────────────── - # Monte Carlo over FULL frequency vector: one LineParameters per trial - # ───────────────────────────────────────────────────────────────────────── - Dlp = domain(first_lp) - - for i in 1:ntrials - if i == 1 - ws, lp = first_ws, first_lp - else - prob = if trial_sampler === nothing - sample(sys_det; distribution = distribution) - else - trial_sampler(sys_det, i, distribution) - end - ws, lp = compute!(prob, F) # lp::LineParameters{Tc, Tr} - end - domain(lp) === Dlp || throw( - DomainError( - domain(lp), - "mc: inconsistent LineParameters domain across trials", - ), - ) - size(lp.Z) == (nph, nph, nfreq) || throw( - DimensionMismatch("mc: LineParameters dimensions changed between trials"), - ) - size(lp.Y) == (nph, nph, nfreq) || throw( - DimensionMismatch("mc: LineParameters dimensions changed between trials"), - ) - if per_length - Zscaled = lp.Z.values - Yscaled = lp.Y.values - else - Zscaled = lp.Z.values .* ws.line_length - Yscaled = lp.Y.values .* ws.line_length - end - - @inbounds for j1 in 1:nph, j2 in 1:nph, k in 1:nfreq - Zval = Zscaled[j1, j2, k] - Yval = Yscaled[j1, j2, k] - fk = fvec[k] - ω = 2π * fk - - Rval = real(Zval) - Lval = imag(Zval) / ω - Gval = real(Yval) - Cval = imag(Yval) / ω - - Rsamp[j1, j2, k, i] = Rval - Lsamp[j1, j2, k, i] = Lval - Gsamp[j1, j2, k, i] = Gval - Csamp[j1, j2, k, i] = Cval - end - - (i % print_step == 0) && @info "mc[Z,Y]: progress" done = i - end - - # ───────────────────────────────────────────────────────────────────────── - # Aggregate statistics per element (j1,j2) and per frequency k - # ───────────────────────────────────────────────────────────────────────── - - # 3D arrays of stats for R,L,C,G - Rstats = Array{NamedTuple, 3}(undef, nph, nph, nfreq) - Lstats = Array{NamedTuple, 3}(undef, nph, nph, nfreq) - Gstats = Array{NamedTuple, 3}(undef, nph, nph, nfreq) - Cstats = Array{NamedTuple, 3}(undef, nph, nph, nfreq) - - # Optional PDFs: same 3D shape, one distribution per scalar - Rpdf = return_pdf ? Array{LineParametersPDF{U}, 3}(undef, nph, nph, nfreq) : nothing - Lpdf = return_pdf ? Array{LineParametersPDF{U}, 3}(undef, nph, nph, nfreq) : nothing - Gpdf = return_pdf ? Array{LineParametersPDF{U}, 3}(undef, nph, nph, nfreq) : nothing - Cpdf = return_pdf ? Array{LineParametersPDF{U}, 3}(undef, nph, nph, nfreq) : nothing - - @inbounds for j1 in 1:nph, j2 in 1:nph, k in 1:nfreq - rvec = @view Rsamp[j1, j2, k, :] - lvec = @view Lsamp[j1, j2, k, :] - gvec = @view Gsamp[j1, j2, k, :] - cvec = @view Csamp[j1, j2, k, :] - - sR = _stats(rvec) - sL = _stats(lvec) - sG = _stats(gvec) - sC = _stats(cvec) - - Rstats[j1, j2, k] = sR - Lstats[j1, j2, k] = sL - Gstats[j1, j2, k] = sG - Cstats[j1, j2, k] = sC - - # Optional PDFs per (j1,j2,k) - if return_pdf - Rpdf[j1, j2, k] = _pdf_from_hist(rvec; nbins = nbins) - Lpdf[j1, j2, k] = _pdf_from_hist(lvec; nbins = nbins) - Gpdf[j1, j2, k] = _pdf_from_hist(gvec; nbins = nbins) - Cpdf[j1, j2, k] = _pdf_from_hist(cvec; nbins = nbins) - end - end - - # Frequency-dependent LineParameters whose entries all share the same latent - # primitive set and therefore retain the complete empirical covariance. - LP_meas = _joint_line_parameters(Dlp, Rsamp, Lsamp, Gsamp, Csamp, fvec) - - @info "mc[Z,Y]: done" total = ntrials nfreq = nfreq - - stats_nt = (R = Rstats, L = Lstats, C = Cstats, G = Gstats) - pdf_nt = return_pdf ? (R = Rpdf, L = Lpdf, C = Cpdf, G = Gpdf) : nothing - - samples_nt = return_samples ? (R = Rsamp, L = Lsamp, C = Csamp, G = Gsamp) : nothing - - return LineParametersMC(fvec, stats_nt, pdf_nt, samples_nt, LP_meas) - -end - -function _joint_measurements( - Rsamp::AbstractArray{U, 4}, - Lsamp::AbstractArray{U, 4}, - Gsamp::AbstractArray{U, 4}, - Csamp::AbstractArray{U, 4}, -) where {U <: Real} - sample_size = size(Rsamp) - all(size(samples) == sample_size for samples in (Lsamp, Gsamp, Csamp)) || - throw(DimensionMismatch("R, L, G, and C sample tensors must have equal dimensions")) - - ntrials = sample_size[4] - ntrials > 0 || throw(ArgumentError("at least one Monte Carlo trial is required")) - tensor_size = (sample_size[1], sample_size[2], sample_size[3]) - ncoordinates = prod(tensor_size) - - # Rows are scalar R/L/G/C coordinates; columns are Monte Carlo trials. The - # centered matrix is used directly as a covariance factor, without forming - # the potentially prohibitive output covariance matrix. - X = vcat( - reshape(Rsamp, ncoordinates, ntrials), - reshape(Lsamp, ncoordinates, ntrials), - reshape(Gsamp, ncoordinates, ntrials), - reshape(Csamp, ncoordinates, ntrials), - ) - μ = vec(mean(X; dims = 2)) - - joint = if ntrials == 1 - map(x -> measurement(x, zero(U)), μ) - else - A = X - A .-= μ - A ./= sqrt(ntrials - 1) - ξ = [measurement(zero(U), one(U)) for _ in axes(A, 2)] - μ + A * ξ - end - - block = ncoordinates - Rmeas = reshape(joint[1:block], tensor_size) - Lmeas = reshape(joint[(block+1):(2*block)], tensor_size) - Gmeas = reshape(joint[(2*block+1):(3*block)], tensor_size) - Cmeas = reshape(joint[(3*block+1):(4*block)], tensor_size) - return Rmeas, Lmeas, Gmeas, Cmeas -end - -function _joint_line_parameters( - ::Type{D}, - Rsamp::AbstractArray{U, 4}, - Lsamp::AbstractArray{U, 4}, - Gsamp::AbstractArray{U, 4}, - Csamp::AbstractArray{U, 4}, - fvec::AbstractVector{U}, -) where {D <: LineParamsDomain, U <: Real} - Rmeas, Lmeas, Gmeas, Cmeas = _joint_measurements(Rsamp, Lsamp, Gsamp, Csamp) - nph, _, nfreq = size(Rmeas) - length(fvec) == nfreq || - throw(DimensionMismatch("sample and frequency dimensions must agree")) - - T = Complex{typeof(measurement(zero(U), zero(U)))} - Zmeas = Array{T}(undef, nph, nph, nfreq) - Ymeas = Array{T}(undef, nph, nph, nfreq) - @inbounds for j1 in 1:nph, j2 in 1:nph, k in 1:nfreq - ω = 2π * fvec[k] - Zmeas[j1, j2, k] = Rmeas[j1, j2, k] + im * ω * Lmeas[j1, j2, k] - Ymeas[j1, j2, k] = Gmeas[j1, j2, k] + im * ω * Cmeas[j1, j2, k] - end - return LineParameters(D, Zmeas, Ymeas, fvec) -end diff --git a/src/uq/montecarlo/compute.jl b/src/uq/montecarlo/compute.jl new file mode 100644 index 000000000..18a73d7ba --- /dev/null +++ b/src/uq/montecarlo/compute.jl @@ -0,0 +1,617 @@ +function _dkw_trials(observable_count::Integer, confidence::Real, cdf_tol::Real) + observable_count > 0 || throw(ArgumentError("observable count must be positive")) + alpha = 1 - confidence + return ceil(Int, log(2 * observable_count / alpha) / (2 * cdf_tol^2)) +end + +_observable_count(value::Engine.CableConstants) = 4length(value) + +function _observable_count(value::Engine.LineParameters) + n = size(observe(value, Engine.Z), 1) + return 2 * n * (n + 1) * length(observe(value, Engine.frequencies)) +end + +""" +$(TYPEDSIGNATURES) + +Extend `buffers` with the `R`, `L`, `C` and `G` samples of `plan.trials` realizations, +one row per assembly of `input`, the first accepted result. The samples keep the scalar +type of that result. +""" +function initialize_buffers(::MonteCarlo, ::Type, input::Engine.CableConstants, plan, + buffers) + T = eltype(observe(input, R)) + return merge(buffers, ( + R = Matrix{T}(undef, length(input), plan.trials), + L = Matrix{T}(undef, length(input), plan.trials), + C = Matrix{T}(undef, length(input), plan.trials), + G = Matrix{T}(undef, length(input), plan.trials) + )) +end + +function _record_sample!( + buffers::NamedTuple{(:R, :L, :C, :G)}, + value::Engine.CableConstants, + trial::Int, + expected_axis +) + value.cores == expected_axis.cores || throw(DimensionMismatch( + "Monte Carlo cable-constant realizations produced incompatible assembly labels", + )) + value.frequency == expected_axis.frequency || throw(DimensionMismatch( + "Monte Carlo cable-constant realizations produced incompatible frequencies", + )) + buffers.R[:, trial] .= observe(value, R) + buffers.L[:, trial] .= observe(value, L) + buffers.C[:, trial] .= observe(value, C) + buffers.G[:, trial] .= observe(value, Engine.G) + return buffers +end + +function _sample_axis(value::Engine.CableConstants) + ( + cores = copy(value.cores), + frequency = value.frequency + ) +end +_sample_axis(value::Engine.LineParameters) = observe(value, Engine.frequencies) + +function _cable_summaries(values::AbstractMatrix) + return [SampleSummary(collect(@view values[assembly, :])) + for assembly in axes(values, 1)] +end + +function _sample_shunt_model(value::Union{Engine.LineParameters,Engine.CableConstants}) + model=get(details(value).data,:shunt_model,nothing) + model===nothing && return ComputationDetails() + return ComputationDetails(; shunt_model=(requested=model.requested,effective=model.effective, + domains=[(design=d.design,terminals=collect(d.terminals),effective=d.effective) + for d in model.domains])) +end + +function _aggregate( + sample_values::NamedTuple{(:R, :L, :C, :G)}, + first_result::Engine.CableConstants, + formulation::MonteCarlo +) + summaries = ( + R = _cable_summaries(sample_values.R), + L = _cable_summaries(sample_values.L), + C = _cable_summaries(sample_values.C), + G = _cable_summaries(sample_values.G) + ) + representation = materialize(first_result, summaries; details=_sample_shunt_model(first_result)) + retained = formulation.options.data.return_samples ? sample_values : nothing + hist = formulation.options.data.return_histograms ? + ( + R = [HistogramDensity(collect(@view sample_values.R[assembly, :]); + bins = formulation.options.data.bins) for assembly in axes(sample_values.R, 1)], + L = [HistogramDensity(collect(@view sample_values.L[assembly, :]); + bins = formulation.options.data.bins) for assembly in axes(sample_values.L, 1)], + C = [HistogramDensity(collect(@view sample_values.C[assembly, :]); + bins = formulation.options.data.bins) for assembly in axes(sample_values.C, 1)], + G = [HistogramDensity(collect(@view sample_values.G[assembly, :]); + bins = formulation.options.data.bins) for assembly in axes(sample_values.G, 1)] + ) : nothing + return (; representation, statistics = summaries, samples = retained, histograms = hist) +end + +""" +$(TYPEDSIGNATURES) + +Extend `buffers` with the `R`, `L`, `C` and `G` samples of `plan.trials` realizations, in +the phase and frequency layout of `input`, the first accepted result, with scalar type `T`. +""" +function initialize_buffers(::MonteCarlo, ::Type{T}, input::Engine.LineParameters, plan, + buffers) where {T} + dimensions = (size(observe(input, Engine.Z))..., plan.trials) + Rs = Array{T}(undef, dimensions) + Ls = similar(Rs) + Cs = similar(Rs) + Gs = similar(Rs) + return merge(buffers, (; R = Rs, L = Ls, C = Cs, G = Gs)) +end + +function _record_sample!( + buffers::NamedTuple{(:R, :L, :C, :G)}, + value::Engine.LineParameters, + trial::Int, + expected_frequencies +) + impedance = observe(value, Engine.Z) + size(impedance) == size(buffers.R)[1:3] || throw(DimensionMismatch( + "Monte Carlo realizations produced incompatible impedance dimensions", + )) + observe(value, Engine.frequencies) == expected_frequencies || throw(DimensionMismatch( + "Monte Carlo realizations produced incompatible frequency axes", + )) + for index in CartesianIndices(size(impedance)) + i, j, k = index.I + buffers.R[i, j, k, trial] = observe(value, R, i, j, k) + buffers.L[i, j, k, trial] = observe(value, L, i, j, k) + buffers.G[i, j, k, trial] = observe(value, Engine.G, i, j, k) + buffers.C[i, j, k, trial] = observe(value, C, i, j, k) + end + return buffers +end + +function _map_samples(function_value, sample_values::Array{<:Real, 4}) + output_size = size(sample_values)[1:3] + indices = CartesianIndices(output_size) + first_index = first(indices) + first_value = function_value(collect(@view sample_values[first_index.I..., :])) + output = Array{typeof(first_value)}(undef, output_size) + output[first_index] = first_value + for index in Iterators.drop(indices, 1) + output[index] = function_value(collect(@view sample_values[index.I..., :])) + end + return output +end + +function _aggregate( + sample_values::NamedTuple{(:R, :L, :C, :G)}, + first_result::Engine.LineParameters, + formulation::MonteCarlo +) + summary_values = Tuple( + _map_samples(SampleSummary, values) + for values in (sample_values.R, sample_values.L, sample_values.C, sample_values.G) + ) + summaries = NamedTuple{(:R, :L, :C, :G)}(summary_values) + retained_details=_sample_shunt_model(first_result) + source_details=details(first_result).data + haskey(source_details,:coordinates) && (retained_details=ComputationDetails( + merge(retained_details.data,(coordinates=source_details.coordinates,)))) + haskey(source_details,:comparison_unsupported) && (retained_details=ComputationDetails( + merge(retained_details.data,(comparison_unsupported=source_details.comparison_unsupported,)))) + representation = materialize(first_result, summaries; details=retained_details) + hist = formulation.options.data.return_histograms ? + NamedTuple{(:R, :L, :C, :G)}(Tuple( + _map_samples( + values -> HistogramDensity(values; bins = formulation.options.data.bins), + samples + ) + for samples in (sample_values.R, sample_values.L, sample_values.C, sample_values.G) + )) : nothing + retained = formulation.options.data.return_samples ? sample_values : nothing + return (; representation, statistics = summaries, samples = retained, histograms = hist) +end + +const _MONTE_CARLO_STACKTRACE_LIMIT = 8 + +function _failure_stack(backtrace) + summaries = @NamedTuple{ + function_name::String, + file::String, + line::Int + }[] + for frame in Iterators.take( + Base.StackTraces.stacktrace(backtrace), + _MONTE_CARLO_STACKTRACE_LIMIT + ) + push!(summaries, + ( + function_name = string(frame.func), + file = string(frame.file), + line = Int(frame.line) + )) + end + return summaries +end + +function _failure_record( + attempt::Int, + target_trial::Int, + stage::Symbol, + sample, + exception, + backtrace +) + return ( + attempt, + target_trial, + stage, + sample, + error = ( + type = string(typeof(exception)), + message = sprint(showerror, exception), + stack = _failure_stack(backtrace) + ) + ) +end + +function _failure_summary(failures, accepted::Int, attempts::Int) + by_type = Dict{String, Int}() + by_stage = Dict{Symbol, Int}() + for failure in failures + error_type = failure.error.type + by_type[error_type] = get(by_type, error_type, 0) + 1 + by_stage[failure.stage] = get(by_stage, failure.stage, 0) + 1 + end + type_counts = [(type = error_type, count = by_type[error_type]) + for error_type in sort!(collect(keys(by_type)))] + stage_counts = [(stage, count = by_stage[stage]) + for stage in sort!(collect(keys(by_stage)))] + return ( + attempts, + accepted, + failed = length(failures), + acceptance_rate = accepted / attempts, + by_type = type_counts, + by_stage = stage_counts + ) +end + +function _resampling_limit_error(failures, accepted::Int, attempts::Int, maximum::Int) + summary = _failure_summary(failures, accepted, attempts) + final_failure = last(failures) + stack = final_failure.error.stack + location = isempty(stack) ? "unknown location" : + "$(first(stack).file):$(first(stack).line) in $(first(stack).function_name)" + throw(ErrorException( + "Monte Carlo resampling limit of $maximum failures was exhausted after " * + "$(summary.attempts) attempts ($(summary.accepted) accepted); final " * + "$(final_failure.error.type) during $(final_failure.stage) at $location: " * + final_failure.error.message, + )) +end + +function realize(rng::Random.AbstractRNG, point::Gridpoint{Engine.LineParametersProblem}, distribution) + return DataModel.realize_clearance(rng, point, distribution) +end + +function _uncertain_arguments(point::Gridpoint) + records=NamedTuple[] + function collect!(value,path) + if value isa UncertainValue + push!(records,(argument_path=path,nominal=value.nominal,standard_deviation=value.sigma)) + elseif value isa Gridpoint + for (index,argument) in enumerate(value.args) + collect!(argument,(path...,index)) + end + elseif value isa Union{Tuple,AbstractArray} + for (index,argument) in enumerate(value) + collect!(argument,(path...,index)) + end + end + end + collect!(point,()) + return records +end + +function _monte_carlo(point, formulation::MonteCarlo, options, seed, details_owner, child_options) + clearance = point isa Gridpoint{Engine.LineParametersProblem} ? + DataModel.collect_clearance_requirements(point) : nothing + physical_inputs=clearance===nothing ? nothing : Engine.completed_inputs(clearance.declaration[]) + clearance===nothing || (clearance.declaration[]=nothing) + try + aggregate = _monte_carlo(point, formulation, options, seed, details_owner, child_options, clearance) + if physical_inputs===nothing + # Capture the nominal physical declaration once after successful + # sampling. A Monte Carlo builder need not accept first-order numbers. + declaration=realize(Random.Xoshiro(0),point,(_rng,mean,_sigma) -> mean) + physical_inputs=merge(Engine.completed_inputs(declaration),(interpretation=:nominal_declaration,)) + end + physical_inputs=merge(physical_inputs,(uncertain_arguments=_uncertain_arguments(point),)) + representation=Engine.retain_gridpoint(aggregate.representation,Commons.gridpoint_id(); + fields=merge(aggregate.completion,(inputs=physical_inputs, + uncertainty=(estimator=:empirical,representation=:marginal_mean_std), + uncertainty_descriptions=_uncertainty_descriptions(MonteCarlo)))) + return merge(aggregate,(;representation)) + finally + DataModel.warn_clearance_summary(clearance) + end +end + +# Draw the arguments, build the problem inside the clearance scope and compute it, once. +# After a failure that the study resamples, add its record to `failures` and return +# `nothing`. `stage` and `drawn` keep the stage and the drawn arguments for that record. +function _attempt(rng, sampler, attempts::Int, accepted::Int) + (; point, formulation, clearance, child_options, failures, stage, drawn) = sampler + stage[] = :sample + drawn[] = nothing + try + realization = DataModel.with_clearance(clearance) do + arguments = realize_arguments(rng, point, formulation.options.data.distribution) + drawn[] = arguments + stage[] = :build + realize(point, arguments) + end + stage[] = :compute + return compute(realization, formulation.inner; options=child_options) + catch exception + backtrace = catch_backtrace() + formulation.options.data.on_error === :resample && exception isa DomainError || + rethrow() + push!(failures, _failure_record( + attempts, + accepted + 1, + stage[], + drawn[], + exception, + backtrace + )) + length(failures) < formulation.options.data.max_failures || + _resampling_limit_error( + failures, + accepted, + attempts, + formulation.options.data.max_failures + ) + return nothing + end +end + +function _monte_carlo(point, formulation::MonteCarlo, options, seed, details_owner, child_options, clearance) + rng = Random.Xoshiro(seed) + failures = NamedTuple[] + timing = get(options.data, :timing, false) + timing isa Bool || throw(ArgumentError("timing must be Bool")) + trial_timings = timing ? NamedTuple[] : nothing + progress = point isa Gridpoint{<:Engine.LineParametersProblem} && verbosity(options, :progress) > 0 + started = progress ? time_ns() : UInt64(0) + sampler = (; point, formulation, clearance, child_options, failures, + stage = Ref(:sample), drawn = Ref{Any}(nothing)) + + # Pilot: draw until the first accepted realization. + attempts = 0 + pilot = nothing + while pilot === nothing + attempts += 1 + pilot = _attempt(rng, sampler, attempts, 0) + end + first_result = something(pilot) + + # The storage of every trial, allocated once from the pilot result. + record = formulation.options.data.retain_details ? + computation_details(details_owner, first_result) : nothing + ntrials = something( + formulation.options.data.trials, + _dkw_trials( + _observable_count(first_result), + formulation.options.data.confidence, + formulation.options.data.cdf_tol + ) + ) + sample_values = initialize_buffers(formulation, Float64, first_result, + (; trials = ntrials), (;)) + sample_axis = _sample_axis(first_result) + retained = formulation.options.data.retain_details ? + Vector{typeof(record)}(undef, ntrials) : nothing + + trials = _trials!(rng, first_result, sample_values, retained, record, sampler, + details_owner, sample_axis, trial_timings, attempts, ntrials, progress, started) + attempts = trials.attempts + accepted = ntrials + + failure_summary = _failure_summary(failures, accepted, attempts) + retained_details = retained === nothing ? nothing : + ( + trials = retained, + failures, + failure_summary, + clearance = DataModel.clearance_summary(clearance) + ) + aggregation_started = progress ? time_ns() : UInt64(0) + aggregate = _aggregate(sample_values, first_result, formulation) + if progress + now = time_ns() + if now - trials.last_log >= 5_000_000_000 + @info "Monte Carlo aggregation completed" _group=:progress accepted trials=ntrials attempts rejected=length(failures) aggregation_seconds=(now-aggregation_started)*1e-9 elapsed_seconds=(now-started)*1e-9 + end + end + # The trials computed with `formulation.inner` and completed it. The completed record + # includes the equivalent earth that each earth formula consumed. + completion = details(first_result).data[(:formulations, :selections, :formulation_fields)] + return merge(aggregate, + (; trials = ntrials, seed, details = retained_details, timing=trial_timings, completion)) +end + +# Record the pilot result in `buffers`, then draw and record further trials until the study +# accepts `ntrials` of them. Return `attempts` and the time of the last progress message. +function _trials!(rng, first_result::R, buffers, retained, record, sampler, details_owner, + sample_axis, trial_timings, attempts::Int, ntrials::Int, progress::Bool, + started::UInt64) where {R} + formulation = sampler.formulation + failures = sampler.failures + previous = started + last_log = started + average_seconds = 0.0 + accepted = 0 + value = first_result + while true + accepted += 1 + _record_sample!(buffers, value, accepted, sample_axis) + retained === nothing || (retained[accepted] = record) + trial_timings === nothing || push!(trial_timings, details(value).data.timing) + if progress + now = time_ns() + interval = (now - previous) * 1e-9 + average_seconds = accepted == 1 ? interval : 0.2 * interval + 0.8 * average_seconds + previous = now + if now - last_log >= 5_000_000_000 + @info "Monte Carlo sampling progress" _group=:progress accepted trials=ntrials attempts rejected=length(failures) elapsed_seconds=(now-started)*1e-9 eta_hours=(ntrials-accepted)*average_seconds/3600 + last_log = now + end + end + accepted < ntrials || break + result = nothing + while result === nothing + attempts += 1 + result = _attempt(rng, sampler, attempts, accepted) + end + record = formulation.options.data.retain_details ? + computation_details(details_owner, result) : nothing + _sample_shunt_model(result) == _sample_shunt_model(first_result) || throw(ArgumentError( + "Monte Carlo realizations changed shunt-model coverage; select a fixed geometry model for this study")) + result isa R || throw(ArgumentError( + "Monte Carlo realizations produced incompatible result types", + )) + if retained !== nothing + typeof(record) === eltype(retained) || throw(ArgumentError( + "Monte Carlo trials produced incompatible details record types", + )) + end + value = result + end + return (; attempts, last_log) +end + +function compute(problem::ParametricProblem, formulation::MonteCarlo) + levels = verbosity(get(problem.options.data, :verbosity, (default=0,))) + progress = problem.space isa LineCableModels.Gridspace{<:Engine.LineParametersProblem} && get(levels, :progress, levels.default) > 0 + child_options = progress ? ComputationOptions(merge(problem.options.data, + (verbosity=merge(problem.options.data.verbosity, (progress=0,)),))) : problem.options + logger = haskey(problem.options.data, :verbosity) ? VerbosityLogger(Logging.current_logger(), levels) : Logging.current_logger() + return Logging.with_logger(logger) do + started = progress ? time_ns() : UInt64(0) + previous = started + last_log = started + average_seconds = 0.0 + source_id=Commons.gridpoint_id().source_id + Base.get_extension(LineCableModels, :LineCableModelsMeasurementsExt) === nothing && + throw(ArgumentError("MonteCarlo requires the Measurements extension to construct its result; " * + "load it with `using Measurements` before compute")) + point_count = length(problem.space) + point_count > 0 || throw(ArgumentError( + "higher-order problem space must contain at least one core problem", + )) + progress && @info "Monte Carlo computation started" _group=:progress populations=point_count trials=formulation.options.data.trials + root_seed = formulation.options.data.seed === nothing ? rand(Random.RandomDevice(), UInt64) : + formulation.options.data.seed + details_owner = formulation.options.data.retain_details ? + typeof(formulation.inner) : nothing + point_source = points(problem.space) + first_item = iterate(point_source) + first_item === nothing && throw(DimensionMismatch( + "problem-space iteration ended before its declared cardinality", + )) + first_point, state = first_item + first_seed = root_seed + first_aggregate = _monte_carlo( + first_point, + formulation, + problem.options, + first_seed, + details_owner, + child_options + ) + validate(typeof(first_aggregate.representation), MonteCarloResult) + + values = Vector{typeof(first_aggregate.representation)}(undef, point_count) + stats_values = Vector{typeof(first_aggregate.statistics)}(undef, point_count) + sample_values = formulation.options.data.return_samples ? + Vector{typeof(first_aggregate.samples)}(undef, point_count) : nothing + histogram_values = formulation.options.data.return_histograms ? + Vector{typeof(first_aggregate.histograms)}(undef, point_count) : + nothing + seeds = Vector{UInt64}(undef, point_count) + trial_counts = Vector{Int}(undef, point_count) + retained = formulation.options.data.retain_details ? + Vector{typeof(first_aggregate.details)}(undef, point_count) : nothing + + values[1] = Engine.retain_gridpoint(first_aggregate.representation,Commons.gridpoint_id(;source_id)) + stats_values[1] = first_aggregate.statistics + sample_values === nothing || (sample_values[1] = first_aggregate.samples) + histogram_values === nothing || + (histogram_values[1] = first_aggregate.histograms) + seeds[1] = first_aggregate.seed + trial_counts[1] = first_aggregate.trials + retained === nothing || (retained[1] = first_aggregate.details) + timing = get(problem.options.data, :timing, false) + population_timings = timing ? Vector{Vector{NamedTuple}}(undef, point_count) : nothing + timing && (population_timings[1]=first_aggregate.timing) + if progress + now = time_ns() + average_seconds = (now - previous) * 1e-9 + previous = now + if now - last_log >= 5_000_000_000 + @info "Monte Carlo population progress" _group=:progress completed=1 populations=point_count elapsed_seconds=(now-started)*1e-9 eta_hours=(point_count-1)*average_seconds/3600 + last_log = now + end + end + + for index in 2:point_count + item = iterate(point_source, state) + item === nothing && throw(DimensionMismatch( + "problem-space iteration ended before its declared cardinality", + )) + point, state = item + point_seed = root_seed ⊻ (UInt64(index - 1) * 0x9e3779b97f4a7c15) + aggregate = _monte_carlo( + point, + formulation, + problem.options, + point_seed, + details_owner, + child_options + ) + typeof(aggregate.representation) === eltype(values) || throw(ArgumentError( + "Monte Carlo points produced incompatible core result types", + )) + typeof(aggregate.statistics) === eltype(stats_values) || throw(ArgumentError( + "Monte Carlo points produced incompatible statistics product types", + )) + values[index] = Engine.retain_gridpoint(aggregate.representation, + Commons.gridpoint_id(;source_id,problem_index=index)) + stats_values[index] = aggregate.statistics + if sample_values !== nothing + typeof(aggregate.samples) === eltype(sample_values) || throw(ArgumentError( + "Monte Carlo points produced incompatible sample product types", + )) + sample_values[index] = aggregate.samples + end + if histogram_values !== nothing + typeof(aggregate.histograms) === eltype(histogram_values) || + throw(ArgumentError( + "Monte Carlo points produced incompatible histogram product types", + )) + histogram_values[index] = aggregate.histograms + end + seeds[index] = aggregate.seed + trial_counts[index] = aggregate.trials + if retained !== nothing + typeof(aggregate.details) === eltype(retained) || throw(ArgumentError( + "Monte Carlo points produced incompatible details product types", + )) + retained[index] = aggregate.details + end + timing && (population_timings[index]=aggregate.timing) + if progress + now = time_ns() + interval = (now - previous) * 1e-9 + average_seconds = 0.2 * interval + 0.8 * average_seconds + previous = now + if now - last_log >= 5_000_000_000 + @info "Monte Carlo population progress" _group=:progress completed=index populations=point_count elapsed_seconds=(now-started)*1e-9 eta_hours=(point_count-index)*average_seconds/3600 + last_log = now + end + end + end + iterate(point_source, state) === nothing || throw(DimensionMismatch( + "problem-space iteration exceeded its declared cardinality", + )) + + retained_details = retained === nothing ? (;) : + ( + trials = getproperty.(retained, :trials), + failures = getproperty.(retained, :failures), + failure_summary = getproperty.(retained, :failure_summary), + clearance = getproperty.(retained, :clearance) + ) + timing && (retained_details=merge(retained_details, (timing=population_timings,))) + result = MonteCarloResult( + formulation, + values, + stats_values, + sample_values, + histogram_values, + root_seed, + seeds, + trial_counts, + ComputationDetails(retained_details) + ) + progress && @info "Monte Carlo computation completed successfully" _group=:progress completed=point_count populations=point_count elapsed_seconds=(time_ns()-started)*1e-9 + result + end +end diff --git a/src/uq/observations.jl b/src/uq/observations.jl new file mode 100644 index 000000000..e9a0759fe --- /dev/null +++ b/src/uq/observations.jl @@ -0,0 +1,459 @@ +""" +Return core results in Gridspace traversal order. +""" +Units.label(::Units.Quantity{:sample_count}) = "Count" +Units.symbol(::Units.Quantity{:sample_count}) = "n" +Units.label(::Units.Quantity{:probability}) = "Probability" +Units.symbol(::Units.Quantity{:probability}) = "p" +Units.label(::Units.Quantity{:cumulative_probability}) = "Cumulative probability" +Units.symbol(::Units.Quantity{:cumulative_probability}) = "F" +Units.label(::Units.Quantity{:probability_density}) = "Probability density" +Units.symbol(::Units.Quantity{:probability_density}) = "p" + +const _DimensionlessStatisticalQuantity = Union{ + Units.Quantity{:sample_count}, + Units.Quantity{:probability}, + Units.Quantity{:cumulative_probability} +} +Units.native_unit(::_DimensionlessStatisticalQuantity) = Units.units(:base, :dimensionless) +Units.display_unit(::_DimensionlessStatisticalQuantity) = Units.units(:base, :dimensionless) + +""" +Return the sample-summary product for each Monte Carlo point. +""" +statistics(value::MonteCarloResult) = value.stats + +""" +Return retained sample products, or `nothing` when retention was disabled. +""" +samples(value::MonteCarloResult) = value.sample_values + +""" +Return retained histogram products, or `nothing` when retention was disabled. +""" +histograms(value::MonteCarloResult) = value.histogram_values + +basis(value::MonteCarloResult) = basis(first(value.values)) +basis(value::LinearErrorResult) = basis(first(value.values)) + +""" +$(TYPEDSIGNATURES) + +Return uncertainty-bearing core results, in configuration order, or the core +result at `point`. Monte Carlo stores its marginal mean and standard deviation +representation during computation, without joint output correlations. Access +returns the stored values and preserves their uncertainty-source identities. +""" +uncertain(value::Union{LinearErrorResult,MonteCarloResult}) = value.values +uncertain(value::AbstractUncertaintyResult, point::Integer) = uncertain(value)[point] + +""" +$(TYPEDSIGNATURES) + +Return the resolved root random seed of a Monte Carlo computation. +""" +root_seed(value::MonteCarloResult) = value.root_seed + +""" +$(TYPEDSIGNATURES) + +Return the random seed used for the Gridspace point at `index`. +""" +point_seed(value::MonteCarloResult, index::Integer) = value.point_seeds[index] + +""" +$(TYPEDSIGNATURES) + +Return the number of trials evaluated for the Gridspace point at `index`. +""" +trial_count(value::MonteCarloResult, index::Integer) = value.trial_counts[index] + +""" +$(TYPEDSIGNATURES) + +Return the simultaneous empirical-CDF confidence of a Monte Carlo computation. +""" +confidence(value::MonteCarloResult) = (value.formulation isa NamedTuple ? value.formulation.options : value.formulation.options.data).confidence + +""" +$(TYPEDSIGNATURES) + +Return the empirical-CDF tolerance used to size a Monte Carlo computation. +""" +cdf_tolerance(value::MonteCarloResult) = (value.formulation isa NamedTuple ? value.formulation.options : value.formulation.options.data).cdf_tol + +""" +$(TYPEDSIGNATURES) + +Return the sampling distribution of a Monte Carlo computation. +""" +sampling_distribution(value::MonteCarloResult) = (value.formulation isa NamedTuple ? value.formulation.options : value.formulation.options.data).distribution + +""" +$(TYPEDSIGNATURES) + +Return the simultaneous DKW bound for all retained real marginal summaries at +one outer point. Both matrix orientations are counted conservatively. This is +an iid-sampling bound, not measured CDF error. Resampling concerns the +population conditional on success. A fixed trial count is insufficient to certify the configured target. +""" +function confidence(value::MonteCarloResult, point::Integer) + count = sum(length, values(value.stats[point])) + trials = trial_count(value,point) + bound = sqrt(log(2count / (1-confidence(value))) / (2trials)) + retained=details(value).data + diagnostics = isempty(retained) ? nothing : + (failures=retained.failures[point],failure_summary=retained.failure_summary[point], + clearance=haskey(retained,:clearance) ? retained.clearance[point] : nothing) + mean_standard_error=map(value.stats[point]) do summaries + map(summary -> summary.n>1 ? summary.std/sqrt(summary.n) : missing,summaries) + end + return (point, trials, spread_estimated=trials>1,marginal_count=count, confidence=confidence(value), + target_cdf=cdf_tolerance(value), cdf_bound=min(1.0,bound), + target_supported=bound <= cdf_tolerance(value), scope=:point_all_retained_marginals, + assumption=:iid, distribution=sampling_distribution(value), + conditioning=(value.formulation isa NamedTuple ? value.formulation.options : value.formulation.options.data).on_error === :resample ? :successful_realizations : :none, + diagnostics, mean_standard_error, root_seed=root_seed(value), point_seed=point_seed(value,point), + samples_retained=samples(value) !== nothing, histograms_retained=histograms(value) !== nothing) +end + +const _MonteCarloProductSelector = Union{ + typeof(statistics), + typeof(samples), + typeof(histograms) +} + +const _MonteCarloScientificSelector = Union{ + typeof(R), + typeof(L), + typeof(C), + typeof(Engine.G), typeof(Engine.X), typeof(Engine.B), typeof(Engine.Z), typeof(Engine.Y) +} + +const _StatisticSelector = Union{ + typeof(Statistics.mean), + typeof(Statistics.std), + typeof(Statistics.median), + typeof(minimum), + typeof(maximum), Base.Fix2{typeof(Statistics.quantile)} +} + +function Units.quantity( + ::_MonteCarloProductSelector, + selector::_MonteCarloScientificSelector +) + return Units.quantity(selector) +end + +function Units.quantity(::typeof(statistics), selector::_MonteCarloScientificSelector, + ::_StatisticSelector) + return Units.quantity(selector) +end + +function _monte_carlo_product(value::MonteCarloResult, ::typeof(statistics), point::Integer) + return value.stats[point] +end + +function _monte_carlo_product(value::MonteCarloResult, ::typeof(samples), point::Integer) + retained = value.sample_values + retained === nothing && throw(ArgumentError("Monte Carlo samples were not retained")) + return retained[point] +end + +function _monte_carlo_product(value::MonteCarloResult, ::typeof(histograms), point::Integer) + retained = value.histogram_values + retained === nothing && throw(ArgumentError( + "Monte Carlo histograms were not retained or derived", + )) + return retained[point] +end + +function observe( + value::MonteCarloResult, + selector::_MonteCarloScientificSelector, + point::Integer, + indices... +) + return observe(value.values[point], selector, indices...) +end + +function observe( + value::MonteCarloResult{<:Engine.LineParameters}, + ::typeof(frequencies), + point::Integer, + indices... +) + return observe(value.values[point], frequencies, indices...) +end + +function observe( + value::MonteCarloResult, + product::_MonteCarloProductSelector, + selector::_MonteCarloScientificSelector, + point::Integer, + indices... +) + stored = if selector in (Engine.X, Engine.B) + source = selector === Engine.X ? L : C + values = observe(value, product, source, point) + angular = reshape(2pi .* frequencies(value[point]), 1, 1, :) + product === samples && (angular = reshape(angular, 1, 1, :, 1)) + detach.(values, angular) + elseif selector in (Engine.Z, Engine.Y) + product === samples || throw(ArgumentError( + "complex quantities support selected mean/std or retained joint samples, not ordered summaries")) + real_selector, imaginary_selector = selector === Engine.Z ? (R, Engine.X) : (Engine.G, Engine.B) + observe(value, samples, real_selector, point) .+ + im .* observe(value, samples, imaginary_selector, point) + else + _monte_carlo_field(_monte_carlo_product(value, product, point), selector) + end + return _product_value(stored, indices) +end + +function _histogram_observation( + value::MonteCarloResult, + selector::_MonteCarloScientificSelector, + point::Integer, + indices::Tuple, + bins::Union{Nothing, Integer} +) + bins === nothing || bins > 0 || throw(ArgumentError("histogram bins must be positive")) + if selector in (Engine.X,Engine.B) + base_selector=selector===Engine.X ? L : C + original=_histogram_observation(value,base_selector,point,indices,bins) + return detach(original,2pi*frequencies(value[point])[last(indices)]) + end + selector in (Engine.Z,Engine.Y) && throw(ArgumentError("complex histograms require a real-valued observable")) + if value.histogram_values !== nothing + stored = _monte_carlo_field(value.histogram_values[point], selector) + histogram = _product_value(stored, indices) + (bins === nothing || length(histogram.density) == bins) && return histogram + value.sample_values === nothing && throw(ArgumentError( + "Changing histogram bins requires retained samples; use " * + "MonteCarlo(...; return_samples=true), or keep the retained bin count", + )) + end + retained = value.sample_values + retained === nothing && throw(ArgumentError( + "Monte Carlo histograms were not retained and samples are unavailable for derivation", + )) + stored = _monte_carlo_field(retained[point], selector) + sample = _product_value(stored, (indices..., Colon())) + return HistogramDensity(collect(sample); bins) +end + +""" +$(TYPEDSIGNATURES) + +Observe a cable-constant marginal as a [`HistogramDensity`](@ref), using +retained samples when a new binning is requested. Simulation is unnecessary and retained products remain unchanged. + +# Arguments + +- `value`: monte Carlo result. +- `histograms`: histogram product selector. +- `selector`: `R`, `L`, `C`, or `G`. +- `point`: gridspace point index. +- `assembly`: cable assembly index. +- `bins`: positive maximum bin count, or `nothing` to reuse the retained model. Without + a retained model, `nothing` selects the sample-based automatic bin count. + +# Returns + +- A normalized histogram in the observed quantity's native units. Constant + samples produce one finite-width bin, irrespective of the requested count. + Coincident representable edges in varying samples are merged. + +# Errors + +An `ArgumentError` is raised for nonpositive counts or when deriving a new +histogram requires samples that were not retained. A retained model can be +reused without samples when its bin count matches the request. +""" +function observe( + value::MonteCarloResult{<:Engine.CableConstants}, + ::typeof(histograms), + selector::_MonteCarloScientificSelector, + point::Integer, + assembly::Integer, + bins::Union{Nothing, Integer} +) + return _histogram_observation(value, selector, point, (assembly,), bins) +end + +""" +$(TYPEDSIGNATURES) + +Observe a line-parameter histogram at one matrix element and frequency index. +The `bins` argument follows the same retention and derivation rules as the +cable-constant histogram observation. `row`, `column`, and `frequency` select +the marginal in place of `assembly`. +""" +function observe( + value::MonteCarloResult{<:Engine.LineParameters}, + ::typeof(histograms), + selector::_MonteCarloScientificSelector, + point::Integer, + row::Integer, + column::Integer, + frequency::Integer, + bins::Union{Nothing, Integer} +) + return _histogram_observation( + value, + selector, + point, + (row, column, frequency), + bins + ) +end + +function observe( + value::MonteCarloResult, + ::typeof(statistics), + selector::_MonteCarloScientificSelector, + transform::_StatisticSelector, + point::Integer, + indices... +) + if selector in (Engine.X,Engine.B) + base_selector=selector===Engine.X ? L : C + values=observe(value,statistics,base_selector,transform,point,indices...) + sample=length(indices)==3 ? last(indices) : Colon() + angular=2pi .* frequencies(value[point])[sample] + factor=angular isa AbstractArray ? reshape(angular,ntuple(_ -> 1,ndims(values)-1)...,:) : angular + return values .* factor + end + if selector in (Engine.Z, Engine.Y) + transform in (Statistics.mean, Statistics.std) || throw(ArgumentError( + "complex statistics require mean or std; ordered statistics need a real-valued observable")) + real_selector, imaginary_selector = selector === Engine.Z ? (R, Engine.X) : (Engine.G, Engine.B) + real_values = observe(value, statistics, real_selector, transform, point, indices...) + imaginary_values = observe(value, statistics, imaginary_selector, transform, point, indices...) + return transform === Statistics.mean ? real_values .+ im .* imaginary_values : + hypot.(real_values, imaginary_values) + end + return _statistic(transform, observe(value, statistics, selector, point, indices...)) +end + +function _monte_carlo_observables(selectors::Tuple) + product_selectors = (statistics, samples, histograms) + real_selectors = filter(selector -> !(selector in (Engine.Z, Engine.Y)), selectors) + products = Tuple((product, selector) + for product in product_selectors for selector in real_selectors) + selected = Tuple((statistics, selector, transform) + for selector in selectors for transform in + (selector in (Engine.Z, Engine.Y) ? (Statistics.mean, Statistics.std) : + (Statistics.mean, Statistics.std, minimum, Statistics.median, maximum, + Base.Fix2(Statistics.quantile, 0.0), Base.Fix2(Statistics.quantile, 0.05), + Base.Fix2(Statistics.quantile, 0.5), Base.Fix2(Statistics.quantile, 0.95), + Base.Fix2(Statistics.quantile, 1.0)))) + complex_samples=Tuple((samples,selector) for selector in selectors if selector in (Engine.Z,Engine.Y)) + return (selectors..., products..., selected..., complex_samples...) +end + +function observables( + ::Type{<:MonteCarloResult{T}} +) where {T <: Engine.CableConstants} + return _monte_carlo_observables((R, L, C, Engine.G)) +end + +function observables( + ::Type{<:MonteCarloResult{T}} +) where {T <: Engine.LineParameters} + return (frequencies, _monte_carlo_observables((R, L, C, Engine.G, Engine.X, Engine.B, Engine.Z, Engine.Y))...) +end + +function observables(::Type{<:LinearErrorResult{T}}) where {T} + selectors = T <: Engine.CableConstants ? (R, L, C, Engine.G) : + (R, L, C, Engine.G, Engine.X, Engine.B, Engine.Z, Engine.Y) + selected = Tuple((statistics, selector, transform) for selector in selectors + for transform in (Statistics.mean, Statistics.std)) + return (selectors..., selected..., (T <: Engine.LineParameters ? (frequencies,) : ())...) +end + +function observe(value::LinearErrorResult, selector::_MonteCarloScientificSelector, + point::Integer, indices...) + return observe(value[point], selector, indices...) +end + +function observe(value::LinearErrorResult{<:Engine.LineParameters}, ::typeof(frequencies), + point::Integer, indices...) + return observe(value[point], frequencies, indices...) +end + +""" +$(TYPEDSIGNATURES) + +Observe first-order nominal values or propagated standard uncertainties in the +quantity's native units. Complex standard deviation is the nonnegative root +sum of component variances. It is not a magnitude-distribution statistic. +""" +function observe(value::LinearErrorResult, ::typeof(statistics), + selector::_MonteCarloScientificSelector, + transform::Union{typeof(Statistics.mean),typeof(Statistics.std)}, + point::Integer, indices...) + values = observe(value[point], selector, indices...) + if transform === Statistics.mean + return nominal.(values) + end + return hypot.(uncertainty.(real.(values)), uncertainty.(imag.(values))) +end + +function observation_resolution(source::Union{MonteCarloResult,LinearErrorResult}, request; + atol=nothing, frequencies=nothing) + identity = request_identity(request) + if !(identity isa Tuple && length(identity) == 3 && first(identity) === statistics) + return observation_resolution(nothing, request; atol, frequencies) + end + indices = request_indices(request) + isempty(indices) && throw(ArgumentError("a UQ resolution request requires an explicit point index")) + point,indices=first(indices),Base.tail(indices) + core = source[point] + core isa Engine.LineParameters || return observation_resolution(nothing, request; atol, frequencies) + f = observe(source, LineCableModels.frequencies, point) + frequencies === nothing || frequencies == f || throw(ArgumentError("supplied frequencies differ from the UQ point")) + sample = length(indices) == 3 ? last(indices) : Colon() + values = observe(source, request...) + return observation_resolution(values, identity[2]; atol, frequencies=f[sample], result_basis=basis(core)) +end + +@inline _product_value(value, ::Tuple{}) = value +@inline _product_value(value, indices::Tuple) = getindex(value, indices...) + +@inline _monte_carlo_field(product, ::typeof(R)) = product.R +@inline _monte_carlo_field(product, ::typeof(L)) = product.L +@inline _monte_carlo_field(product, ::typeof(C)) = product.C +@inline _monte_carlo_field(product, ::typeof(Engine.G)) = product.G + +@inline _statistic(transform, value::AbstractArray) = map(transform, value) +@inline _statistic(transform, value) = transform(value) + +function detach(summary::SampleSummary, factor) + return SampleSummary( + summary.mean * factor, + summary.std * abs(factor), + summary.min * factor, + summary.q05 * factor, + summary.median * factor, + summary.q95 * factor, + summary.max * factor, + summary.n + ) +end + +function detach( + summaries::AbstractArray{<:SampleSummary}, + factor +) + return map(summary -> detach(summary, factor), summaries) +end + +function detach(histogram::HistogramDensity, factor) + factor > zero(factor) || throw(ArgumentError("histogram conversion must be positive")) + return HistogramDensity( + histogram.edges .* factor, + histogram.density ./ factor + ) +end diff --git a/src/uq/observedresult.jl b/src/uq/observedresult.jl new file mode 100644 index 000000000..9582906b1 --- /dev/null +++ b/src/uq/observedresult.jl @@ -0,0 +1,175 @@ +# Acquisition is owned by UQ because primary elements do not own empirical +# statistics, samples, histogram models, or sampling-workload information. +_is_uq_product(request) = request_identity(request) isa Tuple && + first(request_identity(request)) in (statistics,samples,histograms) + +function Commons.observation_requests(source::Union{MonteCarloResult,LinearErrorResult}, + requests::Tuple;point::Integer=firstindex(source),complete_pairs::Bool=false) + primary=Tuple(filter(!_is_uq_product,requests)) + products=Tuple(filter(_is_uq_product,requests)) + normalized=isempty(requests) || !isempty(primary) ? + Commons.observation_requests(source[point],primary;complete_pairs) : (retained=(),displayed=()) + expanded=Tuple[] + for request in products + identity=request_identity(request) + observation_request(source,request) + if first(identity)===statistics && length(identity)==2 + for transform in (Statistics.mean,Statistics.std,minimum, + Base.Fix2(Statistics.quantile,0.05),Statistics.median, + Base.Fix2(Statistics.quantile,0.95),maximum) + push!(expanded,(identity...,transform,request_indices(request)...)) + end + else + push!(expanded,request) + end + end + selected=(normalized.retained...,expanded...) + allunique(selected) || throw(ArgumentError("observation requests must be distinct")) + return (retained=selected,displayed=(normalized.displayed...,expanded...)) +end + +function _uq_coordinates(core::Engine.LineParameters,selector,indices) + rank=3 + selected=isempty(indices) ? ntuple(_ -> Colon(),rank) : indices + length(selected)==rank || throw(DimensionMismatch("statistical products require row, column and sample indices")) + return Engine.line_coordinates(core,(selector,selected...),nothing) +end +function _uq_coordinates(core::Engine.CableConstants,selector,indices) + length(indices)<=1 || throw(DimensionMismatch("cable statistics select assembly indices")) + selected=isempty(indices) ? Colon() : only(indices) + return (kind=:assemblies,indices=(selected,),assemblies=observation_indices(selected,length(core)), + labels=copy(core.cores),frequencies=[core.frequency],frequency_unit=Units.units(:base,:hertz),extent=(length(core),1)) +end + +function Commons.observation_quantity(source::Union{MonteCarloResult,LinearErrorResult}, + point::Integer,request;unit=nothing,clip=true,atol=nothing,frequencies=nothing) + core=source[point] + _is_uq_product(request) || return Commons.observation_quantity(core,request;unit,clip,atol,frequencies) + identity=request_identity(request) + product,selector=identity[1:2] + indices=request_indices(request) + q=request_quantity(request) + target=unit===nothing ? Units.display_unit(q,basis(core)) : unit + distribution=nothing + if product===histograms + rank=core isa Engine.LineParameters ? 3 : 1 + length(indices) in (rank,rank+1) || throw(DimensionMismatch("a histogram selects exactly one marginal and optional bin count")) + marginal=indices[1:rank] + all(i -> i isa Integer && !(i isa Bool),marginal) || throw(ArgumentError("histograms require integer marginal coordinates")) + bins=length(indices)==rank ? nothing : last(indices) + model=observe(source,histograms,selector,point,marginal...,bins) + T=typeof(float(first(model.edges))) + factor=Units.scale_factor(Units.native_unit(q,basis(core)),target,T) + model=detach(model,factor) + retained_samples=samples(source)===nothing ? nothing : + vec(detach(observe(source,samples,selector,point,marginal...,Colon()),factor)) + edges=model.edges + probabilities=model.density.*diff(edges) + counts=retained_samples===nothing ? nothing : begin + count=zeros(Int,length(model.density)) + for value in retained_samples + index=value==last(edges) ? length(count) : clamp(searchsortedlast(edges,value),1,length(count)) + count[index]+=1 + end + count + end + empirical=retained_samples===nothing ? nothing : + (x=sort(retained_samples),y=collect(1:length(retained_samples))./length(retained_samples)) + model_cdf=(x=copy(edges),y=cumulative_probability.(Ref(model),edges)) + qq=retained_samples===nothing ? nothing : quantile_pairs(model,retained_samples) + distribution=(edges=copy(edges),empirical_cdf=empirical,model_cdf,qq) + values=(lower=edges[1:end-1],upper=edges[2:end],density=copy(model.density), + probability=probabilities,count=counts===nothing ? fill(missing,length(probabilities)) : counts) + coordinates=merge(_uq_coordinates(core,selector,marginal),(kind=:histogram,)) + return (request,quantity=q,family=:statistics,statistic=:histogram,values,unit=target,basis=basis(core),coordinates, + thresholds=nothing,available=true,engineering_zero=false,clipped=false,missing_reason=nothing, + assumptions=nothing,distribution,ordinate_units=(density=Units.UnitExpr(target.denominator,target.numerator), + probability=Units.units(:base,:dimensionless),count=Units.units(:base,:dimensionless))) + end + acquisition_indices=indices + if product===samples && !isempty(indices) + rank=core isa Engine.LineParameters ? 3 : 1 + length(indices)==rank && (acquisition_indices=(indices...,Colon())) + end + values=observe(source,identity...,point,acquisition_indices...) + T=typeof(float(real(nominal(zero(eltype(values)))))) + factor=Units.scale_factor(Units.native_unit(q,basis(core)),target,T) + coordinate_indices=product===samples && !isempty(indices) ? indices[1:(core isa Engine.LineParameters ? 3 : 1)] : indices + coordinates=_uq_coordinates(core,selector,coordinate_indices) + if product===samples + rank=core isa Engine.LineParameters ? 3 : 1 + trial_index=length(indices)>rank ? indices[rank+1] : Colon() + coordinates=merge(coordinates,(kind=:samples,indices=(coordinates.indices...,trial_index),trials=observation_indices(trial_index,trial_count(source,point)))) + end + # These selected estimators or individual trials leave primary uncertain numbers + # unchanged. A retained standard deviation is never clipped. + available=Engine.resolution_available.(values) + retained=map((value,valid) -> valid ? value : missing,values,available) + statistic=product===samples ? :samples : last(identity) isa Base.Fix2 ? + Symbol("quantile_",last(identity).x) : nameof(last(identity)) + return (request,quantity=q,family=:statistics,statistic,values=detach(retained,factor), + unit=target,basis=basis(core),coordinates,thresholds=nothing,available, + engineering_zero=false,clipped=false,missing_reason=map(valid -> valid ? nothing : :nonfinite_value,available), + assumptions=nothing,distribution) +end + +""" +$(TYPEDSIGNATURES) + +Detach one UQ point using its owner's point-indexed accessors. Product requests +omit the point index because `point` already identifies the atomic observation. +`(statistics,R,mean)` retains an estimator and +`(histograms,R,1,2,3,20)` retains a marginal distribution with up to twenty bins. +Primary requests follow the ordinary primary-result pair rules. +""" +function Commons.ObservedResult(source::Union{MonteCarloResult,LinearErrorResult},point::Integer, + requests::Tuple=();comparisons=(),timings=(;),gridpoint=nothing,clip::Bool=true, + atol=nothing,units::Tuple=(),length_unit::Symbol=:kilo,frequency_unit::Symbol=:base, + quantity_units=nothing,frequencies=nothing,complete_pairs::Bool=false) + selected=Commons.observation_requests(source,requests;point,complete_pairs).retained + isempty(units) || length(units)==length(selected) || throw(DimensionMismatch("units must align with retained requests")) + targets = if isempty(units) + Commons.unit_targets(selected, basis(source); + length_prefix=length_unit, overrides=quantity_units) + else + map(selected, units) do request, unit + Units.display_unit(request_quantity(request), basis(source), unit; + length_prefix=length_unit) + end + end + description=gridpoint===nothing ? Commons.observation_gridpoint(source[point]) : gridpoint + sampling=source isa MonteCarloResult ? merge(confidence(source,point), + (frequencies=source[point] isa Engine.LineParameters ? detach(Engine.frequencies(source[point])) : + [source[point].frequency],basis=basis(source))) : nothing + description=merge(description,(sampling, + uncertainty_descriptions=_uncertainty_descriptions(source isa MonteCarloResult ? MonteCarlo : LinearError), + uncertainty=(estimator=source isa MonteCarloResult ? :empirical : :first_order, + representation=source isa MonteCarloResult ? :marginal_mean_std : :dependency_preserving))) + quantities=map(eachindex(selected)) do index + request=selected[index] + unit=targets[index] + record=Commons.observation_quantity(source,point,request;unit,clip,atol,frequencies) + if record.coordinates.frequencies!==nothing + f=record.coordinates.frequencies + target=Units.units(frequency_unit,:hertz) + factor=Units.scale_factor(Units.units(:base,:hertz),target,typeof(float(nominal(first(f))))) + record=merge(record,(coordinates=merge(record.coordinates,(frequencies=detach(f,factor),frequency_unit=target)),)) + end + record + end + return Commons.ObservedResult(description,quantities,collect(comparisons),timings) +end + +function observables(source::Union{MonteCarloResult,LinearErrorResult},requests::Tuple=(); + comparisons=(),timings=(;),kwargs...) + return [begin + id=Commons.observation_gridpoint(source[point]).id + errors=filter(record -> record.result_id==id,comparisons) + recorded=timings isa NamedTuple ? timings : begin + matched=filter(record -> record.result_id==id,timings) + length(matched)<=1 || throw(ArgumentError("duplicate point timings")) + isempty(matched) ? (;) : only(matched) + end + Commons.ObservedResult(source,point,requests;comparisons=errors,timings=recorded,kwargs...) + end for point in eachindex(source)] +end diff --git a/src/uq/plot.jl b/src/uq/plot.jl deleted file mode 100644 index 7ad8b9578..000000000 --- a/src/uq/plot.jl +++ /dev/null @@ -1,709 +0,0 @@ -using Makie -import Makie: plot - -using ..Engine: - Engine, - SeriesImpedance, - ShuntAdmittance, - UnitSpec, - LP_FIG_SIZE, - quantity_scale, - length_scale, - normalize_quantity_units, - composite_unit, - resolve_quantity_prefix, - autoscale_axis, - _axis_label, - _ICON_FN, - get_description, - get_unit_symbol, - ComponentMetadata - -include("../plotbuilder/plothelpers.jl") - -struct MCPlotHistSpec - quantity::Symbol - symbol::String - title::String - xlabel::String - ylabel::String - values::Union{Nothing, Vector{<:Real}} - pdf_obj::Union{Nothing, LineParametersPDF} - bins::Vector{<:Real} - x_exp::Int - fig_size::Union{Nothing, Tuple{Int, Int}} - normalization::Symbol - data::Symbol - mode::Symbol -end - - - -function _mc_quantity_metadata() - sdesc = get_description(SeriesImpedance(zeros(1, 1, 1))) - symb = Engine.get_symbol(SeriesImpedance(zeros(1, 1, 1))) - sunit = get_unit_symbol(SeriesImpedance(zeros(1, 1, 1))) - - adesc = get_description(ShuntAdmittance(zeros(1, 1, 1))) - asymb = Engine.get_symbol(ShuntAdmittance(zeros(1, 1, 1))) - aunit = get_unit_symbol(ShuntAdmittance(zeros(1, 1, 1))) - - return Dict( - :R => ComponentMetadata( - :resistance, - :resistance, - symb.resistance, - sdesc.resistance, - symb.resistance, - UnitSpec(sunit.resistance, true), - ), - :L => ComponentMetadata( - :inductance, - :inductance, - symb.inductance, - sdesc.inductance, - symb.inductance, - UnitSpec(sunit.inductance, true), - ), - :C => ComponentMetadata( - :capacitance, - :capacitance, - asymb.capacitance, - adesc.capacitance, - asymb.capacitance, - UnitSpec(aunit.capacitance, true), - ), - :G => ComponentMetadata( - :conductance, - :conductance, - asymb.conductance, - adesc.conductance, - asymb.conductance, - UnitSpec(aunit.conductance, true), - ), - ) -end - -const _MC_META = _mc_quantity_metadata() - -_quantity_metadata(q::Symbol) = - get(_MC_META, q) do - Base.error("Unsupported quantity $(q); use one of :R, :L, :C, :G") - end - -function _parse_values_ref(values, ijk) - if ijk === nothing - if values isa Expr && values.head === :ref && length(values.args) == 4 - q = values.args[1] - q isa Symbol || - Base.error("Expected values symbol as first argument in $(values)") - i, j, k = values.args[2:4] - return q, (Int(i), Int(j), Int(k)) - else - Base.error( - "Provide values as Expr like :R[1,1,1] or pass indices via `ijk = (i,j,k)`", - ) - end - else - ijk isa NTuple{3, Int} || - Base.error("ijk must be NTuple{3,Int}, got $(typeof(ijk))") - values isa Symbol || - Base.error("values must be Symbol when ijk is provided; got $(typeof(values))") - return values, ijk - end -end - -function _build_hist_spec( - obj::LineParametersMC, - values_expr; - ijk::Union{Nothing, NTuple{3, Int}} = nothing, - length_unit::Symbol = :kilo, - fig_size::Union{Nothing, Tuple{Int, Int}} = LP_FIG_SIZE, - per_length::Bool = true, - quantity_units = nothing, - nbins::Union{Nothing, Int} = nothing, - normalization::Symbol = :none, - data::Symbol = :samples, - mode::Symbol = :hist, -) - # Force data=:both if mode is not :hist - plot_data = (mode == :hist) ? data : :both - - vals = nothing - pdf_obj = nothing - - values_sym, _ijk = _parse_values_ref(values_expr, ijk) - meta = _quantity_metadata(values_sym) - units = normalize_quantity_units(quantity_units) - q_prefix = resolve_quantity_prefix(meta.quantity, units) - c_scale = (per_length ? length_scale(length_unit) : 1.0) * quantity_scale(q_prefix) - i, j, k = _ijk - - # --- Load Data (uses plot_data) --- - if plot_data == :samples || plot_data == :both - obj.samples === nothing && - Base.error("mode=:$mode requires samples, but none are available.") - samps = getproperty(obj.samples, values_sym) - - max_i, max_j, max_k, _ = size(samps) - (1 <= i <= max_i && 1 <= j <= max_j && 1 <= k <= max_k) || Base.error( - "indices (i=$(i), j=$(j), k=$(k)) out of bounds for samples size $(size(samps))", - ) - - raw_vals = @view samps[i, j, k, :] - vals = collect(raw_vals) .* c_scale - end - if plot_data == :pdf || plot_data == :both - obj.pdf === nothing && - Base.error("mode=:$mode requires PDF, but none is available.") - pdfs = getproperty(obj.pdf, values_sym) - - max_i, max_j, max_k = size(pdfs) - (1 <= i <= max_i && 1 <= j <= max_j && 1 <= k <= max_k) || Base.error( - "indices (i=$(i), j=$(j), k=$(k)) out of bounds for pdf size $(size(pdfs))", - ) - - raw_pdf = pdfs[i, j, k] - scaled_edges = raw_pdf.edges .* c_scale - scaled_dens = raw_pdf.dens ./ c_scale - pdf_obj = LineParametersPDF(scaled_edges, scaled_dens) - end - - # --- Binning (Conditional) & Scaling --- - local bin_edges::Vector{<:Real} - local current_norm::Symbol - local x_exp::Int - - if mode == :hist - if pdf_obj !== nothing - bin_edges = pdf_obj.edges - current_norm = :pdf - elseif vals !== nothing - current_norm = normalization - hist_nbins = isnothing(nbins) ? _auto_nbins(vals) : nbins - h_fit = fit(Histogram, vals; nbins = hist_nbins, closed = :left) - bin_edges = collect(h_fit.edges[1]) - else - error("No data (samples or PDF) to plot.") - end - else - bin_edges = Float64[] # Not used - current_norm = :none # Not used - end - - # Autoscale is *always* needed - if vals !== nothing - _, x_exp = autoscale_axis(vals) - else - _, x_exp = autoscale_axis(pdf_obj.edges) - end - - # --- Labels and Title (Conditional) --- - local title::String - local xlabel::String - local ylabel::String - - xlabel_unit = composite_unit(q_prefix, meta.unit.symbol, per_length, length_unit) - base_xlabel = string(meta.axis_label, " [", xlabel_unit, "]") - freq_str = @sprintf("%.4g", obj.f[k]) - - if mode == :hist - title = string(meta.title, " histogram @ f=", freq_str, " Hz") - xlabel = base_xlabel - ylabel = - current_norm == :none ? "count" : - (current_norm == :pdf ? "density" : String(current_norm)) - elseif mode == :ecdf - title = string(meta.title, " CDF @ f=", freq_str, " Hz") - xlabel = base_xlabel - ylabel = "cumulative probability" - elseif mode == :qq - title = string(meta.title, " Q-Q plot @ f=", freq_str, " Hz") - xlabel = "sample quantiles" - ylabel = "model quantiles" - end - - return MCPlotHistSpec( - values_sym, meta.symbol, title, xlabel, ylabel, - vals, pdf_obj, bin_edges, x_exp, - fig_size, current_norm, plot_data, mode, - ) -end - -function _hist_specs( - obj::LineParametersMC, - values_expr; - ijk::Union{Nothing, NTuple{3, Int}} = nothing, - length_unit::Symbol = :kilo, - fig_size::Union{Nothing, Tuple{Int, Int}} = LP_FIG_SIZE, - per_length::Bool = true, - quantity_units = nothing, - nbins::Union{Nothing, Int} = nothing, - normalization::Symbol = :none, - data::Symbol = :samples, - mode::Symbol = :hist, -) - spec = _build_hist_spec( # Calls LP-MC builder - obj, - values_expr; - ijk = ijk, - length_unit = length_unit, - fig_size = fig_size, - per_length = per_length, - quantity_units = quantity_units, - nbins = nbins, - normalization = normalization, - data = data, - mode = mode, - ) - return [spec] -end - -function _default_export_path_hist(spec::MCPlotHistSpec) - base_title = strip(spec.title) - name = _sanitize_filename_plot(base_title) - timestamp = Dates.format(Dates.now(), EXPORT_TIMESTAMP_FORMAT) - filename = string(name, "_", timestamp, ".", EXPORT_EXTENSION) - return joinpath(pwd(), filename) -end - -function _save_hist_export(spec::MCPlotHistSpec, axis) - fig = _build_hist_export(spec) - trim!(fig.layout) - path = _default_export_path_hist(spec) - Makie.save(path, fig) - return path -end - -function _build_hist_export(spec::MCPlotHistSpec) - backend_ctx = _make_window( - BackendHandler, - :cairo; - icons = _ICON_FN, - icons_font = ICON_TTF, - interactive_override = false, - use_latex_fonts = true, - ) - pipeline_kwargs = - spec.fig_size === nothing ? - (; initial_status = "") : - (; fig_size = spec.fig_size, initial_status = "") - assembly = with_plot_theme(backend_ctx; mode = :export) do - _run_plot_pipeline( - backend_ctx, - (fig_ctx, ctx, axis) -> _build_hist_plot!(fig_ctx, ctx, axis, spec); - pipeline_kwargs..., - ) - end - ensure_export_background!(assembly.figure) - return assembly.figure -end - -function _render_hist_spec( - spec::MCPlotHistSpec; # <-- Unified spec - backend = nothing, - display_plot::Bool = true, -) - n = next_fignum() - backend_ctx = _make_window( - BackendHandler, - backend; - title = "Fig. $(n) – $(spec.title)", - icons = _ICON_FN, - icons_font = ICON_TTF, - ) - pipeline_kwargs = - spec.fig_size === nothing ? - (; initial_status = " ") : - (; fig_size = spec.fig_size, initial_status = " ") - assembly = with_plot_theme(backend_ctx) do - _run_plot_pipeline( - backend_ctx, - # Calls the single, unified _build_hist_plot! - (fig_ctx, ctx, axis) -> _build_hist_plot!(fig_ctx, ctx, axis, spec); - pipeline_kwargs..., - ) - end - if display_plot - _display!(backend_ctx, assembly.figure; title = spec.title) - end - return assembly -end - -function _display!(backend_ctx, fig::Makie.Figure; title::AbstractString = "") - if backend_ctx.interactive && backend_ctx.window !== nothing - display(backend_ctx.window, fig) - if !isempty(title) && hasproperty(backend_ctx.window, :title) - backend_ctx.window.title[] = title - end - else - BackendHandler.renderfig(fig) - end - return nothing -end - -function _build_hist_plot!(fig_ctx, ctx, axis, spec::MCPlotHistSpec) - axis.title = spec.title - - x_scale = 10.0 ^ spec.x_exp - axis.ytickformat[] = vals -> TICKFORMATTER(vals) - - # Conditional axis labels - if spec.mode == :qq - # Q-Q plot: both axes get the exponent - axis.xlabel = _axis_label(spec.xlabel, spec.x_exp) - axis.ylabel = _axis_label(spec.ylabel, spec.x_exp) - else - # Hist/ECDF: only x-axis gets the exponent - axis.xlabel = _axis_label(spec.xlabel, spec.x_exp) - axis.ylabel = spec.ylabel - end - - # --- PLOTTING LOGIC SWITCH --- - if spec.mode == :hist - - scaled_edges = spec.bins ./ x_scale - - if spec.data == :samples || spec.data == :both - vals_scaled = spec.values ./ x_scale - hist!( - axis, vals_scaled; - bins = scaled_edges, - normalization = spec.normalization, - color = :steelblue, strokecolor = :white, strokewidth = 0.5, - label = "samples", - ) - - end - - if spec.data == :pdf || spec.data == :both - pdf = spec.pdf_obj - dens_scaled = pdf.dens .* x_scale - y_values = [dens_scaled; dens_scaled[end]] - stairs!( - axis, scaled_edges, y_values; - step = :post, color = :red, linewidth = 2, overdraw = true, - label = "model PDF", - ) - - end - - Makie.autolimits!(axis) - ylims!(axis, 0, nothing) # Glue bars to x-axis - - elseif spec.mode == :ecdf - # --- ECDF PLOT --- - pdf = spec.pdf_obj - vals_scaled = spec.values ./ x_scale - ecdf_func = ecdf(vals_scaled) - - # Define plot range from scaled PDF edges - xmin = minimum(pdf.edges) / x_scale - xmax = maximum(pdf.edges) / x_scale - pad = (xmax - xmin) * 0.05 - xs = range(xmin - pad, xmax + pad, length = 500) - - - # 1. Plot Model CDF (Theory) - # We must feed *physical* values (xs .* x_scale) to the cdf function - model_cdf_data = cdf.(Ref(pdf), xs .* x_scale) - lines!(axis, xs, model_cdf_data, - color = :red, linewidth = 2, label = "model CDF", - ) - - # 2. Plot ECDF (Data) - lines!(axis, xs, ecdf_func.(xs), - color = :blue, linestyle = :dash, linewidth = 2, label = "empirical", - ) - - - Makie.autolimits!(axis) - ylims!(axis, 0, nothing) # CDFs are bounded [0, 1] - - elseif spec.mode == :qq - # --- Q-Q PLOT --- - vals_scaled = spec.values ./ x_scale - sample_quantiles = sort(vals_scaled) - n = length(sample_quantiles) - probs = ((1:n) .- 0.5) ./ n - - pdf = spec.pdf_obj - s = sampler(pdf) # Sampler on physically-scaled PDF - - model_quantiles_physical = quantile.(Ref(s), probs) - model_quantiles_scaled = model_quantiles_physical ./ x_scale - - # 1. Plot the quantiles - scatter!(axis, sample_quantiles, model_quantiles_scaled, - color = :steelblue, markersize = 6, label = "quantiles", - ) - - - # 2. Plot the y=x line - diag_min = min(sample_quantiles[1], model_quantiles_scaled[1]) - diag_max = max(sample_quantiles[end], model_quantiles_scaled[end]) - lines!(axis, [diag_min, diag_max], [diag_min, diag_max], - color = :black, linestyle = :dash, linewidth = 2, label = "perfect fit", - ) - - Makie.autolimits!(axis) - # No ylims! for Q-Q - end - - # --- Buttons (identical) --- - buttons = [ - ControlButtonSpec( - (_ctx, _btn) -> (Makie.autolimits!(axis); nothing), # Use autolimits - icon = MI_REFRESH, - on_success = ControlReaction(status_string = "Axis limits reset"), - ), - ControlButtonSpec( - (_ctx, _btn) -> _save_hist_export(spec, axis), - icon = MI_SAVE, - on_success = ControlReaction( - status_string = path -> string("Saved SVG to ", basename(path)), - ), - ), - ] - - legend_builder = - parent -> - Makie.Legend( - parent, - axis; - orientation = :vertical, - ) - - # --- Return (identical) --- - return PlotBuildArtifacts( - axis = axis, - legends = legend_builder, - colorbars = Any[], - control_buttons = buttons, - control_toggles = ControlToggleSpec[], - status_message = nothing, - ) -end - -function plot( - obj::LineParametersMC, - values_expr; - ijk::Union{Nothing, NTuple{3, Int}} = nothing, - length_unit::Symbol = :kilo, - fig_size::Union{Nothing, Tuple{Int, Int}} = LP_FIG_SIZE, - per_length::Bool = true, - quantity_units = nothing, - nbins::Union{Nothing, Int} = nothing, - normalization::Symbol = :none, - data::Symbol = :samples, - mode::Symbol = :hist, - backend = nothing, - display_plot::Bool = true, -) - data in (:samples, :pdf, :both) || - Base.error("`data` must be one of :samples, :pdf, or :both") - - mode in (:hist, :ecdf, :qq) || - Base.error("`mode` must be one of :hist, :ecdf, or :qq") - - specs = _hist_specs( - obj, - values_expr; - ijk = ijk, - length_unit = length_unit, - fig_size = fig_size, - per_length = per_length, - quantity_units = quantity_units, - nbins = nbins, - normalization = normalization, - data = data, - mode = mode, - ) - spec = first(specs) - return _render_hist_spec(spec; backend = backend, display_plot = display_plot) -end - - -# ### Histogram methods for CableDesignMC -> CableDesignHistSpec - -const _CABLE_DESIGN_SUPPORTED_QUANTITIES = (:R, :L, :C) - -function _parse_cabledesign_quantity(values_expr) - values_expr isa Symbol && return values_expr - values_expr isa Expr && - values_expr.head === :quote && - length(values_expr.args) == 1 && - values_expr.args[1] isa Symbol && - return values_expr.args[1] - Base.error( - "Provide values as Symbol (:R, :L or :C) when plotting CableDesignMC samples", - ) -end - -# --- Builder for CableDesign (New, refactored logic) --- -function _build_hist_spec( - obj::CableDesignMC, - values_expr; - length_unit::Symbol = :kilo, - fig_size::Union{Nothing, Tuple{Int, Int}} = LP_FIG_SIZE, - per_length::Bool = true, - quantity_units = nothing, - nbins::Union{Nothing, Int} = nothing, - normalization::Symbol = :none, - data::Symbol = :samples, - mode::Symbol = :hist, # <-- ADDED -) - # MODIFIED: Force data=:both - plot_data = (mode == :hist) ? data : :both - - vals = nothing - pdf_obj = nothing - - values_sym = _parse_cabledesign_quantity(values_expr) - values_sym in _CABLE_DESIGN_SUPPORTED_QUANTITIES || Base.error( - "CableDesignMC provides samples only for :R, :L and :C; got $(values_sym)", - ) - - meta = _quantity_metadata(values_sym) - units = normalize_quantity_units(quantity_units) - q_prefix = resolve_quantity_prefix(meta.quantity, units) - c_scale = (per_length ? length_scale(length_unit) : 1.0) * quantity_scale(q_prefix) - - # --- Load Data (uses plot_data) --- - if plot_data == :samples || plot_data == :both - obj.samples === nothing && - Base.error("mode=:$mode requires samples, but none are available.") - samps = getproperty(obj.samples, values_sym) - vals = collect(samps) .* c_scale - end - if plot_data == :pdf || plot_data == :both - obj.pdf === nothing && - Base.error("mode=:$mode requires PDF, but none is available.") - raw_pdf = getproperty(obj.pdf, values_sym) - scaled_edges = raw_pdf.edges .* c_scale - scaled_dens = raw_pdf.dens ./ c_scale - pdf_obj = LineParametersPDF(scaled_edges, scaled_dens) - end - - # --- Binning (Conditional) & Scaling --- - local bin_edges::Vector{<:Real} - local current_norm::Symbol - local x_exp::Int - - if mode == :hist - if pdf_obj !== nothing - bin_edges = pdf_obj.edges - current_norm = :pdf - elseif vals !== nothing - current_norm = normalization - hist_nbins = isnothing(nbins) ? _auto_nbins(vals) : nbins - h_fit = fit(Histogram, vals; nbins = hist_nbins, closed = :left) - bin_edges = collect(h_fit.edges[1]) - else - error("No data (samples or PDF) to plot.") - end - else - bin_edges = Float64[] - current_norm = :none - end - - if vals !== nothing - _, x_exp = autoscale_axis(vals) - else - _, x_exp = autoscale_axis(pdf_obj.edges) - end - - # --- Labels and Title (Conditional) --- - local title::String - local xlabel::String - local ylabel::String - - xlabel_unit = composite_unit(q_prefix, meta.unit.symbol, per_length, length_unit) - base_xlabel = string(meta.axis_label, " [", xlabel_unit, "]") - - if mode == :hist - title = string(meta.title, " histogram (base values)") - xlabel = base_xlabel - ylabel = - current_norm == :none ? "count" : - (current_norm == :pdf ? "density" : String(current_norm)) - elseif mode == :ecdf - title = string(meta.title, " CDF (base values)") - xlabel = base_xlabel - ylabel = "cumulative probability" - elseif mode == :qq - title = string(meta.title, " Q-Q plot (base values)") - xlabel = "sampled quantiles" - ylabel = "model quantiles" - end - - return MCPlotHistSpec( - values_sym, meta.symbol, title, xlabel, ylabel, - vals, pdf_obj, bin_edges, x_exp, - fig_size, current_norm, plot_data, mode, - ) -end - -function _hist_specs( - obj::CableDesignMC, - values_expr; - length_unit::Symbol = :kilo, - fig_size::Union{Nothing, Tuple{Int, Int}} = LP_FIG_SIZE, - per_length::Bool = true, - quantity_units = nothing, - nbins::Union{Nothing, Int} = nothing, - normalization::Symbol = :none, - data::Symbol = :samples, - mode::Symbol = :hist, -) - spec = _build_hist_spec( # Calls CD-MC builder - obj, - values_expr; - length_unit = length_unit, - fig_size = fig_size, - per_length = per_length, - quantity_units = quantity_units, - nbins = nbins, - normalization = normalization, - data = data, - mode = mode, - ) - return [spec] -end - -# --- hist for CableDesignMC --- -function plot( - obj::CableDesignMC, - values_expr; - length_unit::Symbol = :kilo, - fig_size::Union{Nothing, Tuple{Int, Int}} = LP_FIG_SIZE, - quantity_units = nothing, - nbins::Union{Nothing, Int} = nothing, # <-- Now Union{Nothing, Int} - normalization::Symbol = :none, - data::Symbol = :samples, # <-- New arg - mode::Symbol = :hist, - backend = nothing, - display_plot::Bool = true, -) - data in (:samples, :pdf, :both) || - Base.error("`data` must be one of :samples, :pdf, or :both") - - mode in (:hist, :ecdf, :qq) || - Base.error("`mode` must be one of :hist, :ecdf, or :qq") - - specs = _hist_specs( # Dispatches to CableDesignMC version - obj, - values_expr; - length_unit = length_unit, - fig_size = fig_size, - per_length = true, - quantity_units = quantity_units, - nbins = nbins, - normalization = normalization, - data = data, - mode = mode, - ) - spec = first(specs) - return _render_hist_spec(spec; backend = backend, display_plot = display_plot) -end - diff --git a/src/uq/plotspecs/mcstatsplotspec.jl b/src/uq/plotspecs/mcstatsplotspec.jl deleted file mode 100644 index 55df90d3d..000000000 --- a/src/uq/plotspecs/mcstatsplotspec.jl +++ /dev/null @@ -1,88 +0,0 @@ - -using ..UnitHandler: - QuantityTag, default_unit, display_unit, get_label, get_symbol, scale_factor -using ..PlotBuilder: AbstractPlotSpec, PlotBuilder - -struct MCStatsPlotSpec <: AbstractPlotSpec end - -# --- Traits --------------------------------------------------------------- - -PlotBuilder.plot_kind(::Type{MCStatsPlotSpec}) = :line - -# no logscales for now. -PlotBuilder.enable_logscale(::Type{MCStatsPlotSpec}) = () - -# Dispatch on MC container -PlotBuilder.dispatch_on(::Type{MCStatsPlotSpec}) = LineParametersMC - -PlotBuilder.default_figsize(::Type{MCStatsPlotSpec}) = (800, 400) - -# Geometric axes: 2D line plot -PlotBuilder.geom_axes(::Type{MCStatsPlotSpec}) = (:x, :y) - -# Index convention: all MC stats are per-(i,j) element over frequency -PlotBuilder.index_keys(::Type{MCStatsPlotSpec}) = (:i, :j, :k) -PlotBuilder.ranged_keys(::Type{MCStatsPlotSpec}) = (:k,) - -# X is always frequency; Y will depend on user kwarg, so the valid possible quantities are defined below. -PlotBuilder.axis_quantity(::Type{MCStatsPlotSpec}, ::Val{:x}, ::Val{:f}) = - QuantityTag{:freq}() -PlotBuilder.axis_quantity(::Type{MCStatsPlotSpec}, ::Val{:y}, ::Val{:R}) = - QuantityTag{:resistance}() -PlotBuilder.axis_quantity(::Type{MCStatsPlotSpec}, ::Val{:y}, ::Val{:L}) = - QuantityTag{:inductance}() -PlotBuilder.axis_quantity(::Type{MCStatsPlotSpec}, ::Val{:y}, ::Val{:C}) = - QuantityTag{:capacitance}() -PlotBuilder.axis_quantity(::Type{MCStatsPlotSpec}, ::Val{:y}, ::Val{:G}) = - QuantityTag{:conductance}() - -PlotBuilder.data_container(::Type{MCStatsPlotSpec}, ::Val{:x}) = nothing # obj.f -PlotBuilder.data_container(::Type{MCStatsPlotSpec}, ::Val{:y}) = :stats # obj.stats[Sym] - -# PlotBuilder.has_complex_qty(::Type{MCStatsPlotSpec}, ::Val{:y}, ::Val{:R}) = true - -# Define plot title -function PlotBuilder.default_title(::Type{MCStatsPlotSpec}, nt::NamedTuple) - qx = nt.x_quantity - qy = nt.y_quantity - key = nt.field - - y_label = get_label(qy) # "Series resistance" - x_label = get_label(qx) # "Frequency" - - return string(y_label, " ", String(key), " vs. ", x_label) -end - -# Define legend labels -function PlotBuilder.legend_labels(::Type{MCStatsPlotSpec}, nt::NamedTuple) - qy = nt.y_quantity - key = nt.field - i = nt.i - j = nt.j - - y_label = get_symbol(qy) - - entry = string(y_label, "[", i, ",", j, "] ", String(key)) - return [entry] -end - -# Semantic knobs: -# :field → which field from stats NamedTuple -PlotBuilder.input_kwargs(::Type{MCStatsPlotSpec}) = (:field,) - -# Backend knobs this spec forwards to Makie -PlotBuilder.renderer_kwargs(::Type{MCStatsPlotSpec}) = () - -# Defaults for semantic knobs, given the dispatched object -PlotBuilder.input_defaults(::Type{MCStatsPlotSpec}, ::LineParametersMC) = ( - x = :f, - y = :R, - field = :mean, - # i,j,k are handled via index_keys + parse_kwargs (default 1 or :) -) - -PlotBuilder.select_field(::Type{MCStatsPlotSpec}, ::Val{:x}) = nothing -PlotBuilder.select_field(::Type{MCStatsPlotSpec}, ::Val{:y}) = :field # or :mean directly - -# # Defaults for renderer knobs -# PlotBuilder.renderer_defaults(::Type{MCStatsPlotSpec}, ::LineParametersMC) = () diff --git a/src/uq/results.jl b/src/uq/results.jl new file mode 100644 index 000000000..dbc2e64ec --- /dev/null +++ b/src/uq/results.jl @@ -0,0 +1,420 @@ +""" +$(TYPEDEF) + +Store ordered core results from a [`LinearError`](@ref) computation. + +$(TYPEDFIELDS) +""" +struct LinearErrorResult{T, F, D <: ComputationDetails} <: AbstractUncertaintyResult{T} + "Higher-order formulation used for the computation." + formulation::F + "Uncertainty-bearing core results in Gridspace traversal order." + values::Vector{T} + "Typed supplemental output retained by the propagation." + details::D + + function LinearErrorResult( + formulation::F, + values::Vector{T}, + details::D + ) where {T, F, D <: ComputationDetails} + validate(T, LinearErrorResult) + isempty(details.data) || keys(details.data) == (:points,) || + throw(ArgumentError( + "LinearErrorResult details must be empty or contain only points", + )) + isempty(details.data) || length(details.data.points) == length(values) || + throw(DimensionMismatch( + "retained details must contain one entry per core result", + )) + return new{T, F, D}(formulation, values, details) + end +end + +LinearErrorResult(formulation, values) = LinearErrorResult(formulation, values, ComputationDetails()) + +_monte_carlo_keys(::Type{<:Engine.CableConstants}) = (:R, :L, :C, :G) +_monte_carlo_keys(::Type{<:Engine.LineParameters}) = (:R, :L, :C, :G) + +function _validated_product(product, expected_keys::Tuple, name::AbstractString) + product isa NamedTuple || throw(ArgumentError( + "Monte Carlo $name must be a named tuple", + )) + keys(product) == expected_keys || throw(ArgumentError( + "Monte Carlo $name must contain exactly $(join(expected_keys, ", "))", + )) + return product +end + +function _line_product_shape(product, expected_shape, name::AbstractString) + all(field -> field isa AbstractArray && size(field) == expected_shape, + Base.values(product)) || throw(DimensionMismatch( + "line-parameter $name must match the core matrix and frequency dimensions", + )) + return nothing +end + + +""" +$(TYPEDEF) + +Store uncertainty-bearing core results and their empirical Monte Carlo products. +The marginal representation is materialized during computation. Access reuses +the retained uncertainty-source identities. + +$(TYPEDFIELDS) +""" +struct MonteCarloResult{T, F, ST <: AbstractVector, S, H, D <: ComputationDetails} <: + AbstractUncertaintyResult{T} + "Higher-order formulation used for the computation." + formulation::F + "Stored mean ± sample standard deviation core results in Gridspace traversal order." + values::Vector{T} + "Per-observable sample summaries." + stats::ST + "Retained joint samples, or `nothing` when not requested." + sample_values::S + "Retained marginal histograms, or `nothing` when not requested." + histogram_values::H + "Resolved root random seed." + root_seed::UInt64 + "Deterministic random seed for each Gridspace point." + point_seeds::Vector{UInt64} + "Accepted-trial count used for each Gridspace point." + trial_counts::Vector{Int} + "Typed accepted-trial details and failure diagnostics retained by the propagation." + details::D + + function MonteCarloResult( + formulation::F, + values::Vector{T}, + stats::ST, + sample_values::S, + histogram_values::H, + root_seed::UInt64, + point_seeds::Vector{UInt64}, + trial_counts::Vector{Int}, + details::D + ) where {T, F, ST <: AbstractVector, S, H, D <: ComputationDetails} + return validate(new{T, F, ST, S, H, D}( + formulation, + values, + stats, + sample_values, + histogram_values, + root_seed, + point_seeds, + trial_counts, + details + )) + end +end + +# Checks the stored cores, the empirical products of each point against its core result, +# and the retained trial, failure and timing details. +function validate(result::MonteCarloResult{T}) where {T} + (; values, stats, sample_values, histogram_values, point_seeds, trial_counts, details) = + result + validate(T, MonteCarloResult) + isempty(values) && throw(ArgumentError( + "MonteCarloResult requires at least one core result", + )) + isconcretetype(eltype(stats)) || throw(ArgumentError( + "Monte Carlo statistics must use a concrete product type", + )) + length(stats) == length(values) || throw(DimensionMismatch( + "Monte Carlo statistics must contain one entry per core result", + )) + length(point_seeds) == length(values) || throw(DimensionMismatch( + "Monte Carlo seeds must contain one entry per core result", + )) + length(trial_counts) == length(values) || throw(DimensionMismatch( + "Monte Carlo trial counts must contain one entry per core result", + )) + sample_values === nothing || length(sample_values) == length(values) || + throw(DimensionMismatch( + "retained samples must contain one entry per core result", + )) + histogram_values === nothing || length(histogram_values) == length(values) || + throw(DimensionMismatch( + "retained histograms must contain one entry per core result", + )) + sample_values === nothing || sample_values isa AbstractVector || throw( + ArgumentError("retained samples must be stored in a vector"), + ) + histogram_values === nothing || histogram_values isa AbstractVector || throw( + ArgumentError("retained histograms must be stored in a vector"), + ) + sample_values === nothing || isconcretetype(eltype(sample_values)) || throw( + ArgumentError("retained samples must use a concrete product type"), + ) + histogram_values === nothing || isconcretetype(eltype(histogram_values)) || throw( + ArgumentError("retained histograms must use a concrete product type"), + ) + if T <: Union{Engine.CableConstants, Engine.LineParameters} + Engine.has_uncertainty_type(eltype(observe(first(values),R))) || throw(ArgumentError( + "MonteCarloResult requires stored uncertainty-bearing cores; materialize the empirical summaries before construction")) + expected_keys = _monte_carlo_keys(T) + for point in eachindex(values) + trials = trial_counts[point] + trials > 0 || throw(ArgumentError("Monte Carlo trial counts must be positive")) + statistics_product = _validated_product( + stats[point], expected_keys, "statistics products") + sample_product = sample_values === nothing ? nothing : _validated_product( + sample_values[point], expected_keys, "sample products") + histogram_product = histogram_values === nothing ? nothing : _validated_product( + histogram_values[point], expected_keys, "histogram products") + value = values[point] + if value isa Engine.CableConstants + count = length(value) + all(field -> field isa AbstractVector && length(field) == count, + Base.values(statistics_product)) || throw(DimensionMismatch( + "cable-constant summaries must match the assembly count", + )) + all(summary -> summary isa SampleSummary && summary.n == trials, + Iterators.flatten(Base.values(statistics_product))) || throw(DimensionMismatch( + "cable-constant summaries must contain the Monte Carlo trial count", + )) + if sample_product !== nothing + all(sample -> sample isa AbstractMatrix && size(sample) == (count, trials), + Base.values(sample_product)) || throw(DimensionMismatch( + "cable-constant samples must contain assembly and trial dimensions", + )) + end + if histogram_product !== nothing + all(field -> field isa AbstractVector && length(field) == count, + Base.values(histogram_product)) || throw(DimensionMismatch( + "cable-constant histograms must match the assembly count", + )) + all(histogram -> histogram isa HistogramDensity, + Iterators.flatten(Base.values(histogram_product))) || throw(ArgumentError( + "cable-constant histograms must contain HistogramDensity values", + )) + end + else + core_shape = size(observe(value, Engine.Z)) + _line_product_shape(statistics_product, core_shape, "summaries") + all(summary -> summary isa SampleSummary && summary.n == trials, + Iterators.flatten(Base.values(statistics_product))) || throw(DimensionMismatch( + "line-parameter summaries must contain the Monte Carlo trial count", + )) + if sample_product !== nothing + sample_shape = (core_shape..., trials) + _line_product_shape(sample_product, sample_shape, "samples") + end + if histogram_product !== nothing + _line_product_shape(histogram_product, core_shape, "histograms") + all(histogram -> histogram isa HistogramDensity, + Iterators.flatten(Base.values(histogram_product))) || throw(ArgumentError( + "line-parameter histograms must contain HistogramDensity values", + )) + end + end + end + else + all(>(0), trial_counts) || throw(ArgumentError( + "Monte Carlo trial counts must be positive", + )) + end + if !isempty(details.data) + fields = Tuple(key for key in keys(details.data) if key !== :timing) + fields in ((), (:trials, :failures, :failure_summary), + (:trials, :failures, :failure_summary, :clearance)) || throw(ArgumentError( + "MonteCarloResult details must contain timing, the full trial/failure bundle, or both", + )) + point_count = length(values) + if haskey(details.data, :timing) + details.data.timing isa AbstractVector || throw(ArgumentError( + "Monte Carlo timing must be a vector of population vectors")) + end + all(length(product) == point_count + for product in Base.values(details.data)) || + throw(DimensionMismatch( + "retained Monte Carlo details must contain one entry per Gridspace point", + )) + if haskey(details.data, :timing) + timing = details.data.timing + for point in eachindex(values) + records = timing[point] + records isa AbstractVector && all(record -> record isa NamedTuple, records) || + throw(ArgumentError("Monte Carlo timing must contain vectors of named tuples")) + length(records) == trial_counts[point] || throw(DimensionMismatch( + "Monte Carlo timing must contain one record per accepted trial")) + end + end + if !isempty(fields) + for point in eachindex(values) + records = details.data.trials[point] + failures = details.data.failures[point] + summary = details.data.failure_summary[point] + length(records) == trial_counts[point] || throw(DimensionMismatch( + "retained details must contain one entry per accepted Monte Carlo trial", + )) + failures isa AbstractVector || throw(ArgumentError( + "retained Monte Carlo failures must be stored in vectors", + )) + for record in failures + record isa NamedTuple && + keys(record) == (:attempt, :target_trial, :stage, :sample, :error) || + throw(ArgumentError( + "Monte Carlo failure records must contain attempt, target_trial, stage, sample, and error", + )) + record.attempt isa Int && record.attempt > 0 || throw(ArgumentError( + "Monte Carlo failure attempts must be positive integers", + )) + record.target_trial isa Int && record.target_trial > 0 || throw(ArgumentError( + "Monte Carlo failure target trials must be positive integers", + )) + record.stage in (:sample, :build, :compute) || throw(ArgumentError( + "Monte Carlo failure stages must be :sample, :build, or :compute", + )) + error = record.error + error isa NamedTuple && keys(error) == (:type, :message, :stack) || throw( + ArgumentError( + "Monte Carlo failure errors must contain type, message, and stack", + ), + ) + error.type isa String && error.message isa String || throw(ArgumentError( + "Monte Carlo failure type and message summaries must be strings", + )) + error.stack isa AbstractVector || throw(ArgumentError( + "Monte Carlo failure stacks must be vectors", + )) + all(error.stack) do frame + frame isa NamedTuple && + keys(frame) == (:function_name, :file, :line) && + frame.function_name isa String && frame.file isa String && frame.line isa Int + end || throw(ArgumentError( + "Monte Carlo failure stack frames must contain function_name, file, and line", + )) + end + accepted = trial_counts[point] + summary isa NamedTuple && keys(summary) == ( + :attempts, :accepted, :failed, :acceptance_rate, :by_type, :by_stage + ) || throw(ArgumentError( + "Monte Carlo failure summaries have an invalid schema", + )) + summary.attempts isa Int && summary.attempts > 0 || throw(ArgumentError( + "Monte Carlo failure-summary attempts must be positive integers", + )) + summary.accepted == accepted || throw(DimensionMismatch( + "Monte Carlo failure-summary accepted counts must match trial counts", + )) + summary.failed == length(failures) || throw(DimensionMismatch( + "Monte Carlo failure-summary failed counts must match retained failures", + )) + summary.attempts == summary.accepted + summary.failed || throw(DimensionMismatch( + "Monte Carlo failure-summary attempts must equal accepted plus failed trials", + )) + summary.acceptance_rate == summary.accepted / summary.attempts || throw( + ArgumentError( + "Monte Carlo failure-summary acceptance rates are inconsistent", + ), + ) + if haskey(details.data, :clearance) + clearance = details.data.clearance[point] + keys(clearance) == (:adjustments, :max_displacement_m) && + clearance.adjustments isa Int && + clearance.adjustments >= 0 && + isfinite(clearance.max_displacement_m) && + clearance.max_displacement_m >= 0 || throw(ArgumentError( + "Monte Carlo clearance diagnostics must contain a nonnegative count and displacement")) + end + end + end + end + return result +end + +function MonteCarloResult( + formulation, + values, + stats, + sample_values, + histogram_values, + root_seed, + point_seeds, + trial_counts +) + return MonteCarloResult( + formulation, + values, + stats, + sample_values, + histogram_values, + root_seed, + point_seeds, + trial_counts, + ComputationDetails() + ) +end + +details(value::LinearErrorResult) = value.details +details(value::MonteCarloResult) = value.details + +"""Expose retained UQ products for owner-controlled scientific serialization.""" +function Base.NamedTuple(value::MonteCarloResult) + return (formulation=value.formulation, values=value.values, statistics=value.stats, + samples=value.sample_values,histograms=value.histogram_values,root_seed=value.root_seed, + point_seeds=value.point_seeds,trial_counts=value.trial_counts,details=value.details) +end +function Base.NamedTuple(value::LinearErrorResult) + return (formulation=value.formulation,values=value.values,details=value.details) +end + +""" +$(TYPEDSIGNATURES) + +Transport every uncertainty-bearing linear-error result into one +target-bearing downstream problem point while preserving source cardinality +and order. +""" +function ParametricBuilder.Gridspace{Target}( + source::LinearErrorResult +) where {Target} + return ParametricBuilder.Gridspace{Target}(Target, (source,)) +end + +function ParametricBuilder.Gridspace{Target}( + source::MonteCarloResult{T} +) where {Target, T} + target_name = nameof(Target) + throw(ArgumentError( + "Gridspace transport from MonteCarloResult to problem $target_name requires " * + "a transport for result type $T. See `?Gridspace` and " * + "https://electa-git.github.io/LineCableModels.jl/dev/gridspace/" * + "#Transporting-completed-result-spaces", + )) +end + +function ParametricBuilder.Gridspace{Target}( + source::MonteCarloResult{<:Union{Engine.CableConstants,Engine.LineParameters}} +) where {Target} + return ParametricBuilder.Gridspace{Target}(Target, (source,)) +end + +""" +$(TYPEDSIGNATURES) + +Materialize retained R/L/C/G marginal summaries as an uncertainty-bearing core +result using `source`'s physical axes. The summaries include native units and +their standard deviations describe output spread, not uncertainty of the mean. +`details` defaults to the source's supplemental output. MC computation selects +only its point-wide metadata. Neither histogram bins nor sample retention +participate in this construction. +""" +function materialize(source::Engine.CableConstants, + summaries::NamedTuple{(:R,:L,:C,:G)}; details::ComputationDetails=source.details) + return Engine.CableConstants(source.cores, + materialize.(summaries.R), materialize.(summaries.L), + materialize.(summaries.C), materialize.(summaries.G), source.frequency, details) +end + +function materialize(source::Engine.LineParameters, + summaries::NamedTuple{(:R,:L,:C,:G)}; details::ComputationDetails=source.details) + angular = reshape(2π .* frequencies(source), 1, 1, :) + return Engine.LineParameters(source.domain, + complex.(materialize.(summaries.R), materialize.(summaries.L) .* angular), + complex.(materialize.(summaries.G), materialize.(summaries.C) .* angular), + frequencies(source); basis=basis(source), details) +end diff --git a/src/uq/statistics.jl b/src/uq/statistics.jl new file mode 100644 index 000000000..0a7e115bc --- /dev/null +++ b/src/uq/statistics.jl @@ -0,0 +1,253 @@ +""" +$(TYPEDEF) + +Store scalar Monte Carlo statistics for one real-valued observation. + +$(TYPEDFIELDS) +""" +struct SampleSummary{T <: Real} + "Arithmetic mean." + mean::T + "Sample standard deviation." + std::T + "Minimum." + min::T + "5th percentile." + q05::T + "Median." + median::T + "95th percentile." + q95::T + "Maximum." + max::T + "Number of successful samples." + n::Int + + function SampleSummary( + mean::T, + std::T, + min::T, + q05::T, + median::T, + q95::T, + max::T, + n::Integer + ) where {T <: Real} + all(isfinite, (mean, std, min, q05, median, q95, max)) || throw( + ArgumentError("sample statistics must be finite"), + ) + std >= zero(T) || throw(ArgumentError( + "sample standard deviation must be nonnegative", + )) + min <= q05 <= median <= q95 <= max || throw(ArgumentError( + "sample quantiles must be ordered between the minimum and maximum", + )) + n > 0 || throw(ArgumentError("sample count must be positive")) + return new{T}(mean, std, min, q05, median, q95, max, Int(n)) + end +end + +function SampleSummary(mean::Real, std::Real, min::Real, q05::Real, + median::Real, q95::Real, max::Real, n::Integer) + promoted = promote(mean, std, min, q05, median, q95, max) + return SampleSummary(promoted..., n) +end + +function SampleSummary(values::AbstractVector{<:Real}) + isempty(values) && throw(ArgumentError("cannot summarize an empty sample")) + all(isfinite, values) || throw(ArgumentError("sample values must be finite")) + sigma = length(values) == 1 ? zero(float(first(values))) : Statistics.std(values) + promoted = promote( + Statistics.mean(values), + sigma, + minimum(values), + Statistics.quantile(values, 0.05), + Statistics.quantile(values, 0.50), + Statistics.quantile(values, 0.95), + maximum(values) + ) + return SampleSummary(promoted..., length(values)) +end + +Statistics.mean(summary::SampleSummary) = summary.mean +Statistics.std(summary::SampleSummary) = summary.std + +""" +$(TYPEDSIGNATURES) + +Materialize an empirical marginal as its mean and sample standard deviation in +the observation's native units. Requires the Measurements extension. This is +output spread, not the standard error of the estimated mean. No joint output +covariance is inferred. +""" +materialize(summary::SampleSummary) = materialize(UncertainValue(summary.mean, summary.std)) +Statistics.median(summary::SampleSummary) = summary.median +Base.minimum(summary::SampleSummary) = summary.min +Base.maximum(summary::SampleSummary) = summary.max +Base.NamedTuple(summary::SampleSummary) = (mean=summary.mean,std=summary.std,min=summary.min, + q05=summary.q05,median=summary.median,q95=summary.q95,max=summary.max,n=summary.n) + +""" +$(TYPEDSIGNATURES) + +Return a retained empirical percentile in the summary's physical units. +Only probabilities 0, 0.05, 0.5, 0.95 and 1 are retained. Other probabilities +raise `ArgumentError`. +""" +function Statistics.quantile(summary::SampleSummary, probability::Real) + probability == 0 && return minimum(summary) + probability == 0.05 && return summary.q05 + probability == 0.5 && return Statistics.median(summary) + probability == 0.95 && return summary.q95 + probability == 1 && return maximum(summary) + throw(ArgumentError("the summary retains only quantiles at 0, 0.05, 0.5, 0.95 and 1")) +end + +""" +$(TYPEDEF) + +Store a normalized piecewise-constant probability density. `edges` use the +units of the sampled quantity and `density` uses their reciprocal. + +$(TYPEDFIELDS) +""" +struct HistogramDensity{T <: AbstractFloat} + "Strictly increasing bin edges." + edges::Vector{T} + "Normalized density in each bin." + density::Vector{T} + + function HistogramDensity(edges::Vector{T}, density::Vector{T}) where {T <: + AbstractFloat} + length(edges) == length(density) + 1 || throw(ArgumentError( + "histogram edges must contain one more value than density", + )) + isempty(density) && throw(ArgumentError("histogram density cannot be empty")) + all(isfinite, edges) || throw(ArgumentError("histogram edges must be finite")) + widths = diff(edges) + all(>(zero(T)), widths) || throw(ArgumentError( + "histogram edges must be strictly increasing", + )) + all(value -> isfinite(value) && value >= zero(T), density) || + throw(ArgumentError("histogram density must be finite and nonnegative")) + area = sum(density .* widths) + area > zero(T) || throw(ArgumentError("histogram density must have positive area")) + return new{T}(copy(edges), density ./ area) + end +end + +function HistogramDensity(edges::AbstractVector{<:Real}, density::AbstractVector{<:Real}) + T = promote_type(float(eltype(edges)), float(eltype(density))) + isconcretetype(T) || (T = Float64) + return HistogramDensity(Vector{T}(edges), Vector{T}(density)) +end + +""" +$(TYPEDSIGNATURES) + +Estimate a normalized piecewise-constant density without modifying the samples. + +# Arguments + +- `values`: nonempty finite observations in the sampled quantity's native units. + +# Keywords + +- `bins=nothing`: maximum bin count for varying samples. The default is the + ceiling of the square root of the sample count. Coincident floating-point + edges are merged. Constant samples use one finite-width bin. + +# Returns + +- A `HistogramDensity` with density in reciprocal sample units. Bins include + their left endpoint. The last bin also includes its right endpoint. + +# Notes + +Constant samples use a half-width of `sqrt(eps(Float64)) * max(1, abs(value))`. +This is a density representation, not added sample variation. Varying samples +retain their extrema as endpoints, even when they are adjacent floating-point +values. If finite edges or densities cannot be represented in the working +type, construction uses `BigFloat` with additional precision. +""" +function HistogramDensity( + values::AbstractVector{<:Real}; + bins::Union{Nothing, Integer} = nothing +) + isempty(values) && throw(ArgumentError("cannot estimate a histogram from no samples")) + all(isfinite, values) || throw(ArgumentError("histogram samples must be finite")) + bins === nothing || bins > 0 || throw(ArgumentError("histogram bins must be positive")) + + lo, hi = extrema(values) + edges = if lo == hi + width = max(abs(float(lo)) * sqrt(eps(Float64)), sqrt(eps(Float64))) + [float(lo) - width, float(hi) + width] + else + count = something(bins, max(1, ceil(Int, sqrt(length(values))))) + unique(collect(range(float(lo), float(hi); length = count + 1))) + end + # Extreme finite inputs can overflow a padded endpoint, bin width or its + # reciprocal. Re-evaluate this same bin construction at higher precision. + # Do not move the observations or collapse a varying population to its mean. + if length(edges) < 2 || !all(isfinite, edges) || + !all(width -> isfinite(width) && width > 0 && isfinite(inv(width)), diff(edges)) + bits = max(precision(BigFloat), precision(float(lo)), precision(float(hi))) + + 64 + ndigits(something(bins, length(values)); base=2) + return setprecision(BigFloat, bits) do + HistogramDensity(BigFloat.(values); bins) + end + end + counts = zeros(Float64, length(edges) - 1) + for value in values + index = value == last(edges) ? length(counts) : + clamp(searchsortedlast(edges, value), 1, length(counts)) + counts[index] += 1 + end + return HistogramDensity(edges, (counts ./ length(values)) ./ diff(edges)) +end + +""" +Evaluate the cumulative probability of a piecewise-constant histogram model. +""" +function cumulative_probability(histogram::HistogramDensity, value::Real) + value <= first(histogram.edges) && return 0.0 + value >= last(histogram.edges) && return 1.0 + bin = clamp(searchsortedlast(histogram.edges, value), 1, length(histogram.density)) + prior = bin == 1 ? zero(eltype(histogram.density)) : + sum(histogram.density[1:(bin - 1)] .* diff(histogram.edges[1:bin])) + return clamp( + prior + histogram.density[bin] * (value - histogram.edges[bin]), + 0.0, + 1.0 + ) +end + +function Statistics.quantile(histogram::HistogramDensity, probability::Real) + 0 <= probability <= 1 || throw(DomainError( + probability, + "probability must lie between zero and one", + )) + probability == 0 && return first(histogram.edges) + probability == 1 && return last(histogram.edges) + cumulative = cumsum(histogram.density .* diff(histogram.edges)) + bin = something(findfirst(>=(probability), cumulative), length(cumulative)) + prior = bin == 1 ? zero(eltype(cumulative)) : cumulative[bin - 1] + density = histogram.density[bin] + iszero(density) && return histogram.edges[bin] + return histogram.edges[bin] + (probability - prior) / density +end + +""" +Return model and sample quantile coordinates and identity-line endpoints. +""" +function quantile_pairs(histogram::HistogramDensity, samples::AbstractVector{<:Real}) + isempty(samples) && throw(ArgumentError("Q-Q coordinates require retained samples")) + all(isfinite, samples) || throw(ArgumentError("Q-Q samples must be finite")) + sample = sort(collect(samples)) + probabilities = ((1:length(sample)) .- 0.5) ./ length(sample) + model = Statistics.quantile.(Ref(histogram), probabilities) + reference = extrema(vcat(model, sample)) + return (; model, sample, reference) +end + +Base.NamedTuple(histogram::HistogramDensity) = (edges=histogram.edges,density=histogram.density) diff --git a/src/uq/textdisplay.jl b/src/uq/textdisplay.jl new file mode 100644 index 000000000..fb55837b3 --- /dev/null +++ b/src/uq/textdisplay.jl @@ -0,0 +1,116 @@ +_uq_compact(value) = sprint(show, value; context = :compact => true, sizehint = 80) + +TextDisplay.name(::Type{<:LinearError}) = "LinearError" +Base.summary(io::IO, ::LinearError) = print(io, "Linear uncertainty propagation") +function Base.show(io::IO, formulation::LinearError) + print(io, "LinearError(", _uq_compact(formulation.inner), ")") +end +function Base.show(io::IO, ::MIME"text/plain", formulation::LinearError) + get(io, :compact, false) && return show(io, formulation) + return TextDisplay.tree(io, "Linear uncertainty propagation", ( + (label = "inner $(_uq_compact(formulation.inner))", noun = "fields"), + (label = "details $(formulation.options.data.retain_details ? "retained" : "not retained")", noun = "fields"), + )) +end + +TextDisplay.name(::Type{<:MonteCarlo}) = "MonteCarlo" +Base.summary(io::IO, formulation::MonteCarlo) = print(io, "Monte Carlo propagation") +function Base.show(io::IO, formulation::MonteCarlo) + trials = formulation.options.data.trials === nothing ? "DKW-sized" : string(formulation.options.data.trials) + print(io, "MonteCarlo(trials=", trials, + "; samples=", formulation.options.data.return_samples, + ", histograms=", formulation.options.data.return_histograms, ")") +end +function Base.show(io::IO, ::MIME"text/plain", formulation::MonteCarlo) + get(io, :compact, false) && return show(io, formulation) + trials = formulation.options.data.trials === nothing ? "DKW-sized" : string(formulation.options.data.trials) + distribution = formulation.options.data.distribution isa Symbol ? + ":$(formulation.options.data.distribution)" : _uq_compact(formulation.options.data.distribution) + children = Any[ + (label = "inner $(_uq_compact(formulation.inner))", noun = "fields"), + (label = "trials $trials", noun = "fields"), + (label = "confidence $(TextDisplay.value(formulation.options.data.confidence))", noun = "fields"), + (label = "CDF tol $(TextDisplay.value(formulation.options.data.cdf_tol))", noun = "fields"), + (label = "distribution $distribution", noun = "fields"), + ] + formulation.options.data.seed === nothing || push!(children, + (label = "seed $(formulation.options.data.seed)", noun = "fields")) + formulation.options.data.return_samples && push!(children, + (label = "samples retained", noun = "fields")) + formulation.options.data.return_histograms && push!(children, + (label = "histograms retained", noun = "fields")) + return TextDisplay.tree(io, "Monte Carlo propagation", Tuple(children)) +end + +TextDisplay.@showfields SampleSummary "SampleSummary" summary -> ( + mean = TextDisplay.value(summary.mean), + std = TextDisplay.value(summary.std), + min = TextDisplay.value(summary.min), + q05 = TextDisplay.value(summary.q05), + median = TextDisplay.value(summary.median), + q95 = TextDisplay.value(summary.q95), + max = TextDisplay.value(summary.max), + n = summary.n +) + +TextDisplay.name(::Type{<:HistogramDensity}) = "HistogramDensity" +function Base.summary(io::IO, histogram::HistogramDensity) + print(io, "Histogram density with $(length(histogram.density)) bins") +end +function Base.show(io::IO, histogram::HistogramDensity) + print(io, "HistogramDensity(", length(histogram.density), " bins; range=", + TextDisplay.value(first(histogram.edges)), "…", + TextDisplay.value(last(histogram.edges)), ")") +end +function Base.show(io::IO, ::MIME"text/plain", histogram::HistogramDensity) + get(io, :compact, false) && return show(io, histogram) + return TextDisplay.tree(io, "Histogram density · $(length(histogram.density)) bins", ( + (label = "range $(TextDisplay.value(first(histogram.edges))) … $(TextDisplay.value(last(histogram.edges)))", noun = "fields"), + (label = "density normalized", noun = "fields"), + )) +end + +TextDisplay.name(::Type{<:LinearErrorResult}) = "LinearErrorResult" +function Base.summary(io::IO, result::LinearErrorResult) + print(io, "Linear-error result with $(length(result)) values") +end +Base.show(io::IO, result::LinearErrorResult) = + print(io, "LinearErrorResult($(length(result)) values)") +function Base.show(io::IO, ::MIME"text/plain", result::LinearErrorResult) + get(io, :compact, false) && return show(io, result) + kind = isempty(result.values) ? "none" : String(nameof(eltype(result.values))) + return TextDisplay.tree(io, "Linear-error result · $(length(result)) values", ( + (label = "result type $kind", noun = "fields"), + (label = "details $(length(result.details.data)) entries", noun = "fields"), + )) +end + +TextDisplay.name(::Type{<:MonteCarloResult}) = "MonteCarloResult" +function Base.summary(io::IO, result::MonteCarloResult) + print(io, "Monte Carlo result with $(length(result)) points") +end +function Base.show(io::IO, result::MonteCarloResult) + trials = all(==(first(result.trial_counts)), result.trial_counts) ? + string(first(result.trial_counts)) : "varying" + print(io, "MonteCarloResult($(length(result)) points; trials=", trials, ")") +end +function Base.show(io::IO, ::MIME"text/plain", result::MonteCarloResult) + get(io, :compact, false) && return show(io, result) + trial_span = extrema(result.trial_counts) + trials = first(trial_span) == last(trial_span) ? string(first(trial_span)) : + "$(first(trial_span))…$(last(trial_span))" + result_type = String(nameof(eltype(result.values))) + children = Any[ + (label = "result type $result_type", noun = "fields"), + (label = "trials $trials per point", noun = "fields"), + (label = "statistics retained", noun = "fields"), + (label = "root seed $(result.root_seed)", noun = "fields"), + ] + result.sample_values === nothing || push!(children, + (label = "samples retained", noun = "fields")) + result.histogram_values === nothing || push!(children, + (label = "histograms retained", noun = "fields")) + isempty(result.details.data) || push!(children, + (label = "details $(length(result.details.data)) entries", noun = "fields")) + return TextDisplay.tree(io, "Monte Carlo result · $(length(result)) points", Tuple(children)) +end diff --git a/src/uq/types.jl b/src/uq/types.jl deleted file mode 100644 index 3effa9b8f..000000000 --- a/src/uq/types.jl +++ /dev/null @@ -1,140 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# Piecewise-constant PDF as a ContinuousUnivariateDistribution -# ───────────────────────────────────────────────────────────────────────────── -struct LineParametersPDF{T <: Real} <: ContinuousUnivariateDistribution - edges::Vector{T} # length B+1, sorted ascending - dens::Vector{T} # length B, area ≈ 1 -end - -# Your constructor (modified slightly to remove redundant collect) -function LineParametersPDF( - edges::AbstractVector{T}, - dens::AbstractVector{T}, -) where {T <: Real} - length(edges) == length(dens) + 1 || - throw(ArgumentError("edges must have length length(dens)+1")) - - e = Vector(edges) # Just convert once - d = Vector(dens) - - # Ensure increasing edges - issorted(e) || throw(ArgumentError("edges must be sorted ascending")) - - # Normalize area to 1 (robust against floating crap) - widths = diff(e) - area = dot(d, widths) # Cleaner than sum(d .* widths) - area <= zero(T) && throw(ArgumentError("non-positive total area in LineParametersPDF")) - - # Only normalize if it's not already 1 (avoids float division noise) - if !(area ≈ 1.0) - d ./= area - end - - return LineParametersPDF{T}(e, d) -end - -""" - LineParametersMC{U, D} - -Store Monte Carlo summaries and a joint moment-matched line-parameter surrogate. - -# Notes - -The `measurements` field matches the empirical mean and covariance of all R, L, -G, and C samples jointly. Normal downstream sampling therefore gives a Gaussian -surrogate with those moments. Variance-equivalent uniform sampling also preserves -the moments, but neither law reproduces the original nonlinear Monte Carlo -distribution. Use `samples` or `pdf` when distribution shape is required. -When `samples` are retained, [`trial`](@ref) and `rand` reconstruct complete -members of the discrete empirical joint distribution. -""" -struct LineParametersMC{U <: Real, D <: LineParamsDomain} - "Frequencies \\[Hz\\]." - f::Vector{U} - - "Statistics tensors for R, L, C, G \\[per element and frequency\\]." - stats::NamedTuple{ - (:R, :L, :C, :G), - Tuple{ - Array{NamedTuple, 3}, # R[i,j,k] - Array{NamedTuple, 3}, # L[i,j,k] - Array{NamedTuple, 3}, # C[i,j,k] - Array{NamedTuple, 3}, # G[i,j,k] - }, - } - - "Empirical PDFs per R, L, C, G entry or `nothing` if not requested." - pdf::Union{ - Nothing, - NamedTuple{ - (:R, :L, :C, :G), - Tuple{ - Array{LineParametersPDF{U}, 3}, - Array{LineParametersPDF{U}, 3}, - Array{LineParametersPDF{U}, 3}, - Array{LineParametersPDF{U}, 3}, - }, - }, - } - - "Optional Monte Carlo samples of R/L/C/G." - samples::Union{ - Nothing, - NamedTuple{ - (:R, :L, :C, :G), - Tuple{ - Array{U, 4}, # R[i,j,k,trial] - Array{U, 4}, # L - Array{U, 4}, # C - Array{U, 4}, # G - }, - }, - } - - "Joint moment-matched `LineParameters` with covariance-preserving entries." - measurements::LineParameters{Complex{Measurement{U}}, U, D} -end - -# Constructor to infer D from lp_meas at compile time -function LineParametersMC( - f::Vector{U}, - stats::NamedTuple, - pdf, - samples, - lp_meas::LineParameters{Tc, U, D}, -) where {Tc, U <: Real, D <: LineParamsDomain} - return LineParametersMC{U, D}(f, stats, pdf, samples, lp_meas) -end - -@inline domain(::Type{<:LineParametersMC{U, D}}) where {U <: Real, D <: LineParamsDomain} = - D -@inline domain(lp::LineParametersMC) = domain(typeof(lp)) - -struct CableDesignMC{U <: Real} - "Statistics for R, L, C (each is a NamedTuple from the mc stats kernel)." - stats::NamedTuple{ - (:R, :L, :C), - Tuple{NamedTuple, NamedTuple, NamedTuple}, - } - - "Empirical PDFs for R, L, C or `nothing` if not requested." - pdf::Union{ - Nothing, - NamedTuple{ - (:R, :L, :C), - Tuple{LineParametersPDF{U}, LineParametersPDF{U}, LineParametersPDF{U}}, - }, - } - - "Optional raw samples of R, L, C (each a Vector of length ntrials)." - samples::Union{ - Nothing, - NamedTuple{ - (:R, :L, :C), - Tuple{Vector{U}, Vector{U}, Vector{U}}, - }, - } - - "Measurements for R, L, C (mean ± std)." - measurements::Vector{Measurement{U}} -end diff --git a/src/utils/Utils.jl b/src/utils/Utils.jl deleted file mode 100644 index 389d398aa..000000000 --- a/src/utils/Utils.jl +++ /dev/null @@ -1,498 +0,0 @@ -""" - LineCableModels.Utils - -The [`Utils`](@ref) module provides utility functions for the [`LineCableModels.jl`](index.md) package. This module includes functions for handling measurements, numerical comparisons, and other common tasks. - -# Overview - -- Provides general constants used throughout the package. -- Includes utility functions for numerical comparisons and handling measurements. -- Contains functions to compute uncertainties and bounds for measurements. - -# Dependencies - -$(IMPORTS) - -# Exports - -$(EXPORTS) -""" -module Utils - -# Export public API -export resolve_T, coerce_to_T, is_headless, is_in_testset, display_path -export set_verbosity! - -export to_nominal, - to_certain, - percent_to_uncertain, - bias_to_uncertain, - to_upper, - to_lower, - percent_error - -export _to_σ, _bessel_diff, symtrans!, line_transpose! - -# Module-specific dependencies -using ..Commons -using ..UncertainBessels: besselk -using Measurements: Measurement, value, uncertainty, measurement, ±, Measurements, result -using Statistics -using Plots -using LinearAlgebra - -""" -$(TYPEDSIGNATURES) - -Returns the nominal (deterministic) value of inputs that may contain -`Measurements.Measurement` numbers, recursively handling Complex and arrays. - -# Arguments - -- `x`: Input value which can be a `Measurement` type or any other type. - -# Returns - -- Measurement → its `value` -- Complex → `complex(to_nominal(real(z)), to_nominal(imag(z)))` -- AbstractArray → broadcasts `to_nominal` elementwise -- Anything else → returned unchanged - -# Examples - -```julia -using Measurements - -$(FUNCTIONNAME)(1.0) # Output: 1.0 -$(FUNCTIONNAME)(5.2 ± 0.3) # Output: 5.2 -``` -""" -to_nominal(x::Measurement) = value(x) -to_nominal(z::Complex) = complex(to_nominal(real(z)), to_nominal(imag(z))) -to_nominal(A::AbstractArray) = to_nominal.(A) -to_nominal(x) = x - -""" -$(TYPEDSIGNATURES) - -Converts a measurement to a value with zero uncertainty, retaining the numeric type `Measurement`. - -# Arguments - -- `value`: Input value that may be a `Measurement` type or another type. - -# Returns - -- If input is a `Measurement`, returns the same value with zero uncertainty; otherwise returns the original value unchanged. - -# Examples - -```julia -x = 5.0 ± 0.1 -result = $(FUNCTIONNAME)(x) # Output: 5.0 ± 0.0 - -y = 10.0 -result = $(FUNCTIONNAME)(y) # Output: 10.0 -``` -""" -function to_certain(value) - return value isa Measurement ? (Measurements.value(value) ± 0.0) : value -end - -""" -$(TYPEDSIGNATURES) - -Converts a value to a measurement with uncertainty based on percentage. - -# Arguments - -- `val`: The nominal value. -- `perc`: The percentage uncertainty (0 to 100). - -# Returns - -- A `Measurement` type with the given value and calculated uncertainty. - -# Examples - -```julia -using Measurements - -$(FUNCTIONNAME)(100.0, 5) # Output: 100.0 ± 5.0 -$(FUNCTIONNAME)(10.0, 10) # Output: 10.0 ± 1.0 -``` -""" -function percent_to_uncertain(val, perc) #perc from 0 to 100 - measurement(val, (perc * val) / 100) -end - -""" -$(TYPEDSIGNATURES) - -Computes the uncertainty of a measurement by incorporating systematic bias. - -# Arguments - -- `nominal`: The deterministic nominal value (Float64). -- `measurements`: A vector of `Measurement` values from the `Measurements.jl` package. - -# Returns - -- A new `Measurement` object representing the mean measurement value with an uncertainty that accounts for both statistical variation and systematic bias. - -# Notes - -- Computes the mean value and its associated uncertainty from the given `measurements`. -- Determines the **bias** as the absolute difference between the deterministic `nominal` value and the mean measurement. -- The final uncertainty is the sum of the standard uncertainty (`sigma_mean`) and the systematic bias. - -# Examples -```julia -using Measurements - -nominal = 10.0 -measurements = [10.2 ± 0.1, 9.8 ± 0.2, 10.1 ± 0.15] -result = $(FUNCTIONNAME)(nominal, measurements) -println(result) # Output: Measurement with adjusted uncertainty -``` -""" -function bias_to_uncertain(nominal::Float64, measurements::Vector{<:Measurement}) - # Compute the mean value and uncertainty from the measurements - mean_measurement = mean(measurements) - mean_value = Measurements.value(mean_measurement) # Central value - sigma_mean = Measurements.uncertainty(mean_measurement) # Uncertainty of the mean - # Compute the bias (deterministic nominal value minus mean measurement) - bias = abs(nominal - mean_value) - return mean_value ± (sigma_mean + bias) -end - -""" -$(TYPEDSIGNATURES) - -Computes the upper bound of a measurement value. - -# Arguments - -- `m`: A numerical value, expected to be of type `Measurement` from the `Measurements.jl` package. - -# Returns - -- The upper bound of `m`, computed as `value(m) + uncertainty(m)` if `m` is a `Measurement`. -- `NaN` if `m` is not a `Measurement`. - -# Examples - -```julia -using Measurements - -m = 10.0 ± 2.0 -upper = $(FUNCTIONNAME)(m) # Output: 12.0 - -not_a_measurement = 5.0 -upper_invalid = $(FUNCTIONNAME)(not_a_measurement) # Output: NaN -``` -""" -function to_upper(m::Number) - if m isa Measurement - return Measurements.value(m) + Measurements.uncertainty(m) - else - return NaN - end -end - -""" -$(TYPEDSIGNATURES) - -Computes the lower bound of a measurement value. - -# Arguments - -- `m`: A numerical value, expected to be of type `Measurement` from the `Measurements.jl` package. - -# Returns - -- The lower bound, computed as `value(m) - uncertainty(m)` if `m` is a `Measurement`. -- `NaN` if `m` is not a `Measurement`. - -# Examples - -```julia -using Measurements - -m = 10.0 ± 2.0 -lower = $(FUNCTIONNAME)(m) # Output: 8.0 - -not_a_measurement = 5.0 -lower_invalid = $(FUNCTIONNAME)(not_a_measurement) # Output: NaN -``` -""" -function to_lower(m::Number) - if m isa Measurement - return Measurements.value(m) - Measurements.uncertainty(m) - else - return NaN - end -end - -""" -$(TYPEDSIGNATURES) - -Computes the percentage uncertainty of a measurement. - -# Arguments - -- `m`: A numerical value, expected to be of type `Measurement` from the `Measurements.jl` package. - -# Returns - -- The percentage uncertainty, computed as `100 * uncertainty(m) / value(m)`, if `m` is a `Measurement`. -- `NaN` if `m` is not a `Measurement`. - -# Examples - -```julia -using Measurements - -m = 10.0 ± 2.0 -percent_err = $(FUNCTIONNAME)(m) # Output: 20.0 - -not_a_measurement = 5.0 -percent_err_invalid = $(FUNCTIONNAME)(not_a_measurement) # Output: NaN -``` -""" -function percent_error(m::Number) - if m isa Measurement - return 100 * Measurements.uncertainty(m) / Measurements.value(m) - else - return NaN - end -end - -@inline _nudge_float(x::AbstractFloat) = isfinite(x) && x == trunc(x) ? nextfloat(x) : x #redundant and I dont care - -_coerce_args_to_T(args...) = - any(x -> x isa Measurement, args) ? Measurement{BASE_FLOAT} : BASE_FLOAT - -# Promote scalar to T if T is Measurement; otherwise take nominal if x is Measurement. -function _coerce_scalar_to_T(x, ::Type{T}) where {T} - if T <: Measurement - return x isa Measurement ? x : (zero(T) + x) - else - return x isa Measurement ? T(value(x)) : convert(T, x) - end -end - -# Arrays: promote/demote elementwise, preserving shape. Arrays NEVER decide T. -function _coerce_array_to_T(A::AbstractArray, ::Type{T}) where {T} - if T <: Measurement - return (eltype(A) === T) ? A : (A .+ zero(T)) # Real → Measurement(σ=0) - elseif eltype(A) <: Measurement - B = value.(A) # Measurement → Real (nominal) - return (eltype(B) === T) ? B : convert.(T, B) - else - return (eltype(A) === T) ? A : convert.(T, A) - end -end - -""" -$(TYPEDSIGNATURES) - -Determines if the current execution environment is headless (without display capability). - -# Returns - -- `true` if running in a continuous integration environment or without display access. -- `false` otherwise when a display is available. - -# Examples - -```julia -if $(FUNCTIONNAME)() - # Use non-graphical backend - gr() -else - # Use interactive backend - plotlyjs() -end -``` -""" -function is_headless()::Bool - # 1. Check for common CI environment variables - if get(ENV, "CI", "false") == "true" - return true - end - - # 2. Check if a display is available (primarily for Linux) - if !haskey(ENV, "DISPLAY") && Sys.islinux() - return true - end - - # 3. Check for GR backend's specific headless setting - if get(ENV, "GKSwstype", "") in ("100", "nul", "nil") - return true - end - - return false -end - -function display_path(file_name) - return is_headless() ? basename(file_name) : relpath(file_name) -end - -""" -$(TYPEDSIGNATURES) - -Checks if the code is running inside a `@testset` by checking if `Test` is loaded -in the current session and then calling `get_testset_depth()`. -""" -function is_in_testset() - # Start with the current module - current_module = @__MODULE__ - - # Walk up the module tree (e.g., from the sandbox to Main) - while true - if isdefined(current_module, :Test) && - isdefined(current_module.Test, :get_testset_depth) - # Found the Test module, check the test set depth - return current_module.Test.get_testset_depth() > 0 - end - - # Move to the parent module - parent = parentmodule(current_module) - if parent === current_module # Reached the top (Main) - break - end - current_module = parent - end - - return false -end - - - -""" -Apply `f` to every square block of `M` defined by `map`, in-place. - -- `M`: n×n or n×n×nf (any eltype). -- `map`: length-n; equal ids => same block (non-contiguous ok). -- `f`: function like `f(B::AbstractMatrix, args...) -> k×k Matrix`. -- `args...`: extra positional args passed to `f`. -- `slice_positions`: positions in `args` that should be indexed as `args[i][idx]` - per block (useful for `phase_map`). - -Returns `M`. -""" -function block_transform!(M, - map::AbstractVector{<:Integer}, - f::F, - args...; slice_positions = Int[]) where {F} - n = size(M, 1) - (size(M, 2) == n && length(map) == n) || throw(ArgumentError("shape mismatch")) - groups = unique(map) # preserve first-seen order - blocks = [findall(==(g), map) for g in groups] - - # helper to build per-block args (slice selected ones) - make_args(idx) = - ntuple(i -> (i in slice_positions ? args[i][idx] : args[i]), length(args)) - - if ndims(M) == 2 - for idx in blocks - Bv = @view M[idx, idx] - R = f(Matrix(Bv), make_args(idx)...) # f decides what to do - size(R) == size(Bv) || throw(ArgumentError("f must return $(size(Bv))")) - @inbounds Bv .= R - end - elseif ndims(M) == 3 - _, _, nf = size(M) - for k in 1:nf - for idx in blocks - Bv = @view M[idx, idx, k] - R = f(Matrix(Bv), make_args(idx)...) - size(R) == size(Bv) || throw(ArgumentError("f must return $(size(Bv))")) - @inbounds Bv .= R - end - end - else - throw(ArgumentError("M must be 2D or 3D")) - end - return M -end - -# Non-mutating -block_transform(M, cmap, f, args...; slice_positions = Int[]) = - block_transform!(copy(M), cmap, f, args...; slice_positions = slice_positions) - - -# Reciprocity symmetrization — in place -symtrans!(A) = (A .= 0.5 .* (A .+ transpose(A)); A) - -# Reciprocity symmetrization (power lines want transpose, not adjoint) -symtrans(A) = (A .+ transpose(A)) / 2 - - -# Circulant projection (N×N), least-squares fit: C[i,j] = c[(j-i) mod N] -function line_transpose!(A::AbstractMatrix) - n = size(A, 1); - n == size(A, 2) || throw(ArgumentError("square")) - c = similar(diag(A)) # length n - - # Average wrap-diagonals (use mod to avoid negatives) - @inbounds for k in 0:(n-1) - s = zero(eltype(A)) - for i in 1:n - j = 1 + mod(i-1 + k, n) - s += A[i, j] - end - c[k+1] = s / n - end - # Write back circulant matrix - @inbounds for i in 1:n, j in 1:n - A[i, j] = c[mod1(j - i + 1, n)] #c[1+mod(j-i, n)] - end - return A -end - - -function isdiag_approx(A; rtol = 1e-8, atol = 1e-8) - isapprox(A, Diagonal(diag(A)); rtol = rtol, atol = atol) -end - -function offdiag_ratio(A) - n = size(A, 1) - n == size(A, 2) || throw(ArgumentError("square")) - T = real(float(eltype(A))) - dmax = zero(T) - odmax = zero(T) - @inbounds for j in 1:n - dj = abs(A[j, j]) - dmax = dj > dmax ? dj : dmax - for i in 1:n - i == j && continue - v = abs(A[i, j]) - odmax = v > odmax ? v : odmax - end - end - return odmax / max(dmax, eps(T)) -end - -isdiag_rel(A; τ = 1e-4) = offdiag_ratio(A) ≤ τ - -function issymmetric_approx(A; rtol = 1e-8, atol = 1e-8) - size(A, 1) == size(A, 2) || return false - return isapprox(A, transpose(A); rtol = rtol, atol = atol) -end - - -@inline _to_σ(ρ) = isinf(ρ) ? zero(ρ) : (iszero(ρ) ? inv(zero(ρ)) : inv(ρ)) - -@inline function _bessel_diff(γs, d::T, D::T) where {T} - zmax = max(abs(γs)*d, abs(γs)*D) - return isapprox(to_nominal(zmax), 0.0, atol = TOL) ? log(D/d) : - (besselk(0, γs*d) - besselk(0, γs*D)) -end - -include("logging.jl") -include("typecoercion.jl") -# include("macros.jl") - -end # module Utils diff --git a/src/utils/commondeps.jl b/src/utils/commondeps.jl deleted file mode 100644 index ec7185e74..000000000 --- a/src/utils/commondeps.jl +++ /dev/null @@ -1 +0,0 @@ -using DocStringExtensions, Reexport, ForceImport diff --git a/src/utils/logging.jl b/src/utils/logging.jl deleted file mode 100644 index 3f9ba85a4..000000000 --- a/src/utils/logging.jl +++ /dev/null @@ -1,59 +0,0 @@ -using Logging -using Logging: AbstractLogger, LogLevel, Info, global_logger -using LoggingExtras: TeeLogger, FileLogger -using Dates -using Printf - -levelfrom(v::Integer)::Logging.LogLevel = - v >= 2 ? Logging.Debug : - v == 1 ? Logging.Info : - v == 0 ? Logging.Warn : - Logging.Error - - - -struct TimestampLogger <: AbstractLogger - logger::AbstractLogger -end - -Logging.min_enabled_level(logger::TimestampLogger) = - Logging.min_enabled_level(logger.logger) -Logging.shouldlog(logger::TimestampLogger, level, _module, group, id) = - Logging.shouldlog(logger.logger, level, _module, group, id) - -function Logging.handle_message(logger::TimestampLogger, level, message, _module, group, id, - filepath, line; kwargs...) - timestamp = Dates.format(now(), "yyyy-mm-dd HH:MM:SS") - new_message = "[$timestamp] $message" - Logging.handle_message(logger.logger, level, new_message, _module, group, id, - filepath, line; kwargs...) -end - -function set_verbosity!(verbosity::Int, logfile::Union{String, Nothing} = nothing) - # level = verbosity >= 2 ? Logging.Debug : - # verbosity == 1 ? Logging.Info : Logging.Warn - level = levelfrom(verbosity) - # Create console logger - console_logger = ConsoleLogger(stderr, level) - - if isnothing(logfile) - # Log to console only - global_logger(TimestampLogger(console_logger)) - else - # Try to set up file logging with fallback to console-only - try - file_logger = FileLogger(logfile, level) - combined_logger = TeeLogger(console_logger, file_logger) - global_logger(TimestampLogger(combined_logger)) - catch e - @warn "Failed to set up file logging to $(display_path(logfile)): $e" - - global_logger(TimestampLogger(console_logger)) - end - end -end - -function __init__() - # Set a default logging level when the package is loaded at runtime. - set_verbosity!(0) -end diff --git a/src/utils/macros.jl b/src/utils/macros.jl deleted file mode 100644 index 8c6bd6dd6..000000000 --- a/src/utils/macros.jl +++ /dev/null @@ -1,263 +0,0 @@ -export @parameterize, @measurify - -using MacroTools - -macro parameterize(container_expr, union_expr) - # Evaluate the Union type from the provided expression - local union_type - try - # Core.eval gets the *value* of the symbol passed in (e.g., the actual Union type) - union_type = Core.eval(__module__, union_expr) - catch e - Base.error("Expression `$union_expr` could not be evaluated. Make sure it's a defined const or type.") - end - - # Sanity check - if !(union_type isa Union) - Base.error("Second argument must be a Union type. Got a `$(typeof(union_type))` instead.") - end - - # Base.uniontypes gets the component types, e.g., (Float64, Measurement{Float64}) - component_types = Base.uniontypes(union_type) - - # Define a recursive function to substitute the placeholder `_` - function substitute_placeholder(expr, replacement_type) - # If the current part of the expression is the placeholder symbol, - # we replace it with the target type (e.g., Float64). - if expr == :_ - return replacement_type - # If the current part is another expression (like `Array{_,3}`), - # we need to recurse into its arguments to find the placeholder. - elseif expr isa Expr - # Rebuild the expression with the substituted arguments. - new_args = [substitute_placeholder(arg, replacement_type) for arg in expr.args] - return Expr(expr.head, new_args...) - # Otherwise, it's a literal or symbol we don't need to change (e.g., `:Array` or `3`). - else - return expr - end - end - - # Build the list of new, concrete types - parameterized_types = [substitute_placeholder(container_expr, t) for t in component_types] - - # Wrap the new types in a single `Union{...}` expression and escape - final_expr = Expr(:curly, :Union, parameterized_types...) - return esc(final_expr) -end - -""" - @measurify(function_definition) - -Wraps a function definition. If any argument tied to a parametric type `T` is a -`Measurement`, this macro automatically promotes any other arguments of the same -parametric type `T` to `Measurement` with zero uncertainty. Other arguments -(e.g., `i::Int`) are forwarded without changes. -""" -macro measurify(def) - # --- helpers at expand time --- - function _contains_tvar(expr, typevars::Set{Symbol}) - found = false - MacroTools.postwalk(expr) do x - if x isa Symbol && x in typevars - found = true - end - x - end - found - end - function _relax_container_type(ty, typevars::Set{Symbol}, bounds::Dict{Symbol,Any}) - if ty isa Expr && ty.head == :curly - head = ty.args[1] - params = Any[_contains_tvar(p, typevars) ? - MacroTools.postwalk(p) do x - (x isa Symbol && haskey(bounds, x)) ? bounds[x] : x - end |> x -> Expr(:<:, x) : - p - for p in ty.args[2:end]] - return Expr(:curly, head, params...) - else - return MacroTools.postwalk(ty) do x - (x isa Symbol && haskey(bounds, x)) ? bounds[x] : x - end - end - end - _ARRAY_HEADS = Set{Symbol}( - [:Vector, :Array, :AbstractVector, :AbstractArray, - :Matrix, :AbstractMatrix, :UnitRange, :StepRange, :AbstractRange] - ) - _is_array_annot(ty) = - ty isa Expr && ty.head == :curly && ty.args[1] isa Symbol && (ty.args[1] in _ARRAY_HEADS) - # --- end helpers --- - - # Normalize and split - if def.head == :(=) - def = MacroTools.longdef(def) - elseif def.head != :function - Base.error("@measurify must wrap a function definition") - end - dict = MacroTools.splitdef(def) - - where_items = get(dict, :whereparams, []) - typevars = Set{Symbol}() - bounds = Dict{Symbol,Any}() - for w in where_items - tv, ub = w isa Expr && w.head == :(<:) ? (w.args[1], w.args[2]) : (w, :Any) - push!(typevars, tv) - bounds[tv] = ub - end - - posargs = get(dict, :args, []) - kwargs = get(dict, :kwargs, []) - - # Collect symbols by role - scalar_syms = Symbol[] # positionals/keywords whose annot is exactly ::T - array_syms = Symbol[] # args like Vector{T}, AbstractArray{T}, ranges - anchor_sym = nothing # first parametric NON-array arg that contains T (e.g., EarthModel{T}) - - # Build relaxed wrapper signature - wrapper_pos = Expr[] - wrapper_kw = Expr[] - - # Positionals - for arg in posargs - nm, ty, default = MacroTools.splitarg(arg) - if (ty isa Symbol) && haskey(bounds, ty) - push!(scalar_syms, nm) - push!(wrapper_pos, Expr(:(::), nm, bounds[ty])) - elseif (ty !== nothing) && _contains_tvar(ty, typevars) - if _is_array_annot(ty) - push!(array_syms, nm) - elseif anchor_sym === nothing - anchor_sym = nm - end - push!(wrapper_pos, Expr(:(::), nm, _relax_container_type(ty, typevars, bounds))) - else - push!(wrapper_pos, arg) - end - end - - # Keywords - for kw in kwargs - nm, ty, default = MacroTools.splitarg(kw) - if (ty isa Symbol) && haskey(bounds, ty) - push!(scalar_syms, nm) - elseif (ty !== nothing) && _contains_tvar(ty, typevars) && _is_array_annot(ty) - push!(array_syms, nm) - end - push!(wrapper_kw, MacroTools.postwalk(kw) do x - (x isa Symbol && haskey(bounds, x)) ? bounds[x] : x - end) - end - - tight = def - wrapper_dict = deepcopy(dict) - wrapper_dict[:args] = wrapper_pos - wrapper_dict[:kwargs] = wrapper_kw - - # ---- wrapper body ---- - # 1) TargetType: scalars-only; if there is an anchor (e.g., model::EarthModel{T}), - # use a WIDENING rule seeded with zero(T). - scalar_vals = Any[:($s) for s in scalar_syms] - target_decl = anchor_sym === nothing ? - :(TargetType = _coerce_args_to_T($(scalar_vals...))) : - quote - Tanchor = first(typeof($(anchor_sym)).parameters) - TargetType = isempty(($(scalar_vals...,))) ? Tanchor : - _coerce_args_to_T(zero(Tanchor), $(scalar_vals...)) - end - - # 2) If anchored, CONVERT the anchor only if its T differs (avoid clone-on-noop) - anchor_convert = anchor_sym === nothing ? nothing : quote - Tcur = first(typeof($(anchor_sym)).parameters) - if Tcur !== TargetType - $(anchor_sym) = convert(EarthModel{TargetType}, $(anchor_sym)) - end - end - - # 3) Coerce arrays and scalars - array_casts = [:($a = _coerce_array_to_T($a, TargetType)) for a in array_syms] - scalar_casts = [:($s = _coerce_scalar_to_T($s, TargetType)) for s in scalar_syms] - - # 4) Forward call - arg_names = [MacroTools.splitarg(a)[1] for a in posargs] - kw_forwards = [Expr(:kw, MacroTools.splitarg(kw)[1], MacroTools.splitarg(kw)[1]) for kw in kwargs] - forward_call = Expr(:call, dict[:name]) - !isempty(kw_forwards) && push!(forward_call.args, Expr(:parameters, kw_forwards...)) - append!(forward_call.args, arg_names) - - wrapper_body = quote - $target_decl - $(anchor_convert === nothing ? nothing : anchor_convert) - $(array_casts...) - $(scalar_casts...) - $forward_call - end - # ---------------------- - - # Drop where if no raw typevars remain in wrapper signature - needs_where = any(_contains_tvar(arg, typevars) for arg in [wrapper_pos..., wrapper_kw...]) - if !needs_where - delete!(wrapper_dict, :whereparams) - end - - wrapper_dict[:body] = wrapper_body - loose = MacroTools.combinedef(wrapper_dict) - - # Keep your doc footer - return esc(Expr(:block, :(Base.@__doc__ $tight), loose)) -end - -""" -$(TYPEDSIGNATURES) - -Automatically exports public functions, types, and modules from a module. This is meant for temporary development chores and should never be used in production code. - -# Arguments - -- None. - -# Returns - -- An `export` expression containing all public symbols that should be exported. - -# Notes - -This macro scans the current module for all defined symbols and automatically generates an `export` statement for public functions, types, and submodules, excluding built-in and private names. Private names are considered those starting with an underscore ('_'), as per standard Julia conventions. - -# Examples - -```julia -@autoexport -``` -""" -macro autoexport() - mod = __module__ - - # Get all names defined in the module, including unexported ones - all_names = names(mod; all=true) - - # List of names to explicitly exclude - excluded_names = Set([:eval, :include, :using, :import, :export, :require]) - - # Filter out private names (starting with '_'), module name, built-in functions, and auto-generated method symbols - public_names = Symbol[] - for name in all_names - str_name = string(name) - - startswith(str_name, "@_") && continue # Skip private macros - startswith(str_name, "_") && continue # Skip private names - name === nameof(mod) && continue # Skip the module's own name - name in excluded_names && continue # Skip built-in functions - startswith(str_name, "#") && continue # Skip generated method symbols (e.g., #eval, #include) - - if isdefined(mod, name) - val = getfield(mod, name) - if val isa Function || val isa Type || val isa Module - push!(public_names, name) - end - end - end - - return esc(Expr(:export, public_names...)) -end \ No newline at end of file diff --git a/src/utils/typecoercion.jl b/src/utils/typecoercion.jl deleted file mode 100644 index cec9066c8..000000000 --- a/src/utils/typecoercion.jl +++ /dev/null @@ -1,329 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Determines whether a `Type` contains or is a `Measurements.Measurement` somewhere in its structure. The check is recursive over arrays, tuples (including variadic tuples), named tuples, and union types. For concrete struct types, the predicate descends into field types. Guards are present to avoid infinite recursion through known self‑contained types (e.g., `Complex`). - -# Arguments - -- `::Type`: Type to inspect \\[dimensionless\\]. - -# Returns - -- `Bool` indicating whether a `Measurement` occurs anywhere within the type structure. - -# Notes - -- For `AbstractArray{S}`, only the element type `S` is inspected. -- For `Tuple` and `NamedTuple{N,T}`, the parameters are traversed. -- For `Union`, both branches are inspected. -- Concrete `Complex` types are treated as terminal and are not descended. - -# Examples - -```julia -using Measurements - -$(FUNCTIONNAME)(Float64) # false -$(FUNCTIONNAME)(Measurement{Float64}) # true -$(FUNCTIONNAME)(Vector{Measurement{Float64}}) # true -$(FUNCTIONNAME)(Tuple{Int, Float64}) # false -$(FUNCTIONNAME)(Union{Int, Measurement{Float64}}) # true -``` -""" -_hasmeas_type(::Type{<:Measurement}) = true -_hasmeas_type(::Type{<:AbstractArray{S}}) where {S} = _hasmeas_type(S) -_hasmeas_type(::Type{<:Tuple{}}) = false -_hasmeas_type(::Type{T}) where {T<:Tuple} = - any(_hasmeas_type, Base.unwrap_unionall(T).parameters) -_hasmeas_type(::Type{NamedTuple{N,T}}) where {N,T} = _hasmeas_type(T) -_hasmeas_type(T::Union) = _hasmeas_type(T.a) || _hasmeas_type(T.b) -function _hasmeas_type(T::DataType) - # FIX: Add guard against recursing into Complex, which is self-contained. - T <: Complex && return false - isconcretetype(T) && any(_hasmeas_type, fieldtypes(T)) -end -_hasmeas_type(::Type) = false - -""" -$(TYPEDSIGNATURES) - -Determines whether a `Type` contains or is a `Complex` number type somewhere in its structure. The check is recursive over arrays, tuples (including variadic tuples), named tuples, and union types. For concrete struct types, the predicate descends into field types. Guards are present to avoid infinite recursion through known self‑referential types (e.g., `Measurements.Measurement`). - -# Arguments - -- `::Type`: Type to inspect \\[dimensionless\\]. - -# Returns - -- `Bool` indicating whether a `Complex` type occurs anywhere within the type structure. - -# Notes - -- For `AbstractArray{S}`, only the element type `S` is inspected. -- For `Tuple` and `NamedTuple{N,T}`, the parameters are traversed. -- For `Union`, both branches are inspected. -- Concrete `Measurement` types are treated as terminal and are not descended. - -# Examples - -```julia -$(FUNCTIONNAME)(Float64) # false -$(FUNCTIONNAME)(Complex{Float64}) # true -$(FUNCTIONNAME)(Vector{ComplexF64}) # true -$(FUNCTIONNAME)(Tuple{Int, ComplexF64}) # true -``` - -# Methods - -$(METHODLIST) -""" -function _hascomplex_type end -_hascomplex_type(::Type{<:Complex}) = true -_hascomplex_type(::Type{<:AbstractArray{S}}) where {S} = _hascomplex_type(S) -_hascomplex_type(::Type{<:Tuple{}}) = false -_hascomplex_type(::Type{T}) where {T<:Tuple} = - any(_hascomplex_type, Base.unwrap_unionall(T).parameters) -_hascomplex_type(::Type{NamedTuple{N,T}}) where {N,T} = _hascomplex_type(T) -_hascomplex_type(T::Union) = _hascomplex_type(T.a) || _hascomplex_type(T.b) -function _hascomplex_type(T::DataType) - # FIX: Add guard against recursing into Measurement, which is self-referential - # and known not to contain Complex types. This prevents StackOverflowError. - T <: Measurement && return false - isconcretetype(T) && any(_hascomplex_type, fieldtypes(T)) -end -_hascomplex_type(::Type) = false - -""" -$(TYPEDSIGNATURES) - -Resolves the **promotion target type** to be used by constructors and coercion utilities based on the runtime arguments. The decision uses structure‑aware predicates for `Measurement` and `Complex`: - -- If any argument contains `Measurement` and any contains `Complex`, returns `Complex{Measurement{BASE_FLOAT}}`. -- Else if any contains `Measurement`, returns `Measurement{BASE_FLOAT}`. -- Else if any contains `Complex`, returns `Complex{BASE_FLOAT}`. -- Otherwise returns `BASE_FLOAT`. - -# Arguments - -- `args...`: Values whose types will drive the promotion decision \\[dimensionless\\]. - -# Returns - -- A `Type` suitable for numeric promotion in subsequent coercion. - -# Examples - -```julia -using Measurements - -T = $(FUNCTIONNAME)(1.0, 2.0) # BASE_FLOAT -T = $(FUNCTIONNAME)(1 + 0im, 2.0) # Complex{BASE_FLOAT} -T = $(FUNCTIONNAME)(measurement(1.0, 0.1), 2.0) # Measurement{BASE_FLOAT} -T = $(FUNCTIONNAME)(measurement(1.0, 0.1), 2 + 0im) # Complex{Measurement{BASE_FLOAT}} -``` -""" -function resolve_T(args...) - types = map(typeof, args) - has_meas = any(_hasmeas_type, types) - has_complex = any(_hascomplex_type, types) - - if has_meas && has_complex - return Complex{Measurement{BASE_FLOAT}} - elseif has_meas - return Measurement{BASE_FLOAT} - elseif has_complex - return Complex{BASE_FLOAT} - else - return BASE_FLOAT - end -end - -""" -$(TYPEDSIGNATURES) - -Extracts the real inner type `S` from `Measurement{S}`. - -# Arguments - -- `::Type{Measurement{S}}`: Measurement type wrapper \\[dimensionless\\]. - -# Returns - -- The inner floating‐point type `S` \\[dimensionless\\]. - -# Examples - -```julia -using Measurements - -S = $(FUNCTIONNAME)(Measurement{Float64}) # Float64 -``` -""" -_meas_inner(::Type{Measurement{S}}) where {S} = S - -""" -$(TYPEDSIGNATURES) - -Element‑wise coercion kernel. Converts a *single leaf value* to the target type `T` while preserving semantics for `Measurement`, numeric types, and sentinels. - -# Arguments - -- `x`: Input leaf value \\[dimensionless\\]. -- `::Type{T}`: Target type \\[dimensionless\\]. - -# Returns - -- Value coerced to the target, according to the rules below. - -# Notes - -- `Number → R<:AbstractFloat`: uses `convert(R, x)`. -- `Number → M<:Measurement`: embeds the number as a zero‑uncertainty measurement (i.e., `zero(M) + x`). -- `Measurement → M<:Measurement`: uses `convert(M, measurement)`, which - preserves the source tag and derivative graph while converting the inner - floating-point type. -- `Measurement → R<:AbstractFloat`: drops uncertainty and converts the nominal value. -- `nothing` and `missing` pass through unchanged. -- `Bool`, `Symbol`, `String`, `Function`, `DataType`: passed through unchanged for measurement/real targets. -- Fallback: returns `x` unchanged. - -# Examples - -```julia -using Measurements - -$(FUNCTIONNAME)(1.2, Float32) # 1.2f0 -$(FUNCTIONNAME)(1.2, Measurement{Float64}) # 1.2 ± 0.0 -$(FUNCTIONNAME)(measurement(2.0, 0.1), Float32) # 2.0f0 -$(FUNCTIONNAME)(measurement(2.0, 0.1), Measurement{Float32}) # 2.0 ± 0.1 (Float32 inner) -$(FUNCTIONNAME)(missing, Float64) # missing -``` - -# Methods - -$(METHODLIST) -""" -function _coerce_elt_to_T end -_coerce_elt_to_T(x::Number, ::Type{R}) where {R<:AbstractFloat} = convert(R, x) -_coerce_elt_to_T(x::Number, ::Type{M}) where {M<:Measurement} = zero(M) + x -_coerce_elt_to_T(m::Measurement, ::Type{M}) where {M<:Measurement} = convert(M, m) -_coerce_elt_to_T(m::Measurement, ::Type{R}) where {R<:AbstractFloat} = convert(R, value(m)) -_coerce_elt_to_T(::Nothing, ::Type{T}) where {T} = nothing -_coerce_elt_to_T(::Missing, ::Type{T}) where {T} = missing -_coerce_elt_to_T(x::Bool, ::Type{M}) where {M<:Measurement} = x -_coerce_elt_to_T(x::Bool, ::Type{R}) where {R<:AbstractFloat} = x -_coerce_elt_to_T(x::Union{Symbol,String,Function,DataType}, ::Type{T}) where {T} = x -_coerce_elt_to_T(x, ::Type{T}) where {T} = x - -""" -$(TYPEDSIGNATURES) - -Public coercion API. Converts scalars and containers to a target type `T`, applying element‑wise coercion recursively. Complex numbers are handled by splitting into real and imaginary parts and coercing each side independently. - -# Arguments - -- `x`: Input value (scalar or container) \\[dimensionless\\]. -- `::Type{T}`: Target type \\[dimensionless\\]. - -# Returns - -- Value coerced to the target type: - - For `Real → Complex{P}`: constructs `Complex{P}(coerce_to_T(re, P), coerce_to_T(im, P))` (imaginary part from `0`). - - For `Complex → Real`: discards the imaginary part and coerces the real part. - - For `AbstractArray`, `Tuple`, `NamedTuple`: coerces each element recursively. - - For other types: defers to `_coerce_elt_to_T`. - -# Examples - -```julia -using Measurements - -# Scalar -$(FUNCTIONNAME)(1.2, Float32) # 1.2f0 -$(FUNCTIONNAME)(1.2, Measurement{Float64}) # 1.2 ± 0.0 -$(FUNCTIONNAME)(1 + 2im, Complex{Float32}) # 1.0f0 + 2.0f0im -$(FUNCTIONNAME)(1 + 2im, Float64) # 1.0 - -# Containers -$(FUNCTIONNAME)([1.0, 2.0], Measurement{Float64}) # measurement array -$(FUNCTIONNAME)((1.0, 2.0), Float32) # (1.0f0, 2.0f0) -$(FUNCTIONNAME)((; a=1.0, b=2.0), Float32) # (a = 1.0f0, b = 2.0f0) -``` -# Methods - -$(METHODLIST) - -# See also - -- [`_coerce_elt_to_T`](@ref) -- [`resolve_T`](@ref) -""" -function coerce_to_T end -# --- No-op for exact type matches (universal short-circuit) -coerce_to_T(x::T, ::Type{T}) where {T} = x # exact-type pass-through, no allocation - -# `Measurement <: Number`, so the generic Number method below is otherwise -# more specific than the universal exact-type method above. Keep this explicit -# specialization: rebuilding a Measurement from only its nominal value and -# standard uncertainty would turn a dependent quantity into a new independent -# variable and destroy covariance information. -coerce_to_T(x::M, ::Type{M}) where {M<:Measurement} = x - -# --- Numbers -# Promote Real to Complex when target is Complex -coerce_to_T(x::Real, ::Type{C}) where {P,C<:Complex{P}} = C(coerce_to_T(x, P)) - -# Complex → same Complex{P}: pass-through (avoid rebuilding) -coerce_to_T(x::Complex{P}, ::Type{Complex{P}}) where {P} = x - -# Complex → Complex{P′}: rebuild parts -coerce_to_T(x::Complex{S}, ::Type{Complex{P}}) where {S,P} = - Complex{P}(coerce_to_T(real(x), P), coerce_to_T(imag(x), P)) - -# Complex → Real: drop imag -coerce_to_T(x::Complex, ::Type{R}) where {R<:Real} = coerce_to_T(real(x), R) - -# Generic numbers → element coercion -coerce_to_T(x::Number, ::Type{T}) where {T} = _coerce_elt_to_T(x, T) - -# --- Containers -# Arrays: return the SAME array when element type already matches exactly -coerce_to_T(A::AbstractArray{T}, ::Type{T}) where {T} = A -coerce_to_T(A::AbstractArray, ::Type{T}) where {T} = broadcast(y -> coerce_to_T(y, T), A) - -# Tuples / NamedTuples (immutables): unavoidable allocation if types change -coerce_to_T(t::Tuple{Vararg{T}}, ::Type{T}) where {T} = t -coerce_to_T(t::Tuple, ::Type{T}) where {T} = map(y -> coerce_to_T(y, T), t) - -# --- NamedTuples (two non-overlapping methods) -# 1) Pass-through when every field is already T (strictly more specific) -coerce_to_T(nt::NamedTuple{K,TT}, ::Type{T}) where {K,T,TT<:Tuple{Vararg{T}}} = nt -# 2) Fallback: rebuild with coerced values -coerce_to_T(nt::NamedTuple{K,TT}, ::Type{T}) where {K,TT<:Tuple,T} = - NamedTuple{K}(map(v -> coerce_to_T(v, T), values(nt))) - -# --- Catch-all (must come last; pairs with the universal short-circuit above) -coerce_to_T(x, ::Type{T}) where {T} = _coerce_elt_to_T(x, T) - -# # No-op for exact type matches -# coerce_to_T(x::T, ::Type{T}) where {T} = x # exact-type pass-through, no allocation - -# # --- Numbers -# # Promote Real to Complex when target is Complex. -# coerce_to_T(x::Real, ::Type{C}) where {P,C<:Complex{P}} = C(coerce_to_T(x, P)) -# # General numbers fall back to element coercion. -# coerce_to_T(x::Number, ::Type{T}) where {T} = _coerce_elt_to_T(x, T) -# # Coerce a Complex number to a target Complex type. -# coerce_to_T(x::Complex, ::Type{C}) where {P,C<:Complex{P}} = -# C(coerce_to_T(real(x), P), coerce_to_T(imag(x), P)) -# # Coerce a Complex number to a Real type (drops imaginary part). -# coerce_to_T(x::Complex, ::Type{R}) where {R<:Real} = coerce_to_T(real(x), R) -# # --- Containers -# # Arrays: return the SAME array when element type already matches exactly -# coerce_to_T(A::AbstractArray, ::Type{T}) where {T} = broadcast(y -> coerce_to_T(y, T), A) -# # Tuples / NamedTuples (immutables): unavoidable allocation if types change -# coerce_to_T(t::Tuple, ::Type{T}) where {T} = map(y -> coerce_to_T(y, T), t) -# coerce_to_T(nt::NamedTuple, ::Type{T}) where {T} = -# NamedTuple{keys(nt)}(map(v -> coerce_to_T(v, T), values(nt))) -# # Fallback for other types. -# coerce_to_T(x, ::Type{T}) where {T} = _coerce_elt_to_T(x, T) diff --git a/src/validation/Validation.jl b/src/validation/Validation.jl deleted file mode 100644 index 5ce8b8469..000000000 --- a/src/validation/Validation.jl +++ /dev/null @@ -1,500 +0,0 @@ -""" - LineCableModels.Validation - -The [`Validation`](@ref) module implements a trait-driven, three-phase input checking pipeline for component constructors in `LineCableModels`. Inputs are first *sanitized* (arity and shape checks on raw arguments), then *parsed* (proxy values normalized to numeric radii), and finally validated by a generated set of rules. - -# Overview - -- Centralized constructor input handling: `sanitize` → `parse` → rule application. -- Trait hooks configure per‑type behavior (`has_radii`, `has_temperature`, `required_fields`, `keyword_fields`, `coercive_fields`, etc.). -- Rules are small value objects (`Rule` subtypes) applied to a normalized `NamedTuple`. - -# Dependencies - -$(IMPORTS) - -# Exports - -$(EXPORTS) -""" -module Validation - -# Export public API -export validate!, has_radii, has_temperature, extra_rules, - sanitize, parse, is_radius_input, required_fields, keyword_fields, keyword_defaults, - coercive_fields, Finite, Nonneg, Positive, IntegerField, Less, LessEq, IsA, Normalized, - OneOf, GreaterEq, Greater, PhysicalFillLimit, Satisfies - -# Module-specific dependencies -using ..Commons - -include("rules.jl") -include("applyrules.jl") - -""" -$(TYPEDSIGNATURES) - -Trait hook enabling the annular radii rule bundle on fields `:r_in` and `:r_ex` (normalized numbers required, finiteness, non‑negativity, and the ordering constraint `:r_in` < `:r_ex`). It does **not** indicate the mere existence of radii; it opts in to the annular/coaxial shell geometry checks. - -# Arguments - -- `::Type{T}`: Component type \\[dimensionless\\]. - -# Returns - -- `Bool` flag. - -# Examples - -```julia -Validation.has_radii(Tubular) # true -``` -""" -has_radii(::Type) = false # Default = false/empty. Components extend these. - -""" -$(TYPEDSIGNATURES) - -Trait hook indicating whether a component type uses a `:temperature` field subject to finiteness checks. - -# Arguments - -- `::Type{T}`: Component type \\[dimensionless\\]. - -# Returns - -- `Bool` flag. -""" -has_temperature(::Type) = false - -""" -$(TYPEDSIGNATURES) - -Trait hook providing additional rule instances for a component type. Used to append per‑type constraints after the standard bundles. - -# Arguments - -- `::Type{T}`: Component type \\[dimensionless\\]. - -# Returns - -- Tuple of [`Rule`](@ref) instances. -""" -extra_rules(::Type) = () # per-type extras - -""" -$(TYPEDSIGNATURES) - -Trait hook listing required fields that must be present after positional→named merge in `sanitize`. - -# Arguments - -- `::Type{T}`: Component type \\[dimensionless\\]. - -# Returns - -- Tuple of required field names. -""" -required_fields(::Type) = () - -""" -$(TYPEDSIGNATURES) - -Trait hook listing optional keyword fields considered by `sanitize`. - -# Arguments - -- `::Type{T}`: Component type \\[dimensionless\\]. - -# Returns - -- Tuple of optional keyword field names. -""" -keyword_fields(::Type) = () - -""" -$(TYPEDSIGNATURES) - -Trait hook supplying **default values** for optional keyword fields. - -Return either: -- a `NamedTuple` mapping defaults by name (e.g., `(temperature = T₀,)`), or -- a plain `Tuple` of defaults aligned with `keyword_fields(T)` (same order + length). - -Defaults are applied in `sanitize` **after** positional→named merge and before rule application. -User-provided keywords always override these defaults. - -# Arguments - -- `::Type{T}`: Component type \\[dimensionless\\]. - -# Returns - -- `NamedTuple` or `Tuple` with default keyword values. -""" -keyword_defaults(::Type) = () - -""" -$(TYPEDSIGNATURES) - -Trait hook listing **coercive** fields: values that participate in numeric promotion and will be converted to the promoted type by the convenience constructor. Defaults to all fields (`required_fields ∪ keyword_fields`). Types may override to exclude integers or categorical fields. - -# Arguments - -- `::Type{T}`: Component type \\[dimensionless\\]. - -# Returns - -- Tuple of field names that are coerced. -""" -coercive_fields(::Type{T}) where {T} = (required_fields(T)..., keyword_fields(T)...) - -""" -$(TYPEDSIGNATURES) - -Trait predicate that defines admissible *raw* radius inputs for a component type during `sanitize`. The default accepts real, non‑complex numbers only. Component code may extend this to allow proxies (e.g., `AbstractCablePart`, `Thickness`, `Diameter`). - -# Arguments - -- `::Type{T}`: Component type \\[dimensionless\\]. -- `x`: Candidate value \\[dimensionless\\]. - -# Returns - -- `Bool` indicating acceptance. - -# Examples - -```julia -Validation.is_radius_input(Tubular, 0.01) # true by default -Validation.is_radius_input(Tubular, 1 + 0im) # false (complex) -``` - -# See also - -- [`sanitize`](@ref) -""" -is_radius_input(::Type{T}, x) where {T} = (x isa Number) && !(x isa Complex) - -""" -$(TYPEDSIGNATURES) - -Field‑aware acceptance predicate used by `sanitize` to distinguish inner vs. outer radius policies. The default forwards to [`is_radius_input(::Type{T}, x)`](@ref) when no field‑specific method is defined. - -# Arguments - -- `::Type{T}`: Component type \\[dimensionless\\]. -- `::Val{F}`: Field tag; typically `Val(:r_in)` or `Val(:r_ex)` \\[dimensionless\\]. -- `x`: Candidate value \\[dimensionless\\]. - -# Returns - -- `Bool` indicating acceptance. - -# Examples - -```julia -Validation.is_radius_input(Tubular, Val(:r_in), 0.01) # true -Validation.is_radius_input(Tubular, Val(:r_ex), 0.01) # true -``` - -# See also - -- [`sanitize`](@ref) -- [`is_radius_input(::Type{T}, x)`](@ref) -""" -is_radius_input(::Type{T}, ::Val{F}, x) where {T, F} = is_radius_input(T, x) - -""" -$(TYPEDSIGNATURES) - -Default policy for **inner** radius raw inputs: accept real numbers. - -# Arguments - -- `::Type{T}`: Component type \\[dimensionless\\]. -- `::Val{:r_in}`: Field tag for the inner radius \\[dimensionless\\]. -- `x::Number`: Candidate value \\[dimensionless\\]. - -# Returns - -- `Bool` indicating acceptance (`true` for real, non‑complex numbers). - -# Examples - -```julia -Validation.is_radius_input(Tubular, Val(:r_in), 0.0) # true -Validation.is_radius_input(Tubular, Val(:r_in), 1+0im) # false -``` -""" -is_radius_input(::Type{T}, ::Val{:r_in}, x::Number) where {T} = - (x isa Number) && !(x isa Complex) -is_radius_input(::Type{T}, ::Val{:r_in}, ::Any) where {T} = false - -""" -$(TYPEDSIGNATURES) - -Default policy for **outer** radius raw inputs (annular shells): accept real numbers. Proxies are rejected at this stage to prevent zero‑thickness stacking. - -# Arguments - -- `::Type{T}`: Component type \\[dimensionless\\]. -- `::Val{:r_ex}`: Field tag for the outer radius \\[dimensionless\\]. -- `x::Number`: Candidate value \\[dimensionless\\]. - -# Returns - -- `Bool` indicating acceptance (`true` for real, non‑complex numbers). - -# Examples - -```julia -Validation.is_radius_input(Tubular, Val(:r_ex), 0.02) # true -``` -""" -is_radius_input(::Type{T}, ::Val{:r_ex}, x::Number) where {T} = - (x isa Number) && !(x isa Complex) -is_radius_input(::Type{T}, ::Val{:r_ex}, ::Any) where {T} = false - -""" -$(TYPEDSIGNATURES) - -Internal helper that canonicalizes `keyword_defaults(T)` into a `NamedTuple` -keyed by `keyword_fields(T)`. Accepts: - -- `()` → returns an empty `NamedTuple()`. -- `NamedTuple` → returned unchanged. -- `Tuple` → zipped **by position** with `keyword_fields(T)`; lengths must match. - -This function does **not** merge user-provided keywords; callers should perform -`merge(_kwdefaults_nt(T), kwargs)` so that user values take precedence. - -# Arguments - -- `::Type{T}`: Component type \\[dimensionless\\]. - -# Returns - -- `NamedTuple` mapping optional keyword field names to their default values. - -# Errors - -- If `keyword_defaults(T)` returns a `Tuple` whose length differs from - `length(keyword_fields(T))`. -- If `keyword_defaults(T)` returns a value that is neither `()` nor a - `NamedTuple` nor a `Tuple`. - -# Examples - -```julia -# Suppose: -# keyword_fields(::Type{X}) = (:temperature, :lay_direction) -# keyword_defaults(::Type{X}) = (T₀, 1) - -$(FUNCTIONNAME)(X) # => (temperature = T₀, lay_direction = 1) - -# If defaults are already a NamedTuple: -# keyword_defaults(::Type{Y}) = (temperature = 25.0,) -$(FUNCTIONNAME)(Y) # => (temperature = 25.0,) -```` - -# See also - -* [`keyword_fields`](@ref) -* [`keyword_defaults`](@ref) -* [`sanitize`](@ref) - """ -@inline function _kwdefaults_nt(::Type{T}) where {T} - defs = keyword_defaults(T) - defs === () && return NamedTuple() - if defs isa NamedTuple - return defs - elseif defs isa Tuple - keys = keyword_fields(T) - length(keys) == length(defs) || - Base.error( - "[$(String(nameof(T)))] keyword_defaults length $(length(defs)) ≠ keyword_fields length $(length(keys))", - ) - return NamedTuple{keys}(defs) - else - Base.error( - "[$(String(nameof(T)))] keyword_defaults must be NamedTuple or Tuple; got $(typeof(defs))", - ) - end -end - -""" -$(TYPEDSIGNATURES) - -Performs raw input checks and shapes the input into a `NamedTuple` without parsing proxies. Responsibilities: arity validation, positional→named mapping, required field presence, and raw acceptance of radius inputs via `is_radius_input` when `has_radii(T)` is true. - -# Arguments - -- `::Type{T}`: Component type \\[dimensionless\\]. -- `args::Tuple`: Positional arguments as received by the convenience constructor \\[dimensionless\\]. -- `kwargs::NamedTuple`: Keyword arguments \\[dimensionless\\]. - -# Returns - -- `NamedTuple` with raw (unparsed) fields. - -# Errors - -- `ArgumentError` on invalid arity, excess positional arguments, missing required fields, or rejected raw radius inputs. - -# Notes - -Required arguments must be positional; optional arguments must be passed as keywords. Positional arity must equal `length(required_fields(T))`. - -# Examples - -```julia -nt = $(FUNCTIONNAME)(Tubular, (0.01, 0.02, material), (; temperature = 20.0,)) -``` -""" -function sanitize(::Type{T}, args::Tuple, kwargs::NamedTuple) where {T} - # -- hard arity on required positionals -- - req = required_fields(T) - kw = keyword_fields(T) - nreq = length(req) - na = length(args) - if na != nreq - names = join(string.(req), ", ") - throw( - ArgumentError( - "[$(_typename(T))] expected exactly $nreq positional args ($names); got $na. Optionals must be keywords.", - ), - ) - end - - # positional -> named - nt_pos = (; (req[i] => args[i] for i ∈ 1:nreq)...) - - # reject unknown keywords (strict) - for k in keys(kwargs) - if !(k in kw) && !(k in req) - throw( - ArgumentError( - "[$(_typename(T))] unknown keyword '$k'. Allowed keywords: $(join(string.(kw), ", ")).", - ), - ) - end - end - - # user kw override positionals (if any same names) - nt = merge(nt_pos, kwargs) - - # backfill missing optional keywords with trait defaults --- - # defaults first, then user-provided values win - nt = merge(_kwdefaults_nt(T), nt) - - # radii raw acceptance (unchanged) - if has_radii(T) - haskey(nt, :r_in) || - throw(ArgumentError("[$(_typename(T))] missing 'r_in'.")) - haskey(nt, :r_ex) || - throw(ArgumentError("[$(_typename(T))] missing 'r_ex'.")) - is_radius_input(T, Val(:r_in), nt.r_in) || - throw( - ArgumentError( - "[$(_typename(T))] r_in not an accepted input: $(typeof(nt.r_in))", - ), - ) - is_radius_input(T, Val(:r_ex), nt.r_ex) || - throw( - ArgumentError( - "[$(_typename(T))] r_ex not an accepted input: $(typeof(nt.r_ex))", - ), - ) - end - return nt -end - -""" -$(TYPEDSIGNATURES) - -Parses and normalizes raw inputs produced by [`sanitize`](@ref) into the canonical form expected by rules. Default is identity; component code overrides this to resolve proxy radii to numeric values while preserving uncertainty semantics. - -# Arguments - -- `::Type{T}`: Component type \\[dimensionless\\]. -- `nt::NamedTuple`: Raw inputs from `sanitize` \\[dimensionless\\]. - -# Returns - -- `NamedTuple` with normalized fields (e.g., numeric `:r_in`, `:r_ex`). -""" -parse(::Type, nt) = nt - -""" -$(TYPEDSIGNATURES) - -Generates (at compile time, via a `@generated` function) the tuple of rules to apply for component type `T`. The result concatenates standard bundles driven by traits and any rules returned by `extra_rules(T)`. - -# Arguments - -- `::Type{T}`: Component type \\[dimensionless\\]. - -# Returns - -- Tuple of [`Rule`](@ref) instances to apply in order. -""" -@generated function _rules(::Type{T}) where {T} - :(( - ( - has_radii(T) ? - (Normalized(:r_in), Normalized(:r_ex), - Finite(:r_in), Nonneg(:r_in), - Finite(:r_ex), Nonneg(:r_ex), - Less(:r_in, :r_ex)) : () - )..., - (has_temperature(T) ? (Finite(:temperature),) : ())..., - extra_rules(T)..., - )) -end - -""" -$(TYPEDSIGNATURES) - -Runs the full validation pipeline for a component type: `sanitize` (arity and raw checks), `parse` (proxy normalization), then application of the generated rule set. Intended to be called from convenience constructors. - -# Arguments - -- `::Type{T}`: Component type \\[dimensionless\\]. -- `args...`: Positional arguments \\[dimensionless\\]. -- `kwargs...`: Keyword arguments \\[dimensionless\\]. - -# Returns - -- `NamedTuple` containing normalized fields ready for construction. - -# Errors - -- `ArgumentError` from `sanitize` or rule checks; `DomainError` for finiteness violations. - -# Examples - -```julia -nt = $(FUNCTIONNAME)(Tubular, 0.01, 0.02, material; temperature = 20.0) -# use nt.r_in, nt.r_ex, nt.temperature thereafter -``` - -# See also -- [`sanitize`](@ref) -- [`parse`](@ref) -- [`coercive_fields`](@ref) -""" -function validate!(::Type{T}, args...; kwargs...) where {T} - # One validate! to rule them all - - nt0 = sanitize(T, args, (; kwargs...)) - nt1 = parse(T, nt0) - # if has_radii: Normalized ensures numbers post-parse; if not numbers, rules will throw - rules = _rules(T) - @inbounds for i in eachindex(rules) - _apply(rules[i], nt1, T) - end - return nt1 -end - -end # module Validation diff --git a/src/validation/applyrules.jl b/src/validation/applyrules.jl deleted file mode 100644 index 92e03acd2..000000000 --- a/src/validation/applyrules.jl +++ /dev/null @@ -1,418 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Returns the simple (unqualified) name of type `T` as a `String`. Utility for constructing diagnostic messages. - -# Arguments - -- `::Type{T}`: Type whose name is requested \\[dimensionless\\]. - -# Returns - -- `String` with the type name \\[dimensionless\\]. - -# Examples - -```julia -name = $(FUNCTIONNAME)(Float64) # "Float64" -``` -""" -@inline _typename(::Type{T}) where {T} = String(nameof(T)) - -""" -$(TYPEDSIGNATURES) - -Returns a compact textual representation of `x` for error messages. - -# Arguments - -- `x`: Value to represent \\[dimensionless\\]. - -# Returns - -- `String` with a compact `repr` \\[dimensionless\\]. - -# Examples - -```julia -s = $(FUNCTIONNAME)(:field) # ":field" -``` -""" -@inline _repr(x) = repr(x; context = :compact => true) - -""" -$(TYPEDSIGNATURES) - -Asserts that `x` is a real (non‑complex) number. Used by rule implementations before performing numeric comparisons. - -# Arguments - -- `field`: Field name used in diagnostics \\[dimensionless\\]. -- `x`: Value to check \\[dimensionless\\]. -- `::Type{T}`: Component type for contextualized messages \\[dimensionless\\]. - -# Returns - -- Nothing. Throws on failure. - -# Errors - -- `ArgumentError` if `x` is not `isa Number` or is a `Complex` value. - -# Examples - -```julia -$(FUNCTIONNAME)(:r_in, 0.01, SomeType) # ok -``` -""" -@inline function _ensure_real(field::Symbol, x, ::Type{T}) where {T} - if !(x isa Number) || x isa Complex - throw( - ArgumentError( - "[$(_typename(T))] $field must be a real number, got $(typeof(x)): $(_repr(x))", - ), - ) - end -end - - -""" -$(TYPEDSIGNATURES) - -Applies [`Finite`](@ref) to ensure the target field is a finite real number. - -# Arguments - -- `r`: Rule instance \\[dimensionless\\]. -- `nt`: Normalized `NamedTuple` of inputs \\[dimensionless\\]. -- `::Type{T}`: Component type \\[dimensionless\\]. - -# Returns - -- Nothing. Throws on failure. -""" -@inline function _apply(r::Finite, nt, ::Type{T}) where {T} - x = getfield(nt, r.name) - _ensure_real(r.name, x, T) - isfinite(x) || throw(DomainError("[$(_typename(T))] $(r.name) must be finite, got $x")) -end - -""" -$(TYPEDSIGNATURES) - -Applies [`Nonneg`](@ref) to ensure the target field is `≥ 0`. - -# Arguments - -- `r`: Rule instance \\[dimensionless\\]. -- `nt`: Normalized `NamedTuple` of inputs \\[dimensionless\\]. -- `::Type{T}`: Component type \\[dimensionless\\]. - -# Returns - -- Nothing. Throws on failure. -""" -@inline function _apply(r::Nonneg, nt, ::Type{T}) where {T} - x = getfield(nt, r.name) - _ensure_real(r.name, x, T) - x >= 0 || throw(ArgumentError("[$(_typename(T))] $(r.name) must be ≥ 0, got $x")) -end - -""" -$(TYPEDSIGNATURES) - -Applies [`Positive`](@ref) to ensure the target field is `> 0`. - -# Arguments - -- `r`: Rule instance \\[dimensionless\\]. -- `nt`: Normalized `NamedTuple` of inputs \\[dimensionless\\]. -- `::Type{T}`: Component type \\[dimensionless\\]. - -# Returns - -- Nothing. Throws on failure. -""" -@inline function _apply(r::Positive, nt, ::Type{T}) where {T} - x = getfield(nt, r.name) - _ensure_real(r.name, x, T) - x > 0 || throw(ArgumentError("[$(_typename(T))] $(r.name) must be > 0, got $x")) -end - -""" -$(TYPEDSIGNATURES) - -Applies [`IntegerField`](@ref) to ensure the target field is an `Integer`. - -# Arguments - -- `r`: Rule instance \\[dimensionless\\]. -- `nt`: Normalized `NamedTuple` of inputs \\[dimensionless\\]. -- `::Type{T}`: Component type \\[dimensionless\\]. - -# Returns - -- Nothing. Throws on failure. -""" -@inline function _apply(r::IntegerField, nt, ::Type{T}) where {T} - x = getfield(nt, r.name) - x isa Integer || throw( - ArgumentError("[$(_typename(T))] $(r.name) must be Integer, got $(typeof(x))"), - ) -end - -""" -$(TYPEDSIGNATURES) - -Applies [`Less`](@ref) to ensure `nt[a] < nt[b]`. - -# Arguments - -- `r`: Rule instance with fields `a` and `b` \\[dimensionless\\]. -- `nt`: Normalized `NamedTuple` \\[dimensionless\\]. -- `::Type{T}`: Component type \\[dimensionless\\]. - -# Returns - -- Nothing. Throws on failure. -""" -@inline function _apply(r::Less, nt, ::Type{T}) where {T} - a = getfield(nt, r.a) - b = getfield(nt, r.b) - _ensure_real(r.a, a, T) - _ensure_real(r.b, b, T) - a < b || - throw(ArgumentError("[$(_typename(T))] $(r.a) < $(r.b) violated (got $a ≥ $b)")) -end - -""" -$(TYPEDSIGNATURES) - -Applies [`Greater`](@ref) to ensure `nt[a] > nt[b]`. - -# Arguments - -- `r`: Rule instance with fields `a` and `b` \\[dimensionless\\]. -- `nt`: Normalized `NamedTuple` \\[dimensionless\\]. -- `::Type{T}`: Component type \\[dimensionless\\]. - -# Returns - -- Nothing. Throws on failure. -""" -@inline function _apply(r::Greater, nt, ::Type{T}) where {T} - a = getfield(nt, r.a) - b = getfield(nt, r.b) - _ensure_real(r.a, a, T) - _ensure_real(r.b, b, T) - a > b || - throw(ArgumentError("[$(_typename(T))] $(r.a) > $(r.b) violated (got $a ≤ $b)")) -end - -""" -$(TYPEDSIGNATURES) - -Applies [`LessEq`](@ref) to ensure `nt[a] ≤ nt[b]`. - -# Arguments - -- `r`: Rule instance with fields `a` and `b` \\[dimensionless\\]. -- `nt`: Normalized `NamedTuple` \\[dimensionless\\]. -- `::Type{T}`: Component type \\[dimensionless\\]. - -# Returns - -- Nothing. Throws on failure. -""" -@inline function _apply(r::LessEq, nt, ::Type{T}) where {T} - a = getfield(nt, r.a) - b = getfield(nt, r.b) - _ensure_real(r.a, a, T) - _ensure_real(r.b, b, T) - a <= b || - throw(ArgumentError("[$(_typename(T))] $(r.a) ≤ $(r.b) violated (got $a > $b)")) -end - -""" -$(TYPEDSIGNATURES) - -Applies [`GreaterEq`](@ref) to ensure `nt[a] ≥ nt[b]`. - -# Arguments - -- `r`: Rule instance with fields `a` and `b` \\[dimensionless\\]. -- `nt`: Normalized `NamedTuple` \\[dimensionless\\]. -- `::Type{T}`: Component type \\[dimensionless\\]. - -# Returns - -- Nothing. Throws on failure. -""" -@inline function _apply(r::GreaterEq, nt, ::Type{T}) where {T} - a = getfield(nt, r.a) - b = getfield(nt, r.b) - _ensure_real(r.a, a, T) - _ensure_real(r.b, b, T) - a >= b || - throw(ArgumentError("[$(_typename(T))] $(r.a) ≥ $(r.b) violated (got $a < $b)")) -end - -""" -$(TYPEDSIGNATURES) - -Applies [`IsA{M}`](@ref) to ensure a field is of type `M`. - -# Arguments - -- `r`: Rule instance parameterized by `M` \\[dimensionless\\]. -- `nt`: Normalized `NamedTuple` \\[dimensionless\\]. -- `::Type{T}`: Component type \\[dimensionless\\]. - -# Returns - -- Nothing. Throws on failure. -""" -@inline function _apply(r::IsA{M}, nt, ::Type{T}) where {T, M} - x = getfield(nt, r.name) - x isa M || - throw(ArgumentError("[$(_typename(T))] $(r.name) must be $(M), got $(typeof(x))")) -end - -""" -$(TYPEDSIGNATURES) - -Applies [`Normalized`](@ref) to ensure the field has been converted to a numeric value during parsing. - -# Arguments - -- `r`: Rule instance \\[dimensionless\\]. -- `nt`: Normalized `NamedTuple` \\[dimensionless\\]. -- `::Type{T}`: Component type \\[dimensionless\\]. - -# Returns - -- Nothing. Throws on failure. -""" -@inline function _apply(r::Normalized, nt, ::Type{T}) where {T} - x = getfield(nt, r.name) - x isa Number || throw( - ArgumentError( - "[$(_typename(T))] $(r.name) must be normalized Number; got $(typeof(x))", - ), - ) -end - -""" -$(TYPEDSIGNATURES) - -Applies [`OneOf`](@ref) to ensure the target field is contained in a specified set. - -# Arguments - -- `r`: Rule instance with fields `name` and `set` \\[dimensionless\\]. -- `nt`: Normalized `NamedTuple` of inputs \\[dimensionless\\]. -- `::Type{T}`: Component type \\[dimensionless\\]. - -# Returns - -- Nothing. Throws on failure. -""" -@inline function _apply(r::OneOf{S}, nt, ::Type{T}) where {S, T} - x = getfield(nt, r.name) - (x in r.set) || throw( - ArgumentError( - "[$(String(nameof(T)))] $(r.name) must be one of $(collect(r.set)); got $(x)", - ), - ) -end - -""" - maxfill(::Type{T}, args...) - -Calculates the maximum physical number of strands that can fit for component `T`. -Custom shapes must overload this method. -""" -function maxfill end - -""" -$(TYPEDSIGNATURES) - -Applies [`PhysicalFillLimit`](@ref) to ensure the element count does not exceed the capacity computed by the extensible [`maxfill`](@ref) interface. - -# Arguments - -- `rule`: Rule instance with fields `n_field` and `geometry_fields` \\[dimensionless\\]. -- `nt`: Normalized `NamedTuple` of inputs \\[dimensionless\\]. -- `::Type{T}`: Component type \\[dimensionless\\]. - -# Returns - -- Nothing. Throws an `ArgumentError` on failure. -""" -@inline function Validation._apply(rule::PhysicalFillLimit, nt, ::Type{T}) where T - n = getfield(nt, rule.n_field) - - # We only run the math if the types are sound. If they are garbage, we quietly - # return and let the dedicated type rules (like IntegerField or _ensure_real) throw. - if n isa Integer - geom_args = getfield.(Ref(nt), rule.geometry_fields) - - if all(x -> x isa Real, geom_args) - limit = maxfill(T, geom_args...) - - n <= limit || throw( - ArgumentError( - "[$(_typename(T))] $(rule.n_field) $(_repr(n)) exceeds the physical maximum " * - "limit ($limit) given geometry dimensions $(_repr(geom_args))", - ), - ) - end - end - return nothing -end - -""" -$(TYPEDSIGNATURES) - -Applies [`Satisfies`](@ref) to evaluate an arbitrary predicate function against the specified fields. - -# Arguments - -- `rule`: Rule instance with `fields`, `predicate`, and `error_msg` \\[dimensionless\\]. -- `nt`: Normalized `NamedTuple` of inputs \\[dimensionless\\]. -- `::Type{T}`: Component type \\[dimensionless\\]. - -# Returns - -- Nothing. Throws an `ArgumentError` on failure. - -# Examples - -```julia -Validation.extra_rules(::Type{SomeWeirdType}) = ( - # ... basic type rules ... - - Satisfies( - (:width, :lay_angle, :overlap_pct), - (w, a, pct) -> pct < 1.0 && pct >= 0.0 && w * cos(a) > 0, - "Overlap percentage must be between 0 and 1, and effective width must be positive. Because I said so." - ) -) -``` -""" -@inline function Validation._apply(rule::Satisfies, nt, ::Type{T}) where T - args = getfield.(Ref(nt), rule.fields) - - # Evaluate the arbitrary predicate. We assume the predicate is robust enough - # or that prior type-enforcing rules have already sanitized the inputs. - if !rule.predicate(args...) - throw( - ArgumentError( - "[$(_typename(T))] Validation failed for fields $(_repr(rule.fields)): " * - "$(rule.error_msg) (Got values: $(_repr(args)))", - ), - ) - end - - return nothing -end diff --git a/src/validation/rules.jl b/src/validation/rules.jl deleted file mode 100644 index 145eca118..000000000 --- a/src/validation/rules.jl +++ /dev/null @@ -1,179 +0,0 @@ -""" -$(TYPEDEF) - -Base abstract type for validation rules. All concrete rule types must subtype [`Rule`](@ref) and provide an `_apply(::Rule, nt, ::Type{T})` method that checks a field in the normalized `NamedTuple` `nt` for the component type `T`. - -$(TYPEDFIELDS) -""" -abstract type Rule end - -""" -$(TYPEDEF) - -Rule that enforces finiteness of a numeric field. - -$(TYPEDFIELDS) -""" -struct Finite <: Rule - "Name of the field to check." - name::Symbol -end - -""" -$(TYPEDEF) - -Rule that enforces a field to be non‑negative (`≥ 0`). - -$(TYPEDFIELDS) -""" -struct Nonneg <: Rule - "Name of the field to check." - name::Symbol -end - -""" -$(TYPEDEF) - -Rule that enforces a field to be strictly positive (`> 0`). - -$(TYPEDFIELDS) -""" -struct Positive <: Rule - "Name of the field to check." - name::Symbol -end - -""" -$(TYPEDEF) - -Rule that enforces a field to be of an integer type. - -$(TYPEDFIELDS) -""" -struct IntegerField <: Rule - "Name of the field to check." - name::Symbol -end - -""" -$(TYPEDEF) - -Rule that enforces a strict ordering constraint `a < b` between two fields. - -$(TYPEDFIELDS) -""" -struct Less <: Rule - "Left‑hand field name." - a::Symbol - "Right‑hand field name." - b::Symbol -end - -""" -$(TYPEDEF) - -Rule that enforces a strict ordering constraint `a > b` between two fields. - -$(TYPEDFIELDS) -""" -struct Greater <: Rule - "Left‑hand field name." - a::Symbol - "Right‑hand field name." - b::Symbol -end - -""" -$(TYPEDEF) - -Rule that enforces a non‑strict ordering constraint `a ≤ b` between two fields. - -$(TYPEDFIELDS) -""" -struct LessEq <: Rule - "Left‑hand field name." - a::Symbol - "Right‑hand field name." - b::Symbol -end - -""" -$(TYPEDEF) - -Rule that enforces a non‑strict ordering constraint `a ≥ b` between two fields. - -$(TYPEDFIELDS) -""" -struct GreaterEq <: Rule - "Left‑hand field name." - a::Symbol - "Right‑hand field name." - b::Symbol -end - -""" -$(TYPEDEF) - -Rule that enforces a field to be `isa M` for a specified type parameter `M`. - -$(TYPEDFIELDS) -""" -struct IsA{M} <: Rule - "Name of the field to check." - name::Symbol -end - -""" -$(TYPEDEF) - -Rule that enforces that a field has already been normalized to a numeric value during parsing. Intended to guard that `parse` has executed and removed proxies. - -$(TYPEDFIELDS) -""" -struct Normalized <: Rule - "Name of the field to check." - name::Symbol -end - -""" -$(TYPEDEF) - -Rule that enforces a field to be `in` the set `S`. - -$(TYPEDFIELDS) -""" -struct OneOf{S} <: Rule - name::Symbol - set::S -end - -""" -$(TYPEDEF) - -Rule that enforces that the number of discrete elements (e.g., wires or strands) does not exceed the theoretical physical maximum for a given geometry. - -$(TYPEDFIELDS) -""" -struct PhysicalFillLimit <: Rule - "Symbol representing the field containing the element count (e.g., `:num_wires`)." - n_field::Symbol - "Tuple of symbols representing the geometric fields required to compute the limit." - geometry_fields::Tuple{Vararg{Symbol}} -end - -""" -$(TYPEDEF) - -A highly flexible rule that enforces an arbitrary user-defined predicate across one or more fields. -Useful for complex, cross-field physics or one-off geometrical constraints without polluting the ruleset with bespoke types. - -$(TYPEDFIELDS) -""" -struct Satisfies <: Rule - "Tuple of symbols representing the fields to be evaluated." - fields::Tuple{Vararg{Symbol}} - "A function (often anonymous) that accepts the extracted fields as arguments and returns a boolean." - predicate::Function - "The diagnostic message appended to the error if the predicate returns `false`." - error_msg::String -end diff --git a/test/Project.toml b/test/Project.toml new file mode 100644 index 000000000..12cb61298 --- /dev/null +++ b/test/Project.toml @@ -0,0 +1,69 @@ +[deps] +Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" +Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" +BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +Argus = "e39743ec-b2cc-45ef-a990-b666fc093538" +Calculus = "49dc2e85-a5d0-5ad3-a950-438e2897f1b9" +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" +Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" +DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +ExplicitImports = "7d51a73a-1435-4ff3-83d9-f097790105c7" +EzXML = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615" +GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" +Gmsh = "705231aa-382f-11e9-3f0c-b7cb4346fdeb" +JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" +JSONSchema = "7d188eb4-7ad8-530c-ae41-71a32a6d4692" +JuliaSyntax = "70703baa-626e-46a2-a12c-08ffd08c73b4" +LineCableModels = "dffb7669-8cd4-4628-99dd-7694e356ba37" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" +Measurements = "eff96d63-e80a-5855-80a2-b1b0885c5ab7" +QuadGK = "1fd47b50-473d-5c70-9696-f719f8f3bcdc" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +ReLint = "b3cc710f-9c33-5bdb-a03d-a94903873e97" +RequiredInterfaces = "97f35ef4-7bc5-4ec1-a41a-dcc69c7308c6" +SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" +Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" +SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" +TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" +XLSX = "fdbf4ff8-1666-58a4-91e7-1b58723a45e0" + +[sources] +LineCableModels = {path = ".."} +ReLint = {url = "https://github.com/RelationalAI-oss/ReLint.jl", rev = "61079427d9fc91cb3ead8f5b287aa3ae9b2269cb"} + +[compat] +Base64 = "1.11" +BenchmarkTools = "1" +JLD2 = "0.6" +Aqua = "0.8" +Argus = "=0.4.2" +Calculus = "0.5.2" +CairoMakie = "0.15" +DataFrames = "1.7.0" +Distributions = "0.25.122" +DocStringExtensions = "0.9.3" +ExplicitImports = "1.15" +EzXML = "1.2.0" +GeometryBasics = "0.5" +Gmsh = "0.3.1" +JET = "=0.12.2" +JSON3 = "1.14.2" +JSONSchema = "1.5" +JuliaSyntax = "1.0.2" +Measurements = "2.11.0" +QuadGK = "2.11.2" +ReLint = "=0.9.0" +RequiredInterfaces = "0.1.7" +SHA = "0.7" +SpecialFunctions = "2.5.0" +TestItemRunner = "1.3.1" +XLSX = "0.12" +julia = "1.12" diff --git a/test/README.md b/test/README.md new file mode 100644 index 000000000..221d8c017 --- /dev/null +++ b/test/README.md @@ -0,0 +1,241 @@ +# Test suite + +Tests check the current implementation and architecture, with a **95% production +line-coverage gate**. Scientific acceptance is a research judgment. Float32 inputs +must work without type-induced crashes. There is no extra Float32 accuracy promise. +See the [testing requirements](../docs/src/developers.md#testing-requirements). + +## While coding + +Run from the repository root with Julia 1.12. Resolve and instantiate the environment before first +use and after dependency changes: + +```sh +julia --project=test -e 'using Pkg; Pkg.resolve(); Pkg.instantiate()' +``` + +Use the corresponding project for additional environments. `resolve` refreshes +local manifests after changes to the developed package. `instantiate` installs +and precompiles that resolved graph. Finish those operations before starting tests. +Ordinary tests share the root workspace `Manifest.toml`. There is no separate +`test/Manifest.toml`. Do not run these environment operations concurrently or use `--compiled-modules=no` to +work around stale manifests. That flag is retained only for the existing native +and visual commands below. + +```sh +julia --project=test test/runtests.jl +julia --project=test test/runtests.jl unit/importexport/atp +julia --project=test test/runtests.jl integration/feasible_geometry_uq +julia --project=test test/runtests.jl tag:quality +julia --project=test test/runtests.jl changed:HEAD +``` + +Use a file selection while editing. The unfiltered command is the full sweep. +The unfiltered Julia command runs ordinary unit, integration and non-native +extension checks, including small UQ sampling, aggregation and linear propagation. +`tag:quality` checks architecture, formula ownership, descriptions and the actual +external interfaces of loaded adapters. + +Selectors match substrings in filenames or item names, or exact `tag:`. Multiple names are +alternatives. Multiple tags are alternatives. Both groups must match when combined. +Append `--list` to inspect selection without executing bodies. Empty selections +and unknown options fail. The runner prints item starts, selected files and items, +elapsed time and completion or failure. A started item is not necessarily completed. + +`changed:REF` selects the items that the changes since the git revision `REF` can +affect, untracked files included. It takes no other selector. The runner uses the +test taxonomy below to choose them: + +- every item whose owner tag is the owner of a changed file under `src/` or `ext/`, + or a later owner. `slow` items are left out. +- every item of a changed test file, `slow` or not, and every item that uses a setup + defined in a changed test file. +- after a change under `test/support/`, every item that is not `slow`, and every item + under `test/unit/core/`, which checks the runner itself. +- every `quality` item. + +Items with an environment tag stay out, as in an ordinary run. The selected items +outside `quality` run in the current process. The `quality` items then run in a new +process, exactly as `tag:quality` runs alone, because the architecture guards read the +live method table that earlier items can extend. The run fails when either part fails. +`--list` lists both parts. + +## Test taxonomy + +Each item outside `quality` has exactly one owner tag. The owner tag is the latest layer, +in load order, whose code the item exercises. A change in one layer can affect only that +layer and the layers loaded after it. `changed:REF` relies on this order. The owner tags +in load order are `units`, `commons`, `materials`, `earth`, `datamodel`, `engine`, +`parametric`, `uq`, `report`, `importexport` and `pscad`. The extension owners +follow them: `measurements`, `distributions`, `xlsx`, `fem` and `makie`. `quality` +items have no owner tag. + +`test/support/taxonomy.jl` defines the owner tags, the package module that each owner +covers and the kind tags. The runner, the architecture guards and the tools read it. +It also gives each source file the owner of its load position. A file that +`src/LineCableModels.jl` includes directly takes the owner of the last module included +before it, so `src/gridspace.jl` belongs to `commons` and `src/performance.jl` to +`uq`. TextDisplay and PlotBuilder belong to `commons`. The files of a child module take +the owner of its parent: the formula families and ModalAnalysis belong to `engine`. + +Each item also has at least one kind tag: `unit`, `integration`, `extension`, +`visual`, `quality` or `aqua`. The environment tags `visual`, `fem_numerical`, +`pscad_native`, `core_only` and `aqua` keep items out of ordinary runs. `slow` marks +items that took more than 10 s in the last full run. + +The quality items check the taxonomy. An item outside `quality` has one owner tag and a +`quality` item has none. Each item has a kind tag and only known tags. An item under +`test/unit//` has the owner of `src/` or a later one. + +Give a new item the owner of the latest layer it reaches. To check the choice, record +the code that each item runs and compare it with the tags: + +```sh +julia --project=test --code-coverage=@$PWD --code-coverage=/tmp/trace.info test/tools/owners.jl record DIR +julia --project=test test/tools/owners.jl report DIR +``` + +The report lists every item whose owner tag is earlier than the code it runs. + +At the end of a track, run `test/tools/durations.jl` on the log of the full run and of +each environment run. It derives the duration of each item from the `Starting` lines. +It lists the items above 10 s without `slow`, the `slow` items below 5 s and the minutes +per owner tag. It reports these findings and leaves the tags unchanged. + +```sh +julia --project=test test/tools/durations.jl full-suite.log visual.log fem_numerical.log +``` + +## Cadence + +During a change, run `changed:REF` from the commit that the step starts from. At the end +of a track, before a push, run the full suite including `slow` items. Also run the +extension and visual environments that the change touches, the documentation build, and +Vale and cspell with the versions that CI uses. Finally, run `test/tools/durations.jl` on +the new local logs and the equivalence check that the +[developer guide](../docs/src/developers.md#preservation-locks) describes. The timing +comparison runs in CI. Tests are deferred to the end of a track, never skipped. + +## Advisory source diagnostics + +`tag:quality` also runs Fatou and three repository-owned ReLint rules against every +Julia source file under `src/` and `ext/`, including PSCAD. These items read source +without importing LineCableModels or starting external solvers. Ordinary runs +exclude them, and `--list` executes neither scanner. + +Resolve and instantiate the test project to install ReLint at revision +`61079427d9fc91cb3ead8f5b287aa3ae9b2269cb` (0.9.0) and Argus 0.4.2. Install +Fatou **0.22.0** separately and place it on `PATH`. For Linux or macOS, use the +same pinned installer as CI: + +```sh +curl --fail --location \ + https://raw.githubusercontent.com/jolars/fatou-action/6b6a66a1f28fce0d1ff31048c609e63b7006136e/scripts/install-fatou.sh \ + --output /tmp/install-lcm-fatou.sh +FATOU_VERSION=v0.22.0 FATOU_VERIFY_CHECKSUM=true \ + FATOU_INSTALL_DIR="$HOME/.local/bin" sh /tmp/install-lcm-fatou.sh +export PATH="$HOME/.local/bin:$PATH" +fatou --version + +julia --project=test test/runtests.jl tag:quality --list +julia --project=test test/runtests.jl quality/fatou.jl quality/relint.jl +julia --project=test test/runtests.jl tag:quality +``` + +Windows binaries are available from the +[Fatou 0.22.0 release](https://github.com/jolars/fatou/releases/tag/v0.22.0). +Tests never install tools. A wrong Fatou version fails the item. A missing Fatou +fails the item in CI, which sets `CI=true`. A local run without Fatou records the item +as skipped and prints a message. + +All selected source findings are **advisory**, regardless of their count. +`fatou.toml` selects 15 rules explicitly and sets their severity to `warning`. +ReLint runs only `runtime-eval`, `constant-catch-result`, and `private-forwarder`. +The scanner can report typed forwarding methods as candidates without determining +whether their dispatch purpose is useful. Inert quotations and the documented +syntactic exclusions are exercised by controls in each test item. + +Invalid configuration, unreadable source, parse failures, malformed output, +unexpected termination, and failed controls fail the quality job. Fatou's +findings-only exit status is accepted after validating its diagnostics. A passing +item establishes that its controls and scan completed. It does not establish the +absence of findings or certify architecture or scientific behavior. + +Each scanner prints every finding and per-rule counts. Fatou also prints its +native JSON, including parse diagnostics. CI retains the complete quality log in +the `quality-test-report` artifact, including failed runs that produced a log. +Source-file counts do not contribute to the production line-coverage requirement. +No automatic repairs, formatting, suppression additions, or promotion to blocking +source rules are part of these checks. + +## Additional execution environments + +| Command | What it executes | +| --- | --- | +| `DISPLAY= julia --project=test --compiled-modules=no test/runtests.jl tag:fem_numerical` | Native GetDP/Gmsh execution, extraction, material transport, reductions, failure and resume. The UI item is skipped here. | +| `julia --project=test test/runtests.jl extensions/fem_ui.jl` | Four UI lifecycle scenarios in fresh processes. Requires an accessible `DISPLAY`. CI uses `xvfb-run -a`. | +| `julia --project=test/core test/runtests.jl tag:core_only` | Optional-extension behavior with core dependencies only. | +| `LINECABLEMODELS_TEST_PLOTTING=true julia --project=test/visual --compiled-modules=no test/runtests.jl tag:visual` | Existing rendering, plot lifecycle, layout and output checks. No new calibration. | +| `julia --project=test test/runtests.jl tag:pscad_native` | Real PSCAD acceptance. Set `LINECABLEMODELS_PSCAD_CONFIG` to your private TOML filename. Requires the configured live station. | +| `julia --project=test test/runtests.jl tag:aqua` | Aqua in a fresh Julia process. | +| `julia --project=docs docs/doctest.jl` and `julia --project=docs docs/make.jl` | Docstrings and documentation build. Instantiate with `docs/instantiate.jl`. | +| `julia --project=src/pscad/remote test/integration/pscad/remote_runner_protocol.jl` | Remote-runner protocol against a mocked Python automation interface. Does not execute PSCAD or validate solver numerics. | + +Ordinary exclusions are `quality`, `aqua`, `visual`, `core_only`, `fem_numerical` and `pscad_native`. +These tags describe purpose and environment, never outcome. +PSCAD native tests require the explicit `tag:pscad_native` selector. A filename or item-name +search alone cannot launch a station. Ordinary local PSCAD tests use Julia protocol +fixtures. The dedicated remote-runner protocol test mocks only the Python automation +surface needed to verify runner orchestration. It does not imitate PSCAD numerics. +Native coverage gaps remain visible in the unchanged production coverage inventory +and 95% gate. +There is no required scientific-study job or `scientific` selection. + +## Coverage and release verification + +The [existing CI workflow](../.github/workflows/CI.yml) runs ordinary tests on +Julia 1.12 and prereleases. It also checks the environments above, Cairo/GL/WGL +activation, clean installation, documentation and merged coverage. Release +verification adds execution environments. It does not certify model physics. + +For coverage, use one Julia/BLAS thread and remove old traces first: + +```sh +export JULIA_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=1 +julia --project=test/coverage test/coverage.jl clean +julia --project=. -e 'using Pkg; Pkg.test(coverage=true)' +``` + +Run the additional environments above with `--code-coverage=@.`. For activation, +use `julia --project=test/visual --code-coverage=@. test/runtests.jl "loaded extension activation"` +for Cairo. CI contains the temporary-environment commands for GLMakie and WGLMakie. +Complete the display-dependent UI run as well. Then merge and enforce the gate: + +```sh +julia --project=test/coverage test/coverage.jl check +``` + +This inventories every production Julia file under `src/` and `ext/`, writes +`lcov.info` and fails below 95%. The check removes traces afterward, including on failure. +Retain the report. Missing executions and failing tests remain visible even if +the line percentage passes. + +## Measured execution and remaining work + +The [execution report](../local/validation-refoundation/2026-09-15/execution/report.md) +records scopes and measured wall times on the shared local machine. +The ATP file selection took 41 s. Core-only environment setup plus six checks took +74 s. Quality took 279 s. Native FEM and the existing visual selection took +about 25 minutes each. These include compilation and are not isolated CI timing +predictions. + +The archived runs describe an earlier solver inventory and are not validation +of the current QuadGK-only spectral path. The Julia 1.12 full run reached its +90-minute limit. Final coverage is **19,609/20,582 production lines (95.27%)**, passing the unchanged +95% gate. Three earth-return items remain unfinished on Julia 1.12. They completed +on 1.13.0-rc4. The report identifies these exact items. Coverage does not close them. + +The earlier FEM/analytical mutual-admittance difference, transformed-exterior +quadrature sensitivity and unresolved derivative-reference study remain in the +[dated evidence](../local/validation-refoundation/2026-09-15/test-system-audit.md). +These research observations require scientific review. They do not establish a code failure or approve a release. diff --git a/test/aqua.jl b/test/aqua.jl deleted file mode 100644 index cda8ad5d9..000000000 --- a/test/aqua.jl +++ /dev/null @@ -1,5 +0,0 @@ -@testitem "Aqua tests" tags=[:skipci] begin - using Aqua - Aqua.test_all(LineCableModels) -end - diff --git a/test/baseparams.jl b/test/baseparams.jl deleted file mode 100644 index e09486109..000000000 --- a/test/baseparams.jl +++ /dev/null @@ -1,377 +0,0 @@ -@testitem "BaseParams module" setup = [defaults, defs_materials] begin - @testset "Temperature correction" begin - alpha = 0.004 - T0 = 20.0 - # Correction factor should be 1 at reference temperature - @test calc_temperature_correction(alpha, T0, T0) ≈ 1.0 atol = TEST_TOL - # Test T > T0 - @test calc_temperature_correction(alpha, 30.0, T0) ≈ (1 + alpha * (30.0 - T0)) atol = - TEST_TOL - # Test T < T0 - @test calc_temperature_correction(alpha, 10.0, T0) ≈ (1 + alpha * (10.0 - T0)) atol = - TEST_TOL - # No correction if alpha is zero - @test calc_temperature_correction(0.0, 50.0, T0) ≈ 1.0 atol = TEST_TOL - end - - @testset "Parallel impedance calculations" begin - # Parallel equivalent of two equal resistors - @test calc_parallel_equivalent(10.0, 10.0) ≈ 5.0 atol = TEST_TOL - # Parallel equivalent of two different resistors - @test calc_parallel_equivalent(10.0, 5.0) ≈ (10.0 * 5.0) / (10.0 + 5.0) atol = - TEST_TOL - # Adding infinite resistance changes nothing - @test calc_parallel_equivalent(10.0, Inf) ≈ 10.0 atol = TEST_TOL - # Adding zero resistance results in zero (short circuit) - @test calc_parallel_equivalent(10.0, 0.0) ≈ 0.0 atol = TEST_TOL - - # Complex numbers (impedances) - Z1 = 3.0 + 4.0im - Z2 = 8.0 - 6.0im - Zeq_expected = (Z1 * Z2) / (Z1 + Z2) - # Parallel equivalent of complex impedances - @test calc_parallel_equivalent(Z1, Z2) ≈ Zeq_expected atol = TEST_TOL - # Parallel equivalent of two equal complex impedances - @test calc_parallel_equivalent(Z1, Z1) ≈ Z1 / 2 atol = TEST_TOL - end - - @testset "Equivalent temperature coefficient" begin - alpha1, R1 = 0.004, 10.0 - alpha2, R2 = 0.003, 5.0 - expected_alpha = (alpha1 * R2 + alpha2 * R1) / (R1 + R2) - @test calc_equivalent_alpha(alpha1, R1, alpha2, R2) ≈ expected_alpha atol = TEST_TOL - # Equivalent alpha of identical conductors - @test calc_equivalent_alpha(alpha1, R1, alpha1, R1) ≈ alpha1 atol = TEST_TOL - # Check symmetry - @test calc_equivalent_alpha(alpha1, R1, alpha2, R2) ≈ - calc_equivalent_alpha(alpha2, R2, alpha1, R1) atol = TEST_TOL - end - - @testset "Resistance calculations" begin - # Using Copper properties - rho = copper_props.rho - alpha = copper_props.alpha - T0 = copper_props.T0 - T = T0 # Test at reference temperature first - - # calc_tubular_resistance - r_in, r_ext = 0.01, 0.02 - area_tube = π * (r_ext^2 - r_in^2) - # Tubular resistance at T0 - @test calc_tubular_resistance(r_in, r_ext, rho, alpha, T0, T) ≈ rho / area_tube atol = - TEST_TOL - # Solid conductor - area_solid = π * r_ext^2 - # Solid conductor resistance (r_in=0) - @test calc_tubular_resistance(0.0, r_ext, rho, alpha, T0, T) ≈ rho / area_solid atol = - TEST_TOL - # Temperature dependence - T_hot = 70.0 - k = calc_temperature_correction(alpha, T_hot, T0) - # Tubular resistance temperature dependence - @test calc_tubular_resistance(r_in, r_ext, rho, alpha, T0, T_hot) ≈ - (rho / area_tube) * k atol = TEST_TOL - # Thin tube limit (resistance should increase) - check relative magnitude - r_in_thin = r_ext * 0.999 - # Thin tube has higher resistance - @test calc_tubular_resistance(r_in_thin, r_ext, rho, alpha, T0, T) > - calc_tubular_resistance(r_in, r_ext, rho, alpha, T0, T) - - # calc_strip_resistance - thickness, width = 0.002, 0.05 - area_strip = thickness * width - # Strip resistance at T0 - @test calc_strip_resistance(thickness, width, rho, alpha, T0, T) ≈ rho / area_strip atol = - TEST_TOL - # Strip resistance temperature dependence - @test calc_strip_resistance(thickness, width, rho, alpha, T0, T_hot) ≈ - (rho / area_strip) * k atol = TEST_TOL - end - - @testset "Helical parameters correction" begin - r_in, r_ext = 0.01, 0.015 - mean_diam_expected = r_in + r_ext # 0.025 - lay_ratio = 12.0 - pitch_expected = lay_ratio * mean_diam_expected # 12.0 * 0.025 = 0.3 - - mean_diam, pitch, overlength = calc_helical_params(r_in, r_ext, lay_ratio) - @test mean_diam ≈ mean_diam_expected atol = TEST_TOL - @test pitch ≈ pitch_expected atol = TEST_TOL - @test overlength ≈ sqrt(1 + (π * mean_diam_expected / pitch_expected)^2) atol = - TEST_TOL - # Overlength factor must be > 1 for finite lay ratio - @test overlength > 1.0 - - # Edge case: No twist (infinite pitch length) - mean_diam_no, pitch_no, overlength_no = calc_helical_params(r_in, r_ext, 0.0) - # Note: lay_ratio=0 implies pitch=0 in the code, which makes overlength=1 - @test mean_diam_no ≈ mean_diam_expected atol = TEST_TOL - @test pitch_no == 0.0 - # Overlength factor must be 1 for zero lay ratio (infinite pitch) - @test overlength_no ≈ 1.0 atol = TEST_TOL - end - - @testset "GMR calculations & consistency" begin - r_in, r_ext = 0.01, 0.02 - mu_r = 1.0 # Non-magnetic - - # calc_tubular_gmr - gmr_tube = calc_tubular_gmr(r_ext, r_in, mu_r) - # GMR of tube should be less than outer radius - @test gmr_tube < r_ext - # GMR must be positive - @test gmr_tube > 0 - - # Solid conductor GMR - gmr_solid_expected = r_ext * exp(-mu_r / 4.0) - gmr_solid_calc = calc_tubular_gmr(r_ext, 0.0, mu_r) - # Solid conductor GMR (analytical) - @test gmr_solid_calc ≈ gmr_solid_expected atol = TEST_TOL - - # Thin shell GMR - gmr_shell_calc = calc_tubular_gmr(r_ext, r_ext * (1 - 1e-12), mu_r) # Approx thin shell - # Thin shell GMR approaches outer radius # Relax tolerance slightly - @test gmr_shell_calc ≈ r_ext atol = 1e-5 - - # Magnetic material - mu_r_mag = 100.0 - gmr_solid_mag = calc_tubular_gmr(r_ext, 0.0, mu_r_mag) - gmr_solid_mag_expected = r_ext * exp(-mu_r_mag / 4.0) - # Solid conductor GMR with mu_r > 1 - @test gmr_solid_mag ≈ gmr_solid_mag_expected atol = TEST_TOL - # Higher mu_r should decrease GMR - @test gmr_solid_mag < gmr_solid_calc - - # Error handling - @test_throws ArgumentError calc_tubular_gmr(r_in, r_ext, mu_r) # Should throw error if r_ext < r_in - - # calc_equivalent_mu (inverse consistency) - @test calc_equivalent_mu(gmr_tube, r_ext, r_in) ≈ mu_r atol = TEST_TOL # Inverse check: mu_r from tubular GMR - @test calc_equivalent_mu(gmr_solid_calc, r_ext, 0.0) ≈ mu_r atol = TEST_TOL # Inverse check: mu_r from solid GMR - @test calc_equivalent_mu(gmr_solid_mag, r_ext, 0.0) ≈ mu_r_mag atol = TEST_TOL # Inverse check: magnetic mu_r from solid GMR - @test_throws ArgumentError calc_equivalent_mu(gmr_tube, r_in, r_ext) # Should throw error if r_ext < r_in - - # calc_circstrands_gmr - wire_rad = 0.001 - num_wires = 7 - layout_rad = 0.005 # Center-to-center radius - gmr_array = calc_circstrands_gmr(layout_rad, num_wires, wire_rad, mu_r) - # Single wire case should match solid wire GMR - @test gmr_array > 0 - - gmr_single_circ_strands = calc_circstrands_gmr(0.0, 1, wire_rad, mu_r) # Layout radius irrelevant for N=1 - gmr_single_wire_solid = calc_tubular_gmr(wire_rad, 0.0, mu_r) - # GMR of 1-wire array matches solid wire GMR - @test gmr_single_circ_strands ≈ gmr_single_wire_solid atol = TEST_TOL - - end - - @testset "GMD and equivalent GMR" begin - # Need some cable parts - part1_solid = Tubular(0.0, 0.01, copper_props) # Solid conductor r=1cm - part2_tubular = Tubular(0.015, 0.02, copper_props) # Tubular conductor, separate - part3_circstrands = CircStrands(0.03, 0.002, 7, 10.0, aluminum_props) # Wire array, separate - - # calc_gmd - # Case 1: Two separate solid/tubular conductors (distance between centers) - # Place part2 at (d, 0) relative to part1 at (0,0) - d = 0.1 # 10 cm separation - # GMD calculation for simple geometries relies on center-to-center distance if not wire arrays - # This test might be trivial for Tubular/Tubular if code assumes center-to-center - # Let's test Tubular vs CircStrands where sub-elements exist - gmd_1_3 = calc_gmd(part1_solid, part3_circstrands) # Should be approx layout_radius of part3 (0.03 + 0.002) if part1 is at center - # GMD between central solid and wire array approx layout radius - @test gmd_1_3 ≈ (0.03 + 0.002) atol = 1e-4 - - # Case 2: Concentric Tubular Conductors (test based on comment in code) - part_inner = Tubular(0.01, 0.02, copper_props) - part_outer = Tubular(0.02, 0.03, copper_props) # Directly outside inner part - # If truly concentric, d_ij = 0 for internal logic, should return max(r_ext1, r_ext2) - gmd_concentric = calc_gmd(part_inner, part_outer) - # GMD of concentric tubular conductors - @test gmd_concentric ≈ part_outer.r_ex atol = TEST_TOL - - # calc_equivalent_gmr - # Create a conductor group to test adding layers - core = ConductorGroup(part1_solid) - layer2 = Tubular(core.r_ex, 0.015, aluminum_props) # Add tubular layer outside - beta = core.cross_section / (core.cross_section + layer2.cross_section) - gmd_core_layer2 = calc_gmd(core.layers[end], layer2) # GMD between solid core and new layer - - gmr_eq_expected = - (core.gmr^(beta^2)) * (layer2.gmr^((1 - beta)^2)) * - (gmd_core_layer2^(2 * beta * (1 - beta))) - gmr_eq_calc = calc_equivalent_gmr(core, layer2) # Test the function directly - @test gmr_eq_calc ≈ gmr_eq_expected atol = TEST_TOL - - # Test adding a CircStrands layer - layer3_wa = CircStrands(layer2.r_ex, 0.001, 12, 15.0, copper_props) - # Need to update core equivalent properties first before calculating next step - core.gmr = gmr_eq_calc # Update core GMR based on previous step - core.cross_section += layer2.cross_section # Update core area - push!(core.layers, layer2) # Add layer for subsequent GMD calculation - - beta2 = core.cross_section / (core.cross_section + layer3_wa.cross_section) - gmd_core_layer3 = calc_gmd(core.layers[end], layer3_wa) # GMD between tubular layer2 and wire array layer3 - - gmr_eq2_expected = - (core.gmr^(beta2^2)) * (layer3_wa.gmr^((1 - beta2)^2)) * - (gmd_core_layer3^(2 * beta2 * (1 - beta2))) - gmr_eq2_calc = calc_equivalent_gmr(core, layer3_wa) - @test gmr_eq2_calc ≈ gmr_eq2_expected atol = TEST_TOL - end - - - @testset "Inductance calculations" begin - # calc_tubular_inductance - r_in, r_ext = 0.01, 0.02 - mu_r = 1.0 - L_expected = mu_r * μ₀ / (2 * π) * log(r_ext / r_in) - @test calc_tubular_inductance(r_in, r_ext, mu_r) ≈ L_expected atol = TEST_TOL - @test calc_tubular_inductance(r_in, r_ext, 2.0 * mu_r) ≈ 2.0 * L_expected atol = - TEST_TOL # Check mu_r scaling - # Internal inductance of solid conductor is infinite in this simple model - @test calc_tubular_inductance(0.0, r_ext, mu_r) == Inf - - # calc_inductance_trifoil - Requires benchmark data or simplified checks - # This is complex. A simple check could be ensuring L > 0 for typical inputs. - r_in_co, r_ext_co = 0.01, 0.015 - r_in_scr, r_ext_scr = 0.02, 0.022 - S = 0.1 - L_trifoil = - calc_inductance_trifoil(r_in_co, r_ext_co, copper_props.rho, copper_props.mu_r, - r_in_scr, r_ext_scr, copper_props.rho, copper_props.mu_r, S) - # Trifoil inductance should be positive - @test L_trifoil > 0 - # Could test sensitivity: increasing S should generally decrease L - L_trifoil_S2 = - calc_inductance_trifoil(r_in_co, r_ext_co, copper_props.rho, copper_props.mu_r, - r_in_scr, r_ext_scr, copper_props.rho, copper_props.mu_r, 2 * S) - # Increasing separation S should decrease L - @test L_trifoil_S2 < L_trifoil - - end - - @testset "Capacitance & conductance" begin - r_in, r_ext = 0.01, 0.02 - eps_r = insulator_props.eps_r - rho_ins = insulator_props.rho - - # calc_shunt_capacitance - C_expected = 2 * π * ε₀ * eps_r / log(r_ext / r_in) - @test calc_shunt_capacitance(r_in, r_ext, eps_r) ≈ C_expected atol = TEST_TOL - # Increasing r_ext decreases C - @test calc_shunt_capacitance(r_in, r_ext * 10, eps_r) < C_expected - # Decreasing r_in decreases C - @test calc_shunt_capacitance(r_in / 10, r_ext, eps_r) < C_expected - # If r_in -> r_ext, log -> 0, C -> Inf. Test approach? - # Capacitance -> Inf as r_in approaches r_ext - @test isinf(calc_shunt_capacitance(r_ext, r_ext, eps_r)) - - # calc_shunt_conductance - G_expected = 2 * π * (1 / rho_ins) / log(r_ext / r_in) - @test calc_shunt_conductance(r_in, r_ext, rho_ins) ≈ G_expected atol = TEST_TOL - # Lower rho increases G - @test calc_shunt_conductance(r_in, r_ext, rho_ins / 10) ≈ 10 * G_expected atol = - TEST_TOL - # Infinite rho (perfect insulator) gives zero G - @test calc_shunt_conductance(r_in, r_ext, Inf) ≈ 0.0 atol = TEST_TOL - # Conductance -> Inf as r_in approaches r_ext - @test isinf(calc_shunt_conductance(r_ext, r_ext, rho_ins)) - end - - @testset "Equivalent dielectric properties consistency" begin - r_in, r_ext = 0.01, 0.02 - eps_r = insulator_props.eps_r - rho_ins = insulator_props.rho - C_eq = calc_shunt_capacitance(r_in, r_ext, eps_r) - G_eq = calc_shunt_conductance(r_in, r_ext, rho_ins) - - # calc_equivalent_eps - # Inverse check: eps_r from C_eq - @test calc_equivalent_eps(C_eq, r_ext, r_in) ≈ eps_r atol = TEST_TOL - - # calc_sigma_lossfact & inverse check - sigma_eq = calc_sigma_lossfact(G_eq, r_in, r_ext) - # Check sigma_eq calculation - @test sigma_eq ≈ 1 / rho_ins atol = TEST_TOL - # Conductance from sigma - G_from_sigma = 2 * π * sigma_eq / log(r_ext / r_in) - # Inverse check: G_eq from sigma_eq - @test G_from_sigma ≈ G_eq atol = TEST_TOL - - # calc_equivalent_lossfact - f = 50.0 - ω = 2 * π * f - tand_expected = G_eq / (ω * C_eq) - @test calc_equivalent_lossfact(G_eq, C_eq, ω) ≈ tand_expected atol = TEST_TOL - end - - @testset "Equivalent resistivity consistency" begin - r_in, r_ext = 0.01, 0.02 - rho = copper_props.rho - alpha = copper_props.alpha - T0 = copper_props.T0 - - R_tube = calc_tubular_resistance(r_in, r_ext, rho, alpha, T0, T0) - rho_eq = calc_equivalent_rho(R_tube, r_ext, r_in) - # Inverse check: rho from R_tube - @test rho_eq ≈ rho atol = TEST_TOL - - R_solid = calc_tubular_resistance(0.0, r_ext, rho, alpha, T0, T0) - rho_eq_solid = calc_equivalent_rho(R_solid, r_ext, 0.0) - # Inverse check: rho from R_solid - @test rho_eq_solid ≈ rho atol = TEST_TOL - end - - @testset "Solenoid correction consistency" begin - num_turns = 10.0 # turns/m - r_con_ext = 0.01 - r_ins_ext = 0.015 - - mu_r_corr = calc_solenoid_correction(num_turns, r_con_ext, r_ins_ext) - # Correction factor should be > 1 for non-zero turns - @test mu_r_corr > 1.0 - - # No twist (num_turns = NaN as per code comment) - # Correction factor is 1 if num_turns is NaN - @test calc_solenoid_correction(NaN, r_con_ext, r_ins_ext) ≈ 1.0 atol = TEST_TOL - # Zero turns - # Correction factor is 1 if num_turns is - @test calc_solenoid_correction(0.0, r_con_ext, r_ins_ext) ≈ 1.0 atol = TEST_TOL - - # Edge case: r_con_ext == r_ins_ext (zero thickness insulator) - # This leads to log(1) = 0 in denominator. Should return 1 or NaN/Inf? - # Let's test the behavior. Assuming it might result in NaN due to 0/0 or X/0. - # Correction factor is likely NaN if radii are equal (0/0 form) - # Or maybe it should default to 1? Depends on desired behavior. - # If the function should handle this, add a check inside it. - @test isnan(calc_solenoid_correction(num_turns, r_con_ext, r_con_ext)) - - end - - @testset "Basic uncertainty propagation" begin - using Measurements - r_in_m = (0.01 ± 0.001) - r_ext_m = (0.02 ± 0.001) - rho_m = (1.7241e-8 ± 0.001e-8) - R_m = calc_tubular_resistance( - r_in_m, - r_ext_m, - rho_m, - (0.0 ± 0.0), - (20.0 ± 0.0), - (20.0 ± 0.0), - ) - @test Measurements.value(R_m) ≈ calc_tubular_resistance( - Measurements.value(r_in_m), - Measurements.value(r_ext_m), - Measurements.value(rho_m), - (0.0), - (20.0), - (20.0), - ) atol = - TEST_TOL - @test Measurements.uncertainty(R_m) > 0 - end -end diff --git a/test/cable_test.json b/test/cable_test.json deleted file mode 100644 index 70f354dbb..000000000 --- a/test/cable_test.json +++ /dev/null @@ -1,310 +0,0 @@ -{ - "__julia_type__": "LineCableModels.DataModel.CablesLibrary", - "data": { - "test_cable": { - "cable_id": "test_cable", - "nominal_data": { - "U": 30, - "screen_cross_section": 35, - "capacitance": 0.39, - "__julia_type__": "LineCableModels.DataModel.NominalData", - "U0": 18, - "conductor_cross_section": 1000, - "armor_cross_section": null, - "resistance": 0.0291, - "inductance": 0.3, - "designation_code": "NA2XS(FL)2Y" - }, - "__julia_type__": "LineCableModels.DataModel.CableDesign", - "components": [ - { - "insulator_group": { - "__julia_type__": "LineCableModels.DataModel.InsulatorGroup", - "layers": [ - { - "material_props": { - "T0": 20, - "alpha": 0, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": 32.3, - "mu_r": 1, - "rho": 5300 - }, - "r_in": 0.02115, - "__julia_type__": "LineCableModels.DataModel.Semicon", - "r_ex": 0.02145, - "temperature": 20 - }, - { - "material_props": { - "T0": 20, - "alpha": 0, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": 1000, - "mu_r": 1, - "rho": 1000 - }, - "r_in": 0.02145, - "__julia_type__": "LineCableModels.DataModel.Semicon", - "r_ex": 0.02205, - "temperature": 20 - }, - { - "material_props": { - "T0": 20, - "alpha": 0, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": 2.3, - "mu_r": 1, - "rho": 197000000000000 - }, - "r_in": 0.02205, - "__julia_type__": "LineCableModels.DataModel.Insulator", - "r_ex": 0.03005, - "temperature": 20 - }, - { - "material_props": { - "T0": 20, - "alpha": 0, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": 1000, - "mu_r": 1, - "rho": 500 - }, - "r_in": 0.03005, - "__julia_type__": "LineCableModels.DataModel.Semicon", - "r_ex": 0.030350000000000002, - "temperature": 20 - }, - { - "material_props": { - "T0": 20, - "alpha": 0, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": 32.3, - "mu_r": 1, - "rho": 5300 - }, - "r_in": 0.030350000000000002, - "__julia_type__": "LineCableModels.DataModel.Semicon", - "r_ex": 0.030650000000000004, - "temperature": 20 - } - ] - }, - "__julia_type__": "LineCableModels.DataModel.CableComponent", - "id": "core", - "conductor_group": { - "__julia_type__": "LineCableModels.DataModel.ConductorGroup", - "layers": [ - { - "lay_ratio": 0, - "material_props": { - "T0": 20, - "alpha": 0.00429, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": 1, - "mu_r": 1.000022, - "rho": 2.8264e-8 - }, - "lay_direction": 1, - "r_in": 0, - "__julia_type__": "LineCableModels.DataModel.CircStrands", - "radius_wire": 0.00235, - "num_wires": 1, - "temperature": 20 - }, - { - "lay_ratio": 15, - "material_props": { - "T0": 20, - "alpha": 0.00429, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": 1, - "mu_r": 1.000022, - "rho": 2.8264e-8 - }, - "lay_direction": 1, - "r_in": 0.00235, - "__julia_type__": "LineCableModels.DataModel.CircStrands", - "radius_wire": 0.00235, - "num_wires": 6, - "temperature": 20 - }, - { - "lay_ratio": 13.5, - "material_props": { - "T0": 20, - "alpha": 0.00429, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": 1, - "mu_r": 1.000022, - "rho": 2.8264e-8 - }, - "lay_direction": 1, - "r_in": 0.007050000000000001, - "__julia_type__": "LineCableModels.DataModel.CircStrands", - "radius_wire": 0.00235, - "num_wires": 12, - "temperature": 20 - }, - { - "lay_ratio": 12.5, - "material_props": { - "T0": 20, - "alpha": 0.00429, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": 1, - "mu_r": 1.000022, - "rho": 2.8264e-8 - }, - "lay_direction": 1, - "r_in": 0.01175, - "__julia_type__": "LineCableModels.DataModel.CircStrands", - "radius_wire": 0.00235, - "num_wires": 18, - "temperature": 20 - }, - { - "lay_ratio": 11, - "material_props": { - "T0": 20, - "alpha": 0.00429, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": 1, - "mu_r": 1.000022, - "rho": 2.8264e-8 - }, - "lay_direction": 1, - "r_in": 0.01645, - "__julia_type__": "LineCableModels.DataModel.CircStrands", - "radius_wire": 0.00235, - "num_wires": 24, - "temperature": 20 - } - ] - } - }, - { - "insulator_group": { - "__julia_type__": "LineCableModels.DataModel.InsulatorGroup", - "layers": [ - { - "material_props": { - "T0": 20, - "alpha": 0, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": 32.3, - "mu_r": 1, - "rho": 5300 - }, - "r_in": 0.031700000000000006, - "__julia_type__": "LineCableModels.DataModel.Semicon", - "r_ex": 0.03200000000000001, - "temperature": 20 - } - ] - }, - "__julia_type__": "LineCableModels.DataModel.CableComponent", - "id": "sheath", - "conductor_group": { - "__julia_type__": "LineCableModels.DataModel.ConductorGroup", - "layers": [ - { - "lay_ratio": 10, - "material_props": { - "T0": 20, - "alpha": 0.00393, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": 1, - "mu_r": 0.999994, - "rho": 1.7241e-8 - }, - "lay_direction": 1, - "r_in": 0.030650000000000004, - "__julia_type__": "LineCableModels.DataModel.CircStrands", - "radius_wire": 0.000475, - "num_wires": 49, - "temperature": 20 - }, - { - "lay_ratio": 10, - "material_props": { - "T0": 20, - "alpha": 0.00393, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": 1, - "mu_r": 0.999994, - "rho": 1.7241e-8 - }, - "lay_direction": 1, - "r_in": 0.0316, - "__julia_type__": "LineCableModels.DataModel.Strip", - "width": 0.01, - "r_ex": 0.031700000000000006, - "temperature": 20 - } - ] - } - }, - { - "insulator_group": { - "__julia_type__": "LineCableModels.DataModel.InsulatorGroup", - "layers": [ - { - "material_props": { - "T0": 20, - "alpha": 0, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": 2.3, - "mu_r": 1, - "rho": 197000000000000 - }, - "r_in": 0.032150000000000005, - "__julia_type__": "LineCableModels.DataModel.Insulator", - "r_ex": 0.032200000000000006, - "temperature": 20 - }, - { - "material_props": { - "T0": 20, - "alpha": 0, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": 2.3, - "mu_r": 1, - "rho": 197000000000000 - }, - "r_in": 0.032200000000000006, - "__julia_type__": "LineCableModels.DataModel.Insulator", - "r_ex": 0.034600000000000006, - "temperature": 20 - } - ] - }, - "__julia_type__": "LineCableModels.DataModel.CableComponent", - "id": "jacket", - "conductor_group": { - "__julia_type__": "LineCableModels.DataModel.ConductorGroup", - "layers": [ - { - "material_props": { - "T0": 20, - "alpha": 0.00429, - "__julia_type__": "LineCableModels.Materials.Material", - "eps_r": 1, - "mu_r": 1.000022, - "rho": 2.8264e-8 - }, - "r_in": 0.03200000000000001, - "__julia_type__": "LineCableModels.DataModel.Tubular", - "r_ex": 0.032150000000000005, - "temperature": 20 - } - ] - } - } - ] - } - } -} \ No newline at end of file diff --git a/test/core/Project.toml b/test/core/Project.toml new file mode 100644 index 000000000..688e623c9 --- /dev/null +++ b/test/core/Project.toml @@ -0,0 +1,13 @@ +[deps] +LineCableModels = "dffb7669-8cd4-4628-99dd-7694e356ba37" +RequiredInterfaces = "97f35ef4-7bc5-4ec1-a41a-dcc69c7308c6" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" + +[sources] +LineCableModels = {path = "../.."} + +[compat] +RequiredInterfaces = "0.1.7" +TestItemRunner = "1.3.1" +julia = "1.12" diff --git a/test/core/clearance.jl b/test/core/clearance.jl new file mode 100644 index 000000000..c563a8f35 --- /dev/null +++ b/test/core/clearance.jl @@ -0,0 +1,16 @@ +@testitem "Core / clearance / sampling does not require Measurements" tags=[:core_only, :extension, :parametric] begin + using LineCableModels + using Random + @test Base.get_extension(LineCableModels, :LineCableModelsMeasurementsExt) === nothing + copper = Material(kind = :conductor, rho = 1.7e-8) + design = build(CableDesign, "core-clearance", Group(:core, + Region(:metal, Disk(0.01), copper))) + space = Gridspace{LineCableSystem}(spacing -> build(LineCableSystem, + [design, design], [Pose2(-spacing / 2, -1), Pose2(spacing / 2, -1)]), + (Grid(0.021, AbsoluteError(0.003)),)) + sampled = @test_logs (:warn, r"Sampled cable placements adjusted") rand( + MersenneTwister(4), space; distribution = (_rng, mean, sigma) -> mean - 3sigma) + @test sampled.positions[2].x - sampled.positions[1].x - 0.02 >= 1e-6 + @test sampled.clearances[1, 2] == 1e-6 + @test Base.get_extension(LineCableModels, :LineCableModelsMeasurementsExt) === nothing +end diff --git a/test/coverage.jl b/test/coverage.jl new file mode 100644 index 000000000..434fffab5 --- /dev/null +++ b/test/coverage.jl @@ -0,0 +1,104 @@ +module CoverageGate + +using Printf + +# Cleanup uses only the standard library and remains available if environment +# setup or tests fail. +if !(abspath(PROGRAM_FILE) == (@__FILE__) && ARGS == ["traces"]) + using CoverageTools +end + +const REPOSITORY_ROOT = normpath(joinpath(@__DIR__, "..")) +const SOURCE_DIRECTORIES = ("src", "ext") +const DEFAULT_MINIMUM = 0.95 + +function source_files() + files = String[] + for relative_directory in SOURCE_DIRECTORIES + directory = joinpath(REPOSITORY_ROOT, relative_directory) + for (root, _, names) in walkdir(directory) + append!(files, joinpath(root, name) for name in names if endswith(name, ".jl")) + end + end + return sort!(normpath.(files)) +end + +function clean_traces!(; report::Bool=true) + removed = String[] + for relative_directory in (SOURCE_DIRECTORIES..., "test", "docs") + directory = joinpath(REPOSITORY_ROOT, relative_directory) + for (root, _, names) in walkdir(directory) + for name in names + if endswith(name,".cov") + coverage_path = joinpath(root, name) + rm(coverage_path; force = true) + push!(removed, coverage_path) + end + end + end + end + path = joinpath(REPOSITORY_ROOT, "lcov.info") + if report && isfile(path) + rm(path) + push!(removed, path) + end + return removed +end + +function collect_coverage() + coverage = CoverageTools.FileCoverage[] + for relative_directory in SOURCE_DIRECTORIES + append!(coverage, CoverageTools.process_folder( + joinpath(REPOSITORY_ROOT, relative_directory), + )) + end + return coverage +end + +function assert_complete_inventory(coverage) + represented = Set(normpath(record.filename) for record in coverage) + missing = setdiff(Set(source_files()), represented) + isempty(missing) || error( + "Coverage report omitted production files:\n" * join(sort!(collect(missing)), "\n"), + ) + return nothing +end + +function check_coverage(; minimum = DEFAULT_MINIMUM, output = "lcov.info") + coverage = collect_coverage() + assert_complete_inventory(coverage) + covered, total = CoverageTools.get_summary(coverage) + ratio = total == 0 ? 0.0 : covered / total + @printf("Production line coverage: %d/%d (%.2f%%)\n", covered, total, 100ratio) + CoverageTools.LCOV.writefile(joinpath(REPOSITORY_ROOT, output), coverage) + ratio >= minimum || error( + @sprintf("Production line coverage %.2f%% is below the required %.2f%%", + 100ratio, + 100minimum,), + ) + return ratio +end + +function main(arguments) + command = isempty(arguments) ? "check" : only(arguments) + if command in ("clean","traces") + removed = clean_traces!(;report=command=="clean") + println("Removed $(length(removed)) coverage trace files.") + elseif command == "check" + minimum = parse(Float64, get(ENV, "LINECABLEMODELS_MIN_COVERAGE", "0.95")) + try + check_coverage(;minimum) + finally + clean_traces!(;report=false) + end + else + error("Unknown coverage command '$command'; expected 'clean', 'traces', or 'check'.") + end + return nothing +end + +end + +if abspath(PROGRAM_FILE) == @__FILE__ + CoverageGate.main(ARGS) +end diff --git a/test/coverage/Project.toml b/test/coverage/Project.toml new file mode 100644 index 000000000..00c031428 --- /dev/null +++ b/test/coverage/Project.toml @@ -0,0 +1,7 @@ +[deps] +CoverageTools = "c36e975a-824b-4404-a568-ef97ca766997" +Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" + +[compat] +CoverageTools = "1.4" +julia = "1.12" diff --git a/test/datamodel.jl b/test/datamodel.jl deleted file mode 100644 index 0f54371c6..000000000 --- a/test/datamodel.jl +++ /dev/null @@ -1,777 +0,0 @@ -@testsnippet deps_datamodel begin - using Plots - using EzXML - using Makie: Makie, Figure, Axis -end - -@testitem "DataModel module" setup = [defaults, deps_datamodel] begin - - println("\nSetting up materials and dimensions for DataModel test...") - materials = MaterialsLibrary(add_defaults = true) - @test haskey(materials, "aluminum") - @test haskey(materials, "copper") - @test haskey(materials, "polyacrylate") - @test haskey(materials, "semicon1") - @test haskey(materials, "semicon2") - @test haskey(materials, "pe") - - initial_default_count = length(materials) - @test initial_default_count > 5 # Should have several defaults - - materials_empty = MaterialsLibrary(add_defaults = false) - @test isempty(materials_empty) - - # Add a custom material for removal tests - mat_remove_test = Material(1e-5, 5.0, 1.0, 20.0, 0.05) - add!(materials, "remove_me", mat_remove_test) - @test length(materials) == initial_default_count + 1 - @test haskey(materials, "remove_me") - - println(" Testing delete!...") - delete!(materials, "remove_me") - @test !haskey(materials, "remove_me") - @test length(materials) == initial_default_count - - # Test removing non-existent (should throw KeyError based on source) - @test_throws KeyError delete!( - materials, - "does_not_exist", - ) - # Verify count didn't change - @test length(materials) == initial_default_count - - println(" Testing DataFrame...") - # Use the empty DB + one material for simpler checking - mat_list_test = Material(9e9, 9.0, 9.0, 99.0, 0.9) - add!(materials_empty, "list_test_mat", mat_list_test) - df_listed = DataFrame(materials_empty) - - @test df_listed isa DataFrame - @test names(df_listed) == ["name", "rho", "eps_r", "mu_r", "T0", "alpha"] # Check column names - @test nrow(df_listed) == 1 - @test df_listed[1, :name] == "list_test_mat" - @test df_listed[1, :rho] == 9e9 - @test df_listed[1, :eps_r] == 9.0 - @test df_listed[1, :mu_r] == 9.0 - @test df_listed[1, :T0] == 99.0 - @test df_listed[1, :alpha] == 0.9 - - println(" Testing save/load cycle for MaterialsLibrary...") - mktempdir(joinpath(@__DIR__)) do tmpdir - output_file = joinpath(tmpdir, "materials_library_test.json") - println(" Saving to: ", output_file) - - # Save the db that had defaults + 'remove_me' (before removal) - # Let's re-add it for a more comprehensive save file - db_to_save = MaterialsLibrary(add_defaults = true) - mat_temp = Material(1e-5, 5.0, 1.0, 20.0, 0.05) - add!(db_to_save, "temp_mat", mat_temp) - num_expected = length(db_to_save) - - save(db_to_save, file_name = output_file) - @test isfile(output_file) - @test filesize(output_file) > 0 - - # Load into a NEW, EMPTY library - materials_from_json = MaterialsLibrary(add_defaults = false) - load!(materials_from_json, file_name = output_file) - - # Verify loaded content - @test length(materials_from_json) == num_expected - @test haskey(materials_from_json, "temp_mat") - @test haskey(materials_from_json, "copper") # Check a default also loaded - loaded_temp_mat = get(materials_from_json, "temp_mat") - @test loaded_temp_mat.rho == mat_temp.rho - @test loaded_temp_mat.eps_r == mat_temp.eps_r - - println(" Save/load cycle completed.") - println("Materials Library tests completed.") - end # Temp dir cleanup - - # Cable dimensions from tutorial - num_co_wires = 61 - num_sc_wires = 49 - d_core = 38.1e-3 - d_w = 4.7e-3 - t_sc_in = 0.6e-3 - t_ins = 8e-3 - t_sc_out = 0.3e-3 - d_ws = 0.95e-3 - t_cut = 0.1e-3 - w_cut = 10e-3 - t_wbt = 0.3e-3 - t_sct = 0.3e-3 # Semiconductive tape thickness - t_alt = 0.15e-3 - t_pet = 0.05e-3 - t_jac = 2.4e-3 - - # Nominal data for final comparison - datasheet_info = NominalData( - designation_code = "NA2XS(FL)2Y", - U0 = 18.0, # Phase-to-ground voltage [kV] - U = 30.0, # Phase-to-phase voltage [kV] - conductor_cross_section = 1000.0, # [mm²] - screen_cross_section = 35.0, # [mm²] - resistance = 0.0291, # DC resistance [Ω/km] - capacitance = 0.39, # Capacitance [μF/km] - inductance = 0.3, # Inductance in trifoil [mH/km] - ) - @test datasheet_info.resistance > 0 - @test datasheet_info.capacitance > 0 - @test datasheet_info.inductance > 0 - - function calculate_rlc( - design::CableDesign; - rho_e::Float64 = 100.0, - default_S_factor::Float64 = 2.0, - ) - # Get components - core_comp = design.components[findfirst(c -> c.id == "core", design.components)] - sheath_comp = - design.components[findfirst(c -> c.id == "sheath", design.components)] - last_comp = design.components[end] # Usually jacket - - if isnothing(core_comp) || isnothing(sheath_comp) - error( - "Required 'core' or 'sheath' component not found in design for RLC calculation.", - ) - end - - # Resistance (from effective core conductor group resistance) - R = core_comp.conductor_group.resistance * 1e3 # Ω/m to Ω/km - - # Inductance (Trifoil) - # Use outermost radius for separation calculation - corrected access path - outermost_radius = last_comp.insulator_group.r_ex - S = default_S_factor * outermost_radius # Approx center-to-center distance [m] - - L = - calc_inductance_trifoil( - core_comp.conductor_group.r_in, - core_comp.conductor_group.r_ex, - core_comp.conductor_props.rho, core_comp.conductor_props.mu_r, - sheath_comp.conductor_group.r_in, - sheath_comp.conductor_group.r_ex, sheath_comp.conductor_props.rho, - sheath_comp.conductor_props.mu_r, - S, rho_e = rho_e, - ) * 1e6 # H/m to mH/km - - # Capacitance - C = - calc_shunt_capacitance( - core_comp.conductor_group.r_ex, - core_comp.insulator_group.r_ex, - core_comp.insulator_props.eps_r, - ) * 1e6 * 1e3 # F/m to μF/km - - return R, L, C - end - - println("Constructing core conductor group...") - material_alu = get(materials, "aluminum") - core = ConductorGroup(CircStrands(0.0, Diameter(d_w), 1, 0.0, material_alu)) - @test core isa ConductorGroup - @test length(core.layers) == 1 - @test core.r_in == 0 - @test core.r_ex ≈ d_w / 2.0 - @test core.resistance > 0 - @test core.gmr > 0 - - add!(core, CircStrands, Diameter(d_w), 6, 15.0, material_alu) - @test length(core.layers) == 2 - @test core.r_ex ≈ (d_w / 2.0) * 3 # Approximation for 1+6 wires - @test core.resistance > 0 # Resistance should decrease - - add!(core, CircStrands, Diameter(d_w), 12, 13.5, material_alu) - @test length(core.layers) == 3 - @test core.r_ex ≈ (d_w / 2.0) * 5 # Approximation for 1+6+12 wires - - add!(core, CircStrands, Diameter(d_w), 18, 12.5, material_alu) - @test length(core.layers) == 4 - @test core.r_ex ≈ (d_w / 2.0) * 7 # Approximation - - add!(core, CircStrands, Diameter(d_w), 24, 11.0, material_alu) - @test length(core.layers) == 5 - @test core.r_ex ≈ (d_w / 2.0) * 9 # Approximation - # Check final calculated radius against nominal diameter - # Note: constructor uses internal calculations, may differ slightly from d_core/2 - @test core.r_ex ≈ d_core / 2.0 rtol = 0.1 # Allow 10% tolerance for geometric approximation vs nominal - final_core_radius = core.r_ex # Store for later use - final_core_resistance = core.resistance # Store for later use - - println("Constructing main insulation group...") - # Inner semiconductive tape - material_sc_tape = get(materials, "polyacrylate") - main_insu = InsulatorGroup(Semicon(core, Thickness(t_sct), material_sc_tape)) - @test main_insu isa InsulatorGroup - @test length(main_insu.layers) == 1 - @test main_insu.r_in ≈ final_core_radius - @test main_insu.r_ex ≈ final_core_radius + t_sct - - # Inner semiconductor - material_sc1 = get(materials, "semicon1") - add!(main_insu, Semicon, Thickness(t_sc_in), material_sc1) - @test length(main_insu.layers) == 2 - @test main_insu.r_ex ≈ final_core_radius + t_sct + t_sc_in - - # Main insulation (XLPE) - material_pe = get(materials, "pe") - add!(main_insu, Insulator, Thickness(t_ins), material_pe) - @test length(main_insu.layers) == 3 - @test main_insu.r_ex ≈ final_core_radius + t_sct + t_sc_in + t_ins - - # Outer semiconductor - material_sc2 = get(materials, "semicon2") - add!(main_insu, Semicon, Thickness(t_sc_out), material_sc2) - @test length(main_insu.layers) == 4 - @test main_insu.r_ex ≈ final_core_radius + t_sct + t_sc_in + t_ins + t_sc_out - - # Outer semiconductive tape - add!(main_insu, Semicon, Thickness(t_sct), material_sc_tape) - @test length(main_insu.layers) == 5 - @test main_insu.r_ex ≈ - final_core_radius + t_sct + t_sc_in + t_ins + t_sc_out + t_sct - @test main_insu.shunt_capacitance > 0 - @test main_insu.shunt_conductance >= 0 - final_insu_radius = main_insu.r_ex # Store for later use - - println("Creating core cable component...") - core_cc = CableComponent("core", core, main_insu) - @test core_cc isa CableComponent - @test core_cc.id == "core" - @test core_cc.conductor_group === core - @test core_cc.insulator_group === main_insu - @test core_cc.conductor_props isa Material # Check effective props were created - @test core_cc.insulator_props isa Material - - println("Initializing CableDesign...") - cable_id = "tutorial2_test" - cable_design = CableDesign(cable_id, core_cc, nominal_data = datasheet_info) - @test cable_design isa CableDesign - @test length(cable_design.components) == 1 - @test cable_design.components[1] === core_cc - @test cable_design.nominal_data === datasheet_info - - println("Constructing sheath group...") - # Wire screens - lay_ratio_screen = 10.0 - material_cu = get(materials, "copper") - screen_con = ConductorGroup( - CircStrands( - main_insu, - Diameter(d_ws), - num_sc_wires, - lay_ratio_screen, - material_cu, - ), - ) - @test screen_con isa ConductorGroup - @test screen_con.r_in ≈ final_insu_radius - @test screen_con.r_ex ≈ final_insu_radius + d_ws # Approx radius of single layer of wires - - # Copper tape - add!( - screen_con, - Strip, - Thickness(t_cut), - w_cut, - lay_ratio_screen, - material_cu, - ) - @test screen_con.r_ex ≈ final_insu_radius + d_ws + t_cut - final_screen_con_radius = screen_con.r_ex - - # Water blocking tape - material_wbt = get(materials, "polyacrylate") # Assuming same as sc tape - screen_insu = InsulatorGroup(Semicon(screen_con, Thickness(t_wbt), material_wbt)) - @test screen_insu.r_ex ≈ final_screen_con_radius + t_wbt - final_screen_insu_radius = screen_insu.r_ex - - # Sheath Cable Component & Add to Design - sheath_cc = CableComponent("sheath", screen_con, screen_insu) - @test sheath_cc isa CableComponent - add!(cable_design, sheath_cc) - @test length(cable_design.components) == 2 - @test cable_design.components[2] === sheath_cc - - println("Constructing jacket group...") - # Aluminum foil - material_alu = get(materials, "aluminum") # Re-get just in case - jacket_con = ConductorGroup(Tubular(screen_insu, Thickness(t_alt), material_alu)) - @test jacket_con.r_ex ≈ final_screen_insu_radius + t_alt - final_jacket_con_radius = jacket_con.r_ex - - # PE layer after foil - material_pe = get(materials, "pe") # Re-get just in case - jacket_insu = InsulatorGroup(Insulator(jacket_con, Thickness(t_pet), material_pe)) - @test jacket_insu.r_ex ≈ final_jacket_con_radius + t_pet - - # PE jacket - add!(jacket_insu, Insulator, Thickness(t_jac), material_pe) - @test jacket_insu.r_ex ≈ final_jacket_con_radius + t_pet + t_jac - final_jacket_insu_radius = jacket_insu.r_ex - - # Add Jacket Component to Design (using alternative signature) - add!(cable_design, "jacket", jacket_con, jacket_insu) - @test length(cable_design.components) == 3 - @test cable_design.components[3].id == "jacket" - # Check overall radius - @test cable_design.components[3].insulator_group.r_ex ≈ - final_jacket_insu_radius - - println("Checking DataFrame...") - @test DataFrame(cable_design, :baseparams) isa DataFrame - @test DataFrame(cable_design, :components) isa DataFrame - @test DataFrame(cable_design, :detailed) isa DataFrame - - println("Validating calculated RLC against nominal values (rtol=6%)...") - - # Get components for calculation (assuming they are named consistently) - cable_core = - cable_design.components[findfirst(c -> c.id == "core", cable_design.components)] - cable_sheath = - cable_design.components[findfirst( - c -> c.id == "sheath", - cable_design.components, - )] # Note: Tutorial used 'cable_shield' variable name - cable_jacket = - cable_design.components[findfirst( - c -> c.id == "jacket", - cable_design.components, - )] - - @test cable_core !== nothing - @test cable_sheath !== nothing - @test cable_jacket !== nothing - - (R_orig, L_orig, C_orig) = calculate_rlc(cable_design) - println(" Original design RLC = ($R_orig, $L_orig, $C_orig)") - @test R_orig ≈ datasheet_info.resistance rtol = 0.06 - @test L_orig ≈ datasheet_info.inductance rtol = 0.06 - @test C_orig ≈ datasheet_info.capacitance rtol = 0.06 - - println("\nTesting CableDesign reconstruction...") - new_components = [] - for original_component in cable_design.components - println(" Reconstructing component: $(original_component.id)") - - # Extract effective properties and dimensions - eff_cond_props = original_component.conductor_props - eff_ins_props = original_component.insulator_props - r_in_cond = original_component.conductor_group.r_in - r_ext_cond = original_component.conductor_group.r_ex - r_in_ins = original_component.insulator_group.r_in - r_ext_ins = original_component.insulator_group.r_ex - - # Sanity check dimensions - @test r_ext_cond ≈ r_in_ins atol = 1e-9 # Inner radius of insulator must match outer of conductor - - # Create simplified Tubular conductor using effective properties - # Note: We must provide a material object, which are the effective props here - equiv_conductor = Tubular(r_in_cond, r_ext_cond, eff_cond_props) - # Wrap it in a ConductorGroup (which recalculates R, L based on the Tubular part) - equiv_cond_group = ConductorGroup(equiv_conductor) - - # Create simplified Insulator using effective properties - equiv_insulator = Insulator(r_in_ins, r_ext_ins, eff_ins_props) - # Wrap it in an InsulatorGroup (which recalculates C, G based on the Insulator part) - equiv_ins_group = InsulatorGroup(equiv_insulator) - - # Create the new, equivalent CableComponent - equiv_component = CableComponent( - original_component.id, - equiv_cond_group, - equiv_ins_group, - ) - - # Check if the recalculated R/L/C/G of the simple groups match the effective props closely. Note: This tests the self-consistency of the effective property calculations and the Tubular/Insulator constructors. Tolerance might need adjustment. - @test equiv_cond_group.resistance ≈ - calc_tubular_resistance( - r_in_cond, - r_ext_cond, - eff_cond_props.rho, - 0.0, - 20.0, - 20.0, - ) rtol = - 1e-6 - @test equiv_ins_group.shunt_capacitance ≈ - calc_shunt_capacitance(r_in_ins, r_ext_ins, eff_ins_props.eps_r) rtol = - 1e-6 - # GMR/Inductance and Conductance checks could also be added here - - push!(new_components, equiv_component) - end - - # Assemble the new CableDesign from the equivalent components - @test length(new_components) == length(cable_design.components) - equiv_cable_design = CableDesign( - cable_design.cable_id * "_equiv", - new_components[1], # Initialize with the first equivalent component - nominal_data = datasheet_info, # Keep same nominal data for reference - ) - - # Add remaining equivalent components - if length(new_components) > 1 - for i in eachindex(new_components)[2:end] - add!(equiv_cable_design, new_components[i]) - end - end - @test length(equiv_cable_design.components) == length(new_components) - println(" Equivalent cable design assembled.") - - println(" Calculating RLC for equivalent design...") - - (R_equiv, L_equiv, C_equiv) = calculate_rlc(equiv_cable_design) - - println(" Original R, L, C = ", R_orig, ", ", L_orig, ", ", C_orig) - println(" Equivalent R, L, C = ", R_equiv, ", ", L_equiv, ", ", C_equiv) - - # Use a tight tolerance because they *should* be mathematically equivalent if the model is self-consistent - rtol_equiv = 1e-6 - # Resistance mismatch in equivalent model? - @test R_equiv ≈ R_orig rtol = rtol_equiv - # Inductance mismatch in equivalent model? - @test L_equiv ≈ L_orig rtol = rtol_equiv - # Capacitance mismatch in equivalent model? - @test C_equiv ≈ C_orig rtol = rtol_equiv - - println(" Effective properties reconstruction test passed.") - - println("\nTesting CablesLibrary methods...") - library = CablesLibrary() - add!(library, cable_design) - - initial_count = length(library) - test_cable_id = cable_design.cable_id # Should be "tutorial2_test" - @test initial_count >= 1 - @test haskey(library, test_cable_id) - - println(" Testing delete!...") - delete!(library, test_cable_id) - @test !haskey(library, test_cable_id) - @test length(library) == initial_count - 1 - - # Test removing non-existent (should throw error) - @test_throws KeyError delete!(library, "non_existent_cable_id_123") - @test length(library) == initial_count - 1 # Count remains unchanged - - - println("\nTesting JSON Save/Load and RLC consistency...") - - add!(library, cable_design) - @test length(library) == initial_count # Should be back to original count - @test haskey(library, test_cable_id) - - mktempdir(joinpath(@__DIR__)) do tmpdir # Create a temporary directory for the test file - output_file = joinpath(tmpdir, "cables_library_test.json") - println(" Saving library to: ", output_file) - - # Test saving - @test isfile(save(library, file_name = output_file)) - @test filesize(output_file) > 0 # Check if file is not empty - - # Test loading into a new library - loaded_library = CablesLibrary() - load!(loaded_library, file_name = output_file) - @test length(loaded_library) == length(library) - - # Retrieve the reloaded design - reloaded_design = get(loaded_library, cable_design.cable_id) - println("Reloaded components:") - for comp in reloaded_design.components - println(" ID: ", repr(comp.id), " Type: ", typeof(comp)) # Use repr to see if ID is empty or weird - end - @test reloaded_design isa CableDesign - @test reloaded_design.cable_id == cable_design.cable_id - @test length(reloaded_design.components) == length(cable_design.components) - # Optionally, add more granular checks on reloaded components/layers if needed - - println(" Calculating RLC for reloaded design...") - (R_reload, L_reload, C_reload) = calculate_rlc(reloaded_design) - println(" Reloaded Design RLC = ($R_reload, $L_reload, $C_reload)") - println(" Original Design RLC = ($R_orig, $L_orig, $C_orig)") # Print original for comparison - - # Use a very tight tolerance - should be almost identical if serialization is good - rtol_serial = 1e-9 - # Resistance mismatch after JSON reload? - @test R_reload ≈ R_orig rtol = rtol_serial - # Inductance mismatch after JSON reload? - @test L_reload ≈ L_orig rtol = rtol_serial - # Capacitance mismatch after JSON reload? - @test C_reload ≈ C_orig rtol = rtol_serial - - println(" JSON save/load test passed.") - end # mktempdir ensures cleanup - - - println(" Setting up CableSystem...") - f_pscad = 10.0 .^ range(0, stop = 6, length = 10) # Frequency range - earth_params_pscad = EarthModel(f_pscad, 100.0, 10.0, 1.0) # 100 Ω·m, εr=10, μr=1 - - # Use outermost radius for trifoil calculation spacing - r = cable_design.components[end].insulator_group.r_ex - - s = 2r + 0.01 - - x0, y0 = 0.0, -1.0 # System center 1 m underground - xa, ya, xb, yb, xc, yc = trifoil_formation(x0, y0, s) - - d_ab = hypot(xa - xb, ya - yb) - d_bc = hypot(xb - xc, yb - yc) - d_ca = hypot(xc - xa, yc - ya) - @assert min(d_ab, d_bc, d_ca) ≥ 2r - - cable_system_id = "tutorial2_pscad_test" - cablepos = - CablePosition(cable_design, xa, ya, Dict("core" => 1, "sheath" => 0, "jacket" => 0)) - cable_system = - LineCableSystem(cable_system_id, 1000.0, cablepos) - add!( - cable_system, - cable_design, - xb, - yb, - Dict("core" => 2, "sheath" => 0, "jacket" => 0), - ) - add!( - cable_system, - cable_design, - xc, - yc, - Dict("core" => 3, "sheath" => 0, "jacket" => 0), - ) - @test cable_system.num_cables == 3 - @test cable_system.num_phases == 3 - - mktempdir(joinpath(@__DIR__)) do tmpdir - output_file = joinpath(tmpdir, "tutorial2_export_test.pscx") - println(" Exporting PSCAD file to: ", output_file) - - # Run export and use returned path (exporter may prefix basename with system_id) - result_path = - export_data(:pscad, cable_system, earth_params_pscad, file_name = output_file) - - # Basic file checks (use returned path) - @test result_path != nothing - @test isfile(result_path) - @test filesize(result_path) > 200 - - # Basic XML content checks - xml_content = read(result_path, String) - - @test occursin("", xml_content) # Check for closing root tag - - println(" Performing XML structure checks via XPath...") - local xml_doc - try - xml_doc = readxml(result_path) - catch parse_err - println("Failed to parse generated XML: $(parse_err)") - println("Skipping XPath validation due to parsing error.") - return # Exit testset early - end - - # 3. Check Root Element and Attributes - project_node = root(xml_doc) - @test nodename(project_node) == "project" - @test haskey(project_node, "name") - @test project_node["name"] == cable_system.system_id - # Check for expected version if needed - @test project_node["version"] == "5.0.2" - - # 4. Check Count of Cable Definitions - # Finds all 'User' components representing a coaxial cable definition - cable_coax_nodes = findall("//User[@name='master:Cable_Coax']", project_node) - @test length(cable_coax_nodes) == length(cable_system.cables) # Should be 3 - - # 5. Check Data within the First Cable Definition (CABNUM=1) - # Construct XPath to find the within the first Cable_Coax User component - # This is a bit complex: find User where name='master:Cable_Coax' AND which has a child param CABNUM=1 - xpath_cable1_params = "//User[@name='master:Cable_Coax'][paramlist/param[@name='CABNUM' and @value='1']]/paramlist" - params_cable1_node = findfirst(xpath_cable1_params, project_node) - @test !isnothing(params_cable1_node) - - if !isnothing(params_cable1_node) - # Helper to get a specific param value from the paramlist node - function get_param_value(paramlist_node, param_name) - p_node = findfirst("param[@name='$(param_name)']", paramlist_node) - return isnothing(p_node) ? nothing : p_node["value"] - end - - # Check component names exported - @test get_param_value(params_cable1_node, "CONNAM1") == "Core" # Matches cable_system.cables[1].design_data.components[1].id ? - @test get_param_value(params_cable1_node, "CONNAM2") == "Sheath" # Matches cable_system.cables[1].design_data.components[2].id ? - @test get_param_value(params_cable1_node, "CONNAM3") == "Jacket" # Matches cable_system.cables[1].design_data.components[3].id ? - - # Check X position - x_val_str = get_param_value(params_cable1_node, "X") - @test !isnothing(x_val_str) - if !isnothing(x_val_str) - parsed_x = parse(Float64, x_val_str) - expected_x = cable_system.cables[1].horz # Get horz from the first cable in the system - println( - " Checking first cable horz: XML='$(x_val_str)', Expected='$(expected_x)'", - ) - @test parsed_x ≈ expected_x rtol = 1e-6 - end - - # Check Y position (in PSCAD Y is oriented downwards) - y_val_str = get_param_value(params_cable1_node, "Y") - @test !isnothing(y_val_str) - if !isnothing(y_val_str) - parsed_y = abs(parse(Float64, y_val_str)) - expected_y = abs(cable_system.cables[1].vert) - println( - " Checking first cable vert: XML='$(y_val_str)', Expected='$(expected_y)' (May differ due to PSCAD coord system)", - ) - # Don't assert exact equality - @test isapprox(parsed_y, expected_y, rtol = 1e-4) - end - - - # Check an effective property, e.g., Core conductor effective resistivity (RHOC) - rhoc_val_str = get_param_value(params_cable1_node, "RHOC") - @test !isnothing(rhoc_val_str) - if !isnothing(rhoc_val_str) - parsed_rhoc = parse(Float64, rhoc_val_str) - # Get effective rho from the first component (core) of the first cable design - expected_rhoc = - cable_system.cables[1].design_data.components[1].conductor_props.rho - println( - " Checking first cable RHOC: XML='$(rhoc_val_str)', Expected='$(expected_rhoc)'", - ) - # Use a slightly looser tolerance for calculated effective properties - @test parsed_rhoc ≈ expected_rhoc rtol = 1e-4 - end - - # Check an effective dielectric property, e.g., Main insulation Epsilon_r (EPS1) - eps1_val_str = get_param_value(params_cable1_node, "EPS1") - @test !isnothing(eps1_val_str) - if !isnothing(eps1_val_str) - parsed_eps1 = parse(Float64, eps1_val_str) - # Get effective eps_r from the first component (core) insulator props - expected_eps1 = - cable_system.cables[1].design_data.components[1].insulator_props.eps_r - println( - " Checking first cable EPS1: XML='$(eps1_val_str)', Expected='$(expected_eps1)'", - ) - @test parsed_eps1 ≈ expected_eps1 rtol = 1e-4 - end - - end - - # 6. Check Ground Parameters (Example) - ground_params = - findfirst("//User[@name='master:Line_Ground']/paramlist", project_node) - @test !isnothing(ground_params) - - println(" XML structure checks via XPath passed.") - - println(" PSCAD export basic checks passed.") - end # mktempdir cleanup - - println("\nTesting plotting functions...") - - println(" Testing preview...") - # Reuse the fully constructed cable_design - fig, ax = preview( - cable_design, - display_plot = false, - display_legend = true, - ) - @test fig isa Makie.Figure - @test ax isa Makie.Axis - fig, ax = preview( - cable_design, - display_plot = false, - display_legend = false, - ) - @test fig isa Makie.Figure - @test ax isa Makie.Axis - - println(" Testing preview...") - # Reuse the fully constructed cable_system - fig, ax = preview(cable_system, zoom_factor = 0.5, display_plot = false) - @test fig isa Makie.Figure - @test ax isa Makie.Axis - - - println(" Plotting functions executed without errors.") - - println("\nTesting DataFrame generation...") - - # Reuse the fully constructed cable_design - println(" Testing DataFrame...") - df_core = DataFrame(cable_design, :baseparams) - @test df_core isa DataFrame - @test names(df_core) == ["parameter", "computed", "nominal", "percent_diff"] || - names(df_core) == [ - "parameter", - "computed", - "nominal", - "percent_diff", - "lower", - "upper", - "in_range?", - ] # Allow for uncertainty columns - @test nrow(df_core) == 3 - - df_comp = DataFrame(cable_design, :components) - @test df_comp isa DataFrame - # Expected columns: "property", "core", "sheath", "jacket" (based on tutorial build) - @test names(df_comp) == ["property", "core", "sheath", "jacket"] - @test nrow(df_comp) > 5 # Should have several properties - - df_detail = DataFrame(cable_design, :detailed) - @test df_detail isa DataFrame - @test "property" in names(df_detail) - # Check if columns were generated for layers, e.g., "core, cond. layer 1" - @test occursin("core, cond. layer 1", join(names(df_detail))) - @test occursin("jacket, ins. layer 1", join(names(df_detail))) - @test nrow(df_detail) > 10 # Should have many properties - - # Test invalid format - @test_throws ErrorException DataFrame(cable_design, :invalid_format) - - println(" Testing DataFrame...") - # Reuse the fully constructed cable_system - df_sys = DataFrame(cable_system) - @test df_sys isa DataFrame - @test names(df_sys) == ["cable_id", "horz", "vert", "phase_mapping"] - @test nrow(df_sys) == 3 # Because we added 3 cables - - println(" DataFrame functions executed successfully.") - - println("\nTesting Base.show methods...") - - # Reuse objects created earlier in the test file - # List of objects that have custom text/plain show methods in DataModel - # Add more as needed (e.g., specific part types if they have custom shows) - objects_to_show = [ - core, # ConductorGroup - main_insu, # InsulatorGroup - core_cc, # CableComponent - cable_design, # CableDesign - cable_system, # LineCableSystem - materials, - # Add an example of a basic part if desired and has a show method - Tubular(0.0, 0.01, get(materials, "aluminum")), - ] - - mime = MIME"text/plain"() - - for obj in objects_to_show - println(" Testing show for: $(typeof(obj))") - obj_repr = sprint(show, mime, obj) - @test obj_repr isa String - @test length(obj_repr) > 10 # Check that it produced some reasonable output - end - - println(" Custom show methods executed without errors.") - - println("\nDataModel test completed.") - -end diff --git a/test/earthprops.jl b/test/earthprops.jl deleted file mode 100644 index 0e2d49c81..000000000 --- a/test/earthprops.jl +++ /dev/null @@ -1,370 +0,0 @@ -@testsnippet defs_earthprops begin - # Access internal components for testing - const LCM = LineCableModels - const EP = LCM.EarthProps -end - -@testitem "EarthProps module" setup = [defaults, defs_earthprops] begin - - @testset "FDEM Formulations" begin - @testset "CPEarth" begin - cp_formulation = EP.CPEarth() - @test cp_formulation isa EP.AbstractFDEMFormulation - @test LCM.Commons.get_description(cp_formulation) == "Constant properties (CP)" - end - end - - @testset "CPEarth numerical tests" begin - frequencies = [50.0, 60.0, 1000.0] - base_rho_g = 100.0 - base_epsr_g = 10.0 - base_mur_g = 1.0 - formulation = EP.CPEarth() - - @testset "Float64 inputs" begin - rho, epsilon, mu = formulation(frequencies, base_rho_g, base_epsr_g, base_mur_g) - - @test length(rho) == length(frequencies) - @test all(r -> r == base_rho_g, rho) - - @test length(epsilon) == length(frequencies) - @test all(e -> isapprox(e, LCM.Commons.ε₀ * base_epsr_g), epsilon) - - @test length(mu) == length(frequencies) - @test all(m -> isapprox(m, LCM.Commons.μ₀ * base_mur_g), mu) - end - - @testset "Measurement inputs" begin - rho_m = 100.0 ± 5.0 - epsr_m = 10.0 ± 0.5 - mur_m = 1.0 ± 0.01 - - rho, epsilon, mu = formulation(frequencies, rho_m, epsr_m, mur_m) - - @test length(rho) == length(frequencies) - @test all(r -> r == rho_m, rho) - - @test length(epsilon) == length(frequencies) - @test all(e -> e.val ≈ (LCM.Commons.ε₀ * epsr_m).val, epsilon) - @test all(e -> e.err ≈ (LCM.Commons.ε₀ * epsr_m).err, epsilon) - - @test length(mu) == length(frequencies) - @test all(m -> m.val ≈ (LCM.Commons.μ₀ * mur_m).val, mu) - @test all(m -> m.err ≈ (LCM.Commons.μ₀ * mur_m).err, mu) - end - end - - @testset "EarthLayer Constructor" begin - frequencies = [50.0, 60.0] - base_rho_g = 100.0 - base_epsr_g = 10.0 - base_mur_g = 1.0 - t = 5.0 - formulation = EP.CPEarth() - - layer = EP.EarthLayer(frequencies, base_rho_g, base_epsr_g, base_mur_g, t, formulation) - - @test layer.base_rho_g == base_rho_g - @test layer.base_epsr_g == base_epsr_g - @test layer.base_mur_g == base_mur_g - @test layer.t == t - @test length(layer.rho_g) == length(frequencies) - @test all(layer.rho_g .== base_rho_g) - end - - @testset "EarthModel Constructor" begin - frequencies = [50.0, 60.0] - rho_g = 100.0 - epsr_g = 10.0 - mur_g = 1.0 - - @testset "Homogeneous Model" begin - model = EarthModel(frequencies, rho_g, epsr_g, mur_g) - @test length(model.layers) == 2 # Air + 1 earth layer - @test model.vertical_layers == false - @test model.freq_dependence isa EP.CPEarth - @test isinf(model.layers[1].t) # Air layer - @test isinf(model.layers[2].t) # Homogeneous earth - @test model.layers[2].base_rho_g == rho_g - end - - @testset "Finite Thickness Layer" begin - model = EarthModel(frequencies, rho_g, epsr_g, mur_g, t=20.0) - @test length(model.layers) == 2 - @test model.layers[2].t == 20.0 - end - - @testset "Vertical Layers" begin - model = EarthModel(frequencies, rho_g, epsr_g, mur_g, vertical_layers=true) - @test model.vertical_layers == true - end - - @testset "Input Validation" begin - @test_throws AssertionError EarthModel([-50.0], rho_g, epsr_g, mur_g) - @test_throws AssertionError EarthModel(frequencies, -100.0, epsr_g, mur_g) - @test_throws AssertionError EarthModel(frequencies, rho_g, -10.0, mur_g) - @test_throws AssertionError EarthModel(frequencies, rho_g, epsr_g, -1.0) - @test_throws AssertionError EarthModel(frequencies, rho_g, epsr_g, mur_g, t=-5.0) - end - end - - @testset "add! for EarthModel" begin - frequencies = [50.0, 60.0] - - @testset "Horizontal Layering" begin - model = EarthModel(frequencies, 100.0, 10.0, 1.0, t=20.0) - @test length(model.layers) == 2 - - add!(model, frequencies, 200.0, 15.0, 1.0, t=50.0) - @test length(model.layers) == 3 - @test model.layers[3].base_rho_g == 200.0 - @test model.layers[3].t == 50.0 - - add!(model, frequencies, 500.0, 20.0, 1.0, t=Inf) - @test length(model.layers) == 4 - @test isinf(model.layers[4].t) - - # Test invalid addition - @test_throws ErrorException add!(model, frequencies, 1000.0, 25.0, 1.0, t=Inf) - end - - @testset "Input Validation in add!" begin - model = EarthModel(frequencies, 100.0, 10.0, 1.0, t=20.0) - @test_throws AssertionError add!(model, [-50.0], 200.0, 15.0, 1.0) - @test_throws AssertionError add!(model, frequencies, -200.0, 15.0, 1.0) - @test_throws AssertionError add!(model, frequencies, 200.0, -15.0, 1.0) - @test_throws AssertionError add!(model, frequencies, 200.0, 15.0, -1.0) - @test_throws AssertionError add!(model, frequencies, 200.0, 15.0, 1.0, t=-5.0) - end - end - - @testset "Consecutive Infinite Layer Checks" begin - frequencies = [50.0] - @testset "Horizontal Model" begin - model = EarthModel(frequencies, 100.0, 10.0, 1.0, t=Inf) - # It's an error to add any layer after an infinite one in a horizontal model - @test_throws ErrorException add!(model, frequencies, 200.0, 15.0, 1.0, t=10.0) - @test_throws ErrorException add!(model, frequencies, 200.0, 15.0, 1.0, t=Inf) - end - - @testset "Vertical Model" begin - # Setup: model with two earth layers, the second being infinite - model = EarthModel(frequencies, 100.0, 10.0, 1.0, t=Inf, vertical_layers=true) - add!(model, frequencies, 150.0, 12.0, 1.0, t=20.0) # Add one more layer - add!(model, frequencies, 150.0, 12.0, 1.0, t=Inf) # Add one more layer, now Inf - - # It's an error to add another infinite layer - @test_throws ErrorException add!(model, frequencies, 200.0, 15.0, 1.0, t=Inf) - - # It should not be posssible to add a finite layer after an infinite one - @test_throws ErrorException add!(model, frequencies, 300.0, 20.0, 1.0, t=5.0) - @test length(model.layers) == 4 - @test model.layers[4].base_rho_g == 150.0 - @test isinf(model.layers[4].t) # The last layer should still be infinite - end - end - - @testset "show method for EarthModel" begin - frequencies = [50.0] - # Homogeneous model - model_homo = EarthModel(frequencies, 100.0, 10.0, 1.0) - s_homo = sprint(show, "text/plain", model_homo) - @test contains(s_homo, "EarthModel with 1 horizontal earth layer (homogeneous)") - @test contains(s_homo, "└─ Layer 2: [rho_g=100.0, epsr_g=10.0, mur_g=1.0, t=Inf]") - - # Multilayer horizontal model - model_multi_h = EarthModel(frequencies, 100.0, 10.0, 1.0, t=20.0) - add!(model_multi_h, frequencies, 200.0, 15.0, 1.0, t=Inf) - s_multi_h = sprint(show, "text/plain", model_multi_h) - @test contains(s_multi_h, "EarthModel with 2 horizontal earth layers (multilayer)") - @test contains(s_multi_h, "├─ Layer 2: [rho_g=100.0, epsr_g=10.0, mur_g=1.0, t=20.0]") - @test contains(s_multi_h, "└─ Layer 3: [rho_g=200.0, epsr_g=15.0, mur_g=1.0, t=Inf]") - - # Multilayer vertical model - model_multi_v = EarthModel(frequencies, 100.0, 10.0, 1.0, t=Inf, vertical_layers=true) - add!(model_multi_v, frequencies, 200.0, 15.0, 1.0, t=30.0) - s_multi_v = sprint(show, "text/plain", model_multi_v) - @test contains(s_multi_v, "EarthModel with 2 vertical earth layers (multilayer)") - @test contains(s_multi_v, "├─ Layer 2: [rho_g=100.0, epsr_g=10.0, mur_g=1.0, t=Inf]") - @test contains(s_multi_v, "└─ Layer 3: [rho_g=200.0, epsr_g=15.0, mur_g=1.0, t=30.0]") - end - - @testset "DataFrame for EarthModel" begin - frequencies = [50.0] - model = EarthModel(frequencies, 100.0, 10.0, 1.0, t=20.0) - add!(model, frequencies, 200.0, 15.0, 1.0, t=Inf) - - df = DataFrame(model) - @test df isa DataFrame - @test names(df) == ["rho_g", "epsr_g", "mur_g", "thickness"] - @test nrow(df) == 3 - - # Air layer - @test isinf(df.rho_g[1]) - @test df.epsr_g[1] == 1.0 - @test df.mur_g[1] == 1.0 - @test isinf(df.thickness[1]) - - # First earth layer - @test df.rho_g[2] == 100.0 - @test df.epsr_g[2] == 10.0 - @test df.thickness[2] == 20.0 - - # Second earth layer - @test df.rho_g[3] == 200.0 - @test df.epsr_g[3] == 15.0 - @test isinf(df.thickness[3]) - end - - @testset "show for EarthModel" begin - frequencies = [50.0, 60.0] - - @testset "Homogeneous Horizontal" begin - model = EarthModel(frequencies, 100.0, 10.0, 1.0) - str_repr = sprint(show, "text/plain", model) - @test occursin("EarthModel with 1 horizontal earth layer (homogeneous) and 2 frequency samples", str_repr) - @test occursin("└─ Layer 2:", str_repr) - @test occursin("t=Inf", str_repr) - @test occursin("Frequency-dependent model: Constant properties (CP)", str_repr) - end - - @testset "Multilayer Horizontal" begin - model = EarthModel(frequencies, 100.0, 10.0, 1.0, t=20.0) - add!(model, frequencies, 200.0, 15.0, 1.0, t=Inf) - str_repr = sprint(show, "text/plain", model) - @test occursin("EarthModel with 2 horizontal earth layers (multilayer) and 2 frequency samples", str_repr) - @test occursin("├─ Layer 2:", str_repr) - @test occursin("└─ Layer 3:", str_repr) - @test occursin("t=20", str_repr) - end - - @testset "Multilayer Vertical" begin - model = EarthModel(frequencies, 100.0, 10.0, 1.0, t=Inf, vertical_layers=true) - add!(model, frequencies, 200.0, 15.0, 1.0, t=5.0) - str_repr = sprint(show, "text/plain", model) - @test occursin("EarthModel with 2 vertical earth layers (multilayer) and 2 frequency samples", str_repr) - @test occursin("t=5", str_repr) - end - end - - cp = CPEarth() - - freqF = [1e3, 1e4, 1e5] # Vector{Float64} - freqM = measurement.(freqF, 0.1) # Vector{Measurement} - ρF, εF, μF = 100.0, 10.0, 1.0 - ρM, εM, μM = measurement.((ρF, εF, μF), 0.1) - - cases = ( - (freqF, ρF, εF, μF), - (freqM, ρM, εM, μM), - (freqF, ρM, εM, μM), # <- the one that used to blow up - (freqM, ρF, εM, μF), - (freqF, ρF, εM, μM), - (freqM, ρM, εF, μF), - ) - - promT(ρ, ε, μ) = promote_type(typeof(ρ), typeof(ε), typeof(μ)) - - @testset "CPEarth measurify promotion" begin - for (fr, ρ, ε, μ) in cases - ρv, εv, μv = cp(fr, ρ, ε, μ) - - # element types follow the COMMON promoted scalar type T - Texp = promT(ρ, ε, μ) - @test eltype(ρv) === Texp - @test eltype(εv) === Texp - @test eltype(μv) === Texp - - # values still match the right physics - @test all(≈(ρ), ρv) - @test all(≈(ε₀ * ε), εv) - @test all(≈(μ₀ * μ), μv) - end - end - - @testset "Mixed-type constructors & add!" begin - freqsF = [50.0, 60.0, 1000.0] # Vector{Float64} - freqsM = measurement.(freqsF, 0.1) # Vector{Measurement} - ρF, εF, μF = 100.0, 10.0, 1.0 - ρM, εM, μM = measurement.((ρF, εF, μF), 0.2) - - # --- EarthLayer: Float freqs + Measurement scalars (used to recurse) --- - @testset "EarthLayer: freqs Float64, scalars Measurement" begin - layer = EP.EarthLayer(freqsF, ρM, εM, μM, 5.0, EP.CPEarth()) - # Base params keep types (after macro coercion they must share one T) - Texp = promT(ρM, εM, μM) # Measurement{Float64} - @test layer.base_rho_g isa Texp - @test layer.base_epsr_g isa Texp - @test layer.base_mur_g isa Texp - # Frequency-dependent arrays adopt that same T - @test eltype(layer.rho_g) === Texp - @test length(layer.rho_g) == length(freqsF) - @test all(≈(ρM), layer.rho_g) - end - - # --- EarthModel: scalars Measurement, freqs Float64 (lift container) --- - @testset "EarthModel: freqs Float64, scalars Measurement" begin - model = EarthModel(freqsF, ρM, εM, μM; t=20.0) - @test model.freq_dependence isa EP.CPEarth - @test length(model.layers) == 2 - # Layer 2 (earth) carries Measurement T - earth = model.layers[2] - Texp = promT(ρM, εM, μM) - @test earth.base_rho_g isa Texp - @test eltype(earth.rho_g) === Texp - @test all(≈(ρM), earth.rho_g) - end - - # --- EarthModel: scalars Float64, freqs Measurement --- - @testset "EarthModel: freqs Measurement, scalars Float64" begin - model = EarthModel(freqsM, ρF, εF, μF; t=10.0) - @test length(model.layers) == 2 - earth = model.layers[2] - Texp = eltype(freqsM) - @test earth.base_rho_g isa Texp - @test eltype(earth.rho_g) === Texp - @test all(≈(ρF), earth.rho_g) - end - - # --- add!: Measurement model, add Float64 layers (promote to Measurement) --- - @testset "add!: model T=Measurement, add Float64" begin - modelM = EarthModel(freqsF, ρM, εM, μM; t=10.0) - @test eltype(modelM.layers) <: EP.EarthLayer # sanity - # Add deterministic layer; should be coerced to Measurement with zero-σ - add!(modelM, freqsF, 200.0, 15.0, 1.0; t=30.0) - bottom_layer = last(modelM.layers) - @test bottom_layer.base_rho_g isa typeof(ρM) - @test value(bottom_layer.base_rho_g) ≈ 200.0 - @test uncertainty(bottom_layer.base_rho_g) ≈ 0.0 - @test eltype(bottom_layer.rho_g) === typeof(ρM) - @test all(x -> value(x) ≈ 200.0 && uncertainty(x) ≈ 0.0, bottom_layer.rho_g) - end - - # --- add!: Float64 model, add Measurement layers --- - @testset "add!: model T=Float64, add Measurement" begin - modelF = EarthModel(freqsF, ρF, εF, μF; t=25.0) - modelF = add!(modelF, freqsF, ρM, εM, μM; t=12.0) - bottom_layer = last(modelF.layers) - @test bottom_layer.base_rho_g isa typeof(ρM) - @test bottom_layer.base_rho_g ≈ value(ρM) - @test eltype(bottom_layer.rho_g) === typeof(ρM) - @test all(≈(value(ρM)), bottom_layer.rho_g) - # model T=Float64, adding Measurement ⇒ MUST capture widened model - modelF = EarthModel(freqsF, ρF, εF, μF; t=25.0) - modelF = add!(modelF, freqsF, ρM, εM, μM; t=12.0) # <-- capture - @test first(typeof(modelF).parameters) <: Measurement - end - - # --- Keyword thickness as Measurement (forces T=Measurement) --- - @testset "EarthLayer: thickness Measurement" begin - tM = measurement(7.5, 0.3) - layer = EP.EarthLayer(freqsF, ρF, εF, μF, tM, EP.CPEarth()) - # If your method ties a single T across all args, this lifts everything to Measurement - @test isapprox(layer.t, tM) - @test layer.base_rho_g isa promote_type(typeof(ρF), typeof(tM)) - end - end - - @info "EarthProps tests completed." - -end diff --git a/test/extensions/backends.jl b/test/extensions/backends.jl new file mode 100644 index 000000000..d0c8467f9 --- /dev/null +++ b/test/extensions/backends.jl @@ -0,0 +1,150 @@ +@testitem "Extensions / backend dispatch / loaded extension activation" tags=[:extension, :makie, :slow] begin + import LineCableModels + using TOML + + packages=( + (:cairo, :CairoMakie, :LineCableModelsCairoMakieExt), + (:gl, :GLMakie, :LineCableModelsGLMakieExt), + (:wgl, :WGLMakie, :LineCableModelsWGLMakieExt) + ) + dependencies=get(TOML.parsefile(Base.active_project()), "deps", Dict{String, Any}()) + installed=filter(entry -> haskey(dependencies, String(entry[2])), packages) + if isempty(installed) + @test Base.get_extension(LineCableModels, :LineCableModelsMakieExt) === nothing + else + for (backend, package_name, extension_name) in installed + Core.eval(@__MODULE__, Expr(:using, Expr(:., package_name))) + extension=Base.get_extension(LineCableModels, extension_name) + makie_extension=Base.get_extension(LineCableModels, :LineCableModelsMakieExt) + @test extension !== nothing + @test makie_extension !== nothing + # Exercise the consumer method, not only the activation symbol. + # The GL-only CI environment does not install CairoMakie. + Base.invokelatest() do + @test extension.activate!() === backend + @test makie_extension.current_backend_symbol() === backend + parameters = LineCableModels.LineParameters( + reshape(ComplexF64[1+im, 2+im], 1, 1, 2), + reshape(ComplexF64[1+im, 2+im], 1, 1, 2) .* 1e-6, + [1.0, 10.0]) + page = LineCableModels.plot(parameters, (LineCableModels.R,); + backend, display_plot=false, open_export=false) + available = Base.get_extension(LineCableModels, :LineCableModelsCairoMakieExt) !== nothing + @test haskey(page.controls, :export_svg) == available + mktempdir() do directory + path = joinpath(directory, "export", "backend.svg") + if available + @test LineCableModels.export_svg(page; path, open_file=false) == path + @test occursin(" plot.figure isa Makie.Figure, plots) + + copper=Material(kind = :conductor, rho = 1.72e-8, eps_r = 1.0, mu_r = 1.0) + insulation_material=Material( + kind = :insulator, rho = Inf, eps_r = 2.3, mu_r = 1.0 + ) + silver=extension._conductor_color(1.72e-8) + bronze=extension._conductor_color(2.5e-7) + air=extension._dielectric_color(1.0) + amber=extension._dielectric_color(10.0) + @test silver.b > bronze.b + @test bronze.r > bronze.b + @test air.r > 0.95 + @test air.g > 0.95 + @test air.b > 0.95 + @test amber.r > amber.g > amber.b + + conductive_eps=Material( + kind = :conductor, rho = copper.rho, eps_r = 8.0, mu_r = 1.0 + ) + lossy_insulation=Material( + kind = :insulator, rho = 1.0e6, eps_r = 2.3, mu_r = 1.0 + ) + magnetic_copper=Material( + kind = :conductor, rho = copper.rho, eps_r = 1.0, mu_r = 300.0 + ) + earth=layer(rho = 100.0, eps_r = 4.0, mu_r = 1.0) + different_earth_eps=layer(rho = 100.0, eps_r = 20.0, mu_r = 1.0) + @test extension._material_color(copper) == + extension._material_color(conductive_eps) + @test extension._material_color(insulation_material) == + extension._material_color(lossy_insulation) + @test extension._material_color(copper) != + extension._material_color(magnetic_copper) + @test extension._material_color(earth) == + extension._material_color(different_earth_eps) + strand_radius=0.5e-3 + core_radius=sqrt(7)*strand_radius + core=stranded( + copper; + shape = Disk(strand_radius), + lay = nothing, + compact = true, + boundary = Disk(core_radius) + ) + design=build( + CableDesign, + "outlined-compacted-strands", + terminal(:phase, core), + insulation(insulation_material; t = 1e-3) + ) + polygons=extension._native_design_shapes( + design, 0.0, 0.0; display_legend = false + ) + @test length(polygons) == 8 + inner_boundary=extension.RGB(102 / 255, 109 / 255, 118 / 255) + @test all(polygon -> polygon.stroke == inner_boundary, polygons[1:7]) + @test all(polygon -> polygon.width == 0.5, polygons[1:7]) + @test polygons[8].stroke == inner_boundary + @test polygons[8].width == 0.45 + + enclosed_design=build( + CableDesign, + "outlined-enclosure", + pipe( + terminal(:phase, LineCableModels.core(copper; r = 0.5e-3)); + shape = Disk(2.0e-3), + fill = insulation_material, + wall = insulation(insulation_material; t = 0.5e-3) + ) + ) + enclosed_polygons=extension._native_design_shapes( + enclosed_design, 0.0, 0.0; display_legend = false + ) + @test length(enclosed_polygons) == 3 + @test all(polygon -> polygon.stroke === :black, enclosed_polygons[2:3]) + @test all(polygon -> polygon.width == 0.8, enclosed_polygons[2:3]) + + @test_throws ArgumentError Makie.plot( + parameters; backend = :gl, display_plot = false + ) +end diff --git a/test/extensions/bessel_sensitivities.jl b/test/extensions/bessel_sensitivities.jl new file mode 100644 index 000000000..2047d572d --- /dev/null +++ b/test/extensions/bessel_sensitivities.jl @@ -0,0 +1,54 @@ +@testitem "Measurements / Bessel derivatives at small complex arguments and scaling branches" tags=[:extension, :measurements] begin + using Measurements, SpecialFunctions + for magnitude in (1e-8,1e-6,1e-4) + x=measurement(magnitude,magnitude/100) + y=measurement(magnitude,magnitude/100) + z=complex(x,y) + expected=-besselk(1,complex(magnitude,magnitude)) + result=besselk(0,z) + @test complex(Measurements.derivative(real(result),x), + Measurements.derivative(imag(result),x)) ≈ expected rtol=1e-12 + @test complex(Measurements.derivative(real(result),y), + Measurements.derivative(imag(result),y)) ≈ im*expected rtol=1e-12 + # Stable small-argument series also check the regular and scaled I/J + # slopes, which Float64 output subtraction cannot resolve reliably. + z0=complex(magnitude,magnitude) + for (kernel,constant,slope,scale,scale_x,scale_y) in ( + (besseli,1+z0^2/4+z0^4/64,z0/2+z0^3/16+z0^5/384,1.,0.,0.), + (besselj,1-z0^2/4+z0^4/64,-z0/2+z0^3/16-z0^5/384,1.,0.,0.), + (besselix,1+z0^2/4+z0^4/64,z0/2+z0^3/16+z0^5/384,exp(-magnitude),-1.,0.), + (besseljx,1-z0^2/4+z0^4/64,-z0/2+z0^3/16-z0^5/384,exp(-magnitude),0.,-1.)) + output=kernel(0,z) + for (input,derivative) in ((x,scale*(slope+scale_x*constant)), + (y,scale*(im*slope+scale_y*constant))) + actual=complex(Measurements.derivative(real(output),input), + Measurements.derivative(imag(output),input)) + @test actual ≈ derivative rtol=1e-12 + end + end + end + # Rebuilt relative-step Cartesian differences are independent of the adapter's + # recurrence implementation. Exercise both signs of each scaling coordinate. + kernels=(besseli,besselk,besselj,bessely,besselh, + besselix,besselkx,besseljx,besselyx,besselhx) + # At tiny arguments, regular I/J values are nearly constant and subtracting + # two Float64 outputs cannot resolve their slopes. Use the exact K0 test + # above for that regime. Rebuilt differences below are well-scaled controls. + for magnitude in (1.,20.), sx in (-1.,1.), sy in (-1.,1.), + order in (0.,0.5,1.,2.), kernel in kernels + nominal_z=magnitude*complex(1.25sx,0.75sy) + q=measurement(0.,1e-4*magnitude) + direction=1+2im + z=complex(real(nominal_z)+q,imag(nominal_z)+2q) + actual=kernel(order,z) + h=1e-5*magnitude + expected=(kernel(order,nominal_z+h*direction)- + kernel(order,nominal_z-h*direction))/(2h) + slope=complex(Measurements.derivative(real(actual),q), + Measurements.derivative(imag(actual),q)) + @test slope ≈ expected rtol=2e-6 atol=1e-11*max(1.,abs(expected)) + @test Measurements.value(actual) ≈ kernel(order,nominal_z) rtol=1e-12 + @test Measurements.cov(real(actual),imag(actual)) ≈ + real(slope)*imag(slope)*Measurements.uncertainty(q)^2 rtol=1e-12 + end +end diff --git a/test/extensions/distributions.jl b/test/extensions/distributions.jl new file mode 100644 index 000000000..cc65c8edb --- /dev/null +++ b/test/extensions/distributions.jl @@ -0,0 +1,76 @@ +@testitem "Extensions / Distributions / unloaded sampler fails explicitly" tags=[ + :extension, + :core_only, + :commons +] begin + using Random + import LineCableModels + + @test Base.get_extension(LineCableModels, :LineCableModelsDistributionsExt) === nothing + PB=LineCableModels.ParametricBuilder + uncertain=only(PB.Grid(10.0, PB.AbsoluteError(1.0))) + @test_throws ArgumentError rand( + MersenneTwister(1), + uncertain; + distribution = (family = :unsupported,) + ) +end + +@testitem "Extensions / Distributions adapter / normalized sampling and density laws" tags=[ + :extension, + :distributions +] begin + using Distributions + using Random + using Statistics + import LineCableModels + + extension_module=Base.get_extension( + LineCableModels, + :LineCableModelsDistributionsExt + ) + @test extension_module !== nothing + + PB=LineCableModels.ParametricBuilder + @test Base.ispublic(LineCableModels, :sample_uncertainty) + @test parentmodule(LineCableModels.sample_uncertainty) === LineCableModels + @test any( + method -> method.module === extension_module, + methods(LineCableModels.sample_uncertainty) + ) + uncertain=only(PB.Grid(10.0, PB.AbsoluteError(2.0))) + standard_draw=rand(MersenneTwister(41), uncertain; distribution = Normal(0, 1)) + shifted_draw=rand(MersenneTwister(41), uncertain; distribution = Normal(10, 3)) + @test standard_draw == shifted_draw + @test isfinite(standard_draw) + for law in (Normal(10.,3.),Uniform(-2.,7.)) + restored=LineCableModels.ImportExport.deserialize_value(LineCableModels.ImportExport.serialize_value(law)) + @test typeof(restored)===typeof(law) + @test params(restored)==params(law) + @test rand(MersenneTwister(41),uncertain;distribution=restored)== + rand(MersenneTwister(41),uncertain;distribution=law) + end + + density=LineCableModels.UQ.HistogramDensity( + [1.0, 3.0, 5.0], + [0.25, 0.25] + ) + @test pdf(density, 1.0) == 0.25 + @test pdf(density, 5.0) == 0.25 + @test pdf(density, 6.0) == 0.0 + @test logpdf(density, 6.0) == -Inf + @test cdf(density, 2.0) == 0.25 + @test cdf(density, 4.0) == 0.75 + @test quantile(density, 0.0) == 1.0 + @test quantile(density, 1.0) == 5.0 + @test insupport(density, 3.0) + @test !insupport(density, 6.0) + @test moment(density, 0) == 1.0 + @test mode(density) == 2.0 + @test modes(density) == [2.0, 4.0] + @test isapprox(mean(density), 3.0) + @test var(density) > 0 + @test first(density.edges) <= rand(MersenneTwister(9), density) <= + last(density.edges) + @test_throws ArgumentError extension_module._raw_moment(density, -1) +end diff --git a/test/extensions/fem_armour_contacts.jl b/test/extensions/fem_armour_contacts.jl new file mode 100644 index 000000000..7548aa446 --- /dev/null +++ b/test/extensions/fem_armour_contacts.jl @@ -0,0 +1,115 @@ +@testitem "Gmsh FEM / touching armour wires retain every filler face" tags=[:extension, :fem, :slow] begin + using Gmsh + const FEM = Base.get_extension(LineCableModels, :LineCableModelsGmshExt) + const gmsh = Gmsh.gmsh + copper = Material(kind=:conductor, rho=1.72e-8) + bedding = Material(kind=:insulator, rho=Inf, eps_r=2.3) + matrix = Material(kind=:insulator, rho=Inf, eps_r=1.0) + session = FEM._start_gmsh(0) + try + for count in (6, 8, 68), pose in (Pose2(0.0,-0.1), Pose2(-0.5,-1.0,0.37)) + wire_radius = 0.00291 + radius = wire_radius / sinpi(1/count) + inner, outer = radius-wire_radius, radius+wire_radius + ring = Group(:armour, Region(:wire, Disk(wire_radius), copper); + pattern=Ring(count; r=radius)) + design = build(CableDesign, "touching-armour", + Region(:bedding, Disk(inner), bedding), + Enclosure(:matrix, ring; primitive=Annulus(inner,outer), fill=matrix), + Region(:jacket, Shell(0.002), bedding)) + system = build(LineCableSystem, design, pose; connections=Dict(:armour=>1)) + problem = LineParametersProblem(system; frequencies=[0.1,50.0], + earth_props=homogeneous(rho=100.0)) + model = FEM._resolved_fem_model(problem, LineCableModelsFEM()) + fill_index = only(findall(m -> m.field === :matrix_fill, model.material_plans)) + geometry = FEM._build_geometry!(model, "touching-armour-$count") + surfaces = geometry.material_surfaces[fill_index] + # Every pair of neighboring wires encloses an inner and an outer + # filler lobe. A point contact cannot join them into one CAD face. + @test length(surfaces) == 2count + for surface in surfaces + incidence = Dict{Int,Int}() + for (_,curve) in gmsh.model.get_boundary([(2,surface)], false, false, false) + for (_,point) in gmsh.model.get_boundary([(1,curve)], false, false, false) + incidence[point] = get(incidence,point,0)+1 + end + end + @test all(==(2), values(incidence)) + end + for plan in model.mesh_plans + FEM._update_exterior_mesh!(model, geometry, plan) + FEM._configure_mesh!(model, geometry, plan) + gmsh.model.mesh.generate(2) + @test isnothing(FEM._inspect_loaded_mesh(model, "touching-armour")) + mktempdir() do directory + mesh = joinpath(directory,"armour.msh") + gmsh.write(mesh) + @test validate(mesh,model) === mesh + end + end + end + finally + FEM._finish_gmsh(session) + end +end + +@testitem "Gmsh FEM / partial and separated armour rings preserve physical gaps" tags=[:extension, :fem] begin + using Gmsh + const FEM = Base.get_extension(LineCableModels, :LineCableModelsGmshExt) + const gmsh = Gmsh.gmsh + copper = Material(kind=:conductor, rho=1.72e-8) + bedding = Material(kind=:insulator, rho=Inf, eps_r=2.3) + matrix = Material(kind=:insulator, rho=Inf, eps_r=1.0) + wire_radius = 0.00291 + session = FEM._start_gmsh(0) + try + for (count,span,gap) in ((7,6pi/4,0.0), (2,pi/4,0.0), + (8,2pi,1e-6), (1,2pi,0.0)), padding in (0.0,0.001) + radius = (wire_radius+gap/2)/sinpi(1/8) + inner,outer = radius-wire_radius,radius+wire_radius+padding + ring = Group(:armour,Region(:wire,Disk(wire_radius),copper); + pattern=Ring(count;r=radius,span,φ0=0.23)) + design = build(CableDesign,"partial-armour", + Region(:bedding,Disk(inner),bedding), + Enclosure(:matrix,ring;primitive=Annulus(inner,outer),fill=matrix), + Region(:jacket,Shell(0.002),bedding)) + system = build(LineCableSystem,design,Pose2(0.0,-0.1);connections=Dict(:armour=>1)) + problem = LineParametersProblem(system;frequencies=[50.0], + earth_props=homogeneous(rho=100.0)) + model = FEM._resolved_fem_model(problem,LineCableModelsFEM()) + fill_index = only(findall(m -> m.field === :matrix_fill,model.material_plans)) + geometry = FEM._build_geometry!(model,"partial-armour") + expected = (count == 1 || gap > 0 ? count : 2count-1) + (padding > 0) + @test length(geometry.material_surfaces[fill_index]) == expected + @test sum(r -> area(r.shape),filter(r -> r.material_index==fill_index, + model.region_plans)) ≈ pi*(outer^2-inner^2)-count*pi*wire_radius^2 + FEM._configure_mesh!(model,geometry,only(model.mesh_plans)) + gmsh.model.mesh.generate(2) + @test isnothing(FEM._inspect_loaded_mesh(model,"partial-armour")) + end + finally + FEM._finish_gmsh(session) + end +end + +@testitem "Gmsh FEM / touching armour filler reaches GetDP" tags=[:extension,:integration,:fem_numerical,:fem,:slow] begin + using Gmsh + copper = Material(kind=:conductor,rho=1.72e-8) + dielectric = Material(kind=:insulator,rho=Inf,eps_r=2.3) + matrix = Material(kind=:insulator,rho=Inf,eps_r=1.0) + count,wire_radius = 8,0.00291 + radius = wire_radius/sinpi(1/count) + ring = Group(:armour,Region(:wire,Disk(wire_radius),copper); + pattern=Ring(count;r=radius)) + design = build(CableDesign,"armour-contact-solve", + Region(:bedding,Disk(radius-wire_radius),dielectric), + Enclosure(:matrix,ring;primitive=Annulus(radius-wire_radius,radius+wire_radius),fill=matrix), + Region(:jacket,Shell(0.002),dielectric)) + system = build(LineCableSystem,design,Pose2(0.0,-0.1);connections=Dict(:armour=>1)) + problem = LineParametersProblem(system;frequencies=[0.1,50.0],earth_props=homogeneous(rho=100.0)) + result = compute(problem,LineCableModelsFEM();options=(gmsh_verbosity=0,getdp_verbosity=0)) + @test result.f == problem.frequencies + @test all(isfinite,result.Z) + @test all(isfinite,result.Y) + @test all(real(z)>0 for z in result.Z) +end diff --git a/test/extensions/fem_artifact.jl b/test/extensions/fem_artifact.jl new file mode 100644 index 000000000..f1ed5fdd9 --- /dev/null +++ b/test/extensions/fem_artifact.jl @@ -0,0 +1,34 @@ +@testitem "Gmsh FEM / GetDP installs and executes from an empty artifact cache" tags=[:extension,:fem_numerical,:fem,:slow] begin + # Resolve the real package artifact in a child process whose artifact depot + # starts empty. Existing Julia packages load before the depot is replaced. + for mode in ("yes","no") + mktempdir() do directory + cache=joinpath(directory,"depot") + mkpath(cache) + script=joinpath(directory,"cold_getdp.jl") + write(script,raw""" + using Pkg + using LineCableModels, Gmsh + extension=Base.get_extension(LineCableModels,:LineCableModelsGmshExt) + cache=only(ARGS) + @assert isempty(readdir(cache)) + empty!(DEPOT_PATH) + push!(DEPOT_PATH,cache) + selected=withenv("LINECABLEMODELS_GETDP"=>nothing) do + extension._getdp_selection(computation_options(LineCableModelsFEM, ComputationOptions((;)))) + end + @assert selected.source === :artifact + @assert startswith(selected.path,joinpath(cache,"artifacts")) + @assert isfile(selected.path) + identity=extension._getdp_identity(selected.path) + @assert occursin(r"Version\s*:\s*3\.5\.0",identity.info) + @assert occursin("complex arithmetic",identity.info) + println("COLD_GETDP_OK ",selected.artifact_hash) + """) + command=`$(Base.julia_cmd()) --project=$(dirname(Base.active_project())) --startup-file=no --compiled-modules=$mode $script $cache` + output=read(command,String) + @test occursin("COLD_GETDP_OK",output) + @test isdir(joinpath(cache,"artifacts")) + end + end +end diff --git a/test/extensions/fem_cleanup.jl b/test/extensions/fem_cleanup.jl new file mode 100644 index 000000000..074b22712 --- /dev/null +++ b/test/extensions/fem_cleanup.jl @@ -0,0 +1,92 @@ +@testmodule GmshCleanupFailures begin + const FEMGmshSession = NamedTuple + const FEMResolvedModel = NamedTuple + struct LineCableModelsFEMError <: Exception end + const primary = LineCableModelsFEMError() + const calls = Any[] + const fail_inspection = Ref(false) + const gmsh = ( + view=(get_tags=() -> [1, 2], remove=tag -> begin + push!(calls, (:remove_view, tag)) + error("view removal failed") + end), + model=(list=() -> ["caller", "temporary"], get_current=() -> "caller", + add=name -> push!(calls, (:add_model, name)), + set_current=name -> begin + push!(calls, (:set_current, name)) + name == "caller" && error("model restoration failed") + end, + remove=() -> begin + push!(calls, :remove_model) + error("model removal failed") + end), + option=(set_number=(name, value) -> push!(calls, (name, value)),), + merge=path -> push!(calls, (:merge, path))) + _restore_onelab(snapshot) = push!(calls, (:onelab, snapshot)) + function _inspect_loaded_mesh(model, path) + push!(calls, :inspect) + fail_inspection[] && throw(primary) + end + + # Load the actual cleanup methods against a local Gmsh API fixture. + for (file, name) in (("compute.jl", :_finish_gmsh), ("mesh.jl", :validate)) + Base.include(@__MODULE__, joinpath(@__DIR__, "../../ext/LineCableModelsGmshExt", file)) do expression + expression isa Expr && expression.head === :function && + expression.args[1] isa Expr && expression.args[1].head === :call && + expression.args[1].args[1] === name ? expression : nothing + end + end +end + +@testitem "Gmsh FEM / cleanup reports failures and preserves the primary exception" tags=[:extension, :fem] default_imports=false setup=[GmshCleanupFailures] begin + using Test, Logging + const G = GmshCleanupFailures + session = (owned=false, previous_model="caller", initial_models=Set(["caller"]), + initial_views=Set([1]), terminal_option=1.0, verbosity_option=2.0, + geometry_tolerance=1e-7, boolean_tolerance=2e-7, onelab=:snapshot) + logger = Test.TestLogger(min_level=Logging.Warn) + empty!(G.calls) + caught = try + try + throw(G.primary) + finally + with_logger(logger) do + G._finish_gmsh(session) + end + end + catch exception + exception + end + @test caught === G.primary + @test G.calls == [(:remove_view, 2), (:set_current, "temporary"), :remove_model, + (:set_current, "caller"), ("General.Terminal", 1.0), ("General.Verbosity", 2.0), + ("Geometry.Tolerance", 1e-7), ("Geometry.ToleranceBoolean", 2e-7), (:onelab, :snapshot)] + @test length(logger.logs) == 3 + @test logger.logs[1].kwargs[:view] == 2 + @test logger.logs[2].kwargs[:model] == "temporary" + @test logger.logs[3].kwargs[:model] == "caller" + @test all(record -> first(record.kwargs[:exception]) isa ErrorException, logger.logs) + + mktemp() do path, io + close(io) + for fails in (false, true) + G.fail_inspection[] = fails + empty!(G.calls) + logger = Test.TestLogger(min_level=Logging.Warn) + result = with_logger(logger) do + try + G.validate(path, (;)) + catch exception + exception + end + end + @test result === (fails ? G.primary : path) + @test G.calls[end-1:end] == [:remove_model, (:set_current, "caller")] + @test length(logger.logs) == 2 + @test startswith(logger.logs[1].kwargs[:model], "LineCableModelsFEM-validation-") + @test logger.logs[2].kwargs[:model] == "caller" + @test all(record -> record.kwargs[:mesh] == path, logger.logs) + @test all(record -> first(record.kwargs[:exception]) isa ErrorException, logger.logs) + end + end +end diff --git a/test/extensions/fem_constitutive.jl b/test/extensions/fem_constitutive.jl new file mode 100644 index 000000000..5273c9265 --- /dev/null +++ b/test/extensions/fem_constitutive.jl @@ -0,0 +1,169 @@ +@testitem "Gmsh FEM / constitutive selection, evaluated state and rejection" tags=[:extension, :fem] setup=[FormulaFixtures] begin + using Gmsh, Measurements + const FEM = Base.get_extension(LineCableModels, :LineCableModelsGmshExt) + const FD = LineCableModels.Earth.FrequencyDependent + const TD = LineCableModels.Materials.TemperatureDependent + copper = Material(:conductor, 1.72e-8, 1, 1, 20, 0.004) + dielectric = Material(:insulator, 1e8, 2.3, 1, 20, -0.003; tan_delta=0.025) + design = build(CableDesign, "constitutive", terminal(:core, + core(copper; r=0.005), insulation(dielectric; t=0.005))) + system = build(LineCableSystem, design, (0.0,-0.1); connections=Dict(:core=>1)) + problem = LineParametersProblem(system; temperature=80.0, frequencies=[50.0,1000.0], + earth_props=homogeneous(rho=100.0, eps_r=10.0)) + soil_law=FormulaFixtures.DispersiveSoil() + temperature_law=FormulaFixtures.ExponentialResistivity() + dielectric_law=FormulaFixtures.OhmicDielectric() + formulation=LineCableModelsFEM(earth_properties=soil_law, + temperature_dependence=temperature_law,insulation_admittance=dielectric_law) + model = FEM._resolved_fem_model(problem, formulation) + @test soil_law.seen == problem.frequencies + @test length(dielectric_law.temperatures)==2 + @test all(pair->pair[1]==pair[2],dielectric_law.temperatures) + @test keys(formulation.methods) == keys(formulation.definitions) == + (:insulation_admittance, :semicon_admittance, :earth_properties, :temperature_dependence) + metal, passive = model.material_plans + @test real.(metal.admittivity) ≈ fill(inv(copper.rho*exp(0.06)), 2) + @test real.(passive.admittivity) ≈ fill(inv(dielectric.rho*exp(0.06)), 2) + @test problem.system.designs[1].geometry.regions[1].source.material.rho == copper.rho + # This cable's envelope is below the 5 m layout floor. + for (index,f) in pairs(problem.frequencies) + earth = model.earth_materials[index] + @test (earth.rho,earth.eps_r,earth.mu_r) == (100/(1+f/1000),10*(1+f/2000),1+f/10000) + @test model.mesh_plans[index].domain_radius ≈ max(5.0, + 2sqrt(earth.rho/(pi*f*earth.mu_r*4pi*1e-7))) + end + record = FEM.formulation_record(formulation) + @test record.requested == NamedTuple(formulation).requested + @test record.methods.temperature_dependence == NamedTuple(temperature_law) + @test typeof(record) === typeof(FEM.formulation_record(LineCableModelsFEM())) + + @test record.methods.temperature_dependence.identifier === :ExponentialResistivity + @test record.methods.temperature_dependence.parameters.scale==1000.0 + @test record.methods.semicon_admittance.identifier === :lossless + for name in (:internal_impedance,:insulation_impedance,:earth_impedance,:earth_admittance,:pipe_impedance) + @test_throws MethodError Formulation(:fem; NamedTuple{(name,)}((formula(:default),))...) + end + @test_throws ArgumentError LineCableModelsFEM(earth_properties=formula(:default; equivalent_earth=:default)) + @test_throws MethodError LineCableModelsFEM(earth_properties=LineCableModels.Earth.EquivalentHomogeneous.Formula(:default)) + finite_soil = EarthModel(100.0,10.0,1.0; thickness=5.0) + finite_problem = LineParametersProblem(system; frequencies=[50.0], earth_props=finite_soil) + @test_throws LineCableModelsFEMError FEM._resolved_fem_model(finite_problem, LineCableModelsFEM()) + hot = LineParametersProblem(system; temperature=250.0, frequencies=[50.0], + earth_props=problem.earth_props) + @test_throws LineCableModelsFEMError FEM._resolved_fem_model(hot, LineCableModelsFEM()) + @test FEM._resolved_fem_model(hot, LineCableModelsFEM(temperature_dependence=nothing)) isa FEM.FEMResolvedModel + @test FEM._fem_float64_scalar(measurement(big"1.25",big"0.01")) === 1.25 + @test_throws OverflowError FEM._fem_float64_scalar(big"1e400") + conducting_limit = LineParametersProblem(system;frequencies=[50.0], + earth_props=homogeneous(rho=1e-309)) + @test_throws LineCableModelsFEMError FEM._resolved_fem_model(conducting_limit,LineCableModelsFEM()) +end + +@testitem "Gmsh FEM / metallic enclosure solves and reduces without a pipe formula" tags=[:extension,:integration,:fem_numerical,:fem,:slow] setup=[FormulaFixtures] begin + using Gmsh, LinearAlgebra + copper = Material(:conductor,1.72e-8,1,1,20,0.004) + dielectric = Material(:insulator,1e14,2.3) + air = Material(:insulator,Inf,1.0) + function enclosed_system(metal) + first_core = terminal(:a,core(metal;r=0.005),insulation(dielectric;t=0.002)) + second_core = terminal(:b,core(metal;r=0.005),insulation(dielectric;t=0.002)) + wall = terminal(:pipe,sheath(metal;t=0.001),insulation(dielectric;t=0.002)) + design = build(CableDesign,"constitutive-enclosure", + pipe(at(first_core,-0.01,0),at(second_core,0.01,0); + shape=Disk(0.025),fill=air,wall)) + build(LineCableSystem,design,Pose2(0.0,-1.0); + connections=Dict(:a=>1,:b=>2,:pipe=>0)) + end + problem = LineParametersProblem(enclosed_system(copper);temperature=80.0, + frequencies=[50.0],earth_props=homogeneous(rho=100.0)) + selected = LineCableModelsFEM(options=(ideal_transposition=false,)) + selected_controls = (gmsh_verbosity=0,getdp_verbosity=0) + result = compute(problem,selected; options=merge(selected_controls, (trace=true,))) + primitive = result.details.data.fem.primitive + @test primitive.phase_map == [1,2,0] + @test size(primitive.Z_primitive) == (3,3,1) + @test result.details.data.fem.reduced_phase_map == [1,2] + @test all(isfinite,result.Z.values) && all(isfinite,result.Y.values) + z = primitive.Z_primitive[:,:,1] + p = primitive.P_primitive[:,:,1] + # Independently eliminate the grounded wall's current and charge unknowns. + @test result.Z.values[:,:,1] ≈ z[1:2,1:2]-z[1:2,3:3]*(z[3:3,3:3]\z[3:3,1:2]) + @test result.Y.values[:,:,1] ≈ inv(p[1:2,1:2]-p[1:2,3:3]*(p[3:3,3:3]\p[3:3,1:2])) + @test z ≈ transpose(z) rtol=1e-10 + @test p ≈ transpose(p) rtol=1e-8 + @test eigmin(Symmetric(real(z))) > 0 + @test eigmin(Symmetric(imag(result.Y.values[:,:,1]))) > 0 + corrected = Material(:conductor,1.72e-8*(1+0.004*60)) + reference_problem = LineParametersProblem(enclosed_system(corrected); + temperature=80.0,frequencies=[50.0],earth_props=problem.earth_props) + reference = compute(reference_problem,LineCableModelsFEM(temperature_dependence=nothing, + options=selected.options);options=(trace=true,)) + @test z ≈ reference.details.data.fem.primitive.Z_primitive[:,:,1] rtol=2e-9 + @test result.Y.values ≈ reference.Y.values rtol=2e-9 +end + +@testitem "Gmsh FEM / real constitutive laws match independent static material solves" tags=[:extension,:integration,:fem_numerical,:fem,:slow] setup=[FormulaFixtures] begin + using Gmsh + const FEM = Base.get_extension(LineCableModels, :LineCableModelsGmshExt) + const FD = LineCableModels.Earth.FrequencyDependent + copper = Material(:conductor, 1.72e-8, 1, 1, 20, 0.004) + dielectric = Material(:insulator, 1e8, 2.3, 1, 20, -0.003; tan_delta=0.025) + function system_for(metal, passive) + design = build(CableDesign,"constitutive-numerical",terminal(:core, + core(metal; r=0.005), insulation(passive; t=0.005))) + build(LineCableSystem,[design,design],[(0.0,-0.1),(0.1,-0.1)]; + connections=[Dict(:core=>1),Dict(:core=>2)]) + end + system = system_for(copper,dielectric) + options = (reduce_bundle=false,kron_reduction=false,ideal_transposition=false) + execution = (gmsh_verbosity=0,getdp_verbosity=0) + for law in (:default,:lossy), temperature in (20.0,80.0) + problem = LineParametersProblem(system; temperature, frequencies=[50.0], + earth_props=homogeneous(rho=100.0,eps_r=10.0)) + selected = LineCableModelsFEM(insulation_admittance=law; options) + selected_controls = execution + actual = compute(problem,selected; options=selected_controls) + corrected = map((copper,dielectric)) do material + rho = material.rho*(1+material.alpha*(temperature-material.T0)) + Material(material.kind,rho,material.eps_r,material.mu_r,material.T0,0; + tan_delta=material.tan_delta) + end + reference_problem = LineParametersProblem(system_for(corrected...); + temperature, frequencies=[50.0],earth_props=problem.earth_props) + reference = compute(reference_problem,LineCableModelsFEM(insulation_admittance=law, + temperature_dependence=nothing; options);options=execution) + @test actual.Z.values ≈ reference.Z.values rtol=2e-9 + @test actual.Y.values ≈ reference.Y.values rtol=2e-9 + end + soil_law=FormulaFixtures.DispersiveSoil() + air = EarthLayer(Inf,1.5,1.2,Inf) + earth = EarthModel(100.0,10.0,1.0; air_layer=air) + problem = LineParametersProblem(system; frequencies=[50.0,1000.0],earth_props=earth) + selected = LineCableModelsFEM(earth_properties=soil_law; + options) + selected_controls = execution + actual = compute(problem,selected; options=merge(selected_controls, (trace=true,))) + for (index,f) in pairs(problem.frequencies) + # Compute the reference from explicit published inputs without calling + # the selected constitutive law. + static = EarthModel(100/(1+f/1000),10*(1+f/2000),1+f/10000; air_layer=air) + reference_problem = LineParametersProblem(system;frequencies=[f],earth_props=static) + reference = compute(reference_problem,LineCableModelsFEM(earth_properties=nothing; + options);options=(;execution...,trace=true)) + @test actual.Z.values[:,:,index] ≈ reference.Z.values[:,:,1] rtol=2e-9 + @test actual.Y.values[:,:,index] ≈ reference.Y.values[:,:,1] rtol=2e-9 + @test actual.details.data.fem.inputs.mesh_plans[index].domain_radius == + only(reference.details.data.fem.inputs.mesh_plans).domain_radius + end + vacuum = LineParametersProblem(system;frequencies=problem.frequencies, + earth_props=homogeneous(rho=100.0,eps_r=10.0)) + other = compute(vacuum,selected; options=selected_controls) + @test !isapprox(actual.Z.values,other.Z.values;rtol=1e-5) + # Buried cable capacitance is dominated by insulation. Exercise the air + # permittivity response with the same cables placed above the interface. + overhead = build(LineCableSystem,system.designs,[(0.0,1.0),(0.1,1.0)]; + connections=[Dict(:core=>1),Dict(:core=>2)]) + declared_air = compute(LineParametersProblem(overhead;frequencies=[50.0],earth_props=earth),selected; options=selected_controls) + vacuum_air = compute(LineParametersProblem(overhead;frequencies=[50.0],earth_props=vacuum.earth_props),selected; options=selected_controls) + @test !isapprox(declared_air.Y.values,vacuum_air.Y.values;rtol=1e-5) +end diff --git a/test/extensions/fem_enclosure.jl b/test/extensions/fem_enclosure.jl new file mode 100644 index 000000000..68e1d2f1b --- /dev/null +++ b/test/extensions/fem_enclosure.jl @@ -0,0 +1,87 @@ +@testitem "Gmsh FEM / complete strand envelopes preserve enclosing material" tags=[:extension, :fem] begin + using Gmsh + const FEM = Base.get_extension(LineCableModels, :LineCableModelsGmshExt) + const DM = LineCableModels.DataModel + copper = Material(kind=:conductor, rho=1.72e-8) + dielectric = Material(kind=:insulator, rho=1e10, eps_r=3.0) + wire_radius = 0.5e-3 + envelope = Disk(sqrt(7) * wire_radius) + exterior = Ellipse(4e-3, 3e-3) + compacted = stranded(copper; shape=Disk(wire_radius), compact=true, boundary=envelope) + designs = map((compacted, solid(copper, envelope))) do body + build(CableDesign, "bounded-enclosure", + pipe(terminal(:core, body); shape=exterior, fill=dielectric)) + end + source_fill = last(first(designs).geometry.regions).primitive + @test source_fill isa DM.DifferenceShape + @test length(source_fill.holes) == 7 + for dielectric_formula in (:default, :lossy) + formulation = Formulation(:fem; + insulation_admittance=dielectric_formula) + models = map(designs) do design + system = build(LineCableSystem, design, Pose2(0.0, -0.1); + connections=Dict(:core=>1)) + problem = LineParametersProblem(system; frequencies=[50.0, 1000.0], + earth_props=homogeneous(rho=100.0)) + FEM._resolved_fem_model(problem, formulation) + end + for model in models + @test length(model.region_plans) == 2 + metal, matrix = model.region_plans + @test metal.terminal_index == 1 + @test matrix.terminal_index == 0 + @test metal.shape isa Disk + @test matrix.shape isa DM.DifferenceShape + @test only(matrix.shape.holes) == metal.shape + @test area(metal.shape) ≈ 7pi * wire_radius^2 + @test sum(plan -> area(plan.shape), model.region_plans) ≈ area(exterior) + coefficients = model.material_plans[matrix.material_index].admittivity + @test real.(coefficients) == fill(dielectric_formula === :default ? 0.0 : 1e-10, 2) + @test imag.(coefficients) ≈ 2pi .* [50.0, 1000.0] .* 8.8541878128e-12 .* 3 + end + @test [plan.admittivity for plan in first(models).material_plans] == + [plan.admittivity for plan in last(models).material_plans] + @test area.(getproperty.(first(models).region_plans, :shape)) ≈ + area.(getproperty.(last(models).region_plans, :shape)) + end + # Coalescing adapts detached geometry for FEM and preserves strand identity. + @test length(first(designs).geometry.regions) == 8 + @test length(source_fill.holes) == 7 +end + +@testitem "Gmsh FEM / disjoint assembly boundaries retain terminal and mesh ownership" tags=[:extension, :fem] begin + using Gmsh + const FEM = Base.get_extension(LineCableModels, :LineCableModelsGmshExt) + const DM = LineCableModels.DataModel + copper = Material(kind=:conductor, rho=1.72e-8) + design = build(CableDesign, "disjoint-assembly", assembly( + at(terminal(:a, core(copper; r=0.5e-3)), -2e-3, 0), + at(terminal(:b, core(copper; r=0.8e-3)), 2e-3, 0), + )) + for height in (-1.0, 1.0) + system = build(LineCableSystem, design, Pose2(0.0, height); + connections=Dict(:a=>1, :b=>2)) + problem = LineParametersProblem(system; frequencies=[50.0], + earth_props=homogeneous(rho=100.0)) + model = FEM._resolved_fem_model(problem, LineCableModelsFEM()) + @test only(model.cable_boundaries) isa DM.AssemblyShape + @test length(only(model.cable_boundaries).members) == 2 + @test model.cable_hosts == [height > 0 ? :air : :earth] + @test getproperty.(model.region_plans, :terminal_index) == [1, 2] + @test only(model.cable_outer_mesh_sizes) == minimum(plan -> plan.mesh_size, model.region_plans) + @test sum(plan -> area(plan.shape), model.region_plans) ≈ pi * (0.5e-3^2 + 0.8e-3^2) + boundary_shape = only(model.cable_boundaries) + @test boundary(boundary_shape) === boundary_shape + @test area(boundary_shape) ≈ pi * (0.5e-3^2 + 0.8e-3^2) + @test perimeter(boundary_shape) ≈ 2pi * (0.5e-3 + 0.8e-3) + center = centroid(boundary_shape) + @test center[1] ≈ 2e-3 * (0.8e-3^2 - 0.5e-3^2) / (0.8e-3^2 + 0.5e-3^2) + @test center[2] ≈ height + for angle in (0.0, pi / 4, pi / 2, pi) + @test support(boundary_shape, angle) ≈ max( + -2e-3 * cos(angle) + height * sin(angle) + 0.5e-3, + 2e-3 * cos(angle) + height * sin(angle) + 0.8e-3) + end + @test support(boundary_shape) ≈ hypot(2e-3, height) + 0.8e-3 + end +end diff --git a/test/extensions/fem_failure.jl b/test/extensions/fem_failure.jl new file mode 100644 index 000000000..6f16ed266 --- /dev/null +++ b/test/extensions/fem_failure.jl @@ -0,0 +1,71 @@ +@testitem "Gmsh FEM / public solver failure retains log and run evidence" tags=[:extension, :fem, :slow] begin + using Gmsh + using JSON3 + + if Sys.isunix() + extension = Base.get_extension(LineCableModels, :LineCableModelsGmshExt) + copper = Material(kind = :conductor, rho = 1.72e-8) + design = build(CableDesign, "fem-public-failure", + terminal(:core, core(copper; r = 0.005))) + system = build(LineCableSystem, design, (0.0, -0.1); + connections = Dict(:core=>1), system_id = "fem-public-failure") + problem = LineParametersProblem(system; frequencies = [50.0], + earth_props = homogeneous(rho = 100.0, eps_r = 10.0)) + @test !Bool(Gmsh.gmsh.is_initialized()) + mktempdir() do directory + executable = joinpath(directory, "failing-getdp") + write(executable, raw""" + #!/bin/sh + if [ "$1" = "-info" ]; then + echo 'GetDP Version 3.5.0 failure fixture' + exit 0 + fi + echo 'deliberate solver failure' >&2 + exit 17 + """) + chmod(executable, 0o700) + log_file = joinpath(directory, "logs", "computation.log") + formulation = Formulation(:fem; + options = (ideal_transposition = false,)) + formulation_controls = (getdp_executable = executable, gmsh_verbosity = 0, + getdp_verbosity = 0, keep_run_directory = true) + failure = try + compute(problem, formulation; options=merge(formulation_controls, (log_file = log_file, verbosity = (default = 0,)))) + nothing + catch exception + exception + end + @test failure isa LineCableModelsFEMError + if failure isa LineCableModelsFEMError + @test failure.run_directory !== nothing + end + if failure isa LineCableModelsFEMError && failure.run_directory !== nothing + run_path = failure.run_directory + try + @test failure.category === :getdp + @test failure.field === :client + @test occursin(run_path, sprint(showerror, failure)) + state = JSON3.read(read(joinpath(run_path, "run.json"), String)) + @test state.state == "failed" + @test state.getdp_invocations == 1 + @test occursin("GetDP", state.message) + @test isfile(joinpath(run_path, "input", "problem.json")) + @test isfile(joinpath(run_path, "input", "getdp", "quasi-tem.pro")) + @test isfile(joinpath(run_path, "logs", "getdp.log")) + @test !isfile(joinpath(run_path, "raw", "checksums.json")) + messages = read(log_file, String) + @test occursin("FEM geometry ready", messages) + @test occursin("FEM mesh ready", messages) + @test occursin("Starting isolated GetDP", messages) + @test !occursin("FEM scan completed successfully", messages) + finally + # Remove only this fixture's precisely identified failed run. + @assert realpath(dirname(run_path)) == + realpath(joinpath(extension._runtime_root(), "runs")) + rm(run_path; recursive = true) + end + end + @test !Bool(Gmsh.gmsh.is_initialized()) + end + end +end diff --git a/test/extensions/fem_partition.jl b/test/extensions/fem_partition.jl new file mode 100644 index 000000000..65e6505a7 --- /dev/null +++ b/test/extensions/fem_partition.jl @@ -0,0 +1,101 @@ +@testitem "Gmsh FEM / polygon contacts preserve complete material partitions" tags=[:extension, :fem] begin + using Gmsh + const FEM = Base.get_extension(LineCableModels, :LineCableModelsGmshExt) + const DM = LineCableModels.DataModel + const gmsh = Gmsh.gmsh + copper = Material(kind=:conductor, rho=1.72e-8) + dielectric = Material(kind=:insulator, rho=Inf, eps_r=2.3) + polygons = ( + Polygon(((-0.003,-0.001), (0.0,-0.001), (0.0,0.001), (-0.003,0.001))), + Polygon(((0.0,-0.001), (0.003,-0.001), (0.003,0.001), (0.0,0.001))), + Polygon(((0.003,0.001), (0.005,0.001), (0.005,0.003), (0.003,0.003))) + ) + body = terminal(:core, assembly((solid(copper, shape) for shape in polygons)...)) + design = build(CableDesign, "polygon-contacts", + Enclosure(:matrix, body; primitive=Disk(0.01), fill=dielectric)) + session = FEM._start_gmsh(0) + try + for (index, pose) in enumerate((Pose2(0.0,-0.1), Pose2(0.06,-0.2,0.43))) + system = build(LineCableSystem, design, pose; connections=Dict(:core=>1)) + problem = LineParametersProblem(system; frequencies=[50.0], earth_props=homogeneous(rho=100.0)) + model = FEM._resolved_fem_model(problem, LineCableModelsFEM()) + @test length(model.material_plans) == 2 + @test getproperty.(model.region_plans, :material_index) == [1,1,1,2] + @test getproperty.(model.region_plans, :terminal_index) == [1,1,1,0] + @test all(model.region_plans[i].shape == system.geometry[i].primitive for i in 1:3) + geometry = FEM._build_geometry!(model, "polygon-contacts-$index") + @test length(only(geometry.terminal_surfaces)) == 3 + FEM._configure_mesh!(model, geometry, only(model.mesh_plans)) + gmsh.model.mesh.generate(2) + @test isnothing(FEM._inspect_loaded_mesh(model, "polygon-contacts")) + mktempdir() do directory + path = joinpath(directory, "polygon-contacts.msh") + gmsh.write(path) + @test validate(path, model) === path + end + # A nonempty but incomplete filler must be rejected, including on + # import. The former validator accepted exactly this situation. + surface = first(geometry.material_surfaces[2]) + elements, _ = gmsh.model.mesh.get_elements_by_type(2, surface) + @test length(elements) > 1 + gmsh.model.mesh.remove_elements(2, surface, elements[1:1]) + @test_throws LineCableModelsFEMError FEM._inspect_loaded_mesh(model, "partial-filler") + mktempdir() do directory + path = joinpath(directory, "partial.msh") + gmsh.write(path) + @test_throws LineCableModelsFEMError validate(path, model) + end + end + finally + FEM._finish_gmsh(session) + end +end + +@testitem "Gmsh FEM / Milliken filler coverage and invariant scan topology" tags=[:extension, :fem] begin + using Gmsh + const FEM = Base.get_extension(LineCableModels, :LineCableModelsGmshExt) + const DM = LineCableModels.DataModel + const gmsh = Gmsh.gmsh + copper = Material(kind=:conductor, rho=1.72e-8) + core = milliken(copper; shape=Disk(0.33e-3), + segment=Sector(span=pi/3, r_base=0.85e-3, r_back=3e-3, fillet=0.1e-3)) + design = build(CableDesign, "partition-milliken", terminal(:core, core)) + system = build(LineCableSystem, design, Pose2(0.0,-0.1); connections=Dict(:core=>1)) + problem = LineParametersProblem(system; frequencies=[0.1,50.0,1e7], earth_props=homogeneous(rho=100.0)) + model = FEM._resolved_fem_model(problem, LineCableModelsFEM()) + @test length(model.material_plans) == 2 + session = FEM._start_gmsh(0) + try + geometry = FEM._build_geometry!(model, "partition-milliken") + points = unique(gmsh.model.get_boundary( + [(1,c) for c in only(geometry.terminal_curves)], false, false, true)) + coordinates = [gmsh.model.get_value(dim, tag, Float64[]) for (dim,tag) in points] + entities = gmsh.model.get_entities() + model_names = gmsh.model.list() + for plan in model.mesh_plans + FEM._update_exterior_mesh!(model, geometry, plan) + @test length(gmsh.model.get_entities()) == length(entities) + @test [gmsh.model.get_value(dim, tag, Float64[]) for (dim,tag) in points] == coordinates + for (curves, radius) in ((geometry.inner_shell_curves,plan.domain_radius), + (geometry.outer_curves,plan.shell_outer_radius)) + vertices = unique(gmsh.model.get_boundary([(1,c) for c in curves],false,false,true)) + @test all(vertices) do (dim,tag) + p = gmsh.model.get_value(dim,tag,Float64[]) + hypot(p[1]-model.center[1],p[2]) ≈ radius + end + @test all(curves) do curve + lower, upper = gmsh.model.get_parametrization_bounds(1, curve) + p = gmsh.model.get_value(1, curve, (lower + upper) / 2) + hypot(p[1]-model.center[1],p[2]) ≈ radius + end + end + FEM._configure_mesh!(model, geometry, plan) + gmsh.model.mesh.generate(2) + @test isnothing(FEM._inspect_loaded_mesh(model, "milliken-frequency-$(plan.frequency)")) + @test gmsh.model.list() == model_names + end + @test length(geometry.material_surfaces[2]) > 1 + finally + FEM._finish_gmsh(session) + end +end diff --git a/test/extensions/fem_quasi_full.jl b/test/extensions/fem_quasi_full.jl new file mode 100644 index 000000000..1f2c1b4f9 --- /dev/null +++ b/test/extensions/fem_quasi_full.jl @@ -0,0 +1,136 @@ +@testitem "Gmsh FEM / production edge paths / affine field and Stokes controls" tags=[:extension, :fem] begin + using Gmsh + FEM=Base.get_extension(LineCableModels, :LineCableModelsGmshExt) + triangles=[((0.0, 0.0), (2.0, 0.0), (2.0, 1.0)), ((0.0, 0.0), (2.0, 1.0), (0.0, 1.0))] + a=(3.0, -2.0) + b=0.7 + integrate(vertices; kwargs...) = sum((a[1]-b*y)*dx+(a[2]+b*x)*dy + for (x, y, dx, dy) in FEM._voltage_path_points(triangles, vertices; kwargs...)) + p=(0.3, 0.0) + q=(1.7, 1.0) + exact=a[1]*(q[1]-p[1])+a[2]*(q[2]-p[2])+b*(p[1]*q[2]-p[2]*q[1]) + @test integrate([p, q]) ≈ exact rtol=1e-10 + @test integrate([q, p]) ≈ -exact rtol=1e-10 + @test integrate([(0.0, 0.0), (2.0, 1.0)]) ≈ 2a[1]+a[2] + @test integrate([(0.0, 0.0), (2.0, 0.0), (2.0, 1.0), (0.0, 1.0), (0.0, 0.0)]) ≈ 2b*2 + @test integrate([(0.5, 0.0), (0.5, 1.0)]; active = [false, true]) ≈ (a[2]+0.5b)*0.75 + @test_throws ErrorException integrate([(0.0, 0.0), (0.0, 2.0)]) + @test_throws DimensionMismatch integrate([p, q]; active = [true]) +end + +@testitem "Gmsh FEM / physics selection, native maps, and resume isolation" tags=[ + :extension, :integration, :fem_numerical, + :fem, + :slow] begin + using LineCableModels, Gmsh, LinearAlgebra, JSON3 + FEM = Base.get_extension(LineCableModels, :LineCableModelsGmshExt) + @test formulation_options(LineCableModelsFEM, FormulationOptions()).data.physics === + Symbol("quasi-tem") + for value in (:quasi_tem, "quasi-tem", Symbol("quasi-tem")) + @test formulation_options(LineCableModelsFEM, FormulationOptions(physics = value)).data.physics === + Symbol("quasi-tem") + end + for value in (:quasi_fw, "quasi-fw", Symbol("quasi-fw")) + @test formulation_options(LineCableModelsFEM, FormulationOptions(physics = value)).data.physics === + Symbol("quasi-fw") + end + @test_throws ArgumentError formulation_options(LineCableModelsFEM, FormulationOptions(physics = :fullwave)) + wire = build(CableDesign, "physics-selection", + terminal(:core, + core(Material(kind = :conductor, rho = 2e-8); r = 0.005))) + system = build(LineCableSystem, [wire, wire], [(0.0, 1.0), (0.4, 1.5)]; + connections = [Dict(:core=>1), Dict(:core=>2)], line_length = 1.0) + problem = LineParametersProblem(system; frequencies = [1e4], + earth_props = homogeneous(rho = 100.0, eps_r = 10.0, mu_r = 1.0)) + reductions = ( + reduce_bundle = false, kron_reduction = false, ideal_transposition = false) + formulations = [Formulation(:fem; options = (; reductions..., physics)) + for physics in (:quasi_tem, :quasi_fw)] + controls = (gmsh_verbosity = 0, getdp_verbosity = 0, + keep_run_directory = true, plot_field_maps = true) + results = compute(problem, formulations; options = (; controls..., trace = true)) + tem, fw = results + @test all(isfinite, Z(fw)) # No cross-formulation numerical authority. + @test tem.details.data.fem.inputs.options.physics !== + fw.details.data.fem.inputs.options.physics + @test all(isfinite, Y(fw)) + @test fw.details.data.fem.inputs.options.physics === Symbol("quasi-fw") + @test fw.details.data.fem.run.run_directory != tem.details.data.fem.run.run_directory + @test details(fw).data.fem.run isa NamedTuple + @test fw.details.data.fem.run.getdp_invocations == 1 + @test fw.details.data.fem.run.completed_columns == 2 + @test fw.details.data.fem.run.factorized_columns == 1 + @test length(fw.details.data.fem.run.map_paths) == 24 + @test length(tem.details.data.fem.run.map_paths) == 18 + @test occursin("Coupled", fw.details.data.formulations.assumptions.admittance) + path = fw.details.data.fem.run.run_directory + @test isfile(joinpath(path, "raw/jobs/getdp-f0001-b0001-Pscalar.tsv")) + @test isfile(joinpath(path, "input/paths-f0001.pro")) + checkpoint = JSON3.read(read(joinpath(path, "raw/jobs/getdp-f0001-b0001.json"), String)) + @test checkpoint.physics == "quasi-fw" + @test length(checkpoint.checksums) == 16 # Z/P, timing, twelve maps, scalar diagnostic + repeated = compute(problem, last(formulations); options = (; + controls..., resume_run_directory = path)) + @test Y(repeated) == Y(fw) + @test repeated.details.data.fem.run.reused + @test_throws ArgumentError compute(problem, first(formulations); + options = (; controls..., resume_run_directory = path)) + # Both physics use the same named ONELAB selector and resolution. + session = FEM._start_gmsh(0) + try + model = FEM._resolved_fem_model(FEM._preflight_fem_problem(problem), last(formulations)) + run = FEM.FEMRun(path, FEM.completed, "test", :none, "") + FEM._publish_transport!(run, model, joinpath(path, "input/model_data.pro"), + joinpath(path, "mesh/model.msh"), last(formulations), + computation_options(LineCableModelsFEM, ComputationOptions(controls))) + @test gmsh.onelab.get_number("LineCableModels/FEM/physics") == [1.0] + finally + FEM._finish_gmsh(session) + end +end + +@testitem "Gmsh FEM / voltage path file writing preserves the caller session" tags=[:extension, :fem] begin + using LineCableModels, Gmsh + FEM = Base.get_extension(LineCableModels, :LineCableModelsGmshExt) + wire = build(CableDesign, "bare", terminal(:core, + core(Material(kind = :conductor, rho = 2e-8); r = 0.005))) + positions = [(0.0, 1.0), (0.4, 1.5)] + system = build(LineCableSystem, [wire, wire], positions; + connections = [Dict(:core=>1), Dict(:core=>2)], line_length = 1.0) + problem = LineParametersProblem(system; frequencies = [1e4], + earth_props = homogeneous(rho = 100.0, eps_r = 10.0, mu_r = 1.0)) + fem = Formulation(:fem; + options = (physics = :quasi_fw, reduce_bundle = false, + kron_reduction = false, ideal_transposition = false)) + fem_controls = (gmsh_verbosity = 0, getdp_verbosity = 3, mesh_mode = :remesh) + model = FEM._resolved_fem_model(FEM._preflight_fem_problem(problem), fem) + mktempdir() do root + info = FEM._create_run(root) + path = joinpath(info.path, "paths.pro") + lock(FEM.FEM_SESSION_LOCK) do + session = FEM._start_gmsh(0) + try + geometry = FEM._build_geometry!(model, "quasi-full-test") + mesh = only(FEM._select_meshes!(info, + model, + geometry, + computation_options(LineCableModelsFEM, ComputationOptions(fem_controls)), + root)) + # Exercise a caller model whose name collides with the mesh + # basename. File writing must preserve both its identity + # and its contents rather than opening another "model". + gmsh.model.add(splitext(basename(mesh))[1]) + caller_model = gmsh.model.get_current() + gmsh.model.add_discrete_entity(0, 991) + initial_models = sort(gmsh.model.list()) + FEM._write_voltage_paths(path, mesh, only(model.mesh_plans), model) + @test gmsh.model.get_current() == caller_model + @test sort(gmsh.model.list()) == initial_models + @test gmsh.model.get_entities(0) == [(0, 991)] + gmsh.model.remove() + finally + FEM._finish_gmsh(session) + end + end + end +end diff --git a/test/extensions/fem_recovery_errors.jl b/test/extensions/fem_recovery_errors.jl new file mode 100644 index 000000000..d414782be --- /dev/null +++ b/test/extensions/fem_recovery_errors.jl @@ -0,0 +1,223 @@ +@testmodule FEMRecoveryFaults begin + import LineCableModels, Gmsh, JSON3 + using SHA: sha256 + const E = Base.get_extension(LineCableModels, :LineCableModelsGmshExt) + const ImportExport = LineCableModels.ImportExport + const FEMResolvedModel = NamedTuple + const FEMScan = NamedTuple + const FEMRun = E.FEMRun + const FEMActiveWorker = E.FEMActiveWorker + const created, completed, running = E.created, E.completed, E.running + const _fem_error = E._fem_error + const _create_run = E._create_run + const _resume_value_matches = E._resume_value_matches + const _column_paths = E._column_paths + const _column_files = E._column_files + const _column_timing = E._column_timing + const _write_json_atomic = E._write_json_atomic + const _attempt_record = E._attempt_record + const _transition! = E._transition! + const read_fault = Ref{Any}(nothing) + const fault_path = Ref("") + const fault_after = Ref(1) + const read_count = Ref(0) + const raw_fault = Ref{Any}(nothing) + const pid_fault = Ref{Any}(nothing) + const wait_for_exit = Ref(false) + const child = Ref{Base.Process}() + const log = Ref{IOStream}() + + function read(path::String, ::Type{String}) + if path == fault_path[] + read_count[] += 1 + if read_count[] >= fault_after[] && read_fault[] !== nothing + read_fault[] isa String && return read_fault[] + throw(read_fault[]) + end + end + return Base.read(path, String) + end + function getpid(process::Base.Process) + child[] = process + wait_for_exit[] && wait(process) + pid_fault[] === nothing || throw(pid_fault[]) + return Base.getpid(process) + end + function open(path::String, mode::String) + log[] = Base.open(path, mode) + end + open(f::Function, path::String) = Base.open(f, path) + function _valid_job_raw(arguments...) + raw_fault[] === nothing || throw(raw_fault[]) + return E._valid_job_raw(arguments...) + end + + # Isolate file and process faults without changing package or Base methods. A name + # selects every definition of that function. A signature selects one method. + for (file, selected) in (("compute.jl", (:_resume_inputs_match, :_resume_run)), + ("results.jl", (:(validate(scan::FEMScan, run::FEMRun)),)), + ("workers.jl", (:_valid_column_checkpoint, :_process_token, :_start_worker!))) + Base.include(@__MODULE__, joinpath(@__DIR__, "../../ext/LineCableModelsGmshExt", file)) do expression + expression isa Expr && expression.head === :function && + expression.args[1] isa Expr && expression.args[1].head === :call && + (expression.args[1].args[1] in selected || expression.args[1] in selected) ? + expression : nothing + end + end +end + +@testitem "Gmsh FEM / recovery rejects invalid files and propagates unexpected failures" tags=[:extension, :fem] setup=[FEMRecoveryFaults] begin + using JSON3 + const F = FEMRecoveryFaults + model = (problem=(system=(system_id="recovery-fixture",),),) + inputs = (owned_gmsh=true, schema_version=7, solver_protocol=3, + getdp_identity=nothing, execution=(ui=false, mesh_mode=:reuse)) + unexpected = (InterruptException(), ErrorException("unexpected reader bug"), + MethodError(sin, (nothing,))) + caught(f) = try + f() + catch exception + exception + end + + mktempdir() do root + run = F.E._create_run(root) + F.E._write_json_atomic(joinpath(run.path, "input", "problem.json"), + LineCableModels.ImportExport.serialize_value(model.problem)) + F.E._write_json_atomic(joinpath(run.path, "input", "computation.json"), inputs) + state = joinpath(run.path, "run.json") + original = read(state, String) + @test F._resume_inputs_match(run.path, model, inputs) + # JSON snapshots keep their lossless integer view. Input records on + # both sides are parsed alike and remain insensitive to object order. + nested = merge(inputs, (extra=(large=Int64(9007199254740993), + values=[(a=1, b=2), (a=3, b=4)]),)) + computation = joinpath(run.path, "input", "computation.json") + reordered = merge(nested, (extra=(values=[(b=2, a=1), (b=4, a=3)], + large=Int64(9007199254740993)),)) + F.E._write_json_atomic(computation, reordered) + @test F._resume_inputs_match(run.path, model, nested) + changed = merge(nested, (extra=merge(nested.extra, (large=Int64(9007199254740992),)),)) + @test !F._resume_inputs_match(run.path, model, changed) + F.E._write_json_atomic(computation, inputs) + @test F._resume_run(root, run.path, model, inputs).getdp_invocations == 0 + for malformed in ("{", "[]", "null", "{}", "{\"state\":42}") + write(state, malformed) + @test !F._resume_inputs_match(run.path, model, inputs) + end + write(state, original) + F.fault_path[] = state + for exception in unexpected + F.read_fault[] = exception + @test caught(() -> F._resume_inputs_match(run.path, model, inputs)) === exception + end + F.read_fault[] = SystemError("read fixture", 2) + @test !F._resume_inputs_match(run.path, model, inputs) + + # The first metadata read selects the run. A later read must not invent + # fresh metadata if the selected file disappears or becomes malformed. + F.fault_after[] = 2 + for fault in (SystemError("selected file disappeared", 2), "{") + F.read_count[] = 0 + F.read_fault[] = fault + exception = caught(() -> F._resume_run(root, run.path, model, inputs)) + @test exception isa LineCableModelsFEMError + @test exception.run_directory == run.path + @test occursin(state, exception.message) + @test occursin(fault isa String ? "invalid JSON" : "selected file disappeared", exception.message) + @test read(state, String) == original + end + for exception in unexpected + F.read_count[] = 0 + F.read_fault[] = exception + @test caught(() -> F._resume_run(root, run.path, model, inputs)) === exception + end + F.read_count[] = 0 + F.read_fault[] = "{}" + @test caught(() -> F._resume_run(root, run.path, model, inputs)) isa LineCableModelsFEMError + + F.fault_after[] = 1 + F.fault_path[] = joinpath(run.path, "raw", "checksums.json") + for fault in (SystemError("checksum file disappeared", 2), "{") + F.read_fault[] = fault + exception = caught(() -> F.validate((map_paths=String[],), run)) + @test exception isa LineCableModelsFEMError + @test exception.field === :checksum + @test occursin(F.fault_path[], exception.message) + @test occursin(fault isa String ? "invalid JSON" : "checksum file disappeared", exception.message) + end + for exception in unexpected + F.read_fault[] = exception + @test caught(() -> F.validate((map_paths=String[],), run)) === exception + end + + paths = F._column_paths(run.path, 1, 1, false) + mkpath(dirname(paths.checkpoint)) + write(paths.checkpoint, JSON3.write((protocol=2, frequency_index=1, basis=1, + terminals=1, frequency_hz=50.0, plot_field_maps=false, + mesh_digest="mesh", checksums=Dict()))) + F.fault_path[] = paths.checkpoint + for exception in unexpected + F.read_fault[] = exception + @test caught(() -> F._valid_column_checkpoint(run.path, 1, 50.0, 1, 1, false, "mesh")) === exception + end + F.read_fault[] = SystemError("checkpoint file disappeared", 2) + @test !F._valid_column_checkpoint(run.path, 1, 50.0, 1, 1, false, "mesh") + F.read_fault[] = nothing + for exception in (unexpected..., ArgumentError("raw validator bug")) + F.raw_fault[] = exception + @test caught(() -> F._valid_column_checkpoint(run.path, 1, 50.0, 1, 1, false, "mesh")) === exception + end + F.raw_fault[] = SystemError("raw file disappeared", 2) + @test !F._valid_column_checkpoint(run.path, 1, 50.0, 1, 1, false, "mesh") + F.raw_fault[] = nothing + + if Sys.islinux() + F.fault_path[] = "/proc/$(getpid())/stat" + for exception in unexpected + F.read_fault[] = exception + @test caught(() -> F._process_token(getpid())) === exception + end + F.read_fault[] = SystemError("process disappeared", 2) + @test F._process_token(getpid()) === nothing + F.read_fault[] = "incomplete stat" + @test F._process_token(getpid()) === nothing + F.read_fault[] = nothing + @test F._process_token(getpid()) isa String + end + F.read_fault[] = nothing + end +end + +@testitem "Gmsh FEM / PID lookup only recovers exited children and cleans up failures" tags=[:extension, :fem] setup=[FEMRecoveryFaults] begin + const F = FEMRecoveryFaults + mktempdir() do root + run = F.E._create_run(root) + execution = (data=(solver_threads=1,),) + exited = `$(Base.julia_cmd()) --startup-file=no --handle-signals=no --project=@stdlib -e 'exit()'` + job = F.E.FEMFrequencyJob(1, 50.0, [1], root, "mesh", exited) + F.wait_for_exit[] = true + worker = F._start_worker!(run, job, execution) + @test worker.pid == 0 + @test worker.process_token === nothing + @test process_exited(worker.process) + close(worker.log) + F.wait_for_exit[] = false + + active = `$(Base.julia_cmd()) --startup-file=no --handle-signals=no --project=@stdlib -e 'sleep(30)'` + job = F.E.FEMFrequencyJob(1, 50.0, [1], root, "mesh", active) + for exception in (InterruptException(), ErrorException("PID lookup bug"), + Base.IOError("unexpected PID error", 0)) + F.pid_fault[] = exception + caught = try + F._start_worker!(run, job, execution) + catch error + error + end + @test caught === exception + @test process_exited(F.child[]) + @test !isopen(F.log[]) + end + F.pid_fault[] = nothing + end +end diff --git a/test/extensions/fem_rectangular.jl b/test/extensions/fem_rectangular.jl new file mode 100644 index 000000000..3f0a761a3 --- /dev/null +++ b/test/extensions/fem_rectangular.jl @@ -0,0 +1,83 @@ +@testitem "Gmsh FEM / rectangular boundaries and filled ring ownership" tags=[:extension, :fem] begin + using Gmsh + const FEM = Base.get_extension(LineCableModels,:LineCableModelsGmshExt) + const DM = LineCableModels.DataModel + const gmsh = Gmsh.gmsh + copper = Material(kind=:conductor,rho=1.72e-8) + matrix = Material(kind=:semicon,rho=100.0,eps_r=10.0) + dielectric = Material(kind=:insulator,rho=Inf,eps_r=2.3) + limit, wire_radius = 0.6e-3, 0.05e-3 + body = stranded(copper;center=Disk(0.2e-3),shape=Rectangle(0.3e-3,0.1e-3), + boundary=Disk(limit)) + core = terminal(:core,body) + occupied = outer_radius(build(CableDesign,"core",core)) + designs = [build(CableDesign,"rectangular-bare",core), + build(CableDesign,"rectangular-insulated",core,insulation(dielectric;t=0.1e-3))] + for radius in (nothing,limit+wire_radius) + ring = Group(:core,Region(:round_wire,Disk(wire_radius),copper); + pattern=Ring(12;r=radius)) + filled = Enclosure(:ring_matrix,ring; + primitive=Annulus(limit,limit+2wire_radius),fill=matrix) + push!(designs,build(CableDesign,"rectangular-ring-$(radius)",core,filled, + insulation(dielectric;t=0.1e-3))) + end + session = FEM._start_gmsh(0) + try + for design in designs + system = build(LineCableSystem,design,Pose2(0.,-0.1);connections=Dict(:core=>1)) + problem = LineParametersProblem(system;frequencies=[50.0],earth_props=homogeneous(rho=100.0)) + model = FEM._resolved_fem_model(problem,LineCableModelsFEM()) + @test first(model.region_plans).shape isa Disk + @test first(model.region_plans).shape.r == occupied + @test sum(r -> area(r.shape),model.region_plans) ≈ area(design.geometry.outer) + @test sum(r -> area(r.shape),filter(r -> r.terminal_index>0,model.region_plans)) ≈ + sum(area,filter(r -> r.source.material.kind===:conductor,design.geometry.regions)) + geometry = FEM._build_geometry!(model,design.cable_id) + FEM._configure_mesh!(model,geometry,only(model.mesh_plans)) + gmsh.model.mesh.generate(2) + @test isnothing(FEM._inspect_loaded_mesh(model,design.cable_id)) + mktempdir() do directory + mesh = joinpath(directory,"model.msh") + gmsh.write(mesh) + @test validate(mesh,model) === mesh + end + end + # An explicitly declared thin layer is not absorbed into the conductor, + # even in the interval between the former 2 ppm / 5 ppm cutoffs. + for fraction in (1e-6,3e-6,6e-6) + design = build(CableDesign,"explicit-thin-wrap",Enclosure(:paper,core; + primitive=Disk(occupied/sqrt(1-fraction)),fill=dielectric)) + system = build(LineCableSystem,design,Pose2(0.,-0.1);connections=Dict(:core=>1)) + problem = LineParametersProblem(system;frequencies=[50.],earth_props=homogeneous(rho=100.)) + model = FEM._resolved_fem_model(problem,LineCableModelsFEM()) + metal, fill_region = model.region_plans + @test metal.shape.r == fill_region.shape.ri == occupied + @test area(fill_region.shape)>0 + @test metal.shape.r < fill_region.shape.ro + @test FEM._build_geometry!(model,"explicit-thin-$fraction") isa FEM.FEMGeometry + end + finally + FEM._finish_gmsh(session) + end +end + +@testitem "Gmsh FEM / rectangular core capacitance uses occupied radius" tags=[:extension,:integration,:fem_numerical,:fem] begin + using Gmsh + copper = Material(kind=:conductor,rho=1.72e-8) + dielectric = Material(kind=:insulator,rho=Inf,eps_r=2.3) + body = stranded(copper;center=Disk(0.2e-3),shape=Rectangle(0.3e-3,0.1e-3), + boundary=Disk(0.6e-3)) + design = build(CableDesign,"rectangular-coaxial", + terminal(:core,body),insulation(dielectric;t=0.2e-3), + terminal(:sheath,Region(:screen,Shell(0.05e-3),copper)), + insulation(dielectric;t=0.1e-3)) + occupied = design.geometry.regions[end-2].primitive.ri + system = build(LineCableSystem,design,Pose2(0.,-0.1);connections=Dict(:core=>1,:sheath=>0)) + problem = LineParametersProblem(system;frequencies=[50.,1000.],earth_props=homogeneous(rho=100.)) + result = compute(problem,LineCableModelsFEM();options=(gmsh_verbosity=0,getdp_verbosity=0)) + expected = 2pi*8.8541878128e-12*2.3/log((occupied+0.2e-3)/occupied) + @test all(isfinite,result.Z) + @test all(isfinite,result.Y) + @test all(isapprox(imag(result.Y[1,1,i])/(2pi*f),expected;rtol=0.02) + for (i,f) in enumerate(result.f)) +end diff --git a/test/extensions/fem_reduction.jl b/test/extensions/fem_reduction.jl new file mode 100644 index 000000000..3560ddaa0 --- /dev/null +++ b/test/extensions/fem_reduction.jl @@ -0,0 +1,29 @@ +@testitem "Gmsh FEM / P/(jω) through the FEM call site gives the coaxial admittance" tags=[:extension, :fem, :slow] begin + using Gmsh + include(joinpath(pkgdir(LineCableModels), "test", "support", "scenarios.jl")) + using .CurrentScenarios + FEM = Base.get_extension(LineCableModels, :LineCableModelsGmshExt) + frequencies = [50.0, 1e3, 1e5] + problem = three_bare_wires_problem(; heights = (10.0, 10.0, 10.0), + horizontal = (0.0, 4.0, 8.0), rho = 100.0, frequencies) + execution = computation_options(LineCableModelsFEM, ComputationOptions((;))) + for ideal_transposition in (false, true) + options = (; ideal_transposition) + coaxial = compute(problem, Formulation(; options); options = (trace = true,)) + trace = details(coaxial).data.trace + # FEM extracts the inverse-admittance coefficient P/(jω) in Ω·m from GetDP. + extracted = similar(trace.P) + for (k, f) in pairs(frequencies) + extracted[:, :, k] = trace.P[:, :, k] / (im * 2π * f) + end + formulation = Formulation(:fem; options) + model = FEM._resolved_fem_model(problem, formulation) + run = FEM._create_run(mktempdir()) + # The input record only enters the result details. + fem = FEM._line_parameters(run, model, formulation, execution, + FEM.FEMScan(copy(trace.Z), extracted, String[]), (;)) + @test Z(fem) == Z(coaxial) + @test Y(fem) ≈ Y(coaxial) rtol = 1e-12 + @test all(≤(1e-10), details(fem).data.fem.inversion_residuals) + end +end diff --git a/test/extensions/fem_resume.jl b/test/extensions/fem_resume.jl new file mode 100644 index 000000000..5299d37d7 --- /dev/null +++ b/test/extensions/fem_resume.jl @@ -0,0 +1,233 @@ +@testitem "Gmsh FEM / resume requires effective inputs and preserves completed runs" tags=[:extension, :fem, :slow] setup=[FormulaFixtures] begin + using Gmsh + using LineCableModels + extension = Base.get_extension(LineCableModels, :LineCableModelsGmshExt) + artifact = withenv("LINECABLEMODELS_GETDP"=>nothing) do + extension._getdp_selection(computation_options(LineCableModelsFEM, ComputationOptions((;)))) + end + @test artifact.source === :artifact + @test artifact.artifact_hash == string(extension.artifact_hash( + "getdp", extension.GETDP_ARTIFACTS_TOML, + )) + @test isfile(artifact.path) + copper = Material(kind=:conductor, rho=1.72e-8) + dielectric = Material(kind=:insulator, rho=1.0e8, eps_r=2.3, tan_delta=0.025) + design = build(CableDesign, "fem-resume-inputs", Stack( + Group(:core, Region(:metal, Disk(0.005), copper)), + Region(:insulation, Shell(0.005), dielectric))) + system = build(LineCableSystem, design, (0.0, -0.1); + connections = Dict(:core=>1), system_id = "fem-resume-inputs") + problem = LineParametersProblem(system; frequencies = [50.0, 1000.0], + earth_props = LineCableModels.Earth.EarthModel(100.0, 10.0, 1.0)) + formulation = Formulation(:fem; + options=(ideal_transposition=false,)) + formulation_controls = (getdp_executable=artifact.path, gmsh_verbosity=0,) + model = extension._resolved_fem_model(problem, formulation) + inputs = extension._fem_input_record(model, formulation, computation_options(LineCableModelsFEM, ComputationOptions(formulation_controls))) + @test inputs.schema_version == 7 + @test inputs.getdp_selection.source === :explicit + @test inputs.getdp_selection.artifact_hash === nothing + @test isfile(inputs.getdp_selection.path) + @test !hasproperty(inputs.getdp_identity, :path) + @test occursin(r"Version\s*:\s*3\.5\.0", inputs.getdp_identity.info) + @test occursin("PETSc", inputs.getdp_identity.info) + @test occursin("complex arithmetic", inputs.getdp_identity.info) + @test_throws LineCableModelsFEMError extension._getdp_selection( + computation_options(LineCableModelsFEM, ComputationOptions((getdp_executable=joinpath(tempdir(), "missing-getdp"),))), + ) + withenv("LINECABLEMODELS_GETDP"=>joinpath(tempdir(), "missing-getdp")) do + @test_throws LineCableModelsFEMError extension._getdp_selection( + computation_options(LineCableModelsFEM, ComputationOptions((;))), + ) + end + @test inputs.mesh_fingerprint == extension._mesh_fingerprint(model, Gmsh.gmsh.GMSH_API_VERSION) + smaller_controls = computation_options(LineCableModelsFEM, ComputationOptions((;formulation_controls..., domain_skin_depths=1.5))) + smaller_model = extension._resolved_fem_model(problem, formulation, smaller_controls) + smaller_inputs = extension._fem_input_record(smaller_model, formulation, smaller_controls) + @test smaller_inputs.mesh_fingerprint != inputs.mesh_fingerprint + # Dictionary iteration order can change between Julia versions. Resume + # records the Julia version. The key must be repeatable within that runtime. + recorded_key=extension._mesh_fingerprint(model, "recorded-gmsh-version") + @test extension._mesh_fingerprint(deepcopy(model), "recorded-gmsh-version") == recorded_key + @test extension._mesh_fingerprint(model, "different-gmsh-version") != recorded_key + @test inputs.adapter_sources isa NamedTuple + @test haskey(inputs.adapter_sources, Symbol("geometry.jl")) + @test !haskey(inputs.adapter_sources, Symbol("formulations.jl")) + other = Formulation(:fem; earth_properties = nothing, + options = formulation.options) + other_controls = formulation_controls + other_model = extension._resolved_fem_model(problem, other) + other_inputs = extension._fem_input_record(other_model, other, computation_options(LineCableModelsFEM, ComputationOptions(other_controls))) + lossy = Formulation(:fem; insulation_admittance = :lossy, + options = formulation.options) + lossy_controls = formulation_controls + lossy_model = extension._resolved_fem_model(problem, lossy) + lossy_inputs = extension._fem_input_record(lossy_model, lossy, computation_options(LineCableModelsFEM, ComputationOptions(lossy_controls))) + @test extension.JSON3.write(inputs) == extension.JSON3.write(other_inputs) + @test extension.JSON3.write(inputs) != extension.JSON3.write(lossy_inputs) + temperature_law=FormulaFixtures.ScaledResistivity(1.06) + soil_law=FormulaFixtures.ScaledSoil(epsilon=2) + thermal = LineCableModelsFEM( + temperature_dependence=temperature_law, + options=formulation.options) + thermal_controls = formulation_controls + hot_problem = LineParametersProblem(system;temperature=80.0, + frequencies=problem.frequencies,earth_props=problem.earth_props) + hot_model = extension._resolved_fem_model(hot_problem,thermal) + hot_inputs = extension._fem_input_record(hot_model,thermal, computation_options(LineCableModelsFEM, ComputationOptions(thermal_controls))) + @test first(hot_inputs.materials).sigma ≈ first(inputs.materials).sigma ./ 1.06 + dispersive = LineCableModelsFEM( + earth_properties=soil_law, + options=formulation.options) + dispersive_controls = formulation_controls + dispersive_model = extension._resolved_fem_model(problem,dispersive) + dispersive_inputs = extension._fem_input_record(dispersive_model,dispersive, computation_options(LineCableModelsFEM, ComputationOptions(dispersive_controls))) + @test dispersive_inputs.mesh_fingerprint == inputs.mesh_fingerprint + @test getproperty.(dispersive_inputs.earth_materials,:eps_r) == + 2 .* getproperty.(inputs.earth_materials,:eps_r) + # Simulate a changed resolver with the declaration and mesh-size settings + # held fixed. The domains handed to Gmsh are authoritative for reuse. + changed_model = deepcopy(model) + region = first(changed_model.region_plans) + changed_model.region_plans[1] = extension.FEMRegionPlan(region.object_id, + region.cable_index, region.region_index, region.terminal_index, + region.material_index, Disk(region.shape.r * 0.9, region.shape.at), region.mesh_size) + changed_inputs = extension._fem_input_record(changed_model, formulation, computation_options(LineCableModelsFEM, ComputationOptions(formulation_controls))) + @test changed_inputs.mesh_fingerprint != inputs.mesh_fingerprint + @test changed_inputs.materials == inputs.materials + @test changed_inputs.region_mesh_sizes == inputs.region_mesh_sizes + @test LineCableModels.ImportExport.serialize_value(changed_model.problem) == + LineCableModels.ImportExport.serialize_value(model.problem) + ownership = deepcopy(model) + ownership.region_plans[1] = extension.FEMRegionPlan(region.object_id, + region.cable_index, region.region_index, 0, region.material_index, + region.shape, region.mesh_size) + @test extension._fem_input_record(ownership, formulation, computation_options(LineCableModelsFEM, ComputationOptions(formulation_controls))).mesh_fingerprint != + inputs.mesh_fingerprint + @test_throws ArgumentError compute(problem, LineCableModelsFEM[]) + law=FormulaFixtures.ScaledSoil(rho=Inf) + unsupported = Formulation(:fem; + earth_properties = law, + options = formulation.options) + unsupported_controls = formulation_controls + before = Bool(Gmsh.gmsh.is_initialized()) + @test_throws LineCableModelsFEMError compute(problem, [formulation, unsupported]) + @test Bool(Gmsh.gmsh.is_initialized()) == before + mktempdir() do root + run = extension._create_run(root) + extension._write_run_inputs!(run, model) + assets = extension._getdp_assets(joinpath(run.path, "input", "getdp")) + @test keys(assets) == + (:model, :jacobian, :integration, :materials, :quasi_tem, :quasi_full) + @test map(basename, values(assets)) == + ("model.pro", "jacobian.pro", "integration.pro", "materials.pro", + "quasi-tem.pro", "quasi-full.pro") + captured = map(path -> (read(path), stat(path).mtime), assets) + extension._write_run_inputs!(run, model) + @test map(path -> (read(path), stat(path).mtime), assets) == captured + # A changed equation file must not be silently repaired or reused. + write(assets.quasi_tem, "// changed equation snapshot\n") + @test_throws LineCableModelsFEMError extension._write_run_inputs!(run, model) + @test read(assets.quasi_tem, String) == "// changed equation snapshot\n" + write(assets.quasi_tem, captured.quasi_tem[1]) + @test !extension._resume_inputs_match(run.path, model, inputs) + extension._write_json_atomic(joinpath(run.path, "input", "computation.json"), inputs) + @test extension._resume_inputs_match(run.path, model, other_inputs) + @test !extension._resume_inputs_match(run.path,hot_model,hot_inputs) + @test !extension._resume_inputs_match(run.path,dispersive_model,dispersive_inputs) + # Current missing metadata is corruption. Obsolete development schema + # examples do not establish a compatibility obligation. + incomplete=Dict(String(k)=>v for (k,v) in pairs(extension.JSON3.read(extension.JSON3.write(inputs)))) + delete!(incomplete,"solver_protocol") + extension._write_json_atomic(joinpath(run.path,"input","computation.json"),incomplete) + @test !extension._resume_inputs_match(run.path,model,inputs) + extension._write_json_atomic(joinpath(run.path,"input","computation.json"),inputs) + @test !extension._resume_inputs_match(run.path, changed_model, changed_inputs) + @test !extension._resume_inputs_match(run.path, lossy_model, lossy_inputs) + @test_throws ArgumentError extension._resume_run(root, run.path, lossy_model, lossy_inputs) + resumed = extension._resume_run(root, run.path, model, inputs) + @test resumed.path == run.path + extension._transition!(run, extension.completed, "test completion") + preserved = read(joinpath(run.path, "run.json")) + @test !extension._resume_inputs_match(run.path, model, inputs) + @test_throws ArgumentError extension._resume_run(root, run.path, model, inputs) + @test read(joinpath(run.path, "run.json")) == preserved + next_run = extension._resume_run(root, :latest, model, inputs) + @test next_run.path != run.path + @test read(joinpath(run.path, "run.json")) == preserved + # Completed results can be read only when their numerical files are + # protected. This does not transition or rewrite their run state. + for name in ("Z.tsv", "P.tsv", "scan_complete.tsv") + write(joinpath(run.path, "raw", name), "checksum fixture $name") + end + scan = extension.FEMScan(zeros(ComplexF64, 1, 1, 2), zeros(ComplexF64, 1, 1, 2), String[]) + extension._write_scan_checksums(run, scan) + retained_inputs = merge(inputs, (; getdp_identity=(sha256="fixture", info="fixture"))) + extension._write_json_atomic(joinpath(run.path, "input", "computation.json"), retained_inputs) + @test extension._resume_inputs_match(run.path, model, retained_inputs) + @test !extension._resume_inputs_match( + run.path, model, merge(retained_inputs, (; getdp_identity = nothing))) + retained = extension._resume_run(root, run.path, model, retained_inputs) + @test retained.state === extension.completed + @test retained.path == run.path + @test read(joinpath(run.path, "run.json")) == preserved + @test validate(scan, retained) === scan + write(joinpath(run.path, "raw", "Z.tsv"), "corrupted numerical payload") + @test_throws LineCableModelsFEMError validate(scan, retained) + write(joinpath(run.path, "raw", "checksums.json"), "broken JSON") + @test_throws LineCableModelsFEMError validate(scan, retained) + changed = merge(retained_inputs, (; + adapter_sources = Dict("geometry.jl"=>"different implementation"))) + @test !extension._resume_inputs_match(run.path, model, changed) + @test !extension._resume_inputs_match(run.path, model, merge(retained_inputs, (; + owned_gmsh = false))) + for execution in + (merge(inputs.execution, (ui=true,)), + merge(inputs.execution, (mesh_mode=:remesh,))) + excluded = merge(retained_inputs, (; execution)) + extension._write_json_atomic(joinpath(run.path, "input", "computation.json"), excluded) + @test !extension._resume_inputs_match(run.path, model, excluded) + end + if Sys.isunix() + executable = joinpath(root, "getdp-identity") + write(executable, "#!/bin/sh\necho 'GetDP Version 3.6.0 fixture A'\n") + chmod(executable, 0o700) + configured = Formulation(:fem; options=formulation.options) + configured_controls = (getdp_executable=executable, gmsh_verbosity=0,) + first_record = extension._fem_input_record(model, configured, computation_options(LineCableModelsFEM, ComputationOptions(configured_controls))) + write(executable, "#!/bin/sh\necho 'GetDP Version 3.6.0 fixture B'\n") + second_record = extension._fem_input_record(model, configured, computation_options(LineCableModelsFEM, ComputationOptions(configured_controls))) + @test first_record.getdp_selection.path == second_record.getdp_selection.path + @test first_record.getdp_selection.source === :explicit + @test first_record.getdp_identity.sha256 != second_record.getdp_identity.sha256 + @test first_record.getdp_identity.info != second_record.getdp_identity.info + + environment = joinpath(root, "getdp-environment") + cp(executable, environment) + chmod(environment, 0o700) + relocated = Formulation(:fem; options=formulation.options) + relocated_controls = (getdp_executable=environment, gmsh_verbosity=0,) + relocated_inputs = extension._fem_input_record(model, relocated, computation_options(LineCableModelsFEM, ComputationOptions(relocated_controls))) + @test relocated_inputs.getdp_identity == second_record.getdp_identity + @test relocated_inputs.getdp_selection.path != second_record.getdp_selection.path + relocation_run = extension._create_run(root) + extension._write_run_inputs!(relocation_run, model) + extension._write_json_atomic( + joinpath(relocation_run.path, "input", "computation.json"), second_record) + @test extension._resume_inputs_match(relocation_run.path, model, relocated_inputs) + @test extension._resolve_getdp(computation_options(LineCableModelsFEM, ComputationOptions(relocated_controls)), relocation_run) == realpath(environment) + withenv("LINECABLEMODELS_GETDP"=>environment) do + selected = extension._getdp_selection(computation_options(LineCableModelsFEM, ComputationOptions((;)))) + @test selected.source === :environment + @test selected.path == realpath(environment) + explicit = extension._getdp_selection(computation_options(LineCableModelsFEM, ComputationOptions(configured_controls))) + @test explicit.source === :explicit + @test explicit.path == realpath(executable) + end + write(environment, "#!/bin/sh\necho 'GetDP Version 3.6.0 changed binary'\n") + @test_throws LineCableModelsFEMError extension._resolve_getdp(computation_options(LineCableModelsFEM, ComputationOptions(relocated_controls)), relocation_run) + @test !extension._resume_inputs_match(relocation_run.path, model, + extension._fem_input_record(model, relocated, computation_options(LineCableModelsFEM, ComputationOptions(relocated_controls)))) + end + end +end diff --git a/test/extensions/fem_transport.jl b/test/extensions/fem_transport.jl new file mode 100644 index 000000000..3cc931a97 --- /dev/null +++ b/test/extensions/fem_transport.jl @@ -0,0 +1,50 @@ +@testitem "Gmsh FEM / data-only GetDP input transport" tags=[:extension, :fem] begin + using LineCableModels, Gmsh + + extension = Base.get_extension(LineCableModels, :LineCableModelsGmshExt) + copper = Material(kind = :conductor, rho = 1.72e-8, mu_r = 0.999994) + dielectric = Material(kind = :insulator, rho = 1e8, eps_r = 2.3, tan_delta = 0.025) + design = build(CableDesign, + "transport", + Stack( + Group(:core, Region(:metal, Disk(0.005), copper)), + Region(:insulation, Shell(0.005), dielectric))) + system = build(LineCableSystem, [design, design], [(0.0, -0.1), (0.1, -0.1)]; + connections = [Dict(:core=>1), Dict(:core=>2)]) + for method in (:default, :lossy) + formulation = Formulation(:fem; insulation_admittance = method, + options = (ideal_transposition = false,)) + line_counts = Int[] + for frequencies in ([50.0, 1000.0], collect(10.0 .^ range(-1, 6; length = 101))) + problem = LineParametersProblem(system; frequencies, + earth_props = LineCableModels.Earth.EarthModel(100.0, 10.0, 1.0)) + model = extension._resolved_fem_model(problem, formulation) + mktempdir() do directory + path = joinpath(directory, "model_data.pro") + @test extension._write_model_data(path, model) == path + @test readdir(directory) == ["model_data.pro"] + text = read(path, String) + push!(line_counts, length(readlines(path))) + # Serialized inputs must contain data without solver or output instructions. + @test !occursin(r"(?m)^\s*(Group|Function|Macro|PostOperation|For|If|Include)\b", text) + @test !occursin("MaterialSigma()", text) + @test !occursin("MaterialEpsilon()", text) + @test !occursin("MaterialTanDelta", text) + for (index, material) in enumerate(model.material_plans) + for (name, expected) in ( + ("MaterialSigma_$index", real.(material.admittivity)), + ("MaterialEpsilon_$index", + imag.(material.admittivity) ./ (2π .* frequencies))) + entry = match(Regex(name * raw"\(\) = \{([^}]*)\};"), text) + @test entry !== nothing + @test parse.(Float64, split(entry[1], ',')) == expected + end + end + @test extension._write_model_data(path, model) == path + @test read(path, String) == text + end + end + # More samples add numerical array entries, never per-job declarations. + @test first(line_counts) == last(line_counts) + end +end diff --git a/test/extensions/fem_ui.jl b/test/extensions/fem_ui.jl new file mode 100644 index 000000000..f6b2c2d8e --- /dev/null +++ b/test/extensions/fem_ui.jl @@ -0,0 +1,123 @@ +@testitem "Gmsh FEM / UI maps and cancellation preserve session ownership" tags=[:extension,:fem_numerical,:fem] begin + using Gmsh, JSON3 + if isempty(get(ENV,"DISPLAY","")) && + !(Sys.islinux() && get(ENV,"LINECABLEMODELS_FEM_UI_CASE","") == "unavailable_display") + @test_skip "An accessible display is required for UI execution" + elseif !haskey(ENV,"LINECABLEMODELS_FEM_UI_CASE") + # FLTK/Gmsh retain native GUI state after finalize. Exercise each window closure + # scenario in a fresh process, including when other tests used the GUI. + actions=("before_mesh","before_solve","during_solve","complete") + Sys.islinux() && (actions=(actions...,"unavailable_display")) + for action in actions + runner=joinpath(pkgdir(LineCableModels),"test","runtests.jl") + command=`$(Base.julia_cmd()) --startup-file=no --project=$(dirname(Base.active_project())) $runner extensions/fem_ui.jl` + action == "unavailable_display" && (command=addenv(command,"DISPLAY"=>"")) + @test success(addenv(command,"LINECABLEMODELS_FEM_UI_CASE"=>action)) + end + else + E=Base.get_extension(LineCableModels,:LineCableModelsGmshExt) + gmsh=Gmsh.gmsh + copper=Material(kind=:conductor,rho=1.72e-8) + dielectric=Material(kind=:insulator,rho=1e8,eps_r=2.3) + design=build(CableDesign,"ui-worker",Stack( + Group(:core,Region(:metal,Disk(0.005),copper)), + Region(:insulation,Annulus(0.005,0.01),dielectric), + Group(:sheath,Region(:shield,Annulus(0.01,0.011),copper)))) + system=build(LineCableSystem,design,(0.0,-0.1);connections=Dict(:core=>1,:sheath=>0)) + problem=LineParametersProblem(system;frequencies=[50.0,1000.0], + earth_props=homogeneous(rho=100.0,eps_r=10.0)) + form=Formulation(:fem;options=(ideal_transposition=false,)) + form_controls = (ui=true,plot_field_maps=true,keep_run_directory=true, + gmsh_verbosity=0,getdp_verbosity=0) + function await_condition(predicate) + deadline=time()+90 + while !predicate() + time()!isempty(gmsh.onelab.get_string(E._onelab_name("ui/run_directory")))) + run_path[]=only(gmsh.onelab.get_string(E._onelab_name("ui/run_directory"))) + await_condition(()->Bool(gmsh.fltk.is_available())) + if action !== :before_mesh + gmsh.onelab.set_string(E._onelab_name("ui/action"),["generate_mesh"]) + await_condition(()->gmsh.onelab.get_string(E._onelab_name("ui/mesh_state"))==["ready"]) + end + if action in (:during_solve,:complete) + gmsh.onelab.set_string(E._onelab_name("ui/action"),["run_model"]) + if action === :during_solve + await_condition(()->JSON3.read(read(joinpath(run_path[],"run.json"),String)).getdp_invocations>0) + else + await_condition(()->gmsh.onelab.get_string(E._onelab_name("ui/solve_state"))==["completed"]) + @test only(gmsh.onelab.get_number(E._onelab_name("ui/completed_columns")))==4 + @test only(gmsh.onelab.get_number(E._onelab_name("ui/completed_frequencies")))==2 + @test length(gmsh.view.get_tags())==36 + end + end + gmsh.fltk.finalize() + catch + gmsh.fltk.finalize() + rethrow() + end + result=try + compute(problem,form; options=merge(form_controls, (trace=true,))) + catch exception + exception + end + try + wait(driver) + @test Bool(gmsh.is_initialized()) + @test gmsh.model.get_current()=="caller-owned-ui-model" + @test gmsh.onelab.get_string("caller-owned-parameter")==["preserve me"] + @test isempty(gmsh.view.get_tags()) + if action===:complete + @test result isa LineParameters + @test length(result.details.data.fem.run.map_paths)==36 + @test all(isfile,result.details.data.fem.run.map_paths) + else + @test result isa LineCableModelsFEMError + @test result.category== (action===:during_solve ? :canceled : :not_executed) + state=JSON3.read(read(joinpath(run_path[],"run.json"),String)) + @test state.state== (action===:during_solve ? "canceled" : "not_executed") + @test E._live_attempt(E.FEMRun(run_path[],E.canceled,"test",:none,""))===nothing + end + finally + Gmsh.finalize() + isempty(run_path[]) || rm(run_path[];recursive=true,force=true) + end + end + end +end diff --git a/test/extensions/fem_workers.jl b/test/extensions/fem_workers.jl new file mode 100644 index 000000000..7e598f13e --- /dev/null +++ b/test/extensions/fem_workers.jl @@ -0,0 +1,177 @@ +@testitem "Gmsh FEM / isolated workers, checkpoints and recovery" tags=[:extension, :fem] begin + using Gmsh, JSON3, SHA + E = Base.get_extension(LineCableModels, :LineCableModelsGmshExt) + python = Sys.which("python3") + if Sys.isunix() && python !== nothing + copper = Material(kind=:conductor, rho=1.72e-8) + design = build(CableDesign, "worker-fixture", terminal(:core, core(copper; r=0.005))) + system = build(LineCableSystem, [design,design], [(0.0,-0.1),(0.1,-0.1)]; + connections=[Dict(:core=>1),Dict(:core=>2)]) + problem = LineParametersProblem(system; frequencies=[50.0,1000.0], + earth_props=homogeneous(rho=100.0,eps_r=10.0)) + mktempdir(;prefix="fem workers with spaces ") do root + config = joinpath(root,"control.json") + executable = joinpath(root,"getdp fixture") + write(executable,"#!$python\ncontrol_path = " * JSON3.write(config) * "\n" * raw""" +import json, sys, pathlib, re, time, os +if sys.argv[1:] == ['-info']: + print('GetDP Version 3.6.0 worker protocol fixture'); sys.exit(0) +args=sys.argv[1:]; settings={} +for i,a in enumerate(args): + if a in ('-setstring','-setnumber'): settings[args[i+1]]=args[i+2] +control=json.loads(pathlib.Path(control_path).read_text()) +if control.get('unsupported'): + print('Unknown operation GenerateRHSGroup',flush=True);sys.exit(2) +root=pathlib.Path(settings['RunDirectory']) +f=int(settings['FrequencyIndex']); hz=float(settings['FrequencyHz']) +bases=[int(x) for x in re.search(r'\{([^}]*)\}',pathlib.Path(settings['BasisListPath']).read_text())[1].split(',')] +assert os.getcwd()==str(root) +assert all(os.environ[x]=='1' for x in ('OPENBLAS_NUM_THREADS','OMP_NUM_THREADS','MKL_NUM_THREADS')) +(root/'observed.json').write_text(json.dumps({'frequency':f,'bases':bases,'pid':os.getpid()})) +for b in bases: + time.sleep(control.get('delay',0.0)*(3-f)) + stem=root/'raw'/'jobs'/f'getdp-f{f:04d}-b{b:04d}' + for q in ('Z','P'): + text=''.join(f'{f}\t{hz:.17g}\t{r}\t{b}\t{100*f+10*r+b}\t{r-b}\n' for r in (1,2)) + pathlib.Path(str(stem)+f'-{q}.tsv').write_text(text) + pathlib.Path(str(stem)+'-timing.tsv').write_text(f'{f}\t{b}\t0\t0\t0\t0\t{int(b==bases[0])}\n') + if control.get('fail') and f==1 and b==2: + print('deliberate failure after partial column',flush=True); sys.exit(17) + pathlib.Path(str(stem)+'.done').write_text(f'2\t{f}\t{hz:.17g}\t{b}\t2\t0\n') +""") + chmod(executable,0o700) + form = Formulation(:fem; options=(ideal_transposition=false,)) + form_controls = (getdp_executable=executable,frequency_workers=2,solver_threads=1,gmsh_verbosity=0) + execution_options = computation_options(LineCableModelsFEM, ComputationOptions(form_controls)) + model=E._resolved_fem_model(problem,form) + inputs=E._fem_input_record(model,form, execution_options) + execution=inputs.execution + meshes=map(1:2) do i + path=joinpath(root,"mesh $i.msh") + write(path,"mesh $i") + path + end + function fresh() + run=E._create_run(root) + E._write_json_atomic(joinpath(run.path,"input/computation.json"),inputs) + E._write_run_inputs!(run,model) + run + end + write(config,JSON3.write((delay=0.02,fail=false))) + run=fresh() + # Resume identity checks accept the same executable and reject a + # changed identity before starting a worker. + @test E._resolve_getdp(execution_options, run) == realpath(executable) + input_path = joinpath(run.path, "input/computation.json") + changed_identity = merge(inputs.getdp_identity, (sha256="changed",)) + E._write_json_atomic(input_path, merge(inputs, (getdp_identity=changed_identity,))) + @test_throws LineCableModelsFEMError E._resolve_getdp(execution_options, run) + E._write_json_atomic(input_path, inputs) + owner=E._claim_run(run) + @test_throws LineCableModelsFEMError E._claim_run(run) + lock_probe=raw""" +import fcntl,sys +with open(sys.argv[1],'a+') as f: + try: fcntl.flock(f,fcntl.LOCK_EX|fcntl.LOCK_NB);print('acquired') + except BlockingIOError: print('busy') +""" + lock_command=`$python -c $lock_probe $(joinpath(run.path,"coordinator.lock"))` + @test strip(read(lock_command,String))=="busy" + E._release_run(owner) + @test strip(read(lock_command,String))=="acquired" + E._release_run(E._claim_run(run)) + E._run_getdp!(run,model,form, execution_options,meshes) + @test (run.getdp_invocations,run.completed_columns,run.completed_frequencies)==(2,4,2) + scan=E._parse_scan(run,model,form, execution_options) + @test real.(scan.Z[:,:,1])==[111 112;121 122] + @test real.(scan.Z[:,:,2])==[211 212;221 222] + digest=bytes2hex(open(sha256,meshes[1])) + @test E._valid_column_checkpoint(run.path,1,50.0,2,2,false,digest) + @test !E._valid_column_checkpoint(run.path,1,50.0,2,2,false,"different mesh") + @test !E._valid_column_checkpoint(run.path,1,50.0,2,2,true,digest) + paths=E._column_paths(run.path,1,2,false) + checkpoint = read(paths.checkpoint, String) + record = Dict(pairs(JSON3.read(checkpoint))) + for malformed in ("{", "{}", "[]", "null", "1", + (JSON3.write(merge(record, Dict(pairs(patch)))) for patch in ( + (frequency_hz="50",), (frequency_hz=nothing,), (frequency_index=true,), + (protocol=2.5,), (checksums=nothing,), (checksums=[],), + (plot_field_maps=0,), (mesh_digest=nothing,)))...) + write(paths.checkpoint, malformed) + @test !E._valid_column_checkpoint(run.path,1,50.0,2,2,false,digest) + end + write(paths.checkpoint, checkpoint) + @test E._valid_column_checkpoint(run.path,1,50.0,2,2,false,digest) + before=read(paths.Z) + write(paths.Z,"broken\n") + @test !E._valid_column_checkpoint(run.path,1,50.0,2,2,false,digest) + # The intact attempt is adopted, without starting another solver. + E._run_getdp!(run,model,form, execution_options,meshes) + @test run.getdp_invocations==2 + @test read(paths.Z)==before + # A corrupt manifest cannot prevent adopting other attempts. + for (kind,content) in (("object","{}"),("array","[]"),("scalar","1")) + directory=joinpath(run.path,"attempts","malformed_"*kind) + mkpath(directory) + write(joinpath(directory,"attempt.json"),content) + end + @test E._live_attempt(run)===nothing + # Lose both the completed frequency column and its completed attempt marker. + rm(paths.checkpoint) + for attempt in filter(isdir,readdir(joinpath(run.path,"attempts");join=true)) + rm(E._column_paths(attempt,1,2,false).marker;force=true) + end + E._run_getdp!(run,model,form, execution_options,meshes) + @test run.getdp_invocations==3 + attempts=filter(isfile,[joinpath(d,"observed.json") for d in readdir(joinpath(run.path,"attempts");join=true)]) + @test any(p->JSON3.read(read(p,String)).bases==[2],attempts) + @test E._parse_scan(run,model,form, execution_options).Z==scan.Z + # Concurrency is scheduling metadata. Thread count records numerical execution. + one=merge(inputs,(execution=merge(execution,(frequency_workers=1,)),)) + threads=merge(inputs,(execution=merge(execution,(solver_threads=2,)),)) + @test E._resume_inputs_match(run.path,model,one) + @test !E._resume_inputs_match(run.path,model,threads) + serial=fresh() + serial_form=Formulation(:fem;options=form.options) + serial_form_controls = merge(execution,(frequency_workers=1,)) + E._run_getdp!(serial,model,serial_form, computation_options(LineCableModelsFEM, ComputationOptions(serial_form_controls)),meshes) + @test E._parse_scan(serial,model,serial_form, computation_options(LineCableModelsFEM, ComputationOptions(serial_form_controls))).Z==scan.Z + # A failed basis does not get a checkpoint and must be retried. + broken=fresh();write(config,JSON3.write((delay=0.01,fail=true))) + @test_throws LineCableModelsFEMError E._run_getdp!(broken,model,form, execution_options,meshes) + @test isfile(E._column_paths(broken.path,1,1,false).checkpoint) + @test !isfile(E._column_paths(broken.path,1,2,false).checkpoint) + @test E._live_attempt(broken)===nothing + write(config,JSON3.write((delay=0.0,fail=false))) + E._run_getdp!(broken,model,form, execution_options,meshes) + @test E._parse_scan(broken,model,form, execution_options).Z==scan.Z + unsupported=fresh();write(config,JSON3.write((unsupported=true,))) + failure=try + E._run_getdp!(unsupported,model,form, execution_options,meshes) + nothing + catch exception + exception + end + @test failure isa LineCableModelsFEMError + @test failure.field===:capability + @test E._live_attempt(unsupported)===nothing + # Cancellation kills and reaps workers and does not report completion. + stopped=fresh();write(config,JSON3.write((delay=2.0,fail=false))) + @test_throws LineCableModelsFEMError E._run_getdp!(stopped,model,form, execution_options,meshes; + pump=()->stopped.getdp_invocations==0) + @test stopped.state===E.canceled + @test E._live_attempt(stopped)===nothing + @test_throws LineCableModelsFEMError E._parse_scan(stopped,model,form, execution_options) + # A surviving child from a crashed coordinator blocks retry. + orphan=joinpath(stopped.path,"attempts","orphan");mkpath(orphan) + E._write_json_atomic(joinpath(orphan,"attempt.json"), + (state="running",pid=getpid(),process_token=E._process_token(getpid()))) + @test E._live_attempt(stopped) !== nothing + E._write_json_atomic(joinpath(orphan,"attempt.json"), + (state="running",pid=getpid(),process_token="old boot or reused PID")) + Sys.islinux() && @test E._live_attempt(stopped)===nothing + end + else + @test_skip "The worker process fixture requires Unix and python3" + end +end diff --git a/test/extensions/gmsh.jl b/test/extensions/gmsh.jl new file mode 100644 index 000000000..1e87c8989 --- /dev/null +++ b/test/extensions/gmsh.jl @@ -0,0 +1,1420 @@ +@testitem "Gmsh FEM / public API, strict parsing, and UI transitions" tags=[:extension, :fem] begin + import LineCableModels + using Gmsh + using LinearAlgebra + + extension_module = Base.get_extension( + LineCableModels, :LineCableModelsGmshExt + ) + @test extension_module !== nothing + @test LineCableModels.LineCableModelsFEM <: + LineCableModels.AbstractFormulation + @test supertype(LineCableModels.LineCableModelsFEM) === LineCableModels.AbstractFormulation + mktempdir() do directory + run = extension_module.FEMRun( + directory, extension_module.created, "fixture", :none, "" + ) + mesh_source = joinpath(directory, "source.msh") + mesh_snapshot = joinpath(directory, "snapshot.msh") + write(mesh_source, "mesh snapshot fixture") + extension_module._copy_or_link_mesh(mesh_source, mesh_snapshot) + @test read(mesh_snapshot, String) == "mesh snapshot fixture" + Sys.iswindows() || @test Base.Filesystem.samefile( + mesh_source, mesh_snapshot + ) + end + + options = computation_options(LineCableModelsFEM, ComputationOptions((;mesh_mode = :remesh, + keep_run_directory = true, + gmsh_verbosity = 0, + getdp_verbosity = 5))) + @test options.data.mesh_mode === :remesh + @test options.data.keep_run_directory + @test options.data.getdp_verbosity == 5 + @test options.data.frequency_workers == 2 + @test options.data.solver_threads == 1 + @test_throws ArgumentError computation_options(LineCableModelsFEM, ComputationOptions((;frequency_workers = 0))) + @test_throws ArgumentError computation_options(LineCableModelsFEM, ComputationOptions((;solver_threads = -1))) + @test_throws ArgumentError computation_options(LineCableModelsFEM, ComputationOptions((;mesh_mode = :invalid))) + @test_throws ArgumentError computation_options(LineCableModelsFEM, ComputationOptions((;getdp_verbosity = 6))) + resume_options = computation_options(LineCableModelsFEM, ComputationOptions((trace = true, resume_run_directory = :latest))) + @test resume_options.data.trace === Val(true) + @test resume_options.data.resume_run_directory === :latest + @test_throws ArgumentError computation_options(LineCableModelsFEM, ComputationOptions((resume_run_directory = :invalid,))) + @test extension_module._resume_value_matches( + Dict("first" => 1, "second" => [2.0, 3.0]), + Dict("second" => [2.0, 3.0], "first" => 1) + ) + + formulation = LineCableModels.Formulation( + :fem; + options = (ideal_transposition = false,)) + @test formulation isa LineCableModels.LineCableModelsFEM + @test !formulation.options.data.ideal_transposition + + transition = extension_module._ui_transition + @test transition(:geometry_ready, :run_model) === :mesh_required + @test transition(:geometry_ready, :generate_mesh) === :mesh_requested + @test transition(:mesh_ready, :run_model) === :solve_requested + @test transition(:geometry_ready, Symbol("")) === :geometry_ready + @test transition(:geometry_ready, Symbol(""), false) === :closed_before_mesh + @test transition(:mesh_ready, Symbol(""), false) === :closed_before_solve + @test extension_module._ui_state_labels(:geometry_ready) == ( + mesh_state = "not generated", solve_state = "not run" + ) + @test extension_module._ui_state_labels(:mesh_ready) == ( + mesh_state = "ready", solve_state = "not run" + ) + @test extension_module._ui_state_labels(:results_ready) == ( + mesh_state = "ready", solve_state = "completed" + ) + + mktempdir() do directory + run = extension_module.FEMRun( + directory, extension_module.created, "fixture", :none, "" + ) + path = joinpath(directory, "Z.tsv") + header = join(extension_module.FEM_RAW_HEADER, '\t') + rows = [ + "1\t50\t1\t1\t1\t2", + "1\t50\t2\t1\t3\t4", + "1\t50\t1\t2\t5\t6", + "1\t50\t2\t2\t7\t8" + ] + open(path, "w") do io + println(io, header) + println.(Ref(io), rows) + end + matrix = extension_module._parse_raw_matrix( + Float64, path, [50.0], 2, run + ) + @test size(matrix) == (2, 2, 1) + @test matrix[1, 1, 1] == 1 + 2im + @test matrix[2, 1, 1] == 3 + 4im + @test matrix[1, 2, 1] == 5 + 6im + @test matrix[2, 2, 1] == 7 + 8im + + job_path = joinpath(directory, "getdp-f0001-b0001-Z.tsv") + open(job_path, "w") do io + println(io, rows[1]) + println(io, rows[2]) + end + @test extension_module._valid_job_raw(job_path, 2, 1, 50.0, 1) + open(job_path, "w") do io + println(io, rows[1]) + println(io, rows[1]) + end + @test !extension_module._valid_job_raw(job_path, 2, 1, 50.0, 1) + + open(path, "w") do io + println(io, header) + println.(Ref(io), [rows[1], rows[1], rows[3], rows[4]]) + end + @test_throws LineCableModels.LineCableModelsFEMError extension_module._parse_raw_matrix( + Float64, path, [50.0], 2, run + ) + + extension_module._transition!(run, extension_module.running, "fixture running") + @test run.state === extension_module.running + @test occursin("\"state\": \"running\"", read( + joinpath(directory, "run.json"), String + )) + missing_completion = try + validate(joinpath(directory, "missing.tsv"), run, 1, 2) + nothing + catch exception + exception + end + @test missing_completion isa LineCableModels.LineCableModelsFEMError + @test missing_completion.run_directory == directory + + open(path, "w") do io + println(io, header) + println.(Ref(io), ["1\t50\t1\t1\tNaN\t0", rows[2], rows[3], rows[4]]) + end + @test_throws LineCableModels.LineCableModelsFEMError extension_module._parse_raw_matrix( + Float64, path, [50.0], 2, run + ) + end +end + +@testitem "Gmsh FEM / nominal Float64 preflight" tags=[:extension, :fem] begin + using Gmsh + using LineCableModels + using Measurements + + extension_module = Base.get_extension(LineCableModels, :LineCableModelsGmshExt) + @test extension_module !== nothing + + radius = measurement(0.005, 1.0e-6) + copper = Material(kind = :conductor, rho = 1 / 5.8e7) + design = build( + CableDesign, + "fem-preflight", + Group(:core, Region(:core_metal, Disk(radius), copper)) + ) + system = build( + LineCableSystem, + design, + (0.0, -0.1); + connections = Dict(:core => 1), + system_id = "fem-preflight", + line_length = 1.0 + ) + problem = LineParametersProblem( + system; + temperature = measurement(35.0, 0.5), + earth_props = LineCableModels.Earth.EarthModel(100.0, 10.0, 1.0), + frequencies = [measurement(50.0, 0.25)] + ) + + runtime_root = extension_module._runtime_root() + runs = joinpath(runtime_root, "runs") + before = isdir(runs) ? sort(readdir(runs)) : nothing + @test !Bool(gmsh.is_initialized()) + + normalized = extension_module._preflight_fem_problem(problem) + + @test eltype(problem) === Measurement{Float64} + @test eltype(normalized) === Float64 + @test eltype(normalized.system) === Float64 + @test eltype(only(normalized.system.designs)) === Float64 + @test normalized.temperature === 35.0 + @test normalized.frequencies == [50.0] + @test normalized.earth_props.layers[2].rho === 100.0 + @test only(normalized.system.designs).geometry.regions[1].primitive.r === 0.005 + @test Measurements.uncertainty(problem.temperature) == 0.5 + @test Measurements.uncertainty(only(problem.frequencies)) == 0.25 + @test !Bool(gmsh.is_initialized()) + @test (isdir(runs) ? sort(readdir(runs)) : nothing) == before + + float32_design = build( + CableDesign, + "fem-preflight-f32", + Group( + :core, + Region( + :core_metal, + Disk(0.005f0), + Material(kind = :conductor, rho = Float32(1 / 5.8e7)) + ) + ) + ) + float32_system = build( + LineCableSystem, + float32_design, + (0.0f0, -0.1f0); + connections = Dict(:core => 1), + system_id = "fem-preflight-f32", + line_length = 1.0f0 + ) + float32_problem = LineParametersProblem( + float32_system; + temperature = 20.0f0, + earth_props = LineCableModels.Earth.EarthModel( + 100.0f0, 10.0f0, 1.0f0 + ), + frequencies = Float32[50.0] + ) + promoted = extension_module._preflight_fem_problem(float32_problem) + @test eltype(promoted) === Float64 + @test promoted.frequencies == [50.0] + @test only(promoted.system.designs).geometry.regions[1].primitive.r === + Float64(0.005f0) + + @test !Bool(gmsh.is_initialized()) + @test (isdir(runs) ? sort(readdir(runs)) : nothing) == before +end + +@testitem "Gmsh FEM / deterministic geometry and mesh lifecycle" tags=[:extension, :fem] begin + using LineCableModels + using Gmsh + + copper = Material(kind = :conductor, rho = 1 / 5.8e7) + dielectric = Material(kind = :insulator, rho = 1.0e15, eps_r = 2.3) + design = build(CableDesign, + "fem-mesh", + Stack( + Group(:core, Region(:core_metal, Disk(0.005), copper)), + Region(:dielectric, Annulus(0.005, 0.01), dielectric), + Group(:sheath, Region(:sheath_metal, Annulus(0.01, 0.011), copper)), + Region(:jacket, Annulus(0.011, 0.013), dielectric) + )) + system = build( + LineCableSystem, + design, + (0.0, -0.1); + connections = Dict(:core => 1, :sheath => 0), + system_id = "fem-mesh", + line_length = 1.0 + ) + problem = LineParametersProblem( + system; + earth_props = LineCableModels.Earth.EarthModel(100.0, 10.0, 1.0), + frequencies = [50.0] + ) + formulation = Formulation( + :fem; + options = (ideal_transposition = false,)) + formulation_controls = (gmsh_verbosity = 0,) + extension_module = Base.get_extension(LineCableModels, :LineCableModelsGmshExt) + model = extension_module._resolved_fem_model(problem, formulation) + @test model.terminal_ids == [ + "cable_0001/fem-mesh/core", + "cable_0001/fem-mesh/sheath" + ] + @test getproperty.(model.material_plans, :physical_tag) == 10_001:10_002 + @test getproperty.(model.region_plans, :material_index) == [1, 2, 1, 2] + @test model.tags.terminal_base == 3_000 + @test model.tags.infinite_domain == 1_005 + @test model.tags.outer_air_boundary == 2_004 + @test model.tags.outer_earth_boundary == 2_005 + @test model.shell_outer_radius == 1.25model.domain_radius + @test getproperty.(model.region_plans, :mesh_size) ≈ [ + 0.001, 0.0025, 0.0005, 0.001 + ] + @test model.fine_mesh_size ≈ 0.0005 + @test model.cable_outer_mesh_sizes ≈ [0.001] + @test model.mesh_growth_factor == 1.2 + @test length(model.mesh_plans) == 1 + skin_depth = sqrt(100.0 / (π * 50.0 * 4π * 1e-7)) + @test model.domain_radius ≈ 2skin_depth + @test only(model.mesh_plans).domain_mesh_size ≈ skin_depth / 20 + @test only(model.mesh_plans).infinite_mesh_size ≈ skin_depth / 10 + @test only(model.mesh_plans).cable_interface_mesh_sizes ≈ [ + min( + skin_depth / 20, + 0.001 + 0.2 * (0.1 - 0.013) + ) + ] + smaller_model = extension_module._resolved_fem_model(problem, formulation, + computation_options(LineCableModelsFEM, ComputationOptions((;domain_skin_depths=1.5)))) + smaller, larger = only(smaller_model.mesh_plans), only(model.mesh_plans) + @test smaller.domain_radius ≈ 1.5skin_depth + @test smaller.shell_outer_radius ≈ 1.25smaller.domain_radius + # A larger finite domain retains the conductor, interface and medium + # resolution targets without globally coarsening or refining the mesh. + @test getproperty.(smaller_model.region_plans, :mesh_size) == + getproperty.(model.region_plans, :mesh_size) + for property in (:domain_mesh_size, :infinite_mesh_size, :interface_mesh_size, + :cable_interface_mesh_sizes, :wave_mesh_sizes, :wave_decay_radii) + @test getproperty(smaller, property) == getproperty(larger, property) + end + @test extension_module._mesh_fingerprint(smaller_model, Gmsh.gmsh.GMSH_API_VERSION) != + extension_module._mesh_fingerprint(model, Gmsh.gmsh.GMSH_API_VERSION) + + multifrequency_problem = LineParametersProblem( + system; + earth_props = LineCableModels.Earth.EarthModel(100.0, 10.0, 1.0), + frequencies = [50.0, 1000.0] + ) + multifrequency_model = extension_module._resolved_fem_model( + multifrequency_problem, formulation + ) + @test getproperty.(multifrequency_model.mesh_plans, :frequency) == [ + 50.0, 1000.0 + ] + @test multifrequency_model.mesh_plans[1].domain_radius > + multifrequency_model.mesh_plans[2].domain_radius + @test multifrequency_model.domain_radius == + multifrequency_model.mesh_plans[2].domain_radius + + matrix = Material(kind = :insulator, rho = Inf, eps_r = 1.0) + sector_core = Stack( + Group(:core, Region(:core_center, Disk(0.001), copper)), + Group( + :core, + Region( + :core_sectors, + Sector(span = pi / 3, r_base = 0.001, r_back = 0.002), + copper + ); + pattern = Ring(6; r = 0.0) + ) + ) + filled_design = build( + CableDesign, + "fem-filled-sector", + Enclosure( + :matrix, + sector_core; + primitive = Disk(0.002), + fill = matrix + ), + Region(:outer_insulation, Annulus(0.002, 0.003), dielectric) + ) + filled_system = build( + LineCableSystem, + filled_design, + (0.0, -0.1); + connections = Dict(:core => 1), + system_id = "fem-filled-sector", + line_length = 1.0 + ) + filled_problem = LineParametersProblem( + filled_system; + earth_props = LineCableModels.Earth.EarthModel(100.0, 10.0, 1.0), + frequencies = [50.0] + ) + filled_model = extension_module._resolved_fem_model( + filled_problem, formulation + ) + @test length(filled_model.region_plans) == 9 + @test count(plan -> plan.field === :matrix_fill, + filled_model.material_plans) == 1 + + armor_wire_radius = 0.002 + armor_inner_radius = 0.010 + armor_outer_radius = 0.014 + tangent_fill_design = build( + CableDesign, + "fem-tangent-circular-fill", + Stack( + Region(:inner_bedding, Disk(armor_inner_radius), dielectric), + Enclosure( + :armor_matrix, + Group( + :armor, + Region(:armor_wires, Disk(armor_wire_radius), copper); + pattern = Ring(8; r = 0.012) + ); + primitive = Annulus(armor_inner_radius, armor_outer_radius), + fill = matrix + ), + Region( + :outer_jacket, + Annulus(armor_outer_radius, 0.016), + dielectric + ) + ) + ) + tangent_fill_system = build( + LineCableSystem, + tangent_fill_design, + (0.0, -0.1); + connections = Dict(:armor => 1), + system_id = "fem-tangent-circular-fill", + line_length = 1.0 + ) + tangent_fill_problem = LineParametersProblem( + tangent_fill_system; + earth_props = LineCableModels.Earth.EarthModel(100.0, 10.0, 1.0), + frequencies = [50.0] + ) + tangent_fill_model = extension_module._resolved_fem_model( + tangent_fill_problem, formulation + ) + tangent_fill_index = findfirst( + plan -> plan.field === :armor_matrix_fill, + tangent_fill_model.material_plans + ) + @test tangent_fill_index !== nothing + + tape_inner_radius = armor_outer_radius + tape_outer_radius = tape_inner_radius + 0.0002 + mixed_fill_design = build( + CableDesign, + "fem-tangent-fill-with-tape", + Stack( + Region(:inner_bedding, Disk(armor_inner_radius), dielectric), + Enclosure( + :screen_matrix, + Stack( + Group( + :screen, + Region(:screen_wires, Disk(armor_wire_radius), copper); + pattern = Ring(8; r = 0.012) + ), + Group( + :screen, + Region( + :screen_tape, + Rectangle( + 0.4 * (tape_inner_radius + tape_outer_radius) / 2, + tape_outer_radius - tape_inner_radius + ), + copper + ); + pattern = Ring( + 1; + r = (tape_inner_radius + tape_outer_radius) / 2 + ) + ) + ); + primitive = Annulus(armor_inner_radius, tape_outer_radius), + fill = matrix + ), + Region( + :outer_jacket, + Annulus(tape_outer_radius, 0.016), + dielectric + ) + ) + ) + mixed_fill_system = build( + LineCableSystem, + mixed_fill_design, + (0.0, -0.1); + connections = Dict(:screen => 1), + system_id = "fem-tangent-fill-with-tape", + line_length = 1.0 + ) + mixed_fill_problem = LineParametersProblem( + mixed_fill_system; + earth_props = LineCableModels.Earth.EarthModel(100.0, 10.0, 1.0), + frequencies = [50.0] + ) + mixed_fill_model = extension_module._resolved_fem_model( + mixed_fill_problem, formulation + ) + mixed_fill_index = findfirst( + plan -> plan.field === :screen_matrix_fill, + mixed_fill_model.material_plans + ) + @test mixed_fill_index !== nothing + + incomplete_design = build( + CableDesign, + "fem-incomplete-partition", + Group(:core, Region(:core_metal, Disk(0.001), copper)), + Region(:outer_insulation, Annulus(0.002, 0.003), dielectric) + ) + incomplete_system = build( + LineCableSystem, + incomplete_design, + (0.0, -0.1); + connections = Dict(:core => 1), + system_id = "fem-incomplete-partition", + line_length = 1.0 + ) + incomplete_problem = LineParametersProblem( + incomplete_system; + earth_props = LineCableModels.Earth.EarthModel(100.0, 10.0, 1.0), + frequencies = [50.0] + ) + partition_error = try + extension_module._resolved_fem_model(incomplete_problem, formulation) + nothing + catch exception + exception + end + @test partition_error isa LineCableModelsFEMError + @test partition_error.category === :adaptation + @test partition_error.field === :material_partition + + + vertical_problem = LineParametersProblem( + system; + earth_props = LineCableModels.Earth.EarthModel( + 100.0, 10.0, 1.0; vertical_layers = true + ), + frequencies = [50.0] + ) + vertical_error = try + extension_module._resolved_fem_model(vertical_problem, formulation) + nothing + catch exception + exception + end + @test vertical_error isa LineCableModelsFEMError + @test vertical_error.category === :unsupported + @test vertical_error.field === :vertical_layers + + Gmsh.initialize(String[]; finalize_atexit = false) + gmsh.model.add("caller-owned") + caller_view = gmsh.view.add("caller-view") + gmsh.option.set_number("General.Terminal", 1) + gmsh.option.set_number("General.Verbosity", 4) + gmsh.option.set_number("Geometry.Tolerance", 2.0e-7) + gmsh.option.set_number("Geometry.ToleranceBoolean", 3.0e-7) + gmsh.option.set_string("Solver.SocketName", "caller-owned-socket") + caller_parameter = extension_module._onelab_name("caller_parameter") + gmsh.onelab.set_string(caller_parameter, ["preserve-me"]) + caller_models = Set(String.(gmsh.model.list())) + try + mktempdir() do runtime_root + session = extension_module._start_gmsh(0) + try + @test !session.owned + @test gmsh.option.get_number("Geometry.Tolerance") == 1.0e-8 + @test gmsh.option.get_number("Geometry.ToleranceBoolean") == 0.0 + gmsh.view.add("temporary-view") + gmsh.model.add("fem-exact-curves") + registry = extension_module.FEMLoopRegistry(1.0e-3) + ellipse_loop = extension_module._boundary_loop!( + registry, + Ellipse(0.02, 0.01, Pose2(-0.05, 0.0, π / 7)) + ) + sector_loop = extension_module._boundary_loop!( + registry, + LineCableModels.DataModel.SectorShape( + Sector( + span = π / 3, + r_base = 0.005, + r_back = 0.02, + fillet = 0.001 + ), + Pose2(0.05, 0.0, -π / 11) + ) + ) + gmsh.model.geo.synchronize() + @test length(ellipse_loop.curves) == 4 + @test length(sector_loop.curves) == 10 + @test all( + gmsh.model.get_type(1, curve) == "Ellipse" + for curve in ellipse_loop.curves + ) + @test count( + curve -> gmsh.model.get_type(1, curve) == "Circle", + sector_loop.curves + ) == 8 + @test count( + curve -> gmsh.model.get_type(1, curve) == "Line", + sector_loop.curves + ) == 2 + + gmsh.model.add("fem-zero-inner-strip") + zero_inner_registry = extension_module.FEMLoopRegistry(1.0e-3) + zero_inner_shape = LineCableModels.DataModel.BentStrip( + 0.0, 0.01, π / 3 + ) + extension_module._register_shape_breaks!( + zero_inner_registry, zero_inner_shape + ) + @test all( + key -> !iszero(key[3]), + keys(zero_inner_registry.circle_breaks) + ) + zero_inner_strip = extension_module._boundary_loop!( + zero_inner_registry, + zero_inner_shape + ) + gmsh.model.geo.synchronize() + @test count( + curve -> gmsh.model.get_type(1, curve) == "Circle", + zero_inner_strip.curves + ) >= 1 + @test count( + curve -> gmsh.model.get_type(1, curve) == "Line", + zero_inner_strip.curves + ) == 2 + + gmsh.model.add("fem-shared-circle-arcs") + shared_registry = extension_module.FEMLoopRegistry(1.0e-3) + contact = 0.5343318251524625 + first_contact = extension_module._point!( + shared_registry, (contact, -1.0) + ) + second_contact = extension_module._point!( + shared_registry, (nextfloat(contact), -1.0) + ) + @test first_contact == second_contact + full_circle = Disk(0.02) + partial_sector = LineCableModels.DataModel.SectorShape( + Sector(span = 0.8, r_base = 0.01, r_back = 0.02) + ) + extension_module._register_shape_breaks!( + shared_registry, full_circle + ) + extension_module._register_shape_breaks!( + shared_registry, partial_sector + ) + extension_module._register_circle_contacts!(shared_registry) + full_loop = extension_module._boundary_loop!( + shared_registry, full_circle + ) + partial_loop = extension_module._boundary_loop!( + shared_registry, partial_sector; mesh_size = 2.0e-4 + ) + gmsh.model.geo.synchronize() + shared_curves = intersect(full_loop.curves, partial_loop.curves) + @test length(shared_curves) == 2 + @test all( + point -> shared_registry.point_sizes[point] == 2.0e-4, + Iterators.flatten( + shared_registry.curve_points[curve] + for curve in shared_curves + ) + ) + + gmsh.model.add("fem-tangent-circles") + tangent_registry = extension_module.FEMLoopRegistry(1.0e-3) + first_circle = Disk(0.01, Pose2(-0.01, 0.0, 0.0)) + second_circle = Disk(0.01, Pose2(0.01, 0.0, 0.0)) + extension_module._register_shape_breaks!( + tangent_registry, first_circle + ) + extension_module._register_shape_breaks!( + tangent_registry, second_circle + ) + extension_module._register_circle_contacts!(tangent_registry) + first_loop = extension_module._boundary_loop!( + tangent_registry, first_circle + ) + second_loop = extension_module._boundary_loop!( + tangent_registry, second_circle + ) + gmsh.model.geo.synchronize() + first_points = reduce( + union, + (Set(gmsh.model.get_adjacencies(1, curve)[2]) + for curve in first_loop.curves); + init = Set{Int32}() + ) + second_points = reduce( + union, + (Set(gmsh.model.get_adjacencies(1, curve)[2]) + for curve in second_loop.curves); + init = Set{Int32}() + ) + @test length(intersect(first_points, second_points)) == 1 + + filled_geometry = extension_module._build_geometry!( + filled_model, "fem-filled-sector-geometry" + ) + fill_index = findfirst( + plan -> plan.field === :matrix_fill, + filled_model.material_plans + ) + fill_surfaces = filled_geometry.material_surfaces[fill_index] + fill_curves = Set(extension_module._entity_boundary(fill_surfaces)) + terminal_curves = Set(only(filled_geometry.terminal_curves)) + @test !isempty(intersect(terminal_curves, fill_curves)) + @test all(terminal_curves) do curve + adjacent, _ = gmsh.model.get_adjacencies(1, curve) + length(unique(adjacent)) >= 2 + end + + tangent_fill_geometry = extension_module._build_geometry!( + tangent_fill_model, "fem-tangent-fill-geometry" + ) + tangent_fill_surfaces = tangent_fill_geometry.material_surfaces[ + tangent_fill_index +] + @test length(tangent_fill_surfaces) == 8 + tangent_fill_curves = extension_module._entity_boundary( + tangent_fill_surfaces + ) + @test all(tangent_fill_curves) do curve + adjacent, _ = gmsh.model.get_adjacencies(1, curve) + length(unique(adjacent)) >= 2 + end + minimum_curve_extent = minimum(tangent_fill_curves) do curve + bounds = gmsh.model.get_bounding_box(1, curve) + hypot(bounds[4] - bounds[1], bounds[5] - bounds[2]) + end + @test minimum_curve_extent > 1.0e-8 + gmsh.model.set_current(tangent_fill_geometry.model_name) + extension_module._configure_mesh!( + tangent_fill_model, + tangent_fill_geometry, + only(tangent_fill_model.mesh_plans) + ) + gmsh.model.mesh.generate(2) + @test !isempty(first(gmsh.model.mesh.get_nodes())) + + mixed_fill_geometry = extension_module._build_geometry!( + mixed_fill_model, "fem-tangent-fill-with-tape-geometry" + ) + mixed_fill_surfaces = mixed_fill_geometry.material_surfaces[ + mixed_fill_index +] + @test length(mixed_fill_surfaces) == 9 + gmsh.model.set_current(mixed_fill_geometry.model_name) + extension_module._configure_mesh!( + mixed_fill_model, + mixed_fill_geometry, + only(mixed_fill_model.mesh_plans) + ) + gmsh.model.mesh.generate(2) + @test !isempty(first(gmsh.model.mesh.get_nodes())) + + first_run = extension_module._create_run(runtime_root) + first_geometry = extension_module._build_geometry!(model, "fem-mesh-first") + first_mesh = extension_module._select_mesh!( + first_run, model, first_geometry, computation_options(LineCableModelsFEM, ComputationOptions(formulation_controls)), runtime_root + ) + @test isfile(first_mesh) + @test first_run.mesh_source === :generated + @test !isempty(first_run.mesh_fingerprint) + @test length(first_geometry.infinite_surfaces) == 2 + @test length(first_geometry.inner_shell_curves) == 4 + @test !isempty(first_geometry.outer_air_curves) + @test !isempty(first_geometry.outer_earth_curves) + @test isempty(intersect( + first_geometry.outer_air_curves, + first_geometry.outer_earth_curves + )) + @test sort!(unique([first_geometry.outer_air_curves; + first_geometry.outer_earth_curves])) == + first_geometry.outer_curves + validate(first_mesh, model) + + second_run = extension_module._create_run(runtime_root) + second_geometry = extension_module._build_geometry!(model, "fem-mesh-second") + second_mesh = extension_module._select_mesh!( + second_run, model, second_geometry, computation_options(LineCableModelsFEM, ComputationOptions(formulation_controls)), runtime_root + ) + @test isfile(second_mesh) + @test second_run.mesh_source === :cache + @test second_run.mesh_fingerprint == first_run.mesh_fingerprint + + remesh = Formulation( + :fem; + options = (ideal_transposition = false,)) + remesh_controls = (mesh_mode = :remesh, gmsh_verbosity = 0) + third_run = extension_module._create_run(runtime_root) + third_geometry = extension_module._build_geometry!(model, "fem-mesh-third") + third_mesh = extension_module._select_mesh!( + third_run, model, third_geometry, computation_options(LineCableModelsFEM, ComputationOptions(remesh_controls)), runtime_root + ) + @test isfile(third_mesh) + @test third_run.mesh_source === :generated + + if Sys.isunix() + failing_getdp = joinpath(runtime_root, "getdp-failure") + open(failing_getdp, "w") do io + println(io, "#!/bin/sh") + println(io, "if [ \"\$1\" = \"-info\" ]; then") + println(io, " echo 'GetDP Version 3.5.0'") + println(io, " exit 0") + println(io, "fi") + println(io, "echo 'intentional GetDP failure marker' >&2") + println(io, "exit 17") + end + chmod(failing_getdp, 0o700) + failing_formulation = Formulation( + :fem; + options = (ideal_transposition = false,)) + failing_formulation_controls = ( + getdp_executable = failing_getdp, + gmsh_verbosity = 0, + getdp_verbosity = 0 + ) + failure_run = extension_module._create_run(runtime_root) + extension_module._write_run_inputs!(failure_run, model) + extension_module._write_json_atomic( + joinpath(failure_run.path, "input", "computation.json"), + extension_module._fem_input_record(model, failing_formulation, computation_options(LineCableModelsFEM, ComputationOptions(failing_formulation_controls)))) + failure = try + extension_module._run_getdp!( + failure_run, + model, + failing_formulation, computation_options(LineCableModelsFEM, ComputationOptions(failing_formulation_controls)), + first_mesh + ) + nothing + catch exception + exception + end + @test failure isa LineCableModelsFEMError + @test failure.category === :getdp + @test failure.field === :client + @test occursin("GetDP log tail", failure.message) + @test failure.run_directory == failure_run.path + @test failure_run.getdp_invocations == 1 + @test isdir(failure_run.path) + @test isfile(joinpath(failure_run.path, "logs", "getdp.log")) + end + finally + extension_module._finish_gmsh(session) + end + end + @test Bool(gmsh.is_initialized()) + @test Set(String.(gmsh.model.list())) == caller_models + @test gmsh.model.get_current() == "caller-owned" + @test gmsh.view.get_tags() == [caller_view] + @test gmsh.option.get_number("General.Terminal") == 1 + @test gmsh.option.get_number("General.Verbosity") == 4 + @test gmsh.option.get_number("Geometry.Tolerance") == 2.0e-7 + @test gmsh.option.get_number("Geometry.ToleranceBoolean") == 3.0e-7 + @test gmsh.option.get_string("Solver.SocketName") == + "caller-owned-socket" + @test gmsh.onelab.get_string(caller_parameter) == ["preserve-me"] + finally + Gmsh.finalize() + end +end + +@testitem "Gmsh FEM / bounded formations and complete material ownership" tags=[ + :extension, + :fem, + :slow +] begin + using Gmsh + using LineCableModels + + const DM = LineCableModels.DataModel + extension_module = Base.get_extension(LineCableModels, :LineCableModelsGmshExt) + formulation = Formulation( + :fem; options = (ideal_transposition = false,) + ) + copper = Material( + kind = :conductor, rho = 1.72e-8, eps_r = 1.0, mu_r = 1.0 + ) + dielectric = Material( + kind = :insulator, rho = Inf, eps_r = 2.3, mu_r = 1.0 + ) + + function problem(design, connections, id) + system = build( + LineCableSystem, + design, + Pose2(0.0, -0.1); + connections, + system_id = id, + line_length = 1.0 + ) + return LineParametersProblem( + system; + earth_props = homogeneous(rho = 100.0, eps_r = 10.0), + frequencies = [50.0] + ) + end + + strand_radius = 0.5e-3 + strand_count = 19 + circular_boundary = Disk(sqrt(strand_count) * strand_radius) + circular = stranded( + copper; + shape = Disk(strand_radius), + compact = true, + boundary = circular_boundary + ) + circular_design = build( + CableDesign, + "fem-compacted-circular-core", + terminal(:core, circular), + Region(:insulation, Shell(1e-3), dielectric) + ) + circular_model = extension_module._resolved_fem_model( + problem(circular_design, Dict(:core => 1), circular_design.cable_id), + formulation + ) + @test length(circular_model.region_plans) == 2 + @test circular_model.region_plans[1].shape isa DM.Disk + @test circular_model.region_plans[1].shape.r == circular_boundary.r + @test circular_model.region_plans[1].shape.at == Pose2(0.0, -0.1) + @test circular_model.region_plans[1].terminal_index == 1 + + natural = terminal( + :core, + stranded( + copper; + shape = Disk(strand_radius), + boundary = Disk(3strand_radius), + fill = dielectric + ) + ) + natural_design = build(CableDesign, "fem-natural-circular-core", natural) + natural_model = extension_module._resolved_fem_model( + problem(natural_design, Dict(:core => 1), natural_design.cable_id), + formulation + ) + @test length(natural_model.region_plans) == 8 + @test count( + plan -> plan.shape isa DM.Disk && plan.terminal_index == 1, + natural_model.region_plans + ) == 7 + @test count( + plan -> plan.shape isa DM.DifferenceShape && plan.terminal_index == 0, + natural_model.region_plans + ) == 1 + + partial_boundary = Disk(sqrt(7 / 0.9) * strand_radius) + partial = terminal( + :core, + stranded( + copper; + shape = Disk(strand_radius), + compact = true, + boundary = partial_boundary, + fill = dielectric + ) + ) + filled_design = build( + CableDesign, + "fem-filled-compaction", + partial + ) + filled_model = extension_module._resolved_fem_model( + problem(filled_design, Dict(:core => 1), filled_design.cable_id), + formulation + ) + @test length(filled_model.region_plans) == 8 + @test count( + plan -> plan.shape isa DM.Polygon, + filled_model.region_plans + ) == 7 + @test count( + plan -> plan.shape isa DM.DifferenceShape, + filled_model.region_plans + ) == 1 + + sector = Sector( + span = 2pi / 3, + r_base = 0.6e-3, + r_back = 4.0e-3, + fillet = 0.2e-3 + ) + sector_shape = DM.resolve(DM.EmptyBoundary(), sector) + sector_strand_radius = sqrt(0.9DM.area(sector_shape) / (7pi)) + sector_part = stranded( + copper; + shape = Disk(sector_strand_radius), + boundary = sector + ) + sector_names = (:a, :b, :c) + sectors = assembly(( + at( + terminal(name, sector_part), + 0.15e-3cos(2pi * (index - 1) / 3), + 0.15e-3sin(2pi * (index - 1) / 3); + φ = 2pi * (index - 1) / 3 + ) for (index, name) in enumerate(sector_names) + )...) + sector_design = build( + CableDesign, + "fem-sector-formations", + Enclosure( + :sector_matrix, + sectors; + primitive = Disk(4.5e-3), + fill = dielectric + ) + ) + sector_model = extension_module._resolved_fem_model( + problem( + sector_design, + Dict(:a => 1, :b => 2, :c => 3), + sector_design.cable_id + ), + formulation + ) + @test length(sector_model.region_plans) == 25 + @test count( + plan -> plan.shape isa DM.Polygon, + sector_model.region_plans + ) == 21 + @test [count(==(terminal), getproperty.(sector_model.region_plans, :terminal_index)) + for terminal in 1:3] == [7, 7, 7] + matrix_plan = only(filter( + plan -> plan.terminal_index == 0 && + plan.shape isa DM.DifferenceShape && + plan.shape.outer isa DM.Disk, + sector_model.region_plans + )) + @test count( + hole -> hole isa DM.SectorShape, + matrix_plan.shape.holes + ) == 3 + @test all(hole -> !(hole isa DM.Polygon), matrix_plan.shape.holes) + @test count( + plan -> plan.terminal_index == 0 && + plan.shape isa DM.DifferenceShape && + plan.shape.outer isa DM.SectorShape, + sector_model.region_plans + ) == 3 + + four_sector = Sector( + span = pi / 2, + r_base = 0.6e-3, + r_back = 4.0e-3, + fillet = 0.2e-3 + ) + four_sector_shape = DM.resolve(DM.EmptyBoundary(), four_sector) + four_strand_radius = sqrt(0.9DM.area(four_sector_shape) / (7pi)) + four_part = stranded( + copper; + shape = Disk(four_strand_radius), + boundary = four_sector + ) + four_names = (:d, :e, :f, :g) + four_assembly = assembly(( + at( + terminal(name, four_part), + 0.15e-3cos(2pi * (index - 1) / 4), + 0.15e-3sin(2pi * (index - 1) / 4); + φ = 2pi * (index - 1) / 4 + ) for (index, name) in enumerate(four_names) + )...) + four_design = build( + CableDesign, + "fem-four-sector-formations", + Enclosure( + :four_sector_matrix, + four_assembly; + primitive = Disk(4.5e-3), + fill = dielectric + ) + ) + four_model = extension_module._resolved_fem_model( + problem( + four_design, + Dict(:d => 1, :e => 2, :f => 3, :g => 4), + four_design.cable_id + ), + formulation + ) + @test length(four_model.region_plans) == 33 + @test count(plan -> plan.shape isa DM.Polygon, four_model.region_plans) == 28 + @test [count(==(terminal), getproperty.(four_model.region_plans, :terminal_index)) + for terminal in 1:4] == [7, 7, 7, 7] + @test count( + plan -> plan.terminal_index == 0 && + plan.shape isa DM.DifferenceShape && + plan.shape.outer isa DM.SectorShape, + four_model.region_plans + ) == 4 + + milliken_core = milliken( + copper; + shape = Disk(0.33e-3), + segment = Sector( + span = pi / 3, + r_base = 0.85e-3, + r_back = 3.0e-3, + fillet = 0.1e-3 + ) + ) + milliken_design = build( + CableDesign, + "fem-milliken-core", + terminal(:core, milliken_core) + ) + milliken_model = extension_module._resolved_fem_model( + problem( + milliken_design, + Dict(:core => 1), + milliken_design.cable_id + ), + formulation + ) + @test count( + plan -> plan.terminal_index == 1, + milliken_model.region_plans + ) > 7 + milliken_fill = only(filter( + plan -> plan.terminal_index == 0, + milliken_model.region_plans + )) + @test milliken_fill.shape isa DM.DifferenceShape + @test length(milliken_fill.shape.holes) == + count(plan -> plan.terminal_index == 1, milliken_model.region_plans) + + annular_design = build(CableDesign, "fem-rectangular-last-strip", + terminal(:core, + stranded(copper; shape = Rectangle(0.3e-3, 0.1e-3), + center = Disk(0.2e-3), boundary = Disk(0.6e-3), lay = LayRatio(12)), + insulation(dielectric; t = 0.2e-3))) + annular_model = extension_module._resolved_fem_model( + problem(annular_design, Dict(:core => 1), annular_design.cable_id), + formulation) + @test any( + region -> region.source.primitive isa Rectangle && + region.primitive isa Annulus, + annular_design.geometry.regions) + @test all(region -> region.source.tag !== :stranded_fill, + annular_design.geometry.regions) + @test length(annular_model.region_plans) == 2 + @test first(annular_model.region_plans).shape isa Disk + @test first(annular_model.region_plans).shape.r == + last(annular_design.geometry.regions).primitive.ri + + Gmsh.initialize(String[]; finalize_atexit = false) + try + gmsh.option.set_number("General.Terminal", 1) + gmsh.option.set_number("General.Verbosity", 0) + for (name, model) in ( + ("fem-compacted-circular-core", circular_model), + ("fem-natural-circular-core", natural_model), + ("fem-filled-compaction", filled_model), + ("fem-sector-formations", sector_model), + ("fem-four-sector-formations", four_model), + ("fem-milliken-core", milliken_model), + ("fem-rectangular-last-strip", annular_model) + ) + @testset "$name" begin + geometry = extension_module._build_geometry!(model, name) + gmsh.model.set_current(geometry.model_name) + @test all(!isempty, geometry.material_surfaces) + @test all(!isempty, geometry.terminal_surfaces) + extension_module._configure_mesh!( + model, geometry, only(model.mesh_plans) + ) + gmsh.model.mesh.generate(2) + @test !isempty(first(gmsh.model.mesh.get_nodes())) + for surfaces in geometry.material_surfaces, surface in surfaces + + _, element_tags, _ = gmsh.model.mesh.get_elements(2, surface) + @test any(!isempty, element_tags) + end + @test isnothing(extension_module._inspect_loaded_mesh(model, name)) + # Physical tags alone do not prove a material was meshed. This + # also guards validation of reused or imported mesh files. + surface = first(first(geometry.material_surfaces)) + gmsh.model.mesh.clear([(2, surface)]) + @test_throws LineCableModelsFEMError extension_module._inspect_loaded_mesh( + model, name + ) + end + end + @testset "contact edges share subdivisions in either direction" begin + gmsh.model.add("contact-edge-subdivision") + registry = extension_module.FEMLoopRegistry(1e-12) + for point in ((0.0, 0.0), (0.5e-3, 0.0), (1e-3, 0.0)) + extension_module._point!(registry, point; mesh_size = nothing) + end + @test isempty(registry.point_sizes) + forward = extension_module._line_path!( + registry, (0.0, 0.0), (1e-3, 0.0); mesh_size = 1e-4 + ) + backward = extension_module._line_path!( + registry, (1e-3, 0.0), (0.0, 0.0); mesh_size = 1e-4 + ) + @test length(forward) == 2 + @test backward == -reverse(forward) + @test all(==(1e-4), values(registry.point_sizes)) + end + finally + Gmsh.finalize() + end +end + +@testitem "Gmsh FEM / optional real GetDP multi-frequency scan" tags=[ + :extension, + :integration, + :fem_numerical, + :fem, + :slow +] begin + using LineCableModels + using Gmsh + using SHA + using Serialization, JSON3, Logging + + copper = Material(kind = :conductor, rho = 1 / 5.8e7) + dielectric = Material(kind = :insulator, rho = 1.0e8, eps_r = 2.3, + tan_delta = 0.025) + design = build(CableDesign, + "fem-integration", + Stack( + Group(:core, Region(:core_metal, Disk(0.005), copper)), + Region(:dielectric, Annulus(0.005, 0.01), dielectric), + Group(:sheath, Region(:sheath_metal, Annulus(0.01, 0.011), copper)), + Region(:jacket, Annulus(0.011, 0.013), dielectric) + )) + system = build( + LineCableSystem, + design, + (0.0, -0.1); + connections = Dict(:core => 1, :sheath => 0), + system_id = "fem-integration", + line_length = 1.0 + ) + problem = LineParametersProblem( + system; + earth_props = LineCableModels.Earth.EarthModel(100.0, 10.0, 1.0), + frequencies = [50.0, 1000.0] + ) + formulation = Formulation( + :fem; + options = (ideal_transposition = false,)) + formulation_controls = ( + getdp_verbosity = 0, + gmsh_verbosity = 0, + keep_run_directory = true, timing = true + ) + formulation_space = Formulation(:fem; + earth_properties = Grid((formula(:default), nothing)), + insulation_admittance = Grid((:default, :lossy)), + options = formulation.options) + selected = collect(formulation_space) + completions = Tuple[] + on_result = (resolved, index, result) -> push!(completions, (index, result)) + batch = compute(ParametricProblem(problem, ComputationOptions((;formulation_controls..., trace = true, on_result = on_result))), + Combinatorial(formulation_space; options = (retain_details = true,))) + @test length(batch) == 4 + @test first.(completions) == collect(eachindex(selected)) + @test all(index -> completions[index][2].Z.values == batch[index].Z.values, + eachindex(selected)) + run_directories = [value.details.data.fem.run.run_directory for value in batch] + @test length(unique(run_directories)) == 2 + for index in eachindex(selected) + files = details(batch[index]).data.files + @test any(file -> file.path == "run.json", files) + @test all(file -> bytes2hex(open(sha256, file.source)) == file.sha256, files) + soil = batch[index].details.data.formulations.methods.earth_properties + @test soil === nothing ? selected[index].methods.earth_properties === nothing : + soil.identifier === formula_id(selected[index].methods.earth_properties) + @test keys(batch[index].details.data.formulations.methods) == keys(selected[index].methods) + @test details(batch).data.points[index] == details(batch[index]) + end + default_indices = findall( + value -> formula_id(value.methods.insulation_admittance) === :lossless, selected) + lossy_indices = findall( + value -> formula_id(value.methods.insulation_admittance) === :lossy, selected) + for indices in (default_indices, lossy_indices) + first_result, second_result = batch[indices[1]], batch[indices[2]] + @test first_result.Z.values == second_result.Z.values + @test first_result.Y.values == second_result.Y.values + @test first_result.Z.values !== second_result.Z.values + @test first_result.Y.values !== second_result.Y.values + @test first_result.details.data.fem.primitive.Z_primitive !== + second_result.details.data.fem.primitive.Z_primitive + @test run_directories[indices[1]] == run_directories[indices[2]] + @test !isempty(details(first_result).data.timing) + @test isempty(details(second_result).data.timing) + @test details(second_result).data.fem.run.reused + @test typeof(first_result) === typeof(second_result) + end + result = batch[first(default_indices)] + run_directory = result.details.data.fem.run.run_directory + @test size(result.Z) == (1, 1, 2) + @test size(result.Y) == (1, 1, 2) + @test all(isfinite, result.Z) + @test all(isfinite, result.Y) + @test result.f == [50.0, 1000.0] + expected_capacitance = 2π * 8.8541878128e-12 * dielectric.eps_r / log(0.01 / 0.005) + fem_capacitance = [imag(result.Y[1, 1, index]) / (2π * frequency) + for (index, frequency) in pairs(result.f)] + @test all( + isapprox(value, expected_capacitance; rtol = 0.02) + for value in fem_capacitance + ) + @test result.details.data.fem.run.state === Base.get_extension( + LineCableModels, :LineCableModelsGmshExt + ).completed + @test result.details.data.fem.run.getdp_invocations == + length(problem.frequencies) + @test result.details.data.fem.run.completed_columns == + length(problem.frequencies) * length(result.details.data.fem.terminal_ids) + @test run_directory !== nothing + timing=result.details.data.timing + @test result.details.data.fem.run.columns == result.details.data.fem.run.completed_columns + @test result.details.data.fem.run.recovered_columns == 0 + @test !result.details.data.fem.run.reused + @test keys(timing) == (:wall_seconds, :constraint_seconds, :assembly_seconds, + :solve_seconds, :output_seconds, :worker_wall_seconds) + @test timing.wall_seconds >= 0 + @test timing.worker_wall_seconds >= 0 + @test !haskey(result.details.data.fem, :timing) + extension=Base.get_extension(LineCableModels,:LineCableModelsGmshExt) + native=[extension._column_timing(extension._column_paths( + run_directory,frequency,basis,false).timing,frequency,basis) + for frequency in eachindex(problem.frequencies) + for basis in eachindex(result.details.data.fem.terminal_ids)] + for phase in (:constraint_seconds,:assembly_seconds,:solve_seconds,:output_seconds) + @test getproperty(timing,phase) ≈ sum(getproperty(row,phase) for row in native) + end + expected_rows = 1 + + length(problem.frequencies) * + length(result.details.data.fem.terminal_ids)^2 + for filename in ("Z.tsv", "P.tsv") + content = read(joinpath(run_directory, "raw", filename), String) + @test !occursin("\n\n", content) + @test length(readlines(IOBuffer(content))) == expected_rows + end + + # Same geometry and terminal conditions, now with explicitly requested + # conduction plus polarization loss. The shared .pro is unchanged. + lossy = batch[first(lossy_indices)] + for (index, frequency) in pairs(problem.frequencies) + expected_g = 2π / log(0.01 / 0.005) * ( + inv(dielectric.rho) + + 2π * frequency * 8.8541878128e-12 * + dielectric.eps_r * dielectric.tan_delta) + @test real(lossy.Y[1, 1, index]) ≈ expected_g rtol = 0.03 + @test abs(real(result.Y[1, 1, index])) < 0.03 * expected_g + @test imag(lossy.Y[1, 1, index]) / (2π * frequency) ≈ + expected_capacitance rtol = 0.02 + end + # A fresh call can reconstruct the selected result from a completed run, + # without starting Gmsh, touching historical files, or another solve. + function snapshot_files(root) + Dict(relpath(joinpath(directory, file), + root) => (bytes2hex(open(sha256, joinpath(directory, file))), + stat(joinpath(directory, file)).mtime) + for (directory, _, files) in walkdir(root) for file in files) + end + before_reuse = snapshot_files(run_directory) + @test !Bool(Gmsh.gmsh.is_initialized()) + repeated = compute(problem, selected[last(default_indices)]; + options = (;formulation_controls..., trace = true, resume_run_directory = run_directory)) + @test repeated.Z.values == result.Z.values + @test repeated.Y.values == result.Y.values + @test repeated.details.data.formulations.methods.earth_properties === nothing + @test repeated.details.data.fem.run.run_directory == run_directory + @test repeated.details.data.fem.run.reused + @test isempty(repeated.details.data.timing) + @test repeated.details.data.fem.inputs.getdp_identity.sha256 != "" + @test !Bool(Gmsh.gmsh.is_initialized()) + @test snapshot_files(run_directory) == before_reuse + batch_log = Test.TestLogger() + reused_batch = with_logger(batch_log) do + compute(problem, fill(selected[last(default_indices)], 3); options=(; + formulation_controls..., trace=true, resume_run_directory=run_directory, + verbosity=(default=0, progress=1))) + end + @test all(value->isempty(details(value).data.timing), reused_batch) + @test all(value->Z(value) == Z(result) && Y(value) == Y(result), reused_batch) + @test last(batch_log.logs).message == "FEM computation completed successfully" + @test last(batch_log.logs).kwargs[:completed] == 3 + @test snapshot_files(run_directory) == before_reuse + mktempdir() do temporary + expected = joinpath(temporary, "expected.bin") + serialize(expected, (result.Z.values, result.Y.values)) + code = raw""" + using LineCableModels, Gmsh, JSON3, Serialization + path, expected = ARGS + problem = LineCableModels.ImportExport.deserialize_value( + JSON3.read(read(joinpath(path, "input", "problem.json"), String))) + formulation = Formulation(:fem; earth_properties=nothing, + options=(ideal_transposition=false,)) + formulation_controls = (getdp_verbosity=0, gmsh_verbosity=0, + keep_run_directory=true) + result = compute(problem, formulation; options=merge(formulation_controls, (trace=true, resume_run_directory=path))) + Z, Y = deserialize(expected) + @assert result.Z.values == Z && result.Y.values == Y + @assert result.details.data.fem.run.run_directory == path + @assert !Bool(Gmsh.gmsh.is_initialized()) + println("completed-run reuse verified in fresh Julia") + """ + command = `$(Base.julia_cmd()) --startup-file=no --project=$(dirname(Base.active_project())) -e $code $run_directory $expected` + @test occursin("completed-run reuse verified in fresh Julia", read(command, String)) + @test snapshot_files(run_directory) == before_reuse + end + # Timing does not alter native compatibility, including completed reuse. + untimed = compute(problem, selected[last(default_indices)]; options=(; + formulation_controls..., timing=false, trace=true, resume_run_directory=run_directory)) + @test !haskey(details(untimed).data, :timing) + @test JSON3.write(details(untimed).data.fem.inputs) == JSON3.write(details(repeated).data.fem.inputs) + @test Z(untimed) == Z(result) && Y(untimed) == Y(result) + @test snapshot_files(run_directory) == before_reuse + # Reopen this test-owned run with one missing column. The remaining + # completed work must be recovered without becoming a fresh timing sample. + state = JSON3.read(read(joinpath(run_directory, "run.json"), String), Dict{String,Any}) + state["state"] = "failed" + write(joinpath(run_directory, "run.json"), JSON3.write(state)) + paths = extension._column_paths(run_directory, 1, 1, false) + rm(paths.checkpoint) + rm(paths.marker; force=true) + for attempt in readdir(joinpath(run_directory, "attempts"); join=true) + marker = extension._column_paths(attempt, 1, 1, false).marker + rm(marker; force=true) + end + recovered = compute(problem, selected[last(default_indices)]; options=(; + formulation_controls..., trace=true, resume_run_directory=run_directory)) + @test isempty(details(recovered).data.timing) + @test details(recovered).data.fem.run.recovered_columns > 0 + @test details(recovered).data.fem.run.getdp_invocations == + details(result).data.fem.run.getdp_invocations + 1 + @test Z(recovered) == Z(result) && Y(recovered) == Y(result) + rm(lossy.details.data.fem.run.run_directory; recursive = true, force = true) + + # The display lifecycle belongs to fem_ui.jl and its isolated children. + rm(run_directory; recursive = true, force = true) +end + +# Historical Python records and operators are retired. Current formulation and +# extraction controls are owned by fem_electrodynamics.jl and fem_quasi_full.jl. diff --git a/test/extensions/gmsh_clearance.jl b/test/extensions/gmsh_clearance.jl new file mode 100644 index 000000000..8364b7b6f --- /dev/null +++ b/test/extensions/gmsh_clearance.jl @@ -0,0 +1,37 @@ +@testitem "Gmsh FEM / exterior clearance / touching trefoil mesh" tags=[:extension, :fem] begin + using Gmsh + using Measurements: measurement + using LineCableModels + ext = Base.get_extension(LineCableModels, :LineCableModelsGmshExt) + copper = Material(kind = :conductor, rho = 1.7e-8) + formulation = Formulation(:fem; + options = (ideal_transposition = false,)) + execution = computation_options(LineCableModelsFEM, ComputationOptions((mesh_mode=:remesh, gmsh_verbosity=0,))) + for radius in (0.01, measurement(0.01, 1e-4)) + design = build(CableDesign, "touching-mesh", + Group(:core, Region(:metal, Disk(radius), copper))) + system = @test_logs (:warn, r"Cable placements adjusted") build(LineCableSystem, + trefoil(design; center = at(0, -0.1), spacing = 2radius, + connections = (core = (1, 2, 3),))) + problem = LineParametersProblem(system; + earth_props = homogeneous(rho = 100.0), frequencies = [50.0]) + normalized = ext._preflight_fem_problem(problem) + @test normalized.system.clearances ≈ nominal.(system.clearances) + @test getproperty.(normalized.system.positions, :x) ≈ nominal.(getproperty.(system.positions, :x)) + model = ext._resolved_fem_model(normalized, formulation) + session = ext._start_gmsh(0) + try + mktempdir() do directory + run = ext._create_run(directory) + geometry = ext._build_geometry!(model, "touching-trefoil") + mesh = ext._select_mesh!(run, model, geometry, execution, directory) + @test isfile(mesh) + @test run.mesh_source === :generated + @test !isempty(first(Gmsh.gmsh.model.mesh.get_nodes())) + validate(mesh, model) + end + finally + ext._finish_gmsh(session) + end + end +end diff --git a/test/extensions/gmsh_core.jl b/test/extensions/gmsh_core.jl new file mode 100644 index 000000000..e9f5bb789 --- /dev/null +++ b/test/extensions/gmsh_core.jl @@ -0,0 +1,21 @@ +@testitem "Gmsh FEM / core package remains Gmsh-independent" tags=[ + :core_only, + :extension, + :engine +] begin + import LineCableModels + + @test Base.get_extension(LineCableModels, :LineCableModelsGmshExt) === nothing + @test LineCableModels.LineCableModelsFEM <: + LineCableModels.AbstractFormulation + @test supertype(LineCableModels.LineCableModelsFEM) === LineCableModels.AbstractFormulation + + formulation = LineCableModels.Formulation( + :fem; + options = (ideal_transposition = false,)) + execution = computation_options(LineCableModelsFEM, ComputationOptions((ui=false,))) + @test execution isa ComputationOptions + @test !execution.data.ui + @test formulation isa LineCableModels.LineCableModelsFEM + @test Base.get_extension(LineCableModels, :LineCableModelsGmshExt) === nothing +end diff --git a/test/extensions/internal_shunt_measurements.jl b/test/extensions/internal_shunt_measurements.jl new file mode 100644 index 000000000..66f3296d9 --- /dev/null +++ b/test/extensions/internal_shunt_measurements.jl @@ -0,0 +1,123 @@ +@testitem "Measurements / internal shunt / correlated implicit sensitivities" tags=[:extension, :measurements, :slow] begin + using Measurements, LinearAlgebra, Random, Statistics + E = LineCableModels.Engine + include(joinpath(pkgdir(LineCableModels), "test", "support", "internal_shunt.jl")) + epsilon = measurement(2.5, 0.0025) + design = internal_shunt_test_design(; epsilon, count = 4) + domain, _ = internal_shunt_test_domain(design) + values = E.ShuntModel._shunt_values(domain, Formulation().methods) + level = (wire = 16, order = 8, quadrature = 64, modes = 128) + propagated = E.internal_shunt_response(E.ShuntModel.Formula(:boundary), values, domain; level) + @test all(isfinite, Measurements.value.(propagated.C)) + @test all(isfinite, Measurements.uncertainty.(propagated.C)) + @test maximum(Measurements.uncertainty.(propagated.C)) > 0 + @test abs(Measurements.cor(vec(propagated.C))[1, 2]) ≈ 1 rtol=1e-8 + deterministic(eps) = begin + d, + _ = internal_shunt_test_domain(internal_shunt_test_design(epsilon = eps, count = 4)) + E.internal_shunt_response(E.ShuntModel.Formula(:boundary), + E.ShuntModel._shunt_values(d, Formulation().methods), d; level).C + end + central = (deterministic(2.5001)-deterministic(2.4999))/0.0002 + @test Measurements.derivative.(propagated.C, Ref(epsilon)) ≈ central rtol=2e-3 + @test Measurements.uncertainty.(propagated.C) ≈ abs.(central)*0.0025 rtol=2e-3 + # Equal nominal values must not cause sharing across independent inputs. + independent, + _ = internal_shunt_test_domain(internal_shunt_test_design( + epsilon = measurement(2.5, 0.0025), count = 4)) + @test !E.ShuntModel._shunt_domain_equal(domain, independent) + rng = MersenneTwister(314) + samples = [deterministic(2.5+0.0025randn(rng))[1, 1] for _ in 1:24] + # 3 standard errors of the sampled standard deviation. No requirement + # that a finite MC sample exactly equals the linearized variance. + sigma = Measurements.uncertainty(propagated.C[1, 1]) + @test abs(std(samples)-sigma) < 3sigma/sqrt(2(length(samples)-1)) + @test abs(mean(samples)-Measurements.value(propagated.C[1, 1])) < + 4sigma/sqrt(length(samples)) + radius = measurement(0.2e-3, 0.1e-6) + geometry_domain, + _ = internal_shunt_test_domain(internal_shunt_test_design(; radius, count = 4)) + geometry_values = E.ShuntModel._shunt_values(geometry_domain, Formulation().methods) + geometry_result = E.internal_shunt_response( + E.ShuntModel.Formula(:boundary), geometry_values, geometry_domain; level) + function varying_radius(r) + d, _ = internal_shunt_test_domain(internal_shunt_test_design(radius = r, count = 4)) + E.internal_shunt_response(E.ShuntModel.Formula(:boundary), + E.ShuntModel._shunt_values(d, Formulation().methods), d; level).C + end + geometric_central = (varying_radius(0.2e-3+1e-8)-varying_radius(0.2e-3-1e-8))/2e-8 + @test Measurements.derivative.(geometry_result.C, Ref(radius)) ≈ geometric_central rtol=5e-3 + @test maximum(Measurements.uncertainty.(geometry_result.C)) > 0 + center = measurement(0.0, 1e-6) + other_center = measurement(0.0, 1e-6) + @test E.ShuntModel._shunt_same(center, center, 0.01) + @test !E.ShuntModel._shunt_same(center, other_center, 0.01) + @test !E.ShuntModel._shunt_same(center, 0.0, 0.01) + wire_angle = measurement(0.31, 1e-4) + angle_domain, + _ = internal_shunt_test_domain(internal_shunt_test_design(; wire_angle, count = 4)) + angle_values = E.ShuntModel._shunt_values(angle_domain, Formulation().methods) + angle_result = E.internal_shunt_response(E.ShuntModel.Formula(:boundary), angle_values, angle_domain; level) + function varying_angle(angle) + d, + _ = internal_shunt_test_domain(internal_shunt_test_design(wire_angle = angle, count = 4)) + E.internal_shunt_response(E.ShuntModel.Formula(:boundary), + E.ShuntModel._shunt_values(d, Formulation().methods), d; level).C + end + angular_central = (varying_angle(0.31001)-varying_angle(0.30999))/2e-5 + @test Measurements.derivative.(angle_result.C, Ref(wire_angle)) ≈ angular_central rtol=5e-3 + @test maximum(Measurements.uncertainty.(angle_result.C)) > 0 +end + +@testitem "Measurements / internal shunt / public parametric studies" tags=[ + :extension, :integration, + :measurements, + :slow] begin + using Measurements, Statistics + include(joinpath(pkgdir(LineCableModels), "test", "support", "internal_shunt.jl")) + space = Gridspace{CableConstantsProblem}( + epsilon->CableConstantsProblem( + internal_shunt_test_design(; epsilon, tapes = false, count = 4)), + (Grid(2.5, AbsoluteError(0.0025)),)) + problem = ParametricProblem(space) + inner = CableConstantsFormulation(shunt_model = formula(:boundary; + options = (resolution = (wire = 16, order = 8, quadrature = 64, modes = 128),))) + linear = compute(problem, LinearError(inner)) + sampled = compute(problem, + MonteCarlo(inner; trials = 12, seed = 314, + distribution = :normal, return_samples = true, retain_details = true)) + @test sampled.trial_counts == [12] + @test all(isempty, sampled.details.data.failures) + @test only(sampled.values).details.data.shunt_model.effective === :boundary + @test only(linear.values).details.data.shunt_model.effective === :boundary + IE=LineCableModels.ImportExport + restored=IE.deserialize_value(IE.serialize_value(linear)) + original=only(linear.values) + saved=only(restored.values) + @test Measurements.value.(saved.C) == Measurements.value.(original.C) + @test Measurements.uncertainty.(saved.C) ≈ Measurements.uncertainty.(original.C) + @test details(saved).data.shunt_model.effective === :boundary + # One shared uncertain input must remain one source across distinct points. + duplicated=LineCableModels.UQ.LinearErrorResult( + LinearError(inner), [original, original], ComputationDetails((;))) + copied=IE.deserialize_value(IE.serialize_value(duplicated)) + @test Measurements.uncertainty(copied.values[1].C[1]-copied.values[2].C[1]) == 0 + strict = CableConstantsFormulation(shunt_model = formula(:boundary; + options = (resolution = (wire = 100_000,),))) + @test_throws BoundarySolveError compute( + problem, MonteCarlo(strict; + trials = 2, seed = 314, on_error = :resample, retain_details = true)) + # Blueprint sharing must not erase independent Measurements sources. + independent = CableConstantsProblem(internal_shunt_test_design( + epsilon = measurement(2.5, 0.0025), tapes = false, count = 4)) + original_problem = first(space) + blueprints = only(LineCableModels.Engine.flatten(LineCableModelsCoaxial(), + [original_problem.design, independent.design], eltype(original_problem), [inner])) + @test blueprints[1].shunt[1].C !== blueprints[2].shunt[1].C + @test Measurements.uncertainty(blueprints[1].shunt[1].C[1, 1] - + blueprints[2].shunt[1].C[1, 1]) > 0 + cap = only(only(linear.values).C) + @test Measurements.uncertainty(cap) > 0 + @test abs(only(only(sampled.values).C)-Measurements.value(cap)) < + 4Measurements.uncertainty(cap)/sqrt(12) +end diff --git a/test/extensions/makie.jl b/test/extensions/makie.jl new file mode 100644 index 000000000..ef8d504cf --- /dev/null +++ b/test/extensions/makie.jl @@ -0,0 +1,19 @@ +@testitem "Extensions / Makie / unavailable graphics fail explicitly" tags=[ + :extension, + :core_only, + :commons +] begin + import LineCableModels + + @test Base.get_extension(LineCableModels, :LineCableModelsMakieExt) === nothing + @test Base.get_extension(LineCableModels, :LineCableModelsCairoMakieExt) === nothing + @test Base.get_extension(LineCableModels, :LineCableModelsGLMakieExt) === nothing + @test Base.get_extension(LineCableModels, :LineCableModelsWGLMakieExt) === nothing + @test isdefined(LineCableModels, :PlotBuilder) + @test_throws ArgumentError LineCableModels.preview(nothing) + @test_throws ArgumentError LineCableModels.show_material_scale() + @test_throws ArgumentError LineCableModels.plot(nothing) + @test_throws MethodError LineCableModels.materialcolors(:rho) + @test_throws MethodError LineCableModels.export_svg(nothing) + @test_throws MethodError LineCableModels.plotwindow(identity; title = "headless") +end diff --git a/test/extensions/measurements.jl b/test/extensions/measurements.jl new file mode 100644 index 000000000..60cc31582 --- /dev/null +++ b/test/extensions/measurements.jl @@ -0,0 +1,443 @@ +@testitem "Extensions / Measurements / unloaded uncertainty APIs fail explicitly" tags=[ + :extension, + :core_only, + :importexport +] begin + program=raw""" + using Test + import LineCableModels + + @test Base.get_extension(LineCableModels, :LineCableModelsMeasurementsExt) === nothing + @test LineCableModels.nominal(1.0) == 1.0 + @test LineCableModels.nominal(1.0 + 2.0im) == 1.0 + 2.0im + @test LineCableModels.uncertainty(1.0) == 0.0 + + uncertain_space=LineCableModels.Material( + kind = :insulator, + rho = 1e8, + eps_r = LineCableModels.Grid((3.0,), 0.1), + mu_r = 1.0, + T0 = 20.0, + alpha = 0.0 + ) + materialization_error=try + first(uncertain_space) + nothing + catch error + error + end + @test materialization_error isa ArgumentError + @test occursin("using Measurements", sprint(showerror, materialization_error)) + + attempts = Ref(0) + space = LineCableModels.Gridspace{LineCableModels.CableConstantsProblem}( + _ -> (attempts[] += 1; error("must fail before realization")), + (LineCableModels.Grid(1.0),)) + @test_throws r"using Measurements" LineCableModels.compute( + LineCableModels.ParametricProblem(space), + LineCableModels.MonteCarlo(LineCableModels.CableConstantsFormulation();trials=2,seed=1)) + @test attempts[] == 0 + + encoded_measurement=Dict( + "__type__"=>"Measurement", + "value"=>Dict("__type__"=>"Float", "value"=>1.0), + "uncertainty"=>Dict("__type__"=>"Float", "value"=>0.1) + ) + @test_throws ArgumentError LineCableModels.ImportExport.deserialize_value( + encoded_measurement + ) + @test_throws ArgumentError LineCableModels.ImportExport.deserialize_value( + Dict("__type__"=>"MeasurementLinearErrorResult","version"=>1)) + """ + project=dirname(Base.active_project()) + process=run(`$(Base.julia_cmd()) --startup-file=no --project=$project -e $program`;wait=false) + status=timedwait(()->process_exited(process),180.;pollint=.1) + if status===:timed_out + kill(process) + end + wait(process) + @test status===:ok + @test success(process) +end + +@testitem "Measurements / scientific UQ records preserve signed shared sources" tags=[:extension, :measurements] begin + using Measurements, JSON3, Statistics + IE=LineCableModels.ImportExport + q=measurement(2.0,0.25) + f=[measurement(50.0,0.1),measurement(1000.0,0.2)] + z=reshape([complex(q,2q),complex(3q,-q)],1,1,2) + y=reshape([complex(4q,-2q),complex(-q,5q)],1,1,2) + line=LineParameters(PhaseDomain,z,y,f) + cable=CableConstants([:core],[q],[2q],[-q],[3q],f[1],ComputationDetails()) + for core in (line,cable) + summaries=map((R=R(core),L=L(core),C=C(core),G=G(core))) do values + map(values) do x + mu,sigma=nominal(x),uncertainty(x) + SampleSummary([mu-sigma,mu,mu+sigma]) + end + end + for result in (LinearErrorResult(LinearError(Formulation()),[core,core]), + MonteCarloResult(MonteCarlo(Formulation();trials=3,seed=7), + [core,core],[summaries,summaries],nothing,nothing,UInt64(7),UInt64[8,9],[3,3])) + record=IE.serialize_value(result) + restored=IE.deserialize_value(JSON3.read(JSON3.write(record),Dict{String,Any})) + @test (restored isa MonteCarloResult) == (result isa MonteCarloResult) + for quantity in (R,L,C,G) + before=observe(first(result),quantity) + after=observe(first(restored),quantity) + @test nominal.(after) ≈ nominal.(before) + @test uncertainty.(after) ≈ uncertainty.(before) + @test all(iszero,uncertainty.(after.-observe(last(restored),quantity))) + end + @test Measurements.cov(first(R(restored[1])),first(G(restored[1]))) ≈ + Measurements.cov(first(R(core)),first(G(core))) + if core isa LineParameters + @test uncertainty.(frequencies(restored[1])) ≈ uncertainty.(f) + @test all(iszero,uncertainty.(frequencies(restored[1]).-frequencies(restored[2]))) + else + @test uncertainty(restored[1].frequency-restored[2].frequency)==0 + end + if core isa LineParameters && result isa LinearErrorResult + legacy_points=map(record["points"]) do point + merge(IE.deserialize_value(point),(frequencies=nominal.(f),)) + end + payload=(formulation=NamedTuple(result.formulation),points=legacy_points, + sources=IE.deserialize_value(record["sources"]),details=(;)) + legacy=Dict("__type__"=>"MeasurementLinearErrorResult","version"=>1, + "payload"=>IE.serialize_value(payload,Val(:scientific))) + recovered=IE.deserialize_value(JSON3.read(JSON3.write(legacy),Dict{String,Any})) + @test uncertainty(first(R(recovered[1]))-first(R(recovered[2])))==0 + @test uncertainty(first(G(recovered[1]))-4first(R(recovered[1])))==0 + end + end + end +end + +@testitem "Measurements / natural promotion preserves covariance" tags=[:extension, :measurements] setup=[ + UseDataModelSupport, TestNumerics] begin + using Measurements + import Measurements: derivative + + @testset "Owned constructors preserve primitive dependencies" begin + x = measurement(2.0, 0.1) + material = Material(:conductor, x, 1.0f0, 1, 20, 0.004) + + @test eltype(material) === Measurement{Float64} + @test derivative(material.rho, x) ≈ 1.0 + @test nominal(material.rho) == value(x) + @test uncertainty(material.rho) == uncertainty(x) + + space = LineCableModels.ParametricBuilder.Material( + kind = :conductor, + rho = Grid((1.0, 100.0)), + eps_r = Grid(1.0, 5.0), + mu_r = 1.0, + T0 = 20.0, + alpha = 0.0 + ) + propagated = collect(space) + @test eltype(space) === Any + @test Base.IteratorEltype(typeof(space)) isa Base.EltypeUnknown + @test eltype(propagated) === Material{Measurement{Float64}} + end + + @testset "Mixed BaseParams calls retain primitive sensitivities" begin + r_ex = measurement(0.02, 0.001) + rho = measurement(1.7241e-8, 1.0e-9) + resistance = tubular_resistance(0.0, r_ex, rho) + + @test !iszero(derivative(resistance, r_ex)) + @test !iszero(derivative(resistance, rho)) + end +end + +@testitem "DataModel / Measurements / covariance through complete assembly" tags=[:extension, :measurements] setup=[ + UseDataModelSupport, TestNumerics] begin + using Measurements + import Measurements: derivative + copper_props=Material(:conductor, 1.7241e-8, 1.0, 1.0, 20.0, 0.00393) + insulator_props=Material(:insulator, 1.0e14, 2.3, 1.0, 20.0, 0.0) + semicon_props=Material(:semicon, 1000.0, 1000.0, 1.0, 20.0, 0.0) + + diameter=measurement(0.02, 0.001) + insulation_thickness=measurement(0.01, 0.001) + semicon_thickness=measurement(0.005, 0.0005) + + core=Region(:core_metal, Disk(diameter/2), copper_props) + insulation=Region(:insulation, Shell(insulation_thickness), insulator_props) + semicon=Region(:semicon, Shell(semicon_thickness), semicon_props) + root=Stack(Group(:core, core), insulation, semicon) + resolved=resolve(EmptyBoundary(), root) + insulation_shape=resolved.regions[2].primitive + semicon_shape=resolved.regions[3].primitive + + @test r_in(insulation_shape) === r_ex(resolved.regions[1].primitive) + @test iszero(uncertainty( + r_ex(resolved.regions[1].primitive) - r_in(insulation_shape) + )) + + expected_radius=diameter/2+insulation_thickness+semicon_thickness + @test value(r_ex(semicon_shape)) ≈ value(expected_radius) + @test uncertainty(r_ex(semicon_shape)) ≈ uncertainty(expected_radius) + @test derivative(r_ex(semicon_shape), diameter) ≈ 0.5 + @test derivative(r_ex(semicon_shape), insulation_thickness) ≈ 1.0 + @test derivative(r_ex(semicon_shape), semicon_thickness) ≈ 1.0 + + design=build(CableDesign, "uq-path", root) + @test design.origin === root + system=build( + LineCableSystem, + design, + Pose2(0.0, -1.0, 0.0); + connections = Dict(:core=>1), + system_id = "uq-path", + line_length = 1000.0 + ) + assembled_radius=r_ex(system.geometry[end].primitive) + + @test system.designs[1] === design + @test derivative(assembled_radius, diameter) ≈ 0.5 + @test derivative(assembled_radius, insulation_thickness) ≈ 1.0 + @test derivative(assembled_radius, semicon_thickness) ≈ 1.0 +end + +@testitem "Measurements / retained values and numerical kernels" tags=[:extension, :measurements] setup=[ + UseEngineSupport, TestNumerics] begin + using Measurements + using Statistics + using SpecialFunctions + import LineCableModels + + extension_module=Base.get_extension(LineCableModels, :LineCableModelsMeasurementsExt) + @test extension_module !== nothing + uncertain=measurement(-20.0, 1.0) + @test LineCableModels.nominal(uncertain) == -20.0 + @test LineCableModels.uncertainty(uncertain) == 1.0 + @test LineCableModels.uncertainty(complex(1.0 ± 0.1, 2.0 ± 0.2)) == complex(0.1, 0.2) + @test LineCableModels.uncertainty([1.0 ± 0.1, 2.0 ± 0.2]) == [0.1, 0.2] + + clipped=LineCableModels.Commons.detach( + measurement(eps(Float64)/2, eps(Float64)/4), + 1.0 + ) + @test value(clipped) == eps(Float64)/2 + @test uncertainty(clipped) == eps(Float64)/4 + clipped_array=LineCableModels.Commons.detach( + [measurement(eps(Float64)/2, eps(Float64)/4)], + 1.0 + ) + @test value(only(clipped_array)) == eps(Float64)/2 + @test uncertainty(only(clipped_array)) == eps(Float64)/4 + retained=LineCableModels.Commons.detach( + measurement(2.0, 0.25), + 1.0 + ) + @test value(retained) == 2.0 + @test uncertainty(retained) == 0.25 + + encoded=LineCableModels.ImportExport.serialize_value(uncertain) + @test encoded["__type__"] == "Measurement" + decoded=LineCableModels.ImportExport.deserialize_value(encoded) + @test value(decoded) == value(uncertain) + @test uncertainty(decoded) == uncertainty(uncertain) + @test LineCableModels.ReportBuilder.encode_cell( + LineCableModels.ReportBuilder.XLSXReportDefinition(), + LineCableModels.nominal(uncertain) + ) == -20.0 + + argument=complex(measurement(1.25, 0.01), measurement(0.5, 0.02)) + nominal=complex(1.25, 0.5) + for function_value in (besselix, besselkx) + propagated=function_value(0, argument) + @test value(real(propagated)) ≈ real(function_value(0, nominal)) rtol=1.0e-11 + @test value(imag(propagated)) ≈ imag(function_value(0, nominal)) rtol=1.0e-11 + @test uncertainty(real(propagated)) > 0 + @test uncertainty(imag(propagated)) > 0 + end + +end + +@testitem "Measurements / Bessel sensitivities preserve complex covariance" tags=[:extension, :measurements] begin + using Measurements + using SpecialFunctions + import Measurements: value + + # Independent derivative recurrences: DLMF 10.6.1 and 10.29.1. + # The scaled variants also differentiate their documented exponential factor. + # Both Cartesian coordinates depend on q: testing independent coordinates + # alone would not detect a lost covariance in the complex adapter. + q = measurement(1.25, 1.0e-4) + argument = complex(q, 2q) + nominal_argument = complex(value(q), 2value(q)) + direction = 1 + 2im + for order in (0, 1, 2), kernel in ( + besseli, besselk, besselj, bessely, besselh, + besselix, besselkx, besseljx, besselyx, besselhx) + @testset "$kernel, order=$order" begin + expected = kernel(order, nominal_argument) + lower = kernel(order - 1, nominal_argument) + upper = kernel(order + 1, nominal_argument) + slope = if kernel === besseli + direction * (lower + upper) / 2 + elseif kernel === besselk + -direction * (lower + upper) / 2 + elseif kernel === besselix + direction * (lower + upper) / 2 - expected + elseif kernel === besselkx + direction * (expected - (lower + upper) / 2) + elseif kernel === besseljx || kernel === besselyx + direction * (lower - upper) / 2 - 2expected + elseif kernel === besselhx + direction * ((lower - upper) / 2 - im * expected) + else + direction * (lower - upper) / 2 + end + propagated = kernel(order, argument) + @test propagated isa Complex{Measurement{Float64}} + for component in (real, imag) + @test value(component(propagated)) ≈ component(expected) rtol=1.0e-12 + @test Measurements.derivative(component(propagated), q) ≈ + component(slope) rtol=5.0e-6 atol=1.0e-8 + @test uncertainty(component(propagated)) ≈ + abs(component(slope)) * uncertainty(q) rtol=5.0e-6 atol=1.0e-12 + end + @test Measurements.cov(real(propagated), imag(propagated)) ≈ + real(slope) * imag(slope) * uncertainty(q)^2 rtol=1.0e-5 atol=1.0e-14 + end + end +end + +@testitem "Measurements / Monte Carlo / stored values and Gridspace transport" tags=[:extension, :measurements] setup=[ + UseEngineSupport, TestNumerics] begin + using Measurements + using Statistics + formulation=MonteCarlo(Formulation(); trials = 3, seed = 9, + return_samples = true) + values=[CableConstants(2.0, 20.0, 200.0)] + stats=[( + R = [SampleSummary(2.0, 1.0, 1.0, 1.1, 2.0, 2.9, 3.0, 3)], + L = [SampleSummary(20.0, 10.0, 10.0, 11.0, 20.0, 29.0, 30.0, 3)], + C = [SampleSummary(200.0, 100.0, 100.0, 110.0, 200.0, 290.0, 300.0, 3)], + G = [SampleSummary(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3)] + )] + sample_values=[( + R = [1.0 2.0 3.0], + L = [10.0 20.0 30.0], + C = [100.0 200.0 300.0], + G = zeros(1, 3) + )] + completed=MonteCarloResult( + formulation, map(LineCableModels.materialize,values,stats), stats, sample_values, nothing, + UInt64(9), UInt64[9], [3] + ) + + struct ConstantsProblem<:AbstractProblemDefinition + constants::CableConstants + end + LineCableModels.validate(problem::ConstantsProblem) = problem + transported=Gridspace{ConstantsProblem}(completed) + reconstructed=only(transported).constants + @test value.(reconstructed.R) == [2.0] + @test uncertainty.(reconstructed.R) == [1.0] + @test value.(reconstructed.L) == [20.0] + @test uncertainty.(reconstructed.L) == [10.0] + @test value.(reconstructed.C) == [200.0] + @test uncertainty.(reconstructed.C) == [100.0] + @test iszero(Measurements.cov(reconstructed.R[1], reconstructed.L[1])) + + retained=only(samples(completed)) + sample_matrix=vcat( + reshape(retained.R, 1, :), + reshape(retained.L, 1, :), + reshape(retained.C, 1, :) + ) + means=vec(mean(sample_matrix; dims = 2)) + centered=sample_matrix .- means + covariance=centered*transpose(centered)/(size(sample_matrix, 2)-1) + coordinates=Measurements.correlated_values(means, covariance) + @test value.(coordinates) ≈ [2.0, 20.0, 200.0] + @test uncertainty(coordinates[2] - 10 * coordinates[1]) ≤ 20 * eps(Float64) + + frequency=[50.0] + angular=2π*only(frequency) + parameters=LineParameters( + PhaseDomain, + reshape(ComplexF64[2.0 + 20.0angular * im], 1, 1, 1), + reshape(ComplexF64[0.0 + 200.0angular * im], 1, 1, 1), + frequency + ) + line_stats=[( + R = reshape([stats[1].R[1]], 1, 1, 1), + L = reshape([stats[1].L[1]], 1, 1, 1), + C = reshape([stats[1].C[1]], 1, 1, 1), + G = reshape([stats[1].G[1]], 1, 1, 1) + )] + line_completed=MonteCarloResult( + formulation, + [LineCableModels.materialize(parameters,only(line_stats))], + line_stats, + nothing, + nothing, + UInt64(9), + UInt64[9], + [3] + ) + modal_space=Gridspace{ModalAnalysisProblem}(line_completed) + modal_problem=only(modal_space) + @test modal_problem isa ModalAnalysisProblem + @test isconcretetype(eltype(modal_space)) + @test value(real(modal_problem.parameters.Z.values[1])) == 2.0 + @test uncertainty(real(modal_problem.parameters.Z.values[1])) == 1.0 + @test value(imag(modal_problem.parameters.Z.values[1])) == 20.0angular + @test uncertainty(imag(modal_problem.parameters.Z.values[1])) == 10.0angular + + # Transport every design point, even without retaining Monte Carlo samples. + # Marginal reconstruction must neither merge points nor invent covariance. + second_parameters = LineParameters( + PhaseDomain, 2 .* parameters.Z.values, 2 .* parameters.Y.values, frequency + ) + second_stats = map(only(line_stats)) do quantities + map(quantities) do summary + SampleSummary(2 .* [summary.min, summary.median, summary.max]) + end + end + two_points = MonteCarloResult( + formulation, [LineCableModels.materialize(parameters,only(line_stats)), + LineCableModels.materialize(second_parameters,second_stats)], + [only(line_stats), second_stats], nothing, nothing, + UInt64(9), UInt64[9, 10], [3, 3] + ) + transported_points = Gridspace{ModalAnalysisProblem}(two_points) + @test length(transported_points) == 2 + @test isconcretetype(eltype(transported_points)) + modal_problems = collect(transported_points) + selected_core = @inferred uncertain(two_points,2) + all_cores = @inferred uncertain(two_points) + @test length(all_cores) == 2 + @test all_cores === two_points.values + @test selected_core === two_points[2] + @test all_cores[2] === selected_core + @test iszero(uncertainty(real(first(Z(selected_core))) - real(first(Z(uncertain(two_points,2)))))) + @test nominal.(Z(selected_core)) == nominal.(Z(all_cores[2])) + @test uncertainty.(real.(Z(selected_core))) == uncertainty.(real.(Z(all_cores[2]))) + @test_throws BoundsError uncertain(two_points,3) + for (index, problem) in enumerate(modal_problems) + point_parameters = problem.parameters + @test point_parameters.f == frequency + @test LineCableModels.basis(point_parameters) == LineCableModels.basis(parameters) + @test point_parameters.domain isa PhaseDomain + @test first(point_parameters.Z.values) isa Complex{Measurement{Float64}} + for quantity in (R, L, C, G) + selected = quantity(point_parameters) + summary = getproperty(two_points.stats[index], nameof(quantity)) + @test value.(selected) ≈ getproperty.(summary, :mean) + @test uncertainty.(selected) ≈ getproperty.(summary, :std) + end + end + @test iszero(Measurements.cov( + real(first(modal_problems[1].parameters.Z.values)), + real(first(modal_problems[2].parameters.Z.values)) + )) + @test transported_points.grids === (two_points,) + @test modal_problems[2].parameters === selected_core +end diff --git a/test/extensions/xlsx.jl b/test/extensions/xlsx.jl new file mode 100644 index 000000000..6813f3bb8 --- /dev/null +++ b/test/extensions/xlsx.jl @@ -0,0 +1,34 @@ +@testitem "Extensions / XLSX / unloaded writer is absent" tags=[:extension,:core_only,:report] begin + const RB=LineCableModels.ReportBuilder + @test Base.get_extension(LineCableModels,:LineCableModelsXLSXExt)===nothing + observed=ObservedResult(LineParameters(fill(1.0+2im,2,2,1),fill(3.0+4im,2,2,1),[50.])) + definition=XLSXReportDefinition() + tables=RB.tabulate(definition,observed) + encoded=RB.encode(definition,observed,tables,nothing) + @test length(encoded)==4 + @test all(book -> book isa RB.XLSXWorkbook,encoded) + @test first(encoded).sheets[1].cells[2,2]==1000 + tables.Z.R[1,2]=12345 + supplied=RB.encode(definition,observed,tables,nothing) + @test only(filter(book -> endswith(book.destination,"_R.xlsx"),supplied)).sheets[1].cells[2,2]==12345 + @test getproperty.(first(encoded).sheets,:name)==["values","std","metadata"] + @test !applicable(RB.write,definition,encoded) + @test_throws MethodError report(definition,observed) +end + +@testitem "Extensions / XLSX writer / explicit activation" tags=[:extension, :xlsx] begin + using XLSX + extension=Base.get_extension(LineCableModels,:LineCableModelsXLSXExt) + @test extension!==nothing + @test any(method -> method.module===extension,methods(LineCableModels.ReportBuilder.write)) + parameters=LineParameters(fill(1.0+2im,1,1,1),fill(3e-6+4e-6im,1,1,1),[50.]) + mktempdir() do directory + files=export_data(:xlsx,parameters;file_name=joinpath(directory,"owned.xlsx")) + @test length(files)==4 + r=XLSX.readxlsx(only(filter(path -> endswith(path,"_R.xlsx"),files))) + g=XLSX.readxlsx(only(filter(path -> endswith(path,"_G.xlsx"),files))) + @test r["values"]["B2"]==1000 + @test g["values"]["B2"]≈0.003 + @test r["values"]["B2"] isa Number + end +end diff --git a/test/fixtures/data/README.md b/test/fixtures/data/README.md new file mode 100644 index 000000000..875a469ce --- /dev/null +++ b/test/fixtures/data/README.md @@ -0,0 +1,5 @@ +# Current serialized inputs + +Inherited model JSON was removed. Serialization tests construct current objects +and write temporary current-format records through the production serializer. +The production examples and material libraries are separate protected product inputs. diff --git a/test/fixtures/reference/README.md b/test/fixtures/reference/README.md new file mode 100644 index 000000000..c3b66a6c0 --- /dev/null +++ b/test/fixtures/reference/README.md @@ -0,0 +1,7 @@ +# Retired inherited references + +The inherited numerical payloads have all been removed. No file in this directory is +an approved initial-release reference. Necessary scenarios and independent expectation +recipes belong beside their maintained test families. + +Do not regenerate rejected expectations after a failure. diff --git a/test/fixtures/reference/three_bare_wires/README.md b/test/fixtures/reference/three_bare_wires/README.md new file mode 100644 index 000000000..bcb1e993e --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/README.md @@ -0,0 +1,32 @@ +# 3 bare wires: retained baseline + +The physical fixtures live in `test/support/scenarios.jl` and select no formula. +Run `dev/run_three_bare_wires_baseline.jl` in an environment with this package +and Gmsh available. The script does not activate or install an environment. + +Each additive `capture-*` directory retains five complete nine-frequency scans +for current unified and FEM (`quasi_tem`), or the actual failure for each scan. +CSV files contain every ordered entry of phase-domain Z [Ω/m] and Y [S/m] with +round-trip Float64 precision. The manifest records inputs, source revisions, +timings, failures and file hashes. FEM artifacts remain at the recorded persistent +run directories under `.linecablemodels/fem`. Preserve those directories with +the capture. Reference approval and tolerance selection require a separate decision. + +The baseline was reviewed before production cleanup. The human authorized +non-degradation of the observed agreement, allowing improvement and insignificant +Y changes. This does not assert exact FEM agreement. Other formulas can reuse the +physical fixtures without inheriting the unified comparison requirement. + +`capture-20260917T102438-Nt9nic` is the immutable pre-refactor baseline. +`validation-20260917T111424-R4HWRE` retains all post-refactor entries, before and after +errors and fresh-workspace timings. Its comparison never reruns FEM or replaces +the captured reference. + +The versioned checkpoint contains the original numerical payloads and capture +metadata, plus the Engine-convergence starting scan +`validation-20260919T144543-aqKVmx` and final scan +`validation-20260919T170632-eFIkJx`. Historical source copies, dirty patches, +execution logs, native FEM artifacts and other intermediate validation captures +remain local evidence. They are not required to run the maintained fixture tests. +Historical serialized problems retain their original schema and are not current +execution inputs. Tests construct the physical fixture from `scenarios.jl`. diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/REVIEW.md b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/REVIEW.md new file mode 100644 index 000000000..6c833c2a8 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/REVIEW.md @@ -0,0 +1,72 @@ +# Step 0 baseline review + +Captured on 2026-09-17, before any production changes in this execution round. +All five physical placements completed with both current unified and existing +quasi-TEM FEM. Each scan has nine frequencies, three distinct terminals, and +full ordered 3×3 Z/Y matrices. No fixture, frequency or matrix entry was dropped. + +## Evidence + +- 10 successful public `compute` calls; 90 frequency points across the two backends. +- 1,620 retained complex entries, saved with exact Float64 round-trip checks. +- 75 capture-file and 1,480 persistent FEM-artifact checksums verified after completion. +- 91 focused physical-fixture tests passed. +- Julia 1.12.7; Gmsh 4.15.0; GetDP 3.5.0 (PETSc 3.14.4). +- QuadGK controls remain the current resolved defaults: `rtol=1e-8`, + `atol=0`, `maxevals=10000000`; the existing production acceptance logic was + not changed for the capture. +- No warnings or failures in the captured public-compute logs. Native solver + logs and inputs are retained in the referenced FEM directories. + +`manifest.toml` identifies each run, source revision, dirty patch, environment, +terminal order, timing and file checksums. Each backend directory includes the +complete serialized physical problem, formulation record, computation options, +trace/details, timing/log, and Z/Y CSVs. `verification.toml` records the checksum +verification. Original execution sources are under `sources/`; the subsequently +extended comparison script is retained separately as `comparison-runner.jl`. + +## Observed differences, not acceptance thresholds + +For each quantity, this table selects the entry with the largest **absolute** +complex difference over all nine frequencies. The percentage is the relative +difference at that same entry, not a normwise error or the largest relative error. + +| Placement | max abs ΔZ [Ω/m] | Relative at that entry | max abs ΔY [S/m] | Relative at that entry | +| --- | ---: | ---: | ---: | ---: | +| all air | 1.94850723 | 14.8994% | 1.26094674e-5 | 21.0683% | +| all earth | 0.01110244 | 0.137638% | 0.12638102 | 0.278505% | +| wire 1 in air | 1.87231519 | 3.88664% | 0.12586657 | 0.277378% | +| wire 2 in air | 1.70714875 | 3.32557% | 0.12511125 | 0.275711% | +| wire 3 in air | 2.13082152 | 4.44982% | 0.12560263 | 0.276792% | + +All these Z maxima occur at 10 MHz. The all-air Y maximum occurs at 10 MHz; +the other Y maxima occur at 1 MHz on buried-wire diagonal entries. + +The largest all-air entry-relative Z difference is **27.6661%**, at 10 MHz, +response wire 3 / source wire 1: absolute difference 1.93230389 Ω/m against +a FEM magnitude of 6.98437655 Ω/m. This is not a tiny-denominator effect. +Below 10 MHz the all-air complex entry-relative differences stay below 0.781% +for Z and 1.402% for Y on the sampled grid. + +Some mixed-layout relative discrepancies are much larger but involve small +cross-interface mutual entries. For example, wire-1-in-air Y at 1 MHz, +response wire 1 / source wire 3, differs by 8.21753790e-6 S/m against a FEM +magnitude of 6.01129668e-9 S/m. This is retained explicitly, not hidden in a +matrix norm or portrayed as the error of the large buried-wire diagonals. + +`comparison.toml` records both kinds of extrema, coordinates and denominator +magnitudes. Each placement's `Z-differences.csv` and `Y-differences.csv` retain +absolute and relative complex/component differences for every entry. Relative +errors are marked NaN at arithmetic-scale zero denominators; their absolute +errors remain present. This reporting convention is not an acceptance floor. + +## Disposition + +The computations completed, but this is **not a demonstrated matching +baseline**. No final comparison tolerances have been approved. The cause of +the discrepancies is not established by these measurements alone. + +Per §0.3 of the locked plan, production cleanup awaits the user's baseline +review/direction. No production refactor, tolerance relaxation, FEM alteration +or commit was performed in this execution round. Preserve these payloads; +do not regenerate them to make a subsequent refactor pass. diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/comparison.toml b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/comparison.toml new file mode 100644 index 000000000..f6499a630 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/comparison.toml @@ -0,0 +1,112 @@ +near_zero_rule = "abs(component) <= eps(Float64)*maximum(abs, FEM channel); relative error is NaN there" +status = "differences only; no acceptance verdict" + +["three_bare_wires_air_1/Y"] +absolute_at_worst_relative_entry = 8.21753789586833e-6 +max_absolute_error = 0.1258665684345085 +max_relative_error = 1367.0158584768271 +near_zero_threshold = 2.173476446050181e-14 +reference_magnitude_at_worst_absolute_entry = 45.37729467327596 +reference_magnitude_at_worst_relative_entry = 6.011296683144974e-9 +relative_at_worst_absolute_entry = 0.002773778589948313 +worst_absolute_entry = "1000000,cable:2:core,cable:2:core" +worst_relative_entry = "1000000,cable:1:core,cable:3:core" + +["three_bare_wires_air_1/Z"] +absolute_at_worst_relative_entry = 2.447964046819071e-9 +max_absolute_error = 1.8723151853984727 +max_relative_error = 0.6589980316379983 +near_zero_threshold = 1.0696584042954152e-14 +reference_magnitude_at_worst_absolute_entry = 48.173131909985514 +reference_magnitude_at_worst_relative_entry = 3.714675809781159e-9 +relative_at_worst_absolute_entry = 0.038866378646441546 +worst_absolute_entry = "10000000,cable:1:core,cable:1:core" +worst_relative_entry = "10000000,cable:2:core,cable:1:core" + +["three_bare_wires_air_2/Y"] +absolute_at_worst_relative_entry = 5.508319762379648e-10 +max_absolute_error = 0.12511124689781913 +max_relative_error = 971.0245927478688 +near_zero_threshold = 2.1734709781339032e-14 +reference_magnitude_at_worst_absolute_entry = 45.37761481912594 +reference_magnitude_at_worst_relative_entry = 5.672688213582567e-13 +relative_at_worst_absolute_entry = 0.002757113775074109 +worst_absolute_entry = "1000000,cable:1:core,cable:1:core" +worst_relative_entry = "10000000,cable:2:core,cable:3:core" + +["three_bare_wires_air_2/Z"] +absolute_at_worst_relative_entry = 6.637328439257838e-13 +max_absolute_error = 1.707148753384576 +max_relative_error = 0.9999798443213148 +near_zero_threshold = 1.1398436278542267e-14 +reference_magnitude_at_worst_absolute_entry = 51.333993376649296 +reference_magnitude_at_worst_relative_entry = 6.637462221813666e-13 +relative_at_worst_absolute_entry = 0.033255716944887444 +worst_absolute_entry = "10000000,cable:2:core,cable:2:core" +worst_relative_entry = "10000000,cable:1:core,cable:3:core" + +["three_bare_wires_air_3/Y"] +absolute_at_worst_relative_entry = 8.217553136847389e-6 +max_absolute_error = 0.1256026262798634 +max_relative_error = 1363.548140415765 +near_zero_threshold = 2.1734722713276887e-14 +reference_magnitude_at_worst_absolute_entry = 45.37804086122362 +reference_magnitude_at_worst_relative_entry = 6.026595536510902e-9 +relative_at_worst_absolute_entry = 0.0027679164612677935 +worst_absolute_entry = "1000000,cable:1:core,cable:1:core" +worst_relative_entry = "1000000,cable:3:core,cable:1:core" + +["three_bare_wires_air_3/Z"] +absolute_at_worst_relative_entry = 3.6038560694105644e-9 +max_absolute_error = 2.130821524209557 +max_relative_error = 0.7224401579316764 +near_zero_threshold = 1.063272582215814e-14 +reference_magnitude_at_worst_absolute_entry = 47.88554005060405 +reference_magnitude_at_worst_relative_entry = 4.9884492574835425e-9 +relative_at_worst_absolute_entry = 0.044498224765926554 +worst_absolute_entry = "10000000,cable:3:core,cable:3:core" +worst_relative_entry = "10000000,cable:2:core,cable:3:core" + +["three_bare_wires_all_air/Y"] +absolute_at_worst_relative_entry = 1.2609467382028214e-5 +max_absolute_error = 1.2609467382028214e-5 +max_relative_error = 0.21068315503871385 +near_zero_threshold = 2.167476027322301e-19 +reference_magnitude_at_worst_absolute_entry = 5.985038234172625e-5 +reference_magnitude_at_worst_relative_entry = 5.985038234172625e-5 +relative_at_worst_absolute_entry = 0.21068315503871385 +worst_absolute_entry = "10000000,cable:1:core,cable:3:core" +worst_relative_entry = "10000000,cable:1:core,cable:3:core" + +["three_bare_wires_all_air/Z"] +absolute_at_worst_relative_entry = 1.9323038886674555 +max_absolute_error = 1.948507230227846 +max_relative_error = 0.2766608979472721 +near_zero_threshold = 1.1405953677907427e-14 +reference_magnitude_at_worst_absolute_entry = 13.077777389784018 +reference_magnitude_at_worst_relative_entry = 6.984376552684099 +relative_at_worst_absolute_entry = 0.1489937603426377 +worst_absolute_entry = "10000000,cable:2:core,cable:1:core" +worst_relative_entry = "10000000,cable:3:core,cable:1:core" + +["three_bare_wires_all_earth/Y"] +absolute_at_worst_relative_entry = 2.800418985726602e-7 +max_absolute_error = 0.12638102356686592 +max_relative_error = 0.6585051417903717 +near_zero_threshold = 2.173466689249507e-14 +reference_magnitude_at_worst_absolute_entry = 45.37840039881675 +reference_magnitude_at_worst_relative_entry = 4.252691145452113e-7 +relative_at_worst_absolute_entry = 0.002785048006455541 +worst_absolute_entry = "1000000,cable:1:core,cable:1:core" +worst_relative_entry = "10000000,cable:3:core,cable:2:core" + +["three_bare_wires_all_earth/Z"] +absolute_at_worst_relative_entry = 2.3084447437255037e-8 +max_absolute_error = 0.01110243513586638 +max_relative_error = 0.65870223072826 +near_zero_threshold = 1.7911055519184423e-15 +reference_magnitude_at_worst_absolute_entry = 8.066422296201123 +reference_magnitude_at_worst_relative_entry = 3.504534577290397e-8 +relative_at_worst_absolute_entry = 0.0013763766299583725 +worst_absolute_entry = "10000000,cable:3:core,cable:3:core" +worst_relative_entry = "10000000,cable:2:core,cable:3:core" diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/manifest.toml b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/manifest.toml new file mode 100644 index 000000000..8c229ce31 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/manifest.toml @@ -0,0 +1,2280 @@ +julia = "1.12.7" +matching_criteria = "Pending human review; no numerical acceptance tolerance imposed." +project = "/home/amartins/.julia/environments/v1.12/Project.toml" +revision = "1d931911b45aa5232e472c935a602e2392ff76e3" +status = "captured; pending comparison and human review" + +[packages.0234f1f7-429e-5d53-9886-15a909be8d59] +name = "HDF5_jll" +source = "/home/amartins/.julia/packages/HDF5_jll/NQMjC" +version = "1.14.6+0" + +[packages.02c8fc9c-b97f-50b9-bbe4-9be30ff0a78a] +name = "XML2_jll" +source = "/home/amartins/.julia/packages/XML2_jll/XNg4P" +version = "2.15.3+0" + +[packages.03a91e81-4c3e-53e1-a0a4-9c0c8f19dd66] +name = "SplitApplyCombine" +source = "/home/amartins/.julia/packages/SplitApplyCombine/ebXW1" +version = "1.3.0" + +[packages.05181044-ff0b-4ac5-8273-598c1e38db00] +name = "RelocatableFolders" +source = "/home/amartins/.julia/packages/RelocatableFolders/URtlI" +version = "1.0.1" + +[packages.05823500-19ac-5b8b-9628-191a04bc5112] +name = "OpenLibm_jll" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/OpenLibm_jll" +version = "0.8.7+0" + +[packages.0656b61e-2033-5cc2-a64a-77c0f6c09b89] +name = "GLFW_jll" +source = "/home/amartins/.julia/packages/GLFW_jll/EvIAA" +version = "3.5.1+0" + +[packages.069b7b12-0de2-55c6-9aab-29f3d0a68a2e] +name = "FunctionWrappers" +source = "/home/amartins/.julia/packages/FunctionWrappers/Q5cBx" +version = "1.1.3" + +[packages.075b6546-f08a-558a-be8f-8157d0f608a5] +name = "libsixel_jll" +source = "/home/amartins/.julia/packages/libsixel_jll/8qnSY" +version = "1.10.5+0" + +[packages.08abe8d2-0d0c-5749-adfa-8a2ac140af0d] +name = "PrettyTables" +source = "/home/amartins/.julia/packages/PrettyTables/ueaXN" +version = "3.4.8" + +[packages.0987c9cc-fe09-11e8-30f0-b96dd679fdca] +name = "MonteCarloMeasurements" +source = "/home/amartins/.julia/packages/MonteCarloMeasurements/zXluZ" +version = "1.5.2" + +[packages.09ab397b-f2b6-538f-b94a-2f83cf4a842a] +name = "StructArrays" +source = "/home/amartins/.julia/packages/StructArrays/fvsNe" +version = "0.7.3" + +[packages.0a4f8689-d25c-4efe-a92b-7142dfc1aa53] +name = "MathTeXEngine" +source = "/home/amartins/.julia/packages/MathTeXEngine/Zd227" +version = "0.6.9" + +[packages.0ac62f75-1d6f-5e53-bd7c-93b484bb37c0] +name = "libass_jll" +source = "/home/amartins/.julia/packages/libass_jll/95TY2" +version = "0.17.5+0" + +[packages.0bca4576-84f4-4d90-8ffe-ffa030f20462] +name = "SciMLBase" +source = "/home/amartins/.julia/packages/SciMLBase/1DcFZ" +version = "3.55.0" + +[packages.0c0b7dd1-d40b-584c-a123-a41640f87eec] +name = "Xorg_libXau_jll" +source = "/home/amartins/.julia/packages/Xorg_libXau_jll/J4NX6" +version = "1.0.13+0" + +[packages.0c0d3e7f-3a8b-4f7e-b6f1-9a4d2e7c1f01] +name = "PureKLU" +source = "/home/amartins/.julia/packages/PureKLU/EusaS" +version = "1.5.0" + +[packages.0c81fc1b-5583-44fc-8770-48be1e1cca08] +name = "InputBuffers" +source = "/home/amartins/.julia/packages/InputBuffers/6eq7q" +version = "1.1.1" + +[packages.0d7ed370-da01-4f52-bd93-41d350b8b718] +name = "StaticArrayInterface" +source = "/home/amartins/.julia/packages/StaticArrayInterface/qmd3i" +version = "1.10.0" + +[packages.0dad84c5-d112-42e6-8d28-ef12dabb789f] +name = "ArgTools" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/ArgTools" +version = "1.1.2" + +[packages.0f8b85d8-7281-11e9-16c2-39a750bddbf1] +name = "JSON3" +source = "/home/amartins/.julia/packages/JSON3/rT1w2" +version = "1.14.3" + +[packages.10745b16-79ce-11e8-11f9-7d13ad32a3b2] +name = "Statistics" +source = "/home/amartins/.julia/packages/Statistics/2IU8F" +version = "1.11.5" + +[packages.1082639a-0dae-5f34-9b06-72781eeb8cb3] +name = "Xorg_libXext_jll" +source = "/home/amartins/.julia/packages/Xorg_libXext_jll/ZknMc" +version = "1.3.8+0" + +[packages.10f19ff3-798f-405d-979b-55457f8fc047] +name = "LayoutPointers" +source = "/home/amartins/.julia/packages/LayoutPointers/nNKcM" +version = "0.1.17" + +[packages.1183f4f0-6f2a-5f1a-908b-139f9cdfea6f] +name = "libdecor_jll" +source = "/home/amartins/.julia/packages/libdecor_jll/Ha22M" +version = "0.2.2+0" + +[packages.1270edf5-f2f9-52d2-97e9-ab00b5d0237a] +name = "x264_jll" +source = "/home/amartins/.julia/packages/x264_jll/fFdOY" +version = "10164.0.1+0" + +[packages.13072b0f-2c55-5437-9ae7-d433b7a33950] +name = "AxisAlgorithms" +source = "/home/amartins/.julia/packages/AxisAlgorithms/08cuY" +version = "1.1.0" + +[packages.1317d2d5-d96f-522e-a858-c73665f53c3e] +name = "oneTBB_jll" +source = "/home/amartins/.julia/packages/oneTBB_jll/IZRZe" +version = "2022.3.0+0" + +[packages.14a3606d-f60d-562e-9121-12d972cd8159] +name = "MozillaCACerts_jll" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/MozillaCACerts_jll" +version = "2025.11.4" + +[packages.14f7f29c-3bd6-536c-9a0b-7339e30b5a3e] +name = "AMD" +source = "/home/amartins/.julia/packages/AMD/XmreB" +version = "0.5.4" + +[packages.1520ce14-60c1-5f80-bbc7-55ef81b5835c] +name = "AbstractTrees" +source = "/home/amartins/.julia/packages/AbstractTrees/Ftf8W" +version = "0.4.5" + +[packages.163ba53b-c6d8-5494-b064-1a9d43ac40c5] +name = "DiffResults" +source = "/home/amartins/.julia/packages/DiffResults/YLo25" +version = "1.1.0" + +[packages.187b0558-2788-49d3-abe0-74a17ed4e7c9] +name = "ConstructionBase" +source = "/home/amartins/.julia/packages/ConstructionBase/3a7dg" +version = "1.6.0" + +[packages.189a3867-3050-52da-a836-e630ba90ab69] +name = "Reexport" +source = "/home/amartins/.julia/packages/Reexport/OxbHO" +version = "1.2.2" + +[packages.18a262bb-aa17-5467-a713-aee519bc75cb] +name = "OpenEXR_jll" +source = "/home/amartins/.julia/packages/OpenEXR_jll/n7HRj" +version = "3.4.15+0" + +[packages.18c40d15-f7cd-5a6d-bc92-87468d86c5db] +name = "LinearElasticity_jll" +source = "/home/amartins/.julia/packages/LinearElasticity_jll/WfjJL" +version = "5.0.1+0" + +[packages.18cc8868-cbac-4acf-b575-c8ff214dc66f] +name = "BaseDirs" +source = "/home/amartins/.julia/packages/BaseDirs/xcQ4N" +version = "1.4.0" + +[packages.18e31ff7-3703-566c-8e60-38913d67486b] +name = "Peaks" +source = "/home/amartins/.julia/packages/Peaks/qZdvG" +version = "0.6.2" + +[packages.18e54dd8-cb9d-406c-a71d-865a43cbb235] +name = "IntegerMathUtils" +source = "/home/amartins/.julia/packages/IntegerMathUtils/ufRWt" +version = "0.1.4" + +[packages.1914dd2f-81c6-5fcd-8719-6d5c9610ff09] +name = "MacroTools" +source = "/home/amartins/.julia/packages/MacroTools/Ar0jT" +version = "0.5.16" + +[packages.1986cc42-f94f-5a68-af5c-568840ba703d] +name = "Unitful" +source = "/home/amartins/.julia/packages/Unitful/ikxOk" +version = "1.29.0" + +[packages.19eb6ba3-879d-56ad-ad62-d5c202156566] +name = "Packing" +source = "/home/amartins/.julia/packages/Packing/Riovd" +version = "0.5.1" + +[packages.19f34311-ddf3-4b8b-af20-060888a46c0e] +name = "SciMLJacobianOperators" +source = "/home/amartins/.julia/packages/SciMLJacobianOperators/yaf1p" +version = "0.1.19" + +[packages.1a1011a3-84de-559e-8e89-a11a2f7dc383] +name = "SharedArrays" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/SharedArrays" +version = "1.11.0" + +[packages.1a297f60-69ca-5386-bcde-b61e274b549b] +name = "FillArrays" +source = "/home/amartins/.julia/packages/FillArrays/zA1cC" +version = "1.17.0" + +[packages.1c621080-faea-4a02-84b6-bbd5e436b8fe] +name = "TestItems" +source = "/home/amartins/.julia/packages/TestItems/FaZSd" +version = "1.1.0" + +[packages.1cfade01-22cf-5700-b092-accc4b62d6e1] +name = "UnicodeFun" +source = "/home/amartins/.julia/packages/UnicodeFun/QjvWn" +version = "0.4.1" + +[packages.1d5cc7b8-4909-519e-a0f8-d0f5ad9712d0] +name = "IntelOpenMP_jll" +source = "/home/amartins/.julia/packages/IntelOpenMP_jll/eZgzz" +version = "2025.2.0+0" + +[packages.1d63c593-3942-5779-bab2-d838dc0a180e] +name = "LLVMOpenMP_jll" +source = "/home/amartins/.julia/packages/LLVMOpenMP_jll/bgCml" +version = "23.1.1+0" + +[packages.1e83bf80-4336-4d27-bf5d-d5a4f845583c] +name = "StaticArraysCore" +source = "/home/amartins/.julia/packages/StaticArraysCore/MoJEf" +version = "1.4.4" + +[packages.1fa38f19-a742-5d3f-a2b9-30dd87b9d5f8] +name = "Format" +source = "/home/amartins/.julia/packages/Format/0LfJN" +version = "1.3.7" + +[packages.1fd47b50-473d-5c70-9696-f719f8f3bcdc] +name = "QuadGK" +source = "/home/amartins/.julia/packages/QuadGK/5mgi5" +version = "2.11.3" + +[packages.2030c09a-7f63-5d83-885d-db604e0e9cc0] +name = "InfrastructureModels" +source = "/home/amartins/.julia/packages/InfrastructureModels/3h9nD" +version = "0.7.9" + +[packages.21216c6a-2e73-6563-6e65-726566657250] +name = "Preferences" +source = "/home/amartins/.julia/packages/Preferences/DwMOj" +version = "1.6.0" + +[packages.21fd56a4-db03-40ee-82ee-a87907bee541] +name = "RobustAndOptimalControl" +source = "/home/amartins/.julia/packages/RobustAndOptimalControl/p3cMV" +version = "0.4.52" + +[packages.2569d6c7-a4a2-43d3-a901-331e8e4be471] +name = "ConcreteStructs" +source = "/home/amartins/.julia/packages/ConcreteStructs/4xZSO" +version = "0.2.8" + +[packages.26075421-4e9a-44e1-8bd1-420ed7ad02b2] +name = "NonlinearSolveSpectralMethods" +source = "/home/amartins/.julia/packages/NonlinearSolveSpectralMethods/VzvhW" +version = "1.8.3" + +[packages.2702e6a9-849d-5ed8-8c21-79e8b8f9ee43] +name = "EpollShim_jll" +source = "/home/amartins/.julia/packages/EpollShim_jll/MeNmS" +version = "0.0.20230411+1" + +[packages.276daf66-3868-5448-9aa4-cd146d93841b] +name = "SpecialFunctions" +source = "/home/amartins/.julia/packages/SpecialFunctions/EJiJa" +version = "2.9.0" + +[packages.2774e3e8-f4cf-5e23-947b-6d7e65073b56] +name = "NLsolve" +source = "/home/amartins/.julia/packages/NLsolve/gJL1I" +version = "4.5.1" + +[packages.27a7e980-b3e6-11e9-2bcd-0b925532e340] +name = "Animations" +source = "/home/amartins/.julia/packages/Animations/OGXDY" +version = "0.4.2" + +[packages.27ebfcd6-29c5-5fa9-bf4b-fb8fc14df3ae] +name = "Primes" +source = "/home/amartins/.julia/packages/Primes/qiMp0" +version = "0.5.7" + +[packages.2803e5a7-5153-5ecf-9a86-9b4c37f5f5ac] +name = "ImageAxes" +source = "/home/amartins/.julia/packages/ImageAxes/1Fb97" +version = "0.6.12" + +[packages.2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91] +name = "StatsBase" +source = "/home/amartins/.julia/packages/StatsBase/TkBxP" +version = "0.34.13" + +[packages.295af30f-e4ad-537b-8983-00126c2a3abe] +name = "Revise" +source = "/home/amartins/.julia/packages/Revise/0LsA7" +version = "3.17.0" + +[packages.29816b5a-b9ab-546f-933c-edad1886dfa8] +name = "LibSSH2_jll" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/LibSSH2_jll" +version = "1.11.3+1" + +[packages.2a0f44e3-6c83-55bd-87e4-b1978d98bd5f] +name = "Base64" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/Base64" +version = "1.11.0" + +[packages.2a0fbf3d-bb9c-48f3-b0a9-814d99fd7ab9] +name = "CPUSummary" +source = "/home/amartins/.julia/packages/CPUSummary/Wjls3" +version = "0.2.7" + +[packages.2ab3a3ac-af41-5b50-aa03-7779005ae688] +name = "LogExpFunctions" +source = "/home/amartins/.julia/packages/LogExpFunctions/LITgj" +version = "1.0.1" + +[packages.2b5f629d-d688-5b77-993f-72d75c75574e] +name = "DiffEqBase" +source = "/home/amartins/.julia/packages/DiffEqBase/wRYJL" +version = "7.21.1" + +[packages.2b9e73d6-9f99-4895-a28a-fad5d1cd90f9] +name = "PowerImpedance" +source = "/home/amartins/Documents/KUL/PowerImpedance.jl" +version = "0.3.0" + +[packages.2c808117-e144-5220-80d1-69d4eaa9352c] +name = "Xorg_libXft_jll" +source = "/home/amartins/.julia/packages/Xorg_libXft_jll/i5Yyq" +version = "2.3.9+0" + +[packages.2dfb63ee-cc39-5dd5-95bd-886bf059d720] +name = "PooledArrays" +source = "/home/amartins/.julia/packages/PooledArrays/Vy2X0" +version = "1.4.3" + +[packages.2e619515-83b5-522b-bb60-26c02a35a201] +name = "Expat_jll" +source = "/home/amartins/.julia/packages/Expat_jll/uTocy" +version = "2.8.4+0" + +[packages.2e76f6c2-a576-52d4-95c1-20adfe4de566] +name = "HarfBuzz_jll" +source = "/home/amartins/.julia/packages/HarfBuzz_jll/SooXK" +version = "100.14004.0+0" + +[packages.2efcf032-c050-4f8e-a9bb-153293bab1f5] +name = "SymbolicIndexingInterface" +source = "/home/amartins/.julia/packages/SymbolicIndexingInterface/faxpV" +version = "0.3.55" + +[packages.2f01184e-e22b-5df5-ae63-d93ebab69eaf] +name = "SparseArrays" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/SparseArrays" +version = "1.12.0" + +[packages.2faa5264-e118-4071-8864-e10559f68c7c] +name = "LHLFactorization" +source = "/home/amartins/.julia/packages/LHLFactorization/OdOla" +version = "2.2.2" + +[packages.2fda8390-95c7-5789-9bda-21331edee243] +name = "LsqFit" +source = "/home/amartins/.julia/packages/LsqFit/rHpK1" +version = "0.15.1" + +[packages.30392449-352a-5448-841d-b1acce4e97dc] +name = "Pixman_jll" +source = "/home/amartins/.julia/packages/Pixman_jll/boKTq" +version = "0.46.4+0" + +[packages.313cdc1a-70c2-5d6a-ae34-0150d3930a38] +name = "Indexing" +source = "/home/amartins/.julia/packages/Indexing/iF84V" +version = "1.1.1" + +[packages.3161d3a3-bdf6-5164-811a-617609db77b4] +name = "Zstd_jll" +source = "/home/amartins/.julia/packages/Zstd_jll/YX1LN" +version = "1.5.7+1" + +[packages.319450e9-13b8-58e8-aa9f-8fd1420848ab] +name = "SPRAL_jll" +source = "/home/amartins/.julia/packages/SPRAL_jll/2bs1h" +version = "2025.9.18+1" + +[packages.31c24e10-a181-5473-b8eb-7969acd0382f] +name = "Distributions" +source = "/home/amartins/.julia/packages/Distributions/RTt4y" +version = "0.25.131" + +[packages.33bec58e-1273-512f-9401-5d533626f822] +name = "Xorg_xkeyboard_config_jll" +source = "/home/amartins/.julia/packages/Xorg_xkeyboard_config_jll/MfIRq" +version = "2.47.0+2" + +[packages.34004b35-14d8-5ef3-9330-4cdb6864b03a] +name = "HypergeometricFunctions" +source = "/home/amartins/.julia/packages/HypergeometricFunctions/O0DoB" +version = "0.3.30" + +[packages.34da2185-b29b-5c13-b0c7-acf172513d20] +name = "Compat" +source = "/home/amartins/.julia/packages/Compat/FiBO2" +version = "4.18.1" + +[packages.35492f91-a3bd-45ad-95db-fcad7dcfedb7] +name = "AdaptivePredicates" +source = "/home/amartins/.julia/packages/AdaptivePredicates/rvPuS" +version = "1.2.0" + +[packages.35661453-b289-5fab-8a00-3d9160c6a3a4] +name = "Xorg_xkbcomp_jll" +source = "/home/amartins/.julia/packages/Xorg_xkbcomp_jll/nNUE1" +version = "1.4.7+0" + +[packages.3587e190-3f89-42d0-90ee-14403ec27112] +name = "InverseFunctions" +source = "/home/amartins/.julia/packages/InverseFunctions/4sFsZ" +version = "0.1.17" + +[packages.35d6a980-a343-548e-a6ea-1d62b119f2f4] +name = "ColorSchemes" +source = "/home/amartins/.julia/packages/ColorSchemes/3BWhh" +version = "3.31.0" + +[packages.36c8627f-9965-5494-a995-c6b170f724f3] +name = "Pango_jll" +source = "/home/amartins/.julia/packages/Pango_jll/16ssY" +version = "1.58.2+0" + +[packages.3783bdb8-4a98-5b6b-af9a-565f29a5fe9c] +name = "TableTraits" +source = "/home/amartins/.julia/packages/TableTraits/o8VMV" +version = "1.0.1" + +[packages.37e2e46d-f89d-539d-b4ee-838fcccc9c8e] +name = "LinearAlgebra" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/LinearAlgebra" +version = "1.12.0" + +[packages.38540f10-b2f7-11e9-35d8-d573e4eb0ff2] +name = "CommonSolve" +source = "/home/amartins/.julia/packages/CommonSolve/zocpq" +version = "0.2.14" + +[packages.38a345b3-de98-5d2b-a5d3-14cd9215e700] +name = "Libuuid_jll" +source = "/home/amartins/.julia/packages/Libuuid_jll/fHaZa" +version = "2.42.0+0" + +[packages.3955a311-db13-416c-9275-1d80ed98e5e9] +name = "GridLayoutBase" +source = "/home/amartins/.julia/packages/GridLayoutBase/K7sJt" +version = "0.11.3" + +[packages.39de3d68-74b9-583c-8d2d-e117c070f3a9] +name = "AxisArrays" +source = "/home/amartins/.julia/packages/AxisArrays/gTmxS" +version = "0.4.8" + +[packages.3a884ed6-31ef-47d7-9d2a-63182c4928ed] +name = "UnPack" +source = "/home/amartins/.julia/packages/UnPack/EkESO" +version = "1.0.2" + +[packages.3b182d85-2403-5c21-9c21-1e1f0cc25472] +name = "Graphite2_jll" +source = "/home/amartins/.julia/packages/Graphite2_jll/qMpcV" +version = "1.3.16+0" + +[packages.3bb67fe8-82b1-5028-8e26-92a6c54297fa] +name = "TranscodingStreams" +source = "/home/amartins/.julia/packages/TranscodingStreams/O3BYF" +version = "0.11.3" + +[packages.3cdcf5f2-1ef4-517c-9805-6587b60abb01] +name = "RecipesBase" +source = "/home/amartins/.julia/packages/RecipesBase/BRe07" +version = "1.3.4" + +[packages.3d5dd08c-fd9d-11e8-17fa-ed2836048c2f] +name = "VectorizationBase" +source = "/home/amartins/.julia/packages/VectorizationBase/F4R96" +version = "0.21.74" + +[packages.3da002f7-5984-5a60-b8a6-cbb66c0b333f] +name = "ColorTypes" +source = "/home/amartins/.julia/packages/ColorTypes/L7biZ" +version = "0.12.1" + +[packages.3da0fdf6-3ccc-4f1b-acd9-58baa6c99267] +name = "MPIPreferences" +source = "/home/amartins/.julia/packages/MPIPreferences/wIB4a" +version = "0.1.12" + +[packages.3e5b6fbb-0976-4d2c-9146-d79de83f2fb0] +name = "HostCPUFeatures" +source = "/home/amartins/.julia/packages/HostCPUFeatures/ZTXz4" +version = "0.1.18" + +[packages.3f19e933-33d8-53b3-aaab-bd5110c3b7a0] +name = "p7zip_jll" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/p7zip_jll" +version = "17.7.0+0" + +[packages.3fa0cd96-eef1-5676-8a61-b3b8758bbffb] +name = "REPL" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/REPL" +version = "1.11.0" + +[packages.4076af6c-e467-56ae-b986-b466b2749572] +name = "JuMP" +source = "/home/amartins/.julia/packages/JuMP/Y8Mtc" +version = "1.31.2" + +[packages.41ab1584-1d38-5bbf-9106-f11c6c58b48f] +name = "InvertedIndices" +source = "/home/amartins/.julia/packages/InvertedIndices/jfcUx" +version = "1.3.1" + +[packages.429524aa-4258-5aef-a3af-852621145aeb] +name = "Optim" +source = "/home/amartins/.julia/packages/Optim/gmigl" +version = "1.13.3" + +[packages.429591f6-91af-11e9-00e2-59fbe8cec110] +name = "ExactPredicates" +source = "/home/amartins/.julia/packages/ExactPredicates/V0dTA" +version = "2.2.9" + +[packages.431bcebd-1456-4ced-9d72-93c2757fff0b] +name = "SciMLPublic" +source = "/home/amartins/.julia/packages/SciMLPublic/hI11w" +version = "1.3.0" + +[packages.43287f4e-b6f4-7ad1-bb20-aadabca52c3d] +name = "PtrArrays" +source = "/home/amartins/.julia/packages/PtrArrays/m7fuD" +version = "1.4.0" + +[packages.44cfe95a-1eb2-52ea-b672-e2afdf69b78f] +name = "Pkg" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/Pkg" +version = "1.12.1" + +[packages.4536629a-c528-5b80-bd46-f80d51c5b363] +name = "OpenBLAS_jll" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/OpenBLAS_jll" +version = "0.3.29+0" + +[packages.45858cf5-a6b0-47a3-bbea-62219f50df47] +name = "Sixel" +source = "/home/amartins/.julia/packages/Sixel/X78VX" +version = "0.1.5" + +[packages.458c3c95-2e84-50aa-8efc-19380b2a3a95] +name = "OpenSSL_jll" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/OpenSSL_jll" +version = "3.5.6+0" + +[packages.459566f4-90b8-5000-8ac3-15dfb0a30def] +name = "DiffEqCallbacks" +source = "/home/amartins/.julia/packages/DiffEqCallbacks/LYx6b" +version = "4.19.4" + +[packages.4607b0f0-06f3-5cda-b6b1-a6196a1729e9] +name = "SuiteSparse" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/SuiteSparse" +version = "nothing" + +[packages.46192b85-c4d5-4398-a991-12ede77f4527] +name = "GPUArraysCore" +source = "/home/amartins/.julia/packages/GPUArraysCore/aNaXo" +version = "0.2.0" + +[packages.46d2c3a1-f734-5fdb-9937-b9b9aeba4221] +name = "MuladdMacro" +source = "/home/amartins/.julia/packages/MuladdMacro/zEkh6" +version = "0.2.7" + +[packages.476501e8-09a2-5ece-8869-fb82de89a1fa] +name = "SLEEFPirates" +source = "/home/amartins/.julia/packages/SLEEFPirates/OTXBS" +version = "0.6.46" + +[packages.477f73a3-ac25-53e9-8cc3-50b2fa2566f0] +name = "libaec_jll" +source = "/home/amartins/.julia/packages/libaec_jll/sha5F" +version = "1.1.7+0" + +[packages.47edcb42-4c32-4615-8424-f2b9edc5f35b] +name = "ADTypes" +source = "/home/amartins/.julia/packages/ADTypes/0z2Sm" +version = "1.24.0" + +[packages.48062228-2e41-5def-b9a4-89aafe57970f] +name = "FilePathsBase" +source = "/home/amartins/.julia/packages/FilePathsBase/NV2We" +version = "0.9.24" + +[packages.48965c70-4690-11ea-1f13-43a2532b2fa8] +name = "MatrixPencils" +source = "/home/amartins/.julia/packages/MatrixPencils/AzObp" +version = "1.9.1" + +[packages.49080126-0e18-4c2a-b176-c102e4b3760c] +name = "ZipArchives" +source = "/home/amartins/.julia/packages/ZipArchives/2srn2" +version = "2.6.0" + +[packages.49dc2e85-a5d0-5ad3-a950-438e2897f1b9] +name = "Calculus" +source = "/home/amartins/.julia/packages/Calculus/sTHut" +version = "0.5.2" + +[packages.4af54fe1-eca0-43a8-85a7-787d91b784e3] +name = "LazyArtifacts" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/LazyArtifacts" +version = "1.11.0" + +[packages.4b2f31a3-9ecc-558c-b454-b3730dcb73e9] +name = "Libmount_jll" +source = "/home/amartins/.julia/packages/Libmount_jll/Obn9P" +version = "2.42.0+0" + +[packages.4b34888f-f399-49d4-9bb3-47ed5cae4e65] +name = "QOI" +source = "/home/amartins/.julia/packages/QOI/NQrwL" +version = "1.0.2" + +[packages.4c555306-a7a7-4459-81d9-ec55ddd5c99a] +name = "ArrayLayouts" +source = "/home/amartins/.julia/packages/ArrayLayouts/RCHQ8" +version = "1.12.2" + +[packages.4c63d2b9-4356-54db-8cca-17b64c39e42c] +name = "StatsFuns" +source = "/home/amartins/.julia/packages/StatsFuns/Qq3vY" +version = "2.2.1" + +[packages.4cefb923-2b6f-58c4-9e65-6821f26b7b51] +name = "Munkres" +source = "/home/amartins/.julia/packages/Munkres/D30ch" +version = "0.2.0" + +[packages.4e289a0a-7415-4d19-859d-a7e5c4648b56] +name = "EnumX" +source = "/home/amartins/.julia/packages/EnumX/CGSQY" +version = "1.0.7" + +[packages.4e9b3aee-d8a1-5a3d-ad8b-7d824db253f0] +name = "CRlibm_jll" +source = "/home/amartins/.julia/packages/CRlibm_jll/0eQYo" +version = "1.0.1+0" + +[packages.4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5] +name = "Unicode" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/Unicode" +version = "1.11.0" + +[packages.4f6342f7-b3d2-589e-9d20-edeb45f2b2bc] +name = "Xorg_libX11_jll" +source = "/home/amartins/.julia/packages/Xorg_libX11_jll/n2y3B" +version = "1.8.13+0" + +[packages.4fba245c-0d91-5ea0-9b3e-6abc04ee57a9] +name = "ArrayInterface" +source = "/home/amartins/.julia/packages/ArrayInterface/TT5ip" +version = "7.30.2" + +[packages.4fce6fc7-ba6a-5f4c-898f-77e99806d6f8] +name = "FLTK_jll" +source = "/home/amartins/.julia/packages/FLTK_jll/UfoZV" +version = "1.3.8+0" + +[packages.510215fc-4207-5dde-b226-833fc4488ee2] +name = "Observables" +source = "/home/amartins/.julia/packages/Observables/YdEbO" +version = "0.5.5" + +[packages.523fee87-0ab8-5b00-afb7-3ecf72e48cfd] +name = "CodecBzip2" +source = "/home/amartins/.julia/packages/CodecBzip2/gnqO5" +version = "0.8.5" + +[packages.52e1d378-f018-4a11-a4be-720524705ac7] +name = "OpenEXR" +source = "/home/amartins/.julia/packages/OpenEXR/KjZGg" +version = "0.3.3" + +[packages.53ae85a6-f571-4167-b2af-e1d143709226] +name = "SciMLStructures" +source = "/home/amartins/.julia/packages/SciMLStructures/YPjs4" +version = "1.10.5" + +[packages.53c48c17-4a7d-5ca2-90c5-79b7896eea93] +name = "FixedPointNumbers" +source = "/home/amartins/.julia/packages/FixedPointNumbers/1T7Qh" +version = "0.8.6" + +[packages.5432bcbf-9aad-5242-b902-cca2824c8663] +name = "PaddedViews" +source = "/home/amartins/.julia/packages/PaddedViews/Zxi9Z" +version = "0.5.12" + +[packages.559328eb-81f9-559d-9380-de523a88c83c] +name = "FriBidi_jll" +source = "/home/amartins/.julia/packages/FriBidi_jll/pOqh0" +version = "1.0.17+0" + +[packages.56ddb016-857b-54e1-b83d-db4d58db5568] +name = "Logging" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/Logging" +version = "1.11.0" + +[packages.56f22d72-fd6d-98f1-02f0-08ddc0907c33] +name = "Artifacts" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/Artifacts" +version = "1.11.0" + +[packages.5789e2e9-d7fb-5bc7-8068-2c6fae9b9549] +name = "FileIO" +source = "/home/amartins/.julia/packages/FileIO/pFGbU" +version = "1.20.0" + +[packages.5959db7a-ea39-4486-b5fe-2dd0bf03d60d] +name = "NonlinearSolveFirstOrder" +source = "/home/amartins/.julia/packages/NonlinearSolveFirstOrder/HPOPL" +version = "2.8.0" + +[packages.59f7168a-df46-5410-90c8-f2779963d0ec] +name = "Giflib_jll" +source = "/home/amartins/.julia/packages/Giflib_jll/8qnLE" +version = "5.2.3+0" + +[packages.5ab0869b-81aa-558d-bb23-cbf5423bbe9b] +name = "KernelDensity" +source = "/home/amartins/.julia/packages/KernelDensity/uVtNL" +version = "0.6.12" + +[packages.5ae413db-bbd1-5e63-b57d-d24a61df00f5] +name = "EarCut_jll" +source = "/home/amartins/.julia/packages/EarCut_jll/vCK9i" +version = "2.2.4+0" + +[packages.5ae59095-9a9b-59fe-a467-6f913c188581] +name = "Colors" +source = "/home/amartins/.julia/packages/Colors/VFEJ1" +version = "0.13.1" + +[packages.5c1252a2-5f33-56bf-86c9-59e7332b4326] +name = "GeometryBasics" +source = "/home/amartins/.julia/packages/GeometryBasics/eeEec" +version = "0.5.12" + +[packages.5eaf0fd0-dfba-4ccb-bf02-d820a40db705] +name = "RoundingEmulator" +source = "/home/amartins/.julia/packages/RoundingEmulator/5KuEV" +version = "0.2.1" + +[packages.615f187c-cbe4-4ef1-ba3b-2fcf58d6d173] +name = "IfElse" +source = "/home/amartins/.julia/packages/IfElse/dlhAd" +version = "0.1.1" + +[packages.621f4979-c628-5d54-868e-fcf4e3e8185c] +name = "AbstractFFTs" +source = "/home/amartins/.julia/packages/AbstractFFTs/4iQz5" +version = "1.5.0" + +[packages.62783981-4cbd-42fc-bca8-16325de8dc4b] +name = "BitTwiddlingConvenienceFunctions" +source = "/home/amartins/.julia/packages/BitTwiddlingConvenienceFunctions/q3iiO" +version = "0.1.6" + +[packages.62fd8b95-f654-4bbd-a8a5-9c27f68ccd50] +name = "TensorCore" +source = "/home/amartins/.julia/packages/TensorCore/77QBu" +version = "0.1.1" + +[packages.630162c2-fc9b-58b3-9910-8442a8a132e6] +name = "gmsh_jll" +source = "/home/amartins/.julia/packages/gmsh_jll/YykAt" +version = "4.15.0+0" + +[packages.6309b1aa-fc58-479c-8956-599a07234577] +name = "CodecInflate64" +source = "/home/amartins/.julia/packages/CodecInflate64/81Eoa" +version = "0.1.3" + +[packages.6462fe0b-24de-5631-8697-dd941f90decc] +name = "Sockets" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/Sockets" +version = "1.11.0" + +[packages.647866c9-e3ac-4575-94e7-e3d426903924] +name = "PolygonOps" +source = "/home/amartins/.julia/packages/PolygonOps/SnsUB" +version = "0.1.2" + +[packages.64ca27bc-2ba2-4a57-88aa-44e436879224] +name = "FindFirstFunctions" +source = "/home/amartins/.julia/packages/FindFirstFunctions/Rer7g" +version = "3.2.1" + +[packages.65257c39-d410-5151-9873-9b3e5be5013e] +name = "ShaderAbstractions" +source = "/home/amartins/.julia/packages/ShaderAbstractions/BCZHP" +version = "0.5.0" + +[packages.656ef2d0-ae68-5445-9ca0-591084a874a2] +name = "OpenBLAS32_jll" +source = "/home/amartins/.julia/packages/OpenBLAS32_jll/BP214" +version = "0.3.34+0" + +[packages.663a7486-cb36-511b-a19d-713bb74d65c9] +name = "FreeTypeAbstraction" +source = "/home/amartins/.julia/packages/FreeTypeAbstraction/GGCCW" +version = "0.10.8" + +[packages.66dad0bd-aa9a-41b7-9441-69ab47430ed8] +name = "AliasTables" +source = "/home/amartins/.julia/packages/AliasTables/N6kwW" +version = "1.1.3" + +[packages.66fc600b-dfda-50eb-8b99-91cfa97b1301] +name = "ModernGL" +source = "/home/amartins/.julia/packages/ModernGL/BUvna" +version = "1.1.8" + +[packages.67c07d97-cdcb-5c2c-af73-a7f9c32a568b] +name = "Automa" +source = "/home/amartins/.julia/packages/Automa/fzRJn" +version = "1.2.0" + +[packages.682c06a0-de6a-54ab-a142-c8b1cf79cde6] +name = "JSON" +source = "/home/amartins/.julia/packages/JSON/tMaHX" +version = "1.8.0" + +[packages.692b3bcd-3c85-4b1f-b108-f13ce0eb3210] +name = "JLLWrappers" +source = "/home/amartins/.julia/packages/JLLWrappers/Kp7TC" +version = "1.8.0" + +[packages.699a6c99-e7fa-54fc-8d76-47d257e15c1d] +name = "SimpleTraits" +source = "/home/amartins/.julia/packages/SimpleTraits/nEtvW" +version = "0.9.6" + +[packages.69de0a69-1ddd-5017-9359-2bf0b02dc9f0] +name = "Parsers" +source = "/home/amartins/.julia/packages/Parsers/xi9N1" +version = "2.8.8" + +[packages.6a86dc24-6348-571c-b903-95158fe2bd41] +name = "FiniteDiff" +source = "/home/amartins/.julia/packages/FiniteDiff/Z4dvL" +version = "2.33.0" + +[packages.6b39b394-51ab-5f42-8807-6242bab2b4c2] +name = "CodecZstd" +source = "/home/amartins/.julia/packages/CodecZstd/dNeRy" +version = "0.8.7" + +[packages.6c6a2e73-6563-6170-7368-637461726353] +name = "Scratch" +source = "/home/amartins/.julia/packages/Scratch/tDtSY" +version = "1.3.0" + +[packages.6cdc7f73-28fd-5e50-80fb-958a8875b1af] +name = "OpenBLASConsistentFPCSR_jll" +source = "/home/amartins/.julia/packages/OpenBLASConsistentFPCSR_jll/3j4TV" +version = "0.3.34+0" + +[packages.6e34b625-4abd-537c-b88f-471c36dfa7a0] +name = "Bzip2_jll" +source = "/home/amartins/.julia/packages/Bzip2_jll/eIOLw" +version = "1.0.9+0" + +[packages.6f1432cf-f94c-5a45-995e-cdbf5db27b0b] +name = "LoweredCodeUtils" +source = "/home/amartins/.julia/packages/LoweredCodeUtils/LjxQL" +version = "3.8.0" + +[packages.6fe1bfb0-de20-5000-8ca7-80f57d26f881] +name = "OffsetArrays" +source = "/home/amartins/.julia/packages/OffsetArrays/b7qpm" +version = "1.17.0" + +[packages.7034ab61-46d4-4ed7-9d0f-46aef9175898] +name = "FastBroadcast" +source = "/home/amartins/.julia/packages/FastBroadcast/FugmW" +version = "1.4.0" + +[packages.705231aa-382f-11e9-3f0c-b7cb4346fdeb] +name = "Gmsh" +source = "/home/amartins/.julia/packages/Gmsh/Yk7b3" +version = "0.3.1" + +[packages.70df07ce-3d50-431d-a3e7-ca6ddb60ac1e] +name = "BracketingNonlinearSolve" +source = "/home/amartins/.julia/packages/BracketingNonlinearSolve/riRCN" +version = "1.12.7" + +[packages.7269a6da-0436-5bbc-96c2-40638cbb6118] +name = "MeshIO" +source = "/home/amartins/.julia/packages/MeshIO/K87As" +version = "0.5.4" + +[packages.727e6d20-b764-4bd8-a329-72de5adea6c7] +name = "SimpleNonlinearSolve" +source = "/home/amartins/.julia/packages/SimpleNonlinearSolve/4wDnl" +version = "2.14.5" + +[packages.72c71f33-b9b6-44de-8c94-c961784809e2] +name = "XML" +source = "/home/amartins/.julia/packages/XML/sBoky" +version = "0.4.6" + +[packages.731186ca-8d62-57ce-b412-fbd966d074cd] +name = "RecursiveArrayTools" +source = "/home/amartins/.julia/packages/RecursiveArrayTools/9DBPt" +version = "4.5.1" + +[packages.731e570b-9d59-4bfa-96dc-6df516fadf69] +name = "TiffImages" +source = "/home/amartins/.julia/packages/TiffImages/VlJh4" +version = "0.11.9" + +[packages.73760f76-fbc4-59ce-8f25-708e95d2df96] +name = "SignedDistanceFields" +source = "/home/amartins/.julia/packages/SignedDistanceFields/TUaux" +version = "0.4.1" + +[packages.76f85450-5226-5b5a-8eaa-529ad045b433] +name = "LibGit2" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/LibGit2" +version = "1.11.0" + +[packages.7746bdde-850d-59dc-9ae8-88ece973131d] +name = "Glib_jll" +source = "/home/amartins/.julia/packages/Glib_jll/d3glN" +version = "2.88.3+0" + +[packages.77ba4419-2d1f-58cd-9bb1-8ffee604a2e3] +name = "NaNMath" +source = "/home/amartins/.julia/packages/NaNMath/1MJdn" +version = "1.1.4" + +[packages.77dc65aa-8811-40c2-897b-53d922fa7daf] +name = "FunctionWrappersWrappers" +source = "/home/amartins/.julia/packages/FunctionWrappersWrappers/7CPBX" +version = "1.13.0" + +[packages.781609d7-10c4-51f6-84f2-b8444358ff6d] +name = "GMP_jll" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/GMP_jll" +version = "6.3.0+2" + +[packages.781d530d-4396-4725-bb49-402e4bee1e77] +name = "TruncatedStacktraces" +source = "/home/amartins/.julia/packages/TruncatedStacktraces/wmUUK" +version = "1.4.0" + +[packages.79098fc4-a85e-5d69-aa6a-4863f24498fa] +name = "Rmath" +source = "/home/amartins/.julia/packages/Rmath/SBB4Z" +version = "0.9.0" + +[packages.79e6a3ab-5dfb-504d-930d-738a2a938a0e] +name = "Adapt" +source = "/home/amartins/.julia/packages/Adapt/EMZqd" +version = "4.7.1" + +[packages.7a12625a-238d-50fd-b39a-03d52299707e] +name = "LinearMaps" +source = "/home/amartins/.julia/packages/LinearMaps/KV78T" +version = "3.11.4" + +[packages.7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee] +name = "FileWatching" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/FileWatching" +version = "1.11.0" + +[packages.7cb0a576-ebde-5e09-9194-50597f1243b4] +name = "MPICH_jll" +source = "/home/amartins/.julia/packages/MPICH_jll/F2DHh" +version = "4.3.2+0" + +[packages.7d9f7c33-5ae7-4f3b-8dc6-eff91059b697] +name = "Accessors" +source = "/home/amartins/.julia/packages/Accessors/e7kZq" +version = "0.1.45" + +[packages.7e49a35a-f44a-4d26-94aa-eba1b4ca6b47] +name = "RuntimeGeneratedFunctions" +source = "/home/amartins/.julia/packages/RuntimeGeneratedFunctions/xVoUB" +version = "0.5.26" + +[packages.7e76a0d4-f3c7-5321-8279-8d96eeed0f29] +name = "Libglvnd_jll" +source = "/home/amartins/.julia/packages/Libglvnd_jll/upkN8" +version = "1.7.1+1" + +[packages.7ed4a6bd-45f5-4d41-b270-4a48e9bafcae] +name = "LinearSolve" +source = "/home/amartins/.julia/packages/LinearSolve/jTUFm" +version = "5.17.5" + +[packages.807dbc54-b67e-4c79-8afb-eafe4df6f2e1] +name = "Compiler" +source = "/home/amartins/.julia/packages/Compiler/dYFDw" +version = "0.1.1" + +[packages.8197267c-284f-5f27-9208-e0e47529a953] +name = "IntervalSets" +source = "/home/amartins/.julia/packages/IntervalSets/2xOE8" +version = "0.7.14" + +[packages.82899510-4779-5014-852e-03e436cf321d] +name = "IteratorInterfaceExtensions" +source = "/home/amartins/.julia/packages/IteratorInterfaceExtensions/NZdaj" +version = "1.0.0" + +[packages.82ae8749-77ed-4fe6-ae5f-f523153014b0] +name = "StatsAPI" +source = "/home/amartins/.julia/packages/StatsAPI/b8dYo" +version = "1.8.0" + +[packages.82e4d734-157c-48bb-816b-45c225c6df19] +name = "ImageIO" +source = "/home/amartins/.julia/packages/ImageIO/Ydbk5" +version = "0.6.10" + +[packages.83423d85-b0ee-5818-9007-b63ccbeb887a] +name = "Cairo_jll" +source = "/home/amartins/.julia/packages/Cairo_jll/qZ5M2" +version = "1.18.7+0" + +[packages.83775a58-1f1d-513f-b197-d71354ab007a] +name = "Zlib_jll" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/Zlib_jll" +version = "1.3.1+2" + +[packages.842dd82b-1e85-43dc-bf29-5d0ee9dffc48] +name = "InlineStrings" +source = "/home/amartins/.julia/packages/InlineStrings/3hOt5" +version = "1.4.6" + +[packages.856f044c-d86e-5d09-b602-aeab76dc8ba7] +name = "MKL_jll" +source = "/home/amartins/.julia/packages/MKL_jll/A29wq" +version = "2025.2.0+0" + +[packages.856f2bd8-1eba-4b0a-8007-ebc267875bd4] +name = "StructTypes" +source = "/home/amartins/.julia/packages/StructTypes/PaLwj" +version = "1.11.0" + +[packages.85a47980-9c8c-11e8-2b9f-f7ca1fa99fb4] +name = "Dictionaries" +source = "/home/amartins/.julia/packages/Dictionaries/P05gc" +version = "0.4.6" + +[packages.85a6dd25-e78a-55b7-8502-1745935b8125] +name = "PositiveFactorizations" +source = "/home/amartins/.julia/packages/PositiveFactorizations/BN7VB" +version = "0.2.4" + +[packages.86086c02-e288-5929-a127-40944b0018b7] +name = "MMG_jll" +source = "/home/amartins/.julia/packages/MMG_jll/rtCl3" +version = "5.8.0+0" + +[packages.860ef19b-820b-49d6-a774-d7a799459cd3] +name = "StableRNGs" +source = "/home/amartins/.julia/packages/StableRNGs/yMYQp" +version = "1.0.4" + +[packages.864edb3b-99cc-5e75-8d2d-829cb0a9cfe8] +name = "DataStructures" +source = "/home/amartins/.julia/packages/DataStructures/IwRP2" +version = "0.19.6" + +[packages.87fe0de2-c867-4266-b59a-2f0a94fc965b] +name = "LineSearch" +source = "/home/amartins/.julia/packages/LineSearch/AH6Up" +version = "0.1.18" + +[packages.88015f11-f218-50d7-93a8-a6af411a945d] +name = "LERC_jll" +source = "/home/amartins/.julia/packages/LERC_jll/vI05H" +version = "4.2.0+0" + +[packages.8913a72c-1f9b-4ce2-8d82-65094dcecaec] +name = "NonlinearSolve" +source = "/home/amartins/.julia/packages/NonlinearSolve/CN1dw" +version = "4.28.0" + +[packages.892a3eda-7b42-436c-8928-eab12a02cf0e] +name = "StringManipulation" +source = "/home/amartins/.julia/packages/StringManipulation/up6yo" +version = "0.5.0" + +[packages.89763e89-9b03-5906-acba-b20f662cd828] +name = "Libtiff_jll" +source = "/home/amartins/.julia/packages/Libtiff_jll/5PvcP" +version = "4.7.3+0" + +[packages.8ba89e20-285c-5b6f-9357-94700520ee1b] +name = "Distributed" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/Distributed" +version = "1.11.0" + +[packages.8bb1440f-4735-579b-a4ab-409b98df4dab] +name = "DelimitedFiles" +source = "/home/amartins/.julia/packages/DelimitedFiles/aGcsu" +version = "1.9.1" + +[packages.8bf52ea8-c179-5cab-976a-9e18b702a9bc] +name = "CRC32c" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/CRC32c" +version = "1.11.0" + +[packages.8cdb02fc-e678-4876-92c5-9defec4f444e] +name = "LazyModules" +source = "/home/amartins/.julia/packages/LazyModules/d9Be6" +version = "0.3.1" + +[packages.8dfed614-e22c-5e08-85e1-65c5234f0b40] +name = "Test" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/Test" +version = "1.11.0" + +[packages.8e53e030-5e6c-5a89-a30b-be5b7263a166] +name = "libdrm_jll" +source = "/home/amartins/.julia/packages/libdrm_jll/0jtGm" +version = "2.4.134+0" + +[packages.8e850b90-86db-534c-a0d3-1478176c7d93] +name = "libblastrampoline_jll" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/libblastrampoline_jll" +version = "5.15.0+0" + +[packages.8e850ede-7688-5339-a07c-302acd2aaf8d] +name = "nghttp2_jll" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/nghttp2_jll" +version = "1.64.0+1" + +[packages.8f399da3-3557-5675-b5ff-fb832c97cbdb] +name = "Libdl" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/Libdl" +version = "1.11.0" + +[packages.8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615] +name = "EzXML" +source = "/home/amartins/.julia/packages/EzXML/ROYMh" +version = "1.2.3" + +[packages.8fc22ac5-c921-52a6-82fd-178b2807b824] +name = "FilePaths" +source = "/home/amartins/.julia/packages/FilePaths/3Niq6" +version = "0.9.0" + +[packages.90014a1f-27ba-587c-ab20-58faa44d9150] +name = "PDMats" +source = "/home/amartins/.julia/packages/PDMats/ZCpqF" +version = "0.11.41" + +[packages.90137ffa-7385-5640-81b9-e52037218182] +name = "StaticArrays" +source = "/home/amartins/.julia/packages/StaticArrays/k7bbZ" +version = "1.9.20" + +[packages.905a6f67-0a94-5f89-b386-d35d92009cd1] +name = "Imath_jll" +source = "/home/amartins/.julia/packages/Imath_jll/hEXZc" +version = "3.2.2+0" + +[packages.91c51154-3ec4-41a3-a24f-3f23e20d615c] +name = "SentinelArrays" +source = "/home/amartins/.julia/packages/SentinelArrays/YQPM3" +version = "1.4.10" + +[packages.91d4177d-7536-5919-b921-800302f37372] +name = "Opus_jll" +source = "/home/amartins/.julia/packages/Opus_jll/1vPwF" +version = "1.6.1+0" + +[packages.9237b28f-5490-5468-be7b-bb81f5f5e6cf] +name = "MicrosoftMPI_jll" +source = "/home/amartins/.julia/packages/MicrosoftMPI_jll/D5Wy1" +version = "10.1.4+3" + +[packages.927a84f5-c5f4-47a5-9785-b46e178433df] +name = "DelaunayTriangulation" +source = "/home/amartins/.julia/packages/DelaunayTriangulation/iZKpa" +version = "1.6.7" + +[packages.92933f4c-e287-5a05-a399-4b506db050ca] +name = "ProgressMeter" +source = "/home/amartins/.julia/packages/ProgressMeter/N660J" +version = "1.11.0" + +[packages.92d709cd-6900-40b7-9082-c6be49f344b6] +name = "IrrationalConstants" +source = "/home/amartins/.julia/packages/IrrationalConstants/RokwY" +version = "0.2.6" + +[packages.935fb764-8cf2-53bf-bb30-45bb1f8bf724] +name = "Xorg_libXcursor_jll" +source = "/home/amartins/.julia/packages/Xorg_libXcursor_jll/drhiH" +version = "1.2.4+0" + +[packages.944b1d66-785c-5afd-91f1-9de20f533193] +name = "CodecZlib" +source = "/home/amartins/.julia/packages/CodecZlib/BjTS1" +version = "0.7.9" + +[packages.94ce4f54-9a6c-5748-9c1c-f9c7231a4531] +name = "Libiconv_jll" +source = "/home/amartins/.julia/packages/Libiconv_jll/sE4g7" +version = "1.18.0+0" + +[packages.94e857df-77ce-4151-89e5-788b33177be4] +name = "SIMDTypes" +source = "/home/amartins/.julia/packages/SIMDTypes/8cg0U" +version = "0.1.0" + +[packages.95dc2771-c249-4cd0-9c9f-1f3b4330693c] +name = "ComputePipeline" +source = "/home/amartins/.julia/packages/ComputePipeline/E2l50" +version = "0.1.8" + +[packages.96374032-68de-5a5b-8d9e-752f78720389] +name = "CRlibm" +source = "/home/amartins/.julia/packages/CRlibm/A3Rgc" +version = "1.0.2" + +[packages.9672c7b4-1e72-59bd-8a11-6ac3964bc41f] +name = "SteadyStateDiffEq" +source = "/home/amartins/.julia/packages/SteadyStateDiffEq/daMik" +version = "2.16.0" + +[packages.97f35ef4-7bc5-4ec1-a41a-dcc69c7308c6] +name = "RequiredInterfaces" +source = "/home/amartins/.julia/packages/RequiredInterfaces/FQnDq" +version = "0.1.7" + +[packages.981d1d27-644d-49a2-9326-4793e63143c3] +name = "TriplotBase" +source = "/home/amartins/.julia/packages/TriplotBase/I5oWP" +version = "0.1.0" + +[packages.995b91a9-d308-5afd-9ec6-746e21dbc043] +name = "PlotUtils" +source = "/home/amartins/.julia/packages/PlotUtils/HX80C" +version = "1.4.4" + +[packages.99c1a7ee-ab34-5fd5-8076-27c950a045f4] +name = "MatrixEquations" +source = "/home/amartins/.julia/packages/MatrixEquations/8YHP3" +version = "2.6.6" + +[packages.9a156e7d-b971-5f62-b2c9-67348b8fb97c] +name = "libva_jll" +source = "/home/amartins/.julia/packages/libva_jll/ZulEV" +version = "2.23.0+0" + +[packages.9a2c21bd-3a47-402d-9113-8faf9a0ee114] +name = "NonlinearSolveQuasiNewton" +source = "/home/amartins/.julia/packages/NonlinearSolveQuasiNewton/rzYZ4" +version = "1.15.3" + +[packages.9a3f8284-a2c9-5f02-9a11-845980a1fd5c] +name = "Random" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/Random" +version = "1.11.0" + +[packages.9a68df92-36a6-505f-a73e-abb412b6bfb4] +name = "isoband_jll" +source = "/home/amartins/.julia/packages/isoband_jll/yAVhA" +version = "0.2.3+0" + +[packages.9a962f9c-6df0-11e9-0e5d-c546b8b5ee8a] +name = "DataAPI" +source = "/home/amartins/.julia/packages/DataAPI/atdEM" +version = "1.16.0" + +[packages.9aa1b823-49e4-5ca5-8b0f-3971ec8bab6a] +name = "FastClosures" +source = "/home/amartins/.julia/packages/FastClosures/3LNoZ" +version = "0.3.2" + +[packages.9b13fd28-a010-5f03-acff-a1bbcff69959] +name = "IndirectArrays" +source = "/home/amartins/.julia/packages/IndirectArrays/BUQO3" +version = "1.0.0" + +[packages.9cc047cb-c261-5740-88fc-0cf96f7bdcc7] +name = "Ipopt_jll" +source = "/home/amartins/.julia/packages/Ipopt_jll/dtdDk" +version = "300.1400.1902+0" + +[packages.9d95f2ec-7b3d-5a63-8d20-e2491e220bb9] +name = "TypedTables" +source = "/home/amartins/.julia/packages/TypedTables/ItVth" +version = "1.4.6" + +[packages.9e88b42a-f829-5b0c-bbe9-9e923198166b] +name = "Serialization" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/Serialization" +version = "1.11.0" + +[packages.9fa8497b-333b-5362-9e8d-4d0656e87820] +name = "Future" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/Future" +version = "1.11.0" + +[packages.9fe22ead-9e00-4db2-8b46-706a60d40f5e] +name = "RespecializeParams" +source = "/home/amartins/.julia/packages/RespecializeParams/fhYLh" +version = "1.3.0" + +[packages.a0844989-3bd2-4988-8bea-c9407ab0941b] +name = "Gamma" +source = "/home/amartins/.julia/packages/Gamma/Xa01a" +version = "1.2.0" + +[packages.a09fc81d-aa75-5fe9-8630-4744c3626534] +name = "ImageCore" +source = "/home/amartins/.julia/packages/ImageCore/0ilgc" +version = "0.10.5" + +[packages.a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63] +name = "DifferentiationInterface" +source = "/home/amartins/.julia/packages/DifferentiationInterface/seMaz" +version = "0.7.21" + +[packages.a2964d1f-97da-50d4-b82a-358c7fce9d89] +name = "Wayland_jll" +source = "/home/amartins/.julia/packages/Wayland_jll/m5orr" +version = "1.24.0+0" + +[packages.a2af1166-a08f-5f64-846c-94a0d3cef48c] +name = "SortingAlgorithms" +source = "/home/amartins/.julia/packages/SortingAlgorithms/ZonD6" +version = "1.2.3" + +[packages.a2cac450-b92f-5266-8821-25eda20663c8] +name = "ColorBrewer" +source = "/home/amartins/.julia/packages/ColorBrewer/Ic9lh" +version = "0.4.2" + +[packages.a33af91c-f02d-484b-be07-31d278c5ca2b] +name = "CompositionsBase" +source = "/home/amartins/.julia/packages/CompositionsBase/IN0VV" +version = "0.1.2" + +[packages.a3789734-cfe1-5b06-b2d0-1dd0d9d62d05] +name = "Xorg_libXdmcp_jll" +source = "/home/amartins/.julia/packages/Xorg_libXdmcp_jll/tJKX9" +version = "1.1.6+0" + +[packages.a38c48d9-6df1-5ac9-9223-b6ada3b5572b] +name = "CoreMath_jll" +source = "/home/amartins/.julia/packages/CoreMath_jll/p6YLU" +version = "0.1.0+0" + +[packages.a3b82374-2e81-5b9e-98ce-41277c0e4c87] +name = "MatrixFactorizations" +source = "/home/amartins/.julia/packages/MatrixFactorizations/r2wSS" +version = "3.1.3" + +[packages.a3f928ae-7b40-5064-980b-68af3947d34b] +name = "Fontconfig_jll" +source = "/home/amartins/.julia/packages/Fontconfig_jll/SN8rj" +version = "2.17.1+0" + +[packages.a4ae2306-e953-59d6-aa16-d00cac43593b] +name = "libaom_jll" +source = "/home/amartins/.julia/packages/libaom_jll/nuw9H" +version = "3.14.1+0" + +[packages.a4df4552-cc26-4903-aec0-212e50a0e84b] +name = "FastPower" +source = "/home/amartins/.julia/packages/FastPower/soIUe" +version = "1.5.0" + +[packages.a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e] +name = "Tar" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/Tar" +version = "1.10.0" + +[packages.a51aa0fd-4e3c-5386-b890-e753decda492] +name = "Xorg_libXi_jll" +source = "/home/amartins/.julia/packages/Xorg_libXi_jll/KWLGF" +version = "1.8.4+0" + +[packages.a57abbd0-fea5-4d57-96be-5e525945e8e4] +name = "SparseColumnPivotedQR" +source = "/home/amartins/.julia/packages/SparseColumnPivotedQR/SPDic" +version = "2.1.8" + +[packages.a63ad114-7e13-5084-954f-fe012c677804] +name = "Mmap" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/Mmap" +version = "1.11.0" + +[packages.a65dc6b1-eb27-53a1-bb3e-dea574b5389e] +name = "Xorg_libpciaccess_jll" +source = "/home/amartins/.julia/packages/Xorg_libpciaccess_jll/6MCDN" +version = "0.19.0+0" + +[packages.a6db7da4-7206-11f0-1eab-35f2a5dbe1d1] +name = "SciMLLogging" +source = "/home/amartins/.julia/packages/SciMLLogging/zP0QP" +version = "2.1.0" + +[packages.a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f] +name = "TimerOutputs" +source = "/home/amartins/.julia/packages/TimerOutputs/yewJH" +version = "1.2.1" + +[packages.a81e2ce2-54d1-11eb-2c75-db236b00f339] +name = "DescriptorSystems" +source = "/home/amartins/.julia/packages/DescriptorSystems/b6V8a" +version = "1.5.0" + +[packages.a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f] +name = "Crayons" +source = "/home/amartins/.julia/packages/Crayons/mwQuU" +version = "4.2.0" + +[packages.a8d0f55d-b80e-548d-aff6-1a04c175f0f9] +name = "SCOTCH_jll" +source = "/home/amartins/.julia/packages/SCOTCH_jll/mstUT" +version = "7.0.11+0" + +[packages.a93c6f00-e57d-5684-b7b6-d8193f3e46c0] +name = "DataFrames" +source = "/home/amartins/.julia/packages/DataFrames/0Y1g5" +version = "1.8.2" + +[packages.a98d9a8b-a2ab-59e6-89dd-64a1c18fca59] +name = "Interpolations" +source = "/home/amartins/.julia/packages/Interpolations/rXcvn" +version = "0.16.3" + +[packages.aa1ae85d-cabe-5617-a682-6adf51b2e16a] +name = "JuliaInterpreter" +source = "/home/amartins/.julia/packages/JuliaInterpreter/c4WlU" +version = "0.11.4" + +[packages.aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e] +name = "ControlSystemsBase" +source = "/home/amartins/.julia/packages/ControlSystemsBase/VGC12" +version = "1.21.0" + +[packages.aacddb02-875f-59d6-b918-886e6ef4fbf8] +name = "JpegTurbo_jll" +source = "/home/amartins/.julia/packages/JpegTurbo_jll/dWdb4" +version = "3.2.0+1" + +[packages.aae01518-5342-5314-be14-df237901396f] +name = "BandedMatrices" +source = "/home/amartins/.julia/packages/BandedMatrices/Kml90" +version = "1.12.0" + +[packages.ac6e5ff7-fb65-4e79-a425-ec3bc9c03011] +name = "JuliaSyntaxHighlighting" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/JuliaSyntaxHighlighting" +version = "1.12.0" + +[packages.adafc99b-e345-5852-983c-f28acb93d879] +name = "CpuId" +source = "/home/amartins/.julia/packages/CpuId/n7zKC" +version = "0.3.1" + +[packages.ade2ca70-3891-5945-98fb-dc099432e06a] +name = "Dates" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/Dates" +version = "1.11.0" + +[packages.ae029012-a4dd-5104-9daa-d747884805df] +name = "Requires" +source = "/home/amartins/.julia/packages/Requires/1eCOK" +version = "1.3.1" + +[packages.ae81ac8f-d209-56e5-92de-9978fef736f9] +name = "ASL_jll" +source = "/home/amartins/.julia/packages/ASL_jll/HkA3G" +version = "0.1.5+0" + +[packages.aea7be01-6a6a-4083-8856-8a6e6704d82a] +name = "PrecompileTools" +source = "/home/amartins/.julia/packages/PrecompileTools/QUxvR" +version = "1.3.4" + +[packages.aedffcd0-7271-4cad-89d0-dc628f76c6d3] +name = "Static" +source = "/home/amartins/.julia/packages/Static/WX7YF" +version = "1.4.6" + +[packages.b0724c58-0f36-5564-988d-3bb0596ebc4a] +name = "GettextRuntime_jll" +source = "/home/amartins/.julia/packages/GettextRuntime_jll/oLbdj" +version = "0.22.4+0" + +[packages.b0b7db55-cfe3-40fc-9ded-d10e2dbeff66] +name = "ComponentArrays" +source = "/home/amartins/.julia/packages/ComponentArrays/ZG0hs" +version = "0.15.49" + +[packages.b22a6f82-2f65-5046-a5b2-351ab43fb4e5] +name = "FFMPEG_jll" +source = "/home/amartins/.julia/packages/FFMPEG_jll/Bz1KQ" +version = "8.1.2+0" + +[packages.b27032c2-a3e7-50c8-80cd-2d36dbcbfd21] +name = "LibCURL" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/LibCURL" +version = "0.6.4" + +[packages.b38be410-82b0-50bf-ab77-7b57e271db43] +name = "FreeType" +source = "/home/amartins/.julia/packages/FreeType/ATaWH" +version = "4.1.1" + +[packages.b3c3ace0-ae52-54e7-9d0b-2c1406fd6b9d] +name = "RangeArrays" +source = "/home/amartins/.julia/packages/RangeArrays/vV4Oe" +version = "0.3.2" + +[packages.b4f34e82-e78d-54a5-968a-f98e89d6e8f7] +name = "Distances" +source = "/home/amartins/.julia/packages/Distances/mlz7p" +version = "0.10.12" + +[packages.b53b4c65-9356-5827-b1ea-8c7a1a84506f] +name = "libpng_jll" +source = "/home/amartins/.julia/packages/libpng_jll/lJMP0" +version = "1.6.58+0" + +[packages.b552c78f-8df3-52c6-915a-8e097449b14b] +name = "DiffRules" +source = "/home/amartins/.julia/packages/DiffRules/7zWhL" +version = "1.16.0" + +[packages.b6b21f68-93f8-5de0-b562-5493be1d77c9] +name = "Ipopt" +source = "/home/amartins/.julia/packages/Ipopt/JN6dO" +version = "1.16.0" + +[packages.b77e0a4c-d291-57a0-90e8-8db25a27a240] +name = "InteractiveUtils" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/InteractiveUtils" +version = "1.11.0" + +[packages.b7a15901-be09-4a0e-87d2-2e66b0e09b5a] +name = "CoreMath" +source = "/home/amartins/.julia/packages/CoreMath/8F08b" +version = "0.1.0" + +[packages.b835a17e-a41a-41e7-81f0-2f016b05efe0] +name = "JpegTurbo" +source = "/home/amartins/.julia/packages/JpegTurbo/Fr9ri" +version = "0.1.6" + +[packages.b86e33f2-c0db-4aa1-a6e0-ab43e668529e] +name = "FFTA" +source = "/home/amartins/.julia/packages/FFTA/fhqKV" +version = "0.3.1" + +[packages.b8f27783-ece8-5eb3-8dc8-9495eed66fee] +name = "MathOptInterface" +source = "/home/amartins/.julia/packages/MathOptInterface/13jDB" +version = "1.53.0" + +[packages.b964fa9f-0449-5b57-a5c2-d3ea65f4040f] +name = "LaTeXStrings" +source = "/home/amartins/.julia/packages/LaTeXStrings/xwv3L" +version = "1.4.1" + +[packages.ba0b0d4f-ebba-5204-a429-3ac8c609bfb7] +name = "Krylov" +source = "/home/amartins/.julia/packages/Krylov/ciZbW" +version = "0.10.10" + +[packages.baad4e97-8daa-5946-aac2-2edac59d34e1] +name = "OCCT_jll" +source = "/home/amartins/.julia/packages/OCCT_jll/7QDHX" +version = "7.9.3+0" + +[packages.bac558e1-5e72-5ebc-8fee-abe8a469f55d] +name = "OrderedCollections" +source = "/home/amartins/.julia/packages/OrderedCollections/1sijc" +version = "2.0.1" + +[packages.bb5d69b7-63fc-4a16-80bd-7e42200c7bdb] +name = "MaybeInplace" +source = "/home/amartins/.julia/packages/MaybeInplace/6lGEI" +version = "0.1.8" + +[packages.bbf7d656-a473-5ed7-a52c-81e309532950] +name = "CommonSubexpressions" +source = "/home/amartins/.julia/packages/CommonSubexpressions/nws5L" +version = "0.3.1" + +[packages.bc367c6b-8a6b-528e-b4bd-a4b897500b49] +name = "ImageMetadata" +source = "/home/amartins/.julia/packages/ImageMetadata/cJkCb" +version = "0.9.10" + +[packages.bd17208b-e95e-5925-bf81-e2f59b3e5c61] +name = "GLU_jll" +source = "/home/amartins/.julia/packages/GLU_jll/xvGet" +version = "9.0.1+0" + +[packages.bd369af6-aec1-5ad0-b16a-f7cc5008161c] +name = "Tables" +source = "/home/amartins/.julia/packages/Tables/qGyu7" +version = "1.14.0" + +[packages.be0214bd-f91f-a760-ac4e-3421ce2b2da0] +name = "NonlinearSolveBase" +source = "/home/amartins/.julia/packages/NonlinearSolveBase/zLkkA" +version = "2.51.1" + +[packages.bea87d4a-7f5b-5778-9afe-8cc45184846c] +name = "SuiteSparse_jll" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/SuiteSparse_jll" +version = "7.8.3+2" + +[packages.c0aeaf25-5076-4817-a8d5-81caf7dfa961] +name = "SciMLOperators" +source = "/home/amartins/.julia/packages/SciMLOperators/g0aVc" +version = "1.30.1" + +[packages.c145ed77-6b09-5dd9-b285-bf645a82121e] +name = "GenericSchur" +source = "/home/amartins/.julia/packages/GenericSchur/6AunB" +version = "0.5.8" + +[packages.c1c5ebd0-6772-5130-a774-d5fcae4a789d] +name = "LAME_jll" +source = "/home/amartins/.julia/packages/LAME_jll/hLSZH" +version = "3.100.3+0" + +[packages.c3611d14-8923-5661-9e6a-0046d554d3a4] +name = "ColorVectorSpace" +source = "/home/amartins/.julia/packages/ColorVectorSpace/JXxVe" +version = "0.11.0" + +[packages.c36e90e8-916a-50a6-bd94-075b64ef4655] +name = "PowerModels" +source = "/home/amartins/.julia/packages/PowerModels/9nxfE" +version = "0.21.6" + +[packages.c5f90fcd-3b7e-5836-afba-fc50a0988cb2] +name = "libwebp_jll" +source = "/home/amartins/.julia/packages/libwebp_jll/spcJQ" +version = "1.6.0+0" + +[packages.c5fb5394-a638-5e4d-96e5-b29de1b5cf10] +name = "Xorg_xtrans_jll" +source = "/home/amartins/.julia/packages/Xorg_xtrans_jll/GstJ7" +version = "1.6.0+0" + +[packages.c7cfdc94-dc32-55de-ac96-5a1b8d977c5b] +name = "Xorg_libxcb_jll" +source = "/home/amartins/.julia/packages/Xorg_libxcb_jll/f1ZGe" +version = "1.17.1+0" + +[packages.c817782e-172a-44cc-b673-b171935fbb9e] +name = "ImageBase" +source = "/home/amartins/.julia/packages/ImageBase/z0ixH" +version = "0.1.7" + +[packages.c84ed2f1-dad5-54f0-aa8e-dbefe2724439] +name = "Ratios" +source = "/home/amartins/.julia/packages/Ratios/FsiCW" +version = "0.4.5" + +[packages.c8e1da08-722c-5040-9ed9-7db0dc04731e] +name = "IterTools" +source = "/home/amartins/.julia/packages/IterTools/cLYFo" +version = "1.10.0" + +[packages.ca575930-c2e3-43a9-ace4-1e988b2c1908] +name = "NetworkOptions" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/NetworkOptions" +version = "1.3.0" + +[packages.cae243ae-269e-4f55-b966-ac2d0dc13c15] +name = "StackViews" +source = "/home/amartins/.julia/packages/StackViews/O9ART" +version = "0.1.2" + +[packages.cc61e674-0454-545c-8b26-ed2c68acab7a] +name = "Xorg_libxkbfile_jll" +source = "/home/amartins/.julia/packages/Xorg_libxkbfile_jll/epSk2" +version = "1.2.0+0" + +[packages.cf7118a7-6976-5b1a-9a39-7adc72f591a4] +name = "UUIDs" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/UUIDs" +version = "1.11.0" + +[packages.d00139f3-1899-568f-a2f0-47f597d42d70] +name = "METIS_jll" +source = "/home/amartins/.julia/packages/METIS_jll/HBwKI" +version = "5.1.4+0" + +[packages.d091e8ba-531a-589c-9de9-94069b037ed8] +name = "Xorg_libXfixes_jll" +source = "/home/amartins/.julia/packages/Xorg_libXfixes_jll/iQEXI" +version = "6.0.2+0" + +[packages.d125e4d3-2237-4719-b19c-fa641b8a4667] +name = "ManualMemory" +source = "/home/amartins/.julia/packages/ManualMemory/iySrG" +version = "0.1.8" + +[packages.d1454406-59df-5ea1-beac-c340f2130bc3] +name = "Xorg_libXinerama_jll" +source = "/home/amartins/.julia/packages/Xorg_libXinerama_jll/ZTxyU" +version = "1.1.7+0" + +[packages.d1acc4aa-44c8-5952-acd4-ba5d80a2a253] +name = "IntervalArithmetic" +source = "/home/amartins/.julia/packages/IntervalArithmetic/mlFMS" +version = "1.0.12" + +[packages.d236fae5-4411-538c-8e31-a6e3d9e00b46] +name = "PreallocationTools" +source = "/home/amartins/.julia/packages/PreallocationTools/dNQ2d" +version = "1.7.1" + +[packages.d25df0c9-e2be-5dd7-82c8-3ad0b3e990b9] +name = "Inflate" +source = "/home/amartins/.julia/packages/Inflate/AToAX" +version = "0.1.5" + +[packages.d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4] +name = "ChainRulesCore" +source = "/home/amartins/.julia/packages/ChainRulesCore/IZ7FD" +version = "1.26.1" + +[packages.d38c429a-6771-53c6-b99e-75d170b6e991] +name = "Contour" +source = "/home/amartins/.julia/packages/Contour/vefVR" +version = "0.6.3" + +[packages.d3d80556-e9d4-5f37-9878-2ab0fcc64255] +name = "LineSearches" +source = "/home/amartins/.julia/packages/LineSearches/2BCHN" +version = "7.5.1" + +[packages.d41bc354-129a-5804-8e4c-c37616107c6c] +name = "NLSolversBase" +source = "/home/amartins/.julia/packages/NLSolversBase/n7XXO" +version = "7.10.0" + +[packages.d6f4376e-aef5-505a-96c1-9c027394607a] +name = "Markdown" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/Markdown" +version = "1.11.0" + +[packages.d7e528f0-a631-5988-bf34-fe36492bcfd7] +name = "FreeType2_jll" +source = "/home/amartins/.julia/packages/FreeType2_jll/tFigR" +version = "2.14.3+1" + +[packages.d7ed1dd3-d0ae-5e8e-bfb4-87a502085b8d] +name = "MUMPS_seq_jll" +source = "/home/amartins/.julia/packages/MUMPS_seq_jll/8TSrC" +version = "500.900.100+0" + +[packages.d8a4904e-b15c-11e9-3269-09a3773c0cb0] +name = "MutableArithmetics" +source = "/home/amartins/.julia/packages/MutableArithmetics/xwKH0" +version = "1.8.0" + +[packages.d8fb68d0-12a3-5cfd-a85a-d49703b185fd] +name = "xkbcommon_jll" +source = "/home/amartins/.julia/packages/xkbcommon_jll/kKUyp" +version = "1.13.0+0" + +[packages.d96e819e-fc66-5662-9728-84c9c7592b0a] +name = "Parameters" +source = "/home/amartins/.julia/packages/Parameters/mlriZ" +version = "0.13.1" + +[packages.d9f16b24-f501-4c13-a1f2-28368ffc5196] +name = "Functors" +source = "/home/amartins/.julia/packages/Functors/PfDWx" +version = "0.5.3" + +[packages.da1fd8a2-8d9e-5ec2-8556-3022fb5608a2] +name = "CodeTracking" +source = "/home/amartins/.julia/packages/CodeTracking/hHLkR" +version = "3.0.2" + +[packages.dbb5928d-eab1-5f90-85c2-b9b0edb7c900] +name = "MappedArrays" +source = "/home/amartins/.julia/packages/MappedArrays/WoTkJ" +version = "0.4.3" + +[packages.dce04be8-c92d-5529-be00-80e4d2c0e197] +name = "ArgCheck" +source = "/home/amartins/.julia/packages/ArgCheck/DaoPJ" +version = "2.5.0" + +[packages.de0858da-6303-5e67-8744-51eddeeeb8d7] +name = "Printf" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/Printf" +version = "1.11.0" + +[packages.deac9b47-8bc7-5906-a0fe-35ac56dc84c0] +name = "LibCURL_jll" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/LibCURL_jll" +version = "8.15.0+0" + +[packages.dfaa095f-4041-5dcd-9319-2fabd8486b76] +name = "x265_jll" +source = "/home/amartins/.julia/packages/x265_jll/QkM1f" +version = "4.1.0+0" + +[packages.dffb7669-8cd4-4628-99dd-7694e356ba37] +name = "LineCableModels" +source = "/home/amartins/Documents/KUL/LineCableModels" +version = "0.2.0" + +[packages.e1d29d7a-bbdc-5cf2-9ac0-f12de2c33e28] +name = "Missings" +source = "/home/amartins/.julia/packages/Missings/MBhnW" +version = "1.2.0" + +[packages.e2ba6199-217a-4e67-a87a-7c52f15ade04] +name = "ExprTools" +source = "/home/amartins/.julia/packages/ExprTools/9lavJ" +version = "0.1.11" + +[packages.e2d170a0-9d28-54be-80f0-106bbe20a464] +name = "DataValueInterfaces" +source = "/home/amartins/.julia/packages/DataValueInterfaces/0j6Kp" +version = "1.0.0" + +[packages.e33a78d0-f292-5ffc-b300-72abe9b543c8] +name = "Hwloc_jll" +source = "/home/amartins/.julia/packages/Hwloc_jll/8nfxf" +version = "2.12.1+0" + +[packages.e37daf67-58a4-590a-8e99-b0245dd2ffc5] +name = "LibGit2_jll" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/LibGit2_jll" +version = "1.9.0+0" + +[packages.e3aaa7dc-3e4b-44e0-be63-ffb868ccd7c1] +name = "WebP" +source = "/home/amartins/.julia/packages/WebP/EiIqv" +version = "0.1.3" + +[packages.e66e0078-7015-5450-92f7-15fbd957f2ae] +name = "CompilerSupportLibraries_jll" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/CompilerSupportLibraries_jll" +version = "1.3.1+2" + +[packages.e6f89c97-d47a-5376-807f-9c37f3926c36] +name = "LoggingExtras" +source = "/home/amartins/.julia/packages/LoggingExtras/IICqi" +version = "1.2.0" + +[packages.e7412a2a-1a6e-54c0-be00-318e2571c051] +name = "Ogg_jll" +source = "/home/amartins/.julia/packages/Ogg_jll/ypXgB" +version = "1.3.6+0" + +[packages.e91730f6-4275-51fb-a7a0-7064cfbd3b39] +name = "Hungarian" +source = "/home/amartins/.julia/packages/Hungarian/ZFj4j" +version = "0.7.0" + +[packages.e9467ef8-e4e7-5192-8a1a-b1aee30e663a] +name = "GLMakie" +source = "/home/amartins/.julia/packages/GLMakie/hxEgI" +version = "0.13.14" + +[packages.e94cdb99-869f-56ef-bcf0-1ae2bcbe0389] +name = "MosaicViews" +source = "/home/amartins/.julia/packages/MosaicViews/rPrgS" +version = "0.3.4" + +[packages.e9f186c6-92d2-5b65-8a66-fee21dc1b490] +name = "Libffi_jll" +source = "/home/amartins/.julia/packages/Libffi_jll/EP787" +version = "3.4.7+0" + +[packages.ea2f1a96-1ddc-540d-b46f-429655e07cfa] +name = "Xorg_libXrender_jll" +source = "/home/amartins/.julia/packages/Xorg_libXrender_jll/TCAvu" +version = "0.9.12+0" + +[packages.ea8e919c-243c-51af-8825-aaa63cd721ce] +name = "SHA" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/SHA" +version = "0.7.0" + +[packages.eb426421-6452-4132-80ef-4b100a757ac5] +name = "DoubleExponentialFormulas" +source = "/home/amartins/.julia/packages/DoubleExponentialFormulas/bRCbp" +version = "0.1.0" + +[packages.ec057cc2-7a8d-4b58-b3b3-92acb9f63b42] +name = "StructUtils" +source = "/home/amartins/.julia/packages/StructUtils/019BB" +version = "2.8.5" + +[packages.ec84b674-ba8e-5d96-8ba1-2a689ba10484] +name = "Xorg_libXrandr_jll" +source = "/home/amartins/.julia/packages/Xorg_libXrandr_jll/q66gc" +version = "1.5.6+0" + +[packages.ed01d8cd-4d21-5b2a-85b4-cc3bdc58bad4] +name = "Sobol" +source = "/home/amartins/.julia/packages/Sobol/5NmUh" +version = "1.5.0" + +[packages.ee1fde0b-3d02-5ea6-8484-8dfef6360eab] +name = "Dbus_jll" +source = "/home/amartins/.julia/packages/Dbus_jll/vwYlz" +version = "1.16.2+0" + +[packages.ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a] +name = "Makie" +source = "/home/amartins/.julia/packages/Makie/Iy6pu" +version = "0.24.14" + +[packages.eebad327-c553-4316-9ea0-9fa01ccd7688] +name = "PkgVersion" +source = "/home/amartins/.julia/packages/PkgVersion/9yTWs" +version = "0.3.3" + +[packages.efce3f68-66dc-5838-9240-27a6d6f5f9b6] +name = "WoodburyMatrices" +source = "/home/amartins/.julia/packages/WoodburyMatrices/inxuE" +version = "1.1.0" + +[packages.efcefdf7-47ab-520b-bdef-62a2eaa19f15] +name = "PCRE2_jll" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/PCRE2_jll" +version = "10.44.0+1" + +[packages.efcf1570-3423-57d1-acb7-fd33fddbac46] +name = "Setfield" +source = "/home/amartins/.julia/packages/Setfield/ZezIj" +version = "1.1.2" + +[packages.efe28fd5-8261-553b-a9e1-b2916fc3738e] +name = "OpenSpecFun_jll" +source = "/home/amartins/.julia/packages/OpenSpecFun_jll/4QIOW" +version = "0.5.6+0" + +[packages.eff96d63-e80a-5855-80a2-b1b0885c5ab7] +name = "Measurements" +source = "/home/amartins/.julia/packages/Measurements/FIcLC" +version = "2.14.1" + +[packages.f09324ee-3d7c-5217-9330-fc30815ba969] +name = "Netpbm" +source = "/home/amartins/.julia/packages/Netpbm/EXoxl" +version = "1.1.1" + +[packages.f151be2c-9106-41f4-ab19-57ee4f262869] +name = "EnzymeCore" +source = "/home/amartins/.julia/packages/EnzymeCore/w6Eem" +version = "0.8.21" + +[packages.f1662d9f-8043-43de-a69a-05efc1cc6ff4] +name = "Isoband" +source = "/home/amartins/.julia/packages/Isoband/qUEGP" +version = "0.1.1" + +[packages.f1f71cc9-e9ae-5b93-9b94-4fe0e1ad3748] +name = "MPItrampoline_jll" +source = "/home/amartins/.julia/packages/MPItrampoline_jll/ldHYr" +version = "5.5.6+0" + +[packages.f27b6e38-b328-58d1-80ce-0feddd5e7a45] +name = "Polynomials" +source = "/home/amartins/.julia/packages/Polynomials/CQYvl" +version = "4.1.3" + +[packages.f27f6e37-5d2b-51aa-960f-b287f2bc3b7a] +name = "libvorbis_jll" +source = "/home/amartins/.julia/packages/libvorbis_jll/UePdg" +version = "1.3.8+0" + +[packages.f2b01f46-fcfa-551c-844a-d8ac1e96c665] +name = "Roots" +source = "/home/amartins/.julia/packages/Roots/is6NK" +version = "3.0.8" + +[packages.f43a241f-c20a-4ad4-852c-f6b1247861c6] +name = "Downloads" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/Downloads" +version = "1.7.0" + +[packages.f489334b-da3d-4c2e-b8f0-e476e12c162b] +name = "StyledStrings" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/StyledStrings" +version = "1.11.0" + +[packages.f50d1b31-88e8-58de-be2c-1cc44531875f] +name = "Rmath_jll" +source = "/home/amartins/.julia/packages/Rmath_jll/Ad6Tb" +version = "0.5.2+0" + +[packages.f57f5aa1-a3ce-4bc8-8ab9-96f992907883] +name = "PNGFiles" +source = "/home/amartins/.julia/packages/PNGFiles/tc8G3" +version = "0.4.5" + +[packages.f6369f11-7733-5829-9624-2563aa707210] +name = "ForwardDiff" +source = "/home/amartins/.julia/packages/ForwardDiff/wp23H" +version = "1.4.6" + +[packages.f638f0a6-7fb0-5443-88ba-1cc74229b280] +name = "libfdk_aac_jll" +source = "/home/amartins/.julia/packages/libfdk_aac_jll/6fM3s" +version = "2.0.4+0" + +[packages.f70d9fcc-98c5-4d4a-abd7-e4cdeebd8ca8] +name = "CommonWorldInvalidations" +source = "/home/amartins/.julia/packages/CommonWorldInvalidations/werPC" +version = "1.2.2" + +[packages.f7f18e0c-5ee9-5ccd-a5bf-e8befd85ed98] +name = "GLFW" +source = "/home/amartins/.julia/packages/GLFW/wA4ue" +version = "3.4.6" + +[packages.f8b46487-2199-4994-9208-9a1283c18c0a] +name = "TestItemRunner" +source = "/home/amartins/.julia/packages/TestItemRunner/uGBn3" +version = "1.3.2" + +[packages.fa267f1f-6049-4f14-aa54-33bafae1ed76] +name = "TOML" +source = "/home/amartins/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/TOML" +version = "1.0.3" + +[packages.fa961155-64e5-5f13-b03f-caf6b980ea82] +name = "CEnum" +source = "/home/amartins/.julia/packages/CEnum/B6ahP" +version = "0.5.0" + +[packages.fdbf4ff8-1666-58a4-91e7-1b58723a45e0] +name = "XLSX" +source = "/home/amartins/.julia/packages/XLSX/CSF3F" +version = "0.12.3" + +[packages.fdea26ae-647d-5447-a871-4b548cad5224] +name = "SIMD" +source = "/home/amartins/.julia/packages/SIMD/UiGbs" +version = "3.7.2" + +[packages.fe0851c0-eecd-5654-98d4-656369965a5c] +name = "OpenMPI_jll" +source = "/home/amartins/.julia/packages/OpenMPI_jll/3lpnV" +version = "5.0.11+0" + +[packages.ff45984e-d068-5f4c-9e32-c4133509d236] +name = "PowerModelsACDC" +source = "/home/amartins/.julia/packages/PowerModelsACDC/rDOL9" +version = "0.10.1" + +[packages.ffbed154-4ef7-542d-bbb7-c09d3a79fcae] +name = "DocStringExtensions" +source = "/home/amartins/.julia/packages/DocStringExtensions/zNis6" +version = "0.9.5" + +[packages.ffd25f8a-64ca-5728-b0f7-c24cf3aae800] +name = "XZ_jll" +source = "/home/amartins/.julia/packages/XZ_jll/dRNtS" +version = "5.8.4+0" + +[runs."three_bare_wires_air_1/fem"] +directory = "three_bare_wires_air_1/fem" +fem_run_directory = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq" +seconds = 44.18872594833374 +status = "captured" +terminal_order = ["cable:1:core", "cable:2:core", "cable:3:core"] + + [runs."three_bare_wires_air_1/fem".sha256] + "Y.csv" = "d2dc753fa5d79c0345fca421b35415d4af68526791a7d58fef044729399276d1" + "Z.csv" = "5fba0d0a60a9196c2ef04b2b2a1cab12f56a0b7a573f229c4d8771698c7c4ff0" + "computation-options.txt" = "0dbde8116245047b46af5ba9cec83c49c05a3ad26dfc4795dbc1096be3fe2fb7" + "compute.log" = "4518aff1a2ed01ec24925ec017a191588e2fb076c6f24f72f5c10ed4776fe0e2" + "details.txt" = "37385b0fb34ac320f82f09435dc141d283c33e70e54922244d36b8687df422a6" + "fem-artifacts.toml" = "8a29856df22113e3e9a84a9fe252120794b96b2112e78bff424703bb75b15025" + "formulation.txt" = "3a46e5789375b82d5bdb834c8e7e44a224d45617f42466bd404172aa16799f68" + "problem.json" = "923fde848c12b5dfb72720cc69a12b65d22972341d8fbd1cee5e5b6fb38c867f" + +[runs."three_bare_wires_air_1/unified"] +directory = "three_bare_wires_air_1/unified" +seconds = 3.6788880825042725 +status = "captured" +terminal_order = ["cable:1:core", "cable:2:core", "cable:3:core"] + + [runs."three_bare_wires_air_1/unified".sha256] + "Y.csv" = "c1781a943be20f3d99bdf8a9558ce4e70134f3cd19460da22eca39f526aed6c4" + "Z.csv" = "58f904dc16552619aaffb4bfb00a663eb0f1e76feca62e142c7b1899e6b1fa07" + "computation-options.txt" = "b86069c50956980c051629764fd9ab441dd338f909dd6d4fa44084e6876a35ee" + "compute.log" = "178aa9f8367d78a1838c9b61d67860f686754fc1991ecbffccfa110af813b6ce" + "details.txt" = "ddaa96e65a5107a0e11aed7bd95f0ab289da24df53278e5649e730cda8d8f52d" + "formulation.txt" = "a7c87d4d0d59abe1436e6a8a6db6e6acf5e2d88bb56f2f4bc6428fa61f523018" + "problem.json" = "923fde848c12b5dfb72720cc69a12b65d22972341d8fbd1cee5e5b6fb38c867f" + +[runs."three_bare_wires_air_2/fem"] +directory = "three_bare_wires_air_2/fem" +fem_run_directory = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO" +seconds = 39.63953399658203 +status = "captured" +terminal_order = ["cable:1:core", "cable:2:core", "cable:3:core"] + + [runs."three_bare_wires_air_2/fem".sha256] + "Y.csv" = "51ff0beb013a2fe9624150538d4b1f14b17520b00fb1823b8b60c3cdab70e4c3" + "Z.csv" = "f2a7eeceef9db641c9218ba816d2c1108f143b37f2a48ec786f39e3d54a36c15" + "computation-options.txt" = "0dbde8116245047b46af5ba9cec83c49c05a3ad26dfc4795dbc1096be3fe2fb7" + "compute.log" = "55687c58b92b8988a65f8f2d0239816890944fa3d16b099b3c854545ada5e83a" + "details.txt" = "8b7710bcc89fed1ee3dfee441d0980d1f974d22812b45cd101ec39b866d241bc" + "fem-artifacts.toml" = "584ad81100c163d81f163a090e2ea6259beb335e91c9fdc473732676fc9c178c" + "formulation.txt" = "3a46e5789375b82d5bdb834c8e7e44a224d45617f42466bd404172aa16799f68" + "problem.json" = "d6cf220a44b5f13afe1b224f05f05cccdb657c2f105fb193e9841a8055816797" + +[runs."three_bare_wires_air_2/unified"] +directory = "three_bare_wires_air_2/unified" +seconds = 2.6573989391326904 +status = "captured" +terminal_order = ["cable:1:core", "cable:2:core", "cable:3:core"] + + [runs."three_bare_wires_air_2/unified".sha256] + "Y.csv" = "d942680200138e2bfaf84b60ffce59ca762ad80a65941e8b74d0bb9b6e22e219" + "Z.csv" = "47e67a8dc68a2766876a1e1937377c3949ae592904d224542044679412cd3dc6" + "computation-options.txt" = "b86069c50956980c051629764fd9ab441dd338f909dd6d4fa44084e6876a35ee" + "compute.log" = "a5204e565c04ab3e07ae19bdeb9cee7d13ac49d8227e32423878381f3d3da107" + "details.txt" = "0e2cdd53bd90b78467b9d9e523d1668f9b2b24e03caef61e23364aaa7ee099d9" + "formulation.txt" = "a7c87d4d0d59abe1436e6a8a6db6e6acf5e2d88bb56f2f4bc6428fa61f523018" + "problem.json" = "d6cf220a44b5f13afe1b224f05f05cccdb657c2f105fb193e9841a8055816797" + +[runs."three_bare_wires_air_3/fem"] +directory = "three_bare_wires_air_3/fem" +fem_run_directory = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH" +seconds = 44.83907389640808 +status = "captured" +terminal_order = ["cable:1:core", "cable:2:core", "cable:3:core"] + + [runs."three_bare_wires_air_3/fem".sha256] + "Y.csv" = "6df558e978eb303643fb47203c04e4507cf131309c2ea71c633d9fe721c1ca49" + "Z.csv" = "a70bba3ce78afdf163876ec11ca3398222b028d45eb2bf46e4c36e012b984276" + "computation-options.txt" = "0dbde8116245047b46af5ba9cec83c49c05a3ad26dfc4795dbc1096be3fe2fb7" + "compute.log" = "2d194d357dc478ab94fd9168121f8db63dba6acf022bb1a7cca8eeb70089e1a6" + "details.txt" = "2c2bc700158f9c0021a62f640f4a2d4448c1b65ab1cbdefa2c8402ed7ae2fd62" + "fem-artifacts.toml" = "a091495fa60fe3a8d72d68672a1d84ed6125594f8813088d4fb51def7ee0af8d" + "formulation.txt" = "3a46e5789375b82d5bdb834c8e7e44a224d45617f42466bd404172aa16799f68" + "problem.json" = "9cd63d9004b9cde69d04605db197b799489fc8dd601e9ceaf806010fe79a2f91" + +[runs."three_bare_wires_air_3/unified"] +directory = "three_bare_wires_air_3/unified" +seconds = 2.554433822631836 +status = "captured" +terminal_order = ["cable:1:core", "cable:2:core", "cable:3:core"] + + [runs."three_bare_wires_air_3/unified".sha256] + "Y.csv" = "d755dd9ecf39ac2edb7a9bd74e040b9421272992ef1a2625c611d932c3d90a06" + "Z.csv" = "a10546be75ece17056a779c6ebd6f154d19b5b4f5404d74121243c5b2e8e5122" + "computation-options.txt" = "b86069c50956980c051629764fd9ab441dd338f909dd6d4fa44084e6876a35ee" + "compute.log" = "21b631000801eccdd8849a064cd1ab75e12eec3378e301c8d868d4a853e9e56a" + "details.txt" = "d5e4f7c0458e87af128d1f3d0ad5e6eff75ef88c34ff167addd4074c729ec2f7" + "formulation.txt" = "a7c87d4d0d59abe1436e6a8a6db6e6acf5e2d88bb56f2f4bc6428fa61f523018" + "problem.json" = "9cd63d9004b9cde69d04605db197b799489fc8dd601e9ceaf806010fe79a2f91" + +[runs."three_bare_wires_all_air/fem"] +directory = "three_bare_wires_all_air/fem" +fem_run_directory = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d" +seconds = 64.65753602981567 +status = "captured" +terminal_order = ["cable:1:core", "cable:2:core", "cable:3:core"] + + [runs."three_bare_wires_all_air/fem".sha256] + "Y.csv" = "2e0d25927c3c7515dffe960d854bd83a2127a03ffe9d6631757f5b52fdea2f7e" + "Z.csv" = "5a3bee66ff67323ba4cd90b27c0fb5ca1d7e94fcea73545144ba7c591be691e9" + "computation-options.txt" = "0dbde8116245047b46af5ba9cec83c49c05a3ad26dfc4795dbc1096be3fe2fb7" + "compute.log" = "f844490c7e09e2f0c59dfae4b10cbc273749a6813e480cf3f19640b2b416ba1a" + "details.txt" = "5b4a17ce722d0543f1969ff1e4c0e28d7ed49e8470012b3de9d4a5df3251c1f2" + "fem-artifacts.toml" = "5fbd08cfc142e15a4aeb12fe622ea06710d8f7a24590bda79330f859d6aa4727" + "formulation.txt" = "3a46e5789375b82d5bdb834c8e7e44a224d45617f42466bd404172aa16799f68" + "problem.json" = "8d595a0ff4913df649d1f7129a69011ba6c0cc657905141120a4316c7a26d043" + +[runs."three_bare_wires_all_air/unified"] +directory = "three_bare_wires_all_air/unified" +seconds = 58.08219909667969 +status = "captured" +terminal_order = ["cable:1:core", "cable:2:core", "cable:3:core"] + + [runs."three_bare_wires_all_air/unified".sha256] + "Y.csv" = "9edfe22b78aac77a31505d80e51d1690f5bee7651155533d395f231788970e7d" + "Z.csv" = "509fce8b1d62060461705c315d79345f58c20b4703ab9dacf4d9983dc43ed52a" + "computation-options.txt" = "b86069c50956980c051629764fd9ab441dd338f909dd6d4fa44084e6876a35ee" + "compute.log" = "28bb3fe1832ab79bb63ae4d946258ff50edea90c21f12e0834a169bb37070470" + "details.txt" = "6e15d355d1d5b65fb8a586a1ef41c93a3614ce678b4efbd646a1b410dcad212b" + "formulation.txt" = "a7c87d4d0d59abe1436e6a8a6db6e6acf5e2d88bb56f2f4bc6428fa61f523018" + "problem.json" = "8d595a0ff4913df649d1f7129a69011ba6c0cc657905141120a4316c7a26d043" + +[runs."three_bare_wires_all_earth/fem"] +directory = "three_bare_wires_all_earth/fem" +fem_run_directory = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1" +seconds = 43.08330798149109 +status = "captured" +terminal_order = ["cable:1:core", "cable:2:core", "cable:3:core"] + + [runs."three_bare_wires_all_earth/fem".sha256] + "Y.csv" = "6178fb7bb6c30653f628a82279d9637b280f82723f830d8e85297c90cb57fad8" + "Z.csv" = "0e7a1341b5dfb48bec0350a353822b8a919460b1bf84d9ce12010b8ab70687a4" + "computation-options.txt" = "0dbde8116245047b46af5ba9cec83c49c05a3ad26dfc4795dbc1096be3fe2fb7" + "compute.log" = "c0c963f948081b9840ab65fe33aef1230619ac572793f4de9281b23a565acda6" + "details.txt" = "7e9cf9b6cd2ff748fa12854b4c0e0202a76b0e075f400077873d59541731f1ae" + "fem-artifacts.toml" = "f563fe3f6d193443019596047b7c250e05978bd618add0e3c75d6a4b41ea0061" + "formulation.txt" = "3a46e5789375b82d5bdb834c8e7e44a224d45617f42466bd404172aa16799f68" + "problem.json" = "65a8aedcb06ac0089cca15af43390bba1fb8dcb4adb0ae1002fe694b73a8dcc6" + +[runs."three_bare_wires_all_earth/unified"] +directory = "three_bare_wires_all_earth/unified" +seconds = 3.271066904067993 +status = "captured" +terminal_order = ["cable:1:core", "cable:2:core", "cable:3:core"] + + [runs."three_bare_wires_all_earth/unified".sha256] + "Y.csv" = "0682607582edc6218ce090a8b55e671e94fbcacd84081c4d2094ef3e880639e8" + "Z.csv" = "2191de0b3d3cce5bbae75eec5dee2042c9f65683200cf947c22a31e42bef0b17" + "computation-options.txt" = "b86069c50956980c051629764fd9ab441dd338f909dd6d4fa44084e6876a35ee" + "compute.log" = "8e0148f8593665de0306d2aba90831ab6cf8722a072a1483c83ba8c92a351e87" + "details.txt" = "3636ac08c936bf0827cec2f2027eceabc16229f6b6224c7ab1df5b232d36484d" + "formulation.txt" = "a7c87d4d0d59abe1436e6a8a6db6e6acf5e2d88bb56f2f4bc6428fa61f523018" + "problem.json" = "65a8aedcb06ac0089cca15af43390bba1fb8dcb4adb0ae1002fe694b73a8dcc6" diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/Y-differences.csv b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/Y-differences.csv new file mode 100644 index 000000000..4f87c720c --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/Y-differences.csv @@ -0,0 +1,82 @@ +frequency_hz,response_terminal,basis_terminal,absolute_error,relative_error,real_absolute_error,real_relative_error,imaginary_absolute_error,imaginary_relative_error,real_reference_near_zero,imaginary_reference_near_zero +0.10000000000000001,cable:1:core,cable:1:core,5.762974072550828e-14,0.0063096853104526677,3.8961430303893327e-26,NaN,5.762974072550828e-14,0.0063096853104526677,true,false +0.10000000000000001,cable:1:core,cable:2:core,5.288262338390966e-14,0.012333923926933294,2.2050186548412614e-14,0.19348990491703602,4.806621608931404e-14,0.011214545249970542,false,false +0.10000000000000001,cable:1:core,cable:3:core,4.6130055423992855e-14,0.015499786037223238,2.1998797786908534e-14,0.19327935052455786,4.0546700350970126e-14,0.013633741808426129,false,false +0.10000000000000001,cable:2:core,cable:1:core,2.8002253153424086e-14,0.0065310235778156742,9.9622291428173082e-16,0.0087418342941149429,2.7984526466844303e-14,0.00652919584471294,false,false +0.10000000000000001,cable:2:core,cable:2:core,0.06991739718287969,0.0064222609881899742,0.069906710145021478,0.0064218840352398068,0.0012224179025618065,0.0081823337656598584,false,false +0.10000000000000001,cable:2:core,cable:3:core,0.059305216800966121,0.0070096527232738877,0.059292141717884661,0.0070091983405770842,0.0012452591360339049,0.0083421603500547278,false,false +0.10000000000000001,cable:3:core,cable:1:core,2.0490213471832484e-14,0.0068847505547380916,1.0133582118700546e-15,0.0089032691211664919,2.0465139952018429e-14,0.0068813598089110788,false,false +0.10000000000000001,cable:3:core,cable:2:core,0.059305216800976682,0.0070096527232751255,0.059292141717895319,0.0070091983405783341,0.0012452591360292697,0.0083421603500237595,false,false +0.10000000000000001,cable:3:core,cable:3:core,0.070280756221349369,0.0064554219599144659,0.070269330454733492,0.0064549806267344585,0.00126723813397564,0.0084797969012760568,false,false +1,cable:1:core,cable:1:core,5.784931690461863e-13,0.0063335737000786803,7.7343238504434158e-24,NaN,5.784931690461863e-13,0.0063335737000786803,true,false +1,cable:1:core,cable:2:core,1.2697612936692786e-12,0.030999695076452637,8.136765199112671e-13,0.4940635681107054,9.7479447261757149e-13,0.023817694738959963,false,false +1,cable:1:core,cable:3:core,1.2067854153556263e-12,0.04332856228458494,8.0992737273885299e-13,0.49538175183537791,8.9462209317867333e-13,0.032176099865759464,false,false +1,cable:2:core,cable:1:core,2.7873922154664215e-13,0.0068050829056412005,1.2976133308740261e-14,0.0078790951513453954,2.7843701907753189e-13,0.0068032063277982408,false,false +1,cable:2:core,cable:2:core,0.070059662650640772,0.0062877421458306694,0.070041871751710616,0.006287378460615045,0.001578775550391126,0.0071548942664742821,false,false +1,cable:2:core,cable:3:core,0.058775449511429961,0.0071611781584668046,0.05875284301150252,0.0071609852260741107,0.0016300010234326923,0.0074258294188597005,false,false +1,cable:3:core,cable:1:core,1.9875212448124282e-13,0.0071360191258537714,1.3328056049310346e-14,0.0081519355642227249,1.9830474000324319e-13,0.0071322552470470856,false,false +1,cable:3:core,cable:2:core,0.058775449511311702,0.0071611781584523951,0.058752843011387057,0.0071609852260600361,0.0016300010233304407,0.0074258294183939472,false,false +1,cable:3:core,cable:3:core,0.070712585649814752,0.0063459692289450269,0.070692697705645458,0.0063454297883891895,0.0016769795409055654,0.0075965666269133909,false,false +10,cable:1:core,cable:1:core,5.8207319860659567e-12,0.0063725195154425507,2.1831114747276453e-21,NaN,5.8207319860659567e-12,0.0063725195154425507,true,false +10,cable:1:core,cable:2:core,4.0933747434082193e-11,0.10728771847134705,3.0333117295242988e-11,1.1936009371354783,2.7485881360259511e-11,0.072201083740390901,false,false +10,cable:1:core,cable:3:core,4.0137114774762647e-11,0.15999839432994978,2.9999398439430719e-11,1.2224757069568586,2.6665034703047957e-11,0.10680697050047183,false,false +10,cable:2:core,cable:1:core,2.7484938711597683e-12,0.007203827041344626,1.7766099382164265e-13,0.0069909177699711972,2.7427459107757532e-12,0.0072047617679401,false,false +10,cable:2:core,cable:2:core,0.070494247851576916,0.0061131884458517664,0.070459089760818827,0.0061131103501942362,0.0022261289801880357,0.0061929594712795575,false,false +10,cable:2:core,cable:3:core,0.058083988829417323,0.0074201009092482346,0.058038331520704034,0.0074216976996770434,0.0023025708737689499,0.0065755873935504647,false,false +10,cable:3:core,cable:1:core,1.942359203011314e-12,0.0077428174754907673,1.8046057171662424e-13,0.0073537696241652815,1.9339579249762537e-12,0.0077464810881525421,false,false +10,cable:3:core,cable:2:core,0.0580839888250596,0.007420100908691548,0.058038331516484298,0.0074216976991374438,0.0023025708702043568,0.0065755873833708411,false,false +10,cable:3:core,cable:3:core,0.071229094283709632,0.0061765200533299928,0.071189604569532605,0.0061760991985957336,0.0023715129582564942,0.006594741880502109,false,false +100,cable:1:core,cable:1:core,5.6346360501697491e-11,0.0061700393237806069,7.7225684764132677e-19,NaN,5.6346360501697485e-11,0.0061700393237806061,true,false +100,cable:1:core,cable:2:core,1.4841556080924008e-09,0.43915996451163936,1.147471153788312e-09,2.6837494778680311,9.4129050789639012e-10,0.28078298429138671,false,false +100,cable:1:core,cable:3:core,1.4528298150466409e-09,0.69386954866857675,1.1173729904636555e-09,2.9900852391534718,9.2854298321120511e-10,0.45070723290022241,false,false +100,cable:2:core,cable:1:core,2.668085580299013e-11,0.0078948350319152146,2.6110290785413136e-12,0.0061067748004811663,2.65527888089173e-11,0.007920584793411501,false,false +100,cable:2:core,cable:2:core,0.072139059545459905,0.0059149574465560744,0.072040285066240273,0.0059162355566590745,0.0037737567062873234,0.0054979754788992393,false,false +100,cable:2:core,cable:3:core,0.057221242836034171,0.0079335619960233406,0.057107726546459681,0.0079471543263363673,0.0036025269457946596,0.0058189731891994329,false,false +100,cable:3:core,cable:1:core,1.7300577563890915e-11,0.008262732373493651,2.3042110240646636e-12,0.0061660586301549311,1.7146445567545029e-11,0.0083227456084977846,false,false +100,cable:3:core,cable:2:core,0.057221242681753583,0.0079335619746327777,0.057107726400289494,0.007947154305995215,0.0036025268123660581,0.0058189729736792019,false,false +100,cable:3:core,cable:3:core,0.071521144223717956,0.0058645897345041284,0.071431556973371002,0.0058665376529219342,0.0035786505040190653,0.0052152082302039313,false,false +1000,cable:1:core,cable:1:core,5.3153477670666778e-10,0.0058224479808793375,3.2917860316711133e-16,NaN,5.3153477670656583e-10,0.0058224479808782204,true,false +1000,cable:1:core,cable:2:core,5.5505399045096105e-08,2.0896863010547984,4.1310190198475435e-08,5.334718944174778,3.7071788585407361e-08,1.4590735750479038,false,false +1000,cable:1:core,cable:3:core,5.324294659110877e-08,3.5680502298860661,3.9030986679137342e-08,7.3045327617879305,3.6213166673969076e-08,2.5991606205553666,false,false +1000,cable:2:core,cable:1:core,2.3404782759350137e-10,0.0088115128893391167,4.460358030291794e-11,0.0057600210426719013,2.2975836486491303e-10,0.0090428428627953653,false,false +1000,cable:2:core,cable:2:core,0.071955309693187364,0.0052936233733021816,0.071548132989009972,0.005302196485334457,0.0076440341986052651,0.0046733227248708957,false,false +1000,cable:2:core,cable:3:core,0.052689608328873289,0.0085932431791693412,0.052252039581356158,0.0086960167668720563,0.006776369635613344,0.0055502614328227055,false,false +1000,cable:3:core,cable:1:core,1.2373086717660297e-10,0.0082917640239543716,3.0815841313846309e-11,0.005767092805249667,1.1983201332028938e-10,0.0086008123207807936,false,false +1000,cable:3:core,cable:2:core,0.052689604040242428,0.0085932424797288097,0.052252035943983088,0.0086960161615242618,0.0067763643368867754,0.0055502570928413668,false,false +1000,cable:3:core,cable:3:core,0.071530364580114594,0.0052625265336032779,0.071144970075534886,0.005272476938665744,0.0074152740957673657,0.0045341000945269072,false,false +10000,cable:1:core,cable:1:core,5.303863790877314e-09,0.0058099428992592498,1.5791074361099307e-13,NaN,5.3038637885265933e-09,0.0058099428966842314,true,false +10000,cable:1:core,cable:2:core,1.8009562770739958e-06,12.451735183997533,7.6293938397289777e-07,6.995030043821731,1.6313696724885142e-06,17.173954190622684,false,false +10000,cable:1:core,cable:3:core,1.6820782301209971e-06,22.78681045583658,6.8473613682187193e-07,11.599471802128017,1.5363995558373626e-06,34.66511388722774,false,false +10000,cable:2:core,cable:1:core,1.420830196538177e-09,0.0098235596132651929,8.6274803579669588e-10,0.0079101283239828863,1.128904014575047e-09,0.01188433630885608,false,false +10000,cable:2:core,cable:2:core,0.085878359511655275,0.0048540883417152094,0.082191372938755336,0.0047872728601222984,0.024893188764309571,0.0058285745176749441,false,false +10000,cable:2:core,cable:3:core,0.040255673358350737,0.0098756963403934112,0.036375075302347071,0.010836871130127062,0.01724451026509044,0.0074562051431581646,false,false +10000,cable:3:core,cable:1:core,6.4260174978691039e-10,0.0087052100245836664,4.4424486150016302e-10,0.0075255349719506887,4.6430971544853548e-10,0.010476017845629721,false,false +10000,cable:3:core,cable:2:core,0.040255664881250826,0.0098756942607544987,0.036375119233504893,0.010836884218108099,0.01724439780856013,0.0074561565190484121,false,false +10000,cable:3:core,cable:3:core,0.084860615234054318,0.004796839125662194,0.0811961906153158,0.0047295821661083801,0.024667846429380802,0.0057761166780425175,false,false +100000,cable:1:core,cable:1:core,5.2968116155264175e-08,0.0058023189635719868,5.5135144215817649e-11,161.85584535912537,5.2968087459841613e-08,0.0058023158201716482,false,false +100000,cable:1:core,cable:2:core,1.9334871162455997e-05,91.085372495896408,1.7097594958925113e-05,122.27931914753397,9.0282606015395432e-06,56.527617747807582,false,false +100000,cable:1:core,cable:3:core,1.751388927245889e-05,190.41141642792607,1.5723368400303449e-05,298.19456363326532,7.7144023486132424e-06,102.36064202883288,false,false +100000,cable:2:core,cable:1:core,2.8006500654196122e-09,0.013193687834586973,2.0292939137864511e-09,0.014513191985432442,1.9301831520361343e-09,0.012085235486329587,false,false +100000,cable:2:core,cable:2:core,0.11102108066238134,0.0041919253259874098,0.095454819377454925,0.0037864610227487487,0.05669266098059289,0.0069831532905185699,false,false +100000,cable:2:core,cable:3:core,0.0093996796146047969,0.0087562848060661353,0.00020032281720772094,0.0011654196429576184,0.0093975447658483091,0.0088687293154129238,false,false +100000,cable:3:core,cable:1:core,8.5599791858829219e-10,0.0093064295201444932,1.6094356891386351e-10,0.0030523038117534467,8.4073158870876083e-10,0.011155475344064652,false,false +100000,cable:3:core,cable:2:core,0.0093998807130873142,0.0087564721396412356,0.00020039096135279766,0.0011658160856910635,0.0093977444572024282,0.0088689177698029198,false,false +100000,cable:3:core,cable:3:core,0.1125911518021756,0.0042509777892140357,0.096584138475790837,0.0038310867589823466,0.057864251995768612,0.0071264365365915298,false,false +1000000,cable:1:core,cable:1:core,5.4401437613241005e-07,0.0059614747554868766,2.4729962338116049e-08,2329.3632046409962,5.4345199456942901e-07,0.0059553120075196714,false,false +1000000,cable:1:core,cable:2:core,9.5138043650246079e-06,624.93618448120003,7.7945867565448172e-06,2063.2865579931422,5.4549876984881844e-06,369.89314698043268,false,false +1000000,cable:1:core,cable:3:core,8.2175378958683303e-06,1367.0158584768271,6.4334543075703563e-06,4090.2897798139911,5.1125917832774934e-06,881.19588531866157,false,false +1000000,cable:2:core,cable:1:core,2.033792503396546e-10,0.013359435177914946,7.8419431459165179e-11,0.020758221554119143,1.8765263717395816e-10,0.012724396156326737,false,false +1000000,cable:2:core,cable:2:core,0.1258665684345085,0.002773778589948313,0.064881809377482114,0.0015864044208416203,0.10785519858302095,0.005486851586501796,false,false +1000000,cable:2:core,cable:3:core,0.00019265323443156234,0.006864521977926509,0.00015802916553651555,0.0076855477060028821,0.00011019097774670611,0.005768785089388509,false,false +1000000,cable:3:core,cable:1:core,2.2876853134545371e-11,0.0038056436639851057,2.2466637429011349e-11,0.014283937223978881,4.3128426788957521e-12,0.00074335276192016126,false,false +1000000,cable:3:core,cable:2:core,0.00019265323493243477,0.0068645219957733442,0.00015802874726200677,0.0076855273637535266,0.00011019157848371486,0.0057688165395377846,false,false +1000000,cable:3:core,cable:3:core,0.1199194649656956,0.0026429640409972297,0.063029578262266739,0.0015411859065242828,0.10201936258249944,0.0051915099394641779,false,false +10000000,cable:1:core,cable:1:core,0.00099468651991342204,9.3164539539167421,1.6781551990640758e-05,829.81539467612038,0.00099454494740572677,9.3151281216009441,false,false +10000000,cable:1:core,cable:2:core,5.5098453127675587e-10,294.35270366985196,9.1698954767380838e-11,52.654784939536214,5.4330033627896476e-10,791.6745550377974,false,false +10000000,cable:1:core,cable:3:core,4.3964871733412077e-10,164.69818462293117,3.9187923401968799e-11,16.243712368677603,4.3789873408469571e-10,383.22931646589728,false,false +10000000,cable:2:core,cable:1:core,1.7213158674595239e-12,0.91957931792104819,1.6314738280345071e-12,0.93681442463068287,5.4883646380899715e-13,0.79974156881693259,false,false +10000000,cable:2:core,cable:2:core,0.11192532169222402,0.0011434450267779742,0.023561752379222867,0.00029152297293693366,0.10941718996907213,0.0019815632157001422,false,false +10000000,cable:2:core,cable:3:core,2.0906550528392246e-07,0.59743851816518001,2.0197016562640905e-07,0.68549430053068516,5.4004052592955504e-08,0.28603572510208375,false,false +10000000,cable:3:core,cable:1:core,2.5989984083309108e-12,0.9736189435182262,2.360339404759044e-12,0.97837984396579447,1.0879295109740936e-12,0.95210707499565761,false,false +10000000,cable:3:core,cable:2:core,2.0906550528396134e-07,0.59743851816529148,2.0197016562644309e-07,0.68549430053080096,5.400405259297877e-08,0.28603572510220732,false,false +10000000,cable:3:core,cable:3:core,0.11463687868724544,0.0011711422262802136,0.024819493527772352,0.00030708943125687187,0.11191785691380574,0.0020267589164667085,false,false diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/Z-differences.csv b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/Z-differences.csv new file mode 100644 index 000000000..c17cef808 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/Z-differences.csv @@ -0,0 +1,82 @@ +frequency_hz,response_terminal,basis_terminal,absolute_error,relative_error,real_absolute_error,real_relative_error,imaginary_absolute_error,imaginary_relative_error,real_reference_near_zero,imaginary_reference_near_zero +0.10000000000000001,cable:1:core,cable:1:core,2.0405272333677916e-08,0.0060184571470754513,1.9476258782114692e-08,0.006170638564682706,6.0869107816413532e-09,0.0049160849491190943,false,false +0.10000000000000001,cable:1:core,cable:2:core,2.6569518709410696e-09,0.0036972079475629089,1.3359290510143195e-10,0.0013554681046403941,2.6535911855830035e-09,0.0037277558098590289,false,false +0.10000000000000001,cable:1:core,cable:3:core,2.5598133903386247e-09,0.0037126008775244007,1.3461550446415719e-10,0.0013658877775692766,2.5562713587009469e-09,0.0037459286659705967,false,false +0.10000000000000001,cable:2:core,cable:1:core,2.6569518709408607e-09,0.0036972079475626166,1.3359290510148489e-10,0.0013554681046409316,2.6535911855827917e-09,0.0037277558098587301,false,false +0.10000000000000001,cable:2:core,cable:2:core,2.0322220281450704e-08,0.005993122305261364,1.9476493898345088e-08,0.0061694254435843204,5.80162241063074e-09,0.0046871097420875764,false,false +0.10000000000000001,cable:2:core,cable:3:core,3.0955437426436837e-09,0.0037833729615709966,1.3473309224739877e-10,0.0013624687390742319,3.0926102335202087e-09,0.0038077006874871954,false,false +0.10000000000000001,cable:3:core,cable:1:core,2.5598133903398795e-09,0.0037126008775262192,1.3461550446389249e-10,0.0013658877775665908,2.5562713587022175e-09,0.0037459286659724577,false,false +0.10000000000000001,cable:3:core,cable:2:core,3.0955437426436816e-09,0.0037833729615709944,1.3473309224734583e-10,0.001362468739073696,3.0926102335202087e-09,0.0038077006874871959,false,false +0.10000000000000001,cable:3:core,cable:3:core,2.0367431862918889e-08,0.0060065573188623055,1.9476294330999475e-08,0.0061693626181061299,5.9587112552062272e-09,0.0048146322831630079,false,false +1,cable:1:core,cable:1:core,5.5112729847392547e-08,0.0047213246161881542,1.8115277703504193e-08,0.004479613004868167,5.2050453455821088e-08,0.0047533373761735557,false,false +1,cable:1:core,cable:2:core,1.7482647314675746e-08,0.00303219367860406,1.3190020261771238e-09,0.0013386529148220396,1.7432819358448011e-08,0.0030686938595446238,false,false +1,cable:1:core,cable:3:core,1.6632147267183239e-08,0.0030374090709155005,1.4193467582911901e-09,0.0014408926797883001,1.6571474813576465e-08,0.003076518083174645,false,false +1,cable:2:core,cable:1:core,1.7482647314674919e-08,0.0030321936786039164,1.3190020261773356e-09,0.0013386529148222547,1.7432819358447164e-08,0.0030686938595444746,false,false +1,cable:2:core,cable:2:core,5.1950520201492852e-08,0.0044539817697942623,1.8264712246605383e-08,0.0044936873828952222,4.8633906235818857e-08,0.0044484658927681766,false,false +1,cable:2:core,cable:3:core,2.1685581760339246e-08,0.0032121136374929221,1.367075236348446e-09,0.001373023336425111,2.1642448142077667e-08,0.0032411668273899467,false,false +1,cable:3:core,cable:1:core,1.6632147267187494e-08,0.003037409070916279,1.4193467582916137e-09,0.0014408926797887303,1.65714748135807e-08,0.0030765180831754326,false,false +1,cable:3:core,cable:2:core,2.1685581760342654e-08,0.0032121136374934278,1.3670752363488695e-09,0.0013730233364255366,2.1642448142081055e-08,0.0032411668273904549,false,false +1,cable:3:core,cable:3:core,5.344097416492523e-08,0.0045823559991124358,1.8246536262334073e-08,0.0044892356005764317,5.0229489686075187e-08,0.0045950819994171324,false,false +10,cable:1:core,cable:1:core,4.2611946917243133e-07,0.0044442818814534836,1.286615749361805e-08,0.00095611241988860366,4.2592518591783707e-07,0.0044866643152002865,false,false +10,cable:1:core,cable:2:core,1.0047697455918364e-07,0.0023074389292056367,1.1531663967798665e-08,0.0011727162310488097,9.9813040945061931e-08,0.0023529713765713047,false,false +10,cable:1:core,cable:3:core,8.747408461533662e-08,0.002150006771041425,1.5168652107557597e-08,0.0015452016607091206,8.6148868086185321e-08,0.0021818978215102985,false,false +10,cable:2:core,cable:1:core,1.0047697455917652e-07,0.0023074389292054727,1.1531663967795277e-08,0.0011727162310484649,9.9813040945055155e-08,0.0023529713765711442,false,false +10,cable:2:core,cable:2:core,3.9892918944182097e-07,0.0041840165400588826,1.8697999529755974e-08,0.0013281377596651668,3.9849075648287443e-07,0.0042257371214084278,false,false +10,cable:2:core,cable:3:core,1.4154680019011417e-07,0.002663793035197452,1.3161204087686141e-08,0.0012964697398289064,1.4093359908489664e-07,0.0027020203934062072,false,false +10,cable:3:core,cable:1:core,8.7474084615322102e-08,0.0021500067710410672,1.5168652107550821e-08,0.0015452016607084295,8.6148868086171769e-08,0.0021818978215099542,false,false +10,cable:3:core,cable:2:core,1.4154680019010853e-07,0.0026637930351973453,1.3161204087698e-08,0.0012964697398300765,1.4093359908488986e-07,0.0027020203934060767,false,false +10,cable:3:core,cable:3:core,4.1598067454887159e-07,0.00436363292053358,1.8042158438419503e-08,0.0012816124096507037,4.1558922281144058e-07,0.0044078545386059317,false,false +100,cable:1:core,cable:1:core,4.3336951598280937e-06,0.0054137666875932588,7.6670670295045796e-07,0.0075868465868017413,4.2653340513924918e-06,0.0053713427933806878,false,false +100,cable:1:core,cable:2:core,3.522452605684939e-07,0.0011865858128200211,1.0826105681359681e-07,0.0011180413238073741,3.3519586389239549e-07,0.0011944852096031722,false,false +100,cable:1:core,cable:3:core,2.1497899804362638e-07,0.00079856022041533976,1.1922313688325517e-07,0.0012426468163679657,1.7889050626446927e-07,0.00071120548716501299,false,false +100,cable:2:core,cable:1:core,3.5224526056878517e-07,0.0011865858128210034,1.0826105681370523e-07,0.0011180413238084947,3.3519586389266654e-07,0.0011944852096041393,false,false +100,cable:2:core,cable:2:core,4.2336567043358251e-06,0.0054075508292252576,7.6128406256521981e-07,0.0064370965962463576,4.1646483244389167e-06,0.0053811567561407496,false,false +100,cable:2:core,cable:3:core,6.7884021189399916e-07,0.0017594819672218783,1.3211676678649806e-07,0.0012454031343943311,6.6585973989885571e-07,0.0017950245947834767,false,false +100,cable:3:core,cable:1:core,2.1497899804379931e-07,0.00079856022041598269,1.1922313688324162e-07,0.0012426468163678241,1.7889050626468611e-07,0.00071120548716587558,false,false +100,cable:3:core,cable:2:core,6.7884021189383706e-07,0.0017594819672214579,1.3211676678648451e-07,0.0012454031343942034,6.6585973989869308e-07,0.0017950245947830378,false,false +100,cable:3:core,cable:3:core,4.254973889354382e-06,0.0054349416120347775,7.5511090339672486e-07,0.0063852322627839676,4.1874348141384765e-06,0.0054107585659494114,false,false +1000,cable:1:core,cable:1:core,5.2522928572915652e-05,0.0078833666855376294,3.0633875654722089e-06,0.0039001420578539847,5.2433516785538141e-05,0.0079252136128534195,false,false +1000,cable:1:core,cable:2:core,1.2931769567957754e-06,0.00078127110159509035,1.2906846466136362e-06,0.0014679155477250868,8.0248268413815257e-08,5.7223129110910478e-05,false,false +1000,cable:1:core,cable:3:core,1.4151381576305953e-06,0.0010056040545897938,1.0436389726869869e-06,0.0012523158138528362,9.5573725462125107e-07,0.00084283432442855714,false,false +1000,cable:2:core,cable:1:core,1.2931769567956134e-06,0.00078127110159499278,1.2906846466135278e-06,0.0014679155477249634,8.0248268412947896e-08,5.7223129110292022e-05,false,false +1000,cable:2:core,cable:2:core,4.9964123951654399e-05,0.0081275599595141655,3.1418221999339122e-06,0.0026526974405685906,4.9865244765470544e-05,0.0082663437523804555,false,false +1000,cable:2:core,cable:3:core,1.5307534734316936e-06,0.00064674079606106736,1.2613272531390978e-06,0.0011458347371659616,8.6732909377685088e-07,0.00041393742653576446,false,false +1000,cable:3:core,cable:1:core,1.4151381576312484e-06,0.0010056040545902585,1.0436389726880711e-06,0.001252315813854139,9.5573725462103423e-07,0.0008428343244283661,false,false +1000,cable:3:core,cable:2:core,1.5307534734311352e-06,0.00064674079606083133,1.2613272531393147e-06,0.0011458347371661587,8.6732909377554984e-07,0.00041393742653514338,false,false +1000,cable:3:core,cable:3:core,4.9910215147663765e-05,0.0081187371353865726,3.237533894589464e-06,0.0027337294752325597,4.9805099642179912e-05,0.0082562909564613939,false,false +10000,cable:1:core,cable:1:core,0.00040191867837663007,0.0070839260769350052,8.8246287900828335e-05,0.017273900997363333,0.00039211123000972709,0.0069392541687628645,false,false +10000,cable:1:core,cable:2:core,1.2051116443585497e-05,0.0020738197714685932,1.2022516383321941e-05,0.0022875307105903854,8.2976342990776636e-07,0.00033468957970692335,false,false +10000,cable:1:core,cable:3:core,6.7607040420953695e-06,0.0016062531647138068,6.3364232701845422e-06,0.0015647111408132162,2.3572991506952412e-06,0.0020544913180350118,false,false +10000,cable:2:core,cable:1:core,1.2051116443123893e-05,0.0020738197713891572,1.202251638287178e-05,0.0022875307105047317,8.2976342972605407e-07,0.0003346895796336289,false,false +10000,cable:2:core,cable:2:core,0.00033073950572559544,0.0074904637291431453,9.7642293326087592e-05,0.0087809807227620749,0.00031599778986827337,0.007394938044586824,false,false +10000,cable:2:core,cable:3:core,8.0931006239359349e-06,0.0008617945279457077,5.8433112415882255e-06,0.00072784936423221061,5.5994634960039591e-06,0.0011492560045611741,false,false +10000,cable:3:core,cable:1:core,6.7607040420904923e-06,0.0016062531647126471,6.336423270179338e-06,0.0015647111408119301,2.3572991506952412e-06,0.0020544913180350114,false,false +10000,cable:3:core,cable:2:core,8.0931006239402395e-06,0.0008617945279461661,5.8433112415916949e-06,0.00072784936423264288,5.5994634960065612e-06,0.0011492560045617075,false,false +10000,cable:3:core,cable:3:core,0.0003285472932987013,0.0074404406547007801,9.7687327159796597e-05,0.0087850661982802197,0.00031368855581018207,0.0073405009742860015,false,false +100000,cable:1:core,cable:1:core,0.0031230030416582472,0.006093600401213776,0.00028411690974827764,0.011481638098095666,0.0031100523435790839,0.0060754168139512772,false,false +100000,cable:1:core,cable:2:core,5.4180234556469762e-06,0.00086556509062880134,2.8906178196389486e-06,0.00098895546660958465,4.5825000585626371e-06,0.0008278856860426309,false,false +100000,cable:1:core,cable:3:core,7.0826488815231745e-06,0.0021963059409219042,6.5176909328812675e-06,0.0074695193562911832,2.7719343575162078e-06,0.00089287451004234158,false,false +100000,cable:2:core,cable:1:core,5.4180234556469363e-06,0.00086556509062879505,2.8906178196402496e-06,0.00098895546661002917,4.5825000585617698e-06,0.00082788568604247445,false,false +100000,cable:2:core,cable:2:core,0.0015934802683458705,0.0053454955773149159,0.00038139528778975695,0.0041687654279040885,0.0015471641800595681,0.0054533055631960474,false,false +100000,cable:2:core,cable:3:core,1.2437376628723592e-05,0.0010078050383296327,4.1481643741417273e-06,0.00047503335558758106,1.1725232182341686e-05,0.0013445568745015591,false,false +100000,cable:3:core,cable:1:core,7.0826488815342859e-06,0.0021963059409253497,6.5176909328985063e-06,0.0074695193563109434,2.7719343575040647e-06,0.00089287451003842999,false,false +100000,cable:3:core,cable:2:core,1.2437376628734561e-05,0.0010078050383305211,4.1481643741451968e-06,0.00047503335558797826,1.1725232182352094e-05,0.0013445568745027515,false,false +100000,cable:3:core,cable:3:core,0.0016088331037954157,0.0053972647983758573,0.0003771376372920382,0.004122036196112077,0.0015640048460301426,0.0055129912751730433,false,false +1000000,cable:1:core,cable:1:core,0.026654473716088627,0.0054205664587009313,0.0015160231060837165,0.016269818602532505,0.026611325465363045,0.0054127635520777838,false,false +1000000,cable:1:core,cable:2:core,1.6794773759742062e-06,0.0058110348053217161,1.5575560459016639e-06,0.0086442908793655931,6.2822242898863379e-07,0.0027800773869481564,false,false +1000000,cable:1:core,cable:3:core,6.4950102917469219e-07,0.0053551492705499453,6.4905121699117881e-07,0.0079242894816266389,2.4168256479399218e-08,0.00027018488030897369,false,false +1000000,cable:2:core,cable:1:core,1.6794773759744775e-06,0.0058110348053226563,1.5575560459013115e-06,0.0086442908793636468,6.2822242899023299e-07,0.0027800773869552328,false,false +1000000,cable:2:core,cable:2:core,0.0057783363591952103,0.003320840874799719,0.00029488740605831953,0.00039121175095803076,0.0057708069191184652,0.0036797111803443389,false,false +1000000,cable:2:core,cable:3:core,4.8688840104649706e-06,0.0045229487804855561,4.5106164360322605e-06,0.028533470295772412,1.8331314394655612e-06,0.0017215507542855694,false,false +1000000,cable:3:core,cable:1:core,6.4950102917473783e-07,0.0053551492705503209,6.4905121699121947e-07,0.0079242894816271368,2.4168256479534743e-08,0.00027018488031048876,false,false +1000000,cable:3:core,cable:2:core,4.8688840104655483e-06,0.0045229487804860887,4.5106164360325315e-06,0.028533470295774088,1.8331314394664285e-06,0.0017215507542863826,false,false +1000000,cable:3:core,cable:3:core,0.005564629456123976,0.0031977265857614408,0.00037925251315817921,0.00050319085964031654,0.0055516905997385901,0.0035394987931461853,false,false +10000000,cable:1:core,cable:1:core,1.8723151853984727,0.038866378646441546,0.95201057451783921,3.1792981729840379,1.6122158724810802,0.033467764200018527,false,false +10000000,cable:1:core,cable:2:core,2.447964046819069e-09,0.65899803163799786,2.0795353292406094e-09,0.57488344773813904,1.2915342771134424e-09,1.5286427284042945,false,false +10000000,cable:1:core,cable:3:core,1.1289458468631831e-09,0.64295298585397354,1.0496682578963815e-09,0.60595213174866736,4.1559027119797339e-10,1.4480648676447094,false,false +10000000,cable:2:core,cable:1:core,2.4479640468190711e-09,0.6589980316379983,2.079535329240614e-09,0.57488344773814004,1.2915342771134395e-09,1.5286427284042934,false,false +10000000,cable:2:core,cable:2:core,0.010976466408856866,0.0013607683966721531,0.0020622649927259928,0.00045324535509920689,0.010780996147135014,0.001618616532633206,false,false +10000000,cable:2:core,cable:3:core,1.7238359243450832e-08,0.59778475460050118,1.3912098286017606e-08,0.82022436958268452,1.0179123276906159e-08,0.43647057976443221,false,false +10000000,cable:3:core,cable:1:core,1.1289458468631827e-09,0.64295298585397409,1.0496682578963811e-09,0.6059521317486678,4.1559027119797308e-10,1.4480648676447065,false,false +10000000,cable:3:core,cable:2:core,1.7238359243450792e-08,0.59778475460050073,1.3912098286017593e-08,0.82022436958268419,1.0179123276906109e-08,0.43647057976443099,false,false +10000000,cable:3:core,cable:3:core,0.011160987062473668,0.0013836489863149047,0.0022333275584109913,0.00049082309533170871,0.010935258580643925,0.0016418149151784885,false,false diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/fem/Y.csv b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/fem/Y.csv new file mode 100644 index 000000000..ddee71fd6 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/fem/Y.csv @@ -0,0 +1,82 @@ +frequency_hz,response_terminal,basis_terminal,real_S_per_m,imaginary_S_per_m +0.10000000000000001,cable:1:core,cable:1:core,3.3668435289524459e-24,9.1335364427824092e-12 +0.10000000000000001,cable:1:core,cable:2:core,-1.1396039787123376e-13,-4.2860602028816373e-12 +0.10000000000000001,cable:1:core,cable:3:core,-1.138186657146977e-13,-2.9739964949248815e-12 +0.10000000000000001,cable:2:core,cable:1:core,-1.1396039787123331e-13,-4.2860602028816397e-12 +0.10000000000000001,cable:2:core,cable:2:core,10.885701106001211,0.14939721814968343 +0.10000000000000001,cable:2:core,cable:3:core,-8.459190172239154,0.14927298011308732 +0.10000000000000001,cable:3:core,cable:1:core,-1.1381866571469942e-13,-2.973996494924871e-12 +0.10000000000000001,cable:3:core,cable:2:core,-8.4591901722391665,0.14927298011308582 +0.10000000000000001,cable:3:core,cable:3:core,10.886063726310885,0.14944203838006351 +1,cable:1:core,cable:1:core,3.219734897938415e-22,9.1337560189597194e-11 +1,cable:1:core,cable:2:core,-1.646906536789909e-12,-4.0927322450860221e-11 +1,cable:1:core,cable:3:core,-1.6349560106687234e-12,-2.7803932015100898e-11 +1,cable:2:core,cable:1:core,-1.64690653678989e-12,-4.0927322450860311e-11 +1,cable:2:core,cable:2:core,11.140075659587218,0.2206567269328914 +1,cable:2:core,cable:3:core,-8.2045753701006827,0.21950423737083269 +1,cable:3:core,cable:1:core,-1.6349560106687567e-12,-2.7803932015100808e-11 +1,cable:3:core,cable:2:core,-8.2045753701006845,0.21950423737083044 +1,cable:3:core,cable:3:core,11.140726485540558,0.22075493091369747 +10,cable:1:core,cable:1:core,3.0077099906617912e-20,9.1341140218724399e-10 +10,cable:1:core,cable:2:core,-2.5413114510482376e-11,-3.8068516338465009e-10 +10,cable:1:core,cable:3:core,-2.4539872873309703e-11,-2.4965631529573399e-10 +10,cable:2:core,cable:1:core,-2.5413114510482166e-11,-3.8068516338464952e-10 +10,cable:2:core,cable:2:core,11.525898556465627,0.35946125443124921 +10,cable:2:core,cable:3:core,-7.8200883233535077,0.35016961009861736 +10,cable:3:core,cable:1:core,-2.4539872873309943e-11,-2.4965631529573425e-10 +10,cable:3:core,cable:2:core,-7.820088323353505,0.35016961009861763 +10,cable:3:core,cable:3:core,11.526629071262175,0.35960663832318673 +100,cable:1:core,cable:1:core,2.6849740688989855e-18,9.1322530610991367e-09 +100,cable:1:core,cable:2:core,-4.2756269288587337e-10,-3.3523773182763521e-09 +100,cable:1:core,cable:3:core,-3.7369268803186256e-10,-2.0601909963507641e-09 +100,cable:2:core,cable:1:core,-4.2756269288587238e-10,-3.3523773182763517e-09 +100,cable:2:core,cable:2:core,12.176710067798881,0.68639023960195522 +100,cable:2:core,cable:3:core,-7.1859340087568553,0.61910011073453508 +100,cable:3:core,cable:1:core,-3.7369268803186308e-10,-2.0601909963507678e-09 +100,cable:3:core,cable:2:core,-7.1859340087568544,0.61910011073453464 +100,cable:3:core,cable:3:core,12.176101339397905,0.68619513278362976 +1000,cable:1:core,cable:1:core,2.1468668223221234e-16,9.1290601213133132e-08 +1000,cable:1:core,cable:2:core,-7.7436488465027592e-09,-2.5407758196285771e-08 +1000,cable:1:core,cable:3:core,-5.343392651111024e-09,-1.3932639017219087e-08 +1000,cable:2:core,cable:1:core,-7.7436488465027675e-09,-2.5407758196285751e-08 +1000,cable:2:core,cable:2:core,13.494055376278043,1.6356743688863127 +1000,cable:2:core,cable:3:core,-6.0087326165714305,1.2209099909311973 +1000,cable:3:core,cable:1:core,-5.3433926511110199e-09,-1.3932639017219117e-08 +1000,cable:3:core,cable:2:core,-6.0087326165714261,1.2209099909311989 +1000,cable:3:core,cable:3:core,13.493652206194168,1.6354456101924859 +10000,cable:1:core,cable:1:core,1.1772351924736467e-14,9.1289430599284201e-07 +10000,cable:1:core,cable:2:core,-1.0906877871764883e-07,-9.4990917897013724e-08 +10000,cable:1:core,cable:3:core,-5.90316652777458e-08,-4.4321203179529856e-08 +10000,cable:2:core,cable:1:core,-1.0906877871764885e-07,-9.4990917897013724e-08 +10000,cable:2:core,cable:2:core,17.168725355808448,4.2708879656289653 +10000,cable:2:core,cable:3:core,-3.3566031067050783,2.3127730439275873 +10000,cable:3:core,cable:1:core,-5.9031665277745781e-08,-4.432120317952985e-08 +10000,cable:3:core,cable:2:core,-3.3566031067050797,2.3127730439275886 +10000,cable:3:core,cable:3:core,17.167730206096849,4.2706627660680407 +100000,cable:1:core,cable:1:core,3.4064351579940734e-13,9.1287839375614467e-06 +100000,cable:1:core,cable:2:core,-1.3982409354353952e-07,1.597141532094673e-07 +100000,cable:1:core,cable:3:core,-5.2728554835898479e-08,7.5364927336429311e-08 +100000,cable:2:core,cable:1:core,-1.3982409354353934e-07,1.5971415320946725e-07 +100000,cable:2:core,cable:2:core,25.209507982247846,8.1184901178623399 +100000,cable:2:core,cable:3:core,0.17188900017107905,1.0596269692791682 +100000,cable:3:core,cable:1:core,-5.272855483589846e-08,7.5364927336429271e-08 +100000,cable:3:core,cable:2:core,0.17188900017107883,1.0596269692791682 +100000,cable:3:core,cable:3:core,25.210637229590311,8.1196614463144066 +1000000,cable:1:core,cable:1:core,1.0616619292708136e-11,9.1254999550522529e-05 +1000000,cable:1:core,cable:2:core,-3.7777528896064878e-09,-1.4747468946151504e-08 +1000000,cable:1:core,cable:3:core,-1.5728602749174711e-09,-5.801878865365625e-09 +1000000,cable:2:core,cable:1:core,-3.7777528896064837e-09,-1.4747468946151509e-08 +1000000,cable:2:core,cable:2:core,40.898656436585682,19.657028604228248 +1000000,cable:2:core,cable:3:core,-0.020561861246802895,-0.019101245069676594 +1000000,cable:3:core,cable:1:core,-1.572860274917473e-09,-5.8018788653656292e-09 +1000000,cable:3:core,cable:2:core,-0.020561861246802884,-0.019101245069676587 +1000000,cable:3:core,cable:3:core,40.896804204764933,19.65119276898254 +10000000,cable:1:core,cable:1:core,2.0223235310295311e-08,-0.00010676664179201856 +10000000,cable:1:core,cable:2:core,-1.7415122836923419e-12,-6.8626727083962635e-13 +10000000,cable:1:core,cable:3:core,-2.4124979876850083e-12,-1.1426545811342262e-12 +10000000,cable:2:core,cable:1:core,-1.7415122836923411e-12,-6.8626727083962585e-13 +10000000,cable:2:core,cable:2:core,80.822969599449294,55.217612590982597 +10000000,cable:2:core,cable:3:core,-2.9463434702527936e-07,-1.8880177493102272e-07 +10000000,cable:3:core,cable:1:core,-2.4124979876850006e-12,-1.1426545811342232e-12 +10000000,cable:3:core,cable:2:core,-2.9463434702527925e-07,-1.8880177493102251e-07 +10000000,cable:3:core,cable:3:core,80.821711858300745,55.220113257927331 diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/fem/Z.csv b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/fem/Z.csv new file mode 100644 index 000000000..896ebf3d1 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/fem/Z.csv @@ -0,0 +1,82 @@ +frequency_hz,response_terminal,basis_terminal,real_ohm_per_m,imaginary_ohm_per_m +0.10000000000000001,cable:1:core,cable:1:core,3.1562793020459726e-06,1.2381622459009902e-06 +0.10000000000000001,cable:1:core,cable:2:core,9.8558501409278213e-08,7.1184683786552884e-07 +0.10000000000000001,cable:1:core,cable:3:core,9.8555318141668934e-08,6.8241325093108754e-07 +0.10000000000000001,cable:2:core,cable:1:core,9.8558501409278186e-08,7.1184683786552915e-07 +0.10000000000000001,cable:2:core,cable:2:core,3.1569380449518183e-06,1.2377824992095822e-06 +0.10000000000000001,cable:2:core,cable:3:core,9.8888942097083999e-08,8.1219887993903893e-07 +0.10000000000000001,cable:3:core,cable:1:core,9.8555318141668934e-08,6.8241325093108775e-07 +0.10000000000000001,cable:3:core,cable:2:core,9.8888942097084039e-08,8.1219887993903882e-07 +0.10000000000000001,cable:3:core,cable:3:core,3.1569378453844727e-06,1.2376254103650067e-06 +1,cable:1:core,cable:1:core,4.0439380999692672e-06,1.0950296462592307e-05 +1,cable:1:core,cable:2:core,9.8532040051059231e-07,5.6808597261099677e-06 +1,cable:1:core,cable:3:core,9.8504682423657282e-07,5.3864382934087735e-06 +1,cable:2:core,cable:1:core,9.853204005105921e-07,5.6808597261099677e-06 +1,cable:2:core,cable:2:core,4.0645266771623253e-06,1.093273668005019e-05 +1,cable:2:core,cable:3:core,9.9566788129606608e-07,6.6773632135146649e-06 +1,cable:3:core,cable:1:core,9.8504682423657261e-07,5.386438293408771e-06 +1,cable:3:core,cable:2:core,9.9566788129606587e-07,6.6773632135146632e-06 +1,cable:3:core,cable:3:core,4.064508501178054e-06,1.093114109659993e-05 +10,cable:1:core,cable:1:core,1.3456741305710767e-05,9.4931369051803823e-05 +10,cable:1:core,cable:2:core,9.8332944172567729e-06,4.2419997939162006e-05 +10,cable:1:core,cable:3:core,9.8166164930190614e-06,3.9483456666432487e-05 +10,cable:2:core,cable:1:core,9.8332944172567746e-06,4.2419997939162019e-05 +10,cable:2:core,cable:2:core,1.4078358508886816e-05,9.4300886456954623e-05 +10,cable:2:core,cable:3:core,1.0151570594638799e-05,5.2158599331381674e-05 +10,cable:3:core,cable:1:core,9.8166164930190665e-06,3.9483456666432508e-05 +10,cable:3:core,cable:2:core,1.0151570594638785e-05,5.2158599331381681e-05 +10,cable:3:core,cable:3:core,1.407770266779548e-05,9.428378799062607e-05 +100,cable:1:core,cable:1:core,0.00010105736213043232,0.00079409082895413549 +100,cable:1:core,cable:2:core,9.6830997663775953e-05,0.00028061951809663104 +100,cable:1:core,cable:3:core,9.5942898104968443e-05,0.00025153139211222554 +100,cable:2:core,cable:1:core,9.6830997663775858e-05,0.00028061951809663077 +100,cable:2:core,cable:2:core,0.00011826512950095309,0.0007739317981559253 +100,cable:2:core,cable:3:core,0.00010608353483127337,0.00037094741867822398 +100,cable:3:core,cable:1:core,9.594289810496847e-05,0.00025153139211222538 +100,cable:3:core,cable:2:core,0.00010608353483127338,0.00037094741867822409 +100,cable:3:core,cable:3:core,0.00011825895634178478,0.00077390901166622618 +1000,cable:1:core,cable:1:core,0.00078545538086318019,0.0066160382983872414 +1000,cable:1:core,cable:2:core,0.00087926355750770843,0.001402374698144822 +1000,cable:1:core,cable:3:core,0.00083336723943152919,0.0011339562556012919 +1000,cable:2:core,cable:1:core,0.00087926355750770843,0.0014023746981448211 +1000,cable:2:core,cable:2:core,0.0011843876922731453,0.0060323216961683784 +1000,cable:2:core,cable:3:core,0.0011007933449973671,0.0020953145045025617 +1000,cable:3:core,cable:1:core,0.00083336723943152799,0.0011339562556012916 +1000,cable:3:core,cable:2:core,0.0011007933449973669,0.0020953145045025626 +1000,cable:3:core,cable:3:core,0.0011842919805786728,0.0060323818412918598 +10000,cable:1:core,cable:1:core,0.0051086484699835975,0.056506249875501097 +10000,cable:1:core,cable:2:core,0.0052556743075239708,0.0024792030592478047 +10000,cable:1:core,cable:3:core,0.0040495802099877413,0.001147388226955296 +10000,cable:2:core,cable:1:core,0.0052556743075239743,0.0024792030592478034 +10000,cable:2:core,cable:2:core,0.011119748056498867,0.042731634526618816 +10000,cable:2:core,cable:3:core,0.0080281876013619698,0.0048722508072881719 +10000,cable:3:core,cable:1:core,0.0040495802099877439,0.0011473882269552962 +10000,cable:3:core,cable:2:core,0.008028187601361968,0.0048722508072881745 +10000,cable:3:core,cable:3:core,0.011119703022718263,0.042733943760656479 +100000,cable:1:core,cable:1:core,0.024745328786787053,0.51190764993067117 +100000,cable:1:core,cable:2:core,0.0029228998850158476,-0.0055351845500160842 +100000,cable:1:core,cable:3:core,0.00087257166385033315,-0.0031045060939019955 +100000,cable:2:core,cable:1:core,0.0029228998850158493,-0.0055351845500160824 +100000,cable:2:core,cable:2:core,0.091488785921329557,0.28371125771885292 +100000,cable:2:core,cable:3:core,0.0087323644231482554,-0.008720517818696474 +100000,cable:3:core,cable:1:core,0.00087257166385033271,-0.0031045060939019959 +100000,cable:3:core,cable:2:core,0.0087323644231482572,-0.0087205178186964827 +100000,cable:3:core,cable:3:core,0.091493043570979815,0.28369441705329945 +1000000,cable:1:core,cable:1:core,0.093180086583616706,4.9164027228102034 +1000000,cable:1:core,cable:2:core,0.00018018320619215134,0.00022597300058552257 +1000000,cable:1:core,cable:3:core,8.1906550548926487e-05,8.9450810318331917e-05 +1000000,cable:2:core,cable:1:core,0.00018018320619215113,0.00022597300058552262 +1000000,cable:2:core,cable:2:core,0.75377952051842911,1.568277138147143 +1000000,cable:2:core,cable:3:core,0.00015808159292494346,0.001064814054945651 +1000000,cable:3:core,cable:1:core,8.1906550548926474e-05,8.9450810318331931e-05 +1000000,cable:3:core,cable:2:core,0.00015808159292494367,0.0010648140549456518 +1000000,cable:3:core,cable:3:core,0.75369515541135013,1.5684962544665286 +10000000,cable:1:core,cable:1:core,0.29944048111231336,48.172201251501221 +10000000,cable:1:core,cable:2:core,3.6173164098261593e-09,-8.4488955667334859e-10 +10000000,cable:1:core,cable:3:core,1.7322626704311287e-09,-2.8699699888026061e-10 +10000000,cable:2:core,cable:1:core,3.6173164098261606e-09,-8.4488955667334734e-10 +10000000,cable:2:core,cable:2:core,4.5499969707899197,6.6606240142600166 +10000000,cable:2:core,cable:3:core,1.6961332535262068e-08,2.3321441922614669e-08 +10000000,cable:3:core,cable:1:core,1.7322626704311266e-09,-2.8699699888026098e-10 +10000000,cable:3:core,cable:2:core,1.6961332535262058e-08,2.3321441922614619e-08 +10000000,cable:3:core,cable:3:core,4.5501680333556047,6.6604697518265077 diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/fem/computation-options.txt b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/fem/computation-options.txt new file mode 100644 index 000000000..987cdbcf4 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/fem/computation-options.txt @@ -0,0 +1 @@ +(ui = false, mesh_policy = :remesh, resume_run_directory = nothing, keep_run_directory = true, trace = true, output_basis = :pul, verbosity = (default = 1,), gmsh_verbosity = 2, getdp_verbosity = 4, frequency_workers = 2, solver_threads = 4, plot_field_maps = false) \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/fem/details.txt b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/fem/details.txt new file mode 100644 index 000000000..dd9fe7937 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/fem/details.txt @@ -0,0 +1 @@ +(files = [(path = "coordinator.lock", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/coordinator.lock", sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"), (path = "run.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/run.json", sha256 = "3cfe7084db85bc9c9529dcc20a688cdd99aa4b8f807904a6caf582aa6627a10a"), (path = "timing-summary.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/timing-summary.json", sha256 = "9eaf44fc38ed9d55bb67332e0617efe6b10962a5356cc7e34f0934de30acad2a"), (path = "attempts/f0001-UhE6WA/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0001-UhE6WA/attempt.json", sha256 = "0539816db924003ceddf786c52dd6ce8974769c6adcbf82d98d64c3b4ed9de01"), (path = "attempts/f0001-UhE6WA/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0001-UhE6WA/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0001-UhE6WA/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0001-UhE6WA/getdp.log", sha256 = "6d40b9add4d28c3c735479948a2e831c3a8a821240555fda78fb40ef4dc50bdb"), (path = "attempts/f0001-UhE6WA/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0001-UhE6WA/petsc.log", sha256 = "170493fdc27bc5755f45e31db387180b5bf9a9c723a167c36bf47cf4dfa4026b"), (path = "attempts/f0001-UhE6WA/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0001-UhE6WA/solver.pre", sha256 = "91f7cc28ea32c5fc51829d9a7cce1483208cc67a3bad5fe95c41ef0ddeb1eca7"), (path = "attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0001-P.tsv", sha256 = "c2aa0854dfcfaf5da0928ca17d1d6432fa66daba938130b06d348e855abfdd29"), (path = "attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0001-Z.tsv", sha256 = "b0919d31bdb401c244e9bb43fe29d64bb917454863de8d631c530f40b719617f"), (path = "attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0001-timing.tsv", sha256 = "4baba3c8f2dcb03e554cfbdf277fe7856ea0f7b2eb10a63f41970e261b293594"), (path = "attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0001.done", sha256 = "a40a11bab26b32dfb169550347f289dc710df24f59fe3ef20963d46f838f0586"), (path = "attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0002-P.tsv", sha256 = "b846b53e06337704ed9d5f7245d79d192c944b7c0dbeb1ec369e75a382efa4da"), (path = "attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0002-Z.tsv", sha256 = "fe4b1a4e25f68e63af0d8e923b5a748c1b4f9d22b6cf1f4195d263078898e641"), (path = "attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0002-timing.tsv", sha256 = "a8d5d9d2473c0b7d3a796119af263ddb425bf8ea95c0fedf93e005da28f89500"), (path = "attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0002.done", sha256 = "f58042f034284ccf2955f2795037937a5bd0e4288ffc0d0d5124e33ee07f17ec"), (path = "attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0003-P.tsv", sha256 = "66b97dd74250b87e97dd64c87741ac04d70f968dc8003a5ed03e9dd3c904d47c"), (path = "attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0003-Z.tsv", sha256 = "bb249f951e08313fdc9f40bfc6b9219967308bb934553bedbdb3e15c3f729c37"), (path = "attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0003-timing.tsv", sha256 = "c28a02f5ec153f79e8b47f34391a6c1da8f24f17cc1f01b6a31ea7641ba9d97d"), (path = "attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0003.done", sha256 = "41695cf0039479bd9b703276e3c15366a77a1e43f8490b48634d8a1c2a61be94"), (path = "attempts/f0002-IEMEPP/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0002-IEMEPP/attempt.json", sha256 = "79567044d2cfc38dd38546c9ce3c35b8422e1b6a15789ea608c13a452ced0670"), (path = "attempts/f0002-IEMEPP/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0002-IEMEPP/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0002-IEMEPP/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0002-IEMEPP/getdp.log", sha256 = "2662b1546dd170b6af7076be323e65bb4d43e0b0ce8e1728105a45b5e4d63e4d"), (path = "attempts/f0002-IEMEPP/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0002-IEMEPP/petsc.log", sha256 = "9cb8f0bff35d21a6f96b8d57ef5cb4b9f2efd070845c1cbab5f4a16c8c1ee133"), (path = "attempts/f0002-IEMEPP/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0002-IEMEPP/solver.pre", sha256 = "fd41ac3534b89222c29ff5e49f259898ee0aa2e26f041d65ca030364cc77746c"), (path = "attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0001-P.tsv", sha256 = "eac818132cc104f8d58d3b541111d4c8ff8addc0f6f0a0f6022f551132f5f2b1"), (path = "attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0001-Z.tsv", sha256 = "81581cc6460a1377c8b7f9f34d45b0a17ed3186af459df4ca095b1c8a394cfff"), (path = "attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0001-timing.tsv", sha256 = "85c7f9f1493fcd5def5bb6c85e668016001a1124187662de5df1d91ea915d8dd"), (path = "attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0001.done", sha256 = "3dbd9539f415ee889a34b9c6efe6b9cb8168b7c0945d52d7e63e920c8990d2d1"), (path = "attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0002-P.tsv", sha256 = "e89f568b14a42d0c7b1fb1056a46245347152d70abb521c72aa32532b692d653"), (path = "attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0002-Z.tsv", sha256 = "8c941f48178e9aff0c6d1769a2692e1b3c392313ccf25ce9e775798f3446183c"), (path = "attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0002-timing.tsv", sha256 = "8f5be96c13cb8f693471dae42daaa2ed59b30ae1246f77eea2d334c9946111fc"), (path = "attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0002.done", sha256 = "d498381e814531161686031fcde5aa549b456c6cbee61f6eb67916650247e717"), (path = "attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0003-P.tsv", sha256 = "3346ebbf3cdf16d297c9150e10f08f76d8cfacfae8da36db72737021375afb8b"), (path = "attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0003-Z.tsv", sha256 = "500da276e14936892aeb21cf1fbc336526908a0ca60c9f5600d868b50ce3a2a0"), (path = "attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0003-timing.tsv", sha256 = "6463220f1dd45b73a9fb664890a08c95267f98adc8db8d8e831b635c7f905855"), (path = "attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0003.done", sha256 = "fa5c0c361441906d1aaa6b254ae52969806be93c36846f4c70c70a18dc833050"), (path = "attempts/f0003-VT8lki/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0003-VT8lki/attempt.json", sha256 = "5a95bc23f8dd9fa191e7e9ef22702b383a5bea4e7cab91d24acde4504ec058ca"), (path = "attempts/f0003-VT8lki/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0003-VT8lki/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0003-VT8lki/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0003-VT8lki/getdp.log", sha256 = "05f764893ee5c2eb44a9992729413b0c115d602c7e5a74b69e7ef62d1146e81d"), (path = "attempts/f0003-VT8lki/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0003-VT8lki/petsc.log", sha256 = "3c4fb0f91e9830ca8e2f5fdda34669feac4f96a87cd61d0ebacc329cdf417f73"), (path = "attempts/f0003-VT8lki/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0003-VT8lki/solver.pre", sha256 = "5f4af8aaccaa0a92d211ca49eb2521168ed323c70d085f88e0feb85df146a89e"), (path = "attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0001-P.tsv", sha256 = "a743c86ce34f4e6c6e31329fef90292a2b9e264e6506bc9ce3eb08b27935399c"), (path = "attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0001-Z.tsv", sha256 = "03cf0e3cee60dced6152c5b370cc947c31fabd9c7911a865ad0cce06f7f2f9cd"), (path = "attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0001-timing.tsv", sha256 = "57da1d496e7a90c54f4039b30129de4539532e36f0da019a19164d0312a3b57b"), (path = "attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0001.done", sha256 = "02395cb824ac1c8ce1d2e8cd3c57fd19d5c904215aef49b1fba677c51e99d519"), (path = "attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0002-P.tsv", sha256 = "89ff1eb9b80a37ed050df698a08c1e3626bc4d8e0558aa57ef82434b8067d444"), (path = "attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0002-Z.tsv", sha256 = "ae83d620d67c83530d0a86f3d74383230d475f2b3e9429cb279a6d9b7f65c02c"), (path = "attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0002-timing.tsv", sha256 = "21b7590ee76cc557460b586b8922419c2fdadcbf85b8c81296a4807fc3dcadff"), (path = "attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0002.done", sha256 = "84034583a44a97df0ec5820a76fc891b95a9b6bc0bf693bed9882420a3eb5b05"), (path = "attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0003-P.tsv", sha256 = "55d90d455b3a02541f46a5169151dab2a31d812c371693a9f67f6f91fc3a9e47"), (path = "attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0003-Z.tsv", sha256 = "0b628fe51bc41b6a4a787526dd4113c7289e964fae96645e5a2e296689aa270e"), (path = "attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0003-timing.tsv", sha256 = "dbd214bd11f8c52edef5f63512faa4a8fa27744f6f1bef9c60cf6e87d18c8ff0"), (path = "attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0003.done", sha256 = "aec9dd1bab4102d3c07f9c6abf83e4d604721961d8e37cbbc9620f7f71cafcae"), (path = "attempts/f0004-Gb4HeY/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0004-Gb4HeY/attempt.json", sha256 = "61d5bb4ac22d747404c8ab8b64d1c242ab9ed015e845c9e858d84fc26e136425"), (path = "attempts/f0004-Gb4HeY/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0004-Gb4HeY/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0004-Gb4HeY/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0004-Gb4HeY/getdp.log", sha256 = "c06abc2a98bde093703121b8503c324fcf5295fb206776b342c2994471fb5b50"), (path = "attempts/f0004-Gb4HeY/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0004-Gb4HeY/petsc.log", sha256 = "59f15ea42eaf567e59bcea28d8aefc2a0e6fb583871c889fc7cefd297a29da8e"), (path = "attempts/f0004-Gb4HeY/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0004-Gb4HeY/solver.pre", sha256 = "06fc41e4f32bcc98f94f523eb25b26f694bc9aae65fe034817e2718eb3f374b2"), (path = "attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0001-P.tsv", sha256 = "1bd299e45b4efb8e27bad5cead17a591b8ef4b311dc9a9c9c00fc8a83b275a86"), (path = "attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0001-Z.tsv", sha256 = "bc963a0c03ed15ca0a5a6c60767671a74fc901951a8181619379d456dcf480ed"), (path = "attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0001-timing.tsv", sha256 = "7aeab688b092d4315e226b96cb394b3c04ff0bf3ef3f79df2b1d455d0c2b8026"), (path = "attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0001.done", sha256 = "05b52afc2d2a0ffefd0bb6e40585165d1b2572547d59d3c360b4163202709006"), (path = "attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0002-P.tsv", sha256 = "863f98fb7c167bb29dcdb14f9306f9466b3b838c6636e2d317bd279a6b03c81d"), (path = "attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0002-Z.tsv", sha256 = "f7bcf9bf8ee99cb5ef9e0c5660e5a29f693dc1eba181cccd96e2915d8cc11f7c"), (path = "attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0002-timing.tsv", sha256 = "2353c82478acc4954d2892f2fdbefbe3d3e6307df7785ab2b36de8f68104a3fe"), (path = "attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0002.done", sha256 = "35d216e20aed80a36327575b350d8e4c12b70f2038f5d07d55614c92afdd6b6b"), (path = "attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0003-P.tsv", sha256 = "07e28b67275f88973e853fc47601af1ec1c860b1feca71ed1d83ee7bdf5da110"), (path = "attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0003-Z.tsv", sha256 = "7af31afdc071841eb7232f791af1c7e72ac2ec530e9ea60845baa6ded1dec20e"), (path = "attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0003-timing.tsv", sha256 = "b1287a560de503b9528805d417657b6249350df2348442071086dd8b23011b27"), (path = "attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0003.done", sha256 = "eae2a23f21e24dd158a817c096e6e77f32e1d2a850c4163d279c3531eef2af5a"), (path = "attempts/f0005-zQphNs/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0005-zQphNs/attempt.json", sha256 = "0fcc7caee92c1951211baac63272f3003b9cdf50104211e49c592989da02124e"), (path = "attempts/f0005-zQphNs/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0005-zQphNs/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0005-zQphNs/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0005-zQphNs/getdp.log", sha256 = "87011105acb3ad5b56ec94d908fbedd9edb09d70c990f7346b2ab132b9b6eb71"), (path = "attempts/f0005-zQphNs/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0005-zQphNs/petsc.log", sha256 = "0602e9a89ad4ce1a36a0962fec93fa5b3155b7a8bcf42fc9495dbdbf350479ec"), (path = "attempts/f0005-zQphNs/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0005-zQphNs/solver.pre", sha256 = "28357a205c714f2107f5c19e6a5f52426f6bd09e74edcc5ae049eb3ec435e046"), (path = "attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0001-P.tsv", sha256 = "3398b57ec64eb6fae19ee1356c7dab47aa48dbbebe492f35703519b4ef4b0f6f"), (path = "attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0001-Z.tsv", sha256 = "e685c4abcc4b447849682b072c360d6e77374002167192f4dc8ef9339e0d0e0a"), (path = "attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0001-timing.tsv", sha256 = "1fc6e8e52ccc711ab9eb43002d21a40a8b2c971984450b1fc364a205a26dcc3f"), (path = "attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0001.done", sha256 = "ee7bf9802d600055d484ec4e501666418037b219c7bf910eefda35e16a843b08"), (path = "attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0002-P.tsv", sha256 = "908051f6c7688c8d2123e11334ab950e800b12fc08832372776a36ba8b1698a1"), (path = "attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0002-Z.tsv", sha256 = "5141eee98ece2f2b13dab47e1358a70589cc4e09facf02d1c6c63d18505ae832"), (path = "attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0002-timing.tsv", sha256 = "41edcadb637cac180a7728d032312db1c26a7787b7cfaf7aecc41afb3c870891"), (path = "attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0002.done", sha256 = "e26c3631aaa36c47efb3ede81242002dbebf27057f428fe57f60849cede6b3f3"), (path = "attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0003-P.tsv", sha256 = "09d4c5a0830a8ca5769c143d6b32d662386bee0ed11a353cfb9a4208e06bf61f"), (path = "attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0003-Z.tsv", sha256 = "7dd4aa242c093f26730ae7e97e16754be0306aa86fcb5cde1e220119dec6c786"), (path = "attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0003-timing.tsv", sha256 = "3e9eba1b4d0a4387f829fafdd33e7abb5bcd2fa1f572d0313c3d7ae5ee4b2c2b"), (path = "attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0003.done", sha256 = "a91d7ca1bb120ef43c71df371c45a71b1cc7744f1ee2eb082338700de7f329c9"), (path = "attempts/f0006-7pB1cx/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0006-7pB1cx/attempt.json", sha256 = "32a09038058481a22cf0860a407753fdbcbf2b188d7e68da76779da743764e7b"), (path = "attempts/f0006-7pB1cx/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0006-7pB1cx/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0006-7pB1cx/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0006-7pB1cx/getdp.log", sha256 = "16ec0b7375d4fe1d2761abb61432aa0bb894453ca121b973b955642d1fbe57b2"), (path = "attempts/f0006-7pB1cx/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0006-7pB1cx/petsc.log", sha256 = "2d7229923894bed921eb3b6d9cabd14ae26a9b20d398d448e6f008abc560b1a2"), (path = "attempts/f0006-7pB1cx/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0006-7pB1cx/solver.pre", sha256 = "aaeea057a70b35919f9de8d9e08f285053383cb1e7203b239a5a78f6524e85b1"), (path = "attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0001-P.tsv", sha256 = "865759434f809d392a1595c73b3f58ffb9d97e9242aea6c5f1d84a4e992bfa92"), (path = "attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0001-Z.tsv", sha256 = "89ae231d90380bdff7e76123b3372118793a506645ce6d1a3a75ce0b4bcf3a64"), (path = "attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0001-timing.tsv", sha256 = "b90f7c2e5e9561e364e7ad705e3e59f41a37cc9c19431ba12b5e592edd17f56e"), (path = "attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0001.done", sha256 = "e41a78288341a2ae156cd28b6102b3a31a74b012eb4deb233e88fae392912e8e"), (path = "attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0002-P.tsv", sha256 = "b9b04974edbcddcdb59dcdfb7299831919debb827ea9e02dda1ab5e93761b88e"), (path = "attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0002-Z.tsv", sha256 = "7ebed250deaa9ee9de867d79edc82254a76e841d7740b664f84644aae8a34a45"), (path = "attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0002-timing.tsv", sha256 = "700dd33f153d388e42fedcb3c62dd689eecb750008f39d542bdeb45cba720d3c"), (path = "attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0002.done", sha256 = "c105367e4bac19858725b1b7c56efbb25ae370173d002f9a678a4762a5c69ac9"), (path = "attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0003-P.tsv", sha256 = "5941985e00927cd56caf2e9b9cff07af500fa171eb0d1f06523a3d1d21918b61"), (path = "attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0003-Z.tsv", sha256 = "89504b5b4da4cb1d6003d262377da8b171ba95ad63a3877be4623e96e2b7b5dd"), (path = "attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0003-timing.tsv", sha256 = "9e2085f8b2425afbf99d2f9462e9b3eb5434ae20965430b38aa2537e9f8b619b"), (path = "attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0003.done", sha256 = "de6214d7f2cf6d2097d94d720289a7424da0b9bf7a5df27326d8b69e936798e8"), (path = "attempts/f0007-eCJ3wP/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0007-eCJ3wP/attempt.json", sha256 = "35f3b0bfd739bd41126562f02d7650dee68b7fff68fc1cc0432b574dc94ef2c6"), (path = "attempts/f0007-eCJ3wP/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0007-eCJ3wP/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0007-eCJ3wP/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0007-eCJ3wP/getdp.log", sha256 = "a8bec326f1bf46c9e4573c903dc9bf3d5d601e880ecfea7cfd0210ce3e1ae4f7"), (path = "attempts/f0007-eCJ3wP/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0007-eCJ3wP/petsc.log", sha256 = "3108117ad5d5cc6031983d2fd9657ea288dfd4104f32fad34f632f1883b71828"), (path = "attempts/f0007-eCJ3wP/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0007-eCJ3wP/solver.pre", sha256 = "98fbe22e311432a5cf63e2990866c6845417a5b867e04e25976ee643e8c2b508"), (path = "attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0001-P.tsv", sha256 = "4f21d6c2a0089b426f25fd933153dca0434e8c2cfa19fea8b8885f106b24442b"), (path = "attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0001-Z.tsv", sha256 = "2f401e56c1f6be6f4e3de378ce697cd224faa80804d2dbe891be6bfa8263ca25"), (path = "attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0001-timing.tsv", sha256 = "019a7f6d8d008a4fb431697deb309c64b7989ee49f43f2fc26cf0b05df41b781"), (path = "attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0001.done", sha256 = "d95e4d651bb7fa68e36e0077af5a10e10baacf852173da7ac0f1cca3e414dc7f"), (path = "attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0002-P.tsv", sha256 = "3247adec4590aa01d86bfda50e7414c2d37f33eab782330f9209b01e891bde30"), (path = "attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0002-Z.tsv", sha256 = "61ee9cd65d8aa189d0fe5ca7d73a553cea0fb351a2cb23a9950c14ffc1bed663"), (path = "attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0002-timing.tsv", sha256 = "308e5ffce6dc41a9ac1b5ed885777ed7f127f6bab880161971d77c55840d80c5"), (path = "attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0002.done", sha256 = "a953202777b2aea0282f4e5ecac9feb227f850cb3f80f2247ca07a708f3635be"), (path = "attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0003-P.tsv", sha256 = "a7fba6388c8dabacc4ebcd345fc59faf8dc00ddb6bc96ce1ec19232bd1e86ac0"), (path = "attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0003-Z.tsv", sha256 = "a1baca9f06df00bff1eebc6a31412d0a38d1fdb5dd303c0c62970ca7bb2278af"), (path = "attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0003-timing.tsv", sha256 = "b77562ae48824f289d666d6cff9974dc0b30808f1169ef74541eac5cd77ef858"), (path = "attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0003.done", sha256 = "bc6e3e671a966700354c8fb7f9ab0ff830db8a4b88332e0ec4974ce29df37b0c"), (path = "attempts/f0008-PNwfpm/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0008-PNwfpm/attempt.json", sha256 = "078d355a5d6821f1b6057d34efa4870fb1d617a9d504277885d02dc3432b2147"), (path = "attempts/f0008-PNwfpm/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0008-PNwfpm/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0008-PNwfpm/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0008-PNwfpm/getdp.log", sha256 = "3ca388bad2af86eb35af7ce40e078621e1e028aa37a72b799afd81317f2dace4"), (path = "attempts/f0008-PNwfpm/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0008-PNwfpm/petsc.log", sha256 = "ab5ba703133433f0fd4686077e7899c52b3ed3ff24a52d15658ca68865c42b6f"), (path = "attempts/f0008-PNwfpm/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0008-PNwfpm/solver.pre", sha256 = "49d13134d2183a62ff2dd6ea88dbd76d815995fb64f3fdc2bc1d6d78b67bcfb8"), (path = "attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0001-P.tsv", sha256 = "7ec170463ed10318b84d75dbf349106889f6203587627107fa68b08e72875ef4"), (path = "attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0001-Z.tsv", sha256 = "34e4772ed0d223b1897452b90094fa71d30f065b0f906841320f216b23e66095"), (path = "attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0001-timing.tsv", sha256 = "55f45e179a33d9447288d277b1caaf8e3c8809e72bcbe3d28887ead084fc4d9a"), (path = "attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0001.done", sha256 = "c03ca6db0131d3218478fb4e85f0350dca26787b9ffdfc9d50b5d68ad93b352c"), (path = "attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0002-P.tsv", sha256 = "77b8d6a0a84bc66c1ae733c9b191b32836ffefc9d03e14020812c25ffa5e7003"), (path = "attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0002-Z.tsv", sha256 = "7413fa7f6e036b018ef3683b03ff41f458833daf1faa94a82ae1f0548a45c0a8"), (path = "attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0002-timing.tsv", sha256 = "5ef5a19f429019393e2740709599ed76a9f3ba045ff0d28f1442f338bcbb11d0"), (path = "attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0002.done", sha256 = "76f50b528daca79ab1eac47c47f96ca1e7b8ad94bcaefbaac04335941306f993"), (path = "attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0003-P.tsv", sha256 = "7238161273ea9daf4c5754d4f07278913fad3d1af45309be865b041e895162c1"), (path = "attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0003-Z.tsv", sha256 = "b5c816019ac161e29efcf04512ee7eac006b7128fe57c2ac1fa69060e9aad2c8"), (path = "attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0003-timing.tsv", sha256 = "0180204ba44884eef9956d7b4fdaf4c654ef35cd9416fc4b5e8af45b16c487e0"), (path = "attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0003.done", sha256 = "20bdbe7d9351850f55ebf49a31f3a93235d04aea44253ddd2d75828a83fa1c74"), (path = "attempts/f0009-LxLb8P/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0009-LxLb8P/attempt.json", sha256 = "c1a6799729913375fbb5d4bc6a7ca15e1d93d7ddfdbc28f2e9130e7e9ac38746"), (path = "attempts/f0009-LxLb8P/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0009-LxLb8P/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0009-LxLb8P/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0009-LxLb8P/getdp.log", sha256 = "13cadeb7cfec42460d4f8e643ffa24e671708c0048c9d56b626184ec3f8120ba"), (path = "attempts/f0009-LxLb8P/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0009-LxLb8P/petsc.log", sha256 = "6b4967511e4cc67af51510a4e19fb4c174d1648f0c011bb21d43c5de2c5e7912"), (path = "attempts/f0009-LxLb8P/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0009-LxLb8P/solver.pre", sha256 = "9469af55317a8fa5a727ad7d47b0d90e1abb2004e6b52de8177a88668578aec8"), (path = "attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0001-P.tsv", sha256 = "ca8bf539a2aa791698165bb81d085f5acbb189b3b62d246ba937dc92ce7353f7"), (path = "attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0001-Z.tsv", sha256 = "e474b796474a4166bc2883c204746842000b1b101b09acf18c4419197c451a47"), (path = "attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0001-timing.tsv", sha256 = "32ef8681dc4f1d2fe0bfbdccc584179f43ed906d8567e350078c46246e98cfec"), (path = "attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0001.done", sha256 = "22b8b831af1067ac03eaf9570ddd71235af29f6b65e559199200376f6ef90f2a"), (path = "attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0002-P.tsv", sha256 = "9792cefef8b214884d006bafa09587a935868ec363641eec307a112df4090d34"), (path = "attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0002-Z.tsv", sha256 = "2ac4420529eb51e337786056ac19b4f6b190e515cc8194d4a6dc6eb84241cd41"), (path = "attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0002-timing.tsv", sha256 = "77a626f5fa0b16f4b318ee4e5218e69642558e9f4578f95e2817835cbff61abf"), (path = "attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0002.done", sha256 = "2ab6b278377a8a5090d5d456a9193028844db99b8f3fa96c2433d70920e73c22"), (path = "attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0003-P.tsv", sha256 = "baeca8aeb00e5b88a304688c8e3138f47a07a19b67685f5f0163d88e8fac0c42"), (path = "attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0003-Z.tsv", sha256 = "e40082bcb62bad066853868f3ad9406ed0b293ad12b07856df4861040df35107"), (path = "attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0003-timing.tsv", sha256 = "de46b42118e3fcaeebbd26d9e831304addcb47db275fb8ce4d3e758d345e6b7d"), (path = "attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0003.done", sha256 = "3449a1ca026b39cca413695b5bd20e176addf6bcf3103f9147e8ac56407cafa3"), (path = "input/computation.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/input/computation.json", sha256 = "98dd49d4fab5ebaa2cf0347ad83da5460980f9a78b7ed918aa828a7303d5d9ea"), (path = "input/model_data.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/input/model_data.pro", sha256 = "d38113a1162b9d199ae30895953776d5e0c71e5de4b70ba24789c48df687a09e"), (path = "input/problem.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/input/problem.json", sha256 = "0d236876854a892fd2068076209dd8cec97cacf6920c7fbd62e1b55852b9f052"), (path = "input/getdp/integration.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/input/getdp/integration.pro", sha256 = "e113951ac1e6e658f1be50e53875e30c85497445f0e6584a8a7ab1848588e902"), (path = "input/getdp/jacobian.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/input/getdp/jacobian.pro", sha256 = "014208d39043ab13183c42c312aba459265715da6d6ddc38ec1fbc39b6090c73"), (path = "input/getdp/materials.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/input/getdp/materials.pro", sha256 = "581f089113810a379d8a49c03d79f55045b723c8f4c761d0a493a019ff1f5d9e"), (path = "input/getdp/model.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/input/getdp/model.pro", sha256 = "c0bb424db5ab4db9ca22f549f675edb4646f69910d2aa14e20cf4f40e41bd313"), (path = "input/getdp/quasi-full.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/input/getdp/quasi-full.pro", sha256 = "1aede35114ea686c5f109089478027d241dec12dc9fd14a24452b348f89b08e4"), (path = "input/getdp/quasi-tem.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/input/getdp/quasi-tem.pro", sha256 = "448d9019ad417c9a784da20170b855dd25e559297499a83d19b007ee095c084d"), (path = "logs/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/logs/getdp.log", sha256 = "4efaf0e96254e59de97305f31f145e7015cf96f1b73f1f88d1219bf963129113"), (path = "mesh/frequency_0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/mesh/frequency_0001.json", sha256 = "fd9bf880bbf984ecf0d22113f436522d849bc65471655dfe09c842a3ebb91f01"), (path = "mesh/frequency_0001.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/mesh/frequency_0001.msh", sha256 = "30808a238d48b33964a2b6df37f63f4fe84402cb6576153b6033972e159f4623"), (path = "mesh/frequency_0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/mesh/frequency_0002.json", sha256 = "ab0f1dc27cfe09cdf7dadd4e43964b1195d25f6dd36994931fe9fa1833a07d6b"), (path = "mesh/frequency_0002.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/mesh/frequency_0002.msh", sha256 = "cef5bfc15ed4d508849804258954130cc4800cce559dc0908b8c550ddad1f1f0"), (path = "mesh/frequency_0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/mesh/frequency_0003.json", sha256 = "3587f476377b846bdfd55affe74b8e8790bf707a33105f30c4ca350422d4fece"), (path = "mesh/frequency_0003.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/mesh/frequency_0003.msh", sha256 = "a045634bc341469391173419f8bf11beaa4da0e8dd467f8765b79446b7bc963c"), (path = "mesh/frequency_0004.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/mesh/frequency_0004.json", sha256 = "4340cac5bcf001e3d653ea848d77b31d613d6091af9e31242bd0f6d791fe6979"), (path = "mesh/frequency_0004.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/mesh/frequency_0004.msh", sha256 = "3854ff79dd225253f26590eb542b65b3e45d1986ed407521fd1dfc00497c1ff2"), (path = "mesh/frequency_0005.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/mesh/frequency_0005.json", sha256 = "5bd0cb902f9b56efa112fb325a3b34014a243acf0012f58aecf20411c0ec0387"), (path = "mesh/frequency_0005.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/mesh/frequency_0005.msh", sha256 = "5f50316c4720fbe239e7c02cd5f1871f90f15341ab3bfb272ed24cf153fc979f"), (path = "mesh/frequency_0006.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/mesh/frequency_0006.json", sha256 = "bccaaa578168c9796004cfd3ccc96abde23d68adbf67c789e0ac664bd5405afc"), (path = "mesh/frequency_0006.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/mesh/frequency_0006.msh", sha256 = "b4a18b97046fee852e8a83f5c04841a61e894860c5e6f163187c98944bdcaef7"), (path = "mesh/frequency_0007.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/mesh/frequency_0007.json", sha256 = "12634fd99bbcf70243eae804880443312e0ac330b39cb48dc3eab45d615a1efb"), (path = "mesh/frequency_0007.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/mesh/frequency_0007.msh", sha256 = "149eb973c09b9be9492291ac1bb45147a4860198b87c7d46d98135bf513cefda"), (path = "mesh/frequency_0008.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/mesh/frequency_0008.json", sha256 = "4f1865f48f276e337f2669f3b5334b1f50c8731641af2bfd1607892ac62cfc3b"), (path = "mesh/frequency_0008.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/mesh/frequency_0008.msh", sha256 = "0d8146664febdd2bb4f0dae6f0058fff3141a7c812f1daa11de77856f1bd8aab"), (path = "mesh/model.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/mesh/model.json", sha256 = "0c2709eda9d3a07914ac97418cd68558262dca5df46ab0266ee32c09d29ae619"), (path = "mesh/model.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/mesh/model.msh", sha256 = "9e1a15c73ea3c5b3f4238b57b71348061afa00aa85380569ee663a5899906737"), (path = "raw/P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/P.tsv", sha256 = "ff5c34e9d74d65fd75b4832a7a71ea0bf4f17c3bccf816d7fc5da0eee68ca721"), (path = "raw/Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/Z.tsv", sha256 = "1f55f46e944065e22c24757b863d710071f47e4072484e6fd1b47bb2121de820"), (path = "raw/checksums.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/checksums.json", sha256 = "7120e2db42512d47de45db0731d31da6295279cb4618aa2cb180919f530ffba4"), (path = "raw/scan_complete.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/scan_complete.tsv", sha256 = "2cc9eff50bb7e6fff0e2555f96d93ebe952c4a1a158a1dca82fe6cca3aa1afab"), (path = "raw/jobs/getdp-f0001-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0001-b0001-P.tsv", sha256 = "c2aa0854dfcfaf5da0928ca17d1d6432fa66daba938130b06d348e855abfdd29"), (path = "raw/jobs/getdp-f0001-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0001-b0001-Z.tsv", sha256 = "b0919d31bdb401c244e9bb43fe29d64bb917454863de8d631c530f40b719617f"), (path = "raw/jobs/getdp-f0001-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0001-b0001-timing.tsv", sha256 = "4baba3c8f2dcb03e554cfbdf277fe7856ea0f7b2eb10a63f41970e261b293594"), (path = "raw/jobs/getdp-f0001-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0001-b0001.json", sha256 = "3857cd575f8f6e4cbc6565951580b632e940f72b3146ea376728288cdbddf84a"), (path = "raw/jobs/getdp-f0001-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0001-b0002-P.tsv", sha256 = "b846b53e06337704ed9d5f7245d79d192c944b7c0dbeb1ec369e75a382efa4da"), (path = "raw/jobs/getdp-f0001-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0001-b0002-Z.tsv", sha256 = "fe4b1a4e25f68e63af0d8e923b5a748c1b4f9d22b6cf1f4195d263078898e641"), (path = "raw/jobs/getdp-f0001-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0001-b0002-timing.tsv", sha256 = "a8d5d9d2473c0b7d3a796119af263ddb425bf8ea95c0fedf93e005da28f89500"), (path = "raw/jobs/getdp-f0001-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0001-b0002.json", sha256 = "e5318cbffdc5e87cc4654b4f877f9f2939c0a0c80b5dfdaf76e8cd36775766fa"), (path = "raw/jobs/getdp-f0001-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0001-b0003-P.tsv", sha256 = "66b97dd74250b87e97dd64c87741ac04d70f968dc8003a5ed03e9dd3c904d47c"), (path = "raw/jobs/getdp-f0001-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0001-b0003-Z.tsv", sha256 = "bb249f951e08313fdc9f40bfc6b9219967308bb934553bedbdb3e15c3f729c37"), (path = "raw/jobs/getdp-f0001-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0001-b0003-timing.tsv", sha256 = "c28a02f5ec153f79e8b47f34391a6c1da8f24f17cc1f01b6a31ea7641ba9d97d"), (path = "raw/jobs/getdp-f0001-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0001-b0003.json", sha256 = "d5dc4f9b512885ae10aab7a8891e6ec04aa42a07ef3313fb422978991040cb20"), (path = "raw/jobs/getdp-f0002-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0002-b0001-P.tsv", sha256 = "eac818132cc104f8d58d3b541111d4c8ff8addc0f6f0a0f6022f551132f5f2b1"), (path = "raw/jobs/getdp-f0002-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0002-b0001-Z.tsv", sha256 = "81581cc6460a1377c8b7f9f34d45b0a17ed3186af459df4ca095b1c8a394cfff"), (path = "raw/jobs/getdp-f0002-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0002-b0001-timing.tsv", sha256 = "85c7f9f1493fcd5def5bb6c85e668016001a1124187662de5df1d91ea915d8dd"), (path = "raw/jobs/getdp-f0002-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0002-b0001.json", sha256 = "4494a7fecd50484db524c0899c66a2f1e9c5502dd9e9f077205dc149c665d7f1"), (path = "raw/jobs/getdp-f0002-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0002-b0002-P.tsv", sha256 = "e89f568b14a42d0c7b1fb1056a46245347152d70abb521c72aa32532b692d653"), (path = "raw/jobs/getdp-f0002-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0002-b0002-Z.tsv", sha256 = "8c941f48178e9aff0c6d1769a2692e1b3c392313ccf25ce9e775798f3446183c"), (path = "raw/jobs/getdp-f0002-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0002-b0002-timing.tsv", sha256 = "8f5be96c13cb8f693471dae42daaa2ed59b30ae1246f77eea2d334c9946111fc"), (path = "raw/jobs/getdp-f0002-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0002-b0002.json", sha256 = "effd22fe8752d78ced2dd70a43e73d5aace3bb790d23a7319aa9c658ca6d156d"), (path = "raw/jobs/getdp-f0002-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0002-b0003-P.tsv", sha256 = "3346ebbf3cdf16d297c9150e10f08f76d8cfacfae8da36db72737021375afb8b"), (path = "raw/jobs/getdp-f0002-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0002-b0003-Z.tsv", sha256 = "500da276e14936892aeb21cf1fbc336526908a0ca60c9f5600d868b50ce3a2a0"), (path = "raw/jobs/getdp-f0002-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0002-b0003-timing.tsv", sha256 = "6463220f1dd45b73a9fb664890a08c95267f98adc8db8d8e831b635c7f905855"), (path = "raw/jobs/getdp-f0002-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0002-b0003.json", sha256 = "9985fcbb546761fc6e0950b4b2dfec4cf3a57ca776034dc6a95ea146781896c1"), (path = "raw/jobs/getdp-f0003-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0003-b0001-P.tsv", sha256 = "a743c86ce34f4e6c6e31329fef90292a2b9e264e6506bc9ce3eb08b27935399c"), (path = "raw/jobs/getdp-f0003-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0003-b0001-Z.tsv", sha256 = "03cf0e3cee60dced6152c5b370cc947c31fabd9c7911a865ad0cce06f7f2f9cd"), (path = "raw/jobs/getdp-f0003-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0003-b0001-timing.tsv", sha256 = "57da1d496e7a90c54f4039b30129de4539532e36f0da019a19164d0312a3b57b"), (path = "raw/jobs/getdp-f0003-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0003-b0001.json", sha256 = "4d77ed245a8282f169a98d7ec27fbfa7b8b8102d5217e116c1620503545bab2c"), (path = "raw/jobs/getdp-f0003-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0003-b0002-P.tsv", sha256 = "89ff1eb9b80a37ed050df698a08c1e3626bc4d8e0558aa57ef82434b8067d444"), (path = "raw/jobs/getdp-f0003-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0003-b0002-Z.tsv", sha256 = "ae83d620d67c83530d0a86f3d74383230d475f2b3e9429cb279a6d9b7f65c02c"), (path = "raw/jobs/getdp-f0003-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0003-b0002-timing.tsv", sha256 = "21b7590ee76cc557460b586b8922419c2fdadcbf85b8c81296a4807fc3dcadff"), (path = "raw/jobs/getdp-f0003-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0003-b0002.json", sha256 = "ef1088694fa66cf6df385d4a8b8a5cbfaa9b9c0829b9e4e58c27cb717e918643"), (path = "raw/jobs/getdp-f0003-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0003-b0003-P.tsv", sha256 = "55d90d455b3a02541f46a5169151dab2a31d812c371693a9f67f6f91fc3a9e47"), (path = "raw/jobs/getdp-f0003-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0003-b0003-Z.tsv", sha256 = "0b628fe51bc41b6a4a787526dd4113c7289e964fae96645e5a2e296689aa270e"), (path = "raw/jobs/getdp-f0003-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0003-b0003-timing.tsv", sha256 = "dbd214bd11f8c52edef5f63512faa4a8fa27744f6f1bef9c60cf6e87d18c8ff0"), (path = "raw/jobs/getdp-f0003-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0003-b0003.json", sha256 = "bd007d6bdb57701852d8c14848698dc79ad49983d0d17468ee55d263272335b9"), (path = "raw/jobs/getdp-f0004-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0004-b0001-P.tsv", sha256 = "1bd299e45b4efb8e27bad5cead17a591b8ef4b311dc9a9c9c00fc8a83b275a86"), (path = "raw/jobs/getdp-f0004-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0004-b0001-Z.tsv", sha256 = "bc963a0c03ed15ca0a5a6c60767671a74fc901951a8181619379d456dcf480ed"), (path = "raw/jobs/getdp-f0004-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0004-b0001-timing.tsv", sha256 = "7aeab688b092d4315e226b96cb394b3c04ff0bf3ef3f79df2b1d455d0c2b8026"), (path = "raw/jobs/getdp-f0004-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0004-b0001.json", sha256 = "df2841700247652e6491839a1393bed58e18dc80dddcf0d4c55b7378bfe6158e"), (path = "raw/jobs/getdp-f0004-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0004-b0002-P.tsv", sha256 = "863f98fb7c167bb29dcdb14f9306f9466b3b838c6636e2d317bd279a6b03c81d"), (path = "raw/jobs/getdp-f0004-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0004-b0002-Z.tsv", sha256 = "f7bcf9bf8ee99cb5ef9e0c5660e5a29f693dc1eba181cccd96e2915d8cc11f7c"), (path = "raw/jobs/getdp-f0004-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0004-b0002-timing.tsv", sha256 = "2353c82478acc4954d2892f2fdbefbe3d3e6307df7785ab2b36de8f68104a3fe"), (path = "raw/jobs/getdp-f0004-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0004-b0002.json", sha256 = "3d559ddebbd63913bbaa84973b5503bcc0f9db3dee993b5b532239cacfdfbf1c"), (path = "raw/jobs/getdp-f0004-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0004-b0003-P.tsv", sha256 = "07e28b67275f88973e853fc47601af1ec1c860b1feca71ed1d83ee7bdf5da110"), (path = "raw/jobs/getdp-f0004-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0004-b0003-Z.tsv", sha256 = "7af31afdc071841eb7232f791af1c7e72ac2ec530e9ea60845baa6ded1dec20e"), (path = "raw/jobs/getdp-f0004-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0004-b0003-timing.tsv", sha256 = "b1287a560de503b9528805d417657b6249350df2348442071086dd8b23011b27"), (path = "raw/jobs/getdp-f0004-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0004-b0003.json", sha256 = "69d021834e7765e8ac9763295f1ab973e33bfd3b0d9db1ad94c3c0208a50acd5"), (path = "raw/jobs/getdp-f0005-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0005-b0001-P.tsv", sha256 = "3398b57ec64eb6fae19ee1356c7dab47aa48dbbebe492f35703519b4ef4b0f6f"), (path = "raw/jobs/getdp-f0005-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0005-b0001-Z.tsv", sha256 = "e685c4abcc4b447849682b072c360d6e77374002167192f4dc8ef9339e0d0e0a"), (path = "raw/jobs/getdp-f0005-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0005-b0001-timing.tsv", sha256 = "1fc6e8e52ccc711ab9eb43002d21a40a8b2c971984450b1fc364a205a26dcc3f"), (path = "raw/jobs/getdp-f0005-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0005-b0001.json", sha256 = "7d9d02df5234b63cd33221759290a78151023cbf236f4f05cd6b612504b37046"), (path = "raw/jobs/getdp-f0005-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0005-b0002-P.tsv", sha256 = "908051f6c7688c8d2123e11334ab950e800b12fc08832372776a36ba8b1698a1"), (path = "raw/jobs/getdp-f0005-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0005-b0002-Z.tsv", sha256 = "5141eee98ece2f2b13dab47e1358a70589cc4e09facf02d1c6c63d18505ae832"), (path = "raw/jobs/getdp-f0005-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0005-b0002-timing.tsv", sha256 = "41edcadb637cac180a7728d032312db1c26a7787b7cfaf7aecc41afb3c870891"), (path = "raw/jobs/getdp-f0005-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0005-b0002.json", sha256 = "8259abf413d084c08e0c7fad122814a05fa6c84c22d337701dce3d03a0f1bf01"), (path = "raw/jobs/getdp-f0005-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0005-b0003-P.tsv", sha256 = "09d4c5a0830a8ca5769c143d6b32d662386bee0ed11a353cfb9a4208e06bf61f"), (path = "raw/jobs/getdp-f0005-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0005-b0003-Z.tsv", sha256 = "7dd4aa242c093f26730ae7e97e16754be0306aa86fcb5cde1e220119dec6c786"), (path = "raw/jobs/getdp-f0005-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0005-b0003-timing.tsv", sha256 = "3e9eba1b4d0a4387f829fafdd33e7abb5bcd2fa1f572d0313c3d7ae5ee4b2c2b"), (path = "raw/jobs/getdp-f0005-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0005-b0003.json", sha256 = "8e3ca22aed4133227e6008e4e79509e50785d316a9a65412b1caa8952df3279a"), (path = "raw/jobs/getdp-f0006-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0006-b0001-P.tsv", sha256 = "865759434f809d392a1595c73b3f58ffb9d97e9242aea6c5f1d84a4e992bfa92"), (path = "raw/jobs/getdp-f0006-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0006-b0001-Z.tsv", sha256 = "89ae231d90380bdff7e76123b3372118793a506645ce6d1a3a75ce0b4bcf3a64"), (path = "raw/jobs/getdp-f0006-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0006-b0001-timing.tsv", sha256 = "b90f7c2e5e9561e364e7ad705e3e59f41a37cc9c19431ba12b5e592edd17f56e"), (path = "raw/jobs/getdp-f0006-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0006-b0001.json", sha256 = "bad565cc01125dccde1a5ecdc47cae1f63fdd4f670e2188027fe87877416e90d"), (path = "raw/jobs/getdp-f0006-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0006-b0002-P.tsv", sha256 = "b9b04974edbcddcdb59dcdfb7299831919debb827ea9e02dda1ab5e93761b88e"), (path = "raw/jobs/getdp-f0006-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0006-b0002-Z.tsv", sha256 = "7ebed250deaa9ee9de867d79edc82254a76e841d7740b664f84644aae8a34a45"), (path = "raw/jobs/getdp-f0006-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0006-b0002-timing.tsv", sha256 = "700dd33f153d388e42fedcb3c62dd689eecb750008f39d542bdeb45cba720d3c"), (path = "raw/jobs/getdp-f0006-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0006-b0002.json", sha256 = "7eae4dc199df86a9717982584fed8f98cd746e47965f830760d89b03ddabb38a"), (path = "raw/jobs/getdp-f0006-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0006-b0003-P.tsv", sha256 = "5941985e00927cd56caf2e9b9cff07af500fa171eb0d1f06523a3d1d21918b61"), (path = "raw/jobs/getdp-f0006-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0006-b0003-Z.tsv", sha256 = "89504b5b4da4cb1d6003d262377da8b171ba95ad63a3877be4623e96e2b7b5dd"), (path = "raw/jobs/getdp-f0006-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0006-b0003-timing.tsv", sha256 = "9e2085f8b2425afbf99d2f9462e9b3eb5434ae20965430b38aa2537e9f8b619b"), (path = "raw/jobs/getdp-f0006-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0006-b0003.json", sha256 = "935523aef728d4345f46e44d3d0736d17088d31e41ee2e14710b1764e86650b4"), (path = "raw/jobs/getdp-f0007-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0007-b0001-P.tsv", sha256 = "4f21d6c2a0089b426f25fd933153dca0434e8c2cfa19fea8b8885f106b24442b"), (path = "raw/jobs/getdp-f0007-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0007-b0001-Z.tsv", sha256 = "2f401e56c1f6be6f4e3de378ce697cd224faa80804d2dbe891be6bfa8263ca25"), (path = "raw/jobs/getdp-f0007-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0007-b0001-timing.tsv", sha256 = "019a7f6d8d008a4fb431697deb309c64b7989ee49f43f2fc26cf0b05df41b781"), (path = "raw/jobs/getdp-f0007-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0007-b0001.json", sha256 = "0e9a9b7d4e66b2a4e4a89cf9f3677518437253bc085a16c2c2b43df42b34bc23"), (path = "raw/jobs/getdp-f0007-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0007-b0002-P.tsv", sha256 = "3247adec4590aa01d86bfda50e7414c2d37f33eab782330f9209b01e891bde30"), (path = "raw/jobs/getdp-f0007-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0007-b0002-Z.tsv", sha256 = "61ee9cd65d8aa189d0fe5ca7d73a553cea0fb351a2cb23a9950c14ffc1bed663"), (path = "raw/jobs/getdp-f0007-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0007-b0002-timing.tsv", sha256 = "308e5ffce6dc41a9ac1b5ed885777ed7f127f6bab880161971d77c55840d80c5"), (path = "raw/jobs/getdp-f0007-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0007-b0002.json", sha256 = "b51a8e184dea9b640695753043e2496b4e0e647ade883ff952d0718dd57ef2ef"), (path = "raw/jobs/getdp-f0007-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0007-b0003-P.tsv", sha256 = "a7fba6388c8dabacc4ebcd345fc59faf8dc00ddb6bc96ce1ec19232bd1e86ac0"), (path = "raw/jobs/getdp-f0007-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0007-b0003-Z.tsv", sha256 = "a1baca9f06df00bff1eebc6a31412d0a38d1fdb5dd303c0c62970ca7bb2278af"), (path = "raw/jobs/getdp-f0007-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0007-b0003-timing.tsv", sha256 = "b77562ae48824f289d666d6cff9974dc0b30808f1169ef74541eac5cd77ef858"), (path = "raw/jobs/getdp-f0007-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0007-b0003.json", sha256 = "bafebe410a66b76df6190042f61a62edad50c8e5dafa0d2ed6c9977ea8b7d359"), (path = "raw/jobs/getdp-f0008-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0008-b0001-P.tsv", sha256 = "7ec170463ed10318b84d75dbf349106889f6203587627107fa68b08e72875ef4"), (path = "raw/jobs/getdp-f0008-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0008-b0001-Z.tsv", sha256 = "34e4772ed0d223b1897452b90094fa71d30f065b0f906841320f216b23e66095"), (path = "raw/jobs/getdp-f0008-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0008-b0001-timing.tsv", sha256 = "55f45e179a33d9447288d277b1caaf8e3c8809e72bcbe3d28887ead084fc4d9a"), (path = "raw/jobs/getdp-f0008-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0008-b0001.json", sha256 = "6e957a61541e938a9bb08b4ccffa44b84f51d0b58ae427a4a3adf0a33bad8b01"), (path = "raw/jobs/getdp-f0008-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0008-b0002-P.tsv", sha256 = "77b8d6a0a84bc66c1ae733c9b191b32836ffefc9d03e14020812c25ffa5e7003"), (path = "raw/jobs/getdp-f0008-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0008-b0002-Z.tsv", sha256 = "7413fa7f6e036b018ef3683b03ff41f458833daf1faa94a82ae1f0548a45c0a8"), (path = "raw/jobs/getdp-f0008-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0008-b0002-timing.tsv", sha256 = "5ef5a19f429019393e2740709599ed76a9f3ba045ff0d28f1442f338bcbb11d0"), (path = "raw/jobs/getdp-f0008-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0008-b0002.json", sha256 = "ed21946bfc5fa28540838cd0c2073e781046998a9f0e1fc4ccb88338d9d3d44b"), (path = "raw/jobs/getdp-f0008-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0008-b0003-P.tsv", sha256 = "7238161273ea9daf4c5754d4f07278913fad3d1af45309be865b041e895162c1"), (path = "raw/jobs/getdp-f0008-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0008-b0003-Z.tsv", sha256 = "b5c816019ac161e29efcf04512ee7eac006b7128fe57c2ac1fa69060e9aad2c8"), (path = "raw/jobs/getdp-f0008-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0008-b0003-timing.tsv", sha256 = "0180204ba44884eef9956d7b4fdaf4c654ef35cd9416fc4b5e8af45b16c487e0"), (path = "raw/jobs/getdp-f0008-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0008-b0003.json", sha256 = "6e1dfd89429f100e59ee198995197c74b2a48f85a7de11aa4027ca9654fa93b5"), (path = "raw/jobs/getdp-f0009-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0009-b0001-P.tsv", sha256 = "ca8bf539a2aa791698165bb81d085f5acbb189b3b62d246ba937dc92ce7353f7"), (path = "raw/jobs/getdp-f0009-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0009-b0001-Z.tsv", sha256 = "e474b796474a4166bc2883c204746842000b1b101b09acf18c4419197c451a47"), (path = "raw/jobs/getdp-f0009-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0009-b0001-timing.tsv", sha256 = "32ef8681dc4f1d2fe0bfbdccc584179f43ed906d8567e350078c46246e98cfec"), (path = "raw/jobs/getdp-f0009-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0009-b0001.json", sha256 = "d7fccb1dffe6ee790daba952b3ab9db13481bbe91cf43124467a89d4e7c1a612"), (path = "raw/jobs/getdp-f0009-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0009-b0002-P.tsv", sha256 = "9792cefef8b214884d006bafa09587a935868ec363641eec307a112df4090d34"), (path = "raw/jobs/getdp-f0009-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0009-b0002-Z.tsv", sha256 = "2ac4420529eb51e337786056ac19b4f6b190e515cc8194d4a6dc6eb84241cd41"), (path = "raw/jobs/getdp-f0009-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0009-b0002-timing.tsv", sha256 = "77a626f5fa0b16f4b318ee4e5218e69642558e9f4578f95e2817835cbff61abf"), (path = "raw/jobs/getdp-f0009-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0009-b0002.json", sha256 = "eefc711a0655e8216cc77c07b4070cc17709c324ebdb8127be119356ef6e3450"), (path = "raw/jobs/getdp-f0009-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0009-b0003-P.tsv", sha256 = "baeca8aeb00e5b88a304688c8e3138f47a07a19b67685f5f0163d88e8fac0c42"), (path = "raw/jobs/getdp-f0009-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0009-b0003-Z.tsv", sha256 = "e40082bcb62bad066853868f3ad9406ed0b293ad12b07856df4861040df35107"), (path = "raw/jobs/getdp-f0009-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0009-b0003-timing.tsv", sha256 = "de46b42118e3fcaeebbd26d9e831304addcb47db275fb8ce4d3e758d345e6b7d"), (path = "raw/jobs/getdp-f0009-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq/raw/jobs/getdp-f0009-b0003.json", sha256 = "a4085e4ff461ca9070f7aa8541aa3304a6bcda241a20912d177779b36592b9af")], coordinates = ["cable:1:core", "cable:2:core", "cable:3:core"], formulations = @NamedTuple{schema_version::Int64, selections::@NamedTuple{insulation_admittance::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}, semicon_admittance::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}, earth_properties::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}, temperature_dependence::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}}, assumptions::@NamedTuple{impedance::String, admittance::String, earth::String, propagation::String, semicon_domain::String, enclosure::String}, backend::Symbol, requested::NamedTuple, methods::NamedTuple, options::NamedTuple}((4, @NamedTuple{insulation_admittance::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}, semicon_admittance::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}, earth_properties::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}, temperature_dependence::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}}((@NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}((:lossless, NamedTuple(), NamedTuple())), @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}((:lossless, NamedTuple(), NamedTuple())), @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}((:constant, NamedTuple(), NamedTuple())), @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}((:linear, NamedTuple(), NamedTuple())))), (impedance = "Axial current-driven A_z/u_r finite-element equations", admittance = "Scalar electrodynamic Helmholtz equation in surrounding media; equipotential terminals with unit transverse-current excitation; Y = inv(P)", earth = "Horizontal air and one semi-infinite soil; soil constitutive properties evaluated at each frequency", propagation = "Gamma = 0; medium diffusion and displacement retained; independent Z/P blocks in one factorization", semicon_domain = "Passive material region, without electrical terminal ownership", enclosure = "Supported enclosures are represented by their material and terminal domains"), :fem, (insulation_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), semicon_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_properties = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), temperature_dependence = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing)), (insulation_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), semicon_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), earth_properties = (identifier = :constant, parameters = NamedTuple(), options = NamedTuple()), temperature_dependence = (identifier = :linear, parameters = NamedTuple(), options = NamedTuple())), (reduce_bundle = false, kron_reduction = false, ideal_transposition = false, physics = Symbol("quasi-tem")))), fem = (timing = (assembly_seconds = 11.949678421020508, recovered_columns = 0, scope = "accumulated native worker wall time; not elapsed scan time", columns = 27, worker_wall_seconds = 33.182426031999995, factorized_columns = 9, constraint_seconds = 4.229004859924316, solve_seconds = 12.483957052230835, output_seconds = 0.0069580078125, worker_wall_scope = "sum of newly executed process wall durations in this invocation", backend = "getdp", schema = 1, reused = false), run = (state = LineCableModelsGmshExt.completed, run_directory = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-nZpQeq", mesh_source = :generated, mesh_fingerprint = "305cf3373df1e63c4f07609b6c95ee4fdd6a9adf5d9dc8c65cdc652253783728", getdp_invocations = 9, map_paths = String[], completed_columns = 27, completed_frequencies = 9), inputs = (schema_version = 7, solver_protocol = 3, mesh_fingerprint = "cd9d532c437c9947f2ebbd36240699a03da1eb1ee45d1a9f2db689a7e9415f2f", materials = [(kind = :conductor, tag = 10001, mu_r = 0.999994, sigma = [5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7], omega_epsilon = [5.563250277239352e-12, 5.563250277239352e-11, 5.563250277239352e-10, 5.563250277239352e-9, 5.5632502772393515e-8, 5.563250277239353e-7, 5.563250277239352e-6, 5.563250277239352e-5, 0.0005563250277239352])], earth_materials = [(rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0)], air = (eps_r = 1.0, mu_r = 1.0), mesh_plans = LineCableModelsGmshExt.FEMMeshPlan{Float64}[LineCableModelsGmshExt.FEMMeshPlan{Float64}(1, 0.1, 1006.5842420897408, 1258.230302612176, 25.16460605224352, 50.32921210448704, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (25.16460605224352, 25.16460605224352), (1006.5842420897408, 1006.5842420897408)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(2, 1.0, 318.3098861837907, 397.88735772973837, 7.957747154594768, 15.915494309189535, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (7.957747154594768, 7.957747154594768), (318.3098861837907, 318.3098861837907)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(3, 10.0, 100.65842420897408, 125.82303026121761, 2.516460605224352, 5.032921210448704, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (2.516460605224352, 2.516460605224352), (100.65842420897408, 100.65842420897408)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(4, 100.0, 31.83098861837907, 39.78873577297384, 0.7957747154594768, 1.5915494309189535, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (0.7957747154594768, 0.7957747154594768), (31.83098861837907, 31.83098861837907)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(5, 1000.0, 10.06584242089741, 12.582303026121762, 0.282842712474619, 0.565685424949238, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (0.282842712474619, 0.282842712474619), (11.313708498984761, 11.313708498984761)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(6, 10000.0, 5.656854249492381, 7.0710678118654755, 0.282842712474619, 0.565685424949238, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (0.282842712474619, 0.14067442439954772), (11.313708498984761, 9.549296851139358)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(7, 100000.0, 5.656854249492381, 7.0710678118654755, 0.282842712474619, 0.565685424949238, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (0.282842712474619, 0.04448515896357012), (11.313708498984761, 3.0197535662513486)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(8, 1.0e6, 5.656854249492381, 7.0710678118654755, 0.282842712474619, 0.565685424949238, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (0.282842712474619, 0.014067442439845935), (11.313708498984761, 0.9549323148114102)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(9, 1.0e7, 5.656854249492381, 7.0710678118654755, 0.282842712474619, 0.565685424949238, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (0.282842712474619, 0.004448515892915345), (11.313708498984761, 0.3019836725638405))], region_mesh_sizes = [0.0085, 0.0085, 0.0085], cable_outer_mesh_sizes = [0.0085, 0.0085, 0.0085], mesh_growth_factor = 1.2, options = (reduce_bundle = false, kron_reduction = false, ideal_transposition = false, physics = Symbol("quasi-tem")), execution = (ui = false, plot_field_maps = false, mesh_policy = :remesh, mesh_path = nothing, domain_skin_depths = 2.0, keep_run_directory = true, getdp_executable = nothing, gmsh_verbosity = 2, getdp_verbosity = 4, frequency_workers = 2, solver_threads = 4), supplied_mesh = nothing, owned_gmsh = true, getdp_identity = (sha256 = "49d44b1dfe13d9f9b63f8dfe37914af8947f856feebac13fbb6989a41aa88e51", info = "Version : 3.5.0\nLicense : GNU General Public License\nBuild OS : Linux64\nBuild date : 20220513\nBuild host : getdp.info\nBuild options : 64Bit Arpack[contrib] Blas[openblas] Gmsh Kernel Lapack[openblas] PETSc PeWe SLEPc\nPETSc version : 3.14.4 (complex arithmetic)\nGmsh lib version : 4.9.5-git-e286855ce-git-e286855ce (20220218)\nGmsh lib options : 64Bit ANN[contrib] Dlopen Eigen[contrib] LinuxJoystick Parser Plugins Post\nPackaged by : geuzaine\nWeb site : http://getdp.info\nIssue tracker : https://gitlab.onelab.info/getdp/getdp/issues\n"), getdp_provenance = (path = "/home/amartins/.julia/artifacts/51e049a32eeb2ffebcc8a5945bb00f70470a3d29/getdp-3.5.0-Linux64/bin/getdp", source = :artifact, artifact_hash = "51e049a32eeb2ffebcc8a5945bb00f70470a3d29"), gmsh_version = "4.15.0", gmsh_library = "/home/amartins/.julia/artifacts/13aa9c68927e7d05cb18794fbcfe42323c9c0fda/lib/libgmsh.so", julia_version = "1.12.7", adapter_sources = (var"model.jl" = "5dc038f3abd29cd606ab56ecded593b653d3c41cdbfc29e580c189ceeb008584", var"geometry.jl" = "6c6b4236d78945b54c72e7fd34d12a32ccadad75412bbc22ab70ff21df9822f1", var"mesh.jl" = "134502f8ca5ea3243151549b04780259b627adec1682cbfe13b209f9a6993de3", var"onelab.jl" = "baa1af49661cf66d88935d8fb08d7b2a35c7693bebb1fdf540f1463c91dc8164", var"getdp.jl" = "b6a8d3813ce7a200b01cc77428b20dcbb2728f9a4760a1cbde84ef9a698ddd6b", var"voltage_paths.jl" = "81bb6ad0c4078af4f5cce6dcae2bafb6ed60a8374345dfae0b8f61369d61e786", var"workers.jl" = "62ffec093cda6f52d4f08206771dcb82e16e80175ebac6200196c9ed6ffd34ec", var"results.jl" = "bb48b425e16a9048bdf0a4f73c6d7345554d22e396f8d371aa926d4c665a3e4f", var"compute.jl" = "152e200c45a72dab0c2a439900d98a1ec18a6377f2b640ee271850f1ac74b702"), solver_sources = (model = "c0bb424db5ab4db9ca22f549f675edb4646f69910d2aa14e20cf4f40e41bd313", jacobian = "014208d39043ab13183c42c312aba459265715da6d6ddc38ec1fbc39b6090c73", integration = "e113951ac1e6e658f1be50e53875e30c85497445f0e6584a8a7ab1848588e902", materials = "581f089113810a379d8a49c03d79f55045b723c8f4c761d0a493a019ff1f5d9e", quasi_tem = "448d9019ad417c9a784da20170b855dd25e559297499a83d19b007ee095c084d", quasi_full = "1aede35114ea686c5f109089478027d241dec12dc9fd14a24452b348f89b08e4")), terminal_ids = ["cable_0001/three_bare_wires_air_1/core", "cable_0002/three_bare_wires_air_1/core", "cable_0003/three_bare_wires_air_1/core"], reduced_phase_map = [1, 2, 3], inversion_residuals = [6.753223014464259e-16, 4.449557262054371e-16, 2.237726045655905e-16, 3.3766115072321297e-16, 6.206335383118183e-17, 1.1188630228279524e-16, 2.263236652141162e-16, 2.233415718988296e-16, 7.530207661076685e-19], condition_numbers = [2.1180265401158499e12, 2.117965100717674e11, 2.1180329440259346e10, 2.1202277445682497e9, 2.1368020004261553e8, 2.258532323780617e7, 2.9566481165753105e6, 497529.8404592254, 916809.536450053], primitive = (Z_primitive = ComplexF64[3.1562793020459726e-6 + 1.2381622459009902e-6im 9.855850140927821e-8 + 7.118468378655288e-7im 9.855531814166893e-8 + 6.824132509310875e-7im; 9.855850140927819e-8 + 7.118468378655292e-7im 3.1569380449518183e-6 + 1.2377824992095822e-6im 9.8888942097084e-8 + 8.121988799390389e-7im; 9.855531814166893e-8 + 6.824132509310878e-7im 9.888894209708404e-8 + 8.121988799390388e-7im 3.1569378453844727e-6 + 1.2376254103650067e-6im;;; 4.043938099969267e-6 + 1.0950296462592307e-5im 9.853204005105923e-7 + 5.680859726109968e-6im 9.850468242365728e-7 + 5.3864382934087735e-6im; 9.85320400510592e-7 + 5.680859726109968e-6im 4.064526677162325e-6 + 1.093273668005019e-5im 9.95667881296066e-7 + 6.677363213514665e-6im; 9.850468242365726e-7 + 5.386438293408771e-6im 9.956678812960659e-7 + 6.677363213514663e-6im 4.064508501178054e-6 + 1.093114109659993e-5im;;; 1.3456741305710767e-5 + 9.493136905180382e-5im 9.833294417256773e-6 + 4.2419997939162006e-5im 9.816616493019061e-6 + 3.948345666643249e-5im; 9.833294417256775e-6 + 4.241999793916202e-5im 1.4078358508886816e-5 + 9.430088645695462e-5im 1.0151570594638799e-5 + 5.2158599331381674e-5im; 9.816616493019066e-6 + 3.948345666643251e-5im 1.0151570594638785e-5 + 5.215859933138168e-5im 1.407770266779548e-5 + 9.428378799062607e-5im;;; 0.00010105736213043232 + 0.0007940908289541355im 9.683099766377595e-5 + 0.00028061951809663104im 9.594289810496844e-5 + 0.00025153139211222554im; 9.683099766377586e-5 + 0.0002806195180966308im 0.00011826512950095309 + 0.0007739317981559253im 0.00010608353483127337 + 0.000370947418678224im; 9.594289810496847e-5 + 0.0002515313921122254im 0.00010608353483127338 + 0.0003709474186782241im 0.00011825895634178478 + 0.0007739090116662262im;;; 0.0007854553808631802 + 0.006616038298387241im 0.0008792635575077084 + 0.001402374698144822im 0.0008333672394315292 + 0.0011339562556012919im; 0.0008792635575077084 + 0.001402374698144821im 0.0011843876922731453 + 0.006032321696168378im 0.0011007933449973671 + 0.0020953145045025617im; 0.000833367239431528 + 0.0011339562556012916im 0.001100793344997367 + 0.0020953145045025626im 0.0011842919805786728 + 0.00603238184129186im;;; 0.0051086484699835975 + 0.0565062498755011im 0.005255674307523971 + 0.0024792030592478047im 0.004049580209987741 + 0.001147388226955296im; 0.005255674307523974 + 0.0024792030592478034im 0.011119748056498867 + 0.042731634526618816im 0.00802818760136197 + 0.004872250807288172im; 0.004049580209987744 + 0.0011473882269552962im 0.008028187601361968 + 0.0048722508072881745im 0.011119703022718263 + 0.04273394376065648im;;; 0.024745328786787053 + 0.5119076499306712im 0.0029228998850158476 - 0.005535184550016084im 0.0008725716638503331 - 0.0031045060939019955im; 0.0029228998850158493 - 0.0055351845500160824im 0.09148878592132956 + 0.2837112577188529im 0.008732364423148255 - 0.008720517818696474im; 0.0008725716638503327 - 0.003104506093901996im 0.008732364423148257 - 0.008720517818696483im 0.09149304357097982 + 0.28369441705329945im;;; 0.0931800865836167 + 4.916402722810203im 0.00018018320619215134 + 0.00022597300058552257im 8.190655054892649e-5 + 8.945081031833192e-5im; 0.00018018320619215113 + 0.00022597300058552262im 0.7537795205184291 + 1.568277138147143im 0.00015808159292494346 + 0.001064814054945651im; 8.190655054892647e-5 + 8.945081031833193e-5im 0.00015808159292494367 + 0.0010648140549456518im 0.7536951554113501 + 1.5684962544665286im;;; 0.29944048111231336 + 48.17220125150122im 3.6173164098261593e-9 - 8.448895566733486e-10im 1.7322626704311287e-9 - 2.869969988802606e-10im; 3.6173164098261606e-9 - 8.448895566733473e-10im 4.54999697078992 + 6.660624014260017im 1.6961332535262068e-8 + 2.332144192261467e-8im; 1.7322626704311266e-9 - 2.86996998880261e-10im 1.6961332535262058e-8 + 2.332144192261462e-8im 4.550168033355605 + 6.660469751826508im], P_primitive = ComplexF64[0.1680175930391486 - 1.0948661630298163e11im 0.16442574824611095 - 0.024920229748941015im 0.15699683790992067 - 0.024919276508712913im; 0.16442574824611086 - 0.024920229748941033im 0.22881702614397534 - 0.024982628274228123im 0.1771204137031789 - 0.024982238343131527im; 0.15699683790992072 - 0.024919276508712947im 0.17712041370317919 - 0.024982238343131527im 0.22880930122092807 - 0.024982632131950965im;;; 0.1318804202782531 - 1.0948398423675055e10im 0.12815842511366854 - 0.024782531658477998im 0.12072685637845089 - 0.024775104677860597im; 0.12815842511366857 - 0.02478253165847802im 0.1924171490129812 - 0.024977045059590572im 0.14071841169817423 - 0.024973839172452238im; 0.12072685637845085 - 0.02477510467786062im 0.14071841169817426 - 0.024973839172452206im 0.192405687901686 - 0.02497705496252662im;;; 0.0962877889539774 - 1.0947969311803677e9im 0.09215431192433335 - 0.024341119390589135im 0.08473598176877457 - 0.024287859035494686im; 0.09215431192433332 - 0.024341119390589132im 0.15598766540914996 - 0.02493286386323613im 0.10429324511270961 - 0.024907878879295027im; 0.08473598176877457 - 0.024287859035494703im 0.10429324511270957 - 0.024907878879295024im 0.1559774628641269 - 0.024932932866836437im;;; 0.062378319515393084 - 1.0950200278901191e8im 0.05707900373123456 - 0.022930808586504318im 0.049774211739263684 - 0.02260097929560259im; 0.05707900373123458 - 0.02293080858650432im 0.11959444967280723 - 0.024619065562016006im 0.06795161656670805 - 0.024439677173393265im; 0.04977421173926372 - 0.022600979295602595im 0.06795161656670805 - 0.02443967717339326im 0.1196008414634171 - 0.024618740230862145im;;; 0.0330244597933091 - 1.0954030181866178e7im 0.025143404144695205 - 0.018689830144358557im 0.018730390630187457 - 0.01720544155223875im; 0.025143404144695205 - 0.018689830144358568im 0.08393220451363259 - 0.02279637904225846im 0.03268159273308996 - 0.021706501396786287im; 0.018730390630187468 - 0.017205441552238746im 0.03268159273308993 - 0.021706501396786287im 0.08393514542713619 - 0.02279599372224402im;;; 0.013010429128053188 - 1.0954170652031903e6im 0.003126059327468781 - 0.009028526977206337im 0.0006987888213880585 - 0.006126822273011336im; 0.003126059327468781 - 0.009028526977206335im 0.053638400908464286 - 0.01636362895268574im 0.0053578570105692535 - 0.011758179673846254im; 0.0006987888213880585 - 0.0061268222730113345im 0.005357857010569254 - 0.01175817967384626im 0.053641473655739215 - 0.016364638243162144im;;; 0.004099115765188747 - 109543.61573781159im -0.0008022006202142063 - 0.0003335507654334338im -0.00036079430140275687 - 7.691920445903226e-5im; -0.0008022006202142057 - 0.0003335507654334332im 0.035920958219756084 - 0.011515308573420006im -0.0010780604837334418 - 0.0010840652212131983im; -0.00036079430140275665 - 7.691920445903224e-5im -0.0010780604837334416 - 0.0010840652212131985im 0.03591867921966004 - 0.011515727720219861im;;; 0.0012748909826077073 - 10958.30370856994im 2.8155324493205307e-6 - 2.365389712478281e-6im 1.1005775707026617e-6 - 9.50157171969026e-7im; 2.8155324493205324e-6 - 2.3653897124782807e-6im 0.019862413675948303 - 0.009546421604118371im 1.3483287798675836e-5 - 2.001575780756868e-6im; 1.1005775707026624e-6 - 9.501571719690267e-7im 1.3483287798675829e-5 - 2.0015757807568653e-6im 0.019865188883086905 - 0.009545353172041759im;;; 1.774105548215399 + 9366.221015641253im 3.981538174174671e-11 + 1.7462976538402714e-10im 3.999706435547946e-11 + 2.522759504686304e-10im; 3.9815381741746705e-11 + 1.7462976538402703e-10im 0.008435458255035772 - 0.005763038258336218im 2.9535755796069446e-11 - 2.1483426464418997e-11im; 3.999706435547932e-11 + 2.522759504686297e-10im 2.953575579606942e-11 - 2.148342646441899e-11im 0.008435262839600152 - 0.005763255425469768im], phase_map = [1, 2, 3]))) \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/fem/fem-artifacts.toml b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/fem/fem-artifacts.toml new file mode 100644 index 000000000..e988313fe --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/fem/fem-artifacts.toml @@ -0,0 +1,296 @@ +"attempts/f0001-UhE6WA/attempt.json" = "0539816db924003ceddf786c52dd6ce8974769c6adcbf82d98d64c3b4ed9de01" +"attempts/f0001-UhE6WA/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0001-UhE6WA/getdp.log" = "6d40b9add4d28c3c735479948a2e831c3a8a821240555fda78fb40ef4dc50bdb" +"attempts/f0001-UhE6WA/petsc.log" = "170493fdc27bc5755f45e31db387180b5bf9a9c723a167c36bf47cf4dfa4026b" +"attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0001-P.tsv" = "c2aa0854dfcfaf5da0928ca17d1d6432fa66daba938130b06d348e855abfdd29" +"attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0001-Z.tsv" = "b0919d31bdb401c244e9bb43fe29d64bb917454863de8d631c530f40b719617f" +"attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0001-timing.tsv" = "4baba3c8f2dcb03e554cfbdf277fe7856ea0f7b2eb10a63f41970e261b293594" +"attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0001.done" = "a40a11bab26b32dfb169550347f289dc710df24f59fe3ef20963d46f838f0586" +"attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0002-P.tsv" = "b846b53e06337704ed9d5f7245d79d192c944b7c0dbeb1ec369e75a382efa4da" +"attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0002-Z.tsv" = "fe4b1a4e25f68e63af0d8e923b5a748c1b4f9d22b6cf1f4195d263078898e641" +"attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0002-timing.tsv" = "a8d5d9d2473c0b7d3a796119af263ddb425bf8ea95c0fedf93e005da28f89500" +"attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0002.done" = "f58042f034284ccf2955f2795037937a5bd0e4288ffc0d0d5124e33ee07f17ec" +"attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0003-P.tsv" = "66b97dd74250b87e97dd64c87741ac04d70f968dc8003a5ed03e9dd3c904d47c" +"attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0003-Z.tsv" = "bb249f951e08313fdc9f40bfc6b9219967308bb934553bedbdb3e15c3f729c37" +"attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0003-timing.tsv" = "c28a02f5ec153f79e8b47f34391a6c1da8f24f17cc1f01b6a31ea7641ba9d97d" +"attempts/f0001-UhE6WA/raw/jobs/getdp-f0001-b0003.done" = "41695cf0039479bd9b703276e3c15366a77a1e43f8490b48634d8a1c2a61be94" +"attempts/f0001-UhE6WA/solver.pre" = "91f7cc28ea32c5fc51829d9a7cce1483208cc67a3bad5fe95c41ef0ddeb1eca7" +"attempts/f0002-IEMEPP/attempt.json" = "79567044d2cfc38dd38546c9ce3c35b8422e1b6a15789ea608c13a452ced0670" +"attempts/f0002-IEMEPP/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0002-IEMEPP/getdp.log" = "2662b1546dd170b6af7076be323e65bb4d43e0b0ce8e1728105a45b5e4d63e4d" +"attempts/f0002-IEMEPP/petsc.log" = "9cb8f0bff35d21a6f96b8d57ef5cb4b9f2efd070845c1cbab5f4a16c8c1ee133" +"attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0001-P.tsv" = "eac818132cc104f8d58d3b541111d4c8ff8addc0f6f0a0f6022f551132f5f2b1" +"attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0001-Z.tsv" = "81581cc6460a1377c8b7f9f34d45b0a17ed3186af459df4ca095b1c8a394cfff" +"attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0001-timing.tsv" = "85c7f9f1493fcd5def5bb6c85e668016001a1124187662de5df1d91ea915d8dd" +"attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0001.done" = "3dbd9539f415ee889a34b9c6efe6b9cb8168b7c0945d52d7e63e920c8990d2d1" +"attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0002-P.tsv" = "e89f568b14a42d0c7b1fb1056a46245347152d70abb521c72aa32532b692d653" +"attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0002-Z.tsv" = "8c941f48178e9aff0c6d1769a2692e1b3c392313ccf25ce9e775798f3446183c" +"attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0002-timing.tsv" = "8f5be96c13cb8f693471dae42daaa2ed59b30ae1246f77eea2d334c9946111fc" +"attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0002.done" = "d498381e814531161686031fcde5aa549b456c6cbee61f6eb67916650247e717" +"attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0003-P.tsv" = "3346ebbf3cdf16d297c9150e10f08f76d8cfacfae8da36db72737021375afb8b" +"attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0003-Z.tsv" = "500da276e14936892aeb21cf1fbc336526908a0ca60c9f5600d868b50ce3a2a0" +"attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0003-timing.tsv" = "6463220f1dd45b73a9fb664890a08c95267f98adc8db8d8e831b635c7f905855" +"attempts/f0002-IEMEPP/raw/jobs/getdp-f0002-b0003.done" = "fa5c0c361441906d1aaa6b254ae52969806be93c36846f4c70c70a18dc833050" +"attempts/f0002-IEMEPP/solver.pre" = "fd41ac3534b89222c29ff5e49f259898ee0aa2e26f041d65ca030364cc77746c" +"attempts/f0003-VT8lki/attempt.json" = "5a95bc23f8dd9fa191e7e9ef22702b383a5bea4e7cab91d24acde4504ec058ca" +"attempts/f0003-VT8lki/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0003-VT8lki/getdp.log" = "05f764893ee5c2eb44a9992729413b0c115d602c7e5a74b69e7ef62d1146e81d" +"attempts/f0003-VT8lki/petsc.log" = "3c4fb0f91e9830ca8e2f5fdda34669feac4f96a87cd61d0ebacc329cdf417f73" +"attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0001-P.tsv" = "a743c86ce34f4e6c6e31329fef90292a2b9e264e6506bc9ce3eb08b27935399c" +"attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0001-Z.tsv" = "03cf0e3cee60dced6152c5b370cc947c31fabd9c7911a865ad0cce06f7f2f9cd" +"attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0001-timing.tsv" = "57da1d496e7a90c54f4039b30129de4539532e36f0da019a19164d0312a3b57b" +"attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0001.done" = "02395cb824ac1c8ce1d2e8cd3c57fd19d5c904215aef49b1fba677c51e99d519" +"attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0002-P.tsv" = "89ff1eb9b80a37ed050df698a08c1e3626bc4d8e0558aa57ef82434b8067d444" +"attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0002-Z.tsv" = "ae83d620d67c83530d0a86f3d74383230d475f2b3e9429cb279a6d9b7f65c02c" +"attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0002-timing.tsv" = "21b7590ee76cc557460b586b8922419c2fdadcbf85b8c81296a4807fc3dcadff" +"attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0002.done" = "84034583a44a97df0ec5820a76fc891b95a9b6bc0bf693bed9882420a3eb5b05" +"attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0003-P.tsv" = "55d90d455b3a02541f46a5169151dab2a31d812c371693a9f67f6f91fc3a9e47" +"attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0003-Z.tsv" = "0b628fe51bc41b6a4a787526dd4113c7289e964fae96645e5a2e296689aa270e" +"attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0003-timing.tsv" = "dbd214bd11f8c52edef5f63512faa4a8fa27744f6f1bef9c60cf6e87d18c8ff0" +"attempts/f0003-VT8lki/raw/jobs/getdp-f0003-b0003.done" = "aec9dd1bab4102d3c07f9c6abf83e4d604721961d8e37cbbc9620f7f71cafcae" +"attempts/f0003-VT8lki/solver.pre" = "5f4af8aaccaa0a92d211ca49eb2521168ed323c70d085f88e0feb85df146a89e" +"attempts/f0004-Gb4HeY/attempt.json" = "61d5bb4ac22d747404c8ab8b64d1c242ab9ed015e845c9e858d84fc26e136425" +"attempts/f0004-Gb4HeY/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0004-Gb4HeY/getdp.log" = "c06abc2a98bde093703121b8503c324fcf5295fb206776b342c2994471fb5b50" +"attempts/f0004-Gb4HeY/petsc.log" = "59f15ea42eaf567e59bcea28d8aefc2a0e6fb583871c889fc7cefd297a29da8e" +"attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0001-P.tsv" = "1bd299e45b4efb8e27bad5cead17a591b8ef4b311dc9a9c9c00fc8a83b275a86" +"attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0001-Z.tsv" = "bc963a0c03ed15ca0a5a6c60767671a74fc901951a8181619379d456dcf480ed" +"attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0001-timing.tsv" = "7aeab688b092d4315e226b96cb394b3c04ff0bf3ef3f79df2b1d455d0c2b8026" +"attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0001.done" = "05b52afc2d2a0ffefd0bb6e40585165d1b2572547d59d3c360b4163202709006" +"attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0002-P.tsv" = "863f98fb7c167bb29dcdb14f9306f9466b3b838c6636e2d317bd279a6b03c81d" +"attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0002-Z.tsv" = "f7bcf9bf8ee99cb5ef9e0c5660e5a29f693dc1eba181cccd96e2915d8cc11f7c" +"attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0002-timing.tsv" = "2353c82478acc4954d2892f2fdbefbe3d3e6307df7785ab2b36de8f68104a3fe" +"attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0002.done" = "35d216e20aed80a36327575b350d8e4c12b70f2038f5d07d55614c92afdd6b6b" +"attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0003-P.tsv" = "07e28b67275f88973e853fc47601af1ec1c860b1feca71ed1d83ee7bdf5da110" +"attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0003-Z.tsv" = "7af31afdc071841eb7232f791af1c7e72ac2ec530e9ea60845baa6ded1dec20e" +"attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0003-timing.tsv" = "b1287a560de503b9528805d417657b6249350df2348442071086dd8b23011b27" +"attempts/f0004-Gb4HeY/raw/jobs/getdp-f0004-b0003.done" = "eae2a23f21e24dd158a817c096e6e77f32e1d2a850c4163d279c3531eef2af5a" +"attempts/f0004-Gb4HeY/solver.pre" = "06fc41e4f32bcc98f94f523eb25b26f694bc9aae65fe034817e2718eb3f374b2" +"attempts/f0005-zQphNs/attempt.json" = "0fcc7caee92c1951211baac63272f3003b9cdf50104211e49c592989da02124e" +"attempts/f0005-zQphNs/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0005-zQphNs/getdp.log" = "87011105acb3ad5b56ec94d908fbedd9edb09d70c990f7346b2ab132b9b6eb71" +"attempts/f0005-zQphNs/petsc.log" = "0602e9a89ad4ce1a36a0962fec93fa5b3155b7a8bcf42fc9495dbdbf350479ec" +"attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0001-P.tsv" = "3398b57ec64eb6fae19ee1356c7dab47aa48dbbebe492f35703519b4ef4b0f6f" +"attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0001-Z.tsv" = "e685c4abcc4b447849682b072c360d6e77374002167192f4dc8ef9339e0d0e0a" +"attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0001-timing.tsv" = "1fc6e8e52ccc711ab9eb43002d21a40a8b2c971984450b1fc364a205a26dcc3f" +"attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0001.done" = "ee7bf9802d600055d484ec4e501666418037b219c7bf910eefda35e16a843b08" +"attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0002-P.tsv" = "908051f6c7688c8d2123e11334ab950e800b12fc08832372776a36ba8b1698a1" +"attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0002-Z.tsv" = "5141eee98ece2f2b13dab47e1358a70589cc4e09facf02d1c6c63d18505ae832" +"attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0002-timing.tsv" = "41edcadb637cac180a7728d032312db1c26a7787b7cfaf7aecc41afb3c870891" +"attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0002.done" = "e26c3631aaa36c47efb3ede81242002dbebf27057f428fe57f60849cede6b3f3" +"attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0003-P.tsv" = "09d4c5a0830a8ca5769c143d6b32d662386bee0ed11a353cfb9a4208e06bf61f" +"attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0003-Z.tsv" = "7dd4aa242c093f26730ae7e97e16754be0306aa86fcb5cde1e220119dec6c786" +"attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0003-timing.tsv" = "3e9eba1b4d0a4387f829fafdd33e7abb5bcd2fa1f572d0313c3d7ae5ee4b2c2b" +"attempts/f0005-zQphNs/raw/jobs/getdp-f0005-b0003.done" = "a91d7ca1bb120ef43c71df371c45a71b1cc7744f1ee2eb082338700de7f329c9" +"attempts/f0005-zQphNs/solver.pre" = "28357a205c714f2107f5c19e6a5f52426f6bd09e74edcc5ae049eb3ec435e046" +"attempts/f0006-7pB1cx/attempt.json" = "32a09038058481a22cf0860a407753fdbcbf2b188d7e68da76779da743764e7b" +"attempts/f0006-7pB1cx/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0006-7pB1cx/getdp.log" = "16ec0b7375d4fe1d2761abb61432aa0bb894453ca121b973b955642d1fbe57b2" +"attempts/f0006-7pB1cx/petsc.log" = "2d7229923894bed921eb3b6d9cabd14ae26a9b20d398d448e6f008abc560b1a2" +"attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0001-P.tsv" = "865759434f809d392a1595c73b3f58ffb9d97e9242aea6c5f1d84a4e992bfa92" +"attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0001-Z.tsv" = "89ae231d90380bdff7e76123b3372118793a506645ce6d1a3a75ce0b4bcf3a64" +"attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0001-timing.tsv" = "b90f7c2e5e9561e364e7ad705e3e59f41a37cc9c19431ba12b5e592edd17f56e" +"attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0001.done" = "e41a78288341a2ae156cd28b6102b3a31a74b012eb4deb233e88fae392912e8e" +"attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0002-P.tsv" = "b9b04974edbcddcdb59dcdfb7299831919debb827ea9e02dda1ab5e93761b88e" +"attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0002-Z.tsv" = "7ebed250deaa9ee9de867d79edc82254a76e841d7740b664f84644aae8a34a45" +"attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0002-timing.tsv" = "700dd33f153d388e42fedcb3c62dd689eecb750008f39d542bdeb45cba720d3c" +"attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0002.done" = "c105367e4bac19858725b1b7c56efbb25ae370173d002f9a678a4762a5c69ac9" +"attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0003-P.tsv" = "5941985e00927cd56caf2e9b9cff07af500fa171eb0d1f06523a3d1d21918b61" +"attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0003-Z.tsv" = "89504b5b4da4cb1d6003d262377da8b171ba95ad63a3877be4623e96e2b7b5dd" +"attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0003-timing.tsv" = "9e2085f8b2425afbf99d2f9462e9b3eb5434ae20965430b38aa2537e9f8b619b" +"attempts/f0006-7pB1cx/raw/jobs/getdp-f0006-b0003.done" = "de6214d7f2cf6d2097d94d720289a7424da0b9bf7a5df27326d8b69e936798e8" +"attempts/f0006-7pB1cx/solver.pre" = "aaeea057a70b35919f9de8d9e08f285053383cb1e7203b239a5a78f6524e85b1" +"attempts/f0007-eCJ3wP/attempt.json" = "35f3b0bfd739bd41126562f02d7650dee68b7fff68fc1cc0432b574dc94ef2c6" +"attempts/f0007-eCJ3wP/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0007-eCJ3wP/getdp.log" = "a8bec326f1bf46c9e4573c903dc9bf3d5d601e880ecfea7cfd0210ce3e1ae4f7" +"attempts/f0007-eCJ3wP/petsc.log" = "3108117ad5d5cc6031983d2fd9657ea288dfd4104f32fad34f632f1883b71828" +"attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0001-P.tsv" = "4f21d6c2a0089b426f25fd933153dca0434e8c2cfa19fea8b8885f106b24442b" +"attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0001-Z.tsv" = "2f401e56c1f6be6f4e3de378ce697cd224faa80804d2dbe891be6bfa8263ca25" +"attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0001-timing.tsv" = "019a7f6d8d008a4fb431697deb309c64b7989ee49f43f2fc26cf0b05df41b781" +"attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0001.done" = "d95e4d651bb7fa68e36e0077af5a10e10baacf852173da7ac0f1cca3e414dc7f" +"attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0002-P.tsv" = "3247adec4590aa01d86bfda50e7414c2d37f33eab782330f9209b01e891bde30" +"attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0002-Z.tsv" = "61ee9cd65d8aa189d0fe5ca7d73a553cea0fb351a2cb23a9950c14ffc1bed663" +"attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0002-timing.tsv" = "308e5ffce6dc41a9ac1b5ed885777ed7f127f6bab880161971d77c55840d80c5" +"attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0002.done" = "a953202777b2aea0282f4e5ecac9feb227f850cb3f80f2247ca07a708f3635be" +"attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0003-P.tsv" = "a7fba6388c8dabacc4ebcd345fc59faf8dc00ddb6bc96ce1ec19232bd1e86ac0" +"attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0003-Z.tsv" = "a1baca9f06df00bff1eebc6a31412d0a38d1fdb5dd303c0c62970ca7bb2278af" +"attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0003-timing.tsv" = "b77562ae48824f289d666d6cff9974dc0b30808f1169ef74541eac5cd77ef858" +"attempts/f0007-eCJ3wP/raw/jobs/getdp-f0007-b0003.done" = "bc6e3e671a966700354c8fb7f9ab0ff830db8a4b88332e0ec4974ce29df37b0c" +"attempts/f0007-eCJ3wP/solver.pre" = "98fbe22e311432a5cf63e2990866c6845417a5b867e04e25976ee643e8c2b508" +"attempts/f0008-PNwfpm/attempt.json" = "078d355a5d6821f1b6057d34efa4870fb1d617a9d504277885d02dc3432b2147" +"attempts/f0008-PNwfpm/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0008-PNwfpm/getdp.log" = "3ca388bad2af86eb35af7ce40e078621e1e028aa37a72b799afd81317f2dace4" +"attempts/f0008-PNwfpm/petsc.log" = "ab5ba703133433f0fd4686077e7899c52b3ed3ff24a52d15658ca68865c42b6f" +"attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0001-P.tsv" = "7ec170463ed10318b84d75dbf349106889f6203587627107fa68b08e72875ef4" +"attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0001-Z.tsv" = "34e4772ed0d223b1897452b90094fa71d30f065b0f906841320f216b23e66095" +"attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0001-timing.tsv" = "55f45e179a33d9447288d277b1caaf8e3c8809e72bcbe3d28887ead084fc4d9a" +"attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0001.done" = "c03ca6db0131d3218478fb4e85f0350dca26787b9ffdfc9d50b5d68ad93b352c" +"attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0002-P.tsv" = "77b8d6a0a84bc66c1ae733c9b191b32836ffefc9d03e14020812c25ffa5e7003" +"attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0002-Z.tsv" = "7413fa7f6e036b018ef3683b03ff41f458833daf1faa94a82ae1f0548a45c0a8" +"attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0002-timing.tsv" = "5ef5a19f429019393e2740709599ed76a9f3ba045ff0d28f1442f338bcbb11d0" +"attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0002.done" = "76f50b528daca79ab1eac47c47f96ca1e7b8ad94bcaefbaac04335941306f993" +"attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0003-P.tsv" = "7238161273ea9daf4c5754d4f07278913fad3d1af45309be865b041e895162c1" +"attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0003-Z.tsv" = "b5c816019ac161e29efcf04512ee7eac006b7128fe57c2ac1fa69060e9aad2c8" +"attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0003-timing.tsv" = "0180204ba44884eef9956d7b4fdaf4c654ef35cd9416fc4b5e8af45b16c487e0" +"attempts/f0008-PNwfpm/raw/jobs/getdp-f0008-b0003.done" = "20bdbe7d9351850f55ebf49a31f3a93235d04aea44253ddd2d75828a83fa1c74" +"attempts/f0008-PNwfpm/solver.pre" = "49d13134d2183a62ff2dd6ea88dbd76d815995fb64f3fdc2bc1d6d78b67bcfb8" +"attempts/f0009-LxLb8P/attempt.json" = "c1a6799729913375fbb5d4bc6a7ca15e1d93d7ddfdbc28f2e9130e7e9ac38746" +"attempts/f0009-LxLb8P/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0009-LxLb8P/getdp.log" = "13cadeb7cfec42460d4f8e643ffa24e671708c0048c9d56b626184ec3f8120ba" +"attempts/f0009-LxLb8P/petsc.log" = "6b4967511e4cc67af51510a4e19fb4c174d1648f0c011bb21d43c5de2c5e7912" +"attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0001-P.tsv" = "ca8bf539a2aa791698165bb81d085f5acbb189b3b62d246ba937dc92ce7353f7" +"attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0001-Z.tsv" = "e474b796474a4166bc2883c204746842000b1b101b09acf18c4419197c451a47" +"attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0001-timing.tsv" = "32ef8681dc4f1d2fe0bfbdccc584179f43ed906d8567e350078c46246e98cfec" +"attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0001.done" = "22b8b831af1067ac03eaf9570ddd71235af29f6b65e559199200376f6ef90f2a" +"attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0002-P.tsv" = "9792cefef8b214884d006bafa09587a935868ec363641eec307a112df4090d34" +"attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0002-Z.tsv" = "2ac4420529eb51e337786056ac19b4f6b190e515cc8194d4a6dc6eb84241cd41" +"attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0002-timing.tsv" = "77a626f5fa0b16f4b318ee4e5218e69642558e9f4578f95e2817835cbff61abf" +"attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0002.done" = "2ab6b278377a8a5090d5d456a9193028844db99b8f3fa96c2433d70920e73c22" +"attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0003-P.tsv" = "baeca8aeb00e5b88a304688c8e3138f47a07a19b67685f5f0163d88e8fac0c42" +"attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0003-Z.tsv" = "e40082bcb62bad066853868f3ad9406ed0b293ad12b07856df4861040df35107" +"attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0003-timing.tsv" = "de46b42118e3fcaeebbd26d9e831304addcb47db275fb8ce4d3e758d345e6b7d" +"attempts/f0009-LxLb8P/raw/jobs/getdp-f0009-b0003.done" = "3449a1ca026b39cca413695b5bd20e176addf6bcf3103f9147e8ac56407cafa3" +"attempts/f0009-LxLb8P/solver.pre" = "9469af55317a8fa5a727ad7d47b0d90e1abb2004e6b52de8177a88668578aec8" +"coordinator.lock" = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" +"input/computation.json" = "98dd49d4fab5ebaa2cf0347ad83da5460980f9a78b7ed918aa828a7303d5d9ea" +"input/getdp/integration.pro" = "e113951ac1e6e658f1be50e53875e30c85497445f0e6584a8a7ab1848588e902" +"input/getdp/jacobian.pro" = "014208d39043ab13183c42c312aba459265715da6d6ddc38ec1fbc39b6090c73" +"input/getdp/materials.pro" = "581f089113810a379d8a49c03d79f55045b723c8f4c761d0a493a019ff1f5d9e" +"input/getdp/model.pro" = "c0bb424db5ab4db9ca22f549f675edb4646f69910d2aa14e20cf4f40e41bd313" +"input/getdp/quasi-full.pro" = "1aede35114ea686c5f109089478027d241dec12dc9fd14a24452b348f89b08e4" +"input/getdp/quasi-tem.pro" = "448d9019ad417c9a784da20170b855dd25e559297499a83d19b007ee095c084d" +"input/model_data.pro" = "d38113a1162b9d199ae30895953776d5e0c71e5de4b70ba24789c48df687a09e" +"input/problem.json" = "0d236876854a892fd2068076209dd8cec97cacf6920c7fbd62e1b55852b9f052" +"logs/getdp.log" = "4efaf0e96254e59de97305f31f145e7015cf96f1b73f1f88d1219bf963129113" +"mesh/frequency_0001.json" = "fd9bf880bbf984ecf0d22113f436522d849bc65471655dfe09c842a3ebb91f01" +"mesh/frequency_0001.msh" = "30808a238d48b33964a2b6df37f63f4fe84402cb6576153b6033972e159f4623" +"mesh/frequency_0002.json" = "ab0f1dc27cfe09cdf7dadd4e43964b1195d25f6dd36994931fe9fa1833a07d6b" +"mesh/frequency_0002.msh" = "cef5bfc15ed4d508849804258954130cc4800cce559dc0908b8c550ddad1f1f0" +"mesh/frequency_0003.json" = "3587f476377b846bdfd55affe74b8e8790bf707a33105f30c4ca350422d4fece" +"mesh/frequency_0003.msh" = "a045634bc341469391173419f8bf11beaa4da0e8dd467f8765b79446b7bc963c" +"mesh/frequency_0004.json" = "4340cac5bcf001e3d653ea848d77b31d613d6091af9e31242bd0f6d791fe6979" +"mesh/frequency_0004.msh" = "3854ff79dd225253f26590eb542b65b3e45d1986ed407521fd1dfc00497c1ff2" +"mesh/frequency_0005.json" = "5bd0cb902f9b56efa112fb325a3b34014a243acf0012f58aecf20411c0ec0387" +"mesh/frequency_0005.msh" = "5f50316c4720fbe239e7c02cd5f1871f90f15341ab3bfb272ed24cf153fc979f" +"mesh/frequency_0006.json" = "bccaaa578168c9796004cfd3ccc96abde23d68adbf67c789e0ac664bd5405afc" +"mesh/frequency_0006.msh" = "b4a18b97046fee852e8a83f5c04841a61e894860c5e6f163187c98944bdcaef7" +"mesh/frequency_0007.json" = "12634fd99bbcf70243eae804880443312e0ac330b39cb48dc3eab45d615a1efb" +"mesh/frequency_0007.msh" = "149eb973c09b9be9492291ac1bb45147a4860198b87c7d46d98135bf513cefda" +"mesh/frequency_0008.json" = "4f1865f48f276e337f2669f3b5334b1f50c8731641af2bfd1607892ac62cfc3b" +"mesh/frequency_0008.msh" = "0d8146664febdd2bb4f0dae6f0058fff3141a7c812f1daa11de77856f1bd8aab" +"mesh/model.json" = "0c2709eda9d3a07914ac97418cd68558262dca5df46ab0266ee32c09d29ae619" +"mesh/model.msh" = "9e1a15c73ea3c5b3f4238b57b71348061afa00aa85380569ee663a5899906737" +"raw/P.tsv" = "ff5c34e9d74d65fd75b4832a7a71ea0bf4f17c3bccf816d7fc5da0eee68ca721" +"raw/Z.tsv" = "1f55f46e944065e22c24757b863d710071f47e4072484e6fd1b47bb2121de820" +"raw/checksums.json" = "7120e2db42512d47de45db0731d31da6295279cb4618aa2cb180919f530ffba4" +"raw/jobs/getdp-f0001-b0001-P.tsv" = "c2aa0854dfcfaf5da0928ca17d1d6432fa66daba938130b06d348e855abfdd29" +"raw/jobs/getdp-f0001-b0001-Z.tsv" = "b0919d31bdb401c244e9bb43fe29d64bb917454863de8d631c530f40b719617f" +"raw/jobs/getdp-f0001-b0001-timing.tsv" = "4baba3c8f2dcb03e554cfbdf277fe7856ea0f7b2eb10a63f41970e261b293594" +"raw/jobs/getdp-f0001-b0001.json" = "3857cd575f8f6e4cbc6565951580b632e940f72b3146ea376728288cdbddf84a" +"raw/jobs/getdp-f0001-b0002-P.tsv" = "b846b53e06337704ed9d5f7245d79d192c944b7c0dbeb1ec369e75a382efa4da" +"raw/jobs/getdp-f0001-b0002-Z.tsv" = "fe4b1a4e25f68e63af0d8e923b5a748c1b4f9d22b6cf1f4195d263078898e641" +"raw/jobs/getdp-f0001-b0002-timing.tsv" = "a8d5d9d2473c0b7d3a796119af263ddb425bf8ea95c0fedf93e005da28f89500" +"raw/jobs/getdp-f0001-b0002.json" = "e5318cbffdc5e87cc4654b4f877f9f2939c0a0c80b5dfdaf76e8cd36775766fa" +"raw/jobs/getdp-f0001-b0003-P.tsv" = "66b97dd74250b87e97dd64c87741ac04d70f968dc8003a5ed03e9dd3c904d47c" +"raw/jobs/getdp-f0001-b0003-Z.tsv" = "bb249f951e08313fdc9f40bfc6b9219967308bb934553bedbdb3e15c3f729c37" +"raw/jobs/getdp-f0001-b0003-timing.tsv" = "c28a02f5ec153f79e8b47f34391a6c1da8f24f17cc1f01b6a31ea7641ba9d97d" +"raw/jobs/getdp-f0001-b0003.json" = "d5dc4f9b512885ae10aab7a8891e6ec04aa42a07ef3313fb422978991040cb20" +"raw/jobs/getdp-f0002-b0001-P.tsv" = "eac818132cc104f8d58d3b541111d4c8ff8addc0f6f0a0f6022f551132f5f2b1" +"raw/jobs/getdp-f0002-b0001-Z.tsv" = "81581cc6460a1377c8b7f9f34d45b0a17ed3186af459df4ca095b1c8a394cfff" +"raw/jobs/getdp-f0002-b0001-timing.tsv" = "85c7f9f1493fcd5def5bb6c85e668016001a1124187662de5df1d91ea915d8dd" +"raw/jobs/getdp-f0002-b0001.json" = "4494a7fecd50484db524c0899c66a2f1e9c5502dd9e9f077205dc149c665d7f1" +"raw/jobs/getdp-f0002-b0002-P.tsv" = "e89f568b14a42d0c7b1fb1056a46245347152d70abb521c72aa32532b692d653" +"raw/jobs/getdp-f0002-b0002-Z.tsv" = "8c941f48178e9aff0c6d1769a2692e1b3c392313ccf25ce9e775798f3446183c" +"raw/jobs/getdp-f0002-b0002-timing.tsv" = "8f5be96c13cb8f693471dae42daaa2ed59b30ae1246f77eea2d334c9946111fc" +"raw/jobs/getdp-f0002-b0002.json" = "effd22fe8752d78ced2dd70a43e73d5aace3bb790d23a7319aa9c658ca6d156d" +"raw/jobs/getdp-f0002-b0003-P.tsv" = "3346ebbf3cdf16d297c9150e10f08f76d8cfacfae8da36db72737021375afb8b" +"raw/jobs/getdp-f0002-b0003-Z.tsv" = "500da276e14936892aeb21cf1fbc336526908a0ca60c9f5600d868b50ce3a2a0" +"raw/jobs/getdp-f0002-b0003-timing.tsv" = "6463220f1dd45b73a9fb664890a08c95267f98adc8db8d8e831b635c7f905855" +"raw/jobs/getdp-f0002-b0003.json" = "9985fcbb546761fc6e0950b4b2dfec4cf3a57ca776034dc6a95ea146781896c1" +"raw/jobs/getdp-f0003-b0001-P.tsv" = "a743c86ce34f4e6c6e31329fef90292a2b9e264e6506bc9ce3eb08b27935399c" +"raw/jobs/getdp-f0003-b0001-Z.tsv" = "03cf0e3cee60dced6152c5b370cc947c31fabd9c7911a865ad0cce06f7f2f9cd" +"raw/jobs/getdp-f0003-b0001-timing.tsv" = "57da1d496e7a90c54f4039b30129de4539532e36f0da019a19164d0312a3b57b" +"raw/jobs/getdp-f0003-b0001.json" = "4d77ed245a8282f169a98d7ec27fbfa7b8b8102d5217e116c1620503545bab2c" +"raw/jobs/getdp-f0003-b0002-P.tsv" = "89ff1eb9b80a37ed050df698a08c1e3626bc4d8e0558aa57ef82434b8067d444" +"raw/jobs/getdp-f0003-b0002-Z.tsv" = "ae83d620d67c83530d0a86f3d74383230d475f2b3e9429cb279a6d9b7f65c02c" +"raw/jobs/getdp-f0003-b0002-timing.tsv" = "21b7590ee76cc557460b586b8922419c2fdadcbf85b8c81296a4807fc3dcadff" +"raw/jobs/getdp-f0003-b0002.json" = "ef1088694fa66cf6df385d4a8b8a5cbfaa9b9c0829b9e4e58c27cb717e918643" +"raw/jobs/getdp-f0003-b0003-P.tsv" = "55d90d455b3a02541f46a5169151dab2a31d812c371693a9f67f6f91fc3a9e47" +"raw/jobs/getdp-f0003-b0003-Z.tsv" = "0b628fe51bc41b6a4a787526dd4113c7289e964fae96645e5a2e296689aa270e" +"raw/jobs/getdp-f0003-b0003-timing.tsv" = "dbd214bd11f8c52edef5f63512faa4a8fa27744f6f1bef9c60cf6e87d18c8ff0" +"raw/jobs/getdp-f0003-b0003.json" = "bd007d6bdb57701852d8c14848698dc79ad49983d0d17468ee55d263272335b9" +"raw/jobs/getdp-f0004-b0001-P.tsv" = "1bd299e45b4efb8e27bad5cead17a591b8ef4b311dc9a9c9c00fc8a83b275a86" +"raw/jobs/getdp-f0004-b0001-Z.tsv" = "bc963a0c03ed15ca0a5a6c60767671a74fc901951a8181619379d456dcf480ed" +"raw/jobs/getdp-f0004-b0001-timing.tsv" = "7aeab688b092d4315e226b96cb394b3c04ff0bf3ef3f79df2b1d455d0c2b8026" +"raw/jobs/getdp-f0004-b0001.json" = "df2841700247652e6491839a1393bed58e18dc80dddcf0d4c55b7378bfe6158e" +"raw/jobs/getdp-f0004-b0002-P.tsv" = "863f98fb7c167bb29dcdb14f9306f9466b3b838c6636e2d317bd279a6b03c81d" +"raw/jobs/getdp-f0004-b0002-Z.tsv" = "f7bcf9bf8ee99cb5ef9e0c5660e5a29f693dc1eba181cccd96e2915d8cc11f7c" +"raw/jobs/getdp-f0004-b0002-timing.tsv" = "2353c82478acc4954d2892f2fdbefbe3d3e6307df7785ab2b36de8f68104a3fe" +"raw/jobs/getdp-f0004-b0002.json" = "3d559ddebbd63913bbaa84973b5503bcc0f9db3dee993b5b532239cacfdfbf1c" +"raw/jobs/getdp-f0004-b0003-P.tsv" = "07e28b67275f88973e853fc47601af1ec1c860b1feca71ed1d83ee7bdf5da110" +"raw/jobs/getdp-f0004-b0003-Z.tsv" = "7af31afdc071841eb7232f791af1c7e72ac2ec530e9ea60845baa6ded1dec20e" +"raw/jobs/getdp-f0004-b0003-timing.tsv" = "b1287a560de503b9528805d417657b6249350df2348442071086dd8b23011b27" +"raw/jobs/getdp-f0004-b0003.json" = "69d021834e7765e8ac9763295f1ab973e33bfd3b0d9db1ad94c3c0208a50acd5" +"raw/jobs/getdp-f0005-b0001-P.tsv" = "3398b57ec64eb6fae19ee1356c7dab47aa48dbbebe492f35703519b4ef4b0f6f" +"raw/jobs/getdp-f0005-b0001-Z.tsv" = "e685c4abcc4b447849682b072c360d6e77374002167192f4dc8ef9339e0d0e0a" +"raw/jobs/getdp-f0005-b0001-timing.tsv" = "1fc6e8e52ccc711ab9eb43002d21a40a8b2c971984450b1fc364a205a26dcc3f" +"raw/jobs/getdp-f0005-b0001.json" = "7d9d02df5234b63cd33221759290a78151023cbf236f4f05cd6b612504b37046" +"raw/jobs/getdp-f0005-b0002-P.tsv" = "908051f6c7688c8d2123e11334ab950e800b12fc08832372776a36ba8b1698a1" +"raw/jobs/getdp-f0005-b0002-Z.tsv" = "5141eee98ece2f2b13dab47e1358a70589cc4e09facf02d1c6c63d18505ae832" +"raw/jobs/getdp-f0005-b0002-timing.tsv" = "41edcadb637cac180a7728d032312db1c26a7787b7cfaf7aecc41afb3c870891" +"raw/jobs/getdp-f0005-b0002.json" = "8259abf413d084c08e0c7fad122814a05fa6c84c22d337701dce3d03a0f1bf01" +"raw/jobs/getdp-f0005-b0003-P.tsv" = "09d4c5a0830a8ca5769c143d6b32d662386bee0ed11a353cfb9a4208e06bf61f" +"raw/jobs/getdp-f0005-b0003-Z.tsv" = "7dd4aa242c093f26730ae7e97e16754be0306aa86fcb5cde1e220119dec6c786" +"raw/jobs/getdp-f0005-b0003-timing.tsv" = "3e9eba1b4d0a4387f829fafdd33e7abb5bcd2fa1f572d0313c3d7ae5ee4b2c2b" +"raw/jobs/getdp-f0005-b0003.json" = "8e3ca22aed4133227e6008e4e79509e50785d316a9a65412b1caa8952df3279a" +"raw/jobs/getdp-f0006-b0001-P.tsv" = "865759434f809d392a1595c73b3f58ffb9d97e9242aea6c5f1d84a4e992bfa92" +"raw/jobs/getdp-f0006-b0001-Z.tsv" = "89ae231d90380bdff7e76123b3372118793a506645ce6d1a3a75ce0b4bcf3a64" +"raw/jobs/getdp-f0006-b0001-timing.tsv" = "b90f7c2e5e9561e364e7ad705e3e59f41a37cc9c19431ba12b5e592edd17f56e" +"raw/jobs/getdp-f0006-b0001.json" = "bad565cc01125dccde1a5ecdc47cae1f63fdd4f670e2188027fe87877416e90d" +"raw/jobs/getdp-f0006-b0002-P.tsv" = "b9b04974edbcddcdb59dcdfb7299831919debb827ea9e02dda1ab5e93761b88e" +"raw/jobs/getdp-f0006-b0002-Z.tsv" = "7ebed250deaa9ee9de867d79edc82254a76e841d7740b664f84644aae8a34a45" +"raw/jobs/getdp-f0006-b0002-timing.tsv" = "700dd33f153d388e42fedcb3c62dd689eecb750008f39d542bdeb45cba720d3c" +"raw/jobs/getdp-f0006-b0002.json" = "7eae4dc199df86a9717982584fed8f98cd746e47965f830760d89b03ddabb38a" +"raw/jobs/getdp-f0006-b0003-P.tsv" = "5941985e00927cd56caf2e9b9cff07af500fa171eb0d1f06523a3d1d21918b61" +"raw/jobs/getdp-f0006-b0003-Z.tsv" = "89504b5b4da4cb1d6003d262377da8b171ba95ad63a3877be4623e96e2b7b5dd" +"raw/jobs/getdp-f0006-b0003-timing.tsv" = "9e2085f8b2425afbf99d2f9462e9b3eb5434ae20965430b38aa2537e9f8b619b" +"raw/jobs/getdp-f0006-b0003.json" = "935523aef728d4345f46e44d3d0736d17088d31e41ee2e14710b1764e86650b4" +"raw/jobs/getdp-f0007-b0001-P.tsv" = "4f21d6c2a0089b426f25fd933153dca0434e8c2cfa19fea8b8885f106b24442b" +"raw/jobs/getdp-f0007-b0001-Z.tsv" = "2f401e56c1f6be6f4e3de378ce697cd224faa80804d2dbe891be6bfa8263ca25" +"raw/jobs/getdp-f0007-b0001-timing.tsv" = "019a7f6d8d008a4fb431697deb309c64b7989ee49f43f2fc26cf0b05df41b781" +"raw/jobs/getdp-f0007-b0001.json" = "0e9a9b7d4e66b2a4e4a89cf9f3677518437253bc085a16c2c2b43df42b34bc23" +"raw/jobs/getdp-f0007-b0002-P.tsv" = "3247adec4590aa01d86bfda50e7414c2d37f33eab782330f9209b01e891bde30" +"raw/jobs/getdp-f0007-b0002-Z.tsv" = "61ee9cd65d8aa189d0fe5ca7d73a553cea0fb351a2cb23a9950c14ffc1bed663" +"raw/jobs/getdp-f0007-b0002-timing.tsv" = "308e5ffce6dc41a9ac1b5ed885777ed7f127f6bab880161971d77c55840d80c5" +"raw/jobs/getdp-f0007-b0002.json" = "b51a8e184dea9b640695753043e2496b4e0e647ade883ff952d0718dd57ef2ef" +"raw/jobs/getdp-f0007-b0003-P.tsv" = "a7fba6388c8dabacc4ebcd345fc59faf8dc00ddb6bc96ce1ec19232bd1e86ac0" +"raw/jobs/getdp-f0007-b0003-Z.tsv" = "a1baca9f06df00bff1eebc6a31412d0a38d1fdb5dd303c0c62970ca7bb2278af" +"raw/jobs/getdp-f0007-b0003-timing.tsv" = "b77562ae48824f289d666d6cff9974dc0b30808f1169ef74541eac5cd77ef858" +"raw/jobs/getdp-f0007-b0003.json" = "bafebe410a66b76df6190042f61a62edad50c8e5dafa0d2ed6c9977ea8b7d359" +"raw/jobs/getdp-f0008-b0001-P.tsv" = "7ec170463ed10318b84d75dbf349106889f6203587627107fa68b08e72875ef4" +"raw/jobs/getdp-f0008-b0001-Z.tsv" = "34e4772ed0d223b1897452b90094fa71d30f065b0f906841320f216b23e66095" +"raw/jobs/getdp-f0008-b0001-timing.tsv" = "55f45e179a33d9447288d277b1caaf8e3c8809e72bcbe3d28887ead084fc4d9a" +"raw/jobs/getdp-f0008-b0001.json" = "6e957a61541e938a9bb08b4ccffa44b84f51d0b58ae427a4a3adf0a33bad8b01" +"raw/jobs/getdp-f0008-b0002-P.tsv" = "77b8d6a0a84bc66c1ae733c9b191b32836ffefc9d03e14020812c25ffa5e7003" +"raw/jobs/getdp-f0008-b0002-Z.tsv" = "7413fa7f6e036b018ef3683b03ff41f458833daf1faa94a82ae1f0548a45c0a8" +"raw/jobs/getdp-f0008-b0002-timing.tsv" = "5ef5a19f429019393e2740709599ed76a9f3ba045ff0d28f1442f338bcbb11d0" +"raw/jobs/getdp-f0008-b0002.json" = "ed21946bfc5fa28540838cd0c2073e781046998a9f0e1fc4ccb88338d9d3d44b" +"raw/jobs/getdp-f0008-b0003-P.tsv" = "7238161273ea9daf4c5754d4f07278913fad3d1af45309be865b041e895162c1" +"raw/jobs/getdp-f0008-b0003-Z.tsv" = "b5c816019ac161e29efcf04512ee7eac006b7128fe57c2ac1fa69060e9aad2c8" +"raw/jobs/getdp-f0008-b0003-timing.tsv" = "0180204ba44884eef9956d7b4fdaf4c654ef35cd9416fc4b5e8af45b16c487e0" +"raw/jobs/getdp-f0008-b0003.json" = "6e1dfd89429f100e59ee198995197c74b2a48f85a7de11aa4027ca9654fa93b5" +"raw/jobs/getdp-f0009-b0001-P.tsv" = "ca8bf539a2aa791698165bb81d085f5acbb189b3b62d246ba937dc92ce7353f7" +"raw/jobs/getdp-f0009-b0001-Z.tsv" = "e474b796474a4166bc2883c204746842000b1b101b09acf18c4419197c451a47" +"raw/jobs/getdp-f0009-b0001-timing.tsv" = "32ef8681dc4f1d2fe0bfbdccc584179f43ed906d8567e350078c46246e98cfec" +"raw/jobs/getdp-f0009-b0001.json" = "d7fccb1dffe6ee790daba952b3ab9db13481bbe91cf43124467a89d4e7c1a612" +"raw/jobs/getdp-f0009-b0002-P.tsv" = "9792cefef8b214884d006bafa09587a935868ec363641eec307a112df4090d34" +"raw/jobs/getdp-f0009-b0002-Z.tsv" = "2ac4420529eb51e337786056ac19b4f6b190e515cc8194d4a6dc6eb84241cd41" +"raw/jobs/getdp-f0009-b0002-timing.tsv" = "77a626f5fa0b16f4b318ee4e5218e69642558e9f4578f95e2817835cbff61abf" +"raw/jobs/getdp-f0009-b0002.json" = "eefc711a0655e8216cc77c07b4070cc17709c324ebdb8127be119356ef6e3450" +"raw/jobs/getdp-f0009-b0003-P.tsv" = "baeca8aeb00e5b88a304688c8e3138f47a07a19b67685f5f0163d88e8fac0c42" +"raw/jobs/getdp-f0009-b0003-Z.tsv" = "e40082bcb62bad066853868f3ad9406ed0b293ad12b07856df4861040df35107" +"raw/jobs/getdp-f0009-b0003-timing.tsv" = "de46b42118e3fcaeebbd26d9e831304addcb47db275fb8ce4d3e758d345e6b7d" +"raw/jobs/getdp-f0009-b0003.json" = "a4085e4ff461ca9070f7aa8541aa3304a6bcda241a20912d177779b36592b9af" +"raw/scan_complete.tsv" = "2cc9eff50bb7e6fff0e2555f96d93ebe952c4a1a158a1dca82fe6cca3aa1afab" +"run.json" = "3cfe7084db85bc9c9529dcc20a688cdd99aa4b8f807904a6caf582aa6627a10a" +"timing-summary.json" = "9eaf44fc38ed9d55bb67332e0617efe6b10962a5356cc7e34f0934de30acad2a" diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/fem/formulation.txt b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/fem/formulation.txt new file mode 100644 index 000000000..0e94e36ec --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/fem/formulation.txt @@ -0,0 +1 @@ +@NamedTuple{backend::Symbol, requested::NamedTuple, methods::NamedTuple, options::NamedTuple}((:fem, (insulation_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), semicon_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_properties = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), temperature_dependence = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing)), (insulation_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), semicon_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), earth_properties = (identifier = :constant, parameters = NamedTuple(), options = NamedTuple()), temperature_dependence = (identifier = :linear, parameters = NamedTuple(), options = NamedTuple())), (reduce_bundle = false, kron_reduction = false, ideal_transposition = false, physics = Symbol("quasi-tem")))) \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/fem/problem.json b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/fem/problem.json new file mode 100644 index 000000000..ed2585440 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/fem/problem.json @@ -0,0 +1,516 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "root": { + "kind": "line_parameters_problem", + "earth_props": { + "kind": "earth_model", + "vertical_layers": false, + "layers": [ + { + "kind": "earth_layer", + "thickness": { + "special": "Inf", + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "rho": { + "special": "Inf", + "__type__": "Float64" + } + }, + { + "kind": "earth_layer", + "thickness": { + "special": "Inf", + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "rho": { + "value": 0.1, + "__type__": "Float64" + } + } + ] + }, + "Gamma": null, + "frequencies": [ + { + "value": 0.1, + "__type__": "Float64" + }, + { + "value": 1, + "__type__": "Float64" + }, + { + "value": 10, + "__type__": "Float64" + }, + { + "value": 100, + "__type__": "Float64" + }, + { + "value": 1000, + "__type__": "Float64" + }, + { + "value": 10000, + "__type__": "Float64" + }, + { + "value": 100000, + "__type__": "Float64" + }, + { + "value": 1000000, + "__type__": "Float64" + }, + { + "value": 10000000, + "__type__": "Float64" + } + ], + "system": { + "kind": "line_cable_system", + "input_positions": [ + { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 1, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 2, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + } + ], + "clearances": [ + [ + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + } + ], + [ + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + } + ], + [ + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + } + ] + ], + "connections": [ + [ + 1 + ], + [ + 2 + ], + [ + 3 + ] + ], + "line_length": { + "value": 1, + "__type__": "Float64" + }, + "environment": null, + "system_id": "three_bare_wires_air_1", + "positions": [ + { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 1, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 2, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + } + ], + "designs": [ + { + "kind": "cable_design", + "cable_id": "three_bare_wires_air_1", + "nominal_data": [ + ], + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "core_metal", + "material": { + "value": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "type": "material" + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.0425, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + } + ] + } + }, + { + "kind": "cable_design", + "cable_id": "three_bare_wires_air_1", + "nominal_data": [ + ], + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "core_metal", + "material": { + "value": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "type": "material" + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.0425, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + } + ] + } + }, + { + "kind": "cable_design", + "cable_id": "three_bare_wires_air_1", + "nominal_data": [ + ], + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "core_metal", + "material": { + "value": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "type": "material" + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.0425, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + } + ] + } + } + ] + }, + "temperature": { + "value": 20, + "__type__": "Float64" + } + }, + "format": "linecablemodels.line_parameters_problem", + "version": "1.0.0" +} \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/unified/Y.csv b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/unified/Y.csv new file mode 100644 index 000000000..ade1c8772 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/unified/Y.csv @@ -0,0 +1,82 @@ +frequency_hz,response_terminal,basis_terminal,real_S_per_m,imaginary_S_per_m +0.10000000000000001,cable:1:core,cable:1:core,3.3278820986485526e-24,9.0759067020569009e-12 +0.10000000000000001,cable:1:core,cable:2:core,-1.3601058441964637e-13,-4.2379939867923232e-12 +0.10000000000000001,cable:1:core,cable:3:core,-1.3581746350160624e-13,-2.9334497945739113e-12 +0.10000000000000001,cable:2:core,cable:1:core,-1.1296417495695157e-13,-4.2580756764147954e-12 +0.10000000000000001,cable:2:core,cable:2:core,10.815794395856189,0.14817480024712162 +0.10000000000000001,cable:2:core,cable:3:core,-8.3998980305212694,0.14802772097705341 +0.10000000000000001,cable:3:core,cable:1:core,-1.1280530750282936e-13,-2.9535313549728525e-12 +0.10000000000000001,cable:3:core,cable:2:core,-8.3998980305212712,0.14802772097705655 +0.10000000000000001,cable:3:core,cable:3:core,10.815794395856152,0.14817480024608787 +1,cable:1:core,cable:1:core,3.1423916594339808e-22,9.0759067020551008e-11 +1,cable:1:core,cable:2:core,-2.4605830567011761e-12,-3.9952527978242649e-11 +1,cable:1:core,cable:3:core,-2.4448833834075763e-12,-2.6909309921922225e-11 +1,cable:2:core,cable:1:core,-1.6339304034811497e-12,-4.064888543178278e-11 +1,cable:2:core,cable:2:core,11.070033787835508,0.21907795138250027 +1,cable:2:core,cable:3:core,-8.1458225270891802,0.2178742363474 +1,cable:3:core,cable:1:core,-1.6216279546194463e-12,-2.7605627275097564e-11 +1,cable:3:core,cable:2:core,-8.1458225270892974,0.2178742363475 +1,cable:3:core,cable:3:core,11.070033787834912,0.2190779513727919 +10,cable:1:core,cable:1:core,2.7893988431890267e-20,9.0759067020117803e-10 +10,cable:1:core,cable:2:core,-5.5746231805725367e-11,-3.5319928202439058e-10 +10,cable:1:core,cable:3:core,-5.4539271312740426e-11,-2.2299128059268604e-10 +10,cable:2:core,cable:1:core,-2.5235453516660523e-11,-3.7794241747387376e-10 +10,cable:2:core,cable:2:core,11.455439466704808,0.35723512545106118 +10,cable:2:core,cable:3:core,-7.7620499918328036,0.34786703922484841 +10,cable:3:core,cable:1:core,-2.4359412301593318e-11,-2.47722357370758e-10 +10,cable:3:core,cable:2:core,-7.7620499918370207,0.34786703922841328 +10,cable:3:core,cable:3:core,11.455439466692642,0.35723512536493024 +100,cable:1:core,cable:1:core,1.9127172212576587e-18,9.0759067005974392e-09 +100,cable:1:core,cable:2:core,-1.5750338466741854e-09,-2.411086810379962e-09 +100,cable:1:core,cable:3:core,-1.491065678495518e-09,-1.131648013139559e-09 +100,cable:2:core,cable:1:core,-4.2495166380733107e-10,-3.3258245294674344e-09 +100,cable:2:core,cable:2:core,12.104669782732641,0.6826164828956679 +100,cable:2:core,cable:3:core,-7.1288262822103956,0.61549758378874042 +100,cable:3:core,cable:1:core,-3.7138847700779842e-10,-2.0430445507832228e-09 +100,cable:3:core,cable:2:core,-7.1288262823565649,0.61549758392216858 +100,cable:3:core,cable:3:core,12.104669782424534,0.68261648227961069 +1000,cable:1:core,cable:1:core,-1.1449192093489899e-16,9.0759066436426566e-08 +1000,cable:1:core,cable:2:core,-4.9053839044978193e-08,1.1664030389121588e-08 +1000,cable:1:core,cable:3:core,-4.4374379330248368e-08,2.2280527656749991e-08 +1000,cable:2:core,cable:1:core,-7.6990452661998496e-09,-2.5177999831420838e-08 +1000,cable:2:core,cable:2:core,13.422507243289033,1.6280303346877074 +1000,cable:2:core,cable:3:core,-5.9564805769900744,1.214133621295584 +1000,cable:3:core,cable:1:core,-5.3125768097971736e-09,-1.3812807003898827e-08 +1000,cable:3:core,cable:2:core,-5.956480580627443,1.2141336265943121 +1000,cable:3:core,cable:3:core,13.422507236118634,1.6280303360967185 +10000,cable:1:core,cable:1:core,-1.461383916862566e-13,9.0759044220431542e-07 +10000,cable:1:core,cable:2:core,-8.7200816269054662e-07,1.5363787545915005e-06 +10000,cable:1:core,cable:3:core,-7.4376780209961771e-07,1.4920783526578328e-06 +10000,cable:2:core,cable:1:core,-1.0820603068185215e-07,-9.3862013882438677e-08 +10000,cable:2:core,cable:2:core,17.086533982869692,4.2459947768646558 +10000,cable:2:core,cable:3:core,-3.3202280314027313,2.2955285336624969 +10000,cable:3:core,cable:1:core,-5.8587420416245618e-08,-4.3856893464081314e-08 +10000,cable:3:core,cable:2:core,-3.3202279874715748,2.2955286461190285 +10000,cable:3:core,cable:3:core,17.086534015481533,4.2459949196386599 +100000,cable:1:core,cable:1:core,-5.4794500700018239e-11,9.0758158501016051e-06 +100000,cable:1:core,cable:2:core,1.6957770865381572e-05,9.1879747547490105e-06 +100000,cable:1:core,cable:3:core,1.5670639845467551e-05,7.7897672759496725e-06 +100000,cable:2:core,cable:1:core,-1.3779479962975289e-07,1.5778397005743111e-07 +100000,cable:2:core,cable:2:core,25.114053162870391,8.061797456881747 +100000,cable:2:core,cable:3:core,0.17168867735387133,1.0502294245133199 +100000,cable:3:core,cable:1:core,-5.2567611266984596e-08,7.452419574772051e-08 +100000,cable:3:core,cable:2:core,0.17168860920972603,1.0502292248219658 +100000,cable:3:core,cable:3:core,25.114053091114521,8.0617971943186379 +1000000,cable:1:core,cable:1:core,-2.4719345718823342e-08,9.07115475559531e-05 +1000000,cable:1:core,cable:2:core,-7.7983645094344234e-06,5.4402402295420326e-06 +1000000,cable:1:core,cable:3:core,-6.4350271678452739e-06,5.1067899044121279e-06 +1000000,cable:2:core,cable:1:core,-3.6993334581473185e-09,-1.455981630897755e-08 +1000000,cable:2:core,cable:2:core,40.8337746272082,19.549173405645227 +1000000,cable:2:core,cable:3:core,-0.020403832081266379,-0.018991054091929888 +1000000,cable:3:core,cable:1:core,-1.5503936374884616e-09,-5.8061917080445249e-09 +1000000,cable:3:core,cable:2:core,-0.020403832499540878,-0.018991053491192872 +1000000,cable:3:core,cable:3:core,40.833774626502667,19.54917340640004 +10000000,cable:1:core,cable:1:core,-1.6761328755330462e-05,0.00088777830561370815 +10000000,cable:1:core,cable:2:core,-9.3440467051073175e-11,5.4261406900812513e-10 +10000000,cable:1:core,cable:3:core,3.6775425414283791e-11,4.3675607950356147e-10 +10000000,cable:2:core,cable:1:core,-1.1003845565783392e-13,-1.3743080703062872e-13 +10000000,cable:2:core,cable:2:core,80.846531351828517,55.108195401013525 +10000000,cable:2:core,cable:3:core,-9.2664181398870313e-08,-1.3479772233806722e-07 +10000000,cable:3:core,cable:1:core,-5.2158582925956725e-14,-5.4725070160129558e-14 +10000000,cable:3:core,cable:2:core,-9.266418139883618e-08,-1.3479772233804374e-07 +10000000,cable:3:core,cable:3:core,80.846531351828517,55.108195401013525 diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/unified/Z.csv b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/unified/Z.csv new file mode 100644 index 000000000..0294ac732 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/unified/Z.csv @@ -0,0 +1,82 @@ +frequency_hz,response_terminal,basis_terminal,real_ohm_per_m,imaginary_ohm_per_m +0.10000000000000001,cable:1:core,cable:1:core,3.1368030432638579e-06,1.2442491566826316e-06 +0.10000000000000001,cable:1:core,cable:2:core,9.8692094314379645e-08,7.1450042905111184e-07 +0.10000000000000001,cable:1:core,cable:3:core,9.8689933646133091e-08,6.8496952228978849e-07 +0.10000000000000001,cable:2:core,cable:1:core,9.8692094314379671e-08,7.1450042905111195e-07 +0.10000000000000001,cable:2:core,cable:2:core,3.1374615510534732e-06,1.2435841216202129e-06 +0.10000000000000001,cable:2:core,cable:3:core,9.9023675189331398e-08,8.1529149017255914e-07 +0.10000000000000001,cable:3:core,cable:1:core,9.8689933646132827e-08,6.8496952228978997e-07 +0.10000000000000001,cable:3:core,cable:2:core,9.9023675189331385e-08,8.1529149017255903e-07 +0.10000000000000001,cable:3:core,cable:3:core,3.1374615510534732e-06,1.2435841216202129e-06 +1,cable:1:core,cable:1:core,4.025822822265763e-06,1.1002346916048128e-05 +1,cable:1:core,cable:2:core,9.8663940253676944e-07,5.6982925454684157e-06 +1,cable:1:core,cable:3:core,9.8646617099486401e-07,5.40300976822235e-06 +1,cable:2:core,cable:1:core,9.8663940253676944e-07,5.6982925454684148e-06 +1,cable:2:core,cable:2:core,4.0462619649157199e-06,1.0981370586286009e-05 +1,cable:2:core,cable:3:core,9.9703495653241453e-07,6.6990056616567426e-06 +1,cable:3:core,cable:1:core,9.8646617099486423e-07,5.4030097682223517e-06 +1,cable:3:core,cable:2:core,9.9703495653241474e-07,6.6990056616567443e-06 +1,cable:3:core,cable:3:core,4.0462619649157199e-06,1.0981370586286005e-05 +10,cable:1:core,cable:1:core,1.3443875148217149e-05,9.535729423772166e-05 +10,cable:1:core,cable:2:core,9.8448260812245715e-06,4.2519810980107068e-05 +10,cable:1:core,cable:3:core,9.831785145126619e-06,3.9569605534518673e-05 +10,cable:2:core,cable:1:core,9.8448260812245698e-06,4.2519810980107075e-05 +10,cable:2:core,cable:2:core,1.405966050935706e-05,9.4699377213437497e-05 +10,cable:2:core,cable:3:core,1.0164731798726485e-05,5.2299532930466571e-05 +10,cable:3:core,cable:1:core,9.8317851451266173e-06,3.9569605534518679e-05 +10,cable:3:core,cable:2:core,1.0164731798726483e-05,5.2299532930466571e-05 +10,cable:3:core,cable:3:core,1.405966050935706e-05,9.4699377213437511e-05 +100,cable:1:core,cable:1:core,0.00010029065542748186,0.00079835616300552798 +100,cable:1:core,cable:2:core,9.693925872058955e-05,0.00028095471396052344 +100,cable:1:core,cable:3:core,9.6062121241851698e-05,0.00025171028261849001 +100,cable:2:core,cable:1:core,9.6939258720589563e-05,0.00028095471396052344 +100,cable:2:core,cable:2:core,0.00011750384543838787,0.00077809644648036422 +100,cable:2:core,cable:3:core,0.00010621565159805986,0.00037161327841812283 +100,cable:3:core,cable:1:core,9.6062121241851712e-05,0.00025171028261849007 +100,cable:3:core,cable:2:core,0.00010621565159805986,0.00037161327841812278 +100,cable:3:core,cable:3:core,0.00011750384543838806,0.00077809644648036465 +1000,cable:1:core,cable:1:core,0.0007885187684286524,0.0066684718151727796 +1000,cable:1:core,cable:2:core,0.00088055424215432207,0.0014022944498764082 +1000,cable:1:core,cable:3:core,0.00083441087840421617,0.0011330005183466706 +1000,cable:2:core,cable:1:core,0.00088055424215432196,0.0014022944498764082 +1000,cable:2:core,cable:2:core,0.0011875295144730793,0.0060821869409338489 +1000,cable:2:core,cable:3:core,0.0011020546722505062,0.0020961818335963386 +1000,cable:3:core,cable:1:core,0.00083441087840421607,0.0011330005183466706 +1000,cable:3:core,cable:2:core,0.0011020546722505062,0.0020961818335963381 +1000,cable:3:core,cable:3:core,0.0011875295144732623,0.0060821869409340397 +10000,cable:1:core,cable:1:core,0.0051968947578844259,0.056898361105510824 +10000,cable:1:core,cable:2:core,0.0052676968239072927,0.0024783732958178969 +10000,cable:1:core,cable:3:core,0.0040559166332579258,0.0011450309278046007 +10000,cable:2:core,cable:1:core,0.0052676968239068461,0.0024783732958180774 +10000,cable:2:core,cable:2:core,0.011217390349824954,0.043047632316487089 +10000,cable:2:core,cable:3:core,0.008034030912603558,0.004866651343792168 +10000,cable:3:core,cable:1:core,0.0040559166332579232,0.0011450309278046009 +10000,cable:3:core,cable:2:core,0.0080340309126035597,0.004866651343792168 +10000,cable:3:core,cable:3:core,0.01121739034987806,0.043047632316466661 +100000,cable:1:core,cable:1:core,0.02502944569653533,0.51501770227425026 +100000,cable:1:core,cable:2:core,0.0029200092671962086,-0.0055306020499575215 +100000,cable:1:core,cable:3:core,0.00087908935478321442,-0.0031017341595444793 +100000,cable:2:core,cable:1:core,0.0029200092671962091,-0.0055306020499575207 +100000,cable:2:core,cable:2:core,0.091870181209119314,0.28525842189891248 +100000,cable:2:core,cable:3:core,0.0087282162587741137,-0.0087087925865141323 +100000,cable:3:core,cable:1:core,0.00087908935478323122,-0.0031017341595444919 +100000,cable:3:core,cable:2:core,0.008728216258774112,-0.0087087925865141306 +100000,cable:3:core,cable:3:core,0.091870181208271853,0.2852584218993296 +1000000,cable:1:core,cable:1:core,0.094696109689700422,4.9430140482755665 +1000000,cable:1:core,cable:2:core,0.00017862565014624968,0.00022534477815653394 +1000000,cable:1:core,cable:3:core,8.1257499331935308e-05,8.9426642061852518e-05 +1000000,cable:2:core,cable:1:core,0.00017862565014624982,0.00022534477815653239 +1000000,cable:2:core,cable:2:core,0.75407440792448743,1.5740479450662614 +1000000,cable:2:core,cable:3:core,0.0001535709764889112,0.0010629809235061854 +1000000,cable:3:core,cable:1:core,8.1257499331935254e-05,8.9426642061852396e-05 +1000000,cable:3:core,cable:2:core,0.00015357097648891114,0.0010629809235061854 +1000000,cable:3:core,cable:3:core,0.75407440792450831,1.5740479450662672 +10000000,cable:1:core,cable:1:core,1.2514510556301526,49.784417123982301 +10000000,cable:1:core,cable:2:core,1.5377810805855499e-09,4.4664472044009375e-10 +10000000,cable:1:core,cable:3:core,6.8259441253474715e-10,1.285932723177128e-10 +10000000,cable:2:core,cable:1:core,1.5377810805855464e-09,4.4664472044009209e-10 +10000000,cable:2:core,cable:2:core,4.5479347057971937,6.6714050104071516 +10000000,cable:2:core,cable:3:core,3.0492342492444631e-09,1.314231864570851e-08 +10000000,cable:3:core,cable:1:core,6.8259441253474539e-10,1.2859327231771213e-10 +10000000,cable:3:core,cable:2:core,3.0492342492444647e-09,1.314231864570851e-08 +10000000,cable:3:core,cable:3:core,4.5479347057971937,6.6714050104071516 diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/unified/computation-options.txt b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/unified/computation-options.txt new file mode 100644 index 000000000..3c73cbbf9 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/unified/computation-options.txt @@ -0,0 +1 @@ +(trace = true, output_basis = :pul) \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/unified/details.txt b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/unified/details.txt new file mode 100644 index 000000000..d8190a357 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/unified/details.txt @@ -0,0 +1 @@ +@NamedTuple{shunt_model::NamedTuple, trace::@NamedTuple{phase_map::Vector{Int64}, cable_map::Vector{Int64}, Zin::Array{ComplexF64, 3}, Pin::Array{ComplexF64, 3}, Zg::Array{ComplexF64, 3}, Pg::Array{ComplexF64, 3}, Z::Array{ComplexF64, 3}, P::Array{ComplexF64, 3}}, formulations::@NamedTuple{effective::@NamedTuple{internal_impedance::Union{Nothing, Symbol, NamedTuple}, insulation_impedance::Union{Nothing, Symbol, NamedTuple}, earth_impedance::Union{Nothing, Symbol, NamedTuple}, shunt_model::Union{Nothing, Symbol, NamedTuple}, insulation_admittance::Union{Nothing, Symbol, NamedTuple}, semicon_admittance::Union{Nothing, Symbol, NamedTuple}, earth_admittance::Union{Nothing, Symbol, NamedTuple}, earth_properties::Union{Nothing, Symbol, NamedTuple}, pipe_impedance::Union{Nothing, Symbol, NamedTuple}, temperature_dependence::Union{Nothing, Symbol, NamedTuple}}, numerical::@NamedTuple{internal_impedance::NamedTuple, insulation_impedance::NamedTuple, shunt_model::NamedTuple, insulation_admittance::NamedTuple, semicon_admittance::NamedTuple, earth_properties::NamedTuple, temperature_dependence::NamedTuple, earth_impedance::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}, earth_admittance::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}, equivalent_earth::@NamedTuple{earth_impedance::Union{Nothing, @NamedTuple{air::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, mixed::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth_admittance::Union{Nothing, @NamedTuple{air::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, mixed::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, backend::Symbol, requested::NamedTuple, methods::NamedTuple, options::NamedTuple}, coordinates::Vector{String}}(((requested = :coaxial, effective = :coaxial, solves = 0, domains = @NamedTuple{design::Int64, terminals::UnitRange{Int64}, requested::Symbol, effective::Symbol, reason::Symbol, message::String}[], diagnostics = @NamedTuple{boundary_residual::Union{Nothing, Float64}, wire_residual::Union{Nothing, Float64}, tape_residual::Union{Nothing, Float64}, common_residual::Union{Nothing, Float64}, penetration_indicator::Union{Nothing, Float64}, reciprocity::Float64, log_moment_error::Float64, unknowns::Int64, equations::Int64, matrix_bytes::Int64, level::@NamedTuple{wire::Int64, order::Int64, quadrature::Int64, modes::Int64}}[]), (phase_map = [1, 2, 3], cable_map = [1, 2, 3], Zin = ComplexF64[3.0384367161282473e-6 + 3.1415178272814096e-8im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 3.0384367161282473e-6 + 3.1415178272814096e-8im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 3.0384367161282473e-6 + 3.1415178272814096e-8im;;; 3.0491254273478614e-6 + 3.135993190598609e-7im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 3.0491254273478614e-6 + 3.135993190598609e-7im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 3.0491254273478614e-6 + 3.135993190598609e-7im;;; 3.886234398887704e-6 + 2.71311425444745e-6im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 3.886234398887704e-6 + 2.71311425444745e-6im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 3.886234398887704e-6 + 2.71311425444745e-6im;;; 1.0572845476093172e-5 + 9.718155377880437e-6im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 1.0572845476093172e-5 + 9.718155377880437e-6im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 1.0572845476093172e-5 + 9.718155377880437e-6im;;; 3.1668762815475746e-5 + 3.088048172738763e-5im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 3.1668762815475746e-5 + 3.088048172738763e-5im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 3.1668762815475746e-5 + 3.088048172738763e-5im;;; 9.846320725460222e-5 + 9.76946980065643e-5im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 9.846320725460222e-5 + 9.76946980065643e-5im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 9.846320725460222e-5 + 9.76946980065643e-5im;;; 0.0003097129699189164 + 0.0003089505796854997im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0003097129699189164 + 0.0003089505796854997im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0003097129699189164 + 0.0003089505796854997im;;; 0.0009777519922518445 + 0.0009769915236240853im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0009777519922518445 + 0.0009769915236240853im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0009777519922518445 + 0.0009769915236240853im;;; 0.0030902795943006773 + 0.0030895197319977846im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0030902795943006773 + 0.0030895197319977846im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0030902795943006773 + 0.0030895197319977846im], Pin = ComplexF64[0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im], Zg = ComplexF64[9.836632713561072e-8 + 1.2128339784098175e-6im 9.869209431437964e-8 + 7.145004290511118e-7im 9.868993364613309e-8 + 6.849695222897885e-7im; 9.869209431437967e-8 + 7.145004290511119e-7im 9.902483492522585e-8 + 1.2121689433473988e-6im 9.90236751893314e-8 + 8.152914901725591e-7im; 9.868993364613283e-8 + 6.8496952228979e-7im 9.902367518933138e-8 + 8.15291490172559e-7im 9.902483492522585e-8 + 1.2121689433473988e-6im;;; 9.766973949179014e-7 + 1.0688747596988267e-5im 9.866394025367694e-7 + 5.698292545468416e-6im 9.86466170994864e-7 + 5.40300976822235e-6im; 9.866394025367694e-7 + 5.698292545468415e-6im 9.971365375678585e-7 + 1.0667771267226147e-5im 9.970349565324145e-7 + 6.699005661656743e-6im; 9.864661709948642e-7 + 5.403009768222352e-6im 9.970349565324147e-7 + 6.699005661656744e-6im 9.971365375678583e-7 + 1.0667771267226144e-5im;;; 9.557640749329446e-6 + 9.26441799832742e-5im 9.844826081224572e-6 + 4.251981098010707e-5im 9.831785145126619e-6 + 3.956960553451867e-5im; 9.84482608122457e-6 + 4.2519810980107075e-5im 1.0173426110469355e-5 + 9.198626295899004e-5im 1.0164731798726485e-5 + 5.229953293046657e-5im; 9.831785145126617e-6 + 3.956960553451868e-5im 1.0164731798726483e-5 + 5.229953293046657e-5im 1.0173426110469355e-5 + 9.198626295899006e-5im;;; 8.97178099513887e-5 + 0.0007886380076276476im 9.693925872058955e-5 + 0.00028095471396052344im 9.60621212418517e-5 + 0.00025171028261849im; 9.693925872058956e-5 + 0.00028095471396052344im 0.0001069309999622947 + 0.0007683782911024838im 0.00010621565159805986 + 0.00037161327841812283im; 9.606212124185171e-5 + 0.00025171028261849007im 0.00010621565159805986 + 0.0003716132784181228im 0.00010693099996229489 + 0.0007683782911024842im;;; 0.0007568500056131767 + 0.006637591333445392im 0.0008805542421543221 + 0.0014022944498764082im 0.0008344108784042162 + 0.0011330005183466706im; 0.000880554242154322 + 0.0014022944498764082im 0.0011558607516576034 + 0.006051306459206461im 0.0011020546722505062 + 0.0020961818335963386im; 0.0008344108784042161 + 0.0011330005183466706im 0.0011020546722505062 + 0.002096181833596338im 0.0011558607516577865 + 0.006051306459206652im;;; 0.0050984315506298235 + 0.05680066640750426im 0.005267696823907293 + 0.002478373295817897im 0.004055916633257926 + 0.0011450309278046007im; 0.005267696823906846 + 0.0024783732958180774im 0.011118927142570352 + 0.042949937618480524im 0.008034030912603558 + 0.004866651343792168im; 0.004055916633257923 + 0.001145030927804601im 0.00803403091260356 + 0.004866651343792168im 0.011118927142623457 + 0.042949937618460096im;;; 0.024719732726616414 + 0.5147087516945648im 0.0029200092671962086 - 0.0055306020499575215im 0.0008790893547832144 - 0.0031017341595444793im; 0.002920009267196209 - 0.005530602049957521im 0.0915604682392004 + 0.284949471319227im 0.008728216258774114 - 0.008708792586514132im; 0.0008790893547832312 - 0.003101734159544492im 0.008728216258774112 - 0.00870879258651413im 0.09156046823835294 + 0.2849494713196441im;;; 0.09371835769744857 + 4.942037056751943im 0.00017862565014624968 + 0.00022534477815653394im 8.125749933193531e-5 + 8.942664206185252e-5im; 0.00017862565014624982 + 0.0002253447781565324im 0.7530966559322356 + 1.5730709535426373im 0.0001535709764889112 + 0.0010629809235061854im; 8.125749933193525e-5 + 8.94266420618524e-5im 0.00015357097648891114 + 0.0010629809235061854im 0.7530966559322565 + 1.573070953542643im;;; 1.248360776035852 + 49.7813276042503im 1.5377810805855499e-9 + 4.4664472044009375e-10im 6.825944125347471e-10 + 1.285932723177128e-10im; 1.5377810805855464e-9 + 4.466447204400921e-10im 4.544844426202893 + 6.668315490675154im 3.049234249244463e-9 + 1.314231864570851e-8im; 6.825944125347454e-10 + 1.2859327231771213e-10im 3.0492342492444647e-9 + 1.314231864570851e-8im 4.544844426202893 + 6.668315490675154im], Pg = ComplexF64[6.922928489069867e10 + 0.10538021488669605im 0.016248746875902706 + 0.10310932298863519im 0.016248015136087722 + 0.09840937986798147im; 0.015668046278581316 + 0.10375596654056676im 0.01570745005023129 + 0.1444616792842265im 0.015707199774202586 + 0.11176348050276326im; 0.015667437945338984 + 0.09905602295340873im 0.015707199774202586 + 0.11176348050276325im 0.015707450050231293 + 0.14446167928422657im;;; 6.922928489085417e10 + 0.8127136789853869im 0.17251042139422043 + 0.7891832798644066im 0.1724509163207054 + 0.7421922651374684im; 0.15581001431818908 + 0.8081633580148528im 0.157037855870973 + 1.2143641150778552im 0.15701737331905474 + 0.8873849172970905im; 0.1557627484724948 + 0.7611721980509771im 0.15701737331905477 + 0.8873849172970908im 0.15703785587097296 + 1.2143641150778548im;;; 6.922928489267545e10 + 5.513001458814889im 1.9942181491037116 + 5.254340987792914im 1.9896415257990692 + 4.785280553470411im; 1.5302348267576622 + 5.806502856515001im 1.5675656005751466 + 9.841634050447217im 1.56597178766981 + 6.572120710716003im; 1.5268502890592122 + 5.337398400794161im 1.5659717876698098 + 6.572120710716004im 1.5675656005751466 + 9.841634050447219im;;; 6.922928491879378e10 + 23.20180102824903im 26.149213879547393 + 20.166855504911123im 25.83091087993872 + 15.562738366536419im; 14.413975777454764 + 35.92388795975219im 15.478748776504249 + 75.4473776096615im 15.364749279477115 + 42.7798439964949im; 14.205069545682512 + 31.307362770786696im 15.36474927947712 + 42.779843996494904im 15.478748776504275 + 75.44737760966156im;;; 6.922928543754704e10 - 252.87532604360234im 334.76556652611515 - 262.704547578839im 317.2466773695943 - 300.2566382471346im; 117.49012590414134 + 158.03362859036642im 143.36993536442608 + 529.5639819677624im 136.44825405615447 + 205.48511340100336im; 108.10186004750466 + 117.6332520618994im 136.44825405615757 + 205.4851134010069im 143.3699353644524 + 529.563981967786im;;; 6.922930091239401e10 - 12412.03797037801im 570.9030427351584 - 7882.199664577669im 154.02443357987366 - 7692.066854289179im; 567.8140356674953 + 196.0010148031134im 1030.4672887292406 + 3387.263729801053im 738.8450359091654 + 336.1646045732113im; 384.940881914527 + 43.8391418791161im 738.8450359099868 + 336.1646045727079im 1030.4672887349896 + 3387.2637297979145im;;; 6.922997897279497e10 - 416791.30687939236im -49106.80924791681 - 7656.9024053314115im -44784.81918754503 - 4991.020116856682im; 208.72520669654767 - 502.48427635210817im 7244.073338635197 + 22669.33778595756im 681.2764993494048 - 676.6058133093303im; 49.70071122048152 - 226.68367918927154im 681.2764993445952 - 676.6058133009168im 7244.073338563593 + 22669.33778600523im;;; 6.92655456770063e10 - 1.8875207195415117e7im 7169.30304363719 - 12667.171050102053im 5509.81310472989 - 11306.342705549287im; 14.731275378157445 + 17.649506674802318im 59930.23996636772 + 125180.70294016431im 12.216545310536256 + 84.57413449480369im; 5.982806013775303 + 6.99961596430051im 12.216545310584563 + 84.57413449476347im 59930.23996636943 + 125180.70294016469im;;; 7.074904147370567e10 - 1.3357478277705052e9im -0.17200170824752853 - 0.35914384934587784im -0.20449588493526663 - 0.24220645996069157im; 0.00012242285718003855 + 3.4999788662782536e-5im 361696.7481463518 + 530627.5641232601im 0.00024270853323619257 + 0.0010458189856197419im; 5.366851808728216e-5 + 1.0445721894996296e-5im 0.00024270853323619257 + 0.0010458189856197419im 361696.7481463518 + 530627.5641232601im], Z = ComplexF64[3.136803043263858e-6 + 1.2442491566826316e-6im 9.869209431437964e-8 + 7.145004290511118e-7im 9.868993364613309e-8 + 6.849695222897885e-7im; 9.869209431437967e-8 + 7.145004290511119e-7im 3.137461551053473e-6 + 1.243584121620213e-6im 9.90236751893314e-8 + 8.152914901725591e-7im; 9.868993364613283e-8 + 6.8496952228979e-7im 9.902367518933138e-8 + 8.15291490172559e-7im 3.137461551053473e-6 + 1.243584121620213e-6im;;; 4.025822822265763e-6 + 1.1002346916048128e-5im 9.866394025367694e-7 + 5.698292545468416e-6im 9.86466170994864e-7 + 5.40300976822235e-6im; 9.866394025367694e-7 + 5.698292545468415e-6im 4.04626196491572e-6 + 1.0981370586286009e-5im 9.970349565324145e-7 + 6.699005661656743e-6im; 9.864661709948642e-7 + 5.403009768222352e-6im 9.970349565324147e-7 + 6.699005661656744e-6im 4.04626196491572e-6 + 1.0981370586286005e-5im;;; 1.3443875148217149e-5 + 9.535729423772166e-5im 9.844826081224572e-6 + 4.251981098010707e-5im 9.831785145126619e-6 + 3.956960553451867e-5im; 9.84482608122457e-6 + 4.2519810980107075e-5im 1.405966050935706e-5 + 9.46993772134375e-5im 1.0164731798726485e-5 + 5.229953293046657e-5im; 9.831785145126617e-6 + 3.956960553451868e-5im 1.0164731798726483e-5 + 5.229953293046657e-5im 1.405966050935706e-5 + 9.469937721343751e-5im;;; 0.00010029065542748186 + 0.000798356163005528im 9.693925872058955e-5 + 0.00028095471396052344im 9.60621212418517e-5 + 0.00025171028261849im; 9.693925872058956e-5 + 0.00028095471396052344im 0.00011750384543838787 + 0.0007780964464803642im 0.00010621565159805986 + 0.00037161327841812283im; 9.606212124185171e-5 + 0.00025171028261849007im 0.00010621565159805986 + 0.0003716132784181228im 0.00011750384543838806 + 0.0007780964464803647im;;; 0.0007885187684286524 + 0.00666847181517278im 0.0008805542421543221 + 0.0014022944498764082im 0.0008344108784042162 + 0.0011330005183466706im; 0.000880554242154322 + 0.0014022944498764082im 0.0011875295144730793 + 0.006082186940933849im 0.0011020546722505062 + 0.0020961818335963386im; 0.0008344108784042161 + 0.0011330005183466706im 0.0011020546722505062 + 0.002096181833596338im 0.0011875295144732623 + 0.00608218694093404im;;; 0.005196894757884426 + 0.056898361105510824im 0.005267696823907293 + 0.002478373295817897im 0.004055916633257926 + 0.0011450309278046007im; 0.005267696823906846 + 0.0024783732958180774im 0.011217390349824954 + 0.04304763231648709im 0.008034030912603558 + 0.004866651343792168im; 0.004055916633257923 + 0.001145030927804601im 0.00803403091260356 + 0.004866651343792168im 0.01121739034987806 + 0.04304763231646666im;;; 0.02502944569653533 + 0.5150177022742503im 0.0029200092671962086 - 0.0055306020499575215im 0.0008790893547832144 - 0.0031017341595444793im; 0.002920009267196209 - 0.005530602049957521im 0.09187018120911931 + 0.2852584218989125im 0.008728216258774114 - 0.008708792586514132im; 0.0008790893547832312 - 0.003101734159544492im 0.008728216258774112 - 0.00870879258651413im 0.09187018120827185 + 0.2852584218993296im;;; 0.09469610968970042 + 4.9430140482755665im 0.00017862565014624968 + 0.00022534477815653394im 8.125749933193531e-5 + 8.942664206185252e-5im; 0.00017862565014624982 + 0.0002253447781565324im 0.7540744079244874 + 1.5740479450662614im 0.0001535709764889112 + 0.0010629809235061854im; 8.125749933193525e-5 + 8.94266420618524e-5im 0.00015357097648891114 + 0.0010629809235061854im 0.7540744079245083 + 1.5740479450662672im;;; 1.2514510556301526 + 49.7844171239823im 1.5377810805855499e-9 + 4.4664472044009375e-10im 6.825944125347471e-10 + 1.285932723177128e-10im; 1.5377810805855464e-9 + 4.466447204400921e-10im 4.547934705797194 + 6.671405010407152im 3.049234249244463e-9 + 1.314231864570851e-8im; 6.825944125347454e-10 + 1.2859327231771213e-10im 3.0492342492444647e-9 + 1.314231864570851e-8im 4.547934705797194 + 6.671405010407152im], P = ComplexF64[6.922928489069867e10 + 0.10538021488669605im 0.016248746875902706 + 0.10310932298863519im 0.016248015136087722 + 0.09840937986798147im; 0.015668046278581316 + 0.10375596654056676im 0.01570745005023129 + 0.1444616792842265im 0.015707199774202586 + 0.11176348050276326im; 0.015667437945338984 + 0.09905602295340873im 0.015707199774202586 + 0.11176348050276325im 0.015707450050231293 + 0.14446167928422657im;;; 6.922928489085417e10 + 0.8127136789853869im 0.17251042139422043 + 0.7891832798644066im 0.1724509163207054 + 0.7421922651374684im; 0.15581001431818908 + 0.8081633580148528im 0.157037855870973 + 1.2143641150778552im 0.15701737331905474 + 0.8873849172970905im; 0.1557627484724948 + 0.7611721980509771im 0.15701737331905477 + 0.8873849172970908im 0.15703785587097296 + 1.2143641150778548im;;; 6.922928489267545e10 + 5.513001458814889im 1.9942181491037116 + 5.254340987792914im 1.9896415257990692 + 4.785280553470411im; 1.5302348267576622 + 5.806502856515001im 1.5675656005751466 + 9.841634050447217im 1.56597178766981 + 6.572120710716003im; 1.5268502890592122 + 5.337398400794161im 1.5659717876698098 + 6.572120710716004im 1.5675656005751466 + 9.841634050447219im;;; 6.922928491879378e10 + 23.20180102824903im 26.149213879547393 + 20.166855504911123im 25.83091087993872 + 15.562738366536419im; 14.413975777454764 + 35.92388795975219im 15.478748776504249 + 75.4473776096615im 15.364749279477115 + 42.7798439964949im; 14.205069545682512 + 31.307362770786696im 15.36474927947712 + 42.779843996494904im 15.478748776504275 + 75.44737760966156im;;; 6.922928543754704e10 - 252.87532604360234im 334.76556652611515 - 262.704547578839im 317.2466773695943 - 300.2566382471346im; 117.49012590414134 + 158.03362859036642im 143.36993536442608 + 529.5639819677624im 136.44825405615447 + 205.48511340100336im; 108.10186004750466 + 117.6332520618994im 136.44825405615757 + 205.4851134010069im 143.3699353644524 + 529.563981967786im;;; 6.922930091239401e10 - 12412.03797037801im 570.9030427351584 - 7882.199664577669im 154.02443357987366 - 7692.066854289179im; 567.8140356674953 + 196.0010148031134im 1030.4672887292406 + 3387.263729801053im 738.8450359091654 + 336.1646045732113im; 384.940881914527 + 43.8391418791161im 738.8450359099868 + 336.1646045727079im 1030.4672887349896 + 3387.2637297979145im;;; 6.922997897279497e10 - 416791.30687939236im -49106.80924791681 - 7656.9024053314115im -44784.81918754503 - 4991.020116856682im; 208.72520669654767 - 502.48427635210817im 7244.073338635197 + 22669.33778595756im 681.2764993494048 - 676.6058133093303im; 49.70071122048152 - 226.68367918927154im 681.2764993445952 - 676.6058133009168im 7244.073338563593 + 22669.33778600523im;;; 6.92655456770063e10 - 1.8875207195415117e7im 7169.30304363719 - 12667.171050102053im 5509.81310472989 - 11306.342705549287im; 14.731275378157445 + 17.649506674802318im 59930.23996636772 + 125180.70294016431im 12.216545310536256 + 84.57413449480369im; 5.982806013775303 + 6.99961596430051im 12.216545310584563 + 84.57413449476347im 59930.23996636943 + 125180.70294016469im;;; 7.074904147370567e10 - 1.3357478277705052e9im -0.17200170824752853 - 0.35914384934587784im -0.20449588493526663 - 0.24220645996069157im; 0.00012242285718003855 + 3.4999788662782536e-5im 361696.7481463518 + 530627.5641232601im 0.00024270853323619257 + 0.0010458189856197419im; 5.366851808728216e-5 + 1.0445721894996296e-5im 0.00024270853323619257 + 0.0010458189856197419im 361696.7481463518 + 530627.5641232601im]), @NamedTuple{effective::@NamedTuple{internal_impedance::Union{Nothing, Symbol, NamedTuple}, insulation_impedance::Union{Nothing, Symbol, NamedTuple}, earth_impedance::Union{Nothing, Symbol, NamedTuple}, shunt_model::Union{Nothing, Symbol, NamedTuple}, insulation_admittance::Union{Nothing, Symbol, NamedTuple}, semicon_admittance::Union{Nothing, Symbol, NamedTuple}, earth_admittance::Union{Nothing, Symbol, NamedTuple}, earth_properties::Union{Nothing, Symbol, NamedTuple}, pipe_impedance::Union{Nothing, Symbol, NamedTuple}, temperature_dependence::Union{Nothing, Symbol, NamedTuple}}, numerical::@NamedTuple{internal_impedance::NamedTuple, insulation_impedance::NamedTuple, shunt_model::NamedTuple, insulation_admittance::NamedTuple, semicon_admittance::NamedTuple, earth_properties::NamedTuple, temperature_dependence::NamedTuple, earth_impedance::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}, earth_admittance::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}, equivalent_earth::@NamedTuple{earth_impedance::Union{Nothing, @NamedTuple{air::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, mixed::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth_admittance::Union{Nothing, @NamedTuple{air::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, mixed::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, backend::Symbol, requested::NamedTuple, methods::NamedTuple, options::NamedTuple}((@NamedTuple{internal_impedance::Union{Nothing, Symbol, NamedTuple}, insulation_impedance::Union{Nothing, Symbol, NamedTuple}, earth_impedance::Union{Nothing, Symbol, NamedTuple}, shunt_model::Union{Nothing, Symbol, NamedTuple}, insulation_admittance::Union{Nothing, Symbol, NamedTuple}, semicon_admittance::Union{Nothing, Symbol, NamedTuple}, earth_admittance::Union{Nothing, Symbol, NamedTuple}, earth_properties::Union{Nothing, Symbol, NamedTuple}, pipe_impedance::Union{Nothing, Symbol, NamedTuple}, temperature_dependence::Union{Nothing, Symbol, NamedTuple}}((:schelkunoff1934, :ametani1980, :unified, :coaxial, :lossless, :lossless, :unified, :constant, :none, :linear)), @NamedTuple{internal_impedance::NamedTuple, insulation_impedance::NamedTuple, shunt_model::NamedTuple, insulation_admittance::NamedTuple, semicon_admittance::NamedTuple, earth_properties::NamedTuple, temperature_dependence::NamedTuple, earth_impedance::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}, earth_admittance::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}(((outer = NamedTuple(),), NamedTuple(), NamedTuple(), NamedTuple(), NamedTuple(), NamedTuple(), NamedTuple(), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}[@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :self, 1, 1, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),))), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :mutual, 1, 2, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),))), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :mutual, 2, 1, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),))), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :self, 2, 2, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),))), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :mutual, 2, 2, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),)))], @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}[@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :self, 1, 1, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),))), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :mutual, 1, 2, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),))), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :mutual, 2, 1, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),))), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :self, 2, 2, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),))), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :mutual, 2, 2, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),)))])), @NamedTuple{earth_impedance::Union{Nothing, @NamedTuple{air::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, mixed::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth_admittance::Union{Nothing, @NamedTuple{air::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, mixed::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}((nothing, nothing)), :coaxial, (internal_impedance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), insulation_impedance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_impedance = (identifier = :unified, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), shunt_model = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), insulation_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), semicon_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_admittance = (identifier = :unified, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_properties = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), pipe_impedance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), temperature_dependence = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing)), (internal_impedance = (identifier = :schelkunoff1934, parameters = NamedTuple(), options = (inner = NamedTuple(), outer = NamedTuple(), transfer = NamedTuple()), configured_options = ()), insulation_impedance = (identifier = :ametani1980, parameters = NamedTuple(), options = NamedTuple()), earth_impedance = (identifier = :unified, assumptions = (media = Val{:homogeneous}(), layers = 2:2, longitudinal = :prescribed, permittivity = :positive), parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), shunt_model = (identifier = :coaxial, parameters = NamedTuple(), options = NamedTuple()), insulation_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), semicon_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), earth_admittance = (identifier = :unified, assumptions = (media = Val{:homogeneous}(), layers = 2:2, longitudinal = :prescribed, permittivity = :positive), parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_properties = (identifier = :constant, parameters = NamedTuple(), options = NamedTuple()), pipe_impedance = (identifier = :none, parameters = NamedTuple(), options = NamedTuple()), temperature_dependence = (identifier = :linear, parameters = NamedTuple(), options = NamedTuple())), (reduce_bundle = false, kron_reduction = false, ideal_transposition = false))), ["cable:1:core", "cable:2:core", "cable:3:core"])) \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/unified/formulation.txt b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/unified/formulation.txt new file mode 100644 index 000000000..1a6917548 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/unified/formulation.txt @@ -0,0 +1 @@ +@NamedTuple{backend::Symbol, requested::NamedTuple, methods::NamedTuple, options::NamedTuple}((:coaxial, (internal_impedance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), insulation_impedance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_impedance = (identifier = :unified, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), shunt_model = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), insulation_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), semicon_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_admittance = (identifier = :unified, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_properties = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), pipe_impedance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), temperature_dependence = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing)), (internal_impedance = (identifier = :schelkunoff1934, parameters = NamedTuple(), options = (inner = NamedTuple(), outer = NamedTuple(), transfer = NamedTuple()), configured_options = ()), insulation_impedance = (identifier = :ametani1980, parameters = NamedTuple(), options = NamedTuple()), earth_impedance = (identifier = :unified, assumptions = (media = Val{:homogeneous}(), layers = 2:2, longitudinal = :prescribed, permittivity = :positive), parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), shunt_model = (identifier = :coaxial, parameters = NamedTuple(), options = NamedTuple()), insulation_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), semicon_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), earth_admittance = (identifier = :unified, assumptions = (media = Val{:homogeneous}(), layers = 2:2, longitudinal = :prescribed, permittivity = :positive), parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_properties = (identifier = :constant, parameters = NamedTuple(), options = NamedTuple()), pipe_impedance = (identifier = :none, parameters = NamedTuple(), options = NamedTuple()), temperature_dependence = (identifier = :linear, parameters = NamedTuple(), options = NamedTuple())), (reduce_bundle = false, kron_reduction = false, ideal_transposition = false))) \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/unified/problem.json b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/unified/problem.json new file mode 100644 index 000000000..ed2585440 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_1/unified/problem.json @@ -0,0 +1,516 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "root": { + "kind": "line_parameters_problem", + "earth_props": { + "kind": "earth_model", + "vertical_layers": false, + "layers": [ + { + "kind": "earth_layer", + "thickness": { + "special": "Inf", + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "rho": { + "special": "Inf", + "__type__": "Float64" + } + }, + { + "kind": "earth_layer", + "thickness": { + "special": "Inf", + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "rho": { + "value": 0.1, + "__type__": "Float64" + } + } + ] + }, + "Gamma": null, + "frequencies": [ + { + "value": 0.1, + "__type__": "Float64" + }, + { + "value": 1, + "__type__": "Float64" + }, + { + "value": 10, + "__type__": "Float64" + }, + { + "value": 100, + "__type__": "Float64" + }, + { + "value": 1000, + "__type__": "Float64" + }, + { + "value": 10000, + "__type__": "Float64" + }, + { + "value": 100000, + "__type__": "Float64" + }, + { + "value": 1000000, + "__type__": "Float64" + }, + { + "value": 10000000, + "__type__": "Float64" + } + ], + "system": { + "kind": "line_cable_system", + "input_positions": [ + { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 1, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 2, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + } + ], + "clearances": [ + [ + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + } + ], + [ + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + } + ], + [ + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + } + ] + ], + "connections": [ + [ + 1 + ], + [ + 2 + ], + [ + 3 + ] + ], + "line_length": { + "value": 1, + "__type__": "Float64" + }, + "environment": null, + "system_id": "three_bare_wires_air_1", + "positions": [ + { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 1, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 2, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + } + ], + "designs": [ + { + "kind": "cable_design", + "cable_id": "three_bare_wires_air_1", + "nominal_data": [ + ], + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "core_metal", + "material": { + "value": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "type": "material" + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.0425, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + } + ] + } + }, + { + "kind": "cable_design", + "cable_id": "three_bare_wires_air_1", + "nominal_data": [ + ], + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "core_metal", + "material": { + "value": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "type": "material" + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.0425, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + } + ] + } + }, + { + "kind": "cable_design", + "cable_id": "three_bare_wires_air_1", + "nominal_data": [ + ], + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "core_metal", + "material": { + "value": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "type": "material" + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.0425, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + } + ] + } + } + ] + }, + "temperature": { + "value": 20, + "__type__": "Float64" + } + }, + "format": "linecablemodels.line_parameters_problem", + "version": "1.0.0" +} \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/Y-differences.csv b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/Y-differences.csv new file mode 100644 index 000000000..f61beb50b --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/Y-differences.csv @@ -0,0 +1,82 @@ +frequency_hz,response_terminal,basis_terminal,absolute_error,relative_error,real_absolute_error,real_relative_error,imaginary_absolute_error,imaginary_relative_error,real_reference_near_zero,imaginary_reference_near_zero +0.10000000000000001,cable:1:core,cable:1:core,0.04905682882206721,0.0055834856560000087,0.04903964043140796,0.005582463468563466,0.0012985070026080292,0.0080790589311429042,false,false +0.10000000000000001,cable:1:core,cable:2:core,2.595836505670303e-14,0.0067394628803151366,7.5895069426839315e-16,0.0084269676646886657,2.5947267876613337e-14,0.0067384240981989548,false,false +0.10000000000000001,cable:1:core,cable:3:core,0.038199955459822722,0.0060906343289310544,0.038178658875890115,0.006089229616057382,0.0012753836955428921,0.0079528864381459511,false,false +0.10000000000000001,cable:2:core,cable:1:core,5.2120581849254745e-14,0.013531850943084534,2.3218780785571361e-14,0.25780846683606851,4.6663082529301952e-14,0.01211825619972501,false,false +0.10000000000000001,cable:2:core,cable:2:core,5.8342929993784682e-14,0.0063872712421902738,3.0483223595081406e-26,NaN,5.8342929993784682e-14,0.0063872712421902738,true,false +0.10000000000000001,cable:2:core,cable:3:core,5.1971202315389685e-14,0.013493675951093551,2.3231460207596593e-14,0.25798557267623767,4.6489838962186372e-14,0.012073808599070313,false,false +0.10000000000000001,cable:3:core,cable:1:core,0.038199955459823569,0.0060906343289311889,0.038178658875891003,0.0060892296160575234,0.0012753836955416986,0.0079528864381385542,false,false +0.10000000000000001,cable:3:core,cable:2:core,2.5784825962525592e-14,0.006694709194569372,7.4627127224428478e-16,0.0082873491300735971,2.577402430948564e-14,0.0066937344436411046,false,false +0.10000000000000001,cable:3:core,cable:3:core,0.048703949106892831,0.0055435445203254868,0.048687823738902125,0.0055426361358639591,0.0012531872072462924,0.007799287119639552,false,false +1,cable:1:core,cable:1:core,0.04946044482134742,0.0054574146988789086,0.049431288473058999,0.0054561373201644347,0.0016980346930428603,0.0070270678925479372,false,false +1,cable:1:core,cable:2:core,2.4935296141618038e-13,0.0067417314047761917,9.6800543030343842e-15,0.0073874819399981084,2.4916499737264353e-13,0.0067408809902199541,false,false +1,cable:1:core,cable:3:core,0.037900457537375781,0.0063168184094564281,0.037861699896205359,0.0063153694563121534,0.0017135817786461394,0.007170984427213041,false,false +1,cable:2:core,cable:1:core,1.2955810738123898e-12,0.03502849760896351,8.5763845470773516e-13,0.65451994346924514,9.7107497127018634e-13,0.026271349840217464,false,false +1,cable:2:core,cable:2:core,5.6973249117713817e-13,0.0062382566531374818,5.9370349804084415e-24,NaN,5.6973249117713817e-13,0.0062382566531374818,true,false +1,cable:2:core,cable:3:core,1.2965125185608181e-12,0.035052402136222974,8.5752083547547529e-13,0.65437144225540922,9.7242106492525056e-13,0.026306808885421459,false,false +1,cable:3:core,cable:1:core,0.037900457537376649,0.006316818409456572,0.037861699896206247,0.0063153694563122999,0.0017135817786455843,0.0071709844272107295,false,false +1,cable:3:core,cable:2:core,2.5070261493371604e-13,0.0067779745659640941,9.7976735352912252e-15,0.0074765737423525668,2.5051109102772067e-13,0.0067770512518167724,false,false +1,cable:3:core,cable:3:core,0.049707018244084328,0.0054844723188977439,0.04967649169701005,0.0054830540146343755,0.0017417908584625164,0.0072068413709576278,false,false +10,cable:1:core,cable:1:core,0.050678442533818306,0.0053378719151271972,0.05061822431888352,0.0053364243324350649,0.0024697984647968907,0.0060748500213394481,false,false +10,cable:1:core,cable:2:core,2.4956610301291435e-12,0.007183136914216663,1.2689220474722158e-13,0.006274732797982188,2.4924330172904641e-12,0.0071860291534989167,false,false +10,cable:1:core,cable:3:core,0.037895491297663474,0.0067816531016595787,0.037820359964277017,0.0067844116517951844,0.0023850855045050579,0.0061793673049826909,false,false +10,cable:2:core,cable:1:core,4.2931529645634855e-11,0.1235676847369728,3.2280594866214824e-11,1.5962533533790564,2.8303346671327223e-11,0.081602463500844158,false,false +10,cable:2:core,cable:2:core,5.6975701723867369e-12,0.0062385235246741333,1.6868880972992378e-21,NaN,5.6975701723867369e-12,0.0062385235246741333,true,false +10,cable:2:core,cable:3:core,4.2914201490957232e-11,0.12352729165793205,3.2280947099337258e-11,1.5962985748954226,2.8276653691274455e-11,0.081531778596358032,false,false +10,cable:3:core,cable:1:core,0.037895491297668754,0.0067816531016605206,0.037820359964282346,0.0067844116517961368,0.0023850855045045027,0.0061793673049812614,false,false +10,cable:3:core,cable:2:core,2.468984871491344e-12,0.0071069017650019616,1.2653997162448672e-13,0.0062574241006585485,2.4657400372371787e-12,0.007109616752644001,false,false +10,cable:3:core,cable:3:core,0.05064524612586558,0.0053343940132645172,0.050585567730239589,0.0053329998734023337,0.0024579040987893763,0.0060457708699680791,false,false +100,cable:1:core,cable:1:core,0.05046776702050737,0.0049191340818300934,0.050276485284959094,0.0049163614587009732,0.0043898217990601118,0.0053292560559264918,false,false +100,cable:1:core,cable:2:core,2.474099137582113e-11,0.0079119045809081766,1.7769835293104854e-12,0.0052727200076890029,2.4677094314262667e-11,0.0079377044898874417,false,false +100,cable:1:core,cable:3:core,0.035570923043139302,0.0072352105591345283,0.035388689952341679,0.007268395978754413,0.0035959963289705632,0.0052735367584174206,false,false +100,cable:2:core,cable:1:core,1.5884578646978357e-09,0.50797184580746435,1.2447321377115562e-09,3.6934073605464826,9.868334678497824e-10,0.31742766586562993,false,false +100,cable:2:core,cable:2:core,5.6914726353389725e-11,0.0062318886673475554,6.0666530899387622e-19,NaN,5.6914726353389718e-11,0.0062318886673475545,true,false +100,cable:2:core,cable:3:core,1.5880247940234506e-09,0.50795447214884915,1.2447706198066311e-09,3.6939433396510153,9.8608764848741415e-10,0.31726387584331978,false,false +100,cable:3:core,cable:1:core,0.035570923043142057,0.0072352105591350861,0.035388689952344343,0.0072683959787549568,0.0035959963289715624,0.0052735367584188786,false,false +100,cable:3:core,cable:2:core,2.3994339083628422e-11,0.0076749631930526302,1.7385014342388727e-12,0.005159123851250608,2.3931274951896896e-11,0.0076996492723109787,false,false +100,cable:3:core,cable:3:core,0.049894989349989934,0.00486357621332503,0.049715145789065218,0.0048617369025239247,0.0042325218732570669,0.0051392749110278999,false,false +1000,cable:1:core,cable:1:core,0.051617242844457005,0.0042854973213296817,0.050402537976365736,0.0042500002015914634,0.011132112306505793,0.0052906146247933992,false,false +1000,cable:1:core,cable:2:core,2.0847367313198535e-10,0.0081284079682633365,2.5958991142928718e-11,0.0040720172135806339,2.068511618724517e-10,0.0083264611003597364,false,false +1000,cable:1:core,cable:3:core,0.030870902075776783,0.008158968085081604,0.030220284515704687,0.0084956851209608171,0.0063045220883155562,0.0048888185377315352,false,false +1000,cable:2:core,cable:1:core,6.1290416542563131e-08,2.3897190600529248,4.6484328638807526e-08,7.2916927062677734,3.9946493600317853e-08,1.6079819037415455,false,false +1000,cable:2:core,cable:2:core,5.0675402202112665e-10,0.0055525060658814631,2.6595970804234763e-16,NaN,5.0675402202105685e-10,0.0055525060658806981,true,false +1000,cable:2:core,cable:3:core,6.1290305207370033e-08,2.3898080272141291,4.6484936264979575e-08,7.2924830988609362,3.9945615690002289e-08,1.6080033898936346,false,false +1000,cable:3:core,cable:1:core,0.030870902075778087,0.0081589680850819458,0.03022028451570602,0.0084956851209611883,0.0063045220883155562,0.0048888185377315334,false,false +1000,cable:3:core,cable:2:core,2.0752752121780618e-10,0.0080918333559632325,2.5351364970922431e-11,0.0039770819417641289,2.0597325155689046e-10,0.008291415240691595,false,false +1000,cable:3:core,cable:3:core,0.051660316729683735,0.004289055860946459,0.050479363846632097,0.0042564506741989139,0.010982811582221252,0.0052200287996004317,false,false +10000,cable:1:core,cable:1:core,0.080209755488829332,0.0045484771326271649,0.070011015183638392,0.0041487264890963989,0.039141571615536108,0.00764738775335763,false,false +10000,cable:1:core,cable:2:core,1.5416532837832709e-09,0.010054594170852258,8.4400213136191747e-10,0.0074128207890390135,1.2900989301818225e-09,0.012562519238290819,false,false +10000,cable:1:core,cable:3:core,0.01880872688318418,0.010065440776585359,0.010636223235576669,0.015411826948521697,0.015512542095001525,0.008933054425747447,false,false +10000,cable:2:core,cable:1:core,2.0426017213327153e-06,13.321757607057339,7.9272124747157441e-07,6.9624238195796773,1.8825022751111041e-06,18.331129879998027,false,false +10000,cable:2:core,cable:2:core,5.232554389900984e-09,0.0057322771714669986,1.499130742170015e-13,NaN,5.2325543877534734e-09,0.0057322771691143961,true,false +10000,cable:2:core,cable:3:core,2.0425947471293261e-06,13.322207291965903,7.9272186308633675e-07,6.9624668719058178,1.8824944485403515e-06,18.332450826463432,false,false +10000,cable:3:core,cable:1:core,0.018808726883183816,0.010065440776585161,0.010636223235580555,0.015411826948527324,0.015512542094998416,0.008933054425745655,false,false +10000,cable:3:core,cable:2:core,1.5347729184896534e-09,0.010010092797384488,8.4338830738496862e-10,0.0074074696608702325,1.2822730108270883e-09,0.012487264934733721,false,false +10000,cable:3:core,cable:3:core,0.079332431832233208,0.0044989472947478187,0.069226065907585621,0.0041024026326578191,0.038747729473796788,0.0075710223811558059,false,false +100000,cable:1:core,cable:1:core,0.11477374506790895,0.0043290408174478665,0.099009693841249913,0.0039216507364199255,0.058052502808882522,0.0071727104968902699,false,false +100000,cable:1:core,cable:2:core,2.7513130363666894e-09,0.013061810300911754,1.9031534762261028e-09,0.014016558901788611,1.9868895968346588e-09,0.012338209403688681,false,false +100000,cable:1:core,cable:3:core,0.0010566935488419962,0.0095301959990094828,0.00058025937397644667,0.0069347999255450566,0.00088311964935491871,0.012138862625646092,false,false +100000,cable:2:core,cable:1:core,1.9035767976467474e-05,90.371974019044487,1.7139710735087445e-05,126.23247051749075,8.2819549848711919e-06,51.429377373587151,false,false +100000,cable:2:core,cable:2:core,5.2453944914515099e-08,0.0057463184085724947,5.5204227781494676e-11,162.32737588626711,5.2453915865149854e-08,0.0057463152262207705,false,false +100000,cable:2:core,cable:3:core,1.9035735886080225e-05,90.388429427122801,1.7139668031027508e-05,126.27186998209692,8.2819696033477532e-06,51.434137238670196,false,false +100000,cable:3:core,cable:1:core,0.0010566935488361059,0.0095301959989563621,0.00058025937397385152,0.0069347999255140423,0.00088311964934957576,0.012138862625572656,false,false +100000,cable:3:core,cable:2:core,2.7112989370241977e-09,0.012874209544178996,1.8604505626100925e-09,0.01370636648998955,1.9722742278882859e-09,0.012248562620719253,false,false +100000,cable:3:core,cable:3:core,0.11466772653085855,0.0043250564878453013,0.09895229280003548,0.0039193860636767162,0.057940756444594399,0.0071590024538767906,false,false +1000000,cable:1:core,cable:1:core,0.12511124689781913,0.0027571137750741089,0.066009530397209915,0.0016139338321431755,0.10628060028559716,0.0054071848554695613,false,false +1000000,cable:1:core,cable:2:core,1.9783294936306376e-10,0.012998459609595534,8.6170154819103346e-11,0.022762248767251927,1.7808026356713511e-10,0.012080285700149213,false,false +1000000,cable:1:core,cable:3:core,3.2167967389299072e-07,0.0085906666931782494,2.9432876430116387e-07,0.010695355428826783,1.2980135246156249e-07,0.0051115786147663492,false,false +1000000,cable:2:core,cable:1:core,9.5185530023719264e-06,625.40909965438709,7.7989128608520716e-06,2060.1192492401096,5.4570880007381014e-06,370.18803105555634,false,false +1000000,cable:2:core,cable:2:core,5.5739851691683139e-07,0.0061072455960837924,2.4729960571642033e-08,2329.3248056360894,5.5684965269918972e-07,0.0061012318582027549,false,false +1000000,cable:2:core,cable:3:core,9.5185546442339522e-06,625.27510405166106,7.7989125605606535e-06,2059.9557672623728,5.4570912937202197e-06,370.10557901702651,false,false +1000000,cable:3:core,cable:1:core,3.2167967389299379e-07,0.008590666693178331,2.9432876430116726e-07,0.010695355428826905,1.2980135246156249e-07,0.0051115786147663484,false,false +1000000,cable:3:core,cable:2:core,2.0093131244845993e-10,0.013199204290386133,8.6470446236516966e-11,0.022839760420375669,1.8137324568391629e-10,0.01230092122323303,false,false +1000000,cable:3:core,cable:3:core,0.12382098025959387,0.0027287789812792582,0.064444698275906376,0.0015757339553227132,0.10572850143917734,0.0053792470959542222,false,false +10000000,cable:1:core,cable:1:core,0.11198634605952913,0.0011440785816614484,0.024510675797429826,0.00030326729760542616,0.1092710779470849,0.0019789223395231328,false,false +10000000,cable:1:core,cable:2:core,4.0883558691564161e-13,0.71655166917326008,3.7474476432999039e-13,0.77301513096280983,1.634408111088449e-13,0.54322442269133175,false,false +10000000,cable:1:core,cable:3:core,8.0542872611412412e-12,0.99997984522328609,5.1431452864822016e-12,0.99994710598959002,6.1983546080486535e-12,1.00000238809669,false,false +10000000,cable:2:core,cable:1:core,5.5081520722050515e-10,965.39432664727224,9.2955683856547105e-11,191.74690874888827,5.4291494116862332e-10,1804.4737632292843,false,false +10000000,cable:2:core,cable:2:core,1.8585638045362502e-05,0.021126070250618127,1.6761722249932015e-05,42597.083120637399,8.02935917551084e-06,0.0091268755796968786,false,false +10000000,cable:2:core,cable:3:core,5.5083197623796477e-10,971.02459274786884,9.2969089025329706e-11,197.2282943102127,5.4292965891726901e-10,1720.3674012173349,false,false +10000000,cable:3:core,cable:1:core,8.0542872611412476e-12,0.99997984522328609,5.1431452864822072e-12,0.99994710598959002,6.1983546080486576e-12,1.00000238809669,false,false +10000000,cable:3:core,cable:2:core,4.0287315091000468e-13,0.71019794450428997,3.6133959554739029e-13,0.76656007758812472,1.7815855975451701e-13,0.56452649696966684,false,false +10000000,cable:3:core,cable:3:core,0.11376121045378251,0.0011621992327080089,0.02454629078870596,0.00030370809093797528,0.11108146835735511,0.0020116429380293093,false,false diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/Z-differences.csv b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/Z-differences.csv new file mode 100644 index 000000000..1f4dc451c --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/Z-differences.csv @@ -0,0 +1,82 @@ +frequency_hz,response_terminal,basis_terminal,absolute_error,relative_error,real_absolute_error,real_relative_error,imaginary_absolute_error,imaginary_relative_error,real_reference_near_zero,imaginary_reference_near_zero +0.10000000000000001,cable:1:core,cable:1:core,2.0375682388736233e-08,0.0060090100516075971,1.9475926187194815e-08,0.0061692467084122869,5.9880490944543177e-09,0.0048384519247632804,false,false +0.10000000000000001,cable:1:core,cable:2:core,2.7034966531228829e-09,0.0037622173631328983,1.3281554406561623e-10,0.0013475701405770779,2.7002322464375513e-09,0.0037935255704903748,false,false +0.10000000000000001,cable:1:core,cable:3:core,2.7339130405750742e-09,0.003734005367946915,1.3360618507675841e-10,0.0013510964708691358,2.7306464254340365e-09,0.0037640326265317514,false,false +0.10000000000000001,cable:2:core,cable:1:core,2.7034966531226736e-09,0.0037622173631326073,1.3281554406565594e-10,0.0013475701405774817,2.7002322464373395e-09,0.0037935255704900773,false,false +0.10000000000000001,cable:2:core,cable:2:core,2.0408470797639916e-08,0.0060194028590558422,1.9477497289690336e-08,0.006171028542585168,6.0936671740608614e-09,0.0049215685946284206,false,false +0.10000000000000001,cable:2:core,cable:3:core,2.6933489427986132e-09,0.0037480432007590903,1.32818391523371e-10,0.0013475990703233096,2.6900720812921634e-09,0.0037791977261475446,false,false +0.10000000000000001,cable:3:core,cable:1:core,2.7339130405749671e-09,0.003734005367946768,1.3360618507673194e-10,0.0013510964708688675,2.7306464254339306e-09,0.0037640326265316053,false,false +0.10000000000000001,cable:3:core,cable:2:core,2.69334894279882e-09,0.0037480432007593774,1.3281839152327835e-10,0.001347599070322368,2.6900720812923752e-09,0.0037791977261478413,false,false +0.10000000000000001,cable:3:core,cable:3:core,2.0354618950440301e-08,0.0060027516412558962,1.947592803916798e-08,0.0061692472914291795,5.9159732614992337e-09,0.0047799349720881127,false,false +1,cable:1:core,cable:1:core,5.353162848494773e-08,0.0045902079069174116,1.8086982198862461e-08,0.0044501541769718456,5.0383492566400152e-08,0.004609235432210714,false,false +1,cable:1:core,cable:2:core,1.8235677081874812e-08,0.0031632095319905572,1.3570738746829915e-09,0.001377344951695163,1.818511119386041e-08,0.0032015435459556781,false,false +1,cable:1:core,cable:3:core,1.8516037324468971e-08,0.0031414008226381077,1.375564243129563e-09,0.0013818956072247383,1.8464871004535923e-08,0.0031783727832197868,false,false +1,cable:2:core,cable:1:core,1.8235677081874031e-08,0.0031632095319904206,1.3570738746838385e-09,0.0013773449516960232,1.8185111193859563e-08,0.003201543545955528,false,false +1,cable:2:core,cable:2:core,5.4455246848209535e-08,0.0046647348682673602,1.8130844068386431e-08,0.004483445261307146,5.134828529432401e-08,0.0046889134836150138,false,false +1,cable:2:core,cable:3:core,1.8224899048895695e-08,0.0031613339346576808,1.3538703335929727e-09,0.001374089090510954,1.8174542097732642e-08,0.0031996768710727194,false,false +1,cable:3:core,cable:1:core,1.8516037324467267e-08,0.003141400822637818,1.3755642431293513e-09,0.0013818956072245252,1.8464871004534229e-08,0.0031783727832194945,false,false +1,cable:3:core,cable:2:core,1.8224899048890593e-08,0.0031613339346567939,1.3538703335925492e-09,0.001374089090510524,1.817454209772756e-08,0.0031996768710718226,false,false +1,cable:3:core,cable:3:core,5.3908611274027899e-08,0.0046226801673670001,1.8093818582681698e-08,0.0044518287252109821,5.0781414893553826e-08,0.0046458077012975262,false,false +10,cable:1:core,cable:1:core,4.0862133084201546e-07,0.0042861563423491648,1.1961252277261516e-08,0.00085002370199078949,4.0844622713774646e-07,0.0043317662345722218,false,false +10,cable:1:core,cable:2:core,9.9480292139152401e-08,0.0022845064943293734,1.2887954058400005e-08,0.0013108237495270143,9.8641923968866702e-08,0.0023252996920737341,false,false +10,cable:1:core,cable:3:core,1.0142186410137432e-07,0.002271169784242826,1.347356803469934e-08,0.0013298650487903905,1.0052292018346846e-07,0.0023113112909751649,false,false +10,cable:2:core,cable:1:core,9.9480292139179506e-08,0.0022845064943299979,1.2887954058401699e-08,0.0013108237495271869,9.8641923968893807e-08,0.0023252996920743751,false,false +10,cable:2:core,cable:2:core,4.1169942245166172e-07,0.0042932417524727956,1.3419339222137469e-08,0.0009971805940552983,4.114806627070986e-07,0.004333847198457706,false,false +10,cable:2:core,cable:3:core,9.9559619314161443e-08,0.0022863322926298967,1.2888801408649189e-08,0.0013109100458381835,9.8721814186273705e-08,0.0023271873379042906,false,false +10,cable:3:core,cable:1:core,1.0142186410134746e-07,0.0022711697842422232,1.347356803469934e-08,0.0013298650487903907,1.0052292018344135e-07,0.0023113112909745404,false,false +10,cable:3:core,cable:2:core,9.955961931413369e-08,0.0022863322926292575,1.2888801408642413e-08,0.0013109100458374935,9.8721814186246599e-08,0.0023271873379036496,false,false +10,cable:3:core,cable:3:core,4.0403880672063024e-07,0.0042378857327233267,1.2155146959884198e-08,0.00086379088174274178,4.0385592695739209e-07,0.0042828754206643942,false,false +100,cable:1:core,cable:1:core,4.1992086090216516e-06,0.0053633911681116551,7.2884246027008067e-07,0.0061643075257958452,4.1354735654080795e-06,0.0053432660831357077,false,false +100,cable:1:core,cable:2:core,3.5891820349919017e-07,0.0012090921092759823,1.1447963686357101e-07,0.0011823288944082353,3.4017155899144624e-07,0.0012122391371887104,false,false +100,cable:1:core,cable:3:core,3.672811501241812e-07,0.0012117085875665494,1.2781903188358154e-07,0.0012222673339682591,3.4432214323927134e-07,0.001210274900907369,false,false +100,cable:2:core,cable:1:core,3.5891820349920742e-07,0.0012090921092760404,1.1447963686362522e-07,0.001182328894408796,3.4017155899144624e-07,0.0012122391371887104,false,false +100,cable:2:core,cable:2:core,4.3300464155515501e-06,0.0054091786073077212,7.6842599217271973e-07,0.0076037960109097188,4.2613171033594995e-06,0.005366254754142809,false,false +100,cable:2:core,cable:3:core,3.6926994353979856e-07,0.0012440074648782961,1.1644065756975656e-07,0.00120260638511206,3.5043096961685435e-07,0.0012488453450536583,false,false +100,cable:3:core,cable:1:core,3.672811501243478e-07,0.0012117085875670995,1.278190318836222e-07,0.0012222673339686483,3.4432214323943397e-07,0.0012102749009079412,false,false +100,cable:3:core,cable:2:core,3.6926994353976849e-07,0.0012440074648781951,1.1644065756982432e-07,0.0012026063851127604,3.5043096961680014e-07,0.0012488453450534651,false,false +100,cable:3:core,cable:3:core,4.1594882632846649e-06,0.0053123808674385969,7.3216426747591608e-07,0.0061922282991187707,4.0945424772292124e-06,0.0052901010303411161,false,false +1000,cable:1:core,cable:1:core,4.9766371581957252e-05,0.0080952148868409929,2.9318203485920787e-06,0.0024746434650640536,4.9679937297434501e-05,0.0082355332020480187,false,false +1000,cable:1:core,cable:2:core,1.0533101027247158e-06,0.00063635578367444621,1.051451237672296e-06,0.0011954627313124702,6.2549718619258135e-08,4.4608120001946284e-05,false,false +1000,cable:1:core,cable:3:core,1.0582115444950828e-06,0.00065579006147741805,8.1493835093864042e-07,0.00081292884212517112,6.7504604070543067e-07,0.00053385492867976097,false,false +1000,cable:2:core,cable:1:core,1.0533101027246076e-06,0.00063635578367438072,1.0514512376721876e-06,0.0011954627313123466,6.2549718619258135e-08,4.4608120001946277e-05,false,false +1000,cable:2:core,cable:2:core,5.127377008341523e-05,0.0076943809230435277,2.6908949187563584e-06,0.0034244294807014245,5.1203111068597561e-05,0.0077377629825872716,false,false +1000,cable:2:core,cable:3:core,1.0885618833534022e-06,0.0006576606542476642,1.0867450314204236e-06,0.0012356401054018703,6.2866609364022746e-08,4.4834124761883501e-05,false,false +1000,cable:3:core,cable:1:core,1.0582115444953307e-06,0.00065579006147757158,8.1493835093842358e-07,0.00081292884212495471,6.7504604070608119e-07,0.00053385492868027521,false,false +1000,cable:3:core,cable:2:core,1.0885618833527278e-06,0.00065766065424725665,1.0867450314197731e-06,0.0012356401054011302,6.2866609363589065e-08,4.4834124761574218e-05,false,false +1000,cable:3:core,cable:3:core,4.9947781747544514e-05,0.0081249826848652489,3.0628488641487508e-06,0.0025855258435570358,4.9853784794494024e-05,0.0082645903939145039,false,false +10000,cable:1:core,cable:1:core,0.00031951048990139962,0.0072351251299446187,8.7702105008495221e-05,0.0078808938640029091,0.00030723817134938047,0.0071892368113713754,false,false +10000,cable:1:core,cable:2:core,1.3145475909202062e-05,0.0022621876764855331,1.2682001659624666e-05,0.0024134033566517023,3.4598223631696762e-06,0.0013946719356921738,false,false +10000,cable:1:core,cable:3:core,5.5189627335702372e-06,0.0012569573134158856,1.3510574808674181e-06,0.00030807460616221881,5.3510366601182294e-06,0.024943113747423036,false,false +10000,cable:2:core,cable:1:core,1.3145475909684139e-05,0.0022621876765684932,1.2682001660073959e-05,0.0024134033567372046,3.4598223633544242e-06,0.0013946719357666442,false,false +10000,cable:2:core,cable:2:core,0.00040308577748440804,0.0071043946307819683,9.5400823574957189e-05,0.018699211849232018,0.00039163353644884602,0.0069306249571960401,false,false +10000,cable:2:core,cable:3:core,1.3318984124535541e-05,0.0022920189498846929,1.2765409455065858e-05,0.0024293145410048682,3.7999552042287249e-06,0.0015315712157614827,false,false +10000,cable:3:core,cable:1:core,5.5189627335705447e-06,0.0012569573134159566,1.3510574808639486e-06,0.00030807460616142789,5.351036660119422e-06,0.02494311374742874,false,false +10000,cable:3:core,cable:2:core,1.3318984124054069e-05,0.0022920189498018368,1.2765409454616565e-05,0.0024293145409193645,3.7999552040504821e-06,0.0015315712156896409,false,false +10000,cable:3:core,cable:3:core,0.00031673451047403256,0.007171810600624876,8.7761802811591946e-05,0.0078863006023484235,0.00030433306769472868,0.0071207746185875474,false,false +100000,cable:1:core,cable:1:core,0.0016327801252331434,0.0054778925830629103,0.00038604579234202685,0.0042198001146674232,0.0015864865532274064,0.0055925104313017671,false,false +100000,cable:1:core,cable:2:core,5.4062944718432588e-06,0.00086395064956048141,1.1936276179563311e-06,0.00040849844949449271,5.2728809038261881e-06,0.0009528944979872682,false,false +100000,cable:1:core,cable:3:core,4.8846054078058608e-06,0.0033001176488430565,4.8262216662217118e-06,0.0033609421681318201,7.5296375640424018e-07,0.0020983051396989585,false,false +100000,cable:2:core,cable:1:core,5.4062944718364918e-06,0.00086395064955940067,1.1936276179563311e-06,0.00040849844949449277,5.2728809038192492e-06,0.0009528944979860153,false,false +100000,cable:2:core,cable:2:core,0.0030969737677598325,0.0060425210873081264,0.00028665671753311944,0.011585826095791351,0.0030836787194008508,0.0060236038658684852,false,false +100000,cable:2:core,cable:3:core,4.5408917919535732e-06,0.00072578892127079446,1.1595890551203622e-07,3.9699575629109124e-05,4.539410952806959e-06,0.00082045341641785979,false,false +100000,cable:3:core,cable:1:core,4.8846054078058608e-06,0.0033001176488430574,4.8262216662217118e-06,0.0033609421681318214,7.5296375640424018e-07,0.0020983051396989594,false,false +100000,cable:3:core,cable:2:core,4.540891791959621e-06,0.0007257889212717607,1.1595890551116886e-07,3.9699575628812195e-05,4.5394109528130305e-06,0.00082045341641895635,false,false +100000,cable:3:core,cable:3:core,0.0016319952670749466,0.0054752464898157127,0.00038690809045370533,0.0042292656104204376,0.0015854685999087126,0.0055889019975061916,false,false +1000000,cable:1:core,cable:1:core,0.0057623411043699439,0.0033116726712239556,0.00036630005021021095,0.00048599718950705849,0.005750686852570519,0.0036668347848890807,false,false +1000000,cable:1:core,cable:2:core,1.5931642442973085e-06,0.005515631756989766,1.5331773449724887e-06,0.0085099595041434745,4.3305835423245766e-07,0.001918122442274014,false,false +1000000,cable:1:core,cable:3:core,1.219756605359054e-08,0.0085041401903732348,1.0917328441426816e-08,0.043705413443994486,5.439904165857836e-09,0.0038515591267921693,false,false +1000000,cable:2:core,cable:1:core,1.5931642442969515e-06,0.0055156317569885283,1.5331773449725158e-06,0.0085099595041436306,4.3305835423104819e-07,0.0019181224422677697,false,false +1000000,cable:2:core,cable:2:core,0.027655859230498691,0.0056253563463567035,0.00159247343618861,0.017104309886540308,0.027609972439902819,0.0056170300535694617,false,false +1000000,cable:2:core,cable:3:core,1.6273053668582831e-06,0.0056327182711444768,1.5512834738509187e-06,0.0086095930116932007,4.9157129774102682e-07,0.0021767265867867012,false,false +1000000,cable:3:core,cable:1:core,1.2197566053589217e-08,0.0085041401903723241,1.0917328441426287e-08,0.043705413443992466,5.4399041658559301e-09,0.0038515591267908249,false,false +1000000,cable:3:core,cable:2:core,1.6273053668584082e-06,0.0056327182711449165,1.5512834738506205e-06,0.0086095930116915527,4.9157129774238207e-07,0.0021767265867927046,false,false +1000000,cable:3:core,cable:3:core,0.0057063753848006615,0.0032793894824094515,0.00033275395260268237,0.00044146946814843359,0.0056966652384782268,0.0036322636375048199,false,false +10000000,cable:1:core,cable:1:core,0.010950726075362412,0.0013575652703185375,0.002131239760707615,0.00046839755427902823,0.010741332257220293,0.0016126519482520712,false,false +10000000,cable:1:core,cable:2:core,4.1294769849572631e-09,0.7361132176669769,4.065075689683595e-09,0.72553624989608345,7.2645702311642297e-10,2.5962297414101219,false,false +10000000,cable:1:core,cable:3:core,6.6373284392578382e-13,0.99997984432131481,2.0910964967395046e-13,0.99989784271617177,6.2993209357759082e-13,0.99998888169712496,false,false +10000000,cable:2:core,cable:1:core,4.129476984957259e-09,0.73611321766697624,4.06507568968359e-09,0.72553624989608267,7.2645702311642473e-10,2.596229741410125,false,false +10000000,cable:2:core,cable:2:core,1.7071487533845759,0.033255716944887444,0.72223088058047868,1.3647077617792789,1.5468482218105493,0.030134620905801032,false,false +10000000,cable:2:core,cable:3:core,4.0457443342348382e-09,0.72912828774712768,4.0102625343629026e-09,0.72282462302201367,5.346415841276498e-10,6.0756890601225173,false,false +10000000,cable:3:core,cable:1:core,6.6373284392578322e-13,0.99997984432131481,2.0910964967395034e-13,0.99989784271617177,6.2993209357759022e-13,0.99998888169712496,false,false +10000000,cable:3:core,cable:2:core,4.0457443342348332e-09,0.7291282877471279,4.0102625343628976e-09,0.72282462302201378,5.3464158412765052e-10,6.0756890601223672,false,false +10000000,cable:3:core,cable:3:core,0.011096944194357663,0.0013757060123651213,0.0021872689187807381,0.00048070555711141586,0.010879247452357355,0.0016333916930201945,false,false diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/fem/Y.csv b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/fem/Y.csv new file mode 100644 index 000000000..ab37e6c67 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/fem/Y.csv @@ -0,0 +1,82 @@ +frequency_hz,response_terminal,basis_terminal,real_S_per_m,imaginary_S_per_m +0.10000000000000001,cable:1:core,cable:1:core,8.7845877913155999,0.16072503167449181 +0.10000000000000001,cable:1:core,cable:2:core,-9.0062134384187483e-14,-3.8506433401169452e-12 +0.10000000000000001,cable:1:core,cable:3:core,-6.2698668441098802,0.16036739685172985 +0.10000000000000001,cable:2:core,cable:1:core,-9.0062134384187546e-14,-3.8506433401169419e-12 +0.10000000000000001,cable:2:core,cable:2:core,2.4947508256144476e-24,9.1342496320507243e-12 +0.10000000000000001,cable:2:core,cable:3:core,-9.0049454962162604e-14,-3.8504700965498247e-12 +0.10000000000000001,cable:3:core,cable:1:core,-6.2698668441098802,0.16036739685172893 +0.10000000000000001,cable:3:core,cable:2:core,-9.0049454962163198e-14,-3.8504700965498231e-12 +0.10000000000000001,cable:3:core,cable:3:core,8.784235974623094,0.16067971187912994 +1,cable:1:core,cable:1:core,9.0597588683067194,0.24164199336163972 +1,cable:1:core,cable:2:core,-1.3103320429960825e-12,-3.6963269005067141e-11 +1,cable:1:core,cable:3:core,-5.9951678453843966,0.23896046575464569 +1,cable:2:core,cable:1:core,-1.3103320429960805e-12,-3.6963269005067161e-11 +1,cable:2:core,cable:2:core,2.4083147716325383e-22,9.1328799511734699e-11 +1,cable:2:core,cable:3:core,-1.310449662228344e-12,-3.6964615098722244e-11 +1,cable:3:core,cable:1:core,-5.9951678453843975,0.2389604657546453 +1,cable:3:core,cable:2:core,-1.3104496622283438e-12,-3.6964615098722231e-11 +1,cable:3:core,cable:3:core,9.060004071530674,0.24168574952705965 +10,cable:1:core,cable:1:core,9.4854196678519962,0.40656122474153245 +10,cable:1:core,cable:2:core,-2.022272642239479e-11,-3.4684426740418731e-10 +10,cable:1:core,cable:3:core,-5.5745968707941813,0.38597568113192759 +10,cable:2:core,cable:1:core,-2.0222726422394722e-11,-3.4684426740418726e-10 +10,cable:2:core,cable:2:core,2.2848695838332346e-20,9.1328824037484855e-10 +10,cable:2:core,cable:3:core,-2.0222374189272243e-11,-3.4681757442413444e-10 +10,cable:3:core,cable:1:core,-5.574596870794184,0.38597568113192704 +10,cable:3:core,cable:2:core,-2.0222374189272117e-11,-3.4681757442413371e-10 +10,cable:3:core,cable:3:core,9.4853870112633505,0.40654933037552476 +100,cable:1:core,cable:1:core,10.22636063424096,0.82372131363032075 +100,cable:1:core,cable:2:core,-3.3701458198409536e-10,-3.1088451762976369e-09 +100,cable:1:core,cable:3:core,-4.8688445230258699,0.68189461716195865 +100,cable:2:core,cable:1:core,-3.3701458198409598e-10,-3.1088451762976394e-09 +100,cable:2:core,cable:2:core,2.1048044610369403e-18,9.1328214272502451e-09 +100,cable:2:core,cable:3:core,-3.3697609988902282e-10,-3.1080993569352719e-09 +100,cable:3:core,cable:1:core,-4.8688445230258717,0.68189461716195954 +100,cable:3:core,cable:2:core,-3.3697609988902432e-10,-3.1080993569352732e-09 +100,cable:3:core,cable:3:core,10.225799294745068,0.82356401370451804 +1000,cable:1:core,cable:1:core,11.859420137790089,2.1041245858916642 +1000,cable:1:core,cable:2:core,-6.3749708759463428e-09,-2.4842626342602489e-08 +1000,cable:1:core,cable:3:core,-3.5571333077239724,1.2895798933132263 +1000,cable:2:core,cable:1:core,-6.3749708759463561e-09,-2.4842626342602513e-08 +1000,cable:2:core,cable:2:core,1.8282901688147461e-16,9.1265820515709644e-08 +1000,cable:2:core,cable:3:core,-6.3743632497743299e-09,-2.4841748432286945e-08 +1000,cable:3:core,cable:1:core,-3.5571333077239737,1.2895798933132268 +1000,cable:3:core,cable:2:core,-6.374363249774339e-09,-2.4841748432286941e-08 +1000,cable:3:core,cable:3:core,11.859496963660357,2.1039752851673796 +10000,cable:1:core,cable:1:core,16.875302666406174,5.1182930535137015 +10000,cable:1:core,cable:2:core,-1.1385708023724294e-07,-1.0269428493685997e-07 +10000,cable:1:core,cable:3:core,-0.69013383495049541,1.7365328090121379 +10000,cable:2:core,cable:1:core,-1.1385708023724289e-07,-1.0269428493685991e-07 +10000,cable:2:core,cable:2:core,1.2129066978187954e-14,9.1282299047690203e-07 +10000,cable:2:core,cable:3:core,-1.1385646462248044e-07,-1.0268645836610756e-07 +10000,cable:3:core,cable:1:core,-0.69013383495049552,1.7365328090121384 +10000,cable:3:core,cable:2:core,-1.1385646462248041e-07,-1.026864583661075e-07 +10000,cable:3:core,cable:3:core,16.874517717130121,5.1178992113719639 +100000,cable:1:core,cable:1:core,25.246943314395164,8.093523757030372 +100000,cable:1:core,cable:2:core,-1.3577893758098125e-07,1.6103548998290223e-07 +100000,cable:1:core,cable:3:core,0.083673556585100742,-0.072751432863992438 +100000,cable:2:core,cable:1:core,-1.3577893758098135e-07,1.6103548998290223e-07 +100000,cable:2:core,cable:2:core,3.4007959212112756e-13,9.1282698216414564e-06 +100000,cable:2:core,cable:3:core,-1.3573623352103367e-07,1.6102087150634743e-07 +100000,cable:3:core,cable:1:core,0.083673556585100728,-0.07275143286399241 +100000,cable:3:core,cable:2:core,-1.357362335210337e-07,1.6102087150634751e-07 +100000,cable:3:core,cable:3:core,25.246885913353953,8.0934120106660856 +1000000,cable:1:core,cable:1:core,40.899774874633195,19.655440515981347 +1000000,cable:1:core,cable:2:core,-3.7856608852758189e-09,-1.4741395028839053e-08 +1000000,cable:1:core,cable:3:core,-2.7519306512046415e-05,-2.5393594081990994e-05 +1000000,cable:2:core,cable:1:core,-3.7856608852758205e-09,-1.4741395028839069e-08 +1000000,cable:2:core,cable:2:core,1.0616793549703688e-11,9.1268397209087773e-05 +1000000,cable:2:core,cable:3:core,-3.7859611766932275e-09,-1.4744688010955839e-08 +1000000,cable:3:core,cable:1:core,-2.7519306512046418e-05,-2.5393594081990998e-05 +1000000,cable:3:core,cable:2:core,-3.7859611766932317e-09,-1.4744688010955837e-08 +1000000,cable:3:core,cable:3:core,40.898210042511892,19.654888417134927 +10000000,cable:1:core,cable:1:core,80.822020676031087,55.21746647896061 +10000000,cable:1:core,cable:2:core,-4.8478322004284232e-13,-3.0087161821462218e-13 +10000000,cable:1:core,cable:3:core,-5.1434173424526559e-12,-6.19833980581388e-12 +10000000,cable:2:core,cable:1:core,-4.8478322004284232e-13,-3.0087161821462192e-13 +10000000,cable:2:core,cable:2:core,3.9349460155433293e-10,0.00087974894643819731 +10000000,cable:2:core,cable:3:core,-4.7137805126024283e-13,-3.1558936686029454e-13 +10000000,cable:3:core,cable:1:core,-5.1434173424526615e-12,-6.1983398058138841e-12 +10000000,cable:3:core,cable:2:core,-4.7137805126024222e-13,-3.1558936686029429e-13 +10000000,cable:3:core,cable:3:core,80.821985061039811,55.21927686937088 diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/fem/Z.csv b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/fem/Z.csv new file mode 100644 index 000000000..225523893 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/fem/Z.csv @@ -0,0 +1,82 @@ +frequency_hz,response_terminal,basis_terminal,real_ohm_per_m,imaginary_ohm_per_m +0.10000000000000001,cable:1:core,cable:1:core,3.1569374848695469e-06,1.2375960715466406e-06 +0.10000000000000001,cable:1:core,cable:2:core,9.8559280935640084e-08,7.1180019648279383e-07 +0.10000000000000001,cable:1:core,cable:3:core,9.8887228230869388e-08,7.254576929504737e-07 +0.10000000000000001,cable:2:core,cable:1:core,9.8559280935640018e-08,7.1180019648279383e-07 +0.10000000000000001,cable:2:core,cable:2:core,3.1562805382084363e-06,1.238155489839502e-06 +0.10000000000000001,cable:2:core,cable:3:core,9.8559278088182289e-08,7.118103566479389e-07 +0.10000000000000001,cable:3:core,cable:1:core,9.8887228230869441e-08,7.2545769295047381e-07 +0.10000000000000001,cable:3:core,cable:2:core,9.8559278088182408e-08,7.11810356647939e-07 +0.10000000000000001,cable:3:core,cable:3:core,3.1569374867215201e-06,1.2376681473795957e-06 +1,cable:1:core,cable:1:core,4.0643495662368124e-06,1.0930986995002525e-05 +1,cable:1:core,cable:2:core,9.8528249805016315e-07,5.6801074022037254e-06 +1,cable:1:core,cable:3:core,9.9541834849023757e-07,5.809535968222851e-06 +1,cable:2:core,cable:1:core,9.8528249805016272e-07,5.6801074022037271e-06 +1,cable:2:core,cable:2:core,4.0439534803421229e-06,1.0950998663924164e-05 +1,cable:2:core,cable:3:core,9.8528570159125359e-07,5.6801179712998549e-06 +1,cable:3:core,cable:1:core,9.9541834849023778e-07,5.8095359682228527e-06 +1,cable:3:core,cable:2:core,9.8528570159125359e-07,5.6801179712998583e-06 +1,cable:3:core,cable:3:core,4.0643564026206317e-06,1.0930589072675371e-05 +10,cable:1:core,cable:1:core,1.4071669118458445e-05,9.4290920843765721e-05 +10,cable:1:core,cable:2:core,9.8319503770437314e-06,4.2421165884598937e-05 +10,cable:1:core,cable:3:core,1.0131530298472416e-05,4.3491727218213369e-05 +10,cable:2:core,cable:1:core,9.831950377043728e-06,4.2421165884598896e-05 +10,cable:2:core,cable:2:core,1.3457280759510354e-05,9.4945816929939975e-05 +10,cable:2:core,cable:3:core,9.8319495296934822e-06,4.2421085994381516e-05 +10,cable:3:core,cable:1:core,1.0131530298472415e-05,4.349172721821339e-05 +10,cable:3:core,cable:2:core,9.8319495296934873e-06,4.242108599438155e-05 +10,cable:3:core,cable:3:core,1.4071863013141068e-05,9.4295511143946075e-05 +100,cable:1:core,cable:1:core,0.00011823590195980419,0.00077395987792192587 +100,cable:1:core,cable:2:core,9.6825542710659164e-05,0.0002806142357194754 +100,cable:1:core,cable:3:core,0.00010457534806939449,0.00028449911915146356 +100,cable:2:core,cable:1:core,9.6825542710659096e-05,0.0002806142357194754 +100,cable:2:core,cable:2:core,0.00010105820712052285,0.00079409519275426034 +100,cable:2:core,cable:3:core,9.6823581689952951e-05,0.00028060397630884999 +100,cable:3:core,cable:1:core,0.00010457534806939445,0.00028449911915146345 +100,cable:3:core,cable:2:core,9.682358168995291e-05,0.00028060397630884999 +100,cable:3:core,cable:3:core,0.00011823922376701001,0.00077400080901010473 +1000,cable:1:core,cable:1:core,0.0011847445460254178,0.0060323886843270886 +1000,cable:1:core,cable:2:core,0.00087953493666668525,0.0014022047693677529 +1000,cable:1:core,cable:3:core,0.0010024719369142027,0.0012644746811176577 +1000,cable:2:core,cable:1:core,0.00087953493666668547,0.0014022047693677531 +1000,cable:2:core,cable:2:core,0.00078579364356049853,0.006617301561681695 +1000,cable:2:core,cable:3:core,0.00087949964287293734,0.0014022044524770085 +1000,cable:3:core,cable:1:core,0.0010024719369142029,0.0012644746811176582 +1000,cable:3:core,cable:2:core,0.00087949964287293778,0.0014022044524770085 +1000,cable:3:core,cable:3:core,0.0011846135175098611,0.0060322148368300316 +10000,cable:1:core,cable:1:core,0.011128446407467421,0.042735853528070605 +10000,cable:1:core,cable:2:core,0.0052548205937773158,0.0024807428002432495 +10000,cable:1:core,cable:3:core,0.0043854879754549108,-0.00021452961784576972 +10000,cable:2:core,cable:1:core,0.0052548205937773132,0.0024807428002432538 +10000,cable:2:core,cable:2:core,0.0051018633482606017,0.056507679879895176 +10000,cable:2:core,cable:3:core,0.0052547371859823222,0.002481082933084129 +10000,cable:3:core,cable:1:core,0.0043854879754549082,-0.0002145296178457685 +10000,cable:3:core,cable:2:core,0.0052547371859823248,0.0024810829330841307 +10000,cable:3:core,cable:3:core,0.011128386709664324,0.042738758631725257 +100000,cable:1:core,cable:1:core,0.091484378845383393,0.28368057113451289 +100000,cable:1:core,cable:2:core,0.0029219881236597529,-0.0055335411369923137 +100000,cable:1:core,cable:3:core,-0.0014359728388020337,-0.00035884378404194684 +100000,cable:2:core,cable:1:core,0.0029219881236597525,-0.0055335411369923076 +100000,cable:2:core,cable:2:core,0.024742017976365958,0.51193252213577378 +100000,cable:2:core,cable:3:core,0.0029209104549473086,-0.0055328076670412953 +100000,cable:3:core,cable:1:core,-0.0014359728388020332,-0.00035884378404194673 +100000,cable:3:core,cable:2:core,0.0029209104549473073,-0.0055328076670413005 +100000,cable:3:core,cable:3:core,0.091483516547271729,0.28368158908783159 +1000000,cable:1:core,cable:1:core,0.75370816564133836,1.5682972345165187 +1000000,cable:1:core,cable:2:core,0.00018016270749890044,0.00022577200740065841 +1000000,cable:1:core,cable:3:core,2.4979350568131865e-07,1.4123901481913753e-06 +1000000,cable:2:core,cable:1:core,0.00018016270749890033,0.00022577200740065857 +1000000,cable:2:core,cable:2:core,0.093103635677330446,4.9154040794845795 +1000000,cable:2:core,cable:3:core,0.00018018081362777873,0.0002258305203441686 +1000000,cable:3:core,cable:1:core,2.4979350568131807e-07,1.4123901481913734e-06 +1000000,cable:3:core,cable:2:core,0.00018018081362777857,0.00022583052034416836 +1000000,cable:3:core,cable:3:core,0.75374171173894589,1.568351256130611 +10000000,cable:1:core,cable:1:core,4.5500659455579013,6.6606636781499313 +10000000,cable:1:core,cable:2:core,5.6028567701004943e-09,-2.7981230302132413e-10 +10000000,cable:1:core,cable:3:core,2.0913101393029784e-13,6.2993909743126889e-13 +10000000,cable:2:core,cable:1:core,5.6028567701004935e-09,-2.7981230302132444e-10 +10000000,cable:2:core,cable:2:core,0.52922017504967389,51.33126534579285 +10000000,cable:2:core,cable:3:core,5.5480436147798052e-09,-8.7996864032549524e-11 +10000000,cable:3:core,cable:1:core,2.0913101393029771e-13,6.2993909743126828e-13 +10000000,cable:3:core,cable:2:core,5.5480436147797969e-09,-8.7996864032551824e-11 +10000000,cable:3:core,cable:3:core,4.5501219747159745,6.6605257629547943 diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/fem/computation-options.txt b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/fem/computation-options.txt new file mode 100644 index 000000000..987cdbcf4 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/fem/computation-options.txt @@ -0,0 +1 @@ +(ui = false, mesh_policy = :remesh, resume_run_directory = nothing, keep_run_directory = true, trace = true, output_basis = :pul, verbosity = (default = 1,), gmsh_verbosity = 2, getdp_verbosity = 4, frequency_workers = 2, solver_threads = 4, plot_field_maps = false) \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/fem/details.txt b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/fem/details.txt new file mode 100644 index 000000000..db42bc593 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/fem/details.txt @@ -0,0 +1 @@ +(files = [(path = "coordinator.lock", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/coordinator.lock", sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"), (path = "run.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/run.json", sha256 = "6ccac2fce367b28b758052f4ff2707b8f9b4e7fc686d5563dd35ace4e4fee501"), (path = "timing-summary.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/timing-summary.json", sha256 = "f343d06fbc9ddf36d0fcfe102ce80bf4dec7ac6e63928a89fba5c854b204441c"), (path = "attempts/f0001-wIKlX1/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0001-wIKlX1/attempt.json", sha256 = "8ffc34a2c657d1603a33656b9f692a2002f6e256b4c08a3f991cecfde3f19bc2"), (path = "attempts/f0001-wIKlX1/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0001-wIKlX1/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0001-wIKlX1/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0001-wIKlX1/getdp.log", sha256 = "3920b4627dcb0bb5e56c6d6ace5f0ca374d50ed4b7d169f3a44fbfab147763e8"), (path = "attempts/f0001-wIKlX1/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0001-wIKlX1/petsc.log", sha256 = "9eec988b40434579ceae4cb2b67f4a9775944aefc83920098ec85d6d228c8f97"), (path = "attempts/f0001-wIKlX1/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0001-wIKlX1/solver.pre", sha256 = "4de61cb288ae912fea193d8f0c287231605b9b48e5205c2761614468a738da14"), (path = "attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0001-P.tsv", sha256 = "7d980e540ffc0d69b8a01ce50a20f9c2384fd0b8b853dca31a5b06e83f520b9e"), (path = "attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0001-Z.tsv", sha256 = "a60321da47854a4de01adf1302c5484074c0f99019815a86782ede78567ecefe"), (path = "attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0001-timing.tsv", sha256 = "e1643b1ba2c46404858c3eb8e98689afc9e6b9d2fafd72fe0311748d06bda95c"), (path = "attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0001.done", sha256 = "a40a11bab26b32dfb169550347f289dc710df24f59fe3ef20963d46f838f0586"), (path = "attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0002-P.tsv", sha256 = "3878f7365c6e2189dac0f8c2410a3eb1ac3a34f18beff5ceeba3f6a6e39468df"), (path = "attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0002-Z.tsv", sha256 = "dafccab8e16a3eca4c6d50faa6465f1c41afd133f4bc391cfc8b5a9218527563"), (path = "attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0002-timing.tsv", sha256 = "155998fff9ed1ac43fa05c89be8d293d6e2fa328af71d40df4038e6940446621"), (path = "attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0002.done", sha256 = "f58042f034284ccf2955f2795037937a5bd0e4288ffc0d0d5124e33ee07f17ec"), (path = "attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0003-P.tsv", sha256 = "ccb871cfde604349db3c02150d10c3ebd59ef4881bee1e88069eb90ecc41901d"), (path = "attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0003-Z.tsv", sha256 = "ddfaee4bc77e5f93bfbc12e512dcd6e16569d1629f1bb2034b6d86ddf959612d"), (path = "attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0003-timing.tsv", sha256 = "b96bb7171be1563c0b51fba2bb0105edad1eae81ec5264be0cb83c0a547cd349"), (path = "attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0003.done", sha256 = "41695cf0039479bd9b703276e3c15366a77a1e43f8490b48634d8a1c2a61be94"), (path = "attempts/f0002-TpcYhy/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0002-TpcYhy/attempt.json", sha256 = "1135f7e33f8114643aabb99e10816978cad9a477e25bfbd5228622196ba93ae9"), (path = "attempts/f0002-TpcYhy/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0002-TpcYhy/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0002-TpcYhy/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0002-TpcYhy/getdp.log", sha256 = "62d37c1273f0edda6e8602402e5056fca8e65c7414cbdde71e0653a56f8db5f4"), (path = "attempts/f0002-TpcYhy/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0002-TpcYhy/petsc.log", sha256 = "0ffb0e3bc68c35ea297495571943855520789cd3f68b06e06fc2c1d909d63bba"), (path = "attempts/f0002-TpcYhy/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0002-TpcYhy/solver.pre", sha256 = "1873f4f4d5f23b6c5b618ebf57aee3f091de54f7d7420197b056425ca0690304"), (path = "attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0001-P.tsv", sha256 = "fce2e98f6924961dbc7ba69bda3cc8f1b155ab410b3568b5ec0598e855e592f2"), (path = "attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0001-Z.tsv", sha256 = "90bff2c9e6d258f000771cee22280260765eb10e70c8fbbe687e8b347ee40672"), (path = "attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0001-timing.tsv", sha256 = "995de5e3ee5fc49a777944b7bf5f84b036e76ad5e18c66be5e16c8d153e29c2a"), (path = "attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0001.done", sha256 = "3dbd9539f415ee889a34b9c6efe6b9cb8168b7c0945d52d7e63e920c8990d2d1"), (path = "attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0002-P.tsv", sha256 = "3dd0638b6d29e28921b8858c441c5c460b08df57630ae236f89c9c374f570f6e"), (path = "attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0002-Z.tsv", sha256 = "ff4740658f7674d842f0acc95da5db093b113640ee3bcdd90d96c3e0559737ee"), (path = "attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0002-timing.tsv", sha256 = "d17188b3665d42413b7623534ba89c42d32432fcfc16829d5eb4ad79b1924e0a"), (path = "attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0002.done", sha256 = "d498381e814531161686031fcde5aa549b456c6cbee61f6eb67916650247e717"), (path = "attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0003-P.tsv", sha256 = "4957bca5b6df0e65d10bdba3ffae367150f6fa4983ac62802bec745f7c15bc6f"), (path = "attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0003-Z.tsv", sha256 = "07ee32261f3bb9eef7216fbfd4e2ea776dad0991e60f61f20016dbe611e1af91"), (path = "attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0003-timing.tsv", sha256 = "992c15097975e80aae67549bf9495f400f4245d848e087dee908b4d440823ee1"), (path = "attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0003.done", sha256 = "fa5c0c361441906d1aaa6b254ae52969806be93c36846f4c70c70a18dc833050"), (path = "attempts/f0003-IRSVRX/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0003-IRSVRX/attempt.json", sha256 = "f2e4e938efe35a0ae0691e6e1d0dc3621e069573ef2262839bc2e6e16f2853b9"), (path = "attempts/f0003-IRSVRX/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0003-IRSVRX/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0003-IRSVRX/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0003-IRSVRX/getdp.log", sha256 = "f7eaf989d58a79e3410265489b26b7a9f7ad0a9662d11ec005b620968683145b"), (path = "attempts/f0003-IRSVRX/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0003-IRSVRX/petsc.log", sha256 = "63e3a04f3714ebfc45ffef611703acd6c91c64aedd241478670230928d6c30e6"), (path = "attempts/f0003-IRSVRX/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0003-IRSVRX/solver.pre", sha256 = "09a7fe8f033fae8b5e56b402574544e6b17d6f9235732a684b909c3c0bbac658"), (path = "attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0001-P.tsv", sha256 = "4780b7d3613e2ecf7009d3162721dee65244ff349e0c953dde6bd98b6493c982"), (path = "attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0001-Z.tsv", sha256 = "49eaab99e9366305eeb287b5bce041292939552446e3b0da868535b4037a07b3"), (path = "attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0001-timing.tsv", sha256 = "de7860b3937f4d04d88d4f9c891bfbe934497dad27b11c3ebc60f35cda826cb3"), (path = "attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0001.done", sha256 = "02395cb824ac1c8ce1d2e8cd3c57fd19d5c904215aef49b1fba677c51e99d519"), (path = "attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0002-P.tsv", sha256 = "2acde5f3e22c83d5a2117aeab125f6688344b966b4533eb2b9653b187ff89b8a"), (path = "attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0002-Z.tsv", sha256 = "f2ca43ed615d68deda8c36683541ee2c0c1054a68849793cb8b144b2b5c4aea2"), (path = "attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0002-timing.tsv", sha256 = "44c217e12064774e7898c48b2e6f2ac0a16621eb6047a86d02815579e3022246"), (path = "attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0002.done", sha256 = "84034583a44a97df0ec5820a76fc891b95a9b6bc0bf693bed9882420a3eb5b05"), (path = "attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0003-P.tsv", sha256 = "d51e8f7d7f1934c97401418472e49664decb7fb133714fdb8008124a16ce504a"), (path = "attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0003-Z.tsv", sha256 = "8356831df7703227ea286922c104f484b094397d6b8fec8e077f89f346434f63"), (path = "attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0003-timing.tsv", sha256 = "13583ff00a362bbc4cca6cf8349b54d9188b9b5ae448c67c4989783a4fe23717"), (path = "attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0003.done", sha256 = "aec9dd1bab4102d3c07f9c6abf83e4d604721961d8e37cbbc9620f7f71cafcae"), (path = "attempts/f0004-pcls4E/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0004-pcls4E/attempt.json", sha256 = "340e1e75e87c9622566120a79e6a3a219edc57447b2c719e2cf76efc54fd4cf0"), (path = "attempts/f0004-pcls4E/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0004-pcls4E/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0004-pcls4E/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0004-pcls4E/getdp.log", sha256 = "33b8a9d314492b97ed549413d4c288b597c64c8d1248806787b7175791009a2f"), (path = "attempts/f0004-pcls4E/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0004-pcls4E/petsc.log", sha256 = "2005b67ef5e7d0126699426a1852d31c63622a92ac8c0f7b0f64f15be64c95fa"), (path = "attempts/f0004-pcls4E/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0004-pcls4E/solver.pre", sha256 = "8d371fb7497e747a76ce284178f93dd49f259020d771bbb62d77945b892ae74f"), (path = "attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0001-P.tsv", sha256 = "84ace93ddfec0c78d6ce2c44565f9121516f1fe44b37040e187a1872766b09c1"), (path = "attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0001-Z.tsv", sha256 = "3a29730bee1b706e082476f111cd28165e1e953224bc7fd7e40df469d406be58"), (path = "attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0001-timing.tsv", sha256 = "90d73e48cf47f6bcf0070e6199360b6c4ae81b38a40b31479b0bec863ad4263c"), (path = "attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0001.done", sha256 = "05b52afc2d2a0ffefd0bb6e40585165d1b2572547d59d3c360b4163202709006"), (path = "attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0002-P.tsv", sha256 = "cf379f24762455ef6473ddd54bbc34aaf7459881f24a3cb1faa15aba188238f4"), (path = "attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0002-Z.tsv", sha256 = "005d8eef7269f9f7045285ec648e6bf9bde498cd701e4e4593dfca55d23fd40e"), (path = "attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0002-timing.tsv", sha256 = "79b8a131cb7b172981d96d303422d5ffed3dd695774472233526bfd35400c88b"), (path = "attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0002.done", sha256 = "35d216e20aed80a36327575b350d8e4c12b70f2038f5d07d55614c92afdd6b6b"), (path = "attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0003-P.tsv", sha256 = "dfcdc833dc6aed949f370429d1b3d7a5f3d7bdd6a6e7ef990087da3dca21ce06"), (path = "attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0003-Z.tsv", sha256 = "2381d18ea5f5b76798b442ca4850e93139bda0df20ce619808efbb84b5b3b10b"), (path = "attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0003-timing.tsv", sha256 = "9714b737c4fe9821963919edccec2cd8dced3ca9415528095927469b04b5329e"), (path = "attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0003.done", sha256 = "eae2a23f21e24dd158a817c096e6e77f32e1d2a850c4163d279c3531eef2af5a"), (path = "attempts/f0005-gCunzX/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0005-gCunzX/attempt.json", sha256 = "59e2ce183be8590a3b72758c7ed3b59a3292ba877adbdceaf2462c0630b2ce06"), (path = "attempts/f0005-gCunzX/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0005-gCunzX/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0005-gCunzX/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0005-gCunzX/getdp.log", sha256 = "60135aa1f9070c972edc56c5d935f4635ef479363a62c450e2f764a6a1a99b23"), (path = "attempts/f0005-gCunzX/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0005-gCunzX/petsc.log", sha256 = "453849fb5486a7f3d0e0950f8e8d0c3de09da6811081db1fa163148a1cb048f8"), (path = "attempts/f0005-gCunzX/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0005-gCunzX/solver.pre", sha256 = "4a8e23396875d3cce5a9d3d1b1696d7ab02b23bdc6c89c268366a71e97763e69"), (path = "attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0001-P.tsv", sha256 = "55df259870ee17c16a34bb902dda01fe2530f0498b52bfe3ea8a9b6f778cdbca"), (path = "attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0001-Z.tsv", sha256 = "86378d5d3cec39828bcc40dd1bd5d3a1d9a21882b544305c52499cf89640c141"), (path = "attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0001-timing.tsv", sha256 = "14af070450a972fcfd86c8268c1829a6a1ab6fc4e646e186c956530d16779422"), (path = "attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0001.done", sha256 = "ee7bf9802d600055d484ec4e501666418037b219c7bf910eefda35e16a843b08"), (path = "attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0002-P.tsv", sha256 = "a568f37204ffc8ed8ed5f557bbb268d5681062dc5d8414917cf6979857d596cf"), (path = "attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0002-Z.tsv", sha256 = "04af27f87cc684e46970f4b2428c13962bd374d7b0d67d642e491bb52e82e086"), (path = "attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0002-timing.tsv", sha256 = "45f874a67ba112a4febd5d0f11095a41f9fdf9e9359b0fe51d98015adc67ece3"), (path = "attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0002.done", sha256 = "e26c3631aaa36c47efb3ede81242002dbebf27057f428fe57f60849cede6b3f3"), (path = "attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0003-P.tsv", sha256 = "668a14b8dc4b7796c62c5e268433c50d5be9fa4cdcfece871155a7ff03cb8c08"), (path = "attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0003-Z.tsv", sha256 = "6dc17e1c0f6e63b477b8793998657fff64d52625493a62ce831d065d9d9551e2"), (path = "attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0003-timing.tsv", sha256 = "86f21c1b3e1e4144d82ddec8a9f5e924fd307423ac92af08405f5bbd594eb457"), (path = "attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0003.done", sha256 = "a91d7ca1bb120ef43c71df371c45a71b1cc7744f1ee2eb082338700de7f329c9"), (path = "attempts/f0006-EwzCMZ/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0006-EwzCMZ/attempt.json", sha256 = "08c8e562f87b4cd2579f10aaccb71378da3ecb850118faf60798d8814e26719d"), (path = "attempts/f0006-EwzCMZ/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0006-EwzCMZ/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0006-EwzCMZ/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0006-EwzCMZ/getdp.log", sha256 = "37c301495c9b0330573ccbd6bf32f35b4f5052c7308203335d1db5dcefa3878b"), (path = "attempts/f0006-EwzCMZ/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0006-EwzCMZ/petsc.log", sha256 = "a80d9abc9be1926073797b9515161f872732304dfd0ff8989248b7710f3695e6"), (path = "attempts/f0006-EwzCMZ/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0006-EwzCMZ/solver.pre", sha256 = "442b0137d30e93881664918e4537310e4a98f6b63a8f708c37308ef2e7b436d9"), (path = "attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0001-P.tsv", sha256 = "2fb3008802b1d2ac39765be77153e220b19e23062049b04b5d47459c613634c8"), (path = "attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0001-Z.tsv", sha256 = "9453c3ff01f761480899a795c29756b1423f516d4ceb9b4476919fb62e8ec002"), (path = "attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0001-timing.tsv", sha256 = "00e0c6f61aae65addc3d5c5ae15ab5c49679767c97fddd33912f65eb9c0cd8c7"), (path = "attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0001.done", sha256 = "e41a78288341a2ae156cd28b6102b3a31a74b012eb4deb233e88fae392912e8e"), (path = "attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0002-P.tsv", sha256 = "c23bb0da77a088f37c99f48b772f3c70a52d3eb69447747980219ef8f2a95378"), (path = "attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0002-Z.tsv", sha256 = "f90e6d92872970317c1211037aaabff139245277fcdcf7bde4e3218c0476f31d"), (path = "attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0002-timing.tsv", sha256 = "b00502f7403b818c10d12352ecf46b6a85991b472ed15dd22ca9589d4cc1ff32"), (path = "attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0002.done", sha256 = "c105367e4bac19858725b1b7c56efbb25ae370173d002f9a678a4762a5c69ac9"), (path = "attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0003-P.tsv", sha256 = "e4a6fb0e91437f3aa7aa37bcfea7abe60c60556cc092d29b48c50ed1087447b2"), (path = "attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0003-Z.tsv", sha256 = "73cd0efa6d4b2f933300d062566e0e12a16a4839d1048cb760d19d24428b8afb"), (path = "attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0003-timing.tsv", sha256 = "02b2820d165c40db53599b1333a58b06669158c48d0d2dfc40f471313bbedbb9"), (path = "attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0003.done", sha256 = "de6214d7f2cf6d2097d94d720289a7424da0b9bf7a5df27326d8b69e936798e8"), (path = "attempts/f0007-hio5iQ/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0007-hio5iQ/attempt.json", sha256 = "4065585146851781aae076cc46590cd24d5f74a3ab1bec01e6da340eb376b675"), (path = "attempts/f0007-hio5iQ/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0007-hio5iQ/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0007-hio5iQ/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0007-hio5iQ/getdp.log", sha256 = "447506c34122184cc5267826b6a6b93059e17da7efb25860e29683043288f34c"), (path = "attempts/f0007-hio5iQ/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0007-hio5iQ/petsc.log", sha256 = "a22d83a5ee7323394aa8b3dbebbe1fb400818b7f3345a9c6ecc7e27d2bfb9356"), (path = "attempts/f0007-hio5iQ/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0007-hio5iQ/solver.pre", sha256 = "93e92e0f6ae3a3ff783d9a34f2db39ba0a46bf7d221802ce5bf4fd5aa3cf5801"), (path = "attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0001-P.tsv", sha256 = "6ed9911e6ea19c2229cb9e290324c9eace399847ea135f96b4e9268a46cc8c76"), (path = "attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0001-Z.tsv", sha256 = "08bb2afeddfa7316e645ffdca79212a9c5708cf87852490504d2a9c31d1ef122"), (path = "attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0001-timing.tsv", sha256 = "01faa96a1ff80b58b174a64dcd69d609e0b0fa406e344fcd5cf53a8e908c208f"), (path = "attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0001.done", sha256 = "d95e4d651bb7fa68e36e0077af5a10e10baacf852173da7ac0f1cca3e414dc7f"), (path = "attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0002-P.tsv", sha256 = "51b6b281d2bce47291e6bf34ab43881edf769f99fac63e114d1e2c94d7f753dd"), (path = "attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0002-Z.tsv", sha256 = "704052c9b5d87ce6dd2b6836ce45cfaa963861c477b14cf64bd0b3fea6a117e7"), (path = "attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0002-timing.tsv", sha256 = "123c272fbf0d8a4a7ddea34dca16848cb88423aec9cd6e1928488c2c553ea59c"), (path = "attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0002.done", sha256 = "a953202777b2aea0282f4e5ecac9feb227f850cb3f80f2247ca07a708f3635be"), (path = "attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0003-P.tsv", sha256 = "2f2f768c634d762747b69afafb1645be63c5cc543030f75b662d062c65905e38"), (path = "attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0003-Z.tsv", sha256 = "5e4d322a375529f978a9260a22e0d5ec1639016c1146a9da62097b62586690ce"), (path = "attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0003-timing.tsv", sha256 = "bcf9fc7261155c3892108e52b94c6af81ef8b4d5c6ae0a101637803018d7ea87"), (path = "attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0003.done", sha256 = "bc6e3e671a966700354c8fb7f9ab0ff830db8a4b88332e0ec4974ce29df37b0c"), (path = "attempts/f0008-t5WGBB/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0008-t5WGBB/attempt.json", sha256 = "3e8cb67e6b1296c72ed3a752ae312cd7620fcd8c8e4af11a076bf3b0bcb5f928"), (path = "attempts/f0008-t5WGBB/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0008-t5WGBB/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0008-t5WGBB/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0008-t5WGBB/getdp.log", sha256 = "76f2b36fdb329f57e3fe69658f31d6b2b6e1c2e059bc8977d784f0d9bf3312cb"), (path = "attempts/f0008-t5WGBB/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0008-t5WGBB/petsc.log", sha256 = "a5076fc0b59e1486a0fbef232f9d5ba858e6a58d5044a373eaa84b9b28fa0d2e"), (path = "attempts/f0008-t5WGBB/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0008-t5WGBB/solver.pre", sha256 = "68563937aff9272cf07564c292ffd5a85c5f09a0aa8f19e461b6499e6b99e499"), (path = "attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0001-P.tsv", sha256 = "b5cb24e313a2f7ad2c154f50f1b100f700f145a08193a8f8edadfcb03aeafb5a"), (path = "attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0001-Z.tsv", sha256 = "04007b40d90923d74d88fbe86aa3243f6c15f2b63e7fd9373cafe4095703ae99"), (path = "attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0001-timing.tsv", sha256 = "524d5374da5579aea44825b686434e557d441d01c7202e7c97a168af9814b4aa"), (path = "attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0001.done", sha256 = "c03ca6db0131d3218478fb4e85f0350dca26787b9ffdfc9d50b5d68ad93b352c"), (path = "attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0002-P.tsv", sha256 = "c743dcb9a8d47c165a365c8621615125550fb98794496d4b4d0c84a1f306e3ed"), (path = "attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0002-Z.tsv", sha256 = "5b7b6059261bc6e81a491d8fe9609d68103852e96d6fffe28788fc53ded06cc8"), (path = "attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0002-timing.tsv", sha256 = "8b611c675d54531e81f98bc4878696f1ea13e0252f1788ff4bb20148db65d185"), (path = "attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0002.done", sha256 = "76f50b528daca79ab1eac47c47f96ca1e7b8ad94bcaefbaac04335941306f993"), (path = "attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0003-P.tsv", sha256 = "358654a68195014400ca3e9eee9602ddd01b0a70be3f69f4cd09069fabad5cbb"), (path = "attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0003-Z.tsv", sha256 = "d3cb527f9cdf2bc56449a3d84cbbad30fb01b406c725b963626d9c4e9ae87e05"), (path = "attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0003-timing.tsv", sha256 = "3cb2a99be1b94193012284f026bca4b6194c763d0957c795198c3a2ae3caf35d"), (path = "attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0003.done", sha256 = "20bdbe7d9351850f55ebf49a31f3a93235d04aea44253ddd2d75828a83fa1c74"), (path = "attempts/f0009-UbLL6z/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0009-UbLL6z/attempt.json", sha256 = "62d6b0111a4cfc26672debeb26f27f358367ae152ca9764b3234cf3326bdd75c"), (path = "attempts/f0009-UbLL6z/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0009-UbLL6z/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0009-UbLL6z/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0009-UbLL6z/getdp.log", sha256 = "a1585bba376a6931ba2a108413e2ebe7e45b0d4377d9ffb2fa72964c2c1363fc"), (path = "attempts/f0009-UbLL6z/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0009-UbLL6z/petsc.log", sha256 = "8af0b26522f72972cdf8719032ceaa3439d9f526005f5f6a16abca76dd3e39fd"), (path = "attempts/f0009-UbLL6z/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0009-UbLL6z/solver.pre", sha256 = "8dce5cba3a71c61b00f97453b23afbfefe610961c99b77a5497acd6908df3b98"), (path = "attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0001-P.tsv", sha256 = "312177b45ff96145802d4f94cdc5ae6b2723548f4274cdf5ae676611b2acc29c"), (path = "attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0001-Z.tsv", sha256 = "778105611da0cfce5d3635ef28e9345ffc0bfb6533fe7c4652a8b366ae0b5b78"), (path = "attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0001-timing.tsv", sha256 = "9064552b387a3175b04931d1317af1d031edc577f67679e86f54d0d8e8be82d1"), (path = "attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0001.done", sha256 = "22b8b831af1067ac03eaf9570ddd71235af29f6b65e559199200376f6ef90f2a"), (path = "attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0002-P.tsv", sha256 = "006824828b9b6c1f38aaf5b741364c615cca3bfbf5a3d12e68edea88adcad768"), (path = "attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0002-Z.tsv", sha256 = "266b76ed64e522873ad8394b605ccaef34f3514ef9ad60a5f6740ff3d6872efe"), (path = "attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0002-timing.tsv", sha256 = "81802d95fb3639ae62510ff48ef13aa7e9e804468da053efe0c655098c865c8c"), (path = "attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0002.done", sha256 = "2ab6b278377a8a5090d5d456a9193028844db99b8f3fa96c2433d70920e73c22"), (path = "attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0003-P.tsv", sha256 = "fab6f65e74720b6fdfb3f3531dbd815893bae9bfc02f9b6d8d30a3c923479cb5"), (path = "attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0003-Z.tsv", sha256 = "08a5327a9f57103a1514ad2c72fb477f0276ef272e38f8c02fe26bcffab612f2"), (path = "attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0003-timing.tsv", sha256 = "ebadfdacd79f8ac1b12044bab9d9b0d7dbe9998669849c4f40247e243c2e669d"), (path = "attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0003.done", sha256 = "3449a1ca026b39cca413695b5bd20e176addf6bcf3103f9147e8ac56407cafa3"), (path = "input/computation.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/input/computation.json", sha256 = "dd5de6278edac71742d8e453785458604cdf76b1dd98005d8c9e1f8b5caf2913"), (path = "input/model_data.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/input/model_data.pro", sha256 = "0c18ecf5e16df8e6a270e25c2d3eefd3e4a55f63853b8e71e6b3e7d15ff3f601"), (path = "input/problem.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/input/problem.json", sha256 = "eb8dc8ed237ca7801e89dd3cad47f6608e200fdc6347e4c9616b187188cf7d26"), (path = "input/getdp/integration.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/input/getdp/integration.pro", sha256 = "e113951ac1e6e658f1be50e53875e30c85497445f0e6584a8a7ab1848588e902"), (path = "input/getdp/jacobian.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/input/getdp/jacobian.pro", sha256 = "014208d39043ab13183c42c312aba459265715da6d6ddc38ec1fbc39b6090c73"), (path = "input/getdp/materials.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/input/getdp/materials.pro", sha256 = "581f089113810a379d8a49c03d79f55045b723c8f4c761d0a493a019ff1f5d9e"), (path = "input/getdp/model.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/input/getdp/model.pro", sha256 = "c0bb424db5ab4db9ca22f549f675edb4646f69910d2aa14e20cf4f40e41bd313"), (path = "input/getdp/quasi-full.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/input/getdp/quasi-full.pro", sha256 = "1aede35114ea686c5f109089478027d241dec12dc9fd14a24452b348f89b08e4"), (path = "input/getdp/quasi-tem.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/input/getdp/quasi-tem.pro", sha256 = "448d9019ad417c9a784da20170b855dd25e559297499a83d19b007ee095c084d"), (path = "logs/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/logs/getdp.log", sha256 = "846f6e9d19204a75253ce69eddab2e8b4efb9d97e7ae24856e040ab21aa3687c"), (path = "mesh/frequency_0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/mesh/frequency_0001.json", sha256 = "2aafaedebb99d2c04c9d6f5fa91f698b4f2da954d17c07120444514cde17ed10"), (path = "mesh/frequency_0001.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/mesh/frequency_0001.msh", sha256 = "523f6a1b18ec7c704e752a41f1c57de8cea183a5bf8adbc8098b0bb03981db1f"), (path = "mesh/frequency_0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/mesh/frequency_0002.json", sha256 = "2cee8da71a88f95911e8371be7785de8a11c9d27835e4872a19b50987e7454ac"), (path = "mesh/frequency_0002.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/mesh/frequency_0002.msh", sha256 = "ca1fee0e46f16320f9df32da5d0a906d7860363517781c2805f51ab74b8b5c4c"), (path = "mesh/frequency_0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/mesh/frequency_0003.json", sha256 = "78753bb179a7841e89b8a057e95440187cc29ade86cc57d6cb2e4ac578df99f0"), (path = "mesh/frequency_0003.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/mesh/frequency_0003.msh", sha256 = "c8f05f998bd8f08132c488f160a79b65ddcca6497fcc95a9e88bd58908645bfd"), (path = "mesh/frequency_0004.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/mesh/frequency_0004.json", sha256 = "c69519464a75ed4981046c70eddd02db3f9ae7c04f1d0006332a80dabe679a77"), (path = "mesh/frequency_0004.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/mesh/frequency_0004.msh", sha256 = "e397de871f12985d3bdf9fda433ae9755e47bde482d58ed8a130197dec9d159b"), (path = "mesh/frequency_0005.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/mesh/frequency_0005.json", sha256 = "114155df68cef682e8514c1bd475b5fe8d8448b13bf3c2e42d84be17c10bd415"), (path = "mesh/frequency_0005.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/mesh/frequency_0005.msh", sha256 = "9e95d3f9518cd95bb44831ce3c1cc0dc8209fb1eb269b8ebf622c4220a1aa95e"), (path = "mesh/frequency_0006.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/mesh/frequency_0006.json", sha256 = "0ce5ce9a7177373270e5b0ba26f264663b2d7613591866b764dca6bccec046fb"), (path = "mesh/frequency_0006.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/mesh/frequency_0006.msh", sha256 = "c6a1f5e574f7389a71a203d4552b893a43b222a7b30e92f9efa049bf9143da69"), (path = "mesh/frequency_0007.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/mesh/frequency_0007.json", sha256 = "2fc252ab17d8f2aaf29f8592f8b8a573f73004c0df7fe4347e34c32ae1cf179e"), (path = "mesh/frequency_0007.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/mesh/frequency_0007.msh", sha256 = "0d91340b682c4bc4f0a8752a7f630194bf5abbc1a5de0188e5dc0d39e6806f96"), (path = "mesh/frequency_0008.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/mesh/frequency_0008.json", sha256 = "8249f710d6ef747cc795c5f7754c42f376950fabf141a8fb20ccc76c59a7633c"), (path = "mesh/frequency_0008.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/mesh/frequency_0008.msh", sha256 = "92cdb4e8c7eba6145f3fd058115b9cd795cd4ee7675aff9fe2fdbf971b45a5d2"), (path = "mesh/model.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/mesh/model.json", sha256 = "32ec746031275855de3faac843fde733a26a33bd5a4ea93dc2f4d90dfec0002e"), (path = "mesh/model.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/mesh/model.msh", sha256 = "6dbee7f8e8814cb0a01b6ae79017c08ad9de6c60c0d78d0a85c3e101278f4037"), (path = "raw/P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/P.tsv", sha256 = "5e9299b565761e34d0e9a2eca291eceab25bf7d06a448fe39e7073a72d1ce998"), (path = "raw/Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/Z.tsv", sha256 = "f40979d07e7d2f0e03c5fe71a1ffefc4a5198dc0744be971f6cedda11940d011"), (path = "raw/checksums.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/checksums.json", sha256 = "f27157dde9627d8705487c695dd5f6bf6ee5cc328fc228c7f1f4cc97bfe5c1a8"), (path = "raw/scan_complete.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/scan_complete.tsv", sha256 = "2cc9eff50bb7e6fff0e2555f96d93ebe952c4a1a158a1dca82fe6cca3aa1afab"), (path = "raw/jobs/getdp-f0001-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0001-b0001-P.tsv", sha256 = "7d980e540ffc0d69b8a01ce50a20f9c2384fd0b8b853dca31a5b06e83f520b9e"), (path = "raw/jobs/getdp-f0001-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0001-b0001-Z.tsv", sha256 = "a60321da47854a4de01adf1302c5484074c0f99019815a86782ede78567ecefe"), (path = "raw/jobs/getdp-f0001-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0001-b0001-timing.tsv", sha256 = "e1643b1ba2c46404858c3eb8e98689afc9e6b9d2fafd72fe0311748d06bda95c"), (path = "raw/jobs/getdp-f0001-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0001-b0001.json", sha256 = "3122b383c3693801c4bbdf6ba54a2f9bf14c2c1e835b41a1f937e7e7b6e5445e"), (path = "raw/jobs/getdp-f0001-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0001-b0002-P.tsv", sha256 = "3878f7365c6e2189dac0f8c2410a3eb1ac3a34f18beff5ceeba3f6a6e39468df"), (path = "raw/jobs/getdp-f0001-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0001-b0002-Z.tsv", sha256 = "dafccab8e16a3eca4c6d50faa6465f1c41afd133f4bc391cfc8b5a9218527563"), (path = "raw/jobs/getdp-f0001-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0001-b0002-timing.tsv", sha256 = "155998fff9ed1ac43fa05c89be8d293d6e2fa328af71d40df4038e6940446621"), (path = "raw/jobs/getdp-f0001-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0001-b0002.json", sha256 = "3dc94ec563963cf252056fe110d65861cda452658f7a378ce9bba51fd8394d68"), (path = "raw/jobs/getdp-f0001-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0001-b0003-P.tsv", sha256 = "ccb871cfde604349db3c02150d10c3ebd59ef4881bee1e88069eb90ecc41901d"), (path = "raw/jobs/getdp-f0001-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0001-b0003-Z.tsv", sha256 = "ddfaee4bc77e5f93bfbc12e512dcd6e16569d1629f1bb2034b6d86ddf959612d"), (path = "raw/jobs/getdp-f0001-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0001-b0003-timing.tsv", sha256 = "b96bb7171be1563c0b51fba2bb0105edad1eae81ec5264be0cb83c0a547cd349"), (path = "raw/jobs/getdp-f0001-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0001-b0003.json", sha256 = "eb4f2939e95ac9a472f11c414bfb73fee30e6116de101ee1b9f674029ce38494"), (path = "raw/jobs/getdp-f0002-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0002-b0001-P.tsv", sha256 = "fce2e98f6924961dbc7ba69bda3cc8f1b155ab410b3568b5ec0598e855e592f2"), (path = "raw/jobs/getdp-f0002-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0002-b0001-Z.tsv", sha256 = "90bff2c9e6d258f000771cee22280260765eb10e70c8fbbe687e8b347ee40672"), (path = "raw/jobs/getdp-f0002-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0002-b0001-timing.tsv", sha256 = "995de5e3ee5fc49a777944b7bf5f84b036e76ad5e18c66be5e16c8d153e29c2a"), (path = "raw/jobs/getdp-f0002-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0002-b0001.json", sha256 = "a4de680148cd182ad51750147ceeb9b87e1897588b79c5c3fc96434a8fbe1dfd"), (path = "raw/jobs/getdp-f0002-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0002-b0002-P.tsv", sha256 = "3dd0638b6d29e28921b8858c441c5c460b08df57630ae236f89c9c374f570f6e"), (path = "raw/jobs/getdp-f0002-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0002-b0002-Z.tsv", sha256 = "ff4740658f7674d842f0acc95da5db093b113640ee3bcdd90d96c3e0559737ee"), (path = "raw/jobs/getdp-f0002-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0002-b0002-timing.tsv", sha256 = "d17188b3665d42413b7623534ba89c42d32432fcfc16829d5eb4ad79b1924e0a"), (path = "raw/jobs/getdp-f0002-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0002-b0002.json", sha256 = "64a5d6fbc0c885a95da41cd43968c57f0b29489dd6147047f863331febe8eb8e"), (path = "raw/jobs/getdp-f0002-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0002-b0003-P.tsv", sha256 = "4957bca5b6df0e65d10bdba3ffae367150f6fa4983ac62802bec745f7c15bc6f"), (path = "raw/jobs/getdp-f0002-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0002-b0003-Z.tsv", sha256 = "07ee32261f3bb9eef7216fbfd4e2ea776dad0991e60f61f20016dbe611e1af91"), (path = "raw/jobs/getdp-f0002-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0002-b0003-timing.tsv", sha256 = "992c15097975e80aae67549bf9495f400f4245d848e087dee908b4d440823ee1"), (path = "raw/jobs/getdp-f0002-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0002-b0003.json", sha256 = "a3539278bd2b32461c2cdc27e8fba51d922b70ecea2f9b19e7cbf6f18a628e47"), (path = "raw/jobs/getdp-f0003-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0003-b0001-P.tsv", sha256 = "4780b7d3613e2ecf7009d3162721dee65244ff349e0c953dde6bd98b6493c982"), (path = "raw/jobs/getdp-f0003-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0003-b0001-Z.tsv", sha256 = "49eaab99e9366305eeb287b5bce041292939552446e3b0da868535b4037a07b3"), (path = "raw/jobs/getdp-f0003-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0003-b0001-timing.tsv", sha256 = "de7860b3937f4d04d88d4f9c891bfbe934497dad27b11c3ebc60f35cda826cb3"), (path = "raw/jobs/getdp-f0003-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0003-b0001.json", sha256 = "32f715d70b58d03a550afe9769244d98d5dab8c587ad7f2081c69cdd8d3006a4"), (path = "raw/jobs/getdp-f0003-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0003-b0002-P.tsv", sha256 = "2acde5f3e22c83d5a2117aeab125f6688344b966b4533eb2b9653b187ff89b8a"), (path = "raw/jobs/getdp-f0003-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0003-b0002-Z.tsv", sha256 = "f2ca43ed615d68deda8c36683541ee2c0c1054a68849793cb8b144b2b5c4aea2"), (path = "raw/jobs/getdp-f0003-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0003-b0002-timing.tsv", sha256 = "44c217e12064774e7898c48b2e6f2ac0a16621eb6047a86d02815579e3022246"), (path = "raw/jobs/getdp-f0003-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0003-b0002.json", sha256 = "79dc3371cd8656e10ab6fbfc1537c327ede11d48703e8bcc8e1dbda5b7c1a8dd"), (path = "raw/jobs/getdp-f0003-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0003-b0003-P.tsv", sha256 = "d51e8f7d7f1934c97401418472e49664decb7fb133714fdb8008124a16ce504a"), (path = "raw/jobs/getdp-f0003-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0003-b0003-Z.tsv", sha256 = "8356831df7703227ea286922c104f484b094397d6b8fec8e077f89f346434f63"), (path = "raw/jobs/getdp-f0003-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0003-b0003-timing.tsv", sha256 = "13583ff00a362bbc4cca6cf8349b54d9188b9b5ae448c67c4989783a4fe23717"), (path = "raw/jobs/getdp-f0003-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0003-b0003.json", sha256 = "4eba223e6599332394311e337bcb3d617a582d4005554302677e2750d9215fdb"), (path = "raw/jobs/getdp-f0004-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0004-b0001-P.tsv", sha256 = "84ace93ddfec0c78d6ce2c44565f9121516f1fe44b37040e187a1872766b09c1"), (path = "raw/jobs/getdp-f0004-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0004-b0001-Z.tsv", sha256 = "3a29730bee1b706e082476f111cd28165e1e953224bc7fd7e40df469d406be58"), (path = "raw/jobs/getdp-f0004-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0004-b0001-timing.tsv", sha256 = "90d73e48cf47f6bcf0070e6199360b6c4ae81b38a40b31479b0bec863ad4263c"), (path = "raw/jobs/getdp-f0004-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0004-b0001.json", sha256 = "347ecc55acdd3b5411397d6577aaa873489a6730c9d7024acd4da66e6301b71c"), (path = "raw/jobs/getdp-f0004-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0004-b0002-P.tsv", sha256 = "cf379f24762455ef6473ddd54bbc34aaf7459881f24a3cb1faa15aba188238f4"), (path = "raw/jobs/getdp-f0004-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0004-b0002-Z.tsv", sha256 = "005d8eef7269f9f7045285ec648e6bf9bde498cd701e4e4593dfca55d23fd40e"), (path = "raw/jobs/getdp-f0004-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0004-b0002-timing.tsv", sha256 = "79b8a131cb7b172981d96d303422d5ffed3dd695774472233526bfd35400c88b"), (path = "raw/jobs/getdp-f0004-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0004-b0002.json", sha256 = "0a83cf7da4f7fe7e5d1fb8093135396b35e4c84a14e9f665bac335dd9f6166e3"), (path = "raw/jobs/getdp-f0004-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0004-b0003-P.tsv", sha256 = "dfcdc833dc6aed949f370429d1b3d7a5f3d7bdd6a6e7ef990087da3dca21ce06"), (path = "raw/jobs/getdp-f0004-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0004-b0003-Z.tsv", sha256 = "2381d18ea5f5b76798b442ca4850e93139bda0df20ce619808efbb84b5b3b10b"), (path = "raw/jobs/getdp-f0004-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0004-b0003-timing.tsv", sha256 = "9714b737c4fe9821963919edccec2cd8dced3ca9415528095927469b04b5329e"), (path = "raw/jobs/getdp-f0004-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0004-b0003.json", sha256 = "ad24b5782ba04b62702fda9d7c3f7874df909b4f16557f15412178db435f6ac1"), (path = "raw/jobs/getdp-f0005-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0005-b0001-P.tsv", sha256 = "55df259870ee17c16a34bb902dda01fe2530f0498b52bfe3ea8a9b6f778cdbca"), (path = "raw/jobs/getdp-f0005-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0005-b0001-Z.tsv", sha256 = "86378d5d3cec39828bcc40dd1bd5d3a1d9a21882b544305c52499cf89640c141"), (path = "raw/jobs/getdp-f0005-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0005-b0001-timing.tsv", sha256 = "14af070450a972fcfd86c8268c1829a6a1ab6fc4e646e186c956530d16779422"), (path = "raw/jobs/getdp-f0005-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0005-b0001.json", sha256 = "1b754b8008ba2dffbabb08c9f808d50d60ffc0292ecf40f25f06a3eec14c505a"), (path = "raw/jobs/getdp-f0005-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0005-b0002-P.tsv", sha256 = "a568f37204ffc8ed8ed5f557bbb268d5681062dc5d8414917cf6979857d596cf"), (path = "raw/jobs/getdp-f0005-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0005-b0002-Z.tsv", sha256 = "04af27f87cc684e46970f4b2428c13962bd374d7b0d67d642e491bb52e82e086"), (path = "raw/jobs/getdp-f0005-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0005-b0002-timing.tsv", sha256 = "45f874a67ba112a4febd5d0f11095a41f9fdf9e9359b0fe51d98015adc67ece3"), (path = "raw/jobs/getdp-f0005-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0005-b0002.json", sha256 = "c7031a6872994aa0b82f5bd44d4d594b7ca82c99b7a6395b356e2cb1fd982ea4"), (path = "raw/jobs/getdp-f0005-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0005-b0003-P.tsv", sha256 = "668a14b8dc4b7796c62c5e268433c50d5be9fa4cdcfece871155a7ff03cb8c08"), (path = "raw/jobs/getdp-f0005-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0005-b0003-Z.tsv", sha256 = "6dc17e1c0f6e63b477b8793998657fff64d52625493a62ce831d065d9d9551e2"), (path = "raw/jobs/getdp-f0005-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0005-b0003-timing.tsv", sha256 = "86f21c1b3e1e4144d82ddec8a9f5e924fd307423ac92af08405f5bbd594eb457"), (path = "raw/jobs/getdp-f0005-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0005-b0003.json", sha256 = "22a7153411224930b2f75f413beabe6fe1920c8262a38a7a0125685210195194"), (path = "raw/jobs/getdp-f0006-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0006-b0001-P.tsv", sha256 = "2fb3008802b1d2ac39765be77153e220b19e23062049b04b5d47459c613634c8"), (path = "raw/jobs/getdp-f0006-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0006-b0001-Z.tsv", sha256 = "9453c3ff01f761480899a795c29756b1423f516d4ceb9b4476919fb62e8ec002"), (path = "raw/jobs/getdp-f0006-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0006-b0001-timing.tsv", sha256 = "00e0c6f61aae65addc3d5c5ae15ab5c49679767c97fddd33912f65eb9c0cd8c7"), (path = "raw/jobs/getdp-f0006-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0006-b0001.json", sha256 = "378dda8d0dcb0a9a39750d5cfaa093c115fc717143aba917acbfba3b862a871d"), (path = "raw/jobs/getdp-f0006-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0006-b0002-P.tsv", sha256 = "c23bb0da77a088f37c99f48b772f3c70a52d3eb69447747980219ef8f2a95378"), (path = "raw/jobs/getdp-f0006-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0006-b0002-Z.tsv", sha256 = "f90e6d92872970317c1211037aaabff139245277fcdcf7bde4e3218c0476f31d"), (path = "raw/jobs/getdp-f0006-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0006-b0002-timing.tsv", sha256 = "b00502f7403b818c10d12352ecf46b6a85991b472ed15dd22ca9589d4cc1ff32"), (path = "raw/jobs/getdp-f0006-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0006-b0002.json", sha256 = "9dcb78a80b9a8122bc0f1e9c6584ede60f5f49eda06771d0bc7872fb2e59d2fc"), (path = "raw/jobs/getdp-f0006-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0006-b0003-P.tsv", sha256 = "e4a6fb0e91437f3aa7aa37bcfea7abe60c60556cc092d29b48c50ed1087447b2"), (path = "raw/jobs/getdp-f0006-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0006-b0003-Z.tsv", sha256 = "73cd0efa6d4b2f933300d062566e0e12a16a4839d1048cb760d19d24428b8afb"), (path = "raw/jobs/getdp-f0006-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0006-b0003-timing.tsv", sha256 = "02b2820d165c40db53599b1333a58b06669158c48d0d2dfc40f471313bbedbb9"), (path = "raw/jobs/getdp-f0006-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0006-b0003.json", sha256 = "da2645477facdac6754fd11269499cf925639ec5f7b779ed621b2c4302ed7144"), (path = "raw/jobs/getdp-f0007-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0007-b0001-P.tsv", sha256 = "6ed9911e6ea19c2229cb9e290324c9eace399847ea135f96b4e9268a46cc8c76"), (path = "raw/jobs/getdp-f0007-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0007-b0001-Z.tsv", sha256 = "08bb2afeddfa7316e645ffdca79212a9c5708cf87852490504d2a9c31d1ef122"), (path = "raw/jobs/getdp-f0007-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0007-b0001-timing.tsv", sha256 = "01faa96a1ff80b58b174a64dcd69d609e0b0fa406e344fcd5cf53a8e908c208f"), (path = "raw/jobs/getdp-f0007-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0007-b0001.json", sha256 = "638c83f33344a7b17a2604c6905ca7774d636825c88f7d84623cd5763af2920f"), (path = "raw/jobs/getdp-f0007-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0007-b0002-P.tsv", sha256 = "51b6b281d2bce47291e6bf34ab43881edf769f99fac63e114d1e2c94d7f753dd"), (path = "raw/jobs/getdp-f0007-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0007-b0002-Z.tsv", sha256 = "704052c9b5d87ce6dd2b6836ce45cfaa963861c477b14cf64bd0b3fea6a117e7"), (path = "raw/jobs/getdp-f0007-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0007-b0002-timing.tsv", sha256 = "123c272fbf0d8a4a7ddea34dca16848cb88423aec9cd6e1928488c2c553ea59c"), (path = "raw/jobs/getdp-f0007-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0007-b0002.json", sha256 = "f40f39496d7dc4f4335242c16cd1c17b1b3dbab8040c969ce94ec714f53d0442"), (path = "raw/jobs/getdp-f0007-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0007-b0003-P.tsv", sha256 = "2f2f768c634d762747b69afafb1645be63c5cc543030f75b662d062c65905e38"), (path = "raw/jobs/getdp-f0007-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0007-b0003-Z.tsv", sha256 = "5e4d322a375529f978a9260a22e0d5ec1639016c1146a9da62097b62586690ce"), (path = "raw/jobs/getdp-f0007-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0007-b0003-timing.tsv", sha256 = "bcf9fc7261155c3892108e52b94c6af81ef8b4d5c6ae0a101637803018d7ea87"), (path = "raw/jobs/getdp-f0007-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0007-b0003.json", sha256 = "bf5296d3cc4a5bfb92bc1a1d446ac1936e3553f5ae4f76a34fbc76e56ea5fabc"), (path = "raw/jobs/getdp-f0008-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0008-b0001-P.tsv", sha256 = "b5cb24e313a2f7ad2c154f50f1b100f700f145a08193a8f8edadfcb03aeafb5a"), (path = "raw/jobs/getdp-f0008-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0008-b0001-Z.tsv", sha256 = "04007b40d90923d74d88fbe86aa3243f6c15f2b63e7fd9373cafe4095703ae99"), (path = "raw/jobs/getdp-f0008-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0008-b0001-timing.tsv", sha256 = "524d5374da5579aea44825b686434e557d441d01c7202e7c97a168af9814b4aa"), (path = "raw/jobs/getdp-f0008-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0008-b0001.json", sha256 = "4620f880637d8bb9389c9986ebb943b3f9346824063a1e3521346e90f937a438"), (path = "raw/jobs/getdp-f0008-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0008-b0002-P.tsv", sha256 = "c743dcb9a8d47c165a365c8621615125550fb98794496d4b4d0c84a1f306e3ed"), (path = "raw/jobs/getdp-f0008-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0008-b0002-Z.tsv", sha256 = "5b7b6059261bc6e81a491d8fe9609d68103852e96d6fffe28788fc53ded06cc8"), (path = "raw/jobs/getdp-f0008-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0008-b0002-timing.tsv", sha256 = "8b611c675d54531e81f98bc4878696f1ea13e0252f1788ff4bb20148db65d185"), (path = "raw/jobs/getdp-f0008-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0008-b0002.json", sha256 = "228dba3f84f93a38f332bd249576afbab7c11d7ac423db2b2a7001cd59b55073"), (path = "raw/jobs/getdp-f0008-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0008-b0003-P.tsv", sha256 = "358654a68195014400ca3e9eee9602ddd01b0a70be3f69f4cd09069fabad5cbb"), (path = "raw/jobs/getdp-f0008-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0008-b0003-Z.tsv", sha256 = "d3cb527f9cdf2bc56449a3d84cbbad30fb01b406c725b963626d9c4e9ae87e05"), (path = "raw/jobs/getdp-f0008-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0008-b0003-timing.tsv", sha256 = "3cb2a99be1b94193012284f026bca4b6194c763d0957c795198c3a2ae3caf35d"), (path = "raw/jobs/getdp-f0008-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0008-b0003.json", sha256 = "642881f409c10ffc19f1a9300dd6cffa6264a2b15c006ec48e1da02f619ee0ea"), (path = "raw/jobs/getdp-f0009-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0009-b0001-P.tsv", sha256 = "312177b45ff96145802d4f94cdc5ae6b2723548f4274cdf5ae676611b2acc29c"), (path = "raw/jobs/getdp-f0009-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0009-b0001-Z.tsv", sha256 = "778105611da0cfce5d3635ef28e9345ffc0bfb6533fe7c4652a8b366ae0b5b78"), (path = "raw/jobs/getdp-f0009-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0009-b0001-timing.tsv", sha256 = "9064552b387a3175b04931d1317af1d031edc577f67679e86f54d0d8e8be82d1"), (path = "raw/jobs/getdp-f0009-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0009-b0001.json", sha256 = "5d3a35bd65fa9aa1cb3e10171d284bc40e6fedb38c3e8a9e2ea25a9a6797365d"), (path = "raw/jobs/getdp-f0009-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0009-b0002-P.tsv", sha256 = "006824828b9b6c1f38aaf5b741364c615cca3bfbf5a3d12e68edea88adcad768"), (path = "raw/jobs/getdp-f0009-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0009-b0002-Z.tsv", sha256 = "266b76ed64e522873ad8394b605ccaef34f3514ef9ad60a5f6740ff3d6872efe"), (path = "raw/jobs/getdp-f0009-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0009-b0002-timing.tsv", sha256 = "81802d95fb3639ae62510ff48ef13aa7e9e804468da053efe0c655098c865c8c"), (path = "raw/jobs/getdp-f0009-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0009-b0002.json", sha256 = "9e526454e3879ef5ae561155924abcae2c1e30794b60f0c5c32c83884463712b"), (path = "raw/jobs/getdp-f0009-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0009-b0003-P.tsv", sha256 = "fab6f65e74720b6fdfb3f3531dbd815893bae9bfc02f9b6d8d30a3c923479cb5"), (path = "raw/jobs/getdp-f0009-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0009-b0003-Z.tsv", sha256 = "08a5327a9f57103a1514ad2c72fb477f0276ef272e38f8c02fe26bcffab612f2"), (path = "raw/jobs/getdp-f0009-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0009-b0003-timing.tsv", sha256 = "ebadfdacd79f8ac1b12044bab9d9b0d7dbe9998669849c4f40247e243c2e669d"), (path = "raw/jobs/getdp-f0009-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO/raw/jobs/getdp-f0009-b0003.json", sha256 = "3abe3cec70b548cdef2c7e71ed35eeab48ec8a6366db4913a38c77de6732b167")], coordinates = ["cable:1:core", "cable:2:core", "cable:3:core"], formulations = @NamedTuple{schema_version::Int64, selections::@NamedTuple{insulation_admittance::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}, semicon_admittance::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}, earth_properties::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}, temperature_dependence::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}}, assumptions::@NamedTuple{impedance::String, admittance::String, earth::String, propagation::String, semicon_domain::String, enclosure::String}, backend::Symbol, requested::NamedTuple, methods::NamedTuple, options::NamedTuple}((4, @NamedTuple{insulation_admittance::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}, semicon_admittance::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}, earth_properties::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}, temperature_dependence::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}}((@NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}((:lossless, NamedTuple(), NamedTuple())), @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}((:lossless, NamedTuple(), NamedTuple())), @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}((:constant, NamedTuple(), NamedTuple())), @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}((:linear, NamedTuple(), NamedTuple())))), (impedance = "Axial current-driven A_z/u_r finite-element equations", admittance = "Scalar electrodynamic Helmholtz equation in surrounding media; equipotential terminals with unit transverse-current excitation; Y = inv(P)", earth = "Horizontal air and one semi-infinite soil; soil constitutive properties evaluated at each frequency", propagation = "Gamma = 0; medium diffusion and displacement retained; independent Z/P blocks in one factorization", semicon_domain = "Passive material region, without electrical terminal ownership", enclosure = "Supported enclosures are represented by their material and terminal domains"), :fem, (insulation_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), semicon_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_properties = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), temperature_dependence = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing)), (insulation_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), semicon_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), earth_properties = (identifier = :constant, parameters = NamedTuple(), options = NamedTuple()), temperature_dependence = (identifier = :linear, parameters = NamedTuple(), options = NamedTuple())), (reduce_bundle = false, kron_reduction = false, ideal_transposition = false, physics = Symbol("quasi-tem")))), fem = (timing = (assembly_seconds = 10.8147451877594, recovered_columns = 0, scope = "accumulated native worker wall time; not elapsed scan time", columns = 27, worker_wall_seconds = 30.176773681, factorized_columns = 9, constraint_seconds = 3.793778896331787, solve_seconds = 11.393909215927124, output_seconds = 0.007370948791503906, worker_wall_scope = "sum of newly executed process wall durations in this invocation", backend = "getdp", schema = 1, reused = false), run = (state = LineCableModelsGmshExt.completed, run_directory = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-10qUQO", mesh_source = :generated, mesh_fingerprint = "9aa3d880eff7f50b5075cdd97e6aaf3e081dd329f0b6718deb4822b754bde2f1", getdp_invocations = 9, map_paths = String[], completed_columns = 27, completed_frequencies = 9), inputs = (schema_version = 7, solver_protocol = 3, mesh_fingerprint = "51dd9eee74967998c618c26f7379064bb2ab70cb1903f932473079d516d40661", materials = [(kind = :conductor, tag = 10001, mu_r = 0.999994, sigma = [5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7], omega_epsilon = [5.563250277239352e-12, 5.563250277239352e-11, 5.563250277239352e-10, 5.563250277239352e-9, 5.5632502772393515e-8, 5.563250277239353e-7, 5.563250277239352e-6, 5.563250277239352e-5, 0.0005563250277239352])], earth_materials = [(rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0)], air = (eps_r = 1.0, mu_r = 1.0), mesh_plans = LineCableModelsGmshExt.FEMMeshPlan{Float64}[LineCableModelsGmshExt.FEMMeshPlan{Float64}(1, 0.1, 1006.5842420897408, 1258.230302612176, 25.16460605224352, 50.32921210448704, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (25.16460605224352, 25.16460605224352), (1006.5842420897408, 1006.5842420897408)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(2, 1.0, 318.3098861837907, 397.88735772973837, 7.957747154594768, 15.915494309189535, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (7.957747154594768, 7.957747154594768), (318.3098861837907, 318.3098861837907)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(3, 10.0, 100.65842420897408, 125.82303026121761, 2.516460605224352, 5.032921210448704, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (2.516460605224352, 2.516460605224352), (100.65842420897408, 100.65842420897408)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(4, 100.0, 31.83098861837907, 39.78873577297384, 0.7957747154594768, 1.5915494309189535, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (0.7957747154594768, 0.7957747154594768), (31.83098861837907, 31.83098861837907)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(5, 1000.0, 10.06584242089741, 12.582303026121762, 0.2516460605224352, 0.5032921210448704, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (0.2516460605224352, 0.2516460605224352), (10.06584242089741, 10.06584242089741)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(6, 10000.0, 5.0, 6.25, 0.25, 0.5, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (0.25, 0.14067442439954772), (10.0, 9.549296851139358)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(7, 100000.0, 5.0, 6.25, 0.25, 0.5, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (0.25, 0.04448515896357012), (10.0, 3.0197535662513486)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(8, 1.0e6, 5.0, 6.25, 0.25, 0.5, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (0.25, 0.014067442439845935), (10.0, 0.9549323148114102)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(9, 1.0e7, 5.0, 6.25, 0.25, 0.5, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (0.25, 0.004448515892915345), (10.0, 0.3019836725638405))], region_mesh_sizes = [0.0085, 0.0085, 0.0085], cable_outer_mesh_sizes = [0.0085, 0.0085, 0.0085], mesh_growth_factor = 1.2, options = (reduce_bundle = false, kron_reduction = false, ideal_transposition = false, physics = Symbol("quasi-tem")), execution = (ui = false, plot_field_maps = false, mesh_policy = :remesh, mesh_path = nothing, domain_skin_depths = 2.0, keep_run_directory = true, getdp_executable = nothing, gmsh_verbosity = 2, getdp_verbosity = 4, frequency_workers = 2, solver_threads = 4), supplied_mesh = nothing, owned_gmsh = true, getdp_identity = (sha256 = "49d44b1dfe13d9f9b63f8dfe37914af8947f856feebac13fbb6989a41aa88e51", info = "Version : 3.5.0\nLicense : GNU General Public License\nBuild OS : Linux64\nBuild date : 20220513\nBuild host : getdp.info\nBuild options : 64Bit Arpack[contrib] Blas[openblas] Gmsh Kernel Lapack[openblas] PETSc PeWe SLEPc\nPETSc version : 3.14.4 (complex arithmetic)\nGmsh lib version : 4.9.5-git-e286855ce-git-e286855ce (20220218)\nGmsh lib options : 64Bit ANN[contrib] Dlopen Eigen[contrib] LinuxJoystick Parser Plugins Post\nPackaged by : geuzaine\nWeb site : http://getdp.info\nIssue tracker : https://gitlab.onelab.info/getdp/getdp/issues\n"), getdp_provenance = (path = "/home/amartins/.julia/artifacts/51e049a32eeb2ffebcc8a5945bb00f70470a3d29/getdp-3.5.0-Linux64/bin/getdp", source = :artifact, artifact_hash = "51e049a32eeb2ffebcc8a5945bb00f70470a3d29"), gmsh_version = "4.15.0", gmsh_library = "/home/amartins/.julia/artifacts/13aa9c68927e7d05cb18794fbcfe42323c9c0fda/lib/libgmsh.so", julia_version = "1.12.7", adapter_sources = (var"model.jl" = "5dc038f3abd29cd606ab56ecded593b653d3c41cdbfc29e580c189ceeb008584", var"geometry.jl" = "6c6b4236d78945b54c72e7fd34d12a32ccadad75412bbc22ab70ff21df9822f1", var"mesh.jl" = "134502f8ca5ea3243151549b04780259b627adec1682cbfe13b209f9a6993de3", var"onelab.jl" = "baa1af49661cf66d88935d8fb08d7b2a35c7693bebb1fdf540f1463c91dc8164", var"getdp.jl" = "b6a8d3813ce7a200b01cc77428b20dcbb2728f9a4760a1cbde84ef9a698ddd6b", var"voltage_paths.jl" = "81bb6ad0c4078af4f5cce6dcae2bafb6ed60a8374345dfae0b8f61369d61e786", var"workers.jl" = "62ffec093cda6f52d4f08206771dcb82e16e80175ebac6200196c9ed6ffd34ec", var"results.jl" = "bb48b425e16a9048bdf0a4f73c6d7345554d22e396f8d371aa926d4c665a3e4f", var"compute.jl" = "152e200c45a72dab0c2a439900d98a1ec18a6377f2b640ee271850f1ac74b702"), solver_sources = (model = "c0bb424db5ab4db9ca22f549f675edb4646f69910d2aa14e20cf4f40e41bd313", jacobian = "014208d39043ab13183c42c312aba459265715da6d6ddc38ec1fbc39b6090c73", integration = "e113951ac1e6e658f1be50e53875e30c85497445f0e6584a8a7ab1848588e902", materials = "581f089113810a379d8a49c03d79f55045b723c8f4c761d0a493a019ff1f5d9e", quasi_tem = "448d9019ad417c9a784da20170b855dd25e559297499a83d19b007ee095c084d", quasi_full = "1aede35114ea686c5f109089478027d241dec12dc9fd14a24452b348f89b08e4")), terminal_ids = ["cable_0001/three_bare_wires_air_2/core", "cable_0002/three_bare_wires_air_2/core", "cable_0003/three_bare_wires_air_2/core"], reduced_phase_map = [1, 2, 3], inversion_residuals = [4.47545209131181e-16, 3.3306690738754696e-16, 2.237726045655905e-16, 2.220446049250313e-16, 2.220446049250313e-16, 5.204170427930421e-17, 1.1948529671393354e-16, 1.1203564392981743e-16, 5.55111479383945e-17], condition_numbers = [1.6481133469897517e12, 1.6484449198762585e11, 1.6489880856872606e10, 1.6528944689461493e9, 1.6915523609288982e8, 1.9595898759930134e7, 2.9107495278683463e6, 497188.69820734294, 111264.05012308923], primitive = (Z_primitive = ComplexF64[3.156937484869547e-6 + 1.2375960715466406e-6im 9.855928093564008e-8 + 7.118001964827938e-7im 9.888722823086939e-8 + 7.254576929504737e-7im; 9.855928093564002e-8 + 7.118001964827938e-7im 3.1562805382084363e-6 + 1.238155489839502e-6im 9.855927808818229e-8 + 7.118103566479389e-7im; 9.888722823086944e-8 + 7.254576929504738e-7im 9.855927808818241e-8 + 7.11810356647939e-7im 3.15693748672152e-6 + 1.2376681473795957e-6im;;; 4.064349566236812e-6 + 1.0930986995002525e-5im 9.852824980501631e-7 + 5.680107402203725e-6im 9.954183484902376e-7 + 5.809535968222851e-6im; 9.852824980501627e-7 + 5.680107402203727e-6im 4.043953480342123e-6 + 1.0950998663924164e-5im 9.852857015912536e-7 + 5.680117971299855e-6im; 9.954183484902378e-7 + 5.809535968222853e-6im 9.852857015912536e-7 + 5.680117971299858e-6im 4.064356402620632e-6 + 1.0930589072675371e-5im;;; 1.4071669118458445e-5 + 9.429092084376572e-5im 9.831950377043731e-6 + 4.242116588459894e-5im 1.0131530298472416e-5 + 4.349172721821337e-5im; 9.831950377043728e-6 + 4.2421165884598896e-5im 1.3457280759510354e-5 + 9.494581692993998e-5im 9.831949529693482e-6 + 4.2421085994381516e-5im; 1.0131530298472415e-5 + 4.349172721821339e-5im 9.831949529693487e-6 + 4.242108599438155e-5im 1.4071863013141068e-5 + 9.429551114394608e-5im;;; 0.00011823590195980419 + 0.0007739598779219259im 9.682554271065916e-5 + 0.0002806142357194754im 0.0001045753480693945 + 0.00028449911915146356im; 9.68255427106591e-5 + 0.0002806142357194754im 0.00010105820712052285 + 0.0007940951927542603im 9.682358168995295e-5 + 0.00028060397630885im; 0.00010457534806939445 + 0.00028449911915146345im 9.682358168995291e-5 + 0.00028060397630885im 0.00011823922376701001 + 0.0007740008090101047im;;; 0.0011847445460254178 + 0.0060323886843270886im 0.0008795349366666853 + 0.0014022047693677529im 0.0010024719369142027 + 0.0012644746811176577im; 0.0008795349366666855 + 0.001402204769367753im 0.0007857936435604985 + 0.006617301561681695im 0.0008794996428729373 + 0.0014022044524770085im; 0.001002471936914203 + 0.0012644746811176582im 0.0008794996428729378 + 0.0014022044524770085im 0.001184613517509861 + 0.006032214836830032im;;; 0.011128446407467421 + 0.042735853528070605im 0.005254820593777316 + 0.0024807428002432495im 0.004385487975454911 - 0.00021452961784576972im; 0.005254820593777313 + 0.002480742800243254im 0.005101863348260602 + 0.056507679879895176im 0.005254737185982322 + 0.002481082933084129im; 0.004385487975454908 - 0.0002145296178457685im 0.005254737185982325 + 0.0024810829330841307im 0.011128386709664324 + 0.04273875863172526im;;; 0.0914843788453834 + 0.2836805711345129im 0.002921988123659753 - 0.005533541136992314im -0.0014359728388020337 - 0.00035884378404194684im; 0.0029219881236597525 - 0.005533541136992308im 0.02474201797636596 + 0.5119325221357738im 0.0029209104549473086 - 0.005532807667041295im; -0.0014359728388020332 - 0.00035884378404194673im 0.0029209104549473073 - 0.0055328076670413005im 0.09148351654727173 + 0.2836815890878316im;;; 0.7537081656413384 + 1.5682972345165187im 0.00018016270749890044 + 0.0002257720074006584im 2.4979350568131865e-7 + 1.4123901481913753e-6im; 0.00018016270749890033 + 0.00022577200740065857im 0.09310363567733045 + 4.9154040794845795im 0.00018018081362777873 + 0.0002258305203441686im; 2.4979350568131807e-7 + 1.4123901481913734e-6im 0.00018018081362777857 + 0.00022583052034416836im 0.7537417117389459 + 1.568351256130611im;;; 4.550065945557901 + 6.660663678149931im 5.602856770100494e-9 - 2.7981230302132413e-10im 2.0913101393029784e-13 + 6.299390974312689e-13im; 5.6028567701004935e-9 - 2.7981230302132444e-10im 0.5292201750496739 + 51.33126534579285im 5.548043614779805e-9 - 8.799686403254952e-11im; 2.091310139302977e-13 + 6.299390974312683e-13im 5.548043614779797e-9 - 8.799686403255182e-11im 4.5501219747159745 + 6.660525762954794im], P_primitive = ComplexF64[0.22886180170340276 - 0.02498274238484471im 0.16446195707520003 - 0.024920344961566445im 0.16244015160827824 - 0.024981259260678366im; 0.16446195707519995 - 0.02492034496156642im 0.16806881174760516 - 1.0947806774312036e11im 0.1644646157176804 - 0.024920340093977228im; 0.16244015160827824 - 0.024981259260678342im 0.16446461571768045 - 0.024920340093977242im 0.2288710969049945 - 0.024982732244644652im;;; 0.19244533865235805 - 0.02497737731227583im 0.12817683735464955 - 0.024782934823233863im 0.12601977710469878 - 0.024965505487143817im; 0.1281768373546496 - 0.024782934823233863im 0.1319169851858537 - 1.0949448644331295e10im 0.12817565670095316 - 0.024782989045099398im; 0.12601977710469878 - 0.02496550548714381im 0.12817565670095313 - 0.024782989045099395im 0.19244000664514918 - 0.024977488511615544im;;; 0.15600763711750423 - 0.02493253121838372im 0.09217360450621268 - 0.024340866558837725im 0.08960698563460746 - 0.024841751233200482im; 0.09217360450621274 - 0.024340866558837743im 0.09632260543163784 - 1.0949445704131413e9im 0.09217188294050387 - 0.02434081858059561im; 0.0896069856346075 - 0.02484175123320048im 0.09217188294050378 - 0.024340818580595582im 0.15600820914491592 - 0.02493244594583187im;;; 0.11961750732350271 - 0.02462002313620696im 0.057090427220526534 - 0.02293141222278582im 0.053379385076117954 - 0.023998020354700143im; 0.057090427220526575 - 0.022931412222785826im 0.06240447935893342 - 1.0949518811320125e8im 0.0570873796269431 - 0.022931362313193118im; 0.05337938507611797 - 0.02399802035470016im 0.05708737962694309 - 0.022931362313193135im 0.11962444671456036 - 0.0246200934875929im;;; 0.08394462409769952 - 0.022802046230804365im 0.025145584755456897 - 0.018691688091691604im 0.019259964114885564 - 0.019384102564368018im; 0.025145584755456918 - 0.01869168809169162im 0.033027549841483296 - 1.0957004446994703e7im 0.025145058577017425 - 0.01869089177375824im; 0.01925996411488557 - 0.019384102564368025im 0.025145058577017418 - 0.018690891773758243im 0.08394458236421108 - 0.022800834325571225im;;; 0.053641578988399635 - 0.016378124112473448im 0.003126237618808673 - 0.009030513406676184im -0.0012536750153507809 - 0.005809782502931379im; 0.0031262376188086706 - 0.009030513406676181im 0.013006984384956854 - 1.095502647281196e6im 0.0031260421137156327 - 0.009030770842411521im; -0.001253675015350781 - 0.00580978250293138im 0.0031260421137156297 - 0.009030770842411516im 0.05364435021379346 - 0.016378474494910162im;;; 0.03591711019781254 - 0.011514720262725375im -0.0008019762967182735 - 0.0003332760916716736im -3.6674860483590646e-5 + 0.00015341787823683537im; -0.0008019762967182736 - 0.0003332760916716739im 0.004099729858877512 - 109549.78536065375im -0.0008018657174859701 - 0.00033313038435121264im; -3.6674860483590626e-5 + 0.00015341787823683535im -0.0008018657174859704 - 0.00033313038435121264im 0.035917269068037404 - 0.011514638397287653im;;; 0.019862668241671707 - 0.009545516944998528im 2.812228947541073e-6 - 2.365634188399441e-6im 1.7979463104180457e-8 - 2.730823192467637e-9im; 2.8122289475410758e-6 - 2.365634188399443e-6im 0.0012745379384745369 - 10956.695094679559im 2.813007051251135e-6 - 2.3661411283534247e-6im; 1.797946310418046e-8 - 2.730823192467635e-9im 2.813007051251134e-6 - 2.3661411283534256e-6im 0.019863352403784413 - 0.009545942834182265im;;; 0.008435508467532432 - 0.0057631249768678im -2.9083137532414885e-13 - 6.619335929153608e-12im 7.978138491426564e-16 - 2.6491198959325527e-16im; -2.9083137532415046e-13 - 6.619335929153607e-12im 0.0005084184095484191 - 1136.6879199440455im -6.199540059119692e-14 - 6.587146239366973e-12im; 7.978138491426571e-16 - 2.6491198959325566e-16im -6.199540059119544e-14 - 6.5871462393669645e-12im 0.008435333795708173 - 0.005763197130075267im], phase_map = [1, 2, 3]))) \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/fem/fem-artifacts.toml b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/fem/fem-artifacts.toml new file mode 100644 index 000000000..fd3d05b51 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/fem/fem-artifacts.toml @@ -0,0 +1,296 @@ +"attempts/f0001-wIKlX1/attempt.json" = "8ffc34a2c657d1603a33656b9f692a2002f6e256b4c08a3f991cecfde3f19bc2" +"attempts/f0001-wIKlX1/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0001-wIKlX1/getdp.log" = "3920b4627dcb0bb5e56c6d6ace5f0ca374d50ed4b7d169f3a44fbfab147763e8" +"attempts/f0001-wIKlX1/petsc.log" = "9eec988b40434579ceae4cb2b67f4a9775944aefc83920098ec85d6d228c8f97" +"attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0001-P.tsv" = "7d980e540ffc0d69b8a01ce50a20f9c2384fd0b8b853dca31a5b06e83f520b9e" +"attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0001-Z.tsv" = "a60321da47854a4de01adf1302c5484074c0f99019815a86782ede78567ecefe" +"attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0001-timing.tsv" = "e1643b1ba2c46404858c3eb8e98689afc9e6b9d2fafd72fe0311748d06bda95c" +"attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0001.done" = "a40a11bab26b32dfb169550347f289dc710df24f59fe3ef20963d46f838f0586" +"attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0002-P.tsv" = "3878f7365c6e2189dac0f8c2410a3eb1ac3a34f18beff5ceeba3f6a6e39468df" +"attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0002-Z.tsv" = "dafccab8e16a3eca4c6d50faa6465f1c41afd133f4bc391cfc8b5a9218527563" +"attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0002-timing.tsv" = "155998fff9ed1ac43fa05c89be8d293d6e2fa328af71d40df4038e6940446621" +"attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0002.done" = "f58042f034284ccf2955f2795037937a5bd0e4288ffc0d0d5124e33ee07f17ec" +"attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0003-P.tsv" = "ccb871cfde604349db3c02150d10c3ebd59ef4881bee1e88069eb90ecc41901d" +"attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0003-Z.tsv" = "ddfaee4bc77e5f93bfbc12e512dcd6e16569d1629f1bb2034b6d86ddf959612d" +"attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0003-timing.tsv" = "b96bb7171be1563c0b51fba2bb0105edad1eae81ec5264be0cb83c0a547cd349" +"attempts/f0001-wIKlX1/raw/jobs/getdp-f0001-b0003.done" = "41695cf0039479bd9b703276e3c15366a77a1e43f8490b48634d8a1c2a61be94" +"attempts/f0001-wIKlX1/solver.pre" = "4de61cb288ae912fea193d8f0c287231605b9b48e5205c2761614468a738da14" +"attempts/f0002-TpcYhy/attempt.json" = "1135f7e33f8114643aabb99e10816978cad9a477e25bfbd5228622196ba93ae9" +"attempts/f0002-TpcYhy/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0002-TpcYhy/getdp.log" = "62d37c1273f0edda6e8602402e5056fca8e65c7414cbdde71e0653a56f8db5f4" +"attempts/f0002-TpcYhy/petsc.log" = "0ffb0e3bc68c35ea297495571943855520789cd3f68b06e06fc2c1d909d63bba" +"attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0001-P.tsv" = "fce2e98f6924961dbc7ba69bda3cc8f1b155ab410b3568b5ec0598e855e592f2" +"attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0001-Z.tsv" = "90bff2c9e6d258f000771cee22280260765eb10e70c8fbbe687e8b347ee40672" +"attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0001-timing.tsv" = "995de5e3ee5fc49a777944b7bf5f84b036e76ad5e18c66be5e16c8d153e29c2a" +"attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0001.done" = "3dbd9539f415ee889a34b9c6efe6b9cb8168b7c0945d52d7e63e920c8990d2d1" +"attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0002-P.tsv" = "3dd0638b6d29e28921b8858c441c5c460b08df57630ae236f89c9c374f570f6e" +"attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0002-Z.tsv" = "ff4740658f7674d842f0acc95da5db093b113640ee3bcdd90d96c3e0559737ee" +"attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0002-timing.tsv" = "d17188b3665d42413b7623534ba89c42d32432fcfc16829d5eb4ad79b1924e0a" +"attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0002.done" = "d498381e814531161686031fcde5aa549b456c6cbee61f6eb67916650247e717" +"attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0003-P.tsv" = "4957bca5b6df0e65d10bdba3ffae367150f6fa4983ac62802bec745f7c15bc6f" +"attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0003-Z.tsv" = "07ee32261f3bb9eef7216fbfd4e2ea776dad0991e60f61f20016dbe611e1af91" +"attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0003-timing.tsv" = "992c15097975e80aae67549bf9495f400f4245d848e087dee908b4d440823ee1" +"attempts/f0002-TpcYhy/raw/jobs/getdp-f0002-b0003.done" = "fa5c0c361441906d1aaa6b254ae52969806be93c36846f4c70c70a18dc833050" +"attempts/f0002-TpcYhy/solver.pre" = "1873f4f4d5f23b6c5b618ebf57aee3f091de54f7d7420197b056425ca0690304" +"attempts/f0003-IRSVRX/attempt.json" = "f2e4e938efe35a0ae0691e6e1d0dc3621e069573ef2262839bc2e6e16f2853b9" +"attempts/f0003-IRSVRX/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0003-IRSVRX/getdp.log" = "f7eaf989d58a79e3410265489b26b7a9f7ad0a9662d11ec005b620968683145b" +"attempts/f0003-IRSVRX/petsc.log" = "63e3a04f3714ebfc45ffef611703acd6c91c64aedd241478670230928d6c30e6" +"attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0001-P.tsv" = "4780b7d3613e2ecf7009d3162721dee65244ff349e0c953dde6bd98b6493c982" +"attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0001-Z.tsv" = "49eaab99e9366305eeb287b5bce041292939552446e3b0da868535b4037a07b3" +"attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0001-timing.tsv" = "de7860b3937f4d04d88d4f9c891bfbe934497dad27b11c3ebc60f35cda826cb3" +"attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0001.done" = "02395cb824ac1c8ce1d2e8cd3c57fd19d5c904215aef49b1fba677c51e99d519" +"attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0002-P.tsv" = "2acde5f3e22c83d5a2117aeab125f6688344b966b4533eb2b9653b187ff89b8a" +"attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0002-Z.tsv" = "f2ca43ed615d68deda8c36683541ee2c0c1054a68849793cb8b144b2b5c4aea2" +"attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0002-timing.tsv" = "44c217e12064774e7898c48b2e6f2ac0a16621eb6047a86d02815579e3022246" +"attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0002.done" = "84034583a44a97df0ec5820a76fc891b95a9b6bc0bf693bed9882420a3eb5b05" +"attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0003-P.tsv" = "d51e8f7d7f1934c97401418472e49664decb7fb133714fdb8008124a16ce504a" +"attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0003-Z.tsv" = "8356831df7703227ea286922c104f484b094397d6b8fec8e077f89f346434f63" +"attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0003-timing.tsv" = "13583ff00a362bbc4cca6cf8349b54d9188b9b5ae448c67c4989783a4fe23717" +"attempts/f0003-IRSVRX/raw/jobs/getdp-f0003-b0003.done" = "aec9dd1bab4102d3c07f9c6abf83e4d604721961d8e37cbbc9620f7f71cafcae" +"attempts/f0003-IRSVRX/solver.pre" = "09a7fe8f033fae8b5e56b402574544e6b17d6f9235732a684b909c3c0bbac658" +"attempts/f0004-pcls4E/attempt.json" = "340e1e75e87c9622566120a79e6a3a219edc57447b2c719e2cf76efc54fd4cf0" +"attempts/f0004-pcls4E/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0004-pcls4E/getdp.log" = "33b8a9d314492b97ed549413d4c288b597c64c8d1248806787b7175791009a2f" +"attempts/f0004-pcls4E/petsc.log" = "2005b67ef5e7d0126699426a1852d31c63622a92ac8c0f7b0f64f15be64c95fa" +"attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0001-P.tsv" = "84ace93ddfec0c78d6ce2c44565f9121516f1fe44b37040e187a1872766b09c1" +"attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0001-Z.tsv" = "3a29730bee1b706e082476f111cd28165e1e953224bc7fd7e40df469d406be58" +"attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0001-timing.tsv" = "90d73e48cf47f6bcf0070e6199360b6c4ae81b38a40b31479b0bec863ad4263c" +"attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0001.done" = "05b52afc2d2a0ffefd0bb6e40585165d1b2572547d59d3c360b4163202709006" +"attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0002-P.tsv" = "cf379f24762455ef6473ddd54bbc34aaf7459881f24a3cb1faa15aba188238f4" +"attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0002-Z.tsv" = "005d8eef7269f9f7045285ec648e6bf9bde498cd701e4e4593dfca55d23fd40e" +"attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0002-timing.tsv" = "79b8a131cb7b172981d96d303422d5ffed3dd695774472233526bfd35400c88b" +"attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0002.done" = "35d216e20aed80a36327575b350d8e4c12b70f2038f5d07d55614c92afdd6b6b" +"attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0003-P.tsv" = "dfcdc833dc6aed949f370429d1b3d7a5f3d7bdd6a6e7ef990087da3dca21ce06" +"attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0003-Z.tsv" = "2381d18ea5f5b76798b442ca4850e93139bda0df20ce619808efbb84b5b3b10b" +"attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0003-timing.tsv" = "9714b737c4fe9821963919edccec2cd8dced3ca9415528095927469b04b5329e" +"attempts/f0004-pcls4E/raw/jobs/getdp-f0004-b0003.done" = "eae2a23f21e24dd158a817c096e6e77f32e1d2a850c4163d279c3531eef2af5a" +"attempts/f0004-pcls4E/solver.pre" = "8d371fb7497e747a76ce284178f93dd49f259020d771bbb62d77945b892ae74f" +"attempts/f0005-gCunzX/attempt.json" = "59e2ce183be8590a3b72758c7ed3b59a3292ba877adbdceaf2462c0630b2ce06" +"attempts/f0005-gCunzX/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0005-gCunzX/getdp.log" = "60135aa1f9070c972edc56c5d935f4635ef479363a62c450e2f764a6a1a99b23" +"attempts/f0005-gCunzX/petsc.log" = "453849fb5486a7f3d0e0950f8e8d0c3de09da6811081db1fa163148a1cb048f8" +"attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0001-P.tsv" = "55df259870ee17c16a34bb902dda01fe2530f0498b52bfe3ea8a9b6f778cdbca" +"attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0001-Z.tsv" = "86378d5d3cec39828bcc40dd1bd5d3a1d9a21882b544305c52499cf89640c141" +"attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0001-timing.tsv" = "14af070450a972fcfd86c8268c1829a6a1ab6fc4e646e186c956530d16779422" +"attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0001.done" = "ee7bf9802d600055d484ec4e501666418037b219c7bf910eefda35e16a843b08" +"attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0002-P.tsv" = "a568f37204ffc8ed8ed5f557bbb268d5681062dc5d8414917cf6979857d596cf" +"attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0002-Z.tsv" = "04af27f87cc684e46970f4b2428c13962bd374d7b0d67d642e491bb52e82e086" +"attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0002-timing.tsv" = "45f874a67ba112a4febd5d0f11095a41f9fdf9e9359b0fe51d98015adc67ece3" +"attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0002.done" = "e26c3631aaa36c47efb3ede81242002dbebf27057f428fe57f60849cede6b3f3" +"attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0003-P.tsv" = "668a14b8dc4b7796c62c5e268433c50d5be9fa4cdcfece871155a7ff03cb8c08" +"attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0003-Z.tsv" = "6dc17e1c0f6e63b477b8793998657fff64d52625493a62ce831d065d9d9551e2" +"attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0003-timing.tsv" = "86f21c1b3e1e4144d82ddec8a9f5e924fd307423ac92af08405f5bbd594eb457" +"attempts/f0005-gCunzX/raw/jobs/getdp-f0005-b0003.done" = "a91d7ca1bb120ef43c71df371c45a71b1cc7744f1ee2eb082338700de7f329c9" +"attempts/f0005-gCunzX/solver.pre" = "4a8e23396875d3cce5a9d3d1b1696d7ab02b23bdc6c89c268366a71e97763e69" +"attempts/f0006-EwzCMZ/attempt.json" = "08c8e562f87b4cd2579f10aaccb71378da3ecb850118faf60798d8814e26719d" +"attempts/f0006-EwzCMZ/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0006-EwzCMZ/getdp.log" = "37c301495c9b0330573ccbd6bf32f35b4f5052c7308203335d1db5dcefa3878b" +"attempts/f0006-EwzCMZ/petsc.log" = "a80d9abc9be1926073797b9515161f872732304dfd0ff8989248b7710f3695e6" +"attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0001-P.tsv" = "2fb3008802b1d2ac39765be77153e220b19e23062049b04b5d47459c613634c8" +"attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0001-Z.tsv" = "9453c3ff01f761480899a795c29756b1423f516d4ceb9b4476919fb62e8ec002" +"attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0001-timing.tsv" = "00e0c6f61aae65addc3d5c5ae15ab5c49679767c97fddd33912f65eb9c0cd8c7" +"attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0001.done" = "e41a78288341a2ae156cd28b6102b3a31a74b012eb4deb233e88fae392912e8e" +"attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0002-P.tsv" = "c23bb0da77a088f37c99f48b772f3c70a52d3eb69447747980219ef8f2a95378" +"attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0002-Z.tsv" = "f90e6d92872970317c1211037aaabff139245277fcdcf7bde4e3218c0476f31d" +"attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0002-timing.tsv" = "b00502f7403b818c10d12352ecf46b6a85991b472ed15dd22ca9589d4cc1ff32" +"attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0002.done" = "c105367e4bac19858725b1b7c56efbb25ae370173d002f9a678a4762a5c69ac9" +"attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0003-P.tsv" = "e4a6fb0e91437f3aa7aa37bcfea7abe60c60556cc092d29b48c50ed1087447b2" +"attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0003-Z.tsv" = "73cd0efa6d4b2f933300d062566e0e12a16a4839d1048cb760d19d24428b8afb" +"attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0003-timing.tsv" = "02b2820d165c40db53599b1333a58b06669158c48d0d2dfc40f471313bbedbb9" +"attempts/f0006-EwzCMZ/raw/jobs/getdp-f0006-b0003.done" = "de6214d7f2cf6d2097d94d720289a7424da0b9bf7a5df27326d8b69e936798e8" +"attempts/f0006-EwzCMZ/solver.pre" = "442b0137d30e93881664918e4537310e4a98f6b63a8f708c37308ef2e7b436d9" +"attempts/f0007-hio5iQ/attempt.json" = "4065585146851781aae076cc46590cd24d5f74a3ab1bec01e6da340eb376b675" +"attempts/f0007-hio5iQ/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0007-hio5iQ/getdp.log" = "447506c34122184cc5267826b6a6b93059e17da7efb25860e29683043288f34c" +"attempts/f0007-hio5iQ/petsc.log" = "a22d83a5ee7323394aa8b3dbebbe1fb400818b7f3345a9c6ecc7e27d2bfb9356" +"attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0001-P.tsv" = "6ed9911e6ea19c2229cb9e290324c9eace399847ea135f96b4e9268a46cc8c76" +"attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0001-Z.tsv" = "08bb2afeddfa7316e645ffdca79212a9c5708cf87852490504d2a9c31d1ef122" +"attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0001-timing.tsv" = "01faa96a1ff80b58b174a64dcd69d609e0b0fa406e344fcd5cf53a8e908c208f" +"attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0001.done" = "d95e4d651bb7fa68e36e0077af5a10e10baacf852173da7ac0f1cca3e414dc7f" +"attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0002-P.tsv" = "51b6b281d2bce47291e6bf34ab43881edf769f99fac63e114d1e2c94d7f753dd" +"attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0002-Z.tsv" = "704052c9b5d87ce6dd2b6836ce45cfaa963861c477b14cf64bd0b3fea6a117e7" +"attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0002-timing.tsv" = "123c272fbf0d8a4a7ddea34dca16848cb88423aec9cd6e1928488c2c553ea59c" +"attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0002.done" = "a953202777b2aea0282f4e5ecac9feb227f850cb3f80f2247ca07a708f3635be" +"attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0003-P.tsv" = "2f2f768c634d762747b69afafb1645be63c5cc543030f75b662d062c65905e38" +"attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0003-Z.tsv" = "5e4d322a375529f978a9260a22e0d5ec1639016c1146a9da62097b62586690ce" +"attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0003-timing.tsv" = "bcf9fc7261155c3892108e52b94c6af81ef8b4d5c6ae0a101637803018d7ea87" +"attempts/f0007-hio5iQ/raw/jobs/getdp-f0007-b0003.done" = "bc6e3e671a966700354c8fb7f9ab0ff830db8a4b88332e0ec4974ce29df37b0c" +"attempts/f0007-hio5iQ/solver.pre" = "93e92e0f6ae3a3ff783d9a34f2db39ba0a46bf7d221802ce5bf4fd5aa3cf5801" +"attempts/f0008-t5WGBB/attempt.json" = "3e8cb67e6b1296c72ed3a752ae312cd7620fcd8c8e4af11a076bf3b0bcb5f928" +"attempts/f0008-t5WGBB/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0008-t5WGBB/getdp.log" = "76f2b36fdb329f57e3fe69658f31d6b2b6e1c2e059bc8977d784f0d9bf3312cb" +"attempts/f0008-t5WGBB/petsc.log" = "a5076fc0b59e1486a0fbef232f9d5ba858e6a58d5044a373eaa84b9b28fa0d2e" +"attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0001-P.tsv" = "b5cb24e313a2f7ad2c154f50f1b100f700f145a08193a8f8edadfcb03aeafb5a" +"attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0001-Z.tsv" = "04007b40d90923d74d88fbe86aa3243f6c15f2b63e7fd9373cafe4095703ae99" +"attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0001-timing.tsv" = "524d5374da5579aea44825b686434e557d441d01c7202e7c97a168af9814b4aa" +"attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0001.done" = "c03ca6db0131d3218478fb4e85f0350dca26787b9ffdfc9d50b5d68ad93b352c" +"attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0002-P.tsv" = "c743dcb9a8d47c165a365c8621615125550fb98794496d4b4d0c84a1f306e3ed" +"attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0002-Z.tsv" = "5b7b6059261bc6e81a491d8fe9609d68103852e96d6fffe28788fc53ded06cc8" +"attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0002-timing.tsv" = "8b611c675d54531e81f98bc4878696f1ea13e0252f1788ff4bb20148db65d185" +"attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0002.done" = "76f50b528daca79ab1eac47c47f96ca1e7b8ad94bcaefbaac04335941306f993" +"attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0003-P.tsv" = "358654a68195014400ca3e9eee9602ddd01b0a70be3f69f4cd09069fabad5cbb" +"attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0003-Z.tsv" = "d3cb527f9cdf2bc56449a3d84cbbad30fb01b406c725b963626d9c4e9ae87e05" +"attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0003-timing.tsv" = "3cb2a99be1b94193012284f026bca4b6194c763d0957c795198c3a2ae3caf35d" +"attempts/f0008-t5WGBB/raw/jobs/getdp-f0008-b0003.done" = "20bdbe7d9351850f55ebf49a31f3a93235d04aea44253ddd2d75828a83fa1c74" +"attempts/f0008-t5WGBB/solver.pre" = "68563937aff9272cf07564c292ffd5a85c5f09a0aa8f19e461b6499e6b99e499" +"attempts/f0009-UbLL6z/attempt.json" = "62d6b0111a4cfc26672debeb26f27f358367ae152ca9764b3234cf3326bdd75c" +"attempts/f0009-UbLL6z/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0009-UbLL6z/getdp.log" = "a1585bba376a6931ba2a108413e2ebe7e45b0d4377d9ffb2fa72964c2c1363fc" +"attempts/f0009-UbLL6z/petsc.log" = "8af0b26522f72972cdf8719032ceaa3439d9f526005f5f6a16abca76dd3e39fd" +"attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0001-P.tsv" = "312177b45ff96145802d4f94cdc5ae6b2723548f4274cdf5ae676611b2acc29c" +"attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0001-Z.tsv" = "778105611da0cfce5d3635ef28e9345ffc0bfb6533fe7c4652a8b366ae0b5b78" +"attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0001-timing.tsv" = "9064552b387a3175b04931d1317af1d031edc577f67679e86f54d0d8e8be82d1" +"attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0001.done" = "22b8b831af1067ac03eaf9570ddd71235af29f6b65e559199200376f6ef90f2a" +"attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0002-P.tsv" = "006824828b9b6c1f38aaf5b741364c615cca3bfbf5a3d12e68edea88adcad768" +"attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0002-Z.tsv" = "266b76ed64e522873ad8394b605ccaef34f3514ef9ad60a5f6740ff3d6872efe" +"attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0002-timing.tsv" = "81802d95fb3639ae62510ff48ef13aa7e9e804468da053efe0c655098c865c8c" +"attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0002.done" = "2ab6b278377a8a5090d5d456a9193028844db99b8f3fa96c2433d70920e73c22" +"attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0003-P.tsv" = "fab6f65e74720b6fdfb3f3531dbd815893bae9bfc02f9b6d8d30a3c923479cb5" +"attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0003-Z.tsv" = "08a5327a9f57103a1514ad2c72fb477f0276ef272e38f8c02fe26bcffab612f2" +"attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0003-timing.tsv" = "ebadfdacd79f8ac1b12044bab9d9b0d7dbe9998669849c4f40247e243c2e669d" +"attempts/f0009-UbLL6z/raw/jobs/getdp-f0009-b0003.done" = "3449a1ca026b39cca413695b5bd20e176addf6bcf3103f9147e8ac56407cafa3" +"attempts/f0009-UbLL6z/solver.pre" = "8dce5cba3a71c61b00f97453b23afbfefe610961c99b77a5497acd6908df3b98" +"coordinator.lock" = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" +"input/computation.json" = "dd5de6278edac71742d8e453785458604cdf76b1dd98005d8c9e1f8b5caf2913" +"input/getdp/integration.pro" = "e113951ac1e6e658f1be50e53875e30c85497445f0e6584a8a7ab1848588e902" +"input/getdp/jacobian.pro" = "014208d39043ab13183c42c312aba459265715da6d6ddc38ec1fbc39b6090c73" +"input/getdp/materials.pro" = "581f089113810a379d8a49c03d79f55045b723c8f4c761d0a493a019ff1f5d9e" +"input/getdp/model.pro" = "c0bb424db5ab4db9ca22f549f675edb4646f69910d2aa14e20cf4f40e41bd313" +"input/getdp/quasi-full.pro" = "1aede35114ea686c5f109089478027d241dec12dc9fd14a24452b348f89b08e4" +"input/getdp/quasi-tem.pro" = "448d9019ad417c9a784da20170b855dd25e559297499a83d19b007ee095c084d" +"input/model_data.pro" = "0c18ecf5e16df8e6a270e25c2d3eefd3e4a55f63853b8e71e6b3e7d15ff3f601" +"input/problem.json" = "eb8dc8ed237ca7801e89dd3cad47f6608e200fdc6347e4c9616b187188cf7d26" +"logs/getdp.log" = "846f6e9d19204a75253ce69eddab2e8b4efb9d97e7ae24856e040ab21aa3687c" +"mesh/frequency_0001.json" = "2aafaedebb99d2c04c9d6f5fa91f698b4f2da954d17c07120444514cde17ed10" +"mesh/frequency_0001.msh" = "523f6a1b18ec7c704e752a41f1c57de8cea183a5bf8adbc8098b0bb03981db1f" +"mesh/frequency_0002.json" = "2cee8da71a88f95911e8371be7785de8a11c9d27835e4872a19b50987e7454ac" +"mesh/frequency_0002.msh" = "ca1fee0e46f16320f9df32da5d0a906d7860363517781c2805f51ab74b8b5c4c" +"mesh/frequency_0003.json" = "78753bb179a7841e89b8a057e95440187cc29ade86cc57d6cb2e4ac578df99f0" +"mesh/frequency_0003.msh" = "c8f05f998bd8f08132c488f160a79b65ddcca6497fcc95a9e88bd58908645bfd" +"mesh/frequency_0004.json" = "c69519464a75ed4981046c70eddd02db3f9ae7c04f1d0006332a80dabe679a77" +"mesh/frequency_0004.msh" = "e397de871f12985d3bdf9fda433ae9755e47bde482d58ed8a130197dec9d159b" +"mesh/frequency_0005.json" = "114155df68cef682e8514c1bd475b5fe8d8448b13bf3c2e42d84be17c10bd415" +"mesh/frequency_0005.msh" = "9e95d3f9518cd95bb44831ce3c1cc0dc8209fb1eb269b8ebf622c4220a1aa95e" +"mesh/frequency_0006.json" = "0ce5ce9a7177373270e5b0ba26f264663b2d7613591866b764dca6bccec046fb" +"mesh/frequency_0006.msh" = "c6a1f5e574f7389a71a203d4552b893a43b222a7b30e92f9efa049bf9143da69" +"mesh/frequency_0007.json" = "2fc252ab17d8f2aaf29f8592f8b8a573f73004c0df7fe4347e34c32ae1cf179e" +"mesh/frequency_0007.msh" = "0d91340b682c4bc4f0a8752a7f630194bf5abbc1a5de0188e5dc0d39e6806f96" +"mesh/frequency_0008.json" = "8249f710d6ef747cc795c5f7754c42f376950fabf141a8fb20ccc76c59a7633c" +"mesh/frequency_0008.msh" = "92cdb4e8c7eba6145f3fd058115b9cd795cd4ee7675aff9fe2fdbf971b45a5d2" +"mesh/model.json" = "32ec746031275855de3faac843fde733a26a33bd5a4ea93dc2f4d90dfec0002e" +"mesh/model.msh" = "6dbee7f8e8814cb0a01b6ae79017c08ad9de6c60c0d78d0a85c3e101278f4037" +"raw/P.tsv" = "5e9299b565761e34d0e9a2eca291eceab25bf7d06a448fe39e7073a72d1ce998" +"raw/Z.tsv" = "f40979d07e7d2f0e03c5fe71a1ffefc4a5198dc0744be971f6cedda11940d011" +"raw/checksums.json" = "f27157dde9627d8705487c695dd5f6bf6ee5cc328fc228c7f1f4cc97bfe5c1a8" +"raw/jobs/getdp-f0001-b0001-P.tsv" = "7d980e540ffc0d69b8a01ce50a20f9c2384fd0b8b853dca31a5b06e83f520b9e" +"raw/jobs/getdp-f0001-b0001-Z.tsv" = "a60321da47854a4de01adf1302c5484074c0f99019815a86782ede78567ecefe" +"raw/jobs/getdp-f0001-b0001-timing.tsv" = "e1643b1ba2c46404858c3eb8e98689afc9e6b9d2fafd72fe0311748d06bda95c" +"raw/jobs/getdp-f0001-b0001.json" = "3122b383c3693801c4bbdf6ba54a2f9bf14c2c1e835b41a1f937e7e7b6e5445e" +"raw/jobs/getdp-f0001-b0002-P.tsv" = "3878f7365c6e2189dac0f8c2410a3eb1ac3a34f18beff5ceeba3f6a6e39468df" +"raw/jobs/getdp-f0001-b0002-Z.tsv" = "dafccab8e16a3eca4c6d50faa6465f1c41afd133f4bc391cfc8b5a9218527563" +"raw/jobs/getdp-f0001-b0002-timing.tsv" = "155998fff9ed1ac43fa05c89be8d293d6e2fa328af71d40df4038e6940446621" +"raw/jobs/getdp-f0001-b0002.json" = "3dc94ec563963cf252056fe110d65861cda452658f7a378ce9bba51fd8394d68" +"raw/jobs/getdp-f0001-b0003-P.tsv" = "ccb871cfde604349db3c02150d10c3ebd59ef4881bee1e88069eb90ecc41901d" +"raw/jobs/getdp-f0001-b0003-Z.tsv" = "ddfaee4bc77e5f93bfbc12e512dcd6e16569d1629f1bb2034b6d86ddf959612d" +"raw/jobs/getdp-f0001-b0003-timing.tsv" = "b96bb7171be1563c0b51fba2bb0105edad1eae81ec5264be0cb83c0a547cd349" +"raw/jobs/getdp-f0001-b0003.json" = "eb4f2939e95ac9a472f11c414bfb73fee30e6116de101ee1b9f674029ce38494" +"raw/jobs/getdp-f0002-b0001-P.tsv" = "fce2e98f6924961dbc7ba69bda3cc8f1b155ab410b3568b5ec0598e855e592f2" +"raw/jobs/getdp-f0002-b0001-Z.tsv" = "90bff2c9e6d258f000771cee22280260765eb10e70c8fbbe687e8b347ee40672" +"raw/jobs/getdp-f0002-b0001-timing.tsv" = "995de5e3ee5fc49a777944b7bf5f84b036e76ad5e18c66be5e16c8d153e29c2a" +"raw/jobs/getdp-f0002-b0001.json" = "a4de680148cd182ad51750147ceeb9b87e1897588b79c5c3fc96434a8fbe1dfd" +"raw/jobs/getdp-f0002-b0002-P.tsv" = "3dd0638b6d29e28921b8858c441c5c460b08df57630ae236f89c9c374f570f6e" +"raw/jobs/getdp-f0002-b0002-Z.tsv" = "ff4740658f7674d842f0acc95da5db093b113640ee3bcdd90d96c3e0559737ee" +"raw/jobs/getdp-f0002-b0002-timing.tsv" = "d17188b3665d42413b7623534ba89c42d32432fcfc16829d5eb4ad79b1924e0a" +"raw/jobs/getdp-f0002-b0002.json" = "64a5d6fbc0c885a95da41cd43968c57f0b29489dd6147047f863331febe8eb8e" +"raw/jobs/getdp-f0002-b0003-P.tsv" = "4957bca5b6df0e65d10bdba3ffae367150f6fa4983ac62802bec745f7c15bc6f" +"raw/jobs/getdp-f0002-b0003-Z.tsv" = "07ee32261f3bb9eef7216fbfd4e2ea776dad0991e60f61f20016dbe611e1af91" +"raw/jobs/getdp-f0002-b0003-timing.tsv" = "992c15097975e80aae67549bf9495f400f4245d848e087dee908b4d440823ee1" +"raw/jobs/getdp-f0002-b0003.json" = "a3539278bd2b32461c2cdc27e8fba51d922b70ecea2f9b19e7cbf6f18a628e47" +"raw/jobs/getdp-f0003-b0001-P.tsv" = "4780b7d3613e2ecf7009d3162721dee65244ff349e0c953dde6bd98b6493c982" +"raw/jobs/getdp-f0003-b0001-Z.tsv" = "49eaab99e9366305eeb287b5bce041292939552446e3b0da868535b4037a07b3" +"raw/jobs/getdp-f0003-b0001-timing.tsv" = "de7860b3937f4d04d88d4f9c891bfbe934497dad27b11c3ebc60f35cda826cb3" +"raw/jobs/getdp-f0003-b0001.json" = "32f715d70b58d03a550afe9769244d98d5dab8c587ad7f2081c69cdd8d3006a4" +"raw/jobs/getdp-f0003-b0002-P.tsv" = "2acde5f3e22c83d5a2117aeab125f6688344b966b4533eb2b9653b187ff89b8a" +"raw/jobs/getdp-f0003-b0002-Z.tsv" = "f2ca43ed615d68deda8c36683541ee2c0c1054a68849793cb8b144b2b5c4aea2" +"raw/jobs/getdp-f0003-b0002-timing.tsv" = "44c217e12064774e7898c48b2e6f2ac0a16621eb6047a86d02815579e3022246" +"raw/jobs/getdp-f0003-b0002.json" = "79dc3371cd8656e10ab6fbfc1537c327ede11d48703e8bcc8e1dbda5b7c1a8dd" +"raw/jobs/getdp-f0003-b0003-P.tsv" = "d51e8f7d7f1934c97401418472e49664decb7fb133714fdb8008124a16ce504a" +"raw/jobs/getdp-f0003-b0003-Z.tsv" = "8356831df7703227ea286922c104f484b094397d6b8fec8e077f89f346434f63" +"raw/jobs/getdp-f0003-b0003-timing.tsv" = "13583ff00a362bbc4cca6cf8349b54d9188b9b5ae448c67c4989783a4fe23717" +"raw/jobs/getdp-f0003-b0003.json" = "4eba223e6599332394311e337bcb3d617a582d4005554302677e2750d9215fdb" +"raw/jobs/getdp-f0004-b0001-P.tsv" = "84ace93ddfec0c78d6ce2c44565f9121516f1fe44b37040e187a1872766b09c1" +"raw/jobs/getdp-f0004-b0001-Z.tsv" = "3a29730bee1b706e082476f111cd28165e1e953224bc7fd7e40df469d406be58" +"raw/jobs/getdp-f0004-b0001-timing.tsv" = "90d73e48cf47f6bcf0070e6199360b6c4ae81b38a40b31479b0bec863ad4263c" +"raw/jobs/getdp-f0004-b0001.json" = "347ecc55acdd3b5411397d6577aaa873489a6730c9d7024acd4da66e6301b71c" +"raw/jobs/getdp-f0004-b0002-P.tsv" = "cf379f24762455ef6473ddd54bbc34aaf7459881f24a3cb1faa15aba188238f4" +"raw/jobs/getdp-f0004-b0002-Z.tsv" = "005d8eef7269f9f7045285ec648e6bf9bde498cd701e4e4593dfca55d23fd40e" +"raw/jobs/getdp-f0004-b0002-timing.tsv" = "79b8a131cb7b172981d96d303422d5ffed3dd695774472233526bfd35400c88b" +"raw/jobs/getdp-f0004-b0002.json" = "0a83cf7da4f7fe7e5d1fb8093135396b35e4c84a14e9f665bac335dd9f6166e3" +"raw/jobs/getdp-f0004-b0003-P.tsv" = "dfcdc833dc6aed949f370429d1b3d7a5f3d7bdd6a6e7ef990087da3dca21ce06" +"raw/jobs/getdp-f0004-b0003-Z.tsv" = "2381d18ea5f5b76798b442ca4850e93139bda0df20ce619808efbb84b5b3b10b" +"raw/jobs/getdp-f0004-b0003-timing.tsv" = "9714b737c4fe9821963919edccec2cd8dced3ca9415528095927469b04b5329e" +"raw/jobs/getdp-f0004-b0003.json" = "ad24b5782ba04b62702fda9d7c3f7874df909b4f16557f15412178db435f6ac1" +"raw/jobs/getdp-f0005-b0001-P.tsv" = "55df259870ee17c16a34bb902dda01fe2530f0498b52bfe3ea8a9b6f778cdbca" +"raw/jobs/getdp-f0005-b0001-Z.tsv" = "86378d5d3cec39828bcc40dd1bd5d3a1d9a21882b544305c52499cf89640c141" +"raw/jobs/getdp-f0005-b0001-timing.tsv" = "14af070450a972fcfd86c8268c1829a6a1ab6fc4e646e186c956530d16779422" +"raw/jobs/getdp-f0005-b0001.json" = "1b754b8008ba2dffbabb08c9f808d50d60ffc0292ecf40f25f06a3eec14c505a" +"raw/jobs/getdp-f0005-b0002-P.tsv" = "a568f37204ffc8ed8ed5f557bbb268d5681062dc5d8414917cf6979857d596cf" +"raw/jobs/getdp-f0005-b0002-Z.tsv" = "04af27f87cc684e46970f4b2428c13962bd374d7b0d67d642e491bb52e82e086" +"raw/jobs/getdp-f0005-b0002-timing.tsv" = "45f874a67ba112a4febd5d0f11095a41f9fdf9e9359b0fe51d98015adc67ece3" +"raw/jobs/getdp-f0005-b0002.json" = "c7031a6872994aa0b82f5bd44d4d594b7ca82c99b7a6395b356e2cb1fd982ea4" +"raw/jobs/getdp-f0005-b0003-P.tsv" = "668a14b8dc4b7796c62c5e268433c50d5be9fa4cdcfece871155a7ff03cb8c08" +"raw/jobs/getdp-f0005-b0003-Z.tsv" = "6dc17e1c0f6e63b477b8793998657fff64d52625493a62ce831d065d9d9551e2" +"raw/jobs/getdp-f0005-b0003-timing.tsv" = "86f21c1b3e1e4144d82ddec8a9f5e924fd307423ac92af08405f5bbd594eb457" +"raw/jobs/getdp-f0005-b0003.json" = "22a7153411224930b2f75f413beabe6fe1920c8262a38a7a0125685210195194" +"raw/jobs/getdp-f0006-b0001-P.tsv" = "2fb3008802b1d2ac39765be77153e220b19e23062049b04b5d47459c613634c8" +"raw/jobs/getdp-f0006-b0001-Z.tsv" = "9453c3ff01f761480899a795c29756b1423f516d4ceb9b4476919fb62e8ec002" +"raw/jobs/getdp-f0006-b0001-timing.tsv" = "00e0c6f61aae65addc3d5c5ae15ab5c49679767c97fddd33912f65eb9c0cd8c7" +"raw/jobs/getdp-f0006-b0001.json" = "378dda8d0dcb0a9a39750d5cfaa093c115fc717143aba917acbfba3b862a871d" +"raw/jobs/getdp-f0006-b0002-P.tsv" = "c23bb0da77a088f37c99f48b772f3c70a52d3eb69447747980219ef8f2a95378" +"raw/jobs/getdp-f0006-b0002-Z.tsv" = "f90e6d92872970317c1211037aaabff139245277fcdcf7bde4e3218c0476f31d" +"raw/jobs/getdp-f0006-b0002-timing.tsv" = "b00502f7403b818c10d12352ecf46b6a85991b472ed15dd22ca9589d4cc1ff32" +"raw/jobs/getdp-f0006-b0002.json" = "9dcb78a80b9a8122bc0f1e9c6584ede60f5f49eda06771d0bc7872fb2e59d2fc" +"raw/jobs/getdp-f0006-b0003-P.tsv" = "e4a6fb0e91437f3aa7aa37bcfea7abe60c60556cc092d29b48c50ed1087447b2" +"raw/jobs/getdp-f0006-b0003-Z.tsv" = "73cd0efa6d4b2f933300d062566e0e12a16a4839d1048cb760d19d24428b8afb" +"raw/jobs/getdp-f0006-b0003-timing.tsv" = "02b2820d165c40db53599b1333a58b06669158c48d0d2dfc40f471313bbedbb9" +"raw/jobs/getdp-f0006-b0003.json" = "da2645477facdac6754fd11269499cf925639ec5f7b779ed621b2c4302ed7144" +"raw/jobs/getdp-f0007-b0001-P.tsv" = "6ed9911e6ea19c2229cb9e290324c9eace399847ea135f96b4e9268a46cc8c76" +"raw/jobs/getdp-f0007-b0001-Z.tsv" = "08bb2afeddfa7316e645ffdca79212a9c5708cf87852490504d2a9c31d1ef122" +"raw/jobs/getdp-f0007-b0001-timing.tsv" = "01faa96a1ff80b58b174a64dcd69d609e0b0fa406e344fcd5cf53a8e908c208f" +"raw/jobs/getdp-f0007-b0001.json" = "638c83f33344a7b17a2604c6905ca7774d636825c88f7d84623cd5763af2920f" +"raw/jobs/getdp-f0007-b0002-P.tsv" = "51b6b281d2bce47291e6bf34ab43881edf769f99fac63e114d1e2c94d7f753dd" +"raw/jobs/getdp-f0007-b0002-Z.tsv" = "704052c9b5d87ce6dd2b6836ce45cfaa963861c477b14cf64bd0b3fea6a117e7" +"raw/jobs/getdp-f0007-b0002-timing.tsv" = "123c272fbf0d8a4a7ddea34dca16848cb88423aec9cd6e1928488c2c553ea59c" +"raw/jobs/getdp-f0007-b0002.json" = "f40f39496d7dc4f4335242c16cd1c17b1b3dbab8040c969ce94ec714f53d0442" +"raw/jobs/getdp-f0007-b0003-P.tsv" = "2f2f768c634d762747b69afafb1645be63c5cc543030f75b662d062c65905e38" +"raw/jobs/getdp-f0007-b0003-Z.tsv" = "5e4d322a375529f978a9260a22e0d5ec1639016c1146a9da62097b62586690ce" +"raw/jobs/getdp-f0007-b0003-timing.tsv" = "bcf9fc7261155c3892108e52b94c6af81ef8b4d5c6ae0a101637803018d7ea87" +"raw/jobs/getdp-f0007-b0003.json" = "bf5296d3cc4a5bfb92bc1a1d446ac1936e3553f5ae4f76a34fbc76e56ea5fabc" +"raw/jobs/getdp-f0008-b0001-P.tsv" = "b5cb24e313a2f7ad2c154f50f1b100f700f145a08193a8f8edadfcb03aeafb5a" +"raw/jobs/getdp-f0008-b0001-Z.tsv" = "04007b40d90923d74d88fbe86aa3243f6c15f2b63e7fd9373cafe4095703ae99" +"raw/jobs/getdp-f0008-b0001-timing.tsv" = "524d5374da5579aea44825b686434e557d441d01c7202e7c97a168af9814b4aa" +"raw/jobs/getdp-f0008-b0001.json" = "4620f880637d8bb9389c9986ebb943b3f9346824063a1e3521346e90f937a438" +"raw/jobs/getdp-f0008-b0002-P.tsv" = "c743dcb9a8d47c165a365c8621615125550fb98794496d4b4d0c84a1f306e3ed" +"raw/jobs/getdp-f0008-b0002-Z.tsv" = "5b7b6059261bc6e81a491d8fe9609d68103852e96d6fffe28788fc53ded06cc8" +"raw/jobs/getdp-f0008-b0002-timing.tsv" = "8b611c675d54531e81f98bc4878696f1ea13e0252f1788ff4bb20148db65d185" +"raw/jobs/getdp-f0008-b0002.json" = "228dba3f84f93a38f332bd249576afbab7c11d7ac423db2b2a7001cd59b55073" +"raw/jobs/getdp-f0008-b0003-P.tsv" = "358654a68195014400ca3e9eee9602ddd01b0a70be3f69f4cd09069fabad5cbb" +"raw/jobs/getdp-f0008-b0003-Z.tsv" = "d3cb527f9cdf2bc56449a3d84cbbad30fb01b406c725b963626d9c4e9ae87e05" +"raw/jobs/getdp-f0008-b0003-timing.tsv" = "3cb2a99be1b94193012284f026bca4b6194c763d0957c795198c3a2ae3caf35d" +"raw/jobs/getdp-f0008-b0003.json" = "642881f409c10ffc19f1a9300dd6cffa6264a2b15c006ec48e1da02f619ee0ea" +"raw/jobs/getdp-f0009-b0001-P.tsv" = "312177b45ff96145802d4f94cdc5ae6b2723548f4274cdf5ae676611b2acc29c" +"raw/jobs/getdp-f0009-b0001-Z.tsv" = "778105611da0cfce5d3635ef28e9345ffc0bfb6533fe7c4652a8b366ae0b5b78" +"raw/jobs/getdp-f0009-b0001-timing.tsv" = "9064552b387a3175b04931d1317af1d031edc577f67679e86f54d0d8e8be82d1" +"raw/jobs/getdp-f0009-b0001.json" = "5d3a35bd65fa9aa1cb3e10171d284bc40e6fedb38c3e8a9e2ea25a9a6797365d" +"raw/jobs/getdp-f0009-b0002-P.tsv" = "006824828b9b6c1f38aaf5b741364c615cca3bfbf5a3d12e68edea88adcad768" +"raw/jobs/getdp-f0009-b0002-Z.tsv" = "266b76ed64e522873ad8394b605ccaef34f3514ef9ad60a5f6740ff3d6872efe" +"raw/jobs/getdp-f0009-b0002-timing.tsv" = "81802d95fb3639ae62510ff48ef13aa7e9e804468da053efe0c655098c865c8c" +"raw/jobs/getdp-f0009-b0002.json" = "9e526454e3879ef5ae561155924abcae2c1e30794b60f0c5c32c83884463712b" +"raw/jobs/getdp-f0009-b0003-P.tsv" = "fab6f65e74720b6fdfb3f3531dbd815893bae9bfc02f9b6d8d30a3c923479cb5" +"raw/jobs/getdp-f0009-b0003-Z.tsv" = "08a5327a9f57103a1514ad2c72fb477f0276ef272e38f8c02fe26bcffab612f2" +"raw/jobs/getdp-f0009-b0003-timing.tsv" = "ebadfdacd79f8ac1b12044bab9d9b0d7dbe9998669849c4f40247e243c2e669d" +"raw/jobs/getdp-f0009-b0003.json" = "3abe3cec70b548cdef2c7e71ed35eeab48ec8a6366db4913a38c77de6732b167" +"raw/scan_complete.tsv" = "2cc9eff50bb7e6fff0e2555f96d93ebe952c4a1a158a1dca82fe6cca3aa1afab" +"run.json" = "6ccac2fce367b28b758052f4ff2707b8f9b4e7fc686d5563dd35ace4e4fee501" +"timing-summary.json" = "f343d06fbc9ddf36d0fcfe102ce80bf4dec7ac6e63928a89fba5c854b204441c" diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/fem/formulation.txt b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/fem/formulation.txt new file mode 100644 index 000000000..0e94e36ec --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/fem/formulation.txt @@ -0,0 +1 @@ +@NamedTuple{backend::Symbol, requested::NamedTuple, methods::NamedTuple, options::NamedTuple}((:fem, (insulation_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), semicon_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_properties = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), temperature_dependence = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing)), (insulation_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), semicon_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), earth_properties = (identifier = :constant, parameters = NamedTuple(), options = NamedTuple()), temperature_dependence = (identifier = :linear, parameters = NamedTuple(), options = NamedTuple())), (reduce_bundle = false, kron_reduction = false, ideal_transposition = false, physics = Symbol("quasi-tem")))) \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/fem/problem.json b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/fem/problem.json new file mode 100644 index 000000000..a4a9a71b8 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/fem/problem.json @@ -0,0 +1,516 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "root": { + "kind": "line_parameters_problem", + "earth_props": { + "kind": "earth_model", + "vertical_layers": false, + "layers": [ + { + "kind": "earth_layer", + "thickness": { + "special": "Inf", + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "rho": { + "special": "Inf", + "__type__": "Float64" + } + }, + { + "kind": "earth_layer", + "thickness": { + "special": "Inf", + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "rho": { + "value": 0.1, + "__type__": "Float64" + } + } + ] + }, + "Gamma": null, + "frequencies": [ + { + "value": 0.1, + "__type__": "Float64" + }, + { + "value": 1, + "__type__": "Float64" + }, + { + "value": 10, + "__type__": "Float64" + }, + { + "value": 100, + "__type__": "Float64" + }, + { + "value": 1000, + "__type__": "Float64" + }, + { + "value": 10000, + "__type__": "Float64" + }, + { + "value": 100000, + "__type__": "Float64" + }, + { + "value": 1000000, + "__type__": "Float64" + }, + { + "value": 10000000, + "__type__": "Float64" + } + ], + "system": { + "kind": "line_cable_system", + "input_positions": [ + { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 1, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 2, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + } + ], + "clearances": [ + [ + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + } + ], + [ + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + } + ], + [ + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + } + ] + ], + "connections": [ + [ + 1 + ], + [ + 2 + ], + [ + 3 + ] + ], + "line_length": { + "value": 1, + "__type__": "Float64" + }, + "environment": null, + "system_id": "three_bare_wires_air_2", + "positions": [ + { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 1, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 2, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + } + ], + "designs": [ + { + "kind": "cable_design", + "cable_id": "three_bare_wires_air_2", + "nominal_data": [ + ], + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "core_metal", + "material": { + "value": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "type": "material" + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.0425, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + } + ] + } + }, + { + "kind": "cable_design", + "cable_id": "three_bare_wires_air_2", + "nominal_data": [ + ], + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "core_metal", + "material": { + "value": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "type": "material" + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.0425, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + } + ] + } + }, + { + "kind": "cable_design", + "cable_id": "three_bare_wires_air_2", + "nominal_data": [ + ], + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "core_metal", + "material": { + "value": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "type": "material" + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.0425, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + } + ] + } + } + ] + }, + "temperature": { + "value": 20, + "__type__": "Float64" + } + }, + "format": "linecablemodels.line_parameters_problem", + "version": "1.0.0" +} \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/unified/Y.csv b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/unified/Y.csv new file mode 100644 index 000000000..09e69ac94 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/unified/Y.csv @@ -0,0 +1,82 @@ +frequency_hz,response_terminal,basis_terminal,real_S_per_m,imaginary_S_per_m +0.10000000000000001,cable:1:core,cable:1:core,8.7355481508841919,0.15942652467188378 +0.10000000000000001,cable:1:core,cable:2:core,-8.930318368991909e-14,-3.8246960722403318e-12 +0.10000000000000001,cable:1:core,cable:3:core,-6.23168818523399,0.15909201315618696 +0.10000000000000001,cable:2:core,cable:1:core,-1.1328091516975891e-13,-3.80398025758764e-12 +0.10000000000000001,cable:2:core,cable:2:core,2.4642676020193662e-24,9.0759067020569396e-12 +0.10000000000000001,cable:2:core,cable:3:core,-1.132809151697592e-13,-3.8039802575876384e-12 +0.10000000000000001,cable:3:core,cable:1:core,-6.2316881852339892,0.15909201315618723 +0.10000000000000001,cable:3:core,cable:2:core,-8.9303183689918913e-14,-3.8246960722403375e-12 +0.10000000000000001,cable:3:core,cable:3:core,8.7355481508841919,0.15942652467188365 +1,cable:1:core,cable:1:core,9.0103275798336604,0.23994395866859686 +1,cable:1:core,cable:2:core,-1.3006519886930481e-12,-3.6714104007694498e-11 +1,cable:1:core,cable:3:core,-5.9573061454881913,0.23724688397599955 +1,cable:2:core,cable:1:core,-2.1679704977038157e-12,-3.5992194033796974e-11 +1,cable:2:core,cable:2:core,2.3489444218284539e-22,9.0759067020557561e-11 +1,cable:2:core,cable:3:core,-2.1679704977038193e-12,-3.5992194033796994e-11 +1,cable:3:core,cable:1:core,-5.9573061454881913,0.23724688397599972 +1,cable:3:core,cable:2:core,-1.3006519886930526e-12,-3.6714104007694511e-11 +1,cable:3:core,cable:3:core,9.0103275798336639,0.23994395866859713 +10,cable:1:core,cable:1:core,9.4348014435331127,0.40409142627673555 +10,cable:1:core,cable:2:core,-2.0095834217647568e-11,-3.4435183438689684e-10 +10,cable:1:core,cable:3:core,-5.5367765108299043,0.38359059562742254 +10,cable:2:core,cable:1:core,-5.2503321288609543e-11,-3.1854092073286003e-10 +10,cable:2:core,cable:2:core,2.1161807741033108e-20,9.0759067020246181e-10 +10,cable:2:core,cable:3:core,-5.2503321288609504e-11,-3.1854092073285998e-10 +10,cable:3:core,cable:1:core,-5.5367765108299016,0.38359059562742254 +10,cable:3:core,cable:2:core,-2.009583421764763e-11,-3.4435183438689653e-10 +10,cable:3:core,cable:3:core,9.4348014435331109,0.40409142627673539 +100,cable:1:core,cable:1:core,10.176084148956001,0.81933149183126064 +100,cable:1:core,cable:2:core,-3.3523759845478488e-10,-3.0841680819833742e-09 +100,cable:1:core,cable:3:core,-4.8334558330735282,0.67829862083298809 +100,cable:2:core,cable:1:core,-1.581746719695652e-09,-2.122011708447857e-09 +100,cable:2:core,cable:2:core,1.4981391520430641e-18,9.0759067008968554e-09 +100,cable:2:core,cable:3:core,-1.5817467196956539e-09,-2.1220117084478578e-09 +100,cable:3:core,cable:1:core,-4.8334558330735273,0.67829862083298798 +100,cable:3:core,cable:2:core,-3.3523759845478545e-10,-3.0841680819833763e-09 +100,cable:3:core,cable:3:core,10.176084148956003,0.81933149183126097 +1000,cable:1:core,cable:1:core,11.809017599813723,2.0929924735851584 +1000,cable:1:core,cable:2:core,-6.3490118848034141e-09,-2.4635775180730038e-08 +1000,cable:1:core,cable:3:core,-3.5269130232082677,1.2832753712249108 +1000,cable:2:core,cable:1:core,-5.2859299514753883e-08,1.5103867257715344e-08 +1000,cable:2:core,cable:2:core,-8.3130691160873046e-17,9.0759066493688587e-08 +1000,cable:2:core,cable:3:core,-5.2859299514753903e-08,1.5103867257715347e-08 +1000,cable:3:core,cable:1:core,-3.5269130232082677,1.2832753712249112 +1000,cable:3:core,cable:2:core,-6.3490118848034166e-09,-2.4635775180730051e-08 +1000,cable:3:core,cable:3:core,11.809017599813725,2.0929924735851584 +10000,cable:1:core,cable:1:core,16.805291651222536,5.0791514818981653 +10000,cable:1:core,cable:2:core,-1.1301307810588102e-07,-1.0140418600667815e-07 +10000,cable:1:core,cable:3:core,-0.67949761171491874,1.7210202669171364 +10000,cable:2:core,cable:1:core,-9.0657832770881726e-07,1.7798079901742443e-06 +10000,cable:2:core,cable:2:core,-1.3778400723881355e-13,9.0759043608914856e-07 +10000,cable:2:core,cable:3:core,-9.0657832770881716e-07,1.7798079901742439e-06 +10000,cable:3:core,cable:1:core,-0.67949761171491496,1.7210202669171399 +10000,cable:3:core,cable:2:core,-1.1301307631509544e-07,-1.0140418535528041e-07 +10000,cable:3:core,cable:3:core,16.805291651222536,5.0791514818981671 +100000,cable:1:core,cable:1:core,25.147933620553914,8.0354712542214894 +100000,cable:1:core,cable:2:core,-1.3387578410475514e-07,1.5904860038606757e-07 +100000,cable:1:core,cable:3:core,0.083093297211124295,-0.071868313214637519 +100000,cable:2:core,cable:1:core,1.7003931797506463e-05,8.4429904748540932e-06 +100000,cable:2:core,cable:2:core,-5.4864148189373549e-11,9.0758159057763065e-06 +100000,cable:2:core,cable:3:core,1.7003931797506473e-05,8.4429904748541e-06 +100000,cable:3:core,cable:1:core,0.083093297211126876,-0.071868313214642834 +100000,cable:3:core,cable:2:core,-1.3387578295842361e-07,1.5904859727845923e-07 +100000,cable:3:core,cable:3:core,25.147933620553918,8.0354712542214912 +1000000,cable:1:core,cable:1:core,40.833765344235985,19.54915991569575 +1000000,cable:1:core,cable:2:core,-3.6994907304567155e-09,-1.4563314765271918e-08 +1000000,cable:1:core,cable:3:core,-2.7224977747745251e-05,-2.5263792729529432e-05 +1000000,cable:2:core,cable:1:core,-7.8026985217373471e-06,5.4423466057092621e-06 +1000000,cable:2:core,cable:2:core,-2.4719343778092328e-08,9.0711547556388583e-05 +1000000,cable:2:core,cable:3:core,-7.8026985217373471e-06,5.4423466057092638e-06 +1000000,cable:3:core,cable:1:core,-2.7224977747745251e-05,-2.5263792729529435e-05 +1000000,cable:3:core,cable:2:core,-3.6994907304567147e-09,-1.4563314765271921e-08 +1000000,cable:3:core,cable:3:core,40.833765344235985,19.54915991569575 +10000000,cable:1:core,cable:1:core,80.846531351828517,55.108195401013525 +10000000,cable:1:core,cable:2:core,-1.1003845571285191e-13,-1.3743080710577727e-13 +10000000,cable:1:core,cable:3:core,-2.7205597045460625e-16,1.4802234773622071e-17 +10000000,cable:2:core,cable:1:core,-9.3440467076589953e-11,5.4261406955040871e-10 +10000000,cable:2:core,cable:2:core,-1.6761328755330462e-05,0.00088777830561370815 +10000000,cable:2:core,cable:3:core,-9.3440467076589953e-11,5.4261406955040871e-10 +10000000,cable:3:core,cable:1:core,-2.7205597045460621e-16,1.4802234773622071e-17 +10000000,cable:3:core,cable:2:core,-1.1003845571285191e-13,-1.3743080710577727e-13 +10000000,cable:3:core,cable:3:core,80.846531351828517,55.108195401013525 diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/unified/Z.csv b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/unified/Z.csv new file mode 100644 index 000000000..7e5a23624 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/unified/Z.csv @@ -0,0 +1,82 @@ +frequency_hz,response_terminal,basis_terminal,real_ohm_per_m,imaginary_ohm_per_m +0.10000000000000001,cable:1:core,cable:1:core,3.1374615586823521e-06,1.2435841206410949e-06 +0.10000000000000001,cable:1:core,cable:2:core,9.86920964797057e-08,7.1450042872923138e-07 +0.10000000000000001,cable:1:core,cable:3:core,9.9020834415946147e-08,7.2818833937590774e-07 +0.10000000000000001,cable:2:core,cable:1:core,9.8692096479705673e-08,7.1450042872923117e-07 +0.10000000000000001,cable:2:core,cable:2:core,3.1368030409187459e-06,1.2442491570135629e-06 +0.10000000000000001,cable:2:core,cable:3:core,9.869209647970566e-08,7.1450042872923106e-07 +0.10000000000000001,cable:3:core,cable:1:core,9.9020834415946173e-08,7.2818833937590774e-07 +0.10000000000000001,cable:3:core,cable:2:core,9.8692096479705687e-08,7.1450042872923138e-07 +0.10000000000000001,cable:3:core,cable:3:core,3.1374615586823521e-06,1.2435841206410949e-06 +1,cable:1:core,cable:1:core,4.04626258403795e-06,1.0981370487568925e-05 +1,cable:1:core,cable:2:core,9.8663957192484614e-07,5.6982925133975858e-06 +1,cable:1:core,cable:3:core,9.9679391273336713e-07,5.828000839227387e-06 +1,cable:2:core,cable:1:core,9.8663957192484656e-07,5.6982925133975867e-06 +1,cable:2:core,cable:2:core,4.0258226362737365e-06,1.1002346949218488e-05 +1,cable:2:core,cable:3:core,9.8663957192484656e-07,5.6982925133975875e-06 +1,cable:3:core,cable:1:core,9.9679391273336713e-07,5.828000839227387e-06 +1,cable:3:core,cable:2:core,9.8663957192484614e-07,5.6982925133975858e-06 +1,cable:3:core,cable:3:core,4.04626258403795e-06,1.0981370487568925e-05 +10,cable:1:core,cable:1:core,1.4059707866181184e-05,9.4699367070903468e-05 +10,cable:1:core,cable:2:core,9.8448383311021314e-06,4.2519807808567804e-05 +10,cable:1:core,cable:3:core,1.0145003866507116e-05,4.3592250138396838e-05 +10,cable:2:core,cable:1:core,9.8448383311021297e-06,4.251980780856779e-05 +10,cable:2:core,cable:2:core,1.3443861420288216e-05,9.5357297592647074e-05 +10,cable:2:core,cable:3:core,9.8448383311021314e-06,4.251980780856779e-05 +10,cable:3:core,cable:1:core,1.0145003866507114e-05,4.3592250138396831e-05 +10,cable:3:core,cable:2:core,9.8448383311021297e-06,4.2519807808567797e-05 +10,cable:3:core,cable:3:core,1.4059707866181184e-05,9.4699367070903468e-05 +100,cable:1:core,cable:1:core,0.00011750705949953411,0.00077809535148733395 +100,cable:1:core,cable:2:core,9.6940022347522735e-05,0.00028095440727846685 +100,cable:1:core,cable:3:core,0.00010470316710127808,0.00028484344129470283 +100,cable:2:core,cable:1:core,9.6940022347522721e-05,0.00028095440727846685 +100,cable:2:core,cable:2:core,0.00010028978112835013,0.00079835650985761984 +100,cable:2:core,cable:3:core,9.6940022347522707e-05,0.00028095440727846685 +100,cable:3:core,cable:1:core,0.00010470316710127808,0.00028484344129470288 +100,cable:3:core,cable:2:core,9.6940022347522735e-05,0.00028095440727846679 +100,cable:3:core,cable:3:core,0.0001175070594995341,0.00077809535148733395 +1000,cable:1:core,cable:1:core,0.0011876763663740098,0.0060820686216245231 +1000,cable:1:core,cable:2:core,0.00088058638790435755,0.0014022673190863721 +1000,cable:1:core,cable:3:core,0.0010032868752651413,0.0012637996350769523 +1000,cable:2:core,cable:1:core,0.00088058638790435766,0.0014022673190863724 +1000,cable:2:core,cable:2:core,0.00078848453847925489,0.0066685046727502926 +1000,cable:2:core,cable:3:core,0.00088058638790435777,0.0014022673190863726 +1000,cable:3:core,cable:1:core,0.0010032868752651413,0.0012637996350769521 +1000,cable:3:core,cable:2:core,0.00088058638790435755,0.0014022673190863721 +1000,cable:3:core,cable:3:core,0.0011876763663740098,0.0060820686216245257 +10000,cable:1:core,cable:1:core,0.011216148512475916,0.043043091699419986 +10000,cable:1:core,cable:2:core,0.0052675025954369405,0.0024772829778800798 +10000,cable:1:core,cable:3:core,0.0043841369179740434,-0.00021988065450588795 +10000,cable:2:core,cable:1:core,0.0052675025954373872,0.0024772829778798994 +10000,cable:2:core,cable:2:core,0.0051972641718355589,0.056899313416344022 +10000,cable:2:core,cable:3:core,0.0052675025954373881,0.0024772829778799002 +10000,cable:3:core,cable:1:core,0.0043841369179740443,-0.00021988065450588793 +10000,cable:3:core,cable:2:core,0.0052675025954369414,0.0024772829778800802 +10000,cable:3:core,cable:3:core,0.011216148512475916,0.043043091699419986 +100000,cable:1:core,cable:1:core,0.09187042463772542,0.2852670576877403 +100000,cable:1:core,cable:2:core,0.0029207944960417966,-0.0055282682560884875 +100000,cable:1:core,cable:3:core,-0.001431146617135812,-0.00035959674779835108 +100000,cable:2:core,cable:1:core,0.0029207944960417962,-0.0055282682560884883 +100000,cable:2:core,cable:2:core,0.025028674693899078,0.51501620085517463 +100000,cable:2:core,cable:3:core,0.0029207944960417966,-0.0055282682560884883 +100000,cable:3:core,cable:1:core,-0.0014311466171358115,-0.00035959674779835097 +100000,cable:3:core,cable:2:core,0.0029207944960417962,-0.0055282682560884875 +100000,cable:3:core,cable:3:core,0.091870424637725434,0.2852670576877403 +1000000,cable:1:core,cable:1:core,0.75407446569154857,1.5740479213690892 +1000000,cable:1:core,cable:2:core,0.00017862953015392795,0.00022533894904642595 +1000000,cable:1:core,cable:3:core,2.3887617723989184e-07,1.4069502440255175e-06 +1000000,cable:2:core,cable:1:core,0.00017862953015392781,0.00022533894904642752 +1000000,cable:2:core,cable:2:core,0.094696109113519056,4.9430140519244823 +1000000,cable:2:core,cable:3:core,0.00017862953015392781,0.00022533894904642758 +1000000,cable:3:core,cable:1:core,2.3887617723989178e-07,1.4069502440255175e-06 +1000000,cable:3:core,cable:2:core,0.00017862953015392795,0.00022533894904642598 +1000000,cable:3:core,cable:3:core,0.75407446569154857,1.5740479213690892 +10000000,cable:1:core,cable:1:core,4.5479347057971937,6.6714050104071516 +10000000,cable:1:core,cable:2:core,1.5377810804168995e-09,4.4664472009509879e-10 +10000000,cable:1:core,cable:3:core,2.1364256347378569e-17,7.0038536780304023e-18 +10000000,cable:2:core,cable:1:core,1.5377810804169031e-09,4.4664472009510029e-10 +10000000,cable:2:core,cable:2:core,1.2514510556301526,49.784417123982301 +10000000,cable:2:core,cable:3:core,1.5377810804169031e-09,4.4664472009510029e-10 +10000000,cable:3:core,cable:1:core,2.1364256347378569e-17,7.0038536780304008e-18 +10000000,cable:3:core,cable:2:core,1.5377810804168995e-09,4.4664472009509874e-10 +10000000,cable:3:core,cable:3:core,4.5479347057971937,6.6714050104071516 diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/unified/computation-options.txt b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/unified/computation-options.txt new file mode 100644 index 000000000..3c73cbbf9 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/unified/computation-options.txt @@ -0,0 +1 @@ +(trace = true, output_basis = :pul) \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/unified/details.txt b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/unified/details.txt new file mode 100644 index 000000000..9d744b360 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/unified/details.txt @@ -0,0 +1 @@ +@NamedTuple{shunt_model::NamedTuple, trace::@NamedTuple{phase_map::Vector{Int64}, cable_map::Vector{Int64}, Zin::Array{ComplexF64, 3}, Pin::Array{ComplexF64, 3}, Zg::Array{ComplexF64, 3}, Pg::Array{ComplexF64, 3}, Z::Array{ComplexF64, 3}, P::Array{ComplexF64, 3}}, formulations::@NamedTuple{effective::@NamedTuple{internal_impedance::Union{Nothing, Symbol, NamedTuple}, insulation_impedance::Union{Nothing, Symbol, NamedTuple}, earth_impedance::Union{Nothing, Symbol, NamedTuple}, shunt_model::Union{Nothing, Symbol, NamedTuple}, insulation_admittance::Union{Nothing, Symbol, NamedTuple}, semicon_admittance::Union{Nothing, Symbol, NamedTuple}, earth_admittance::Union{Nothing, Symbol, NamedTuple}, earth_properties::Union{Nothing, Symbol, NamedTuple}, pipe_impedance::Union{Nothing, Symbol, NamedTuple}, temperature_dependence::Union{Nothing, Symbol, NamedTuple}}, numerical::@NamedTuple{internal_impedance::NamedTuple, insulation_impedance::NamedTuple, shunt_model::NamedTuple, insulation_admittance::NamedTuple, semicon_admittance::NamedTuple, earth_properties::NamedTuple, temperature_dependence::NamedTuple, earth_impedance::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}, earth_admittance::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}, equivalent_earth::@NamedTuple{earth_impedance::Union{Nothing, @NamedTuple{air::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, mixed::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth_admittance::Union{Nothing, @NamedTuple{air::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, mixed::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, backend::Symbol, requested::NamedTuple, methods::NamedTuple, options::NamedTuple}, coordinates::Vector{String}}(((requested = :coaxial, effective = :coaxial, solves = 0, domains = @NamedTuple{design::Int64, terminals::UnitRange{Int64}, requested::Symbol, effective::Symbol, reason::Symbol, message::String}[], diagnostics = @NamedTuple{boundary_residual::Union{Nothing, Float64}, wire_residual::Union{Nothing, Float64}, tape_residual::Union{Nothing, Float64}, common_residual::Union{Nothing, Float64}, penetration_indicator::Union{Nothing, Float64}, reciprocity::Float64, log_moment_error::Float64, unknowns::Int64, equations::Int64, matrix_bytes::Int64, level::@NamedTuple{wire::Int64, order::Int64, quadrature::Int64, modes::Int64}}[]), (phase_map = [1, 2, 3], cable_map = [1, 2, 3], Zin = ComplexF64[3.0384367161282473e-6 + 3.1415178272814096e-8im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 3.0384367161282473e-6 + 3.1415178272814096e-8im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 3.0384367161282473e-6 + 3.1415178272814096e-8im;;; 3.0491254273478614e-6 + 3.135993190598609e-7im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 3.0491254273478614e-6 + 3.135993190598609e-7im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 3.0491254273478614e-6 + 3.135993190598609e-7im;;; 3.886234398887704e-6 + 2.71311425444745e-6im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 3.886234398887704e-6 + 2.71311425444745e-6im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 3.886234398887704e-6 + 2.71311425444745e-6im;;; 1.0572845476093172e-5 + 9.718155377880437e-6im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 1.0572845476093172e-5 + 9.718155377880437e-6im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 1.0572845476093172e-5 + 9.718155377880437e-6im;;; 3.1668762815475746e-5 + 3.088048172738763e-5im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 3.1668762815475746e-5 + 3.088048172738763e-5im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 3.1668762815475746e-5 + 3.088048172738763e-5im;;; 9.846320725460222e-5 + 9.76946980065643e-5im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 9.846320725460222e-5 + 9.76946980065643e-5im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 9.846320725460222e-5 + 9.76946980065643e-5im;;; 0.0003097129699189164 + 0.0003089505796854997im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0003097129699189164 + 0.0003089505796854997im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0003097129699189164 + 0.0003089505796854997im;;; 0.0009777519922518445 + 0.0009769915236240853im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0009777519922518445 + 0.0009769915236240853im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0009777519922518445 + 0.0009769915236240853im;;; 0.0030902795943006773 + 0.0030895197319977846im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0030902795943006773 + 0.0030895197319977846im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0030902795943006773 + 0.0030895197319977846im], Pin = ComplexF64[0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im], Zg = ComplexF64[9.902484255410494e-8 + 1.2121689423682808e-6im 9.86920964797057e-8 + 7.145004287292314e-7im 9.902083441594615e-8 + 7.281883393759077e-7im; 9.869209647970567e-8 + 7.145004287292312e-7im 9.836632479049869e-8 + 1.2128339787407488e-6im 9.869209647970566e-8 + 7.145004287292311e-7im; 9.902083441594617e-8 + 7.281883393759077e-7im 9.869209647970569e-8 + 7.145004287292314e-7im 9.902484255410494e-8 + 1.2121689423682808e-6im;;; 9.971371566900882e-7 + 1.0667771168509064e-5im 9.866395719248461e-7 + 5.698292513397586e-6im 9.967939127333671e-7 + 5.828000839227387e-6im; 9.866395719248466e-7 + 5.698292513397587e-6im 9.766972089258747e-7 + 1.0688747630158626e-5im 9.866395719248466e-7 + 5.6982925133975875e-6im; 9.967939127333671e-7 + 5.828000839227387e-6im 9.866395719248461e-7 + 5.698292513397586e-6im 9.971371566900882e-7 + 1.0667771168509064e-5im;;; 1.0173473467293479e-5 + 9.198625281645601e-5im 9.844838331102131e-6 + 4.2519807808567804e-5im 1.0145003866507116e-5 + 4.359225013839684e-5im; 9.84483833110213e-6 + 4.251980780856779e-5im 9.557627021400511e-6 + 9.264418333819962e-5im 9.844838331102131e-6 + 4.251980780856779e-5im; 1.0145003866507114e-5 + 4.359225013839683e-5im 9.84483833110213e-6 + 4.25198078085678e-5im 1.017347346729348e-5 + 9.198625281645601e-5im;;; 0.00010693421402344094 + 0.0007683771961094535im 9.694002234752273e-5 + 0.00028095440727846685im 0.00010470316710127808 + 0.00028484344129470283im; 9.694002234752272e-5 + 0.00028095440727846685im 8.971693565225696e-5 + 0.0007886383544797394im 9.694002234752271e-5 + 0.00028095440727846685im; 0.00010470316710127808 + 0.0002848434412947029im 9.694002234752273e-5 + 0.0002809544072784668im 0.00010693421402344093 + 0.0007683771961094535im;;; 0.001156007603558534 + 0.006051188139897135im 0.0008805863879043575 + 0.0014022673190863721im 0.0010032868752651413 + 0.0012637996350769523im; 0.0008805863879043577 + 0.0014022673190863724im 0.0007568157756637792 + 0.006637624191022905im 0.0008805863879043578 + 0.0014022673190863726im; 0.0010032868752651413 + 0.001263799635076952im 0.0008805863879043575 + 0.0014022673190863721im 0.001156007603558534 + 0.006051188139897138im;;; 0.011117685305221314 + 0.04294539700141342im 0.0052675025954369405 + 0.00247728297788008im 0.004384136917974043 - 0.00021988065450588795im; 0.005267502595437387 + 0.0024772829778798994im 0.0050988009645809565 + 0.05680161871833746im 0.005267502595437388 + 0.0024772829778799002im; 0.004384136917974044 - 0.00021988065450588793im 0.005267502595436941 + 0.0024772829778800802im 0.011117685305221314 + 0.04294539700141342im;;; 0.09156071166780651 + 0.2849581071080548im 0.0029207944960417966 - 0.0055282682560884875im -0.001431146617135812 - 0.0003595967477983511im; 0.002920794496041796 - 0.005528268256088488im 0.02471896172398016 + 0.5147072502754891im 0.0029207944960417966 - 0.005528268256088488im; -0.0014311466171358115 - 0.00035959674779835097im 0.002920794496041796 - 0.0055282682560884875im 0.09156071166780652 + 0.2849581071080548im;;; 0.7530967136992968 + 1.573070929845465im 0.00017862953015392795 + 0.00022533894904642595im 2.3887617723989184e-7 + 1.4069502440255175e-6im; 0.0001786295301539278 + 0.00022533894904642752im 0.0937183571212672 + 4.9420370604008586im 0.0001786295301539278 + 0.00022533894904642758im; 2.388761772398918e-7 + 1.4069502440255175e-6im 0.00017862953015392795 + 0.00022533894904642598im 0.7530967136992968 + 1.573070929845465im;;; 4.544844426202893 + 6.668315490675154im 1.5377810804168995e-9 + 4.466447200950988e-10im 2.136425634737857e-17 + 7.003853678030402e-18im; 1.537781080416903e-9 + 4.466447200951003e-10im 1.248360776035852 + 49.7813276042503im 1.537781080416903e-9 + 4.466447200951003e-10im; 2.136425634737857e-17 + 7.0038536780304e-18im 1.5377810804168995e-9 + 4.4664472009509874e-10im 4.544844426202893 + 6.668315490675154im], Pg = ComplexF64[0.015707450986152026 + 0.14446167915439295im 0.01566804646760189 + 0.10375596651489519im 0.01570651275962332 + 0.10248208343651923im; 0.016248747168102633 + 0.10310932293474967im 6.922928489069867e10 + 0.10538021494028615im 0.016248747168102633 + 0.10310932293474967im; 0.015706512759623325 + 0.10248208343651923im 0.01566804646760189 + 0.1037559665148952im 0.015707450986152026 + 0.14446167915439295im;;; 0.15703793041660494 + 1.2143641020374698im 0.15581002945644956 + 0.8081633554408221im 0.15696228780158744 + 0.7945793038207634im; 0.17251044253781592 + 0.7891832740084662im 6.922928489085417e10 + 0.8127136845323917im 0.17251044253781592 + 0.7891832740084662im; 0.15696228780158744 + 0.7945793038207634im 0.15581002945644956 + 0.8081633554408221im 0.15703793041660494 + 1.2143641020374696im;;; 1.567571137292638 + 9.841632725281324im 1.5302359625548283 + 5.80650259554005im 1.5618244791809552 + 5.644898389591565im; 1.9942193536037005 + 5.2543402807096im 6.922928489267545e10 + 5.5130020685373164im 1.9942193536037005 + 5.2543402807096im; 1.561824479180955 + 5.644898389591563im 1.5302359625548285 + 5.806502595540049im 1.567571137292638 + 9.841632725281324im;;; 15.479104974644029 + 75.4472391592675im 14.414050532337209 + 35.92386058367612im 15.085802110086975 + 33.589656392707106im; 26.14921575966495 + 20.166759483777966im 6.922928491879367e10 + 23.20187507351694im 26.149215759664955 + 20.166759483777966im; 15.085802110086975 + 33.5896563927071im 14.414050532337209 + 35.92386058367612im 15.479104974644027 + 75.44723915926748im;;; 143.38409719054815 + 529.5505208755997im 117.49333490746795 + 158.03086396978034im 121.8124200827982 + 120.98592148357612im; 334.74790794696156 - 262.7144499994028im 6.922928543755035e10 - 252.86799853057568im 334.7479079469617 - 262.7144499994028im; 121.81242008279821 + 120.9859214835761im 117.49333490746795 + 158.03086396978037im 143.38409719054812 + 529.5505208755997im;;; 1030.312137001452 + 3386.882930875641im 567.7856781680553 + 195.9034895024706im 364.6938366898155 - 78.79335317906869im; 568.7570434663479 - 7880.69842885873im 6.922930091313478e10 - 12412.554860739992im 568.7570434663479 - 7880.698428858731im; 364.69383668981556 - 78.79335317906867im 567.7856781680554 + 195.9034895024706im 1030.312137001452 + 3386.882930875641im;;; 7244.089591533101 + 22670.0096559975im 208.78986073306797 - 502.30640358220063im -96.21863039641438 - 23.45864842268956im; -49078.80910625864 - 7675.344253436806im 6.92299789655214e10 - 416784.72005565686im -49078.80910625866 - 7675.344253436808im; -96.21863039641435 - 23.458648422689553im 208.78986073306794 - 502.30640358220063im 7244.089591533102 + 22670.009655997495im;;; 59930.24456252724 + 125180.70105485745im 14.731584058037516 + 17.64904288065159im 0.016451661164062895 + 0.11266530087389895im; 7168.588643851441 - 12667.321520067413im 6.926554567714917e10 - 1.88752072221334e7im 7168.588643851441 - 12667.321520067415im; 0.016451661164062895 + 0.11266530087389895im 14.731584058037516 + 17.64904288065159im 59930.244562527245 + 125180.70105485746im;;; 361696.7481463518 + 530627.5641232601im 0.00012242285716661654 + 3.499978863532959e-5im 1.697450816722568e-12 + 5.61556157150826e-13im; -0.17200170828726433 - 0.3591438491751954im 7.074904147370567e10 - 1.3357478277705052e9im -0.17200170828726433 - 0.3591438491751954im; 1.6974508167225679e-12 + 5.615561571508258e-13im 0.00012242285716661654 + 3.499978863532959e-5im 361696.7481463518 + 530627.5641232601im], Z = ComplexF64[3.137461558682352e-6 + 1.243584120641095e-6im 9.86920964797057e-8 + 7.145004287292314e-7im 9.902083441594615e-8 + 7.281883393759077e-7im; 9.869209647970567e-8 + 7.145004287292312e-7im 3.136803040918746e-6 + 1.244249157013563e-6im 9.869209647970566e-8 + 7.145004287292311e-7im; 9.902083441594617e-8 + 7.281883393759077e-7im 9.869209647970569e-8 + 7.145004287292314e-7im 3.137461558682352e-6 + 1.243584120641095e-6im;;; 4.04626258403795e-6 + 1.0981370487568925e-5im 9.866395719248461e-7 + 5.698292513397586e-6im 9.967939127333671e-7 + 5.828000839227387e-6im; 9.866395719248466e-7 + 5.698292513397587e-6im 4.0258226362737365e-6 + 1.1002346949218488e-5im 9.866395719248466e-7 + 5.6982925133975875e-6im; 9.967939127333671e-7 + 5.828000839227387e-6im 9.866395719248461e-7 + 5.698292513397586e-6im 4.04626258403795e-6 + 1.0981370487568925e-5im;;; 1.4059707866181184e-5 + 9.469936707090347e-5im 9.844838331102131e-6 + 4.2519807808567804e-5im 1.0145003866507116e-5 + 4.359225013839684e-5im; 9.84483833110213e-6 + 4.251980780856779e-5im 1.3443861420288216e-5 + 9.535729759264707e-5im 9.844838331102131e-6 + 4.251980780856779e-5im; 1.0145003866507114e-5 + 4.359225013839683e-5im 9.84483833110213e-6 + 4.25198078085678e-5im 1.4059707866181184e-5 + 9.469936707090347e-5im;;; 0.00011750705949953411 + 0.0007780953514873339im 9.694002234752273e-5 + 0.00028095440727846685im 0.00010470316710127808 + 0.00028484344129470283im; 9.694002234752272e-5 + 0.00028095440727846685im 0.00010028978112835013 + 0.0007983565098576198im 9.694002234752271e-5 + 0.00028095440727846685im; 0.00010470316710127808 + 0.0002848434412947029im 9.694002234752273e-5 + 0.0002809544072784668im 0.0001175070594995341 + 0.0007780953514873339im;;; 0.0011876763663740098 + 0.006082068621624523im 0.0008805863879043575 + 0.0014022673190863721im 0.0010032868752651413 + 0.0012637996350769523im; 0.0008805863879043577 + 0.0014022673190863724im 0.0007884845384792549 + 0.0066685046727502926im 0.0008805863879043578 + 0.0014022673190863726im; 0.0010032868752651413 + 0.001263799635076952im 0.0008805863879043575 + 0.0014022673190863721im 0.0011876763663740098 + 0.006082068621624526im;;; 0.011216148512475916 + 0.043043091699419986im 0.0052675025954369405 + 0.00247728297788008im 0.004384136917974043 - 0.00021988065450588795im; 0.005267502595437387 + 0.0024772829778798994im 0.005197264171835559 + 0.05689931341634402im 0.005267502595437388 + 0.0024772829778799002im; 0.004384136917974044 - 0.00021988065450588793im 0.005267502595436941 + 0.0024772829778800802im 0.011216148512475916 + 0.043043091699419986im;;; 0.09187042463772542 + 0.2852670576877403im 0.0029207944960417966 - 0.0055282682560884875im -0.001431146617135812 - 0.0003595967477983511im; 0.002920794496041796 - 0.005528268256088488im 0.025028674693899078 + 0.5150162008551746im 0.0029207944960417966 - 0.005528268256088488im; -0.0014311466171358115 - 0.00035959674779835097im 0.002920794496041796 - 0.0055282682560884875im 0.09187042463772543 + 0.2852670576877403im;;; 0.7540744656915486 + 1.5740479213690892im 0.00017862953015392795 + 0.00022533894904642595im 2.3887617723989184e-7 + 1.4069502440255175e-6im; 0.0001786295301539278 + 0.00022533894904642752im 0.09469610911351906 + 4.943014051924482im 0.0001786295301539278 + 0.00022533894904642758im; 2.388761772398918e-7 + 1.4069502440255175e-6im 0.00017862953015392795 + 0.00022533894904642598im 0.7540744656915486 + 1.5740479213690892im;;; 4.547934705797194 + 6.671405010407152im 1.5377810804168995e-9 + 4.466447200950988e-10im 2.136425634737857e-17 + 7.003853678030402e-18im; 1.537781080416903e-9 + 4.466447200951003e-10im 1.2514510556301526 + 49.7844171239823im 1.537781080416903e-9 + 4.466447200951003e-10im; 2.136425634737857e-17 + 7.0038536780304e-18im 1.5377810804168995e-9 + 4.4664472009509874e-10im 4.547934705797194 + 6.671405010407152im], P = ComplexF64[0.015707450986152026 + 0.14446167915439295im 0.01566804646760189 + 0.10375596651489519im 0.01570651275962332 + 0.10248208343651923im; 0.016248747168102633 + 0.10310932293474967im 6.922928489069867e10 + 0.10538021494028615im 0.016248747168102633 + 0.10310932293474967im; 0.015706512759623325 + 0.10248208343651923im 0.01566804646760189 + 0.1037559665148952im 0.015707450986152026 + 0.14446167915439295im;;; 0.15703793041660494 + 1.2143641020374698im 0.15581002945644956 + 0.8081633554408221im 0.15696228780158744 + 0.7945793038207634im; 0.17251044253781592 + 0.7891832740084662im 6.922928489085417e10 + 0.8127136845323917im 0.17251044253781592 + 0.7891832740084662im; 0.15696228780158744 + 0.7945793038207634im 0.15581002945644956 + 0.8081633554408221im 0.15703793041660494 + 1.2143641020374696im;;; 1.567571137292638 + 9.841632725281324im 1.5302359625548283 + 5.80650259554005im 1.5618244791809552 + 5.644898389591565im; 1.9942193536037005 + 5.2543402807096im 6.922928489267545e10 + 5.5130020685373164im 1.9942193536037005 + 5.2543402807096im; 1.561824479180955 + 5.644898389591563im 1.5302359625548285 + 5.806502595540049im 1.567571137292638 + 9.841632725281324im;;; 15.479104974644029 + 75.4472391592675im 14.414050532337209 + 35.92386058367612im 15.085802110086975 + 33.589656392707106im; 26.14921575966495 + 20.166759483777966im 6.922928491879367e10 + 23.20187507351694im 26.149215759664955 + 20.166759483777966im; 15.085802110086975 + 33.5896563927071im 14.414050532337209 + 35.92386058367612im 15.479104974644027 + 75.44723915926748im;;; 143.38409719054815 + 529.5505208755997im 117.49333490746795 + 158.03086396978034im 121.8124200827982 + 120.98592148357612im; 334.74790794696156 - 262.7144499994028im 6.922928543755035e10 - 252.86799853057568im 334.7479079469617 - 262.7144499994028im; 121.81242008279821 + 120.9859214835761im 117.49333490746795 + 158.03086396978037im 143.38409719054812 + 529.5505208755997im;;; 1030.312137001452 + 3386.882930875641im 567.7856781680553 + 195.9034895024706im 364.6938366898155 - 78.79335317906869im; 568.7570434663479 - 7880.69842885873im 6.922930091313478e10 - 12412.554860739992im 568.7570434663479 - 7880.698428858731im; 364.69383668981556 - 78.79335317906867im 567.7856781680554 + 195.9034895024706im 1030.312137001452 + 3386.882930875641im;;; 7244.089591533101 + 22670.0096559975im 208.78986073306797 - 502.30640358220063im -96.21863039641438 - 23.45864842268956im; -49078.80910625864 - 7675.344253436806im 6.92299789655214e10 - 416784.72005565686im -49078.80910625866 - 7675.344253436808im; -96.21863039641435 - 23.458648422689553im 208.78986073306794 - 502.30640358220063im 7244.089591533102 + 22670.009655997495im;;; 59930.24456252724 + 125180.70105485745im 14.731584058037516 + 17.64904288065159im 0.016451661164062895 + 0.11266530087389895im; 7168.588643851441 - 12667.321520067413im 6.926554567714917e10 - 1.88752072221334e7im 7168.588643851441 - 12667.321520067415im; 0.016451661164062895 + 0.11266530087389895im 14.731584058037516 + 17.64904288065159im 59930.244562527245 + 125180.70105485746im;;; 361696.7481463518 + 530627.5641232601im 0.00012242285716661654 + 3.499978863532959e-5im 1.697450816722568e-12 + 5.61556157150826e-13im; -0.17200170828726433 - 0.3591438491751954im 7.074904147370567e10 - 1.3357478277705052e9im -0.17200170828726433 - 0.3591438491751954im; 1.6974508167225679e-12 + 5.615561571508258e-13im 0.00012242285716661654 + 3.499978863532959e-5im 361696.7481463518 + 530627.5641232601im]), @NamedTuple{effective::@NamedTuple{internal_impedance::Union{Nothing, Symbol, NamedTuple}, insulation_impedance::Union{Nothing, Symbol, NamedTuple}, earth_impedance::Union{Nothing, Symbol, NamedTuple}, shunt_model::Union{Nothing, Symbol, NamedTuple}, insulation_admittance::Union{Nothing, Symbol, NamedTuple}, semicon_admittance::Union{Nothing, Symbol, NamedTuple}, earth_admittance::Union{Nothing, Symbol, NamedTuple}, earth_properties::Union{Nothing, Symbol, NamedTuple}, pipe_impedance::Union{Nothing, Symbol, NamedTuple}, temperature_dependence::Union{Nothing, Symbol, NamedTuple}}, numerical::@NamedTuple{internal_impedance::NamedTuple, insulation_impedance::NamedTuple, shunt_model::NamedTuple, insulation_admittance::NamedTuple, semicon_admittance::NamedTuple, earth_properties::NamedTuple, temperature_dependence::NamedTuple, earth_impedance::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}, earth_admittance::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}, equivalent_earth::@NamedTuple{earth_impedance::Union{Nothing, @NamedTuple{air::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, mixed::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth_admittance::Union{Nothing, @NamedTuple{air::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, mixed::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, backend::Symbol, requested::NamedTuple, methods::NamedTuple, options::NamedTuple}((@NamedTuple{internal_impedance::Union{Nothing, Symbol, NamedTuple}, insulation_impedance::Union{Nothing, Symbol, NamedTuple}, earth_impedance::Union{Nothing, Symbol, NamedTuple}, shunt_model::Union{Nothing, Symbol, NamedTuple}, insulation_admittance::Union{Nothing, Symbol, NamedTuple}, semicon_admittance::Union{Nothing, Symbol, NamedTuple}, earth_admittance::Union{Nothing, Symbol, NamedTuple}, earth_properties::Union{Nothing, Symbol, NamedTuple}, pipe_impedance::Union{Nothing, Symbol, NamedTuple}, temperature_dependence::Union{Nothing, Symbol, NamedTuple}}((:schelkunoff1934, :ametani1980, :unified, :coaxial, :lossless, :lossless, :unified, :constant, :none, :linear)), @NamedTuple{internal_impedance::NamedTuple, insulation_impedance::NamedTuple, shunt_model::NamedTuple, insulation_admittance::NamedTuple, semicon_admittance::NamedTuple, earth_properties::NamedTuple, temperature_dependence::NamedTuple, earth_impedance::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}, earth_admittance::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}(((outer = NamedTuple(),), NamedTuple(), NamedTuple(), NamedTuple(), NamedTuple(), NamedTuple(), NamedTuple(), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}[@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :self, 2, 2, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),))), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :mutual, 2, 1, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),))), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :mutual, 2, 2, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),))), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :mutual, 1, 2, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),))), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :self, 1, 1, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),)))], @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}[@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :self, 2, 2, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),))), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :mutual, 2, 1, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),))), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :mutual, 2, 2, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),))), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :mutual, 1, 2, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),))), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :self, 1, 1, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),)))])), @NamedTuple{earth_impedance::Union{Nothing, @NamedTuple{air::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, mixed::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth_admittance::Union{Nothing, @NamedTuple{air::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, mixed::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}((nothing, nothing)), :coaxial, (internal_impedance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), insulation_impedance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_impedance = (identifier = :unified, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), shunt_model = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), insulation_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), semicon_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_admittance = (identifier = :unified, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_properties = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), pipe_impedance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), temperature_dependence = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing)), (internal_impedance = (identifier = :schelkunoff1934, parameters = NamedTuple(), options = (inner = NamedTuple(), outer = NamedTuple(), transfer = NamedTuple()), configured_options = ()), insulation_impedance = (identifier = :ametani1980, parameters = NamedTuple(), options = NamedTuple()), earth_impedance = (identifier = :unified, assumptions = (media = Val{:homogeneous}(), layers = 2:2, longitudinal = :prescribed, permittivity = :positive), parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), shunt_model = (identifier = :coaxial, parameters = NamedTuple(), options = NamedTuple()), insulation_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), semicon_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), earth_admittance = (identifier = :unified, assumptions = (media = Val{:homogeneous}(), layers = 2:2, longitudinal = :prescribed, permittivity = :positive), parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_properties = (identifier = :constant, parameters = NamedTuple(), options = NamedTuple()), pipe_impedance = (identifier = :none, parameters = NamedTuple(), options = NamedTuple()), temperature_dependence = (identifier = :linear, parameters = NamedTuple(), options = NamedTuple())), (reduce_bundle = false, kron_reduction = false, ideal_transposition = false))), ["cable:1:core", "cable:2:core", "cable:3:core"])) \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/unified/formulation.txt b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/unified/formulation.txt new file mode 100644 index 000000000..1a6917548 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/unified/formulation.txt @@ -0,0 +1 @@ +@NamedTuple{backend::Symbol, requested::NamedTuple, methods::NamedTuple, options::NamedTuple}((:coaxial, (internal_impedance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), insulation_impedance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_impedance = (identifier = :unified, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), shunt_model = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), insulation_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), semicon_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_admittance = (identifier = :unified, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_properties = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), pipe_impedance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), temperature_dependence = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing)), (internal_impedance = (identifier = :schelkunoff1934, parameters = NamedTuple(), options = (inner = NamedTuple(), outer = NamedTuple(), transfer = NamedTuple()), configured_options = ()), insulation_impedance = (identifier = :ametani1980, parameters = NamedTuple(), options = NamedTuple()), earth_impedance = (identifier = :unified, assumptions = (media = Val{:homogeneous}(), layers = 2:2, longitudinal = :prescribed, permittivity = :positive), parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), shunt_model = (identifier = :coaxial, parameters = NamedTuple(), options = NamedTuple()), insulation_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), semicon_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), earth_admittance = (identifier = :unified, assumptions = (media = Val{:homogeneous}(), layers = 2:2, longitudinal = :prescribed, permittivity = :positive), parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_properties = (identifier = :constant, parameters = NamedTuple(), options = NamedTuple()), pipe_impedance = (identifier = :none, parameters = NamedTuple(), options = NamedTuple()), temperature_dependence = (identifier = :linear, parameters = NamedTuple(), options = NamedTuple())), (reduce_bundle = false, kron_reduction = false, ideal_transposition = false))) \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/unified/problem.json b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/unified/problem.json new file mode 100644 index 000000000..a4a9a71b8 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_2/unified/problem.json @@ -0,0 +1,516 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "root": { + "kind": "line_parameters_problem", + "earth_props": { + "kind": "earth_model", + "vertical_layers": false, + "layers": [ + { + "kind": "earth_layer", + "thickness": { + "special": "Inf", + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "rho": { + "special": "Inf", + "__type__": "Float64" + } + }, + { + "kind": "earth_layer", + "thickness": { + "special": "Inf", + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "rho": { + "value": 0.1, + "__type__": "Float64" + } + } + ] + }, + "Gamma": null, + "frequencies": [ + { + "value": 0.1, + "__type__": "Float64" + }, + { + "value": 1, + "__type__": "Float64" + }, + { + "value": 10, + "__type__": "Float64" + }, + { + "value": 100, + "__type__": "Float64" + }, + { + "value": 1000, + "__type__": "Float64" + }, + { + "value": 10000, + "__type__": "Float64" + }, + { + "value": 100000, + "__type__": "Float64" + }, + { + "value": 1000000, + "__type__": "Float64" + }, + { + "value": 10000000, + "__type__": "Float64" + } + ], + "system": { + "kind": "line_cable_system", + "input_positions": [ + { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 1, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 2, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + } + ], + "clearances": [ + [ + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + } + ], + [ + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + } + ], + [ + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + } + ] + ], + "connections": [ + [ + 1 + ], + [ + 2 + ], + [ + 3 + ] + ], + "line_length": { + "value": 1, + "__type__": "Float64" + }, + "environment": null, + "system_id": "three_bare_wires_air_2", + "positions": [ + { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 1, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 2, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + } + ], + "designs": [ + { + "kind": "cable_design", + "cable_id": "three_bare_wires_air_2", + "nominal_data": [ + ], + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "core_metal", + "material": { + "value": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "type": "material" + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.0425, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + } + ] + } + }, + { + "kind": "cable_design", + "cable_id": "three_bare_wires_air_2", + "nominal_data": [ + ], + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "core_metal", + "material": { + "value": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "type": "material" + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.0425, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + } + ] + } + }, + { + "kind": "cable_design", + "cable_id": "three_bare_wires_air_2", + "nominal_data": [ + ], + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "core_metal", + "material": { + "value": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "type": "material" + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.0425, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + } + ] + } + } + ] + }, + "temperature": { + "value": 20, + "__type__": "Float64" + } + }, + "format": "linecablemodels.line_parameters_problem", + "version": "1.0.0" +} \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/Y-differences.csv b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/Y-differences.csv new file mode 100644 index 000000000..c41264c8c --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/Y-differences.csv @@ -0,0 +1,82 @@ +frequency_hz,response_terminal,basis_terminal,absolute_error,relative_error,real_absolute_error,real_relative_error,imaginary_absolute_error,imaginary_relative_error,real_reference_near_zero,imaginary_reference_near_zero +0.10000000000000001,cable:1:core,cable:1:core,0.069854980237735106,0.0064165644380051552,0.069844551988886394,0.0064162105985255975,0.0012069886023108045,0.008079891207727629,false,false +0.10000000000000001,cable:1:core,cable:2:core,0.05937284059626674,0.007017589420792383,0.059360479404411137,0.0070172201658999329,0.0012114806429245062,0.0081177105597857041,false,false +0.10000000000000001,cable:1:core,cable:3:core,1.9902734619529257e-14,0.0066886765298462538,9.7464347319962591e-16,0.0085660387866133333,1.9878855989104547e-14,0.0066855410383060898,false,false +0.10000000000000001,cable:2:core,cable:1:core,0.059372840596273867,0.0070175894207932252,0.059360479404418243,0.0070172201659007725,0.0012114806429258385,0.0081177105597947091,false,false +0.10000000000000001,cable:2:core,cable:2:core,0.069918916058994848,0.0064223995925957239,0.069908355464118443,0.0064220342095634925,0.0012151786565066658,0.0081342715584060681,false,false +0.10000000000000001,cable:2:core,cable:3:core,2.7871047777869144e-14,0.006500621011536257,9.7771453394539536e-16,0.0085808172772447074,2.7853893417732043e-14,0.006498915337710613,false,false +0.10000000000000001,cable:3:core,cable:1:core,4.5634272605365255e-14,0.015336228612152988,2.2037512525574646e-14,0.19368537546845224,3.9960416388044935e-14,0.013439254442836163,false,false +0.10000000000000001,cable:3:core,cable:2:core,5.277165353921273e-14,0.012308418490205237,2.2068694928753506e-14,0.19368377185387425,4.7935583040204203e-14,0.011184407550140248,false,false +0.10000000000000001,cable:3:core,cable:3:core,5.664708869163415e-14,0.006202765402709024,3.8095905650047746e-26,NaN,5.664708869163415e-14,0.006202765402709024,true,false +1,cable:1:core,cable:1:core,0.07054846030638201,0.0063313332928883869,0.070529373965603881,0.0063308625373122603,0.0016409325461933244,0.0074344909554527917,false,false +1,cable:1:core,cable:2:core,0.058863344623693713,0.0071718104745286974,0.058840327458417718,0.0071715716418260219,0.0016459662432459776,0.0074980171474903923,false,false +1,cable:1:core,cable:3:core,1.9764889199463251e-13,0.0070966892428370671,1.305981154407551e-14,0.00798917800353139,1.9721695117088476e-13,0.0070934092053989263,false,false +1,cable:2:core,cable:1:core,0.058863344623811924,0.0071718104745430982,0.058840327458533181,0.0071715716418400931,0.0016459662433460087,0.0074980171479460712,false,false +1,cable:2:core,cable:2:core,0.0706150487263877,0.0063372713720829697,0.070595844315674228,0.0063367912448986635,0.0016467762407796904,0.0074607691603557564,false,false +1,cable:2:core,cable:3:core,2.7840994568837659e-13,0.0067970988164539527,1.3093299656502532e-14,0.0079496728745064617,2.7810189384883706e-13,0.0067950736670682813,false,false +1,cable:3:core,cable:1:core,1.2061593971133471e-12,0.043307798653754177,8.1019561724406177e-13,0.49562713688469417,8.9353430434614365e-13,0.03213823366682482,false,false +1,cable:3:core,cable:2:core,1.2694289430624162e-12,0.030991830931642522,8.1355935356352805e-13,0.49395728307592951,9.7445934738903176e-13,0.023809701399124233,false,false +1,cable:3:core,cable:3:core,5.7535811878914124e-13,0.0062994661422719062,7.5998864285756267e-24,NaN,5.7535811878914124e-13,0.0062994661422719062,true,false +10,cable:1:core,cable:1:core,0.071489223671738211,0.0061989369533425138,0.071449239664667985,0.006198484385926472,0.0023906594338489739,0.006647630773156654,false,false +10,cable:1:core,cable:2:core,0.05816819864049088,0.0074307786725537842,0.058123133781054825,0.0074324612572385989,0.0022892471577373175,0.0065377868304575875,false,false +10,cable:1:core,cable:3:core,1.9430571579725093e-12,0.007745578471616445,1.7878033102926694e-13,0.007285798661128183,1.9348149039081932e-12,0.0077498871203249822,false,false +10,cable:2:core,cable:1:core,0.058168198644849387,0.0074307786731105637,0.058123133785276337,0.0074324612577784171,0.0022892471613012999,0.0065377868406358789,false,false +10,cable:2:core,cable:2:core,0.070377870567039619,0.0061031579193572679,0.070344149746706108,0.0061031988876052859,0.0021783622205614472,0.0060608805603636984,false,false +10,cable:2:core,cable:3:core,2.6735786895396618e-12,0.0070088498652705576,1.73493148033171e-13,0.0068280338544786526,2.6679436157359391e-12,0.0070096452658255665,false,false +10,cable:3:core,cable:1:core,4.0138939960663476e-11,0.16000523091005178,3.0001078680118138e-11,1.2226278898892193,2.6665891681980336e-11,0.10681003649533935,false,false +10,cable:3:core,cable:2:core,4.0886649719046023e-11,0.10718532074477341,3.0337285141031566e-11,1.1939607549015652,2.7411079065219438e-11,0.072018741126087832,false,false +10,cable:3:core,cable:3:core,5.7216385748054646e-12,0.0062647119937248794,2.1773062330641011e-21,NaN,5.7216385748054646e-12,0.0062647119937248794,true,false +100,cable:1:core,cable:1:core,0.071590734027195041,0.0058702623123268443,0.071498393802281512,0.0058719946018713446,0.0036349528534019848,0.0052968236819751595,false,false +100,cable:1:core,cable:2:core,0.057155364473394997,0.0079245001433820902,0.05704105357402689,0.0079379497153825342,0.0036130174718117525,0.0058358190986824302,false,false +100,cable:1:core,cable:3:core,1.7791230087403235e-11,0.00849508537841368,2.3349516370448269e-12,0.0062478064206774198,1.7637343021996931e-11,0.0085589838368638899,false,false +100,cable:2:core,cable:1:core,0.057155364627710863,0.0079245001647777324,0.05704105372020063,0.0079379497357243631,0.0036130176052391327,0.0058358193141970407,false,false +100,cable:2:core,cable:2:core,0.071944533467769134,0.0058991016551025628,0.071847479412420867,0.0059004950155808278,0.0037357192047466903,0.0054428603759329704,false,false +100,cable:2:core,cable:3:core,2.6585825809172263e-11,0.0078669359869180146,2.615159571603197e-12,0.0061163762682435469,2.6456890867422464e-11,0.0078922046063721225,false,false +100,cable:3:core,cable:1:core,1.4531199725852168e-09,0.69384624736711165,1.1173422498506774e-09,2.9897570347737479,9.2903388066565577e-10,0.45083808590666086,false,false +100,cable:3:core,cable:2:core,1.4840915953397954e-09,0.43915331662306895,1.1474670232952493e-09,2.6837138911460663,9.4119460995490149e-10,0.28076240981608397,false,false +100,cable:3:core,cable:3:core,5.6897289157743825e-11,0.0062299912733886475,7.7246766318970027e-19,NaN,5.6897289157743819e-11,0.0062299912733886466,true,false +1000,cable:1:core,cable:1:core,0.072776384588157661,0.0053537052833279248,0.072369285529193306,0.005362723061089294,0.0076869152276790587,0.0046994156364464374,false,false +1000,cable:1:core,cable:2:core,0.053271951369298395,0.0086873963200910492,0.052830501560770848,0.0087914406224304312,0.0068438956399321516,0.0056052593232090251,false,false +1000,cable:1:core,cable:3:core,1.2497179223762959e-10,0.0083742270091450837,3.120313645480314e-11,0.0058391507076724644,1.2101368984733997e-10,0.0086848892710133622,false,false +1000,cable:2:core,cable:1:core,0.053271955657261144,0.0086873970193564326,0.05283050519814303,0.0087914412277198054,0.0068439009386591643,0.0056052636629506866,false,false +1000,cable:2:core,cable:2:core,0.072431911813774466,0.0053284993777964673,0.07202303459089876,0.0053372020446653123,0.0076853326093671015,0.0046984526485991671,false,false +1000,cable:2:core,cable:3:core,2.3282217934759386e-10,0.0087657615371490982,4.4658606630519418e-11,0.0057670860564811924,2.2849896290789524e-10,0.0089937210458491401,false,false +1000,cable:3:core,cable:1:core,5.3243466404105376e-08,3.5677881099276916,3.9030599383996353e-08,7.3039308834885075,3.621434835049612e-08,2.5990250015740517,false,false +1000,cable:3:core,cable:2:core,5.5504516950264258e-08,2.0897466091247408,4.131013517214787e-08,5.3346739300154615,3.7070529183450343e-08,1.4590963313577563,false,false +1000,cable:3:core,cable:3:core,5.3011298942671402e-10,0.0058069641195239588,3.2918229537705948e-16,NaN,5.3011298942661187e-10,0.0058069641195228399,true,false +10000,cable:1:core,cable:1:core,0.085880946928772295,0.0048542309469669608,0.082256452396396895,0.0047910452719599084,0.024686293454909247,0.0057804111857431614,false,false +10000,cable:1:core,cable:2:core,0.040804898256488663,0.010009162278858546,0.036934800562177461,0.011001790170505774,0.017345322976369371,0.0074994674193827335,false,false +10000,cable:1:core,cable:3:core,6.434680346049712e-10,0.0087169059183770485,4.4256366707671702e-10,0.0074972689826434263,4.6710653189823113e-10,0.010538456228984227,false,false +10000,cable:2:core,cable:1:core,0.04080490629507455,0.010009164250668641,0.036934756631022303,0.01100177708470745,0.017345435432901901,0.0074995160413716599,false,false +10000,cable:2:core,cable:2:core,0.086082138996785421,0.0048655494879547793,0.082409151697980576,0.0047998965953856138,0.024877024956395211,0.0058248119144639126,false,false +10000,cable:2:core,cable:3:core,1.4351438560701812e-09,0.0099216978479292912,8.6367296045520011e-10,0.0079185413082893934,1.1461705392281461e-09,0.012063913806131525,false,false +10000,cable:3:core,cable:1:core,1.6820814690113849e-06,22.786751048821138,6.8473781823465634e-07,11.599830686064246,1.5364023524685001e-06,34.662989780509641,false,false +10000,cable:3:core,cable:2:core,1.8009715248965488e-06,12.450804305902423,7.6293845820456004e-07,6.9949621831292736,1.6313869383076013e-06,17.17101490101571,false,false +10000,cable:3:core,cable:3:core,5.285397531143633e-09,0.005789831761446265,1.57910138580664e-13,NaN,5.2853975287847173e-09,0.0057898317588622175,true,false +100000,cable:1:core,cable:1:core,0.11337506880641381,0.0042804534634525281,0.09723403552558807,0.0038567660205989004,0.058304790217166058,0.0071803027016413912,false,false +100000,cable:1:core,cable:2:core,0.0096204784379613478,0.0089602552946762264,0.00011463186313717677,0.00066722759373648288,0.0096197954713825862,0.0090765715561259425,false,false +100000,cable:1:core,cable:3:core,8.5270761403345421e-10,0.0092720032399396871,1.3617243542018226e-10,0.0025837316867236212,8.4176442242611002e-10,0.011169026788037941,false,false +100000,cable:2:core,cable:1:core,0.0096202779490492437,0.0089600685646868822,0.00011456371899762341,0.00066683095314269594,0.0096195957800342402,0.0090763831412248377,false,false +100000,cable:2:core,cable:2:core,0.11544758345030423,0.0043583664354513068,0.099042196040574737,0.0039282045552400053,0.059319372282377714,0.0073043367716806046,false,false +100000,cable:2:core,cable:3:core,2.8652519083969159e-09,0.013493978028802577,2.0812939460693973e-09,0.014879554488164004,1.9692343711775512e-09,0.012326728714965056,false,false +100000,cable:3:core,cable:1:core,1.7513866578886239e-05,190.43881512348455,1.5723343628960205e-05,298.33424826422248,7.7144013162827442e-06,102.35922623921593,false,false +100000,cable:3:core,cable:2:core,1.9334898911874188e-05,91.058206903672612,1.7097646958293164e-05,122.23423318737314,9.0282215536595117e-06,56.513556486429557,false,false +100000,cable:3:core,cable:3:core,5.3908822099139899e-08,0.0059047588181680259,5.5135282406143012e-11,161.7906162549485,5.3908793904309792e-08,0.0059047557299226339,false,false +1000000,cable:1:core,cable:1:core,0.12560262627986341,0.0027679164612677935,0.06626459814604857,0.0016201597700696993,0.10670062212068743,0.0054284343685691364,false,false +1000000,cable:1:core,cable:2:core,0.00019479521501035552,0.0069403052986080618,0.00015954935190635663,0.0077589062469861992,0.00011175500032299321,0.0058501869174172457,false,false +1000000,cable:1:core,cable:3:core,2.1287744983319436e-11,0.0035323002604624766,1.6808021517686314e-11,0.010724861999154104,1.3063632692931766e-11,0.0022448976592383462,false,false +1000000,cable:2:core,cable:1:core,0.00019479521295861048,0.0069403052255069993,0.00015954977018086888,0.0077589265877314722,0.00011175439958597752,0.0058501554698415685,false,false +1000000,cable:2:core,cable:2:core,0.12469522463931708,0.002747982005379142,0.065376950821857349,0.0015984916141139741,0.10618264146783218,0.0054022242697266279,false,false +1000000,cable:2:core,cable:3:core,1.9485346815318598e-10,0.012807745855603602,8.3258403149062826e-11,0.022010940170671303,1.7617012333648629e-10,0.011955095381342705,false,false +1000000,cable:3:core,cable:1:core,8.2175531368473888e-06,1363.5481404157649,6.4334599661862695e-06,4105.0619932766494,5.1126091597528681e-06,878.56759334175513,false,false +1000000,cable:3:core,cable:2:core,9.5137938166960399e-06,625.3430050886343,7.794581917573129e-06,2060.6457697240812,5.4549762159743463e-06,370.18059435860476,false,false +1000000,cable:3:core,cable:3:core,5.4343690190922188e-07,0.0059551843418274271,2.4729960611093198e-08,2329.7420249195879,5.4287392220005095e-07,0.0059490150001117268,false,false +10000000,cable:1:core,cable:1:core,0.11016346819205822,0.0011254513410626985,0.023008082585917577,0.00028467062131493721,0.10773401440502539,0.0019511401065892707,false,false +10000000,cable:1:core,cable:2:core,3.0145043422117251e-07,0.67544194975282035,2.8089274382018313e-07,0.751940935522728,1.0941494761367126e-07,0.44803141309295896,false,false +10000000,cable:1:core,cable:3:core,2.0448407290378886e-12,0.96636016368605737,1.8325572834442899e-12,0.97232549274050084,9.0725267154607416e-13,0.94311191643263292,false,false +10000000,cable:2:core,cable:1:core,3.0145043422113206e-07,0.67544194975272975,2.8089274382014878e-07,0.75194093552263652,1.0941494761364802e-07,0.44803141309286332,false,false +10000000,cable:2:core,cable:2:core,0.1141603187090951,0.0011662758803171742,0.024727609828119057,0.00030595221441796333,0.11145009501992575,0.0020183051524286129,false,false +10000000,cable:2:core,cable:3:core,2.1694495953845462e-12,0.93228946241669342,1.9849471987184003e-12,0.94747531782474326,8.7549766716659367e-13,0.86432328586720109,false,false +10000000,cable:3:core,cable:1:core,4.3942201146780478e-10,207.66405955200685,3.8660141280654003e-11,20.512450693753539,4.3771805724526766e-10,455.01890352361789,false,false +10000000,cable:3:core,cable:2:core,5.5124795633475065e-10,236.89080491336637,9.1345481396696934e-11,43.601960331271854,5.4362699748232235e-10,536.68843489977348,false,false +10000000,cable:3:core,cable:3:core,0.00097617470674829054,11.061193117969086,1.6777938920729379e-05,1010.1006532917843,0.00097603051123450202,11.059559411221462,false,false diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/Z-differences.csv b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/Z-differences.csv new file mode 100644 index 000000000..9fb2f7b82 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/Z-differences.csv @@ -0,0 +1,82 @@ +frequency_hz,response_terminal,basis_terminal,absolute_error,relative_error,real_absolute_error,real_relative_error,imaginary_absolute_error,imaginary_relative_error,real_reference_near_zero,imaginary_reference_near_zero +0.10000000000000001,cable:1:core,cable:1:core,2.0353936935062379e-08,0.0060025453549572361,1.9476894405826001e-08,0.006169551526682469,5.9104427127585249e-09,0.0047754451059958845,false,false +0.10000000000000001,cable:1:core,cable:2:core,3.0923847979138876e-09,0.0037794973675009044,1.3408223132989862e-10,0.0013558780789687695,3.0894766051243228e-09,0.0038038278082498404,false,false +0.10000000000000001,cable:1:core,cable:3:core,2.5704177549214906e-09,0.0037280373348655758,1.3399067195513791e-10,0.0013595392414868818,2.5669230480566899e-09,0.0037615962349923393,false,false +0.10000000000000001,cable:2:core,cable:1:core,3.0923847979137788e-09,0.0037794973675007713,1.3408223132983245e-10,0.0013558780789680995,3.0894766051242169e-09,0.0038038278082497103,false,false +0.10000000000000001,cable:2:core,cable:2:core,2.0327202684685417e-08,0.0059946003389987892,1.9477163381564036e-08,0.0061696362024720678,5.8168097435057221e-09,0.0046994371944483289,false,false +0.10000000000000001,cable:2:core,cable:3:core,2.6655976861156242e-09,0.0037092827583557471,1.3294174943655672e-10,0.0013488523995674382,2.6622805102922041e-09,0.0037400081967362245,false,false +0.10000000000000001,cable:3:core,cable:1:core,2.5704177549202378e-09,0.0037280373348637604,1.3399067195544231e-10,0.0013595392414899711,2.5669230480554194e-09,0.0037615962349904788,false,false +0.10000000000000001,cable:3:core,cable:2:core,2.6655976861157359e-09,0.0037092827583559036,1.3294174943667583e-10,0.0013488523995686483,2.6622805102923099e-09,0.0037400081967363745,false,false +0.10000000000000001,cable:3:core,cable:3:core,2.0397936913737283e-08,0.0060162751800927651,1.9476914326239281e-08,0.0061708449782478794,6.0601682043604264e-09,0.0048943806323203916,false,false +1,cable:1:core,cable:1:core,5.3743970945282693e-08,0.0046084600110046781,1.8232313467230042e-08,0.0044857520317346166,5.0556870538039165e-08,0.0046251698961077463,false,false +1,cable:1:core,cable:2:core,2.2372843939747775e-08,0.003314247151264345,1.3779810631337709e-09,0.0013839917733557685,2.2330367532621434e-08,0.0033445340006684932,false,false +1,cable:1:core,cable:3:core,1.6963305264206145e-08,0.0030980717574050601,1.4325426630686043e-09,0.0014543083828464699,1.6902708274271679e-08,0.0031382050312298501,false,false +1,cable:2:core,cable:1:core,2.2372843939749452e-08,0.0033142471512645935,1.3779810631335591e-09,0.0013839917733555554,2.2330367532623128e-08,0.0033445340006687469,false,false +1,cable:2:core,cable:2:core,5.2928797265652761e-08,0.0045382387913634045,1.8250784723001575e-08,0.0044902761652362427,4.9682657305977744e-08,0.0045448294562332372,false,false +1,cable:2:core,cable:3:core,1.7889585735953143e-08,0.0031029902632174191,1.3326825955991187e-09,0.0013525560808909224,1.7839877659429321e-08,0.0031405732434894775,false,false +1,cable:3:core,cable:1:core,1.6963305264208696e-08,0.0030980717574055267,1.432542663068816e-09,0.0014543083828466852,1.690270827427422e-08,0.0031382050312303233,false,false +1,cable:3:core,cable:2:core,1.7889585735953957e-08,0.0031029902632175605,1.3326825955986952e-09,0.001352556080890492,1.7839877659430168e-08,0.0031405732434896271,false,false +1,cable:3:core,cable:3:core,5.5570281720479191e-08,0.0047607104208511695,1.8102102977027185e-08,0.0044763696932233147,5.2539224188243659e-08,0.0047981869332099289,false,false +10,cable:1:core,cable:1:core,4.1486501850077148e-07,0.0043518833853649136,1.756924904203845e-08,0.0012480615393490938,4.1449282872414608e-07,0.0043961747572694573,false,false +10,cable:1:core,cable:2:core,1.4028235383557433e-07,0.0026399361884357079,1.3349382177292604e-08,0.0013150309612540954,1.3964574033293627e-07,0.0026772630819522003,false,false +10,cable:1:core,cable:3:core,8.685297415419253e-08,0.0021347094927609009,1.544487400599343e-08,0.0015733841308895721,8.5468678393712302e-08,0.002164633323634476,false,false +10,cable:2:core,cable:1:core,1.4028235383561465e-07,0.0026399361884364685,1.334938217729091e-08,0.0013150309612539283,1.3964574033297693e-07,0.0026772630819529818,false,false +10,cable:2:core,cable:2:core,3.9522425803181795e-07,0.0041450015868589162,1.8427541634940517e-08,0.0013089520088377367,3.9479442732401066e-07,0.004186375843678986,false,false +10,cable:2:core,cable:3:core,1.0028086830537464e-07,0.0023029267712253013,1.1796347978866025e-08,0.001199665647199489,9.9584630954989211e-08,0.0023475742476084115,false,false +10,cable:3:core,cable:1:core,8.6852974154179493e-08,0.0021347094927605809,1.5444874005995124e-08,0.0015733841308897447,8.5468678393698749e-08,0.002164633323634133,false,false +10,cable:3:core,cable:2:core,1.0028086830536851e-07,0.0023029267712251613,1.1796347978871108e-08,0.0011996656472000062,9.9584630954982435e-08,0.0023475742476082519,false,false +10,cable:3:core,cable:3:core,4.2063457786632498e-07,0.0043868295663449198,1.2631708255857237e-08,0.00093870633668803746,4.2044486920798582e-07,0.0044286794011454593,false,false +100,cable:1:core,cable:1:core,4.2752438207191567e-06,0.0054609830498349833,7.5146550436151847e-07,0.0063546025829260399,4.208682611263532e-06,0.005438363038875244,false,false +100,cable:1:core,cable:2:core,6.9464487974450093e-07,0.0018005195975041419,1.3544698654155516e-07,0.0012768356456096821,6.813116928338042e-07,0.0018367564781451727,false,false +100,cable:1:core,cable:3:core,2.1518347395472479e-07,0.00079932067807002316,1.189293172222574e-07,0.0012395805771782667,1.7933138311035076e-07,0.00071295950748635523,false,false +100,cable:2:core,cable:1:core,6.9464487974461252e-07,0.0018005195975044318,1.3544698654158226e-07,0.0012768356456099379,6.8131169283391262e-07,0.0018367564781454656,false,false +100,cable:2:core,cable:2:core,4.2285450459029626e-06,0.0054009887719494087,7.6024383381003541e-07,0.0064283574032371737,4.1596421142189996e-06,0.005374653448127137,false,false +100,cable:2:core,cable:3:core,3.6358297357299128e-07,0.001224824917127208,1.0897983932723547e-07,0.0011254727404107912,3.4686592985242641e-07,0.001236123409543817,false,false +100,cable:3:core,cable:1:core,2.1518347395470232e-07,0.00079932067806993989,1.1892931722221674e-07,0.0012395805771778425,1.7933138311035076e-07,0.00071295950748635534,false,false +100,cable:3:core,cable:2:core,3.6358297357301452e-07,0.0012248249171272864,1.089798393271406e-07,0.0011254727404098107,3.4686592985248062e-07,0.0012361234095440105,false,false +100,cable:3:core,cable:3:core,4.3763410797056339e-06,0.0054673377844122635,7.6517685839568256e-07,0.007571822833847614,4.3089285931997975e-06,0.0054265393876584958,false,false +1000,cable:1:core,cable:1:core,5.0525754997700245e-05,0.0082196615691729393,3.1642586673650327e-06,0.0026716915679968374,5.0426574196286988e-05,0.008360175326985006,false,false +1000,cable:1:core,cable:2:core,1.5740140239114191e-06,0.00066503889558218519,1.2506564812313041e-06,0.0011361300134405013,9.557083830457648e-07,0.0004561361523441649,false,false +1000,cable:1:core,cable:3:core,1.4121857474324659e-06,0.0010035129201567358,1.047758281782097e-06,0.0012572649982736995,9.4682161372056789e-07,0.00083497846888872331,false,false +1000,cable:2:core,cable:1:core,1.5740140239113378e-06,0.00066503889558215093,1.2506564812308704e-06,0.0011361300134401071,9.5570838304619848e-07,0.00045613615234437198,false,false +1000,cable:2:core,cable:2:core,5.0106966300493903e-05,0.0081509806890415742,3.1340439520936066e-06,0.0026461127470497835,5.0008857618877647e-05,0.0082903483498277929,false,false +1000,cable:2:core,cable:3:core,1.3156290285851886e-06,0.00079485594137271743,1.3148545453915694e-06,0.0014954454542434495,4.5136075584126939e-08,3.2186266462331847e-05,false,false +1000,cable:3:core,cable:1:core,1.4121857474324504e-06,0.0010035129201567245,1.0477582817818801e-06,0.0012572649982734393,9.4682161372078473e-07,0.00083497846888891446,false,false +1000,cable:3:core,cable:2:core,1.3156290285846615e-06,0.00079485594137239856,1.3148545453910273e-06,0.0014954454542428321,4.513607558456062e-08,3.2186266462641096e-05,false,false +1000,cable:3:core,cable:3:core,5.2422523085336487e-05,0.007868183652700653,3.1373711805317934e-06,0.0039947103299425657,5.2328556531861317e-05,0.0079092236195941433,false,false +10000,cable:1:core,cable:1:core,0.00033115837371719322,0.0075000148609756052,9.8531746989403285e-05,0.0088616782089309901,0.0003161603443163491,0.0073987702675536778,false,false +10000,cable:1:core,cable:2:core,8.6843954097837918e-06,0.00092467091254244671,5.4880144089546057e-06,0.00068356294268399648,6.7305587792233432e-06,0.0013810858483452794,false,false +10000,cable:1:core,cable:3:core,6.7117560997829913e-06,0.0015945810636581533,6.2467947228445064e-06,0.0015425442003697296,2.4546335029116285e-06,0.0021391410800103441,false,false +10000,cable:2:core,cable:1:core,8.6843954097861601e-06,0.00092467091254269966,5.4880144089615446e-06,0.00068356294268486135,6.7305587792207411e-06,0.0013810858483447467,false,false +10000,cable:2:core,cable:2:core,0.00033082163482768073,0.0074923388149703033,9.752409604567476e-05,0.0087702580067030491,0.00031612023782183474,0.0073978247537761603,false,false +10000,cable:2:core,cable:3:core,1.1712500294852345e-05,0.0020153767441258739,1.1650399696996189e-05,0.0022165709274053098,1.2045123731815183e-06,0.00048577317041732841,false,false +10000,cable:3:core,cable:1:core,6.7117560997843677e-06,0.0015945810636584811,6.2467947228462412e-06,0.0015425442003701583,2.454633502910978e-06,0.002139141080009779,false,false +10000,cable:3:core,cable:2:core,1.1712500295312677e-05,0.0020153767442050827,1.1650399697440278e-05,0.0022165709274897995,1.2045123733623632e-06,0.00048577317049026215,false,false +10000,cable:3:core,cable:3:core,0.00040049525319223449,0.007058670637102386,8.7428635939385858e-05,0.017111109821020602,0.00039083587533297187,0.0069165279091756415,false,false +100000,cable:1:core,cable:1:core,0.0016174516288031745,0.0054263319394923688,0.00037741575029746899,0.0041250884527130006,0.0015728023152785098,0.0055441735734952048,false,false +100000,cable:1:core,cable:2:core,1.2451391359256568e-05,0.0010089228566527275,4.6171988018865739e-06,0.00052871714825627558,1.1563676837633707e-05,0.0013260555519080456,false,false +100000,cable:1:core,cable:3:core,7.5632311481977735e-06,0.0023455286322702326,7.0832808931331481e-06,0.0081229719668510919,2.6513387542982658e-06,0.00085406234365906089,false,false +100000,cable:2:core,cable:1:core,1.2451391359263974e-05,0.001008922856653327,4.6171988018935128e-06,0.00052871714825706987,1.1563676837638912e-05,0.0013260555519086414,false,false +100000,cable:2:core,cable:2:core,0.0016417112990095785,0.0055081636054669222,0.00038086376589062387,0.0041629315480133395,0.0015969216577926826,0.0056296735948842434,false,false +100000,cable:2:core,cable:3:core,5.7289502696843057e-06,0.00091519117450634035,2.9499987696216153e-06,0.0010092507288659907,4.911046573974516e-06,0.00088718904432756856,false,false +100000,cable:3:core,cable:1:core,7.5632311481850722e-06,0.0023455286322662926,7.0832808931147167e-06,0.0081229719668299404,2.6513387543112762e-06,0.00085406234366325144,false,false +100000,cable:3:core,cable:2:core,5.7289502696847521e-06,0.00091519117450641159,2.9499987696224826e-06,0.0010092507288662872,4.911046573974516e-06,0.00088718904432756834,false,false +100000,cable:3:core,cable:3:core,0.0031650778262676369,0.0061762080540927926,0.00027833731324062097,0.011245448443370703,0.0031528155649179768,0.0061594683417077918,false,false +1000000,cable:1:core,cable:1:core,0.0057808281311672342,0.0033223274842110141,0.00036387032699036759,0.0004827719778871854,0.0057693649795476087,0.0036787883561022593,false,false +1000000,cable:1:core,cable:2:core,4.8730339028257937e-06,0.0045269246088306082,4.5277918127758479e-06,0.028639007510392684,1.8015439817694115e-06,0.0016919361811240481,false,false +1000000,cable:1:core,cable:3:core,6.5660750744571341e-07,0.0054075870204730444,6.0745790722704506e-07,0.0074202433826771885,2.4925551103518529e-07,0.002779515095821518,false,false +1000000,cable:2:core,cable:1:core,4.8730339028266611e-06,0.0045269246088314079,4.5277918127760918e-06,0.028639007510394183,1.8015439817711462e-06,0.0016919361811256748,false,false +1000000,cable:2:core,cable:2:core,0.0057432494167894326,0.0033006559843933086,0.0003497797903695421,0.00046406841081395869,0.005732588242818526,0.0036552522538768235,false,false +1000000,cable:2:core,cable:3:core,1.645204679612525e-06,0.0056943975606065797,1.5699363759206879e-06,0.0087124019307072715,4.919331391357645e-07,0.0021782691410685644,false,false +1000000,cable:3:core,cable:1:core,6.5660750744562436e-07,0.0054075870204723124,6.074579072270044e-07,0.0074202433826766907,2.4925551103504977e-07,0.0027795150958200075,false,false +1000000,cable:3:core,cable:2:core,1.6452046796121777e-06,0.0056943975606053793,1.5699363759208505e-06,0.0087124019307081736,4.9193313913408398e-07,0.0021782691410611242,false,false +1000000,cable:3:core,cable:3:core,0.026624738873008071,0.0054144865043148985,0.0015198873810448321,0.016311966115239707,0.026581321682805914,0.0054066277646857156,false,false +10000000,cable:1:core,cable:1:core,0.010848679018519814,0.0013449195900571108,0.0019698575986675237,0.00043294481702211863,0.010668340896683226,0.0016016758244651185,false,false +10000000,cable:1:core,cable:2:core,2.4848252140232918e-08,0.67562077331270665,1.8310032772560315e-08,0.8572406887309556,1.6798164610824511e-08,0.56105188640063608,false,false +10000000,cable:1:core,cable:3:core,1.0513975968354568e-09,0.6298538404347922,9.5909892984494925e-10,0.58421320540576727,4.3077389591483677e-10,1.4255510190790928,false,false +10000000,cable:2:core,cable:1:core,2.4848252140232915e-08,0.67562077331270665,1.8310032772560322e-08,0.85724068873095571,1.6798164610824498e-08,0.56105188640063586,false,false +10000000,cable:2:core,cable:2:core,0.011124399425467658,0.0013791104639083534,0.0022126516609075608,0.00048628131950074658,0.010902129847182529,0.0016368328402052959,false,false +10000000,cable:2:core,cable:3:core,3.6038560694105644e-09,0.72244015793167637,3.3898025800328791e-09,0.68792390216008004,1.2235264759823538e-09,1.5749198217794871,false,false +10000000,cable:3:core,cable:1:core,1.0513975968354547e-09,0.62985384043479131,9.5909892984494656e-10,0.58421320540576605,4.3077389591483811e-10,1.4255510190790941,false,false +10000000,cable:3:core,cable:2:core,3.6038560694105582e-09,0.7224401579316756,3.3898025800328725e-09,0.68792390216007915,1.2235264759823547e-09,1.5749198217794895,false,false +10000000,cable:3:core,cable:3:core,2.1308215242095572,0.044498224765926554,0.96509785533290171,3.3703058123012957,1.899733270137304,0.039673087869510099,false,false diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/fem/Y.csv b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/fem/Y.csv new file mode 100644 index 000000000..a3f43fba3 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/fem/Y.csv @@ -0,0 +1,82 @@ +frequency_hz,response_terminal,basis_terminal,real_S_per_m,imaginary_S_per_m +0.10000000000000001,cable:1:core,cable:1:core,10.885638947845042,0.14938178884839903 +0.10000000000000001,cable:1:core,cable:2:core,-8.4592585099256858,0.14923920161998083 +0.10000000000000001,cable:1:core,cable:3:core,-1.1377995097602876e-13,-2.9734102109619595e-12 +0.10000000000000001,cable:2:core,cable:1:core,-8.4592585099256858,0.14923920161997939 +0.10000000000000001,cable:2:core,cable:2:core,10.885702751320308,0.14938997890362823 +0.10000000000000001,cable:2:core,cable:3:core,-1.1394188949089691e-13,-4.2859295698325242e-12 +0.10000000000000001,cable:3:core,cable:1:core,-1.1377995097603096e-13,-2.9734102109619599e-12 +0.10000000000000001,cable:3:core,cable:2:core,-1.1394188949089317e-13,-4.2859295698325218e-12 +0.10000000000000001,cable:3:core,cable:3:core,3.3659780042985981e-24,9.1325537907485334e-12 +1,cable:1:core,cable:1:core,11.140563161800511,0.22071888391898442 +1,cable:1:core,cable:2:core,-8.2046628545477134,0.21952020259074589 +1,cable:1:core,cable:3:core,-1.6346877661635265e-12,-2.7802844226268414e-11 +1,cable:2:core,cable:1:core,-8.2046628545477152,0.21952020259074595 +1,cable:2:core,cable:2:core,11.140629632151182,0.22072472762327983 +1,cable:2:core,cable:3:core,-1.6470237031376464e-12,-4.0926987325631668e-11 +1,cable:3:core,cable:1:core,-1.6346877661635198e-12,-2.7802844226268355e-11 +1,cable:3:core,cable:2:core,-1.6470237031376464e-12,-4.0926987325631655e-11 +1,cable:3:core,cable:3:core,3.2183905237197333e-22,9.1334425139340136e-11 +10,cable:1:core,cable:1:core,11.526888706357312,0.35962578479877871 +10,cable:1:core,cable:2:core,-7.8201731256180755,0.35015628638615159 +10,cable:1:core,cable:3:core,-2.4538192632622566e-11,-2.496571722746665e-10 +10,cable:2:core,cable:1:core,-7.8201731256180809,0.35015628638615004 +10,cable:2:core,cable:2:core,11.525783616451514,0.35941348767162129 +10,cable:2:core,cable:3:core,-2.5408946664693694e-11,-3.8061036108960929e-10 +10,cable:3:core,cable:1:core,-2.4538192632622259e-11,-2.4965717227466635e-10 +10,cable:3:core,cable:2:core,-2.5408946664693927e-11,-3.8061036108961007e-10 +10,cable:3:core,cable:3:core,3.0071294664954368e-20,9.133123087759835e-10 +100,cable:1:core,cable:1:core,12.176168176226815,0.68625143513301334 +100,cable:1:core,cable:2:core,-7.1858673359305918,0.61911060139397978 +100,cable:1:core,cable:3:core,-3.7372342864484257e-10,-2.0606818938052181e-09 +100,cable:2:core,cable:1:core,-7.185867335930598,0.61911060139397978 +100,cable:2:core,cable:2:core,12.176517262145065,0.68635220210041559 +100,cable:2:core,cable:3:core,-4.2756682337893478e-10,-3.3522814203348597e-09 +100,cable:3:core,cable:1:core,-3.7372342864484076e-10,-2.0606818938052144e-09 +100,cable:3:core,cable:2:core,-4.275668233789368e-10,-3.3522814203348651e-09 +100,cable:3:core,cable:3:core,2.6851848844473589e-18,9.1328039897551847e-09 +1000,cable:1:core,cable:1:core,13.494876521647832,1.6357172513243972 +1000,cable:1:core,cable:2:core,-6.0093110821882147,1.2209775222342441 +1000,cable:1:core,cable:3:core,-5.3437799462519742e-09,-1.3933820693746159e-08 +1000,cable:2:core,cable:1:core,-6.0093110821882192,1.2209775222342427 +1000,cable:2:core,cable:2:core,13.494530277879935,1.6357156672970763 +1000,cable:2:core,cable:3:core,-7.7437038728303673e-09,-2.5406498794328743e-08 +1000,cable:3:core,cable:1:core,-5.3437799462519742e-09,-1.3933820693746142e-08 +1000,cable:3:core,cable:2:core,-7.7437038728303574e-09,-2.5406498794328753e-08 +1000,cable:3:core,cable:3:core,2.1469037444216061e-16,9.1289179425853152e-08 +10000,cable:1:core,cable:1:core,17.16879046787793,4.2706812130935701 +10000,cable:1:core,cable:2:core,-3.3571627880337496,2.3128739690953988 +10000,cable:1:core,cable:3:core,-5.9029983864961397e-08,-4.4323999810668119e-08 +10000,cable:2:core,cable:1:core,-3.3571627880337513,2.312873969095401 +10000,cable:2:core,cable:2:core,17.168943134567673,4.2708718018210501 +10000,cable:2:core,cable:3:core,-1.0906970448598638e-07,-9.5008183716100581e-08 +10000,cable:3:core,cable:1:core,-5.9029983864961377e-08,-4.4323999810668119e-08 +10000,cable:3:core,cable:2:core,-1.0906970448598638e-07,-9.5008183716100581e-08 +10000,cable:3:core,cable:3:core,1.1771746799955883e-14,9.1287583973309992e-07 +100000,cable:1:core,cable:1:core,25.211287126640112,8.120101984535804 +100000,cable:1:core,cable:2:core,0.17180324107286526,1.0598490202933522 +100000,cable:1:core,cable:3:core,-5.2703783492650436e-08,7.536595966692837e-08 +100000,cable:2:core,cable:1:core,0.17180324107286571,1.0598490202933519 +100000,cable:2:core,cable:2:core,25.213095358910977,8.1211168291641247 +100000,cable:2:core,cable:3:core,-1.3987609291158349e-07,1.59753201089502e-07 +100000,cable:3:core,cable:1:core,-5.270378349265043e-08,7.536595966692837e-08 +100000,cable:3:core,cable:2:core,-1.3987609291158347e-07,1.59753201089502e-07 +100000,cable:3:core,cable:3:core,3.4078170713721261e-13,9.1297246440059132e-06 +1000000,cable:1:core,cable:1:core,40.900039224648729,19.655874028520731 +1000000,cable:1:core,cable:2:core,-0.020563381851447241,-0.019102808491515869 +1000000,cable:1:core,cable:3:core,-1.5672016590061488e-09,-5.8192553407374583e-09 +1000000,cable:2:core,cable:1:core,-0.020563381851447248,-0.019102808491515869 +1000000,cable:2:core,cable:2:core,40.89915157803005,19.655356047113056 +1000000,cable:2:core,cable:3:core,-3.7825918612963801e-09,-1.4735986432314035e-08 +1000000,cable:3:core,cable:1:core,-1.5672016590061527e-09,-5.8192553407374633e-09 +1000000,cable:3:core,cable:2:core,-3.7825918612963826e-09,-1.4735986432314038e-08 +1000000,cable:3:core,cable:3:core,1.0614892269862695e-11,9.1254421478153164e-05 +10000000,cable:1:core,cable:1:core,80.8235232692426,55.21592941541855 +10000000,cable:1:core,cable:2:core,-3.735569252190193e-07,-2.4421266995171498e-07 +10000000,cable:1:core,cable:3:core,-1.8847158663702465e-12,-9.6197774170620373e-13 +10000000,cable:2:core,cable:1:core,-3.7355692521901909e-07,-2.4421266995171524e-07 +10000000,cable:2:core,cable:2:core,80.821803742000398,55.219645496033451 +10000000,cable:2:core,cable:3:core,-2.0949856543762342e-12,-1.0129284741972224e-12 +10000000,cable:3:core,cable:1:core,-1.8847158663702142e-12,-9.6197774170617021e-13 +10000000,cable:3:core,cable:2:core,-2.0949856543762516e-12,-1.0129284741972214e-12 +10000000,cable:3:core,cable:3:core,1.6610165398915739e-08,-8.8252205620793824e-05 diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/fem/Z.csv b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/fem/Z.csv new file mode 100644 index 000000000..d163ee5a0 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/fem/Z.csv @@ -0,0 +1,82 @@ +frequency_hz,response_terminal,basis_terminal,real_ohm_per_m,imaginary_ohm_per_m +0.10000000000000001,cable:1:core,cable:1:core,3.1569384454592992e-06,1.2376736789074544e-06 +0.10000000000000001,cable:1:core,cable:2:core,9.8889592958001499e-08,8.1220201356743481e-07 +0.10000000000000001,cable:1:core,cable:3:core,9.8555942974177675e-08,6.8240259924173328e-07 +0.10000000000000001,cable:2:core,cable:1:core,9.8889592958001552e-08,8.1220201356743481e-07 +0.10000000000000001,cable:2:core,cable:2:core,3.1569387144350372e-06,1.2377673118767072e-06 +0.10000000000000001,cable:2:core,cable:3:core,9.8559152564943101e-08,7.1183814854081974e-07 +0.10000000000000001,cable:3:core,cable:1:core,9.8555942974177636e-08,6.8240259924173296e-07 +0.10000000000000001,cable:3:core,cable:2:core,9.8559152564942969e-08,7.1183814854081953e-07 +0.10000000000000001,cable:3:core,cable:3:core,3.1562799575900971e-06,1.2381889884782712e-06 +1,cable:1:core,cable:1:core,4.06449427838295e-06,1.0930813715747969e-05 +1,cable:1:core,cable:2:core,9.9565697546928076e-07,6.6766752941241212e-06 +1,cable:1:core,cable:3:core,9.8503362833179562e-07,5.3861070599480792e-06 +1,cable:2:core,cable:1:core,9.9565697546928118e-07,6.6766752941241212e-06 +1,cable:2:core,cable:2:core,4.0645127496387215e-06,1.0931687928980027e-05 +1,cable:2:core,cable:3:core,9.8530671994117011e-07,5.6804526678089855e-06 +1,cable:3:core,cable:1:core,9.8503362833179541e-07,5.3861070599480775e-06 +1,cable:3:core,cable:2:core,9.8530671994117053e-07,5.6804526678089847e-06 +1,cable:3:core,cable:3:core,4.0439249252427902e-06,1.0949807691859883e-05 +10,cable:1:core,cable:1:core,1.4077229758399098e-05,9.4284884384713351e-05 +10,cable:1:core,cable:2:core,1.015138241654919e-05,5.2159887190133634e-05 +10,cable:1:core,cable:3:core,9.8163402711206239e-06,3.9484136856124967e-05 +10,cable:2:core,cable:1:core,1.0151382416549192e-05,5.2159887190133594e-05 +10,cable:2:core,cable:2:core,1.4078088050992001e-05,9.43045827861135e-05 +10,cable:2:core,cable:3:core,9.8330297332457038e-06,4.2420226349152085e-05 +10,cable:3:core,cable:1:core,9.8163402711206239e-06,3.9484136856124967e-05 +10,cable:3:core,cable:2:core,9.8330297332457004e-06,4.2420226349152079e-05 +10,cable:3:core,cable:3:core,1.345650685647301e-05,9.4936849368513674e-05 +100,cable:1:core,cable:1:core,0.00011825531094274957,0.00077388776386910112 +100,cable:1:core,cable:2:core,0.00010608020461151831,0.00037093196672528898 +100,cable:1:core,cable:3:core,9.5943191924629455e-05,0.00025153095123537972 +100,cable:2:core,cable:1:core,0.00010608020461151828,0.00037093196672528887 +100,cable:2:core,cable:2:core,0.0001182640892721979,0.00077393680436614522 +100,cable:2:core,cable:3:core,9.6830278881262328e-05,0.00028060784803067107 +100,cable:3:core,cable:1:core,9.5943191924629482e-05,0.00025153095123537966 +100,cable:3:core,cable:2:core,9.6830278881262396e-05,0.00028060784803067101 +100,cable:3:core,cable:3:core,0.00010105583228587755,0.00079404723441232818 +1000,cable:1:core,cable:1:core,0.001184365255805897,0.0060317603667377527 +1000,cable:1:core,cable:2:core,0.0011008040157692749,0.0020952261252132928 +1000,cable:1:core,cable:3:core,0.00083336312012243408,0.001133947339960391 +1000,cable:2:core,cable:1:core,0.0011008040157692751,0.0020952261252132924 +1000,cable:2:core,cable:2:core,0.0011843954705209859,0.0060321780833149713 +1000,cable:2:core,cable:3:core,0.0008792393876089305,0.0014023395859519923 +1000,cable:3:core,cable:1:core,0.00083336312012243419,0.0011339473399603912 +1000,cable:3:core,cable:2:core,0.00087923938760893104,0.0014023395859519927 +1000,cable:3:core,cable:3:core,0.00078538139724812072,0.0066161432586409183 +10000,cable:1:core,cable:1:core,0.011118858602888656,0.042731471972150319 +10000,cable:1:core,cable:2:core,0.0080285428981946051,0.0048733819025713922 +10000,cable:1:core,cable:3:core,0.0040496698385350796,0.0011474855613075126 +10000,cable:2:core,cable:1:core,0.0080285428981945982,0.0048733819025713878 +10000,cable:2:core,cable:2:core,0.011119866253779278,0.042731512078665247 +10000,cable:2:core,cable:3:core,0.0052560464242098499,0.0024795778081912593 +10000,cable:3:core,cable:1:core,0.0040496698385350787,0.0011474855613075117 +10000,cable:3:core,cable:2:core,0.0052560464242098525,0.0024795778081912593 +10000,cable:3:core,cable:3:core,0.0051094661219450417,0.056507525230177859 +100000,cable:1:core,cable:1:core,0.09149276545797437,0.28368561958405109 +100000,cable:1:core,cable:2:core,0.0087328334575760003,-0.008720356263351766 +100000,cable:1:core,cable:3:core,0.00087200607389009807,-0.0031043854982987897 +100000,cable:2:core,cable:1:core,0.0087328334575760055,-0.0087203562633517712 +100000,cable:2:core,cable:2:core,0.091489317443228704,0.2836615002411198 +100000,cable:2:core,cable:3:core,0.0029229592659658302,-0.0055355130965314943 +100000,cable:3:core,cable:1:core,0.00087200607389009959,-0.003104385498298791 +100000,cable:3:core,cable:2:core,0.0029229592659658311,-0.0055355130965314952 +100000,cable:3:core,cable:3:core,0.024751108383294702,0.51186488670933217 +1000000,cable:1:core,cable:1:core,0.75371053759751805,1.5682785800867196 +1000000,cable:1:core,cable:2:core,0.00015809876830168704,0.001064782467487955 +1000000,cable:1:core,cable:3:core,8.1864957239162299e-05,8.9675897572887595e-05 +1000000,cable:2:core,cable:1:core,0.00015809876830168729,0.0010647824674879566 +1000000,cable:2:core,cable:2:core,0.753724628134118,1.5683153568234434 +1000000,cable:2:core,cable:3:core,0.00018019558652217053,0.00022583671129566818 +1000000,cable:3:core,cable:1:core,8.1864957239162313e-05,8.9675897572887568e-05 +1000000,cable:3:core,cable:2:core,0.00018019558652217056,0.00022583671129566807 +1000000,cable:3:core,cable:3:core,0.09317622230865559,4.9164327265927605 +10000000,cable:1:core,cable:1:core,4.5499045633958612,6.6607366695104684 +10000000,cable:1:core,cable:2:core,2.1359267021804777e-08,2.9940483256533021e-08 +10000000,cable:1:core,cable:3:core,1.6416933423796947e-09,-3.0218062359712461e-10 +10000000,cable:2:core,cable:1:core,2.1359267021804784e-08,2.9940483256533008e-08 +10000000,cable:2:core,cable:2:core,4.5501473574581013,6.6605028805599691 +10000000,cable:2:core,cable:3:core,4.9275836606184257e-09,-7.7688175554226174e-10 +10000000,cable:3:core,cable:1:core,1.6416933423796937e-09,-3.0218062359712528e-10 +10000000,cable:3:core,cable:2:core,4.9275836606184224e-09,-7.7688175554226101e-10 +10000000,cable:3:core,cable:3:core,0.28635320029725086,47.884683853844997 diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/fem/computation-options.txt b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/fem/computation-options.txt new file mode 100644 index 000000000..987cdbcf4 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/fem/computation-options.txt @@ -0,0 +1 @@ +(ui = false, mesh_policy = :remesh, resume_run_directory = nothing, keep_run_directory = true, trace = true, output_basis = :pul, verbosity = (default = 1,), gmsh_verbosity = 2, getdp_verbosity = 4, frequency_workers = 2, solver_threads = 4, plot_field_maps = false) \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/fem/details.txt b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/fem/details.txt new file mode 100644 index 000000000..bc36911bf --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/fem/details.txt @@ -0,0 +1 @@ +(files = [(path = "coordinator.lock", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/coordinator.lock", sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"), (path = "run.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/run.json", sha256 = "c88e7a97a913e572acc90bee68b631f9713fd26dc66204950e39e5c65c5ce4b1"), (path = "timing-summary.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/timing-summary.json", sha256 = "5c5a800eafc3caf43ae4253396b5abe8ad9a5cf6b1dba6c4ea423887d285cf74"), (path = "attempts/f0001-rejLad/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0001-rejLad/attempt.json", sha256 = "8562a2ee7ef32051107bb4bafb70a43ea79fc50e7464792a78d93b1b87645ba6"), (path = "attempts/f0001-rejLad/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0001-rejLad/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0001-rejLad/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0001-rejLad/getdp.log", sha256 = "657449d6a796cfa72e29bb53dc47a6fc681420936cd4e323b8bb1a6d7e10b29c"), (path = "attempts/f0001-rejLad/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0001-rejLad/petsc.log", sha256 = "0880109eaf22a8978ac07daf855e108e33b63e62e7df229df8d32559a43b8e69"), (path = "attempts/f0001-rejLad/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0001-rejLad/solver.pre", sha256 = "9b308817170e306be5b11166886bf25307f5c2440ecfd21256be6fe4a2e0e8cb"), (path = "attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0001-P.tsv", sha256 = "46fa546412f46ef46b2fb434f7008e166c57e8a707ee228c7505b3d7b74b69a1"), (path = "attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0001-Z.tsv", sha256 = "d0b571a4b155655571ecfc86ddfb815b07e9f85eb602f48868f7293cf7a09647"), (path = "attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0001-timing.tsv", sha256 = "b394dfc548f5fadb7cad04fa6a8d85adb2d610b12c98e2aac61edb3fbcdaca80"), (path = "attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0001.done", sha256 = "a40a11bab26b32dfb169550347f289dc710df24f59fe3ef20963d46f838f0586"), (path = "attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0002-P.tsv", sha256 = "8a559a373497f763d32a6095a7d9b497549217d33cf1719267290ae2c929dd07"), (path = "attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0002-Z.tsv", sha256 = "b4cd701a5b69fb49d365066b2927e54c950dc5d06a66f1a0df3339679ffd3a12"), (path = "attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0002-timing.tsv", sha256 = "7d414b81550475e600571ef870b8d9dd35888eba9dd214939853bbb35405cc99"), (path = "attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0002.done", sha256 = "f58042f034284ccf2955f2795037937a5bd0e4288ffc0d0d5124e33ee07f17ec"), (path = "attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0003-P.tsv", sha256 = "2e1b353722f20454ee70cef7fb46c153313f12094a7a8b0b988362c2913c6df9"), (path = "attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0003-Z.tsv", sha256 = "4352656d9afe9722eabfa397f9f0d6437a4ba11c5d9e993b6fc7f1f1bd7cba7a"), (path = "attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0003-timing.tsv", sha256 = "8bac2c85564c9dabe6a8a7d99cd43e117120c90f2453e43fc67f95f041061d75"), (path = "attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0003.done", sha256 = "41695cf0039479bd9b703276e3c15366a77a1e43f8490b48634d8a1c2a61be94"), (path = "attempts/f0002-u9t1S3/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0002-u9t1S3/attempt.json", sha256 = "ab84882854d5d8e8da09d15003ee6341cfb0a6a3b94f49ac8dde3596cef2ea55"), (path = "attempts/f0002-u9t1S3/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0002-u9t1S3/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0002-u9t1S3/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0002-u9t1S3/getdp.log", sha256 = "a2d9192db61f7a2564e5bd2ab5fa542ab96bfbdc52e8c2111cfbdfd2bdedbe0c"), (path = "attempts/f0002-u9t1S3/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0002-u9t1S3/petsc.log", sha256 = "b322b0e58b01a0f72ab978a536d971ba13612c04f080f8f8e56c8fcf239bed3c"), (path = "attempts/f0002-u9t1S3/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0002-u9t1S3/solver.pre", sha256 = "a9cb42910aa58597cce8145df1038bdb6383aed5f35eadf5ffa5ca296d1587d2"), (path = "attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0001-P.tsv", sha256 = "0f4072dfbd4ec940e6f063a1c8f953cd1ecc302f0ff0105bc650a2279ba9e3f8"), (path = "attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0001-Z.tsv", sha256 = "2d42112d21371805fdca88142eb211b62824faae1f28012e57c0c159b08ab6ee"), (path = "attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0001-timing.tsv", sha256 = "4de08b845373f35cd877dbf9768cce9e45195a4c25c6f18631d6c7623ba29f5c"), (path = "attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0001.done", sha256 = "3dbd9539f415ee889a34b9c6efe6b9cb8168b7c0945d52d7e63e920c8990d2d1"), (path = "attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0002-P.tsv", sha256 = "12ea30983f26ced34ad2cf546ddb66465e38832e80abe29f53f6d35c31e7add7"), (path = "attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0002-Z.tsv", sha256 = "ec3fca324f4aab7629a78d0dab90eb8c9b142a3299f96ffd6d7a942eac2e8c42"), (path = "attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0002-timing.tsv", sha256 = "6efc242b70bfbde32425ab607e68b509c0f1e3c34eb6fdf743ede33a1e72e93e"), (path = "attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0002.done", sha256 = "d498381e814531161686031fcde5aa549b456c6cbee61f6eb67916650247e717"), (path = "attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0003-P.tsv", sha256 = "b29350aa094132c0cb923ddd344529b8a145dfcffabfcdacc77886ea71e44f94"), (path = "attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0003-Z.tsv", sha256 = "4634c1eedd44f096ba8b45639a54915e2677ee47d8b32698dfbea647d5d228e5"), (path = "attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0003-timing.tsv", sha256 = "733a6b26b8e195789e81df0ac97dfd4e48c6971a4fdb1545b1c760d2c080b3e3"), (path = "attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0003.done", sha256 = "fa5c0c361441906d1aaa6b254ae52969806be93c36846f4c70c70a18dc833050"), (path = "attempts/f0003-Bw5FOk/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0003-Bw5FOk/attempt.json", sha256 = "e72cf67d138b6a70d0828e70383354e8df9856d54473de3898637a84bedc90f7"), (path = "attempts/f0003-Bw5FOk/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0003-Bw5FOk/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0003-Bw5FOk/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0003-Bw5FOk/getdp.log", sha256 = "bea36142bb54aa0f1c763f33f63efd7d8bbdfb6a981762c909549542257c6fb2"), (path = "attempts/f0003-Bw5FOk/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0003-Bw5FOk/petsc.log", sha256 = "8621f1245011d0c84f5b57c4ab0bc5309879c8a1f4d321bf5d3a39d12cb2959b"), (path = "attempts/f0003-Bw5FOk/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0003-Bw5FOk/solver.pre", sha256 = "b9b0dcd1101458efc97451050d7555b0dc921e075812327f2562fb8087cd6df8"), (path = "attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0001-P.tsv", sha256 = "0b1ea83c170a28e72f9251fb8a8637f72a704856561fd966c0869a1a5893e082"), (path = "attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0001-Z.tsv", sha256 = "03865b1fe279f225b912f741b6ba8efc40b88adcd335b8def4efa6b2ad1ca52a"), (path = "attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0001-timing.tsv", sha256 = "423ab5efb1eaadeba6420651381573e3428bae3d8c77b3b2ae7edc758768d7bb"), (path = "attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0001.done", sha256 = "02395cb824ac1c8ce1d2e8cd3c57fd19d5c904215aef49b1fba677c51e99d519"), (path = "attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0002-P.tsv", sha256 = "93d32ccea24bc99036d1291a31b68b68a0312515a1b10e590982cb94ab8cefbf"), (path = "attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0002-Z.tsv", sha256 = "e49b22f5faeac4fc17d06a7a0f0568bbadda235a1431126db6c0268bbbcda12d"), (path = "attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0002-timing.tsv", sha256 = "87b2df7edd6d066c5918d51d1b03bf4f3420ada46f96b977734d523d120afcf9"), (path = "attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0002.done", sha256 = "84034583a44a97df0ec5820a76fc891b95a9b6bc0bf693bed9882420a3eb5b05"), (path = "attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0003-P.tsv", sha256 = "24c8257bb4b0c512b85b43e1db9d18158684f268f276cc77192791029497ff51"), (path = "attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0003-Z.tsv", sha256 = "73291fd6c57b0b709db8b8e1034f28adb0f9ca38a0ba5b720d526c0931ad5b7f"), (path = "attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0003-timing.tsv", sha256 = "176375f083f0c5b3f87286b5363ef90dc10709a1dfa714ea30735a406e488f0e"), (path = "attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0003.done", sha256 = "aec9dd1bab4102d3c07f9c6abf83e4d604721961d8e37cbbc9620f7f71cafcae"), (path = "attempts/f0004-gQEWGq/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0004-gQEWGq/attempt.json", sha256 = "ce423681f144a989fb08ff86d9d18b8165bedf8ae015702a5e17b31980e6a4f1"), (path = "attempts/f0004-gQEWGq/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0004-gQEWGq/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0004-gQEWGq/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0004-gQEWGq/getdp.log", sha256 = "dcbd126b7d74df5773e6a8a3eab718375be886a1bf43cae55490daaaafd150a4"), (path = "attempts/f0004-gQEWGq/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0004-gQEWGq/petsc.log", sha256 = "cfb1c1836e72a8e259e0d66335bfe469c68731c377abaeea00907d6e515c6c66"), (path = "attempts/f0004-gQEWGq/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0004-gQEWGq/solver.pre", sha256 = "c3a2b0d2ab4f747f6c87e9d10c5261aa0d78551e53c7f8b3e6c707678423a1c6"), (path = "attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0001-P.tsv", sha256 = "0414f7d9279ba3fc48565c59dfce50ff1a6ac9c5a9cadede2426432da9316f4e"), (path = "attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0001-Z.tsv", sha256 = "037c545ac4f3ac6a1608b35709600ccd4616928d836607f5a52c89ff361d03ed"), (path = "attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0001-timing.tsv", sha256 = "7f63264a16c152a4f0b86e1d43fd7f4eed1f18785cf2d7d5fd51a2c04d5c8154"), (path = "attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0001.done", sha256 = "05b52afc2d2a0ffefd0bb6e40585165d1b2572547d59d3c360b4163202709006"), (path = "attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0002-P.tsv", sha256 = "3afc45db4632952d5724691256d58678bcd2eb881e27e70142173c723b6dabae"), (path = "attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0002-Z.tsv", sha256 = "967fe79df063cebf45dbeb09e712f518afb20029986bc826ee2818437af3731b"), (path = "attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0002-timing.tsv", sha256 = "5e44fba2b54e0ef8dd365c0b2a16dd1c5b5b52bf94086a2a5821aed7e411e616"), (path = "attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0002.done", sha256 = "35d216e20aed80a36327575b350d8e4c12b70f2038f5d07d55614c92afdd6b6b"), (path = "attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0003-P.tsv", sha256 = "aa81bae32c350b92b595eabe7e3ba951a469af09a3b429afd7ec99d91078a857"), (path = "attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0003-Z.tsv", sha256 = "a2d6f4c7ae9ac114eae3cc56462cf2e1bb40e5b4de651496ac60726a61824058"), (path = "attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0003-timing.tsv", sha256 = "1ee834f682654b899d943de24e87147c2621eaba998f8b2d6c7c89f8963723a5"), (path = "attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0003.done", sha256 = "eae2a23f21e24dd158a817c096e6e77f32e1d2a850c4163d279c3531eef2af5a"), (path = "attempts/f0005-tKhfHk/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0005-tKhfHk/attempt.json", sha256 = "9cf2d2dfd6b77b105cebebfd3a19ecf0c6f1802b30109a55ed3d6bbdbcc90c0a"), (path = "attempts/f0005-tKhfHk/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0005-tKhfHk/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0005-tKhfHk/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0005-tKhfHk/getdp.log", sha256 = "4f365fe953b9210f118cb0374bda120041d54424fd16b6d3c761003d1e685a6d"), (path = "attempts/f0005-tKhfHk/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0005-tKhfHk/petsc.log", sha256 = "9fa42b00ddf30ecfcb37c665668b18455b2bcdbaf048838381a2feebfe1a0399"), (path = "attempts/f0005-tKhfHk/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0005-tKhfHk/solver.pre", sha256 = "f365d54cd23d09ce06cfef0173cc1fdf77ff32c007bf6083b3c91f09c19e7e39"), (path = "attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0001-P.tsv", sha256 = "4fd374e319aa02cd4a461d06a94a2b964d070dfaa6672007ffe0dd38761e0c9b"), (path = "attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0001-Z.tsv", sha256 = "fab5e56f6c89d4c928ecd9ace48cdc4cfba8476d91d72299c823cc80afb8160d"), (path = "attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0001-timing.tsv", sha256 = "53574e2b5e1083732f43d22985fe6095f459d67fd3dc411bf8dbb873b397967d"), (path = "attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0001.done", sha256 = "ee7bf9802d600055d484ec4e501666418037b219c7bf910eefda35e16a843b08"), (path = "attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0002-P.tsv", sha256 = "7424966f1503b3e63246c9dcf726fa0954ecd7968cd2ea40a39811bd44812764"), (path = "attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0002-Z.tsv", sha256 = "13f053e4852ab5b18315cc0d54edc75d52bc8a3e0ecf5af5933d217c5ca270d9"), (path = "attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0002-timing.tsv", sha256 = "6d3a950a55d9c9e8434c0d8132ca99b73fda65a5b4b0cd3966f5f3c036be4567"), (path = "attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0002.done", sha256 = "e26c3631aaa36c47efb3ede81242002dbebf27057f428fe57f60849cede6b3f3"), (path = "attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0003-P.tsv", sha256 = "a7009ee10a8bf5f8c516c5ebb74715bd10e620e7e08459d5aff103c1ffe692e8"), (path = "attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0003-Z.tsv", sha256 = "e6edda7298951e5ff0eb0f3d453b23678cf248e4072abede586c0e0ab4292a44"), (path = "attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0003-timing.tsv", sha256 = "7add1c8273c631dc052688e7e4a99216fca5b5579b2aa3266953f773a181ba68"), (path = "attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0003.done", sha256 = "a91d7ca1bb120ef43c71df371c45a71b1cc7744f1ee2eb082338700de7f329c9"), (path = "attempts/f0006-iHjV2j/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0006-iHjV2j/attempt.json", sha256 = "e5381b46788c5107cda6872e2cc33527a7d232d5036ba379528ac9cafd3fdc3c"), (path = "attempts/f0006-iHjV2j/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0006-iHjV2j/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0006-iHjV2j/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0006-iHjV2j/getdp.log", sha256 = "c58db5232513e5377d942053ef7689788460431b84cf33ccae2dcc68167f77f7"), (path = "attempts/f0006-iHjV2j/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0006-iHjV2j/petsc.log", sha256 = "5658b011e07d3553bc66292595bd603b84a7a7c488bfbb72c1e2791eecc68be8"), (path = "attempts/f0006-iHjV2j/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0006-iHjV2j/solver.pre", sha256 = "3eeaeffb9b8b1557836dc8b1acb586c0ff94c96b6225e666de5f80d704090928"), (path = "attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0001-P.tsv", sha256 = "ab503fe79c9dda6c9ae0c08c9330cdcf21a85965792c0355a3901e5839cb5853"), (path = "attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0001-Z.tsv", sha256 = "4842e5855355757b6dc19a967a4a92014f541c22a5a48e700d02dc7231e3ec4e"), (path = "attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0001-timing.tsv", sha256 = "7a39d6225512aeac0c8379081c92bc76df854c5c7db4cbf3dae125b580616236"), (path = "attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0001.done", sha256 = "e41a78288341a2ae156cd28b6102b3a31a74b012eb4deb233e88fae392912e8e"), (path = "attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0002-P.tsv", sha256 = "0c47b4d70c891a27b8d4c928b85461b116329f11a83ab2b3312ef76837dc8730"), (path = "attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0002-Z.tsv", sha256 = "53f38964d8b833d664ffcfed54d520b10cd1f97d5005888aa15e8483e683d1df"), (path = "attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0002-timing.tsv", sha256 = "994b3247fe03648d4c1582a28104f9dd758d559c518a4120a1928f0251a9bd4f"), (path = "attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0002.done", sha256 = "c105367e4bac19858725b1b7c56efbb25ae370173d002f9a678a4762a5c69ac9"), (path = "attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0003-P.tsv", sha256 = "cedf5b927eebe2818abd67c2445ce6599c08892e7b8984d278302cbe5cddb03c"), (path = "attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0003-Z.tsv", sha256 = "67209302d9a76618a9976956b513903dd42632964eba09bb10367acaf5e3a623"), (path = "attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0003-timing.tsv", sha256 = "a418c43a4f4399b78eb81c969c8efb698635a7a9c8c8f00f380196470e977ed6"), (path = "attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0003.done", sha256 = "de6214d7f2cf6d2097d94d720289a7424da0b9bf7a5df27326d8b69e936798e8"), (path = "attempts/f0007-pVC67f/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0007-pVC67f/attempt.json", sha256 = "deee433e8de398d52deea84e4287e7d086e0ba7c716072c857f408c58dcc958e"), (path = "attempts/f0007-pVC67f/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0007-pVC67f/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0007-pVC67f/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0007-pVC67f/getdp.log", sha256 = "0eaa3065bd21d6e6cd69fef3762da1bb150b636e1696f30ef93abca8d33f8c3c"), (path = "attempts/f0007-pVC67f/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0007-pVC67f/petsc.log", sha256 = "6c4f1df3c8294ea8eaa03770a97a49eac7880d0222e55f1eed128f313e56afb4"), (path = "attempts/f0007-pVC67f/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0007-pVC67f/solver.pre", sha256 = "2521310c3054739963c072a037e3188018853290e8609a458f4de0b8936c72e3"), (path = "attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0001-P.tsv", sha256 = "6a953a9b70ca02ac0cd4488e33722d3dd0fc58163b0ebd9c3fa5f572dacf9866"), (path = "attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0001-Z.tsv", sha256 = "a6a42398bb170d2d9b55b83b578a8dc3a7cea50f8f21ee566fe488d9f630512b"), (path = "attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0001-timing.tsv", sha256 = "ede82f09fc10b7433e42ba302e2cde9791c8997192de4cb2940d8e7a0680ef74"), (path = "attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0001.done", sha256 = "d95e4d651bb7fa68e36e0077af5a10e10baacf852173da7ac0f1cca3e414dc7f"), (path = "attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0002-P.tsv", sha256 = "7e3db93e6e731004069d9a6c1a6524289fe6b924ebab0115aba05df38154f6ce"), (path = "attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0002-Z.tsv", sha256 = "f40055d73faf28e512cf13715e8e03533361ee0c1cd710f2dac96d1f18ca6b89"), (path = "attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0002-timing.tsv", sha256 = "899406ac6a6adc605a7153cc734fdcc98bfe9c5c2d34b033da25252cf57ca027"), (path = "attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0002.done", sha256 = "a953202777b2aea0282f4e5ecac9feb227f850cb3f80f2247ca07a708f3635be"), (path = "attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0003-P.tsv", sha256 = "c5ea44f265565130c3d12c198b2b8a8699d983889e3a6b95afba3dbffb9bc6ef"), (path = "attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0003-Z.tsv", sha256 = "a697df98dcb85881385c17ed46a6e7ad2a35516c8fe9fa6059dcbf752f6f0aff"), (path = "attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0003-timing.tsv", sha256 = "cc042d0730a1ca8702092704495c615b8bf94153e909d74ae1827f741ccd6c6f"), (path = "attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0003.done", sha256 = "bc6e3e671a966700354c8fb7f9ab0ff830db8a4b88332e0ec4974ce29df37b0c"), (path = "attempts/f0008-RVEsvw/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0008-RVEsvw/attempt.json", sha256 = "44801cbd3e273c45337372df0718f495cff40d3f9f222bc52261d1684968ceb8"), (path = "attempts/f0008-RVEsvw/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0008-RVEsvw/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0008-RVEsvw/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0008-RVEsvw/getdp.log", sha256 = "2118f6f654e30d2a50f79fbbe90babad7c873f22146712b9924cfdf3fa4123f2"), (path = "attempts/f0008-RVEsvw/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0008-RVEsvw/petsc.log", sha256 = "a06887ba0af613c7ee6f693a15f806972ef5e7086f8f13797b0b25817aabe3fa"), (path = "attempts/f0008-RVEsvw/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0008-RVEsvw/solver.pre", sha256 = "d78b89782facd5771242d1147b04332a07f9466f7415dc70d3afd65e302b2e1d"), (path = "attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0001-P.tsv", sha256 = "c56d8b5c9e45b5e774e2f4745c7f5e46b1739ad02ded9b4ab4b75806144623ff"), (path = "attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0001-Z.tsv", sha256 = "d5c8d42902eb6e828326e1d52e541d31927607be3360db8087af5ee1dd382a54"), (path = "attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0001-timing.tsv", sha256 = "113721716ab550a5288b188f326176926117290c8321e0953dbdf482945b1684"), (path = "attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0001.done", sha256 = "c03ca6db0131d3218478fb4e85f0350dca26787b9ffdfc9d50b5d68ad93b352c"), (path = "attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0002-P.tsv", sha256 = "344aa533e8e46c9a85a5916ffb56f37a269dc67882102d52490cf76cfba8a2bc"), (path = "attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0002-Z.tsv", sha256 = "d0208d54fda3554aa0ce358a0cef8f045f5294db8c096b7fe5fbd56bf83f6454"), (path = "attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0002-timing.tsv", sha256 = "9ffb5b15ef7c464b3719de0f34b6e4ae3c73ea5eea8e3369d2d0538697702a2d"), (path = "attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0002.done", sha256 = "76f50b528daca79ab1eac47c47f96ca1e7b8ad94bcaefbaac04335941306f993"), (path = "attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0003-P.tsv", sha256 = "59b6bb9720ab227ab3d2d09906c21dd6c34e5a333e4f09edefe6d3d820209c3a"), (path = "attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0003-Z.tsv", sha256 = "62d43155696ae2feb0e2b0bfe6190a6da458137a597810a3c0bfb6b4a61a0c7d"), (path = "attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0003-timing.tsv", sha256 = "c2043f0b3bd54de33add441283f0c17ad4e4879a907aad34feef7bc9307affa9"), (path = "attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0003.done", sha256 = "20bdbe7d9351850f55ebf49a31f3a93235d04aea44253ddd2d75828a83fa1c74"), (path = "attempts/f0009-M2mf88/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0009-M2mf88/attempt.json", sha256 = "05fe13f80e4ec7431bf5dd5b37e3f7b8b0526a8c6f37910c59bc8ca72e091ba3"), (path = "attempts/f0009-M2mf88/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0009-M2mf88/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0009-M2mf88/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0009-M2mf88/getdp.log", sha256 = "65945274edc317bd815a50c5123484fd775657873cf6fb045067affe4beed8ce"), (path = "attempts/f0009-M2mf88/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0009-M2mf88/petsc.log", sha256 = "a336358acc75bfcb449a3e7b6df84d2f3ab1b3f1600d9ad76797739e018e9819"), (path = "attempts/f0009-M2mf88/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0009-M2mf88/solver.pre", sha256 = "0b3af1d7a334ee1386f03963cc96bc125f3ac443ccbe3d1d9d07879487f9657e"), (path = "attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0001-P.tsv", sha256 = "84dcbcce7647f22987130619c1e2fb73b76e0501359492b79b1e313a21cfbf38"), (path = "attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0001-Z.tsv", sha256 = "c569f16e96adc30dbc9ad683e8312fcdfc07f8c7ba11fa3a2aa485952c205f07"), (path = "attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0001-timing.tsv", sha256 = "ff524787b38a6bc2db153e66fc91cb14edf04ca974ff7a507b5df955d9f91f31"), (path = "attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0001.done", sha256 = "22b8b831af1067ac03eaf9570ddd71235af29f6b65e559199200376f6ef90f2a"), (path = "attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0002-P.tsv", sha256 = "127a7c2b65f83575a74b5528c95fc23ce7a55636b743a3325a122ad3392da8bb"), (path = "attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0002-Z.tsv", sha256 = "13a2c974c7616931b0317730c32260e188603249ebf4898e3bcec1917cc08765"), (path = "attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0002-timing.tsv", sha256 = "5e5e4b27f6e9dd766c91a45c7aa5411fd4d7f1ea58b38c00a45a4aa0e3945726"), (path = "attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0002.done", sha256 = "2ab6b278377a8a5090d5d456a9193028844db99b8f3fa96c2433d70920e73c22"), (path = "attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0003-P.tsv", sha256 = "9a54f72af8f974313fe3a96ffbb33e97f9a58404c07a375a366db862653a9dba"), (path = "attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0003-Z.tsv", sha256 = "afafa17b6905e9c8497efe5775c4db109951650d8bf6949000eda092f771a03b"), (path = "attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0003-timing.tsv", sha256 = "8b05303ac1ca02207487c1ef70cdb4839ef26bc1ad314c7b9c665edfb4fd063b"), (path = "attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0003.done", sha256 = "3449a1ca026b39cca413695b5bd20e176addf6bcf3103f9147e8ac56407cafa3"), (path = "input/computation.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/input/computation.json", sha256 = "4a7d5ce6eaf39ffb37f3a3c9b337b2e8780c05af7552c261e1353d01542fab25"), (path = "input/model_data.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/input/model_data.pro", sha256 = "a1b206a600577a6ee660999e24c6a66687c4a846aa825e45e47fd243da2dea3a"), (path = "input/problem.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/input/problem.json", sha256 = "b634b2da621381c164831235b0c668a328d977158438c617014aed408c689eae"), (path = "input/getdp/integration.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/input/getdp/integration.pro", sha256 = "e113951ac1e6e658f1be50e53875e30c85497445f0e6584a8a7ab1848588e902"), (path = "input/getdp/jacobian.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/input/getdp/jacobian.pro", sha256 = "014208d39043ab13183c42c312aba459265715da6d6ddc38ec1fbc39b6090c73"), (path = "input/getdp/materials.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/input/getdp/materials.pro", sha256 = "581f089113810a379d8a49c03d79f55045b723c8f4c761d0a493a019ff1f5d9e"), (path = "input/getdp/model.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/input/getdp/model.pro", sha256 = "c0bb424db5ab4db9ca22f549f675edb4646f69910d2aa14e20cf4f40e41bd313"), (path = "input/getdp/quasi-full.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/input/getdp/quasi-full.pro", sha256 = "1aede35114ea686c5f109089478027d241dec12dc9fd14a24452b348f89b08e4"), (path = "input/getdp/quasi-tem.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/input/getdp/quasi-tem.pro", sha256 = "448d9019ad417c9a784da20170b855dd25e559297499a83d19b007ee095c084d"), (path = "logs/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/logs/getdp.log", sha256 = "f10f17af9f97e9a9126e657b5cdc995e426b8c7911d28e966d3da39506673536"), (path = "mesh/frequency_0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/mesh/frequency_0001.json", sha256 = "5a42638f462abe444900a4a6039e5b689de2e12167bd9c045af0342785d973eb"), (path = "mesh/frequency_0001.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/mesh/frequency_0001.msh", sha256 = "9f7ac3f89024f7ac31e673df1e5aee620ea2a7ca488066e8452a28ef936bda4c"), (path = "mesh/frequency_0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/mesh/frequency_0002.json", sha256 = "493d03a653898e6a73baeb9ecfecb2439064508a8ae16e2966a5cc813c7b94d3"), (path = "mesh/frequency_0002.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/mesh/frequency_0002.msh", sha256 = "6fb71fe421a70e09392fd82abdc22a0576eabd43ac8930b4c5bb3f50c2b9da11"), (path = "mesh/frequency_0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/mesh/frequency_0003.json", sha256 = "4b2eea760259765cadefa24dc48d0fc44c537956ae3530d65dffa4833f8e8828"), (path = "mesh/frequency_0003.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/mesh/frequency_0003.msh", sha256 = "8179ba77ef1c70adbaa3c352923b796251875d73a5a2a2d528ca3ae4c406c9ed"), (path = "mesh/frequency_0004.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/mesh/frequency_0004.json", sha256 = "9e71f0e5320732734a343ce809cb84c9d909c4c2424635135e96761567890d67"), (path = "mesh/frequency_0004.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/mesh/frequency_0004.msh", sha256 = "89986de0c9236fc43de83f4bfddd8ba4affe3931ca8014faed026c4496355ff9"), (path = "mesh/frequency_0005.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/mesh/frequency_0005.json", sha256 = "9057e79cc5d785cc171fe4a1cb6445de90ae1eb9dc84ac999f195756aa932fad"), (path = "mesh/frequency_0005.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/mesh/frequency_0005.msh", sha256 = "34f32f2aea1e8d264d267ac80bcf7d89c9d238fc9923e571b41c2e7d75226451"), (path = "mesh/frequency_0006.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/mesh/frequency_0006.json", sha256 = "0bff4d15bb12c712df8beb043fd3944a566c2ce4cc049fb33abeafca4eda39ae"), (path = "mesh/frequency_0006.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/mesh/frequency_0006.msh", sha256 = "b6cd8780850b7790d802ab66c6ff7347ebd785ec37b2e97c4c92babe07d68ad9"), (path = "mesh/frequency_0007.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/mesh/frequency_0007.json", sha256 = "f9fb0398f2c39b9edda760c06956a842e3ff5fe7fd96eb6085ba860677f81de0"), (path = "mesh/frequency_0007.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/mesh/frequency_0007.msh", sha256 = "88997a13269109f19a2b18a9c4fb807dec2eb10aed16b0371056cb244a0c6e12"), (path = "mesh/frequency_0008.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/mesh/frequency_0008.json", sha256 = "35fc92197f1e533841783a2e802788c791c43a993fec22a3fe47ff1700406ef0"), (path = "mesh/frequency_0008.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/mesh/frequency_0008.msh", sha256 = "6f8c976f3dab78a0a2e5c067c776117eef0e75af5393fcffe3be01b3b22a7988"), (path = "mesh/model.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/mesh/model.json", sha256 = "fd86724063e5a2569cd1c947450b2f33a0a2323d3e9f0484234e6e3035748546"), (path = "mesh/model.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/mesh/model.msh", sha256 = "7251370696dd8bdb8983b72a9c4314ad39b966f0653ef4480f5bf6cd2c316fa1"), (path = "raw/P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/P.tsv", sha256 = "ea3edfb5e92d0ed70880a6f2ebd39b41bae5299088bab46afd3329e6aafa6987"), (path = "raw/Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/Z.tsv", sha256 = "b78fec03f2275df0ed1ebc733c00c8dc0bb7cd2a6b26423b12f00d336032695b"), (path = "raw/checksums.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/checksums.json", sha256 = "77cf48dba03bb993c7baaccbc23c83ef026684b2cba3ea08d5d35611d400a07a"), (path = "raw/scan_complete.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/scan_complete.tsv", sha256 = "2cc9eff50bb7e6fff0e2555f96d93ebe952c4a1a158a1dca82fe6cca3aa1afab"), (path = "raw/jobs/getdp-f0001-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0001-b0001-P.tsv", sha256 = "46fa546412f46ef46b2fb434f7008e166c57e8a707ee228c7505b3d7b74b69a1"), (path = "raw/jobs/getdp-f0001-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0001-b0001-Z.tsv", sha256 = "d0b571a4b155655571ecfc86ddfb815b07e9f85eb602f48868f7293cf7a09647"), (path = "raw/jobs/getdp-f0001-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0001-b0001-timing.tsv", sha256 = "b394dfc548f5fadb7cad04fa6a8d85adb2d610b12c98e2aac61edb3fbcdaca80"), (path = "raw/jobs/getdp-f0001-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0001-b0001.json", sha256 = "e4cbf1face5a404351a252c2f93b6933374548b1320037741dd904c5602b6914"), (path = "raw/jobs/getdp-f0001-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0001-b0002-P.tsv", sha256 = "8a559a373497f763d32a6095a7d9b497549217d33cf1719267290ae2c929dd07"), (path = "raw/jobs/getdp-f0001-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0001-b0002-Z.tsv", sha256 = "b4cd701a5b69fb49d365066b2927e54c950dc5d06a66f1a0df3339679ffd3a12"), (path = "raw/jobs/getdp-f0001-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0001-b0002-timing.tsv", sha256 = "7d414b81550475e600571ef870b8d9dd35888eba9dd214939853bbb35405cc99"), (path = "raw/jobs/getdp-f0001-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0001-b0002.json", sha256 = "46a6ce16f246c2d69d3584db742c93448921f883cb132b83d3ac6416ddc17df6"), (path = "raw/jobs/getdp-f0001-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0001-b0003-P.tsv", sha256 = "2e1b353722f20454ee70cef7fb46c153313f12094a7a8b0b988362c2913c6df9"), (path = "raw/jobs/getdp-f0001-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0001-b0003-Z.tsv", sha256 = "4352656d9afe9722eabfa397f9f0d6437a4ba11c5d9e993b6fc7f1f1bd7cba7a"), (path = "raw/jobs/getdp-f0001-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0001-b0003-timing.tsv", sha256 = "8bac2c85564c9dabe6a8a7d99cd43e117120c90f2453e43fc67f95f041061d75"), (path = "raw/jobs/getdp-f0001-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0001-b0003.json", sha256 = "45a62eb18e93fb0c329a9bd8725c74a8d887077402b4d7e390eac2eea408e8c7"), (path = "raw/jobs/getdp-f0002-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0002-b0001-P.tsv", sha256 = "0f4072dfbd4ec940e6f063a1c8f953cd1ecc302f0ff0105bc650a2279ba9e3f8"), (path = "raw/jobs/getdp-f0002-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0002-b0001-Z.tsv", sha256 = "2d42112d21371805fdca88142eb211b62824faae1f28012e57c0c159b08ab6ee"), (path = "raw/jobs/getdp-f0002-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0002-b0001-timing.tsv", sha256 = "4de08b845373f35cd877dbf9768cce9e45195a4c25c6f18631d6c7623ba29f5c"), (path = "raw/jobs/getdp-f0002-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0002-b0001.json", sha256 = "36e56bb257f73a06d1f1b67f3d0d4bd1dac9572c539e3cdfb421acb351d75b44"), (path = "raw/jobs/getdp-f0002-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0002-b0002-P.tsv", sha256 = "12ea30983f26ced34ad2cf546ddb66465e38832e80abe29f53f6d35c31e7add7"), (path = "raw/jobs/getdp-f0002-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0002-b0002-Z.tsv", sha256 = "ec3fca324f4aab7629a78d0dab90eb8c9b142a3299f96ffd6d7a942eac2e8c42"), (path = "raw/jobs/getdp-f0002-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0002-b0002-timing.tsv", sha256 = "6efc242b70bfbde32425ab607e68b509c0f1e3c34eb6fdf743ede33a1e72e93e"), (path = "raw/jobs/getdp-f0002-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0002-b0002.json", sha256 = "38ee2286796f7f8cfef1c995be19ad0f17a5bf561bad329ac2baf9b1af272784"), (path = "raw/jobs/getdp-f0002-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0002-b0003-P.tsv", sha256 = "b29350aa094132c0cb923ddd344529b8a145dfcffabfcdacc77886ea71e44f94"), (path = "raw/jobs/getdp-f0002-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0002-b0003-Z.tsv", sha256 = "4634c1eedd44f096ba8b45639a54915e2677ee47d8b32698dfbea647d5d228e5"), (path = "raw/jobs/getdp-f0002-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0002-b0003-timing.tsv", sha256 = "733a6b26b8e195789e81df0ac97dfd4e48c6971a4fdb1545b1c760d2c080b3e3"), (path = "raw/jobs/getdp-f0002-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0002-b0003.json", sha256 = "f1ef2feee5b64e5f2fe75dcd2b5220542254ede44c2a29f441717c95f2f546d4"), (path = "raw/jobs/getdp-f0003-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0003-b0001-P.tsv", sha256 = "0b1ea83c170a28e72f9251fb8a8637f72a704856561fd966c0869a1a5893e082"), (path = "raw/jobs/getdp-f0003-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0003-b0001-Z.tsv", sha256 = "03865b1fe279f225b912f741b6ba8efc40b88adcd335b8def4efa6b2ad1ca52a"), (path = "raw/jobs/getdp-f0003-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0003-b0001-timing.tsv", sha256 = "423ab5efb1eaadeba6420651381573e3428bae3d8c77b3b2ae7edc758768d7bb"), (path = "raw/jobs/getdp-f0003-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0003-b0001.json", sha256 = "b4db402c9a77fa493c6cc32af591155ca21ade728bc7a96cb20b83b1b61730b5"), (path = "raw/jobs/getdp-f0003-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0003-b0002-P.tsv", sha256 = "93d32ccea24bc99036d1291a31b68b68a0312515a1b10e590982cb94ab8cefbf"), (path = "raw/jobs/getdp-f0003-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0003-b0002-Z.tsv", sha256 = "e49b22f5faeac4fc17d06a7a0f0568bbadda235a1431126db6c0268bbbcda12d"), (path = "raw/jobs/getdp-f0003-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0003-b0002-timing.tsv", sha256 = "87b2df7edd6d066c5918d51d1b03bf4f3420ada46f96b977734d523d120afcf9"), (path = "raw/jobs/getdp-f0003-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0003-b0002.json", sha256 = "dc720457b290b24a56cb8d19fe650e4d9928ca7fd9f97a90e7a9244b64af9f2c"), (path = "raw/jobs/getdp-f0003-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0003-b0003-P.tsv", sha256 = "24c8257bb4b0c512b85b43e1db9d18158684f268f276cc77192791029497ff51"), (path = "raw/jobs/getdp-f0003-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0003-b0003-Z.tsv", sha256 = "73291fd6c57b0b709db8b8e1034f28adb0f9ca38a0ba5b720d526c0931ad5b7f"), (path = "raw/jobs/getdp-f0003-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0003-b0003-timing.tsv", sha256 = "176375f083f0c5b3f87286b5363ef90dc10709a1dfa714ea30735a406e488f0e"), (path = "raw/jobs/getdp-f0003-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0003-b0003.json", sha256 = "42dd87cefadd1cd807a76d1612e9ee59d2a394e71dec1758060c75976c5f7c1e"), (path = "raw/jobs/getdp-f0004-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0004-b0001-P.tsv", sha256 = "0414f7d9279ba3fc48565c59dfce50ff1a6ac9c5a9cadede2426432da9316f4e"), (path = "raw/jobs/getdp-f0004-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0004-b0001-Z.tsv", sha256 = "037c545ac4f3ac6a1608b35709600ccd4616928d836607f5a52c89ff361d03ed"), (path = "raw/jobs/getdp-f0004-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0004-b0001-timing.tsv", sha256 = "7f63264a16c152a4f0b86e1d43fd7f4eed1f18785cf2d7d5fd51a2c04d5c8154"), (path = "raw/jobs/getdp-f0004-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0004-b0001.json", sha256 = "900ff5a09fc1277b901349f4b38d681e356476f52b9971a9ebeee28ed4ecd5dc"), (path = "raw/jobs/getdp-f0004-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0004-b0002-P.tsv", sha256 = "3afc45db4632952d5724691256d58678bcd2eb881e27e70142173c723b6dabae"), (path = "raw/jobs/getdp-f0004-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0004-b0002-Z.tsv", sha256 = "967fe79df063cebf45dbeb09e712f518afb20029986bc826ee2818437af3731b"), (path = "raw/jobs/getdp-f0004-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0004-b0002-timing.tsv", sha256 = "5e44fba2b54e0ef8dd365c0b2a16dd1c5b5b52bf94086a2a5821aed7e411e616"), (path = "raw/jobs/getdp-f0004-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0004-b0002.json", sha256 = "1c4f66f014440c685269a7c5c6f12235a7de22dcd389fa8945a2b199625e500a"), (path = "raw/jobs/getdp-f0004-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0004-b0003-P.tsv", sha256 = "aa81bae32c350b92b595eabe7e3ba951a469af09a3b429afd7ec99d91078a857"), (path = "raw/jobs/getdp-f0004-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0004-b0003-Z.tsv", sha256 = "a2d6f4c7ae9ac114eae3cc56462cf2e1bb40e5b4de651496ac60726a61824058"), (path = "raw/jobs/getdp-f0004-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0004-b0003-timing.tsv", sha256 = "1ee834f682654b899d943de24e87147c2621eaba998f8b2d6c7c89f8963723a5"), (path = "raw/jobs/getdp-f0004-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0004-b0003.json", sha256 = "b57ef8cffff6783a05e0d42f2237b28d51b9d8d2140bce4798a461adc088c02f"), (path = "raw/jobs/getdp-f0005-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0005-b0001-P.tsv", sha256 = "4fd374e319aa02cd4a461d06a94a2b964d070dfaa6672007ffe0dd38761e0c9b"), (path = "raw/jobs/getdp-f0005-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0005-b0001-Z.tsv", sha256 = "fab5e56f6c89d4c928ecd9ace48cdc4cfba8476d91d72299c823cc80afb8160d"), (path = "raw/jobs/getdp-f0005-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0005-b0001-timing.tsv", sha256 = "53574e2b5e1083732f43d22985fe6095f459d67fd3dc411bf8dbb873b397967d"), (path = "raw/jobs/getdp-f0005-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0005-b0001.json", sha256 = "aa6dbb92b0a34dbff246f4ed08b7b5b8e6097d75c106f4c63339f49813c34696"), (path = "raw/jobs/getdp-f0005-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0005-b0002-P.tsv", sha256 = "7424966f1503b3e63246c9dcf726fa0954ecd7968cd2ea40a39811bd44812764"), (path = "raw/jobs/getdp-f0005-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0005-b0002-Z.tsv", sha256 = "13f053e4852ab5b18315cc0d54edc75d52bc8a3e0ecf5af5933d217c5ca270d9"), (path = "raw/jobs/getdp-f0005-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0005-b0002-timing.tsv", sha256 = "6d3a950a55d9c9e8434c0d8132ca99b73fda65a5b4b0cd3966f5f3c036be4567"), (path = "raw/jobs/getdp-f0005-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0005-b0002.json", sha256 = "edd7f292a0952addc23c6401d479197a043d3af1b9b450a76b8e1b2a2276c915"), (path = "raw/jobs/getdp-f0005-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0005-b0003-P.tsv", sha256 = "a7009ee10a8bf5f8c516c5ebb74715bd10e620e7e08459d5aff103c1ffe692e8"), (path = "raw/jobs/getdp-f0005-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0005-b0003-Z.tsv", sha256 = "e6edda7298951e5ff0eb0f3d453b23678cf248e4072abede586c0e0ab4292a44"), (path = "raw/jobs/getdp-f0005-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0005-b0003-timing.tsv", sha256 = "7add1c8273c631dc052688e7e4a99216fca5b5579b2aa3266953f773a181ba68"), (path = "raw/jobs/getdp-f0005-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0005-b0003.json", sha256 = "811dd406b2341ba32efa6c2a4377fdedffccfab6e86d02598638ffba530f080f"), (path = "raw/jobs/getdp-f0006-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0006-b0001-P.tsv", sha256 = "ab503fe79c9dda6c9ae0c08c9330cdcf21a85965792c0355a3901e5839cb5853"), (path = "raw/jobs/getdp-f0006-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0006-b0001-Z.tsv", sha256 = "4842e5855355757b6dc19a967a4a92014f541c22a5a48e700d02dc7231e3ec4e"), (path = "raw/jobs/getdp-f0006-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0006-b0001-timing.tsv", sha256 = "7a39d6225512aeac0c8379081c92bc76df854c5c7db4cbf3dae125b580616236"), (path = "raw/jobs/getdp-f0006-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0006-b0001.json", sha256 = "bc10b2fd33ccfbce609f0438c3d4a553571542dc1c8a3d31f6e417e707f39b98"), (path = "raw/jobs/getdp-f0006-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0006-b0002-P.tsv", sha256 = "0c47b4d70c891a27b8d4c928b85461b116329f11a83ab2b3312ef76837dc8730"), (path = "raw/jobs/getdp-f0006-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0006-b0002-Z.tsv", sha256 = "53f38964d8b833d664ffcfed54d520b10cd1f97d5005888aa15e8483e683d1df"), (path = "raw/jobs/getdp-f0006-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0006-b0002-timing.tsv", sha256 = "994b3247fe03648d4c1582a28104f9dd758d559c518a4120a1928f0251a9bd4f"), (path = "raw/jobs/getdp-f0006-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0006-b0002.json", sha256 = "661bfa3b19ebdd8e241529de918be23383b1351a69de5bff0dc9f390514cf894"), (path = "raw/jobs/getdp-f0006-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0006-b0003-P.tsv", sha256 = "cedf5b927eebe2818abd67c2445ce6599c08892e7b8984d278302cbe5cddb03c"), (path = "raw/jobs/getdp-f0006-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0006-b0003-Z.tsv", sha256 = "67209302d9a76618a9976956b513903dd42632964eba09bb10367acaf5e3a623"), (path = "raw/jobs/getdp-f0006-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0006-b0003-timing.tsv", sha256 = "a418c43a4f4399b78eb81c969c8efb698635a7a9c8c8f00f380196470e977ed6"), (path = "raw/jobs/getdp-f0006-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0006-b0003.json", sha256 = "76e43fdef27f8cb5c2dda9cefcec7b211b79c62ba0fd24aaf8fe97915548a7be"), (path = "raw/jobs/getdp-f0007-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0007-b0001-P.tsv", sha256 = "6a953a9b70ca02ac0cd4488e33722d3dd0fc58163b0ebd9c3fa5f572dacf9866"), (path = "raw/jobs/getdp-f0007-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0007-b0001-Z.tsv", sha256 = "a6a42398bb170d2d9b55b83b578a8dc3a7cea50f8f21ee566fe488d9f630512b"), (path = "raw/jobs/getdp-f0007-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0007-b0001-timing.tsv", sha256 = "ede82f09fc10b7433e42ba302e2cde9791c8997192de4cb2940d8e7a0680ef74"), (path = "raw/jobs/getdp-f0007-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0007-b0001.json", sha256 = "95c0cfef823994d5e8b1edf1a2484f958d36eaca1d34577ec6378b73edbc41ed"), (path = "raw/jobs/getdp-f0007-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0007-b0002-P.tsv", sha256 = "7e3db93e6e731004069d9a6c1a6524289fe6b924ebab0115aba05df38154f6ce"), (path = "raw/jobs/getdp-f0007-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0007-b0002-Z.tsv", sha256 = "f40055d73faf28e512cf13715e8e03533361ee0c1cd710f2dac96d1f18ca6b89"), (path = "raw/jobs/getdp-f0007-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0007-b0002-timing.tsv", sha256 = "899406ac6a6adc605a7153cc734fdcc98bfe9c5c2d34b033da25252cf57ca027"), (path = "raw/jobs/getdp-f0007-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0007-b0002.json", sha256 = "88d3ad72026026b21dc650afd33995d2677a997506508ae8ecc66a09e34da641"), (path = "raw/jobs/getdp-f0007-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0007-b0003-P.tsv", sha256 = "c5ea44f265565130c3d12c198b2b8a8699d983889e3a6b95afba3dbffb9bc6ef"), (path = "raw/jobs/getdp-f0007-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0007-b0003-Z.tsv", sha256 = "a697df98dcb85881385c17ed46a6e7ad2a35516c8fe9fa6059dcbf752f6f0aff"), (path = "raw/jobs/getdp-f0007-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0007-b0003-timing.tsv", sha256 = "cc042d0730a1ca8702092704495c615b8bf94153e909d74ae1827f741ccd6c6f"), (path = "raw/jobs/getdp-f0007-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0007-b0003.json", sha256 = "d0ea75e7d550fa6e17f8d15e01c7e8d13c0c988e6db7b326de19be4f652f1e49"), (path = "raw/jobs/getdp-f0008-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0008-b0001-P.tsv", sha256 = "c56d8b5c9e45b5e774e2f4745c7f5e46b1739ad02ded9b4ab4b75806144623ff"), (path = "raw/jobs/getdp-f0008-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0008-b0001-Z.tsv", sha256 = "d5c8d42902eb6e828326e1d52e541d31927607be3360db8087af5ee1dd382a54"), (path = "raw/jobs/getdp-f0008-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0008-b0001-timing.tsv", sha256 = "113721716ab550a5288b188f326176926117290c8321e0953dbdf482945b1684"), (path = "raw/jobs/getdp-f0008-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0008-b0001.json", sha256 = "636705458f03980092455b1e282293be48c22c3ae85122a0f0c663eb7c1f626a"), (path = "raw/jobs/getdp-f0008-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0008-b0002-P.tsv", sha256 = "344aa533e8e46c9a85a5916ffb56f37a269dc67882102d52490cf76cfba8a2bc"), (path = "raw/jobs/getdp-f0008-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0008-b0002-Z.tsv", sha256 = "d0208d54fda3554aa0ce358a0cef8f045f5294db8c096b7fe5fbd56bf83f6454"), (path = "raw/jobs/getdp-f0008-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0008-b0002-timing.tsv", sha256 = "9ffb5b15ef7c464b3719de0f34b6e4ae3c73ea5eea8e3369d2d0538697702a2d"), (path = "raw/jobs/getdp-f0008-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0008-b0002.json", sha256 = "e96c92e605431d9f24699b87a246469389a74f40b6a281eb1c818cd8c1f14e34"), (path = "raw/jobs/getdp-f0008-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0008-b0003-P.tsv", sha256 = "59b6bb9720ab227ab3d2d09906c21dd6c34e5a333e4f09edefe6d3d820209c3a"), (path = "raw/jobs/getdp-f0008-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0008-b0003-Z.tsv", sha256 = "62d43155696ae2feb0e2b0bfe6190a6da458137a597810a3c0bfb6b4a61a0c7d"), (path = "raw/jobs/getdp-f0008-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0008-b0003-timing.tsv", sha256 = "c2043f0b3bd54de33add441283f0c17ad4e4879a907aad34feef7bc9307affa9"), (path = "raw/jobs/getdp-f0008-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0008-b0003.json", sha256 = "b35ddbcfac080dc2a539cb49460a57ef7b268c2b87ef58a18e2dd5bc211ae4e7"), (path = "raw/jobs/getdp-f0009-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0009-b0001-P.tsv", sha256 = "84dcbcce7647f22987130619c1e2fb73b76e0501359492b79b1e313a21cfbf38"), (path = "raw/jobs/getdp-f0009-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0009-b0001-Z.tsv", sha256 = "c569f16e96adc30dbc9ad683e8312fcdfc07f8c7ba11fa3a2aa485952c205f07"), (path = "raw/jobs/getdp-f0009-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0009-b0001-timing.tsv", sha256 = "ff524787b38a6bc2db153e66fc91cb14edf04ca974ff7a507b5df955d9f91f31"), (path = "raw/jobs/getdp-f0009-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0009-b0001.json", sha256 = "904db2b53163a8dc55584458a74769187efae6bc9e04299e250b841fbafaf469"), (path = "raw/jobs/getdp-f0009-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0009-b0002-P.tsv", sha256 = "127a7c2b65f83575a74b5528c95fc23ce7a55636b743a3325a122ad3392da8bb"), (path = "raw/jobs/getdp-f0009-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0009-b0002-Z.tsv", sha256 = "13a2c974c7616931b0317730c32260e188603249ebf4898e3bcec1917cc08765"), (path = "raw/jobs/getdp-f0009-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0009-b0002-timing.tsv", sha256 = "5e5e4b27f6e9dd766c91a45c7aa5411fd4d7f1ea58b38c00a45a4aa0e3945726"), (path = "raw/jobs/getdp-f0009-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0009-b0002.json", sha256 = "9771fb54906a0c3857eecff9b2f288998ae8abc9ad9d8f91d5bd546f5be8fe17"), (path = "raw/jobs/getdp-f0009-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0009-b0003-P.tsv", sha256 = "9a54f72af8f974313fe3a96ffbb33e97f9a58404c07a375a366db862653a9dba"), (path = "raw/jobs/getdp-f0009-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0009-b0003-Z.tsv", sha256 = "afafa17b6905e9c8497efe5775c4db109951650d8bf6949000eda092f771a03b"), (path = "raw/jobs/getdp-f0009-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0009-b0003-timing.tsv", sha256 = "8b05303ac1ca02207487c1ef70cdb4839ef26bc1ad314c7b9c665edfb4fd063b"), (path = "raw/jobs/getdp-f0009-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH/raw/jobs/getdp-f0009-b0003.json", sha256 = "a6943a782e8c0671a210e3f3be9793a97cfd033b09429fe90641cfb2ea8d7e98")], coordinates = ["cable:1:core", "cable:2:core", "cable:3:core"], formulations = @NamedTuple{schema_version::Int64, selections::@NamedTuple{insulation_admittance::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}, semicon_admittance::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}, earth_properties::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}, temperature_dependence::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}}, assumptions::@NamedTuple{impedance::String, admittance::String, earth::String, propagation::String, semicon_domain::String, enclosure::String}, backend::Symbol, requested::NamedTuple, methods::NamedTuple, options::NamedTuple}((4, @NamedTuple{insulation_admittance::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}, semicon_admittance::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}, earth_properties::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}, temperature_dependence::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}}((@NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}((:lossless, NamedTuple(), NamedTuple())), @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}((:lossless, NamedTuple(), NamedTuple())), @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}((:constant, NamedTuple(), NamedTuple())), @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}((:linear, NamedTuple(), NamedTuple())))), (impedance = "Axial current-driven A_z/u_r finite-element equations", admittance = "Scalar electrodynamic Helmholtz equation in surrounding media; equipotential terminals with unit transverse-current excitation; Y = inv(P)", earth = "Horizontal air and one semi-infinite soil; soil constitutive properties evaluated at each frequency", propagation = "Gamma = 0; medium diffusion and displacement retained; independent Z/P blocks in one factorization", semicon_domain = "Passive material region, without electrical terminal ownership", enclosure = "Supported enclosures are represented by their material and terminal domains"), :fem, (insulation_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), semicon_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_properties = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), temperature_dependence = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing)), (insulation_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), semicon_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), earth_properties = (identifier = :constant, parameters = NamedTuple(), options = NamedTuple()), temperature_dependence = (identifier = :linear, parameters = NamedTuple(), options = NamedTuple())), (reduce_bundle = false, kron_reduction = false, ideal_transposition = false, physics = Symbol("quasi-tem")))), fem = (timing = (assembly_seconds = 12.021270751953125, recovered_columns = 0, scope = "accumulated native worker wall time; not elapsed scan time", columns = 27, worker_wall_seconds = 33.440127606000004, factorized_columns = 9, constraint_seconds = 4.28119969367981, solve_seconds = 12.49652647972107, output_seconds = 0.007587432861328125, worker_wall_scope = "sum of newly executed process wall durations in this invocation", backend = "getdp", schema = 1, reused = false), run = (state = LineCableModelsGmshExt.completed, run_directory = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-ZOwVWH", mesh_source = :generated, mesh_fingerprint = "d7fde94540b72427e86e702a44421bfe2fc6b740ad27ded21e65dc9e89989ee0", getdp_invocations = 9, map_paths = String[], completed_columns = 27, completed_frequencies = 9), inputs = (schema_version = 7, solver_protocol = 3, mesh_fingerprint = "bb7af8bf02dd71d7b9ea5c278b5a3093998c2c857011dc103128309c04fb9bbe", materials = [(kind = :conductor, tag = 10001, mu_r = 0.999994, sigma = [5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7], omega_epsilon = [5.563250277239352e-12, 5.563250277239352e-11, 5.563250277239352e-10, 5.563250277239352e-9, 5.5632502772393515e-8, 5.563250277239353e-7, 5.563250277239352e-6, 5.563250277239352e-5, 0.0005563250277239352])], earth_materials = [(rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0)], air = (eps_r = 1.0, mu_r = 1.0), mesh_plans = LineCableModelsGmshExt.FEMMeshPlan{Float64}[LineCableModelsGmshExt.FEMMeshPlan{Float64}(1, 0.1, 1006.5842420897408, 1258.230302612176, 25.16460605224352, 50.32921210448704, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (25.16460605224352, 25.16460605224352), (1006.5842420897408, 1006.5842420897408)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(2, 1.0, 318.3098861837907, 397.88735772973837, 7.957747154594768, 15.915494309189535, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (7.957747154594768, 7.957747154594768), (318.3098861837907, 318.3098861837907)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(3, 10.0, 100.65842420897408, 125.82303026121761, 2.516460605224352, 5.032921210448704, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (2.516460605224352, 2.516460605224352), (100.65842420897408, 100.65842420897408)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(4, 100.0, 31.83098861837907, 39.78873577297384, 0.7957747154594768, 1.5915494309189535, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (0.7957747154594768, 0.7957747154594768), (31.83098861837907, 31.83098861837907)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(5, 1000.0, 10.06584242089741, 12.582303026121762, 0.282842712474619, 0.565685424949238, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (0.282842712474619, 0.282842712474619), (11.313708498984761, 11.313708498984761)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(6, 10000.0, 5.656854249492381, 7.0710678118654755, 0.282842712474619, 0.565685424949238, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (0.282842712474619, 0.14067442439954772), (11.313708498984761, 9.549296851139358)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(7, 100000.0, 5.656854249492381, 7.0710678118654755, 0.282842712474619, 0.565685424949238, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (0.282842712474619, 0.04448515896357012), (11.313708498984761, 3.0197535662513486)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(8, 1.0e6, 5.656854249492381, 7.0710678118654755, 0.282842712474619, 0.565685424949238, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (0.282842712474619, 0.014067442439845935), (11.313708498984761, 0.9549323148114102)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(9, 1.0e7, 5.656854249492381, 7.0710678118654755, 0.282842712474619, 0.565685424949238, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (0.282842712474619, 0.004448515892915345), (11.313708498984761, 0.3019836725638405))], region_mesh_sizes = [0.0085, 0.0085, 0.0085], cable_outer_mesh_sizes = [0.0085, 0.0085, 0.0085], mesh_growth_factor = 1.2, options = (reduce_bundle = false, kron_reduction = false, ideal_transposition = false, physics = Symbol("quasi-tem")), execution = (ui = false, plot_field_maps = false, mesh_policy = :remesh, mesh_path = nothing, domain_skin_depths = 2.0, keep_run_directory = true, getdp_executable = nothing, gmsh_verbosity = 2, getdp_verbosity = 4, frequency_workers = 2, solver_threads = 4), supplied_mesh = nothing, owned_gmsh = true, getdp_identity = (sha256 = "49d44b1dfe13d9f9b63f8dfe37914af8947f856feebac13fbb6989a41aa88e51", info = "Version : 3.5.0\nLicense : GNU General Public License\nBuild OS : Linux64\nBuild date : 20220513\nBuild host : getdp.info\nBuild options : 64Bit Arpack[contrib] Blas[openblas] Gmsh Kernel Lapack[openblas] PETSc PeWe SLEPc\nPETSc version : 3.14.4 (complex arithmetic)\nGmsh lib version : 4.9.5-git-e286855ce-git-e286855ce (20220218)\nGmsh lib options : 64Bit ANN[contrib] Dlopen Eigen[contrib] LinuxJoystick Parser Plugins Post\nPackaged by : geuzaine\nWeb site : http://getdp.info\nIssue tracker : https://gitlab.onelab.info/getdp/getdp/issues\n"), getdp_provenance = (path = "/home/amartins/.julia/artifacts/51e049a32eeb2ffebcc8a5945bb00f70470a3d29/getdp-3.5.0-Linux64/bin/getdp", source = :artifact, artifact_hash = "51e049a32eeb2ffebcc8a5945bb00f70470a3d29"), gmsh_version = "4.15.0", gmsh_library = "/home/amartins/.julia/artifacts/13aa9c68927e7d05cb18794fbcfe42323c9c0fda/lib/libgmsh.so", julia_version = "1.12.7", adapter_sources = (var"model.jl" = "5dc038f3abd29cd606ab56ecded593b653d3c41cdbfc29e580c189ceeb008584", var"geometry.jl" = "6c6b4236d78945b54c72e7fd34d12a32ccadad75412bbc22ab70ff21df9822f1", var"mesh.jl" = "134502f8ca5ea3243151549b04780259b627adec1682cbfe13b209f9a6993de3", var"onelab.jl" = "baa1af49661cf66d88935d8fb08d7b2a35c7693bebb1fdf540f1463c91dc8164", var"getdp.jl" = "b6a8d3813ce7a200b01cc77428b20dcbb2728f9a4760a1cbde84ef9a698ddd6b", var"voltage_paths.jl" = "81bb6ad0c4078af4f5cce6dcae2bafb6ed60a8374345dfae0b8f61369d61e786", var"workers.jl" = "62ffec093cda6f52d4f08206771dcb82e16e80175ebac6200196c9ed6ffd34ec", var"results.jl" = "bb48b425e16a9048bdf0a4f73c6d7345554d22e396f8d371aa926d4c665a3e4f", var"compute.jl" = "152e200c45a72dab0c2a439900d98a1ec18a6377f2b640ee271850f1ac74b702"), solver_sources = (model = "c0bb424db5ab4db9ca22f549f675edb4646f69910d2aa14e20cf4f40e41bd313", jacobian = "014208d39043ab13183c42c312aba459265715da6d6ddc38ec1fbc39b6090c73", integration = "e113951ac1e6e658f1be50e53875e30c85497445f0e6584a8a7ab1848588e902", materials = "581f089113810a379d8a49c03d79f55045b723c8f4c761d0a493a019ff1f5d9e", quasi_tem = "448d9019ad417c9a784da20170b855dd25e559297499a83d19b007ee095c084d", quasi_full = "1aede35114ea686c5f109089478027d241dec12dc9fd14a24452b348f89b08e4")), terminal_ids = ["cable_0001/three_bare_wires_air_3/core", "cable_0002/three_bare_wires_air_3/core", "cable_0003/three_bare_wires_air_3/core"], reduced_phase_map = [1, 2, 3], inversion_residuals = [6.661363048501331e-16, 4.51139127782893e-16, 2.3263411494723067e-16, 2.225035121631394e-16, 4.449955543025987e-16, 2.221859231748073e-16, 2.225140393844813e-16, 2.2295582508993733e-16, 2.2204460492768967e-16], condition_numbers = [2.118245070436865e12, 2.1180652128414883e11, 2.118279854591803e10, 2.1200855600736995e9, 2.1369918684198987e8, 2.258707684333542e7, 2.956618321545104e6, 497557.7319420023, 1.109144940803826e6], primitive = (Z_primitive = ComplexF64[3.156938445459299e-6 + 1.2376736789074544e-6im 9.88895929580015e-8 + 8.122020135674348e-7im 9.855594297417768e-8 + 6.824025992417333e-7im; 9.888959295800155e-8 + 8.122020135674348e-7im 3.1569387144350372e-6 + 1.2377673118767072e-6im 9.85591525649431e-8 + 7.118381485408197e-7im; 9.855594297417764e-8 + 6.82402599241733e-7im 9.855915256494297e-8 + 7.118381485408195e-7im 3.156279957590097e-6 + 1.2381889884782712e-6im;;; 4.06449427838295e-6 + 1.093081371574797e-5im 9.956569754692808e-7 + 6.676675294124121e-6im 9.850336283317956e-7 + 5.386107059948079e-6im; 9.956569754692812e-7 + 6.676675294124121e-6im 4.0645127496387215e-6 + 1.0931687928980027e-5im 9.853067199411701e-7 + 5.6804526678089855e-6im; 9.850336283317954e-7 + 5.3861070599480775e-6im 9.853067199411705e-7 + 5.680452667808985e-6im 4.04392492524279e-6 + 1.0949807691859883e-5im;;; 1.4077229758399098e-5 + 9.428488438471335e-5im 1.015138241654919e-5 + 5.2159887190133634e-5im 9.816340271120624e-6 + 3.948413685612497e-5im; 1.0151382416549192e-5 + 5.2159887190133594e-5im 1.4078088050992e-5 + 9.43045827861135e-5im 9.833029733245704e-6 + 4.2420226349152085e-5im; 9.816340271120624e-6 + 3.948413685612497e-5im 9.8330297332457e-6 + 4.242022634915208e-5im 1.345650685647301e-5 + 9.493684936851367e-5im;;; 0.00011825531094274957 + 0.0007738877638691011im 0.00010608020461151831 + 0.000370931966725289im 9.594319192462945e-5 + 0.0002515309512353797im; 0.00010608020461151828 + 0.00037093196672528887im 0.0001182640892721979 + 0.0007739368043661452im 9.683027888126233e-5 + 0.00028060784803067107im; 9.594319192462948e-5 + 0.00025153095123537966im 9.68302788812624e-5 + 0.000280607848030671im 0.00010105583228587755 + 0.0007940472344123282im;;; 0.001184365255805897 + 0.006031760366737753im 0.001100804015769275 + 0.002095226125213293im 0.0008333631201224341 + 0.001133947339960391im; 0.0011008040157692751 + 0.0020952261252132924im 0.0011843954705209859 + 0.006032178083314971im 0.0008792393876089305 + 0.0014023395859519923im; 0.0008333631201224342 + 0.0011339473399603912im 0.000879239387608931 + 0.0014023395859519927im 0.0007853813972481207 + 0.006616143258640918im;;; 0.011118858602888656 + 0.04273147197215032im 0.008028542898194605 + 0.004873381902571392im 0.00404966983853508 + 0.0011474855613075126im; 0.008028542898194598 + 0.004873381902571388im 0.011119866253779278 + 0.04273151207866525im 0.00525604642420985 + 0.0024795778081912593im; 0.004049669838535079 + 0.0011474855613075117im 0.0052560464242098525 + 0.0024795778081912593im 0.005109466121945042 + 0.05650752523017786im;;; 0.09149276545797437 + 0.2836856195840511im 0.008732833457576 - 0.008720356263351766im 0.0008720060738900981 - 0.0031043854982987897im; 0.008732833457576005 - 0.008720356263351771im 0.0914893174432287 + 0.2836615002411198im 0.0029229592659658302 - 0.005535513096531494im; 0.0008720060738900996 - 0.003104385498298791im 0.002922959265965831 - 0.005535513096531495im 0.024751108383294702 + 0.5118648867093322im;;; 0.753710537597518 + 1.5682785800867196im 0.00015809876830168704 + 0.001064782467487955im 8.18649572391623e-5 + 8.96758975728876e-5im; 0.0001580987683016873 + 0.0010647824674879566im 0.753724628134118 + 1.5683153568234434im 0.00018019558652217053 + 0.00022583671129566818im; 8.186495723916231e-5 + 8.967589757288757e-5im 0.00018019558652217056 + 0.00022583671129566807im 0.09317622230865559 + 4.9164327265927605im;;; 4.549904563395861 + 6.660736669510468im 2.1359267021804777e-8 + 2.994048325653302e-8im 1.6416933423796947e-9 - 3.021806235971246e-10im; 2.1359267021804784e-8 + 2.994048325653301e-8im 4.550147357458101 + 6.660502880559969im 4.927583660618426e-9 - 7.768817555422617e-10im; 1.6416933423796937e-9 - 3.021806235971253e-10im 4.927583660618422e-9 - 7.76881755542261e-10im 0.28635320029725086 + 47.884683853845im], P_primitive = ComplexF64[0.2288381195325415 - 0.0249828245564933im 0.1771443067761026 - 0.02498243613250194im 0.15701725564835925 - 0.024919480316687274im; 0.1771443067761026 - 0.024982436132501912im 0.22883675936910744 - 0.024982831630982288im 0.16444598819102652 - 0.024920439125649056im; 0.1570172556483592 - 0.024919480316687233im 0.16444598819102646 - 0.024920439125649im 0.16803350368884368 - 1.0949839693397025e11im;;; 0.19240523954088187 - 0.024977023568096262im 0.14071240874088004 - 0.024973777239175717im 0.1207254090407251 - 0.024775027819954668im; 0.14071240874088006 - 0.024973777239175723im 0.19240407986555758 - 0.02497695249104542im 0.12815280262860004 - 0.024782423342533774im; 0.12072540904072498 - 0.02477502781995464im 0.12815280262859993 - 0.02478242334253374im 0.13186533227269087 - 1.0948774226985147e10im;;; 0.15597541236652313 - 0.02493215761279159im 0.10429422017441287 - 0.024907140288884144im 0.0847368267870751 - 0.024287167055710993im; 0.10429422017441294 - 0.024907140288884144im 0.15599082654896335 - 0.024932155807702056im 0.09215341573885288 - 0.02434045515359149im; 0.0847368267870752 - 0.02428716705571099im 0.09215341573885297 - 0.02434045515359151im 0.09628777052618787 - 1.094915715479564e9im;;; 0.11959939590893748 - 0.024619150399709154im 0.06795120260529673 - 0.024439965353201695im 0.04977632198896537 - 0.02260123566434426im; 0.06795120260529677 - 0.024439965353201705im 0.11959575895740338 - 0.02461922933464422im 0.05707823149305443 - 0.022930932758101406im; 0.04977632198896539 - 0.022601235664344264im 0.05707823149305446 - 0.02293093275810142im 0.06237730811786392 - 1.0949539717355289e8im;;; 0.08392802971210292 - 0.022795705377636484im 0.03268073478470456 - 0.02170635852065592im 0.018729912293457596 - 0.01720550785300464im; 0.03268073478470459 - 0.021706358520655927im 0.08393008593348898 - 0.02279652963716449im 0.025142230064370306 - 0.018690067128185005im; 0.018729912293457596 - 0.01720550785300464im 0.025142230064370302 - 0.01869006712818499im 0.0330252059629319 - 1.0954200785883825e7im;;; 0.05363866178099689 - 0.01636362270359184im 0.005358943161716706 - 0.01175856708504279im 0.0006990827384161557 - 0.006127064323831359im; 0.005358943161716709 - 0.011758567085042802im 0.053637936199075656 - 0.01636389215720884im 0.0031271064115693217 - 0.009029160572452937im; 0.0006990827384161559 - 0.006127064323831358im 0.0031271064115693226 - 0.009029160572452935im 0.013010347216029217 - 1.095439224028683e6im;;; 0.0359175481557119 - 0.011515690178551374im -0.0010780087766796278 - 0.001084123534775724im -0.0003607237503016814 - 7.679529552702153e-5im; -0.001078008776679628 - 0.0010841235347757233im 0.035914613519691434 - 0.011515364756701733im -0.0008022247246510795 - 0.00033357616755566187im; -0.00036072375030168143 - 7.679529552702149e-5im -0.0008022247246510795 - 0.00033357616755566187im 0.004099932135426627 - 109532.32860820658im;;; 0.019862431982314705 - 0.009545546946847077im 1.3482907472136551e-5 - 2.0017979898985396e-6im 1.1047781224996322e-6 - 9.507144648661198e-7im; 1.3482907472136553e-5 - 2.0017979898985413e-6im 0.019862897720080312 - 0.009545726386842519im 2.8126544289813593e-6 - 2.3651640746534306e-6im; 1.104778122499633e-6 - 9.507144648661211e-7im 2.81265442898136e-6 - 2.3651640746534314e-6im 0.0012746997432706594 - 10958.373126494971im;;; 0.00843560089641984 - 0.005762920556206585im 3.7918562660632036e-11 - 2.70539353528268e-11im 3.116798224212565e-11 + 2.42962704499157e-10im; 3.791856266063205e-11 - 2.705393535282677e-11im 0.008435304833718153 - 0.005763228745250534im 4.004371052648083e-11 + 2.663833262383991e-10im; 3.116798224212674e-11 + 2.429627044991517e-10im 4.004371052648202e-11 + 2.663833262384007e-10im 2.132665803626758 + 11331.161159562569im], phase_map = [1, 2, 3]))) \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/fem/fem-artifacts.toml b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/fem/fem-artifacts.toml new file mode 100644 index 000000000..1d9d4e193 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/fem/fem-artifacts.toml @@ -0,0 +1,296 @@ +"attempts/f0001-rejLad/attempt.json" = "8562a2ee7ef32051107bb4bafb70a43ea79fc50e7464792a78d93b1b87645ba6" +"attempts/f0001-rejLad/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0001-rejLad/getdp.log" = "657449d6a796cfa72e29bb53dc47a6fc681420936cd4e323b8bb1a6d7e10b29c" +"attempts/f0001-rejLad/petsc.log" = "0880109eaf22a8978ac07daf855e108e33b63e62e7df229df8d32559a43b8e69" +"attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0001-P.tsv" = "46fa546412f46ef46b2fb434f7008e166c57e8a707ee228c7505b3d7b74b69a1" +"attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0001-Z.tsv" = "d0b571a4b155655571ecfc86ddfb815b07e9f85eb602f48868f7293cf7a09647" +"attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0001-timing.tsv" = "b394dfc548f5fadb7cad04fa6a8d85adb2d610b12c98e2aac61edb3fbcdaca80" +"attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0001.done" = "a40a11bab26b32dfb169550347f289dc710df24f59fe3ef20963d46f838f0586" +"attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0002-P.tsv" = "8a559a373497f763d32a6095a7d9b497549217d33cf1719267290ae2c929dd07" +"attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0002-Z.tsv" = "b4cd701a5b69fb49d365066b2927e54c950dc5d06a66f1a0df3339679ffd3a12" +"attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0002-timing.tsv" = "7d414b81550475e600571ef870b8d9dd35888eba9dd214939853bbb35405cc99" +"attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0002.done" = "f58042f034284ccf2955f2795037937a5bd0e4288ffc0d0d5124e33ee07f17ec" +"attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0003-P.tsv" = "2e1b353722f20454ee70cef7fb46c153313f12094a7a8b0b988362c2913c6df9" +"attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0003-Z.tsv" = "4352656d9afe9722eabfa397f9f0d6437a4ba11c5d9e993b6fc7f1f1bd7cba7a" +"attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0003-timing.tsv" = "8bac2c85564c9dabe6a8a7d99cd43e117120c90f2453e43fc67f95f041061d75" +"attempts/f0001-rejLad/raw/jobs/getdp-f0001-b0003.done" = "41695cf0039479bd9b703276e3c15366a77a1e43f8490b48634d8a1c2a61be94" +"attempts/f0001-rejLad/solver.pre" = "9b308817170e306be5b11166886bf25307f5c2440ecfd21256be6fe4a2e0e8cb" +"attempts/f0002-u9t1S3/attempt.json" = "ab84882854d5d8e8da09d15003ee6341cfb0a6a3b94f49ac8dde3596cef2ea55" +"attempts/f0002-u9t1S3/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0002-u9t1S3/getdp.log" = "a2d9192db61f7a2564e5bd2ab5fa542ab96bfbdc52e8c2111cfbdfd2bdedbe0c" +"attempts/f0002-u9t1S3/petsc.log" = "b322b0e58b01a0f72ab978a536d971ba13612c04f080f8f8e56c8fcf239bed3c" +"attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0001-P.tsv" = "0f4072dfbd4ec940e6f063a1c8f953cd1ecc302f0ff0105bc650a2279ba9e3f8" +"attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0001-Z.tsv" = "2d42112d21371805fdca88142eb211b62824faae1f28012e57c0c159b08ab6ee" +"attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0001-timing.tsv" = "4de08b845373f35cd877dbf9768cce9e45195a4c25c6f18631d6c7623ba29f5c" +"attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0001.done" = "3dbd9539f415ee889a34b9c6efe6b9cb8168b7c0945d52d7e63e920c8990d2d1" +"attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0002-P.tsv" = "12ea30983f26ced34ad2cf546ddb66465e38832e80abe29f53f6d35c31e7add7" +"attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0002-Z.tsv" = "ec3fca324f4aab7629a78d0dab90eb8c9b142a3299f96ffd6d7a942eac2e8c42" +"attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0002-timing.tsv" = "6efc242b70bfbde32425ab607e68b509c0f1e3c34eb6fdf743ede33a1e72e93e" +"attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0002.done" = "d498381e814531161686031fcde5aa549b456c6cbee61f6eb67916650247e717" +"attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0003-P.tsv" = "b29350aa094132c0cb923ddd344529b8a145dfcffabfcdacc77886ea71e44f94" +"attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0003-Z.tsv" = "4634c1eedd44f096ba8b45639a54915e2677ee47d8b32698dfbea647d5d228e5" +"attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0003-timing.tsv" = "733a6b26b8e195789e81df0ac97dfd4e48c6971a4fdb1545b1c760d2c080b3e3" +"attempts/f0002-u9t1S3/raw/jobs/getdp-f0002-b0003.done" = "fa5c0c361441906d1aaa6b254ae52969806be93c36846f4c70c70a18dc833050" +"attempts/f0002-u9t1S3/solver.pre" = "a9cb42910aa58597cce8145df1038bdb6383aed5f35eadf5ffa5ca296d1587d2" +"attempts/f0003-Bw5FOk/attempt.json" = "e72cf67d138b6a70d0828e70383354e8df9856d54473de3898637a84bedc90f7" +"attempts/f0003-Bw5FOk/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0003-Bw5FOk/getdp.log" = "bea36142bb54aa0f1c763f33f63efd7d8bbdfb6a981762c909549542257c6fb2" +"attempts/f0003-Bw5FOk/petsc.log" = "8621f1245011d0c84f5b57c4ab0bc5309879c8a1f4d321bf5d3a39d12cb2959b" +"attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0001-P.tsv" = "0b1ea83c170a28e72f9251fb8a8637f72a704856561fd966c0869a1a5893e082" +"attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0001-Z.tsv" = "03865b1fe279f225b912f741b6ba8efc40b88adcd335b8def4efa6b2ad1ca52a" +"attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0001-timing.tsv" = "423ab5efb1eaadeba6420651381573e3428bae3d8c77b3b2ae7edc758768d7bb" +"attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0001.done" = "02395cb824ac1c8ce1d2e8cd3c57fd19d5c904215aef49b1fba677c51e99d519" +"attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0002-P.tsv" = "93d32ccea24bc99036d1291a31b68b68a0312515a1b10e590982cb94ab8cefbf" +"attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0002-Z.tsv" = "e49b22f5faeac4fc17d06a7a0f0568bbadda235a1431126db6c0268bbbcda12d" +"attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0002-timing.tsv" = "87b2df7edd6d066c5918d51d1b03bf4f3420ada46f96b977734d523d120afcf9" +"attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0002.done" = "84034583a44a97df0ec5820a76fc891b95a9b6bc0bf693bed9882420a3eb5b05" +"attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0003-P.tsv" = "24c8257bb4b0c512b85b43e1db9d18158684f268f276cc77192791029497ff51" +"attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0003-Z.tsv" = "73291fd6c57b0b709db8b8e1034f28adb0f9ca38a0ba5b720d526c0931ad5b7f" +"attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0003-timing.tsv" = "176375f083f0c5b3f87286b5363ef90dc10709a1dfa714ea30735a406e488f0e" +"attempts/f0003-Bw5FOk/raw/jobs/getdp-f0003-b0003.done" = "aec9dd1bab4102d3c07f9c6abf83e4d604721961d8e37cbbc9620f7f71cafcae" +"attempts/f0003-Bw5FOk/solver.pre" = "b9b0dcd1101458efc97451050d7555b0dc921e075812327f2562fb8087cd6df8" +"attempts/f0004-gQEWGq/attempt.json" = "ce423681f144a989fb08ff86d9d18b8165bedf8ae015702a5e17b31980e6a4f1" +"attempts/f0004-gQEWGq/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0004-gQEWGq/getdp.log" = "dcbd126b7d74df5773e6a8a3eab718375be886a1bf43cae55490daaaafd150a4" +"attempts/f0004-gQEWGq/petsc.log" = "cfb1c1836e72a8e259e0d66335bfe469c68731c377abaeea00907d6e515c6c66" +"attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0001-P.tsv" = "0414f7d9279ba3fc48565c59dfce50ff1a6ac9c5a9cadede2426432da9316f4e" +"attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0001-Z.tsv" = "037c545ac4f3ac6a1608b35709600ccd4616928d836607f5a52c89ff361d03ed" +"attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0001-timing.tsv" = "7f63264a16c152a4f0b86e1d43fd7f4eed1f18785cf2d7d5fd51a2c04d5c8154" +"attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0001.done" = "05b52afc2d2a0ffefd0bb6e40585165d1b2572547d59d3c360b4163202709006" +"attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0002-P.tsv" = "3afc45db4632952d5724691256d58678bcd2eb881e27e70142173c723b6dabae" +"attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0002-Z.tsv" = "967fe79df063cebf45dbeb09e712f518afb20029986bc826ee2818437af3731b" +"attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0002-timing.tsv" = "5e44fba2b54e0ef8dd365c0b2a16dd1c5b5b52bf94086a2a5821aed7e411e616" +"attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0002.done" = "35d216e20aed80a36327575b350d8e4c12b70f2038f5d07d55614c92afdd6b6b" +"attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0003-P.tsv" = "aa81bae32c350b92b595eabe7e3ba951a469af09a3b429afd7ec99d91078a857" +"attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0003-Z.tsv" = "a2d6f4c7ae9ac114eae3cc56462cf2e1bb40e5b4de651496ac60726a61824058" +"attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0003-timing.tsv" = "1ee834f682654b899d943de24e87147c2621eaba998f8b2d6c7c89f8963723a5" +"attempts/f0004-gQEWGq/raw/jobs/getdp-f0004-b0003.done" = "eae2a23f21e24dd158a817c096e6e77f32e1d2a850c4163d279c3531eef2af5a" +"attempts/f0004-gQEWGq/solver.pre" = "c3a2b0d2ab4f747f6c87e9d10c5261aa0d78551e53c7f8b3e6c707678423a1c6" +"attempts/f0005-tKhfHk/attempt.json" = "9cf2d2dfd6b77b105cebebfd3a19ecf0c6f1802b30109a55ed3d6bbdbcc90c0a" +"attempts/f0005-tKhfHk/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0005-tKhfHk/getdp.log" = "4f365fe953b9210f118cb0374bda120041d54424fd16b6d3c761003d1e685a6d" +"attempts/f0005-tKhfHk/petsc.log" = "9fa42b00ddf30ecfcb37c665668b18455b2bcdbaf048838381a2feebfe1a0399" +"attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0001-P.tsv" = "4fd374e319aa02cd4a461d06a94a2b964d070dfaa6672007ffe0dd38761e0c9b" +"attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0001-Z.tsv" = "fab5e56f6c89d4c928ecd9ace48cdc4cfba8476d91d72299c823cc80afb8160d" +"attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0001-timing.tsv" = "53574e2b5e1083732f43d22985fe6095f459d67fd3dc411bf8dbb873b397967d" +"attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0001.done" = "ee7bf9802d600055d484ec4e501666418037b219c7bf910eefda35e16a843b08" +"attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0002-P.tsv" = "7424966f1503b3e63246c9dcf726fa0954ecd7968cd2ea40a39811bd44812764" +"attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0002-Z.tsv" = "13f053e4852ab5b18315cc0d54edc75d52bc8a3e0ecf5af5933d217c5ca270d9" +"attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0002-timing.tsv" = "6d3a950a55d9c9e8434c0d8132ca99b73fda65a5b4b0cd3966f5f3c036be4567" +"attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0002.done" = "e26c3631aaa36c47efb3ede81242002dbebf27057f428fe57f60849cede6b3f3" +"attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0003-P.tsv" = "a7009ee10a8bf5f8c516c5ebb74715bd10e620e7e08459d5aff103c1ffe692e8" +"attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0003-Z.tsv" = "e6edda7298951e5ff0eb0f3d453b23678cf248e4072abede586c0e0ab4292a44" +"attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0003-timing.tsv" = "7add1c8273c631dc052688e7e4a99216fca5b5579b2aa3266953f773a181ba68" +"attempts/f0005-tKhfHk/raw/jobs/getdp-f0005-b0003.done" = "a91d7ca1bb120ef43c71df371c45a71b1cc7744f1ee2eb082338700de7f329c9" +"attempts/f0005-tKhfHk/solver.pre" = "f365d54cd23d09ce06cfef0173cc1fdf77ff32c007bf6083b3c91f09c19e7e39" +"attempts/f0006-iHjV2j/attempt.json" = "e5381b46788c5107cda6872e2cc33527a7d232d5036ba379528ac9cafd3fdc3c" +"attempts/f0006-iHjV2j/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0006-iHjV2j/getdp.log" = "c58db5232513e5377d942053ef7689788460431b84cf33ccae2dcc68167f77f7" +"attempts/f0006-iHjV2j/petsc.log" = "5658b011e07d3553bc66292595bd603b84a7a7c488bfbb72c1e2791eecc68be8" +"attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0001-P.tsv" = "ab503fe79c9dda6c9ae0c08c9330cdcf21a85965792c0355a3901e5839cb5853" +"attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0001-Z.tsv" = "4842e5855355757b6dc19a967a4a92014f541c22a5a48e700d02dc7231e3ec4e" +"attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0001-timing.tsv" = "7a39d6225512aeac0c8379081c92bc76df854c5c7db4cbf3dae125b580616236" +"attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0001.done" = "e41a78288341a2ae156cd28b6102b3a31a74b012eb4deb233e88fae392912e8e" +"attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0002-P.tsv" = "0c47b4d70c891a27b8d4c928b85461b116329f11a83ab2b3312ef76837dc8730" +"attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0002-Z.tsv" = "53f38964d8b833d664ffcfed54d520b10cd1f97d5005888aa15e8483e683d1df" +"attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0002-timing.tsv" = "994b3247fe03648d4c1582a28104f9dd758d559c518a4120a1928f0251a9bd4f" +"attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0002.done" = "c105367e4bac19858725b1b7c56efbb25ae370173d002f9a678a4762a5c69ac9" +"attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0003-P.tsv" = "cedf5b927eebe2818abd67c2445ce6599c08892e7b8984d278302cbe5cddb03c" +"attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0003-Z.tsv" = "67209302d9a76618a9976956b513903dd42632964eba09bb10367acaf5e3a623" +"attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0003-timing.tsv" = "a418c43a4f4399b78eb81c969c8efb698635a7a9c8c8f00f380196470e977ed6" +"attempts/f0006-iHjV2j/raw/jobs/getdp-f0006-b0003.done" = "de6214d7f2cf6d2097d94d720289a7424da0b9bf7a5df27326d8b69e936798e8" +"attempts/f0006-iHjV2j/solver.pre" = "3eeaeffb9b8b1557836dc8b1acb586c0ff94c96b6225e666de5f80d704090928" +"attempts/f0007-pVC67f/attempt.json" = "deee433e8de398d52deea84e4287e7d086e0ba7c716072c857f408c58dcc958e" +"attempts/f0007-pVC67f/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0007-pVC67f/getdp.log" = "0eaa3065bd21d6e6cd69fef3762da1bb150b636e1696f30ef93abca8d33f8c3c" +"attempts/f0007-pVC67f/petsc.log" = "6c4f1df3c8294ea8eaa03770a97a49eac7880d0222e55f1eed128f313e56afb4" +"attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0001-P.tsv" = "6a953a9b70ca02ac0cd4488e33722d3dd0fc58163b0ebd9c3fa5f572dacf9866" +"attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0001-Z.tsv" = "a6a42398bb170d2d9b55b83b578a8dc3a7cea50f8f21ee566fe488d9f630512b" +"attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0001-timing.tsv" = "ede82f09fc10b7433e42ba302e2cde9791c8997192de4cb2940d8e7a0680ef74" +"attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0001.done" = "d95e4d651bb7fa68e36e0077af5a10e10baacf852173da7ac0f1cca3e414dc7f" +"attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0002-P.tsv" = "7e3db93e6e731004069d9a6c1a6524289fe6b924ebab0115aba05df38154f6ce" +"attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0002-Z.tsv" = "f40055d73faf28e512cf13715e8e03533361ee0c1cd710f2dac96d1f18ca6b89" +"attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0002-timing.tsv" = "899406ac6a6adc605a7153cc734fdcc98bfe9c5c2d34b033da25252cf57ca027" +"attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0002.done" = "a953202777b2aea0282f4e5ecac9feb227f850cb3f80f2247ca07a708f3635be" +"attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0003-P.tsv" = "c5ea44f265565130c3d12c198b2b8a8699d983889e3a6b95afba3dbffb9bc6ef" +"attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0003-Z.tsv" = "a697df98dcb85881385c17ed46a6e7ad2a35516c8fe9fa6059dcbf752f6f0aff" +"attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0003-timing.tsv" = "cc042d0730a1ca8702092704495c615b8bf94153e909d74ae1827f741ccd6c6f" +"attempts/f0007-pVC67f/raw/jobs/getdp-f0007-b0003.done" = "bc6e3e671a966700354c8fb7f9ab0ff830db8a4b88332e0ec4974ce29df37b0c" +"attempts/f0007-pVC67f/solver.pre" = "2521310c3054739963c072a037e3188018853290e8609a458f4de0b8936c72e3" +"attempts/f0008-RVEsvw/attempt.json" = "44801cbd3e273c45337372df0718f495cff40d3f9f222bc52261d1684968ceb8" +"attempts/f0008-RVEsvw/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0008-RVEsvw/getdp.log" = "2118f6f654e30d2a50f79fbbe90babad7c873f22146712b9924cfdf3fa4123f2" +"attempts/f0008-RVEsvw/petsc.log" = "a06887ba0af613c7ee6f693a15f806972ef5e7086f8f13797b0b25817aabe3fa" +"attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0001-P.tsv" = "c56d8b5c9e45b5e774e2f4745c7f5e46b1739ad02ded9b4ab4b75806144623ff" +"attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0001-Z.tsv" = "d5c8d42902eb6e828326e1d52e541d31927607be3360db8087af5ee1dd382a54" +"attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0001-timing.tsv" = "113721716ab550a5288b188f326176926117290c8321e0953dbdf482945b1684" +"attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0001.done" = "c03ca6db0131d3218478fb4e85f0350dca26787b9ffdfc9d50b5d68ad93b352c" +"attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0002-P.tsv" = "344aa533e8e46c9a85a5916ffb56f37a269dc67882102d52490cf76cfba8a2bc" +"attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0002-Z.tsv" = "d0208d54fda3554aa0ce358a0cef8f045f5294db8c096b7fe5fbd56bf83f6454" +"attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0002-timing.tsv" = "9ffb5b15ef7c464b3719de0f34b6e4ae3c73ea5eea8e3369d2d0538697702a2d" +"attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0002.done" = "76f50b528daca79ab1eac47c47f96ca1e7b8ad94bcaefbaac04335941306f993" +"attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0003-P.tsv" = "59b6bb9720ab227ab3d2d09906c21dd6c34e5a333e4f09edefe6d3d820209c3a" +"attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0003-Z.tsv" = "62d43155696ae2feb0e2b0bfe6190a6da458137a597810a3c0bfb6b4a61a0c7d" +"attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0003-timing.tsv" = "c2043f0b3bd54de33add441283f0c17ad4e4879a907aad34feef7bc9307affa9" +"attempts/f0008-RVEsvw/raw/jobs/getdp-f0008-b0003.done" = "20bdbe7d9351850f55ebf49a31f3a93235d04aea44253ddd2d75828a83fa1c74" +"attempts/f0008-RVEsvw/solver.pre" = "d78b89782facd5771242d1147b04332a07f9466f7415dc70d3afd65e302b2e1d" +"attempts/f0009-M2mf88/attempt.json" = "05fe13f80e4ec7431bf5dd5b37e3f7b8b0526a8c6f37910c59bc8ca72e091ba3" +"attempts/f0009-M2mf88/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0009-M2mf88/getdp.log" = "65945274edc317bd815a50c5123484fd775657873cf6fb045067affe4beed8ce" +"attempts/f0009-M2mf88/petsc.log" = "a336358acc75bfcb449a3e7b6df84d2f3ab1b3f1600d9ad76797739e018e9819" +"attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0001-P.tsv" = "84dcbcce7647f22987130619c1e2fb73b76e0501359492b79b1e313a21cfbf38" +"attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0001-Z.tsv" = "c569f16e96adc30dbc9ad683e8312fcdfc07f8c7ba11fa3a2aa485952c205f07" +"attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0001-timing.tsv" = "ff524787b38a6bc2db153e66fc91cb14edf04ca974ff7a507b5df955d9f91f31" +"attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0001.done" = "22b8b831af1067ac03eaf9570ddd71235af29f6b65e559199200376f6ef90f2a" +"attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0002-P.tsv" = "127a7c2b65f83575a74b5528c95fc23ce7a55636b743a3325a122ad3392da8bb" +"attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0002-Z.tsv" = "13a2c974c7616931b0317730c32260e188603249ebf4898e3bcec1917cc08765" +"attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0002-timing.tsv" = "5e5e4b27f6e9dd766c91a45c7aa5411fd4d7f1ea58b38c00a45a4aa0e3945726" +"attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0002.done" = "2ab6b278377a8a5090d5d456a9193028844db99b8f3fa96c2433d70920e73c22" +"attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0003-P.tsv" = "9a54f72af8f974313fe3a96ffbb33e97f9a58404c07a375a366db862653a9dba" +"attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0003-Z.tsv" = "afafa17b6905e9c8497efe5775c4db109951650d8bf6949000eda092f771a03b" +"attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0003-timing.tsv" = "8b05303ac1ca02207487c1ef70cdb4839ef26bc1ad314c7b9c665edfb4fd063b" +"attempts/f0009-M2mf88/raw/jobs/getdp-f0009-b0003.done" = "3449a1ca026b39cca413695b5bd20e176addf6bcf3103f9147e8ac56407cafa3" +"attempts/f0009-M2mf88/solver.pre" = "0b3af1d7a334ee1386f03963cc96bc125f3ac443ccbe3d1d9d07879487f9657e" +"coordinator.lock" = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" +"input/computation.json" = "4a7d5ce6eaf39ffb37f3a3c9b337b2e8780c05af7552c261e1353d01542fab25" +"input/getdp/integration.pro" = "e113951ac1e6e658f1be50e53875e30c85497445f0e6584a8a7ab1848588e902" +"input/getdp/jacobian.pro" = "014208d39043ab13183c42c312aba459265715da6d6ddc38ec1fbc39b6090c73" +"input/getdp/materials.pro" = "581f089113810a379d8a49c03d79f55045b723c8f4c761d0a493a019ff1f5d9e" +"input/getdp/model.pro" = "c0bb424db5ab4db9ca22f549f675edb4646f69910d2aa14e20cf4f40e41bd313" +"input/getdp/quasi-full.pro" = "1aede35114ea686c5f109089478027d241dec12dc9fd14a24452b348f89b08e4" +"input/getdp/quasi-tem.pro" = "448d9019ad417c9a784da20170b855dd25e559297499a83d19b007ee095c084d" +"input/model_data.pro" = "a1b206a600577a6ee660999e24c6a66687c4a846aa825e45e47fd243da2dea3a" +"input/problem.json" = "b634b2da621381c164831235b0c668a328d977158438c617014aed408c689eae" +"logs/getdp.log" = "f10f17af9f97e9a9126e657b5cdc995e426b8c7911d28e966d3da39506673536" +"mesh/frequency_0001.json" = "5a42638f462abe444900a4a6039e5b689de2e12167bd9c045af0342785d973eb" +"mesh/frequency_0001.msh" = "9f7ac3f89024f7ac31e673df1e5aee620ea2a7ca488066e8452a28ef936bda4c" +"mesh/frequency_0002.json" = "493d03a653898e6a73baeb9ecfecb2439064508a8ae16e2966a5cc813c7b94d3" +"mesh/frequency_0002.msh" = "6fb71fe421a70e09392fd82abdc22a0576eabd43ac8930b4c5bb3f50c2b9da11" +"mesh/frequency_0003.json" = "4b2eea760259765cadefa24dc48d0fc44c537956ae3530d65dffa4833f8e8828" +"mesh/frequency_0003.msh" = "8179ba77ef1c70adbaa3c352923b796251875d73a5a2a2d528ca3ae4c406c9ed" +"mesh/frequency_0004.json" = "9e71f0e5320732734a343ce809cb84c9d909c4c2424635135e96761567890d67" +"mesh/frequency_0004.msh" = "89986de0c9236fc43de83f4bfddd8ba4affe3931ca8014faed026c4496355ff9" +"mesh/frequency_0005.json" = "9057e79cc5d785cc171fe4a1cb6445de90ae1eb9dc84ac999f195756aa932fad" +"mesh/frequency_0005.msh" = "34f32f2aea1e8d264d267ac80bcf7d89c9d238fc9923e571b41c2e7d75226451" +"mesh/frequency_0006.json" = "0bff4d15bb12c712df8beb043fd3944a566c2ce4cc049fb33abeafca4eda39ae" +"mesh/frequency_0006.msh" = "b6cd8780850b7790d802ab66c6ff7347ebd785ec37b2e97c4c92babe07d68ad9" +"mesh/frequency_0007.json" = "f9fb0398f2c39b9edda760c06956a842e3ff5fe7fd96eb6085ba860677f81de0" +"mesh/frequency_0007.msh" = "88997a13269109f19a2b18a9c4fb807dec2eb10aed16b0371056cb244a0c6e12" +"mesh/frequency_0008.json" = "35fc92197f1e533841783a2e802788c791c43a993fec22a3fe47ff1700406ef0" +"mesh/frequency_0008.msh" = "6f8c976f3dab78a0a2e5c067c776117eef0e75af5393fcffe3be01b3b22a7988" +"mesh/model.json" = "fd86724063e5a2569cd1c947450b2f33a0a2323d3e9f0484234e6e3035748546" +"mesh/model.msh" = "7251370696dd8bdb8983b72a9c4314ad39b966f0653ef4480f5bf6cd2c316fa1" +"raw/P.tsv" = "ea3edfb5e92d0ed70880a6f2ebd39b41bae5299088bab46afd3329e6aafa6987" +"raw/Z.tsv" = "b78fec03f2275df0ed1ebc733c00c8dc0bb7cd2a6b26423b12f00d336032695b" +"raw/checksums.json" = "77cf48dba03bb993c7baaccbc23c83ef026684b2cba3ea08d5d35611d400a07a" +"raw/jobs/getdp-f0001-b0001-P.tsv" = "46fa546412f46ef46b2fb434f7008e166c57e8a707ee228c7505b3d7b74b69a1" +"raw/jobs/getdp-f0001-b0001-Z.tsv" = "d0b571a4b155655571ecfc86ddfb815b07e9f85eb602f48868f7293cf7a09647" +"raw/jobs/getdp-f0001-b0001-timing.tsv" = "b394dfc548f5fadb7cad04fa6a8d85adb2d610b12c98e2aac61edb3fbcdaca80" +"raw/jobs/getdp-f0001-b0001.json" = "e4cbf1face5a404351a252c2f93b6933374548b1320037741dd904c5602b6914" +"raw/jobs/getdp-f0001-b0002-P.tsv" = "8a559a373497f763d32a6095a7d9b497549217d33cf1719267290ae2c929dd07" +"raw/jobs/getdp-f0001-b0002-Z.tsv" = "b4cd701a5b69fb49d365066b2927e54c950dc5d06a66f1a0df3339679ffd3a12" +"raw/jobs/getdp-f0001-b0002-timing.tsv" = "7d414b81550475e600571ef870b8d9dd35888eba9dd214939853bbb35405cc99" +"raw/jobs/getdp-f0001-b0002.json" = "46a6ce16f246c2d69d3584db742c93448921f883cb132b83d3ac6416ddc17df6" +"raw/jobs/getdp-f0001-b0003-P.tsv" = "2e1b353722f20454ee70cef7fb46c153313f12094a7a8b0b988362c2913c6df9" +"raw/jobs/getdp-f0001-b0003-Z.tsv" = "4352656d9afe9722eabfa397f9f0d6437a4ba11c5d9e993b6fc7f1f1bd7cba7a" +"raw/jobs/getdp-f0001-b0003-timing.tsv" = "8bac2c85564c9dabe6a8a7d99cd43e117120c90f2453e43fc67f95f041061d75" +"raw/jobs/getdp-f0001-b0003.json" = "45a62eb18e93fb0c329a9bd8725c74a8d887077402b4d7e390eac2eea408e8c7" +"raw/jobs/getdp-f0002-b0001-P.tsv" = "0f4072dfbd4ec940e6f063a1c8f953cd1ecc302f0ff0105bc650a2279ba9e3f8" +"raw/jobs/getdp-f0002-b0001-Z.tsv" = "2d42112d21371805fdca88142eb211b62824faae1f28012e57c0c159b08ab6ee" +"raw/jobs/getdp-f0002-b0001-timing.tsv" = "4de08b845373f35cd877dbf9768cce9e45195a4c25c6f18631d6c7623ba29f5c" +"raw/jobs/getdp-f0002-b0001.json" = "36e56bb257f73a06d1f1b67f3d0d4bd1dac9572c539e3cdfb421acb351d75b44" +"raw/jobs/getdp-f0002-b0002-P.tsv" = "12ea30983f26ced34ad2cf546ddb66465e38832e80abe29f53f6d35c31e7add7" +"raw/jobs/getdp-f0002-b0002-Z.tsv" = "ec3fca324f4aab7629a78d0dab90eb8c9b142a3299f96ffd6d7a942eac2e8c42" +"raw/jobs/getdp-f0002-b0002-timing.tsv" = "6efc242b70bfbde32425ab607e68b509c0f1e3c34eb6fdf743ede33a1e72e93e" +"raw/jobs/getdp-f0002-b0002.json" = "38ee2286796f7f8cfef1c995be19ad0f17a5bf561bad329ac2baf9b1af272784" +"raw/jobs/getdp-f0002-b0003-P.tsv" = "b29350aa094132c0cb923ddd344529b8a145dfcffabfcdacc77886ea71e44f94" +"raw/jobs/getdp-f0002-b0003-Z.tsv" = "4634c1eedd44f096ba8b45639a54915e2677ee47d8b32698dfbea647d5d228e5" +"raw/jobs/getdp-f0002-b0003-timing.tsv" = "733a6b26b8e195789e81df0ac97dfd4e48c6971a4fdb1545b1c760d2c080b3e3" +"raw/jobs/getdp-f0002-b0003.json" = "f1ef2feee5b64e5f2fe75dcd2b5220542254ede44c2a29f441717c95f2f546d4" +"raw/jobs/getdp-f0003-b0001-P.tsv" = "0b1ea83c170a28e72f9251fb8a8637f72a704856561fd966c0869a1a5893e082" +"raw/jobs/getdp-f0003-b0001-Z.tsv" = "03865b1fe279f225b912f741b6ba8efc40b88adcd335b8def4efa6b2ad1ca52a" +"raw/jobs/getdp-f0003-b0001-timing.tsv" = "423ab5efb1eaadeba6420651381573e3428bae3d8c77b3b2ae7edc758768d7bb" +"raw/jobs/getdp-f0003-b0001.json" = "b4db402c9a77fa493c6cc32af591155ca21ade728bc7a96cb20b83b1b61730b5" +"raw/jobs/getdp-f0003-b0002-P.tsv" = "93d32ccea24bc99036d1291a31b68b68a0312515a1b10e590982cb94ab8cefbf" +"raw/jobs/getdp-f0003-b0002-Z.tsv" = "e49b22f5faeac4fc17d06a7a0f0568bbadda235a1431126db6c0268bbbcda12d" +"raw/jobs/getdp-f0003-b0002-timing.tsv" = "87b2df7edd6d066c5918d51d1b03bf4f3420ada46f96b977734d523d120afcf9" +"raw/jobs/getdp-f0003-b0002.json" = "dc720457b290b24a56cb8d19fe650e4d9928ca7fd9f97a90e7a9244b64af9f2c" +"raw/jobs/getdp-f0003-b0003-P.tsv" = "24c8257bb4b0c512b85b43e1db9d18158684f268f276cc77192791029497ff51" +"raw/jobs/getdp-f0003-b0003-Z.tsv" = "73291fd6c57b0b709db8b8e1034f28adb0f9ca38a0ba5b720d526c0931ad5b7f" +"raw/jobs/getdp-f0003-b0003-timing.tsv" = "176375f083f0c5b3f87286b5363ef90dc10709a1dfa714ea30735a406e488f0e" +"raw/jobs/getdp-f0003-b0003.json" = "42dd87cefadd1cd807a76d1612e9ee59d2a394e71dec1758060c75976c5f7c1e" +"raw/jobs/getdp-f0004-b0001-P.tsv" = "0414f7d9279ba3fc48565c59dfce50ff1a6ac9c5a9cadede2426432da9316f4e" +"raw/jobs/getdp-f0004-b0001-Z.tsv" = "037c545ac4f3ac6a1608b35709600ccd4616928d836607f5a52c89ff361d03ed" +"raw/jobs/getdp-f0004-b0001-timing.tsv" = "7f63264a16c152a4f0b86e1d43fd7f4eed1f18785cf2d7d5fd51a2c04d5c8154" +"raw/jobs/getdp-f0004-b0001.json" = "900ff5a09fc1277b901349f4b38d681e356476f52b9971a9ebeee28ed4ecd5dc" +"raw/jobs/getdp-f0004-b0002-P.tsv" = "3afc45db4632952d5724691256d58678bcd2eb881e27e70142173c723b6dabae" +"raw/jobs/getdp-f0004-b0002-Z.tsv" = "967fe79df063cebf45dbeb09e712f518afb20029986bc826ee2818437af3731b" +"raw/jobs/getdp-f0004-b0002-timing.tsv" = "5e44fba2b54e0ef8dd365c0b2a16dd1c5b5b52bf94086a2a5821aed7e411e616" +"raw/jobs/getdp-f0004-b0002.json" = "1c4f66f014440c685269a7c5c6f12235a7de22dcd389fa8945a2b199625e500a" +"raw/jobs/getdp-f0004-b0003-P.tsv" = "aa81bae32c350b92b595eabe7e3ba951a469af09a3b429afd7ec99d91078a857" +"raw/jobs/getdp-f0004-b0003-Z.tsv" = "a2d6f4c7ae9ac114eae3cc56462cf2e1bb40e5b4de651496ac60726a61824058" +"raw/jobs/getdp-f0004-b0003-timing.tsv" = "1ee834f682654b899d943de24e87147c2621eaba998f8b2d6c7c89f8963723a5" +"raw/jobs/getdp-f0004-b0003.json" = "b57ef8cffff6783a05e0d42f2237b28d51b9d8d2140bce4798a461adc088c02f" +"raw/jobs/getdp-f0005-b0001-P.tsv" = "4fd374e319aa02cd4a461d06a94a2b964d070dfaa6672007ffe0dd38761e0c9b" +"raw/jobs/getdp-f0005-b0001-Z.tsv" = "fab5e56f6c89d4c928ecd9ace48cdc4cfba8476d91d72299c823cc80afb8160d" +"raw/jobs/getdp-f0005-b0001-timing.tsv" = "53574e2b5e1083732f43d22985fe6095f459d67fd3dc411bf8dbb873b397967d" +"raw/jobs/getdp-f0005-b0001.json" = "aa6dbb92b0a34dbff246f4ed08b7b5b8e6097d75c106f4c63339f49813c34696" +"raw/jobs/getdp-f0005-b0002-P.tsv" = "7424966f1503b3e63246c9dcf726fa0954ecd7968cd2ea40a39811bd44812764" +"raw/jobs/getdp-f0005-b0002-Z.tsv" = "13f053e4852ab5b18315cc0d54edc75d52bc8a3e0ecf5af5933d217c5ca270d9" +"raw/jobs/getdp-f0005-b0002-timing.tsv" = "6d3a950a55d9c9e8434c0d8132ca99b73fda65a5b4b0cd3966f5f3c036be4567" +"raw/jobs/getdp-f0005-b0002.json" = "edd7f292a0952addc23c6401d479197a043d3af1b9b450a76b8e1b2a2276c915" +"raw/jobs/getdp-f0005-b0003-P.tsv" = "a7009ee10a8bf5f8c516c5ebb74715bd10e620e7e08459d5aff103c1ffe692e8" +"raw/jobs/getdp-f0005-b0003-Z.tsv" = "e6edda7298951e5ff0eb0f3d453b23678cf248e4072abede586c0e0ab4292a44" +"raw/jobs/getdp-f0005-b0003-timing.tsv" = "7add1c8273c631dc052688e7e4a99216fca5b5579b2aa3266953f773a181ba68" +"raw/jobs/getdp-f0005-b0003.json" = "811dd406b2341ba32efa6c2a4377fdedffccfab6e86d02598638ffba530f080f" +"raw/jobs/getdp-f0006-b0001-P.tsv" = "ab503fe79c9dda6c9ae0c08c9330cdcf21a85965792c0355a3901e5839cb5853" +"raw/jobs/getdp-f0006-b0001-Z.tsv" = "4842e5855355757b6dc19a967a4a92014f541c22a5a48e700d02dc7231e3ec4e" +"raw/jobs/getdp-f0006-b0001-timing.tsv" = "7a39d6225512aeac0c8379081c92bc76df854c5c7db4cbf3dae125b580616236" +"raw/jobs/getdp-f0006-b0001.json" = "bc10b2fd33ccfbce609f0438c3d4a553571542dc1c8a3d31f6e417e707f39b98" +"raw/jobs/getdp-f0006-b0002-P.tsv" = "0c47b4d70c891a27b8d4c928b85461b116329f11a83ab2b3312ef76837dc8730" +"raw/jobs/getdp-f0006-b0002-Z.tsv" = "53f38964d8b833d664ffcfed54d520b10cd1f97d5005888aa15e8483e683d1df" +"raw/jobs/getdp-f0006-b0002-timing.tsv" = "994b3247fe03648d4c1582a28104f9dd758d559c518a4120a1928f0251a9bd4f" +"raw/jobs/getdp-f0006-b0002.json" = "661bfa3b19ebdd8e241529de918be23383b1351a69de5bff0dc9f390514cf894" +"raw/jobs/getdp-f0006-b0003-P.tsv" = "cedf5b927eebe2818abd67c2445ce6599c08892e7b8984d278302cbe5cddb03c" +"raw/jobs/getdp-f0006-b0003-Z.tsv" = "67209302d9a76618a9976956b513903dd42632964eba09bb10367acaf5e3a623" +"raw/jobs/getdp-f0006-b0003-timing.tsv" = "a418c43a4f4399b78eb81c969c8efb698635a7a9c8c8f00f380196470e977ed6" +"raw/jobs/getdp-f0006-b0003.json" = "76e43fdef27f8cb5c2dda9cefcec7b211b79c62ba0fd24aaf8fe97915548a7be" +"raw/jobs/getdp-f0007-b0001-P.tsv" = "6a953a9b70ca02ac0cd4488e33722d3dd0fc58163b0ebd9c3fa5f572dacf9866" +"raw/jobs/getdp-f0007-b0001-Z.tsv" = "a6a42398bb170d2d9b55b83b578a8dc3a7cea50f8f21ee566fe488d9f630512b" +"raw/jobs/getdp-f0007-b0001-timing.tsv" = "ede82f09fc10b7433e42ba302e2cde9791c8997192de4cb2940d8e7a0680ef74" +"raw/jobs/getdp-f0007-b0001.json" = "95c0cfef823994d5e8b1edf1a2484f958d36eaca1d34577ec6378b73edbc41ed" +"raw/jobs/getdp-f0007-b0002-P.tsv" = "7e3db93e6e731004069d9a6c1a6524289fe6b924ebab0115aba05df38154f6ce" +"raw/jobs/getdp-f0007-b0002-Z.tsv" = "f40055d73faf28e512cf13715e8e03533361ee0c1cd710f2dac96d1f18ca6b89" +"raw/jobs/getdp-f0007-b0002-timing.tsv" = "899406ac6a6adc605a7153cc734fdcc98bfe9c5c2d34b033da25252cf57ca027" +"raw/jobs/getdp-f0007-b0002.json" = "88d3ad72026026b21dc650afd33995d2677a997506508ae8ecc66a09e34da641" +"raw/jobs/getdp-f0007-b0003-P.tsv" = "c5ea44f265565130c3d12c198b2b8a8699d983889e3a6b95afba3dbffb9bc6ef" +"raw/jobs/getdp-f0007-b0003-Z.tsv" = "a697df98dcb85881385c17ed46a6e7ad2a35516c8fe9fa6059dcbf752f6f0aff" +"raw/jobs/getdp-f0007-b0003-timing.tsv" = "cc042d0730a1ca8702092704495c615b8bf94153e909d74ae1827f741ccd6c6f" +"raw/jobs/getdp-f0007-b0003.json" = "d0ea75e7d550fa6e17f8d15e01c7e8d13c0c988e6db7b326de19be4f652f1e49" +"raw/jobs/getdp-f0008-b0001-P.tsv" = "c56d8b5c9e45b5e774e2f4745c7f5e46b1739ad02ded9b4ab4b75806144623ff" +"raw/jobs/getdp-f0008-b0001-Z.tsv" = "d5c8d42902eb6e828326e1d52e541d31927607be3360db8087af5ee1dd382a54" +"raw/jobs/getdp-f0008-b0001-timing.tsv" = "113721716ab550a5288b188f326176926117290c8321e0953dbdf482945b1684" +"raw/jobs/getdp-f0008-b0001.json" = "636705458f03980092455b1e282293be48c22c3ae85122a0f0c663eb7c1f626a" +"raw/jobs/getdp-f0008-b0002-P.tsv" = "344aa533e8e46c9a85a5916ffb56f37a269dc67882102d52490cf76cfba8a2bc" +"raw/jobs/getdp-f0008-b0002-Z.tsv" = "d0208d54fda3554aa0ce358a0cef8f045f5294db8c096b7fe5fbd56bf83f6454" +"raw/jobs/getdp-f0008-b0002-timing.tsv" = "9ffb5b15ef7c464b3719de0f34b6e4ae3c73ea5eea8e3369d2d0538697702a2d" +"raw/jobs/getdp-f0008-b0002.json" = "e96c92e605431d9f24699b87a246469389a74f40b6a281eb1c818cd8c1f14e34" +"raw/jobs/getdp-f0008-b0003-P.tsv" = "59b6bb9720ab227ab3d2d09906c21dd6c34e5a333e4f09edefe6d3d820209c3a" +"raw/jobs/getdp-f0008-b0003-Z.tsv" = "62d43155696ae2feb0e2b0bfe6190a6da458137a597810a3c0bfb6b4a61a0c7d" +"raw/jobs/getdp-f0008-b0003-timing.tsv" = "c2043f0b3bd54de33add441283f0c17ad4e4879a907aad34feef7bc9307affa9" +"raw/jobs/getdp-f0008-b0003.json" = "b35ddbcfac080dc2a539cb49460a57ef7b268c2b87ef58a18e2dd5bc211ae4e7" +"raw/jobs/getdp-f0009-b0001-P.tsv" = "84dcbcce7647f22987130619c1e2fb73b76e0501359492b79b1e313a21cfbf38" +"raw/jobs/getdp-f0009-b0001-Z.tsv" = "c569f16e96adc30dbc9ad683e8312fcdfc07f8c7ba11fa3a2aa485952c205f07" +"raw/jobs/getdp-f0009-b0001-timing.tsv" = "ff524787b38a6bc2db153e66fc91cb14edf04ca974ff7a507b5df955d9f91f31" +"raw/jobs/getdp-f0009-b0001.json" = "904db2b53163a8dc55584458a74769187efae6bc9e04299e250b841fbafaf469" +"raw/jobs/getdp-f0009-b0002-P.tsv" = "127a7c2b65f83575a74b5528c95fc23ce7a55636b743a3325a122ad3392da8bb" +"raw/jobs/getdp-f0009-b0002-Z.tsv" = "13a2c974c7616931b0317730c32260e188603249ebf4898e3bcec1917cc08765" +"raw/jobs/getdp-f0009-b0002-timing.tsv" = "5e5e4b27f6e9dd766c91a45c7aa5411fd4d7f1ea58b38c00a45a4aa0e3945726" +"raw/jobs/getdp-f0009-b0002.json" = "9771fb54906a0c3857eecff9b2f288998ae8abc9ad9d8f91d5bd546f5be8fe17" +"raw/jobs/getdp-f0009-b0003-P.tsv" = "9a54f72af8f974313fe3a96ffbb33e97f9a58404c07a375a366db862653a9dba" +"raw/jobs/getdp-f0009-b0003-Z.tsv" = "afafa17b6905e9c8497efe5775c4db109951650d8bf6949000eda092f771a03b" +"raw/jobs/getdp-f0009-b0003-timing.tsv" = "8b05303ac1ca02207487c1ef70cdb4839ef26bc1ad314c7b9c665edfb4fd063b" +"raw/jobs/getdp-f0009-b0003.json" = "a6943a782e8c0671a210e3f3be9793a97cfd033b09429fe90641cfb2ea8d7e98" +"raw/scan_complete.tsv" = "2cc9eff50bb7e6fff0e2555f96d93ebe952c4a1a158a1dca82fe6cca3aa1afab" +"run.json" = "c88e7a97a913e572acc90bee68b631f9713fd26dc66204950e39e5c65c5ce4b1" +"timing-summary.json" = "5c5a800eafc3caf43ae4253396b5abe8ad9a5cf6b1dba6c4ea423887d285cf74" diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/fem/formulation.txt b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/fem/formulation.txt new file mode 100644 index 000000000..0e94e36ec --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/fem/formulation.txt @@ -0,0 +1 @@ +@NamedTuple{backend::Symbol, requested::NamedTuple, methods::NamedTuple, options::NamedTuple}((:fem, (insulation_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), semicon_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_properties = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), temperature_dependence = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing)), (insulation_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), semicon_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), earth_properties = (identifier = :constant, parameters = NamedTuple(), options = NamedTuple()), temperature_dependence = (identifier = :linear, parameters = NamedTuple(), options = NamedTuple())), (reduce_bundle = false, kron_reduction = false, ideal_transposition = false, physics = Symbol("quasi-tem")))) \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/fem/problem.json b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/fem/problem.json new file mode 100644 index 000000000..08854494a --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/fem/problem.json @@ -0,0 +1,516 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "root": { + "kind": "line_parameters_problem", + "earth_props": { + "kind": "earth_model", + "vertical_layers": false, + "layers": [ + { + "kind": "earth_layer", + "thickness": { + "special": "Inf", + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "rho": { + "special": "Inf", + "__type__": "Float64" + } + }, + { + "kind": "earth_layer", + "thickness": { + "special": "Inf", + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "rho": { + "value": 0.1, + "__type__": "Float64" + } + } + ] + }, + "Gamma": null, + "frequencies": [ + { + "value": 0.1, + "__type__": "Float64" + }, + { + "value": 1, + "__type__": "Float64" + }, + { + "value": 10, + "__type__": "Float64" + }, + { + "value": 100, + "__type__": "Float64" + }, + { + "value": 1000, + "__type__": "Float64" + }, + { + "value": 10000, + "__type__": "Float64" + }, + { + "value": 100000, + "__type__": "Float64" + }, + { + "value": 1000000, + "__type__": "Float64" + }, + { + "value": 10000000, + "__type__": "Float64" + } + ], + "system": { + "kind": "line_cable_system", + "input_positions": [ + { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 1, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 2, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 1, + "__type__": "Float64" + } + } + ], + "clearances": [ + [ + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + } + ], + [ + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + } + ], + [ + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + } + ] + ], + "connections": [ + [ + 1 + ], + [ + 2 + ], + [ + 3 + ] + ], + "line_length": { + "value": 1, + "__type__": "Float64" + }, + "environment": null, + "system_id": "three_bare_wires_air_3", + "positions": [ + { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 1, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 2, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 1, + "__type__": "Float64" + } + } + ], + "designs": [ + { + "kind": "cable_design", + "cable_id": "three_bare_wires_air_3", + "nominal_data": [ + ], + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "core_metal", + "material": { + "value": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "type": "material" + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.0425, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + } + ] + } + }, + { + "kind": "cable_design", + "cable_id": "three_bare_wires_air_3", + "nominal_data": [ + ], + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "core_metal", + "material": { + "value": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "type": "material" + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.0425, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + } + ] + } + }, + { + "kind": "cable_design", + "cable_id": "three_bare_wires_air_3", + "nominal_data": [ + ], + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "core_metal", + "material": { + "value": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "type": "material" + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.0425, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + } + ] + } + } + ] + }, + "temperature": { + "value": 20, + "__type__": "Float64" + } + }, + "format": "linecablemodels.line_parameters_problem", + "version": "1.0.0" +} \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/unified/Y.csv b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/unified/Y.csv new file mode 100644 index 000000000..186460074 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/unified/Y.csv @@ -0,0 +1,82 @@ +frequency_hz,response_terminal,basis_terminal,real_S_per_m,imaginary_S_per_m +0.10000000000000001,cable:1:core,cable:1:core,10.815794395856155,0.14817480024608823 +0.10000000000000001,cable:1:core,cable:2:core,-8.3998980305212747,0.14802772097705633 +0.10000000000000001,cable:1:core,cable:3:core,-1.1280530750282914e-13,-2.953531354972855e-12 +0.10000000000000001,cable:2:core,cable:1:core,-8.3998980305212676,0.14802772097705355 +0.10000000000000001,cable:2:core,cable:2:core,10.815794395856189,0.14817480024712157 +0.10000000000000001,cable:2:core,cable:3:core,-1.1296417495695151e-13,-4.2580756764147922e-12 +0.10000000000000001,cable:3:core,cable:1:core,-1.358174635016056e-13,-2.933449794573915e-12 +0.10000000000000001,cable:3:core,cable:2:core,-1.3601058441964668e-13,-4.2379939867923176e-12 +0.10000000000000001,cable:3:core,cable:3:core,3.3278820986485504e-24,9.0759067020568993e-12 +1,cable:1:core,cable:1:core,11.070033787834907,0.2190779513727911 +1,cable:1:core,cable:2:core,-8.1458225270892957,0.21787423634749992 +1,cable:1:core,cable:3:core,-1.621627954619451e-12,-2.7605627275097529e-11 +1,cable:2:core,cable:1:core,-8.145822527089182,0.21787423634739994 +1,cable:2:core,cable:2:core,11.070033787835508,0.21907795138250014 +1,cable:2:core,cable:3:core,-1.6339304034811439e-12,-4.0648885431782831e-11 +1,cable:3:core,cable:1:core,-2.4448833834075816e-12,-2.6909309921922212e-11 +1,cable:3:core,cable:2:core,-2.4605830567011745e-12,-3.9952527978242624e-11 +1,cable:3:core,cable:3:core,3.1423916594339771e-22,9.0759067020550995e-11 +10,cable:1:core,cable:1:core,11.455439466692644,0.35723512536492974 +10,cable:1:core,cable:2:core,-7.7620499918370207,0.34786703922841428 +10,cable:1:core,cable:3:core,-2.4359412301593299e-11,-2.4772235737075831e-10 +10,cable:2:core,cable:1:core,-7.7620499918328045,0.34786703922484874 +10,cable:2:core,cable:2:core,11.455439466704808,0.35723512545105984 +10,cable:2:core,cable:3:core,-2.5235453516660523e-11,-3.7794241747387335e-10 +10,cable:3:core,cable:1:core,-5.4539271312740393e-11,-2.2299128059268601e-10 +10,cable:3:core,cable:2:core,-5.574623180572549e-11,-3.5319928202439063e-10 +10,cable:3:core,cable:3:core,2.7893988431890267e-20,9.0759067020117803e-10 +100,cable:1:core,cable:1:core,12.104669782424534,0.68261648227961136 +100,cable:1:core,cable:2:core,-7.1288262823565649,0.61549758392216802 +100,cable:1:core,cable:3:core,-3.7138847700779774e-10,-2.0430445507832211e-09 +100,cable:2:core,cable:1:core,-7.1288262822103974,0.61549758378874064 +100,cable:2:core,cable:2:core,12.104669782732644,0.6826164828956689 +100,cable:2:core,cable:3:core,-4.2495166380733159e-10,-3.3258245294674373e-09 +100,cable:3:core,cable:1:core,-1.4910656784955182e-09,-1.1316480131395586e-09 +100,cable:3:core,cable:2:core,-1.575033846674186e-09,-2.4110868103799636e-09 +100,cable:3:core,cable:3:core,1.9127172212576587e-18,9.0759067005974408e-09 +1000,cable:1:core,cable:1:core,13.422507236118639,1.6280303360967181 +1000,cable:1:core,cable:2:core,-5.9564805806274439,1.2141336265943119 +1000,cable:1:core,cable:3:core,-5.3125768097971711e-09,-1.3812807003898819e-08 +1000,cable:2:core,cable:1:core,-5.9564805769900762,1.2141336212955836 +1000,cable:2:core,cable:2:core,13.422507243289036,1.6280303346877092 +1000,cable:2:core,cable:3:core,-7.6990452661998479e-09,-2.5177999831420848e-08 +1000,cable:3:core,cable:1:core,-4.4374379330248328e-08,2.2280527656749974e-08 +1000,cable:3:core,cable:2:core,-4.9053839044978226e-08,1.1664030389121588e-08 +1000,cable:3:core,cable:3:core,-1.1449192093489889e-16,9.075906643642654e-08 +10000,cable:1:core,cable:1:core,17.086534015481533,4.2459949196386608 +10000,cable:1:core,cable:2:core,-3.3202279874715721,2.2955286461190294 +10000,cable:1:core,cable:3:core,-5.858742019788468e-08,-4.3856893278769888e-08 +10000,cable:2:core,cable:1:core,-3.320228031402729,2.2955285336624991 +10000,cable:2:core,cable:2:core,17.086533982869692,4.2459947768646549 +10000,cable:2:core,cable:3:core,-1.0820603152553118e-07,-9.3862013176872435e-08 +10000,cable:3:core,cable:1:core,-7.4376780209961771e-07,1.4920783526578319e-06 +10000,cable:3:core,cable:2:core,-8.720081626905464e-07,1.5363787545915007e-06 +10000,cable:3:core,cable:3:core,-1.4613839178070811e-13,9.075904422043152e-07 +100000,cable:1:core,cable:1:core,25.114053091114524,8.0617971943186379 +100000,cable:1:core,cable:2:core,0.17168860920972809,1.0502292248219696 +100000,cable:1:core,cable:3:core,-5.2567611057230254e-08,7.452419524450226e-08 +100000,cable:2:core,cable:1:core,0.17168867735386809,1.0502294245133177 +100000,cable:2:core,cable:2:core,25.114053162870402,8.061797456881747 +100000,cable:2:core,cable:3:core,-1.377947989655141e-07,1.5778396671832445e-07 +100000,cable:3:core,cable:1:core,1.5670639845467555e-05,7.7897672759496725e-06 +100000,cable:3:core,cable:2:core,1.6957770865381582e-05,9.1879747547490139e-06 +100000,cable:3:core,cable:3:core,-5.4794500699005802e-11,9.0758158501016034e-06 +1000000,cable:1:core,cable:1:core,40.833774626502681,19.549173406400044 +1000000,cable:1:core,cable:2:core,-0.020403832499540885,-0.018991053491192875 +1000000,cable:1:core,cable:3:core,-1.5503936374884625e-09,-5.8061917080445266e-09 +1000000,cable:2:core,cable:1:core,-0.020403832081266379,-0.018991054091929891 +1000000,cable:2:core,cable:2:core,40.833774627208193,19.549173405645224 +1000000,cable:2:core,cable:3:core,-3.6993334581473173e-09,-1.4559816308977549e-08 +1000000,cable:3:core,cable:1:core,-6.4350271678452756e-06,5.1067899044121304e-06 +1000000,cable:3:core,cable:2:core,-7.798364509434425e-06,5.4402402295420326e-06 +1000000,cable:3:core,cable:3:core,-2.4719345718823336e-08,9.0711547555953113e-05 +10000000,cable:1:core,cable:1:core,80.846531351828517,55.108195401013525 +10000000,cable:1:core,cable:2:core,-9.2664181398836154e-08,-1.3479772233804371e-07 +10000000,cable:1:core,cable:3:core,-5.2158582925956706e-14,-5.4725070160129558e-14 +10000000,cable:2:core,cable:1:core,-9.2664181398870313e-08,-1.3479772233806722e-07 +10000000,cable:2:core,cable:2:core,80.846531351828517,55.108195401013525 +10000000,cable:2:core,cable:3:core,-1.1003845565783392e-13,-1.3743080703062872e-13 +10000000,cable:3:core,cable:1:core,3.6775425414283791e-11,4.3675607950356147e-10 +10000000,cable:3:core,cable:2:core,-9.3440467051073188e-11,5.4261406900812513e-10 +10000000,cable:3:core,cable:3:core,-1.6761328755330462e-05,0.00088777830561370815 diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/unified/Z.csv b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/unified/Z.csv new file mode 100644 index 000000000..49c412c89 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/unified/Z.csv @@ -0,0 +1,82 @@ +frequency_hz,response_terminal,basis_terminal,real_ohm_per_m,imaginary_ohm_per_m +0.10000000000000001,cable:1:core,cable:1:core,3.1374615510534732e-06,1.2435841216202129e-06 +0.10000000000000001,cable:1:core,cable:2:core,9.9023675189331398e-08,8.1529149017255914e-07 +0.10000000000000001,cable:1:core,cable:3:core,9.8689933646132813e-08,6.8496952228978997e-07 +0.10000000000000001,cable:2:core,cable:1:core,9.9023675189331385e-08,8.1529149017255903e-07 +0.10000000000000001,cable:2:core,cable:2:core,3.1374615510534732e-06,1.2435841216202129e-06 +0.10000000000000001,cable:2:core,cable:3:core,9.8692094314379658e-08,7.1450042905111195e-07 +0.10000000000000001,cable:3:core,cable:1:core,9.8689933646133078e-08,6.8496952228978838e-07 +0.10000000000000001,cable:3:core,cable:2:core,9.8692094314379645e-08,7.1450042905111184e-07 +0.10000000000000001,cable:3:core,cable:3:core,3.1368030432638579e-06,1.2442491566826316e-06 +1,cable:1:core,cable:1:core,4.0462619649157199e-06,1.0981370586286009e-05 +1,cable:1:core,cable:2:core,9.9703495653241453e-07,6.6990056616567426e-06 +1,cable:1:core,cable:3:core,9.8646617099486423e-07,5.4030097682223509e-06 +1,cable:2:core,cable:1:core,9.9703495653241474e-07,6.6990056616567443e-06 +1,cable:2:core,cable:2:core,4.0462619649157199e-06,1.0981370586286005e-05 +1,cable:2:core,cable:3:core,9.8663940253676923e-07,5.6982925454684148e-06 +1,cable:3:core,cable:1:core,9.8646617099486423e-07,5.4030097682223517e-06 +1,cable:3:core,cable:2:core,9.8663940253676923e-07,5.6982925454684148e-06 +1,cable:3:core,cable:3:core,4.025822822265763e-06,1.1002346916048127e-05 +10,cable:1:core,cable:1:core,1.405966050935706e-05,9.4699377213437497e-05 +10,cable:1:core,cable:2:core,1.0164731798726483e-05,5.2299532930466571e-05 +10,cable:1:core,cable:3:core,9.8317851451266173e-06,3.9569605534518679e-05 +10,cable:2:core,cable:1:core,1.0164731798726483e-05,5.2299532930466571e-05 +10,cable:2:core,cable:2:core,1.405966050935706e-05,9.4699377213437511e-05 +10,cable:2:core,cable:3:core,9.8448260812245698e-06,4.2519810980107075e-05 +10,cable:3:core,cable:1:core,9.831785145126619e-06,3.9569605534518666e-05 +10,cable:3:core,cable:2:core,9.8448260812245715e-06,4.2519810980107061e-05 +10,cable:3:core,cable:3:core,1.3443875148217152e-05,9.535729423772166e-05 +100,cable:1:core,cable:1:core,0.00011750384543838806,0.00077809644648036465 +100,cable:1:core,cable:2:core,0.00010621565159805986,0.00037161327841812278 +100,cable:1:core,cable:3:core,9.6062121241851712e-05,0.00025171028261849007 +100,cable:2:core,cable:1:core,0.00010621565159805986,0.00037161327841812278 +100,cable:2:core,cable:2:core,0.00011750384543838787,0.00077809644648036422 +100,cable:2:core,cable:3:core,9.6939258720589563e-05,0.00028095471396052349 +100,cable:3:core,cable:1:core,9.6062121241851698e-05,0.00025171028261849001 +100,cable:3:core,cable:2:core,9.6939258720589536e-05,0.00028095471396052349 +100,cable:3:core,cable:3:core,0.00010029065542748186,0.00079835616300552798 +1000,cable:1:core,cable:1:core,0.0011875295144732621,0.0060821869409340397 +1000,cable:1:core,cable:2:core,0.0011020546722505062,0.0020961818335963386 +1000,cable:1:core,cable:3:core,0.00083441087840421617,0.0011330005183466704 +1000,cable:2:core,cable:1:core,0.001102054672250506,0.0020961818335963386 +1000,cable:2:core,cable:2:core,0.0011875295144730795,0.0060821869409338489 +1000,cable:2:core,cable:3:core,0.00088055424215432207,0.0014022944498764082 +1000,cable:3:core,cable:1:core,0.00083441087840421607,0.0011330005183466704 +1000,cable:3:core,cable:2:core,0.00088055424215432207,0.0014022944498764082 +1000,cable:3:core,cable:3:core,0.00078851876842865251,0.0066684718151727796 +10000,cable:1:core,cable:1:core,0.01121739034987806,0.043047632316466668 +10000,cable:1:core,cable:2:core,0.0080340309126035597,0.0048666513437921688 +10000,cable:1:core,cable:3:core,0.0040559166332579241,0.0011450309278046009 +10000,cable:2:core,cable:1:core,0.0080340309126035597,0.0048666513437921671 +10000,cable:2:core,cable:2:core,0.011217390349824953,0.043047632316487082 +10000,cable:2:core,cable:3:core,0.0052676968239068461,0.0024783732958180778 +10000,cable:3:core,cable:1:core,0.004055916633257925,0.0011450309278046007 +10000,cable:3:core,cable:2:core,0.0052676968239072927,0.0024783732958178969 +10000,cable:3:core,cable:3:core,0.0051968947578844276,0.056898361105510831 +100000,cable:1:core,cable:1:core,0.091870181208271839,0.2852584218993296 +100000,cable:1:core,cable:2:core,0.0087282162587741137,-0.0087087925865141323 +100000,cable:1:core,cable:3:core,0.00087908935478323122,-0.0031017341595444914 +100000,cable:2:core,cable:1:core,0.008728216258774112,-0.0087087925865141323 +100000,cable:2:core,cable:2:core,0.091870181209119328,0.28525842189891248 +100000,cable:2:core,cable:3:core,0.0029200092671962086,-0.0055306020499575198 +100000,cable:3:core,cable:1:core,0.00087908935478321431,-0.0031017341595444797 +100000,cable:3:core,cable:2:core,0.0029200092671962086,-0.0055306020499575207 +100000,cable:3:core,cable:3:core,0.025029445696535323,0.51501770227425014 +1000000,cable:1:core,cable:1:core,0.75407440792450842,1.5740479450662672 +1000000,cable:1:core,cable:2:core,0.0001535709764889112,0.0010629809235061856 +1000000,cable:1:core,cable:3:core,8.1257499331935254e-05,8.9426642061852409e-05 +1000000,cable:2:core,cable:1:core,0.0001535709764889112,0.0010629809235061854 +1000000,cable:2:core,cable:2:core,0.75407440792448754,1.5740479450662619 +1000000,cable:2:core,cable:3:core,0.00017862565014624984,0.00022534477815653242 +1000000,cable:3:core,cable:1:core,8.1257499331935308e-05,8.9426642061852518e-05 +1000000,cable:3:core,cable:2:core,0.00017862565014624971,0.00022534477815653399 +1000000,cable:3:core,cable:3:core,0.094696109689700422,4.9430140482755665 +10000000,cable:1:core,cable:1:core,4.5479347057971937,6.6714050104071516 +10000000,cable:1:core,cable:2:core,3.0492342492444631e-09,1.314231864570851e-08 +10000000,cable:1:core,cable:3:core,6.8259441253474539e-10,1.2859327231771216e-10 +10000000,cable:2:core,cable:1:core,3.0492342492444631e-09,1.314231864570851e-08 +10000000,cable:2:core,cable:2:core,4.5479347057971937,6.6714050104071516 +10000000,cable:2:core,cable:3:core,1.5377810805855466e-09,4.4664472044009214e-10 +10000000,cable:3:core,cable:1:core,6.8259441253474715e-10,1.285932723177128e-10 +10000000,cable:3:core,cable:2:core,1.5377810805855499e-09,4.4664472044009375e-10 +10000000,cable:3:core,cable:3:core,1.2514510556301526,49.784417123982301 diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/unified/computation-options.txt b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/unified/computation-options.txt new file mode 100644 index 000000000..3c73cbbf9 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/unified/computation-options.txt @@ -0,0 +1 @@ +(trace = true, output_basis = :pul) \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/unified/details.txt b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/unified/details.txt new file mode 100644 index 000000000..b1f3bf32e --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/unified/details.txt @@ -0,0 +1 @@ +@NamedTuple{shunt_model::NamedTuple, trace::@NamedTuple{phase_map::Vector{Int64}, cable_map::Vector{Int64}, Zin::Array{ComplexF64, 3}, Pin::Array{ComplexF64, 3}, Zg::Array{ComplexF64, 3}, Pg::Array{ComplexF64, 3}, Z::Array{ComplexF64, 3}, P::Array{ComplexF64, 3}}, formulations::@NamedTuple{effective::@NamedTuple{internal_impedance::Union{Nothing, Symbol, NamedTuple}, insulation_impedance::Union{Nothing, Symbol, NamedTuple}, earth_impedance::Union{Nothing, Symbol, NamedTuple}, shunt_model::Union{Nothing, Symbol, NamedTuple}, insulation_admittance::Union{Nothing, Symbol, NamedTuple}, semicon_admittance::Union{Nothing, Symbol, NamedTuple}, earth_admittance::Union{Nothing, Symbol, NamedTuple}, earth_properties::Union{Nothing, Symbol, NamedTuple}, pipe_impedance::Union{Nothing, Symbol, NamedTuple}, temperature_dependence::Union{Nothing, Symbol, NamedTuple}}, numerical::@NamedTuple{internal_impedance::NamedTuple, insulation_impedance::NamedTuple, shunt_model::NamedTuple, insulation_admittance::NamedTuple, semicon_admittance::NamedTuple, earth_properties::NamedTuple, temperature_dependence::NamedTuple, earth_impedance::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}, earth_admittance::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}, equivalent_earth::@NamedTuple{earth_impedance::Union{Nothing, @NamedTuple{air::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, mixed::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth_admittance::Union{Nothing, @NamedTuple{air::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, mixed::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, backend::Symbol, requested::NamedTuple, methods::NamedTuple, options::NamedTuple}, coordinates::Vector{String}}(((requested = :coaxial, effective = :coaxial, solves = 0, domains = @NamedTuple{design::Int64, terminals::UnitRange{Int64}, requested::Symbol, effective::Symbol, reason::Symbol, message::String}[], diagnostics = @NamedTuple{boundary_residual::Union{Nothing, Float64}, wire_residual::Union{Nothing, Float64}, tape_residual::Union{Nothing, Float64}, common_residual::Union{Nothing, Float64}, penetration_indicator::Union{Nothing, Float64}, reciprocity::Float64, log_moment_error::Float64, unknowns::Int64, equations::Int64, matrix_bytes::Int64, level::@NamedTuple{wire::Int64, order::Int64, quadrature::Int64, modes::Int64}}[]), (phase_map = [1, 2, 3], cable_map = [1, 2, 3], Zin = ComplexF64[3.0384367161282473e-6 + 3.1415178272814096e-8im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 3.0384367161282473e-6 + 3.1415178272814096e-8im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 3.0384367161282473e-6 + 3.1415178272814096e-8im;;; 3.0491254273478614e-6 + 3.135993190598609e-7im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 3.0491254273478614e-6 + 3.135993190598609e-7im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 3.0491254273478614e-6 + 3.135993190598609e-7im;;; 3.886234398887704e-6 + 2.71311425444745e-6im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 3.886234398887704e-6 + 2.71311425444745e-6im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 3.886234398887704e-6 + 2.71311425444745e-6im;;; 1.0572845476093172e-5 + 9.718155377880437e-6im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 1.0572845476093172e-5 + 9.718155377880437e-6im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 1.0572845476093172e-5 + 9.718155377880437e-6im;;; 3.1668762815475746e-5 + 3.088048172738763e-5im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 3.1668762815475746e-5 + 3.088048172738763e-5im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 3.1668762815475746e-5 + 3.088048172738763e-5im;;; 9.846320725460222e-5 + 9.76946980065643e-5im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 9.846320725460222e-5 + 9.76946980065643e-5im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 9.846320725460222e-5 + 9.76946980065643e-5im;;; 0.0003097129699189164 + 0.0003089505796854997im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0003097129699189164 + 0.0003089505796854997im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0003097129699189164 + 0.0003089505796854997im;;; 0.0009777519922518445 + 0.0009769915236240853im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0009777519922518445 + 0.0009769915236240853im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0009777519922518445 + 0.0009769915236240853im;;; 0.0030902795943006773 + 0.0030895197319977846im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0030902795943006773 + 0.0030895197319977846im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0030902795943006773 + 0.0030895197319977846im], Pin = ComplexF64[0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im], Zg = ComplexF64[9.902483492522585e-8 + 1.2121689433473988e-6im 9.90236751893314e-8 + 8.152914901725591e-7im 9.868993364613281e-8 + 6.8496952228979e-7im; 9.902367518933138e-8 + 8.15291490172559e-7im 9.902483492522585e-8 + 1.2121689433473988e-6im 9.869209431437966e-8 + 7.145004290511119e-7im; 9.868993364613308e-8 + 6.849695222897884e-7im 9.869209431437964e-8 + 7.145004290511118e-7im 9.836632713561072e-8 + 1.2128339784098175e-6im;;; 9.971365375678585e-7 + 1.0667771267226147e-5im 9.970349565324145e-7 + 6.699005661656743e-6im 9.864661709948642e-7 + 5.403009768222351e-6im; 9.970349565324147e-7 + 6.699005661656744e-6im 9.971365375678583e-7 + 1.0667771267226144e-5im 9.866394025367692e-7 + 5.698292545468415e-6im; 9.864661709948642e-7 + 5.403009768222352e-6im 9.866394025367692e-7 + 5.698292545468415e-6im 9.766973949179014e-7 + 1.0688747596988266e-5im;;; 1.0173426110469357e-5 + 9.198626295899004e-5im 1.0164731798726483e-5 + 5.229953293046657e-5im 9.831785145126617e-6 + 3.956960553451868e-5im; 1.0164731798726483e-5 + 5.229953293046657e-5im 1.0173426110469355e-5 + 9.198626295899006e-5im 9.84482608122457e-6 + 4.2519810980107075e-5im; 9.831785145126619e-6 + 3.9569605534518666e-5im 9.844826081224572e-6 + 4.251981098010706e-5im 9.557640749329448e-6 + 9.26441799832742e-5im;;; 0.00010693099996229489 + 0.0007683782911024842im 0.00010621565159805986 + 0.0003716132784181228im 9.606212124185171e-5 + 0.00025171028261849007im; 0.00010621565159805986 + 0.0003716132784181228im 0.0001069309999622947 + 0.0007683782911024838im 9.693925872058956e-5 + 0.0002809547139605235im; 9.60621212418517e-5 + 0.00025171028261849im 9.693925872058954e-5 + 0.0002809547139605235im 8.97178099513887e-5 + 0.0007886380076276476im;;; 0.0011558607516577862 + 0.006051306459206652im 0.0011020546722505062 + 0.0020961818335963386im 0.0008344108784042162 + 0.0011330005183466704im; 0.001102054672250506 + 0.0020961818335963386im 0.0011558607516576037 + 0.006051306459206461im 0.0008805542421543221 + 0.0014022944498764082im; 0.0008344108784042161 + 0.0011330005183466704im 0.0008805542421543221 + 0.0014022944498764082im 0.0007568500056131768 + 0.006637591333445392im;;; 0.011118927142623457 + 0.0429499376184601im 0.00803403091260356 + 0.004866651343792169im 0.004055916633257924 + 0.001145030927804601im; 0.00803403091260356 + 0.004866651343792167im 0.01111892714257035 + 0.04294993761848052im 0.005267696823906846 + 0.002478373295818078im; 0.004055916633257925 + 0.0011450309278046007im 0.005267696823907293 + 0.002478373295817897im 0.005098431550629825 + 0.056800666407504266im;;; 0.09156046823835293 + 0.2849494713196441im 0.008728216258774114 - 0.008708792586514132im 0.0008790893547832312 - 0.0031017341595444914im; 0.008728216258774112 - 0.008708792586514132im 0.09156046823920042 + 0.284949471319227im 0.0029200092671962086 - 0.00553060204995752im; 0.0008790893547832143 - 0.0031017341595444797im 0.0029200092671962086 - 0.005530602049957521im 0.024719732726616407 + 0.5147087516945646im;;; 0.7530966559322566 + 1.573070953542643im 0.0001535709764889112 + 0.0010629809235061856im 8.125749933193525e-5 + 8.942664206185241e-5im; 0.0001535709764889112 + 0.0010629809235061854im 0.7530966559322357 + 1.5730709535426377im 0.00017862565014624984 + 0.00022534477815653242im; 8.125749933193531e-5 + 8.942664206185252e-5im 0.0001786256501462497 + 0.000225344778156534im 0.09371835769744857 + 4.942037056751943im;;; 4.544844426202893 + 6.668315490675154im 3.049234249244463e-9 + 1.314231864570851e-8im 6.825944125347454e-10 + 1.2859327231771216e-10im; 3.049234249244463e-9 + 1.314231864570851e-8im 4.544844426202893 + 6.668315490675154im 1.5377810805855466e-9 + 4.4664472044009214e-10im; 6.825944125347471e-10 + 1.285932723177128e-10im 1.5377810805855499e-9 + 4.4664472044009375e-10im 1.248360776035852 + 49.7813276042503im], Pg = ComplexF64[0.01570745005023129 + 0.1444616792842265im 0.015707199774202586 + 0.11176348050276326im 0.01566743794533898 + 0.09905602295340873im; 0.015707199774202586 + 0.11176348050276325im 0.015707450050231293 + 0.14446167928422657im 0.015668046278581316 + 0.10375596654056676im; 0.016248015136087715 + 0.09840937986798146im 0.016248746875902706 + 0.10310932298863519im 6.922928489069867e10 + 0.10538021488669605im;;; 0.157037855870973 + 1.2143641150778552im 0.15701737331905474 + 0.8873849172970905im 0.15576274847249483 + 0.7611721980509771im; 0.15701737331905477 + 0.8873849172970908im 0.15703785587097296 + 1.2143641150778548im 0.15581001431818906 + 0.8081633580148528im; 0.17245091632070542 + 0.7421922651374685im 0.1725104213942204 + 0.7891832798644063im 6.922928489085417e10 + 0.8127136789853869im;;; 1.5675656005751466 + 9.841634050447217im 1.56597178766981 + 6.572120710716003im 1.5268502890592122 + 5.337398400794161im; 1.5659717876698098 + 6.572120710716004im 1.5675656005751466 + 9.841634050447219im 1.5302348267576622 + 5.806502856515im; 1.989641525799069 + 4.785280553470411im 1.9942181491037119 + 5.254340987792914im 6.922928489267546e10 + 5.513001458814889im;;; 15.478748776504279 + 75.44737760966156im 15.364749279477117 + 42.779843996494904im 14.20506954568251 + 31.307362770786696im; 15.364749279477119 + 42.7798439964949im 15.478748776504247 + 75.44737760966149im 14.413975777454764 + 35.92388795975219im; 25.83091087993872 + 15.562738366536419im 26.149213879547386 + 20.166855504911123im 6.922928491879378e10 + 23.201801028249033im;;; 143.36993536445237 + 529.563981967786im 136.44825405615757 + 205.4851134010069im 108.10186004750466 + 117.63325206189941im; 136.44825405615447 + 205.48511340100336im 143.3699353644261 + 529.5639819677624im 117.49012590414134 + 158.03362859036645im; 317.24667736959424 - 300.25663824713456im 334.7655665261152 - 262.704547578839im 6.922928543754706e10 - 252.87532604360237im;;; 1030.4672887349896 + 3387.2637297979145im 738.8450359099868 + 336.1646045727079im 384.94088191452704 + 43.8391418791161im; 738.8450359091655 + 336.1646045732113im 1030.4672887292404 + 3387.2637298010536im 567.8140356674952 + 196.00101480311338im; 154.02443357987366 - 7692.066854289178im 570.9030427351584 - 7882.199664577669im 6.922930091239403e10 - 12412.037970378013im;;; 7244.073338563591 + 22669.337786005228im 681.2764993445954 - 676.6058133009168im 49.700711220481516 - 226.68367918927154im; 681.2764993494047 - 676.6058133093304im 7244.073338635197 + 22669.337785957556im 208.72520669654764 - 502.4842763521081im; -44784.81918754502 - 4991.020116856682im -49106.809247916804 - 7656.902405331411im 6.922997897279495e10 - 416791.30687939224im;;; 59930.23996636944 + 125180.7029401647im 12.216545310584559 + 84.5741344947635im 5.982806013775304 + 6.99961596430051im; 12.216545310536256 + 84.5741344948037im 59930.23996636774 + 125180.70294016432im 14.731275378157445 + 17.649506674802318im; 5509.81310472989 - 11306.342705549288im 7169.303043637193 - 12667.171050102057im 6.92655456770063e10 - 1.8875207195415117e7im;;; 361696.7481463518 + 530627.5641232601im 0.00024270853323619257 + 0.0010458189856197419im 5.366851808728216e-5 + 1.04457218949963e-5im; 0.00024270853323619262 + 0.0010458189856197419im 361696.7481463518 + 530627.5641232601im 0.00012242285718003855 + 3.4999788662782536e-5im; -0.20449588493526663 - 0.24220645996069157im -0.17200170824752853 - 0.35914384934587784im 7.074904147370567e10 - 1.3357478277705052e9im], Z = ComplexF64[3.137461551053473e-6 + 1.243584121620213e-6im 9.90236751893314e-8 + 8.152914901725591e-7im 9.868993364613281e-8 + 6.8496952228979e-7im; 9.902367518933138e-8 + 8.15291490172559e-7im 3.137461551053473e-6 + 1.243584121620213e-6im 9.869209431437966e-8 + 7.145004290511119e-7im; 9.868993364613308e-8 + 6.849695222897884e-7im 9.869209431437964e-8 + 7.145004290511118e-7im 3.136803043263858e-6 + 1.2442491566826316e-6im;;; 4.04626196491572e-6 + 1.0981370586286009e-5im 9.970349565324145e-7 + 6.699005661656743e-6im 9.864661709948642e-7 + 5.403009768222351e-6im; 9.970349565324147e-7 + 6.699005661656744e-6im 4.04626196491572e-6 + 1.0981370586286005e-5im 9.866394025367692e-7 + 5.698292545468415e-6im; 9.864661709948642e-7 + 5.403009768222352e-6im 9.866394025367692e-7 + 5.698292545468415e-6im 4.025822822265763e-6 + 1.1002346916048127e-5im;;; 1.405966050935706e-5 + 9.46993772134375e-5im 1.0164731798726483e-5 + 5.229953293046657e-5im 9.831785145126617e-6 + 3.956960553451868e-5im; 1.0164731798726483e-5 + 5.229953293046657e-5im 1.405966050935706e-5 + 9.469937721343751e-5im 9.84482608122457e-6 + 4.2519810980107075e-5im; 9.831785145126619e-6 + 3.9569605534518666e-5im 9.844826081224572e-6 + 4.251981098010706e-5im 1.3443875148217152e-5 + 9.535729423772166e-5im;;; 0.00011750384543838806 + 0.0007780964464803647im 0.00010621565159805986 + 0.0003716132784181228im 9.606212124185171e-5 + 0.00025171028261849007im; 0.00010621565159805986 + 0.0003716132784181228im 0.00011750384543838787 + 0.0007780964464803642im 9.693925872058956e-5 + 0.0002809547139605235im; 9.60621212418517e-5 + 0.00025171028261849im 9.693925872058954e-5 + 0.0002809547139605235im 0.00010029065542748186 + 0.000798356163005528im;;; 0.001187529514473262 + 0.00608218694093404im 0.0011020546722505062 + 0.0020961818335963386im 0.0008344108784042162 + 0.0011330005183466704im; 0.001102054672250506 + 0.0020961818335963386im 0.0011875295144730795 + 0.006082186940933849im 0.0008805542421543221 + 0.0014022944498764082im; 0.0008344108784042161 + 0.0011330005183466704im 0.0008805542421543221 + 0.0014022944498764082im 0.0007885187684286525 + 0.00666847181517278im;;; 0.01121739034987806 + 0.04304763231646667im 0.00803403091260356 + 0.004866651343792169im 0.004055916633257924 + 0.001145030927804601im; 0.00803403091260356 + 0.004866651343792167im 0.011217390349824953 + 0.04304763231648708im 0.005267696823906846 + 0.002478373295818078im; 0.004055916633257925 + 0.0011450309278046007im 0.005267696823907293 + 0.002478373295817897im 0.005196894757884428 + 0.05689836110551083im;;; 0.09187018120827184 + 0.2852584218993296im 0.008728216258774114 - 0.008708792586514132im 0.0008790893547832312 - 0.0031017341595444914im; 0.008728216258774112 - 0.008708792586514132im 0.09187018120911933 + 0.2852584218989125im 0.0029200092671962086 - 0.00553060204995752im; 0.0008790893547832143 - 0.0031017341595444797im 0.0029200092671962086 - 0.005530602049957521im 0.025029445696535323 + 0.5150177022742501im;;; 0.7540744079245084 + 1.5740479450662672im 0.0001535709764889112 + 0.0010629809235061856im 8.125749933193525e-5 + 8.942664206185241e-5im; 0.0001535709764889112 + 0.0010629809235061854im 0.7540744079244875 + 1.5740479450662619im 0.00017862565014624984 + 0.00022534477815653242im; 8.125749933193531e-5 + 8.942664206185252e-5im 0.0001786256501462497 + 0.000225344778156534im 0.09469610968970042 + 4.9430140482755665im;;; 4.547934705797194 + 6.671405010407152im 3.049234249244463e-9 + 1.314231864570851e-8im 6.825944125347454e-10 + 1.2859327231771216e-10im; 3.049234249244463e-9 + 1.314231864570851e-8im 4.547934705797194 + 6.671405010407152im 1.5377810805855466e-9 + 4.4664472044009214e-10im; 6.825944125347471e-10 + 1.285932723177128e-10im 1.5377810805855499e-9 + 4.4664472044009375e-10im 1.2514510556301526 + 49.7844171239823im], P = ComplexF64[0.01570745005023129 + 0.1444616792842265im 0.015707199774202586 + 0.11176348050276326im 0.01566743794533898 + 0.09905602295340873im; 0.015707199774202586 + 0.11176348050276325im 0.015707450050231293 + 0.14446167928422657im 0.015668046278581316 + 0.10375596654056676im; 0.016248015136087715 + 0.09840937986798146im 0.016248746875902706 + 0.10310932298863519im 6.922928489069867e10 + 0.10538021488669605im;;; 0.157037855870973 + 1.2143641150778552im 0.15701737331905474 + 0.8873849172970905im 0.15576274847249483 + 0.7611721980509771im; 0.15701737331905477 + 0.8873849172970908im 0.15703785587097296 + 1.2143641150778548im 0.15581001431818906 + 0.8081633580148528im; 0.17245091632070542 + 0.7421922651374685im 0.1725104213942204 + 0.7891832798644063im 6.922928489085417e10 + 0.8127136789853869im;;; 1.5675656005751466 + 9.841634050447217im 1.56597178766981 + 6.572120710716003im 1.5268502890592122 + 5.337398400794161im; 1.5659717876698098 + 6.572120710716004im 1.5675656005751466 + 9.841634050447219im 1.5302348267576622 + 5.806502856515im; 1.989641525799069 + 4.785280553470411im 1.9942181491037119 + 5.254340987792914im 6.922928489267546e10 + 5.513001458814889im;;; 15.478748776504279 + 75.44737760966156im 15.364749279477117 + 42.779843996494904im 14.20506954568251 + 31.307362770786696im; 15.364749279477119 + 42.7798439964949im 15.478748776504247 + 75.44737760966149im 14.413975777454764 + 35.92388795975219im; 25.83091087993872 + 15.562738366536419im 26.149213879547386 + 20.166855504911123im 6.922928491879378e10 + 23.201801028249033im;;; 143.36993536445237 + 529.563981967786im 136.44825405615757 + 205.4851134010069im 108.10186004750466 + 117.63325206189941im; 136.44825405615447 + 205.48511340100336im 143.3699353644261 + 529.5639819677624im 117.49012590414134 + 158.03362859036645im; 317.24667736959424 - 300.25663824713456im 334.7655665261152 - 262.704547578839im 6.922928543754706e10 - 252.87532604360237im;;; 1030.4672887349896 + 3387.2637297979145im 738.8450359099868 + 336.1646045727079im 384.94088191452704 + 43.8391418791161im; 738.8450359091655 + 336.1646045732113im 1030.4672887292404 + 3387.2637298010536im 567.8140356674952 + 196.00101480311338im; 154.02443357987366 - 7692.066854289178im 570.9030427351584 - 7882.199664577669im 6.922930091239403e10 - 12412.037970378013im;;; 7244.073338563591 + 22669.337786005228im 681.2764993445954 - 676.6058133009168im 49.700711220481516 - 226.68367918927154im; 681.2764993494047 - 676.6058133093304im 7244.073338635197 + 22669.337785957556im 208.72520669654764 - 502.4842763521081im; -44784.81918754502 - 4991.020116856682im -49106.809247916804 - 7656.902405331411im 6.922997897279495e10 - 416791.30687939224im;;; 59930.23996636944 + 125180.7029401647im 12.216545310584559 + 84.5741344947635im 5.982806013775304 + 6.99961596430051im; 12.216545310536256 + 84.5741344948037im 59930.23996636774 + 125180.70294016432im 14.731275378157445 + 17.649506674802318im; 5509.81310472989 - 11306.342705549288im 7169.303043637193 - 12667.171050102057im 6.92655456770063e10 - 1.8875207195415117e7im;;; 361696.7481463518 + 530627.5641232601im 0.00024270853323619257 + 0.0010458189856197419im 5.366851808728216e-5 + 1.04457218949963e-5im; 0.00024270853323619262 + 0.0010458189856197419im 361696.7481463518 + 530627.5641232601im 0.00012242285718003855 + 3.4999788662782536e-5im; -0.20449588493526663 - 0.24220645996069157im -0.17200170824752853 - 0.35914384934587784im 7.074904147370567e10 - 1.3357478277705052e9im]), @NamedTuple{effective::@NamedTuple{internal_impedance::Union{Nothing, Symbol, NamedTuple}, insulation_impedance::Union{Nothing, Symbol, NamedTuple}, earth_impedance::Union{Nothing, Symbol, NamedTuple}, shunt_model::Union{Nothing, Symbol, NamedTuple}, insulation_admittance::Union{Nothing, Symbol, NamedTuple}, semicon_admittance::Union{Nothing, Symbol, NamedTuple}, earth_admittance::Union{Nothing, Symbol, NamedTuple}, earth_properties::Union{Nothing, Symbol, NamedTuple}, pipe_impedance::Union{Nothing, Symbol, NamedTuple}, temperature_dependence::Union{Nothing, Symbol, NamedTuple}}, numerical::@NamedTuple{internal_impedance::NamedTuple, insulation_impedance::NamedTuple, shunt_model::NamedTuple, insulation_admittance::NamedTuple, semicon_admittance::NamedTuple, earth_properties::NamedTuple, temperature_dependence::NamedTuple, earth_impedance::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}, earth_admittance::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}, equivalent_earth::@NamedTuple{earth_impedance::Union{Nothing, @NamedTuple{air::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, mixed::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth_admittance::Union{Nothing, @NamedTuple{air::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, mixed::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, backend::Symbol, requested::NamedTuple, methods::NamedTuple, options::NamedTuple}((@NamedTuple{internal_impedance::Union{Nothing, Symbol, NamedTuple}, insulation_impedance::Union{Nothing, Symbol, NamedTuple}, earth_impedance::Union{Nothing, Symbol, NamedTuple}, shunt_model::Union{Nothing, Symbol, NamedTuple}, insulation_admittance::Union{Nothing, Symbol, NamedTuple}, semicon_admittance::Union{Nothing, Symbol, NamedTuple}, earth_admittance::Union{Nothing, Symbol, NamedTuple}, earth_properties::Union{Nothing, Symbol, NamedTuple}, pipe_impedance::Union{Nothing, Symbol, NamedTuple}, temperature_dependence::Union{Nothing, Symbol, NamedTuple}}((:schelkunoff1934, :ametani1980, :unified, :coaxial, :lossless, :lossless, :unified, :constant, :none, :linear)), @NamedTuple{internal_impedance::NamedTuple, insulation_impedance::NamedTuple, shunt_model::NamedTuple, insulation_admittance::NamedTuple, semicon_admittance::NamedTuple, earth_properties::NamedTuple, temperature_dependence::NamedTuple, earth_impedance::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}, earth_admittance::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}(((outer = NamedTuple(),), NamedTuple(), NamedTuple(), NamedTuple(), NamedTuple(), NamedTuple(), NamedTuple(), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}[@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :self, 2, 2, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),))), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :mutual, 2, 2, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),))), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :mutual, 2, 1, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),))), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :mutual, 1, 2, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),))), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :self, 1, 1, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),)))], @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}[@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :self, 2, 2, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),))), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :mutual, 2, 2, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),))), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :mutual, 2, 1, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),))), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :mutual, 1, 2, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),))), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :self, 1, 1, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),)))])), @NamedTuple{earth_impedance::Union{Nothing, @NamedTuple{air::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, mixed::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth_admittance::Union{Nothing, @NamedTuple{air::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, mixed::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}((nothing, nothing)), :coaxial, (internal_impedance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), insulation_impedance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_impedance = (identifier = :unified, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), shunt_model = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), insulation_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), semicon_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_admittance = (identifier = :unified, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_properties = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), pipe_impedance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), temperature_dependence = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing)), (internal_impedance = (identifier = :schelkunoff1934, parameters = NamedTuple(), options = (inner = NamedTuple(), outer = NamedTuple(), transfer = NamedTuple()), configured_options = ()), insulation_impedance = (identifier = :ametani1980, parameters = NamedTuple(), options = NamedTuple()), earth_impedance = (identifier = :unified, assumptions = (media = Val{:homogeneous}(), layers = 2:2, longitudinal = :prescribed, permittivity = :positive), parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), shunt_model = (identifier = :coaxial, parameters = NamedTuple(), options = NamedTuple()), insulation_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), semicon_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), earth_admittance = (identifier = :unified, assumptions = (media = Val{:homogeneous}(), layers = 2:2, longitudinal = :prescribed, permittivity = :positive), parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_properties = (identifier = :constant, parameters = NamedTuple(), options = NamedTuple()), pipe_impedance = (identifier = :none, parameters = NamedTuple(), options = NamedTuple()), temperature_dependence = (identifier = :linear, parameters = NamedTuple(), options = NamedTuple())), (reduce_bundle = false, kron_reduction = false, ideal_transposition = false))), ["cable:1:core", "cable:2:core", "cable:3:core"])) \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/unified/formulation.txt b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/unified/formulation.txt new file mode 100644 index 000000000..1a6917548 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/unified/formulation.txt @@ -0,0 +1 @@ +@NamedTuple{backend::Symbol, requested::NamedTuple, methods::NamedTuple, options::NamedTuple}((:coaxial, (internal_impedance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), insulation_impedance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_impedance = (identifier = :unified, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), shunt_model = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), insulation_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), semicon_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_admittance = (identifier = :unified, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_properties = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), pipe_impedance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), temperature_dependence = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing)), (internal_impedance = (identifier = :schelkunoff1934, parameters = NamedTuple(), options = (inner = NamedTuple(), outer = NamedTuple(), transfer = NamedTuple()), configured_options = ()), insulation_impedance = (identifier = :ametani1980, parameters = NamedTuple(), options = NamedTuple()), earth_impedance = (identifier = :unified, assumptions = (media = Val{:homogeneous}(), layers = 2:2, longitudinal = :prescribed, permittivity = :positive), parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), shunt_model = (identifier = :coaxial, parameters = NamedTuple(), options = NamedTuple()), insulation_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), semicon_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), earth_admittance = (identifier = :unified, assumptions = (media = Val{:homogeneous}(), layers = 2:2, longitudinal = :prescribed, permittivity = :positive), parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_properties = (identifier = :constant, parameters = NamedTuple(), options = NamedTuple()), pipe_impedance = (identifier = :none, parameters = NamedTuple(), options = NamedTuple()), temperature_dependence = (identifier = :linear, parameters = NamedTuple(), options = NamedTuple())), (reduce_bundle = false, kron_reduction = false, ideal_transposition = false))) \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/unified/problem.json b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/unified/problem.json new file mode 100644 index 000000000..08854494a --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_air_3/unified/problem.json @@ -0,0 +1,516 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "root": { + "kind": "line_parameters_problem", + "earth_props": { + "kind": "earth_model", + "vertical_layers": false, + "layers": [ + { + "kind": "earth_layer", + "thickness": { + "special": "Inf", + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "rho": { + "special": "Inf", + "__type__": "Float64" + } + }, + { + "kind": "earth_layer", + "thickness": { + "special": "Inf", + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "rho": { + "value": 0.1, + "__type__": "Float64" + } + } + ] + }, + "Gamma": null, + "frequencies": [ + { + "value": 0.1, + "__type__": "Float64" + }, + { + "value": 1, + "__type__": "Float64" + }, + { + "value": 10, + "__type__": "Float64" + }, + { + "value": 100, + "__type__": "Float64" + }, + { + "value": 1000, + "__type__": "Float64" + }, + { + "value": 10000, + "__type__": "Float64" + }, + { + "value": 100000, + "__type__": "Float64" + }, + { + "value": 1000000, + "__type__": "Float64" + }, + { + "value": 10000000, + "__type__": "Float64" + } + ], + "system": { + "kind": "line_cable_system", + "input_positions": [ + { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 1, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 2, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 1, + "__type__": "Float64" + } + } + ], + "clearances": [ + [ + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + } + ], + [ + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + } + ], + [ + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + } + ] + ], + "connections": [ + [ + 1 + ], + [ + 2 + ], + [ + 3 + ] + ], + "line_length": { + "value": 1, + "__type__": "Float64" + }, + "environment": null, + "system_id": "three_bare_wires_air_3", + "positions": [ + { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 1, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 2, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 1, + "__type__": "Float64" + } + } + ], + "designs": [ + { + "kind": "cable_design", + "cable_id": "three_bare_wires_air_3", + "nominal_data": [ + ], + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "core_metal", + "material": { + "value": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "type": "material" + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.0425, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + } + ] + } + }, + { + "kind": "cable_design", + "cable_id": "three_bare_wires_air_3", + "nominal_data": [ + ], + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "core_metal", + "material": { + "value": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "type": "material" + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.0425, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + } + ] + } + }, + { + "kind": "cable_design", + "cable_id": "three_bare_wires_air_3", + "nominal_data": [ + ], + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "core_metal", + "material": { + "value": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "type": "material" + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.0425, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + } + ] + } + } + ] + }, + "temperature": { + "value": 20, + "__type__": "Float64" + } + }, + "format": "linecablemodels.line_parameters_problem", + "version": "1.0.0" +} \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/Y-differences.csv b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/Y-differences.csv new file mode 100644 index 000000000..318c00414 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/Y-differences.csv @@ -0,0 +1,82 @@ +frequency_hz,response_terminal,basis_terminal,absolute_error,relative_error,real_absolute_error,real_relative_error,imaginary_absolute_error,imaginary_relative_error,real_reference_near_zero,imaginary_reference_near_zero +0.10000000000000001,cable:1:core,cable:1:core,6.1225368622909116e-14,0.0063951164588853379,1.0462974512145288e-25,NaN,6.1225368622909116e-14,0.0063951164588853379,true,false +0.10000000000000001,cable:1:core,cable:2:core,1.5315115104407224e-14,0.0080326915168015788,8.6241252674396925e-26,NaN,1.5315115104407224e-14,0.0080326915168015788,true,false +0.10000000000000001,cable:1:core,cable:3:core,6.2597318528151031e-15,0.013401523019709932,1.0563253352523985e-25,NaN,6.2597318528151031e-15,0.013401523019709932,true,false +0.10000000000000001,cable:2:core,cable:1:core,1.5315115104408032e-14,0.0080326915168020003,8.6241249330900139e-26,NaN,1.5315115104408032e-14,0.0080326915168020003,true,false +0.10000000000000001,cable:2:core,cable:2:core,6.1197496095808435e-14,0.0061644783194101382,6.7804278212834814e-26,NaN,6.1197496095808435e-14,0.0061644783194101382,true,false +0.10000000000000001,cable:2:core,cable:3:core,1.5432436189108823e-14,0.0080937277227594688,8.5384256123071786e-26,NaN,1.5432436189108823e-14,0.0080937277227594688,true,false +0.10000000000000001,cable:3:core,cable:1:core,6.2597318528153051e-15,0.013401523019710355,1.0563253352523838e-25,NaN,6.2597318528153051e-15,0.013401523019710355,true,false +0.10000000000000001,cable:3:core,cable:2:core,1.5432436189108823e-14,0.0080937277227594688,8.5384259466577387e-26,NaN,1.5432436189108823e-14,0.0080937277227594688,true,false +0.10000000000000001,cable:3:core,cable:3:core,6.0583846057079155e-14,0.0063285321638092695,1.033276796336783e-25,NaN,6.0583846057079155e-14,0.0063285321638092695,true,false +1,cable:1:core,cable:1:core,6.0492647308112628e-13,0.0063190658262049764,1.9799423473533405e-23,NaN,6.0492647308112628e-13,0.0063190658262049764,true,false +1,cable:1:core,cable:2:core,1.4947921278843586e-13,0.0078416106500505903,1.6676612561022131e-23,NaN,1.4947921278843586e-13,0.0078416106500505903,true,false +1,cable:1:core,cable:3:core,5.9773786693830928e-14,0.012804770634278937,1.98987500664447e-23,NaN,5.9773786693830928e-14,0.012804770634278937,true,false +1,cable:2:core,cable:1:core,1.4947921278843262e-13,0.0078416106500504221,1.6676609242069409e-23,NaN,1.4947921278843262e-13,0.0078416106500504221,true,false +1,cable:2:core,cable:2:core,6.0725055630592353e-13,0.0061171800829458323,1.3675653473031361e-23,NaN,6.0725055630592353e-13,0.0061171800829458323,true,false +1,cable:2:core,cable:3:core,1.5292738825915747e-13,0.0080210494217538789,1.6526357064427424e-23,NaN,1.5292738825915747e-13,0.0080210494217538789,true,false +1,cable:3:core,cable:1:core,5.9773786693824466e-14,0.012804770634277568,1.9898750066444324e-23,NaN,5.9773786693824466e-14,0.012804770634277568,true,false +1,cable:3:core,cable:2:core,1.5292738825915424e-13,0.0080210494217537107,1.652636038337977e-23,NaN,1.5292738825915424e-13,0.0080210494217537107,true,false +1,cable:3:core,cable:3:core,5.9276570341715909e-13,0.0061928210313361935,1.9618899841925868e-23,NaN,5.9276570341715909e-13,0.0061928210313361935,true,false +10,cable:1:core,cable:1:core,6.116284247154873e-12,0.0063886271971195022,4.8757523727096107e-21,NaN,6.116284247154873e-12,0.0063886271971195022,true,false +10,cable:1:core,cable:2:core,1.5639650987747745e-12,0.0082015127182378418,4.1254521841388295e-21,NaN,1.5639650987747745e-12,0.0082015127182378418,true,false +10,cable:1:core,cable:3:core,6.2663506194790321e-13,0.013415503099214725,4.8823482537433071e-21,NaN,6.2663506194790321e-13,0.013415503099214725,true,false +10,cable:2:core,cable:1:core,1.5639650987748004e-12,0.0082015127182379736,4.1254489424420749e-21,NaN,1.5639650987748004e-12,0.0082015127182379736,true,false +10,cable:2:core,cable:2:core,6.1533251192670879e-12,0.0061980896045749781,3.4667723756717502e-21,NaN,6.1533251192670879e-12,0.0061980896045749781,true,false +10,cable:2:core,cable:3:core,1.5736209531590275e-12,0.0082517306780178143,4.1242315559046296e-21,NaN,1.5736209531590275e-12,0.0082517306780178143,true,false +10,cable:3:core,cable:1:core,6.2663506194788382e-13,0.013415503099214316,4.882348253743277e-21,NaN,6.2663506194788382e-13,0.013415503099214316,true,false +10,cable:3:core,cable:2:core,1.5736209531590275e-12,0.0082517306780178143,4.1242347976014323e-21,NaN,1.5736209531590275e-12,0.0082517306780178143,true,false +10,cable:3:core,cable:3:core,6.057629487848298e-12,0.0063277483660878322,4.8594432761764409e-21,NaN,6.057629487848298e-12,0.0063277483660878322,true,false +100,cable:1:core,cable:1:core,6.0724523806168252e-11,0.0063431340192774055,1.3366429338008849e-18,0.38800043739524753,6.0724523806168239e-11,0.0063431340192774037,false,false +100,cable:1:core,cable:2:core,1.628773963911108e-11,0.008538471809772543,1.1338300226348507e-18,0.43777769494589497,1.6287739639111041e-11,0.0085384718097725222,false,false +100,cable:1:core,cable:3:core,6.5494340296448564e-12,0.014013057995825596,1.334144950532159e-18,0.56443872954499619,6.5494340296447207e-12,0.014013057995825307,false,false +100,cable:2:core,cable:1:core,1.6287739639118525e-11,0.0085384718097764496,1.1338270162393275e-18,0.43777653416087775,1.6287739639118486e-11,0.0085384718097764288,false,false +100,cable:2:core,cable:2:core,6.1053024670704833e-11,0.006150015084583951,9.6087299588595953e-19,0.38258564583449739,6.105302467070482e-11,0.0061500150845839501,false,false +100,cable:2:core,cable:3:core,1.5689226772019991e-11,0.0082272966129350242,1.1341987758353015e-18,0.43785722319184334,1.5689226772019949e-11,0.0082272966129350034,false,false +100,cable:3:core,cable:1:core,6.5494340296450632e-12,0.01401305799582603,1.3341449505321588e-18,0.56443872954499608,6.5494340296449275e-12,0.01401305799582574,false,false +100,cable:3:core,cable:2:core,1.5689226772012133e-11,0.0082272966129309025,1.1342017822308234e-18,0.43785838381026709,1.5689226772012091e-11,0.0082272966129308799,false,false +100,cable:3:core,cable:3:core,5.9987219354081907e-11,0.0062665996490304148,1.3362058015155947e-18,0.38792277050528939,5.9987219354081894e-11,0.006266599649030413,false,false +1000,cable:1:core,cable:1:core,5.5919113347770054e-10,0.005844106233878054,3.7820756782824836e-16,1.9819420287542049,5.5919113347757263e-10,0.0058441062338767166,false,false +1000,cable:1:core,cable:2:core,1.6473963406097957e-10,0.0086352521603473963,3.200323499202485e-16,2.4511473187216084,1.6473963406066871e-10,0.0086352521603311021,false,false +1000,cable:1:core,cable:3:core,6.532237988805393e-11,0.013976778712139707,3.7290393354025568e-16,4.0553886633684,6.5322379886989542e-11,0.013976778711911966,false,false +1000,cable:2:core,cable:1:core,1.6473963406499303e-10,0.008635252160557768,3.2002997348205955e-16,2.4511291174363441,1.6473963406468219e-10,0.0086352521605414737,false,false +1000,cable:2:core,cable:2:core,5.9321909067939758e-10,0.0059766779575333882,2.7272997364668807e-16,1.9420010050263212,5.9321909067933492e-10,0.0059766779575327568,false,false +1000,cable:2:core,cable:3:core,1.685193579588281e-10,0.0088316263652770521,3.2002377653363901e-16,2.4511979953660239,1.6851935795852424e-10,0.0088316263652611273,false,false +1000,cable:3:core,cable:1:core,6.5322379888054757e-11,0.013976778712139882,3.7290393354025558e-16,4.0553886633684035,6.5322379886990369e-11,0.013976778711912141,false,false +1000,cable:3:core,cable:2:core,1.68519357954808e-10,0.0088316263650663786,3.2002615297182801e-16,2.4512161975152145,1.6851935795450414e-10,0.0088316263650504538,false,false +1000,cable:3:core,cable:3:core,5.6950543534875172e-10,0.0059512594825530587,3.7824111245565547e-16,1.9817694476601253,5.6950543534862609e-10,0.0059512594825517464,false,false +10000,cable:1:core,cable:1:core,5.8131135007922797e-09,0.0060738812868803057,1.0908057176635004e-13,13.518091179575167,5.8131134997688546e-09,0.006073881285810971,false,false +10000,cable:1:core,cable:2:core,1.5465585756411843e-09,0.0081109678538234479,9.110849440623336e-14,19.831942888955716,1.5465585729575621e-09,0.0081109678397491211,false,false +10000,cable:1:core,cable:3:core,5.2674430226161561e-10,0.011301102482028603,1.0321563498931522e-13,57.96519353180247,5.26744292149055e-10,0.011301102265067397,false,false +10000,cable:2:core,cable:1:core,1.5465585769013903e-09,0.0081109678604326368,9.1107251928188937e-14,19.831672433892454,1.5465585742178412e-09,0.0081109678463586951,false,false +10000,cable:2:core,cable:2:core,6.0130303576453305e-09,0.0060576308604776852,7.8778601020149337e-14,12.917896022162155,6.013030357129279e-09,0.006057630859957806,false,false +10000,cable:2:core,cable:3:core,1.5640146556058312e-09,0.0082017658701096577,9.110680635473151e-14,19.833499089695582,1.5640146529522594e-09,0.0082017658561942038,false,false +10000,cable:3:core,cable:1:core,5.2674430226160238e-10,0.011301102482028317,1.0321563498931526e-13,57.965193531802569,5.2674429214904177e-10,0.011301102265067109,false,false +10000,cable:3:core,cable:2:core,1.5640146543456509e-09,0.0082017658635012061,9.1108048832775884e-14,19.83376957099275,1.5640146516920067e-09,0.0082017658495853758,false,false +10000,cable:3:core,cable:3:core,5.8626336901066072e-09,0.0061253059465156941,1.0908245207537849e-13,13.515174866673451,5.8626336890917922e-09,0.0061253059454554112,false,false +100000,cable:1:core,cable:1:core,5.770762385174679e-08,0.0060299338192602095,3.4865628978388312e-11,133.70647911993981,5.770761331923727e-08,0.0060299327187065743,false,false +100000,cable:1:core,cable:2:core,1.5869077542343327e-08,0.008320626940059429,2.8568578813507331e-11,215.51379850263339,1.5869051826785678e-08,0.0083206134566316207,false,false +100000,cable:1:core,cable:3:core,5.3614809796148935e-09,0.011499400133850181,3.176143166995537e-11,666.62090293285235,5.361386901374546e-09,0.011499198353160585,false,false +100000,cable:2:core,cable:1:core,1.5869077853760675e-08,0.0083206271033447587,2.8568205964404224e-11,215.5109858276665,1.5869052138874756e-08,0.0083206136202691582,false,false +100000,cable:2:core,cable:2:core,6.0118305568099178e-08,0.006056455646043615,2.5285058850992882e-11,124.93441451008843,6.0118300250798368e-08,0.0060564551103665671,false,false +100000,cable:2:core,cable:3:core,1.5579339640163257e-08,0.0081699499513747647,2.8568245049070324e-11,215.44775711205577,1.5579313446969897e-08,0.0081699362154216727,false,false +100000,cable:3:core,cable:1:core,5.3614809796149993e-09,0.011499400133850413,3.1761431669955364e-11,666.62090293285166,5.3613869013746518e-09,0.011499198353160817,false,false +100000,cable:3:core,cable:2:core,1.5579339328757355e-08,0.0081699497880706289,2.856861789817347e-11,215.45056895796623,1.557931313487976e-08,0.008169936051758718,false,false +100000,cable:3:core,cable:3:core,5.8119123653714196e-08,0.0060726707782011525,3.4865691158964131e-11,133.67484190342532,5.8119113195740532e-08,0.0060726696854828364,false,false +1000000,cable:1:core,cable:1:core,6.000075943997591e-07,0.006270104250335193,1.5603952811020256e-08,1937.6380625065212,5.9980465986357362e-07,0.0062679835760141691,false,false +1000000,cable:1:core,cable:2:core,1.5125215038988852e-07,0.0079246637817000452,1.2866175699018907e-08,3076.6140249069763,1.507039300099617e-07,0.0078959404731191851,false,false +1000000,cable:1:core,cable:3:core,4.9305640920684913e-08,0.010540560497534986,1.4545890414896714e-08,8977.105224634046,4.711118018726908e-08,0.010071428656064472,false,false +1000000,cable:2:core,cable:1:core,1.5125223474544779e-07,0.0079246682014021338,1.286607407373948e-08,3076.5897238429088,1.5070402334848627e-07,0.0078959453634723006,false,false +1000000,cable:2:core,cable:2:core,6.1779838047781191e-07,0.0062242701385392465,1.131455882962669e-08,1816.5037994581603,6.1769476254821697e-07,0.0062232261960405806,false,false +1000000,cable:2:core,cable:3:core,1.5365851055144858e-07,0.008049723465093573,1.2866073892881139e-08,3076.7227414950648,1.531189145973565e-07,0.0080214555989161201,false,false +1000000,cable:3:core,cable:1:core,4.9305640920685727e-08,0.010540560497535158,1.4545890414896724e-08,8977.1052246340587,4.7111180187269927e-08,0.010071428656064653,false,false +1000000,cable:3:core,cable:2:core,1.5365842605000035e-07,0.0080497190383079145,1.2866175518160561e-08,3076.7470436101357,1.5311882125884887e-07,0.0080214507091825645,false,false +1000000,cable:3:core,cable:3:core,6.0679168782415467e-07,0.006340548650427295,1.560395353779806e-08,1937.46329972383,6.0659102288648868e-07,0.006338451842205918,false,false +10000000,cable:1:core,cable:1:core,1.0371784536962841e-05,0.011092226863015033,1.0245256608837238e-05,36347.070614800323,1.6151258465699854e-06,0.0017273153176898173,false,false +10000000,cable:1:core,cable:2:core,1.166487233752054e-05,0.056787647952458817,8.4134256720760269e-06,55728.199315763173,8.0798214776753546e-06,0.039334683168131129,false,false +10000000,cable:1:core,cable:3:core,1.2609467382028214e-05,0.21068315503871385,9.4192382621511205e-06,133557.64156328657,8.383115066564564e-06,0.14006786153354073,false,false +10000000,cable:2:core,cable:1:core,1.1664877473773147e-05,0.056787672957079247,8.4133981838769422e-06,55728.017241552683,8.0798575159266495e-06,0.039334858611759511,false,false +10000000,cable:2:core,cable:2:core,7.3486162844970809e-06,0.007528205982756048,7.3111105864650451e-06,34465.747116801125,7.4150070077810565e-07,0.00075962192006024678,false,false +10000000,cable:2:core,cable:3:core,1.1637631532530349e-05,0.056665897511365693,8.4133982058298745e-06,55719.915146567408,8.0404725182717544e-06,0.039150628750350032,false,false +10000000,cable:3:core,cable:1:core,1.260946738202819e-05,0.21068315503871363,9.4192382621511137e-06,133557.64156328616,8.3831150665645369e-06,0.14006786153354039,false,false +10000000,cable:3:core,cable:2:core,1.1637626506216432e-05,0.056665873037262962,8.4134256940289558e-06,55720.097194306421,8.0404364800205138e-06,0.039150453273076515,false,false +10000000,cable:3:core,cable:3:core,1.034563946082993e-05,0.011062166621899204,1.0245256605320489e-05,36347.524087524682,1.4376970976594022e-06,0.001537270354949533,false,false diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/Z-differences.csv b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/Z-differences.csv new file mode 100644 index 000000000..75244f049 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/Z-differences.csv @@ -0,0 +1,82 @@ +frequency_hz,response_terminal,basis_terminal,absolute_error,relative_error,real_absolute_error,real_relative_error,imaginary_absolute_error,imaginary_relative_error,real_reference_near_zero,imaginary_reference_near_zero +0.10000000000000001,cable:1:core,cable:1:core,2.0421687531063286e-08,0.006023318557434835,1.9480476992451874e-08,0.0061719666598822167,6.1283225896601503e-09,0.0049496967488917749,false,false +0.10000000000000001,cable:1:core,cable:2:core,3.2089882152586086e-09,0.003919779856153092,1.3008348782821628e-10,0.0013241980477614965,3.2063505191826856e-09,0.0039450629294037615,false,false +0.10000000000000001,cable:1:core,cable:3:core,2.8459320665693526e-09,0.0038845644572714816,1.3355984979257542e-10,0.0013596597050290144,2.8427963511393303e-09,0.0039156405027265849,false,false +0.10000000000000001,cable:2:core,cable:1:core,3.2089882152586082e-09,0.0039197798561530911,1.3008348782820304e-10,0.0013241980477613617,3.2063505191826856e-09,0.0039450629294037615,false,false +0.10000000000000001,cable:2:core,cable:2:core,2.0333646480624677e-08,0.005997150910064389,1.9482137631376542e-08,0.0061724895497332639,5.8226705652236887e-09,0.0047016683704622485,false,false +0.10000000000000001,cable:2:core,cable:3:core,3.200226523490131e-09,0.0039090359121177433,1.3016923691393027e-10,0.0013250720959140118,3.1975781102908145e-09,0.00393422697760119,false,false +0.10000000000000001,cable:3:core,cable:1:core,2.8459320665703076e-09,0.00388456445727279,1.3355984979264159e-10,0.001359659705029689,2.8427963511402832e-09,0.0039156405027279025,false,false +0.10000000000000001,cable:3:core,cable:2:core,3.2002265234907684e-09,0.0039090359121185247,1.3016923691399645e-10,0.0013250720959146864,3.1975781102914497e-09,0.0039342269776019749,false,false +0.10000000000000001,cable:3:core,cable:3:core,2.0408956621405036e-08,0.0060195365806944637,1.9480326189516824e-08,0.006171919176128627,6.0862469490168901e-09,0.0049155462324226981,false,false +1,cable:1:core,cable:1:core,5.5071217378795182e-08,0.004717699299481245,1.8307367899385977e-08,0.0045268949560578146,5.1939188135540247e-08,0.004743128778540882,false,false +1,cable:1:core,cable:2:core,2.3104205898647905e-08,0.0034139587900371472,1.2561300500812365e-09,0.0012878149161753873,2.3070033972762145e-08,0.0034448772096392884,false,false +1,cable:1:core,cable:3:core,1.9416667176051124e-08,0.0032850688975510005,1.5944904400124783e-09,0.0016355159549059492,1.9351086906482795e-08,0.003319440175309712,false,false +1,cable:2:core,cable:1:core,2.3104205898646201e-08,0.0034139587900368948,1.2561300500810247e-09,0.0012878149161751698,2.3070033972760451e-08,0.0034448772096390347,false,false +1,cable:2:core,cable:2:core,5.23359514746707e-08,0.0044822879684521307,1.8477133701228792e-08,0.0045686814580810382,4.8965777303602577e-08,0.0044703806285773754,false,false +1,cable:2:core,cable:3:core,2.2795672648706442e-08,0.0033682167140297956,1.2575856586531114e-09,0.001289309165366433,2.276095713756636e-08,0.0033985682050529822,false,false +1,cable:3:core,cable:1:core,1.9416667176047709e-08,0.0032850688975504206,1.5944904400120547e-09,0.001635515954905514,1.9351086906479407e-08,0.0033194401753091286,false,false +1,cable:3:core,cable:2:core,2.2795672648703931e-08,0.0033682167140294231,1.2575856586535349e-09,0.0012893091653668678,2.2760957137563819e-08,0.0033985682050526019,false,false +1,cable:3:core,cable:3:core,5.4047088154091449e-08,0.0046295626551396338,1.830631208547952e-08,0.004526635064884159,5.0852400884964703e-08,0.004643421643146791,false,false +10,cable:1:core,cable:1:core,4.2634777286456104e-07,0.0044466141225671524,2.0383144272500343e-08,0.0015138502730194809,4.258602480346549e-07,0.0044859815271325555,false,false +10,cable:1:core,cable:2:core,1.4982334088850044e-07,0.0027919058276288339,9.0612896944036655e-09,0.00094929050515883131,1.4954907724244206e-07,0.0028319553810874643,false,false +10,cable:1:core,cable:3:core,1.0052960079539756e-07,0.0022257418472240091,2.162713035054446e-08,0.0022711083597624204,9.8175698973231441e-08,0.0022236089100505295,false,false +10,cable:2:core,cable:1:core,1.4982334088848671e-07,0.0027919058276285771,9.0612896944002773e-09,0.00094929050515847602,1.4954907724242851e-07,0.0028319553810872066,false,false +10,cable:2:core,cable:2:core,4.0647004685357752e-07,0.004238371968339167,2.6722462873797972e-08,0.0019837356643082676,4.0559069142068335e-07,0.004271551407768269,false,false +10,cable:2:core,cable:3:core,1.4833091919156415e-07,0.0027640190818798002,9.0278377678162133e-09,0.00094578265680026949,1.4805593447563095e-07,0.0028036010185150673,false,false +10,cable:3:core,cable:1:core,1.0052960079537881e-07,0.0022257418472235928,2.1627130350549542e-08,0.0022711083597629551,9.8175698973211112e-08,0.002223608910050068,false,false +10,cable:3:core,cable:2:core,1.4833091919156415e-07,0.0027640190818798002,9.0278377678162133e-09,0.00094578265680026971,1.4805593447563095e-07,0.0028036010185150673,false,false +10,cable:3:core,cable:3:core,4.2465241001491889e-07,0.0044288546208002873,2.0384495669959043e-08,0.0015139504889687128,4.2416287162805777e-07,0.0044680215965215786,false,false +100,cable:1:core,cable:1:core,4.4382160815824066e-06,0.0055450428979991924,7.9406483940174411e-07,0.0078548935858579298,4.3666031440517438e-06,0.0054996129142431197,false,false +100,cable:1:core,cable:2:core,7.878593130789853e-07,0.0019640058494340357,9.4564761748763024e-08,0.0010575446619718019,7.8216353983082739e-07,0.0020001313201422014,false,false +100,cable:1:core,cable:3:core,2.3666842789824236e-07,0.00074551025733565681,1.4363588360412891e-07,0.0016182183335687256,1.8809752179411261e-07,0.00061712418931066137,false,false +100,cable:2:core,cable:1:core,7.8785931307908483e-07,0.0019640058494342846,9.4564761748695261e-08,0.0010575446619710434,7.8216353983093581e-07,0.0020001313201424794,false,false +100,cable:2:core,cable:2:core,4.3109535319546705e-06,0.0053851244828224459,8.1984684146332041e-07,0.0081078614923036762,4.2322773433714229e-06,0.0053295317272390357,false,false +100,cable:2:core,cable:3:core,8.0250516839758451e-07,0.0020005879183127391,9.5709397946816695e-08,0.0010703591560039954,7.9677741964082624e-07,0.0020375777555337849,false,false +100,cable:3:core,cable:1:core,2.3666842789815618e-07,0.00074551025733538522,1.4363588360412891e-07,0.0016182183335687256,1.8809752179400419e-07,0.00061712418931030542,false,false +100,cable:3:core,cable:2:core,8.0250516839758938e-07,0.0020005879183127516,9.5709397946857353e-08,0.0010703591560044508,7.9677741964082624e-07,0.0020375777555337849,false,false +100,cable:3:core,cable:3:core,4.4192709679411133e-06,0.0055212405402839532,7.944569284383025e-07,0.0078587416521812507,4.3472743273163198e-06,0.0054751355300965317,false,false +1000,cable:1:core,cable:1:core,5.1441022724376449e-05,0.0077199694890550587,2.6025662955141571e-06,0.0033110814677694135,5.1375144453395631e-05,0.0077642912366139519,false,false +1000,cable:1:core,cable:2:core,2.9953594179309089e-06,0.0010796764074255197,1.1959987687666434e-06,0.0016034535150578251,2.7462274100474755e-06,0.0010277168652870197,false,false +1000,cable:1:core,cable:3:core,2.3533695335462284e-06,0.0012000804519261116,1.1894047228886307e-06,0.001655190645107506,2.0306808135681521e-06,0.0011129421385364928,false,false +1000,cable:2:core,cable:1:core,2.9953594179324912e-06,0.0010796764074260908,1.1959987687676192e-06,0.0016034535150591354,2.7462274100487766e-06,0.0010277168652875071,false,false +1000,cable:2:core,cable:2:core,5.1779731182568038e-05,0.0077711865836618413,2.51460394403867e-06,0.0031988145223710109,5.1718636180236172e-05,0.0078166086849163121,false,false +1000,cable:2:core,cable:3:core,2.9944771118823559e-06,0.0010793571711456673,1.2101884888297658e-06,0.0016225082777543481,2.7390394292691779e-06,0.0010250241599728367,false,false +1000,cable:3:core,cable:1:core,2.3533695335473604e-06,0.0012000804519266882,1.1894047228893896e-06,0.0016551906451085638,2.0306808135690194e-06,0.0011129421385369677,false,false +1000,cable:3:core,cable:2:core,2.9944771118816041e-06,0.001079357171145396,1.21018848883085e-06,0.0016225082777558044,2.7390394292678769e-06,0.0010250241599723492,false,false +1000,cable:3:core,cable:3:core,5.1975334218482814e-05,0.0078007802258625103,2.6332499492694389e-06,0.0033502491345675117,5.1908586590540713e-05,0.0078455424880475733,false,false +10000,cable:1:core,cable:1:core,0.00041249482111915353,0.0072719246745931469,9.240517016477056e-05,0.018110134858219169,0.00040201151970676446,0.0071159598575711771,false,false +10000,cable:1:core,cable:2:core,5.8404910968574194e-05,0.0032390616548324314,2.4487903390712183e-05,0.005100546388945474,5.3023355351903417e-05,0.0030507345718507736,false,false +10000,cable:1:core,cable:3:core,2.3302240927691969e-05,0.0022274199513152054,2.0415572518963851e-05,0.0049326663533755567,1.1233825304638248e-05,0.0011692154952621797,false,false +10000,cable:2:core,cable:1:core,5.840491096858013e-05,0.0032390616548327619,2.4487903390711316e-05,0.0051005463889452927,5.3023355351910356e-05,0.0030507345718511739,false,false +10000,cable:2:core,cable:2:core,0.00040820808163423238,0.0071958204857737705,9.1098524381754559e-05,0.017849479389219707,0.00039791317742312454,0.0070429046478802408,false,false +10000,cable:2:core,cable:3:core,5.8120308347846536e-05,0.0032232205619593434,2.4590552420412914e-05,0.0051220365040542401,5.2661892997761528e-05,0.0030298745788641165,false,false +10000,cable:3:core,cable:1:core,2.3302240927693565e-05,0.0022274199513153581,2.0415572518964718e-05,0.0049326663533757666,1.1233825304639983e-05,0.0011692154952623603,false,false +10000,cable:3:core,cable:2:core,5.8120308347844707e-05,0.0032232205619592415,2.4590552420408578e-05,0.0051220365040533329,5.2661892997761528e-05,0.0030298745788641165,false,false +10000,cable:3:core,cable:3:core,0.00041436133979592309,0.0073050687451673936,9.2777916483635994e-05,0.01818451641517799,0.00040384103076634981,0.0071485753224736987,false,false +100000,cable:1:core,cable:1:core,0.0031650281739091352,0.0061760586330841874,0.00032345519322843699,0.013091233221723102,0.0031484567774724415,0.0061508752390419053,false,false +100000,cable:1:core,cable:2:core,0.00037019878430946077,0.0028985127206438191,4.1206122245930621e-05,0.0019116117695098329,0.00036789834926737042,0.0029224233906190423,false,false +100000,cable:1:core,cable:3:core,4.9423630082577968e-05,0.00080297659635237694,1.6656115333050711e-05,0.0010723982359216674,4.6532451392029828e-05,0.00078128765990084119,false,false +100000,cable:2:core,cable:1:core,0.00037019878430949026,0.0028985127206440503,4.1206122245947968e-05,0.0019116117695106389,0.00036789834926739817,0.002922423390619263,false,false +100000,cable:2:core,cable:2:core,0.0031364926471985645,0.0061200262228040874,0.00033458315394417279,0.013547717973225485,0.0031185958761961174,0.0060921831138666281,false,false +100000,cable:2:core,cable:3:core,0.00038701841948086171,0.0030305987949418313,4.0635338410914545e-05,0.0018850823636139269,0.00038487923598148899,0.003057724571635544,false,false +100000,cable:3:core,cable:1:core,4.9423630082536911e-05,0.00080297659635170928,1.6656115333064589e-05,0.0010723982359225619,4.6532451391981255e-05,0.00078128765990002501,false,false +100000,cable:3:core,cable:2:core,0.00038701841948086171,0.0030305987949418313,4.0635338410914545e-05,0.0018850823636139269,0.00038487923598148899,0.003057724571635544,false,false +100000,cable:3:core,cable:3:core,0.003190897013032208,0.0062268539233451305,0.00032239416296917303,0.013047729730733641,0.0031745685929683853,0.0062022040867988227,false,false +1000000,cable:1:core,cable:1:core,0.027706003291211687,0.0056356078961270102,0.0017228033997900155,0.018530100121009148,0.027652388085268598,0.0056257082504430024,false,false +1000000,cable:1:core,cable:2:core,0.0017180998509297644,0.001569398093823283,0.00083107494740822241,0.010830321555015619,0.0015037225573739654,0.0013769620127795502,false,false +1000000,cable:1:core,cable:3:core,0.0018525607014287209,0.0037652024876764761,0.00075269034613903441,0.014906298844995438,0.0016927605841663418,0.0034586819344899519,false,false +1000000,cable:2:core,cable:1:core,0.0017180998509301396,0.0015693980938236267,0.00083107494740819465,0.010830321555015253,0.0015037225573744095,0.0013769620127799577,false,false +1000000,cable:2:core,cable:2:core,0.026981972938736398,0.0054875216991341746,0.0017947387452811325,0.019318776304155894,0.02692221715429266,0.0054763462973970393,false,false +1000000,cable:2:core,cable:3:core,0.0016575335857403374,0.0015139776156323829,0.00083139733094654555,0.010834568279668534,0.0014339442339059705,0.0013129819558710281,false,false +1000000,cable:3:core,cable:1:core,0.0018525607014285659,0.003765202487676163,0.00075269034613902747,0.014906298844995298,0.0016927605841661753,0.0034586819344896132,false,false +1000000,cable:3:core,cable:2:core,0.0016575335857406729,0.0015139776156326899,0.00083139733094644841,0.010834568279667259,0.0014339442339064146,0.0013129819558714351,false,false +1000000,cable:3:core,cable:3:core,0.027969378436457814,0.0056894854502846327,0.0017277821236851149,0.018584645405745094,0.027915961367197539,0.0056796351687205676,false,false +10000000,cable:1:core,cable:1:core,1.7374214206238898,0.033823129904337439,0.72011245207975794,1.3555829368144476,1.5811613608997703,0.030782794224993932,false,false +10000000,cable:1:core,cable:2:core,1.9485072302278281,0.14899376034263651,0.71400623821048659,1.5295978739049265,1.81297421880363,0.13871854263585123,false,false +10000000,cable:1:core,cable:3:core,1.9323038886674515,0.27666089794727172,0.70250850636208262,1.9227731742851586,1.8000778084983633,0.25808256426085302,false,false +10000000,cable:2:core,cable:1:core,1.9485072302278461,0.1489937603426377,0.71400623821048592,1.529597873904923,1.8129742188036495,0.13871854263585254,false,false +10000000,cable:2:core,cable:2:core,1.7038115972825583,0.033193943564225004,0.72333123474448058,1.3700702159726974,1.5426489827169263,0.030055736361019866,false,false +10000000,cable:2:core,cable:3:core,1.8928829072696307,0.14542953948724663,0.71870739680553353,1.5553330820454596,1.7511325987516031,0.13462378026046248,false,false +10000000,cable:3:core,cable:1:core,1.9323038886674555,0.27666089794727211,0.70250850636208217,1.9227731742851566,1.8000778084983677,0.25808256426085352,false,false +10000000,cable:3:core,cable:2:core,1.8928829072696207,0.14542953948724599,0.71870739680553331,1.5553330820454594,1.7511325987515924,0.13462378026046179,false,false +10000000,cable:3:core,cable:3:core,1.6277903595325474,0.031766857627179998,0.72948176651725516,1.397875124361436,1.4551830836379196,0.028399843359741325,false,false diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/fem/Y.csv b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/fem/Y.csv new file mode 100644 index 000000000..e91c4a645 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/fem/Y.csv @@ -0,0 +1,82 @@ +frequency_hz,response_terminal,basis_terminal,real_S_per_m,imaginary_S_per_m +0.10000000000000001,cable:1:core,cable:1:core,8.922128316011062e-24,9.5737691434599193e-12 +0.10000000000000001,cable:1:core,cable:2:core,7.2386070908688633e-24,-1.9065981897068207e-12 +0.10000000000000001,cable:1:core,cable:3:core,7.8258392890097311e-24,-4.6709107939513815e-13 +0.10000000000000001,cable:2:core,cable:1:core,7.2386070908688677e-24,-1.9065981897068211e-12 +0.10000000000000001,cable:2:core,cable:2:core,6.4613855928501904e-24,9.9274412083039412e-12 +0.10000000000000001,cable:2:core,cable:3:core,7.2377500976610394e-24,-1.9067155107915219e-12 +0.10000000000000001,cable:3:core,cable:1:core,7.8258392890097296e-24,-4.6709107939513845e-13 +0.10000000000000001,cable:3:core,cable:2:core,7.2377500976610423e-24,-1.9067155107915219e-12 +0.10000000000000001,cable:3:core,cable:3:core,8.9208262505232889e-24,9.5731276208940894e-12 +1,cable:1:core,cable:1:core,7.0481422084003561e-22,9.5730364221324353e-11 +1,cable:1:core,cable:2:core,5.6476607782501255e-22,-1.9062309958920426e-11 +1,cable:1:core,cable:3:core,5.9520194157421592e-22,-4.6680872622437967e-12 +1,cable:2:core,cable:1:core,5.6476607782501142e-22,-1.9062309958920426e-11 +1,cable:2:core,cable:2:core,5.1101837548125129e-22,9.9269687678295667e-11 +1,cable:2:core,cable:3:core,5.6461582564736953e-22,-1.9065758134391154e-11 +1,cable:3:core,cable:1:core,5.9520194157421583e-22,-4.6680872622437911e-12 +1,cable:3:core,cable:2:core,5.6461582564737038e-22,-1.9065758134391151e-11 +1,cable:3:core,cable:3:core,7.0463369720842808e-22,9.5718203451660386e-11 +10,cable:1:core,cable:1:core,5.2042336276529873e-20,9.5737066171470717e-10 +10,cable:1:core,cable:2:core,4.0811616664672256e-20,-1.9069227257271197e-10 +10,cable:1:core,cable:3:core,4.1095181352562286e-20,-4.6709769832231127e-11 +10,cable:2:core,cable:1:core,4.0811616664672268e-20,-1.9069227257271205e-10 +10,cable:2:core,cable:2:core,3.7800341164145078e-20,9.9277769632842221e-10 +10,cable:2:core,cable:3:core,4.0810399278134823e-20,-1.9070192842709623e-10 +10,cable:3:core,cable:1:core,4.1095181352562256e-20,-4.6709769832231108e-11 +10,cable:3:core,cable:2:core,4.0810399278134859e-20,-1.9070192842709623e-10 +10,cable:3:core,cable:3:core,5.2026027179996697e-20,9.5731200695540039e-10 +100,cable:1:core,cable:1:core,3.4449521314309137e-18,9.5732682963375009e-09 +100,cable:1:core,cable:2:core,2.5899675468275766e-18,-1.9075708161815633e-09 +100,cable:1:core,cable:3:core,2.3636665605984129e-18,-4.6738078380863707e-10 +100,cable:2:core,cable:1:core,2.589967546827577e-18,-1.9075708161815624e-09 +100,cable:2:core,cable:2:core,2.5115239067323068e-18,9.92729673521363e-09 +100,cable:2:core,cable:3:core,2.590339306423551e-18,-1.9069723033144639e-09 +100,cable:3:core,cable:1:core,2.3636665605984129e-18,-4.6738078380863738e-10 +100,cable:3:core,cable:2:core,2.5903393064235491e-18,-1.9069723033144643e-09 +100,cable:3:core,cable:3:core,3.4445149991456232e-18,9.5725309918854146e-09 +1000,cable:1:core,cable:1:core,1.9082675595006146e-16,9.568462842719928e-08 +1000,cable:1:core,cable:2:core,1.3056430654978372e-16,-1.9077570753226516e-08 +1000,cable:1:core,cable:3:core,9.195269911084729e-17,-4.673636267226392e-09 +1000,cable:2:core,cable:1:core,1.3056430654978367e-16,-1.9077570753226526e-08 +1000,cable:2:core,cable:2:core,1.4043760685025576e-16,9.9255655883493975e-08 +1000,cable:2:core,cable:3:core,1.3055810960136316e-16,-1.9081350477120368e-08 +1000,cable:3:core,cable:1:core,9.1952699110847192e-17,-4.6736362672263928e-09 +1000,cable:3:core,cable:2:core,1.3055810960136316e-16,-1.9081350477120351e-08 +1000,cable:3:core,cable:3:core,1.9086030057746862e-16,9.5694942729070334e-08 +10000,cable:1:core,cable:1:core,8.0692288813055717e-15,9.5706735548959623e-07 +10000,cable:1:core,cable:2:core,4.594027671235939e-15,-1.9067497289021409e-07 +10000,cable:1:core,cable:3:core,1.7806485012887294e-15,-4.6609992529424619e-08 +10000,cable:2:core,cable:1:core,4.5940276712359398e-15,-1.9067497289021396e-07 +10000,cable:2:core,cable:2:core,6.0984080445449842e-15,9.9263730262546271e-07 +10000,cable:2:core,cable:3:core,4.5935820977784853e-15,-1.9069242896894837e-07 +10000,cable:3:core,cable:1:core,1.7806485012887271e-15,-4.6609992529424632e-08 +10000,cable:3:core,cable:2:core,4.5935820977784813e-15,-1.9069242896894851e-07 +10000,cable:3:core,cable:3:core,8.0711091903339482e-15,9.5711687567891938e-07 +100000,cable:1:core,cable:1:core,2.6076244926853928e-13,9.5701919094738434e-06 +100000,cable:1:core,cable:2:core,1.325603233389172e-13,-1.9071973370109952e-06 +100000,cable:1:core,cable:3:core,4.7645418153283817e-14,-4.6624005749939556e-07 +100000,cable:2:core,cable:1:core,1.3256032333891698e-13,-1.9071973370109952e-06 +100000,cable:2:core,cable:2:core,2.023866598338372e-13,9.9263181440735071e-06 +100000,cable:2:core,cable:3:core,1.3259940800503111e-13,-1.90690759831909e-06 +100000,cable:3:core,cable:1:core,4.7645418153283849e-14,-4.6624005749939535e-07 +100000,cable:3:core,cable:2:core,1.3259940800503118e-13,-1.9069075983190895e-06 +100000,cable:3:core,cable:3:core,2.6082462984435911e-13,9.5706034093503467e-06 +1000000,cable:1:core,cable:1:core,8.0530792168868948e-12,9.5693400052747324e-05 +1000000,cable:1:core,cable:2:core,4.1819271429109229e-12,-1.9086254578921379e-05 +1000000,cable:1:core,cable:3:core,1.620331950101396e-12,-4.6777057948875268e-06 +1000000,cable:2:core,cable:1:core,4.1819271429109229e-12,-1.9086254578921386e-05 +1000000,cable:2:core,cable:2:core,6.228755939294857e-12,9.9256357247823405e-05 +1000000,cable:2:core,cable:3:core,4.1817462845641913e-12,-1.9088669470170267e-05 +1000000,cable:3:core,cable:1:core,1.6203319501013948e-12,-4.6777057948875277e-06 +1000000,cable:3:core,cable:2:core,4.1817462845641962e-12,-1.908866947017027e-05 +1000000,cable:3:core,cable:3:core,8.05380599468505e-12,9.5700186415770239e-05 +10000000,cable:1:core,cable:1:core,2.8187296625399645e-10,0.00093504980244725748 +10000000,cable:1:core,cable:2:core,1.5097250181016025e-10,-0.00020541214081067286 +10000000,cable:1:core,cable:3:core,7.0525640853636929e-11,-5.9850382341684698e-05 +10000000,cable:2:core,cable:1:core,1.5097250181016004e-10,-0.00020541214081067278 +10000000,cable:2:core,cable:2:core,2.121268563158776e-10,0.00097614442289829665 +10000000,cable:2:core,cable:3:core,1.5099445474206141e-10,-0.00020537275581301788 +10000000,cable:3:core,cable:1:core,7.0525640853637097e-11,-5.9850382341684651e-05 +10000000,cable:3:core,cable:2:core,1.5099445474206126e-10,-0.00020537275581301799 +10000000,cable:3:core,cable:3:core,2.8186944950225374e-10,0.00093522723119616807 diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/fem/Z.csv b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/fem/Z.csv new file mode 100644 index 000000000..36bc03581 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/fem/Z.csv @@ -0,0 +1,82 @@ +frequency_hz,response_terminal,basis_terminal,real_ohm_per_m,imaginary_ohm_per_m +0.10000000000000001,cable:1:core,cable:1:core,3.1562835747436832e-06,1.2381208184182731e-06 +0.10000000000000001,cable:1:core,cable:2:core,9.8235674073162367e-08,8.1275015799742355e-07 +0.10000000000000001,cable:1:core,cable:3:core,9.8230350799228339e-08,7.2601055923285115e-07 +0.10000000000000001,cable:2:core,cable:1:core,9.823567407316238e-08,8.1275015799742355e-07 +0.10000000000000001,cable:2:core,cable:2:core,3.1562852353826078e-06,1.2384264704427096e-06 +0.10000000000000001,cable:2:core,cable:3:core,9.8235588324076653e-08,8.1275893040631542e-07 +0.10000000000000001,cable:3:core,cable:1:core,9.8230350799228273e-08,7.2601055923285019e-07 +0.10000000000000001,cable:3:core,cable:2:core,9.8235588324076587e-08,8.1275893040631479e-07 +0.10000000000000001,cable:3:core,cable:3:core,3.1562834239407481e-06,1.2381628940589164e-06 +1,cable:1:core,cable:1:core,4.0441335787761909e-06,1.0950406484961215e-05 +1,cable:1:core,cable:2:core,9.7539641318315375e-07,6.696910388622472e-06 +1,cable:1:core,cable:3:core,9.7491585773259541e-07,5.8296236366655687e-06 +1,cable:2:core,cable:1:core,9.7539641318315396e-07,6.6969103886224737e-06 +1,cable:2:core,cable:2:core,4.0443033445780337e-06,1.0953379895793153e-05 +1,cable:2:core,cable:3:core,9.7539495757458187e-07,6.6972194654576678e-06 +1,cable:3:core,cable:1:core,9.7491585773259584e-07,5.8296236366655721e-06 +1,cable:3:core,cable:2:core,9.7539495757458145e-07,6.6972194654576703e-06 +1,cable:3:core,cable:3:core,4.0441325229622845e-06,1.0951493272211791e-05 +10,cable:1:core,cable:1:core,1.3464438746537811e-05,9.4931342329192611e-05 +10,cable:1:core,cable:2:core,9.5453284797023948e-06,5.280770955685593e-05 +10,cable:1:core,cable:3:core,9.5227205948054659e-06,4.4151513573041263e-05 +10,cable:2:core,cable:1:core,9.5453284797023982e-06,5.2807709556855943e-05 +10,cable:2:core,cable:2:core,1.3470778065139108e-05,9.4951611885806569e-05 +10,cable:2:core,cable:3:core,9.545361931628984e-06,5.2809202699622741e-05 +10,cable:3:core,cable:1:core,9.5227205948054608e-06,4.4151513573041283e-05 +10,cable:3:core,cable:2:core,9.5453619316289823e-06,5.2809202699622741e-05 +10,cable:3:core,cable:3:core,1.346444009793527e-05,9.4933039705599208e-05 +100,cable:1:core,cable:1:core,0.00010109173736374871,0.00079398372433502344 +100,cable:1:core,cable:2:core,8.9419166063820077e-05,0.00039105609314453346 +100,cable:1:core,cable:3:core,8.8761745324787293e-05,0.00030479687079552149 +100,cable:2:core,cable:1:core,8.9419166063820144e-05,0.00039105609314453335 +100,cable:2:core,cable:2:core,0.00010111751936580979,0.00079411805013570192 +100,cable:2:core,cable:3:core,8.9418021427622023e-05,0.00039104147926472346 +100,cable:3:core,cable:1:core,8.8761745324787293e-05,0.0003047968707955216 +100,cable:3:core,cable:2:core,8.9418021427621982e-05,0.00039104147926472346 +100,cable:3:core,cable:3:core,0.00010109212945278526,0.00079400305315175887 +1000,cable:1:core,cable:1:core,0.00078601699198523078,0.0066168492252231129 +1000,cable:1:core,cable:2:core,0.00074588926809238511,0.0026721634166045451 +1000,cable:1:core,cable:3:core,0.00071859077164575073,0.001824605919080821 +1000,cable:2:core,cable:1:core,0.00074588926809238414,0.0026721634166045438 +1000,cable:2:core,cable:2:core,0.0007861049543362728,0.0066165057334950745 +1000,cable:2:core,cable:3:core,0.0007458750783723221,0.0026721706045853234 +1000,cable:3:core,cable:1:core,0.00071859077164575008,0.0018246059190808221 +1000,cable:3:core,cable:2:core,0.0007458750783723209,0.0026721706045853247 +1000,cable:3:core,cable:3:core,0.00078598630833147539,0.0066163157830859678 +10000,cable:1:core,cable:1:core,0.0051024009974631997,0.056494349006063566 +10000,cable:1:core,cable:2:core,0.0048010353251144531,0.017380520692016812 +10000,cable:1:core,cable:3:core,0.0041388512938834643,0.0096080024171414403 +10000,cable:2:core,cable:1:core,0.004801035325114454,0.017380520692016805 +10000,cable:2:core,cable:2:core,0.0051037076429676729,0.056498447347698745 +10000,cable:2:core,cable:3:core,0.0048009326760847524,0.017380882154370954 +10000,cable:3:core,cable:1:core,0.0041388512938834634,0.0096080024171414386 +10000,cable:3:core,cable:2:core,0.0048009326760847567,0.017380882154370954 +10000,cable:3:core,cable:3:core,0.0051020282511443343,0.05649251949500398 +100000,cable:1:core,cable:1:core,0.024707771051829367,0.51187134433942161 +100000,cable:1:core,cable:2:core,0.021555696037850045,0.12588810726341756 +100000,cable:1:core,cable:3:core,0.015531651186217865,0.059558666775737366 +100000,cable:2:core,cable:1:core,0.021555696037850031,0.12588810726341754 +100000,cable:2:core,cable:2:core,0.02469664297746775,0.5119012048567243 +100000,cable:2:core,cable:3:core,0.021556266821685061,0.12587112637670345 +100000,cable:3:core,cable:1:core,0.015531651186217852,0.059558666775737415 +100000,cable:3:core,cable:2:core,0.021556266821685061,0.12587112637670345 +100000,cable:3:core,cable:3:core,0.024708832082088631,0.51184523252392566 +1000000,cable:1:core,cable:1:core,0.092973237518383781,4.9153612050697939 +1000000,cable:1:core,cable:2:core,0.076735943913257512,1.092058127543065 +1000000,cable:1:core,cable:3:core,0.05049478438383373,0.48942360593674289 +1000000,cable:2:core,cable:1:core,0.07673594391325754,1.0920581275430643 +1000000,cable:2:core,cable:2:core,0.092901264398151592,4.9160910746438828 +1000000,cable:2:core,cable:3:core,0.076735621529719203,1.092127905866533 +1000000,cable:3:core,cable:1:core,0.050494784383833737,0.48942360593674267 +1000000,cable:3:core,cable:2:core,0.076735621529719272,1.0921279058665325 +1000000,cable:3:core,cable:3:core,0.092968258794488681,4.9150976317878659 +10000000,cable:1:core,cable:1:core,0.53121976717410324,51.365101859919996 +10000000,cable:1:core,cable:2:core,0.46679343008479252,13.069443957199377 +10000000,cable:1:core,cable:3:core,0.36536213202748608,6.9748137137964967 +10000000,cable:2:core,cable:1:core,0.46679343008479313,13.069443957199393 +10000000,cable:2:core,cable:2:core,0.52795194458770356,51.326274764561461 +10000000,cable:2:core,cable:3:core,0.46209227148974574,13.00760233714735 +10000000,cable:3:core,cable:1:core,0.36536213202748624,6.9748137137965012 +10000000,cable:3:core,cable:2:core,0.46209227148974569,13.007602337147338 +10000000,cable:3:core,cable:3:core,0.52185045273660624,51.239123582658166 diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/fem/computation-options.txt b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/fem/computation-options.txt new file mode 100644 index 000000000..987cdbcf4 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/fem/computation-options.txt @@ -0,0 +1 @@ +(ui = false, mesh_policy = :remesh, resume_run_directory = nothing, keep_run_directory = true, trace = true, output_basis = :pul, verbosity = (default = 1,), gmsh_verbosity = 2, getdp_verbosity = 4, frequency_workers = 2, solver_threads = 4, plot_field_maps = false) \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/fem/details.txt b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/fem/details.txt new file mode 100644 index 000000000..6645173ef --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/fem/details.txt @@ -0,0 +1 @@ +(files = [(path = "coordinator.lock", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/coordinator.lock", sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"), (path = "run.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/run.json", sha256 = "f763c2cf27056f3f92eca61940a4ab8913b464ffa696114c93731f8e4989adb7"), (path = "timing-summary.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/timing-summary.json", sha256 = "3cdb63131603d4dbf156d8a54e39690c85fc48cfba213586ed404e80a4c081eb"), (path = "attempts/f0001-7NRuy2/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0001-7NRuy2/attempt.json", sha256 = "0f2bbaf6f77f83e57852f9a8e4c0adecc60d15ee4e6373c51efa617504522555"), (path = "attempts/f0001-7NRuy2/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0001-7NRuy2/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0001-7NRuy2/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0001-7NRuy2/getdp.log", sha256 = "39f3d76d963c3271d0de811899e0b65320ef6c024b28393ec50e39798f1eb568"), (path = "attempts/f0001-7NRuy2/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0001-7NRuy2/petsc.log", sha256 = "f8b913b392c22084509e558e08596f91e424e07a3580a2cf92ff1cbce6db9ad9"), (path = "attempts/f0001-7NRuy2/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0001-7NRuy2/solver.pre", sha256 = "99dc9e3ec2c1f1b455c47a6dd9ab8676aefe770fafb313754b2d26b782afcabb"), (path = "attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0001-P.tsv", sha256 = "031a280c87ddc96904d1f86d1ef5415fca772c6b3ce3ce4300bdec75de98eeeb"), (path = "attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0001-Z.tsv", sha256 = "c2163b4e92999b1751331366e286a7b55c548f1d488467a05b277b4190b5bdad"), (path = "attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0001-timing.tsv", sha256 = "fec35bd592ad36327d68b415791e6dfc12587c27e2386f483cbff814ee9f7797"), (path = "attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0001.done", sha256 = "a40a11bab26b32dfb169550347f289dc710df24f59fe3ef20963d46f838f0586"), (path = "attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0002-P.tsv", sha256 = "86a470e1837a982ad0b60df3a9f2bc036f0b04fba5509a142e90d079273ac195"), (path = "attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0002-Z.tsv", sha256 = "3f340376a310d046c7a8ddf7c4edfa2c2ec4ffb1f2b39eba79352d247f3ae6bb"), (path = "attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0002-timing.tsv", sha256 = "f035f9a83a0bc955a45d10e11410210082cedf40a3d4e917c2e93d54f6a46733"), (path = "attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0002.done", sha256 = "f58042f034284ccf2955f2795037937a5bd0e4288ffc0d0d5124e33ee07f17ec"), (path = "attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0003-P.tsv", sha256 = "65167a9aa871d23ab936b1bb21bc509b8d74d94242e6297cfc2451ce806a00b3"), (path = "attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0003-Z.tsv", sha256 = "89773a7454a74c37898661a0397237a6a51c7b1225697078d3d358ce2680e5a9"), (path = "attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0003-timing.tsv", sha256 = "9133b1cb3a423655d0379c70af7a47034b559065727f192cb68af9312dc47e77"), (path = "attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0003.done", sha256 = "41695cf0039479bd9b703276e3c15366a77a1e43f8490b48634d8a1c2a61be94"), (path = "attempts/f0002-jWBavL/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0002-jWBavL/attempt.json", sha256 = "ce2919d3b38e7564d58ce864486030f275f1c3ac99e3f59a095b7575520e165e"), (path = "attempts/f0002-jWBavL/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0002-jWBavL/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0002-jWBavL/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0002-jWBavL/getdp.log", sha256 = "ebd30293a1cf71debce9742a4939a6f8285abc8aef4857ac0e83fdfc45785eb9"), (path = "attempts/f0002-jWBavL/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0002-jWBavL/petsc.log", sha256 = "4952a5630d94ef9e5576125f3711110714defc14aa9307a5139319b2f6b27f61"), (path = "attempts/f0002-jWBavL/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0002-jWBavL/solver.pre", sha256 = "310ee72c13d3183e8ac6d8cb00410b455758a676ee2ce377e2a0dfa6e5449b43"), (path = "attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0001-P.tsv", sha256 = "75360e62d1f6396fe7bf30be166ab6b76ced00b406d41e3cff7f921310f4638d"), (path = "attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0001-Z.tsv", sha256 = "97214f5c8ba29de45e9d35e462a2b3d6ef3eb9d0959812aa835286a0e33491f3"), (path = "attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0001-timing.tsv", sha256 = "067ab77f6136d789cb6e8a80b579d471e9917b0f82993ee0472f6b1033a40290"), (path = "attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0001.done", sha256 = "3dbd9539f415ee889a34b9c6efe6b9cb8168b7c0945d52d7e63e920c8990d2d1"), (path = "attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0002-P.tsv", sha256 = "790d625eddc047985df0af0c08e1984183df9bac9dc59a8107d67c0223829a68"), (path = "attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0002-Z.tsv", sha256 = "4a9daa553ea47fd0f118a09f27cf90351c79fc6f59aca92a9079e94b74950974"), (path = "attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0002-timing.tsv", sha256 = "493434a9b35cfb2bc991dc0ee378b19b6c0c3c3a8f18e67300fb8379ff3f0aa5"), (path = "attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0002.done", sha256 = "d498381e814531161686031fcde5aa549b456c6cbee61f6eb67916650247e717"), (path = "attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0003-P.tsv", sha256 = "9a0f04d6a64b9690bb4304da7520f2ebd37d7749d0c746d1a01b0d759641283b"), (path = "attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0003-Z.tsv", sha256 = "e0ab6f15a6d62ebef17c41c6f20586c61499c3898af3ea9d65801b1dd20450f6"), (path = "attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0003-timing.tsv", sha256 = "6ad68000f813c19cd34f8b178f16c5539b54ecfd269035c408d4fbce139dd9aa"), (path = "attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0003.done", sha256 = "fa5c0c361441906d1aaa6b254ae52969806be93c36846f4c70c70a18dc833050"), (path = "attempts/f0003-JRn69i/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0003-JRn69i/attempt.json", sha256 = "3f721255cd5e1bfe319820ea11e0ae9f92f96a49ac106a9204c085bfa8373502"), (path = "attempts/f0003-JRn69i/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0003-JRn69i/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0003-JRn69i/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0003-JRn69i/getdp.log", sha256 = "353e3a75bad8c1f6fa3570751456f76937d01f3359ccd94a9d35a55aa1534a82"), (path = "attempts/f0003-JRn69i/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0003-JRn69i/petsc.log", sha256 = "d4bd0b9603ae2ad0399c690c2410ad4cb4f51c5c485e7d2bbae2b8c9283323f9"), (path = "attempts/f0003-JRn69i/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0003-JRn69i/solver.pre", sha256 = "3cac9d6471bf7bb1fff80012cdc8f2fbdf842145fba4d71bd8dc1b0cec7dd6e2"), (path = "attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0001-P.tsv", sha256 = "8c89e9137f9157c4fcbb7f19ed185a3b1755fc8c475671f75a5c138cb326b187"), (path = "attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0001-Z.tsv", sha256 = "e392c6378c451328d736fae1d29e818251caf06e40928c154fb30f3abdca563a"), (path = "attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0001-timing.tsv", sha256 = "e62951a512bc6a0927b91df494b1b058821b7547d85dade8747fb960f9b210b0"), (path = "attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0001.done", sha256 = "02395cb824ac1c8ce1d2e8cd3c57fd19d5c904215aef49b1fba677c51e99d519"), (path = "attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0002-P.tsv", sha256 = "2e7b78e1e6203a93287a3758917f0f4e0a8dd5a2c163824e548d65bd8252009b"), (path = "attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0002-Z.tsv", sha256 = "48219f308edd90574abf7865948acb574d675bc223eaf505e034822a783162dd"), (path = "attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0002-timing.tsv", sha256 = "300f2d86ea6705d8df22e7131131c15d36476a8535f756e54bfeac8d6244dd03"), (path = "attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0002.done", sha256 = "84034583a44a97df0ec5820a76fc891b95a9b6bc0bf693bed9882420a3eb5b05"), (path = "attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0003-P.tsv", sha256 = "01a9abbfc7ac762dd2ca47f576452b9ab89f745a4736f9b5298c7059b67e0b4e"), (path = "attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0003-Z.tsv", sha256 = "998a2152fa3e61432a06dd2fb1549914a9bccfbd27a91ac88a28e69d155da0a8"), (path = "attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0003-timing.tsv", sha256 = "4244046606e8dfab3ab2cdd7a5a490edd70cbf2f780476a4fa83505836766943"), (path = "attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0003.done", sha256 = "aec9dd1bab4102d3c07f9c6abf83e4d604721961d8e37cbbc9620f7f71cafcae"), (path = "attempts/f0004-H0KlIr/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0004-H0KlIr/attempt.json", sha256 = "0a5f1a88dbf61ec72e760165f4e68165c027616d9e8ef4b449f76a562656e4ca"), (path = "attempts/f0004-H0KlIr/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0004-H0KlIr/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0004-H0KlIr/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0004-H0KlIr/getdp.log", sha256 = "0bff62190696542513d5212a1a54d5cfbef5badd8e2eed4cbf4f72fbe8b2c435"), (path = "attempts/f0004-H0KlIr/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0004-H0KlIr/petsc.log", sha256 = "dbd2fece68fedf85ceb1116de0e8001f8029e095de48dec5963f2ca984b94410"), (path = "attempts/f0004-H0KlIr/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0004-H0KlIr/solver.pre", sha256 = "84fc9a2a171fb180ee858b88ce101a1f4e7880f4b64622a09d64c5ede01879dd"), (path = "attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0001-P.tsv", sha256 = "5d4004862d3d0d80660a14391f0b1ff16ca269d36e14cf4d9eab44c1f0508b46"), (path = "attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0001-Z.tsv", sha256 = "4fa281428aa093aedd060a88d6a0395519fcd9704aa92d11e6f744217e04cb35"), (path = "attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0001-timing.tsv", sha256 = "bc38c85ddedbafe212c6c766d6156cf67b285f0f4d2017bf23aa875229230d4c"), (path = "attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0001.done", sha256 = "05b52afc2d2a0ffefd0bb6e40585165d1b2572547d59d3c360b4163202709006"), (path = "attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0002-P.tsv", sha256 = "dab42728b3411be92f90c8ea8b1b252e86bebcfda885ebf09268009a05f1cf79"), (path = "attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0002-Z.tsv", sha256 = "fa0c078e62461975905443eb2ce0bbf2bf4630dbb987a114389e8a57a8da87dd"), (path = "attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0002-timing.tsv", sha256 = "2836501b252019196717fecf2a9ae89b0ecf1b14823706d5ffffb73f6a72e104"), (path = "attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0002.done", sha256 = "35d216e20aed80a36327575b350d8e4c12b70f2038f5d07d55614c92afdd6b6b"), (path = "attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0003-P.tsv", sha256 = "2943f3ccc2a6d1dcb7e1048402eda28d3ac814489a6bd08b90b7f18248097e57"), (path = "attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0003-Z.tsv", sha256 = "8fb401c5207e00b3e9f2ef769f807c8069860a0da7ad72726f31c51ff97eb048"), (path = "attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0003-timing.tsv", sha256 = "62b6ec66fd6d6bc029a6ddea6372b9e25ef0b520585b28e7bb004ef4eee81f01"), (path = "attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0003.done", sha256 = "eae2a23f21e24dd158a817c096e6e77f32e1d2a850c4163d279c3531eef2af5a"), (path = "attempts/f0005-yXakvi/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0005-yXakvi/attempt.json", sha256 = "33462aa5e71dbabb439fe596965b1dd4a0bcabc94afaa103dca79e6404fc472b"), (path = "attempts/f0005-yXakvi/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0005-yXakvi/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0005-yXakvi/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0005-yXakvi/getdp.log", sha256 = "d7f274181d4d34a0f348eaccb2ce6b884dd426c053c490291a8deab790ef32b2"), (path = "attempts/f0005-yXakvi/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0005-yXakvi/petsc.log", sha256 = "d13aed750d08ea36d58913e719d499217fdd4ea74df09537187614d329339c9b"), (path = "attempts/f0005-yXakvi/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0005-yXakvi/solver.pre", sha256 = "daca4fa3af8341b0d7bcfa961b3d5627fc53dac7f9d044eecdcd605aee27a791"), (path = "attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0001-P.tsv", sha256 = "95be2f65e1d4a75ecf78c946091a48fc88d0aa7f377314f7eafff3d07d66063e"), (path = "attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0001-Z.tsv", sha256 = "08efd37d7184ef83df023c05c4252e34ef6837c05b1d2536961cf8f578e1498a"), (path = "attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0001-timing.tsv", sha256 = "ba24f1db805497624823cbd28ea4d3a62fc1c8c943fa4af2f989779f3087a4f3"), (path = "attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0001.done", sha256 = "ee7bf9802d600055d484ec4e501666418037b219c7bf910eefda35e16a843b08"), (path = "attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0002-P.tsv", sha256 = "f12870a70dabd22b1f8ad9983660e86610763eb1fd54a7ad6a4ee13128610582"), (path = "attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0002-Z.tsv", sha256 = "0ae728113cd00cd81fc7c4e273a38bb5392397b2215d3c9383262ac11b2e1971"), (path = "attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0002-timing.tsv", sha256 = "f42e70be94f6183e5a9d1dc2c665cce176f6414f7dc513dc1c953318cf41cfe3"), (path = "attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0002.done", sha256 = "e26c3631aaa36c47efb3ede81242002dbebf27057f428fe57f60849cede6b3f3"), (path = "attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0003-P.tsv", sha256 = "efef11eba3bb928a1d2ffef23e345fbc2b46ba9da84dbdaf95592f17e7e87e04"), (path = "attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0003-Z.tsv", sha256 = "fc56dc47dff9b7da06d88d76e886ff130d29538b8e9c97b6db83c4f9f466eede"), (path = "attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0003-timing.tsv", sha256 = "5efd4f2ffe191f5682583e0dfbeec9c6287151ff82cdd590e6a45e4f67502405"), (path = "attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0003.done", sha256 = "a91d7ca1bb120ef43c71df371c45a71b1cc7744f1ee2eb082338700de7f329c9"), (path = "attempts/f0006-ndYYTp/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0006-ndYYTp/attempt.json", sha256 = "72f9c4d1fd54ee122478e6a0195d62b8eef44ee1f5df50c522a727b2d8e14473"), (path = "attempts/f0006-ndYYTp/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0006-ndYYTp/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0006-ndYYTp/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0006-ndYYTp/getdp.log", sha256 = "9c9539dea9579a1424de9c640ac4748edb833d36d729ef411c1507ca68d6c11f"), (path = "attempts/f0006-ndYYTp/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0006-ndYYTp/petsc.log", sha256 = "78f5c4f2de48c6f38b0ae5e1effe998f7a4951d4c49953ca0ff6d8b220ab795f"), (path = "attempts/f0006-ndYYTp/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0006-ndYYTp/solver.pre", sha256 = "cfdb2e89538a7f1b844294060c303c14f90ef01eb8a0bf67d2a284eb3eeb97cc"), (path = "attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0001-P.tsv", sha256 = "1a36d60a0e090f2caa2cee9f7d46c7c514a2395f67831d7d9a4b4f0efa58cfbc"), (path = "attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0001-Z.tsv", sha256 = "62e6757e71fef71e561b10e28c9051bb4b86e88c7a41b23a88adf304bb43f71b"), (path = "attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0001-timing.tsv", sha256 = "cf010779455a1cb0a54599a104e3c82b40c06d5c66bf421df2bcccd906cc67ca"), (path = "attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0001.done", sha256 = "e41a78288341a2ae156cd28b6102b3a31a74b012eb4deb233e88fae392912e8e"), (path = "attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0002-P.tsv", sha256 = "3c04d81056ac404e60379b85ba6dba801725fa135594030f6995a4895821e26c"), (path = "attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0002-Z.tsv", sha256 = "269547724c63e0a2226f7e41fcf61c3409afa7c7e680f931e177af978642cf59"), (path = "attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0002-timing.tsv", sha256 = "34554347ed48775dc282d912dda0c27e8141936b83e15abda66b2d6b9e5462c6"), (path = "attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0002.done", sha256 = "c105367e4bac19858725b1b7c56efbb25ae370173d002f9a678a4762a5c69ac9"), (path = "attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0003-P.tsv", sha256 = "73b7fdf511c7c409936ae5eb019ede5429469504d0ca1cb86baf647b8322c3fa"), (path = "attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0003-Z.tsv", sha256 = "a21e602b7b8ae32b66915d272d3186c31cd7842c02667c602064f7598716f291"), (path = "attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0003-timing.tsv", sha256 = "865fceb647473629d0fbebb2e971d642a44f4e3a299376b5f6e02e8487969a32"), (path = "attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0003.done", sha256 = "de6214d7f2cf6d2097d94d720289a7424da0b9bf7a5df27326d8b69e936798e8"), (path = "attempts/f0007-G1cv2g/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0007-G1cv2g/attempt.json", sha256 = "d5111d96f6a3686d450cbb3fcf9f785b966c976269a11746d8ae9a300396c0b4"), (path = "attempts/f0007-G1cv2g/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0007-G1cv2g/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0007-G1cv2g/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0007-G1cv2g/getdp.log", sha256 = "eb635f1319c6629091027d9fd37a3828487c4a6f5f7f3356fc2a614f5b6f7ca3"), (path = "attempts/f0007-G1cv2g/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0007-G1cv2g/petsc.log", sha256 = "e27a2f3bd5b7d36853f6ff45aeffdcbacefa671200c5c50308762fffce54a9c6"), (path = "attempts/f0007-G1cv2g/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0007-G1cv2g/solver.pre", sha256 = "35b75bea2130c9af1b19db06c29b4a4d4ed8d53d02a1c60f5a0e2a51ae6e61f7"), (path = "attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0001-P.tsv", sha256 = "2fb1d14577dc5925f78e9541758ae3b8667618ec0582c5cddf727c1220f83f23"), (path = "attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0001-Z.tsv", sha256 = "f1f34e597d4653c8cfb0551ac7b2a8e82a5835c14f7bc4d16244d53564e12b03"), (path = "attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0001-timing.tsv", sha256 = "79bec814d003d677859197c36a54d06b33b5592483616194dc1cda2ed330de09"), (path = "attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0001.done", sha256 = "d95e4d651bb7fa68e36e0077af5a10e10baacf852173da7ac0f1cca3e414dc7f"), (path = "attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0002-P.tsv", sha256 = "88c5897a343d651d77168dcccdd84b304ab89aaffd9063878c8b87761ece1ed6"), (path = "attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0002-Z.tsv", sha256 = "d3635277a9973c4d18b6482f2c33d73654ccc35f066b551bd17e2fdbd6850ff4"), (path = "attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0002-timing.tsv", sha256 = "14661c5ed6b2be7497371c93164997c788b960448c05e13720cbd00f3cbe9cfb"), (path = "attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0002.done", sha256 = "a953202777b2aea0282f4e5ecac9feb227f850cb3f80f2247ca07a708f3635be"), (path = "attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0003-P.tsv", sha256 = "fde259d49daf8d0728a8d6ccc708b458aca7d655b0d89562474d7960d7a55c94"), (path = "attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0003-Z.tsv", sha256 = "a9acd7442ff0d134b7a65270ffc58e2114eee22ce422635c2b21f0ac9fa1d31a"), (path = "attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0003-timing.tsv", sha256 = "4bfe3b64dee4e687f467bbdc044d55fd6fea1f78201b9f316040fa82e46792be"), (path = "attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0003.done", sha256 = "bc6e3e671a966700354c8fb7f9ab0ff830db8a4b88332e0ec4974ce29df37b0c"), (path = "attempts/f0008-wUdCI1/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0008-wUdCI1/attempt.json", sha256 = "8cd81f8cd9fc0b36ec1ce765d8ae9ab5a4d197a1d7ca82c7423d2473c19ee6c4"), (path = "attempts/f0008-wUdCI1/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0008-wUdCI1/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0008-wUdCI1/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0008-wUdCI1/getdp.log", sha256 = "b0eb2de540ba9494f3e1993d37aa7c02597f18e4619a60f316b5804f7a036a23"), (path = "attempts/f0008-wUdCI1/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0008-wUdCI1/petsc.log", sha256 = "6a935d0c75793172d490a82a93b2d38d41a1959dc5225544b5e78cf35d215797"), (path = "attempts/f0008-wUdCI1/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0008-wUdCI1/solver.pre", sha256 = "b3b00cb8bf2a65215251ad52d1a0e372205e91f8ace7480064d51dcdf635bb81"), (path = "attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0001-P.tsv", sha256 = "412308a407308fca889dee31cb0f4ddfe43f3464aabe4ffe8648558dab68e053"), (path = "attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0001-Z.tsv", sha256 = "2ec4bf6591207af6e57ef0941c6a37537d16ef0d9b1c67f0c07930c38360135b"), (path = "attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0001-timing.tsv", sha256 = "de27cb70de616419e2207d7d7176ff95c060aaa7b0d22557b0b591abb39377a3"), (path = "attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0001.done", sha256 = "c03ca6db0131d3218478fb4e85f0350dca26787b9ffdfc9d50b5d68ad93b352c"), (path = "attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0002-P.tsv", sha256 = "b4928623e3b217cf5bc370f5152cd2e723cdd9a24dce1b604a2fa76ddbd59f4e"), (path = "attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0002-Z.tsv", sha256 = "c2d607d773deb123b6342f6d3ebb18954babe1d7ce20e402b55758ed2776d6a2"), (path = "attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0002-timing.tsv", sha256 = "aa3c56bdde4e222e0ea9a8aa3273dc28e0decdf55a14af28a5df5f13b9601fab"), (path = "attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0002.done", sha256 = "76f50b528daca79ab1eac47c47f96ca1e7b8ad94bcaefbaac04335941306f993"), (path = "attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0003-P.tsv", sha256 = "241c0469357c8bc65f03e3a385e6b0ab7f72e05e477b30c78adf613e8a97aa9e"), (path = "attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0003-Z.tsv", sha256 = "ef5b33d8131365e9b063db67172cf66351886cb5b946691d6ada15a21cdd2fb5"), (path = "attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0003-timing.tsv", sha256 = "69a5283f85550a89524284e9f638ac3a806fa220448ea0ec0d37732ff6ba31d4"), (path = "attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0003.done", sha256 = "20bdbe7d9351850f55ebf49a31f3a93235d04aea44253ddd2d75828a83fa1c74"), (path = "attempts/f0009-LD2vT2/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0009-LD2vT2/attempt.json", sha256 = "a96b88ab0871d4dd6244aa614ee14e0963838948673dcf4154eb12ff8545e1fe"), (path = "attempts/f0009-LD2vT2/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0009-LD2vT2/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0009-LD2vT2/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0009-LD2vT2/getdp.log", sha256 = "fb775c552b660bee7b3a352cf64934ef18070c573d0c20697b6849ff732b98ff"), (path = "attempts/f0009-LD2vT2/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0009-LD2vT2/petsc.log", sha256 = "bfdc97d923a620b0d40994655065cc401db8b156dd9a3e2cf36f40f51f21fe90"), (path = "attempts/f0009-LD2vT2/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0009-LD2vT2/solver.pre", sha256 = "c468c1eb5023edd92956da2dab13e42e9bbc733c725034d12f238b5deb6d5f47"), (path = "attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0001-P.tsv", sha256 = "cf5f55fd71d26c7ef644a0bb9d00fbb04a47411fde6215fe59de29bcb10b9eaf"), (path = "attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0001-Z.tsv", sha256 = "3fcd842bf3cf63e79bc3e8f623626ba34eeacc992a1b810d1289b0f6945003a3"), (path = "attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0001-timing.tsv", sha256 = "6f08951d35fb19246f8f093d2155c99243b2cdf39b85234604ff9e10b4efb576"), (path = "attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0001.done", sha256 = "22b8b831af1067ac03eaf9570ddd71235af29f6b65e559199200376f6ef90f2a"), (path = "attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0002-P.tsv", sha256 = "4d85587686a4a1b58da0074626478ff1ccc4899449ffb5958252e5324ad524f5"), (path = "attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0002-Z.tsv", sha256 = "60aa2fe4f7f9dfc5e8fea8c0d780ea754b220a672a613ee9650ce5d398a13926"), (path = "attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0002-timing.tsv", sha256 = "7154dfc17be5cfd92161898e03253129472470ccee24c0599d905ba61208842e"), (path = "attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0002.done", sha256 = "2ab6b278377a8a5090d5d456a9193028844db99b8f3fa96c2433d70920e73c22"), (path = "attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0003-P.tsv", sha256 = "8937eba2516490766a41906e7ee715b6aed994d21e9ae295598c93f7b29234b9"), (path = "attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0003-Z.tsv", sha256 = "f4f06bdabd1bb68cae14f42ddac34584e7f6994b27d7142030192e6935c20800"), (path = "attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0003-timing.tsv", sha256 = "f5f59f01a884e46e8622c088a68404011a16e00c74bdc6740495e9c5a5328dfb"), (path = "attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0003.done", sha256 = "3449a1ca026b39cca413695b5bd20e176addf6bcf3103f9147e8ac56407cafa3"), (path = "input/computation.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/input/computation.json", sha256 = "7f88c0b11d387951ddacf8611f94f9d06188ca5e474dad26532aece3e7be2f25"), (path = "input/model_data.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/input/model_data.pro", sha256 = "bdd987fd050af492aeec116b53eabe74bae14953bff8ea41534ae6aaf722cb22"), (path = "input/problem.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/input/problem.json", sha256 = "bfa5a741cc75bd7b7a7ce917a85836bc628af114b0ce0462110fc37279424cd8"), (path = "input/getdp/integration.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/input/getdp/integration.pro", sha256 = "e113951ac1e6e658f1be50e53875e30c85497445f0e6584a8a7ab1848588e902"), (path = "input/getdp/jacobian.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/input/getdp/jacobian.pro", sha256 = "014208d39043ab13183c42c312aba459265715da6d6ddc38ec1fbc39b6090c73"), (path = "input/getdp/materials.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/input/getdp/materials.pro", sha256 = "581f089113810a379d8a49c03d79f55045b723c8f4c761d0a493a019ff1f5d9e"), (path = "input/getdp/model.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/input/getdp/model.pro", sha256 = "c0bb424db5ab4db9ca22f549f675edb4646f69910d2aa14e20cf4f40e41bd313"), (path = "input/getdp/quasi-full.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/input/getdp/quasi-full.pro", sha256 = "1aede35114ea686c5f109089478027d241dec12dc9fd14a24452b348f89b08e4"), (path = "input/getdp/quasi-tem.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/input/getdp/quasi-tem.pro", sha256 = "448d9019ad417c9a784da20170b855dd25e559297499a83d19b007ee095c084d"), (path = "logs/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/logs/getdp.log", sha256 = "63791f3412510c8e158ee8889ac002e00d1e569dcbccb0e6689ff6c61c562bf7"), (path = "mesh/frequency_0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/mesh/frequency_0001.json", sha256 = "55bd35bc31facd4ab58cf8f4a57bf9d1d3cd930742fb85d3a9684a5f4cd0d056"), (path = "mesh/frequency_0001.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/mesh/frequency_0001.msh", sha256 = "38384c803a509cdfbb27513b23180d2dcd6f62b655bd374cb0861c50c7c9c11b"), (path = "mesh/frequency_0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/mesh/frequency_0002.json", sha256 = "8d0c820b30fbb8266f5f0bd227e3653d2618d671dd14ca6f3584787d5a201672"), (path = "mesh/frequency_0002.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/mesh/frequency_0002.msh", sha256 = "68a5235e6da5688300344ef472fb1ce30f3c9b270f17b8d9d584b8306bb69bde"), (path = "mesh/frequency_0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/mesh/frequency_0003.json", sha256 = "61e6550f797f5a2b22910280ca58d036748cad329c2410ee206b8046ea277f9f"), (path = "mesh/frequency_0003.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/mesh/frequency_0003.msh", sha256 = "d0bfd44ede5381a46b7e4b6549bf6fe606c77b344ed141b12836d9bd2d7f2aec"), (path = "mesh/frequency_0004.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/mesh/frequency_0004.json", sha256 = "e7c1a4d2fb48242b142eb2ad20cc58cacf1cdae0d86f7116fe0989c41c48edff"), (path = "mesh/frequency_0004.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/mesh/frequency_0004.msh", sha256 = "d5f14cc29e9767722151cdb86604ad138bc1f1ceb533a6190fc3639ee870a7ed"), (path = "mesh/frequency_0005.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/mesh/frequency_0005.json", sha256 = "b439939050c134ca8db4fa176fae7618b94391a6b004f50240a6f45e0f6ee785"), (path = "mesh/frequency_0005.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/mesh/frequency_0005.msh", sha256 = "2dc86c1f757405dc1f8b968c42bab7515dfa0085367c4af26372aeaeaad8ebd3"), (path = "mesh/frequency_0006.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/mesh/frequency_0006.json", sha256 = "edb036e71c6600753a74bf12e884376077f37f2c27fd2b4c2fcd60e7db3aed14"), (path = "mesh/frequency_0006.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/mesh/frequency_0006.msh", sha256 = "e59362b1faa322a9973a6dd64847c901b3b6c2beba9e750dbb0269b5cc56e979"), (path = "mesh/frequency_0007.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/mesh/frequency_0007.json", sha256 = "22c5cc5f5f055895ff9b5021b20b5373456ee835fd8ab46d8d66d787f1d933a1"), (path = "mesh/frequency_0007.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/mesh/frequency_0007.msh", sha256 = "9f174b7066d5b98dcc208e6901f5c79428984d399ced80b1eb2a89d2aeb20810"), (path = "mesh/frequency_0008.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/mesh/frequency_0008.json", sha256 = "aaa2b4dbee2529a5a13a554f8bf2bd4325408fda8b0ecb0065a1cf390d051f9b"), (path = "mesh/frequency_0008.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/mesh/frequency_0008.msh", sha256 = "13e29179b39e78b933a4bff61ec3c48d8c4079270ffa2aeadc4dedba633d6f49"), (path = "mesh/model.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/mesh/model.json", sha256 = "5908f8e55adcea9f1ac1bf5a2f769de0caa3521cd865ab55d6071d74d38f369c"), (path = "mesh/model.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/mesh/model.msh", sha256 = "f21eaef57ca94e028771e45111d66287e224c416be21d63d6f187a3b7f56900f"), (path = "raw/P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/P.tsv", sha256 = "e73177d930d18ec3a3d6c7f0c81d269daf82c3d2b9f60309662ae6547ef859af"), (path = "raw/Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/Z.tsv", sha256 = "ce1450aa75aeaced963056b53215092716a3490011ff2f8f518e372a6217879e"), (path = "raw/checksums.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/checksums.json", sha256 = "76bc5639f764edadf46129bf4e2ca4e823a9b08e28afcc7ff43797c1cab23068"), (path = "raw/scan_complete.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/scan_complete.tsv", sha256 = "2cc9eff50bb7e6fff0e2555f96d93ebe952c4a1a158a1dca82fe6cca3aa1afab"), (path = "raw/jobs/getdp-f0001-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0001-b0001-P.tsv", sha256 = "031a280c87ddc96904d1f86d1ef5415fca772c6b3ce3ce4300bdec75de98eeeb"), (path = "raw/jobs/getdp-f0001-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0001-b0001-Z.tsv", sha256 = "c2163b4e92999b1751331366e286a7b55c548f1d488467a05b277b4190b5bdad"), (path = "raw/jobs/getdp-f0001-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0001-b0001-timing.tsv", sha256 = "fec35bd592ad36327d68b415791e6dfc12587c27e2386f483cbff814ee9f7797"), (path = "raw/jobs/getdp-f0001-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0001-b0001.json", sha256 = "08afd3895c0b4bd4404edaefd386b45c6fa9f15f25f991aee9565c66f1c22da7"), (path = "raw/jobs/getdp-f0001-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0001-b0002-P.tsv", sha256 = "86a470e1837a982ad0b60df3a9f2bc036f0b04fba5509a142e90d079273ac195"), (path = "raw/jobs/getdp-f0001-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0001-b0002-Z.tsv", sha256 = "3f340376a310d046c7a8ddf7c4edfa2c2ec4ffb1f2b39eba79352d247f3ae6bb"), (path = "raw/jobs/getdp-f0001-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0001-b0002-timing.tsv", sha256 = "f035f9a83a0bc955a45d10e11410210082cedf40a3d4e917c2e93d54f6a46733"), (path = "raw/jobs/getdp-f0001-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0001-b0002.json", sha256 = "f11392bb58aaa338d24e295467841f1478e9e07e4128cb9a32fec4d97bd5c8d1"), (path = "raw/jobs/getdp-f0001-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0001-b0003-P.tsv", sha256 = "65167a9aa871d23ab936b1bb21bc509b8d74d94242e6297cfc2451ce806a00b3"), (path = "raw/jobs/getdp-f0001-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0001-b0003-Z.tsv", sha256 = "89773a7454a74c37898661a0397237a6a51c7b1225697078d3d358ce2680e5a9"), (path = "raw/jobs/getdp-f0001-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0001-b0003-timing.tsv", sha256 = "9133b1cb3a423655d0379c70af7a47034b559065727f192cb68af9312dc47e77"), (path = "raw/jobs/getdp-f0001-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0001-b0003.json", sha256 = "807066ffef45efe9353cab6d4225e25b0a305d811d4d2bcc443873f45197973b"), (path = "raw/jobs/getdp-f0002-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0002-b0001-P.tsv", sha256 = "75360e62d1f6396fe7bf30be166ab6b76ced00b406d41e3cff7f921310f4638d"), (path = "raw/jobs/getdp-f0002-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0002-b0001-Z.tsv", sha256 = "97214f5c8ba29de45e9d35e462a2b3d6ef3eb9d0959812aa835286a0e33491f3"), (path = "raw/jobs/getdp-f0002-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0002-b0001-timing.tsv", sha256 = "067ab77f6136d789cb6e8a80b579d471e9917b0f82993ee0472f6b1033a40290"), (path = "raw/jobs/getdp-f0002-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0002-b0001.json", sha256 = "e4b59bc1961dcd0c7b5d7b528bfc40dca6eec9d7f1171a5a0815c898f101e24b"), (path = "raw/jobs/getdp-f0002-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0002-b0002-P.tsv", sha256 = "790d625eddc047985df0af0c08e1984183df9bac9dc59a8107d67c0223829a68"), (path = "raw/jobs/getdp-f0002-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0002-b0002-Z.tsv", sha256 = "4a9daa553ea47fd0f118a09f27cf90351c79fc6f59aca92a9079e94b74950974"), (path = "raw/jobs/getdp-f0002-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0002-b0002-timing.tsv", sha256 = "493434a9b35cfb2bc991dc0ee378b19b6c0c3c3a8f18e67300fb8379ff3f0aa5"), (path = "raw/jobs/getdp-f0002-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0002-b0002.json", sha256 = "bdd435735409b936f07f27a616d49b0ab0dd3d196c91bce1a108b4a4b0f81e80"), (path = "raw/jobs/getdp-f0002-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0002-b0003-P.tsv", sha256 = "9a0f04d6a64b9690bb4304da7520f2ebd37d7749d0c746d1a01b0d759641283b"), (path = "raw/jobs/getdp-f0002-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0002-b0003-Z.tsv", sha256 = "e0ab6f15a6d62ebef17c41c6f20586c61499c3898af3ea9d65801b1dd20450f6"), (path = "raw/jobs/getdp-f0002-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0002-b0003-timing.tsv", sha256 = "6ad68000f813c19cd34f8b178f16c5539b54ecfd269035c408d4fbce139dd9aa"), (path = "raw/jobs/getdp-f0002-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0002-b0003.json", sha256 = "a09ca9165619eab61b6e7c35c373eccd9d160ddd93a8c8501e12ca9d9e6acd9d"), (path = "raw/jobs/getdp-f0003-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0003-b0001-P.tsv", sha256 = "8c89e9137f9157c4fcbb7f19ed185a3b1755fc8c475671f75a5c138cb326b187"), (path = "raw/jobs/getdp-f0003-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0003-b0001-Z.tsv", sha256 = "e392c6378c451328d736fae1d29e818251caf06e40928c154fb30f3abdca563a"), (path = "raw/jobs/getdp-f0003-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0003-b0001-timing.tsv", sha256 = "e62951a512bc6a0927b91df494b1b058821b7547d85dade8747fb960f9b210b0"), (path = "raw/jobs/getdp-f0003-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0003-b0001.json", sha256 = "34c5c81461612f9197de1927e502b95d02a3a617c9075d8f38cf2d09173fc708"), (path = "raw/jobs/getdp-f0003-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0003-b0002-P.tsv", sha256 = "2e7b78e1e6203a93287a3758917f0f4e0a8dd5a2c163824e548d65bd8252009b"), (path = "raw/jobs/getdp-f0003-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0003-b0002-Z.tsv", sha256 = "48219f308edd90574abf7865948acb574d675bc223eaf505e034822a783162dd"), (path = "raw/jobs/getdp-f0003-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0003-b0002-timing.tsv", sha256 = "300f2d86ea6705d8df22e7131131c15d36476a8535f756e54bfeac8d6244dd03"), (path = "raw/jobs/getdp-f0003-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0003-b0002.json", sha256 = "3a18ce34bd5ece68e55270458a8068cb7a7ee3e42cb0c2753fe3d02967eaa0ec"), (path = "raw/jobs/getdp-f0003-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0003-b0003-P.tsv", sha256 = "01a9abbfc7ac762dd2ca47f576452b9ab89f745a4736f9b5298c7059b67e0b4e"), (path = "raw/jobs/getdp-f0003-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0003-b0003-Z.tsv", sha256 = "998a2152fa3e61432a06dd2fb1549914a9bccfbd27a91ac88a28e69d155da0a8"), (path = "raw/jobs/getdp-f0003-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0003-b0003-timing.tsv", sha256 = "4244046606e8dfab3ab2cdd7a5a490edd70cbf2f780476a4fa83505836766943"), (path = "raw/jobs/getdp-f0003-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0003-b0003.json", sha256 = "f35ef6a68aabf978275307e64d7c27daefe68badb02fbe2f281813f4a14d9303"), (path = "raw/jobs/getdp-f0004-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0004-b0001-P.tsv", sha256 = "5d4004862d3d0d80660a14391f0b1ff16ca269d36e14cf4d9eab44c1f0508b46"), (path = "raw/jobs/getdp-f0004-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0004-b0001-Z.tsv", sha256 = "4fa281428aa093aedd060a88d6a0395519fcd9704aa92d11e6f744217e04cb35"), (path = "raw/jobs/getdp-f0004-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0004-b0001-timing.tsv", sha256 = "bc38c85ddedbafe212c6c766d6156cf67b285f0f4d2017bf23aa875229230d4c"), (path = "raw/jobs/getdp-f0004-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0004-b0001.json", sha256 = "a6612bd9ff45f14ab763bdc8bb1fa057601d5e4a262eaea9cc3f0eccb6418bbc"), (path = "raw/jobs/getdp-f0004-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0004-b0002-P.tsv", sha256 = "dab42728b3411be92f90c8ea8b1b252e86bebcfda885ebf09268009a05f1cf79"), (path = "raw/jobs/getdp-f0004-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0004-b0002-Z.tsv", sha256 = "fa0c078e62461975905443eb2ce0bbf2bf4630dbb987a114389e8a57a8da87dd"), (path = "raw/jobs/getdp-f0004-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0004-b0002-timing.tsv", sha256 = "2836501b252019196717fecf2a9ae89b0ecf1b14823706d5ffffb73f6a72e104"), (path = "raw/jobs/getdp-f0004-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0004-b0002.json", sha256 = "c46bff8847504c1a23ba5d0883381f17190c9e71248d21a9b459a96b32f745c2"), (path = "raw/jobs/getdp-f0004-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0004-b0003-P.tsv", sha256 = "2943f3ccc2a6d1dcb7e1048402eda28d3ac814489a6bd08b90b7f18248097e57"), (path = "raw/jobs/getdp-f0004-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0004-b0003-Z.tsv", sha256 = "8fb401c5207e00b3e9f2ef769f807c8069860a0da7ad72726f31c51ff97eb048"), (path = "raw/jobs/getdp-f0004-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0004-b0003-timing.tsv", sha256 = "62b6ec66fd6d6bc029a6ddea6372b9e25ef0b520585b28e7bb004ef4eee81f01"), (path = "raw/jobs/getdp-f0004-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0004-b0003.json", sha256 = "cc6d3516eb7bee8b57aee610f85f9ffbd5708110be6d5c956d018e377f4967c6"), (path = "raw/jobs/getdp-f0005-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0005-b0001-P.tsv", sha256 = "95be2f65e1d4a75ecf78c946091a48fc88d0aa7f377314f7eafff3d07d66063e"), (path = "raw/jobs/getdp-f0005-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0005-b0001-Z.tsv", sha256 = "08efd37d7184ef83df023c05c4252e34ef6837c05b1d2536961cf8f578e1498a"), (path = "raw/jobs/getdp-f0005-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0005-b0001-timing.tsv", sha256 = "ba24f1db805497624823cbd28ea4d3a62fc1c8c943fa4af2f989779f3087a4f3"), (path = "raw/jobs/getdp-f0005-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0005-b0001.json", sha256 = "e242e5e233fba51e92710201aa3cca53479b5047c2df1a7e0e2c3fea52de8c12"), (path = "raw/jobs/getdp-f0005-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0005-b0002-P.tsv", sha256 = "f12870a70dabd22b1f8ad9983660e86610763eb1fd54a7ad6a4ee13128610582"), (path = "raw/jobs/getdp-f0005-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0005-b0002-Z.tsv", sha256 = "0ae728113cd00cd81fc7c4e273a38bb5392397b2215d3c9383262ac11b2e1971"), (path = "raw/jobs/getdp-f0005-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0005-b0002-timing.tsv", sha256 = "f42e70be94f6183e5a9d1dc2c665cce176f6414f7dc513dc1c953318cf41cfe3"), (path = "raw/jobs/getdp-f0005-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0005-b0002.json", sha256 = "6c3253a4249ef87dd26e0ef335ea836bf75d0a440a1ccc0d88abe3dec9baf01f"), (path = "raw/jobs/getdp-f0005-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0005-b0003-P.tsv", sha256 = "efef11eba3bb928a1d2ffef23e345fbc2b46ba9da84dbdaf95592f17e7e87e04"), (path = "raw/jobs/getdp-f0005-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0005-b0003-Z.tsv", sha256 = "fc56dc47dff9b7da06d88d76e886ff130d29538b8e9c97b6db83c4f9f466eede"), (path = "raw/jobs/getdp-f0005-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0005-b0003-timing.tsv", sha256 = "5efd4f2ffe191f5682583e0dfbeec9c6287151ff82cdd590e6a45e4f67502405"), (path = "raw/jobs/getdp-f0005-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0005-b0003.json", sha256 = "bd9f15b88c30da6e32a151a995da74c6ff3bcc106bcc95d169eace5073810318"), (path = "raw/jobs/getdp-f0006-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0006-b0001-P.tsv", sha256 = "1a36d60a0e090f2caa2cee9f7d46c7c514a2395f67831d7d9a4b4f0efa58cfbc"), (path = "raw/jobs/getdp-f0006-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0006-b0001-Z.tsv", sha256 = "62e6757e71fef71e561b10e28c9051bb4b86e88c7a41b23a88adf304bb43f71b"), (path = "raw/jobs/getdp-f0006-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0006-b0001-timing.tsv", sha256 = "cf010779455a1cb0a54599a104e3c82b40c06d5c66bf421df2bcccd906cc67ca"), (path = "raw/jobs/getdp-f0006-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0006-b0001.json", sha256 = "0e7317bb8936cd3bda7016092043b08c86bef1c5108dc5aff8bc19693196903f"), (path = "raw/jobs/getdp-f0006-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0006-b0002-P.tsv", sha256 = "3c04d81056ac404e60379b85ba6dba801725fa135594030f6995a4895821e26c"), (path = "raw/jobs/getdp-f0006-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0006-b0002-Z.tsv", sha256 = "269547724c63e0a2226f7e41fcf61c3409afa7c7e680f931e177af978642cf59"), (path = "raw/jobs/getdp-f0006-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0006-b0002-timing.tsv", sha256 = "34554347ed48775dc282d912dda0c27e8141936b83e15abda66b2d6b9e5462c6"), (path = "raw/jobs/getdp-f0006-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0006-b0002.json", sha256 = "5f9f65cccf07a4fa2b8c58d4e9278441a5c8e71e41e67b4e10783cc3fb5eddca"), (path = "raw/jobs/getdp-f0006-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0006-b0003-P.tsv", sha256 = "73b7fdf511c7c409936ae5eb019ede5429469504d0ca1cb86baf647b8322c3fa"), (path = "raw/jobs/getdp-f0006-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0006-b0003-Z.tsv", sha256 = "a21e602b7b8ae32b66915d272d3186c31cd7842c02667c602064f7598716f291"), (path = "raw/jobs/getdp-f0006-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0006-b0003-timing.tsv", sha256 = "865fceb647473629d0fbebb2e971d642a44f4e3a299376b5f6e02e8487969a32"), (path = "raw/jobs/getdp-f0006-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0006-b0003.json", sha256 = "a51072fa4c42f3a8a9ef2af6362630c39fb403de1e53fd79f520da0343e3fb93"), (path = "raw/jobs/getdp-f0007-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0007-b0001-P.tsv", sha256 = "2fb1d14577dc5925f78e9541758ae3b8667618ec0582c5cddf727c1220f83f23"), (path = "raw/jobs/getdp-f0007-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0007-b0001-Z.tsv", sha256 = "f1f34e597d4653c8cfb0551ac7b2a8e82a5835c14f7bc4d16244d53564e12b03"), (path = "raw/jobs/getdp-f0007-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0007-b0001-timing.tsv", sha256 = "79bec814d003d677859197c36a54d06b33b5592483616194dc1cda2ed330de09"), (path = "raw/jobs/getdp-f0007-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0007-b0001.json", sha256 = "1ab1a54f7ccdbf72649fec766ce34bc626381542b87353e1b62e2b24da9644de"), (path = "raw/jobs/getdp-f0007-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0007-b0002-P.tsv", sha256 = "88c5897a343d651d77168dcccdd84b304ab89aaffd9063878c8b87761ece1ed6"), (path = "raw/jobs/getdp-f0007-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0007-b0002-Z.tsv", sha256 = "d3635277a9973c4d18b6482f2c33d73654ccc35f066b551bd17e2fdbd6850ff4"), (path = "raw/jobs/getdp-f0007-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0007-b0002-timing.tsv", sha256 = "14661c5ed6b2be7497371c93164997c788b960448c05e13720cbd00f3cbe9cfb"), (path = "raw/jobs/getdp-f0007-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0007-b0002.json", sha256 = "8579b149df0573903d9c5d61ccd87b8373a23fe89cc5c249cb7d799f66409f01"), (path = "raw/jobs/getdp-f0007-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0007-b0003-P.tsv", sha256 = "fde259d49daf8d0728a8d6ccc708b458aca7d655b0d89562474d7960d7a55c94"), (path = "raw/jobs/getdp-f0007-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0007-b0003-Z.tsv", sha256 = "a9acd7442ff0d134b7a65270ffc58e2114eee22ce422635c2b21f0ac9fa1d31a"), (path = "raw/jobs/getdp-f0007-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0007-b0003-timing.tsv", sha256 = "4bfe3b64dee4e687f467bbdc044d55fd6fea1f78201b9f316040fa82e46792be"), (path = "raw/jobs/getdp-f0007-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0007-b0003.json", sha256 = "2a05ea19b78030d3f2dd5cc3039edeace017e9d10a8c6bf5dcb7f5bd3b31d316"), (path = "raw/jobs/getdp-f0008-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0008-b0001-P.tsv", sha256 = "412308a407308fca889dee31cb0f4ddfe43f3464aabe4ffe8648558dab68e053"), (path = "raw/jobs/getdp-f0008-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0008-b0001-Z.tsv", sha256 = "2ec4bf6591207af6e57ef0941c6a37537d16ef0d9b1c67f0c07930c38360135b"), (path = "raw/jobs/getdp-f0008-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0008-b0001-timing.tsv", sha256 = "de27cb70de616419e2207d7d7176ff95c060aaa7b0d22557b0b591abb39377a3"), (path = "raw/jobs/getdp-f0008-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0008-b0001.json", sha256 = "b419c55b1af862ea25eb5940a3a83d11b088c53598cccf1547b475fc9b1c9ee7"), (path = "raw/jobs/getdp-f0008-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0008-b0002-P.tsv", sha256 = "b4928623e3b217cf5bc370f5152cd2e723cdd9a24dce1b604a2fa76ddbd59f4e"), (path = "raw/jobs/getdp-f0008-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0008-b0002-Z.tsv", sha256 = "c2d607d773deb123b6342f6d3ebb18954babe1d7ce20e402b55758ed2776d6a2"), (path = "raw/jobs/getdp-f0008-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0008-b0002-timing.tsv", sha256 = "aa3c56bdde4e222e0ea9a8aa3273dc28e0decdf55a14af28a5df5f13b9601fab"), (path = "raw/jobs/getdp-f0008-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0008-b0002.json", sha256 = "722b1eac2d25f47c47654737970ba344186d73af97b870d6c6c648ec58688f38"), (path = "raw/jobs/getdp-f0008-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0008-b0003-P.tsv", sha256 = "241c0469357c8bc65f03e3a385e6b0ab7f72e05e477b30c78adf613e8a97aa9e"), (path = "raw/jobs/getdp-f0008-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0008-b0003-Z.tsv", sha256 = "ef5b33d8131365e9b063db67172cf66351886cb5b946691d6ada15a21cdd2fb5"), (path = "raw/jobs/getdp-f0008-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0008-b0003-timing.tsv", sha256 = "69a5283f85550a89524284e9f638ac3a806fa220448ea0ec0d37732ff6ba31d4"), (path = "raw/jobs/getdp-f0008-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0008-b0003.json", sha256 = "16b5729dbfd40e859ee952c3ee149bcff4130e36043b6ce0e80b6b120b14ee55"), (path = "raw/jobs/getdp-f0009-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0009-b0001-P.tsv", sha256 = "cf5f55fd71d26c7ef644a0bb9d00fbb04a47411fde6215fe59de29bcb10b9eaf"), (path = "raw/jobs/getdp-f0009-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0009-b0001-Z.tsv", sha256 = "3fcd842bf3cf63e79bc3e8f623626ba34eeacc992a1b810d1289b0f6945003a3"), (path = "raw/jobs/getdp-f0009-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0009-b0001-timing.tsv", sha256 = "6f08951d35fb19246f8f093d2155c99243b2cdf39b85234604ff9e10b4efb576"), (path = "raw/jobs/getdp-f0009-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0009-b0001.json", sha256 = "9b7029fb15eea0e127fd6baa05b5c4ba5fb0ace1e4abc4727d6ff66512ad121a"), (path = "raw/jobs/getdp-f0009-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0009-b0002-P.tsv", sha256 = "4d85587686a4a1b58da0074626478ff1ccc4899449ffb5958252e5324ad524f5"), (path = "raw/jobs/getdp-f0009-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0009-b0002-Z.tsv", sha256 = "60aa2fe4f7f9dfc5e8fea8c0d780ea754b220a672a613ee9650ce5d398a13926"), (path = "raw/jobs/getdp-f0009-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0009-b0002-timing.tsv", sha256 = "7154dfc17be5cfd92161898e03253129472470ccee24c0599d905ba61208842e"), (path = "raw/jobs/getdp-f0009-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0009-b0002.json", sha256 = "3da2298a3a579332e8a86937c1d24b5f236dcc6f7eb84d04122aed0e8c15443d"), (path = "raw/jobs/getdp-f0009-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0009-b0003-P.tsv", sha256 = "8937eba2516490766a41906e7ee715b6aed994d21e9ae295598c93f7b29234b9"), (path = "raw/jobs/getdp-f0009-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0009-b0003-Z.tsv", sha256 = "f4f06bdabd1bb68cae14f42ddac34584e7f6994b27d7142030192e6935c20800"), (path = "raw/jobs/getdp-f0009-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0009-b0003-timing.tsv", sha256 = "f5f59f01a884e46e8622c088a68404011a16e00c74bdc6740495e9c5a5328dfb"), (path = "raw/jobs/getdp-f0009-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d/raw/jobs/getdp-f0009-b0003.json", sha256 = "d93f4a8177c097d64bbf11abe60825c4644d42ad8e32e386035a3a9b92511313")], coordinates = ["cable:1:core", "cable:2:core", "cable:3:core"], formulations = @NamedTuple{schema_version::Int64, selections::@NamedTuple{insulation_admittance::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}, semicon_admittance::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}, earth_properties::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}, temperature_dependence::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}}, assumptions::@NamedTuple{impedance::String, admittance::String, earth::String, propagation::String, semicon_domain::String, enclosure::String}, backend::Symbol, requested::NamedTuple, methods::NamedTuple, options::NamedTuple}((4, @NamedTuple{insulation_admittance::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}, semicon_admittance::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}, earth_properties::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}, temperature_dependence::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}}((@NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}((:lossless, NamedTuple(), NamedTuple())), @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}((:lossless, NamedTuple(), NamedTuple())), @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}((:constant, NamedTuple(), NamedTuple())), @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}((:linear, NamedTuple(), NamedTuple())))), (impedance = "Axial current-driven A_z/u_r finite-element equations", admittance = "Scalar electrodynamic Helmholtz equation in surrounding media; equipotential terminals with unit transverse-current excitation; Y = inv(P)", earth = "Horizontal air and one semi-infinite soil; soil constitutive properties evaluated at each frequency", propagation = "Gamma = 0; medium diffusion and displacement retained; independent Z/P blocks in one factorization", semicon_domain = "Passive material region, without electrical terminal ownership", enclosure = "Supported enclosures are represented by their material and terminal domains"), :fem, (insulation_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), semicon_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_properties = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), temperature_dependence = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing)), (insulation_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), semicon_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), earth_properties = (identifier = :constant, parameters = NamedTuple(), options = NamedTuple()), temperature_dependence = (identifier = :linear, parameters = NamedTuple(), options = NamedTuple())), (reduce_bundle = false, kron_reduction = false, ideal_transposition = false, physics = Symbol("quasi-tem")))), fem = (timing = (assembly_seconds = 9.426222324371338, recovered_columns = 0, scope = "accumulated native worker wall time; not elapsed scan time", columns = 27, worker_wall_seconds = 27.064801214, factorized_columns = 9, constraint_seconds = 3.209627866744995, solve_seconds = 9.570050477981567, output_seconds = 0.007592916488647461, worker_wall_scope = "sum of newly executed process wall durations in this invocation", backend = "getdp", schema = 1, reused = false), run = (state = LineCableModelsGmshExt.completed, run_directory = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-Fon40d", mesh_source = :generated, mesh_fingerprint = "8913ae9905751e803a5ccc44f994aef1f69cb61ffdc61d6e49b7488169e1840c", getdp_invocations = 9, map_paths = String[], completed_columns = 27, completed_frequencies = 9), inputs = (schema_version = 7, solver_protocol = 3, mesh_fingerprint = "1ff6e227c359847de3ea04c7a3c0bd2623fd1788ca7cc882bf702406fd344146", materials = [(kind = :conductor, tag = 10001, mu_r = 0.999994, sigma = [5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7], omega_epsilon = [5.563250277239352e-12, 5.563250277239352e-11, 5.563250277239352e-10, 5.563250277239352e-9, 5.5632502772393515e-8, 5.563250277239353e-7, 5.563250277239352e-6, 5.563250277239352e-5, 0.0005563250277239352])], earth_materials = [(rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0)], air = (eps_r = 1.0, mu_r = 1.0), mesh_plans = LineCableModelsGmshExt.FEMMeshPlan{Float64}[LineCableModelsGmshExt.FEMMeshPlan{Float64}(1, 0.1, 1006.5842420897408, 1258.230302612176, 25.16460605224352, 50.32921210448704, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (25.16460605224352, 25.16460605224352), (1006.5842420897408, 1006.5842420897408)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(2, 1.0, 318.3098861837907, 397.88735772973837, 7.957747154594768, 15.915494309189535, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (7.957747154594768, 7.957747154594768), (318.3098861837907, 318.3098861837907)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(3, 10.0, 100.65842420897408, 125.82303026121761, 2.516460605224352, 5.032921210448704, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (2.516460605224352, 2.516460605224352), (100.65842420897408, 100.65842420897408)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(4, 100.0, 31.83098861837907, 39.78873577297384, 0.7957747154594768, 1.5915494309189535, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (0.7957747154594768, 0.7957747154594768), (31.83098861837907, 31.83098861837907)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(5, 1000.0, 10.06584242089741, 12.582303026121762, 0.2516460605224352, 0.5032921210448704, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (0.2516460605224352, 0.2516460605224352), (10.06584242089741, 10.06584242089741)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(6, 10000.0, 5.0, 6.25, 0.25, 0.5, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (0.25, 0.14067442439954772), (10.0, 9.549296851139358)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(7, 100000.0, 5.0, 6.25, 0.25, 0.5, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (0.25, 0.04448515896357012), (10.0, 3.0197535662513486)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(8, 1.0e6, 5.0, 6.25, 0.25, 0.5, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (0.25, 0.014067442439845935), (10.0, 0.9549323148114102)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(9, 1.0e7, 5.0, 6.25, 0.25, 0.5, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (0.25, 0.004448515892915345), (10.0, 0.3019836725638405))], region_mesh_sizes = [0.0085, 0.0085, 0.0085], cable_outer_mesh_sizes = [0.0085, 0.0085, 0.0085], mesh_growth_factor = 1.2, options = (reduce_bundle = false, kron_reduction = false, ideal_transposition = false, physics = Symbol("quasi-tem")), execution = (ui = false, plot_field_maps = false, mesh_policy = :remesh, mesh_path = nothing, domain_skin_depths = 2.0, keep_run_directory = true, getdp_executable = nothing, gmsh_verbosity = 2, getdp_verbosity = 4, frequency_workers = 2, solver_threads = 4), supplied_mesh = nothing, owned_gmsh = true, getdp_identity = (sha256 = "49d44b1dfe13d9f9b63f8dfe37914af8947f856feebac13fbb6989a41aa88e51", info = "Version : 3.5.0\nLicense : GNU General Public License\nBuild OS : Linux64\nBuild date : 20220513\nBuild host : getdp.info\nBuild options : 64Bit Arpack[contrib] Blas[openblas] Gmsh Kernel Lapack[openblas] PETSc PeWe SLEPc\nPETSc version : 3.14.4 (complex arithmetic)\nGmsh lib version : 4.9.5-git-e286855ce-git-e286855ce (20220218)\nGmsh lib options : 64Bit ANN[contrib] Dlopen Eigen[contrib] LinuxJoystick Parser Plugins Post\nPackaged by : geuzaine\nWeb site : http://getdp.info\nIssue tracker : https://gitlab.onelab.info/getdp/getdp/issues\n"), getdp_provenance = (path = "/home/amartins/.julia/artifacts/51e049a32eeb2ffebcc8a5945bb00f70470a3d29/getdp-3.5.0-Linux64/bin/getdp", source = :artifact, artifact_hash = "51e049a32eeb2ffebcc8a5945bb00f70470a3d29"), gmsh_version = "4.15.0", gmsh_library = "/home/amartins/.julia/artifacts/13aa9c68927e7d05cb18794fbcfe42323c9c0fda/lib/libgmsh.so", julia_version = "1.12.7", adapter_sources = (var"model.jl" = "5dc038f3abd29cd606ab56ecded593b653d3c41cdbfc29e580c189ceeb008584", var"geometry.jl" = "6c6b4236d78945b54c72e7fd34d12a32ccadad75412bbc22ab70ff21df9822f1", var"mesh.jl" = "134502f8ca5ea3243151549b04780259b627adec1682cbfe13b209f9a6993de3", var"onelab.jl" = "baa1af49661cf66d88935d8fb08d7b2a35c7693bebb1fdf540f1463c91dc8164", var"getdp.jl" = "b6a8d3813ce7a200b01cc77428b20dcbb2728f9a4760a1cbde84ef9a698ddd6b", var"voltage_paths.jl" = "81bb6ad0c4078af4f5cce6dcae2bafb6ed60a8374345dfae0b8f61369d61e786", var"workers.jl" = "62ffec093cda6f52d4f08206771dcb82e16e80175ebac6200196c9ed6ffd34ec", var"results.jl" = "bb48b425e16a9048bdf0a4f73c6d7345554d22e396f8d371aa926d4c665a3e4f", var"compute.jl" = "152e200c45a72dab0c2a439900d98a1ec18a6377f2b640ee271850f1ac74b702"), solver_sources = (model = "c0bb424db5ab4db9ca22f549f675edb4646f69910d2aa14e20cf4f40e41bd313", jacobian = "014208d39043ab13183c42c312aba459265715da6d6ddc38ec1fbc39b6090c73", integration = "e113951ac1e6e658f1be50e53875e30c85497445f0e6584a8a7ab1848588e902", materials = "581f089113810a379d8a49c03d79f55045b723c8f4c761d0a493a019ff1f5d9e", quasi_tem = "448d9019ad417c9a784da20170b855dd25e559297499a83d19b007ee095c084d", quasi_full = "1aede35114ea686c5f109089478027d241dec12dc9fd14a24452b348f89b08e4")), terminal_ids = ["cable_0001/three_bare_wires_all_air/core", "cable_0002/three_bare_wires_all_air/core", "cable_0003/three_bare_wires_all_air/core"], reduced_phase_map = [1, 2, 3], inversion_residuals = [2.220446049250313e-16, 1.1102230246251565e-16, 1.1102230246251565e-16, 3.3306690738754696e-16, 1.1102230246251565e-16, 1.1102230246251565e-16, 2.220446049250314e-16, 4.440892098500626e-16, 2.220446049250329e-16], condition_numbers = [1.8034560924630167, 1.803389646420359, 1.8036317883072202, 1.803890794001709, 1.8046821008909093, 1.8037287948648788, 1.8039147155548123, 1.805116045060735, 1.9345776085985984], primitive = (Z_primitive = ComplexF64[3.156283574743683e-6 + 1.238120818418273e-6im 9.823567407316237e-8 + 8.127501579974236e-7im 9.823035079922834e-8 + 7.260105592328511e-7im; 9.823567407316238e-8 + 8.127501579974236e-7im 3.156285235382608e-6 + 1.2384264704427096e-6im 9.823558832407665e-8 + 8.127589304063154e-7im; 9.823035079922827e-8 + 7.260105592328502e-7im 9.823558832407659e-8 + 8.127589304063148e-7im 3.156283423940748e-6 + 1.2381628940589164e-6im;;; 4.044133578776191e-6 + 1.0950406484961215e-5im 9.753964131831537e-7 + 6.696910388622472e-6im 9.749158577325954e-7 + 5.829623636665569e-6im; 9.75396413183154e-7 + 6.696910388622474e-6im 4.044303344578034e-6 + 1.0953379895793153e-5im 9.753949575745819e-7 + 6.697219465457668e-6im; 9.749158577325958e-7 + 5.829623636665572e-6im 9.753949575745814e-7 + 6.69721946545767e-6im 4.0441325229622845e-6 + 1.095149327221179e-5im;;; 1.346443874653781e-5 + 9.493134232919261e-5im 9.545328479702395e-6 + 5.280770955685593e-5im 9.522720594805466e-6 + 4.415151357304126e-5im; 9.545328479702398e-6 + 5.280770955685594e-5im 1.3470778065139108e-5 + 9.495161188580657e-5im 9.545361931628984e-6 + 5.280920269962274e-5im; 9.52272059480546e-6 + 4.4151513573041283e-5im 9.545361931628982e-6 + 5.280920269962274e-5im 1.346444009793527e-5 + 9.493303970559921e-5im;;; 0.00010109173736374871 + 0.0007939837243350234im 8.941916606382008e-5 + 0.00039105609314453346im 8.876174532478729e-5 + 0.0003047968707955215im; 8.941916606382014e-5 + 0.00039105609314453335im 0.00010111751936580979 + 0.0007941180501357019im 8.941802142762202e-5 + 0.00039104147926472346im; 8.876174532478729e-5 + 0.0003047968707955216im 8.941802142762198e-5 + 0.00039104147926472346im 0.00010109212945278526 + 0.0007940030531517589im;;; 0.0007860169919852308 + 0.006616849225223113im 0.0007458892680923851 + 0.002672163416604545im 0.0007185907716457507 + 0.001824605919080821im; 0.0007458892680923841 + 0.002672163416604544im 0.0007861049543362728 + 0.0066165057334950745im 0.0007458750783723221 + 0.0026721706045853234im; 0.0007185907716457501 + 0.001824605919080822im 0.0007458750783723209 + 0.0026721706045853247im 0.0007859863083314754 + 0.006616315783085968im;;; 0.0051024009974632 + 0.056494349006063566im 0.004801035325114453 + 0.017380520692016812im 0.004138851293883464 + 0.00960800241714144im; 0.004801035325114454 + 0.017380520692016805im 0.005103707642967673 + 0.056498447347698745im 0.004800932676084752 + 0.017380882154370954im; 0.004138851293883463 + 0.009608002417141439im 0.004800932676084757 + 0.017380882154370954im 0.005102028251144334 + 0.05649251949500398im;;; 0.024707771051829367 + 0.5118713443394216im 0.021555696037850045 + 0.12588810726341756im 0.015531651186217865 + 0.059558666775737366im; 0.02155569603785003 + 0.12588810726341754im 0.02469664297746775 + 0.5119012048567243im 0.02155626682168506 + 0.12587112637670345im; 0.015531651186217852 + 0.059558666775737415im 0.02155626682168506 + 0.12587112637670345im 0.02470883208208863 + 0.5118452325239257im;;; 0.09297323751838378 + 4.915361205069794im 0.07673594391325751 + 1.092058127543065im 0.05049478438383373 + 0.4894236059367429im; 0.07673594391325754 + 1.0920581275430643im 0.09290126439815159 + 4.916091074643883im 0.0767356215297192 + 1.092127905866533im; 0.050494784383833737 + 0.48942360593674267im 0.07673562152971927 + 1.0921279058665325im 0.09296825879448868 + 4.915097631787866im;;; 0.5312197671741032 + 51.365101859919996im 0.4667934300847925 + 13.069443957199377im 0.3653621320274861 + 6.974813713796497im; 0.4667934300847931 + 13.069443957199393im 0.5279519445877036 + 51.32627476456146im 0.46209227148974574 + 13.00760233714735im; 0.36536213202748624 + 6.974813713796501im 0.4620922714897457 + 13.007602337147338im 0.5218504527366062 + 51.239123582658166im], P_primitive = ComplexF64[0.1678918336022674 - 1.0950283733279399e11im 0.1644000031914332 - 2.293388095261864e10im 0.15702679576933287 - 9.910677970544819e9im; 0.16440000319143322 - 2.2933880952618645e10im 0.16788954099799616 - 1.0954074612480457e11im 0.1644004446500366 - 2.2936626315257668e10im; 0.15702679576933287 - 9.910677970544823e9im 0.16440044465003664 - 2.2936626315257668e10im 0.16789792555616811 - 1.09511003294597e11im;;; 0.1317500982435688 - 1.0950948013084763e10im 0.12825997581143758 - 2.293160866097902e9im 0.12088975278609783 - 9.90833801154083e8im; 0.12825997581143744 - 2.293160866097902e9im 0.13174682608342023 - 1.0954464443250582e10im 0.12826023182451265 - 2.2938149346586213e9im; 0.12088975278609777 - 9.908338011540823e8im 0.12826023182451274 - 2.293814934658621e9im 0.13176292263747016 - 1.0952552195830395e10im;;; 0.09618536273638748 - 1.0950519387848685e9im 0.09269072249082928 - 2.293776748464292e8im 0.085331077326664 - 9.912378436379313e7im; 0.09269072249082927 - 2.2937767484642923e8im 0.09617632146562104 - 1.0953991850023596e9im 0.09269226884954948 - 2.2940160969314972e8im; 0.08533107732666399 - 9.912378436379313e7im 0.09269226884954951 - 2.2940160969314972e8im 0.09617860312035065 - 1.0951261144624276e9im;;; 0.06231130110588007 - 1.0951461121085618e8im 0.058848504124828144 - 2.2949025417543363e7im 0.05158644800159058 - 9.918816744080542e6im; 0.05884850412482814 - 2.2949025417543355e7im 0.06230224627789516 - 1.0954954164850722e8im 0.05885164736616918 - 2.294418011683387e7im; 0.051586448001590575 - 9.918816744080544e6im 0.058851647366169164 - 2.2944180116833877e7im 0.06230979792958018 - 1.0952064623641102e8im;;; 0.033010806686801855 - 1.0957491818704447e7im 0.029773916796851065 - 2.297032249235774e6im 0.023192780774531064 - 993174.8309388722im; 0.029773916796851072 - 2.2970322492357753e6im 0.0329943223913019 - 1.0958122567372698e7im 0.029772364785236552 - 2.2972088622141657e6im; 0.023192780774531047 - 993174.8309388722im 0.029772364785236535 - 2.297208862214164e6im 0.033009944083797135 - 1.0956436729337426e7im;;; 0.012986888106888963 - 1.095394281430624e6im 0.010727872150437861 - 229443.064134681im 0.006744236375519118 - 99057.25924738745im; 0.010727872150437856 - 229443.0641346808im 0.012976041298589676 - 1.0955699298034906e6im 0.01072756049056026 - 229450.79760403855im; 0.006744236375519115 - 99057.25924738747im 0.01072756049056026 - 229450.7976040387im 0.012988002366681317 - 1.095343332332149e6im;;; 0.0040919295377149344 - 109547.83918682227im 0.0032291248138142563 - 22951.74855321228im 0.0019761571566826205 - 9909.767492775569im; 0.003229124813814254 - 22951.74855321229im 0.004087677205896245 - 109560.51742447035im 0.003229356093213226 - 22947.6448165311im; 0.00197615715668262 - 9909.767492775565im 0.0032293560932132267 - 22947.644816531094im 0.004092280950978976 - 109541.6165511105im;;; 0.0012726707274377644 - 10956.989061906423im 0.0010144162296366322 - 2298.100413940254im 0.0006341278385427699 - 993.9505240574919im; 0.0010144162296366324 - 2298.100413940255im 0.0012715094607191927 - 10958.81317610521im 0.0010143820704403448 - 2298.2086907542584im; 0.00063412783854277 - 993.9505240574921im 0.0010143820704403453 - 2298.2086907542584im 0.0012726241711060581 - 10956.29165905885im;;; 0.0005071898482327651 - 1136.4725477876398im 0.0004225547887416437 - 266.77742139868855im 0.00029359937174952463 - 131.31261219800447im; 0.0004225547887416434 - 266.7774213986885im 0.0005073285559638625 - 1136.6854131035277im 0.00042246384039667807 - 266.6848634562801im; 0.00029359937174952474 - 131.31261219800444im 0.0004224638403966781 - 266.6848634562802im 0.0005069684091590665 - 1136.225379182583im], phase_map = [1, 2, 3]))) \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/fem/fem-artifacts.toml b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/fem/fem-artifacts.toml new file mode 100644 index 000000000..e6d3f2617 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/fem/fem-artifacts.toml @@ -0,0 +1,296 @@ +"attempts/f0001-7NRuy2/attempt.json" = "0f2bbaf6f77f83e57852f9a8e4c0adecc60d15ee4e6373c51efa617504522555" +"attempts/f0001-7NRuy2/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0001-7NRuy2/getdp.log" = "39f3d76d963c3271d0de811899e0b65320ef6c024b28393ec50e39798f1eb568" +"attempts/f0001-7NRuy2/petsc.log" = "f8b913b392c22084509e558e08596f91e424e07a3580a2cf92ff1cbce6db9ad9" +"attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0001-P.tsv" = "031a280c87ddc96904d1f86d1ef5415fca772c6b3ce3ce4300bdec75de98eeeb" +"attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0001-Z.tsv" = "c2163b4e92999b1751331366e286a7b55c548f1d488467a05b277b4190b5bdad" +"attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0001-timing.tsv" = "fec35bd592ad36327d68b415791e6dfc12587c27e2386f483cbff814ee9f7797" +"attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0001.done" = "a40a11bab26b32dfb169550347f289dc710df24f59fe3ef20963d46f838f0586" +"attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0002-P.tsv" = "86a470e1837a982ad0b60df3a9f2bc036f0b04fba5509a142e90d079273ac195" +"attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0002-Z.tsv" = "3f340376a310d046c7a8ddf7c4edfa2c2ec4ffb1f2b39eba79352d247f3ae6bb" +"attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0002-timing.tsv" = "f035f9a83a0bc955a45d10e11410210082cedf40a3d4e917c2e93d54f6a46733" +"attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0002.done" = "f58042f034284ccf2955f2795037937a5bd0e4288ffc0d0d5124e33ee07f17ec" +"attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0003-P.tsv" = "65167a9aa871d23ab936b1bb21bc509b8d74d94242e6297cfc2451ce806a00b3" +"attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0003-Z.tsv" = "89773a7454a74c37898661a0397237a6a51c7b1225697078d3d358ce2680e5a9" +"attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0003-timing.tsv" = "9133b1cb3a423655d0379c70af7a47034b559065727f192cb68af9312dc47e77" +"attempts/f0001-7NRuy2/raw/jobs/getdp-f0001-b0003.done" = "41695cf0039479bd9b703276e3c15366a77a1e43f8490b48634d8a1c2a61be94" +"attempts/f0001-7NRuy2/solver.pre" = "99dc9e3ec2c1f1b455c47a6dd9ab8676aefe770fafb313754b2d26b782afcabb" +"attempts/f0002-jWBavL/attempt.json" = "ce2919d3b38e7564d58ce864486030f275f1c3ac99e3f59a095b7575520e165e" +"attempts/f0002-jWBavL/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0002-jWBavL/getdp.log" = "ebd30293a1cf71debce9742a4939a6f8285abc8aef4857ac0e83fdfc45785eb9" +"attempts/f0002-jWBavL/petsc.log" = "4952a5630d94ef9e5576125f3711110714defc14aa9307a5139319b2f6b27f61" +"attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0001-P.tsv" = "75360e62d1f6396fe7bf30be166ab6b76ced00b406d41e3cff7f921310f4638d" +"attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0001-Z.tsv" = "97214f5c8ba29de45e9d35e462a2b3d6ef3eb9d0959812aa835286a0e33491f3" +"attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0001-timing.tsv" = "067ab77f6136d789cb6e8a80b579d471e9917b0f82993ee0472f6b1033a40290" +"attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0001.done" = "3dbd9539f415ee889a34b9c6efe6b9cb8168b7c0945d52d7e63e920c8990d2d1" +"attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0002-P.tsv" = "790d625eddc047985df0af0c08e1984183df9bac9dc59a8107d67c0223829a68" +"attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0002-Z.tsv" = "4a9daa553ea47fd0f118a09f27cf90351c79fc6f59aca92a9079e94b74950974" +"attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0002-timing.tsv" = "493434a9b35cfb2bc991dc0ee378b19b6c0c3c3a8f18e67300fb8379ff3f0aa5" +"attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0002.done" = "d498381e814531161686031fcde5aa549b456c6cbee61f6eb67916650247e717" +"attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0003-P.tsv" = "9a0f04d6a64b9690bb4304da7520f2ebd37d7749d0c746d1a01b0d759641283b" +"attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0003-Z.tsv" = "e0ab6f15a6d62ebef17c41c6f20586c61499c3898af3ea9d65801b1dd20450f6" +"attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0003-timing.tsv" = "6ad68000f813c19cd34f8b178f16c5539b54ecfd269035c408d4fbce139dd9aa" +"attempts/f0002-jWBavL/raw/jobs/getdp-f0002-b0003.done" = "fa5c0c361441906d1aaa6b254ae52969806be93c36846f4c70c70a18dc833050" +"attempts/f0002-jWBavL/solver.pre" = "310ee72c13d3183e8ac6d8cb00410b455758a676ee2ce377e2a0dfa6e5449b43" +"attempts/f0003-JRn69i/attempt.json" = "3f721255cd5e1bfe319820ea11e0ae9f92f96a49ac106a9204c085bfa8373502" +"attempts/f0003-JRn69i/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0003-JRn69i/getdp.log" = "353e3a75bad8c1f6fa3570751456f76937d01f3359ccd94a9d35a55aa1534a82" +"attempts/f0003-JRn69i/petsc.log" = "d4bd0b9603ae2ad0399c690c2410ad4cb4f51c5c485e7d2bbae2b8c9283323f9" +"attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0001-P.tsv" = "8c89e9137f9157c4fcbb7f19ed185a3b1755fc8c475671f75a5c138cb326b187" +"attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0001-Z.tsv" = "e392c6378c451328d736fae1d29e818251caf06e40928c154fb30f3abdca563a" +"attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0001-timing.tsv" = "e62951a512bc6a0927b91df494b1b058821b7547d85dade8747fb960f9b210b0" +"attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0001.done" = "02395cb824ac1c8ce1d2e8cd3c57fd19d5c904215aef49b1fba677c51e99d519" +"attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0002-P.tsv" = "2e7b78e1e6203a93287a3758917f0f4e0a8dd5a2c163824e548d65bd8252009b" +"attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0002-Z.tsv" = "48219f308edd90574abf7865948acb574d675bc223eaf505e034822a783162dd" +"attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0002-timing.tsv" = "300f2d86ea6705d8df22e7131131c15d36476a8535f756e54bfeac8d6244dd03" +"attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0002.done" = "84034583a44a97df0ec5820a76fc891b95a9b6bc0bf693bed9882420a3eb5b05" +"attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0003-P.tsv" = "01a9abbfc7ac762dd2ca47f576452b9ab89f745a4736f9b5298c7059b67e0b4e" +"attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0003-Z.tsv" = "998a2152fa3e61432a06dd2fb1549914a9bccfbd27a91ac88a28e69d155da0a8" +"attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0003-timing.tsv" = "4244046606e8dfab3ab2cdd7a5a490edd70cbf2f780476a4fa83505836766943" +"attempts/f0003-JRn69i/raw/jobs/getdp-f0003-b0003.done" = "aec9dd1bab4102d3c07f9c6abf83e4d604721961d8e37cbbc9620f7f71cafcae" +"attempts/f0003-JRn69i/solver.pre" = "3cac9d6471bf7bb1fff80012cdc8f2fbdf842145fba4d71bd8dc1b0cec7dd6e2" +"attempts/f0004-H0KlIr/attempt.json" = "0a5f1a88dbf61ec72e760165f4e68165c027616d9e8ef4b449f76a562656e4ca" +"attempts/f0004-H0KlIr/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0004-H0KlIr/getdp.log" = "0bff62190696542513d5212a1a54d5cfbef5badd8e2eed4cbf4f72fbe8b2c435" +"attempts/f0004-H0KlIr/petsc.log" = "dbd2fece68fedf85ceb1116de0e8001f8029e095de48dec5963f2ca984b94410" +"attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0001-P.tsv" = "5d4004862d3d0d80660a14391f0b1ff16ca269d36e14cf4d9eab44c1f0508b46" +"attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0001-Z.tsv" = "4fa281428aa093aedd060a88d6a0395519fcd9704aa92d11e6f744217e04cb35" +"attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0001-timing.tsv" = "bc38c85ddedbafe212c6c766d6156cf67b285f0f4d2017bf23aa875229230d4c" +"attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0001.done" = "05b52afc2d2a0ffefd0bb6e40585165d1b2572547d59d3c360b4163202709006" +"attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0002-P.tsv" = "dab42728b3411be92f90c8ea8b1b252e86bebcfda885ebf09268009a05f1cf79" +"attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0002-Z.tsv" = "fa0c078e62461975905443eb2ce0bbf2bf4630dbb987a114389e8a57a8da87dd" +"attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0002-timing.tsv" = "2836501b252019196717fecf2a9ae89b0ecf1b14823706d5ffffb73f6a72e104" +"attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0002.done" = "35d216e20aed80a36327575b350d8e4c12b70f2038f5d07d55614c92afdd6b6b" +"attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0003-P.tsv" = "2943f3ccc2a6d1dcb7e1048402eda28d3ac814489a6bd08b90b7f18248097e57" +"attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0003-Z.tsv" = "8fb401c5207e00b3e9f2ef769f807c8069860a0da7ad72726f31c51ff97eb048" +"attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0003-timing.tsv" = "62b6ec66fd6d6bc029a6ddea6372b9e25ef0b520585b28e7bb004ef4eee81f01" +"attempts/f0004-H0KlIr/raw/jobs/getdp-f0004-b0003.done" = "eae2a23f21e24dd158a817c096e6e77f32e1d2a850c4163d279c3531eef2af5a" +"attempts/f0004-H0KlIr/solver.pre" = "84fc9a2a171fb180ee858b88ce101a1f4e7880f4b64622a09d64c5ede01879dd" +"attempts/f0005-yXakvi/attempt.json" = "33462aa5e71dbabb439fe596965b1dd4a0bcabc94afaa103dca79e6404fc472b" +"attempts/f0005-yXakvi/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0005-yXakvi/getdp.log" = "d7f274181d4d34a0f348eaccb2ce6b884dd426c053c490291a8deab790ef32b2" +"attempts/f0005-yXakvi/petsc.log" = "d13aed750d08ea36d58913e719d499217fdd4ea74df09537187614d329339c9b" +"attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0001-P.tsv" = "95be2f65e1d4a75ecf78c946091a48fc88d0aa7f377314f7eafff3d07d66063e" +"attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0001-Z.tsv" = "08efd37d7184ef83df023c05c4252e34ef6837c05b1d2536961cf8f578e1498a" +"attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0001-timing.tsv" = "ba24f1db805497624823cbd28ea4d3a62fc1c8c943fa4af2f989779f3087a4f3" +"attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0001.done" = "ee7bf9802d600055d484ec4e501666418037b219c7bf910eefda35e16a843b08" +"attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0002-P.tsv" = "f12870a70dabd22b1f8ad9983660e86610763eb1fd54a7ad6a4ee13128610582" +"attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0002-Z.tsv" = "0ae728113cd00cd81fc7c4e273a38bb5392397b2215d3c9383262ac11b2e1971" +"attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0002-timing.tsv" = "f42e70be94f6183e5a9d1dc2c665cce176f6414f7dc513dc1c953318cf41cfe3" +"attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0002.done" = "e26c3631aaa36c47efb3ede81242002dbebf27057f428fe57f60849cede6b3f3" +"attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0003-P.tsv" = "efef11eba3bb928a1d2ffef23e345fbc2b46ba9da84dbdaf95592f17e7e87e04" +"attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0003-Z.tsv" = "fc56dc47dff9b7da06d88d76e886ff130d29538b8e9c97b6db83c4f9f466eede" +"attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0003-timing.tsv" = "5efd4f2ffe191f5682583e0dfbeec9c6287151ff82cdd590e6a45e4f67502405" +"attempts/f0005-yXakvi/raw/jobs/getdp-f0005-b0003.done" = "a91d7ca1bb120ef43c71df371c45a71b1cc7744f1ee2eb082338700de7f329c9" +"attempts/f0005-yXakvi/solver.pre" = "daca4fa3af8341b0d7bcfa961b3d5627fc53dac7f9d044eecdcd605aee27a791" +"attempts/f0006-ndYYTp/attempt.json" = "72f9c4d1fd54ee122478e6a0195d62b8eef44ee1f5df50c522a727b2d8e14473" +"attempts/f0006-ndYYTp/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0006-ndYYTp/getdp.log" = "9c9539dea9579a1424de9c640ac4748edb833d36d729ef411c1507ca68d6c11f" +"attempts/f0006-ndYYTp/petsc.log" = "78f5c4f2de48c6f38b0ae5e1effe998f7a4951d4c49953ca0ff6d8b220ab795f" +"attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0001-P.tsv" = "1a36d60a0e090f2caa2cee9f7d46c7c514a2395f67831d7d9a4b4f0efa58cfbc" +"attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0001-Z.tsv" = "62e6757e71fef71e561b10e28c9051bb4b86e88c7a41b23a88adf304bb43f71b" +"attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0001-timing.tsv" = "cf010779455a1cb0a54599a104e3c82b40c06d5c66bf421df2bcccd906cc67ca" +"attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0001.done" = "e41a78288341a2ae156cd28b6102b3a31a74b012eb4deb233e88fae392912e8e" +"attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0002-P.tsv" = "3c04d81056ac404e60379b85ba6dba801725fa135594030f6995a4895821e26c" +"attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0002-Z.tsv" = "269547724c63e0a2226f7e41fcf61c3409afa7c7e680f931e177af978642cf59" +"attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0002-timing.tsv" = "34554347ed48775dc282d912dda0c27e8141936b83e15abda66b2d6b9e5462c6" +"attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0002.done" = "c105367e4bac19858725b1b7c56efbb25ae370173d002f9a678a4762a5c69ac9" +"attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0003-P.tsv" = "73b7fdf511c7c409936ae5eb019ede5429469504d0ca1cb86baf647b8322c3fa" +"attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0003-Z.tsv" = "a21e602b7b8ae32b66915d272d3186c31cd7842c02667c602064f7598716f291" +"attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0003-timing.tsv" = "865fceb647473629d0fbebb2e971d642a44f4e3a299376b5f6e02e8487969a32" +"attempts/f0006-ndYYTp/raw/jobs/getdp-f0006-b0003.done" = "de6214d7f2cf6d2097d94d720289a7424da0b9bf7a5df27326d8b69e936798e8" +"attempts/f0006-ndYYTp/solver.pre" = "cfdb2e89538a7f1b844294060c303c14f90ef01eb8a0bf67d2a284eb3eeb97cc" +"attempts/f0007-G1cv2g/attempt.json" = "d5111d96f6a3686d450cbb3fcf9f785b966c976269a11746d8ae9a300396c0b4" +"attempts/f0007-G1cv2g/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0007-G1cv2g/getdp.log" = "eb635f1319c6629091027d9fd37a3828487c4a6f5f7f3356fc2a614f5b6f7ca3" +"attempts/f0007-G1cv2g/petsc.log" = "e27a2f3bd5b7d36853f6ff45aeffdcbacefa671200c5c50308762fffce54a9c6" +"attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0001-P.tsv" = "2fb1d14577dc5925f78e9541758ae3b8667618ec0582c5cddf727c1220f83f23" +"attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0001-Z.tsv" = "f1f34e597d4653c8cfb0551ac7b2a8e82a5835c14f7bc4d16244d53564e12b03" +"attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0001-timing.tsv" = "79bec814d003d677859197c36a54d06b33b5592483616194dc1cda2ed330de09" +"attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0001.done" = "d95e4d651bb7fa68e36e0077af5a10e10baacf852173da7ac0f1cca3e414dc7f" +"attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0002-P.tsv" = "88c5897a343d651d77168dcccdd84b304ab89aaffd9063878c8b87761ece1ed6" +"attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0002-Z.tsv" = "d3635277a9973c4d18b6482f2c33d73654ccc35f066b551bd17e2fdbd6850ff4" +"attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0002-timing.tsv" = "14661c5ed6b2be7497371c93164997c788b960448c05e13720cbd00f3cbe9cfb" +"attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0002.done" = "a953202777b2aea0282f4e5ecac9feb227f850cb3f80f2247ca07a708f3635be" +"attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0003-P.tsv" = "fde259d49daf8d0728a8d6ccc708b458aca7d655b0d89562474d7960d7a55c94" +"attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0003-Z.tsv" = "a9acd7442ff0d134b7a65270ffc58e2114eee22ce422635c2b21f0ac9fa1d31a" +"attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0003-timing.tsv" = "4bfe3b64dee4e687f467bbdc044d55fd6fea1f78201b9f316040fa82e46792be" +"attempts/f0007-G1cv2g/raw/jobs/getdp-f0007-b0003.done" = "bc6e3e671a966700354c8fb7f9ab0ff830db8a4b88332e0ec4974ce29df37b0c" +"attempts/f0007-G1cv2g/solver.pre" = "35b75bea2130c9af1b19db06c29b4a4d4ed8d53d02a1c60f5a0e2a51ae6e61f7" +"attempts/f0008-wUdCI1/attempt.json" = "8cd81f8cd9fc0b36ec1ce765d8ae9ab5a4d197a1d7ca82c7423d2473c19ee6c4" +"attempts/f0008-wUdCI1/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0008-wUdCI1/getdp.log" = "b0eb2de540ba9494f3e1993d37aa7c02597f18e4619a60f316b5804f7a036a23" +"attempts/f0008-wUdCI1/petsc.log" = "6a935d0c75793172d490a82a93b2d38d41a1959dc5225544b5e78cf35d215797" +"attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0001-P.tsv" = "412308a407308fca889dee31cb0f4ddfe43f3464aabe4ffe8648558dab68e053" +"attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0001-Z.tsv" = "2ec4bf6591207af6e57ef0941c6a37537d16ef0d9b1c67f0c07930c38360135b" +"attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0001-timing.tsv" = "de27cb70de616419e2207d7d7176ff95c060aaa7b0d22557b0b591abb39377a3" +"attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0001.done" = "c03ca6db0131d3218478fb4e85f0350dca26787b9ffdfc9d50b5d68ad93b352c" +"attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0002-P.tsv" = "b4928623e3b217cf5bc370f5152cd2e723cdd9a24dce1b604a2fa76ddbd59f4e" +"attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0002-Z.tsv" = "c2d607d773deb123b6342f6d3ebb18954babe1d7ce20e402b55758ed2776d6a2" +"attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0002-timing.tsv" = "aa3c56bdde4e222e0ea9a8aa3273dc28e0decdf55a14af28a5df5f13b9601fab" +"attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0002.done" = "76f50b528daca79ab1eac47c47f96ca1e7b8ad94bcaefbaac04335941306f993" +"attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0003-P.tsv" = "241c0469357c8bc65f03e3a385e6b0ab7f72e05e477b30c78adf613e8a97aa9e" +"attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0003-Z.tsv" = "ef5b33d8131365e9b063db67172cf66351886cb5b946691d6ada15a21cdd2fb5" +"attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0003-timing.tsv" = "69a5283f85550a89524284e9f638ac3a806fa220448ea0ec0d37732ff6ba31d4" +"attempts/f0008-wUdCI1/raw/jobs/getdp-f0008-b0003.done" = "20bdbe7d9351850f55ebf49a31f3a93235d04aea44253ddd2d75828a83fa1c74" +"attempts/f0008-wUdCI1/solver.pre" = "b3b00cb8bf2a65215251ad52d1a0e372205e91f8ace7480064d51dcdf635bb81" +"attempts/f0009-LD2vT2/attempt.json" = "a96b88ab0871d4dd6244aa614ee14e0963838948673dcf4154eb12ff8545e1fe" +"attempts/f0009-LD2vT2/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0009-LD2vT2/getdp.log" = "fb775c552b660bee7b3a352cf64934ef18070c573d0c20697b6849ff732b98ff" +"attempts/f0009-LD2vT2/petsc.log" = "bfdc97d923a620b0d40994655065cc401db8b156dd9a3e2cf36f40f51f21fe90" +"attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0001-P.tsv" = "cf5f55fd71d26c7ef644a0bb9d00fbb04a47411fde6215fe59de29bcb10b9eaf" +"attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0001-Z.tsv" = "3fcd842bf3cf63e79bc3e8f623626ba34eeacc992a1b810d1289b0f6945003a3" +"attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0001-timing.tsv" = "6f08951d35fb19246f8f093d2155c99243b2cdf39b85234604ff9e10b4efb576" +"attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0001.done" = "22b8b831af1067ac03eaf9570ddd71235af29f6b65e559199200376f6ef90f2a" +"attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0002-P.tsv" = "4d85587686a4a1b58da0074626478ff1ccc4899449ffb5958252e5324ad524f5" +"attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0002-Z.tsv" = "60aa2fe4f7f9dfc5e8fea8c0d780ea754b220a672a613ee9650ce5d398a13926" +"attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0002-timing.tsv" = "7154dfc17be5cfd92161898e03253129472470ccee24c0599d905ba61208842e" +"attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0002.done" = "2ab6b278377a8a5090d5d456a9193028844db99b8f3fa96c2433d70920e73c22" +"attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0003-P.tsv" = "8937eba2516490766a41906e7ee715b6aed994d21e9ae295598c93f7b29234b9" +"attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0003-Z.tsv" = "f4f06bdabd1bb68cae14f42ddac34584e7f6994b27d7142030192e6935c20800" +"attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0003-timing.tsv" = "f5f59f01a884e46e8622c088a68404011a16e00c74bdc6740495e9c5a5328dfb" +"attempts/f0009-LD2vT2/raw/jobs/getdp-f0009-b0003.done" = "3449a1ca026b39cca413695b5bd20e176addf6bcf3103f9147e8ac56407cafa3" +"attempts/f0009-LD2vT2/solver.pre" = "c468c1eb5023edd92956da2dab13e42e9bbc733c725034d12f238b5deb6d5f47" +"coordinator.lock" = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" +"input/computation.json" = "7f88c0b11d387951ddacf8611f94f9d06188ca5e474dad26532aece3e7be2f25" +"input/getdp/integration.pro" = "e113951ac1e6e658f1be50e53875e30c85497445f0e6584a8a7ab1848588e902" +"input/getdp/jacobian.pro" = "014208d39043ab13183c42c312aba459265715da6d6ddc38ec1fbc39b6090c73" +"input/getdp/materials.pro" = "581f089113810a379d8a49c03d79f55045b723c8f4c761d0a493a019ff1f5d9e" +"input/getdp/model.pro" = "c0bb424db5ab4db9ca22f549f675edb4646f69910d2aa14e20cf4f40e41bd313" +"input/getdp/quasi-full.pro" = "1aede35114ea686c5f109089478027d241dec12dc9fd14a24452b348f89b08e4" +"input/getdp/quasi-tem.pro" = "448d9019ad417c9a784da20170b855dd25e559297499a83d19b007ee095c084d" +"input/model_data.pro" = "bdd987fd050af492aeec116b53eabe74bae14953bff8ea41534ae6aaf722cb22" +"input/problem.json" = "bfa5a741cc75bd7b7a7ce917a85836bc628af114b0ce0462110fc37279424cd8" +"logs/getdp.log" = "63791f3412510c8e158ee8889ac002e00d1e569dcbccb0e6689ff6c61c562bf7" +"mesh/frequency_0001.json" = "55bd35bc31facd4ab58cf8f4a57bf9d1d3cd930742fb85d3a9684a5f4cd0d056" +"mesh/frequency_0001.msh" = "38384c803a509cdfbb27513b23180d2dcd6f62b655bd374cb0861c50c7c9c11b" +"mesh/frequency_0002.json" = "8d0c820b30fbb8266f5f0bd227e3653d2618d671dd14ca6f3584787d5a201672" +"mesh/frequency_0002.msh" = "68a5235e6da5688300344ef472fb1ce30f3c9b270f17b8d9d584b8306bb69bde" +"mesh/frequency_0003.json" = "61e6550f797f5a2b22910280ca58d036748cad329c2410ee206b8046ea277f9f" +"mesh/frequency_0003.msh" = "d0bfd44ede5381a46b7e4b6549bf6fe606c77b344ed141b12836d9bd2d7f2aec" +"mesh/frequency_0004.json" = "e7c1a4d2fb48242b142eb2ad20cc58cacf1cdae0d86f7116fe0989c41c48edff" +"mesh/frequency_0004.msh" = "d5f14cc29e9767722151cdb86604ad138bc1f1ceb533a6190fc3639ee870a7ed" +"mesh/frequency_0005.json" = "b439939050c134ca8db4fa176fae7618b94391a6b004f50240a6f45e0f6ee785" +"mesh/frequency_0005.msh" = "2dc86c1f757405dc1f8b968c42bab7515dfa0085367c4af26372aeaeaad8ebd3" +"mesh/frequency_0006.json" = "edb036e71c6600753a74bf12e884376077f37f2c27fd2b4c2fcd60e7db3aed14" +"mesh/frequency_0006.msh" = "e59362b1faa322a9973a6dd64847c901b3b6c2beba9e750dbb0269b5cc56e979" +"mesh/frequency_0007.json" = "22c5cc5f5f055895ff9b5021b20b5373456ee835fd8ab46d8d66d787f1d933a1" +"mesh/frequency_0007.msh" = "9f174b7066d5b98dcc208e6901f5c79428984d399ced80b1eb2a89d2aeb20810" +"mesh/frequency_0008.json" = "aaa2b4dbee2529a5a13a554f8bf2bd4325408fda8b0ecb0065a1cf390d051f9b" +"mesh/frequency_0008.msh" = "13e29179b39e78b933a4bff61ec3c48d8c4079270ffa2aeadc4dedba633d6f49" +"mesh/model.json" = "5908f8e55adcea9f1ac1bf5a2f769de0caa3521cd865ab55d6071d74d38f369c" +"mesh/model.msh" = "f21eaef57ca94e028771e45111d66287e224c416be21d63d6f187a3b7f56900f" +"raw/P.tsv" = "e73177d930d18ec3a3d6c7f0c81d269daf82c3d2b9f60309662ae6547ef859af" +"raw/Z.tsv" = "ce1450aa75aeaced963056b53215092716a3490011ff2f8f518e372a6217879e" +"raw/checksums.json" = "76bc5639f764edadf46129bf4e2ca4e823a9b08e28afcc7ff43797c1cab23068" +"raw/jobs/getdp-f0001-b0001-P.tsv" = "031a280c87ddc96904d1f86d1ef5415fca772c6b3ce3ce4300bdec75de98eeeb" +"raw/jobs/getdp-f0001-b0001-Z.tsv" = "c2163b4e92999b1751331366e286a7b55c548f1d488467a05b277b4190b5bdad" +"raw/jobs/getdp-f0001-b0001-timing.tsv" = "fec35bd592ad36327d68b415791e6dfc12587c27e2386f483cbff814ee9f7797" +"raw/jobs/getdp-f0001-b0001.json" = "08afd3895c0b4bd4404edaefd386b45c6fa9f15f25f991aee9565c66f1c22da7" +"raw/jobs/getdp-f0001-b0002-P.tsv" = "86a470e1837a982ad0b60df3a9f2bc036f0b04fba5509a142e90d079273ac195" +"raw/jobs/getdp-f0001-b0002-Z.tsv" = "3f340376a310d046c7a8ddf7c4edfa2c2ec4ffb1f2b39eba79352d247f3ae6bb" +"raw/jobs/getdp-f0001-b0002-timing.tsv" = "f035f9a83a0bc955a45d10e11410210082cedf40a3d4e917c2e93d54f6a46733" +"raw/jobs/getdp-f0001-b0002.json" = "f11392bb58aaa338d24e295467841f1478e9e07e4128cb9a32fec4d97bd5c8d1" +"raw/jobs/getdp-f0001-b0003-P.tsv" = "65167a9aa871d23ab936b1bb21bc509b8d74d94242e6297cfc2451ce806a00b3" +"raw/jobs/getdp-f0001-b0003-Z.tsv" = "89773a7454a74c37898661a0397237a6a51c7b1225697078d3d358ce2680e5a9" +"raw/jobs/getdp-f0001-b0003-timing.tsv" = "9133b1cb3a423655d0379c70af7a47034b559065727f192cb68af9312dc47e77" +"raw/jobs/getdp-f0001-b0003.json" = "807066ffef45efe9353cab6d4225e25b0a305d811d4d2bcc443873f45197973b" +"raw/jobs/getdp-f0002-b0001-P.tsv" = "75360e62d1f6396fe7bf30be166ab6b76ced00b406d41e3cff7f921310f4638d" +"raw/jobs/getdp-f0002-b0001-Z.tsv" = "97214f5c8ba29de45e9d35e462a2b3d6ef3eb9d0959812aa835286a0e33491f3" +"raw/jobs/getdp-f0002-b0001-timing.tsv" = "067ab77f6136d789cb6e8a80b579d471e9917b0f82993ee0472f6b1033a40290" +"raw/jobs/getdp-f0002-b0001.json" = "e4b59bc1961dcd0c7b5d7b528bfc40dca6eec9d7f1171a5a0815c898f101e24b" +"raw/jobs/getdp-f0002-b0002-P.tsv" = "790d625eddc047985df0af0c08e1984183df9bac9dc59a8107d67c0223829a68" +"raw/jobs/getdp-f0002-b0002-Z.tsv" = "4a9daa553ea47fd0f118a09f27cf90351c79fc6f59aca92a9079e94b74950974" +"raw/jobs/getdp-f0002-b0002-timing.tsv" = "493434a9b35cfb2bc991dc0ee378b19b6c0c3c3a8f18e67300fb8379ff3f0aa5" +"raw/jobs/getdp-f0002-b0002.json" = "bdd435735409b936f07f27a616d49b0ab0dd3d196c91bce1a108b4a4b0f81e80" +"raw/jobs/getdp-f0002-b0003-P.tsv" = "9a0f04d6a64b9690bb4304da7520f2ebd37d7749d0c746d1a01b0d759641283b" +"raw/jobs/getdp-f0002-b0003-Z.tsv" = "e0ab6f15a6d62ebef17c41c6f20586c61499c3898af3ea9d65801b1dd20450f6" +"raw/jobs/getdp-f0002-b0003-timing.tsv" = "6ad68000f813c19cd34f8b178f16c5539b54ecfd269035c408d4fbce139dd9aa" +"raw/jobs/getdp-f0002-b0003.json" = "a09ca9165619eab61b6e7c35c373eccd9d160ddd93a8c8501e12ca9d9e6acd9d" +"raw/jobs/getdp-f0003-b0001-P.tsv" = "8c89e9137f9157c4fcbb7f19ed185a3b1755fc8c475671f75a5c138cb326b187" +"raw/jobs/getdp-f0003-b0001-Z.tsv" = "e392c6378c451328d736fae1d29e818251caf06e40928c154fb30f3abdca563a" +"raw/jobs/getdp-f0003-b0001-timing.tsv" = "e62951a512bc6a0927b91df494b1b058821b7547d85dade8747fb960f9b210b0" +"raw/jobs/getdp-f0003-b0001.json" = "34c5c81461612f9197de1927e502b95d02a3a617c9075d8f38cf2d09173fc708" +"raw/jobs/getdp-f0003-b0002-P.tsv" = "2e7b78e1e6203a93287a3758917f0f4e0a8dd5a2c163824e548d65bd8252009b" +"raw/jobs/getdp-f0003-b0002-Z.tsv" = "48219f308edd90574abf7865948acb574d675bc223eaf505e034822a783162dd" +"raw/jobs/getdp-f0003-b0002-timing.tsv" = "300f2d86ea6705d8df22e7131131c15d36476a8535f756e54bfeac8d6244dd03" +"raw/jobs/getdp-f0003-b0002.json" = "3a18ce34bd5ece68e55270458a8068cb7a7ee3e42cb0c2753fe3d02967eaa0ec" +"raw/jobs/getdp-f0003-b0003-P.tsv" = "01a9abbfc7ac762dd2ca47f576452b9ab89f745a4736f9b5298c7059b67e0b4e" +"raw/jobs/getdp-f0003-b0003-Z.tsv" = "998a2152fa3e61432a06dd2fb1549914a9bccfbd27a91ac88a28e69d155da0a8" +"raw/jobs/getdp-f0003-b0003-timing.tsv" = "4244046606e8dfab3ab2cdd7a5a490edd70cbf2f780476a4fa83505836766943" +"raw/jobs/getdp-f0003-b0003.json" = "f35ef6a68aabf978275307e64d7c27daefe68badb02fbe2f281813f4a14d9303" +"raw/jobs/getdp-f0004-b0001-P.tsv" = "5d4004862d3d0d80660a14391f0b1ff16ca269d36e14cf4d9eab44c1f0508b46" +"raw/jobs/getdp-f0004-b0001-Z.tsv" = "4fa281428aa093aedd060a88d6a0395519fcd9704aa92d11e6f744217e04cb35" +"raw/jobs/getdp-f0004-b0001-timing.tsv" = "bc38c85ddedbafe212c6c766d6156cf67b285f0f4d2017bf23aa875229230d4c" +"raw/jobs/getdp-f0004-b0001.json" = "a6612bd9ff45f14ab763bdc8bb1fa057601d5e4a262eaea9cc3f0eccb6418bbc" +"raw/jobs/getdp-f0004-b0002-P.tsv" = "dab42728b3411be92f90c8ea8b1b252e86bebcfda885ebf09268009a05f1cf79" +"raw/jobs/getdp-f0004-b0002-Z.tsv" = "fa0c078e62461975905443eb2ce0bbf2bf4630dbb987a114389e8a57a8da87dd" +"raw/jobs/getdp-f0004-b0002-timing.tsv" = "2836501b252019196717fecf2a9ae89b0ecf1b14823706d5ffffb73f6a72e104" +"raw/jobs/getdp-f0004-b0002.json" = "c46bff8847504c1a23ba5d0883381f17190c9e71248d21a9b459a96b32f745c2" +"raw/jobs/getdp-f0004-b0003-P.tsv" = "2943f3ccc2a6d1dcb7e1048402eda28d3ac814489a6bd08b90b7f18248097e57" +"raw/jobs/getdp-f0004-b0003-Z.tsv" = "8fb401c5207e00b3e9f2ef769f807c8069860a0da7ad72726f31c51ff97eb048" +"raw/jobs/getdp-f0004-b0003-timing.tsv" = "62b6ec66fd6d6bc029a6ddea6372b9e25ef0b520585b28e7bb004ef4eee81f01" +"raw/jobs/getdp-f0004-b0003.json" = "cc6d3516eb7bee8b57aee610f85f9ffbd5708110be6d5c956d018e377f4967c6" +"raw/jobs/getdp-f0005-b0001-P.tsv" = "95be2f65e1d4a75ecf78c946091a48fc88d0aa7f377314f7eafff3d07d66063e" +"raw/jobs/getdp-f0005-b0001-Z.tsv" = "08efd37d7184ef83df023c05c4252e34ef6837c05b1d2536961cf8f578e1498a" +"raw/jobs/getdp-f0005-b0001-timing.tsv" = "ba24f1db805497624823cbd28ea4d3a62fc1c8c943fa4af2f989779f3087a4f3" +"raw/jobs/getdp-f0005-b0001.json" = "e242e5e233fba51e92710201aa3cca53479b5047c2df1a7e0e2c3fea52de8c12" +"raw/jobs/getdp-f0005-b0002-P.tsv" = "f12870a70dabd22b1f8ad9983660e86610763eb1fd54a7ad6a4ee13128610582" +"raw/jobs/getdp-f0005-b0002-Z.tsv" = "0ae728113cd00cd81fc7c4e273a38bb5392397b2215d3c9383262ac11b2e1971" +"raw/jobs/getdp-f0005-b0002-timing.tsv" = "f42e70be94f6183e5a9d1dc2c665cce176f6414f7dc513dc1c953318cf41cfe3" +"raw/jobs/getdp-f0005-b0002.json" = "6c3253a4249ef87dd26e0ef335ea836bf75d0a440a1ccc0d88abe3dec9baf01f" +"raw/jobs/getdp-f0005-b0003-P.tsv" = "efef11eba3bb928a1d2ffef23e345fbc2b46ba9da84dbdaf95592f17e7e87e04" +"raw/jobs/getdp-f0005-b0003-Z.tsv" = "fc56dc47dff9b7da06d88d76e886ff130d29538b8e9c97b6db83c4f9f466eede" +"raw/jobs/getdp-f0005-b0003-timing.tsv" = "5efd4f2ffe191f5682583e0dfbeec9c6287151ff82cdd590e6a45e4f67502405" +"raw/jobs/getdp-f0005-b0003.json" = "bd9f15b88c30da6e32a151a995da74c6ff3bcc106bcc95d169eace5073810318" +"raw/jobs/getdp-f0006-b0001-P.tsv" = "1a36d60a0e090f2caa2cee9f7d46c7c514a2395f67831d7d9a4b4f0efa58cfbc" +"raw/jobs/getdp-f0006-b0001-Z.tsv" = "62e6757e71fef71e561b10e28c9051bb4b86e88c7a41b23a88adf304bb43f71b" +"raw/jobs/getdp-f0006-b0001-timing.tsv" = "cf010779455a1cb0a54599a104e3c82b40c06d5c66bf421df2bcccd906cc67ca" +"raw/jobs/getdp-f0006-b0001.json" = "0e7317bb8936cd3bda7016092043b08c86bef1c5108dc5aff8bc19693196903f" +"raw/jobs/getdp-f0006-b0002-P.tsv" = "3c04d81056ac404e60379b85ba6dba801725fa135594030f6995a4895821e26c" +"raw/jobs/getdp-f0006-b0002-Z.tsv" = "269547724c63e0a2226f7e41fcf61c3409afa7c7e680f931e177af978642cf59" +"raw/jobs/getdp-f0006-b0002-timing.tsv" = "34554347ed48775dc282d912dda0c27e8141936b83e15abda66b2d6b9e5462c6" +"raw/jobs/getdp-f0006-b0002.json" = "5f9f65cccf07a4fa2b8c58d4e9278441a5c8e71e41e67b4e10783cc3fb5eddca" +"raw/jobs/getdp-f0006-b0003-P.tsv" = "73b7fdf511c7c409936ae5eb019ede5429469504d0ca1cb86baf647b8322c3fa" +"raw/jobs/getdp-f0006-b0003-Z.tsv" = "a21e602b7b8ae32b66915d272d3186c31cd7842c02667c602064f7598716f291" +"raw/jobs/getdp-f0006-b0003-timing.tsv" = "865fceb647473629d0fbebb2e971d642a44f4e3a299376b5f6e02e8487969a32" +"raw/jobs/getdp-f0006-b0003.json" = "a51072fa4c42f3a8a9ef2af6362630c39fb403de1e53fd79f520da0343e3fb93" +"raw/jobs/getdp-f0007-b0001-P.tsv" = "2fb1d14577dc5925f78e9541758ae3b8667618ec0582c5cddf727c1220f83f23" +"raw/jobs/getdp-f0007-b0001-Z.tsv" = "f1f34e597d4653c8cfb0551ac7b2a8e82a5835c14f7bc4d16244d53564e12b03" +"raw/jobs/getdp-f0007-b0001-timing.tsv" = "79bec814d003d677859197c36a54d06b33b5592483616194dc1cda2ed330de09" +"raw/jobs/getdp-f0007-b0001.json" = "1ab1a54f7ccdbf72649fec766ce34bc626381542b87353e1b62e2b24da9644de" +"raw/jobs/getdp-f0007-b0002-P.tsv" = "88c5897a343d651d77168dcccdd84b304ab89aaffd9063878c8b87761ece1ed6" +"raw/jobs/getdp-f0007-b0002-Z.tsv" = "d3635277a9973c4d18b6482f2c33d73654ccc35f066b551bd17e2fdbd6850ff4" +"raw/jobs/getdp-f0007-b0002-timing.tsv" = "14661c5ed6b2be7497371c93164997c788b960448c05e13720cbd00f3cbe9cfb" +"raw/jobs/getdp-f0007-b0002.json" = "8579b149df0573903d9c5d61ccd87b8373a23fe89cc5c249cb7d799f66409f01" +"raw/jobs/getdp-f0007-b0003-P.tsv" = "fde259d49daf8d0728a8d6ccc708b458aca7d655b0d89562474d7960d7a55c94" +"raw/jobs/getdp-f0007-b0003-Z.tsv" = "a9acd7442ff0d134b7a65270ffc58e2114eee22ce422635c2b21f0ac9fa1d31a" +"raw/jobs/getdp-f0007-b0003-timing.tsv" = "4bfe3b64dee4e687f467bbdc044d55fd6fea1f78201b9f316040fa82e46792be" +"raw/jobs/getdp-f0007-b0003.json" = "2a05ea19b78030d3f2dd5cc3039edeace017e9d10a8c6bf5dcb7f5bd3b31d316" +"raw/jobs/getdp-f0008-b0001-P.tsv" = "412308a407308fca889dee31cb0f4ddfe43f3464aabe4ffe8648558dab68e053" +"raw/jobs/getdp-f0008-b0001-Z.tsv" = "2ec4bf6591207af6e57ef0941c6a37537d16ef0d9b1c67f0c07930c38360135b" +"raw/jobs/getdp-f0008-b0001-timing.tsv" = "de27cb70de616419e2207d7d7176ff95c060aaa7b0d22557b0b591abb39377a3" +"raw/jobs/getdp-f0008-b0001.json" = "b419c55b1af862ea25eb5940a3a83d11b088c53598cccf1547b475fc9b1c9ee7" +"raw/jobs/getdp-f0008-b0002-P.tsv" = "b4928623e3b217cf5bc370f5152cd2e723cdd9a24dce1b604a2fa76ddbd59f4e" +"raw/jobs/getdp-f0008-b0002-Z.tsv" = "c2d607d773deb123b6342f6d3ebb18954babe1d7ce20e402b55758ed2776d6a2" +"raw/jobs/getdp-f0008-b0002-timing.tsv" = "aa3c56bdde4e222e0ea9a8aa3273dc28e0decdf55a14af28a5df5f13b9601fab" +"raw/jobs/getdp-f0008-b0002.json" = "722b1eac2d25f47c47654737970ba344186d73af97b870d6c6c648ec58688f38" +"raw/jobs/getdp-f0008-b0003-P.tsv" = "241c0469357c8bc65f03e3a385e6b0ab7f72e05e477b30c78adf613e8a97aa9e" +"raw/jobs/getdp-f0008-b0003-Z.tsv" = "ef5b33d8131365e9b063db67172cf66351886cb5b946691d6ada15a21cdd2fb5" +"raw/jobs/getdp-f0008-b0003-timing.tsv" = "69a5283f85550a89524284e9f638ac3a806fa220448ea0ec0d37732ff6ba31d4" +"raw/jobs/getdp-f0008-b0003.json" = "16b5729dbfd40e859ee952c3ee149bcff4130e36043b6ce0e80b6b120b14ee55" +"raw/jobs/getdp-f0009-b0001-P.tsv" = "cf5f55fd71d26c7ef644a0bb9d00fbb04a47411fde6215fe59de29bcb10b9eaf" +"raw/jobs/getdp-f0009-b0001-Z.tsv" = "3fcd842bf3cf63e79bc3e8f623626ba34eeacc992a1b810d1289b0f6945003a3" +"raw/jobs/getdp-f0009-b0001-timing.tsv" = "6f08951d35fb19246f8f093d2155c99243b2cdf39b85234604ff9e10b4efb576" +"raw/jobs/getdp-f0009-b0001.json" = "9b7029fb15eea0e127fd6baa05b5c4ba5fb0ace1e4abc4727d6ff66512ad121a" +"raw/jobs/getdp-f0009-b0002-P.tsv" = "4d85587686a4a1b58da0074626478ff1ccc4899449ffb5958252e5324ad524f5" +"raw/jobs/getdp-f0009-b0002-Z.tsv" = "60aa2fe4f7f9dfc5e8fea8c0d780ea754b220a672a613ee9650ce5d398a13926" +"raw/jobs/getdp-f0009-b0002-timing.tsv" = "7154dfc17be5cfd92161898e03253129472470ccee24c0599d905ba61208842e" +"raw/jobs/getdp-f0009-b0002.json" = "3da2298a3a579332e8a86937c1d24b5f236dcc6f7eb84d04122aed0e8c15443d" +"raw/jobs/getdp-f0009-b0003-P.tsv" = "8937eba2516490766a41906e7ee715b6aed994d21e9ae295598c93f7b29234b9" +"raw/jobs/getdp-f0009-b0003-Z.tsv" = "f4f06bdabd1bb68cae14f42ddac34584e7f6994b27d7142030192e6935c20800" +"raw/jobs/getdp-f0009-b0003-timing.tsv" = "f5f59f01a884e46e8622c088a68404011a16e00c74bdc6740495e9c5a5328dfb" +"raw/jobs/getdp-f0009-b0003.json" = "d93f4a8177c097d64bbf11abe60825c4644d42ad8e32e386035a3a9b92511313" +"raw/scan_complete.tsv" = "2cc9eff50bb7e6fff0e2555f96d93ebe952c4a1a158a1dca82fe6cca3aa1afab" +"run.json" = "f763c2cf27056f3f92eca61940a4ab8913b464ffa696114c93731f8e4989adb7" +"timing-summary.json" = "3cdb63131603d4dbf156d8a54e39690c85fc48cfba213586ed404e80a4c081eb" diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/fem/formulation.txt b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/fem/formulation.txt new file mode 100644 index 000000000..0e94e36ec --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/fem/formulation.txt @@ -0,0 +1 @@ +@NamedTuple{backend::Symbol, requested::NamedTuple, methods::NamedTuple, options::NamedTuple}((:fem, (insulation_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), semicon_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_properties = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), temperature_dependence = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing)), (insulation_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), semicon_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), earth_properties = (identifier = :constant, parameters = NamedTuple(), options = NamedTuple()), temperature_dependence = (identifier = :linear, parameters = NamedTuple(), options = NamedTuple())), (reduce_bundle = false, kron_reduction = false, ideal_transposition = false, physics = Symbol("quasi-tem")))) \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/fem/problem.json b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/fem/problem.json new file mode 100644 index 000000000..99477a7d5 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/fem/problem.json @@ -0,0 +1,516 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "root": { + "kind": "line_parameters_problem", + "earth_props": { + "kind": "earth_model", + "vertical_layers": false, + "layers": [ + { + "kind": "earth_layer", + "thickness": { + "special": "Inf", + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "rho": { + "special": "Inf", + "__type__": "Float64" + } + }, + { + "kind": "earth_layer", + "thickness": { + "special": "Inf", + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "rho": { + "value": 0.1, + "__type__": "Float64" + } + } + ] + }, + "Gamma": null, + "frequencies": [ + { + "value": 0.1, + "__type__": "Float64" + }, + { + "value": 1, + "__type__": "Float64" + }, + { + "value": 10, + "__type__": "Float64" + }, + { + "value": 100, + "__type__": "Float64" + }, + { + "value": 1000, + "__type__": "Float64" + }, + { + "value": 10000, + "__type__": "Float64" + }, + { + "value": 100000, + "__type__": "Float64" + }, + { + "value": 1000000, + "__type__": "Float64" + }, + { + "value": 10000000, + "__type__": "Float64" + } + ], + "system": { + "kind": "line_cable_system", + "input_positions": [ + { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 1, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 2, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 1, + "__type__": "Float64" + } + } + ], + "clearances": [ + [ + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + } + ], + [ + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + } + ], + [ + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + } + ] + ], + "connections": [ + [ + 1 + ], + [ + 2 + ], + [ + 3 + ] + ], + "line_length": { + "value": 1, + "__type__": "Float64" + }, + "environment": null, + "system_id": "three_bare_wires_all_air", + "positions": [ + { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 1, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 2, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 1, + "__type__": "Float64" + } + } + ], + "designs": [ + { + "kind": "cable_design", + "cable_id": "three_bare_wires_all_air", + "nominal_data": [ + ], + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "core_metal", + "material": { + "value": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "type": "material" + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.0425, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + } + ] + } + }, + { + "kind": "cable_design", + "cable_id": "three_bare_wires_all_air", + "nominal_data": [ + ], + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "core_metal", + "material": { + "value": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "type": "material" + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.0425, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + } + ] + } + }, + { + "kind": "cable_design", + "cable_id": "three_bare_wires_all_air", + "nominal_data": [ + ], + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "core_metal", + "material": { + "value": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "type": "material" + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.0425, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + } + ] + } + } + ] + }, + "temperature": { + "value": 20, + "__type__": "Float64" + } + }, + "format": "linecablemodels.line_parameters_problem", + "version": "1.0.0" +} \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/unified/Y.csv b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/unified/Y.csv new file mode 100644 index 000000000..fec5149e6 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/unified/Y.csv @@ -0,0 +1,82 @@ +frequency_hz,response_terminal,basis_terminal,real_S_per_m,imaginary_S_per_m +0.10000000000000001,cable:1:core,cable:1:core,8.8174985708896091e-24,9.5125437748370102e-12 +0.10000000000000001,cable:1:core,cable:2:core,7.1523658381944664e-24,-1.8912830746024135e-12 +0.10000000000000001,cable:1:core,cable:3:core,7.7202067554844912e-24,-4.6083134754232305e-13 +0.10000000000000001,cable:2:core,cable:1:core,7.1523658415379676e-24,-1.891283074602413e-12 +0.10000000000000001,cable:2:core,cable:2:core,6.3935813146373556e-24,9.8662437122081328e-12 +0.10000000000000001,cable:2:core,cable:3:core,7.1523658415379676e-24,-1.891283074602413e-12 +0.10000000000000001,cable:3:core,cable:1:core,7.7202067554844912e-24,-4.6083134754232315e-13 +0.10000000000000001,cable:3:core,cable:2:core,7.1523658381944649e-24,-1.891283074602413e-12 +0.10000000000000001,cable:3:core,cable:3:core,8.8174985708896106e-24,9.5125437748370102e-12 +1,cable:1:core,cable:1:core,6.8501479736650221e-22,9.5125437748243227e-11 +1,cable:1:core,cable:2:core,5.4808946526399042e-22,-1.891283074613199e-11 +1,cable:1:core,cable:3:core,5.7530319150777122e-22,-4.6083134755499658e-12 +1,cable:2:core,cable:1:core,5.4808946858294201e-22,-1.8912830746131994e-11 +1,cable:2:core,cable:2:core,4.9734272200821993e-22,9.8662437121989743e-11 +1,cable:2:core,cable:3:core,5.4808946858294211e-22,-1.8912830746131997e-11 +1,cable:3:core,cable:1:core,5.753031915077715e-22,-4.6083134755499666e-12 +1,cable:3:core,cable:2:core,5.4808946526399061e-22,-1.8912830746131997e-11 +1,cable:3:core,cable:3:core,6.8501479736650221e-22,9.5125437748243227e-11 +10,cable:1:core,cable:1:core,4.7166583903820262e-20,9.512543774675523e-10 +10,cable:1:core,cable:2:core,3.6686164480533427e-20,-1.891283074739372e-10 +10,cable:1:core,cable:3:core,3.6212833098818979e-20,-4.6083134770283224e-11 +10,cable:2:core,cable:1:core,3.6686167722230193e-20,-1.8912830747393725e-10 +10,cable:2:core,cable:2:core,3.4333568788473328e-20,9.8662437120915512e-10 +10,cable:2:core,cable:3:core,3.6686167722230193e-20,-1.891283074739372e-10 +10,cable:3:core,cable:1:core,3.6212833098818979e-20,-4.6083134770283224e-11 +10,cable:3:core,cable:2:core,3.6686164480533427e-20,-1.891283074739372e-10 +10,cable:3:core,cable:3:core,4.7166583903820256e-20,9.5125437746755209e-10 +100,cable:1:core,cable:1:core,2.1083091976300289e-18,9.5125437725313327e-09 +100,cable:1:core,cable:2:core,1.4561375241927259e-18,-1.8912830765424522e-09 +100,cable:1:core,cable:3:core,1.0295216100662539e-18,-4.6083134977899235e-10 +100,cable:2:core,cable:1:core,1.4561405305882495e-18,-1.891283076542444e-09 +100,cable:2:core,cable:2:core,1.5506509108463473e-18,9.8662437105429252e-09 +100,cable:2:core,cable:3:core,1.4561405305882495e-18,-1.891283076542444e-09 +100,cable:3:core,cable:1:core,1.0295216100662541e-18,-4.6083134977899245e-10 +100,cable:3:core,cable:2:core,1.4561375241927257e-18,-1.8912830765424522e-09 +100,cable:3:core,cable:3:core,2.1083091976300285e-18,9.5125437725313327e-09 +1000,cable:1:core,cable:1:core,-1.873808118781869e-16,9.5125437293721708e-08 +1000,cable:1:core,cable:2:core,-1.8946804337046478e-16,-1.8912831119165847e-08 +1000,cable:1:core,cable:3:core,-2.8095123442940841e-16,-4.6083138873394025e-09 +1000,cable:2:core,cable:1:core,-1.894656669322759e-16,-1.8912831119161843e-08 +1000,cable:2:core,cable:2:core,-1.3229236679643231e-16,9.866243679281464e-08 +1000,cable:2:core,cable:3:core,-1.8946566693227583e-16,-1.8912831119161843e-08 +1000,cable:3:core,cable:1:core,-2.8095123442940836e-16,-4.6083138873394025e-09 +1000,cable:3:core,cable:2:core,-1.8946804337046485e-16,-1.8912831119165847e-08 +1000,cable:3:core,cable:3:core,-1.8738081187818683e-16,9.5125437293721708e-08 +10000,cable:1:core,cable:1:core,-1.0101134288504447e-13,9.5125424198982738e-07 +10000,cable:1:core,cable:2:core,-8.6514466734997425e-14,-1.8912841431725653e-07 +10000,cable:1:core,cable:3:core,-1.0143498648802649e-13,-4.6083248237275564e-08 +10000,cable:2:core,cable:1:core,-8.6513224256953002e-14,-1.8912841431599611e-07 +10000,cable:2:core,cable:2:core,-7.2680192975604353e-14,9.8662427226833344e-07 +10000,cable:2:core,cable:3:core,-8.6513224256953027e-14,-1.8912841431599611e-07 +10000,cable:3:core,cable:1:core,-1.0143498648802654e-13,-4.608324823727559e-08 +10000,cable:3:core,cable:2:core,-8.65144667349974e-14,-1.891284143172565e-07 +10000,cable:3:core,cable:3:core,-1.0101134288504453e-13,9.5125424198982759e-07 +100000,cable:1:core,cable:1:core,-3.4604866529119772e-11,9.5124842961546061e-06 +100000,cable:1:core,cable:2:core,-2.8436018490168415e-11,-1.8913282851842096e-06 +100000,cable:1:core,cable:3:core,-3.1713786251802083e-11,-4.6087867059802101e-07 +100000,cable:2:core,cable:1:core,-2.8435645641065308e-11,-1.8913282848721205e-06 +100000,cable:2:core,cable:2:core,-2.5082672191159045e-11,9.8661998438227087e-06 +100000,cable:2:core,cable:3:core,-2.8435645641065292e-11,-1.8913282848721201e-06 +100000,cable:3:core,cable:1:core,-3.1713786251802077e-11,-4.6087867059802069e-07 +100000,cable:3:core,cable:2:core,-2.8436018490168438e-11,-1.8913282851842098e-06 +100000,cable:3:core,cable:3:core,-3.4604866529119772e-11,9.5124842961546061e-06 +1000000,cable:1:core,cable:1:core,-1.559589973180337e-08,9.5093595392883751e-05 +1000000,cable:1:core,cable:2:core,-1.2861993771875996e-08,-1.8935550648911418e-05 +1000000,cable:1:core,cable:3:core,-1.4544270082946612e-08,-4.6305946147002577e-06 +1000000,cable:2:core,cable:1:core,-1.2861892146596569e-08,-1.89355505555729e-05 +1000000,cable:2:core,cable:2:core,-1.1308330073687395e-08,9.8638662485275188e-05 +1000000,cable:2:core,cable:3:core,-1.2861892146596574e-08,-1.893555055557291e-05 +1000000,cable:3:core,cable:1:core,-1.4544270082946622e-08,-4.6305946147002577e-06 +1000000,cable:3:core,cable:2:core,-1.2861993771875996e-08,-1.8935550648911421e-05 +1000000,cable:3:core,cable:3:core,-1.5595899731803376e-08,9.5093595392883751e-05 +10000000,cable:1:core,cable:1:core,-1.0244974735870984e-05,0.00093666492829382747 +10000000,cable:1:core,cable:2:core,-8.4132746995742166e-06,-0.0001973323193329975 +10000000,cable:1:core,cable:3:core,-9.4191677365102664e-06,-5.1467267275120134e-05 +10000000,cable:2:core,cable:1:core,-8.4132472113751319e-06,-0.00019733228329474613 +10000000,cable:2:core,cable:2:core,-7.3108984596087289e-06,0.00097540292219751854 +10000000,cable:2:core,cable:3:core,-8.4132472113751319e-06,-0.00019733228329474613 +10000000,cable:3:core,cable:1:core,-9.4191677365102597e-06,-5.1467267275120114e-05 +10000000,cable:3:core,cable:2:core,-8.4132746995742132e-06,-0.00019733231933299748 +10000000,cable:3:core,cable:3:core,-1.0244974735870988e-05,0.00093666492829382747 diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/unified/Z.csv b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/unified/Z.csv new file mode 100644 index 000000000..da67ee213 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/unified/Z.csv @@ -0,0 +1,82 @@ +frequency_hz,response_terminal,basis_terminal,real_ohm_per_m,imaginary_ohm_per_m +0.10000000000000001,cable:1:core,cable:1:core,3.1368030977512313e-06,1.2442491410079333e-06 +0.10000000000000001,cable:1:core,cable:2:core,9.8365757560990584e-08,8.1595650851660624e-07 +0.10000000000000001,cable:1:core,cable:3:core,9.8363910649020915e-08,7.2885335558399048e-07 +0.10000000000000001,cable:2:core,cable:1:core,9.8365757560990584e-08,8.1595650851660624e-07 +0.10000000000000001,cable:2:core,cable:2:core,3.1368030977512313e-06,1.2442491410079333e-06 +0.10000000000000001,cable:2:core,cable:3:core,9.8365757560990584e-08,8.1595650851660624e-07 +0.10000000000000001,cable:3:core,cable:1:core,9.8363910649020915e-08,7.2885335558399048e-07 +0.10000000000000001,cable:3:core,cable:2:core,9.8365757560990584e-08,8.1595650851660624e-07 +0.10000000000000001,cable:3:core,cable:3:core,3.1368030977512313e-06,1.2442491410079333e-06 +1,cable:1:core,cable:1:core,4.0258262108768049e-06,1.1002345673096755e-05 +1,cable:1:core,cable:2:core,9.7665254323323498e-07,6.7199804225952341e-06 +1,cable:1:core,cable:3:core,9.7651034817260789e-07,5.8489747235720515e-06 +1,cable:2:core,cable:1:core,9.7665254323323498e-07,6.7199804225952341e-06 +1,cable:2:core,cable:2:core,4.0258262108768049e-06,1.1002345673096755e-05 +1,cable:2:core,cable:3:core,9.7665254323323498e-07,6.7199804225952341e-06 +1,cable:3:core,cable:1:core,9.7651034817260789e-07,5.8489747235720515e-06 +1,cable:3:core,cable:2:core,9.7665254323323498e-07,6.7199804225952341e-06 +1,cable:3:core,cable:3:core,4.0258262108768049e-06,1.1002345673096755e-05 +10,cable:1:core,cable:1:core,1.344405560226531e-05,9.5357202577227266e-05 +10,cable:1:core,cable:2:core,9.5543897693967985e-06,5.2957258634098372e-05 +10,cable:1:core,cable:3:core,9.5443477251560104e-06,4.4249689272014495e-05 +10,cable:2:core,cable:1:core,9.5543897693967985e-06,5.2957258634098372e-05 +10,cable:2:core,cable:2:core,1.344405560226531e-05,9.5357202577227252e-05 +10,cable:2:core,cable:3:core,9.5543897693968002e-06,5.2957258634098372e-05 +10,cable:3:core,cable:1:core,9.5443477251560104e-06,4.4249689272014495e-05 +10,cable:3:core,cable:2:core,9.5543897693967985e-06,5.2957258634098372e-05 +10,cable:3:core,cable:3:core,1.344405560226531e-05,9.5357202577227266e-05 +100,cable:1:core,cable:1:core,0.00010029767252434697,0.00079835032747907519 +100,cable:1:core,cable:2:core,8.951373082556884e-05,0.00039183825668436429 +100,cable:1:core,cable:3:core,8.8905381208391422e-05,0.00030498496831731561 +100,cable:2:core,cable:1:core,8.951373082556884e-05,0.00039183825668436429 +100,cable:2:core,cable:2:core,0.00010029767252434647,0.00079835032747907334 +100,cable:2:core,cable:3:core,8.951373082556884e-05,0.00039183825668436429 +100,cable:3:core,cable:1:core,8.8905381208391422e-05,0.00030498496831731561 +100,cable:3:core,cable:2:core,8.951373082556884e-05,0.00039183825668436429 +100,cable:3:core,cable:3:core,0.00010029767252434695,0.00079835032747907519 +1000,cable:1:core,cable:1:core,0.00078861955828074494,0.0066682243696765085 +1000,cable:1:core,cable:2:core,0.00074708526686115175,0.0026749096440145926 +1000,cable:1:core,cable:3:core,0.00071978017636863936,0.0018225752382672528 +1000,cable:2:core,cable:1:core,0.00074708526686115175,0.0026749096440145926 +1000,cable:2:core,cable:2:core,0.00078861955828031147,0.0066682243696753107 +1000,cable:2:core,cable:3:core,0.00074708526686115186,0.0026749096440145926 +1000,cable:3:core,cable:1:core,0.00071978017636863947,0.001822575238267253 +1000,cable:3:core,cable:2:core,0.00074708526686115175,0.0026749096440145926 +1000,cable:3:core,cable:3:core,0.00078861955828074483,0.0066682243696765085 +10000,cable:1:core,cable:1:core,0.0051948061676279703,0.05689636052577033 +10000,cable:1:core,cable:2:core,0.0048255232285051653,0.017433544047368715 +10000,cable:1:core,cable:3:core,0.0041592668664024281,0.0096192362424460786 +10000,cable:2:core,cable:1:core,0.0048255232285051653,0.017433544047368715 +10000,cable:2:core,cable:2:core,0.0051948061673494274,0.05689636052512187 +10000,cable:2:core,cable:3:core,0.0048255232285051653,0.017433544047368715 +10000,cable:3:core,cable:1:core,0.0041592668664024281,0.0096192362424460786 +10000,cable:3:core,cable:2:core,0.0048255232285051653,0.017433544047368715 +10000,cable:3:core,cable:3:core,0.0051948061676279703,0.05689636052577033 +100000,cable:1:core,cable:1:core,0.025031226245057804,0.51501980111689405 +100000,cable:1:core,cable:2:core,0.021596902160095976,0.12625600561268494 +100000,cable:1:core,cable:3:core,0.015548307301550915,0.059605199227129396 +100000,cable:2:core,cable:1:core,0.021596902160095979,0.12625600561268494 +100000,cable:2:core,cable:2:core,0.025031226131411923,0.51501980073292042 +100000,cable:2:core,cable:3:core,0.021596902160095976,0.12625600561268494 +100000,cable:3:core,cable:1:core,0.015548307301550917,0.059605199227129396 +100000,cable:3:core,cable:2:core,0.021596902160095976,0.12625600561268494 +100000,cable:3:core,cable:3:core,0.025031226245057804,0.51501980111689405 +1000000,cable:1:core,cable:1:core,0.094696040918173796,4.9430135931550625 +1000000,cable:1:core,cable:2:core,0.077567018860665735,1.0935618501004389 +1000000,cable:1:core,cable:3:core,0.051247474729972764,0.48773084535257655 +1000000,cable:2:core,cable:1:core,0.077567018860665735,1.0935618501004387 +1000000,cable:2:core,cable:2:core,0.094696003143432725,4.9430132917981755 +1000000,cable:2:core,cable:3:core,0.077567018860665748,1.0935618501004389 +1000000,cable:3:core,cable:1:core,0.051247474729972764,0.48773084535257649 +1000000,cable:3:core,cable:2:core,0.077567018860665721,1.0935618501004389 +1000000,cable:3:core,cable:3:core,0.094696040918173796,4.9430135931550634 +10000000,cable:1:core,cable:1:core,1.2513322192538612,49.783940499020225 +10000000,cable:1:core,cable:2:core,1.180799668295279,11.256469738395747 +10000000,cable:1:core,cable:3:core,1.0678706383895686,5.1747359052981334 +10000000,cable:2:core,cable:1:core,1.180799668295279,11.256469738395744 +10000000,cable:2:core,cable:2:core,1.2512831793321841,49.783625781844535 +10000000,cable:2:core,cable:3:core,1.1807996682952793,11.256469738395747 +10000000,cable:3:core,cable:1:core,1.0678706383895684,5.1747359052981334 +10000000,cable:3:core,cable:2:core,1.180799668295279,11.256469738395745 +10000000,cable:3:core,cable:3:core,1.2513322192538614,49.783940499020247 diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/unified/computation-options.txt b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/unified/computation-options.txt new file mode 100644 index 000000000..3c73cbbf9 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/unified/computation-options.txt @@ -0,0 +1 @@ +(trace = true, output_basis = :pul) \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/unified/details.txt b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/unified/details.txt new file mode 100644 index 000000000..23d0d7a4b --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/unified/details.txt @@ -0,0 +1 @@ +@NamedTuple{shunt_model::NamedTuple, trace::@NamedTuple{phase_map::Vector{Int64}, cable_map::Vector{Int64}, Zin::Array{ComplexF64, 3}, Pin::Array{ComplexF64, 3}, Zg::Array{ComplexF64, 3}, Pg::Array{ComplexF64, 3}, Z::Array{ComplexF64, 3}, P::Array{ComplexF64, 3}}, formulations::@NamedTuple{effective::@NamedTuple{internal_impedance::Union{Nothing, Symbol, NamedTuple}, insulation_impedance::Union{Nothing, Symbol, NamedTuple}, earth_impedance::Union{Nothing, Symbol, NamedTuple}, shunt_model::Union{Nothing, Symbol, NamedTuple}, insulation_admittance::Union{Nothing, Symbol, NamedTuple}, semicon_admittance::Union{Nothing, Symbol, NamedTuple}, earth_admittance::Union{Nothing, Symbol, NamedTuple}, earth_properties::Union{Nothing, Symbol, NamedTuple}, pipe_impedance::Union{Nothing, Symbol, NamedTuple}, temperature_dependence::Union{Nothing, Symbol, NamedTuple}}, numerical::@NamedTuple{internal_impedance::NamedTuple, insulation_impedance::NamedTuple, shunt_model::NamedTuple, insulation_admittance::NamedTuple, semicon_admittance::NamedTuple, earth_properties::NamedTuple, temperature_dependence::NamedTuple, earth_impedance::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}, earth_admittance::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}, equivalent_earth::@NamedTuple{earth_impedance::Union{Nothing, @NamedTuple{air::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, mixed::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth_admittance::Union{Nothing, @NamedTuple{air::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, mixed::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, backend::Symbol, requested::NamedTuple, methods::NamedTuple, options::NamedTuple}, coordinates::Vector{String}}(((requested = :coaxial, effective = :coaxial, solves = 0, domains = @NamedTuple{design::Int64, terminals::UnitRange{Int64}, requested::Symbol, effective::Symbol, reason::Symbol, message::String}[], diagnostics = @NamedTuple{boundary_residual::Union{Nothing, Float64}, wire_residual::Union{Nothing, Float64}, tape_residual::Union{Nothing, Float64}, common_residual::Union{Nothing, Float64}, penetration_indicator::Union{Nothing, Float64}, reciprocity::Float64, log_moment_error::Float64, unknowns::Int64, equations::Int64, matrix_bytes::Int64, level::@NamedTuple{wire::Int64, order::Int64, quadrature::Int64, modes::Int64}}[]), (phase_map = [1, 2, 3], cable_map = [1, 2, 3], Zin = ComplexF64[3.0384367161282473e-6 + 3.1415178272814096e-8im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 3.0384367161282473e-6 + 3.1415178272814096e-8im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 3.0384367161282473e-6 + 3.1415178272814096e-8im;;; 3.0491254273478614e-6 + 3.135993190598609e-7im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 3.0491254273478614e-6 + 3.135993190598609e-7im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 3.0491254273478614e-6 + 3.135993190598609e-7im;;; 3.886234398887704e-6 + 2.71311425444745e-6im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 3.886234398887704e-6 + 2.71311425444745e-6im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 3.886234398887704e-6 + 2.71311425444745e-6im;;; 1.0572845476093172e-5 + 9.718155377880437e-6im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 1.0572845476093172e-5 + 9.718155377880437e-6im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 1.0572845476093172e-5 + 9.718155377880437e-6im;;; 3.1668762815475746e-5 + 3.088048172738763e-5im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 3.1668762815475746e-5 + 3.088048172738763e-5im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 3.1668762815475746e-5 + 3.088048172738763e-5im;;; 9.846320725460222e-5 + 9.76946980065643e-5im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 9.846320725460222e-5 + 9.76946980065643e-5im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 9.846320725460222e-5 + 9.76946980065643e-5im;;; 0.0003097129699189164 + 0.0003089505796854997im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0003097129699189164 + 0.0003089505796854997im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0003097129699189164 + 0.0003089505796854997im;;; 0.0009777519922518445 + 0.0009769915236240853im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0009777519922518445 + 0.0009769915236240853im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0009777519922518445 + 0.0009769915236240853im;;; 0.0030902795943006773 + 0.0030895197319977846im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0030902795943006773 + 0.0030895197319977846im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0030902795943006773 + 0.0030895197319977846im], Pin = ComplexF64[0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im], Zg = ComplexF64[9.836638162298405e-8 + 1.2128339627351192e-6im 9.836575756099058e-8 + 8.159565085166062e-7im 9.836391064902091e-8 + 7.288533555839905e-7im; 9.836575756099058e-8 + 8.159565085166062e-7im 9.836638162298405e-8 + 1.2128339627351192e-6im 9.836575756099058e-8 + 8.159565085166062e-7im; 9.836391064902091e-8 + 7.288533555839905e-7im 9.836575756099058e-8 + 8.159565085166062e-7im 9.836638162298405e-8 + 1.2128339627351192e-6im;;; 9.767007835289436e-7 + 1.0688746354036894e-5im 9.76652543233235e-7 + 6.719980422595234e-6im 9.765103481726079e-7 + 5.8489747235720515e-6im; 9.76652543233235e-7 + 6.719980422595234e-6im 9.767007835289436e-7 + 1.0688746354036894e-5im 9.76652543233235e-7 + 6.719980422595234e-6im; 9.765103481726079e-7 + 5.8489747235720515e-6im 9.76652543233235e-7 + 6.719980422595234e-6im 9.767007835289436e-7 + 1.0688746354036894e-5im;;; 9.557821203377606e-6 + 9.264408832277981e-5im 9.554389769396799e-6 + 5.295725863409837e-5im 9.54434772515601e-6 + 4.4249689272014495e-5im; 9.554389769396799e-6 + 5.295725863409837e-5im 9.557821203377606e-6 + 9.26440883227798e-5im 9.5543897693968e-6 + 5.295725863409837e-5im; 9.54434772515601e-6 + 4.4249689272014495e-5im 9.554389769396799e-6 + 5.295725863409837e-5im 9.557821203377607e-6 + 9.264408832277981e-5im;;; 8.97248270482538e-5 + 0.0007886321721011948im 8.951373082556884e-5 + 0.0003918382566843643im 8.890538120839142e-5 + 0.0003049849683173156im; 8.951373082556884e-5 + 0.0003918382566843643im 8.97248270482533e-5 + 0.0007886321721011929im 8.951373082556884e-5 + 0.0003918382566843643im; 8.890538120839142e-5 + 0.0003049849683173156im 8.951373082556884e-5 + 0.0003918382566843643im 8.972482704825379e-5 + 0.0007886321721011948im;;; 0.0007569507954652692 + 0.006637343887949121im 0.0007470852668611518 + 0.0026749096440145926im 0.0007197801763686394 + 0.0018225752382672528im; 0.0007470852668611518 + 0.0026749096440145926im 0.0007569507954648358 + 0.006637343887947923im 0.0007470852668611519 + 0.0026749096440145926im; 0.0007197801763686395 + 0.001822575238267253im 0.0007470852668611518 + 0.0026749096440145926im 0.0007569507954652691 + 0.006637343887949121im;;; 0.005096342960373368 + 0.056798665827763765im 0.004825523228505165 + 0.017433544047368715im 0.004159266866402428 + 0.009619236242446079im; 0.004825523228505165 + 0.017433544047368715im 0.005096342960094825 + 0.056798665827115305im 0.004825523228505165 + 0.017433544047368715im; 0.004159266866402428 + 0.009619236242446079im 0.004825523228505165 + 0.017433544047368715im 0.005096342960373368 + 0.056798665827763765im;;; 0.024721513275138888 + 0.5147108505372086im 0.021596902160095976 + 0.12625600561268494im 0.015548307301550915 + 0.059605199227129396im; 0.02159690216009598 + 0.12625600561268494im 0.024721513161493007 + 0.5147108501532349im 0.021596902160095976 + 0.12625600561268494im; 0.015548307301550917 + 0.059605199227129396im 0.021596902160095976 + 0.12625600561268494im 0.024721513275138888 + 0.5147108505372086im;;; 0.09371828892592195 + 4.942036601631439im 0.07756701886066573 + 1.093561850100439im 0.051247474729972764 + 0.48773084535257655im; 0.07756701886066573 + 1.0935618501004387im 0.09371825115118088 + 4.942036300274552im 0.07756701886066575 + 1.093561850100439im; 0.051247474729972764 + 0.4877308453525765im 0.07756701886066572 + 1.093561850100439im 0.09371828892592195 + 4.94203660163144im;;; 1.2482419396595605 + 49.780850979288225im 1.180799668295279 + 11.256469738395747im 1.0678706383895686 + 5.174735905298133im; 1.180799668295279 + 11.256469738395744im 1.2481928997378835 + 49.780536262112534im 1.1807996682952793 + 11.256469738395747im; 1.0678706383895684 + 5.174735905298133im 1.180799668295279 + 11.256469738395745im 1.2482419396595608 + 49.780850979288246im], Pg = ComplexF64[6.922928489069867e10 + 0.10538021253207482im 1.4464906594446354e10 + 0.10314880802887795im 6.229696184958544e9 + 0.09844886459695319im; 1.4464906594446354e10 + 0.10314880805444972im 6.922928489069867e10 + 0.10538021255764778im 1.4464906594446354e10 + 0.10314880805444972im; 6.229696184958544e9 + 0.09844886459695318im 1.4464906594446354e10 + 0.10314880802887795im 6.922928489069867e10 + 0.10538021253207482im;;; 6.922928489085417e10 + 0.8127134909819286im 1.4464906594601873e10 + 0.7904022255228458im 6.229696185113944e9 + 0.743411113797329im; 1.4464906594601873e10 + 0.7904022280612456im 6.922928489085417e10 + 0.8127134935212574im 1.4464906594601873e10 + 0.7904022280612456im; 6.229696185113944e9 + 0.7434111137973292im 1.4464906594601873e10 + 0.7904022255228458im 6.922928489085417e10 + 0.8127134909819286im;;; 6.922928489267548e10 + 5.512987198483716im 1.446490659642145e10 + 5.290153034657082im 6.229696186929153e9 + 4.8210631637084616im; 1.446490659642145e10 + 5.290153282588454im 6.922928489267548e10 + 5.512987447071136im 1.446490659642145e10 + 5.290153282588454im; 6.229696186929153e9 + 4.8210631637084616im 1.446490659642145e10 + 5.290153034657082im 6.922928489267548e10 + 5.512987198483716im;;; 6.92292849187938e10 + 23.2008777734567im 1.4464906622413193e10 + 20.998817906381202im 6.229696212617747e9 + 16.386416923005697im; 1.446490662241313e10 + 20.998840899888084im 6.922928491879367e10 + 23.20090116439899im 1.446490662241313e10 + 20.998840899888084im; 6.229696212617747e9 + 16.386416923005697im 1.4464906622413193e10 + 20.998817906381202im 6.92292849187938e10 + 23.2008777734567im;;; 6.92292854374593e10 - 252.890153433153im 1.4464907132551594e10 - 272.588731271271im 6.229696704726179e9 - 311.99650477827737im; 1.4464907132548532e10 - 272.58691372433674im 6.922928543745068e10 - 252.8881575015807im 1.4464907132548532e10 - 272.5869137243367im; 6.229696704726179e9 - 311.9965047782773im 1.4464907132551594e10 - 272.588731271271im 6.92292854374593e10 - 252.89015343315296im;;; 6.922930090960797e10 - 12407.698852530832im 1.4464922092706055e10 - 12444.420684450159im 6.229710813492707e9 - 12434.45947628782im; 1.4464922092609657e10 - 12444.325657187734im 6.922930090900107e10 - 12407.560297986229im 1.4464922092609657e10 - 12444.325657187734im; 6.229710813492707e9 - 12434.459476287822im 1.4464922092706055e10 - 12444.420684450159im 6.922930090960797e10 - 12407.698852530833im;;; 6.922997891661166e10 - 416800.2665583979im 1.446556805480457e10 - 410563.0935096884im 6.230313426397473e9 - 398537.9120262761im; 1.4465568052417637e10 - 410560.24179199606im 6.922997887067914e10 - 416793.4613400297im 1.4465568052417637e10 - 410560.241791996im; 6.230313426397472e9 - 398537.91202627606im 1.4465568054804571e10 - 410563.09350968845im 6.922997891661166e10 - 416800.2665583979im;;; 6.926553970838646e10 - 1.887474157288432e7im 1.4498526948115458e10 - 1.862949150023123e7im 6.259910823328287e9 - 1.8210323166724756e7im; 1.4498526876695347e10 - 1.862941364471365e7im 6.92655356671563e10 - 1.887448419497529e7im 1.449852687669535e10 - 1.8629413644713655e7im; 6.259910823328286e9 - 1.8210323166724756e7im 1.4498526948115458e10 - 1.8629491500231232e7im 6.926553970838646e10 - 1.8874741572884325e7im;;; 7.07483746419682e10 - 1.3355971745810187e9im 1.5729446750932125e10 - 1.3195611453365066e9im 7.161934012351531e9 - 1.2824563823147187e9im; 1.5729443963495493e10 - 1.3195588320947046e9im 7.07479332123416e10 - 1.3355367323975365e9im 1.5729443963495497e10 - 1.3195588320947049e9im; 7.161934012351529e9 - 1.2824563823147182e9im 1.5729446750932125e10 - 1.3195611453365064e9im 7.074837464196822e10 - 1.335597174581019e9im], Z = ComplexF64[3.1368030977512313e-6 + 1.2442491410079333e-6im 9.836575756099058e-8 + 8.159565085166062e-7im 9.836391064902091e-8 + 7.288533555839905e-7im; 9.836575756099058e-8 + 8.159565085166062e-7im 3.1368030977512313e-6 + 1.2442491410079333e-6im 9.836575756099058e-8 + 8.159565085166062e-7im; 9.836391064902091e-8 + 7.288533555839905e-7im 9.836575756099058e-8 + 8.159565085166062e-7im 3.1368030977512313e-6 + 1.2442491410079333e-6im;;; 4.025826210876805e-6 + 1.1002345673096755e-5im 9.76652543233235e-7 + 6.719980422595234e-6im 9.765103481726079e-7 + 5.8489747235720515e-6im; 9.76652543233235e-7 + 6.719980422595234e-6im 4.025826210876805e-6 + 1.1002345673096755e-5im 9.76652543233235e-7 + 6.719980422595234e-6im; 9.765103481726079e-7 + 5.8489747235720515e-6im 9.76652543233235e-7 + 6.719980422595234e-6im 4.025826210876805e-6 + 1.1002345673096755e-5im;;; 1.344405560226531e-5 + 9.535720257722727e-5im 9.554389769396799e-6 + 5.295725863409837e-5im 9.54434772515601e-6 + 4.4249689272014495e-5im; 9.554389769396799e-6 + 5.295725863409837e-5im 1.344405560226531e-5 + 9.535720257722725e-5im 9.5543897693968e-6 + 5.295725863409837e-5im; 9.54434772515601e-6 + 4.4249689272014495e-5im 9.554389769396799e-6 + 5.295725863409837e-5im 1.344405560226531e-5 + 9.535720257722727e-5im;;; 0.00010029767252434697 + 0.0007983503274790752im 8.951373082556884e-5 + 0.0003918382566843643im 8.890538120839142e-5 + 0.0003049849683173156im; 8.951373082556884e-5 + 0.0003918382566843643im 0.00010029767252434647 + 0.0007983503274790733im 8.951373082556884e-5 + 0.0003918382566843643im; 8.890538120839142e-5 + 0.0003049849683173156im 8.951373082556884e-5 + 0.0003918382566843643im 0.00010029767252434695 + 0.0007983503274790752im;;; 0.0007886195582807449 + 0.0066682243696765085im 0.0007470852668611518 + 0.0026749096440145926im 0.0007197801763686394 + 0.0018225752382672528im; 0.0007470852668611518 + 0.0026749096440145926im 0.0007886195582803115 + 0.006668224369675311im 0.0007470852668611519 + 0.0026749096440145926im; 0.0007197801763686395 + 0.001822575238267253im 0.0007470852668611518 + 0.0026749096440145926im 0.0007886195582807448 + 0.0066682243696765085im;;; 0.00519480616762797 + 0.05689636052577033im 0.004825523228505165 + 0.017433544047368715im 0.004159266866402428 + 0.009619236242446079im; 0.004825523228505165 + 0.017433544047368715im 0.0051948061673494274 + 0.05689636052512187im 0.004825523228505165 + 0.017433544047368715im; 0.004159266866402428 + 0.009619236242446079im 0.004825523228505165 + 0.017433544047368715im 0.00519480616762797 + 0.05689636052577033im;;; 0.025031226245057804 + 0.515019801116894im 0.021596902160095976 + 0.12625600561268494im 0.015548307301550915 + 0.059605199227129396im; 0.02159690216009598 + 0.12625600561268494im 0.025031226131411923 + 0.5150198007329204im 0.021596902160095976 + 0.12625600561268494im; 0.015548307301550917 + 0.059605199227129396im 0.021596902160095976 + 0.12625600561268494im 0.025031226245057804 + 0.515019801116894im;;; 0.0946960409181738 + 4.9430135931550625im 0.07756701886066573 + 1.093561850100439im 0.051247474729972764 + 0.48773084535257655im; 0.07756701886066573 + 1.0935618501004387im 0.09469600314343272 + 4.9430132917981755im 0.07756701886066575 + 1.093561850100439im; 0.051247474729972764 + 0.4877308453525765im 0.07756701886066572 + 1.093561850100439im 0.0946960409181738 + 4.943013593155063im;;; 1.2513322192538612 + 49.783940499020225im 1.180799668295279 + 11.256469738395747im 1.0678706383895686 + 5.174735905298133im; 1.180799668295279 + 11.256469738395744im 1.2512831793321841 + 49.783625781844535im 1.1807996682952793 + 11.256469738395747im; 1.0678706383895684 + 5.174735905298133im 1.180799668295279 + 11.256469738395745im 1.2513322192538614 + 49.78394049902025im], P = ComplexF64[6.922928489069867e10 + 0.10538021253207482im 1.4464906594446354e10 + 0.10314880802887795im 6.229696184958544e9 + 0.09844886459695319im; 1.4464906594446354e10 + 0.10314880805444972im 6.922928489069867e10 + 0.10538021255764778im 1.4464906594446354e10 + 0.10314880805444972im; 6.229696184958544e9 + 0.09844886459695318im 1.4464906594446354e10 + 0.10314880802887795im 6.922928489069867e10 + 0.10538021253207482im;;; 6.922928489085417e10 + 0.8127134909819286im 1.4464906594601873e10 + 0.7904022255228458im 6.229696185113944e9 + 0.743411113797329im; 1.4464906594601873e10 + 0.7904022280612456im 6.922928489085417e10 + 0.8127134935212574im 1.4464906594601873e10 + 0.7904022280612456im; 6.229696185113944e9 + 0.7434111137973292im 1.4464906594601873e10 + 0.7904022255228458im 6.922928489085417e10 + 0.8127134909819286im;;; 6.922928489267548e10 + 5.512987198483716im 1.446490659642145e10 + 5.290153034657082im 6.229696186929153e9 + 4.8210631637084616im; 1.446490659642145e10 + 5.290153282588454im 6.922928489267548e10 + 5.512987447071136im 1.446490659642145e10 + 5.290153282588454im; 6.229696186929153e9 + 4.8210631637084616im 1.446490659642145e10 + 5.290153034657082im 6.922928489267548e10 + 5.512987198483716im;;; 6.92292849187938e10 + 23.2008777734567im 1.4464906622413193e10 + 20.998817906381202im 6.229696212617747e9 + 16.386416923005697im; 1.446490662241313e10 + 20.998840899888084im 6.922928491879367e10 + 23.20090116439899im 1.446490662241313e10 + 20.998840899888084im; 6.229696212617747e9 + 16.386416923005697im 1.4464906622413193e10 + 20.998817906381202im 6.92292849187938e10 + 23.2008777734567im;;; 6.92292854374593e10 - 252.890153433153im 1.4464907132551594e10 - 272.588731271271im 6.229696704726179e9 - 311.99650477827737im; 1.4464907132548532e10 - 272.58691372433674im 6.922928543745068e10 - 252.8881575015807im 1.4464907132548532e10 - 272.5869137243367im; 6.229696704726179e9 - 311.9965047782773im 1.4464907132551594e10 - 272.588731271271im 6.92292854374593e10 - 252.89015343315296im;;; 6.922930090960797e10 - 12407.698852530832im 1.4464922092706055e10 - 12444.420684450159im 6.229710813492707e9 - 12434.45947628782im; 1.4464922092609657e10 - 12444.325657187734im 6.922930090900107e10 - 12407.560297986229im 1.4464922092609657e10 - 12444.325657187734im; 6.229710813492707e9 - 12434.459476287822im 1.4464922092706055e10 - 12444.420684450159im 6.922930090960797e10 - 12407.698852530833im;;; 6.922997891661166e10 - 416800.2665583979im 1.446556805480457e10 - 410563.0935096884im 6.230313426397473e9 - 398537.9120262761im; 1.4465568052417637e10 - 410560.24179199606im 6.922997887067914e10 - 416793.4613400297im 1.4465568052417637e10 - 410560.241791996im; 6.230313426397472e9 - 398537.91202627606im 1.4465568054804571e10 - 410563.09350968845im 6.922997891661166e10 - 416800.2665583979im;;; 6.926553970838646e10 - 1.887474157288432e7im 1.4498526948115458e10 - 1.862949150023123e7im 6.259910823328287e9 - 1.8210323166724756e7im; 1.4498526876695347e10 - 1.862941364471365e7im 6.92655356671563e10 - 1.887448419497529e7im 1.449852687669535e10 - 1.8629413644713655e7im; 6.259910823328286e9 - 1.8210323166724756e7im 1.4498526948115458e10 - 1.8629491500231232e7im 6.926553970838646e10 - 1.8874741572884325e7im;;; 7.07483746419682e10 - 1.3355971745810187e9im 1.5729446750932125e10 - 1.3195611453365066e9im 7.161934012351531e9 - 1.2824563823147187e9im; 1.5729443963495493e10 - 1.3195588320947046e9im 7.07479332123416e10 - 1.3355367323975365e9im 1.5729443963495497e10 - 1.3195588320947049e9im; 7.161934012351529e9 - 1.2824563823147182e9im 1.5729446750932125e10 - 1.3195611453365064e9im 7.074837464196822e10 - 1.335597174581019e9im]), @NamedTuple{effective::@NamedTuple{internal_impedance::Union{Nothing, Symbol, NamedTuple}, insulation_impedance::Union{Nothing, Symbol, NamedTuple}, earth_impedance::Union{Nothing, Symbol, NamedTuple}, shunt_model::Union{Nothing, Symbol, NamedTuple}, insulation_admittance::Union{Nothing, Symbol, NamedTuple}, semicon_admittance::Union{Nothing, Symbol, NamedTuple}, earth_admittance::Union{Nothing, Symbol, NamedTuple}, earth_properties::Union{Nothing, Symbol, NamedTuple}, pipe_impedance::Union{Nothing, Symbol, NamedTuple}, temperature_dependence::Union{Nothing, Symbol, NamedTuple}}, numerical::@NamedTuple{internal_impedance::NamedTuple, insulation_impedance::NamedTuple, shunt_model::NamedTuple, insulation_admittance::NamedTuple, semicon_admittance::NamedTuple, earth_properties::NamedTuple, temperature_dependence::NamedTuple, earth_impedance::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}, earth_admittance::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}, equivalent_earth::@NamedTuple{earth_impedance::Union{Nothing, @NamedTuple{air::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, mixed::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth_admittance::Union{Nothing, @NamedTuple{air::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, mixed::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, backend::Symbol, requested::NamedTuple, methods::NamedTuple, options::NamedTuple}((@NamedTuple{internal_impedance::Union{Nothing, Symbol, NamedTuple}, insulation_impedance::Union{Nothing, Symbol, NamedTuple}, earth_impedance::Union{Nothing, Symbol, NamedTuple}, shunt_model::Union{Nothing, Symbol, NamedTuple}, insulation_admittance::Union{Nothing, Symbol, NamedTuple}, semicon_admittance::Union{Nothing, Symbol, NamedTuple}, earth_admittance::Union{Nothing, Symbol, NamedTuple}, earth_properties::Union{Nothing, Symbol, NamedTuple}, pipe_impedance::Union{Nothing, Symbol, NamedTuple}, temperature_dependence::Union{Nothing, Symbol, NamedTuple}}((:schelkunoff1934, :ametani1980, :unified, :coaxial, :lossless, :lossless, :unified, :constant, :none, :linear)), @NamedTuple{internal_impedance::NamedTuple, insulation_impedance::NamedTuple, shunt_model::NamedTuple, insulation_admittance::NamedTuple, semicon_admittance::NamedTuple, earth_properties::NamedTuple, temperature_dependence::NamedTuple, earth_impedance::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}, earth_admittance::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}(((outer = NamedTuple(),), NamedTuple(), NamedTuple(), NamedTuple(), NamedTuple(), NamedTuple(), NamedTuple(), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}[@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :self, 1, 1, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),))), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :mutual, 1, 1, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),)))], @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}[@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :self, 1, 1, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),))), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :mutual, 1, 1, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),)))])), @NamedTuple{earth_impedance::Union{Nothing, @NamedTuple{air::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, mixed::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth_admittance::Union{Nothing, @NamedTuple{air::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, mixed::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}((nothing, nothing)), :coaxial, (internal_impedance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), insulation_impedance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_impedance = (identifier = :unified, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), shunt_model = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), insulation_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), semicon_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_admittance = (identifier = :unified, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_properties = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), pipe_impedance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), temperature_dependence = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing)), (internal_impedance = (identifier = :schelkunoff1934, parameters = NamedTuple(), options = (inner = NamedTuple(), outer = NamedTuple(), transfer = NamedTuple()), configured_options = ()), insulation_impedance = (identifier = :ametani1980, parameters = NamedTuple(), options = NamedTuple()), earth_impedance = (identifier = :unified, assumptions = (media = Val{:homogeneous}(), layers = 2:2, longitudinal = :prescribed, permittivity = :positive), parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), shunt_model = (identifier = :coaxial, parameters = NamedTuple(), options = NamedTuple()), insulation_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), semicon_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), earth_admittance = (identifier = :unified, assumptions = (media = Val{:homogeneous}(), layers = 2:2, longitudinal = :prescribed, permittivity = :positive), parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_properties = (identifier = :constant, parameters = NamedTuple(), options = NamedTuple()), pipe_impedance = (identifier = :none, parameters = NamedTuple(), options = NamedTuple()), temperature_dependence = (identifier = :linear, parameters = NamedTuple(), options = NamedTuple())), (reduce_bundle = false, kron_reduction = false, ideal_transposition = false))), ["cable:1:core", "cable:2:core", "cable:3:core"])) \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/unified/formulation.txt b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/unified/formulation.txt new file mode 100644 index 000000000..1a6917548 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/unified/formulation.txt @@ -0,0 +1 @@ +@NamedTuple{backend::Symbol, requested::NamedTuple, methods::NamedTuple, options::NamedTuple}((:coaxial, (internal_impedance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), insulation_impedance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_impedance = (identifier = :unified, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), shunt_model = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), insulation_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), semicon_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_admittance = (identifier = :unified, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_properties = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), pipe_impedance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), temperature_dependence = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing)), (internal_impedance = (identifier = :schelkunoff1934, parameters = NamedTuple(), options = (inner = NamedTuple(), outer = NamedTuple(), transfer = NamedTuple()), configured_options = ()), insulation_impedance = (identifier = :ametani1980, parameters = NamedTuple(), options = NamedTuple()), earth_impedance = (identifier = :unified, assumptions = (media = Val{:homogeneous}(), layers = 2:2, longitudinal = :prescribed, permittivity = :positive), parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), shunt_model = (identifier = :coaxial, parameters = NamedTuple(), options = NamedTuple()), insulation_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), semicon_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), earth_admittance = (identifier = :unified, assumptions = (media = Val{:homogeneous}(), layers = 2:2, longitudinal = :prescribed, permittivity = :positive), parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_properties = (identifier = :constant, parameters = NamedTuple(), options = NamedTuple()), pipe_impedance = (identifier = :none, parameters = NamedTuple(), options = NamedTuple()), temperature_dependence = (identifier = :linear, parameters = NamedTuple(), options = NamedTuple())), (reduce_bundle = false, kron_reduction = false, ideal_transposition = false))) \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/unified/problem.json b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/unified/problem.json new file mode 100644 index 000000000..99477a7d5 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_air/unified/problem.json @@ -0,0 +1,516 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "root": { + "kind": "line_parameters_problem", + "earth_props": { + "kind": "earth_model", + "vertical_layers": false, + "layers": [ + { + "kind": "earth_layer", + "thickness": { + "special": "Inf", + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "rho": { + "special": "Inf", + "__type__": "Float64" + } + }, + { + "kind": "earth_layer", + "thickness": { + "special": "Inf", + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "rho": { + "value": 0.1, + "__type__": "Float64" + } + } + ] + }, + "Gamma": null, + "frequencies": [ + { + "value": 0.1, + "__type__": "Float64" + }, + { + "value": 1, + "__type__": "Float64" + }, + { + "value": 10, + "__type__": "Float64" + }, + { + "value": 100, + "__type__": "Float64" + }, + { + "value": 1000, + "__type__": "Float64" + }, + { + "value": 10000, + "__type__": "Float64" + }, + { + "value": 100000, + "__type__": "Float64" + }, + { + "value": 1000000, + "__type__": "Float64" + }, + { + "value": 10000000, + "__type__": "Float64" + } + ], + "system": { + "kind": "line_cable_system", + "input_positions": [ + { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 1, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 2, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 1, + "__type__": "Float64" + } + } + ], + "clearances": [ + [ + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + } + ], + [ + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + } + ], + [ + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + } + ] + ], + "connections": [ + [ + 1 + ], + [ + 2 + ], + [ + 3 + ] + ], + "line_length": { + "value": 1, + "__type__": "Float64" + }, + "environment": null, + "system_id": "three_bare_wires_all_air", + "positions": [ + { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 1, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 2, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 1, + "__type__": "Float64" + } + } + ], + "designs": [ + { + "kind": "cable_design", + "cable_id": "three_bare_wires_all_air", + "nominal_data": [ + ], + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "core_metal", + "material": { + "value": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "type": "material" + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.0425, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + } + ] + } + }, + { + "kind": "cable_design", + "cable_id": "three_bare_wires_all_air", + "nominal_data": [ + ], + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "core_metal", + "material": { + "value": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "type": "material" + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.0425, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + } + ] + } + }, + { + "kind": "cable_design", + "cable_id": "three_bare_wires_all_air", + "nominal_data": [ + ], + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "core_metal", + "material": { + "value": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "type": "material" + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.0425, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + } + ] + } + } + ] + }, + "temperature": { + "value": 20, + "__type__": "Float64" + } + }, + "format": "linecablemodels.line_parameters_problem", + "version": "1.0.0" +} \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/Y-differences.csv b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/Y-differences.csv new file mode 100644 index 000000000..1fff999f2 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/Y-differences.csv @@ -0,0 +1,82 @@ +frequency_hz,response_terminal,basis_terminal,absolute_error,relative_error,real_absolute_error,real_relative_error,imaginary_absolute_error,imaginary_relative_error,real_reference_near_zero,imaginary_reference_near_zero +0.10000000000000001,cable:1:core,cable:1:core,0.0773906557155805,0.0065617092401915635,0.077387386419937698,0.0065616300780875032,0.000711347438847168,0.0077631437371352549,false,false +0.10000000000000001,cable:1:core,cable:2:core,0.044167505962995623,0.0066708899844850457,0.044164480544665707,0.0066707596322472312,0.00051695378042730666,0.0078905199147572321,false,false +0.10000000000000001,cable:1:core,cable:3:core,0.025351952512180406,0.0077355966791449477,0.025342105662352399,0.007735593904414936,0.000706524435541242,0.0077391690188005026,false,false +0.10000000000000001,cable:2:core,cable:1:core,0.044167506421697915,0.0066708900537656799,0.044164481088851737,0.0066707597144430248,0.0005169464799997664,0.0078904084847396232,false,false +0.10000000000000001,cable:2:core,cable:2:core,0.093971936696731331,0.0064364247916477895,0.093971186505447335,0.0064364066846708343,0.00037549073612106604,0.007998122538713388,false,false +0.10000000000000001,cable:2:core,cable:3:core,0.044008244777567443,0.0066469956736024327,0.044005251065200923,0.0066468689090916477,0.00051330993651495238,0.0078353370431707165,false,false +0.10000000000000001,cable:3:core,cable:1:core,0.025351952512174612,0.0077355966791431783,0.025342105662346626,0.0077355939044131718,0.00070652443554045097,0.0077391690187917379,false,false +0.10000000000000001,cable:3:core,cable:2:core,0.04400824431857886,0.0066469956042768969,0.044005250521022887,0.0066468688268950925,0.00051331723694353348,0.007835448479389915,false,false +0.10000000000000001,cable:3:core,cable:3:core,0.07713434566243918,0.0065401196423399343,0.077131158912232323,0.0065400467863728183,0.00070114593959781646,0.0076526637626344761,false,false +1,cable:1:core,cable:1:core,0.077988272994409066,0.0065248428124142976,0.077982575974283819,0.006524811948760938,0.00094263941437094934,0.006746916171852425,false,false +1,cable:1:core,cable:2:core,0.044067032163557202,0.0067703347820267275,0.044061479308771467,0.0067702676694039498,0.00069954615765421835,0.0070534007231423746,false,false +1,cable:1:core,cable:3:core,0.025459635877577678,0.008153947858134657,0.025441790648349727,0.0081560905019780992,0.00095307272775210783,0.0069553404901981789,false,false +1,cable:2:core,cable:1:core,0.044067039064787109,0.0067703358423122787,0.044061487221096485,0.0067702688851724556,0.00069948252488842566,0.0070527591251700788,false,false +1,cable:2:core,cable:2:core,0.095208900831434906,0.006484796453189415,0.095207491692518076,0.0064847768876221045,0.00051799917867405287,0.0072677350140419074,false,false +1,cable:2:core,cable:3:core,0.044666897915518956,0.006861864072795717,0.044661302575407902,0.0068618011125053478,0.00070698066681851301,0.0071278226890675121,false,false +1,cable:3:core,cable:1:core,0.025459635877582109,0.0081539478581360639,0.025441790648354168,0.0081560905019795096,0.0009530727277519413,0.0069553404901969837,false,false +1,cable:3:core,cable:2:core,0.044666891011373409,0.0068618630121599893,0.044661294663056239,0.0068617998968448146,0.0007070442995844306,0.0071284642385383038,false,false +1,cable:3:core,cable:3:core,0.078770813578310409,0.0065898821665799062,0.078764916561876674,0.0065898391229935009,0.00096384168281823945,0.0068976240895065365,false,false +10,cable:1:core,cable:1:core,0.078638577373295687,0.0064452283085030104,0.078627172879121687,0.0064455600551924083,0.0013392297534491615,0.0055373067026587277,false,false +10,cable:1:core,cable:2:core,0.044289071142545544,0.0069906346418380999,0.044278136602227569,0.0069913236889810942,0.00098409435723206817,0.0059102167786036315,false,false +10,cable:1:core,cable:3:core,0.02522567792979501,0.0087430018601509681,0.025190622214158687,0.0087566580699134141,0.00132942825352933,0.0060061317555362313,false,false +10,cable:2:core,cable:1:core,0.044289178042812648,0.0069906515150912714,0.044278254571622711,0.0069913423158314949,0.00098359737623199273,0.0059072320389557632,false,false +10,cable:2:core,cable:2:core,0.09531206744696942,0.0064366840498130988,0.095309304281714802,0.0064367143164253161,0.00072575364356509964,0.0059706808851085948,false,false +10,cable:2:core,cable:3:core,0.044041405506186383,0.0069518146186880779,0.044030081726194048,0.0069524293450889283,0.0009986501614766552,0.005997093026922958,false,false +10,cable:3:core,cable:1:core,0.025225677929784498,0.0087430018601473564,0.025190622214148028,0.00875665806990974,0.0013294282535317725,0.006006131755547226,false,false +10,cable:3:core,cable:2:core,0.044041298839148882,0.0069517977815898354,0.044029963756820223,0.0069524107175119431,0.00099914714247403835,0.0060000774967492411,false,false +10,cable:3:core,cable:3:core,0.078582934237913696,0.0064406971080819796,0.078570835006983586,0.0064409714349911509,0.0013789270269418674,0.005700507165808829,false,false +100,cable:1:core,cable:1:core,0.078506859213295546,0.0062050114328727904,0.078472076361085996,0.006207498330190961,0.0023367017601947371,0.0044974976049810614,false,false +100,cable:1:core,cable:2:core,0.044623664076426955,0.0073898906143090182,0.044594677676407457,0.007395965221025768,0.0016081413318249638,0.0049127724909519476,false,false +100,cable:1:core,cable:3:core,0.02374023411694599,0.0095000867116944322,0.023660260481479511,0.0095783759852227958,0.0019469950888343912,0.0051489635525206797,false,false +100,cable:2:core,cable:1:core,0.044625191151152786,0.0073901435051331316,0.044596308609869517,0.007396235709070333,0.0016052861583539246,0.0049040501122606149,false,false +100,cable:2:core,cable:2:core,0.096977583514753135,0.0064526716430934467,0.096970615014242867,0.0064531518307158085,0.0011625524162005574,0.0045228760784483792,false,false +100,cable:2:core,cable:3:core,0.044427119942100003,0.0073575831247469888,0.044398125330420868,0.0073636093182377748,0.0016048219508657535,0.004902638939597778,false,false +100,cable:3:core,cable:1:core,0.023740234116939936,0.0095000867116920261,0.023660260481473294,0.0095783759852202961,0.0019469950888361121,0.0051489635525252169,false,false +100,cable:3:core,cable:2:core,0.044425593304720248,0.007357330298062798,0.044396494396971242,0.0073633388213044495,0.001607677124333684,0.0049113613306490988,false,false +100,cable:3:core,cable:3:core,0.078305022104803307,0.0061891574827913041,0.078270797483721566,0.0061916748393765521,0.0023148969946805709,0.0044557165252902585,false,false +1000,cable:1:core,cable:1:core,0.07431763829796087,0.0054060922772405417,0.074106951579780755,0.0054210957293142591,0.0055920559491631217,0.0038509478450420172,false,false +1000,cable:1:core,cable:2:core,0.04194112370447349,0.0076899031008897676,0.041823784910440587,0.0077522767528017643,0.0031351034048576754,0.0039179708463386274,false,false +1000,cable:1:core,cable:3:core,0.019637915800238218,0.010489955575026549,0.019406973705721642,0.011030833328742738,0.0030028500732928176,0.0046932860386825941,false,false +1000,cable:2:core,cable:1:core,0.041954144204047611,0.0076922904088874153,0.041836950529193118,0.0077547170752261911,0.003133653827701699,0.0039161592949148866,false,false +1000,cable:2:core,cable:2:core,0.093402547568307542,0.0060229309186851498,0.093380590533941543,0.0060293854903412814,0.0020251429041673319,0.0025566344206970609,false,false +1000,cable:2:core,cable:3:core,0.042296513991160765,0.0077545872364662878,0.04218461072415014,0.0078186540322622948,0.0030746892292352612,0.0038427537994676526,false,false +1000,cable:3:core,cable:1:core,0.019637915800236008,0.010489955575025378,0.019406973705718977,0.011030833328741239,0.0030028500732955932,0.004693286038686917,false,false +1000,cable:3:core,cable:2:core,0.04228348864787293,0.0077521991871619886,0.042171445105410044,0.0078162138670863478,0.0030761388063912376,0.0038445654843920298,false,false +1000,cable:3:core,cable:3:core,0.074501476744519221,0.0054193957081953353,0.074307191450508014,0.0054356641195338288,0.0053769262456990763,0.0037033482458058444,false,false +10000,cable:1:core,cable:1:core,0.085465509129258405,0.0048437210339593193,0.082039596165728312,0.0047896337109809301,0.023955331592102169,0.0056551647533908279,false,false +10000,cable:1:core,cable:2:core,0.037730453173686632,0.0093897903233363857,0.035678537640796648,0.010417326088059656,0.012273102643831191,0.0058402469056391265,false,false +10000,cable:1:core,cable:3:core,0.013435807386100814,0.01382150757153951,0.010487941289764935,0.023371839650942958,0.0083978573230821807,0.0097386694898818748,false,false +10000,cable:2:core,cable:1:core,0.037701977297683216,0.0093827036736289348,0.035639371641854378,0.010405890502143722,0.012299361004898035,0.0058527421414746221,false,false +10000,cable:2:core,cable:2:core,0.093345186446843004,0.0052730139931134231,0.092713916964939358,0.005335295441166104,0.010837593543504553,0.003209837679843887,false,false +10000,cable:2:core,cable:3:core,0.037677773886801151,0.0093766584919387306,0.035566012128483759,0.010384693616395921,0.012436777168602031,0.0059177456776463438,false,false +10000,cable:3:core,cable:1:core,0.013435807386101846,0.013821507571540549,0.010487941289765101,0.023371839650943319,0.008397857323083624,0.0097386694898835297,false,false +10000,cable:3:core,cable:2:core,0.037706096145820024,0.0093837068953645277,0.035605178127425585,0.01039612944725943,0.012410518807532522,0.0059052512588258872,false,false +10000,cable:3:core,cable:3:core,0.085903929936605811,0.0048684511517558373,0.082374053223681187,0.0048090660717229859,0.024372126170185027,0.0057529919095197094,false,false +100000,cable:1:core,cable:1:core,0.11315741718908486,0.0042722858396769137,0.097036454738812949,0.0038489658268120956,0.058211060088499522,0.0071689875184347881,false,false +100000,cable:1:core,cable:2:core,0.0096689793938681268,0.0089942077693072487,0.00023101820279822216,0.0013241111027446773,0.0096662191734422365,0.0091124498508629338,false,false +100000,cable:1:core,cable:3:core,0.0018557961159780922,0.027176280454428874,0.0018318868222285886,0.035918527118305732,0.00029693415873660589,0.0065389998277578746,false,false +100000,cable:2:core,cable:1:core,0.0096811327794820955,0.0090055130033820947,0.00022951954710173639,0.0013155213612313565,0.0096784116812347332,0.0091239438604463954,false,false +100000,cable:2:core,cable:2:core,0.10966791262461896,0.0041448686019930599,0.0938428818912449,0.0037277748094630601,0.056750018306489736,0.006967775024588895,false,false +100000,cable:2:core,cable:3:core,0.0097377283842100225,0.0090577358045038203,0.00018953563635074056,0.0010865973559371573,0.0097358836438816709,0.0091776260654667131,false,false +100000,cable:3:core,cable:1:core,0.0018557961159780811,0.0271762804544287,0.0018318868222285886,0.035918527118305739,0.0002969341587365365,0.0065389998277563394,false,false +100000,cable:3:core,cable:2:core,0.009725567510988152,0.0090464241338092111,0.00019103429204658795,0.0010951890664350986,0.0097236911360887301,0.0091661326785881427,false,false +100000,cable:3:core,cable:3:core,0.1144135426988117,0.00431950170976351,0.098252285992323607,0.0038970040221093893,0.058625481236195043,0.0072196571089488378,false,false +1000000,cable:1:core,cable:1:core,0.12638102356686592,0.002785048006455541,0.06620646713563616,0.0016187407760549527,0.10765159927853674,0.0054765507270382267,false,false +1000000,cable:1:core,cable:2:core,0.0001923118575921154,0.0068524612517472088,0.00015803851878552766,0.0076860020710100285,0.00010957954713634518,0.0057369617815247334,false,false +1000000,cable:1:core,cable:3:core,3.4795553209754845e-06,0.15502131367650746,1.2599509207989033e-06,0.075522694414567146,3.2434285731162328e-06,0.2159971047182615,false,false +1000000,cable:2:core,cable:1:core,0.0001923118086873636,0.0068524595091717828,0.00015803847191010051,0.0076859997912833303,0.00010957952891359549,0.0057369608274852124,false,false +1000000,cable:2:core,cable:2:core,0.11988280686557391,0.0026421705289196174,0.062646652944557957,0.0015318366929217447,0.10221195750406409,0.0052012560663941077,false,false +1000000,cable:2:core,cable:3:core,0.0001921380664239248,0.0068462959984796025,0.00015765170904975812,0.0076673342925860791,0.00010983157652886058,0.0057500807301349771,false,false +1000000,cable:3:core,cable:1:core,3.4795553209754739e-06,0.15502131367650696,1.259950920798883e-06,0.075522694414565869,3.2434285731162294e-06,0.21599710471826145,false,false +1000000,cable:3:core,cable:2:core,0.00019213811530244143,0.0068462977401271569,0.0001576517559251818,0.0076673365723554914,0.00010983159475160681,0.0057500816841617296,false,false +1000000,cable:3:core,cable:3:core,0.12432322724984333,0.0027398256795864925,0.064591028744004575,0.0015793058649085352,0.10622741566849569,0.0054044899225272405,false,false +10000000,cable:1:core,cable:1:core,0.11054851137261967,0.0011293843148707356,0.02317849471691602,0.00028677967317061823,0.10809130746438456,0.0019575982723189574,false,false +10000000,cable:1:core,cable:2:core,2.1066173926479604e-07,0.59967121560503911,2.0364987070292197e-07,0.68727712796085827,5.3898966156616346e-08,0.28563811366595671,false,false +10000000,cable:1:core,cable:3:core,1.0092849640468347e-15,NaN,9.0543505800134012e-16,NaN,4.4591870827890843e-16,NaN,true,true +10000000,cable:2:core,cable:1:core,2.1066173926479602e-07,0.59967121560503911,2.0364987070292194e-07,0.68727712796085816,5.389896615661632e-08,0.28563811366595659,false,false +10000000,cable:2:core,cable:2:core,0.11279405220794937,0.00115232089289832,0.024161497530116094,0.00029894567028500844,0.11017586056206596,0.0019952754499068907,false,false +10000000,cable:2:core,cable:3:core,2.8004189857265998e-07,0.65850514179037156,2.6112108204838523e-07,0.73807789364680476,1.0118915686011257e-07,0.42879145316872191,false,false +10000000,cable:3:core,cable:1:core,1.0092849640468369e-15,NaN,9.054350580013415e-16,NaN,4.459187082789108e-16,NaN,true,true +10000000,cable:3:core,cable:2:core,2.800418985726602e-07,0.65850514179037167,2.6112108204838539e-07,0.73807789364680476,1.0118915686011265e-07,0.42879145316872208,false,false +10000000,cable:3:core,cable:3:core,0.11422886716367056,0.0011669854375752117,0.02558395459499252,0.00031655103557803941,0.11132697499158439,0.0020160800057908508,false,false diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/Z-differences.csv b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/Z-differences.csv new file mode 100644 index 000000000..09d8b77f7 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/Z-differences.csv @@ -0,0 +1,82 @@ +frequency_hz,response_terminal,basis_terminal,absolute_error,relative_error,real_absolute_error,real_relative_error,imaginary_absolute_error,imaginary_relative_error,real_reference_near_zero,imaginary_reference_near_zero +0.10000000000000001,cable:1:core,cable:1:core,2.0406816271213964e-08,0.0060182561639335946,1.9476931204814946e-08,0.0061695631690259481,6.0899344142600404e-09,0.0049211822059048145,false,false +0.10000000000000001,cable:1:core,cable:2:core,3.1963136966295769e-09,0.0039070116150096406,1.3355561698157166e-10,0.001350546063710222,3.1935222160546346e-09,0.0039324346420730348,false,false +0.10000000000000001,cable:1:core,cable:3:core,2.8513597146125118e-09,0.0038950365518541905,1.3701194878825601e-10,0.0013855855979363627,2.8480660013427338e-09,0.0039265239638714752,false,false +0.10000000000000001,cable:2:core,cable:1:core,3.1963136966292585e-09,0.0039070116150092512,1.3355561698154519e-10,0.001350546063709954,3.193522216054317e-09,0.0039324346420726427,false,false +0.10000000000000001,cable:2:core,cable:2:core,2.0319334575079277e-08,0.0059922564469127642,1.9478689642084083e-08,0.0061701167542804998,5.7841168212077511e-09,0.0046729009177310125,false,false +0.10000000000000001,cable:2:core,cable:3:core,3.1901829464966795e-09,0.0038994886832207183,1.336073470417303e-10,0.0013510698770968759,3.1873839286999468e-09,0.0039248464207411486,false,false +0.10000000000000001,cable:3:core,cable:1:core,2.8513597146125118e-09,0.0038950365518541905,1.3701194878825601e-10,0.0013855855979363625,2.8480660013427338e-09,0.0039265239638714752,false,false +0.10000000000000001,cable:3:core,cable:2:core,3.190182946496467e-09,0.0038994886832204568,1.3360734704170383e-10,0.0013510698770966079,3.1873839286997351e-09,0.0039248464207408858,false,false +0.10000000000000001,cable:3:core,cable:3:core,2.0397310578959553e-08,0.006015432163948049,1.9476922016400587e-08,0.0061695602764368085,6.0580349637143344e-09,0.0048952785633975754,false,false +1,cable:1:core,cable:1:core,5.507808424665062e-08,0.0047233900558140574,1.8252745830095994e-08,0.0044907585606797579,5.1965687082372505e-08,0.0047546672876408397,false,false +1,cable:1:core,cable:2:core,2.3181662332445536e-08,0.0034344673526018263,1.3054720732853326e-09,0.0013110738652539551,2.314487440366433e-08,0.0034669494593144818,false,false +1,cable:1:core,cable:3:core,1.9588004696494286e-08,0.0033238802967687844,1.6439879133883607e-09,0.0016520043724800244,1.9518894224072101e-08,0.0033604118703711786,false,false +1,cable:2:core,cable:1:core,2.318166233244724e-08,0.0034344673526020787,1.3054720732855444e-09,0.0013110738652541676,2.3144874403666024e-08,0.0034669494593147355,false,false +1,cable:2:core,cable:2:core,5.2788258378960634e-08,0.0045260868022904861,1.8426510160760427e-08,0.0045333170210325622,4.946780716768533e-08,0.0045250863218513496,false,false +1,cable:2:core,cable:3:core,2.3150253870865903e-08,0.0034297983685706692,1.3082859102836262e-09,0.0013139034894257516,2.3113256851047518e-08,0.0034621969612539847,false,false +1,cable:3:core,cable:1:core,1.9588004696495186e-08,0.0033238802967689375,1.643987913388996e-09,0.001652004372480664,1.9518894224072948e-08,0.0033604118703713247,false,false +1,cable:3:core,cable:2:core,2.3150253870865913e-08,0.0034297983685706713,1.3082859102838379e-09,0.001313903489425965,2.3113256851047518e-08,0.0034621969612539851,false,false +1,cable:3:core,cable:3:core,5.5406691686930907e-08,0.0047517033168123137,1.8254397547477161e-08,0.0044911631108223767,5.2313272253504448e-08,0.0047866222670935044,false,false +10,cable:1:core,cable:1:core,4.2422382934974958e-07,0.0044504784527611236,1.8540431297873252e-08,0.0013169698099788608,4.2381848684956431e-07,0.0044955263797251304,false,false +10,cable:1:core,cable:2:core,1.4817972443400351e-07,0.0027889450380819262,1.050984110898917e-08,0.0010350343925043532,1.4780654238970959e-07,0.0028341609383735408,false,false +10,cable:1:core,cable:3:core,1.0019506524912759e-07,0.0022436580289046677,2.3028904625196987e-08,0.0022751730275270159,9.7512669187344654e-08,0.0022419386711860865,false,false +10,cable:2:core,cable:1:core,1.4817972443398336e-07,0.0027889450380815463,1.0509841108990864e-08,0.0010350343925045202,1.4780654238968926e-07,0.0028341609383731501,false,false +10,cable:2:core,cable:2:core,4.0475034496165286e-07,0.0042452664397349873,2.5168651668982476e-08,0.0017869523484917772,4.039670540028511e-07,0.004284055765296624,false,false +10,cable:2:core,cable:3:core,1.4942960623149575e-07,0.0028125349093116991,1.0553314557766328e-08,0.0010393202118583434,1.490564818125824e-07,0.0028581967806074315,false,false +10,cable:3:core,cable:1:core,1.001950652491396e-07,0.0022436580289049378,2.3028904625191905e-08,0.0022751730275265133,9.7512669187358207e-08,0.0022419386711863987,false,false +10,cable:3:core,cable:2:core,1.4942960623151505e-07,0.0028125349093120642,1.0553314557752776e-08,0.0010393202118570075,1.4905648181260273e-07,0.0028581967806078231,false,false +10,cable:3:core,cable:3:core,4.2489057230425445e-07,0.004457501831262185,1.8786942318055786e-08,0.001334456692461488,4.2447502780655416e-07,0.0045025217951304118,false,false +100,cable:1:core,cable:1:core,4.3920589304340287e-06,0.0056110070277203489,7.5600064921126629e-07,0.0063929231348321507,4.3265049019731213e-06,0.0055914371237546683,false,false +100,cable:1:core,cable:2:core,7.5309638760734481e-07,0.0019522968660933113,1.2272338860826138e-07,0.0011568123260514694,7.4302970257974056e-07,0.0020034549606339504,false,false +100,cable:1:core,cable:3:core,2.1121540174638963e-07,0.00069640262305146095,1.6056073732991253e-07,0.0015359459239639591,1.3723044692401029e-07,0.00048199962797012397,false,false +100,cable:2:core,cable:1:core,7.5309638760739828e-07,0.0019522968660934501,1.2272338860826138e-07,0.0011568123260514694,7.4302970257979477e-07,0.002003454960634097,false,false +100,cable:2:core,cable:2:core,4.2990934870278096e-06,0.0054915018851088944,7.9023278534119874e-07,0.0066806459942483163,4.2258415676852843e-06,0.0054606249144078861,false,false +100,cable:2:core,cable:3:core,7.4595327491769492e-07,0.0019337429926763682,1.202262059744896e-07,0.0011332467663396791,7.3620102401274461e-07,0.0019850060255912267,false,false +100,cable:3:core,cable:1:core,2.1121540174654681e-07,0.00069640262305197941,1.605607373299803e-07,0.0015359459239646081,1.3723044692417292e-07,0.00048199962797069535,false,false +100,cable:3:core,cable:2:core,7.4595327491785543e-07,0.0019337429926767848,1.202262059744896e-07,0.0011332467663396791,7.3620102401290724e-07,0.0019850060255916652,false,false +100,cable:3:core,cable:3:core,4.3647434890483931e-06,0.0055759083438218417,7.590151355272147e-07,0.006418250746368208,4.2982417043752833e-06,0.0055547077736520841,false,false +1000,cable:1:core,cable:1:core,5.0112361333283211e-05,0.0081516651063368428,3.0113070048497399e-06,0.0025422928901207612,5.0021803131435408e-05,0.0082923143960317962,false,false +1000,cable:1:core,cable:2:core,1.9580474854186773e-06,0.00082742681037456295,1.1766336228261672e-06,0.0010688988097058998,1.5650825130929594e-06,0.00074712201664113489,false,false +1000,cable:1:core,cable:3:core,3.1556723090945284e-06,0.0019529719880883797,5.2059395967219216e-07,0.00051925122194247761,3.1124346822927577e-06,0.0024562009287664079,false,false +1000,cable:2:core,cable:1:core,1.9580474854190237e-06,0.00082742681037470964,1.1766336228261672e-06,0.0010688988097059,1.5650825130933931e-06,0.00074712201664134219,false,false +1000,cable:2:core,cable:2:core,5.0194191117460129e-05,0.0081649638133294983,2.9968213311843891e-06,0.0025303461411112812,5.010464932364133e-05,0.0083059992971100706,false,false +1000,cable:2:core,cable:3:core,1.8103662132010835e-06,0.00076496431933728896,1.1926462802477542e-06,0.0010834610783242388,1.3619914376056946e-06,0.00065010958680645928,false,false +1000,cable:3:core,cable:1:core,3.1556723090935306e-06,0.0019529719880877641,5.2059395967262584e-07,0.00051925122194291054,3.1124346822916735e-06,0.0024562009287655549,false,false +1000,cable:3:core,cable:2:core,1.8103662132013694e-06,0.00076496431933740995,1.1926462802481878e-06,0.0010834610783246332,1.3619914376056946e-06,0.0006501095868064595,false,false +1000,cable:3:core,cable:3:core,5.0090961754964124e-05,0.008148176031697282,3.1242533888877454e-06,0.0026378992970405909,4.9993434471931435e-05,0.0082875726351871615,false,false +10000,cable:1:core,cable:1:core,0.00033016370676636285,0.0074772879693647334,9.8613975890518568e-05,0.008868271280559276,0.00031509261658244081,0.0073736180936282607,false,false +10000,cable:1:core,cable:2:core,1.1946069448571345e-05,0.0012721294666787422,9.7824412583786408e-06,0.0012187968629714455,6.8565602379371676e-06,0.0014065849209818396,false,false +10000,cable:1:core,cable:3:core,2.2983225404856133e-05,0.0052183487158196486,1.3443567075076268e-05,0.0030554182686121732,1.8641329199076703e-05,0.09471799452032828,false,false +10000,cable:2:core,cable:1:core,1.1946069448570349e-05,0.0012721294666786362,9.7824412583786408e-06,0.001218796862971445,6.8565602379354329e-06,0.0014065849209814847,false,false +10000,cable:2:core,cable:2:core,0.00033718036335005135,0.0076365321513983796,0.00010655851822037946,0.0095884968850909504,0.00031989979622305126,0.0074861596739023064,false,false +10000,cable:2:core,cable:3:core,1.1717644452949676e-05,0.0012478059452165494,9.6588427848046804e-06,0.0012033791664438508,6.6339993657045862e-06,0.001360989943750026,false,false +10000,cable:3:core,cable:1:core,2.2983225404855164e-05,0.0052183487158194292,1.3443567075075401e-05,0.0030554182686119763,1.8641329199076134e-05,0.094717994520325116,false,false +10000,cable:3:core,cable:2:core,1.1717644452947755e-05,0.0012478059452163447,9.6588427848029457e-06,0.0012033791664436341,6.6339993657037188e-06,0.0013609899437498484,false,false +10000,cable:3:core,cable:3:core,0.0003310937166445695,0.0074985098650658943,9.8657342247943786e-05,0.0088722057810446311,0.00031605344171846406,0.0073962690794204212,false,false +100000,cable:1:core,cable:1:core,0.0016154782906252691,0.005419674749492156,0.00037728651404855817,0.0041236651845613778,0.0015708039323221179,0.0055370890873728774,false,false +100000,cable:1:core,cable:2:core,1.4715903302091928e-05,0.0011922821576872793,6.1572803095356898e-06,0.00070501888428775833,1.3365841132765427e-05,0.0015324951427680992,false,false +100000,cable:1:core,cable:3:core,2.4219256447629282e-05,0.016224684861121757,9.4892987920968622e-06,0.0065862675174225725,2.228285420026666e-05,0.057068527865840213,false,false +100000,cable:2:core,cable:1:core,1.4715903302093751e-05,0.0011922821576874272,6.1572803095287509e-06,0.00070501888428696447,1.3365841132770631e-05,0.0015324951427686949,false,false +100000,cable:2:core,cable:2:core,0.0015794754683649504,0.0052984100213463075,0.00037609033400165526,0.0041105483744021077,0.0015340465494362299,0.0054069832031223052,false,false +100000,cable:2:core,cable:3:core,1.45811371622721e-05,0.0011813581962924681,6.5952481166104554e-06,0.0007551290617974529,1.3004317099538087e-05,0.0014911054766371654,false,false +100000,cable:3:core,cable:1:core,2.4219256447629147e-05,0.01622468486112167,9.4892987920966454e-06,0.0065862675174224233,2.2282854200266606e-05,0.057068527865840081,false,false +100000,cable:3:core,cable:2:core,1.4581137162272122e-05,0.0011813581962924699,6.5952481166139248e-06,0.00075512906179784993,1.3004317099536353e-05,0.0014911054766369673,false,false +100000,cable:3:core,cable:3:core,0.0016299713952056781,0.0054685631684245721,0.00038152287703621668,0.0041701608632480807,0.0015846914663387168,0.0055863161612910934,false,false +1000000,cable:1:core,cable:1:core,0.0058063708541588772,0.0033370337035997892,0.00033934730314533201,0.00045022093421742486,0.0057964459717894101,0.0036961201536463901,false,false +1000000,cable:1:core,cable:2:core,4.8574616610321926e-06,0.0045124853754643004,4.512739422138346e-06,0.028546530713694677,1.7972525410403766e-06,0.001687912590686447,false,false +1000000,cable:1:core,cable:3:core,1.2882541074491399e-07,0.085849265245670214,1.0927199145217743e-07,1.5211092262339549,6.8232091699368725e-08,0.045522064500799438,false,false +1000000,cable:2:core,cable:1:core,4.8574616610321307e-06,0.0045124853754642423,4.5127394221380207e-06,0.028546530713692685,1.7972525410410271e-06,0.0016879125906870576,false,false +1000000,cable:2:core,cable:2:core,0.0055601444398579404,0.003195130814093403,0.00036316180591211555,0.00048183151786090157,0.0055482717754999911,0.0035373113467311768,false,false +1000000,cable:2:core,cable:3:core,4.8538463217835019e-06,0.0045089385260994042,4.4890222792185537e-06,0.028400762572894461,1.8462131762531347e-06,0.0017338148699000979,false,false +1000000,cable:3:core,cable:1:core,1.2882541074491317e-07,0.085849265245669756,1.0927199145217738e-07,1.5211092262339532,6.8232091699367243e-08,0.045522064500798494,false,false +1000000,cable:3:core,cable:2:core,4.8538463217833511e-06,0.0045089385260992628,4.4890222792183911e-06,0.028400762572893451,1.8462131762531347e-06,0.0017338148699000975,false,false +1000000,cable:3:core,cable:3:core,0.0057243482323389152,0.0032897435000615268,0.00032518024774375043,0.00043141702280223522,0.00571510459148028,0.0036440635839456005,false,false +10000000,cable:1:core,cable:1:core,0.010874978661416526,0.0013481808196322126,0.0019936321636171428,0.00043816781617941918,0.010690677793407488,0.0016050347244380826,false,false +10000000,cable:1:core,cable:2:core,1.7369940958772128e-08,0.60001217996457723,1.4043604584509934e-08,0.82160750014076456,1.0222133788263637e-08,0.43750795432297629,false,false +10000000,cable:1:core,cable:3:core,1.758490602438255e-16,NaN,1.6125915676464352e-16,NaN,7.0131136795240231e-17,NaN,true,true +10000000,cable:2:core,cable:1:core,1.7369940958772141e-08,0.60001217996457734,1.4043604584509934e-08,0.82160750014076456,1.022213378826366e-08,0.43750795432297684,false,false +10000000,cable:2:core,cable:2:core,0.011029102417545492,0.0013672928193374241,0.0021310376934859931,0.00046835316534374344,0.010821265105598776,0.0016246721788059813,false,false +10000000,cable:2:core,cable:3:core,2.3084447437255037e-08,0.65870223072825995,1.7039661389847225e-08,0.84821294788794399,1.5573748848711423e-08,0.54233570985085944,false,false +10000000,cable:3:core,cable:1:core,1.758490602438255e-16,NaN,1.6125915676464347e-16,NaN,7.0131136795240355e-17,NaN,true,true +10000000,cable:3:core,cable:2:core,2.3084447437255044e-08,0.65870223072825984,1.7039661389847219e-08,0.84821294788794388,1.5573748848711439e-08,0.54233570985085966,false,false +10000000,cable:3:core,cable:3:core,0.01110243513586638,0.0013763766299583725,0.0022743031280532122,0.00049982388140680143,0.010866996421635911,0.0016315493431338206,false,false diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/fem/Y.csv b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/fem/Y.csv new file mode 100644 index 000000000..3059225f5 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/fem/Y.csv @@ -0,0 +1,82 @@ +frequency_hz,response_terminal,basis_terminal,real_S_per_m,imaginary_S_per_m +0.10000000000000001,cable:1:core,cable:1:core,11.793927042362855,0.09163136261981264 +0.10000000000000001,cable:1:core,cable:2:core,-6.6206073939719623,0.065515807071277354 +0.10000000000000001,cable:1:core,cable:3:core,-3.2760387858376196,0.091292028100808498 +0.10000000000000001,cable:2:core,cable:1:core,-6.6206073939719552,0.065515807071276772 +0.10000000000000001,cable:2:core,cable:2:core,14.599945452367439,0.046947359746437323 +0.10000000000000001,cable:2:core,cable:3:core,-6.6204481639483124,0.065512170527795427 +0.10000000000000001,cable:3:core,cable:1:core,-3.2760387858376205,0.091292028100809677 +0.10000000000000001,cable:3:core,cable:2:core,-6.6204481639483124,0.065512170527793637 +0.10000000000000001,cable:3:core,cable:3:core,11.793670814855151,0.091621161120561595 +1,cable:1:core,cable:1:core,11.951697089000811,0.13971411387969557 +1,cable:1:core,cable:2:core,-6.5080852722992288,0.09917856437094108 +1,cable:1:core,cable:3:core,-3.119361002943668,0.13702747250048025 +1,cable:2:core,cable:1:core,-6.5080852722992155,0.099178564370941497 +1,cable:2:core,cable:2:core,14.681691188828179,0.071273811947358101 +1,cable:2:core,cable:3:core,-6.5086850876535216,0.099186062512871348 +1,cable:3:core,cable:1:core,-3.1193610029436729,0.13702747250047986 +1,cable:3:core,cable:2:core,-6.5086850876535101,0.099186062512871709 +1,cable:3:core,cable:3:core,11.952479429588399,0.1397353161481425 +10,cable:1:core,cable:1:core,12.198656471407986,0.24185580199235357 +10,cable:1:core,cable:2:core,-6.3332980379686301,0.16650732013666913 +10,cable:1:core,cable:3:core,-2.8767392780483174,0.22134516984312108 +10,cable:2:core,cable:1:core,-6.3332980379686363,0.16650732013666858 +10,cable:2:core,cable:2:core,14.807136000816893,0.12155291122243583 +10,cable:2:core,cable:3:core,-6.333049865123205,0.16652237292191072 +10,cable:3:core,cable:1:core,-2.8767392780483072,0.22134516984312255 +10,cable:3:core,cable:2:core,-6.3330498651232174,0.16652237292191016 +10,cable:3:core,cable:3:core,12.198600133535841,0.24189549926584741 +100,cable:1:core,cable:1:core,12.641497780100403,0.51955597655167107 +100,cable:1:core,cable:2:core,-6.0295953731137866,0.32733885698691378 +100,cable:1:core,cable:3:core,-2.470174538771686,0.37813339888204062 +100,cable:2:core,cable:1:core,-6.0295953731137963,0.32733885698691145 +100,cable:2:core,cable:2:core,15.026860913558655,0.25703830837642216 +100,cable:2:core,cable:3:core,-6.0293971898343495,0.32733839277942345 +100,cable:3:core,cable:1:core,-2.4701745387716816,0.37813339888204167 +100,cable:3:core,cable:2:core,-6.029397189834353,0.32733839277942295 +100,cable:3:core,cable:3:core,12.641296501223044,0.51953417178615768 +1000,cable:1:core,cable:1:core,13.670105690820352,1.4521245610642923 +1000,cable:1:core,cable:2:core,-5.3950324845310735,0.80018548575660087 +1000,cable:1:core,cable:3:core,-1.7593388574871698,0.6398182528281865 +1000,cable:2:core,cable:1:core,-5.3950324845310762,0.80018548575660153 +1000,cable:2:core,cable:2:core,15.487580066580868,0.79211282136113192 +1000,cable:2:core,cable:3:core,-5.3953801447260359,0.80012652115813576 +1000,cable:3:core,cable:1:core,-1.7593388574871671,0.63981825282818849 +1000,cable:3:core,cable:2:core,-5.395380144726043,0.80012652115813565 +1000,cable:3:core,cable:3:core,13.670305930691081,1.4519094313608261 +10000,cable:1:core,cable:1:core,17.128574149133918,4.2360094951678624 +10000,cable:1:core,cable:2:core,-3.4249228006495258,2.1014698251850854 +10000,cable:1:core,cable:3:core,-0.44874265125902446,0.86232080591781557 +10000,cable:2:core,cable:1:core,-3.4249228006495258,2.1014698251850819 +10000,cable:2:core,cable:2:core,17.377466344145962,3.3763680984739541 +10000,cable:2:core,cable:3:core,-3.4248494411361543,2.1016072413487854 +10000,cable:3:core,cable:1:core,-0.44874265125902463,0.86232080591781735 +10000,cable:3:core,cable:2:core,-3.4248494411361552,2.1016072413487867 +10000,cable:3:core,cable:3:core,17.128908606191871,4.2364262897459461 +100000,cable:1:core,cable:1:core,25.211046058879496,8.1198439722222862 +100000,cable:1:core,cable:2:core,0.17447040684075316,1.0607706304717672 +100000,cable:1:core,cable:3:core,0.051001167620121454,-0.045409721143611068 +100000,cable:2:core,cable:1:core,0.17447040684075332,1.0607706304717672 +100000,cable:2:core,cable:2:core,25.173967497452402,8.1446398751713485 +100000,cable:2:core,cable:3:core,0.17443042293000227,1.0608281024344142 +100000,cable:3:core,cable:1:core,0.051001167620121447,-0.045409721143611116 +100000,cable:3:core,cable:2:core,0.17443042293000166,1.0608281024344137 +100000,cable:3:core,cable:3:core,25.212261890133007,8.1202583933699799 +1000000,cable:1:core,cable:1:core,40.899981093321514,19.656825006121288 +1000000,cable:1:core,cable:2:core,-0.020561862633581049,-0.019100623519793053 +1000000,cable:1:core,cable:3:core,-1.6683076928938046e-05,-1.5016074300379346e-05 +1000000,cable:2:core,cable:1:core,-0.020561862633581056,-0.019100623519793056 +1000000,cable:2:core,cable:2:core,40.89643056210452,19.651398854301153 +1000000,cable:2:core,cable:3:core,-0.02056147587072071,-0.019100875567408322 +1000000,cable:3:core,cable:1:core,-1.6683076928938059e-05,-1.5016074300379334e-05 +1000000,cable:3:core,cable:2:core,-0.0205614758707207,-0.019100875567408311 +1000000,cable:3:core,cable:3:core,40.898365654929883,19.65540082251125 +10000000,cable:1:core,cable:1:core,80.823352857111601,55.216286708477909 +10000000,cable:1:core,cable:2:core,-2.9631405210173127e-07,-1.886966884946216e-07 +10000000,cable:1:core,cable:3:core,-1.0854589216983598e-15,6.4134107784461141e-16 +10000000,cable:2:core,cable:1:core,-2.9631405210173127e-07,-1.8869668849462157e-07 +10000000,cable:2:core,cable:2:core,80.822369854298401,55.218371261575591 +10000000,cable:2:core,cable:3:core,-3.537852634471945e-07,-2.3598687919811782e-07 +10000000,cable:3:core,cable:1:core,-1.0854589216983612e-15,6.4134107784461368e-16 +10000000,cable:3:core,cable:2:core,-3.5378526344719471e-07,-2.359868791981179e-07 +10000000,cable:3:core,cable:3:core,80.820947397233525,55.219522376005109 diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/fem/Z.csv b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/fem/Z.csv new file mode 100644 index 000000000..866a65f45 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/fem/Z.csv @@ -0,0 +1,82 @@ +frequency_hz,response_terminal,basis_terminal,real_ohm_per_m,imaginary_ohm_per_m +0.10000000000000001,cable:1:core,cable:1:core,3.1569384527251656e-06,1.2374941953892434e-06 +0.10000000000000001,cable:1:core,cable:2:core,9.8890086440048917e-08,8.1209797662934007e-07 +0.10000000000000001,cable:1:core,cable:3:core,9.8883785305156379e-08,7.2534028253697365e-07 +0.10000000000000001,cable:2:core,cable:1:core,9.8890086440048931e-08,8.1209797662934018e-07 +0.10000000000000001,cable:2:core,cable:2:core,3.1569402035335558e-06,1.2378000139614139e-06 +0.10000000000000001,cable:2:core,cable:3:core,9.8890034709988745e-08,8.1210411491669455e-07 +0.10000000000000001,cable:3:core,cable:1:core,9.8883785305156392e-08,7.2534028253697365e-07 +0.10000000000000001,cable:3:core,cable:2:core,9.8890034709988785e-08,8.1210411491669486e-07 +0.10000000000000001,cable:3:core,cable:3:core,3.1569384435367512e-06,1.2375260948397891e-06 +1,cable:1:core,cable:1:core,4.0645128397490841e-06,1.0929405558502649e-05 +1,cable:1:core,cable:2:core,9.957273254260641e-07,6.6758614958986898e-06 +1,cable:1:core,cable:3:core,9.9514743470101773e-07,5.8084827030194123e-06 +1,cable:2:core,cable:1:core,9.957273254260641e-07,6.6758614958986898e-06 +1,cable:2:core,cable:2:core,4.0646859849575193e-06,1.0931903537134415e-05 +1,cable:2:core,cable:3:core,9.9572451158906602e-07,6.6758931134513074e-06 +1,cable:3:core,cable:1:core,9.9514743470101688e-07,5.8084827030194114e-06 +1,cable:3:core,cable:2:core,9.957245115890656e-07,6.6758931134513066e-06 +1,cable:3:core,cable:3:core,4.0645144914664652e-06,1.0929057973331517e-05 +10,cable:1:core,cable:1:core,1.4078098949110193e-05,9.4275608916675479e-05 +10,cable:1:core,cable:2:core,1.0154098438757886e-05,5.2151781639659582e-05 +10,cable:1:core,cable:3:core,1.0121825613513052e-05,4.3494797801831069e-05 +10,cable:2:core,cable:1:core,1.0154098438757886e-05,5.2151781639659596e-05 +10,cable:2:core,cable:2:core,1.4084679812657182e-05,9.4295470492056221e-05 +10,cable:2:core,cable:3:core,1.015405496530911e-05,5.2150531700236723e-05 +10,cable:3:core,cable:1:core,1.0121825613513055e-05,4.3494797801831048e-05 +10,cable:3:core,cable:2:core,1.0154054965309122e-05,5.2150531700236689e-05 +10,cable:3:core,cable:3:core,1.4078345460130376e-05,9.4274952375718503e-05 +100,cable:1:core,cable:1:core,0.00011825586406508788,0.00077377332628715296 +100,cable:1:core,cable:2:core,0.0001060875527036882,0.00037087417345515203 +100,cable:1:core,cable:3:core,0.00010453541028029063,0.00028471069054957095 +100,cable:2:core,cable:1:core,0.0001060875527036882,0.00037087417345515198 +100,cable:2:core,cable:2:core,0.0001182868821400726,0.00077387508461446977 +100,cable:2:core,cable:3:core,0.00010609004988632196,0.00037088100213371892 +100,cable:3:core,cable:1:core,0.00010453541028029058,0.00028471069054957084 +100,cable:3:core,cable:2:core,0.00010609004988632197,0.00037088100213371886 +100,cable:3:core,cable:3:core,0.00011825887855140381,0.00077380158948475069 +1000,cable:1:core,cable:1:core,0.0011844846896089539,0.0060323090445500768 +1000,cable:1:core,cable:2:core,0.0011007904697264187,0.0020948151416138971 +1000,cable:1:core,cable:3:core,0.001002585911545266,0.0012671742958162359 +1000,cable:2:core,cable:1:core,0.0011007904697264185,0.0020948151416138963 +1000,cable:2:core,cable:2:core,0.001184352323381433,0.0060323445176638027 +1000,cable:2:core,cable:3:core,0.0011007744570689971,0.0020950182326893848 +1000,cable:3:core,cable:1:core,0.0010025859115452653,0.0012671742958162346 +1000,cable:3:core,cable:2:core,0.0011007744570689967,0.0020950182326893844 +1000,cable:3:core,cable:3:core,0.0011843717432249161,0.0060323374132095807 +10000,cable:1:core,cable:1:core,0.011119864601649793,0.042732429667698781 +10000,cable:1:core,cable:2:core,0.0080263098434048309,0.0048746152014419982 +10000,cable:1:core,cable:3:core,0.0043999105501135146,-0.00019680874044557522 +10000,cable:2:core,cable:1:core,0.0080263098434048327,0.0048746152014419956 +10000,cable:2:core,cable:2:core,0.011113161895694636,0.042732163106039826 +10000,cable:2:core,cable:3:core,0.0080264334418784031,0.0048743926405697656 +10000,cable:3:core,cable:1:core,0.0043999105501135137,-0.00019680874044557579 +10000,cable:3:core,cable:2:core,0.0080264334418784066,0.0048743926405697647 +10000,cable:3:core,cable:3:core,0.011119821235292367,0.042731468842562757 +100000,cable:1:core,cable:1:core,0.091493003714531451,0.28368767551605356 +100000,cable:1:core,cable:2:core,0.0087334969981067243,-0.0087216205518427393 +100000,cable:1:core,cable:3:core,-0.0014407703250733342,-0.00039045784136311349 +100000,cable:2:core,cable:1:core,0.0087334969981067174,-0.0087216205518427445 +100000,cable:2:core,cable:2:core,0.091493956461796602,0.28371579711037065 +100000,cable:2:core,cable:3:core,0.0087339349659137991,-0.0087212590278095137 +100000,cable:3:core,cable:1:core,-0.001440770325073334,-0.00039045784136311344 +100000,cable:3:core,cable:2:core,0.0087339349659138008,-0.0087212590278095102 +100000,cable:3:core,cable:3:core,0.091488767351543793,0.28367378798203707 +1000000,cable:1:core,cable:1:core,0.75373506062126217,1.5682514990945176 +1000000,cable:1:core,cable:2:core,0.00015808363781219276,0.0010647782065002921 +1000000,cable:1:core,cable:3:core,7.1837044682661596e-08,1.4988795531927263e-06 +1000000,cable:2:core,cable:1:core,0.00015808363781219241,0.0010647782065002923 +1000000,cable:2:core,cable:2:core,0.75371118835143447,1.5684996969879792 +1000000,cable:2:core,cable:3:core,0.00015805992066927291,0.0010648271671355046 +1000000,cable:3:core,cable:1:core,7.1837044682661649e-08,1.4988795531927248e-06 +1000000,cable:3:core,cable:2:core,0.00015805992066927281,0.0010648271671355048 +1000000,cable:3:core,cable:3:core,0.75374922767666375,1.5683328404748265 +10000000,cable:1:core,cable:1:core,4.5499283379608109,6.6607143326137441 +10000000,cable:1:core,cable:2:core,1.70928388337544e-08,2.3364452433972144e-08 +10000000,cable:1:core,cable:3:core,1.7780742088352628e-16,8.2830433131016448e-17 +10000000,cable:2:core,cable:1:core,1.70928388337544e-08,2.3364452433972167e-08 +10000000,cable:2:core,cable:2:core,4.5500657434906797,6.6605837453015528 +10000000,cable:2:core,cable:3:core,2.0088895639091691e-08,2.8716067494419929e-08 +10000000,cable:3:core,cable:1:core,1.7780742088352623e-16,8.2830433131016571e-17 +10000000,cable:3:core,cable:2:core,2.0088895639091684e-08,2.8716067494419946e-08 +10000000,cable:3:core,cable:3:core,4.5502090089252469,6.6605380139855157 diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/fem/computation-options.txt b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/fem/computation-options.txt new file mode 100644 index 000000000..987cdbcf4 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/fem/computation-options.txt @@ -0,0 +1 @@ +(ui = false, mesh_policy = :remesh, resume_run_directory = nothing, keep_run_directory = true, trace = true, output_basis = :pul, verbosity = (default = 1,), gmsh_verbosity = 2, getdp_verbosity = 4, frequency_workers = 2, solver_threads = 4, plot_field_maps = false) \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/fem/details.txt b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/fem/details.txt new file mode 100644 index 000000000..addcde39b --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/fem/details.txt @@ -0,0 +1 @@ +(files = [(path = "coordinator.lock", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/coordinator.lock", sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"), (path = "run.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/run.json", sha256 = "ffdc3637591af411121db56364e2f2c04d6aeafc79d5c16a1200855cd0d62813"), (path = "timing-summary.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/timing-summary.json", sha256 = "e442c67db38bc4f8c1a982a95cb6ffa37f4dbbb7afe9e7c6a2b1f61ab3e36f74"), (path = "attempts/f0001-8W5TJ5/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0001-8W5TJ5/attempt.json", sha256 = "e0096f0b8084da736ebd0b97f6019987c9ab2f5cc26d31a0e0f286453e4015d8"), (path = "attempts/f0001-8W5TJ5/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0001-8W5TJ5/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0001-8W5TJ5/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0001-8W5TJ5/getdp.log", sha256 = "e425257e5bf869802a21e03bb2bcfe820abc43f43e71e9905126a39c28cbc085"), (path = "attempts/f0001-8W5TJ5/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0001-8W5TJ5/petsc.log", sha256 = "9e6afafefe3027f4a119499a1d49f586531bf1652a93b23d56303f5f1c15494f"), (path = "attempts/f0001-8W5TJ5/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0001-8W5TJ5/solver.pre", sha256 = "b1197bca659ee38214b5e1c5ac7a29739e43eabdda4306801c6df5793112a46c"), (path = "attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0001-P.tsv", sha256 = "04c9e6b2c890764d1fe53b5595feab56c4b9964ad522d22157758fe08511d41e"), (path = "attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0001-Z.tsv", sha256 = "ea6e5ec751ce81e04b81e925b9eff27bab7f148fc641aef12ae784e589a0d558"), (path = "attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0001-timing.tsv", sha256 = "b4d24bfdc6ff3d55fc16b1f6131e1aef94d8b0815edd8029a6f9172566a692bd"), (path = "attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0001.done", sha256 = "a40a11bab26b32dfb169550347f289dc710df24f59fe3ef20963d46f838f0586"), (path = "attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0002-P.tsv", sha256 = "58b5ac96646f67a8a9d68027ed7e65e00aaa7ccdec213ad9721e91001d305633"), (path = "attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0002-Z.tsv", sha256 = "1900dfd1f89bc54d88574665eee50adcd1e71e12caf41623d8b182074eda2916"), (path = "attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0002-timing.tsv", sha256 = "2296ec408a6bf04d984403a0256e1c18e167bbc7944d628d66da1d90a7fbb23e"), (path = "attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0002.done", sha256 = "f58042f034284ccf2955f2795037937a5bd0e4288ffc0d0d5124e33ee07f17ec"), (path = "attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0003-P.tsv", sha256 = "d1d337ca87a70e6d469f4530db9baa306c88627441482c75760e247c5087e00a"), (path = "attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0003-Z.tsv", sha256 = "37a39f817bcd20c901f7efdf9e9870f3e2fc0364c9b6a7a82b347f92140eab27"), (path = "attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0003-timing.tsv", sha256 = "ffdfd1faec39eef91c74586e5aa85cb800f3c7941340a4c4182c9904993a13f3"), (path = "attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0003.done", sha256 = "41695cf0039479bd9b703276e3c15366a77a1e43f8490b48634d8a1c2a61be94"), (path = "attempts/f0002-LZaICJ/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0002-LZaICJ/attempt.json", sha256 = "5ef66fb9a24b7219ebad79e36bec402529bdc1a5009886df5ae74c2cbd50e6a9"), (path = "attempts/f0002-LZaICJ/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0002-LZaICJ/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0002-LZaICJ/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0002-LZaICJ/getdp.log", sha256 = "361aaef0f48d463d93b480ce40100523c1e7b9863fb385c7e28ed18211b84311"), (path = "attempts/f0002-LZaICJ/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0002-LZaICJ/petsc.log", sha256 = "e124a500f3edaa1dbd5d87a382ae14a3e92ec840bc8d93725596d00e335754f3"), (path = "attempts/f0002-LZaICJ/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0002-LZaICJ/solver.pre", sha256 = "a18f62323a8274bb11d4fa84dd4f4d7b2565ba38426d561be33eaac16219c518"), (path = "attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0001-P.tsv", sha256 = "a2f141d70f0dd2d524a23219f83cfd6b6c6fbc8b4f98ce1cfe7a0048aa180f22"), (path = "attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0001-Z.tsv", sha256 = "ed761307661a09df2f10d0b6d7629be3fbd7b5d18a535a689a617105a8c89806"), (path = "attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0001-timing.tsv", sha256 = "e0ab4f02f2e74b89d0be68e501df864d67415c680b768402a4c81a32ee6f1cc6"), (path = "attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0001.done", sha256 = "3dbd9539f415ee889a34b9c6efe6b9cb8168b7c0945d52d7e63e920c8990d2d1"), (path = "attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0002-P.tsv", sha256 = "acb7239febeba6f0b026459ab2b13d63ff3c1a1b9857d02e0f3e07b7d991d69f"), (path = "attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0002-Z.tsv", sha256 = "f34bce613e012e1fecab9d0ee72589bd857a9747198349cf5c3e73f56060a138"), (path = "attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0002-timing.tsv", sha256 = "abafdd5f0b8dcdf79b68284c06d318fc87089e7492d5eb6ef3386cea1023a447"), (path = "attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0002.done", sha256 = "d498381e814531161686031fcde5aa549b456c6cbee61f6eb67916650247e717"), (path = "attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0003-P.tsv", sha256 = "25e7131c3d0de464fcb954e57953730133762473caa9f21c03cd809814dc1be8"), (path = "attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0003-Z.tsv", sha256 = "0341d1a792571476890702a3abdd764109ca9c72c953260e9df965d1df93fa74"), (path = "attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0003-timing.tsv", sha256 = "837caea517bfd3000c718be9bcd3c264a537dbeac0929f62dd0653cc70f6acdc"), (path = "attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0003.done", sha256 = "fa5c0c361441906d1aaa6b254ae52969806be93c36846f4c70c70a18dc833050"), (path = "attempts/f0003-AQ7KkO/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0003-AQ7KkO/attempt.json", sha256 = "d15ff0978ad018b157d0ab3b6533696f9063d4778dc4c85a874f2769f6fce0ae"), (path = "attempts/f0003-AQ7KkO/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0003-AQ7KkO/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0003-AQ7KkO/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0003-AQ7KkO/getdp.log", sha256 = "450a735060c386e538543cd578e7aae55a230193f0c14540e8bde1e5f7bba1ba"), (path = "attempts/f0003-AQ7KkO/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0003-AQ7KkO/petsc.log", sha256 = "72d17eb0dbc247aa836d5b3f16409127e904fd02d0195e788e3e740dd61ee9d3"), (path = "attempts/f0003-AQ7KkO/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0003-AQ7KkO/solver.pre", sha256 = "fd78895c6aee8930de5414719771d2bd45f7d1689a6cba5b6a9b226a6af18e09"), (path = "attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0001-P.tsv", sha256 = "d3ee51cf43f3af9d5f5da01dd439b4481af34be5e677bfb8e853965168f6ca00"), (path = "attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0001-Z.tsv", sha256 = "8d996e6af519268f196199ddd8eaa5cf72f4565d6f3e9560957a081d3e870621"), (path = "attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0001-timing.tsv", sha256 = "4a2b576e2c1a90c38f3fc60330a9d4582a9783d41689dd400cfced6c6244a478"), (path = "attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0001.done", sha256 = "02395cb824ac1c8ce1d2e8cd3c57fd19d5c904215aef49b1fba677c51e99d519"), (path = "attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0002-P.tsv", sha256 = "ff9ad50deb948f3e122991f4ff69320f86515f9299b50b0ea5c987f555d1e416"), (path = "attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0002-Z.tsv", sha256 = "0707de70a3edab0d95ef4653d5826789369b92a47a5c184b37d6cc928d56235b"), (path = "attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0002-timing.tsv", sha256 = "a4d2ea803ecfd00a4f71a93e38f17faf9a1da09610de6db19a0874bb2d501de8"), (path = "attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0002.done", sha256 = "84034583a44a97df0ec5820a76fc891b95a9b6bc0bf693bed9882420a3eb5b05"), (path = "attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0003-P.tsv", sha256 = "06fb17367d186daf81c3693349f1bf625f6768c7e4694f5d4bba6f683bb8034f"), (path = "attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0003-Z.tsv", sha256 = "1fe2894addaefb05945108a1d1c4c3cd3cbc9d550c8e038943cff347a711f32f"), (path = "attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0003-timing.tsv", sha256 = "de73b8e8ccb704ec849dd695e0ef633f091f145f3283cd0de524078188a7b92f"), (path = "attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0003.done", sha256 = "aec9dd1bab4102d3c07f9c6abf83e4d604721961d8e37cbbc9620f7f71cafcae"), (path = "attempts/f0004-uOYCzA/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0004-uOYCzA/attempt.json", sha256 = "29cd0d4629d64e4b0ca270d91db8206464fecc2921e59abca914445d5f35911b"), (path = "attempts/f0004-uOYCzA/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0004-uOYCzA/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0004-uOYCzA/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0004-uOYCzA/getdp.log", sha256 = "67d5b9f38e558a64157718198aaad82ec7d4d7c6b4e4e7f9f2cf5c90027cd891"), (path = "attempts/f0004-uOYCzA/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0004-uOYCzA/petsc.log", sha256 = "a31481984a64836823af595cfa092155a310b1b2f31a80493fdd55af054a60c5"), (path = "attempts/f0004-uOYCzA/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0004-uOYCzA/solver.pre", sha256 = "622b234a45ba9054b0346bb8754259116d8b5013c8fed7d602ef8dfce33d9f2b"), (path = "attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0001-P.tsv", sha256 = "d9c7b2db4db87ef5e9ab5d6851f757c9ccfa86c1030ce1621f0bf49650ddfb78"), (path = "attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0001-Z.tsv", sha256 = "74055d09ed213b75641b96a531210d9cd56772f52522fac43e717642c8b1fec9"), (path = "attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0001-timing.tsv", sha256 = "b7c8ea9b5cbf37b7eadd58889748ef24f4df19aa1f36a61448f805c36c77830d"), (path = "attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0001.done", sha256 = "05b52afc2d2a0ffefd0bb6e40585165d1b2572547d59d3c360b4163202709006"), (path = "attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0002-P.tsv", sha256 = "601e72dfc3504f29924d481efc36843de65df1c59391d52458314eb16efe9124"), (path = "attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0002-Z.tsv", sha256 = "7d591d72b116da6bb5b917e70064a74ff7dfb4a14032179f77a138c9eb32b813"), (path = "attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0002-timing.tsv", sha256 = "14e11d0e186bee73caeca39ea46683cec8df473cee9f4b767d77cb856e5a0c0e"), (path = "attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0002.done", sha256 = "35d216e20aed80a36327575b350d8e4c12b70f2038f5d07d55614c92afdd6b6b"), (path = "attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0003-P.tsv", sha256 = "6d07ae7ee95708c5078bfb614504d6df9a2d6a02276e7ff46321b47bbe8a7579"), (path = "attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0003-Z.tsv", sha256 = "8b78acaa7a69e9b289ff78e2d1cee0043520d80a8a97f8b5aade642f6b8d28f8"), (path = "attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0003-timing.tsv", sha256 = "57400f8e43c5962161e395ef7541ec4761a9b34cf8026064554c417137f64184"), (path = "attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0003.done", sha256 = "eae2a23f21e24dd158a817c096e6e77f32e1d2a850c4163d279c3531eef2af5a"), (path = "attempts/f0005-tlXUhL/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0005-tlXUhL/attempt.json", sha256 = "08a6b513d8fc42487f1801a51442960ab06f327def9adad390c2140b3547efa9"), (path = "attempts/f0005-tlXUhL/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0005-tlXUhL/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0005-tlXUhL/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0005-tlXUhL/getdp.log", sha256 = "bc350cd44067250d5de617b517d24269c0f64947c0553bc750aec0f7fb6072d6"), (path = "attempts/f0005-tlXUhL/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0005-tlXUhL/petsc.log", sha256 = "2f7fce5e8e52d6c259c5403f71ae5bc4dcc1d8658f8b3cd0814a17f9b4be54dc"), (path = "attempts/f0005-tlXUhL/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0005-tlXUhL/solver.pre", sha256 = "28fb78402edeb8e400e6c391bf9155ff08eebb2056d9d24ad9500176a9bccc92"), (path = "attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0001-P.tsv", sha256 = "0195d231bec422564610e649f64e7c646f0c40508b789db03e5c9b96af01bfad"), (path = "attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0001-Z.tsv", sha256 = "d59f8d9fac1686a569d277b27f0a0702decd9a2931a64b28cd0d55127cbfd09a"), (path = "attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0001-timing.tsv", sha256 = "54cdacfc5f0b0dc43d04da0f509e6f00035787a94a673001afbf108a09466c9f"), (path = "attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0001.done", sha256 = "ee7bf9802d600055d484ec4e501666418037b219c7bf910eefda35e16a843b08"), (path = "attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0002-P.tsv", sha256 = "0d3a498655da1048906846d62e6168f8805e269122e3a9cf287f60df4f92e7bf"), (path = "attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0002-Z.tsv", sha256 = "3ad7d164f2327fd8d0bfe86a734e779d5a46ca99fedbff01c823fb892090f44b"), (path = "attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0002-timing.tsv", sha256 = "ca6bb49c1c85055742ad1b0dfde706bc99ebc5305682a4906dae1c22087e2b30"), (path = "attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0002.done", sha256 = "e26c3631aaa36c47efb3ede81242002dbebf27057f428fe57f60849cede6b3f3"), (path = "attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0003-P.tsv", sha256 = "393b407d46d6f00ff603cf7c07eaaeca8e3fbb073ee1194e633bd4a7624f8f2c"), (path = "attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0003-Z.tsv", sha256 = "14222044b68256889e1bddf12d1407ab7255fe9422061ab9a53bfe11fabffaae"), (path = "attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0003-timing.tsv", sha256 = "175f49f83cd725e49534494110c4dbc5b9e667f2cada1f81c0916c7238344b91"), (path = "attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0003.done", sha256 = "a91d7ca1bb120ef43c71df371c45a71b1cc7744f1ee2eb082338700de7f329c9"), (path = "attempts/f0006-pGwU3U/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0006-pGwU3U/attempt.json", sha256 = "0f757c5486999602172c295e6db676d048beff5b10c8b560ffd72a64af4651a7"), (path = "attempts/f0006-pGwU3U/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0006-pGwU3U/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0006-pGwU3U/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0006-pGwU3U/getdp.log", sha256 = "91c02bef108ac503067bcff6e08bae0aebabab43e48de09c81e3aba6f88d3cc8"), (path = "attempts/f0006-pGwU3U/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0006-pGwU3U/petsc.log", sha256 = "257aed36362714c9c9677da6f6f0c8b0ec473eede825647cbdcf3aebbfdc100e"), (path = "attempts/f0006-pGwU3U/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0006-pGwU3U/solver.pre", sha256 = "0e83ce495eeb37264b72b1ccfa0e26358e87f410aeb1e3d507925bfb214b8531"), (path = "attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0001-P.tsv", sha256 = "a82402fe73b5a0c4da1a80757377e3ac43e2f50384f039e0a90257c465bf1ea0"), (path = "attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0001-Z.tsv", sha256 = "7957113d319bff3dc7cc96746b79efb3212d09fa4dd35ecde0edf2997606c8ee"), (path = "attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0001-timing.tsv", sha256 = "fff2a4084606e6dad3e90d75c1b270d79a527f43a364260ccd73d309c5d2d233"), (path = "attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0001.done", sha256 = "e41a78288341a2ae156cd28b6102b3a31a74b012eb4deb233e88fae392912e8e"), (path = "attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0002-P.tsv", sha256 = "c2751c83dbc41cedf1067fd0c40986ce6e83ffa8b45ccb0dc5fb935a3d309b68"), (path = "attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0002-Z.tsv", sha256 = "c4486b646ad519626acb5ff787688a63fad719e202ea064fb9693f75ba84c714"), (path = "attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0002-timing.tsv", sha256 = "005557eb52f47fe3ac08bc100bba81039d39ca05df70fb1f13b330efb8a0f43f"), (path = "attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0002.done", sha256 = "c105367e4bac19858725b1b7c56efbb25ae370173d002f9a678a4762a5c69ac9"), (path = "attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0003-P.tsv", sha256 = "a929d1a50d16f50b0ad5f8fb47d66e3ced5f55507e1c90d00af1e0776ba964de"), (path = "attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0003-Z.tsv", sha256 = "fc017ac098faa689d6c9a16c5a7807f1657187a8230fb4a35f6eafdcb6e6327a"), (path = "attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0003-timing.tsv", sha256 = "63d182bf9d710f7eb5314cd327c4ed4b75af5038567071a82a04a69c448b5cd1"), (path = "attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0003.done", sha256 = "de6214d7f2cf6d2097d94d720289a7424da0b9bf7a5df27326d8b69e936798e8"), (path = "attempts/f0007-xOgtG9/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0007-xOgtG9/attempt.json", sha256 = "e1e0e1f0ad201ed0f46362398e7417b388a7af6a1aebed2f9c4ed8e9ee992c08"), (path = "attempts/f0007-xOgtG9/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0007-xOgtG9/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0007-xOgtG9/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0007-xOgtG9/getdp.log", sha256 = "0301504737bf92480a1cce7cb241106fadf95b5b1829c57c64ca15f445bd7aea"), (path = "attempts/f0007-xOgtG9/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0007-xOgtG9/petsc.log", sha256 = "0b4a8d70bbf39a11099e2477a7a6f3ba2c45696abe6d7e72fbe6766486719f45"), (path = "attempts/f0007-xOgtG9/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0007-xOgtG9/solver.pre", sha256 = "6caa5bc65668d1a427339a3dbe1c36af8d776a04cadf3e71310ab40573953ade"), (path = "attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0001-P.tsv", sha256 = "9249112a255c56a6846ecb051da3b3bdf9261538e5ddd23b0d993ffcba378b79"), (path = "attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0001-Z.tsv", sha256 = "d8f3054f6b1bd1d7260f02a51a12fd26b5710f84933cad1f514a4cffa067f19a"), (path = "attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0001-timing.tsv", sha256 = "c8aabe04afcd8da27f4179c975c8a17f329eea77419c0bf41c5028f382957878"), (path = "attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0001.done", sha256 = "d95e4d651bb7fa68e36e0077af5a10e10baacf852173da7ac0f1cca3e414dc7f"), (path = "attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0002-P.tsv", sha256 = "83e7b9bad4a20daf882ea57f0b6522b3d63fae68d8d5a7e88fb741ae114e3706"), (path = "attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0002-Z.tsv", sha256 = "f3e1ef07ed1183eac3cf47374a77010b03dbdcb4553a5bdc87c79c57872483c3"), (path = "attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0002-timing.tsv", sha256 = "4396c8a8a9dc1861c06b951b8a41334eaeb5049c4de36813cedaca7b96faf8d2"), (path = "attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0002.done", sha256 = "a953202777b2aea0282f4e5ecac9feb227f850cb3f80f2247ca07a708f3635be"), (path = "attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0003-P.tsv", sha256 = "b927cd849430d14e21cf567f47a0804d9d509b23fc7d68aa205f8bec6a621a1f"), (path = "attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0003-Z.tsv", sha256 = "6ac3065659e8357f8f93d2951e040771a56d298f65843c3524be433d4dd5697e"), (path = "attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0003-timing.tsv", sha256 = "c97cfa12ecb62cd515d72f0810059f972eb4314e0d54248963fe3dd7e71d0938"), (path = "attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0003.done", sha256 = "bc6e3e671a966700354c8fb7f9ab0ff830db8a4b88332e0ec4974ce29df37b0c"), (path = "attempts/f0008-dar4Cz/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0008-dar4Cz/attempt.json", sha256 = "07965d252f8d03ca2e73b227d9093a1afb27b33ac842e3021d4d630d2651dfad"), (path = "attempts/f0008-dar4Cz/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0008-dar4Cz/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0008-dar4Cz/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0008-dar4Cz/getdp.log", sha256 = "7a22ff739c13e9feaf96f4514328c13fb21c2d338af957497cfa06808cdf7e59"), (path = "attempts/f0008-dar4Cz/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0008-dar4Cz/petsc.log", sha256 = "7b866a6845db0f5f662a784a101f52849e3465d284559a91186213a414df54b8"), (path = "attempts/f0008-dar4Cz/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0008-dar4Cz/solver.pre", sha256 = "f730c362a1c8175df0c750fb8d63445aa6f6bb66d5711bd24fa0d41a669d28dd"), (path = "attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0001-P.tsv", sha256 = "1b5071aa6ae6390e09800c28347da82417b0fa2b2a3d61db90f9bd477231f789"), (path = "attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0001-Z.tsv", sha256 = "dd382d525223d354279aa84f7459491d9b81c00eee3c9dbab10fb7bc8e84aea8"), (path = "attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0001-timing.tsv", sha256 = "56dfd2998d962fef93128299ee10ce52cf45f7a69a844f07b7e729ad51a703d7"), (path = "attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0001.done", sha256 = "c03ca6db0131d3218478fb4e85f0350dca26787b9ffdfc9d50b5d68ad93b352c"), (path = "attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0002-P.tsv", sha256 = "6e1068ab50509cc6ccc88ffa58da5125be06c744248ebd787f1a725a0d3ce19c"), (path = "attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0002-Z.tsv", sha256 = "3219910dbd6b5b68a5f56630becc77e2427ba31e965fdcd3f3b2ba9d583eebfb"), (path = "attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0002-timing.tsv", sha256 = "f645e39ee1dae92a40de8945d6854c29ff576aa4b90375f550aa3ef8d2175845"), (path = "attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0002.done", sha256 = "76f50b528daca79ab1eac47c47f96ca1e7b8ad94bcaefbaac04335941306f993"), (path = "attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0003-P.tsv", sha256 = "e271103adf21200c4f34c3c7e3ad80bb11042de627738f0ba14defd006942a15"), (path = "attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0003-Z.tsv", sha256 = "fef20b3ea2c6e03fbcd2ceb6acfdeaf0d3326e1e51e06f9762cb54d4acbdeb13"), (path = "attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0003-timing.tsv", sha256 = "ad93931de6d8f1ca6e4481e29ba4fad116814b7a9f699670c846c22eb78ee814"), (path = "attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0003.done", sha256 = "20bdbe7d9351850f55ebf49a31f3a93235d04aea44253ddd2d75828a83fa1c74"), (path = "attempts/f0009-qFY1kw/attempt.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0009-qFY1kw/attempt.json", sha256 = "775ff0b0e5c1eb4d2f654d4280b0d42a2a3f6c4990c04338459df06114154c04"), (path = "attempts/f0009-qFY1kw/bases.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0009-qFY1kw/bases.pro", sha256 = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636"), (path = "attempts/f0009-qFY1kw/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0009-qFY1kw/getdp.log", sha256 = "5553c64fa4c0e06fdf8eaa5b91fc5f8090edbaf2b18b6f288d85b53234ba8376"), (path = "attempts/f0009-qFY1kw/petsc.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0009-qFY1kw/petsc.log", sha256 = "b8581b22ad0079f8bc1d298b5e49278baa7b84f3b0725965b56a977d98dc1fdd"), (path = "attempts/f0009-qFY1kw/solver.pre", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0009-qFY1kw/solver.pre", sha256 = "9da97cdf74ca6b768ed4b91afe309520e44ca84b43c41f4ac153971e3060556f"), (path = "attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0001-P.tsv", sha256 = "2ee20b18c0c855a857ffa64a61d5ebbd7d0834abca56824221744b5b5f1f250d"), (path = "attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0001-Z.tsv", sha256 = "24b9d2147f2172fbff8181910ad95a079f6c67a849e4f1e2eb9c3622470a7a94"), (path = "attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0001-timing.tsv", sha256 = "2f10ee9aee2eeac69d6207e568e50d41a962f4560fe2d324b94c4dad48b73d14"), (path = "attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0001.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0001.done", sha256 = "22b8b831af1067ac03eaf9570ddd71235af29f6b65e559199200376f6ef90f2a"), (path = "attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0002-P.tsv", sha256 = "a72b35bdb650f4fa23879e9b7e993657d19ac85eafce3ff14e40071f68284442"), (path = "attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0002-Z.tsv", sha256 = "1b6dd8be9a57ce679e09d8f852dc5a9d659fdc25a7c6af41333f148c6640571f"), (path = "attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0002-timing.tsv", sha256 = "537a16539c2d71dce3ce71ec3e3870e8d04b9c12928603f2e02fa221a3983e19"), (path = "attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0002.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0002.done", sha256 = "2ab6b278377a8a5090d5d456a9193028844db99b8f3fa96c2433d70920e73c22"), (path = "attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0003-P.tsv", sha256 = "cf0156d87713c607ff736097e3939e4caf80c742f6bef29e21f3684dd14a16be"), (path = "attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0003-Z.tsv", sha256 = "1141e3b846e0407089025c2310354fd992ee98592d76645c8547011189ead1f3"), (path = "attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0003-timing.tsv", sha256 = "495a2a6840774175d78d9817a2f55ffcedc82da1a9b00fcb860a071f0669521f"), (path = "attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0003.done", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0003.done", sha256 = "3449a1ca026b39cca413695b5bd20e176addf6bcf3103f9147e8ac56407cafa3"), (path = "input/computation.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/input/computation.json", sha256 = "b48e71939cea59c3075535510a2a2ee254158e6bab1f1ed172159f459085eebe"), (path = "input/model_data.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/input/model_data.pro", sha256 = "83f1e7217a46f45f5f12a55c73795fdf4d90552c5ad57e9dd579cd69ff6a50f6"), (path = "input/problem.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/input/problem.json", sha256 = "fb186a621d0ec64b232f902e99f57f242ae48625abbbfe95a2eda97664742ac9"), (path = "input/getdp/integration.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/input/getdp/integration.pro", sha256 = "e113951ac1e6e658f1be50e53875e30c85497445f0e6584a8a7ab1848588e902"), (path = "input/getdp/jacobian.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/input/getdp/jacobian.pro", sha256 = "014208d39043ab13183c42c312aba459265715da6d6ddc38ec1fbc39b6090c73"), (path = "input/getdp/materials.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/input/getdp/materials.pro", sha256 = "581f089113810a379d8a49c03d79f55045b723c8f4c761d0a493a019ff1f5d9e"), (path = "input/getdp/model.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/input/getdp/model.pro", sha256 = "c0bb424db5ab4db9ca22f549f675edb4646f69910d2aa14e20cf4f40e41bd313"), (path = "input/getdp/quasi-full.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/input/getdp/quasi-full.pro", sha256 = "1aede35114ea686c5f109089478027d241dec12dc9fd14a24452b348f89b08e4"), (path = "input/getdp/quasi-tem.pro", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/input/getdp/quasi-tem.pro", sha256 = "448d9019ad417c9a784da20170b855dd25e559297499a83d19b007ee095c084d"), (path = "logs/getdp.log", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/logs/getdp.log", sha256 = "d38123045757e16c6d99756fefa5fb9e236f45e526b4bb7c314aba55d0ba1722"), (path = "mesh/frequency_0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/mesh/frequency_0001.json", sha256 = "48dd98f6dfe7fc4e7d874e41aad3bc1d2afba1a9946d12d0885eb8c60311116f"), (path = "mesh/frequency_0001.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/mesh/frequency_0001.msh", sha256 = "199709c5a52023bfe64ced1f53a8ddf86809ab05c516751dfdd34c0787c889bd"), (path = "mesh/frequency_0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/mesh/frequency_0002.json", sha256 = "1bf682e97aade85744ea807d2e4617f0238ab30e54c685e39c6597e4c64fd322"), (path = "mesh/frequency_0002.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/mesh/frequency_0002.msh", sha256 = "7812570d87b9321ae1bb67b72c250c0c1f4bb1a078ced2690bd1e281fa246011"), (path = "mesh/frequency_0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/mesh/frequency_0003.json", sha256 = "6483df14df08244848adcc8bbe549eaead7bc0b0475ff1400e351d79eb423e28"), (path = "mesh/frequency_0003.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/mesh/frequency_0003.msh", sha256 = "e9918131636504e7fa4817a36bc87a7d68c896f120cd27a604e32dabe5a9be5a"), (path = "mesh/frequency_0004.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/mesh/frequency_0004.json", sha256 = "df309d892ee51b1f39e2e18a82cc263b971da57aa702bbdd6e9fcb2710e8f816"), (path = "mesh/frequency_0004.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/mesh/frequency_0004.msh", sha256 = "fc47ed5f3fa6e825426cac5b9d2fe2c58bd8868e52e440e9ec1e7140a613699e"), (path = "mesh/frequency_0005.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/mesh/frequency_0005.json", sha256 = "e5c8c15308b560c7c6ef57741a28997589a562d35366c1333ec2a6ee81e4e8e1"), (path = "mesh/frequency_0005.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/mesh/frequency_0005.msh", sha256 = "209b6569356f7c621ff436762f1a1fbc58e49488e841a7b70645a4f0a4fd68b2"), (path = "mesh/frequency_0006.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/mesh/frequency_0006.json", sha256 = "de86d60fadd372e654fff85ee551351146d2f997b8fab07593196cc98dbce883"), (path = "mesh/frequency_0006.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/mesh/frequency_0006.msh", sha256 = "e1f4eb09593eab4c6445e0739bb718aaa2cee30b5ec4fb59805e6906d43ad183"), (path = "mesh/frequency_0007.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/mesh/frequency_0007.json", sha256 = "6f78ea55382e107a5a715fc51e758f787be0039b5d2f8272849c1a9bd1315791"), (path = "mesh/frequency_0007.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/mesh/frequency_0007.msh", sha256 = "247c7ddef1b64805899c4ebdf91e40f40e56b9412d054545f16875505ca501fe"), (path = "mesh/frequency_0008.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/mesh/frequency_0008.json", sha256 = "02287cc9a839718f85da9fc7560140da9a90037b09501aa451d3f04f83113052"), (path = "mesh/frequency_0008.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/mesh/frequency_0008.msh", sha256 = "945d16cac2cf148e02dc0fb16e072a0a7c85395d005d869fe002b2dbe26d270e"), (path = "mesh/model.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/mesh/model.json", sha256 = "fce94f26e50383d76251afaa1959b21d7976e436bd366a1cd08429bc0098134c"), (path = "mesh/model.msh", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/mesh/model.msh", sha256 = "54c0c560a95ea9bf6f7aee2944791c8b1c23c13adb7080004e7d4ad6a19eeecd"), (path = "raw/P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/P.tsv", sha256 = "959d8cd451e4797fd2471b8832f0c2805d1548a5ef7fe4ae51ea9e138bdc61c6"), (path = "raw/Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/Z.tsv", sha256 = "3de44131cb37d91434ba75874a929db6e370119a35fc63b66ee7ead13b25cc30"), (path = "raw/checksums.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/checksums.json", sha256 = "70d05414763c6def29e00f8f9d8d7ce3f0ca431b070778dddf8cdb864fd6ca39"), (path = "raw/scan_complete.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/scan_complete.tsv", sha256 = "2cc9eff50bb7e6fff0e2555f96d93ebe952c4a1a158a1dca82fe6cca3aa1afab"), (path = "raw/jobs/getdp-f0001-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0001-b0001-P.tsv", sha256 = "04c9e6b2c890764d1fe53b5595feab56c4b9964ad522d22157758fe08511d41e"), (path = "raw/jobs/getdp-f0001-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0001-b0001-Z.tsv", sha256 = "ea6e5ec751ce81e04b81e925b9eff27bab7f148fc641aef12ae784e589a0d558"), (path = "raw/jobs/getdp-f0001-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0001-b0001-timing.tsv", sha256 = "b4d24bfdc6ff3d55fc16b1f6131e1aef94d8b0815edd8029a6f9172566a692bd"), (path = "raw/jobs/getdp-f0001-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0001-b0001.json", sha256 = "c51c5bd457a9d230693de66ed8d8947bdf7e0ffe6efee8059ae6f141f0ce7c80"), (path = "raw/jobs/getdp-f0001-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0001-b0002-P.tsv", sha256 = "58b5ac96646f67a8a9d68027ed7e65e00aaa7ccdec213ad9721e91001d305633"), (path = "raw/jobs/getdp-f0001-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0001-b0002-Z.tsv", sha256 = "1900dfd1f89bc54d88574665eee50adcd1e71e12caf41623d8b182074eda2916"), (path = "raw/jobs/getdp-f0001-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0001-b0002-timing.tsv", sha256 = "2296ec408a6bf04d984403a0256e1c18e167bbc7944d628d66da1d90a7fbb23e"), (path = "raw/jobs/getdp-f0001-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0001-b0002.json", sha256 = "5e3e8965d1ca74c62e0b8a1dcc14fabd2540a6bb6e7591bdd21240ec7ca74bf7"), (path = "raw/jobs/getdp-f0001-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0001-b0003-P.tsv", sha256 = "d1d337ca87a70e6d469f4530db9baa306c88627441482c75760e247c5087e00a"), (path = "raw/jobs/getdp-f0001-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0001-b0003-Z.tsv", sha256 = "37a39f817bcd20c901f7efdf9e9870f3e2fc0364c9b6a7a82b347f92140eab27"), (path = "raw/jobs/getdp-f0001-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0001-b0003-timing.tsv", sha256 = "ffdfd1faec39eef91c74586e5aa85cb800f3c7941340a4c4182c9904993a13f3"), (path = "raw/jobs/getdp-f0001-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0001-b0003.json", sha256 = "83b8faa92f6badd7bb3cc0f6f3e0aba95db234386f3b2d8a5cf51d409a6f42da"), (path = "raw/jobs/getdp-f0002-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0002-b0001-P.tsv", sha256 = "a2f141d70f0dd2d524a23219f83cfd6b6c6fbc8b4f98ce1cfe7a0048aa180f22"), (path = "raw/jobs/getdp-f0002-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0002-b0001-Z.tsv", sha256 = "ed761307661a09df2f10d0b6d7629be3fbd7b5d18a535a689a617105a8c89806"), (path = "raw/jobs/getdp-f0002-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0002-b0001-timing.tsv", sha256 = "e0ab4f02f2e74b89d0be68e501df864d67415c680b768402a4c81a32ee6f1cc6"), (path = "raw/jobs/getdp-f0002-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0002-b0001.json", sha256 = "a8f8c7964d9183df28771f084b6fadbebade1b0cef64fdbe6c97c76c69c5f49b"), (path = "raw/jobs/getdp-f0002-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0002-b0002-P.tsv", sha256 = "acb7239febeba6f0b026459ab2b13d63ff3c1a1b9857d02e0f3e07b7d991d69f"), (path = "raw/jobs/getdp-f0002-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0002-b0002-Z.tsv", sha256 = "f34bce613e012e1fecab9d0ee72589bd857a9747198349cf5c3e73f56060a138"), (path = "raw/jobs/getdp-f0002-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0002-b0002-timing.tsv", sha256 = "abafdd5f0b8dcdf79b68284c06d318fc87089e7492d5eb6ef3386cea1023a447"), (path = "raw/jobs/getdp-f0002-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0002-b0002.json", sha256 = "360a0bbbc22926e01b3f3b535a0f9abec824a38a45143e26cf19956e18bceb48"), (path = "raw/jobs/getdp-f0002-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0002-b0003-P.tsv", sha256 = "25e7131c3d0de464fcb954e57953730133762473caa9f21c03cd809814dc1be8"), (path = "raw/jobs/getdp-f0002-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0002-b0003-Z.tsv", sha256 = "0341d1a792571476890702a3abdd764109ca9c72c953260e9df965d1df93fa74"), (path = "raw/jobs/getdp-f0002-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0002-b0003-timing.tsv", sha256 = "837caea517bfd3000c718be9bcd3c264a537dbeac0929f62dd0653cc70f6acdc"), (path = "raw/jobs/getdp-f0002-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0002-b0003.json", sha256 = "e539d7651baf4aa5b8fdd1c85da428a88f48e0751300badc5d6abb6491dadc1d"), (path = "raw/jobs/getdp-f0003-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0003-b0001-P.tsv", sha256 = "d3ee51cf43f3af9d5f5da01dd439b4481af34be5e677bfb8e853965168f6ca00"), (path = "raw/jobs/getdp-f0003-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0003-b0001-Z.tsv", sha256 = "8d996e6af519268f196199ddd8eaa5cf72f4565d6f3e9560957a081d3e870621"), (path = "raw/jobs/getdp-f0003-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0003-b0001-timing.tsv", sha256 = "4a2b576e2c1a90c38f3fc60330a9d4582a9783d41689dd400cfced6c6244a478"), (path = "raw/jobs/getdp-f0003-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0003-b0001.json", sha256 = "6c4e4658b6321160e1dc3c6049ccc90855f0282995ccb03124f1dac8a5da60de"), (path = "raw/jobs/getdp-f0003-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0003-b0002-P.tsv", sha256 = "ff9ad50deb948f3e122991f4ff69320f86515f9299b50b0ea5c987f555d1e416"), (path = "raw/jobs/getdp-f0003-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0003-b0002-Z.tsv", sha256 = "0707de70a3edab0d95ef4653d5826789369b92a47a5c184b37d6cc928d56235b"), (path = "raw/jobs/getdp-f0003-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0003-b0002-timing.tsv", sha256 = "a4d2ea803ecfd00a4f71a93e38f17faf9a1da09610de6db19a0874bb2d501de8"), (path = "raw/jobs/getdp-f0003-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0003-b0002.json", sha256 = "21f5b525cc6381c4496e1492509dcf70b028cd29e435e5963e896a97bfefd29d"), (path = "raw/jobs/getdp-f0003-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0003-b0003-P.tsv", sha256 = "06fb17367d186daf81c3693349f1bf625f6768c7e4694f5d4bba6f683bb8034f"), (path = "raw/jobs/getdp-f0003-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0003-b0003-Z.tsv", sha256 = "1fe2894addaefb05945108a1d1c4c3cd3cbc9d550c8e038943cff347a711f32f"), (path = "raw/jobs/getdp-f0003-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0003-b0003-timing.tsv", sha256 = "de73b8e8ccb704ec849dd695e0ef633f091f145f3283cd0de524078188a7b92f"), (path = "raw/jobs/getdp-f0003-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0003-b0003.json", sha256 = "d8613efe09efd4c6a3d25851b86acc107fb983227e09ed3b851f6a0ffcb6cdbc"), (path = "raw/jobs/getdp-f0004-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0004-b0001-P.tsv", sha256 = "d9c7b2db4db87ef5e9ab5d6851f757c9ccfa86c1030ce1621f0bf49650ddfb78"), (path = "raw/jobs/getdp-f0004-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0004-b0001-Z.tsv", sha256 = "74055d09ed213b75641b96a531210d9cd56772f52522fac43e717642c8b1fec9"), (path = "raw/jobs/getdp-f0004-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0004-b0001-timing.tsv", sha256 = "b7c8ea9b5cbf37b7eadd58889748ef24f4df19aa1f36a61448f805c36c77830d"), (path = "raw/jobs/getdp-f0004-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0004-b0001.json", sha256 = "160fa536493c0c1d866a705675a67c255c94f46d77f25e533068b8492f0817b8"), (path = "raw/jobs/getdp-f0004-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0004-b0002-P.tsv", sha256 = "601e72dfc3504f29924d481efc36843de65df1c59391d52458314eb16efe9124"), (path = "raw/jobs/getdp-f0004-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0004-b0002-Z.tsv", sha256 = "7d591d72b116da6bb5b917e70064a74ff7dfb4a14032179f77a138c9eb32b813"), (path = "raw/jobs/getdp-f0004-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0004-b0002-timing.tsv", sha256 = "14e11d0e186bee73caeca39ea46683cec8df473cee9f4b767d77cb856e5a0c0e"), (path = "raw/jobs/getdp-f0004-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0004-b0002.json", sha256 = "215545b42852933a185884dfbfb05ff23fd988a29a15544155649cc10d55f486"), (path = "raw/jobs/getdp-f0004-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0004-b0003-P.tsv", sha256 = "6d07ae7ee95708c5078bfb614504d6df9a2d6a02276e7ff46321b47bbe8a7579"), (path = "raw/jobs/getdp-f0004-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0004-b0003-Z.tsv", sha256 = "8b78acaa7a69e9b289ff78e2d1cee0043520d80a8a97f8b5aade642f6b8d28f8"), (path = "raw/jobs/getdp-f0004-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0004-b0003-timing.tsv", sha256 = "57400f8e43c5962161e395ef7541ec4761a9b34cf8026064554c417137f64184"), (path = "raw/jobs/getdp-f0004-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0004-b0003.json", sha256 = "aec80eee1c448a39a12ce4580d4f63c64a441f6ef18a368145ddf29476829ef0"), (path = "raw/jobs/getdp-f0005-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0005-b0001-P.tsv", sha256 = "0195d231bec422564610e649f64e7c646f0c40508b789db03e5c9b96af01bfad"), (path = "raw/jobs/getdp-f0005-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0005-b0001-Z.tsv", sha256 = "d59f8d9fac1686a569d277b27f0a0702decd9a2931a64b28cd0d55127cbfd09a"), (path = "raw/jobs/getdp-f0005-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0005-b0001-timing.tsv", sha256 = "54cdacfc5f0b0dc43d04da0f509e6f00035787a94a673001afbf108a09466c9f"), (path = "raw/jobs/getdp-f0005-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0005-b0001.json", sha256 = "8016e286965e2cc2fd7fa1ed116f1ecc54a8cfcbe852eb90f055da798a5d9666"), (path = "raw/jobs/getdp-f0005-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0005-b0002-P.tsv", sha256 = "0d3a498655da1048906846d62e6168f8805e269122e3a9cf287f60df4f92e7bf"), (path = "raw/jobs/getdp-f0005-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0005-b0002-Z.tsv", sha256 = "3ad7d164f2327fd8d0bfe86a734e779d5a46ca99fedbff01c823fb892090f44b"), (path = "raw/jobs/getdp-f0005-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0005-b0002-timing.tsv", sha256 = "ca6bb49c1c85055742ad1b0dfde706bc99ebc5305682a4906dae1c22087e2b30"), (path = "raw/jobs/getdp-f0005-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0005-b0002.json", sha256 = "2d3980e110ff2d1d2c84b6f67a91fa3c1efc6f07d4ed9c202d3c8030e6ad0639"), (path = "raw/jobs/getdp-f0005-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0005-b0003-P.tsv", sha256 = "393b407d46d6f00ff603cf7c07eaaeca8e3fbb073ee1194e633bd4a7624f8f2c"), (path = "raw/jobs/getdp-f0005-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0005-b0003-Z.tsv", sha256 = "14222044b68256889e1bddf12d1407ab7255fe9422061ab9a53bfe11fabffaae"), (path = "raw/jobs/getdp-f0005-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0005-b0003-timing.tsv", sha256 = "175f49f83cd725e49534494110c4dbc5b9e667f2cada1f81c0916c7238344b91"), (path = "raw/jobs/getdp-f0005-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0005-b0003.json", sha256 = "359965d2fbe026801921926b0adb10c0d6138746a2b4e272b6eba59e5d71db28"), (path = "raw/jobs/getdp-f0006-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0006-b0001-P.tsv", sha256 = "a82402fe73b5a0c4da1a80757377e3ac43e2f50384f039e0a90257c465bf1ea0"), (path = "raw/jobs/getdp-f0006-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0006-b0001-Z.tsv", sha256 = "7957113d319bff3dc7cc96746b79efb3212d09fa4dd35ecde0edf2997606c8ee"), (path = "raw/jobs/getdp-f0006-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0006-b0001-timing.tsv", sha256 = "fff2a4084606e6dad3e90d75c1b270d79a527f43a364260ccd73d309c5d2d233"), (path = "raw/jobs/getdp-f0006-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0006-b0001.json", sha256 = "e6e5ac8b4c99e1f99db84104f58c226e3e25f21b3abd3cf24b60c711c2649ac3"), (path = "raw/jobs/getdp-f0006-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0006-b0002-P.tsv", sha256 = "c2751c83dbc41cedf1067fd0c40986ce6e83ffa8b45ccb0dc5fb935a3d309b68"), (path = "raw/jobs/getdp-f0006-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0006-b0002-Z.tsv", sha256 = "c4486b646ad519626acb5ff787688a63fad719e202ea064fb9693f75ba84c714"), (path = "raw/jobs/getdp-f0006-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0006-b0002-timing.tsv", sha256 = "005557eb52f47fe3ac08bc100bba81039d39ca05df70fb1f13b330efb8a0f43f"), (path = "raw/jobs/getdp-f0006-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0006-b0002.json", sha256 = "a5ee955bc4d206343ba929782d22263c7bc10154a2fe8b9e987c0a0a4c3a280e"), (path = "raw/jobs/getdp-f0006-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0006-b0003-P.tsv", sha256 = "a929d1a50d16f50b0ad5f8fb47d66e3ced5f55507e1c90d00af1e0776ba964de"), (path = "raw/jobs/getdp-f0006-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0006-b0003-Z.tsv", sha256 = "fc017ac098faa689d6c9a16c5a7807f1657187a8230fb4a35f6eafdcb6e6327a"), (path = "raw/jobs/getdp-f0006-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0006-b0003-timing.tsv", sha256 = "63d182bf9d710f7eb5314cd327c4ed4b75af5038567071a82a04a69c448b5cd1"), (path = "raw/jobs/getdp-f0006-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0006-b0003.json", sha256 = "3e214a283723e55e7af11c3aba48967a0927e6afd0eee5d2f51dd00bba37d923"), (path = "raw/jobs/getdp-f0007-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0007-b0001-P.tsv", sha256 = "9249112a255c56a6846ecb051da3b3bdf9261538e5ddd23b0d993ffcba378b79"), (path = "raw/jobs/getdp-f0007-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0007-b0001-Z.tsv", sha256 = "d8f3054f6b1bd1d7260f02a51a12fd26b5710f84933cad1f514a4cffa067f19a"), (path = "raw/jobs/getdp-f0007-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0007-b0001-timing.tsv", sha256 = "c8aabe04afcd8da27f4179c975c8a17f329eea77419c0bf41c5028f382957878"), (path = "raw/jobs/getdp-f0007-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0007-b0001.json", sha256 = "6e7255066615a624e9424a6200ee05416d3f2d0bcd026b735c5ec1d3dc51c44e"), (path = "raw/jobs/getdp-f0007-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0007-b0002-P.tsv", sha256 = "83e7b9bad4a20daf882ea57f0b6522b3d63fae68d8d5a7e88fb741ae114e3706"), (path = "raw/jobs/getdp-f0007-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0007-b0002-Z.tsv", sha256 = "f3e1ef07ed1183eac3cf47374a77010b03dbdcb4553a5bdc87c79c57872483c3"), (path = "raw/jobs/getdp-f0007-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0007-b0002-timing.tsv", sha256 = "4396c8a8a9dc1861c06b951b8a41334eaeb5049c4de36813cedaca7b96faf8d2"), (path = "raw/jobs/getdp-f0007-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0007-b0002.json", sha256 = "9906a5fd78a06f014e1d719c76d34d321d6e11f21b694df92cf9878a10cdeda1"), (path = "raw/jobs/getdp-f0007-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0007-b0003-P.tsv", sha256 = "b927cd849430d14e21cf567f47a0804d9d509b23fc7d68aa205f8bec6a621a1f"), (path = "raw/jobs/getdp-f0007-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0007-b0003-Z.tsv", sha256 = "6ac3065659e8357f8f93d2951e040771a56d298f65843c3524be433d4dd5697e"), (path = "raw/jobs/getdp-f0007-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0007-b0003-timing.tsv", sha256 = "c97cfa12ecb62cd515d72f0810059f972eb4314e0d54248963fe3dd7e71d0938"), (path = "raw/jobs/getdp-f0007-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0007-b0003.json", sha256 = "163aec47d56a4c50a781fd284e0cb3f5c492be6c4a04f98efcccc42f5403c539"), (path = "raw/jobs/getdp-f0008-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0008-b0001-P.tsv", sha256 = "1b5071aa6ae6390e09800c28347da82417b0fa2b2a3d61db90f9bd477231f789"), (path = "raw/jobs/getdp-f0008-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0008-b0001-Z.tsv", sha256 = "dd382d525223d354279aa84f7459491d9b81c00eee3c9dbab10fb7bc8e84aea8"), (path = "raw/jobs/getdp-f0008-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0008-b0001-timing.tsv", sha256 = "56dfd2998d962fef93128299ee10ce52cf45f7a69a844f07b7e729ad51a703d7"), (path = "raw/jobs/getdp-f0008-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0008-b0001.json", sha256 = "2f4e8d6fb58e82c92921936bdc955886a0bfb273ec52f00b0b97bc4846414f59"), (path = "raw/jobs/getdp-f0008-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0008-b0002-P.tsv", sha256 = "6e1068ab50509cc6ccc88ffa58da5125be06c744248ebd787f1a725a0d3ce19c"), (path = "raw/jobs/getdp-f0008-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0008-b0002-Z.tsv", sha256 = "3219910dbd6b5b68a5f56630becc77e2427ba31e965fdcd3f3b2ba9d583eebfb"), (path = "raw/jobs/getdp-f0008-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0008-b0002-timing.tsv", sha256 = "f645e39ee1dae92a40de8945d6854c29ff576aa4b90375f550aa3ef8d2175845"), (path = "raw/jobs/getdp-f0008-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0008-b0002.json", sha256 = "bd0bde2bddeafb1415a2d8b54d22f51b18e464c0228affc00dd84d5a0f21a797"), (path = "raw/jobs/getdp-f0008-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0008-b0003-P.tsv", sha256 = "e271103adf21200c4f34c3c7e3ad80bb11042de627738f0ba14defd006942a15"), (path = "raw/jobs/getdp-f0008-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0008-b0003-Z.tsv", sha256 = "fef20b3ea2c6e03fbcd2ceb6acfdeaf0d3326e1e51e06f9762cb54d4acbdeb13"), (path = "raw/jobs/getdp-f0008-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0008-b0003-timing.tsv", sha256 = "ad93931de6d8f1ca6e4481e29ba4fad116814b7a9f699670c846c22eb78ee814"), (path = "raw/jobs/getdp-f0008-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0008-b0003.json", sha256 = "98efee8fe2cf64ae86eb6e2558bafd0346d4f74968178030a4fd103b96d58655"), (path = "raw/jobs/getdp-f0009-b0001-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0009-b0001-P.tsv", sha256 = "2ee20b18c0c855a857ffa64a61d5ebbd7d0834abca56824221744b5b5f1f250d"), (path = "raw/jobs/getdp-f0009-b0001-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0009-b0001-Z.tsv", sha256 = "24b9d2147f2172fbff8181910ad95a079f6c67a849e4f1e2eb9c3622470a7a94"), (path = "raw/jobs/getdp-f0009-b0001-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0009-b0001-timing.tsv", sha256 = "2f10ee9aee2eeac69d6207e568e50d41a962f4560fe2d324b94c4dad48b73d14"), (path = "raw/jobs/getdp-f0009-b0001.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0009-b0001.json", sha256 = "b8721c75a6b4125e1f8360f191f957461dfa7a8ad74eeebd35713f68278fc954"), (path = "raw/jobs/getdp-f0009-b0002-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0009-b0002-P.tsv", sha256 = "a72b35bdb650f4fa23879e9b7e993657d19ac85eafce3ff14e40071f68284442"), (path = "raw/jobs/getdp-f0009-b0002-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0009-b0002-Z.tsv", sha256 = "1b6dd8be9a57ce679e09d8f852dc5a9d659fdc25a7c6af41333f148c6640571f"), (path = "raw/jobs/getdp-f0009-b0002-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0009-b0002-timing.tsv", sha256 = "537a16539c2d71dce3ce71ec3e3870e8d04b9c12928603f2e02fa221a3983e19"), (path = "raw/jobs/getdp-f0009-b0002.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0009-b0002.json", sha256 = "eb22032e15510f33863b93a4449c035d02839d65820f38b1bb19689bc6b94654"), (path = "raw/jobs/getdp-f0009-b0003-P.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0009-b0003-P.tsv", sha256 = "cf0156d87713c607ff736097e3939e4caf80c742f6bef29e21f3684dd14a16be"), (path = "raw/jobs/getdp-f0009-b0003-Z.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0009-b0003-Z.tsv", sha256 = "1141e3b846e0407089025c2310354fd992ee98592d76645c8547011189ead1f3"), (path = "raw/jobs/getdp-f0009-b0003-timing.tsv", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0009-b0003-timing.tsv", sha256 = "495a2a6840774175d78d9817a2f55ffcedc82da1a9b00fcb860a071f0669521f"), (path = "raw/jobs/getdp-f0009-b0003.json", source = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1/raw/jobs/getdp-f0009-b0003.json", sha256 = "a22541ba0984d39981f6f32bbb28113cfdabf01b0f8e074d2f96ff43804bf028")], coordinates = ["cable:1:core", "cable:2:core", "cable:3:core"], formulations = @NamedTuple{schema_version::Int64, selections::@NamedTuple{insulation_admittance::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}, semicon_admittance::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}, earth_properties::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}, temperature_dependence::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}}, assumptions::@NamedTuple{impedance::String, admittance::String, earth::String, propagation::String, semicon_domain::String, enclosure::String}, backend::Symbol, requested::NamedTuple, methods::NamedTuple, options::NamedTuple}((4, @NamedTuple{insulation_admittance::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}, semicon_admittance::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}, earth_properties::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}, temperature_dependence::Union{Nothing, @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}}}((@NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}((:lossless, NamedTuple(), NamedTuple())), @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}((:lossless, NamedTuple(), NamedTuple())), @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}((:constant, NamedTuple(), NamedTuple())), @NamedTuple{identifier::Symbol, parameters::NamedTuple, options::NamedTuple}((:linear, NamedTuple(), NamedTuple())))), (impedance = "Axial current-driven A_z/u_r finite-element equations", admittance = "Scalar electrodynamic Helmholtz equation in surrounding media; equipotential terminals with unit transverse-current excitation; Y = inv(P)", earth = "Horizontal air and one semi-infinite soil; soil constitutive properties evaluated at each frequency", propagation = "Gamma = 0; medium diffusion and displacement retained; independent Z/P blocks in one factorization", semicon_domain = "Passive material region, without electrical terminal ownership", enclosure = "Supported enclosures are represented by their material and terminal domains"), :fem, (insulation_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), semicon_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_properties = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), temperature_dependence = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing)), (insulation_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), semicon_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), earth_properties = (identifier = :constant, parameters = NamedTuple(), options = NamedTuple()), temperature_dependence = (identifier = :linear, parameters = NamedTuple(), options = NamedTuple())), (reduce_bundle = false, kron_reduction = false, ideal_transposition = false, physics = Symbol("quasi-tem")))), fem = (timing = (assembly_seconds = 11.814276695251465, recovered_columns = 0, scope = "accumulated native worker wall time; not elapsed scan time", columns = 27, worker_wall_seconds = 32.726472757, factorized_columns = 9, constraint_seconds = 4.249899387359619, solve_seconds = 12.039140701293945, output_seconds = 0.007434368133544922, worker_wall_scope = "sum of newly executed process wall durations in this invocation", backend = "getdp", schema = 1, reused = false), run = (state = LineCableModelsGmshExt.completed, run_directory = "/home/amartins/Documents/KUL/LineCableModels/.linecablemodels/fem/runs/run-fEugm1", mesh_source = :generated, mesh_fingerprint = "0f11ac95caed7b6155ebb1e4f93caf68d8a088ef918ed599b0f3b13f828fb123", getdp_invocations = 9, map_paths = String[], completed_columns = 27, completed_frequencies = 9), inputs = (schema_version = 7, solver_protocol = 3, mesh_fingerprint = "b895425808d352288d25b0b81847767f784fb934dee3e7c5e58b0793cea74aaa", materials = [(kind = :conductor, tag = 10001, mu_r = 0.999994, sigma = [5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7, 5.8001276028072625e7], omega_epsilon = [5.563250277239352e-12, 5.563250277239352e-11, 5.563250277239352e-10, 5.563250277239352e-9, 5.5632502772393515e-8, 5.563250277239353e-7, 5.563250277239352e-6, 5.563250277239352e-5, 0.0005563250277239352])], earth_materials = [(rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0), (rho = 0.1, eps_r = 1.0, mu_r = 1.0)], air = (eps_r = 1.0, mu_r = 1.0), mesh_plans = LineCableModelsGmshExt.FEMMeshPlan{Float64}[LineCableModelsGmshExt.FEMMeshPlan{Float64}(1, 0.1, 1006.5842420897408, 1258.230302612176, 25.16460605224352, 50.32921210448704, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (25.16460605224352, 25.16460605224352), (1006.5842420897408, 1006.5842420897408)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(2, 1.0, 318.3098861837907, 397.88735772973837, 7.957747154594768, 15.915494309189535, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (7.957747154594768, 7.957747154594768), (318.3098861837907, 318.3098861837907)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(3, 10.0, 100.65842420897408, 125.82303026121761, 2.516460605224352, 5.032921210448704, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (2.516460605224352, 2.516460605224352), (100.65842420897408, 100.65842420897408)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(4, 100.0, 31.83098861837907, 39.78873577297384, 0.7957747154594768, 1.5915494309189535, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (0.7957747154594768, 0.7957747154594768), (31.83098861837907, 31.83098861837907)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(5, 1000.0, 10.06584242089741, 12.582303026121762, 0.2516460605224352, 0.5032921210448704, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (0.2516460605224352, 0.2516460605224352), (10.06584242089741, 10.06584242089741)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(6, 10000.0, 5.0, 6.25, 0.25, 0.5, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (0.25, 0.14067442439954772), (10.0, 9.549296851139358)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(7, 100000.0, 5.0, 6.25, 0.25, 0.5, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (0.25, 0.04448515896357012), (10.0, 3.0197535662513486)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(8, 1.0e6, 5.0, 6.25, 0.25, 0.5, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (0.25, 0.014067442439845935), (10.0, 0.9549323148114102)), LineCableModelsGmshExt.FEMMeshPlan{Float64}(9, 1.0e7, 5.0, 6.25, 0.25, 0.5, 0.19999999999999996, [0.19999999999999996, 0.19999999999999996, 0.19999999999999996], (0.25, 0.004448515892915345), (10.0, 0.3019836725638405))], region_mesh_sizes = [0.0085, 0.0085, 0.0085], cable_outer_mesh_sizes = [0.0085, 0.0085, 0.0085], mesh_growth_factor = 1.2, options = (reduce_bundle = false, kron_reduction = false, ideal_transposition = false, physics = Symbol("quasi-tem")), execution = (ui = false, plot_field_maps = false, mesh_policy = :remesh, mesh_path = nothing, domain_skin_depths = 2.0, keep_run_directory = true, getdp_executable = nothing, gmsh_verbosity = 2, getdp_verbosity = 4, frequency_workers = 2, solver_threads = 4), supplied_mesh = nothing, owned_gmsh = true, getdp_identity = (sha256 = "49d44b1dfe13d9f9b63f8dfe37914af8947f856feebac13fbb6989a41aa88e51", info = "Version : 3.5.0\nLicense : GNU General Public License\nBuild OS : Linux64\nBuild date : 20220513\nBuild host : getdp.info\nBuild options : 64Bit Arpack[contrib] Blas[openblas] Gmsh Kernel Lapack[openblas] PETSc PeWe SLEPc\nPETSc version : 3.14.4 (complex arithmetic)\nGmsh lib version : 4.9.5-git-e286855ce-git-e286855ce (20220218)\nGmsh lib options : 64Bit ANN[contrib] Dlopen Eigen[contrib] LinuxJoystick Parser Plugins Post\nPackaged by : geuzaine\nWeb site : http://getdp.info\nIssue tracker : https://gitlab.onelab.info/getdp/getdp/issues\n"), getdp_provenance = (path = "/home/amartins/.julia/artifacts/51e049a32eeb2ffebcc8a5945bb00f70470a3d29/getdp-3.5.0-Linux64/bin/getdp", source = :artifact, artifact_hash = "51e049a32eeb2ffebcc8a5945bb00f70470a3d29"), gmsh_version = "4.15.0", gmsh_library = "/home/amartins/.julia/artifacts/13aa9c68927e7d05cb18794fbcfe42323c9c0fda/lib/libgmsh.so", julia_version = "1.12.7", adapter_sources = (var"model.jl" = "5dc038f3abd29cd606ab56ecded593b653d3c41cdbfc29e580c189ceeb008584", var"geometry.jl" = "6c6b4236d78945b54c72e7fd34d12a32ccadad75412bbc22ab70ff21df9822f1", var"mesh.jl" = "134502f8ca5ea3243151549b04780259b627adec1682cbfe13b209f9a6993de3", var"onelab.jl" = "baa1af49661cf66d88935d8fb08d7b2a35c7693bebb1fdf540f1463c91dc8164", var"getdp.jl" = "b6a8d3813ce7a200b01cc77428b20dcbb2728f9a4760a1cbde84ef9a698ddd6b", var"voltage_paths.jl" = "81bb6ad0c4078af4f5cce6dcae2bafb6ed60a8374345dfae0b8f61369d61e786", var"workers.jl" = "62ffec093cda6f52d4f08206771dcb82e16e80175ebac6200196c9ed6ffd34ec", var"results.jl" = "bb48b425e16a9048bdf0a4f73c6d7345554d22e396f8d371aa926d4c665a3e4f", var"compute.jl" = "152e200c45a72dab0c2a439900d98a1ec18a6377f2b640ee271850f1ac74b702"), solver_sources = (model = "c0bb424db5ab4db9ca22f549f675edb4646f69910d2aa14e20cf4f40e41bd313", jacobian = "014208d39043ab13183c42c312aba459265715da6d6ddc38ec1fbc39b6090c73", integration = "e113951ac1e6e658f1be50e53875e30c85497445f0e6584a8a7ab1848588e902", materials = "581f089113810a379d8a49c03d79f55045b723c8f4c761d0a493a019ff1f5d9e", quasi_tem = "448d9019ad417c9a784da20170b855dd25e559297499a83d19b007ee095c084d", quasi_full = "1aede35114ea686c5f109089478027d241dec12dc9fd14a24452b348f89b08e4")), terminal_ids = ["cable_0001/three_bare_wires_all_earth/core", "cable_0002/three_bare_wires_all_earth/core", "cable_0003/three_bare_wires_all_earth/core"], reduced_phase_map = [1, 2, 3], inversion_residuals = [6.713178136967714e-16, 5.064917260848195e-16, 2.8066088919804617e-16, 3.3422138886441676e-16, 4.440892098500626e-16, 6.418476861114186e-17, 1.179739509965479e-16, 2.3793527209928034e-16, 2.2204616223000164e-16], condition_numbers = [12.377925740433954, 10.06903060210585, 7.773721224812223, 5.509828954428295, 3.3370923955036242, 1.5466660711208702, 1.0537301308777796, 1.001675900700382, 1.0000053648668035], primitive = (Z_primitive = ComplexF64[3.1569384527251656e-6 + 1.2374941953892434e-6im 9.889008644004892e-8 + 8.120979766293401e-7im 9.888378530515638e-8 + 7.253402825369737e-7im; 9.889008644004893e-8 + 8.120979766293402e-7im 3.156940203533556e-6 + 1.237800013961414e-6im 9.889003470998875e-8 + 8.121041149166945e-7im; 9.888378530515639e-8 + 7.253402825369737e-7im 9.889003470998879e-8 + 8.121041149166949e-7im 3.1569384435367512e-6 + 1.2375260948397891e-6im;;; 4.064512839749084e-6 + 1.0929405558502649e-5im 9.95727325426064e-7 + 6.67586149589869e-6im 9.951474347010177e-7 + 5.808482703019412e-6im; 9.95727325426064e-7 + 6.67586149589869e-6im 4.064685984957519e-6 + 1.0931903537134415e-5im 9.95724511589066e-7 + 6.675893113451307e-6im; 9.951474347010169e-7 + 5.8084827030194114e-6im 9.957245115890656e-7 + 6.6758931134513066e-6im 4.064514491466465e-6 + 1.0929057973331517e-5im;;; 1.4078098949110193e-5 + 9.427560891667548e-5im 1.0154098438757886e-5 + 5.215178163965958e-5im 1.0121825613513052e-5 + 4.349479780183107e-5im; 1.0154098438757886e-5 + 5.2151781639659596e-5im 1.4084679812657182e-5 + 9.429547049205622e-5im 1.015405496530911e-5 + 5.215053170023672e-5im; 1.0121825613513055e-5 + 4.349479780183105e-5im 1.0154054965309122e-5 + 5.215053170023669e-5im 1.4078345460130376e-5 + 9.42749523757185e-5im;;; 0.00011825586406508788 + 0.000773773326287153im 0.0001060875527036882 + 0.00037087417345515203im 0.00010453541028029063 + 0.00028471069054957095im; 0.0001060875527036882 + 0.000370874173455152im 0.0001182868821400726 + 0.0007738750846144698im 0.00010609004988632196 + 0.0003708810021337189im; 0.00010453541028029058 + 0.00028471069054957084im 0.00010609004988632197 + 0.00037088100213371886im 0.0001182588785514038 + 0.0007738015894847507im;;; 0.001184484689608954 + 0.006032309044550077im 0.0011007904697264187 + 0.002094815141613897im 0.001002585911545266 + 0.001267174295816236im; 0.0011007904697264185 + 0.0020948151416138963im 0.001184352323381433 + 0.006032344517663803im 0.0011007744570689971 + 0.002095018232689385im; 0.0010025859115452653 + 0.0012671742958162346im 0.0011007744570689967 + 0.0020950182326893844im 0.001184371743224916 + 0.006032337413209581im;;; 0.011119864601649793 + 0.04273242966769878im 0.008026309843404831 + 0.004874615201441998im 0.004399910550113515 - 0.00019680874044557522im; 0.008026309843404833 + 0.004874615201441996im 0.011113161895694636 + 0.042732163106039826im 0.008026433441878403 + 0.004874392640569766im; 0.004399910550113514 - 0.0001968087404455758im 0.008026433441878407 + 0.004874392640569765im 0.011119821235292367 + 0.04273146884256276im;;; 0.09149300371453145 + 0.28368767551605356im 0.008733496998106724 - 0.00872162055184274im -0.0014407703250733342 - 0.0003904578413631135im; 0.008733496998106717 - 0.008721620551842744im 0.0914939564617966 + 0.28371579711037065im 0.008733934965913799 - 0.008721259027809514im; -0.001440770325073334 - 0.00039045784136311344im 0.0087339349659138 - 0.00872125902780951im 0.09148876735154379 + 0.2836737879820371im;;; 0.7537350606212622 + 1.5682514990945176im 0.00015808363781219276 + 0.001064778206500292im 7.18370446826616e-8 + 1.4988795531927263e-6im; 0.0001580836378121924 + 0.0010647782065002923im 0.7537111883514345 + 1.5684996969879792im 0.00015805992066927291 + 0.0010648271671355046im; 7.183704468266165e-8 + 1.4988795531927248e-6im 0.0001580599206692728 + 0.0010648271671355048im 0.7537492276766637 + 1.5683328404748265im;;; 4.549928337960811 + 6.660714332613744im 1.70928388337544e-8 + 2.3364452433972144e-8im 1.7780742088352628e-16 + 8.283043313101645e-17im; 1.70928388337544e-8 + 2.3364452433972167e-8im 4.55006574349068 + 6.660583745301553im 2.008889563909169e-8 + 2.871606749441993e-8im; 1.7780742088352623e-16 + 8.283043313101657e-17im 2.0088895639091684e-8 + 2.8716067494419946e-8im 4.550209008925247 + 6.660538013985516im], P_primitive = ComplexF64[0.22883661246162 - 0.024983095382574455im 0.17714340233199596 - 0.02498269523276316im 0.16248037517470684 - 0.024981609266753056im; 0.17714340233199585 - 0.02498269523276316im 0.2288449231522497 - 0.02498308017567229im 0.17714401918181763 - 0.02498268090385491im; 0.1624803751747068 - 0.024981609266753056im 0.17714401918181766 - 0.0249826809038549im 0.22883956835186328 - 0.024983066643801212im;;; 0.19240843787908346 - 0.0249771030174076im 0.14071472505193086 - 0.02497388367407443im 0.12605510729509145 - 0.02496521988118678im; 0.14071472505193075 - 0.024973883674074417im 0.1924110151013279 - 0.024977093506367943im 0.1407150307530831 - 0.02497387508757355im; 0.12605510729509134 - 0.024965219881186756im 0.140715030753083 - 0.02497387508757354im 0.19240301223600556 - 0.024977085447185122im;;; 0.15597567295920783 - 0.02493196812926132im 0.10428932628375648 - 0.024906948420560016im 0.08964107328083884 - 0.024841740453530308im; 0.10428932628375649 - 0.024906948420560016im 0.1559805482274641 - 0.024932014150075267im 0.10428705761239163 - 0.024907058340290184im; 0.08964107328083881 - 0.024841740453530325im 0.10428705761239168 - 0.02490705834029019im 0.15597297612732627 - 0.02493218545179192im;;; 0.11958540243897073 - 0.024616863307245176im 0.06794030284806876 - 0.024437181247560346im 0.053417041197561466 - 0.023997546719685268im; 0.06794030284806883 - 0.024437181247560346im 0.1195833325016026 - 0.0246161470179541im 0.06793967647880747 - 0.024437076338959432im; 0.053417041197561486 - 0.023997546719685278im 0.06793967647880747 - 0.02443707633895943im 0.11958599864064232 - 0.024616621632717994im;;; 0.08393271086676495 - 0.02278997737658976im 0.032672868345281894 - 0.02169739108426287im 0.019301546215482752 - 0.019387215842086036im; 0.032672868345281894 - 0.021697391084262874im 0.08392487261803071 - 0.022785202917857253im 0.032674748361871536 - 0.02169694558271067im; 0.019301546215482746 - 0.019387215842086053im 0.032674748361871564 - 0.021696945582710684im 0.08393366105065148 - 0.02278876870409151im;;; 0.05363902116790498 - 0.016364079411300417im 0.005360993141685143 - 0.01175399292578833im -0.0012313293537008763 - 0.005832433852295408im; 0.005360993141685147 - 0.01175399292578832im 0.053638835713676794 - 0.01635152575248076im 0.005360239640150436 - 0.011754102845842484im; -0.0012313293537008776 - 0.005832433852295412im 0.005360239640150436 - 0.011754102845842489im 0.053637182185623085 - 0.016364532445051597im;;; 0.035917864857161745 - 0.011515756641272808im -0.0010781911246275672 - 0.0010842093696258272im -4.0508361875138124e-5 + 0.00015390012161888925im; -0.0010781911246275672 - 0.0010842093696258272im 0.03592135582950522 - 0.011515895597995604im -0.0010781411662237755 - 0.0010842557832089856im; -4.0508361875138103e-5 + 0.00015390012161888925im -0.0010781411662237745 - 0.0010842557832089858im 0.03591610764636348 - 0.01151522678745252im;;; 0.01986208900870756 - 0.009545857506274225im 1.3482826957135782e-5 - 2.0016021141850554e-6im 1.9087476703643448e-8 - 4.840723917992238e-10im; 1.3482826957135785e-5 - 2.001602114185057e-6im 0.019865232462830455 - 0.00954555613595454im 1.3483446866069231e-5 - 2.001296898214388e-6im; 1.9087476703643445e-8 - 4.84072391799233e-10im 1.3483446866069226e-5 - 2.001296898214387e-6im 0.019863119200701735 - 0.009546038009754787im;;; 0.008435572624461533 - 0.005762950683398575im 2.959022231507037e-11 - 2.164997801882138e-11im 1.0633299872980132e-19 - 2.2535892514426856e-19im; 2.9590222315070365e-11 - 2.164997801882138e-11im 0.00843540724725469 - 0.005763125357016769im 3.636790859868074e-11 - 2.5444912495177172e-11im; 1.0633299872980118e-19 - 2.2535892514426884e-19im 3.6367908598680745e-11 - 2.5444912495177182e-11im 0.008435349293969758 - 0.005763307336628993im], phase_map = [1, 2, 3]))) \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/fem/fem-artifacts.toml b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/fem/fem-artifacts.toml new file mode 100644 index 000000000..7ed7d892e --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/fem/fem-artifacts.toml @@ -0,0 +1,296 @@ +"attempts/f0001-8W5TJ5/attempt.json" = "e0096f0b8084da736ebd0b97f6019987c9ab2f5cc26d31a0e0f286453e4015d8" +"attempts/f0001-8W5TJ5/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0001-8W5TJ5/getdp.log" = "e425257e5bf869802a21e03bb2bcfe820abc43f43e71e9905126a39c28cbc085" +"attempts/f0001-8W5TJ5/petsc.log" = "9e6afafefe3027f4a119499a1d49f586531bf1652a93b23d56303f5f1c15494f" +"attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0001-P.tsv" = "04c9e6b2c890764d1fe53b5595feab56c4b9964ad522d22157758fe08511d41e" +"attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0001-Z.tsv" = "ea6e5ec751ce81e04b81e925b9eff27bab7f148fc641aef12ae784e589a0d558" +"attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0001-timing.tsv" = "b4d24bfdc6ff3d55fc16b1f6131e1aef94d8b0815edd8029a6f9172566a692bd" +"attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0001.done" = "a40a11bab26b32dfb169550347f289dc710df24f59fe3ef20963d46f838f0586" +"attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0002-P.tsv" = "58b5ac96646f67a8a9d68027ed7e65e00aaa7ccdec213ad9721e91001d305633" +"attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0002-Z.tsv" = "1900dfd1f89bc54d88574665eee50adcd1e71e12caf41623d8b182074eda2916" +"attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0002-timing.tsv" = "2296ec408a6bf04d984403a0256e1c18e167bbc7944d628d66da1d90a7fbb23e" +"attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0002.done" = "f58042f034284ccf2955f2795037937a5bd0e4288ffc0d0d5124e33ee07f17ec" +"attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0003-P.tsv" = "d1d337ca87a70e6d469f4530db9baa306c88627441482c75760e247c5087e00a" +"attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0003-Z.tsv" = "37a39f817bcd20c901f7efdf9e9870f3e2fc0364c9b6a7a82b347f92140eab27" +"attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0003-timing.tsv" = "ffdfd1faec39eef91c74586e5aa85cb800f3c7941340a4c4182c9904993a13f3" +"attempts/f0001-8W5TJ5/raw/jobs/getdp-f0001-b0003.done" = "41695cf0039479bd9b703276e3c15366a77a1e43f8490b48634d8a1c2a61be94" +"attempts/f0001-8W5TJ5/solver.pre" = "b1197bca659ee38214b5e1c5ac7a29739e43eabdda4306801c6df5793112a46c" +"attempts/f0002-LZaICJ/attempt.json" = "5ef66fb9a24b7219ebad79e36bec402529bdc1a5009886df5ae74c2cbd50e6a9" +"attempts/f0002-LZaICJ/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0002-LZaICJ/getdp.log" = "361aaef0f48d463d93b480ce40100523c1e7b9863fb385c7e28ed18211b84311" +"attempts/f0002-LZaICJ/petsc.log" = "e124a500f3edaa1dbd5d87a382ae14a3e92ec840bc8d93725596d00e335754f3" +"attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0001-P.tsv" = "a2f141d70f0dd2d524a23219f83cfd6b6c6fbc8b4f98ce1cfe7a0048aa180f22" +"attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0001-Z.tsv" = "ed761307661a09df2f10d0b6d7629be3fbd7b5d18a535a689a617105a8c89806" +"attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0001-timing.tsv" = "e0ab4f02f2e74b89d0be68e501df864d67415c680b768402a4c81a32ee6f1cc6" +"attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0001.done" = "3dbd9539f415ee889a34b9c6efe6b9cb8168b7c0945d52d7e63e920c8990d2d1" +"attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0002-P.tsv" = "acb7239febeba6f0b026459ab2b13d63ff3c1a1b9857d02e0f3e07b7d991d69f" +"attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0002-Z.tsv" = "f34bce613e012e1fecab9d0ee72589bd857a9747198349cf5c3e73f56060a138" +"attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0002-timing.tsv" = "abafdd5f0b8dcdf79b68284c06d318fc87089e7492d5eb6ef3386cea1023a447" +"attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0002.done" = "d498381e814531161686031fcde5aa549b456c6cbee61f6eb67916650247e717" +"attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0003-P.tsv" = "25e7131c3d0de464fcb954e57953730133762473caa9f21c03cd809814dc1be8" +"attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0003-Z.tsv" = "0341d1a792571476890702a3abdd764109ca9c72c953260e9df965d1df93fa74" +"attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0003-timing.tsv" = "837caea517bfd3000c718be9bcd3c264a537dbeac0929f62dd0653cc70f6acdc" +"attempts/f0002-LZaICJ/raw/jobs/getdp-f0002-b0003.done" = "fa5c0c361441906d1aaa6b254ae52969806be93c36846f4c70c70a18dc833050" +"attempts/f0002-LZaICJ/solver.pre" = "a18f62323a8274bb11d4fa84dd4f4d7b2565ba38426d561be33eaac16219c518" +"attempts/f0003-AQ7KkO/attempt.json" = "d15ff0978ad018b157d0ab3b6533696f9063d4778dc4c85a874f2769f6fce0ae" +"attempts/f0003-AQ7KkO/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0003-AQ7KkO/getdp.log" = "450a735060c386e538543cd578e7aae55a230193f0c14540e8bde1e5f7bba1ba" +"attempts/f0003-AQ7KkO/petsc.log" = "72d17eb0dbc247aa836d5b3f16409127e904fd02d0195e788e3e740dd61ee9d3" +"attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0001-P.tsv" = "d3ee51cf43f3af9d5f5da01dd439b4481af34be5e677bfb8e853965168f6ca00" +"attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0001-Z.tsv" = "8d996e6af519268f196199ddd8eaa5cf72f4565d6f3e9560957a081d3e870621" +"attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0001-timing.tsv" = "4a2b576e2c1a90c38f3fc60330a9d4582a9783d41689dd400cfced6c6244a478" +"attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0001.done" = "02395cb824ac1c8ce1d2e8cd3c57fd19d5c904215aef49b1fba677c51e99d519" +"attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0002-P.tsv" = "ff9ad50deb948f3e122991f4ff69320f86515f9299b50b0ea5c987f555d1e416" +"attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0002-Z.tsv" = "0707de70a3edab0d95ef4653d5826789369b92a47a5c184b37d6cc928d56235b" +"attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0002-timing.tsv" = "a4d2ea803ecfd00a4f71a93e38f17faf9a1da09610de6db19a0874bb2d501de8" +"attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0002.done" = "84034583a44a97df0ec5820a76fc891b95a9b6bc0bf693bed9882420a3eb5b05" +"attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0003-P.tsv" = "06fb17367d186daf81c3693349f1bf625f6768c7e4694f5d4bba6f683bb8034f" +"attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0003-Z.tsv" = "1fe2894addaefb05945108a1d1c4c3cd3cbc9d550c8e038943cff347a711f32f" +"attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0003-timing.tsv" = "de73b8e8ccb704ec849dd695e0ef633f091f145f3283cd0de524078188a7b92f" +"attempts/f0003-AQ7KkO/raw/jobs/getdp-f0003-b0003.done" = "aec9dd1bab4102d3c07f9c6abf83e4d604721961d8e37cbbc9620f7f71cafcae" +"attempts/f0003-AQ7KkO/solver.pre" = "fd78895c6aee8930de5414719771d2bd45f7d1689a6cba5b6a9b226a6af18e09" +"attempts/f0004-uOYCzA/attempt.json" = "29cd0d4629d64e4b0ca270d91db8206464fecc2921e59abca914445d5f35911b" +"attempts/f0004-uOYCzA/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0004-uOYCzA/getdp.log" = "67d5b9f38e558a64157718198aaad82ec7d4d7c6b4e4e7f9f2cf5c90027cd891" +"attempts/f0004-uOYCzA/petsc.log" = "a31481984a64836823af595cfa092155a310b1b2f31a80493fdd55af054a60c5" +"attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0001-P.tsv" = "d9c7b2db4db87ef5e9ab5d6851f757c9ccfa86c1030ce1621f0bf49650ddfb78" +"attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0001-Z.tsv" = "74055d09ed213b75641b96a531210d9cd56772f52522fac43e717642c8b1fec9" +"attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0001-timing.tsv" = "b7c8ea9b5cbf37b7eadd58889748ef24f4df19aa1f36a61448f805c36c77830d" +"attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0001.done" = "05b52afc2d2a0ffefd0bb6e40585165d1b2572547d59d3c360b4163202709006" +"attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0002-P.tsv" = "601e72dfc3504f29924d481efc36843de65df1c59391d52458314eb16efe9124" +"attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0002-Z.tsv" = "7d591d72b116da6bb5b917e70064a74ff7dfb4a14032179f77a138c9eb32b813" +"attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0002-timing.tsv" = "14e11d0e186bee73caeca39ea46683cec8df473cee9f4b767d77cb856e5a0c0e" +"attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0002.done" = "35d216e20aed80a36327575b350d8e4c12b70f2038f5d07d55614c92afdd6b6b" +"attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0003-P.tsv" = "6d07ae7ee95708c5078bfb614504d6df9a2d6a02276e7ff46321b47bbe8a7579" +"attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0003-Z.tsv" = "8b78acaa7a69e9b289ff78e2d1cee0043520d80a8a97f8b5aade642f6b8d28f8" +"attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0003-timing.tsv" = "57400f8e43c5962161e395ef7541ec4761a9b34cf8026064554c417137f64184" +"attempts/f0004-uOYCzA/raw/jobs/getdp-f0004-b0003.done" = "eae2a23f21e24dd158a817c096e6e77f32e1d2a850c4163d279c3531eef2af5a" +"attempts/f0004-uOYCzA/solver.pre" = "622b234a45ba9054b0346bb8754259116d8b5013c8fed7d602ef8dfce33d9f2b" +"attempts/f0005-tlXUhL/attempt.json" = "08a6b513d8fc42487f1801a51442960ab06f327def9adad390c2140b3547efa9" +"attempts/f0005-tlXUhL/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0005-tlXUhL/getdp.log" = "bc350cd44067250d5de617b517d24269c0f64947c0553bc750aec0f7fb6072d6" +"attempts/f0005-tlXUhL/petsc.log" = "2f7fce5e8e52d6c259c5403f71ae5bc4dcc1d8658f8b3cd0814a17f9b4be54dc" +"attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0001-P.tsv" = "0195d231bec422564610e649f64e7c646f0c40508b789db03e5c9b96af01bfad" +"attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0001-Z.tsv" = "d59f8d9fac1686a569d277b27f0a0702decd9a2931a64b28cd0d55127cbfd09a" +"attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0001-timing.tsv" = "54cdacfc5f0b0dc43d04da0f509e6f00035787a94a673001afbf108a09466c9f" +"attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0001.done" = "ee7bf9802d600055d484ec4e501666418037b219c7bf910eefda35e16a843b08" +"attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0002-P.tsv" = "0d3a498655da1048906846d62e6168f8805e269122e3a9cf287f60df4f92e7bf" +"attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0002-Z.tsv" = "3ad7d164f2327fd8d0bfe86a734e779d5a46ca99fedbff01c823fb892090f44b" +"attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0002-timing.tsv" = "ca6bb49c1c85055742ad1b0dfde706bc99ebc5305682a4906dae1c22087e2b30" +"attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0002.done" = "e26c3631aaa36c47efb3ede81242002dbebf27057f428fe57f60849cede6b3f3" +"attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0003-P.tsv" = "393b407d46d6f00ff603cf7c07eaaeca8e3fbb073ee1194e633bd4a7624f8f2c" +"attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0003-Z.tsv" = "14222044b68256889e1bddf12d1407ab7255fe9422061ab9a53bfe11fabffaae" +"attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0003-timing.tsv" = "175f49f83cd725e49534494110c4dbc5b9e667f2cada1f81c0916c7238344b91" +"attempts/f0005-tlXUhL/raw/jobs/getdp-f0005-b0003.done" = "a91d7ca1bb120ef43c71df371c45a71b1cc7744f1ee2eb082338700de7f329c9" +"attempts/f0005-tlXUhL/solver.pre" = "28fb78402edeb8e400e6c391bf9155ff08eebb2056d9d24ad9500176a9bccc92" +"attempts/f0006-pGwU3U/attempt.json" = "0f757c5486999602172c295e6db676d048beff5b10c8b560ffd72a64af4651a7" +"attempts/f0006-pGwU3U/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0006-pGwU3U/getdp.log" = "91c02bef108ac503067bcff6e08bae0aebabab43e48de09c81e3aba6f88d3cc8" +"attempts/f0006-pGwU3U/petsc.log" = "257aed36362714c9c9677da6f6f0c8b0ec473eede825647cbdcf3aebbfdc100e" +"attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0001-P.tsv" = "a82402fe73b5a0c4da1a80757377e3ac43e2f50384f039e0a90257c465bf1ea0" +"attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0001-Z.tsv" = "7957113d319bff3dc7cc96746b79efb3212d09fa4dd35ecde0edf2997606c8ee" +"attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0001-timing.tsv" = "fff2a4084606e6dad3e90d75c1b270d79a527f43a364260ccd73d309c5d2d233" +"attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0001.done" = "e41a78288341a2ae156cd28b6102b3a31a74b012eb4deb233e88fae392912e8e" +"attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0002-P.tsv" = "c2751c83dbc41cedf1067fd0c40986ce6e83ffa8b45ccb0dc5fb935a3d309b68" +"attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0002-Z.tsv" = "c4486b646ad519626acb5ff787688a63fad719e202ea064fb9693f75ba84c714" +"attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0002-timing.tsv" = "005557eb52f47fe3ac08bc100bba81039d39ca05df70fb1f13b330efb8a0f43f" +"attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0002.done" = "c105367e4bac19858725b1b7c56efbb25ae370173d002f9a678a4762a5c69ac9" +"attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0003-P.tsv" = "a929d1a50d16f50b0ad5f8fb47d66e3ced5f55507e1c90d00af1e0776ba964de" +"attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0003-Z.tsv" = "fc017ac098faa689d6c9a16c5a7807f1657187a8230fb4a35f6eafdcb6e6327a" +"attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0003-timing.tsv" = "63d182bf9d710f7eb5314cd327c4ed4b75af5038567071a82a04a69c448b5cd1" +"attempts/f0006-pGwU3U/raw/jobs/getdp-f0006-b0003.done" = "de6214d7f2cf6d2097d94d720289a7424da0b9bf7a5df27326d8b69e936798e8" +"attempts/f0006-pGwU3U/solver.pre" = "0e83ce495eeb37264b72b1ccfa0e26358e87f410aeb1e3d507925bfb214b8531" +"attempts/f0007-xOgtG9/attempt.json" = "e1e0e1f0ad201ed0f46362398e7417b388a7af6a1aebed2f9c4ed8e9ee992c08" +"attempts/f0007-xOgtG9/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0007-xOgtG9/getdp.log" = "0301504737bf92480a1cce7cb241106fadf95b5b1829c57c64ca15f445bd7aea" +"attempts/f0007-xOgtG9/petsc.log" = "0b4a8d70bbf39a11099e2477a7a6f3ba2c45696abe6d7e72fbe6766486719f45" +"attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0001-P.tsv" = "9249112a255c56a6846ecb051da3b3bdf9261538e5ddd23b0d993ffcba378b79" +"attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0001-Z.tsv" = "d8f3054f6b1bd1d7260f02a51a12fd26b5710f84933cad1f514a4cffa067f19a" +"attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0001-timing.tsv" = "c8aabe04afcd8da27f4179c975c8a17f329eea77419c0bf41c5028f382957878" +"attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0001.done" = "d95e4d651bb7fa68e36e0077af5a10e10baacf852173da7ac0f1cca3e414dc7f" +"attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0002-P.tsv" = "83e7b9bad4a20daf882ea57f0b6522b3d63fae68d8d5a7e88fb741ae114e3706" +"attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0002-Z.tsv" = "f3e1ef07ed1183eac3cf47374a77010b03dbdcb4553a5bdc87c79c57872483c3" +"attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0002-timing.tsv" = "4396c8a8a9dc1861c06b951b8a41334eaeb5049c4de36813cedaca7b96faf8d2" +"attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0002.done" = "a953202777b2aea0282f4e5ecac9feb227f850cb3f80f2247ca07a708f3635be" +"attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0003-P.tsv" = "b927cd849430d14e21cf567f47a0804d9d509b23fc7d68aa205f8bec6a621a1f" +"attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0003-Z.tsv" = "6ac3065659e8357f8f93d2951e040771a56d298f65843c3524be433d4dd5697e" +"attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0003-timing.tsv" = "c97cfa12ecb62cd515d72f0810059f972eb4314e0d54248963fe3dd7e71d0938" +"attempts/f0007-xOgtG9/raw/jobs/getdp-f0007-b0003.done" = "bc6e3e671a966700354c8fb7f9ab0ff830db8a4b88332e0ec4974ce29df37b0c" +"attempts/f0007-xOgtG9/solver.pre" = "6caa5bc65668d1a427339a3dbe1c36af8d776a04cadf3e71310ab40573953ade" +"attempts/f0008-dar4Cz/attempt.json" = "07965d252f8d03ca2e73b227d9093a1afb27b33ac842e3021d4d630d2651dfad" +"attempts/f0008-dar4Cz/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0008-dar4Cz/getdp.log" = "7a22ff739c13e9feaf96f4514328c13fb21c2d338af957497cfa06808cdf7e59" +"attempts/f0008-dar4Cz/petsc.log" = "7b866a6845db0f5f662a784a101f52849e3465d284559a91186213a414df54b8" +"attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0001-P.tsv" = "1b5071aa6ae6390e09800c28347da82417b0fa2b2a3d61db90f9bd477231f789" +"attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0001-Z.tsv" = "dd382d525223d354279aa84f7459491d9b81c00eee3c9dbab10fb7bc8e84aea8" +"attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0001-timing.tsv" = "56dfd2998d962fef93128299ee10ce52cf45f7a69a844f07b7e729ad51a703d7" +"attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0001.done" = "c03ca6db0131d3218478fb4e85f0350dca26787b9ffdfc9d50b5d68ad93b352c" +"attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0002-P.tsv" = "6e1068ab50509cc6ccc88ffa58da5125be06c744248ebd787f1a725a0d3ce19c" +"attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0002-Z.tsv" = "3219910dbd6b5b68a5f56630becc77e2427ba31e965fdcd3f3b2ba9d583eebfb" +"attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0002-timing.tsv" = "f645e39ee1dae92a40de8945d6854c29ff576aa4b90375f550aa3ef8d2175845" +"attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0002.done" = "76f50b528daca79ab1eac47c47f96ca1e7b8ad94bcaefbaac04335941306f993" +"attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0003-P.tsv" = "e271103adf21200c4f34c3c7e3ad80bb11042de627738f0ba14defd006942a15" +"attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0003-Z.tsv" = "fef20b3ea2c6e03fbcd2ceb6acfdeaf0d3326e1e51e06f9762cb54d4acbdeb13" +"attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0003-timing.tsv" = "ad93931de6d8f1ca6e4481e29ba4fad116814b7a9f699670c846c22eb78ee814" +"attempts/f0008-dar4Cz/raw/jobs/getdp-f0008-b0003.done" = "20bdbe7d9351850f55ebf49a31f3a93235d04aea44253ddd2d75828a83fa1c74" +"attempts/f0008-dar4Cz/solver.pre" = "f730c362a1c8175df0c750fb8d63445aa6f6bb66d5711bd24fa0d41a669d28dd" +"attempts/f0009-qFY1kw/attempt.json" = "775ff0b0e5c1eb4d2f654d4280b0d42a2a3f6c4990c04338459df06114154c04" +"attempts/f0009-qFY1kw/bases.pro" = "c85d167622ebb283cf2c025f4158745da60962b68b8c6d3b7f1d8377fcf3d636" +"attempts/f0009-qFY1kw/getdp.log" = "5553c64fa4c0e06fdf8eaa5b91fc5f8090edbaf2b18b6f288d85b53234ba8376" +"attempts/f0009-qFY1kw/petsc.log" = "b8581b22ad0079f8bc1d298b5e49278baa7b84f3b0725965b56a977d98dc1fdd" +"attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0001-P.tsv" = "2ee20b18c0c855a857ffa64a61d5ebbd7d0834abca56824221744b5b5f1f250d" +"attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0001-Z.tsv" = "24b9d2147f2172fbff8181910ad95a079f6c67a849e4f1e2eb9c3622470a7a94" +"attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0001-timing.tsv" = "2f10ee9aee2eeac69d6207e568e50d41a962f4560fe2d324b94c4dad48b73d14" +"attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0001.done" = "22b8b831af1067ac03eaf9570ddd71235af29f6b65e559199200376f6ef90f2a" +"attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0002-P.tsv" = "a72b35bdb650f4fa23879e9b7e993657d19ac85eafce3ff14e40071f68284442" +"attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0002-Z.tsv" = "1b6dd8be9a57ce679e09d8f852dc5a9d659fdc25a7c6af41333f148c6640571f" +"attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0002-timing.tsv" = "537a16539c2d71dce3ce71ec3e3870e8d04b9c12928603f2e02fa221a3983e19" +"attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0002.done" = "2ab6b278377a8a5090d5d456a9193028844db99b8f3fa96c2433d70920e73c22" +"attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0003-P.tsv" = "cf0156d87713c607ff736097e3939e4caf80c742f6bef29e21f3684dd14a16be" +"attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0003-Z.tsv" = "1141e3b846e0407089025c2310354fd992ee98592d76645c8547011189ead1f3" +"attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0003-timing.tsv" = "495a2a6840774175d78d9817a2f55ffcedc82da1a9b00fcb860a071f0669521f" +"attempts/f0009-qFY1kw/raw/jobs/getdp-f0009-b0003.done" = "3449a1ca026b39cca413695b5bd20e176addf6bcf3103f9147e8ac56407cafa3" +"attempts/f0009-qFY1kw/solver.pre" = "9da97cdf74ca6b768ed4b91afe309520e44ca84b43c41f4ac153971e3060556f" +"coordinator.lock" = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" +"input/computation.json" = "b48e71939cea59c3075535510a2a2ee254158e6bab1f1ed172159f459085eebe" +"input/getdp/integration.pro" = "e113951ac1e6e658f1be50e53875e30c85497445f0e6584a8a7ab1848588e902" +"input/getdp/jacobian.pro" = "014208d39043ab13183c42c312aba459265715da6d6ddc38ec1fbc39b6090c73" +"input/getdp/materials.pro" = "581f089113810a379d8a49c03d79f55045b723c8f4c761d0a493a019ff1f5d9e" +"input/getdp/model.pro" = "c0bb424db5ab4db9ca22f549f675edb4646f69910d2aa14e20cf4f40e41bd313" +"input/getdp/quasi-full.pro" = "1aede35114ea686c5f109089478027d241dec12dc9fd14a24452b348f89b08e4" +"input/getdp/quasi-tem.pro" = "448d9019ad417c9a784da20170b855dd25e559297499a83d19b007ee095c084d" +"input/model_data.pro" = "83f1e7217a46f45f5f12a55c73795fdf4d90552c5ad57e9dd579cd69ff6a50f6" +"input/problem.json" = "fb186a621d0ec64b232f902e99f57f242ae48625abbbfe95a2eda97664742ac9" +"logs/getdp.log" = "d38123045757e16c6d99756fefa5fb9e236f45e526b4bb7c314aba55d0ba1722" +"mesh/frequency_0001.json" = "48dd98f6dfe7fc4e7d874e41aad3bc1d2afba1a9946d12d0885eb8c60311116f" +"mesh/frequency_0001.msh" = "199709c5a52023bfe64ced1f53a8ddf86809ab05c516751dfdd34c0787c889bd" +"mesh/frequency_0002.json" = "1bf682e97aade85744ea807d2e4617f0238ab30e54c685e39c6597e4c64fd322" +"mesh/frequency_0002.msh" = "7812570d87b9321ae1bb67b72c250c0c1f4bb1a078ced2690bd1e281fa246011" +"mesh/frequency_0003.json" = "6483df14df08244848adcc8bbe549eaead7bc0b0475ff1400e351d79eb423e28" +"mesh/frequency_0003.msh" = "e9918131636504e7fa4817a36bc87a7d68c896f120cd27a604e32dabe5a9be5a" +"mesh/frequency_0004.json" = "df309d892ee51b1f39e2e18a82cc263b971da57aa702bbdd6e9fcb2710e8f816" +"mesh/frequency_0004.msh" = "fc47ed5f3fa6e825426cac5b9d2fe2c58bd8868e52e440e9ec1e7140a613699e" +"mesh/frequency_0005.json" = "e5c8c15308b560c7c6ef57741a28997589a562d35366c1333ec2a6ee81e4e8e1" +"mesh/frequency_0005.msh" = "209b6569356f7c621ff436762f1a1fbc58e49488e841a7b70645a4f0a4fd68b2" +"mesh/frequency_0006.json" = "de86d60fadd372e654fff85ee551351146d2f997b8fab07593196cc98dbce883" +"mesh/frequency_0006.msh" = "e1f4eb09593eab4c6445e0739bb718aaa2cee30b5ec4fb59805e6906d43ad183" +"mesh/frequency_0007.json" = "6f78ea55382e107a5a715fc51e758f787be0039b5d2f8272849c1a9bd1315791" +"mesh/frequency_0007.msh" = "247c7ddef1b64805899c4ebdf91e40f40e56b9412d054545f16875505ca501fe" +"mesh/frequency_0008.json" = "02287cc9a839718f85da9fc7560140da9a90037b09501aa451d3f04f83113052" +"mesh/frequency_0008.msh" = "945d16cac2cf148e02dc0fb16e072a0a7c85395d005d869fe002b2dbe26d270e" +"mesh/model.json" = "fce94f26e50383d76251afaa1959b21d7976e436bd366a1cd08429bc0098134c" +"mesh/model.msh" = "54c0c560a95ea9bf6f7aee2944791c8b1c23c13adb7080004e7d4ad6a19eeecd" +"raw/P.tsv" = "959d8cd451e4797fd2471b8832f0c2805d1548a5ef7fe4ae51ea9e138bdc61c6" +"raw/Z.tsv" = "3de44131cb37d91434ba75874a929db6e370119a35fc63b66ee7ead13b25cc30" +"raw/checksums.json" = "70d05414763c6def29e00f8f9d8d7ce3f0ca431b070778dddf8cdb864fd6ca39" +"raw/jobs/getdp-f0001-b0001-P.tsv" = "04c9e6b2c890764d1fe53b5595feab56c4b9964ad522d22157758fe08511d41e" +"raw/jobs/getdp-f0001-b0001-Z.tsv" = "ea6e5ec751ce81e04b81e925b9eff27bab7f148fc641aef12ae784e589a0d558" +"raw/jobs/getdp-f0001-b0001-timing.tsv" = "b4d24bfdc6ff3d55fc16b1f6131e1aef94d8b0815edd8029a6f9172566a692bd" +"raw/jobs/getdp-f0001-b0001.json" = "c51c5bd457a9d230693de66ed8d8947bdf7e0ffe6efee8059ae6f141f0ce7c80" +"raw/jobs/getdp-f0001-b0002-P.tsv" = "58b5ac96646f67a8a9d68027ed7e65e00aaa7ccdec213ad9721e91001d305633" +"raw/jobs/getdp-f0001-b0002-Z.tsv" = "1900dfd1f89bc54d88574665eee50adcd1e71e12caf41623d8b182074eda2916" +"raw/jobs/getdp-f0001-b0002-timing.tsv" = "2296ec408a6bf04d984403a0256e1c18e167bbc7944d628d66da1d90a7fbb23e" +"raw/jobs/getdp-f0001-b0002.json" = "5e3e8965d1ca74c62e0b8a1dcc14fabd2540a6bb6e7591bdd21240ec7ca74bf7" +"raw/jobs/getdp-f0001-b0003-P.tsv" = "d1d337ca87a70e6d469f4530db9baa306c88627441482c75760e247c5087e00a" +"raw/jobs/getdp-f0001-b0003-Z.tsv" = "37a39f817bcd20c901f7efdf9e9870f3e2fc0364c9b6a7a82b347f92140eab27" +"raw/jobs/getdp-f0001-b0003-timing.tsv" = "ffdfd1faec39eef91c74586e5aa85cb800f3c7941340a4c4182c9904993a13f3" +"raw/jobs/getdp-f0001-b0003.json" = "83b8faa92f6badd7bb3cc0f6f3e0aba95db234386f3b2d8a5cf51d409a6f42da" +"raw/jobs/getdp-f0002-b0001-P.tsv" = "a2f141d70f0dd2d524a23219f83cfd6b6c6fbc8b4f98ce1cfe7a0048aa180f22" +"raw/jobs/getdp-f0002-b0001-Z.tsv" = "ed761307661a09df2f10d0b6d7629be3fbd7b5d18a535a689a617105a8c89806" +"raw/jobs/getdp-f0002-b0001-timing.tsv" = "e0ab4f02f2e74b89d0be68e501df864d67415c680b768402a4c81a32ee6f1cc6" +"raw/jobs/getdp-f0002-b0001.json" = "a8f8c7964d9183df28771f084b6fadbebade1b0cef64fdbe6c97c76c69c5f49b" +"raw/jobs/getdp-f0002-b0002-P.tsv" = "acb7239febeba6f0b026459ab2b13d63ff3c1a1b9857d02e0f3e07b7d991d69f" +"raw/jobs/getdp-f0002-b0002-Z.tsv" = "f34bce613e012e1fecab9d0ee72589bd857a9747198349cf5c3e73f56060a138" +"raw/jobs/getdp-f0002-b0002-timing.tsv" = "abafdd5f0b8dcdf79b68284c06d318fc87089e7492d5eb6ef3386cea1023a447" +"raw/jobs/getdp-f0002-b0002.json" = "360a0bbbc22926e01b3f3b535a0f9abec824a38a45143e26cf19956e18bceb48" +"raw/jobs/getdp-f0002-b0003-P.tsv" = "25e7131c3d0de464fcb954e57953730133762473caa9f21c03cd809814dc1be8" +"raw/jobs/getdp-f0002-b0003-Z.tsv" = "0341d1a792571476890702a3abdd764109ca9c72c953260e9df965d1df93fa74" +"raw/jobs/getdp-f0002-b0003-timing.tsv" = "837caea517bfd3000c718be9bcd3c264a537dbeac0929f62dd0653cc70f6acdc" +"raw/jobs/getdp-f0002-b0003.json" = "e539d7651baf4aa5b8fdd1c85da428a88f48e0751300badc5d6abb6491dadc1d" +"raw/jobs/getdp-f0003-b0001-P.tsv" = "d3ee51cf43f3af9d5f5da01dd439b4481af34be5e677bfb8e853965168f6ca00" +"raw/jobs/getdp-f0003-b0001-Z.tsv" = "8d996e6af519268f196199ddd8eaa5cf72f4565d6f3e9560957a081d3e870621" +"raw/jobs/getdp-f0003-b0001-timing.tsv" = "4a2b576e2c1a90c38f3fc60330a9d4582a9783d41689dd400cfced6c6244a478" +"raw/jobs/getdp-f0003-b0001.json" = "6c4e4658b6321160e1dc3c6049ccc90855f0282995ccb03124f1dac8a5da60de" +"raw/jobs/getdp-f0003-b0002-P.tsv" = "ff9ad50deb948f3e122991f4ff69320f86515f9299b50b0ea5c987f555d1e416" +"raw/jobs/getdp-f0003-b0002-Z.tsv" = "0707de70a3edab0d95ef4653d5826789369b92a47a5c184b37d6cc928d56235b" +"raw/jobs/getdp-f0003-b0002-timing.tsv" = "a4d2ea803ecfd00a4f71a93e38f17faf9a1da09610de6db19a0874bb2d501de8" +"raw/jobs/getdp-f0003-b0002.json" = "21f5b525cc6381c4496e1492509dcf70b028cd29e435e5963e896a97bfefd29d" +"raw/jobs/getdp-f0003-b0003-P.tsv" = "06fb17367d186daf81c3693349f1bf625f6768c7e4694f5d4bba6f683bb8034f" +"raw/jobs/getdp-f0003-b0003-Z.tsv" = "1fe2894addaefb05945108a1d1c4c3cd3cbc9d550c8e038943cff347a711f32f" +"raw/jobs/getdp-f0003-b0003-timing.tsv" = "de73b8e8ccb704ec849dd695e0ef633f091f145f3283cd0de524078188a7b92f" +"raw/jobs/getdp-f0003-b0003.json" = "d8613efe09efd4c6a3d25851b86acc107fb983227e09ed3b851f6a0ffcb6cdbc" +"raw/jobs/getdp-f0004-b0001-P.tsv" = "d9c7b2db4db87ef5e9ab5d6851f757c9ccfa86c1030ce1621f0bf49650ddfb78" +"raw/jobs/getdp-f0004-b0001-Z.tsv" = "74055d09ed213b75641b96a531210d9cd56772f52522fac43e717642c8b1fec9" +"raw/jobs/getdp-f0004-b0001-timing.tsv" = "b7c8ea9b5cbf37b7eadd58889748ef24f4df19aa1f36a61448f805c36c77830d" +"raw/jobs/getdp-f0004-b0001.json" = "160fa536493c0c1d866a705675a67c255c94f46d77f25e533068b8492f0817b8" +"raw/jobs/getdp-f0004-b0002-P.tsv" = "601e72dfc3504f29924d481efc36843de65df1c59391d52458314eb16efe9124" +"raw/jobs/getdp-f0004-b0002-Z.tsv" = "7d591d72b116da6bb5b917e70064a74ff7dfb4a14032179f77a138c9eb32b813" +"raw/jobs/getdp-f0004-b0002-timing.tsv" = "14e11d0e186bee73caeca39ea46683cec8df473cee9f4b767d77cb856e5a0c0e" +"raw/jobs/getdp-f0004-b0002.json" = "215545b42852933a185884dfbfb05ff23fd988a29a15544155649cc10d55f486" +"raw/jobs/getdp-f0004-b0003-P.tsv" = "6d07ae7ee95708c5078bfb614504d6df9a2d6a02276e7ff46321b47bbe8a7579" +"raw/jobs/getdp-f0004-b0003-Z.tsv" = "8b78acaa7a69e9b289ff78e2d1cee0043520d80a8a97f8b5aade642f6b8d28f8" +"raw/jobs/getdp-f0004-b0003-timing.tsv" = "57400f8e43c5962161e395ef7541ec4761a9b34cf8026064554c417137f64184" +"raw/jobs/getdp-f0004-b0003.json" = "aec80eee1c448a39a12ce4580d4f63c64a441f6ef18a368145ddf29476829ef0" +"raw/jobs/getdp-f0005-b0001-P.tsv" = "0195d231bec422564610e649f64e7c646f0c40508b789db03e5c9b96af01bfad" +"raw/jobs/getdp-f0005-b0001-Z.tsv" = "d59f8d9fac1686a569d277b27f0a0702decd9a2931a64b28cd0d55127cbfd09a" +"raw/jobs/getdp-f0005-b0001-timing.tsv" = "54cdacfc5f0b0dc43d04da0f509e6f00035787a94a673001afbf108a09466c9f" +"raw/jobs/getdp-f0005-b0001.json" = "8016e286965e2cc2fd7fa1ed116f1ecc54a8cfcbe852eb90f055da798a5d9666" +"raw/jobs/getdp-f0005-b0002-P.tsv" = "0d3a498655da1048906846d62e6168f8805e269122e3a9cf287f60df4f92e7bf" +"raw/jobs/getdp-f0005-b0002-Z.tsv" = "3ad7d164f2327fd8d0bfe86a734e779d5a46ca99fedbff01c823fb892090f44b" +"raw/jobs/getdp-f0005-b0002-timing.tsv" = "ca6bb49c1c85055742ad1b0dfde706bc99ebc5305682a4906dae1c22087e2b30" +"raw/jobs/getdp-f0005-b0002.json" = "2d3980e110ff2d1d2c84b6f67a91fa3c1efc6f07d4ed9c202d3c8030e6ad0639" +"raw/jobs/getdp-f0005-b0003-P.tsv" = "393b407d46d6f00ff603cf7c07eaaeca8e3fbb073ee1194e633bd4a7624f8f2c" +"raw/jobs/getdp-f0005-b0003-Z.tsv" = "14222044b68256889e1bddf12d1407ab7255fe9422061ab9a53bfe11fabffaae" +"raw/jobs/getdp-f0005-b0003-timing.tsv" = "175f49f83cd725e49534494110c4dbc5b9e667f2cada1f81c0916c7238344b91" +"raw/jobs/getdp-f0005-b0003.json" = "359965d2fbe026801921926b0adb10c0d6138746a2b4e272b6eba59e5d71db28" +"raw/jobs/getdp-f0006-b0001-P.tsv" = "a82402fe73b5a0c4da1a80757377e3ac43e2f50384f039e0a90257c465bf1ea0" +"raw/jobs/getdp-f0006-b0001-Z.tsv" = "7957113d319bff3dc7cc96746b79efb3212d09fa4dd35ecde0edf2997606c8ee" +"raw/jobs/getdp-f0006-b0001-timing.tsv" = "fff2a4084606e6dad3e90d75c1b270d79a527f43a364260ccd73d309c5d2d233" +"raw/jobs/getdp-f0006-b0001.json" = "e6e5ac8b4c99e1f99db84104f58c226e3e25f21b3abd3cf24b60c711c2649ac3" +"raw/jobs/getdp-f0006-b0002-P.tsv" = "c2751c83dbc41cedf1067fd0c40986ce6e83ffa8b45ccb0dc5fb935a3d309b68" +"raw/jobs/getdp-f0006-b0002-Z.tsv" = "c4486b646ad519626acb5ff787688a63fad719e202ea064fb9693f75ba84c714" +"raw/jobs/getdp-f0006-b0002-timing.tsv" = "005557eb52f47fe3ac08bc100bba81039d39ca05df70fb1f13b330efb8a0f43f" +"raw/jobs/getdp-f0006-b0002.json" = "a5ee955bc4d206343ba929782d22263c7bc10154a2fe8b9e987c0a0a4c3a280e" +"raw/jobs/getdp-f0006-b0003-P.tsv" = "a929d1a50d16f50b0ad5f8fb47d66e3ced5f55507e1c90d00af1e0776ba964de" +"raw/jobs/getdp-f0006-b0003-Z.tsv" = "fc017ac098faa689d6c9a16c5a7807f1657187a8230fb4a35f6eafdcb6e6327a" +"raw/jobs/getdp-f0006-b0003-timing.tsv" = "63d182bf9d710f7eb5314cd327c4ed4b75af5038567071a82a04a69c448b5cd1" +"raw/jobs/getdp-f0006-b0003.json" = "3e214a283723e55e7af11c3aba48967a0927e6afd0eee5d2f51dd00bba37d923" +"raw/jobs/getdp-f0007-b0001-P.tsv" = "9249112a255c56a6846ecb051da3b3bdf9261538e5ddd23b0d993ffcba378b79" +"raw/jobs/getdp-f0007-b0001-Z.tsv" = "d8f3054f6b1bd1d7260f02a51a12fd26b5710f84933cad1f514a4cffa067f19a" +"raw/jobs/getdp-f0007-b0001-timing.tsv" = "c8aabe04afcd8da27f4179c975c8a17f329eea77419c0bf41c5028f382957878" +"raw/jobs/getdp-f0007-b0001.json" = "6e7255066615a624e9424a6200ee05416d3f2d0bcd026b735c5ec1d3dc51c44e" +"raw/jobs/getdp-f0007-b0002-P.tsv" = "83e7b9bad4a20daf882ea57f0b6522b3d63fae68d8d5a7e88fb741ae114e3706" +"raw/jobs/getdp-f0007-b0002-Z.tsv" = "f3e1ef07ed1183eac3cf47374a77010b03dbdcb4553a5bdc87c79c57872483c3" +"raw/jobs/getdp-f0007-b0002-timing.tsv" = "4396c8a8a9dc1861c06b951b8a41334eaeb5049c4de36813cedaca7b96faf8d2" +"raw/jobs/getdp-f0007-b0002.json" = "9906a5fd78a06f014e1d719c76d34d321d6e11f21b694df92cf9878a10cdeda1" +"raw/jobs/getdp-f0007-b0003-P.tsv" = "b927cd849430d14e21cf567f47a0804d9d509b23fc7d68aa205f8bec6a621a1f" +"raw/jobs/getdp-f0007-b0003-Z.tsv" = "6ac3065659e8357f8f93d2951e040771a56d298f65843c3524be433d4dd5697e" +"raw/jobs/getdp-f0007-b0003-timing.tsv" = "c97cfa12ecb62cd515d72f0810059f972eb4314e0d54248963fe3dd7e71d0938" +"raw/jobs/getdp-f0007-b0003.json" = "163aec47d56a4c50a781fd284e0cb3f5c492be6c4a04f98efcccc42f5403c539" +"raw/jobs/getdp-f0008-b0001-P.tsv" = "1b5071aa6ae6390e09800c28347da82417b0fa2b2a3d61db90f9bd477231f789" +"raw/jobs/getdp-f0008-b0001-Z.tsv" = "dd382d525223d354279aa84f7459491d9b81c00eee3c9dbab10fb7bc8e84aea8" +"raw/jobs/getdp-f0008-b0001-timing.tsv" = "56dfd2998d962fef93128299ee10ce52cf45f7a69a844f07b7e729ad51a703d7" +"raw/jobs/getdp-f0008-b0001.json" = "2f4e8d6fb58e82c92921936bdc955886a0bfb273ec52f00b0b97bc4846414f59" +"raw/jobs/getdp-f0008-b0002-P.tsv" = "6e1068ab50509cc6ccc88ffa58da5125be06c744248ebd787f1a725a0d3ce19c" +"raw/jobs/getdp-f0008-b0002-Z.tsv" = "3219910dbd6b5b68a5f56630becc77e2427ba31e965fdcd3f3b2ba9d583eebfb" +"raw/jobs/getdp-f0008-b0002-timing.tsv" = "f645e39ee1dae92a40de8945d6854c29ff576aa4b90375f550aa3ef8d2175845" +"raw/jobs/getdp-f0008-b0002.json" = "bd0bde2bddeafb1415a2d8b54d22f51b18e464c0228affc00dd84d5a0f21a797" +"raw/jobs/getdp-f0008-b0003-P.tsv" = "e271103adf21200c4f34c3c7e3ad80bb11042de627738f0ba14defd006942a15" +"raw/jobs/getdp-f0008-b0003-Z.tsv" = "fef20b3ea2c6e03fbcd2ceb6acfdeaf0d3326e1e51e06f9762cb54d4acbdeb13" +"raw/jobs/getdp-f0008-b0003-timing.tsv" = "ad93931de6d8f1ca6e4481e29ba4fad116814b7a9f699670c846c22eb78ee814" +"raw/jobs/getdp-f0008-b0003.json" = "98efee8fe2cf64ae86eb6e2558bafd0346d4f74968178030a4fd103b96d58655" +"raw/jobs/getdp-f0009-b0001-P.tsv" = "2ee20b18c0c855a857ffa64a61d5ebbd7d0834abca56824221744b5b5f1f250d" +"raw/jobs/getdp-f0009-b0001-Z.tsv" = "24b9d2147f2172fbff8181910ad95a079f6c67a849e4f1e2eb9c3622470a7a94" +"raw/jobs/getdp-f0009-b0001-timing.tsv" = "2f10ee9aee2eeac69d6207e568e50d41a962f4560fe2d324b94c4dad48b73d14" +"raw/jobs/getdp-f0009-b0001.json" = "b8721c75a6b4125e1f8360f191f957461dfa7a8ad74eeebd35713f68278fc954" +"raw/jobs/getdp-f0009-b0002-P.tsv" = "a72b35bdb650f4fa23879e9b7e993657d19ac85eafce3ff14e40071f68284442" +"raw/jobs/getdp-f0009-b0002-Z.tsv" = "1b6dd8be9a57ce679e09d8f852dc5a9d659fdc25a7c6af41333f148c6640571f" +"raw/jobs/getdp-f0009-b0002-timing.tsv" = "537a16539c2d71dce3ce71ec3e3870e8d04b9c12928603f2e02fa221a3983e19" +"raw/jobs/getdp-f0009-b0002.json" = "eb22032e15510f33863b93a4449c035d02839d65820f38b1bb19689bc6b94654" +"raw/jobs/getdp-f0009-b0003-P.tsv" = "cf0156d87713c607ff736097e3939e4caf80c742f6bef29e21f3684dd14a16be" +"raw/jobs/getdp-f0009-b0003-Z.tsv" = "1141e3b846e0407089025c2310354fd992ee98592d76645c8547011189ead1f3" +"raw/jobs/getdp-f0009-b0003-timing.tsv" = "495a2a6840774175d78d9817a2f55ffcedc82da1a9b00fcb860a071f0669521f" +"raw/jobs/getdp-f0009-b0003.json" = "a22541ba0984d39981f6f32bbb28113cfdabf01b0f8e074d2f96ff43804bf028" +"raw/scan_complete.tsv" = "2cc9eff50bb7e6fff0e2555f96d93ebe952c4a1a158a1dca82fe6cca3aa1afab" +"run.json" = "ffdc3637591af411121db56364e2f2c04d6aeafc79d5c16a1200855cd0d62813" +"timing-summary.json" = "e442c67db38bc4f8c1a982a95cb6ffa37f4dbbb7afe9e7c6a2b1f61ab3e36f74" diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/fem/formulation.txt b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/fem/formulation.txt new file mode 100644 index 000000000..0e94e36ec --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/fem/formulation.txt @@ -0,0 +1 @@ +@NamedTuple{backend::Symbol, requested::NamedTuple, methods::NamedTuple, options::NamedTuple}((:fem, (insulation_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), semicon_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_properties = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), temperature_dependence = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing)), (insulation_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), semicon_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), earth_properties = (identifier = :constant, parameters = NamedTuple(), options = NamedTuple()), temperature_dependence = (identifier = :linear, parameters = NamedTuple(), options = NamedTuple())), (reduce_bundle = false, kron_reduction = false, ideal_transposition = false, physics = Symbol("quasi-tem")))) \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/fem/problem.json b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/fem/problem.json new file mode 100644 index 000000000..805425d6c --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/fem/problem.json @@ -0,0 +1,516 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "root": { + "kind": "line_parameters_problem", + "earth_props": { + "kind": "earth_model", + "vertical_layers": false, + "layers": [ + { + "kind": "earth_layer", + "thickness": { + "special": "Inf", + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "rho": { + "special": "Inf", + "__type__": "Float64" + } + }, + { + "kind": "earth_layer", + "thickness": { + "special": "Inf", + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "rho": { + "value": 0.1, + "__type__": "Float64" + } + } + ] + }, + "Gamma": null, + "frequencies": [ + { + "value": 0.1, + "__type__": "Float64" + }, + { + "value": 1, + "__type__": "Float64" + }, + { + "value": 10, + "__type__": "Float64" + }, + { + "value": 100, + "__type__": "Float64" + }, + { + "value": 1000, + "__type__": "Float64" + }, + { + "value": 10000, + "__type__": "Float64" + }, + { + "value": 100000, + "__type__": "Float64" + }, + { + "value": 1000000, + "__type__": "Float64" + }, + { + "value": 10000000, + "__type__": "Float64" + } + ], + "system": { + "kind": "line_cable_system", + "input_positions": [ + { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 1, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 2, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + } + ], + "clearances": [ + [ + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + } + ], + [ + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + } + ], + [ + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + } + ] + ], + "connections": [ + [ + 1 + ], + [ + 2 + ], + [ + 3 + ] + ], + "line_length": { + "value": 1, + "__type__": "Float64" + }, + "environment": null, + "system_id": "three_bare_wires_all_earth", + "positions": [ + { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 1, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 2, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + } + ], + "designs": [ + { + "kind": "cable_design", + "cable_id": "three_bare_wires_all_earth", + "nominal_data": [ + ], + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "core_metal", + "material": { + "value": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "type": "material" + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.0425, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + } + ] + } + }, + { + "kind": "cable_design", + "cable_id": "three_bare_wires_all_earth", + "nominal_data": [ + ], + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "core_metal", + "material": { + "value": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "type": "material" + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.0425, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + } + ] + } + }, + { + "kind": "cable_design", + "cable_id": "three_bare_wires_all_earth", + "nominal_data": [ + ], + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "core_metal", + "material": { + "value": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "type": "material" + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.0425, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + } + ] + } + } + ] + }, + "temperature": { + "value": 20, + "__type__": "Float64" + } + }, + "format": "linecablemodels.line_parameters_problem", + "version": "1.0.0" +} \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/unified/Y.csv b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/unified/Y.csv new file mode 100644 index 000000000..b9aae5ab5 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/unified/Y.csv @@ -0,0 +1,82 @@ +frequency_hz,response_terminal,basis_terminal,real_S_per_m,imaginary_S_per_m +0.10000000000000001,cable:1:core,cable:1:core,11.716539655942917,0.090920015180965472 +0.10000000000000001,cable:1:core,cable:2:core,-6.5764429134272966,0.064998853290850048 +0.10000000000000001,cable:1:core,cable:3:core,-3.2506966801752673,0.090585503665267256 +0.10000000000000001,cable:2:core,cable:1:core,-6.5764429128831035,0.064998860591277005 +0.10000000000000001,cable:2:core,cable:2:core,14.505974265861992,0.046571869010316257 +0.10000000000000001,cable:2:core,cable:3:core,-6.5764429128831114,0.064998860591280475 +0.10000000000000001,cable:3:core,cable:1:core,-3.2506966801752739,0.090585503665269226 +0.10000000000000001,cable:3:core,cable:2:core,-6.5764429134272895,0.064998853290850103 +0.10000000000000001,cable:3:core,cable:3:core,11.716539655942919,0.090920015180963779 +1,cable:1:core,cable:1:core,11.873714513026528,0.13877147446532462 +1,cable:1:core,cable:2:core,-6.4640237929904574,0.098479018213286862 +1,cable:1:core,cable:3:core,-3.0939192122953183,0.13607439977272814 +1,cable:2:core,cable:1:core,-6.464023785078119,0.098479081846053071 +1,cable:2:core,cable:2:core,14.586483697135661,0.070755812768684048 +1,cable:2:core,cable:3:core,-6.4640237850781137,0.098479081846052835 +1,cable:3:core,cable:1:core,-3.0939192122953187,0.13607439977272792 +1,cable:3:core,cable:2:core,-6.4640237929904538,0.098479018213287278 +1,cable:3:core,cable:3:core,11.873714513026522,0.13877147446532426 +10,cable:1:core,cable:1:core,12.120029298528864,0.24051657223890441 +10,cable:1:core,cable:2:core,-6.2890199013664025,0.16552322577943707 +10,cable:1:core,cable:3:core,-2.8515486558341587,0.22001574158959175 +10,cable:2:core,cable:1:core,-6.2890197833970136,0.16552372276043659 +10,cable:2:core,cable:2:core,14.711826696535178,0.12082715757887073 +10,cable:2:core,cable:3:core,-6.289019783397011,0.16552372276043406 +10,cable:3:core,cable:1:core,-2.8515486558341592,0.22001574158959078 +10,cable:3:core,cable:2:core,-6.2890199013663972,0.16552322577943612 +10,cable:3:core,cable:3:core,12.120029298528857,0.24051657223890555 +100,cable:1:core,cable:1:core,12.563025703739317,0.51721927479147634 +100,cable:1:core,cable:2:core,-5.9850006954373791,0.32573071565508882 +100,cable:1:core,cable:3:core,-2.4465142782902065,0.37618640379320623 +100,cable:2:core,cable:1:core,-5.9849990645039268,0.32573357082855753 +100,cable:2:core,cable:2:core,14.929890298544413,0.25587575596022161 +100,cable:2:core,cable:3:core,-5.9849990645039286,0.32573357082855769 +100,cable:3:core,cable:1:core,-2.4465142782902083,0.37618640379320556 +100,cable:3:core,cable:2:core,-5.9850006954373818,0.32573071565508926 +100,cable:3:core,cable:3:core,12.563025703739322,0.51721927479147711 +1000,cable:1:core,cable:1:core,13.595998739240571,1.4465325051151292 +1000,cable:1:core,cable:2:core,-5.3532086996206329,0.79705038235174319 +1000,cable:1:core,cable:3:core,-1.7399318837814481,0.63681540275489368 +1000,cable:2:core,cable:1:core,-5.3531955340018831,0.79705183192889983 +1000,cable:2:core,cable:2:core,15.394199476046927,0.79008767845696459 +1000,cable:2:core,cable:3:core,-5.3531955340018857,0.7970518319289005 +1000,cable:3:core,cable:1:core,-1.7399318837814481,0.6368154027548929 +1000,cable:3:core,cable:2:core,-5.3532086996206329,0.79705038235174441 +1000,cable:3:core,cable:3:core,13.595998739240573,1.446532505115127 +10000,cable:1:core,cable:1:core,17.04653455296819,4.2120541635757602 +10000,cable:1:core,cable:2:core,-3.3892442630087292,2.0891967225412542 +10000,cable:1:core,cable:3:core,-0.43825470996925953,0.85392294859473339 +10000,cable:2:core,cable:1:core,-3.3892834290076714,2.0891704641801838 +10000,cable:2:core,cable:2:core,17.284752427181022,3.3655305049304496 +10000,cable:2:core,cable:3:core,-3.3892834290076705,2.0891704641801834 +10000,cable:3:core,cable:1:core,-0.43825470996925953,0.85392294859473372 +10000,cable:3:core,cable:2:core,-3.3892442630087296,2.0891967225412542 +10000,cable:3:core,cable:3:core,17.04653455296819,4.2120541635757611 +100000,cable:1:core,cable:1:core,25.114009604140684,8.0616329121337866 +100000,cable:1:core,cable:2:core,0.17423938863795493,1.051104411298325 +100000,cable:1:core,cable:3:core,0.049169280797892866,-0.045706655302347673 +100000,cable:2:core,cable:1:core,0.17424088729365159,1.0510922187905325 +100000,cable:2:core,cable:2:core,25.080124615561157,8.0878898568648587 +100000,cable:2:core,cable:3:core,0.17424088729365153,1.0510922187905325 +100000,cable:3:core,cable:1:core,0.049169280797892859,-0.045706655302347653 +100000,cable:3:core,cable:2:core,0.17423938863795507,1.051104411298325 +100000,cable:3:core,cable:3:core,25.114009604140684,8.0616329121337849 +1000000,cable:1:core,cable:1:core,40.833774626185878,19.549173406842751 +1000000,cable:1:core,cable:2:core,-0.020403824114795521,-0.018991043972656708 +1000000,cable:1:core,cable:3:core,-1.7943027849736949e-05,-1.1772645727263113e-05 +1000000,cable:2:core,cable:1:core,-0.020403824161670955,-0.018991043990879461 +1000000,cable:2:core,cable:2:core,40.833783909159962,19.549186896797089 +1000000,cable:2:core,cable:3:core,-0.020403824161670952,-0.018991043990879461 +1000000,cable:3:core,cable:1:core,-1.7943027849736942e-05,-1.1772645727263105e-05 +1000000,cable:3:core,cable:2:core,-0.020403824114795518,-0.018991043972656704 +1000000,cable:3:core,cable:3:core,40.833774626185878,19.549173406842755 +10000000,cable:1:core,cable:1:core,80.846531351828517,55.108195401013525 +10000000,cable:1:core,cable:2:core,-9.26641813988093e-08,-1.3479772233800525e-07 +10000000,cable:1:core,cable:3:core,-1.8002386369701979e-16,1.9542236956570295e-16 +10000000,cable:2:core,cable:1:core,-9.2664181398809313e-08,-1.3479772233800525e-07 +10000000,cable:2:core,cable:2:core,80.846531351828517,55.108195401013525 +10000000,cable:2:core,cable:3:core,-9.26641813988093e-08,-1.3479772233800525e-07 +10000000,cable:3:core,cable:1:core,-1.8002386369701972e-16,1.9542236956570285e-16 +10000000,cable:3:core,cable:2:core,-9.2664181398809313e-08,-1.3479772233800525e-07 +10000000,cable:3:core,cable:3:core,80.846531351828517,55.108195401013525 diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/unified/Z.csv b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/unified/Z.csv new file mode 100644 index 000000000..bd54f81bc --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/unified/Z.csv @@ -0,0 +1,82 @@ +frequency_hz,response_terminal,basis_terminal,real_ohm_per_m,imaginary_ohm_per_m +0.10000000000000001,cable:1:core,cable:1:core,3.1374615215203507e-06,1.2435841298035035e-06 +0.10000000000000001,cable:1:core,cable:2:core,9.9023642057030489e-08,8.152914988453947e-07 +0.10000000000000001,cable:1:core,cable:3:core,9.9020797253944635e-08,7.2818834853831638e-07 +0.10000000000000001,cable:2:core,cable:1:core,9.9023642057030476e-08,8.1529149884539449e-07 +0.10000000000000001,cable:2:core,cable:2:core,3.1374615138914718e-06,1.2435841307826217e-06 +0.10000000000000001,cable:2:core,cable:3:core,9.9023642057030476e-08,8.1529149884539449e-07 +0.10000000000000001,cable:3:core,cable:1:core,9.9020797253944648e-08,7.2818834853831638e-07 +0.10000000000000001,cable:3:core,cable:2:core,9.9023642057030489e-08,8.152914988453946e-07 +0.10000000000000001,cable:3:core,cable:3:core,3.1374615215203507e-06,1.2435841298035035e-06 +1,cable:1:core,cable:1:core,4.0462600939189881e-06,1.0981371245585021e-05 +1,cable:1:core,cable:2:core,9.9703279749934944e-07,6.6990063703023541e-06 +1,cable:1:core,cable:3:core,9.9679142261440609e-07,5.8280015972434844e-06 +1,cable:2:core,cable:1:core,9.9703279749934965e-07,6.6990063703023558e-06 +1,cable:2:core,cable:2:core,4.0462594747967589e-06,1.09813713443021e-05 +1,cable:2:core,cable:3:core,9.9703279749934965e-07,6.6990063703023549e-06 +1,cable:3:core,cable:1:core,9.9679142261440587e-07,5.8280015972434844e-06 +1,cable:3:core,cable:2:core,9.9703279749934944e-07,6.6990063703023541e-06 +1,cable:3:core,cable:3:core,4.0462600939189881e-06,1.0981371245585021e-05 +10,cable:1:core,cable:1:core,1.405955851781232e-05,9.4699427403525043e-05 +10,cable:1:core,cable:2:core,1.0164608279866875e-05,5.2299588182049292e-05 +10,cable:1:core,cable:3:core,1.0144854518138249e-05,4.3592310471018413e-05 +10,cable:2:core,cable:1:core,1.0164608279866877e-05,5.2299588182049285e-05 +10,cable:2:core,cable:2:core,1.40595111609882e-05,9.4699437546059073e-05 +10,cable:2:core,cable:3:core,1.0164608279866877e-05,5.2299588182049305e-05 +10,cable:3:core,cable:1:core,1.0144854518138247e-05,4.3592310471018407e-05 +10,cable:3:core,cable:2:core,1.0164608279866875e-05,5.2299588182049292e-05 +10,cable:3:core,cable:3:core,1.405955851781232e-05,9.4699427403525057e-05 +100,cable:1:core,cable:1:core,0.00011749986341587662,0.00077809983118912608 +100,cable:1:core,cable:2:core,0.00010621027609229646,0.00037161720315773177 +100,cable:1:core,cable:3:core,0.00010469597101762054,0.00028484792099649496 +100,cable:2:core,cable:1:core,0.00010621027609229646,0.00037161720315773177 +100,cable:2:core,cable:2:core,0.0001174966493547314,0.00077810092618215505 +100,cable:2:core,cable:3:core,0.00010621027609229645,0.00037161720315773166 +100,cable:3:core,cable:1:core,0.00010469597101762056,0.00028484792099649502 +100,cable:3:core,cable:2:core,0.00010621027609229646,0.00037161720315773177 +100,cable:3:core,cable:3:core,0.00011749986341587659,0.00077809983118912597 +1000,cable:1:core,cable:1:core,0.0011874959966138036,0.0060823308476815122 +1000,cable:1:core,cable:2:core,0.0011019671033492449,0.0020963802241269901 +1000,cable:1:core,cable:3:core,0.0010031065055049382,0.0012640618611339432 +1000,cable:2:core,cable:1:core,0.0011019671033492447,0.0020963802241269897 +1000,cable:2:core,cable:2:core,0.0011873491447126174,0.006082449166987444 +1000,cable:2:core,cable:3:core,0.0011019671033492449,0.0020963802241269905 +1000,cable:3:core,cable:1:core,0.001003106505504938,0.0012640618611339429 +1000,cable:3:core,cable:2:core,0.0011019671033492449,0.0020963802241269901 +1000,cable:3:core,cable:3:core,0.0011874959966138039,0.0060823308476815122 +10000,cable:1:core,cable:1:core,0.011218478577540311,0.043047522284281221 +10000,cable:1:core,cable:2:core,0.0080360922846632096,0.004867758641204061 +10000,cable:1:core,cable:3:core,0.0043864669830384383,-0.00021545006964465192 +10000,cable:2:core,cable:1:core,0.0080360922846632113,0.0048677586412040601 +10000,cable:2:core,cable:2:core,0.011219720413915016,0.043052062902262878 +10000,cable:2:core,cable:3:core,0.0080360922846632078,0.004867758641204061 +10000,cable:3:core,cable:1:core,0.0043864669830384383,-0.00021545006964465192 +10000,cable:3:core,cable:2:core,0.0080360922846632096,0.004867758641204061 +10000,cable:3:core,cable:3:core,0.011218478577540311,0.043047522284281221 +100000,cable:1:core,cable:1:core,0.091870290228580009,0.28525847944837568 +100000,cable:1:core,cable:2:core,0.0087273397177971886,-0.0087082547107099739 +100000,cable:1:core,cable:3:core,-0.0014312810262812373,-0.00036817498716284683 +100000,cable:2:core,cable:1:core,0.0087273397177971886,-0.0087082547107099739 +100000,cable:2:core,cable:2:core,0.091870046795798257,0.28524984365980688 +100000,cable:2:core,cable:3:core,0.0087273397177971886,-0.0087082547107099756 +100000,cable:3:core,cable:1:core,-0.0014312810262812373,-0.00036817498716284683 +100000,cable:3:core,cable:2:core,0.0087273397177971869,-0.0087082547107099739 +100000,cable:3:core,cable:3:core,0.091870290228580009,0.28525847944837579 +1000000,cable:1:core,cable:1:core,0.7540744079244075,1.574047945066307 +1000000,cable:1:core,cable:2:core,0.00015357089839005441,0.0010629809539592517 +1000000,cable:1:core,cable:3:core,1.8110903613483902e-07,1.4306474614933575e-06 +1000000,cable:2:core,cable:1:core,0.00015357089839005439,0.0010629809539592513 +1000000,cable:2:core,cable:2:core,0.75407435015734658,1.5740479687634792 +1000000,cable:2:core,cable:3:core,0.00015357089839005436,0.0010629809539592515 +1000000,cable:3:core,cable:1:core,1.8110903613483902e-07,1.4306474614933575e-06 +1000000,cable:3:core,cable:2:core,0.00015357089839005441,0.0010629809539592517 +1000000,cable:3:core,cable:3:core,0.7540744079244075,1.5740479450663067 +10000000,cable:1:core,cable:1:core,4.5479347057971937,6.6714050104071516 +10000000,cable:1:core,cable:2:core,3.0492342492444656e-09,1.3142318645708507e-08 +10000000,cable:1:core,cable:3:core,1.6548264118882764e-17,1.2699296335776218e-17 +10000000,cable:2:core,cable:1:core,3.0492342492444656e-09,1.3142318645708507e-08 +10000000,cable:2:core,cable:2:core,4.5479347057971937,6.6714050104071516 +10000000,cable:2:core,cable:3:core,3.0492342492444656e-09,1.3142318645708507e-08 +10000000,cable:3:core,cable:1:core,1.6548264118882761e-17,1.2699296335776215e-17 +10000000,cable:3:core,cable:2:core,3.0492342492444656e-09,1.3142318645708507e-08 +10000000,cable:3:core,cable:3:core,4.5479347057971937,6.6714050104071516 diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/unified/computation-options.txt b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/unified/computation-options.txt new file mode 100644 index 000000000..3c73cbbf9 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/unified/computation-options.txt @@ -0,0 +1 @@ +(trace = true, output_basis = :pul) \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/unified/details.txt b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/unified/details.txt new file mode 100644 index 000000000..a448be70f --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/unified/details.txt @@ -0,0 +1 @@ +@NamedTuple{shunt_model::NamedTuple, trace::@NamedTuple{phase_map::Vector{Int64}, cable_map::Vector{Int64}, Zin::Array{ComplexF64, 3}, Pin::Array{ComplexF64, 3}, Zg::Array{ComplexF64, 3}, Pg::Array{ComplexF64, 3}, Z::Array{ComplexF64, 3}, P::Array{ComplexF64, 3}}, formulations::@NamedTuple{effective::@NamedTuple{internal_impedance::Union{Nothing, Symbol, NamedTuple}, insulation_impedance::Union{Nothing, Symbol, NamedTuple}, earth_impedance::Union{Nothing, Symbol, NamedTuple}, shunt_model::Union{Nothing, Symbol, NamedTuple}, insulation_admittance::Union{Nothing, Symbol, NamedTuple}, semicon_admittance::Union{Nothing, Symbol, NamedTuple}, earth_admittance::Union{Nothing, Symbol, NamedTuple}, earth_properties::Union{Nothing, Symbol, NamedTuple}, pipe_impedance::Union{Nothing, Symbol, NamedTuple}, temperature_dependence::Union{Nothing, Symbol, NamedTuple}}, numerical::@NamedTuple{internal_impedance::NamedTuple, insulation_impedance::NamedTuple, shunt_model::NamedTuple, insulation_admittance::NamedTuple, semicon_admittance::NamedTuple, earth_properties::NamedTuple, temperature_dependence::NamedTuple, earth_impedance::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}, earth_admittance::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}, equivalent_earth::@NamedTuple{earth_impedance::Union{Nothing, @NamedTuple{air::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, mixed::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth_admittance::Union{Nothing, @NamedTuple{air::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, mixed::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, backend::Symbol, requested::NamedTuple, methods::NamedTuple, options::NamedTuple}, coordinates::Vector{String}}(((requested = :coaxial, effective = :coaxial, solves = 0, domains = @NamedTuple{design::Int64, terminals::UnitRange{Int64}, requested::Symbol, effective::Symbol, reason::Symbol, message::String}[], diagnostics = @NamedTuple{boundary_residual::Union{Nothing, Float64}, wire_residual::Union{Nothing, Float64}, tape_residual::Union{Nothing, Float64}, common_residual::Union{Nothing, Float64}, penetration_indicator::Union{Nothing, Float64}, reciprocity::Float64, log_moment_error::Float64, unknowns::Int64, equations::Int64, matrix_bytes::Int64, level::@NamedTuple{wire::Int64, order::Int64, quadrature::Int64, modes::Int64}}[]), (phase_map = [1, 2, 3], cable_map = [1, 2, 3], Zin = ComplexF64[3.0384367161282473e-6 + 3.1415178272814096e-8im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 3.0384367161282473e-6 + 3.1415178272814096e-8im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 3.0384367161282473e-6 + 3.1415178272814096e-8im;;; 3.0491254273478614e-6 + 3.135993190598609e-7im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 3.0491254273478614e-6 + 3.135993190598609e-7im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 3.0491254273478614e-6 + 3.135993190598609e-7im;;; 3.886234398887704e-6 + 2.71311425444745e-6im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 3.886234398887704e-6 + 2.71311425444745e-6im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 3.886234398887704e-6 + 2.71311425444745e-6im;;; 1.0572845476093172e-5 + 9.718155377880437e-6im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 1.0572845476093172e-5 + 9.718155377880437e-6im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 1.0572845476093172e-5 + 9.718155377880437e-6im;;; 3.1668762815475746e-5 + 3.088048172738763e-5im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 3.1668762815475746e-5 + 3.088048172738763e-5im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 3.1668762815475746e-5 + 3.088048172738763e-5im;;; 9.846320725460222e-5 + 9.76946980065643e-5im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 9.846320725460222e-5 + 9.76946980065643e-5im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 9.846320725460222e-5 + 9.76946980065643e-5im;;; 0.0003097129699189164 + 0.0003089505796854997im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0003097129699189164 + 0.0003089505796854997im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0003097129699189164 + 0.0003089505796854997im;;; 0.0009777519922518445 + 0.0009769915236240853im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0009777519922518445 + 0.0009769915236240853im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0009777519922518445 + 0.0009769915236240853im;;; 0.0030902795943006773 + 0.0030895197319977846im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0030902795943006773 + 0.0030895197319977846im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0030902795943006773 + 0.0030895197319977846im], Pin = ComplexF64[0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im;;; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im; 0.0 + 0.0im 0.0 + 0.0im 0.0 + 0.0im], Zg = ComplexF64[9.902480539210339e-8 + 1.2121689515306894e-6im 9.902364205703049e-8 + 8.152914988453947e-7im 9.902079725394464e-8 + 7.281883485383164e-7im; 9.902364205703048e-8 + 8.152914988453945e-7im 9.902479776322432e-8 + 1.2121689525098076e-6im 9.902364205703048e-8 + 8.152914988453945e-7im; 9.902079725394465e-8 + 7.281883485383164e-7im 9.902364205703049e-8 + 8.152914988453946e-7im 9.90248053921034e-8 + 1.2121689515306894e-6im;;; 9.97134666571127e-7 + 1.066777192652516e-5im 9.970327974993494e-7 + 6.699006370302354e-6im 9.96791422614406e-7 + 5.828001597243484e-6im; 9.970327974993496e-7 + 6.699006370302356e-6im 9.971340474488975e-7 + 1.0667772025242239e-5im 9.970327974993496e-7 + 6.699006370302355e-6im; 9.967914226144059e-7 + 5.828001597243484e-6im 9.970327974993494e-7 + 6.699006370302354e-6im 9.97134666571127e-7 + 1.066777192652516e-5im;;; 1.0173324118924617e-5 + 9.198631314907759e-5im 1.0164608279866875e-5 + 5.229958818204929e-5im 1.0144854518138249e-5 + 4.3592310471018413e-5im; 1.0164608279866877e-5 + 5.2299588182049285e-5im 1.0173276762100495e-5 + 9.198632329161162e-5im 1.0164608279866877e-5 + 5.2299588182049305e-5im; 1.0144854518138247e-5 + 4.359231047101841e-5im 1.0164608279866875e-5 + 5.229958818204929e-5im 1.0173324118924617e-5 + 9.19863131490776e-5im;;; 0.00010692701793978345 + 0.0007683816758112457im 0.00010621027609229646 + 0.00037161720315773177im 0.00010469597101762054 + 0.00028484792099649496im; 0.00010621027609229646 + 0.00037161720315773177im 0.00010692380387863824 + 0.0007683827708042746im 0.00010621027609229645 + 0.00037161720315773166im; 0.00010469597101762056 + 0.000284847920996495im 0.00010621027609229646 + 0.00037161720315773177im 0.00010692701793978342 + 0.0007683816758112456im;;; 0.0011558272337983278 + 0.0060514503659541245im 0.0011019671033492449 + 0.00209638022412699im 0.0010031065055049382 + 0.0012640618611339432im; 0.0011019671033492447 + 0.0020963802241269897im 0.0011556803818971416 + 0.006051568685260056im 0.0011019671033492449 + 0.0020963802241269905im; 0.001003106505504938 + 0.001264061861133943im 0.0011019671033492449 + 0.00209638022412699im 0.001155827233798328 + 0.0060514503659541245im;;; 0.011120015370285709 + 0.04294982758627466im 0.00803609228466321 + 0.004867758641204061im 0.004386466983038438 - 0.00021545006964465192im; 0.008036092284663211 + 0.00486775864120406im 0.011121257206660413 + 0.04295436820425631im 0.008036092284663208 + 0.004867758641204061im; 0.004386466983038438 - 0.00021545006964465192im 0.00803609228466321 + 0.004867758641204061im 0.011120015370285709 + 0.04294982758627466im;;; 0.0915605772586611 + 0.2849495288686902im 0.008727339717797189 - 0.008708254710709974im -0.0014312810262812373 - 0.00036817498716284683im; 0.008727339717797189 - 0.008708254710709974im 0.09156033382587934 + 0.2849408930801214im 0.008727339717797189 - 0.008708254710709976im; -0.0014312810262812373 - 0.00036817498716284683im 0.008727339717797187 - 0.008708254710709974im 0.0915605772586611 + 0.2849495288686903im;;; 0.7530966559321557 + 1.5730709535426828im 0.00015357089839005441 + 0.0010629809539592517im 1.8110903613483902e-7 + 1.4306474614933575e-6im; 0.0001535708983900544 + 0.0010629809539592513im 0.7530965981650948 + 1.573070977239855im 0.00015357089839005436 + 0.0010629809539592515im; 1.8110903613483902e-7 + 1.4306474614933575e-6im 0.00015357089839005441 + 0.0010629809539592517im 0.7530966559321557 + 1.5730709535426826im;;; 4.544844426202893 + 6.668315490675154im 3.0492342492444656e-9 + 1.3142318645708507e-8im 1.6548264118882764e-17 + 1.2699296335776218e-17im; 3.0492342492444656e-9 + 1.3142318645708507e-8im 4.544844426202893 + 6.668315490675154im 3.0492342492444656e-9 + 1.3142318645708507e-8im; 1.654826411888276e-17 + 1.2699296335776215e-17im 3.0492342492444656e-9 + 1.3142318645708507e-8im 4.544844426202893 + 6.668315490675154im], Pg = ComplexF64[0.01570744590381686 + 0.14446168050907676im 0.015707195121255586 + 0.11176348180526213im 0.015706507677288154 + 0.10248208479120303im; 0.015707195244274844 + 0.11176348177979348im 0.015707444967896125 + 0.14446168063891032im 0.015707195244274847 + 0.11176348177979348im; 0.015706507677288158 + 0.10248208479120305im 0.015707195121255586 + 0.11176348180526212im 0.015707445903816855 + 0.14446168050907673im;;; 0.1570376019738798 + 1.2143642119306588im 0.1570170801518872 + 0.8873850219186742im 0.1569619593588623 + 0.794579413713952im; 0.15701708873333825 + 0.887385019417503im 0.15703752742824928 + 1.2143642249710467im 0.15701708873333822 + 0.887385019417503im; 0.1569619593588623 + 0.794579413713952im 0.1570170801518872 + 0.8873850219186742im 0.15703760197387978 + 1.2143642119306588im;;; 1.567552536202268 + 9.841641163472342im 1.5659559359352924 + 6.572128601763149im 1.5618058780905852 + 5.644906827782586im; 1.565956432072304 + 6.57212836779376im 1.5675469994857574 + 9.841642488639579im 1.5659564320723038 + 6.572128367793763im; 1.5618058780905846 + 5.644906827782585im 1.5659559359352921 + 6.572128601763149im 1.5675525362022684 + 9.841641163472346im;;; 15.478295476396786 + 75.44782101559731im 15.364131891033537 + 42.78036457014046im 15.084992611839734 + 33.590238249036894im; 15.364149095250482 + 42.78034651914047im 15.477939278741466 + 75.44795946624511im 15.364149095250479 + 42.78034651914046im; 15.084992611839736 + 33.590238249036915im 15.364131891033537 + 42.78036457014046im 15.478295476396783 + 75.44782101559728im;;; 143.3681936268939 + 529.5796062713804im 136.44148270271506 + 205.50717041477057im 121.7965165191444 + 121.01500687935756im; 136.44128786247003 + 205.50659903614968im 143.3540318564716 + 529.5930673042715im 136.44128786247003 + 205.50659903614974im; 121.79651651914438 + 121.01500687935754im 136.44148270271506 + 205.5071704147706im 143.36819362689388 + 529.5796062713805im;;; 1030.5570468033618 + 3387.239524970035im 739.0331668451581 + 336.2291077840597im 364.9387464917255 - 78.43675908467455im; 739.0331746610495 + 336.23872375594703im 1030.712195817392 + 3387.620328375071im 739.0331746610494 + 336.238723755947im; 364.93874649172557 - 78.43675908467458im 739.0331668451581 + 336.2291077840597im 1030.5570468033618 + 3387.239524970035im;;; 7244.080680301634 + 22669.341604306504im 681.2178812753247 - 676.569347969176im -96.22754162788127 - 24.126700113678233im; 681.2081423711342 - 676.5640856325163im 7244.064424999822 + 22668.669741703314im 681.2081423711344 - 676.5640856325163im; -96.22754162788125 - 24.126700113678236im 681.2178812753247 - 676.569347969176im 7244.080680301634 + 22669.341604306508im;;; 59930.239966361274 + 125180.70294016767im 12.216539019807177 + 84.57413678394165im 0.01185549520058545 + 0.11455061109107394im; 12.216539096714744 + 84.57413691756922im 59930.23537020177 + 125180.70482547453im 12.21653909671475 + 84.57413691756922im; 0.011855495200585459 + 0.11455061109107392im 12.216539019807177 + 84.57413678394165im 59930.23996636129 + 125180.70294016768im;;; 361696.7481463518 + 530627.5641232601im 0.00024270853323619268 + 0.0010458189856197416im 1.3142315450927868e-12 + 1.0148064016559267e-12im; 0.00024270853323619279 + 0.0010458189856197416im 361696.7481463518 + 530627.5641232601im 0.00024270853323619268 + 0.0010458189856197416im; 1.3142315450927868e-12 + 1.0148064016559267e-12im 0.00024270853323619279 + 0.0010458189856197416im 361696.7481463518 + 530627.5641232601im], Z = ComplexF64[3.1374615215203507e-6 + 1.2435841298035035e-6im 9.902364205703049e-8 + 8.152914988453947e-7im 9.902079725394464e-8 + 7.281883485383164e-7im; 9.902364205703048e-8 + 8.152914988453945e-7im 3.1374615138914718e-6 + 1.2435841307826217e-6im 9.902364205703048e-8 + 8.152914988453945e-7im; 9.902079725394465e-8 + 7.281883485383164e-7im 9.902364205703049e-8 + 8.152914988453946e-7im 3.1374615215203507e-6 + 1.2435841298035035e-6im;;; 4.046260093918988e-6 + 1.0981371245585021e-5im 9.970327974993494e-7 + 6.699006370302354e-6im 9.96791422614406e-7 + 5.828001597243484e-6im; 9.970327974993496e-7 + 6.699006370302356e-6im 4.046259474796759e-6 + 1.09813713443021e-5im 9.970327974993496e-7 + 6.699006370302355e-6im; 9.967914226144059e-7 + 5.828001597243484e-6im 9.970327974993494e-7 + 6.699006370302354e-6im 4.046260093918988e-6 + 1.0981371245585021e-5im;;; 1.405955851781232e-5 + 9.469942740352504e-5im 1.0164608279866875e-5 + 5.229958818204929e-5im 1.0144854518138249e-5 + 4.3592310471018413e-5im; 1.0164608279866877e-5 + 5.2299588182049285e-5im 1.40595111609882e-5 + 9.469943754605907e-5im 1.0164608279866877e-5 + 5.2299588182049305e-5im; 1.0144854518138247e-5 + 4.359231047101841e-5im 1.0164608279866875e-5 + 5.229958818204929e-5im 1.405955851781232e-5 + 9.469942740352506e-5im;;; 0.00011749986341587662 + 0.0007780998311891261im 0.00010621027609229646 + 0.00037161720315773177im 0.00010469597101762054 + 0.00028484792099649496im; 0.00010621027609229646 + 0.00037161720315773177im 0.0001174966493547314 + 0.000778100926182155im 0.00010621027609229645 + 0.00037161720315773166im; 0.00010469597101762056 + 0.000284847920996495im 0.00010621027609229646 + 0.00037161720315773177im 0.00011749986341587659 + 0.000778099831189126im;;; 0.0011874959966138036 + 0.006082330847681512im 0.0011019671033492449 + 0.00209638022412699im 0.0010031065055049382 + 0.0012640618611339432im; 0.0011019671033492447 + 0.0020963802241269897im 0.0011873491447126174 + 0.006082449166987444im 0.0011019671033492449 + 0.0020963802241269905im; 0.001003106505504938 + 0.001264061861133943im 0.0011019671033492449 + 0.00209638022412699im 0.0011874959966138039 + 0.006082330847681512im;;; 0.011218478577540311 + 0.04304752228428122im 0.00803609228466321 + 0.004867758641204061im 0.004386466983038438 - 0.00021545006964465192im; 0.008036092284663211 + 0.00486775864120406im 0.011219720413915016 + 0.04305206290226288im 0.008036092284663208 + 0.004867758641204061im; 0.004386466983038438 - 0.00021545006964465192im 0.00803609228466321 + 0.004867758641204061im 0.011218478577540311 + 0.04304752228428122im;;; 0.09187029022858001 + 0.2852584794483757im 0.008727339717797189 - 0.008708254710709974im -0.0014312810262812373 - 0.00036817498716284683im; 0.008727339717797189 - 0.008708254710709974im 0.09187004679579826 + 0.2852498436598069im 0.008727339717797189 - 0.008708254710709976im; -0.0014312810262812373 - 0.00036817498716284683im 0.008727339717797187 - 0.008708254710709974im 0.09187029022858001 + 0.2852584794483758im;;; 0.7540744079244075 + 1.574047945066307im 0.00015357089839005441 + 0.0010629809539592517im 1.8110903613483902e-7 + 1.4306474614933575e-6im; 0.0001535708983900544 + 0.0010629809539592513im 0.7540743501573466 + 1.5740479687634792im 0.00015357089839005436 + 0.0010629809539592515im; 1.8110903613483902e-7 + 1.4306474614933575e-6im 0.00015357089839005441 + 0.0010629809539592517im 0.7540744079244075 + 1.5740479450663067im;;; 4.547934705797194 + 6.671405010407152im 3.0492342492444656e-9 + 1.3142318645708507e-8im 1.6548264118882764e-17 + 1.2699296335776218e-17im; 3.0492342492444656e-9 + 1.3142318645708507e-8im 4.547934705797194 + 6.671405010407152im 3.0492342492444656e-9 + 1.3142318645708507e-8im; 1.654826411888276e-17 + 1.2699296335776215e-17im 3.0492342492444656e-9 + 1.3142318645708507e-8im 4.547934705797194 + 6.671405010407152im], P = ComplexF64[0.01570744590381686 + 0.14446168050907676im 0.015707195121255586 + 0.11176348180526213im 0.015706507677288154 + 0.10248208479120303im; 0.015707195244274844 + 0.11176348177979348im 0.015707444967896125 + 0.14446168063891032im 0.015707195244274847 + 0.11176348177979348im; 0.015706507677288158 + 0.10248208479120305im 0.015707195121255586 + 0.11176348180526212im 0.015707445903816855 + 0.14446168050907673im;;; 0.1570376019738798 + 1.2143642119306588im 0.1570170801518872 + 0.8873850219186742im 0.1569619593588623 + 0.794579413713952im; 0.15701708873333825 + 0.887385019417503im 0.15703752742824928 + 1.2143642249710467im 0.15701708873333822 + 0.887385019417503im; 0.1569619593588623 + 0.794579413713952im 0.1570170801518872 + 0.8873850219186742im 0.15703760197387978 + 1.2143642119306588im;;; 1.567552536202268 + 9.841641163472342im 1.5659559359352924 + 6.572128601763149im 1.5618058780905852 + 5.644906827782586im; 1.565956432072304 + 6.57212836779376im 1.5675469994857574 + 9.841642488639579im 1.5659564320723038 + 6.572128367793763im; 1.5618058780905846 + 5.644906827782585im 1.5659559359352921 + 6.572128601763149im 1.5675525362022684 + 9.841641163472346im;;; 15.478295476396786 + 75.44782101559731im 15.364131891033537 + 42.78036457014046im 15.084992611839734 + 33.590238249036894im; 15.364149095250482 + 42.78034651914047im 15.477939278741466 + 75.44795946624511im 15.364149095250479 + 42.78034651914046im; 15.084992611839736 + 33.590238249036915im 15.364131891033537 + 42.78036457014046im 15.478295476396783 + 75.44782101559728im;;; 143.3681936268939 + 529.5796062713804im 136.44148270271506 + 205.50717041477057im 121.7965165191444 + 121.01500687935756im; 136.44128786247003 + 205.50659903614968im 143.3540318564716 + 529.5930673042715im 136.44128786247003 + 205.50659903614974im; 121.79651651914438 + 121.01500687935754im 136.44148270271506 + 205.5071704147706im 143.36819362689388 + 529.5796062713805im;;; 1030.5570468033618 + 3387.239524970035im 739.0331668451581 + 336.2291077840597im 364.9387464917255 - 78.43675908467455im; 739.0331746610495 + 336.23872375594703im 1030.712195817392 + 3387.620328375071im 739.0331746610494 + 336.238723755947im; 364.93874649172557 - 78.43675908467458im 739.0331668451581 + 336.2291077840597im 1030.5570468033618 + 3387.239524970035im;;; 7244.080680301634 + 22669.341604306504im 681.2178812753247 - 676.569347969176im -96.22754162788127 - 24.126700113678233im; 681.2081423711342 - 676.5640856325163im 7244.064424999822 + 22668.669741703314im 681.2081423711344 - 676.5640856325163im; -96.22754162788125 - 24.126700113678236im 681.2178812753247 - 676.569347969176im 7244.080680301634 + 22669.341604306508im;;; 59930.239966361274 + 125180.70294016767im 12.216539019807177 + 84.57413678394165im 0.01185549520058545 + 0.11455061109107394im; 12.216539096714744 + 84.57413691756922im 59930.23537020177 + 125180.70482547453im 12.21653909671475 + 84.57413691756922im; 0.011855495200585459 + 0.11455061109107392im 12.216539019807177 + 84.57413678394165im 59930.23996636129 + 125180.70294016768im;;; 361696.7481463518 + 530627.5641232601im 0.00024270853323619268 + 0.0010458189856197416im 1.3142315450927868e-12 + 1.0148064016559267e-12im; 0.00024270853323619279 + 0.0010458189856197416im 361696.7481463518 + 530627.5641232601im 0.00024270853323619268 + 0.0010458189856197416im; 1.3142315450927868e-12 + 1.0148064016559267e-12im 0.00024270853323619279 + 0.0010458189856197416im 361696.7481463518 + 530627.5641232601im]), @NamedTuple{effective::@NamedTuple{internal_impedance::Union{Nothing, Symbol, NamedTuple}, insulation_impedance::Union{Nothing, Symbol, NamedTuple}, earth_impedance::Union{Nothing, Symbol, NamedTuple}, shunt_model::Union{Nothing, Symbol, NamedTuple}, insulation_admittance::Union{Nothing, Symbol, NamedTuple}, semicon_admittance::Union{Nothing, Symbol, NamedTuple}, earth_admittance::Union{Nothing, Symbol, NamedTuple}, earth_properties::Union{Nothing, Symbol, NamedTuple}, pipe_impedance::Union{Nothing, Symbol, NamedTuple}, temperature_dependence::Union{Nothing, Symbol, NamedTuple}}, numerical::@NamedTuple{internal_impedance::NamedTuple, insulation_impedance::NamedTuple, shunt_model::NamedTuple, insulation_admittance::NamedTuple, semicon_admittance::NamedTuple, earth_properties::NamedTuple, temperature_dependence::NamedTuple, earth_impedance::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}, earth_admittance::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}, equivalent_earth::@NamedTuple{earth_impedance::Union{Nothing, @NamedTuple{air::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, mixed::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth_admittance::Union{Nothing, @NamedTuple{air::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, mixed::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, backend::Symbol, requested::NamedTuple, methods::NamedTuple, options::NamedTuple}((@NamedTuple{internal_impedance::Union{Nothing, Symbol, NamedTuple}, insulation_impedance::Union{Nothing, Symbol, NamedTuple}, earth_impedance::Union{Nothing, Symbol, NamedTuple}, shunt_model::Union{Nothing, Symbol, NamedTuple}, insulation_admittance::Union{Nothing, Symbol, NamedTuple}, semicon_admittance::Union{Nothing, Symbol, NamedTuple}, earth_admittance::Union{Nothing, Symbol, NamedTuple}, earth_properties::Union{Nothing, Symbol, NamedTuple}, pipe_impedance::Union{Nothing, Symbol, NamedTuple}, temperature_dependence::Union{Nothing, Symbol, NamedTuple}}((:schelkunoff1934, :ametani1980, :unified, :coaxial, :lossless, :lossless, :unified, :constant, :none, :linear)), @NamedTuple{internal_impedance::NamedTuple, insulation_impedance::NamedTuple, shunt_model::NamedTuple, insulation_admittance::NamedTuple, semicon_admittance::NamedTuple, earth_properties::NamedTuple, temperature_dependence::NamedTuple, earth_impedance::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}, earth_admittance::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}(((outer = NamedTuple(),), NamedTuple(), NamedTuple(), NamedTuple(), NamedTuple(), NamedTuple(), NamedTuple(), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}[@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :self, 2, 2, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),))), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :mutual, 2, 2, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),)))], @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}[@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :self, 2, 2, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),))), @NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}((:unified, :mutual, 2, 2, (integration = (method = Val{:quad}(), options = (rtol = 1.0e-8, atol = 0.0, maxevals = 10000000)),)))])), @NamedTuple{earth_impedance::Union{Nothing, @NamedTuple{air::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, mixed::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth_admittance::Union{Nothing, @NamedTuple{air::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, earth::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}, mixed::Union{Nothing, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}, @NamedTuple{identifier::Symbol, order::Symbol, parameters::NamedTuple, numerical::Vector{@NamedTuple{formula::Symbol, kind::Symbol, source::Int64, target::Int64, options::NamedTuple}}}}}((nothing, nothing)), :coaxial, (internal_impedance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), insulation_impedance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_impedance = (identifier = :unified, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), shunt_model = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), insulation_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), semicon_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_admittance = (identifier = :unified, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_properties = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), pipe_impedance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), temperature_dependence = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing)), (internal_impedance = (identifier = :schelkunoff1934, parameters = NamedTuple(), options = (inner = NamedTuple(), outer = NamedTuple(), transfer = NamedTuple()), configured_options = ()), insulation_impedance = (identifier = :ametani1980, parameters = NamedTuple(), options = NamedTuple()), earth_impedance = (identifier = :unified, assumptions = (media = Val{:homogeneous}(), layers = 2:2, longitudinal = :prescribed, permittivity = :positive), parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), shunt_model = (identifier = :coaxial, parameters = NamedTuple(), options = NamedTuple()), insulation_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), semicon_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), earth_admittance = (identifier = :unified, assumptions = (media = Val{:homogeneous}(), layers = 2:2, longitudinal = :prescribed, permittivity = :positive), parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_properties = (identifier = :constant, parameters = NamedTuple(), options = NamedTuple()), pipe_impedance = (identifier = :none, parameters = NamedTuple(), options = NamedTuple()), temperature_dependence = (identifier = :linear, parameters = NamedTuple(), options = NamedTuple())), (reduce_bundle = false, kron_reduction = false, ideal_transposition = false))), ["cable:1:core", "cable:2:core", "cable:3:core"])) \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/unified/formulation.txt b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/unified/formulation.txt new file mode 100644 index 000000000..1a6917548 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/unified/formulation.txt @@ -0,0 +1 @@ +@NamedTuple{backend::Symbol, requested::NamedTuple, methods::NamedTuple, options::NamedTuple}((:coaxial, (internal_impedance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), insulation_impedance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_impedance = (identifier = :unified, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), shunt_model = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), insulation_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), semicon_admittance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_admittance = (identifier = :unified, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_properties = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), pipe_impedance = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), temperature_dependence = (identifier = :default, order = :default, parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing)), (internal_impedance = (identifier = :schelkunoff1934, parameters = NamedTuple(), options = (inner = NamedTuple(), outer = NamedTuple(), transfer = NamedTuple()), configured_options = ()), insulation_impedance = (identifier = :ametani1980, parameters = NamedTuple(), options = NamedTuple()), earth_impedance = (identifier = :unified, assumptions = (media = Val{:homogeneous}(), layers = 2:2, longitudinal = :prescribed, permittivity = :positive), parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), shunt_model = (identifier = :coaxial, parameters = NamedTuple(), options = NamedTuple()), insulation_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), semicon_admittance = (identifier = :lossless, parameters = NamedTuple(), options = NamedTuple()), earth_admittance = (identifier = :unified, assumptions = (media = Val{:homogeneous}(), layers = 2:2, longitudinal = :prescribed, permittivity = :positive), parameters = NamedTuple(), options = NamedTuple(), equivalent_earth = nothing), earth_properties = (identifier = :constant, parameters = NamedTuple(), options = NamedTuple()), pipe_impedance = (identifier = :none, parameters = NamedTuple(), options = NamedTuple()), temperature_dependence = (identifier = :linear, parameters = NamedTuple(), options = NamedTuple())), (reduce_bundle = false, kron_reduction = false, ideal_transposition = false))) \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/unified/problem.json b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/unified/problem.json new file mode 100644 index 000000000..805425d6c --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/three_bare_wires_all_earth/unified/problem.json @@ -0,0 +1,516 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "root": { + "kind": "line_parameters_problem", + "earth_props": { + "kind": "earth_model", + "vertical_layers": false, + "layers": [ + { + "kind": "earth_layer", + "thickness": { + "special": "Inf", + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "rho": { + "special": "Inf", + "__type__": "Float64" + } + }, + { + "kind": "earth_layer", + "thickness": { + "special": "Inf", + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 1, + "__type__": "Float64" + }, + "rho": { + "value": 0.1, + "__type__": "Float64" + } + } + ] + }, + "Gamma": null, + "frequencies": [ + { + "value": 0.1, + "__type__": "Float64" + }, + { + "value": 1, + "__type__": "Float64" + }, + { + "value": 10, + "__type__": "Float64" + }, + { + "value": 100, + "__type__": "Float64" + }, + { + "value": 1000, + "__type__": "Float64" + }, + { + "value": 10000, + "__type__": "Float64" + }, + { + "value": 100000, + "__type__": "Float64" + }, + { + "value": 1000000, + "__type__": "Float64" + }, + { + "value": 10000000, + "__type__": "Float64" + } + ], + "system": { + "kind": "line_cable_system", + "input_positions": [ + { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 1, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 2, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + } + ], + "clearances": [ + [ + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + } + ], + [ + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + } + ], + [ + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + }, + { + "value": 1.0e-6, + "__type__": "Float64" + } + ] + ], + "connections": [ + [ + 1 + ], + [ + 2 + ], + [ + 3 + ] + ], + "line_length": { + "value": 1, + "__type__": "Float64" + }, + "environment": null, + "system_id": "three_bare_wires_all_earth", + "positions": [ + { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 1, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + }, + { + "kind": "pose2", + "x": { + "value": 2, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": -1, + "__type__": "Float64" + } + } + ], + "designs": [ + { + "kind": "cable_design", + "cable_id": "three_bare_wires_all_earth", + "nominal_data": [ + ], + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "core_metal", + "material": { + "value": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "type": "material" + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.0425, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + } + ] + } + }, + { + "kind": "cable_design", + "cable_id": "three_bare_wires_all_earth", + "nominal_data": [ + ], + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "core_metal", + "material": { + "value": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "type": "material" + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.0425, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + } + ] + } + }, + { + "kind": "cable_design", + "cable_id": "three_bare_wires_all_earth", + "nominal_data": [ + ], + "origin": { + "kind": "stack", + "items": [ + { + "kind": "group", + "name": "core", + "pattern": null, + "compact": null, + "item": { + "kind": "region", + "tag": "core_metal", + "material": { + "value": { + "kind": "conductor", + "rho_thermal": { + "value": 0, + "__type__": "Float64" + }, + "T0": { + "value": 20, + "__type__": "Float64" + }, + "theta_max": { + "value": 90, + "__type__": "Float64" + }, + "alpha": { + "value": 0.00393, + "__type__": "Float64" + }, + "eps_r": { + "value": 1, + "__type__": "Float64" + }, + "mu_r": { + "value": 0.999994, + "__type__": "Float64" + }, + "tan_delta": { + "value": 0, + "__type__": "Float64" + }, + "sigma_solar": { + "value": 0, + "__type__": "Float64" + }, + "rho": { + "value": 1.7241e-8, + "__type__": "Float64" + } + }, + "type": "material" + }, + "primitive": { + "kind": "disk", + "r": { + "value": 0.0425, + "__type__": "Float64" + } + } + }, + "at": { + "kind": "pose2", + "x": { + "value": 0, + "__type__": "Float64" + }, + "φ": { + "value": 0, + "__type__": "Float64" + }, + "y": { + "value": 0, + "__type__": "Float64" + } + }, + "path": null, + "boundary": null + } + ] + } + } + ] + }, + "temperature": { + "value": 20, + "__type__": "Float64" + } + }, + "format": "linecablemodels.line_parameters_problem", + "version": "1.0.0" +} \ No newline at end of file diff --git a/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/verification.toml b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/verification.toml new file mode 100644 index 000000000..f5709e856 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/capture-20260917T102438-Nt9nic/verification.toml @@ -0,0 +1,9 @@ +capture_files_verified = 75 +comparison_source_sha256 = "8c655d549fb12eb08e4ff0db3ec2f415f1474e2651ac4bd91647ed075aebae47" +fem_artifacts_verified = 1480 +runs = 10 + +[source_sha256] +"dev/run_three_bare_wires_baseline.jl" = "f983929224224837c3f02c0cd76e553ef68ec2d3f766e994864ba128905f4891" +"docs/spectral-integration-ownership-plan.md" = "626d66f37a29fa69dc58d0f51f1d76730f2d352bfa548dd7921ca79371b979f4" +"test/support/scenarios.jl" = "bd81faedc37a67fd81175efd5af7b30da24da63cba979bb832b0eca2f592397e" diff --git a/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/air_1-Y.csv b/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/air_1-Y.csv new file mode 100644 index 000000000..bbcfb393f --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/air_1-Y.csv @@ -0,0 +1,82 @@ +frequency_hz,receiver,source,new_real,new_imaginary,old_real,old_imaginary,fem_real,fem_imaginary,before_absolute_error,after_absolute_error,before_relative_error,after_relative_error,real_absolute_error,imaginary_absolute_error +0.10000000000000001,cable:1:core,cable:1:core,-1.0666964456901865e-24,9.0759067020569639e-12,3.3278820986485526e-24,9.0759067020569009e-12,3.3668435289524459e-24,9.1335364427824092e-12,5.762974072550828e-14,5.7629740725445272e-14,0.0063096853104526677,0.0063096853104457696,4.4335399746426325e-24,5.7629740725445272e-14 +0.10000000000000001,cable:1:core,cable:2:core,-5.1155498185351802e-14,8.7999087097332524e-13,-1.3601058441964637e-13,-4.2379939867923232e-12,-1.1396039787123376e-13,-4.2860602028816373e-12,5.288262338390966e-14,5.1664328267289576e-12,0.012333923926933294,1.204977843021956,6.2804899685881958e-14,5.1660510738549623e-12 +0.10000000000000001,cable:1:core,cable:3:core,-5.1136148413973948e-14,-3.5844900350302839e-13,-1.3581746350160624e-13,-2.9334497945739113e-12,-1.138186657146977e-13,-2.9739964949248815e-12,4.6130055423992855e-14,2.6162984879134687e-12,0.015499786037223238,0.87908124972851831,6.268251730072376e-14,2.615547491421853e-12 +0.10000000000000001,cable:2:core,cable:1:core,-1.1296417471165001e-13,-4.2580756761562078e-12,-1.1296417495695157e-13,-4.2580756764147954e-12,-1.1396039787123331e-13,-4.2860602028816397e-12,2.8002253153424086e-14,2.8002253420575015e-14,0.0065310235778156742,0.0065310236401238397,9.9622315958329258e-16,2.7984526725431948e-14 +0.10000000000000001,cable:2:core,cable:2:core,10.815794395856088,0.14817480024469867,10.815794395856189,0.14817480024712162,10.885701106001211,0.14939721814968343,0.06991739718287969,0.069917397183023283,0.0064222609881899742,0.0064222609882031642,0.06990671014512273,0.0012224179049847572 +0.10000000000000001,cable:2:core,cable:3:core,-8.399898030521344,0.14802772097586725,-8.3998980305212694,0.14802772097705341,-8.459190172239154,0.14927298011308732,0.059305216800966121,0.059305216800916438,0.0070096527232738877,0.0070096527232680157,0.059292141717810054,0.0012452591372200672 +0.10000000000000001,cable:3:core,cable:1:core,-1.1280530780399827e-13,-2.9535313553156199e-12,-1.1280530750282936e-13,-2.9535313549728525e-12,-1.1381866571469942e-13,-2.973996494924871e-12,2.0490213471832484e-14,2.0490213114589991e-14,0.0068847505547380916,0.0068847504347039372,1.013357910701149e-15,2.046513960925102e-14 +0.10000000000000001,cable:3:core,cable:2:core,-8.3998980305213671,0.14802772097541272,-8.3998980305212712,0.14802772097705655,-8.4591901722391665,0.14927298011308582,0.059305216800976682,0.059305216800915293,0.0070096527232751255,0.00700965272326787,0.059292141717799396,0.0012452591376730937 +0.10000000000000001,cable:3:core,cable:3:core,10.815794395856095,0.14817480024522803,10.815794395856152,0.14817480024608787,10.886063726310885,0.14944203838006351,0.070280756221349369,0.0702807562214217,0.0064554219599144659,0.0064554219599211099,0.070269330454790335,0.00126723813483548 +1,cable:1:core,cable:1:core,-1.1415236378499652e-22,9.0759067020560391e-11,3.1423916594339808e-22,9.0759067020551008e-11,3.219734897938415e-22,9.1337560189597194e-11,5.784931690461863e-13,5.7849316903680297e-13,0.0063335737000786803,0.0063335736999759482,4.3612585357883799e-22,5.7849316903680297e-13 +1,cable:1:core,cable:2:core,-1.2038703207920688e-12,9.7714076065056048e-12,-2.4605830567011761e-12,-3.9952527978242649e-11,-1.646906536789909e-12,-4.0927322450860221e-11,1.2697612936692786e-12,5.0700665779833052e-11,0.030999695076452637,1.2377957866443887,4.4303621599784019e-13,5.0698730057365822e-11 +1,cable:1:core,cable:3:core,-1.2021168565223695e-12,-2.6131097494743189e-12,-2.4448833834075763e-12,-2.6909309921922225e-11,-1.6349560106687234e-12,-2.7803932015100898e-11,1.2067854153556263e-12,2.5194540602117553e-11,0.04332856228458494,0.90458768213457352,4.3283915414635381e-13,2.519082226562658e-11 +1,cable:2:core,cable:1:core,-1.6339304032893058e-12,-4.0648885431794787e-11,-1.6339304034811497e-12,-4.064888543178278e-11,-1.64690653678989e-12,-4.0927322450860311e-11,2.7873922154664215e-13,2.78739221543579e-13,0.0068050829056412005,0.0068050829055664175,1.2976133500584165e-14,2.7843701906552484e-13 +1,cable:2:core,cable:2:core,11.070033787834017,0.21907795136028552,11.070033787835508,0.21907795138250027,11.140075659587218,0.2206567269328914,0.070059662650640772,0.07005966265263136,0.0062877421458306694,0.0062877421460093208,0.070041871753200979,0.0015787755726058839 +1,cable:2:core,cable:3:core,-8.1458225270901394,0.21787423633650826,-8.1458225270891802,0.2178742363474,-8.2045753701006827,0.21950423737083269,0.058775449511429961,0.058775449510773153,0.0071611781584668046,0.0071611781583867801,0.058752843010543288,0.0016300010343244242 +1,cable:3:core,cable:1:core,-1.6216279544894657e-12,-2.7605627275107898e-11,-1.6216279546194463e-12,-2.7605627275097564e-11,-1.6349560106687567e-12,-2.7803932015100808e-11,1.9875212448124282e-13,1.9875212447964912e-13,0.0071360191258537714,0.0071360191257965506,1.3328056179290995e-14,1.983047399929099e-13 +1,cable:3:core,cable:2:core,-8.1458225270905356,0.21787423633236563,-8.1458225270892974,0.2178742363475,-8.2045753701006845,0.21950423737083044,0.058775449511311702,0.058775449510493773,0.0071611781584523951,0.0071611781583527387,0.058752843010148936,0.0016300010384648067 +1,cable:3:core,cable:3:core,11.07003378783407,0.21907795136545705,11.070033787834912,0.2190779513727919,11.140726485540558,0.22075493091369747,0.070712585649814752,0.070712585650830453,0.0063459692289450269,0.0063459692290361788,0.070692697706487451,0.0016769795482404204 +10,cable:1:core,cable:1:core,-1.3303446776678944e-20,9.0759067020265848e-10,2.7893988431890267e-20,9.0759067020117803e-10,3.0077099906617912e-20,9.1341140218724399e-10,5.8207319860659567e-12,5.8207319845855102e-12,0.0063725195154425507,0.0063725195138217621,4.3380546683296853e-20,5.8207319845855102e-12 +10,cable:1:core,cable:2:core,-3.5126392564573668e-11,1.2192069752793325e-10,-5.5746231805725367e-11,-3.5319928202439058e-10,-2.5413114510482376e-11,-3.8068516338465009e-10,4.0933747434082193e-11,5.026997107560687e-10,0.10728771847134705,1.317580442154159,9.7132780540912919e-12,5.0260586091258334e-10 +10,cable:1:core,cable:3:core,-3.4971191232074974e-11,-1.9389230827135353e-12,-5.4539271312740426e-11,-2.2299128059268604e-10,-2.4539872873309703e-11,-2.4965631529573399e-10,4.0137114774762647e-11,2.4793692505861508e-10,0.15999839432994978,0.98834981356026208,1.0431318358765271e-11,2.4771739221302046e-10 +10,cable:2:core,cable:1:core,-2.5235453499545093e-11,-3.7794241747563783e-10,-2.5235453516660523e-11,-3.7794241747387376e-10,-2.5413114510482166e-11,-3.8068516338464952e-10,2.7484938711597683e-12,2.7484938705057229e-12,0.007203827041344626,0.0072038270396303679,1.7766101093707258e-13,2.7427459090116871e-12 +10,cable:2:core,cable:2:core,11.45543946668298,0.35723512525375894,11.455439466704808,0.35723512545106118,11.525898556465627,0.35946125443124921,0.070494247851576916,0.070494247879624494,0.0061131884458517664,0.0061131884482840231,0.0704590897826467,0.0022261291774902658 +10,cable:2:core,cable:3:core,-7.7620499918470696,0.34786703913335942,-7.7620499918328036,0.34786703922484841,-7.8200883233535077,0.35016961009861736,0.058083988829417323,0.058083988818789428,0.0074201009092482346,0.0074201009078905446,0.058038331506438112,0.0023025709652579329 +10,cable:3:core,cable:1:core,-2.4359412290390294e-11,-2.4772235737226404e-10,-2.4359412301593318e-11,-2.47722357370758e-10,-2.4539872873309943e-11,-2.4965631529573425e-10,1.942359203011314e-12,1.9423592025526408e-12,0.0077428174754907673,0.0077428174736623601,1.8046058291964827e-13,1.9339579234702163e-12 +10,cable:3:core,cable:2:core,-7.7620499918553723,0.3478670390993035,-7.7620499918370207,0.34786703922841328,-7.820088323353505,0.35016961009861763,0.0580839888250596,0.05808398881184066,0.007420100908691548,0.0074201009070028572,0.058038331498132756,0.0023025709993141352 +10,cable:3:core,cable:3:core,11.455439466681341,0.35723512530509677,11.455439466692642,0.35723512536493024,11.526629071262175,0.35960663832318673,0.071229094283709632,0.071229094296996656,0.0061765200533299928,0.0061765200544821562,0.071189604580833787,0.0023715130180899657 +100,cable:1:core,cable:1:core,-1.9401763256901172e-18,9.0759067008644283e-09,1.9127172212576587e-18,9.0759067005974392e-09,2.6849740688989855e-18,9.1322530610991367e-09,5.6346360501697491e-11,5.6346360234708586e-11,0.0061700393237806069,0.0061700392945447866,4.6251503945891026e-18,5.6346360234708399e-11 +100,cable:1:core,cable:2:core,-1.1765965611356636e-09,1.9628848004581836e-09,-1.5750338466741854e-09,-2.411086810379962e-09,-4.2756269288587337e-10,-3.3523773182763521e-09,1.4841556080924008e-09,5.3677800929843974e-09,0.43915996451163936,1.5883200536978668,7.4903386824979017e-10,5.3152621187345357e-09 +100,cable:1:core,cable:3:core,-1.1636921547696423e-09,7.2216173971858769e-10,-1.491065678495518e-09,-1.131648013139559e-09,-3.7369268803186256e-10,-2.0601909963507641e-09,1.4528298150466409e-09,2.8923322605396814e-09,0.69386954866857675,1.3813739637190778,7.8999946673777976e-10,2.7823527360693517e-09 +100,cable:2:core,cable:1:core,-4.2495166240793803e-10,-3.3258245297456913e-09,-4.2495166380733107e-10,-3.3258245294674344e-09,-4.2756269288587238e-10,-3.3523773182763517e-09,2.668085580299013e-11,2.6680855663015577e-11,0.0078948350319152146,0.0078948349904969031,2.6110304779343512e-12,2.6552788530660359e-11 +100,cable:2:core,cable:2:core,12.10466978238181,0.68261648131153441,12.104669782732641,0.6826164828956679,12.176710067798881,0.68639023960195522,0.072139059545459905,0.072139059978679629,0.0059149574465560744,0.0059149574820774172,0.072040285417070749,0.0037737582904208145 +100,cable:2:core,cable:3:core,-7.1288262824171325,0.61549758312471348,-7.1288262822103956,0.61549758378874042,-7.1859340087568553,0.61910011073453508,0.057221242836034171,0.05722124267151317,0.0079335619960233406,0.0079335619732129742,0.057107726339722831,0.0036025276098216041 +100,cable:3:core,cable:1:core,-3.7138847615143487e-10,-2.0430445510009429e-09,-3.7138847700779842e-10,-2.0430445507832228e-09,-3.7369268803186308e-10,-2.0601909963507678e-09,1.7300577563890915e-11,1.7300577462166922e-11,0.008262732373493651,0.0082627323249104094,2.3042118804282082e-12,1.7146445349824888e-11 +100,cable:3:core,cable:2:core,-7.1288262826252131,0.61549758295382873,-7.1288262823565649,0.61549758392216858,-7.1859340087568544,0.61910011073453464,0.057221242681753583,0.057221242474602925,0.0079335619746327777,0.0079335619459119308,0.057107726131641279,0.0036025277807059108 +100,cable:3:core,cable:3:core,12.104669782274954,0.6826164818757352,12.104669782424534,0.68261648227961069,12.176101339397905,0.68619513278362976,0.071521144223717956,0.071521144393318914,0.0058645897345041284,0.0058645897484110648,0.071431557122950906,0.0035786509078945539 +1000,cable:1:core,cable:1:core,-4.4750964200884585e-16,9.0759066492957148e-08,-1.1449192093489899e-16,9.0759066436426566e-08,2.1468668223221234e-16,9.1290601213133132e-08,5.3153477670666778e-10,5.3153472017639643e-10,0.0058224479808793375,0.0058224473616450391,6.6219632424105819e-16,5.3153472017598398e-10 +1000,cable:1:core,cable:2:core,-3.9862609765354866e-08,4.739255430708393e-08,-4.9053839044978193e-08,1.1664030389121588e-08,-7.7436488465027592e-09,-2.5407758196285771e-08,5.5505399045096105e-08,7.957080589698105e-08,2.0896863010547984,2.9957089924120153,3.2118960918852108e-08,7.2800312503369694e-08 +1000,cable:1:core,cable:3:core,-3.8993651370144037e-08,3.4644002403347795e-08,-4.4374379330248368e-08,2.2280527656749991e-08,-5.343392651111024e-09,-1.3932639017219087e-08,5.324294659110877e-08,5.9093400676896156e-08,3.5680502298860661,3.9601155715367384,3.3650258719033011e-08,4.857664142056688e-08 +1000,cable:2:core,cable:1:core,-7.6990451786107599e-09,-2.5177999875353155e-08,-7.6990452661998496e-09,-2.5177999831420838e-08,-7.7436488465027675e-09,-2.5407758196285751e-08,2.3404782759350137e-10,2.340478011586232e-10,0.0088115128893391167,0.008811511894109756,4.4603667892007594e-11,2.2975832093259631e-10 +1000,cable:2:core,cable:2:core,13.422507237708384,1.6280303255557746,13.422507243289033,1.6280303346877074,13.494055376278043,1.6356743688863127,0.071955309693187364,0.071955316212370585,0.0052936233733021816,0.0052936238529068258,0.071548138569658803,0.0076440433305380306 +1000,cable:2:core,cable:3:core,-5.956480579531533,1.2141336183221656,-5.9564805769900744,1.214133621295584,-6.0087326165714305,1.2209099909311973,0.052689608328873289,0.052689606190929744,0.0085932431791693412,0.0085932428304882746,0.052252037039897559,0.0067763726090317444 +1000,cable:3:core,cable:1:core,-5.3125767624237946e-09,-1.3812807031995066e-08,-5.3125768097971736e-09,-1.3812807003898827e-08,-5.3433926511110199e-09,-1.3932639017219117e-08,1.2373086717660297e-10,1.2373085176430589e-10,0.0082917640239543716,0.008291762991106762,3.0815888687225279e-11,1.1983198522405101e-10 +1000,cable:3:core,cable:2:core,-5.9564805841176129,1.2141336216946841,-5.956480580627443,1.2141336265943121,-6.0087326165714261,1.2209099909311989,0.052689604040242428,0.052689601209194283,0.0085932424797288097,0.0085932420180080665,0.052252032453813158,0.006776369236514812 +1000,cable:3:core,cable:3:core,13.422507234576006,1.6280303345300924,13.422507236118634,1.6280303360967185,13.493652206194168,1.6354456101924859,0.071530364580114594,0.071530366276836566,0.0052625265336032779,0.0052625266584320051,0.071144971618162245,0.0074152756623935279 +10000,cable:1:core,cable:1:core,-1.6692047511660082e-13,9.0759045224291654e-07,-1.461383916862566e-13,9.0759044220431542e-07,1.1772351924736467e-14,9.1289430599284201e-07,5.303863790877314e-09,5.303853752935647e-09,0.0058099428992592498,0.0058099319035266649,1.7869282704133729e-13,5.3038537499254656e-09 +10000,cable:1:core,cable:2:core,-7.0566345675390599e-07,1.6746422052936652e-06,-8.7200816269054662e-07,1.5363787545915005e-06,-1.0906877871764883e-07,-9.4990917897013724e-08,1.8009562770739958e-06,1.867492061711316e-06,12.451735183997533,12.911760772130876,5.9659467803625714e-07,1.7696331231906789e-06 +10000,cable:1:core,cable:3:core,-6.8597772458705922e-07,1.5110795613731924e-06,-7.4376780209961771e-07,1.4920783526578328e-06,-5.90316652777458e-08,-4.4321203179529856e-08,1.6820782301209971e-06,1.677001162687334e-06,22.78681045583658,22.718032338855622,6.2694605930931344e-07,1.5554007645527222e-06 +10000,cable:2:core,cable:1:core,-1.0820602972944975e-07,-9.3862017398338054e-08,-1.0820603068185215e-07,-9.3862013882438677e-08,-1.0906877871764885e-07,-9.4990917897013724e-08,1.420830196538177e-09,1.4208279813364973e-09,0.0098235596132651929,0.0098235442974548941,8.6274898819909917e-10,1.12890049867567e-09 +10000,cable:2:core,cable:2:core,17.086533949182268,4.2459947823977897,17.086533982869692,4.2459947768646558,17.168725355808448,4.2708879656289653,0.085878359511655275,0.08587839014892458,0.0048540883417152094,0.0048540900734205267,0.082191406626179742,0.02489318323117562 +10000,cable:2:core,cable:3:core,-3.3202280396447059,2.2955285385873436,-3.3202280314027313,2.2955285336624969,-3.3566031067050783,2.3127730439275873,0.040255673358350737,0.040255663801212906,0.0098756963403934112,0.0098756939957949184,0.036375067060372412,0.017244505340243688 +10000,cable:3:core,cable:1:core,-5.8587420059988364e-08,-4.3856895290619899e-08,-5.8587420416245618e-08,-4.3856893464081314e-08,-5.9031665277745781e-08,-4.432120317952985e-08,6.4260174978691039e-10,6.4260067631799851e-10,0.0087052100245836664,0.0087051954824938944,4.4424521775741637e-10,4.6430788890995042e-10 +10000,cable:3:core,cable:2:core,-3.3202280044350188,2.2955286501758185,-3.3202279874715748,2.2955286461190285,-3.3566031067050797,2.3127730439275886,0.040255664881250826,0.040255647815225615,0.0098756942607544987,0.0098756900740431947,0.036375102270060911,0.017244393751770115 +10000,cable:3:core,cable:3:core,17.086534011462401,4.2459949224509836,17.086534015481533,4.2459949196386599,17.167730206096849,4.2706627660680407,0.084860615234054318,0.084860618262128304,0.004796839125662194,0.0047968392968273897,0.081196194634447494,0.024667843617057095 +100000,cable:1:core,cable:1:core,-5.5477726061553848e-11,9.0758164316572844e-06,-5.4794500700018239e-11,9.0758158501016051e-06,3.4064351579940734e-13,9.1287839375614467e-06,5.2968116155264175e-08,5.2967535315491585e-08,0.0058023189635719868,0.0058022553362831212,5.5818369577353259e-11,5.2967505904162281e-08 +100000,cable:1:core,cable:2:core,1.7038431448127925e-05,8.9143470293325821e-06,1.6957770865381572e-05,9.1879747547490105e-06,-1.3982409354353952e-07,1.597141532094673e-07,1.9334871162455997e-05,1.9280457988612775e-05,91.085372495896408,90.829035426642079,1.7178255541671466e-05,8.7546328761231148e-06 +100000,cable:1:core,cable:3:core,1.5638455966719112e-05,7.7776922470880283e-06,1.5670639845467551e-05,7.7897672759496725e-06,-5.2728554835898479e-08,7.5364927336429311e-08,1.751388927245889e-05,1.7479677280490033e-05,190.41141642792607,190.03946284592789,1.5691184521555011e-05,7.7023273197515982e-06 +100000,cable:2:core,cable:1:core,-1.3779482033724361e-07,1.5778396979479867e-07,-1.3779479962975289e-07,1.5778397005743111e-07,-1.3982409354353934e-07,1.5971415320946725e-07,2.8006500654196122e-09,2.8006352422354799e-09,0.013193687834586973,0.013193618003490767,2.0292732062957301e-09,1.9301834146685728e-09 +100000,cable:2:core,cable:2:core,25.114053168427059,8.0617974533493069,25.114053162870391,8.061797456881747,25.209507982247846,8.1184901178623399,0.11102108066238134,0.11102107768864615,0.0041919253259874098,0.0041919252137053477,0.09545481382078691,0.056692664513033009 +100000,cable:2:core,cable:3:core,0.17168867697767834,1.0502294238147609,0.17168867735387133,1.0502294245133199,0.17188900017107905,1.0596269692791682,0.0093996796146047969,0.0093996803210224349,0.0087562848060661353,0.0087562854641304831,0.00020032319340071436,0.0093975454644072975 +100000,cable:3:core,cable:1:core,-5.2567620245545815e-08,7.4524196565821724e-08,-5.2567611266984596e-08,7.452419574772051e-08,-5.272855483589846e-08,7.5364927336429271e-08,8.5599791858829219e-10,8.5599542698447529e-10,0.0093064295201444932,0.0093064024313691453,1.6093459035264504e-10,8.4073077060754738e-10 +100000,cable:3:core,cable:2:core,0.17168861145690964,1.0502292230423049,0.17168860920972603,1.0502292248219658,0.17188900017107883,1.0596269692791682,0.0093998807130873142,0.0093998824444375129,0.0087564721396412356,0.0087564737524829551,0.0002003887141691918,0.0093977462368632914 +100000,cable:3:core,cable:3:core,25.114053090920191,8.0617971940330779,25.114053091114521,8.0617971943186379,25.210637229590311,8.1196614463144066,0.1125911518021756,0.11259115211563631,0.0042509777892140357,0.0042509778010490192,0.096584138670120723,0.057864252281328632 +1000000,cable:1:core,cable:1:core,-2.4740421441749431e-08,9.0711568120478795e-05,-2.4719345718823342e-08,9.07115475559531e-05,1.0616619292708136e-11,9.1254999550522529e-05,5.4401437613241005e-07,5.439947913762378e-07,0.0059614747554868766,0.005961260139780683,2.4751038061042137e-08,5.4343143004373362e-07 +1000000,cable:1:core,cable:2:core,-7.7938841060709405e-06,5.4353142732557047e-06,-7.7983645094344234e-06,5.4402402295420326e-06,-3.7777528896064878e-09,-1.4747468946151504e-08,9.5138043650246079e-06,9.5073092927330662e-06,624.93618448120003,624.5095406760438,7.7901063531813343e-06,5.4500617422018565e-06 +1000000,cable:1:core,cable:3:core,-6.4349987748322076e-06,5.106801280693119e-06,-6.4350271678452739e-06,5.1067899044121279e-06,-1.5728602749174711e-09,-5.801878865365625e-09,8.2175378958683303e-06,8.2175227450384768e-06,1367.0158584768271,1367.0133380838649,6.43342591455729e-06,5.1126031595584845e-06 +1000000,cable:2:core,cable:1:core,-3.6993309151330325e-09,-1.4559820469910918e-08,-3.6993334581473185e-09,-1.455981630897755e-08,-3.7777528896064837e-09,-1.4747468946151509e-08,2.033792503396546e-10,2.0337639173643051e-10,0.013359435177914946,0.013359247403968452,7.8421974473451184e-11,1.8764847624059038e-10 +1000000,cable:2:core,cable:2:core,40.833774627207681,19.549173405646204,40.8337746272082,19.549173405645227,40.898656436585682,19.657028604228248,0.1258665684345085,0.12586656843393867,0.002773778589948313,0.0027737785899357558,0.06488180937800081,0.10785519858204395 +1000000,cable:2:core,cable:3:core,-0.020403832081271406,-0.018991054091930557,-0.020403832081266379,-0.018991054091929888,-0.020561861246802895,-0.019101245069676594,0.00019265323443156234,0.00019265323442705564,0.006864521977926509,0.0068645219777659292,0.00015802916553148832,0.00011019097774603651 +1000000,cable:3:core,cable:1:core,-1.5503926404254643e-09,-5.8061933848089228e-09,-1.5503936374884616e-09,-5.8061917080445249e-09,-1.572860274917473e-09,-5.8018788653656292e-09,2.2876853134545371e-11,2.2878148476068964e-11,0.0038056436639851057,0.0038058591485289352,2.2467634492008675e-11,4.3145194432936257e-12 +1000000,cable:3:core,cable:2:core,-0.020403832499743361,-0.018991053490800946,-0.020403832499540878,-0.018991053491192872,-0.020561861246802884,-0.019101245069676587,0.00019265323493243477,0.00019265323499051157,0.0068645219957733442,0.0068645219978427071,0.00015802874705952291,0.00011019157887564093 +1000000,cable:3:core,cable:3:core,40.833774626502667,19.549173406400044,40.833774626502667,19.54917340640004,40.896804204764933,19.65119276898254,0.1199194649656956,0.11991946496569258,0.0026429640409972297,0.0026429640409971634,0.063029578262266739,0.10201936258249589 +10000000,cable:1:core,cable:1:core,-1.6761991871629861e-05,0.0008877789650905823,-1.6761328755330462e-05,0.00088777830561370815,2.0223235310295311e-08,-0.00010676664179201856,0.00099468651991342204,0.00099468719048421278,9.3164539539167421,9.3164602346310872,1.6782215106940157e-05,0.00099454560688260092 +10000000,cable:1:core,cable:2:core,-9.3439526677307196e-11,5.4261359972629985e-10,-9.3440467051073175e-11,5.4261406900812513e-10,-1.7415122836923419e-12,-6.8626727083962635e-13,5.5098453127675587e-10,5.509839120363144e-10,294.35270366985196,294.35237285276395,9.1698014393614859e-11,5.4329986699713949e-10 +10000000,cable:1:core,cable:3:core,3.6775716049205592e-11,4.3675577430240467e-10,3.6775425414283791e-11,4.3675607950356147e-10,-2.4124979876850083e-12,-1.1426545811342262e-12,4.3964871733412077e-10,4.3964843925353919e-10,164.69818462293117,164.69808045029254,3.91882140368906e-11,4.3789842888353891e-10 +10000000,cable:2:core,cable:1:core,-1.1003843823177017e-13,-1.3743099164070029e-13,-1.1003845565783392e-13,-1.3743080703062872e-13,-1.7415122836923411e-12,-6.8626727083962585e-13,1.7213158674595239e-12,1.7213158251136901e-12,0.91957931792104819,0.91957929529861515,1.6314738454605708e-12,5.4883627919892558e-13 +10000000,cable:2:core,cable:2:core,80.846531351828517,55.108195401013525,80.846531351828517,55.108195401013525,80.822969599449294,55.217612590982597,0.11192532169222402,0.11192532169222402,0.0011434450267779742,0.0011434450267779742,0.023561752379222867,0.10941718996907213 +10000000,cable:2:core,cable:3:core,-9.2664181398870313e-08,-1.3479772233806722e-07,-9.2664181398870313e-08,-1.3479772233806722e-07,-2.9463434702527936e-07,-1.8880177493102272e-07,2.0906550528392246e-07,2.0906550528392246e-07,0.59743851816518001,0.59743851816518001,2.0197016562640905e-07,5.4004052592955504e-08 +10000000,cable:3:core,cable:1:core,-5.2158582298414681e-14,-5.472514978320991e-14,-5.2158582925956725e-14,-5.4725070160129558e-14,-2.4124979876850006e-12,-1.1426545811342232e-12,2.5989984083309108e-12,2.5989983755709509e-12,0.9736189435182262,0.97361893124591492,2.3603394053865861e-12,1.0879294313510132e-12 +10000000,cable:3:core,cable:2:core,-9.266418139883618e-08,-1.3479772233804374e-07,-9.266418139883618e-08,-1.3479772233804374e-07,-2.9463434702527925e-07,-1.8880177493102251e-07,2.0906550528396134e-07,2.0906550528396134e-07,0.59743851816529148,0.59743851816529148,2.0197016562644309e-07,5.400405259297877e-08 +10000000,cable:3:core,cable:3:core,80.846531351828517,55.108195401013525,80.846531351828517,55.108195401013525,80.821711858300745,55.220113257927331,0.11463687868724544,0.11463687868724544,0.0011711422262802136,0.0011711422262802136,0.024819493527772352,0.11191785691380574 diff --git a/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/air_1-Z.csv b/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/air_1-Z.csv new file mode 100644 index 000000000..2f3e2c9b3 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/air_1-Z.csv @@ -0,0 +1,82 @@ +frequency_hz,receiver,source,new_real,new_imaginary,old_real,old_imaginary,fem_real,fem_imaginary,before_absolute_error,after_absolute_error,before_relative_error,after_relative_error,real_absolute_error,imaginary_absolute_error +0.10000000000000001,cable:1:core,cable:1:core,3.1368030432638579e-06,1.2442491566826318e-06,3.1368030432638579e-06,1.2442491566826316e-06,3.1562793020459726e-06,1.2381622459009902e-06,2.0405272333677916e-08,2.0405272333677979e-08,0.0060184571470754513,0.0060184571470754704,1.9476258782114692e-08,6.0869107816415649e-09 +0.10000000000000001,cable:1:core,cable:2:core,9.8692094314379155e-08,7.1450042905111216e-07,9.8692094314379645e-08,7.1450042905111184e-07,9.8558501409278213e-08,7.1184683786552884e-07,2.6569518709410696e-09,2.656951870941362e-09,0.0036972079475629089,0.0036972079475633161,1.3359290510094226e-10,2.6535911855833211e-09 +0.10000000000000001,cable:1:core,cable:3:core,9.8689933658488536e-08,6.8496952230340285e-07,9.8689933646133091e-08,6.8496952228978849e-07,9.8555318141668934e-08,6.8241325093108754e-07,2.5598133903386247e-09,2.5598134045838953e-09,0.0037126008775244007,0.0037126008981848927,1.346155168196018e-10,2.5562713723153075e-09 +0.10000000000000001,cable:2:core,cable:1:core,9.8692094314379168e-08,7.1450042905111216e-07,9.8692094314379671e-08,7.1450042905111195e-07,9.8558501409278186e-08,7.1184683786552915e-07,2.6569518709408607e-09,2.6569518709410469e-09,0.0036972079475626166,0.0036972079475628759,1.3359290510098197e-10,2.6535911855830035e-09 +0.10000000000000001,cable:2:core,cable:2:core,3.1374615510534723e-06,1.2435841216202133e-06,3.1374615510534732e-06,1.2435841216202129e-06,3.1569380449518183e-06,1.2377824992095822e-06,2.0322220281450704e-08,2.0322220281451637e-08,0.005993122305261364,0.0059931223052616398,1.9476493898345935e-08,5.8016224106311635e-09 +0.10000000000000001,cable:2:core,cable:3:core,9.9023675189330895e-08,8.1529149017255945e-07,9.9023675189331398e-08,8.1529149017255914e-07,9.8888942097083999e-08,8.1219887993903893e-07,3.0955437426436837e-09,3.0955437426439794e-09,0.0037833729615709966,0.0037833729615713578,1.3473309224689585e-10,3.0926102335205263e-09 +0.10000000000000001,cable:3:core,cable:1:core,9.8689933658488549e-08,6.8496952230340285e-07,9.8689933646132827e-08,6.8496952228978997e-07,9.8555318141668934e-08,6.8241325093108775e-07,2.5598133903398795e-09,2.5598134045836848e-09,0.0037126008775262192,0.0037126008981845861,1.3461551681961503e-10,2.5562713723150957e-09 +0.10000000000000001,cable:3:core,cable:2:core,9.9023675189330882e-08,8.1529149017255935e-07,9.9023675189331385e-08,8.1529149017255903e-07,9.8888942097084039e-08,8.1219887993903882e-07,3.0955437426436816e-09,3.0955437426439769e-09,0.0037833729615709944,0.0037833729615713556,1.3473309224684291e-10,3.0926102335205263e-09 +0.10000000000000001,cable:3:core,cable:3:core,3.1374615510534723e-06,1.2435841216202133e-06,3.1374615510534732e-06,1.2435841216202129e-06,3.1569378453844727e-06,1.2376254103650067e-06,2.0367431862918889e-08,2.0367431862919823e-08,0.0060065573188623055,0.0060065573188625805,1.9476294331000322e-08,5.9587112552066507e-09 +1,cable:1:core,cable:1:core,4.0258228222657537e-06,1.1002346916048132e-05,4.025822822265763e-06,1.1002346916048128e-05,4.0439380999692672e-06,1.0950296462592307e-05,5.5112729847392547e-08,5.5112729847398814e-08,0.0047213246161881542,0.0047213246161886911,1.811527770351351e-08,5.2050453455824476e-08 +1,cable:1:core,cable:2:core,9.8663940253676711e-07,5.6982925454684157e-06,9.8663940253676944e-07,5.6982925454684157e-06,9.8532040051059231e-07,5.6808597261099677e-06,1.7482647314675746e-08,1.7482647314675571e-08,0.00303219367860406,0.0030321936786040296,1.3190020261747945e-09,1.7432819358448011e-08 +1,cable:1:core,cable:3:core,9.8646617111842135e-07,5.4030097683584801e-06,9.8646617099486401e-07,5.40300976822235e-06,9.8504682423657282e-07,5.3864382934087735e-06,1.6632147267183239e-08,1.6632147413360784e-08,0.0030374090709155005,0.0030374090976108509,1.4193468818485214e-09,1.6571474949706518e-08 +1,cable:2:core,cable:1:core,9.866394025367669e-07,5.6982925454684148e-06,9.8663940253676944e-07,5.6982925454684148e-06,9.853204005105921e-07,5.6808597261099677e-06,1.7482647314674919e-08,1.7482647314674727e-08,0.0030321936786039164,0.003032193678603883,1.3190020261747945e-09,1.7432819358447164e-08 +1,cable:2:core,cable:2:core,4.0462619649157106e-06,1.098137058628601e-05,4.0462619649157199e-06,1.0981370586286009e-05,4.0645266771623253e-06,1.093273668005019e-05,5.1950520201492852e-08,5.1950520201497715e-08,0.0044539817697942623,0.0044539817697946795,1.8264712246614701e-08,4.8633906235820551e-08 +1,cable:2:core,cable:3:core,9.9703495653241241e-07,6.6990056616567392e-06,9.9703495653241453e-07,6.6990056616567426e-06,9.9566788129606608e-07,6.6773632135146649e-06,2.1685581760339246e-08,2.1685581760335732e-08,0.0032121136374929221,0.0032121136374924017,1.3670752363463284e-09,2.1642448142074279e-08 +1,cable:3:core,cable:1:core,9.8646617111842135e-07,5.4030097683584818e-06,9.8646617099486423e-07,5.4030097682223517e-06,9.8504682423657261e-07,5.386438293408771e-06,1.6632147267187494e-08,1.6632147413365019e-08,0.003037409070916279,0.0030374090976116258,1.4193468818487332e-09,1.6571474949710753e-08 +1,cable:3:core,cable:2:core,9.9703495653241262e-07,6.6990056616567409e-06,9.9703495653241474e-07,6.6990056616567443e-06,9.9566788129606587e-07,6.6773632135146632e-06,2.1685581760342654e-08,2.168558176033914e-08,0.0032121136374934278,0.0032121136374929074,1.367075236346752e-09,2.1642448142077667e-08 +1,cable:3:core,cable:3:core,4.0462619649157106e-06,1.0981370586286007e-05,4.0462619649157199e-06,1.0981370586286005e-05,4.064508501178054e-06,1.093114109659993e-05,5.344097416492523e-08,5.3440974164930001e-08,0.0045823559991124358,0.0045823559991128452,1.8246536262343391e-08,5.0229489686076881e-08 +10,cable:1:core,cable:1:core,1.3443875148217054e-05,9.5357294237721687e-05,1.3443875148217149e-05,9.535729423772166e-05,1.3456741305710767e-05,9.4931369051803823e-05,4.2611946917243133e-07,4.2611946917246129e-07,0.0044442818814534836,0.0044442818814537958,1.2866157493712918e-08,4.2592518591786418e-07 +10,cable:1:core,cable:2:core,9.8448260812245156e-06,4.2519810980107041e-05,9.8448260812245715e-06,4.2519810980107068e-05,9.8332944172567729e-06,4.2419997939162006e-05,1.0047697455918364e-07,1.0047697455915029e-07,0.0023074389292056367,0.0023074389292048708,1.1531663967742761e-08,9.9813040945034826e-08 +10,cable:1:core,cable:3:core,9.8317851451266156e-06,3.9569605534518693e-05,9.831785145126619e-06,3.9569605534518673e-05,9.8166164930190614e-06,3.9483456666432487e-05,8.747408461533662e-08,8.7474084615356062e-08,0.002150006771041425,0.0021500067710419029,1.5168652107554209e-08,8.614886808620565e-08 +10,cable:2:core,cable:1:core,9.8448260812245156e-06,4.2519810980107054e-05,9.8448260812245698e-06,4.2519810980107075e-05,9.8332944172567746e-06,4.2419997939162019e-05,1.0047697455917652e-07,1.004769745591501e-07,0.0023074389292054727,0.002307438929204866,1.1531663967741067e-08,9.9813040945034826e-08 +10,cable:2:core,cable:2:core,1.4059660509356969e-05,9.4699377213437565e-05,1.405966050935706e-05,9.4699377213437497e-05,1.4078358508886816e-05,9.4300886456954623e-05,3.9892918944182097e-07,3.9892918944189297e-07,0.0041840165400588826,0.0041840165400596381,1.8697999529847454e-08,3.9849075648294219e-07 +10,cable:2:core,cable:3:core,1.0164731798726432e-05,5.2299532930466564e-05,1.0164731798726485e-05,5.2299532930466571e-05,1.0151570594638799e-05,5.2158599331381674e-05,1.4154680019011417e-07,1.4154680019010255e-07,0.002663793035197452,0.0026637930351972334,1.3161204087633625e-08,1.4093359908488986e-07 +10,cable:3:core,cable:1:core,9.8317851451266156e-06,3.9569605534518693e-05,9.8317851451266173e-06,3.9569605534518679e-05,9.8166164930190665e-06,3.9483456666432508e-05,8.7474084615322102e-08,8.7474084615335151e-08,0.0021500067710410672,0.0021500067710413877,1.5168652107549127e-08,8.6148868086185321e-08 +10,cable:3:core,cable:2:core,1.0164731798726432e-05,5.2299532930466564e-05,1.0164731798726483e-05,5.2299532930466571e-05,1.0151570594638785e-05,5.2158599331381681e-05,1.4154680019010853e-07,1.4154680019009707e-07,0.0026637930351973453,0.0026637930351971297,1.3161204087647178e-08,1.4093359908488309e-07 +10,cable:3:core,cable:3:core,1.4059660509356969e-05,9.4699377213437578e-05,1.405966050935706e-05,9.4699377213437511e-05,1.407770266779548e-05,9.428378799062607e-05,4.1598067454887159e-07,4.1598067454894327e-07,0.00436363292053358,0.004363632920534332,1.8042158438510983e-08,4.1558922281150835e-07 +100,cable:1:core,cable:1:core,0.00010029065542748212,0.00079835616300552852,0.00010029065542748186,0.00079835616300552798,0.00010105736213043232,0.00079409082895413549,4.3336951598280937e-06,4.3336951598285816e-06,0.0054137666875932588,0.0054137666875938685,7.6670670295020046e-07,4.2653340513930339e-06 +100,cable:1:core,cable:2:core,9.6939258720589021e-05,0.00028095471396052328,9.693925872058955e-05,0.00028095471396052344,9.6830997663775953e-05,0.00028061951809663104,3.522452605684939e-07,3.5224526056817668e-07,0.0011865858128200211,0.0011865858128189525,1.0826105681306826e-07,3.3519586389223286e-07 +100,cable:1:core,cable:3:core,9.6062121241851888e-05,0.0002517102826184886,9.6062121241851698e-05,0.00025171028261849001,9.5942898104968443e-05,0.00025153139211222554,2.1497899804362638e-07,2.1497899804255875e-07,0.00079856022041533976,0.00079856022041137396,1.1922313688344491e-07,1.7889050626305981e-07 +100,cable:2:core,cable:1:core,9.6939258720589035e-05,0.00028095471396052328,9.6939258720589563e-05,0.00028095471396052344,9.6830997663775858e-05,0.00028061951809663077,3.5224526056878517e-07,3.5224526056846796e-07,0.0011865858128210034,0.0011865858128199348,1.0826105681317668e-07,3.3519586389250391e-07 +100,cable:2:core,cable:2:core,0.00011750384543838808,0.00077809644648036455,0.00011750384543838787,0.00077809644648036422,0.00011826512950095309,0.0007739317981559253,4.2336567043358251e-06,4.2336567043361064e-06,0.0054075508292252576,0.0054075508292256167,7.6128406256500297e-07,4.1646483244392419e-06 +100,cable:2:core,cable:3:core,0.00010621565159805932,0.00037161327841812262,0.00010621565159805986,0.00037161327841812283,0.00010608353483127337,0.00037094741867822398,6.7884021189399916e-07,6.7884021189368099e-07,0.0017594819672218783,0.0017594819672210537,1.3211676678595596e-07,6.6585973989863887e-07 +100,cable:3:core,cable:1:core,9.6062121241851902e-05,0.00025171028261848866,9.6062121241851712e-05,0.00025171028261849007,9.594289810496847e-05,0.00025153139211222538,2.1497899804379931e-07,2.1497899804273168e-07,0.00079856022041598269,0.00079856022041201679,1.1922313688343135e-07,1.7889050626327665e-07 +100,cable:3:core,cable:2:core,0.00010621565159805932,0.00037161327841812256,0.00010621565159805986,0.00037161327841812278,0.00010608353483127338,0.00037094741867822409,6.7884021189383706e-07,6.7884021189351878e-07,0.0017594819672214579,0.0017594819672206328,1.3211676678594241e-07,6.6585973989847624e-07 +100,cable:3:core,cable:3:core,0.00011750384543838827,0.00077809644648036498,0.00011750384543838806,0.00077809644648036465,0.00011825895634178478,0.00077390901166622618,4.254973889354382e-06,4.2549738893546632e-06,0.0054349416120347775,0.0054349416120351365,7.5511090339650802e-07,4.1874348141388018e-06 +1000,cable:1:core,cable:1:core,0.00078851876842865208,0.0066684718151727796,0.0007885187684286524,0.0066684718151727796,0.00078545538086318019,0.0066160382983872414,5.2522928572915652e-05,5.2522928572915638e-05,0.0078833666855376294,0.0078833666855376277,3.0633875654718836e-06,5.2433516785538141e-05 +1000,cable:1:core,cable:2:core,0.00088055424215431675,0.0014022944498764066,0.00088055424215432207,0.0014022944498764082,0.00087926355750770843,0.001402374698144822,1.2931769567957754e-06,1.2931769567905672e-06,0.00078127110159509035,0.00078127110159194378,1.2906846466083236e-06,8.024826841533314e-08 +1000,cable:1:core,cable:3:core,0.00083441087840421878,0.0011330005183466552,0.00083441087840421617,0.0011330005183466706,0.00083336723943152919,0.0011339562556012919,1.4151381576305953e-06,1.415138157642912e-06,0.0010056040545897938,0.0010056040545985462,1.043638972689589e-06,9.5573725463664674e-07 +1000,cable:2:core,cable:1:core,0.00088055424215431675,0.0014022944498764066,0.00088055424215432196,0.0014022944498764082,0.00087926355750770843,0.0014023746981448211,1.2931769567956134e-06,1.2931769567905134e-06,0.00078127110159499278,0.00078127110159191158,1.2906846466083236e-06,8.0248268414465779e-08 +1000,cable:2:core,cable:2:core,0.0011875295144730819,0.0060821869409338524,0.0011875295144730793,0.0060821869409338489,0.0011843876922731453,0.0060323216961683784,4.9964123951654399e-05,4.9964123951658031e-05,0.0081275599595141655,0.0081275599595147553,3.1418221999365142e-06,4.9865244765474014e-05 +1000,cable:2:core,cable:3:core,0.001102054672250501,0.0020961818335963364,0.0011020546722505062,0.0020961818335963386,0.0011007933449973671,0.0020953145045025617,1.5307534734316936e-06,1.5307534734261769e-06,0.00064674079606106736,0.00064674079605873654,1.2613272531338936e-06,8.6732909377468248e-07 +1000,cable:3:core,cable:1:core,0.00083441087840421867,0.0011330005183466552,0.00083441087840421607,0.0011330005183466706,0.00083336723943152799,0.0011339562556012916,1.4151381576312484e-06,1.4151381576435651e-06,0.0010056040545902585,0.0010056040545990109,1.0436389726906732e-06,9.557372546364299e-07 +1000,cable:3:core,cable:2:core,0.001102054672250501,0.002096181833596336,0.0011020546722505062,0.0020961818335963381,0.0011007933449973669,0.0020953145045025626,1.5307534734311352e-06,1.5307534734256185e-06,0.00064674079606083133,0.00064674079605850051,1.2613272531341105e-06,8.6732909377338144e-07 +1000,cable:3:core,cable:3:core,0.0011875295144732649,0.0060821869409340432,0.0011875295144732623,0.0060821869409340397,0.0011842919805786728,0.0060323818412918598,4.9910215147663765e-05,4.991021514766739e-05,0.0081187371353865726,0.0081187371353871624,3.2375338945920661e-06,4.9805099642183381e-05 +10000,cable:1:core,cable:1:core,0.0051968947578844198,0.056898361105510734,0.0051968947578844259,0.056898361105510824,0.0051086484699835975,0.056506249875501097,0.00040191867837663007,0.00040191867837654073,0.0070839260769350052,0.0070839260769334309,8.8246287900822264e-05,0.00039211123000963688 +10000,cable:1:core,cable:2:core,0.0052676968239068487,0.0024783732958180761,0.0052676968239072927,0.0024783732958178969,0.0052556743075239708,0.0024792030592478047,1.2051116443585497e-05,1.2051116443130131e-05,0.0020738197714685932,0.0020738197713902314,1.2022516382877851e-05,8.2976342972865616e-07 +10000,cable:1:core,cable:3:core,0.0040559166332564903,0.0011450309278026656,0.0040559166332579258,0.0011450309278046007,0.0040495802099877413,0.001147388226955296,6.7607040420953695e-06,6.7607040414246914e-06,0.0016062531647138068,0.0016062531645544627,6.3364232687490585e-06,2.3572991526303252e-06 +10000,cable:2:core,cable:1:core,0.0052676968239068452,0.0024783732958180761,0.0052676968239068461,0.0024783732958180774,0.0052556743075239743,0.0024792030592478034,1.2051116443123893e-05,1.2051116443123119e-05,0.0020738197713891572,0.002073819771389024,1.2022516382870913e-05,8.2976342972735512e-07 +10000,cable:2:core,cable:2:core,0.011217390349824954,0.043047632316487089,0.011217390349824954,0.043047632316487089,0.011119748056498867,0.042731634526618816,0.00033073950572559544,0.00033073950572559544,0.0074904637291431453,0.0074904637291431453,9.7642293326087592e-05,0.00031599778986827337 +10000,cable:2:core,cable:3:core,0.008034030912603558,0.004866651343792168,0.008034030912603558,0.004866651343792168,0.0080281876013619698,0.0048722508072881719,8.0931006239359349e-06,8.0931006239359349e-06,0.0008617945279457077,0.0008617945279457077,5.8433112415882255e-06,5.5994634960039591e-06 +10000,cable:3:core,cable:1:core,0.0040559166332564877,0.0011450309278026654,0.0040559166332579232,0.0011450309278046009,0.0040495802099877439,0.0011473882269552962,6.7607040420904923e-06,6.7607040414199649e-06,0.0016062531647126471,0.0016062531645533388,6.3364232687438543e-06,2.3572991526307589e-06 +10000,cable:3:core,cable:2:core,0.0080340309126035597,0.004866651343792168,0.0080340309126035597,0.004866651343792168,0.008028187601361968,0.0048722508072881745,8.0931006239402395e-06,8.0931006239402395e-06,0.0008617945279461661,0.0008617945279461661,5.8433112415916949e-06,5.5994634960065612e-06 +10000,cable:3:core,cable:3:core,0.01121739034987806,0.043047632316466661,0.01121739034987806,0.043047632316466661,0.011119703022718263,0.042733943760656479,0.0003285472932987013,0.0003285472932987013,0.0074404406547007801,0.0074404406547007801,9.7687327159796597e-05,0.00031368855581018207 +100000,cable:1:core,cable:1:core,0.025029445696535337,0.51501770227425026,0.02502944569653533,0.51501770227425026,0.024745328786787053,0.51190764993067117,0.0031230030416582472,0.0031230030416582477,0.006093600401213776,0.0060936004012137768,0.00028411690974828457,0.0031100523435790839 +100000,cable:1:core,cable:2:core,0.0029200092671962073,-0.0055306020499575189,0.0029200092671962086,-0.0055306020499575215,0.0029228998850158476,-0.0055351845500160842,5.4180234556469762e-06,5.4180234556498713e-06,0.00086556509062880134,0.00086556509062926386,2.8906178196402496e-06,4.5825000585652392e-06 +100000,cable:1:core,cable:3:core,0.00087908935478322992,-0.0031017341595444893,0.00087908935478321442,-0.0031017341595444793,0.00087257166385033315,-0.0031045060939019955,7.0826488815231745e-06,7.0826488815335388e-06,0.0021963059409219042,0.0021963059409251182,6.5176909328967716e-06,2.7719343575062332e-06 +100000,cable:2:core,cable:1:core,0.0029200092671962078,-0.0055306020499575198,0.0029200092671962091,-0.0055306020499575207,0.0029228998850158493,-0.0055351845500160824,5.4180234556469363e-06,5.4180234556483644e-06,0.00086556509062879505,0.00086556509062902316,2.8906178196415507e-06,4.5825000585626371e-06 +100000,cable:2:core,cable:2:core,0.091870181209119314,0.28525842189891248,0.091870181209119314,0.28525842189891248,0.091488785921329557,0.28371125771885292,0.0015934802683458705,0.0015934802683458705,0.0053454955773149159,0.0053454955773149159,0.00038139528778975695,0.0015471641800595681 +100000,cable:2:core,cable:3:core,0.0087282162587741137,-0.0087087925865141323,0.0087282162587741137,-0.0087087925865141323,0.0087323644231482554,-0.008720517818696474,1.2437376628723592e-05,1.2437376628723592e-05,0.0010078050383296327,0.0010078050383296327,4.1481643741417273e-06,1.1725232182341686e-05 +100000,cable:3:core,cable:1:core,0.000879089354783231,-0.0031017341595444893,0.00087908935478323122,-0.0031017341595444919,0.00087257166385033271,-0.0031045060939019959,7.0826488815342859e-06,7.0826488815351049e-06,0.0021963059409253497,0.0021963059409256039,6.5176909328982895e-06,2.7719343575066668e-06 +100000,cable:3:core,cable:2:core,0.008728216258774112,-0.0087087925865141306,0.008728216258774112,-0.0087087925865141306,0.0087323644231482572,-0.0087205178186964827,1.2437376628734561e-05,1.2437376628734561e-05,0.0010078050383305211,0.0010078050383305211,4.1481643741451968e-06,1.1725232182352094e-05 +100000,cable:3:core,cable:3:core,0.091870181208271853,0.2852584218993296,0.091870181208271853,0.2852584218993296,0.091493043570979815,0.28369441705329945,0.0016088331037954157,0.0016088331037954157,0.0053972647983758573,0.0053972647983758573,0.0003771376372920382,0.0015640048460301426 +1000000,cable:1:core,cable:1:core,0.094696109689700492,4.9430140482755665,0.094696109689700422,4.9430140482755665,0.093180086583616706,4.9164027228102034,0.026654473716088627,0.026654473716088631,0.0054205664587009313,0.0054205664587009322,0.0015160231060837859,0.026611325465363045 +1000000,cable:1:core,cable:2:core,0.00017862565014624984,0.00022534477815653253,0.00017862565014624968,0.00022534477815653394,0.00018018320619215134,0.00022597300058552257,1.6794773759742062e-06,1.6794773759745825e-06,0.0058110348053217161,0.0058110348053230189,1.5575560459015012e-06,6.2822242899004325e-07 +1000000,cable:1:core,cable:3:core,8.1257499331941055e-05,8.9426642061850282e-05,8.1257499331935308e-05,8.9426642061852518e-05,8.1906550548926487e-05,8.9450810318331917e-05,6.4950102917469219e-07,6.4950102916903306e-07,0.0053551492705499453,0.0053551492705032856,6.4905121698543254e-07,2.4168256481635385e-08 +1000000,cable:2:core,cable:1:core,0.00017862565014624982,0.00022534477815653247,0.00017862565014624982,0.00022534477815653239,0.00018018320619215113,0.00022597300058552262,1.6794773759744775e-06,1.6794773759744472e-06,0.0058110348053226563,0.0058110348053225513,1.5575560459013115e-06,6.2822242899015167e-07 +1000000,cable:2:core,cable:2:core,0.75407440792448743,1.5740479450662614,0.75407440792448743,1.5740479450662614,0.75377952051842911,1.568277138147143,0.0057783363591952103,0.0057783363591952103,0.003320840874799719,0.003320840874799719,0.00029488740605831953,0.0057708069191184652 +1000000,cable:2:core,cable:3:core,0.0001535709764889112,0.0010629809235061854,0.0001535709764889112,0.0010629809235061854,0.00015808159292494346,0.001064814054945651,4.8688840104649706e-06,4.8688840104649706e-06,0.0045229487804855561,0.0045229487804855561,4.5106164360322605e-06,1.8331314394655612e-06 +1000000,cable:3:core,cable:1:core,8.1257499331941041e-05,8.9426642061850268e-05,8.1257499331935254e-05,8.9426642061852396e-05,8.1906550548926474e-05,8.9450810318331931e-05,6.4950102917473783e-07,6.4950102916903412e-07,0.0053551492705503209,0.0053551492705032943,6.4905121698543254e-07,2.416825648166249e-08 +1000000,cable:3:core,cable:2:core,0.00015357097648891114,0.0010629809235061854,0.00015357097648891114,0.0010629809235061854,0.00015808159292494367,0.0010648140549456518,4.8688840104655483e-06,4.8688840104655483e-06,0.0045229487804860887,0.0045229487804860887,4.5106164360325315e-06,1.8331314394664285e-06 +1000000,cable:3:core,cable:3:core,0.75407440792450831,1.5740479450662672,0.75407440792450831,1.5740479450662672,0.75369515541135013,1.5684962544665286,0.005564629456123976,0.005564629456123976,0.0031977265857614408,0.0031977265857614408,0.00037925251315817921,0.0055516905997385901 +10000000,cable:1:core,cable:1:core,1.2514510556301524,49.784417123982301,1.2514510556301526,49.784417123982301,0.29944048111231336,48.172201251501221,1.8723151853984727,1.8723151853984725,0.038866378646441546,0.038866378646441539,0.95201057451783899,1.6122158724810802 +10000000,cable:1:core,cable:2:core,1.5377810805855476e-09,4.4664472044009277e-10,1.5377810805855499e-09,4.4664472044009375e-10,3.6173164098261593e-09,-8.4488955667334859e-10,2.447964046819069e-09,2.4479640468190699e-09,0.65899803163799786,0.65899803163799808,2.0795353292406115e-09,1.2915342771134414e-09 +10000000,cable:1:core,cable:3:core,6.8259441253474777e-10,1.2859327231771482e-10,6.8259441253474715e-10,1.285932723177128e-10,1.7322626704311287e-09,-2.8699699888026061e-10,1.1289458468631831e-09,1.1289458468631833e-09,0.64295298585397354,0.64295298585397365,1.0496682578963809e-09,4.1559027119797546e-10 +10000000,cable:2:core,cable:1:core,1.5377810805855464e-09,4.4664472044009266e-10,1.5377810805855464e-09,4.4664472044009209e-10,3.6173164098261606e-09,-8.4488955667334734e-10,2.4479640468190711e-09,2.4479640468190715e-09,0.6589980316379983,0.65899803163799842,2.079535329240614e-09,1.29153427711344e-09 +10000000,cable:2:core,cable:2:core,4.5479347057971937,6.6714050104071516,4.5479347057971937,6.6714050104071516,4.5499969707899197,6.6606240142600166,0.010976466408856866,0.010976466408856866,0.0013607683966721531,0.0013607683966721531,0.0020622649927259928,0.010780996147135014 +10000000,cable:2:core,cable:3:core,3.0492342492444631e-09,1.314231864570851e-08,3.0492342492444631e-09,1.314231864570851e-08,1.6961332535262068e-08,2.3321441922614669e-08,1.7238359243450832e-08,1.7238359243450832e-08,0.59778475460050118,0.59778475460050118,1.3912098286017606e-08,1.0179123276906159e-08 +10000000,cable:3:core,cable:1:core,6.8259441253474497e-10,1.2859327231771355e-10,6.8259441253474539e-10,1.2859327231771213e-10,1.7322626704311266e-09,-2.8699699888026098e-10,1.1289458468631827e-09,1.1289458468631835e-09,0.64295298585397409,0.64295298585397453,1.0496682578963815e-09,4.1559027119797453e-10 +10000000,cable:3:core,cable:2:core,3.0492342492444647e-09,1.314231864570851e-08,3.0492342492444647e-09,1.314231864570851e-08,1.6961332535262058e-08,2.3321441922614619e-08,1.7238359243450792e-08,1.7238359243450792e-08,0.59778475460050073,0.59778475460050073,1.3912098286017593e-08,1.0179123276906109e-08 +10000000,cable:3:core,cable:3:core,4.5479347057971937,6.6714050104071516,4.5479347057971937,6.6714050104071516,4.5501680333556047,6.6604697518265077,0.011160987062473668,0.011160987062473668,0.0013836489863149047,0.0013836489863149047,0.0022333275584109913,0.010935258580643925 diff --git a/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/air_2-Y.csv b/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/air_2-Y.csv new file mode 100644 index 000000000..3de0c5327 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/air_2-Y.csv @@ -0,0 +1,82 @@ +frequency_hz,receiver,source,new_real,new_imaginary,old_real,old_imaginary,fem_real,fem_imaginary,before_absolute_error,after_absolute_error,before_relative_error,after_relative_error,real_absolute_error,imaginary_absolute_error +0.10000000000000001,cable:1:core,cable:1:core,8.7355481512572375,0.15942652432646789,8.7355481508841919,0.15942652467188378,8.7845877913155999,0.16072503167449181,0.04905682882206721,0.049056828458295294,0.0055834856560000087,0.0055834856145966956,0.049039640058362366,0.0012985073480239195 +0.10000000000000001,cable:1:core,cable:2:core,-8.9303183661905415e-14,-3.8246960722041443e-12,-8.930318368991909e-14,-3.8246960722403318e-12,-9.0062134384187483e-14,-3.8506433401169452e-12,2.595836505670303e-14,2.5958365093694138e-14,0.0067394628803151366,0.0067394628899189847,7.5895072228206821e-16,2.5947267912800873e-14 +0.10000000000000001,cable:1:core,cable:3:core,-6.231688185633419,0.15909201351190405,-6.23168818523399,0.15909201315618696,-6.2698668441098802,0.16036739685172985,0.038199955459822722,0.038199955048740124,0.0060906343289310544,0.0060906342633876851,0.03817865847646118,0.0012753833398257974 +0.10000000000000001,cable:2:core,cable:1:core,-6.4528599342827461e-14,3.5086158546484902e-13,-1.1328091516975891e-13,-3.80398025758764e-12,-9.0062134384187546e-14,-3.8506433401169419e-12,5.2120581849254745e-14,4.2015825115186966e-12,0.013531850943084534,1.0908394774904977,2.5533535041360085e-14,4.2015049255817909e-12 +0.10000000000000001,cable:2:core,cable:2:core,-8.1493030322248482e-25,9.0759067020569655e-12,2.4642676020193662e-24,9.0759067020569396e-12,2.4947508256144476e-24,9.1342496320507243e-12,5.8342929993784682e-14,5.8342929993758832e-14,0.0063872712421902738,0.0063872712421874436,3.3096811288369321e-24,5.8342929993758832e-14 +0.10000000000000001,cable:2:core,cable:3:core,-6.4528599342827423e-14,3.5086158546484866e-13,-1.132809151697592e-13,-3.8039802575876384e-12,-9.0049454962162604e-14,-3.8504700965498247e-12,5.1971202315389685e-14,4.2014091941123505e-12,0.013493675951093551,1.0908436148784155,2.5520855619335181e-14,4.2013316820146737e-12 +0.10000000000000001,cable:3:core,cable:1:core,-6.2316881856334199,0.15909201351190413,-6.2316881852339892,0.15909201315618723,-6.2698668441098802,0.16036739685172893,0.038199955459823569,0.038199955048739208,0.0060906343289311889,0.0060906342633875385,0.038178658476460292,0.0012753833398247982 +0.10000000000000001,cable:3:core,cable:2:core,-8.9303183661905743e-14,-3.8246960722041483e-12,-8.9303183689918913e-14,-3.8246960722403375e-12,-9.0049454962163198e-14,-3.8504700965498231e-12,2.5784825962525592e-14,2.5784825999510348e-14,0.006694709194569372,0.006694709204172004,7.4627130025745497e-16,2.5774024345674792e-14 +0.10000000000000001,cable:3:core,cable:3:core,8.7355481512572375,0.15942652432646795,8.7355481508841919,0.15942652467188365,8.784235974623094,0.16067971187912994,0.048703949106892831,0.048703948742858542,0.0055435445203254868,0.0055435444788906486,0.048687823365856531,0.0012531875526619884 +1,cable:1:core,cable:1:core,9.0103275798327527,0.23994395865214355,9.0103275798336604,0.23994395866859686,9.0597588683067194,0.24164199336163972,0.04946044482134742,0.049460444822819465,0.0054574146988789086,0.0054574146990413325,0.049431288473966717,0.0016980347094961712 +1,cable:1:core,cable:2:core,-1.3006519885623142e-12,-3.6714104007700747e-11,-1.3006519886930481e-12,-3.6714104007694498e-11,-1.3103320429960825e-12,-3.6963269005067141e-11,2.4935296141618038e-13,2.493529614150112e-13,0.0067417314047761917,0.0067417314047445807,9.680054433768301e-15,2.4916499736639444e-13 +1,cable:1:core,cable:3:core,-5.9573061454890368,0.23724688395954213,-5.9573061454881913,0.23724688397599955,-5.9951678453843966,0.23896046575464569,0.037900457537375781,0.037900457537275188,0.0063168184094564281,0.0063168184094396628,0.037861699895359813,0.0017135817951035581 +1,cable:2:core,cable:1:core,-1.4362301504123105e-12,4.7159331155725707e-12,-2.1679704977038157e-12,-3.5992194033796974e-11,-1.3103320429960805e-12,-3.6963269005067161e-11,1.2955810738123898e-12,4.1679392267001575e-11,0.03502849760896351,1.1268816146500233,1.2589810741622997e-13,4.1679202120639733e-11 +1,cable:2:core,cable:2:core,-8.8334396494623505e-23,9.0759067020561554e-11,2.3489444218284539e-22,9.0759067020557561e-11,2.4083147716325383e-22,9.1328799511734699e-11,5.6973249117713817e-13,5.6973249117314444e-13,0.0062382566531374818,0.0062382566530937529,3.2916587365787733e-22,5.6973249117314444e-13 +1,cable:2:core,cable:3:core,-1.4362301504123115e-12,4.7159331155725715e-12,-2.1679704977038193e-12,-3.5992194033796994e-11,-1.310449662228344e-12,-3.6964615098722244e-11,1.2965125185608181e-12,4.1680737999408822e-11,0.035052402136222974,1.126876886087925,1.2578048818396747e-13,4.1680548214294817e-11 +1,cable:3:core,cable:1:core,-5.9573061454890368,0.23724688395954194,-5.9573061454881913,0.23724688397599972,-5.9951678453843975,0.2389604657546453,0.037900457537376649,0.037900457537276069,0.006316818409456572,0.0063168184094398085,0.037861699895360701,0.0017135817951033638 +1,cable:3:core,cable:2:core,-1.3006519885623144e-12,-3.6714104007700728e-11,-1.3006519886930526e-12,-3.6714104007694511e-11,-1.3104496622283438e-12,-3.6964615098722231e-11,2.5070261493371604e-13,2.5070261493261335e-13,0.0067779745659640941,0.0067779745659342821,9.7976736660293829e-15,2.5051109102150389e-13 +1,cable:3:core,cable:3:core,9.0103275798327545,0.23994395865214355,9.0103275798336639,0.23994395866859713,9.060004071530674,0.24168574952705965,0.049707018244084328,0.049707018245569813,0.0054844723188977439,0.0054844723190616458,0.049676491697919545,0.0017417908749161048 +10,cable:1:core,cable:1:core,9.4348014435196959,0.40409142612742754,9.4348014435331127,0.40409142627673555,9.4854196678519962,0.40656122474153245,0.050678442533818306,0.050678442554495662,0.0053378719151271972,0.005337871917305107,0.050618224332300343,0.0024697986141049011 +10,cable:1:core,cable:2:core,-2.0095834205662074e-11,-3.4435183438783357e-10,-2.0095834217647568e-11,-3.4435183438689684e-10,-2.022272642239479e-11,-3.4684426740418731e-10,2.4956610301291435e-12,2.4956610298030287e-12,0.007183136914216663,0.0071831369132780232,1.2689221673271646e-13,2.4924330163537337e-12 +10,cable:1:core,cable:3:core,-5.5367765108432021,0.3835905954781802,-5.5367765108299043,0.38359059562742254,-5.5745968707941813,0.38597568113192759,0.037895491297663474,0.037895491293785118,0.0067816531016595787,0.006781653100965521,0.03782035995097921,0.0023850856537473986 +10,cable:2:core,cable:1:core,-4.0296701053309782e-11,7.5606278096443942e-11,-5.2503321288609543e-11,-3.1854092073286003e-10,-2.0222726422394722e-11,-3.4684426740418726e-10,4.2931529645634855e-11,4.2292721342006767e-10,0.1235676847369728,1.2172903459518589,2.007397463091506e-11,4.2245054550063118e-10 +10,cable:2:core,cable:2:core,-1.0463443661702291e-20,9.0759067020308551e-10,2.1161807741033108e-20,9.0759067020246181e-10,2.2848695838332346e-20,9.1328824037484855e-10,5.6975701723867369e-12,5.6975701717630428e-12,0.0062385235246741333,0.0062385235239912221,3.3312139500034635e-20,5.6975701717630428e-12 +10,cable:2:core,cable:3:core,-4.0296701053309762e-11,7.5606278096443968e-11,-5.2503321288609504e-11,-3.1854092073285998e-10,-2.0222374189272243e-11,-3.4681757442413444e-10,4.2914201490957232e-11,4.2290056724645286e-10,0.12352729165793205,1.2173070894390361,2.007432686403752e-11,4.2242385252057842e-10 +10,cable:3:core,cable:1:core,-5.5367765108431994,0.38359059547818058,-5.5367765108299016,0.38359059562742254,-5.574596870794184,0.38597568113192704,0.037895491297668754,0.037895491293790377,0.0067816531016605206,0.0067816531009664586,0.037820359950984539,0.0023850856537464549 +10,cable:3:core,cable:2:core,-2.0095834205662174e-11,-3.4435183438783378e-10,-2.009583421764763e-11,-3.4435183438689653e-10,-2.0222374189272117e-11,-3.4681757442413371e-10,2.468984871491344e-12,2.4689848711696051e-12,0.0071069017650019616,0.0071069017640758457,1.2653998360994283e-13,2.4657400362999313e-12 +10,cable:3:core,cable:3:core,9.4348014435196976,0.40409142612742743,9.4348014435331109,0.40409142627673539,9.4853870112633505,0.40654933037552476,0.05064524612586558,0.050645246146509226,0.0053343940132645172,0.0053343940154388838,0.050585567743652859,0.0024579042480973312 +100,cable:1:core,cable:1:core,10.176084148735754,0.81933149057631394,10.176084148956001,0.81933149183126064,10.22636063424096,0.82372131363032075,0.05046776702050737,0.05046776734907818,0.0049191340818300934,0.0049191341138561566,0.050276485505206026,0.0043898230540068139 +100,cable:1:core,cable:2:core,-3.35237597421708e-10,-3.0841680821338702e-09,-3.3523759845478488e-10,-3.0841680819833742e-09,-3.3701458198409536e-10,-3.1088451762976369e-09,2.474099137582113e-11,2.4740991299912982e-11,0.0079119045809081766,0.0079119045566335613,1.7769845623873643e-12,2.4677094163766667e-11 +100,cable:1:core,cable:3:core,-4.8334558332936481,0.67829861957804283,-4.8334558330735282,0.67829862083298809,-4.8688445230258699,0.68189461716195865,0.035570923043139302,0.035570922951014161,0.0072352105591345283,0.0072352105403960558,0.035388689732221756,0.003595997583915822 +100,cable:2:core,cable:1:core,-1.3380887052430523e-09,1.5974656850471151e-09,-1.581746719695652e-09,-2.122011708447857e-09,-3.3701458198409598e-10,-3.1088451762976394e-09,1.5884578646978357e-09,4.8116017420262758e-09,0.50797184580746435,1.5386988049899819,1.0010741232589563e-09,4.7063108613447547e-09 +100,cable:2:core,cable:2:core,-1.5539876856874734e-18,9.0759067010079656e-09,1.4981391520430641e-18,9.0759067008968554e-09,2.1048044610369403e-18,9.1328214272502451e-09,5.6914726353389725e-11,5.6914726242279626e-11,0.0062318886673475554,0.0062318886551815337,3.6587921467244138e-18,5.691472624227951e-11 +100,cable:2:core,cable:3:core,-1.3380887052430521e-09,1.5974656850471154e-09,-1.5817467196956539e-09,-2.1220117084478578e-09,-3.3697609988902282e-10,-3.1080993569352719e-09,1.5880247940234506e-09,4.8108802534385988e-09,0.50795447214884915,1.5388350036495955,1.0011126053540294e-09,4.7055650419823869e-09 +100,cable:3:core,cable:1:core,-4.8334558332936481,0.67829861957804294,-4.8334558330735273,0.67829862083298798,-4.8688445230258717,0.68189461716195954,0.035570923043142057,0.035570922951016007,0.0072352105591350861,0.0072352105403964279,0.035388689732223533,0.0035959975839165992 +100,cable:3:core,cable:2:core,-3.352375974217081e-10,-3.0841680821338723e-09,-3.3523759845478545e-10,-3.0841680819833763e-09,-3.3697609988902432e-10,-3.1080993569352732e-09,2.3994339083628422e-11,2.3994339008379249e-11,0.0076749631930526302,0.0076749631689830098,1.7385024673162168e-12,2.3931274801400896e-11 +100,cable:3:core,cable:3:core,10.176084148735756,0.81933149057631427,10.176084148956003,0.81933149183126097,10.225799294745068,0.82356401370451804,0.049894989349989934,0.04989498967589838,0.00486357621332503,0.0048635762450933619,0.04971514600931215,0.004232523128203769 +1000,cable:1:core,cable:1:core,11.809017595800965,2.0929924652253051,11.809017599813723,2.0929924735851584,11.859420137790089,2.1041245858916642,0.051617242844457005,0.051617248565724347,0.0042854973213296817,0.0042854977963352131,0.050402541989123861,0.011132120666359047 +1000,cable:1:core,cable:2:core,-6.3490118092788199e-09,-2.4635775208045401e-08,-6.3490118848034141e-09,-2.4635775180730038e-08,-6.3749708759463428e-09,-2.4842626342602489e-08,2.0847367313198535e-10,2.0847365543349496e-10,0.0081284079682633365,0.0081284072781975455,2.5959066667522964e-11,2.0685113455708837e-10 +1000,cable:1:core,cable:3:core,-3.526913027220937,1.2832753628650815,-3.5269130232082677,1.2832753712249108,-3.5571333077239724,1.2895798933132263,0.030870902075776783,0.030870899854939855,0.008158968085081604,0.0081589674981296066,0.030220280503035379,0.0063045304481448294 +1000,cable:2:core,cable:1:core,-4.6439763000069587e-08,4.7556279357279568e-08,-5.2859299514753883e-08,1.5103867257715344e-08,-6.3749708759463561e-09,-2.4842626342602513e-08,6.1290416542563131e-08,8.2745326843814112e-08,2.3897190600529248,3.2262480146737502,4.006479212412323e-08,7.239890569988208e-08 +1000,cable:2:core,cable:2:core,-3.6835580752459175e-16,9.0759066520812581e-08,-8.3130691160873046e-17,9.0759066493688587e-08,1.8282901688147461e-16,9.1265820515709644e-08,5.0675402202112665e-10,5.0675399489736317e-10,0.0055525060658814631,0.0055525057686862658,5.5118482440606636e-16,5.0675399489706342e-10 +1000,cable:2:core,cable:3:core,-4.6439763000069607e-08,4.7556279357279608e-08,-5.2859299514753903e-08,1.5103867257715347e-08,-6.3743632497743299e-09,-2.4841748432286945e-08,6.1290305207370033e-08,8.2744852921313551e-08,2.3898080272141291,3.2263555068449761,4.006539975029528e-08,7.2398027789566556e-08 +1000,cable:3:core,cable:1:core,-3.526913027220937,1.2832753628650815,-3.5269130232082677,1.2832753712249112,-3.5571333077239737,1.2895798933132268,0.030870902075778087,0.03087089985494125,0.0081589680850819458,0.0081589674981299726,0.030220280503036712,0.0063045304481452735 +1000,cable:3:core,cable:2:core,-6.3490118092788257e-09,-2.4635775208045421e-08,-6.3490118848034166e-09,-2.4635775180730051e-08,-6.374363249774339e-09,-2.4841748432286941e-08,2.0752752121780618e-10,2.0752750333304077e-10,0.0080918333559632325,0.0080918326586073321,2.5351440495513369e-11,2.0597322424152052e-10 +1000,cable:3:core,cable:3:core,11.809017595800968,2.0929924652253051,11.809017599813725,2.0929924735851584,11.859496963660357,2.1039752851673796,0.051660316729683735,0.051660322427986148,0.004289055860946459,0.0042890563340433913,0.050479367859388447,0.010982819942074507 +10000,cable:1:core,cable:1:core,16.80529161487884,5.0791514856025755,16.805291651222536,5.0791514818981653,16.875302666406174,5.1182930535137015,0.080209755488829332,0.080209785403679817,0.0045484771326271649,0.0045484788290169938,0.070011051527334445,0.039141567911125996 +10000,cable:1:core,cable:2:core,-1.1301307458540772e-07,-1.0140418883622688e-07,-1.1301307810588102e-07,-1.0140418600667815e-07,-1.1385708023724294e-07,-1.0269428493685997e-07,1.5416532837832709e-09,1.5416528432816889e-09,0.010054594170852258,0.010054591297920525,8.4400565183521943e-10,1.2900961006330971e-09 +10000,cable:1:core,cable:3:core,-0.67949764805676527,1.7210202706210376,-0.67949761171491874,1.7210202669171364,-0.69013383495049541,1.7365328090121379,0.01880872688318418,0.018808703277305393,0.010065440776585359,0.010065428143961344,0.010636186893730137,0.015512538391100339 +10000,cable:2:core,cable:1:core,-7.4501716373140391e-07,1.9267131819503934e-06,-9.0657832770881726e-07,1.7798079901742443e-06,-1.1385708023724289e-07,-1.0269428493685991e-07,2.0426017213327153e-06,2.1252900314202047e-06,13.321757607057339,13.861047088906993,6.3116008349416106e-07,2.0294074668872532e-06 +10000,cable:2:core,cable:2:core,-1.5910507586317832e-13,9.0759044455228302e-07,-1.3778400723881355e-13,9.0759043608914856e-07,1.2129066978187954e-14,9.1282299047690203e-07,5.232554389900984e-09,5.2325459274208202e-09,0.0057322771714669986,0.0057322679007976007,1.7123414284136627e-13,5.2325459246190164e-09 +10000,cable:2:core,cable:3:core,-7.4501716373140391e-07,1.9267131819503934e-06,-9.0657832770881716e-07,1.7798079901742439e-06,-1.1385646462248044e-07,-1.0268645836610756e-07,2.0425947471293261e-06,2.1252827407703683e-06,13.322207291965903,13.861514755372875,6.311606991089235e-07,2.029399640316501e-06 +10000,cable:3:core,cable:1:core,-0.67949764805676138,1.721020270621038,-0.67949761171491496,1.7210202669171399,-0.69013383495049552,1.7365328090121384,0.018808726883183816,0.018808703277307652,0.010065440776585161,0.01006542814396255,0.010636186893734134,0.015512538391100339 +10000,cable:3:core,cable:2:core,-1.1301307505740721e-07,-1.0140418884653474e-07,-1.1301307631509544e-07,-1.0140418535528041e-07,-1.1385646462248041e-07,-1.026864583661075e-07,1.5347729184896534e-09,1.5347706927419842e-09,0.010010092797384488,0.010010078280617582,8.4338956507320005e-10,1.282269519572757e-09 +10000,cable:3:core,cable:3:core,16.805291614878836,5.0791514856025746,16.805291651222536,5.0791514818981671,16.874517717130121,5.1178992113719639,0.079332431832233208,0.079332461736701429,0.0044989472947478187,0.0044989489906321284,0.069226102251285226,0.038747725769389341 +100000,cable:1:core,cable:1:core,25.147933625985093,8.0354712506157853,25.147933620553914,8.0354712542214894,25.246943314395164,8.093523757030372,0.11477374506790895,0.11477374220646004,0.0043290408174478665,0.0043290407095196194,0.099009688410070851,0.058052506414586702 +100000,cable:1:core,cable:2:core,-1.3387580438105773e-07,1.5904860038167041e-07,-1.3387578410475514e-07,1.5904860038606757e-07,-1.3577893758098125e-07,1.6103548998290223e-07,2.7513130363666894e-09,2.7512990139449113e-09,0.013061810300911754,0.01306174372971073,1.9031331999235123e-09,1.9868896012318186e-09 +100000,cable:1:core,cable:3:core,0.083093302642308284,-0.071868316820341893,0.083093297211124295,-0.071868313214637519,0.083673556585100742,-0.072751432863992438,0.0010566935488419962,0.0010566875530066511,0.0095301959990094828,0.0095301419232690938,0.00058025394279245801,0.00088311604365054475 +100000,cable:2:core,cable:1:core,1.708396352158113e-05,8.1698742789972538e-06,1.7003931797506463e-05,8.4429904748540932e-06,-1.3577893758098135e-07,1.6103548998290223e-07,1.9035767976467474e-05,1.8991077618405211e-05,90.371974019044487,90.159807329331869,1.7219742459162112e-05,8.0088387890143524e-06 +100000,cable:2:core,cable:2:core,-5.5545680886170519e-11,9.0758164887964398e-06,-5.4864148189373549e-11,9.0758159057763065e-06,3.4007959212112756e-13,9.1282698216414564e-06,5.2453944914515099e-08,5.2453362616407385e-08,0.0057463184085724947,0.0057462546179397576,5.5885760478291646e-11,5.2453332845016625e-08 +100000,cable:2:core,cable:3:core,1.7083963521581134e-05,8.1698742789972572e-06,1.7003931797506473e-05,8.4429904748541e-06,-1.3573623352103367e-07,1.6102087150634743e-07,1.9035735886080225e-05,1.8991045062306193e-05,90.388429427122801,90.176221535874802,1.7219699755102169e-05,8.0088534074909104e-06 +100000,cable:3:core,cable:1:core,0.083093302642306133,-0.071868316820346778,0.083093297211126876,-0.071868313214642834,0.083673556585100728,-0.07275143286399241,0.0010566935488361059,0.0010566875530037189,0.0095301959989563621,0.0095301419232426531,0.00058025394279459519,0.00088311604364563201 +100000,cable:3:core,cable:2:core,-1.3387580348475153e-07,1.5904859744381848e-07,-1.3387578295842361e-07,1.5904859727845923e-07,-1.357362335210337e-07,1.6102087150634751e-07,2.7112989370241977e-09,2.7112847319353321e-09,0.012874209544178996,0.012874142093375932,1.8604300362821695e-09,1.9722740625290317e-09 +100000,cable:3:core,cable:3:core,25.147933625985093,8.0354712506157835,25.147933620553918,8.0354712542214912,25.246885913353953,8.0934120106660856,0.11466772653085855,0.11466772366597382,0.0043250564878453013,0.0043250563797871001,0.09895228736885997,0.057940760050302131 +1000000,cable:1:core,cable:1:core,40.833765344235459,19.54915991569672,40.833765344235985,19.54915991569575,40.899774874633195,19.655440515981347,0.12511124689781913,0.12511124689727263,0.0027571137750741089,0.0027571137750620651,0.066009530397735716,0.10628060028462727 +1000000,cable:1:core,cable:2:core,-3.6994881866651507e-09,-1.4563318927035237e-08,-3.6994907304567155e-09,-1.4563314765271918e-08,-3.7856608852758189e-09,-1.4741395028839053e-08,1.9783294936306376e-10,1.9783031117472364e-10,0.012998459609595534,0.012998286269488681,8.6172698610668224e-11,1.7807610180381567e-10 +1000000,cable:1:core,cable:3:core,-2.7224978265894648e-05,-2.5263791755826295e-05,-2.7224977747745251e-05,-2.5263792729529432e-05,-2.7519306512046415e-05,-2.5393594081990994e-05,3.2167967389299072e-07,3.2167959270140555e-07,0.0085906666931782494,0.0085906645249037121,2.9432824615176719e-07,1.2980232616469897e-07 +1000000,cable:2:core,cable:1:core,-7.7982181077902926e-06,5.4374206625622219e-06,-7.8026985217373471e-06,5.4423466057092621e-06,-3.7856608852758205e-09,-1.4741395028839069e-08,9.5185530023719264e-06,9.5120580460592919e-06,625.40909965438709,624.98235361654213,7.794432446905017e-06,5.4521620575910613e-06 +1000000,cable:2:core,cable:2:core,-2.4740419503128501e-08,9.0711568120914278e-05,-2.4719343778092328e-08,9.0711547556388583e-05,1.0616793549703688e-11,9.1268397209087773e-05,5.5739851691683139e-07,5.5737890813510746e-07,0.0061072455960837924,0.0061070307486412647,2.4751036296678206e-08,5.5682908817349434e-07 +1000000,cable:2:core,cable:3:core,-7.7982181077902926e-06,5.4374206625622211e-06,-7.8026985217373471e-06,5.4423466057092638e-06,-3.7859611766932275e-09,-1.4744688010955839e-08,9.5185546442339522e-06,9.5120596874785329e-06,625.27510405166106,624.84844948982425,7.7944321466135989e-06,5.452165350573177e-06 +1000000,cable:3:core,cable:1:core,-2.7224978265894644e-05,-2.5263791755826299e-05,-2.7224977747745251e-05,-2.5263792729529435e-05,-2.7519306512046418e-05,-2.5393594081990998e-05,3.2167967389299379e-07,3.2167959270141174e-07,0.008590666693178331,0.0085906645249038752,2.9432824615177396e-07,1.2980232616469897e-07 +1000000,cable:3:core,cable:2:core,-3.6994881866651494e-09,-1.4563318927035239e-08,-3.6994907304567147e-09,-1.4563314765271921e-08,-3.7859611766932317e-09,-1.4744688010955837e-08,2.0093131244845993e-10,2.009286505369354e-10,0.013199204290386133,0.013199029429068665,8.6472990028082259e-11,1.813690839205985e-10 +1000000,cable:3:core,cable:3:core,40.833765344235466,19.549159915696727,40.833765344235985,19.54915991569575,40.898210042511892,19.654888417134927,0.12382098025959387,0.12382098025902959,0.0027287789812792582,0.0027287789812668224,0.064444698276425072,0.10572850143820034 +10000000,cable:1:core,cable:1:core,80.846531351828517,55.108195401013525,80.846531351828517,55.108195401013525,80.822020676031087,55.21746647896061,0.11198634605952913,0.11198634605952913,0.0011440785816614484,0.0011440785816614484,0.024510675797429826,0.1092710779470849 +10000000,cable:1:core,cable:2:core,-1.1003843828678816e-13,-1.3743099171584897e-13,-1.1003845571285191e-13,-1.3743080710577727e-13,-4.8478322004284232e-13,-3.0087161821462218e-13,4.0883558691564161e-13,4.08835529086825e-13,0.71655166917326008,0.71655156781873908,3.7474478175605415e-13,1.6344062649877321e-13 +10000000,cable:1:core,cable:3:core,-2.7205597053841646e-16,1.48022349643543e-17,-2.7205597045460625e-16,1.4802234773622071e-17,-5.1434173424526559e-12,-6.19833980581388e-12,8.0542872611412412e-12,8.0542872611413333e-12,0.99997984522328609,0.99997984522329753,5.1431452864821176e-12,6.1983546080488442e-12 +10000000,cable:2:core,cable:1:core,-9.3439526702823559e-11,5.4261360026858313e-10,-9.3440467076589953e-11,5.4261406955040871e-10,-4.8478322004284232e-13,-3.0087161821462192e-13,5.5081520722050515e-10,5.5081458597251225e-10,965.39432664727224,965.39323780789391,9.2954743482780711e-11,5.4291447188679773e-10 +10000000,cable:2:core,cable:2:core,-1.6761991871629861e-05,0.0008877789650905823,-1.6761328755330462e-05,0.00088777830561370815,3.9349460155433293e-10,0.00087974894643819731,1.8585638045362502e-05,1.8586520995702771e-05,0.021126070250618127,0.021127073889603824,1.6762385366231415e-05,8.0300186523849914e-06 +10000000,cable:2:core,cable:3:core,-9.3439526702823559e-11,5.4261360026858313e-10,-9.3440467076589953e-11,5.4261406955040871e-10,-4.7137805126024283e-13,-3.1558936686029454e-13,5.5083197623796477e-10,5.5083135497346052e-10,971.02459274786884,971.02349756251613,9.2968148651563312e-11,5.4292918963544343e-10 +10000000,cable:3:core,cable:1:core,-2.7205597053841637e-16,1.4802234964354346e-17,-2.7205597045460621e-16,1.4802234773622071e-17,-5.1434173424526615e-12,-6.1983398058138841e-12,8.0542872611412476e-12,8.0542872611413397e-12,0.99997984522328609,0.99997984522329753,5.1431452864821232e-12,6.1983546080488482e-12 +10000000,cable:3:core,cable:2:core,-1.100384382867881e-13,-1.3743099171584894e-13,-1.1003845571285191e-13,-1.3743080710577727e-13,-4.7137805126024222e-13,-3.1558936686029429e-13,4.0287315091000468e-13,4.0287308490133031e-13,0.71019794450428997,0.71019782814204357,3.613396129734541e-13,1.7815837514444535e-13 +10000000,cable:3:core,cable:3:core,80.846531351828517,55.108195401013525,80.846531351828517,55.108195401013525,80.821985061039811,55.21927686937088,0.11376121045378251,0.11376121045378251,0.0011621992327080089,0.0011621992327080089,0.02454629078870596,0.11108146835735511 diff --git a/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/air_2-Z.csv b/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/air_2-Z.csv new file mode 100644 index 000000000..aa77ca078 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/air_2-Z.csv @@ -0,0 +1,82 @@ +frequency_hz,receiver,source,new_real,new_imaginary,old_real,old_imaginary,fem_real,fem_imaginary,before_absolute_error,after_absolute_error,before_relative_error,after_relative_error,real_absolute_error,imaginary_absolute_error +0.10000000000000001,cable:1:core,cable:1:core,3.1374615586823512e-06,1.2435841206410953e-06,3.1374615586823521e-06,1.2435841206410949e-06,3.1569374848695469e-06,1.2375960715466406e-06,2.0375682388736233e-08,2.037568238873717e-08,0.0060090100516075971,0.0060090100516078729,1.9475926187195662e-08,5.9880490944547412e-09 +0.10000000000000001,cable:1:core,cable:2:core,9.8692096479705197e-08,7.1450042872923159e-07,9.86920964797057e-08,7.1450042872923138e-07,9.8559280935640084e-08,7.1180019648279383e-07,2.7034966531228829e-09,2.7034966531230698e-09,0.0037622173631328983,0.0037622173631331585,1.3281554406511331e-10,2.700232246437763e-09 +0.10000000000000001,cable:1:core,cable:3:core,9.9020834428301869e-08,7.2818833938952072e-07,9.9020834415946147e-08,7.2818833937590774e-07,9.8887228230869388e-08,7.254576929504737e-07,2.7339130405750742e-09,2.7339130547756163e-09,0.003734005367946915,0.0037340053873421552,1.3360619743248095e-10,2.7306464390470207e-09 +0.10000000000000001,cable:2:core,cable:1:core,9.8692096479705197e-08,7.1450042872923149e-07,9.8692096479705673e-08,7.1450042872923117e-07,9.8559280935640018e-08,7.1180019648279383e-07,2.7034966531226736e-09,2.7034966531229672e-09,0.0037622173631326073,0.0037622173631330158,1.3281554406517948e-10,2.7002322464376571e-09 +0.10000000000000001,cable:2:core,cable:2:core,3.1368030409187459e-06,1.2442491570135631e-06,3.1368030409187459e-06,1.2442491570135629e-06,3.1562805382084363e-06,1.238155489839502e-06,2.0408470797639916e-08,2.0408470797639982e-08,0.0060194028590558422,0.0060194028590558613,1.9477497289690336e-08,6.0936671740610731e-09 +0.10000000000000001,cable:2:core,cable:3:core,9.8692096479705184e-08,7.1450042872923138e-07,9.869209647970566e-08,7.1450042872923106e-07,9.8559278088182289e-08,7.118103566479389e-07,2.6933489427986132e-09,2.6933489427989069e-09,0.0037480432007590903,0.0037480432007594988,1.3281839152289454e-10,2.690072081292481e-09 +0.10000000000000001,cable:3:core,cable:1:core,9.9020834428301896e-08,7.2818833938952072e-07,9.9020834415946173e-08,7.2818833937590774e-07,9.8887228230869441e-08,7.2545769295047381e-07,2.7339130405749671e-09,2.7339130547755092e-09,0.003734005367946768,0.0037340053873420082,1.3360619743245448e-10,2.7306464390469148e-09 +0.10000000000000001,cable:3:core,cable:2:core,9.8692096479705197e-08,7.1450042872923159e-07,9.8692096479705687e-08,7.1450042872923138e-07,9.8559278088182408e-08,7.11810356647939e-07,2.69334894279882e-09,2.6933489427990074e-09,0.0037480432007593774,0.003748043200759638,1.3281839152278866e-10,2.6900720812925869e-09 +0.10000000000000001,cable:3:core,cable:3:core,3.1374615586823512e-06,1.2435841206410953e-06,3.1374615586823521e-06,1.2435841206410949e-06,3.1569374867215201e-06,1.2376681473795957e-06,2.0354618950440301e-08,2.0354618950441234e-08,0.0060027516412558962,0.0060027516412561712,1.9475928039168827e-08,5.9159732614996572e-09 +1,cable:1:core,cable:1:core,4.0462625840379406e-06,1.0981370487568926e-05,4.04626258403795e-06,1.0981370487568925e-05,4.0643495662368124e-06,1.0930986995002525e-05,5.353162848494773e-08,5.3531628484952468e-08,0.0045902079069174116,0.0045902079069178175,1.8086982198871778e-08,5.0383492566401846e-08 +1,cable:1:core,cable:2:core,9.8663957192484381e-07,5.698292513397585e-06,9.8663957192484614e-07,5.6982925133975858e-06,9.8528249805016315e-07,5.6801074022037254e-06,1.8235677081874812e-08,1.8235677081873796e-08,0.0031632095319905572,0.0031632095319903812,1.3570738746806621e-09,1.8185111193859563e-08 +1,cable:1:core,cable:3:core,9.9679391285692425e-07,5.8280008393635153e-06,9.9679391273336713e-07,5.828000839227387e-06,9.9541834849023757e-07,5.809535968222851e-06,1.8516037324468971e-08,1.8516037469400272e-08,0.0031414008226381077,0.003141400847226917,1.3755643666866825e-09,1.8464871140664282e-08 +1,cable:2:core,cable:1:core,9.8663957192484423e-07,5.6982925133975858e-06,9.8663957192484656e-07,5.6982925133975867e-06,9.8528249805016272e-07,5.6801074022037271e-06,1.8235677081874031e-08,1.8235677081873012e-08,0.0031632095319904206,0.0031632095319902441,1.3570738746815091e-09,1.8185111193858716e-08 +1,cable:2:core,cable:2:core,4.0258226362737272e-06,1.1002346949218491e-05,4.0258226362737365e-06,1.1002346949218488e-05,4.0439534803421229e-06,1.0950998663924164e-05,5.4455246848209535e-08,5.4455246848215835e-08,0.0046647348682673602,0.0046647348682679005,1.8130844068395749e-08,5.1348285294327398e-08 +1,cable:2:core,cable:3:core,9.8663957192484402e-07,5.6982925133975867e-06,9.8663957192484656e-07,5.6982925133975875e-06,9.8528570159125359e-07,5.6801179712998549e-06,1.8224899048895695e-08,1.822489904889466e-08,0.0031613339346576808,0.0031613339346575008,1.3538703335904317e-09,1.8174542097731795e-08 +1,cable:3:core,cable:1:core,9.9679391285692425e-07,5.8280008393635153e-06,9.9679391273336713e-07,5.828000839227387e-06,9.9541834849023778e-07,5.8095359682228527e-06,1.8516037324467267e-08,1.8516037469398565e-08,0.003141400822637818,0.0031414008472266264,1.3755643666864708e-09,1.8464871140662588e-08 +1,cable:3:core,cable:2:core,9.8663957192484381e-07,5.6982925133975858e-06,9.8663957192484614e-07,5.6982925133975858e-06,9.8528570159125359e-07,5.6801179712998583e-06,1.8224899048890593e-08,1.8224899048890421e-08,0.0031613339346567939,0.003161333934656764,1.3538703335902199e-09,1.817454209772756e-08 +1,cable:3:core,cable:3:core,4.0462625840379398e-06,1.0981370487568926e-05,4.04626258403795e-06,1.0981370487568925e-05,4.0643564026206317e-06,1.0930589072675371e-05,5.3908611274027899e-08,5.3908611274032908e-08,0.0046226801673670001,0.0046226801673674295,1.8093818582691863e-08,5.078141489355552e-08 +10,cable:1:core,cable:1:core,1.4059707866181092e-05,9.4699367070903535e-05,1.4059707866181184e-05,9.4699367070903468e-05,1.4071669118458445e-05,9.4290920843765721e-05,4.0862133084201546e-07,4.0862133084208587e-07,0.0042861563423491648,0.0042861563423499029,1.1961252277352996e-08,4.0844622713781423e-07 +10,cable:1:core,cable:2:core,9.8448383311020772e-06,4.2519807808567783e-05,9.8448383311021314e-06,4.2519807808567804e-05,9.8319503770437314e-06,4.2421165884598937e-05,9.9480292139152401e-08,9.948029213912523e-08,0.0022845064943293734,0.0022845064943287493,1.2887954058345795e-08,9.8641923968846373e-08 +10,cable:1:core,cable:3:core,1.0145003866507134e-05,4.3592250138396668e-05,1.0145003866507116e-05,4.3592250138396838e-05,1.0131530298472416e-05,4.3491727218213369e-05,1.0142186410137432e-07,1.014218641012089e-07,0.002271169784242826,0.0022711697842391215,1.3473568034717975e-08,1.0052292018329905e-07 +10,cable:2:core,cable:1:core,9.8448383311020755e-06,4.2519807808567763e-05,9.8448383311021297e-06,4.251980780856779e-05,9.831950377043728e-06,4.2421165884598896e-05,9.9480292139179506e-08,9.9480292139145598e-08,0.0022845064943299979,0.002284506494329219,1.2887954058347489e-08,9.8641923968866702e-08 +10,cable:2:core,cable:2:core,1.3443861420288121e-05,9.5357297592647101e-05,1.3443861420288216e-05,9.5357297592647074e-05,1.3457280759510354e-05,9.4945816929939975e-05,4.1169942245166172e-07,4.1169942245169195e-07,0.0042932417524727956,0.0042932417524731105,1.3419339222232337e-08,4.114806627071257e-07 +10,cable:2:core,cable:3:core,9.8448383311020755e-06,4.2519807808567763e-05,9.8448383311021314e-06,4.251980780856779e-05,9.8319495296934822e-06,4.2421085994381516e-05,9.9559619314161443e-08,9.9559619314127337e-08,0.0022863322926298967,0.0022863322926291131,1.2888801408593285e-08,9.8721814186246599e-08 +10,cable:3:core,cable:1:core,1.0145003866507133e-05,4.3592250138396662e-05,1.0145003866507114e-05,4.3592250138396831e-05,1.0131530298472415e-05,4.349172721821339e-05,1.0142186410134746e-07,1.0142186410118203e-07,0.0022711697842422232,0.0022711697842385187,1.3473568034717975e-08,1.0052292018327194e-07 +10,cable:3:core,cable:2:core,9.8448383311020755e-06,4.2519807808567777e-05,9.8448383311021297e-06,4.2519807808567797e-05,9.8319495296934873e-06,4.242108599438155e-05,9.955961931413369e-08,9.9559619314106519e-08,0.0022863322926292575,0.0022863322926286334,1.2888801408588203e-08,9.8721814186226271e-08 +10,cable:3:core,cable:3:core,1.4059707866181092e-05,9.4699367070903535e-05,1.4059707866181184e-05,9.4699367070903468e-05,1.4071863013141068e-05,9.4295511143946075e-05,4.0403880672063024e-07,4.040388067207007e-07,0.0042378857327233267,0.0042378857327240657,1.2155146959975677e-08,4.0385592695745985e-07 +100,cable:1:core,cable:1:core,0.00011750705949953433,0.00077809535148733427,0.00011750705949953411,0.00077809535148733395,0.00011823590195980419,0.00077395987792192587,4.1992086090216516e-06,4.1992086090219345e-06,0.0053633911681116551,0.005363391168112016,7.2884246026986383e-07,4.1354735654084047e-06 +100,cable:1:core,cable:2:core,9.6940022347522206e-05,0.00028095440727846668,9.6940022347522735e-05,0.00028095440727846685,9.6825542710659164e-05,0.0002806142357194754,3.5891820349919017e-07,3.5891820349886745e-07,0.0012090921092759823,0.0012090921092748951,1.1447963686304246e-07,3.4017155899128361e-07 +100,cable:1:core,cable:3:core,0.00010470316710127832,0.00028484344129470137,0.00010470316710127808,0.00028484344129470283,0.00010457534806939449,0.00028449911915146356,3.672811501241812e-07,3.6728115012289393e-07,0.0012117085875665494,0.0012117085875623026,1.2781903188382549e-07,3.4432214323780767e-07 +100,cable:2:core,cable:1:core,9.6940022347522192e-05,0.00028095440727846668,9.6940022347522721e-05,0.00028095440727846685,9.6825542710659096e-05,0.0002806142357194754,3.5891820349920742e-07,3.589182034988847e-07,0.0012090921092760404,0.0012090921092749532,1.1447963686309667e-07,3.4017155899128361e-07 +100,cable:2:core,cable:2:core,0.00010028978112835039,0.00079835650985762038,0.00010028978112835013,0.00079835650985761984,0.00010105820712052285,0.00079409519275426034,4.3300464155515501e-06,4.3300464155520372e-06,0.0054091786073077212,0.0054091786073083301,7.6842599217246223e-07,4.2613171033600416e-06 +100,cable:2:core,cable:3:core,9.6940022347522192e-05,0.00028095440727846668,9.6940022347522707e-05,0.00028095440727846685,9.6823581689952951e-05,0.00028060397630884999,3.6926994353979856e-07,3.6926994353948182e-07,0.0012440074648782961,0.001244007464877229,1.1644065756924156e-07,3.5043096961669172e-07 +100,cable:3:core,cable:1:core,0.00010470316710127832,0.00028484344129470142,0.00010470316710127808,0.00028484344129470288,0.00010457534806939445,0.00028449911915146345,3.672811501243478e-07,3.6728115012306053e-07,0.0012117085875670995,0.0012117085875628527,1.2781903188386614e-07,3.443221432379703e-07 +100,cable:3:core,cable:2:core,9.6940022347522206e-05,0.00028095440727846663,9.6940022347522735e-05,0.00028095440727846679,9.682358168995291e-05,0.00028060397630884999,3.6926994353976849e-07,3.6926994353944747e-07,0.0012440074648781951,0.0012440074648771135,1.1644065756929577e-07,3.5043096961663751e-07 +100,cable:3:core,cable:3:core,0.00011750705949953431,0.00077809535148733427,0.0001175070594995341,0.00077809535148733395,0.00011823922376701001,0.00077400080901010473,4.1594882632846649e-06,4.159488263284947e-06,0.0053123808674385969,0.0053123808674389577,7.3216426747569924e-07,4.0945424772295376e-06 +1000,cable:1:core,cable:1:core,0.0011876763663740096,0.0060820686216245231,0.0011876763663740098,0.0060820686216245231,0.0011847445460254178,0.0060323886843270886,4.9766371581957252e-05,4.9766371581957238e-05,0.0080952148868409929,0.0080952148868409912,2.9318203485918619e-06,4.9679937297434501e-05 +1000,cable:1:core,cable:2:core,0.00088058638790435234,0.0014022673190863706,0.00088058638790435755,0.0014022673190863721,0.00087953493666668525,0.0014022047693677529,1.0533101027247158e-06,1.0533101027194308e-06,0.00063635578367444621,0.00063635578367125323,1.0514512376670919e-06,6.2549718617740252e-08 +1000,cable:1:core,cable:3:core,0.0010032868752651442,0.0012637996350769367,0.0010032868752651413,0.0012637996350769523,0.0010024719369142027,0.0012644746811176577,1.0582115444950828e-06,1.0582115445072131e-06,0.00065579006147741805,0.00065579006148493548,8.1493835094145935e-07,6.7504604072104318e-07 +1000,cable:2:core,cable:1:core,0.00088058638790435234,0.0014022673190863708,0.00088058638790435766,0.0014022673190863724,0.00087953493666668547,0.0014022047693677531,1.0533101027246076e-06,1.0533101027192144e-06,0.00063635578367438072,0.00063635578367112237,1.051451237666875e-06,6.2549718617740252e-08 +1000,cable:2:core,cable:2:core,0.00078848453847925456,0.0066685046727502926,0.00078848453847925489,0.0066685046727502926,0.00078579364356049853,0.006617301561681695,5.127377008341523e-05,5.1273770083415217e-05,0.0076943809230435277,0.007694380923043526,2.6908949187560332e-06,5.1203111068597561e-05 +1000,cable:2:core,cable:3:core,0.00088058638790435245,0.0014022673190863711,0.00088058638790435777,0.0014022673190863726,0.00087949964287293734,0.0014022044524770085,1.0885618833534022e-06,1.0885618833480108e-06,0.0006576606542476642,0.00065766065424440693,1.086745031415111e-06,6.2866609362504863e-08 +1000,cable:3:core,cable:1:core,0.0010032868752651442,0.0012637996350769365,0.0010032868752651413,0.0012637996350769521,0.0010024719369142029,0.0012644746811176582,1.0582115444953307e-06,1.0582115445074611e-06,0.00065579006147757158,0.00065579006148508889,8.1493835094124251e-07,6.750460407216937e-07 +1000,cable:3:core,cable:2:core,0.00088058638790435234,0.0014022673190863706,0.00088058638790435755,0.0014022673190863721,0.00087949964287293778,0.0014022044524770085,1.0885618833527278e-06,1.0885618833474446e-06,0.00065766065424725665,0.00065766065424406476,1.0867450314145689e-06,6.2866609362071182e-08 +1000,cable:3:core,cable:3:core,0.0011876763663740096,0.0060820686216245257,0.0011876763663740098,0.0060820686216245257,0.0011846135175098611,0.0060322148368300316,4.9947781747544514e-05,4.99477817475445e-05,0.0081249826848652489,0.0081249826848652472,3.0628488641485339e-06,4.9853784794494024e-05 +10000,cable:1:core,cable:1:core,0.011216148512475915,0.043043091699419979,0.011216148512475916,0.043043091699419986,0.011128446407467421,0.042735853528070605,0.00031951048990139962,0.00031951048990139247,0.0072351251299446187,0.0072351251299444565,8.7702105008493486e-05,0.00030723817134937353 +10000,cable:1:core,cable:2:core,0.0052675025954369396,0.0024772829778800789,0.0052675025954369405,0.0024772829778800798,0.0052548205937773158,0.0024807428002432495,1.3145475909202062e-05,1.3145475909201454e-05,0.0022621876764855331,0.0022621876764854281,1.2682001659623798e-05,3.4598223631705435e-06 +10000,cable:1:core,cable:3:core,0.0043841369179726019,-0.00021988065450779786,0.0043841369179740434,-0.00021988065450588795,0.0043854879754549108,-0.00021452961784576972,5.5189627335702372e-06,5.5189627357749251e-06,0.0012569573134158856,0.0012569573139180087,1.3510574823089733e-06,5.3510366620281328e-06 +10000,cable:2:core,cable:1:core,0.0052675025954369422,0.0024772829778800794,0.0052675025954373872,0.0024772829778798994,0.0052548205937773132,0.0024807428002432538,1.3145475909684139e-05,1.3145475909207501e-05,0.0022621876765684932,0.0022621876764864694,1.2682001659629002e-05,3.4598223631744467e-06 +10000,cable:2:core,cable:2:core,0.0051972641718355519,0.056899313416343932,0.0051972641718355589,0.056899313416344022,0.0051018633482606017,0.056507679879895176,0.00040308577748440804,0.00040308577748431875,0.0071043946307819683,0.0071043946307803949,9.540082357495025e-05,0.00039163353644875581 +10000,cable:2:core,cable:3:core,0.0052675025954369422,0.0024772829778800798,0.0052675025954373881,0.0024772829778799002,0.0052547371859823222,0.002481082933084129,1.3318984124535541e-05,1.3318984124057023e-05,0.0022920189498846929,0.0022920189498023464,1.2765409454620034e-05,3.799955204049181e-06 +10000,cable:3:core,cable:1:core,0.0043841369179726027,-0.00021988065450779786,0.0043841369179740443,-0.00021988065450588793,0.0043854879754549082,-0.0002145296178457685,5.5189627335705447e-06,5.518962735775258e-06,0.0012569573134159566,0.0012569573139180853,1.3510574823055038e-06,5.3510366620293525e-06 +10000,cable:3:core,cable:2:core,0.0052675025954369405,0.0024772829778800794,0.0052675025954369414,0.0024772829778800802,0.0052547371859823248,0.0024810829330841307,1.3318984124054069e-05,1.3318984124053485e-05,0.0022920189498018368,0.0022920189498017362,1.2765409454615698e-05,3.7999552040513494e-06 +10000,cable:3:core,cable:3:core,0.011216148512475916,0.043043091699419986,0.011216148512475916,0.043043091699419986,0.011128386709664324,0.042738758631725257,0.00031673451047403256,0.00031673451047403256,0.007171810600624876,0.007171810600624876,8.7761802811591946e-05,0.00030433306769472868 +100000,cable:1:core,cable:1:core,0.09187042463772542,0.2852670576877403,0.09187042463772542,0.2852670576877403,0.091484378845383393,0.28368057113451289,0.0016327801252331434,0.0016327801252331434,0.0054778925830629103,0.0054778925830629103,0.00038604579234202685,0.0015864865532274064 +100000,cable:1:core,cable:2:core,0.0029207944960417953,-0.0055282682560884866,0.0029207944960417966,-0.0055282682560884875,0.0029219881236597529,-0.0055335411369923137,5.4062944718432588e-06,5.4062944718443921e-06,0.00086395064956048141,0.00086395064956066247,1.1936276179576322e-06,5.2728809038270555e-06 +100000,cable:1:core,cable:3:core,-0.0014311466171358115,-0.00035959674779834989,-0.001431146617135812,-0.00035959674779835108,-0.0014359728388020337,-0.00035884378404194684,4.8846054078058608e-06,4.8846054078061056e-06,0.0033001176488430565,0.0033001176488432218,4.8262216662221455e-06,7.5296375640304756e-07 +100000,cable:2:core,cable:1:core,0.0029207944960417944,-0.0055282682560884857,0.0029207944960417962,-0.0055282682560884883,0.0029219881236597525,-0.0055335411369923076,5.4062944718364918e-06,5.4062944718394124e-06,0.00086395064955940067,0.00086395064955986742,1.1936276179580659e-06,5.2728809038218513e-06 +100000,cable:2:core,cable:2:core,0.025028674693899085,0.51501620085517463,0.025028674693899078,0.51501620085517463,0.024742017976365958,0.51193252213577378,0.0030969737677598325,0.0030969737677598334,0.0060425210873081264,0.0060425210873081281,0.00028665671753312638,0.0030836787194008508 +100000,cable:2:core,cable:3:core,0.0029207944960417953,-0.0055282682560884875,0.0029207944960417966,-0.0055282682560884883,0.0029209104549473086,-0.0055328076670412953,4.5408917919535732e-06,4.5408917919544736e-06,0.00072578892127079446,0.00072578892127093844,1.1595890551333726e-07,4.5394109528078264e-06 +100000,cable:3:core,cable:1:core,-0.0014311466171358113,-0.00035959674779834983,-0.0014311466171358115,-0.00035959674779835097,-0.0014359728388020332,-0.00035884378404194673,4.8846054078058608e-06,4.8846054078058998e-06,0.0033001176488430574,0.0033001176488430838,4.8262216662219286e-06,7.5296375640310177e-07 +100000,cable:3:core,cable:2:core,0.0029207944960417949,-0.0055282682560884866,0.0029207944960417962,-0.0055282682560884875,0.0029209104549473073,-0.0055328076670413005,4.540891791959621e-06,4.5408917919605205e-06,0.0007257889212717607,0.00072578892127190446,1.159589055124699e-07,4.5394109528138979e-06 +100000,cable:3:core,cable:3:core,0.091870424637725434,0.2852670576877403,0.091870424637725434,0.2852670576877403,0.091483516547271729,0.28368158908783159,0.0016319952670749466,0.0016319952670749466,0.0054752464898157127,0.0054752464898157127,0.00038690809045370533,0.0015854685999087126 +1000000,cable:1:core,cable:1:core,0.75407446569154857,1.5740479213690892,0.75407446569154857,1.5740479213690892,0.75370816564133836,1.5682972345165187,0.0057623411043699439,0.0057623411043699439,0.0033116726712239556,0.0033116726712239556,0.00036630005021021095,0.005750686852570519 +1000000,cable:1:core,cable:2:core,0.00017862953015392792,0.00022533894904642603,0.00017862953015392795,0.00022533894904642595,0.00018016270749890044,0.00022577200740065841,1.5931642442973085e-06,1.5931642442973125e-06,0.005515631756989766,0.0055156317569897799,1.5331773449725158e-06,4.3305835423237634e-07 +1000000,cable:1:core,cable:3:core,2.3887617723989342e-07,1.4069502440255183e-06,2.3887617723989184e-07,1.4069502440255175e-06,2.4979350568131865e-07,1.4123901481913753e-06,1.219756605359054e-08,1.2197566053588742e-08,0.0085041401903732348,0.0085041401903719806,1.0917328441425228e-08,5.4399041658569889e-09 +1000000,cable:2:core,cable:1:core,0.00017862953015392797,0.00022533894904642611,0.00017862953015392781,0.00022533894904642752,0.00018016270749890033,0.00022577200740065857,1.5931642442969515e-06,1.5931642442971781e-06,0.0055156317569885283,0.0055156317569893133,1.5331773449723531e-06,4.3305835423245766e-07 +1000000,cable:2:core,cable:2:core,0.094696109113519125,4.9430140519244823,0.094696109113519056,4.9430140519244823,0.093103635677330446,4.9154040794845795,0.027655859230498691,0.027655859230498694,0.0056253563463567035,0.0056253563463567044,0.0015924734361886794,0.027609972439902819 +1000000,cable:2:core,cable:3:core,0.00017862953015392797,0.00022533894904642614,0.00017862953015392781,0.00022533894904642758,0.00018018081362777873,0.0002258305203441686,1.6273053668582831e-06,1.627305366858562e-06,0.0056327182711444768,0.0056327182711454421,1.551283473850756e-06,4.9157129774246339e-07 +1000000,cable:3:core,cable:1:core,2.3887617723989342e-07,1.4069502440255183e-06,2.3887617723989178e-07,1.4069502440255175e-06,2.4979350568131807e-07,1.4123901481913734e-06,1.2197566053589217e-08,1.2197566053587371e-08,0.0085041401903723241,0.008504140190371037,1.0917328441424646e-08,5.4399041658550831e-09 +1000000,cable:3:core,cable:2:core,0.00017862953015392795,0.00022533894904642606,0.00017862953015392795,0.00022533894904642598,0.00018018081362777857,0.00022583052034416836,1.6273053668584082e-06,1.6273053668583837e-06,0.0056327182711449165,0.0056327182711448315,1.5512834738506205e-06,4.9157129774230076e-07 +1000000,cable:3:core,cable:3:core,0.75407446569154857,1.5740479213690892,0.75407446569154857,1.5740479213690892,0.75374171173894589,1.568351256130611,0.0057063753848006615,0.0057063753848006615,0.0032793894824094515,0.0032793894824094515,0.00033275395260268237,0.0056966652384782268 +10000000,cable:1:core,cable:1:core,4.5479347057971937,6.6714050104071516,4.5479347057971937,6.6714050104071516,4.5500659455579013,6.6606636781499313,0.010950726075362412,0.010950726075362412,0.0013575652703185375,0.0013575652703185375,0.002131239760707615,0.010741332257220293 +10000000,cable:1:core,cable:2:core,1.5377810804168995e-09,4.4664472009509941e-10,1.5377810804168995e-09,4.4664472009509879e-10,5.6028567701004943e-09,-2.7981230302132413e-10,4.1294769849572631e-09,4.1294769849572631e-09,0.7361132176669769,0.7361132176669769,4.065075689683595e-09,7.2645702311642359e-10 +10000000,cable:1:core,cable:3:core,2.1364256347380883e-17,7.0038536780327319e-18,2.1364256347378569e-17,7.0038536780304023e-18,2.0913101393029784e-13,6.2993909743126889e-13,6.6373284392578382e-13,6.6373284392578382e-13,0.99997984432131481,0.99997984432131481,2.0910964967395046e-13,6.2993209357759082e-13 +10000000,cable:2:core,cable:1:core,1.5377810804169008e-09,4.4664472009509931e-10,1.5377810804169031e-09,4.4664472009510029e-10,5.6028567701004935e-09,-2.7981230302132444e-10,4.129476984957259e-09,4.1294769849572614e-09,0.73611321766697624,0.73611321766697668,4.0650756896835925e-09,7.264570231164238e-10 +10000000,cable:2:core,cable:2:core,1.2514510556301524,49.784417123982301,1.2514510556301526,49.784417123982301,0.52922017504967389,51.33126534579285,1.7071487533845759,1.7071487533845759,0.033255716944887444,0.033255716944887444,0.72223088058047846,1.5468482218105493 +10000000,cable:2:core,cable:3:core,1.5377810804169008e-09,4.4664472009509931e-10,1.5377810804169031e-09,4.4664472009510029e-10,5.5480436147798052e-09,-8.7996864032549524e-11,4.0457443342348382e-09,4.0457443342348398e-09,0.72912828774712768,0.72912828774712801,4.0102625343629042e-09,5.3464158412764887e-10 +10000000,cable:3:core,cable:1:core,2.1364256347380883e-17,7.0038536780327304e-18,2.1364256347378569e-17,7.0038536780304008e-18,2.0913101393029771e-13,6.2993909743126828e-13,6.6373284392578322e-13,6.6373284392578322e-13,0.99997984432131481,0.99997984432131481,2.0910964967395034e-13,6.2993209357759022e-13 +10000000,cable:3:core,cable:2:core,1.5377810804168995e-09,4.4664472009509931e-10,1.5377810804168995e-09,4.4664472009509874e-10,5.5480436147797969e-09,-8.7996864032551824e-11,4.0457443342348332e-09,4.0457443342348332e-09,0.7291282877471279,0.7291282877471279,4.0102625343628976e-09,5.3464158412765114e-10 +10000000,cable:3:core,cable:3:core,4.5479347057971937,6.6714050104071516,4.5479347057971937,6.6714050104071516,4.5501219747159745,6.6605257629547943,0.011096944194357663,0.011096944194357663,0.0013757060123651213,0.0013757060123651213,0.0021872689187807381,0.010879247452357355 diff --git a/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/air_3-Y.csv b/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/air_3-Y.csv new file mode 100644 index 000000000..54a1f3f59 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/air_3-Y.csv @@ -0,0 +1,82 @@ +frequency_hz,receiver,source,new_real,new_imaginary,old_real,old_imaginary,fem_real,fem_imaginary,before_absolute_error,after_absolute_error,before_relative_error,after_relative_error,real_absolute_error,imaginary_absolute_error +0.10000000000000001,cable:1:core,cable:1:core,10.815794395856093,0.14817480024522853,10.815794395856155,0.14817480024608823,10.885638947845042,0.14938178884839903,0.069854980237735106,0.069854980237812128,0.0064165644380051552,0.0064165644380122294,0.069844551988948567,0.0012069886031705057 +0.10000000000000001,cable:1:core,cable:2:core,-8.3998980305213653,0.14802772097541228,-8.3998980305212747,0.14802772097705633,-8.4592585099256858,0.14923920161998083,0.05937284059626674,0.05937284059620971,0.007017589420792383,0.0070175894207856427,0.059360479404320543,0.0012114806445685522 +0.10000000000000001,cable:1:core,cable:3:core,-1.1280530780399818e-13,-2.9535313553156216e-12,-1.1280530750282914e-13,-2.953531354972855e-12,-1.1377995097602876e-13,-2.9734102109619595e-12,1.9902734619529257e-14,1.9902734262425548e-14,0.0066886765298462538,0.0066886764098350471,9.7464317203058143e-16,1.9878855646337946e-14 +0.10000000000000001,cable:2:core,cable:1:core,-8.3998980305213387,0.14802772097586703,-8.3998980305212676,0.14802772097705355,-8.4592585099256858,0.14923920161997939,0.059372840596273867,0.059372840596227043,0.0070175894207932252,0.0070175894207876914,0.059360479404347188,0.0012114806441123616 +0.10000000000000001,cable:2:core,cable:2:core,10.815794395856084,0.14817480024469887,10.815794395856189,0.14817480024712157,10.885702751320308,0.14938997890362823,0.069918916058994848,0.069918916059141745,0.0064223995925957239,0.0064223995926092174,0.069908355464223249,0.0012151786589293667 +0.10000000000000001,cable:2:core,cable:3:core,-1.1296417471165001e-13,-4.2580756761562053e-12,-1.1296417495695151e-13,-4.2580756764147922e-12,-1.1394188949089691e-13,-4.2859295698325242e-12,2.7871047777869144e-14,2.7871048044901984e-14,0.006500621011536257,0.0065006210738187842,9.7771477924689401e-16,2.785389367631888e-14 +0.10000000000000001,cable:3:core,cable:1:core,-5.1136148413973961e-14,-3.5844900350302824e-13,-1.358174635016056e-13,-2.933449794573915e-12,-1.1377995097603096e-13,-2.9734102109619599e-12,4.5634272605365255e-14,2.6157114448108585e-12,0.015336228612152988,0.8790574805026683,6.2643802562056991e-14,2.6149612074589318e-12 +0.10000000000000001,cable:3:core,cable:2:core,-5.1155498185351796e-14,8.7999087097332483e-13,-1.3601058441964668e-13,-4.2379939867923176e-12,-1.1394188949089317e-13,-4.2859295698325218e-12,5.277165353921273e-14,5.1663019783660389e-12,0.012308418490205237,1.2049841635008409,6.2786391305541381e-14,5.1659204408058468e-12 +0.10000000000000001,cable:3:core,cable:3:core,-1.0666964456901868e-24,9.0759067020569623e-12,3.3278820986485504e-24,9.0759067020568993e-12,3.3659780042985981e-24,9.1325537907485334e-12,5.664708869163415e-14,5.6647088691571142e-14,0.006202765402709024,0.006202765402702125,4.4326744499887848e-24,5.6647088691571142e-14 +1,cable:1:core,cable:1:core,11.070033787834065,0.21907795136545627,11.070033787834907,0.2190779513727911,11.140563161800511,0.22071888391898442,0.07054846030638201,0.070548460307394381,0.0063313332928883869,0.0063313332929792422,0.070529373966445874,0.0016409325535281516 +1,cable:1:core,cable:2:core,-8.145822527090532,0.21787423633236641,-8.1458225270892957,0.21787423634749992,-8.2046628545477134,0.21952020259074589,0.058863344623693713,0.058863344622881023,0.0071718104745286974,0.0071718104744296811,0.058840327457181374,0.0016459662583794832 +1,cable:1:core,cable:3:core,-1.6216279544894657e-12,-2.7605627275107872e-11,-1.621627954619451e-12,-2.7605627275097529e-11,-1.6346877661635265e-12,-2.7802844226268414e-11,1.9764889199463251e-13,1.9764889199290103e-13,0.0070966892428370671,0.0070966892427748972,1.3059811674060804e-14,1.9721695116054177e-13 +1,cable:2:core,cable:1:core,-8.145822527090143,0.21787423633650868,-8.145822527089182,0.21787423634739994,-8.2046628545477152,0.21952020259074595,0.058863344623811924,0.058863344623155837,0.0071718104745430982,0.0071718104744631622,0.058840327457572172,0.0016459662542372688 +1,cable:2:core,cable:2:core,11.070033787834019,0.21907795136028499,11.070033787835508,0.21907795138250014,11.140629632151182,0.22072472762327983,0.0706150487263877,0.070615048728393956,0.0063372713720829697,0.0063372713722630184,0.070595844317162815,0.0016467762629948368 +1,cable:2:core,cable:3:core,-1.6339304032893034e-12,-4.0648885431794825e-11,-1.6339304034811439e-12,-4.0648885431782831e-11,-1.6470237031376464e-12,-4.0926987325631668e-11,2.7840994568837659e-13,2.7840994568541779e-13,0.0067970988164539527,0.0067970988163817171,1.3093299848343002e-14,2.7810189383684294e-13 +1,cable:3:core,cable:1:core,-1.2021168565223691e-12,-2.6131097494743181e-12,-2.4448833834075816e-12,-2.6909309921922212e-11,-1.6346877661635198e-12,-2.7802844226268355e-11,1.2061593971133471e-12,2.5193448366653858e-11,0.043307798653754177,0.90458424638403356,4.3257090964115068e-13,2.5189734476794037e-11 +1,cable:3:core,cable:2:core,-1.2038703207920688e-12,9.7714076065056096e-12,-2.4605830567011745e-12,-3.9952527978242624e-11,-1.6470237031376464e-12,-4.0926987325631655e-11,1.2694289430624162e-12,5.070033169137304e-11,0.030991830931642522,1.237797606982695,4.4315338234557762e-13,5.0698394932137263e-11 +1,cable:3:core,cable:3:core,-1.1415236378499647e-22,9.0759067020560391e-11,3.1423916594339771e-22,9.0759067020550995e-11,3.2183905237197333e-22,9.1334425139340136e-11,5.7535811878914124e-13,5.7535811877974499e-13,0.0062994661422719062,0.0062994661421690292,4.3599141615696978e-22,5.7535811877974499e-13 +10,cable:1:core,cable:1:core,11.455439466681346,0.35723512530509688,11.455439466692644,0.35723512536492974,11.526888706357312,0.35962578479877871,0.071489223671738211,0.071489223685030384,0.0061989369533425138,0.0061989369544950979,0.071449239675965615,0.0023906594936818348 +10,cable:1:core,cable:2:core,-7.7620499918553731,0.3478670390993045,-7.7620499918370207,0.34786703922841428,-7.8201731256180755,0.35015628638615159,0.05816819864049088,0.058168198627233866,0.0074307786725537842,0.0074307786708602483,0.058123133762702395,0.0022892472868470959 +10,cable:1:core,cable:3:core,-2.4359412290390375e-11,-2.4772235737226435e-10,-2.4359412301593299e-11,-2.4772235737075831e-10,-2.4538192632622566e-11,-2.496571722746665e-10,1.9430571579725093e-12,1.9430571575036392e-12,0.007745578471616445,0.007745578469747395,1.7878034223219079e-13,1.9348149024021558e-12 +10,cable:2:core,cable:1:core,-7.7620499918470722,0.34786703913335953,-7.7620499918328045,0.34786703922484874,-7.8201731256180809,0.35015628638615004,0.058168198644849387,0.058168198634193355,0.0074307786731105637,0.0074307786717492932,0.058123133771008639,0.002289247252790505 +10,cable:2:core,cable:2:core,11.455439466682984,0.35723512525375883,11.455439466704808,0.35723512545105984,11.525783616451514,0.35941348767162129,0.070377870567039619,0.070377870594960423,0.0061031579193572679,0.0061031579217785559,0.070344149768530428,0.0021783624178624561 +10,cable:2:core,cable:3:core,-2.5235453499545022e-11,-3.7794241747563747e-10,-2.5235453516660523e-11,-3.7794241747387335e-10,-2.5408946664693694e-11,-3.8061036108960929e-10,2.6735786895396618e-12,2.6735786888899171e-12,0.0070088498652705576,0.0070088498635672361,1.7349316514867201e-13,2.6679436139718214e-12 +10,cable:3:core,cable:1:core,-3.4971191232074981e-11,-1.938923082713509e-12,-5.4539271312740393e-11,-2.2299128059268601e-10,-2.4538192632622259e-11,-2.4965717227466635e-10,4.0138939960663476e-11,2.4793785197606e-10,0.16000523091005178,0.98835079590168551,1.0432998599452722e-11,2.4771824919195286e-10 +10,cable:3:core,cable:2:core,-3.5126392564573668e-11,1.2192069752793325e-10,-5.574623180572549e-11,-3.5319928202439063e-10,-2.5408946664693927e-11,-3.8061036108961007e-10,4.0886649719046023e-11,5.0262500298939145e-10,0.10718532074477341,1.3176433513129042,9.717445899879741e-12,5.0253105861754327e-10 +10,cable:3:core,cable:3:core,-1.330344677667895e-20,9.0759067020265848e-10,2.7893988431890267e-20,9.0759067020117803e-10,3.0071294664954368e-20,9.133123087759835e-10,5.7216385748054646e-12,5.7216385733250181e-12,0.0062647119937248794,0.0062647119921039156,4.3374741441633315e-20,5.7216385733250181e-12 +100,cable:1:core,cable:1:core,12.104669782274954,0.68261648187573531,12.104669782424534,0.68261648227961136,12.176168176226815,0.68625143513301334,0.071590734027195041,0.071590734197088446,0.0058702623123268443,0.0058702623262576808,0.071498393951861416,0.0036349532572780285 +100,cable:1:core,cable:2:core,-7.1288262826252131,0.61549758295382839,-7.1288262823565649,0.61549758392216802,-7.1858673359305918,0.61911060139397978,0.057155364473394997,0.057155364266496679,0.0079245001433820902,0.007924500114695969,0.057041053305378675,0.0036130184401513832 +100,cable:1:core,cable:3:core,-3.7138847615143461e-10,-2.0430445510009438e-09,-3.7138847700779774e-10,-2.0430445507832211e-09,-3.7372342864484257e-10,-2.0606818938052181e-09,1.7791230087403235e-11,1.7791229983954418e-11,0.00849508537841368,0.0084950853290181906,2.3349524934079579e-12,1.7637342804274308e-11 +100,cable:2:core,cable:1:core,-7.1288262824171342,0.61549758312471459,-7.1288262822103974,0.61549758378874064,-7.185867335930598,0.61911060139397978,0.057155364627710863,0.057155364463363216,0.0079245001647777324,0.0079245001419911941,0.05704105351346378,0.0036130182692651891 +100,cable:2:core,cable:2:core,12.10466978238181,0.6826164813115343,12.104669782732644,0.6826164828956689,12.176517262145065,0.68635220210041559,0.071944533467769134,0.071944533900386076,0.0058991016551025628,0.0058991016905750471,0.071847479763254896,0.0037357207888812916 +100,cable:2:core,cable:3:core,-4.2495166240793845e-10,-3.3258245297456921e-09,-4.2495166380733159e-10,-3.3258245294674373e-09,-4.2756682337893478e-10,-3.3522814203348597e-09,2.6585825809172263e-11,2.6585825669920562e-11,0.0078669359869180146,0.0078669359457124409,2.6151609709963381e-12,2.6456890589167592e-11 +100,cable:3:core,cable:1:core,-1.1636921547696429e-09,7.2216173971858748e-10,-1.4910656784955182e-09,-1.1316480131395586e-09,-3.7372342864484076e-10,-2.0606818938052144e-09,1.4531199725852168e-09,2.8927961001251365e-09,0.69384624736711165,1.381273230247569,7.8996872612480218e-10,2.7828436335238019e-09 +100,cable:3:core,cable:2:core,-1.1765965611356642e-09,1.9628848004581849e-09,-1.575033846674186e-09,-2.4110868103799636e-09,-4.275668233789368e-10,-3.3522814203348651e-09,1.4840915953397954e-09,5.3676845569298669e-09,0.43915331662306895,1.588336247684544,7.4902973775672746e-10,5.31516622079305e-09 +100,cable:3:core,cable:3:core,-1.9401763256901179e-18,9.0759067008644299e-09,1.9127172212576587e-18,9.0759067005974408e-09,2.6851848844473589e-18,9.1328039897551847e-09,5.6897289157743825e-11,5.689728889075492e-11,0.0062299912733886475,0.0062299912441545913,4.6253612101374773e-18,5.6897288890754732e-11 +1000,cable:1:core,cable:1:core,13.422507234576008,1.6280303345300919,13.422507236118639,1.6280303360967181,13.494876521647832,1.6357172513243972,0.072776384588157661,0.072776386287632305,0.0053537052833279248,0.0053537054083476884,0.072369287071824218,0.0076869167943052208 +1000,cable:1:core,cable:2:core,-5.9564805841176112,1.2141336216946836,-5.9564805806274439,1.2141336265943119,-6.0093110821882147,1.2209775222342441,0.053271951369298395,0.053271948537513103,0.0086873963200910492,0.0086873958582937907,0.052830498070603582,0.0068439005395604102 +1000,cable:1:core,cable:3:core,-5.3125767624237946e-09,-1.3812807031995072e-08,-5.3125768097971711e-09,-1.3812807003898819e-08,-5.3437799462519742e-09,-1.3933820693746159e-08,1.2497179223762959e-10,1.2497177685950375e-10,0.0083742270091450837,0.0083742259786732127,3.1203183828179629e-11,1.2101366175108671e-10 +1000,cable:2:core,cable:1:core,-5.9564805795315312,1.2141336183221656,-5.9564805769900762,1.2141336212955836,-6.0093110821882192,1.2209775222342427,0.053271955657261144,0.053271953518864511,0.0086873970193564326,0.0086873966706344768,0.052830502656687983,0.0068439039120771206 +1000,cable:2:core,cable:2:core,13.422507237708379,1.628030325555774,13.422507243289036,1.6280303346877092,13.494530277879935,1.6357156672970763,0.072431911813774466,0.072431918331866935,0.0053284993777964673,0.0053284998573040606,0.072023040171556474,0.0076853417413023095 +1000,cable:2:core,cable:3:core,-7.6990451786107599e-09,-2.5177999875353145e-08,-7.6990452661998479e-09,-2.5177999831420848e-08,-7.7437038728303673e-09,-2.5406498794328743e-08,2.3282217934759386e-10,2.3282215303191718e-10,0.0087657615371490982,0.0087657605463631397,4.4658694219607418e-11,2.2849891897559837e-10 +1000,cable:3:core,cable:1:core,-3.8993651370144031e-08,3.4644002403347795e-08,-4.4374379330248328e-08,2.2280527656749974e-08,-5.3437799462519742e-09,-1.3933820693746142e-08,5.3243466404105376e-08,5.9094151518547249e-08,3.5677881099276916,3.9598363028046895,3.3649871423892055e-08,4.8577823097093937e-08 +1000,cable:3:core,cable:2:core,-3.9862609765354866e-08,4.7392554307083943e-08,-4.9053839044978226e-08,1.1664030389121588e-08,-7.7437038728303574e-09,-2.5406498794328753e-08,5.5504516950264258e-08,7.9569631444384297e-08,2.0897466091247408,2.9957988401053144,3.211890589252451e-08,7.2799053101412696e-08 +1000,cable:3:core,cable:3:core,-4.4750964200884614e-16,9.0759066492957161e-08,-1.1449192093489889e-16,9.075906643642654e-08,2.1469037444216061e-16,9.1289179425853152e-08,5.3011298942671402e-10,5.301129328964039e-10,0.0058069641195239588,0.0058069635002795912,6.6220001645100678e-16,5.3011293289599031e-10 +10000,cable:1:core,cable:1:core,17.086534011462401,4.2459949224509836,17.086534015481533,4.2459949196386608,17.16879046787793,4.2706812130935701,0.085880946928772295,0.08588094996988567,0.0048542309469669608,0.0048542311188591817,0.082256456415528589,0.024686290642586428 +10000,cable:1:core,cable:2:core,-3.3202280044350214,2.2955286501758181,-3.3202279874715721,2.2955286461190294,-3.3571627880337496,2.3128739690953988,0.040804898256488663,0.040804881177462601,0.010009162278858546,0.010009158089490339,0.036934783598728149,0.017345318919580688 +10000,cable:1:core,cable:3:core,-5.8587418784551338e-08,-4.3856895157340185e-08,-5.858742019788468e-08,-4.3856893278769888e-08,-5.9029983864961397e-08,-4.4323999810668119e-08,6.434680346049712e-10,6.4346764297746234e-10,0.0087169059183770485,0.0087169006130938548,4.425650804100583e-10,4.6710465332793414e-10 +10000,cable:2:core,cable:1:core,-3.3202280396447068,2.2955285385873432,-3.320228031402729,2.2955285336624991,-3.3571627880337513,2.312873969095401,0.04080490629507455,0.04080489674134577,0.010009164250668641,0.010009161907204358,0.036934748389044536,0.017345430508057813 +10000,cable:2:core,cable:2:core,17.086533949182265,4.245994782397787,17.086533982869692,4.2459947768646549,17.168943134567673,4.2708718018210501,0.086082138996785421,0.086082169647796622,0.0048655494879547793,0.0048655512204165141,0.082409185385408534,0.024877019423263036 +10000,cable:2:core,cable:3:core,-1.0820603063721758e-07,-9.3862017659670496e-08,-1.0820603152553118e-07,-9.3862013176872435e-08,-1.0906970448598638e-07,-9.5008183716100581e-08,1.4351438560701812e-09,1.4351408104989582e-09,0.0099216978479292912,0.0099216767927317075,8.6367384876880097e-10,1.1461660564300851e-09 +10000,cable:3:core,cable:1:core,-6.8597772458705922e-07,1.5110795613731931e-06,-7.4376780209961771e-07,1.4920783526578319e-06,-5.9029983864961377e-08,-4.4323999810668119e-08,1.6820814690113849e-06,1.6770043851284292e-06,22.786751048821138,22.717973020749234,6.2694774072209786e-07,1.5554035611838613e-06 +10000,cable:3:core,cable:2:core,-7.0566345675390556e-07,1.6746422052936641e-06,-8.720081626905464e-07,1.5363787545915007e-06,-1.0906970448598638e-07,-9.5008183716100581e-08,1.8009715248965488e-06,1.8675081270418952e-06,12.450804305902423,12.910797260282411,5.965937522679192e-07,1.7696503890097647e-06 +10000,cable:3:core,cable:3:core,-1.6692047509887364e-13,9.0759045224291654e-07,-1.4613839178070811e-13,9.075904422043152e-07,1.1771746799955883e-14,9.1287583973309992e-07,5.285397531143633e-09,5.2853874932040563e-09,0.005789831761446265,0.0057898207654935412,1.7869222189882952e-13,5.285387490183378e-09 +100000,cable:1:core,cable:1:core,25.114053090920187,8.0617971940330779,25.114053091114524,8.0617971943186379,25.211287126640112,8.120101984535804,0.11337506880641381,0.11337506911993676,0.0042804534634525281,0.0042804534752895257,0.09723403571992506,0.058304790502726078 +100000,cable:1:core,cable:2:core,0.17168861145691555,1.0502292230423047,0.17168860920972809,1.0502292248219696,0.17180324107286526,1.0598490202933522,0.0096204784379613478,0.0096204801907239946,0.0089602552946762264,0.0089602569271522922,0.00011462961594971288,0.0096197972510474461 +100000,cable:1:core,cable:3:core,-5.2567619721435549e-08,7.4524194220000796e-08,-5.2567611057230254e-08,7.452419524450226e-08,-5.2703783492650436e-08,7.536595966692837e-08,8.5270761403345421e-10,8.5270724180848757e-10,0.0092720032399396871,0.0092719991925135359,1.361637712148869e-10,8.4176544692757397e-10 +100000,cable:2:core,cable:1:core,0.17168867697767981,1.0502294238147607,0.17168867735386809,1.0502294245133177,0.17180324107286571,1.0598490202933519,0.0096202779490492437,0.00962027865203657,0.0089600685646868822,0.008960069219430402,0.00011456409518589838,0.0096195964785912302 +100000,cable:2:core,cable:2:core,25.114053168427063,8.0617974533493086,25.114053162870402,8.061797456881747,25.213095358910977,8.1211168291641247,0.11544758345030423,0.11544758049829912,0.0043583664354513068,0.0043583663240074847,0.099042190483913828,0.059319375814816055 +100000,cable:2:core,cable:3:core,-1.3779481951670331e-07,1.5778396825373155e-07,-1.377947989655141e-07,1.5778396671832445e-07,-1.3987609291158349e-07,1.59753201089502e-07,2.8652519083969159e-09,2.8652359249654539e-09,0.013493978028802577,0.013493902754418379,2.0812733948801838e-09,1.9692328357704569e-09 +100000,cable:3:core,cable:1:core,1.5638455966719109e-05,7.7776922470880267e-06,1.5670639845467555e-05,7.7897672759496725e-06,-5.270378349265043e-08,7.536595966692837e-08,1.7513866578886239e-05,1.7479654588823077e-05,190.43881512348455,190.06680755900422,1.569115975021176e-05,7.7023262874210983e-06 +100000,cable:3:core,cable:2:core,1.7038431448127932e-05,8.9143470293325838e-06,1.6957770865381582e-05,9.1879747547490139e-06,-1.3987609291158347e-07,1.59753201089502e-07,1.9334898911874188e-05,1.9280486588051839e-05,91.058206903672612,90.801950656184175,1.7178307541039514e-05,8.7545938282430816e-06 +100000,cable:3:core,cable:3:core,-5.5477726060310505e-11,9.0758164316572827e-06,-5.4794500699005802e-11,9.0758158501016034e-06,3.4078170713721261e-13,9.1297246440059132e-06,5.3908822099139899e-08,5.3908241246870991e-08,0.0059047588181680259,0.0059046951960664232,5.5818507767447715e-11,5.390821234863046e-08 +1000000,cable:1:core,cable:1:core,40.833774626502681,19.549173406400048,40.833774626502681,19.549173406400044,40.900039224648729,19.655874028520731,0.12560262627986341,0.12560262627986038,0.0027679164612677935,0.0027679164612677267,0.06626459814604857,0.10670062212068387 +1000000,cable:1:core,cable:2:core,-0.020403832499743355,-0.018991053490800946,-0.020403832499540885,-0.018991053491192875,-0.020563381851447241,-0.019102808491515869,0.00019479521501035552,0.00019479521506937203,0.0069403052986080618,0.0069403053007107453,0.00015954935170388665,0.00011175500071492275 +1000000,cable:1:core,cable:3:core,-1.550392640425466e-09,-5.8061933848089211e-09,-1.5503936374884625e-09,-5.8061917080445266e-09,-1.5672016590061488e-09,-5.8192553407374583e-09,2.1287744983319436e-11,2.1287503337047024e-11,0.0035323002604624766,0.0035322601638157129,1.6809018580682813e-11,1.3061955928537201e-11 +1000000,cable:2:core,cable:1:core,-0.020403832081271396,-0.018991054091930554,-0.020403832081266379,-0.018991054091929891,-0.020563381851447248,-0.019102808491515869,0.00019479521295861048,0.00019479521295412121,0.0069403052255069993,0.0069403052253470517,0.00015954977017585206,0.00011175439958531486 +1000000,cable:2:core,cable:2:core,40.833774627207667,19.549173405646197,40.833774627208193,19.549173405645224,40.89915157803005,19.655356047113056,0.12469522463931708,0.12469522463876383,0.002747982005379142,0.0027479820053669499,0.065376950822383151,0.10618264146685874 +1000000,cable:2:core,cable:3:core,-3.6993309151330288e-09,-1.4559820469910908e-08,-3.6993334581473173e-09,-1.4559816308977549e-08,-3.7825918612963801e-09,-1.4735986432314035e-08,1.9485346815318598e-10,1.9485079282732304e-10,0.012807745855603602,0.012807570006058514,8.3260946163351313e-11,1.7616596240312679e-10 +1000000,cable:3:core,cable:1:core,-6.4349987748322059e-06,5.1068012806931181e-06,-6.4350271678452756e-06,5.1067899044121304e-06,-1.5672016590061527e-09,-5.8192553407374633e-09,8.2175531368473888e-06,8.2175379860501342e-06,1363.5481404157649,1363.5456264263719,6.4334315731731998e-06,5.1126205360338558e-06 +1000000,cable:3:core,cable:2:core,-7.7938841060709354e-06,5.4353142732557005e-06,-7.798364509434425e-06,5.4402402295420326e-06,-3.7825918612963826e-09,-1.4735986432314038e-08,9.5137938166960399e-06,9.5072987454279935e-06,625.3430050886343,624.91608314106202,7.7901015142096393e-06,5.4500502596880142e-06 +1000000,cable:3:core,cable:3:core,-2.4740421441749414e-08,9.0711568120478741e-05,-2.4719345718823336e-08,9.0711547555953113e-05,1.0614892269862695e-11,9.1254421478153164e-05,5.4343690190922188e-07,5.4341731821685927e-07,0.0059551843418274271,0.0059549697364193232,2.4751036334019276e-08,5.4285335767442332e-07 +10000000,cable:1:core,cable:1:core,80.846531351828517,55.108195401013525,80.846531351828517,55.108195401013525,80.8235232692426,55.21592941541855,0.11016346819205822,0.11016346819205822,0.0011254513410626985,0.0011254513410626985,0.023008082585917577,0.10773401440502539 +10000000,cable:1:core,cable:2:core,-9.2664181398836154e-08,-1.3479772233804371e-07,-9.2664181398836154e-08,-1.3479772233804371e-07,-3.735569252190193e-07,-2.4421266995171498e-07,3.0145043422117251e-07,3.0145043422117251e-07,0.67544194975282035,0.67544194975282035,2.8089274382018313e-07,1.0941494761367126e-07 +10000000,cable:1:core,cable:3:core,-5.2158582298414681e-14,-5.4725149783209922e-14,-5.2158582925956706e-14,-5.4725070160129558e-14,-1.8847158663702465e-12,-9.6197774170620373e-13,2.0448407290378886e-12,2.0448406942732041e-12,0.96636016368605737,0.96636014725680408,1.832557284071832e-12,9.0725259192299378e-13 +10000000,cable:2:core,cable:1:core,-9.2664181398870313e-08,-1.3479772233806722e-07,-9.2664181398870313e-08,-1.3479772233806722e-07,-3.7355692521901909e-07,-2.4421266995171524e-07,3.0145043422113206e-07,3.0145043422113206e-07,0.67544194975272975,0.67544194975272975,2.8089274382014878e-07,1.0941494761364802e-07 +10000000,cable:2:core,cable:2:core,80.846531351828517,55.108195401013525,80.846531351828517,55.108195401013525,80.821803742000398,55.219645496033451,0.1141603187090951,0.1141603187090951,0.0011662758803171742,0.0011662758803171742,0.024727609828119057,0.11145009501992575 +10000000,cable:2:core,cable:3:core,-1.1003843823177019e-13,-1.3743099164070031e-13,-1.1003845565783392e-13,-1.3743080703062872e-13,-2.0949856543762342e-12,-1.0129284741972224e-12,2.1694495953845462e-12,2.1694495368278252e-12,0.93228946241669342,0.93228943725279279,1.984947216144464e-12,8.754974825565221e-13 +10000000,cable:3:core,cable:1:core,3.6775716049205592e-11,4.3675577430240467e-10,3.6775425414283791e-11,4.3675607950356147e-10,-1.8847158663702142e-12,-9.6197774170617021e-13,4.3942201146780478e-10,4.3942173302016889e-10,207.66405955200685,207.66392796195234,3.8660431915575804e-11,4.3771775204411086e-10 +10000000,cable:3:core,cable:2:core,-9.3439526677307196e-11,5.4261359972629985e-10,-9.3440467051073188e-11,5.4261406900812513e-10,-2.0949856543762516e-12,-1.0129284741972214e-12,5.5124795633475065e-10,5.5124733771512746e-10,236.89080491336637,236.89053907056589,9.1344541022930942e-11,5.4362652820049708e-10 +10000000,cable:3:core,cable:3:core,-1.6761991871629861e-05,0.0008877789650905823,-1.6761328755330462e-05,0.00088777830561370815,1.6610165398915739e-08,-8.8252205620793824e-05,0.00097617470674829054,0.00097617537752523571,11.061193117969086,11.061200718650889,1.6778602037028778e-05,0.00097603117071137617 diff --git a/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/air_3-Z.csv b/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/air_3-Z.csv new file mode 100644 index 000000000..c857a1463 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/air_3-Z.csv @@ -0,0 +1,82 @@ +frequency_hz,receiver,source,new_real,new_imaginary,old_real,old_imaginary,fem_real,fem_imaginary,before_absolute_error,after_absolute_error,before_relative_error,after_relative_error,real_absolute_error,imaginary_absolute_error +0.10000000000000001,cable:1:core,cable:1:core,3.1374615510534723e-06,1.2435841216202133e-06,3.1374615510534732e-06,1.2435841216202129e-06,3.1569384454592992e-06,1.2376736789074544e-06,2.0353936935062379e-08,2.0353936935063312e-08,0.0060025453549572361,0.0060025453549575119,1.9476894405826848e-08,5.9104427127589484e-09 +0.10000000000000001,cable:1:core,cable:2:core,9.9023675189330895e-08,8.1529149017255945e-07,9.9023675189331398e-08,8.1529149017255914e-07,9.8889592958001499e-08,8.1220201356743481e-07,3.0923847979138876e-09,3.0923847979141829e-09,0.0037794973675009044,0.0037794973675012652,1.3408223132939569e-10,3.0894766051246404e-09 +0.10000000000000001,cable:1:core,cable:3:core,9.8689933658488536e-08,6.8496952230340285e-07,9.8689933646132813e-08,6.8496952228978997e-07,9.8555942974177675e-08,6.8240259924173328e-07,2.5704177549214906e-09,2.5704177691599399e-09,0.0037280373348655758,0.0037280373555164874,1.3399068431086045e-10,2.5669230616695682e-09 +0.10000000000000001,cable:2:core,cable:1:core,9.9023675189330882e-08,8.1529149017255935e-07,9.9023675189331385e-08,8.1529149017255903e-07,9.8889592958001552e-08,8.1220201356743481e-07,3.0923847979137788e-09,3.0923847979140746e-09,0.0037794973675007713,0.003779497367501133,1.3408223132932952e-10,3.0894766051245346e-09 +0.10000000000000001,cable:2:core,cable:2:core,3.1374615510534723e-06,1.2435841216202133e-06,3.1374615510534732e-06,1.2435841216202129e-06,3.1569387144350372e-06,1.2377673118767072e-06,2.0327202684685417e-08,2.032720268468635e-08,0.0059946003389987892,0.0059946003389990642,1.9477163381564883e-08,5.8168097435061456e-09 +0.10000000000000001,cable:2:core,cable:3:core,9.8692094314379168e-08,7.1450042905111216e-07,9.8692094314379658e-08,7.1450042905111195e-07,9.8559152564943101e-08,7.1183814854081974e-07,2.6655976861156242e-09,2.6655976861158115e-09,0.0037092827583557471,0.0037092827583560077,1.3294174943606703e-10,2.6622805102924158e-09 +0.10000000000000001,cable:3:core,cable:1:core,9.8689933658488523e-08,6.8496952230340274e-07,9.8689933646133078e-08,6.8496952228978838e-07,9.8555942974177636e-08,6.8240259924173296e-07,2.5704177549202378e-09,2.5704177691601529e-09,0.0037280373348637604,0.0037280373555167983,1.3399068431088692e-10,2.5669230616697799e-09 +0.10000000000000001,cable:3:core,cable:2:core,9.8692094314379168e-08,7.1450042905111216e-07,9.8692094314379645e-08,7.1450042905111184e-07,9.8559152564942969e-08,7.1183814854081953e-07,2.6655976861157359e-09,2.6655976861160295e-09,0.0037092827583559036,0.0037092827583563122,1.3294174943619938e-10,2.6622805102926276e-09 +0.10000000000000001,cable:3:core,cable:3:core,3.1368030432638579e-06,1.2442491566826318e-06,3.1368030432638579e-06,1.2442491566826316e-06,3.1562799575900971e-06,1.2381889884782712e-06,2.0397936913737283e-08,2.0397936913737346e-08,0.0060162751800927651,0.0060162751800927833,1.9476914326239281e-08,6.0601682043606381e-09 +1,cable:1:core,cable:1:core,4.0462619649157106e-06,1.098137058628601e-05,4.0462619649157199e-06,1.0981370586286009e-05,4.06449427838295e-06,1.0930813715747969e-05,5.3743970945282693e-08,5.3743970945287451e-08,0.0046084600110046781,0.0046084600110050857,1.8232313467239359e-08,5.0556870538040859e-08 +1,cable:1:core,cable:2:core,9.9703495653241241e-07,6.6990056616567392e-06,9.9703495653241453e-07,6.6990056616567426e-06,9.9565697546928076e-07,6.6766752941241212e-06,2.2372843939747775e-08,2.2372843939744264e-08,0.003314247151264345,0.003314247151263825,1.3779810631316533e-09,2.2330367532618046e-08 +1,cable:1:core,cable:3:core,9.8646617111842156e-07,5.4030097683584809e-06,9.8646617099486423e-07,5.4030097682223509e-06,9.8503362833179562e-07,5.3861070599480792e-06,1.6963305264206145e-08,1.6963305410284266e-08,0.0030980717574050601,0.0030980717840838525,1.4325427866259356e-09,1.6902708410401732e-08 +1,cable:2:core,cable:1:core,9.9703495653241262e-07,6.6990056616567409e-06,9.9703495653241474e-07,6.6990056616567443e-06,9.9565697546928118e-07,6.6766752941241212e-06,2.2372843939749452e-08,2.2372843939745942e-08,0.0033142471512645935,0.0033142471512640735,1.3779810631314415e-09,2.233036753261974e-08 +1,cable:2:core,cable:2:core,4.0462619649157106e-06,1.0981370586286007e-05,4.0462619649157199e-06,1.0981370586286005e-05,4.0645127496387215e-06,1.0931687928980027e-05,5.2928797265652761e-08,5.2928797265657566e-08,0.0045382387913634045,0.0045382387913638156,1.8250784723010892e-08,4.9682657305979438e-08 +1,cable:2:core,cable:3:core,9.866394025367669e-07,5.698292545468414e-06,9.8663940253676923e-07,5.6982925454684148e-06,9.8530671994117011e-07,5.6804526678089855e-06,1.7889585735953143e-08,1.7889585735952127e-08,0.0031029902632174191,0.0031029902632172431,1.3326825955967894e-09,1.7839877659428474e-08 +1,cable:3:core,cable:1:core,9.8646617111842156e-07,5.4030097683584818e-06,9.8646617099486423e-07,5.4030097682223517e-06,9.8503362833179541e-07,5.3861070599480775e-06,1.6963305264208696e-08,1.6963305410286817e-08,0.0030980717574055267,0.0030980717840843192,1.4325427866261473e-09,1.6902708410404273e-08 +1,cable:3:core,cable:2:core,9.866394025367669e-07,5.698292545468414e-06,9.8663940253676923e-07,5.6982925454684148e-06,9.8530671994117053e-07,5.6804526678089847e-06,1.7889585735953957e-08,1.7889585735952937e-08,0.0031029902632175605,0.003102990263217384,1.3326825955963659e-09,1.7839877659429321e-08 +1,cable:3:core,cable:3:core,4.0258228222657537e-06,1.100234691604813e-05,4.025822822265763e-06,1.1002346916048127e-05,4.0439249252427902e-06,1.0949807691859883e-05,5.5570281720479191e-08,5.5570281720485432e-08,0.0047607104208511695,0.0047607104208517038,1.8102102977036502e-08,5.2539224188247047e-08 +10,cable:1:core,cable:1:core,1.4059660509356969e-05,9.4699377213437565e-05,1.405966050935706e-05,9.4699377213437497e-05,1.4077229758399098e-05,9.4284884384713351e-05,4.1486501850077148e-07,4.1486501850084305e-07,0.0043518833853649136,0.0043518833853656638,1.756924904212993e-08,4.1449282872421385e-07 +10,cable:1:core,cable:2:core,1.016473179872643e-05,5.2299532930466564e-05,1.0164731798726483e-05,5.2299532930466571e-05,1.015138241654919e-05,5.2159887190133634e-05,1.4028235383557433e-07,1.4028235383556261e-07,0.0026399361884357079,0.0026399361884354871,1.3349382177240088e-08,1.3964574033292949e-07 +10,cable:1:core,cable:3:core,9.8317851451266156e-06,3.9569605534518693e-05,9.8317851451266173e-06,3.9569605534518679e-05,9.8163402711206239e-06,3.9484136856124967e-05,8.685297415419253e-08,8.6852974154205566e-08,0.0021347094927609009,0.0021347094927612214,1.5444874005991736e-08,8.5468678393725854e-08 +10,cable:2:core,cable:1:core,1.0164731798726432e-05,5.2299532930466564e-05,1.0164731798726483e-05,5.2299532930466571e-05,1.0151382416549192e-05,5.2159887190133594e-05,1.4028235383561465e-07,1.4028235383560308e-07,0.0026399361884364685,0.0026399361884362508,1.3349382177240088e-08,1.3964574033297015e-07 +10,cable:2:core,cable:2:core,1.4059660509356969e-05,9.4699377213437578e-05,1.405966050935706e-05,9.4699377213437511e-05,1.4078088050992001e-05,9.43045827861135e-05,3.9522425803181795e-07,3.9522425803188989e-07,0.0041450015868589162,0.0041450015868596708,1.8427541635031996e-08,3.9479442732407843e-07 +10,cable:2:core,cable:3:core,9.8448260812245156e-06,4.2519810980107054e-05,9.8448260812245698e-06,4.2519810980107075e-05,9.8330297332457038e-06,4.2420226349152085e-05,1.0028086830537464e-07,1.0028086830534808e-07,0.0023029267712253013,0.0023029267712246916,1.1796347978811815e-08,9.9584630954968883e-08 +10,cable:3:core,cable:1:core,9.8317851451266156e-06,3.9569605534518686e-05,9.831785145126619e-06,3.9569605534518666e-05,9.8163402711206239e-06,3.9484136856124967e-05,8.6852974154179493e-08,8.6852974154198896e-08,0.0021347094927605809,0.0021347094927610575,1.5444874005991736e-08,8.5468678393719078e-08 +10,cable:3:core,cable:2:core,9.8448260812245156e-06,4.2519810980107034e-05,9.8448260812245715e-06,4.2519810980107061e-05,9.8330297332457004e-06,4.2420226349152079e-05,1.0028086830536851e-07,1.0028086830533502e-07,0.0023029267712251613,0.0023029267712243919,1.1796347978815203e-08,9.958463095495533e-08 +10,cable:3:core,cable:3:core,1.3443875148217057e-05,9.5357294237721687e-05,1.3443875148217152e-05,9.535729423772166e-05,1.345650685647301e-05,9.4936849368513674e-05,4.2063457786632498e-07,4.2063457786635489e-07,0.0043868295663449198,0.0043868295663452312,1.2631708255952105e-08,4.2044486920801293e-07 +100,cable:1:core,cable:1:core,0.00011750384543838827,0.00077809644648036498,0.00011750384543838806,0.00077809644648036465,0.00011825531094274957,0.00077388776386910112,4.2752438207191567e-06,4.2752438207194387e-06,0.0054609830498349833,0.0054609830498353433,7.5146550436130163e-07,4.2086826112638573e-06 +100,cable:1:core,cable:2:core,0.00010621565159805932,0.00037161327841812256,0.00010621565159805986,0.00037161327841812278,0.00010608020461151831,0.00037093196672528898,6.9464487974450093e-07,6.9464487974418255e-07,0.0018005195975041419,0.0018005195975033168,1.3544698654101306e-07,6.8131169283358736e-07 +100,cable:1:core,cable:3:core,9.6062121241851902e-05,0.00025171028261848866,9.6062121241851712e-05,0.00025171028261849007,9.5943191924629455e-05,0.00025153095123537972,2.1518347395472479e-07,2.1518347395365504e-07,0.00079932067807002316,0.00079932067806604945,1.1892931722244714e-07,1.7933138310894129e-07 +100,cable:2:core,cable:1:core,0.00010621565159805932,0.00037161327841812256,0.00010621565159805986,0.00037161327841812278,0.00010608020461151828,0.00037093196672528887,6.9464487974461252e-07,6.9464487974429415e-07,0.0018005195975044318,0.0018005195975036065,1.3544698654104016e-07,6.8131169283369578e-07 +100,cable:2:core,cable:2:core,0.00011750384543838808,0.00077809644648036455,0.00011750384543838787,0.00077809644648036422,0.0001182640892721979,0.00077393680436614522,4.2285450459029626e-06,4.2285450459032438e-06,0.0054009887719494087,0.0054009887719497678,7.6024383380981857e-07,4.1596421142193248e-06 +100,cable:2:core,cable:3:core,9.6939258720589035e-05,0.00028095471396052333,9.6939258720589563e-05,0.00028095471396052349,9.6830278881262328e-05,0.00028060784803067107,3.6358297357299128e-07,3.6358297357267767e-07,0.001224824917127208,0.0012248249171261515,1.0897983932670692e-07,3.4686592985226378e-07 +100,cable:3:core,cable:1:core,9.6062121241851888e-05,0.00025171028261848866,9.6062121241851698e-05,0.00025171028261849001,9.5943191924629482e-05,0.00025153095123537966,2.1518347395470232e-07,2.1518347395367775e-07,0.00079932067806993989,0.00079932067806613401,1.1892931722240648e-07,1.793313831089955e-07 +100,cable:3:core,cable:2:core,9.6939258720589008e-05,0.00028095471396052333,9.6939258720589536e-05,0.00028095471396052349,9.6830278881262396e-05,0.00028060784803067101,3.6358297357301452e-07,3.6358297357270096e-07,0.0012248249171272864,0.0012248249171262302,1.0897983932661205e-07,3.4686592985231799e-07 +100,cable:3:core,cable:3:core,0.00010029065542748212,0.00079835616300552852,0.00010029065542748186,0.00079835616300552798,0.00010105583228587755,0.00079404723441232818,4.3763410797056339e-06,4.3763410797061226e-06,0.0054673377844122635,0.0054673377844128741,7.6517685839542506e-07,4.3089285932003396e-06 +1000,cable:1:core,cable:1:core,0.0011875295144732644,0.0060821869409340423,0.0011875295144732621,0.0060821869409340397,0.001184365255805897,0.0060317603667377527,5.0525754997700245e-05,5.0525754997702996e-05,0.0082196615691729393,0.0082196615691733869,3.1642586673674179e-06,5.0426574196289591e-05 +1000,cable:1:core,cable:2:core,0.001102054672250501,0.0020961818335963364,0.0011020546722505062,0.0020961818335963386,0.0011008040157692749,0.0020952261252132928,1.5740140239114191e-06,1.5740140239059674e-06,0.00066503889558218519,0.0006650388955798818,1.2506564812260999e-06,9.557083830435964e-07 +1000,cable:1:core,cable:3:core,0.00083441087840421878,0.001133000518346655,0.00083441087840421617,0.0011330005183466704,0.00083336312012243408,0.001133947339960391,1.4121857474324659e-06,1.4121857474447188e-06,0.0010035129201567358,0.0010035129201654428,1.0477582817846991e-06,9.4682161373596356e-07 +1000,cable:2:core,cable:1:core,0.0011020546722505008,0.0020961818335963364,0.001102054672250506,0.0020961818335963386,0.0011008040157692751,0.0020952261252132924,1.5740140239113378e-06,1.5740140239058863e-06,0.00066503889558215093,0.00066503889557984765,1.2506564812256663e-06,9.5570838304403008e-07 +1000,cable:2:core,cable:2:core,0.0011875295144730821,0.0060821869409338524,0.0011875295144730795,0.0060821869409338489,0.0011843954705209859,0.0060321780833149713,5.0106966300493903e-05,5.0106966300497529e-05,0.0081509806890415742,0.008150980689042164,3.1340439520962087e-06,5.0008857618881117e-05 +1000,cable:2:core,cable:3:core,0.00088055424215431686,0.0014022944498764066,0.00088055424215432207,0.0014022944498764082,0.0008792393876089305,0.0014023395859519923,1.3156290285851886e-06,1.3156290285800395e-06,0.00079485594137271743,0.00079485594136960653,1.3148545453863653e-06,4.5136075585644822e-08 +1000,cable:3:core,cable:1:core,0.00083441087840421878,0.0011330005183466552,0.00083441087840421607,0.0011330005183466704,0.00083336312012243419,0.0011339473399603912,1.4121857474324504e-06,1.4121857474446384e-06,0.0010035129201567245,0.0010035129201653855,1.0477582817845906e-06,9.4682161373596356e-07 +1000,cable:3:core,cable:2:core,0.00088055424215431675,0.0014022944498764066,0.00088055424215432207,0.0014022944498764082,0.00087923938760893104,0.0014023395859519927,1.3156290285846615e-06,1.3156290285794042e-06,0.00079485594137239856,0.00079485594136922228,1.3148545453857147e-06,4.5136075586078503e-08 +1000,cable:3:core,cable:3:core,0.00078851876842865219,0.0066684718151727796,0.00078851876842865251,0.0066684718151727796,0.00078538139724812072,0.0066161432586409183,5.2422523085336487e-05,5.2422523085336473e-05,0.007868183652700653,0.0078681836527006496,3.1373711805314682e-06,5.2328556531861317e-05 +10000,cable:1:core,cable:1:core,0.01121739034987806,0.043047632316466668,0.01121739034987806,0.043047632316466668,0.011118858602888656,0.042731471972150319,0.00033115837371719322,0.00033115837371719322,0.0075000148609756052,0.0075000148609756052,9.8531746989403285e-05,0.0003161603443163491 +10000,cable:1:core,cable:2:core,0.0080340309126035597,0.0048666513437921688,0.0080340309126035597,0.0048666513437921688,0.0080285428981946051,0.0048733819025713922,8.6843954097837918e-06,8.6843954097837918e-06,0.00092467091254244671,0.00092467091254244671,5.4880144089546057e-06,6.7305587792233432e-06 +10000,cable:1:core,cable:3:core,0.0040559166332564877,0.0011450309278026654,0.0040559166332579241,0.0011450309278046009,0.0040496698385350796,0.0011474855613075126,6.7117560997829913e-06,6.7117560991540041e-06,0.0015945810636581533,0.0015945810635087183,6.2467947214081554e-06,2.4546335048471462e-06 +10000,cable:2:core,cable:1:core,0.0080340309126035597,0.0048666513437921671,0.0080340309126035597,0.0048666513437921671,0.0080285428981945982,0.0048733819025713878,8.6843954097861601e-06,8.6843954097861601e-06,0.00092467091254269966,0.00092467091254269966,5.4880144089615446e-06,6.7305587792207411e-06 +10000,cable:2:core,cable:2:core,0.011217390349824953,0.043047632316487082,0.011217390349824953,0.043047632316487082,0.011119866253779278,0.042731512078665247,0.00033082163482768073,0.00033082163482768073,0.0074923388149703033,0.0074923388149703033,9.752409604567476e-05,0.00031612023782183474 +10000,cable:2:core,cable:3:core,0.0052676968239068452,0.0024783732958180765,0.0052676968239068461,0.0024783732958180778,0.0052560464242098499,0.0024795778081912593,1.1712500294852345e-05,1.1712500294851615e-05,0.0020153767441258739,0.0020153767441257486,1.1650399696995321e-05,1.2045123731828193e-06 +10000,cable:3:core,cable:1:core,0.0040559166332564895,0.0011450309278026659,0.004055916633257925,0.0011450309278046007,0.0040496698385350787,0.0011474855613075117,6.7117560997843677e-06,6.7117560991559506e-06,0.0015945810636584811,0.0015945810635091815,6.2467947214107575e-06,2.4546335048458452e-06 +10000,cable:3:core,cable:2:core,0.0052676968239068469,0.0024783732958180761,0.0052676968239072927,0.0024783732958178969,0.0052560464242098525,0.0024795778081912593,1.1712500295312677e-05,1.1712500294850797e-05,0.0020153767442050827,0.0020153767441256068,1.1650399696994454e-05,1.204512373183253e-06 +10000,cable:3:core,cable:3:core,0.0051968947578844215,0.056898361105510734,0.0051968947578844276,0.056898361105510831,0.0051094661219450417,0.056507525230177859,0.00040049525319223449,0.00040049525319213838,0.007058670637102386,0.007058670637100692,8.7428635939379787e-05,0.00039083587533287473 +100000,cable:1:core,cable:1:core,0.091870181208271839,0.2852584218993296,0.091870181208271839,0.2852584218993296,0.09149276545797437,0.28368561958405109,0.0016174516288031745,0.0016174516288031745,0.0054263319394923688,0.0054263319394923688,0.00037741575029746899,0.0015728023152785098 +100000,cable:1:core,cable:2:core,0.0087282162587741137,-0.0087087925865141323,0.0087282162587741137,-0.0087087925865141323,0.0087328334575760003,-0.008720356263351766,1.2451391359256568e-05,1.2451391359256568e-05,0.0010089228566527275,0.0010089228566527275,4.6171988018865739e-06,1.1563676837633707e-05 +100000,cable:1:core,cable:3:core,0.000879089354783231,-0.0031017341595444888,0.00087908935478323122,-0.0031017341595444914,0.00087200607389009807,-0.0031043854982987897,7.5632311481977735e-06,7.5632311481984825e-06,0.0023455286322702326,0.0023455286322704525,7.0832808931329313e-06,2.6513387543008679e-06 +100000,cable:2:core,cable:1:core,0.008728216258774112,-0.0087087925865141323,0.008728216258774112,-0.0087087925865141323,0.0087328334575760055,-0.0087203562633517712,1.2451391359263974e-05,1.2451391359263974e-05,0.001008922856653327,0.001008922856653327,4.6171988018935128e-06,1.1563676837638912e-05 +100000,cable:2:core,cable:2:core,0.091870181209119328,0.28525842189891248,0.091870181209119328,0.28525842189891248,0.091489317443228704,0.2836615002411198,0.0016417112990095785,0.0016417112990095785,0.0055081636054669222,0.0055081636054669222,0.00038086376589062387,0.0015969216577926826 +100000,cable:2:core,cable:3:core,0.0029200092671962073,-0.0055306020499575189,0.0029200092671962086,-0.0055306020499575198,0.0029229592659658302,-0.0055355130965314943,5.7289502696843057e-06,5.7289502696857185e-06,0.00091519117450634035,0.00091519117450656608,2.9499987696229163e-06,4.9110465739753834e-06 +100000,cable:3:core,cable:1:core,0.00087908935478322992,-0.0031017341595444897,0.00087908935478321431,-0.0031017341595444797,0.00087200607389009959,-0.003104385498298791,7.5632311481850722e-06,7.5632311481961972e-06,0.0023455286322662926,0.0023455286322697426,7.0832808931303292e-06,2.6513387543013016e-06 +100000,cable:3:core,cable:2:core,0.0029200092671962073,-0.0055306020499575181,0.0029200092671962086,-0.0055306020499575207,0.0029229592659658311,-0.0055355130965314952,5.7289502696847521e-06,5.7289502696876523e-06,0.00091519117450641159,0.00091519117450687487,2.9499987696237837e-06,4.9110465739771181e-06 +100000,cable:3:core,cable:3:core,0.02502944569653533,0.51501770227425014,0.025029445696535323,0.51501770227425014,0.024751108383294702,0.51186488670933217,0.0031650778262676369,0.0031650778262676373,0.0061762080540927926,0.0061762080540927935,0.00027833731324062791,0.0031528155649179768 +1000000,cable:1:core,cable:1:core,0.75407440792450842,1.5740479450662672,0.75407440792450842,1.5740479450662672,0.75371053759751805,1.5682785800867196,0.0057808281311672342,0.0057808281311672342,0.0033223274842110141,0.0033223274842110141,0.00036387032699036759,0.0057693649795476087 +1000000,cable:1:core,cable:2:core,0.0001535709764889112,0.0010629809235061856,0.0001535709764889112,0.0010629809235061856,0.00015809876830168704,0.001064782467487955,4.8730339028257937e-06,4.8730339028257937e-06,0.0045269246088306082,0.0045269246088306082,4.5277918127758479e-06,1.8015439817694115e-06 +1000000,cable:1:core,cable:3:core,8.1257499331941041e-05,8.9426642061850282e-05,8.1257499331935254e-05,8.9426642061852409e-05,8.1864957239162299e-05,8.9675897572887595e-05,6.5660750744571341e-07,6.5660750744116738e-07,0.0054075870204730444,0.0054075870204356056,6.0745790722125813e-07,2.4925551103731304e-07 +1000000,cable:2:core,cable:1:core,0.0001535709764889112,0.0010629809235061854,0.0001535709764889112,0.0010629809235061854,0.00015809876830168729,0.0010647824674879566,4.8730339028266611e-06,4.8730339028266611e-06,0.0045269246088314079,0.0045269246088314079,4.5277918127760918e-06,1.8015439817711462e-06 +1000000,cable:2:core,cable:2:core,0.75407440792448754,1.5740479450662619,0.75407440792448754,1.5740479450662619,0.753724628134118,1.5683153568234434,0.0057432494167894326,0.0057432494167894326,0.0033006559843933086,0.0033006559843933086,0.0003497797903695421,0.005732588242818526 +1000000,cable:2:core,cable:3:core,0.00017862565014624984,0.00022534477815653253,0.00017862565014624984,0.00022534477815653242,0.00018019558652217053,0.00022583671129566818,1.645204679612525e-06,1.6452046796124926e-06,0.0056943975606065797,0.005694397560606467,1.5699363759206879e-06,4.9193313913565608e-07 +1000000,cable:3:core,cable:1:core,8.1257499331941068e-05,8.9426642061850295e-05,8.1257499331935308e-05,8.9426642061852518e-05,8.1864957239162313e-05,8.9675897572887568e-05,6.5660750744562436e-07,6.5660750744113943e-07,0.0054075870204723124,0.0054075870204353758,6.0745790722124457e-07,2.4925551103727238e-07 +1000000,cable:3:core,cable:2:core,0.0001786256501462499,0.00022534477815653258,0.00017862565014624971,0.00022534477815653399,0.00018019558652217056,0.00022583671129566807,1.6452046796121777e-06,1.645204679612418e-06,0.0056943975606053793,0.0056943975606062111,1.5699363759206608e-06,4.9193313913549344e-07 +1000000,cable:3:core,cable:3:core,0.094696109689700492,4.9430140482755665,0.094696109689700422,4.9430140482755665,0.09317622230865559,4.9164327265927605,0.026624738873008071,0.026624738873008078,0.0054144865043148985,0.0054144865043149002,0.0015198873810449015,0.026581321682805914 +10000000,cable:1:core,cable:1:core,4.5479347057971937,6.6714050104071516,4.5479347057971937,6.6714050104071516,4.5499045633958612,6.6607366695104684,0.010848679018519814,0.010848679018519814,0.0013449195900571108,0.0013449195900571108,0.0019698575986675237,0.010668340896683226 +10000000,cable:1:core,cable:2:core,3.0492342492444631e-09,1.314231864570851e-08,3.0492342492444631e-09,1.314231864570851e-08,2.1359267021804777e-08,2.9940483256533021e-08,2.4848252140232918e-08,2.4848252140232918e-08,0.67562077331270665,0.67562077331270665,1.8310032772560315e-08,1.6798164610824511e-08 +10000000,cable:1:core,cable:3:core,6.8259441253474497e-10,1.2859327231771361e-10,6.8259441253474539e-10,1.2859327231771216e-10,1.6416933423796947e-09,-3.0218062359712461e-10,1.0513975968354568e-09,1.0513975968354576e-09,0.6298538404347922,0.62985384043479264,9.5909892984494966e-10,4.3077389591483822e-10 +10000000,cable:2:core,cable:1:core,3.0492342492444631e-09,1.314231864570851e-08,3.0492342492444631e-09,1.314231864570851e-08,2.1359267021804784e-08,2.9940483256533008e-08,2.4848252140232915e-08,2.4848252140232915e-08,0.67562077331270665,0.67562077331270665,1.8310032772560322e-08,1.6798164610824498e-08 +10000000,cable:2:core,cable:2:core,4.5479347057971937,6.6714050104071516,4.5479347057971937,6.6714050104071516,4.5501473574581013,6.6605028805599691,0.011124399425467658,0.011124399425467658,0.0013791104639083534,0.0013791104639083534,0.0022126516609075608,0.010902129847182529 +10000000,cable:2:core,cable:3:core,1.5377810805855466e-09,4.4664472044009271e-10,1.5377810805855466e-09,4.4664472044009214e-10,4.9275836606184257e-09,-7.7688175554226174e-10,3.6038560694105644e-09,3.6038560694105644e-09,0.72244015793167637,0.72244015793167637,3.3898025800328791e-09,1.2235264759823544e-09 +10000000,cable:3:core,cable:1:core,6.8259441253474777e-10,1.2859327231771482e-10,6.8259441253474715e-10,1.285932723177128e-10,1.6416933423796937e-09,-3.0218062359712528e-10,1.0513975968354547e-09,1.0513975968354551e-09,0.62985384043479131,0.62985384043479153,9.5909892984494594e-10,4.3077389591484008e-10 +10000000,cable:3:core,cable:2:core,1.5377810805855476e-09,4.4664472044009277e-10,1.5377810805855499e-09,4.4664472044009375e-10,4.9275836606184224e-09,-7.7688175554226101e-10,3.6038560694105582e-09,3.6038560694105603e-09,0.7224401579316756,0.72244015793167604,3.389802580032875e-09,1.2235264759823538e-09 +10000000,cable:3:core,cable:3:core,1.2514510556301524,49.784417123982301,1.2514510556301526,49.784417123982301,0.28635320029725086,47.884683853844997,2.1308215242095572,2.1308215242095572,0.044498224765926554,0.044498224765926554,0.96509785533290149,1.899733270137304 diff --git a/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/all_air-Y.csv b/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/all_air-Y.csv new file mode 100644 index 000000000..e7adfe9be --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/all_air-Y.csv @@ -0,0 +1,82 @@ +frequency_hz,receiver,source,new_real,new_imaginary,old_real,old_imaginary,fem_real,fem_imaginary,before_absolute_error,after_absolute_error,before_relative_error,after_relative_error,real_absolute_error,imaginary_absolute_error +0.10000000000000001,cable:1:core,cable:1:core,-1.5633838100332428e-24,9.5125437748382881e-12,8.8174985708896091e-24,9.5125437748370102e-12,8.922128316011062e-24,9.5737691434599193e-12,6.1225368622909116e-14,6.1225368621631187e-14,0.0063951164588853379,0.0063951164587518561,1.0485512126044304e-23,6.1225368621631187e-14 +0.10000000000000001,cable:1:core,cable:2:core,-5.3180305293633861e-25,-1.8912830746013254e-12,7.1523658381944664e-24,-1.8912830746024135e-12,7.2386070908688633e-24,-1.9065981897068207e-12,1.5315115104407224e-14,1.5315115105495322e-14,0.0080326915168015788,0.0080326915173722785,7.7704101438052021e-24,1.5315115105495322e-14 +0.10000000000000001,cable:1:core,cable:3:core,-1.1441727970654028e-25,-4.6083134754104461e-13,7.7202067554844912e-24,-4.6083134754232305e-13,7.8258392890097311e-24,-4.6709107939513815e-13,6.2597318528151031e-15,6.2597318540935374e-15,0.013401523019709932,0.013401523022446944,7.9402565687162711e-24,6.2597318540935374e-15 +0.10000000000000001,cable:2:core,cable:1:core,-5.3180304959283696e-25,-1.8912830746013262e-12,7.1523658415379676e-24,-1.891283074602413e-12,7.2386070908688677e-24,-1.9065981897068211e-12,1.5315115104408032e-14,1.5315115105494918e-14,0.0080326915168020003,0.0080326915173720651,7.7704101404617053e-24,1.5315115105494918e-14 +0.10000000000000001,cable:2:core,cable:2:core,-1.304873055325844e-24,9.8662437122090504e-12,6.3935813146373556e-24,9.8662437122081328e-12,6.4613855928501904e-24,9.9274412083039412e-12,6.1197496095808435e-14,6.1197496094890781e-14,0.0061644783194101382,0.0061644783193177026,7.7662586481760348e-24,6.1197496094890781e-14 +0.10000000000000001,cable:2:core,cable:3:core,-5.3180304959283696e-25,-1.8912830746013258e-12,7.1523658415379676e-24,-1.891283074602413e-12,7.2377500976610394e-24,-1.9067155107915219e-12,1.5432436189108823e-14,1.5432436190196113e-14,0.0080937277227594688,0.0080937277233297123,7.769553147253877e-24,1.5432436190196113e-14 +0.10000000000000001,cable:3:core,cable:1:core,-1.144172797065401e-25,-4.6083134754104451e-13,7.7202067554844912e-24,-4.6083134754232315e-13,7.8258392890097296e-24,-4.6709107939513845e-13,6.2597318528153051e-15,6.2597318540939413e-15,0.013401523019710355,0.013401523022447801,7.9402565687162696e-24,6.2597318540939413e-15 +0.10000000000000001,cable:3:core,cable:2:core,-5.3180305293633861e-25,-1.8912830746013258e-12,7.1523658381944649e-24,-1.891283074602413e-12,7.2377500976610423e-24,-1.9067155107915219e-12,1.5432436189108823e-14,1.5432436190196113e-14,0.0080937277227594688,0.0080937277233297123,7.7695531505973811e-24,1.5432436190196113e-14 +0.10000000000000001,cable:3:core,cable:3:core,-1.563383810033243e-24,9.5125437748382881e-12,8.8174985708896106e-24,9.5125437748370102e-12,8.9208262505232889e-24,9.5731276208940894e-12,6.0583846057079155e-14,6.0583846055801226e-14,0.0063285321638092695,0.0063285321636757782,1.0484210060556531e-23,6.0583846055801226e-14 +1,cable:1:core,cable:1:core,-1.6587484628463939e-22,9.5125437748370393e-11,6.8501479736650221e-22,9.5125437748243227e-11,7.0481422084003561e-22,9.5730364221324353e-11,6.0492647308112628e-13,6.0492647295396019e-13,0.0063190658262049764,0.0063190658248765989,8.7068906712467494e-22,6.0492647295396019e-13 +1,cable:1:core,cable:2:core,-6.1282691033068498e-23,-1.8912830746023943e-11,5.4808946526399042e-22,-1.891283074613199e-11,5.6476607782501255e-22,-1.9062309958920426e-11,1.4947921278843586e-13,1.4947921289648309e-13,0.0078416106500505903,0.0078416106557186987,6.2604876885808105e-22,1.4947921289648309e-13 +1,cable:1:core,cable:3:core,-2.0978087755451175e-23,-4.6083134754228781e-12,5.7530319150777122e-22,-4.6083134755499658e-12,5.9520194157421592e-22,-4.6680872622437967e-12,5.9773786693830928e-14,5.9773786820918667e-14,0.012804770634278937,0.012804770661503736,6.1618002932966713e-22,5.9773786820918667e-14 +1,cable:2:core,cable:1:core,-6.1282687714116693e-23,-1.8912830746023946e-11,5.4808946858294201e-22,-1.8912830746131994e-11,5.6476607782501142e-22,-1.9062309958920426e-11,1.4947921278843262e-13,1.4947921289647986e-13,0.0078416106500504221,0.0078416106557185304,6.2604876553912814e-22,1.4947921289647986e-13 +1,cable:2:core,cable:2:core,-1.3737131791915714e-22,9.8662437122081522e-11,4.9734272200821993e-22,9.8662437121989743e-11,5.1101837548125129e-22,9.9269687678295667e-11,6.0725055630592353e-13,6.0725055621414526e-13,0.0061171800829458323,0.0061171800820212976,6.4838969340040844e-22,6.0725055621414526e-13 +1,cable:2:core,cable:3:core,-6.1282687714116716e-23,-1.8912830746023946e-11,5.4808946858294211e-22,-1.8912830746131997e-11,5.6461582564736953e-22,-1.9065758134391154e-11,1.5292738825915747e-13,1.5292738836720794e-13,0.0080210494217538789,0.0080210494274211303,6.2589851336148625e-22,1.5292738836720794e-13 +1,cable:3:core,cable:1:core,-2.0978087755451172e-23,-4.6083134754228781e-12,5.753031915077715e-22,-4.6083134755499666e-12,5.9520194157421583e-22,-4.6680872622437911e-12,5.9773786693824466e-14,5.9773786820913012e-14,0.012804770634277568,0.01280477066150254,6.1618002932966704e-22,5.9773786820913012e-14 +1,cable:3:core,cable:2:core,-6.1282691033068486e-23,-1.8912830746023946e-11,5.4808946526399061e-22,-1.8912830746131997e-11,5.6461582564737038e-22,-1.9065758134391151e-11,1.5292738825915424e-13,1.5292738836720471e-13,0.0080210494217537107,0.0080210494274209621,6.2589851668043887e-22,1.5292738836720471e-13 +1,cable:3:core,cable:3:core,-1.6587484628463939e-22,9.5125437748370393e-11,6.8501479736650221e-22,9.5125437748243227e-11,7.0463369720842808e-22,9.5718203451660386e-11,5.9276570341715909e-13,5.92765703289993e-13,0.0061928210313361935,0.0061928210300076468,8.705085434930674e-22,5.92765703289993e-13 +10,cable:1:core,cable:1:core,-1.9349443967013251e-20,9.5125437748007416e-10,4.7166583903820262e-20,9.512543774675523e-10,5.2042336276529873e-20,9.5737066171470717e-10,6.116284247154873e-12,6.1162842346330137e-12,0.0063886271971195022,0.0063886271840400757,7.1391780243543127e-20,6.1162842346330129e-12 +10,cable:1:core,cable:2:core,-8.4747514536548164e-21,-1.891283074633155e-10,3.6686164480533427e-20,-1.891283074739372e-10,4.0811616664672256e-20,-1.9069227257271197e-10,1.5639650987747745e-12,1.5639651093964723e-12,0.0082015127182378418,0.0082015127739385671,4.9286368118327074e-20,1.5639651093964715e-12 +10,cable:1:core,cable:3:core,-4.8594067576892981e-21,-4.6083134757836114e-11,3.6212833098818979e-20,-4.6083134770283224e-11,4.1095181352562286e-20,-4.6709769832231127e-11,6.2663506194790321e-13,6.2663507439501505e-13,0.013415503099214725,0.013415503365692423,4.5954588110251586e-20,6.2663507439501334e-13 +10,cable:2:core,cable:1:core,-8.4747482119580589e-21,-1.8912830746331555e-10,3.6686167722230193e-20,-1.8912830747393725e-10,4.0811616664672268e-20,-1.9069227257271205e-10,1.5639650987748004e-12,1.5639651093964982e-12,0.0082015127182379736,0.0082015127739386989,4.9286364876630325e-20,1.5639651093964974e-12 +10,cable:2:core,cable:2:core,-1.5730906353677381e-20,9.8662437121819414e-10,3.4333568788473328e-20,9.8662437120915512e-10,3.7800341164145078e-20,9.9277769632842221e-10,6.1533251192670879e-12,6.1533251102280717e-12,0.0061980896045749781,0.0061980895954702039,5.3531247517822459e-20,6.1533251102280717e-12 +10,cable:2:core,cable:3:core,-8.4747482119580634e-21,-1.8912830746331555e-10,3.6686167722230193e-20,-1.891283074739372e-10,4.0810399278134823e-20,-1.9070192842709623e-10,1.5736209531590275e-12,1.5736209637806736e-12,0.0082517306780178143,0.0082517307337154483,4.9285147490092886e-20,1.5736209637806728e-12 +10,cable:3:core,cable:1:core,-4.8594067576892981e-21,-4.608313475783612e-11,3.6212833098818979e-20,-4.6083134770283224e-11,4.1095181352562256e-20,-4.6709769832231108e-11,6.2663506194788382e-13,6.266350743949892e-13,0.013415503099214316,0.013415503365691875,4.5954588110251556e-20,6.2663507439498749e-13 +10,cable:3:core,cable:2:core,-8.4747514536548179e-21,-1.8912830746331555e-10,3.6686164480533427e-20,-1.891283074739372e-10,4.0810399278134859e-20,-1.9070192842709623e-10,1.5736209531590275e-12,1.5736209637806736e-12,0.0082517306780178143,0.0082517307337154483,4.9285150731789677e-20,1.5736209637806728e-12 +10,cable:3:core,cable:3:core,-1.9349443967013254e-20,9.5125437748007416e-10,4.7166583903820256e-20,9.5125437746755209e-10,5.2026027179996697e-20,9.5731200695540039e-10,6.057629487848298e-12,6.0576294753262319e-12,0.0063277483660878322,0.0063277483530073883,7.1375471147009951e-20,6.0576294753262311e-12 +100,cable:1:core,cable:1:core,-2.7295121647878419e-18,9.5125437737306668e-09,2.1083091976300289e-18,9.5125437725313327e-09,3.4449521314309137e-18,9.5732682963375009e-09,6.0724523806168252e-11,6.0724522606834416e-11,0.0063431340192774055,0.0063431338939979503,6.1744642962187556e-18,6.0724522606834106e-11 +100,cable:1:core,cable:2:core,-1.5223928822208093e-18,-1.8912830755349785e-09,1.4561375241927259e-18,-1.8912830765424522e-09,2.5899675468275766e-18,-1.9075708161815633e-09,1.628773963911108e-11,1.6287740646585288e-11,0.008538471809772543,0.0085384723379176575,4.1123604290483863e-18,1.6287740646584767e-11 +100,cable:1:core,cable:3:core,-1.2797236657190993e-18,-4.608313486273717e-10,1.0295216100662539e-18,-4.6083134977899235e-10,2.3636665605984129e-18,-4.6738078380863707e-10,6.5494340296448564e-12,6.5494351812663839e-12,0.014013057995825596,0.01401306045981549,3.643390226317512e-18,6.5494351812653701e-12 +100,cable:2:core,cable:1:core,-1.5223898758252984e-18,-1.8912830755349698e-09,1.4561405305882495e-18,-1.891283076542444e-09,2.589967546827577e-18,-1.9075708161815624e-09,1.6287739639118525e-11,1.6287740646593146e-11,0.0085384718097764496,0.0085384723379217809,4.1123574226528751e-18,1.6287740646592626e-11 +100,cable:2:core,cable:2:core,-2.1466469028642353e-18,9.8662437114086359e-09,1.5506509108463473e-18,9.8662437105429252e-09,2.5115239067323068e-18,9.92729673521363e-09,6.1053024670704833e-11,6.1053023804994315e-11,0.006150015084583951,0.0061500149973788901,4.6581708095965421e-18,6.1053023804994135e-11 +100,cable:2:core,cable:3:core,-1.5223898758252982e-18,-1.8912830755349698e-09,1.4561405305882495e-18,-1.891283076542444e-09,2.590339306423551e-18,-1.9069723033144639e-09,1.5689226772019991e-11,1.5689227779494629e-11,0.0082272966129350242,0.0082272971412461256,4.1127291822488488e-18,1.5689227779494089e-11 +100,cable:3:core,cable:1:core,-1.2797236657190993e-18,-4.608313486273717e-10,1.0295216100662541e-18,-4.6083134977899245e-10,2.3636665605984129e-18,-4.6738078380863738e-10,6.5494340296450632e-12,6.5494351812666941e-12,0.01401305799582603,0.014013060459816144,3.643390226317512e-18,6.5494351812656803e-12 +100,cable:3:core,cable:2:core,-1.5223928822208089e-18,-1.8912830755349777e-09,1.4561375241927257e-18,-1.8912830765424522e-09,2.5903393064235491e-18,-1.9069723033144643e-09,1.5689226772012133e-11,1.5689227779487184e-11,0.0082272966129309025,0.0082272971412422208,4.1127321886443584e-18,1.5689227779486644e-11 +100,cable:3:core,cable:3:core,-2.7295121647878415e-18,9.5125437737306668e-09,2.1083091976300285e-18,9.5125437725313327e-09,3.4445149991456232e-18,9.5725309918854146e-09,5.9987219354081907e-11,5.9987218154748084e-11,0.0062665996490304148,0.0062665995237413119,6.1740271639334647e-18,5.9987218154747761e-11 +1000,cable:1:core,cable:1:core,-5.0118206940306591e-16,9.512543740175937e-08,-1.873808118781869e-16,9.5125437293721708e-08,1.9082675595006146e-16,9.568462842719928e-08,5.5919113347770054e-10,5.5919102544033804e-10,0.005844106233878054,0.0058441051047796368,6.9200882535312736e-16,5.5919102543990988e-10 +1000,cable:1:core,cable:2:core,-3.4574583514384184e-16,-1.8912831033485189e-08,-1.8946804337046478e-16,-1.8912831119165847e-08,1.3056430654978372e-16,-1.9077570753226516e-08,1.6473963406097957e-10,1.6473971974201554e-10,0.0086352521603473963,0.0086352566515395437,4.7631014169362561e-16,1.6473971974132696e-10 +1000,cable:1:core,cable:3:core,-3.5442949770542699e-16,-4.6083138025453172e-09,-2.8095123442940841e-16,-4.6083138873394025e-09,9.195269911084729e-17,-4.673636267226392e-09,6.532237988805393e-11,6.5322464682599963e-11,0.013976778712139707,0.013976796855303014,4.463821968162743e-16,6.5322464681074784e-11 +1000,cable:2:core,cable:1:core,-3.4574345870567365e-16,-1.8912831033481185e-08,-1.894656669322759e-16,-1.8912831119161843e-08,1.3056430654978367e-16,-1.9077570753226526e-08,1.6473963406499303e-10,1.6473971974602902e-10,0.008635252160557768,0.0086352566517499171,4.7630776525545727e-16,1.6473971974534044e-10 +1000,cable:2:core,cable:2:core,-3.7940269276055945e-16,9.8662436870980812e-08,-1.3229236679643231e-16,9.866243679281464e-08,1.4043760685025576e-16,9.9255655883493975e-08,5.9321909067939758e-10,5.9321901251339034e-10,0.0059766779575333882,0.0059766771700114421,5.198402996108152e-16,5.9321901251316255e-10 +1000,cable:2:core,cable:3:core,-3.4574345870567375e-16,-1.8912831033481185e-08,-1.8946566693227583e-16,-1.8912831119161843e-08,1.3055810960136316e-16,-1.9081350477120368e-08,1.685193579588281e-10,1.6851944363985561e-10,0.0088316263652770521,0.0088316308555791197,4.7630156830703693e-16,1.6851944363918249e-10 +1000,cable:3:core,cable:1:core,-3.5442949770542699e-16,-4.608313802545318e-09,-2.8095123442940836e-16,-4.6083138873394025e-09,9.1952699110847192e-17,-4.6736362672263928e-09,6.5322379888054757e-11,6.5322464682599963e-11,0.013976778712139882,0.013976796855303012,4.463821968162742e-16,6.5322464681074784e-11 +1000,cable:3:core,cable:2:core,-3.4574583514384184e-16,-1.8912831033485189e-08,-1.8946804337046485e-16,-1.8912831119165847e-08,1.3055810960136316e-16,-1.9081350477120351e-08,1.68519357954808e-10,1.6851944363583551e-10,0.0088316263650663786,0.0088316308553684462,4.7630394474520498e-16,1.6851944363516239e-10 +1000,cable:3:core,cable:3:core,-5.0118206940306571e-16,9.512543740175937e-08,-1.8738081187818683e-16,9.5125437293721708e-08,1.9086030057746862e-16,9.5694942729070334e-08,5.6950543534875172e-10,5.6950532731138385e-10,0.0059512594825530587,0.0059512583535762838,6.9204236998053433e-16,5.6950532731096333e-10 +10000,cable:1:core,cable:1:core,-1.1772692878024628e-13,9.5125425061182361e-07,-1.0101134288504447e-13,9.5125424198982738e-07,8.0692288813055717e-15,9.5706735548959623e-07,5.8131135007922797e-09,5.8131048791337479e-09,0.0060738812868803057,0.0060738722784667576,1.2579615766155186e-13,5.8131048777726271e-09 +10000,cable:1:core,cable:2:core,-9.1362925460009304e-14,-1.8912840908954329e-07,-8.6514466734997425e-14,-1.8912841431725653e-07,4.594027671235939e-15,-1.9067497289021409e-07,1.5465585756411843e-09,1.5465638036476314e-09,0.0081109678538234479,0.0081109952722432221,9.5956953131245239e-14,1.5465638006707943e-09 +10000,cable:1:core,cable:3:core,-1.0045430431703108e-13,-4.608324586372502e-08,-1.0143498648802649e-13,-4.6083248237275564e-08,1.7806485012887294e-15,-4.6609992529424619e-08,5.2674430226161561e-10,5.2674667562086237e-10,0.011301102482028603,0.011301153401565766,1.0223495281831981e-13,5.2674666569959834e-10 +10000,cable:2:core,cable:1:core,-9.1361682981908575e-14,-1.8912840908828286e-07,-8.6513224256953002e-14,-1.8912841431599611e-07,4.5940276712359398e-15,-1.9067497289021396e-07,1.5465585769013903e-09,1.5465638049078599e-09,0.0081109678604326368,0.0081109952788525289,9.595571065314451e-14,1.5465638019310999e-09 +10000,cable:2:core,cable:2:core,-8.6751292961194326e-14,9.866242787209633e-07,-7.2680192975604353e-14,9.8662427226833344e-07,6.0984080445449842e-15,9.9263730262546271e-07,6.0130303576453305e-09,6.0130239052162803e-09,0.0060576308604776852,0.0060576243601889769,9.284970100573931e-14,6.0130239044994142e-09 +10000,cable:2:core,cable:3:core,-9.1361682981908562e-14,-1.891284090882828e-07,-8.6513224256953027e-14,-1.8912841431599611e-07,4.5935820977784853e-15,-1.9069242896894837e-07,1.5640146556058312e-09,1.56401988360908e-09,0.0082017658701096577,0.0082017932860027638,9.5955265079687046e-14,1.5640198806655711e-09 +10000,cable:3:core,cable:1:core,-1.0045430431703113e-13,-4.6083245863725027e-08,-1.0143498648802654e-13,-4.608324823727559e-08,1.7806485012887271e-15,-4.6609992529424632e-08,5.2674430226160238e-10,5.2674667562086899e-10,0.011301102482028317,0.011301153401565905,1.0223495281831985e-13,5.2674666569960496e-10 +10000,cable:3:core,cable:2:core,-9.1362925460009165e-14,-1.8912840908954321e-07,-8.65144667349974e-14,-1.891284143172565e-07,4.5935820977784813e-15,-1.9069242896894851e-07,1.5640146543456509e-09,1.564019882348877e-09,0.0082017658635012061,0.0082017932793941942,9.5956507557787649e-14,1.5640198794052919e-09 +10000,cable:3:core,cable:3:core,-1.1772692878024628e-13,9.512542506118234e-07,-1.0101134288504453e-13,9.5125424198982759e-07,8.0711091903339482e-15,9.5711687567891938e-07,5.8626336901066072e-09,5.8626250684456517e-09,0.0061253059465156941,0.0061252969385656988,1.2579803797058023e-13,5.8626250670959883e-09 +100000,cable:1:core,cable:1:core,-3.5238754535009548e-11,9.5124847980076869e-06,-3.4604866529119772e-11,9.5124842961546061e-06,2.6076244926853928e-13,9.5701919094738434e-06,5.770762385174679e-08,5.7707122385223088e-08,0.0060299338192602095,0.0060298814204652381,3.5499516984278088e-11,5.770711146615649e-08 +100000,cable:1:core,cable:2:core,-2.8499648231873601e-11,-1.8913281513774834e-06,-2.8436018490168415e-11,-1.8913282851842096e-06,1.325603233389172e-13,-1.9071973370109952e-06,1.5869077542343327e-08,1.5869211463529829e-08,0.008320626940059429,0.0083206971589003965,2.8632208555212517e-11,1.586918563351188e-08 +100000,cable:1:core,cable:3:core,-3.166763593333253e-11,-4.608786997428773e-07,-3.1713786251802083e-11,-4.6087867059802101e-07,4.7645418153283817e-14,-4.6624005749939556e-07,5.3614809796148935e-09,5.3614515620725e-09,0.011499400133850181,0.011499337038580007,3.1715281351485816e-11,5.3613577565182542e-09 +100000,cable:2:core,cable:1:core,-2.849927538271401e-11,-1.8913281510653945e-06,-2.8435645641065308e-11,-1.8913282848721205e-06,1.3256032333891698e-13,-1.9071973370109952e-06,1.5869077853760675e-08,1.5869211774945476e-08,0.0083206271033447587,0.0083206973221848363,2.8631835706052925e-11,1.5869185945600746e-08 +100000,cable:2:core,cable:2:core,-2.5663216825296612e-11,9.8662002709805753e-06,-2.5082672191159045e-11,9.8661998438227087e-06,2.023866598338372e-13,9.9263181440735071e-06,6.0118305568099178e-08,6.0117878657245518e-08,0.006056455646043615,0.0060564126380674987,2.5865603485130448e-11,6.0117873092931795e-08 +100000,cable:2:core,cable:3:core,-2.849927538271401e-11,-1.8913281510653945e-06,-2.8435645641065292e-11,-1.8913282848721201e-06,1.3259940800503111e-13,-1.90690759831909e-06,1.5579339640163257e-08,1.5579473563472328e-08,0.0081699499513747647,0.0081700201819979949,2.8631874790719042e-11,1.5579447253695464e-08 +100000,cable:3:core,cable:1:core,-3.166763593333253e-11,-4.6087869974287757e-07,-3.1713786251802077e-11,-4.6087867059802069e-07,4.7645418153283849e-14,-4.6624005749939535e-07,5.3614809796149993e-09,5.3614515620720236e-09,0.011499400133850413,0.011499337038578991,3.1715281351485816e-11,5.3613577565177778e-09 +100000,cable:3:core,cable:2:core,-2.8499648231873591e-11,-1.8913281513774834e-06,-2.8436018490168438e-11,-1.8913282851842098e-06,1.3259940800503118e-13,-1.9069075983190895e-06,1.5579339328757355e-08,1.5579473252068792e-08,0.0081699497880706289,0.0081700200186950993,2.8632247639878624e-11,1.5579446941606174e-08 +100000,cable:3:core,cable:3:core,-3.5238754535009541e-11,9.5124847980076869e-06,-3.4604866529119772e-11,9.5124842961546061e-06,2.6082462984435911e-13,9.5706034093503467e-06,5.8119123653714196e-08,5.8118622184453578e-08,0.0060726707782011525,0.0060726183813731606,3.54995791648539e-11,5.8118611342659752e-08 +1000000,cable:1:core,cable:1:core,-1.5615266547687123e-08,9.5093614110976026e-05,-1.559589973180337e-08,9.5093595392883751e-05,8.0530792168868948e-12,9.5693400052747324e-05,6.000075943997591e-07,5.9998938662500328e-07,0.006270104250335193,0.0062699139783337198,1.5623319626904009e-08,5.9978594177129835e-07 +1000000,cable:1:core,cable:2:core,-1.2864123870071459e-08,-1.893554815543121e-05,-1.2861993771875996e-08,-1.8935550648911418e-05,4.1819271429109229e-12,-1.9086254578921379e-05,1.5125215038988852e-07,1.5125481603998266e-07,0.0079246637817000452,0.007924803445041699,1.286830579721437e-08,1.5070642349016971e-07 +1000000,cable:1:core,cable:3:core,-1.4543198704248249e-08,-4.6305956615392597e-06,-1.4544270082946612e-08,-4.6305946147002577e-06,1.620331950101396e-12,-4.6777057948875268e-06,4.9305640920684913e-08,4.9304324606339191e-08,0.010540560497534986,0.010540279095838634,1.4544819036198351e-08,4.7110133348267081e-08 +1000000,cable:2:core,cable:1:core,-1.2864022244693608e-08,-1.8935548062092855e-05,-1.2861892146596569e-08,-1.89355505555729e-05,4.1819271429109229e-12,-1.9086254578921386e-05,1.5125223474544779e-07,1.5125490039399239e-07,0.0079246682014021338,0.0079248078646626009,1.2868204171836519e-08,1.5070651682853165e-07 +1000000,cable:2:core,cable:2:core,-1.1326024378319978e-08,9.8638679408527877e-05,-1.1308330073687395e-08,9.8638662485275188e-05,6.228755939294857e-12,9.9256357247823405e-05,6.1779838047781191e-07,6.1778178438498085e-07,0.0062242701385392465,0.0062241029342080456,1.1332253134259273e-08,6.1767783929552789e-07 +1000000,cable:2:core,cable:3:core,-1.2864022244693615e-08,-1.8935548062092855e-05,-1.2861892146596574e-08,-1.893555055557291e-05,4.1817462845641913e-12,-1.9088669470170267e-05,1.5365851055144858e-07,1.5366117364371122e-07,0.008049723465093573,0.0080498629767691669,1.286820399097818e-08,1.5312140807741204e-07 +1000000,cable:3:core,cable:1:core,-1.4543198704248249e-08,-4.6305956615392597e-06,-1.4544270082946622e-08,-4.6305946147002577e-06,1.6203319501013948e-12,-4.6777057948875277e-06,4.9305640920685727e-08,4.9304324606339999e-08,0.010540560497535158,0.010540279095838804,1.4544819036198351e-08,4.7110133348267928e-08 +1000000,cable:3:core,cable:2:core,-1.2864123870071464e-08,-1.8935548155431213e-05,-1.2861993771875996e-08,-1.8935550648911421e-05,4.1817462845641962e-12,-1.908866947017027e-05,1.5365842605000035e-07,1.536610891437818e-07,0.0080497190383079145,0.0080498585500630732,1.2868305616356029e-08,1.5312131473905688e-07 +1000000,cable:3:core,cable:3:core,-1.5615266547687126e-08,9.5093614110976026e-05,-1.5595899731803376e-08,9.5093595392883751e-05,8.05380599468505e-12,9.5700186415770239e-05,6.0679168782415467e-07,6.0677347427375065e-07,0.006340548650427295,0.0063403583315670481,1.562332035368181e-08,6.0657230479421341e-07 +10000000,cable:1:core,cable:1:core,-1.0245582137099509e-05,0.00093666552576450414,-1.0244974735870984e-05,0.00093666492829382747,2.8187296625399645e-10,0.00093504980244725748,1.0371784536962841e-05,1.0372477580147157e-05,0.011092226863015033,0.011092968046193193,1.0245864010065763e-05,1.6157233172466512e-06 +10000000,cable:1:core,cable:2:core,-8.4133474228971043e-06,-0.00019733225671301211,-8.4132746995742166e-06,-0.0001973323193329975,1.5097250181016025e-10,-0.00020541214081067286,1.166487233752054e-05,1.1664968164594844e-05,0.056787647952458817,0.056788114463707504,8.4134983953989146e-06,8.0798840976607506e-06 +10000000,cable:1:core,cable:3:core,-9.4191344030158392e-06,-5.1467309144819749e-05,-9.4191677365102664e-06,-5.1467267275120134e-05,7.0525640853636929e-11,-5.9850382341684698e-05,1.2609467382028214e-05,1.2609414645891618e-05,0.21068315503871385,0.21068227390588676,9.4192049286566933e-06,8.3830731968649486e-06 +10000000,cable:2:core,cable:1:core,-8.4133199344201284e-06,-0.00019733222067529809,-8.4132472113751319e-06,-0.00019733228329474613,1.5097250181016004e-10,-0.00020541214081067278,1.1664877473773147e-05,1.1664973300254706e-05,0.056787672957079247,0.05678813946544229,8.4134709069219387e-06,8.0799201353746878e-06 +10000000,cable:2:core,cable:2:core,-7.3114509332476665e-06,0.00097540345949388219,-7.3108984596087289e-06,0.00097540292219751854,2.121268563158776e-10,0.00097614442289829665,7.3486162844970809e-06,7.3491117470868954e-06,0.007528205982756048,0.0075287135537447179,7.3116630601039827e-06,7.4096340441445491e-07 +10000000,cable:2:core,cable:3:core,-8.4133199344201318e-06,-0.0001973322206752982,-8.4132472113751319e-06,-0.00019733228329474613,1.5099445474206141e-10,-0.00020537275581301788,1.1637631532530349e-05,1.1637727371438444e-05,0.056665897511365693,0.056666364169699779,8.4134709288748745e-06,8.0405351377196843e-06 +10000000,cable:3:core,cable:1:core,-9.4191344030158291e-06,-5.1467309144819716e-05,-9.4191677365102597e-06,-5.1467267275120114e-05,7.0525640853637097e-11,-5.9850382341684651e-05,1.260946738202819e-05,1.2609414645891603e-05,0.21068315503871363,0.21068227390588667,9.4192049286566831e-06,8.383073196864935e-06 +10000000,cable:3:core,cable:2:core,-8.4133474228971111e-06,-0.00019733225671301224,-8.4132746995742132e-06,-0.00019733231933299748,1.5099445474206126e-10,-0.00020537275581301799,1.1637626506216432e-05,1.1637722345715908e-05,0.056665873037262962,0.056666339698476606,8.4134984173518538e-06,8.0404991000057472e-06 +10000000,cable:3:core,cable:3:core,-1.0245582137099514e-05,0.00093666552576450457,-1.0244974735870988e-05,0.00093666492829382747,2.8186944950225374e-10,0.00093522723119616807,1.034563946082993e-05,1.0346324009328286e-05,0.011062166621899204,0.011062898581443924,1.0245864006549016e-05,1.4382945683365017e-06 diff --git a/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/all_air-Z.csv b/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/all_air-Z.csv new file mode 100644 index 000000000..1121bc150 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/all_air-Z.csv @@ -0,0 +1,82 @@ +frequency_hz,receiver,source,new_real,new_imaginary,old_real,old_imaginary,fem_real,fem_imaginary,before_absolute_error,after_absolute_error,before_relative_error,after_relative_error,real_absolute_error,imaginary_absolute_error +0.10000000000000001,cable:1:core,cable:1:core,3.1368030977512313e-06,1.2442491410079335e-06,3.1368030977512313e-06,1.2442491410079333e-06,3.1562835747436832e-06,1.2381208184182731e-06,2.0421687531063286e-08,2.0421687531063349e-08,0.006023318557434835,0.0060233185574348541,1.9480476992451874e-08,6.1283225896603621e-09 +0.10000000000000001,cable:1:core,cable:2:core,9.836575756099057e-08,8.1595650851660634e-07,9.8365757560990584e-08,8.1595650851660624e-07,9.8235674073162367e-08,8.1275015799742355e-07,3.2089882152586086e-09,3.2089882152587141e-09,0.003919779856153092,0.0039197798561532203,1.3008348782820304e-10,3.2063505191827915e-09 +0.10000000000000001,cable:1:core,cable:3:core,9.8363910649020915e-08,7.2885335558399048e-07,9.8363910649020915e-08,7.2885335558399048e-07,9.8230350799228339e-08,7.2601055923285115e-07,2.8459320665693526e-09,2.8459320665693526e-09,0.0038845644572714816,0.0038845644572714816,1.3355984979257542e-10,2.8427963511393303e-09 +0.10000000000000001,cable:2:core,cable:1:core,9.836575756099057e-08,8.1595650851660634e-07,9.8365757560990584e-08,8.1595650851660624e-07,9.823567407316238e-08,8.1275015799742355e-07,3.2089882152586082e-09,3.2089882152587137e-09,0.0039197798561530911,0.0039197798561532203,1.3008348782818981e-10,3.2063505191827915e-09 +0.10000000000000001,cable:2:core,cable:2:core,3.1368030977512313e-06,1.2442491410079335e-06,3.1368030977512313e-06,1.2442491410079333e-06,3.1562852353826078e-06,1.2384264704427096e-06,2.0333646480624677e-08,2.0333646480624737e-08,0.005997150910064389,0.0059971509100644063,1.9482137631376542e-08,5.8226705652239005e-09 +0.10000000000000001,cable:2:core,cable:3:core,9.836575756099057e-08,8.1595650851660634e-07,9.8365757560990584e-08,8.1595650851660624e-07,9.8235588324076653e-08,8.1275893040631542e-07,3.200226523490131e-09,3.2002265234902365e-09,0.0039090359121177433,0.0039090359121178716,1.3016923691391704e-10,3.1975781102909203e-09 +0.10000000000000001,cable:3:core,cable:1:core,9.8363910649020915e-08,7.2885335558399048e-07,9.8363910649020915e-08,7.2885335558399048e-07,9.8230350799228273e-08,7.2601055923285019e-07,2.8459320665703076e-09,2.8459320665703076e-09,0.00388456445727279,0.00388456445727279,1.3355984979264159e-10,2.8427963511402832e-09 +0.10000000000000001,cable:3:core,cable:2:core,9.836575756099057e-08,8.1595650851660634e-07,9.8365757560990584e-08,8.1595650851660624e-07,9.8235588324076587e-08,8.1275893040631479e-07,3.2002265234907684e-09,3.2002265234908739e-09,0.0039090359121185247,0.003909035912118654,1.3016923691398321e-10,3.1975781102915556e-09 +0.10000000000000001,cable:3:core,cable:3:core,3.1368030977512313e-06,1.2442491410079335e-06,3.1368030977512313e-06,1.2442491410079333e-06,3.1562834239407481e-06,1.2381628940589164e-06,2.0408956621405036e-08,2.0408956621405099e-08,0.0060195365806944637,0.0060195365806944819,1.9480326189516824e-08,6.0862469490171018e-09 +1,cable:1:core,cable:1:core,4.0258262108767965e-06,1.1002345673096759e-05,4.0258262108768049e-06,1.1002345673096755e-05,4.0441335787761909e-06,1.0950406484961215e-05,5.5071217378795182e-08,5.5071217378801198e-08,0.004717699299481245,0.0047176992994817602,1.8307367899394447e-08,5.1939188135543635e-08 +1,cable:1:core,cable:2:core,9.7665254323323244e-07,6.7199804225952308e-06,9.7665254323323498e-07,6.7199804225952341e-06,9.7539641318315375e-07,6.696910388622472e-06,2.3104205898647905e-08,2.3104205898644384e-08,0.0034139587900371472,0.0034139587900366272,1.2561300500786954e-09,2.3070033972758757e-08 +1,cable:1:core,cable:3:core,9.7651034829616332e-07,5.8489747237081976e-06,9.7651034817260789e-07,5.8489747235720515e-06,9.7491585773259541e-07,5.8296236366655687e-06,1.9416667176051124e-08,1.9416667321883764e-08,0.0032850688975510005,0.0032850689222241468,1.5944905635679037e-09,1.9351087042628942e-08 +1,cable:2:core,cable:1:core,9.7665254323323244e-07,6.7199804225952308e-06,9.7665254323323498e-07,6.7199804225952341e-06,9.7539641318315396e-07,6.6969103886224737e-06,2.3104205898646201e-08,2.310420589864268e-08,0.0034139587900368948,0.0034139587900363744,1.2561300500784836e-09,2.3070033972757063e-08 +1,cable:2:core,cable:2:core,4.0258262108767965e-06,1.1002345673096759e-05,4.0258262108768049e-06,1.1002345673096755e-05,4.0443033445780337e-06,1.0953379895793153e-05,5.23359514746707e-08,5.233595147467686e-08,0.0044822879684521307,0.0044822879684526589,1.8477133701237262e-08,4.8965777303605965e-08 +1,cable:2:core,cable:3:core,9.7665254323323244e-07,6.7199804225952308e-06,9.7665254323323498e-07,6.7199804225952341e-06,9.7539495757458187e-07,6.6972194654576678e-06,2.2795672648706442e-08,2.2795672648702921e-08,0.0033682167140297956,0.0033682167140292756,1.2575856586505703e-09,2.2760957137562972e-08 +1,cable:3:core,cable:1:core,9.7651034829616332e-07,5.8489747237081976e-06,9.7651034817260789e-07,5.8489747235720515e-06,9.7491585773259584e-07,5.8296236366655721e-06,1.9416667176047709e-08,1.9416667321880353e-08,0.0032850688975504206,0.0032850689222235678,1.5944905635674802e-09,1.9351087042625554e-08 +1,cable:3:core,cable:2:core,9.7665254323323244e-07,6.7199804225952308e-06,9.7665254323323498e-07,6.7199804225952341e-06,9.7539495757458145e-07,6.6972194654576703e-06,2.2795672648703931e-08,2.2795672648700407e-08,0.0033682167140294231,0.0033682167140289027,1.2575856586509938e-09,2.2760957137560431e-08 +1,cable:3:core,cable:3:core,4.0258262108767965e-06,1.1002345673096759e-05,4.0258262108768049e-06,1.1002345673096755e-05,4.0441325229622845e-06,1.0951493272211791e-05,5.4047088154091449e-08,5.404708815409751e-08,0.0046295626551396338,0.0046295626551401533,1.830631208548799e-08,5.0852400884968092e-08 +10,cable:1:core,cable:1:core,1.3444055602265216e-05,9.5357202577227293e-05,1.344405560226531e-05,9.5357202577227266e-05,1.3464438746537811e-05,9.4931342329192611e-05,4.2634777286456104e-07,4.263477728645927e-07,0.0044466141225671524,0.004446614122567482,2.0383144272595211e-08,4.25860248034682e-07 +10,cable:1:core,cable:2:core,9.5543897693967477e-06,5.2957258634098345e-05,9.5543897693967985e-06,5.2957258634098372e-05,9.5453284797023948e-06,5.280770955685593e-05,1.4982334088850044e-07,1.498233408884703e-07,0.0027919058276288339,0.0027919058276282718,9.0612896943528435e-09,1.4954907724241495e-07 +10,cable:1:core,cable:3:core,9.5443477251560087e-06,4.4249689272014522e-05,9.5443477251560104e-06,4.4249689272014495e-05,9.5227205948054659e-06,4.4151513573041263e-05,1.0052960079539756e-07,1.0052960079542367e-07,0.0022257418472240091,0.0022257418472245872,2.1627130350542766e-08,9.8175698973258546e-08 +10,cable:2:core,cable:1:core,9.5543897693967477e-06,5.2957258634098345e-05,9.5543897693967985e-06,5.2957258634098372e-05,9.5453284797023982e-06,5.2807709556855943e-05,1.4982334088848671e-07,1.4982334088845658e-07,0.0027919058276285771,0.0027919058276280159,9.0612896943494554e-09,1.495490772424014e-07 +10,cable:2:core,cable:2:core,1.3444055602265216e-05,9.5357202577227279e-05,1.344405560226531e-05,9.5357202577227252e-05,1.3470778065139108e-05,9.4951611885806569e-05,4.0647004685357752e-07,4.0647004685361076e-07,0.004238371968339167,0.0042383719683395131,2.6722462873892839e-08,4.0559069142071045e-07 +10,cable:2:core,cable:3:core,9.5543897693967494e-06,5.2957258634098345e-05,9.5543897693968002e-06,5.2957258634098372e-05,9.545361931628984e-06,5.2809202699622741e-05,1.4833091919156415e-07,1.4833091919153401e-07,0.0027640190818798002,0.0027640190818792386,9.0278377677653913e-09,1.4805593447560385e-07 +10,cable:3:core,cable:1:core,9.5443477251560087e-06,4.4249689272014522e-05,9.5443477251560104e-06,4.4249689272014495e-05,9.5227205948054608e-06,4.4151513573041283e-05,1.0052960079537881e-07,1.0052960079540491e-07,0.0022257418472235928,0.0022257418472241704,2.1627130350547848e-08,9.8175698973238217e-08 +10,cable:3:core,cable:2:core,9.5543897693967477e-06,5.2957258634098345e-05,9.5543897693967985e-06,5.2957258634098372e-05,9.5453619316289823e-06,5.2809202699622741e-05,1.4833091919156415e-07,1.4833091919153401e-07,0.0027640190818798002,0.0027640190818792386,9.0278377677653913e-09,1.4805593447560385e-07 +10,cable:3:core,cable:3:core,1.3444055602265216e-05,9.5357202577227293e-05,1.344405560226531e-05,9.5357202577227266e-05,1.346444009793527e-05,9.4933039705599208e-05,4.2465241001491889e-07,4.2465241001495054e-07,0.0044288546208002873,0.0044288546208006178,2.0384495670053911e-08,4.2416287162808488e-07 +100,cable:1:core,cable:1:core,0.00010029767252434723,0.00079835032747907573,0.00010029767252434697,0.00079835032747907519,0.00010109173736374871,0.00079398372433502344,4.4382160815824066e-06,4.4382160815828937e-06,0.0055450428979991924,0.0055450428979998004,7.9406483940148661e-07,4.3666031440522859e-06 +100,cable:1:core,cable:2:core,8.9513730825568297e-05,0.00039183825668436418,8.951373082556884e-05,0.00039183825668436429,8.9419166063820077e-05,0.00039105609314453346,7.878593130789853e-07,7.8785931307881262e-07,0.0019640058494340357,0.0019640058494336055,9.4564761748220923e-08,7.8216353983071897e-07 +100,cable:1:core,cable:3:core,8.8905381208391612e-05,0.00030498496831731425,8.8905381208391422e-05,0.00030498496831731561,8.8761745324787293e-05,0.00030479687079552149,2.3666842789824236e-07,2.366684278972804e-07,0.00074551025733565681,0.00074551025733262657,1.4363588360431865e-07,1.8809752179275736e-07 +100,cable:2:core,cable:1:core,8.9513730825568297e-05,0.00039183825668436418,8.951373082556884e-05,0.00039183825668436429,8.9419166063820144e-05,0.00039105609314453335,7.8785931307908483e-07,7.8785931307891214e-07,0.0019640058494342846,0.001964005849433854,9.456476174815316e-08,7.8216353983082739e-07 +100,cable:2:core,cable:2:core,0.00010029767252434672,0.00079835032747907389,0.00010029767252434647,0.00079835032747907334,0.00010111751936580979,0.00079411805013570192,4.3109535319546705e-06,4.3109535319551542e-06,0.0053851244828224459,0.0053851244828230505,8.1984684146306291e-07,4.232277343371965e-06 +100,cable:2:core,cable:3:core,8.9513730825568297e-05,0.00039183825668436418,8.951373082556884e-05,0.00039183825668436429,8.9418021427622023e-05,0.00039104147926472346,8.0250516839758451e-07,8.0250516839741224e-07,0.0020005879183127391,0.0020005879183123097,9.5709397946274594e-08,7.9677741964071782e-07 +100,cable:3:core,cable:1:core,8.8905381208391612e-05,0.00030498496831731425,8.8905381208391422e-05,0.00030498496831731561,8.8761745324787293e-05,0.0003047968707955216,2.3666842789815618e-07,2.3666842789719421e-07,0.00074551025733538522,0.00074551025733235498,1.4363588360431865e-07,1.8809752179264894e-07 +100,cable:3:core,cable:2:core,8.9513730825568297e-05,0.00039183825668436418,8.951373082556884e-05,0.00039183825668436429,8.9418021427621982e-05,0.00039104147926472346,8.0250516839758938e-07,8.0250516839741701e-07,0.0020005879183127516,0.0020005879183123219,9.5709397946315252e-08,7.9677741964071782e-07 +100,cable:3:core,cable:3:core,0.00010029767252434721,0.00079835032747907573,0.00010029767252434695,0.00079835032747907519,0.00010109212945278526,0.00079400305315175887,4.4192709679411133e-06,4.4192709679416004e-06,0.0055212405402839532,0.0055212405402845612,7.94456928438045e-07,4.3472743273168619e-06 +1000,cable:1:core,cable:1:core,0.00078861955828074461,0.0066682243696765085,0.00078861955828074494,0.0066682243696765085,0.00078601699198523078,0.0066168492252231129,5.1441022724376449e-05,5.1441022724376435e-05,0.0077199694890550587,0.007719969489055057,2.6025662955138319e-06,5.1375144453395631e-05 +1000,cable:1:core,cable:2:core,0.00074708526686114655,0.0026749096440145913,0.00074708526686115175,0.0026749096440145926,0.00074588926809238511,0.0026721634166045451,2.9953594179309089e-06,2.9953594179276381e-06,0.0010796764074255197,0.0010796764074243407,1.1959987687614393e-06,2.7462274100461745e-06 +1000,cable:1:core,cable:3:core,0.00071978017636864153,0.0018225752382672376,0.00071978017636863936,0.0018225752382672528,0.00071859077164575073,0.001824605919080821,2.3533695335462284e-06,2.3533695335604217e-06,0.0012000804519261116,0.0012000804519333493,1.1894047228907991e-06,2.0306808135833309e-06 +1000,cable:2:core,cable:1:core,0.00074708526686114655,0.0026749096440145913,0.00074708526686115175,0.0026749096440145926,0.00074588926809238414,0.0026721634166045438,2.9953594179324912e-06,2.9953594179292203e-06,0.0010796764074260908,0.0010796764074249118,1.195998768762415e-06,2.7462274100474755e-06 +1000,cable:2:core,cable:2:core,0.00078861955828031115,0.0066682243696753107,0.00078861955828031147,0.0066682243696753107,0.0007861049543362728,0.0066165057334950745,5.1779731182568038e-05,5.1779731182568024e-05,0.0077711865836618413,0.0077711865836618396,2.5146039440383447e-06,5.1718636180236172e-05 +1000,cable:2:core,cable:3:core,0.00074708526686114666,0.0026749096440145913,0.00074708526686115186,0.0026749096440145926,0.0007458750783723221,0.0026721706045853234,2.9944771118823559e-06,2.9944771118790626e-06,0.0010793571711456673,0.0010793571711444803,1.2101884888245616e-06,2.7390394292678769e-06 +1000,cable:3:core,cable:1:core,0.00071978017636864164,0.0018225752382672379,0.00071978017636863947,0.001822575238267253,0.00071859077164575008,0.0018246059190808221,2.3533695335473604e-06,2.3533695335615537e-06,0.0012000804519266882,0.0012000804519339261,1.1894047228915581e-06,2.0306808135841983e-06 +1000,cable:3:core,cable:2:core,0.00074708526686114655,0.0026749096440145913,0.00074708526686115175,0.0026749096440145926,0.0007458750783723209,0.0026721706045853247,2.9944771118816041e-06,2.9944771118783109e-06,0.001079357171145396,0.0010793571711442088,1.2101884888256458e-06,2.7390394292665758e-06 +1000,cable:3:core,cable:3:core,0.0007886195582807445,0.0066682243696765085,0.00078861955828074483,0.0066682243696765085,0.00078598630833147539,0.0066163157830859678,5.1975334218482814e-05,5.1975334218482801e-05,0.0078007802258625103,0.0078007802258625077,2.6332499492691137e-06,5.1908586590540713e-05 +10000,cable:1:core,cable:1:core,0.0051948061676279642,0.05689636052577024,0.0051948061676279703,0.05689636052577033,0.0051024009974631997,0.056494349006063566,0.00041249482111915353,0.00041249482111906425,0.0072719246745931469,0.0072719246745915735,9.2405170164764489e-05,0.00040201151970667426 +10000,cable:1:core,cable:2:core,0.0048255232285051635,0.017433544047368715,0.0048255232285051653,0.017433544047368715,0.0048010353251144531,0.017380520692016812,5.8404910968574194e-05,5.8404910968573462e-05,0.0032390616548324314,0.0032390616548323907,2.4487903390710448e-05,5.3023355351903417e-05 +10000,cable:1:core,cable:3:core,0.0041592668664023873,0.0096192362424460456,0.0041592668664024281,0.0096192362424460786,0.0041388512938834643,0.0096080024171414403,2.3302240927691969e-05,2.3302240927640361e-05,0.0022274199513152054,0.0022274199513102723,2.0415572518923085e-05,1.1233825304605288e-05 +10000,cable:2:core,cable:1:core,0.0048255232285051635,0.017433544047368715,0.0048255232285051653,0.017433544047368715,0.004801035325114454,0.017380520692016805,5.840491096858013e-05,5.8404910968579398e-05,0.0032390616548327619,0.0032390616548327211,2.4487903390709581e-05,5.3023355351910356e-05 +10000,cable:2:core,cable:2:core,0.0051948061673494214,0.056896360525121779,0.0051948061673494274,0.05689636052512187,0.0051037076429676729,0.056498447347698745,0.00040820808163423238,0.0004082080816341431,0.0071958204857737705,0.0071958204857721971,9.1098524381748487e-05,0.00039791317742303434 +10000,cable:2:core,cable:3:core,0.0048255232285051635,0.017433544047368715,0.0048255232285051653,0.017433544047368715,0.0048009326760847524,0.017380882154370954,5.8120308347846536e-05,5.8120308347845804e-05,0.0032232205619593434,0.003223220561959303,2.459055242041118e-05,5.2661892997761528e-05 +10000,cable:3:core,cable:1:core,0.0041592668664023873,0.0096192362424460456,0.0041592668664024281,0.0096192362424460786,0.0041388512938834634,0.0096080024171414386,2.3302240927693565e-05,2.3302240927641957e-05,0.0022274199513153581,0.002227419951310425,2.0415572518923952e-05,1.1233825304607023e-05 +10000,cable:3:core,cable:2:core,0.0048255232285051635,0.017433544047368715,0.0048255232285051653,0.017433544047368715,0.0048009326760847567,0.017380882154370954,5.8120308347844707e-05,5.8120308347843968e-05,0.0032232205619592415,0.0032232205619592003,2.4590552420406843e-05,5.2661892997761528e-05 +10000,cable:3:core,cable:3:core,0.0051948061676279642,0.05689636052577024,0.0051948061676279703,0.05689636052577033,0.0051020282511443343,0.05649251949500398,0.00041436133979592309,0.0004143613397958338,0.0073050687451673936,0.0073050687451658193,9.2777916483629923e-05,0.0004038410307662596 +100000,cable:1:core,cable:1:core,0.025031226245057811,0.51501980111689405,0.025031226245057804,0.51501980111689405,0.024707771051829367,0.51187134433942161,0.0031650281739091352,0.0031650281739091361,0.0061760586330841874,0.0061760586330841891,0.00032345519322844393,0.0031484567774724415 +100000,cable:1:core,cable:2:core,0.021596902160099213,0.12625600561257661,0.021596902160095976,0.12625600561268494,0.021555696037850045,0.12588810726341756,0.00037019878430946077,0.00037019878420216422,0.0028985127206438191,0.0028985127198037286,4.1206122249167615e-05,0.00036789834915904041 +100000,cable:1:core,cable:3:core,0.01554830730155091,0.059605199227129396,0.015548307301550915,0.059605199227129396,0.015531651186217865,0.059558666775737366,4.9423630082577968e-05,4.9423630082576213e-05,0.00080297659635237694,0.00080297659635234842,1.6656115333045507e-05,4.6532451392029828e-05 +100000,cable:2:core,cable:1:core,0.021596902160099216,0.12625600561257661,0.021596902160095979,0.12625600561268494,0.021555696037850031,0.12588810726341754,0.00037019878430949026,0.00037019878420219371,0.0028985127206440503,0.0028985127198039601,4.1206122249184962e-05,0.00036789834915906816 +100000,cable:2:core,cable:2:core,0.02503122613141193,0.51501980073292042,0.025031226131411923,0.51501980073292042,0.02469664297746775,0.5119012048567243,0.0031364926471985645,0.0031364926471985654,0.0061200262228040874,0.0061200262228040892,0.00033458315394417973,0.0031185958761961174 +100000,cable:2:core,cable:3:core,0.021596902160099213,0.12625600561257661,0.021596902160095976,0.12625600561268494,0.021556266821685061,0.12587112637670345,0.00038701841948086171,0.00038701841937347034,0.0030305987949418313,0.003030598794100889,4.0635338414151539e-05,0.00038487923587315898 +100000,cable:3:core,cable:1:core,0.015548307301550912,0.059605199227129396,0.015548307301550917,0.059605199227129396,0.015531651186217852,0.059558666775737415,4.9423630082536911e-05,4.9423630082535156e-05,0.00080297659635170928,0.00080297659635168077,1.6656115333059385e-05,4.6532451391981255e-05 +100000,cable:3:core,cable:2:core,0.021596902160099213,0.12625600561257661,0.021596902160095976,0.12625600561268494,0.021556266821685061,0.12587112637670345,0.00038701841948086171,0.00038701841937347034,0.0030305987949418313,0.003030598794100889,4.0635338414151539e-05,0.00038487923587315898 +100000,cable:3:core,cable:3:core,0.025031226245057811,0.51501980111689405,0.025031226245057804,0.51501980111689405,0.024708832082088631,0.51184523252392566,0.003190897013032208,0.0031908970130322088,0.0062268539233451305,0.0062268539233451322,0.00032239416296917997,0.0031745685929683853 +1000000,cable:1:core,cable:1:core,0.094696040918173865,4.9430135931550625,0.094696040918173796,4.9430135931550625,0.092973237518383781,4.9153612050697939,0.027706003291211687,0.02770600329121169,0.0056356078961270102,0.0056356078961270111,0.0017228033997900849,0.027652388085268598 +1000000,cable:1:core,cable:2:core,0.077567018860665804,1.0935618501004387,0.077567018860665735,1.0935618501004389,0.076735943913257512,1.092058127543065,0.0017180998509297644,0.0017180998509296035,0.001569398093823283,0.001569398093823136,0.0008310749474082918,0.0015037225573737434 +1000000,cable:1:core,cable:3:core,0.051247474729972854,0.48773084535257633,0.051247474729972764,0.48773084535257655,0.05049478438383373,0.48942360593674289,0.0018525607014287209,0.0018525607014289603,0.0037652024876764761,0.0037652024876769627,0.00075269034613912461,0.0016927605841665638 +1000000,cable:2:core,cable:1:core,0.077567018860665804,1.0935618501004385,0.077567018860665735,1.0935618501004387,0.07673594391325754,1.0920581275430643,0.0017180998509301396,0.0017180998509299789,0.0015693980938236267,0.0015693980938234799,0.00083107494740826404,0.0015037225573741875 +1000000,cable:2:core,cable:2:core,0.094696003143432794,4.9430132917981755,0.094696003143432725,4.9430132917981755,0.092901264398151592,4.9160910746438828,0.026981972938736398,0.026981972938736401,0.0054875216991341746,0.0054875216991341754,0.0017947387452812019,0.02692221715429266 +1000000,cable:2:core,cable:3:core,0.077567018860665818,1.0935618501004387,0.077567018860665748,1.0935618501004389,0.076735621529719203,1.092127905866533,0.0016575335857403374,0.0016575335857401802,0.0015139776156323829,0.0015139776156322393,0.00083139733094661494,0.0014339442339057484 +1000000,cable:3:core,cable:1:core,0.051247474729972854,0.48773084535257627,0.051247474729972764,0.48773084535257649,0.050494784383833737,0.48942360593674267,0.0018525607014285659,0.0018525607014288053,0.003765202487676163,0.0037652024876766495,0.00075269034613911767,0.0016927605841663973 +1000000,cable:3:core,cable:2:core,0.07756701886066579,1.0935618501004387,0.077567018860665721,1.0935618501004389,0.076735621529719272,1.0921279058665325,0.0016575335857406729,0.0016575335857405156,0.0015139776156326899,0.0015139776156325461,0.0008313973309465178,0.0014339442339061925 +1000000,cable:3:core,cable:3:core,0.094696040918173865,4.9430135931550634,0.094696040918173796,4.9430135931550634,0.092968258794488681,4.9150976317878659,0.027969378436457814,0.027969378436457817,0.0056894854502846327,0.0056894854502846327,0.0017277821236851842,0.027915961367197539 +10000000,cable:1:core,cable:1:core,1.251332219253861,49.783940499020225,1.2513322192538612,49.783940499020225,0.53121976717410324,51.365101859919996,1.7374214206238898,1.7374214206238898,0.033823129904337439,0.033823129904337439,0.72011245207975771,1.5811613608997703 +10000000,cable:1:core,cable:2:core,1.180799668295279,11.256469738395747,1.180799668295279,11.256469738395747,0.46679343008479252,13.069443957199377,1.9485072302278281,1.9485072302278281,0.14899376034263651,0.14899376034263651,0.71400623821048659,1.81297421880363 +10000000,cable:1:core,cable:3:core,1.0678706383895686,5.1747359052981334,1.0678706383895686,5.1747359052981334,0.36536213202748608,6.9748137137964967,1.9323038886674515,1.9323038886674515,0.27666089794727172,0.27666089794727172,0.70250850636208262,1.8000778084983633 +10000000,cable:2:core,cable:1:core,1.180799668295279,11.256469738395744,1.180799668295279,11.256469738395744,0.46679343008479313,13.069443957199393,1.9485072302278461,1.9485072302278461,0.1489937603426377,0.1489937603426377,0.71400623821048592,1.8129742188036495 +10000000,cable:2:core,cable:2:core,1.2512831793321839,49.783625781844535,1.2512831793321841,49.783625781844535,0.52795194458770356,51.326274764561461,1.7038115972825583,1.703811597282558,0.033193943564225004,0.033193943564225004,0.72333123474448036,1.5426489827169263 +10000000,cable:2:core,cable:3:core,1.1807996682952793,11.256469738395747,1.1807996682952793,11.256469738395747,0.46209227148974574,13.00760233714735,1.8928829072696307,1.8928829072696307,0.14542953948724663,0.14542953948724663,0.71870739680553353,1.7511325987516031 +10000000,cable:3:core,cable:1:core,1.0678706383895684,5.1747359052981334,1.0678706383895684,5.1747359052981334,0.36536213202748624,6.9748137137965012,1.9323038886674555,1.9323038886674555,0.27666089794727211,0.27666089794727211,0.70250850636208217,1.8000778084983677 +10000000,cable:3:core,cable:2:core,1.180799668295279,11.256469738395745,1.180799668295279,11.256469738395745,0.46209227148974569,13.007602337147338,1.8928829072696207,1.8928829072696207,0.14542953948724599,0.14542953948724599,0.71870739680553331,1.7511325987515924 +10000000,cable:3:core,cable:3:core,1.2513322192538612,49.783940499020247,1.2513322192538614,49.783940499020247,0.52185045273660624,51.239123582658166,1.6277903595325474,1.6277903595325471,0.031766857627179998,0.031766857627179998,0.72948176651725494,1.4551830836379196 diff --git a/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/all_earth-Y.csv b/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/all_earth-Y.csv new file mode 100644 index 000000000..c540d834c --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/all_earth-Y.csv @@ -0,0 +1,82 @@ +frequency_hz,receiver,source,new_real,new_imaginary,old_real,old_imaginary,fem_real,fem_imaginary,before_absolute_error,after_absolute_error,before_relative_error,after_relative_error,real_absolute_error,imaginary_absolute_error +0.10000000000000001,cable:1:core,cable:1:core,11.716539656200831,0.090920014937197369,11.716539655942917,0.090920015180965472,11.793927042362855,0.09163136261981264,0.0773906557155805,0.07739065545991744,0.0065617092401915635,0.0065617092185147,0.07738738616202312,0.00071134768261527093 +0.10000000000000001,cable:1:core,cable:2:core,-6.5764429132332642,0.064998853118795732,-6.5764429134272966,0.064998853290850048,-6.6206073939719623,0.065515807071277354,0.044167505962995623,0.044167506159028466,0.0066708899844850457,0.0066708900140930909,0.044164480738698053,0.0005169539524816219 +0.10000000000000001,cable:1:core,cable:3:core,-3.2506966806898179,0.090585504122632499,-3.2506966801752673,0.090585503665267256,-3.2760387858376196,0.091292028100808498,0.025351952512180406,0.025351951985083492,0.0077355966791449477,0.0077355965183127937,0.025342105147801774,0.0007065239781759991 +0.10000000000000001,cable:2:core,cable:1:core,-6.5764429126890791,0.06499886041922423,-6.5764429128831035,0.064998860591277005,-6.6206073939719552,0.065515807071276772,0.044167506421697915,0.044167506617722722,0.0066708900537656799,0.0066708900833725125,0.044164481282876089,0.0005169466520525412 +0.10000000000000001,cable:2:core,cable:2:core,14.505974265569115,0.046571869286876094,14.505974265861992,0.046571869010316257,14.599945452367439,0.046947359746437323,0.093971936696731331,0.093971936988500759,0.0064364247916477895,0.0064364248116319687,0.093971186798324169,0.00037549045956122851 +0.10000000000000001,cable:2:core,cable:3:core,-6.5764429126890844,0.064998860419225785,-6.5764429128831114,0.064998860591280475,-6.6204481639483124,0.065512170527795427,0.044008244777567443,0.044008244973588094,0.0066469956736024327,0.0066469957032093486,0.04400525125922794,0.00051331010856964232 +0.10000000000000001,cable:3:core,cable:1:core,-3.2506966806898188,0.09058550412263261,-3.2506966801752739,0.090585503665269226,-3.2760387858376205,0.091292028100809677,0.025351952512174612,0.025351951985083523,0.0077355966791431783,0.0077355965183128006,0.025342105147801774,0.00070652397817706769 +0.10000000000000001,cable:3:core,cable:2:core,-6.5764429132332678,0.064998853118797009,-6.5764429134272895,0.064998853290850103,-6.6204481639483124,0.065512170527793637,0.04400824431857886,0.044008244514594196,0.0066469956042768969,0.0066469956338830105,0.044005250715044575,0.00051331740899662748 +0.10000000000000001,cable:3:core,cable:3:core,11.716539656200839,0.09092001493719605,11.716539655942919,0.090920015180963779,11.793670814855151,0.091621161120561595,0.07713434566243918,0.077134345406745755,0.0065401196423399343,0.0065401196206600247,0.077131158654312415,0.00070114618336554468 +1,cable:1:core,cable:1:core,11.873714513026494,0.13877147446534627,11.873714513026528,0.13877147446532462,11.951697089000811,0.13971411387969557,0.077988272994409066,0.077988272994442553,0.0065248428124142976,0.0065248428124170992,0.077982575974317569,0.00094263941434929999 +1,cable:1:core,cable:2:core,-6.4640237929904476,0.098479018213251987,-6.4640237929904574,0.098479018213286862,-6.5080852722992288,0.09917856437094108,0.044067032163557202,0.044067032163567527,0.0067703347820267275,0.0067703347820283139,0.044061479308781237,0.00069954615768909323 +1,cable:1:core,cable:3:core,-3.0939192122952939,0.13607439977274632,-3.0939192122953183,0.13607439977272814,-3.119361002943668,0.13702747250048025,0.025459635877577678,0.025459635877601406,0.008153947858134657,0.0081539478581422568,0.025441790648374152,0.00095307272773392793 +1,cable:2:core,cable:1:core,-6.4640237850781093,0.098479081846017405,-6.464023785078119,0.098479081846053071,-6.5080852722992155,0.099178564370941497,0.044067039064787109,0.044067039064797441,0.0067703358423122787,0.0067703358423138659,0.044061487221106255,0.00069948252492409158 +1,cable:2:core,cable:2:core,14.586483697135645,0.070755812768748191,14.586483697135661,0.070755812768684048,14.681691188828179,0.071273811947358101,0.095208900831434906,0.095208900831450533,0.006484796453189415,0.0064847964531904792,0.095207491692534063,0.00051799917860990974 +1,cable:2:core,cable:3:core,-6.4640237850781084,0.098479081846017516,-6.4640237850781137,0.098479081846052835,-6.5086850876535216,0.099186062512871348,0.044666897915518956,0.044666897915524841,0.006861864072795717,0.0068618640727966208,0.044661302575413231,0.00070698066685383198 +1,cable:3:core,cable:1:core,-3.0939192122952925,0.13607439977274649,-3.0939192122953187,0.13607439977272792,-3.1193610029436729,0.13702747250047986,0.025459635877582109,0.025459635877607596,0.0081539478581360639,0.0081539478581442257,0.025441790648380369,0.00095307272773337282 +1,cable:3:core,cable:2:core,-6.4640237929904476,0.098479018213251418,-6.4640237929904538,0.098479018213287278,-6.5086850876535101,0.099186062512871709,0.044666891011373409,0.044666891011380189,0.0068618630121599893,0.006861863012161031,0.044661294663062456,0.00070704429962029081 +1,cable:3:core,cable:3:core,11.873714513026496,0.13877147446534671,11.873714513026522,0.13877147446532426,11.952479429588399,0.1397353161481425,0.078770813578310409,0.078770813578336776,0.0065898821665799062,0.0065898821665821127,0.07876491656190332,0.00096384168279578519 +10,cable:1:core,cable:1:core,12.120029298528808,0.24051657223886777,12.120029298528864,0.24051657223890441,12.198656471407986,0.24185580199235357,0.078638577373295687,0.078638577373353141,0.0064452283085030104,0.0064452283085077193,0.078627172879178531,0.0013392297534857989 +10,cable:1:core,cable:2:core,-6.2890199013663937,0.16552322577944362,-6.2890199013664025,0.16552322577943707,-6.3332980379686301,0.16650732013666913,0.044289071142545544,0.04428907114255428,0.0069906346418380999,0.006990634641839479,0.044278136602236451,0.00098409435722551786 +10,cable:1:core,cable:3:core,-2.8515486558341041,0.22001574158962375,-2.8515486558341587,0.22001574158959175,-2.8767392780483174,0.22134516984312108,0.02522567792979501,0.025225677929847871,0.0087430018601509681,0.0087430018601692902,0.02519062221421331,0.0013294282534973278 +10,cable:2:core,cable:1:core,-6.2890197833970021,0.16552372276044355,-6.2890197833970136,0.16552372276043659,-6.3332980379686363,0.16650732013666858,0.044289178042812648,0.044289178042824034,0.0069906515150912714,0.0069906515150930694,0.044278254571634257,0.00098359737622502608 +10,cable:2:core,cable:2:core,14.711826696535155,0.12082715757884763,14.711826696535178,0.12082715757887073,14.807136000816893,0.12155291122243583,0.09531206744696942,0.095312067446992679,0.0064366840498130988,0.0064366840498146696,0.095309304281737894,0.00072575364358820615 +10,cable:2:core,cable:3:core,-6.2890197833970056,0.16552372276044061,-6.289019783397011,0.16552372276043406,-6.333049865123205,0.16652237292191072,0.044041405506186383,0.044041405506191567,0.0069518146186880779,0.0069518146186888959,0.044030081726199377,0.00099865016147010488 +10,cable:3:core,cable:1:core,-2.8515486558341059,0.22001574158962234,-2.8515486558341592,0.22001574158959078,-2.8767392780483072,0.22134516984312255,0.025225677929784498,0.02522567792983605,0.0087430018601473564,0.008743001860165224,0.025190622214201319,0.0013294282535002144 +10,cable:3:core,cable:2:core,-6.2890199013663901,0.16552322577944287,-6.2890199013663972,0.16552322577943612,-6.3330498651232174,0.16652237292191016,0.044041298839148882,0.044041298839155828,0.0069517977815898354,0.0069517977815909317,0.044029963756827328,0.00099914714246729375 +10,cable:3:core,cable:3:core,12.120029298528806,0.24051657223886974,12.120029298528857,0.24051657223890555,12.198600133535841,0.24189549926584741,0.078582934237913696,0.078582934237965821,0.0064406971080819796,0.0064406971080862522,0.0785708350070351,0.0013789270269776721 +100,cable:1:core,cable:1:core,12.563025703739259,0.51721927479150309,12.563025703739317,0.51721927479147634,12.641497780100403,0.51955597655167107,0.078506859213295546,0.078506859213353347,0.0062050114328727904,0.0062050114328773588,0.078472076361144616,0.0023367017601679807 +100,cable:1:core,cable:2:core,-5.98500069543738,0.32573071565504808,-5.9850006954373791,0.32573071565508882,-6.0295953731137866,0.32733885698691378,0.044623664076426955,0.044623664076427531,0.0073898906143090182,0.0073898906143091136,0.044594677676406569,0.001608141331865709 +100,cable:1:core,cable:3:core,-2.446514278290135,0.37618640379323293,-2.4465142782902065,0.37618640379320623,-2.470174538771686,0.37813339888204062,0.02374023411694599,0.023740234117015056,0.0095000867116944322,0.0095000867117220698,0.02366026048155101,0.0019469950888076903 +100,cable:2:core,cable:1:core,-5.9849990645039277,0.32573357082851701,-5.9849990645039268,0.32573357082855753,-6.0295953731137963,0.32733885698691145,0.044625191151152786,0.044625191151153355,0.0073901435051331316,0.0073901435051332252,0.044596308609868629,0.0016052861583944478 +100,cable:2:core,cable:2:core,14.9298902985444,0.25587575596028456,14.929890298544413,0.25587575596022161,15.026860913558655,0.25703830837642216,0.096977583514753135,0.096977583514764806,0.0064526716430934467,0.0064526716430942238,0.096970615014255301,0.0011625524161376077 +100,cable:2:core,cable:3:core,-5.9849990645039286,0.32573357082851723,-5.9849990645039286,0.32573357082855769,-6.0293971898343495,0.32733839277942345,0.044427119942100003,0.04442711994210146,0.0073575831247469888,0.0073575831247472299,0.044398125330420868,0.0016048219509062212 +100,cable:3:core,cable:1:core,-2.4465142782901381,0.37618640379323198,-2.4465142782902083,0.37618640379320556,-2.4701745387716816,0.37813339888204167,0.023740234116939936,0.023740234117007698,0.0095000867116920261,0.0095000867117191433,0.02366026048154346,0.0019469950888096887 +100,cable:3:core,cable:2:core,-5.98500069543738,0.3257307156550488,-5.9850006954373818,0.32573071565508926,-6.029397189834353,0.32733839277942295,0.044425593304720248,0.044425593304723489,0.007357330298062798,0.0073573302980633349,0.044396494396973019,0.0016076771243741517 +100,cable:3:core,cable:3:core,12.563025703739264,0.51721927479150287,12.563025703739322,0.51721927479147711,12.641296501223044,0.51953417178615768,0.078305022104803307,0.078305022104861149,0.0061891574827913041,0.006189157482795876,0.078270797483780186,0.0023148969946548137 +1000,cable:1:core,cable:1:core,13.595998739240526,1.4465325051151612,13.595998739240571,1.4465325051151292,13.670105690820352,1.4521245610642923,0.07431763829796087,0.074317638298002753,0.0054060922772405417,0.0054060922772435878,0.074106951579825164,0.0055920559491311472 +1000,cable:1:core,cable:2:core,-5.3532086996206294,0.79705038235169645,-5.3532086996206329,0.79705038235174319,-5.3950324845310735,0.80018548575660087,0.04194112370447349,0.041941123704480526,0.0076899031008897676,0.0076899031008910582,0.041823784910444139,0.0031351034049044157 +1000,cable:1:core,cable:3:core,-1.7399318837813844,0.63681540275493598,-1.7399318837814481,0.63681540275489368,-1.7593388574871698,0.6398182528281865,0.019637915800238218,0.019637915800294729,0.010489955575026549,0.010489955575056735,0.019406973705785369,0.0030028500732505181 +1000,cable:2:core,cable:1:core,-5.3531955340018786,0.79705183192885243,-5.3531955340018831,0.79705183192889983,-5.3950324845310762,0.80018548575660153,0.041954144204047611,0.041954144204055584,0.0076922904088874153,0.0076922904088888768,0.041836950529197559,0.0031336538277491055 +1000,cable:2:core,cable:2:core,15.394199476046905,0.79008767845702987,15.394199476046927,0.79008767845696459,15.487580066580868,0.79211282136113192,0.093402547568307542,0.093402547568327429,0.0060229309186851498,0.0060229309186864326,0.093380590533962859,0.0020251429041020508 +1000,cable:2:core,cable:3:core,-5.3531955340018795,0.79705183192885209,-5.3531955340018857,0.7970518319289005,-5.3953801447260359,0.80012652115813576,0.042296513991160765,0.042296513991170487,0.0077545872364662878,0.0077545872364680702,0.042184610724156357,0.0030746892292836669 +1000,cable:3:core,cable:1:core,-1.7399318837813844,0.63681540275493553,-1.7399318837814481,0.6368154027548929,-1.7593388574871671,0.63981825282818849,0.019637915800236008,0.019637915800292467,0.010489955575025378,0.010489955575055537,0.019406973705782704,0.0030028500732529606 +1000,cable:3:core,cable:2:core,-5.3532086996206267,0.79705038235169601,-5.3532086996206329,0.79705038235174441,-5.395380144726043,0.80012652115813565,0.04228348864787293,0.042283488647882651,0.0077521991871619886,0.007752199187163771,0.042171445105416261,0.0030761388064396433 +1000,cable:3:core,cable:3:core,13.595998739240521,1.4465325051151596,13.595998739240573,1.446532505115127,13.670305930691081,1.4519094313608261,0.074501476744519221,0.074501476744568251,0.0054193957081953353,0.0054193957081989019,0.074307191450559529,0.0053769262456664357 +10000,cable:1:core,cable:1:core,17.046534552968179,4.2120541635759645,17.04653455296819,4.2120541635757602,17.128574149133918,4.2360094951678624,0.085465509129258405,0.085465509129211373,0.0048437210339593193,0.0048437210339566539,0.082039596165738971,0.023955331591897888 +10000,cable:1:core,cable:2:core,-3.3892442630089872,2.0891967225415851,-3.3892442630087292,2.0891967225412542,-3.4249228006495258,2.1014698251850854,0.037730453173686632,0.037730453173335024,0.0093897903233363857,0.0093897903232488828,0.035678537640538632,0.012273102643500344 +10000,cable:1:core,cable:3:core,-0.43825470996742211,0.85392294859442852,-0.43825470996925953,0.85392294859473339,-0.44874265125902446,0.86232080591781557,0.013435807386100814,0.013435807387725649,0.01382150757153951,0.013821507573210991,0.010487941291602354,0.0083978573233870479 +10000,cable:2:core,cable:1:core,-3.389283429007929,2.0891704641805133,-3.3892834290076714,2.0891704641801838,-3.4249228006495258,2.1014698251850819,0.037701977297683216,0.03770197729733224,0.0093827036736289348,0.0093827036735415897,0.035639371641596806,0.012299361004568521 +10000,cable:2:core,cable:2:core,17.284752427180987,3.3655305049302604,17.284752427181022,3.3655305049304496,17.377466344145962,3.3763680984739541,0.093345186446843004,0.09334518644690025,0.0052730139931134231,0.0052730139931166566,0.092713916964974885,0.010837593543693735 +10000,cable:2:core,cable:3:core,-3.3892834290079294,2.0891704641805124,-3.3892834290076705,2.0891704641801834,-3.4248494411361543,2.1016072413487854,0.037677773886801151,0.037677773886448135,0.0093766584919387306,0.0093766584918508773,0.035566012128224855,0.012436777168272961 +10000,cable:3:core,cable:1:core,-0.43825470996742194,0.85392294859442863,-0.43825470996925953,0.85392294859473372,-0.44874265125902463,0.86232080591781735,0.013435807386101846,0.013435807387726951,0.013821507571540549,0.013821507573212306,0.010487941291602687,0.0083978573233887133 +10000,cable:3:core,cable:2:core,-3.3892442630089881,2.0891967225415837,-3.3892442630087296,2.0891967225412542,-3.4248494411361552,2.1016072413487867,0.037706096145820024,0.037706096145467508,0.0093837068953645277,0.0093837068952767992,0.035605178127167125,0.012410518807203008 +10000,cable:3:core,cable:3:core,17.046534552968179,4.2120541635759663,17.04653455296819,4.2120541635757611,17.128908606191871,4.2364262897459461,0.085903929936605811,0.085903929936557821,0.0048684511517558373,0.0048684511517531172,0.082374053223691845,0.024372126169979857 +100000,cable:1:core,cable:1:core,25.114009604140684,8.0616329121337866,25.114009604140684,8.0616329121337866,25.211046058879496,8.1198439722222862,0.11315741718908486,0.11315741718908486,0.0042722858396769137,0.0042722858396769137,0.097036454738812949,0.058211060088499522 +100000,cable:1:core,cable:2:core,0.17423938863795488,1.0511044112983252,0.17423938863795493,1.051104411298325,0.17447040684075316,1.0607706304717672,0.0096689793938681268,0.0096689793938679047,0.0089942077693072487,0.0089942077693070423,0.00023101820279827767,0.0096662191734420144 +100000,cable:1:core,cable:3:core,0.049169280797893136,-0.045706655302344225,0.049169280797892866,-0.045706655302347673,0.051001167620121454,-0.045409721143611068,0.0018557961159780922,0.0018557961159772732,0.027176280454428874,0.02717628045441688,0.001831886822228318,0.00029693415873315726 +100000,cable:2:core,cable:1:core,0.17424088729365148,1.0510922187905325,0.17424088729365159,1.0510922187905325,0.17447040684075332,1.0607706304717672,0.0096811327794820955,0.0096811327794820972,0.0090055130033820947,0.0090055130033820964,0.00022951954710184741,0.0096784116812347332 +100000,cable:2:core,cable:2:core,25.080124615561157,8.0878898568648587,25.080124615561157,8.0878898568648587,25.173967497452402,8.1446398751713485,0.10966791262461896,0.10966791262461896,0.0041448686019930599,0.0041448686019930599,0.0938428818912449,0.056750018306489736 +100000,cable:2:core,cable:3:core,0.17424088729365148,1.0510922187905325,0.17424088729365153,1.0510922187905325,0.17443042293000227,1.0608281024344142,0.0097377283842100225,0.0097377283842100225,0.0090577358045038203,0.0090577358045038203,0.00018953563635079607,0.0097358836438816709 +100000,cable:3:core,cable:1:core,0.04916928079789315,-0.045706655302344218,0.049169280797892859,-0.045706655302347653,0.051001167620121447,-0.045409721143611116,0.0018557961159780811,0.0018557961159772437,0.0271762804544287,0.027176280454416436,0.0018318868222282972,0.00029693415873310175 +100000,cable:3:core,cable:2:core,0.17423938863795488,1.051104411298325,0.17423938863795507,1.051104411298325,0.17443042293000166,1.0608281024344137,0.009725567510988152,0.0097255675109881555,0.0090464241338092111,0.0090464241338092145,0.00019103429204678224,0.0097236911360887301 +100000,cable:3:core,cable:3:core,25.114009604140684,8.0616329121337849,25.114009604140684,8.0616329121337849,25.212261890133007,8.1202583933699799,0.1144135426988117,0.1144135426988117,0.00431950170976351,0.00431950170976351,0.098252285992323607,0.058625481236195043 +1000000,cable:1:core,cable:1:core,40.833774626185878,19.549173406842751,40.833774626185878,19.549173406842751,40.899981093321514,19.656825006121288,0.12638102356686592,0.12638102356686592,0.002785048006455541,0.002785048006455541,0.06620646713563616,0.10765159927853674 +1000000,cable:1:core,cable:2:core,-0.020403824114795528,-0.018991043972656708,-0.020403824114795521,-0.018991043972656708,-0.020561862633581049,-0.019100623519793053,0.0001923118575921154,0.00019231185759210971,0.0068524612517472088,0.0068524612517470058,0.00015803851878552072,0.00010957954713634518 +1000000,cable:1:core,cable:3:core,-1.7943027849737e-05,-1.177264572726311e-05,-1.7943027849736949e-05,-1.1772645727263113e-05,-1.6683076928938046e-05,-1.5016074300379346e-05,3.4795553209754845e-06,3.4795553209755061e-06,0.15502131367650746,0.15502131367650843,1.2599509207989541e-06,3.2434285731162361e-06 +1000000,cable:2:core,cable:1:core,-0.020403824161670955,-0.018991043990879461,-0.020403824161670955,-0.018991043990879461,-0.020561862633581056,-0.019100623519793056,0.0001923118086873636,0.0001923118086873636,0.0068524595091717828,0.0068524595091717828,0.00015803847191010051,0.00010957952891359549 +1000000,cable:2:core,cable:2:core,40.833783909159962,19.549186896797089,40.833783909159962,19.549186896797089,40.89643056210452,19.651398854301153,0.11988280686557391,0.11988280686557391,0.0026421705289196174,0.0026421705289196174,0.062646652944557957,0.10221195750406409 +1000000,cable:2:core,cable:3:core,-0.020403824161670955,-0.018991043990879461,-0.020403824161670952,-0.018991043990879461,-0.02056147587072071,-0.019100875567408322,0.0001921380664239248,0.00019213806642392195,0.0068462959984796025,0.006846295998479501,0.00015765170904975465,0.00010983157652886058 +1000000,cable:3:core,cable:1:core,-1.7943027849736996e-05,-1.1772645727263103e-05,-1.7943027849736942e-05,-1.1772645727263105e-05,-1.6683076928938059e-05,-1.5016074300379334e-05,3.4795553209754739e-06,3.4795553209754951e-06,0.15502131367650696,0.1550213136765079,1.2599509207989372e-06,3.2434285731162311e-06 +1000000,cable:3:core,cable:2:core,-0.020403824114795518,-0.018991043972656704,-0.020403824114795518,-0.018991043972656704,-0.0205614758707207,-0.019100875567408311,0.00019213811530244143,0.00019213811530244143,0.0068462977401271569,0.0068462977401271569,0.0001576517559251818,0.00010983159475160681 +1000000,cable:3:core,cable:3:core,40.833774626185878,19.549173406842755,40.833774626185878,19.549173406842755,40.898365654929883,19.65540082251125,0.12432322724984333,0.12432322724984333,0.0027398256795864925,0.0027398256795864925,0.064591028744004575,0.10622741566849569 +10000000,cable:1:core,cable:1:core,80.846531351828517,55.108195401013525,80.846531351828517,55.108195401013525,80.823352857111601,55.216286708477909,0.11054851137261967,0.11054851137261967,0.0011293843148707356,0.0011293843148707356,0.02317849471691602,0.10809130746438456 +10000000,cable:1:core,cable:2:core,-9.26641813988093e-08,-1.3479772233800525e-07,-9.26641813988093e-08,-1.3479772233800525e-07,-2.9631405210173127e-07,-1.886966884946216e-07,2.1066173926479604e-07,2.1066173926479604e-07,0.59967121560503911,0.59967121560503911,2.0364987070292197e-07,5.3898966156616346e-08 +10000000,cable:1:core,cable:3:core,-1.8002386369702588e-16,1.9542236956564906e-16,-1.8002386369701979e-16,1.9542236956570295e-16,-1.0854589216983598e-15,6.4134107784461141e-16,1.0092849640468347e-15,1.0092849640468531e-15,NaN,NaN,9.05435058001334e-16,4.4591870827896237e-16 +10000000,cable:2:core,cable:1:core,-9.2664181398809313e-08,-1.3479772233800525e-07,-9.2664181398809313e-08,-1.3479772233800525e-07,-2.9631405210173127e-07,-1.8869668849462157e-07,2.1066173926479602e-07,2.1066173926479602e-07,0.59967121560503911,0.59967121560503911,2.0364987070292194e-07,5.389896615661632e-08 +10000000,cable:2:core,cable:2:core,80.846531351828517,55.108195401013525,80.846531351828517,55.108195401013525,80.822369854298401,55.218371261575591,0.11279405220794937,0.11279405220794937,0.00115232089289832,0.00115232089289832,0.024161497530116094,0.11017586056206596 +10000000,cable:2:core,cable:3:core,-9.26641813988093e-08,-1.3479772233800525e-07,-9.26641813988093e-08,-1.3479772233800525e-07,-3.537852634471945e-07,-2.3598687919811782e-07,2.8004189857265998e-07,2.8004189857265998e-07,0.65850514179037156,0.65850514179037156,2.6112108204838523e-07,1.0118915686011257e-07 +10000000,cable:3:core,cable:1:core,-1.8002386369702576e-16,1.9542236956564906e-16,-1.8002386369701972e-16,1.9542236956570285e-16,-1.0854589216983612e-15,6.4134107784461368e-16,1.0092849640468369e-15,1.0092849640468553e-15,NaN,NaN,9.0543505800133538e-16,4.4591870827896464e-16 +10000000,cable:3:core,cable:2:core,-9.2664181398809313e-08,-1.3479772233800525e-07,-9.2664181398809313e-08,-1.3479772233800525e-07,-3.5378526344719471e-07,-2.359868791981179e-07,2.800418985726602e-07,2.800418985726602e-07,0.65850514179037167,0.65850514179037167,2.6112108204838539e-07,1.0118915686011265e-07 +10000000,cable:3:core,cable:3:core,80.846531351828517,55.108195401013525,80.846531351828517,55.108195401013525,80.820947397233525,55.219522376005109,0.11422886716367056,0.11422886716367056,0.0011669854375752117,0.0011669854375752117,0.02558395459499252,0.11132697499158439 diff --git a/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/all_earth-Z.csv b/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/all_earth-Z.csv new file mode 100644 index 000000000..7e79e1e97 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/all_earth-Z.csv @@ -0,0 +1,82 @@ +frequency_hz,receiver,source,new_real,new_imaginary,old_real,old_imaginary,fem_real,fem_imaginary,before_absolute_error,after_absolute_error,before_relative_error,after_relative_error,real_absolute_error,imaginary_absolute_error +0.10000000000000001,cable:1:core,cable:1:core,3.1374615215203498e-06,1.2435841298035039e-06,3.1374615215203507e-06,1.2435841298035035e-06,3.1569384527251656e-06,1.2374941953892434e-06,2.0406816271213964e-08,2.0406816271214897e-08,0.0060182561639335946,0.0060182561639338696,1.9476931204815793e-08,6.0899344142604639e-09 +0.10000000000000001,cable:1:core,cable:2:core,9.9023642057029986e-08,8.1529149884539513e-07,9.9023642057030489e-08,8.152914988453947e-07,9.8890086440048917e-08,8.1209797662934007e-07,3.1963136966295769e-09,3.1963136966299794e-09,0.0039070116150096406,0.0039070116150101333,1.3355561698106873e-10,3.1935222160550581e-09 +0.10000000000000001,cable:1:core,cable:3:core,9.9020797266300371e-08,7.2818834855192947e-07,9.9020797253944635e-08,7.2818834853831638e-07,9.8883785305156379e-08,7.2534028253697365e-07,2.8513597146125118e-09,2.8513597288035877e-09,0.0038950365518541905,0.0038950365712395944,1.3701196114399179e-10,2.8480660149558238e-09 +0.10000000000000001,cable:2:core,cable:1:core,9.9023642057029973e-08,8.1529149884539492e-07,9.9023642057030476e-08,8.1529149884539449e-07,9.8890086440048931e-08,8.1209797662934018e-07,3.1963136966292585e-09,3.1963136966296609e-09,0.0039070116150092512,0.0039070116150097429,1.3355561698104226e-10,3.1935222160547405e-09 +0.10000000000000001,cable:2:core,cable:2:core,3.1374615138914709e-06,1.2435841307826221e-06,3.1374615138914718e-06,1.2435841307826217e-06,3.1569402035335558e-06,1.2378000139614139e-06,2.0319334575079277e-08,2.031933457508021e-08,0.0059922564469127642,0.0059922564469130392,1.947868964208493e-08,5.7841168212081746e-09 +0.10000000000000001,cable:2:core,cable:3:core,9.9023642057029973e-08,8.1529149884539492e-07,9.9023642057030476e-08,8.1529149884539449e-07,9.8890034709988745e-08,8.1210411491669455e-07,3.1901829464966795e-09,3.190182946497082e-09,0.0038994886832207183,0.0038994886832212101,1.3360734704122737e-10,3.1873839287003704e-09 +0.10000000000000001,cable:3:core,cable:1:core,9.9020797266300371e-08,7.2818834855192947e-07,9.9020797253944648e-08,7.2818834853831638e-07,9.8883785305156392e-08,7.2534028253697365e-07,2.8513597146125118e-09,2.8513597288035873e-09,0.0038950365518541905,0.003895036571239594,1.3701196114397855e-10,2.8480660149558238e-09 +0.10000000000000001,cable:3:core,cable:2:core,9.9023642057029986e-08,8.1529149884539502e-07,9.9023642057030489e-08,8.152914988453946e-07,9.8890034709988785e-08,8.1210411491669486e-07,3.190182946496467e-09,3.190182946496869e-09,0.0038994886832204568,0.0038994886832209481,1.336073470412009e-10,3.1873839287001586e-09 +0.10000000000000001,cable:3:core,cable:3:core,3.1374615215203498e-06,1.2435841298035039e-06,3.1374615215203507e-06,1.2435841298035035e-06,3.1569384435367512e-06,1.2375260948397891e-06,2.0397310578959553e-08,2.0397310578960486e-08,0.006015432163948049,0.0060154321639483239,1.9476922016401434e-08,6.0580349637147579e-09 +1,cable:1:core,cable:1:core,4.0462600939189788e-06,1.0981371245585023e-05,4.0462600939189881e-06,1.0981371245585021e-05,4.0645128397490841e-06,1.0929405558502649e-05,5.507808424665062e-08,5.5078084246655305e-08,0.0047233900558140574,0.004723390055814459,1.8252745830105311e-08,5.1965687082374199e-08 +1,cable:1:core,cable:2:core,9.9703279749934732e-07,6.6990063703023516e-06,9.9703279749934944e-07,6.6990063703023541e-06,9.957273254260641e-07,6.6758614958986898e-06,2.3181662332445536e-08,2.3181662332442879e-08,0.0034344673526018263,0.0034344673526014325,1.3054720732832151e-09,2.3144874403661789e-08 +1,cable:1:core,cable:3:core,9.9679142273796299e-07,5.8280015973796128e-06,9.9679142261440609e-07,5.8280015972434844e-06,9.9514743470101773e-07,5.8084827030194123e-06,1.9588004696494286e-08,1.9588004842512277e-08,0.0033238802967687844,0.0033238803215465164,1.6439880369452685e-09,1.951889436020046e-08 +1,cable:2:core,cable:1:core,9.9703279749934753e-07,6.6990063703023532e-06,9.9703279749934965e-07,6.6990063703023558e-06,9.957273254260641e-07,6.6758614958986898e-06,2.318166233244724e-08,2.3181662332444583e-08,0.0034344673526020787,0.0034344673526016849,1.3054720732834268e-09,2.3144874403663483e-08 +1,cable:2:core,cable:2:core,4.0462594747967495e-06,1.0981371344302105e-05,4.0462594747967589e-06,1.09813713443021e-05,4.0646859849575193e-06,1.0931903537134415e-05,5.2788258378960634e-08,5.2788258378968647e-08,0.0045260868022904861,0.0045260868022911731,1.8426510160769745e-08,4.9467807167690413e-08 +1,cable:2:core,cable:3:core,9.9703279749934753e-07,6.6990063703023524e-06,9.9703279749934965e-07,6.6990063703023549e-06,9.9572451158906602e-07,6.6758931134513074e-06,2.3150253870865903e-08,2.3150253870863247e-08,0.0034297983685706692,0.0034297983685702758,1.3082859102815086e-09,2.3113256851044977e-08 +1,cable:3:core,cable:1:core,9.9679142273796299e-07,5.8280015973796128e-06,9.9679142261440587e-07,5.8280015972434844e-06,9.9514743470101688e-07,5.8084827030194114e-06,1.9588004696495186e-08,1.9588004842513194e-08,0.0033238802967689375,0.0033238803215466721,1.6439880369461155e-09,1.9518894360201307e-08 +1,cable:3:core,cable:2:core,9.9703279749934732e-07,6.6990063703023516e-06,9.9703279749934944e-07,6.6990063703023541e-06,9.957245115890656e-07,6.6758931134513066e-06,2.3150253870865913e-08,2.3150253870863256e-08,0.0034297983685706713,0.0034297983685702776,1.3082859102817204e-09,2.3113256851044977e-08 +1,cable:3:core,cable:3:core,4.0462600939189788e-06,1.0981371245585023e-05,4.0462600939189881e-06,1.0981371245585021e-05,4.0645144914664652e-06,1.0929057973331517e-05,5.5406691686930907e-08,5.5406691686935579e-08,0.0047517033168123137,0.0047517033168127144,1.8254397547486479e-08,5.2313272253506142e-08 +10,cable:1:core,cable:1:core,1.4059558517812229e-05,9.4699427403525111e-05,1.405955851781232e-05,9.4699427403525043e-05,1.4078098949110193e-05,9.4275608916675479e-05,4.2422382934974958e-07,4.2422382934982131e-07,0.0044504784527611236,0.0044504784527618765,1.8540431297964731e-08,4.2381848684963207e-07 +10,cable:1:core,cable:2:core,1.0164608279866819e-05,5.2299588182049285e-05,1.0164608279866875e-05,5.2299588182049292e-05,1.0154098438757886e-05,5.2151781639659582e-05,1.4817972443400351e-07,1.4817972443399279e-07,0.0027889450380819262,0.0027889450380817245,1.0509841108933266e-08,1.4780654238970281e-07 +10,cable:1:core,cable:3:core,1.0144854518138271e-05,4.3592310471018251e-05,1.0144854518138249e-05,4.3592310471018413e-05,1.0121825613513052e-05,4.3494797801831069e-05,1.0019506524912759e-07,1.0019506524897438e-07,0.0022436580289046677,0.0022436580289012372,2.302890462521901e-08,9.7512669187182024e-08 +10,cable:2:core,cable:1:core,1.0164608279866821e-05,5.2299588182049278e-05,1.0164608279866877e-05,5.2299588182049285e-05,1.0154098438757886e-05,5.2151781639659596e-05,1.4817972443398336e-07,1.4817972443397262e-07,0.0027889450380815463,0.0027889450380813442,1.050984110893496e-08,1.4780654238968248e-07 +10,cable:2:core,cable:2:core,1.4059511160988105e-05,9.4699437546059127e-05,1.40595111609882e-05,9.4699437546059073e-05,1.4084679812657182e-05,9.4295470492056221e-05,4.0475034496165286e-07,4.047503449617129e-07,0.0042452664397349873,0.004245266439735617,2.5168651669077343e-08,4.0396705400290531e-07 +10,cable:2:core,cable:3:core,1.0164608279866821e-05,5.2299588182049299e-05,1.0164608279866877e-05,5.2299588182049305e-05,1.015405496530911e-05,5.2150531700236723e-05,1.4942960623149575e-07,1.4942960623148503e-07,0.0028125349093116991,0.0028125349093114974,1.0553314557710424e-08,1.4905648181257563e-07 +10,cable:3:core,cable:1:core,1.0144854518138269e-05,4.3592310471018237e-05,1.0144854518138247e-05,4.3592310471018407e-05,1.0121825613513055e-05,4.3494797801831048e-05,1.001950652491396e-07,1.0019506524897981e-07,0.0022436580289049378,0.0022436580289013595,2.3028904625213928e-08,9.75126691871888e-08 +10,cable:3:core,cable:2:core,1.0164608279866819e-05,5.2299588182049285e-05,1.0164608279866875e-05,5.2299588182049292e-05,1.0154054965309122e-05,5.2150531700236689e-05,1.4942960623151505e-07,1.4942960623150436e-07,0.0028125349093120642,0.002812534909311863,1.0553314557696871e-08,1.4905648181259596e-07 +10,cable:3:core,cable:3:core,1.4059558517812229e-05,9.4699427403525124e-05,1.405955851781232e-05,9.4699427403525057e-05,1.4078345460130376e-05,9.4274952375718503e-05,4.2489057230425445e-07,4.2489057230432618e-07,0.004457501831262185,0.0044575018312629378,1.8786942318147265e-08,4.2447502780662192e-07 +100,cable:1:core,cable:1:core,0.00011749986341587683,0.0007780998311891264,0.00011749986341587662,0.00077809983118912608,0.00011825586406508788,0.00077377332628715296,4.3920589304340287e-06,4.3920589304343116e-06,0.0056110070277203489,0.0056110070277207106,7.5600064921104945e-07,4.3265049019734466e-06 +100,cable:1:core,cable:2:core,0.00010621027609229592,0.00037161720315773155,0.00010621027609229646,0.00037161720315773177,0.0001060875527036882,0.00037087417345515203,7.5309638760734481e-07,7.5309638760704253e-07,0.0019522968660933113,0.0019522968660925276,1.2272338860771928e-07,7.4302970257952372e-07 +100,cable:1:core,cable:3:core,0.00010469597101762082,0.0002848479209964935,0.00010469597101762054,0.00028484792099649496,0.00010453541028029063,0.00028471069054957095,2.1121540174638963e-07,2.1121540174564472e-07,0.00069640262305146095,0.0006964026230490049,1.6056073733018358e-07,1.3723044692254662e-07 +100,cable:2:core,cable:1:core,0.00010621027609229592,0.00037161720315773155,0.00010621027609229646,0.00037161720315773177,0.0001060875527036882,0.00037087417345515198,7.5309638760739828e-07,7.53096387607096e-07,0.0019522968660934501,0.0019522968660926664,1.2272338860771928e-07,7.4302970257957793e-07 +100,cable:2:core,cable:2:core,0.00011749664935473164,0.00077810092618215538,0.0001174966493547314,0.00077810092618215505,0.0001182868821400726,0.00077387508461446977,4.2990934870278096e-06,4.2990934870280866e-06,0.0054915018851088944,0.0054915018851092482,7.9023278534096834e-07,4.2258415676856096e-06 +100,cable:2:core,cable:3:core,0.0001062102760922959,0.00037161720315773144,0.00010621027609229645,0.00037161720315773166,0.00010609004988632196,0.00037088100213371892,7.4595327491769492e-07,7.4595327491739359e-07,0.0019337429926763682,0.0019337429926755871,1.202262059739475e-07,7.3620102401252777e-07 +100,cable:3:core,cable:1:core,0.00010469597101762083,0.00028484792099649361,0.00010469597101762056,0.00028484792099649502,0.00010453541028029058,0.00028471069054957084,2.1121540174654681e-07,2.112154017458371e-07,0.00069640262305197941,0.00069640262304963949,1.6056073733025135e-07,1.3723044692276346e-07 +100,cable:3:core,cable:2:core,0.00010621027609229592,0.00037161720315773155,0.00010621027609229646,0.00037161720315773177,0.00010609004988632197,0.00037088100213371886,7.4595327491785543e-07,7.459532749175541e-07,0.0019337429926767848,0.0019337429926760035,1.202262059739475e-07,7.362010240126904e-07 +100,cable:3:core,cable:3:core,0.00011749986341587681,0.00077809983118912629,0.00011749986341587659,0.00077809983118912597,0.00011825887855140381,0.00077380158948475069,4.3647434890483931e-06,4.3647434890486752e-06,0.0055759083438218417,0.0055759083438222017,7.5901513552699786e-07,4.2982417043756086e-06 +1000,cable:1:core,cable:1:core,0.0011874959966138062,0.0060823308476815156,0.0011874959966138036,0.0060823308476815122,0.0011844846896089539,0.0060323090445500768,5.0112361333283211e-05,5.0112361333286829e-05,0.0081516651063368428,0.0081516651063374308,3.011307004852342e-06,5.0021803131438877e-05 +1000,cable:1:core,cable:2:core,0.0011019671033492395,0.0020963802241269879,0.0011019671033492449,0.0020963802241269901,0.0011007904697264187,0.0020948151416138971,1.9580474854186773e-06,1.9580474854136861e-06,0.00082742681037456295,0.00082742681037245385,1.1766336228207462e-06,1.565082513090791e-06 +1000,cable:1:core,cable:3:core,0.001003106505504941,0.0012640618611339275,0.0010031065055049382,0.0012640618611339432,0.001002585911545266,0.0012671742958162359,3.1556723090945284e-06,3.1556723091103921e-06,0.0019529719880883797,0.0019529719880981974,5.2059395967501108e-07,3.1124346823083703e-06 +1000,cable:2:core,cable:1:core,0.0011019671033492392,0.0020963802241269875,0.0011019671033492447,0.0020963802241269897,0.0011007904697264185,0.0020948151416138963,1.9580474854190237e-06,1.958047485414033e-06,0.00082742681037470964,0.00082742681037260065,1.1766336228207462e-06,1.5650825130912246e-06 +1000,cable:2:core,cable:2:core,0.00118734914471262,0.0060824491669874475,0.0011873491447126174,0.006082449166987444,0.001184352323381433,0.0060323445176638027,5.0194191117460129e-05,5.0194191117463747e-05,0.0081649638133294983,0.0081649638133300863,2.9968213311869912e-06,5.01046493236448e-05 +1000,cable:2:core,cable:3:core,0.0011019671033492395,0.0020963802241269884,0.0011019671033492449,0.0020963802241269905,0.0011007744570689971,0.0020950182326893848,1.8103662132010835e-06,1.810366213195881e-06,0.00076496431933728896,0.00076496431933509074,1.1926462802423331e-06,1.3619914376035262e-06 +1000,cable:3:core,cable:1:core,0.0010031065055049406,0.0012640618611339273,0.001003106505504938,0.0012640618611339429,0.0010025859115452653,0.0012671742958162346,3.1556723090935306e-06,3.1556723091093583e-06,0.0019529719880877641,0.0019529719880975595,5.2059395967522792e-07,3.1124346823072861e-06 +1000,cable:3:core,cable:2:core,0.0011019671033492395,0.0020963802241269879,0.0011019671033492449,0.0020963802241269901,0.0011007744570689967,0.0020950182326893844,1.8103662132013694e-06,1.8103662131961667e-06,0.00076496431933740995,0.00076496431933521152,1.1926462802427668e-06,1.3619914376035262e-06 +1000,cable:3:core,cable:3:core,0.0011874959966138065,0.0060823308476815156,0.0011874959966138039,0.0060823308476815122,0.0011843717432249161,0.0060323374132095807,5.0090961754964124e-05,5.0090961754967749e-05,0.008148176031697282,0.0081481760316978718,3.1242533888903475e-06,4.9993434471934904e-05 +10000,cable:1:core,cable:1:core,0.011218478577540311,0.043047522284281221,0.011218478577540311,0.043047522284281221,0.011119864601649793,0.042732429667698781,0.00033016370676636285,0.00033016370676636285,0.0074772879693647334,0.0074772879693647334,9.8613975890518568e-05,0.00031509261658244081 +10000,cable:1:core,cable:2:core,0.0080360922846632078,0.0048677586412040601,0.0080360922846632096,0.004867758641204061,0.0080263098434048309,0.0048746152014419982,1.1946069448571345e-05,1.1946069448570422e-05,0.0012721294666787422,0.001272129466678644,9.782441258376906e-06,6.856560237938035e-06 +10000,cable:1:core,cable:3:core,0.0043864669830369976,-0.00021545006964656183,0.0043864669830384383,-0.00021545006964465192,0.0043999105501135146,-0.00019680874044557522,2.2983225404856133e-05,2.2983225407247927e-05,0.0052183487158196486,0.0052183487163627055,1.3443567076516956e-05,1.8641329200986607e-05 +10000,cable:2:core,cable:1:core,0.0080360922846632113,0.0048677586412040593,0.0080360922846632113,0.0048677586412040601,0.0080263098434048327,0.0048746152014419956,1.1946069448570349e-05,1.1946069448570847e-05,0.0012721294666786362,0.0012721294666786893,9.7824412583786408e-06,6.8565602379363003e-06 +10000,cable:2:core,cable:2:core,0.011219720413915016,0.043052062902262878,0.011219720413915016,0.043052062902262878,0.011113161895694636,0.042732163106039826,0.00033718036335005135,0.00033718036335005135,0.0076365321513983796,0.0076365321513983796,0.00010655851822037946,0.00031989979622305126 +10000,cable:2:core,cable:3:core,0.0080360922846632078,0.0048677586412040593,0.0080360922846632078,0.004867758641204061,0.0080264334418784031,0.0048743926405697656,1.1717644452949676e-05,1.1717644452950657e-05,0.0012478059452165494,0.0012478059452166539,9.6588427848046804e-06,6.6339993657063209e-06 +10000,cable:3:core,cable:1:core,0.0043864669830369976,-0.00021545006964656183,0.0043864669830384383,-0.00021545006964465192,0.0043999105501135137,-0.00019680874044557579,2.2983225404855164e-05,2.2983225407246958e-05,0.0052183487158194292,0.0052183487163624869,1.3443567076516089e-05,1.8641329200986038e-05 +10000,cable:3:core,cable:2:core,0.0080360922846632078,0.0048677586412040601,0.0080360922846632096,0.004867758641204061,0.0080264334418784066,0.0048743926405697647,1.1717644452947755e-05,1.1717644452946814e-05,0.0012478059452163447,0.0012478059452162445,9.6588427848012109e-06,6.6339993657045862e-06 +10000,cable:3:core,cable:3:core,0.011218478577540311,0.043047522284281221,0.011218478577540311,0.043047522284281221,0.011119821235292367,0.042731468842562757,0.0003310937166445695,0.0003310937166445695,0.0074985098650658943,0.0074985098650658943,9.8657342247943786e-05,0.00031605344171846406 +100000,cable:1:core,cable:1:core,0.091870290228580009,0.28525847944837568,0.091870290228580009,0.28525847944837568,0.091493003714531451,0.28368767551605356,0.0016154782906252691,0.0016154782906252691,0.005419674749492156,0.005419674749492156,0.00037728651404855817,0.0015708039323221179 +100000,cable:1:core,cable:2:core,0.0087273397177971886,-0.0087082547107099739,0.0087273397177971886,-0.0087082547107099739,0.0087334969981067243,-0.0087216205518427393,1.4715903302091928e-05,1.4715903302091928e-05,0.0011922821576872793,0.0011922821576872793,6.1572803095356898e-06,1.3365841132765427e-05 +100000,cable:1:core,cable:3:core,-0.0014312810262812371,-0.00036817498716284558,-0.0014312810262812373,-0.00036817498716284683,-0.0014407703250733342,-0.00039045784136311349,2.4219256447629282e-05,2.4219256447630512e-05,0.016224684861121757,0.016224684861122583,9.4892987920970791e-06,2.2282854200267907e-05 +100000,cable:2:core,cable:1:core,0.0087273397177971886,-0.0087082547107099739,0.0087273397177971886,-0.0087082547107099739,0.0087334969981067174,-0.0087216205518427445,1.4715903302093751e-05,1.4715903302093751e-05,0.0011922821576874272,0.0011922821576874272,6.1572803095287509e-06,1.3365841132770631e-05 +100000,cable:2:core,cable:2:core,0.091870046795798257,0.28524984365980688,0.091870046795798257,0.28524984365980688,0.091493956461796602,0.28371579711037065,0.0015794754683649504,0.0015794754683649504,0.0052984100213463075,0.0052984100213463075,0.00037609033400165526,0.0015340465494362299 +100000,cable:2:core,cable:3:core,0.0087273397177971886,-0.0087082547107099756,0.0087273397177971886,-0.0087082547107099756,0.0087339349659137991,-0.0087212590278095137,1.45811371622721e-05,1.45811371622721e-05,0.0011813581962924681,0.0011813581962924681,6.5952481166104554e-06,1.3004317099538087e-05 +100000,cable:3:core,cable:1:core,-0.0014312810262812369,-0.00036817498716284564,-0.0014312810262812373,-0.00036817498716284683,-0.001440770325073334,-0.00039045784136311344,2.4219256447629147e-05,2.4219256447630414e-05,0.01622468486112167,0.016224684861122517,9.4892987920970791e-06,2.2282854200267798e-05 +100000,cable:3:core,cable:2:core,0.0087273397177971869,-0.0087082547107099739,0.0087273397177971869,-0.0087082547107099739,0.0087339349659138008,-0.0087212590278095102,1.4581137162272122e-05,1.4581137162272122e-05,0.0011813581962924699,0.0011813581962924699,6.5952481166139248e-06,1.3004317099536353e-05 +100000,cable:3:core,cable:3:core,0.091870290228580009,0.28525847944837579,0.091870290228580009,0.28525847944837579,0.091488767351543793,0.28367378798203707,0.0016299713952056781,0.0016299713952056781,0.0054685631684245721,0.0054685631684245721,0.00038152287703621668,0.0015846914663387168 +1000000,cable:1:core,cable:1:core,0.7540744079244075,1.574047945066307,0.7540744079244075,1.574047945066307,0.75373506062126217,1.5682514990945176,0.0058063708541588772,0.0058063708541588772,0.0033370337035997892,0.0033370337035997892,0.00033934730314533201,0.0057964459717894101 +1000000,cable:1:core,cable:2:core,0.00015357089839005441,0.0010629809539592517,0.00015357089839005441,0.0010629809539592517,0.00015808363781219276,0.0010647782065002921,4.8574616610321926e-06,4.8574616610321926e-06,0.0045124853754643004,0.0045124853754643004,4.512739422138346e-06,1.7972525410403766e-06 +1000000,cable:1:core,cable:3:core,1.8110903613484064e-07,1.4306474614933584e-06,1.8110903613483902e-07,1.4306474614933575e-06,7.1837044682661596e-08,1.4988795531927263e-06,1.2882541074491399e-07,1.2882541074491492e-07,0.085849265245670214,0.085849265245670825,1.0927199145217904e-07,6.8232091699367878e-08 +1000000,cable:2:core,cable:1:core,0.00015357089839005439,0.0010629809539592513,0.00015357089839005439,0.0010629809539592513,0.00015808363781219241,0.0010647782065002923,4.8574616610321307e-06,4.8574616610321307e-06,0.0045124853754642423,0.0045124853754642423,4.5127394221380207e-06,1.7972525410410271e-06 +1000000,cable:2:core,cable:2:core,0.75407435015734658,1.5740479687634792,0.75407435015734658,1.5740479687634792,0.75371118835143447,1.5684996969879792,0.0055601444398579404,0.0055601444398579404,0.003195130814093403,0.003195130814093403,0.00036316180591211555,0.0055482717754999911 +1000000,cable:2:core,cable:3:core,0.00015357089839005436,0.0010629809539592515,0.00015357089839005436,0.0010629809539592515,0.00015805992066927291,0.0010648271671355046,4.8538463217835019e-06,4.8538463217835019e-06,0.0045089385260994042,0.0045089385260994042,4.4890222792185537e-06,1.8462131762531347e-06 +1000000,cable:3:core,cable:1:core,1.8110903613484059e-07,1.4306474614933584e-06,1.8110903613483902e-07,1.4306474614933575e-06,7.1837044682661649e-08,1.4988795531927248e-06,1.2882541074491317e-07,1.2882541074491404e-07,0.085849265245669756,0.085849265245670339,1.0927199145217894e-07,6.8232091699366396e-08 +1000000,cable:3:core,cable:2:core,0.00015357089839005441,0.0010629809539592517,0.00015357089839005441,0.0010629809539592517,0.00015805992066927281,0.0010648271671355048,4.8538463217833511e-06,4.8538463217833511e-06,0.0045089385260992628,0.0045089385260992628,4.4890222792183911e-06,1.8462131762531347e-06 +1000000,cable:3:core,cable:3:core,0.7540744079244075,1.5740479450663067,0.7540744079244075,1.5740479450663067,0.75374922767666375,1.5683328404748265,0.0057243482323389152,0.0057243482323389152,0.0032897435000615268,0.0032897435000615268,0.00032518024774375043,0.00571510459148028 +10000000,cable:1:core,cable:1:core,4.5479347057971937,6.6714050104071516,4.5479347057971937,6.6714050104071516,4.5499283379608109,6.6607143326137441,0.010874978661416526,0.010874978661416526,0.0013481808196322126,0.0013481808196322126,0.0019936321636171428,0.010690677793407488 +10000000,cable:1:core,cable:2:core,3.0492342492444656e-09,1.3142318645708507e-08,3.0492342492444656e-09,1.3142318645708507e-08,1.70928388337544e-08,2.3364452433972144e-08,1.7369940958772128e-08,1.7369940958772128e-08,0.60001217996457723,0.60001217996457723,1.4043604584509934e-08,1.0222133788263637e-08 +10000000,cable:1:core,cable:3:core,1.6548264118885075e-17,1.2699296335778548e-17,1.6548264118882764e-17,1.2699296335776218e-17,1.7780742088352628e-16,8.2830433131016448e-17,1.758490602438255e-16,1.7584906024382244e-16,NaN,NaN,1.612591567646412e-16,7.0131136795237902e-17 +10000000,cable:2:core,cable:1:core,3.0492342492444656e-09,1.3142318645708507e-08,3.0492342492444656e-09,1.3142318645708507e-08,1.70928388337544e-08,2.3364452433972167e-08,1.7369940958772141e-08,1.7369940958772141e-08,0.60001217996457734,0.60001217996457734,1.4043604584509934e-08,1.022213378826366e-08 +10000000,cable:2:core,cable:2:core,4.5479347057971937,6.6714050104071516,4.5479347057971937,6.6714050104071516,4.5500657434906797,6.6605837453015528,0.011029102417545492,0.011029102417545492,0.0013672928193374241,0.0013672928193374241,0.0021310376934859931,0.010821265105598776 +10000000,cable:2:core,cable:3:core,3.0492342492444656e-09,1.3142318645708507e-08,3.0492342492444656e-09,1.3142318645708507e-08,2.0088895639091691e-08,2.8716067494419929e-08,2.3084447437255037e-08,2.3084447437255037e-08,0.65870223072825995,0.65870223072825995,1.7039661389847225e-08,1.5573748848711423e-08 +10000000,cable:3:core,cable:1:core,1.6548264118885075e-17,1.2699296335778546e-17,1.6548264118882761e-17,1.2699296335776215e-17,1.7780742088352623e-16,8.2830433131016571e-17,1.758490602438255e-16,1.7584906024382247e-16,NaN,NaN,1.6125915676464116e-16,7.0131136795238025e-17 +10000000,cable:3:core,cable:2:core,3.0492342492444656e-09,1.3142318645708507e-08,3.0492342492444656e-09,1.3142318645708507e-08,2.0088895639091684e-08,2.8716067494419946e-08,2.3084447437255044e-08,2.3084447437255044e-08,0.65870223072825984,0.65870223072825984,1.7039661389847219e-08,1.5573748848711439e-08 +10000000,cable:3:core,cable:3:core,4.5479347057971937,6.6714050104071516,4.5479347057971937,6.6714050104071516,4.5502090089252469,6.6605380139855157,0.01110243513586638,0.01110243513586638,0.0013763766299583725,0.0013763766299583725,0.0022743031280532122,0.010866996421635911 diff --git a/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/comparison.toml b/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/comparison.toml new file mode 100644 index 000000000..4208df51d --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/validation-20260919T144543-aqKVmx/comparison.toml @@ -0,0 +1,133 @@ +baseline = "capture-20260917T102438-Nt9nic" +criterion = "No material degradation; retain complete entry/component differences; no FEM rerun" +julia = "1.12.7" + +[runs."air_1/Y"] +after_max_absolute_error = 0.12586656843393867 +after_worst_entry = "1000000,cable:2:core,cable:2:core" +baseline_csv_sha256 = "c1781a943be20f3d99bdf8a9558ce4e70134f3cd19460da22eca39f526aed6c4" +before_max_absolute_error = 0.1258665684345085 +before_worst_entry = "1000000,cable:2:core,cable:2:core" +fem_csv_sha256 = "d2dc753fa5d79c0345fca421b35415d4af68526791a7d58fef044729399276d1" +first_seconds = 1.346708892 +fresh_warmed_bytes = 280656 +fresh_warmed_seconds = 0.018906096 +max_entry_error_increase = 6.653578463732021e-8 +max_result_change = 2.8526875350369104e-7 + +[runs."air_1/Z"] +after_max_absolute_error = 1.8723151853984725 +after_worst_entry = "10000000,cable:1:core,cable:1:core" +baseline_csv_sha256 = "58f904dc16552619aaffb4bfb00a663eb0f1e76feca62e142c7b1899e6b1fa07" +before_max_absolute_error = 1.8723151853984727 +before_worst_entry = "10000000,cable:1:core,cable:1:core" +fem_csv_sha256 = "5fba0d0a60a9196c2ef04b2b2a1cab12f56a0b7a573f229c4d8771698c7c4ff0" +first_seconds = 1.346708892 +fresh_warmed_bytes = 280656 +fresh_warmed_seconds = 0.018906096 +max_entry_error_increase = 1.4617754467046833e-16 +max_result_change = 2.4097390362987555e-15 + +[runs."air_2/Y"] +after_max_absolute_error = 0.12511124689727263 +after_worst_entry = "1000000,cable:1:core,cable:1:core" +baseline_csv_sha256 = "d942680200138e2bfaf84b60ffce59ca762ad80a65941e8b74d0bb9b6e22e219" +before_max_absolute_error = 0.12511124689781913 +before_worst_entry = "1000000,cable:1:core,cable:1:core" +fem_csv_sha256 = "51ff0beb013a2fe9624150538d4b1f14b17520b00fb1823b8b60c3cdab70e4c3" +first_seconds = 0.153318952 +fresh_warmed_bytes = 280656 +fresh_warmed_seconds = 0.019364102 +max_entry_error_increase = 8.268831008748942e-8 +max_result_change = 2.84600655827909e-7 + +[runs."air_2/Z"] +after_max_absolute_error = 1.707148753384576 +after_worst_entry = "10000000,cable:2:core,cable:2:core" +baseline_csv_sha256 = "47e67a8dc68a2766876a1e1937377c3949ae592904d224542044679412cd3dc6" +before_max_absolute_error = 1.707148753384576 +before_worst_entry = "10000000,cable:2:core,cable:2:core" +fem_csv_sha256 = "f2a7eeceef9db641c9218ba816d2c1108f143b37f2a48ec786f39e3d54a36c15" +first_seconds = 0.153318952 +fresh_warmed_bytes = 280656 +fresh_warmed_seconds = 0.019364102 +max_entry_error_increase = 2.2047132592880117e-15 +max_result_change = 2.3928886547049893e-15 + +[runs."air_3/Y"] +after_max_absolute_error = 0.12560262627986038 +after_worst_entry = "1000000,cable:1:core,cable:1:core" +baseline_csv_sha256 = "d755dd9ecf39ac2edb7a9bd74e040b9421272992ef1a2625c611d932c3d90a06" +before_max_absolute_error = 0.1256026262798634 +before_worst_entry = "1000000,cable:1:core,cable:1:core" +fem_csv_sha256 = "6df558e978eb303643fb47203c04e4507cf131309c2ea71c633d9fe721c1ca49" +first_seconds = 0.018768522 +fresh_warmed_bytes = 280656 +fresh_warmed_seconds = 0.018732335 +max_entry_error_increase = 6.653660214534632e-8 +max_result_change = 2.852687535036917e-7 + +[runs."air_3/Z"] +after_max_absolute_error = 2.130821524209557 +after_worst_entry = "10000000,cable:3:core,cable:3:core" +baseline_csv_sha256 = "a10546be75ece17056a779c6ebd6f154d19b5b4f5404d74121243c5b2e8e5122" +before_max_absolute_error = 2.130821524209557 +before_worst_entry = "10000000,cable:3:core,cable:3:core" +fem_csv_sha256 = "a70bba3ce78afdf163876ec11ca3398222b028d45eb2bf46e4c36e012b984276" +first_seconds = 0.018768522 +fresh_warmed_bytes = 280656 +fresh_warmed_seconds = 0.018732335 +max_entry_error_increase = 1.460781208695619e-16 +max_result_change = 2.4102558251379085e-15 + +[runs."all_air/Y"] +after_max_absolute_error = 1.2609414645891618e-5 +after_worst_entry = "10000000,cable:1:core,cable:3:core" +baseline_csv_sha256 = "9edfe22b78aac77a31505d80e51d1690f5bee7651155533d395f231788970e7d" +before_max_absolute_error = 1.2609467382028214e-5 +before_worst_entry = "10000000,cable:1:core,cable:3:core" +fem_csv_sha256 = "2e0d25927c3c7515dffe960d854bd83a2127a03ffe9d6631757f5b52fdea2f7e" +first_seconds = 18.586098401 +fresh_warmed_bytes = 244688 +fresh_warmed_seconds = 0.021776624 +max_entry_error_increase = 6.930431843153376e-10 +max_result_change = 8.520020319279558e-10 + +[runs."all_air/Z"] +after_max_absolute_error = 1.948507230227846 +after_worst_entry = "10000000,cable:2:core,cable:1:core" +baseline_csv_sha256 = "509fce8b1d62060461705c315d79345f58c20b4703ab9dacf4d9983dc43ed52a" +before_max_absolute_error = 1.948507230227846 +before_worst_entry = "10000000,cable:2:core,cable:1:core" +fem_csv_sha256 = "5a3bee66ff67323ba4cd90b27c0fb5ca1d7e94fcea73545144ba7c591be691e9" +first_seconds = 18.586098401 +fresh_warmed_bytes = 244688 +fresh_warmed_seconds = 0.021776624 +max_entry_error_increase = 2.393918396847994e-16 +max_result_change = 1.0837836292118093e-13 + +[runs."all_earth/Y"] +after_max_absolute_error = 0.12638102356686592 +after_worst_entry = "1000000,cable:1:core,cable:1:core" +baseline_csv_sha256 = "0682607582edc6218ce090a8b55e671e94fbcacd84081c4d2094ef3e880639e8" +before_max_absolute_error = 0.12638102356686592 +before_worst_entry = "1000000,cable:1:core,cable:1:core" +fem_csv_sha256 = "6178fb7bb6c30653f628a82279d9637b280f82723f830d8e85297c90cb57fad8" +first_seconds = 1.888247315 +fresh_warmed_bytes = 244688 +fresh_warmed_seconds = 0.017507348 +max_entry_error_increase = 2.917694280624161e-10 +max_result_change = 6.884368601498597e-10 + +[runs."all_earth/Z"] +after_max_absolute_error = 0.01110243513586638 +after_worst_entry = "10000000,cable:3:core,cable:3:core" +baseline_csv_sha256 = "2191de0b3d3cce5bbae75eec5dee2042c9f65683200cf947c22a31e42bef0b17" +before_max_absolute_error = 0.01110243513586638 +before_worst_entry = "10000000,cable:3:core,cable:3:core" +fem_csv_sha256 = "0e7a1341b5dfb48bec0350a353822b8a919460b1bf84d9ce12010b8ab70687a4" +first_seconds = 1.888247315 +fresh_warmed_bytes = 244688 +fresh_warmed_seconds = 0.017507348 +max_entry_error_increase = 2.39179403821628e-15 +max_result_change = 2.392344588403254e-15 diff --git a/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/air_1-Y.csv b/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/air_1-Y.csv new file mode 100644 index 000000000..bbcfb393f --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/air_1-Y.csv @@ -0,0 +1,82 @@ +frequency_hz,receiver,source,new_real,new_imaginary,old_real,old_imaginary,fem_real,fem_imaginary,before_absolute_error,after_absolute_error,before_relative_error,after_relative_error,real_absolute_error,imaginary_absolute_error +0.10000000000000001,cable:1:core,cable:1:core,-1.0666964456901865e-24,9.0759067020569639e-12,3.3278820986485526e-24,9.0759067020569009e-12,3.3668435289524459e-24,9.1335364427824092e-12,5.762974072550828e-14,5.7629740725445272e-14,0.0063096853104526677,0.0063096853104457696,4.4335399746426325e-24,5.7629740725445272e-14 +0.10000000000000001,cable:1:core,cable:2:core,-5.1155498185351802e-14,8.7999087097332524e-13,-1.3601058441964637e-13,-4.2379939867923232e-12,-1.1396039787123376e-13,-4.2860602028816373e-12,5.288262338390966e-14,5.1664328267289576e-12,0.012333923926933294,1.204977843021956,6.2804899685881958e-14,5.1660510738549623e-12 +0.10000000000000001,cable:1:core,cable:3:core,-5.1136148413973948e-14,-3.5844900350302839e-13,-1.3581746350160624e-13,-2.9334497945739113e-12,-1.138186657146977e-13,-2.9739964949248815e-12,4.6130055423992855e-14,2.6162984879134687e-12,0.015499786037223238,0.87908124972851831,6.268251730072376e-14,2.615547491421853e-12 +0.10000000000000001,cable:2:core,cable:1:core,-1.1296417471165001e-13,-4.2580756761562078e-12,-1.1296417495695157e-13,-4.2580756764147954e-12,-1.1396039787123331e-13,-4.2860602028816397e-12,2.8002253153424086e-14,2.8002253420575015e-14,0.0065310235778156742,0.0065310236401238397,9.9622315958329258e-16,2.7984526725431948e-14 +0.10000000000000001,cable:2:core,cable:2:core,10.815794395856088,0.14817480024469867,10.815794395856189,0.14817480024712162,10.885701106001211,0.14939721814968343,0.06991739718287969,0.069917397183023283,0.0064222609881899742,0.0064222609882031642,0.06990671014512273,0.0012224179049847572 +0.10000000000000001,cable:2:core,cable:3:core,-8.399898030521344,0.14802772097586725,-8.3998980305212694,0.14802772097705341,-8.459190172239154,0.14927298011308732,0.059305216800966121,0.059305216800916438,0.0070096527232738877,0.0070096527232680157,0.059292141717810054,0.0012452591372200672 +0.10000000000000001,cable:3:core,cable:1:core,-1.1280530780399827e-13,-2.9535313553156199e-12,-1.1280530750282936e-13,-2.9535313549728525e-12,-1.1381866571469942e-13,-2.973996494924871e-12,2.0490213471832484e-14,2.0490213114589991e-14,0.0068847505547380916,0.0068847504347039372,1.013357910701149e-15,2.046513960925102e-14 +0.10000000000000001,cable:3:core,cable:2:core,-8.3998980305213671,0.14802772097541272,-8.3998980305212712,0.14802772097705655,-8.4591901722391665,0.14927298011308582,0.059305216800976682,0.059305216800915293,0.0070096527232751255,0.00700965272326787,0.059292141717799396,0.0012452591376730937 +0.10000000000000001,cable:3:core,cable:3:core,10.815794395856095,0.14817480024522803,10.815794395856152,0.14817480024608787,10.886063726310885,0.14944203838006351,0.070280756221349369,0.0702807562214217,0.0064554219599144659,0.0064554219599211099,0.070269330454790335,0.00126723813483548 +1,cable:1:core,cable:1:core,-1.1415236378499652e-22,9.0759067020560391e-11,3.1423916594339808e-22,9.0759067020551008e-11,3.219734897938415e-22,9.1337560189597194e-11,5.784931690461863e-13,5.7849316903680297e-13,0.0063335737000786803,0.0063335736999759482,4.3612585357883799e-22,5.7849316903680297e-13 +1,cable:1:core,cable:2:core,-1.2038703207920688e-12,9.7714076065056048e-12,-2.4605830567011761e-12,-3.9952527978242649e-11,-1.646906536789909e-12,-4.0927322450860221e-11,1.2697612936692786e-12,5.0700665779833052e-11,0.030999695076452637,1.2377957866443887,4.4303621599784019e-13,5.0698730057365822e-11 +1,cable:1:core,cable:3:core,-1.2021168565223695e-12,-2.6131097494743189e-12,-2.4448833834075763e-12,-2.6909309921922225e-11,-1.6349560106687234e-12,-2.7803932015100898e-11,1.2067854153556263e-12,2.5194540602117553e-11,0.04332856228458494,0.90458768213457352,4.3283915414635381e-13,2.519082226562658e-11 +1,cable:2:core,cable:1:core,-1.6339304032893058e-12,-4.0648885431794787e-11,-1.6339304034811497e-12,-4.064888543178278e-11,-1.64690653678989e-12,-4.0927322450860311e-11,2.7873922154664215e-13,2.78739221543579e-13,0.0068050829056412005,0.0068050829055664175,1.2976133500584165e-14,2.7843701906552484e-13 +1,cable:2:core,cable:2:core,11.070033787834017,0.21907795136028552,11.070033787835508,0.21907795138250027,11.140075659587218,0.2206567269328914,0.070059662650640772,0.07005966265263136,0.0062877421458306694,0.0062877421460093208,0.070041871753200979,0.0015787755726058839 +1,cable:2:core,cable:3:core,-8.1458225270901394,0.21787423633650826,-8.1458225270891802,0.2178742363474,-8.2045753701006827,0.21950423737083269,0.058775449511429961,0.058775449510773153,0.0071611781584668046,0.0071611781583867801,0.058752843010543288,0.0016300010343244242 +1,cable:3:core,cable:1:core,-1.6216279544894657e-12,-2.7605627275107898e-11,-1.6216279546194463e-12,-2.7605627275097564e-11,-1.6349560106687567e-12,-2.7803932015100808e-11,1.9875212448124282e-13,1.9875212447964912e-13,0.0071360191258537714,0.0071360191257965506,1.3328056179290995e-14,1.983047399929099e-13 +1,cable:3:core,cable:2:core,-8.1458225270905356,0.21787423633236563,-8.1458225270892974,0.2178742363475,-8.2045753701006845,0.21950423737083044,0.058775449511311702,0.058775449510493773,0.0071611781584523951,0.0071611781583527387,0.058752843010148936,0.0016300010384648067 +1,cable:3:core,cable:3:core,11.07003378783407,0.21907795136545705,11.070033787834912,0.2190779513727919,11.140726485540558,0.22075493091369747,0.070712585649814752,0.070712585650830453,0.0063459692289450269,0.0063459692290361788,0.070692697706487451,0.0016769795482404204 +10,cable:1:core,cable:1:core,-1.3303446776678944e-20,9.0759067020265848e-10,2.7893988431890267e-20,9.0759067020117803e-10,3.0077099906617912e-20,9.1341140218724399e-10,5.8207319860659567e-12,5.8207319845855102e-12,0.0063725195154425507,0.0063725195138217621,4.3380546683296853e-20,5.8207319845855102e-12 +10,cable:1:core,cable:2:core,-3.5126392564573668e-11,1.2192069752793325e-10,-5.5746231805725367e-11,-3.5319928202439058e-10,-2.5413114510482376e-11,-3.8068516338465009e-10,4.0933747434082193e-11,5.026997107560687e-10,0.10728771847134705,1.317580442154159,9.7132780540912919e-12,5.0260586091258334e-10 +10,cable:1:core,cable:3:core,-3.4971191232074974e-11,-1.9389230827135353e-12,-5.4539271312740426e-11,-2.2299128059268604e-10,-2.4539872873309703e-11,-2.4965631529573399e-10,4.0137114774762647e-11,2.4793692505861508e-10,0.15999839432994978,0.98834981356026208,1.0431318358765271e-11,2.4771739221302046e-10 +10,cable:2:core,cable:1:core,-2.5235453499545093e-11,-3.7794241747563783e-10,-2.5235453516660523e-11,-3.7794241747387376e-10,-2.5413114510482166e-11,-3.8068516338464952e-10,2.7484938711597683e-12,2.7484938705057229e-12,0.007203827041344626,0.0072038270396303679,1.7766101093707258e-13,2.7427459090116871e-12 +10,cable:2:core,cable:2:core,11.45543946668298,0.35723512525375894,11.455439466704808,0.35723512545106118,11.525898556465627,0.35946125443124921,0.070494247851576916,0.070494247879624494,0.0061131884458517664,0.0061131884482840231,0.0704590897826467,0.0022261291774902658 +10,cable:2:core,cable:3:core,-7.7620499918470696,0.34786703913335942,-7.7620499918328036,0.34786703922484841,-7.8200883233535077,0.35016961009861736,0.058083988829417323,0.058083988818789428,0.0074201009092482346,0.0074201009078905446,0.058038331506438112,0.0023025709652579329 +10,cable:3:core,cable:1:core,-2.4359412290390294e-11,-2.4772235737226404e-10,-2.4359412301593318e-11,-2.47722357370758e-10,-2.4539872873309943e-11,-2.4965631529573425e-10,1.942359203011314e-12,1.9423592025526408e-12,0.0077428174754907673,0.0077428174736623601,1.8046058291964827e-13,1.9339579234702163e-12 +10,cable:3:core,cable:2:core,-7.7620499918553723,0.3478670390993035,-7.7620499918370207,0.34786703922841328,-7.820088323353505,0.35016961009861763,0.0580839888250596,0.05808398881184066,0.007420100908691548,0.0074201009070028572,0.058038331498132756,0.0023025709993141352 +10,cable:3:core,cable:3:core,11.455439466681341,0.35723512530509677,11.455439466692642,0.35723512536493024,11.526629071262175,0.35960663832318673,0.071229094283709632,0.071229094296996656,0.0061765200533299928,0.0061765200544821562,0.071189604580833787,0.0023715130180899657 +100,cable:1:core,cable:1:core,-1.9401763256901172e-18,9.0759067008644283e-09,1.9127172212576587e-18,9.0759067005974392e-09,2.6849740688989855e-18,9.1322530610991367e-09,5.6346360501697491e-11,5.6346360234708586e-11,0.0061700393237806069,0.0061700392945447866,4.6251503945891026e-18,5.6346360234708399e-11 +100,cable:1:core,cable:2:core,-1.1765965611356636e-09,1.9628848004581836e-09,-1.5750338466741854e-09,-2.411086810379962e-09,-4.2756269288587337e-10,-3.3523773182763521e-09,1.4841556080924008e-09,5.3677800929843974e-09,0.43915996451163936,1.5883200536978668,7.4903386824979017e-10,5.3152621187345357e-09 +100,cable:1:core,cable:3:core,-1.1636921547696423e-09,7.2216173971858769e-10,-1.491065678495518e-09,-1.131648013139559e-09,-3.7369268803186256e-10,-2.0601909963507641e-09,1.4528298150466409e-09,2.8923322605396814e-09,0.69386954866857675,1.3813739637190778,7.8999946673777976e-10,2.7823527360693517e-09 +100,cable:2:core,cable:1:core,-4.2495166240793803e-10,-3.3258245297456913e-09,-4.2495166380733107e-10,-3.3258245294674344e-09,-4.2756269288587238e-10,-3.3523773182763517e-09,2.668085580299013e-11,2.6680855663015577e-11,0.0078948350319152146,0.0078948349904969031,2.6110304779343512e-12,2.6552788530660359e-11 +100,cable:2:core,cable:2:core,12.10466978238181,0.68261648131153441,12.104669782732641,0.6826164828956679,12.176710067798881,0.68639023960195522,0.072139059545459905,0.072139059978679629,0.0059149574465560744,0.0059149574820774172,0.072040285417070749,0.0037737582904208145 +100,cable:2:core,cable:3:core,-7.1288262824171325,0.61549758312471348,-7.1288262822103956,0.61549758378874042,-7.1859340087568553,0.61910011073453508,0.057221242836034171,0.05722124267151317,0.0079335619960233406,0.0079335619732129742,0.057107726339722831,0.0036025276098216041 +100,cable:3:core,cable:1:core,-3.7138847615143487e-10,-2.0430445510009429e-09,-3.7138847700779842e-10,-2.0430445507832228e-09,-3.7369268803186308e-10,-2.0601909963507678e-09,1.7300577563890915e-11,1.7300577462166922e-11,0.008262732373493651,0.0082627323249104094,2.3042118804282082e-12,1.7146445349824888e-11 +100,cable:3:core,cable:2:core,-7.1288262826252131,0.61549758295382873,-7.1288262823565649,0.61549758392216858,-7.1859340087568544,0.61910011073453464,0.057221242681753583,0.057221242474602925,0.0079335619746327777,0.0079335619459119308,0.057107726131641279,0.0036025277807059108 +100,cable:3:core,cable:3:core,12.104669782274954,0.6826164818757352,12.104669782424534,0.68261648227961069,12.176101339397905,0.68619513278362976,0.071521144223717956,0.071521144393318914,0.0058645897345041284,0.0058645897484110648,0.071431557122950906,0.0035786509078945539 +1000,cable:1:core,cable:1:core,-4.4750964200884585e-16,9.0759066492957148e-08,-1.1449192093489899e-16,9.0759066436426566e-08,2.1468668223221234e-16,9.1290601213133132e-08,5.3153477670666778e-10,5.3153472017639643e-10,0.0058224479808793375,0.0058224473616450391,6.6219632424105819e-16,5.3153472017598398e-10 +1000,cable:1:core,cable:2:core,-3.9862609765354866e-08,4.739255430708393e-08,-4.9053839044978193e-08,1.1664030389121588e-08,-7.7436488465027592e-09,-2.5407758196285771e-08,5.5505399045096105e-08,7.957080589698105e-08,2.0896863010547984,2.9957089924120153,3.2118960918852108e-08,7.2800312503369694e-08 +1000,cable:1:core,cable:3:core,-3.8993651370144037e-08,3.4644002403347795e-08,-4.4374379330248368e-08,2.2280527656749991e-08,-5.343392651111024e-09,-1.3932639017219087e-08,5.324294659110877e-08,5.9093400676896156e-08,3.5680502298860661,3.9601155715367384,3.3650258719033011e-08,4.857664142056688e-08 +1000,cable:2:core,cable:1:core,-7.6990451786107599e-09,-2.5177999875353155e-08,-7.6990452661998496e-09,-2.5177999831420838e-08,-7.7436488465027675e-09,-2.5407758196285751e-08,2.3404782759350137e-10,2.340478011586232e-10,0.0088115128893391167,0.008811511894109756,4.4603667892007594e-11,2.2975832093259631e-10 +1000,cable:2:core,cable:2:core,13.422507237708384,1.6280303255557746,13.422507243289033,1.6280303346877074,13.494055376278043,1.6356743688863127,0.071955309693187364,0.071955316212370585,0.0052936233733021816,0.0052936238529068258,0.071548138569658803,0.0076440433305380306 +1000,cable:2:core,cable:3:core,-5.956480579531533,1.2141336183221656,-5.9564805769900744,1.214133621295584,-6.0087326165714305,1.2209099909311973,0.052689608328873289,0.052689606190929744,0.0085932431791693412,0.0085932428304882746,0.052252037039897559,0.0067763726090317444 +1000,cable:3:core,cable:1:core,-5.3125767624237946e-09,-1.3812807031995066e-08,-5.3125768097971736e-09,-1.3812807003898827e-08,-5.3433926511110199e-09,-1.3932639017219117e-08,1.2373086717660297e-10,1.2373085176430589e-10,0.0082917640239543716,0.008291762991106762,3.0815888687225279e-11,1.1983198522405101e-10 +1000,cable:3:core,cable:2:core,-5.9564805841176129,1.2141336216946841,-5.956480580627443,1.2141336265943121,-6.0087326165714261,1.2209099909311989,0.052689604040242428,0.052689601209194283,0.0085932424797288097,0.0085932420180080665,0.052252032453813158,0.006776369236514812 +1000,cable:3:core,cable:3:core,13.422507234576006,1.6280303345300924,13.422507236118634,1.6280303360967185,13.493652206194168,1.6354456101924859,0.071530364580114594,0.071530366276836566,0.0052625265336032779,0.0052625266584320051,0.071144971618162245,0.0074152756623935279 +10000,cable:1:core,cable:1:core,-1.6692047511660082e-13,9.0759045224291654e-07,-1.461383916862566e-13,9.0759044220431542e-07,1.1772351924736467e-14,9.1289430599284201e-07,5.303863790877314e-09,5.303853752935647e-09,0.0058099428992592498,0.0058099319035266649,1.7869282704133729e-13,5.3038537499254656e-09 +10000,cable:1:core,cable:2:core,-7.0566345675390599e-07,1.6746422052936652e-06,-8.7200816269054662e-07,1.5363787545915005e-06,-1.0906877871764883e-07,-9.4990917897013724e-08,1.8009562770739958e-06,1.867492061711316e-06,12.451735183997533,12.911760772130876,5.9659467803625714e-07,1.7696331231906789e-06 +10000,cable:1:core,cable:3:core,-6.8597772458705922e-07,1.5110795613731924e-06,-7.4376780209961771e-07,1.4920783526578328e-06,-5.90316652777458e-08,-4.4321203179529856e-08,1.6820782301209971e-06,1.677001162687334e-06,22.78681045583658,22.718032338855622,6.2694605930931344e-07,1.5554007645527222e-06 +10000,cable:2:core,cable:1:core,-1.0820602972944975e-07,-9.3862017398338054e-08,-1.0820603068185215e-07,-9.3862013882438677e-08,-1.0906877871764885e-07,-9.4990917897013724e-08,1.420830196538177e-09,1.4208279813364973e-09,0.0098235596132651929,0.0098235442974548941,8.6274898819909917e-10,1.12890049867567e-09 +10000,cable:2:core,cable:2:core,17.086533949182268,4.2459947823977897,17.086533982869692,4.2459947768646558,17.168725355808448,4.2708879656289653,0.085878359511655275,0.08587839014892458,0.0048540883417152094,0.0048540900734205267,0.082191406626179742,0.02489318323117562 +10000,cable:2:core,cable:3:core,-3.3202280396447059,2.2955285385873436,-3.3202280314027313,2.2955285336624969,-3.3566031067050783,2.3127730439275873,0.040255673358350737,0.040255663801212906,0.0098756963403934112,0.0098756939957949184,0.036375067060372412,0.017244505340243688 +10000,cable:3:core,cable:1:core,-5.8587420059988364e-08,-4.3856895290619899e-08,-5.8587420416245618e-08,-4.3856893464081314e-08,-5.9031665277745781e-08,-4.432120317952985e-08,6.4260174978691039e-10,6.4260067631799851e-10,0.0087052100245836664,0.0087051954824938944,4.4424521775741637e-10,4.6430788890995042e-10 +10000,cable:3:core,cable:2:core,-3.3202280044350188,2.2955286501758185,-3.3202279874715748,2.2955286461190285,-3.3566031067050797,2.3127730439275886,0.040255664881250826,0.040255647815225615,0.0098756942607544987,0.0098756900740431947,0.036375102270060911,0.017244393751770115 +10000,cable:3:core,cable:3:core,17.086534011462401,4.2459949224509836,17.086534015481533,4.2459949196386599,17.167730206096849,4.2706627660680407,0.084860615234054318,0.084860618262128304,0.004796839125662194,0.0047968392968273897,0.081196194634447494,0.024667843617057095 +100000,cable:1:core,cable:1:core,-5.5477726061553848e-11,9.0758164316572844e-06,-5.4794500700018239e-11,9.0758158501016051e-06,3.4064351579940734e-13,9.1287839375614467e-06,5.2968116155264175e-08,5.2967535315491585e-08,0.0058023189635719868,0.0058022553362831212,5.5818369577353259e-11,5.2967505904162281e-08 +100000,cable:1:core,cable:2:core,1.7038431448127925e-05,8.9143470293325821e-06,1.6957770865381572e-05,9.1879747547490105e-06,-1.3982409354353952e-07,1.597141532094673e-07,1.9334871162455997e-05,1.9280457988612775e-05,91.085372495896408,90.829035426642079,1.7178255541671466e-05,8.7546328761231148e-06 +100000,cable:1:core,cable:3:core,1.5638455966719112e-05,7.7776922470880283e-06,1.5670639845467551e-05,7.7897672759496725e-06,-5.2728554835898479e-08,7.5364927336429311e-08,1.751388927245889e-05,1.7479677280490033e-05,190.41141642792607,190.03946284592789,1.5691184521555011e-05,7.7023273197515982e-06 +100000,cable:2:core,cable:1:core,-1.3779482033724361e-07,1.5778396979479867e-07,-1.3779479962975289e-07,1.5778397005743111e-07,-1.3982409354353934e-07,1.5971415320946725e-07,2.8006500654196122e-09,2.8006352422354799e-09,0.013193687834586973,0.013193618003490767,2.0292732062957301e-09,1.9301834146685728e-09 +100000,cable:2:core,cable:2:core,25.114053168427059,8.0617974533493069,25.114053162870391,8.061797456881747,25.209507982247846,8.1184901178623399,0.11102108066238134,0.11102107768864615,0.0041919253259874098,0.0041919252137053477,0.09545481382078691,0.056692664513033009 +100000,cable:2:core,cable:3:core,0.17168867697767834,1.0502294238147609,0.17168867735387133,1.0502294245133199,0.17188900017107905,1.0596269692791682,0.0093996796146047969,0.0093996803210224349,0.0087562848060661353,0.0087562854641304831,0.00020032319340071436,0.0093975454644072975 +100000,cable:3:core,cable:1:core,-5.2567620245545815e-08,7.4524196565821724e-08,-5.2567611266984596e-08,7.452419574772051e-08,-5.272855483589846e-08,7.5364927336429271e-08,8.5599791858829219e-10,8.5599542698447529e-10,0.0093064295201444932,0.0093064024313691453,1.6093459035264504e-10,8.4073077060754738e-10 +100000,cable:3:core,cable:2:core,0.17168861145690964,1.0502292230423049,0.17168860920972603,1.0502292248219658,0.17188900017107883,1.0596269692791682,0.0093998807130873142,0.0093998824444375129,0.0087564721396412356,0.0087564737524829551,0.0002003887141691918,0.0093977462368632914 +100000,cable:3:core,cable:3:core,25.114053090920191,8.0617971940330779,25.114053091114521,8.0617971943186379,25.210637229590311,8.1196614463144066,0.1125911518021756,0.11259115211563631,0.0042509777892140357,0.0042509778010490192,0.096584138670120723,0.057864252281328632 +1000000,cable:1:core,cable:1:core,-2.4740421441749431e-08,9.0711568120478795e-05,-2.4719345718823342e-08,9.07115475559531e-05,1.0616619292708136e-11,9.1254999550522529e-05,5.4401437613241005e-07,5.439947913762378e-07,0.0059614747554868766,0.005961260139780683,2.4751038061042137e-08,5.4343143004373362e-07 +1000000,cable:1:core,cable:2:core,-7.7938841060709405e-06,5.4353142732557047e-06,-7.7983645094344234e-06,5.4402402295420326e-06,-3.7777528896064878e-09,-1.4747468946151504e-08,9.5138043650246079e-06,9.5073092927330662e-06,624.93618448120003,624.5095406760438,7.7901063531813343e-06,5.4500617422018565e-06 +1000000,cable:1:core,cable:3:core,-6.4349987748322076e-06,5.106801280693119e-06,-6.4350271678452739e-06,5.1067899044121279e-06,-1.5728602749174711e-09,-5.801878865365625e-09,8.2175378958683303e-06,8.2175227450384768e-06,1367.0158584768271,1367.0133380838649,6.43342591455729e-06,5.1126031595584845e-06 +1000000,cable:2:core,cable:1:core,-3.6993309151330325e-09,-1.4559820469910918e-08,-3.6993334581473185e-09,-1.455981630897755e-08,-3.7777528896064837e-09,-1.4747468946151509e-08,2.033792503396546e-10,2.0337639173643051e-10,0.013359435177914946,0.013359247403968452,7.8421974473451184e-11,1.8764847624059038e-10 +1000000,cable:2:core,cable:2:core,40.833774627207681,19.549173405646204,40.8337746272082,19.549173405645227,40.898656436585682,19.657028604228248,0.1258665684345085,0.12586656843393867,0.002773778589948313,0.0027737785899357558,0.06488180937800081,0.10785519858204395 +1000000,cable:2:core,cable:3:core,-0.020403832081271406,-0.018991054091930557,-0.020403832081266379,-0.018991054091929888,-0.020561861246802895,-0.019101245069676594,0.00019265323443156234,0.00019265323442705564,0.006864521977926509,0.0068645219777659292,0.00015802916553148832,0.00011019097774603651 +1000000,cable:3:core,cable:1:core,-1.5503926404254643e-09,-5.8061933848089228e-09,-1.5503936374884616e-09,-5.8061917080445249e-09,-1.572860274917473e-09,-5.8018788653656292e-09,2.2876853134545371e-11,2.2878148476068964e-11,0.0038056436639851057,0.0038058591485289352,2.2467634492008675e-11,4.3145194432936257e-12 +1000000,cable:3:core,cable:2:core,-0.020403832499743361,-0.018991053490800946,-0.020403832499540878,-0.018991053491192872,-0.020561861246802884,-0.019101245069676587,0.00019265323493243477,0.00019265323499051157,0.0068645219957733442,0.0068645219978427071,0.00015802874705952291,0.00011019157887564093 +1000000,cable:3:core,cable:3:core,40.833774626502667,19.549173406400044,40.833774626502667,19.54917340640004,40.896804204764933,19.65119276898254,0.1199194649656956,0.11991946496569258,0.0026429640409972297,0.0026429640409971634,0.063029578262266739,0.10201936258249589 +10000000,cable:1:core,cable:1:core,-1.6761991871629861e-05,0.0008877789650905823,-1.6761328755330462e-05,0.00088777830561370815,2.0223235310295311e-08,-0.00010676664179201856,0.00099468651991342204,0.00099468719048421278,9.3164539539167421,9.3164602346310872,1.6782215106940157e-05,0.00099454560688260092 +10000000,cable:1:core,cable:2:core,-9.3439526677307196e-11,5.4261359972629985e-10,-9.3440467051073175e-11,5.4261406900812513e-10,-1.7415122836923419e-12,-6.8626727083962635e-13,5.5098453127675587e-10,5.509839120363144e-10,294.35270366985196,294.35237285276395,9.1698014393614859e-11,5.4329986699713949e-10 +10000000,cable:1:core,cable:3:core,3.6775716049205592e-11,4.3675577430240467e-10,3.6775425414283791e-11,4.3675607950356147e-10,-2.4124979876850083e-12,-1.1426545811342262e-12,4.3964871733412077e-10,4.3964843925353919e-10,164.69818462293117,164.69808045029254,3.91882140368906e-11,4.3789842888353891e-10 +10000000,cable:2:core,cable:1:core,-1.1003843823177017e-13,-1.3743099164070029e-13,-1.1003845565783392e-13,-1.3743080703062872e-13,-1.7415122836923411e-12,-6.8626727083962585e-13,1.7213158674595239e-12,1.7213158251136901e-12,0.91957931792104819,0.91957929529861515,1.6314738454605708e-12,5.4883627919892558e-13 +10000000,cable:2:core,cable:2:core,80.846531351828517,55.108195401013525,80.846531351828517,55.108195401013525,80.822969599449294,55.217612590982597,0.11192532169222402,0.11192532169222402,0.0011434450267779742,0.0011434450267779742,0.023561752379222867,0.10941718996907213 +10000000,cable:2:core,cable:3:core,-9.2664181398870313e-08,-1.3479772233806722e-07,-9.2664181398870313e-08,-1.3479772233806722e-07,-2.9463434702527936e-07,-1.8880177493102272e-07,2.0906550528392246e-07,2.0906550528392246e-07,0.59743851816518001,0.59743851816518001,2.0197016562640905e-07,5.4004052592955504e-08 +10000000,cable:3:core,cable:1:core,-5.2158582298414681e-14,-5.472514978320991e-14,-5.2158582925956725e-14,-5.4725070160129558e-14,-2.4124979876850006e-12,-1.1426545811342232e-12,2.5989984083309108e-12,2.5989983755709509e-12,0.9736189435182262,0.97361893124591492,2.3603394053865861e-12,1.0879294313510132e-12 +10000000,cable:3:core,cable:2:core,-9.266418139883618e-08,-1.3479772233804374e-07,-9.266418139883618e-08,-1.3479772233804374e-07,-2.9463434702527925e-07,-1.8880177493102251e-07,2.0906550528396134e-07,2.0906550528396134e-07,0.59743851816529148,0.59743851816529148,2.0197016562644309e-07,5.400405259297877e-08 +10000000,cable:3:core,cable:3:core,80.846531351828517,55.108195401013525,80.846531351828517,55.108195401013525,80.821711858300745,55.220113257927331,0.11463687868724544,0.11463687868724544,0.0011711422262802136,0.0011711422262802136,0.024819493527772352,0.11191785691380574 diff --git a/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/air_1-Z.csv b/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/air_1-Z.csv new file mode 100644 index 000000000..2f3e2c9b3 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/air_1-Z.csv @@ -0,0 +1,82 @@ +frequency_hz,receiver,source,new_real,new_imaginary,old_real,old_imaginary,fem_real,fem_imaginary,before_absolute_error,after_absolute_error,before_relative_error,after_relative_error,real_absolute_error,imaginary_absolute_error +0.10000000000000001,cable:1:core,cable:1:core,3.1368030432638579e-06,1.2442491566826318e-06,3.1368030432638579e-06,1.2442491566826316e-06,3.1562793020459726e-06,1.2381622459009902e-06,2.0405272333677916e-08,2.0405272333677979e-08,0.0060184571470754513,0.0060184571470754704,1.9476258782114692e-08,6.0869107816415649e-09 +0.10000000000000001,cable:1:core,cable:2:core,9.8692094314379155e-08,7.1450042905111216e-07,9.8692094314379645e-08,7.1450042905111184e-07,9.8558501409278213e-08,7.1184683786552884e-07,2.6569518709410696e-09,2.656951870941362e-09,0.0036972079475629089,0.0036972079475633161,1.3359290510094226e-10,2.6535911855833211e-09 +0.10000000000000001,cable:1:core,cable:3:core,9.8689933658488536e-08,6.8496952230340285e-07,9.8689933646133091e-08,6.8496952228978849e-07,9.8555318141668934e-08,6.8241325093108754e-07,2.5598133903386247e-09,2.5598134045838953e-09,0.0037126008775244007,0.0037126008981848927,1.346155168196018e-10,2.5562713723153075e-09 +0.10000000000000001,cable:2:core,cable:1:core,9.8692094314379168e-08,7.1450042905111216e-07,9.8692094314379671e-08,7.1450042905111195e-07,9.8558501409278186e-08,7.1184683786552915e-07,2.6569518709408607e-09,2.6569518709410469e-09,0.0036972079475626166,0.0036972079475628759,1.3359290510098197e-10,2.6535911855830035e-09 +0.10000000000000001,cable:2:core,cable:2:core,3.1374615510534723e-06,1.2435841216202133e-06,3.1374615510534732e-06,1.2435841216202129e-06,3.1569380449518183e-06,1.2377824992095822e-06,2.0322220281450704e-08,2.0322220281451637e-08,0.005993122305261364,0.0059931223052616398,1.9476493898345935e-08,5.8016224106311635e-09 +0.10000000000000001,cable:2:core,cable:3:core,9.9023675189330895e-08,8.1529149017255945e-07,9.9023675189331398e-08,8.1529149017255914e-07,9.8888942097083999e-08,8.1219887993903893e-07,3.0955437426436837e-09,3.0955437426439794e-09,0.0037833729615709966,0.0037833729615713578,1.3473309224689585e-10,3.0926102335205263e-09 +0.10000000000000001,cable:3:core,cable:1:core,9.8689933658488549e-08,6.8496952230340285e-07,9.8689933646132827e-08,6.8496952228978997e-07,9.8555318141668934e-08,6.8241325093108775e-07,2.5598133903398795e-09,2.5598134045836848e-09,0.0037126008775262192,0.0037126008981845861,1.3461551681961503e-10,2.5562713723150957e-09 +0.10000000000000001,cable:3:core,cable:2:core,9.9023675189330882e-08,8.1529149017255935e-07,9.9023675189331385e-08,8.1529149017255903e-07,9.8888942097084039e-08,8.1219887993903882e-07,3.0955437426436816e-09,3.0955437426439769e-09,0.0037833729615709944,0.0037833729615713556,1.3473309224684291e-10,3.0926102335205263e-09 +0.10000000000000001,cable:3:core,cable:3:core,3.1374615510534723e-06,1.2435841216202133e-06,3.1374615510534732e-06,1.2435841216202129e-06,3.1569378453844727e-06,1.2376254103650067e-06,2.0367431862918889e-08,2.0367431862919823e-08,0.0060065573188623055,0.0060065573188625805,1.9476294331000322e-08,5.9587112552066507e-09 +1,cable:1:core,cable:1:core,4.0258228222657537e-06,1.1002346916048132e-05,4.025822822265763e-06,1.1002346916048128e-05,4.0439380999692672e-06,1.0950296462592307e-05,5.5112729847392547e-08,5.5112729847398814e-08,0.0047213246161881542,0.0047213246161886911,1.811527770351351e-08,5.2050453455824476e-08 +1,cable:1:core,cable:2:core,9.8663940253676711e-07,5.6982925454684157e-06,9.8663940253676944e-07,5.6982925454684157e-06,9.8532040051059231e-07,5.6808597261099677e-06,1.7482647314675746e-08,1.7482647314675571e-08,0.00303219367860406,0.0030321936786040296,1.3190020261747945e-09,1.7432819358448011e-08 +1,cable:1:core,cable:3:core,9.8646617111842135e-07,5.4030097683584801e-06,9.8646617099486401e-07,5.40300976822235e-06,9.8504682423657282e-07,5.3864382934087735e-06,1.6632147267183239e-08,1.6632147413360784e-08,0.0030374090709155005,0.0030374090976108509,1.4193468818485214e-09,1.6571474949706518e-08 +1,cable:2:core,cable:1:core,9.866394025367669e-07,5.6982925454684148e-06,9.8663940253676944e-07,5.6982925454684148e-06,9.853204005105921e-07,5.6808597261099677e-06,1.7482647314674919e-08,1.7482647314674727e-08,0.0030321936786039164,0.003032193678603883,1.3190020261747945e-09,1.7432819358447164e-08 +1,cable:2:core,cable:2:core,4.0462619649157106e-06,1.098137058628601e-05,4.0462619649157199e-06,1.0981370586286009e-05,4.0645266771623253e-06,1.093273668005019e-05,5.1950520201492852e-08,5.1950520201497715e-08,0.0044539817697942623,0.0044539817697946795,1.8264712246614701e-08,4.8633906235820551e-08 +1,cable:2:core,cable:3:core,9.9703495653241241e-07,6.6990056616567392e-06,9.9703495653241453e-07,6.6990056616567426e-06,9.9566788129606608e-07,6.6773632135146649e-06,2.1685581760339246e-08,2.1685581760335732e-08,0.0032121136374929221,0.0032121136374924017,1.3670752363463284e-09,2.1642448142074279e-08 +1,cable:3:core,cable:1:core,9.8646617111842135e-07,5.4030097683584818e-06,9.8646617099486423e-07,5.4030097682223517e-06,9.8504682423657261e-07,5.386438293408771e-06,1.6632147267187494e-08,1.6632147413365019e-08,0.003037409070916279,0.0030374090976116258,1.4193468818487332e-09,1.6571474949710753e-08 +1,cable:3:core,cable:2:core,9.9703495653241262e-07,6.6990056616567409e-06,9.9703495653241474e-07,6.6990056616567443e-06,9.9566788129606587e-07,6.6773632135146632e-06,2.1685581760342654e-08,2.168558176033914e-08,0.0032121136374934278,0.0032121136374929074,1.367075236346752e-09,2.1642448142077667e-08 +1,cable:3:core,cable:3:core,4.0462619649157106e-06,1.0981370586286007e-05,4.0462619649157199e-06,1.0981370586286005e-05,4.064508501178054e-06,1.093114109659993e-05,5.344097416492523e-08,5.3440974164930001e-08,0.0045823559991124358,0.0045823559991128452,1.8246536262343391e-08,5.0229489686076881e-08 +10,cable:1:core,cable:1:core,1.3443875148217054e-05,9.5357294237721687e-05,1.3443875148217149e-05,9.535729423772166e-05,1.3456741305710767e-05,9.4931369051803823e-05,4.2611946917243133e-07,4.2611946917246129e-07,0.0044442818814534836,0.0044442818814537958,1.2866157493712918e-08,4.2592518591786418e-07 +10,cable:1:core,cable:2:core,9.8448260812245156e-06,4.2519810980107041e-05,9.8448260812245715e-06,4.2519810980107068e-05,9.8332944172567729e-06,4.2419997939162006e-05,1.0047697455918364e-07,1.0047697455915029e-07,0.0023074389292056367,0.0023074389292048708,1.1531663967742761e-08,9.9813040945034826e-08 +10,cable:1:core,cable:3:core,9.8317851451266156e-06,3.9569605534518693e-05,9.831785145126619e-06,3.9569605534518673e-05,9.8166164930190614e-06,3.9483456666432487e-05,8.747408461533662e-08,8.7474084615356062e-08,0.002150006771041425,0.0021500067710419029,1.5168652107554209e-08,8.614886808620565e-08 +10,cable:2:core,cable:1:core,9.8448260812245156e-06,4.2519810980107054e-05,9.8448260812245698e-06,4.2519810980107075e-05,9.8332944172567746e-06,4.2419997939162019e-05,1.0047697455917652e-07,1.004769745591501e-07,0.0023074389292054727,0.002307438929204866,1.1531663967741067e-08,9.9813040945034826e-08 +10,cable:2:core,cable:2:core,1.4059660509356969e-05,9.4699377213437565e-05,1.405966050935706e-05,9.4699377213437497e-05,1.4078358508886816e-05,9.4300886456954623e-05,3.9892918944182097e-07,3.9892918944189297e-07,0.0041840165400588826,0.0041840165400596381,1.8697999529847454e-08,3.9849075648294219e-07 +10,cable:2:core,cable:3:core,1.0164731798726432e-05,5.2299532930466564e-05,1.0164731798726485e-05,5.2299532930466571e-05,1.0151570594638799e-05,5.2158599331381674e-05,1.4154680019011417e-07,1.4154680019010255e-07,0.002663793035197452,0.0026637930351972334,1.3161204087633625e-08,1.4093359908488986e-07 +10,cable:3:core,cable:1:core,9.8317851451266156e-06,3.9569605534518693e-05,9.8317851451266173e-06,3.9569605534518679e-05,9.8166164930190665e-06,3.9483456666432508e-05,8.7474084615322102e-08,8.7474084615335151e-08,0.0021500067710410672,0.0021500067710413877,1.5168652107549127e-08,8.6148868086185321e-08 +10,cable:3:core,cable:2:core,1.0164731798726432e-05,5.2299532930466564e-05,1.0164731798726483e-05,5.2299532930466571e-05,1.0151570594638785e-05,5.2158599331381681e-05,1.4154680019010853e-07,1.4154680019009707e-07,0.0026637930351973453,0.0026637930351971297,1.3161204087647178e-08,1.4093359908488309e-07 +10,cable:3:core,cable:3:core,1.4059660509356969e-05,9.4699377213437578e-05,1.405966050935706e-05,9.4699377213437511e-05,1.407770266779548e-05,9.428378799062607e-05,4.1598067454887159e-07,4.1598067454894327e-07,0.00436363292053358,0.004363632920534332,1.8042158438510983e-08,4.1558922281150835e-07 +100,cable:1:core,cable:1:core,0.00010029065542748212,0.00079835616300552852,0.00010029065542748186,0.00079835616300552798,0.00010105736213043232,0.00079409082895413549,4.3336951598280937e-06,4.3336951598285816e-06,0.0054137666875932588,0.0054137666875938685,7.6670670295020046e-07,4.2653340513930339e-06 +100,cable:1:core,cable:2:core,9.6939258720589021e-05,0.00028095471396052328,9.693925872058955e-05,0.00028095471396052344,9.6830997663775953e-05,0.00028061951809663104,3.522452605684939e-07,3.5224526056817668e-07,0.0011865858128200211,0.0011865858128189525,1.0826105681306826e-07,3.3519586389223286e-07 +100,cable:1:core,cable:3:core,9.6062121241851888e-05,0.0002517102826184886,9.6062121241851698e-05,0.00025171028261849001,9.5942898104968443e-05,0.00025153139211222554,2.1497899804362638e-07,2.1497899804255875e-07,0.00079856022041533976,0.00079856022041137396,1.1922313688344491e-07,1.7889050626305981e-07 +100,cable:2:core,cable:1:core,9.6939258720589035e-05,0.00028095471396052328,9.6939258720589563e-05,0.00028095471396052344,9.6830997663775858e-05,0.00028061951809663077,3.5224526056878517e-07,3.5224526056846796e-07,0.0011865858128210034,0.0011865858128199348,1.0826105681317668e-07,3.3519586389250391e-07 +100,cable:2:core,cable:2:core,0.00011750384543838808,0.00077809644648036455,0.00011750384543838787,0.00077809644648036422,0.00011826512950095309,0.0007739317981559253,4.2336567043358251e-06,4.2336567043361064e-06,0.0054075508292252576,0.0054075508292256167,7.6128406256500297e-07,4.1646483244392419e-06 +100,cable:2:core,cable:3:core,0.00010621565159805932,0.00037161327841812262,0.00010621565159805986,0.00037161327841812283,0.00010608353483127337,0.00037094741867822398,6.7884021189399916e-07,6.7884021189368099e-07,0.0017594819672218783,0.0017594819672210537,1.3211676678595596e-07,6.6585973989863887e-07 +100,cable:3:core,cable:1:core,9.6062121241851902e-05,0.00025171028261848866,9.6062121241851712e-05,0.00025171028261849007,9.594289810496847e-05,0.00025153139211222538,2.1497899804379931e-07,2.1497899804273168e-07,0.00079856022041598269,0.00079856022041201679,1.1922313688343135e-07,1.7889050626327665e-07 +100,cable:3:core,cable:2:core,0.00010621565159805932,0.00037161327841812256,0.00010621565159805986,0.00037161327841812278,0.00010608353483127338,0.00037094741867822409,6.7884021189383706e-07,6.7884021189351878e-07,0.0017594819672214579,0.0017594819672206328,1.3211676678594241e-07,6.6585973989847624e-07 +100,cable:3:core,cable:3:core,0.00011750384543838827,0.00077809644648036498,0.00011750384543838806,0.00077809644648036465,0.00011825895634178478,0.00077390901166622618,4.254973889354382e-06,4.2549738893546632e-06,0.0054349416120347775,0.0054349416120351365,7.5511090339650802e-07,4.1874348141388018e-06 +1000,cable:1:core,cable:1:core,0.00078851876842865208,0.0066684718151727796,0.0007885187684286524,0.0066684718151727796,0.00078545538086318019,0.0066160382983872414,5.2522928572915652e-05,5.2522928572915638e-05,0.0078833666855376294,0.0078833666855376277,3.0633875654718836e-06,5.2433516785538141e-05 +1000,cable:1:core,cable:2:core,0.00088055424215431675,0.0014022944498764066,0.00088055424215432207,0.0014022944498764082,0.00087926355750770843,0.001402374698144822,1.2931769567957754e-06,1.2931769567905672e-06,0.00078127110159509035,0.00078127110159194378,1.2906846466083236e-06,8.024826841533314e-08 +1000,cable:1:core,cable:3:core,0.00083441087840421878,0.0011330005183466552,0.00083441087840421617,0.0011330005183466706,0.00083336723943152919,0.0011339562556012919,1.4151381576305953e-06,1.415138157642912e-06,0.0010056040545897938,0.0010056040545985462,1.043638972689589e-06,9.5573725463664674e-07 +1000,cable:2:core,cable:1:core,0.00088055424215431675,0.0014022944498764066,0.00088055424215432196,0.0014022944498764082,0.00087926355750770843,0.0014023746981448211,1.2931769567956134e-06,1.2931769567905134e-06,0.00078127110159499278,0.00078127110159191158,1.2906846466083236e-06,8.0248268414465779e-08 +1000,cable:2:core,cable:2:core,0.0011875295144730819,0.0060821869409338524,0.0011875295144730793,0.0060821869409338489,0.0011843876922731453,0.0060323216961683784,4.9964123951654399e-05,4.9964123951658031e-05,0.0081275599595141655,0.0081275599595147553,3.1418221999365142e-06,4.9865244765474014e-05 +1000,cable:2:core,cable:3:core,0.001102054672250501,0.0020961818335963364,0.0011020546722505062,0.0020961818335963386,0.0011007933449973671,0.0020953145045025617,1.5307534734316936e-06,1.5307534734261769e-06,0.00064674079606106736,0.00064674079605873654,1.2613272531338936e-06,8.6732909377468248e-07 +1000,cable:3:core,cable:1:core,0.00083441087840421867,0.0011330005183466552,0.00083441087840421607,0.0011330005183466706,0.00083336723943152799,0.0011339562556012916,1.4151381576312484e-06,1.4151381576435651e-06,0.0010056040545902585,0.0010056040545990109,1.0436389726906732e-06,9.557372546364299e-07 +1000,cable:3:core,cable:2:core,0.001102054672250501,0.002096181833596336,0.0011020546722505062,0.0020961818335963381,0.0011007933449973669,0.0020953145045025626,1.5307534734311352e-06,1.5307534734256185e-06,0.00064674079606083133,0.00064674079605850051,1.2613272531341105e-06,8.6732909377338144e-07 +1000,cable:3:core,cable:3:core,0.0011875295144732649,0.0060821869409340432,0.0011875295144732623,0.0060821869409340397,0.0011842919805786728,0.0060323818412918598,4.9910215147663765e-05,4.991021514766739e-05,0.0081187371353865726,0.0081187371353871624,3.2375338945920661e-06,4.9805099642183381e-05 +10000,cable:1:core,cable:1:core,0.0051968947578844198,0.056898361105510734,0.0051968947578844259,0.056898361105510824,0.0051086484699835975,0.056506249875501097,0.00040191867837663007,0.00040191867837654073,0.0070839260769350052,0.0070839260769334309,8.8246287900822264e-05,0.00039211123000963688 +10000,cable:1:core,cable:2:core,0.0052676968239068487,0.0024783732958180761,0.0052676968239072927,0.0024783732958178969,0.0052556743075239708,0.0024792030592478047,1.2051116443585497e-05,1.2051116443130131e-05,0.0020738197714685932,0.0020738197713902314,1.2022516382877851e-05,8.2976342972865616e-07 +10000,cable:1:core,cable:3:core,0.0040559166332564903,0.0011450309278026656,0.0040559166332579258,0.0011450309278046007,0.0040495802099877413,0.001147388226955296,6.7607040420953695e-06,6.7607040414246914e-06,0.0016062531647138068,0.0016062531645544627,6.3364232687490585e-06,2.3572991526303252e-06 +10000,cable:2:core,cable:1:core,0.0052676968239068452,0.0024783732958180761,0.0052676968239068461,0.0024783732958180774,0.0052556743075239743,0.0024792030592478034,1.2051116443123893e-05,1.2051116443123119e-05,0.0020738197713891572,0.002073819771389024,1.2022516382870913e-05,8.2976342972735512e-07 +10000,cable:2:core,cable:2:core,0.011217390349824954,0.043047632316487089,0.011217390349824954,0.043047632316487089,0.011119748056498867,0.042731634526618816,0.00033073950572559544,0.00033073950572559544,0.0074904637291431453,0.0074904637291431453,9.7642293326087592e-05,0.00031599778986827337 +10000,cable:2:core,cable:3:core,0.008034030912603558,0.004866651343792168,0.008034030912603558,0.004866651343792168,0.0080281876013619698,0.0048722508072881719,8.0931006239359349e-06,8.0931006239359349e-06,0.0008617945279457077,0.0008617945279457077,5.8433112415882255e-06,5.5994634960039591e-06 +10000,cable:3:core,cable:1:core,0.0040559166332564877,0.0011450309278026654,0.0040559166332579232,0.0011450309278046009,0.0040495802099877439,0.0011473882269552962,6.7607040420904923e-06,6.7607040414199649e-06,0.0016062531647126471,0.0016062531645533388,6.3364232687438543e-06,2.3572991526307589e-06 +10000,cable:3:core,cable:2:core,0.0080340309126035597,0.004866651343792168,0.0080340309126035597,0.004866651343792168,0.008028187601361968,0.0048722508072881745,8.0931006239402395e-06,8.0931006239402395e-06,0.0008617945279461661,0.0008617945279461661,5.8433112415916949e-06,5.5994634960065612e-06 +10000,cable:3:core,cable:3:core,0.01121739034987806,0.043047632316466661,0.01121739034987806,0.043047632316466661,0.011119703022718263,0.042733943760656479,0.0003285472932987013,0.0003285472932987013,0.0074404406547007801,0.0074404406547007801,9.7687327159796597e-05,0.00031368855581018207 +100000,cable:1:core,cable:1:core,0.025029445696535337,0.51501770227425026,0.02502944569653533,0.51501770227425026,0.024745328786787053,0.51190764993067117,0.0031230030416582472,0.0031230030416582477,0.006093600401213776,0.0060936004012137768,0.00028411690974828457,0.0031100523435790839 +100000,cable:1:core,cable:2:core,0.0029200092671962073,-0.0055306020499575189,0.0029200092671962086,-0.0055306020499575215,0.0029228998850158476,-0.0055351845500160842,5.4180234556469762e-06,5.4180234556498713e-06,0.00086556509062880134,0.00086556509062926386,2.8906178196402496e-06,4.5825000585652392e-06 +100000,cable:1:core,cable:3:core,0.00087908935478322992,-0.0031017341595444893,0.00087908935478321442,-0.0031017341595444793,0.00087257166385033315,-0.0031045060939019955,7.0826488815231745e-06,7.0826488815335388e-06,0.0021963059409219042,0.0021963059409251182,6.5176909328967716e-06,2.7719343575062332e-06 +100000,cable:2:core,cable:1:core,0.0029200092671962078,-0.0055306020499575198,0.0029200092671962091,-0.0055306020499575207,0.0029228998850158493,-0.0055351845500160824,5.4180234556469363e-06,5.4180234556483644e-06,0.00086556509062879505,0.00086556509062902316,2.8906178196415507e-06,4.5825000585626371e-06 +100000,cable:2:core,cable:2:core,0.091870181209119314,0.28525842189891248,0.091870181209119314,0.28525842189891248,0.091488785921329557,0.28371125771885292,0.0015934802683458705,0.0015934802683458705,0.0053454955773149159,0.0053454955773149159,0.00038139528778975695,0.0015471641800595681 +100000,cable:2:core,cable:3:core,0.0087282162587741137,-0.0087087925865141323,0.0087282162587741137,-0.0087087925865141323,0.0087323644231482554,-0.008720517818696474,1.2437376628723592e-05,1.2437376628723592e-05,0.0010078050383296327,0.0010078050383296327,4.1481643741417273e-06,1.1725232182341686e-05 +100000,cable:3:core,cable:1:core,0.000879089354783231,-0.0031017341595444893,0.00087908935478323122,-0.0031017341595444919,0.00087257166385033271,-0.0031045060939019959,7.0826488815342859e-06,7.0826488815351049e-06,0.0021963059409253497,0.0021963059409256039,6.5176909328982895e-06,2.7719343575066668e-06 +100000,cable:3:core,cable:2:core,0.008728216258774112,-0.0087087925865141306,0.008728216258774112,-0.0087087925865141306,0.0087323644231482572,-0.0087205178186964827,1.2437376628734561e-05,1.2437376628734561e-05,0.0010078050383305211,0.0010078050383305211,4.1481643741451968e-06,1.1725232182352094e-05 +100000,cable:3:core,cable:3:core,0.091870181208271853,0.2852584218993296,0.091870181208271853,0.2852584218993296,0.091493043570979815,0.28369441705329945,0.0016088331037954157,0.0016088331037954157,0.0053972647983758573,0.0053972647983758573,0.0003771376372920382,0.0015640048460301426 +1000000,cable:1:core,cable:1:core,0.094696109689700492,4.9430140482755665,0.094696109689700422,4.9430140482755665,0.093180086583616706,4.9164027228102034,0.026654473716088627,0.026654473716088631,0.0054205664587009313,0.0054205664587009322,0.0015160231060837859,0.026611325465363045 +1000000,cable:1:core,cable:2:core,0.00017862565014624984,0.00022534477815653253,0.00017862565014624968,0.00022534477815653394,0.00018018320619215134,0.00022597300058552257,1.6794773759742062e-06,1.6794773759745825e-06,0.0058110348053217161,0.0058110348053230189,1.5575560459015012e-06,6.2822242899004325e-07 +1000000,cable:1:core,cable:3:core,8.1257499331941055e-05,8.9426642061850282e-05,8.1257499331935308e-05,8.9426642061852518e-05,8.1906550548926487e-05,8.9450810318331917e-05,6.4950102917469219e-07,6.4950102916903306e-07,0.0053551492705499453,0.0053551492705032856,6.4905121698543254e-07,2.4168256481635385e-08 +1000000,cable:2:core,cable:1:core,0.00017862565014624982,0.00022534477815653247,0.00017862565014624982,0.00022534477815653239,0.00018018320619215113,0.00022597300058552262,1.6794773759744775e-06,1.6794773759744472e-06,0.0058110348053226563,0.0058110348053225513,1.5575560459013115e-06,6.2822242899015167e-07 +1000000,cable:2:core,cable:2:core,0.75407440792448743,1.5740479450662614,0.75407440792448743,1.5740479450662614,0.75377952051842911,1.568277138147143,0.0057783363591952103,0.0057783363591952103,0.003320840874799719,0.003320840874799719,0.00029488740605831953,0.0057708069191184652 +1000000,cable:2:core,cable:3:core,0.0001535709764889112,0.0010629809235061854,0.0001535709764889112,0.0010629809235061854,0.00015808159292494346,0.001064814054945651,4.8688840104649706e-06,4.8688840104649706e-06,0.0045229487804855561,0.0045229487804855561,4.5106164360322605e-06,1.8331314394655612e-06 +1000000,cable:3:core,cable:1:core,8.1257499331941041e-05,8.9426642061850268e-05,8.1257499331935254e-05,8.9426642061852396e-05,8.1906550548926474e-05,8.9450810318331931e-05,6.4950102917473783e-07,6.4950102916903412e-07,0.0053551492705503209,0.0053551492705032943,6.4905121698543254e-07,2.416825648166249e-08 +1000000,cable:3:core,cable:2:core,0.00015357097648891114,0.0010629809235061854,0.00015357097648891114,0.0010629809235061854,0.00015808159292494367,0.0010648140549456518,4.8688840104655483e-06,4.8688840104655483e-06,0.0045229487804860887,0.0045229487804860887,4.5106164360325315e-06,1.8331314394664285e-06 +1000000,cable:3:core,cable:3:core,0.75407440792450831,1.5740479450662672,0.75407440792450831,1.5740479450662672,0.75369515541135013,1.5684962544665286,0.005564629456123976,0.005564629456123976,0.0031977265857614408,0.0031977265857614408,0.00037925251315817921,0.0055516905997385901 +10000000,cable:1:core,cable:1:core,1.2514510556301524,49.784417123982301,1.2514510556301526,49.784417123982301,0.29944048111231336,48.172201251501221,1.8723151853984727,1.8723151853984725,0.038866378646441546,0.038866378646441539,0.95201057451783899,1.6122158724810802 +10000000,cable:1:core,cable:2:core,1.5377810805855476e-09,4.4664472044009277e-10,1.5377810805855499e-09,4.4664472044009375e-10,3.6173164098261593e-09,-8.4488955667334859e-10,2.447964046819069e-09,2.4479640468190699e-09,0.65899803163799786,0.65899803163799808,2.0795353292406115e-09,1.2915342771134414e-09 +10000000,cable:1:core,cable:3:core,6.8259441253474777e-10,1.2859327231771482e-10,6.8259441253474715e-10,1.285932723177128e-10,1.7322626704311287e-09,-2.8699699888026061e-10,1.1289458468631831e-09,1.1289458468631833e-09,0.64295298585397354,0.64295298585397365,1.0496682578963809e-09,4.1559027119797546e-10 +10000000,cable:2:core,cable:1:core,1.5377810805855464e-09,4.4664472044009266e-10,1.5377810805855464e-09,4.4664472044009209e-10,3.6173164098261606e-09,-8.4488955667334734e-10,2.4479640468190711e-09,2.4479640468190715e-09,0.6589980316379983,0.65899803163799842,2.079535329240614e-09,1.29153427711344e-09 +10000000,cable:2:core,cable:2:core,4.5479347057971937,6.6714050104071516,4.5479347057971937,6.6714050104071516,4.5499969707899197,6.6606240142600166,0.010976466408856866,0.010976466408856866,0.0013607683966721531,0.0013607683966721531,0.0020622649927259928,0.010780996147135014 +10000000,cable:2:core,cable:3:core,3.0492342492444631e-09,1.314231864570851e-08,3.0492342492444631e-09,1.314231864570851e-08,1.6961332535262068e-08,2.3321441922614669e-08,1.7238359243450832e-08,1.7238359243450832e-08,0.59778475460050118,0.59778475460050118,1.3912098286017606e-08,1.0179123276906159e-08 +10000000,cable:3:core,cable:1:core,6.8259441253474497e-10,1.2859327231771355e-10,6.8259441253474539e-10,1.2859327231771213e-10,1.7322626704311266e-09,-2.8699699888026098e-10,1.1289458468631827e-09,1.1289458468631835e-09,0.64295298585397409,0.64295298585397453,1.0496682578963815e-09,4.1559027119797453e-10 +10000000,cable:3:core,cable:2:core,3.0492342492444647e-09,1.314231864570851e-08,3.0492342492444647e-09,1.314231864570851e-08,1.6961332535262058e-08,2.3321441922614619e-08,1.7238359243450792e-08,1.7238359243450792e-08,0.59778475460050073,0.59778475460050073,1.3912098286017593e-08,1.0179123276906109e-08 +10000000,cable:3:core,cable:3:core,4.5479347057971937,6.6714050104071516,4.5479347057971937,6.6714050104071516,4.5501680333556047,6.6604697518265077,0.011160987062473668,0.011160987062473668,0.0013836489863149047,0.0013836489863149047,0.0022333275584109913,0.010935258580643925 diff --git a/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/air_2-Y.csv b/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/air_2-Y.csv new file mode 100644 index 000000000..3de0c5327 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/air_2-Y.csv @@ -0,0 +1,82 @@ +frequency_hz,receiver,source,new_real,new_imaginary,old_real,old_imaginary,fem_real,fem_imaginary,before_absolute_error,after_absolute_error,before_relative_error,after_relative_error,real_absolute_error,imaginary_absolute_error +0.10000000000000001,cable:1:core,cable:1:core,8.7355481512572375,0.15942652432646789,8.7355481508841919,0.15942652467188378,8.7845877913155999,0.16072503167449181,0.04905682882206721,0.049056828458295294,0.0055834856560000087,0.0055834856145966956,0.049039640058362366,0.0012985073480239195 +0.10000000000000001,cable:1:core,cable:2:core,-8.9303183661905415e-14,-3.8246960722041443e-12,-8.930318368991909e-14,-3.8246960722403318e-12,-9.0062134384187483e-14,-3.8506433401169452e-12,2.595836505670303e-14,2.5958365093694138e-14,0.0067394628803151366,0.0067394628899189847,7.5895072228206821e-16,2.5947267912800873e-14 +0.10000000000000001,cable:1:core,cable:3:core,-6.231688185633419,0.15909201351190405,-6.23168818523399,0.15909201315618696,-6.2698668441098802,0.16036739685172985,0.038199955459822722,0.038199955048740124,0.0060906343289310544,0.0060906342633876851,0.03817865847646118,0.0012753833398257974 +0.10000000000000001,cable:2:core,cable:1:core,-6.4528599342827461e-14,3.5086158546484902e-13,-1.1328091516975891e-13,-3.80398025758764e-12,-9.0062134384187546e-14,-3.8506433401169419e-12,5.2120581849254745e-14,4.2015825115186966e-12,0.013531850943084534,1.0908394774904977,2.5533535041360085e-14,4.2015049255817909e-12 +0.10000000000000001,cable:2:core,cable:2:core,-8.1493030322248482e-25,9.0759067020569655e-12,2.4642676020193662e-24,9.0759067020569396e-12,2.4947508256144476e-24,9.1342496320507243e-12,5.8342929993784682e-14,5.8342929993758832e-14,0.0063872712421902738,0.0063872712421874436,3.3096811288369321e-24,5.8342929993758832e-14 +0.10000000000000001,cable:2:core,cable:3:core,-6.4528599342827423e-14,3.5086158546484866e-13,-1.132809151697592e-13,-3.8039802575876384e-12,-9.0049454962162604e-14,-3.8504700965498247e-12,5.1971202315389685e-14,4.2014091941123505e-12,0.013493675951093551,1.0908436148784155,2.5520855619335181e-14,4.2013316820146737e-12 +0.10000000000000001,cable:3:core,cable:1:core,-6.2316881856334199,0.15909201351190413,-6.2316881852339892,0.15909201315618723,-6.2698668441098802,0.16036739685172893,0.038199955459823569,0.038199955048739208,0.0060906343289311889,0.0060906342633875385,0.038178658476460292,0.0012753833398247982 +0.10000000000000001,cable:3:core,cable:2:core,-8.9303183661905743e-14,-3.8246960722041483e-12,-8.9303183689918913e-14,-3.8246960722403375e-12,-9.0049454962163198e-14,-3.8504700965498231e-12,2.5784825962525592e-14,2.5784825999510348e-14,0.006694709194569372,0.006694709204172004,7.4627130025745497e-16,2.5774024345674792e-14 +0.10000000000000001,cable:3:core,cable:3:core,8.7355481512572375,0.15942652432646795,8.7355481508841919,0.15942652467188365,8.784235974623094,0.16067971187912994,0.048703949106892831,0.048703948742858542,0.0055435445203254868,0.0055435444788906486,0.048687823365856531,0.0012531875526619884 +1,cable:1:core,cable:1:core,9.0103275798327527,0.23994395865214355,9.0103275798336604,0.23994395866859686,9.0597588683067194,0.24164199336163972,0.04946044482134742,0.049460444822819465,0.0054574146988789086,0.0054574146990413325,0.049431288473966717,0.0016980347094961712 +1,cable:1:core,cable:2:core,-1.3006519885623142e-12,-3.6714104007700747e-11,-1.3006519886930481e-12,-3.6714104007694498e-11,-1.3103320429960825e-12,-3.6963269005067141e-11,2.4935296141618038e-13,2.493529614150112e-13,0.0067417314047761917,0.0067417314047445807,9.680054433768301e-15,2.4916499736639444e-13 +1,cable:1:core,cable:3:core,-5.9573061454890368,0.23724688395954213,-5.9573061454881913,0.23724688397599955,-5.9951678453843966,0.23896046575464569,0.037900457537375781,0.037900457537275188,0.0063168184094564281,0.0063168184094396628,0.037861699895359813,0.0017135817951035581 +1,cable:2:core,cable:1:core,-1.4362301504123105e-12,4.7159331155725707e-12,-2.1679704977038157e-12,-3.5992194033796974e-11,-1.3103320429960805e-12,-3.6963269005067161e-11,1.2955810738123898e-12,4.1679392267001575e-11,0.03502849760896351,1.1268816146500233,1.2589810741622997e-13,4.1679202120639733e-11 +1,cable:2:core,cable:2:core,-8.8334396494623505e-23,9.0759067020561554e-11,2.3489444218284539e-22,9.0759067020557561e-11,2.4083147716325383e-22,9.1328799511734699e-11,5.6973249117713817e-13,5.6973249117314444e-13,0.0062382566531374818,0.0062382566530937529,3.2916587365787733e-22,5.6973249117314444e-13 +1,cable:2:core,cable:3:core,-1.4362301504123115e-12,4.7159331155725715e-12,-2.1679704977038193e-12,-3.5992194033796994e-11,-1.310449662228344e-12,-3.6964615098722244e-11,1.2965125185608181e-12,4.1680737999408822e-11,0.035052402136222974,1.126876886087925,1.2578048818396747e-13,4.1680548214294817e-11 +1,cable:3:core,cable:1:core,-5.9573061454890368,0.23724688395954194,-5.9573061454881913,0.23724688397599972,-5.9951678453843975,0.2389604657546453,0.037900457537376649,0.037900457537276069,0.006316818409456572,0.0063168184094398085,0.037861699895360701,0.0017135817951033638 +1,cable:3:core,cable:2:core,-1.3006519885623144e-12,-3.6714104007700728e-11,-1.3006519886930526e-12,-3.6714104007694511e-11,-1.3104496622283438e-12,-3.6964615098722231e-11,2.5070261493371604e-13,2.5070261493261335e-13,0.0067779745659640941,0.0067779745659342821,9.7976736660293829e-15,2.5051109102150389e-13 +1,cable:3:core,cable:3:core,9.0103275798327545,0.23994395865214355,9.0103275798336639,0.23994395866859713,9.060004071530674,0.24168574952705965,0.049707018244084328,0.049707018245569813,0.0054844723188977439,0.0054844723190616458,0.049676491697919545,0.0017417908749161048 +10,cable:1:core,cable:1:core,9.4348014435196959,0.40409142612742754,9.4348014435331127,0.40409142627673555,9.4854196678519962,0.40656122474153245,0.050678442533818306,0.050678442554495662,0.0053378719151271972,0.005337871917305107,0.050618224332300343,0.0024697986141049011 +10,cable:1:core,cable:2:core,-2.0095834205662074e-11,-3.4435183438783357e-10,-2.0095834217647568e-11,-3.4435183438689684e-10,-2.022272642239479e-11,-3.4684426740418731e-10,2.4956610301291435e-12,2.4956610298030287e-12,0.007183136914216663,0.0071831369132780232,1.2689221673271646e-13,2.4924330163537337e-12 +10,cable:1:core,cable:3:core,-5.5367765108432021,0.3835905954781802,-5.5367765108299043,0.38359059562742254,-5.5745968707941813,0.38597568113192759,0.037895491297663474,0.037895491293785118,0.0067816531016595787,0.006781653100965521,0.03782035995097921,0.0023850856537473986 +10,cable:2:core,cable:1:core,-4.0296701053309782e-11,7.5606278096443942e-11,-5.2503321288609543e-11,-3.1854092073286003e-10,-2.0222726422394722e-11,-3.4684426740418726e-10,4.2931529645634855e-11,4.2292721342006767e-10,0.1235676847369728,1.2172903459518589,2.007397463091506e-11,4.2245054550063118e-10 +10,cable:2:core,cable:2:core,-1.0463443661702291e-20,9.0759067020308551e-10,2.1161807741033108e-20,9.0759067020246181e-10,2.2848695838332346e-20,9.1328824037484855e-10,5.6975701723867369e-12,5.6975701717630428e-12,0.0062385235246741333,0.0062385235239912221,3.3312139500034635e-20,5.6975701717630428e-12 +10,cable:2:core,cable:3:core,-4.0296701053309762e-11,7.5606278096443968e-11,-5.2503321288609504e-11,-3.1854092073285998e-10,-2.0222374189272243e-11,-3.4681757442413444e-10,4.2914201490957232e-11,4.2290056724645286e-10,0.12352729165793205,1.2173070894390361,2.007432686403752e-11,4.2242385252057842e-10 +10,cable:3:core,cable:1:core,-5.5367765108431994,0.38359059547818058,-5.5367765108299016,0.38359059562742254,-5.574596870794184,0.38597568113192704,0.037895491297668754,0.037895491293790377,0.0067816531016605206,0.0067816531009664586,0.037820359950984539,0.0023850856537464549 +10,cable:3:core,cable:2:core,-2.0095834205662174e-11,-3.4435183438783378e-10,-2.009583421764763e-11,-3.4435183438689653e-10,-2.0222374189272117e-11,-3.4681757442413371e-10,2.468984871491344e-12,2.4689848711696051e-12,0.0071069017650019616,0.0071069017640758457,1.2653998360994283e-13,2.4657400362999313e-12 +10,cable:3:core,cable:3:core,9.4348014435196976,0.40409142612742743,9.4348014435331109,0.40409142627673539,9.4853870112633505,0.40654933037552476,0.05064524612586558,0.050645246146509226,0.0053343940132645172,0.0053343940154388838,0.050585567743652859,0.0024579042480973312 +100,cable:1:core,cable:1:core,10.176084148735754,0.81933149057631394,10.176084148956001,0.81933149183126064,10.22636063424096,0.82372131363032075,0.05046776702050737,0.05046776734907818,0.0049191340818300934,0.0049191341138561566,0.050276485505206026,0.0043898230540068139 +100,cable:1:core,cable:2:core,-3.35237597421708e-10,-3.0841680821338702e-09,-3.3523759845478488e-10,-3.0841680819833742e-09,-3.3701458198409536e-10,-3.1088451762976369e-09,2.474099137582113e-11,2.4740991299912982e-11,0.0079119045809081766,0.0079119045566335613,1.7769845623873643e-12,2.4677094163766667e-11 +100,cable:1:core,cable:3:core,-4.8334558332936481,0.67829861957804283,-4.8334558330735282,0.67829862083298809,-4.8688445230258699,0.68189461716195865,0.035570923043139302,0.035570922951014161,0.0072352105591345283,0.0072352105403960558,0.035388689732221756,0.003595997583915822 +100,cable:2:core,cable:1:core,-1.3380887052430523e-09,1.5974656850471151e-09,-1.581746719695652e-09,-2.122011708447857e-09,-3.3701458198409598e-10,-3.1088451762976394e-09,1.5884578646978357e-09,4.8116017420262758e-09,0.50797184580746435,1.5386988049899819,1.0010741232589563e-09,4.7063108613447547e-09 +100,cable:2:core,cable:2:core,-1.5539876856874734e-18,9.0759067010079656e-09,1.4981391520430641e-18,9.0759067008968554e-09,2.1048044610369403e-18,9.1328214272502451e-09,5.6914726353389725e-11,5.6914726242279626e-11,0.0062318886673475554,0.0062318886551815337,3.6587921467244138e-18,5.691472624227951e-11 +100,cable:2:core,cable:3:core,-1.3380887052430521e-09,1.5974656850471154e-09,-1.5817467196956539e-09,-2.1220117084478578e-09,-3.3697609988902282e-10,-3.1080993569352719e-09,1.5880247940234506e-09,4.8108802534385988e-09,0.50795447214884915,1.5388350036495955,1.0011126053540294e-09,4.7055650419823869e-09 +100,cable:3:core,cable:1:core,-4.8334558332936481,0.67829861957804294,-4.8334558330735273,0.67829862083298798,-4.8688445230258717,0.68189461716195954,0.035570923043142057,0.035570922951016007,0.0072352105591350861,0.0072352105403964279,0.035388689732223533,0.0035959975839165992 +100,cable:3:core,cable:2:core,-3.352375974217081e-10,-3.0841680821338723e-09,-3.3523759845478545e-10,-3.0841680819833763e-09,-3.3697609988902432e-10,-3.1080993569352732e-09,2.3994339083628422e-11,2.3994339008379249e-11,0.0076749631930526302,0.0076749631689830098,1.7385024673162168e-12,2.3931274801400896e-11 +100,cable:3:core,cable:3:core,10.176084148735756,0.81933149057631427,10.176084148956003,0.81933149183126097,10.225799294745068,0.82356401370451804,0.049894989349989934,0.04989498967589838,0.00486357621332503,0.0048635762450933619,0.04971514600931215,0.004232523128203769 +1000,cable:1:core,cable:1:core,11.809017595800965,2.0929924652253051,11.809017599813723,2.0929924735851584,11.859420137790089,2.1041245858916642,0.051617242844457005,0.051617248565724347,0.0042854973213296817,0.0042854977963352131,0.050402541989123861,0.011132120666359047 +1000,cable:1:core,cable:2:core,-6.3490118092788199e-09,-2.4635775208045401e-08,-6.3490118848034141e-09,-2.4635775180730038e-08,-6.3749708759463428e-09,-2.4842626342602489e-08,2.0847367313198535e-10,2.0847365543349496e-10,0.0081284079682633365,0.0081284072781975455,2.5959066667522964e-11,2.0685113455708837e-10 +1000,cable:1:core,cable:3:core,-3.526913027220937,1.2832753628650815,-3.5269130232082677,1.2832753712249108,-3.5571333077239724,1.2895798933132263,0.030870902075776783,0.030870899854939855,0.008158968085081604,0.0081589674981296066,0.030220280503035379,0.0063045304481448294 +1000,cable:2:core,cable:1:core,-4.6439763000069587e-08,4.7556279357279568e-08,-5.2859299514753883e-08,1.5103867257715344e-08,-6.3749708759463561e-09,-2.4842626342602513e-08,6.1290416542563131e-08,8.2745326843814112e-08,2.3897190600529248,3.2262480146737502,4.006479212412323e-08,7.239890569988208e-08 +1000,cable:2:core,cable:2:core,-3.6835580752459175e-16,9.0759066520812581e-08,-8.3130691160873046e-17,9.0759066493688587e-08,1.8282901688147461e-16,9.1265820515709644e-08,5.0675402202112665e-10,5.0675399489736317e-10,0.0055525060658814631,0.0055525057686862658,5.5118482440606636e-16,5.0675399489706342e-10 +1000,cable:2:core,cable:3:core,-4.6439763000069607e-08,4.7556279357279608e-08,-5.2859299514753903e-08,1.5103867257715347e-08,-6.3743632497743299e-09,-2.4841748432286945e-08,6.1290305207370033e-08,8.2744852921313551e-08,2.3898080272141291,3.2263555068449761,4.006539975029528e-08,7.2398027789566556e-08 +1000,cable:3:core,cable:1:core,-3.526913027220937,1.2832753628650815,-3.5269130232082677,1.2832753712249112,-3.5571333077239737,1.2895798933132268,0.030870902075778087,0.03087089985494125,0.0081589680850819458,0.0081589674981299726,0.030220280503036712,0.0063045304481452735 +1000,cable:3:core,cable:2:core,-6.3490118092788257e-09,-2.4635775208045421e-08,-6.3490118848034166e-09,-2.4635775180730051e-08,-6.374363249774339e-09,-2.4841748432286941e-08,2.0752752121780618e-10,2.0752750333304077e-10,0.0080918333559632325,0.0080918326586073321,2.5351440495513369e-11,2.0597322424152052e-10 +1000,cable:3:core,cable:3:core,11.809017595800968,2.0929924652253051,11.809017599813725,2.0929924735851584,11.859496963660357,2.1039752851673796,0.051660316729683735,0.051660322427986148,0.004289055860946459,0.0042890563340433913,0.050479367859388447,0.010982819942074507 +10000,cable:1:core,cable:1:core,16.80529161487884,5.0791514856025755,16.805291651222536,5.0791514818981653,16.875302666406174,5.1182930535137015,0.080209755488829332,0.080209785403679817,0.0045484771326271649,0.0045484788290169938,0.070011051527334445,0.039141567911125996 +10000,cable:1:core,cable:2:core,-1.1301307458540772e-07,-1.0140418883622688e-07,-1.1301307810588102e-07,-1.0140418600667815e-07,-1.1385708023724294e-07,-1.0269428493685997e-07,1.5416532837832709e-09,1.5416528432816889e-09,0.010054594170852258,0.010054591297920525,8.4400565183521943e-10,1.2900961006330971e-09 +10000,cable:1:core,cable:3:core,-0.67949764805676527,1.7210202706210376,-0.67949761171491874,1.7210202669171364,-0.69013383495049541,1.7365328090121379,0.01880872688318418,0.018808703277305393,0.010065440776585359,0.010065428143961344,0.010636186893730137,0.015512538391100339 +10000,cable:2:core,cable:1:core,-7.4501716373140391e-07,1.9267131819503934e-06,-9.0657832770881726e-07,1.7798079901742443e-06,-1.1385708023724289e-07,-1.0269428493685991e-07,2.0426017213327153e-06,2.1252900314202047e-06,13.321757607057339,13.861047088906993,6.3116008349416106e-07,2.0294074668872532e-06 +10000,cable:2:core,cable:2:core,-1.5910507586317832e-13,9.0759044455228302e-07,-1.3778400723881355e-13,9.0759043608914856e-07,1.2129066978187954e-14,9.1282299047690203e-07,5.232554389900984e-09,5.2325459274208202e-09,0.0057322771714669986,0.0057322679007976007,1.7123414284136627e-13,5.2325459246190164e-09 +10000,cable:2:core,cable:3:core,-7.4501716373140391e-07,1.9267131819503934e-06,-9.0657832770881716e-07,1.7798079901742439e-06,-1.1385646462248044e-07,-1.0268645836610756e-07,2.0425947471293261e-06,2.1252827407703683e-06,13.322207291965903,13.861514755372875,6.311606991089235e-07,2.029399640316501e-06 +10000,cable:3:core,cable:1:core,-0.67949764805676138,1.721020270621038,-0.67949761171491496,1.7210202669171399,-0.69013383495049552,1.7365328090121384,0.018808726883183816,0.018808703277307652,0.010065440776585161,0.01006542814396255,0.010636186893734134,0.015512538391100339 +10000,cable:3:core,cable:2:core,-1.1301307505740721e-07,-1.0140418884653474e-07,-1.1301307631509544e-07,-1.0140418535528041e-07,-1.1385646462248041e-07,-1.026864583661075e-07,1.5347729184896534e-09,1.5347706927419842e-09,0.010010092797384488,0.010010078280617582,8.4338956507320005e-10,1.282269519572757e-09 +10000,cable:3:core,cable:3:core,16.805291614878836,5.0791514856025746,16.805291651222536,5.0791514818981671,16.874517717130121,5.1178992113719639,0.079332431832233208,0.079332461736701429,0.0044989472947478187,0.0044989489906321284,0.069226102251285226,0.038747725769389341 +100000,cable:1:core,cable:1:core,25.147933625985093,8.0354712506157853,25.147933620553914,8.0354712542214894,25.246943314395164,8.093523757030372,0.11477374506790895,0.11477374220646004,0.0043290408174478665,0.0043290407095196194,0.099009688410070851,0.058052506414586702 +100000,cable:1:core,cable:2:core,-1.3387580438105773e-07,1.5904860038167041e-07,-1.3387578410475514e-07,1.5904860038606757e-07,-1.3577893758098125e-07,1.6103548998290223e-07,2.7513130363666894e-09,2.7512990139449113e-09,0.013061810300911754,0.01306174372971073,1.9031331999235123e-09,1.9868896012318186e-09 +100000,cable:1:core,cable:3:core,0.083093302642308284,-0.071868316820341893,0.083093297211124295,-0.071868313214637519,0.083673556585100742,-0.072751432863992438,0.0010566935488419962,0.0010566875530066511,0.0095301959990094828,0.0095301419232690938,0.00058025394279245801,0.00088311604365054475 +100000,cable:2:core,cable:1:core,1.708396352158113e-05,8.1698742789972538e-06,1.7003931797506463e-05,8.4429904748540932e-06,-1.3577893758098135e-07,1.6103548998290223e-07,1.9035767976467474e-05,1.8991077618405211e-05,90.371974019044487,90.159807329331869,1.7219742459162112e-05,8.0088387890143524e-06 +100000,cable:2:core,cable:2:core,-5.5545680886170519e-11,9.0758164887964398e-06,-5.4864148189373549e-11,9.0758159057763065e-06,3.4007959212112756e-13,9.1282698216414564e-06,5.2453944914515099e-08,5.2453362616407385e-08,0.0057463184085724947,0.0057462546179397576,5.5885760478291646e-11,5.2453332845016625e-08 +100000,cable:2:core,cable:3:core,1.7083963521581134e-05,8.1698742789972572e-06,1.7003931797506473e-05,8.4429904748541e-06,-1.3573623352103367e-07,1.6102087150634743e-07,1.9035735886080225e-05,1.8991045062306193e-05,90.388429427122801,90.176221535874802,1.7219699755102169e-05,8.0088534074909104e-06 +100000,cable:3:core,cable:1:core,0.083093302642306133,-0.071868316820346778,0.083093297211126876,-0.071868313214642834,0.083673556585100728,-0.07275143286399241,0.0010566935488361059,0.0010566875530037189,0.0095301959989563621,0.0095301419232426531,0.00058025394279459519,0.00088311604364563201 +100000,cable:3:core,cable:2:core,-1.3387580348475153e-07,1.5904859744381848e-07,-1.3387578295842361e-07,1.5904859727845923e-07,-1.357362335210337e-07,1.6102087150634751e-07,2.7112989370241977e-09,2.7112847319353321e-09,0.012874209544178996,0.012874142093375932,1.8604300362821695e-09,1.9722740625290317e-09 +100000,cable:3:core,cable:3:core,25.147933625985093,8.0354712506157835,25.147933620553918,8.0354712542214912,25.246885913353953,8.0934120106660856,0.11466772653085855,0.11466772366597382,0.0043250564878453013,0.0043250563797871001,0.09895228736885997,0.057940760050302131 +1000000,cable:1:core,cable:1:core,40.833765344235459,19.54915991569672,40.833765344235985,19.54915991569575,40.899774874633195,19.655440515981347,0.12511124689781913,0.12511124689727263,0.0027571137750741089,0.0027571137750620651,0.066009530397735716,0.10628060028462727 +1000000,cable:1:core,cable:2:core,-3.6994881866651507e-09,-1.4563318927035237e-08,-3.6994907304567155e-09,-1.4563314765271918e-08,-3.7856608852758189e-09,-1.4741395028839053e-08,1.9783294936306376e-10,1.9783031117472364e-10,0.012998459609595534,0.012998286269488681,8.6172698610668224e-11,1.7807610180381567e-10 +1000000,cable:1:core,cable:3:core,-2.7224978265894648e-05,-2.5263791755826295e-05,-2.7224977747745251e-05,-2.5263792729529432e-05,-2.7519306512046415e-05,-2.5393594081990994e-05,3.2167967389299072e-07,3.2167959270140555e-07,0.0085906666931782494,0.0085906645249037121,2.9432824615176719e-07,1.2980232616469897e-07 +1000000,cable:2:core,cable:1:core,-7.7982181077902926e-06,5.4374206625622219e-06,-7.8026985217373471e-06,5.4423466057092621e-06,-3.7856608852758205e-09,-1.4741395028839069e-08,9.5185530023719264e-06,9.5120580460592919e-06,625.40909965438709,624.98235361654213,7.794432446905017e-06,5.4521620575910613e-06 +1000000,cable:2:core,cable:2:core,-2.4740419503128501e-08,9.0711568120914278e-05,-2.4719343778092328e-08,9.0711547556388583e-05,1.0616793549703688e-11,9.1268397209087773e-05,5.5739851691683139e-07,5.5737890813510746e-07,0.0061072455960837924,0.0061070307486412647,2.4751036296678206e-08,5.5682908817349434e-07 +1000000,cable:2:core,cable:3:core,-7.7982181077902926e-06,5.4374206625622211e-06,-7.8026985217373471e-06,5.4423466057092638e-06,-3.7859611766932275e-09,-1.4744688010955839e-08,9.5185546442339522e-06,9.5120596874785329e-06,625.27510405166106,624.84844948982425,7.7944321466135989e-06,5.452165350573177e-06 +1000000,cable:3:core,cable:1:core,-2.7224978265894644e-05,-2.5263791755826299e-05,-2.7224977747745251e-05,-2.5263792729529435e-05,-2.7519306512046418e-05,-2.5393594081990998e-05,3.2167967389299379e-07,3.2167959270141174e-07,0.008590666693178331,0.0085906645249038752,2.9432824615177396e-07,1.2980232616469897e-07 +1000000,cable:3:core,cable:2:core,-3.6994881866651494e-09,-1.4563318927035239e-08,-3.6994907304567147e-09,-1.4563314765271921e-08,-3.7859611766932317e-09,-1.4744688010955837e-08,2.0093131244845993e-10,2.009286505369354e-10,0.013199204290386133,0.013199029429068665,8.6472990028082259e-11,1.813690839205985e-10 +1000000,cable:3:core,cable:3:core,40.833765344235466,19.549159915696727,40.833765344235985,19.54915991569575,40.898210042511892,19.654888417134927,0.12382098025959387,0.12382098025902959,0.0027287789812792582,0.0027287789812668224,0.064444698276425072,0.10572850143820034 +10000000,cable:1:core,cable:1:core,80.846531351828517,55.108195401013525,80.846531351828517,55.108195401013525,80.822020676031087,55.21746647896061,0.11198634605952913,0.11198634605952913,0.0011440785816614484,0.0011440785816614484,0.024510675797429826,0.1092710779470849 +10000000,cable:1:core,cable:2:core,-1.1003843828678816e-13,-1.3743099171584897e-13,-1.1003845571285191e-13,-1.3743080710577727e-13,-4.8478322004284232e-13,-3.0087161821462218e-13,4.0883558691564161e-13,4.08835529086825e-13,0.71655166917326008,0.71655156781873908,3.7474478175605415e-13,1.6344062649877321e-13 +10000000,cable:1:core,cable:3:core,-2.7205597053841646e-16,1.48022349643543e-17,-2.7205597045460625e-16,1.4802234773622071e-17,-5.1434173424526559e-12,-6.19833980581388e-12,8.0542872611412412e-12,8.0542872611413333e-12,0.99997984522328609,0.99997984522329753,5.1431452864821176e-12,6.1983546080488442e-12 +10000000,cable:2:core,cable:1:core,-9.3439526702823559e-11,5.4261360026858313e-10,-9.3440467076589953e-11,5.4261406955040871e-10,-4.8478322004284232e-13,-3.0087161821462192e-13,5.5081520722050515e-10,5.5081458597251225e-10,965.39432664727224,965.39323780789391,9.2954743482780711e-11,5.4291447188679773e-10 +10000000,cable:2:core,cable:2:core,-1.6761991871629861e-05,0.0008877789650905823,-1.6761328755330462e-05,0.00088777830561370815,3.9349460155433293e-10,0.00087974894643819731,1.8585638045362502e-05,1.8586520995702771e-05,0.021126070250618127,0.021127073889603824,1.6762385366231415e-05,8.0300186523849914e-06 +10000000,cable:2:core,cable:3:core,-9.3439526702823559e-11,5.4261360026858313e-10,-9.3440467076589953e-11,5.4261406955040871e-10,-4.7137805126024283e-13,-3.1558936686029454e-13,5.5083197623796477e-10,5.5083135497346052e-10,971.02459274786884,971.02349756251613,9.2968148651563312e-11,5.4292918963544343e-10 +10000000,cable:3:core,cable:1:core,-2.7205597053841637e-16,1.4802234964354346e-17,-2.7205597045460621e-16,1.4802234773622071e-17,-5.1434173424526615e-12,-6.1983398058138841e-12,8.0542872611412476e-12,8.0542872611413397e-12,0.99997984522328609,0.99997984522329753,5.1431452864821232e-12,6.1983546080488482e-12 +10000000,cable:3:core,cable:2:core,-1.100384382867881e-13,-1.3743099171584894e-13,-1.1003845571285191e-13,-1.3743080710577727e-13,-4.7137805126024222e-13,-3.1558936686029429e-13,4.0287315091000468e-13,4.0287308490133031e-13,0.71019794450428997,0.71019782814204357,3.613396129734541e-13,1.7815837514444535e-13 +10000000,cable:3:core,cable:3:core,80.846531351828517,55.108195401013525,80.846531351828517,55.108195401013525,80.821985061039811,55.21927686937088,0.11376121045378251,0.11376121045378251,0.0011621992327080089,0.0011621992327080089,0.02454629078870596,0.11108146835735511 diff --git a/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/air_2-Z.csv b/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/air_2-Z.csv new file mode 100644 index 000000000..aa77ca078 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/air_2-Z.csv @@ -0,0 +1,82 @@ +frequency_hz,receiver,source,new_real,new_imaginary,old_real,old_imaginary,fem_real,fem_imaginary,before_absolute_error,after_absolute_error,before_relative_error,after_relative_error,real_absolute_error,imaginary_absolute_error +0.10000000000000001,cable:1:core,cable:1:core,3.1374615586823512e-06,1.2435841206410953e-06,3.1374615586823521e-06,1.2435841206410949e-06,3.1569374848695469e-06,1.2375960715466406e-06,2.0375682388736233e-08,2.037568238873717e-08,0.0060090100516075971,0.0060090100516078729,1.9475926187195662e-08,5.9880490944547412e-09 +0.10000000000000001,cable:1:core,cable:2:core,9.8692096479705197e-08,7.1450042872923159e-07,9.86920964797057e-08,7.1450042872923138e-07,9.8559280935640084e-08,7.1180019648279383e-07,2.7034966531228829e-09,2.7034966531230698e-09,0.0037622173631328983,0.0037622173631331585,1.3281554406511331e-10,2.700232246437763e-09 +0.10000000000000001,cable:1:core,cable:3:core,9.9020834428301869e-08,7.2818833938952072e-07,9.9020834415946147e-08,7.2818833937590774e-07,9.8887228230869388e-08,7.254576929504737e-07,2.7339130405750742e-09,2.7339130547756163e-09,0.003734005367946915,0.0037340053873421552,1.3360619743248095e-10,2.7306464390470207e-09 +0.10000000000000001,cable:2:core,cable:1:core,9.8692096479705197e-08,7.1450042872923149e-07,9.8692096479705673e-08,7.1450042872923117e-07,9.8559280935640018e-08,7.1180019648279383e-07,2.7034966531226736e-09,2.7034966531229672e-09,0.0037622173631326073,0.0037622173631330158,1.3281554406517948e-10,2.7002322464376571e-09 +0.10000000000000001,cable:2:core,cable:2:core,3.1368030409187459e-06,1.2442491570135631e-06,3.1368030409187459e-06,1.2442491570135629e-06,3.1562805382084363e-06,1.238155489839502e-06,2.0408470797639916e-08,2.0408470797639982e-08,0.0060194028590558422,0.0060194028590558613,1.9477497289690336e-08,6.0936671740610731e-09 +0.10000000000000001,cable:2:core,cable:3:core,9.8692096479705184e-08,7.1450042872923138e-07,9.869209647970566e-08,7.1450042872923106e-07,9.8559278088182289e-08,7.118103566479389e-07,2.6933489427986132e-09,2.6933489427989069e-09,0.0037480432007590903,0.0037480432007594988,1.3281839152289454e-10,2.690072081292481e-09 +0.10000000000000001,cable:3:core,cable:1:core,9.9020834428301896e-08,7.2818833938952072e-07,9.9020834415946173e-08,7.2818833937590774e-07,9.8887228230869441e-08,7.2545769295047381e-07,2.7339130405749671e-09,2.7339130547755092e-09,0.003734005367946768,0.0037340053873420082,1.3360619743245448e-10,2.7306464390469148e-09 +0.10000000000000001,cable:3:core,cable:2:core,9.8692096479705197e-08,7.1450042872923159e-07,9.8692096479705687e-08,7.1450042872923138e-07,9.8559278088182408e-08,7.11810356647939e-07,2.69334894279882e-09,2.6933489427990074e-09,0.0037480432007593774,0.003748043200759638,1.3281839152278866e-10,2.6900720812925869e-09 +0.10000000000000001,cable:3:core,cable:3:core,3.1374615586823512e-06,1.2435841206410953e-06,3.1374615586823521e-06,1.2435841206410949e-06,3.1569374867215201e-06,1.2376681473795957e-06,2.0354618950440301e-08,2.0354618950441234e-08,0.0060027516412558962,0.0060027516412561712,1.9475928039168827e-08,5.9159732614996572e-09 +1,cable:1:core,cable:1:core,4.0462625840379406e-06,1.0981370487568926e-05,4.04626258403795e-06,1.0981370487568925e-05,4.0643495662368124e-06,1.0930986995002525e-05,5.353162848494773e-08,5.3531628484952468e-08,0.0045902079069174116,0.0045902079069178175,1.8086982198871778e-08,5.0383492566401846e-08 +1,cable:1:core,cable:2:core,9.8663957192484381e-07,5.698292513397585e-06,9.8663957192484614e-07,5.6982925133975858e-06,9.8528249805016315e-07,5.6801074022037254e-06,1.8235677081874812e-08,1.8235677081873796e-08,0.0031632095319905572,0.0031632095319903812,1.3570738746806621e-09,1.8185111193859563e-08 +1,cable:1:core,cable:3:core,9.9679391285692425e-07,5.8280008393635153e-06,9.9679391273336713e-07,5.828000839227387e-06,9.9541834849023757e-07,5.809535968222851e-06,1.8516037324468971e-08,1.8516037469400272e-08,0.0031414008226381077,0.003141400847226917,1.3755643666866825e-09,1.8464871140664282e-08 +1,cable:2:core,cable:1:core,9.8663957192484423e-07,5.6982925133975858e-06,9.8663957192484656e-07,5.6982925133975867e-06,9.8528249805016272e-07,5.6801074022037271e-06,1.8235677081874031e-08,1.8235677081873012e-08,0.0031632095319904206,0.0031632095319902441,1.3570738746815091e-09,1.8185111193858716e-08 +1,cable:2:core,cable:2:core,4.0258226362737272e-06,1.1002346949218491e-05,4.0258226362737365e-06,1.1002346949218488e-05,4.0439534803421229e-06,1.0950998663924164e-05,5.4455246848209535e-08,5.4455246848215835e-08,0.0046647348682673602,0.0046647348682679005,1.8130844068395749e-08,5.1348285294327398e-08 +1,cable:2:core,cable:3:core,9.8663957192484402e-07,5.6982925133975867e-06,9.8663957192484656e-07,5.6982925133975875e-06,9.8528570159125359e-07,5.6801179712998549e-06,1.8224899048895695e-08,1.822489904889466e-08,0.0031613339346576808,0.0031613339346575008,1.3538703335904317e-09,1.8174542097731795e-08 +1,cable:3:core,cable:1:core,9.9679391285692425e-07,5.8280008393635153e-06,9.9679391273336713e-07,5.828000839227387e-06,9.9541834849023778e-07,5.8095359682228527e-06,1.8516037324467267e-08,1.8516037469398565e-08,0.003141400822637818,0.0031414008472266264,1.3755643666864708e-09,1.8464871140662588e-08 +1,cable:3:core,cable:2:core,9.8663957192484381e-07,5.6982925133975858e-06,9.8663957192484614e-07,5.6982925133975858e-06,9.8528570159125359e-07,5.6801179712998583e-06,1.8224899048890593e-08,1.8224899048890421e-08,0.0031613339346567939,0.003161333934656764,1.3538703335902199e-09,1.817454209772756e-08 +1,cable:3:core,cable:3:core,4.0462625840379398e-06,1.0981370487568926e-05,4.04626258403795e-06,1.0981370487568925e-05,4.0643564026206317e-06,1.0930589072675371e-05,5.3908611274027899e-08,5.3908611274032908e-08,0.0046226801673670001,0.0046226801673674295,1.8093818582691863e-08,5.078141489355552e-08 +10,cable:1:core,cable:1:core,1.4059707866181092e-05,9.4699367070903535e-05,1.4059707866181184e-05,9.4699367070903468e-05,1.4071669118458445e-05,9.4290920843765721e-05,4.0862133084201546e-07,4.0862133084208587e-07,0.0042861563423491648,0.0042861563423499029,1.1961252277352996e-08,4.0844622713781423e-07 +10,cable:1:core,cable:2:core,9.8448383311020772e-06,4.2519807808567783e-05,9.8448383311021314e-06,4.2519807808567804e-05,9.8319503770437314e-06,4.2421165884598937e-05,9.9480292139152401e-08,9.948029213912523e-08,0.0022845064943293734,0.0022845064943287493,1.2887954058345795e-08,9.8641923968846373e-08 +10,cable:1:core,cable:3:core,1.0145003866507134e-05,4.3592250138396668e-05,1.0145003866507116e-05,4.3592250138396838e-05,1.0131530298472416e-05,4.3491727218213369e-05,1.0142186410137432e-07,1.014218641012089e-07,0.002271169784242826,0.0022711697842391215,1.3473568034717975e-08,1.0052292018329905e-07 +10,cable:2:core,cable:1:core,9.8448383311020755e-06,4.2519807808567763e-05,9.8448383311021297e-06,4.251980780856779e-05,9.831950377043728e-06,4.2421165884598896e-05,9.9480292139179506e-08,9.9480292139145598e-08,0.0022845064943299979,0.002284506494329219,1.2887954058347489e-08,9.8641923968866702e-08 +10,cable:2:core,cable:2:core,1.3443861420288121e-05,9.5357297592647101e-05,1.3443861420288216e-05,9.5357297592647074e-05,1.3457280759510354e-05,9.4945816929939975e-05,4.1169942245166172e-07,4.1169942245169195e-07,0.0042932417524727956,0.0042932417524731105,1.3419339222232337e-08,4.114806627071257e-07 +10,cable:2:core,cable:3:core,9.8448383311020755e-06,4.2519807808567763e-05,9.8448383311021314e-06,4.251980780856779e-05,9.8319495296934822e-06,4.2421085994381516e-05,9.9559619314161443e-08,9.9559619314127337e-08,0.0022863322926298967,0.0022863322926291131,1.2888801408593285e-08,9.8721814186246599e-08 +10,cable:3:core,cable:1:core,1.0145003866507133e-05,4.3592250138396662e-05,1.0145003866507114e-05,4.3592250138396831e-05,1.0131530298472415e-05,4.349172721821339e-05,1.0142186410134746e-07,1.0142186410118203e-07,0.0022711697842422232,0.0022711697842385187,1.3473568034717975e-08,1.0052292018327194e-07 +10,cable:3:core,cable:2:core,9.8448383311020755e-06,4.2519807808567777e-05,9.8448383311021297e-06,4.2519807808567797e-05,9.8319495296934873e-06,4.242108599438155e-05,9.955961931413369e-08,9.9559619314106519e-08,0.0022863322926292575,0.0022863322926286334,1.2888801408588203e-08,9.8721814186226271e-08 +10,cable:3:core,cable:3:core,1.4059707866181092e-05,9.4699367070903535e-05,1.4059707866181184e-05,9.4699367070903468e-05,1.4071863013141068e-05,9.4295511143946075e-05,4.0403880672063024e-07,4.040388067207007e-07,0.0042378857327233267,0.0042378857327240657,1.2155146959975677e-08,4.0385592695745985e-07 +100,cable:1:core,cable:1:core,0.00011750705949953433,0.00077809535148733427,0.00011750705949953411,0.00077809535148733395,0.00011823590195980419,0.00077395987792192587,4.1992086090216516e-06,4.1992086090219345e-06,0.0053633911681116551,0.005363391168112016,7.2884246026986383e-07,4.1354735654084047e-06 +100,cable:1:core,cable:2:core,9.6940022347522206e-05,0.00028095440727846668,9.6940022347522735e-05,0.00028095440727846685,9.6825542710659164e-05,0.0002806142357194754,3.5891820349919017e-07,3.5891820349886745e-07,0.0012090921092759823,0.0012090921092748951,1.1447963686304246e-07,3.4017155899128361e-07 +100,cable:1:core,cable:3:core,0.00010470316710127832,0.00028484344129470137,0.00010470316710127808,0.00028484344129470283,0.00010457534806939449,0.00028449911915146356,3.672811501241812e-07,3.6728115012289393e-07,0.0012117085875665494,0.0012117085875623026,1.2781903188382549e-07,3.4432214323780767e-07 +100,cable:2:core,cable:1:core,9.6940022347522192e-05,0.00028095440727846668,9.6940022347522721e-05,0.00028095440727846685,9.6825542710659096e-05,0.0002806142357194754,3.5891820349920742e-07,3.589182034988847e-07,0.0012090921092760404,0.0012090921092749532,1.1447963686309667e-07,3.4017155899128361e-07 +100,cable:2:core,cable:2:core,0.00010028978112835039,0.00079835650985762038,0.00010028978112835013,0.00079835650985761984,0.00010105820712052285,0.00079409519275426034,4.3300464155515501e-06,4.3300464155520372e-06,0.0054091786073077212,0.0054091786073083301,7.6842599217246223e-07,4.2613171033600416e-06 +100,cable:2:core,cable:3:core,9.6940022347522192e-05,0.00028095440727846668,9.6940022347522707e-05,0.00028095440727846685,9.6823581689952951e-05,0.00028060397630884999,3.6926994353979856e-07,3.6926994353948182e-07,0.0012440074648782961,0.001244007464877229,1.1644065756924156e-07,3.5043096961669172e-07 +100,cable:3:core,cable:1:core,0.00010470316710127832,0.00028484344129470142,0.00010470316710127808,0.00028484344129470288,0.00010457534806939445,0.00028449911915146345,3.672811501243478e-07,3.6728115012306053e-07,0.0012117085875670995,0.0012117085875628527,1.2781903188386614e-07,3.443221432379703e-07 +100,cable:3:core,cable:2:core,9.6940022347522206e-05,0.00028095440727846663,9.6940022347522735e-05,0.00028095440727846679,9.682358168995291e-05,0.00028060397630884999,3.6926994353976849e-07,3.6926994353944747e-07,0.0012440074648781951,0.0012440074648771135,1.1644065756929577e-07,3.5043096961663751e-07 +100,cable:3:core,cable:3:core,0.00011750705949953431,0.00077809535148733427,0.0001175070594995341,0.00077809535148733395,0.00011823922376701001,0.00077400080901010473,4.1594882632846649e-06,4.159488263284947e-06,0.0053123808674385969,0.0053123808674389577,7.3216426747569924e-07,4.0945424772295376e-06 +1000,cable:1:core,cable:1:core,0.0011876763663740096,0.0060820686216245231,0.0011876763663740098,0.0060820686216245231,0.0011847445460254178,0.0060323886843270886,4.9766371581957252e-05,4.9766371581957238e-05,0.0080952148868409929,0.0080952148868409912,2.9318203485918619e-06,4.9679937297434501e-05 +1000,cable:1:core,cable:2:core,0.00088058638790435234,0.0014022673190863706,0.00088058638790435755,0.0014022673190863721,0.00087953493666668525,0.0014022047693677529,1.0533101027247158e-06,1.0533101027194308e-06,0.00063635578367444621,0.00063635578367125323,1.0514512376670919e-06,6.2549718617740252e-08 +1000,cable:1:core,cable:3:core,0.0010032868752651442,0.0012637996350769367,0.0010032868752651413,0.0012637996350769523,0.0010024719369142027,0.0012644746811176577,1.0582115444950828e-06,1.0582115445072131e-06,0.00065579006147741805,0.00065579006148493548,8.1493835094145935e-07,6.7504604072104318e-07 +1000,cable:2:core,cable:1:core,0.00088058638790435234,0.0014022673190863708,0.00088058638790435766,0.0014022673190863724,0.00087953493666668547,0.0014022047693677531,1.0533101027246076e-06,1.0533101027192144e-06,0.00063635578367438072,0.00063635578367112237,1.051451237666875e-06,6.2549718617740252e-08 +1000,cable:2:core,cable:2:core,0.00078848453847925456,0.0066685046727502926,0.00078848453847925489,0.0066685046727502926,0.00078579364356049853,0.006617301561681695,5.127377008341523e-05,5.1273770083415217e-05,0.0076943809230435277,0.007694380923043526,2.6908949187560332e-06,5.1203111068597561e-05 +1000,cable:2:core,cable:3:core,0.00088058638790435245,0.0014022673190863711,0.00088058638790435777,0.0014022673190863726,0.00087949964287293734,0.0014022044524770085,1.0885618833534022e-06,1.0885618833480108e-06,0.0006576606542476642,0.00065766065424440693,1.086745031415111e-06,6.2866609362504863e-08 +1000,cable:3:core,cable:1:core,0.0010032868752651442,0.0012637996350769365,0.0010032868752651413,0.0012637996350769521,0.0010024719369142029,0.0012644746811176582,1.0582115444953307e-06,1.0582115445074611e-06,0.00065579006147757158,0.00065579006148508889,8.1493835094124251e-07,6.750460407216937e-07 +1000,cable:3:core,cable:2:core,0.00088058638790435234,0.0014022673190863706,0.00088058638790435755,0.0014022673190863721,0.00087949964287293778,0.0014022044524770085,1.0885618833527278e-06,1.0885618833474446e-06,0.00065766065424725665,0.00065766065424406476,1.0867450314145689e-06,6.2866609362071182e-08 +1000,cable:3:core,cable:3:core,0.0011876763663740096,0.0060820686216245257,0.0011876763663740098,0.0060820686216245257,0.0011846135175098611,0.0060322148368300316,4.9947781747544514e-05,4.99477817475445e-05,0.0081249826848652489,0.0081249826848652472,3.0628488641485339e-06,4.9853784794494024e-05 +10000,cable:1:core,cable:1:core,0.011216148512475915,0.043043091699419979,0.011216148512475916,0.043043091699419986,0.011128446407467421,0.042735853528070605,0.00031951048990139962,0.00031951048990139247,0.0072351251299446187,0.0072351251299444565,8.7702105008493486e-05,0.00030723817134937353 +10000,cable:1:core,cable:2:core,0.0052675025954369396,0.0024772829778800789,0.0052675025954369405,0.0024772829778800798,0.0052548205937773158,0.0024807428002432495,1.3145475909202062e-05,1.3145475909201454e-05,0.0022621876764855331,0.0022621876764854281,1.2682001659623798e-05,3.4598223631705435e-06 +10000,cable:1:core,cable:3:core,0.0043841369179726019,-0.00021988065450779786,0.0043841369179740434,-0.00021988065450588795,0.0043854879754549108,-0.00021452961784576972,5.5189627335702372e-06,5.5189627357749251e-06,0.0012569573134158856,0.0012569573139180087,1.3510574823089733e-06,5.3510366620281328e-06 +10000,cable:2:core,cable:1:core,0.0052675025954369422,0.0024772829778800794,0.0052675025954373872,0.0024772829778798994,0.0052548205937773132,0.0024807428002432538,1.3145475909684139e-05,1.3145475909207501e-05,0.0022621876765684932,0.0022621876764864694,1.2682001659629002e-05,3.4598223631744467e-06 +10000,cable:2:core,cable:2:core,0.0051972641718355519,0.056899313416343932,0.0051972641718355589,0.056899313416344022,0.0051018633482606017,0.056507679879895176,0.00040308577748440804,0.00040308577748431875,0.0071043946307819683,0.0071043946307803949,9.540082357495025e-05,0.00039163353644875581 +10000,cable:2:core,cable:3:core,0.0052675025954369422,0.0024772829778800798,0.0052675025954373881,0.0024772829778799002,0.0052547371859823222,0.002481082933084129,1.3318984124535541e-05,1.3318984124057023e-05,0.0022920189498846929,0.0022920189498023464,1.2765409454620034e-05,3.799955204049181e-06 +10000,cable:3:core,cable:1:core,0.0043841369179726027,-0.00021988065450779786,0.0043841369179740443,-0.00021988065450588793,0.0043854879754549082,-0.0002145296178457685,5.5189627335705447e-06,5.518962735775258e-06,0.0012569573134159566,0.0012569573139180853,1.3510574823055038e-06,5.3510366620293525e-06 +10000,cable:3:core,cable:2:core,0.0052675025954369405,0.0024772829778800794,0.0052675025954369414,0.0024772829778800802,0.0052547371859823248,0.0024810829330841307,1.3318984124054069e-05,1.3318984124053485e-05,0.0022920189498018368,0.0022920189498017362,1.2765409454615698e-05,3.7999552040513494e-06 +10000,cable:3:core,cable:3:core,0.011216148512475916,0.043043091699419986,0.011216148512475916,0.043043091699419986,0.011128386709664324,0.042738758631725257,0.00031673451047403256,0.00031673451047403256,0.007171810600624876,0.007171810600624876,8.7761802811591946e-05,0.00030433306769472868 +100000,cable:1:core,cable:1:core,0.09187042463772542,0.2852670576877403,0.09187042463772542,0.2852670576877403,0.091484378845383393,0.28368057113451289,0.0016327801252331434,0.0016327801252331434,0.0054778925830629103,0.0054778925830629103,0.00038604579234202685,0.0015864865532274064 +100000,cable:1:core,cable:2:core,0.0029207944960417953,-0.0055282682560884866,0.0029207944960417966,-0.0055282682560884875,0.0029219881236597529,-0.0055335411369923137,5.4062944718432588e-06,5.4062944718443921e-06,0.00086395064956048141,0.00086395064956066247,1.1936276179576322e-06,5.2728809038270555e-06 +100000,cable:1:core,cable:3:core,-0.0014311466171358115,-0.00035959674779834989,-0.001431146617135812,-0.00035959674779835108,-0.0014359728388020337,-0.00035884378404194684,4.8846054078058608e-06,4.8846054078061056e-06,0.0033001176488430565,0.0033001176488432218,4.8262216662221455e-06,7.5296375640304756e-07 +100000,cable:2:core,cable:1:core,0.0029207944960417944,-0.0055282682560884857,0.0029207944960417962,-0.0055282682560884883,0.0029219881236597525,-0.0055335411369923076,5.4062944718364918e-06,5.4062944718394124e-06,0.00086395064955940067,0.00086395064955986742,1.1936276179580659e-06,5.2728809038218513e-06 +100000,cable:2:core,cable:2:core,0.025028674693899085,0.51501620085517463,0.025028674693899078,0.51501620085517463,0.024742017976365958,0.51193252213577378,0.0030969737677598325,0.0030969737677598334,0.0060425210873081264,0.0060425210873081281,0.00028665671753312638,0.0030836787194008508 +100000,cable:2:core,cable:3:core,0.0029207944960417953,-0.0055282682560884875,0.0029207944960417966,-0.0055282682560884883,0.0029209104549473086,-0.0055328076670412953,4.5408917919535732e-06,4.5408917919544736e-06,0.00072578892127079446,0.00072578892127093844,1.1595890551333726e-07,4.5394109528078264e-06 +100000,cable:3:core,cable:1:core,-0.0014311466171358113,-0.00035959674779834983,-0.0014311466171358115,-0.00035959674779835097,-0.0014359728388020332,-0.00035884378404194673,4.8846054078058608e-06,4.8846054078058998e-06,0.0033001176488430574,0.0033001176488430838,4.8262216662219286e-06,7.5296375640310177e-07 +100000,cable:3:core,cable:2:core,0.0029207944960417949,-0.0055282682560884866,0.0029207944960417962,-0.0055282682560884875,0.0029209104549473073,-0.0055328076670413005,4.540891791959621e-06,4.5408917919605205e-06,0.0007257889212717607,0.00072578892127190446,1.159589055124699e-07,4.5394109528138979e-06 +100000,cable:3:core,cable:3:core,0.091870424637725434,0.2852670576877403,0.091870424637725434,0.2852670576877403,0.091483516547271729,0.28368158908783159,0.0016319952670749466,0.0016319952670749466,0.0054752464898157127,0.0054752464898157127,0.00038690809045370533,0.0015854685999087126 +1000000,cable:1:core,cable:1:core,0.75407446569154857,1.5740479213690892,0.75407446569154857,1.5740479213690892,0.75370816564133836,1.5682972345165187,0.0057623411043699439,0.0057623411043699439,0.0033116726712239556,0.0033116726712239556,0.00036630005021021095,0.005750686852570519 +1000000,cable:1:core,cable:2:core,0.00017862953015392792,0.00022533894904642603,0.00017862953015392795,0.00022533894904642595,0.00018016270749890044,0.00022577200740065841,1.5931642442973085e-06,1.5931642442973125e-06,0.005515631756989766,0.0055156317569897799,1.5331773449725158e-06,4.3305835423237634e-07 +1000000,cable:1:core,cable:3:core,2.3887617723989342e-07,1.4069502440255183e-06,2.3887617723989184e-07,1.4069502440255175e-06,2.4979350568131865e-07,1.4123901481913753e-06,1.219756605359054e-08,1.2197566053588742e-08,0.0085041401903732348,0.0085041401903719806,1.0917328441425228e-08,5.4399041658569889e-09 +1000000,cable:2:core,cable:1:core,0.00017862953015392797,0.00022533894904642611,0.00017862953015392781,0.00022533894904642752,0.00018016270749890033,0.00022577200740065857,1.5931642442969515e-06,1.5931642442971781e-06,0.0055156317569885283,0.0055156317569893133,1.5331773449723531e-06,4.3305835423245766e-07 +1000000,cable:2:core,cable:2:core,0.094696109113519125,4.9430140519244823,0.094696109113519056,4.9430140519244823,0.093103635677330446,4.9154040794845795,0.027655859230498691,0.027655859230498694,0.0056253563463567035,0.0056253563463567044,0.0015924734361886794,0.027609972439902819 +1000000,cable:2:core,cable:3:core,0.00017862953015392797,0.00022533894904642614,0.00017862953015392781,0.00022533894904642758,0.00018018081362777873,0.0002258305203441686,1.6273053668582831e-06,1.627305366858562e-06,0.0056327182711444768,0.0056327182711454421,1.551283473850756e-06,4.9157129774246339e-07 +1000000,cable:3:core,cable:1:core,2.3887617723989342e-07,1.4069502440255183e-06,2.3887617723989178e-07,1.4069502440255175e-06,2.4979350568131807e-07,1.4123901481913734e-06,1.2197566053589217e-08,1.2197566053587371e-08,0.0085041401903723241,0.008504140190371037,1.0917328441424646e-08,5.4399041658550831e-09 +1000000,cable:3:core,cable:2:core,0.00017862953015392795,0.00022533894904642606,0.00017862953015392795,0.00022533894904642598,0.00018018081362777857,0.00022583052034416836,1.6273053668584082e-06,1.6273053668583837e-06,0.0056327182711449165,0.0056327182711448315,1.5512834738506205e-06,4.9157129774230076e-07 +1000000,cable:3:core,cable:3:core,0.75407446569154857,1.5740479213690892,0.75407446569154857,1.5740479213690892,0.75374171173894589,1.568351256130611,0.0057063753848006615,0.0057063753848006615,0.0032793894824094515,0.0032793894824094515,0.00033275395260268237,0.0056966652384782268 +10000000,cable:1:core,cable:1:core,4.5479347057971937,6.6714050104071516,4.5479347057971937,6.6714050104071516,4.5500659455579013,6.6606636781499313,0.010950726075362412,0.010950726075362412,0.0013575652703185375,0.0013575652703185375,0.002131239760707615,0.010741332257220293 +10000000,cable:1:core,cable:2:core,1.5377810804168995e-09,4.4664472009509941e-10,1.5377810804168995e-09,4.4664472009509879e-10,5.6028567701004943e-09,-2.7981230302132413e-10,4.1294769849572631e-09,4.1294769849572631e-09,0.7361132176669769,0.7361132176669769,4.065075689683595e-09,7.2645702311642359e-10 +10000000,cable:1:core,cable:3:core,2.1364256347380883e-17,7.0038536780327319e-18,2.1364256347378569e-17,7.0038536780304023e-18,2.0913101393029784e-13,6.2993909743126889e-13,6.6373284392578382e-13,6.6373284392578382e-13,0.99997984432131481,0.99997984432131481,2.0910964967395046e-13,6.2993209357759082e-13 +10000000,cable:2:core,cable:1:core,1.5377810804169008e-09,4.4664472009509931e-10,1.5377810804169031e-09,4.4664472009510029e-10,5.6028567701004935e-09,-2.7981230302132444e-10,4.129476984957259e-09,4.1294769849572614e-09,0.73611321766697624,0.73611321766697668,4.0650756896835925e-09,7.264570231164238e-10 +10000000,cable:2:core,cable:2:core,1.2514510556301524,49.784417123982301,1.2514510556301526,49.784417123982301,0.52922017504967389,51.33126534579285,1.7071487533845759,1.7071487533845759,0.033255716944887444,0.033255716944887444,0.72223088058047846,1.5468482218105493 +10000000,cable:2:core,cable:3:core,1.5377810804169008e-09,4.4664472009509931e-10,1.5377810804169031e-09,4.4664472009510029e-10,5.5480436147798052e-09,-8.7996864032549524e-11,4.0457443342348382e-09,4.0457443342348398e-09,0.72912828774712768,0.72912828774712801,4.0102625343629042e-09,5.3464158412764887e-10 +10000000,cable:3:core,cable:1:core,2.1364256347380883e-17,7.0038536780327304e-18,2.1364256347378569e-17,7.0038536780304008e-18,2.0913101393029771e-13,6.2993909743126828e-13,6.6373284392578322e-13,6.6373284392578322e-13,0.99997984432131481,0.99997984432131481,2.0910964967395034e-13,6.2993209357759022e-13 +10000000,cable:3:core,cable:2:core,1.5377810804168995e-09,4.4664472009509931e-10,1.5377810804168995e-09,4.4664472009509874e-10,5.5480436147797969e-09,-8.7996864032551824e-11,4.0457443342348332e-09,4.0457443342348332e-09,0.7291282877471279,0.7291282877471279,4.0102625343628976e-09,5.3464158412765114e-10 +10000000,cable:3:core,cable:3:core,4.5479347057971937,6.6714050104071516,4.5479347057971937,6.6714050104071516,4.5501219747159745,6.6605257629547943,0.011096944194357663,0.011096944194357663,0.0013757060123651213,0.0013757060123651213,0.0021872689187807381,0.010879247452357355 diff --git a/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/air_3-Y.csv b/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/air_3-Y.csv new file mode 100644 index 000000000..54a1f3f59 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/air_3-Y.csv @@ -0,0 +1,82 @@ +frequency_hz,receiver,source,new_real,new_imaginary,old_real,old_imaginary,fem_real,fem_imaginary,before_absolute_error,after_absolute_error,before_relative_error,after_relative_error,real_absolute_error,imaginary_absolute_error +0.10000000000000001,cable:1:core,cable:1:core,10.815794395856093,0.14817480024522853,10.815794395856155,0.14817480024608823,10.885638947845042,0.14938178884839903,0.069854980237735106,0.069854980237812128,0.0064165644380051552,0.0064165644380122294,0.069844551988948567,0.0012069886031705057 +0.10000000000000001,cable:1:core,cable:2:core,-8.3998980305213653,0.14802772097541228,-8.3998980305212747,0.14802772097705633,-8.4592585099256858,0.14923920161998083,0.05937284059626674,0.05937284059620971,0.007017589420792383,0.0070175894207856427,0.059360479404320543,0.0012114806445685522 +0.10000000000000001,cable:1:core,cable:3:core,-1.1280530780399818e-13,-2.9535313553156216e-12,-1.1280530750282914e-13,-2.953531354972855e-12,-1.1377995097602876e-13,-2.9734102109619595e-12,1.9902734619529257e-14,1.9902734262425548e-14,0.0066886765298462538,0.0066886764098350471,9.7464317203058143e-16,1.9878855646337946e-14 +0.10000000000000001,cable:2:core,cable:1:core,-8.3998980305213387,0.14802772097586703,-8.3998980305212676,0.14802772097705355,-8.4592585099256858,0.14923920161997939,0.059372840596273867,0.059372840596227043,0.0070175894207932252,0.0070175894207876914,0.059360479404347188,0.0012114806441123616 +0.10000000000000001,cable:2:core,cable:2:core,10.815794395856084,0.14817480024469887,10.815794395856189,0.14817480024712157,10.885702751320308,0.14938997890362823,0.069918916058994848,0.069918916059141745,0.0064223995925957239,0.0064223995926092174,0.069908355464223249,0.0012151786589293667 +0.10000000000000001,cable:2:core,cable:3:core,-1.1296417471165001e-13,-4.2580756761562053e-12,-1.1296417495695151e-13,-4.2580756764147922e-12,-1.1394188949089691e-13,-4.2859295698325242e-12,2.7871047777869144e-14,2.7871048044901984e-14,0.006500621011536257,0.0065006210738187842,9.7771477924689401e-16,2.785389367631888e-14 +0.10000000000000001,cable:3:core,cable:1:core,-5.1136148413973961e-14,-3.5844900350302824e-13,-1.358174635016056e-13,-2.933449794573915e-12,-1.1377995097603096e-13,-2.9734102109619599e-12,4.5634272605365255e-14,2.6157114448108585e-12,0.015336228612152988,0.8790574805026683,6.2643802562056991e-14,2.6149612074589318e-12 +0.10000000000000001,cable:3:core,cable:2:core,-5.1155498185351796e-14,8.7999087097332483e-13,-1.3601058441964668e-13,-4.2379939867923176e-12,-1.1394188949089317e-13,-4.2859295698325218e-12,5.277165353921273e-14,5.1663019783660389e-12,0.012308418490205237,1.2049841635008409,6.2786391305541381e-14,5.1659204408058468e-12 +0.10000000000000001,cable:3:core,cable:3:core,-1.0666964456901868e-24,9.0759067020569623e-12,3.3278820986485504e-24,9.0759067020568993e-12,3.3659780042985981e-24,9.1325537907485334e-12,5.664708869163415e-14,5.6647088691571142e-14,0.006202765402709024,0.006202765402702125,4.4326744499887848e-24,5.6647088691571142e-14 +1,cable:1:core,cable:1:core,11.070033787834065,0.21907795136545627,11.070033787834907,0.2190779513727911,11.140563161800511,0.22071888391898442,0.07054846030638201,0.070548460307394381,0.0063313332928883869,0.0063313332929792422,0.070529373966445874,0.0016409325535281516 +1,cable:1:core,cable:2:core,-8.145822527090532,0.21787423633236641,-8.1458225270892957,0.21787423634749992,-8.2046628545477134,0.21952020259074589,0.058863344623693713,0.058863344622881023,0.0071718104745286974,0.0071718104744296811,0.058840327457181374,0.0016459662583794832 +1,cable:1:core,cable:3:core,-1.6216279544894657e-12,-2.7605627275107872e-11,-1.621627954619451e-12,-2.7605627275097529e-11,-1.6346877661635265e-12,-2.7802844226268414e-11,1.9764889199463251e-13,1.9764889199290103e-13,0.0070966892428370671,0.0070966892427748972,1.3059811674060804e-14,1.9721695116054177e-13 +1,cable:2:core,cable:1:core,-8.145822527090143,0.21787423633650868,-8.145822527089182,0.21787423634739994,-8.2046628545477152,0.21952020259074595,0.058863344623811924,0.058863344623155837,0.0071718104745430982,0.0071718104744631622,0.058840327457572172,0.0016459662542372688 +1,cable:2:core,cable:2:core,11.070033787834019,0.21907795136028499,11.070033787835508,0.21907795138250014,11.140629632151182,0.22072472762327983,0.0706150487263877,0.070615048728393956,0.0063372713720829697,0.0063372713722630184,0.070595844317162815,0.0016467762629948368 +1,cable:2:core,cable:3:core,-1.6339304032893034e-12,-4.0648885431794825e-11,-1.6339304034811439e-12,-4.0648885431782831e-11,-1.6470237031376464e-12,-4.0926987325631668e-11,2.7840994568837659e-13,2.7840994568541779e-13,0.0067970988164539527,0.0067970988163817171,1.3093299848343002e-14,2.7810189383684294e-13 +1,cable:3:core,cable:1:core,-1.2021168565223691e-12,-2.6131097494743181e-12,-2.4448833834075816e-12,-2.6909309921922212e-11,-1.6346877661635198e-12,-2.7802844226268355e-11,1.2061593971133471e-12,2.5193448366653858e-11,0.043307798653754177,0.90458424638403356,4.3257090964115068e-13,2.5189734476794037e-11 +1,cable:3:core,cable:2:core,-1.2038703207920688e-12,9.7714076065056096e-12,-2.4605830567011745e-12,-3.9952527978242624e-11,-1.6470237031376464e-12,-4.0926987325631655e-11,1.2694289430624162e-12,5.070033169137304e-11,0.030991830931642522,1.237797606982695,4.4315338234557762e-13,5.0698394932137263e-11 +1,cable:3:core,cable:3:core,-1.1415236378499647e-22,9.0759067020560391e-11,3.1423916594339771e-22,9.0759067020550995e-11,3.2183905237197333e-22,9.1334425139340136e-11,5.7535811878914124e-13,5.7535811877974499e-13,0.0062994661422719062,0.0062994661421690292,4.3599141615696978e-22,5.7535811877974499e-13 +10,cable:1:core,cable:1:core,11.455439466681346,0.35723512530509688,11.455439466692644,0.35723512536492974,11.526888706357312,0.35962578479877871,0.071489223671738211,0.071489223685030384,0.0061989369533425138,0.0061989369544950979,0.071449239675965615,0.0023906594936818348 +10,cable:1:core,cable:2:core,-7.7620499918553731,0.3478670390993045,-7.7620499918370207,0.34786703922841428,-7.8201731256180755,0.35015628638615159,0.05816819864049088,0.058168198627233866,0.0074307786725537842,0.0074307786708602483,0.058123133762702395,0.0022892472868470959 +10,cable:1:core,cable:3:core,-2.4359412290390375e-11,-2.4772235737226435e-10,-2.4359412301593299e-11,-2.4772235737075831e-10,-2.4538192632622566e-11,-2.496571722746665e-10,1.9430571579725093e-12,1.9430571575036392e-12,0.007745578471616445,0.007745578469747395,1.7878034223219079e-13,1.9348149024021558e-12 +10,cable:2:core,cable:1:core,-7.7620499918470722,0.34786703913335953,-7.7620499918328045,0.34786703922484874,-7.8201731256180809,0.35015628638615004,0.058168198644849387,0.058168198634193355,0.0074307786731105637,0.0074307786717492932,0.058123133771008639,0.002289247252790505 +10,cable:2:core,cable:2:core,11.455439466682984,0.35723512525375883,11.455439466704808,0.35723512545105984,11.525783616451514,0.35941348767162129,0.070377870567039619,0.070377870594960423,0.0061031579193572679,0.0061031579217785559,0.070344149768530428,0.0021783624178624561 +10,cable:2:core,cable:3:core,-2.5235453499545022e-11,-3.7794241747563747e-10,-2.5235453516660523e-11,-3.7794241747387335e-10,-2.5408946664693694e-11,-3.8061036108960929e-10,2.6735786895396618e-12,2.6735786888899171e-12,0.0070088498652705576,0.0070088498635672361,1.7349316514867201e-13,2.6679436139718214e-12 +10,cable:3:core,cable:1:core,-3.4971191232074981e-11,-1.938923082713509e-12,-5.4539271312740393e-11,-2.2299128059268601e-10,-2.4538192632622259e-11,-2.4965717227466635e-10,4.0138939960663476e-11,2.4793785197606e-10,0.16000523091005178,0.98835079590168551,1.0432998599452722e-11,2.4771824919195286e-10 +10,cable:3:core,cable:2:core,-3.5126392564573668e-11,1.2192069752793325e-10,-5.574623180572549e-11,-3.5319928202439063e-10,-2.5408946664693927e-11,-3.8061036108961007e-10,4.0886649719046023e-11,5.0262500298939145e-10,0.10718532074477341,1.3176433513129042,9.717445899879741e-12,5.0253105861754327e-10 +10,cable:3:core,cable:3:core,-1.330344677667895e-20,9.0759067020265848e-10,2.7893988431890267e-20,9.0759067020117803e-10,3.0071294664954368e-20,9.133123087759835e-10,5.7216385748054646e-12,5.7216385733250181e-12,0.0062647119937248794,0.0062647119921039156,4.3374741441633315e-20,5.7216385733250181e-12 +100,cable:1:core,cable:1:core,12.104669782274954,0.68261648187573531,12.104669782424534,0.68261648227961136,12.176168176226815,0.68625143513301334,0.071590734027195041,0.071590734197088446,0.0058702623123268443,0.0058702623262576808,0.071498393951861416,0.0036349532572780285 +100,cable:1:core,cable:2:core,-7.1288262826252131,0.61549758295382839,-7.1288262823565649,0.61549758392216802,-7.1858673359305918,0.61911060139397978,0.057155364473394997,0.057155364266496679,0.0079245001433820902,0.007924500114695969,0.057041053305378675,0.0036130184401513832 +100,cable:1:core,cable:3:core,-3.7138847615143461e-10,-2.0430445510009438e-09,-3.7138847700779774e-10,-2.0430445507832211e-09,-3.7372342864484257e-10,-2.0606818938052181e-09,1.7791230087403235e-11,1.7791229983954418e-11,0.00849508537841368,0.0084950853290181906,2.3349524934079579e-12,1.7637342804274308e-11 +100,cable:2:core,cable:1:core,-7.1288262824171342,0.61549758312471459,-7.1288262822103974,0.61549758378874064,-7.185867335930598,0.61911060139397978,0.057155364627710863,0.057155364463363216,0.0079245001647777324,0.0079245001419911941,0.05704105351346378,0.0036130182692651891 +100,cable:2:core,cable:2:core,12.10466978238181,0.6826164813115343,12.104669782732644,0.6826164828956689,12.176517262145065,0.68635220210041559,0.071944533467769134,0.071944533900386076,0.0058991016551025628,0.0058991016905750471,0.071847479763254896,0.0037357207888812916 +100,cable:2:core,cable:3:core,-4.2495166240793845e-10,-3.3258245297456921e-09,-4.2495166380733159e-10,-3.3258245294674373e-09,-4.2756682337893478e-10,-3.3522814203348597e-09,2.6585825809172263e-11,2.6585825669920562e-11,0.0078669359869180146,0.0078669359457124409,2.6151609709963381e-12,2.6456890589167592e-11 +100,cable:3:core,cable:1:core,-1.1636921547696429e-09,7.2216173971858748e-10,-1.4910656784955182e-09,-1.1316480131395586e-09,-3.7372342864484076e-10,-2.0606818938052144e-09,1.4531199725852168e-09,2.8927961001251365e-09,0.69384624736711165,1.381273230247569,7.8996872612480218e-10,2.7828436335238019e-09 +100,cable:3:core,cable:2:core,-1.1765965611356642e-09,1.9628848004581849e-09,-1.575033846674186e-09,-2.4110868103799636e-09,-4.275668233789368e-10,-3.3522814203348651e-09,1.4840915953397954e-09,5.3676845569298669e-09,0.43915331662306895,1.588336247684544,7.4902973775672746e-10,5.31516622079305e-09 +100,cable:3:core,cable:3:core,-1.9401763256901179e-18,9.0759067008644299e-09,1.9127172212576587e-18,9.0759067005974408e-09,2.6851848844473589e-18,9.1328039897551847e-09,5.6897289157743825e-11,5.689728889075492e-11,0.0062299912733886475,0.0062299912441545913,4.6253612101374773e-18,5.6897288890754732e-11 +1000,cable:1:core,cable:1:core,13.422507234576008,1.6280303345300919,13.422507236118639,1.6280303360967181,13.494876521647832,1.6357172513243972,0.072776384588157661,0.072776386287632305,0.0053537052833279248,0.0053537054083476884,0.072369287071824218,0.0076869167943052208 +1000,cable:1:core,cable:2:core,-5.9564805841176112,1.2141336216946836,-5.9564805806274439,1.2141336265943119,-6.0093110821882147,1.2209775222342441,0.053271951369298395,0.053271948537513103,0.0086873963200910492,0.0086873958582937907,0.052830498070603582,0.0068439005395604102 +1000,cable:1:core,cable:3:core,-5.3125767624237946e-09,-1.3812807031995072e-08,-5.3125768097971711e-09,-1.3812807003898819e-08,-5.3437799462519742e-09,-1.3933820693746159e-08,1.2497179223762959e-10,1.2497177685950375e-10,0.0083742270091450837,0.0083742259786732127,3.1203183828179629e-11,1.2101366175108671e-10 +1000,cable:2:core,cable:1:core,-5.9564805795315312,1.2141336183221656,-5.9564805769900762,1.2141336212955836,-6.0093110821882192,1.2209775222342427,0.053271955657261144,0.053271953518864511,0.0086873970193564326,0.0086873966706344768,0.052830502656687983,0.0068439039120771206 +1000,cable:2:core,cable:2:core,13.422507237708379,1.628030325555774,13.422507243289036,1.6280303346877092,13.494530277879935,1.6357156672970763,0.072431911813774466,0.072431918331866935,0.0053284993777964673,0.0053284998573040606,0.072023040171556474,0.0076853417413023095 +1000,cable:2:core,cable:3:core,-7.6990451786107599e-09,-2.5177999875353145e-08,-7.6990452661998479e-09,-2.5177999831420848e-08,-7.7437038728303673e-09,-2.5406498794328743e-08,2.3282217934759386e-10,2.3282215303191718e-10,0.0087657615371490982,0.0087657605463631397,4.4658694219607418e-11,2.2849891897559837e-10 +1000,cable:3:core,cable:1:core,-3.8993651370144031e-08,3.4644002403347795e-08,-4.4374379330248328e-08,2.2280527656749974e-08,-5.3437799462519742e-09,-1.3933820693746142e-08,5.3243466404105376e-08,5.9094151518547249e-08,3.5677881099276916,3.9598363028046895,3.3649871423892055e-08,4.8577823097093937e-08 +1000,cable:3:core,cable:2:core,-3.9862609765354866e-08,4.7392554307083943e-08,-4.9053839044978226e-08,1.1664030389121588e-08,-7.7437038728303574e-09,-2.5406498794328753e-08,5.5504516950264258e-08,7.9569631444384297e-08,2.0897466091247408,2.9957988401053144,3.211890589252451e-08,7.2799053101412696e-08 +1000,cable:3:core,cable:3:core,-4.4750964200884614e-16,9.0759066492957161e-08,-1.1449192093489889e-16,9.075906643642654e-08,2.1469037444216061e-16,9.1289179425853152e-08,5.3011298942671402e-10,5.301129328964039e-10,0.0058069641195239588,0.0058069635002795912,6.6220001645100678e-16,5.3011293289599031e-10 +10000,cable:1:core,cable:1:core,17.086534011462401,4.2459949224509836,17.086534015481533,4.2459949196386608,17.16879046787793,4.2706812130935701,0.085880946928772295,0.08588094996988567,0.0048542309469669608,0.0048542311188591817,0.082256456415528589,0.024686290642586428 +10000,cable:1:core,cable:2:core,-3.3202280044350214,2.2955286501758181,-3.3202279874715721,2.2955286461190294,-3.3571627880337496,2.3128739690953988,0.040804898256488663,0.040804881177462601,0.010009162278858546,0.010009158089490339,0.036934783598728149,0.017345318919580688 +10000,cable:1:core,cable:3:core,-5.8587418784551338e-08,-4.3856895157340185e-08,-5.858742019788468e-08,-4.3856893278769888e-08,-5.9029983864961397e-08,-4.4323999810668119e-08,6.434680346049712e-10,6.4346764297746234e-10,0.0087169059183770485,0.0087169006130938548,4.425650804100583e-10,4.6710465332793414e-10 +10000,cable:2:core,cable:1:core,-3.3202280396447068,2.2955285385873432,-3.320228031402729,2.2955285336624991,-3.3571627880337513,2.312873969095401,0.04080490629507455,0.04080489674134577,0.010009164250668641,0.010009161907204358,0.036934748389044536,0.017345430508057813 +10000,cable:2:core,cable:2:core,17.086533949182265,4.245994782397787,17.086533982869692,4.2459947768646549,17.168943134567673,4.2708718018210501,0.086082138996785421,0.086082169647796622,0.0048655494879547793,0.0048655512204165141,0.082409185385408534,0.024877019423263036 +10000,cable:2:core,cable:3:core,-1.0820603063721758e-07,-9.3862017659670496e-08,-1.0820603152553118e-07,-9.3862013176872435e-08,-1.0906970448598638e-07,-9.5008183716100581e-08,1.4351438560701812e-09,1.4351408104989582e-09,0.0099216978479292912,0.0099216767927317075,8.6367384876880097e-10,1.1461660564300851e-09 +10000,cable:3:core,cable:1:core,-6.8597772458705922e-07,1.5110795613731931e-06,-7.4376780209961771e-07,1.4920783526578319e-06,-5.9029983864961377e-08,-4.4323999810668119e-08,1.6820814690113849e-06,1.6770043851284292e-06,22.786751048821138,22.717973020749234,6.2694774072209786e-07,1.5554035611838613e-06 +10000,cable:3:core,cable:2:core,-7.0566345675390556e-07,1.6746422052936641e-06,-8.720081626905464e-07,1.5363787545915007e-06,-1.0906970448598638e-07,-9.5008183716100581e-08,1.8009715248965488e-06,1.8675081270418952e-06,12.450804305902423,12.910797260282411,5.965937522679192e-07,1.7696503890097647e-06 +10000,cable:3:core,cable:3:core,-1.6692047509887364e-13,9.0759045224291654e-07,-1.4613839178070811e-13,9.075904422043152e-07,1.1771746799955883e-14,9.1287583973309992e-07,5.285397531143633e-09,5.2853874932040563e-09,0.005789831761446265,0.0057898207654935412,1.7869222189882952e-13,5.285387490183378e-09 +100000,cable:1:core,cable:1:core,25.114053090920187,8.0617971940330779,25.114053091114524,8.0617971943186379,25.211287126640112,8.120101984535804,0.11337506880641381,0.11337506911993676,0.0042804534634525281,0.0042804534752895257,0.09723403571992506,0.058304790502726078 +100000,cable:1:core,cable:2:core,0.17168861145691555,1.0502292230423047,0.17168860920972809,1.0502292248219696,0.17180324107286526,1.0598490202933522,0.0096204784379613478,0.0096204801907239946,0.0089602552946762264,0.0089602569271522922,0.00011462961594971288,0.0096197972510474461 +100000,cable:1:core,cable:3:core,-5.2567619721435549e-08,7.4524194220000796e-08,-5.2567611057230254e-08,7.452419524450226e-08,-5.2703783492650436e-08,7.536595966692837e-08,8.5270761403345421e-10,8.5270724180848757e-10,0.0092720032399396871,0.0092719991925135359,1.361637712148869e-10,8.4176544692757397e-10 +100000,cable:2:core,cable:1:core,0.17168867697767981,1.0502294238147607,0.17168867735386809,1.0502294245133177,0.17180324107286571,1.0598490202933519,0.0096202779490492437,0.00962027865203657,0.0089600685646868822,0.008960069219430402,0.00011456409518589838,0.0096195964785912302 +100000,cable:2:core,cable:2:core,25.114053168427063,8.0617974533493086,25.114053162870402,8.061797456881747,25.213095358910977,8.1211168291641247,0.11544758345030423,0.11544758049829912,0.0043583664354513068,0.0043583663240074847,0.099042190483913828,0.059319375814816055 +100000,cable:2:core,cable:3:core,-1.3779481951670331e-07,1.5778396825373155e-07,-1.377947989655141e-07,1.5778396671832445e-07,-1.3987609291158349e-07,1.59753201089502e-07,2.8652519083969159e-09,2.8652359249654539e-09,0.013493978028802577,0.013493902754418379,2.0812733948801838e-09,1.9692328357704569e-09 +100000,cable:3:core,cable:1:core,1.5638455966719109e-05,7.7776922470880267e-06,1.5670639845467555e-05,7.7897672759496725e-06,-5.270378349265043e-08,7.536595966692837e-08,1.7513866578886239e-05,1.7479654588823077e-05,190.43881512348455,190.06680755900422,1.569115975021176e-05,7.7023262874210983e-06 +100000,cable:3:core,cable:2:core,1.7038431448127932e-05,8.9143470293325838e-06,1.6957770865381582e-05,9.1879747547490139e-06,-1.3987609291158347e-07,1.59753201089502e-07,1.9334898911874188e-05,1.9280486588051839e-05,91.058206903672612,90.801950656184175,1.7178307541039514e-05,8.7545938282430816e-06 +100000,cable:3:core,cable:3:core,-5.5477726060310505e-11,9.0758164316572827e-06,-5.4794500699005802e-11,9.0758158501016034e-06,3.4078170713721261e-13,9.1297246440059132e-06,5.3908822099139899e-08,5.3908241246870991e-08,0.0059047588181680259,0.0059046951960664232,5.5818507767447715e-11,5.390821234863046e-08 +1000000,cable:1:core,cable:1:core,40.833774626502681,19.549173406400048,40.833774626502681,19.549173406400044,40.900039224648729,19.655874028520731,0.12560262627986341,0.12560262627986038,0.0027679164612677935,0.0027679164612677267,0.06626459814604857,0.10670062212068387 +1000000,cable:1:core,cable:2:core,-0.020403832499743355,-0.018991053490800946,-0.020403832499540885,-0.018991053491192875,-0.020563381851447241,-0.019102808491515869,0.00019479521501035552,0.00019479521506937203,0.0069403052986080618,0.0069403053007107453,0.00015954935170388665,0.00011175500071492275 +1000000,cable:1:core,cable:3:core,-1.550392640425466e-09,-5.8061933848089211e-09,-1.5503936374884625e-09,-5.8061917080445266e-09,-1.5672016590061488e-09,-5.8192553407374583e-09,2.1287744983319436e-11,2.1287503337047024e-11,0.0035323002604624766,0.0035322601638157129,1.6809018580682813e-11,1.3061955928537201e-11 +1000000,cable:2:core,cable:1:core,-0.020403832081271396,-0.018991054091930554,-0.020403832081266379,-0.018991054091929891,-0.020563381851447248,-0.019102808491515869,0.00019479521295861048,0.00019479521295412121,0.0069403052255069993,0.0069403052253470517,0.00015954977017585206,0.00011175439958531486 +1000000,cable:2:core,cable:2:core,40.833774627207667,19.549173405646197,40.833774627208193,19.549173405645224,40.89915157803005,19.655356047113056,0.12469522463931708,0.12469522463876383,0.002747982005379142,0.0027479820053669499,0.065376950822383151,0.10618264146685874 +1000000,cable:2:core,cable:3:core,-3.6993309151330288e-09,-1.4559820469910908e-08,-3.6993334581473173e-09,-1.4559816308977549e-08,-3.7825918612963801e-09,-1.4735986432314035e-08,1.9485346815318598e-10,1.9485079282732304e-10,0.012807745855603602,0.012807570006058514,8.3260946163351313e-11,1.7616596240312679e-10 +1000000,cable:3:core,cable:1:core,-6.4349987748322059e-06,5.1068012806931181e-06,-6.4350271678452756e-06,5.1067899044121304e-06,-1.5672016590061527e-09,-5.8192553407374633e-09,8.2175531368473888e-06,8.2175379860501342e-06,1363.5481404157649,1363.5456264263719,6.4334315731731998e-06,5.1126205360338558e-06 +1000000,cable:3:core,cable:2:core,-7.7938841060709354e-06,5.4353142732557005e-06,-7.798364509434425e-06,5.4402402295420326e-06,-3.7825918612963826e-09,-1.4735986432314038e-08,9.5137938166960399e-06,9.5072987454279935e-06,625.3430050886343,624.91608314106202,7.7901015142096393e-06,5.4500502596880142e-06 +1000000,cable:3:core,cable:3:core,-2.4740421441749414e-08,9.0711568120478741e-05,-2.4719345718823336e-08,9.0711547555953113e-05,1.0614892269862695e-11,9.1254421478153164e-05,5.4343690190922188e-07,5.4341731821685927e-07,0.0059551843418274271,0.0059549697364193232,2.4751036334019276e-08,5.4285335767442332e-07 +10000000,cable:1:core,cable:1:core,80.846531351828517,55.108195401013525,80.846531351828517,55.108195401013525,80.8235232692426,55.21592941541855,0.11016346819205822,0.11016346819205822,0.0011254513410626985,0.0011254513410626985,0.023008082585917577,0.10773401440502539 +10000000,cable:1:core,cable:2:core,-9.2664181398836154e-08,-1.3479772233804371e-07,-9.2664181398836154e-08,-1.3479772233804371e-07,-3.735569252190193e-07,-2.4421266995171498e-07,3.0145043422117251e-07,3.0145043422117251e-07,0.67544194975282035,0.67544194975282035,2.8089274382018313e-07,1.0941494761367126e-07 +10000000,cable:1:core,cable:3:core,-5.2158582298414681e-14,-5.4725149783209922e-14,-5.2158582925956706e-14,-5.4725070160129558e-14,-1.8847158663702465e-12,-9.6197774170620373e-13,2.0448407290378886e-12,2.0448406942732041e-12,0.96636016368605737,0.96636014725680408,1.832557284071832e-12,9.0725259192299378e-13 +10000000,cable:2:core,cable:1:core,-9.2664181398870313e-08,-1.3479772233806722e-07,-9.2664181398870313e-08,-1.3479772233806722e-07,-3.7355692521901909e-07,-2.4421266995171524e-07,3.0145043422113206e-07,3.0145043422113206e-07,0.67544194975272975,0.67544194975272975,2.8089274382014878e-07,1.0941494761364802e-07 +10000000,cable:2:core,cable:2:core,80.846531351828517,55.108195401013525,80.846531351828517,55.108195401013525,80.821803742000398,55.219645496033451,0.1141603187090951,0.1141603187090951,0.0011662758803171742,0.0011662758803171742,0.024727609828119057,0.11145009501992575 +10000000,cable:2:core,cable:3:core,-1.1003843823177019e-13,-1.3743099164070031e-13,-1.1003845565783392e-13,-1.3743080703062872e-13,-2.0949856543762342e-12,-1.0129284741972224e-12,2.1694495953845462e-12,2.1694495368278252e-12,0.93228946241669342,0.93228943725279279,1.984947216144464e-12,8.754974825565221e-13 +10000000,cable:3:core,cable:1:core,3.6775716049205592e-11,4.3675577430240467e-10,3.6775425414283791e-11,4.3675607950356147e-10,-1.8847158663702142e-12,-9.6197774170617021e-13,4.3942201146780478e-10,4.3942173302016889e-10,207.66405955200685,207.66392796195234,3.8660431915575804e-11,4.3771775204411086e-10 +10000000,cable:3:core,cable:2:core,-9.3439526677307196e-11,5.4261359972629985e-10,-9.3440467051073188e-11,5.4261406900812513e-10,-2.0949856543762516e-12,-1.0129284741972214e-12,5.5124795633475065e-10,5.5124733771512746e-10,236.89080491336637,236.89053907056589,9.1344541022930942e-11,5.4362652820049708e-10 +10000000,cable:3:core,cable:3:core,-1.6761991871629861e-05,0.0008877789650905823,-1.6761328755330462e-05,0.00088777830561370815,1.6610165398915739e-08,-8.8252205620793824e-05,0.00097617470674829054,0.00097617537752523571,11.061193117969086,11.061200718650889,1.6778602037028778e-05,0.00097603117071137617 diff --git a/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/air_3-Z.csv b/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/air_3-Z.csv new file mode 100644 index 000000000..c857a1463 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/air_3-Z.csv @@ -0,0 +1,82 @@ +frequency_hz,receiver,source,new_real,new_imaginary,old_real,old_imaginary,fem_real,fem_imaginary,before_absolute_error,after_absolute_error,before_relative_error,after_relative_error,real_absolute_error,imaginary_absolute_error +0.10000000000000001,cable:1:core,cable:1:core,3.1374615510534723e-06,1.2435841216202133e-06,3.1374615510534732e-06,1.2435841216202129e-06,3.1569384454592992e-06,1.2376736789074544e-06,2.0353936935062379e-08,2.0353936935063312e-08,0.0060025453549572361,0.0060025453549575119,1.9476894405826848e-08,5.9104427127589484e-09 +0.10000000000000001,cable:1:core,cable:2:core,9.9023675189330895e-08,8.1529149017255945e-07,9.9023675189331398e-08,8.1529149017255914e-07,9.8889592958001499e-08,8.1220201356743481e-07,3.0923847979138876e-09,3.0923847979141829e-09,0.0037794973675009044,0.0037794973675012652,1.3408223132939569e-10,3.0894766051246404e-09 +0.10000000000000001,cable:1:core,cable:3:core,9.8689933658488536e-08,6.8496952230340285e-07,9.8689933646132813e-08,6.8496952228978997e-07,9.8555942974177675e-08,6.8240259924173328e-07,2.5704177549214906e-09,2.5704177691599399e-09,0.0037280373348655758,0.0037280373555164874,1.3399068431086045e-10,2.5669230616695682e-09 +0.10000000000000001,cable:2:core,cable:1:core,9.9023675189330882e-08,8.1529149017255935e-07,9.9023675189331385e-08,8.1529149017255903e-07,9.8889592958001552e-08,8.1220201356743481e-07,3.0923847979137788e-09,3.0923847979140746e-09,0.0037794973675007713,0.003779497367501133,1.3408223132932952e-10,3.0894766051245346e-09 +0.10000000000000001,cable:2:core,cable:2:core,3.1374615510534723e-06,1.2435841216202133e-06,3.1374615510534732e-06,1.2435841216202129e-06,3.1569387144350372e-06,1.2377673118767072e-06,2.0327202684685417e-08,2.032720268468635e-08,0.0059946003389987892,0.0059946003389990642,1.9477163381564883e-08,5.8168097435061456e-09 +0.10000000000000001,cable:2:core,cable:3:core,9.8692094314379168e-08,7.1450042905111216e-07,9.8692094314379658e-08,7.1450042905111195e-07,9.8559152564943101e-08,7.1183814854081974e-07,2.6655976861156242e-09,2.6655976861158115e-09,0.0037092827583557471,0.0037092827583560077,1.3294174943606703e-10,2.6622805102924158e-09 +0.10000000000000001,cable:3:core,cable:1:core,9.8689933658488523e-08,6.8496952230340274e-07,9.8689933646133078e-08,6.8496952228978838e-07,9.8555942974177636e-08,6.8240259924173296e-07,2.5704177549202378e-09,2.5704177691601529e-09,0.0037280373348637604,0.0037280373555167983,1.3399068431088692e-10,2.5669230616697799e-09 +0.10000000000000001,cable:3:core,cable:2:core,9.8692094314379168e-08,7.1450042905111216e-07,9.8692094314379645e-08,7.1450042905111184e-07,9.8559152564942969e-08,7.1183814854081953e-07,2.6655976861157359e-09,2.6655976861160295e-09,0.0037092827583559036,0.0037092827583563122,1.3294174943619938e-10,2.6622805102926276e-09 +0.10000000000000001,cable:3:core,cable:3:core,3.1368030432638579e-06,1.2442491566826318e-06,3.1368030432638579e-06,1.2442491566826316e-06,3.1562799575900971e-06,1.2381889884782712e-06,2.0397936913737283e-08,2.0397936913737346e-08,0.0060162751800927651,0.0060162751800927833,1.9476914326239281e-08,6.0601682043606381e-09 +1,cable:1:core,cable:1:core,4.0462619649157106e-06,1.098137058628601e-05,4.0462619649157199e-06,1.0981370586286009e-05,4.06449427838295e-06,1.0930813715747969e-05,5.3743970945282693e-08,5.3743970945287451e-08,0.0046084600110046781,0.0046084600110050857,1.8232313467239359e-08,5.0556870538040859e-08 +1,cable:1:core,cable:2:core,9.9703495653241241e-07,6.6990056616567392e-06,9.9703495653241453e-07,6.6990056616567426e-06,9.9565697546928076e-07,6.6766752941241212e-06,2.2372843939747775e-08,2.2372843939744264e-08,0.003314247151264345,0.003314247151263825,1.3779810631316533e-09,2.2330367532618046e-08 +1,cable:1:core,cable:3:core,9.8646617111842156e-07,5.4030097683584809e-06,9.8646617099486423e-07,5.4030097682223509e-06,9.8503362833179562e-07,5.3861070599480792e-06,1.6963305264206145e-08,1.6963305410284266e-08,0.0030980717574050601,0.0030980717840838525,1.4325427866259356e-09,1.6902708410401732e-08 +1,cable:2:core,cable:1:core,9.9703495653241262e-07,6.6990056616567409e-06,9.9703495653241474e-07,6.6990056616567443e-06,9.9565697546928118e-07,6.6766752941241212e-06,2.2372843939749452e-08,2.2372843939745942e-08,0.0033142471512645935,0.0033142471512640735,1.3779810631314415e-09,2.233036753261974e-08 +1,cable:2:core,cable:2:core,4.0462619649157106e-06,1.0981370586286007e-05,4.0462619649157199e-06,1.0981370586286005e-05,4.0645127496387215e-06,1.0931687928980027e-05,5.2928797265652761e-08,5.2928797265657566e-08,0.0045382387913634045,0.0045382387913638156,1.8250784723010892e-08,4.9682657305979438e-08 +1,cable:2:core,cable:3:core,9.866394025367669e-07,5.698292545468414e-06,9.8663940253676923e-07,5.6982925454684148e-06,9.8530671994117011e-07,5.6804526678089855e-06,1.7889585735953143e-08,1.7889585735952127e-08,0.0031029902632174191,0.0031029902632172431,1.3326825955967894e-09,1.7839877659428474e-08 +1,cable:3:core,cable:1:core,9.8646617111842156e-07,5.4030097683584818e-06,9.8646617099486423e-07,5.4030097682223517e-06,9.8503362833179541e-07,5.3861070599480775e-06,1.6963305264208696e-08,1.6963305410286817e-08,0.0030980717574055267,0.0030980717840843192,1.4325427866261473e-09,1.6902708410404273e-08 +1,cable:3:core,cable:2:core,9.866394025367669e-07,5.698292545468414e-06,9.8663940253676923e-07,5.6982925454684148e-06,9.8530671994117053e-07,5.6804526678089847e-06,1.7889585735953957e-08,1.7889585735952937e-08,0.0031029902632175605,0.003102990263217384,1.3326825955963659e-09,1.7839877659429321e-08 +1,cable:3:core,cable:3:core,4.0258228222657537e-06,1.100234691604813e-05,4.025822822265763e-06,1.1002346916048127e-05,4.0439249252427902e-06,1.0949807691859883e-05,5.5570281720479191e-08,5.5570281720485432e-08,0.0047607104208511695,0.0047607104208517038,1.8102102977036502e-08,5.2539224188247047e-08 +10,cable:1:core,cable:1:core,1.4059660509356969e-05,9.4699377213437565e-05,1.405966050935706e-05,9.4699377213437497e-05,1.4077229758399098e-05,9.4284884384713351e-05,4.1486501850077148e-07,4.1486501850084305e-07,0.0043518833853649136,0.0043518833853656638,1.756924904212993e-08,4.1449282872421385e-07 +10,cable:1:core,cable:2:core,1.016473179872643e-05,5.2299532930466564e-05,1.0164731798726483e-05,5.2299532930466571e-05,1.015138241654919e-05,5.2159887190133634e-05,1.4028235383557433e-07,1.4028235383556261e-07,0.0026399361884357079,0.0026399361884354871,1.3349382177240088e-08,1.3964574033292949e-07 +10,cable:1:core,cable:3:core,9.8317851451266156e-06,3.9569605534518693e-05,9.8317851451266173e-06,3.9569605534518679e-05,9.8163402711206239e-06,3.9484136856124967e-05,8.685297415419253e-08,8.6852974154205566e-08,0.0021347094927609009,0.0021347094927612214,1.5444874005991736e-08,8.5468678393725854e-08 +10,cable:2:core,cable:1:core,1.0164731798726432e-05,5.2299532930466564e-05,1.0164731798726483e-05,5.2299532930466571e-05,1.0151382416549192e-05,5.2159887190133594e-05,1.4028235383561465e-07,1.4028235383560308e-07,0.0026399361884364685,0.0026399361884362508,1.3349382177240088e-08,1.3964574033297015e-07 +10,cable:2:core,cable:2:core,1.4059660509356969e-05,9.4699377213437578e-05,1.405966050935706e-05,9.4699377213437511e-05,1.4078088050992001e-05,9.43045827861135e-05,3.9522425803181795e-07,3.9522425803188989e-07,0.0041450015868589162,0.0041450015868596708,1.8427541635031996e-08,3.9479442732407843e-07 +10,cable:2:core,cable:3:core,9.8448260812245156e-06,4.2519810980107054e-05,9.8448260812245698e-06,4.2519810980107075e-05,9.8330297332457038e-06,4.2420226349152085e-05,1.0028086830537464e-07,1.0028086830534808e-07,0.0023029267712253013,0.0023029267712246916,1.1796347978811815e-08,9.9584630954968883e-08 +10,cable:3:core,cable:1:core,9.8317851451266156e-06,3.9569605534518686e-05,9.831785145126619e-06,3.9569605534518666e-05,9.8163402711206239e-06,3.9484136856124967e-05,8.6852974154179493e-08,8.6852974154198896e-08,0.0021347094927605809,0.0021347094927610575,1.5444874005991736e-08,8.5468678393719078e-08 +10,cable:3:core,cable:2:core,9.8448260812245156e-06,4.2519810980107034e-05,9.8448260812245715e-06,4.2519810980107061e-05,9.8330297332457004e-06,4.2420226349152079e-05,1.0028086830536851e-07,1.0028086830533502e-07,0.0023029267712251613,0.0023029267712243919,1.1796347978815203e-08,9.958463095495533e-08 +10,cable:3:core,cable:3:core,1.3443875148217057e-05,9.5357294237721687e-05,1.3443875148217152e-05,9.535729423772166e-05,1.345650685647301e-05,9.4936849368513674e-05,4.2063457786632498e-07,4.2063457786635489e-07,0.0043868295663449198,0.0043868295663452312,1.2631708255952105e-08,4.2044486920801293e-07 +100,cable:1:core,cable:1:core,0.00011750384543838827,0.00077809644648036498,0.00011750384543838806,0.00077809644648036465,0.00011825531094274957,0.00077388776386910112,4.2752438207191567e-06,4.2752438207194387e-06,0.0054609830498349833,0.0054609830498353433,7.5146550436130163e-07,4.2086826112638573e-06 +100,cable:1:core,cable:2:core,0.00010621565159805932,0.00037161327841812256,0.00010621565159805986,0.00037161327841812278,0.00010608020461151831,0.00037093196672528898,6.9464487974450093e-07,6.9464487974418255e-07,0.0018005195975041419,0.0018005195975033168,1.3544698654101306e-07,6.8131169283358736e-07 +100,cable:1:core,cable:3:core,9.6062121241851902e-05,0.00025171028261848866,9.6062121241851712e-05,0.00025171028261849007,9.5943191924629455e-05,0.00025153095123537972,2.1518347395472479e-07,2.1518347395365504e-07,0.00079932067807002316,0.00079932067806604945,1.1892931722244714e-07,1.7933138310894129e-07 +100,cable:2:core,cable:1:core,0.00010621565159805932,0.00037161327841812256,0.00010621565159805986,0.00037161327841812278,0.00010608020461151828,0.00037093196672528887,6.9464487974461252e-07,6.9464487974429415e-07,0.0018005195975044318,0.0018005195975036065,1.3544698654104016e-07,6.8131169283369578e-07 +100,cable:2:core,cable:2:core,0.00011750384543838808,0.00077809644648036455,0.00011750384543838787,0.00077809644648036422,0.0001182640892721979,0.00077393680436614522,4.2285450459029626e-06,4.2285450459032438e-06,0.0054009887719494087,0.0054009887719497678,7.6024383380981857e-07,4.1596421142193248e-06 +100,cable:2:core,cable:3:core,9.6939258720589035e-05,0.00028095471396052333,9.6939258720589563e-05,0.00028095471396052349,9.6830278881262328e-05,0.00028060784803067107,3.6358297357299128e-07,3.6358297357267767e-07,0.001224824917127208,0.0012248249171261515,1.0897983932670692e-07,3.4686592985226378e-07 +100,cable:3:core,cable:1:core,9.6062121241851888e-05,0.00025171028261848866,9.6062121241851698e-05,0.00025171028261849001,9.5943191924629482e-05,0.00025153095123537966,2.1518347395470232e-07,2.1518347395367775e-07,0.00079932067806993989,0.00079932067806613401,1.1892931722240648e-07,1.793313831089955e-07 +100,cable:3:core,cable:2:core,9.6939258720589008e-05,0.00028095471396052333,9.6939258720589536e-05,0.00028095471396052349,9.6830278881262396e-05,0.00028060784803067101,3.6358297357301452e-07,3.6358297357270096e-07,0.0012248249171272864,0.0012248249171262302,1.0897983932661205e-07,3.4686592985231799e-07 +100,cable:3:core,cable:3:core,0.00010029065542748212,0.00079835616300552852,0.00010029065542748186,0.00079835616300552798,0.00010105583228587755,0.00079404723441232818,4.3763410797056339e-06,4.3763410797061226e-06,0.0054673377844122635,0.0054673377844128741,7.6517685839542506e-07,4.3089285932003396e-06 +1000,cable:1:core,cable:1:core,0.0011875295144732644,0.0060821869409340423,0.0011875295144732621,0.0060821869409340397,0.001184365255805897,0.0060317603667377527,5.0525754997700245e-05,5.0525754997702996e-05,0.0082196615691729393,0.0082196615691733869,3.1642586673674179e-06,5.0426574196289591e-05 +1000,cable:1:core,cable:2:core,0.001102054672250501,0.0020961818335963364,0.0011020546722505062,0.0020961818335963386,0.0011008040157692749,0.0020952261252132928,1.5740140239114191e-06,1.5740140239059674e-06,0.00066503889558218519,0.0006650388955798818,1.2506564812260999e-06,9.557083830435964e-07 +1000,cable:1:core,cable:3:core,0.00083441087840421878,0.001133000518346655,0.00083441087840421617,0.0011330005183466704,0.00083336312012243408,0.001133947339960391,1.4121857474324659e-06,1.4121857474447188e-06,0.0010035129201567358,0.0010035129201654428,1.0477582817846991e-06,9.4682161373596356e-07 +1000,cable:2:core,cable:1:core,0.0011020546722505008,0.0020961818335963364,0.001102054672250506,0.0020961818335963386,0.0011008040157692751,0.0020952261252132924,1.5740140239113378e-06,1.5740140239058863e-06,0.00066503889558215093,0.00066503889557984765,1.2506564812256663e-06,9.5570838304403008e-07 +1000,cable:2:core,cable:2:core,0.0011875295144730821,0.0060821869409338524,0.0011875295144730795,0.0060821869409338489,0.0011843954705209859,0.0060321780833149713,5.0106966300493903e-05,5.0106966300497529e-05,0.0081509806890415742,0.008150980689042164,3.1340439520962087e-06,5.0008857618881117e-05 +1000,cable:2:core,cable:3:core,0.00088055424215431686,0.0014022944498764066,0.00088055424215432207,0.0014022944498764082,0.0008792393876089305,0.0014023395859519923,1.3156290285851886e-06,1.3156290285800395e-06,0.00079485594137271743,0.00079485594136960653,1.3148545453863653e-06,4.5136075585644822e-08 +1000,cable:3:core,cable:1:core,0.00083441087840421878,0.0011330005183466552,0.00083441087840421607,0.0011330005183466704,0.00083336312012243419,0.0011339473399603912,1.4121857474324504e-06,1.4121857474446384e-06,0.0010035129201567245,0.0010035129201653855,1.0477582817845906e-06,9.4682161373596356e-07 +1000,cable:3:core,cable:2:core,0.00088055424215431675,0.0014022944498764066,0.00088055424215432207,0.0014022944498764082,0.00087923938760893104,0.0014023395859519927,1.3156290285846615e-06,1.3156290285794042e-06,0.00079485594137239856,0.00079485594136922228,1.3148545453857147e-06,4.5136075586078503e-08 +1000,cable:3:core,cable:3:core,0.00078851876842865219,0.0066684718151727796,0.00078851876842865251,0.0066684718151727796,0.00078538139724812072,0.0066161432586409183,5.2422523085336487e-05,5.2422523085336473e-05,0.007868183652700653,0.0078681836527006496,3.1373711805314682e-06,5.2328556531861317e-05 +10000,cable:1:core,cable:1:core,0.01121739034987806,0.043047632316466668,0.01121739034987806,0.043047632316466668,0.011118858602888656,0.042731471972150319,0.00033115837371719322,0.00033115837371719322,0.0075000148609756052,0.0075000148609756052,9.8531746989403285e-05,0.0003161603443163491 +10000,cable:1:core,cable:2:core,0.0080340309126035597,0.0048666513437921688,0.0080340309126035597,0.0048666513437921688,0.0080285428981946051,0.0048733819025713922,8.6843954097837918e-06,8.6843954097837918e-06,0.00092467091254244671,0.00092467091254244671,5.4880144089546057e-06,6.7305587792233432e-06 +10000,cable:1:core,cable:3:core,0.0040559166332564877,0.0011450309278026654,0.0040559166332579241,0.0011450309278046009,0.0040496698385350796,0.0011474855613075126,6.7117560997829913e-06,6.7117560991540041e-06,0.0015945810636581533,0.0015945810635087183,6.2467947214081554e-06,2.4546335048471462e-06 +10000,cable:2:core,cable:1:core,0.0080340309126035597,0.0048666513437921671,0.0080340309126035597,0.0048666513437921671,0.0080285428981945982,0.0048733819025713878,8.6843954097861601e-06,8.6843954097861601e-06,0.00092467091254269966,0.00092467091254269966,5.4880144089615446e-06,6.7305587792207411e-06 +10000,cable:2:core,cable:2:core,0.011217390349824953,0.043047632316487082,0.011217390349824953,0.043047632316487082,0.011119866253779278,0.042731512078665247,0.00033082163482768073,0.00033082163482768073,0.0074923388149703033,0.0074923388149703033,9.752409604567476e-05,0.00031612023782183474 +10000,cable:2:core,cable:3:core,0.0052676968239068452,0.0024783732958180765,0.0052676968239068461,0.0024783732958180778,0.0052560464242098499,0.0024795778081912593,1.1712500294852345e-05,1.1712500294851615e-05,0.0020153767441258739,0.0020153767441257486,1.1650399696995321e-05,1.2045123731828193e-06 +10000,cable:3:core,cable:1:core,0.0040559166332564895,0.0011450309278026659,0.004055916633257925,0.0011450309278046007,0.0040496698385350787,0.0011474855613075117,6.7117560997843677e-06,6.7117560991559506e-06,0.0015945810636584811,0.0015945810635091815,6.2467947214107575e-06,2.4546335048458452e-06 +10000,cable:3:core,cable:2:core,0.0052676968239068469,0.0024783732958180761,0.0052676968239072927,0.0024783732958178969,0.0052560464242098525,0.0024795778081912593,1.1712500295312677e-05,1.1712500294850797e-05,0.0020153767442050827,0.0020153767441256068,1.1650399696994454e-05,1.204512373183253e-06 +10000,cable:3:core,cable:3:core,0.0051968947578844215,0.056898361105510734,0.0051968947578844276,0.056898361105510831,0.0051094661219450417,0.056507525230177859,0.00040049525319223449,0.00040049525319213838,0.007058670637102386,0.007058670637100692,8.7428635939379787e-05,0.00039083587533287473 +100000,cable:1:core,cable:1:core,0.091870181208271839,0.2852584218993296,0.091870181208271839,0.2852584218993296,0.09149276545797437,0.28368561958405109,0.0016174516288031745,0.0016174516288031745,0.0054263319394923688,0.0054263319394923688,0.00037741575029746899,0.0015728023152785098 +100000,cable:1:core,cable:2:core,0.0087282162587741137,-0.0087087925865141323,0.0087282162587741137,-0.0087087925865141323,0.0087328334575760003,-0.008720356263351766,1.2451391359256568e-05,1.2451391359256568e-05,0.0010089228566527275,0.0010089228566527275,4.6171988018865739e-06,1.1563676837633707e-05 +100000,cable:1:core,cable:3:core,0.000879089354783231,-0.0031017341595444888,0.00087908935478323122,-0.0031017341595444914,0.00087200607389009807,-0.0031043854982987897,7.5632311481977735e-06,7.5632311481984825e-06,0.0023455286322702326,0.0023455286322704525,7.0832808931329313e-06,2.6513387543008679e-06 +100000,cable:2:core,cable:1:core,0.008728216258774112,-0.0087087925865141323,0.008728216258774112,-0.0087087925865141323,0.0087328334575760055,-0.0087203562633517712,1.2451391359263974e-05,1.2451391359263974e-05,0.001008922856653327,0.001008922856653327,4.6171988018935128e-06,1.1563676837638912e-05 +100000,cable:2:core,cable:2:core,0.091870181209119328,0.28525842189891248,0.091870181209119328,0.28525842189891248,0.091489317443228704,0.2836615002411198,0.0016417112990095785,0.0016417112990095785,0.0055081636054669222,0.0055081636054669222,0.00038086376589062387,0.0015969216577926826 +100000,cable:2:core,cable:3:core,0.0029200092671962073,-0.0055306020499575189,0.0029200092671962086,-0.0055306020499575198,0.0029229592659658302,-0.0055355130965314943,5.7289502696843057e-06,5.7289502696857185e-06,0.00091519117450634035,0.00091519117450656608,2.9499987696229163e-06,4.9110465739753834e-06 +100000,cable:3:core,cable:1:core,0.00087908935478322992,-0.0031017341595444897,0.00087908935478321431,-0.0031017341595444797,0.00087200607389009959,-0.003104385498298791,7.5632311481850722e-06,7.5632311481961972e-06,0.0023455286322662926,0.0023455286322697426,7.0832808931303292e-06,2.6513387543013016e-06 +100000,cable:3:core,cable:2:core,0.0029200092671962073,-0.0055306020499575181,0.0029200092671962086,-0.0055306020499575207,0.0029229592659658311,-0.0055355130965314952,5.7289502696847521e-06,5.7289502696876523e-06,0.00091519117450641159,0.00091519117450687487,2.9499987696237837e-06,4.9110465739771181e-06 +100000,cable:3:core,cable:3:core,0.02502944569653533,0.51501770227425014,0.025029445696535323,0.51501770227425014,0.024751108383294702,0.51186488670933217,0.0031650778262676369,0.0031650778262676373,0.0061762080540927926,0.0061762080540927935,0.00027833731324062791,0.0031528155649179768 +1000000,cable:1:core,cable:1:core,0.75407440792450842,1.5740479450662672,0.75407440792450842,1.5740479450662672,0.75371053759751805,1.5682785800867196,0.0057808281311672342,0.0057808281311672342,0.0033223274842110141,0.0033223274842110141,0.00036387032699036759,0.0057693649795476087 +1000000,cable:1:core,cable:2:core,0.0001535709764889112,0.0010629809235061856,0.0001535709764889112,0.0010629809235061856,0.00015809876830168704,0.001064782467487955,4.8730339028257937e-06,4.8730339028257937e-06,0.0045269246088306082,0.0045269246088306082,4.5277918127758479e-06,1.8015439817694115e-06 +1000000,cable:1:core,cable:3:core,8.1257499331941041e-05,8.9426642061850282e-05,8.1257499331935254e-05,8.9426642061852409e-05,8.1864957239162299e-05,8.9675897572887595e-05,6.5660750744571341e-07,6.5660750744116738e-07,0.0054075870204730444,0.0054075870204356056,6.0745790722125813e-07,2.4925551103731304e-07 +1000000,cable:2:core,cable:1:core,0.0001535709764889112,0.0010629809235061854,0.0001535709764889112,0.0010629809235061854,0.00015809876830168729,0.0010647824674879566,4.8730339028266611e-06,4.8730339028266611e-06,0.0045269246088314079,0.0045269246088314079,4.5277918127760918e-06,1.8015439817711462e-06 +1000000,cable:2:core,cable:2:core,0.75407440792448754,1.5740479450662619,0.75407440792448754,1.5740479450662619,0.753724628134118,1.5683153568234434,0.0057432494167894326,0.0057432494167894326,0.0033006559843933086,0.0033006559843933086,0.0003497797903695421,0.005732588242818526 +1000000,cable:2:core,cable:3:core,0.00017862565014624984,0.00022534477815653253,0.00017862565014624984,0.00022534477815653242,0.00018019558652217053,0.00022583671129566818,1.645204679612525e-06,1.6452046796124926e-06,0.0056943975606065797,0.005694397560606467,1.5699363759206879e-06,4.9193313913565608e-07 +1000000,cable:3:core,cable:1:core,8.1257499331941068e-05,8.9426642061850295e-05,8.1257499331935308e-05,8.9426642061852518e-05,8.1864957239162313e-05,8.9675897572887568e-05,6.5660750744562436e-07,6.5660750744113943e-07,0.0054075870204723124,0.0054075870204353758,6.0745790722124457e-07,2.4925551103727238e-07 +1000000,cable:3:core,cable:2:core,0.0001786256501462499,0.00022534477815653258,0.00017862565014624971,0.00022534477815653399,0.00018019558652217056,0.00022583671129566807,1.6452046796121777e-06,1.645204679612418e-06,0.0056943975606053793,0.0056943975606062111,1.5699363759206608e-06,4.9193313913549344e-07 +1000000,cable:3:core,cable:3:core,0.094696109689700492,4.9430140482755665,0.094696109689700422,4.9430140482755665,0.09317622230865559,4.9164327265927605,0.026624738873008071,0.026624738873008078,0.0054144865043148985,0.0054144865043149002,0.0015198873810449015,0.026581321682805914 +10000000,cable:1:core,cable:1:core,4.5479347057971937,6.6714050104071516,4.5479347057971937,6.6714050104071516,4.5499045633958612,6.6607366695104684,0.010848679018519814,0.010848679018519814,0.0013449195900571108,0.0013449195900571108,0.0019698575986675237,0.010668340896683226 +10000000,cable:1:core,cable:2:core,3.0492342492444631e-09,1.314231864570851e-08,3.0492342492444631e-09,1.314231864570851e-08,2.1359267021804777e-08,2.9940483256533021e-08,2.4848252140232918e-08,2.4848252140232918e-08,0.67562077331270665,0.67562077331270665,1.8310032772560315e-08,1.6798164610824511e-08 +10000000,cable:1:core,cable:3:core,6.8259441253474497e-10,1.2859327231771361e-10,6.8259441253474539e-10,1.2859327231771216e-10,1.6416933423796947e-09,-3.0218062359712461e-10,1.0513975968354568e-09,1.0513975968354576e-09,0.6298538404347922,0.62985384043479264,9.5909892984494966e-10,4.3077389591483822e-10 +10000000,cable:2:core,cable:1:core,3.0492342492444631e-09,1.314231864570851e-08,3.0492342492444631e-09,1.314231864570851e-08,2.1359267021804784e-08,2.9940483256533008e-08,2.4848252140232915e-08,2.4848252140232915e-08,0.67562077331270665,0.67562077331270665,1.8310032772560322e-08,1.6798164610824498e-08 +10000000,cable:2:core,cable:2:core,4.5479347057971937,6.6714050104071516,4.5479347057971937,6.6714050104071516,4.5501473574581013,6.6605028805599691,0.011124399425467658,0.011124399425467658,0.0013791104639083534,0.0013791104639083534,0.0022126516609075608,0.010902129847182529 +10000000,cable:2:core,cable:3:core,1.5377810805855466e-09,4.4664472044009271e-10,1.5377810805855466e-09,4.4664472044009214e-10,4.9275836606184257e-09,-7.7688175554226174e-10,3.6038560694105644e-09,3.6038560694105644e-09,0.72244015793167637,0.72244015793167637,3.3898025800328791e-09,1.2235264759823544e-09 +10000000,cable:3:core,cable:1:core,6.8259441253474777e-10,1.2859327231771482e-10,6.8259441253474715e-10,1.285932723177128e-10,1.6416933423796937e-09,-3.0218062359712528e-10,1.0513975968354547e-09,1.0513975968354551e-09,0.62985384043479131,0.62985384043479153,9.5909892984494594e-10,4.3077389591484008e-10 +10000000,cable:3:core,cable:2:core,1.5377810805855476e-09,4.4664472044009277e-10,1.5377810805855499e-09,4.4664472044009375e-10,4.9275836606184224e-09,-7.7688175554226101e-10,3.6038560694105582e-09,3.6038560694105603e-09,0.7224401579316756,0.72244015793167604,3.389802580032875e-09,1.2235264759823538e-09 +10000000,cable:3:core,cable:3:core,1.2514510556301524,49.784417123982301,1.2514510556301526,49.784417123982301,0.28635320029725086,47.884683853844997,2.1308215242095572,2.1308215242095572,0.044498224765926554,0.044498224765926554,0.96509785533290149,1.899733270137304 diff --git a/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/all_air-Y.csv b/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/all_air-Y.csv new file mode 100644 index 000000000..e7adfe9be --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/all_air-Y.csv @@ -0,0 +1,82 @@ +frequency_hz,receiver,source,new_real,new_imaginary,old_real,old_imaginary,fem_real,fem_imaginary,before_absolute_error,after_absolute_error,before_relative_error,after_relative_error,real_absolute_error,imaginary_absolute_error +0.10000000000000001,cable:1:core,cable:1:core,-1.5633838100332428e-24,9.5125437748382881e-12,8.8174985708896091e-24,9.5125437748370102e-12,8.922128316011062e-24,9.5737691434599193e-12,6.1225368622909116e-14,6.1225368621631187e-14,0.0063951164588853379,0.0063951164587518561,1.0485512126044304e-23,6.1225368621631187e-14 +0.10000000000000001,cable:1:core,cable:2:core,-5.3180305293633861e-25,-1.8912830746013254e-12,7.1523658381944664e-24,-1.8912830746024135e-12,7.2386070908688633e-24,-1.9065981897068207e-12,1.5315115104407224e-14,1.5315115105495322e-14,0.0080326915168015788,0.0080326915173722785,7.7704101438052021e-24,1.5315115105495322e-14 +0.10000000000000001,cable:1:core,cable:3:core,-1.1441727970654028e-25,-4.6083134754104461e-13,7.7202067554844912e-24,-4.6083134754232305e-13,7.8258392890097311e-24,-4.6709107939513815e-13,6.2597318528151031e-15,6.2597318540935374e-15,0.013401523019709932,0.013401523022446944,7.9402565687162711e-24,6.2597318540935374e-15 +0.10000000000000001,cable:2:core,cable:1:core,-5.3180304959283696e-25,-1.8912830746013262e-12,7.1523658415379676e-24,-1.891283074602413e-12,7.2386070908688677e-24,-1.9065981897068211e-12,1.5315115104408032e-14,1.5315115105494918e-14,0.0080326915168020003,0.0080326915173720651,7.7704101404617053e-24,1.5315115105494918e-14 +0.10000000000000001,cable:2:core,cable:2:core,-1.304873055325844e-24,9.8662437122090504e-12,6.3935813146373556e-24,9.8662437122081328e-12,6.4613855928501904e-24,9.9274412083039412e-12,6.1197496095808435e-14,6.1197496094890781e-14,0.0061644783194101382,0.0061644783193177026,7.7662586481760348e-24,6.1197496094890781e-14 +0.10000000000000001,cable:2:core,cable:3:core,-5.3180304959283696e-25,-1.8912830746013258e-12,7.1523658415379676e-24,-1.891283074602413e-12,7.2377500976610394e-24,-1.9067155107915219e-12,1.5432436189108823e-14,1.5432436190196113e-14,0.0080937277227594688,0.0080937277233297123,7.769553147253877e-24,1.5432436190196113e-14 +0.10000000000000001,cable:3:core,cable:1:core,-1.144172797065401e-25,-4.6083134754104451e-13,7.7202067554844912e-24,-4.6083134754232315e-13,7.8258392890097296e-24,-4.6709107939513845e-13,6.2597318528153051e-15,6.2597318540939413e-15,0.013401523019710355,0.013401523022447801,7.9402565687162696e-24,6.2597318540939413e-15 +0.10000000000000001,cable:3:core,cable:2:core,-5.3180305293633861e-25,-1.8912830746013258e-12,7.1523658381944649e-24,-1.891283074602413e-12,7.2377500976610423e-24,-1.9067155107915219e-12,1.5432436189108823e-14,1.5432436190196113e-14,0.0080937277227594688,0.0080937277233297123,7.7695531505973811e-24,1.5432436190196113e-14 +0.10000000000000001,cable:3:core,cable:3:core,-1.563383810033243e-24,9.5125437748382881e-12,8.8174985708896106e-24,9.5125437748370102e-12,8.9208262505232889e-24,9.5731276208940894e-12,6.0583846057079155e-14,6.0583846055801226e-14,0.0063285321638092695,0.0063285321636757782,1.0484210060556531e-23,6.0583846055801226e-14 +1,cable:1:core,cable:1:core,-1.6587484628463939e-22,9.5125437748370393e-11,6.8501479736650221e-22,9.5125437748243227e-11,7.0481422084003561e-22,9.5730364221324353e-11,6.0492647308112628e-13,6.0492647295396019e-13,0.0063190658262049764,0.0063190658248765989,8.7068906712467494e-22,6.0492647295396019e-13 +1,cable:1:core,cable:2:core,-6.1282691033068498e-23,-1.8912830746023943e-11,5.4808946526399042e-22,-1.891283074613199e-11,5.6476607782501255e-22,-1.9062309958920426e-11,1.4947921278843586e-13,1.4947921289648309e-13,0.0078416106500505903,0.0078416106557186987,6.2604876885808105e-22,1.4947921289648309e-13 +1,cable:1:core,cable:3:core,-2.0978087755451175e-23,-4.6083134754228781e-12,5.7530319150777122e-22,-4.6083134755499658e-12,5.9520194157421592e-22,-4.6680872622437967e-12,5.9773786693830928e-14,5.9773786820918667e-14,0.012804770634278937,0.012804770661503736,6.1618002932966713e-22,5.9773786820918667e-14 +1,cable:2:core,cable:1:core,-6.1282687714116693e-23,-1.8912830746023946e-11,5.4808946858294201e-22,-1.8912830746131994e-11,5.6476607782501142e-22,-1.9062309958920426e-11,1.4947921278843262e-13,1.4947921289647986e-13,0.0078416106500504221,0.0078416106557185304,6.2604876553912814e-22,1.4947921289647986e-13 +1,cable:2:core,cable:2:core,-1.3737131791915714e-22,9.8662437122081522e-11,4.9734272200821993e-22,9.8662437121989743e-11,5.1101837548125129e-22,9.9269687678295667e-11,6.0725055630592353e-13,6.0725055621414526e-13,0.0061171800829458323,0.0061171800820212976,6.4838969340040844e-22,6.0725055621414526e-13 +1,cable:2:core,cable:3:core,-6.1282687714116716e-23,-1.8912830746023946e-11,5.4808946858294211e-22,-1.8912830746131997e-11,5.6461582564736953e-22,-1.9065758134391154e-11,1.5292738825915747e-13,1.5292738836720794e-13,0.0080210494217538789,0.0080210494274211303,6.2589851336148625e-22,1.5292738836720794e-13 +1,cable:3:core,cable:1:core,-2.0978087755451172e-23,-4.6083134754228781e-12,5.753031915077715e-22,-4.6083134755499666e-12,5.9520194157421583e-22,-4.6680872622437911e-12,5.9773786693824466e-14,5.9773786820913012e-14,0.012804770634277568,0.01280477066150254,6.1618002932966704e-22,5.9773786820913012e-14 +1,cable:3:core,cable:2:core,-6.1282691033068486e-23,-1.8912830746023946e-11,5.4808946526399061e-22,-1.8912830746131997e-11,5.6461582564737038e-22,-1.9065758134391151e-11,1.5292738825915424e-13,1.5292738836720471e-13,0.0080210494217537107,0.0080210494274209621,6.2589851668043887e-22,1.5292738836720471e-13 +1,cable:3:core,cable:3:core,-1.6587484628463939e-22,9.5125437748370393e-11,6.8501479736650221e-22,9.5125437748243227e-11,7.0463369720842808e-22,9.5718203451660386e-11,5.9276570341715909e-13,5.92765703289993e-13,0.0061928210313361935,0.0061928210300076468,8.705085434930674e-22,5.92765703289993e-13 +10,cable:1:core,cable:1:core,-1.9349443967013251e-20,9.5125437748007416e-10,4.7166583903820262e-20,9.512543774675523e-10,5.2042336276529873e-20,9.5737066171470717e-10,6.116284247154873e-12,6.1162842346330137e-12,0.0063886271971195022,0.0063886271840400757,7.1391780243543127e-20,6.1162842346330129e-12 +10,cable:1:core,cable:2:core,-8.4747514536548164e-21,-1.891283074633155e-10,3.6686164480533427e-20,-1.891283074739372e-10,4.0811616664672256e-20,-1.9069227257271197e-10,1.5639650987747745e-12,1.5639651093964723e-12,0.0082015127182378418,0.0082015127739385671,4.9286368118327074e-20,1.5639651093964715e-12 +10,cable:1:core,cable:3:core,-4.8594067576892981e-21,-4.6083134757836114e-11,3.6212833098818979e-20,-4.6083134770283224e-11,4.1095181352562286e-20,-4.6709769832231127e-11,6.2663506194790321e-13,6.2663507439501505e-13,0.013415503099214725,0.013415503365692423,4.5954588110251586e-20,6.2663507439501334e-13 +10,cable:2:core,cable:1:core,-8.4747482119580589e-21,-1.8912830746331555e-10,3.6686167722230193e-20,-1.8912830747393725e-10,4.0811616664672268e-20,-1.9069227257271205e-10,1.5639650987748004e-12,1.5639651093964982e-12,0.0082015127182379736,0.0082015127739386989,4.9286364876630325e-20,1.5639651093964974e-12 +10,cable:2:core,cable:2:core,-1.5730906353677381e-20,9.8662437121819414e-10,3.4333568788473328e-20,9.8662437120915512e-10,3.7800341164145078e-20,9.9277769632842221e-10,6.1533251192670879e-12,6.1533251102280717e-12,0.0061980896045749781,0.0061980895954702039,5.3531247517822459e-20,6.1533251102280717e-12 +10,cable:2:core,cable:3:core,-8.4747482119580634e-21,-1.8912830746331555e-10,3.6686167722230193e-20,-1.891283074739372e-10,4.0810399278134823e-20,-1.9070192842709623e-10,1.5736209531590275e-12,1.5736209637806736e-12,0.0082517306780178143,0.0082517307337154483,4.9285147490092886e-20,1.5736209637806728e-12 +10,cable:3:core,cable:1:core,-4.8594067576892981e-21,-4.608313475783612e-11,3.6212833098818979e-20,-4.6083134770283224e-11,4.1095181352562256e-20,-4.6709769832231108e-11,6.2663506194788382e-13,6.266350743949892e-13,0.013415503099214316,0.013415503365691875,4.5954588110251556e-20,6.2663507439498749e-13 +10,cable:3:core,cable:2:core,-8.4747514536548179e-21,-1.8912830746331555e-10,3.6686164480533427e-20,-1.891283074739372e-10,4.0810399278134859e-20,-1.9070192842709623e-10,1.5736209531590275e-12,1.5736209637806736e-12,0.0082517306780178143,0.0082517307337154483,4.9285150731789677e-20,1.5736209637806728e-12 +10,cable:3:core,cable:3:core,-1.9349443967013254e-20,9.5125437748007416e-10,4.7166583903820256e-20,9.5125437746755209e-10,5.2026027179996697e-20,9.5731200695540039e-10,6.057629487848298e-12,6.0576294753262319e-12,0.0063277483660878322,0.0063277483530073883,7.1375471147009951e-20,6.0576294753262311e-12 +100,cable:1:core,cable:1:core,-2.7295121647878419e-18,9.5125437737306668e-09,2.1083091976300289e-18,9.5125437725313327e-09,3.4449521314309137e-18,9.5732682963375009e-09,6.0724523806168252e-11,6.0724522606834416e-11,0.0063431340192774055,0.0063431338939979503,6.1744642962187556e-18,6.0724522606834106e-11 +100,cable:1:core,cable:2:core,-1.5223928822208093e-18,-1.8912830755349785e-09,1.4561375241927259e-18,-1.8912830765424522e-09,2.5899675468275766e-18,-1.9075708161815633e-09,1.628773963911108e-11,1.6287740646585288e-11,0.008538471809772543,0.0085384723379176575,4.1123604290483863e-18,1.6287740646584767e-11 +100,cable:1:core,cable:3:core,-1.2797236657190993e-18,-4.608313486273717e-10,1.0295216100662539e-18,-4.6083134977899235e-10,2.3636665605984129e-18,-4.6738078380863707e-10,6.5494340296448564e-12,6.5494351812663839e-12,0.014013057995825596,0.01401306045981549,3.643390226317512e-18,6.5494351812653701e-12 +100,cable:2:core,cable:1:core,-1.5223898758252984e-18,-1.8912830755349698e-09,1.4561405305882495e-18,-1.891283076542444e-09,2.589967546827577e-18,-1.9075708161815624e-09,1.6287739639118525e-11,1.6287740646593146e-11,0.0085384718097764496,0.0085384723379217809,4.1123574226528751e-18,1.6287740646592626e-11 +100,cable:2:core,cable:2:core,-2.1466469028642353e-18,9.8662437114086359e-09,1.5506509108463473e-18,9.8662437105429252e-09,2.5115239067323068e-18,9.92729673521363e-09,6.1053024670704833e-11,6.1053023804994315e-11,0.006150015084583951,0.0061500149973788901,4.6581708095965421e-18,6.1053023804994135e-11 +100,cable:2:core,cable:3:core,-1.5223898758252982e-18,-1.8912830755349698e-09,1.4561405305882495e-18,-1.891283076542444e-09,2.590339306423551e-18,-1.9069723033144639e-09,1.5689226772019991e-11,1.5689227779494629e-11,0.0082272966129350242,0.0082272971412461256,4.1127291822488488e-18,1.5689227779494089e-11 +100,cable:3:core,cable:1:core,-1.2797236657190993e-18,-4.608313486273717e-10,1.0295216100662541e-18,-4.6083134977899245e-10,2.3636665605984129e-18,-4.6738078380863738e-10,6.5494340296450632e-12,6.5494351812666941e-12,0.01401305799582603,0.014013060459816144,3.643390226317512e-18,6.5494351812656803e-12 +100,cable:3:core,cable:2:core,-1.5223928822208089e-18,-1.8912830755349777e-09,1.4561375241927257e-18,-1.8912830765424522e-09,2.5903393064235491e-18,-1.9069723033144643e-09,1.5689226772012133e-11,1.5689227779487184e-11,0.0082272966129309025,0.0082272971412422208,4.1127321886443584e-18,1.5689227779486644e-11 +100,cable:3:core,cable:3:core,-2.7295121647878415e-18,9.5125437737306668e-09,2.1083091976300285e-18,9.5125437725313327e-09,3.4445149991456232e-18,9.5725309918854146e-09,5.9987219354081907e-11,5.9987218154748084e-11,0.0062665996490304148,0.0062665995237413119,6.1740271639334647e-18,5.9987218154747761e-11 +1000,cable:1:core,cable:1:core,-5.0118206940306591e-16,9.512543740175937e-08,-1.873808118781869e-16,9.5125437293721708e-08,1.9082675595006146e-16,9.568462842719928e-08,5.5919113347770054e-10,5.5919102544033804e-10,0.005844106233878054,0.0058441051047796368,6.9200882535312736e-16,5.5919102543990988e-10 +1000,cable:1:core,cable:2:core,-3.4574583514384184e-16,-1.8912831033485189e-08,-1.8946804337046478e-16,-1.8912831119165847e-08,1.3056430654978372e-16,-1.9077570753226516e-08,1.6473963406097957e-10,1.6473971974201554e-10,0.0086352521603473963,0.0086352566515395437,4.7631014169362561e-16,1.6473971974132696e-10 +1000,cable:1:core,cable:3:core,-3.5442949770542699e-16,-4.6083138025453172e-09,-2.8095123442940841e-16,-4.6083138873394025e-09,9.195269911084729e-17,-4.673636267226392e-09,6.532237988805393e-11,6.5322464682599963e-11,0.013976778712139707,0.013976796855303014,4.463821968162743e-16,6.5322464681074784e-11 +1000,cable:2:core,cable:1:core,-3.4574345870567365e-16,-1.8912831033481185e-08,-1.894656669322759e-16,-1.8912831119161843e-08,1.3056430654978367e-16,-1.9077570753226526e-08,1.6473963406499303e-10,1.6473971974602902e-10,0.008635252160557768,0.0086352566517499171,4.7630776525545727e-16,1.6473971974534044e-10 +1000,cable:2:core,cable:2:core,-3.7940269276055945e-16,9.8662436870980812e-08,-1.3229236679643231e-16,9.866243679281464e-08,1.4043760685025576e-16,9.9255655883493975e-08,5.9321909067939758e-10,5.9321901251339034e-10,0.0059766779575333882,0.0059766771700114421,5.198402996108152e-16,5.9321901251316255e-10 +1000,cable:2:core,cable:3:core,-3.4574345870567375e-16,-1.8912831033481185e-08,-1.8946566693227583e-16,-1.8912831119161843e-08,1.3055810960136316e-16,-1.9081350477120368e-08,1.685193579588281e-10,1.6851944363985561e-10,0.0088316263652770521,0.0088316308555791197,4.7630156830703693e-16,1.6851944363918249e-10 +1000,cable:3:core,cable:1:core,-3.5442949770542699e-16,-4.608313802545318e-09,-2.8095123442940836e-16,-4.6083138873394025e-09,9.1952699110847192e-17,-4.6736362672263928e-09,6.5322379888054757e-11,6.5322464682599963e-11,0.013976778712139882,0.013976796855303012,4.463821968162742e-16,6.5322464681074784e-11 +1000,cable:3:core,cable:2:core,-3.4574583514384184e-16,-1.8912831033485189e-08,-1.8946804337046485e-16,-1.8912831119165847e-08,1.3055810960136316e-16,-1.9081350477120351e-08,1.68519357954808e-10,1.6851944363583551e-10,0.0088316263650663786,0.0088316308553684462,4.7630394474520498e-16,1.6851944363516239e-10 +1000,cable:3:core,cable:3:core,-5.0118206940306571e-16,9.512543740175937e-08,-1.8738081187818683e-16,9.5125437293721708e-08,1.9086030057746862e-16,9.5694942729070334e-08,5.6950543534875172e-10,5.6950532731138385e-10,0.0059512594825530587,0.0059512583535762838,6.9204236998053433e-16,5.6950532731096333e-10 +10000,cable:1:core,cable:1:core,-1.1772692878024628e-13,9.5125425061182361e-07,-1.0101134288504447e-13,9.5125424198982738e-07,8.0692288813055717e-15,9.5706735548959623e-07,5.8131135007922797e-09,5.8131048791337479e-09,0.0060738812868803057,0.0060738722784667576,1.2579615766155186e-13,5.8131048777726271e-09 +10000,cable:1:core,cable:2:core,-9.1362925460009304e-14,-1.8912840908954329e-07,-8.6514466734997425e-14,-1.8912841431725653e-07,4.594027671235939e-15,-1.9067497289021409e-07,1.5465585756411843e-09,1.5465638036476314e-09,0.0081109678538234479,0.0081109952722432221,9.5956953131245239e-14,1.5465638006707943e-09 +10000,cable:1:core,cable:3:core,-1.0045430431703108e-13,-4.608324586372502e-08,-1.0143498648802649e-13,-4.6083248237275564e-08,1.7806485012887294e-15,-4.6609992529424619e-08,5.2674430226161561e-10,5.2674667562086237e-10,0.011301102482028603,0.011301153401565766,1.0223495281831981e-13,5.2674666569959834e-10 +10000,cable:2:core,cable:1:core,-9.1361682981908575e-14,-1.8912840908828286e-07,-8.6513224256953002e-14,-1.8912841431599611e-07,4.5940276712359398e-15,-1.9067497289021396e-07,1.5465585769013903e-09,1.5465638049078599e-09,0.0081109678604326368,0.0081109952788525289,9.595571065314451e-14,1.5465638019310999e-09 +10000,cable:2:core,cable:2:core,-8.6751292961194326e-14,9.866242787209633e-07,-7.2680192975604353e-14,9.8662427226833344e-07,6.0984080445449842e-15,9.9263730262546271e-07,6.0130303576453305e-09,6.0130239052162803e-09,0.0060576308604776852,0.0060576243601889769,9.284970100573931e-14,6.0130239044994142e-09 +10000,cable:2:core,cable:3:core,-9.1361682981908562e-14,-1.891284090882828e-07,-8.6513224256953027e-14,-1.8912841431599611e-07,4.5935820977784853e-15,-1.9069242896894837e-07,1.5640146556058312e-09,1.56401988360908e-09,0.0082017658701096577,0.0082017932860027638,9.5955265079687046e-14,1.5640198806655711e-09 +10000,cable:3:core,cable:1:core,-1.0045430431703113e-13,-4.6083245863725027e-08,-1.0143498648802654e-13,-4.608324823727559e-08,1.7806485012887271e-15,-4.6609992529424632e-08,5.2674430226160238e-10,5.2674667562086899e-10,0.011301102482028317,0.011301153401565905,1.0223495281831985e-13,5.2674666569960496e-10 +10000,cable:3:core,cable:2:core,-9.1362925460009165e-14,-1.8912840908954321e-07,-8.65144667349974e-14,-1.891284143172565e-07,4.5935820977784813e-15,-1.9069242896894851e-07,1.5640146543456509e-09,1.564019882348877e-09,0.0082017658635012061,0.0082017932793941942,9.5956507557787649e-14,1.5640198794052919e-09 +10000,cable:3:core,cable:3:core,-1.1772692878024628e-13,9.512542506118234e-07,-1.0101134288504453e-13,9.5125424198982759e-07,8.0711091903339482e-15,9.5711687567891938e-07,5.8626336901066072e-09,5.8626250684456517e-09,0.0061253059465156941,0.0061252969385656988,1.2579803797058023e-13,5.8626250670959883e-09 +100000,cable:1:core,cable:1:core,-3.5238754535009548e-11,9.5124847980076869e-06,-3.4604866529119772e-11,9.5124842961546061e-06,2.6076244926853928e-13,9.5701919094738434e-06,5.770762385174679e-08,5.7707122385223088e-08,0.0060299338192602095,0.0060298814204652381,3.5499516984278088e-11,5.770711146615649e-08 +100000,cable:1:core,cable:2:core,-2.8499648231873601e-11,-1.8913281513774834e-06,-2.8436018490168415e-11,-1.8913282851842096e-06,1.325603233389172e-13,-1.9071973370109952e-06,1.5869077542343327e-08,1.5869211463529829e-08,0.008320626940059429,0.0083206971589003965,2.8632208555212517e-11,1.586918563351188e-08 +100000,cable:1:core,cable:3:core,-3.166763593333253e-11,-4.608786997428773e-07,-3.1713786251802083e-11,-4.6087867059802101e-07,4.7645418153283817e-14,-4.6624005749939556e-07,5.3614809796148935e-09,5.3614515620725e-09,0.011499400133850181,0.011499337038580007,3.1715281351485816e-11,5.3613577565182542e-09 +100000,cable:2:core,cable:1:core,-2.849927538271401e-11,-1.8913281510653945e-06,-2.8435645641065308e-11,-1.8913282848721205e-06,1.3256032333891698e-13,-1.9071973370109952e-06,1.5869077853760675e-08,1.5869211774945476e-08,0.0083206271033447587,0.0083206973221848363,2.8631835706052925e-11,1.5869185945600746e-08 +100000,cable:2:core,cable:2:core,-2.5663216825296612e-11,9.8662002709805753e-06,-2.5082672191159045e-11,9.8661998438227087e-06,2.023866598338372e-13,9.9263181440735071e-06,6.0118305568099178e-08,6.0117878657245518e-08,0.006056455646043615,0.0060564126380674987,2.5865603485130448e-11,6.0117873092931795e-08 +100000,cable:2:core,cable:3:core,-2.849927538271401e-11,-1.8913281510653945e-06,-2.8435645641065292e-11,-1.8913282848721201e-06,1.3259940800503111e-13,-1.90690759831909e-06,1.5579339640163257e-08,1.5579473563472328e-08,0.0081699499513747647,0.0081700201819979949,2.8631874790719042e-11,1.5579447253695464e-08 +100000,cable:3:core,cable:1:core,-3.166763593333253e-11,-4.6087869974287757e-07,-3.1713786251802077e-11,-4.6087867059802069e-07,4.7645418153283849e-14,-4.6624005749939535e-07,5.3614809796149993e-09,5.3614515620720236e-09,0.011499400133850413,0.011499337038578991,3.1715281351485816e-11,5.3613577565177778e-09 +100000,cable:3:core,cable:2:core,-2.8499648231873591e-11,-1.8913281513774834e-06,-2.8436018490168438e-11,-1.8913282851842098e-06,1.3259940800503118e-13,-1.9069075983190895e-06,1.5579339328757355e-08,1.5579473252068792e-08,0.0081699497880706289,0.0081700200186950993,2.8632247639878624e-11,1.5579446941606174e-08 +100000,cable:3:core,cable:3:core,-3.5238754535009541e-11,9.5124847980076869e-06,-3.4604866529119772e-11,9.5124842961546061e-06,2.6082462984435911e-13,9.5706034093503467e-06,5.8119123653714196e-08,5.8118622184453578e-08,0.0060726707782011525,0.0060726183813731606,3.54995791648539e-11,5.8118611342659752e-08 +1000000,cable:1:core,cable:1:core,-1.5615266547687123e-08,9.5093614110976026e-05,-1.559589973180337e-08,9.5093595392883751e-05,8.0530792168868948e-12,9.5693400052747324e-05,6.000075943997591e-07,5.9998938662500328e-07,0.006270104250335193,0.0062699139783337198,1.5623319626904009e-08,5.9978594177129835e-07 +1000000,cable:1:core,cable:2:core,-1.2864123870071459e-08,-1.893554815543121e-05,-1.2861993771875996e-08,-1.8935550648911418e-05,4.1819271429109229e-12,-1.9086254578921379e-05,1.5125215038988852e-07,1.5125481603998266e-07,0.0079246637817000452,0.007924803445041699,1.286830579721437e-08,1.5070642349016971e-07 +1000000,cable:1:core,cable:3:core,-1.4543198704248249e-08,-4.6305956615392597e-06,-1.4544270082946612e-08,-4.6305946147002577e-06,1.620331950101396e-12,-4.6777057948875268e-06,4.9305640920684913e-08,4.9304324606339191e-08,0.010540560497534986,0.010540279095838634,1.4544819036198351e-08,4.7110133348267081e-08 +1000000,cable:2:core,cable:1:core,-1.2864022244693608e-08,-1.8935548062092855e-05,-1.2861892146596569e-08,-1.89355505555729e-05,4.1819271429109229e-12,-1.9086254578921386e-05,1.5125223474544779e-07,1.5125490039399239e-07,0.0079246682014021338,0.0079248078646626009,1.2868204171836519e-08,1.5070651682853165e-07 +1000000,cable:2:core,cable:2:core,-1.1326024378319978e-08,9.8638679408527877e-05,-1.1308330073687395e-08,9.8638662485275188e-05,6.228755939294857e-12,9.9256357247823405e-05,6.1779838047781191e-07,6.1778178438498085e-07,0.0062242701385392465,0.0062241029342080456,1.1332253134259273e-08,6.1767783929552789e-07 +1000000,cable:2:core,cable:3:core,-1.2864022244693615e-08,-1.8935548062092855e-05,-1.2861892146596574e-08,-1.893555055557291e-05,4.1817462845641913e-12,-1.9088669470170267e-05,1.5365851055144858e-07,1.5366117364371122e-07,0.008049723465093573,0.0080498629767691669,1.286820399097818e-08,1.5312140807741204e-07 +1000000,cable:3:core,cable:1:core,-1.4543198704248249e-08,-4.6305956615392597e-06,-1.4544270082946622e-08,-4.6305946147002577e-06,1.6203319501013948e-12,-4.6777057948875277e-06,4.9305640920685727e-08,4.9304324606339999e-08,0.010540560497535158,0.010540279095838804,1.4544819036198351e-08,4.7110133348267928e-08 +1000000,cable:3:core,cable:2:core,-1.2864123870071464e-08,-1.8935548155431213e-05,-1.2861993771875996e-08,-1.8935550648911421e-05,4.1817462845641962e-12,-1.908866947017027e-05,1.5365842605000035e-07,1.536610891437818e-07,0.0080497190383079145,0.0080498585500630732,1.2868305616356029e-08,1.5312131473905688e-07 +1000000,cable:3:core,cable:3:core,-1.5615266547687126e-08,9.5093614110976026e-05,-1.5595899731803376e-08,9.5093595392883751e-05,8.05380599468505e-12,9.5700186415770239e-05,6.0679168782415467e-07,6.0677347427375065e-07,0.006340548650427295,0.0063403583315670481,1.562332035368181e-08,6.0657230479421341e-07 +10000000,cable:1:core,cable:1:core,-1.0245582137099509e-05,0.00093666552576450414,-1.0244974735870984e-05,0.00093666492829382747,2.8187296625399645e-10,0.00093504980244725748,1.0371784536962841e-05,1.0372477580147157e-05,0.011092226863015033,0.011092968046193193,1.0245864010065763e-05,1.6157233172466512e-06 +10000000,cable:1:core,cable:2:core,-8.4133474228971043e-06,-0.00019733225671301211,-8.4132746995742166e-06,-0.0001973323193329975,1.5097250181016025e-10,-0.00020541214081067286,1.166487233752054e-05,1.1664968164594844e-05,0.056787647952458817,0.056788114463707504,8.4134983953989146e-06,8.0798840976607506e-06 +10000000,cable:1:core,cable:3:core,-9.4191344030158392e-06,-5.1467309144819749e-05,-9.4191677365102664e-06,-5.1467267275120134e-05,7.0525640853636929e-11,-5.9850382341684698e-05,1.2609467382028214e-05,1.2609414645891618e-05,0.21068315503871385,0.21068227390588676,9.4192049286566933e-06,8.3830731968649486e-06 +10000000,cable:2:core,cable:1:core,-8.4133199344201284e-06,-0.00019733222067529809,-8.4132472113751319e-06,-0.00019733228329474613,1.5097250181016004e-10,-0.00020541214081067278,1.1664877473773147e-05,1.1664973300254706e-05,0.056787672957079247,0.05678813946544229,8.4134709069219387e-06,8.0799201353746878e-06 +10000000,cable:2:core,cable:2:core,-7.3114509332476665e-06,0.00097540345949388219,-7.3108984596087289e-06,0.00097540292219751854,2.121268563158776e-10,0.00097614442289829665,7.3486162844970809e-06,7.3491117470868954e-06,0.007528205982756048,0.0075287135537447179,7.3116630601039827e-06,7.4096340441445491e-07 +10000000,cable:2:core,cable:3:core,-8.4133199344201318e-06,-0.0001973322206752982,-8.4132472113751319e-06,-0.00019733228329474613,1.5099445474206141e-10,-0.00020537275581301788,1.1637631532530349e-05,1.1637727371438444e-05,0.056665897511365693,0.056666364169699779,8.4134709288748745e-06,8.0405351377196843e-06 +10000000,cable:3:core,cable:1:core,-9.4191344030158291e-06,-5.1467309144819716e-05,-9.4191677365102597e-06,-5.1467267275120114e-05,7.0525640853637097e-11,-5.9850382341684651e-05,1.260946738202819e-05,1.2609414645891603e-05,0.21068315503871363,0.21068227390588667,9.4192049286566831e-06,8.383073196864935e-06 +10000000,cable:3:core,cable:2:core,-8.4133474228971111e-06,-0.00019733225671301224,-8.4132746995742132e-06,-0.00019733231933299748,1.5099445474206126e-10,-0.00020537275581301799,1.1637626506216432e-05,1.1637722345715908e-05,0.056665873037262962,0.056666339698476606,8.4134984173518538e-06,8.0404991000057472e-06 +10000000,cable:3:core,cable:3:core,-1.0245582137099514e-05,0.00093666552576450457,-1.0244974735870988e-05,0.00093666492829382747,2.8186944950225374e-10,0.00093522723119616807,1.034563946082993e-05,1.0346324009328286e-05,0.011062166621899204,0.011062898581443924,1.0245864006549016e-05,1.4382945683365017e-06 diff --git a/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/all_air-Z.csv b/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/all_air-Z.csv new file mode 100644 index 000000000..1121bc150 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/all_air-Z.csv @@ -0,0 +1,82 @@ +frequency_hz,receiver,source,new_real,new_imaginary,old_real,old_imaginary,fem_real,fem_imaginary,before_absolute_error,after_absolute_error,before_relative_error,after_relative_error,real_absolute_error,imaginary_absolute_error +0.10000000000000001,cable:1:core,cable:1:core,3.1368030977512313e-06,1.2442491410079335e-06,3.1368030977512313e-06,1.2442491410079333e-06,3.1562835747436832e-06,1.2381208184182731e-06,2.0421687531063286e-08,2.0421687531063349e-08,0.006023318557434835,0.0060233185574348541,1.9480476992451874e-08,6.1283225896603621e-09 +0.10000000000000001,cable:1:core,cable:2:core,9.836575756099057e-08,8.1595650851660634e-07,9.8365757560990584e-08,8.1595650851660624e-07,9.8235674073162367e-08,8.1275015799742355e-07,3.2089882152586086e-09,3.2089882152587141e-09,0.003919779856153092,0.0039197798561532203,1.3008348782820304e-10,3.2063505191827915e-09 +0.10000000000000001,cable:1:core,cable:3:core,9.8363910649020915e-08,7.2885335558399048e-07,9.8363910649020915e-08,7.2885335558399048e-07,9.8230350799228339e-08,7.2601055923285115e-07,2.8459320665693526e-09,2.8459320665693526e-09,0.0038845644572714816,0.0038845644572714816,1.3355984979257542e-10,2.8427963511393303e-09 +0.10000000000000001,cable:2:core,cable:1:core,9.836575756099057e-08,8.1595650851660634e-07,9.8365757560990584e-08,8.1595650851660624e-07,9.823567407316238e-08,8.1275015799742355e-07,3.2089882152586082e-09,3.2089882152587137e-09,0.0039197798561530911,0.0039197798561532203,1.3008348782818981e-10,3.2063505191827915e-09 +0.10000000000000001,cable:2:core,cable:2:core,3.1368030977512313e-06,1.2442491410079335e-06,3.1368030977512313e-06,1.2442491410079333e-06,3.1562852353826078e-06,1.2384264704427096e-06,2.0333646480624677e-08,2.0333646480624737e-08,0.005997150910064389,0.0059971509100644063,1.9482137631376542e-08,5.8226705652239005e-09 +0.10000000000000001,cable:2:core,cable:3:core,9.836575756099057e-08,8.1595650851660634e-07,9.8365757560990584e-08,8.1595650851660624e-07,9.8235588324076653e-08,8.1275893040631542e-07,3.200226523490131e-09,3.2002265234902365e-09,0.0039090359121177433,0.0039090359121178716,1.3016923691391704e-10,3.1975781102909203e-09 +0.10000000000000001,cable:3:core,cable:1:core,9.8363910649020915e-08,7.2885335558399048e-07,9.8363910649020915e-08,7.2885335558399048e-07,9.8230350799228273e-08,7.2601055923285019e-07,2.8459320665703076e-09,2.8459320665703076e-09,0.00388456445727279,0.00388456445727279,1.3355984979264159e-10,2.8427963511402832e-09 +0.10000000000000001,cable:3:core,cable:2:core,9.836575756099057e-08,8.1595650851660634e-07,9.8365757560990584e-08,8.1595650851660624e-07,9.8235588324076587e-08,8.1275893040631479e-07,3.2002265234907684e-09,3.2002265234908739e-09,0.0039090359121185247,0.003909035912118654,1.3016923691398321e-10,3.1975781102915556e-09 +0.10000000000000001,cable:3:core,cable:3:core,3.1368030977512313e-06,1.2442491410079335e-06,3.1368030977512313e-06,1.2442491410079333e-06,3.1562834239407481e-06,1.2381628940589164e-06,2.0408956621405036e-08,2.0408956621405099e-08,0.0060195365806944637,0.0060195365806944819,1.9480326189516824e-08,6.0862469490171018e-09 +1,cable:1:core,cable:1:core,4.0258262108767965e-06,1.1002345673096759e-05,4.0258262108768049e-06,1.1002345673096755e-05,4.0441335787761909e-06,1.0950406484961215e-05,5.5071217378795182e-08,5.5071217378801198e-08,0.004717699299481245,0.0047176992994817602,1.8307367899394447e-08,5.1939188135543635e-08 +1,cable:1:core,cable:2:core,9.7665254323323244e-07,6.7199804225952308e-06,9.7665254323323498e-07,6.7199804225952341e-06,9.7539641318315375e-07,6.696910388622472e-06,2.3104205898647905e-08,2.3104205898644384e-08,0.0034139587900371472,0.0034139587900366272,1.2561300500786954e-09,2.3070033972758757e-08 +1,cable:1:core,cable:3:core,9.7651034829616332e-07,5.8489747237081976e-06,9.7651034817260789e-07,5.8489747235720515e-06,9.7491585773259541e-07,5.8296236366655687e-06,1.9416667176051124e-08,1.9416667321883764e-08,0.0032850688975510005,0.0032850689222241468,1.5944905635679037e-09,1.9351087042628942e-08 +1,cable:2:core,cable:1:core,9.7665254323323244e-07,6.7199804225952308e-06,9.7665254323323498e-07,6.7199804225952341e-06,9.7539641318315396e-07,6.6969103886224737e-06,2.3104205898646201e-08,2.310420589864268e-08,0.0034139587900368948,0.0034139587900363744,1.2561300500784836e-09,2.3070033972757063e-08 +1,cable:2:core,cable:2:core,4.0258262108767965e-06,1.1002345673096759e-05,4.0258262108768049e-06,1.1002345673096755e-05,4.0443033445780337e-06,1.0953379895793153e-05,5.23359514746707e-08,5.233595147467686e-08,0.0044822879684521307,0.0044822879684526589,1.8477133701237262e-08,4.8965777303605965e-08 +1,cable:2:core,cable:3:core,9.7665254323323244e-07,6.7199804225952308e-06,9.7665254323323498e-07,6.7199804225952341e-06,9.7539495757458187e-07,6.6972194654576678e-06,2.2795672648706442e-08,2.2795672648702921e-08,0.0033682167140297956,0.0033682167140292756,1.2575856586505703e-09,2.2760957137562972e-08 +1,cable:3:core,cable:1:core,9.7651034829616332e-07,5.8489747237081976e-06,9.7651034817260789e-07,5.8489747235720515e-06,9.7491585773259584e-07,5.8296236366655721e-06,1.9416667176047709e-08,1.9416667321880353e-08,0.0032850688975504206,0.0032850689222235678,1.5944905635674802e-09,1.9351087042625554e-08 +1,cable:3:core,cable:2:core,9.7665254323323244e-07,6.7199804225952308e-06,9.7665254323323498e-07,6.7199804225952341e-06,9.7539495757458145e-07,6.6972194654576703e-06,2.2795672648703931e-08,2.2795672648700407e-08,0.0033682167140294231,0.0033682167140289027,1.2575856586509938e-09,2.2760957137560431e-08 +1,cable:3:core,cable:3:core,4.0258262108767965e-06,1.1002345673096759e-05,4.0258262108768049e-06,1.1002345673096755e-05,4.0441325229622845e-06,1.0951493272211791e-05,5.4047088154091449e-08,5.404708815409751e-08,0.0046295626551396338,0.0046295626551401533,1.830631208548799e-08,5.0852400884968092e-08 +10,cable:1:core,cable:1:core,1.3444055602265216e-05,9.5357202577227293e-05,1.344405560226531e-05,9.5357202577227266e-05,1.3464438746537811e-05,9.4931342329192611e-05,4.2634777286456104e-07,4.263477728645927e-07,0.0044466141225671524,0.004446614122567482,2.0383144272595211e-08,4.25860248034682e-07 +10,cable:1:core,cable:2:core,9.5543897693967477e-06,5.2957258634098345e-05,9.5543897693967985e-06,5.2957258634098372e-05,9.5453284797023948e-06,5.280770955685593e-05,1.4982334088850044e-07,1.498233408884703e-07,0.0027919058276288339,0.0027919058276282718,9.0612896943528435e-09,1.4954907724241495e-07 +10,cable:1:core,cable:3:core,9.5443477251560087e-06,4.4249689272014522e-05,9.5443477251560104e-06,4.4249689272014495e-05,9.5227205948054659e-06,4.4151513573041263e-05,1.0052960079539756e-07,1.0052960079542367e-07,0.0022257418472240091,0.0022257418472245872,2.1627130350542766e-08,9.8175698973258546e-08 +10,cable:2:core,cable:1:core,9.5543897693967477e-06,5.2957258634098345e-05,9.5543897693967985e-06,5.2957258634098372e-05,9.5453284797023982e-06,5.2807709556855943e-05,1.4982334088848671e-07,1.4982334088845658e-07,0.0027919058276285771,0.0027919058276280159,9.0612896943494554e-09,1.495490772424014e-07 +10,cable:2:core,cable:2:core,1.3444055602265216e-05,9.5357202577227279e-05,1.344405560226531e-05,9.5357202577227252e-05,1.3470778065139108e-05,9.4951611885806569e-05,4.0647004685357752e-07,4.0647004685361076e-07,0.004238371968339167,0.0042383719683395131,2.6722462873892839e-08,4.0559069142071045e-07 +10,cable:2:core,cable:3:core,9.5543897693967494e-06,5.2957258634098345e-05,9.5543897693968002e-06,5.2957258634098372e-05,9.545361931628984e-06,5.2809202699622741e-05,1.4833091919156415e-07,1.4833091919153401e-07,0.0027640190818798002,0.0027640190818792386,9.0278377677653913e-09,1.4805593447560385e-07 +10,cable:3:core,cable:1:core,9.5443477251560087e-06,4.4249689272014522e-05,9.5443477251560104e-06,4.4249689272014495e-05,9.5227205948054608e-06,4.4151513573041283e-05,1.0052960079537881e-07,1.0052960079540491e-07,0.0022257418472235928,0.0022257418472241704,2.1627130350547848e-08,9.8175698973238217e-08 +10,cable:3:core,cable:2:core,9.5543897693967477e-06,5.2957258634098345e-05,9.5543897693967985e-06,5.2957258634098372e-05,9.5453619316289823e-06,5.2809202699622741e-05,1.4833091919156415e-07,1.4833091919153401e-07,0.0027640190818798002,0.0027640190818792386,9.0278377677653913e-09,1.4805593447560385e-07 +10,cable:3:core,cable:3:core,1.3444055602265216e-05,9.5357202577227293e-05,1.344405560226531e-05,9.5357202577227266e-05,1.346444009793527e-05,9.4933039705599208e-05,4.2465241001491889e-07,4.2465241001495054e-07,0.0044288546208002873,0.0044288546208006178,2.0384495670053911e-08,4.2416287162808488e-07 +100,cable:1:core,cable:1:core,0.00010029767252434723,0.00079835032747907573,0.00010029767252434697,0.00079835032747907519,0.00010109173736374871,0.00079398372433502344,4.4382160815824066e-06,4.4382160815828937e-06,0.0055450428979991924,0.0055450428979998004,7.9406483940148661e-07,4.3666031440522859e-06 +100,cable:1:core,cable:2:core,8.9513730825568297e-05,0.00039183825668436418,8.951373082556884e-05,0.00039183825668436429,8.9419166063820077e-05,0.00039105609314453346,7.878593130789853e-07,7.8785931307881262e-07,0.0019640058494340357,0.0019640058494336055,9.4564761748220923e-08,7.8216353983071897e-07 +100,cable:1:core,cable:3:core,8.8905381208391612e-05,0.00030498496831731425,8.8905381208391422e-05,0.00030498496831731561,8.8761745324787293e-05,0.00030479687079552149,2.3666842789824236e-07,2.366684278972804e-07,0.00074551025733565681,0.00074551025733262657,1.4363588360431865e-07,1.8809752179275736e-07 +100,cable:2:core,cable:1:core,8.9513730825568297e-05,0.00039183825668436418,8.951373082556884e-05,0.00039183825668436429,8.9419166063820144e-05,0.00039105609314453335,7.8785931307908483e-07,7.8785931307891214e-07,0.0019640058494342846,0.001964005849433854,9.456476174815316e-08,7.8216353983082739e-07 +100,cable:2:core,cable:2:core,0.00010029767252434672,0.00079835032747907389,0.00010029767252434647,0.00079835032747907334,0.00010111751936580979,0.00079411805013570192,4.3109535319546705e-06,4.3109535319551542e-06,0.0053851244828224459,0.0053851244828230505,8.1984684146306291e-07,4.232277343371965e-06 +100,cable:2:core,cable:3:core,8.9513730825568297e-05,0.00039183825668436418,8.951373082556884e-05,0.00039183825668436429,8.9418021427622023e-05,0.00039104147926472346,8.0250516839758451e-07,8.0250516839741224e-07,0.0020005879183127391,0.0020005879183123097,9.5709397946274594e-08,7.9677741964071782e-07 +100,cable:3:core,cable:1:core,8.8905381208391612e-05,0.00030498496831731425,8.8905381208391422e-05,0.00030498496831731561,8.8761745324787293e-05,0.0003047968707955216,2.3666842789815618e-07,2.3666842789719421e-07,0.00074551025733538522,0.00074551025733235498,1.4363588360431865e-07,1.8809752179264894e-07 +100,cable:3:core,cable:2:core,8.9513730825568297e-05,0.00039183825668436418,8.951373082556884e-05,0.00039183825668436429,8.9418021427621982e-05,0.00039104147926472346,8.0250516839758938e-07,8.0250516839741701e-07,0.0020005879183127516,0.0020005879183123219,9.5709397946315252e-08,7.9677741964071782e-07 +100,cable:3:core,cable:3:core,0.00010029767252434721,0.00079835032747907573,0.00010029767252434695,0.00079835032747907519,0.00010109212945278526,0.00079400305315175887,4.4192709679411133e-06,4.4192709679416004e-06,0.0055212405402839532,0.0055212405402845612,7.94456928438045e-07,4.3472743273168619e-06 +1000,cable:1:core,cable:1:core,0.00078861955828074461,0.0066682243696765085,0.00078861955828074494,0.0066682243696765085,0.00078601699198523078,0.0066168492252231129,5.1441022724376449e-05,5.1441022724376435e-05,0.0077199694890550587,0.007719969489055057,2.6025662955138319e-06,5.1375144453395631e-05 +1000,cable:1:core,cable:2:core,0.00074708526686114655,0.0026749096440145913,0.00074708526686115175,0.0026749096440145926,0.00074588926809238511,0.0026721634166045451,2.9953594179309089e-06,2.9953594179276381e-06,0.0010796764074255197,0.0010796764074243407,1.1959987687614393e-06,2.7462274100461745e-06 +1000,cable:1:core,cable:3:core,0.00071978017636864153,0.0018225752382672376,0.00071978017636863936,0.0018225752382672528,0.00071859077164575073,0.001824605919080821,2.3533695335462284e-06,2.3533695335604217e-06,0.0012000804519261116,0.0012000804519333493,1.1894047228907991e-06,2.0306808135833309e-06 +1000,cable:2:core,cable:1:core,0.00074708526686114655,0.0026749096440145913,0.00074708526686115175,0.0026749096440145926,0.00074588926809238414,0.0026721634166045438,2.9953594179324912e-06,2.9953594179292203e-06,0.0010796764074260908,0.0010796764074249118,1.195998768762415e-06,2.7462274100474755e-06 +1000,cable:2:core,cable:2:core,0.00078861955828031115,0.0066682243696753107,0.00078861955828031147,0.0066682243696753107,0.0007861049543362728,0.0066165057334950745,5.1779731182568038e-05,5.1779731182568024e-05,0.0077711865836618413,0.0077711865836618396,2.5146039440383447e-06,5.1718636180236172e-05 +1000,cable:2:core,cable:3:core,0.00074708526686114666,0.0026749096440145913,0.00074708526686115186,0.0026749096440145926,0.0007458750783723221,0.0026721706045853234,2.9944771118823559e-06,2.9944771118790626e-06,0.0010793571711456673,0.0010793571711444803,1.2101884888245616e-06,2.7390394292678769e-06 +1000,cable:3:core,cable:1:core,0.00071978017636864164,0.0018225752382672379,0.00071978017636863947,0.001822575238267253,0.00071859077164575008,0.0018246059190808221,2.3533695335473604e-06,2.3533695335615537e-06,0.0012000804519266882,0.0012000804519339261,1.1894047228915581e-06,2.0306808135841983e-06 +1000,cable:3:core,cable:2:core,0.00074708526686114655,0.0026749096440145913,0.00074708526686115175,0.0026749096440145926,0.0007458750783723209,0.0026721706045853247,2.9944771118816041e-06,2.9944771118783109e-06,0.001079357171145396,0.0010793571711442088,1.2101884888256458e-06,2.7390394292665758e-06 +1000,cable:3:core,cable:3:core,0.0007886195582807445,0.0066682243696765085,0.00078861955828074483,0.0066682243696765085,0.00078598630833147539,0.0066163157830859678,5.1975334218482814e-05,5.1975334218482801e-05,0.0078007802258625103,0.0078007802258625077,2.6332499492691137e-06,5.1908586590540713e-05 +10000,cable:1:core,cable:1:core,0.0051948061676279642,0.05689636052577024,0.0051948061676279703,0.05689636052577033,0.0051024009974631997,0.056494349006063566,0.00041249482111915353,0.00041249482111906425,0.0072719246745931469,0.0072719246745915735,9.2405170164764489e-05,0.00040201151970667426 +10000,cable:1:core,cable:2:core,0.0048255232285051635,0.017433544047368715,0.0048255232285051653,0.017433544047368715,0.0048010353251144531,0.017380520692016812,5.8404910968574194e-05,5.8404910968573462e-05,0.0032390616548324314,0.0032390616548323907,2.4487903390710448e-05,5.3023355351903417e-05 +10000,cable:1:core,cable:3:core,0.0041592668664023873,0.0096192362424460456,0.0041592668664024281,0.0096192362424460786,0.0041388512938834643,0.0096080024171414403,2.3302240927691969e-05,2.3302240927640361e-05,0.0022274199513152054,0.0022274199513102723,2.0415572518923085e-05,1.1233825304605288e-05 +10000,cable:2:core,cable:1:core,0.0048255232285051635,0.017433544047368715,0.0048255232285051653,0.017433544047368715,0.004801035325114454,0.017380520692016805,5.840491096858013e-05,5.8404910968579398e-05,0.0032390616548327619,0.0032390616548327211,2.4487903390709581e-05,5.3023355351910356e-05 +10000,cable:2:core,cable:2:core,0.0051948061673494214,0.056896360525121779,0.0051948061673494274,0.05689636052512187,0.0051037076429676729,0.056498447347698745,0.00040820808163423238,0.0004082080816341431,0.0071958204857737705,0.0071958204857721971,9.1098524381748487e-05,0.00039791317742303434 +10000,cable:2:core,cable:3:core,0.0048255232285051635,0.017433544047368715,0.0048255232285051653,0.017433544047368715,0.0048009326760847524,0.017380882154370954,5.8120308347846536e-05,5.8120308347845804e-05,0.0032232205619593434,0.003223220561959303,2.459055242041118e-05,5.2661892997761528e-05 +10000,cable:3:core,cable:1:core,0.0041592668664023873,0.0096192362424460456,0.0041592668664024281,0.0096192362424460786,0.0041388512938834634,0.0096080024171414386,2.3302240927693565e-05,2.3302240927641957e-05,0.0022274199513153581,0.002227419951310425,2.0415572518923952e-05,1.1233825304607023e-05 +10000,cable:3:core,cable:2:core,0.0048255232285051635,0.017433544047368715,0.0048255232285051653,0.017433544047368715,0.0048009326760847567,0.017380882154370954,5.8120308347844707e-05,5.8120308347843968e-05,0.0032232205619592415,0.0032232205619592003,2.4590552420406843e-05,5.2661892997761528e-05 +10000,cable:3:core,cable:3:core,0.0051948061676279642,0.05689636052577024,0.0051948061676279703,0.05689636052577033,0.0051020282511443343,0.05649251949500398,0.00041436133979592309,0.0004143613397958338,0.0073050687451673936,0.0073050687451658193,9.2777916483629923e-05,0.0004038410307662596 +100000,cable:1:core,cable:1:core,0.025031226245057811,0.51501980111689405,0.025031226245057804,0.51501980111689405,0.024707771051829367,0.51187134433942161,0.0031650281739091352,0.0031650281739091361,0.0061760586330841874,0.0061760586330841891,0.00032345519322844393,0.0031484567774724415 +100000,cable:1:core,cable:2:core,0.021596902160099213,0.12625600561257661,0.021596902160095976,0.12625600561268494,0.021555696037850045,0.12588810726341756,0.00037019878430946077,0.00037019878420216422,0.0028985127206438191,0.0028985127198037286,4.1206122249167615e-05,0.00036789834915904041 +100000,cable:1:core,cable:3:core,0.01554830730155091,0.059605199227129396,0.015548307301550915,0.059605199227129396,0.015531651186217865,0.059558666775737366,4.9423630082577968e-05,4.9423630082576213e-05,0.00080297659635237694,0.00080297659635234842,1.6656115333045507e-05,4.6532451392029828e-05 +100000,cable:2:core,cable:1:core,0.021596902160099216,0.12625600561257661,0.021596902160095979,0.12625600561268494,0.021555696037850031,0.12588810726341754,0.00037019878430949026,0.00037019878420219371,0.0028985127206440503,0.0028985127198039601,4.1206122249184962e-05,0.00036789834915906816 +100000,cable:2:core,cable:2:core,0.02503122613141193,0.51501980073292042,0.025031226131411923,0.51501980073292042,0.02469664297746775,0.5119012048567243,0.0031364926471985645,0.0031364926471985654,0.0061200262228040874,0.0061200262228040892,0.00033458315394417973,0.0031185958761961174 +100000,cable:2:core,cable:3:core,0.021596902160099213,0.12625600561257661,0.021596902160095976,0.12625600561268494,0.021556266821685061,0.12587112637670345,0.00038701841948086171,0.00038701841937347034,0.0030305987949418313,0.003030598794100889,4.0635338414151539e-05,0.00038487923587315898 +100000,cable:3:core,cable:1:core,0.015548307301550912,0.059605199227129396,0.015548307301550917,0.059605199227129396,0.015531651186217852,0.059558666775737415,4.9423630082536911e-05,4.9423630082535156e-05,0.00080297659635170928,0.00080297659635168077,1.6656115333059385e-05,4.6532451391981255e-05 +100000,cable:3:core,cable:2:core,0.021596902160099213,0.12625600561257661,0.021596902160095976,0.12625600561268494,0.021556266821685061,0.12587112637670345,0.00038701841948086171,0.00038701841937347034,0.0030305987949418313,0.003030598794100889,4.0635338414151539e-05,0.00038487923587315898 +100000,cable:3:core,cable:3:core,0.025031226245057811,0.51501980111689405,0.025031226245057804,0.51501980111689405,0.024708832082088631,0.51184523252392566,0.003190897013032208,0.0031908970130322088,0.0062268539233451305,0.0062268539233451322,0.00032239416296917997,0.0031745685929683853 +1000000,cable:1:core,cable:1:core,0.094696040918173865,4.9430135931550625,0.094696040918173796,4.9430135931550625,0.092973237518383781,4.9153612050697939,0.027706003291211687,0.02770600329121169,0.0056356078961270102,0.0056356078961270111,0.0017228033997900849,0.027652388085268598 +1000000,cable:1:core,cable:2:core,0.077567018860665804,1.0935618501004387,0.077567018860665735,1.0935618501004389,0.076735943913257512,1.092058127543065,0.0017180998509297644,0.0017180998509296035,0.001569398093823283,0.001569398093823136,0.0008310749474082918,0.0015037225573737434 +1000000,cable:1:core,cable:3:core,0.051247474729972854,0.48773084535257633,0.051247474729972764,0.48773084535257655,0.05049478438383373,0.48942360593674289,0.0018525607014287209,0.0018525607014289603,0.0037652024876764761,0.0037652024876769627,0.00075269034613912461,0.0016927605841665638 +1000000,cable:2:core,cable:1:core,0.077567018860665804,1.0935618501004385,0.077567018860665735,1.0935618501004387,0.07673594391325754,1.0920581275430643,0.0017180998509301396,0.0017180998509299789,0.0015693980938236267,0.0015693980938234799,0.00083107494740826404,0.0015037225573741875 +1000000,cable:2:core,cable:2:core,0.094696003143432794,4.9430132917981755,0.094696003143432725,4.9430132917981755,0.092901264398151592,4.9160910746438828,0.026981972938736398,0.026981972938736401,0.0054875216991341746,0.0054875216991341754,0.0017947387452812019,0.02692221715429266 +1000000,cable:2:core,cable:3:core,0.077567018860665818,1.0935618501004387,0.077567018860665748,1.0935618501004389,0.076735621529719203,1.092127905866533,0.0016575335857403374,0.0016575335857401802,0.0015139776156323829,0.0015139776156322393,0.00083139733094661494,0.0014339442339057484 +1000000,cable:3:core,cable:1:core,0.051247474729972854,0.48773084535257627,0.051247474729972764,0.48773084535257649,0.050494784383833737,0.48942360593674267,0.0018525607014285659,0.0018525607014288053,0.003765202487676163,0.0037652024876766495,0.00075269034613911767,0.0016927605841663973 +1000000,cable:3:core,cable:2:core,0.07756701886066579,1.0935618501004387,0.077567018860665721,1.0935618501004389,0.076735621529719272,1.0921279058665325,0.0016575335857406729,0.0016575335857405156,0.0015139776156326899,0.0015139776156325461,0.0008313973309465178,0.0014339442339061925 +1000000,cable:3:core,cable:3:core,0.094696040918173865,4.9430135931550634,0.094696040918173796,4.9430135931550634,0.092968258794488681,4.9150976317878659,0.027969378436457814,0.027969378436457817,0.0056894854502846327,0.0056894854502846327,0.0017277821236851842,0.027915961367197539 +10000000,cable:1:core,cable:1:core,1.251332219253861,49.783940499020225,1.2513322192538612,49.783940499020225,0.53121976717410324,51.365101859919996,1.7374214206238898,1.7374214206238898,0.033823129904337439,0.033823129904337439,0.72011245207975771,1.5811613608997703 +10000000,cable:1:core,cable:2:core,1.180799668295279,11.256469738395747,1.180799668295279,11.256469738395747,0.46679343008479252,13.069443957199377,1.9485072302278281,1.9485072302278281,0.14899376034263651,0.14899376034263651,0.71400623821048659,1.81297421880363 +10000000,cable:1:core,cable:3:core,1.0678706383895686,5.1747359052981334,1.0678706383895686,5.1747359052981334,0.36536213202748608,6.9748137137964967,1.9323038886674515,1.9323038886674515,0.27666089794727172,0.27666089794727172,0.70250850636208262,1.8000778084983633 +10000000,cable:2:core,cable:1:core,1.180799668295279,11.256469738395744,1.180799668295279,11.256469738395744,0.46679343008479313,13.069443957199393,1.9485072302278461,1.9485072302278461,0.1489937603426377,0.1489937603426377,0.71400623821048592,1.8129742188036495 +10000000,cable:2:core,cable:2:core,1.2512831793321839,49.783625781844535,1.2512831793321841,49.783625781844535,0.52795194458770356,51.326274764561461,1.7038115972825583,1.703811597282558,0.033193943564225004,0.033193943564225004,0.72333123474448036,1.5426489827169263 +10000000,cable:2:core,cable:3:core,1.1807996682952793,11.256469738395747,1.1807996682952793,11.256469738395747,0.46209227148974574,13.00760233714735,1.8928829072696307,1.8928829072696307,0.14542953948724663,0.14542953948724663,0.71870739680553353,1.7511325987516031 +10000000,cable:3:core,cable:1:core,1.0678706383895684,5.1747359052981334,1.0678706383895684,5.1747359052981334,0.36536213202748624,6.9748137137965012,1.9323038886674555,1.9323038886674555,0.27666089794727211,0.27666089794727211,0.70250850636208217,1.8000778084983677 +10000000,cable:3:core,cable:2:core,1.180799668295279,11.256469738395745,1.180799668295279,11.256469738395745,0.46209227148974569,13.007602337147338,1.8928829072696207,1.8928829072696207,0.14542953948724599,0.14542953948724599,0.71870739680553331,1.7511325987515924 +10000000,cable:3:core,cable:3:core,1.2513322192538612,49.783940499020247,1.2513322192538614,49.783940499020247,0.52185045273660624,51.239123582658166,1.6277903595325474,1.6277903595325471,0.031766857627179998,0.031766857627179998,0.72948176651725494,1.4551830836379196 diff --git a/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/all_earth-Y.csv b/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/all_earth-Y.csv new file mode 100644 index 000000000..c540d834c --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/all_earth-Y.csv @@ -0,0 +1,82 @@ +frequency_hz,receiver,source,new_real,new_imaginary,old_real,old_imaginary,fem_real,fem_imaginary,before_absolute_error,after_absolute_error,before_relative_error,after_relative_error,real_absolute_error,imaginary_absolute_error +0.10000000000000001,cable:1:core,cable:1:core,11.716539656200831,0.090920014937197369,11.716539655942917,0.090920015180965472,11.793927042362855,0.09163136261981264,0.0773906557155805,0.07739065545991744,0.0065617092401915635,0.0065617092185147,0.07738738616202312,0.00071134768261527093 +0.10000000000000001,cable:1:core,cable:2:core,-6.5764429132332642,0.064998853118795732,-6.5764429134272966,0.064998853290850048,-6.6206073939719623,0.065515807071277354,0.044167505962995623,0.044167506159028466,0.0066708899844850457,0.0066708900140930909,0.044164480738698053,0.0005169539524816219 +0.10000000000000001,cable:1:core,cable:3:core,-3.2506966806898179,0.090585504122632499,-3.2506966801752673,0.090585503665267256,-3.2760387858376196,0.091292028100808498,0.025351952512180406,0.025351951985083492,0.0077355966791449477,0.0077355965183127937,0.025342105147801774,0.0007065239781759991 +0.10000000000000001,cable:2:core,cable:1:core,-6.5764429126890791,0.06499886041922423,-6.5764429128831035,0.064998860591277005,-6.6206073939719552,0.065515807071276772,0.044167506421697915,0.044167506617722722,0.0066708900537656799,0.0066708900833725125,0.044164481282876089,0.0005169466520525412 +0.10000000000000001,cable:2:core,cable:2:core,14.505974265569115,0.046571869286876094,14.505974265861992,0.046571869010316257,14.599945452367439,0.046947359746437323,0.093971936696731331,0.093971936988500759,0.0064364247916477895,0.0064364248116319687,0.093971186798324169,0.00037549045956122851 +0.10000000000000001,cable:2:core,cable:3:core,-6.5764429126890844,0.064998860419225785,-6.5764429128831114,0.064998860591280475,-6.6204481639483124,0.065512170527795427,0.044008244777567443,0.044008244973588094,0.0066469956736024327,0.0066469957032093486,0.04400525125922794,0.00051331010856964232 +0.10000000000000001,cable:3:core,cable:1:core,-3.2506966806898188,0.09058550412263261,-3.2506966801752739,0.090585503665269226,-3.2760387858376205,0.091292028100809677,0.025351952512174612,0.025351951985083523,0.0077355966791431783,0.0077355965183128006,0.025342105147801774,0.00070652397817706769 +0.10000000000000001,cable:3:core,cable:2:core,-6.5764429132332678,0.064998853118797009,-6.5764429134272895,0.064998853290850103,-6.6204481639483124,0.065512170527793637,0.04400824431857886,0.044008244514594196,0.0066469956042768969,0.0066469956338830105,0.044005250715044575,0.00051331740899662748 +0.10000000000000001,cable:3:core,cable:3:core,11.716539656200839,0.09092001493719605,11.716539655942919,0.090920015180963779,11.793670814855151,0.091621161120561595,0.07713434566243918,0.077134345406745755,0.0065401196423399343,0.0065401196206600247,0.077131158654312415,0.00070114618336554468 +1,cable:1:core,cable:1:core,11.873714513026494,0.13877147446534627,11.873714513026528,0.13877147446532462,11.951697089000811,0.13971411387969557,0.077988272994409066,0.077988272994442553,0.0065248428124142976,0.0065248428124170992,0.077982575974317569,0.00094263941434929999 +1,cable:1:core,cable:2:core,-6.4640237929904476,0.098479018213251987,-6.4640237929904574,0.098479018213286862,-6.5080852722992288,0.09917856437094108,0.044067032163557202,0.044067032163567527,0.0067703347820267275,0.0067703347820283139,0.044061479308781237,0.00069954615768909323 +1,cable:1:core,cable:3:core,-3.0939192122952939,0.13607439977274632,-3.0939192122953183,0.13607439977272814,-3.119361002943668,0.13702747250048025,0.025459635877577678,0.025459635877601406,0.008153947858134657,0.0081539478581422568,0.025441790648374152,0.00095307272773392793 +1,cable:2:core,cable:1:core,-6.4640237850781093,0.098479081846017405,-6.464023785078119,0.098479081846053071,-6.5080852722992155,0.099178564370941497,0.044067039064787109,0.044067039064797441,0.0067703358423122787,0.0067703358423138659,0.044061487221106255,0.00069948252492409158 +1,cable:2:core,cable:2:core,14.586483697135645,0.070755812768748191,14.586483697135661,0.070755812768684048,14.681691188828179,0.071273811947358101,0.095208900831434906,0.095208900831450533,0.006484796453189415,0.0064847964531904792,0.095207491692534063,0.00051799917860990974 +1,cable:2:core,cable:3:core,-6.4640237850781084,0.098479081846017516,-6.4640237850781137,0.098479081846052835,-6.5086850876535216,0.099186062512871348,0.044666897915518956,0.044666897915524841,0.006861864072795717,0.0068618640727966208,0.044661302575413231,0.00070698066685383198 +1,cable:3:core,cable:1:core,-3.0939192122952925,0.13607439977274649,-3.0939192122953187,0.13607439977272792,-3.1193610029436729,0.13702747250047986,0.025459635877582109,0.025459635877607596,0.0081539478581360639,0.0081539478581442257,0.025441790648380369,0.00095307272773337282 +1,cable:3:core,cable:2:core,-6.4640237929904476,0.098479018213251418,-6.4640237929904538,0.098479018213287278,-6.5086850876535101,0.099186062512871709,0.044666891011373409,0.044666891011380189,0.0068618630121599893,0.006861863012161031,0.044661294663062456,0.00070704429962029081 +1,cable:3:core,cable:3:core,11.873714513026496,0.13877147446534671,11.873714513026522,0.13877147446532426,11.952479429588399,0.1397353161481425,0.078770813578310409,0.078770813578336776,0.0065898821665799062,0.0065898821665821127,0.07876491656190332,0.00096384168279578519 +10,cable:1:core,cable:1:core,12.120029298528808,0.24051657223886777,12.120029298528864,0.24051657223890441,12.198656471407986,0.24185580199235357,0.078638577373295687,0.078638577373353141,0.0064452283085030104,0.0064452283085077193,0.078627172879178531,0.0013392297534857989 +10,cable:1:core,cable:2:core,-6.2890199013663937,0.16552322577944362,-6.2890199013664025,0.16552322577943707,-6.3332980379686301,0.16650732013666913,0.044289071142545544,0.04428907114255428,0.0069906346418380999,0.006990634641839479,0.044278136602236451,0.00098409435722551786 +10,cable:1:core,cable:3:core,-2.8515486558341041,0.22001574158962375,-2.8515486558341587,0.22001574158959175,-2.8767392780483174,0.22134516984312108,0.02522567792979501,0.025225677929847871,0.0087430018601509681,0.0087430018601692902,0.02519062221421331,0.0013294282534973278 +10,cable:2:core,cable:1:core,-6.2890197833970021,0.16552372276044355,-6.2890197833970136,0.16552372276043659,-6.3332980379686363,0.16650732013666858,0.044289178042812648,0.044289178042824034,0.0069906515150912714,0.0069906515150930694,0.044278254571634257,0.00098359737622502608 +10,cable:2:core,cable:2:core,14.711826696535155,0.12082715757884763,14.711826696535178,0.12082715757887073,14.807136000816893,0.12155291122243583,0.09531206744696942,0.095312067446992679,0.0064366840498130988,0.0064366840498146696,0.095309304281737894,0.00072575364358820615 +10,cable:2:core,cable:3:core,-6.2890197833970056,0.16552372276044061,-6.289019783397011,0.16552372276043406,-6.333049865123205,0.16652237292191072,0.044041405506186383,0.044041405506191567,0.0069518146186880779,0.0069518146186888959,0.044030081726199377,0.00099865016147010488 +10,cable:3:core,cable:1:core,-2.8515486558341059,0.22001574158962234,-2.8515486558341592,0.22001574158959078,-2.8767392780483072,0.22134516984312255,0.025225677929784498,0.02522567792983605,0.0087430018601473564,0.008743001860165224,0.025190622214201319,0.0013294282535002144 +10,cable:3:core,cable:2:core,-6.2890199013663901,0.16552322577944287,-6.2890199013663972,0.16552322577943612,-6.3330498651232174,0.16652237292191016,0.044041298839148882,0.044041298839155828,0.0069517977815898354,0.0069517977815909317,0.044029963756827328,0.00099914714246729375 +10,cable:3:core,cable:3:core,12.120029298528806,0.24051657223886974,12.120029298528857,0.24051657223890555,12.198600133535841,0.24189549926584741,0.078582934237913696,0.078582934237965821,0.0064406971080819796,0.0064406971080862522,0.0785708350070351,0.0013789270269776721 +100,cable:1:core,cable:1:core,12.563025703739259,0.51721927479150309,12.563025703739317,0.51721927479147634,12.641497780100403,0.51955597655167107,0.078506859213295546,0.078506859213353347,0.0062050114328727904,0.0062050114328773588,0.078472076361144616,0.0023367017601679807 +100,cable:1:core,cable:2:core,-5.98500069543738,0.32573071565504808,-5.9850006954373791,0.32573071565508882,-6.0295953731137866,0.32733885698691378,0.044623664076426955,0.044623664076427531,0.0073898906143090182,0.0073898906143091136,0.044594677676406569,0.001608141331865709 +100,cable:1:core,cable:3:core,-2.446514278290135,0.37618640379323293,-2.4465142782902065,0.37618640379320623,-2.470174538771686,0.37813339888204062,0.02374023411694599,0.023740234117015056,0.0095000867116944322,0.0095000867117220698,0.02366026048155101,0.0019469950888076903 +100,cable:2:core,cable:1:core,-5.9849990645039277,0.32573357082851701,-5.9849990645039268,0.32573357082855753,-6.0295953731137963,0.32733885698691145,0.044625191151152786,0.044625191151153355,0.0073901435051331316,0.0073901435051332252,0.044596308609868629,0.0016052861583944478 +100,cable:2:core,cable:2:core,14.9298902985444,0.25587575596028456,14.929890298544413,0.25587575596022161,15.026860913558655,0.25703830837642216,0.096977583514753135,0.096977583514764806,0.0064526716430934467,0.0064526716430942238,0.096970615014255301,0.0011625524161376077 +100,cable:2:core,cable:3:core,-5.9849990645039286,0.32573357082851723,-5.9849990645039286,0.32573357082855769,-6.0293971898343495,0.32733839277942345,0.044427119942100003,0.04442711994210146,0.0073575831247469888,0.0073575831247472299,0.044398125330420868,0.0016048219509062212 +100,cable:3:core,cable:1:core,-2.4465142782901381,0.37618640379323198,-2.4465142782902083,0.37618640379320556,-2.4701745387716816,0.37813339888204167,0.023740234116939936,0.023740234117007698,0.0095000867116920261,0.0095000867117191433,0.02366026048154346,0.0019469950888096887 +100,cable:3:core,cable:2:core,-5.98500069543738,0.3257307156550488,-5.9850006954373818,0.32573071565508926,-6.029397189834353,0.32733839277942295,0.044425593304720248,0.044425593304723489,0.007357330298062798,0.0073573302980633349,0.044396494396973019,0.0016076771243741517 +100,cable:3:core,cable:3:core,12.563025703739264,0.51721927479150287,12.563025703739322,0.51721927479147711,12.641296501223044,0.51953417178615768,0.078305022104803307,0.078305022104861149,0.0061891574827913041,0.006189157482795876,0.078270797483780186,0.0023148969946548137 +1000,cable:1:core,cable:1:core,13.595998739240526,1.4465325051151612,13.595998739240571,1.4465325051151292,13.670105690820352,1.4521245610642923,0.07431763829796087,0.074317638298002753,0.0054060922772405417,0.0054060922772435878,0.074106951579825164,0.0055920559491311472 +1000,cable:1:core,cable:2:core,-5.3532086996206294,0.79705038235169645,-5.3532086996206329,0.79705038235174319,-5.3950324845310735,0.80018548575660087,0.04194112370447349,0.041941123704480526,0.0076899031008897676,0.0076899031008910582,0.041823784910444139,0.0031351034049044157 +1000,cable:1:core,cable:3:core,-1.7399318837813844,0.63681540275493598,-1.7399318837814481,0.63681540275489368,-1.7593388574871698,0.6398182528281865,0.019637915800238218,0.019637915800294729,0.010489955575026549,0.010489955575056735,0.019406973705785369,0.0030028500732505181 +1000,cable:2:core,cable:1:core,-5.3531955340018786,0.79705183192885243,-5.3531955340018831,0.79705183192889983,-5.3950324845310762,0.80018548575660153,0.041954144204047611,0.041954144204055584,0.0076922904088874153,0.0076922904088888768,0.041836950529197559,0.0031336538277491055 +1000,cable:2:core,cable:2:core,15.394199476046905,0.79008767845702987,15.394199476046927,0.79008767845696459,15.487580066580868,0.79211282136113192,0.093402547568307542,0.093402547568327429,0.0060229309186851498,0.0060229309186864326,0.093380590533962859,0.0020251429041020508 +1000,cable:2:core,cable:3:core,-5.3531955340018795,0.79705183192885209,-5.3531955340018857,0.7970518319289005,-5.3953801447260359,0.80012652115813576,0.042296513991160765,0.042296513991170487,0.0077545872364662878,0.0077545872364680702,0.042184610724156357,0.0030746892292836669 +1000,cable:3:core,cable:1:core,-1.7399318837813844,0.63681540275493553,-1.7399318837814481,0.6368154027548929,-1.7593388574871671,0.63981825282818849,0.019637915800236008,0.019637915800292467,0.010489955575025378,0.010489955575055537,0.019406973705782704,0.0030028500732529606 +1000,cable:3:core,cable:2:core,-5.3532086996206267,0.79705038235169601,-5.3532086996206329,0.79705038235174441,-5.395380144726043,0.80012652115813565,0.04228348864787293,0.042283488647882651,0.0077521991871619886,0.007752199187163771,0.042171445105416261,0.0030761388064396433 +1000,cable:3:core,cable:3:core,13.595998739240521,1.4465325051151596,13.595998739240573,1.446532505115127,13.670305930691081,1.4519094313608261,0.074501476744519221,0.074501476744568251,0.0054193957081953353,0.0054193957081989019,0.074307191450559529,0.0053769262456664357 +10000,cable:1:core,cable:1:core,17.046534552968179,4.2120541635759645,17.04653455296819,4.2120541635757602,17.128574149133918,4.2360094951678624,0.085465509129258405,0.085465509129211373,0.0048437210339593193,0.0048437210339566539,0.082039596165738971,0.023955331591897888 +10000,cable:1:core,cable:2:core,-3.3892442630089872,2.0891967225415851,-3.3892442630087292,2.0891967225412542,-3.4249228006495258,2.1014698251850854,0.037730453173686632,0.037730453173335024,0.0093897903233363857,0.0093897903232488828,0.035678537640538632,0.012273102643500344 +10000,cable:1:core,cable:3:core,-0.43825470996742211,0.85392294859442852,-0.43825470996925953,0.85392294859473339,-0.44874265125902446,0.86232080591781557,0.013435807386100814,0.013435807387725649,0.01382150757153951,0.013821507573210991,0.010487941291602354,0.0083978573233870479 +10000,cable:2:core,cable:1:core,-3.389283429007929,2.0891704641805133,-3.3892834290076714,2.0891704641801838,-3.4249228006495258,2.1014698251850819,0.037701977297683216,0.03770197729733224,0.0093827036736289348,0.0093827036735415897,0.035639371641596806,0.012299361004568521 +10000,cable:2:core,cable:2:core,17.284752427180987,3.3655305049302604,17.284752427181022,3.3655305049304496,17.377466344145962,3.3763680984739541,0.093345186446843004,0.09334518644690025,0.0052730139931134231,0.0052730139931166566,0.092713916964974885,0.010837593543693735 +10000,cable:2:core,cable:3:core,-3.3892834290079294,2.0891704641805124,-3.3892834290076705,2.0891704641801834,-3.4248494411361543,2.1016072413487854,0.037677773886801151,0.037677773886448135,0.0093766584919387306,0.0093766584918508773,0.035566012128224855,0.012436777168272961 +10000,cable:3:core,cable:1:core,-0.43825470996742194,0.85392294859442863,-0.43825470996925953,0.85392294859473372,-0.44874265125902463,0.86232080591781735,0.013435807386101846,0.013435807387726951,0.013821507571540549,0.013821507573212306,0.010487941291602687,0.0083978573233887133 +10000,cable:3:core,cable:2:core,-3.3892442630089881,2.0891967225415837,-3.3892442630087296,2.0891967225412542,-3.4248494411361552,2.1016072413487867,0.037706096145820024,0.037706096145467508,0.0093837068953645277,0.0093837068952767992,0.035605178127167125,0.012410518807203008 +10000,cable:3:core,cable:3:core,17.046534552968179,4.2120541635759663,17.04653455296819,4.2120541635757611,17.128908606191871,4.2364262897459461,0.085903929936605811,0.085903929936557821,0.0048684511517558373,0.0048684511517531172,0.082374053223691845,0.024372126169979857 +100000,cable:1:core,cable:1:core,25.114009604140684,8.0616329121337866,25.114009604140684,8.0616329121337866,25.211046058879496,8.1198439722222862,0.11315741718908486,0.11315741718908486,0.0042722858396769137,0.0042722858396769137,0.097036454738812949,0.058211060088499522 +100000,cable:1:core,cable:2:core,0.17423938863795488,1.0511044112983252,0.17423938863795493,1.051104411298325,0.17447040684075316,1.0607706304717672,0.0096689793938681268,0.0096689793938679047,0.0089942077693072487,0.0089942077693070423,0.00023101820279827767,0.0096662191734420144 +100000,cable:1:core,cable:3:core,0.049169280797893136,-0.045706655302344225,0.049169280797892866,-0.045706655302347673,0.051001167620121454,-0.045409721143611068,0.0018557961159780922,0.0018557961159772732,0.027176280454428874,0.02717628045441688,0.001831886822228318,0.00029693415873315726 +100000,cable:2:core,cable:1:core,0.17424088729365148,1.0510922187905325,0.17424088729365159,1.0510922187905325,0.17447040684075332,1.0607706304717672,0.0096811327794820955,0.0096811327794820972,0.0090055130033820947,0.0090055130033820964,0.00022951954710184741,0.0096784116812347332 +100000,cable:2:core,cable:2:core,25.080124615561157,8.0878898568648587,25.080124615561157,8.0878898568648587,25.173967497452402,8.1446398751713485,0.10966791262461896,0.10966791262461896,0.0041448686019930599,0.0041448686019930599,0.0938428818912449,0.056750018306489736 +100000,cable:2:core,cable:3:core,0.17424088729365148,1.0510922187905325,0.17424088729365153,1.0510922187905325,0.17443042293000227,1.0608281024344142,0.0097377283842100225,0.0097377283842100225,0.0090577358045038203,0.0090577358045038203,0.00018953563635079607,0.0097358836438816709 +100000,cable:3:core,cable:1:core,0.04916928079789315,-0.045706655302344218,0.049169280797892859,-0.045706655302347653,0.051001167620121447,-0.045409721143611116,0.0018557961159780811,0.0018557961159772437,0.0271762804544287,0.027176280454416436,0.0018318868222282972,0.00029693415873310175 +100000,cable:3:core,cable:2:core,0.17423938863795488,1.051104411298325,0.17423938863795507,1.051104411298325,0.17443042293000166,1.0608281024344137,0.009725567510988152,0.0097255675109881555,0.0090464241338092111,0.0090464241338092145,0.00019103429204678224,0.0097236911360887301 +100000,cable:3:core,cable:3:core,25.114009604140684,8.0616329121337849,25.114009604140684,8.0616329121337849,25.212261890133007,8.1202583933699799,0.1144135426988117,0.1144135426988117,0.00431950170976351,0.00431950170976351,0.098252285992323607,0.058625481236195043 +1000000,cable:1:core,cable:1:core,40.833774626185878,19.549173406842751,40.833774626185878,19.549173406842751,40.899981093321514,19.656825006121288,0.12638102356686592,0.12638102356686592,0.002785048006455541,0.002785048006455541,0.06620646713563616,0.10765159927853674 +1000000,cable:1:core,cable:2:core,-0.020403824114795528,-0.018991043972656708,-0.020403824114795521,-0.018991043972656708,-0.020561862633581049,-0.019100623519793053,0.0001923118575921154,0.00019231185759210971,0.0068524612517472088,0.0068524612517470058,0.00015803851878552072,0.00010957954713634518 +1000000,cable:1:core,cable:3:core,-1.7943027849737e-05,-1.177264572726311e-05,-1.7943027849736949e-05,-1.1772645727263113e-05,-1.6683076928938046e-05,-1.5016074300379346e-05,3.4795553209754845e-06,3.4795553209755061e-06,0.15502131367650746,0.15502131367650843,1.2599509207989541e-06,3.2434285731162361e-06 +1000000,cable:2:core,cable:1:core,-0.020403824161670955,-0.018991043990879461,-0.020403824161670955,-0.018991043990879461,-0.020561862633581056,-0.019100623519793056,0.0001923118086873636,0.0001923118086873636,0.0068524595091717828,0.0068524595091717828,0.00015803847191010051,0.00010957952891359549 +1000000,cable:2:core,cable:2:core,40.833783909159962,19.549186896797089,40.833783909159962,19.549186896797089,40.89643056210452,19.651398854301153,0.11988280686557391,0.11988280686557391,0.0026421705289196174,0.0026421705289196174,0.062646652944557957,0.10221195750406409 +1000000,cable:2:core,cable:3:core,-0.020403824161670955,-0.018991043990879461,-0.020403824161670952,-0.018991043990879461,-0.02056147587072071,-0.019100875567408322,0.0001921380664239248,0.00019213806642392195,0.0068462959984796025,0.006846295998479501,0.00015765170904975465,0.00010983157652886058 +1000000,cable:3:core,cable:1:core,-1.7943027849736996e-05,-1.1772645727263103e-05,-1.7943027849736942e-05,-1.1772645727263105e-05,-1.6683076928938059e-05,-1.5016074300379334e-05,3.4795553209754739e-06,3.4795553209754951e-06,0.15502131367650696,0.1550213136765079,1.2599509207989372e-06,3.2434285731162311e-06 +1000000,cable:3:core,cable:2:core,-0.020403824114795518,-0.018991043972656704,-0.020403824114795518,-0.018991043972656704,-0.0205614758707207,-0.019100875567408311,0.00019213811530244143,0.00019213811530244143,0.0068462977401271569,0.0068462977401271569,0.0001576517559251818,0.00010983159475160681 +1000000,cable:3:core,cable:3:core,40.833774626185878,19.549173406842755,40.833774626185878,19.549173406842755,40.898365654929883,19.65540082251125,0.12432322724984333,0.12432322724984333,0.0027398256795864925,0.0027398256795864925,0.064591028744004575,0.10622741566849569 +10000000,cable:1:core,cable:1:core,80.846531351828517,55.108195401013525,80.846531351828517,55.108195401013525,80.823352857111601,55.216286708477909,0.11054851137261967,0.11054851137261967,0.0011293843148707356,0.0011293843148707356,0.02317849471691602,0.10809130746438456 +10000000,cable:1:core,cable:2:core,-9.26641813988093e-08,-1.3479772233800525e-07,-9.26641813988093e-08,-1.3479772233800525e-07,-2.9631405210173127e-07,-1.886966884946216e-07,2.1066173926479604e-07,2.1066173926479604e-07,0.59967121560503911,0.59967121560503911,2.0364987070292197e-07,5.3898966156616346e-08 +10000000,cable:1:core,cable:3:core,-1.8002386369702588e-16,1.9542236956564906e-16,-1.8002386369701979e-16,1.9542236956570295e-16,-1.0854589216983598e-15,6.4134107784461141e-16,1.0092849640468347e-15,1.0092849640468531e-15,NaN,NaN,9.05435058001334e-16,4.4591870827896237e-16 +10000000,cable:2:core,cable:1:core,-9.2664181398809313e-08,-1.3479772233800525e-07,-9.2664181398809313e-08,-1.3479772233800525e-07,-2.9631405210173127e-07,-1.8869668849462157e-07,2.1066173926479602e-07,2.1066173926479602e-07,0.59967121560503911,0.59967121560503911,2.0364987070292194e-07,5.389896615661632e-08 +10000000,cable:2:core,cable:2:core,80.846531351828517,55.108195401013525,80.846531351828517,55.108195401013525,80.822369854298401,55.218371261575591,0.11279405220794937,0.11279405220794937,0.00115232089289832,0.00115232089289832,0.024161497530116094,0.11017586056206596 +10000000,cable:2:core,cable:3:core,-9.26641813988093e-08,-1.3479772233800525e-07,-9.26641813988093e-08,-1.3479772233800525e-07,-3.537852634471945e-07,-2.3598687919811782e-07,2.8004189857265998e-07,2.8004189857265998e-07,0.65850514179037156,0.65850514179037156,2.6112108204838523e-07,1.0118915686011257e-07 +10000000,cable:3:core,cable:1:core,-1.8002386369702576e-16,1.9542236956564906e-16,-1.8002386369701972e-16,1.9542236956570285e-16,-1.0854589216983612e-15,6.4134107784461368e-16,1.0092849640468369e-15,1.0092849640468553e-15,NaN,NaN,9.0543505800133538e-16,4.4591870827896464e-16 +10000000,cable:3:core,cable:2:core,-9.2664181398809313e-08,-1.3479772233800525e-07,-9.2664181398809313e-08,-1.3479772233800525e-07,-3.5378526344719471e-07,-2.359868791981179e-07,2.800418985726602e-07,2.800418985726602e-07,0.65850514179037167,0.65850514179037167,2.6112108204838539e-07,1.0118915686011265e-07 +10000000,cable:3:core,cable:3:core,80.846531351828517,55.108195401013525,80.846531351828517,55.108195401013525,80.820947397233525,55.219522376005109,0.11422886716367056,0.11422886716367056,0.0011669854375752117,0.0011669854375752117,0.02558395459499252,0.11132697499158439 diff --git a/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/all_earth-Z.csv b/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/all_earth-Z.csv new file mode 100644 index 000000000..7e79e1e97 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/all_earth-Z.csv @@ -0,0 +1,82 @@ +frequency_hz,receiver,source,new_real,new_imaginary,old_real,old_imaginary,fem_real,fem_imaginary,before_absolute_error,after_absolute_error,before_relative_error,after_relative_error,real_absolute_error,imaginary_absolute_error +0.10000000000000001,cable:1:core,cable:1:core,3.1374615215203498e-06,1.2435841298035039e-06,3.1374615215203507e-06,1.2435841298035035e-06,3.1569384527251656e-06,1.2374941953892434e-06,2.0406816271213964e-08,2.0406816271214897e-08,0.0060182561639335946,0.0060182561639338696,1.9476931204815793e-08,6.0899344142604639e-09 +0.10000000000000001,cable:1:core,cable:2:core,9.9023642057029986e-08,8.1529149884539513e-07,9.9023642057030489e-08,8.152914988453947e-07,9.8890086440048917e-08,8.1209797662934007e-07,3.1963136966295769e-09,3.1963136966299794e-09,0.0039070116150096406,0.0039070116150101333,1.3355561698106873e-10,3.1935222160550581e-09 +0.10000000000000001,cable:1:core,cable:3:core,9.9020797266300371e-08,7.2818834855192947e-07,9.9020797253944635e-08,7.2818834853831638e-07,9.8883785305156379e-08,7.2534028253697365e-07,2.8513597146125118e-09,2.8513597288035877e-09,0.0038950365518541905,0.0038950365712395944,1.3701196114399179e-10,2.8480660149558238e-09 +0.10000000000000001,cable:2:core,cable:1:core,9.9023642057029973e-08,8.1529149884539492e-07,9.9023642057030476e-08,8.1529149884539449e-07,9.8890086440048931e-08,8.1209797662934018e-07,3.1963136966292585e-09,3.1963136966296609e-09,0.0039070116150092512,0.0039070116150097429,1.3355561698104226e-10,3.1935222160547405e-09 +0.10000000000000001,cable:2:core,cable:2:core,3.1374615138914709e-06,1.2435841307826221e-06,3.1374615138914718e-06,1.2435841307826217e-06,3.1569402035335558e-06,1.2378000139614139e-06,2.0319334575079277e-08,2.031933457508021e-08,0.0059922564469127642,0.0059922564469130392,1.947868964208493e-08,5.7841168212081746e-09 +0.10000000000000001,cable:2:core,cable:3:core,9.9023642057029973e-08,8.1529149884539492e-07,9.9023642057030476e-08,8.1529149884539449e-07,9.8890034709988745e-08,8.1210411491669455e-07,3.1901829464966795e-09,3.190182946497082e-09,0.0038994886832207183,0.0038994886832212101,1.3360734704122737e-10,3.1873839287003704e-09 +0.10000000000000001,cable:3:core,cable:1:core,9.9020797266300371e-08,7.2818834855192947e-07,9.9020797253944648e-08,7.2818834853831638e-07,9.8883785305156392e-08,7.2534028253697365e-07,2.8513597146125118e-09,2.8513597288035873e-09,0.0038950365518541905,0.003895036571239594,1.3701196114397855e-10,2.8480660149558238e-09 +0.10000000000000001,cable:3:core,cable:2:core,9.9023642057029986e-08,8.1529149884539502e-07,9.9023642057030489e-08,8.152914988453946e-07,9.8890034709988785e-08,8.1210411491669486e-07,3.190182946496467e-09,3.190182946496869e-09,0.0038994886832204568,0.0038994886832209481,1.336073470412009e-10,3.1873839287001586e-09 +0.10000000000000001,cable:3:core,cable:3:core,3.1374615215203498e-06,1.2435841298035039e-06,3.1374615215203507e-06,1.2435841298035035e-06,3.1569384435367512e-06,1.2375260948397891e-06,2.0397310578959553e-08,2.0397310578960486e-08,0.006015432163948049,0.0060154321639483239,1.9476922016401434e-08,6.0580349637147579e-09 +1,cable:1:core,cable:1:core,4.0462600939189788e-06,1.0981371245585023e-05,4.0462600939189881e-06,1.0981371245585021e-05,4.0645128397490841e-06,1.0929405558502649e-05,5.507808424665062e-08,5.5078084246655305e-08,0.0047233900558140574,0.004723390055814459,1.8252745830105311e-08,5.1965687082374199e-08 +1,cable:1:core,cable:2:core,9.9703279749934732e-07,6.6990063703023516e-06,9.9703279749934944e-07,6.6990063703023541e-06,9.957273254260641e-07,6.6758614958986898e-06,2.3181662332445536e-08,2.3181662332442879e-08,0.0034344673526018263,0.0034344673526014325,1.3054720732832151e-09,2.3144874403661789e-08 +1,cable:1:core,cable:3:core,9.9679142273796299e-07,5.8280015973796128e-06,9.9679142261440609e-07,5.8280015972434844e-06,9.9514743470101773e-07,5.8084827030194123e-06,1.9588004696494286e-08,1.9588004842512277e-08,0.0033238802967687844,0.0033238803215465164,1.6439880369452685e-09,1.951889436020046e-08 +1,cable:2:core,cable:1:core,9.9703279749934753e-07,6.6990063703023532e-06,9.9703279749934965e-07,6.6990063703023558e-06,9.957273254260641e-07,6.6758614958986898e-06,2.318166233244724e-08,2.3181662332444583e-08,0.0034344673526020787,0.0034344673526016849,1.3054720732834268e-09,2.3144874403663483e-08 +1,cable:2:core,cable:2:core,4.0462594747967495e-06,1.0981371344302105e-05,4.0462594747967589e-06,1.09813713443021e-05,4.0646859849575193e-06,1.0931903537134415e-05,5.2788258378960634e-08,5.2788258378968647e-08,0.0045260868022904861,0.0045260868022911731,1.8426510160769745e-08,4.9467807167690413e-08 +1,cable:2:core,cable:3:core,9.9703279749934753e-07,6.6990063703023524e-06,9.9703279749934965e-07,6.6990063703023549e-06,9.9572451158906602e-07,6.6758931134513074e-06,2.3150253870865903e-08,2.3150253870863247e-08,0.0034297983685706692,0.0034297983685702758,1.3082859102815086e-09,2.3113256851044977e-08 +1,cable:3:core,cable:1:core,9.9679142273796299e-07,5.8280015973796128e-06,9.9679142261440587e-07,5.8280015972434844e-06,9.9514743470101688e-07,5.8084827030194114e-06,1.9588004696495186e-08,1.9588004842513194e-08,0.0033238802967689375,0.0033238803215466721,1.6439880369461155e-09,1.9518894360201307e-08 +1,cable:3:core,cable:2:core,9.9703279749934732e-07,6.6990063703023516e-06,9.9703279749934944e-07,6.6990063703023541e-06,9.957245115890656e-07,6.6758931134513066e-06,2.3150253870865913e-08,2.3150253870863256e-08,0.0034297983685706713,0.0034297983685702776,1.3082859102817204e-09,2.3113256851044977e-08 +1,cable:3:core,cable:3:core,4.0462600939189788e-06,1.0981371245585023e-05,4.0462600939189881e-06,1.0981371245585021e-05,4.0645144914664652e-06,1.0929057973331517e-05,5.5406691686930907e-08,5.5406691686935579e-08,0.0047517033168123137,0.0047517033168127144,1.8254397547486479e-08,5.2313272253506142e-08 +10,cable:1:core,cable:1:core,1.4059558517812229e-05,9.4699427403525111e-05,1.405955851781232e-05,9.4699427403525043e-05,1.4078098949110193e-05,9.4275608916675479e-05,4.2422382934974958e-07,4.2422382934982131e-07,0.0044504784527611236,0.0044504784527618765,1.8540431297964731e-08,4.2381848684963207e-07 +10,cable:1:core,cable:2:core,1.0164608279866819e-05,5.2299588182049285e-05,1.0164608279866875e-05,5.2299588182049292e-05,1.0154098438757886e-05,5.2151781639659582e-05,1.4817972443400351e-07,1.4817972443399279e-07,0.0027889450380819262,0.0027889450380817245,1.0509841108933266e-08,1.4780654238970281e-07 +10,cable:1:core,cable:3:core,1.0144854518138271e-05,4.3592310471018251e-05,1.0144854518138249e-05,4.3592310471018413e-05,1.0121825613513052e-05,4.3494797801831069e-05,1.0019506524912759e-07,1.0019506524897438e-07,0.0022436580289046677,0.0022436580289012372,2.302890462521901e-08,9.7512669187182024e-08 +10,cable:2:core,cable:1:core,1.0164608279866821e-05,5.2299588182049278e-05,1.0164608279866877e-05,5.2299588182049285e-05,1.0154098438757886e-05,5.2151781639659596e-05,1.4817972443398336e-07,1.4817972443397262e-07,0.0027889450380815463,0.0027889450380813442,1.050984110893496e-08,1.4780654238968248e-07 +10,cable:2:core,cable:2:core,1.4059511160988105e-05,9.4699437546059127e-05,1.40595111609882e-05,9.4699437546059073e-05,1.4084679812657182e-05,9.4295470492056221e-05,4.0475034496165286e-07,4.047503449617129e-07,0.0042452664397349873,0.004245266439735617,2.5168651669077343e-08,4.0396705400290531e-07 +10,cable:2:core,cable:3:core,1.0164608279866821e-05,5.2299588182049299e-05,1.0164608279866877e-05,5.2299588182049305e-05,1.015405496530911e-05,5.2150531700236723e-05,1.4942960623149575e-07,1.4942960623148503e-07,0.0028125349093116991,0.0028125349093114974,1.0553314557710424e-08,1.4905648181257563e-07 +10,cable:3:core,cable:1:core,1.0144854518138269e-05,4.3592310471018237e-05,1.0144854518138247e-05,4.3592310471018407e-05,1.0121825613513055e-05,4.3494797801831048e-05,1.001950652491396e-07,1.0019506524897981e-07,0.0022436580289049378,0.0022436580289013595,2.3028904625213928e-08,9.75126691871888e-08 +10,cable:3:core,cable:2:core,1.0164608279866819e-05,5.2299588182049285e-05,1.0164608279866875e-05,5.2299588182049292e-05,1.0154054965309122e-05,5.2150531700236689e-05,1.4942960623151505e-07,1.4942960623150436e-07,0.0028125349093120642,0.002812534909311863,1.0553314557696871e-08,1.4905648181259596e-07 +10,cable:3:core,cable:3:core,1.4059558517812229e-05,9.4699427403525124e-05,1.405955851781232e-05,9.4699427403525057e-05,1.4078345460130376e-05,9.4274952375718503e-05,4.2489057230425445e-07,4.2489057230432618e-07,0.004457501831262185,0.0044575018312629378,1.8786942318147265e-08,4.2447502780662192e-07 +100,cable:1:core,cable:1:core,0.00011749986341587683,0.0007780998311891264,0.00011749986341587662,0.00077809983118912608,0.00011825586406508788,0.00077377332628715296,4.3920589304340287e-06,4.3920589304343116e-06,0.0056110070277203489,0.0056110070277207106,7.5600064921104945e-07,4.3265049019734466e-06 +100,cable:1:core,cable:2:core,0.00010621027609229592,0.00037161720315773155,0.00010621027609229646,0.00037161720315773177,0.0001060875527036882,0.00037087417345515203,7.5309638760734481e-07,7.5309638760704253e-07,0.0019522968660933113,0.0019522968660925276,1.2272338860771928e-07,7.4302970257952372e-07 +100,cable:1:core,cable:3:core,0.00010469597101762082,0.0002848479209964935,0.00010469597101762054,0.00028484792099649496,0.00010453541028029063,0.00028471069054957095,2.1121540174638963e-07,2.1121540174564472e-07,0.00069640262305146095,0.0006964026230490049,1.6056073733018358e-07,1.3723044692254662e-07 +100,cable:2:core,cable:1:core,0.00010621027609229592,0.00037161720315773155,0.00010621027609229646,0.00037161720315773177,0.0001060875527036882,0.00037087417345515198,7.5309638760739828e-07,7.53096387607096e-07,0.0019522968660934501,0.0019522968660926664,1.2272338860771928e-07,7.4302970257957793e-07 +100,cable:2:core,cable:2:core,0.00011749664935473164,0.00077810092618215538,0.0001174966493547314,0.00077810092618215505,0.0001182868821400726,0.00077387508461446977,4.2990934870278096e-06,4.2990934870280866e-06,0.0054915018851088944,0.0054915018851092482,7.9023278534096834e-07,4.2258415676856096e-06 +100,cable:2:core,cable:3:core,0.0001062102760922959,0.00037161720315773144,0.00010621027609229645,0.00037161720315773166,0.00010609004988632196,0.00037088100213371892,7.4595327491769492e-07,7.4595327491739359e-07,0.0019337429926763682,0.0019337429926755871,1.202262059739475e-07,7.3620102401252777e-07 +100,cable:3:core,cable:1:core,0.00010469597101762083,0.00028484792099649361,0.00010469597101762056,0.00028484792099649502,0.00010453541028029058,0.00028471069054957084,2.1121540174654681e-07,2.112154017458371e-07,0.00069640262305197941,0.00069640262304963949,1.6056073733025135e-07,1.3723044692276346e-07 +100,cable:3:core,cable:2:core,0.00010621027609229592,0.00037161720315773155,0.00010621027609229646,0.00037161720315773177,0.00010609004988632197,0.00037088100213371886,7.4595327491785543e-07,7.459532749175541e-07,0.0019337429926767848,0.0019337429926760035,1.202262059739475e-07,7.362010240126904e-07 +100,cable:3:core,cable:3:core,0.00011749986341587681,0.00077809983118912629,0.00011749986341587659,0.00077809983118912597,0.00011825887855140381,0.00077380158948475069,4.3647434890483931e-06,4.3647434890486752e-06,0.0055759083438218417,0.0055759083438222017,7.5901513552699786e-07,4.2982417043756086e-06 +1000,cable:1:core,cable:1:core,0.0011874959966138062,0.0060823308476815156,0.0011874959966138036,0.0060823308476815122,0.0011844846896089539,0.0060323090445500768,5.0112361333283211e-05,5.0112361333286829e-05,0.0081516651063368428,0.0081516651063374308,3.011307004852342e-06,5.0021803131438877e-05 +1000,cable:1:core,cable:2:core,0.0011019671033492395,0.0020963802241269879,0.0011019671033492449,0.0020963802241269901,0.0011007904697264187,0.0020948151416138971,1.9580474854186773e-06,1.9580474854136861e-06,0.00082742681037456295,0.00082742681037245385,1.1766336228207462e-06,1.565082513090791e-06 +1000,cable:1:core,cable:3:core,0.001003106505504941,0.0012640618611339275,0.0010031065055049382,0.0012640618611339432,0.001002585911545266,0.0012671742958162359,3.1556723090945284e-06,3.1556723091103921e-06,0.0019529719880883797,0.0019529719880981974,5.2059395967501108e-07,3.1124346823083703e-06 +1000,cable:2:core,cable:1:core,0.0011019671033492392,0.0020963802241269875,0.0011019671033492447,0.0020963802241269897,0.0011007904697264185,0.0020948151416138963,1.9580474854190237e-06,1.958047485414033e-06,0.00082742681037470964,0.00082742681037260065,1.1766336228207462e-06,1.5650825130912246e-06 +1000,cable:2:core,cable:2:core,0.00118734914471262,0.0060824491669874475,0.0011873491447126174,0.006082449166987444,0.001184352323381433,0.0060323445176638027,5.0194191117460129e-05,5.0194191117463747e-05,0.0081649638133294983,0.0081649638133300863,2.9968213311869912e-06,5.01046493236448e-05 +1000,cable:2:core,cable:3:core,0.0011019671033492395,0.0020963802241269884,0.0011019671033492449,0.0020963802241269905,0.0011007744570689971,0.0020950182326893848,1.8103662132010835e-06,1.810366213195881e-06,0.00076496431933728896,0.00076496431933509074,1.1926462802423331e-06,1.3619914376035262e-06 +1000,cable:3:core,cable:1:core,0.0010031065055049406,0.0012640618611339273,0.001003106505504938,0.0012640618611339429,0.0010025859115452653,0.0012671742958162346,3.1556723090935306e-06,3.1556723091093583e-06,0.0019529719880877641,0.0019529719880975595,5.2059395967522792e-07,3.1124346823072861e-06 +1000,cable:3:core,cable:2:core,0.0011019671033492395,0.0020963802241269879,0.0011019671033492449,0.0020963802241269901,0.0011007744570689967,0.0020950182326893844,1.8103662132013694e-06,1.8103662131961667e-06,0.00076496431933740995,0.00076496431933521152,1.1926462802427668e-06,1.3619914376035262e-06 +1000,cable:3:core,cable:3:core,0.0011874959966138065,0.0060823308476815156,0.0011874959966138039,0.0060823308476815122,0.0011843717432249161,0.0060323374132095807,5.0090961754964124e-05,5.0090961754967749e-05,0.008148176031697282,0.0081481760316978718,3.1242533888903475e-06,4.9993434471934904e-05 +10000,cable:1:core,cable:1:core,0.011218478577540311,0.043047522284281221,0.011218478577540311,0.043047522284281221,0.011119864601649793,0.042732429667698781,0.00033016370676636285,0.00033016370676636285,0.0074772879693647334,0.0074772879693647334,9.8613975890518568e-05,0.00031509261658244081 +10000,cable:1:core,cable:2:core,0.0080360922846632078,0.0048677586412040601,0.0080360922846632096,0.004867758641204061,0.0080263098434048309,0.0048746152014419982,1.1946069448571345e-05,1.1946069448570422e-05,0.0012721294666787422,0.001272129466678644,9.782441258376906e-06,6.856560237938035e-06 +10000,cable:1:core,cable:3:core,0.0043864669830369976,-0.00021545006964656183,0.0043864669830384383,-0.00021545006964465192,0.0043999105501135146,-0.00019680874044557522,2.2983225404856133e-05,2.2983225407247927e-05,0.0052183487158196486,0.0052183487163627055,1.3443567076516956e-05,1.8641329200986607e-05 +10000,cable:2:core,cable:1:core,0.0080360922846632113,0.0048677586412040593,0.0080360922846632113,0.0048677586412040601,0.0080263098434048327,0.0048746152014419956,1.1946069448570349e-05,1.1946069448570847e-05,0.0012721294666786362,0.0012721294666786893,9.7824412583786408e-06,6.8565602379363003e-06 +10000,cable:2:core,cable:2:core,0.011219720413915016,0.043052062902262878,0.011219720413915016,0.043052062902262878,0.011113161895694636,0.042732163106039826,0.00033718036335005135,0.00033718036335005135,0.0076365321513983796,0.0076365321513983796,0.00010655851822037946,0.00031989979622305126 +10000,cable:2:core,cable:3:core,0.0080360922846632078,0.0048677586412040593,0.0080360922846632078,0.004867758641204061,0.0080264334418784031,0.0048743926405697656,1.1717644452949676e-05,1.1717644452950657e-05,0.0012478059452165494,0.0012478059452166539,9.6588427848046804e-06,6.6339993657063209e-06 +10000,cable:3:core,cable:1:core,0.0043864669830369976,-0.00021545006964656183,0.0043864669830384383,-0.00021545006964465192,0.0043999105501135137,-0.00019680874044557579,2.2983225404855164e-05,2.2983225407246958e-05,0.0052183487158194292,0.0052183487163624869,1.3443567076516089e-05,1.8641329200986038e-05 +10000,cable:3:core,cable:2:core,0.0080360922846632078,0.0048677586412040601,0.0080360922846632096,0.004867758641204061,0.0080264334418784066,0.0048743926405697647,1.1717644452947755e-05,1.1717644452946814e-05,0.0012478059452163447,0.0012478059452162445,9.6588427848012109e-06,6.6339993657045862e-06 +10000,cable:3:core,cable:3:core,0.011218478577540311,0.043047522284281221,0.011218478577540311,0.043047522284281221,0.011119821235292367,0.042731468842562757,0.0003310937166445695,0.0003310937166445695,0.0074985098650658943,0.0074985098650658943,9.8657342247943786e-05,0.00031605344171846406 +100000,cable:1:core,cable:1:core,0.091870290228580009,0.28525847944837568,0.091870290228580009,0.28525847944837568,0.091493003714531451,0.28368767551605356,0.0016154782906252691,0.0016154782906252691,0.005419674749492156,0.005419674749492156,0.00037728651404855817,0.0015708039323221179 +100000,cable:1:core,cable:2:core,0.0087273397177971886,-0.0087082547107099739,0.0087273397177971886,-0.0087082547107099739,0.0087334969981067243,-0.0087216205518427393,1.4715903302091928e-05,1.4715903302091928e-05,0.0011922821576872793,0.0011922821576872793,6.1572803095356898e-06,1.3365841132765427e-05 +100000,cable:1:core,cable:3:core,-0.0014312810262812371,-0.00036817498716284558,-0.0014312810262812373,-0.00036817498716284683,-0.0014407703250733342,-0.00039045784136311349,2.4219256447629282e-05,2.4219256447630512e-05,0.016224684861121757,0.016224684861122583,9.4892987920970791e-06,2.2282854200267907e-05 +100000,cable:2:core,cable:1:core,0.0087273397177971886,-0.0087082547107099739,0.0087273397177971886,-0.0087082547107099739,0.0087334969981067174,-0.0087216205518427445,1.4715903302093751e-05,1.4715903302093751e-05,0.0011922821576874272,0.0011922821576874272,6.1572803095287509e-06,1.3365841132770631e-05 +100000,cable:2:core,cable:2:core,0.091870046795798257,0.28524984365980688,0.091870046795798257,0.28524984365980688,0.091493956461796602,0.28371579711037065,0.0015794754683649504,0.0015794754683649504,0.0052984100213463075,0.0052984100213463075,0.00037609033400165526,0.0015340465494362299 +100000,cable:2:core,cable:3:core,0.0087273397177971886,-0.0087082547107099756,0.0087273397177971886,-0.0087082547107099756,0.0087339349659137991,-0.0087212590278095137,1.45811371622721e-05,1.45811371622721e-05,0.0011813581962924681,0.0011813581962924681,6.5952481166104554e-06,1.3004317099538087e-05 +100000,cable:3:core,cable:1:core,-0.0014312810262812369,-0.00036817498716284564,-0.0014312810262812373,-0.00036817498716284683,-0.001440770325073334,-0.00039045784136311344,2.4219256447629147e-05,2.4219256447630414e-05,0.01622468486112167,0.016224684861122517,9.4892987920970791e-06,2.2282854200267798e-05 +100000,cable:3:core,cable:2:core,0.0087273397177971869,-0.0087082547107099739,0.0087273397177971869,-0.0087082547107099739,0.0087339349659138008,-0.0087212590278095102,1.4581137162272122e-05,1.4581137162272122e-05,0.0011813581962924699,0.0011813581962924699,6.5952481166139248e-06,1.3004317099536353e-05 +100000,cable:3:core,cable:3:core,0.091870290228580009,0.28525847944837579,0.091870290228580009,0.28525847944837579,0.091488767351543793,0.28367378798203707,0.0016299713952056781,0.0016299713952056781,0.0054685631684245721,0.0054685631684245721,0.00038152287703621668,0.0015846914663387168 +1000000,cable:1:core,cable:1:core,0.7540744079244075,1.574047945066307,0.7540744079244075,1.574047945066307,0.75373506062126217,1.5682514990945176,0.0058063708541588772,0.0058063708541588772,0.0033370337035997892,0.0033370337035997892,0.00033934730314533201,0.0057964459717894101 +1000000,cable:1:core,cable:2:core,0.00015357089839005441,0.0010629809539592517,0.00015357089839005441,0.0010629809539592517,0.00015808363781219276,0.0010647782065002921,4.8574616610321926e-06,4.8574616610321926e-06,0.0045124853754643004,0.0045124853754643004,4.512739422138346e-06,1.7972525410403766e-06 +1000000,cable:1:core,cable:3:core,1.8110903613484064e-07,1.4306474614933584e-06,1.8110903613483902e-07,1.4306474614933575e-06,7.1837044682661596e-08,1.4988795531927263e-06,1.2882541074491399e-07,1.2882541074491492e-07,0.085849265245670214,0.085849265245670825,1.0927199145217904e-07,6.8232091699367878e-08 +1000000,cable:2:core,cable:1:core,0.00015357089839005439,0.0010629809539592513,0.00015357089839005439,0.0010629809539592513,0.00015808363781219241,0.0010647782065002923,4.8574616610321307e-06,4.8574616610321307e-06,0.0045124853754642423,0.0045124853754642423,4.5127394221380207e-06,1.7972525410410271e-06 +1000000,cable:2:core,cable:2:core,0.75407435015734658,1.5740479687634792,0.75407435015734658,1.5740479687634792,0.75371118835143447,1.5684996969879792,0.0055601444398579404,0.0055601444398579404,0.003195130814093403,0.003195130814093403,0.00036316180591211555,0.0055482717754999911 +1000000,cable:2:core,cable:3:core,0.00015357089839005436,0.0010629809539592515,0.00015357089839005436,0.0010629809539592515,0.00015805992066927291,0.0010648271671355046,4.8538463217835019e-06,4.8538463217835019e-06,0.0045089385260994042,0.0045089385260994042,4.4890222792185537e-06,1.8462131762531347e-06 +1000000,cable:3:core,cable:1:core,1.8110903613484059e-07,1.4306474614933584e-06,1.8110903613483902e-07,1.4306474614933575e-06,7.1837044682661649e-08,1.4988795531927248e-06,1.2882541074491317e-07,1.2882541074491404e-07,0.085849265245669756,0.085849265245670339,1.0927199145217894e-07,6.8232091699366396e-08 +1000000,cable:3:core,cable:2:core,0.00015357089839005441,0.0010629809539592517,0.00015357089839005441,0.0010629809539592517,0.00015805992066927281,0.0010648271671355048,4.8538463217833511e-06,4.8538463217833511e-06,0.0045089385260992628,0.0045089385260992628,4.4890222792183911e-06,1.8462131762531347e-06 +1000000,cable:3:core,cable:3:core,0.7540744079244075,1.5740479450663067,0.7540744079244075,1.5740479450663067,0.75374922767666375,1.5683328404748265,0.0057243482323389152,0.0057243482323389152,0.0032897435000615268,0.0032897435000615268,0.00032518024774375043,0.00571510459148028 +10000000,cable:1:core,cable:1:core,4.5479347057971937,6.6714050104071516,4.5479347057971937,6.6714050104071516,4.5499283379608109,6.6607143326137441,0.010874978661416526,0.010874978661416526,0.0013481808196322126,0.0013481808196322126,0.0019936321636171428,0.010690677793407488 +10000000,cable:1:core,cable:2:core,3.0492342492444656e-09,1.3142318645708507e-08,3.0492342492444656e-09,1.3142318645708507e-08,1.70928388337544e-08,2.3364452433972144e-08,1.7369940958772128e-08,1.7369940958772128e-08,0.60001217996457723,0.60001217996457723,1.4043604584509934e-08,1.0222133788263637e-08 +10000000,cable:1:core,cable:3:core,1.6548264118885075e-17,1.2699296335778548e-17,1.6548264118882764e-17,1.2699296335776218e-17,1.7780742088352628e-16,8.2830433131016448e-17,1.758490602438255e-16,1.7584906024382244e-16,NaN,NaN,1.612591567646412e-16,7.0131136795237902e-17 +10000000,cable:2:core,cable:1:core,3.0492342492444656e-09,1.3142318645708507e-08,3.0492342492444656e-09,1.3142318645708507e-08,1.70928388337544e-08,2.3364452433972167e-08,1.7369940958772141e-08,1.7369940958772141e-08,0.60001217996457734,0.60001217996457734,1.4043604584509934e-08,1.022213378826366e-08 +10000000,cable:2:core,cable:2:core,4.5479347057971937,6.6714050104071516,4.5479347057971937,6.6714050104071516,4.5500657434906797,6.6605837453015528,0.011029102417545492,0.011029102417545492,0.0013672928193374241,0.0013672928193374241,0.0021310376934859931,0.010821265105598776 +10000000,cable:2:core,cable:3:core,3.0492342492444656e-09,1.3142318645708507e-08,3.0492342492444656e-09,1.3142318645708507e-08,2.0088895639091691e-08,2.8716067494419929e-08,2.3084447437255037e-08,2.3084447437255037e-08,0.65870223072825995,0.65870223072825995,1.7039661389847225e-08,1.5573748848711423e-08 +10000000,cable:3:core,cable:1:core,1.6548264118885075e-17,1.2699296335778546e-17,1.6548264118882761e-17,1.2699296335776215e-17,1.7780742088352623e-16,8.2830433131016571e-17,1.758490602438255e-16,1.7584906024382247e-16,NaN,NaN,1.6125915676464116e-16,7.0131136795238025e-17 +10000000,cable:3:core,cable:2:core,3.0492342492444656e-09,1.3142318645708507e-08,3.0492342492444656e-09,1.3142318645708507e-08,2.0088895639091684e-08,2.8716067494419946e-08,2.3084447437255044e-08,2.3084447437255044e-08,0.65870223072825984,0.65870223072825984,1.7039661389847219e-08,1.5573748848711439e-08 +10000000,cable:3:core,cable:3:core,4.5479347057971937,6.6714050104071516,4.5479347057971937,6.6714050104071516,4.5502090089252469,6.6605380139855157,0.01110243513586638,0.01110243513586638,0.0013763766299583725,0.0013763766299583725,0.0022743031280532122,0.010866996421635911 diff --git a/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/comparison.toml b/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/comparison.toml new file mode 100644 index 000000000..8b1758292 --- /dev/null +++ b/test/fixtures/reference/three_bare_wires/validation-20260919T170632-eFIkJx/comparison.toml @@ -0,0 +1,133 @@ +baseline = "capture-20260917T102438-Nt9nic" +criterion = "No material degradation; retain complete entry/component differences; no FEM rerun" +julia = "1.12.7" + +[runs."air_1/Y"] +after_max_absolute_error = 0.12586656843393867 +after_worst_entry = "1000000,cable:2:core,cable:2:core" +baseline_csv_sha256 = "c1781a943be20f3d99bdf8a9558ce4e70134f3cd19460da22eca39f526aed6c4" +before_max_absolute_error = 0.1258665684345085 +before_worst_entry = "1000000,cable:2:core,cable:2:core" +fem_csv_sha256 = "d2dc753fa5d79c0345fca421b35415d4af68526791a7d58fef044729399276d1" +first_seconds = 1.511878298 +fresh_warmed_bytes = 203232 +fresh_warmed_seconds = 0.023470093 +max_entry_error_increase = 6.653578463732021e-8 +max_result_change = 2.8526875350369104e-7 + +[runs."air_1/Z"] +after_max_absolute_error = 1.8723151853984725 +after_worst_entry = "10000000,cable:1:core,cable:1:core" +baseline_csv_sha256 = "58f904dc16552619aaffb4bfb00a663eb0f1e76feca62e142c7b1899e6b1fa07" +before_max_absolute_error = 1.8723151853984727 +before_worst_entry = "10000000,cable:1:core,cable:1:core" +fem_csv_sha256 = "5fba0d0a60a9196c2ef04b2b2a1cab12f56a0b7a573f229c4d8771698c7c4ff0" +first_seconds = 1.511878298 +fresh_warmed_bytes = 203232 +fresh_warmed_seconds = 0.023470093 +max_entry_error_increase = 1.4617754467046833e-16 +max_result_change = 2.4097390362987555e-15 + +[runs."air_2/Y"] +after_max_absolute_error = 0.12511124689727263 +after_worst_entry = "1000000,cable:1:core,cable:1:core" +baseline_csv_sha256 = "d942680200138e2bfaf84b60ffce59ca762ad80a65941e8b74d0bb9b6e22e219" +before_max_absolute_error = 0.12511124689781913 +before_worst_entry = "1000000,cable:1:core,cable:1:core" +fem_csv_sha256 = "51ff0beb013a2fe9624150538d4b1f14b17520b00fb1823b8b60c3cdab70e4c3" +first_seconds = 0.189928301 +fresh_warmed_bytes = 203232 +fresh_warmed_seconds = 0.023966455 +max_entry_error_increase = 8.268831008748942e-8 +max_result_change = 2.84600655827909e-7 + +[runs."air_2/Z"] +after_max_absolute_error = 1.707148753384576 +after_worst_entry = "10000000,cable:2:core,cable:2:core" +baseline_csv_sha256 = "47e67a8dc68a2766876a1e1937377c3949ae592904d224542044679412cd3dc6" +before_max_absolute_error = 1.707148753384576 +before_worst_entry = "10000000,cable:2:core,cable:2:core" +fem_csv_sha256 = "f2a7eeceef9db641c9218ba816d2c1108f143b37f2a48ec786f39e3d54a36c15" +first_seconds = 0.189928301 +fresh_warmed_bytes = 203232 +fresh_warmed_seconds = 0.023966455 +max_entry_error_increase = 2.2047132592880117e-15 +max_result_change = 2.3928886547049893e-15 + +[runs."air_3/Y"] +after_max_absolute_error = 0.12560262627986038 +after_worst_entry = "1000000,cable:1:core,cable:1:core" +baseline_csv_sha256 = "d755dd9ecf39ac2edb7a9bd74e040b9421272992ef1a2625c611d932c3d90a06" +before_max_absolute_error = 0.1256026262798634 +before_worst_entry = "1000000,cable:1:core,cable:1:core" +fem_csv_sha256 = "6df558e978eb303643fb47203c04e4507cf131309c2ea71c633d9fe721c1ca49" +first_seconds = 0.023293408 +fresh_warmed_bytes = 203232 +fresh_warmed_seconds = 0.023275569 +max_entry_error_increase = 6.653660214534632e-8 +max_result_change = 2.852687535036917e-7 + +[runs."air_3/Z"] +after_max_absolute_error = 2.130821524209557 +after_worst_entry = "10000000,cable:3:core,cable:3:core" +baseline_csv_sha256 = "a10546be75ece17056a779c6ebd6f154d19b5b4f5404d74121243c5b2e8e5122" +before_max_absolute_error = 2.130821524209557 +before_worst_entry = "10000000,cable:3:core,cable:3:core" +fem_csv_sha256 = "a70bba3ce78afdf163876ec11ca3398222b028d45eb2bf46e4c36e012b984276" +first_seconds = 0.023293408 +fresh_warmed_bytes = 203232 +fresh_warmed_seconds = 0.023275569 +max_entry_error_increase = 1.460781208695619e-16 +max_result_change = 2.4102558251379085e-15 + +[runs."all_air/Y"] +after_max_absolute_error = 1.2609414645891618e-5 +after_worst_entry = "10000000,cable:1:core,cable:3:core" +baseline_csv_sha256 = "9edfe22b78aac77a31505d80e51d1690f5bee7651155533d395f231788970e7d" +before_max_absolute_error = 1.2609467382028214e-5 +before_worst_entry = "10000000,cable:1:core,cable:3:core" +fem_csv_sha256 = "2e0d25927c3c7515dffe960d854bd83a2127a03ffe9d6631757f5b52fdea2f7e" +first_seconds = 22.046257668 +fresh_warmed_bytes = 164992 +fresh_warmed_seconds = 0.027994797 +max_entry_error_increase = 6.930431843153376e-10 +max_result_change = 8.520020319279558e-10 + +[runs."all_air/Z"] +after_max_absolute_error = 1.948507230227846 +after_worst_entry = "10000000,cable:2:core,cable:1:core" +baseline_csv_sha256 = "509fce8b1d62060461705c315d79345f58c20b4703ab9dacf4d9983dc43ed52a" +before_max_absolute_error = 1.948507230227846 +before_worst_entry = "10000000,cable:2:core,cable:1:core" +fem_csv_sha256 = "5a3bee66ff67323ba4cd90b27c0fb5ca1d7e94fcea73545144ba7c591be691e9" +first_seconds = 22.046257668 +fresh_warmed_bytes = 164992 +fresh_warmed_seconds = 0.027994797 +max_entry_error_increase = 2.393918396847994e-16 +max_result_change = 1.0837836292118093e-13 + +[runs."all_earth/Y"] +after_max_absolute_error = 0.12638102356686592 +after_worst_entry = "1000000,cable:1:core,cable:1:core" +baseline_csv_sha256 = "0682607582edc6218ce090a8b55e671e94fbcacd84081c4d2094ef3e880639e8" +before_max_absolute_error = 0.12638102356686592 +before_worst_entry = "1000000,cable:1:core,cable:1:core" +fem_csv_sha256 = "6178fb7bb6c30653f628a82279d9637b280f82723f830d8e85297c90cb57fad8" +first_seconds = 2.154431475 +fresh_warmed_bytes = 164992 +fresh_warmed_seconds = 0.021157815 +max_entry_error_increase = 2.917694280624161e-10 +max_result_change = 6.884368601498597e-10 + +[runs."all_earth/Z"] +after_max_absolute_error = 0.01110243513586638 +after_worst_entry = "10000000,cable:3:core,cable:3:core" +baseline_csv_sha256 = "2191de0b3d3cce5bbae75eec5dee2042c9f65683200cf947c22a31e42bef0b17" +before_max_absolute_error = 0.01110243513586638 +before_worst_entry = "10000000,cable:3:core,cable:3:core" +fem_csv_sha256 = "0e7a1341b5dfb48bec0350a353822b8a919460b1bf84d9ce12010b8ab70687a4" +first_seconds = 2.154431475 +fresh_warmed_bytes = 164992 +fresh_warmed_seconds = 0.021157815 +max_entry_error_increase = 2.39179403821628e-15 +max_result_change = 2.392344588403254e-15 diff --git a/test/integration/cable_constants.jl b/test/integration/cable_constants.jl new file mode 100644 index 000000000..d8fe247fc --- /dev/null +++ b/test/integration/cable_constants.jl @@ -0,0 +1,493 @@ +@testitem "Engine / cable constants / earth-free assemblies and physical invariants" tags=[:integration, :measurements, :slow] setup=[ + UseEngineSupport, + TestFixtures, + TestNumerics +] begin + using Measurements + design=TestFixtures.coaxial_design() + constants=CableConstants(design) + row=only(constants) + + @test row.core === :core + @test row.R > 0 + @test row.L > 0 + @test row.C > 0 + @test row.G >= 0 + @test constants.frequency == 50.0 + @test basis(constants) === :pul + @test resistance(constants) === constants.R + @test inductance(constants) === constants.L + @test capacitance(constants) === constants.C + @test conductance(constants) === constants.G + + packed_material=Material(:conductor, 1.7241e-8, 1.0, 1.0, 20.0, 0.00393) + packed_dielectric=Material(:insulator, 1e14, 2.3) + packed_design=build( + CableDesign, + "hexagonally-packed-core", + terminal( + :packed_core, + stranded( + packed_material; + shape = Disk(0.5e-3), + lay = (LayRatio(15), LayRatio(11)), + boundary = Disk(2.5e-3) + ) + ), + insulation(packed_dielectric; t = 1e-3) + ) + packed_constants=CableConstants(packed_design; frequency = 50.0) + @test all(isfinite, + Iterators.flatten( + (packed_constants.R, packed_constants.L, + packed_constants.C, packed_constants.G) + )) + + shared_boundary=Disk(2.5e-3) + natural_design=build( + CableDesign, + "natural-six-course-core", + terminal( + :core, + stranded( + packed_material; + shape = Disk(0.5e-3), + boundary = shared_boundary + ) + ), + insulation(packed_dielectric; t = 1e-3) + ) + compacted_design=build( + CableDesign, + "compacted-six-course-core", + terminal( + :core, + stranded( + packed_material; + shape = Disk(0.5e-3), + compact = true, + boundary = shared_boundary + ) + ), + insulation(packed_dielectric; t = 1e-3) + ) + natural_component=only(LineCableModels.DataModel.flatten(natural_design, 50.0)) + compacted_component=only(LineCableModels.DataModel.flatten(compacted_design, 50.0)) + @test natural_component.conductor.cross_section == + compacted_component.conductor.cross_section + @test natural_component.conductor.resistance == + compacted_component.conductor.resistance + @test natural_component.conductor.r_ex == compacted_component.conductor.r_ex + @test natural_component.conductor.gmr != compacted_component.conductor.gmr + natural_constants=CableConstants(natural_design; frequency = 50.0) + compacted_constants=CableConstants(compacted_design; frequency = 50.0) + @test natural_constants.C == compacted_constants.C + @test natural_constants.G == compacted_constants.G + + sector_design=build( + CableDesign, + "sector-stranded-core", + terminal( + :sector_core, + stranded( + packed_material; + shape = Disk(0.5e-3), + boundary = Sector( + span = deg2rad(119), + r_base = 1.10e-3, + r_back = 10.24e-3, + fillet = 1.02e-3 + ) + ) + ), + insulation(packed_dielectric; t = 1e-3) + ) + sector_wires=filter( + region -> region.terminal === :sector_core, + sector_design.geometry.regions + ) + @test !isempty(sector_wires) + @test all(region -> region.primitive isa LineCableModels.DataModel.Polygon, + sector_wires) + @test length(sector_design.geometry.regions) == length(sector_wires) + 2 + @test count( + region -> region.source.material.kind === :insulator, + sector_design.geometry.regions + ) == 2 + sector_constants=CableConstants(sector_design; frequency = 50.0) + @test all(isfinite, + Iterators.flatten( + (sector_constants.R, sector_constants.L, + sector_constants.C, sector_constants.G) + )) + + problem=CableConstantsProblem(design) + formulation=CableConstantsFormulation() + @test problem.design === design + blueprint=@inferred LineCableModels.Engine.flatten( + LineCableModelsCoaxial(), design + ) + @test blueprint isa LineCableModels.Engine.CableBlueprint{Float64} + @test blueprint.assembly_ranges == [1:length(design.terminal_order)] + @test getproperty.(blueprint.conductors, :terminal) == design.terminal_order + @test isempty(blueprint.shunt) + @test blueprint.shunt_details.solves == 0 + @test compute(problem, formulation) == constants + @test @inferred(compute(problem, formulation)) == constants + @test_throws ArgumentError compute(problem, formulation; options = (trace = true,)) + @test_throws MethodError CableConstants(design; position = at(x = 0, y = -2)) + @test_throws MethodError CableConstants( + design; earth_props = homogeneous(rho = 200) + ) + + homogenized=LineCableModels.homogenize(design) + source_components=LineCableModels.DataModel.flatten(design, 50.0) + reduced_components=LineCableModels.DataModel.flatten( + homogenized, + 50.0 + ) + @test getproperty.(reduced_components, :name) == + getproperty.(source_components, :name) + for (source, reduced) in zip(source_components, reduced_components) + @test reduced.conductor.resistance ≈ source.conductor.resistance + @test reduced.conductor.gmr ≈ source.conductor.gmr + @test reduced.dielectric.shunt_capacitance ≈ + source.dielectric.shunt_capacitance + @test reduced.dielectric.shunt_conductance ≈ + source.dielectric.shunt_conductance + @test reduced.dielectric.material.mu_r ≈ source.dielectric.material.mu_r + end + flattening_formulation=CableConstantsFormulation( + insulation_admittance = formula(:lossy), + semicon_admittance = formula(:lossy) + ) + source_at_flattening_frequency=CableConstants( + design; + formulation = flattening_formulation, + frequency = 50.0 + ) + homogeneous_constants=CableConstants( + homogenized; + formulation = flattening_formulation, + frequency = 50.0 + ) + @test homogeneous_constants.R ≈ source_at_flattening_frequency.R + @test homogeneous_constants.L ≈ source_at_flattening_frequency.L + @test homogeneous_constants.C ≈ source_at_flattening_frequency.C + @test homogeneous_constants.G ≈ source_at_flattening_frequency.G + + sixty_hertz=CableConstants(design; frequency = 60.0) + @test all(isfinite, + Iterators.flatten( + (sixty_hertz.R, sixty_hertz.L, sixty_hertz.C, sixty_hertz.G) + )) + @test sixty_hertz.frequency == 60.0 + @test_throws DomainError CableConstantsProblem(design; frequency = 1e6) + + hot=CableConstants(design; temperature = 80.0) + uncorrected=CableConstants( + design; + temperature = 80.0, + formulation = CableConstantsFormulation( + temperature_dependence=nothing + ) + ) + @test hot.R[1] > constants.R[1] + @test uncorrected.R ≈ constants.R + + lossless_dielectric=CableConstants( + design; + frequency = 50.0, + formulation = CableConstantsFormulation( + insulation_admittance = formula(:default) + ) + ) + lossy_dielectric=CableConstants(design; frequency = 50.0, + formulation = CableConstantsFormulation( + insulation_admittance = formula(:lossy))) + @test lossless_dielectric.G[1] < lossy_dielectric.G[1] + @test lossless_dielectric.C[1] ≈ lossy_dielectric.C[1] + + shell_return_design=build( + CableDesign, + "contextual-shell-return", + Group(:shell_core, Region(:shell_core_metal, Disk(0.01), packed_material)), + Region(:shell_insulation, Shell(0.003), packed_dielectric), + Group(:shell_return, + Region(:shell_return_metal, Shell(0.001), packed_material)), + Region(:shell_jacket, Shell(0.002), packed_dielectric) + ) + shell_return_constants=CableConstants(shell_return_design; frequency = 50.0) + @test all(isfinite, + Iterators.flatten( + (shell_return_constants.R, shell_return_constants.L, + shell_return_constants.C, shell_return_constants.G) + )) + + traced_system=build( + LineCableSystem, + design, + Pose2(0.0, -1.0); + connections = Dict(:core=>1, :sheath=>0) + ) + traced_problem=LineParametersProblem( + traced_system; + earth_props = homogeneous(rho = 100.0), + frequencies = [50.0, 60.0] + ) + traced=compute( + traced_problem, + Formulation(options = (ideal_transposition = false,)); + options = (trace = true,) + ) + primitive=details(traced).data.trace + for (frequency_index, + (frequency, expected)) in enumerate(( + (50.0, constants), + (60.0, sixty_hertz) + )) + local_Z=kron_reduce( + Matrix(primitive.Zin[:, :, frequency_index]), + [1, 0] + )[1, 1] + local_Y=im*2π*frequency*inv( + Matrix(primitive.Pin[:, :, frequency_index]) + ) + @test only(expected).R ≈ real(local_Z) + @test only(expected).L ≈ imag(local_Z)/(2π*frequency) + @test only(expected).G ≈ real(local_Y[1, 1]) + @test only(expected).C ≈ imag(local_Y[1, 1])/(2π*frequency) + end + + copper=Material(:conductor, 1.7241e-8, 1.0, 1.0, 20.0, 0.00393) + dielectric=Material(:insulator, 1e14, 2.3) + bare_design=build( + CableDesign, + "bare-core", + Group(:bare_core, Region(:bare_metal, Disk(0.01), copper)) + ) + @test_throws ArgumentError CableConstants(bare_design) + + function unsheathed_member(core_name) + Stack( + Group(core_name, + Region(Symbol(core_name, :_metal), Disk(0.01), copper)), + Region(Symbol(core_name, :_insulation), + Annulus(0.01, 0.02), dielectric) + ) + end + unsheathed_design=build( + CableDesign, + "unsheathed-core", + unsheathed_member(:unsheathed_core) + ) + unsheathed_problem=CableConstantsProblem(unsheathed_design; frequency = 50.0) + @test unsheathed_problem.design === unsheathed_design + unsheathed=only(compute(unsheathed_problem, CableConstantsFormulation())) + @test unsheathed.core === :unsheathed_core + @test unsheathed.R > 0 + @test unsheathed.L > 0 + @test unsheathed.C > 0 + @test unsheathed.G >= 0 + + unsheathed_pair=build( + CableDesign, + "two-unsheathed-assemblies", + assembly( + at(unsheathed_member(:left_unsheathed_core), -0.03, 0.0), + at(unsheathed_member(:right_unsheathed_core), 0.03, 0.0) + ) + ) + unsheathed_pair_problem=CableConstantsProblem( + unsheathed_pair; + frequency = 50.0 + ) + @test unsheathed_pair_problem.design === unsheathed_pair + unsheathed_pair_constants=compute( + unsheathed_pair_problem, + CableConstantsFormulation() + ) + @test unsheathed_pair_constants.cores == + [:left_unsheathed_core, :right_unsheathed_core] + @test all(>(0), unsheathed_pair_constants.R) + @test all(>(0), unsheathed_pair_constants.L) + @test all(>(0), unsheathed_pair_constants.C) + @test all(>=(0), unsheathed_pair_constants.G) + + eccentric_member=Stack( + Group(:eccentric_core, Region(:eccentric_metal, Disk(0.01), copper)), + Region(:eccentric_insulation, Annulus(0.01, 0.02), dielectric) + ) + common_return=Group( + :common_return, + Region(:common_return_metal, Annulus(0.05, 0.051), copper) + ) + eccentric_design=build( + CableDesign, + "eccentric-return", + assembly(at(eccentric_member, -0.02, 0.0), at(common_return, 0.0, 0.0)) + ) + @test_throws ArgumentError CableConstants(eccentric_design; frequency = 50.0) + + function coax_member(core_name, sheath_name) + Stack( + Group(core_name, Region(Symbol(core_name, :_metal), Disk(0.01), copper)), + Region(Symbol(core_name, :_insulation), Annulus(0.01, 0.02), dielectric), + Group(sheath_name, + Region(Symbol(sheath_name, :_metal), Annulus(0.02, 0.021), copper)) + ) + end + left=coax_member(:left_core, :left_sheath) + right=coax_member(:right_core, :right_sheath) + multicore=build(CableDesign, "two-assembly", + assembly(at(left, -0.03, 0.0), at(right, 0.03, 0.0))) + combined=CableConstants(multicore; frequency = 50.0) + left_constants=CableConstants(build(CableDesign, "left", left); frequency = 50.0) + right_constants=CableConstants(build(CableDesign, "right", right); frequency = 50.0) + @test combined.cores == [:left_core, :right_core] + @test combined.R ≈ [only(left_constants).R, only(right_constants).R] + @test combined.L ≈ [only(left_constants).L, only(right_constants).L] + @test combined.C ≈ [only(left_constants).C, only(right_constants).C] + @test combined.G ≈ [only(left_constants).G, only(right_constants).G] + + multicore_system=build( + LineCableSystem, + multicore, + Pose2(0.0, -1.0); + connections = Dict( + :left_core=>1, + :left_sheath=>0, + :right_core=>2, + :right_sheath=>0 + ) + ) + multicore_parameters=compute( + LineParametersProblem( + multicore_system; + earth_props = homogeneous(rho = 100.0), + frequencies = [50.0] + ); + options = (trace = true,) + ) + @test size(multicore_parameters.Z) == (2, 2, 1) + @test size(multicore_parameters.Y) == (2, 2, 1) + @test all(isfinite, multicore_parameters.Z) + @test all(isfinite, multicore_parameters.Y) + @test details(multicore_parameters).data.trace.cable_map == [1, 1, 2, 2] + + function many_conductor_design() + parts=AbstractCablePart[ + Group(:terminal_1, Region(:metal_1, Disk(0.005), copper)) + ] + outer_radius=0.005 + for index in 2:6 + dielectric_outer=outer_radius+0.0005 + push!(parts, + Region( + Symbol(:dielectric_, index-1), + Annulus(outer_radius, dielectric_outer), + dielectric + )) + conductor_outer=dielectric_outer+0.0003 + push!(parts, + Group( + Symbol(:terminal_, index), + Region( + Symbol(:metal_, index), + Annulus(dielectric_outer, conductor_outer), + copper + ) + )) + outer_radius=conductor_outer + end + return build(CableDesign, "six-conductor-coax", Stack(parts)) + end + many_layer_design=many_conductor_design() + many_layer_constants=@inferred CableConstants( + many_layer_design; + frequency = 50.0 + ) + @test many_layer_constants.cores == [:terminal_1] + @test all(isfinite, + Iterators.flatten(( + many_layer_constants.R, + many_layer_constants.L, + many_layer_constants.C, + many_layer_constants.G + ))) + + designs=build( + CableDesign, + Grid(("constants-a", "constants-b")), + design.origin + ) + constant_space=CableConstants(designs) + @test constant_space isa Gridspace{CableConstants} + @test length(constant_space) == 2 + @test all(value -> value isa CableConstants, constant_space) + @test first(constant_space) == CableConstants(first(designs)) + + problem_space=CableConstantsProblem(designs; frequency = 50.0) + @test problem_space isa Gridspace{CableConstantsProblem} + selected_problem=first(LineCableModels.points(problem_space)) + @test selected_problem isa LineCableModels.Gridpoint{CableConstantsProblem} + calculated=compute( + ParametricProblem(problem_space), + Combinatorial(CableConstantsFormulation()) + ) + @test length(calculated) == 2 + @test all(value -> value isa CableConstants, calculated) + @test first(calculated) == CableConstants(first(designs); frequency = 50.0) + + sampled=compute( + ParametricProblem(CableConstantsProblem( + build(CableDesign, Grid(("sampled",)), design.origin); + frequency = 50.0 + )), + MonteCarlo( + CableConstantsFormulation(); + trials = 2, + seed = 11, + return_samples = true, + return_histograms = true + ) + ) + @test sampled isa MonteCarloResult{<:CableConstants} + @test size(only(samples(sampled)).R) == (1, 2) + @test keys(only(statistics(sampled))) == (:R, :L, :C, :G) +end + +@testitem "Fixtures / factories / mutable state is never shared" tags=[:integration, :measurements] setup=[ + TestFixtures, +] begin + using Measurements + first_design=TestFixtures.coaxial_design() + second_design=TestFixtures.coaxial_design() + @test first_design !== second_design + @test first_design.origin !== second_design.origin + @test first_design.geometry.regions !== second_design.geometry.regions + + first_system=TestFixtures.three_phase_system() + second_system=TestFixtures.three_phase_system() + @test first_system !== second_system + @test first_system.designs !== second_system.designs + @test first_system.positions !== second_system.positions + @test ncables(first_system) == ncables(second_system) == 3 + @test nphases(first_system) == nphases(second_system) == 3 + + first_parameters=TestFixtures.two_conductor_results() + second_parameters=TestFixtures.two_conductor_results() + @test first_parameters !== second_parameters + @test first_parameters.Z.values !== second_parameters.Z.values + first_parameters.Z.values[1]=complex(Inf, Inf) + @test all(isfinite, second_parameters.Z.values) + + first_monte_carlo=TestFixtures.cable_monte_carlo_result() + second_monte_carlo=TestFixtures.cable_monte_carlo_result() + @test first_monte_carlo !== second_monte_carlo + first_samples=only(samples(first_monte_carlo)) + second_samples=only(samples(second_monte_carlo)) + @test first_samples.R !== second_samples.R + first_samples.R[1, 1]=Inf + @test all(isfinite, second_samples.R) +end diff --git a/test/integration/computation_options.jl b/test/integration/computation_options.jl new file mode 100644 index 000000000..ba0945e49 --- /dev/null +++ b/test/integration/computation_options.jl @@ -0,0 +1,161 @@ +@testitem "Execution options / inner owner validation through traversal" tags=[:integration, :measurements] setup=[TestFixtures] begin + using Measurements + inner = CableConstantsFormulation() + @test isempty(computation_options(CableConstantsFormulation, ComputationOptions()).data) + problem = CableConstantsProblem(TestFixtures.coaxial_design()) + space = Gridspace{CableConstantsProblem}(identity, (Grid((problem,)),)) + point = first(LineCableModels.points(space)) + invalid = (unsupported=true,) + pending = ParametricProblem(space, ComputationOptions(invalid)) + @test pending.options.data === invalid + @test_throws ArgumentError compute(point, inner; options=invalid) + @test_throws ArgumentError compute(problem, [inner, inner]; options=invalid) + formulas = Gridspace{CableConstantsFormulation}(identity, (Grid((inner,)),)) + @test_throws ArgumentError compute(space, formulas; options=invalid) + for outer in (Combinatorial(inner), LinearError(inner), + MonteCarlo(inner; options=(trials=1, seed=1))) + @test_throws ArgumentError compute(pending, outer) + @test isempty(details(compute(ParametricProblem(space), outer)).data) + end +end + +@testitem "Execution options / callbacks and output basis survive all traversal paths" tags=[:integration, :measurements] setup=[TestFixtures] begin + using Measurements + problem = TestFixtures.line_parameters_problem() + inner = Formulation() + space = Gridspace{LineParametersProblem}(identity, (Grid((problem,)),)) + calls = Int[] + callback = function (problem, index, result) + @test basis(result) === :total + @test haskey(details(result).data, :coordinates) + @test length(details(result).data.coordinates) == size(result.Z, 1) + @test details(result).data.formulations.methods.earth_impedance.identifier === :unified + push!(calls, index) + end + options = (output_basis=:total, on_result=callback) + # The selected basis is represented by Val. A runtime Symbol determines + # that field's type. The normalized record must still retain concrete fields. + execution = LineCableModels.computation_options(LineCableModelsCoaxial, ComputationOptions(options)) + @test execution.data.on_result === callback + @test fieldtype(typeof(execution.data), :on_result) === typeof(callback) + batch = compute(problem, [inner, inner]; options) + @test calls == [1, 2] + @test all(value -> basis(value) === :total, batch) + empty!(calls) + value = compute(first(LineCableModels.points(space)), inner; options) + @test calls == [1] + @test basis(value) === :total + for (outer, count) in ((Combinatorial(inner), 1), (LinearError(inner), 1), + (MonteCarlo(inner; options=(trials=2, seed=1)), 2)) + empty!(calls) + result = compute(ParametricProblem(space, ComputationOptions(options)), outer) + @test calls == fill(1, count) + @test basis(first(result)) === :total + end +end + +@testitem "Execution options / completed tensors and callback precede success notification" tags=[:integration, :parametric] setup=[TestFixtures] begin + using Logging + E = LineCableModels.Engine + problem = TestFixtures.three_bare_wires_problem(frequencies=[1e7], line_length=floatmax(Float64)) + selected = Formulation(options=(reduce_bundle=false, kron_reduction=false, ideal_transposition=false)) + calls = Int[] + callback = (problem, index, result) -> push!(calls, index) + pul = compute(problem, selected) + @test all(isfinite, Z(pul)) && all(isfinite, Y(pul)) + failure_log = Test.TestLogger() + with_logger(failure_log) do + @test_throws DomainError compute(problem, selected; + options=(timing=true, output_basis=:total, on_result=callback, verbosity=(default=1,))) + end + @test !any(record -> occursin("completed successfully", string(record.message)), failure_log.logs) + @test isempty(calls) + ordinary = TestFixtures.three_bare_wires_problem(frequencies=[50.]) + failed_callback = function (_, index, result) + push!(calls, index) + @test haskey(details(result).data, :formulations) + @test haskey(details(result).data, :timing) + error("injected callback failure") + end + for choice in (selected, [selected, selected]) + empty!(calls) + empty!(failure_log.logs) + with_logger(failure_log) do + @test_throws ErrorException compute(ordinary, choice; + options=(timing=true, on_result=failed_callback, verbosity=(default=1,))) + end + @test !any(record -> occursin("completed successfully", string(record.message)), failure_log.logs) + @test calls == [1] + end + completed_callback = (_, index, result) -> (@info "callback completed" index) + log = Test.TestLogger() + with_logger(log) do + result = compute(ordinary, [selected, selected]; + options=(on_result=completed_callback, verbosity=(default=1,))) + @test length(result) == 2 + end + lines = filter(record -> record.message in ("callback completed", "Line parameters computation completed successfully"), log.logs) + @test getproperty.(lines, :message) == ["callback completed", "callback completed", "Line parameters computation completed successfully"] + # Exercise both completed tensor checks independently at their numerical entry point. + execution = computation_options(LineCableModelsCoaxial, ComputationOptions()) + blueprints = only(E.flatten(LineCableModelsCoaxial(), ordinary.system.designs, Float64, [selected])) + workspace = E.LineParametersWorkspace(ordinary, selected, execution, blueprints) + E._solve!(workspace, selected) + for field in (:Zout, :Yout) + buffer = getproperty(workspace.buffers, field) + saved = buffer[1] + buffer[1] = complex(Inf) + @test_throws DomainError E._finish(workspace, ordinary, selected, Val(:pul)) + buffer[1] = saved + end +end + +@testitem "UQ / tuple and shorthand controls produce identical seeded computations" tags=[:integration, :distributions] setup=[TestFixtures] begin + using Measurements + using Distributions + using Random + design = TestFixtures.coaxial_design() + inner = CableConstantsFormulation() + space = Gridspace{CableConstantsProblem}( + temperature -> CableConstantsProblem(design; temperature), + (Grid((20.0, 40.0), AbsoluteError(0.5)),)) + problem = ParametricProblem(space) + options = (trials=4, seed=71, distribution=Normal(10, 3), + return_samples=true, return_histograms=true, bins=2, retain_details=true) + formulation = @inferred MonteCarlo(inner; options) + @test formulation.options.data.distribution === options.distribution + sampled = compute(problem, formulation) + replay = compute(problem, MonteCarlo(inner; options...)) + @test samples(sampled) == samples(replay) + @test statistics(sampled) == statistics(replay) + @test sampled.point_seeds == replay.point_seeds + @test sampled.trial_counts == replay.trial_counts == [4, 4] + @test length.(sampled.details.data.trials) == [4, 4] + @test LineCableModels.sampling_distribution(sampled) === options.distribution + for quantity in (R, L, C, G), point in 1:2 + a = only(observe(sampled, histograms, quantity, point)) + b = only(observe(replay, histograms, quantity, point)) + @test a.edges == b.edges + @test a.density == b.density + values = observe(sampled, samples, quantity, point) + @test length(a.density) == (all(==(first(values)), values) ? 1 : 2) + end + + sampler = (rng, center, deviation) -> center + deviation * randn(rng) + custom = compute(problem, MonteCarlo(inner; + options=merge(options, (distribution=sampler,)))) + normal = compute(problem, MonteCarlo(inner; + options=merge(options, (distribution=:normal,)))) + @test samples(custom) == samples(normal) + @test LineCableModels.sampling_distribution(custom) === sampler + + automatic = compute(problem, MonteCarlo(inner; + options=(seed=71, confidence=0.8, cdf_tol=0.5))) + # One concentric assembly contributes four scalar R/L/C/G marginals. + expected = ceil(Int, log(2 * 4 / (1 - 0.8)) / (2 * 0.5^2)) + @test automatic.trial_counts == [expected, expected] + @test LineCableModels.confidence(automatic) == 0.8 + @test LineCableModels.cdf_tolerance(automatic) == 0.5 + @test samples(automatic) === nothing + @test histograms(automatic) === nothing +end diff --git a/test/integration/exporters.jl b/test/integration/exporters.jl new file mode 100644 index 000000000..8e47c6cf2 --- /dev/null +++ b/test/integration/exporters.jl @@ -0,0 +1,310 @@ +@testitem "ImportExport / PSCAD / system structure and physical values" tags=[:integration, :pscad] setup=[ + UseImportExportSupport, + TestFixtures +] begin + using EzXML + + function parameters(node) + Dict(parameter["name"]=>parameter["value"] + for parameter in findall("./paramlist/param", node)) + end + + # A split dielectric with distinct laws exercises the actual multilayer + # exporter. A generic coaxial recipe need not include this extra scenario. + metal=TestFixtures.conductor_material() + first_layer=Material(kind=:insulator,rho=1e8,eps_r=3.0) + second_layer=Material(kind=:insulator,rho=2e8,eps_r=5.0) + design=build(CableDesign,"export-layers",Stack( + terminal(:core,Region(:metal,Disk(.005),metal)), + Region(:inner_dielectric,Shell(.002),first_layer), + Region(:outer_dielectric,Shell(.003),second_layer), + terminal(:sheath,Region(:return,Shell(.001),metal)), + Region(:cover,Shell(.001),first_layer))) + system=build(LineCableSystem,[design,design,design], + [(-.08,-1.0),(0.,-1.08),(.08,-1.0)]; + connections=[Dict(:core=>i,:sheath=>0) for i in 1:3],system_id="export-current") + earth=EarthModel(100.0, 10.0, 1.0) + + mktempdir() do directory + requested=joinpath(directory, "system.pscx") + output=export_data(:pscad, system, earth; file_name = requested, + formulation = Formulation(insulation_admittance=:lossy, semicon_admittance=:lossy)) + @test output == joinpath(directory, "$(system.system_id)_system.pscx") + @test isfile(output) + @test filesize(output) > 200 + + blocked=joinpath(directory, "$(system.system_id)_blocked.pscx") + mkdir(blocked) + @test_throws EzXML.XMLError export_data( + :pscad, + system, + earth; + file_name = joinpath(directory, "blocked.pscx") + ) + + document=readxml(output) + project=root(document) + @test nodename(project) == "project" + @test project["name"] == system.system_id + + definitions=findall("./definitions/Definition", project) + @test length(definitions) == 3 + station=only(filter(node->node["name"]=="DS", definitions)) + main=only(filter(node->node["name"]=="Main", definitions)) + cable_definition=only(filter( + node->node["name"]=="CableSystem", definitions)) + @test station["classid"] == "StationDefn" + @test main["classid"] == "UserCmpDefn" + @test cable_definition["classid"] == "RowDefn" + + components=findall("./schematic/User", cable_definition) + @test length(components) == length(system.designs) + 2 + @test getindex.(components, "id") == + string.(100000010:(100000011 + length(system.designs))) + + frequency=only(filter( + node->node["defn"]== + "master:Line_FrePhase_Options", components)) + cables=filter(node->node["defn"]=="master:Cable_Coax", components) + ground=only(filter(node->node["defn"]=="master:Line_Ground", components)) + @test length(cables) == length(system.designs) + @test parameters(frequency) == Dict( + "FS" => "0.5", + "FE" => "1.0E6", + "Numf" => "100", + "DCCOR" => "1", + "CPASS" => "0", + "enablf" => "1", + "shntcab" => "0.0" + ) + + expected_components=[LineCableModels.DataModel.flatten(design, 50.0) + for design in system.designs] + @test any(length(component.dielectric.layers) > 1 + for components in expected_components for component in components) + outer_radii=("R2", "R4", "R6", "R8") + insulation_radii=("R3", "R5", "R7", "R9") + resistivities=("RHOC", "RHOS", "RHOA", "RHOO") + conductor_mus=("PERMC", "PERMS", "PERMA", "PERMO") + permittivities=("EPS1", "EPS2", "EPS3", "EPS4") + insulation_mus=("PERM1", "PERM2", "PERM3", "PERM4") + losses=("LT1", "LT2", "LT3", "LT4") + for (index, (node, design, position, components_for_design)) in enumerate(zip( + cables, + system.designs, + system.positions, + expected_components + )) + values=parameters(node) + @test values["CABNUM"] == string(index) + @test values["Name"] == design.cable_id + @test parse(Float64, values["X"]) ≈ position.x rtol=1e-5 + @test parse(Float64, values["Y"]) ≈ abs(position.y) rtol=1e-5 + @test values["OHC"] == "0" + @test values["LL"] == string(2length(components_for_design) - 1) + @test values["CONNAM1"] == + uppercasefirst(String(first(components_for_design).name)) + @test parse(Float64, values["R1"]) ≈ + first(components_for_design).conductor.r_in rtol=1e-5 + for (component_index, component) in enumerate(components_for_design) + @test parse(Float64, values[outer_radii[component_index]]) ≈ + component.conductor.r_ex rtol=1e-5 + @test parse(Float64, values[insulation_radii[component_index]]) ≈ + component.dielectric.r_ex rtol=1e-5 + @test parse(Float64, values[resistivities[component_index]]) ≈ + component.conductor.material.rho rtol=1e-5 + @test parse(Float64, values[conductor_mus[component_index]]) ≈ + component.conductor.material.mu_r rtol=1e-5 + @test parse(Float64, values[permittivities[component_index]]) ≈ + component.dielectric.material.eps_r rtol=1e-5 + @test parse(Float64, values[insulation_mus[component_index]]) ≈ + component.dielectric.material.mu_r rtol=1e-5 + capacitance=component.dielectric.shunt_capacitance + expected_loss=iszero(capacitance) ? 0.0 : + min( + component.dielectric.shunt_conductance/ + (2π*50.0*capacitance), + 10.0 + ) + @test parse(Float64, values[losses[component_index]])≈ + expected_loss rtol=1e-5 atol=1e-12 + end + end + + ground_values=parameters(ground) + @test ground_values["GRRES"] == "100.0" + @test ground_values["GPERM"] == "1.0" + @test ground_values["GRP"] == "10.0" + + @test only(findall("./schematic", cable_definition))["classid"] == "RowCanvas" + + station_wire=only(findall("./schematic/Wire", station)) + @test station_wire["classid"] == "Branch" + @test station_wire["name"] == "Main" + @test station_wire["defn"] == "Main" + main_reference=only(findall("./User", station_wire)) + @test main_reference["name"] == "$(system.system_id):Main" + @test main_reference["defn"] == "$(system.system_id):Main" + + main_canvas=only(findall("./schematic", main)) + @test main_canvas["classid"] == "UserCanvas" + cable_wire=only(findall("./Wire", main_canvas)) + @test cable_wire["classid"] == "Cable" + @test cable_wire["name"] == "$(system.system_id):CableSystem" + cable_reference=only(findall("./User", cable_wire)) + @test cable_reference["defn"] == "$(system.system_id):CableSystem" + instance_values=parameters(cable_reference) + @test instance_values["Name"] == system.system_id + @test parse(Float64, instance_values["Freq"]) == 50.0 + @test parse(Float64, instance_values["Length"]) == system.line_length / 1000 + + hierarchy=only(findall("./hierarchy/call", project)) + @test hierarchy["link"] == "100000001" + @test hierarchy["name"] == "$(system.system_id):DS" + main_call=only(findall("./call", hierarchy)) + @test main_call["link"] == "100000003" + cable_call=only(findall("./call", main_call)) + @test cable_call["link"] == "100000008" + @test cable_call["view"] == "true" + @test length(findall("//Wire", document)) == 2 + @test occursin("LineCableModels.jl", read(output, String)) + end +end + +@testitem "ImportExport / PSCAD / four-component limit" tags=[:integration, :pscad] setup=[ + UseImportExportSupport +] begin + using LineCableModels + import LineCableModels.DataModel + + metal=Material(:conductor, 1.7e-8, 1.0, 1.0, 20.0, 0.0039) + dielectric=Material(:insulator, 1.0e14, 2.3, 1.0, 20.0, 0.0) + parts=let radius=0.0 + values=DataModel.AbstractCablePart[] + for index in 1:5 + terminal=Symbol("component$index") + push!(values, + DataModel.Group( + terminal, + DataModel.Region( + Symbol(terminal, :_conductor), + DataModel.Annulus(radius, radius+1.0e-3), + metal + ) + )) + push!(values, + DataModel.Region( + Symbol(terminal, :_insulation), + DataModel.Annulus(radius+1.0e-3, radius+2.0e-3), + dielectric + )) + radius+=2.0e-3 + end + values + end + design=build( + CableDesign, + "five-components", + DataModel.Stack(parts) + ) + connections=Dict(name=>index for (index, name) in enumerate(design.terminal_order)) + system=build( + LineCableSystem, + design, + DataModel.Pose2(0.0, -1.0, 0.0); + connections, + system_id = "five-components", + line_length = 1_000.0 + ) + earth=EarthModel(100.0, 1.0, 1.0) + mktempdir() do directory + @test_throws ArgumentError export_data( + :pscad, + system, + earth; + file_name = joinpath(directory, "invalid.pscx") + ) + @test isempty(readdir(directory)) + end +end + +@testitem "ImportExport / PSCAD / materialized round trip" tags=[:integration, :pscad] setup=[ + UseImportExportSupport, + TestFixtures +] begin + # A split dielectric with distinct laws exercises the actual multilayer + # exporter. A generic coaxial recipe need not include this extra scenario. + metal=TestFixtures.conductor_material() + first_layer=Material(kind=:insulator,rho=1e8,eps_r=3.0) + second_layer=Material(kind=:insulator,rho=2e8,eps_r=5.0) + design=build(CableDesign,"export-layers",Stack( + terminal(:core,Region(:metal,Disk(.005),metal)), + Region(:inner_dielectric,Shell(.002),first_layer), + Region(:outer_dielectric,Shell(.003),second_layer), + terminal(:sheath,Region(:return,Shell(.001),metal)), + Region(:cover,Shell(.001),first_layer))) + system=build(LineCableSystem,[design,design,design], + [(-.08,-1.0),(0.,-1.08),(.08,-1.0)]; + connections=[Dict(:core=>i,:sheath=>0) for i in 1:3],system_id="export-current") + earth=EarthModel(100.0, 10.0, 1.0) + + mktempdir() do directory + output=export_data( + :pscad, + system, + earth; + formulation = Formulation(insulation_admittance=:lossy, semicon_admittance=:lossy), + file_name = joinpath(directory, "system.pscx") + ) + imported_earth, imported_system=import_data(:pscad, output) + + @test imported_earth isa EarthModel{Float64} + @test imported_system isa LineCableSystem{Float64} + @test imported_system.system_id == system.system_id + @test imported_system.line_length ≈ system.line_length + @test ncables(imported_system) == ncables(system) + @test nphases(imported_system) == nphases(system) + @test last(imported_earth.layers).rho ≈ last(earth.layers).rho + @test last(imported_earth.layers).eps_r ≈ last(earth.layers).eps_r + @test last(imported_earth.layers).mu_r ≈ last(earth.layers).mu_r + + for (imported_design, original_design, imported_position, + original_position, imported_connections, original_connections) in zip( + imported_system.designs, + system.designs, + imported_system.positions, + system.positions, + imported_system.connections, + system.connections + ) + @test imported_position.x ≈ original_position.x rtol=1e-5 + @test imported_position.y ≈ original_position.y rtol=1e-5 + @test imported_connections == original_connections + @test imported_design.cable_id == original_design.cable_id + @test imported_design.terminal_order == original_design.terminal_order + actual_components=LineCableModels.DataModel.flatten( + imported_design, 50.0 + ) + expected_components=LineCableModels.DataModel.flatten( + original_design, 50.0 + ) + for (actual, expected) in zip(actual_components, expected_components) + @test actual.conductor.r_in ≈ expected.conductor.r_in rtol=1e-5 + @test actual.conductor.r_ex ≈ expected.conductor.r_ex rtol=1e-5 + @test actual.dielectric.r_ex ≈ expected.dielectric.r_ex rtol=1e-5 + @test actual.conductor.material.rho ≈ expected.conductor.material.rho rtol=1e-5 + @test actual.conductor.material.mu_r ≈ expected.conductor.material.mu_r rtol=1e-5 + @test actual.dielectric.material.eps_r ≈ expected.dielectric.material.eps_r rtol=1e-5 + @test actual.dielectric.material.rho > 0 + @test actual.dielectric.material.mu_r ≈ + expected.dielectric.material.mu_r rtol=1e-5 + end + end + + @test_throws ArgumentError import_data(:pscad, output * ".xml") + @test_throws ArgumentError import_data( + :pscad, + joinpath(directory, "missing.pscx") + ) + end +end diff --git a/test/integration/feasible_geometry_uq.jl b/test/integration/feasible_geometry_uq.jl new file mode 100644 index 000000000..724ed93cf --- /dev/null +++ b/test/integration/feasible_geometry_uq.jl @@ -0,0 +1,155 @@ +@testitem "UQ / current joint radial scale and spacing / actual line sampling and aggregation" tags=[:integration,:extension,:measurements,:slow] begin + using Measurements,Random,Statistics + function problem(scale,spacing) + scale>0 || throw(DomainError(scale,"radial dimensions must be positive")) + conductor=Material(kind=:conductor,rho=2e-8,eps_r=1.0,mu_r=1.0,T0=20.0,alpha=.004) + dielectric=Material(kind=:insulator,rho=1e8,eps_r=3.0) + designs=[build(CableDesign,"joint-wire-$i",Stack( + terminal(:core,Region(:metal,Disk(.005scale),conductor)), + Region(:insulation,Shell(.005scale),dielectric))) for i in 1:2] + system=build(LineCableSystem,designs,[(0.0,-1.0),(spacing,-1.3)]; + connections=[Dict(:core=>1),Dict(:core=>2)]) + return LineParametersProblem(system;frequencies=[50.0,1000.0], + earth_props=homogeneous(rho=100.0,eps_r=10.0),temperature=20.0) + end + # Uniform inputs have half-width sqrt(3)*sigma. Even at both worst-case + # bounds the circular surfaces are separated before clearance adjustment. + @test hypot(.2-sqrt(3)*.002,.3)-2*.01*(1.1+sqrt(3)*.01)>0 + calls=Tuple{Float64,Float64}[] + log_inputs=Ref(false) + builder=(scale,spacing)->begin + log_inputs[] && !(scale isa Measurement) && !(spacing isa Measurement) && push!(calls,(scale,spacing)) + problem(scale,spacing) + end + space=Gridspace{LineParametersProblem}(builder, + (Grid((1.0,1.1),AbsoluteError(.01)),Grid(.2,AbsoluteError(.002)))) + inner=Formulation(options=(reduce_bundle=false,kron_reduction=false,ideal_transposition=false)) + parametric=ParametricProblem(space) + linear=compute(parametric,LinearError(inner)) + @test length(linear.values)==2 + N=4 + seed=2029 + method=MonteCarlo(inner;trials=N,seed,distribution=:uniform, + return_samples=true,retain_details=true) + log_inputs[]=true + sampled=compute(parametric,method) + log_inputs[]=false + @test sampled.trial_counts==[N,N] + @test all(isempty,sampled.details.data.failures) + @test length(unique(sampled.point_seeds))==2 + # The builder log contains the inputs passed to the actual scalar computation. + # Reconstruct the exact retained draws through the current sampler, then + # compare the resulting channel arrays and independently aggregate them. + recorded=copy(calls) + for (index,point) in enumerate(LineCableModels.points(space)) + rng=Xoshiro(sampled.point_seeds[index]) + expected=NamedTuple[] + realized=Tuple{Float64,Float64}[] + for trial in 1:N + arguments=LineCableModels.realize_arguments(rng,point,:uniform) + push!(realized,Tuple(arguments)) + value=compute(problem(arguments...),inner) + push!(expected,(R=R(value),L=L(value),C=C(value),G=G(value))) + end + @test all(pair->count(==(pair),recorded)==1,realized) + for channel in (:R,:L,:C,:G) + values=cat((getproperty(value,channel) for value in expected)...;dims=4) + @test getproperty(sampled.sample_values[index],channel)==values + average=dropdims(sum(values;dims=4)./N;dims=4) + observed = getproperty(LineCableModels,channel)(sampled.values[index]) + @test eltype(observed) <: Measurement + @test nominal.(observed) ≈ average rtol=1e-10 atol=0 + @test uncertainty.(observed) ≈ dropdims(std(values;dims=4);dims=4) + end + end +end + +@testitem "UQ / Monte Carlo / computed marginal ownership and scientific transport" tags=[:integration,:extension,:measurements] setup=[TestFixtures] begin + using Measurements, Statistics, JSON3 + IE=LineCableModels.ImportExport + design=TestFixtures.coaxial_design() + space=Gridspace{CableConstantsProblem}( + temperature -> CableConstantsProblem(design;temperature), + (Grid((20.0,40.0),AbsoluteError(1.0)),)) + problem=ParametricProblem(space) + inner=CableConstantsFormulation() + baseline=compute(problem,MonteCarlo(inner;trials=4,seed=2029,distribution=:uniform,return_samples=true)) + for samples_retained in (false,true), histograms_retained in (false,true), bins in (1,3) + result=compute(problem,MonteCarlo(inner;trials=4,seed=2029,distribution=:uniform, + return_samples=samples_retained,return_histograms=histograms_retained,bins)) + @test uncertain(result) === result.values + @test isempty(details(result).data) + @test (samples(result) !== nothing) == samples_retained + @test (histograms(result) !== nothing) == histograms_retained + @test statistics(result) == statistics(baseline) + for point in eachindex(result), quantity in (R,L,C,G) + output=observe(uncertain(result,point),quantity) + draws=observe(baseline,samples,quantity,point) + @test eltype(output) <: Measurement + @test nominal.(output) ≈ vec(mean(draws;dims=2)) + @test uncertainty.(output) ≈ vec(std(draws;dims=2)) + @test all(iszero,uncertainty.(output .- observe(result[point],quantity))) + end + record=IE.serialize_value(result) + @test record["version"] == 2 + restored=IE.deserialize_value(JSON3.read(JSON3.write(record),Dict{String,Any})) + @test statistics(restored) == statistics(result) + @test restored.point_seeds == result.point_seeds + @test restored.trial_counts == result.trial_counts + for point in eachindex(result), quantity in (R,L,C,G) + @test nominal.(observe(restored[point],quantity)) == nominal.(observe(result[point],quantity)) + @test uncertainty.(observe(restored[point],quantity)) ≈ uncertainty.(observe(result[point],quantity)) + end + end + # V1 is a supported full scientific record, unlike a moments-only publication. + old=IE.serialize_value(baseline) + old["version"]=1 + delete!(old,"sources") + old["points"]=[IE.serialize_value(CableConstants(value.cores, + nominal.(value.R),nominal.(value.L),nominal.(value.C),nominal.(value.G), + nominal(value.frequency),value.details)) for value in baseline] + restored=IE.deserialize_value(JSON3.read(JSON3.write(old),Dict{String,Any})) + @test statistics(restored) == statistics(baseline) + @test uncertainty.(first(restored).R) ≈ uncertainty.(first(baseline).R) + @test uncertain(restored,1) === first(restored) + rounded=deepcopy(old) + rounded["point_seeds"]=[1.0,1.0] + @test_throws r"exact integers" IE.deserialize_value(rounded) + + singleton=compute(problem,MonteCarlo(inner;trials=1,seed=2029,return_samples=true)) + for core in singleton, quantity in (R,L,C,G) + @test eltype(observe(core,quantity)) <: Measurement + @test all(iszero,uncertainty.(observe(core,quantity))) + end +end + +@testitem "UQ / linear propagation / affine means variances and covariance" tags=[:integration,:extension,:measurements] begin + using Measurements + struct AffineProblem{T} <: AbstractProblemDefinition + x::T + y::T + end + LineCableModels.validate(problem::AffineProblem)=problem + struct AffineFormulation <: AbstractFormulation end + function LineCableModels.compute(problem::AffineProblem,::AffineFormulation; + options::Union{NamedTuple,ComputationOptions}=ComputationOptions()) + options = options isa NamedTuple ? ComputationOptions(options) : options + isempty(options.data) || throw(ArgumentError("affine fixture accepts no execution controls")) + x,y=problem.x,problem.y + u,v=2x+3y,x-y + LineParameters(reshape([complex(u,v)],1,1,1),reshape([complex(v,u)],1,1,1),[50.0]) + end + space=Gridspace{AffineProblem}(AffineProblem, + (Grid((2.0,4.0),AbsoluteError(.1)),Grid(3.0,AbsoluteError(.2)))) + result=compute(ParametricProblem(space),LinearError(AffineFormulation())) + @test length(result)==2 + for (index,x) in enumerate((2.0,4.0)) + u,v=real(only(Z(result[index]))),imag(only(Z(result[index]))) + @test Measurements.value(u)==2x+9 + @test Measurements.value(v)==x-3 + @test Measurements.uncertainty(u)^2 ≈ 4*.1^2+9*.2^2 + @test Measurements.uncertainty(v)^2 ≈ .1^2+.2^2 + @test Measurements.cov(u,v) ≈ 2*.1^2-3*.2^2 + @test only(Y(result[index]))==complex(v,u) + end +end diff --git a/test/integration/formulation_grid.jl b/test/integration/formulation_grid.jl new file mode 100644 index 000000000..c85dcab21 --- /dev/null +++ b/test/integration/formulation_grid.jl @@ -0,0 +1,248 @@ +@testitem "Engine / formulation grids / exact batched computations" tags=[:integration, :parametric, :slow] setup=[ + UseEngineSupport, + TestFixtures, FormulaFixtures +] begin + function same_parameters(left, right) + same_domain=if domain(left)===ModalDomain + details(left).data.modal.identifier==details(right).data.modal.identifier&& + operators(left).Tv==operators(right).Tv&& + operators(left).Ti==operators(right).Ti&& + gamma(left)==gamma(right) + else + left.domain==right.domain + end + return same_domain&& + left.Z.values==right.Z.values&& + left.Y.values==right.Y.values&& + left.f==right.f&& + Base.structdiff(left.details.data,(gridpoint=left.details.data.gridpoint,))==Base.structdiff(right.details.data,(gridpoint=right.details.data.gridpoint,)) + end + + system=TestFixtures.three_phase_system() + earth=Grid(( + EarthModel(10.0, 10.0, 1.0), + EarthModel(100.0, 10.0, 1.0) + )) + problem_space=LineParametersProblem( + system, + earth; + frequencies = [50.0] + ) + formulation_space=Formulation( + earth_impedance = Grid(Tuple(FormulaFixtures.selection( + LineCableModels.Engine.EarthImpedance; scale) + for scale in (1.0, 2.0))), + ) + problems=collect(problem_space) + formulations=collect(formulation_space) + expected=[compute(problem, formulation) + for formulation in formulations + for problem in problems] + + run=compute(ParametricProblem(problem_space), + Combinatorial(formulation_space; options=(retain_details=true,))) + @test length(run) == length(problems) * length(formulations) + @test length(details(run).data.points) == length(run) + @test formula_id.(getproperty.( + getproperty.(run.axes.formulations, :methods), + :earth_impedance + )) == [:LayerImpedance, :LayerImpedance] + @test [f.methods.earth_impedance.parameters.scale for f in run.axes.formulations] == [1.0, 2.0] + for index in eachindex(expected) + @test same_parameters(run[index], expected[index]) + end + for formulation_index in eachindex(formulations) + for problem_index in eachindex(problems) + index=problem_index+ + (formulation_index-1)*length(problems) + @test same_parameters( + run[problem_index, formulation_index], + expected[index] + ) + end + end + + direct=compute(first(problems), formulations) + @test length(direct) == length(formulations) + completions=Tuple[] + on_result=(problem, index, result)->push!(completions, (problem, index, result)) + observed=compute(first(problems), formulations; options = (; on_result)) + @test getindex.(completions, 2) == collect(eachindex(formulations)) + @test all(item -> item[1] === first(problems), completions) + @test all(index -> same_parameters(completions[index][3], observed[index]), eachindex(observed)) + empty!(completions) + compute(first(problems), first(formulations); options = (; on_result)) + @test length(completions) == 1 + @test completions[1][2] == 1 + empty!(completions) + failure=ErrorException("checkpoint write failed") + stop_after_first=(problem, index, result)->begin + on_result(problem, index, result) + throw(failure) + end + @test_throws failure compute(first(problems), formulations; options = (on_result = stop_after_first,)) + @test length(completions) == 1 + @test same_parameters(completions[1][3], direct[1]) + for formulation_index in eachindex(formulations) + @test same_parameters( + direct[formulation_index], + compute(first(problems), formulations[formulation_index]) + ) + end + + for source in (problem_space, ParametricProblem(problem_space, ComputationOptions((; on_result)))) + empty!(completions) + automatic=source isa ParametricProblem ? compute(source, formulation_space) : + compute(source, formulation_space; options = (; on_result)) + @test all(same_parameters.(collect(automatic), expected)) + @test length(completions) == length(expected) + @test getindex.(completions, 2) == [1, 2, 1, 2] + end + automatic_scalar=compute(first(problems), formulation_space) + @test all(same_parameters.(collect(automatic_scalar), direct)) + + design=TestFixtures.coaxial_design() + constants_problem=CableConstantsProblem(design; frequency = 50.0) + constants_space=CableConstantsFormulation( + insulation_admittance = Grid((:lossy, :default)), + ) + constants_formulations=collect(constants_space) + constants_batch=compute(constants_problem, constants_formulations) + @test constants_batch == [compute(constants_problem, formulation) + for formulation in constants_formulations] + @test collect(compute(constants_problem, constants_space)) == constants_batch + + phase=first(expected) + modal_problem=ModalAnalysisProblem(phase) + modal_space=ModalAnalysisFormulation( + Grid((:default, :default)), + ) + modal_formulations=collect(modal_space) + modal_batch=compute(modal_problem, modal_formulations) + modal_scalar=[compute(modal_problem, formulation) + for formulation in modal_formulations] + @test length(modal_batch) == 2 + @test all(same_parameters.(collect(compute(modal_problem, modal_space)), modal_batch)) + @test isconcretetype(eltype(modal_batch)) + @test typeof(modal_batch[1]) === typeof(modal_batch[2]) + @test size(modal_batch[1].domain.gamma)==(size(phase.Z,1),length(phase.f)) + for index in eachindex(modal_batch) + @test same_parameters(modal_batch[index], modal_scalar[index]) + end + + stages=Symbol[] + composed=compute(first(problems),formulations; + options=(on_result=(problem,index,value)->begin + @test domain(value)===PhaseDomain + push!(stages,:phase) + end,), + modal=ModalAnalysisFormulation(:default), + modal_options=(on_result=(problem,index,value)->begin + @test domain(value)===ModalDomain + push!(stages,:modal) + end,)) + @test stages==[:phase,:phase,:modal,:modal] + @test composed isa AbstractVector{<:LineParameters} + @test length(composed)==length(formulations) + @test all(index -> Z(composed[index])≈Z(compute( + ModalAnalysisProblem(direct[index]),ModalAnalysisFormulation(:default))), + eachindex(composed)) + @test_throws ArgumentError compute(first(problems),first(formulations); + modal_options=(timing=true,)) + completed_modal_grid=Grid(Tuple(modal_formulations)) + composed_grid=compute(first(problems),first(formulations); + modal=completed_modal_grid) + @test length(composed_grid)==2 + @test all(value -> value isa LineParameters,composed_grid) + @test all(index -> Z(composed_grid[index])≈Z(compute( + ModalAnalysisProblem(direct[1]),modal_formulations[index])),1:2) + + transported=Gridspace{ModalAnalysisProblem}(run) + @test length(transported) == length(run) + @test transported.grids === (run,) + modal_run=compute( + ParametricProblem(transported), + Combinatorial(modal_space) + ) + expected_modal=[compute(ModalAnalysisProblem(parameters), formulation) + for formulation in modal_formulations + for parameters in run] + @test length(modal_run) == length(run) * length(modal_formulations) + @test isconcretetype(eltype(modal_run)) + @test all( + same_parameters(modal_run[index], expected_modal[index]) + for index in eachindex(expected_modal) + ) + for formulation_index in eachindex(modal_formulations), source_index in 1:length(run) + downstream=modal_run[source_index,formulation_index] + @test details(downstream).data.source_gridpoint==details(run[source_index]).data.gridpoint + @test frequencies(downstream)==frequencies(run[source_index]) + end + @test size.(gamma.(modal_run))==fill((size(phase.Z,1),length(phase.f)),length(modal_run)) +end + +@testitem "Engine / formulation grids / one lowering per selected design" tags=[:integration, :parametric, :slow] begin + import LineCableModels.Engine as EN + + # Specialize only designs bearing a test-owned nominal-data type. invoke + # then runs the unmodified production lowering. Global methods remain unchanged. + struct LoweringCounter + calls::Base.RefValue{Int} + end + function EN.flatten(engine::LineCableModelsCoaxial, + design::CableDesign{T, R, G, NamedTuple{(:counter,), Tuple{LoweringCounter}}}, + ::Type{S}, methods::NamedTuple, solutions::Vector, design_index::Int) where { + T <: Real, R <: AbstractCablePart, + G <: LineCableModels.DataModel.CableGeometry, S <: Real + } + design.nominal_data.counter.calls[]+=1 + return invoke(EN.flatten, + Tuple{LineCableModelsCoaxial, CableDesign, Type{S}, NamedTuple, Vector, Int}, + engine, design, S, methods, solutions, design_index) + end + + counter=LoweringCounter(Ref(0)) + copper=Material(kind = :conductor, rho = 1.7241e-8) + dielectric=Material(kind = :insulator, rho = 1e14, eps_r = 2.3) + design=build(CableDesign, "counted-lowering", + terminal(:core, core(copper; r = 5e-3), insulation(dielectric; t = 2e-3)), + terminal(:sheath, sheath(copper; t = 0.5e-3)); + nominal_data = (; counter)) + system=build(LineCableSystem, design, Pose2(0.0, -1.0); + connections = (core = 1, sheath = 0)) + problems=LineParametersProblem(system, homogeneous(rho = Grid((10.0, 100.0))); + frequencies = [1.0, 50.0, 1000.0]) + formulations=Formulation(earth_impedance = Grid(( + formula(:unified; options=(Γ=0,)), + formula(:unified; options=(Γ=1e-4im,))))) + @test counter.calls[] == 0 + + phase=compute(ParametricProblem(problems), Combinatorial(formulations)) + @test length(phase) == 4 + @test counter.calls[] == 2 + @test all(parameters -> domain(parameters) === PhaseDomain, phase) + @test all(parameters -> size(parameters.Z) == (1, 1, 3), phase) + + counter.calls[]=0 + scalar=compute(first(problems), first(formulations)) + @test counter.calls[] == 1 + @test scalar.Z.values == first(phase).Z.values + @test scalar.Y.values == first(phase).Y.values + + counter.calls[]=0 + constants_problem=CableConstantsProblem(design; frequency = 50.0) + constants_formulations=collect(CableConstantsFormulation( + insulation_admittance = Grid((:lossy, :default)))) + constants=compute(constants_problem, constants_formulations) + @test length(constants) == 2 + @test counter.calls[] == 1 + + counter.calls[]=0 + modal_problems=Gridspace{ModalAnalysisProblem}(phase) + modal_formulations=ModalAnalysisFormulation( + Grid((:default, :default))) + modal=compute(ParametricProblem(modal_problems), Combinatorial(modal_formulations)) + @test length(modal) == 8 + @test all(parameters -> domain(parameters) === ModalDomain, modal) + @test counter.calls[] == 0 +end diff --git a/test/integration/formulation_overlays.jl b/test/integration/formulation_overlays.jl new file mode 100644 index 000000000..15a612de7 --- /dev/null +++ b/test/integration/formulation_overlays.jl @@ -0,0 +1,144 @@ +@testitem "Makie addons / completed overlays preserve matrices, identities and exports" tags=[:visual, :makie, :slow] begin + using CairoMakie + using LineCableModels.Engine: retain_gridpoint, completed_formulation + using LineCableModels.Commons: gridpoint_id, observation_labels + using LineCableModels.ReportBuilder: BenchmarkTableDefinition + f=[1.,10.,100.] + z=reshape(complex.(collect(1.:12.),collect(21.:32.)),2,2,3) + y=1e-6im .* z + source_id=gridpoint_id().source_id + choices=[Formulation(earth_impedance=:saad1996,earth_admittance=:unified), + Formulation(earth_impedance=:xue2018,earth_admittance=:xue2018)] + points=[retain_gridpoint(LineParameters(k*z,k*y,f),gridpoint_id(;source_id,formulation_index=index); + fields=merge(completed_formulation(choices[index]),(inputs=(radius=.01,resistivity=100.),coordinates=["a","b"]))) + for (index,k) in enumerate((2,3))] + reference=LineParameters(z,y,f) + results=ParametricResult(nothing,points,(problems=[:unavailable],formulations=choices),ComputationDetails()) + completed=LineCableModels.Engine.compare(reference,results,[Z,Y,R,L,G,C];bands=(:all,:wide)) + timings=[(result_id=point.details.data.gridpoint,seconds=Float64(index),scope=:compute_call_wall, + context=(id=:benchmark_title_probe,)) for (index,point) in enumerate(points)] + observed=observables(results;comparisons=completed,timings,length_unit=:base,clip=false) + observed_reference=ObservedResult(reference;length_unit=:base,clip=false) + artifact=report(BenchmarkTableDefinition(bands=(:all,:wide)),observed;reference=observed_reference) + @test artifact.tables.execution.seconds==[1.,2.] + @test artifact.tables.execution.result_formulation==[1,2] + @test length.(getproperty.(observed,:errors))==[12,12] + @test all(error -> error.reference_id==observed_reference.gridpoint.id,Iterators.flatten(getproperty.(observed,:errors))) + options=(backend=:cairo,display_plot=false,controls=false,open_export=false) + pages=LineCableModels.plot(artifact,(Z,);options...) + @test length(pages)==2 + @test getproperty.(pages,:export_name)==["benchmark_title_probe — Series resistance","benchmark_title_probe — Series reactance"] + @test sum(length(page.axes) for page in pages)==8 + for (transform,page) in zip((real,imag),pages) + @test length(page.plot_state.observed)==3 + @test any(endswith(" (reference)"),values(page.plot_state.labels)) + for ((i,j),panel) in page.plot_state.panel_data + curves=filter(item -> item isa Makie.Lines,panel.axis.scene.plots) + @test length(curves)==3 + @test Makie.to_color(last(curves).color[])==Makie.to_color(:black) + for (factor,curve) in zip((2,3,1),curves) + @test first.(curve[1][])≈f + @test last.(curve[1][])≈factor.*transform.(z[i,j,:]) + end + end + end + filtered=LineCableModels.plot(artifact,(R,);formulations=2,options...) + @test first(filtered.plot_state.observed).gridpoint.id.formulation_index==2 + @test first(filtered.plot_state.observed).timings.seconds==2. + @test all(error -> error.result_id.formulation_index==2,first(filtered.plot_state.observed).errors) + for (all_axis,selected_axis) in zip(first(pages).axes,filtered.axes) + all_curves=filter(item -> item isa Makie.Lines,all_axis.scene.plots) + selected=filter(item -> item isa Makie.Lines,selected_axis.scene.plots) + @test length(selected)==2 + @test selected[1].color[]==all_curves[2].color[] + end + blocked=LineCableModels.plot(artifact,(R,);layout=(1,2),options...) + @test length(blocked)==2 + @test all(length(page.axes)==2 for page in blocked) + @test LineCableModels.plot(artifact,(R,);title="My comparison",options...).export_name=="My comparison" + signed=LineCableModels.plot(artifact;ydata=(G,),options...,controls=true) + signed.controls[:ylog].active[]=true + @test all(axis -> axis.yscale[](1e-18)>0 && axis.yscale[](-1e-18)<0,signed.axes) + @test_throws ArgumentError LineCableModels.plot(artifact,(R,);band=(100.,200.),options...) + @test_throws ArgumentError LineCableModels.plot(artifact,(R,);band=:wide,options...) + raw_page=LineCableModels.plot(results;ydata=(R,),reference,length_unit=:base,options...) + @test length(raw_page.axes)==4 + @test length(first(raw_page.plot_state.observed).quantities)==4 + # Rendering and exporting must not consult either poisoned numerical source. + Z(reference).=NaN;Z(points[1]).=NaN + rebuilt=report(BenchmarkTableDefinition(),artifact.observed;reference=artifact.reference) + @test isequal(rebuilt.tables.terms,artifact.tables.terms) + @test rebuilt.tables.execution.seconds==[1.,2.] + mktempdir() do directory + for controls in (false,true) + page=LineCableModels.plot(artifact,(R,);options...,controls) + xlims!(first(page.axes),2,70);ylims!(first(page.axes),0,25) + limits=[axis.finallimits[] for axis in page.axes] + path=export_svg(page;path=joinpath(directory,"grid-$controls.svg"),open_file=false) + @test occursin(" item isa Makie.Lines,only(page.axes).scene.plots)==3 + @test count(item -> item isa Makie.Errorbars,only(page.axes).scene.plots)==3 +end + +@testitem "Makie addons / quantity assumptions share report and plot grouping" tags=[:visual, :makie, :slow] begin + using CairoMakie + using LineCableModels.Engine: retain_gridpoint,completed_formulation + using LineCableModels.Commons: gridpoint_id,observation_groups + using LineCableModels.ReportBuilder: BenchmarkTableDefinition + source_id=gridpoint_id().source_id + choices=[Formulation(earth_impedance=z,earth_admittance=y) for (z,y) in + ((:unified,:unified),(:pollaczek1926,:pollaczek1926),(:saad1996,:unified),(:wedepohl1973,:unified),(:xue2018,:xue2018))] + f=[1.,50.,1e3,1e7] + base=LineParameters(fill(1.0+2im,1,1,4),fill(3e-6+4e-6im,1,1,4),f) + points=[retain_gridpoint(base,gridpoint_id(;source_id,formulation_index=index); + fields=merge(completed_formulation(choice),(inputs=(radius=.01,resistivity=100.),))) + for (index,choice) in enumerate(choices)] + artifact=report(BenchmarkTableDefinition(),(reference=LineParameters(Z(base),Y(base),f),result=points)) + for (request,count) in ((R,5),(X,5),(G,3),(B,3)) + @test length(observation_groups(artifact.observed;request))==count + page=LineCableModels.plot(artifact;ydata=(request,),backend=:cairo,display_plot=false,controls=false,open_export=false) + @test length(page.plot_state.displayed_indices)==count+1 + labels=collect(values(page.plot_state.labels)) + other=request in (R,X) ? "earth Y" : "earth Z" + @test all(!occursin(other,label) for label in labels) + end + for feature in artifact.tables.features + @test size(feature.relative,1)==(feature.quantity in (:Z,:R,:L,:X) ? 5 : 3) + end + @test length(artifact.tables.quantities)==5 + # Full composite descriptions remain captured. A standalone legend uses + # the compact owner summary rather than dumping all constant branches. + physical=Formulation(internal_impedance=:default, + earth_impedance=(air=:default,earth=:pollaczek1926,mixed=:default), + earth_admittance=(air=:default,earth=:default,mixed=:default)) + observed=ObservedResult(retain_gridpoint(base,gridpoint_id();fields=completed_formulation(physical))) + for (request,labels) in ((R,("internal Z=Schelkunoff","earth Z(air)=Unified", + "earth Z(earth)=Pollaczek","earth Z(mixed)=Unified")), + (B,("earth Y(air)=Unified","earth Y(earth)=Unified","earth Y(mixed)=Unified"))) + page=LineCableModels.plot(observed;ydata=(request,),backend=:cairo,display_plot=false,controls=false,open_export=false) + fields=getproperty(observed.gridpoint.formulation_fields,request===R ? :Z : :Y) + @test all(label in [field.name*"="*field.value for field in fields] for label in labels) + @test only(values(page.plot_state.labels))==description(LineParametersFormulation,physical;quantity=request,compact=true) + end +end diff --git a/test/integration/gridspace_reporting.jl b/test/integration/gridspace_reporting.jl new file mode 100644 index 000000000..88c554854 --- /dev/null +++ b/test/integration/gridspace_reporting.jl @@ -0,0 +1,50 @@ +@testitem "ReportBuilder / formulation axes, five bands and unavailable relative RMS" tags=[:integration, :report, :slow] setup=[TestFixtures, FormulaFixtures] begin + using LineCableModels.Engine: compare + using LineCableModels.ReportBuilder + using DataFrames + system=TestFixtures.three_phase_system() + problem=LineParametersProblem(system;earth_props=EarthModel(100.,10.,1.),frequencies=[0.1,1.,10.,100.,1e3,1e4,1e5,1e6,1e7]) + space=Formulation(earth_impedance=Grid((formula(:default), + FormulaFixtures.selection(LineCableModels.Engine.EarthImpedance)))) + reference=compute(problem,Formulation()) + results=compute(problem,space) + errors=compare(reference,results,Z) + @test errors.axes === results.axes + @test length(errors)==2 + for i in 1:2 + scalar=compare(reference,results[i],Z) + @test isequal(errors[i].absolute,scalar.absolute) + @test isequal(errors[i].relative,scalar.relative) + end + definition=BenchmarkTableDefinition(;bands=(:all,:dc,:harmonic,:narrow,:wide)) + @test definition.settings.bands == (:all,:dc,:harmonic,:narrow,:wide) + artifact=report(definition,(;reference,result=results)) + @test artifact.illustration === nothing + @test nrow(artifact.tables.maxima)==60 + # The 3 shunt quantities share one result. Numerical records for + # both completed computations remain available to the saved-result writer. + @test length(metadata(artifact.tables.maxima,"comparison_records"))==60 + @test nrow(artifact.tables.comparisons)==60 + @test nrow(artifact.tables.summary)==60 + @test Set(artifact.tables.terms.formulation_index)==Set((1,2)) + @test Set(zip(artifact.tables.terms.row,artifact.tables.terms.column)) == + Set((i,j) for i in 1:size(reference.Z,1),j in 1:size(reference.Z,2)) + for q in (Z,Y,R,L,G,C),band in definition.settings.bands,i in 1:2 + exact=compare(reference,results[i],q;band) + row=only(filter(row -> row.quantity==nameof(q) && row.band==band && row.formulation_index==i,eachrow(artifact.tables.comparisons))) + @test isequal(row.absolute_rms,exact.absolute) + @test isequal(row.relative_rms_percent,100 .* exact.relative) + end + @test nrow(report(definition,artifact.observed).tables.summary)==60 + damaged=deepcopy(results) + reverse!(damaged[1].details.data.coordinates) + @test_throws ArgumentError report(definition,(;reference,result=damaged)) + @test_throws ArgumentError compare(results,results,Z) + paired=compare(results,results,Z;pairing=[(1,1),(2,2)]) + @test all(error -> all(iszero,skipmissing(error.absolute)),paired) + @test paired.axes === results.axes + labels=artifact.tables.formulations.label[artifact.tables.formulations.role .== :result] + @test labels[1] != labels[2] + @test occursin("unified",lowercase(labels[1])) + @test occursin("layer",lowercase(labels[2])) +end diff --git a/test/integration/internal_shunt.jl b/test/integration/internal_shunt.jl new file mode 100644 index 000000000..15e173903 --- /dev/null +++ b/test/integration/internal_shunt.jl @@ -0,0 +1,85 @@ +@testitem "Engine / internal shunt / explicit boundary workflow and reuse" tags=[:integration, :parametric, :slow] begin + using LinearAlgebra + E = LineCableModels.Engine + include(joinpath(pkgdir(LineCableModels), "test", "support", "internal_shunt.jl")) + design = internal_shunt_test_design(count = 4) + system = build(LineCableSystem, [design, design], [(-0.02, -1.0), (0.02, -1.0)]; + connections = [Dict(:inner=>1, :middle=>0, :reference=>0), + Dict(:inner=>2, :middle=>0, :reference=>0)]) + problem = LineParametersProblem( + system; earth_props = homogeneous(rho = 100.0), frequencies = [0.1, 50.0, 1e7]) + physical = (reduce_bundle = false, kron_reduction = false, ideal_transposition = false) + boundary = formula( + :boundary; options = (resolution = ( + wire = 32, order = 16, quadrature = 128, modes = 256),)) + first_formula = Formulation(shunt_model = boundary; options = physical) + other_formula = Formulation(shunt_model = boundary, + earth_impedance = formula(:unified; options=(Γ=1e-4im,)); options = physical) + reduced_formula = Formulation(shunt_model = boundary; options = (reduce_bundle = true, + kron_reduction = true, ideal_transposition = false)) + results = compute(problem, [first_formula, other_formula, reduced_formula]; options = (trace = true,)) + first_result = first(results) + shunt = details(first_result).data.shunt_model + @test shunt.effective === :boundary + @test shunt.solves == 1 + @test length(shunt.domains) == 2 + @test details(results[2]).data.trace.Pin == details(first_result).data.trace.Pin + @test typeof(results[1]) == typeof(results[2]) + trace = details(first_result).data.trace + options = reduced_formula.options.data + plan = LineCableModels.Commons.ReductionPlan(problem.system.connection_order; + options.reduce_bundle, options.kron_reduction, options.ideal_transposition) + buffers = LineCableModels.Commons.ReductionBuffers{eltype(trace.Z)}(plan) + for k in eachindex(problem.frequencies) + reduced_Z = similar(trace.Z, length(plan.keep), length(plan.keep)) + reduced_Y = similar(reduced_Z) + LineCableModels.Commons.reduce_line_matrices!(reduced_Z, reduced_Y, trace.Z[:, :, k], + trace.P[:, :, k], 2pi*im*problem.frequencies[k], plan, buffers) + @test observe(results[3], Z)[:, :, k] ≈ reduced_Z + @test observe(results[3], Y)[:, :, k] ≈ reduced_Y rtol=1e-10 + end + @test details(results[3]).data.shunt_model.solves == 1 + constants = @inferred CableConstants(design; frequency = 50.0, + formulation = CableConstantsFormulation(shunt_model = boundary)) + local_domain, blueprint = internal_shunt_test_domain(design) + blueprint = E.flatten(LineCableModelsCoaxial(), design, + CableConstantsFormulation(shunt_model = boundary)) + @test constants.C[1] ≈ blueprint.shunt[1].C[1, 1] rtol=1e-10 + @test constants.G[1] == 0 + lossy = Formulation(insulation_admittance = :lossy; options = physical) + mixed = compute(problem, [first_formula, lossy]) + @test length(mixed) == 2 + @test typeof(mixed[1]) == typeof(mixed[2]) + @test details(mixed[2]).data.shunt_model.effective === :equivalent + @test details(mixed[2]).data.shunt_model.solves == 0 + reference_temperature = Formulation(shunt_model = boundary, + temperature_dependence = nothing; options = physical) + blueprints = E.flatten(LineCableModelsCoaxial(), system.designs, Float64, + [first_formula, other_formula, lossy, reference_temperature]) + @test blueprints[1] === blueprints[2] + @test blueprints[1] === blueprints[4] + @test blueprints[1][1].shunt[1].C === blueprints[1][2].shunt[1].C + @test all(bp -> isempty(bp.shunt), blueprints[3]) + input = E.lineinput(problem, blueprints[1]) + @test input.cable.shunt[1].terminals == 1:3 + @test input.cable.shunt[2].terminals == 4:6 + @test input.cable.shunt[2].C === blueprints[1][2].shunt[1].C + execution = computation_options(LineCableModelsCoaxial, ComputationOptions((;))) + @test observe( + E._compute( + LineCableModelsCoaxial(), problem, other_formula, execution, input, + E.completed_inputs(problem),LineCableModels.Commons.gridpoint_id()), Y) == + observe(results[2], Y) + # The same physical API supports independent translated assemblies inside + # one design. Their local operators must be scattered into distinct ports. + left = internal_shunt_test_design(count = 4, suffix = "_left") + right = internal_shunt_test_design(count = 4, suffix = "_right") + pair = build(CableDesign, "two-local-domains", + assembly(at(left.origin, -0.01, 0.0), at(right.origin, 0.01, 0.0))) + pair_blueprint = E.flatten(LineCableModelsCoaxial(), pair) + domains = E.ShuntModel.internal_shunt_domains([pair], [pair_blueprint]) + @test length(domains) == 2 + @test getproperty.(domains, :terminals) == [1:3, 4:6] + @test CableConstants(pair; formulation = CableConstantsFormulation(shunt_model = boundary)).C ≈ + fill(constants.C[1], 2) rtol=1e-8 +end diff --git a/test/integration/line_parameters.jl b/test/integration/line_parameters.jl new file mode 100644 index 000000000..59c8f9226 --- /dev/null +++ b/test/integration/line_parameters.jl @@ -0,0 +1,562 @@ +@testitem "Engine / solver / multicable reciprocity and modal transformation" tags=[:integration, :parametric] setup=[ + UseEngineSupport, + TestFixtures, + TestNumerics +] begin + using LinearAlgebra + using Logging: NullLogger, with_logger + + problem=TestFixtures.line_parameters_problem(frequencies = [50.0, 500.0]) + formulation=Formulation(; + options = ( + reduce_bundle = true, + kron_reduction = true, + ideal_transposition = false + ) + ) + phase_parameters=compute(problem, formulation; options = (trace = true,)) + trace=details(phase_parameters).data.trace + parameters=compute( + ModalAnalysisProblem(phase_parameters), + ModalAnalysisFormulation(:default) + ) + @test details(parameters).data.formulations === + details(phase_parameters).data.formulations + + @test domain(parameters) === ModalDomain + @test size(parameters.Z) == (3, 3, 2) + @test all(isfinite, parameters.Z) + @test all(isfinite, parameters.Y) + @test trace.phase_map == [1, 0, 2, 0, 3, 0] + @test trace.cable_map == repeat(1:3; inner = 2) + @test size(trace.Zin) == (6, 6, 2) + @test size(trace.Pin) == (6, 6, 2) + @test size(trace.Z) == (6, 6, 2) + @test size(trace.P) == (6, 6, 2) + @test size(trace.Zg) == (3, 3, 2) + @test size(trace.Pg) == (3, 3, 2) + for identifier in (:default,) + tracked=@inferred compute( + ModalAnalysisProblem(phase_parameters), + ModalAnalysisFormulation(identifier) + ) + rebuilt=LineCableModels.ModalAnalysis.transform(PhaseDomain, tracked) + @test TestNumerics.isapprox_scaled(Z(rebuilt), Z(phase_parameters)) + @test TestNumerics.isapprox_scaled(Y(rebuilt), Y(phase_parameters)) + for matrix in (tracked.Z.values, tracked.Y.values), frequency in 1:2 + + slice=@view matrix[:, :, frequency] + @test norm(slice-Diagonal(diag(slice))) <= + 1e-6*max(norm(slice), eps(Float64)) + end + end + for frequency_index in eachindex(problem.frequencies) + @test TestNumerics.isapprox_scaled( + trace.Zg[:, :, frequency_index], + transpose(trace.Zg[:, :, frequency_index]) + ) + # Unequal heights retain the directional path-voltage entries. + Pg=trace.Pg[:, :, frequency_index] + @test norm(Pg-transpose(Pg))>64eps(Float64)*norm(Pg) + @test any(!iszero, + trace.Zg[:, :, frequency_index] .- + Diagonal(diag(trace.Zg[:, :, frequency_index]))) + @test any(!iszero, + trace.Pg[:, :, frequency_index] .- + Diagonal(diag(trace.Pg[:, :, frequency_index]))) + end + + execution=computation_options(LineCableModelsCoaxial, ComputationOptions((;))) + blueprints=LineCableModels.Engine.CableBlueprint{eltype(problem)}[LineCableModels.Engine.flatten( + LineCableModelsCoaxial(), + design, + eltype(problem)) + for design in problem.system.designs] + workspace=LineParametersWorkspace( + problem, Formulation(), execution, blueprints) + @test workspace.input.phase_map == problem.system.connection_order + @test workspace.input.cable_map == + [entry.cable for entry in problem.system.terminal_order] + @test workspace.plan.cable_indices == + [findall(entry -> entry.cable == cable, problem.system.terminal_order) + for cable in 1:ncables(problem.system)] + @test workspace.trace === nothing + + function solve_without_logging(workspace, formulation) + return with_logger(NullLogger()) do + LineCableModels.Engine._solve!(workspace, formulation) + end + end + # The author-specific allocation check was withdrawn with its numerical + # implementation. Preserve Unified's independent ceiling, not a relabelled + # comparison with that old workload. + complete_formulation=Formulation(options = (reduce_bundle = true, + kron_reduction = true, ideal_transposition = false)) + complete_workspace=LineParametersWorkspace(problem, complete_formulation, + execution, blueprints) + @test @inferred(LineCableModels.Engine._solve!(complete_workspace, + complete_formulation)) === complete_workspace + solve_without_logging(complete_workspace, complete_formulation) + solve_without_logging(complete_workspace, complete_formulation) + @test (@allocated solve_without_logging(complete_workspace, complete_formulation))<=32_768 + shared=only(complete_workspace.plan.earth.calculations) + @test shared.impedance.pairs == shared.admittance.pairs + @test !isempty(shared.impedance.pairs) + @test length(complete_workspace.buffers.earth.calculations) == 1 + # Distinct configurations use separate calculations and material tables. + # The main numerical arrays are reused after publishing selected entries. + distinct_formulation=Formulation( + earth_admittance = formula( + :default; options = (integration = (method = :quad, options = (rtol = 1e-9,)),)), + options = ( + reduce_bundle = true, kron_reduction = true, ideal_transposition = false)) + distinct_workspace=LineParametersWorkspace(problem, distinct_formulation, execution, blueprints) + impedance, potential=distinct_workspace.plan.earth.calculations + @test !isempty(impedance.impedance.pairs) && impedance.admittance === nothing + @test potential.impedance === nothing && !isempty(potential.admittance.pairs) + @test distinct_workspace.buffers.earth.calculations[1].rho !== + distinct_workspace.buffers.earth.calculations[2].rho + @test distinct_workspace.buffers.quadrature.segments !== + complete_workspace.buffers.quadrature.segments + @test distinct_workspace.buffers.axial_field !== + complete_workspace.buffers.axial_field +end + +@testitem "Engine / Gridpoint / selected line problem reaches scalar compute" tags=[:integration, :parametric] setup=[ + UseEngineSupport, + TestFixtures +] begin + import LineCableModels.ParametricBuilder as PB + + system=TestFixtures.three_phase_system() + earth=Grid((EarthModel(10.0), EarthModel(100.0))) + problem_space=LineParametersProblem( + system, + earth; + frequencies = [50.0] + ) + @test problem_space isa Gridspace{LineParametersProblem} + @test length(problem_space) == 2 + + point=first(PB.points(problem_space)) + @test point isa LineCableModels.Gridpoint{LineParametersProblem} + result=compute(point, Formulation()) + @test result isa LineParameters + @test size(result.Z) == (3, 3, 1) + @test size(result.Y) == (3, 3, 1) +end + +@testitem "Engine / coaxial choreography / material evaluation precedes local and earth calculations" tags=[:integration, :parametric] setup=[ + UseEngineSupport, + TestFixtures, FormulaFixtures +] begin + const EN=LineCableModels.Engine + const II=EN.InsulationImpedance + const IA=EN.InsulationAdmittance + const SA=EN.SemiconAdmittance + const EZ=EN.EarthImpedance + const EY=EN.EarthAdmittance + + events=Symbol[] + M=FormulaFixtures + formulation=Formulation(insulation_impedance = M.CountedInsulationZ(events), + insulation_admittance = M.CountedInsulationY(events), + semicon_admittance = M.CountedSemiconY(events), + earth_impedance = M.CountedEarthZ(events), + earth_admittance = M.CountedEarthP(events), + options = (ideal_transposition = false,)) + result=compute( + TestFixtures.line_parameters_problem(frequencies = [50.0]), + formulation + ) + + local_z_calls=findall(==(:local_z), events) + earth_z_calls=findall(==(:earth_z), events) + local_y_calls=findall(==(:local_y), events) + earth_y_calls=findall(==(:earth_y), events) + @test all(!isempty, (local_z_calls, earth_z_calls, local_y_calls, earth_y_calls)) + @test maximum(local_y_calls) < minimum(local_z_calls) + @test maximum(local_z_calls) < minimum(earth_z_calls) + @test maximum(earth_z_calls) < minimum(earth_y_calls) + laws=formulation.methods[( + :insulation_impedance, :insulation_admittance, :semicon_admittance)] + workspace=first(laws.insulation_admittance.workspaces) + @test workspace isa EN.LineParametersWorkspace + for law in laws + @test all(w -> w === workspace, law.workspaces) + end + @test all(isfinite, result.Z) + @test all(isfinite, result.Y) + single_frequency_events=copy(events) + empty!(events) + sweep=compute( + TestFixtures.line_parameters_problem(frequencies = [1.0, 50.0, 1000.0]), + formulation + ) + @test events == repeat(single_frequency_events, 3) + @test domain(sweep) === PhaseDomain + @test sweep.Z.values[:, :, 2] == result.Z.values[:, :, 1] + @test sweep.Y.values[:, :, 2] == result.Y.values[:, :, 1] +end + +@testitem "Engine / transform / symmetric two-cable system retains two modes" tags=[:integration, :parametric] setup=[ + UseEngineSupport, + TestFixtures, + TestNumerics +] begin + using LinearAlgebra: Diagonal, diag, norm + + design=TestFixtures.coaxial_design() + connections=[Dict(terminal=>(terminal===:core ? phase : 0) + for terminal in design.terminal_order) + for phase in 1:2] + system=build( + LineCableSystem, + [design, design], + [Pose2(-0.5, -1.0), Pose2(0.5, -1.0)]; + connections, + system_id = "symmetric-pair" + ) + problem=LineParametersProblem( + system; + earth_props = homogeneous(rho = 100.0), + frequencies = [50.0, 500.0] + ) + parameters=compute( + problem, + Formulation(options = (ideal_transposition = false,)) + ) + @test size(Z(parameters)) == (2, 2, 2) + @test size(Y(parameters)) == (2, 2, 2) + for matrix in (Z(parameters), Y(parameters)), frequency in 1:2 + + slice=@view matrix[:, :, frequency] + @test slice ≈ transpose(slice) + @test slice[1, 1] ≈ slice[2, 2] + @test slice[1, 2] ≈ slice[2, 1] atol=100eps(Float64)*norm(slice) + end + + modal=compute( + ModalAnalysisProblem(parameters), + ModalAnalysisFormulation(:default); options = (offdiagonal_tolerance = 1e-10,) + ) + @test domain(modal) === ModalDomain + for matrix in (Z(modal), Y(modal)), frequency in 1:2 + + slice=@view matrix[:, :, frequency] + @test norm(slice - Diagonal(diag(slice))) <= 1e-10 * max(norm(slice), 1) + end + @test size(@observe(modal, (R, diag)[:, :])) == (2, 2) + @test size(@observe(modal, (L, diag)[:, :])) == (2, 2) + @test size(@observe(modal, (G, diag)[:, :])) == (2, 2) + @test size(@observe(modal, (C, diag)[:, :])) == (2, 2) +end + +@testitem "Engine / formulation support / physical geometry precedes backend support" tags=[:integration, :parametric] setup=[ + UseEngineSupport +] begin + conductor=Material(kind = :conductor, rho = 1.7241e-8) + design=build( + CableDesign, + "elliptical", + Group(:phase, Region(:elliptical_core, Ellipse(0.01, 0.006), conductor)) + ) + @test design.geometry.regions[1].primitive isa LineCableModels.DataModel.Ellipse + + system=build( + LineCableSystem, + design, + Pose2(0.0, -1.0); + connections = (phase = 1,) + ) + problem=LineParametersProblem( + system; + earth_props = EarthModel(100.0), + frequencies = [50.0] + ) + @test problem.system === system + execution=computation_options(LineCableModelsCoaxial, ComputationOptions((;))) + @test_throws ArgumentError LineParametersWorkspace( + problem, + Formulation(), + execution, + LineCableModels.Engine.CableBlueprint{eltype(problem)}[LineCableModels.Engine.flatten( + LineCableModelsCoaxial(), source, + eltype(problem) + ) + for source in + problem.system.designs] + ) + @test_throws ArgumentError compute(problem, Formulation()) + @test_throws ArgumentError CableConstants(design) + + neutral=build( + LineCableSystem, + design, + Pose2(0.0, 0.0); + connections = (phase = 1,), + environment = nothing + ) + @test_throws DomainError LineParametersProblem( + neutral; + earth_props = EarthModel(100.0), + frequencies = [50.0] + ) +end + +@testitem "Engine / coaxial profile / explicit radial support boundary" tags=[:integration, :parametric] setup=[ + UseEngineSupport +] begin + conductor=Material(kind = :conductor, rho = 1.7241e-8) + dielectric=Material(kind = :insulator, rho = 1.0e14, eps_r = 2.3) + earth=EarthModel(100.0) + + function problem_for(design) + system=build( + LineCableSystem, + design, + Pose2(0.0, -1.0); + connections = Dict(first(design.terminal_order)=>1) + ) + return LineParametersProblem( + system; + earth_props = earth, + frequencies = [50.0] + ) + end + + bare=build(CableDesign, "bare", Group( + :phase, Region(:bare, Disk(0.01), conductor) + )) + execution=computation_options(LineCableModelsCoaxial, ComputationOptions((;))) + workspace(problem, + formulation = Formulation())=LineParametersWorkspace( + problem, + formulation, + execution, + LineCableModels.Engine.CableBlueprint{eltype(problem)}[LineCableModels.Engine.flatten( + LineCableModelsCoaxial(), source, + eltype(problem) + ) + for source in problem.system.designs] + ) + + bare_input=workspace(problem_for(bare)).input + @test bare_input.cable.r_ins_in == bare_input.cable.r_ins_ext == [0.01] + @test bare_input.cable.dielectric_ranges == [1:0] + @test isempty(bare_input.cable.r_layer_in) + + layered=build(CableDesign, + "layered", + Stack( + Group(:phase, + Stack( + Region(:core, Disk(0.01), conductor), + Region(:outer_conductor, Annulus(0.01, 0.011), conductor) + )), + Region(:semicon, Shell(0.0005), dielectric), + Region(:insulation, Shell(0.002), dielectric), + Group( + :screen, + Region(:screen_metal, Annulus(0.0135, 0.014), conductor) + ) + )) + layered_input=workspace(problem_for(layered)).input + @test layered_input.n_phases == 2 + @test layered_input.cable.dielectric_ranges == [1:2, 3:2] + @test layered_input.cable.r_ins_in[2] == + layered_input.cable.r_ins_ext[2] == 0.014 + + filled=build(CableDesign, + "filled", + Enclosure( + :pipe, + Group(:phase, Region(:core, Disk(0.01), conductor)); + primitive = Disk(0.03), + fill = dielectric + )) + filled_input=workspace(problem_for(filled)).input + @test filled_input.cable.r_layer_in == [0.01] + @test filled_input.cable.r_layer_ext == [0.03] + @test getproperty.(filled_input.cable.dielectric_materials, :rho) == + [dielectric.rho] + + reappearing=build(CableDesign, + "reappearing", + Stack( + Group(:a, Region(:a_inner, Disk(0.01), conductor)), + Region(:a_gap, Shell(0.001), dielectric), + Group(:b, Region(:b_metal, Annulus(0.011, 0.012), conductor)), + Region(:b_gap, Shell(0.001), dielectric), + Group(:a, Region(:a_outer, Annulus(0.013, 0.014), conductor)) + )) + @test_throws ArgumentError workspace(problem_for(reappearing)) + + conductor_after_dielectric=build(CableDesign, + "conductor-after-dielectric", + Stack( + Group(:phase, Region(:inner, Disk(0.01), conductor)), + Region(:gap, Shell(0.001), dielectric), + Group(:phase, Region(:outer, Annulus(0.011, 0.012), conductor)) + )) + @test_throws ArgumentError workspace(problem_for(conductor_after_dielectric)) + + undeclared_gap=build(CableDesign, + "undeclared-gap", + Stack( + Group(:inner, Region(:inner, Disk(0.01), conductor)), + Group(:outer, Region(:outer, Annulus(0.012, 0.013), conductor)) + )) + @test_throws ArgumentError workspace(problem_for(undeclared_gap)) + + @test_throws DomainError build(CableDesign, + "overlap", + Stack( + Group(:inner, Region(:inner, Disk(0.01), conductor)), + Group(:outer, Region(:outer, Annulus(0.009, 0.012), conductor)) + )) +end + +@testitem "Engine / solver / bundle-only and singleton reduction policies" tags=[:integration, :parametric] setup=[ + UseEngineSupport, + TestFixtures +] begin + design=TestFixtures.coaxial_design() + duplicate_mapping=Dict("core"=>1, "sheath"=>1) + duplicate_system=build( + LineCableSystem, + design, + Pose2(0.0, -1.0, 0.0); + connections = duplicate_mapping, + system_id = "duplicate-bundle", + line_length = 500.0 + ) + frequencies=[50.0] + earth=EarthModel(100.0, 10.0, 1.0) + duplicate_problem=LineParametersProblem( + duplicate_system; + earth_props = earth, + frequencies + ) + bundle_only=Formulation(; + temperature_dependence = nothing, + options = ( + reduce_bundle = true, + kron_reduction = false + ) + ) + duplicate_result=@inferred compute(duplicate_problem, bundle_only) + # Passing the options as an argument keeps output_basis a runtime Symbol. + # Basis and optional details may vary, but the public result must not widen to Any. + calculate(problem, formulation, options)=compute(problem, formulation; options) + runtime_options=(output_basis = :pul, verbosity = (default = 0,)) + inferred=only(Base.return_types(calculate, + Tuple{typeof(duplicate_problem), typeof(bundle_only), typeof(runtime_options)})) + @test inferred <: LineParameters + @test Z(calculate(duplicate_problem, bundle_only, runtime_options)) == + Z(duplicate_result) + @test size(duplicate_result.Z) == (1, 1, 1) + @test all(isfinite, duplicate_result.Z) + @test all(isfinite, duplicate_result.Y) + + singleton_design=build( + CableDesign, + "single-component", + Stack( + terminal(:core, Region(:metal, Disk(0.005), TestFixtures.conductor_material())), + Region(:cover, Shell(0.005), Material(kind = :insulator, rho = 1e8, eps_r = 3.0))) + ) + singleton_system=build( + LineCableSystem, + singleton_design, + Pose2(0.0, -1.0, 0.0); + connections = Dict(:core=>1), + system_id = "singleton", + line_length = 100.0 + ) + singleton_problem=LineParametersProblem( + singleton_system; + earth_props = earth, + frequencies + ) + unreduced=Formulation(; + temperature_dependence = nothing, + options = ( + reduce_bundle = false, + kron_reduction = false + ) + ) + singleton_result=compute(singleton_problem, unreduced) + @test size(singleton_result.Z) == (1, 1, 1) + @test real(singleton_result.Z[1, 1, 1]) > 0 + @test imag(singleton_result.Y[1, 1, 1]) > 0 +end + +@testitem "Engine / ideal transposition / averaged Z and P of three-wire lines" tags=[:integration, :parametric] begin + using LinearAlgebra + include(joinpath(pkgdir(LineCableModels), "test", "support", "scenarios.jl")) + using .CurrentScenarios + # Entry (i, j) of the cyclic average is the mean of its cyclic diagonal. + average(M) = (n = size(M, 1); + [sum(M[k, mod1(k + j - i, n)] for k in 1:n) / n for i in 1:n, j in 1:n]) + frequencies = [50.0, 1e3, 1e5, 1e6] + layouts = (overhead = (heights = (10.0, 10.0, 10.0), horizontal = (0.0, 4.0, 8.0)), + buried = (heights = (-1.0, -1.0, -1.0), horizontal = (0.0, 4.0, 8.0))) + for (name, layout) in pairs(layouts) + @testset "$name" begin + problem = three_bare_wires_problem(; layout..., rho = 100.0, frequencies) + plain = compute(problem, Formulation(options = (ideal_transposition = false,))) + transposed = compute(problem, Formulation(options = (ideal_transposition = true,))) + for (k, f) in pairs(frequencies) + jω = im * 2π * f + Zu, Yu = Z(plain)[:, :, k], Y(plain)[:, :, k] + @test Z(transposed)[:, :, k] ≈ average(Zu) rtol = 1e-12 + expected = jω * inv(average(jω * inv(Yu))) + @test Y(transposed)[:, :, k] ≈ expected rtol = 1e-10 + # Averaging the inverted admittance instead gives a different matrix. + @test !isapprox(Y(transposed)[:, :, k], average(Yu); rtol = 1e-4) + end + end + end +end + +@testitem "Engine / indexed restrictions and formula-owned Γ reach public compute" tags=[:integration, :parametric, :slow] setup=[ + UseEngineSupport, TestFixtures +] begin + base=TestFixtures.line_parameters_problem(frequencies = [50.0, 500.0]) + selection=formula(:unified; options = (Γ = [1e-5im, 2e-5im],)) + prescribed=compute(base, Formulation(earth_impedance = selection, earth_admittance = selection)) + @test all(isfinite, prescribed.Z)&&all(isfinite, prescribed.Y) + @test_throws ArgumentError compute(base, Formulation(earth_impedance = :xue2018)) + ordinary=compute(base) + zero_selection=formula(:unified; options = (Γ = zeros(ComplexF64, 2),)) + zero_result=compute(base, Formulation(earth_impedance = zero_selection, earth_admittance = zero_selection)) + @test Z(zero_result)==Z(ordinary) + @test Y(zero_result)==Y(ordinary) + @test_throws DimensionMismatch compute( + base, Formulation( + earth_impedance = formula(:unified; options = (Γ = [0.0],)))) + design=TestFixtures.coaxial_design() + connections(phase)=Dict("core"=>phase, "sheath"=>0) + mixed=build(LineCableSystem, [design, design], [Pose2(0.0, 1.0), Pose2(1.0, -1.0)]; + connections = [connections(1), connections(2)]) + problem=LineParametersProblem(mixed; earth_props = EarthModel(100.0), frequencies = [50.0]) + mixed_result=compute(problem) + @test all(isfinite, mixed_result.Z)&&all(isfinite, mixed_result.Y) + @test_throws ArgumentError compute(problem, Formulation(earth_impedance = :xue2018)) +end + +@testitem "Engine / frequency-dependent earth relation reaches coaxial solve" tags=[:integration, :parametric] setup=[ + UseEngineSupport, + TestFixtures, FormulaFixtures +] begin + problem=TestFixtures.line_parameters_problem(frequencies = [1.0e6]) + static=compute(problem, Formulation()) + law=FormulaFixtures.DispersiveEarth(scale = 1e5) + dispersive=compute(problem, Formulation(earth_properties = law)) + + @test all(isfinite, dispersive.Z) + @test all(isfinite, dispersive.Y) + @test dispersive.Z.values != static.Z.values + @test dispersive.Y.values != static.Y.values +end diff --git a/test/integration/modal_visual.jl b/test/integration/modal_visual.jl new file mode 100644 index 000000000..6f7783067 --- /dev/null +++ b/test/integration/modal_visual.jl @@ -0,0 +1,264 @@ +@testitem "ModalAnalysis / retained finite quantities render with completed formulations" tags=[:visual, :makie, :slow] begin + using CairoMakie + using LineCableModels.Engine: retain_gridpoint, completed_formulation + using LineCableModels.Commons: gridpoint_id, observation_labels + + z=reshape(ComplexF64[1+im,2+im,3+im],1,1,3) + y=reshape(ComplexF64[1e-6im,2e-6im,3e-6im],1,1,3) + phase=retain_gridpoint(LineParameters(z,y,[10.0,50.0,100.0]),gridpoint_id(); + fields=merge(completed_formulation(Formulation()), + (inputs=(system=(line_length=10.0,),),coordinates=["core"]))) + modal=compute(ModalAnalysisProblem(phase),ModalAnalysisFormulation(:default)) + segment=PropagationParameters(modal) + observed=ObservedResult(segment,((H,abs),(Tv,abs));complete_pairs=true) + @test observed.gridpoint.source_gridpoint==details(segment).data.source_gridpoint + pages=LineCableModels.plot(observed; ydata=((H,abs),(Tv,abs)), + backend=:cairo,display_plot=false,controls=false,open_export=false) + pages=pages isa AbstractVector ? pages : [pages] + @test !isempty(pages) + @test all(page -> !isempty(page.axes),pages) + + alternate=compute(ModalAnalysisProblem(phase),ModalAnalysisFormulation(:default; + options=(iteration=(max_iterations=101,),))) + primary=[ObservedResult(value,(R,X)) for value in (modal,alternate)] + labels=observation_labels(primary;request=R) + @test labels[1]!=labels[2] + primary_pages=LineCableModels.plot(primary;ydata=(R,),backend=:cairo, + display_plot=false,controls=false,open_export=false) + primary_pages=primary_pages isa AbstractVector ? primary_pages : [primary_pages] + @test any(page -> !isempty(page.axes),primary_pages) +end + +@testitem "PlotBuilder / full transformation overlays retain 324 coefficients" tags=[:visual, :makie, :slow] begin + using CairoMakie + n=18 + f=10.0 .^ range(1,5;length=321) + voltage=ComplexF64[complex(i/20,j/30+k/10) for i in 1:n,j in 1:n,k in eachindex(f)] + current=ComplexF64[complex(j/25,i/35+k/10) for i in 1:n,j in 1:n,k in eachindex(f)] + roots=fill(0.01+0.02im,n,length(f)) + modal=LineParameters(ModalDomain(ModalOperators(voltage,current),roots), + SeriesImpedance(ones(ComplexF64,n,n,length(f))), + ShuntAdmittance(ones(ComplexF64,n,n,length(f))),f, + ComputationDetails((inputs=(system=(line_length=20.0,),),))) + observed=observables(PropagationParameters(modal),((Tv,abs),(Tv,angle),(Ti,abs),(Ti,angle))) + pages=LineCableModels.plot(observed;overlay=:coordinates, + backend=:cairo,display_plot=false,controls=true,open_export=false, + series_attributes=(linewidth=2.5,)) + @test length(pages)==4 + for (page, expected) in zip(pages,(abs.(voltage),rad2deg.(angle.(voltage)), + abs.(current),rad2deg.(angle.(current)))) + @test length(page.axes)==1 + curves=filter(plot->plot isa Makie.Lines,only(page.axes).scene.plots) + @test length(curves)==n*n + entries=last(only(page.legend.entrygroups[])) + @test 1100 + for i in 1:n,j in 1:n + curve=curves[(i-1)*n+j] + @test curve.label[]=="Conductor $i, Mode $j" + @test getindex.(curve[1][],1)≈f rtol=8eps(Float32) + @test getindex.(curve[1][],2)≈expected[i,j,:] rtol=8eps(Float32) + @test curve.linewidth[]==2.5 + end + @test !isempty(Makie.colorbuffer(page.figure)) + empty!(page.figure) + end +end + +@testitem "PlotBuilder / multimode vector orientation and physical matrix coefficients" tags=[:visual, :makie, :slow] begin + using CairoMakie + using LinearAlgebra + import LineCableModels.Engine as E + import LineCableModels.Commons as G + + f=[10.0,20.0,40.0] + roots=ComplexF64[1+2im 3+4im 5+6im;2+3im 4+5im 6+7im].*1e-3 + voltage=cat([ComplexF64[1 .2; .3 1] for _ in f]...;dims=3) + current=cat([ComplexF64[1 .1; .4 1] for _ in f]...;dims=3) + z=zeros(ComplexF64,2,2,3) + y=copy(z) + for k in eachindex(f), mode in 1:2 + z[mode,mode,k]=roots[mode,k] + y[mode,mode,k]=roots[mode,k] + end + modal=LineParameters(E.ModalDomain(ModalOperators(voltage,current),roots), + SeriesImpedance(z),ShuntAdmittance(y),f, + ComputationDetails((inputs=(system=(line_length=20.0,),), + phase_coordinates=["core","sheath"]))) + first_segment=PropagationParameters(modal) + second_segment=PropagationParameters(first_segment;line_length=30.0) + first_point=ObservedResult(first_segment,(gamma,Tv,velocity)) + second_point=ObservedResult(second_segment,(gamma,Tv,velocity)) + options=(backend=:cairo,display_plot=false,controls=true,open_export=false) + pages(value)=value isa AbstractVector ? value : [value] + native_lines(axis)=filter(plot -> plot isa Makie.Lines,axis.scene.plots) + + automatic=pages(LineCableModels.plot(first_point;ydata=(gamma,),options...)) + @test length(automatic)==2 + @test all(page -> length(page.axes)==1 && length(native_lines(only(page.axes)))==2,automatic) + @test all(page -> page.plot_state.panel_page.coordinates==(1,),automatic) + @test "Mode 1" in values(first(automatic).plot_state.labels) + @test !isempty(Makie.colorbuffer(first(automatic).figure)) + one_cell=pages(LineCableModels.plot(first_point;ydata=(gamma,),layout=(1,1),options...)) + @test all(page -> length(page.axes)==1 && length(native_lines(only(page.axes)))==2, + one_cell) + scalar=pages(LineCableModels.plot(first_point;ydata=(gamma,),options...)) + collection=pages(LineCableModels.plot([first_point];ydata=(gamma,),options...)) + @test [page.plot_state.panel_page.coordinates for page in scalar]== + [page.plot_state.panel_page.coordinates for page in collection] + raw_alpha=LineCableModels.plot(first_segment;ydata=(alpha,),options...) + @test length(raw_alpha.axes)==1 && length(native_lines(only(raw_alpha.axes)))==2 + singleton_alpha=LineCableModels.plot((first_segment,);ydata=(alpha,),options...) + @test singleton_alpha isa UIPlot + @test [q.values for q in only(singleton_alpha.plot_state.observed).quantities]== + [q.values for q in only(raw_alpha.plot_state.observed).quantities] + @test length(singleton_alpha.axes)==length(raw_alpha.axes) && + length(native_lines(only(singleton_alpha.axes)))==length(native_lines(only(raw_alpha.axes))) + retained_alpha=LineCableModels.plot(ObservedResult(first_segment);ydata=alpha,options...) + @test length(retained_alpha.axes)==1 && length(native_lines(only(retained_alpha.axes)))==2 + real_only=LineCableModels.plot(first_segment;ydata=((gamma,real),),options...) + @test real_only isa UIPlot && length(native_lines(only(real_only.axes)))==2 + + explicit=pages(LineCableModels.plot(first_point;ydata=(gamma,),layout=(2,1),options...)) + @test length(explicit)==2 + @test all(page -> length(page.axes)==2 && all(axis -> length(native_lines(axis))==1,page.axes),explicit) + @test all(page -> page.plot_state.panel_page.coordinates==(1,2),explicit) + multi=pages(LineCableModels.plot([first_point,second_point];ydata=(gamma,),options...)) + @test length(multi)==2 + @test all(page -> length(page.axes)==2 && all(axis -> length(native_lines(axis))==2,page.axes),multi) + duplicates=pages(LineCableModels.plot([first_point,first_point];ydata=(gamma,),options...)) + @test length(duplicates)==2 + @test all(page -> length(page.axes)==2 && + all(axis -> length(native_lines(axis))==2,page.axes),duplicates) + @test length(G.observation_groups([first_point,first_point];request=(gamma,real)))==2 + source_id=G.gridpoint_id().source_id + assumptions=(selections=(Z=((:series,:given,(;)),),Y=((:shunt,:given,(;)),)), + modal=(effective=(method=:default,),)) + equivalent=[ObservedResult(PropagationParameters(E.retain_gridpoint(modal, + G.gridpoint_id(;source_id,problem_index=1,formulation_index=index); + fields=assumptions)),(gamma,)) for index in 1:2] + @test length(G.observation_groups(equivalent;request=(gamma,real)))==1 + grouped_auto=LineCableModels.plot(equivalent;ydata=(gamma,real),options...) + @test length(grouped_auto.axes)==2 + @test all(axis -> length(native_lines(axis))==1,grouped_auto.axes) + @test grouped_auto.plot_state.displayed_indices==[1] + grouped_coordinates=LineCableModels.plot(equivalent;ydata=(gamma,real), + overlay=:coordinates,options...) + @test length(grouped_coordinates.axes)==2 + @test all(axis -> length(native_lines(axis))==2,grouped_coordinates.axes) + @test grouped_coordinates.plot_state.displayed_indices==[1,2] + small=pages(LineCableModels.plot([first_point,second_point];ydata=(gamma,), + layout=(1,1),options...)) + @test length(small)==4 + @test [only(page.plot_state.panel_page.coordinates) for page in small]==[1,2,1,2] + with_reference=pages(LineCableModels.plot(first_point;ydata=(gamma,), + reference=second_point,options...)) + @test length(with_reference)==2 + @test all(page -> length(page.axes)==2 && all(axis -> length(native_lines(axis))==2,page.axes),with_reference) + @test all(page -> any(occursin("reference",string(label)) for label in values(page.plot_state.labels)),with_reference) + + coordinate_pages=pages(LineCableModels.plot([first_point,second_point]; + ydata=(gamma,),overlay=:coordinates,layout=(1,1),options...)) + @test length(coordinate_pages)==4 + @test [only(page.plot_state.panel_page.coordinates) for page in coordinate_pages]==[1,2,1,2] + @test all(page -> length(native_lines(only(page.axes)))==2,coordinate_pages) + @test native_lines(only(coordinate_pages[1].axes))[1].color[]== + native_lines(only(coordinate_pages[2].axes))[1].color[] + second_mode=@observe (gamma,real)[2,:] + selected_mode=LineCableModels.plot(first_point;ydata=(second_mode,), + series_attributes=((color=:red,),),options...) + selected_line=only(native_lines(only(selected_mode.axes))) + @test selected_line.label[]=="Mode 2" + @test last.(selected_line[1][])≈[2,4,6] + @test selected_line.color[]==Makie.to_color(:red) + reordered_modes=@observe (gamma,real)[[2,1],:] + reordered=LineCableModels.plot(first_point;ydata=(reordered_modes,), + series_attributes=((color=:red,),(color=:blue,)),options...) + reordered_lines=native_lines(only(reordered.axes)) + @test [line.label[] for line in reordered_lines]==["Mode 2","Mode 1"] + @test [last.(line[1][]) for line in reordered_lines]≈[[2,4,6],[1,3,5]] + @test [line.color[] for line in reordered_lines]==collect(Makie.to_color.((:red,:blue))) + reordered_default=LineCableModels.plot(first_point;ydata=(reordered_modes,),options...) + @test [line.color[] for line in native_lines(only(reordered_default.axes))]== + reverse([line.color[] for line in native_lines(only(automatic[1].axes))]) + relabelled=LineCableModels.plot(first_point;ydata=(reordered_modes,), + series_labels=("Selected 2","Selected 1"),options...) + @test [line.label[] for line in native_lines(only(relabelled.axes))]== + ["Selected 2","Selected 1"] + coordinate_reference=pages(LineCableModels.plot(first_point;ydata=(gamma,), + reference=second_point,overlay=:coordinates,layout=(1,2),options...)) + @test length(coordinate_reference)==2 + @test all(page -> length(page.axes)==2 && all(axis -> length(native_lines(axis))==2,page.axes),coordinate_reference) + @test all(page -> native_lines(page.axes[1])[1].color[]== + native_lines(page.axes[2])[1].color[],coordinate_reference) + distinct=[ObservedResult(PropagationParameters(E.retain_gridpoint(modal, + G.gridpoint_id(;source_id,problem_index=index,formulation_index=1))), + (gamma,)) for index in 1:3] + stable_reference=LineCableModels.plot(distinct;ydata=(gamma,real), + overlay=:coordinates,reference=second_point,problem=1, + panel_legends=Dict(4=>:inside),options...) + @test stable_reference.plot_state.panel_page.coordinates==(1,4) + @test length(stable_reference.axes)==2 + filtered_modal=E.retain_gridpoint(modal,G.gridpoint_id(;problem_index=2)) + filtered_point=ObservedResult(PropagationParameters(filtered_modal),(gamma,)) + filtered=pages(LineCableModels.plot([first_point,filtered_point];ydata=(gamma,), + problem=2,options...)) + @test length(filtered)==2 + @test all(page -> length(page.axes)==1 && length(native_lines(only(page.axes)))==2,filtered) + gridpoint_pages=pages(LineCableModels.plot(first_point;ydata=(gamma,), + overlay=:gridpoints,layout=(1,1),options...)) + @test length(gridpoint_pages)==4 + @test all(page -> length(native_lines(only(page.axes)))==1,gridpoint_pages) + + matrix_pages=pages(LineCableModels.plot(first_point;ydata=(Tv,),options...)) + @test length(matrix_pages)==2 + @test all(page -> length(page.axes)==4,matrix_pages) + @test all(page -> Set(page.plot_state.panel_page.coordinates)== + Set(((1,1),(1,2),(2,1),(2,2))),matrix_pages) + @test all(axis -> startswith(string(axis.title[]),"Re(Tv)") && + occursin("Re(Tv)",string(axis.ylabel[])), + first(matrix_pages).axes) + @test any(page -> any(axis -> any(curve -> any(x -> !iszero(x),last.(curve[1][])),native_lines(axis)),page.axes),matrix_pages) + matrix_coordinates=pages(LineCableModels.plot([first_point,second_point]; + ydata=((Tv,real),),overlay=:coordinates,layout=(1,2),options...)) + @test length(matrix_coordinates)==1 + @test length(only(matrix_coordinates).axes)==2 + @test all(axis -> length(native_lines(axis))==4,only(matrix_coordinates).axes) + @test all(axis -> startswith(axis.title[],"Re(Tv)"),only(matrix_coordinates).axes) + @test all(label -> occursin("Conductor",string(label)) && occursin("Mode",string(label)), + values(only(matrix_coordinates).plot_state.labels)) + matrix_coefficient=@observe (Tv,real)[1,2,:] + selected_matrix=LineCableModels.plot(first_point;ydata=(matrix_coefficient,), + overlay=:coordinates,series_attributes=((color=:green,),),options...) + selected_matrix_line=only(native_lines(only(selected_matrix.axes))) + @test occursin("core",selected_matrix_line.label[]) + @test occursin("Mode 2",selected_matrix_line.label[]) + @test last.(selected_matrix_line[1][])≈fill(0.2,3) + @test selected_matrix_line.color[]==Makie.to_color(:green) + mixed=pages(LineCableModels.plot(first_point;ydata=(gamma,(Tv,real)),options...)) + @test [page.plot_state.nominal_capacity for page in mixed]==[(1,1),(1,1),(2,2)] + mixed_explicit=pages(LineCableModels.plot(first_point; + ydata=(gamma,(Tv,real)),layout=(2,2),options...)) + @test all(page -> page.plot_state.nominal_capacity==(2,2),mixed_explicit) + @test_throws ArgumentError LineCableModels.plot(first_point; + ydata=(gamma,(Tv,real)),series_labels=("Mode 1","Mode 2"),options...) + @test_throws ArgumentError LineCableModels.plot(first_point; + ydata=(gamma,(Tv,real)),series_attributes=((color=:red,),(color=:blue,)),options...) + styled=LineCableModels.plot(first_point;ydata=(gamma,real), + overlay=:coordinates,series_labels=("Mode A","Mode B"), + series_attributes=((color=:red,),(linestyle=:dash,)),options...) + @test length(native_lines(only(styled.axes)))==2 + @test native_lines(only(styled.axes))[1].color[]==Makie.to_color(:red) + @test native_lines(only(styled.axes))[2].linestyle[]==Makie.to_linestyle(:dash) + paneltitle!(styled,1,"Modes") + @test only(styled.axes).title[]=="Modes" + resetview!(styled) + resize!(styled.figure,900,500) + @test !isempty(Makie.colorbuffer(styled.figure)) + mktempdir() do directory + path=export_svg(styled;path=joinpath(directory,"modal-overlay.svg"),open_file=false) + @test filesize(path)>0 + end +end diff --git a/test/integration/monte_carlo_retries.jl b/test/integration/monte_carlo_retries.jl new file mode 100644 index 000000000..9a37fe737 --- /dev/null +++ b/test/integration/monte_carlo_retries.jl @@ -0,0 +1,160 @@ +@testitem "UQ / resampling preserves accepted trials, source identities and seeded replay" tags=[:integration, :measurements] setup=[TestFixtures] begin + using Measurements + using Statistics + design = TestFixtures.coaxial_design() + attempted_temperatures = Float64[] + space = Gridspace{CableConstantsProblem}( + temperature -> begin + push!(attempted_temperatures, temperature) + length(attempted_temperatures) <= 2 && throw(DomainError( + temperature, "injected rejection before resolving the cable")) + CableConstantsProblem(design; temperature) + end, + (Grid((20.0, 40.0), AbsoluteError((0.5,))),), + ) + inner = CableConstantsFormulation() + formulation = MonteCarlo(inner; trials=4, seed=71, distribution=:uniform, + return_samples=true, return_histograms=true, bins=2, + options=(on_error=:resample, retain_details=true, max_failures=3)) + sampled = compute(ParametricProblem(space), formulation) + @test length(sampled) == 2 + @test sampled.trial_counts == [4, 4] + @test length(attempted_temperatures) == 12 + @test attempted_temperatures[[7,12]] == [20.,40.] + @test length(unique(sampled.point_seeds)) == 2 + @test all(isconcretetype, (eltype(sampled.values), eltype(sampled.stats), + eltype(sampled.sample_values), eltype(sampled.histogram_values))) + @test length.(sampled.details.data.trials) == [4, 4] + @test length.(sampled.details.data.failures) == [2, 0] + first_summary, second_summary = sampled.details.data.failure_summary + @test first_summary.attempts == 6 + @test first_summary.accepted == 4 + @test first_summary.failed == 2 + @test first_summary.acceptance_rate == 4 / 6 + @test first_summary.by_type == [(type="DomainError", count=2)] + @test first_summary.by_stage == [(stage=:build, count=2)] + @test second_summary.attempts == second_summary.accepted == 4 + @test second_summary.failed == 0 + for (index, failure) in enumerate(first(sampled.details.data.failures)) + @test failure.attempt == index + @test failure.target_trial == 1 + @test failure.stage === :build + @test failure.sample !== nothing + @test occursin("injected rejection", failure.error.message) + @test !isempty(failure.error.stack) + @test length(failure.error.stack) <= 8 + @test all(frame -> frame.line > 0 && !isempty(frame.function_name), failure.error.stack) + end + for point in 1:2 + accepted_temperatures = attempted_temperatures[point==1 ? (3:6) : (8:11)] + expected = [compute(CableConstantsProblem(design; temperature), inner) + for temperature in accepted_temperatures] + for quantity in (R, L, C, G) + retained = observe(sampled, samples, quantity, point) + @test retained == hcat((observe(value, quantity) for value in expected)...) + @test nominal.(observe(sampled.values[point], quantity)) ≈ vec(mean(retained; dims=2)) + @test uncertainty.(observe(sampled.values[point], quantity)) ≈ vec(std(retained; dims=2)) + end + end + before_observation=length(attempted_temperatures) + observed=observables(sampled) + @test length(attempted_temperatures)==before_observation + @test observed[1].gridpoint.sampling.diagnostics.failure_summary==first_summary + @test observed[1].gridpoint.sampling.diagnostics.failures[1].sample==sampled.details.data.failures[1][1].sample + recorded = copy(attempted_temperatures) + empty!(attempted_temperatures) + replay = compute(ParametricProblem(space), formulation) + @test attempted_temperatures == recorded + @test samples(replay) == samples(sampled) + @test replay.point_seeds == sampled.point_seeds + @test replay.details.data.failure_summary == sampled.details.data.failure_summary + +end + +@testitem "UQ / resampling does not hide non-domain errors or run past its limit" tags=[:integration, :measurements] begin + using Measurements + inner = CableConstantsFormulation() + for (injected, on_error, expected_attempts) in ( + (DomainError(-1, "invalid input marker"), :fail, 1), + (ArgumentError("programming error marker"), :resample, 1), + (DomainError(-1, "unrecoverable input marker"), :resample, 3), + ) + attempts = Ref(0) + space = Gridspace{CableConstantsProblem}( + _ -> begin + attempts[] += 1 + throw(injected) + end, + (Grid((1.0,)),), + ) + formulation = MonteCarlo(inner; trials=2, seed=71, + options=(on_error=on_error, retain_details=true, max_failures=3)) + failure = try + compute(ParametricProblem(space), formulation) + catch exception + exception + end + @test attempts[] == expected_attempts + if expected_attempts == 1 + @test failure === injected + else + @test failure isa ErrorException + message = sprint(showerror, failure) + @test occursin("resampling limit of 3", message) + @test occursin("3 attempts (0 accepted)", message) + @test occursin("during build", message) + @test occursin("unrecoverable input marker", message) + end + end + @test_throws ArgumentError MonteCarlo(inner; options=(on_error=:resample,)) +end + +@testitem "UQ / negative physical radius / failure and resampling records" tags=[:integration, :measurements] begin + using Measurements + attempts=Float64[] + function physical_problem(radius) + push!(attempts,radius) + radius>0 || throw(DomainError(radius,"physical radius must be positive")) + metal=Material(kind=:conductor,rho=2e-8) + dielectric=Material(kind=:insulator,rho=1e8,eps_r=3.) + design=build(CableDesign,"positive-radius",Stack( + terminal(:core,Region(:metal,Disk(radius),metal)), + Region(:insulation,Shell(.005),dielectric))) + CableConstantsProblem(design) + end + problem=ParametricProblem(Gridspace{CableConstantsProblem}(physical_problem, + (Grid(.005,AbsoluteError(.001)),))) + inner=CableConstantsFormulation() + negative=(_rng,mean,_sigma)->-mean + @test_throws DomainError compute(problem,MonteCarlo(inner;trials=2,seed=103, + distribution=negative,on_error=:fail)) + @test attempts==[-.005] + empty!(attempts) + failure=try + compute(problem,MonteCarlo(inner;trials=2,seed=103,distribution=negative, + on_error=:resample,max_failures=3,retain_details=true)) + catch error + error + end + @test attempts==fill(-.005,3) + @test failure isa ErrorException + @test occursin("3 attempts (0 accepted)",sprint(showerror,failure)) + @test occursin("physical radius must be positive",sprint(showerror,failure)) + # This deterministic control tests conditioning and source records only. No claim + # is made that accepted draws follow an unconditioned input law. + calls=Ref(0) + alternating=(_rng,mean,_sigma)->begin + calls[]+=1 + isodd(calls[]) ? -mean : mean + end + empty!(attempts) + sampled=compute(problem,MonteCarlo(inner;trials=2,seed=103,distribution=alternating, + on_error=:resample,max_failures=3,retain_details=true,return_samples=true)) + @test sampled.trial_counts==[2] + @test attempts==[-.005,.005,-.005,.005,.005] + @test only(sampled.values).details.data.inputs.interpretation===:nominal_declaration + @test only(only(sampled.values).details.data.inputs.uncertain_arguments).standard_deviation==.001 + @test length(only(sampled.details.data.failures))==2 + @test only(sampled.details.data.failure_summary).accepted==2 + @test only(sampled.details.data.failure_summary).failed==2 +end diff --git a/test/integration/native_makie.jl b/test/integration/native_makie.jl new file mode 100644 index 000000000..a210f2c1e --- /dev/null +++ b/test/integration/native_makie.jl @@ -0,0 +1,652 @@ +@testitem "Makie addons / material colors dispatch on owned types" tags=[:visual, :makie, :slow] begin + using CairoMakie + using Measurements + using LineCableModels + + extension=Base.get_extension(LineCableModels, :LineCableModelsMakieExt) + for material in ( + Material(kind = :conductor, rho = 1.7241e-8), + Material(kind = :insulator, rho = 1e14, eps_r = 3.5), + RadialDielectric( + [Material(:insulator, 1e14, 3.5), + Material(:semicon, 1e5, 1000.0)], [0.6, 0.1]), + Material(kind = :semicon, rho = 0.06, eps_r = 1000.0), + Material(kind = :conductor, rho = 1e-7, mu_r = 100.0), + layer(rho = 100.0, eps_r = 10.0), + layer(rho = 100.0, eps_r = 10.0, mu_r = 2.0) + ) + base=if material isa EarthLayer + extension._earth_color(material.rho) + elseif material.kind === :conductor + extension._conductor_color(material.rho) + elseif material.kind === :insulator + extension._dielectric_color(material.eps_r) + else + extension._semicon_color(material.rho) + end + expected=extension._magnetic_overlay(base, material.mu_r) + actual=extension._material_color(material; alpha = 0.25) + @test actual == extension.RGBA(expected, 0.25) + @test actual.alpha == 0.25 + end + uncertain=Material(kind = :conductor, rho = measurement(1.7241e-8, 1e-10)) + @test extension._material_color(uncertain) == extension._material_color( + Material(kind = :conductor, rho = 1.7241e-8)) + uncertain_earth=layer(rho = measurement(100.0, 1.0), eps_r = 10.0) + @test extension._material_color(uncertain_earth) == extension._material_color( + layer(rho = 100.0, eps_r = 10.0)) + @test_throws MethodError extension._material_color(( + rho = 100.0, eps_r = 10.0, mu_r = 1.0)) + source = @cable "homogeneous-preview" begin + @terminal :core begin + solid(Material(:conductor, 1.72e-8), Disk(0.01)) + screen(Material(:semicon, 1e5, 1000.0); t = 0.001) + insulation(Material(:insulator, 1e14, 3.5); t = 0.005) + end + end + plot = preview(homogenize(source); backend = :cairo, display_plot = false, controls = false) + @test !isempty(Makie.colorbuffer(plot.figure)) + @test length(plot.axes) == 1 +end + +@testitem "Makie addons / colorbar endpoint labels stay inside the figure" tags=[:visual, :makie, :slow] setup=[ + UseNativePlotSupport +] begin + using CairoMakie + + copper=Material(kind = :conductor, rho = 1.7241e-8) + insulation_material=Material(kind = :insulator, rho = 1e14, eps_r = 3.5) + design=@cable "colorbar-endpoints" begin + @terminal :core begin + core(copper; r = 3e-3) + insulation(insulation_material; t = 1e-3) + end + end + + function ticklabels_fit(plot) + Makie.colorbuffer(plot.figure) + viewport=plot.figure.scene.viewport[] + return all(plot.colorbars) do colorbar + bounds=Makie.boundingbox(colorbar.axis.elements[:ticklabels], :data) + all(>(0), colorbar.layoutobservables.computedbbox[].widths)&&all(1:2) do dimension + bounds.origin[dimension]>=viewport.origin[dimension]-0.5&& + bounds.origin[dimension]+bounds.widths[dimension]<= + viewport.origin[dimension]+viewport.widths[dimension]+0.5 + end + end + end + + @testset "$position / vertical=$vertical" for position in (:left, :right, :top, :bottom), + vertical in (false, true) + + plot = preview(design; backend = :cairo, display_plot = false, controls = false, + size = (850, 650), legend_position = position, colorbar_position = position, + colorbar_attributes = (; vertical)) + @test ticklabels_fit(plot) + colorbar = first(plot.colorbars) + colorbar.ticks[] = (collect(colorbar.limits[]), + ["1.724100 × 10⁻⁸", "2.500000 × 10⁻⁷"]) + colorbar.ticklabelsize[] = 20 + colorbar.ticklabelrotation[] = pi / 8 + @test ticklabels_fit(plot) + # A manual resize is an explicit local allocation, even below the + # complete guide stack's minimum width. It must not snap back or + # silently shrink or reflow the requested scale arrangement. + resize!(plot.figure, 700, 650) + @test Tuple(plot.figure.scene.viewport[].widths)==(700, 650) + if position in (:top, :bottom) && !vertical + group=plot.plot_state.guides[(:colorbars, nothing)].layout[] + @test group.layoutobservables.autosize[][1]>700 + @test colorbar.ticklabelsize[]==20 + @test colorbar.ticklabelrotation[]≈pi/8 + extension=Base.get_extension(LineCableModels, :LineCableModelsMakieExt) + extension._export_presentation!(plot, :default) do + @test ticklabels_fit(plot) # temporary fit includes complete content + end + @test Tuple(plot.figure.scene.viewport[].widths)==(700, 650) + else + @test ticklabels_fit(plot) + end + end + + late=preview(design; backend = :cairo, display_plot = false, controls = false, + display_legend = false, colorbar_attributes = (; vertical = true)) + @test ticklabels_fit(late) + figurelegend!(late; position = :right, max_fraction = 0.5) + @test ticklabels_fit(late) + + figure=Figure(size = (500, 300)) + scheme=materialcolors(:rho, (1.7241e-8, 2.5e-7)) + scale=materialscale!(figure[1, 1], scheme; vertical = false, alignmode = Inside()) + @test scale.alignmode[] isa Inside +end + +@testitem "Makie addons / native figures, layouts, docks, widgets, and export" tags=[:visual, :makie, :slow] setup=[ + UseNativePlotSupport, TestFixtures +] begin + get(ENV, + "LINECABLEMODELS_TEST_PLOTTING", + "false")=="true"|| + error("set LINECABLEMODELS_TEST_PLOTTING=true to run the visual test") + using CairoMakie + + legend_labels(legend) = [entry.label[] for entry in last(first(legend.entrygroups[]))] + + frequency=[50.0, 100.0, 500.0] + parameters=TestFixtures.two_conductor_results(; frequencies = frequency) + + # With no selection, Z is processed before Y and their physical + # components are rendered as four matrix dashboards: `R`, `X`, `G`, then `B`. + automatic=Makie.plot( + parameters; + backend = :cairo, + display_plot = false, + open_export = false + ) + @test automatic isa Vector{UIPlot} + @test length(automatic) == 4 + @test all(page -> length(page.axes) == 4, automatic) + @test all(page -> page.legend === nothing, automatic) + @test [first(page.axes).title[] for page in automatic] == [ + "Series resistance, conductor 1", + "Series reactance, conductor 1", + "Shunt conductance, conductor 1", + "Shunt susceptance, conductor 1" + ] + automatic_page=first(automatic) + @test automatic_page.figure isa Makie.Figure + @test :legend ∉ keys(automatic_page.controls) + + self_impedance_request=@observe Z[1, 1, :] + side_by_side=Makie.plot( + parameters, + self_impedance_request; + backend = :cairo, + display_plot = false, + controls = false + ) + @test side_by_side isa Vector{UIPlot} + @test all(length(page.axes)==1 for page in side_by_side) + @test [only(page.axes).title[] for page in side_by_side] == [ + "Series resistance, conductor 1", + "Series reactance, conductor 1" + ] + @test all(page.legend===nothing for page in side_by_side) + + stacked_complex=Makie.plot( + parameters, + self_impedance_request; + backend = :cairo, + display_plot = false, + controls = false, + layout = (2, 1), + panel_titles = ("Self resistance", "Self reactance") + ) + @test stacked_complex isa Vector{UIPlot} + @test [only(page.axes).title[] for page in stacked_complex] == + ["Self resistance", "Self reactance"] + @test all(Set(keys(page.plot_state.panel_data))==Set(((1, 1),)) + for page in stacked_complex) + + individual=Makie.plot( + parameters, + self_impedance_request; + backend = :cairo, + display_plot = false, + controls = false, + layout = (1, 1) + ) + @test individual isa Vector{UIPlot} + @test length(individual) == 2 + @test all(page -> length(page.axes) == 1, individual) + + horizontal_pages=Makie.plot( + parameters; + backend = :cairo, + display_plot = false, + controls = false, + layout = (1, 2) + ) + @test length(horizontal_pages) == 8 + @test all(page -> length(page.axes) == 2, horizontal_pages) + + stacked_all=Makie.plot( + parameters; + backend = :cairo, + display_plot = false, + controls = false, + layout = (2, 1) + ) + @test length(stacked_all) == 8 + @test all(Set(keys(page.plot_state.panel_data))==Set(( + (1, mod1(index, 2)), (2, mod1(index, 2)))) + for (index, page) in enumerate(stacked_all)) + + one_quantity=Makie.plot( + parameters, + (R,); + backend = :cairo, + display_plot = false, + controls = false + ) + @test one_quantity isa UIPlot + @test length(one_quantity.axes) == 4 + @test isempty(one_quantity.controls) + @test one_quantity.legend === nothing + + initially_logarithmic=Makie.plot( + parameters, + (R, 1, 1, Colon()); + backend = :cairo, + display_plot = false, + controls = false, + xscale = :log10 + ) + Makie.colorbuffer(initially_logarithmic.figure) + @test only(initially_logarithmic.axes).xscale[] === Makie.log10 + @test only(initially_logarithmic.axes).finallimits[].origin[1] > 0 + + compared_self=Makie.plot( + parameters, + parameters, + self_impedance_request; + series_labels = ("reference", "result"), + backend = :cairo, + display_plot = false, + controls = false + ) + @test compared_self isa Vector{UIPlot} + @test length(compared_self) == 2 + @test all(length(page.axes) == 1 for page in compared_self) + @test all(legend_labels(page.legend) == ["reference", "result"] + for page in compared_self) + @test all(Set(keys(page.plot_state.groups)) == Set((:result_1, :result_2)) + for page in compared_self) + + three_sources=Makie.plot( + parameters, + parameters, + parameters, + self_impedance_request; + series_labels = ("reference", "result A", "result B"), + backend = :cairo, + display_plot = false, + controls = false + ) + @test all(legend_labels(page.legend) == + ["reference", "result A", "result B"] for page in three_sources) + + different_frequency=TestFixtures.two_conductor_results( + ; + frequencies = [40.0, 80.0, 160.0, 320.0]) + asynchronous=Makie.plot( + parameters, + different_frequency, + self_impedance_request; + series_labels = ("three samples", "four samples"), + backend = :cairo, + display_plot = false, + controls = false + ) + @test asynchronous isa Vector{UIPlot} + @test all(legend_labels(page.legend) == ["three samples", "four samples"] + for page in asynchronous) + + combined=Makie.plot( + (; reference = parameters, result = parameters), + (R,); + backend = :cairo, + display_plot = false, + layout = (2, 2), + legend_position = :bottom, + legend_attributes = (; orientation = :horizontal), + legend_cap = 0.5 + ) + @test combined isa UIPlot + @test length(combined.axes) == 4 + @test all( + axis -> axis.yticklabelsvisible[] && axis.yticksvisible[] && + axis.ylabelvisible[], combined.axes) + @test combined.legend.orientation[] == :horizontal + @test legend_labels(combined.legend) == ["reference", "result"] + + for (position, + orientation) in ( + (:left, :vertical), (:right, :vertical), + (:top, :horizontal), (:bottom, :horizontal), + ((1, 3), :horizontal) + ) + placed=Makie.plot( + parameters, + parameters, + (R,); + series_labels = ("reference", "result"), + backend = :cairo, + display_plot = false, + legend_position = position, + legend_attributes = (; orientation), + legend_cap = 0.5 + ) + @test placed.legend.orientation[] == orientation + end + + titled_inside=Makie.plot( + parameters, + parameters, + ((R, 1, 1:2, :),); + series_labels = ("reference", "result"), + backend = :cairo, + display_plot = false, + controls = false, + figure_title = "Cable impedance", + panel_titles = ("Resistance", "Mutual resistance"), + legend_position = :inside, + legend_attributes = (halign = :left, valign = :top), + legend_title = "Result set", + legend_cap = 0.5 + ) + Makie.colorbuffer(titled_inside.figure) + @test titled_inside.title.text[] == "Cable impedance" + @test [axis.title[] for axis in titled_inside.axes] == + ["Resistance", "Mutual resistance"] + @test first(only(titled_inside.legend.entrygroups[])) == "Result set" + @test titled_inside.legend.halign[] == :left + @test titled_inside.legend.valign[] == :top + @test legend_labels(titled_inside.legend) == ["reference", "result"] + canvas_bounds=titled_inside.plot_state.shell.canvas.layoutobservables.computedbbox[] + legend_bounds=titled_inside.legend.layoutobservables.computedbbox[] + @test legend_bounds.origin[1] >= canvas_bounds.origin[1] + @test legend_bounds.origin[2] >= canvas_bounds.origin[2] + @test legend_bounds.origin[1] + legend_bounds.widths[1] <= + canvas_bounds.origin[1] + canvas_bounds.widths[1] + @test legend_bounds.origin[2] + legend_bounds.widths[2] <= + canvas_bounds.origin[2] + canvas_bounds.widths[2] + + figuretitle!(titled_inside, "Updated impedance"; fontsize = 20) + paneltitle!(titled_inside, (1, 2), "Updated mutual resistance") + @test titled_inside.title.text[] == "Updated impedance" + @test titled_inside.title.fontsize[] == 20 + @test titled_inside.axes[2].title[] == "Updated mutual resistance" + + figurelegend!( + titled_inside; + position = :right, + title = "Renamed results", + legend_labels = Dict(:result_1=>"baseline"), + max_fraction = 0.5 + ) + @test first(only(titled_inside.legend.entrygroups[])) == "Renamed results" + @test legend_labels(titled_inside.legend) == ["baseline", "result"] + figurelegend!( + titled_inside; + position = :inside, + halign = :right, valign = :bottom, + max_fraction = 0.5 + ) + @test titled_inside.legend.halign[] == :right + @test titled_inside.legend.valign[] == :bottom + + panellegend!( + titled_inside, + (1, 1); + position = :inside, + halign = :left, valign = :bottom, + title = "Resistance results", + legend_labels = ("base R", "result R"), + max_fraction = 0.5 + ) + Makie.colorbuffer(titled_inside.figure) + panel_inside=titled_inside.panel_legends[(1, 1)] + @test first(only(panel_inside.entrygroups[])) == "Resistance results" + @test panel_inside.halign[] == :left + @test panel_inside.valign[] == :bottom + @test legend_labels(panel_inside) == ["base R", "result R"] + first_panel_entry=first(last(first(panel_inside.entrygroups[]))) + Makie.toggle_visibility!(first_panel_entry) + @test all(plot -> !plot.visible[], + titled_inside.plot_state.panel_data[(1, 1)].groups[:result_1]) + @test titled_inside.plot_state.shell.status[] == + "Axis limits fitted to visible series" + Makie.toggle_visibility!(first_panel_entry) + panellegend!(titled_inside, (1, 1); position = :right, max_fraction = 0.5) + @test titled_inside.panel_legends[(1, 1)].orientation[] == :vertical + + paneltitle!(titled_inside, (1, 2), nothing) + @test titled_inside.axes[2].title[] == "" + figuretitle!(titled_inside, nothing) + @test titled_inside.title === nothing + + per_page_titles=Makie.plot( + parameters, + (R, X); + backend = :cairo, + display_plot = false, + controls = false, + figure_title = ("Resistance dashboard", "Reactance dashboard") + ) + @test [page.title.text[] for page in per_page_titles] == + ["Resistance dashboard", "Reactance dashboard"] + @test_throws DimensionMismatch Makie.plot(parameters, (R, X); + backend = :cairo, display_plot = false, controls = false, + figure_title = ("only one title",)) + + @test_throws DimensionMismatch Makie.plot( + parameters, + self_impedance_request; + backend = :cairo, + display_plot = false, + panel_titles = ("only one",) + ) + small_capacity=Makie.plot( + parameters, (R,); backend = :cairo, display_plot = false, layout = (1, 3)) + @test length(small_capacity)==2 + @test all(length(page.axes)==2 for page in small_capacity) + + compared=Makie.plot( + (; reference = parameters, result = parameters), + (R, X, G, B); + backend = :cairo, + display_plot = false, + legend_position = :top, + legend_attributes = (; orientation = :horizontal) + ) + @test length(compared) == 4 + @test all(plot -> length(plot.axes) == 4, compared) + @test all(plot -> legend_labels(plot.legend) == ["reference", "result"], compared) + + first_axis=first(automatic_page.axes) + first_axis.title[]="caller-owned title" + extra=lines!(first_axis, frequency, fill(1.0e-4, 3); color = :red) + @test first_axis.title[] == "caller-owned title" + @test extra in first_axis.scene.plots + automatic_page.controls[:xlog].active[]=true + @test all(axis -> axis.xscale[] === Makie.log10, automatic_page.axes) + automatic_page.controls[:xlog].active[]=false + @test all(axis -> axis.xscale[] === Makie.identity, automatic_page.axes) + @test occursin("10", sprint(show, first_axis.ylabel[])) + + include(joinpath(pkgdir(LineCableModels), "test/support/scenarios.jl")) + design=CurrentScenarios.coaxial_design() + + cable=preview( + design; + backend = :cairo, + display_plot = false, + open_export = false + ) + @test cable isa UIPlot + @test length(cable.axes) == 1 + @test cable.legend !== nothing + @test length(cable.colorbars) == 3 + @test all(colorbar -> !colorbar.vertical[], cable.colorbars) + @test Set(keys(cable.controls)) == Set((:reset, :export_svg, :legend)) + + grouped_cable=preview( + design; + backend = :cairo, + display_plot = false, + controls = false, + display_colorbars = false, + # Both independently tagged metal regions share one caller-defined + # legend entry. The dielectric regions retain their own identities. + legend_group = region->region.source.tag in (:metal, :return) ? + :conductors : region.source.tag, + legend_labels = Dict(:conductors=>"Conductors") + ) + @test count(==("Conductors"), legend_labels(grouped_cable.legend)) == 1 + @test length(legend_labels(grouped_cable.legend)) == 3 + + late_legend_cable=preview( + design; + backend = :cairo, + display_plot = false, + controls = false, + display_legend = false + ) + @test late_legend_cable.legend === nothing + figurelegend!(late_legend_cable; position = :right, max_fraction = 0.5) + Makie.colorbuffer(late_legend_cable.figure) + late_legend_bounds=late_legend_cable.legend.layoutobservables.computedbbox[] + late_colorbar_bounds=[colorbar.layoutobservables.computedbbox[] + for colorbar in late_legend_cable.colorbars] + @test late_legend_bounds.origin[2] > maximum( + bounds.origin[2] + bounds.widths[2] for bounds in late_colorbar_bounds + ) + figurelegend!( + late_legend_cable; + position = :top, + orientation = :horizontal, + max_fraction = 0.5 + ) + Makie.colorbuffer(late_legend_cable.figure) + @test late_legend_cable.legend.orientation[] == :horizontal + @test length(late_legend_cable.colorbars) == 3 + + ranges=LineCableModels.DataModel.material_property_ranges(design) + rho_scheme=LineCableModels.materialcolors(:rho, ranges.rho) + native_scale_figure=Figure(; size = (500, 300)) + native_scale_axis=Axis(native_scale_figure[1, 1]) + lines!(native_scale_axis, 1:3, 1:3) + native_scale=materialscale!( + native_scale_figure[2, 1], + rho_scheme; + vertical = false + ) + Makie.colorbuffer(native_scale_figure) + @test !native_scale.vertical[] + @test native_scale.layoutobservables.computedbbox[].widths[1] > 300 + + moved_docks=preview( + design; + backend = :cairo, + display_plot = false, + legend_position = :top, + legend_attributes = (; orientation = :horizontal), + colorbar_position = :left, + colorbar_attributes = (; vertical = true) + ) + @test moved_docks.legend.orientation[] == :horizontal + @test all(colorbar -> colorbar.vertical[], moved_docks.colorbars) + + collection=preview( + [design, design, design, design]; + layout = (2, 2), + backend = :cairo, + display_plot = false, + figure_title = "Design family", + panel_titles = ("A", "B", "C", "D") + ) + @test length(collection.axes) == 4 + @test length(collection.colorbars) == 3 + @test collection.title.text[] == "Design family" + @test [axis.title[] for axis in collection.axes] == ["A", "B", "C", "D"] + + system=TestFixtures.three_phase_system() + earth=EarthModel(100.0, 10.0, 1.0) + system_plot=preview( + system; + earth_model = earth, + backend = :cairo, + display_plot = false + ) + @test length(system_plot.axes) == 1 + @test system_plot.legend !== nothing + @test length(system_plot.colorbars) == 3 + + scale=LineCableModels.show_material_scale( + backend = :cairo, + display_plot = false, + figure_title = "Material schemes", + colorbar_position = :top, + colorbar_attributes = (; vertical = false) + ) + @test isempty(scale.axes) + @test length(scale.colorbars) == 3 + @test scale.title.text[] == "Material schemes" + + native_grid=LineCableModels.plotwindow( + ; title = "native 2x2", size = (700, 500), + figure_title = "Native dashboard", + backend = :cairo, display_plot = false, open_export = false + ) do grid + for row in 1:2, column in 1:2 + + axis=Axis(grid[row, column]; title = "$row,$column") + lines!(axis, 1:3, (1:3) .* (row+column)) + end + end + @test length(native_grid.axes) == 4 + @test Set(keys(native_grid.controls)) == Set((:reset, :export_svg, :xlog, :ylog)) + @test native_grid.title.text[] == "Native dashboard" + + report_plot=report( + TableReportDefinition( + ((R, Colon(), Colon(), Colon()),); + illustration = true, + plot_options = (; + backend = :cairo, + display_plot = false, + open_export = false + ) + ), + parameters + ).illustration + @test report_plot isa UIPlot + @test length(report_plot.axes) == 4 + @test occursin("Frequency", sprint(show, last(report_plot.axes).xlabel[])) + @test length(report_plot.plot_state.groups) == 1 + + extension=Base.get_extension(LineCableModels, :LineCableModelsMakieExt) + root=combined.figure.layout + row_sizes_before=copy(root.rowsizes) + row_gap_before=root.default_rowgap + background_before=combined.figure.scene.backgroundcolor[] + font_before=combined.figure.scene.theme[:fonts][:regular][] + reset_visible_before=combined.controls[:reset].blockscene.visible[] + extension._export_presentation!(combined, :publication) do + @test !combined.controls[:reset].blockscene.visible[] + publication_font=Makie.to_font(combined.figure.scene.theme[:fonts], :regular) + @test occursin("NewComputerModern", sprint(show, publication_font)) + end + @test root.rowsizes == row_sizes_before + @test root.default_rowgap == row_gap_before + @test combined.figure.scene.backgroundcolor[] == background_before + @test combined.figure.scene.theme[:fonts][:regular][] === font_before + @test combined.controls[:reset].blockscene.visible[] == reset_visible_before + + mktempdir() do directory + path=export_svg( + combined; + path = joinpath(directory, "current.svg"), + theme = :publication, + open_file = false + ) + @test isfile(path) + @test filesize(path) > 0 + @test combined.legend.orientation[] == :horizontal + @test extension.current_backend_symbol() == :cairo + @test root.rowsizes == row_sizes_before + @test root.default_rowgap == row_gap_before + @test combined.controls[:reset].blockscene.visible[] == reset_visible_before + end +end diff --git a/test/integration/native_makie_axis_format.jl b/test/integration/native_makie_axis_format.jl new file mode 100644 index 000000000..f3e30d98a --- /dev/null +++ b/test/integration/native_makie_axis_format.jl @@ -0,0 +1,933 @@ +@testmodule ScientificTickLabels begin + # Reconstruct physical values from the displayed notation, independently of + # the formatter. A zero absolute tolerance detects tiny ticks printed as zero. + function value(label) + text = replace(String(label), "−" => "-") + label isa AbstractString && return parse(Float64, text) + parts = match(r"^(?:(-?\d+(?:\.\d+)?)×|(-?))10(-?\d+)$", text) + parts === nothing && error("Not a scientific tick label: $text") + coefficient = parts[1] === nothing ? (parts[2] == "-" ? "-1" : "1") : parts[1] + return parse(Float64, coefficient * "e" * parts[3]) + end +end + +@testitem "Makie addons / narrow log values and native constructors share the shell" tags=[:visual, :makie, :slow] begin + using CairoMakie, Measurements, Logging + f = collect(range(2.0,8.0;length=9)) + r = reshape(measurement.(reverse(f),0.02),1,1,:) + source = LineParameters(complex.(r,r),ones(ComplexF64,1,1,9),f) + before = deepcopy((Z(source),Y(source),frequencies(source))) + logger = Test.TestLogger(min_level=Logging.Warn) + with_logger(logger) do + page = LineCableModels.plot(source; ydata=(R,),length_unit=:base, + backend=:cairo,display_plot=false,open_export=false, + figure=(size=(800,500),),axis=(limits=((2.0,8.0),(1.9,8.1)),),linewidth=3) + axis = only(page.axes) + line = only(filter(p -> p isa Makie.Lines,axis.scene.plots)) + @test line.linewidth[] == 3 + requested = axis.limits[] + for _ in 1:2 + page.controls[:xlog].active[] = true + page.controls[:ylog].active[] = true + Makie.colorbuffer(page.figure) + @test page.controls[:xlog].active[] && page.controls[:ylog].active[] + for dim in (:x,:y) + native = getproperty(axis,Symbol(dim,:axis)) + @test length(native.tickvalues[]) >= 3 + @test all(label -> label isa AbstractString,native.ticklabels[]) + @test parse.(Float64,native.ticklabels[]) ≈ native.tickvalues[] + end + # Raw points staying equal cannot detect a failed transform. Compare + # rendered geometry with the log of these distinguishable values. + raw = Makie.Point2d.(line[1][]) + pixels = Makie.transform_and_project(line,:data,:pixel,raw) + for dim in 1:2 + projected = getindex.(pixels,dim) + values = getindex.(raw,dim) + @test (projected.-first(projected))./(last(projected)-first(projected)) ≈ + (log10.(values).-log10(first(values)))./(log10(last(values))-log10(first(values))) atol=2e-6 + end + for bars in filter(p -> p isa Makie.Errorbars,axis.scene.plots), child in bars.plots + @test child.transformation.transform_func[] == line.transformation.transform_func[] + # Check rendered bar endpoints as well as their parent scale. + # A pre-logged child could otherwise pass a transform-only check. + endpoints = filter(point -> all(isfinite,point),Makie.Point2d.(child[1][])) + projected = Makie.transform_and_project(child,:data,:pixel,endpoints) + limits = axis.finallimits[] + viewport = axis.scene.viewport[] + for dimension in 1:2 + lower = limits.origin[dimension] + upper = lower+limits.widths[dimension] + fractions = (log10.(getindex.(endpoints,dimension)).-log10(lower))./ + (log10(upper)-log10(lower)) + @test getindex.(projected,dimension)./viewport.widths[dimension] ≈ fractions atol=2e-6 + end + end + @test axis.limits[] == requested + page.controls[:xlog].active[] = false + page.controls[:ylog].active[] = false + end + page.controls[:ylog].active[] = true + for bounds in ((2.71,2.79),(2e-12,8e-12),(2e100,8e100)) + ylims!(axis,bounds...) + Makie.colorbuffer(page.figure) + @test length(axis.yaxis.tickvalues[]) >= 2 + @test allunique(axis.yaxis.ticklabels[]) + @test all(label -> label isa AbstractString,axis.yaxis.ticklabels[]) + end + ylims!(axis,0.1,1e7) + Makie.colorbuffer(page.figure) + @test all(value -> isapprox(log10(Float64(value)),round(log10(Float64(value)));atol=1e-7),axis.yaxis.tickvalues[]) + @test !occursin("× 10",repr(axis.ylabel[])) + for dim in (:x,:y), bounds in ((0.6,3.0),(2.0,80.0),(2.71,2.79),(8.0,12.0)) + getproperty(axis,Symbol(dim,:scale))[] = log10 + (dim === :x ? xlims! : ylims!)(axis,bounds...) + Makie.colorbuffer(page.figure) + native = getproperty(axis,Symbol(dim,:axis)) + @test all(label -> label isa AbstractString,native.ticklabels[]) + # Measure actual strings against their native projected positions. + # Density may change. Collisions or identical labels may not. + dimension = dim === :x ? 1 : 2 + fontsize = getproperty(axis,Symbol(dim,:ticklabelsize))[] + probe = text!(axis.blockscene,0,0;visible=false,fontsize, + font=getproperty(axis,Symbol(dim,:ticklabelfont))[], + rotation=getproperty(axis,Symbol(dim,:ticklabelrotation))[]) + extents = map(native.ticklabels[]) do label + probe.text[] = label + Makie.boundingbox(probe,:data).widths[dimension] + end + @test allunique(native.ticklabels[]) + @test all(diff(getindex.(native.tickpositions[],dimension)) .>= + (extents[1:end-1].+extents[2:end])./2 .+fontsize/2 .-1) + delete!(axis.blockscene,probe) + end + ylims!(axis,2e-12,8e-12) + axis.yticks[] = ([3e-12,7e-12],["low","high"]) + Makie.colorbuffer(page.figure) + @test axis.yaxis.ticklabels[] == ["low","high"] + @test !occursin("× 10",repr(axis.ylabel[])) + mktempdir() do directory + @test isfile(export_svg(page;path=joinpath(directory,"narrow.svg"),open_file=false)) + @test axis.yaxis.ticklabels[] == ["low","high"] + end + end + @test isempty(logger.logs) + @test isequal(before,(Z(source),Y(source),frequencies(source))) +end + +@testitem "Makie addons / current visible extents control log eligibility" tags=[:visual, :makie] begin + using CairoMakie + page = LineCableModels.plotwindow(;title="Native",backend=:cairo, + display_plot=false,open_export=false) do layout + axis = Axis(layout[1,1]) + lines!(axis,[1.0,2.0],[2.0,2.0]) + lines!(axis,[1.0,2.0],[-1.0,-2.0]) + end + axis = only(page.axes) + @test haskey(page.controls,:xlog) && haskey(page.controls,:ylog) + previous = axis.targetlimits[] + @test_throws DomainError axisscale!(page,:y,log10) + @test axis.yscale[] === identity + @test axis.targetlimits[] == previous + page.controls[:ylog].active[] = true + @test page.controls[:ylog].active[] + @test axis.yscale[] isa Makie.ReversibleScale + @test axis.targetlimits[].origin[1] == previous.origin[1] + @test axis.targetlimits[].widths[1] == previous.widths[1] + last(axis.scene.plots).visible[] = false + page.controls[:ylog].active[] = false + page.controls[:ylog].active[] = true + Makie.colorbuffer(page.figure) + @test axis.yscale[] === log10 + low = axis.finallimits[].origin[2] + high = low+axis.finallimits[].widths[2] + @test 1.8 < low < 2 < high < 2.2 +end + +@testitem "Makie addons / signed log separates small conductances in displayed units" tags=[:visual, :makie, :slow] setup=[ScientificTickLabels] begin + using CairoMakie + f = 10.0 .^ range(-1, 6; length=8) + conductance = -10.0 .^ range(-27, -6; length=8) + # Use the exact Float64 cutoff for the separate default-clipping check. + conductance[6] = -1e-12 + admittance = reshape(complex.(conductance, 1e-8), 1, 1, :) + source = LineParameters(ones(ComplexF64, 1, 1, 8), admittance, f) + positions = Vector{Float64}[] + for (length_unit, clip) in ((:base, false), (:kilo, true)) + factor = length_unit === :base ? 1.0 : 1000.0 + expected = factor .* conductance + page = LineCableModels.plot(source; ydata=(G,), backend=:cairo, + display_plot=false, open_export=false, length_unit, clip, atol=(G=0., B=0.), + xscale=:log10, yscale=:log10) + axis = only(page.axes) + line = only(filter(p -> p isa Makie.Lines, axis.scene.plots)) + for _ in 1:2 + Makie.colorbuffer(page.figure) + @test all(!iszero, last.(line[1][])) + @test all(isapprox.(last.(line[1][]), expected; rtol=1e-14, atol=0)) + pixels = last.(Makie.transform_and_project(line, :data, :pixel, Makie.Point2d.(line[1][]))) + normalized = (pixels .- first(pixels)) ./ (last(pixels) - first(pixels)) + # Compare rendered geometry with an independent high-precision evaluation. + transformed = Float64.(-log10.(1 .+ abs.(BigFloat.(expected)) ./ minimum(abs, BigFloat.(expected)))) + @test normalized ≈ (transformed .- first(transformed)) ./ (last(transformed) - first(transformed)) atol=2e-6 + @test all(>(0.1), diff(normalized)) + push!(positions, normalized) + inverse = Makie.inverse_transform(axis.yscale[]) + @test all(isapprox.(inverse.(axis.yscale[].(expected)), expected; rtol=1e-13, atol=0)) + @test any(t -> 0 < abs(t) < factor * 1e-18, axis.yaxis.tickvalues[]) + @test allunique(axis.yaxis.ticklabels[]) + # Labels have at least three significant digits. Check their displayed + # precision with zero absolute tolerance, so false zeros always fail. + @test all(isapprox.(ScientificTickLabels.value.(axis.yaxis.ticklabels[]), axis.yaxis.tickvalues[]; + rtol=0.005, atol=0)) + @test page.controls[:ylog].active[] + page.controls[:ylog].active[] = false + @test axis.yscale[] === identity + @test all(isapprox.(last.(line[1][]), expected; rtol=1e-14, atol=0)) + page.controls[:ylog].active[] = true + end + scale = axis.yscale[] + page.controls[:reset].clicks[] += 1 + resize!(page.figure, 900, 600) + @test axis.yscale[] === scale + axis.yticks[] = (factor .* [-1e-24, -1e-12], ["small", "large"]) + Makie.colorbuffer(page.figure) + @test axis.yaxis.ticklabels[] == ["small", "large"] + mktempdir() do directory + @test isfile(export_svg(page; path=joinpath(directory, "conductance.svg"), open_file=false)) + @test axis.yaxis.ticklabels[] == ["small", "large"] + end + end + @test all(values -> values ≈ first(positions), positions) + clipped = LineCableModels.plot(source; ydata=(G,), backend=:cairo, + display_plot=false, controls=false, length_unit=:base) + line = only(filter(p -> p isa Makie.Lines, only(clipped.axes).scene.plots)) + @test last.(line[1][])[1:6] == zeros(6) + @test last.(line[1][])[7:8] ≈ conductance[7:8] + @test Y(source) == admittance +end + +@testitem "Makie addons / signed and positive axes share scientific notation" tags=[:visual, :makie] setup=[ScientificTickLabels] begin + using CairoMakie + samples=([1.0, 1.5, 2.0] .* 1e-6, [-1.0, -1.5, -2.0] .* 1e-6, + [-3.7e-6, -3.7e-10, 0.0, 3.7e-10, 3.7e-6], [3.7e-10, 3.7e-6]) + page=LineCableModels.plotwindow(; title = "Signed and positive axes", + backend = :cairo, display_plot = false, + open_export = false, size = (1200, 800), axis = (xscale = :log10, yscale = :log10)) do grid + for (i, values) in enumerate(samples) + axis=Axis(grid[cld(i, 2), mod1(i, 2)]; xlabel = "Input", ylabel = "Output") + lines!(axis, values, values) + end + end + for action in (:construction, :toggles, :direct, :reset, :resize) + if action===:toggles + for dim in (:x, :y) + control=page.controls[Symbol(dim, :log)] + control.active[]=false + control.active[]=true + end + elseif action===:direct + for dim in (:x, :y) + axisscale!(page, dim, :linear) + axisscale!(page, dim, :log10) + end + elseif action===:reset + page.controls[:reset].clicks[]+=1 + elseif action===:resize + resize!(page.figure, 1000, 700) + end + Makie.colorbuffer(page.figure) + for (i, axis) in enumerate(page.axes), dim in (:x, :y) + + native=getproperty(axis, Symbol(dim, :axis)) + labels=native.ticklabels[] + @test length(labels) >= 2 + @test allunique(String.(labels)) + displayed=ScientificTickLabels.value.(labels) + if i<=2 + @test all(label -> label isa AbstractString, labels) + @test count("× 10", String(getproperty(axis, Symbol(dim, :label))[])) == 1 + @test occursin("−6", String(getproperty(axis, Symbol(dim, :label))[])) + @test all(label -> !occursin(r"[eE]", label), String.(labels)) + displayed.*=1e-6 + else + @test !occursin("× 10", String(getproperty(axis, Symbol(dim, :label))[])) + @test all( + label -> label == "0" || + (label isa Makie.RichText && + last(label.children).type === :sup), + labels) + end + @test all(isapprox.(displayed, native.tickvalues[]; rtol = 0.005, atol = 0)) + line=only(filter(plot->plot isa Makie.Lines, axis.scene.plots)) + @test first.(line[1][]) ≈ samples[i] + @test last.(line[1][]) ≈ samples[i] + # Inspect spacing in rendered coordinates, for both signs and axes. + dimension=dim===:x ? 1 : 2 + fontsize=getproperty(axis, Symbol(dim, :ticklabelsize))[] + probe=text!(axis.blockscene, 0, 0; visible = false, fontsize, + font = getproperty(axis, Symbol(dim, :ticklabelfont))[], + rotation = getproperty(axis, Symbol(dim, :ticklabelrotation))[]) + extents=map(labels) do label + probe.text[]=label + Makie.boundingbox(probe, :data).widths[dimension] + end + @test all(diff(getindex.(native.tickpositions[], dimension)) .>= + (extents[1:(end - 1)] .+ extents[2:end]) ./ 2 .+ fontsize / 2 .- 1) + delete!(axis.blockscene, probe) + end + end + signed_axis=page.axes[3] + # The signed reference is 3.7e-10, not a power of ten. Labels must include + # its coefficient. Physical positions must retain their values between whole decades. + labels=signed_axis.ytickformat[]([-3.7e-7, -0.0, 3.7e-7]) + @test String.(labels) == ["−3.7×10−7", "0", "3.7×10−7"] + @test ScientificTickLabels.value.(labels) == [-3.7e-7, 0.0, 3.7e-7] + # Zoom changes notation using the displayed transformed span, without + # replacing the signed reference or changing the underlying data. + scale=signed_axis.yscale[] + ylims!(signed_axis, -2e-10, 2e-10) + Makie.colorbuffer(page.figure) + @test signed_axis.yscale[] === scale + @test occursin("× 10−12", String(signed_axis.ylabel[])) + @test signed_axis.ytickformat[]([-2e-10, 0.0, 2e-10]) == ["-200", "0", "200"] + custom=values->fill("custom", length(values)) + signed_axis.ytickformat[]=custom + axisscale!(page, :y, :linear) + axisscale!(page, :y, :log10) + @test signed_axis.ytickformat[] === custom + @test signed_axis.ylabel[] == "Output" + signed_axis.ytickformat[]=Makie.automatic + signed_axis.yticks[]=([-2e-10, 2e-10], ["low", "high"]) + Makie.colorbuffer(page.figure) + @test signed_axis.yaxis.ticklabels[] == ["low", "high"] + @test signed_axis.ylabel[] == "Output" + mktempdir() do directory + @test isfile(export_svg(page; path = joinpath(directory, "signed.svg"), open_file = false)) + @test signed_axis.yaxis.ticklabels[] == ["low", "high"] + end +end + +@testitem "Makie addons / signed reference follows native samples and interval endpoints" tags=[:visual, :makie] begin + using CairoMakie + magnitude = 1e-6 + endpoint = magnitude - prevfloat(magnitude) + page = LineCableModels.plotwindow(; title="Signed intervals", backend=:cairo, + display_plot=false, open_export=false, axis=(yscale=:log10,)) do grid + axis = Axis(grid[1, 1]) + lines!(axis, [1., 2., 3.], [-magnitude, magnitude, magnitude]) + errorbars!(axis, [2.], [magnitude], [prevfloat(magnitude)]; yautolimits=true) + end + axis = only(page.axes) + line = only(filter(p -> p isa Makie.Lines, axis.scene.plots)) + bars = only(filter(p -> p isa Makie.Errorbars, axis.scene.plots)) + @test axis.yscale[](endpoint) ≈ log10(2) rtol=1e-14 + @test axis.yscale[](-endpoint) ≈ -log10(2) rtol=1e-14 + Makie.colorbuffer(page.figure) + @test all(point -> all(isfinite, point), axis.yaxis.tickpositions[]) + for attribute in (:visible, :yautolimits) + getproperty(bars, attribute)[] = false + axisscale!(page, :y, :log10) + @test axis.yscale[](magnitude) ≈ log10(2) rtol=1e-14 + getproperty(bars, attribute)[] = true + axisscale!(page, :y, :log10) + @test axis.yscale[](endpoint) ≈ log10(2) rtol=1e-14 + end + # The nearest sample is interior, and current native points own later edits. + line[1][] = Makie.Point2d[(1., -magnitude), (2., 1e-26), (3., magnitude)] + axisscale!(page, :y, :log10) + @test axis.yscale[](1e-26) ≈ log10(2) rtol=1e-14 + upper_bound = big"1e-28" + ylims!(axis, nothing, upper_bound) + axisscale!(page, :y, :log10) + @test axis.yscale[](upper_bound) ≈ log10(2) rtol=1e-14 + @test axis.limits[][2] == (nothing, upper_bound) + autolimits!(axis) + bars.visible[] = false + line[1][] = Makie.Point2d[(1., -1e-300), (2., 0.), (3., 1e-6)] + axisscale!(page, :y, :log10) + inverse = Makie.inverse_transform(axis.yscale[]) + # The ratio overflows in a naive implementation although these values are finite. + for value in (-1e100, -1e-300, 0., 1e-300, 1e100) + @test isfinite(axis.yscale[](value)) + @test inverse(axis.yscale[](value)) ≈ value rtol=2e-13 atol=0 + end + axisscale!(page, :y, :pseudolog10) + @test axis.yscale[](1.) ≈ log10(2) +end + +@testitem "Makie addons / signed reference includes undrawn uncertainty endpoints" tags=[:visual, :makie] begin + using CairoMakie, Measurements + f = collect(1.:101.) + conductance = fill(1e-6, length(f)) + conductance[1] = -1e-6 + errors = fill(1e-8, length(f)) + errors[2] = prevfloat(1e-6) + endpoint = conductance[2] - errors[2] + admittance = reshape(complex.(measurement.(conductance, errors), 1e-8), 1, 1, :) + source = LineParameters(ones(ComplexF64, 1, 1, length(f)), admittance, f) + page = LineCableModels.plot(source; ydata=(G,), backend=:cairo, display_plot=false, + open_export=false, length_unit=:base, clip=false, errorbar_sampling=:staggered, + yscale=:log10) + axis = only(page.axes) + bars = only(filter(p -> p isa Makie.Errorbars, axis.scene.plots)) + @test f[2] ∉ first.(bars[1][]) + @test axis.yscale[](endpoint) ≈ log10(2) rtol=1e-14 + bars.visible[] = false + axisscale!(page, :y, :log10) + @test axis.yscale[](1e-6) ≈ log10(2) rtol=1e-14 + bars.visible[] = true + # An independently edited native interval replaces its original full support. + bars[1][] = Makie.Vec4d[(1., -1e-6, 1e-8, 1e-8)] + axisscale!(page, :y, :log10) + @test axis.yscale[](0.99e-6) ≈ log10(2) rtol=1e-14 + @test isequal(Y(source), admittance) +end + +@testitem "Makie addons / native override precedence and custom transforms" tags=[:visual, :makie, :slow] begin + using CairoMakie + source = LineParameters(reshape(ComplexF64[2,4,8],1,1,:),ones(ComplexF64,1,1,3),[2.0,4.0,8.0]) + page = LineCableModels.plot(source; ydata=R,backend=:cairo,display_plot=false, + open_export=false,length_unit=:base,xscale=sqrt, + figure=(size=(500,700),),ylabel="Shared",axis=(ylabel="Explicit",), + linewidth=3,series_attributes=((linewidth=5,),)) + axis = only(page.axes) + @test axis.xscale[] === sqrt + @test !haskey(page.controls,:xlog) + @test axis.ylabel[] == "Explicit" + @test Tuple(page.figure.scene.viewport[].widths) == (500,700) + @test only(filter(p->p isa Makie.Lines,axis.scene.plots)).linewidth[] == 5 + @test_throws ArgumentError LineCableModels.plot(source;ydata=R,backend=:cairo, + display_plot=false,not_a_native_attribute=true) + # Reject a second public selection grammar, without banning legitimate + # local variables called quantities inside the implementation. + @test_throws ArgumentError LineCableModels.plot(source;backend=:cairo, + display_plot=false,quantities=(R,)) + @test_throws ArgumentError LineCableModels.plotwindow(_ -> nothing; + title="No axis",backend=:cairo,display_plot=false,axis=(xlabel="Unused",)) + + native = LineCableModels.plotwindow(;title="Native overrides",backend=:cairo, + display_plot=false,open_export=false,axis=(xscale=log10,ylabel="Explicit"), + linewidth=4) do grid + axis = Axis(grid[1,1];xlabel="Native label",ylabel="Native label",limits=(2,8,2,8)) + lines!(axis,[2.0,4.0,8.0],[2.0,4.0,8.0]) + end + axis = only(native.axes) + @test axis.xscale[] === log10 + @test axis.xlabel[] == "Native label" + @test axis.ylabel[] == "Explicit" + @test axis.limits[] == (2,8,2,8) + @test first(axis.scene.plots).linewidth[] == 4 + @test native.controls[:xlog].active[] + @test !native.controls[:ylog].active[] + @test !isempty(Makie.colorbuffer(native.figure)) +end + +@testitem "Makie addons / axis multipliers follow the displayed range" tags=[:visual, :makie] begin + using CairoMakie + + frequency = [1.0, 10.0, 100.0] + impedance = fill(1.0 + 2.0im, 1, 1, 3) + susceptance = reshape([-1.0, 0.0, 1.0] .* 1e-14, 1, 1, 3) + parameters = LineParameters(impedance, complex.(zero.(susceptance), susceptance), frequency) + plot = Makie.plot(parameters, B; backend=:cairo, display_plot=false, + controls=false, length_unit=:base, quantity_units=:base, clip=false) + axis = only(plot.axes) + Makie.colorbuffer(plot.figure) + # Tiny values are not constant merely because their SI magnitude is small. + @test axis.finallimits[].widths[2] < 1e-12 + @test axis.ytickformat[]([-1e-14, 0.0, 1e-14]) == ["-10", "0", "10"] + + ylims!(axis, -2e-8, 2e-8) + Makie.colorbuffer(plot.figure) + @test axis.ytickformat[]([-1e-8, 0.0, 1e-8]) == ["-10", "0", "10"] + @test occursin("−9", repr(axis.ylabel[])) + @test !occursin("−15", repr(axis.ylabel[])) + axis.ylabel[] = "Caller response [S/m]" + @test occursin("Caller response", repr(axis.ylabel[])) + @test occursin("−9", repr(axis.ylabel[])) + resize!(plot.figure, 1000, 600) + Makie.colorbuffer(plot.figure) + label = repr(axis.ylabel[]) + ticks = axis.ytickformat[]([-1e-8, 0.0, 1e-8]) + mktempdir() do directory + path = export_svg(plot; path=joinpath(directory, "axis.svg"), open_file=false) + @test isfile(path) + @test repr(axis.ylabel[]) == label + @test axis.ytickformat[]([-1e-8, 0.0, 1e-8]) == ticks + end + @test Y(parameters) == complex.(zero.(susceptance), susceptance) + publication = observables(parameters, ((@observe B[1, 1, :]),);complete_pairs=true, + length_unit=:base, quantity_units=:base, clip=false) + published_plot = Makie.plot(publication;ydata=(B,), backend=:cairo, display_plot=false, + controls=false) + published_axis = only(published_plot.axes) + ylims!(published_axis, -2e-9, 2e-9) + Makie.colorbuffer(published_plot.figure) + @test published_axis.ytickformat[]([-1e-9, 0.0, 1e-9]) == ["-1", "0", "1"] + @test occursin("−9", repr(published_axis.ylabel[])) +end + +@testitem "Makie addons / near-constant views and stable signed scales share the lifecycle" tags=[:visual, :makie] begin + using CairoMakie, Logging + options = (backend=:cairo, display_plot=false, controls=true, open_export=false, + length_unit=:base, quantity_units=:base, clip=false) + f = [1.0, nextfloat(1.0), nextfloat(nextfloat(1.0))] + response = [-3.592027795269888e-10, -3.592027795269885e-10, -3.592027795269883e-10] + source = LineParameters(reshape(response .+ im, 1, 1, :), ones(ComplexF64, 1, 1, 3), f) + logger = Test.TestLogger(min_level=Logging.Warn) + page = with_logger(logger) do + page = LineCableModels.plot(source; ydata=(R,), xscale=:linear, options...) + Makie.colorbuffer(page.figure) + page + end + @test isempty(logger.logs) + axis = only(page.axes) + @test axis.finallimits[].widths[1] ≈ 0.1 + @test axis.finallimits[].widths[2] ≈ 0.1abs(first(response)) + @test axis.limits[] == (nothing, nothing) + # Native zoom and explicit narrow views remain possible. Padding belongs + # only to automatic fitting, not to tick updates or every targetlimits write. + manual = Makie.Rect2d(1.0, minimum(response), 1e-12, 1e-20) + axis.targetlimits[] = manual + @test axis.targetlimits[] == manual + ylims!(axis, minimum(response), maximum(response)) + @test axis.targetlimits[].widths[2] < 1e-22 + axis.limits[] = ((0.9, nothing), (nothing, -3e-10)) + page.controls[:reset].clicks[] += 1 + @test axis.targetlimits[].origin[1] == 0.9 + @test axis.targetlimits[].origin[2] + axis.targetlimits[].widths[2] ≈ -3e-10 + autolimits!(axis) + with_logger(logger) do + for _ in 1:2 + page.controls[:ylog].active[] = true + scale = axis.yscale[] + inverse = Makie.inverse_transform(scale) + # Numerical behavior, not identity of a dependency's unstable scale, + # is the required scale behavior. It must work both near zero and over large decades. + for value in (-1e100, -1.0, -1e-18, 0.0, 1e-18, 1.0, 1e100) + @test inverse(scale(value)) ≈ value rtol=1e-13 + @test sign(scale(value)) == sign(value) + end + for bounds in ((-2e-18, 2e-18), (-1000.0, 1000.0)) + ylims!(axis, bounds...) + Makie.colorbuffer(page.figure) + @test length(axis.yaxis.tickvalues[]) >= 2 + @test allunique(axis.yaxis.ticklabels[]) + @test all(point -> all(isfinite, point), axis.yaxis.tickpositions[]) + end + autolimits!(axis) + page.controls[:ylog].active[] = false + page.controls[:reset].clicks[] += 1 + end + mktempdir() do directory + @test isfile(export_svg(page; path=joinpath(directory, "resolved.svg"), open_file=false)) + end + end + @test isempty(logger.logs) + @test vec(real.(Z(source))) == response +end + +@testitem "Makie addons / rendered benchmark axes share scale and limit behavior" tags=[:visual, :makie, :slow] begin + using CairoMakie, Statistics, Measurements + using LineCableModels.ReportBuilder: BenchmarkTableDefinition + f = 10.0 .^ range(-1, 7; length=101) + omega = reshape(2pi .* f, 1, 1, :) + r = [(i + 2j) * 1e-3 * (1 + log10(1 + x)) for i in 1:2, j in 1:2, x in f] + v = (R=r, L=r .* 1e-4, C=r .* 1e-8, G=r .* 1e-5) + core = LineParameters(v.R .+ im .* omega .* v.L, v.G .+ im .* omega .* v.C, f) + metadata = (port_order=["a", "b"], formulation=NamedTuple(Formulation()), axes=nothing) + deterministic = report(BenchmarkTableDefinition((R,); bands=(:all,)), + (reference=(result=core, metadata=metadata), result=(result=core, metadata=metadata));observation_options=(length_unit=:base,)) + summaries = map(a -> map(x -> SampleSummary([0.9x, 1.1x]), a), v) + mc = MonteCarloResult(MonteCarlo(Formulation(); trials=2, seed=7), + [LineCableModels.materialize(core,summaries)], + [summaries], nothing, nothing, UInt64(7), UInt64[8], [2]) + m = map(a -> measurement.(a, sqrt(2) * 0.1 .* a), v) + lep = LinearErrorResult(LinearError(Formulation()), [LineParameters( + m.R .+ im .* omega .* m.L, m.G .+ im .* omega .* m.C, measurement.(f, 0.0))]) + uq = report(BenchmarkTableDefinition(((statistics, R, mean), (statistics, R, std)); bands=(:all,)), + (reference=(result=mc, metadata=metadata), result=(result=lep, metadata=metadata));observation_options=(length_unit=:base,)) + for (artifact, request, expected) in ((deterministic, (R,), r), + (uq, ((statistics, R, mean),), r), + (uq, ((statistics, R, std),), sqrt(2) * 0.1 .* r)) + page = LineCableModels.plot(artifact; ydata=request, backend=:cairo, + display_plot=false, controls=true, open_export=false, fig_size=(1100, 750)) + Makie.colorbuffer(page.figure) + axis = last(page.axes) + @test length(axis.xaxis.tickvalues[]) >= 8 + page.controls[:xlog].active[] = false + Makie.colorbuffer(page.figure) + # Test what Makie rendered, not a formatter evaluated on invented ticks. + @test 5 <= length(axis.xaxis.tickvalues[]) <= 10 + @test occursin("6", repr(axis.xlabel[])) + @test parse.(Float64, axis.xaxis.ticklabels[]) .* 1e6 ≈ axis.xaxis.tickvalues[] + for ((i, j), panel) in pairs(page.plot_state.panel_data) + for line in filter(p -> p isa Makie.Lines, panel.axis.scene.plots) + @test first.(line[1][]) ≈ f + @test last.(line[1][]) ≈ expected[i, j, :] + end + end + xlims!(axis, 1e6, 4e6) + ylims!(axis, 0.004, 0.012) + axis.xticks[] = [1e6, 3e6] + requested = axis.limits[] + for _ in 1:2 + page.controls[:xlog].active[] = true + page.controls[:xlog].active[] = false + Makie.colorbuffer(page.figure) + @test axis.limits[] == requested + @test axis.xticks[] == [1e6, 3e6] + @test axis.finallimits[].origin[2] ≈ 0.004 + @test axis.finallimits[].widths[2] ≈ 0.008 + end + axis.xticks[] = Makie.automatic + page.controls[:xlog].active[] = true + xlims!(axis, 2.0, 8.0) + Makie.colorbuffer(page.figure) + @test length(axis.xaxis.tickvalues[]) >= 2 + @test all(x -> 2 <= x <= 8, axis.xaxis.tickvalues[]) + @test !occursin("× 10", repr(axis.xlabel[])) + # A sub-decade LogTicks locator, like PseudologTicks, is specific to its + # transform. It must not leak into native linear tick conversion. + page.controls[:xlog].active[] = false + @test length(axis.xaxis.tickvalues[]) >= 2 + end + @test R(core) == r + @test frequencies(core) == f + @test isequal(observe(lep, statistics, R, std, 1), sqrt(2) * 0.1 .* r) +end + +@testitem "Makie addons / live native overrides and requested limits survive" tags=[:visual, :makie] begin + using CairoMakie + page = LineCableModels.plotwindow(; title="Live overrides", backend=:cairo, + display_plot=false, controls=true, open_export=false) do layout + axis = Axis(layout[1, 1]; xlabel="Frequency [Hz]", ylabel="Response", + limits=((1e6, 4e6), (nothing, 0.8))) + lines!(axis, [0.1, 1e7], [0.0, 1.0]) + end + axis = only(page.axes) + Makie.colorbuffer(page.figure) + @test axis.limits[] == ((1e6, 4e6), (nothing, 0.8)) + @test axis.finallimits[].origin[1] ≈ 1e6 + @test sum((axis.finallimits[].origin[2], axis.finallimits[].widths[2])) ≈ 0.8 + # A labelled tuple must release the managed formatter BEFORE native tick + # conversion sees the new value. A late observer cannot repair that error. + labelled = ([1e6, 3e6], ["low", "high"]) + axis.xticks[] = labelled + Makie.colorbuffer(page.figure) + @test axis.xaxis.ticklabels[] == labelled[2] + @test axis.xlabel[] == "Frequency [Hz]" + axis.xticks[] = (lo, hi) -> labelled + Makie.colorbuffer(page.figure) + @test axis.xaxis.ticklabels[] == labelled[2] + @test axis.xlabel[] == "Frequency [Hz]" + axis.xticks[] = Makie.automatic + axis.xtickformat[] = "{:.1f}" + @test axis.xlabel[] == "Frequency [Hz]" + axis.xtickformat[] = Makie.automatic + @test occursin("× 10", repr(axis.xlabel[])) + page.controls[:reset].clicks[] += 1 + @test axis.limits[] == ((1e6, 4e6), (nothing, 0.8)) +end + +@testitem "Makie addons / scale changes validate the page and preserve orthogonal views" tags=[:visual, :makie, :slow] begin + using CairoMakie + using LineCableModels.ReportBuilder: BenchmarkTableDefinition + f = [1.0, 10.0, 100.0] + z = [complex(i+j+k, k) for i in 1:2, j in 1:2, k in 1:3] + data = LineParameters(z, z .* 1e-6, f) + page = LineCableModels.plot(data; ydata=(R,), backend=:cairo, display_plot=false, + controls=true, open_export=false, length_unit=:base, xscale=:linear) + first_axis, last_axis = first(page.axes), last(page.axes) + # Model a native pan and zoom: target limits change, the stored request does not. + first_axis.targetlimits[] = Makie.Rect2d(2.0, 3.0, 30.0, 2.0) + page.controls[:xlog].active[] = true + @test first_axis.limits[] == (nothing, nothing) + @test first_axis.targetlimits[].origin[2] == 3.0 + @test first_axis.targetlimits[].widths[2] == 2.0 + page.controls[:xlog].active[] = false + xlims!(last_axis, -1.0, 100.0) + old_views = [axis.targetlimits[] for axis in page.axes] + # The invalid request is on the LAST axis: validating only while applying + # changes would leave the earlier axes switched when the exception is raised. + @test_throws DomainError axisscale!(page,:x,log10) + @test !page.controls[:xlog].active[] + @test all(axis -> axis.xscale[] === identity, page.axes) + @test [axis.targetlimits[] for axis in page.axes] == old_views + page.controls[:xlog].active[] = true + @test all(axis -> axis.xscale[]===log10,page.axes[1:end-1]) + @test last_axis.xscale[] isa Makie.ReversibleScale + @test last_axis.limits[][1]==(-1.0,100.0) + page.controls[:xlog].active[] = false + autolimits!(last_axis) + page.controls[:xlog].active[] = true + page.controls[:xlog].active[] = false + first_axis.limits[] = ((5.0, nothing), (nothing, 10.0)) + page.controls[:reset].clicks[] += 1 + @test first_axis.targetlimits[].origin[1] == 5.0 + @test sum((first_axis.targetlimits[].origin[2], first_axis.targetlimits[].widths[2])) ≈ 10.0 + @test Z(data) == z + + negative = LineParameters(z, fill(-1e-6 + 1e-6im, 2, 2, 3), f) + operand = (result=negative, + metadata=(port_order=["a", "b"], formulation=NamedTuple(Formulation()), axes=nothing)) + comparison = report(BenchmarkTableDefinition((G,)), (reference=operand, result=operand)) + signed = LineCableModels.plot(comparison; ydata=(G,), backend=:cairo, + display_plot=false, controls=true, open_export=false, yscale=:pseudolog10) + @test signed.controls[:ylog].active[] + signed.controls[:ylog].active[] = false + signed.controls[:ylog].active[] = true + @test all(axis -> axis.yscale[](1e-18) > 0 && + axis.yscale[](-1e-18) < 0, signed.axes) +end + +@testitem "Makie addons / native axis density responds to size without losing precision" tags=[:visual, :makie] begin + using CairoMakie + page = LineCableModels.plotwindow(; title="Numeric ranges", backend=:cairo, + display_plot=false, controls=false, open_export=false, size=(1200, 600)) do layout + axis = Axis(layout[1, 1]; xlabel="Input", ylabel="Output") + lines!(axis, [0.1, 1e7], [1e8, 1e8 + 2]) + end + axis = only(page.axes) + Makie.colorbuffer(page.figure) + large_count = length(axis.xaxis.tickvalues[]) + @test large_count >= 6 + resize!(page.figure, 600, 400) + Makie.colorbuffer(page.figure) + @test 3 <= length(axis.xaxis.tickvalues[]) < large_count + # Subnormal mantissa formatting is exercised below, independently of Makie's + # camera: its reciprocal viewport scale cannot represent a subnormal span. + for bounds in ((-2e-18, 2e-18), (1e8, 1e8 + 2), (-2e300, 2e300), + (-2e-300, 2e-300)) + xlims!(axis, bounds...) + Makie.colorbuffer(page.figure) + @test length(axis.xaxis.ticklabels[]) >= 2 + @test allunique(axis.xaxis.ticklabels[]) + @test all(label -> !occursin(r"[eE]", label), axis.xaxis.ticklabels[]) + @test all(point -> all(isfinite, point), axis.xaxis.tickpositions[]) + end + xlims!(axis, 1e8, 1e8 + 2) + axis.xticklabelsize[] = 20 + Makie.colorbuffer(page.figure) + @test allunique(axis.xaxis.ticklabels[]) + # Unchanged native layout notifications must not restart automatic density + # fitting and propagate temporary tick and protrusion changes to peer axes. + notify(axis.scene.viewport) + tick_updates=Ref(0) + binding=on(axis.xticks) do _ + tick_updates[]+=1 + end + native_ticks=axis.xticks[] + for _ in 1:8 + notify(axis.scene.viewport) + end + @test tick_updates[]==0 + @test axis.xticks[]===native_ticks + off(binding) + before = (axis.limits[], axis.targetlimits[], repr(axis.xlabel[]), copy(axis.xaxis.ticklabels[])) + mktempdir() do directory + for theme in (:default, :publication) + export_svg(page; path=joinpath(directory,"$theme.svg"), theme, open_file=false) + Makie.colorbuffer(page.figure) + @test (axis.limits[], axis.targetlimits[], repr(axis.xlabel[]), axis.xaxis.ticklabels[]) == before + end + end +end + +@testitem "Makie addons / plain mantissas and relative constant limits" tags=[:visual, :makie] begin + using CairoMakie + using Measurements: measurement + extension = Base.get_extension(LineCableModels, :LineCableModelsMakieExt) + for exponent in (-300, -14, -3, 0, 8, 300) + format = extension._linear_tickformat(exponent) + @test format([-1.0, 0.0, 1.0] .* 10.0^exponent) == ["-1", "0", "1"] + @test format([-0.0, 0.25, 0.5] .* 10.0^exponent) == ["0", "0.25", "0.5"] + end + @test extension._linear_tickformat(-324)([0.0, nextfloat(0.0)]) == + ["0", "4.941"] + format = extension._linear_tickformat(8) + ticks = [1e8, 1e8 + 1, 1e8 + 2] + @test length(unique(format(ticks))) == 3 + @test all(!occursin(r"[eE]", label) for label in format(ticks)) + + for (value, bounds) in ( + (0.0, (-1.0, 1.0)), + (1e-14, (0.95e-14, 1.05e-14)), + (-1e-14, (-1.05e-14, -0.95e-14)), + (measurement(0.0, 1e-14), (-2e-14, 2e-14)), + (measurement(1e-14, 2e-15), (6e-15, 1.4e-14)), + ) + resistance = fill(value, 1, 1, 3) + parameters = LineParameters(complex.(resistance, one.(resistance)), + fill(1.0im, 1, 1, 3), [1.0, 10.0, 100.0]) + plot = Makie.plot(parameters, R; backend=:cairo, display_plot=false, + controls=false, length_unit=:base, quantity_units=:base, clip=false) + axis = only(plot.axes) + Makie.colorbuffer(plot.figure) + @test axis.finallimits[].origin[2] ≈ bounds[1] + @test sum((axis.finallimits[].origin[2], axis.finallimits[].widths[2])) ≈ bounds[2] + end +end + +@testitem "Makie addons / native conversions and visible uncertainty retain ownership" tags=[:visual, :makie] begin + using CairoMakie, Dates, Measurements + for input in (Makie.Categorical(["first", "second", "third"]), + [Date(2026, 1, 1), Date(2026, 2, 1), Date(2026, 3, 1)]) + page = LineCableModels.plotwindow(; title="Native conversion", backend=:cairo, + display_plot=false, controls=true, open_export=false) do layout + axis = Axis(layout[1, 1]; xlabel="Native coordinate") + scatter!(axis, input, [1.0, 2.0, 3.0]) + end + axis = only(page.axes) + Makie.colorbuffer(page.figure) + before = copy(axis.xaxis.ticklabels[]) + page.controls[:reset].clicks[] += 1 + Makie.colorbuffer(page.figure) + @test axis.xlabel[] == "Native coordinate" + @test !isempty(before) + @test axis.xaxis.ticklabels[] == before + end + + f = [1.0, 10.0, 100.0] + z = fill(measurement(1e-14, 4e-15) + im * measurement(1e-14, 0.0), 1, 1, 3) + data = LineParameters(z, z, f) + page = LineCableModels.plot(data; ydata=(R,), backend=:cairo, display_plot=false, + controls=true, open_export=false, length_unit=:base, clip=false) + axis = only(page.axes) + Makie.colorbuffer(page.figure) + with_spread = axis.finallimits[].widths[2] + errorbars = filter(plot -> plot isa Makie.Errorbars, axis.scene.plots) + @test !isempty(errorbars) + foreach(plot -> plot.visible[] = false, errorbars) + Makie.colorbuffer(page.figure) + @test 0 < axis.finallimits[].widths[2] < with_spread + # A caller's additional native series participates in fitting, unless the + # caller excludes it through the native per-dimension autolimits attribute. + added = lines!(axis, f, fill(1e-12, 3); yautolimits=true) + page.controls[:reset].clicks[] += 1 + Makie.colorbuffer(page.figure) + @test sum((axis.finallimits[].origin[2], axis.finallimits[].widths[2])) >= 1e-12 + added.yautolimits[] = false + page.controls[:reset].clicks[] += 1 + Makie.colorbuffer(page.figure) + @test axis.finallimits[].widths[2] < with_spread + @test isequal(Z(data), z) +end + +@testitem "Makie addons / scale controls and visibility keep multipliers synchronized" tags=[:visual, :makie] begin + using CairoMakie + frequency = [1.0, 10.0, 100.0] + small = LineParameters(fill(1e-14 + 1.0im, 1, 1, 3), fill(1.0im, 1, 1, 3), frequency) + large = LineParameters(fill(1e8 + 1.0im, 1, 1, 3), fill(1.0im, 1, 1, 3), frequency) + plot = Makie.plot((; small, large), R; backend=:cairo, display_plot=false, + controls=true, length_unit=:base, quantity_units=:base, clip=false) + axis = only(plot.axes) + Makie.colorbuffer(plot.figure) + @test axis.ytickformat[]([0.0, 1e8]) == ["0", "100"] + for item in filter(item -> item isa Makie.Lines, axis.scene.plots) + last(first(item[1][])) > 1.0 && (item.visible[] = false) + end + Makie.colorbuffer(plot.figure) + @test axis.finallimits[].widths[2] < 1e-12 + @test occursin("−15", repr(axis.ylabel[])) + for _ in 1:2 + plot.controls[:ylog].active[] = true + Makie.colorbuffer(plot.figure) + @test axis.yscale[] === log10 + # A constant's modest log view is value-labelled, with the same single + # engineering multiplier as other narrow numeric views. + @test length(findall("× 10",repr(axis.ylabel[]))) == 1 + @test all(label -> label isa AbstractString, axis.yaxis.ticklabels[]) + plot.controls[:ylog].active[] = false + Makie.colorbuffer(plot.figure) + @test axis.yscale[] === identity + @test axis.ytickformat[]([1e-14]) == ["10"] + @test length(findall("× 10", repr(axis.ylabel[]))) == 1 + end +end + +@testitem "Makie addons / scientific formatting respects native customization" tags=[:visual, :makie] begin + using CairoMakie + custom = values -> fill("custom", length(values)) + plot = LineCableModels.plotwindow(; title="Native customization", backend=:cairo, + display_plot=false, controls=false) do layout + for (column, attributes) in enumerate(( + (; ytickformat=custom), + (; yticks=([0.0, 1e8], ["low", "high"])), + (;), + )) + axis = Axis(layout[1, column]; ylabel="Response", attributes...) + lines!(axis, [1.0, 10.0], [1e7, 1e8]) + end + end + first_axis, labelled_axis, axis = plot.axes + Makie.colorbuffer(plot.figure) + @test first_axis.ytickformat[] === custom + @test first_axis.ylabel[] == "Response" + @test labelled_axis.yticks[] == ([0.0, 1e8], ["low", "high"]) + @test labelled_axis.ylabel[] == "Response" + @test occursin("× 10", repr(axis.ylabel[])) + axis.ytickformat[] = custom + @test axis.ylabel[] == "Response" + ylims!(axis, 1e6, 1e9) + Makie.colorbuffer(plot.figure) + @test axis.ytickformat[] === custom + axis.ytickformat[] = Makie.automatic + @test axis.ytickformat[]([0.0, 1e9]) == ["0", "1"] + @test occursin("× 10", repr(axis.ylabel[])) + for scale in (log10, sqrt) + axis.yscale[] = scale + Makie.colorbuffer(plot.figure) + @test !isempty(axis.yaxis.ticklabels[]) + @test axis.ylabel[] == "Response" + end + axis.yscale[] = identity + Makie.colorbuffer(plot.figure) + @test axis.ytickformat[]([0.0, 1e9]) == ["0", "1"] + @test length(findall("× 10", repr(axis.ylabel[]))) == 1 +end + +@testitem "Makie addons / previews and statistical plots share live multipliers" tags=[:visual, :makie, :slow] setup=[TestFixtures] begin + using CairoMakie, Measurements + copper = Material(kind=:conductor, rho=1.7241e-8) + design = @cable "axis-preview" begin + @terminal :core begin + core(copper; r=1e-3) + end + end + result = TestFixtures.cable_monte_carlo_result() + plots = ( + preview(design; backend=:cairo, display_plot=false, controls=false), + Makie.hist(result, R; backend=:cairo, display_plot=false, controls=false), + Makie.ecdfplot(result, R; backend=:cairo, display_plot=false, controls=false), + ) + for plot in plots + axis = only(plot.axes) + xlims!(axis, -2e-12, 2e-12) + ylims!(axis, -2e-12, 2e-12) + Makie.colorbuffer(plot.figure) + @test axis.xtickformat[]([-1e-12, 0.0, 1e-12]) == ["-1", "0", "1"] + @test axis.ytickformat[]([-1e-12, 0.0, 1e-12]) == ["-1", "0", "1"] + @test occursin("−12", repr(axis.xlabel[])) + @test occursin("−12", repr(axis.ylabel[])) + end +end + +@testitem "Makie addons / native plot windows share axis formatting" tags=[:visual, :makie] begin + using CairoMakie + + plot = LineCableModels.plotwindow(; title="Axis formatting", backend=:cairo, + display_plot=false, controls=false) do layout + axis = Axis(layout[1, 1]; xlabel="Time [s]", ylabel="Response") + lines!(axis, [0.0, 1e-12, 2e-12], [-2e8, 0.0, 2e8]) + end + axis = only(plot.axes) + Makie.colorbuffer(plot.figure) + @test axis.xtickformat[] isa Function + @test axis.ytickformat[] isa Function + if axis.xtickformat[] isa Function && axis.ytickformat[] isa Function + @test axis.xtickformat[]([0.0, 1e-12, 2e-12]) == ["0", "1", "2"] + @test axis.ytickformat[]([-2e8, 0.0, 2e8]) == ["-200", "0", "200"] + end +end diff --git a/test/integration/native_makie_golden.jl b/test/integration/native_makie_golden.jl new file mode 100644 index 000000000..08ec8ed7b --- /dev/null +++ b/test/integration/native_makie_golden.jl @@ -0,0 +1,31 @@ +@testitem "Test support / PNG decoding and asymmetric pixel controls" tags=[:visual, :makie, :slow] begin + using CairoMakie + include(joinpath(pkgdir(LineCableModels),"test/support/golden_fixtures.jl")) + using .GoldenFixtures + @test_throws ArgumentError scene("unknown-scene") + # Test-support controls for image orientation after saving and decoding, + # and for blank-frame detection. + # Owned plotting and export behavior is checked by the other visual items. + figure=Figure(size=(120,80),backgroundcolor=:white) + axis=Axis(figure[1,1];limits=(0,2,0,2)) + hidedecorations!(axis); hidespines!(axis) + scatter!(axis,[.25,1.75,.25,1.75],[.25,.25,1.75,1.75]; + color=[:red,:green,:blue,:black],markersize=[7,11,15,19]) + mktempdir() do directory + pixels=save_pixels(joinpath(directory,"first.png"),(;figure)) + repeat_pixels=save_pixels(joinpath(directory,"repeat.png"),(;figure)) + # Cairo's public colorbuffer already uses row and column image order. Check + # that convention directly. Never choose an orientation by comparison. + buffer=Makie.colorbuffer(figure;backend=CairoMakie,px_per_unit=1) + buffer_pixels=cat((round.(UInt8,255 .* channel.(buffer)) + for channel in (Makie.red,Makie.green,Makie.blue))...;dims=3) + @test eltype(pixels) === UInt8 + @test size(pixels)==(80,120,3) + @test size(buffer_pixels)==size(pixels) + @test buffer_pixels==pixels + @test pixel_error(pixels,repeat_pixels)==0 + @test pixel_error(pixels,reverse(pixels;dims=1))>0 + @test pixel_error(pixels,reverse(pixels;dims=2))>0 + @test pixel_error(pixels,fill(0xff,size(pixels)))>0 + end +end diff --git a/test/integration/native_makie_legends.jl b/test/integration/native_makie_legends.jl new file mode 100644 index 000000000..4d1b2326b --- /dev/null +++ b/test/integration/native_makie_legends.jl @@ -0,0 +1,696 @@ +@testitem "Makie addons / construction-time panel legends preserve source ownership" tags=[:visual, :makie, :slow] begin + using CairoMakie + + frequency = [1.0, 10.0, 100.0] + impedance = [complex(i + 2j + k, i + j + 2k) for i in 1:2, j in 1:2, k in 1:3] + admittance = impedance .* 1e-6 + reference = LineParameters(copy(impedance), copy(admittance), frequency) + result = LineParameters(2impedance, 2admittance, frequency) + sources = (; reference, result) + requests = ((R, 1, 1:2, :),) + options = (; backend = :cairo, display_plot = false, controls = false, + layout = (1, 2), panel_titles = ("Self resistance", "Mutual resistance"), + series_labels = ("baseline", "alternative"), legend_position = nothing, + legend_cap = 0.5, length_unit = :base, quantity_units = :base, + freq_unit = :base, clip = false) + + plot = Makie.plot(sources, requests; options..., + panel_legends = (1, 1) => :right) + @test Set(keys(plot.panel_legends)) == Set(((1, 1),)) + @test plot.panel_legends[(1, 1)] isa Makie.Legend + @test plot.plot_state.guides[(:legend, (1, 1))].position[] == :right + @test plot.panel_legends[(1, 1)].orientation[] == :vertical + + extension = Base.get_extension(LineCableModels, :LineCableModelsMakieExt) + # A malformed plot must fail instead of silently disappearing from a legend. + @test_throws ErrorException extension._panel_legend_data( + [(logical_position=(1, 1),)], [first(plot.axes)], + Dict(:broken => [nothing]), [:broken], Dict(:broken => "broken")) + + local_labels = ("local baseline", "local alternative") + inside = Makie.plot(sources, requests; options..., + panel_legends = ( + (1, 1) => (position = :inside, halign = :left, valign = :bottom, + title = "Local sources", legend_labels = local_labels, + max_fraction = 0.5, labelsize = 13), + (1, 2) => false + )) + legend = inside.panel_legends[(1, 1)] + @test Set(keys(inside.panel_legends)) == Set(((1, 1),)) + @test legend.halign[] == :left + @test legend.valign[] == :bottom + @test legend.labelsize[] == 13 + @test first(only(legend.entrygroups[])) == "Local sources" + @test Set(values(inside.plot_state.panel_data[(1, 1)].labels)) == Set(local_labels) + @test [entry.label[] for entry in last(only(legend.entrygroups[]))] == + collect(local_labels) + @test Set(values(inside.plot_state.labels)) == Set(("baseline", "alternative")) + @test Set(values(inside.plot_state.panel_data[(1, 2)].labels)) == + Set(("baseline", "alternative")) + + moved = panellegend!(inside, (1, 1); position = :bottom, max_fraction = 0.5) + @test moved === inside.panel_legends[(1, 1)] + @test moved === legend + @test moved.orientation[] == :vertical # native row-major grid + @test moved.nbanks[] >= 1 + @test first(only(moved.entrygroups[])) == "Local sources" + @test inside.plot_state.guides[(:legend, (1, 1))].position[] == :bottom + @test panellegend!(inside, (1, 1); position = nothing) === nothing + @test !haskey(inside.panel_legends, (1, 1)) + @test inside.plot_state.guides[(:legend, (1, 1))].position[]===nothing + + configured = Makie.plot(sources, requests; options..., + panel_legends = Dict( + (1, 1) => nothing, + (1, 2) => (position = :top, max_fraction = 0.5, + legend_labels = Dict("baseline"=>"renamed"), title = "Reactance sources") + )) + @test Set(keys(configured.panel_legends)) == Set(((1, 2),)) + @test configured.panel_legends[(1, 2)].orientation[] == :vertical + @test configured.panel_legends[(1, 2)].nbanks[] >= 1 + @test first(only(configured.panel_legends[(1, 2)].entrygroups[])) == + "Reactance sources" + @test Set(values(configured.plot_state.panel_data[(1, 2)].labels)) == + Set(("renamed", "alternative")) + @test [entry.label[] + for entry in last(only( + configured.panel_legends[(1, 2)].entrygroups[]))] == ["renamed", "alternative"] + @test Set(values(configured.plot_state.labels)) == Set(("baseline", "alternative")) + panellegend!(configured, (1, 2); position = :left, max_fraction = 0.5) + @test configured.plot_state.guides[(:legend, (1, 2))].position[] == :left + @test configured.panel_legends[(1, 2)].orientation[] == :vertical + + for rendered in (plot, inside, configured) + Makie.colorbuffer(rendered.figure) + @test [axis.title[] for axis in rendered.axes] == + ["Self resistance", "Mutual resistance"] + for (index, axis) in enumerate(rendered.axes) + curves = filter(item -> item isa Makie.Lines, axis.scene.plots) + @test length(curves) == 2 + expected = real.(impedance[1, index, :]) + @test last.(curves[1][1][]) ≈ expected + @test last.(curves[2][1][]) ≈ 2expected + @test first.(curves[1][1][]) ≈ frequency + end + end + @test Z(reference) == impedance + @test Y(reference) == admittance + @test Z(result) == 2impedance + @test Y(result) == 2admittance + + for requested in (42, ((0, 1) => :right), ((true, 1) => :right), + ((1, 1) => 42), ((1, 1) => (position = :inside, anchor = (:left,)))) + @test_throws ArgumentError Makie.plot(sources, requests; options..., + panel_legends = requested) + end + @test_throws ArgumentError Makie.plot(sources, requests; options..., + panel_legends = (2, 1) => :right) +end + +@testitem "Makie addons / independent scale arrangement and sibling spacing" tags=[:visual, :makie] setup=[TestFixtures] begin + using CairoMakie + design=TestFixtures.coaxial_design() + options=(backend = :cairo, display_plot = false, controls = false, size = (1200, 850), + colorbar_attributes = (vertical = false, width = 160, height = 14)) + @testset "placement chooses default cells independently of bar orientation" begin + p=preview(design; options..., legend_position = nothing, colorbar_position = :bottom) + Makie.colorbuffer(p.figure) + bounds=[bar.layoutobservables.computedbbox[] for bar in p.colorbars] + @test maximum(b.origin[2] for b in bounds)-minimum(b.origin[2] for b in bounds)<=1 + @test all(bounds[i + 1].origin[1]>bounds[i].origin[1]+bounds[i].widths[1] + for i in 1:2) + end + @testset "moving and direct construction converge" begin + p=preview(design; options..., legend_position = nothing, colorbar_position = :right) + bars=copy(p.colorbars) + figurecolorbars!(p; position = :bottom) + Makie.colorbuffer(p.figure) + @test all(a===b for (a, b) in zip(bars, p.colorbars)) + @test all(bar -> !bar.labelvisible[], bars) + bounds=[bar.layoutobservables.computedbbox[] for bar in bars] + @test maximum(b.origin[2] for b in bounds)-minimum(b.origin[2] for b in bounds)<=1 + end + @testset "complete sibling bounds have a minimum skip" begin + p=preview(design; options..., legend_position = :right, + legend_attributes = (valign = :center, margin = (0, 0, 0, 0)), colorbar_position = :right, + colorbar_group_attributes = (valign = :center, margin = (0, 0, 0, 0))) + Makie.colorbuffer(p.figure) + legend=p.legend.layoutobservables.computedbbox[] + scales=p.plot_state.guides[(:colorbars, nothing)].layout[].layoutobservables.computedbbox[] + skip=legend.origin[2]-(scales.origin[2]+scales.widths[2]) + @info "complete guide baseline" skip legend scales + @test skip>=11 + end +end + +@testitem "Makie addons / guide restoration retains native edits and common placement" tags=[:visual, :makie] setup=[TestFixtures] begin + using CairoMakie + design=TestFixtures.coaxial_design() + p=preview(design; backend = :cairo, display_plot = false, controls = false, + legend_position = :right, colorbar_position = :right) + legend=p.legend + bars=copy(p.colorbars) + legend.labelsize[]=17 + bars[1].labelsize[]=19 + bars[1].label[]="Edited native scale" + axis=only(p.axes) + Makie.limits!(axis, -0.04, 0.03, -0.02, 0.04) + view=axis.targetlimits[] + bars[2].blockscene.visible[]=false + for _ in 1:3 + @test figurelegend!(p; position = nothing)===nothing + @test isempty(figurecolorbars!(p; position = nothing)) + @test figurelegend!(p; position = :left)===legend + restored=figurecolorbars!(p; position = :left, + group_attributes = (valign = 0.25, margin = (2, 3, 4, 5))) + @test all(a===b for (a, b) in zip(restored, bars)) + @test !bars[2].blockscene.visible[] + @test bars[1].blockscene.visible[] + @test axis.targetlimits[]==view + @test legend.labelsize[]==17 + @test bars[1].labelsize[]==19 + @test bars[1].label[]=="Edited native scale" + @test length(p.plot_state.guide_docks)==1 + end + companion=filter(p.figure.content) do block + block isa Makie.Label&&block.text[]=="Edited native scale" + end + @test length(companion)==1 + @test only(companion).fontsize[]==19 + @test p.plot_state.shell.body.colsizes[3]==Makie.Fixed(0) + @test_throws ArgumentError figurecolorbars!(p; position = (2, 2)) + @test_throws ArgumentError paneltitle!(p, (9, 9), "absent") + @test axis.targetlimits[]==view + + raw=TestFixtures.two_conductor_results(; frequencies = [1.0, 10.0, 100.0]) + q=Makie.plot(raw, raw, raw, raw; ydata = ((R, 1, 1, :),), + series_labels = ("first complete description", "second complete description", + "third complete description", "fourth complete description"), + backend = :cairo, display_plot = false, controls = false, + legend_position = :right, fig_size = (1200, 700)) + original=q.legend + figurelegend!(q; position = :bottom) + @test q.legend===original + @test q.legend.orientation[]===:vertical + @test q.legend.nbanks[]>1 + @test length(last(only(q.legend.entrygroups[])))==4 + q.legend.nbanks[]=1 + figurelegend!(q; position = :top) + @test q.legend.nbanks[]==1 # an explicit native edit releases automatic wrapping + label=Makie.rich("native ", Makie.rich("label"; color = :red)) + figuretitle!(q, label) + @test q.title.text[]===label + paneltitle!(q, (1, 1), label) + @test only(q.axes).title[]===label +end + +@testitem "Makie addons / guide creation order shares complete scale geometry" tags=[:visual, :makie] setup=[TestFixtures] begin + using CairoMakie + design=TestFixtures.coaxial_design() + options=(backend = :cairo, display_plot = false, controls = false, size = (1000, 650), + legend_position = nothing, colorbar_position = nothing) + first_legend=preview(design; options...) + first_scale=preview(design; options...) + for (p, legend_first) in ((first_legend, true), (first_scale, false)) + if legend_first + figurelegend!(p; position = :right, valign = 0.5) + figurecolorbars!(p; position = :right, group_attributes = (valign = 0.5,)) + else + figurecolorbars!(p; position = :right, group_attributes = (valign = 0.5,)) + figurelegend!(p; position = :right, valign = 0.5) + end + @test length(p.colorbars)==3 + @test p.legend isa Makie.Legend + @test length(p.plot_state.guide_docks)==1 + @test all(isfinite, only(p.axes).targetlimits[].widths) + end + @test all(isapprox.(first_legend.figure.scene.viewport[].widths, + first_scale.figure.scene.viewport[].widths; atol = 1)) + @test all(isapprox.(only(first_legend.axes).scene.viewport[].widths, + only(first_scale.axes).scene.viewport[].widths; atol = 1)) + @test all(isapprox.(first_legend.legend.layoutobservables.computedbbox[].origin, + first_scale.legend.layoutobservables.computedbbox[].origin; atol = 1)) + hidden=preview(design; backend = :cairo, display_plot = false, + controls = false, display_colorbars = false) + @test isempty(hidden.colorbars) + @test length(figurecolorbars!(hidden; position = :bottom))==3 + figurelegend!(hidden; position = nothing) + figurecolorbars!(hidden; position = :right, group_attributes = (valign = 0.25,)) + group=hidden.plot_state.guides[(:colorbars, nothing)].layout[] + bounds=group.layoutobservables.computedbbox[] + frame=hidden.plot_state.inside_bbox[] + @test isapprox(bounds.origin[2], frame.origin[2]+0.25*(frame.widths[2]-bounds.widths[2]); atol = 2) +end + +@testitem "Makie addons / guide arrangement packing and option validation" tags=[:visual, :makie] begin + using CairoMakie + ext=Base.get_extension(LineCableModels, :LineCableModelsMakieExt) + for (extents, gap, span, fractions) in (([20.0, 30.0], 12.0, 62.0, [0.5, 0.5]), + ([20.0, 30.0], 12.0, 120.0, [0.25, 0.25]), ([20.0, 30.0], 0.0, 100.0, [0.0, 1.0]), + ([20.0], 25.0, 100.0, [0.5]), ([20.0, 30.0, 10.0], 7.5, 120.0, [1.0, 0.0, 0.5])) + result=ext._pack_guides(extents, span, gap, fractions) + @test result.extent==sum(extents)+(length(extents)-1)*gap + @test first(result.starts)>=0 + @test last(result.starts)+last(extents)<=result.span + @test all(result.starts[i + 1]>=result.starts[i]+extents[i]+gap + for i in 1:(length(extents) - 1)) + if all(==(first(fractions)), fractions) + @test first(result.starts)==first(fractions)*(span-result.extent) + end + end + @test ext._pack_guides(Float64[], 100.0, 12.0, Float64[]).extent==0 + @test ext._pack_guides([20.0, 30.0], 100.0, 12.0, [0.0, 1.0]).starts==[0.0, 70.0] + @test ext._pack_guides([20.0, 30.0], 30.0, 12.0, [0.0, 1.0]).span==62 + @test ext._pack_guides([20.0, 30.0, 10.0], 200.0, 15.0, fill(0.5, 3)).extent - + ext._pack_guides([20.0, 30.0, 10.0], 200.0, 5.0, fill(0.5, 3)).extent==20 + @test ext._guide_spacing((rowgap = 2.5,))==(rowgap = 2.5, colgap = 12.0) + @test ext._guide_spacing((colgap = 3.5,), (rowgap = 2.5, colgap = 8.0))==( + rowgap = 2.5, colgap = 3.5) + for value in (true, -1, Inf, NaN, (rowgap = true,), (rowgap = -1,), (colgap = Inf,), (extra = 1,)) + @test_throws ArgumentError ext._guide_spacing(value) + end + for value in ((layout = (true, 3),), (layout = (1, 2),), (layout = (0, 3),), (rowgap = NaN,), + (colgap = -1,), (rowgap = true,), (layout = :horizontal,), (horizontal_strip = true,)) + @test_throws ArgumentError ext._colorbar_group_attributes(value, 3) + end +end + +@testitem "Makie addons / guide arrangement reflows retained complete items" tags=[:visual, :makie, :slow] setup=[TestFixtures] begin + using CairoMakie + ext=Base.get_extension(LineCableModels, :LineCableModelsMakieExt) + design=TestFixtures.coaxial_design() + options=(backend = :cairo, display_plot = false, controls = false, size = (1400, 950), + legend_position = nothing, colorbar_attributes = ( + vertical = false, width = 160, height = 14), + colorbar_group_attributes = ( + layout = (1, 3), rowgap = 12.5, colgap = 16.5, margin = (0, 0, 0, 0))) + p=preview(design; options..., colorbar_position = :bottom, guide_spacing = ( + rowgap = 14, colgap = 18)) + direct=preview(design; options..., colorbar_position = :bottom) + guide=p.plot_state.guides[(:colorbars, nothing)] + bars=copy(p.colorbars) + companions=[item.companion for item in guide.items] + plots=copy(only(p.axes).scene.plots) + Makie.limits!(only(p.axes), -0.03, 0.025, -0.025, 0.03) + view=only(p.axes).targetlimits[] + frame=only(p.axes).layoutobservables.computedbbox[].widths + listeners=length(guide.subscriptions) + coordinates() = [Tuple(Makie.GridLayoutBase.gridcontent(item.layout).span.rows)[1]=> + Tuple(Makie.GridLayoutBase.gridcontent(item.layout).span.cols)[1] + for item in guide.items if item.visible[]] + for position in (:right, :bottom, :top, :left, :bottom) + figurecolorbars!(p; position) + Makie.colorbuffer(p.figure) + @test size(guide.layout[])==(1, 3) + @test coordinates()==[1=>1, 1=>2, 1=>3] + @test all(a===b for (a, b) in zip(bars, p.colorbars)) + @test all(a===b + for (a, b) in zip(companions, [item.companion for item in guide.items])) + @test only(p.axes).targetlimits[]==view + @test all(isapprox.(only(p.axes).layoutobservables.computedbbox[].widths, frame; atol = 1)) + @test only(p.axes).scene.plots==plots + @test all(bar -> bar.width[]==160 && bar.height[]==14 && !bar.vertical[], bars) + @test length(guide.subscriptions)==listeners + end + # Compare local geometry. Independent windows retain their own frame sizes. + Makie.colorbuffer(direct.figure) + function local_boxes(q) + g=q.plot_state.guides[(:colorbars, nothing)] + origin=g.layout[].layoutobservables.computedbbox[].origin + [(Tuple(item.layout.layoutobservables.computedbbox[].origin-origin), + Tuple(item.layout.layoutobservables.computedbbox[].widths)) + for item in g.items] + end + @test all(all(isapprox.(a[1], b[1]; atol = 1)) && all(isapprox.(a[2], b[2]; atol = 1)) + for (a, b) in zip(local_boxes(p), local_boxes(direct))) + @test all(bar -> !bar.labelvisible[], bars) + @test all(item -> item.companion.blockscene.visible[], guide.items) + figurecolorbars!(p; position = :right, group_attributes = (layout = nothing,)) + @test size(guide.layout[])==(3, 1) + figurecolorbars!(p; position = :bottom) + @test size(guide.layout[])==(1, 3) + figurecolorbars!(p; group_attributes = (layout = (2, 5),)) + @test size(guide.layout[])==(1, 3) # spare guide capacity is not a matrix domain + figurecolorbars!(p; group_attributes = (layout = (3, 1),), vertical = true) + @test size(guide.layout[])==(3, 1) + @test all(bar -> bar.vertical[] && bar.width[]==160 && bar.height[]==14, bars) + figurecolorbars!(p; group_attributes = (layout = (1, 3),), vertical = false) + bars[1].label[]="A deliberately long retained material-property label" + bars[1].labelsize[]=23 + bars[1].labelcolor[]=:red + bars[2].blockscene.visible[]=false + Makie.colorbuffer(p.figure) + @test size(guide.layout[])==(1, 2) + @test !guide.items[2].companion.blockscene.visible[] + @test Makie.GridLayoutBase.gridcontent(guide.items[2].layout).parent===nothing + @test length(p.colorbars)==3 + for position in (nothing, :right, :bottom) + figurecolorbars!(p; position) + @test !bars[2].blockscene.visible[] + @test !guide.items[2].companion.blockscene.visible[] + end + bars[2].blockscene.visible[]=true + @test coordinates()==[1=>1, 1=>2, 1=>3] + @test bars[1].label[]=="A deliberately long retained material-property label" + @test bars[1].labelsize[]==23 + @test Makie.to_color(bars[1].labelcolor[])==Makie.to_color(:red) + @test length(guide.subscriptions)==listeners + @test only(p.axes).targetlimits[]==view + + before=(position = guide.position[], group = p.plot_state.colorbar_group_attributes[], + spacing = p.plot_state.guide_spacing[], size = p.figure.scene.viewport[], + attributes = guide.attributes[], boxes = local_boxes(p)) + for invalid in ((group_attributes = (layout = (1, 2),),), (guide_spacing = -1,), + (group_attributes = (bogus = 1,),), (group_attributes = (colgap = true,),), + (vertical = :horizontal,), (width = NaN,), (position = (2, 2),), (unknown_attribute = 1,)) + @test_throws ArgumentError figurecolorbars!(p; position = :left, invalid...) + @test guide.position[]==before.position + @test p.plot_state.colorbar_group_attributes[]==before.group + @test p.plot_state.guide_spacing[]==before.spacing + @test p.figure.scene.viewport[]==before.size + @test guide.attributes[]==before.attributes + @test local_boxes(p)==before.boxes + end + figurecolorbars!(p; group_attributes = (rowgap = nothing, colgap = nothing), + guide_spacing = (colgap = 19.5,)) + @test p.plot_state.guide_spacing[]==(rowgap = 14.0, colgap = 19.5) + @test guide.layout[].default_colgap==Makie.Fixed(19.5) + figurelegend!(p; guide_spacing = (rowgap = 15.5,)) + @test p.plot_state.guide_spacing[]==(rowgap = 15.5, colgap = 19.5) + @test guide.layout[].default_rowgap==Makie.Fixed(15.5) + # Structural reflow must not replay constructor appearance over native edits. + guide.layout[].halign[]=:left + guide.layout[].alignmode[]=Makie.Outside(3, 4, 5, 6) + appearance=guide.layout[].alignmode[] + figurecolorbars!(p; group_attributes = (layout = (1, 3),)) + figurelegend!(p; guide_spacing = (rowgap = 15.5,)) + @test guide.layout[].halign[].x==0 + @test guide.layout[].alignmode[]==appearance + geometry=local_boxes(p) + previous_group=p.plot_state.colorbar_group_attributes[] + previous_size=p.figure.scene.viewport[] + @test_throws ArgumentError figurecolorbars!(p; group_attributes = (width = 1,)) + @test p.plot_state.colorbar_group_attributes[]==previous_group + @test p.figure.scene.viewport[]==previous_size + @test local_boxes(p)==geometry + tickformat=bars[1].tickformat[] + ticks=[bar.ticks[] for bar in bars] + @test_throws ErrorException figurecolorbars!(p; position = :right, + ticks = [1.0, 2.0], + tickformat = values -> error("injected native tick formatter failure")) + @test all(bar -> bar.tickformat[]===tickformat, bars) + @test [bar.ticks[] for bar in bars]==ticks + @test guide.position[]===:bottom + @test only(p.axes).targetlimits[]==view + @test !isempty(Makie.colorbuffer(p.figure)) +end + +@testitem "Makie addons / guide arrangement measures visible scales and independent gaps" tags=[:visual, :makie, :slow] setup=[TestFixtures] begin + using CairoMakie + design=TestFixtures.coaxial_design() + options=(backend = :cairo, display_plot = false, controls = false, size = (1500, 1100), + colorbar_attributes = (vertical = false, width = 160, height = 14)) + p=preview(design; options..., legend_position = :bottom, + legend_attributes = (halign = 0.25, margin = (0, 0, 0, 0)), + colorbar_position = :bottom, colorbar_group_attributes = ( + layout = (1, 3), colgap = 16.5, halign = 0.25, margin = (0, 0, 0, 0)), + guide_spacing = (rowgap = 14.5, colgap = 18.5), guide_gap = 8) + guide=p.plot_state.guides[(:colorbars, nothing)] + # Native block scenes use the figure's logical pixels as their data coordinates. + # Measure rendered glyphs, including native endpoint text, not only allocations. + function visible_bounds(item) + boxes=[Makie.boundingbox(item.bar.blockscene, plot->!plot.visible[], :data)] + item.companion.blockscene.visible[]&&push!(boxes, + Makie.boundingbox(item.companion.blockscene, plot->!plot.visible[], :data)) + low=ntuple(i->minimum(box->box.origin[i], boxes), 2) + high=ntuple(i->maximum(box->box.origin[i]+box.widths[i], boxes), 2) + (; low, high) + end + function check_items(p, dimension, minimum_gap) + Makie.colorbuffer(p.figure) + g=p.plot_state.guides[(:colorbars, nothing)] + active=filter(item->item.visible[], g.items) + bounds=visible_bounds.(active) + for (item, box) in zip(active, bounds) + reserved=item.layout.layoutobservables.computedbbox[] + @test all(box.low[i]>=reserved.origin[i]-1 for i in 1:2) + @test all(box.high[i]<=reserved.origin[i]+reserved.widths[i]+1 for i in 1:2) + end + for i in 1:(length(bounds) - 1) + gap=dimension==1 ? bounds[i + 1].low[1]-bounds[i].high[1] : + bounds[i].low[2]-bounds[i + 1].high[2] + @test gap>=minimum_gap-1 + end + bounds + end + boxes=check_items(p, 1, 16.5) + # With identical font sizes these horizontal items need only their visible + # height, not a second copy of the native tick and property-label protrusion. + @test first(guide.items).layout.layoutobservables.computedbbox[].widths[2] <= + first(boxes).high[2]-first(boxes).low[2]+3 + legend=p.legend.layoutobservables.computedbbox[] + @test minimum(b -> b.low[1], boxes)-(legend.origin[1]+legend.widths[1])>=17.5 + @test p.plot_state.guide_gap==(8.0, 8.0, 8.0, 8.0) + dimensions=[(bar.width[], bar.height[]) for bar in p.colorbars] + inner=guide.layout[].addedcolgaps|>copy + figurelegend!(p; guide_spacing = (colgap = 30.5,)) + @test guide.layout[].addedcolgaps==inner + @test [(bar.width[], bar.height[]) for bar in p.colorbars]==dimensions + boxes=check_items(p, 1, 16.5) + legend=p.legend.layoutobservables.computedbbox[] + @test minimum(b -> b.low[1], boxes)-(legend.origin[1]+legend.widths[1])>=29.5 + p.colorbars[2].label[]="A much longer physical scale caption" + p.colorbars[2].labelsize[]=25 + check_items(p, 1, 16.5) + p.colorbars[1].spinewidth[]=4 + check_items(p, 1, 16.5) + figurecolorbars!(p; position = :right, group_attributes = ( + layout = (3, 1), rowgap = 12.5, valign = 0.25)) + figurelegend!(p; position = :right, valign = 0.25, guide_spacing = (rowgap = 14.5,)) + boxes=check_items(p, 2, 12.5) + legend=p.legend.layoutobservables.computedbbox[] + @test legend.origin[2]-maximum(b -> b.high[2], boxes)>=13.5 + for position in (nothing, :right) + figurelegend!(p; position) + check_items(p, 2, 12.5) + end + # Zero or one visible outer sibling has no spacing contribution. + figurecolorbars!(p; position = nothing) + Makie.colorbuffer(p.figure) + size_before=p.figure.scene.viewport[].widths + legend_before=p.legend.layoutobservables.computedbbox[] + figurelegend!(p; guide_spacing = 0) + figurelegend!(p; guide_spacing = 70) + Makie.colorbuffer(p.figure) + @test all(isapprox.(size_before, p.figure.scene.viewport[].widths; atol = 1)) + @test all(isapprox.(legend_before.widths, p.legend.layoutobservables.computedbbox[].widths; atol = 1)) + @test p.legend.margin[]==(0, 0, 0, 0) + @test p.plot_state.guide_gap==(8.0, 8.0, 8.0, 8.0) + # Orientation changes the native bar, never its sibling cells. Measure + # vertical bars in both arrangements as well as the horizontal cases above. + figurelegend!(p; position = nothing) + guide.items[2].bar.label[]="Vertical scale" + for (layout, dimension, gap) in (((1, 3), 1, 16.5), ((3, 1), 2, 12.5)) + figurecolorbars!(p; position = :right, group_attributes = (; layout), + vertical = true, width = 14, height = 160) + @test size(guide.layout[])==layout + check_items(p, dimension, gap) + end +end + +@testitem "Makie addons / scale bars share edges and baselines independently of labels" tags=[:visual, :makie] setup=[TestFixtures] begin + using CairoMakie + design=TestFixtures.coaxial_design() + options=(backend = :cairo, display_plot = false, controls = false, + colorbar_attributes = (vertical = false, width = 160, height = 14)) + p=preview(design; options..., colorbar_position = :right) + bars=copy(p.colorbars) + guide=p.plot_state.guides[(:colorbars, nothing)] + views=[axis.targetlimits[] for axis in p.axes] + listeners=length(guide.subscriptions) + function aligned(p, dimension, indices = eachindex(p.colorbars)) + Makie.colorbuffer(p.figure) + bounds=[p.colorbars[i].layoutobservables.computedbbox[] + for i in indices if p.colorbars[i].blockscene.visible[]] + for edge in (box->box.origin[dimension], box->box.origin[dimension]+box.widths[dimension]) + values=edge.(bounds) + @test maximum(values)-minimum(values)<=1 + end + end + aligned(p, 1) + function local_origins(p) + Makie.colorbuffer(p.figure) + group=p.plot_state.guides[(:colorbars, nothing)].layout[] + [bar.layoutobservables.computedbbox[].origin-group.layoutobservables.computedbbox[].origin + for bar in p.colorbars] + end + right=local_origins(p) + direct=preview(design; options..., colorbar_position = :bottom, + colorbar_group_attributes = (layout = (1, 3),)) + figurecolorbars!(p; position = :bottom, group_attributes = (layout = (1, 3),)) + @test all(all(isapprox.(a, b; atol = 1)) + for (a, b) in zip(local_origins(p), local_origins(direct))) + figurecolorbars!(p; position = :right, group_attributes = (layout = (3, 1),)) + @test all(all(isapprox.(a, b; atol = 1)) for (a, b) in zip(right, local_origins(p))) + # Native caption and endpoint font changes must reserve space without + # independently recentering the unequal decorated bar footprints. + bars[1].labelsize[]=26 + bars[1].labelfont[]=:bold + bars[2].ticklabelsize[]=23 + bars[2].ticklabelfont[]=:bold + positions=first(bars[3].ticks[]) + bars[3].ticks[]=(positions, ["long endpoint $(value)" for value in positions]) + aligned(p, 1) + for halign in (:left, :center, :right) + figurecolorbars!(p; group_attributes = (; halign)) + aligned(p, 1) + end + bars[2].blockscene.visible[]=false + aligned(p, 1) + bars[2].blockscene.visible[]=true + aligned(p, 1) + for position in (:bottom, :right, :bottom) + figurecolorbars!(p; position, group_attributes = (layout = position===:bottom ? + (1, 3) : (3, 1),)) + aligned(p, position===:bottom ? 2 : 1) + end + figurecolorbars!(p; group_attributes = (layout = (2, 2),)) + aligned(p, 1, (1, 3)) + aligned(p, 2, (1, 2)) + @test all(a===b for (a, b) in zip(bars, p.colorbars)) + @test [axis.targetlimits[] for axis in p.axes]==views + @test length(guide.subscriptions)==listeners +end + +@testitem "Makie addons / guide arrangement membership native edits and export" tags=[:visual, :makie, :slow] setup=[TestFixtures] begin + using CairoMakie + ext=Base.get_extension(LineCableModels, :LineCableModelsMakieExt) + design=TestFixtures.coaxial_design() + options=(backend = :cairo, display_plot = false, controls = false, + size = (1400, 1000), open_export = false) + hidden=preview(design; options..., colorbar_position = nothing) + original_content=copy(hidden.figure.content) + original_scenes=copy(hidden.figure.scene.children) + original_size=hidden.figure.scene.viewport[] + @test_throws ErrorException figurecolorbars!( + hidden; position = :bottom, ticks = [1.0, 2.0], + tickformat = values -> error("injected native constructor failure")) + @test hidden.figure.content==original_content + @test hidden.figure.scene.children==original_scenes + @test hidden.figure.scene.viewport[]==original_size + @test hidden.plot_state.guides[(:colorbars, nothing)].position[]===nothing + @test isempty(hidden.colorbars) + @test length(figurecolorbars!(hidden; position = :bottom))==3 + for count in 0:3 + p=preview(design; options..., legend_position = nothing, colorbar_position = nothing) + p.plot_state=merge(p.plot_state, (color_scales = p.plot_state.color_scales[1:count],)) + figurecolorbars!(p; position = :bottom, group_attributes = (layout = (2, 4),), guide_spacing = 12.5) + Makie.colorbuffer(p.figure) + @test length(p.colorbars)==count + guide=p.plot_state.guides[(:colorbars, nothing)] + if count==0 + @test isempty(p.plot_state.guide_docks) + else + @test size(guide.layout[])==(1, count) + for bar in p.colorbars + bar.blockscene.visible[]=false + end + @test isempty(p.plot_state.guide_docks) + @test length(p.colorbars)==count + @test all(item -> !item.companion.blockscene.visible[], guide.items) + figurecolorbars!(p; position = nothing) + figurecolorbars!(p; position = :right) + @test length(p.colorbars)==count + @test isempty(p.plot_state.guide_docks) + foreach(bar->bar.blockscene.visible[]=true, p.colorbars) + @test size(guide.layout[])==(1, count) + end + end + p=preview(design; options..., legend_position = :right, colorbar_position = :bottom, + colorbar_group_attributes = (layout = (1, 3), colgap = 16.5), guide_spacing = 14.5) + guide=p.plot_state.guides[(:colorbars, nothing)] + bars=copy(p.colorbars) + bars[1].width[]=213.5 + bars[1].vertical[]=true + @test size(guide.layout[])==(1, 3) + @test bars[1].width[]==213.5 + @test bars[1].height[]==ext._COLORBAR_DOCK_LENGTH + bars[1].vertical[]=false + @test bars[1].width[]==213.5 + @test bars[1].height[]==Makie.Auto() + @test isapprox(bars[1].layoutobservables.computedbbox[].widths[2], bars[1].size[]; atol = 1) + bars[2].blockscene.visible[]=false + bars[3].label[]="Exported scale caption" + bars[3].labelsize[]=21 + Makie.limits!(only(p.axes), -0.024, 0.036, -0.025, 0.035) + view=only(p.axes).targetlimits[] + native=(size = p.figure.scene.viewport[], labels = [bar.label[] for bar in bars], + fonts = [bar.labelfont[] for bar in bars], labelsize = [bar.labelsize[] + for bar in bars], + visibility = [item.visible[] for item in guide.items], + scenes = [bar.blockscene.visible[] for bar in bars], subscriptions = length(guide.subscriptions)) + mktempdir() do directory + for theme in (:default, :publication) + path=export_svg(p; path = joinpath(directory, string(theme)*".svg"), theme, open_file = false) + @test isfile(path) + @test occursin(" !plot.visible[], :data) + for bar in bars if bar.blockscene.visible[]] + @test length(boxes)==2 + @test boxes[2].origin[1]-(boxes[1].origin[1]+boxes[1].widths[1])>=15.5 + error("injected SVG writer failure") + end + @test p.figure.scene.viewport[]==native.size + @test only(p.axes).targetlimits[]==view + @test [bar.label[] for bar in bars]==native.labels + @test [bar.labelfont[] for bar in bars]==native.fonts + @test [bar.labelsize[] for bar in bars]==native.labelsize + @test [item.visible[] for item in guide.items]==native.visibility + @test [bar.blockscene.visible[] for bar in bars]==native.scenes + @test length(guide.subscriptions)==native.subscriptions + @test all(a===b for (a, b) in zip(bars, p.colorbars)) + end + end + # The shared setting applies to result panels and a caller-owned native canvas. + raw=TestFixtures.two_conductor_results(; frequencies = [1.0, 10.0, 100.0]) + result=LineCableModels.plot(raw, raw; ydata = ((R, 1, 1:2, :),), layout = (1, 2), + backend = :cairo, display_plot = false, controls = false, guide_spacing = (rowgap = 7.5,), + panel_legends = (1, 1)=>:right) + @test result.plot_state.guide_spacing[]==(rowgap = 7.5, colgap = 12.0) + panel=result.panel_legends[(1, 1)] + figurelegend!(result; guide_spacing = (colgap = 17.5,)) + @test result.panel_legends[(1, 1)]===panel + @test result.plot_state.guide_spacing[]==(rowgap = 7.5, colgap = 17.5) + @test_throws ArgumentError panellegend!(result, (1, 1); guide_spacing = 5) + canvas=LineCableModels.plotwindow(; title = "Native guide spacing", backend = :cairo, + display_plot = false, controls = false, guide_spacing = 14.5) do grid + axis=Axis(grid[1, 1]) + lines!(axis, [1.0, 2.0, 3.0], [2.0, 3.0, 4.0]; label = "native curve") + axis + end + @test canvas.plot_state.guide_spacing[]==(rowgap = 14.5, colgap = 14.5) + figurelegend!(canvas; position = :right, valign = 0.5) + panellegend!(canvas, 1; position = :right, valign = 0.5) + Makie.colorbuffer(canvas.figure) + a=canvas.legend.layoutobservables.computedbbox[] + b=canvas.panel_legends[1].layoutobservables.computedbbox[] + @test a.origin[2]-(b.origin[2]+b.widths[2])>=13.5 + original_view=only(canvas.axes).targetlimits[] + canvas.legend.blockscene.visible[]=false + @test !canvas.legend.blockscene.visible[] + canvas.legend.blockscene.visible[]=true + @test only(canvas.axes).targetlimits[]==original_view + scales=LineCableModels.show_material_scale(; + backend = :cairo, display_plot = false, controls = false, guide_spacing = 9.5) + @test size(scales.plot_state.guides[(:colorbars, nothing)].layout[])==(3, 1) + @test scales.plot_state.guide_spacing[]==(rowgap = 9.5, colgap = 9.5) + collection=preview( + fill(design, 2); options..., layout = (1, 2), colorbar_position = :bottom, + colorbar_group_attributes = (layout = (1, 3),), guide_spacing = 10.5) + @test length(collection.axes)==2 + @test size(collection.plot_state.guides[(:colorbars, nothing)].layout[])==(1, 3) +end diff --git a/test/integration/native_makie_matrix_blocks.jl b/test/integration/native_makie_matrix_blocks.jl new file mode 100644 index 000000000..2dd9205af --- /dev/null +++ b/test/integration/native_makie_matrix_blocks.jl @@ -0,0 +1,306 @@ +@testitem "Makie addons / matrix pages retain coordinates and residual axis dimensions" tags=[:visual, :makie, :slow] begin + using CairoMakie + using LineCableModels + + frequency = 10.0 .^ range(-1, 7; length=13) + z = [complex(10.0^(i-j)*(1+log10(f+1)), i+j+f*1e-3) + for i in 1:5, j in 1:5, f in frequency] + parameters = LineParameters(z, z.*1e-6, frequency) + options = (; backend=:cairo, display_plot=false, controls=true, + open_export=false, length_unit=:base, fig_size=(1200,800)) + pages = LineCableModels.plot(parameters; ydata=(R,), layout=(2,3), options..., + panel_legends=(5,5)=>(position=:inside, max_fraction=0.5)) + @test length(pages) == 6 + @test [length(page.axes) for page in pages] == [6,4,6,4,3,2] + @test [page.plot_state.panel_page.index for page in pages] == + [(1,1),(1,2),(2,1),(2,2),(3,1),(3,2)] + seen = Tuple{Int,Int}[] + for page in pages + @test page.plot_state.nominal_capacity == (2,3) + @test count(block -> block isa Makie.Axis, page.figure.content) == length(page.axes) + @test all(>(0),page.figure.scene.viewport[].widths) + for (coordinate, panel) in page.plot_state.panel_data + push!(seen, coordinate) + i,j = coordinate + curve = only(filter(plot -> plot isa Makie.Lines, panel.axis.scene.plots)) + @test first.(curve[1][]) ≈ frequency + @test last.(curve[1][]) ≈ real.(z[i,j,:]) + end + @test !isempty(Makie.colorbuffer(page.figure)) + end + @test allunique(seen) + @test Set(seen) == Set(Iterators.product(1:5,1:5)) + @test all(isempty(page.panel_legends) for page in pages[1:5]) + @test haskey(last(pages).panel_legends, (5,5)) + @test all(axis -> axis.xlabelvisible[], last(pages).axes) + dimensions = [Tuple(axis.layoutobservables.computedbbox[].widths) + for page in pages for axis in page.axes] + @test all(size -> all(isapprox.(size, first(dimensions); atol=1)), dimensions) + mktempdir() do directory + for page in pages[[1,end]] + before = [axis.layoutobservables.computedbbox[] for axis in page.axes] + path = export_svg(page; path=joinpath(directory,page.export_name*".svg"), open_file=false) + @test filesize(path) > 0 + @test [axis.layoutobservables.computedbbox[] for axis in page.axes] == before + @test !isempty(Makie.colorbuffer(page.figure)) + end + end + + # Selections retain their original slots, not a packed and renumbered submatrix. + sparse = LineCableModels.plot(parameters; ydata=((R,[1,5],[2,5],:),), + layout=(2,3), options...) + @test [page.plot_state.panel_page.index for page in sparse] == [(1,1),(1,2),(3,1),(3,2)] + @test Set(coordinate for page in sparse for coordinate in keys(page.plot_state.panel_data)) == + Set(((1,2),(1,5),(5,2),(5,5))) + small = LineParameters(z[1:2,1:2,:], z[1:2,1:2,:].*1e-6, frequency) + full = LineCableModels.plot(small; ydata=(R,), options...) + @test full isa UIPlot && length(full.axes) == 4 + @test full.plot_state.panel_page.dimensions==(2,2) + one = LineCableModels.plot(small; ydata=(R,), layout=(3,4), title="Example", options...) + @test one isa UIPlot && length(one.axes) == 4 + @test one.export_name=="Example" + for layout in ((0,2),(-1,2),(true,2),(2.0,2),(2,),[2,2]) + @test_throws ArgumentError LineCableModels.plot(small; ydata=(R,), layout, options...) + end + @test_throws ArgumentError LineCableModels.plot(small; ydata=(R,), blocks=(2,2), + layout=(1,2), options...) + family = LineCableModels.plot(small; ydata=(R,G), layout=(1,2), options..., + figure_title=("One line", "Two\nlines", "One line", "One line"), + series_labels=("Saved curve",), legend_position=:bottom, + panel_legends=(2,2)=>(position=:right, max_fraction=0.5)) + rectangles = [axis.layoutobservables.computedbbox[] for page in family for axis in page.axes] + @test all(rectangle -> all(isapprox.(rectangle.widths,first(rectangles).widths;atol=1)),rectangles) + mktempdir() do directory + for (index,page) in enumerate(family), theme in (:default,:publication) + before = [axis.layoutobservables.computedbbox[] for axis in page.axes] + rendered = Any[] + observer = on(page.figure.scene, Makie.events(page.figure).tick) do tick + tick.state === Makie.OneTimeRenderTick && + push!(rendered,[axis.layoutobservables.computedbbox[] for axis in page.axes]) + end + export_svg(page;path=joinpath(directory,"block-$index-$theme.svg"),theme,open_file=false) + off(observer) + # Export reclaims chrome, so frame origins may move. Data-frame + # dimensions remain equal within native pixel rounding. + @test !isempty(rendered) && all(all(all(isapprox.(actual.widths,expected.widths;atol=1)) + for (actual,expected) in zip(rectangles,before)) for rectangles in rendered) + @test [axis.layoutobservables.computedbbox[] for axis in page.axes] == before + end + end + @test Z(parameters) == z +end + +@testitem "Makie addons / stable sparse comparison markers and measured legend grid" tags=[:visual, :makie, :slow] begin + using CairoMakie + using LineCableModels + + frequency = 10.0 .^ range(-1,7; length=101) + z = reshape(complex.(1 .+ log10.(frequency.+1), frequency.*1e-4),1,1,:) + reference = LineParameters(z,z.*1e-6,frequency) + records = [Formulation(earth_impedance=id) + for id in (:xue2018,:default,:pollaczek1926,:saad1996,:wedepohl1973)] + source_id = LineCableModels.Commons.gridpoint_id().source_id + completed = [LineCableModels.Engine.retain_gridpoint(reference, + LineCableModels.Commons.gridpoint_id(;source_id,formulation_index=index); + fields=merge(LineCableModels.Engine.completed_formulation(formula), + (inputs=(resistivity=100.,),coordinates=["core"]))) + for (index,formula) in enumerate(records)] + result = ParametricResult(nothing,completed, + (problems=[:one],formulations=records), ComputationDetails((;))) + options = (; backend=:cairo,display_plot=false,controls=true,open_export=false, + fig_size=(1000,650),length_unit=:base) + page = LineCableModels.plot(result; ydata=(R,),reference,options...) + axis = only(page.axes) + curves = filter(plot -> plot isa Makie.Lines,axis.scene.plots) + markers = [only(filter(plot -> plot isa Makie.Scatter,page.plot_state.groups[group])) + for group in page.plot_state.order] + @test length(curves) == length(markers) == 6 + @test all(curve -> curve.linestyle[] == Makie.to_linestyle(:solid),curves) + @test all(curve -> curve[1][] == first(curves)[1][],curves) + @test allunique([curve.color[] for curve in curves]) + @test last(markers).marker[] == Makie.to_spritemarker(:circle) + @test Makie.to_color(last(curves).color[]) == Makie.to_color(:black) + @test allunique([marker.marker[] for marker in markers[1:end-1]]) + @test Makie.alpha(last(markers).color[]) == 0 + @test all(marker -> 1 <= length(marker[1][]) < length(frequency),markers) + @test all(marker -> all(point -> point in first(curves)[1][],marker[1][]),markers) + @test allunique([first(marker[1][]) for marker in markers]) + @test first(last(markers)[1][]) == first(first(curves)[1][]) + @test last(last(markers)[1][]) == last(first(curves)[1][]) + @test allunique(last(markers)[1][]) + entries = last(only(page.legend.entrygroups[])) + @test length(entries) == 6 + @test page.legend.nbanks[] > 1 + @test all(entry -> any(element -> element isa Makie.MarkerElement,entry.elements),entries) + Makie.toggle_visibility!(first(entries)) + @test !first(curves).visible[] && !first(markers).visible[] + Makie.toggle_visibility!(first(entries)) + @test first(curves).visible[] && first(markers).visible[] + + filtered = LineCableModels.plot(result; ydata=(R,),reference,formulations=[5,2],options...) + filtered_curves = filter(plot -> plot isa Makie.Lines,only(filtered.axes).scene.plots) + filtered_markers = [only(filter(plot -> plot isa Makie.Scatter,filtered.plot_state.groups[group])) + for group in filtered.plot_state.order] + @test [curve.color[] for curve in filtered_curves] == [curves[i].color[] for i in [5,2,6]] + @test [marker.marker[] for marker in filtered_markers] == [markers[i].marker[] for i in [5,2,6]] + @test [marker[1][] for marker in filtered_markers] == [markers[i][1][] for i in [5,2,6]] + plain = LineCableModels.plot(result; ydata=(R,),reference, + series_attributes=(marker=nothing,),options...) + @test !any(plot -> plot isa Makie.Scatter,only(plain.axes).scene.plots) + short_reference = reference[1:2] + short_result = ParametricResult(nothing,[point[1:2] for point in completed],result.axes, ComputationDetails((;))) + short = LineCableModels.plot(short_result; ydata=(R,),reference=short_reference,options...) + @test all(group -> any(plot -> plot isa Makie.Scatter && !isempty(plot[1][]),group), + values(short.plot_state.groups)) + short_markers = only(filter(plot -> plot isa Makie.Scatter, + short.plot_state.groups[last(short.plot_state.order)])) + @test length(short_markers[1][]) == 2 + moved = figurelegend!(page;position=:top,max_fraction=0.5) + Makie.toggle_visibility!(first(last(only(moved.entrygroups[])))) + @test !first(curves).visible[] && !first(markers).visible[] + Makie.toggle_visibility!(first(last(only(moved.entrygroups[])))) + @test first(curves).visible[] && first(markers).visible[] + + # Reflow both entries and labels while retaining native click targets. + long_labels = ["F$i · earth Z=" * repeat("long_identifier",12) * "; earth Y=default" for i in 1:6] + long = LineCableModels.plot(result; ydata=(R,),reference,series_labels=long_labels,options...) + @test !isempty(Makie.colorbuffer(long.figure)) + @test long.legend.nbanks[] == 1 + long_entries = last(only(long.legend.entrygroups[])) + @test last(long_entries).label[] == "(...)" + @test length(long_entries)>1 + @test all(occursin('\n',entry.label[]) for entry in long_entries[1:(end-1)]) + @test long.legend.layoutobservables.autosize[][2] <= 0.5only(long.axes).scene.viewport[].widths[2]+1 + @test long.legend.layoutobservables.autosize[][1] <= long.figure.scene.viewport[].widths[1] + @test Set(values(long.plot_state.labels)) == Set(long_labels) + for size in ((700,500),(1400,900),(1000,650)) + resize!(page.figure,size...) + @test !isempty(Makie.colorbuffer(page.figure)) + @test page.legend.layoutobservables.autosize[][1] <= size[1] + @test length(last(only(page.legend.entrygroups[]))) == 6 + @test last(last(markers)[1][]) == last(first(curves)[1][]) + @test allunique(last(markers)[1][]) + end + mktempdir() do directory + @test isfile(export_svg(page;path=joinpath(directory,"overlap.svg"),open_file=false)) + end + @test Z(reference) == z +end + +@testitem "Makie addons / nominal capacity calibrates frames and compact residuals" tags=[:visual, :makie, :slow] setup=[TestFixtures] begin + using CairoMakie + f=[1.,10.,100.] + z=[complex(i+j+k,i-j+k) for i in 1:3,j in 1:3,k in 1:3] + raw=LineParameters(z,z.*1e-6,f) + pages=LineCableModels.plot(raw;ydata=(R,L),layout=(2,2),fig_size=(1000,700), + controls=false,display_plot=false,backend=:cairo,clip=false) + @test length(pages)==8 + @test [length(p.axes) for p in pages]==[4,2,2,1,4,2,2,1] + @test [p.plot_state.panel_page.dimensions for p in pages[1:4]]==[(2,2),(2,1),(1,2),(1,1)] + frames=[Tuple(axis.layoutobservables.computedbbox[].widths) for p in pages for axis in p.axes] + @test all(frame -> all(isapprox.(frame,first(frames);atol=1)),frames) + sizes=[Tuple(p.figure.scene.viewport[].widths) for p in pages] + @test all(abs.(first(sizes).- (1000,700)).<=1) + ext=Base.get_extension(LineCableModels,:LineCableModelsMakieExt) + for p in pages[1:2] + before=Tuple(p.figure.scene.viewport[].widths) + for _ in 1:3 + ext._update_figure_layout!(() -> nothing,p) + @test Tuple(p.figure.scene.viewport[].widths)==before + end + end + @test sizes[2][1] block isa Makie.Axis,p.figure.content)==length(p.axes) for p in pages) + sibling=[(Tuple(p.figure.scene.viewport[].widths),[axis.targetlimits[] for axis in p.axes]) for p in pages[2:end]] + p=first(pages) + before=[Tuple(axis.layoutobservables.computedbbox[].widths) for axis in p.axes] + views=[axis.targetlimits[] for axis in p.axes] + figuretitle!(p,"A new title\nwith two lines") + figurelegend!(p;position=:bottom) + @test all(all(isapprox.(Tuple(axis.layoutobservables.computedbbox[].widths),size;atol=1)) for (axis,size) in zip(p.axes,before)) + @test [axis.targetlimits[] for axis in p.axes]==views + @test [(Tuple(p.figure.scene.viewport[].widths),[axis.targetlimits[] for axis in p.axes]) for p in pages[2:end]]==sibling + oldsize=Tuple(p.figure.scene.viewport[].widths) + Makie.resize!(p.figure,oldsize[1]+150,oldsize[2]-100) + @test length(p.axes)==4 + @test p.plot_state.panel_page.dimensions==(2,2) + @test [axis.targetlimits[] for axis in p.axes]==views + @test_throws ArgumentError LineCableModels.plot(raw;ydata=(R,),blocks=(2,2),display_plot=false) + + design=TestFixtures.coaxial_design() + previews=preview(fill(design,4);layout=(1,2),size=(1000,600), + display_plot=false,backend=:cairo,controls=false,panel_titles=("one","two","three","four")) + @test length(previews)==2 + @test [length(p.axes) for p in previews]==[2,2] + @test Set(keys(previews[1].plot_state.panel_data))==Set((1,2)) + @test Set(keys(previews[2].plot_state.panel_data))==Set((3,4)) + @test [axis.title[] for p in previews for axis in p.axes]==["one","two","three","four"] + @test all(length(p.colorbars)==3 for p in previews) + strip=preview(fill(design,4);layout=(1,4),size=(1200,600), + display_plot=false,backend=:cairo,controls=false) + @test strip isa UIPlot + @test length(strip.axes)==4 + @test all(isapprox(axis.scene.viewport[].widths[1],axis.scene.viewport[].widths[2];atol=1) for axis in strip.axes) + @test all(axis.scene.viewport[].widths[1]>60 for axis in strip.axes) +end + +@testitem "Makie addons / selected matrix footprints preserve original coordinates" tags=[:visual, :makie, :slow] begin + using CairoMakie + using LineCableModels + ext=Base.get_extension(LineCableModels,:LineCableModelsMakieExt) + f=[1.,10.,100.] + z=[complex(10i+j+k,i-j+k) for i in 1:4,j in 1:4,k in 1:3] + raw=LineParameters(z,z.*1e-6,f) + options=(;controls=false,display_plot=false,backend=:cairo,clip=false,fig_size=(1000,650)) + for coordinate in ((1,1),(2,2)) + i,j=coordinate + pages=LineCableModels.plot(raw,(Z,i,j,:);options...,series_labels=("retained",), + legend_position=:inside,legend_attributes=(halign=:right,valign=:bottom)) + @test length(pages)==2 # quantities remain separate families + for p in pages + @test p.plot_state.nominal_capacity==(1,1) + @test p.plot_state.panel_page.dimensions==(1,1) + @test p.plot_state.panel_page.origin==coordinate + @test Set(keys(p.plot_state.page_cells))==Set([(1,1)]) + @test Set(keys(p.plot_state.panel_data))==Set([coordinate]) + axis=only(p.axes) + frame=axis.scene.viewport[] + legend=p.legend.layoutobservables.computedbbox[] + @test all(legend.origin .>= frame.origin .- 1) + @test all(legend.origin+legend.widths .<= frame.origin+frame.widths .+ 1) + end + curve=only(filter(x -> x isa Makie.Lines,only(first(pages).axes).scene.plots)) + @test last.(curve[1][])≈real.(z[i,j,:]).*1000 + end + selected=LineCableModels.plot(raw,(R,2,2:3,:);options...) + @test selected isa UIPlot + @test selected.plot_state.nominal_capacity==(1,2) + @test selected.plot_state.panel_page.origin==(2,2) + @test selected.plot_state.panel_page.dimensions==(1,2) + split=LineCableModels.plot(raw,(R,2,2:3,:);options...,layout=(1,2)) + @test [p.plot_state.panel_page.index for p in split]==[(2,1),(2,2)] + @test all(p -> p.plot_state.panel_page.dimensions==(1,1),split) + @test [p.plot_state.panel_page.origin for p in split]==[(2,2),(2,3)] + sparse=LineCableModels.plot(raw,(R,[1,3],[1,3],:);options...) + @test length(sparse.axes)==4 + @test sparse.plot_state.panel_page.dimensions==(3,3) + @test length(sparse.plot_state.page_cells)==9 # internal holes are intentional + geometry=only(ext._matrix_pages([(1,1),(3,3)],(3,4),(3,4))) + @test geometry.dimensions==(3,3) + @test geometry.positions==((1,1),(3,3)) + explicit=LineCableModels.plot(raw,(R,2,2,:);options...,layout=(1,2)) + full=LineCableModels.plot(raw,(R,2,1:2,:);options...,layout=(1,2)) + @test explicit.plot_state.panel_page.index==(2,1) + @test explicit.plot_state.panel_page.origin==(2,2) + @test explicit.plot_state.panel_page.dimensions==(1,1) + @test all(isapprox.(only(explicit.axes).scene.viewport[].widths, + first(full.axes).scene.viewport[].widths;atol=1)) + @test explicit.figure.scene.viewport[].widths[1]:metal, :insulation=>:dielectric), + legend_labels=group -> uppercasefirst(String(group))) + @test Set(values(grouped.plot_state.labels)) == Set(("Metal", "Dielectric")) + @test isempty(grouped.colorbars) + @test length(grouped.axes) == 1 + @test_throws ArgumentError preview(design; options..., legend_group=1) + @test_throws ArgumentError preview(design; options..., legend_labels=1) + + collection = preview([design, design, design]; options..., layout=nothing) + @test length(collection.axes) == 3 + @test length(collection.colorbars) == 3 + docked = preview(design; options..., display_legend=false, colorbar_position=:top) + @test docked.legend === nothing + geometry_plots = [copy(axis.scene.plots) for axis in docked.axes] + figurelegend!(docked; position=:top, orientation=:horizontal, max_fraction=0.5) + Makie.colorbuffer(docked.figure) + @test docked.legend.orientation[] == :horizontal + bounds = docked.legend.layoutobservables.computedbbox[] + scale_bounds = [bar.layoutobservables.computedbbox[] for bar in docked.colorbars] + @test bounds.origin[1] + bounds.widths[1] <= + minimum(box.origin[1] for box in scale_bounds) + 1 + figurelegend!(docked; position=:right, max_fraction=0.5) + figurelegend!(docked; position=:top, orientation=:horizontal, max_fraction=0.5) + Makie.colorbuffer(docked.figure) + @test [axis.scene.plots for axis in docked.axes] == geometry_plots + @test length(docked.colorbars) == 3 + @test all(isfinite, docked.legend.layoutobservables.computedbbox[].widths) + + system = build(LineCableSystem, design, (0.0, -0.1); connections=Dict(:core=>1)) + system_plot = preview(system; options..., display_colorbars=false) + @test isempty(system_plot.colorbars) + for zoom_factor in ("invalid", -1.0, Inf) + @test_throws ArgumentError preview(system; options..., zoom_factor) + end +end + +@testitem "Makie addons / constant positive observations retain logarithmic limits" tags=[:visual, :makie] begin + using CairoMakie + + frequency = [10.0, 100.0, 1000.0] + impedance = fill(ComplexF64(1e-4, 1e-3), 1, 1, 3) + admittance = fill(ComplexF64(0.0, 1e-8), 1, 1, 3) + parameters = LineParameters(copy(impedance), copy(admittance), frequency) + options = (; backend=:cairo, display_plot=false, controls=false, + length_unit=:base, quantity_units=:base) + plot = Makie.plot(parameters, (R, 1, 1, :); options..., yscale=:log10) + Makie.colorbuffer(plot.figure) + axis = only(plot.axes) + @test axis.yscale[] === Makie.log10 + limits = axis.finallimits[] + @test 0 < limits.origin[2] < 1e-4 < limits.origin[2] + limits.widths[2] + @test all(isfinite, limits.widths) + line = only(filter(item -> item isa Makie.Lines, axis.scene.plots)) + @test last.(line[1][]) ≈ fill(1e-4, 3) + @test parameters.Z.values == impedance + @test parameters.Y.values == admittance +end + +@testitem "Makie / callable controls own subscriptions and preserve retained figures" tags=[:visual, :makie, :slow] begin + using CairoMakie + calls=Ref(0) + constructions=Ref(0) + controls=Any[] + function custom(p) + constructions[]+=1 + push!(controls,addwidget!((p,cell) -> Button(cell;label="Count"),p,:count; + event=button -> button.clicks,callback=(p,_) -> (calls[]+=1),success="Counted")) + end + raw=LineParameters(fill(1+2im,1,1,3),fill(1+2im,1,1,3),[1.,10.,100.]) + options=(backend=:cairo,display_plot=false,open_export=false) + pages=LineCableModels.plot(raw;ydata=(R,L),widgets=(custom,),options...) + @test constructions[]==2 && calls[]==0 + p=first(pages) + @test p.status===p.plot_state.shell.status + controls[1].clicks[]+=1 + @test calls[]==1 && p.status[]=="Counted" + for _ in 1:3 + resetview!(p) + axisscale!(p,:y,:log10) + axisscale!(p,:y,:linear) + end + controls[1].clicks[]+=1 + @test calls[]==2 && constructions[]==2 + before=length(p.figure.content) + @test_throws ErrorException addwidget!(p,:broken) do p,slot + Label(slot,"partial") + error("builder failed") + end + @test length(p.figure.content)==before + @test !haskey(p.controls,:broken) + @test_throws ArgumentError addwidget!((p,slot) -> Button(slot),p,:reset) + @test_throws ArgumentError addwidget!((p,slot) -> Button(slot),p,:count) + @test_throws ArgumentError addwidget!((p,slot) -> Button(slot),p,:unpaired;event=b -> b.clicks) + @test removewidget!(p,:count)===p + controls[1].clicks[]+=1 + @test calls[]==2 + @test !haskey(p.controls,:count) + @test_throws ArgumentError removewidget!(p,:reset) + @test_throws ArgumentError removewidget!(p,:unknown) + composite_button=Ref{Any}(nothing) + group=addwidget!(p,:composite;event=g -> composite_button[].clicks,callback=(p,_) -> (calls[]+=1)) do p,slot + grid=GridLayout(slot) + composite_button[]=Button(grid[1,1];label="Composite") + Label(grid[1,2],"companion") + grid + end + @test group isa GridLayout + composite_button[].clicks[]+=1 + @test calls[]==3 + removewidget!(p,:composite) + composite_button[].clicks[]+=1 + @test calls[]==3 + none=LineCableModels.plot(raw;ydata=(R,),controls=false,widgets=(custom,),options...) + @test constructions[]==2 && isempty(none.controls) + @test_throws ArgumentError addwidget!((p,slot) -> Button(slot),none,:late) + @test axisscale!(none,:y,:log10)===none + @test only(none.axes).yscale[]===log10 + @test resetview!(none;x=false)===none + @test_throws ArgumentError resetview!(none;panel=(8,8)) + @test_throws ArgumentError axisscale!(none,:z,:linear) + canvas=LineCableModels.plotwindow(;title="Caller canvas",widgets=(custom,),options...) do grid + nested=GridLayout(grid[1,1]) + axis=Axis(nested[2,3]) + lines!(axis,[-2.,0.,3.],[-1.,0.,2.]) + end + @test constructions[]==3 + @test axisscale!(canvas,:x,:log10;panel=1)===canvas + @test only(canvas.axes).xscale[]!==log10 + @test_throws DomainError axisscale!(canvas,:x,log10;panel=1) + @test resetview!(canvas;panel=1)===canvas +end + +@testitem "Makie / composite widget destruction and SVG share native lifetime" tags=[:visual, :makie] begin + using CairoMakie + p=LineCableModels.plotwindow(;title="Composite controls",backend=:cairo,display_plot=false) do grid + axis=Axis(grid[1,1]);lines!(axis,[1.,2.,3.],[2.,4.,8.]) + end + calls=Ref(0) + button=Ref{Any}() + widget=addwidget!(p,:composite;event=grid -> button[].clicks, + callback=(p,_) -> (calls[]+=1)) do p,slot + grid=GridLayout(slot) + button[]=Button(grid[1,1];label="Custom action",height=60) + Label(grid[1,2],"A companion label") + grid + end + button[].clicks[]+=1 + @test calls[]==1 + before_size=Tuple(p.figure.scene.viewport[].widths) + before_view=only(p.axes).targetlimits[] + visible=button[].blockscene.visible[] + states=Bool[] + observer=on(p.figure.scene,Makie.events(p.figure).tick) do tick + tick.state===Makie.OneTimeRenderTick && push!(states,button[].blockscene.visible[]) + end + mktempdir() do directory + for theme in (:default,:publication) + export_svg(p;path=joinpath(directory,"custom-$theme.svg"),theme,open_file=false) + @test calls[]==1 + @test button[].blockscene.visible[]==visible + @test Tuple(p.figure.scene.viewport[].widths)==before_size + @test only(p.axes).targetlimits[]==before_view + end + end + off(observer) + @test !isempty(states) && all(!,states) + clicks=button[].clicks + delete!(button[]) + clicks[]+=1 + @test calls[]==1 + @test removewidget!(p,:composite)===p + @test !haskey(p.controls,:composite) +end diff --git a/test/integration/native_makie_preview_materials.jl b/test/integration/native_makie_preview_materials.jl new file mode 100644 index 000000000..0507a21ec --- /dev/null +++ b/test/integration/native_makie_preview_materials.jl @@ -0,0 +1,256 @@ +@testitem "Makie addons / preview material appearance and switches" tags=[:visual, :makie] begin + using CairoMakie + using CairoMakie: RGB, red, green, blue + using Measurements + + extension = Base.get_extension(LineCableModels, :LineCableModelsMakieExt) + copper = Material(kind = :conductor, rho = 1.72e-8) + dielectric = Material(kind = :insulator, rho = Inf, eps_r = 2.3) + semicon = Material(kind = :semicon, rho = 0.1, eps_r = 20.0) + design = build(CableDesign, + "pattern-controls", + terminal(:core, + core(copper; r = 0.01), insulation(dielectric; t = 0.004), + screen(semicon; t = 0.0002))) + options = (; backend = :cairo, display_plot = false, controls = false, + display_colorbars = false, open_export = false) + plain = preview(design; options..., display_dielectric_pattern = false) + patterned = preview(design; options...) + plain_plots = only(plain.axes).scene.plots + pattern_plots = only(patterned.axes).scene.plots + @test length(plain_plots) == length(pattern_plots) + @test count(p -> p.color[] isa Makie.AbstractPattern, pattern_plots) == 1 + @test all(p -> p.color[] isa CairoMakie.Colorant, plain_plots) + for (before, after) in zip(plain_plots, pattern_plots) + @test before[1][] == after[1][] + if !(after.color[] isa Makie.AbstractPattern) + @test before.color[] == after.color[] + end + end + @test length(patterned.plot_state.groups) == length(plain.plot_state.groups) + @test !isempty(Makie.colorbuffer(patterned.figure)) + figurelegend!(patterned; position = :top) + @test length(only(patterned.axes).scene.plots) == length(pattern_plots) + + for enabled in (true, false) + collection = preview([design, design]; options..., + display_dielectric_pattern = enabled) + @test all(collection.axes) do axis + count(p -> p.color[] isa Makie.AbstractPattern, axis.scene.plots) == + Int(enabled) + end + system = build(LineCableSystem, design, (0.0, -0.1); connections = Dict(:core=>1)) + plotted = preview(system; options..., display_dielectric_pattern = enabled) + @test count(p -> p isa Makie.Poly && p.color[] isa Makie.AbstractPattern, + only(plotted.axes).scene.plots) == Int(enabled) + end + + # Physical transfer endpoints and hue progression, independent of palette helpers. + @test materialcolors(:rho).colormap == materialcolors(:rho, (1.72e-8, 2.5e-7)).colormap + copper_color = extension._material_color(copper) + @test [red(copper_color), green(copper_color), blue(copper_color)] ≈ + [221/255, 228/255, 236/255] atol=1e-6 + magnetic = [extension._material_color(Material(kind = :conductor, rho = 1.72e-8, + mu_r = mu)) for mu in (1.0, 2.0, 5.0, 10.0, 30.0, 100.0, 300.0)] + @test first(magnetic) == extension._material_color(copper) + @test all(c -> blue(c) > red(c) && blue(c) > green(c), magnetic[2:5]) + @test red(last(magnetic)) > red(magnetic[5]) + @test length(unique(magnetic)) == length(magnetic) + @test extension._material_color(Material(kind = :conductor, rho = 1.72e-8, + mu_r = measurement(10.0, 0.1))) == magnetic[4] + anchors = ((0.1, RGB(94/255, 114/255, 131/255)), + (100.0, RGB(139/255, 120/255, 96/255)), + (1e4, RGB(184/255, 154/255, 99/255))) + for (rho, expected) in anchors + actual = extension._material_color(layer(; rho)) + @test maximum(abs.((red(actual)-red(expected), green(actual)-green(expected), + blue(actual)-blue(expected)))) < 1e-6 + end +end + +@testitem "Makie addons / dielectric pattern clips holes and concavities in rendered output" tags=[:visual, :makie, :slow] begin + using CairoMakie + extension = Base.get_extension(LineCableModels, :LineCableModelsMakieExt) + material = Material(kind = :insulator, rho = Inf, eps_r = 2.3) + outer = Point2f[(-2, -2), (2, -2), (2, 2), (0.5, 2), ( + 0.5, 0.7), (-0.5, 0.7), (-0.5, 2), (-2, 2)] + hole = Point2f[(-0.6, -1), (-0.6, -0.3), (0.6, -0.3), ( + 0.6, -1)] + figure = Figure(size = (400, 400)) + axis = Axis(figure[1, 1]; aspect = DataAspect()) + hidedecorations!(axis) + geometry = Makie.GeometryBasics.Polygon(outer, [hole]) + poly!(axis, geometry; + color = extension._material_color(material), strokewidth = 0) + pattern = poly!(axis, geometry; visible = false, + color = extension._material_color(material; pattern = true), strokewidth = 0) + for limits in ((-2.2, 2.2, -2.2, 2.2), (-0.8, 0.8, -1.2, 0.9)) + limits!(axis, limits...) + pattern.visible = false + plain = copy(Makie.colorbuffer(axis; include_decorations = false)) + pattern.visible = true + patterned = Makie.colorbuffer(axis; include_decorations = false) + @test size(plain) == size(patterned) + view = axis.finallimits[] + clear_pixels = 0 + expected_clear = 0 + for row in axes(plain, 1), column in axes(plain, 2) + + x = view.origin[1] + (column-0.5)/size(plain, 2)*view.widths[1] + y = view.origin[2] + (1-(row-0.5)/size(plain, 1))*view.widths[2] + in_hole = -0.55 < x < 0.55 && -0.95 < y < -0.35 + in_notch = -0.45 < x < 0.45 && 0.75 < y < 1.9 + if in_hole || in_notch + expected_clear += 1 + clear_pixels += plain[row, column] == patterned[row, column] + end + end + # Compare complete hole or notch interiors, independent of the tile layout. + @test clear_pixels == expected_clear > 100 + # Native image tiles and solid fills can round one color level apart. + # Count visible ink rather than that 8-bit raster conversion difference. + changed = count(eachindex(plain)) do index + a, b = plain[index], patterned[index] + max(abs(Makie.red(a)-Makie.red(b)), abs(Makie.green(a)-Makie.green(b)), + abs(Makie.blue(a)-Makie.blue(b))) > 2/255 + end + @test 0 < changed < 0.08length(plain) + end +end + +@testitem "Makie addons / earth spans follow the viewport and retain physical depths" tags=[:visual, :makie, :slow] begin + using CairoMakie + copper = Material(kind = :conductor, rho = 1.72e-8) + design = build(CableDesign, "earth-viewport", terminal(:core, core(copper; r = 0.01))) + system = build(LineCableSystem, design, (0.0, -0.2); connections = Dict(:core=>1)) + earth = build(EarthModel, + (layer(rho = 1.0, thickness = 0.1), + layer(rho = 100.0, thickness = 0.25), layer(rho = 1e4))) + options = (; + backend = :cairo, display_plot = false, controls = false, open_export = false) + plotted = preview(system; earth_model = earth, options...) + axis = only(plotted.axes) + layers = [only(plotted.plot_state.groups[Symbol("earth_$i")]) for i in 1:3] + scene_plots = copy(axis.scene.plots) + washes = filter(p -> p isa Makie.HSpan && p ∉ layers, scene_plots) + @test length(washes) == 1 + listener_count = length(axis.finallimits.listeners) + for (xrange, yrange) in (((-20.0, 20.0), (-10.0, 10.0)), + ((5.0, 6.0), (-1.0, 0.0)), ((-3.0, 3.0), (-100.0, -94.0)), + ((-1.0, 1.0), (2.0, 4.0)), ((-0.04, 0.04), (-0.24, -0.16))) + limits!(axis, xrange..., yrange...) + Makie.colorbuffer(plotted.figure) + limits = axis.finallimits[] + bottom, top = limits.origin[2], limits.origin[2]+limits.widths[2] + for (i, (physical_bottom, physical_top)) in + enumerate(((-0.1, 0.0), (-0.35, -0.1), (-Inf, -0.35))) + @test layers[i][1][] ≈ clamp(physical_bottom, bottom, top) + @test layers[i][2][] ≈ clamp(physical_top, bottom, top) + @test !layers[i].xautolimits[] && !layers[i].yautolimits[] + bounds = Makie.boundingbox(only(layers[i].plots)) + @test bounds.origin[1] ≈ limits.origin[1] + @test bounds.widths[1] ≈ limits.widths[1] + end + for wash in washes + lows, highs = wash[1][], wash[2][] + @test all(lows .<= highs) + @test all(lows[lows .< highs] .>= 0) + @test all(highs[lows .< highs] .<= top) + @test wash.visible[] == (top > 0) + if top <= 0 + @test lows == highs + else + @test first(lows) ≈ max(bottom, 0) + @test last(highs) ≈ top + end + end + @test axis.scene.plots == scene_plots + @test length(axis.finallimits.listeners) == listener_count + end + autolimits!(axis) + Makie.colorbuffer(plotted.figure) + @test axis.finallimits[].widths[1] < 0.1 + @test axis.finallimits[].widths[2] < 1 + @test earth.layers[2].thickness == 0.1 + @test earth.layers[3].thickness == 0.25 + @test isinf(earth.layers[4].thickness) + + finite = preview(system; earth_model = EarthModel(100.0; thickness = 0.5), options...) + finite_axis = only(finite.axes) + limits!(finite_axis, -1, 1, -1, 1) + Makie.colorbuffer(finite.figure) + finite_layer = only(finite.plot_state.groups[:earth_1]) + @test finite_layer[1][] == -0.5 + @test finite_layer[2][] == 0.0 + flat = preview(system; earth_model = earth, display_surface_gradient = false, options...) + @test length(only(flat.axes).scene.plots) == length(axis.scene.plots) - 1 + + # The sky tint grows from transparency near z=0 to blue at the top. + flat_axis = only(flat.axes) + for preview_axis in (axis, flat_axis) + # Equal fractional widths at different layout origins can round to + # different pixel widths. Match integer-sized frames for pixel checks. + preview_axis.width[] = 500 + preview_axis.height[] = 500 + limits!(preview_axis, -1, 1, -1, 1) + hidedecorations!(preview_axis) + end + with_sky = Makie.colorbuffer(axis; include_decorations = false) + without_sky = Makie.colorbuffer(flat_axis; include_decorations = false) + @test size(with_sky) == size(without_sky) + view = axis.finallimits[] + column = round(Int, 0.25size(with_sky, 2)) + rows = [clamp(round(Int, (1 - (z - view.origin[2]) / view.widths[2]) * + size(with_sky, 1)), 1, size(with_sky, 1)) + for z in (0.01, 0.50, 0.99, -0.05)] + near, middle, top, soil = with_sky[rows, column] + @test near == without_sky[rows[1], column] + @test Makie.red(near) > Makie.red(middle) > Makie.red(top) + @test Makie.blue(middle) > Makie.red(middle) + 0.03 + @test Makie.blue(top) > Makie.red(top) + 0.10 + @test soil == without_sky[rows[4], column] + for preview_axis in (axis, flat_axis) + limits!(preview_axis, -1, 1, -0.5, -0.02) + end + @test Makie.colorbuffer(axis; include_decorations = false) == + Makie.colorbuffer(flat_axis; include_decorations = false) + + controlled = preview(system; earth_model = earth, options..., controls = true) + controlled_axis = only(controlled.axes) + Makie.colorbuffer(controlled.figure) + initial = controlled_axis.finallimits[] + # A native pan changes the view, not the caller's physical-view request. + controlled_axis.targetlimits[] = Makie.Rect2d(-10, -10, 20, 20) + controlled.controls[:reset].clicks[] += 1 + Makie.colorbuffer(controlled.figure) + @test controlled_axis.finallimits[] == initial + # An explicit later request must remain unchanged by the same reset action. + limits!(controlled_axis, -10, 10, -10, 10) + requested = controlled_axis.limits[] + controlled.controls[:reset].clicks[] += 1 + Makie.colorbuffer(controlled.figure) + @test controlled_axis.limits[] == requested + @test controlled_axis.finallimits[] == Makie.Rect2d(-10, -10, 20, 20) + resize!(controlled.figure.scene, 1000, 500) + Makie.colorbuffer(controlled.figure) + resized = controlled_axis.finallimits[] + for index in 1:3 + span = only(controlled.plot_state.groups[Symbol("earth_$index")]) + bounds = Makie.boundingbox(only(span.plots)) + @test bounds.origin[1] ≈ resized.origin[1] + @test bounds.widths[1] ≈ resized.widths[1] + end + + mktempdir() do directory + for suffix in ("png", "pdf") + path = joinpath(directory, "earth.$suffix") + CairoMakie.save(path, plotted.figure) + @test filesize(path) > 1000 + end + saved_limits = axis.finallimits[] + path = export_svg(plotted; path = joinpath(directory, "earth.svg"), open_file = false) + @test filesize(path) > 1000 + @test axis.finallimits[] == saved_limits + @test axis.scene.plots == scene_plots + end +end diff --git a/test/integration/native_makie_publication.jl b/test/integration/native_makie_publication.jl new file mode 100644 index 000000000..9db5ed92a --- /dev/null +++ b/test/integration/native_makie_publication.jl @@ -0,0 +1,282 @@ +@testitem "Makie addons / matrix selections preserve numerical observations" tags=[:visual, :makie, :slow] begin + using CairoMakie + using LinearAlgebra: diag + + frequency = [10.0, 100.0, 1000.0, 10_000.0] + # Deliberately distinguish both off-diagonals and every frequency plane: + # plotting must not assume reciprocity or silently reorder the selection. + resistance = reshape(collect(1.0:16.0), 2, 2, 4) .* 1e-4 + inductance = reshape(collect(21.0:36.0), 2, 2, 4) .* 1e-7 + conductance = reshape(collect(41.0:56.0), 2, 2, 4) .* 1e-9 + capacitance = reshape(collect(61.0:76.0), 2, 2, 4) .* 1e-11 + angular = reshape(2pi .* frequency, 1, 1, :) + impedance = complex.(resistance, angular .* inductance) + admittance = complex.(conductance, angular .* capacitance) + parameters = LineParameters(copy(impedance), copy(admittance), frequency) + options = (; backend=:cairo, display_plot=false, controls=false, + freq_unit=:kilo, length_unit=:base, quantity_units=:base, clip=false) + samples = [4, 2, 1] + + @testset "standalone $selector" for (source, selector, expected) in ( + (SeriesImpedance(copy(impedance)), R, resistance), + (SeriesImpedance(copy(impedance)), L, inductance), + (ShuntAdmittance(copy(admittance)), G, conductance), + (ShuntAdmittance(copy(admittance)), C, capacitance), + ) + plot = Makie.plot(source, frequency, + (selector, [2, 1], [2, 1], samples); options...) + @test plot isa UIPlot + @test Set(keys(plot.plot_state.panel_data)) == + Set(((1, 1), (1, 2), (2, 1), (2, 2))) + for ((row, column), panel) in plot.plot_state.panel_data + curve = only(filter(item -> item isa Makie.Lines, panel.axis.scene.plots)) + points = curve[1][] + @test first.(points) ≈ frequency[samples] ./ 1000 + @test last.(points) ≈ expected[row, column, samples] + end + @test source.values == (source isa SeriesImpedance ? impedance : admittance) + end + + @testset "diagonal $selector" for (selector, expected) in ( + (R, resistance), (L, inductance), (G, conductance), (C, capacitance), + ) + plot = Makie.plot(parameters, (selector, diag, [2, 1], samples); + frequencies=copy(frequency), options...) + @test Set(keys(plot.plot_state.panel_data)) == Set(((1, 1), (2, 2))) + for ((row, column), panel) in plot.plot_state.panel_data + @test row == column + curve = only(filter(item -> item isa Makie.Lines, panel.axis.scene.plots)) + @test first.(curve[1][]) ≈ frequency[samples] ./ 1000 + @test last.(curve[1][]) ≈ expected[row, row, samples] + end + end + + @test_throws ArgumentError Makie.plot(parameters, (R, 1, 1, :); + frequencies=frequency .* 2, options...) + @test_throws ArgumentError Makie.plot(parameters, + ((R, 1, 1, 1:2), (R, 2, 2, 2:3)); options...) + @test_throws BoundsError Makie.plot(parameters, (R, 3, 1, :); options...) + @test_throws DimensionMismatch Makie.plot(SeriesImpedance(impedance), + frequency[1:2], R; options...) + @test_throws ArgumentError Makie.plot(ShuntAdmittance(admittance), + [10.0, 100.0, Inf, 10_000.0], G; options...) + for (source,selector) in ((SeriesImpedance(impedance),L),(ShuntAdmittance(admittance),C)) + page=Makie.plot(source,[0.0,100.0,1000.0,10_000.0],selector;options...) + retained=observe(only(page.plot_state.observed),selector) + @test all(ismissing,retained[:,:,1]) + @test all(isfinite,retained[:,:,2:end]) + for axis in page.axes + curve=only(filter(item -> item isa Makie.Lines,axis.scene.plots)) + @test isnan(last(first(curve[1][]))) + end + end + + residue = fill(complex(eps(Float64) / 2, 1.0), 1, 1, 4) + source = SeriesImpedance(residue) + for clipped in (false, true) + plot = Makie.plot(source, frequency, R; options..., clip=clipped) + curve = only(filter(item -> item isa Makie.Lines, only(plot.axes).scene.plots)) + @test last.(curve[1][]) == fill(clipped ? 0.0 : eps(Float64) / 2, 4) + @test real.(source.values) == fill(eps(Float64) / 2, 1, 1, 4) + end + @test Z(parameters) == impedance + @test Y(parameters) == admittance + @test frequencies(parameters) == frequency +end + +@testitem "Makie addons / SVG retains interactive preview zoom and pan" tags=[:visual, :makie] begin + using CairoMakie + + copper = Material(kind=:conductor, rho=1.72e-8) + design = build(CableDesign, "interactive-export", + terminal(:core, core(copper; r=0.01))) + system = build(LineCableSystem, design, (0.0, -0.1); connections=Dict(:core=>1)) + earth = build(EarthModel, (layer(rho=10.0, thickness=0.05), layer(rho=100.0))) + + for (name, source, options) in (("design", design, (;)), + ("system", system, (; earth_model=earth, zoom_factor=15))) + plot = preview(source; options..., backend=:cairo, display_plot=false, + controls=true, open_export=false) + axis = only(plot.axes) + Makie.colorbuffer(plot.figure) + configured = axis.limits[] + initial = axis.targetlimits[] + area = axis.scene.viewport[] + mouse = Tuple(area.origin + area.widths / 2) + Makie.events(axis.scene).mouseposition[] = mouse + + # Invoke the native wheel and drag handlers, which change targetlimits + # without replacing the configured limits used by the Reset control. + Makie.process_interaction(last(axis.interactions[:scrollzoom]), + Makie.ScrollEvent(0, 3), axis) + zoomed = axis.targetlimits[] + @test all(zoomed.widths .< initial.widths) + Makie.process_interaction(last(axis.interactions[:dragpan]), + Makie.MouseEvent(Makie.MouseEventTypes.rightdrag, + 1.0, Point2d(0), Point2f(mouse .+ (20, 10)), + 0.0, Point2d(0), Point2f(mouse)), axis) + @test axis.targetlimits[].origin != zoomed.origin + @test axis.limits[] == configured + target = axis.targetlimits[] + view = axis.finallimits[] + rendered_views = Rect2d[] + on(plot.figure.scene, Makie.events(plot.figure).tick) do tick + tick.state === Makie.OneTimeRenderTick && push!(rendered_views, axis.finallimits[]) + return nothing + end + + mktempdir() do directory + for theme in (:default, :publication) + path = joinpath(directory, "$name-$theme.svg") + @test export_svg(plot; path, theme, open_file=false) == path + @test filesize(path) > 1000 + @test !isempty(rendered_views) + @test all(==(view), rendered_views) + @test axis.targetlimits[] == target + @test axis.finallimits[] == view + @test axis.limits[] == configured + end + cd(directory) do + files_before = Set(readdir()) + plot.controls[:export_svg].clicks[] += 1 + saved = joinpath(directory, only(setdiff(Set(readdir()), files_before))) + @test plot.plot_state.shell.status[] == "Saved SVG to $saved" + @test all(==(view), rendered_views) + @test axis.targetlimits[] == target + @test axis.finallimits[] == view + @test axis.limits[] == configured + end + end + end +end + +@testitem "Makie addons / SVG exports preserve live state and existing files" tags=[:visual, :makie] begin + using CairoMakie + + copper = Material(kind=:conductor, rho=1.7241e-8) + xlpe = Material(kind=:insulator, rho=1e14, eps_r=3.5) + design = @cable "publication-state" begin + @terminal :core begin + core(copper; r=1e-3) + insulation(xlpe; t=0.3e-3) + end + end + plot = preview(design; backend=:cairo, display_plot=false, + controls=true, open_export=false, size=(750, 550)) + axis = only(plot.axes) + overlay = lines!(axis, [-0.001, 0.001], [0.0, 0.0]; color=:red) + annotation = text!(axis, 0, 0; text="caller annotation") + axis.title[] = "Caller title" + xlims!(axis, -0.002, 0.002) + ylims!(axis, -0.002, 0.002) + plot.figure.scene.backgroundcolor[] = Makie.to_color(:lightgray) + # These are the caller-owned observables that publication export temporarily + # changes, as well as the live content that it must leave untouched. + observables = Any[plot.figure.scene.backgroundcolor, axis.titlefont, + axis.xlabelfont, axis.ylabelfont, axis.xticklabelfont, axis.yticklabelfont, + plot.legend.labelfont, plot.legend.titlefont] + for colorbar in plot.colorbars + colorbar.labelfont[] = :italic + colorbar.ticklabelfont[] = :bold + append!(observables, (colorbar.labelfont, colorbar.ticklabelfont)) + end + for role in (:regular, :italic, :bold) + push!(observables, plot.figure.scene.theme[:fonts][role]) + end + append!(observables, [control.blockscene.visible for control in values(plot.controls)]) + before = map(observable -> observable[], observables) + rows = copy(plot.figure.layout.rowsizes) + gap = plot.figure.layout.default_rowgap + backend = Makie.current_backend() + limits = axis.limits[] + geometry = copy(overlay[1][]) + mktempdir() do directory + for theme in (:default, :publication) + path = joinpath(directory, "$theme.svg") + @test export_svg(plot; path, theme, open_file=false) == path + document = read(path, String) + @test occursin("", document) + @test map(observable -> observable[], observables) == before + @test plot.figure.layout.rowsizes == rows + @test plot.figure.layout.default_rowgap == gap + @test Makie.current_backend() === backend + @test axis.limits[] == limits + @test axis.title[] == "Caller title" + @test overlay[1][] == geometry + @test annotation in axis.scene.plots + @test_throws ArgumentError export_svg(plot; path, open_file=false) + @test read(path, String) == document + end + @test_throws ArgumentError export_svg(plot; + path=joinpath(directory, "invalid.png"), open_file=false) + @test_throws ArgumentError export_svg(plot; + path=joinpath(directory, "invalid.svg"), theme=:invalid, open_file=false) + @test !isfile(joinpath(directory, "invalid.svg")) + @test map(observable -> observable[], observables) == before + @test plot.figure.layout.rowsizes == rows + + # A failed native save must also restore the live layout. Unix directory + # permissions provide a real I/O failure without replacing Makie methods. + if Sys.isunix() && ccall(:geteuid, Cuint, ()) != 0 + destination = mkdir(joinpath(directory, "read-only")) + chmod(destination, 0o500) + failure = try + export_svg(plot; path=joinpath(destination, "failure.svg"), + theme=:publication, open_file=false) + nothing + catch exception + exception + finally + chmod(destination, 0o700) + end + @test failure isa Exception + @test !isfile(joinpath(destination, "failure.svg")) + @test map(observable -> observable[], observables) == before + @test plot.figure.layout.rowsizes == rows + @test plot.figure.layout.default_rowgap == gap + @test Makie.current_backend() === backend + @test axis.limits[] == limits + end + + # Automatic paths outside the checkout stay in the caller's directory. + # An empty PATH exercises the honest unavailable-opener response without + # invoking a desktop application or depending on one being installed. + cd(directory) do + plot.export_name = " Test / Cable # 1 " + plot.open_export = false + first_path = export_svg(plot) + @test dirname(first_path) == directory + @test startswith(basename(first_path), "test_cable_1_") + first_contents = read(first_path, String) + second_path = export_svg(plot) + @test second_path != first_path + @test read(first_path, String) == first_contents + + # The native button reports an expected export failure in the UI, + # then recovers on a later click after the caller fixes its cause. + files_before = Set(readdir(directory)) + plot.export_theme = :invalid + @test_nowarn plot.controls[:export_svg].clicks[] += 1 + @test occursin("theme must be", plot.plot_state.shell.status[]) + @test Set(readdir(directory)) == files_before + @test map(observable -> observable[], observables) == before + plot.export_theme = :default + plot.controls[:export_svg].clicks[] += 1 + saved = joinpath(directory, only(setdiff(Set(readdir(directory)), files_before))) + @test occursin(" "") do + path = joinpath(directory, "no-desktop.svg") + @test_logs (:info, r"automatic opening was unavailable") begin + export_svg(plot; path, open_file=true) + end + @test isfile(path) + end + end + end + end +end diff --git a/test/integration/native_makie_responsiveness.jl b/test/integration/native_makie_responsiveness.jl new file mode 100644 index 000000000..7d40faa58 --- /dev/null +++ b/test/integration/native_makie_responsiveness.jl @@ -0,0 +1,467 @@ +@testitem "Makie addons / responsive native legend and visible-series limits" tags=[:visual, :makie] setup=[ + UseNativePlotSupport, TestFixtures +] begin + get(ENV, "LINECABLEMODELS_TEST_PLOTTING", "false")=="true"|| + error("set LINECABLEMODELS_TEST_PLOTTING=true to run the visual test") + using CairoMakie + + frequency=10.0 .^ range(1, 4; length = 20) + parameters=TestFixtures.two_conductor_results(; frequencies = frequency) + compact=Makie.plot( + parameters, parameters, parameters, parameters, + @observe R[1, 1, :]; + series_labels = ("one", "two", "three", "four"), + backend = :cairo, + display_plot = false, + fig_size = (420, 260), + legend_position = :right, + legend_cap = 0.5 + ) + Makie.colorbuffer(compact.figure) + labels=[entry.label[] for entry in last(first(compact.legend.entrygroups[]))] + @test !isempty(labels) + @test length(labels) <= 4 + @test last(labels) == "(...)" || length(labels) == 4 + figurelegend!(compact; title = "Updated legend") + resize!(compact.figure, 420, 140) + @test length(last(only(compact.legend.entrygroups[])))<4 + @test first(only(compact.legend.entrygroups[]))=="Updated legend" + # The edited title must fit the new width cap as well as the height. + resize!(compact.figure, 680, 400) + @test length(last(only(compact.legend.entrygroups[])))==4 + @test first(only(compact.legend.entrygroups[]))=="Updated legend" + + complete=Makie.plot( + parameters, parameters, parameters, parameters, + @observe R[1, 1, :]; + series_labels = ("one", "two", "three", "four"), + backend = :cairo, + display_plot = false, + fig_size = (420, 260), + legend_position = :right, + legend_cap = 0.5 + ) + Makie.colorbuffer(complete.figure) + complete_labels=[entry.label[] for entry in last(first(complete.legend.entrygroups[]))] + @test length(complete_labels) == 4 + @test_throws ArgumentError Makie.plot( + parameters, + (R, 1, 1, Colon()); + backend = :cairo, + display_plot = false, + legend_cap = :invalid + ) + + constant=Makie.plot( + parameters, + (R, 1, 1, Colon()); + series_labels = ("result",), + backend = :cairo, + display_plot = false + ) + axis=only(constant.axes) + limits=axis.finallimits[] + @test all(isfinite, limits.origin) + @test all(isfinite, limits.widths) + first_entry=first(last(first(constant.legend.entrygroups[]))) + Makie.toggle_visibility!(first_entry) + @test all(isfinite, axis.finallimits[].origin) + Makie.toggle_visibility!(first_entry) + @test all(isfinite, axis.finallimits[].origin) + + narrow=Makie.plot( + parameters, + @observe Z[1, 1, :]; + backend = :cairo, + display_plot = false, + fig_size = (600, 320) + ) + @test length(narrow)==2 + foreach(p->Makie.colorbuffer(p.figure), narrow) + narrow_bounds=[only(p.axes).layoutobservables.computedbbox[] for p in narrow] + @test all(bounds -> bounds.widths[1]>150, narrow_bounds) + @test all(p -> only(p.axes).xlabelvisible[], narrow) + + tall=Makie.plot( + parameters, + @observe Z[1, 1, :]; + backend = :cairo, + display_plot = false, + fig_size = (600, 700) + ) + @test length(tall)==2 + foreach(p->Makie.colorbuffer(p.figure), tall) + tall_bounds=[only(p.axes).layoutobservables.computedbbox[] for p in tall] + @test all(p -> p.figure.scene.viewport[].widths[1] bounds.widths[1]>150, tall_bounds) + @test all(bounds -> bounds.widths[2]>220, tall_bounds) + @test all(p -> only(p.axes).xlabelvisible[], tall) +end + +@testitem "Makie addons / numeric legend limits preserve data and native resizing" tags=[:visual, :makie, :slow] begin + using CairoMakie + f=[1.0, 10.0, 100.0] + z=ComplexF64[complex(i+j+k, 1) for i in 1:6, j in 1:6, k in eachindex(f)] + source=LineParameters(z, z .* 1e-6, f) + options=(ydata = (R,), overlay = :coordinates, backend = :cairo, display_plot = false, + open_export = false, fig_size = (680, 440)) + for position in (:bottom, :top, :left, :right, :inside) + p=LineCableModels.plot(source; options..., legend_position = position) + axis=only(p.axes) + curves=filter(q->q isa Makie.Lines, axis.scene.plots) + original=[copy(q[1][]) for q in curves] + @test length(curves)==36 + entries=last(only(p.legend.entrygroups[])) + @test 1=default_count + @test p.legend.layoutobservables.autosize[][dimension]<=axis.scene.viewport[].widths[dimension]+1 + @test [q[1][] for q in curves]==original + for invalid in (0, -0.1, 1.01, Inf, NaN, true, :show_all, :ellipsis) + @test_throws ArgumentError figurelegend!(p; max_fraction = invalid) + end + @test p.plot_state.guides[(:legend, nothing)].max_fraction[]==1 + figurelegend!(p; max_fraction = 0.001) + @test !p.legend.blockscene.visible[] + figurelegend!(p; position = nothing) + figurelegend!(p; max_fraction = 1) + figurelegend!(p; position) + @test p.legend.blockscene.visible[] + empty!(p.figure) + end + for invalid in (0, -0.1, 1.01, Inf, NaN, true, :show_all, :ellipsis) + @test_throws ArgumentError LineCableModels.plot(source; options..., legend_cap = invalid) + end + for sizing in ((fig_size = (680, 440),), ( + fig_size = (400, 300), figure = (size = (680, 440),))) + p=LineCableModels.plot(source; options..., sizing...) + @test p.plot_state.shell.reference_size==(680, 440) + @test !isempty(Makie.colorbuffer(p.figure)) + empty!(p.figure) + end + fixed=LineCableModels.plot(source; options..., legend_attributes = ( + width = 1200, height = 800)) + axis=only(fixed.axes) + @test fixed.legend.layoutobservables.computedbbox[].widths[1]<=axis.scene.viewport[].widths[1]+1 + @test fixed.legend.layoutobservables.computedbbox[].widths[2]<=0.5axis.scene.viewport[].widths[2]+1 + resize!(fixed.figure, 1000, 900) + @test fixed.legend.layoutobservables.computedbbox[].widths[2]<=0.5axis.scene.viewport[].widths[2]+1 + @test !isempty(Makie.colorbuffer(fixed.figure)) + empty!(fixed.figure) + panel=LineCableModels.plot(source; options..., legend_position = nothing, + panel_legends = (1=>(position = :bottom, max_fraction = 0.5),)) + @test panel.panel_legends[1].layoutobservables.autosize[][2]<=0.5only(panel.axes).scene.viewport[].widths[2]+1 + @test last(last(only(panel.panel_legends[1].entrygroups[]))).label[]=="(...)" + @test !isempty(Makie.colorbuffer(panel.figure)) + empty!(panel.figure) +end + +@testitem "Makie addons / legend fitting ignores unchanged bounds and retains native edits" tags=[:visual, :makie] begin + using CairoMakie + ext=Base.get_extension(LineCableModels, :LineCableModelsMakieExt) + f=[1.0, 10.0, 100.0] + z=ComplexF64[complex(i+j+k, 1) for i in 1:2, j in 1:2, k in eachindex(f)] + p=LineCableModels.plot( + LineParameters(z, z .* 1e-6, f); ydata = (R,), overlay = :coordinates, + backend = :cairo, display_plot = false, controls = false, open_export = false) + probe=only(filter(q->q isa Makie.Text && !q.visible[], p.legend.blockscene.plots)) + measurements=Ref(0) + subscription=on(_->(measurements[]+=1), probe.text) + publications=Ref(0) + entry_subscription=on(_->(publications[]+=1), p.legend.entrygroups) + bounds=p.plot_state.inside_bbox + initial=bounds[] + frames=[axis.scene.viewport[] for axis in p.axes] + listeners=length(p.figure.scene.viewport.listeners) + for _ in 1:3 + notify(bounds) + ext._update_figure_layout!(() -> nothing, p) + end + @test measurements[]==0 + @test publications[]==0 + @test [axis.scene.viewport[] for axis in p.axes]==frames + @test length(p.figure.scene.viewport.listeners)==listeners + bounds[]=Makie.Rect2f(initial.origin .+ (1, 2), initial.widths) + @test measurements[]==0 + bounds[]=Makie.Rect2f(initial.origin, initial.widths .* (0.6, 1)) + # Reflow reuses the cached measurements of unchanged labels and fonts. + @test measurements[]==0 + before=measurements[] + p.legend.labelsize[]+=2 + @test measurements[]>before + before=measurements[] + first(last(only(p.legend.entrygroups[]))).label[]="A longer coefficient label" + @test measurements[]>before + p.legend.nbanks[]=2 + resize!(p.figure, 900, 600) + @test p.legend.nbanks[]==2 + @test !isempty(Makie.colorbuffer(p.figure)) + off(subscription) + off(entry_subscription) + empty!(p.figure) +end + +@testitem "Makie addons / compact preview and material scheme geometry" tags=[:visual, :makie] setup=[ + UseNativePlotSupport, TestFixtures +] begin + get(ENV, "LINECABLEMODELS_TEST_PLOTTING", "false")=="true"|| + error("set LINECABLEMODELS_TEST_PLOTTING=true to run the visual test") + using CairoMakie + + function inside(viewport, object) + bounds=object.layoutobservables.computedbbox[] + lower=bounds.origin + upper=bounds.origin .+ bounds.widths + viewport_lower=viewport.origin + viewport_upper=viewport.origin .+ viewport.widths + return all(lower .>= viewport_lower)&&all(upper .<= viewport_upper) + end + + design=TestFixtures.coaxial_design() + compact=preview( + design; + backend = :cairo, + display_plot = false, + open_export = false, + # Keep the compact right-column case explicit now that previews default + # to a bottom strip. + colorbar_position = :right, + size = (900, 350) + ) + Makie.colorbuffer(compact.figure) + viewport=compact.figure.scene.viewport[] + # size is the reference allocation. The physical frame and complete + # right-side guides determine the fitted width. + @test 0=350 + axis=only(compact.axes) + axis_bounds=axis.layoutobservables.computedbbox[] + @test axis_bounds.widths[1] >= 160 + @test axis_bounds.widths[2] >= 160 + axis_viewport=axis.scene.viewport[] + @test isapprox(axis_viewport.widths[1], axis_viewport.widths[2]; atol = 1) + @test inside(viewport, axis) + @test inside(viewport, compact.legend) + @test length(compact.colorbars) == 3 + @test all(colorbar -> inside(viewport, colorbar), compact.colorbars) + @test all( + colorbar -> colorbar.layoutobservables.computedbbox[].widths[1] >= 135, + compact.colorbars + ) + + strip=preview(design; backend = :cairo, display_plot = false, + open_export = false, size = (900, 350)) + Makie.colorbuffer(strip.figure) + strip_viewport=strip.figure.scene.viewport[] + @test all(colorbar -> inside(strip_viewport, colorbar), strip.colorbars) + @test inside(strip_viewport, only(strip.axes)) + @test strip.plot_state.guides[(:colorbars, nothing)].position[]===:bottom + + collection=preview( + fill(design, 4); + layout = (2, 2), + backend = :cairo, + display_plot = false, + controls = false, + open_export = false, + size = (1000, 850), + colorbar_position = :bottom, + colorbar_attributes = (; vertical = false) + ) + Makie.colorbuffer(collection.figure) + collection_viewport=collection.figure.scene.viewport[] + collection_bounds=[axis.layoutobservables.computedbbox[] for axis in collection.axes] + @test length(collection_bounds) == 4 + @test all(bounds -> bounds.widths[1] > 220, collection_bounds) + @test all(bounds -> bounds.widths[2] > 200, collection_bounds) + @test all(axis -> inside(collection_viewport, axis), collection.axes) + @test all( + colorbar -> inside(collection_viewport, colorbar), collection.colorbars + ) + @test maximum( + bounds.origin[2] + bounds.widths[2] for bounds in collection_bounds + ) > 0.75collection_viewport.widths[2] + + pages=preview(fill(design, 3); backend = :cairo, display_plot = false, + controls = false, layout = (1, 2)) + foreach(p->Makie.colorbuffer(p.figure), pages) + frames=[axis.layoutobservables.computedbbox[].widths for p in pages for axis in p.axes] + @test all(frame -> isapprox(frame, first(frames); atol = 2), frames) + for p in pages + viewport=p.figure.scene.viewport[] + @test all(object -> inside(viewport, object), (p.axes..., p.colorbars...)) + end + + wide=preview( + design; + backend = :cairo, + display_plot = false, + open_export = false, + size = (1670, 965) + ) + Makie.colorbuffer(wide.figure) + wide_viewport=wide.figure.scene.viewport[] + wide_axis=only(wide.axes) + wide_axis_bounds=wide_axis.layoutobservables.computedbbox[] + wide_legend_bounds=wide.legend.layoutobservables.computedbbox[] + @test inside(wide_viewport, wide_axis) + @test inside(wide_viewport, wide.legend) + wide_viewport=only(wide.axes).scene.viewport[] + @test isapprox(wide_viewport.widths[1], wide_viewport.widths[2]; atol = 1) + @test 0 < + wide_legend_bounds.origin[1] - + (wide_axis_bounds.origin[1] + wide_axis_bounds.widths[1]) < 100 + + reference=LineCableModels.show_material_scale( + backend = :cairo, + display_plot = false, + open_export = false + ) + Makie.colorbuffer(reference.figure) + reference_viewport=reference.figure.scene.viewport[] + @test length(reference.colorbars) == 3 + @test all(colorbar -> inside(reference_viewport, colorbar), reference.colorbars) + @test all( + colorbar -> colorbar.layoutobservables.computedbbox[].widths[1] > 500, + reference.colorbars + ) + vertical_positions=[colorbar.layoutobservables.computedbbox[].origin[2] + for colorbar in reference.colorbars] + @test length(unique(vertical_positions)) == 3 +end + +@testitem "Makie / local native edits and flow identities preserve frames" tags=[:visual, :makie] setup=[TestFixtures] begin + using CairoMakie + options=(backend = :cairo, display_plot = false, open_export = false) + raw=TestFixtures.two_conductor_results() + p=LineCableModels.plot( + raw; options..., ydata = (R,), layout = (1, 1), series_labels = ("one",))|>first + frame=only(p.axes).layoutobservables.computedbbox[].widths + view=only(p.axes).targetlimits[] + before=p.figure.scene.viewport[].widths + p.legend.labelsize[]=30 + @test all(isapprox.(only(p.axes).layoutobservables.computedbbox[].widths, frame; atol = 1)) + @test only(p.axes).targetlimits[]==view + @test p.figure.scene.viewport[].widths[2]>before[2] + widget=addwidget!((p, cell)->Button(cell; label = repeat("Wide control ", 30)), p, :wide) + @test p.figure.scene.viewport[].widths[1]>=widget.layoutobservables.computedbbox[].widths[1] + @test all(isapprox.(only(p.axes).layoutobservables.computedbbox[].widths, frame; atol = 1)) + removewidget!(p, :wide) + @test only(p.axes).targetlimits[]==view + + design=TestFixtures.coaxial_design() + pages=preview(fill(design, 4); options..., controls = false, layout = (1, 2), + panel_titles = Dict(1=>"First", 4=>"Fourth")) + @test first(pages).axes[1].title[]=="First" + @test last(pages).axes[2].title[]=="Fourth" + @test first(pages).axes[2].title[]==design.cable_id + auto=preview(fill(design, 3); options..., controls = false, panel_titles = i->"Cable $i") + identities=auto.plot_state.panel_page.coordinates + resize!(auto.figure, 1800, 450) + @test auto.plot_state.panel_page.coordinates==identities + @test auto.plot_state.panel_page.dimensions==(1, 3) + resize!(auto.figure, 450, 1800) + @test auto.plot_state.panel_page.dimensions==(3, 1) + @test [auto.plot_state.panel_data[i].axis.title[] for i in identities]==["Cable $i" + for i in identities] + + canvas=LineCableModels.plotwindow(; title = "Nested", options..., controls = false) do grid + nested=GridLayout(grid[1, 1]) + lines!(Axis(nested[1, 1]), [1.0, 2.0], [2.0, 4.0]; label = "A") + lines!(Axis(nested[2, 1]), [1.0, 2.0], [3.0, 6.0]; label = "B") + end + plots=[copy(axis.scene.plots) for axis in canvas.axes] + parent=[Makie.GridLayoutBase.gridcontent(axis).parent for axis in canvas.axes] + for (id, label) in ((1, "A"), (2, "B")) + legend=panellegend!(canvas, id; position = :right) + @test only(last(only(legend.entrygroups[]))).label[]==label + end + @test [axis.scene.plots for axis in canvas.axes]==plots + @test [Makie.GridLayoutBase.gridcontent(axis).parent for axis in canvas.axes]==parent +end + +@testitem "Makie addons / fitting preserves physical frames and native resize allocations" tags=[:visual, :makie] setup=[TestFixtures] begin + using CairoMakie + ext=Base.get_extension(LineCableModels, :LineCableModelsMakieExt) + design=TestFixtures.coaxial_design() + p=preview(fill(design, 2); controls = false, display_plot = false, + backend = :cairo, size = (1200, 900)) + @test p.figure.scene.viewport[].widths[2]<800 + @test p.plot_state.guide_gap==(8.0, 8.0, 24.0, 8.0) + frames=[axis.scene.viewport[] for axis in p.axes] + @test all(frame -> isapprox(frame.widths[1], frame.widths[2]; atol = 1), frames) + @test all(all(isapprox.(axis.layoutobservables.computedbbox[].widths, + axis.scene.viewport[].widths; atol = 1)) for axis in p.axes) + views=[axis.targetlimits[] for axis in p.axes] + counts=[length(axis.scene.plots) for axis in p.axes] + listeners=length(p.figure.scene.viewport.listeners) + dimensions=Tuple(p.figure.scene.viewport[].widths) + for _ in 1:5 + ext._update_figure_layout!(()->nothing, p) + @test Tuple(p.figure.scene.viewport[].widths)==dimensions + @test all(all(isapprox.(axis.scene.viewport[].widths, frame.widths; atol = 1)) + for (axis, frame) in zip(p.axes, frames)) + end + @test [axis.targetlimits[] for axis in p.axes]==views + @test [length(axis.scene.plots) for axis in p.axes]==counts + @test length(p.figure.scene.viewport.listeners)==listeners + resize!(p.figure, 1000, 800) + @test Tuple(p.figure.scene.viewport[].widths)==(1000, 800) + resized=[axis.scene.viewport[] for axis in p.axes] + figuretitle!(p, "A title\nwith two lines") + @test all(all(isapprox.(axis.scene.viewport[].widths, frame.widths; atol = 1)) + for (axis, frame) in zip(p.axes, resized)) + @test [axis.targetlimits[] for axis in p.axes]==views + dimensions=Tuple(p.figure.scene.viewport[].widths) + mktempdir() do directory + for theme in (:default, :publication) + @test isfile(export_svg(p; path = joinpath(directory, "$theme.svg"), theme, open_file = false)) + @test Tuple(p.figure.scene.viewport[].widths)==dimensions + @test all(all(isapprox.(axis.scene.viewport[].widths, frame.widths; atol = 1)) + for (axis, frame) in zip(p.axes, resized)) + end + end + @test_throws ErrorException ext._export_presentation!(p, :publication) do + error("failing native writer") + end + @test Tuple(p.figure.scene.viewport[].widths)==dimensions + @test [axis.targetlimits[] for axis in p.axes]==views + @test all(all(isapprox.(axis.scene.viewport[].widths, frame.widths; atol = 1)) + for (axis, frame) in zip(p.axes, resized)) +end + +@testitem "Makie addons / native axis sizing constraints survive content fitting" tags=[:visual, :makie] begin + using CairoMakie + using LineCableModels + ext=Base.get_extension(LineCableModels, :LineCableModelsMakieExt) + f=[1.0, 10.0, 100.0] + z=reshape(complex.([1.0, 2.0, 3.0], [2.0, 3.0, 4.0]), 1, 1, :) + raw=LineParameters(z, z .* 1e-6, f) + for attributes in ((width = 700.0, height = 250.0), + (width = Relative(0.5), height = Relative(0.5)), (alignmode = Outside(10),)) + p=LineCableModels.plot(raw; ydata = (R,), axis = attributes, layout = (1, 2), + fig_size = (800, 500), controls = false, display_plot = false, backend = :cairo) + axis=only(p.axes) + initial=axis.scene.viewport[] + size=Tuple(p.figure.scene.viewport[].widths) + @test all(initial.origin .>= 0) + @test all(initial.origin+initial.widths .<= size) + haskey(attributes, :width) && attributes.width isa Real && + @test Tuple(initial.widths)==(700, 250) + for _ in 1:3 + ext._update_figure_layout!(() -> nothing, p) + @test all(isapprox.(axis.scene.viewport[].widths, initial.widths; atol = 1)) + @test Tuple(p.figure.scene.viewport[].widths)==size + end + end +end diff --git a/test/integration/native_makie_row_overlays.jl b/test/integration/native_makie_row_overlays.jl new file mode 100644 index 000000000..939225d60 --- /dev/null +++ b/test/integration/native_makie_row_overlays.jl @@ -0,0 +1,188 @@ +@testitem "PlotBuilder / row overlays separate gridpoints and retain physical columns" tags=[:visual, :makie, :slow] begin + using CairoMakie + import LineCableModels.Engine as E + import LineCableModels.Commons as G + + f=[10.0,20.0,40.0,80.0] + voltage=[complex(i+2j+k/10, i-j+k/5) for i in 1:3,j in 1:3,k in eachindex(f)] + current=[complex(2i+j+k/5, j-i-k/10) for i in 1:3,j in 1:3,k in eachindex(f)] + modal=LineParameters(ModalDomain(ModalOperators(voltage,current),fill(.01+.02im,3,4)), + SeriesImpedance(voltage),ShuntAdmittance(current.*1e-6),f, + ComputationDetails((inputs=(system=(line_length=20.0,),), + phase_coordinates=["core","screen","armor"]))) + source_id=G.gridpoint_id().source_id + modal_points=[E.retain_gridpoint(modal, + G.gridpoint_id(;source_id,problem_index=index,formulation_index=1); + fields=(inputs=(system=(line_length=20.0index,),),)) for index in 1:2] + segments=collect(Gridspace{PropagationParameters}((Grid(modal_points),))) + requests=((Tv,abs),(Tv,angle),(Ti,abs),(Ti,angle)) + observed=observables.(segments,Ref(requests)) + options=(overlay=:rows,backend=:cairo,display_plot=false,controls=false,open_export=false) + lines(axis)=filter(p -> p isa Makie.Lines,axis.scene.plots) + kept=UIPlot[] + + result=LineCableModels.plot(observed;layout=(1,2),options...) + append!(kept,result) + @test length(result)==16 # four components × two points × two pages + expected=(abs.(voltage),rad2deg.(angle.(voltage)),abs.(current),rad2deg.(angle.(current))) + for (component,values) in enumerate(expected), point in 1:2 + pair=result[(component-1)*4+(point-1)*2+1:(component-1)*4+point*2] + @test [length(page.axes) for page in pair]==[2,1] + @test [page.plot_state.panel_page.index for page in pair]==[(1,1),(2,1)] + @test [page.plot_state.panel_page.coordinates for page in pair]== + [((point,1),(point,2)),((point,3),)] + @test all(page -> page.plot_state.displayed_indices==[point],pair) + @test all(page -> page.plot_state.nominal_capacity==(1,2),pair) + labels=G.observation_labels(observed;request=requests[component],fallback="") + @test all(page -> occursin(labels[point],page.export_name),pair) + for (mode,axis) in enumerate(vcat(pair[1].axes,pair[2].axes)) + @test endswith(string(axis.title[]),"Mode $mode") + curves=lines(axis) + @test length(curves)==3 + @test [curve.label[] for curve in curves]== + ["Conductor core","Conductor screen","Conductor armor"] + for row in 1:3 + @test first.(curves[row][1][])≈f rtol=8eps(Float32) + @test last.(curves[row][1][])≈values[row,mode,:] rtol=8eps(Float32) + @test curves[row].color[]==lines(result[1].axes[1])[row].color[] + end + end + end + # Each page is rendered by the same native renderer, with mode titles and + # conductor legends. Retained matrix arrays are unchanged. + @test !isempty(Makie.colorbuffer(first(result).figure)) + @test Tv(segments[1])==voltage && Ti(segments[1])==current + + automatic=LineCableModels.plot(observed;ydata=((Tv,abs),),options...) + append!(kept,automatic) + @test length(automatic)==2 + @test all(page -> page.plot_state.nominal_capacity==(2,2),automatic) + @test all(page -> length(page.axes)==3,automatic) + scalar=LineCableModels.plot(first(observed);ydata=((Tv,abs),),options...) + singleton=LineCableModels.plot([first(observed)];ydata=((Tv,abs),),options...) + raw=LineCableModels.plot(first(segments);ydata=((Tv,abs),),options...) + append!(kept,[scalar,singleton,raw]) + @test scalar.plot_state.panel_page==singleton.plot_state.panel_page==raw.plot_state.panel_page + @test scalar.export_name==singleton.export_name==raw.export_name + @test !occursin(r"(?:Point|Result) 1",scalar.export_name) + @test [[curve[1][] for curve in lines(axis)] for axis in scalar.axes]== + [[curve[1][] for curve in lines(axis)] for axis in singleton.axes] + named=LineCableModels.plot((short=segments[1],long=segments[2]); + ydata=((Tv,abs),),options...) + append!(kept,named) + @test length(named)==2 + @test all(page -> [curve.label[] for curve in lines(page.axes[1])]== + ["Conductor core","Conductor screen","Conductor armor"],named) + different_names=observables(PropagationParameters(E.retain_gridpoint(modal,G.gridpoint_id(); + fields=(phase_coordinates=["inner","shield","outer"],))),requests) + labelled=LineCableModels.plot([first(observed),different_names];ydata=((Tv,abs),),options...) + append!(kept,labelled) + @test all(axis -> [curve.label[] for curve in lines(axis)]== + ["Conductor inner","Conductor shield","Conductor outer"],labelled[2].axes) + @test [curve.color[] for curve in lines(labelled[1].axes[1])]== + [curve.color[] for curve in lines(labelled[2].axes[1])] + + selected=@observe (Tv,abs)[[3,1],[3,1],2:3] + reordered=LineCableModels.plot(first(observed);ydata=(selected,), + layout=(1,1),options...) + append!(kept,reordered) + @test [only(page.plot_state.panel_page.coordinates) for page in reordered]==[(1,3),(1,1)] + for (page,mode) in zip(reordered,[3,1]), (curve,row) in zip(lines(only(page.axes)),[3,1]) + @test first.(curve[1][])≈f[2:3] rtol=8eps(Float32) + @test last.(curve[1][])≈abs.(voltage[row,mode,2:3]) rtol=8eps(Float32) + @test curve.color[]==lines(scalar.axes[1])[row].color[] + end + styled=LineCableModels.plot(first(observed);ydata=(selected,), + series_labels=("Armor","Core"),series_attributes=((color=:red,),(color=:blue,)),options...) + push!(kept,styled) + @test all(axis -> [curve.label[] for curve in lines(axis)]==["Armor","Core"],styled.axes) + @test all(axis -> [curve.color[] for curve in lines(axis)]==Makie.to_color.([:red,:blue]),styled.axes) + one=@observe (Ti,abs)[2,2,:] + single_entry=LineCableModels.plot(first(observed);ydata=(one,),options...) + push!(kept,single_entry) + @test only(lines(only(single_entry.axes))).label[]=="Conductor screen" + ranged=@observe (Tv,abs)[1:2,2:3,:] + range_page=LineCableModels.plot(first(observed);ydata=(ranged,),options...) + push!(kept,range_page) + @test range_page.plot_state.panel_page.coordinates==((1,2),(1,3)) + @test last.(lines(range_page.axes[2])[2][1][])≈abs.(voltage[2,3,:]) rtol=8eps(Float32) + + filtered=LineCableModels.plot(observed;ydata=((Tv,abs),),problem=2, + reference=first(observed),layout=(2,2), + panel_titles=Dict((3,2)=>"Reference mode two"), + panel_legends=Dict((3,2)=>:inside),options...) + append!(kept,filtered) + @test length(filtered)==2 + @test filtered[1].plot_state.panel_page.coordinates==((2,1),(2,2),(2,3)) + @test filtered[2].plot_state.panel_page.coordinates==((3,1),(3,2),(3,3)) + @test occursin("reference",lowercase(filtered[2].export_name)) + @test filtered[2].axes[2].title[]=="Reference mode two" + @test haskey(filtered[2].panel_legends,(3,2)) + @test !haskey(filtered[1].panel_legends,(3,2)) + @test lines(filtered[1].axes[1])[1].color[]==lines(filtered[2].axes[1])[1].color[] + assumptions=(selections=(Z=((:series,:given,(;)),),Y=((:shunt,:given,(;)),)), + modal=(effective=(method=:default,),)) + equivalent=[observables(PropagationParameters(E.retain_gridpoint(modal, + G.gridpoint_id(;source_id,problem_index=1,formulation_index=index); + fields=assumptions)),requests) for index in 1:2] + @test length(G.observation_groups(equivalent;request=(Tv,abs)))==1 + separate=LineCableModels.plot(equivalent;ydata=((Tv,abs),),options...) + append!(kept,separate) + @test length(separate)==2 + @test [page.plot_state.displayed_indices for page in separate]==[[1],[2]] + + # Invalid dimensions and positional overrides fail before backend activation. + mixed=observables(first(segments),(gamma,requests...)) + @test_throws r"matrix observations" LineCableModels.plot(mixed; + ydata=(gamma,(Tv,abs)),options...,backend=:invalid) + @test_throws r"no completed comparison retains band" LineCableModels.plot(first(observed); + ydata=(selected,),band=(20.0,40.0),options...,backend=:invalid) + @test_throws r"different overlaid dimensions" LineCableModels.plot(mixed; + ydata=(selected,(Ti,abs)),series_labels=("one","two"),options...,backend=:invalid) + @test_throws r"different overlaid dimensions" LineCableModels.plot(mixed; + ydata=(selected,(Ti,abs)),series_attributes=((color=:red,),(color=:blue,)), + options...,backend=:invalid) + foreach(page -> empty!(page.figure),kept) +end + +@testitem "PlotBuilder / row overlays retain nonmodal quantities and uncertainty" tags=[:visual, :makie] begin + using CairoMakie, Measurements + f=[10.0,20.0,40.0] + z=[complex(measurement(i+2j+k/10,.05i),measurement(j-i,.02)) + for i in 1:2,j in 1:2,k in eachindex(f)] + source=LineParameters(z,z.*1e-6,f) + observed=observables(source,(R,X);length_unit=:base,clip=false) + page=LineCableModels.plot(observed;ydata=(R,),overlay=:rows, + backend=:cairo,display_plot=false,open_export=false,errorbar_sampling=:all) + @test length(page.axes)==2 + for (column,axis) in enumerate(page.axes) + curves=filter(p -> p isa Makie.Lines,axis.scene.plots) + bars=filter(p -> p isa Makie.Errorbars,axis.scene.plots) + @test length(curves)==2 && length(bars)==2 + @test endswith(string(axis.title[]),"Conductor $column") + for row in 1:2 + @test curves[row].label[]=="Conductor $row" + @test last.(curves[row][1][])≈[row+2column+k/10 for k in eachindex(f)] rtol=8eps(Float32) + @test getindex.(bars[row][1][],3)≈fill(.05row,3) rtol=8eps(Float32) + end + end + @test !isempty(Makie.colorbuffer(page.figure)) + empty!(page.figure) + + # A band selects saved comparison samples. It is not a new plotting-side + # frequency filter. The same retained selection works with row overlays. + reference=LineCableModels.Engine.retain_gridpoint(source,LineCableModels.Commons.gridpoint_id()) + comparisons=LineCableModels.Engine.compare(reference,source,[R];bands=((20.0,40.0),)) + retained=observables(source,(R,X);comparisons,length_unit=:base,clip=false) + band_page=LineCableModels.plot(retained;ydata=(R,),overlay=:rows,band=(20.0,40.0), + backend=:cairo,display_plot=false,controls=false,open_export=false) + @test length(band_page.axes)==2 + for (column,axis) in enumerate(band_page.axes) + curves=filter(p -> p isa Makie.Lines,axis.scene.plots) + for row in 1:2 + @test first.(curves[row][1][])≈f[2:3] rtol=8eps(Float32) + @test last.(curves[row][1][])≈[row+2column+k/10 for k in 2:3] rtol=8eps(Float32) + end + end + empty!(band_page.figure) +end diff --git a/test/integration/native_makie_series_styles.jl b/test/integration/native_makie_series_styles.jl new file mode 100644 index 000000000..a1fbbb643 --- /dev/null +++ b/test/integration/native_makie_series_styles.jl @@ -0,0 +1,225 @@ +@testitem "Makie addons / per-series markers and dashes reach plots and legends" tags=[:visual, :makie] begin + using CairoMakie + using LineCableModels + + impedance = reshape(ComplexF64[1 + 2im, 2 + 3im], 1, 1, 2) + parameters = LineParameters(impedance, impedance .* 1e-6, [1.0, 10.0]) + sources = (; fem=parameters, proposed=parameters, xue=parameters) + options = (backend=:cairo, display_plot=false, controls=false) + page = LineCableModels.plot(sources, (R,); options..., + series_labels=("FEM", "Proposed", "Xue"), + series_attributes=((marker=:circle, markersize=8), (;), (linestyle=:dash,))) + fem, markers = page.plot_state.groups[:result_1] + xue = only(filter(p -> p isa Makie.Lines,page.plot_state.groups[:result_3])) + @test fem isa Makie.Lines + @test markers isa Makie.Scatter + @test markers.marker[] == Makie.to_spritemarker(:circle) + @test all(==(8), markers.markersize[]) + @test markers[1][] == fem[1][] + fem.visible[] = false + @test !markers.visible[] + fem.visible[] = true + @test markers.visible[] + fem.color[] = :red + @test markers.color[] == fem.color[] + @test xue.linestyle[] == Makie.to_linestyle(:dash) + entries = last(only(page.legend.entrygroups[])) + @test [entry.label[] for entry in entries] == ["FEM", "Proposed", "Xue"] + @test any(element -> element isa Makie.MarkerElement, first(entries).elements) + @test !isempty(Makie.colorbuffer(page.figure)) + @test_throws ArgumentError LineCableModels.plot(sources, (R,); options..., + series_attributes=((linestyle=:dash,),)) + @test_throws ArgumentError LineCableModels.plot(sources, (R,); options..., + series_attributes=(unsupported_attribute=true,)) + mktempdir() do directory + path = export_svg(page; path=joinpath(directory, "styles.svg"), open_file=false) + @test filesize(path) > 0 + @test markers.marker[] == Makie.to_spritemarker(:circle) + @test xue.linestyle[] == Makie.to_linestyle(:dash) + end +end + +@testitem "Makie addons / large style sets retain original slots and palette" tags=[:visual, :makie] begin + using CairoMakie + ext=Base.get_extension(LineCableModels,:LineCableModelsMakieExt) + prefix=copy(ext._CURVE_COLORS) + styles=ext._comparison_styles(collect(1:324),fill(:result,324),324) + @test length(styles)==324 + @test ext._CURVE_COLORS[eachindex(prefix)]==prefix + # Values recorded from the pre-change palette, including distant slots. + expected=(1=>(0.0,0.36,0.68), + 2=>(0.85,0.5808333333333333,0.04250000000000004), + 3=>(0.85,0.04250000000000004,0.85), + 18=>(0.85,0.3789583333333334,0.04250000000000004), + 81=>(0.31499999999999995,0.7,0.4112499999999999), + 162=>(0.4604166666666667,0.38249999999999995,0.85), + 324=>(0.5716666666666667,0.31499999999999995,0.7)) + for (slot,rgb) in expected + color=styles[slot].attributes.color + @test [color.r,color.g,color.b]≈collect(rgb) rtol=1e-14 atol=0 + @test styles[slot].phase==(slot,324) + end + sparse=ext._comparison_styles([324,0,2,324], + [:result,:reference,:result,:result],324) + @test sparse[[1,3,4]]==styles[[324,2,324]] + @test sparse[2].attributes.color==ext.RGB(0.,0.,0.) + @test sparse[2].attributes.marker===:circle + @test sparse[2].hollow && sparse[2].endpoints + @test sparse[2].phase==(1,1) && sparse[2].priority==1 + @test only(ext._comparison_styles([0],[:reference],0))==sparse[2] + @test isempty(ext._comparison_styles(Int[],Symbol[],0)) + @test_throws ArgumentError ext._comparison_styles([0],[:result],1) + @test ext._comparison_styles([2,1],[:result,:result],324)==styles[[2,1]] + + for attributes in (nothing,(color=:red,), + Tuple((linewidth=i,) for i in 1:324),[(linewidth=i,) for i in 1:324]) + normalized=ext._series_attributes(attributes,324) + @test length(normalized)==324 + for slot in (1,18,324) + expected_attributes=attributes===nothing ? (;) : attributes isa NamedTuple ? attributes : attributes[slot] + @test normalized[slot]==expected_attributes + end + end + @test_throws ArgumentError ext._series_attributes(((color=:red,),),324) +end + +@testitem "Makie addons / shared series attributes cover every plot family" tags=[:visual, :makie, :slow] setup=[TestFixtures] begin + using CairoMakie + using LineCableModels + using Measurements + + options = (backend=:cairo, display_plot=false, controls=false, open_export=false) + attributes = (color=:magenta,) + parameters = TestFixtures.two_conductor_results() + # A common override applies to each matrix facet on every Z/Y page. + pages = LineCableModels.plot(parameters, (Z, Y); options..., + series_attributes=attributes) + @test length(pages) == 4 + @test all(page -> length(page.axes) == 4, pages) + styled = UIPlot[pages...] + push!(styled, Makie.plot(parameters.Z, frequencies(parameters), R; + options..., series_attributes=attributes)) + push!(styled, Makie.plot(parameters.Y, frequencies(parameters), G; + options..., series_attributes=attributes)) + + # Table illustrations use the observation publication renderer. + artifact = report(TableReportDefinition(((R, :, :, :),); + illustration=true, plot_options=(; options..., series_attributes=attributes)), + parameters) + push!(styled, artifact.illustration) + + result = TestFixtures.cable_monte_carlo_result() + for recipe in (Makie.hist, Makie.stairs, Makie.ecdfplot, Makie.lines, Makie.qqplot) + push!(styled, recipe(result, R; options..., series_attributes=attributes)) + end + + copper = Material(kind=:conductor, rho=1.72e-8) + design = build(CableDesign, "shared-style", terminal(:core, core(copper; r=0.01))) + system = build(LineCableSystem, design, (0.0, -1.0); connections=Dict(:core=>1)) + for source in (design, [design, design], system) + push!(styled, preview(source; options..., display_colorbars=false, + series_attributes=attributes)) + end + native = LineCableModels.plotwindow(; title="Shared style", options..., + series_attributes=attributes) do canvas + axis = Axis(canvas[1, 1]) + lines!(axis, [1.0, 2.0], [3.0, 4.0]) + scatter!(axis, [1.0, 2.0], [4.0, 3.0]) + end + push!(styled, native) + for page in styled + @test !isempty(page.plot_state.groups) + @test all(Makie.to_color(handle.color[]) == Makie.to_color(:magenta) + for handles in values(page.plot_state.groups) for handle in handles) + end + + # Adding markers retains measurement error bars and their visibility group. + uncertain = complex.(measurement.(real.(Z(parameters)), 1e-6), + measurement.(imag.(Z(parameters)), 1e-6)) + page = Makie.plot(SeriesImpedance(uncertain), frequencies(parameters), (R, 1, 1, :); + options..., series_attributes=(marker=:circle, markersize=8, alpha=0.35)) + handles = only(values(page.plot_state.groups)) + @test count(handle -> handle isa Makie.Lines, handles) == 1 + @test count(handle -> handle isa Makie.Errorbars, handles) == 1 + @test count(handle -> handle isa Makie.Scatter, handles) == 1 + @test all(handle -> handle.alpha[] == 0.35, + filter(handle -> handle isa Union{Makie.Lines, Makie.Scatter}, handles)) + @test !isempty(Makie.colorbuffer(page.figure)) +end + +@testitem "Makie addons / automatic difference labels and chromatic result prefix" tags=[:visual, :makie, :slow] begin + using CairoMakie + using LineCableModels.Engine: retain_gridpoint,completed_formulation + using LineCableModels.Commons: gridpoint_id,observation_labels + ext=Base.get_extension(LineCableModels,:LineCableModelsMakieExt) + options=(backend=:cairo,display_plot=false,controls=false,open_export=false) + source_id=gridpoint_id().source_id + analytical=Formulation(earth_impedance=:unified,earth_admittance=:unified) + fem=Formulation(:fem) + z=reshape(complex.(1.:12.,21.:32.),2,2,3) + function point(formulation,index;rho=100.,problem=1) + inputs=(rho=rho,radius=.0425,field_descriptions=(rho=(name="electrical resistivity",unit="Ω·m"),radius=(name="radius",unit="m"))) + retain_gridpoint(LineParameters(copy(z),z.*1e-6,[1.,10.,100.]), + gridpoint_id(;source_id,problem_index=problem,formulation_index=index); + fields=merge(completed_formulation(formulation),(inputs=inputs,))) + end + a=point(analytical,1) + # Synthetic numerical fixture retains a FEM identity. No FEM solve is claimed. + reference=point(fem,2) + pages=LineCableModels.plot(a;ydata=(R,X,G,B),reference,length_unit=:base,options...) + @test length(pages)==4 + results=LineCableModels.plot(a;ydata=(R,X,G,B),length_unit=:base,options...) + native_lines(axis)=filter(item -> item isa Makie.Lines,axis.scene.plots) + expected_reference=description(fem;compact=true)*" (reference)" + for (page,solo,request) in zip(pages,results,(R,X,G,B)) + method=request in (R,X) ? analytical.methods.earth_impedance : analytical.methods.earth_admittance + @test Set(values(page.plot_state.labels))==Set((description(method;compact=true),expected_reference)) + for (axis,solo_axis) in zip(page.axes,solo.axes) + curves=native_lines(axis) + @test Makie.to_color(curves[1].color[])==Makie.to_color(ext._comparison_color(1)) + @test curves[1].color[]==only(native_lines(solo_axis)).color[] + @test Makie.to_color(curves[2].color[])==Makie.to_color(:black) + end + end + prefix=[ext._comparison_color(i) for i in 1:2] + palette=[ext._comparison_color(i) for i in 1:12] + @test palette[1:2]==prefix + @test length(unique(palette))==12 + @test all(ext._curve_color_allowed,palette) + labs=ext.Oklab.(palette) + distances=[sqrt(ext._color_distance(labs[i],labs[j])) for i in 1:12 for j in i+1:12] + @test minimum(distances)>.08 # measured engineering separation for this finite prefix + population=[point(analytical,i;rho=100.0i,problem=i) for i in 1:12] + p=LineCableModels.plot(population;ydata=(@observe(R[1,1,:]),),length_unit=:base,options...) + @test [Makie.to_color(c.color[]) for c in native_lines(only(p.axes))]==Makie.to_color.(palette) + @test all(label -> occursin("Ω·m",label) && !occursin("radius",label),values(p.plot_state.labels)) + small=LineCableModels.plot(population[1:2];ydata=(@observe(R[1,1,:]),),length_unit=:base,options...) + @test [c.color[] for c in native_lines(only(small.axes))]==[c.color[] for c in native_lines(only(p.axes))[1:2]] + filtered=LineCableModels.plot(population;ydata=(R,X,G,B),reference,problem=[2,5],layout=(1,2),length_unit=:base,options...) + @test length(filtered)==8 + for page in filtered, axis in page.axes + @test [Makie.to_color(c.color[]) for c in native_lines(axis)]==Makie.to_color.([palette[2],palette[5],ext.RGB(0.,0.,0.)]) + end + # Rebuilding native guides, changing views and exporting use detached data. + observed=pages[1].plot_state.observed + Z(a).=NaN;Z(reference).=NaN + p=LineCableModels.plot(observed[1];reference=observed[2],ydata=(R,),options...,controls=true) + labels=copy(p.plot_state.labels) + colors=[c.color[] for c in native_lines(first(p.axes))] + owner=first(native_lines(first(p.axes))) + owner.visible[]=false + figurelegend!(p;position=:right) + figurelegend!(p;position=:bottom) + @test !owner.visible[] + @test p.plot_state.labels==labels + owner.visible[]=true + axisscale!(p,:y,:log10);resetview!(p) + resize!(p.figure.scene,(1100,800)) + view=[axis.finallimits[] for axis in p.axes] + mktempdir() do directory + export_svg(p;path=joinpath(directory,"automatic.svg"),theme=:publication,open_file=false) + @test [axis.finallimits[] for axis in p.axes]==view + end + @test [c.color[] for c in native_lines(first(p.axes))]==colors + @test p.plot_state.labels==labels +end diff --git a/test/integration/native_makie_uncertainty_visibility.jl b/test/integration/native_makie_uncertainty_visibility.jl new file mode 100644 index 000000000..b9a7d27b6 --- /dev/null +++ b/test/integration/native_makie_uncertainty_visibility.jl @@ -0,0 +1,467 @@ +@testitem "Makie addons / coincident uncertainty intervals retain nested native glyphs" tags=[:visual, :makie, :slow] begin + using CairoMakie, Measurements + + f = measurement.([1.0, 10.0, 100.0], [0.1, 0.2, 0.3]) + z = [measurement(i+2j+k, 0.1i+0.01j+0.001k) + im*measurement(i+j+k, 0.2) + for i in 1:2, j in 1:2, k in eachindex(f)] + source = LineParameters(z, z .* 1e-6, f) + before = deepcopy((Z(source), Y(source), frequencies(source))) + options = (backend = :cairo, display_plot = false, open_export = false, + length_unit = :base, quantity_units = :base, freq_unit = :base, clip = false) + pages = LineCableModels.plot(source, source, source; options..., ydata = (R,), + layout = (1, 2), errorbar_sampling = :all, + series_labels = ("first", "second", "third"), legend_position = :bottom) + for page in pages + for axis in page.axes + curves = filter(p -> p isa Makie.Lines, axis.scene.plots) + bars = filter(p -> p isa Makie.Errorbars, axis.scene.plots) + @test length(curves)==3 && length(bars)==6 + @test all(isempty(p[1][]) for p in axis.scene.plots if p isa Makie.Scatter) + @test all(line -> line.linewidth[] == 2, curves) + @test all(line -> line[1][] == first(curves)[1][], curves) + for direction in (:x, :y) + intervals = filter(bar -> bar.direction[] === direction, bars) + @test all(bar -> bar[1][] == first(intervals)[1][], intervals) + @test length(first(intervals)[1][]) == length(f) + # Wider caps AND stems expose earlier coincident intervals. + # Exact styling constants are not part of the retained quantity data. + caps = [bar.whiskerwidth[] for bar in intervals] + stems = [bar.linewidth[] for bar in intervals] + @test all(diff(caps) .< 0) && all(caps .> 0) + @test all(diff(stems) .< 0) && all(stems .> 0) + end + end + intervals = [p for axis in page.axes + for p in axis.scene.plots if p isa Makie.Errorbars] + saved = [(copy(p[1][]), p.whiskerwidth[], p.linewidth[]) for p in intervals] + entry = first(last(only(page.legend.entrygroups[]))) + Makie.toggle_visibility!(entry) + Makie.toggle_visibility!(entry, true) + for key in (:xlog, :ylog) + page.controls[key].active[] = true + @test !isempty(Makie.colorbuffer(page.figure)) + page.controls[key].active[] = false + end + resize!(page.figure, 1000, 700) + mktempdir() do directory + @test isfile(export_svg(page; path = joinpath(directory, "nested.svg"), open_file = false)) + end + @test [(p[1][], p.whiskerwidth[], p.linewidth[]) for p in intervals] == saved + end + # Default nesting must not take ownership away from explicit native kwargs. + custom = LineCableModels.plot(source, source; options..., ydata = ((R, 1, 1, :),), + series_attributes = ( + (whiskerwidth = 15, linewidth = 2.5), (whiskerwidth = 7, linewidth = 0.8))) + bars = filter(p -> p isa Makie.Errorbars, only(custom.axes).scene.plots) + @test [bar.whiskerwidth[] for bar in bars] == [15, 15, 7, 7] + @test [bar.linewidth[] for bar in bars] ≈ [2.5, 2.5, 0.8, 0.8] + # Styling a caller's native plots must not reset unrelated attributes to + # owned defaults merely because those primitives also happen to be bars. + native = LineCableModels.plotwindow(; + title = "Native bars", backend = :cairo, display_plot = false, + controls = false, open_export = false, series_attributes = (color = :red,)) do canvas + axis = Axis(canvas[1, 1]) + errorbars!( + axis, [1.0, 2.0], [3.0, 4.0], [0.2, 0.3]; whiskerwidth = 15, linewidth = 2.5) + errorbars!( + axis, [1.0, 2.0], [3.0, 4.0], [0.2, 0.3]; whiskerwidth = 7, linewidth = 0.8) + end + bars = filter(p -> p isa Makie.Errorbars, only(native.axes).scene.plots) + @test [bar.whiskerwidth[] for bar in bars] == [15, 7] + @test [bar.linewidth[] for bar in bars] ≈ [2.5, 0.8] + @test isequal(before, (Z(source), Y(source), frequencies(source))) +end + +@testitem "Makie addons / staggered comparisons retain complete uncertainty bounds" tags=[:visual, :makie, :slow] begin + using CairoMakie, Measurements + + f = collect(1.0:101.0) + resistance = 1 .+ f ./ 100 + errors = fill(0.05, length(f)) + errors[1] = 3.0 # Use an endpoint interval. A sparse interior glyph would omit the endpoint. + z = reshape(complex.(measurement.(resistance, errors), 0.1), 1, 1, :) + reference = LineParameters(z, z .* 1e-6, measurement.(f, 0.01)) + other_z = reshape(complex.(measurement.(1.05 .* resistance, 0.02), 0.1), 1, 1, :) + compared_result = LineParameters(other_z, other_z .* 1e-6, measurement.(f, 0.02)) + axes = (problems = [:one], formulations = [NamedTuple(Formulation())]) + result = ParametricResult(nothing, [compared_result], axes, ComputationDetails((;))) + before = deepcopy((Z(reference), Y(reference), frequencies(reference), Z(compared_result))) + options = (; backend = :cairo, display_plot = false, open_export = false, + ydata = ((R, 1, 1, :),), reference, length_unit = :base, quantity_units = :base, + freq_unit = :base, clip = false, fig_size = (1000, 650)) + page = LineCableModels.plot(result; options...) + axis = only(page.axes) + for (key, source) in zip(page.plot_state.order, (compared_result, reference)) + group = page.plot_state.groups[key] + line = only(filter(p -> p isa Makie.Lines, group)) + marker = only(filter(p -> p isa Makie.Scatter, group)) + bars = filter(p -> p isa Makie.Errorbars, group) + @test length(line[1][]) == length(f) + @test length(bars) == 2 + @test 0 < length(first(bars)[1][]) < length(f) + @test first.(bars[1][1][]) == first.(bars[2][1][]) + for bar in bars + indices = [findfirst(==(p[1]), f) for p in bar[1][]] + expected = bar.direction[] === :x ? frequencies(source)[indices] : + R(source)[1, 1, indices] + @test getindex.(bar[1][], 3) ≈ uncertainty.(expected) + @test isdisjoint(first.(marker[1][]), first.(bar[1][])) + end + @test all(p -> p in line[1][], marker[1][]) + @test all(bar -> Makie.to_color(bar.color[]) == Makie.to_color(line.color[]), bars) + end + reference_group = page.plot_state.groups[last(page.plot_state.order)] + reference_bar = only(filter(p -> p isa Makie.Errorbars && p.direction[] === :y, reference_group)) + @test first(f) ∉ first.(reference_bar[1][]) + @test axis.finallimits[].origin[2] <= resistance[1] - errors[1] + @test sum((axis.finallimits[].origin[2], axis.finallimits[].widths[2])) >= + resistance[1] + errors[1] + # A hidden negative interval still rules out an ordinary logarithmic axis. + page.controls[:ylog].active[] = true + @test axis.yscale[] !== log10 + page.controls[:ylog].active[] = false + entry = last(last(only(page.legend.entrygroups[]))) + Makie.toggle_visibility!(entry) + @test all(!p.visible[] for p in reference_group) + @test sum((axis.finallimits[].origin[2], axis.finallimits[].widths[2])) < 4.0 + Makie.toggle_visibility!(entry, true) + for size in ((460, 400), (1200, 750)) + resize!(page.figure, size...) + @test !isempty(Makie.colorbuffer(page.figure)) + @test axis.finallimits[].origin[2] <= resistance[1] - errors[1] + @test sum((axis.finallimits[].origin[2], axis.finallimits[].widths[2])) >= + resistance[1] + errors[1] + end + view = Makie.Rect2d(20.0, 1.2, 30.0, 0.5) + axis.targetlimits[] = view + resize!(page.figure, 900, 650) + @test axis.targetlimits[] == view + page.controls[:reset].clicks[] += 1 + mktempdir() do directory + @test isfile(export_svg(page; path = joinpath(directory, "staggered.svg"), open_file = false)) + end + + for n in (1, 2) + short = ParametricResult(nothing, [compared_result[1:n]], axes, ComputationDetails((;))) + plot = LineCableModels.plot(short; options..., reference = reference[1:n]) + for group in values(plot.plot_state.groups) + marker = only(filter(p -> p isa Makie.Scatter, group)) + bars = filter(p -> p isa Makie.Errorbars, group) + @test all(!isempty(bar[1][]) for bar in bars) + @test all(isdisjoint(first.(marker[1][]), first.(bar[1][])) for bar in bars) + end + @test any(element -> element isa Makie.MarkerElement, + first(last(only(plot.legend.entrygroups[]))).elements) + end + overridden = LineCableModels.plot(result; options..., errorbar_sampling = :all, + series_attributes = (marker = :diamond, color = :magenta, whiskerwidth = 12)) + for group in values(overridden.plot_state.groups) + marker = only(filter(p -> p isa Makie.Scatter, group)) + @test length(marker[1][]) == length(f) + @test marker.marker[] == Makie.to_spritemarker(:diamond) + @test all(p -> Makie.to_color(p.color[]) == Makie.to_color(:magenta), group) + @test all(p.whiskerwidth[] == 12 for p in group if p isa Makie.Errorbars) + end + @test_throws r"errorbar_sampling" LineCableModels.plot(result; options..., errorbar_sampling = :invalid) + @test isequal(before, ( + Z(reference), Y(reference), frequencies(reference), Z(compared_result))) +end + +@testitem "Makie addons / uncertainty legend actions preserve complete series" tags=[:visual, :makie] begin + using CairoMakie, Measurements + + f = [1.0, 10.0, 100.0] + sources = map((1.0, 10.0)) do offset + z = [measurement(offset + i + 2j + k, 0.1offset + 0.01i + 0.001k) + + im * measurement(2offset + i + j + k, 0.2offset) + for i in 1:2, j in 1:2, k in eachindex(f)] + LineParameters(z, z .* 1e-6, f) + end + page = LineCableModels.plot(sources...; ydata = (R,), + backend = :cairo, display_plot = false, controls = true, open_export = false, + series_labels = ("first", "second"), legend_position = :bottom, + series_attributes = ((marker = :circle,), (marker = :utriangle,)), + length_unit = :base, quantity_units = :base, clip = false) + entries = last(only(page.legend.entrygroups[])) + for entry in entries + # Exercise the native legend action, not a direct assignment to the mean + # line: the latter missed the exception in the error-bar child plots. + Makie.toggle_visibility!(entry) + end + for axis in page.axes + @test all(!plot.visible[] + for plot in axis.scene.plots + if plot isa Union{Makie.Lines, Makie.Errorbars, Makie.Scatter}) + end + @test all(entry -> first(Makie.get_n_visible(entry)) == 0, entries) + for entry in entries + Makie.toggle_visibility!(entry, true) + end + for axis in page.axes + @test all(plot.visible[] + for plot in axis.scene.plots + if plot isa Union{Makie.Lines, Makie.Errorbars, Makie.Scatter}) + end + @test all(entry -> ==(Makie.get_n_visible(entry)...), entries) +end + +@testitem "Makie addons / uncertainty coordinates and visibility survive every axis mode" tags=[:visual, :makie, :slow] begin + using CairoMakie, Measurements + + f = [1.0, 10.0, 100.0] + options = ( + backend = :cairo, display_plot = false, controls = true, open_export = false, + length_unit = :base, quantity_units = :base, freq_unit = :base, clip = false) + for (xspread, yspread, markers) in ((0, 0, false), (0, 1, false), (1, 0, true), ( + 1, 1, true)) + frequency = measurement.(f, xspread .* 0.01f) + sources = map((1.0, 10.0)) do offset + z = [measurement(offset+i+2j+k, yspread*(0.1offset+0.01i+0.001k)) + + im*measurement(2offset+i+j+k, yspread*0.2offset) + for i in 1:2, j in 1:2, k in eachindex(f)] + LineParameters(z, z .* 1e-6, frequency) + end + originals = map(p -> (copy(Z(p)), copy(Y(p)), copy(frequencies(p))), sources) + attributes = markers ? ((marker = :circle,), (marker = :utriangle,)) : nothing + page = LineCableModels.plot( + sources...; options..., ydata = (R,), errorbar_sampling = :all, + series_labels = ("first", "second"), series_attributes = attributes, + legend_position = :bottom, panel_legends = (1, 1)=>:right) + for (index, axis) in enumerate(page.axes) + i, j = cld(index, 2), mod1(index, 2) + curves = filter(p -> p isa Makie.Lines, axis.scene.plots) + bars = filter(p -> p isa Makie.Errorbars, axis.scene.plots) + @test length(bars) == 2*(xspread+yspread) + for (source, curve) in zip(sources, curves) + expected = observe(source, R)[i, j, :] + @test first.(curve[1][]) ≈ f + @test last.(curve[1][]) ≈ nominal.(expected) + matching = filter( + bar -> Makie.to_color(bar.color[]) == + Makie.to_color(curve.color[]), bars) + @test length(matching) == xspread+yspread + for bar in matching + @test getindex.(bar[1][], 1) ≈ f + @test getindex.(bar[1][], 2) ≈ nominal.(expected) + spread = bar.direction[] === :x ? uncertainty.(frequency) : + uncertainty.(expected) + @test getindex.(bar[1][], 3) ≈ spread + @test getindex.(bar[1][], 4) ≈ spread + end + end + end + # Panel actions are local. Figure actions recover a mixed + # panel state. No assumption about private dependency storage is needed. + panel_entry = first(last(only(page.panel_legends[(1, 1)].entrygroups[]))) + Makie.toggle_visibility!(panel_entry) + first_color = Makie.to_color(first(filter(p -> p isa Makie.Lines, first(page.axes).scene.plots)).color[]) + for (index, axis) in enumerate(page.axes), plot in axis.scene.plots + + plot isa Union{Makie.Lines, Makie.Errorbars, Makie.Scatter} || continue + @test plot.visible[] == + !(index == 1 && Makie.to_color(plot.color[]) == first_color) + end + entry = first(last(only(page.legend.entrygroups[]))) + visible, total = Makie.get_n_visible(entry) + @test 0 < visible < total + Makie.toggle_visibility!(entry, visible!=total) + for cycle in 1:3 + Makie.toggle_visibility!(entry) + for axis in page.axes, plot in axis.scene.plots + + plot isa Union{Makie.Lines, Makie.Errorbars, Makie.Scatter} || continue + @test plot.visible[] == (Makie.to_color(plot.color[]) != first_color) + end + Makie.toggle_visibility!(entry, true) + end + for key in (:xlog, :ylog) + page.controls[key].active[] = true + @test !isempty(Makie.colorbuffer(page.figure)) + page.controls[key].active[] = false + end + for (source, original) in zip(sources, originals) + @test isequal(Z(source), original[1]) + @test isequal(Y(source), original[2]) + @test isequal(frequencies(source), original[3]) + end + end +end + +@testitem "Makie addons / uncertainty legends survive recreation and publication" tags=[:visual, :makie, :slow] begin + using CairoMakie, Measurements + + f = [1.0, 10.0, 100.0] + z = [measurement(i+2j+k, 0.1i+0.01j+0.001k) + im*measurement(i+j+k, 0.2) + for i in 1:3, j in 1:3, k in eachindex(f)] + parameters = LineParameters(z, z .* 1e-6, measurement.(f, 0.01f)) + options = ( + backend = :cairo, display_plot = false, controls = true, open_export = false, + length_unit = :base, quantity_units = :base, clip = false, fig_size = (1000, 700)) + pages = LineCableModels.plot(parameters; options..., ydata = (R,), layout = (2, 2), + series_labels = ("uncertain",), series_attributes = (marker = :circle,), legend_position = :bottom) + @test length.(getproperty.(pages, :axes)) == [4, 2, 2, 1] + pages = copy(pages) + push!(pages, + Makie.plot(parameters.Z, frequencies(parameters), (R, 1, 1, :); + options..., series_labels = ("uncertain",), legend_position = :bottom)) + # This exercises the publication renderer, not the matrix renderer used by + # the benchmark inspector. Both must honour the same uncertainty visibility rule. + artifact = report( + TableReportDefinition(((R, 1, 1, :),); illustration = true, + clip = false, + plot_options = (backend = :cairo, display_plot = false, + controls = true, open_export = false, + fig_size = (1000, 700), series_labels = ("uncertain",), legend_position = :bottom)), + parameters) + push!(pages, artifact.illustration) + single = Makie.plot(parameters.Y, frequencies(parameters), (G, 1, 1, :); options...) + @test single.legend === nothing + single_line = only(filter(p->p isa Makie.Lines, only(single.axes).scene.plots)) + single_line.visible[] = false + @test all(!p.visible[] for p in only(single.axes).scene.plots if p isa Makie.Errorbars) + single_line.visible[] = true + @test all(p.visible[] for p in only(single.axes).scene.plots if p isa Makie.Errorbars) + for page in pages + for position in (:right, :inside, :bottom) + legend = figurelegend!(page; position, max_fraction = 0.5) + entries = last(only(legend.entrygroups[])) + foreach(Makie.toggle_visibility!, entries) + @test all(!plot.visible[] for axis in page.axes + for plot in axis.scene.plots + if plot isa Union{Makie.Lines, Makie.Errorbars, Makie.Scatter}) + # Recreating a hidden legend must not resurrect its series or lose + # its visibility state. Glyph count is deliberately not an asserted requirement. + previous_legend = legend + legend = figurelegend!(page; position, max_fraction = 0.5, legend_labels = ("uncertain",)) + @test legend !== previous_legend + @test all(entry -> first(Makie.get_n_visible(entry)) == 0, + last(only(legend.entrygroups[]))) + foreach(entry -> Makie.toggle_visibility!(entry, true), last(only(legend.entrygroups[]))) + @test all(plot.visible[] for axis in page.axes + for plot in axis.scene.plots + if plot isa Union{Makie.Lines, Makie.Errorbars, Makie.Scatter}) + end + @test !isempty(Makie.colorbuffer(page.figure)) + end + page = last(pages) + axis = only(page.axes) + line = first(filter(p -> p isa Makie.Lines, axis.scene.plots)) + bar = first(filter(p -> p isa Makie.Errorbars, axis.scene.plots)) + bar.visible[] = false + @test line.visible[] # Independent native edits are still allowed. + figurelegend!(page; position = :bottom, max_fraction = 0.5, legend_labels = ("uncertain",)) + @test !bar.visible[] # Rebuilding the legend preserves the series visibility. + line.visible[] = false + @test !bar.visible[] + line.visible[] = true + @test bar.visible[] # A subsequent series action restores its components. + mktempdir() do directory + path = export_svg(page; path = joinpath(directory, "uncertainty.svg"), open_file = false) + @test filesize(path)>0 + end +end + +@testitem "Makie addons / uncertainty legend shading follows owners after relayout" tags=[:visual, :makie] begin + using CairoMakie, Measurements + + f = [1.0, 10.0, 100.0] + z = reshape(complex.(measurement.([1.0, 2.0, 3.0], 0.4), 1.0), 1, 1, :) + source = LineParameters(z, z .* 1e-6, f) + page = LineCableModels.plot( + source; ydata = (R,), backend = :cairo, display_plot = false, + controls = false, open_export = false, series_labels = ("uncertain",), + series_attributes = (marker = :circle,), legend_position = :bottom, fig_size = ( + 900, 600)) + function legend_pixels(page) + pixels = Makie.colorbuffer(page.figure) + bounds = page.legend.layoutobservables.computedbbox[] + viewport = page.figure.scene.viewport[] + sx, sy = size(pixels, 2)/viewport.widths[1], size(pixels, 1)/viewport.widths[2] + x1 = clamp(floor(Int, bounds.origin[1]*sx)+1, 1, size(pixels, 2)) + x2 = clamp(ceil(Int, (bounds.origin[1]+bounds.widths[1])*sx), x1, size(pixels, 2)) + y1 = clamp(floor(Int, (viewport.widths[2]-bounds.origin[2]-bounds.widths[2])*sy)+1, 1, size(pixels, 1)) + y2 = clamp(ceil(Int, (viewport.widths[2]-bounds.origin[2])*sy), y1, size(pixels, 1)) + return pixels[y1:y2, x1:x2] + end + shown = legend_pixels(page) + Makie.toggle_visibility!(first(last(only(page.legend.entrygroups[])))) + hidden = legend_pixels(page) + @test shown != hidden + figurelegend!(page; position = :bottom, max_fraction = 0.5, legend_labels = ("uncertain",)) + # Pixel comparison checks the actual shading listeners, not just the click + # target list. Replacing a hidden legend must preserve its rendered state. + @test legend_pixels(page) == hidden + Makie.toggle_visibility!(first(last(only(page.legend.entrygroups[]))), true) + @test legend_pixels(page) == shown + figurelegend!(page; position = :bottom, max_fraction = 0.5, joinstyle = :round) + @test !isempty(legend_pixels(page)) + axis = only(page.axes) + view = Makie.Rect2d(2.0, 1.0, 30.0, 1.0) + axis.targetlimits[] = view + figurelegend!(page; position = :bottom, max_fraction = 0.5) + # Reinitializing native legend shading is not a visibility change and + # must not reset a caller's current zoom and pan. + @test axis.targetlimits[] == view +end + +@testitem "Makie addons / overflowing uncertainty entries retain their owners" tags=[:visual, :makie] begin + using CairoMakie, Measurements + f = [1.0, 10.0, 100.0] + sources = Tuple(LineParameters(fill(measurement(Float64(i), 0.1)+im, 1, 1, 3), + fill(measurement(1e-6, 1e-8)+1e-6im, 1, 1, 3), f) for i in 1:24) + legend_bounds = Observable(Rect2f(0, 0, 300, 100)) + page = LineCableModels.plot( + sources...; ydata = (R,), backend = :cairo, display_plot = false, + controls = false, open_export = false, fig_size = (620, 340), legend_position = :inside, + legend_attributes = (bbox = legend_bounds,), legend_cap = 1.0, + series_labels = Tuple("Uncertain result $i" for i in 1:24)) + Makie.colorbuffer(page.figure) + entries = last(only(page.legend.entrygroups[])) + @test last(entries).label[] == "(...)" + resize!(page.figure, 1200, 950) + legend_bounds[] = Rect2f(0, 0, 300, 900) + Makie.colorbuffer(page.figure) + entries = last(only(page.legend.entrygroups[])) + @test last(entries).label[] == "Uncertain result 24" + # This entry was absent from the compact legend. Normalizing only visible + # entries would allow its old derived-child click targets to reappear. + Makie.toggle_visibility!(last(entries)) + axis = only(page.axes) + @test !last(filter(p -> p isa Makie.Lines, axis.scene.plots)).visible[] + @test !last(filter(p -> p isa Makie.Errorbars, axis.scene.plots)).visible[] + @test first(filter(p -> p isa Makie.Lines, axis.scene.plots)).visible[] + Makie.toggle_visibility!(last(entries), true) + @test all(p.visible[] + for p in axis.scene.plots if p isa Union{Makie.Lines, Makie.Errorbars}) +end + +@testitem "Makie / native data edits invalidate display support without source acquisition" tags=[:visual, :makie] begin + using CairoMakie, Measurements + frequency=collect(1.0:20.0) + values=measurement.(fill(2.0, 20), [fill(0.1, 19); 20.0]) + raw=LineParameters(reshape(complex.(values, values), 1, 1, :), fill(1+2im, 1, 1, 20), frequency) + observed=ObservedResult(raw, (R, X); clip = false, length_unit = :base) + p=LineCableModels.plot(observed; ydata = (R,), errorbar_sampling = :staggered, + backend = :cairo, display_plot = false, open_export = false) + axis=only(p.axes) + curve=only(filter(plot -> plot isa Makie.Lines, axis.scene.plots)) + bars=only(filter(plot -> plot isa Makie.Errorbars, axis.scene.plots)) + count=length(axis.scene.plots) + saved=deepcopy(observed.quantities) + native_intervals=copy(bars[1][]) + Makie.update!(curve, [1.0, 2.0, 3.0], [10.0, 20.0, 30.0]) + @test length(curve[1][])==3 + @test bars[1][]==native_intervals + resetview!(p) + @test sum((axis.targetlimits[].origin[2], axis.targetlimits[].widths[2]))>=30.0 + bars.visible[]=false + resetview!(p) + @test axis.targetlimits[].origin[2]>0 + for _ in 1:3 + Makie.resize!(p.figure, 850, 650) + Makie.resize!(p.figure, 900, 700) + end + @test !bars.visible[] + @test length(axis.scene.plots)==count + @test isequal(observed.quantities, saved) +end diff --git a/test/integration/native_makie_uq_comparisons.jl b/test/integration/native_makie_uq_comparisons.jl new file mode 100644 index 000000000..4cb7f9c7d --- /dev/null +++ b/test/integration/native_makie_uq_comparisons.jl @@ -0,0 +1,134 @@ +@testitem "Makie addons / observed UQ overlays retain uncertainty and reference styles" tags=[:visual, :makie, :slow] begin + using CairoMakie,Measurements,Statistics + using LineCableModels.ReportBuilder: BenchmarkTableDefinition + using LineCableModels.Engine: retain_gridpoint + using LineCableModels.Commons: gridpoint_id + f=collect(range(2.,8.;length=7)) + omega=reshape(2pi.*f,1,1,:) + include(joinpath(pkgdir(LineCableModels),"test/support/scenarios.jl")) + parts=NamedTuple{(:R,:L,:C,:G)}(Tuple([CurrentScenarios.channel_value(Val(q),i,j,k) + for i in 1:2,j in 1:2,k in eachindex(f)] for q in (:R,:L,:C,:G))) + summaries=map(a -> map(x -> SampleSummary([.9x,x,1.1x]),a),parts) + core=LineParameters(complex.(parts.R,omega.*parts.L),complex.(parts.G,omega.*parts.C),f) + mc=MonteCarloResult(MonteCarlo(Formulation();trials=3,seed=1), + [LineCableModels.materialize(core,summaries)],[summaries],nothing,nothing,UInt64(1),UInt64[2],[3]) + measured=map(a -> measurement.(1.05 .* a,0.03 .* a),parts) + source_id=gridpoint_id().source_id + results=[retain_gridpoint(LineParameters(factor.*complex.(measured.R,omega.*measured.L), + factor.*complex.(measured.G,omega.*measured.C),f),gridpoint_id(;source_id,problem_index=index); + fields=(inputs=(temperature=20index,),)) for (index,factor) in enumerate((1.,1.2))] + lep=LinearErrorResult(LinearError(Formulation()),results) + requests=(R,X,G,B,(statistics,R,mean),(statistics,R,std)) + artifact=report(BenchmarkTableDefinition(((statistics,R,mean),(statistics,R,std));bands=(:all,)), + (reference=mc,result=lep,context=(id=:benchmark_uq_title_probe,));requests, + observation_options=(length_unit=:base,)) + options=(backend=:cairo,display_plot=false,open_export=false) + page=LineCableModels.plot(artifact;ydata=((R,2,1,:),),problem=2,options..., + axis=(limits=((2.,8.),(.0005,.003)),),linewidth=3) + axis=only(page.axes) + @test axis.xlabelvisible[] && axis.xticklabelsvisible[] + @test page.export_name=="benchmark_uq_title_probe — Series resistance" + @test any(label -> occursin(description(LinearError;compact=true),label),values(page.plot_state.labels)) + @test any(label -> occursin(description(MonteCarlo;compact=true),label),values(page.plot_state.labels)) + @test any(label -> occursin(description(MonteCarlo,Val(:representation);compact=true),label),values(page.plot_state.labels)) + @test all(label -> !occursin("marginal_mean_std",label),values(page.plot_state.labels)) + recorded=first(artifact.observed) + undescribed=ObservedResult(merge(recorded.gridpoint,(uncertainty_descriptions=nothing,)), + recorded.quantities,recorded.errors,recorded.timings) + @test_throws ArgumentError LineCableModels.Commons.observation_labels([undescribed];request=R) + curves=filter(p -> p isa Makie.Lines,axis.scene.plots) + bars=filter(p -> p isa Makie.Errorbars,axis.scene.plots) + @test length(curves)==length(bars)==2 + @test Makie.to_color(last(curves).color[])==Makie.to_color(:black) + @test last(bars).whiskerwidth[] > first(bars).whiskerwidth[] > 0 + for (curve,bar,source) in zip(curves,bars,(lep[2],mc[1])) + @test last.(curve[1][])≈nominal.(R(source)[2,1,:]) + indices=[findfirst(==(point[1]),f) for point in bar[1][]] + @test getindex.(bar[1][],2)≈nominal.(R(source)[2,1,indices]) + @test getindex.(bar[1][],3)≈uncertainty.(R(source)[2,1,indices]) + group=only(filter(handles -> curve in handles,collect(values(page.plot_state.groups)))) + marker=only(filter(p -> p isa Makie.Scatter,group)) + @test isdisjoint(first.(marker[1][]),first.(bar[1][])) + @test curve.linewidth[]==bar.linewidth[]==3 + end + page.controls[:ylog].active[]=true + @test !isempty(Makie.colorbuffer(page.figure)) + @test axis.yscale[]===log10 + @test all(x -> x isa AbstractString,axis.yaxis.ticklabels[]) + full=LineCableModels.plot(artifact;ydata=((R,2,1,:),),problem=2,options...,errorbar_sampling=:all) + for key in full.plot_state.order + group=full.plot_state.groups[key] + @test length(only(filter(p -> p isa Makie.Errorbars,group))[1][])==length(f) + @test isempty(only(filter(p -> p isa Makie.Scatter,group))[1][]) + end + # Persisted observations retain the same uncertainty curves without sources. + mktempdir() do directory + path=LineCableModels.save(artifact,joinpath(directory,"observed.jls")) + restored=import_data(:observed,path) + for point in results;Z(point).=NaN;end + reloaded=report(BenchmarkTableDefinition(),restored.observed;reference=restored.reference) + other=LineCableModels.plot(reloaded;ydata=((R,2,1,:),),problem=2,options...) + @test other.plot_state.labels==page.plot_state.labels + other_curves=filter(p -> p isa Makie.Lines,only(other.axes).scene.plots) + for (left,right) in zip(curves,other_curves) + @test left[1][]==right[1][] + end + end +end + +@testitem "Makie addons / detached UQ statistics retain matrix coordinates and bands" tags=[:visual, :makie, :slow] begin + using CairoMakie,Statistics,Measurements,DataFrames + using LineCableModels.ReportBuilder: BenchmarkTableDefinition + f=10. .^ range(-1,7;length=13) + omega=reshape(2pi.*f,1,1,:) + include(joinpath(pkgdir(LineCableModels),"test/support/scenarios.jl")) + parts=NamedTuple{(:R,:L,:C,:G)}(Tuple([CurrentScenarios.channel_value(Val(q),i,j,k) + for i in 1:3,j in 1:3,k in eachindex(f)] for q in (:R,:L,:C,:G))) + stats=map(a -> map(x -> SampleSummary([.9x,1.1x]),a),parts) + core=LineParameters(complex.(parts.R,omega.*parts.L),complex.(parts.G,omega.*parts.C),f) + reference=MonteCarloResult(MonteCarlo(Formulation();trials=2,seed=7), + [LineCableModels.materialize(core,stats)],[stats],nothing,nothing,UInt64(7),UInt64[8],[2]) + measured=map(a -> measurement.(a,sqrt(2) * 0.1 .* a),parts) + result=LinearErrorResult(LinearError(Formulation()),[LineParameters( + complex.(measured.R,omega.*measured.L),complex.(measured.G,omega.*measured.C),measurement.(f,0.))]) + requests=((statistics,R,mean),(statistics,R,std),(statistics,B,mean)) + artifact=report(BenchmarkTableDefinition(requests;bands=(:all,:wide)), + (reference,result,context=(id=:benchmark_uq_statistics,));observation_options=(length_unit=:base,)) + options=(backend=:cairo,display_plot=false,controls=false,open_export=false) + pages=LineCableModels.plot(artifact;ydata=requests,layout=(2,2),options...,fig_size=(1100,750)) + @test length(pages)==12 + @test [length(page.axes) for page in pages]==repeat([4,2,2,1],3) + @test first(pages).export_name=="benchmark_uq_statistics — Series resistance · mean" + @test occursin("std",pages[5].export_name) + for (index,page) in enumerate(pages) + @test !isempty(Makie.colorbuffer(page.figure)) + request=requests[cld(index,4)] + expected=observe(reference,statistics,request[2],request[3],1) + for ((i,j),panel) in pairs(page.plot_state.panel_data) + curves=filter(p -> p isa Makie.Lines,panel.axis.scene.plots) + @test length(curves)==2 + @test first(curves)[1][]≈last(curves)[1][] + @test last.(last(curves)[1][])≈expected[i,j,:] + group=panel.groups[last(page.plot_state.order)] + markers=only(filter(p -> p isa Makie.Scatter,group)) + @test last(markers[1][])[1]≈last(f) + end + end + @test eltype(frequencies(only(result)))<:Measurement + @test nrow(artifact.tables.statistics)>0 + selection=(statistics,R,mean,[3,1],[2],2:2:12) + subset=LineCableModels.plot(artifact;ydata=(selection,),options...) + @test length(subset.axes)==2 + for panel in values(subset.plot_state.panel_data),curve in filter(p -> p isa Makie.Lines,panel.axis.scene.plots) + @test first.(curve[1][])≈f[2:2:12] + end + @test_throws ArgumentError LineCableModels.plot(artifact;ydata=(R,),options...) + @test any(label -> occursin(description(MonteCarlo;compact=true),label),values(first(pages).plot_state.labels)) + @test any(label -> occursin(description(LinearError;compact=true),label),values(first(pages).plot_state.labels)) + selected=LineCableModels.plot(artifact;ydata=requests[1:2],layout=(2,2),band=:wide,options...) + @test length(selected)==8 + for page in selected,panel in values(page.plot_state.panel_data),curve in filter(p -> p isa Makie.Lines,panel.axis.scene.plots) + @test all(>(1e6),first.(curve[1][])) + @test last(curve[1][])[1]≈last(f) + end +end diff --git a/test/integration/native_monte_carlo.jl b/test/integration/native_monte_carlo.jl new file mode 100644 index 000000000..250253977 --- /dev/null +++ b/test/integration/native_monte_carlo.jl @@ -0,0 +1,62 @@ +@testitem "Makie addons / native Monte Carlo verbs" tags=[:visual, :makie] setup=[ + UseNativePlotSupport, TestFixtures +] begin + get(ENV, "LINECABLEMODELS_TEST_PLOTTING", "false")=="true"|| + error("set LINECABLEMODELS_TEST_PLOTTING=true to run the visual test") + using CairoMakie, Measurements + + result=TestFixtures.cable_monte_carlo_result() + plots=( + Makie.hist( + result, + R; + backend = :cairo, + display_plot = false, + figure_title = "Retained samples", + panel_titles = ("Sample histogram",), + legend_title = "Distribution", + legend_labels = ("observations",), + legend_position = :inside, + legend_attributes=(halign=:left,valign=:top) + ), + Makie.stairs(result, R; backend = :cairo, display_plot = false), + Makie.ecdfplot(result, R; backend = :cairo, display_plot = false), + Makie.lines(result, R; backend = :cairo, display_plot = false), + Makie.qqplot(result, R; backend = :cairo, display_plot = false) + ) + @test all(plot -> plot isa UIPlot, plots) + @test all(plot -> length(plot.axes) == 1, plots) + @test all( + plot -> Set(keys(plot.controls)) == + Set((:reset, :export_svg, :legend, :xlog, :ylog)), plots) + @test all(plot -> plot.legend !== nothing, plots) + @test first(plots).title.text[] == "Retained samples" + @test only(first(plots).axes).title[] == "Sample histogram" + @test first(last(first(first(plots).legend.entrygroups[]))).label[] == + "observations" + @test length(last(plots).legend.entrygroups[][1][2]) == 2 + @test_throws ArgumentError Makie.qqplot( + result, R; qqline = :invalid, backend = :cairo, display_plot = false + ) +end + +@testitem "Makie addons / histogram bin requests reach the retained marginal" tags=[:visual, :makie] setup=[TestFixtures] begin + using CairoMakie, Measurements + result = TestFixtures.cable_monte_carlo_result() + retained = observe(result, histograms, R, 1, 1) + expected = HistogramDensity(observe(result, samples, R, 1, 1, :); bins=3) + plot = Makie.stairs(result, R; bins=3, backend=:cairo, display_plot=false, + controls=false, length_unit=:base, quantity_units=:base, clip=false) + native = only(filter(item -> item isa Makie.Stairs, only(plot.axes).scene.plots)) + @test first.(native[1][]) ≈ expected.edges + @test last.(native[1][]) ≈ [expected.density; last(expected.density)] + @test length(retained.density) == 2 + @test retained.edges == HistogramDensity(vec(only(result.sample_values).R);bins=2).edges + @test_throws ArgumentError Makie.stairs(result, R; bins=0, backend=:cairo, + display_plot=false) + without_samples = MonteCarloResult(result.formulation, result.values, result.stats, + nothing, result.histogram_values, result.root_seed, result.point_seeds, + result.trial_counts) + @test_throws r"return_samples=true" Makie.stairs(without_samples, R; bins=3, + backend=:cairo, display_plot=false) +end diff --git a/test/integration/observable_resolution_plots.jl b/test/integration/observable_resolution_plots.jl new file mode 100644 index 000000000..8d527a2a6 --- /dev/null +++ b/test/integration/observable_resolution_plots.jl @@ -0,0 +1,99 @@ +@testitem "Makie / physical resolution is shared by ordinary and benchmark plots" tags=[:visual, :makie, :slow] begin + using CairoMakie + using LinearAlgebra: diag + using LineCableModels.ReportBuilder: BenchmarkTableDefinition + f = [1.0, 1e3, 1e7] + z = fill(1.0 + im, 1, 1, 3) + y = fill(1e-13 + 1e-18im, 1, 1, 3) + reference = LineParameters(PhaseDomain, z, y, f; details=ComputationDetails(;coordinates=["a"],)) + result = LineParameters(PhaseDomain, 2z, 2y, f; details=ComputationDetails(;coordinates=["a"],)) + using LineCableModels.Engine: retain_gridpoint + using LineCableModels.Commons: gridpoint_id + reference=retain_gridpoint(reference,gridpoint_id()) + result=retain_gridpoint(result,gridpoint_id()) + options=(backend=:cairo,display_plot=false,controls=false,open_export=false) + curves(page)=filter(plot -> plot isa Makie.Lines,only(page.axes).scene.plots) + ordinates(page)=[last.(curve[1][]) for curve in curves(page)] + publication=report(BenchmarkTableDefinition(quantities=(G,B,X),clip=true), + (;reference,result);observation_options=(length_unit=:base,quantity_units=:base)) + ordinary=LineCableModels.plot(reference;ydata=(G,),length_unit=:base,options...) + clean=LineCableModels.plot(publication;ydata=(G,),options...) + @test all(iszero,only(ordinates(ordinary))) + @test length(curves(clean))==2 + @test all(values -> all(iszero,values),ordinates(clean)) + @test_throws ArgumentError LineCableModels.plot(publication;ydata=(G,),clip=false,options...) + raw=report(BenchmarkTableDefinition(quantities=(G,),clip=false),(;reference,result); + observation_options=(length_unit=:base,quantity_units=:base)) + rawpage=LineCableModels.plot(raw;ydata=(G,),options...) + @test first(ordinates(rawpage))≈2vec(Float32.(real.(y))) + @test last(ordinates(rawpage))≈vec(Float32.(real.(y))) + @test observe(reference,Y)==y + phase = LineCableModels.plot(reference; ydata=((Y, angle, 1, 1, :),), options...) + @test all(isnan, only(ordinates(phase))) + @test only(phase.axes).subtitle[] == "Undefined phase" + axisscale!(phase,:x,log10) + axisscale!(phase,:y,log10) + @test only(phase.axes).xscale[]===log10 + @test only(phase.axes).yscale[]===log10 + @test all(>(0),only(phase.axes).targetlimits[].origin) + resetview!(phase) + @test all(isfinite,only(phase.axes).targetlimits[].widths) + standalone = LineCableModels.plot(ShuntAdmittance(y), f; + ydata=((C, diag, :, :),), options...) + @test all(iszero, only(ordinates(standalone))) +end + +@testitem "Makie / clipping removes nominal residue and uncertainty without changing raw curves" tags=[:visual, :makie, :slow] begin + using CairoMakie, Measurements, Logging + f = 10.0 .^ range(-1, 7; length=13) + omega = reshape(2pi .* f, 1, 1, :) + options = (backend=:cairo, display_plot=false, controls=true, + length_unit=:base, quantity_units=:base, open_export=false, + errorbar_sampling=:all,fig_size=(900,500)) + # Independent means and spreads reproduce the noisy-zero and finite-baseline + # failures. Clipped values have zero uncertainty. Raw curves retain their bars. + for (means, spreads, clean_means, clean_spreads) in ( + (range(-1e-27, 2e-27; length=13), fill(4e-27, 13), zeros(13), zeros(13)), + (fill(-7e-10, 13) .+ (0:12) .* 1e-25, + fill(3e-25, 13), nothing, fill(3e-25,13)), + (fill(1e-27, 13), fill(4e-10, 13), zeros(13), zeros(13))) + c = reshape(measurement.(means, spreads), 1, 1, :) + source = LineParameters(one.(c) .+ im .* one.(c), im .* omega .* c, f) + before = deepcopy((Z(source), Y(source), frequencies(source))) + expected = clean_means === nothing ? nominal.(vec(observe(source, C))) : clean_means + for raw in (false, true) + logger = Test.TestLogger(min_level=Logging.Warn) + page = with_logger(logger) do + page = LineCableModels.plot(source, source; ydata=(C,), clip=!raw, options...) + Makie.colorbuffer(page.figure) + for _ in 1:2 + page.controls[:ylog].active[] = true + Makie.colorbuffer(page.figure) + page.controls[:ylog].active[] = false + page.controls[:reset].clicks[] += 1 + end + page + end + @test isempty(logger.logs) + axis = only(page.axes) + lines = filter(p -> p isa Makie.Lines, axis.scene.plots) + bars = filter(p -> p isa Makie.Errorbars, axis.scene.plots) + @test length(lines) == 2 + @test all(line -> last.(line[1][]) ≈ (raw ? nominal.(vec(c)) : expected), lines) + wanted_spread = raw ? vec(uncertainty.(c)) : clean_spreads + @test isempty(bars) == all(iszero, wanted_spread) + for bar in bars + @test [point[2] for point in bar[1][]] ≈ (raw ? nominal.(vec(c)) : expected) + @test [point[3] for point in bar[1][]] ≈ wanted_spread + @test [point[4] for point in bar[1][]] ≈ wanted_spread + end + if !raw && all(iszero, expected) && all(iszero, clean_spreads) + @test axis.finallimits[].origin[2] == -1 + @test axis.finallimits[].widths[2] == 2 + elseif clean_means === nothing + @test axis.finallimits[].widths[2] >= 0.099abs(first(expected)) + end + end + @test isequal((Z(source), Y(source), frequencies(source)), before) + end +end diff --git a/test/integration/observed_grouping.jl b/test/integration/observed_grouping.jl new file mode 100644 index 000000000..2e3548c08 --- /dev/null +++ b/test/integration/observed_grouping.jl @@ -0,0 +1,52 @@ +@testitem "ObservedResult / grouping requires physical identity and owned assumptions" tags=[:visual, :makie, :slow] begin + using CairoMakie,Measurements + using LineCableModels.Engine: retain_gridpoint,compare + using LineCableModels.Commons: gridpoint_id,observation_groups,observation_labels + using LineCableModels.ReportBuilder: BenchmarkTableDefinition,tabulate + source_id=gridpoint_id().source_id + base=LineParameters(fill(1.0+2im,1,1,3),fill(3.0+4im,1,1,3),[1.,10.,100.]) + function point(problem,formula;rho=100.,control=20.,value=base) + retain_gridpoint(value,gridpoint_id(;source_id,problem_index=problem,formulation_index=formula); + fields=(inputs=(radius=.01,earth_resistivity=rho), + selections=(Z=((:internal,:fixture,(accuracy=control,)),),Y=((:shunt,Symbol("formula",formula),(;)),)))) + end + first_point=point(1,1) + irrelevant=point(1,2) + different_input=point(2,1;rho=200.) + different_control=point(1,3;control=30.) + observed=observables([first_point,irrelevant,different_input,different_control]) + groups=observation_groups(observed;request=R) + @test getproperty.(groups,:members)==[[1,2],[3],[4]] + @test groups[1].identities==getproperty.([observed[1].gridpoint,observed[2].gridpoint],:id) + @test length(observation_groups(observed;request=G))==4 + @test occursin("earth_resistivity=200",observation_labels(observed)[3]) + @test all(point -> point.gridpoint.inputs.radius==.01,observed) + @test length(tabulate(observed))==4 + opts=(backend=:cairo,display_plot=false,controls=false,open_export=false) + page=LineCableModels.plot(observed;ydata=(R,),opts...) + @test page.plot_state.displayed_indices==[1,3,4] + @test only(page.plot_state.display_groups).groups==groups + @test length(page.plot_state.observed)==4 + @test length(filter(plot -> plot isa Makie.Lines,only(page.axes).scene.plots))==3 + @test_throws ArgumentError LineCableModels.plot(observed;ydata=(L,),opts...) + @test_throws ArgumentError LineCableModels.plot(observed;clip=false,opts...) + reference=retain_gridpoint(base,gridpoint_id()) + errors=compare(reference,[first_point,irrelevant,different_input],[R];bands=(:all,(10.,100.))) + report_points=observables([first_point,irrelevant,different_input];comparisons=errors) + artifact=report(BenchmarkTableDefinition(),report_points;reference=ObservedResult(reference)) + @test length(artifact.tables.features)==2 + @test all(feature -> length(feature.relative.formula)==1,artifact.tables.features) + overlay=LineCableModels.plot(artifact;ydata=(R,),problem=2,band=(10.,100.),opts...) + @test length(overlay.plot_state.observed)==2 + @test first(overlay.plot_state.observed).gridpoint.id.problem_index==2 + curves=filter(plot -> plot isa Makie.Lines,only(overlay.axes).scene.plots) + @test length(curves)==2 + @test all(curve -> length(curve[1][])==2,curves) + # Equal marginals do not establish the same dependency interpretation. + a=measurement(1.,.1);b=measurement(1.,.1) + uncertain(x)=LineParameters(fill(complex(x,2x),1,1,3),base.Y,[1.,10.,100.]) + independent=observables([point(1,1;value=uncertain(a)),point(1,2;value=uncertain(b))]) + @test length(observation_groups(independent;request=R))==2 + correlated=observables([point(1,1;value=uncertain(a)),point(1,2;value=uncertain(a))]) + @test length(observation_groups(correlated;request=R))==1 +end diff --git a/test/integration/persistence.jl b/test/integration/persistence.jl new file mode 100644 index 000000000..099d605c9 --- /dev/null +++ b/test/integration/persistence.jl @@ -0,0 +1,97 @@ +@testitem "ImportExport / CablesLibrary / versioned JSON and trusted JLS" tags=[:integration, :importexport] setup=[ + UseEngineSupport, + TestFixtures +] begin + design=TestFixtures.coaxial_design() + library=CablesLibrary() + add!(library, design) + reference=CableConstants(design) + + mktempdir() do directory + for extension in ("json", "jls") + destination=joinpath(directory, "cables.$extension") + @test save(library; file_name = destination) == destination + @test isfile(destination) + @test filesize(destination) > 0 + + restored=CablesLibrary() + @test load!(restored; file_name = destination) === restored + @test collect(keys(restored)) == collect(keys(library)) + restored_design=restored[design.cable_id] + @test restored_design !== design + @test CableConstants(restored_design) == reference + end + end +end + +@testitem "ImportExport / CablesLibrary / failures are atomic" tags=[:integration, :importexport] setup=[ + UseEngineSupport, + TestFixtures +] begin + using JSON3 + using Serialization + import LineCableModels.ImportExport as IE + + library=CablesLibrary() + design=TestFixtures.coaxial_design() + add!(library, design) + + mktempdir() do directory + function unchanged_after(path) + before=library.data + exception=try + load!(library; file_name = path) + nothing + catch caught + caught + end + @test exception !== nothing + @test library.data === before + @test library[design.cable_id] === design + return exception + end + + @test unchanged_after(joinpath(directory, "missing.json")) isa ArgumentError + + malformed=joinpath(directory, "malformed.json") + write(malformed, "{not valid json") + @test unchanged_after(malformed) isa Exception + + unsupported=joinpath(directory, "library.unsupported") + write(unsupported, "fixture") + @test unchanged_after(unsupported) isa ArgumentError + + invalid_jls=joinpath(directory, "invalid.jls") + serialize(invalid_jls, 42) + @test unchanged_after(invalid_jls) isa ArgumentError + + invalid_document=IE._json_document(library) + invalid_document["root"]["cables"][design.cable_id]["kind"]="material" + invalid_json=joinpath(directory, "invalid-entry.json") + open(invalid_json, "w") do io + JSON3.pretty(io, invalid_document) + end + @test unchanged_after(invalid_json) isa ArgumentError + + wrong_schema=IE._json_document(library) + wrong_schema["format"]=IE.MATERIALS_SCHEMA + schema_path=joinpath(directory, "wrong-schema.json") + open(schema_path, "w") do io + JSON3.pretty(io, wrong_schema) + end + @test unchanged_after(schema_path) isa ArgumentError + + malformed=joinpath(directory, "missing-fields.json") + write(malformed, "{\"data\":{}}") + before=library.data + @test_throws ArgumentError load!(library; file_name = malformed) + @test library.data === before + + @test_throws ArgumentError save( + library; file_name = joinpath(directory, "cables.archive") + ) + @test_throws SystemError save( + library; file_name = joinpath(directory, "absent", "cables.json") + ) + end +end diff --git a/test/integration/pscad/computation.jl b/test/integration/pscad/computation.jl new file mode 100644 index 000000000..90b6520e5 --- /dev/null +++ b/test/integration/pscad/computation.jl @@ -0,0 +1,179 @@ +@testitem "PSCAD / transport failure retains diagnostics and interruption cancels" tags=[:integration, :pscad] begin + const P = LineCableModels.PSCAD + using Logging + function P.remote_command(::Val{:local_failure_probe}, config::P.RemoteConfig, command::AbstractString) + script = occursin("-SharedCase", command) ? + "println(stderr, \"synthetic transport failure\"); exit(9)" : + "write(" * repr(joinpath(config.local_root, "canceled")) * ", \"canceled\")" + return `$(Base.julia_cmd()) --startup-file=no --project=@stdlib -e $script` + end + function P.remote_command(::Val{:local_interrupt_probe}, config::P.RemoteConfig, command::AbstractString) + # Model a transport command with default OS termination. Julia's native + # SIGTERM diagnostic handler can deadlock while printing its own stack. + script = "write(" * repr(joinpath(config.local_root, "started")) * + ", \"started\"); println(\"running\"); println(stderr, \"diagnostic\"); flush(stdout); flush(stderr); sleep(30)" + return `$(Base.julia_cmd()) --startup-file=no --handle-signals=no --project=@stdlib -e $script` + end + mktempdir() do root + config = P.RemoteConfig("local-fixture", root, "scratch", "julia", "python"; + local_root = root, transport = :local_failure_probe) + directory = mkpath(joinpath(root, "nested", "case")) + project = joinpath(directory, "generated.pscx") + write(project, "protocol fixture") + output = joinpath(directory, "outputs") + caught = try + P.run_remote_pscad(config, project, output, Formulation(:pscad), + 10.0.^range(-1, 5; length = 101); output_stem = "fixture") + nothing + catch error + error + end + @test caught isa ErrorException + @test occursin("Full PSCAD diagnostics:", sprint(showerror, caught)) + @test occursin("Transport stderr:", sprint(showerror, caught)) + @test read(joinpath(output, "transport-stderr.txt"), String) == "synthetic transport failure\n" + @test isfile(joinpath(root, "canceled")) + @test occursin("before the remote failure", read(joinpath(output, "pscad-console.txt"), String)) + @test_throws ArgumentError P.run_remote_pscad(config, project, output, + Formulation(:pscad), 10.0.^range(-1, 5; length = 101); output_stem = "fixture") + missing = joinpath(root, "missing.log") + @test P._diagnostic_tail(missing) == "PSCAD produced no diagnostic log." + write(missing, "\n \n") + @test P._diagnostic_tail(missing) == "PSCAD diagnostic log is empty." + write(missing, "one\n\ntwo\nthree\n") + @test P._diagnostic_tail(missing; count = 2) == "two\nthree" + end + for callback_fails in (false, true) + mktempdir() do root + config = P.RemoteConfig("local-fixture", root, "scratch", "julia", "python"; + local_root = root, transport = :local_interrupt_probe) + canceled = Ref(false) + callback = () -> begin + canceled[] = true + callback_fails && error("synthetic cancellation diagnostic") + end + log = Test.TestLogger(min_level=Logging.Debug) + task = with_logger(log) do + @async try + P._run_remote(config, "ignored"; + stdout_path = joinpath(root, "out.log"), + stderr_path = joinpath(root, "err.log"), + stream = true, on_interrupt = callback) + catch error + error + end + end + ready = timedwait(() -> isfile(joinpath(root, "started")) || istaskdone(task), 15) + @test ready === :ok + @test !istaskdone(task) + schedule(task, InterruptException(); error = true) + @test fetch(task) isa InterruptException + @test canceled[] + @test callback_fails == any(record -> occursin("cancellation could not be confirmed", + string(record.message)), log.logs) + end + end +end + +@testitem "PSCAD / completion verbosity and compile-call timing scope" tags=[:integration, :pscad] begin + using Logging + const P = LineCableModels.PSCAD + function P.remote_command(::Val{:local_completion_probe}, config::P.RemoteConfig, command::AbstractString) + output = joinpath(config.local_root, "case", "outputs") + script = "for name in (\"pscad-console.txt\", \"result_zm.out\", \"result_zp.out\", \"result_ym.out\", \"result_yp.out\"); write(joinpath(" * + repr(output) * ", name), \"fixture\"); end; write(joinpath(" * repr(output) * + ", \"timing.txt\"), \"0.0328471\")" + return `$(Base.julia_cmd()) --startup-file=no --project=@stdlib -e $script` + end + for level in 0:2 + mktempdir() do root + config = P.RemoteConfig("fixture", root, "scratch", "julia", "python"; + local_root=root, transport=:local_completion_probe) + directory = mkpath(joinpath(root, "case")) + project = joinpath(directory, "generated.pscx") + write(project, "protocol fixture") + log = Test.TestLogger(min_level=Logging.Debug) + result = with_logger(log) do + P.run_remote_pscad(config, project, joinpath(directory, "outputs"), + Formulation(:pscad), 10.0.^range(-1, 7; length=101); + output_stem="fixture", verbosity=level) + end + @test result.elapsed_seconds ≈ 0.0328471 + @test result.elapsed_scope == P.PSCAD_TIMING_SCOPE + completions = filter(record->record.message == "PSCAD frequency scan completed", log.logs) + if level < 2 + @test isempty(log.logs) + else + record = only(completions) + @test record.kwargs[:compile_call_seconds] ≈ 0.0328471 + @test record.kwargs[:timing_scope] == P.PSCAD_TIMING_SCOPE + @test !haskey(record.kwargs, :elapsed_seconds) + end + end + end +end + +@testitem "PSCAD / unsupported indexed equations fail without fallback" tags=[:integration, :pscad] begin + const P = LineCableModels.PSCAD + pscad = Formulation(:pscad) + selected = pscad.methods + for (equation, selection) in ((P.earth_impedance, selected.earth_impedance.air), + (P.earth_potential_coefficient, selected.earth_admittance)), + (kind, s, t) in ((:self, 1, 2), (:mutual, 2, 3), (:self, 3, 3)) + caught = try + equation(selection, Val(kind), Val(s), Val(t), pscad) + nothing + catch error + error + end + @test caught isa ArgumentError + @test occursin("source in layer $s and target in layer $t", sprint(showerror, caught)) + end + @test_throws ArgumentError P.internal_impedance(selected.internal_impedance, Val(:invalid), pscad) + @test_throws ArgumentError Formulation(:pscad; insulation_impedance=:not_registered) + for (s, t) in ((1, 2), (2, 1)) + @test P.earth_impedance(Formulation(:pscad; earth_impedance=:ametani2009).methods.earth_impedance, Val(:mutual), Val(s), Val(t), pscad) == + (EarthForm3 = (value = 0, readback = "AMETANIL"),) + @test P.earth_impedance(Formulation(:pscad; earth_impedance=:lucca1994).methods.earth_impedance, Val(:mutual), Val(s), Val(t), pscad) == + (EarthForm3 = (value = 2, readback = "LUCCA"),) + end + # PSCAD's coaxial cable admits coaxial topology only. + copper = Material(:conductor, 1.72e-8) + dielectric = Material(:insulator, 1e14, 2.3) + conductors = [terminal(name, solid(copper, Disk(0.005)), insulation(dielectric; t = 0.002)) + for name in (:a, :b)] + wall = terminal(:pipe, sheath(copper; t = 0.001), insulation(dielectric; t = 0.002)) + enclosed = build(CableDesign, "pscad-pipe", pipe(at(conductors[1], -0.01, 0), + at(conductors[2], 0.01, 0); shape = Disk(0.025), fill = Material(:insulator, Inf, 1.0), wall)) + coaxial = build(CableDesign, "pscad-coaxial", first(conductors)) + none = LineCableModels.Engine.PipeImpedance.Formula(:default) + @test validate(coaxial, none, pscad) === coaxial + @test_throws "not yet implemented for the PSCAD backend" validate(enclosed, none, pscad) +end + +@testitem "PSCAD / mixed native equations survive coaxial author withdrawal" tags=[:integration, :pscad, :slow] begin + const E = LineCableModels.Engine + const P = LineCableModels.PSCAD + selected = E.EarthImpedance.Formula(:ametani2009) + @test occursin("mixed", description(selected)) + pscad = Formulation(:pscad; earth_impedance=:ametani2009) + native = pscad.methods.earth_impedance + for (s,t) in ((1,2), (2,1)) + @test_throws r"not yet implemented" E.EarthImpedance.earth_impedance( + selected, Val(:mutual), Val(s), Val(t), nothing, nothing) + @test P.earth_impedance(native, Val(:mutual), Val(s), Val(t), pscad).EarthForm3.readback == "AMETANIL" + end + copper = Material(:conductor, 1.72e-8, 1.0) + design = build(CableDesign, "mixed-order", terminal(:core, + solid(copper, Disk(0.004)), insulation(Material(:insulator, 1e14, 2.3); t = 0.002))) + choices = (air = :carson1926, earth = :pollaczek1926, mixed = :ametani2009) + configuration = Formulation(earth_impedance = choices) + function problem(poses) + system = build(LineCableSystem, [design, design], poses; + connections = [Dict(:core => 1), Dict(:core => 2)]) + LineParametersProblem(system; earth_props = homogeneous(rho = 100.0), frequencies = [50.0]) + end + for poses in ([Pose2(0, 2), Pose2(0.75, -1)], [Pose2(0.75, -1), Pose2(0, 2)]) + @test_throws r"not yet implemented" compute(problem(poses),configuration) + end +end diff --git a/test/integration/pscad/configuration.jl b/test/integration/pscad/configuration.jl new file mode 100644 index 000000000..d3cff7a0e --- /dev/null +++ b/test/integration/pscad/configuration.jl @@ -0,0 +1,63 @@ +@testitem "PSCAD / user-owned TOML and transport arguments" tags=[:integration, :pscad] begin + using TOML, Base64 + const P = LineCableModels.PSCAD + mktempdir() do directory + filename = joinpath(directory, "chosen-station.toml") + fields = Dict{String, Any}( + "host" => "user@station", "local_root" => "not-created/exchange", + "shared_root" => raw"Z:\shared space\exchange", + "remote_root" => raw"C:\scratch space", "julia_executable" => raw"C:\Julia\julia.exe", + "python_executable" => raw"C:\Python\python.exe") + save() = open(io -> TOML.print(io, fields), filename, "w") + save() + station = P.RemoteConfig(filename) + @test station.local_root == joinpath(directory, "not-created", "exchange") + @test !ispath(station.local_root) + @test station.shared_root == fields["shared_root"] + @test station.remote_root == fields["remote_root"] + @test station.transport === :ssh + @test station.timeout == 1800 + @test isempty(station.command) + relative = cd(dirname(directory)) do + P.RemoteConfig(joinpath(basename(directory), basename(filename))) + end + @test relative.local_root == station.local_root + powershell = "Write-Output 'spaces and quotes'" + arguments = collect(P.remote_command(station, powershell)) + @test arguments[1:2] == ["ssh", fields["host"]] + @test occursin(powershell, transcode(String, reinterpret(UInt16, base64decode(last(arguments))))) + @test first(P.remote_command(Val(:local), station, powershell)) == "powershell.exe" + fields["transport"] = "local" + fields["command"] = String[] + save() + local_station = P.RemoteConfig(filename) + @test local_station.transport === :local + @test collect(P.remote_command(local_station, powershell)) == P._powershell_argv(powershell) + @test !ispath(local_station.local_root) + @test_throws ArgumentError validate(Dict("version"=>5), station) + fields["transport"] = "command" + fields["command"] = ["ts", "ssh", "{host}", "--direct", "-o", "ServerAliveInterval=15", "--"] + save() + station = P.RemoteConfig(filename) + arguments = collect(P.remote_command(station, powershell)) + @test arguments[1:7] == ["ts", "ssh", fields["host"], "--direct", "-o", "ServerAliveInterval=15", "--"] + @test arguments[8:end] == P._powershell_argv(powershell) + fields["command"] = ["wrapper path", "{host}", "literal-{host}", "\$(literal)"] + save() + @test collect(P.remote_command(P.RemoteConfig(filename), powershell))[1:4] == + ["wrapper path", fields["host"], "literal-{host}", "\$(literal)"] + for (key, value) in (("unknown", 1), ("timeout", true), + ("host", 42), ("local_root", ""), ("transport", 1), + ("command", "ts ssh"), ("command", [1]), ("command", String[])) + saved = copy(fields) + fields[key] = value + save() + @test_throws ArgumentError P.RemoteConfig(filename) + empty!(fields); merge!(fields, saved) + end + fields["transport"] = "ssh" + save() + @test_throws ArgumentError P.RemoteConfig(filename) + @test !ispath(joinpath(directory, "not-created")) + end +end diff --git a/test/integration/pscad/formula_ownership.jl b/test/integration/pscad/formula_ownership.jl new file mode 100644 index 000000000..f0ae2f058 --- /dev/null +++ b/test/integration/pscad/formula_ownership.jl @@ -0,0 +1,69 @@ +@testitem "PSCAD / Engine owns scientific selections and explicit defaults" tags=[:integration, :pscad] begin + const E = LineCableModels.Engine + const Expression = LineCableModels.Expression + selected = Formulation(:pscad) + @test selected.methods.internal_impedance isa E.InternalImpedance.Formula{:wedepohl1973} + @test selected.methods.insulation_impedance isa E.InsulationImpedance.Formula{:ametani1980} + @test selected.methods.earth_admittance isa E.EarthAdmittance.Formula{:ideal} + choices = (air=:carson1926, earth=:pollaczek1926, mixed=:lucca1994) + @test map(formula_id, selected.methods.earth_impedance) == choices + @test all(value -> value isa E.EarthImpedance.Formula, selected.methods.earth_impedance) + @test map(formula_id, Formulation(:pscad; + earth_impedance=(air=:default, earth=:default, mixed=:default)).methods.earth_impedance) == choices + @test keys(Formulation(:pscad; earth_impedance=(mixed=:default,)).methods.earth_impedance) == (:mixed,) + @test Formulation(:pscad; earth_impedance=(mixed=nothing,)).methods.earth_impedance.mixed === nothing + record = NamedTuple(selected) + @test map(value -> value.identifier, record.methods.earth_impedance) == choices + @test record.methods.earth_admittance.identifier === :ideal + label = description(selected, Z) + @test all(occursin(text, label) for text in ("air", "earth", "mixed", "Carson", "Pollaczek", "Lucca", "Wedepohl")) + @test !occursin("not yet implemented", label) + @test occursin("Ideal", description(selected, Y)) + for kind in (:inner,:outer,:transfer) + expression = Expression(selected.methods.internal_impedance, E.InternalImpedance.internal_impedance, Val(kind)) + @test expression(selected) == (;) + # The coaxial engine has no expression for Wedepohl's surfaces. + @test_throws "formula :wedepohl1973 has no expression for :$kind" LineCableModels.validate(expression) + end + @test_throws ArgumentError Formulation(:pscad; + internal_impedance=formula(:wedepohl1973; options=(integration=(method=:quad,),))) + @test_throws ArgumentError Formulation(:pscad; + earth_admittance=formula(:ideal; equivalent_earth=formula(:default))) + @test_throws ArgumentError Formulation(:pscad; + earth_admittance=formula(:ideal; options=(integration=(method=:quad,),))) +end + +@testitem "PSCAD / ideal potential registration and native selection limits" tags=[:integration, :pscad] begin + const E = LineCableModels.Engine + const P = LineCableModels.PSCAD + metal = Material(:conductor, 1.72e-8, 1.0) + dielectric = Material(:insulator, 1e14, 2.3) + design = build(CableDesign, "ideal-potential", terminal(:core, + core(metal; r=0.004), insulation(dielectric; t=0.002))) + system = build(LineCableSystem, fill(design, 4), + [Pose2(0,2), Pose2(1,3), Pose2(2,-1), Pose2(3,-2)]; + connections=[Dict(:core=>i) for i in 1:4]) + problem = LineParametersProblem(system; earth_props=homogeneous(rho=100.0), frequencies=[50.0]) + expected = Set((:ideal,kind,s,t) for (kind,s,t) in + ((:self,1,1),(:mutual,1,1),(:self,2,2),(:mutual,2,2),(:mutual,1,2),(:mutual,2,1))) + for choice in ((air=:carson1926,earth=:pollaczek1926,mixed=:lucca1994), + (air=:gary1976,earth=:wedepohl1973,mixed=:ametani2009), + (air=:gary1976,earth=:saad1996,mixed=:lucca1994)) + selected = Formulation(:pscad; earth_impedance=choice, earth_admittance=:ideal) + settings = P.pscad_setting(selected, problem) + @test Set((row.formula,row.kind,row.source,row.target) for row in settings.interactions.earth_admittance) == expected + for row in settings.interactions.earth_admittance + @test P.earth_potential_coefficient(selected.methods.earth_admittance, + Val(row.kind),Val(row.source),Val(row.target),selected) == (;) + end + end + ideal = E.EarthAdmittance.Formula(:ideal) + pscad = Formulation(:pscad) + @test_throws ArgumentError P.earth_potential_coefficient(ideal,Val(:self),Val(1),Val(2),pscad) + @test_throws ArgumentError P.earth_potential_coefficient(ideal,Val(:mutual),Val(2),Val(3),pscad) + @test_throws ArgumentError P.pscad_setting(Formulation(:pscad; earth_admittance=:pollaczek1926),problem) + @test_throws ArgumentError P.pscad_setting(Formulation(:pscad; internal_impedance=:schelkunoff1934),problem) + for id in (:ametani2009,:lucca1994), (kind,s,t) in ((:self,1,1),(:mutual,1,1),(:self,2,2),(:mutual,2,2),(:self,1,2)) + @test_throws ArgumentError P.earth_impedance(E.EarthImpedance.Formula(id),Val(kind),Val(s),Val(t),pscad) + end +end diff --git a/test/integration/pscad/native.jl b/test/integration/pscad/native.jl new file mode 100644 index 000000000..367f09a7b --- /dev/null +++ b/test/integration/pscad/native.jl @@ -0,0 +1,160 @@ +@testitem "PSCAD / real station repair acceptance" tags=[:pscad_native, :integration, :makie] begin + using TOML, SHA, CairoMakie, LinearAlgebra + using LineCableModels.ReportBuilder: BenchmarkTableDefinition + const P = LineCableModels.PSCAD + config_path = get(ENV, "LINECABLEMODELS_PSCAD_CONFIG", "") + isempty(config_path) && error("Set LINECABLEMODELS_PSCAD_CONFIG to your station TOML filename") + remote = P.RemoteConfig(config_path) + copper = Material(:conductor, 1.72e-8, 1.0, 1.0, 20.0, 0.004) + dielectric = Material(:insulator, 1e8, 2.3; tan_delta=0.03) + designs = [build(CableDesign,"native-wire-$i",terminal(:core, + core(copper;r=0.003 + i*0.0005),insulation(dielectric;t=0.002))) for i in 1:4] + function native_problem(order) + system = build(LineCableSystem,designs,[Pose2(0,2),Pose2(1,3),Pose2(2,-1),Pose2(3,-2)]; + connections=[Dict(:core=>i) for i in order],line_length=42.0,system_id="native-repair") + LineParametersProblem(system;earth_props=homogeneous(rho=100.0,eps_r=10.0), + temperature=60.0,frequencies=10.0 .^ range(log10(50.0),log10(1000.0);length=101)) + end + problem = native_problem([1,2,3,4]) + options = (timing=true, remote=remote,verbosity=(default=0,PSCAD=1)) + selected = Formulation(:pscad) + first_timing = @timed compute(problem,selected;options) + first_result = first_timing.value + next_timing = @timed compute(problem,selected;options) + next_result = next_timing.value + @test !details(first_result).data.execution.reused + @test !details(next_result).data.execution.reused + @test details(first_result).data.execution.source_run != details(next_result).data.execution.source_run + @test Z(first_result) == Z(next_result) + @test Y(first_result) == Y(next_result) + @test details(first_result).data.formulations.methods.earth_admittance.identifier === :ideal + @test details(first_result).data.formulations.methods.internal_impedance.identifier === :wedepohl1973 + @test map(value -> value.identifier, details(first_result).data.formulations.methods.earth_impedance) == + (air=:carson1926, earth=:pollaczek1926, mixed=:lucca1994) + # Independent electrostatic reference: air images plus each wire's insulation. + # Buried and mixed external coefficients are zero. Matrix output has 8-digit + # magnitudes. The relative comparison allows their printed rounding. + epsilon0 = 8.8541878128e-12 + radii = [0.003 + i*0.0005 for i in 1:4] + exterior = radii .+ 0.002 + heights = [2.0,3.0,-1.0,-2.0] + potential = Matrix(Diagonal(log.(exterior ./ radii) ./ (2pi * epsilon0 * 2.3))) + for i in 1:2, j in 1:2 + potential[i,j] += i == j ? log(2heights[i]/exterior[i])/(2pi*epsilon0) : + log(hypot(i-j, heights[i]+heights[j])/hypot(i-j, heights[i]-heights[j]))/(2pi*epsilon0) + end + for k in (1,51,101) + expected_y = (2pi*problem.frequencies[k]*im) .* inv(potential) + # Direct integration has a measured aerial conductance. Compare its + # capacitance here and retain the full complex output below. + @test imag.(Y(first_result)[:,:,k]) ≈ imag.(expected_y) rtol=1e-7 + @test iszero(Y(first_result)[3,4,k]) + @test iszero(Y(first_result)[1,3,k]) + end + for result in (first_result, next_result) + @test keys(details(result).data.timing) == (:wall_seconds, :compile_call_seconds) + @test all(>=(0), values(details(result).data.timing)) + @test !haskey(details(result).data.execution, :elapsed_seconds) + end + source = details(first_result).data.execution.source_run + source_hash = bytes2hex(open(sha256,joinpath(source,"outputs","result_zm.out"))) + reuse_timing = @timed compute(native_problem([3,1,4,2]),selected; + options=(;options...,resume_run_directory=source)) + reordered = reuse_timing.value + @test details(reordered).data.execution.reused + @test isempty(details(reordered).data.timing) + @test Z(reordered) == Z(first_result)[[2,4,1,3],[2,4,1,3],:] + @test Y(reordered) == Y(first_result)[[2,4,1,3],[2,4,1,3],:] + @test bytes2hex(open(sha256,joinpath(source,"outputs","result_zm.out"))) == source_hash + callbacks = Int[] + selections = [selected, + Formulation(:pscad;earth_impedance=(air=:carson1926,earth=:pollaczek1926,mixed=:lucca1994)), + Formulation(:pscad;earth_impedance=(air=:gary1976,earth=:wedepohl1973,mixed=:ametani2009)), + Formulation(:pscad;earth_impedance=(air=:gary1976,earth=:saad1996,mixed=:lucca1994), + insulation_admittance=:lossy)] + batch = compute(problem,selections;options=(;options...,resume_run_directory=:latest, + on_result=(p,i,r)->push!(callbacks,i))) + # The first two selections share native settings. Distinct settings require + # separate native outputs, which :latest may reuse from an earlier test run. + @test callbacks == collect(1:length(selections)) + @test isempty(details(batch[2]).data.timing) + @test all(result -> typeof(result) === typeof(first(batch)), batch) + @test Z(batch[1]) == Z(batch[2]) + @test Z(batch[1]) !== Z(batch[2]) + # The Gary/Wedepohl case agrees with strict ideal images. Direct integration + # departs from that law. The adapter must retain the measured conductance. + for k in (1,51,101) + expected_y = (2pi*problem.frequencies[k]*im) .* inv(potential) + @test Y(batch[3])[:,:,k] ≈ expected_y rtol=1e-7 + @test iszero(Y(batch[3])[3,4,k]) + @test iszero(Y(batch[3])[1,3,k]) + end + @test real(Y(first_result)[1,1,end]) > 0 + @test maximum(abs, real.(Y(batch[3]))) <= eps(Float64)*maximum(abs,Y(batch[3])) + for result in (first_result,next_result,reordered,batch...) + @test result isa LineParameters + @test size(Z(result)) == size(Y(result)) == (4,4,101) + @test all(isfinite,Z(result)) && all(isfinite,Y(result)) + @test frequencies(result) == problem.frequencies + @test details(result).data.execution.solver_identity["version"] == "5.1.0" + end + @test any(loss.requested > 0 for values in details(last(batch)).data.dielectric_losses for loss in values) + total = compute(problem,selected;options=(;options...,resume_run_directory=source,output_basis=:total)) + @test Z(total) == 42 .* Z(first_result) + @test Y(total) == 42 .* Y(first_result) + untimed = compute(problem,selected;options=(;options...,timing=false,resume_run_directory=source)) + @test !haskey(details(untimed).data, :timing) + @test details(untimed).data.execution.input_sha256 == details(first_result).data.execution.input_sha256 + @test Z(untimed) == Z(first_result) && Y(untimed) == Y(first_result) + observed = ObservedResult(first_result) + artifact = report(BenchmarkTableDefinition(),(reference=first_result,result=last(batch))) + @test !isempty(artifact.tables.formulations.label) + pages = LineCableModels.plot(artifact,(Z,);backend=:cairo,display_plot=false, + controls=false,open_export=false) + @test !isempty(pages) + for (index,page) in enumerate(pages) + CairoMakie.save(joinpath(remote.local_root,"native-$(basename(source))-$index.png"),page.figure) + end + evidence = Dict("transport"=>string(remote.transport), "source_run"=>source, + "first_compute_seconds"=>first_timing.time,"next_compute_seconds"=>next_timing.time, + "reuse_seconds"=>reuse_timing.time, + "first_compile_seconds"=>details(first_result).data.timing.compile_call_seconds, + "next_compile_seconds"=>details(next_result).data.timing.compile_call_seconds, + "runs"=>[details(result).data.execution.source_run for result in batch], + "ideal_deviation"=>Dict( + "frequency_hz"=>problem.frequencies[end], + "direct_y11_real_s_per_m"=>real(Y(first_result)[1,1,end]), + "direct_y11_imag_s_per_m"=>imag(Y(first_result)[1,1,end]), + "direct_y11_phase_degrees"=>rad2deg(angle(Y(first_result)[1,1,end])), + "gary_y11_phase_degrees"=>rad2deg(angle(Y(batch[3])[1,1,end])), + "direct_max_aerial_conductance_s_per_m"=>maximum(abs,real.(Y(first_result)[1:2,1:2,:])))) + evidence_path = joinpath(remote.local_root,"acceptance-$(basename(source)).toml") + open(io->TOML.print(io,evidence;sorted=true),evidence_path,"w") + println("Native PSCAD acceptance evidence: ",evidence_path) + println("First compute / next compute / reuse seconds: ", + (first_timing.time,next_timing.time,reuse_timing.time)) +end + +@testitem "PSCAD / real station expected identity precedes compilation" tags=[:pscad_native, :integration, :pscad] begin + const P = LineCableModels.PSCAD + remote = P.RemoteConfig(ENV["LINECABLEMODELS_PSCAD_CONFIG"]) + expected = P.identify(remote) + expected["line_constants_sha256"] = repeat("0",64) + metal = Material(:conductor,1.72e-8,1.0) + dielectric = Material(:insulator,1e14,2.3) + design = build(CableDesign,"identity-guard",terminal(:core, + core(metal;r=0.004),insulation(dielectric;t=0.002))) + system = build(LineCableSystem,[design],[Pose2(0,-1)];connections=[Dict(:core=>1)]) + problem = LineParametersProblem(system;earth_props=homogeneous(rho=100.0), + frequencies=10.0 .^ range(0,3;length=101)) + work_root = mktempdir(mkpath(remote.local_root);prefix="identity-rejection-",cleanup=false) + @test_throws r"does not match the expected solver identity" compute(problem,Formulation(:pscad); + options=(;timing=true,remote,work_root,solver_identity=expected)) + run_directory = only(readdir(work_root;join=true)) + @test !isfile(joinpath(run_directory,"complete.toml")) + console = read(joinpath(run_directory,"outputs","pscad-console.txt"),String) + @test count("Launching PSCAD 5.1.0",console) == 1 + @test !occursin("Starting PSCAD line-constants computation",console) + @test !isfile(joinpath(run_directory,"outputs","result_zm.out")) + println("Native identity rejection evidence: ",run_directory) +end diff --git a/test/integration/pscad/ordering.jl b/test/integration/pscad/ordering.jl new file mode 100644 index 000000000..92212c7d1 --- /dev/null +++ b/test/integration/pscad/ordering.jl @@ -0,0 +1,153 @@ +@testitem "PSCAD / matrix coordinates and complete native reuse keys" tags=[:integration, :measurements, :slow] begin + using TOML, SHA, Measurements + const P = LineCableModels.PSCAD + const launches = Ref(0) + const identity = Dict("schema"=>"1", "version"=>"5.1.0", "pscad_sha256"=>repeat("1",64), + "line_constants_sha256"=>repeat("2",64), "master_library_sha256"=>repeat("3",64)) + # Deliberately nonsymmetric protocol matrices, not scientific references. + function P.remote_command(::Val{:ordering_fixture}, ::P.RemoteConfig, command::AbstractString) + if occursin("-SharedCase", command) + launches[] += 1 + root = replace(only(match(r"-SharedCase '([^']+)'", command).captures), '\\'=>'/') + script = """ + using TOML + root = $(repr(root)) + input = TOML.parsefile(joinpath(root, "computation.toml")) + output = joinpath(root, "outputs") + n = input["matrix_size"][1] + identity = TOML.parse($(repr(sprint(TOML.print, identity)))) + get(input, "expected_solver", identity) == identity || error("unexpected identity") + for (name, scale) in (("zm",1.0), ("zp",0.0), ("ym",1e-6), ("yp",0.0)) + open(joinpath(output,"result_"*name*".out"),"w") do io + println(io,"LOG10(FN) FN matrix") + for (k,f) in enumerate(input["frequencies"]) + println(io,join((log10(f), f, (scale*(100r+10c+k) for r in 1:n for c in 1:n)...),' ')) + end + end + end + readback = Dict(name=>Dict(field=>control["readback"] for (field,control) in fields) + for (name,fields) in input["native_settings"]) + open(io->TOML.print(io,readback),joinpath(output,"native-settings.toml"),"w") + open(io->TOML.print(io,identity),joinpath(output,"solver.toml"),"w") + write(joinpath(output,"timing.txt"),"1.25") + write(joinpath(output,"pscad-console.txt"),"protocol fixture") + """ + else + script = "print(" * repr(sprint(TOML.print, identity)) * ")" + end + `$(Base.julia_cmd()) --startup-file=no --project=@stdlib -e $script` + end + copper = Material(:conductor,1.72e-8,1.0) + insulator = Material(:insulator,1e14,2.3) + coax = build(CableDesign,"coax", terminal(:core,core(copper;r=0.004),insulation(insulator;t=0.002)), + terminal(:sheath,sheath(copper;t=0.001),insulation(insulator;t=0.001))) + wire = build(CableDesign,"wire",terminal(:core,core(copper;r=0.003),insulation(insulator;t=0.002))) + function problem(connections) + system = build(LineCableSystem,[coax,wire],[Pose2(0,-1),Pose2(1,-1)]; + connections, line_length=42.0, system_id="ordering") + LineParametersProblem(system; earth_props=homogeneous(rho=100.0), + frequencies=10.0 .^ range(-1,5;length=101)) + end + permuted = problem([Dict(:core=>3,:sheath=>1),Dict(:core=>2)]) + natural = problem([Dict(:core=>1,:sheath=>2),Dict(:core=>3)]) + mktempdir() do directory + remote = P.RemoteConfig("fixture",directory,"scratch","julia","python"; + local_root=directory,transport=:ordering_fixture) + selected = Formulation(:pscad) + first_result = compute(permuted,selected;options=(;remote)) + @test Z(first_result)[1,2,1] == 231 + @test Z(first_result)[2,1,1] == 321 + @test Y(first_result)[1,3,1] ≈ 211e-6 + @test details(first_result).data.coordinates == ["cable:1:sheath","cable:2:core","cable:1:core"] + source = details(first_result).data.execution.source_run + raw_hash = bytes2hex(open(sha256,joinpath(source,"outputs","result_zm.out"))) + reused = compute(natural,selected;options=(;remote,resume_run_directory=source)) + @test launches[] == 1 + @test Z(reused)[1,2,1] == 121 + @test Y(reused)[3,1,1] ≈ 311e-6 + @test bytes2hex(open(sha256,joinpath(source,"outputs","result_zm.out"))) == raw_hash + total = compute(permuted,selected;options=(;remote,resume_run_directory=source,output_basis=:total)) + @test Z(total) == 42 .* Z(first_result) + @test Y(total) == 42 .* Y(first_result) + repeated = compute(permuted,selected;options=(;remote)) + @test launches[] == 2 + @test !details(repeated).data.execution.reused + @test details(repeated).data.execution.source_run != source + @test Z(repeated) == Z(first_result) + callbacks = Int[] + batch = compute(permuted,[selected,Formulation(:pscad;earth_impedance=:pollaczek1926)]; + options=(;remote,on_result=(p,i,r)->push!(callbacks,i))) + @test launches[] == 3 + @test callbacks == [1,2] + @test Z(batch[1]) == Z(batch[2]) && Z(batch[1]) !== Z(batch[2]) + @test Y(batch[1]) == Y(batch[2]) && Y(batch[1]) !== Y(batch[2]) + @test details(batch[1]).data.gridpoint.source_id == details(batch[2]).data.gridpoint.source_id + @test [details(result).data.gridpoint.formulation_index for result in batch] == [1,2] + @test details(batch[2]).data.formulations.requested.earth_impedance.identifier === :pollaczek1926 + near = [Formulation(:pscad;options=(base_frequency=f,)) for f in (50.000001,50.000002)] + project_inputs = [P._pscad_inputs(permuted,f,P._pscad_blueprints(permuted.system)) for f in near] + @test project_inputs[1].project == project_inputs[2].project + distinct = compute(permuted,near;options=(;remote)) + @test launches[] == 5 + @test !any(r->details(r).data.execution.reused,distinct) + @test [details(r).data.base_frequency for r in distinct] == [50.000001,50.000002] + @test_throws r"callback failed" compute(permuted,selected; + options=(;remote,on_result=(args...)->error("callback failed"))) + @test launches[] == 6 + grid = Formulation(:pscad;earth_impedance=Grid((:default,:pollaczek1926))) + traversed = compute(permuted,grid;options=(;remote)) + @test length(traversed) == 2 + @test Z(first(traversed)) == Z(first_result) + @test [details(result).data.gridpoint.formulation_index for result in traversed] == [1,2] + function realized_problem(radius) + T = typeof(radius) + design = build(CableDesign,"realization",terminal(:core, + core(Material(:conductor,convert(T,1.72e-8),1.0);r=radius), + insulation(insulator;t=0.002))) + system = build(LineCableSystem,[design],[Pose2(zero(T),-one(T))]; + connections=[Dict(:core=>1)]) + LineParametersProblem(system;earth_props=homogeneous(rho=100.0), + frequencies=natural.frequencies) + end + space = Gridspace{LineParametersProblem}(realized_problem, + (Grid((0.004,),AbsoluteError(0.0001)),)) + realized_types = Type[] + parametric = ParametricProblem(space,ComputationOptions((;remote, + on_result=(p,i,r)->push!(realized_types,eltype(p))))) + before = launches[] + deterministic = Gridspace{LineParametersProblem}(realized_problem,(Grid((0.004,)),)) + nominal = compute(ParametricProblem(deterministic,parametric.options),Combinatorial(selected)) + @test only(nominal) isa LineParameters + sampled = compute(parametric,MonteCarlo(selected;trials=2,seed=71,return_samples=true)) + @test sampled isa MonteCarloResult{<:LineParameters} + @test launches[] == before + 3 + @test length(realized_types) == 3 + @test all(T->!LineCableModels.Engine.has_uncertainty_type(T),realized_types) + @test_throws r"PSCAD does not support Measurement" compute(parametric,Combinatorial(selected)) + @test_throws r"PSCAD does not support Measurement" compute(parametric,LinearError(selected)) + @test launches[] == before + 3 + oversized = build(LineCableSystem,[coax,wire],[Pose2(0,-1),Pose2(1,-1)]; + connections=[Dict(:core=>1,:sheath=>2),Dict(:core=>3)],line_length=floatmax(Float64)) + overflowing = LineParametersProblem(oversized;earth_props=natural.earth_props, + frequencies=natural.frequencies) + overflow_callbacks = Int[] + @test_throws DomainError compute(overflowing,selected;options=(;remote,output_basis=:total, + on_result=(p,i,r)->push!(overflow_callbacks,i))) + @test isempty(overflow_callbacks) + # A completion from the former protocol never qualifies for new reuse. + old = joinpath(directory,"old-protocol"); mkpath(old) + open(io->TOML.print(io,Dict("schema_version"=>3)),joinpath(old,"complete.toml"),"w") + @test_throws r"fresh version-4" compute(permuted,selected;options=(;remote,resume_run_directory=old)) + before = launches[] + latest = compute(permuted,selected;options=(;remote,resume_run_directory=:latest)) + @test details(latest).data.execution.reused + @test details(latest).data.execution.source_run != old + @test launches[] == before + record_path = joinpath(source,"complete.toml") + corrupted = TOML.parsefile(record_path) + corrupted["request_sha256"] = repeat("0",64) + open(io->TOML.print(io,corrupted),record_path,"w") + @test_throws r"input integrity check failed" compute(permuted,selected; + options=(;remote,resume_run_directory=source)) + end +end diff --git a/test/integration/pscad/parser_tests.jl b/test/integration/pscad/parser_tests.jl new file mode 100644 index 000000000..aa1e99846 --- /dev/null +++ b/test/integration/pscad/parser_tests.jl @@ -0,0 +1,322 @@ +@testitem "PSCAD / parser and fixed mappings" tags=[:integration, :pscad] begin + pscad = Formulation(:pscad) + using Base64: base64decode + using Test + import LineCableModels + using LineCableModels: description + using LineCableModels.Engine + + harness=LineCableModels.PSCAD + overhead=Formulation(:pscad; earth_impedance = :gary1976) + underground=Formulation(:pscad; earth_impedance = :wedepohl1973) + @test overhead isa harness.PSCADFormulation + @test underground isa harness.PSCADFormulation + @test hasmethod(compute, Tuple{LineParametersProblem, harness.PSCADFormulation}) + @test harness.earth_impedance(Formulation(:pscad; earth_impedance=:gary1976).methods.earth_impedance, Val(:mutual), Val(1), Val(1), pscad) == + (EarthForm2 = (value = 0, readback = "DERISEMLYEN"),) + @test harness.earth_impedance(Formulation(:pscad; earth_impedance=:wedepohl1973).methods.earth_impedance, Val(:mutual), Val(2), Val(2), pscad) == + (EarthForm = (value = 0, readback = "WEDEPOHL"),) + @test EarthImpedance.formula_id(overhead.methods.earth_impedance) === :gary1976 + @test EarthImpedance.formula_id(underground.methods.earth_impedance) === + :wedepohl1973 + @test LineCableModels.formula_id(overhead.methods.earth_admittance) === :ideal + @test LineCableModels.formula_id(overhead.methods.insulation_admittance) === :lossless + @test occursin("lossless", lowercase(description(overhead.methods.insulation_admittance))) + # The selected owner supplies descriptions through current computation + # details. A removed consumer-local label helper is not an asserted requirement. + @test !isempty(string(computation_details(overhead))) + @test overhead.options.data == (reduce_bundle = false, kron_reduction = false, + ideal_transposition = false,base_frequency=50.0) + @test_throws ArgumentError Formulation(:pscad; options = (output_stem = "invalid",)) + + identifiers=(:gary1976, :carson1926, :pollaczek1926, :wedepohl1973, + :saad1996, :ametani2009, :lucca1994) + for identifier in identifiers, compact in (false, true) + selected = Formulation(:pscad; earth_impedance=identifier).methods.earth_impedance + @test description(selected; compact) == + description(EarthImpedance.Formula{identifier}; compact) + end + @test all( + id -> Formulation(:pscad; earth_impedance = LineCableModels.formula(id)) isa + harness.PSCADFormulation, + identifiers) + @test haskey(harness.earth_impedance(Formulation(:pscad; earth_impedance=:saad1996).methods.earth_impedance, Val(:mutual), Val(2), Val(2), pscad), :EarthForm) + @test harness.earth_impedance(Formulation(:pscad; earth_impedance=:lucca1994).methods.earth_impedance, Val(:mutual), Val(1), Val(2), pscad).EarthForm3.readback == "LUCCA" + @test harness.earth_impedance(Formulation(:pscad; earth_impedance=:carson1926).methods.earth_impedance, Val(:mutual), Val(1), Val(1), pscad).EarthForm2.value == 2 + @test harness.earth_impedance(Formulation(:pscad; earth_impedance=:pollaczek1926).methods.earth_impedance, Val(:mutual), Val(2), Val(2), pscad).EarthForm.value == 2 + @test_throws ArgumentError harness.earth_impedance(Formulation(:pscad; earth_impedance=:pollaczek1926).methods.earth_impedance, Val(:mutual), Val(1), Val(1), pscad) + + mktempdir() do directory + frequency=[10.0, 100.0] + magnitude=[.1*(1+2k+3i) for k in 1:2,i in 1:4] + phase=[5.0*(2k+i) for k in 1:2,i in 1:4] + ymag=[1e-6*(7+3k+i) for k in 1:2,i in 1:4] + yphase=[-3.0*(k+2i) for k in 1:2,i in 1:4] + function table(path, values) + open(path, "w") do io + println(io, " LOG10(FN) FN V11 V12 V21 V22") + for index in eachindex(frequency) + println( + io, + join( + (log10(frequency[index]), + frequency[index], values[index, :]...), ' ') + ) + end + end + end + table(joinpath(directory, "result_zm.out"), magnitude) + table(joinpath(directory, "result_zp.out"), phase) + table(joinpath(directory, "result_ym.out"), ymag) + table(joinpath(directory, "result_yp.out"), yphase) + result=harness.read_pscad_result(directory, frequency, (2, 2, 2)) + @test result isa LineParameters + @test size(Z(result)) == (2, 2, 2) + @test Z(result)[:, :, 1] ≈ permutedims(reshape(magnitude[1,:].*cispi.(phase[1,:]./180),2,2)) + @test Y(result)[:, :, 2] ≈ permutedims(reshape(ymag[2,:].*cispi.(yphase[2,:]./180),2,2)) + rounded_frequency=[10.0, 100.0*(1+4.0e-8)] + rounded_result=harness.read_pscad_result( + directory, rounded_frequency, (2, 2, 2) + ) + @test frequencies(rounded_result) == rounded_frequency + @test_throws ArgumentError harness.read_pscad_result( + directory, [1.0, 11.0], (2, 2, 2) + ) + mv(joinpath(directory, "result_zm.out"), joinpath(directory, "raw_zm.out")) + @test_throws ArgumentError harness.read_pscad_result(directory, frequency, ( + 2, 2, 2)) + end + + config=harness.RemoteConfig( + "host", + raw"Z:\pscad\benchmarks\.work", + raw"C:\pscad", + "julia", + "python"; local_root=mktempdir(), + transport = :ssh, + timeout = 60 + ) + owner=harness.PSCADFormulation + @test LineCableModels.formulation_options(owner, FormulationOptions()) == overhead.options + @test_throws ArgumentError LineCableModels.formulation_options( + owner, + FormulationOptions(output_stem = "case") + ) + @test_throws ArgumentError LineCableModels.computation_options(owner, ComputationOptions((;))) + execution=LineCableModels.computation_options(owner, ComputationOptions(( + output_stem = "case", + remote = config, + verbosity = (default = 0, PSCAD = 2), + output_basis = :total + ))) + for timing in (0, 1, nothing, :yes) + @test_throws ArgumentError LineCableModels.computation_options(owner, + ComputationOptions(; remote=config, timing)) + end + @test execution.data == ( + work_root=config.local_root, + output_stem = "case", + remote = config, + verbosity = (default = 0, PSCAD = 2), + timing = false, + output_basis = Val(:total), + on_result = nothing, + resume_run_directory = nothing, + solver_identity = nothing + ) + @test_throws ArgumentError LineCableModels.computation_options( + owner, ComputationOptions(( + output_stem = "benchmark_525kV_1600mm2_bipole_pscad", + remote = config + ))) + powershell="[IO.Directory]::CreateDirectory('C:\\pscad') | Out-Null" + command=harness.remote_command(config, powershell) + @test command.exec[1] == "ssh" + @test "host" in command.exec + @test "-EncodedCommand" in command.exec + encoded=command.exec[end] + bytes=base64decode(encoded) + decoded=transcode(String, ltoh.(collect(reinterpret(UInt16, bytes)))) + @test decoded == "\$ProgressPreference='SilentlyContinue'; $powershell" + @test !occursin("Out-Null", encoded) + @test harness._remote_project_name(raw"C:\pscad\case\generated.pscx") == + "generated" + root=config.local_root + @test LineCableModels.computation_options(owner, ComputationOptions((remote=config,work_root=joinpath(root,"any","depth")))).data.work_root == joinpath(root,"any","depth") + @test_throws ArgumentError LineCableModels.computation_options(owner, ComputationOptions((remote=config,work_root=dirname(root)))) + @test_throws ArgumentError harness.RemoteConfig( + "host", "shared", "remote", "julia", "python"; local_root=mktempdir(), timeout = 0 + ) + verbosity_error=try + harness.RemoteConfig( + "host", "shared", "remote", "julia", "python"; local_root=mktempdir(), verbosity = 2 + ) + nothing + catch error + error + end + @test verbosity_error isa ArgumentError + @test occursin("options=(verbosity=", sprint(showerror, verbosity_error)) + @test_throws ArgumentError harness._supervisor_command( + config, + raw"Z:\pscad\benchmarks\.work\case\current", + raw"C:\pscad\case\current", + "case", + overhead, + [1.0, 3.0, 10.0]; + output_stem = "pscad", + verbosity = 2 + ) + @test_throws ArgumentError validate( + collect(10.0 .^ range(0, stop = 6, length = 61)), harness.PSCADFormulation + ) + frequency_probe=collect(10.0 .^ range(0, stop = 6, length = 101)) + @test validate(frequency_probe, harness.PSCADFormulation) === frequency_probe + minimum_frequency_probe=collect(10.0 .^ range(-1, stop = 6, length = 101)) + @test validate(minimum_frequency_probe, harness.PSCADFormulation) === + minimum_frequency_probe + @test_throws DomainError validate( + collect(10.0 .^ range(-2, stop = 6, length = 101)), harness.PSCADFormulation + ) + supervisor_command=harness._supervisor_command( + config, + raw"Z:\pscad\benchmarks\.work\case\current", + raw"C:\pscad\case\current", + "generated", + overhead, + frequency_probe; + output_stem = "pscad", + verbosity = 2 + ) + @test occursin("supervisor.ps1", supervisor_command) + @test occursin(raw"Z:\pscad\benchmarks\.work\case\current", supervisor_command) + @test occursin(raw"C:\pscad\case\current", supervisor_command) + @test occursin("-OutputStem 'pscad'", supervisor_command) + @test occursin("-Verbosity '2'", supervisor_command) + @test occursin("-TimeoutSeconds '60'", supervisor_command) + @test !occursin("-EarthField", supervisor_command) + @test !occursin("OpenStandardInput", supervisor_command) + saad_command=harness._supervisor_command( + config, + raw"Z:\pscad\benchmarks\.work\case\current", + raw"C:\pscad\case\current", + "generated", + Formulation(:pscad; earth_impedance = :saad1996), + frequency_probe; + output_stem = "saad", + verbosity = 0 + ) + cancel_command=harness._cancel_command(raw"C:\pscad\case\current") + @test occursin("owner.txt", cancel_command) + @test occursin("taskkill.exe /PID", cancel_command) + @test occursin("CommandLine.IndexOf", cancel_command) + + mktempdir() do directory + run_directory=joinpath(directory, "case", "current") + output=joinpath(run_directory, "outputs") + project=joinpath(run_directory, "generated.pscx") + mkpath(run_directory) + write(project, "fixture") + staged=harness._stage_toolkit(project, output) + @test staged == joinpath(run_directory, "toolkit") + @test sort(readdir(staged)) == [ + "Manifest.toml", + "Project.toml", + "files.jl", + "identity.py", + "runner.jl", + "supervisor.ps1" + ] + for (name, source) in harness.PSCAD_REMOTE_SOURCES + @test read(joinpath(staged, name), String) == source + end + end + + mktempdir() do directory + partial=joinpath(directory, "fixture_zm.out") + write(partial, "header\n") + writer=@async begin + sleep(0.05) + open(partial, "a") do io + println(io, "0.0 1.0 2.0") + println(io, "1.0 10.0 3.0") + end + end + completed=harness._wait_output( + [directory], "_zm.out", 2; + timeout = 1, + poll_interval = 0.01 + ) + wait(writer) + @test completed == realpath(partial) + @test harness._data_rows(completed) == 2 + end + mktempdir() do directory + partial=joinpath(directory, "fixture_zm.out") + write(partial, "header\n") + error=try + harness._wait_output( + [directory], "_zm.out", 2; + timeout = 0.05, + poll_interval = 0.01 + ) + nothing + catch caught + caught + end + @test error isa ArgumentError + @test occursin("0 of 2 rows", sprint(showerror, error)) + end + + function harness.remote_command( + ::Val{:fixture_fail}, config::harness.RemoteConfig, powershell::AbstractString + ) + Cmd(["julia", "--startup-file=no", "-e", "exit(7)"]) + end + function harness.remote_command( + ::Val{:fixture_logs}, + config::harness.RemoteConfig, + powershell::AbstractString + ) + executable=Base.julia_cmd().exec[1] + return Cmd([ + executable, + "--startup-file=no", + "-e", + "println(\"runner output\"); println(stderr, \"runner error\")" + ]) + end + failed_config=harness.RemoteConfig( + "host", "shared", raw"C:\pscad", "julia", "python"; local_root=mktempdir(), + transport = :fixture_fail + ) + mktempdir() do directory + stdout_path=joinpath(directory, "stdout.txt") + stderr_path=joinpath(directory, "stderr.txt") + @test_throws ErrorException harness._run_remote( + failed_config, "ignored"; stdout_path, stderr_path + ) + @test isfile(stdout_path) + @test isfile(stderr_path) + end + mktempdir() do directory + logs_config=harness.RemoteConfig( + "host", "shared", raw"C:\pscad", "julia", "python"; local_root=mktempdir(), + transport = :fixture_logs + ) + stdout_path=joinpath(directory, "stdout.txt") + stderr_path=joinpath(directory, "stderr.txt") + output=harness._run_remote( + logs_config, "ignored"; stdout_path, stderr_path + ) + @test occursin("runner output", output) + @test read(stdout_path, String) == "runner output\n" + @test read(stderr_path, String) == "runner error\n" + end + + for count in (101,201,501,1001), T in (Float32,Float64) + f=T.(10.0.^range(-1,6;length=count)) + @test validate(f, harness.PSCADFormulation) === f + end + @test_throws ArgumentError validate(collect(range(.1,1e6;length=201)), harness.PSCADFormulation) +end diff --git a/test/integration/pscad/preflight.jl b/test/integration/pscad/preflight.jl new file mode 100644 index 000000000..16aa09e0e --- /dev/null +++ b/test/integration/pscad/preflight.jl @@ -0,0 +1,79 @@ +@testitem "PSCAD / deterministic numeric validation precedes staging and transport" tags=[:integration, :measurements] begin + using Measurements, Calculus, Serialization + const P = LineCableModels.PSCAD + const contacts = Ref(0) + function P.remote_command(::Val{:preflight_probe}, ::P.RemoteConfig, ::AbstractString) + contacts[] += 1 + error("preflight contacted transport") + end + function model(; radius=0.004, rho=1.72e-8, soil=100.0, temperature=20.0, + frequency=10.0 .^ range(-1, 5; length=101)) + # Materialize the system at the problem's numeric type. This test targets + # PSCAD numeric input construction, not the shared constructor's conversion methods. + T = promote_type(typeof(radius), typeof(rho), typeof(soil), typeof(temperature), eltype(frequency)) + design = build(CableDesign, "numeric-preflight", terminal(:core, + solid(Material(:conductor, convert(T,rho), 1.0), Disk(convert(T,radius))), + insulation(Material(:insulator, 1e14, 2.3); t=0.002))) + system = build(LineCableSystem, [design], [Pose2(zero(T), -one(T))]; connections=[Dict(:core=>1)]) + LineParametersProblem(system; earth_props=homogeneous(rho=soil), temperature, frequencies=frequency) + end + mktempdir() do directory + root = joinpath(directory, "untouched") + remote = P.RemoteConfig("fixture", "shared", "scratch", "julia", "python"; + local_root=root, transport=:preflight_probe) + selected = Formulation(:pscad) + for sigma in (0.0, 0.001) + uncertain(x) = measurement(x, abs(x) * sigma) + for inputs in ((radius=uncertain(0.004),), (rho=uncertain(1.72e-8),), + (soil=uncertain(100.0),), (temperature=uncertain(20.0),), + (frequency=uncertain.(10.0 .^ range(-1, 5; length=101)),)) + problem = model(; inputs...) + @test_throws r"PSCAD does not support Measurement" validate(problem, selected) + @test_throws r"PSCAD does not support Measurement" compute(problem, selected; options=(;remote)) + @test !ispath(root) + @test_throws r"PSCAD does not support Measurement" export_data(:pscad, + problem.system, problem.earth_props; temperature=problem.temperature, + file_name=joinpath(root, "invalid.pscx")) + end + @test_throws r"PSCAD does not support Measurement" Formulation(:pscad; + options=(base_frequency=uncertain(50.0),)) + plain = model() + @test_throws r"PSCAD does not support Measurement" export_data(:pscad, + plain.system, plain.earth_props; base_freq=uncertain(50.0), + file_name=joinpath(root, "invalid.pscx")) + end + @test_throws ArgumentError compute(model(frequency=[50.0]), selected; options=(;remote)) + @test_throws ArgumentError compute(model(), [selected, Formulation(:pscad; earth_impedance=:gary1976)]; options=(;remote)) + plain = model() + snapshot(value) = (io=IOBuffer(); serialize(io,value); take!(io)) + original = snapshot(plain) + @test validate(plain,selected) === plain + @test snapshot(plain) == original + metal = Material(:conductor,1.72e-8,1.0) + dielectric = Material(:insulator,1e14,2.3) + design = build(CableDesign,"five-components",(terminal(Symbol("component$i"), + i == 1 ? core(metal;r=0.004) : sheath(metal;t=0.001), + insulation(dielectric;t=0.002)) for i in 1:5)...) + system = build(LineCableSystem,[design],[Pose2(0,-1)]; + connections=[Dict(name=>i for (i,name) in enumerate(design.terminal_order))]) + excessive = LineParametersProblem(system;earth_props=plain.earth_props, + frequencies=plain.frequencies) + @test_throws ArgumentError compute(excessive,selected;options=(;remote)) + @test !ispath(root) + @test contacts[] == 0 + end +end + +@testitem "PSCAD / caller deadline terminates a silent transport" tags=[:integration, :pscad] begin + const P = LineCableModels.PSCAD + function P.remote_command(::Val{:deadline_probe}, ::P.RemoteConfig, ::AbstractString) + `$(Base.julia_cmd()) --startup-file=no --handle-signals=no --project=@stdlib -e 'sleep(30)'` + end + mktempdir() do directory + remote = P.RemoteConfig("fixture", "shared", "scratch", "julia", "python"; + local_root=directory, transport=:deadline_probe) + started = time() + @test_throws r"transport exceeded its timeout" P._run_remote(remote, "unused"; timeout=0.5) + @test time() - started < 10 + end +end diff --git a/test/integration/pscad/pscad_formulation_tests.jl b/test/integration/pscad/pscad_formulation_tests.jl new file mode 100644 index 000000000..289148143 --- /dev/null +++ b/test/integration/pscad/pscad_formulation_tests.jl @@ -0,0 +1,188 @@ +@testitem "PSCAD / shared grammar and constitutive export" tags=[:integration, :pscad] begin + using LineCableModels + const P=LineCableModels.PSCAD + copper=Material(:conductor, 1.72e-8, 1, 1, 20, 0.004) + semicon=Material(:semicon, 1e4, 40; tan_delta = 0.02) + dielectric=Material(:insulator, 1e8, 2.3; tan_delta = 0.03) + design=build(CableDesign, + "pscad-formula-probe", + terminal(:core, + solid(copper, Disk(0.004)), screen(semicon; t = 0.0005), + insulation(dielectric; t = 0.002))) + earth=homogeneous(rho = 100.0) + problem(height)=LineParametersProblem( + build(LineCableSystem, [design], [Pose2(0, height)]; + connections = [Dict(:core=>1)]); + earth_props = earth, + temperature = 60, + frequencies = collect(10.0 .^ range(-1, 6; length = 101))) + overhead, underground=problem(1.0), problem(-1.0) + @test validate(overhead, Formulation(:pscad)) === overhead + @test validate(underground, Formulation(:pscad)) === underground + # The internal slot takes one formula. PSCAD's default is Wedepohl's. + @test_throws MethodError Formulation(:pscad; + internal_impedance=(transfer=:default,inner=:default,outer=:default)) + @test LineCableModels.computation_details(Formulation(:pscad)).data.methods.internal_impedance.identifier === + :wedepohl1973 + @test_throws ArgumentError P.pscad_setting( + Formulation(:pscad;internal_impedance=:schelkunoff1934),underground) + @test_throws MethodError LineCableModelsFEM(internal_impedance=(inner=:default,outer=:default,transfer=:default)) + for selected_problem in (overhead, underground) + resolved=Formulation(:pscad) + @test map(formula_id, resolved.methods.earth_impedance) == + (air=:carson1926, earth=:pollaczek1926, mixed=:lucca1994) + @test formula_id(resolved.definitions.earth_impedance) === :default + @test all(control -> control.value == 2, P.pscad_setting(resolved, selected_problem).ground) + end + @test only(P.pscad_setting(Formulation(:pscad), overhead).interactions.earth_impedance).source == 1 + @test only(P.pscad_setting(Formulation(:pscad), underground).interactions.earth_impedance).source == 2 + @test_throws ArgumentError P.pscad_setting( + Formulation(:pscad; earth_impedance = :gary1976), underground) + @test_throws ArgumentError P.pscad_setting( + Formulation(:pscad; earth_impedance = :carson1926), underground) + @test_throws ArgumentError P.pscad_setting( + Formulation(:pscad; earth_properties = :cigre2019), underground) + @test_throws r"not yet implemented" compute(underground, Formulation(earth_impedance = :pollaczek1926)) + @test P.pscad_setting(Formulation(:pscad; earth_impedance=:pollaczek1926), underground).ground == + P.pscad_setting(Formulation(:pscad), underground).ground + for key in keys(Formulation(:pscad).definitions) + selection=NamedTuple{(key,)}((Grid((formula(:default), formula(:default))),)) + space=Formulation(:pscad; selection...) + @test space isa Gridspace{P.PSCADFormulation} + @test length(space) == 2 + end + product=Formulation(:pscad; earth_impedance = Grid((:default, :saad1996)), + insulation_admittance = Grid((:default, :lossy))) + zipped=Formulation(:pscad; earth_impedance = Grid((:default, :saad1996)), + insulation_admittance = Grid((:default, :lossy)), combine = :zip) + @test length(product) == 4 + @test length(zipped) == 2 + + # Native reuse is determined by exported numerical inputs plus solver setting, + # not by whether the requested selector happened to be :default. + @test_throws ArgumentError compute(underground, P.PSCADFormulation[]) + requested=(Formulation(:pscad), + Formulation(:pscad; earth_impedance = :pollaczek1926), + Formulation(:pscad; insulation_admittance = :lossy)) + resolved=requested + root=mktempdir() + staged_projects=[P._stage_pscad_project(P._pscad_inputs(underground, value, P._pscad_blueprints(underground.system)), root) for value in resolved] + try + @test length(unique(getproperty.(staged_projects, :root))) == 3 + @test all(project -> dirname(project.root) == root,staged_projects) + projects=[read(value.staged, String) for value in staged_projects] + @test projects[1] == projects[2] + @test projects[1] != projects[3] + @test P.pscad_setting(resolved[1], underground).ground == + P.pscad_setting(resolved[2], underground).ground + @test P.pscad_setting(Formulation(:pscad; earth_impedance = :saad1996), underground) != + P.pscad_setting(resolved[1], underground) + finally + foreach(value->rm(value.root; recursive = true), staged_projects) + end + + @test NamedTuple(Formulation(:pscad)).methods.internal_impedance.identifier === :wedepohl1973 + baseline=LineCableModels.computation_details(Formulation(:pscad)) + alternative=LineCableModels.computation_details(Formulation(:pscad; + earth_impedance = :saad1996, insulation_admittance = :lossy)) + @test_throws ArgumentError Formulation(:pscad; + earth_impedance = formula(:default; equivalent_earth = formula(:default))) + @test typeof(baseline) === typeof(alternative) + result=LineParameters(PhaseDomain, zeros(ComplexF64, 1, 1, 1), + zeros(ComplexF64, 1, 1, 1), [50.0]; details = ComputationDetails(;formulations = baseline.data,)) + @test computation_details(typeof(Formulation(:pscad)), result) === details(result) +end + +@testitem "PSCAD / native settings exist for the supported formulas only" tags=[:integration, :pscad] begin + const P=LineCableModels.PSCAD + const EI=LineCableModels.Engine.EarthImpedance + const EA=LineCableModels.Engine.EarthAdmittance + pscad=Formulation(:pscad) + # The registered formulas that PSCAD supports for each interaction. + supported=( + (EI, P.earth_impedance, (:self, 1, 1), (:carson1926, :gary1976)), + (EI, P.earth_impedance, (:mutual, 1, 1), (:carson1926, :gary1976)), + (EI, P.earth_impedance, (:self, 2, 2), (:pollaczek1926, :saad1996, :wedepohl1973)), + (EI, P.earth_impedance, (:mutual, 2, 2), (:pollaczek1926, :saad1996, :wedepohl1973)), + (EI, P.earth_impedance, (:mutual, 1, 2), (:ametani2009, :lucca1994)), + (EI, P.earth_impedance, (:mutual, 2, 1), (:ametani2009, :lucca1994)), + (EI, P.earth_impedance, (:self, 1, 2), ()), + (EI, P.earth_impedance, (:mutual, 2, 3), ()), + (EA, P.earth_potential_coefficient, (:self, 1, 1), (:ideal,)), + (EA, P.earth_potential_coefficient, (:mutual, 1, 1), (:ideal,)), + (EA, P.earth_potential_coefficient, (:self, 2, 2), (:ideal,)), + (EA, P.earth_potential_coefficient, (:mutual, 2, 2), (:ideal,)), + (EA, P.earth_potential_coefficient, (:mutual, 1, 2), (:ideal,)), + (EA, P.earth_potential_coefficient, (:mutual, 2, 1), (:ideal,)), + (EA, P.earth_potential_coefficient, (:self, 1, 2), ()), + (EA, P.earth_potential_coefficient, (:mutual, 2, 3), ())) + for (owner, equation, (kind, source, target), expected) in supported, + identifier in owner.formulas(owner.Formula) + selectors=(Val(kind), Val(source), Val(target)) + if identifier in expected + @test equation(owner.Formula(identifier), selectors..., pscad) isa NamedTuple + else + @test_throws ArgumentError equation(owner.Formula(identifier), selectors..., pscad) + end + end +end + +@testitem "PSCAD / consumes complete homogeneous choices and preserves export settings" tags=[:integration, :pscad] begin + using EzXML + const P = LineCableModels.PSCAD + const E = LineCableModels.Engine + copper = Material(:conductor, 1.72e-8, 1, 1, 20, 0.004) + design = build(CableDesign, "mixed-native-fixture", terminal(:core, + solid(copper, Disk(0.004)), insulation(Material(:insulator, 1e14, 2.3); t = 0.002))) + system = build(LineCableSystem, [design, design], [Pose2(0, 2), Pose2(1, -1)]; + connections = [Dict(:core => 1), Dict(:core => 2)]) + problem = LineParametersProblem(system; temperature = 60, + earth_props = homogeneous(rho = 100.0), frequencies = [50.0]) + choices = (air = :gary1976, earth = :saad1996, mixed = :lucca1994) + selected = Formulation(:pscad; earth_impedance = choices) + setting = P.pscad_setting(selected, problem) + @test map(control -> control.value, setting.ground) == (EarthForm2 = 0, EarthForm = 3, EarthForm3 = 2) + @test Set((r.formula, r.kind, r.source, r.target) for r in setting.interactions.earth_impedance) == + Set(((:gary1976, :self, 1, 1), (:saad1996, :self, 2, 2), + (:lucca1994, :mutual, 1, 2), (:lucca1994, :mutual, 2, 1))) + @test_throws ArgumentError P.pscad_setting( + Formulation(:pscad; earth_impedance = :lucca1994), problem) + @test_throws ArgumentError P.pscad_setting( + Formulation(:pscad; earth_admittance = :pollaczek1926), problem) + for invalid_choices in ((air = :pollaczek1926, earth = :saad1996, mixed = :lucca1994), + (air = :gary1976, earth = :carson1926, mixed = :lucca1994)) + @test_throws ArgumentError P.pscad_setting( + Formulation(:pscad; earth_impedance = invalid_choices), problem) + end + @test_throws ArgumentError P.pscad_setting( + Formulation(:pscad; earth_impedance = ( + air = formula(:gary1976; options = (integration = (method = :quad,),)), + earth = :saad1996, mixed = :lucca1994)), problem) + staged = P._stage_pscad_project(P._pscad_inputs(problem, selected, P._pscad_blueprints(problem.system)), mktempdir()) + try + document = EzXML.readxml(staged.staged) + ground = only(EzXML.findall("//User[@defn='master:Line_Ground']", document)) + fields = Dict(node["name"] => node["value"] for node in EzXML.findall("./paramlist/param", ground)) + @test all(parse(Float64, fields[string(name)]) == control.value for (name, control) in pairs(setting.ground)) + frequency = only(EzXML.findall("//User[@defn='master:Line_FrePhase_Options']", document)) + fields = Dict(node["name"] => node["value"] for node in EzXML.findall("./paramlist/param", frequency)) + @test parse(Float64, fields["enablf"]) == 1 + for cable in EzXML.findall("//User[@defn='master:Cable_Coax']", document) + fields = Dict(node["name"] => node["value"] for node in EzXML.findall("./paramlist/param", cable)) + @test parse(Float64, fields["RHOC"]) ≈ 1.72e-8 * (1 + 0.004 * 40) + @test parse(Float64, fields["LT1"]) == 0 + end + finally + rm(staged.root; recursive = true) + end + record = NamedTuple(selected) + @test map(value -> value.identifier,record.requested.earth_impedance) == choices + @test record.methods.earth_admittance.identifier === :ideal + @test record.requested.earth_impedance.air.identifier === :gary1976 + vertical = LineParametersProblem(build(LineCableSystem, [design, design], + [Pose2(0, -1), Pose2(0, -2)]; connections = [Dict(:core => 1), Dict(:core => 2)]); + earth_props = homogeneous(rho = 100.0), frequencies = [50.0]) + native = P.pscad_setting(Formulation(:pscad; earth_impedance = :wedepohl1973), vertical) + @test native.ground.EarthForm.readback == "WEDEPOHL" + @test Set(row.kind for row in native.interactions.earth_impedance) == Set((:self, :mutual)) +end diff --git a/test/integration/pscad/pscad_resume_tests.jl b/test/integration/pscad/pscad_resume_tests.jl new file mode 100644 index 000000000..ddb401a43 --- /dev/null +++ b/test/integration/pscad/pscad_resume_tests.jl @@ -0,0 +1,218 @@ +@testitem "PSCAD / verified completed-run reuse" tags=[:integration, :pscad, :slow] begin + using LineCableModels + using TOML, SHA, Logging + const P=LineCableModels.PSCAD + const launches=Ref(0) + const identifications=Ref(0) + const mismatched_readback=Ref(false) + const incomplete_matrix=Ref(false) + const station_identity=Dict("schema"=>"1", "version"=>"5.1.0", + "pscad_sha256"=>repeat("1", 64), "line_constants_sha256"=>repeat("2", 64), + "master_library_sha256"=>repeat("3", 64)) + # Exercise export, staging, parsing and checkpoints with an isolated + # transport fixture. These matrices are protocol fixtures, not references. + function P.remote_command(::Val{:completed_run_fixture}, config::P.RemoteConfig, command::AbstractString) + if occursin("-SharedCase", command) + launches[]+=1 + root=replace(only(match(r"-SharedCase '([^']+)'", command).captures), '\\'=>'/') + script=""" + using TOML + root = $(repr(root)) + input = TOML.parsefile(joinpath(root, "computation.toml")) + identity = TOML.parse($(repr(sprint(TOML.print, station_identity)))) + get(input, "expected_solver", identity) == identity || error("unexpected solver identity") + output = joinpath(root, "outputs") + for (name, value) in (("zm", 2.0), ("zp", 30.0), ("ym", 0.001), ("yp", 75.0)) + open(joinpath(output, "result_" * name * ".out"), "w") do io + println(io, "LOG10(FN) FN element") + for frequency in input["frequencies"] + println(io, log10(frequency), " ", frequency, " ", value) + end + end + end + open(joinpath(output, "native-settings.toml"), "w") do io + observed = Dict(component => Dict(field => control["readback"] + for (field, control) in controls) for (component, controls) in input["native_settings"]) + $(mismatched_readback[]) && (observed["ground"]["EarthForm"] = "UNEXPECTED") + TOML.print(io, observed) + end + $(incomplete_matrix[]) && write(joinpath(output, "result_zm.out"), "LOG10(FN) FN element\n") + write(joinpath(output, "timing.txt"), "3.25") + write(joinpath(output, "pscad-console.txt"), "protocol fixture") + open(joinpath(output, "solver.toml"), "w") do io + TOML.print(io, identity) + end + """ + else + occursin("identify.py", command) && (identifications[] += 1) + script="print("*repr(sprint(TOML.print, station_identity))*")" + end + return `$(Base.julia_cmd()) --startup-file=no --project=@stdlib -e $script` + end + + directory=mktempdir() + try + copper=Material(:conductor, 1.72e-8, 1.0) + design=build(CableDesign, + "resume-fixture", + terminal(:core, + solid(copper, Disk(0.004)), insulation(Material(:insulator, 1e14, 2.3); t = 0.002))) + system=build(LineCableSystem, [design], [Pose2(0, -1)]; + connections = [Dict(:core=>1)], system_id = basename(directory), line_length = 42.0) + frequency=collect(10.0 .^ range(-1, 6; length = 101)) + problem=LineParametersProblem(system; earth_props = homogeneous(rho = 100.0), frequencies = frequency) + remote=P.RemoteConfig( + "fixture", directory, "unused", "julia", "python"; + local_root=directory, transport = :completed_run_fixture) + options=(; remote, resume_run_directory = :latest, timing=true) + for value in (:invalid, "", 1) + @test_throws ArgumentError computation_options(P.PSCADFormulation, ComputationOptions((; remote, resume_run_directory = value))) + end + @test_throws ArgumentError computation_options(P.PSCADFormulation, ComputationOptions((; + remote, solver_identity = 1))) + quiet_log=Test.TestLogger() + first_result=with_logger(quiet_log) do + compute(problem, Formulation(:pscad); options) + end + @test isempty(quiet_log.logs) + @test launches[] == 1 + @test !details(first_result).data.execution.reused + @test identifications[] == 0 + @test !haskey(details(first_result).data.execution, :elapsed_scope) + source=details(first_result).data.execution.source_run + files=[joinpath(root, file) for (root, _, names) in walkdir(source) + for file in names] + original=Dict(path=>(bytes2hex(open(sha256, path)), mtime(path)) for path in files) + @test isfile(joinpath(source, "complete.toml")) + verbose_log=Test.TestLogger(min_level=Logging.Debug) + reused=with_logger(verbose_log) do + compute(problem, Formulation(:pscad; earth_impedance = :pollaczek1926); + options=(; options..., verbosity=(default=0, PSCAD=2))) + end + @test identifications[] == 1 + @test any(record->record.message == "PSCAD reuses a verified completed run", verbose_log.logs) + @test launches[] == 1 + @test Z(reused) == Z(first_result) + @test Y(reused) == Y(first_result) + @test details(reused).data.execution.reused + @test isempty(details(reused).data.timing) + @test details(first_result).data.timing.compile_call_seconds == 3.25 + @test details(first_result).data.timing.wall_seconds >= 0 + @test !haskey(details(reused).data.execution, :elapsed_seconds) + @test details(reused).data.formulations.requested.earth_impedance.identifier === :pollaczek1926 + sample_log=Test.TestLogger() + total=with_logger(sample_log) do + compute(problem, Formulation(:pscad); options = (; + options..., output_basis = :total, verbosity=(default=0,))) + end + @test isempty(sample_log.logs) + @test launches[] == 1 + @test Z(total) == 42 .* Z(first_result) + @test Y(total) == 42 .* Y(first_result) + @test original == + Dict(path=>(bytes2hex(open(sha256, path)), mtime(path)) for path in files) + callbacks=Int[] + callback_log=Test.TestLogger() + batch=with_logger(callback_log) do + compute(problem, + [Formulation(:pscad), Formulation(:pscad; earth_impedance = :pollaczek1926)]; + options = (; + options..., on_result = (problem, index, result)->begin + push!(callbacks, index) + @info "optional callback diagnostic" + @warn "visible callback warning" + end)) + end + @test count(record->record.level == Logging.Warn, callback_log.logs) == 2 + @test all(record->record.level >= Logging.Warn, callback_log.logs) + @test launches[] == 1 + @test callbacks == [1, 2] + @test details(batch[1]).data.formulations.requested.earth_impedance.identifier === :default + @test details(batch[2]).data.formulations.requested.earth_impedance.identifier === :pollaczek1926 + @test all(record -> record.formula === :pollaczek1926, + details(batch[2]).data.native_setting.interactions.earth_impedance) + @test Z(batch[1]) == Z(batch[2]) + @test Z(batch[1]) !== Z(batch[2]) + @test isempty(details(batch[2]).data.timing) + homogeneous_choices = (air = :default, earth = :default, mixed = :default) + represented = compute(problem, + [Formulation(:pscad), Formulation(:pscad; earth_impedance = homogeneous_choices)]; options) + @test launches[] == 1 + @test details(represented[1]).data.formulations.requested.earth_impedance.identifier === :default + @test map(record -> record.identifier,details(represented[2]).data.formulations.requested.earth_impedance) == homogeneous_choices + @test Z(represented[1]) == Z(represented[2]) + @test Z(represented[1]) !== Z(represented[2]) + changed_earth=LineParametersProblem( + system; earth_props = homogeneous(rho = 200.0), frequencies = frequency) + @test_throws ArgumentError compute(changed_earth, Formulation(:pscad); + options = (; remote, resume_run_directory = source)) + changed_frequencies=LineParametersProblem( + system; earth_props = homogeneous(rho = 100.0), + frequencies = collect(10.0 .^ range(-1, 5; length = 101))) + @test_throws ArgumentError compute(changed_frequencies, Formulation(:pscad); + options = (; remote, resume_run_directory = source)) + + @test_throws ArgumentError compute( + problem, Formulation(:pscad; earth_impedance = :saad1996); + options = (; remote, resume_run_directory = source)) + @test_throws ArgumentError compute(problem, Formulation(:pscad); + options = (; remote, solver_identity = Dict("version"=>"changed"))) + @test launches[] == 1 + compute(problem, Formulation(:pscad; earth_impedance = :saad1996); options) + @test launches[] == 2 + station_identity["line_constants_sha256"]=repeat("4", 64) + compute(problem, Formulation(:pscad); options) + @test launches[] == 3 + station_identity["line_constants_sha256"]=repeat("2", 64) + # A partial run must never be accepted as a completed numerical result. + partial=mktempdir(directory) + cp(joinpath(source, "computation.toml"), joinpath(partial, "computation.toml")) + @test_throws ArgumentError compute(problem, Formulation(:pscad); + options = (; remote, resume_run_directory = partial)) + open(joinpath(source, "outputs", "result_zm.out"), "a") do io + println(io, "damaged") + end + @test_throws ArgumentError compute(problem, Formulation(:pscad); options) + @test launches[] == 3 + @test endswith(read(joinpath(source, "outputs", "result_zm.out"), String), "damaged\n") + mismatched_readback[] = true + @test_throws ArgumentError compute(problem, Formulation(:pscad); options = (; remote)) + @test launches[] == 4 + mismatched_readback[] = false + incomplete_matrix[] = true + caught = try + compute(problem, Formulation(:pscad); options = (; remote)) + nothing + catch error + error + end + @test caught isa ErrorException + @test occursin("PSCAD result validation failed", sprint(showerror, caught)) + @test occursin("Full PSCAD diagnostics:", sprint(showerror, caught)) + incomplete_matrix[] = false + # Distinct physical settings in one batch require distinct native runs. + before = launches[] + heterogeneous = compute(problem, + [Formulation(:pscad), Formulation(:pscad; earth_impedance = :saad1996)]; + options = (; remote)) + @test launches[] == before + 2 + @test !details(heterogeneous[1]).data.execution.reused + @test !details(heterogeneous[2]).data.execution.reused + @test details(heterogeneous[1]).data.native_setting.ground != + details(heterogeneous[2]).data.native_setting.ground + # The first cache entry may be reused while a later selection is fresh. + before = launches[] + source_root = dirname(details(first(heterogeneous)).data.execution.source_run) + mixed = compute(problem, [Formulation(:pscad), + Formulation(:pscad; options=(base_frequency=55.0,))]; options=(; + remote, work_root=source_root, timing=true, resume_run_directory=:latest)) + @test launches[] == before + 1 + @test details(first(mixed)).data.execution.reused + @test isempty(details(first(mixed)).data.timing) + @test !details(last(mixed)).data.execution.reused + @test details(last(mixed)).data.timing.compile_call_seconds == 3.25 + @test typeof(first(mixed)) === typeof(last(mixed)) + finally + rm(directory; recursive = true) + end +end diff --git a/test/integration/pscad/remote_runner_protocol.jl b/test/integration/pscad/remote_runner_protocol.jl new file mode 100644 index 000000000..1db6c6541 --- /dev/null +++ b/test/integration/pscad/remote_runner_protocol.jl @@ -0,0 +1,200 @@ +using Test +using PythonCall +using TOML + +const REPOSITORY_ROOT=normpath(joinpath(@__DIR__,"..","..","..")) +const REMOTE_ROOT=joinpath( + REPOSITORY_ROOT,"src","pscad","remote") + +include(joinpath(REMOTE_ROOT,"runner.jl")) + +const PYTHON_AUTOMATION_FIXTURE=raw""" +import importlib.metadata +import os +import runpy +import sys +import types + +IDENTITY = { + "schema": "1", + "version": "5.1.0", + "python_version": "fixture", + "automation_version": "3.1.2", + "common_version": "fixture", + "profile": "fixture", + "pscad_path": "fixture", + "pscad_sha256": "0" * 64, + "line_constants_path": "fixture", + "line_constants_sha256": "1" * 64, + "master_library_path": "fixture", + "master_library_sha256": "2" * 64, +} + +class Component: + def __init__(self, definition, **parameters): + self.defn_name = definition + self._parameters = parameters + + def parameters(self, **updates): + self._parameters.update(updates) + if updates.get("Output") == 1: + self._parameters["Output"] = "YES" + return dict(self._parameters) + +class Line(Component): + def __init__(self): + super().__init__("master:fixture", Name="unset") + + def compile(self): + if os.environ.get("LCM_RUNNER_FIXTURE_FAIL") == "1": + raise RuntimeError("synthetic compile failure") + root = os.environ["LCM_RUNNER_FIXTURE_OUTPUT"] + for suffix in ("_zm.out", "_zp.out", "_ym.out", "_yp.out"): + with open(os.path.join(root, "raw" + suffix), "w") as stream: + stream.write("header\n") + for index in range(3): + stream.write(f"{index} {index + 1}\n") + +class Canvas: + def __init__(self, components): + self._components = components + + def components(self): + return self._components + +class Project: + def __init__(self): + self.line = Line() + self.frequency = Component( + "master:line_frephase_options", FS=0.0, FE=0.0, Numf=0, Output="NO") + self.ground = Component("master:line_ground") + self.cable = Component("master:cable_coax", elim1=0, elim2="RETAIN") + self._canvas = Canvas([self.frequency, self.ground, self.cable]) + self.temp_folder = os.environ["LCM_RUNNER_FIXTURE_OUTPUT"] + self.unloaded = False + + def find_all(self, kind): + return [self.line] if kind == "TLine" else [] + + def canvas(self, name): + return self._canvas + + def messages(self): + return ["fixture message"] + + def output(self): + return "fixture project output" + + def save(self): + pass + + def unload(self): + self.unloaded = True + +class Application: + version = "5.1.0" + + def __init__(self): + self._project = Project() + self.closed = False + + def licensed(self): + return True + + def load(self, path): + pass + + def project(self, name): + return self._project + + def quit(self): + self.closed = True + +pscad = types.ModuleType("mhi.pscad") +pscad.launch = lambda **kwargs: Application() +mhi = types.ModuleType("mhi") +mhi.pscad = pscad +sys.modules["mhi"] = mhi +sys.modules["mhi.pscad"] = pscad +importlib.metadata.version = lambda name: "3.1.2" if name == "mhi.pscad" else "fixture" +runpy.run_path = lambda path: {"identify": lambda version, app: dict(IDENTITY)} +""" + +function runner_arguments(directory,output;verbosity="0") + project=joinpath(directory,"generated.pscx") + write(project,"fixture") + input=Dict( + "schema_version"=>4, + "native_settings"=>Dict( + "ground"=>Dict{String,Any}(), + "frequency"=>Dict{String,Any}(), + "configuration"=>Dict{String,Any}(), + ), + ) + open(joinpath(directory,"computation.toml"),"w") do io + TOML.print(io,input;sorted=true) + end + return [project,output,"generated","fixture","pscad","10.0","1000.0","2", + "5.1.0",verbosity] +end + +@testset "Remote PSCAD runner protocol without PSCAD" begin + pyexec(PYTHON_AUTOMATION_FIXTURE,Main) + + @test _same_value(1,1.0) + @test !_same_value(1,2) + @test _same_value("YES",pystr("YES")) + for value in (0,"no","FALSE"," disabled ","retain","none") + @test _disabled(value) + end + @test !_disabled("YES") + @test_throws ArgumentError main(String[]) + + mktempdir() do directory + output=joinpath(directory,"outputs") + arguments=runner_arguments(directory,output) + @test_throws ArgumentError main(setindex!(copy(arguments),"3",10)) + @test_throws ArgumentError main(setindex!(copy(arguments),"5.0.2",9)) + @test_throws ArgumentError main(setindex!(copy(arguments),"bad-stem",4)) + + python_environment=pyimport("os").environ + python_environment["LCM_RUNNER_FIXTURE_OUTPUT"]=output + try + @test main(arguments) === nothing + finally + python_environment.pop("LCM_RUNNER_FIXTURE_OUTPUT",nothing) + end + for name in ("result_zm.out","result_zp.out","result_ym.out","result_yp.out") + @test _data_rows(joinpath(output,name)) == 3 + end + @test TOML.parsefile(joinpath(output,"solver.toml"))["version"] == "5.1.0" + @test TOML.parsefile(joinpath(output,"native-settings.toml")) == Dict( + "ground"=>Dict{String,Any}(), + "frequency"=>Dict{String,Any}(), + "configuration"=>Dict{String,Any}(), + ) + console=read(joinpath(output,"pscad-console.txt"),String) + @test occursin("Starting PSCAD line-constants computation",console) + @test occursin("Collected detailed PSCAD Z and Y outputs",console) + end + + mktempdir() do directory + output=joinpath(directory,"failed") + arguments=runner_arguments(directory,output;verbosity="2") + python_environment=pyimport("os").environ + python_environment["LCM_RUNNER_FIXTURE_OUTPUT"]=output + python_environment["LCM_RUNNER_FIXTURE_FAIL"]="1" + caught=try + main(arguments) + nothing + catch error + error + finally + python_environment.pop("LCM_RUNNER_FIXTURE_OUTPUT",nothing) + python_environment.pop("LCM_RUNNER_FIXTURE_FAIL",nothing) + end + @test caught isa ErrorException + @test occursin("line-constants computation",sprint(showerror,caught)) + @test occursin("synthetic compile failure",read(joinpath(output,"pscad-console.txt"),String)) + end +end diff --git a/test/integration/pscad/transport_readers.jl b/test/integration/pscad/transport_readers.jl new file mode 100644 index 000000000..0574d5506 --- /dev/null +++ b/test/integration/pscad/transport_readers.jl @@ -0,0 +1,88 @@ +@testmodule PSCADReaderFailures begin + const RemoteConfig = NamedTuple + const fail_at_eof = Ref(false) + const readers_started = Channel{Nothing}(2) + const child = Ref{Base.Process}() + + remote_command(config::RemoteConfig, ::AbstractString) = config.command + function run(command; wait) + child[] = Base.run(command; wait) + end + + struct Lines{T} + source::T + end + function eachline(pipe::Pipe) + put!(readers_started, nothing) + return Lines(Base.eachline(pipe)) + end + function Base.iterate(lines::Lines, state...) + entry = iterate(lines.source, state...) + entry === nothing && fail_at_eof[] && error("synthetic reader cleanup failure") + entry === nothing || first(entry) != "FAIL" || error("synthetic reader failure") + return entry + end + + # Execute the production transport method with isolated IO fault injection. + Base.include(@__MODULE__, + joinpath(@__DIR__, "../../../src/pscad/remote/remote.jl")) do expression + expression isa Expr && expression.head === :function && + expression.args[1] isa Expr && expression.args[1].head === :call && + expression.args[1].args[1] === :_run_remote ? expression : nothing + end +end + +@testitem "PSCAD / reader failures propagate and cancellation retains secondary diagnostics" tags=[:integration, :pscad] default_imports=false setup=[PSCADReaderFailures] begin + using Test, Logging + const P=PSCADReaderFailures + for stream in ("stdout", "stderr") + script="println($stream, \"partial\"); println($stream, \"FAIL\"); flush($stream); sleep(30)" + command=`$(Base.julia_cmd()) --startup-file=no --handle-signals=no --project=@stdlib -e $script` + mktempdir() do root + stdout_path, stderr_path=joinpath(root, "out.log"), joinpath(root, "err.log") + caught=try + P._run_remote((; command, timeout = 10.0), "unused"; stdout_path, stderr_path) + catch exception + exception + end + @test caught isa TaskFailedException + @test occursin("synthetic reader failure", sprint(showerror, caught)) + @test process_exited(P.child[]) + @test read(stream == "stdout" ? stdout_path : stderr_path, String) == + "partial\n" + take!(P.readers_started) + take!(P.readers_started) + end + end + + command=`$(Base.julia_cmd()) --startup-file=no --handle-signals=no --project=@stdlib -e 'sleep(30)'` + P.fail_at_eof[]=true + logger=Test.TestLogger(min_level = Logging.Warn) + canceled=Ref(false) + interrupted=InterruptException() + try + task=with_logger(logger) do + @async try + P._run_remote((; command, timeout = 10.0), "unused"; + on_interrupt = () -> (canceled[] = true)) + catch exception + exception + end + end + take!(P.readers_started) + take!(P.readers_started) + schedule(task, interrupted; error = true) + @test fetch(task) === interrupted + @test canceled[] + @test process_exited(P.child[]) + @test Set(record.kwargs[:stream] for record in logger.logs) == + Set((:stdout, :stderr)) + @test all(record -> occursin("reader failed during cleanup", string(record.message)), logger.logs) + @test all( + record -> occursin("synthetic reader cleanup failure", + sprint(showerror, first(record.kwargs[:exception]))), + logger.logs) + finally + P.fail_at_eof[]=false + end +end diff --git a/test/integration/pscad_constitutive.jl b/test/integration/pscad_constitutive.jl new file mode 100644 index 000000000..14d064bd4 --- /dev/null +++ b/test/integration/pscad_constitutive.jl @@ -0,0 +1,104 @@ +@testitem "ImportExport / PSCAD / selected constitutive laws and operating temperature" tags=[:integration, :pscad] begin + using EzXML + const IE = LineCableModels.ImportExport + copper = Material(:conductor, 1.72e-8, 1, 1, 20, 0.004) + semicon = Material(:semicon, 1e4, 40, 1, 20, -0.005; tan_delta=0.02) + dielectric = Material(:insulator, 1e8, 2.3, 1, 20, -0.003; tan_delta=0.03) + design = build(CableDesign, "selected-dielectrics", terminal(:core, + solid(copper, Disk(0.004)), screen(semicon; t=0.0005), + insulation(dielectric; t=0.002))) + system = build(LineCableSystem, [design], [Pose2(0, -1)]; + connections=[Dict(:core=>1)]) + earth = homogeneous(rho=100.0) + frequency = 60.0 + omega = 2pi * frequency + epsilon0 = 8.8541878128e-12 + admittivity(material, id, temperature=60.0) = im * omega * epsilon0 * material.eps_r + + (id === :default ? 0 : inv(material.rho * (1 + material.alpha * (temperature - material.T0))) + + omega * epsilon0 * material.eps_r * material.tan_delta) + + mktempdir() do directory + for constructor in (Formulation, CableConstantsFormulation), + insulation_id in (:default, :lossy), semicon_id in (:default, :lossy) + selected = constructor(insulation_admittance=insulation_id, + semicon_admittance=semicon_id) + component = only(LineCableModels.PSCAD._pscad_components(LineCableModels.Engine.flatten(LineCableModelsCoaxial(), design), frequency, selected, 60.0)) + @test component.conductor.material.rho ≈ copper.rho * 1.16 + expected = inv(log(0.0045 / 0.004) / (2pi * admittivity(semicon, semicon_id)) + + log(0.0065 / 0.0045) / (2pi * admittivity(dielectric, insulation_id))) + @test component.dielectric.shunt_capacitance ≈ imag(expected) / omega + @test component.dielectric.shunt_conductance ≈ real(expected) + path = export_data(:pscad, system, earth; base_freq=frequency, + formulation=selected, temperature=60.0, file_name=joinpath(directory, "selected.pscx")) + document = readxml(path) + cable = only(findall("//User[@defn='master:Cable_Coax']", document)) + values = Dict(node["name"]=>node["value"] for node in findall("./paramlist/param", cable)) + @test parse(Float64, values["FLT"]) == frequency + @test parse(Float64, values["RHOC"]) ≈ copper.rho * 1.16 rtol=1e-5 + @test parse(Float64, values["LT1"]) ≈ real(expected) / imag(expected) atol=5e-5 + @test parse(Float64, values["EPS1"]) ≈ + imag(expected) / omega * log(0.0065 / 0.004) / (2pi * epsilon0) rtol=1e-5 + end + selected = Formulation() + @test only(LineCableModels.PSCAD._pscad_components(LineCableModels.Engine.flatten(LineCableModelsCoaxial(), design), frequency, selected, nothing)).conductor.material.rho == copper.rho + uncorrected = Formulation(temperature_dependence=nothing) + @test only(LineCableModels.PSCAD._pscad_components(LineCableModels.Engine.flatten(LineCableModelsCoaxial(), design), frequency, uncorrected, 60.0)).conductor.material.rho == copper.rho + reference_shunt = inv(log(0.0045 / 0.004) / (2pi * admittivity(semicon, :lossy, 20.0)) + + log(0.0065 / 0.0045) / (2pi * admittivity(dielectric, :lossy, 20.0))) + for (temperature, correction) in ((nothing, true), (60.0, false)) + selected = Formulation(insulation_admittance=:lossy, + semicon_admittance=:lossy, temperature_dependence=correction ? formula(:default) : nothing) + component = only(LineCableModels.PSCAD._pscad_components(LineCableModels.Engine.flatten(LineCableModelsCoaxial(), design), frequency, selected, temperature)) + @test component.dielectric.shunt_conductance ≈ real(reference_shunt) + @test component.dielectric.shunt_capacitance ≈ imag(reference_shunt) / omega + end + default_path = export_data(:pscad, system, earth; + temperature=60.0, file_name=joinpath(directory, "unspecified.pscx")) + @test isfile(default_path) + default_document = readxml(default_path) + default_cable = only(findall("//User[@defn='master:Cable_Coax']", default_document)) + default_values = Dict(node["name"]=>node["value"] for node in findall("./paramlist/param", default_cable)) + @test parse(Float64,default_values["LT1"]) == 0 + @test parse(Float64,default_values["RHOC"]) ≈ copper.rho * 1.16 rtol=1e-5 + frequency_error = try + export_data(:pscad, system, earth; + base_freq=Inf, file_name=joinpath(directory, "infinite.pscx")) + catch exception + exception + end + @test frequency_error isa DomainError + @test occursin("base frequency must be positive and finite", sprint(showerror, frequency_error)) + @test !isfile(joinpath(directory, "$(system.system_id)_infinite.pscx")) + + lossy = build(CableDesign, "loss-cap", terminal(:core, solid(copper, Disk(0.004)), + insulation(Material(:insulator, 1.0, 2.3); t=0.002))) + component = only(LineCableModels.PSCAD._pscad_components(LineCableModels.Engine.flatten(LineCableModelsCoaxial(), lossy), 50.0, + Formulation(insulation_admittance=:lossy), 20.0)) + requested = component.dielectric.shunt_conductance / + (2pi * 50 * component.dielectric.shunt_capacitance) + @test requested > 10 + emitted = Dict(LineCableModels.PSCAD._pscad_part_parameters(component, 1, 2pi * 50)) + @test parse(Float64, emitted["LT1"]) == 10 + @test component.dielectric.shunt_conductance / + (2pi * 50 * component.dielectric.shunt_capacitance) == requested + + bare = build(CableDesign, "bare-wire", terminal(:core, solid(copper, Disk(0.0425)))) + bare_system = build(LineCableSystem, [bare], [Pose2(0, -1)]; + connections=[Dict(:core=>1)]) + for keywords in ((;), (formulation=Formulation(),)) + path = export_data(:pscad, bare_system, earth; keywords..., + file_name=joinpath(directory, "bare.pscx")) + document = EzXML.readxml(path) + cable = only(EzXML.findall("//User[@defn='master:Cable_Coax']", document)) + values = Dict(node["name"]=>node["value"] for node in EzXML.findall("./paramlist/param", cable)) + @test values["LL"] == "0" + @test parse(Float64, values["R2"]) == parse(Float64, values["R3"]) == 0.0425 + _, imported = import_data(:pscad, path) + @test length(only(imported.designs).geometry.regions) == 1 + end + insulated_values = Dict(LineCableModels.PSCAD._pscad_cable_parameters(design, LineCableModels.PSCAD._pscad_components( + LineCableModels.Engine.flatten(LineCableModelsCoaxial(), design), frequency, Formulation(), nothing), + Pose2(0, -1), [1], 1, frequency)) + @test insulated_values["LL"] == "1" + end +end diff --git a/test/integration/pscad_import.jl b/test/integration/pscad_import.jl new file mode 100644 index 000000000..1b0104837 --- /dev/null +++ b/test/integration/pscad_import.jl @@ -0,0 +1,171 @@ +@testitem "ImportExport / PSCAD / simplified physical inputs and circuits" tags=[:integration, :pscad] begin + using EzXML + const DM = LineCableModels.DataModel + epsilon0 = 8.8541878128e-12 + mktempdir() do directory + # A minimal imported project constructed independently of the exporter + # under test. PSCAD resistance and capacitance inputs use Ω/km and μF/km. + function write_project(overrides=Dict{String,String}(); binding="master:Cable_CoaxSimpl") + document = parsexml(""" + + + + + + + + + + + + + + + + + + + + """) + physical = only(findall("//User[@defn='physical']", document)) + physical["defn"] = binding + values = merge(Dict( + "Name"=>"", "CABNUM"=>"1", "NC"=>"2", "D"=>"0.15", + "X"=>"-0.5", "Y"=>"1.2", "FLT"=>"6D1 [Hz]", "LL"=>"3", + "RorT"=>"0", "SemiCL"=>"0", "R1"=>"0", "R2"=>"0.004", + "R3"=>"0.006", "R4"=>"0.0065", "R5"=>"0.0075", + "DTC"=>"0", "DCRC"=>"0.35 [ohm/km]", "RHOC"=>"2.82e-8", + "DTS"=>"1", "DCRS"=>"0.9", "RHOS"=>"2.2e-7", + "PERMC"=>"1", "PERMS"=>"1.1", "DTI1"=>"0", "CI1"=>"0.3 [uF/km]", + "EPS1"=>"2.3", "DTI2"=>"1", "CI2"=>"0.7", "EPS2"=>"3.2", + "mu_r1"=>"1.2", "mu_r2"=>"1", "LT1"=>"0.02", "LT2"=>"0", + "OHC"=>"1", "Y2"=>"2.0", + ), overrides) + parameters = only(findall("./paramlist", physical)) + for name in sort!(collect(keys(values))) + node = addelement!(parameters, "param") + node["name"] = name + node["value"] = values[name] + end + path = joinpath(directory, "input.pscx") + write(path, document) + return path + end + + earth, system = import_data(:pscad, write_project()) + @test ncables(system) == 6 + @test nphases(system) == 12 + @test system.line_length == 3250.0 + @test system.system_id == "six_cables" + @test last(earth.layers).rho == 100.0 + @test [design.cable_id for design in system.designs] == ["cable$i" for i in 1:6] + @test [position.x for position in system.positions] ≈ [-0.5 + 0.15i for i in 0:5] + @test all(position -> position.y == -1.2, system.positions) + @test collect(Iterators.flatten(system.connections)) == collect(1:12) + for design in system.designs + core, insulation, sheath, jacket = design.geometry.regions + @test design.terminal_order == [:core, :sheath] + @test DM.r_ex(core.primitive) == 0.004 + @test DM.r_in(sheath.primitive) == 0.006 + @test DM.r_ex(jacket.primitive) == 0.0075 + @test core.source.material.rho ≈ 0.35 / 1000 * pi * 0.004^2 + @test sheath.source.material.rho == 2.2e-7 + expected_epsilon = 0.3e-9 * log(0.006 / 0.004) / (2pi * epsilon0) + @test insulation.source.material.eps_r ≈ expected_epsilon + @test insulation.source.material.mu_r == 1.2 + @test inv(insulation.source.material.rho) ≈ 2pi * 60 * epsilon0 * expected_epsilon * 0.02 + @test jacket.source.material.eps_r == 3.2 + @test isinf(jacket.source.material.rho) + @test all(region -> region.source.material.T0 == 20.0 && + iszero(region.source.material.alpha), design.geometry.regions) + end + # Exercise the complementary direct-permittivity / DC-resistance inputs. + _, complementary = import_data(:pscad, write_project(Dict( + "DTC"=>"1", "DTS"=>"0", "DTI1"=>"1", "DTI2"=>"0"))) + core, insulation, sheath, jacket = first(complementary.designs).geometry.regions + @test core.source.material.rho == 2.82e-8 + @test insulation.source.material.eps_r == 2.3 + @test sheath.source.material.rho ≈ 0.9 / 1000 * pi * (0.0065^2 - 0.006^2) + @test jacket.source.material.eps_r ≈ 0.7e-9 * log(0.0075 / 0.0065) / (2pi * epsilon0) + _, selected_fields = import_data(:pscad, write_project(Dict( + "RHOC"=>"unused", "EPS1"=>"unused", "DCRS"=>"unused", "CI2"=>"unused"))) + @test [region.source.material for region in first(selected_fields.designs).geometry.regions] == + [region.source.material for region in first(system.designs).geometry.regions] + + # LL=0 is the detailed bare-conductor form, not an insulated component. + _, bare = import_data(:pscad, write_project(Dict("LL"=>"0"); binding="master:Cable_Coax")) + @test ncables(bare) == nphases(bare) == 1 + @test first(bare.designs).terminal_order == [:conductor] + @test length(first(bare.designs).geometry.regions) == 1 + @test first(bare.positions).y == 2.0 + + for (field, value, error_type) in ( + ("NC", "0", DomainError), ("NC", "1.5", ArgumentError), + ("RorT", "1", ArgumentError), ("SemiCL", "1", ArgumentError), + ("LL", "2", ArgumentError), ("LL", "7", ArgumentError), + ("DTC", "2", ArgumentError), ("DTI1", "2", ArgumentError), + ("LT1", "-0.1", DomainError), ("LT1", "10.1", DomainError), + ("FLT", "0", DomainError), ("FLT", "-60", DomainError), + ("FLT", "Inf", DomainError), ("FLT", "text", ArgumentError), + ("CABNUM", "2", ArgumentError), + ) + @testset "$field=$value" begin + @test_throws error_type import_data(:pscad, write_project(Dict(field=>value))) + end + end + for binding in ("master:Cable_PipeType", "master:Line_Tower_3Phase") + @test_throws ArgumentError import_data(:pscad, write_project(; binding)) + end + for binding in ("master:Cable_Coax", "master:Cable_CoaxSimpl"), invalid in ("0", "-60", "Inf") + path = write_project(Dict("FLT"=>invalid); binding) + failure = try + import_data(:pscad, path) + catch exception + exception + end + @test failure isa DomainError + @test occursin("reference frequency must be positive and finite", sprint(showerror, failure)) + end + + # Multiple row definitions must not silently change which physical + # system is imported. Explicit selection wins over the output flags. + path = write_project() + document = readxml(path) + definitions = only(findall("/project/definitions", document)) + other = addelement!(definitions, "Definition") + other["name"] = "Other" + schematic = addelement!(other, "schematic") + for binding in ("master:Line_FrePhase_Options", "master:Line_Ground") + node = addelement!(schematic, "User") + node["defn"] = binding + addelement!(node, "paramlist") + end + write(path, document) + @test_throws ArgumentError import_data(:pscad, path) + for definition in ("CableSystem", "Imported:CableSystem") + _, selected = import_data(:pscad, path; definition) + @test ncables(selected) == 6 + @test selected.system_id == system.system_id + end + @test_throws ArgumentError import_data(:pscad, path; definition="Unknown") + output = addelement!(only(findall( + "//Definition[@name='CableSystem']/schematic/User[@defn='master:Line_FrePhase_Options']/paramlist", + document)), "param") + output["name"] = "Output" + for enabled in ("1", " YES ", "enabled", "True") + output["value"] = enabled + write(path, document) + @test ncables(last(import_data(:pscad, path))) == 6 + end + other_output = addelement!(only(findall( + "./schematic/User[@defn='master:Line_FrePhase_Options']/paramlist", other)), "param") + other_output["name"] = "Output" + other_output["value"] = "1" + write(path, document) + @test_throws ArgumentError import_data(:pscad, path) + @test ncables(last(import_data(:pscad, path; definition="CableSystem"))) == 6 + other["name"] = "CableSystem" + write(path, document) + @test_throws ArgumentError import_data(:pscad, path; definition="CableSystem") + end +end diff --git a/test/integration/report_result_protocol.jl b/test/integration/report_result_protocol.jl new file mode 100644 index 000000000..1a99f83f7 --- /dev/null +++ b/test/integration/report_result_protocol.jl @@ -0,0 +1,45 @@ +@testitem "ObservedResult / independent primary owner reuses tables and plotting" tags=[:integration, :makie] begin + using CairoMakie, DataFrames + using LineCableModels.Commons: observation_quantity + const reads=Ref(0) + struct IndependentResult + values::Array{Float64,3} + frequencies::Vector{Float64} + end + LineCableModels.basis(::IndependentResult)=:pul + LineCableModels.observables(::Type{IndependentResult})=(R,) + function LineCableModels.Commons.observation_quantity(result::IndependentResult,request; + unit=nothing,clip=true,atol=nothing,frequencies=nothing) + reads[]+=1 + n,m,k=size(result.values) + target=something(unit,LineCableModels.Units.native_unit(R,:pul)) + native=LineCableModels.Units.native_unit(R,:pul) + factor=LineCableModels.Units.scale_factor(native,target) + return (request,quantity=LineCableModels.Units.quantity(R),family=:independent,statistic=:value, + values=copy(result.values).*factor,unit=target,basis=:pul, + coordinates=(kind=:matrix,indices=(:,:, :),rows=collect(1:n),columns=collect(1:m), + samples=collect(1:k),frequencies=copy(result.frequencies),frequency_unit=LineCableModels.Units.units(:base,:hertz), + extent=(n,m,k),labels=string.(1:n),domain=:PhaseDomain), + thresholds=nothing,available=trues(n,m,k),engineering_zero=falses(n,m,k),clipped=false,missing_reason=nothing) + end + source=IndependentResult(reshape(collect(1.:12.),2,2,3),[1.,10.,100.]) + observed=ObservedResult(source) + @test reads[]==1 + source.values .= NaN + artifact=report(TableReportDefinition(),observed) + @test size(artifact.tables.independent.R)==(3,5) + @test artifact.tables.independent.R[1,Symbol("[1,2]")]==3000 + table=artifact[R] + before=copy(table) + for _ in 1:2, mime in (MIME"text/plain"(),MIME"text/html"()) + @test !isempty(sprint(show,mime,artifact)) + @test artifact[R]===table + @test artifact[1,R]===table + end + @test isequal(table,before) + @test reads[]==1 + rendered=LineCableModels.plot(observed;backend=:cairo,display_plot=false,controls=false) + @test length(rendered.axes)==4 + @test reads[]==1 + @test all(isfinite,observe(observed,R)) +end diff --git a/test/integration/retained_interpretation.jl b/test/integration/retained_interpretation.jl new file mode 100644 index 000000000..6bf0689f0 --- /dev/null +++ b/test/integration/retained_interpretation.jl @@ -0,0 +1,286 @@ +@testitem "Makie / retained units coordinates and ragged frequencies" tags=[:visual, :makie] begin + using CairoMakie + z=reshape(complex.(1.:12.,101.:112.),2,2,3) + a=ObservedResult(LineParameters(z,2z,[1.,10.,100.]);length_unit=:base) + b=ObservedResult(LineParameters(z,2z,[1.,10.,100.]), + ((R,[2,1],[2,1],[1,3]),(X,[2,1],[2,1],[1,3]), + (G,[2,1],[2,1],[2,3]),(B,[2,1],[2,1],[2,3])); + length_unit=:kilo,frequency_unit=:kilo) + options=(backend=:cairo,display_plot=false,controls=false,open_export=false) + pages=LineCableModels.plot([a,b];ydata=(R,G),options...) + @test length(pages)==2 + for (page,selector,scale,samples) in zip(pages,(R,G),(1.,2.),([1,3],[2,3])) + @test length(page.axes)==4 + for (axis,(i,j)) in zip(page.axes,((1,1),(1,2),(2,1),(2,2))) + curves=filter(p -> p isa Makie.Lines,axis.scene.plots) + @test length(curves)==2 + expected1=Float32.(real.(z[i,j,:]).*scale) + expected2=expected1[samples] + @test getindex.(curves[1][1][],2)≈expected1 + @test getindex.(curves[2][1][],2)≈expected2 + @test getindex.(curves[1][1][],1)≈[1.,10.,100.] + @test getindex.(curves[2][1][],1)≈[1.,10.,100.][samples] + end + end + @test b.quantities[1].coordinates.rows==[2,1] + @test b.quantities[1].coordinates.frequency_unit==LineCableModels.Units.units(:kilo,:hertz) +end + +@testitem "Makie / quantity capacity and original modal panel identities" tags=[:visual, :makie, :slow] begin + using CairoMakie,LinearAlgebra + options=(backend=:cairo,display_plot=false,controls=false,open_export=false) + z=fill(0.0+0im,3,3,2) + for i in 1:3 + z[i,i,:].=i+2im + end + raw=LineParameters(z,z.*1e-6,[1.,10.]) + retained=ObservedResult(raw,(R,L);clip=false,length_unit=:base) + # The source owner declares modal coordinates. The renderer must not infer diag. + modal=ObservedResult(retained.gridpoint, + [merge(q,(coordinates=merge(q.coordinates,(domain=:ModalDomain,)),)) for q in retained.quantities], + retained.errors,retained.timings) + pages=LineCableModels.plot(modal;ydata=(R,L),layout=(2,2),options...) + @test length(pages)==8 + @test [p.plot_state.panel_page.dimensions for p in pages]==repeat([(2,2),(2,1),(1,2),(1,1)],2) + @test sum(length(p.axes) for p in pages)==18 + @test Set(k for p in pages[1:4] for k in keys(p.plot_state.panel_data))==Set(Iterators.product(1:3,1:3)) + @test length(LineCableModels.plot(modal;ydata=(R,L),layout=(1,1),options...))==18 + @test length(LineCableModels.plot(modal;ydata=(R,L),layout=(1,2),options...))==12 + @test_throws ArgumentError LineCableModels.plot(modal;blocks=(2,2),options...) + residual=ObservedResult(modal.gridpoint,[merge(q,(values=q.values .+ 1e-12,)) for q in modal.quantities],[],(;)) + full=LineCableModels.plot(residual;ydata=(R,),options...) + @test length(full.axes)==9 + @test last(only(filter(p -> p isa Makie.Lines,full.plot_state.panel_data[(1,2)].axis.scene.plots))[1][][1])≈1e-12 + diagonal=ObservedResult(raw,((R,diag,:,:),(L,diag,:,:));clip=false) + flow=LineCableModels.plot(diagonal;ydata=((R,diag),),layout=(1,2),options...) + @test [length(p.axes) for p in flow]==[2,1] + @test collect(keys(last(flow).plot_state.panel_data))==[(3,3)] + # An owner can retain a rectangular matrix even though line solvers are square. + q=first(modal.quantities) + rect=merge(q,(values=q.values[:,1:2,:],available=true,engineering_zero=false,missing_reason=nothing, + coordinates=merge(q.coordinates,(extent=(3,2,2),columns=[1,2])),)) + rectangular=ObservedResult((id=nothing,),[rect],[],(;)) + @test length(LineCableModels.plot(rectangular;options...).axes)==6 + one=LineParameters(reshape([1+2im],1,1,1),reshape([1+2im],1,1,1),[1.]) + single=LineCableModels.plot(one;ydata=(R,),options...) + @test length(single.axes)==1 + @test any(p -> p isa Makie.Scatter && length(p[1][])==1,only(single.axes).scene.plots) +end + +@testitem "Makie / retained assembly unions and element-index points" tags=[:visual, :makie, :slow] begin + using CairoMakie,Measurements + options=(backend=:cairo,display_plot=false,controls=true,open_export=false) + a=CableConstants([:core,:sheath],[1.,2.],[.1,.2],[.01,.02],[.001,.002],50.) + b=CableConstants([:core,:screen],[3.,4.],[.3,.4],[.03,.04],[.003,.004],60.) + observed=[ObservedResult(a;length_unit=:base),ObservedResult(b;length_unit=:base)] + page=LineCableModels.plot(observed;ydata=(R,),options...) + axis=only(page.axes) + @test axis.dim1_conversion[] isa Makie.CategoricalConversion + @test !haskey(page.controls,:xlog) + @test isempty(filter(p -> p isa Makie.Lines,axis.scene.plots)) + points=filter(p -> p isa Makie.Scatter,axis.scene.plots) + @test first.(points[1][1][])==[1.,2.] + @test first.(points[2][1][])==[1.,3.] + @test last.(points[2][1][])==[3.,4.] + @test last.(axis.dim1_conversion[].int_to_category)==["core","sheath","screen"] + assembly_overlay=LineCableModels.plot(a;ydata=R,overlay=:coordinates, + series_labels=("core","sheath"), + series_attributes=((color=:red,),(color=:blue,)),options...) + assembly_curves=filter(p -> p isa Makie.Scatter,only(assembly_overlay.axes).scene.plots) + @test length(assembly_curves)==2 + @test first.(assembly_curves[1][1][])==[1.] + @test first.(assembly_curves[2][1][])==[2.] + @test last.(assembly_curves[1][1][])==[1000.] + @test last.(assembly_curves[2][1][])==[2000.] + @test assembly_curves[1].color[]==Makie.to_color(:red) + @test assembly_curves[2].color[]==Makie.to_color(:blue) + @test Set(values(assembly_overlay.plot_state.labels))==Set(("core","sheath")) + reversed=CableConstants([:sheath,:core],[20.,10.],[.2,.1],[.02,.01],[.002,.001],60.) + singleton=CableConstants([:sheath],[30.],[.3],[.03],[.003],70.) + varied=LineCableModels.plot((a,reversed,singleton);ydata=R,overlay=:coordinates, + series_labels=("Core","Sheath"), + series_attributes=((color=:red,),(color=:blue,)),options...) + @test varied.plot_state.labels[:coordinate_1]=="Core" + @test varied.plot_state.labels[:coordinate_2]=="Sheath" + varied_curves=[filter(p -> p isa Makie.Scatter,axis.scene.plots) for axis in varied.axes] + @test length.(varied_curves)==[2,2,1] + @test [last.(curve[1][]) for curve in varied_curves[1]]==[[1000.],[2000.]] + @test [last.(curve[1][]) for curve in varied_curves[2]]==[[20000.],[10000.]] + @test last.(only(varied_curves[3])[1][])==[30000.] + @test Tuple(curve.color[] for curve in varied_curves[1])==Makie.to_color.((:red,:blue)) + @test Tuple(curve.color[] for curve in varied_curves[2])==Makie.to_color.((:blue,:red)) + @test only(varied_curves[3]).color[]==Makie.to_color(:blue) + @test [string.(last.(axis.dim1_conversion[].int_to_category)) for axis in varied.axes]== + [["core","sheath"],["sheath","core"],["sheath"]] + core_group=varied.plot_state.groups[:coordinate_1] + sheath_group=varied.plot_state.groups[:coordinate_2] + @test varied_curves[2][2] in core_group + @test varied_curves[2][1] in sheath_group + @test only(varied_curves[3]) in sheath_group + foreach(plot -> plot.visible[]=false,core_group) + @test !varied_curves[2][2].visible[] + @test varied_curves[2][1].visible[] + foreach(plot -> plot.visible[]=true,core_group) + @test varied_curves[2][2].visible[] + sparse_first=LineCableModels.plot((singleton,a);ydata=R,overlay=:coordinates, + series_labels=("Sheath","Core"), + series_attributes=((color=:blue,),(color=:red,)),options...) + @test Set(values(sparse_first.plot_state.labels))==Set(("Sheath","Core")) + @test [count(p -> p isa Makie.Scatter,axis.scene.plots) for axis in sparse_first.axes]==[1,2] + @test Tuple(plot.color[] for axis in sparse_first.axes for plot in axis.scene.plots + if plot isa Makie.Scatter)==Makie.to_color.((:blue,:red,:blue)) + raw=LineCableModels.plot((a,b);ydata=(R,),length_unit=:base,options...) + @test length(raw.axes)==1 + template=first(first(observed).quantities) + function indexed(values,indices=()) + q=merge(template,(request=R,values,coordinates=(kind=:array,indices,extent=size(values)), + available=true,engineering_zero=false,missing_reason=nothing,thresholds=nothing,clipped=false)) + ObservedResult((id=nothing,),[q],[],(;)) + end + vector=LineCableModels.plot(indexed([-1.,-2.,-3.],([4,7,9],));options...) + @test first.(only(filter(p -> p isa Makie.Scatter,only(vector.axes).scene.plots))[1][])==[4.,7.,9.] + @test only(vector.axes).xlabel[]=="Original element index" + scalar=LineCableModels.plot(indexed(2. ± .5);options...) + @test only(scalar.axes).xlabel[]=="Retained element position" + @test length(only(filter(p -> p isa Makie.Scatter,only(scalar.axes).scene.plots))[1][])==1 + @test_throws ArgumentError LineCableModels.plot(indexed(ones(2,2));options...) +end + +@testitem "ObservedResult / retained band associations and sample intersections" tags=[:integration, :engine] begin + using LineCableModels.Engine: retain_gridpoint,compare + using LineCableModels.Commons: gridpoint_id,observation_product + base=LineParameters(reshape(complex.([1.,2.,3.],[2.,3.,4.]),1,1,3),fill(1+2im,1,1,3),[1.,10.,100.]) + reference=retain_gridpoint(base,gridpoint_id()) + results=[retain_gridpoint(base,gridpoint_id()) for _ in 1:2] + errors=compare(reference,results,[R];bands=((10.,100.),)) + a=ObservedResult(results[1],((R,:,:,[1,3]),(X,:,:,[1,3]));comparisons=errors[1:1]) + b=ObservedResult(results[2],((R,:,:,[1,2]),(X,:,:,[1,2]));comparisons=errors[2:2]) + ref=ObservedResult(reference,((R,:,:,[2,3]),(X,:,:,[2,3]))) + products=observation_product((a,b,ref),R;band=(10.,100.),reference_id=ref.gridpoint.id) + @test [p.coordinates.samples for p in products]==[[3],[2],[2,3]] + @test [p.coordinates.frequencies for p in products]==[[100.],[10.],[10.,100.]] + @test a.quantities[1].coordinates.samples==[1,3] + @test_throws ArgumentError observation_product((a,b,ref),R;band=:absent) + conflicting=merge(only(b.errors),(settings=merge(only(b.errors).settings,(indices=[1,3],)),)) + invalid=ObservedResult(b.gridpoint,b.quantities,[conflicting],b.timings) + @test_throws ArgumentError observation_product((a,invalid,ref),R;band=(10.,100.)) + empty=ObservedResult(a,((R,:,:,1:1),)) + @test isempty(first(observation_product((empty,b,ref),R;band=(10.,100.))).coordinates.samples) + @test_throws ArgumentError observation_product((empty,),R;band=(10.,100.)) + other_reference=retain_gridpoint(base,gridpoint_id()) + additional=compare(other_reference,results[1],[R];bands=((10.,100.),)) + ambiguous=ObservedResult(a.gridpoint,a.quantities,[a.errors;additional],a.timings) + @test_throws ArgumentError observation_product((ambiguous,),R;band=(10.,100.)) + @test only(observation_product((ambiguous,),R;band=(10.,100.),reference_id=ref.gridpoint.id)).coordinates.samples==[3] +end + +@testitem "Makie / observed public forwarding preserves native array dispatch" tags=[:visual, :makie] begin + using CairoMakie + extension=Base.get_extension(LineCableModels,:LineCableModelsMakieExt) + @test LineCableModels.plot===LineCableModels.PlotBuilder.plot + for T in (ObservedResult,Vector{ObservedResult},Tuple{ObservedResult,ObservedResult},Vector{LineParameters}) + @test which(Makie.plot,Tuple{T}).module===extension + end + @test which(Makie.plot,Tuple{Vector{Float64}}).module!==extension + source=LineParameters(fill(1.0+2im,1,1,3),fill(1e-6+2e-6im,1,1,3),[1.,10.,100.]) + observed=ObservedResult(source,(R,X);clip=false) + for input in (observed,[observed],(observed,observed)) + p=Makie.plot(input;ydata=(R,),backend=:cairo,display_plot=false,controls=false) + @test p isa UIPlot + @test length(p.axes)==1 + end +end + +@testitem "Makie / public conveniences re-express retained display units" tags=[:visual, :makie, :slow] begin + using CairoMakie, Measurements + using LineCableModels.Engine: retain_gridpoint,completed_formulation,compare + using LineCableModels.Commons: gridpoint_id,observation_product + using LineCableModels.ReportBuilder: ReportArtifact,BenchmarkTableDefinition + U=LineCableModels.Units + options=(backend=:cairo,display_plot=false,controls=false,open_export=false) + source_id=gridpoint_id().source_id + z=reshape(complex.(1.:12.,21.:32.),2,2,3) + f=[10.,100.,1000.] + point(index;values=z,frequency=f)=retain_gridpoint(LineParameters(copy(values),values.*1e-6,frequency), + gridpoint_id(;source_id,problem_index=index,formulation_index=index);fields=completed_formulation(Formulation())) + a,b=point(1),point(2;values=2z) + reference=point(3;values=.5z) + request=@observe R[:,:,:] + curves(p)=filter(item -> item isa Makie.Lines,first(p.axes).scene.plots) + data(p)=[copy(c[1][]) for c in curves(p)] + raw=LineCableModels.plot(a;ydata=(request,),length_unit=:base,options...) + retained=ObservedResult(a,(request,);complete_pairs=true,length_unit=:base) + explicit=LineCableModels.plot(retained;ydata=(request,),options...) + @test data(raw)==data(explicit) + @test keys(raw.plot_state.panel_data)==keys(explicit.plot_state.panel_data) + for inputs in ([a,b],(a,b),ParametricResult(nothing,[a,b],(problems=[:unused],formulations=[:a,:b]),ComputationDetails())) + p=LineCableModels.plot(inputs;ydata=(request,),length_unit=:base,reference,options...) + @test last.(curves(p)[1][1][])≈real.(z[1,1,:]) + @test last.(curves(p)[2][1][])≈2real.(z[1,1,:]) + @test last.(curves(p)[3][1][])≈.5real.(z[1,1,:]) + @test all(o -> observation_product(o,request).unit==U.units(:base,:ohm;per=(:base,:meter)),p.plot_state.observed) + end + positional=LineCableModels.plot(a,b,(request,);length_unit=:base,options...) + @test length(curves(positional))==2 + native=Makie.plot(a;ydata=(request,),length_unit=:base,options...) + @test data(native)==data(raw) + named=LineCableModels.plot((first_case=a,second_case=b);ydata=(request,),reference,length_unit=:base,options...) + @test Set(values(named.plot_state.labels))==Set(("first_case","second_case",description(Formulation().methods.earth_impedance;compact=true)*" (reference)")) + rich_labels=(rich("First",font=:bold),rich("Second",font=:italic)) + rich_page=LineCableModels.plot((a,b);ydata=(request,),reference,series_labels=rich_labels,options...) + @test rich_page.plot_state.labels[:result_1]===rich_labels[1] + @test endswith(rich_page.plot_state.labels[:result_3]," (reference)") + kilo=ObservedResult(a;length_unit=:kilo) + original=deepcopy(kilo) + for (units,scale) in ((nothing,1000.),(:kilo,1000.),(:base,1.)) + p=units===nothing ? LineCableModels.plot(kilo;ydata=(R,),options...) : + LineCableModels.plot(kilo;ydata=(R,),length_unit=units,options...) + @test last.(only(curves(p))[1][])≈scale.*real.(z[1,1,:]) + repeated=LineCableModels.plot(first(p.plot_state.observed);ydata=(R,),length_unit=:base,options...) + @test data(repeated)==data(raw) + end + single=ObservedResult(kilo,(R,);length_unit=:base) + @test length(single.quantities)==1 + @test data(LineCableModels.plot(single;ydata=(R,),length_unit=:base,options...))==data(raw) + freq=LineCableModels.plot(single;ydata=(R,),freq_unit=:kilo,options...) + @test first.(only(curves(freq))[1][])≈f./1000 + @test first.(only(curves(LineCableModels.plot(first(freq.plot_state.observed);frequency_unit=:base,options...)))[1][])≈f + @test data(LineCableModels.plot(kilo;ydata=(R,),units=(U.units(:base,:ohm;per=(:base,:meter)),),options...))==data(raw) + @test last.(only(curves(LineCableModels.plot(single;ydata=(R,),quantity_units=:milli,options...)))[1][])≈1000real.(z[1,1,:]) + mixed=LineCableModels.plot((kilo,b);ydata=(R,),length_unit=:base,options...) + @test last.(curves(mixed)[1][1][])≈real.(z[1,1,:]) + mixed_omitted=LineCableModels.plot((kilo,b);ydata=(R,),options...) + @test last.(curves(mixed_omitted)[1][1][])≈1000real.(z[1,1,:]) + + comparisons=compare(reference,a,[R];bands=(:all,)) + observed=ObservedResult(a;comparisons,length_unit=:kilo,timings=(seconds=.1,)) + ref=ObservedResult(reference;length_unit=:kilo) + artifact=report(BenchmarkTableDefinition(),[observed];reference=ref) + saved_tables=deepcopy(artifact.tables) + # Reopening these raw sources after detachment would return invalid data. + Z(a).=NaN;Z(reference).=NaN + for results in (observed,[observed]) + report_handle=ReportArtifact(results,ref,artifact.tables,nothing,nothing) + p=LineCableModels.plot(report_handle;ydata=(R,),length_unit=:base,options...) + @test last.(curves(p)[1][1][])≈real.(z[1,1,:]) + @test last.(curves(p)[2][1][])≈.5real.(z[1,1,:]) + @test first(p.plot_state.observed).errors==observed.errors + @test first(p.plot_state.observed).timings==observed.timings + axisscale!(p,:y,:log10);resetview!(p) + mktempdir() do directory + @test isfile(export_svg(p;path=joinpath(directory,"retained.svg"),open_file=false)) + end + @test length(curves(LineCableModels.plot(report_handle;ydata=(R,),reference=nothing,length_unit=:base,options...)))==1 + @test length(curves(LineCableModels.plot(report_handle;ydata=(R,),reference=single,length_unit=:base,options...)))==2 + end + @test isequal(artifact.tables,saved_tables) + @test isequal(kilo.quantities,original.quantities) + @test kilo.gridpoint==original.gridpoint + for kwargs in ((clip=false,),(atol=0.,),(frequencies=[1.,2.,3.],), + (freq_unit=:kilo,frequency_unit=:base,), (units=(U.units(:base,:farad),),)) + @test_throws ArgumentError LineCableModels.plot(single;ydata=(R,),kwargs...,options...) + end + @test_throws ArgumentError LineCableModels.plot(single;ydata=(X,),options...) + @test_throws ArgumentError LineCableModels.plot(single,(R,);ydata=(R,),options...) + @test_throws ArgumentError LineCableModels.plot(b;ydata=(R,),freq_unit=:base,frequency_unit=:kilo,options...) +end diff --git a/test/integration/retained_reporting.jl b/test/integration/retained_reporting.jl new file mode 100644 index 000000000..9589dc82b --- /dev/null +++ b/test/integration/retained_reporting.jl @@ -0,0 +1,62 @@ +@testitem "ReportBuilder / explicit retained selections and concrete empty-band errors" tags=[:integration, :report, :slow] begin + using LineCableModels.ReportBuilder: BenchmarkTableDefinition + using LineCableModels.Engine: compare + using DataFrames + records=[NamedTuple(Formulation()),NamedTuple(Formulation(earth_admittance=:default))] + samples=Float32[1,10] + z=fill(ComplexF32(1,2),2,2,2) + y=fill(ComplexF32(0,1e-6),2,2,2) + reference=LineParameters(PhaseDomain,z,y,samples;details=ComputationDetails(;coordinates=["a","b"],)) + result=LineParameters(PhaseDomain,2z,2y,samples;details=ComputationDetails(;coordinates=["a","b"],)) + artifact=report(BenchmarkTableDefinition(quantities=(Z,G),bands=(:all,)),(;reference,result)) + @test nrow(report(BenchmarkTableDefinition(quantities=(G,)),artifact.observed).tables.maxima)==1 + @test_throws ArgumentError report(BenchmarkTableDefinition(bands=(:wide,)),artifact.observed) + @test_throws ArgumentError report(BenchmarkTableDefinition(atol=(G=1f-8,)),artifact.observed) + @test_throws r"unknown" BenchmarkTableDefinition(no_such_control=true) + @test_throws r"pairing" BenchmarkTableDefinition(pairing=[(1,1),(2,1)]) + @test_throws r"scalar reference" report(BenchmarkTableDefinition(pairing=[(2,1)]),(;reference,result)) + @test all(ismissing,filter(row -> row.quantity === :G,artifact.tables.terms).relative_rms_percent) + @test all(ismissing,filter(row -> row.quantity === :G,artifact.tables.terms).absolute_rms) + far=LineParameters(PhaseDomain,z,y,Float32[1e7,2e7];details=ComputationDetails(;coordinates=["a","b"],)) + space=ParametricResult(nothing,[reference,far],(problems=[:near,:far],formulations=records[1:1]), ComputationDetails((;))) + errors=compare(space,space,Z;pairing=[(1,1),(2,2)],band=:wide) + @test isconcretetype(eltype(errors)) + @test typeof(errors[1]) === typeof(errors[2]) + @test Base.nonmissingtype(eltype(errors[2].absolute)) === Float32 + @test all(ismissing,errors[1].absolute) + @test all(iszero,errors[2].absolute) + @test errors.axes === space.axes + @test errors[2].details.data.actual_bounds == (1f7,2f7) +end + +@testitem "ReportBuilder / two-sided RMS eligibility survives tables and explicit reanalysis" tags=[:integration, :report] begin + using LineCableModels.ReportBuilder: BenchmarkTableDefinition + using LineCableModels.Engine: RMSError + using DataFrames + f = [50.0, 500.0] + z = ones(ComplexF64, 1, 1, 2) + reference = LineParameters(PhaseDomain,z,fill(1e-6+1e-4im,1,1,2),f; + details=ComputationDetails(;coordinates=["a"],)) + result = LineParameters(PhaseDomain,z,fill(1e-14+1e-4im,1,1,2),f; + details=ComputationDetails(;coordinates=["a"],)) + definition = BenchmarkTableDefinition(quantities=(G,Y),bands=(:all,)) + artifact = report(definition,(;reference,result)) + g = only(eachrow(filter(row -> row.quantity === :G,artifact.tables.terms))) + @test ismissing(g.relative_rms_percent) + @test g.status === :result_below_tolerance + @test ismissing(g.absolute_rms) + @test only(filter(row -> row.quantity === :G,artifact.tables.maxima).unavailable) == 1 + @test !ismissing(only(filter(row -> row.quantity === :Y,artifact.tables.terms).relative_rms_percent)) + # Reading recorded errors never reruns comparison. Explicit fresh comparison + # operates on the raw operands and leaves the earlier observation unchanged. + ineligible=first(artifact.observed.errors) + malformed=merge(ineligible,(relative=fill(1.,1,1),)) + @test_throws ArgumentError ObservedResult(artifact.observed.gridpoint,artifact.observed.quantities,[malformed],artifact.observed.timings) + original=only(filter(row -> row.quantity==quantity(Y),artifact.observed.errors)) + recorded=merge(original,(relative=fill(1.,1,1),)) + retained=ObservedResult(artifact.observed.gridpoint,artifact.observed.quantities,[recorded],artifact.observed.timings) + @test only(report(BenchmarkTableDefinition(),retained).tables.terms.relative_rms_percent)==100 + refreshed=report(definition,(;reference,result)) + @test ismissing(only(filter(row -> row.quantity===:G,refreshed.tables.terms).relative_rms_percent)) + @test only(first(retained.errors).relative)==1 +end diff --git a/test/integration/scalar_formulation_records.jl b/test/integration/scalar_formulation_records.jl new file mode 100644 index 000000000..44d2e3d55 --- /dev/null +++ b/test/integration/scalar_formulation_records.jl @@ -0,0 +1,69 @@ +@testitem "ReportBuilder / scalar results retain physical and native formula choices" tags=[:integration, :importexport, :slow] setup=[FormulaFixtures] begin + using LinearAlgebra, Serialization, JSON3 + using LineCableModels.ReportBuilder: BenchmarkTableDefinition + copper=Material(MaterialsLibrary(add_defaults = true), :copper) + design=build(CableDesign, "scalar-records", terminal(:core, solid(copper, Disk(0.0425)))) + system=build(LineCableSystem, [design, design], [Pose2(0, -1), Pose2(1, -1)]; + connections = [Dict(:core=>1), Dict(:core=>2)]) + problem=LineParametersProblem(system; earth_props = homogeneous(rho = 100.0), frequencies = [1e5]) + # Material coefficients are selectable. Receiving-layer voltage references + # are determined by the physical geometry, not formulation parameters. + coefficients=(0.1, 1.0) + choices=[Formulation(earth_properties = formula(:portela1999; parameters = (; beta))) + for beta in coefficients] + results=compute.(Ref(problem), choices) + @test norm(Y(results[1])-Y(results[2]))/norm(Y(results[1])) > 0.1 + @test typeof(results[1]) === typeof(results[2]) + for (result, selection, beta) in zip(results, choices, coefficients) + record=details(result).data.formulations + @test record.requested == NamedTuple(selection).requested + @test record.methods == NamedTuple(selection).methods + @test record.requested.earth_properties.parameters.beta === beta + @test record.methods.earth_properties.parameters.beta === beta + end + artifact=report(BenchmarkTableDefinition(), ( + reference = results[1], result = results[2])) + @test artifact.reference.gridpoint.formulations != + artifact.observed.gridpoint.formulations + @test artifact.tables.formulations.label[1] != artifact.tables.formulations.label[2] + @test all(!isempty, artifact.tables.formulations.label) + io=IOBuffer(); + serialize(io, results); + seekstart(io) + restored=deserialize(io) + # Unavailable cells must remain unavailable after serialization. == propagates + # missing instead of comparing the retained availability mask. + @test isequal( + report(BenchmarkTableDefinition(), ( + reference = restored[1], result = restored[2])).tables.formulations, + artifact.tables.formulations) + custom=FormulaFixtures.DispersiveEarth() + selected=Formulation(earth_properties = custom) + changed=compute(problem, selected) + @test !isempty(custom.seen) + @test details(changed).data.formulations.requested.earth_properties==NamedTuple(custom) + @test details(changed).data.formulations.methods.earth_properties==NamedTuple(custom) + @test details(changed).data.formulations.methods.earth_properties.identifier === :DispersiveEarth + @test typeof(changed) === typeof(results[1]) + # A saved and reloaded result has the details it was saved with, labels included, for a + # built-in and for a custom formula. Loading builds no formulation. + IE=LineCableModels.ImportExport + for saved in (results[1], changed) + loaded=IE.deserialize_value(JSON3.read(JSON3.write(IE.serialize_value(saved)), Dict{String,Any})) + @test isequal(details(loaded).data, details(saved).data) + @test typeof(details(loaded).data) === typeof(details(saved).data) + @test details(loaded).data.formulation_fields == details(saved).data.formulation_fields + @test Z(loaded) == Z(saved) && Y(loaded) == Y(saved) + end + grid=Formulation( + earth_properties = Grid([c.definitions.earth_properties for c in choices]); + combine = :zip) + batch=compute(problem, grid) + @test isconcretetype(eltype(batch)) + @test length(batch)==2 + for i in 1:2 + @test Z(batch[i]) == Z(results[i]) + @test Y(batch[i]) == Y(results[i]) + @test details(batch[i]).data.formulations == details(results[i]).data.formulations + end +end diff --git a/test/integration/scan_timing.jl b/test/integration/scan_timing.jl new file mode 100644 index 000000000..b84303c8e --- /dev/null +++ b/test/integration/scan_timing.jl @@ -0,0 +1,253 @@ +@testitem "Scan timing / stage ownership, progress, composition and persistence" tags=[:integration, :measurements, :slow] setup=[TestFixtures] begin + using Logging, Measurements, JSON3, Serialization + const E=LineCableModels.Engine + const IE=LineCableModels.ImportExport + problem=TestFixtures.three_bare_wires_problem(frequencies = [50.0, 1000.0]) + formulation=Formulation() + plain=compute(problem, formulation) + fields=(:wall_seconds, :bytes, :gc_seconds, :compile_seconds, :recompile_seconds) + timed_result=Ref{Any}(nothing) + for timing in (false, true), progress in (0, 1) + + logger=Test.TestLogger() + calls=Int[] + result=with_logger(logger) do + compute(problem, + formulation; + options = (; timing, verbosity = (default = 0, progress), + on_result = (_, i, + value)->begin + @test haskey(details(value).data, :timing) == timing + push!(calls, i) + end)) + end + @test calls == [1] + @test Z(result) == Z(plain) + @test Y(result) == Y(plain) + @test frequencies(result) == frequencies(plain) + @test haskey(details(result).data, :timing) == timing + if timing + timed_result[]=result + record=details(result).data.timing + @test keys(record) == fields + @test all(>=(0), values(record)) + @test record.bytes isa Integer + end + logs=filter(record->record.group===:progress, logger.logs) + @test all(record -> record.group === :progress, logger.logs) + @test isempty(logs) == (progress == 0) + if progress==1 + @test first(logs).message == "Line parameters computation started" + @test last(logs).message == "Line parameters computation completed successfully" + @test last(logs).kwargs[:completed] == 1 + @test last(logs).kwargs[:elapsed_seconds] >= 0 + end + end + timed=timed_result[] + # Completion attachment retains storage and accommodates empty reuse records. + reused=E.retain_gridpoint(timed, details(timed).data.gridpoint; fields = (timing = (;),)) + @test typeof(reused) === typeof(timed) + @test Z(reused) === Z(timed) + @test Y(reused) === Y(timed) + @test details(reused).data.gridpoint === details(timed).data.gridpoint + for result in (plain, timed, reused) + restored=IE.deserialize_value(JSON3.read(JSON3.write(IE.serialize_value(result)), Dict{ + String, Any})) + @test Z(restored) == Z(result) + @test get(details(restored).data, :timing, nothing) == + get(details(result).data, :timing, nothing) + end + calls=Int[] + forms=[formulation, formulation, formulation] + batch=compute(problem, forms; options = ( + timing = true, on_result = (_, i, _)->push!(calls, i))) + @test calls == [1, 2, 3] + @test all(value -> keys(details(value).data.timing) == fields, batch) + @test all(value -> Z(value) == Z(plain) && Y(value) == Y(plain), batch) + @test all(value -> typeof(value) === typeof(first(batch)), batch) + # Shared-input lookup must not overwrite the throughput clock. A real + # delayed completion makes an intermediate record available for bounds. + batch_log=Test.TestLogger() + with_logger(batch_log) do + compute(problem, forms; options=(verbosity=(default=0, progress=1), + on_result=(_, i, _)->(i == 1 && sleep(5.1)))) + end + intermediate=filter(record->record.message == "Line parameters progress", batch_log.logs) + @test !isempty(intermediate) + @test all(record->0 <= record.kwargs[:eta_hours] * 3600 <= + record.kwargs[:elapsed_seconds] * + (record.kwargs[:total]-record.kwargs[:completed]), intermediate) + # Materialization and formulation ordering stay with the existing traversal. + builds=Ref(0) + space=Gridspace{LineParametersProblem}( + temperature->begin + builds[]+=1 + LineParametersProblem(problem.system; frequencies = problem.frequencies, + earth_props = problem.earth_props, temperature) + end, + (Grid((20.0, 25.0)),)) + choices=Gridspace{typeof(formulation)}(identity, (Grid(Tuple(forms)),)) + log=Test.TestLogger() + product=with_logger(log) do + compute( + ParametricProblem(space, + ComputationOptions(timing = true, verbosity = (default = 0, progress = 1))), + Combinatorial(choices)) + end + @test builds[] == 2 + @test length(product) == 6 + @test isempty(details(product).data) + @test all(value -> haskey(details(value).data, :timing), product) + @test !any(record -> occursin("Line parameters computation", string(record.message)), log.logs) + @test last(log.logs).message == "Parametric computation completed successfully" + @test last(log.logs).kwargs[:completed] == 6 + uncertain_space=Gridspace{LineParametersProblem}( + radius->TestFixtures.three_bare_wires_problem(; frequencies = problem.frequencies, radius), + (Grid((0.04, 0.0425), AbsoluteError(0.0001)),)) + study=ParametricProblem(uncertain_space, ComputationOptions(timing = true)) + linear=compute(study, LinearError(formulation)) + @test isempty(details(linear).data) + @test haskey(details(first(linear)).data, :timing) + restored=IE.deserialize_value(JSON3.read(JSON3.write(IE.serialize_value(linear)), Dict{ + String, Any})) + @test details(first(restored)).data.timing == details(first(linear)).data.timing + @test nominal.(Z(first(restored))) == nominal.(Z(first(linear))) + @test uncertainty.(Z(first(restored))) == uncertainty.(Z(first(linear))) + mc=MonteCarlo(formulation; trials = 4, seed = 71) + sampled=compute(study, mc) + replay=compute(ParametricProblem(uncertain_space), mc) + @test statistics(sampled) == statistics(replay) + @test sampled.point_seeds == replay.point_seeds + @test sampled.trial_counts == replay.trial_counts == [4, 4] + @test keys(details(sampled).data) == (:timing,) + @test length.(details(sampled).data.timing) == [4, 4] + @test samples(sampled) === nothing + @test histograms(sampled) === nothing + @test !haskey(details(first(sampled)).data, :timing) + for retain_details in (false, true), return_samples in (false, true), + return_histograms in (false, true) + value=compute(study, + MonteCarlo(formulation; trials = 2, seed = 71, retain_details, + return_samples, return_histograms, bins = 2)) + local restored_mc=IE.deserialize_value(JSON3.read( + JSON3.write(IE.serialize_value(value)), Dict{String, Any})) + @test details(restored_mc).data.timing == details(value).data.timing + @test statistics(restored_mc) == statistics(value) + @test haskey(details(restored_mc).data, :trials) == retain_details + end + # The scientific record codec is also valid through Julia serialization. + buffer=IOBuffer() + serialize(buffer, IE.serialize_value(sampled)) + seekstart(buffer) + @test details(IE.deserialize_value(deserialize(buffer))).data.timing == + details(sampled).data.timing + for timing in (false, true), progress in (0, 1) + + options=ComputationOptions(; timing, verbosity = (default = 0, progress)) + local combination_log=Test.TestLogger() + values=with_logger(combination_log) do + (compute(problem, forms; options), + compute(ParametricProblem(space, options), Combinatorial(choices)), + compute(ParametricProblem(uncertain_space, options), LinearError(formulation)), + compute(ParametricProblem(uncertain_space, options), mc)) + end + @test all(value -> Z(value) == Z(plain) && Y(value) == Y(plain), values[1]) + @test Z.(values[2]) == Z.(product) + @test Y.(values[2]) == Y.(product) + @test nominal.(Z(first(values[3]))) == nominal.(Z(first(linear))) + @test uncertainty.(Z(first(values[3]))) == uncertainty.(Z(first(linear))) + @test statistics(values[4]) == statistics(sampled) + @test values[4].point_seeds == sampled.point_seeds + @test haskey(details(values[4]).data, :timing) == timing + @test count(record -> endswith(string(record.message), "started"), combination_log.logs) == + 4progress + @test count(record -> endswith(string(record.message), "completed successfully"), combination_log.logs) == + 4progress + end + # Rejected physical builds do not create successful timing entries. + rejected_space=Gridspace{LineParametersProblem}( + radius->begin + radius>0||throw(DomainError(radius, "wire radius must be positive")) + TestFixtures.three_bare_wires_problem(; frequencies = [50.0], radius) + end, + (Grid(0.005, AbsoluteError(0.01)),)) + retried=compute(ParametricProblem(rejected_space, ComputationOptions(timing = true)), + MonteCarlo(formulation; trials = 4, seed = 19, retain_details = true, + on_error = :resample, max_failures = 50)) + @test !isempty(only(details(retried).data.failures)) + @test length(only(details(retried).data.timing)) == only(retried.trial_counts) == 4 + function replace_details(value, retained) + MonteCarloResult(value.formulation, value.values, value.stats, value.sample_values, + value.histogram_values, value.root_seed, value.point_seeds, value.trial_counts, retained) + end + @test_throws DimensionMismatch replace_details(sampled, ComputationDetails(timing = [NamedTuple[], NamedTuple[]])) + @test_throws DimensionMismatch replace_details(sampled, ComputationDetails(timing = Vector{NamedTuple}[])) + @test_throws ArgumentError replace_details(sampled, ComputationDetails(unrelated = [1])) + for owner in (LineCableModelsCoaxial, LineCableModelsFEM), value in (1, :yes, nothing) + + @test_throws ArgumentError computation_options(owner, ComputationOptions(timing = value)) + end + for owner in (LineCableModelsCoaxial, LineCableModelsFEM) + @test !computation_options(owner, ComputationOptions()).data.timing + @test_throws ArgumentError computation_options( + owner, ComputationOptions(timing = true, unrelated = true)) + end + @test_throws ArgumentError computation_options(CableConstantsFormulation, ComputationOptions(timing = true)) + @test_throws ArgumentError compute(ParametricProblem(space, ComputationOptions(timing = 1)), mc) +end + +@testitem "Scan timing / benchmark repeats completed projections" tags=[:integration, :measurements] setup=[TestFixtures] begin + using Measurements + problem=TestFixtures.three_bare_wires_problem(frequencies = [50.0]) + formulation=Formulation() + timed=compute(problem, formulation; options = (timing = true,)) + reused=LineCableModels.Engine.retain_gridpoint( + timed, details(timed).data.gridpoint; fields = (timing = (;),)) + calls=Ref(0) + record=LineCableModels.benchmark(; samples = 3, warmup = 2) do + calls[]+=1 + isodd(calls[]) ? timed : reused + end + @test calls[] == 5 + @test record.result === timed + @test record.timings == [details(timed).data.timing, (;), details(timed).data.timing] + builds = Ref(0) + space = Gridspace{LineParametersProblem}(radius -> begin + builds[] += 1 + TestFixtures.three_bare_wires_problem(; frequencies=[50.], radius) + end, (Grid((0.04, 0.0425)),)) + study = ParametricProblem(space, ComputationOptions(timing=true)) + for result in ([timed, reused], + compute(study, Combinatorial(Grid((formulation, formulation, formulation)))), + compute(study, LinearError(formulation)), + compute(study, MonteCarlo(formulation; trials = 2, seed = 1))) + before = builds[] + projected=LineCableModels.benchmark(()->result; samples = 2) + @test builds[] == before + @test projected.result === result + @test projected.timings[1] == projected.timings[2] + @test projected.timings[1] !== projected.timings[2] + if result isa MonteCarloResult + @test projected.timings[1] == details(result).data.timing + @test projected.timings[1] !== details(result).data.timing + @test projected.timings[1][1] !== details(result).data.timing[1] + else + @test projected.timings[1] == [details(core).data.timing for core in result] + end + end + for count in (0, -1, true, 1.5) + @test_throws ArgumentError LineCableModels.benchmark(() -> timed; samples = count) + end + for count in (-1, true, 1.5) + @test_throws ArgumentError LineCableModels.benchmark(() -> timed; warmup = count) + end + @test_throws r"timing=true" LineCableModels.benchmark(() -> compute(problem, formulation)) + @test_throws r"timing=true" LineCableModels.benchmark(() -> [ + timed, compute(problem, formulation)]) + calls[]=0 + @test_throws ErrorException LineCableModels.benchmark(; samples = 3) do + calls[] += 1 + error("failure") + end + @test calls[] == 1 +end diff --git a/test/integration/shunt_model.jl b/test/integration/shunt_model.jl new file mode 100644 index 000000000..5e72e5eb7 --- /dev/null +++ b/test/integration/shunt_model.jl @@ -0,0 +1,113 @@ +@testitem "Engine / shunt model / selection, fallback and blueprint coefficients" tags=[:integration, :importexport, :slow] begin + using Logging + E = LineCableModels.Engine + IE = LineCableModels.ImportExport + include(joinpath(pkgdir(LineCableModels), "test", "support", "internal_shunt.jl")) + design = internal_shunt_test_design(count = 4) + problem = CableConstantsProblem(design) + default = CableConstantsFormulation() + equivalent = CableConstantsFormulation(shunt_model = :equivalent) + nominal = @inferred compute(problem, default) + @test nominal == compute(problem, equivalent) + @test details(nominal).data.shunt_model.effective === :equivalent + @test details(nominal).data.shunt_model.solves == 0 + @test isempty(details(nominal).data.shunt_model.diagnostics) + @test only(details(nominal).data.shunt_model.domains).terminals == 1:3 + @test occursin("no boundary extraction or audit", + only(details(nominal).data.shunt_model.domains).message) + @test isempty(E.flatten(LineCableModelsCoaxial(), design, default).shunt) + @test E.formula_id(default.methods.shunt_model) === :equivalent + invalid = (formula(:no_such_model), formula(:equivalent; options = (audit = true,)), + formula(:boundary; parameters = (fallback = :silent,)), + formula(:boundary; options = (integration = (rtol = -1.0,),)), + formula(:boundary; options = (resolution = (wire = 0,),)), + formula(:boundary; options = (resolution = (order = 8, quadrature = 8),))) + for value in invalid + @test_throws ArgumentError Formulation(shunt_model = value) + @test_throws ArgumentError CableConstantsFormulation(shunt_model = value) + end + # Exhaust a declared numerical budget before allocating a dense matrix. + budget = (resolution = (wire = 100_000,),) + strict = CableConstantsFormulation(shunt_model = formula(:boundary; options = budget)) + @test_throws BoundarySolveError compute(problem, strict) + fallback = CableConstantsFormulation(shunt_model = formula(:boundary; + parameters = (fallback = :equivalent,), options = budget)) + result = @test_logs (:warn, r"replaced by the equivalent annular layer") compute(problem, fallback) + report = details(result).data.shunt_model + @test result == nominal + @test report.requested === :boundary && report.effective === :equivalent + @test only(report.domains).reason === :budget + @test occursin("budget", only(report.domains).message) + @test_throws ArgumentError MonteCarlo(fallback; trials = 2) + @test_throws ArgumentError LinearError(fallback) + quadrature = CableConstantsFormulation(shunt_model = formula(:boundary; + options = (resolution = (wire = 16, order = 8, quadrature = 64, modes = 128), + integration = (rtol = 1e-15, atol = 0.0, maxevals = 1)))) + logger = Test.TestLogger(; min_level = Logging.Warn) + approximate = with_logger(logger) do + compute(problem, quadrature) + end + warnings = filter(log -> occursin("logarithmic quadrature target", log.message), logger.logs) + @test !isempty(warnings) + @test all(log -> log.kwargs[:context].evaluations > 0 && + log.kwargs[:context].estimate > log.kwargs[:context].tolerance, warnings) + @test all(isfinite, approximate.C) + @test details(approximate).data.shunt_model.effective === :boundary + @test details(approximate).data.shunt_model.solves == 1 + permitted_fallback = CableConstantsFormulation(shunt_model = formula(:boundary; + parameters = (fallback = :equivalent,), options = quadrature.definitions.shunt_model.options)) + fallback_logger = Test.TestLogger(; min_level = Logging.Warn) + unchanged = with_logger(fallback_logger) do + compute(problem, permitted_fallback) + end + @test unchanged.C == approximate.C + @test details(unchanged).data.shunt_model.effective === :boundary + @test all(log -> !occursin("replaced by the equivalent annular layer", log.message), fallback_logger.logs) + lossy = CableConstantsFormulation(shunt_model = :boundary, insulation_admittance = :lossy) + @test_throws BoundarySolveError compute(problem, lossy) + boundary = CableConstantsFormulation(shunt_model = formula(:boundary; + options = (resolution = (wire = 16, order = 8, quadrature = 64, modes = 128),))) + blueprint = @inferred E.flatten(LineCableModelsCoaxial(), design, boundary) + @test length(blueprint.shunt) == 1 + @test blueprint.shunt_details.solves == 1 + @test E.flatten(LineCableModelsCoaxial(), design, Float32, boundary) isa + E.CableBlueprint{Float32} + first_result = compute(problem, boundary) + explicit_lossless = CableConstantsFormulation( + shunt_model = boundary.definitions.shunt_model, + insulation_admittance = :lossless, semicon_admittance = :lossless) + @test compute(problem, explicit_lossless) == first_result + workspace = E.CableConstantsWorkspace(problem, boundary, blueprint) + @test workspace.cable.shunt[1].C === blueprint.shunt[1].C + @test E._solve!(workspace, problem, boundary) == first_result + @test details(first_result).data.shunt_model.effective === :boundary + @test only(details(first_result).data.shunt_model.diagnostics).boundary_residual === + nothing + @test typeof(first_result) === typeof(nominal) + @test compute(problem, boundary) == first_result + @test compute(CableConstantsProblem(design; frequency = 60), boundary).C == + first_result.C + changed = CableConstantsProblem(internal_shunt_test_design(count = 4, epsilon = 4.0)) + @test compute(changed, boundary).C != first_result.C + @test compute(problem, default) == nominal + for id in E.ShuntModel.formulas(E.ShuntModel.Formula) + selected = E.ShuntModel.Formula(id) + @test occursin("id=" * string(formula_id(selected)), sprint(show, selected)) + @test occursin(string(formula_id(selected)), sprint(show, MIME"text/plain"(), selected)) + end + @test E.BoundarySolveError === E.ShuntModel.BoundarySolveError + @test which(show, (IO, MIME"text/plain", BoundarySolveError)).module === E.ShuntModel + for formulation in + (boundary, Formulation(shunt_model = boundary.definitions.shunt_model)) + @test occursin("boundary", description(formulation, C)) + end + for source in (first_result, result) + restored=IE.deserialize_value(IE.serialize_value(source)) + @test restored == source + @test details(restored).data.shunt_model.effective === + details(source).data.shunt_model.effective + @test details(restored).data.formulations == details(source).data.formulations + @test only(details(restored).data.shunt_model.domains).reason === + only(details(source).data.shunt_model.domains).reason + end +end diff --git a/test/integration/temperature_dependence.jl b/test/integration/temperature_dependence.jl new file mode 100644 index 000000000..4c70b43ec --- /dev/null +++ b/test/integration/temperature_dependence.jl @@ -0,0 +1,124 @@ +@testitem "Engine / shared temperature law reaches scalar, Gridspace, constants and export" tags=[:integration, :pscad] setup=[FormulaFixtures] begin + copper = Material(:conductor,1.72e-8,1,1,20,0.004) + dielectric = Material(:insulator,1e7,2.3,1,20,-0.003;tan_delta=0.025) + function cable_with(metal, passive) + build(CableDesign,"temperature-law",terminal(:core, + core(metal;r=0.005),insulation(passive;t=0.005))) + end + function system_with(design) + build(LineCableSystem,[design,design],[(0.0,-1.0),(0.1,-1.0)]; + connections=[Dict(:core=>1),Dict(:core=>2)]) + end + design = cable_with(copper,dielectric) + system = system_with(design) + problem = LineParametersProblem(system;temperature=80.0,frequencies=[50.0,1000.0],earth_props=homogeneous(rho=100.0)) + declaration=FormulaFixtures.ScaledResistivity() + selected = Formulation(temperature_dependence=declaration, + insulation_admittance=:lossy,options=(ideal_transposition=false,)) + reference_design = cable_with( + Material(:conductor,2copper.rho,copper.eps_r,copper.mu_r), + Material(:insulator,2dielectric.rho,dielectric.eps_r,dielectric.mu_r; + tan_delta=dielectric.tan_delta)) + reference_problem = LineParametersProblem(system_with(reference_design); + temperature=80.0,frequencies=problem.frequencies,earth_props=problem.earth_props) + identity = Formulation(temperature_dependence=nothing, + insulation_admittance=:lossy,options=selected.options) + reference = compute(reference_problem,identity) + actual = compute(problem,selected) + @test !isempty(declaration.seen) + @test actual.Z.values ≈ reference.Z.values rtol=2e-13 + @test actual.Y.values ≈ reference.Y.values rtol=2e-13 + @test details(actual).data.formulations.methods.temperature_dependence.identifier === :ScaledResistivity + grid = Formulation(temperature_dependence=Grid((declaration,nothing)), + insulation_admittance=:lossy,options=selected.options) + results = compute(problem,grid) + @test results[1].Z.values == actual.Z.values + @test results[1].Y.values == actual.Y.values + unchanged = compute(problem,identity) + @test results[2].Z.values == unchanged.Z.values + @test results[2].Y.values == unchanged.Y.values + @test !isapprox(results[1].Z.values,results[2].Z.values;rtol=1e-5) + constants = compute(CableConstantsProblem(design;temperature=80.0), + CableConstantsFormulation(temperature_dependence=declaration,insulation_admittance=:lossy)) + reference_constants = compute(CableConstantsProblem(reference_design;temperature=80.0), + CableConstantsFormulation(temperature_dependence=nothing,insulation_admittance=:lossy)) + for request in (R,L,C,G) + @test request(constants) ≈ request(reference_constants) rtol=2e-13 + end + const IE = LineCableModels.ImportExport + exported = only(LineCableModels.PSCAD._pscad_components(LineCableModels.Engine.flatten(LineCableModelsCoaxial(), design),50.0,selected,80.0)) + expected = only(LineCableModels.PSCAD._pscad_components(LineCableModels.Engine.flatten(LineCableModelsCoaxial(), reference_design),50.0,identity,80.0)) + @test exported.conductor.material.rho == expected.conductor.material.rho + @test exported.dielectric.shunt_conductance ≈ expected.dielectric.shunt_conductance + @test exported.dielectric.shunt_capacitance ≈ expected.dielectric.shunt_capacitance + hot = LineParametersProblem(system;temperature=250.0,frequencies=[50.0],earth_props=problem.earth_props) + @test_throws DomainError compute(hot,Formulation()) + @test all(isfinite,compute(hot,identity).Z) + @test all(isfinite,compute(hot,selected).Z) +end + +@testitem "UQ / current AC cable / actual sampling and independently recomputed statistics" tags=[:integration, :measurements] setup=[TestFixtures] begin + using Random + design=TestFixtures.coaxial_design() + temperatures=(20.0,60.0) + space=Gridspace{CableConstantsProblem}(t->CableConstantsProblem(design;temperature=t), + (Grid(temperatures,AbsoluteError(1.0)),)) + problem=ParametricProblem(space) + N=8 + seed=2027 + sampled=compute(problem,MonteCarlo(CableConstantsFormulation();trials=N,seed, + distribution=:uniform,return_samples=true,return_histograms=true,retain_details=true)) + @test sampled.trial_counts==[N,N] + @test length(unique(sampled.point_seeds))==2 + @test all(isempty,sampled.details.data.failures) + # Rebuild each retained draw through scalar compute. Statistics below are + # arithmetic checks on these samples, not distribution-accuracy claims. + for (index,point) in enumerate(LineCableModels.points(space)) + rng=Xoshiro(sampled.point_seeds[index]) + expected=[compute(CableConstantsProblem(design; + temperature=only(LineCableModels.realize_arguments(rng,point,:uniform))), + CableConstantsFormulation()) for _ in 1:N] + for quantity in (:R,:L,:C,:G) + @test vec(getproperty(sampled.sample_values[index],quantity))== + [only(getproperty(value,quantity)) for value in expected] + end + end + quantile7(sorted,p)=begin + position=1+(length(sorted)-1)*p + i=floor(Int,position);fraction=position-i + i==length(sorted) ? last(sorted) : (1-fraction)*sorted[i]+fraction*sorted[i+1] + end + for point in eachindex(temperatures),quantity in (:R,:L,:C,:G) + draws=vec(getproperty(sampled.sample_values[point],quantity)) + summary=only(getproperty(sampled.stats[point],quantity)) + density=only(getproperty(sampled.histogram_values[point],quantity)) + @test length(draws)==N + expected_mean=sum(draws)/N + expected_std=sqrt(sum(x->(x-expected_mean)^2,draws)/(N-1)) + sorted=sort(draws) + @test summary.mean ≈ expected_mean rtol=1e-10 atol=0 + @test summary.std ≈ expected_std rtol=1e-10 atol=eps(maximum(abs,draws)) + @test summary.min==first(sorted) && summary.max==last(sorted) + @test summary.q05 ≈ quantile7(sorted,.05) rtol=1e-10 atol=0 + @test summary.median ≈ quantile7(sorted,.5) rtol=1e-10 atol=0 + @test summary.q95 ≈ quantile7(sorted,.95) rtol=1e-10 atol=0 + counts=zeros(Int,length(density.density)) + for value in draws + index=min(searchsortedlast(density.edges,value),length(counts)) + 1<=index<=length(counts) || error("sample outside declared histogram support") + counts[index]+=1 + end + @test density.density ≈ counts./(N.*diff(density.edges)) rtol=1e-10 atol=0 + @test sum(density.density.*diff(density.edges)) ≈ 1.0 rtol=1e-10 + @test getproperty(sampled.values[point],quantity)[1] ≈ summary.mean rtol=1e-10 atol=0 + end + for point in eachindex(temperatures) + arrays=values(sampled.sample_values[point]);models=values(sampled.histogram_values[point]) + for i in eachindex(arrays),j in eachindex(arrays) + i==j && continue + @test arrays[i] !== arrays[j] + @test only(models[i]).density !== only(models[j]).density + end + @test all(iszero,sampled.sample_values[point].G) + end +end diff --git a/test/manual/README.md b/test/manual/README.md new file mode 100644 index 000000000..991218e2a --- /dev/null +++ b/test/manual/README.md @@ -0,0 +1,66 @@ +# Manual tests + +This tree contains human-operated investigations only. `JuliaTestItems.toml` +excludes it from automated test discovery, so TestItemRunner neither parses nor +runs these files. Nothing here is a CI gate or a maintained feature API. + +Use the active Julia environment when including scripts from an IDE or REPL. +The scripts do not select or install a project on the caller's behalf, except +that `plotting/` has an explicit environment for its optional backends. + +| Area | Purpose | +| --- | --- | +| `performance/` | Local timing and numerical inspection of shunt models. | +| `calculations/` | Editable analytical, grid-space, export, and FEM comparison runs. | +| `fem/` | Direct Gmsh/GetDP investigations and baseline captures. | +| `plotting/` | Human-inspected GL/WGL galleries and plotting experiments. | +| `prototypes/` | Disposable scientific prototypes that do not extend production code. | +| `pscad/` | A manual PSCAD station run and its private local configuration. | +| `verification/` | Manually invoked packaging checks. | + +The filename states the runnable experiment. + +Run `calculations/modal_analysis.jl` with the existing +`test/manual/plotting` project when the active REPL project does not already +provide GLMakie. + +| Script | Manual purpose | +| --- | --- | +| `performance/benchmark_local_shunt.jl` | Inspect local-shunt coefficient construction, allocation, and selected saved references. | +| `performance/benchmark_shunt_models.jl` | Compare warmed public-API shunt-model timings. | +| `calculations/gridspace.jl` | Inspect a small constructed `Gridspace`. | +| `calculations/run_line_parameters.jl` | Run one editable datasheet case through the analytical or FEM backend and export PSCAD input. | +| `calculations/run_two_bare_wires.jl` | Compare two analytical earth-property formulations interactively. | +| `calculations/run_two_bare_wires_fem.jl` | Sweep the two-wire FEM case and inspect CSV/XLSX/plot output. | +| `calculations/modal_analysis.jl` | Direct REPL study of nine-terminal armored cables through phase, modal, segment, observation, report, and interactive GLMakie APIs. | +| `fem/run_18kv_trefoil_fem.jl` | Run the current 18 kV datasheet case through quasi-TEM FEM. | +| `fem/run_quasi_full.jl` | Exercise the disposable quasi-full GetDP formulation directly. | +| `fem/run_three_bare_wires_baseline.jl` | Capture analytical/FEM baseline data for human comparison. | +| `prototypes/prototype_wire_screen.jl` | Investigate the disposable wire-screen geometric boundary model. | +| `plotting/surprised_pikachu_cable.jl` | Preview geometry with arbitrary polygons. | +| `pscad/run_pscad.jl` | Run a two-wire problem on the configured PSCAD station. | +| `verification/verify_getdp_artifact.jl` | Download and verify the packaged GetDP archives. | + +The remaining plotting scripts are described in `plotting/README.md`. + +## Output locations + +Generated output does not belong in the versioned tree. Scripts that explicitly +write reports or images default outside the checkout, under +`joinpath(tempdir(), "linecablemodels-manual", ...)`. Set +`LINECABLEMODELS_MANUAL_OUTPUT` to an absolute directory when the output should +survive temporary-directory cleanup. Backend-specific plotting variables remain +available as overrides and must point outside the checkout. + +FEM and PSCAD engines may retain their own ignored runtime directories according +to their explicit execution configuration. Those directories are runtime state, +not manual-test fixtures. + +For PSCAD, edit the ignored `pscad/local-pscad.toml`, then run +`include("test/manual/pscad/run_pscad.jl")` from the repository root. A +sanitized configuration template is available at +`examples/pscad/remote.example.toml`. Each execution creates a fresh run in the +configured exchange directory and leaves `result`, `Zresult`, and `Yresult` +available for inspection. + +See `plotting/README.md` for the interactive plotting procedures. diff --git a/test/manual/calculations/gridspace.jl b/test/manual/calculations/gridspace.jl new file mode 100644 index 000000000..ba976b385 --- /dev/null +++ b/test/manual/calculations/gridspace.jl @@ -0,0 +1,44 @@ +using LineCableModels + +conductor = Material( + kind = :conductor, + rho = eps(Float64), + eps_r = 1.0, + mu_r = 1.0, + T0 = 20.0, + alpha = 0.0 +) + +insulator = Material( + kind = :insulator, + rho = 1.97e14, + eps_r = Grid((2.3,), 1.0), + mu_r = 1.0, + T0 = 20.0, + alpha = 0.0 +) + +design = @cable "two_bare_wires" begin + @terminal :core begin + core(conductor; r=0.0425, tag=:core_metal) + end + insulation(insulator; t=1.0e-3, tag=:core_insulation) +end + +system = @system "two_bare_wires" line_length=1.0 combine=:zip begin + @at design (0.0, -1.0) core=1 + @at design (1.0, -1.0) core=2 +end + +earth = @earth begin + layer(rho=Grid((10.0, 100.0)), eps_r=1.0, mu_r=1.0) +end + +problems = LineParametersProblem( + system, + earth; + temperature = 20.0, + frequencies = collect(10.0 .^ range(0, stop = 6, length = 101)) +) + +display(problems) diff --git a/test/manual/calculations/modal_analysis.jl b/test/manual/calculations/modal_analysis.jl new file mode 100644 index 000000000..0636c140c --- /dev/null +++ b/test/manual/calculations/modal_analysis.jl @@ -0,0 +1,166 @@ +# Manual study of the nine underground coaxial cables in Case 3, Fig. 17 and +# Table IV. Run with LineCableModels and GLMakie in the active REPL project: +# `include("test/manual/calculations/modal_analysis.jl")` +# Completed values and interactive figures remain available in the REPL. +# This file does not activate a project or write images to disk. + +using Revise +using LineCableModels, GLMakie, LinearAlgebra + +frequency_grid = 10.0 .^ range(-1, 8; length = 321) +source_length = 2_000.0 +phase_options = (output_basis = :pul, verbosity = (default = 0,)) + +# Table IV radii are measured from the cable center. The table omits +# dielectric bulk resistivity and conductor temperature coefficients. Infinite +# dielectric resistivity and zero temperature coefficient avoid adding losses +# beyond its stated insulation loss factor and metal resistivity. +core_radius = 12.38e-3 +sheath_inner_radius = 24.69e-3 +sheath_outer_radius = 25.36e-3 +insulation_outer_radius = 27.358e-3 +metal = Material(:conductor, 1.7e-8) +core_insulator = Material(:insulator, Inf, 2.3; tan_delta = 0.0005) +sheath_insulator = Material(:insulator, Inf, 2.48; tan_delta = 0.0005) + +design = @cable "case-3-coaxial" begin + @terminal :core begin + core(metal; r = core_radius) + insulation(core_insulator; t = sheath_inner_radius - core_radius) + end + @terminal :sheath begin + sheath(metal; t = sheath_outer_radius - sheath_inner_radius) + end + jacket(sheath_insulator; t = insulation_outer_radius - sheath_outer_radius) +end + +# Fig. 17: 1.1335 m to the upper cable centers, 0.1905 m across columns, +# and 0.19035 m down rows. Each core and sheath has its own terminal. +centers = [(column * 0.1905, -1.1335 - row * 0.19035) + for row in 0:2 for column in -1:1] +connections = [Dict(:core => 2i - 1, :sheath => 2i) + for i in eachindex(centers)] +earth = homogeneous(rho = 100.0) +system = build(LineCableSystem, fill(design, length(centers)), + [Pose2(x, y) for (x, y) in centers]; connections, + system_id = "underground-case-3", line_length = source_length, + environment = earth) +problem = LineParametersProblem(system; temperature = 20.0, earth_props = earth, + frequencies = collect(frequency_grid)) + +@assert length(centers) == 9 +@assert length(system.terminal_order) == 18 +@assert sort(system.connection_order) == collect(1:18) +display(design) +display(system) +display(problem) +geometry_plot = preview(system; earth_model = earth, backend = :gl, + display_plot = true, open_export = false) + +# 2 public calculations, followed by the finite line from Table IV. +phase = @time compute(problem, + Formulation(earth_impedance = :unified, + earth_admittance = :unified, shunt_model = :equivalent, insulation_admittance = :lossy; + options = (reduce_bundle = false, kron_reduction = false, + ideal_transposition = false)); + options = phase_options) +# modal = :default,) +modal = @time compute( + ModalAnalysisProblem(phase), + ModalAnalysisFormulation(:default); + options = (rotate = true,) +) +restored_phase = transform(PhaseDomain, modal) +line = PropagationParameters(modal) +display(phase) +display(modal) +display(line) + +@assert size(Z(phase)) == size(Y(phase)) == (18, 18, length(frequency_grid)) +@assert size(Tv(modal)) == size(Ti(modal)) == (18, 18, length(frequency_grid)) +@assert size(gamma(modal)) == (18, length(frequency_grid)) +@assert line_length(line) == source_length + +diagnostics = details(modal).data.modal.diagnostics +# println("Modal diagnostics: ", diagnostics) +println("Inverse-coordinate residuals: Z=", norm(Z(restored_phase)-Z(phase)), + ", Y=", norm(Y(restored_phase)-Y(phase))) + +roots = gamma(line) +voltage_basis = Tv(line) +current_basis = Ti(line) +modal_Zc = Zc(line) +modal_Yc = Yc(line) +modal_H = H(line) +phase_Zc = Zc(line, PhaseDomain) +phase_Yc = Yc(line, PhaseDomain) +phase_voltage_H = H(line, PhaseDomain; field = :voltage) +phase_current_H = H(line, PhaseDomain; field = :current) +@assert size(phase_Zc) == size(phase_Yc) == size(phase_voltage_H) == + size(phase_current_H) == (18, 18, length(frequency_grid)) + +# These bound selectors are ordinary public observation requests. +observed = observables(line) +display(report(observed)) + +# Every retained mode is a curve for scalar modal quantities. For Tv and Ti, +# every phase-row × mode-column coefficient is a curve, including off-diagonals. +# The observation owner supplies physical labels, units and original indices. +# real and imag of Zc and Yc are R꜀/X꜀ and G꜀/B꜀, respectively. +plot_options = (overlay = :coordinates, backend = :gl, + display_plot = true, open_export = false) +plots = ( + cartesian = LineCableModels.plot(observed; + ydata = ((Zc, real), (Zc, imag), (Yc, real), (Yc, imag)), plot_options...), + propagation = LineCableModels.plot(observed; + ydata = (alpha, beta, velocity), plot_options...) +); + +transform_matrices = observables(line, + ((Tv, abs), (Tv, angle), (Ti, abs), (Ti, angle))) + +display(report(transform_matrices)) +LineCableModels.plot(transform_matrices; + plot_options..., + overlay = :rows, + layout = (3, 3) +) + +# Numerical quality is for inspection, not an acceptance threshold. +voltage_condition = [cond(@view voltage_basis[:, :, k]) for k in eachindex(frequency_grid)] +current_condition = [cond(@view current_basis[:, :, k]) for k in eachindex(frequency_grid)] +eigen_residual = [begin + assessed = filter(x -> x !== nothing, column) + isempty(assessed) ? NaN : maximum(assessed) + end + for column in eachcol(diagnostics.eigen_residual)] + +diagnostic_plot = LineCableModels.plotwindow( + title = "Modal diagnostics", + size = (1200, 800), + backend = :gl, + axis = ( + xlabel = "Frequency [Hz]", + xscale = :log10, + yscale = :linear + ) +) do grid + z_axis = Axis(grid[1, 1]; title = "Z modal coupling", ylabel = "Coupling") + y_axis = Axis(grid[1, 2]; title = "Y modal coupling", ylabel = "Coupling") + residual_axis = Axis(grid[2, 1]; + title = "Largest returned eigen-residual", ylabel = "Eigen-residual") + condition_axis = Axis(grid[2, 2]; title = "Basis conditioning", ylabel = "Condition number") + + lines!(z_axis, frequency_grid, diagnostics.z_coupling) + lines!(y_axis, frequency_grid, diagnostics.y_coupling) + lines!(residual_axis, frequency_grid, eigen_residual) + lines!(condition_axis, frequency_grid, voltage_condition; label = "Tv") + lines!(condition_axis, frequency_grid, current_condition; label = "Ti") + axislegend(condition_axis) +end + +println("Case 3: 9 cables, 18 modes, ", length(frequency_grid), + " samples, 2 km cable length") +println("Inspect phase, modal, line, observed, table_report, plots.cartesian, ", + "plots.polar, plots.propagation, plots.transformations, and diagnostic_figure.") +nothing diff --git a/test/manual/calculations/run_line_parameters.jl b/test/manual/calculations/run_line_parameters.jl new file mode 100644 index 000000000..74f82f7e6 --- /dev/null +++ b/test/manual/calculations/run_line_parameters.jl @@ -0,0 +1,104 @@ +# Disposable manual runner. Nothing here is part of the package or Gauntlet. +# +# Open this file in the IDE and run or include it. The script uses the IDE's +# active project environment without changing it. +# +# One-time setup in the user's Julia 1.12 default environment (`@v1.12`): +# import Pkg +# `Pkg.develop(path = "/home/amartins/Documents/KUL/LineCableModels")` +# Then run this file with that consumer environment active. `develop` is used +# only because LineCableModels is not registered yet. Dependency resolution is +# otherwise the same as for a registry install. +# +# Edit `run_fem`, `case_id`, and `frequency_grid` below, then re-include this +# file in a fresh session when changing the backend or case. + +repository = normpath(joinpath(@__DIR__, "..", "..", "..")) +gauntlet_project = joinpath(repository, "gauntlet") +gauntlet_project in LOAD_PATH || push!(LOAD_PATH, gauntlet_project) + +# Set `run_fem` to false for the analytical coaxial solver, or true for FEM. +run_fem = false + +# Revise is optional. Load it in your IDE session to update loaded package +# methods as you edit their source. +using LineCableModels +run_fem && (@eval using Gmsh) +isdefined(@__MODULE__, :Gauntlet) || include(joinpath(gauntlet_project, "Gauntlet.jl")) + +manual_output = joinpath(get(ENV, "LINECABLEMODELS_MANUAL_OUTPUT", + joinpath(tempdir(), "linecablemodels-manual")), "line-parameters") +mkpath(manual_output) +fullfile(filename) = joinpath(manual_output, filename); #hide + +# Edit these inputs for a different datasheet case or frequency sweep. +case_id = :cable_220kv_milliken_1x2500_252_trefoil +frequency_grid = 10.0 .^ range(-1, 7; length = 101) # Hz. Use [50.0] for one frequency. +loaded_case = Gauntlet.load_case(case_id; + variation = Gauntlet.ExactOverrides(; frequencies = frequency_grid)) +problem = loaded_case.problem +system = problem.system +earth = problem.earth_props + +println("\n", run_fem ? "FEM" : "Analytical", ": ", case_id, + " | ", length(frequency_grid), " frequencies") +println("Terminal order: ", loaded_case.port_order) + +output_file = fullfile("pscad_export.pscx") +export_file = export_data(:pscad, system, earth, file_name = output_file); + +if run_fem + # FEM field-model choices. Execution controls belong to compute(.... Options). + fem_formulation = Formulation(:fem; + options = ( + physics = :quasi_tem, + reduce_bundle = false, + kron_reduction = false, + ideal_transposition = false + )) + fem_options = ( + mesh_mode = :remesh, + resume_run_directory = nothing, + keep_run_directory = true, + trace = true, + output_basis = :pul, + verbosity = (default = 1,), + gmsh_verbosity = 2, + getdp_verbosity = 4, + frequency_workers = 2, + solver_threads = 1, + plot_field_maps = false + ) + + fem_result = @time compute(problem, fem_formulation; options = fem_options) + Zfem = observe(fem_result, Z) # ohm/m + Yfem = observe(fem_result, Y) # S/m + println("Run directory: ", details(fem_result).data.fem.run.run_directory) + println("Zfem / Yfem: ", size(Zfem), " | result: fem_result") +end + +# Analytical solver defaults. These are the same selections as Formulation(). +analytical_formulation = Formulation( + internal_impedance = :default, + insulation_impedance = :default, + earth_impedance = :default, + shunt_model = :default, # Equivalent annular layer. `:boundary` opts into the local field solve. + insulation_admittance = :default, + semicon_admittance = :default, + earth_admittance = :default, + earth_properties = :default, + pipe_impedance = :default, + temperature_dependence = :default, + options = ( + reduce_bundle = false, + kron_reduction = false, + ideal_transposition = false + ) +) +analytical_result = @time compute(problem, analytical_formulation) +Zan = observe(analytical_result, Z) # ohm/m +Yan = observe(analytical_result, Y) # S/m +println("Zresult / Yresult: ", size(Zan), " | result: analytical_result") + +# `Results remain available in the REPL. +nothing diff --git a/test/manual/calculations/run_two_bare_wires.jl b/test/manual/calculations/run_two_bare_wires.jl new file mode 100644 index 000000000..391fc77c0 --- /dev/null +++ b/test/manual/calculations/run_two_bare_wires.jl @@ -0,0 +1,32 @@ +# Run in an environment containing LineCableModels and GLMakie: +# `include("test/manual/calculations/run_two_bare_wires.jl")` +# The active project and LOAD_PATH belong to the caller and remain unchanged. +using LineCableModels, GLMakie + +# 2 bare copper wires: 42.5 mm radius, 1 m spacing, buried 1 m deep. +# Geometry, temperature and static earth inputs match the two_bare_wires case. +materials = MaterialsLibrary(add_defaults=true) +copper = Material(materials, :copper) +design = build(CableDesign, "two_bare_wires", + Stack(Group(:core, Region(:core_metal, Disk(0.0425), copper)))) +system = build(LineCableSystem, [design, design], + [Pose2(0.0, -1.0), Pose2(1.0, -1.0)]; + connections=[Dict(:core => 1), Dict(:core => 2)], + system_id="two_bare_wires", line_length=1.0) +earth = homogeneous(rho=0.1, eps_r=1.0, mu_r=1.0) +frequency = 10.0 .^ range(-1, 7; length=17) # Hz +problem = LineParametersProblem(system; + temperature=20.0, earth_props=earth, frequencies=frequency) + +# Both calculations use Unified earth return. Compare constant earth properties +# with the implemented Longmire-Smith dispersion model, keeping static inputs +# fixed. The legend identifies the differing soil law automatically. +formulations = [Formulation( + earth_impedance=:unified, earth_admittance=:unified, + earth_properties=soil_law, + options=(reduce_bundle=false, kron_reduction=false, ideal_transposition=false)) + for soil_law in (:constant, :longmire1975)] +results = @time compute(problem, formulations) +plots = LineCableModels.plot(results; + ydata=(R, X, G, B), length_unit=:base, backend=:gl) +nothing diff --git a/test/manual/calculations/run_two_bare_wires_fem.jl b/test/manual/calculations/run_two_bare_wires_fem.jl new file mode 100644 index 000000000..3b885cbe9 --- /dev/null +++ b/test/manual/calculations/run_two_bare_wires_fem.jl @@ -0,0 +1,345 @@ +# Manual FEM sweep for the two bare underground wires case. +# +# Run from the repository root with: +# `julia --project=gauntlet test/manual/calculations/run_two_bare_wires_fem.jl` +# +# The script leaves the scalar FEM and analytical results in the corresponding +# `case_*_results` arrays, and their admittance tensors in the corresponding +# `case_*_admittance` arrays. FEM run directories are kept by the backend. The +# CSV files contain the full 2x2 FEM admittance matrix in S/m. + +using LineCableModels +using Printf +using XLSX +using Gmsh + +# All dimensions below are SI units. The frequency grid has 20 logarithmic +# intervals per decade and includes both 100 Hz and 1 MHz (81 samples total). +frequency_grid = collect(10.0 .^ range(2.0, 6.0; step = 1 / 20)) +installation_z = -1.0 +soil_relative_permittivity = 1.0 +soil_relative_permeability = 1.0 +line_length = 1.0 +temperature = 20.0 + +# Case 1: fixed 4.25 cm radius, sweep soil resistivity. +fixed_external_radius = 4.25e-2 +soil_resistivity_grid = Grid([0.1, 1.0, 100.0, 1000.0]) + +# Case 2: fixed 0.1 Ohm.m soil, sweep external radius. The 4.25 cm point is +# intentionally not rerun here: it is case_1_results[1], at rho = 0.1 Ohm.m. +fixed_soil_resistivity = 0.1 +external_radius_grid = Grid([0.1e-2, 1.0e-2, 8.5e-2]) + +function build_two_bare_wires_problem( + external_radius, earth_rho, frequencies; vert = installation_z +) + # Keep this construction aligned with `gauntlet/cases/two_bare_wires.jl`. + materials = MaterialsLibrary(add_defaults = true) + copper = Material(materials, :copper) + design = build( + CableDesign, + "two_bare_wires", + Stack( + Group( + :core, + Region(:core_metal, Disk(external_radius), copper) + ) + ) + ) + system = build( + LineCableSystem, + [design, design], + [Pose2(0.0, vert), Pose2(1.0, vert)]; + connections = [Dict(:core => 1), Dict(:core => 2)], + system_id = "two_bare_wires", + line_length = line_length + ) + earth = homogeneous( + rho = earth_rho, + eps_r = soil_relative_permittivity, + mu_r = soil_relative_permeability + ) + return LineParametersProblem( + system; + temperature = temperature, + earth_props = earth, + frequencies = frequencies + ) +end + +function make_problem_space(radius_values, earth_rho_values, frequencies; vert) + build_point = (radius, earth_rho) -> build_two_bare_wires_problem( + radius, earth_rho, frequencies; vert = vert) + return Gridspace{LineParametersProblem}( + build_point, + (Grid(radius_values), Grid(earth_rho_values)) + ) +end + +fem_formulation = Formulation( + :fem; + options = ( + physics = :quasi_fw, + reduce_bundle = false, + kron_reduction = false, + ideal_transposition = false + ) +) +fem_options = ( + ui = false, + mesh_mode = :remesh, + resume_run_directory = nothing, + keep_run_directory = true, + trace = true, + output_basis = :pul, + verbosity = (default = 1,), + gmsh_verbosity = 2, + getdp_verbosity = 4, + frequency_workers = 2, + solver_threads = 4, + plot_field_maps = false +) + +function run_fem_space(space, label, formulation, options) + println("\nFEM ", label, " | ", length(space), " parameter points | ", + length(frequency_grid), " frequencies") + parametric_problem = ParametricProblem(space, ComputationOptions(options)) + return compute(parametric_problem, Combinatorial(formulation)) +end + +function write_admittance_csv(path, parameter_name, parameter_values, results) + length(parameter_values) == length(results) || + throw(DimensionMismatch("parameter values and FEM results must have equal length")) + mkpath(dirname(path)) + open(path, "w") do io + println(io, parameter_name, + ",frequency_hz,response_terminal,basis_terminal,real_s_per_m,imaginary_s_per_m") + for (parameter, result) in zip(parameter_values, results) + frequencies = observe(result, LineCableModels.frequencies) + admittance = observe(result, Y) + for frequency_index in eachindex(frequencies), + response in axes(admittance, 1), basis in axes(admittance, 2) + value = admittance[response, basis, frequency_index] + @printf(io, + "%.17g,%.17g,%d,%d,%.17g,%.17g\n", + parameter, + frequencies[frequency_index], + response, + basis, + real(value), + imag(value)) + end + end + end + return path +end + +function write_impedance_csv(path, parameter_name, parameter_values, results) + length(parameter_values) == length(results) || + throw(DimensionMismatch("parameter values and FEM results must have equal length")) + mkpath(dirname(path)) + open(path, "w") do io + println(io, parameter_name, + ",frequency_hz,response_terminal,basis_terminal,real_ohm_per_m,imaginary_ohm_per_m") + for (parameter, result) in zip(parameter_values, results) + frequencies = observe(result, LineCableModels.frequencies) + impedance = observe(result, Z) + for frequency_index in eachindex(frequencies), + response in axes(impedance, 1), basis in axes(impedance, 2) + value = impedance[response, basis, frequency_index] + @printf(io, + "%.17g,%.17g,%d,%d,%.17g,%.17g\n", + parameter, + frequencies[frequency_index], + response, + basis, + real(value), + imag(value)) + end + end + end + return path +end + +function write_line_parameters_xlsx(directory, file_prefix, parameter_values, results) + length(parameter_values) == length(results) || + throw(DimensionMismatch("parameter values and XLSX results must have equal length")) + mkpath(directory) + paths = String[] + for (parameter, result) in zip(parameter_values, results) + token = replace(@sprintf("%.6g", float(parameter)), "." => "p", "-" => "m") + path = joinpath(directory, "$(file_prefix)_$(token).xlsx") + # Before: export_data(:xlsx, raw) returned one workbook path. Now each + # retained quantity has its own workbook, so append the returned paths. + observed = ObservedResult(result, (R, X, G, B); length_unit = :base) + append!(paths, export_data(:xlsx, observed; file_name = path)) + end + return paths +end + +case_1_space = make_problem_space( + Grid([fixed_external_radius]), soil_resistivity_grid, frequency_grid; + vert = installation_z +) +case_1_result = run_fem_space( + case_1_space, "case 1 (soil resistivity)", fem_formulation, fem_options +) +case_1_results = collect(case_1_result) +case_1_admittance = [observe(result, Y) for result in case_1_results] +case_1_run_directories = [details(result).data.fem.run.run_directory + for result in case_1_results] + +case_2_space = make_problem_space( + external_radius_grid, Grid([fixed_soil_resistivity]), frequency_grid; + vert = installation_z +) +case_2_result = run_fem_space( + case_2_space, "case 2 (external radius)", fem_formulation, fem_options +) +case_2_results = collect(case_2_result) +case_2_admittance = [observe(result, Y) for result in case_2_results] +case_2_run_directories = [details(result).data.fem.run.run_directory + for result in case_2_results] + +# Default analytical coaxial formulation, evaluated over the same two problem +# spaces so that each result has the same parameter ordering as its FEM peer. +analytical_formulation = Formulation() + +function run_analytical_space(space, label, formulation) + println("\nAnalytical ", label, " | ", length(space), " parameter points | ", + length(frequency_grid), " frequencies") + return compute(ParametricProblem(space), Combinatorial(formulation)) +end + +case_1_analytical_result = run_analytical_space( + case_1_space, "case 1 (soil resistivity)", analytical_formulation +) +case_1_analytical_results = collect(case_1_analytical_result) +case_1_analytical_admittance = [observe(result, Y) for result in case_1_analytical_results] + +case_2_analytical_result = run_analytical_space( + case_2_space, "case 2 (external radius)", analytical_formulation +) +case_2_analytical_results = collect(case_2_analytical_result) +case_2_analytical_admittance = [observe(result, Y) for result in case_2_analytical_results] + +output_directory = joinpath( + get(ENV, "LINECABLEMODELS_MANUAL_OUTPUT", + joinpath(tempdir(), "linecablemodels-manual")), + "two-bare-wires-fem") +case_1_csv = write_admittance_csv( + joinpath(output_directory, "case_1_soil_resistivity.csv"), + "soil_resistivity_ohm_m", + collect(soil_resistivity_grid), + case_1_results +) +case_2_csv = write_admittance_csv( + joinpath(output_directory, "case_2_external_radius.csv"), + "external_radius_m", + collect(external_radius_grid), + case_2_results +) +case_1_impedance_csv = write_impedance_csv( + joinpath(output_directory, "case_1_soil_resistivity_impedance.csv"), + "soil_resistivity_ohm_m", + collect(soil_resistivity_grid), + case_1_results +) +case_2_impedance_csv = write_impedance_csv( + joinpath(output_directory, "case_2_external_radius_impedance.csv"), + "external_radius_m", + collect(external_radius_grid), + case_2_results +) + +xlsx_directory = joinpath(output_directory, "xlsx") +case_1_fem_xlsx = write_line_parameters_xlsx( + xlsx_directory, + "case_1_fem_rho_ohm_m", + collect(soil_resistivity_grid), + case_1_results +) +case_1_analytical_xlsx = write_line_parameters_xlsx( + xlsx_directory, + "case_1_analytical_rho_ohm_m", + collect(soil_resistivity_grid), + case_1_analytical_results +) +case_2_fem_xlsx = write_line_parameters_xlsx( + xlsx_directory, + "case_2_fem_radius_m", + collect(external_radius_grid), + case_2_results +) +case_2_analytical_xlsx = write_line_parameters_xlsx( + xlsx_directory, + "case_2_analytical_radius_m", + collect(external_radius_grid), + case_2_analytical_results +) + +println("\nCompleted.") +println("Case 1 admittance tensors: ", [size(value) for value in case_1_admittance]) +println("Case 2 admittance tensors: ", [size(value) for value in case_2_admittance]) +println("Case 1 CSV: ", case_1_csv) +println("Case 2 CSV: ", case_2_csv) +println("Case 1 impedance CSV: ", case_1_impedance_csv) +println("Case 2 impedance CSV: ", case_2_impedance_csv) +println("Case 1 FEM XLSX: ", case_1_fem_xlsx) +println("Case 1 analytical XLSX: ", case_1_analytical_xlsx) +println("Case 2 FEM XLSX: ", case_2_fem_xlsx) +println("Case 2 analytical XLSX: ", case_2_analytical_xlsx) +println("The 4.25 cm, 0.1 Ohm.m point for case 2 is case_1_results[1].") + +using GLMakie + +# Before: a Y request could pair G/B in one figure. Now plot_1 and plot_2 +# each hold separate G and B figures. Layout=(2,2) is coefficient capacity. +case_1_plot_results = vcat(case_1_results, case_1_analytical_results) +case_1_plot_labels = vcat( + ["FEM: ρ = $(ρ) Ω·m" for ρ in soil_resistivity_grid], + ["Analytical: ρ = $(ρ) Ω·m" for ρ in soil_resistivity_grid] +) +case_1_plot_styles = vcat( + fill((linestyle = :solid,), length(case_1_results)), + fill((linestyle = :dash,), length(case_1_analytical_results)) +) +plot_1 = Makie.plot( + case_1_plot_results...; + ydata = (Y,), + series_labels = case_1_plot_labels, + series_attributes = case_1_plot_styles, + layout = (2, 2), + xscale = :log10, + backend = :gl, + figure_title = "Two bare wires — case 1: soil resistivity", + legend_title = "Model and physical assumptions", + legend_attributes = (; valign = :center), + legend_cap = 0.5 +) + +case_2_plot_results = vcat(case_2_results, case_2_analytical_results) +case_2_plot_labels = vcat( + ["FEM: r = $(100r) cm" for r in external_radius_grid], + ["Analytical: r = $(100r) cm" for r in external_radius_grid] +) +case_2_plot_styles = vcat( + fill((linestyle = :solid,), length(case_2_results)), + fill((linestyle = :dash,), length(case_2_analytical_results)) +) +plot_2 = Makie.plot( + case_2_plot_results...; + ydata = (Y,), + series_labels = case_2_plot_labels, + series_attributes = case_2_plot_styles, + layout = (2, 2), + xscale = :log10, + backend = :gl, + figure_title = "Two bare wires — case 2: external radius", + legend_title = "Model and physical assumptions", + legend_attributes = (; valign = :center), + legend_cap = 0.5 +) + +nothing diff --git a/test/manual/fem/run_18kv_trefoil_fem.jl b/test/manual/fem/run_18kv_trefoil_fem.jl new file mode 100644 index 000000000..94b5cb449 --- /dev/null +++ b/test/manual/fem/run_18kv_trefoil_fem.jl @@ -0,0 +1,58 @@ +# Manual run: `include("test/manual/fem/run_18kv_trefoil_fem.jl")` +# Uses the current datasheet case, not saved campaign results. No gauntlet run. +# Before: this include activated Gauntlet. Now preserve the IDE's active project. +gauntlet_project = normpath(joinpath(@__DIR__, "..", "..", "..", "gauntlet")) +gauntlet_project in LOAD_PATH || push!(LOAD_PATH, gauntlet_project) +using LineCableModels, Gmsh +isdefined(@__MODULE__, :Gauntlet) || + include(joinpath(gauntlet_project, "Gauntlet.jl")) + +# Edit these inputs and re-include. Each inclusion starts a fresh FEM run. +case_id = :cable_18kv_1000mm2_trefoil +frequency_grid = 10.0 .^ range(-1, 7; length=101) # Hz. Use [1e7] for one frequency. +loaded_case = Gauntlet.load_case(case_id; + variation=Gauntlet.ExactOverrides(; frequencies=frequency_grid)) +problem = loaded_case.problem +system = problem.system + +# Keep core, wire screen and aluminum foil as separate terminals on each cable. +# Explicitly selects quasi-tem.pro, NOT the coupled quasi-full.pro. +fem_formulation = Formulation(:fem; + options=( + physics=:quasi_tem, + reduce_bundle=false, + kron_reduction=false, + ideal_transposition=false, + )) +fem_options = ( + mesh_mode=:remesh, + resume_run_directory=nothing, + keep_run_directory=true, + trace=true, + output_basis=:pul, + verbosity=(default=1,), + gmsh_verbosity=2, + getdp_verbosity=4, + frequency_workers=2, + solver_threads=1, + plot_field_maps=false, # Set true to retain field maps for every excitation. +) + +# Run through the owned FEM API. Campaign results are not read or overwritten. +println("\nFEM quasi-TEM: ", case_id, " | ", length(frequency_grid), " frequencies") +println("Terminal order: ", loaded_case.port_order) +fem_result = compute(problem, fem_formulation; options=fem_options) + +# Raw per-unit-length matrices, indexed [response, source, frequency], for the IDE. +Zfem = observe(fem_result, Z) # ohm/m +Yfem = observe(fem_result, Y) # S/m +Pfem = details(fem_result).data.fem.primitive.P_primitive # ohm m. Y = inv(P) per frequency. +run_directory = details(fem_result).data.fem.run.run_directory +println("\nRun directory: ", run_directory) +println("Zfem / Yfem / Pfem: ", size(Zfem), " | result: fem_result") +# Meshes, solver inputs, raw columns and logs remain under run_directory. +# Optional matrix plots after the solve: +# using GLMakie +# Before: blocks selected matrix pages. Now layout is the only panel capacity. +# plots = LineCableModels.plot(fem_result. Ydata=(R, X, G, B), layout=(3, 3)) +nothing diff --git a/test/manual/fem/run_quasi_full.jl b/test/manual/fem/run_quasi_full.jl new file mode 100644 index 000000000..686037785 --- /dev/null +++ b/test/manual/fem/run_quasi_full.jl @@ -0,0 +1,96 @@ +# Run from the REPL: `include("test/manual/fem/run_quasi_full.jl")` +# Edit the inputs below, then include again. Each run gets a fresh directory. +using LineCableModels, Gmsh, LinearAlgebra, Printf +FEM = Base.get_extension(LineCableModels, :LineCableModelsGmshExt) + +frequencies = 10.0 .^ (-1:6) # Hz +radius = 0.0425 # m. No insulation +positions = [(0.0, -1.0), (1.0, -1.0)] # m +metal = Material(kind=:conductor, rho=1e-12) # geometry and material input. PEC CLI below excludes its interior +wire = build(CableDesign, "bare", terminal(:core, core(metal; r=radius))) +system = build(LineCableSystem, [wire, wire], positions; + connections=[Dict(:core=>1), Dict(:core=>2)], line_length=1.0) +problem = LineParametersProblem(system; frequencies, + earth_props=homogeneous(rho=0.1, eps_r=1.0, mu_r=1.0)) +formulation = Formulation(:fem; + options=(physics=:quasi_fw, reduce_bundle=false, kron_reduction=false, ideal_transposition=false)) +execution = computation_options(LineCableModelsFEM, ComputationOptions(;mesh_mode=:remesh, gmsh_verbosity=2, getdp_verbosity=3, + plot_field_maps=false, solver_threads=1, keep_run_directory=true)) + +# Reuse the package's material resolver and mesher. No production FEM solve is +# called: the only equations executed below are the new quasi-full.pro. +model = FEM._resolved_fem_model(FEM._preflight_fem_problem(problem), formulation) +runtime_root = joinpath(get(ENV, "LINECABLEMODELS_MANUAL_OUTPUT", + joinpath(tempdir(), "linecablemodels-manual")), "quasi-full") +manual_run = FEM._create_run(runtime_root) +run_directory = manual_run.path +path_files = String[] +lock(FEM.FEM_SESSION_LOCK) do + session = FEM._start_gmsh(execution.data.gmsh_verbosity) + try + geometry = FEM._build_geometry!(model, "quasi-full-$(basename(run_directory))") + global mesh_paths = FEM._select_meshes!(manual_run, model, geometry, + execution, runtime_root) + FEM._write_run_inputs!(manual_run, model) + for (mesh, plan) in zip(mesh_paths, model.mesh_plans) + path = joinpath(run_directory, "input", @sprintf("paths-f%04d.pro", plan.frequency_index)) + # Before: quasi_full_paths.jl forwarded to the extension. The extension + # now defines this voltage-path operation directly. An adapter is unnecessary. + FEM._write_voltage_paths(path, mesh, plan, model; + endpoints=[(x,y-radius) for (x,y) in positions]) + push!(path_files, path) + end + finally + FEM._finish_gmsh(session) + end +end + +# model.pro selects the coupled file through its ONELAB Physics constant. +pro_file = joinpath(run_directory, "input", "getdp", "model.pro") +model_data = joinpath(run_directory, "input", "model_data.pro") +basis_file = joinpath(run_directory, "input", "bases.pro") +write(basis_file, "RequestedBases() = {1,2};\n") +getdp = FEM._getdp_selection(execution).path +Zqf = zeros(ComplexF64, 2, 2, length(frequencies)) # ohm/m +Mqf = similar(Zqf) # ohm m. Raw inverse admittance +Peqf = similar(Zqf) # m/F +Yqf = similar(Zqf) # S/m + +for (index, plan) in enumerate(model.mesh_plans) + job = joinpath(run_directory, @sprintf("f%04d", index)) + mkpath(job) + mesh, paths = mesh_paths[index], path_files[index] + prefix = joinpath(job, "solver") + maps = Int(execution.data.plot_field_maps) + command = `$getdp $pro_file -solve LineCableModelsFEMScan + -msh $mesh -name $prefix -v $(execution.data.getdp_verbosity) + -setstring ModelDataPath $model_data -setstring RunDirectory $job + -setstring BasisListPath $basis_file -setstring PathDataPath $paths + -setnumber FrequencyIndex $index -setnumber FrequencyHz $(plan.frequency) + -setnumber Val_Rint $(plan.domain_radius) -setnumber Val_Rext $(plan.shell_outer_radius) + -setnumber PlotFieldMaps $maps -setnumber ReuseFactorization 1 + -setnumber Physics 1 -setnumber PerfectConductors 1` + println("\n", command) + # Native progress is visible in the REPL. Raw columns and maps stay in job. + run(addenv(Cmd(command; dir=job), "OMP_NUM_THREADS"=>"1", "OPENBLAS_NUM_THREADS"=>"1")) + for (quantity, matrix) in (("Z", Zqf), ("P", Mqf)), basis in 1:2 + path = joinpath(job, "raw", "jobs", @sprintf("getdp-f%04d-b%04d-%s.tsv", index, basis, quantity)) + FEM._valid_job_raw(path, 2, index, plan.frequency, basis) || error("Invalid GetDP column: $path") + for line in eachline(path) + row = split(line) + matrix[parse(Int, row[3]), basis, index] = complex(parse(Float64, row[5]), parse(Float64, row[6])) + end + end + Yqf[:,:,index] = Mqf[:,:,index] \ Matrix{ComplexF64}(I, 2, 2) + Peqf[:,:,index] = (2pi*im*plan.frequency) .* Mqf[:,:,index] + println("f = ", plan.frequency, " Hz") + for (label, matrix) in (("Z [ohm/m]", Zqf), ("Pe [m/F]", Peqf), ("Y [S/m]", Yqf)) + println(label) + show(stdout, MIME"text/plain"(), matrix[:,:,index]); println() + end +end +println("\nRun directory: ", run_directory) +# REPL results: system, problem, formulation, Zqf, Mqf, Peqf, Yqf, run_directory. +# PerfectConductors=1 gives earth and exterior matrices only. Set it to 0 to +# exercise the existing finite-metal a/u block with the chosen material. +nothing diff --git a/test/manual/fem/run_three_bare_wires_baseline.jl b/test/manual/fem/run_three_bare_wires_baseline.jl new file mode 100644 index 000000000..fe9a17745 --- /dev/null +++ b/test/manual/fem/run_three_bare_wires_baseline.jl @@ -0,0 +1,220 @@ +# Load in the IDE or run with +# `julia --startup-file=no test/manual/fem/run_three_bare_wires_baseline.jl`. +# Uses the active environment without activating or installing anything. Each run +# creates a new attempt. Retained results are never replaced. The run completes without plotting. +using LineCableModels +using Gmsh +using Printf, TOML, SHA, Dates, Logging, Pkg + +isdefined(@__MODULE__, :CurrentScenarios) || + include(joinpath(@__DIR__, "..", "..", "..", "test", "support", "scenarios.jl")) + +function baseline_toml(path, record) + open(io -> TOML.print(io, record; sorted = true), path, "w") +end + +function baseline_checksums(directory) + return Dict(relpath(joinpath(root, file), directory) => + open(bytes2hex ∘ sha256, joinpath(root, file)) + for (root, _, files) in walkdir(directory) for file in files) +end + +function baseline_write_result(directory, result, expected_frequencies) + f = LineCableModels.frequencies(result) + labels = String.(details(result).data.coordinates) + @assert f == expected_frequencies + @assert labels == ["cable:$i:core" for i in 1:3] + @assert length(unique(labels)) == 3 + for (name, selector, unit) in (("Z", Z, "ohm_per_m"), ("Y", Y, "S_per_m")) + values = observe(result, selector) + @assert size(values) == (3, 3, 9) + @assert all(isfinite, values) + path = joinpath(directory, "$name.csv") + open(path, "w") do io + println(io, "frequency_hz,response_terminal,basis_terminal,real_$unit,imaginary_$unit") + for k in eachindex(f), i in 1:3, j in 1:3 + value = values[i, j, k] + @printf(io, "%.17g,%s,%s,%.17g,%.17g\n", + f[k], labels[i], labels[j], real(value), imag(value)) + end + end + # Check every stored entry and its coordinates, not only the dimensions. + rows = readlines(path)[2:end] + @assert length(rows) == 81 + for (row, (k, i, j)) in zip(rows, ((k, i, j) for k in eachindex(f) for i in 1:3 + for j in 1:3)) + fs, li, lj, re, im = split(row, ',') + @assert parse(Float64, fs) == f[k] && li == labels[i] && lj == labels[j] + @assert complex(parse(Float64, re), parse(Float64, im)) == values[i, j, k] + end + end + return labels +end + +function compare_three_bare_wires_baseline(attempt) + summary = Dict{String, Any}("status"=>"differences only; no acceptance verdict", + "near_zero_rule"=>"abs(component) <= eps(Float64)*maximum(abs, FEM channel); relative error is NaN there") + for placement in keys(CurrentScenarios.three_bare_wires_layouts), quantity in ("Z", "Y") + + name = "three_bare_wires_$placement" + paths = [joinpath(attempt, name, backend, "$quantity.csv") + for backend in ("unified", "fem")] + if !all(isfile, paths) + summary["$name/$quantity"] = Dict("status"=>"unavailable; inspect scan status in manifest") + continue + end + rows = map(path -> split.(readlines(path)[2:end], ','), paths) + @assert length(rows[1]) == length(rows[2]) == 81 + values = map(rs -> [complex(parse(Float64, r[4]), parse(Float64, r[5])) for r in rs], rows) + result, reference = values + near_zero = eps(Float64)*maximum(abs, reference) + relative(delta, ref) = abs(ref) <= near_zero ? NaN : abs(delta)/abs(ref) + errors = abs.(result-reference) + worst = argmax(errors) + relative_errors = [relative(c-r, r) for (c, r) in zip(result, reference)] + resolved_relative = filter(isfinite, relative_errors) + worst_relative = isempty(resolved_relative) ? nothing : + argmax([isfinite(x) ? x : -Inf for x in relative_errors]) + summary["$name/$quantity"] = Dict("max_absolute_error"=>maximum(errors), + "max_relative_error"=>isempty(resolved_relative) ? NaN : + maximum(resolved_relative), + "worst_absolute_entry"=>join(rows[1][worst][1:3], ","), + "relative_at_worst_absolute_entry"=>relative_errors[worst], + "reference_magnitude_at_worst_absolute_entry"=>abs(reference[worst]), + "worst_relative_entry"=>worst_relative === nothing ? "unresolved" : + join(rows[1][worst_relative][1:3], ","), + "absolute_at_worst_relative_entry"=>worst_relative === nothing ? NaN : + errors[worst_relative], + "reference_magnitude_at_worst_relative_entry"=>worst_relative === nothing ? + NaN : + abs(reference[worst_relative]), + "near_zero_threshold"=>near_zero) + # Near-zero components retain their absolute difference. No arbitrary + # denominator floor turns them into apparently small relative errors. + open(joinpath(attempt, name, "$(quantity)-differences.csv"), "w") do io + println(io, + "frequency_hz,response_terminal,basis_terminal,absolute_error,relative_error,real_absolute_error,real_relative_error,imaginary_absolute_error,imaginary_relative_error,real_reference_near_zero,imaginary_reference_near_zero") + for (i, (c, r)) in enumerate(zip(result, reference)) + @assert rows[1][i][1:3] == rows[2][i][1:3] + d = c-r + @printf(io, "%s,%.17g,%.17g,%.17g,%.17g,%.17g,%.17g,%s,%s\n", + join(rows[1][i][1:3], ","), abs(d), relative(d, r), + abs(real(d)), relative(real(d), real(r)), + abs(imag(d)), relative(imag(d), imag(r)), + string(abs(real(r))<=near_zero), string(abs(imag(r))<=near_zero)) + end + end + end + baseline_toml(joinpath(attempt, "comparison.toml"), summary) + return summary +end + +function run_three_bare_wires_baseline(; analytical = true, fem = true) + repository = normpath(joinpath(@__DIR__, "..", "..", "..")) + root = joinpath(get(ENV, "LINECABLEMODELS_MANUAL_OUTPUT", + joinpath(tempdir(), "linecablemodels-manual")), "three-bare-wires-baseline") + mkpath(root) + attempt = mktempdir(root; prefix = "capture-$(Dates.format(now(UTC), "yyyymmddTHHMMSS"))-", cleanup = false) + println("Baseline directory: ", attempt) + flush(stdout) + write(joinpath(attempt, "source.patch"), read(`git -C $repository diff HEAD --binary`, String)) + write(joinpath(attempt, "source-status.txt"), read(`git -C $repository status --short`, String)) + # Before: capture required an unrelated historical local plan. Now retain + # the executable fixture and runner. Source.patch records the actual code. + for source in ("test/support/scenarios.jl", + "test/manual/fem/run_three_bare_wires_baseline.jl") + target = joinpath(attempt, "sources", source) + mkpath(dirname(target)) + cp(joinpath(repository, source), target) + end + packages = Dict(string(uuid) => Dict("name"=>dep.name, + "version"=>string(dep.version), "source"=>something(dep.source, "")) + for (uuid, dep) in Pkg.dependencies()) + manifest = Dict{String, Any}("status"=>"capturing", "julia"=>string(VERSION), + "project"=>something(Base.active_project(), ""), + "revision"=>strip(read(`git -C $repository rev-parse HEAD`, String)), + "packages"=>packages, "runs"=>Dict{String, Any}(), + "matching_criteria"=>"Pending human review; no numerical acceptance tolerance imposed.") + baseline_toml(joinpath(attempt, "manifest.toml"), manifest) + reductions = ( + reduce_bundle = false, kron_reduction = false, ideal_transposition = false) + selections = Pair{String, Any}[] + analytical && push!(selections, + "unified" => ( + Formulation(earth_impedance = formula(:unified), + earth_admittance = formula(:unified); + options = reductions), (trace = true, output_basis = :pul))) + fem && push!(selections, + "fem" => ( + Formulation(:fem; + options = merge((physics = :quasi_fw,), reductions)), + (ui = false, mesh_mode = :remesh, resume_run_directory = nothing, + keep_run_directory = true, trace = true, output_basis = :pul, + verbosity = (default = 1,), gmsh_verbosity = 2, getdp_verbosity = 4, + frequency_workers = 2, solver_threads = 4, plot_field_maps = false))) + # Each public call computes a complete fresh scan without reordering or resuming a previous solve. + for (backend, (formulation, options)) in selections, + (placement, heights) in pairs(CurrentScenarios.three_bare_wires_layouts) + + name = "three_bare_wires_$placement" + directory = joinpath(attempt, name, backend) + mkpath(directory) + problem = CurrentScenarios.three_bare_wires_problem(; heights, name) + export_data(:json, problem; file_name = joinpath(directory, "problem.json")) + write(joinpath(directory, "formulation.txt"), repr(NamedTuple(formulation))) + write(joinpath(directory, "computation-options.txt"), repr(options)) + record = Dict{String, Any}("status"=>"running", "directory"=>relpath(directory, attempt)) + manifest["runs"]["$name/$backend"] = record + baseline_toml(joinpath(attempt, "manifest.toml"), manifest) + println("Starting $name / $backend / 9 frequencies") + flush(stdout) + started = time() + open(joinpath(directory, "compute.log"), "w") do log + redirect_stdio(stdout = log, stderr = log) do + with_logger(ConsoleLogger(log)) do + try + result = @time compute(problem, formulation; options) + record["seconds"] = time()-started + record["terminal_order"] = baseline_write_result(directory, result, problem.frequencies) + write(joinpath(directory, "details.txt"), repr(details(result).data)) + if backend == "fem" + run_directory = details(result).data.fem.run.run_directory + record["fem_run_directory"] = run_directory + baseline_toml(joinpath(directory, "fem-artifacts.toml"), + baseline_checksums(run_directory)) + end + record["status"] = "captured" + catch exception + record["seconds"] = time()-started + record["status"] = "failed" + record["error"] = sprint(showerror, exception) + showerror(log, exception, catch_backtrace()) + println(log) + # Retain the backend's failed work as well as successful solves. + if hasproperty(exception, :run_directory) && + exception.run_directory !== nothing + record["fem_run_directory"] = exception.run_directory + baseline_toml(joinpath(directory, "fem-artifacts.toml"), + baseline_checksums(exception.run_directory)) + end + end + end + end + end + record["sha256"] = baseline_checksums(directory) + baseline_toml(joinpath(attempt, "manifest.toml"), manifest) + println("$(record["status"]): $name / $backend ($(round(record["seconds"]; digits=2)) s)") + flush(stdout) + end + manifest["status"] = all(run["status"] == "captured" + for run in values(manifest["runs"])) ? + "captured; pending comparison and human review" : + "incomplete; failures retained" + baseline_toml(joinpath(attempt, "manifest.toml"), manifest) + compare_three_bare_wires_baseline(attempt) + return attempt +end + +if abspath(PROGRAM_FILE) == (@__FILE__) || isinteractive() + baseline_directory = run_three_bare_wires_baseline() +end diff --git a/test/manual/performance/benchmark_local_shunt.jl b/test/manual/performance/benchmark_local_shunt.jl new file mode 100644 index 000000000..5fba72832 --- /dev/null +++ b/test/manual/performance/benchmark_local_shunt.jl @@ -0,0 +1,107 @@ +# Disposable local-shunt audit. No FEM/PSCAD launches and no campaign writes. +# Run from a fresh Julia: +# `julia --project=gauntlet test/manual/performance/benchmark_local_shunt.jl` +gauntlet_project=normpath(joinpath(@__DIR__,"..","..","..","gauntlet")) +gauntlet_project in LOAD_PATH || push!(LOAD_PATH,gauntlet_project) +using LineCableModels, LinearAlgebra, BenchmarkTools, DataFrames, TOML, JLD2, SHA +isdefined(@__MODULE__,:Gauntlet) || include(joinpath(gauntlet_project,"Gauntlet.jl")) + +shunt_case = :cable_18kv_1000mm2_trefoil +shunt_warm_repeats = 1 # Increase for stable coefficient-construction timings. Each is a fresh solve. +shunt_compute_sweep = true +shunt_compare_saved = true +shunt_saved_folder = joinpath(gauntlet_project,".work","all-references", + "benchmark_18kv_1000mm2_trefoil_fem") +shunt_blas_threads = 2 +BLAS.set_num_threads(shunt_blas_threads) +println("Julia threads: ",Threads.nthreads(),"; BLAS threads: ",BLAS.get_num_threads()) + +# Numerical inspection only. This does not validate the auxiliary solver files. +function shunt_read_arrays(path) + bytes2hex(open(sha256,path)) == first(split(read(path*".sha256",String))) || + error("Changed numerical payload: $path") + jldopen(path,"r") do file + file["basis"] === :pul || error("Per-unit-length results required") + LineParameters(PhaseDomain,file["Z"],file["Y"],file["frequencies"];basis=:pul) + end +end + +shunt_loaded = Gauntlet.load_case(shunt_case) +shunt_problem = shunt_loaded.nominal_problem +shunt_engine = LineCableModels.Engine +shunt_blueprints = shunt_engine.flatten.(Ref(LineCableModelsCoaxial()),shunt_problem.system.designs) +shunt_domains = shunt_engine.ShuntModel.internal_shunt_domains(shunt_problem.system.designs,shunt_blueprints) +isempty(shunt_domains) && error("No qualified local domains in $shunt_case") +shunt_selection = formula(:boundary;options=(audit=true,)) +shunt_blueprint() = only(shunt_engine.flatten(LineCableModelsCoaxial(), + shunt_problem.system.designs, eltype(shunt_problem), [Formulation(shunt_model=shunt_selection)])) + +shunt_cold = @timed shunt_blueprint() +shunt_cable_data = shunt_engine.LocalCableData(shunt_cold.value) +shunt_measurements = [(pass="cold",seconds=shunt_cold.time,allocated_MiB=shunt_cold.bytes/1024^2, + solves=shunt_cable_data.shunt_details.solves)] +for repetition in 1:shunt_warm_repeats + sample = @timed shunt_blueprint() + push!(shunt_measurements,(pass="warm $repetition",seconds=sample.time, + allocated_MiB=sample.bytes/1024^2,solves=shunt_engine.LocalCableData(sample.value).shunt_details.solves)) +end +shunt_timing_df = DataFrame(shunt_measurements) +shunt_diagnostics_df = DataFrame(shunt_cable_data.shunt_details.diagnostics) +shunt_C = first(shunt_cable_data.shunt).C +shunt_couplings_df = DataFrame(coupling=["inner-open","inner-reference","open-reference"], + nF_per_m=[-shunt_C[1,2],sum(shunt_C[1,:]),sum(shunt_C[2,:])].*1e9) +display(shunt_timing_df) +display(shunt_diagnostics_df) +display(shunt_couplings_df) +println("Allocated MiB is cumulative allocation, not peak/live memory. Dense matrix bytes are in diagnostics.") + +if shunt_compute_sweep + shunt_physical = (reduce_bundle=false,kron_reduction=false,ideal_transposition=false) + # Before: prescribed Γ was under parameters. Now Unified owns it in options. + # Its value remains verbatim and this is not a comparison with withdrawn formulas. + shunt_formulations = [Formulation(shunt_model=shunt_selection;options=shunt_physical), + Formulation(shunt_model=shunt_selection, + earth_impedance=formula(:unified;options=(Γ=1e-4im,));options=shunt_physical)] + shunt_sweep = @timed compute(shunt_problem,shunt_formulations;options=(trace=true,)) + shunt_results = shunt_sweep.value + shunt_default = first(shunt_results) + @assert details(shunt_default).data.shunt_model.solves == shunt_cable_data.shunt_details.solves + shunt_equivalent = Formulation(shunt_model=:equivalent;options=shunt_physical) + shunt_annular = compute(shunt_problem,shunt_equivalent;options=(trace=true,)) + @assert observe(shunt_default,Z) == observe(shunt_annular,Z) + @assert details(shunt_default).data.trace.Pg == details(shunt_annular).data.trace.Pg + shunt_loop_input = shunt_engine.lineinput(shunt_problem, shunt_cold.value) + shunt_loop_options = computation_options(LineCableModelsCoaxial, ComputationOptions(trace=true)) + # Before: the private loop accepted five arguments. Now completion also takes + # the captured physical inputs and original point identity. Capture once here, + # just as the engine does, to exclude input capture from the loop timing. + shunt_inputs = shunt_engine.completed_inputs(shunt_problem) + shunt_id = LineCableModels.Commons.gridpoint_id() + shunt_loop_trial = @benchmark shunt_engine._compute(LineCableModelsCoaxial(), + $shunt_problem,$(first(shunt_formulations)),$shunt_loop_options,$shunt_loop_input, + $shunt_inputs,$shunt_id) samples=3 evals=1 + shunt_annular_trial = @benchmark compute($shunt_problem,$shunt_equivalent; + options=(trace=true,)) samples=3 evals=1 + display(shunt_loop_trial) + display(shunt_annular_trial) + println("End-to-end formulation collection: ",shunt_sweep.time," s; ",shunt_sweep.bytes/1024^2," MiB allocated.") + if shunt_compare_saved && isfile(joinpath(shunt_saved_folder,"state.toml")) + shunt_saved_state = TOML.parsefile(joinpath(shunt_saved_folder,"state.toml")) + shunt_attempt = joinpath(shunt_saved_folder,shunt_saved_state["current"]) + shunt_reference = shunt_read_arrays(joinpath(shunt_attempt,"reference","calculation.jld2")) + shunt_saved = shunt_read_arrays(joinpath(shunt_attempt,"candidate","points","1","calculation.jld2")) + shunt_comparison_rows = NamedTuple[] + for (model,value) in (("saved annular",shunt_saved),("integrated finite strip",shunt_default)) + comparisons = map(band->shunt_engine.compare(shunt_reference,value,B;band), + (:all,:dc,:harmonic,:narrow,:wide)) + for (coupling,i,j) in (("core-screen",1,2),("core-foil",1,3),("screen-foil",2,3)) + errors = NamedTuple{(:all,:dc,:harmonic,:narrow,:wide)}( + Tuple(100comparison.relative[i,j] for comparison in comparisons)) + push!(shunt_comparison_rows,merge((;model,coupling),errors)) + end + end + shunt_comparison_df = DataFrame(shunt_comparison_rows) + println("B relative RMS [%] versus saved FEM; this is a model comparison, not a convergence bound.") + show(stdout,MIME"text/plain"(),shunt_comparison_df;allcols=true);println() + end +end diff --git a/test/manual/performance/benchmark_shunt_models.jl b/test/manual/performance/benchmark_shunt_models.jl new file mode 100644 index 000000000..2c19115ba --- /dev/null +++ b/test/manual/performance/benchmark_shunt_models.jl @@ -0,0 +1,48 @@ +# Disposable public-API timings. Uses the active environment. Never activates, +# installs, updates, exports, or writes Gauntlet campaign artifacts. +# Study dependencies come from the local Gauntlet environment through LOAD_PATH. +gauntlet_project = normpath(joinpath(@__DIR__, "..", "..", "..", "gauntlet")) +gauntlet_project in LOAD_PATH || push!(LOAD_PATH, gauntlet_project) +using LineCableModels, LinearAlgebra +isdefined(@__MODULE__, :Gauntlet) || + include(joinpath(gauntlet_project, "Gauntlet.jl")) + +shunt_benchmark_cases = (:cable_18kv_1000mm2_trefoil, :cable_132kv_630mm2_flathor) +shunt_benchmark_frequencies = 10.0 .^ range(-1, 7; length = 101) +shunt_benchmark_repeats = 3 +shunt_benchmark_boundary = true +shunt_benchmark_rows = NamedTuple[] +println("Julia ", VERSION, "; Julia threads ", Threads.nthreads(), + "; BLAS threads ", BLAS.get_num_threads()) +for case_id in shunt_benchmark_cases + # Before: nominal_problem silently discarded the edited frequency override. + # problem is the materialized case with ExactOverrides applied. + problem = Gauntlet.load_case(case_id; + variation = Gauntlet.ExactOverrides( + frequencies = shunt_benchmark_frequencies)).problem + physical=(reduce_bundle = false, kron_reduction = false, ideal_transposition = false) + equivalent=Formulation(; options = physical) + println( + "\n", case_id, " | default equivalent annular layer | ", length(problem.frequencies), " frequencies") + @time compute(problem, equivalent) # Warm-up. Do not mix with the warm timings. + for repetition in 1:shunt_benchmark_repeats + sample=@timed @time compute(problem, equivalent) + push!(shunt_benchmark_rows, + (; case_id, model = :equivalent, repetition, + seconds = sample.time, MiB = sample.bytes/1024^2)) + @assert details(sample.value).data.shunt_model.solves==0 + end + if shunt_benchmark_boundary + boundary=Formulation(shunt_model = :boundary; options = physical) + println("Boundary compute, including blueprint construction; first call") + @time compute(problem, boundary) + for repetition in 1:shunt_benchmark_repeats + sample=@timed @time compute(problem, boundary) + push!(shunt_benchmark_rows, + (; case_id, model = :boundary, repetition, + seconds = sample.time, MiB = sample.bytes/1024^2)) + end + end +end +display(shunt_benchmark_rows) +nothing diff --git a/test/manual/plotting/Project.toml b/test/manual/plotting/Project.toml new file mode 100644 index 000000000..db183b069 --- /dev/null +++ b/test/manual/plotting/Project.toml @@ -0,0 +1,23 @@ +[deps] +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +Calculus = "49dc2e85-a5d0-5ad3-a950-438e2897f1b9" +Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" +GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" +LineCableModels = "dffb7669-8cd4-4628-99dd-7694e356ba37" +Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" +Measurements = "eff96d63-e80a-5855-80a2-b1b0885c5ab7" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +WGLMakie = "276b4fcb-3e11-5398-bf8b-a0c2d153d008" + +[sources] +LineCableModels = {path = "../../.."} + +[compat] +CairoMakie = "0.15" +Calculus = "0.5" +Distributions = "0.25" +GLMakie = "0.13" +Makie = "0.24.11" +Measurements = "2" +WGLMakie = "0.13" +julia = "1.12" diff --git a/test/manual/plotting/README.md b/test/manual/plotting/README.md new file mode 100644 index 000000000..c3686cfa5 --- /dev/null +++ b/test/manual/plotting/README.md @@ -0,0 +1,75 @@ +# Manual plotting checks + +The automated Cairo suite covers deterministic rendering, responsive docks, +callbacks, and live-state SVG export. Changes to interactive window creation, +display, layout, resizing, or callbacks also require a real-display GL check. + +These checks are deliberately absent from CI. Use the installed development +dependencies without replacing the active project: + +```sh +export JULIA_LOAD_PATH="@:$PWD/test/visual:$PWD/test/manual/plotting:@v1.12:@stdlib" +``` + +Run the GL and SVG inspection under a real display: + +```sh +julia --project=. test/manual/plotting/manual_gl.jl +julia --project=. test/manual/plotting/manual_gl.jl --backends-first +``` + +Both runs begin with CairoMakie installed but unloaded: GL controls work and +SVG controls are absent. They then explicitly import CairoMakie, export the old +window through the API and recreate a GL window with a working SVG button. +Export preserves the live view and does not switch display backends. + +Run the complete interactive gallery under a real display: + +```sh +julia --project=. test/manual/plotting/manual_gl_gallery.jl +``` + +Run the focused cable-collection preview gallery: + +```sh +julia --project=. test/manual/plotting/manual_gl_cable_collection.jl +``` + +It opens an automatically arranged 2×3 canvas and an explicit 1×4 canvas. +Confirm that each cable id appears as its subplot title, neither window has a +legend, and each window has one shared set of three material colorbars. + +Run the focused Monte Carlo distribution gallery: + +```sh +julia --project=. test/manual/plotting/manual_gl_monte_carlo.jl +``` + +It opens five native Makie views: histogram, density, empirical CDF, model CDF, +and Q-Q. Its synthetic completed fixture is sufficient without a new Monte Carlo computation. + +Resize the gallery windows from their initial size to a tall window and back. +Confirm that: + +- axes and canvas remain inside the window. +- the legend stays in its selected native layout dock. +- automatic `:show_all` legends wrap without omitting entries. +- all three material colorbars stay inside the side dock. +- the toolbar and status row retain their positions. +- reset, log toggles, and visibility toggles remain functional. +- SVG export is available when CairoMakie was loaded before plot construction. + +For the comparison grid, run: + +```sh +julia --project=. test/manual/plotting/manual_gl_comparison.jl +``` + +Resize each window and confirm that the matrix grid and responsive legend fit +without clipping. Toggle one legend entry and confirm that the same scenario +is hidden in every panel. + +Before the refactor, `blocks` paginated matrices and `layout` could pair +quantities. Now `layout` is the only panel capacity and each quantity has its +own figures. Result plots use `fig_size`. Previews and `plotwindow` retain +`size`. The complete script and saved-run disposition is in [../README.md](../README.md). diff --git a/test/manual/plotting/_gallery_fixtures.jl b/test/manual/plotting/_gallery_fixtures.jl new file mode 100644 index 000000000..f31a5b95f --- /dev/null +++ b/test/manual/plotting/_gallery_fixtures.jl @@ -0,0 +1,19 @@ +# Current API gallery inputs shared with maintained rendering tests. +using LineCableModels +include(joinpath(@__DIR__,"../../../test/support/scenarios.jl")) +using .CurrentScenarios +function build_manual_plot_gallery(backend=:gl;display_plot=true,export_theme=:default) + options=(;backend,display_plot,export_theme) + parameters=two_conductor_results() + gallery=Pair{String,UIPlot}[] + for (title,requests) in (("RLCG",(R,L,G,C)),("Cartesian ZY",(Z,Y)),("Polar ZY",(abs,angle))) + pages=LineCableModels.plot(parameters,requests;options...) + append!(gallery,["$title $index"=>page for (index,page) in enumerate(pages)]) + end + for (index,scale) in enumerate((1.0,1.2)) + push!(gallery,"Current cable $index"=>preview(coaxial_design(;scale);options...)) + end + push!(gallery,"Current system"=>preview(three_phase_system();earth_model=homogeneous(rho=100.0),options...)) + push!(gallery,"Materials"=>show_material_scale(;options...)) + return gallery +end diff --git a/test/manual/plotting/manual_gl.jl b/test/manual/plotting/manual_gl.jl new file mode 100644 index 000000000..981470894 --- /dev/null +++ b/test/manual/plotting/manual_gl.jl @@ -0,0 +1,245 @@ +using Test +if "--backends-first" in ARGS + using GLMakie + using LineCableModels +else + using LineCableModels + using GLMakie +end + +const ARTIFACT_DIRECTORY = abspath(get( + ENV, + "LINECABLEMODELS_GL_ARTIFACTS", + joinpath(get(ENV, "LINECABLEMODELS_MANUAL_OUTPUT", + joinpath(tempdir(), "linecablemodels-manual")), "plotting", "gl") +)) +mkpath(ARTIFACT_DIRECTORY) + +frequency = [50.0, 100.0, 500.0] +omega = reshape(2π .* frequency, 1, 1, :) +resistance_values = reshape([1.0, 0.2, 0.2, 2.0], 2, 2, 1) .* + ones(1, 1, length(frequency)) .* 1.0e-4 +inductance_values = fill(2.0e-7, 2, 2, length(frequency)) +conductance_values = fill(3.0e-9, 2, 2, length(frequency)) +capacitance_values = fill(4.0e-10, 2, 2, length(frequency)) +parameters = LineParameters( + complex.(resistance_values, inductance_values .* omega), + complex.(conductance_values, capacitance_values .* omega), + frequency +) + +plots = Makie.plot( + parameters, + (R, L, G, C); + backend = :gl, + display_plot = true, + open_export = false +) +handle = first(plots) +empty_shell = LineCableModels.plotwindow(; title="Text-only shell", backend=:gl, + display_plot=false, open_export=false) do canvas + Label(canvas[1,1], "Native canvas without axes") +end + +@testset "manual GL plotting gate / no unloaded SVG action" begin + @test Base.get_extension(LineCableModels, :LineCableModelsCairoMakieExt) === nothing + @test !haskey(handle.controls, :export_svg) + @test isempty(empty_shell.controls) + @test sort!(collect(keys(handle.controls))) == [:reset, :xlog, :ylog] + mktempdir() do directory + path = joinpath(directory, "not-created", "unavailable.svg") + @test_throws r"import CairoMakie" export_svg(handle; path, open_file=false) + @test isempty(readdir(directory)) + end +end + +# Loading a renderer does not retrofit existing toolbars. Explicitly select GL +# again because importing CairoMakie itself activates the upstream backend. +import CairoMakie +GLMakie.activate!() + +@testset "manual GL plotting gate / late-loaded renderer preserves the live view" begin + @test !haskey(handle.controls, :export_svg) + @test Base.get_extension( + LineCableModels, :LineCableModelsMakieExt + ).current_backend_symbol() === :gl + axis = first(handle.axes) + area = axis.scene.viewport[] + mouse = Tuple(area.origin + area.widths / 2) + Makie.events(axis.scene).mouseposition[] = mouse + Makie.process_interaction(last(axis.interactions[:scrollzoom]), + Makie.ScrollEvent(0, 3), axis) + Makie.process_interaction(last(axis.interactions[:dragpan]), + Makie.MouseEvent(Makie.MouseEventTypes.rightdrag, + 1.0, Point2d(0), Point2f(mouse .+ (20, 10)), + 0.0, Point2d(0), Point2f(mouse)), axis) + limits_before = [axis.limits[] for axis in handle.axes] + targets_before = [axis.targetlimits[] for axis in handle.axes] + views_before = [axis.finallimits[] for axis in handle.axes] + background_before = handle.figure.scene.backgroundcolor[] + rows_before = copy(handle.figure.layout.rowsizes) + mktempdir() do directory + cd(directory) do + path = export_svg(handle; open_file=false) + @test filesize(path) > 100 + @test occursin(" block isa Label && block.text[] == "Ready", + empty_shell.figure.content)) + rendered_status = Bool[] + observer = on(empty_shell.figure.scene, Makie.events(empty_shell.figure).tick) do tick + tick.state === Makie.OneTimeRenderTick && push!(rendered_status, status_label.blockscene.visible[]) + end + mktempdir() do directory + export_svg(empty_shell; path=joinpath(directory, "text-only.svg"), open_file=false) + end + off(observer) + @test !isempty(rendered_status) && !any(rendered_status) + @test status_label.blockscene.visible[] +end + +plots = Makie.plot(parameters, (R, L, G, C); backend=:gl, + display_plot=true, open_export=false) +handle = first(plots) + +@testset "manual GL plotting gate" begin + @test plots isa Vector{UIPlot} + @test length(plots) == 4 + @test length(handle.axes) == 4 + @test sort!(collect(keys(handle.controls))) == + [:export_svg, :reset, :xlog, :ylog] + @test handle.figure.scene.backgroundcolor[] == Makie.to_color(:grey90) + @test occursin("\\ue5d5", sprint(show, handle.controls[:reset].label[])) + @test occursin("\\ue161", sprint(show, handle.controls[:export_svg].label[])) + + handle.controls[:xlog].active[] = true + handle.controls[:ylog].active[] = true + @test all(axis -> axis.xscale[] === Makie.log10, handle.axes) + @test all(axis -> axis.yscale[] === Makie.log10, handle.axes) + + handle.controls[:reset].clicks[] += 1 + + GLMakie.save(joinpath(ARTIFACT_DIRECTORY, "gl-ui.png"), handle.figure) + mktempdir() do directory + cd(directory) do + handle.controls[:export_svg].clicks[] += 1 + svg_path = joinpath(directory, only(readdir(directory))) + @test filesize(svg_path) > 100 + @test occursin("= 2 + @test issorted(tick_values) && allunique(tick_values) + @test all(value -> isfinite(value) && value > 0, tick_values) + @test allunique(string.(tick_labels)) +end + +@testset "manual GL retained figure survives screen closure" begin + clicks=Ref(0) + control=addwidget!((p,slot) -> Button(slot;label="Count"),handle,:count; + event=button -> button.clicks,callback=(p,_) -> (clicks[]+=1)) + screen=Makie.getscreen(handle.figure.scene) + @test screen!==nothing + close(screen) + control.clicks[]+=1 + @test clicks[]==1 + figuretitle!(handle,"Retained after closure") + mktempdir() do directory + @test isfile(export_svg(handle;path=joinpath(directory,"closed.svg"),open_file=false)) + end + display(handle.figure) + control.clicks[]+=1 + @test clicks[]==2 + removewidget!(handle,:count) +end + +@testset "manual GL independent guide arrangement and resize" begin + include(joinpath(@__DIR__, "../../../test/support/scenarios.jl")) + # Previously scales defaulted to a right column. Previews now default to a + # bottom strip with left-side property labels and 24-pixel plot clearance. + # Explicit group layout still owns cells independently of bar orientation. + p=preview(CurrentScenarios.coaxial_design();backend=:gl,display_plot=true, + size=(1200,900),legend_position=:right, + legend_attributes=(halign=:left,valign=:top), + colorbar_attributes=(vertical=false,width=160,height=14), + colorbar_group_attributes=(layout=(1,3),colgap=16,halign=:center), + guide_spacing=(rowgap=14,colgap=16)) + guide=p.plot_state.guides[(:colorbars,nothing)] + bars=copy(p.colorbars) + axis=only(p.axes) + limits!(axis,-.024,.032,-.028,.028) + view=axis.targetlimits[] + data=copy(axis.scene.plots) + callbacks=length(guide.subscriptions) + clicks=Ref(0) + button=addwidget!((p,slot) -> Button(slot;label="Count"),p,:guide_count; + event=button -> button.clicks,callback=(p,_) -> (clicks[]+=1)) + GLMakie.save(joinpath(ARTIFACT_DIRECTORY,"guide-bottom.png"),p.figure) + @test size(guide.layout[])==(1,3) + # Complete item bounds reserve spacing. Bar frames share a baseline and edge + # independently of the unequal property labels inside those items. + frames=[bar.layoutobservables.computedbbox[] for bar in bars] + @test maximum(frame.origin[2] for frame in frames)-minimum(frame.origin[2] for frame in frames)<=1 + @test maximum(frame.origin[2]+frame.widths[2] for frame in frames)-minimum(frame.origin[2]+frame.widths[2] for frame in frames)<=1 + figurecolorbars!(p;position=:right, + group_attributes=(layout=(3,1),rowgap=12,valign=:bottom),vertical=false) + figurelegend!(p;position=:right,valign=:top,guide_spacing=14) + resize!(p.figure,1000,800) + GLMakie.colorbuffer(p.figure) + frames=[bar.layoutobservables.computedbbox[] for bar in bars] + @test maximum(frame.origin[1] for frame in frames)-minimum(frame.origin[1] for frame in frames)<=1 + @test maximum(frame.origin[1]+frame.widths[1] for frame in frames)-minimum(frame.origin[1]+frame.widths[1] for frame in frames)<=1 + legend=p.legend.layoutobservables.computedbbox[] + scales=guide.layout[].layoutobservables.computedbbox[] + @test legend.origin[2]-(scales.origin[2]+scales.widths[2])>=13 + @test size(guide.layout[])==(3,1) + @test all(a===b for (a,b) in zip(bars,p.colorbars)) + @test axis.targetlimits[]==view + @test axis.scene.plots==data + @test p.controls[:guide_count]===button + @test length(guide.subscriptions)==callbacks + button.clicks[]+=1 + @test clicks[]==1 + GLMakie.save(joinpath(ARTIFACT_DIRECTORY,"guide-right.png"),p.figure) +end + +GLMakie.closeall() diff --git a/test/manual/plotting/manual_gl_cable_collection.jl b/test/manual/plotting/manual_gl_cable_collection.jl new file mode 100644 index 000000000..d0b2b1e94 --- /dev/null +++ b/test/manual/plotting/manual_gl_cable_collection.jl @@ -0,0 +1,57 @@ +using Test +using LineCableModels +using LineCableModels.DataModel +using GLMakie + +const CABLE_COLLECTION_GALLERY_SMOKE_ONLY = lowercase( + get(ENV, "LINECABLEMODELS_GL_GALLERY_SMOKE", "false") +) == "true" + +# Current construction inputs. No serialized historical model is loaded. +include(joinpath(@__DIR__,"../../../test/support/scenarios.jl")) +designs=[CurrentScenarios.coaxial_design(;scale=1+index/10,name="Cable $index") for index in 1:5] +display_plot = !CABLE_COLLECTION_GALLERY_SMOKE_ONLY + +# Omitting `layout` exercises the near-square rule: five designs become a 2×3 +# canvas. The one set of material colorbars is calculated from all five designs. +automatic = preview( + designs; + size = (1400, 900), + backend = :gl, + display_plot, + open_export = false +) + +# Passing `(rows, columns)` exercises the caller-owned layout choice. The same +# preview data is used. Only the native grid differs. +explicit = preview( + designs[1:4]; + layout = (1, 4), + size = (1600, 500), + backend = :gl, + display_plot, + open_export = false +) + +@testset "manual GL cable collection preview" begin + @test length(automatic.axes) == 5 + @test length(explicit.axes) == 4 + @test automatic.legend === nothing + @test explicit.legend === nothing + @test length(automatic.colorbars) == 3 + @test [axis.title[] for axis in automatic.axes] == + getproperty.(designs, :cable_id) +end + +println("Built automatic 2×3 and explicit 1×4 cable-preview canvases.") +println("Confirm that every subplot title is its cable id and no legend is present.") +println("Confirm that each canvas has one shared, top-aligned set of material colorbars.") + +if CABLE_COLLECTION_GALLERY_SMOKE_ONLY + println("GL gallery smoke-only mode complete; native windows were not opened.") + exit() +end + +println("Inspect the native GLMakie figures, then press Enter to finish.") +readline() +GLMakie.closeall() diff --git a/test/manual/plotting/manual_gl_comparison.jl b/test/manual/plotting/manual_gl_comparison.jl new file mode 100644 index 000000000..0ed77a2cc --- /dev/null +++ b/test/manual/plotting/manual_gl_comparison.jl @@ -0,0 +1,26 @@ +using Test,LineCableModels,GLMakie +include(joinpath(@__DIR__,"../../../test/support/scenarios.jl")) +using .CurrentScenarios +reference=two_conductor_results() +omega=reshape(2pi.*frequencies(reference),1,1,:) +function comparison(channels) + r,l,g,c=channels + LineParameters(r*R(reference).+im.*omega.*l.*L(reference), + g*G(reference).+im.*omega.*c.*C(reference),frequencies(reference); + # Before: an untyped details NamedTuple was accepted. Completed results + # now carry ComputationDetails. The coordinate names keep their meaning. + details=ComputationDetails((coordinates=["west","east"],))) +end +# Manufactured routing variants include no author or physical-accuracy label. +plots=Makie.plot(reference,comparison((1.1,1.2,1.3,1.4)),comparison((.9,.8,.7,.6)); + series_labels=("Current routing input","Larger channels","Smaller channels"), + # Before: result plots accepted size. Now fig_size describes the nominal layout. + # Each quantity still returns its own UIPlot (preview and plotwindow retain size). + ydata=(R,L,G,C),xscale=:log10,fig_size=(1100,750),backend=:gl,display_plot=true,open_export=false) +@test length(plots)==4 +@test all(page->length(page.axes)==4,plots) +@test all(page->haskey(page.controls,:legend),plots) +println("Inspect the four quantity pages, resize them and toggle legend entries.") +println("Press Enter to close the figures.") +readline() +GLMakie.closeall() diff --git a/test/manual/plotting/manual_gl_gallery.jl b/test/manual/plotting/manual_gl_gallery.jl new file mode 100644 index 000000000..52566a0ea --- /dev/null +++ b/test/manual/plotting/manual_gl_gallery.jl @@ -0,0 +1,32 @@ +using LineCableModels +using GLMakie +using CairoMakie + +const GL_GALLERY_SMOKE_ONLY = lowercase( + get(ENV, "LINECABLEMODELS_GL_GALLERY_SMOKE", "false") +) == "true" +const GL_GALLERY_ARTIFACT_DIRECTORY = abspath(get( + ENV, + "LINECABLEMODELS_GL_ARTIFACTS", + joinpath(get(ENV, "LINECABLEMODELS_MANUAL_OUTPUT", + joinpath(tempdir(), "linecablemodels-manual")), "plotting", "gl-gallery") +)) + +mkpath(GL_GALLERY_ARTIFACT_DIRECTORY) +cd(GL_GALLERY_ARTIFACT_DIRECTORY) +include(joinpath(@__DIR__, "_gallery_fixtures.jl")) +gallery = build_manual_plot_gallery(:gl; display_plot = !GL_GALLERY_SMOKE_ONLY) + +println("Built $(length(gallery)) GLMakie inspection panels.") +println("SVG exports are written to $GL_GALLERY_ARTIFACT_DIRECTORY") +println("Resize the compact cable-preview window to inspect legend restoration.") + +if GL_GALLERY_SMOKE_ONLY + println("GL gallery smoke-only mode complete; native windows were not opened.") + exit() +end + +println("Each page is open in its own native GLMakie window.") +println("Press Enter to close all figures and finish.") +readline() +GLMakie.closeall() diff --git a/test/manual/plotting/manual_gl_monte_carlo.jl b/test/manual/plotting/manual_gl_monte_carlo.jl new file mode 100644 index 000000000..a41b00ad0 --- /dev/null +++ b/test/manual/plotting/manual_gl_monte_carlo.jl @@ -0,0 +1,16 @@ +using Test,LineCableModels,GLMakie,Measurements +include(joinpath(@__DIR__,"../../../test/support/scenarios.jl")) +using .CurrentScenarios +smoke=lowercase(get(ENV,"LINECABLEMODELS_GL_GALLERY_SMOKE","false"))=="true" +# Synthetic completed storage exercises presentation without a new sampling run. +result=cable_monte_carlo_result() +views=[verb(result,R;backend=:gl,display_plot=!smoke,open_export=false) + for verb in (Makie.hist,Makie.stairs,Makie.ecdfplot,Makie.lines,Makie.qqplot)] +@test all(view->view isa UIPlot,views) +@test all(view->length(view.axes)==1,views) +println("Built current histogram, density, empirical/model CDF and QQ views.") +if !smoke + println("Press Enter to close the figures.") + readline() +end +GLMakie.closeall() diff --git a/test/manual/plotting/manual_wgl.jl b/test/manual/plotting/manual_wgl.jl new file mode 100644 index 000000000..13123761b --- /dev/null +++ b/test/manual/plotting/manual_wgl.jl @@ -0,0 +1,72 @@ +const WGL_MANUAL_ENABLED = lowercase(get(ENV, "LINECABLEMODELS_WGL_MANUAL", "false")) == + "true" + +if !WGL_MANUAL_ENABLED + println("WGL manual gallery is disabled.") + println("Run with LINECABLEMODELS_WGL_MANUAL=true to opt in.") + exit() +end + +using LineCableModels +using WGLMakie +using CairoMakie + +const Bonito = WGLMakie.Bonito +const DOM = Bonito.DOM +const WGL_SMOKE_ONLY = lowercase(get(ENV, "LINECABLEMODELS_WGL_SMOKE", "false")) == "true" +const WGL_ARTIFACT_DIRECTORY = abspath(get( + ENV, + "LINECABLEMODELS_WGL_ARTIFACTS", + joinpath(get(ENV, "LINECABLEMODELS_MANUAL_OUTPUT", + joinpath(tempdir(), "linecablemodels-manual")), "plotting", "wgl") +)) + +mkpath(WGL_ARTIFACT_DIRECTORY) +cd(WGL_ARTIFACT_DIRECTORY) +include(joinpath(@__DIR__, "_gallery_fixtures.jl")) +gallery = build_manual_plot_gallery(:wgl; display_plot = false) + +println("Built $(length(gallery)) WGL inspection panels.") +println("SVG exports are written to $WGL_ARTIFACT_DIRECTORY") + +if WGL_SMOKE_ONLY + println("WGL smoke-only mode complete; browser server was not started.") + exit() +end + +function gallery_card(title, handle) + return DOM.section( + DOM.h2(title; style = "margin: 0 0 0.5rem 0; font: 600 1.1rem sans-serif;"), + handle.figure; + style = "background: #e5e5e5; padding: 0.75rem; border-radius: 0.4rem;" + ) +end + +app = Bonito.App() do + cards = [gallery_card(title, handle) for (title, handle) in gallery] + return DOM.main( + DOM.h1("LineCableModels WGL manual gallery"), + DOM.p( + "Inspect reset, SVG export, line-parameter logarithmic axes, legends, " * + "visibility, zoom/pan, cable/system previews, material scales, and responsive legends. " * + "The separate manual_gl_monte_carlo.jl script covers retained statistical views." + ), + DOM.div( + cards...; + style = "display: grid; grid-template-columns: repeat(auto-fit, minmax(820px, 1fr)); gap: 1rem;" + ); + style = "max-width: 1800px; margin: auto; padding: 1rem; font-family: sans-serif;" + ) +end + +host = get(ENV, "LINECABLEMODELS_WGL_HOST", "127.0.0.1") +port = parse(Int, get(ENV, "LINECABLEMODELS_WGL_PORT", "8082")) +server = Bonito.Server(app, host, port) +Bonito.HTTPServer.start(server) +println("Open http://$host:$port in a browser; press Ctrl+C here to stop.") + +try + wait(server) +finally + close(server) +end diff --git a/test/manual/plotting/surprised_pikachu_cable.jl b/test/manual/plotting/surprised_pikachu_cable.jl new file mode 100644 index 000000000..8eeee7e96 --- /dev/null +++ b/test/manual/plotting/surprised_pikachu_cable.jl @@ -0,0 +1,122 @@ +# Make the disposable script runnable directly, without requiring `--project=.`. +pushfirst!(LOAD_PATH, normpath(joinpath(@__DIR__, "..", "..", ".."))) + +using LineCableModels +using GLMakie + +# For all you unbelievers: a six-terminal, surprised electric mouse. +const mm = 1.0e-3 + +function ellipse_polygon(cx, cy, rx, ry; vertices = 48, rotation = 0.0) + cosine, sine = cos(rotation), sin(rotation) + return [begin + x, y = rx * cos(angle), ry * sin(angle) + ((cx + cosine * x - sine * y) * mm, + (cy + sine * x + cosine * y) * mm) + end + for angle in range(0, 2pi; length = vertices + 1)[1:(end - 1)]] +end + +polygon(points) = Polygon([(x * mm, y * mm) for (x, y) in points]) + +# The constitutive properties remain physical. The cartoon palette below is only +# presentation metadata applied to the native preview series. +yellow_rubber = Material(:insulator, 1.0e14, 3.2, 1.0, 20.0, 0.0) +copper = Material(:conductor, 1.7241e-8, 1.0, 0.999994, 20.0, 0.00393) + +# One concave arbitrary polygon supplies the insulating outer contour. The ear +# tips and indented cheeks deliberately make this much less cable-like. +body = Region(:pikachu, + polygon([ + (-49, -36), (49, -36), (46, -17), (42, 5), (36, 29), + (34, 38), (48, 52), (61, 73), (34, 62), (13, 51), + (6, 50), (-8, 50), (-16, 52), (-38, 66), (-62, 80), + (-53, 52), (-36, 31), (-33, 12), (-39, -12), (-45, -29) + ]), + yellow_rubber) + +# Every facial feature is also an arbitrary polygon, wrapped in its own +# terminal so this ridiculous cross-section remains a valid cable design. +left_cheek = terminal(:left_cheek, + Region(:left_cheek, Polygon(ellipse_polygon(-27.5, -5, 8.5, 8.2)), copper)) +right_cheek = terminal(:right_cheek, + Region(:right_cheek, Polygon(ellipse_polygon(27.5, -5, 8.5, 8.2)), copper)) + +left_eye = terminal(:left_eye, + assembly( + Region(:eye, Polygon(ellipse_polygon(-18, 18, 7.0, 7.4)), copper), + Region(:eye_highlight, Polygon(ellipse_polygon(-20.2, 20.4, 2.5, 2.7)), copper) + )) +right_eye = terminal(:right_eye, + assembly( + Region(:eye, Polygon(ellipse_polygon(18, 18, 7.0, 7.4)), copper), + Region(:eye_highlight, Polygon(ellipse_polygon(15.8, 20.4, 2.5, 2.7)), copper) + )) + +nose = terminal(:nose, + Region(:nose, Polygon(ellipse_polygon(0, 7.8, 2.5, 1.5)), copper)) +mouth = terminal(:mouth, + assembly( + Region(:mouth_cavity, Polygon(ellipse_polygon(0, -13, 9.0, 9.5)), copper), + Region(:mouth_inside, Polygon(ellipse_polygon(0.5, -13.8, 7.0, 7.2)), copper) + )) + +pikachu_assembly = assembly( + body, + left_cheek, + right_cheek, + left_eye, + right_eye, + nose, + mouth +) + +design = build(CableDesign, "surprised-pikachu", pikachu_assembly) + +presentation_groups = Dict( + :pikachu => :insulating_body, + :left_cheek => :cheeks, + :right_cheek => :cheeks, + :left_eye => :eyes, + :right_eye => :eyes, + :eye => :eyes, + :eye_highlight => :eye_highlights, + :nose => :nose, + :mouth_cavity => :mouth_cavity, + :mouth_inside => :mouth_inside +) + +# Group order follows first appearance in the assembly: body, cheeks, eyes, +# highlights, nose, mouth cavity, mouth interior. Colors are deliberately +# presentation-only overrides. +preview_plot = preview( + design; + backend = :gl, + display_plot = true, + controls = true, + display_legend = false, + display_colorbars = false, + display_dielectric_pattern = false, + title = "Unbelievers, lo and behold!", + panel_titles = ("I told y'all that this could run a Pikachu-shaped cable model!",), + size = (900, 760), + legend_group = presentation_groups, + series_attributes = ( + (; color = "#F6C945", strokecolor = "#2A2510", strokewidth = 5.0), + (; color = "#E64A2E", strokecolor = "#C83A21", strokewidth = 2.0), + (; color = "#211D08", strokecolor = "#211D08", strokewidth = 1.5), + (; color = "#FFFDF4", strokecolor = "#FFFDF4", strokewidth = 0.5), + (; color = "#5B0805", strokecolor = "#3B0503", strokewidth = 1.5), + (; color = "#211D08", strokecolor = "#211D08", strokewidth = 2.0), + (; color = "#EF7C5B", strokecolor = "#EF7C5B", strokewidth = 1.0) + ) +) + +# axis = only(preview_plot.axes) +# hidedecorations!(axis) +# hidespines!(axis) + +# output_path = joinpath(get(ENV, "LINECABLEMODELS_MANUAL_OUTPUT", +# joinpath(tempdir(), "linecablemodels-manual")), "plotting", "surprised_pikachu_cable.png") +# CairoMakie.save(output_path, preview_plot.figure. Px_per_unit = 2) +# println("Refactor morale conductor energized: $output_path") diff --git a/test/manual/prototypes/prototype_tape_element.jl b/test/manual/prototypes/prototype_tape_element.jl new file mode 100644 index 000000000..c7ed5ff72 --- /dev/null +++ b/test/manual/prototypes/prototype_tape_element.jl @@ -0,0 +1,314 @@ +# Included by prototype_wire_screen.jl. 4 exact tape faces, not a general BEM +# backend. Jacobi-weighted continuous charge measures replace fictitious tape +# sources. All kernels, core and foil functions and round-wire sources stay owned by +# the parent experiment. References for the representation and integration: +# https://idus.us.es/server/api/core/bitstreams/aae30e40-44f5-4243-9c30-35d64123f0e5/content +# https://ars.copernicus.org/articles/9/39/2011/ +# https://math.dartmouth.edu/~fastdirect/notes/quadr.pdf + +function ws_jacobi!(values,t,alpha,beta) + p = length(values)-1 + values[1] = 1 + p == 0 && return values + values[2] = ((alpha+beta+2)*t+alpha-beta)/2 + s = alpha+beta + for n in 1:p-1 + A = (2n+s+1)*(2n+s+2)/(2(n+1)*(n+s+1)) + B = (alpha^2-beta^2)*(2n+s+1)/(2(n+1)*(n+s+1)*(2n+s)) + C = (n+alpha)*(n+beta)*(2n+s+2)/((n+1)*(n+s+1)*(2n+s)) + values[n+2] = (A*t+B)*values[n+1]-C*values[n] + end + values +end + +ws_jacobi(t,alpha,beta,p) = ws_jacobi!(Vector{Float64}(undef,p+1),t,alpha,beta) + +function ws_gauss_jacobi(n,alpha,beta) + alpha > -1 && beta > -1 && n > 1 || error("Invalid Jacobi rule.") + s = alpha+beta + diagonal = [(beta-alpha)/(s+2); + [(beta^2-alpha^2)/((2k+s)*(2k+s+2)) for k in 1:n-1]] + off = [2/(s+2)*sqrt((1+alpha)*(1+beta)/(s+3)); + [2/(2k+s)*sqrt(k*(k+alpha)*(k+beta)*(k+s)/ + ((2k+s-1)*(2k+s+1))) for k in 2:n-1]] + decomposition = eigen(SymTridiagonal(diagonal,off)) + # Normalized measure w(t)dt / integral(w). No arclength factor belongs here. + decomposition.values,vec(decomposition.vectors[1,:].^2) +end + +function ws_junction_exponent(epsilon_host,epsilon_outer) + epsilon_host > 0 && epsilon_outer > 0 || error("Positive dielectric permittivities required.") + # Host sector pi/2, outer dielectric sector pi. Determinant has no cotangent + # poles. Exclude the trivial nu=0 root and bracket the first positive root. + determinant(nu) = epsilon_host*cospi(nu/2)*sinpi(nu) + + epsilon_outer*sinpi(nu/2)*cospi(nu) + left,right = 1e-10,1.0 + @assert determinant(left) > 0 && determinant(right) < 0 + for _ in 1:60 + mid = (left+right)/2 + if determinant(mid) > 0 + left = mid + else + right = mid + end + end + (left+right)/2 +end + +function ws_exposed_geometry_check(g) + # Whole-face bases require whole exposed faces. Reject overlaps rather than + # silently impose boundary conditions inside a metal union or move geometry. + tolerance = 64eps(Float64)*g.b + centers = [complex(w.at.x,w.at.y) for w in g.wires] + gaps = [abs(centers[i]-centers[j])-g.wires[i].r-g.wires[j].r + for i in eachindex(centers) for j in 1:i-1] + wire_gap = isempty(gaps) ? Inf : minimum(gaps) + wire_gap >= -tolerance || error("Overlapping screen wires: exposed-union boundaries required.") + radial = [g.tape.ri-abs(c)-w.r for (c,w) in zip(centers,g.wires)] + minimum(radial) >= -tolerance || error("Wire/tape radial overlap: whole-face tape prototype is not applicable.") + @assert all(abs(c)-w.r >= g.a-tolerance for (c,w) in zip(centers,g.wires)) + @assert g.a < g.tape.ri < g.tape.ro <= g.b+tolerance + @assert 0 < g.tape.span < 2pi + (; wire_wire_gap_m = wire_gap, wire_tape_radial_gap_m = minimum(radial), + tolerance_m = tolerance) +end + +ws_face_point(face,t) = face.kind == :arc ? + face.radius*cis(face.phi+face.span*t/2) : + (face.mid+face.half*t)*cis(face.phi) + +function ws_tape_faces(g,p,quadrature) + p >= 0 && quadrature >= max(2,p+1) || error("Quadrature needs at least order+1 nodes.") + s = g.tape + at_interface = isapprox(s.ro,g.b; atol = 64eps(Float64)*g.b,rtol = 0) + nu_inner = 2/3 + nu_outer = at_interface ? ws_junction_exponent(g.epsilon,first(g.right).epsilon) : 2/3 + descriptions = [ + (;kind = :arc,radius = s.ri,phi = s.at.φ,span = s.span,mid = 0.0,half = 0.0, + alpha = nu_inner-1,beta = nu_inner-1), + (;kind = :arc,radius = s.ro,phi = s.at.φ,span = s.span,mid = 0.0,half = 0.0, + alpha = nu_outer-1,beta = nu_outer-1), + [(;kind = :end,radius = 0.0,phi = s.at.φ+sign*s.span/2,span = 0.0, + mid = (s.ri+s.ro)/2,half = (s.ro-s.ri)/2, + alpha = nu_outer-1,beta = nu_inner-1) for sign in (-1,1)]...] + map(descriptions) do face + nodes,weights = ws_gauss_jacobi(quadrature,face.alpha,face.beta) + polys = transpose(reduce(hcat,[ws_jacobi(t,face.alpha,face.beta,p) for t in nodes])) + weighted = weights.*polys + loggamma = LineCableModels.Engine.SpecialFunctions.loggamma + beta_norm = exp(loggamma(face.alpha+1)+loggamma(face.beta+1)- + loggamma(face.alpha+face.beta+2)) + merge(face,(;p,nodes,weights,weighted,beta_norm, + points = ws_face_point.(Ref(face),nodes))) + end +end + +function ws_face_projection(z,face) + if face.kind == :arc + radius = abs(z) + angle_difference = atan(sin(angle(z)-face.phi),cos(angle(z)-face.phi)) + u = 2angle_difference/face.span + delta = abs(radius-face.radius)/(face.radius*face.span/2) + else + rotated = z*cis(-face.phi) + u = (real(rotated)-face.mid)/face.half + delta = abs(imag(rotated))/face.half + end + u,delta +end + +function ws_log_moments(z,face; rtol = 1e-10) + u,delta = ws_face_projection(z,face) + if hypot(max(abs(u)-1,0),delta) > 0.2 + return transpose(face.weighted)*log.(abs.(face.points.-z)),0.0 + end + # Weighted logarithmic product integration: compute the log moments of the + # Jacobi modes, independently of the smooth Green remainder. t=cos(theta) + # absorbs endpoint weights. Split at the logarithmic singularity or near peak. + theta0 = acos(clamp(u,-1,1)) + splits = [0.0,theta0,pi] + if delta > 0 + width = min(sqrt(delta),delta/max(sin(theta0),eps(Float64))) + for sign in (-1,1), factor in (1,4) + push!(splits,clamp(theta0+sign*factor*width,0,pi)) + end + end + sort!(unique!(splits)) + function integrand!(output,theta) + t = cos(theta) + difference = abs(u) <= 1 ? + -2sin((theta+theta0)/2)*sin((theta-theta0)/2) : t-u + distance = if face.kind == :arc + # Exact opposed-arc distance, also valid in the self limit. + hypot(abs(z)-face.radius, + 2sqrt(abs(z)*face.radius)*sin(face.span*difference/4)) + else + hypot(face.half*difference,imag(z*cis(-face.phi))) + end + # Roundoff may identify the endpoint only after its contribution is + # below integration accuracy. Never manufacture a finite log(0) floor. + distance > 0 || error("Unresolved logarithmic integration point.") + weight = sin(theta/2)^(2face.alpha+1)*cos(theta/2)^(2face.beta+1)/face.beta_norm + ws_jacobi!(output,t,face.alpha,face.beta) + output .*= log(distance)*weight + end + # Reuse quadrature work vectors. Modal integration must not allocate a new + # polynomial vector at each of its thousands of function evaluations. + parentmodule(LineCableModels.Engine.quadgk).quadgk!(integrand!,zeros(face.p+1),splits...; + rtol,atol = rtol*0.01,order = max(7,cld(face.p+1,2)),maxevals = 100000) +end + +function ws_tape_columns(targets,faces,g,k; log_rtol = 1e-10) + columns = Matrix{Float64}(undef,length(targets),sum(f.p+1 for f in faces)) + error_bound = 0.0 + offset = 0 + for face in faces + block = ws_kernel_matrix(targets,face.points,g,k; split_images = true)*face.weighted + for (i,z) in pairs(targets) + inner,outer = g.a^2/conj(z),g.b^2/conj(z) + direct_coefficient = 1.0 + images = Tuple{ComplexF64,Float64}[] + for (point,coefficient) in ((inner,k.ra0),(outer,k.rb0)) + if abs(point-z) <= 8eps(Float64)*g.b + direct_coefficient += coefficient + else + push!(images,(point,coefficient)) + end + end + push!(images,(z,direct_coefficient)) + for (point,coefficient) in images + coefficient == 0 && continue + moments,err = ws_log_moments(point,face; rtol = log_rtol) + block[i,:] .-= (coefficient/g.epsilon).*moments + error_bound = max(error_bound,abs(coefficient/g.epsilon)*err) + end + end + columns[:,offset+1:offset+face.p+1] .= block + offset += face.p+1 + end + (;columns,log_moment_error = error_bound) +end + +function ws_face_targets(faces,n; validation = false) + parameters = -cos.(pi.*((1:n).-0.5)./n) + if validation + # Independent uniform targets plus progressively closer corner targets. + parameters = sort!(unique!([parameters;collect(range(-1,1;length = n+1)); + [-1+10.0^-k for k in 2:10];[1-10.0^-k for k in 2:10]])) + end + [ws_face_point(face,t) for face in faces for t in parameters] +end + +function ws_spectral_capacitance(g,level; source_fraction = 0.75,log_rtol = 1e-10, + validation_factor = 3,penetration_target = 0.01) + 0 < source_fraction < 1 || error("Wire source fraction must be between zero and one.") + level.wire >= 4 && level.modes >= 1 || error("Insufficient wire/kernel resolution.") + log_rtol > 0 && penetration_target > 0 || error("Positive accuracy targets required.") + geometry_check = ws_exposed_geometry_check(g) + k = ws_kernel_coefficients(g,level.modes) + faces = ws_tape_faces(g,level.order,level.quadrature) + wires = ws_points(g,level.wire,source_fraction) + targets = [ws_points(g,2level.wire,source_fraction).targets; + ws_face_targets(faces,max(24,4(level.order+1)))] + tape_matrix = ws_tape_columns(targets,faces,g,k;log_rtol) + K = hcat(ws_kernel_matrix(targets,wires.sources,g,k),tape_matrix.columns) + rhs = hcat(-ws_core_voltage.(targets,Ref(g)),ones(length(targets))) + norms = vec(sqrt.(sum(abs2,K;dims = 1))) + scaled = K./transpose(norms) + factor = svd(scaled) + cutoff = max(size(scaled)...)*eps(Float64)*first(factor.S) + retained = factor.S .> cutoff + coefficients = (factor.V[:,retained]* + ((transpose(factor.U[:,retained])*rhs)./factor.S[retained]))./norms + # Exact total-charge moments: higher Jacobi modes integrate to zero. + mass = [ones(length(wires.sources));reduce(vcat,[[1.0;zeros(f.p)] for f in faces])] + core_moment = ws_core_voltage.(wires.sources,Ref(g)) + for face in faces + append!(core_moment,face.kind == :arc ? + [ws_core_voltage(first(face.points),g);zeros(face.p)] : + transpose(face.weighted)*ws_core_voltage.(face.points,Ref(g))) + end + epsilon0 = 8.8541878128e-12 + screen = 2pi*epsilon0*vec(transpose(mass)*coefficients) + core = [2pi*epsilon0/g.Rtotal,0.0] - + 2pi*epsilon0*vec(transpose(core_moment)*coefficients) + C = vcat(transpose(core),transpose(screen)) + check = [ws_points(g,validation_factor*level.wire,source_fraction;shift = 0.37).targets; + ws_face_targets(faces,max(41,validation_factor*4(level.order+1));validation = true)] + tape_check = ws_tape_columns(check,faces,g,k;log_rtol) + errors = hcat(ws_kernel_matrix(check,wires.sources,g,k),tape_check.columns)*coefficients - + hcat(-ws_core_voltage.(check,Ref(g)),ones(length(check))) + wire_count = validation_factor*level.wire*length(g.wires) + residual = maximum(abs,errors) + wire_residual = maximum(abs,errors[1:wire_count,:]) + tape_residual = maximum(abs,errors[wire_count+1:end,:]) + penetration_residual = maximum(abs,vec(sum(errors;dims = 2))) + ccs,cca,csa = -C[1,2],sum(C[1,:]),sum(C[2,:]) + ccs_scale = max(abs(C[1,2]),abs(C[2,1])) + required_residual = penetration_target*abs(cca)/ccs_scale + moment_error = max(tape_matrix.log_moment_error,tape_check.log_moment_error) + log_integration_indicator_V = 6moment_error* + maximum(vec(sum(abs.(coefficients[length(wires.sources)+1:end,:]);dims = 1))) + (;C,q = coefficients,residual,wire_residual,tape_residual,penetration_residual,required_residual, + sampled_penetration_relative_indicator = penetration_residual*ccs_scale/abs(cca), + reciprocity = norm(C-transpose(C))/norm(C), + mutual_reciprocity = abs(C[1,2]-C[2,1])/max(abs(C[1,2]),abs(C[2,1])), + unknowns = length(mass),rank = count(retained), + condition = first(factor.S)/last(factor.S),level,tape = true,model = :spectral, + worst_point = check[argmax(abs.(errors))[1]],faces,geometry_check, + log_moment_error = moment_error,log_integration_indicator_V) +end + +function ws_tape_selfchecks() + @testset "Four-face tape controls" begin + @test ws_junction_exponent(1.0,1.0) ≈ 2/3 + @test ws_junction_exponent(1.0,32.3) ≈ 2/pi*acos(sqrt(32.3/(2*33.3))) + @test 0.5 < ws_junction_exponent(1.0,32.3) < 2/3 + for (alpha,beta) in ((0.0,0.0),(-1/3,-1/3),(-0.4904397072754232,-1/3)) + nodes,weights = ws_gauss_jacobi(48,alpha,beta) + values = reduce(hcat,[ws_jacobi(t,alpha,beta,8) for t in nodes]) + @test sum(weights) ≈ 1 + @test all(weights .> 0) + @test dot(nodes,weights) ≈ (beta-alpha)/(alpha+beta+2) atol=1e-14 + @test values*weights ≈ [1.0;zeros(8)] atol=1e-13 + end + nodes,weights = ws_gauss_jacobi(64,0.0,0.0) + values = transpose(reduce(hcat,[ws_jacobi(t,0.0,0.0,3) for t in nodes])) + face = (;kind = :end,radius = 0.0,phi = 0.0,span = 0.0,mid = 2.0,half = 0.1, + alpha = 0.0,beta = 0.0,p = 3,nodes,weights,beta_norm = 1.0, + weighted = weights.*values,points = complex.(2 .+ 0.1.*nodes)) + moments,_ = ws_log_moments(2.0+0im,face) + @test moments ≈ [log(0.1)-1,0,1/3,0] atol=2e-10 + endpoint,_ = ws_log_moments(2.1+0im,face) + @test endpoint[1] ≈ log(0.2)-1 atol=2e-10 + for distance in (0.1,1e-5) + near,_ = ws_log_moments(2.0+im*distance,face) + exact = log(hypot(0.1,distance))-1+distance/0.1*atan(0.1/distance) + @test near[1] ≈ exact atol=2e-10 + end + h = (;a = 1.0,b = 3.0,epsilon = 1.0,Rleft = 0.0,Rright = log(4/3)/4, + Rtotal = log(3)+log(4/3)/4,left = NamedTuple[], + right = [(ri = 3.0,ro = 4.0,epsilon = 4.0)]) + k = ws_kernel_coefficients(h,512) + for (z,s) in ((1.3cis(0.2),2.1cis(1.1)),(3cis(0.2),2.5cis(-0.4))) + split = ws_kernel(z,s,h,k;split_images = true)- + (log(abs(z-s))+k.ra0*log(abs(h.a^2/conj(z)-s))+ + k.rb0*log(abs(h.b^2/conj(z)-s)))/h.epsilon + @test split ≈ ws_kernel(z,s,h,k) atol=2e-14 + end + # Direct and coincident nearest image produce the interface coefficient. + difference = ws_kernel(3.0+0im,3cis(1e-7),h,k)- + ws_kernel(3.0+0im,3cis(2e-7),h,k) + @test difference ≈ (2/5)*log(2) rtol=1e-5 + # The charge measure contains no extra face-length Jacobian. Scaling all + # radii and target-source positions leaves the potential columns unchanged. + targets = [1.4cis(0.1),2.5cis(0.3)] + original = ws_tape_columns(targets,[face],h,k).columns + scaled_h = merge(h,(a = 10h.a,b = 10h.b, + right = [(ri = 30.0,ro = 40.0,epsilon = 4.0)])) + scaled_face = merge(face,(mid = 10face.mid,half = 10face.half,points = 10face.points)) + scaled = ws_tape_columns(10targets,[scaled_face],scaled_h, + ws_kernel_coefficients(scaled_h,512)).columns + @test scaled ≈ original atol=1e-12 + end +end diff --git a/test/manual/prototypes/prototype_wire_screen.jl b/test/manual/prototypes/prototype_wire_screen.jl new file mode 100644 index 000000000..f291e7cec --- /dev/null +++ b/test/manual/prototypes/prototype_wire_screen.jl @@ -0,0 +1,403 @@ +# Disposable 18 kV internal-shunt experiment. +# Run: `include("test/manual/prototypes/prototype_wire_screen.jl")` +# No engine extensions, FEM solves, campaign writes, or fitting to FEM data. +# A LOCAL layered-annulus Laplace Green function couples round-wire auxiliary +# sources to continuous, corner-weighted charge modes on all four tape faces. The core remains the +# analytical equivalent circle. The foil is the local voltage reference. +# This approximation solves the cross-section. Helical and full-wave fields are outside its scope. +# Before: this experiment activated Gauntlet. Now preserve the active IDE project. +gauntlet_project = normpath(joinpath(@__DIR__, "..", "..", "..", "gauntlet")) +gauntlet_project in LOAD_PATH || push!(LOAD_PATH, gauntlet_project) +using LineCableModels, LinearAlgebra, DataFrames, JLD2, SHA, TOML, Test +isdefined(@__MODULE__, :Gauntlet) || + include(joinpath(gauntlet_project, "Gauntlet.jl")) + +# Manual controls +ws_campaign = joinpath(gauntlet_project, ".work", "all-references") +ws_case_id = :cable_18kv_1000mm2_trefoil +# Each labeled trial changes one numerical control from the baseline. +# The final trial combines these changes. +ws_levels = ( + (label = "baseline", wire = 32, order = 16, quadrature = 128, modes = 512), + (label = "tape order", wire = 32, order = 32, quadrature = 128, modes = 512), + (label = "wire resolution", wire = 64, order = 16, quadrature = 128, modes = 512), + (label = "quadrature", wire = 32, order = 16, quadrature = 256, modes = 512), + (label = "kernel", wire = 32, order = 16, quadrature = 128, modes = 1024), + (label = "combined", wire = 64, order = 32, quadrature = 256, modes = 1024)) +ws_log_rtol = 1e-10 +ws_penetration_target = 0.01 # 1% target on core-to-foil coupling, not the matrix norm. +ws_source_fraction = 0.75 # Numerical source location inside metal, NOT a geometry change. +ws_make_plots = true # Set true in a graphical REPL. Uses the owned plot recipe. + +# All helper names are local to this disposable experiment, not engine methods. +function ws_read_result(path) + digest = first(split(read(path * ".sha256", String))) + bytes2hex(open(sha256, path)) == digest || error("Changed numerical payload: $path") + jldopen(path, "r") do f + f["basis"] == :pul || error("This experiment requires per-unit-length operands.") + LineParameters(PhaseDomain, f["Z"], f["Y"], f["frequencies"]; basis = :pul) + end +end + +function ws_geometry(design) + regions = design.geometry.regions + bytag(tag) = only(filter(r -> r.source.tag == tag, regions)) + wires = [r.primitive for r in regions if r.source.tag == :sheath_wires] + all(w -> w isa Disk, wires) || error("Prototype supports round screen wires only.") + tape = bytag(:sheath_copper_tape).primitive + filler = bytag(:screen_matrix_fill) + host = filler.primitive.outer + host isa Annulus || error("Screen filler must have a concentric annular envelope.") + foil = bytag(:jacket_aluminum_tape).primitive + inner = [bytag(tag) + for tag in (:core_semicon_tape_inner, :core_semicon_inner, + :core_insulation, :core_semicon_outer, :core_semicon_tape_outer)] + outer = [bytag(:sheath_water_blocking)] + all(r -> r.primitive isa Annulus, [inner; outer]) || + error("Concentric layers required.") + all(r -> r.primitive.at.x == r.primitive.at.y == 0, [inner; outer]) || + error("Off-center dielectric layers are outside this prototype.") + layer(r) = (ri = r.primitive.ri, ro = r.primitive.ro, epsilon = r.source.material.eps_r) + left, right = layer.(inner), layer.(outer) + @assert last(left).ro ≈ host.ri + @assert first(right).ri ≈ host.ro && last(right).ro ≈ foil.ri + for layers in (left, right), i in 2:length(layers) + + @assert layers[i - 1].ro ≈ layers[i].ri + end + # Default owned dielectric laws are lossless with frequency-independent eps_r. + # Do not silently extend this one-solve experiment to lossy or dispersive laws. + epsilon = filler.source.material.eps_r + Rleft = sum(log(l.ro/l.ri)/l.epsilon for l in left) + Rright = sum(log(l.ro/l.ri)/l.epsilon for l in right) + L = log(host.ro/host.ri) + Rtotal = Rleft + L/epsilon + Rright + (; wires, tape, left, right, a = host.ri, b = host.ro, epsilon, + Rleft, Rright, Rtotal, core = first(left).ri, foil = foil.ri) +end + +# Mode-m input normal admittance of a radial dielectric stack, terminated by +# a fixed-potential circle. In t=log(r), each layer solves v''-m^2*v=0. +# tanh recursion avoids overflow at large m or large dielectric contrasts. +function ws_load(layers, m; reverse_layers = false) + load = Inf + for layer in (reverse_layers ? reverse(layers) : layers) + t = tanh(m * log(layer.ro/layer.ri)) + characteristic = layer.epsilon * m + load = isinf(load) ? characteristic/t : + characteristic * (load + characteristic*t)/(characteristic + load*t) + end + load +end + +function ws_kernel_coefficients(g, modes) + A, B, D = zeros(modes), zeros(modes), zeros(modes) + ra0 = isempty(g.left) ? -1.0 : + (g.epsilon-last(g.left).epsilon)/(g.epsilon+last(g.left).epsilon) + rb0 = isempty(g.right) ? -1.0 : + (g.epsilon-first(g.right).epsilon)/(g.epsilon+first(g.right).epsilon) + for m in 1:modes + l = ws_load(g.left, m)/g.epsilon + r = ws_load(g.right, m; reverse_layers = true)/g.epsilon + ra = isinf(l) ? -1.0 : (m-l)/(m+l) + rb = isinf(r) ? -1.0 : (m-r)/(m+r) + denominator = g.epsilon*m*(1-ra*rb*(g.a/g.b)^(2m)) + A[m] = ra/denominator-ra0/(g.epsilon*m) + B[m] = rb/denominator-rb0/(g.epsilon*m) + D[m] = ra*rb/denominator + end + (; A, B, D, ra0, rb0) +end + +function ws_selfchecks() + # Independent Dirichlet-annulus separated solution, not the reflection form. + h = (; a = 1.0, b = 3.0, epsilon = 1.0, Rleft = 0.0, Rright = 0.0, + Rtotal = log(3.0), left = NamedTuple[], right = NamedTuple[]) + k = ws_kernel_coefficients(h, 256) + @testset "Local Green function controls" begin + for (z, s) in ((1.3cis(0.2), 2.1cis(1.1)), (1.4cis(2.1), 2.5cis(-0.4))) + x, y, L = log(abs(z)), log(abs(s)), log(3.0) + exact = min(x, y)*(L-max(x, y))/L + for m in 1:256 + exact += 2sinh(m*min(x, y))*sinh(m*(L-max(x, y))) / + (m*sinh(m*L))*cos(m*(angle(z)-angle(s))) + end + @test ws_kernel(z, s, h, k) ≈ exact atol=2e-14 + @test ws_kernel(z, s, h, k) ≈ ws_kernel(s, z, h, k) atol=2e-14 + @test abs(ws_kernel(cis(0.3), s, h, k)) < 2e-14 + @test abs(ws_kernel(3cis(0.3), s, h, k)) < 2e-14 + end + h2 = merge(h, (epsilon = 2.0, Rtotal = log(3.0)/2)) + @test ws_kernel(1.4cis(0.2), 2.1cis(1.1), h2, ws_kernel_coefficients(h2, 256)) ≈ + ws_kernel(1.4cis(0.2), 2.1cis(1.1), h, k)/2 + targets, sources = [1.3cis(0.2), 1.4cis(2.1)], [2.1cis(1.1), 2.5cis(-0.4)] + @test ws_kernel_matrix(targets, sources, h, k) ≈ + [ws_kernel(z, s, h, k) for z in targets, s in sources] + regular = ws_kernel_matrix(targets,sources,h,k; regular = true) + @test regular ≈ [ws_kernel(z,s,h,k; regular = true) for z in targets, s in sources] + @test ws_kernel_matrix(targets,sources,h,k) ≈ + regular-[log(abs(z-s)) for z in targets, s in sources] + @test isfinite(ws_kernel(2cis(0.3),2cis(0.3),h,k; regular = true)) + layers = [(ri = 1.0, ro = 1.2, epsilon = 3.0), (ri = 1.2, ro = 2.0, epsilon = 3.0)] + @test ws_load(layers, 5) ≈ ws_load([(ri = 1.0, ro = 2.0, epsilon = 3.0)], 5) + @test ws_load(layers, 1e-8) ≈ 3/log(2) rtol=1e-12 + + # End-to-end charge extraction control: a complete metal annulus must + # recover the two exact coaxial capacitances and perfect shielding. + theta = 2pi .* (0:255) ./ 256 + targets = [1.8cis.(theta); 2.2cis.(theta)] + sources = [1.98cis.(theta); 2.02cis.(theta)] + kernel = ws_kernel_matrix(targets, sources, h, k) + q = kernel \ hcat(-ws_core_voltage.(targets, Ref(h)), ones(length(targets))) + screen = vec(sum(q; dims = 1)) + core = [1/h.Rtotal, 0.0] - + vec(transpose(ws_core_voltage.(sources, Ref(h)))*q) + capacitance = vcat(transpose(core), transpose(screen)) + left, right = 1/log(1.8), 1/log(3/2.2) + @test capacitance ≈ [left -left; -left left+right] rtol=1e-9 + + end +end + +# Potential per auxiliary charge scaled by 2pi*epsilon0. The direct logarithm +# and the nearest dielectric images are exact. Only the distant reflections are +# Fourier-truncated. This avoids slow convergence where the tape touches the dielectric. +# The zero mode is the exact radial series solution. Nonzero modes sum repeated +# reflections between the two layered Robin boundaries. No Sommerfeld kernel +# appears here: this domain ends at the CLOSED aluminum foil. +function ws_kernel(z, source, g, k; regular = false, split_images = false) + r, s = abs(z), abs(source) + Rr = g.Rleft + log(r/g.a)/g.epsilon + Rs = g.Rleft + log(s/g.a)/g.epsilon + result = min(Rr, Rs)*(g.Rtotal-max(Rr, Rs))/g.Rtotal + if split_images + result += (log(max(r,s))+k.ra0*log(s)+k.rb0*(2log(g.b)-log(r)))/g.epsilon + else + result += (regular ? log(max(r, s)) : log(max(r, s)/abs(z-source)))/g.epsilon + result -= k.ra0/g.epsilon*log(abs(1-g.a^2/(conj(z)*source))) + result -= k.rb0/g.epsilon*log(abs(1-z*conj(source)/g.b^2)) + end + qa, qb = g.a^2/(r*s), r*s/g.b^2 + qd = (g.a/g.b)^2 + qc, qe = qd*r/s, qd*s/r + pa, pb, pc, pe = qa, qb, qc, qe + cosine = real(z*conj(source))/(r*s) + previous, current = 1.0, cosine + @inbounds for m in eachindex(k.A) + result += current*(k.A[m]*pa + k.B[m]*pb + k.D[m]*(pc+pe)) + previous, current = current, 2cosine*current-previous + pa *= qa + pb *= qb + pc *= qc + pe *= qe + end + result +end + +function ws_kernel_matrix(targets, sources, g, k; regular = false, split_images = false) + # Same Green function, batched into small Fourier blocks for BLAS. No dense + # N-by-modes cache: working storage is only 4*64 columns per geometric boundary set. + zero_modes = merge(k, (; A = Float64[], B = Float64[], D = Float64[])) + matrix = [ws_kernel(z, s, g, zero_modes; regular,split_images) for z in targets, s in sources] + ta, tb = g.a ./ conj.(targets), targets ./ g.b + sa, sb = g.a ./ conj.(sources), sources ./ g.b + tpa, tpb = ones(ComplexF64, length(targets)), ones(ComplexF64, length(targets)) + spa, spb = ones(ComplexF64, length(sources)), ones(ComplexF64, length(sources)) + for first_mode in 1:64:length(k.A) + modes = first_mode:min(first_mode + 63, length(k.A)) + U = Matrix{Float64}(undef, length(targets), 4length(modes)) + V = Matrix{Float64}(undef, length(sources), 4length(modes)) + for (column, m) in enumerate(modes) + tpa .*= ta + tpb .*= tb + spa .*= sa + spb .*= sb + cross = k.D[m]*(g.a/g.b)^m + @inbounds for i in eachindex(targets) + U[i, 4column - 3]=real(tpa[i]) + U[i, 4column - 2]=imag(tpa[i]) + U[i, 4column - 1]=real(tpb[i]) + U[i, 4column]=imag(tpb[i]) + end + @inbounds for j in eachindex(sources) + a=k.A[m]*spa[j]+cross*spb[j] + b=k.B[m]*spb[j]+cross*spa[j] + V[j, 4column - 3]=real(a) + V[j, 4column - 2]=imag(a) + V[j, 4column - 1]=real(b) + V[j, 4column]=imag(b) + end + end + mul!(matrix, U, transpose(V), 1.0, 1.0) + end + matrix +end + +ws_core_voltage(z, g) = 1-(g.Rleft+log(abs(z)/g.a)/g.epsilon)/g.Rtotal + +# Round wires retain their existing auxiliary-source treatment. The tape has +# no source contour or inset: its physical faces are handled in the included file. +function ws_points(g, nw, fraction; shift = 0.0) + targets, sources = ComplexF64[], ComplexF64[] + for wire in g.wires + center = complex(wire.at.x, wire.at.y) + for j in 0:(nw - 1) + push!(targets, center + wire.r*cis(2pi*(j+shift)/nw)) + push!(sources, center + fraction*wire.r*cis(2pi*j/nw)) + end + end + (; targets, sources) +end + +include(joinpath(@__DIR__, "prototype_tape_element.jl")) + +function ws_replace_internal(baseline, C, expected_previous) + f = frequencies(baseline) + corrected = copy(observe(baseline, Y)) + # Core and screen voltages and charges relative to the foil. Cancels both the + # external jacket interval and the full existing earth contribution. + H = [1.0 0.0; 0.0 1.0; -1.0 -1.0] + replacement = inv(C) + for (k, frequency) in pairs(f) + s = 2pi*im*frequency + P = s*inv(observe(baseline, Y)[:, :, k]) + updated = copy(P) + for start in (1, 4, 7) + indices = start:(start + 2) + previous = transpose(H)*P[indices, indices]*H + @test isapprox(inv(previous), expected_previous; rtol = 1e-7, atol = 1e-15) + updated[start:(start + 1), start:(start + 1)] += replacement-previous + end + corrected[:, :, k] = s*inv(updated) + foil_indices = [3, 6, 9] + @assert updated[foil_indices, :] == P[foil_indices, :] + @assert updated[:, foil_indices] == P[:, foil_indices] + for left in (1:3, 4:6, 7:9), right in (1:3, 4:6, 7:9) + + left == right && continue + @assert updated[left, right] == P[left, right] + end + end + LineParameters(PhaseDomain, copy(observe(baseline, Z)), corrected, f; basis = :pul) +end + +function ws_run(campaign, case_id, levels, source_fraction; + log_rtol = ws_log_rtol, penetration_target = ws_penetration_target) + ws_selfchecks() + ws_tape_selfchecks() + case_id == :cable_18kv_1000mm2_trefoil || error("This is a disposable 18 kV prototype.") + folder = joinpath(campaign, "benchmark_18kv_1000mm2_trefoil_fem") + state = TOML.parsefile(joinpath(folder, "state.toml")) + state["state"] == "complete" || error("Select a completed saved benchmark.") + attempt = joinpath(folder, state["current"]) + reference = ws_read_result(joinpath(attempt, "reference", "calculation.jld2")) + baseline = ws_read_result(joinpath(attempt, "candidate", "points", "1", "calculation.jld2")) + @assert frequencies(reference) == frequencies(baseline) + original = deepcopy((observe(baseline,Z),observe(baseline,Y),observe(reference,Y))) + loaded = Gauntlet.load_case(case_id) + geometry = ws_geometry(first(loaded.problem.system.designs)) + geometry_check = ws_exposed_geometry_check(geometry) + Cleft = 2pi*8.8541878128e-12/geometry.Rleft + Cright = 2pi*8.8541878128e-12/geometry.Rright + expected_previous = [Cleft -Cleft; -Cleft Cleft+Cright] + println("\nFour-face tape prototype: ",length(geometry.wires)," wires + finite open tape.") + println("Core remains an equivalent circle. Saved earth/Z retained. No FEM solve or campaign writes.") + println("Exposed geometry check: ",geometry_check) + println("Outer junction exponent: ",ws_junction_exponent(geometry.epsilon,first(geometry.right).epsilon)) + results = Any[] + convergence_df = DataFrame() + couplings(C) = (-C[1,2],sum(C[1,:]),sum(C[2,:])) + for level in levels + println("Solving ",level.label,": ",level) + elapsed = @elapsed result = ws_spectral_capacitance(geometry,level; + source_fraction,log_rtol,penetration_target) + ccs,cca,csa = couplings(result.C) + delta = isempty(results) ? (missing,missing,missing) : + 100 .* (([ccs,cca,csa]./collect(couplings(first(results).C))).-1) + push!(results,result) + push!(convergence_df,(refinement = level.label,wire_points = level.wire, + tape_order = level.order,quadrature_nodes = level.quadrature,modes = level.modes, + unknowns = result.unknowns,rank = result.rank, + C_cs_nF_m = ccs*1e9,C_ca_nF_m = cca*1e9,C_sa_nF_m = csa*1e9, + delta_cs_percent = delta[1],delta_ca_percent = delta[2],delta_sa_percent = delta[3], + boundary_residual_V = result.residual,wire_residual_V = result.wire_residual, + tape_residual_V = result.tape_residual, + penetration_residual_V = result.penetration_residual, + required_residual_V = result.required_residual, + sampled_penetration_indicator_percent = 100result.sampled_penetration_relative_indicator, + log_moment_error = result.log_moment_error, + log_integration_indicator_V = result.log_integration_indicator_V, + reciprocity = result.reciprocity,mutual_reciprocity = result.mutual_reciprocity, + condition = result.condition,seconds = elapsed);promote = true) + end + println("\nChanges are against the baseline row; all three couplings are tracked individually.") + show(stdout,MIME"text/plain"(),select(convergence_df, + [:refinement,:C_cs_nF_m,:C_ca_nF_m,:C_sa_nF_m,:delta_ca_percent, + :wire_residual_V,:tape_residual_V,:penetration_residual_V,:required_residual_V]);allcols = true) + println() + best = last(results) + prototype = ws_replace_internal(baseline,best.C,expected_previous) + @test eigmin(Symmetric((best.C+transpose(best.C))/2)) > 0 + @test best.mutual_reciprocity < 1e-3 + converged_boundary = best.penetration_residual <= best.required_residual + if !converged_boundary + @warn "Sampled boundary error still exceeds the core-to-foil coupling target." measured_V=best.penetration_residual required_V=best.required_residual + end + println("Sampled residuals are diagnostics, not a certified maximum; inspect the independent refinements.") + couplings_df = DataFrame() + for (label,value) in (("saved FEM",reference),("saved annular",baseline),("four-face tape",prototype)), + k in (1,length(frequencies(value))) + f = frequencies(value)[k] + coupling = -imag.(observe(value,Y)[:,:,k])/(2pi*f)*1e9 + push!(couplings_df,(model = label,frequency_Hz = f,core_screen = coupling[1,2], + core_foil = coupling[1,3],screen_foil = coupling[2,3])) + end + println("\nCouplings -B/omega [nF/m]; FEM values are apparent capacitances.") + show(stdout,MIME"text/plain"(),couplings_df;allcols = true);println() + summary_df = DataFrame() + for (label,value) in (("saved annular",baseline),("four-face tape",prototype)), + (term,i,j) in (("core-screen",1,2),("core-foil",1,3),("screen-foil",2,3)) + row = Dict{Symbol,Any}(:model=>label,:term=>term) + for band in (:all,:dc,:harmonic,:narrow,:wide) + comparison = Base.invokelatest(LineCableModels.Engine.compare,reference,value,B;band) + metric = comparison.relative[i,j] + row[band] = ismissing(metric) ? missing : 100metric + end + push!(summary_df,row;cols = :union) + end + select!(summary_df,[:model,:term,:all,:dc,:harmonic,:narrow,:wide]) + println("\nB relative RMS [%] against saved FEM. Missing: at least one series is near zero.") + show(stdout,MIME"text/plain"(),summary_df;allcols = true);println() + @test original == (observe(baseline,Z),observe(baseline,Y),observe(reference,Y)) + @test observe(prototype,Z) == observe(baseline,Z) + @test all(isfinite,observe(prototype,Y)) + (;reference,baseline,prototype,geometry,geometry_check,results,convergence_df, + couplings_df,summary_df,attempt,converged_boundary) +end + +# One BLAS thread. Restore the user's setting even if the prototype fails. +ws_previous_threads = BLAS.get_num_threads() +ws_experiment = try + BLAS.set_num_threads(1) + ws_run(ws_campaign, ws_case_id, ws_levels, ws_source_fraction) +finally + BLAS.set_num_threads(ws_previous_threads) +end +ws_convergence_df = ws_experiment.convergence_df +ws_summary_df = ws_experiment.summary_df +ws_couplings_df = ws_experiment.couplings_df +ws_result = ws_experiment.prototype +ws_strip_result = ws_result # The finite tape now uses the four-face charge expansion. +ws_reference = ws_experiment.reference +ws_baseline = ws_experiment.baseline +if ws_make_plots + @eval using GLMakie + # Before: a fabricated ParametricResult supplied plot labels. Now ordinary + # completed-result collections delegate through observation construction. + ws_patterns = [ws_baseline, ws_result] + ws_plots = LineCableModels.plot(ws_patterns; reference = ws_reference, + ydata = (G, B), layout = (3, 3), length_unit = :base, + # Before: reference-first labels. Now results precede the separate reference. + series_labels = ("Saved annular", "Four-face tape", "Saved FEM")) +end +nothing diff --git a/test/manual/pscad/run_pscad.jl b/test/manual/pscad/run_pscad.jl new file mode 100644 index 000000000..5812bc0df --- /dev/null +++ b/test/manual/pscad/run_pscad.jl @@ -0,0 +1,25 @@ +# Include from an IDE/REPL or run with +# `julia --project=. test/manual/pscad/run_pscad.jl`. +# Uses the active environment. Each execution starts a fresh PSCAD computation. +using LineCableModels + +# Edit this filename to use another station configuration. +config_path = joinpath(@__DIR__, "local-pscad.toml") +station = PSCAD.RemoteConfig(config_path) + +# 2 insulated copper wires, buried 1 m deep and spaced 1 m apart. +copper = Material(:conductor, 1.72e-8, 1.0) +dielectric = Material(:insulator, 1e14, 2.3) +design = build(CableDesign, "toy-insulated-wire", terminal(:core, + solid(copper, Disk(0.004)), insulation(dielectric; t=0.002))) +system = build(LineCableSystem, [design, design], [Pose2(0, -1), Pose2(1, -1)]; + connections=[Dict(:core => 1), Dict(:core => 2)]) +problem = LineParametersProblem(system; earth_props=homogeneous(rho=100.0), + frequencies=10.0 .^ range(log10(50.0), log10(1000.0); length=101)) +selected = Formulation(:pscad; earth_impedance=:wedepohl1973) + +@time result = compute(problem, selected; + options=(remote=station, verbosity=(default=0, PSCAD=1))) +Zresult = Z(result) # Series impedance in ohm/m: terminal × terminal × frequency. +Yresult = Y(result) # Shunt admittance in S/m, with the same ordering. +println("PSCAD run: ", details(result).data.execution.source_run) diff --git a/test/manual/verification/verify_getdp_artifact.jl b/test/manual/verification/verify_getdp_artifact.jl new file mode 100644 index 000000000..4bb32b631 --- /dev/null +++ b/test/manual/verification/verify_getdp_artifact.jl @@ -0,0 +1,39 @@ +using Downloads +using Pkg +using SHA +using TOML + +const ROOT = normpath(joinpath(@__DIR__, "..", "..", "..")) +const ARTIFACTS = TOML.parsefile(joinpath(ROOT, "Artifacts.toml"))["getdp"] +const REQUIRED_FILES = Dict( + "linux" => ( + "getdp-3.5.0-Linux64/bin/getdp", + "getdp-3.5.0-Linux64/share/doc/getdp/LICENSE.txt", + ), + "macos" => ( + "getdp-3.5.0-MacOSX/bin/getdp", + "getdp-3.5.0-MacOSX/share/doc/getdp/LICENSE.txt", + ), +) + +for binding in ARTIFACTS + download = only(binding["download"]) + mktempdir() do directory + archive = Downloads.download( + download["url"], joinpath(directory, basename(download["url"])), + ) + digest = bytes2hex(open(sha256, archive)) + digest == download["sha256"] || error( + "archive checksum mismatch for $(binding["os"]): $digest", + ) + unpacked = joinpath(directory, "unpacked") + Pkg.PlatformEngines.unpack(archive, unpacked) + tree = bytes2hex(Pkg.GitTools.tree_hash(unpacked)) + tree == binding["git-tree-sha1"] || error( + "artifact tree mismatch for $(binding["os"]): $tree", + ) + all(isfile(joinpath(unpacked, path)) for path in REQUIRED_FILES[binding["os"]]) || + error("GetDP executable or license is missing for $(binding["os"])") + println(binding["os"], " ", binding["arch"], ": verified ", tree) + end +end diff --git a/test/quality/aqua.jl b/test/quality/aqua.jl new file mode 100644 index 000000000..25e7e1bfd --- /dev/null +++ b/test/quality/aqua.jl @@ -0,0 +1,6 @@ +@testitem "Quality / Aqua / package hygiene" tags = [:aqua, :pscad] begin + using Aqua + # Pkg is imported by LineCableModelsGmshExt, so the root-module + # stale-dependency inspection cannot observe its use there. + Aqua.test_all(LineCableModels; stale_deps=(ignore=[:Pkg],)) +end diff --git a/test/quality/architecture.jl b/test/quality/architecture.jl new file mode 100644 index 000000000..215ec9099 --- /dev/null +++ b/test/quality/architecture.jl @@ -0,0 +1,2058 @@ +# Each guard takes its inputs as arguments. The controls below apply it to probe +# packages and probe sources. `test/tools/architecture_inventory.jl` evaluates +# this module to print the live inventory. +@testmodule ArchitectureGuards begin + using LineCableModels + using JuliaSyntax: JuliaSyntax, SyntaxNode, @K_str, kind, children, numchildren, + is_leaf, is_prefix_call + using TOML: TOML + import DataFrames, LinearAlgebra, Statistics, Random, Logging + # These packages load the extensions checked by `explicit_imports.jl`. + import Measurements, Distributions, Gmsh, Calculus, XLSX, CairoMakie + + # A module may reference only earlier modules, its ancestors and its + # descendants. The root module and package extensions are exempt. The load + # order is defined once, in the test taxonomy. + Base.include(@__MODULE__, joinpath(@__DIR__, "..", "support", "taxonomy.jl")) + const ORDER = keys(MODULE_OWNERS) + const EXTENSIONS = (:LineCableModelsMeasurementsExt, :LineCableModelsDistributionsExt, + :LineCableModelsGmshExt, :LineCableModelsXLSXExt, :LineCableModelsMakieExt, + :LineCableModelsCairoMakieExt) + # Base and the standard libraries whose exported names share a user's scope with + # the names that the root module exports. + const DEPENDENCIES = (Base, LinearAlgebra, Statistics, Random, + Base.require(LineCableModels, :Dates), Logging) + # Per-family formula selections are the only name shared across modules. + const SHARED_NAMES = (:Formula,) + const RESERVED_VERB = r"^_*(validate|check|require|assert|verify|ensure)_" + # Tables of the structural guards (A) and of the Commons and helper guards (C). + const A_TABLES = ("ownership", "placement", "direction", "names", "shadowing", + "validate", "reserved_verbs", "switches", "storage", "dispatch") + const C_TABLES = ("commons", "vocabulary", "fingerprints", "clones", "helpers", "root") + const TABLES = (A_TABLES..., C_TABLES...) + const SWITCHES = ("applicable", "eval", "kind") + const BASELINE = joinpath(@__DIR__, "architecture_baseline.toml") + + const COMMONS = :Commons + const COMMONS_DIRECTORY = "src/commons" + const CONSTANTS_FILE = "src/commons/consts.jl" + const COMMONS_TESTS = "test/unit/commons" + # Directories under `test/` whose files name definitions without testing them. + const NOT_TESTS = ("quality", "tools") + # Each Commons public name and the definition names it reserves outside Commons. + const VOCABULARY = Dict{Symbol, Regex}( + :vacuum_permittivity => + r"(?i)^_*(\w+_)?(vacuum_permittivity|eps(ilon)?_?[0₀]|[εϵ]_?[0₀])$", + :vacuum_permeability => + r"(?i)^_*(\w+_)?(vacuum_permeability|mu_?[0₀]|[μµ]_?[0₀])$", + :ideal_transposition! => r"transpos", + :kron_reduce! => r"kron", + :merge_bundles! => r"merge_bundle", + :bundle_operations => r"^_*bundle_(operations|pairs)$", + :reorder_indices => r"^_*reorder(_|$)", + :ReductionPlan => r"(?i)^_*reduction_?(plan|map)$", + :ReductionBuffers => r"(?i)^_*reduction_?buffers$", + :reduce_line_matrices! => r"^_*reduce_(line|primitive)_matrices") + const SHINGLE = 8 + const CLONE_TOKENS = 30 + const CLONE_SHARE = 0.75 + + # The core modules, the loaded extension modules and the repository + # directory that holds `src/` and `ext/`. + struct PackageTree + root::Module + core::Set{Module} + extensions::Set{Module} + modules::Set{Module} + directory::String + end + + function submodules(m::Module, found = Module[]) + push!(found, m) + for name in names(m; all = true) + isdefined(m, name) || continue + value = getfield(m, name) + value isa Module && value !== m && parentmodule(value) === m && + submodules(value, found) + end + return found + end + + function PackageTree(root::Module, extensions, directory::AbstractString) + core = Set(submodules(root)) + loaded = Set{Module}(Iterators.flatten(submodules(e) for e in extensions)) + return PackageTree(root, core, loaded, union(core, loaded), normpath(directory)) + end + + function is_descendant(m::Module, ancestor::Module) + while true + m === ancestor && return true + parentmodule(m) === m && return false + m = parentmodule(m) + end + end + + # For the root module, a descendant does not count as its own. + within(m, owner::Module, root::Module) = + m isa Module && (m === owner || (owner !== root && is_descendant(m, owner))) + + function module_name(tree::PackageTree, m::Module) + m === tree.root && return string(nameof(m)) + path, prefix = fullname(m), fullname(tree.root) + length(path) > length(prefix) && path[1:length(prefix)] == prefix && + return join(path[length(prefix)+1:end], ".") + return join(path, ".") + end + + inside(path, directory) = startswith(path, joinpath(directory, "")) + + # Methods generated in `boot.jl` and similar report a relative file. + function source_path(tree::PackageTree, file) + path = string(file) + isabspath(path) || return nothing + path = normpath(path) + return inside(path, tree.directory) ? relative(path, tree.directory) : nothing + end + source_name(tree::PackageTree, m::Method) = + something(source_path(tree, m.file), string(m.file)) + + function package_methods(tree::PackageTree) + found = Method[] + Base.visit(Core.methodtable) do m + m.module in tree.modules && push!(found, m) + end + return found + end + + function source_files(directory::AbstractString) + files = String[] + for base in ("src", "ext") + append!(files, filter(endswith(".jl"), repository_files(joinpath(directory, base)))) + end + return sort!(files) + end + + count!(found, key) = (found[key] = get(found, key, 0) + 1; found) + + type_owner(T) = (T = Base.unwrap_unionall(T); T isa DataType ? parentmodule(T) : nothing) + value_owner(value::Module) = value + value_owner(value::Function) = parentmodule(typeof(value)) + value_owner(value::Type) = type_owner(value) + value_owner(_) = nothing + + # A constructor belongs to the owner of the constructed type. + function function_owner(m::Method) + F = Base.unwrap_unionall(m.sig).parameters[1] + F isa TypeVar && (F = F.ub) + F = Base.unwrap_unionall(F) + F isa DataType || return nothing + if F.name === Type.body.name + T = F.parameters[1] + return type_owner(T isa TypeVar ? T.ub : T) + end + return parentmodule(F) + end + + function mentioned!(found::Base.IdSet{DataType}, @nospecialize(T)) + if T isa TypeVar + mentioned!(found, T.lb) + mentioned!(found, T.ub) + elseif T isa UnionAll + mentioned!(found, T.var) + mentioned!(found, T.body) + elseif T isa Union + mentioned!(found, T.a) + mentioned!(found, T.b) + elseif T isa Core.TypeofVararg + isdefined(T, :T) && mentioned!(found, T.T) + elseif T isa DataType && T ∉ found + push!(found, T) + for p in T.parameters + mentioned!(found, p) + end + end + return found + end + + # Ownership. A core method that extends a function owned by another package + # module mentions a type owned by its own module or a descendant. + function ownership(methods, tree::PackageTree) + found = Dict{String, Int}() + for m in methods + M = m.module + M in tree.core || continue + startswith(string(m.name), "#") && continue + F = function_owner(m) + (F isa Module && F in tree.modules) || continue + within(F, M, tree.root) && continue + types = Base.IdSet{DataType}() + foreach(p -> mentioned!(types, p), Base.unwrap_unionall(m.sig).parameters[2:end]) + any(T -> within(parentmodule(T), M, tree.root), types) && continue + count!(found, string(module_name(tree, M), " | ", module_name(tree, F), ".", + m.name, " | ", source_name(tree, m))) + end + return found + end + + # Each directory holding a module's `.jl` is that module's home. + function home_directories(tree::PackageTree) + files = source_files(tree.directory) + homes = Dict{String, Vector{Module}}() + for M in tree.modules + name = string(nameof(M)) + declaration = Regex("(?m)^\\s*(?:bare)?module\\s+" * name * "\\b") + candidates = filter(files) do file + basename(file) == name * ".jl" && occursin(declaration, read(file, String)) + end + # A module declared inline in another file has no home of its own. + isempty(candidates) && continue + length(candidates) == 1 || + error("Ambiguous home directory for $M: $(join(candidates, ", "))") + push!(get!(homes, dirname(only(candidates)), Module[]), M) + end + return homes + end + + function nearest_home(homes, file) + nearest = nothing + for directory in keys(homes) + inside(file, directory) || continue + (nearest === nothing || length(directory) > length(nearest)) && + (nearest = directory) + end + return nearest + end + + # Placement. No core method is defined under `ext/`. The nearest home directory + # around a method's file belongs to its module or one of its ancestors. + function placement(methods, tree::PackageTree, homes) + found = Dict{String, Int}() + extensions = joinpath(tree.directory, "ext") + for m in methods + M = m.module + path = source_path(tree, m.file) + path === nothing && continue + file = normpath(string(m.file)) + misplaced = M in tree.core && inside(file, extensions) + nearest = nearest_home(homes, file) + misplaced |= nearest === nothing || + !any(owner -> is_descendant(M, owner), homes[nearest]) + misplaced && count!(found, string(path, " | ", module_name(tree, M))) + end + return found + end + + function layer(tree::PackageTree, order, m::Module) + m === tree.root && return nothing + while parentmodule(m) !== tree.root + parentmodule(m) === m && return nothing + m = parentmodule(m) + end + index = findfirst(==(nameof(m)), order) + index === nothing && error("Module $m is missing from the declared module order") + return index + end + + lowered_code(m::Method) = isdefined(m, :source) && m.source !== nothing && + !isdefined(m, :generator) ? Base.uncompressed_ir(m).code : nothing + + # Every value that a lowered body names through a global reference or a + # `getproperty(module, :name)` chain of SSA values. + function referenced_values(code) + resolved = Dict{Int, Any}() + found = Any[] + resolve(x::GlobalRef) = isdefined(x.mod, x.name) ? getglobal(x.mod, x.name) : nothing + resolve(x::Core.SSAValue) = get(resolved, x.id, nothing) + resolve(_) = nothing + function references!(x) + if x isa GlobalRef + value = resolve(x) + value === nothing || push!(found, value) + elseif x isa Expr + foreach(references!, x.args) + end + end + for (i, statement) in enumerate(code) + references!(statement) + value = statement isa GlobalRef ? resolve(statement) : nothing + if statement isa Expr && statement.head === :call && length(statement.args) == 3 && + resolve(statement.args[1]) === getproperty && + statement.args[3] isa QuoteNode + parent = resolve(statement.args[2]) + name = statement.args[3].value + parent isa Module && name isa Symbol && isdefined(parent, name) && + (value = getglobal(parent, name)) + value === nothing || push!(found, value) + end + value === nothing || (resolved[i] = value) + end + return found + end + + # Direction. No method body of a core submodule references a value owned by a + # module in a later position of `order`. + function direction(methods, tree::PackageTree, order) + found = Dict{String, Int}() + for m in methods + M = m.module + M in tree.core || continue + L = layer(tree, order, M) + L === nothing && continue + code = lowered_code(m) + code === nothing && continue + targets = Set{Module}() + for value in referenced_values(code) + owner = value_owner(value) + (owner isa Module && owner in tree.core) || continue + target = layer(tree, order, owner) + target !== nothing && target > L && push!(targets, owner) + end + for owner in targets + count!(found, string(module_name(tree, M), " -> ", module_name(tree, owner), + " | ", source_name(tree, m))) + end + end + return found + end + + # Names. Distinct functions or types, owned by different package modules, + # that share a name. The value is the number of modules that define the name. + function shared_names(tree::PackageTree; exceptions = SHARED_NAMES) + owners = Dict{Symbol, Vector{Module}}() + for M in tree.modules, name in names(M; all = true) + (startswith(string(name), "#") || name in (:eval, :include) || + name === nameof(M)) && continue + isdefined(M, name) || continue + value = getfield(M, name) + (value isa Function || value isa Type) && value_owner(value) === M || continue + push!(get!(owners, name, Module[]), M) + end + return Dict{String, Int}(string(name) => length(modules) + for (name, modules) in owners if length(modules) > 1 && name ∉ exceptions) + end + + # Shadowing. A package-owned name that the root module exports, and so `using` + # brings into scope, and that a dependency exports for a different object. A name + # that only a submodule exports stays in that namespace. + function shadowing(tree::PackageTree, dependencies) + found = Dict{String, Int}() + for name in names(tree.root) + Base.isexported(tree.root, name) && isdefined(tree.root, name) || continue + value = getfield(tree.root, name) + value isa Function || value isa Type || continue + owner = value_owner(value) + (owner isa Module && owner in tree.modules) || continue + for dependency in dependencies + Base.isexported(dependency, name) && isdefined(dependency, name) && + getfield(dependency, name) !== value || continue + found[string(module_name(tree, owner), ".", name, " -> ", + nameof(dependency))] = 1 + end + end + return found + end + + # Source spelling only. No binding is resolved. + function syntax_name(node) + kind(node) in (K"Identifier", K"MacroName") && return (node.val,) + if kind(node) == K"." && numchildren(node) == 2 + member = node[2] + kind(member) == K"quote" && numchildren(member) == 1 && (member = member[1]) + left, right = syntax_name(node[1]), syntax_name(member) + !isempty(left) && length(right) == 1 && return (left..., right...) + end + return () + end + + # Calls `f` on every node. A `false` result skips the children of that node. + function visit(f, node) + f(node) === false && return + is_leaf(node) || foreach(child -> visit(f, child), children(node)) + return + end + + parse_source(file) = JuliaSyntax.parseall(SyntaxNode, read(file, String); filename = file) + last_child(node) = children(node)[end] + + # The call of a method definition, without `where` and return annotations. + function signature_call(node) + kind(node) == K"function" && numchildren(node) == 2 || return nothing + signature = node[1] + while kind(signature) in (K"where", K"::") && numchildren(signature) == 2 + signature = signature[1] + end + return kind(signature) == K"call" && is_prefix_call(signature) ? signature : nothing + end + + function definition_name(node) + kind(node) == K"function" || return nothing + call = signature_call(node) + head = call !== nothing ? call[1] : numchildren(node) == 1 ? node[1] : nothing + head === nothing && return nothing + name = syntax_name(head) + return isempty(name) ? nothing : last(name) + end + + function first_argument(call) + arguments = [a for a in children(call)[2:end] if kind(a) != K"parameters"] + isempty(arguments) && return nothing + argument = arguments[1] + kind(argument) == K"=" && numchildren(argument) == 2 && (argument = argument[1]) + kind(argument) == K"::" && numchildren(argument) == 2 && (argument = argument[1]) + return kind(argument) == K"Identifier" ? argument.val : nothing + end + + const THROWS = ((:throw,), (:rethrow,), (:error,), (:Base, :throw), (:Core, :throw), + (:Base, :rethrow), (:Base, :error)) + + # Whether an expression in tail position yields `name` or throws. + function returns_subject(node, name) + k = kind(node) + k == K"Identifier" && return node.val === name + k in (K"block", K"let", K"parens") && + return numchildren(node) > 0 && returns_subject(last_child(node), name) + k == K"return" && return numchildren(node) == 1 && returns_subject(node[1], name) + k in (K"if", K"elseif", K"?") && return numchildren(node) == 3 && + returns_subject(node[2], name) && returns_subject(node[3], name) + if k == K"try" + clauses = Dict(kind(c) => c for c in children(node)[2:end]) + value = haskey(clauses, K"else") ? last_child(clauses[K"else"]) : node[1] + return returns_subject(value, name) && (!haskey(clauses, K"catch") || + returns_subject(last_child(clauses[K"catch"]), name)) + end + return k == K"call" && is_prefix_call(node) && syntax_name(node[1]) in THROWS + end + + # Every `return` of this method, outside nested functions and quotations. + function early_returns_subject(node, name) + is_leaf(node) && return true + for child in children(node) + kind(child) in (K"function", K"->", K"do", K"macro", K"quote") && continue + kind(child) == K"return" && !returns_subject(child, name) && return false + early_returns_subject(child, name) || return false + end + return true + end + + # The occurrences of `name` that a body returns: its tail values and the values of + # its `return` statements outside nested functions. + function returned!(found, node, name) + k = kind(node) + if k == K"Identifier" + node.val === name && push!(found, node) + elseif k in (K"block", K"let", K"parens") + numchildren(node) > 0 && returned!(found, last_child(node), name) + elseif k == K"return" + numchildren(node) == 1 && returned!(found, node[1], name) + elseif k in (K"if", K"elseif", K"?") + foreach(child -> returned!(found, child, name), children(node)[2:end]) + elseif k == K"try" + foreach(child -> returned!(found, child, name), children(node)) + end + return found + end + + function early_returned!(found, node, name) + is_leaf(node) && return found + for child in children(node) + kind(child) in (K"function", K"->", K"do", K"macro", K"quote") && continue + kind(child) == K"return" && returned!(found, child, name) + early_returned!(found, child, name) + end + return found + end + + # Whether a body uses `name` other than by returning it. A body that only returns + # `name` is an identity method: its consumer does not restrict the subject. + function uses_subject(body, name) + kind(body) == K"Identifier" && return true + kind(body) == K"block" && numchildren(body) == 1 && + kind(body[1]) in (K"Identifier", K"return") && return true + found = early_returned!(returned!(Base.IdSet{SyntaxNode}(), body, name), body, name) + used = false + visit(body) do node + kind(node) == K"Identifier" && node.val === name && node ∉ found && (used = true) + return !used + end + return used + end + + function validate_returns_subject(node) + name = first_argument(signature_call(node)) + name === nothing && return false + body = node[2] + return returns_subject(body, name) && early_returns_subject(body, name) && + uses_subject(body, name) + end + + is_required_block(node) = kind(node) == K"macrocall" && numchildren(node) > 0 && + syntax_name(node[1]) in ((Symbol("@required"),), + (:RequiredInterfaces, Symbol("@required"))) + + # validate returns its subject. Every `validate` method names its first positional argument, + # returns it on every path, or throws, and uses it in its work unless it only returns it. + function validate_returns(files, directory) + found = Dict{String, Int}() + for file in files + visit(parse_source(file)) do node + is_required_block(node) && return false + signature_call(node) === nothing && return true + definition_name(node) === :validate || return true + validate_returns_subject(node) || count!(found, relative(file, directory)) + return true + end + end + return found + end + + # Reserved verbs. No function name spells an input check with a reserved verb prefix. + function reserved_verbs(files, directory) + found = Dict{String, Int}() + for file in files + visit(parse_source(file)) do node + name = definition_name(node) + name isa Symbol && occursin(RESERVED_VERB, string(name)) && + count!(found, string(relative(file, directory), " | ", name)) + return true + end + end + return found + end + + symbol_literal(node) = kind(node) == K"quote" && numchildren(node) == 1 && + kind(node[1]) == K"Identifier" + symbol_operand(node) = symbol_literal(node) || + (kind(node) in (K"tuple", K"vect") && numchildren(node) > 0 && + all(symbol_literal, children(node))) + + function kind_field(node) + kind(node) == K"." && numchildren(node) == 2 || return false + member = node[2] + kind(member) == K"quote" && numchildren(member) == 1 && (member = member[1]) + return kind(member) == K"Identifier" && member.val === :kind + end + + # Negated comparisons count too, so negating a switch does not hide it. + const KIND_COMPARISONS = ((:(===),), (:(==),), (:in,), (:∈,), + (:(!==),), (:(!=),), (:∉,)) + + function kind_switch(node) + kind(node) == K"call" && numchildren(node) == 3 || return false + operator, left, right = is_prefix_call(node) ? + (node[1], node[2], node[3]) : (node[2], node[1], node[3]) + return syntax_name(operator) in KIND_COMPARISONS && + kind_field(left) && symbol_operand(right) + end + + # Symbol switches and probes. Per file, calls to `applicable`, `@eval` calls and comparisons of a + # `kind` field against symbols, including negated comparisons. + function switches(files, directory) + found = Dict{String, Dict{String, Int}}() + for file in files + counts = Dict(name => 0 for name in SWITCHES) + visit(parse_source(file)) do node + k = kind(node) + if k == K"call" && is_prefix_call(node) && syntax_name(node[1]) in + ((:applicable,), (:Base, :applicable), (:Core, :applicable)) + counts["applicable"] += 1 + elseif k == K"macrocall" && numchildren(node) > 0 && syntax_name(node[1]) in + ((Symbol("@eval"),), (:Base, Symbol("@eval")), (:Core, Symbol("@eval"))) + counts["eval"] += 1 + end + kind_switch(node) && (counts["kind"] += 1) + return true + end + any(>(0), values(counts)) && (found[relative(file, directory)] = counts) + end + return found + end + + const WORKSPACE_FIELDS = (:input, :plan, :buffers) + const STORAGE_NAMES = (:work, :scratch, :storage, :cache) + const STORAGE_TYPE = r"(Buffer|Storage|Scratch|Cache)$" + const STORAGE_FUNCTION = r"_(workspace|storage|buffers)!?$" + + # The name of a struct, abstract or primitive type definition. + function declared_name(node) + head = node[1] + while kind(head) in (K"<:", K"curly") && numchildren(head) > 0 + head = head[1] + end + return kind(head) == K"Identifier" ? head.val : nothing + end + + # The names that a parameter, an assignment target or a loop variable binds. + function bound_names(node) + k = kind(node) + k == K"Identifier" && return [node.val] + (k == K"::" && numchildren(node) == 2 || k in (K"=", K"...")) && + return bound_names(node[1]) + k in (K"tuple", K"parameters") && + return reduce(vcat, map(bound_names, children(node)); init = Symbol[]) + return Symbol[] + end + + # The field that one entry of a struct body declares, or nothing. + function field_name(node) + k = kind(node) + k == K"Identifier" && return node.val + k == K"doc" && return field_name(last_child(node)) + k in (K"::", K"const", K"=") && numchildren(node) > 0 && return field_name(node[1]) + return nothing + end + + # The keys of a named tuple literal. + function tuple_keys(node) + keys = Symbol[] + for child in children(node) + if kind(child) == K"=" && kind(child[1]) == K"Identifier" + push!(keys, child[1].val) + elseif kind(child) == K"parameters" + for entry in children(child) + kind(entry) == K"Identifier" && push!(keys, entry.val) + kind(entry) == K"=" && kind(entry[1]) == K"Identifier" && + push!(keys, entry[1].val) + end + end + end + return keys + end + + in_struct_body(node) = node.parent !== nothing && kind(node.parent) == K"block" && + node.parent.parent !== nothing && kind(node.parent.parent) == K"struct" + + # Storage vocabulary. A `*Workspace` struct has exactly the fields `input`, `plan` and + # `buffers`, and optionally `trace`. Its entry counts the missing and the other fields. + # No function name ends in `_workspace` or `_storage`, and only `initialize_buffers` + # ends in `_buffers`. Only Commons defines a `*Buffers` struct, and no type name ends + # in `Buffer`, `Storage`, `Scratch` or `Cache`. No field, named tuple key, parameter, + # local or loop variable takes the name `work`, `scratch`, `storage` or `cache`. + function storage_vocabulary(files, directory) + found = Dict{String, Int}() + commons = joinpath(directory, COMMONS_DIRECTORY) + for file in files + path = relative(file, directory) + entry(name, n = 1) = n > 0 && (found[string(path, " | ", name)] = + get(found, string(path, " | ", name), 0) + n) + names!(names) = foreach(name -> name in STORAGE_NAMES && entry(name), names) + visit(parse_source(file)) do node + k = kind(node) + if k == K"struct" + name = declared_name(node) + name === nothing && return true + text = string(name) + fields = filter(!isnothing, map(field_name, children(node[2]))) + names!(fields) + if endswith(text, "Workspace") + entry(name, count(!in(fields), WORKSPACE_FIELDS) + + count(!in((WORKSPACE_FIELDS..., :trace)), fields)) + end + endswith(text, "Buffers") && !inside(file, commons) && entry(name) + occursin(STORAGE_TYPE, text) && entry(name) + elseif k in (K"abstract", K"primitive") + name = declared_name(node) + name !== nothing && occursin(STORAGE_TYPE, string(name)) && entry(name) + elseif k == K"function" + name = definition_name(node) + name isa Symbol && name !== :initialize_buffers && + occursin(STORAGE_FUNCTION, string(name)) && entry(name) + call = signature_call(node) + call === nothing || names!(reduce(vcat, map(bound_names, + children(call)[2:end]); init = Symbol[])) + elseif k in (K"->", K"do") && numchildren(node) > 0 + names!(bound_names(node[1])) + elseif k == K"in" && node.parent !== nothing && + kind(node.parent) == K"iteration" + names!(bound_names(node[1])) + elseif k == K"tuple" && + !(node.parent !== nothing && kind(node.parent) in (K"->", K"do")) + names!(tuple_keys(node)) + elseif k == K"=" && numchildren(node) == 2 && !in_struct_body(node) && + !(node.parent !== nothing && + kind(node.parent) in (NOT_ASSIGNED..., K"const")) + names!(assigned_names(node[1])) + elseif k == K"local" + for child in children(node) + kind(child) == K"Identifier" && names!([child.val]) + end + end + return true + end + end + return found + end + + # The types that the first argument of `m` admits: the members of a `Union`, each + # bound again by the type variables of `m`. A constructor or a method without + # arguments admits none. + function first_argument(m::Method) + parameters = Base.unwrap_unionall(m.sig).parameters + length(parameters) >= 2 || return Any[] + F = parameters[1] isa TypeVar ? parameters[1].ub : parameters[1] + F = Base.unwrap_unionall(F) + F isa DataType && F.name === Type.body.name && return Any[] + found = Any[] + members!(T) = T isa Union ? (members!(T.a); members!(T.b)) : + T isa TypeVar ? members!(T.ub) : + T isa Core.TypeofVararg ? nothing : push!(found, T) + members!(parameters[2]) + return [Base.rewrap_unionall(T, m.sig) for T in found] + end + + # The tag of a `Val` type, or nothing for another type. + function tag(T) + T = Base.unwrap_unionall(T) + return T isa DataType && T.name === Val.body.name ? only(T.parameters) : nothing + end + is_val(T) = (T = Base.unwrap_unionall(T); T isa DataType && T.name === Val.body.name) + is_formula(T) = T !== Union{} && T isa Type && T <: AbstractFormulation + + # The identifiers that the formula families of `tree` register through `formulas`. + function registered(methods) + registry = typeof(LineCableModels.Commons.formulas) + found = Set{Symbol}() + for m in methods + parameters = Base.unwrap_unionall(m.sig).parameters + length(parameters) == 2 && parameters[1] === registry || continue + family = Base.unwrap_unionall(parameters[2]) + family isa DataType && family.name === Type.body.name || continue + F = only(family.parameters) + F isa TypeVar && (F = F.ub) + union!(found, Base.invokelatest(LineCableModels.Commons.formulas, F)) + end + return found + end + + # Dispatch convention. A formula method takes the formula object first, and its `Val` + # tags follow it. A function with a method whose first argument is a formula, or a + # `Union` that contains one, has no method whose first argument is a `Val`, or a `Union` + # that contains one. In any function, a first argument `Val{ID}`, or a `Union` that + # contains one, where `ID` is a registered formula identifier, passes the formula as its + # tag. Type constructors are exempt. An entry counts the methods of one function in one + # file. + function dispatch(methods, tree::PackageTree) + identifiers = registered(methods) + functions = Base.IdSet{Any}() + for m in methods + any(is_formula, first_argument(m)) && + push!(functions, Base.unwrap_unionall(m.sig).parameters[1]) + end + found = Dict{String, Int}() + for m in methods + startswith(string(m.name), "#") && continue + admitted = first_argument(m) + examined = Base.unwrap_unionall(m.sig).parameters[1] in functions && + any(is_val, admitted) + tagged = any(T -> tag(T) in identifiers, admitted) + (examined || tagged) && count!(found, string(module_name(tree, m.module), + " | ", m.name, " | ", source_name(tree, m))) + end + return found + end + + type_name(T) = (T = Base.unwrap_unionall(T); T isa DataType ? nameof(T) : nothing) + + # The functions, types and constants that a module defines, by name. + function definitions(M::Module) + found = Dict{Symbol, Any}() + for name in names(M; all = true) + (startswith(string(name), "#") || name in (:eval, :include) || + name === nameof(M)) && continue + isdefined(M, name) || continue + value = getfield(M, name) + value isa Module && continue + owner = value isa Function ? value_owner(value) : + value isa Type && type_name(value) === name ? type_owner(value) : + isconst(M, name) ? Base.binding_module(M, name) : nothing + owner === M && (found[name] = value) + end + return found + end + + # The function or type whose method `m` is. A keyword sorter belongs to its + # function, and a method of a nested function to the type of that function. + function method_subject(m::Method) + signature = Base.unwrap_unionall(m.sig).parameters + F = Base.unwrap_unionall(signature[1]) + F isa DataType || return nothing + F === typeof(Core.kwcall) && length(signature) >= 3 && + (F = Base.unwrap_unionall(signature[3]); F isa DataType || return nothing) + if F.name === Type.body.name + T = F.parameters[1] + T = Base.unwrap_unionall(T isa TypeVar ? T.ub : T) + return T isa DataType ? T.name.wrapper : nothing + end + return isdefined(F, :instance) ? F.instance : F.name.wrapper + end + + # The root module, a top-level core submodule or a top-level extension module. + function top_owner(tree::PackageTree, m::Module) + while m !== tree.root && parentmodule(m) !== m && parentmodule(m) !== tree.root && + parentmodule(m) in tree.modules + m = parentmodule(m) + end + return m + end + + # The texts of the checkout's `.jl` files under `directory`, except those under + # `excluded` subdirectories. + function test_sources(directory; excluded = ()) + texts = String[] + for file in repository_files(directory) + endswith(file, ".jl") && + !any(x -> inside(file, joinpath(directory, x)), excluded) && + push!(texts, read(file, String)) + end + return texts + end + + named(texts, name) = (pattern = Regex("(? occursin(pattern, text), texts)) + + commons_module(tree::PackageTree) = (C = isdefined(tree.root, COMMONS) ? + getfield(tree.root, COMMONS) : nothing; C isa Module ? C : nothing) + + # Hooks are Commons functions with a method defined outside Commons. The + # user API is the Commons names that the root module exports or declares public. + function commons_roles(methods, tree::PackageTree, C::Module, defined) + functions = IdDict{Any, Symbol}(value => name for (name, value) in defined + if value isa Function) + hooks = Set{Symbol}() + for m in methods + within(m.module, C, tree.root) && continue + name = get(functions, method_subject(m), nothing) + name === nothing || push!(hooks, name) + end + api = Set(name for (name, value) in defined if Base.ispublic(tree.root, name) && + isdefined(tree.root, name) && getfield(tree.root, name) === value) + return hooks, api + end + + # Commons admission. Each Commons name is public, documented and named in a Commons unit + # test. Unless it is user API, methods of at least two owners outside + # Commons use it, directly or through public Commons definitions. Unless it + # is a hook or user API, it has a vocabulary entry. Macros are used where + # source code calls them. + function commons_admission(methods, tree::PackageTree, homes, vocabulary) + found = Dict{String, Int}() + C = commons_module(tree) + C === nothing && return found + defined = definitions(C) + hooks, api = commons_roles(methods, tree, C, defined) + name_of = IdDict{Any, Symbol}(value => name for (name, value) in defined) + # Owners of every Commons value, nested functions and keyword bodies included. + owners = IdDict{Any, Set{Module}}(value => Set{Module}() for value in values(defined)) + # The Commons values that each Commons method subject references. + references = IdDict{Any, Base.IdSet{Any}}() + unnamed(value) = !haskey(name_of, value) && (value isa Function && + within(parentmodule(typeof(value)), C, tree.root) || + value isa Type && value <: Function && within(parentmodule(value), C, tree.root)) + function use!(M::Module, @nospecialize(subject), @nospecialize(value)) + haskey(name_of, value) || unnamed(value) || return + if within(M, C, tree.root) + subject === nothing || subject === value || + push!(get!(Base.IdSet{Any}, references, subject), value) + elseif haskey(name_of, value) + push!(owners[value], top_owner(tree, M)) + end + end + # Loops instead of anonymous functions: an anonymous function compiles once + # per type it receives. + for m in methods + subject = method_subject(m) + use!(m.module, subject, subject) + types = Base.IdSet{DataType}() + for p in Base.unwrap_unionall(m.sig).parameters[2:end] + mentioned!(types, p) + end + for T in types + use!(m.module, subject, T.name.wrapper) + end + code = lowered_code(m) + code === nothing && continue + for value in referenced_values(code) + use!(m.module, subject, value) + end + end + # Supertypes and field types of every package type. + for M in tree.modules, value in values(definitions(M)) + T = value isa Type ? Base.unwrap_unionall(value) : nothing + T isa DataType || continue + related = Base.IdSet{DataType}() + S = supertype(T) + while S !== Any + push!(related, S) + S = supertype(S) + end + if isstructtype(T) + for F in Base.datatype_fieldtypes(T) + mentioned!(related, F) + end + end + for S in related + use!(M, value, S.name.wrapper) + end + end + macros = Set(name for name in keys(defined) if startswith(string(name), "@")) + for file in source_files(tree.directory) + isempty(macros) && break + home = nearest_home(homes, file) + home === nothing && continue + M = first(homes[home]) + visit(parse_source(file)) do node + kind(node) == K"macrocall" && numchildren(node) > 0 || return true + name = syntax_name(node[1]) + !isempty(name) && last(name) in macros && use!(M, nothing, defined[last(name)]) + return true + end + end + public = Set(name for name in keys(defined) if Base.ispublic(C, name)) + # Public definitions pass their owners to what they use, also through + # their nested functions and keyword bodies. Private definitions pass none. + changed = true + while changed + changed = false + for (subject, targets) in references + haskey(name_of, subject) && name_of[subject] ∉ public && continue + from = get!(Set{Module}, owners, subject) + for target in targets + to = get!(Set{Module}, owners, target) + issubset(from, to) && continue + union!(to, from) + changed = true + end + end + end + users = Dict(name => owners[value] for (name, value) in defined) + tests = test_sources(joinpath(tree.directory, COMMONS_TESTS)) + for name in keys(defined) + for (criterion, met) in (("public", name in public), + ("docstring", Base.Docs.hasdoc(C, name)), + ("owners", name in api || length(users[name]) >= 2), + ("tests", named(tests, name)), + ("vocabulary", name in hooks || name in api || haskey(vocabulary, name))) + met || (found[string(name, " | ", criterion)] = 1) + end + end + return found + end + + function reserving(vocabulary, name) + for (owner, pattern) in vocabulary + occursin(pattern, string(name)) && return owner + end + return nothing + end + + function calls(node, owner) + kind(node) == K"call" && is_prefix_call(node) || return false + name = syntax_name(node[1]) + return name == (owner,) || (length(name) >= 2 && name[end-1:end] == (COMMONS, owner)) + end + + # `=` under these kinds binds a keyword, a default or a field. + const NOT_ASSIGNED = (K"call", K"dotcall", K"parameters", K"tuple", K"vect", K"braces", + K"curly", K"ref") + + function assigned_names(target) + kind(target) == K"Identifier" && return [target.val] + kind(target) == K"::" && numchildren(target) == 2 && return assigned_names(target[1]) + kind(target) == K"tuple" && return reduce(vcat, map(assigned_names, children(target)); + init = Symbol[]) + return Symbol[] + end + + # Reserved vocabulary. Outside Commons, no function, constant or assigned local takes a + # name reserved by a Commons definition. A local assigned from a call to + # the reserving definition is exempt. + function reserved_vocabulary(files, directory, vocabulary) + found = Dict{String, Int}() + commons = joinpath(directory, COMMONS_DIRECTORY) + for file in files + inside(file, commons) && continue + path = relative(file, directory) + visit(parse_source(file)) do node + if kind(node) == K"function" + name = definition_name(node) + name isa Symbol && reserving(vocabulary, name) !== nothing && + count!(found, string(path, " | ", name)) + elseif kind(node) == K"=" && numchildren(node) == 2 && + !(node.parent !== nothing && kind(node.parent) in NOT_ASSIGNED) + for name in assigned_names(node[1]) + owner = reserving(vocabulary, name) + owner === nothing || calls(node[2], owner) || + count!(found, string(path, " | ", name)) + end + end + return true + end + end + return found + end + + any_node(predicate, node) = predicate(node) || + (!is_leaf(node) && any(child -> any_node(predicate, child), children(node))) + + # The innermost statement around a node. + function statement(node) + while node.parent !== nothing && !(kind(node.parent) in (K"block", K"toplevel")) + node = node.parent + end + return node + end + + is_pi(node) = kind(node) == K"Identifier" && node.val in (:π, :pi) + is_number(node) = kind(node) in (K"Integer", K"Float", K"Float32", K"HexInt", K"OctInt", + K"BinInt") + + # `base ^ -7` with a literal 10 in `base`. + function minus_seventh_power(node) + kind(node) == K"call" && numchildren(node) == 3 || return false + operator, base, exponent = is_prefix_call(node) ? + (node[1], node[2], node[3]) : (node[2], node[1], node[3]) + return syntax_name(operator) == (:^,) && kind(exponent) == K"Integer" && + exponent.val == -7 && any_node(n -> kind(n) == K"Integer" && n.val == 10, base) + end + + function fingerprint(node) + if is_number(node) + digits = filter(isdigit, JuliaSyntax.sourcetext(node)) + (occursin("8854187", digits) || occursin("299792458", digits)) && return true + value = node.val + value isa AbstractFloat && value == oftype(value, 1e-7) || return false + else + minus_seventh_power(node) || return false + end + return any_node(is_pi, statement(node)) + end + + # Literal fingerprints. Numeric fingerprints of the Commons constants appear only in the + # constants file: digits of ε₀ or c₀, and 10⁻⁷ in a statement with π. + function fingerprints(files, directory) + found = Dict{String, Int}() + for file in files + path = relative(file, directory) + path == CONSTANTS_FILE && continue + visit(parse_source(file)) do node + fingerprint(node) && count!(found, path) + return true + end + end + return found + end + + # A statement `x || throw(...)` or `x && throw(...)`, chains included. + function throw_guard(node) + kind(node) in (K"||", K"&&") || return false + while kind(node) in (K"||", K"&&") && numchildren(node) > 0 + node = last_child(node) + end + return kind(node) == K"call" && is_prefix_call(node) && syntax_name(node[1]) in THROWS + end + + # A body's tokens, without throw guards, with each identifier replaced by + # its role. + function body_tokens(body) + bytes = Vector{UInt8}(JuliaSyntax.sourcetext(body)) + offset = first(JuliaSyntax.byte_range(body)) - 1 + visit(body) do node + statement = node === body || + (node.parent !== nothing && kind(node.parent) in (K"block", K"toplevel")) + statement && throw_guard(node) || return true + bytes[JuliaSyntax.byte_range(node) .- offset] .= UInt8(' ') + return false + end + text = String(bytes) + raw = [(kind(t), JuliaSyntax.untokenize(t, text)) for t in JuliaSyntax.tokenize(text) + if !(kind(t) in (K"Whitespace", K"NewlineWs", K"Comment"))] + tokens = String[] + for (i, (k, token)) in enumerate(raw) + if k == K"Identifier" && !Base.isoperator(Symbol(token)) + previous = i > 1 ? raw[i-1][2] : "" + next = i < length(raw) ? raw[i+1][2] : "" + token = previous == "." ? "FIELD" : next == "(" ? "CALL" : "NAME" + end + push!(tokens, token) + end + return tokens + end + + shingles(tokens) = Set(join(view(tokens, i:i+SHINGLE-1), '\x1f') + for i in 1:length(tokens)-SHINGLE+1) + + function function_bodies(trees) + found = Tuple{String, Any, Any}[] + for (path, tree) in trees + visit(tree) do node + kind(node) == K"function" && numchildren(node) == 2 && + push!(found, (path, node, node[2])) + return true + end + end + return found + end + + # Clones of Commons bodies. No function body contains `CLONE_SHARE` of the token shingles of a + # Commons public function body of at least `CLONE_TOKENS` tokens. Hooks are + # not references. + function clones(files, methods, tree::PackageTree; share = CLONE_SHARE) + found = Dict{String, Int}() + C = commons_module(tree) + C === nothing && return found + hooks, _ = commons_roles(methods, tree, C, definitions(C)) + public = Set(name for name in names(C; all = true) + if Base.ispublic(C, name) && name ∉ hooks) + trees = [(relative(file, tree.directory), parse_source(file)) for file in files] + bodies = [(path, node, shingles(body_tokens(body))) + for (path, node, body) in function_bodies(trees)] + commons = COMMONS_DIRECTORY * "/" + for (path, reference, body) in function_bodies(trees) + startswith(path, commons) && definition_name(reference) in public || continue + tokens = body_tokens(body) + length(tokens) >= CLONE_TOKENS || continue + expected = shingles(tokens) + for (candidate_path, candidate, candidate_shingles) in bodies + candidate === reference && continue + count(in(candidate_shingles), expected) >= share * length(expected) && + count!(found, string(candidate_path, " | ", + something(definition_name(candidate), "anonymous"), " ~ ", + definition_name(reference))) + end + end + return found + end + + function definition_node(trees, file, line, name) + tree = get!(() -> parse_source(file), trees, file) + found = nothing + visit(tree) do node + kind(node) == K"function" && JuliaSyntax.source_line(node) == line && + definition_name(node) === name && (found = node) + return found === nothing + end + return found + end + + function body_statements(node) + body = node[2] + return kind(body) == K"block" ? numchildren(body) : 1 + end + + function argument_name(node) + kind(node) == K"Identifier" && return node.val + kind(node) in (K"::", K"=") && numchildren(node) == 2 && return argument_name(node[1]) + kind(node) == K"..." && numchildren(node) == 1 && + (name = argument_name(node[1]); return name === nothing ? nothing : (name, :...)) + return nothing + end + + function forwarded_name(node) + kind(node) == K"=" && numchildren(node) == 2 && kind(node[2]) == K"Identifier" && + argument_name(node[1]) === node[2].val && return node[2].val + kind(node) == K"..." && numchildren(node) == 1 && + (name = forwarded_name(node[1]); return name === nothing ? nothing : (name, :...)) + return kind(node) == K"Identifier" ? node.val : nothing + end + + # Positional and keyword argument names of a call, by `name`. + function call_arguments(call, name) + positional, keywords = Any[], Any[] + for argument in children(call)[2:end] + if kind(argument) == K"parameters" + append!(keywords, map(name, children(argument))) + else + push!(positional, name(argument)) + end + end + return positional, keywords + end + + # A body that is one call passing the definition's own arguments, unchanged, + # in order and keywords included. + function forwards(node) + signature = signature_call(node) + signature === nothing && return false + body = node[2] + kind(body) == K"block" && numchildren(body) == 1 && (body = body[1]) + kind(body) == K"return" && numchildren(body) == 1 && (body = body[1]) + kind(body) == K"call" && is_prefix_call(body) || return false + own = call_arguments(signature, argument_name) + passed = call_arguments(body, forwarded_name) + return !isempty(own[1]) && !any(isnothing, own[1]) && !any(isnothing, own[2]) && + own == passed + end + + # Tiny private helpers. Private module-level functions with one method and at most three body + # statements, referenced by exactly one method and named in no test file, are + # tiny helpers. Exact forwarders count whatever their references. + function tiny_helpers(methods, tree::PackageTree, tests) + callers = IdDict{Any, Set{Method}}() + for m in methods + code = lowered_code(m) + code === nothing && continue + for value in referenced_values(code) + value isa Function && push!(get!(Set{Method}, callers, value), m) + end + end + trees = Dict{String, Any}() + found = Dict{String, Int}() + for M in tree.modules, (name, value) in definitions(M) + value isa Function && !startswith(string(name), "@") && + !Base.ispublic(M, name) && length(Base.methods(value)) == 1 || continue + m = only(Base.methods(value)) + path = source_path(tree, m.file) + path === nothing && continue + node = definition_node(trees, normpath(string(m.file)), m.line, name) + (node !== nothing && body_statements(node) <= 3) || continue + named(tests, name) && continue + references = length(setdiff(get(callers, value, Set{Method}()), (m,))) + (references == 1 || forwards(node)) && count!(found, path) + end + return found + end + + # Root freeze. The number of functions, types and constants the root module defines. + function root_definitions(tree::PackageTree) + n = length(definitions(tree.root)) + return n == 0 ? Dict{String, Int}() : Dict(module_name(tree, tree.root) => n) + end + + function inventory(tree::PackageTree; order = ORDER, dependencies = DEPENDENCIES, + vocabulary = VOCABULARY, tables = TABLES) + methods = package_methods(tree) + files = source_files(tree.directory) + homes = home_directories(tree) + guards = Dict{String, Function}( + "ownership" => () -> ownership(methods, tree), + "placement" => () -> placement(methods, tree, homes), + "direction" => () -> direction(methods, tree, order), + "names" => () -> shared_names(tree), + "shadowing" => () -> shadowing(tree, dependencies), + "validate" => () -> validate_returns(files, tree.directory), + "reserved_verbs" => () -> reserved_verbs(files, tree.directory), + "switches" => () -> switches(files, tree.directory), + "storage" => () -> storage_vocabulary(files, tree.directory), + "dispatch" => () -> dispatch(methods, tree), + "commons" => () -> commons_admission(methods, tree, homes, vocabulary), + "vocabulary" => () -> reserved_vocabulary(files, tree.directory, vocabulary), + "fingerprints" => () -> fingerprints(files, tree.directory), + "clones" => () -> clones(files, methods, tree), + "helpers" => () -> tiny_helpers(methods, tree, + test_sources(joinpath(tree.directory, "test"); excluded = NOT_TESTS)), + "root" => () -> root_definitions(tree)) + return Dict{String, Any}(table => guards[table]() for table in tables) + end + + function live_tree() + extensions = map(EXTENSIONS) do name + extension = Base.get_extension(LineCableModels, name) + extension === nothing && error("Extension $name is not loaded") + extension + end + return PackageTree(LineCableModels, extensions, pkgdir(LineCableModels)) + end + + # Writes a probe package into a temporary directory, loads it and returns + # its inventory. + function probe_inventory(files, name; extension = nothing, options...) + mktempdir() do directory + for (path, text) in files + mkpath(dirname(joinpath(directory, path))) + write(joinpath(directory, path), text) + end + root = Base.include(Main, joinpath(directory, "src", name * ".jl")) + extensions = extension === nothing ? Module[] : + [Base.include(Main, joinpath(directory, "ext", extension * ".jl"))] + tree = Base.invokelatest(PackageTree, root, extensions, directory) + Base.invokelatest(inventory, tree; options...) + end + end + + # Baseline tables that no guard owns. + unknown_tables(document) = sort!([table for table in keys(document) if table ∉ TABLES]) + + const LIVE = Dict{String, Any}() + live() = isempty(LIVE) ? merge!(LIVE, inventory(live_tree())) : LIVE + baseline(path = BASELINE) = TOML.parsefile(path) + + # Each counter of a file entry is compared on its own. + function flatten(entries) + flat = Dict{String, Int}() + for (key, value) in entries + if value isa AbstractDict + for (counter, n) in value + n == 0 || (flat[string(key, " | ", counter)] = n) + end + else + flat[key] = value + end + end + return flat + end + + # `added` lists unlisted keys and raised counts. `stale` lists listed keys + # that are gone or whose count dropped. + function compare(live, listed) + actual, expected = flatten(live), flatten(listed) + added = sort!([string(key, ": ", n, " (listed ", get(expected, key, 0), ")") + for (key, n) in actual if n > get(expected, key, 0)]) + stale = sort!([string(key, ": ", get(actual, key, 0), " (listed ", n, ")") + for (key, n) in expected if get(actual, key, 0) < n]) + return (; added, stale) + end + + check(table) = compare(live()[table], get(baseline(), table, Dict{String, Any}())) + + toml_string(s) = "\"" * escape_string(s, "\"") * "\"" + + function render(inventory) + io = IOBuffer() + println(io, "# Architecture violations present when the guards in") + println(io, "# `test/quality/architecture.jl` were introduced. Delete or lower entries only.") + println(io, "# Print the live inventory with `test/tools/architecture_inventory.jl`.") + for table in TABLES + haskey(inventory, table) || continue + println(io, "\n[", table, "]") + entries = inventory[table] + for key in sort!(collect(keys(entries))) + value = entries[key] + if value isa AbstractDict + counts = join((string(c, " = ", get(value, c, 0)) for c in SWITCHES), ", ") + println(io, toml_string(key), " = { ", counts, " }") + else + println(io, toml_string(key), " = ", value) + end + end + end + return String(take!(io)) + end +end + +@testitem "Quality / architecture / ownership" tags=[:quality] setup=[ArchitectureGuards] begin + result = ArchitectureGuards.check("ownership") + @test result.added == String[] + @test result.stale == String[] +end + +@testitem "Quality / architecture / placement" tags=[:quality] setup=[ArchitectureGuards] begin + result = ArchitectureGuards.check("placement") + @test result.added == String[] + @test result.stale == String[] +end + +@testitem "Quality / architecture / direction" tags=[:quality] setup=[ArchitectureGuards] begin + A = ArchitectureGuards + # Each top-level submodule has a position in the declared order. + top = Set(nameof(m) for m in A.live_tree().core + if m !== LineCableModels && parentmodule(m) === LineCableModels) + @test top == Set(A.ORDER) + result = A.check("direction") + @test result.added == String[] + @test result.stale == String[] +end + +@testitem "Quality / architecture / names" tags=[:quality] setup=[ArchitectureGuards] begin + result = ArchitectureGuards.check("names") + @test result.added == String[] + @test result.stale == String[] +end + +@testitem "Quality / architecture / shadowing" tags=[:quality] setup=[ArchitectureGuards] begin + result = ArchitectureGuards.check("shadowing") + @test result.added == String[] + @test result.stale == String[] +end + +@testitem "Quality / architecture / validate returns its subject" tags=[:quality] setup=[ArchitectureGuards] begin + result = ArchitectureGuards.check("validate") + @test result.added == String[] + @test result.stale == String[] +end + +@testitem "Quality / architecture / reserved verbs" tags=[:quality] setup=[ArchitectureGuards] begin + result = ArchitectureGuards.check("reserved_verbs") + @test result.added == String[] + @test result.stale == String[] +end + +@testitem "Quality / architecture / symbol switches and probes" tags=[:quality] setup=[ArchitectureGuards] begin + result = ArchitectureGuards.check("switches") + @test result.added == String[] + @test result.stale == String[] +end + +@testitem "Quality / architecture / storage vocabulary" tags=[:quality] setup=[ArchitectureGuards] begin + result = ArchitectureGuards.check("storage") + @test result.added == String[] + @test result.stale == String[] +end + +@testitem "Quality / architecture / dispatch convention" tags=[:quality] setup=[ArchitectureGuards] begin + result = ArchitectureGuards.check("dispatch") + @test result.added == String[] + @test result.stale == String[] +end + +@testitem "Quality / architecture / negative controls of the architecture guards" tags=[:quality] setup=[ArchitectureGuards] begin + A = ArchitectureGuards + + # The planted package has one or two violations for each guard. `Late` is + # declared first in the source and has the second position in the order. The root + # exports `filter`, which Base exports, and `transform`, which only DataFrames + # exports. `Consumer` also exports `count`, which Base exports, but the root does not. + planted_files = Dict( + "src/ArchitectureProbePlanted.jl" => """ + module ArchitectureProbePlanted + include("late/Late.jl") + include("early/Early.jl") + include("consumer/Consumer.jl") + using .Consumer: filter, transform + export filter, transform + end + """, + "src/late/Late.jl" => """ + module Late + marker() = 1 + end + """, + "src/early/Early.jl" => """ + module Early + import ..Late + struct Item end + struct Formula end + function extend end + function quantity end + upward() = Late.marker() + end + """, + "src/consumer/Consumer.jl" => """ + module Consumer + import ..Early + import LineCableModels + export filter, count, transform + struct Local end + struct Formula end + function quantity end + function filter end + function count end + function transform end + filter(::Local) = 1 + Early.extend(::Int) = 1 + Early.Item(::Int) = Early.Item() + # A formula with the registered identifier `:probe`. A constructor may take + # its tag. The other methods with a `Val` first break the dispatch convention. + struct Selection <: LineCableModels.AbstractFormulation end + LineCableModels.Commons.formulas(::Type{<:Selection}) = (:probe,) + Selection(::Val{:probe}) = Selection() + kernel(::Selection, ::Val{:part}) = 1 + kernel(::Val{:part}) = 2 + mixed(::Union{Selection, Val{:part}}) = 3 + tagged(::Val{:probe}) = 4 + include("../early/stray.jl") + include("../../ext/core.jl") + end + """, + "src/early/stray.jl" => "stray() = 1\n", + "ext/core.jl" => "core_method() = 1\n", + "src/sources.jl" => raw""" + validate(x::Int) = nothing + validate(::Float64) = 1 + function validate(x::String) + if isempty(x) + throw(ArgumentError("empty")) + end + end + function validate(x::Symbol) + x === :none && return + return x + end + Owner.validate(x::Char) = normalize(x) + function validate(x::Vector, context) + validate(context) + return x + end + _check_input(x) = x + function ensure_ready end + Owner.require_kind(x) = x + __validate_shape(x) = x + probe(f, x) = applicable(f, x) || Base.applicable(f, x) + @eval generated() = 1 + Base.@eval generated_too() = 2 + switch(x) = x.kind === :a || x.kind == :b || x.kind in (:c, :d) || in(x.kind, [:e]) + negated(x) = x.kind !== :a && x.kind != :b && x.kind ∉ (:c, :d) + """) + # Each storage vocabulary rule has a planted violation. `TracedWorkspace` and + # `initialize_buffers` conform, and `cache_mesh` is not one of the reserved names. + planted_files["src/storage.jl"] = raw""" + struct ProbeWorkspace + input + invariants + buffers + end + mutable struct TracedWorkspace + const input::Int + const plan::Int + const buffers::Int + const trace::Int + end + struct FormulaBuffers end + struct KernelCache end + abstract type AbstractStorage end + struct Table + cache::Int + end + sample_storage(n) = zeros(n) + function kernel_workspace(n) + return n + end + trace_buffers(n) = n + initialize_buffers(selected, T, input, plan, buffers) = buffers + function solve(work, n; scratch = nothing) + storage = zeros(n) + for cache in 1:n + end + return (; work = n, cache_mesh = n) + end + """ + planted_expected = Dict{String, Any}( + "ownership" => Dict( + "Consumer | Early.extend | src/consumer/Consumer.jl" => 1, + "Consumer | Early.Item | src/consumer/Consumer.jl" => 1), + "placement" => Dict("src/early/stray.jl | Consumer" => 1, "ext/core.jl | Consumer" => 1), + "direction" => Dict("Early -> Late | src/early/Early.jl" => 1), + "names" => Dict("quantity" => 2), + "shadowing" => Dict("Consumer.filter -> Base" => 1), + "validate" => Dict("src/sources.jl" => 6), + "reserved_verbs" => Dict( + "src/sources.jl | _check_input" => 1, "src/sources.jl | ensure_ready" => 1, + "src/sources.jl | require_kind" => 1, "src/sources.jl | __validate_shape" => 1), + "switches" => Dict("src/sources.jl" => Dict("applicable" => 2, "eval" => 2, "kind" => 7)), + "storage" => Dict( + "src/storage.jl | ProbeWorkspace" => 2, "src/storage.jl | FormulaBuffers" => 1, + "src/storage.jl | KernelCache" => 1, "src/storage.jl | AbstractStorage" => 1, + "src/storage.jl | sample_storage" => 1, "src/storage.jl | kernel_workspace" => 1, + "src/storage.jl | trace_buffers" => 1, "src/storage.jl | work" => 2, + "src/storage.jl | scratch" => 1, "src/storage.jl | storage" => 1, + "src/storage.jl | cache" => 2), + "dispatch" => Dict("Consumer | kernel | src/consumer/Consumer.jl" => 1, + "Consumer | mixed | src/consumer/Consumer.jl" => 1, + "Consumer | tagged | src/consumer/Consumer.jl" => 1)) + + # A clean package with the same modules and one extension method. + clean_files = Dict( + "src/ArchitectureProbeClean.jl" => """ + module ArchitectureProbeClean + include("early/Early.jl") + include("late/Late.jl") + using .Late: sum + export sum + end + """, + "src/early/Early.jl" => """ + module Early + struct Item end + struct Formula end + function extend end + function quantity end + extend(::Item) = 1 + end + """, + "src/late/Late.jl" => """ + module Late + import ..Early + import Base: sum + export sum + struct Local end + struct Formula end + sum(::Local) = 0 + Early.extend(::Local) = 2 + Early.quantity(::Local) = 3 + Early.Item(::Local) = Early.Item() + downward() = Early.extend(Early.Item()) + # A formula whose methods take it first, and its constructor by tag. + import LineCableModels + struct Selection <: LineCableModels.AbstractFormulation end + LineCableModels.Commons.formulas(::Type{<:Selection}) = (:clean_probe,) + Selection(::Val{:clean_probe}) = Selection() + kernel(::Selection, ::Val{:part}) = 1 + include("helpers.jl") + end + """, + "src/late/helpers.jl" => "helper() = 1\n", + "ext/ArchitectureProbeCleanExt.jl" => """ + module ArchitectureProbeCleanExt + import ..ArchitectureProbeClean: Early + Early.extend(::Int) = 4 + end + """, + "src/sources.jl" => raw""" + validate(x::Int) = x + function Owner.validate(x::Real, context) + x > 0 || throw(ArgumentError("nonpositive")) + foreach(context) do y + return y + end + return x + end + validate(x::Symbol) = x === :none ? error("none") : x + function validate(x::String) + if isempty(x) + throw(ArgumentError("empty")) + elseif length(x) > 9 + return x + else + x + end + end + function validate(x::Char) + try + isvalid(x) || throw(ArgumentError("invalid")) + x + catch + rethrow() + end + end + validate(x::Float32, ::Type{Int}) = x + function validate end + @required Abstract begin + validate(::Abstract) + end + validate!(x) = nothing + validated(x) = nothing + checker(x) = x + check(x) = x + text = "_check_input(x) = x, applicable(f, x), @eval f() = 1, x.kind === :a" + # _check_input(x) = x, applicable(f, x), @eval f() = 1, x.kind === :a + call() = _check_input(1) + different(x, y) = x.kind === y || x.kind !== y || x.mode !== :a || kind === :a + generate(x) = eval(x) + """) + + @test Base.isexported(A.DataFrames, :transform) && A.DataFrames ∉ A.DEPENDENCIES + # These storage names conform. Each workspace has exactly its fields, and Commons defines + # the shared `*Buffers` type. Field reads, call keywords, symbols and longer names define + # no storage name. + clean_files["src/storage.jl"] = raw""" + struct TracedWorkspace + input::Int + plan::Int + buffers::Int + trace::Int + end + struct PlainWorkspace + input::Int + plan::Int + buffers::Int + end + initialize_buffers(selected, T, input, plan, buffers) = merge(buffers, (; kernel = plan)) + function solve(buffers, n; cache_mesh = nothing) + local quadrature = buffers.cache + configure(; scratch = n, cache = :cache) + return (; plan = n, storage_size = quadrature) + end + """ + clean_files["src/commons/buffers.jl"] = "struct SharedBuffers end\n" + planted = A.probe_inventory(planted_files, "ArchitectureProbePlanted"; + order = (:Early, :Late, :Consumer), tables = A.A_TABLES) + clean = A.probe_inventory(clean_files, "ArchitectureProbeClean"; + extension = "ArchitectureProbeCleanExt", order = (:Early, :Late), + tables = A.A_TABLES) + for table in A.A_TABLES + @testset "$table" begin + @test planted[table] == planted_expected[table] + @test isempty(clean[table]) + end + end + + @testset "baseline comparison" begin + listed = Dict{String, Any}("a" => 2, "b" => 1, + "f" => Dict("applicable" => 1, "eval" => 0, "kind" => 2)) + @test A.compare(listed, listed) == (added = String[], stale = String[]) + raised = A.compare(Dict{String, Any}("a" => 3, "b" => 1, "c" => 1, + "f" => Dict("applicable" => 1, "eval" => 1, "kind" => 2)), listed) + @test raised.added == ["a: 3 (listed 2)", "c: 1 (listed 0)", "f | eval: 1 (listed 0)"] + @test raised.stale == String[] + lowered = A.compare(Dict{String, Any}("a" => 1, + "f" => Dict("applicable" => 1, "eval" => 0, "kind" => 1)), listed) + @test lowered.added == String[] + @test lowered.stale == ["a: 1 (listed 2)", "b: 0 (listed 1)", "f | kind: 1 (listed 2)"] + # The rendered baseline parses back to the inventory it was printed from. + @test A.TOML.parse(A.render(planted)) == planted + @test A.TOML.parse(A.render(clean)) == clean + end +end + +@testitem "Quality / architecture / Commons admission" tags=[:quality] setup=[ArchitectureGuards] begin + result = ArchitectureGuards.check("commons") + @test result.added == String[] + @test result.stale == String[] +end + +@testitem "Quality / architecture / reserved vocabulary" tags=[:quality] setup=[ArchitectureGuards] begin + result = ArchitectureGuards.check("vocabulary") + @test result.added == String[] + @test result.stale == String[] +end + +@testitem "Quality / architecture / literal fingerprints" tags=[:quality] setup=[ArchitectureGuards] begin + result = ArchitectureGuards.check("fingerprints") + @test result.added == String[] + @test result.stale == String[] +end + +@testitem "Quality / architecture / clones of Commons bodies" tags=[:quality] setup=[ArchitectureGuards] begin + result = ArchitectureGuards.check("clones") + @test result.added == String[] + @test result.stale == String[] +end + +@testitem "Quality / architecture / tiny private helpers" tags=[:quality] setup=[ArchitectureGuards] begin + result = ArchitectureGuards.check("helpers") + @test result.added == String[] + @test result.stale == String[] +end + +@testitem "Quality / architecture / root freeze" tags=[:quality] setup=[ArchitectureGuards] begin + result = ArchitectureGuards.check("root") + @test result.added == String[] + @test result.stale == String[] +end + +@testitem "Quality / architecture / baseline tables belong to guards" tags=[:quality] setup=[ArchitectureGuards] begin + A = ArchitectureGuards + @test A.unknown_tables(A.baseline()) == String[] + @test A.unknown_tables(Dict("ownership" => Dict(), "retired" => Dict())) == ["retired"] +end + +@testitem "Quality / architecture / negative controls of the Commons guards" tags=[:quality] setup=[ArchitectureGuards] begin + A = ArchitectureGuards + + vocabulary = Dict{Symbol, Regex}(:shared => r"^_*shared$", :lonely => r"^_*lonely$", + :undocumented => r"^_*undocumented$", :untested => r"^_*untested$", + :ideal_transposition! => r"transpos", :inner_only => r"^_*inner_only$", + :kernel => r"^_*kernel$", :scale_of => r"^_*scale_of$", :front => r"^_*front$", + :vacuum_permittivity => A.VOCABULARY[:vacuum_permittivity], + :vacuum_permeability => A.VOCABULARY[:vacuum_permeability]) + matrixops = """ + "Average a square matrix over its cyclic diagonals." + function ideal_transposition!(matrix::AbstractMatrix) + n = checksquare(matrix) + coefficients = similar(diag(matrix)) + @inbounds for offset in 0:(n - 1) + total = zero(eltype(matrix)) + for row in 1:n + total += matrix[row, 1 + mod(row - 1 + offset, n)] + end + coefficients[offset + 1] = total / n + end + @inbounds for row in 1:n, column in 1:n + matrix[row, column] = coefficients[mod1(column - row + 1, n)] + end + return matrix + end + """ + + # Commons has one definition failing each Commons admission criterion and `hidden` + # failing all. `Alpha` and `Beta` plant violations of the reserved vocabulary, the + # literal fingerprints, the clones and the tiny helpers. The root breaks the root freeze. + planted_files = Dict( + "src/CommonsProbePlanted.jl" => """ + module CommonsProbePlanted + include("commons/Commons.jl") + using .Commons: welcome + export welcome + include("alpha/Alpha.jl") + include("beta/Beta.jl") + root_helper() = 1 + const ROOT_LIMIT = 2 + end + """, + "src/commons/Commons.jl" => """ + module Commons + export shared, lonely, undocumented, untested, unreserved, ideal_transposition! + export welcome, extended, inner_only + "Reached only through the closure of a private wrapper." + inner_only(x) = x + 7 + hidden_wrapper(values) = map(value -> inner_only(value), values) + "User API that no test names." + welcome(x) = x + "A hook extended by one owner." + function extended end + "Used by two owners." + shared(x) = x + 1 + "Used by one owner." + lonely(x) = x + 2 + undocumented(x) = x + 3 + "Named in no Commons test." + untested(x) = x + 4 + "Reserves no vocabulary." + unreserved(x) = x + 5 + hidden(x) = x + 6 + include("matrixops.jl") + end + """, + "src/commons/matrixops.jl" => matrixops, + "src/alpha/Alpha.jl" => """ + module Alpha + import ..Commons + uses(m) = Commons.shared(1) + Commons.lonely(1) + Commons.undocumented(1) + + Commons.untested(1) + Commons.unreserved(1) + Commons.ideal_transposition!(m)[1] + + Commons.hidden_wrapper([1])[1] + const EPS0 = 8.8541878128e-12 + transposed!(m) = m + function permeability(T) + μ0 = 4π * 1e-7 + return T(μ0) + end + _tiny(x) = x + 1 + twice(x) = 2 * _tiny(x) + Commons.extended(x::Int) = x + end + """, + "src/beta/Beta.jl" => """ + module Beta + import ..Commons + uses(m) = Commons.shared(1) + Commons.undocumented(1) + Commons.untested(1) + + Commons.unreserved(1) + Commons.ideal_transposition!(m)[1] + + Commons.hidden_wrapper([1])[1] + c0() = 299792458 + mu(T) = 4 * (one(T) * π) * (one(T) * 10)^(-7) + function average_cyclic!(values::AbstractMatrix) + size = checksquare(values) + sums = similar(diag(values)) + @inbounds for shift in 0:(size - 1) + accumulator = zero(eltype(values)) + for i in 1:size + accumulator += values[i, 1 + mod(i - 1 + shift, size)] + end + sums[shift + 1] = accumulator / size + end + @inbounds for i in 1:size, j in 1:size + values[i, j] = sums[mod1(j - i + 1, size)] + end + return values + end + _forward(x; scale = 1) = Commons.shared(x; scale = scale) + first_use(x) = _forward(x) + 1 + second_use(x) = _forward(x; scale = 2) + 1 + end + """, + "test/unit/commons/commons.jl" => + "shared, lonely, undocumented, unreserved, ideal_transposition!, extended, inner_only\n", + # Guard sources name definitions without testing them. + "test/quality/guards.jl" => "_tiny, _forward\n") + planted_expected = Dict{String, Any}( + "commons" => Dict("lonely | owners" => 1, "undocumented | docstring" => 1, + "untested | tests" => 1, "unreserved | vocabulary" => 1, + "hidden | public" => 1, "hidden | docstring" => 1, "hidden | owners" => 1, + "hidden | tests" => 1, "hidden | vocabulary" => 1, + "welcome | tests" => 1, "extended | owners" => 1, "inner_only | owners" => 1, + "hidden_wrapper | public" => 1, "hidden_wrapper | docstring" => 1, + "hidden_wrapper | tests" => 1, "hidden_wrapper | vocabulary" => 1), + "vocabulary" => Dict("src/alpha/Alpha.jl | EPS0" => 1, + "src/alpha/Alpha.jl | transposed!" => 1, "src/alpha/Alpha.jl | μ0" => 1), + "fingerprints" => Dict("src/alpha/Alpha.jl" => 2, "src/beta/Beta.jl" => 2), + "clones" => Dict("src/beta/Beta.jl | average_cyclic! ~ ideal_transposition!" => 1), + "helpers" => Dict("src/alpha/Alpha.jl" => 1, "src/beta/Beta.jl" => 1), + "root" => Dict("CommonsProbePlanted" => 2)) + + # Every Commons definition qualifies, and none of the near misses counts. + clean_files = Dict( + "src/CommonsProbeClean.jl" => """ + module CommonsProbeClean + include("commons/Commons.jl") + using .Commons: greet, declared + export greet + public declared + include("alpha/Alpha.jl") + include("beta/Beta.jl") + end + """, + "src/commons/Commons.jl" => """ + module Commons + export shared, vacuum_permittivity, ideal_transposition!, greet, accumulate_into! + export front, kernel, scale_of + public declared + "Applied inside the closure of `front`." + kernel(x) = x + 1 + "Applied inside the keyword body of `front`." + scale_of(x) = 2x + "Used by two owners. Its closure and keyword body use the other two." + function front(values; factor = 1) + weight = scale_of(factor) + return map(value -> kernel(value) * weight, values) + end + "Used by two owners." + shared(x) = x + 1 + "User API that no owner uses." + greet(x) = x + "User API that the root declares public." + declared(x) = x + "A hook whose default method a hook method of `Alpha` repeats." + function accumulate_into!(values::AbstractVector) + total = zero(eltype(values)) + for index in eachindex(values) + total += values[index] + values[index] = total + end + return values + end + include("consts.jl") + include("matrixops.jl") + end + """, + "src/commons/consts.jl" => """ + "Vacuum permittivity in F/m." + vacuum_permittivity(::Type{T}) where {T} = T(88541878128) / T(10)^22 + """, + "src/commons/matrixops.jl" => matrixops, + "src/alpha/Alpha.jl" => """ + module Alpha + import ..Commons + function capacitance(m, T) + ε0 = Commons.vacuum_permittivity(T) + tolerance = 1e-7 + return Commons.shared(ε0) * Commons.ideal_transposition!(m)[1] * tolerance + end + phase(x) = 2π * x + front_use(values) = Commons.front(values; factor = 2) + "The vacuum permittivity is 8.8541878128e-12 F/m." + documented(x) = x + _tested(x) = x + 1 + calls_tested(x) = _tested(x) * 2 + function _long(x) + a = x + 1 + b = a * 2 + c = b - 3 + return c + end + calls_long(x) = _long(x) + 1 + _reorder(x, y) = Commons.shared(y, x) + first_reorder(x) = _reorder(x, 1) + second_reorder(x) = _reorder(1, x) + function Commons.accumulate_into!(values::Vector{Int}) + total = zero(eltype(values)) + for index in eachindex(values) + total += values[index] + values[index] = total + end + return values + end + end + """, + "src/beta/Beta.jl" => """ + module Beta + import ..Commons + admittance(m, T) = Commons.shared(1) * Commons.vacuum_permittivity(T) * + Commons.ideal_transposition!(m)[1] + Commons.accumulate_into!(values::Vector{Float64}) = values + front_use(values) = Commons.front(values; factor = 3) + end + """, + "test/unit/commons/commons.jl" => + "shared, vacuum_permittivity, ideal_transposition!, greet, declared, accumulate_into!, " * + "front, kernel, scale_of\n", + "test/unit/alpha.jl" => "_tested\n") + + planted = A.probe_inventory(planted_files, "CommonsProbePlanted"; vocabulary, + tables = A.C_TABLES) + clean = A.probe_inventory(clean_files, "CommonsProbeClean"; vocabulary, + tables = A.C_TABLES) + for table in A.C_TABLES + @testset "$table" begin + @test planted[table] == planted_expected[table] + @test isempty(clean[table]) + end + end + @test A.TOML.parse(A.render(planted)) == planted + + # Throw guards are left out of the shingles. Other `||` statements stay in. + body = A.JuliaSyntax.parseall(A.SyntaxNode, """ + function guarded(x) + x > 0 || throw(ArgumentError("x must be positive")) + isempty(x) && error("x is empty") + x === nothing || isfinite(x) || Base.throw(DomainError(x)) + y = f(x) || g(x) + return y + end + """)[1][2] + @test A.body_tokens(body) == ["NAME", "=", "CALL", "(", "NAME", ")", "||", "CALL", "(", + "NAME", ")", "return", "NAME"] +end + +@testitem "Quality / architecture / baseline ratchet follows git renames" tags=[:quality] begin + ratchet = Module(:BaselineRatchet) + Base.include(ratchet, joinpath(pkgdir(LineCableModels), "test", "tools", "baseline_ratchet.jl")) + mktempdir() do repository + git(arguments...) = run(pipeline(Base.invokelatest(ratchet.git, repository, + "-c", "user.name=Probe", "-c", "user.email=probe@example.invalid", + arguments...); stdout = devnull, stderr = devnull)) + definitions = join(("f$i(x) = x + $i" for i in 1:20), "\n") + function write_file(path, text) + mkpath(dirname(joinpath(repository, path))) + write(joinpath(repository, path), text) + end + baseline(rows...; table = "ownership") = write_file(ratchet.BASELINE, + "[$table]\n" * join(("\"$key\" = $n" for (key, n) in rows), "\n") * "\n") + grown() = Base.invokelatest(ratchet.grown, repository, "HEAD").lines + + git("init", "-q") + write_file("src/early/Early.jl", "module Early\n$definitions\nend\n") + write_file("src/early/part.jl", definitions * "\n") + baseline("Early | Early.f1 | src/early/part.jl" => 2, + "Late | Early.f2 | src/late/Late.jl" => 1) + git("add", "-A") + git("commit", "-q", "--no-verify", "--no-gpg-sign", "-m", "start") + # The folder and the entry file move, and the module is renamed with them. + git("mv", "src/early", "src/first") + git("mv", "src/first/Early.jl", "src/first/First.jl") + write_file("src/first/First.jl", "module First\n$definitions\nend\n") + + baseline("First | First.f1 | src/first/part.jl" => 2, + "Late | First.f2 | src/late/Late.jl" => 1) + @test grown() == String[] + baseline("First | First.f1 | src/first/part.jl" => 3) + @test grown() == ["ownership | First | First.f1 | src/first/part.jl: 3 (2 at HEAD)"] + # Keys renamed without a matching git rename are added keys. + baseline("First | First.f1 | src/other/part.jl" => 2, + "Other | First.f2 | src/late/Late.jl" => 1) + @test grown() == [ + "ownership | First | First.f1 | src/other/part.jl: 2 (absent at HEAD)", + "ownership | Other | First.f2 | src/late/Late.jl: 1 (absent at HEAD)"] + # A renamed file that is not a module entry file renames no module. + git("mv", "src/first/part.jl", "src/first/Piece.jl") + baseline("Piece | First.f1 | src/first/Piece.jl" => 2) + @test grown() == ["ownership | Piece | First.f1 | src/first/Piece.jl: 2 (absent at HEAD)"] + # A function that moves to another owner keeps its entries when the defining + # module, the function name, the file and the count are unchanged. + baseline("Late | Other.f2 | src/late/Late.jl" => 1) + @test grown() == String[] + baseline("Late | Other.f2 | src/late/Late.jl" => 2) + @test grown() == ["ownership | Late | Other.f2 | src/late/Late.jl: 2 (absent at HEAD)"] + baseline("Late | Other.f3 | src/late/Late.jl" => 1) + @test grown() == ["ownership | Late | Other.f3 | src/late/Late.jl: 1 (absent at HEAD)"] + # A table absent at the reference belongs to a new guard. + baseline("src/first/Piece.jl" => 1; table = "helpers") + result = Base.invokelatest(ratchet.grown, repository, "HEAD") + @test result.lines == String[] + @test result.tables == ["helpers"] + # An entry file that git moves into another module's directory takes that module's + # name as a prefix. The package root in `src/` is not named. + write_file("src/Package.jl", + "module Package\ninclude(\"host/Host.jl\")\ninclude(\"inner/Inner.jl\")\nend\n") + write_file("src/host/Host.jl", "module Host\nend\n") + write_file("src/inner/Inner.jl", "module Inner\n$definitions\nend\n") + baseline("Inner | Host.f1 | src/inner/Inner.jl" => 1) + git("add", "-A") + git("commit", "-q", "--no-verify", "--no-gpg-sign", "-m", "nested") + # The same keys under the new module name, without a moved entry file, are added. + baseline("Host.Inner | Host.f1 | src/inner/Inner.jl" => 1) + @test grown() == ["ownership | Host.Inner | Host.f1 | src/inner/Inner.jl: 1 (absent at HEAD)"] + git("mv", "src/inner", "src/host/inner") + baseline("Host.Inner | Host.f1 | src/host/inner/Inner.jl" => 1) + @test grown() == String[] + baseline("Host.Inner | Host.f1 | src/host/inner/Inner.jl" => 2) + @test grown() == + ["ownership | Host.Inner | Host.f1 | src/host/inner/Inner.jl: 2 (1 at HEAD)"] + end +end diff --git a/test/quality/architecture_baseline.toml b/test/quality/architecture_baseline.toml new file mode 100644 index 000000000..225dc59c9 --- /dev/null +++ b/test/quality/architecture_baseline.toml @@ -0,0 +1,311 @@ +# Architecture violations present when the guards in +# `test/quality/architecture.jl` were introduced. Delete or lower entries only. +# Print the live inventory with `test/tools/architecture_inventory.jl`. + +[ownership] +"Commons | LineCableModels.description | src/commons/formulas.jl" = 3 +"Commons | LineCableModels.formula_id | src/commons/formulas.jl" = 1 +"Earth.EquivalentHomogeneous | LineCableModels.description | src/earth/equivalenthomogeneous/interface.jl" = 2 +"Earth.FrequencyDependent | LineCableModels.constitutive | src/earth/frequencydependent/interface.jl" = 1 +"Engine | Commons.formulation_options | src/engine/integration.jl" = 1 +"Engine | Commons.observation_resolution | src/engine/lineparameters/resolution.jl" = 1 +"Engine | LineCableModels.constitutive | src/engine/admittance.jl" = 1 +"Engine | LineCableModels.line_length | src/engine/observed_inputs.jl" = 2 +"Engine | Units.quantity | src/engine/lineparameters/quantities.jl" = 3 +"Engine.InsulationAdmittance | Engine.InsulationAdmittanceFormulation | src/engine/insulationadmittance/interface.jl" = 2 +"Engine.InsulationImpedance | Engine.InsulationImpedanceFormulation | src/engine/insulationimpedance/interface.jl" = 1 +"Engine.ModalAnalysis | Commons.normalize_observation_selector | src/engine/modalanalysis/observations.jl" = 2 +"Engine.ModalAnalysis | Commons.observables | src/engine/modalanalysis/observations.jl" = 1 +"Engine.ModalAnalysis | Commons.observation_quantity | src/engine/modalanalysis/observations.jl" = 1 +"Engine.ModalAnalysis | Commons.observation_requests | src/engine/modalanalysis/observations.jl" = 1 +"Engine.ModalAnalysis | Engine.selectdetails | src/engine/modalanalysis/interfaces.jl" = 1 +"Engine.ModalAnalysis | Engine.selectdomain | src/engine/modalanalysis/interfaces.jl" = 1 +"Engine.ModalAnalysis | Units.display_unit | src/engine/modalanalysis/observations.jl" = 51 +"Engine.ModalAnalysis | Units.label | src/engine/modalanalysis/observations.jl" = 51 +"Engine.ModalAnalysis | Units.native_unit | src/engine/modalanalysis/observations.jl" = 51 +"Engine.ModalAnalysis | Units.symbol | src/engine/modalanalysis/observations.jl" = 51 +"Engine.SemiconAdmittance | Engine.SemiconAdmittanceFormulation | src/engine/semiconadmittance/interface.jl" = 2 +"Engine.ShuntModel | Engine.blueprint_dependencies | src/engine/shuntmodel/blueprint.jl" = 1 +"Engine.ShuntModel | Engine.internal_shunt_response | src/engine/shuntmodel/blueprint.jl" = 1 +"ImportExport | Engine.LineParameters | src/importexport/tralin.jl" = 4 +"Materials.TemperatureDependent | LineCableModels.constitutive | src/materials/temperaturedependent/interface.jl" = 1 +"PSCAD | Engine.Formulation | src/pscad/formulations.jl" = 1 +"PSCAD | ImportExport.export_data | src/pscad/importexport/pscad.jl" = 1 +"PSCAD | ImportExport.import_data | src/pscad/importexport/import.jl" = 1 +"ParametricBuilder | Commons.compute | src/parametricbuilder/traversal.jl" = 4 +"ParametricBuilder | DataModel.Annulus | src/parametricbuilder/geometry.jl" = 1 +"ParametricBuilder | DataModel.Assembly | src/parametricbuilder/physicaltree.jl" = 1 +"ParametricBuilder | DataModel.Disk | src/parametricbuilder/geometry.jl" = 1 +"ParametricBuilder | DataModel.Ellipse | src/parametricbuilder/geometry.jl" = 1 +"ParametricBuilder | DataModel.Enclosure | src/parametricbuilder/physicaltree.jl" = 1 +"ParametricBuilder | DataModel.Group | src/parametricbuilder/physicaltree.jl" = 1 +"ParametricBuilder | DataModel.Polygon | src/parametricbuilder/geometry.jl" = 1 +"ParametricBuilder | DataModel.Pose2 | src/parametricbuilder/geometry.jl" = 1 +"ParametricBuilder | DataModel.Rectangle | src/parametricbuilder/geometry.jl" = 1 +"ParametricBuilder | DataModel.Region | src/parametricbuilder/physicaltree.jl" = 11 +"ParametricBuilder | DataModel.Sector | src/parametricbuilder/geometry.jl" = 1 +"ParametricBuilder | DataModel.Shell | src/parametricbuilder/geometry.jl" = 1 +"ParametricBuilder | DataModel.Stack | src/parametricbuilder/physicaltree.jl" = 1 +"ParametricBuilder | Engine.CableConstants | src/parametricbuilder/engine/cableconstants.jl" = 1 +"ParametricBuilder | Engine.CableConstantsProblem | src/parametricbuilder/engine/cableconstants.jl" = 1 +"ParametricBuilder | Engine.LineParametersProblem | src/parametricbuilder/system.jl" = 6 +"ParametricBuilder | LineCableModels.Gridspace | src/parametricbuilder/results.jl" = 1 +"ParametricBuilder | LineCableModels.add! | src/parametricbuilder/material.jl" = 1 +"ParametricBuilder | LineCableModels.build | src/parametricbuilder/physicaltree.jl" = 2 +"ParametricBuilder | LineCableModels.build | src/parametricbuilder/system.jl" = 2 +"ParametricBuilder | Materials.Material | src/parametricbuilder/material.jl" = 4 +"ParametricBuilder | TextDisplay.name | src/parametricbuilder/textdisplay.jl" = 6 +"UQ | LineCableModels.materialize | src/uq/results.jl" = 2 +"UQ | LineCableModels.realize | src/uq/montecarlo/compute.jl" = 1 +"UQ | Units.display_unit | src/uq/observations.jl" = 1 +"UQ | Units.label | src/uq/observations.jl" = 4 +"UQ | Units.native_unit | src/uq/observations.jl" = 1 +"UQ | Units.symbol | src/uq/observations.jl" = 4 + +[placement] + +[direction] + +[names] +"_line!" = 2 +"flatten" = 2 + +[shadowing] + +[validate] + +[reserved_verbs] +"src/commons/observables.jl | validate_observables" = 1 +"src/commons/observed_validation.jl | _validate_observed_comparison" = 1 +"src/commons/observed_validation.jl | _validate_observed_cutoff" = 1 +"src/commons/observed_validation.jl | _validate_observed_id" = 1 +"src/commons/observed_validation.jl | _validate_observed_quantity" = 1 +"src/engine/lineparameters/resolution.jl | _validate_resolution_atol" = 1 +"src/engine/modalanalysis/observations.jl | _validate_phase_selector" = 1 + +[switches] +"ext/LineCableModelsGmshExt/geometry.jl" = { applicable = 0, eval = 0, kind = 2 } +"ext/LineCableModelsGmshExt/getdp.jl" = { applicable = 0, eval = 0, kind = 1 } +"ext/LineCableModelsGmshExt/mesh.jl" = { applicable = 0, eval = 0, kind = 1 } +"ext/LineCableModelsGmshExt/model.jl" = { applicable = 0, eval = 0, kind = 5 } +"ext/LineCableModelsGmshExt/voltage_paths.jl" = { applicable = 0, eval = 0, kind = 1 } +"ext/LineCableModelsMakieExt/guides.jl" = { applicable = 0, eval = 0, kind = 11 } +"ext/LineCableModelsMakieExt/layout.jl" = { applicable = 0, eval = 0, kind = 8 } +"ext/LineCableModelsMakieExt/material_colors.jl" = { applicable = 0, eval = 0, kind = 1 } +"ext/LineCableModelsMakieExt/montecarlo.jl" = { applicable = 0, eval = 0, kind = 1 } +"ext/LineCableModelsMakieExt/plot.jl" = { applicable = 0, eval = 0, kind = 3 } +"ext/LineCableModelsMakieExt/recipes/line_data.jl" = { applicable = 0, eval = 0, kind = 5 } +"ext/LineCableModelsMakieExt/recipes/line_facets.jl" = { applicable = 0, eval = 0, kind = 13 } +"src/commons/observables.jl" = { applicable = 1, eval = 0, kind = 0 } +"src/commons/observed_validation.jl" = { applicable = 0, eval = 0, kind = 13 } +"src/commons/observedresult.jl" = { applicable = 1, eval = 0, kind = 9 } +"src/commons/retained_products.jl" = { applicable = 0, eval = 0, kind = 10 } +"src/datamodel/design/cabledesign.jl" = { applicable = 0, eval = 0, kind = 4 } +"src/datamodel/design/group.jl" = { applicable = 0, eval = 0, kind = 3 } +"src/datamodel/flatten.jl" = { applicable = 0, eval = 0, kind = 4 } +"src/datamodel/placement/patterns.jl" = { applicable = 0, eval = 0, kind = 1 } +"src/datamodel/preview/geometry.jl" = { applicable = 1, eval = 0, kind = 0 } +"src/datamodel/textdisplay.jl" = { applicable = 1, eval = 0, kind = 0 } +"src/engine/admittance.jl" = { applicable = 0, eval = 0, kind = 1 } +"src/engine/blueprint.jl" = { applicable = 0, eval = 0, kind = 4 } +"src/engine/modalanalysis/observations.jl" = { applicable = 0, eval = 9, kind = 0 } +"src/engine/pipeimpedance/interface.jl" = { applicable = 0, eval = 0, kind = 1 } +"src/engine/shuntmodel/charge_collocation.jl" = { applicable = 0, eval = 0, kind = 5 } +"src/importexport/deserialize.jl" = { applicable = 1, eval = 0, kind = 0 } +"src/materials/radialdielectric.jl" = { applicable = 0, eval = 0, kind = 1 } +"src/materials/temperaturedependent/interface.jl" = { applicable = 0, eval = 0, kind = 1 } +"src/parametricbuilder/physicaltree.jl" = { applicable = 0, eval = 0, kind = 1 } +"src/parametricbuilder/textdisplay.jl" = { applicable = 1, eval = 0, kind = 0 } +"src/pscad/importexport/project.jl" = { applicable = 0, eval = 0, kind = 1 } +"src/reportbuilder/tables.jl" = { applicable = 0, eval = 0, kind = 7 } + +[storage] +"src/engine/cableconstants.jl | CableConstantsWorkspace" = 4 +"src/engine/modalanalysis/compute.jl | ModalAnalysisWorkspace" = 8 + +[dispatch] + +[commons] +"@observe | tests" = 1 +"AbstractCoreResult | tests" = 1 +"AbstractParametricResult | tests" = 1 +"AbstractProblemResult | tests" = 1 +"AbstractResultSpace | tests" = 1 +"AbstractUncertaintyResult | tests" = 1 +"ObservedResult | tests" = 1 +"_description_values! | docstring" = 1 +"_description_values! | owners" = 1 +"_description_values! | public" = 1 +"_description_values! | tests" = 1 +"_description_values! | vocabulary" = 1 +"_observable_declaration | docstring" = 1 +"_observable_declaration | public" = 1 +"_observable_declaration | tests" = 1 +"_observable_declaration | vocabulary" = 1 +"_observe_macro_parts | docstring" = 1 +"_observe_macro_parts | owners" = 1 +"_observe_macro_parts | public" = 1 +"_observe_macro_parts | tests" = 1 +"_observe_macro_parts | vocabulary" = 1 +"_observed_fields | docstring" = 1 +"_observed_fields | public" = 1 +"_observed_fields | tests" = 1 +"_observed_fields | vocabulary" = 1 +"_observed_indices | docstring" = 1 +"_observed_indices | owners" = 1 +"_observed_indices | public" = 1 +"_observed_indices | tests" = 1 +"_observed_indices | vocabulary" = 1 +"_observed_scale | docstring" = 1 +"_observed_scale | owners" = 1 +"_observed_scale | public" = 1 +"_observed_scale | tests" = 1 +"_observed_scale | vocabulary" = 1 +"_reexpress_product | docstring" = 1 +"_reexpress_product | public" = 1 +"_reexpress_product | tests" = 1 +"_reexpress_product | vocabulary" = 1 +"_retained_band_samples | docstring" = 1 +"_retained_band_samples | public" = 1 +"_retained_band_samples | tests" = 1 +"_retained_band_samples | vocabulary" = 1 +"_retained_unit | docstring" = 1 +"_retained_unit | public" = 1 +"_retained_unit | tests" = 1 +"_retained_unit | vocabulary" = 1 +"_same_observed_dependencies | docstring" = 1 +"_same_observed_dependencies | public" = 1 +"_same_observed_dependencies | tests" = 1 +"_same_observed_dependencies | vocabulary" = 1 +"_same_observed_values | docstring" = 1 +"_same_observed_values | public" = 1 +"_same_observed_values | vocabulary" = 1 +"_selected_product | docstring" = 1 +"_selected_product | public" = 1 +"_selected_product | tests" = 1 +"_selected_product | vocabulary" = 1 +"_unit_override | docstring" = 1 +"_unit_override | public" = 1 +"_unit_override | tests" = 1 +"_unit_override | vocabulary" = 1 +"_unit_override_keys | docstring" = 1 +"_unit_override_keys | owners" = 1 +"_unit_override_keys | public" = 1 +"_unit_override_keys | tests" = 1 +"_unit_override_keys | vocabulary" = 1 +"_validate_observed_comparison | docstring" = 1 +"_validate_observed_comparison | public" = 1 +"_validate_observed_comparison | tests" = 1 +"_validate_observed_comparison | vocabulary" = 1 +"_validate_observed_cutoff | docstring" = 1 +"_validate_observed_cutoff | owners" = 1 +"_validate_observed_cutoff | public" = 1 +"_validate_observed_cutoff | tests" = 1 +"_validate_observed_cutoff | vocabulary" = 1 +"_validate_observed_id | docstring" = 1 +"_validate_observed_id | public" = 1 +"_validate_observed_id | tests" = 1 +"_validate_observed_id | vocabulary" = 1 +"_validate_observed_quantity | docstring" = 1 +"_validate_observed_quantity | public" = 1 +"_validate_observed_quantity | vocabulary" = 1 +"computation_details | tests" = 1 +"gridpoint_id | tests" = 1 +"gridpoint_id | vocabulary" = 1 +"input_fields | tests" = 1 +"materialize_observation | owners" = 1 +"materialize_observation | tests" = 1 +"materialize_observation | vocabulary" = 1 +"normalize_observation_selector | tests" = 1 +"observables | tests" = 1 +"observation_gridpoint | tests" = 1 +"observation_groups | tests" = 1 +"observation_groups | vocabulary" = 1 +"observation_indices | tests" = 1 +"observation_indices | vocabulary" = 1 +"observation_labels | tests" = 1 +"observation_labels | vocabulary" = 1 +"observation_product | tests" = 1 +"observation_product | vocabulary" = 1 +"observation_request | tests" = 1 +"observation_request | vocabulary" = 1 +"observation_requests | tests" = 1 +"observation_resolution | tests" = 1 +"observation_selection | tests" = 1 +"observation_selection | vocabulary" = 1 +"request_identity | tests" = 1 +"request_identity | vocabulary" = 1 +"request_indices | tests" = 1 +"request_indices | vocabulary" = 1 +"request_quantity | tests" = 1 +"request_quantity | vocabulary" = 1 +"unit_targets | owners" = 1 +"unit_targets | tests" = 1 +"unit_targets | vocabulary" = 1 +"validate_observables | tests" = 1 +"validate_observables | vocabulary" = 1 + +[vocabulary] + +[fingerprints] + +[clones] + +[helpers] +"ext/LineCableModelsGmshExt/compute.jl" = 2 +"ext/LineCableModelsGmshExt/geometry.jl" = 3 +"ext/LineCableModelsGmshExt/getdp.jl" = 2 +"ext/LineCableModelsGmshExt/mesh.jl" = 1 +"ext/LineCableModelsGmshExt/model.jl" = 2 +"ext/LineCableModelsGmshExt/onelab.jl" = 1 +"ext/LineCableModelsGmshExt/results.jl" = 2 +"ext/LineCableModelsGmshExt/workers.jl" = 3 +"ext/LineCableModelsMakieExt/controls.jl" = 1 +"ext/LineCableModelsMakieExt/export_presentation.jl" = 2 +"ext/LineCableModelsMakieExt/layout.jl" = 3 +"ext/LineCableModelsMakieExt/material_colors.jl" = 3 +"ext/LineCableModelsMakieExt/native_export.jl" = 1 +"ext/LineCableModelsMakieExt/recipes/preview_data.jl" = 2 +"ext/LineCableModelsMakieExt/recipes/preview_render.jl" = 1 +"ext/LineCableModelsMakieExt/shell.jl" = 5 +"src/commons/observables.jl" = 1 +"src/commons/observed_validation.jl" = 1 +"src/datamodel/cableslibrary/vdeparse.jl" = 2 +"src/datamodel/flatten.jl" = 6 +"src/datamodel/geometry/pose.jl" = 1 +"src/datamodel/geometry/sector.jl" = 4 +"src/datamodel/linecablesystem/clearance.jl" = 1 +"src/datamodel/placement/compaction.jl" = 1 +"src/datamodel/placement/paths.jl" = 4 +"src/datamodel/placement/patterns.jl" = 1 +"src/datamodel/preview/materials.jl" = 1 +"src/datamodel/textdisplay.jl" = 1 +"src/earth/base.jl" = 2 +"src/earth/earthlayer.jl" = 1 +"src/engine/cableconstants.jl" = 1 +"src/engine/formulations.jl" = 1 +"src/engine/problems.jl" = 1 +"src/engine/shuntmodel/charge_collocation.jl" = 2 +"src/gridspace.jl" = 3 +"src/importexport/deserialize.jl" = 1 +"src/importexport/observed.jl" = 1 +"src/parametricbuilder/engine/cableconstants.jl" = 1 +"src/parametricbuilder/macros.jl" = 3 +"src/parametricbuilder/physicaltree.jl" = 4 +"src/parametricbuilder/positions.jl" = 1 +"src/parametricbuilder/results.jl" = 1 +"src/parametricbuilder/system.jl" = 1 +"src/parametricbuilder/wirepatterns/gauges.jl" = 1 +"src/pscad/compute.jl" = 1 +"src/pscad/importexport/import.jl" = 2 +"src/pscad/importexport/project.jl" = 2 +"src/pscad/importexport/schema.jl" = 1 +"src/pscad/results.jl" = 3 +"src/textdisplay/TextDisplay.jl" = 2 +"src/units/scaling.jl" = 1 +"src/units/units.jl" = 2 +"src/uq/montecarlo/compute.jl" = 4 +"src/uq/results.jl" = 2 + +[root] +"LineCableModels" = 63 diff --git a/test/quality/explicit_imports.jl b/test/quality/explicit_imports.jl new file mode 100644 index 000000000..8529ac91a --- /dev/null +++ b/test/quality/explicit_imports.jl @@ -0,0 +1,112 @@ +@testitem "Quality / explicit imports / package ownership" tags = [:quality] begin + using ExplicitImports: test_explicit_imports, improper_qualified_accesses + # These adapters participate in the numerical/UQ paths. Check the loaded + # extensions too. A cold root-only scan cannot see their ownership errors. + import Measurements, Distributions, Gmsh, Calculus, XLSX, CairoMakie + for name in (:LineCableModelsMeasurementsExt, :LineCableModelsDistributionsExt, + :LineCableModelsGmshExt, :LineCableModelsXLSXExt) + @test Base.get_extension(LineCableModels, name) !== nothing + end + renderer = Base.get_extension(LineCableModels, :LineCableModelsMakieExt) + cairo = Base.get_extension(LineCableModels, :LineCableModelsCairoMakieExt) + extension = Base.get_extension(LineCableModels, :LineCableModelsGmshExt) + @test renderer !== nothing + @test cairo !== nothing + import Logging, JSON3 + + # Gmsh's generated API deliberately uses qualified calls without export/ + # public annotations. Accept its documented bindings, not arbitrary private + # names. The documented logger and JSON protocols below also require these checks. + function documented_fem_access(owner::Module, name::Symbol) + # Generated API/build metadata needed to identify the numerical library. + owner === Gmsh.gmsh && name in (:GMSH_API_VERSION, :lib) && return true + ancestor = owner + while ancestor !== Gmsh.gmsh && parentmodule(ancestor) !== ancestor + ancestor = parentmodule(ancestor) + end + allowed = (ancestor === Gmsh.gmsh && !startswith(string(name), "_")) || + (owner === Gmsh && name === :finalize) || + (owner === Logging && name in (:catch_exceptions, :handle_message, + :min_enabled_level, :shouldlog)) || + (owner === JSON3 && name in (:read, :write, :pretty)) + allowed && isdefined(owner, name) || return false + value = getfield(owner, name) + return Base.Docs.hasdoc(owner, name) || + (value isa Function && Base.Docs.hasdoc(parentmodule(value), nameof(value))) + end + + # Exact external methods, classified in `docs/src/developers.md`. Public + # annotations alone miss documented qualified APIs. The one layout removal + # workaround is explicitly recommended by its upstream maintainer. It is + # not a general permission to consume Makie or package-owned internals. + function supported_external_access(consumer, owner, name) + consumer === LineCableModels.Commons && owner === Base && name === :unalias && + Base.Docs.hasdoc(Base, :unalias) && return true + consumer === cairo && owner === CairoMakie && name === :activate! && return true + consumer === renderer && owner === CairoMakie.Makie && name in ( + :automatic, :current_backend, :get_ticks, :get_tickvalues, + :pseudolog10, :inverse_transform, :CategoricalConversion, + :defaultlimits, :fast_string_boundingboxes) && return true + consumer === renderer && owner === CairoMakie.Makie.GridLayoutBase && + name === :remove_from_gridlayout! && return true + consumer in (renderer, extension) && owner === Base && name === :IOError && return true + return false + end + + # All other ExplicitImports checks remain unchanged, including ownership. + test_explicit_imports(LineCableModels; all_qualified_accesses_are_public=false) + unexpected = String[] + for (consumer, accesses) in improper_qualified_accesses( + LineCableModels; skip=(), allow_internal_accesses=false) + for row in accesses + row.public_access && continue + row.self_qualified && continue # Covered by the existing separate check. + row.accessing_from === Base && Base.ispublic(Core, row.name) && continue + row.accessing_from === Logging && documented_fem_access(Logging, row.name) && continue + consumer === LineCableModels.ImportExport && row.accessing_from === JSON3 && documented_fem_access(JSON3, row.name) && continue + consumer === extension && documented_fem_access(row.accessing_from, row.name) && continue + supported_external_access(consumer, row.accessing_from, row.name) && continue + push!(unexpected, "$(row.accessing_from).$(row.name) at $(row.location)") + end + end + @test unexpected == String[] + mktempdir() do directory + path=joinpath(directory,"OwnershipProbe.jl") + write(path,"""module OwnershipProbe +module Owner + private_value() = 1 +end +module Consumer + using ..Owner + value() = Owner.private_value() +end +end +""") + probe=Base.include(Main,path) + omitted=Base.invokelatest(improper_qualified_accesses,probe,path; + skip=(),allow_internal_accesses=true) + @test all(isempty(accesses) for (_,accesses) in omitted) + checked=Base.invokelatest(improper_qualified_accesses,probe,path; + skip=(),allow_internal_accesses=false) + owner=Base.invokelatest(getfield,probe,:Owner) + @test any(row -> row.accessing_from===owner && + row.name===:private_value && !row.public_access, + Iterators.flatten(accesses for (_,accesses) in checked)) + end + @test documented_fem_access(Gmsh.gmsh.model, :add_physical_group) + @test !documented_fem_access(Gmsh.gmsh.model, :_unregistered_helper) + @test !documented_fem_access(LineCableModels.Engine, :compute) + @test !documented_fem_access(JSON3, :StructTypes) + @test supported_external_access(renderer, CairoMakie.Makie, :get_ticks) + @test supported_external_access(renderer, CairoMakie.Makie, :inverse_transform) + @test supported_external_access(renderer, CairoMakie.Makie, :CategoricalConversion) + @test supported_external_access(renderer, CairoMakie.Makie, :defaultlimits) + @test supported_external_access(LineCableModels.Commons, Base, :unalias) + @test !supported_external_access(LineCableModels.Engine, Base, :unalias) + @test !supported_external_access(renderer, Base, :unalias) + @test !supported_external_access(LineCableModels.Engine, CairoMakie.Makie, :inverse_transform) + @test !supported_external_access(LineCableModels.Engine, CairoMakie.Makie, :get_ticks) + @test !supported_external_access(renderer, CairoMakie.Makie, :get_plot_visibilities) + @test !supported_external_access(renderer, CairoMakie.Makie, :fast_string_boundingboxes_obs) + @test !supported_external_access(renderer, LineCableModels.Engine, :compute) +end diff --git a/test/quality/fatou.jl b/test/quality/fatou.jl new file mode 100644 index 000000000..9d924b678 --- /dev/null +++ b/test/quality/fatou.jl @@ -0,0 +1,194 @@ +# Without Fatou on PATH, a local run records this item as skipped. CI requires Fatou. +@testitem "Quality / Fatou / advisory source diagnostics" tags=[:quality] default_imports=false skip=begin + absent = Sys.which("fatou") === nothing && get(ENV, "CI", "") != "true" + absent && println("Fatou is not on PATH: the Fatou item was not run locally; CI runs it.") + absent +end begin + using Test, TOML, JSON3 + + root = normpath(joinpath(@__DIR__, "..", "..")) + config = joinpath(root, "fatou.toml") + lint = TOML.parsefile(config)["lint"] + selected = lint["select"] + @test !isempty(selected) && allunique(selected) + @test Set(keys(lint["severity"])) == Set(selected) + @test all(==("warning"), values(lint["severity"])) + target = TOML.parsefile(joinpath(root, "Project.toml"))["compat"]["julia"] + + function run_fatou(command) + output, errors = IOBuffer(), IOBuffer() + process = run(pipeline(ignorestatus(command); stdout=output, stderr=errors)) + return (; stdout=String(take!(output)), stderr=String(take!(errors)), + exitcode=process.exitcode, termsignal=process.termsignal) + end + + function check_fatou_version(executable) + executable === nothing && error("Fatou 0.22.0 is required on PATH; install it before testing") + result = run_fatou(`$executable --version`) + result.exitcode == 0 && result.termsignal == 0 && + isempty(result.stderr) && strip(result.stdout) == "fatou 0.22.0" || + error("Expected Fatou 0.22.0: $(repr(result))") + return nothing + end + + function fatou_diagnostics(result, selected; io=devnull, root=pwd()) + result.termsignal == 0 || error("Fatou terminated by signal $(result.termsignal)") + result.exitcode in (0, 1) || error("Unexpected Fatou exit $(result.exitcode)") + # JSON mode puts findings on stdout. Configuration warnings and errors + # remain on stderr, including unknown rule IDs and invalid Julia targets. + any(line -> startswith(strip(line), "warning:") || startswith(strip(line), "error:"), + split(result.stderr, '\n')) && error("Fatou setup failed: $(result.stderr)") + diagnostics = JSON3.read(result.stdout) + diagnostics isa JSON3.Array || error("Expected a Fatou diagnostic array") + for diagnostic in diagnostics + diagnostic isa JSON3.Object || error("Invalid Fatou diagnostic") + all(key -> haskey(diagnostic, key), (:rule, :severity, :path, :range, :message)) || + error("Incomplete Fatou diagnostic") + diagnostic.rule in selected || diagnostic.rule == "parse-error" || + error("Unexpected Fatou rule $(diagnostic.rule)") + diagnostic.severity == (diagnostic.rule == "parse-error" ? "error" : "warning") || + error("Unexpected Fatou diagnostic severity") + diagnostic.path isa String && isfile(diagnostic.path) || + error("Fatou diagnostic has no readable source path") + diagnostic.range isa JSON3.Object && + all(key -> haskey(diagnostic.range, key), (:start, :end)) && + diagnostic.range.start isa Integer && diagnostic.range.end isa Integer && + 0 <= diagnostic.range.start <= diagnostic.range.end <= filesize(diagnostic.path) || + error("Invalid Fatou byte range") + diagnostic.message isa JSON3.Object && haskey(diagnostic.message, :body) && + diagnostic.message.body isa String || error("Invalid Fatou message") + end + result.exitcode == (isempty(diagnostics) ? 0 : 1) || + error("Fatou exit status does not agree with its diagnostic output") + counts = Dict(rule => 0 for rule in selected) + parse_failures = Set{String}() + for diagnostic in diagnostics + line, column = fatou_location(read(diagnostic.path, String), diagnostic.range.start) + status = diagnostic.rule == "parse-error" ? "scan failure" : "advisory" + println(io, "Fatou ", status, " [", diagnostic.rule, "] ", relpath(diagnostic.path, root), + ":", line, ":", column, ": ", diagnostic.message.body) + if diagnostic.rule == "parse-error" + push!(parse_failures, diagnostic.path) + else + counts[diagnostic.rule] += 1 + end + end + for rule in sort(selected) + println(io, "Fatou advisory count [", rule, "]: ", counts[rule]) + end + println(io, "Fatou files with parse failures: ", length(parse_failures)) + isempty(parse_failures) || error("Fatou could not parse: $(join(sort!(collect(parse_failures)), ", "))") + return diagnostics + end + + # Fatou uses zero-based UTF-8 byte offsets. Report one-based character columns. + function fatou_location(source, offset) + 0 <= offset <= ncodeunits(source) || error("Fatou offset is outside the source") + index = offset + 1 + index == ncodeunits(source) + 1 || isvalid(source, index) || + error("Fatou offset splits a UTF-8 character") + prefix = SubString(source, 1, prevind(source, index)) + line = count(==('\n'), prefix) + 1 + newline = findlast(==('\n'), prefix) + column = length(newline === nothing ? prefix : SubString(prefix, nextind(prefix, newline))) + 1 + return line, column + end + + executable = Sys.which("fatou") + check_fatou_version(executable) + println("Fatou 0.22.0; Julia compatibility target: ", target) + + @testset "Fatou integration controls" begin + @test_throws ErrorException check_fatou_version(nothing) + @test_throws ErrorException check_fatou_version(Base.julia_cmd()) + @test_throws Base.IOError run_fatou(`$(joinpath(tempdir(), "missing-fatou-$(getpid())")) --version`) + clean = (; stdout="[]", stderr="", exitcode=0, termsignal=0) + @test isempty(fatou_diagnostics(clean, selected)) + for bad in (merge(clean, (; stdout="")), merge(clean, (; stdout="{")), + merge(clean, (; stdout="{}")), merge(clean, (; stdout="[{}]")), + merge(clean, (; exitcode=1)), merge(clean, (; exitcode=7)), + merge(clean, (; termsignal=15)), + merge(clean, (; stderr="warning: unknown rule `typo`")), + merge(clean, (; stderr="warning: invalid Julia version"))) + @test_throws Exception fatou_diagnostics(bad, selected) + end + terminated = run_fatou(`$(Base.julia_cmd()) --startup-file=no -e 'print("[]"); exit(7)'`) + @test_throws ErrorException fatou_diagnostics(terminated, selected) + @test fatou_location("αβ\nγδ", 0) == (1, 1) + @test fatou_location("αβ\nγδ", 2) == (1, 2) + @test fatou_location("αβ\nγδ", 5) == (2, 1) + @test_throws ErrorException fatou_location("αβ", 1) + + mktempdir() do directory + valid = joinpath(directory, "valid.jl") + write(valid, "\"\"\"Value \$(1).\"\"\"\nidentity_α(α) = α\n") + result = run_fatou(`$executable --config $config lint --julia-version $target --output json $valid`) + @test isempty(fatou_diagnostics(result, selected)) + + nested = mkpath(joinpath(directory, "nested")) + mkdir(joinpath(directory, ".git")) + write(joinpath(directory, ".gitignore"), "nested/\n") + duplicate = joinpath(nested, "duplicate.jl") + unused = joinpath(directory, "unused.jl") + write(duplicate, "duplicate(x) = x\nduplicate(x) = x + 1\n") + write(unused, "function unused(x)\n α = x + 1\n return x\nend\n") + result = run_fatou(`$executable --config $config lint --julia-version $target --output json $valid $duplicate $unused`) + diagnostics = fatou_diagnostics(result, selected) + @test result.exitcode == 1 # Findings are reported, but this control passes. + @test any(d -> d.rule == "duplicate-method" && d.path == duplicate, diagnostics) + @test any(d -> d.rule == "unused-binding" && d.path == unused, diagnostics) + walked = run_fatou(`$executable --config $config lint --julia-version $target --output json $directory`) + @test all(d -> d.path != duplicate, fatou_diagnostics(walked, selected)) + + versioned = joinpath(directory, "versioned.jl") + write(versioned, "public identity_α\nidentity_α(α) = α\n") + result = run_fatou(`$executable --config $config lint --julia-version 1.10 --output json $versioned`) + @test any(d -> d.rule == "julia-version-compat", fatou_diagnostics(result, selected)) + + invalid = joinpath(directory, "invalid.jl") + write(invalid, "function broken(\n") + result = run_fatou(`$executable --config $config lint --output json $invalid`) + @test any(d -> d.rule == "parse-error", JSON3.read(result.stdout)) + @test_throws ErrorException fatou_diagnostics(result, selected) + + badconfig = joinpath(directory, "fatou.toml") + write(badconfig, "[lint]\nselect = [\"misspelled-rule\"]\n") + result = run_fatou(`$executable --config $badconfig lint --output json $valid`) + @test_throws ErrorException fatou_diagnostics(result, selected) + write(badconfig, "[lint\n") + result = run_fatou(`$executable --config $badconfig lint --output json $valid`) + @test_throws ErrorException fatou_diagnostics(result, selected) + result = run_fatou(`$executable --config $config lint --output json $(joinpath(directory, "absent.jl"))`) + @test_throws ErrorException fatou_diagnostics(result, selected) + end + end + + @testset "Fatou production scan" begin + failures = 0 + try + files = String[] + for name in ("src", "ext") + directory = joinpath(root, name) + isdir(directory) || error("Missing production source root: $directory") + for (path, _, names) in walkdir(directory), name in names + endswith(name, ".jl") && push!(files, joinpath(path, name)) + end + end + sort!(files) + isempty(files) && error("Empty production source inventory") + println("Fatou requested source files: ", length(files)) + result = run_fatou(`$executable --config $config lint --julia-version $target --output json $files`) + # Retain native output too, including parse failures and suggested + # fixes. Suggestions are data only. The scanner never invokes a fix option. + println("Fatou native diagnostics: ", result.stdout) + print(result.stderr) + fatou_diagnostics(result, selected; io=stdout, root) + @test result.exitcode in (0, 1) + catch + failures += 1 + rethrow() + finally + println("Fatou production scan failures: ", failures) + end + end +end diff --git a/test/quality/formula_routes.jl b/test/quality/formula_routes.jl new file mode 100644 index 000000000..266ef9438 --- /dev/null +++ b/test/quality/formula_routes.jl @@ -0,0 +1,129 @@ +@testitem "Quality / formula registry / every registered identity constructs through each route" tags=[:quality] setup=[FormulaFamilies] begin + const C=LineCableModels.Commons + families=FormulaFamilies.families() + @test length(families) == 12 + for family in families + F=family.Formula + registered=C.formulas(F) + @test allunique(registered) + @test :default in registered + installed=formula_id(F(:default)) + @test installed in registered && installed !== :default + for identifier in registered + selected=F(identifier) + # The convenience routes and a passive definition construct the same formula type. + @test typeof(F(Val(identifier))) === typeof(selected) + @test typeof(F(formula(identifier))) === typeof(selected) + @test typeof(F{identifier}()) === typeof(selected) + @test formula_id(selected) === (identifier === :default ? installed : identifier) + @test F(selected) === selected + @test formula_id(typeof(selected)) === formula_id(selected) + @test NamedTuple(selected).identifier === formula_id(selected) + end + @test_throws ArgumentError F(:UnregisteredIdentity) + end + # An unspecified slot of a backend resolves through its family's `:default`. + for (backend, formulation) in ((LineParametersFormulation, Formulation()), + (CableConstantsFormulation, CableConstantsFormulation())) + for (slot, F) in pairs(backend) + @test isequal(getproperty(formulation.methods, slot), F(:default)) + end + end + @test isequal(ModalAnalysisFormulation().formula, LineCableModels.ModalAnalysis.Formula(:default)) +end + +@testitem "Quality / native equation bindings and closed built-in formula lists" tags=[:quality] setup=[FormulaFixtures,FormulaFamilies] begin + const E=LineCableModels.Engine + const EP=LineCableModels.Earth + const Expression=LineCableModels.Expression + # The expression routes below cover the families whose formulas have options and run + # in the frequency loop. PipeImpedance formulas have no options, and ShuntModel + # formulas run at blueprint time. + owners=filter(family -> family ∉ (E.PipeImpedance, E.ShuntModel), FormulaFamilies.families()) + scalar_operations=(E.InsulationImpedance=>E.InsulationImpedance.insulation_impedance, + E.InsulationAdmittance=>E.InsulationAdmittance.insulation_material, + E.SemiconAdmittance=>E.SemiconAdmittance.semicon_material, + EP.FrequencyDependent=>EP.FrequencyDependent.earth_material, + LineCableModels.ModalAnalysis=>LineCableModels.ModalAnalysis.decompose!, + LineCableModels.Materials.TemperatureDependent=> + LineCableModels.Materials.TemperatureDependent.temperature_resistivity) + for owner in owners, identifier in owner.formulas(owner.Formula) + selected=owner.Formula(identifier) + @test fieldtype(typeof(selected), :options) <: FormulationOptions + @test formulation_options(selected) === selected.options + @test formula_id(selected) !== :default + @test owner.Formula(selected) === selected + routes = if owner in (E.EarthImpedance,E.EarthAdmittance) + expressions=Expression[] + for kind in (:self,:mutual), source in 1:2, target in 1:2 + kind === :self && source != target && continue + pair=E.EarthPair(1,kind === :self ? 1 : 2, + (source == 1 ? 1.0 : -1.0,target == 1 ? 1.0 : -1.0), + kind === :self ? 0.0 : 1.0,(source,target); + radius=kind === :self ? 0.01 : nothing) + expression=Expression(selected,pair) + # Registration and indexed binding do not claim an implemented + # equation. Actual supported, stub and unsupported calls are covered + # by the execution tests, not a reflected coverage list. + push!(expressions,expression) + end + expressions + elseif owner === E.InternalImpedance + Tuple(Expression(selected,owner.internal_impedance,Val(kind)) for kind in (:inner,:outer,:transfer)) + elseif owner === EP.EquivalentHomogeneous + (Expression(selected,E.EarthPair(1,1,(1.0,1.0),0.0,(1,1);radius=0.01)),) + else + operation=only(last(entry) for entry in scalar_operations if first(entry) === owner) + (Expression(selected,operation),) + end + for route in routes + @test route.selection === selected + @test typeof(route).parameters[1] === typeof(selected) + # Unified's operation serves both earth families from EarthAdmittance. + home = route.method === E.EarthAdmittance.source_coefficients ? + E.EarthAdmittance : owner + @test parentmodule(route.method) === home + @test all(arg->arg isa Val,route.arguments) + @test formulation_options(route) isa FormulationOptions + @test_throws MethodError computation_options(route) + @test_throws MethodError formulation_options(route, ComputationOptions()) + end + end + M=FormulaFixtures + for (owner,custom) in ((E.InternalImpedance,M.SurfaceLaw()), + (E.InsulationImpedance,M.InsulationReactance()), + (E.InsulationAdmittance,M.InsulationLaw()),(E.SemiconAdmittance,M.SemiconLaw()), + (E.EarthImpedance,M.selection(E.EarthImpedance)), + (E.EarthAdmittance,M.selection(E.EarthAdmittance)), + (EP.FrequencyDependent,M.DispersiveEarth()), + (EP.EquivalentHomogeneous,M.MeanEarth()), + (LineCableModels.Materials.TemperatureDependent,M.ConstantResistivity(1e-8)), + (E.ShuntModel,M.UserCoaxialShunt()),(E.PipeImpedance,M.UserCoaxialPipe())) + @test owner.Formula(custom) === custom + @test formula_id(custom) ∉ owner.formulas(owner.Formula) + end +end + +@testitem "Quality / user-owned shunt and pipe selections reach blueprint and compute" tags=[:quality] setup=[TestFixtures,FormulaFixtures,ModalFormulaFixtures] begin + M=FormulaFixtures + shunt=M.UserCoaxialShunt() + selected=Formulation(shunt_model=shunt,pipe_impedance=M.UserCoaxialPipe()) + problem=TestFixtures.line_parameters_problem(frequencies=[50.0,500.0]) + actual=compute(problem,selected) + expected=compute(problem,Formulation()) + @test Z(actual)==Z(expected) && Y(actual)==Y(expected) + @test shunt.response_count[]==length(problem.system.designs) + @test details(actual).data.formulations.methods.shunt_model.identifier === :UserCoaxialShunt + @test details(actual).data.formulations.methods.pipe_impedance.identifier === :UserCoaxialPipe + constants=CableConstantsProblem(first(problem.system.designs);frequency=50.0) + @test compute(constants,CableConstantsFormulation(shunt_model=shunt,pipe_impedance=M.UserCoaxialPipe()))== + compute(constants,CableConstantsFormulation()) + source=TestFixtures.two_conductor_results() + # Modal selections are admitted through the modal action, not a callback bag. + maps=operators(compute(ModalAnalysisProblem(source),ModalAnalysisFormulation())) + custom=ModalFormulaFixtures.FixedModalMaps(maps.Tv,maps.Ti) + action=ModalAnalysisFormulation(custom) + @test action.formula === custom + @test action.definition === custom + @test all(isfinite,Z(compute(ModalAnalysisProblem(source),action))) +end diff --git a/test/quality/formulation_catalogue.jl b/test/quality/formulation_catalogue.jl new file mode 100644 index 000000000..07ac66562 --- /dev/null +++ b/test/quality/formulation_catalogue.jl @@ -0,0 +1,104 @@ +@testitem "Quality / documentation / formula source ownership" tags = [:quality] setup = [FormulaFamilies] begin + using Base.Docs: Binding, DocStr, meta + using DocStringExtensions: TypedMethodSignatures + using LineCableModels + + # Every registered family whose formulas each live in a file of the `formulas` + # directory beside the module that registers them. + categories = map(FormulaFamilies.families()) do family + registration = which(LineCableModels.Commons.formulas, Tuple{Type{family.Formula}}) + (module_owner = family, + registry = LineCableModels.Commons.formulas(family.Formula), + directory = joinpath(dirname(String(registration.file)), "formulas")) + end + filter!(category -> isdir(category.directory), categories) + @test length(categories) == 11 + + for category in categories + directory = category.directory + formula_files = sort(filter( + file -> endswith(file, ".jl"), + readdir(directory) + )) + + selected_default = category.module_owner.Formula(:default) + @test formula_id(selected_default) !== :default + @test :default in category.registry + + formula_docs = DocStr[] + for (binding, multidoc) in meta(category.module_owner) + binding.var in (:description, :earth_impedance, :earth_potential_coefficient, + :source_coefficients) || continue + for docstring in values(multidoc.docs) + dirname(String(docstring.data[:path])) == directory || continue + push!(formula_docs, docstring) + end + end + + documented_files = sort(unique(map( + docstring -> basename(String(docstring.data[:path])), + formula_docs + ))) + @test documented_files == formula_files + + # Each registered choice must produce a real description. Whether its + # sole authority dispatches on an instance or its type is not a docs + # invariant. Indexing the method-signature representation rejected + # legitimate type or instance delegation without checking this behavior. + for identifier in category.registry + selected=category.module_owner.Formula(identifier) + @test formula_id(selected)===(identifier === :default ? + formula_id(selected_default) : identifier) + @test description(selected) isa AbstractString + @test !isempty(description(selected)) + for compact in (false, true) + @test description(selected; compact)==description(typeof(selected); compact) + @test description(selected; compact) isa AbstractString + @test !isempty(description(selected; compact)) + end + end + @test !applicable(description, category.module_owner.Formula{:UnregisteredDescriptionTest}) + + for docstring in formula_docs + @test any( + value -> value isa TypedMethodSignatures, + collect(docstring.text) + ) + end + end + + # Scientific documentation belongs to the implemented equation method. + # Description methods retain only their compact and full labels. + for (owner, name, identifier, kind, first_layer, second_layer) in ( + (LineCableModels.Engine.EarthImpedance, :earth_impedance, :saad1996, :self, 2, 2), + (LineCableModels.Engine.EarthImpedance, + :earth_impedance, :wedepohl1973, :self, 2, 2), + (LineCableModels.Engine.EarthImpedance, :earth_impedance, :gary1976, :self, 1, 1), + (LineCableModels.Engine.EarthImpedance, + :earth_impedance, :lucca1994, :mutual, 1, 2), + (LineCableModels.Engine.EarthAdmittance, + :earth_potential_coefficient, :ideal, :self, 1, 1)) + signature = Tuple{owner.Formula{identifier}, Val{kind}, + Val{first_layer}, Val{second_layer}, Any, Any} + docstring = meta(owner)[Binding(owner, name)].docs[signature] + @test basename(String(docstring.data[:path])) == string(identifier, ".jl") + @test !any( + doc -> basename(String(doc.data[:path])) == string(identifier, ".jl"), + values(meta(owner)[Binding(owner, :description)].docs) + ) + end +end + +@testitem "Quality / local shunt formulations" tags = [:quality] begin + const owner = LineCableModels.Engine.ShuntModel + @test LineCableModels.Commons.formulas(owner.Formula) == (:default, :equivalent, :boundary) + for identifier in LineCableModels.Commons.formulas(owner.Formula) + selected = owner.Formula(identifier) + @test formula_id(selected) === (identifier === :default ? :equivalent : identifier) + @test NamedTuple(selected).identifier === formula_id(selected) + for compact in (false, true) + @test !isempty(description(selected; compact)) + @test description(selected; compact) == description(typeof(selected); compact) + end + end +end diff --git a/test/quality/labels.jl b/test/quality/labels.jl new file mode 100644 index 000000000..aad866b9e --- /dev/null +++ b/test/quality/labels.jl @@ -0,0 +1,152 @@ +# Label check. It reads the comments, docstrings and test item names under +# `test/quality`, `test/tools` and `test/support`, and the prose of `test/README.md` and +# `docs/src/developers.md` outside code. A label made of a capital letter and a number, a +# pull request number or a reference to a planning document fails the check. Code, the +# architecture baseline, `src/`, `ext/` and CI are out of scope, because their letters and +# numbers are domain notation. +@testmodule LabelCheck begin + using JuliaSyntax: JuliaSyntax, @K_str + Base.include(@__MODULE__, joinpath(@__DIR__, "..", "support", "taxonomy.jl")) + + const REPOSITORY = dirname(dirname(@__DIR__)) + const DIRECTORIES = ("test/quality", "test/tools", "test/support") + const DOCUMENTS = ("test/README.md", "docs/src/developers.md") + const EXCLUDED = ("test/quality/architecture_baseline.toml",) + const PATTERNS = (r"\b[A-Z][0-9]{1,2}[a-z]?\b", r"\bPR\s*#?[0-9]+", + r"(?i)\b(?:amendment|roadmap)s?\b") + + line_at(source, index) = count(==('\n'), SubString(source, 1, prevind(source, index))) + 1 + + # The prose of a Julia source: comments, docstrings and test item names, each with + # its line and kind. Code and other strings are left out. + function julia_prose(source) + found = Tuple{Int, String, String}[] + for token in JuliaSyntax.tokenize(source) + JuliaSyntax.kind(token) == K"Comment" || continue + push!(found, (line_at(source, first(token.range)), "comment", source[token.range])) + end + line = Ref(0) + visit(x) = if x isa LineNumberNode + line[] = x.line + elseif x isa Expr + if Meta.isexpr(x, :macrocall) && length(x.args) >= 3 && x.args[2] isa LineNumberNode + line[] = x.args[2].line + name = x.args[1] + if name == GlobalRef(Core, Symbol("@doc")) || name === Symbol("@doc") + push!(found, (line[], "docstring", text(x.args[3]))) + elseif name === Symbol("@testitem") && x.args[3] isa String + push!(found, (line[], "test item name", x.args[3])) + end + end + foreach(visit, x.args) + end + visit(Meta.parseall(source)) + return found + end + + # The literal parts of a string or an interpolated string. + text(x::AbstractString) = String(x) + text(x::Expr) = join(text(part) for part in x.args if part isa Union{AbstractString, Expr}) + text(_) = "" + + # The comments of a TOML source. + function toml_prose(source) + found = Tuple{Int, String, String}[] + for (line, raw) in enumerate(split(source, '\n')) + unquoted = replace(raw, r"\"(?:[^\"\\]|\\.)*\"" => "\"\"") + index = findfirst('#', unquoted) + index === nothing || push!(found, (line, "comment", unquoted[index:end])) + end + return found + end + + # The prose of a Markdown source, outside fenced code blocks, code spans and link + # targets. + function markdown_prose(source) + found = Tuple{Int, String, String}[] + fenced = false + for (line, raw) in enumerate(split(source, '\n')) + startswith(lstrip(raw), "```") && (fenced = !fenced; continue) + fenced && continue + prose = replace(raw, r"(`+)(?:(?!\1).)*\1" => " ", r"\]\([^)]*\)" => "]") + isempty(strip(prose)) || push!(found, (line, "prose", prose)) + end + return found + end + + # Each label, pull request number or planning word in `entries`, as a report line. + function findings(path, entries) + found = String[] + for (line, kind, prose) in entries, pattern in PATTERNS, m in eachmatch(pattern, prose) + push!(found, "$path:$line: `$(m.match)` in a $kind") + end + return found + end + + function scope(root = REPOSITORY) + files = String[] + for directory in DIRECTORIES, path in repository_files(joinpath(root, directory)) + file = relpath(path, root) + endswith(file, ".jl") || endswith(file, ".toml") || continue + endswith(file, "Manifest.toml") || file in EXCLUDED || push!(files, file) + end + return sort!(append!(files, DOCUMENTS)) + end + + function live(root = REPOSITORY) + found = String[] + for file in scope(root) + source = read(joinpath(root, file), String) + entries = endswith(file, ".jl") ? julia_prose(source) : + endswith(file, ".toml") ? toml_prose(source) : markdown_prose(source) + append!(found, findings(file, entries)) + end + return found + end +end + +@testitem "Quality / labels / comments, docstrings and documentation name things" tags=[:quality] setup=[LabelCheck] begin + found = LabelCheck.live() + foreach(println, found) + @test found == String[] +end + +@testitem "Quality / labels / negative controls" tags=[:quality] setup=[LabelCheck] begin + L = LabelCheck + # Each probe source has labels in its prose. The check reads only comments, + # docstrings, test item names and documentation prose, never the code of this item. + label, other = string('A', 1), string('C', 4, 'b') + pull, planning = "PR 54", "roadmap" + julia = """ + # Planted comment $(label). + "Planted docstring $(pull)." + f(x) = x + $(label) = 1 + text = "a string $(other) in code" + @testitem "planted $(planning)" begin + #= block $(other) =# + end + """ + @test L.findings("probe.jl", L.julia_prose(julia)) == [ + "probe.jl:1: `$(label)` in a comment", + "probe.jl:7: `$(other)` in a comment", + "probe.jl:2: `$(pull)` in a docstring", + "probe.jl:6: `$(planning)` in a test item name"] + markdown = """ + Prose names $(label). + + Code `$(other)` stays code, and so does a [link](https://example.org/$(label)). + + ```julia + $(label) = 1 + ``` + """ + @test L.findings("probe.md", L.markdown_prose(markdown)) == ["probe.md:1: `$(label)` in a prose"] + toml = "key = \"$(label) # inside a string\" # comment $(pull)\n" + @test L.findings("probe.toml", L.toml_prose(toml)) == ["probe.toml:1: `$(pull)` in a comment"] + # Ordinary words, units and domain names without a capital-number label pass. + @test isempty(L.findings("probe.md", [(1, "prose", + "Float32 inputs, CO2, 5 %, IEC 60287, LineCableModels and the Commons guards")])) + @test L.scope() ⊇ ["docs/src/developers.md", "test/README.md", "test/support/runner.jl"] + @test "test/quality/architecture_baseline.toml" ∉ L.scope() +end diff --git a/test/quality/preservation.jl b/test/quality/preservation.jl new file mode 100644 index 000000000..0f0e9a794 --- /dev/null +++ b/test/quality/preservation.jl @@ -0,0 +1,450 @@ +# The preservation locks record counts per key in `preservation.toml`. Each table +# declares its direction. `[inferred]` is a floor, and `[jet]` and `[allocations]` are +# ceilings. A check fails when a live count passes its limit. It also fails when the +# live count improves on the table, so that the table records the current state. +# Allocation counts may differ from the table by their allowance, and recorded bytes fail +# only on increases beyond their margin. The measured ceilings hold for the Julia +# version and the `Manifest.toml` recorded in `[environment]`. Across commits, +# `test/tools/baseline_ratchet.jl` checks that floors only rise and ceilings only fall. +@testmodule PreservationLocks begin + using TestItemRunner, TOML, SHA + Base.include(@__MODULE__, joinpath(@__DIR__, "..", "support", "runner.jl")) + const T = ValidationTestRunner + const REPOSITORY = dirname(dirname(@__DIR__)) + const TABLES = joinpath(@__DIR__, "preservation.toml") + + table(name) = Dict{String, Int}(get(TOML.parsefile(TABLES), name, Dict{String, Any}())) + isfloor(name) = TOML.parsefile(TABLES)["directions"][name] == "floor" + + # Keys whose live count passes the limit (`broken`) and keys whose live count + # improves on it (`stale`). A missing key counts as zero. + function compare(live, recorded; floor::Bool) + broken, stale = String[], String[] + for key in sort!(collect(union(keys(live), keys(recorded)))) + now, limit = get(live, key, 0), get(recorded, key, 0) + now == limit && continue + push!((now < limit) == floor ? broken : stale, "$key: $now (recorded $limit)") + end + return (; broken, stale) + end + + report(lines) = (foreach(println, lines); lines) + + # The environment of the measured tables: the Julia version and the committed + # `Manifest.toml`, which the CI quality job instantiates. + const MANIFEST = joinpath(REPOSITORY, "Manifest.toml") + environment() = Dict("julia" => string(VERSION), + "manifest" => isfile(MANIFEST) ? bytes2hex(sha256(read(MANIFEST))) : "absent") + describe(e) = "Julia $(e["julia"]) and Manifest $(first(e["manifest"], 12))" + + # The measured tables hold for the recorded environment only. + function check_environment(recorded = TOML.parsefile(TABLES)["environment"], + running = environment()) + recorded == running || error("recorded with $(describe(recorded)), running " * + "$(describe(running)): re-record [jet] and [allocations]") + return nothing + end + + # Allocation ceilings. Measurements stores a partial derivative only when it is + # nonzero, but whether a round-off derivative is exactly zero depends on the machine's + # last bits. So a count may differ from the table by the scenario's allowance, the number of + # uncertain real scalars that its result publishes. The allocation tool computes it + # live. A plain-number scenario has allowance zero and compares exactly. Bytes fail + # above the recorded minimum plus `HEADROOM` for the runtime's byte accounting and the + # allowance times the bytes of one derivative entry. + const HEADROOM = 512 + function compare_allocations(live, recorded, allowances, derivative) + broken, stale = String[], String[] + for key in sort!(collect(union(keys(live), keys(recorded)))) + now, limit = get(live, key, 0), get(recorded, key, 0) + allowance = get(allowances, first(rsplit(key, " | "; limit = 2)), 0) + if endswith(key, "| allocations") + line = "$key: $now (recorded $limit ± $allowance)" + now > limit + allowance && push!(broken, line) + now < limit - allowance && push!(stale, line) + elseif endswith(key, "| bytes") + margin = HEADROOM + allowance*derivative + now > limit + margin && push!(broken, "$key: $now (recorded $limit + $margin)") + end + end + return (; broken, stale) + end + + const ALLOCATIONS = joinpath(REPOSITORY, "test", "tools", "allocations.jl") + + # The `@inferred` floor: the `@inferred` uses in a Julia source, qualified or not. + function inferred_uses(text, filename = "none") + uses = Ref(0) + visit(x) = if x isa Expr + if Meta.isexpr(x, :macrocall) + name = x.args[1] + name isa Expr && Meta.isexpr(name, :.) && (name = name.args[end]) + name isa QuoteNode && (name = name.value) + name === Symbol("@inferred") && (uses[] += 1) + end + foreach(visit, x.args) + end + visit(Meta.parseall(text; filename)) + return uses[] + end + + # The test files with at least one `@inferred` use, and their counts. + function inferred_counts(root) + counts = Dict{String, Int}() + for file in TestItemRunner.find_test_files(root) + startswith(file, joinpath(root, "test", "")) || continue + uses = inferred_uses(read(file, String), file) + uses > 0 && (counts[T.relative(file, root)] = uses) + end + return counts + end +end + +@testitem "Quality / preservation / @inferred floor" tags=[:quality] setup=[PreservationLocks] begin + P = PreservationLocks + result = P.compare(P.inferred_counts(P.REPOSITORY), P.table("inferred"); + floor = P.isfloor("inferred")) + # A file below its floor lost `@inferred` assertions. + @test P.report(result.broken) == String[] + # A file above its floor raises the floor in the same change. + @test P.report(result.stale) == String[] +end + +@testitem "Quality / preservation / JET ceilings on the frequency-loop kernels" tags=[:quality] setup=[PreservationLocks] begin + using JET + P = PreservationLocks + P.check_environment() + E, M, C = LineCableModels.Engine, LineCableModels.ModalAnalysis, LineCableModels.Commons + include(joinpath(P.REPOSITORY, "test", "support", "scenarios.jl")) + using .CurrentScenarios: line_parameters_problem, three_phase_system + + # The default coaxial computation on a cable system, run once so that every buffer + # holds the state of the frequency loop. Each kernel is analysed with the concrete + # arguments of its call in `_solve!`. + problem = line_parameters_problem(three_phase_system(); frequencies = [50.0, 1000.0]) + formulation = Formulation() + blueprint = only(E.flatten(LineCableModelsCoaxial(), problem.system.designs, + eltype(problem), [formulation])) + execution = E.computation_options(LineCableModelsCoaxial, ComputationOptions()) + workspace = E.LineParametersWorkspace(problem, formulation, execution, + E.lineinput(problem, blueprint)) + E._solve!(workspace, formulation) + buffers, plan, input = workspace.buffers, workspace.plan, workspace.input + jω = input.jω[1] + found = Dict{String, Vector{Any}}() + found["coaxial _solve!"] = JET.get_reports( + @report_opt target_modules=(LineCableModels,) E._solve!(workspace, formulation)) + found["cable_impedance!"] = JET.get_reports( + @report_opt target_modules=(LineCableModels,) E.cable_impedance!(buffers.Zprimitive, + input.cable, buffers.rho_cond, formulation.methods, jω; workspace)) + found["cable_potential!"] = JET.get_reports( + @report_opt target_modules=(LineCableModels,) E.cable_potential!(buffers.Pprimitive, + input.cable, buffers.dielectric_admittivity, jω, buffers.layer_coefficients, + buffers.coefficients, buffers.tails)) + found["earth!"] = JET.get_reports( + @report_opt target_modules=(LineCableModels,) E.earth!(workspace, 1, + plan.earth.calculations, buffers.earth.calculations)) + found["reduce_line_matrices!"] = JET.get_reports( + @report_opt target_modules=(LineCableModels,) C.reduce_line_matrices!( + view(buffers.Zout, :, :, 1), view(buffers.Yout, :, :, 1), buffers.Zprimitive, + buffers.Pprimitive, jω, plan.reduction, buffers.reduction)) + phase = compute(problem, formulation) + for id in (:chrysochos2014, :vieira2026, :wedepohl1996) + selected = ModalAnalysisFormulation(id).formula + modal = M.ModalAnalysisWorkspace(phase, selected) + found["decompose! $id"] = JET.get_reports( + @report_opt target_modules=(LineCableModels,) M.decompose!( + selected, modal, M.formula_parameters(selected), + M.formulation_options(selected))) + end + for (kernel, reports) in sort!(collect(found); by = first), report in reports + println(kernel, ": ", first(split(sprint(show, report), '\n'))) + end + result = P.compare(Dict(k => length(v) for (k, v) in found), P.table("jet"); + floor = P.isfloor("jet")) + # A kernel above its ceiling has a new optimization report. + @test P.report(result.broken) == String[] + # A kernel below its ceiling lowers the ceiling in the same change. + @test P.report(result.stale) == String[] +end + +@testitem "Quality / preservation / allocation ceilings" tags=[:quality] setup=[PreservationLocks] begin + using TOML + P = PreservationLocks + P.check_environment() + # A fresh process runs the corpus in a fixed order: the counts depend on what the + # process computed before. + output, errors = IOBuffer(), IOBuffer() + command = `$(Base.julia_cmd()) --project=$(dirname(Base.active_project())) $(P.ALLOCATIONS)` + process = run(pipeline(ignorestatus(command); stdout = output, stderr = errors)) + text = String(take!(output)) + print(text) + # A nonzero exit reports allocation counts that varied across calls. + success(process) || print(String(take!(errors))) + @test success(process) + measured = TOML.parse(text) + live = Dict{String, Int}(measured["allocations"]) + result = P.compare_allocations(live, P.table("allocations"), + Dict{String, Int}(measured["allowances"]), measured["derivative"]["bytes"]) + # A row above its ceiling allocates more. + @test P.report(result.broken) == String[] + # A row whose counts fell lowers its ceiling, and re-records its bytes, in the same change. + @test P.report(result.stale) == String[] +end + +@testitem "Quality / preservation / negative controls" tags=[:quality] setup=[PreservationLocks] begin + P = PreservationLocks + # `@inferred` counts bare and qualified uses, nested ones too, but not text. + source = """ + @inferred f(1) + @test (@inferred g()) == 1 + Test.@inferred h(2) + # @inferred in a comment + text = "@inferred in a string" + """ + @test P.inferred_uses(source) == 3 + @test P.inferred_uses("x = 1") == 0 + + # A floor breaks below its limit and a ceiling above it. Improvements are stale. + floor = P.compare(Dict("a" => 2, "b" => 5, "new" => 1), Dict("a" => 3, "b" => 4, "gone" => 1); + floor = true) + @test floor.broken == ["a: 2 (recorded 3)", "gone: 0 (recorded 1)"] + @test floor.stale == ["b: 5 (recorded 4)", "new: 1 (recorded 0)"] + ceiling = P.compare(Dict("a" => 2, "b" => 5), Dict("a" => 3, "b" => 4); floor = false) + @test ceiling.broken == ["b: 5 (recorded 4)"] + @test ceiling.stale == ["a: 2 (recorded 3)"] + @test P.compare(Dict("a" => 1), Dict("a" => 1); floor = true) == (; broken = String[], stale = String[]) + + # The ratchet follows each declared direction across commits. + ratchet = Module(:BaselineRatchet) + Base.include(ratchet, joinpath(P.REPOSITORY, "test", "tools", "baseline_ratchet.jl")) + mktempdir() do repository + git(arguments...) = run(pipeline(Base.invokelatest(ratchet.git, repository, + "-c", "user.name=Probe", "-c", "user.email=probe@example.invalid", + arguments...); stdout = devnull, stderr = devnull)) + function locks(inferred, jet; directions = "inferred = \"floor\"\njet = \"ceiling\"\n") + path = joinpath(repository, ratchet.PRESERVATION) + mkpath(dirname(path)) + write(path, "[directions]\n" * directions * + "[inferred]\n" * join(("\"$k\" = $v\n" for (k, v) in inferred)) * + "[jet]\n" * join(("\"$k\" = $v\n" for (k, v) in jet))) + end + moved() = Base.invokelatest(ratchet.moved, repository, "HEAD", ratchet.PRESERVATION).lines + git("init", "-q") + mkpath(joinpath(repository, "test")) + write(joinpath(repository, "test", "a.jl"), "@inferred f()\n") + locks(["test/a.jl" => 3], ["kernel" => 2]) + git("add", "-A") + git("commit", "-q", "--no-verify", "--no-gpg-sign", "-m", "start") + locks(["test/a.jl" => 4, "test/new.jl" => 1], ["kernel" => 1]) + @test moved() == String[] + locks(["test/a.jl" => 2], ["kernel" => 2]) + @test moved() == ["inferred | test/a.jl: 2 (3 at HEAD)"] + locks(Pair{String, Int}[], ["kernel" => 3, "other" => 1]) + @test moved() == ["inferred | test/a.jl: removed (3 at HEAD)", + "jet | kernel: 3 (2 at HEAD)", "jet | other: 1 (absent at HEAD)"] + # An `@inferred` floor can fall when its lines move to another file, unchanged + # apart from indentation. A deleted line, an edited line, a line re-indented in the + # same file and a drop beyond the moved lines fail. + source, target = joinpath(repository, "test", "a.jl"), joinpath(repository, "test", "c.jl") + write(source, "") + write(target, " @inferred f()\n") + locks(["test/a.jl" => 2, "test/c.jl" => 1], ["kernel" => 2]) + @test moved() == String[] + locks(["test/c.jl" => 1], ["kernel" => 2]) + @test moved() == ["inferred | test/a.jl: removed (3 at HEAD)"] + locks(["test/a.jl" => 2, "test/c.jl" => 1], ["kernel" => 2]) + write(target, "@inferred f(1)\n") + @test moved() == ["inferred | test/a.jl: 2 (3 at HEAD)"] + rm(target) + @test moved() == ["inferred | test/a.jl: 2 (3 at HEAD)"] + write(source, " @inferred f()\n") + @test moved() == ["inferred | test/a.jl: 2 (3 at HEAD)"] + write(source, "@inferred f()\n") + # A floor key follows a file renamed by git. + git("mv", "test/a.jl", "test/b.jl") + locks(["test/b.jl" => 3], ["kernel" => 2]) + @test moved() == String[] + locks(["test/b.jl" => 3], ["kernel" => 2]; directions = "inferred = \"floor\"\n") + @test_throws ErrorException moved() + # A measured ceiling may rise only together with its recorded environment. + function measured(jet, allocations, julia; manifest = "a") + write(joinpath(repository, ratchet.PRESERVATION), + "[environment]\njulia = \"$julia\"\nmanifest = \"$manifest\"\n" * + "[directions]\ninferred = \"floor\"\n" * + "jet = \"ceiling\"\nallocations = \"ceiling\"\n[inferred]\n\"test/b.jl\" = 3\n" * + "[jet]\nkernel = $jet\n[allocations]\n\"s | 2 frequencies | allocations\" = $allocations\n") + end + measured(2, 10, "1.12.7") + git("add", "-A") + git("commit", "-q", "--no-verify", "--no-gpg-sign", "-m", "measured") + measured(3, 11, "1.12.7") + @test moved() == ["allocations | s | 2 frequencies | allocations: 11 (10 at HEAD)", + "jet | kernel: 3 (2 at HEAD)"] + measured(3, 11, "1.12.8") + @test moved() == String[] + measured(3, 11, "1.12.7"; manifest = "b") + @test moved() == String[] + # A floor stays a floor across a version change. + write(joinpath(repository, ratchet.PRESERVATION), + replace(read(joinpath(repository, ratchet.PRESERVATION), String), + "\"test/b.jl\" = 3" => "\"test/b.jl\" = 2")) + @test moved() == ["inferred | test/b.jl: 2 (3 at HEAD)"] + # Each allocation key is a corpus scenario. New scenarios pass while every scenario + # at HEAD keeps its key, and a key that replaces another is a rename that fails. + function scenarios(rows) + write(joinpath(repository, ratchet.PRESERVATION), + "[environment]\njulia = \"1.12.7\"\nmanifest = \"a\"\n" * + "[directions]\ninferred = \"floor\"\n" * + "jet = \"ceiling\"\nallocations = \"ceiling\"\n[inferred]\n\"test/b.jl\" = 3\n" * + "[jet]\nkernel = 2\n[allocations]\n" * join(("\"$k\" = $v\n" for (k, v) in rows))) + end + scenarios(["s | 2 frequencies | allocations" => 10, "t | 2 frequencies | allocations" => 7]) + @test moved() == String[] + scenarios(["t | 2 frequencies | allocations" => 7]) + @test moved() == ["allocations | t | 2 frequencies | allocations: 7 (absent at HEAD)"] + end + + # The measured tables refuse another Julia version or another Manifest. + recorded = Dict("julia" => "1.12.7", "manifest" => "0123456789abcdef") + @test P.check_environment(recorded, copy(recorded)) === nothing + @test_throws ErrorException("recorded with Julia 1.12.7 and Manifest 0123456789ab, running " * + "Julia 1.12.8 and Manifest 0123456789ab: re-record [jet] and [allocations]") P.check_environment( + recorded, merge(recorded, Dict("julia" => "1.12.8"))) + @test_throws ErrorException P.check_environment(recorded, merge(recorded, Dict("manifest" => "fedcba"))) + + # A scenario without uncertain results compares exactly in both directions. A scenario + # with uncertain results can move by its allowance in objects. In bytes, it can exceed + # the headroom by the allowance times one derivative entry. + recorded = Dict("s | 2 frequencies | allocations" => 10, "s | 2 frequencies | bytes" => 1000) + row(n, b) = Dict("s | 2 frequencies | allocations" => n, "s | 2 frequencies | bytes" => b) + exact, derivative = Dict("s | 2 frequencies" => 0), 48 + compare(n, b, allowances = exact) = P.compare_allocations(row(n, b), recorded, allowances, derivative) + @test compare(10, 1000 + P.HEADROOM) == (; broken = String[], stale = String[]) + @test compare(10, 900) == (; broken = String[], stale = String[]) + @test compare(10, 1001 + P.HEADROOM).broken == + ["s | 2 frequencies | bytes: 1513 (recorded 1000 + 512)"] + @test compare(11, 1000).broken == ["s | 2 frequencies | allocations: 11 (recorded 10 ± 0)"] + @test compare(9, 1000).stale == ["s | 2 frequencies | allocations: 9 (recorded 10 ± 0)"] + # A scenario that the allowances do not list compares exactly. + @test P.compare_allocations(row(11, 1000), recorded, Dict{String, Int}(), derivative).broken == + ["s | 2 frequencies | allocations: 11 (recorded 10 ± 0)"] + uncertain = Dict("s | 2 frequencies" => 3) + margin = P.HEADROOM + 3*derivative + @test compare(13, 1000 + margin, uncertain) == (; broken = String[], stale = String[]) + @test compare(7, 1000, uncertain) == (; broken = String[], stale = String[]) + @test compare(14, 1000, uncertain).broken == ["s | 2 frequencies | allocations: 14 (recorded 10 ± 3)"] + @test compare(6, 1000, uncertain).stale == ["s | 2 frequencies | allocations: 6 (recorded 10 ± 3)"] + @test compare(10, 1001 + margin, uncertain).broken == + ["s | 2 frequencies | bytes: $(1001 + margin) (recorded 1000 + $margin)"] + + # The allocation tool reports object counts that vary across calls, and records the + # minimum bytes. + tool = Module(:AllocationTool) + Base.include(tool, P.ALLOCATIONS) + rows(objects, bytes) = Base.invokelatest(tool.scenario_rows, "s", 2, (; objects, bytes)) + @test rows([5, 5, 5], [12, 10, 11]) == + (["s | 2 frequencies | allocations" => 5, "s | 2 frequencies | bytes" => 10], nothing) + @test last(rows([5, 6, 5], [10, 10, 10])) == + "s | 2 frequencies: allocations [5, 6, 5] across calls" + # An allowance counts uncertain real and imaginary parts by type. An exact uncertain + # value counts, and so does a plain entry that an uncertain array promotes. One + # derivative entry takes a positive number of bytes. + in_tool(f, arguments...) = Base.invokelatest(getfield(tool, f), arguments...) + exact_uncertain = in_tool(:measurement, 1.0, 0.0) + @test in_tool(:allowance, complex(exact_uncertain, exact_uncertain)) == 2 + @test in_tool(:allowance, [complex(exact_uncertain, exact_uncertain), 1.0 + 2.0im]) == 4 + @test in_tool(:allowance, zeros(ComplexF64, 3)) == 0 + @test in_tool(:derivative_bytes) > 0 + + # The equivalence check reads declarations and renames, classifies differences by kind + # and finds the revision's own files. + equivalence = Module(:EquivalenceTool) + Base.include(equivalence, joinpath(P.REPOSITORY, "test", "tools", "equivalence.jl")) + in_equivalence(f, arguments...; keywords...) = + Base.invokelatest(getfield(equivalence, f), arguments...; keywords...) + mktempdir() do directory + file = joinpath(directory, "allowed") + write(file, "# Declared differences\nrename | Grammar => Commons\ncoaxial | | value\n" * + "modal | result.Z | type # a path prefix\n\n") + @test in_equivalence(:declarations, file) == (; renames = ["Grammar" => "Commons"], declared = [ + (; scenario = "coaxial", prefix = "", kind = :value), + (; scenario = "modal", prefix = "result.Z", kind = :type)]) + for malformed in ("coaxial | result", "coaxial | result | values", "rename | Grammar", + "rename | Grammar => Commons.Types") + write(file, malformed * "\n") + @test_throws ErrorException in_equivalence(:declarations, file) + end + end + # A rename changes whole identifiers in type names, path segments and Symbol values. + nodes = ["result.Grammar.x" => ("Grammar.FormulaDefinition{:Grammar}", ""), + "result.y" => ("Symbol", ":Grammar"), "result.z" => ("String", "\"Grammar\""), + "result.w" => ("MyGrammar.GrammarX", ""), "result.eltype" => ("Type", "Grammar.Point")] + used = falses(2) + @test in_equivalence(:renamed, nodes, ["Grammar" => "Commons", "Absent" => "Present"], used) == [ + "result.Commons.x" => ("Commons.FormulaDefinition{:Commons}", ""), + "result.y" => ("Symbol", ":Commons"), "result.z" => ("String", "\"Grammar\""), + "result.w" => ("MyGrammar.GrammarX", ""), "result.eltype" => ("Type", "Commons.Point")] + # The second rename is unused, and the report says so. + @test used == [true, false] + # Differences come in recorded order with their kind. A declaration covers its kind only. + before = ["a" => ("Float64", "01"), "b" => ("Int64", "1"), "gone" => ("Int64", "1")] + after = ["a" => ("Float64", "02"), "b" => ("Int32", "1"), "added" => ("Int64", "1")] + changes = in_equivalence(:differences, before, after) + @test first.(changes) == ["a", "b", "added", "gone"] + @test [in_equivalence(:kind, c) for c in changes] == [:value, :type, :path, :path] + @test isempty(in_equivalence(:differences, before, before)) + value = (; scenario = "s", prefix = "", kind = :value) + @test in_equivalence(:covers, value, "s", changes[1]) + @test !in_equivalence(:covers, value, "s", changes[2]) + @test in_equivalence(:covers, (; scenario = "s", prefix = "b", kind = :type), "s", changes[2]) + @test !in_equivalence(:covers, value, "other", changes[1]) + # The timing comparison pins both workers to one performance core of a hybrid machine, + # never CPU 0 or its sibling. It grades each scenario and sets its exit status. + timing = Module(:TimingTool) + Base.include(timing, joinpath(P.REPOSITORY, "test", "tools", "performance.jl")) + in_timing(f, arguments...; keywords...) = + Base.invokelatest(getfield(timing, f), arguments...; keywords...) + @test in_timing(:cpu_list, "0-3,8\n") == [0, 1, 2, 3, 8] + mktempdir() do directory + performance, siblings = joinpath(directory, "cpus"), joinpath(directory, "siblings") + write(performance, "0-15\n") + write(siblings, "0-1\n") + @test in_timing(:shared_cpu; taskset = "taskset", performance, siblings, cpus = 32) == 2 + @test in_timing(:shared_cpu; taskset = "taskset", performance = joinpath(directory, "none"), + siblings = joinpath(directory, "none"), cpus = 4) == 1 + @test in_timing(:shared_cpu; taskset = nothing, performance, siblings, cpus = 32) === nothing + write(performance, "0-1\n") + @test in_timing(:shared_cpu; taskset = "taskset", performance, siblings, cpus = 32) === nothing + end + @test in_timing(:spread, [100.0, 101.0, 102.0]) ≈ 0.02 + # A stable scenario more than 10 % slower is a slowdown, one 5 to 10 % slower a possible + # slowdown below the tool's resolution. A round spread above 2 % makes it unstable. + stable = (0.01, 0.015) + @test in_timing(:verdict, 0.11, stable) === :slower + @test in_timing(:verdict, 0.07, stable) === :possible + @test in_timing(:verdict, 0.05, stable) === :same + @test in_timing(:verdict, -0.2, stable) === :same + @test in_timing(:verdict, 0.2, (0.021, 0.01)) === :unstable + # Exit 1 for a slowdown or a failure, otherwise 2 for a possible slowdown or an + # unstable scenario, otherwise 0. + @test in_timing(:status, [:same, :slower, :unstable]) == 1 + @test in_timing(:status, [:same, :failed]) == 1 + @test in_timing(:status, [:same, :possible]) == 2 + @test in_timing(:status, [:same, :unstable]) == 2 + @test in_timing(:status, [:same, :same]) == 0 + + # A revision without its own copy uses the working tree's. + mktempdir() do revision + corpus = joinpath("test", "support", "scenarios.jl") + working = joinpath(P.REPOSITORY, corpus) + marker = "function preservation_corpus(" + @test in_equivalence(:own_copy, revision, corpus; marker) == (; path = working, own = false) + mkpath(dirname(joinpath(revision, corpus))) + write(joinpath(revision, corpus), "module CurrentScenarios end\n") + @test in_equivalence(:own_copy, revision, corpus; marker) == (; path = working, own = false) + write(joinpath(revision, corpus), "function preservation_corpus(n) end\n") + @test in_equivalence(:own_copy, revision, corpus; marker) == + (; path = joinpath(revision, corpus), own = true) + end +end diff --git a/test/quality/preservation.toml b/test/quality/preservation.toml new file mode 100644 index 000000000..585cf0f59 --- /dev/null +++ b/test/quality/preservation.toml @@ -0,0 +1,132 @@ +# Preservation locks, checked by `test/quality/preservation.jl`. Each table declares +# its direction: a floor may only rise and a ceiling may only fall, which +# `test/tools/baseline_ratchet.jl REF` checks across commits. The quality checks +# require the counts to equal the live counts, within the tolerance of [allocations]. + +# The measured ceilings ([jet], [allocations]) hold for this Julia version and the +# committed `Manifest.toml` with this SHA-256 only. An increase in them is accepted only +# together with a change of this environment. +[environment] +julia = "1.12.7" +manifest = "5ecf11c9fd831e437ae143e2840cfd570f5108949b804048957067e9453f76cd" + +[directions] +inferred = "floor" +jet = "ceiling" +allocations = "ceiling" + +# Floor: `@inferred` uses per test file. +[inferred] +"test/extensions/measurements.jl" = 2 +"test/integration/cable_constants.jl" = 3 +"test/integration/computation_options.jl" = 1 +"test/integration/internal_shunt.jl" = 1 +"test/integration/line_parameters.jl" = 3 +"test/integration/shunt_model.jl" = 2 +"test/unit/commons/gridspace.jl" = 10 +"test/unit/commons/observation_quantity.jl" = 9 +"test/unit/commons/owned_records.jl" = 7 +"test/unit/datamodel/annular_strand.jl" = 2 +"test/unit/datamodel/assembly_lowering.jl" = 2 +"test/unit/datamodel/baseparams/geometry.jl" = 1 +"test/unit/datamodel/geometry_v1.jl" = 2 +"test/unit/datamodel/physical_tree_v1.jl" = 2 +"test/unit/datamodel/placement_geometry.jl" = 7 +"test/unit/earth/properties.jl" = 2 +"test/unit/engine/analytical_earth_impedance.jl" = 3 +"test/unit/engine/constitutive_defaults.jl" = 2 +"test/unit/engine/earth_inference.jl" = 4 +"test/unit/engine/formula_architecture.jl" = 1 +"test/unit/engine/formulations.jl" = 7 +"test/unit/engine/ideal_earth_potential.jl" = 2 +"test/unit/engine/insulation_admittance.jl" = 2 +"test/unit/engine/internal_selections.jl" = 2 +"test/unit/engine/internal_shunt.jl" = 2 +"test/unit/engine/line_observations.jl" = 5 +"test/unit/engine/observations.jl" = 3 +"test/unit/engine/options.jl" = 4 +"test/unit/engine/pipe_formulations.jl" = 1 +"test/unit/engine/radial_dielectric.jl" = 3 +"test/unit/engine/result_containers.jl" = 3 +"test/unit/engine/transforms.jl" = 2 +"test/unit/engine/unified_earth_return.jl" = 1 +"test/unit/materials/temperaturedependent.jl" = 1 +"test/unit/modalanalysis/vieira2026.jl" = 2 +"test/unit/modalanalysis/wedepohl1996.jl" = 3 +"test/unit/parametricbuilder/architecture.jl" = 2 +"test/unit/parametricbuilder/construction_api.jl" = 1 +"test/unit/parametricbuilder/gridspace.jl" = 4 +"test/unit/parametricbuilder/placement_composition.jl" = 6 +"test/unit/parametricbuilder/region_grid.jl" = 3 +"test/unit/reportbuilder/histogram_observations.jl" = 1 +"test/unit/reportbuilder/uq_comparisons.jl" = 3 +"test/unit/textdisplay/policy.jl" = 2 +"test/unit/uq/formulations.jl" = 9 +# Ceiling: JET optimization reports per frequency-loop kernel, with +# `target_modules = (LineCableModels,)` on the default coaxial computation of a +# three-phase cable system and on each modal formula. The current reports come from the +# known boxing of earth-return records. The abstract `earth_calculations` tuple dispatches +# at the `_solve!` barrier, and `earth!` recurses over the record tuple. +[jet] +"cable_impedance!" = 0 +"cable_potential!" = 0 +"coaxial _solve!" = 1 +"decompose! chrysochos2014" = 0 +"decompose! vieira2026" = 0 +"decompose! wedepohl1996" = 0 +"earth!" = 1 +"reduce_line_matrices!" = 0 + +# Ceiling: allocations of the preservation corpus (`CurrentScenarios.preservation_corpus`), +# printed by `test/tools/allocations.jl`. `allocations` counts the allocated objects of one +# warmed-up `compute` call, which repeat exactly on every call. Measurements stores a +# partial derivative only when it is nonzero, and whether a round-off derivative is exactly +# zero depends on the machine. A count may differ from its row by the scenario's allowance, +# the uncertain real scalars of its result, which the tool computes and this table does not +# record. `bytes` is the minimum over three calls and may grow by 512 B plus the allowance +# times one derivative entry. Record a row's bytes again whenever its counts change. +[allocations] +"coaxial | 2 frequencies | allocations" = 6886 +"coaxial | 2 frequencies | bytes" = 389408 +"overhead | 2 frequencies | allocations" = 5398 +"overhead | 2 frequencies | bytes" = 292912 +"buried | 2 frequencies | allocations" = 5414 +"buried | 2 frequencies | bytes" = 295152 +"modal | 2 frequencies | allocations" = 7772 +"modal | 2 frequencies | bytes" = 447424 +"cable_constants | 2 frequencies | allocations" = 2311 +"cable_constants | 2 frequencies | bytes" = 120704 +"product | 2 frequencies | allocations" = 55913 +"product | 2 frequencies | bytes" = 3028144 +"zip | 2 frequencies | allocations" = 28041 +"zip | 2 frequencies | bytes" = 1518704 +"linear_error | 2 frequencies | allocations" = 629930 +"linear_error | 2 frequencies | bytes" = 30504480 +"monte_carlo | 2 frequencies | allocations" = 43255 +"monte_carlo | 2 frequencies | bytes" = 2502528 +"three_layer | 2 frequencies | allocations" = 7392 +"three_layer | 2 frequencies | bytes" = 421936 +"three_layer_before_fd | 2 frequencies | allocations" = 7697 +"three_layer_before_fd | 2 frequencies | bytes" = 434272 +"coaxial | 4 frequencies | allocations" = 6924 +"coaxial | 4 frequencies | bytes" = 394608 +"overhead | 4 frequencies | allocations" = 5412 +"overhead | 4 frequencies | bytes" = 295552 +"buried | 4 frequencies | allocations" = 5444 +"buried | 4 frequencies | bytes" = 300032 +"modal | 4 frequencies | allocations" = 7922 +"modal | 4 frequencies | bytes" = 469712 +"cable_constants | 4 frequencies | allocations" = 2311 +"cable_constants | 4 frequencies | bytes" = 120704 +"product | 4 frequencies | allocations" = 56217 +"product | 4 frequencies | bytes" = 3069680 +"zip | 4 frequencies | allocations" = 28193 +"zip | 4 frequencies | bytes" = 1539472 +"linear_error | 4 frequencies | allocations" = 1229385 +"linear_error | 4 frequencies | bytes" = 59291600 +"monte_carlo | 4 frequencies | allocations" = 44071 +"monte_carlo | 4 frequencies | bytes" = 2566960 +"three_layer | 4 frequencies | allocations" = 7458 +"three_layer | 4 frequencies | bytes" = 438880 +"three_layer_before_fd | 4 frequencies | allocations" = 7731 +"three_layer_before_fd | 4 frequencies | bytes" = 437936 diff --git a/test/quality/relint.jl b/test/quality/relint.jl new file mode 100644 index 000000000..5ca71fe8a --- /dev/null +++ b/test/quality/relint.jl @@ -0,0 +1,260 @@ +@testitem "Quality / ReLint / advisory source patterns" tags=[:quality] default_imports=false begin + using Test, ReLint, Argus, JuliaSyntax + using JuliaSyntax: children, numchildren, is_leaf, is_prefix_call + + # Resolve spelling only, never the ownership of a source binding. + function syntax_name(node) + kind(node) in (K"Identifier", K"MacroName") && return (node.val,) + if kind(node) == K"." && numchildren(node) == 2 + member = node[2] + kind(member) == K"quote" && numchildren(member) == 1 && (member = member[1]) + left, right = syntax_name(node[1]), syntax_name(member) + !isempty(left) && length(right) == 1 && return (left..., right...) + end + return () + end + + function execution_context(node) + ancestors = typeof(node)[] + parent = node.parent + while parent !== nothing + push!(ancestors, parent) + parent = parent.parent + end + quoted, in_function, opaque, in_type = 0, false, false, false + for i in length(ancestors):-1:1 + ancestor = ancestors[i] + child = i == 1 ? node : ancestors[i - 1] + k = kind(ancestor) + if k == K"quote" + quoted += 1 + elseif k == K"$" && quoted > 0 + quoted -= 1 + elseif quoted == 0 + if k in (K"function", K"->", K"do") && numchildren(ancestor) >= 2 + # Signatures and default expressions are outside this rule's scope. + in_function |= child === children(ancestor)[end] + elseif k == K"macrocall" + # Docstrings wrap declarations without generating their bodies. + name = syntax_name(ancestor[1]) + opaque |= name ∉ ((Symbol("@doc"),), (:Core, Symbol("@doc"))) + elseif k == K"struct" + in_type = true + end + end + end + return (; quoted=quoted > 0, in_function, opaque, in_type) + end + + function runtime_eval(node) + k = kind(node) + k in (K"call", K"macrocall") && numchildren(node) > 0 || return false + k == K"call" && !is_prefix_call(node) && return false + syntax_name(node[1]) in ((:eval,), (:Base, :eval), (:Core, :eval), + (Symbol("@eval"),), (:Base, Symbol("@eval")), (:Core, Symbol("@eval"))) || return false + context = execution_context(node) + return context.in_function && !context.quoted && !context.opaque + end + + function constant_catch_result(node) + kind(node) == K"catch" || return false + context = execution_context(node) + (context.quoted || context.opaque) && return false + body = children(node)[end] + while kind(body) == K"block" + numchildren(body) == 0 && return true + numchildren(body) == 1 || return false + body = body[1] + end + if kind(body) == K"return" + numchildren(body) == 0 && return true + numchildren(body) == 1 || return false + body = body[1] + end + syntax_name(body) == (:nothing,) && return true + is_leaf(body) && body.val isa Union{Bool,Number} && return true + # Negative numeric literals have a unary operator node in Julia syntax. + return kind(body) == K"call" && numchildren(body) == 2 && + syntax_name(body[1]) in ((:+,), (:-,)) && + is_leaf(body[2]) && body[2].val isa Number + end + + # Only declarations visible in the enclosing source scope identify local + # constructors. No package code is loaded to resolve names or aliases. + function declares_type(node, name) + if kind(node) == K"macrocall" && + syntax_name(node[1]) in ((Symbol("@doc"),), (:Core, Symbol("@doc"))) + return declares_type(children(node)[end], name) + end + if kind(node) in (K"struct", K"abstract", K"primitive") + declaration = node[1] + while kind(declaration) in (K"curly", K"<:") + declaration = declaration[1] + end + return syntax_name(declaration) == (name,) + end + kind(node) in (K"toplevel", K"block", K"if", K"elseif", K"else") || return false + return any(child -> declares_type(child, name), children(node)) + end + + function private_forwarder(node) + kind(node) == K"function" && numchildren(node) == 2 || return false + context = execution_context(node) + (context.quoted || context.opaque || context.in_type) && return false + signature = node[1] + while kind(signature) == K"where" + signature = signature[1] + end + kind(signature) == K"call" && is_prefix_call(signature) || return false + name = syntax_name(signature[1]) + length(name) == 1 && startswith(String(only(name)), "_") || return false + bindings = Symbol[] + for argument in children(signature)[2:end] + if kind(argument) == K"::" && numchildren(argument) == 2 + argument = argument[1] + end + kind(argument) == K"Identifier" || return false + all(==('_'), String(argument.val)) && return false + push!(bindings, argument.val) + end + allunique(bindings) || return false + body = node[2] + while kind(body) == K"block" && numchildren(body) == 1 + body = body[1] + end + kind(body) == K"return" && numchildren(body) == 1 && (body = body[1]) + kind(body) == K"call" && is_prefix_call(body) || return false + target = syntax_name(body[1]) + isempty(target) && return false + last(target) == only(name) && return false + # This pattern excludes known conversion spellings and built-in type + # constructors without resolving arbitrary source aliases. + last(target) in (:convert, :oftype, :reinterpret, :cconvert, :unsafe_convert) && return false + if length(target) == 1 || first(target) in (:Base, :Core) + any(owner -> isdefined(owner, last(target)) && getfield(owner, last(target)) isa Type, + (Base, Core)) && return false + end + scope = node.parent + while scope.parent !== nothing && kind(scope.parent) != K"module" + scope = scope.parent + end + (declares_type(scope, only(name)) || declares_type(scope, last(target))) && return false + forwarded = children(body)[2:end] + return length(forwarded) == length(bindings) && + all(pair -> kind(pair[1]) == K"Identifier" && pair[1].val == pair[2], + zip(forwarded, bindings)) + end + + # Argus compiles conditions in its own evaluation context. Explicit references + # keep the predicates in this test item's module, without registering globals + # or calling dependency internals. ReLint supplies parsing and rule traversal. + rules = [ + Argus.Rule("runtime-eval", + "Runtime evaluation candidate; spelling does not establish binding ownership.", + Argus.Pattern(Argus.SyntaxPatternNode(:(~and({node}, + ~when([:node], $(GlobalRef(@__MODULE__, :runtime_eval))(node.src))))))), + Argus.Rule("constant-catch-result", + "Swallowed-exception candidate: empty or constant-result catch.", + Argus.Pattern(Argus.SyntaxPatternNode(:(~and({node}, + ~when([:node], $(GlobalRef(@__MODULE__, :constant_catch_result))(node.src))))))), + Argus.Rule("private-forwarder", + "Unchanged forwarding candidate; dispatch purpose has not been determined.", + Argus.Pattern(Argus.SyntaxPatternNode(:(~and({node}, + ~when([:node], $(GlobalRef(@__MODULE__, :private_forwarder))(node.src))))))), + ] + context = ReLint.LintContext(rules) + + @testset "ReLint rule controls" begin + examples = [ + ("runtime-eval", true, "function f(x); eval(x); end"), + ("runtime-eval", true, "f(x) = Base.eval(@__MODULE__, x)"), + ("runtime-eval", true, "f(x) = Core.eval(@__MODULE__, x)"), + ("runtime-eval", true, "f(x) = @eval generated() = 1"), + ("runtime-eval", true, "f = x -> eval(x)"), + ("runtime-eval", true, "map(xs) do x; eval(x); end"), + ("runtime-eval", true, raw"f(x) = :($(eval(x)))"), + ("runtime-eval", false, "@eval generated(x) = x"), + ("runtime-eval", false, "f(x) = :(eval(x))"), + ("runtime-eval", false, "quote; f(x) = eval(x); end"), + ("runtime-eval", false, "f(x) = Other.eval(x)"), + ("runtime-eval", false, "f(x) = evaluator(x)"), + ("constant-catch-result", true, "f() = try work() catch; end"), + ("constant-catch-result", true, "function f(); try work() catch err; return nothing; end; end"), + ("constant-catch-result", true, "f() = try work() catch; false; end"), + ("constant-catch-result", true, "f() = try work() catch; return -1; end"), + ("constant-catch-result", false, "f() = try work() catch; rethrow(); end"), + ("constant-catch-result", false, "f() = try work() catch; cleanup(); nothing; end"), + ("constant-catch-result", false, "f() = try work() catch; fallback(); end"), + ("constant-catch-result", false, "quote; try work() catch; nothing; end; end"), + ("constant-catch-result", false, "stage(::Nothing) = nothing"), + ("private-forwarder", true, "_f(x, y) = g(x, y)"), + ("private-forwarder", true, "function _f(x::T, y) where T; return g(x, y); end"), + ("private-forwarder", true, "_f(α::Real) = Owner.g(α)"), + ("private-forwarder", false, "Base.show(io::IO, x::T) = render(io, x)"), + ("private-forwarder", false, "_f(x) = g(convert(Float64, x))"), + ("private-forwarder", false, "_f(x) = Float64(x)"), + ("private-forwarder", false, "_f(x)::Float64 = g(x)"), + ("private-forwarder", false, "_f(kind::Symbol, x) = _f(Val(kind), x)"), + ("private-forwarder", false, "_f(::Val{:known}, x) = g(x)"), + ("private-forwarder", false, "_f(x, y) = g(y, x)"), + ("private-forwarder", false, "_f(x=1) = g(x)"), + ("private-forwarder", false, "_f(x; y=1) = g(x; y=y)"), + ("private-forwarder", false, "_f(xs...) = g(xs...)"), + ("private-forwarder", false, "_f((x, y)) = g(x, y)"), + ("private-forwarder", false, "struct _T; x; _T(x) = new(x); end"), + ("private-forwarder", false, "struct _T; x; end; _T(x) = g(x)"), + ("private-forwarder", false, "\"A type.\"\nstruct _T; x; end; _T(x) = g(x)"), + ("private-forwarder", false, "struct _T{T}; x::T; end; _T{T}(x) where T = g(x)"), + ("private-forwarder", false, "struct LocalType; x; end; _f(x) = LocalType(x)"), + ("private-forwarder", false, "@inline _f(x) = g(x)"), + ("private-forwarder", false, "quote; _f(x) = g(x); end"), + ("private-forwarder", true, "\"\"\"Value \$(1).\"\"\"\n_f(α) = g(α)"), + ] + for (rule, expected, source) in examples + @testset "$rule: $source" begin + findings = ReLint.lint_text(source; context) + @test count(f -> f.rule.name == rule, findings) == Int(expected) + end + end + unicode = ReLint.lint_text("_α(β) = γ(β)"; context, filename="unicode.jl") + @test only(unicode).file == "unicode.jl" + @test (only(unicode).line, only(unicode).column) == (1, 1) + @test_throws JuliaSyntax.ParseError ReLint.lint_text("function broken("; context) + @test_throws SystemError ReLint.lint_file(joinpath(tempdir(), "absent-relint-$(getpid()).jl"), context) + end + + @testset "ReLint production scan" begin + root = normpath(joinpath(@__DIR__, "..", "..")) + files = String[] + for name in ("src", "ext") + directory = joinpath(root, name) + isdir(directory) || error("Missing production source root: $directory") + for (path, _, names) in walkdir(directory), name in names + endswith(name, ".jl") && push!(files, joinpath(path, name)) + end + end + sort!(files) + isempty(files) && error("Empty production source inventory") + println("ReLint ", pkgversion(ReLint), "; Argus ", pkgversion(Argus), + "; JuliaSyntax ", pkgversion(JuliaSyntax), "; requested source files: ", length(files)) + counts = Dict(rule.name => 0 for rule in rules) + failures = 0 + for file in files + try + for finding in ReLint.lint_file(file, context) + println("ReLint advisory [", finding.rule.name, "] ", relpath(file, root), + ":", finding.line, ":", finding.column, ": ", finding.msg) + counts[finding.rule.name] += 1 + end + catch exception + failures += 1 + println("ReLint scan failure ", relpath(file, root), ": ", sprint(showerror, exception)) + end + end + for rule in sort(collect(keys(counts))) + println("ReLint advisory count [", rule, "]: ", counts[rule]) + end + println("ReLint production scan failures: ", failures) + @test failures == 0 + end +end diff --git a/test/quality/report_stages.jl b/test/quality/report_stages.jl new file mode 100644 index 000000000..2e2338fcb --- /dev/null +++ b/test/quality/report_stages.jl @@ -0,0 +1,25 @@ +@testitem "Quality / report stage order / observed protocol calls" tags=[:quality] setup=[TestFixtures] begin + using Measurements, Statistics + const RB=LineCableModels.ReportBuilder + line=TestFixtures.two_conductor_results() + cable=CableConstants(0.2,3e-6,4e-10,5e-9) + mc=TestFixtures.cable_monte_carlo_result() + cases=((RB.CableConstantsTableDefinition(),ObservedResult(cable)), + (RB.LineParametersTableDefinition((R,L,G,C),:base,:base,:base,false),ObservedResult(line,(R,L,G,C))), + (RB.MonteCarloTableDefinition(:base,nothing),observables(mc,((LineCableModels.UQ.statistics,R,mean),(LineCableModels.UQ.statistics,R,std)))), + (XLSXReportDefinition(),ObservedResult(line)), + (TableReportDefinition((R,)),ObservedResult(cable))) + for (definition,observed) in cases + @test applicable(RB.select,definition,observed) + @test applicable(RB.tabulate,definition,observed) + @test RB.select(definition,observed) !== nothing + @test RB.tabulate(definition,observed) !== nothing + @test which(report,(typeof(definition),typeof(observed))).module === RB + end + benchmark=report(RB.BenchmarkTableDefinition(bands=(:all,)),(reference=line,result=line)) + @test RB.tabulate(RB.BenchmarkTableDefinition(bands=(:all,)),benchmark.observed;reference=benchmark.reference) !== nothing + # Selection and tabulation are required. The remaining stages have explicit defaults. + struct UnimplementedReport <: RB.AbstractReportDefinition end + @test_throws MethodError report(UnimplementedReport(),ObservedResult(line)) + @test_throws MethodError report(UnimplementedReport(),line) +end diff --git a/test/quality/taxonomy.jl b/test/quality/taxonomy.jl new file mode 100644 index 000000000..5497abe85 --- /dev/null +++ b/test/quality/taxonomy.jl @@ -0,0 +1,249 @@ +# Taxonomy guards. Each guard takes the item inventory as an argument, and the controls +# apply it to probe items. `test/support/taxonomy.jl` defines the taxonomy. +@testmodule TaxonomyGuards begin + using TestItemRunner + include(joinpath(@__DIR__, "..", "support", "runner.jl")) + const T = ValidationTestRunner + const REPOSITORY = dirname(dirname(@__DIR__)) + const KNOWN = union(Set(T.OWNERS), Set(T.KINDS), T.ORDINARY_EXCLUDED_TAGS, Set((:slow,))) + + label(item) = string(item.file, " | ", item.name) + + # Owner and kind tags. An item outside `quality` has exactly one owner tag, and a + # `quality` item has none. Each item has a kind tag and only known tags. + function owner_and_kind_tags(items) + found = String[] + for item in items + owners = count(in(T.OWNERS), item.tags) + expected = :quality in item.tags ? 0 : 1 + owners == expected || + push!(found, "$(label(item)): $owners owner tags, expected $expected") + any(in(T.KINDS), item.tags) || push!(found, "$(label(item)): no kind tag") + unknown = setdiff(item.tags, KNOWN) + isempty(unknown) || + push!(found, "$(label(item)): unknown tags $(join(unknown, ", "))") + end + return found + end + + # Unit directories bound the owner. An item under `test/unit//` has the owner of + # `src/` or a later one. + function unit_directory_owners(items, owners) + found = String[] + for item in items + parts = split(item.file, '/') + length(parts) > 3 && parts[1] == "test" && parts[2] == "unit" || continue + floor = T.directory_owner(owners, "src/" * parts[3]) + owner = T.owner_tag(item.tags) + (floor === nothing || owner === nothing) && continue + T.rank(owner) < T.rank(floor) && push!(found, + "$(label(item)): $owner is earlier than $floor, the owner of src/$(parts[3])") + end + return found + end + + report(found) = (foreach(println, found); found) +end + +@testitem "Quality / taxonomy / single definition" tags=[:quality] setup=[TaxonomyGuards] begin + T = TaxonomyGuards.T + @test allunique(T.OWNERS) && allunique(T.KINDS) + # Module owners follow load order, and each core owner covers at least one module. + ranks = map(T.rank, values(T.MODULE_OWNERS)) + @test all(>(0), ranks) && issorted(ranks) + @test Set(values(T.MODULE_OWNERS)) == Set(T.OWNERS[1:T.rank(:pscad)]) + @test Set(values(T.EXTENSION_OWNERS)) == Set(T.OWNERS[T.rank(:pscad)+1:end]) + @test isempty(intersect(Set(T.OWNERS), union(Set(T.KINDS), T.ORDINARY_EXCLUDED_TAGS))) + # Every Julia file under `src/` and `ext/` has an owner. + owners = T.loaded_owners(TaxonomyGuards.REPOSITORY) + for base in ("src", "ext"), file in T.repository_files(joinpath(TaxonomyGuards.REPOSITORY, base)) + path = relpath(file, TaxonomyGuards.REPOSITORY) + @test T.path_owner(owners, path) in T.OWNERS + end +end + +@testitem "Quality / taxonomy / owner and kind tags" tags=[:quality] setup=[TaxonomyGuards] begin + G = TaxonomyGuards + @test G.report(G.owner_and_kind_tags(G.T.inventory(G.REPOSITORY).items)) == String[] +end + +@testitem "Quality / taxonomy / unit directories bound the owner" tags=[:quality] setup=[TaxonomyGuards] begin + G = TaxonomyGuards + owners = G.T.loaded_owners(G.REPOSITORY) + @test G.report(G.unit_directory_owners(G.T.inventory(G.REPOSITORY).items, owners)) == String[] +end + +@testitem "Quality / taxonomy / negative controls" tags=[:quality] setup=[TaxonomyGuards] begin + G = TaxonomyGuards + T = G.T + item(file, name, tags...) = (; file, name, tags = collect(Symbol, tags), setups = Symbol[]) + + # The owner and kind check reports each planted defect once and accepts the valid items. + valid = [item("test/unit/engine/a.jl", "valid", :unit, :engine), + item("test/quality/q.jl", "quality", :quality), + item("test/extensions/x.jl", "slow extension", :extension, :fem_numerical, :fem, :slow)] + @test G.owner_and_kind_tags(valid) == String[] + planted = [item("test/unit/engine/a.jl", "no owner", :unit), + item("test/unit/engine/a.jl", "two owners", :unit, :engine, :uq), + item("test/quality/q.jl", "quality owner", :quality, :engine), + item("test/unit/engine/a.jl", "no kind", :engine), + item("test/unit/engine/a.jl", "unknown", :unit, :engine, :slwo)] + found = G.owner_and_kind_tags(planted) + @test length(found) == 5 + for (probe, text) in zip(planted, ("0 owner tags", "2 owner tags", "1 owner tags", + "no kind tag", "unknown tags slwo")) + @test count(f -> startswith(f, G.label(probe)) && occursin(text, f), found) == 1 + end + + # Owners under `test/unit//` are bounded by the earliest owner loaded from `src/`. + owners = Dict("src/units/Units.jl" => :units, "src/engine/Engine.jl" => :engine, + "src/engine/late.jl" => :uq) + accepted = [item("test/unit/engine/a.jl", "same", :unit, :engine), + item("test/unit/engine/a.jl", "later", :unit, :uq), + item("test/unit/core/a.jl", "no source directory", :unit, :units), + item("test/integration/a.jl", "not a unit directory", :integration, :units), + item("test/unit/a.jl", "top level", :unit, :units)] + @test G.unit_directory_owners(accepted, owners) == String[] + early = item("test/unit/engine/a.jl", "earlier", :unit, :commons) + @test G.unit_directory_owners([early], owners) == + ["$(G.label(early)): commons is earlier than engine, the owner of src/engine"] + + # Files are owned at their load position. A root file takes the owner of the last + # module included before it. A module entry behind a docstring counts as a module. A + # file that the package does not load takes the earliest owner of its directory. + mktempdir() do repository + files = Dict( + "src/LineCableModels.jl" => """ + module LineCableModels + include("first.jl") + include("units/Units.jl") + include("between.jl") + include("engine/Engine.jl") + include("modalanalysis/late.jl") + end + """, + "src/first.jl" => "", "src/between.jl" => "", "src/modalanalysis/late.jl" => "", + "src/units/Units.jl" => "module Units\ninclude(\"inner.jl\")\nend\n", + "src/units/inner.jl" => "", + "src/engine/Engine.jl" => "\"Docstring.\"\nmodule Engine\nend\n", + "ext/LineCableModelsXLSXExt.jl" => "module LineCableModelsXLSXExt\nend\n", + "ext/LineCableModelsGmshExt/LineCableModelsGmshExt.jl" => + "module LineCableModelsGmshExt\ninclude(\"mesh.jl\")\nend\n", + "ext/LineCableModelsGmshExt/mesh.jl" => "") + for (path, text) in files + mkpath(dirname(joinpath(repository, path))) + write(joinpath(repository, path), text) + end + loaded = T.loaded_owners(repository) + @test loaded == Dict("src/LineCableModels.jl" => :units, "src/first.jl" => :units, + "src/units/Units.jl" => :units, "src/units/inner.jl" => :units, + "src/between.jl" => :units, "src/engine/Engine.jl" => :engine, + "src/modalanalysis/late.jl" => :engine, "ext/LineCableModelsXLSXExt.jl" => :xlsx, + "ext/LineCableModelsGmshExt/LineCableModelsGmshExt.jl" => :fem, + "ext/LineCableModelsGmshExt/mesh.jl" => :fem) + @test T.path_owner(loaded, "src/engine/data.toml") === :engine + @test T.path_owner(loaded, "ext/LineCableModelsGmshExt/remote/run.py") === :fem + @test T.path_owner(loaded, "src/new/file.jl") === :units + @test T.path_owner(loaded, "test/support/runner.jl") === nothing + @test T.directory_owner(loaded, "src/modalanalysis") === :engine + write(joinpath(repository, "src/twice.jl"), "") + write(joinpath(repository, "src/LineCableModels.jl"), + "module LineCableModels\ninclude(\"twice.jl\")\ninclude(\"twice.jl\")\nend\n") + @test_throws ErrorException T.loaded_owners(repository) + end + + # The guards read the tracked files and the new files that git would add, never the + # ignored ones. Outside a git work tree, they read every file. + mktempdir() do repository + git(arguments...) = run(pipeline(Cmd(["git", "-C", repository, arguments...]); + stdout = devnull, stderr = devnull)) + for (path, text) in ("test/tracked.jl" => "", "test/new.jl" => "", + "test/captured/old.jl" => "", ".gitignore" => "captured/\n") + mkpath(dirname(joinpath(repository, path))) + write(joinpath(repository, path), text) + end + directory = joinpath(repository, "test") + @test T.repository_files(directory) == joinpath.(directory, + ["captured/old.jl", "new.jl", "tracked.jl"]) + git("init", "-q") + git("add", "test/tracked.jl", ".gitignore") + @test T.repository_files(directory) == joinpath.(directory, ["new.jl", "tracked.jl"]) + rm(joinpath(directory, "tracked.jl")) + @test T.repository_files(directory) == [joinpath(directory, "new.jl")] + end +end + +@testitem "Quality / taxonomy / changed: selection controls" tags=[:quality] setup=[TaxonomyGuards] begin + G = TaxonomyGuards + T = G.T + (; items, setups) = T.inventory(G.REPOSITORY) + owners = T.loaded_owners(G.REPOSITORY) + select(paths...) = T.changed_items(collect(String, paths), items, setups, owners) + key(item) = (item.file, item.name) + environment(item) = any(in(T.ENVIRONMENT_TAGS), item.tags) + quality = Set(key(i) for i in items if :quality in i.tags) + owner(item) = T.owner_tag(item.tags) + + # A clean tree, or a change outside `src/`, `ext/` and `test/`, selects only quality. + @test select() == quality + @test select("docs/src/developers.md", "Project.toml") == quality + + # A change in `src/engine/` selects the non-slow items owned by engine or later. + engine = select("src/engine/lineparameters.jl") + expected = Set(key(i) for i in items if !environment(i) && :slow ∉ i.tags && + owner(i) !== nothing && T.rank(owner(i)) >= T.rank(:engine)) + @test engine == union(quality, expected) + @test any(i -> key(i) in engine && owner(i) === :engine, items) + @test any(i -> key(i) in engine && owner(i) === :makie, items) + @test !any(i -> key(i) in engine && owner(i) === :datamodel, items) + @test !any(i -> key(i) in engine && :slow in i.tags, items) + # Root files and extensions count at their load position. + @test select("src/performance.jl") == select("src/uq/UQ.jl") + @test select("src/gridspace.jl") == select("src/commons/Commons.jl") + @test select("src/engine/lineparameters.jl", "src/uq/UQ.jl") == engine + fem = select("ext/LineCableModelsGmshExt/LineCableModelsGmshExt.jl") + @test all(k -> k in quality || owner(only(i for i in items if key(i) == k)) in (:fem, :makie), fem) + + # A changed test file selects all its items, slow ones too, and the files that use + # a setup it defines. Its environment items remain excluded. + file = "test/unit/engine/unified_earth_return.jl" + @test any(i -> i.file == file && :slow in i.tags, items) + @test select(file) == union(quality, Set(key(i) for i in items if i.file == file)) + tracking = select("test/unit/modalanalysis/vieira2026.jl") + @test any(k -> first(k) == "test/unit/modalanalysis/wedepohl1996.jl", tracking) + @test select("test/extensions/fem_artifact.jl") == quality + + # A change under `test/support/` selects every non-slow item and every item under + # `test/unit/core/`, so a planted change to the runner selects the harness test. + support = select("test/support/runner.jl") + harness = [i for i in items if i.file == "test/unit/core/test_harness.jl"] + @test length(harness) == 2 && all(i -> :slow in i.tags && key(i) in support, harness) + @test support == union(quality, Set(key(i) for i in items if !environment(i) && + (:slow ∉ i.tags || startswith(i.file, "test/unit/core/")))) + + # The changed paths come from git: tracked changes, deletions and untracked files. + mktempdir() do repository + git(arguments...) = run(pipeline(Cmd(["git", "-C", repository, arguments...]); + stdout = devnull, stderr = devnull)) + git("init", "--quiet") + git("config", "user.email", "probe@example.invalid") + git("config", "user.name", "probe") + mkpath(joinpath(repository, "src", "engine")) + foreach(f -> write(joinpath(repository, f), "x"), ("src/engine/a.jl", "src/b.jl", "c.md")) + git("add", "--all") + git("commit", "--quiet", "-m", "probe") + @test T.changed_paths("HEAD", repository) == String[] + write(joinpath(repository, "src", "engine", "a.jl"), "y") + rm(joinpath(repository, "src", "b.jl")) + write(joinpath(repository, "d.jl"), "z") + git("mv", "c.md", "e.md") + @test T.changed_paths("HEAD", repository) == ["c.md", "d.jl", "e.md", "src/b.jl", "src/engine/a.jl"] + @test_throws ErrorException T.changed_paths("no-such-revision", repository) + end + # `changed:REF` takes no other selector and runs only through `execute`. + @test T.changed_reference(["changed:HEAD", "--list"]) == "HEAD" + @test T.changed_reference(["tag:unit", "--list"]) === nothing + @test_throws ErrorException T.changed_reference(["changed:HEAD", "tag:unit"]) + @test_throws ErrorException T.changed_reference(["changed:"]) + @test_throws ErrorException T.selection(["changed:HEAD"], joinpath(G.REPOSITORY, "test")) +end diff --git a/test/quality/text_display.jl b/test/quality/text_display.jl new file mode 100644 index 000000000..3e5bbc16e --- /dev/null +++ b/test/quality/text_display.jl @@ -0,0 +1,47 @@ +@testitem "Quality / TextDisplay / ownership and side effects" tags=[:quality] begin + using DataFrames + + # Current show and summary methods are owned by the domain. Observable + # publication and display side effects are exercised by the behavioral + # ReportBuilder/TextDisplay tests, not inferred from source tokens. + owner_modules=( + LineCableModels.Units, + LineCableModels.Materials, + LineCableModels.Earth, + LineCableModels.DataModel, + LineCableModels.Engine, + LineCableModels.ParametricBuilder, + LineCableModels.UQ, + LineCableModels.ReportBuilder + ) + display_modules=(owner_modules..., LineCableModels.TextDisplay) + for binding in names(LineCableModels; all = false, imported = true) + isdefined(LineCableModels, binding) || continue + owned_type=getfield(LineCableModels, binding) + owned_type isa Union{DataType, UnionAll} || continue + Base.isabstracttype(owned_type) && continue + parentmodule(Base.unwrap_unionall(owned_type)) in owner_modules || continue + @test which(summary, (IO, owned_type)).module in display_modules + @test which(show, (IO, owned_type)).module in display_modules + @test which(show, (IO, MIME"text/plain", owned_type)).module in display_modules + end + for owner in owner_modules + for binding in names(owner; all = false, imported = false) + isdefined(owner, binding) || continue + owned_type=getfield(owner, binding) + owned_type isa Union{DataType, UnionAll} || continue + Base.isabstracttype(owned_type) && continue + parentmodule(Base.unwrap_unionall(owned_type)) in owner_modules || continue + @test which(summary, (IO, owned_type)).module in display_modules + @test which(show, (IO, owned_type)).module in display_modules + @test which(show, (IO, MIME"text/plain", owned_type)).module in display_modules + end + end + + report_methods=filter(method -> method.module === LineCableModels.ReportBuilder, + collect(methods(DataFrame))) + @test length(report_methods) == 1 + signature=Base.unwrap_unionall(only(report_methods).sig) + @test signature.parameters[2] <: LineCableModels.ObservedResult + +end diff --git a/test/runtests.jl b/test/runtests.jl index 0bd85fdf9..0204e2466 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,66 +1,4 @@ -using LineCableModels -using Test using TestItemRunner +include(joinpath(@__DIR__, "support", "runner.jl")) -@testsnippet defaults begin - const TEST_TOL = 1e-8 - using Measurements - using Measurements: measurement, uncertainty, value - using DataFrames - using LineCableModels - using LineCableModels.Commons - using LineCableModels.Utils - using LineCableModels.Materials - using LineCableModels.DataModel - using LineCableModels.EarthProps - using LineCableModels.DataModel.BaseParams - using LineCableModels.Engine - using LineCableModels.Engine.FEM - using LineCableModels.ImportExport -end - -@testsnippet defs_materials begin - materials = MaterialsLibrary(add_defaults = true) - copper_props = Material(1.7241e-8, 1.0, 1.0, 20.0, 0.00393) - aluminum_props = Material(2.8264e-8, 1.0, 1.0, 20.0, 0.00429) - insulator_props = Material(1e14, 2.3, 1.0, 20.0, 0.0) - semicon_props = Material(1000.0, 1000.0, 1.0, 20.0, 0.0) -end - -@testsnippet cable_system_export begin - - cables_library = CablesLibrary() - @show file_name = joinpath(@__DIR__, "cable_test.json") - cables_library = load!(cables_library, file_name = file_name) - - # Retrieve the reloaded design - cable_design = collect(values(cables_library.data))[1] - x0, y0 = 0.0, -1.0 - xa, ya, xb, yb, xc, yc = trifoil_formation(x0, y0, 0.035) - - # Initialize the `LineCableSystem` with the first cable (phase A): - cablepos = CablePosition(cable_design, xa, ya, - Dict("core" => 1, "sheath" => 0, "jacket" => 0)) - cable_system = LineCableSystem("test_cable_sys", 1000.0, cablepos) - - # Add remaining cables (phases B and C): - add!(cable_system, cable_design, xb, yb, - Dict("core" => 2, "sheath" => 0, "jacket" => 0)) - add!(cable_system, cable_design, xc, yc, - Dict("core" => 3, "sheath" => 0, "jacket" => 0)) - - freqs = [50.0] - earth_props = EarthModel(freqs, 100.0, 10.0, 1.0) - num_phases = cable_system.num_phases - - # Create minimal mock objects for the other required arguments - problem_atp = LineParametersProblem( - cable_system, - temperature = 20.0, # Operating temperature - earth_props = earth_props, - frequencies = freqs, # Frequency for the analysis - ) - -end - -@run_package_tests(verbose = true, filter=ti->!(:skipci in ti.tags)) +ValidationTestRunner.execute(ARGS, @__DIR__) diff --git a/test/support/assertions.jl b/test/support/assertions.jl new file mode 100644 index 000000000..452826f08 --- /dev/null +++ b/test/support/assertions.jl @@ -0,0 +1,18 @@ +@testmodule TestAssertions begin + using LinearAlgebra + + function same_public_fields(left, right) + typeof(left) === typeof(right) || return false + return all(propertynames(left)) do name + isequal(getproperty(left, name), getproperty(right, name)) + end + end + + function is_symmetric_with_scale(matrix; factor = 64) + T = typeof(float(real(zero(eltype(matrix))))) + scale = max(norm(matrix), floatmin(T)) + return norm(matrix - transpose(matrix)) <= factor * eps(T) * scale + end + + all_finite(values) = all(value -> isfinite(real(value)) && isfinite(imag(value)), values) +end diff --git a/test/support/earth_interactions.jl b/test/support/earth_interactions.jl new file mode 100644 index 000000000..1ea75f24f --- /dev/null +++ b/test/support/earth_interactions.jl @@ -0,0 +1,106 @@ +@testmodule EarthInteractionFixtures begin + using LineCableModels + const E=LineCableModels.Engine + const G=LineCableModels.Commons + const EI, EA=E.EarthImpedance, E.EarthAdmittance + const Expression=LineCableModels.Expression + + # The indexed methods calculate the coefficients. The engine supplies reuse. + for (name, parent, operation, multiplier) in + ((:PairImpedance, E.EarthImpedanceFormulation, EI.earth_impedance, 1e-4+1e-3im), + (:PairPotential, E.EarthAdmittanceFormulation, EA.earth_potential_coefficient, 1e9)) + @eval struct $name <: $parent + parameters::NamedTuple{(), Tuple{}} + options::FormulationOptions{NamedTuple{(), Tuple{}}} + equivalent_earth::Nothing + calls::Vector{Tuple{Int, Int}} + end + @eval $name()=$name((;), FormulationOptions(), nothing, Tuple{Int, Int}[]) + @eval LineCableModels.formulation_options(::Expression{ + <:$name, typeof($operation)})=FormulationOptions() + @eval begin + LineCableModels.formula_id(::$name)=$(QuoteNode(name)) + LineCableModels.formula_id(::Type{$name})=$(QuoteNode(name)) + LineCableModels.description(::$name; compact::Bool = false)=$(string(name)) + LineCableModels.description(::Type{$name}; compact::Bool = false)=$(string(name)) + LineCableModels.formulation_options(selected::$name)=selected.options + Base.NamedTuple(selected::$name)=(identifier = formula_id(selected), + parameters = selected.parameters, + options = selected.options.data, equivalent_earth = nothing) + end + operation_name=GlobalRef(parentmodule(operation), nameof(operation)) + for layer in (1, 2), kind in (:self, :mutual) + + @eval function $operation_name(selected::$name, ::Val{$(QuoteNode(kind))}, + ::Val{$layer}, ::Val{$layer}, functor, workspace) + pair=functor.input.pair + push!(selected.calls, (pair.row, pair.column)) + radii=workspace.plan.geometry.radius + coefficient=10(pair.row==pair.column) + abs(pair.heights[2]) + + 2abs(pair.heights[1]) + + pair.separation + radii[pair.row] + 3radii[pair.column] + + functor.input.rho[2]/100 + imag(functor.input.jω)/(2pi*1000) + return $multiplier*coefficient + end + end + end + + # The arithmetic reads the pair's geometry and the conductor radii, not its indices. + function E.same_physical_state(::Union{PairImpedance, PairPotential}, a::E.EarthPair, + b::E.EarthPair, geometry::NamedTuple) + inputs(pair)=(pair.row==pair.column, pair.layers, pair.heights, pair.separation, + geometry.radius[pair.row], geometry.radius[pair.column]) + return E.same_physical_state(inputs(a), inputs(b)) + end + + struct IntegralImpedance{P} <: E.EarthImpedanceFormulation + parameters::P + options::FormulationOptions{NamedTuple{(), Tuple{}}} + equivalent_earth::Nothing + calls::Base.RefValue{Int} + end + IntegralImpedance(description; + positions = false)=IntegralImpedance( + (; description, positions), FormulationOptions(), nothing, Ref(0)) + LineCableModels.formulation_options(::Expression{ + <:IntegralImpedance, typeof(EI.earth_impedance)})=FormulationOptions() + function EI.earth_impedance( + selected::IntegralImpedance, ::Union{Val{:self}, Val{:mutual}}, + ::Val{2}, ::Val{2}, functor, workspace) + pair=functor.input.pair + selected.calls[]+=1 + integral=E.SpectralIntegral(x->complex(exp(-x)*cos(20x))) + context=selected.parameters.positions ? + (receiver = pair.row, source = pair.column, + frequency = imag(functor.input.jω)/(2pi), term = :test) : + selected.parameters.description + value, + _=E.integrate(integral, Val(:quad), (rtol = 1e-14, atol = 0.0, maxevals = 15), + workspace.buffers; observations = workspace.buffers.observations, context) + return value + end + # The arithmetic does not read any pair input, so every pair with the same media takes one + # value. + E.same_physical_state(::IntegralImpedance, a::E.EarthPair, b::E.EarthPair, + geometry::NamedTuple) = true + G.initialize_buffers(::IntegralImpedance, + ::Type{T}, + input, + plan, + buffers) where {T} = + G.initialize_buffers(E.SpectralIntegral, Val(:quad), T, input, plan, buffers) + + function workspace(problem, impedance = PairImpedance(); trace = false) + selected=Formulation( + earth_impedance = impedance, earth_admittance = PairPotential(); + options = ( + reduce_bundle = false, kron_reduction = false, ideal_transposition = false)) + execution=E.computation_options(LineCableModelsCoaxial, ComputationOptions(; trace)) + blueprints=only(E.flatten( + LineCableModelsCoaxial(), problem.system.designs, eltype(problem), [selected])) + work=E.LineParametersWorkspace(problem, selected, execution, blueprints) + E.materials!(work, selected) + E.materials!(work, selected, 1) + return work + end +end diff --git a/test/support/fixtures.jl b/test/support/fixtures.jl new file mode 100644 index 000000000..f413cd97f --- /dev/null +++ b/test/support/fixtures.jl @@ -0,0 +1,12 @@ +@testmodule TestFixtures begin + include("scenarios.jl") + using .CurrentScenarios +end + +@testsnippet CableSystemFixture begin + cable_system=TestFixtures.three_phase_system() + problem_atp=TestFixtures.line_parameters_problem(cable_system) + earth_props=problem_atp.earth_props + freqs=problem_atp.frequencies + num_phases=LineCableModels.nphases(cable_system) +end diff --git a/test/support/formula_families.jl b/test/support/formula_families.jl new file mode 100644 index 000000000..f20bc1a2a --- /dev/null +++ b/test/support/formula_families.jl @@ -0,0 +1,21 @@ +# The formula families of the package, found from the registry instead of a hand-kept list. +@testmodule FormulaFamilies begin + using LineCableModels + const C = LineCableModels.Commons + + # Each package module that owns a `Formula` type registered with `Commons.formulas`, + # sorted by module name. + function families(root::Module = LineCableModels, found = Module[]) + for name in names(root; all = true) + isdefined(root, name) || continue + value = getfield(root, name) + value isa Module && parentmodule(value) === root && value !== root || continue + families(value, found) + end + isdefined(root, :Formula) || return found + F = getfield(root, :Formula) + F isa Type && parentmodule(F) === root && + hasmethod(C.formulas, Tuple{Type{F}}) && push!(found, root) + return sort!(found; by = string) + end +end diff --git a/test/support/formula_fixtures.jl b/test/support/formula_fixtures.jl new file mode 100644 index 000000000..a88ceb477 --- /dev/null +++ b/test/support/formula_fixtures.jl @@ -0,0 +1,545 @@ +@testmodule FormulaFixtures begin + using LineCableModels + const E = LineCableModels.Engine + const G = LineCableModels.Commons + const II = E.InternalImpedance + const EI = E.EarthImpedance + const EA = E.EarthAdmittance + const EP = LineCableModels.Earth + const FD = EP.FrequencyDependent + const TD = LineCableModels.Materials.TemperatureDependent + const EH = EP.EquivalentHomogeneous + const IA = E.InsulationAdmittance + const SA = E.SemiconAdmittance + const Expression = LineCableModels.Expression + const calls = Tuple[] + + struct BufferReplacement <: E.AbstractFormulation end + G.initialize_buffers(::BufferReplacement, ::Type, input, plan, + buffers) = merge(buffers, (destination = copy(buffers.destination),)) + + # These scientific selections belong to the consumer and leave built-in + # Formula registrations and private constructors unchanged. The built-in formula lists remain closed. + # These formulas have expressions up to layer 3 (`Layer…`) or layer 2 (`HalfSpace…`, + # usable in a recipe). Their expression signatures alone declare the media they handle. + for (name, parent, owner, operation, layers) in ( + (:LayerImpedance, E.EarthImpedanceFormulation, EI, EI.earth_impedance, 3), + (:LayerPotential, E.EarthAdmittanceFormulation, EA, EA.earth_potential_coefficient, 3), + (:HalfSpaceImpedance, E.EarthImpedanceFormulation, EI, EI.earth_impedance, 2), + (:HalfSpacePotential, E.EarthAdmittanceFormulation, EA, EA.earth_potential_coefficient, 2)) + @eval struct $name{P, O, R} <: $parent + parameters::P + options::O + equivalent_earth::R + initialized::Vector{Tuple} + end + operation_name = GlobalRef(owner, nameof(operation)) + for s in 1:layers, t in 1:layers, kind in (s == t ? (:self, :mutual) : (:mutual,)) + @eval function $operation_name( + selected::$name, ::Val{$(QuoteNode(kind))}, + ::Val{$s}, ::Val{$t}, functor, workspace) + input = functor.input + pair = input.pair + push!(calls, + ($(QuoteNode(nameof(owner))), input.jω, + pair.row, pair.column, pair.layers, pair.heights, + copy(input.rho), input.physical)) + coefficient = 11 * $s + 17 * $t + 3 * pair.row + 5 * pair.column + + real(input.jω / (2pi*im)) / 100 + + (pair.row == pair.column ? 101 : 0) + if haskey(input.options.data, :integration) + integral = E.SpectralIntegral(λ -> complex(exp(-2λ))) + value, + _ = E.integrate(integral, + input.options.data.integration.method, input.options.data.integration.options, + workspace.buffers) + coefficient *= 2value + end + return selected.parameters.scale * + $(owner === EI ? :(coefficient * (1e-4 + 1e-3im)) : + :(coefficient * 1e9)) + end + end + @eval LineCableModels.formulation_options(::Expression{ + <:$name, typeof($operation)}) = FormulationOptions() + end + LineCableModels.formulation_options(::Expression{<:Union{LayerImpedance, HalfSpaceImpedance}, + typeof(EI.earth_impedance), + A}) where {A <: + Tuple{Union{Val{:self}, Val{:mutual}}, Val{1}, + Val{1}}} = FormulationOptions(integration = (method = :quad, options = (;))) + + function G.initialize_buffers( + selected::Union{LayerImpedance, HalfSpaceImpedance}, ::Type{T}, input, plan, + buffers) where {T} + layers = [E.layer_index(entry.pair) + for calculation in plan.earth.calculations + if something(calculation.impedance, calculation.admittance).formula === selected + for entry in calculation.pairs] + initialized = (1, 1) in layers ? + G.initialize_buffers( + E.SpectralIntegral, Val(:quad), T, input, plan, buffers) : + buffers + push!(selected.initialized, (layers, get(initialized, :quadrature, nothing))) + return initialized + end + + function selection( + owner; options::Union{NamedTuple, FormulationOptions} = FormulationOptions(), + scale = 1.0, equivalent_earth = nothing, layers = 3) + options = options isa NamedTuple ? FormulationOptions(options) : options + selected_type = layers == 3 ? (owner === EI ? LayerImpedance : LayerPotential) : + (owner === EI ? HalfSpaceImpedance : HalfSpacePotential) + return selected_type((scale = scale,), options, equivalent_earth, Tuple[]) + end + + # These formulas admit the deepest-layer reduction. + LineCableModels.validate(rule::EH.Formula{:bottommost}, + ::Expression{<:Union{LayerImpedance, LayerPotential, HalfSpaceImpedance, HalfSpacePotential}}) = rule + + # Two more layered earth-impedance formulas. One declares its expressions up to layer 3 + # with typed runtime arguments, the other for every layer with generic `Val{S}` + # positions. Each records the number of media its expressions receive. + struct TypedLayerImpedance{P, O} <: E.EarthImpedanceFormulation + parameters::P + options::O + equivalent_earth::Nothing + media::Vector{Int} + end + TypedLayerImpedance() = TypedLayerImpedance((;), FormulationOptions(), nothing, Int[]) + for s in 1:3, t in 1:3, kind in (s == t ? (:self, :mutual) : (:mutual,)) + @eval function EI.earth_impedance(selected::TypedLayerImpedance, + ::Val{$(QuoteNode(kind))}, ::Val{$s}, ::Val{$t}, + functor::G.Functor, workspace) + push!(selected.media, length(functor.input.rho)) + pair = functor.input.pair + return (1e-4 + 1e-3im) * (pair.row == pair.column ? 10 : 1) + end + end + + struct GenericLayerImpedance{P, O} <: E.EarthImpedanceFormulation + parameters::P + options::O + equivalent_earth::Nothing + media::Vector{Int} + end + GenericLayerImpedance() = GenericLayerImpedance((;), FormulationOptions(), nothing, Int[]) + function EI.earth_impedance(selected::GenericLayerImpedance, + ::Union{Val{:self}, Val{:mutual}}, ::Val{S}, ::Val{T}, + functor, workspace) where {S, T} + push!(selected.media, length(functor.input.rho)) + pair = functor.input.pair + return (1e-4 + 1e-3im) * (pair.row == pair.column ? 10 : 1) + end + + for selected_type in (TypedLayerImpedance, GenericLayerImpedance) + @eval LineCableModels.formulation_options(::Expression{ + <:$selected_type, typeof(EI.earth_impedance)}) = FormulationOptions() + end + + # These formulas admit nonzero permittivities of either sign. + function LineCableModels.validate(rho::AbstractVector, + ::Union{LayerImpedance, LayerPotential, HalfSpaceImpedance, HalfSpacePotential}, + epsilon::AbstractVector, mu::AbstractVector, thickness) + length(rho) == length(epsilon) == length(mu) || + throw(DimensionMismatch("material vectors must align")) + all(x -> isfinite(x) && !iszero(x), epsilon) || throw(DomainError(epsilon, + "the layered formula requires nonzero finite permittivities")) + return rho + end + + # An algebraic coupled equation uses the same main workspace and explicit + # calculation action without any formula-owned lifecycle. + struct CoupledImpedance{P, O} <: E.EarthImpedanceFormulation + parameters::P + options::O + equivalent_earth::Nothing + solves::Vector{ComplexF64} + end + CoupledImpedance() = CoupledImpedance((;), FormulationOptions(), nothing, ComplexF64[]) + LineCableModels.formulation_options(::Expression{ + <:CoupledImpedance, typeof(EI.earth_impedance)}) = FormulationOptions() + function G.initialize_buffers( + ::CoupledImpedance, ::Type{T}, input, plan, buffers) where {T} + geometry=plan.geometry + return merge(buffers, + (coupled = Matrix{Complex{T}}(undef, + length(geometry.radius), length(geometry.radius)),)) + end + # The parts write into the formula's own matrix, which its conversion completes. + function G.Functor(selected::CoupledImpedance, input::NamedTuple; workspace) + return G.Functor(selected, merge(input, (destinations = (workspace.buffers.coupled,),)), (;)) + end + function E.earth!(selected::CoupledImpedance, functor::G.Functor, workspace) + values=only(functor.input.destinations) + # Complete-system coupling adds the sum over all source conductors. + values .+= functor.input.jω*1e-6*sum(axes(values, 2)) + push!(selected.solves, functor.input.jω) + return (impedance = values,) + end + for source in 1:2, target in 1:2, + kind in (source==target ? (:self, :mutual) : (:mutual,)) + @eval function EI.earth_impedance(::CoupledImpedance, ::Val{$(QuoteNode(kind))}, + ::Val{$source}, ::Val{$target}, functor, workspace) + n=length(workspace.plan.geometry.radius) + pair=functor.input.pair + return functor.input.jω*1e-6*(pair.row+2pair.column+(pair.row==pair.column ? n : + 0)) + end + end + + struct SurfaceLaw{Kinds, P, O} <: E.InternalImpedanceFormulation + parameters::P + options::O + state_inputs::Vector{Tuple} + evaluations::Vector{Tuple} + end + function SurfaceLaw(; kinds = (:inner, :outer, :transfer), + coefficients = (inner = 2.0+1im, outer = 2.0+1im, transfer = 0.5+0.1im)) + parameters = (coefficients = coefficients,) + options = FormulationOptions(NamedTuple{kinds}(map(_ -> (;), kinds))) + SurfaceLaw{kinds, typeof(parameters), typeof(options)}( + parameters, options, Tuple[], Tuple[]) + end + # The surface impedances share a serial number of the evaluation point. + function G.Functor(selected::SurfaceLaw, input::NamedTuple; workspace = nothing) + (; r_in, r_ex, rho, mu_r, jω) = input + push!(selected.state_inputs, (r_in, r_ex, rho, mu_r, jω)) + state = (serial = length(selected.state_inputs), rho = rho, radius = r_ex) + return G.Functor(selected, input, state) + end + # Availability is dispatched by actual surface, not inferred from options. + for kinds in ((:inner, :outer, :transfer), (:outer,), (:transfer,), (:inner,)), + kind in kinds + + @eval function II.internal_impedance(selected::SurfaceLaw{$kinds}, + ::Val{$(QuoteNode(kind))}, functor, workspace) + push!(selected.evaluations, (functor.state.serial, Val($(QuoteNode(kind))))) + return selected.parameters.coefficients[$(QuoteNode(kind))] + end + end + LineCableModels.formulation_options(::Expression{ + <:SurfaceLaw, typeof(II.internal_impedance)}) = FormulationOptions() + + struct SpectralSurface{P, O} <: E.InternalImpedanceFormulation + parameters::P + options::O + seen::Vector{Tuple} + end + function SpectralSurface(method = :quad) + seed = SpectralSurface((;), FormulationOptions(outer = (;)), Tuple[]) + outer = formulation_options(Expression(seed, II.internal_impedance, Val(:outer)), + FormulationOptions(integration = (method = method,))) + return SpectralSurface((;), FormulationOptions(outer = outer.data), seed.seen) + end + function G.initialize_buffers( + selected::SpectralSurface, ::Type{T}, input, plan, buffers) where {T} + push!(selected.seen, (:initialize, buffers)) + return G.initialize_buffers( + E.SpectralIntegral, Val(:quad), T, input, plan, buffers) + end + LineCableModels.formulation_options(::Expression{ + <:SpectralSurface, typeof(II.internal_impedance), + Tuple{Val{:outer}}}) = FormulationOptions(integration = ( + method = :quad, options = (;))) + # Its inner and transfer surface impedances are fixed coefficients. Only `outer` integrates. + II.internal_impedance(::SpectralSurface, ::Val{:inner}, functor, workspace) = 3e-5 + 1e-6im + II.internal_impedance(::SpectralSurface, ::Val{:transfer}, functor, workspace) = 1e-6 + 0im + LineCableModels.formulation_options(::Expression{<:SpectralSurface, typeof(II.internal_impedance), + <:Union{Tuple{Val{:inner}}, Tuple{Val{:transfer}}}}) = FormulationOptions() + function II.internal_impedance(selected::SpectralSurface, ::Val{:outer}, functor, workspace) + integration=functor.input.options.data.integration + push!(selected.seen, (integration.method, workspace)) + integral=E.SpectralIntegral(λ->complex(exp(-2λ))) + value, + _=E.integrate(integral, integration.method, integration.options, + workspace===nothing ? nothing : workspace.buffers) + return value*1e-4 + end + LineCableModels.description(::Type{<:SpectralSurface}, ::Val{:method}, + ::Val{:quad}; compact::Bool = false) = "quad" + + struct DispersiveEarth{P, O} <: FD.FrequencyDependentFormulation + parameters::P + options::O + seen::Vector{Tuple} + events::Vector{Symbol} + end + DispersiveEarth(; scale = 100.0, + exponent = 1, + events = Symbol[]) = DispersiveEarth( + (scale = scale, exponent = exponent), FormulationOptions(), Tuple[], events) + function FD.earth_material(selected::DispersiveEarth, functor, workspace) + (; material, frequency) = functor.input + parameters = selected.parameters + push!(selected.seen, (material.rho, frequency, workspace)) + push!(selected.events, :fd) + return EP.EarthMaterial( + material.rho/(1+(frequency/parameters.scale)^parameters.exponent), + material.eps_r, material.mu_r) + end + LineCableModels.formulation_options(::Expression{ + <:DispersiveEarth, typeof(FD.earth_material)}) = FormulationOptions() + + struct InsulationReactance{P, O} <: E.InsulationImpedanceFormulation + parameters::P + options::O + end + InsulationReactance(inductance = 2) = InsulationReactance((inductance = inductance,), FormulationOptions()) + E.InsulationImpedance.insulation_impedance(selected::InsulationReactance, functor, + workspace) = selected.parameters.inductance*functor.input.jω + LineCableModels.formulation_options(::Expression{<:InsulationReactance, + typeof(E.InsulationImpedance.insulation_impedance)}) = FormulationOptions() + + struct ConstantResistivity{P, O} <: TD.TemperatureDependentFormulation + parameters::P + options::O + seen::Vector{Tuple} + end + ConstantResistivity(value) = ConstantResistivity((rho = value,), FormulationOptions(), Tuple[]) + function TD.temperature_resistivity(selected::ConstantResistivity, functor, workspace) + (; material, temperature) = functor.input + push!(selected.seen, (material, temperature, workspace)) + return selected.parameters.rho + end + LineCableModels.formulation_options(::Expression{ + <:ConstantResistivity, typeof(TD.temperature_resistivity)}) = FormulationOptions() + + struct ScaledResistivity{P, O} <: TD.TemperatureDependentFormulation + parameters::P + options::O + seen::Vector{Tuple} + end + ScaledResistivity(scale = 2) = ScaledResistivity((scale = scale,), FormulationOptions(), Tuple[]) + function TD.temperature_resistivity(selected::ScaledResistivity, functor, workspace) + (; material, temperature) = functor.input + push!(selected.seen, (material, temperature, workspace)) + return selected.parameters.scale*material.rho + end + LineCableModels.formulation_options(::Expression{ + <:ScaledResistivity, typeof(TD.temperature_resistivity)}) = FormulationOptions() + + struct ExponentialResistivity{P, O} <: TD.TemperatureDependentFormulation + parameters::P + options::O + end + ExponentialResistivity(scale = 1000.0) = ExponentialResistivity((scale = scale,), FormulationOptions()) + function TD.temperature_resistivity(selected::ExponentialResistivity, functor, workspace) + (; material, temperature) = functor.input + return material.rho*exp((temperature-material.T0)/selected.parameters.scale) + end + LineCableModels.formulation_options(::Expression{ + <:ExponentialResistivity, typeof(TD.temperature_resistivity)}) = FormulationOptions() + + struct DispersiveSoil{P, O} <: FD.FrequencyDependentFormulation + parameters::P + options::O + seen::Vector{Float64} + end + DispersiveSoil() = DispersiveSoil( + (rho_scale = 1000, epsilon_scale = 2000, mu_scale = 10000), + FormulationOptions(), Float64[]) + function FD.earth_material(selected::DispersiveSoil, functor, workspace) + (; material, frequency) = functor.input + p = selected.parameters + push!(selected.seen, frequency) + EP.EarthMaterial(material.rho/(1+frequency/p.rho_scale), + material.eps_r*(1+frequency/p.epsilon_scale), material.mu_r*(1+frequency/p.mu_scale)) + end + LineCableModels.formulation_options(::Expression{ + <:DispersiveSoil, typeof(FD.earth_material)}) = FormulationOptions() + + struct ScaledSoil{P, O} <: FD.FrequencyDependentFormulation + parameters::P + options::O + end + ScaledSoil(; rho = 1, epsilon = 1, + mu = 1) = ScaledSoil((rho = rho, epsilon = epsilon, mu = mu), FormulationOptions()) + function FD.earth_material(selected::ScaledSoil, functor, workspace) + (; material) = functor.input + p = selected.parameters + return EP.EarthMaterial(material.rho*p.rho, material.eps_r*p.epsilon, material.mu_r*p.mu) + end + LineCableModels.formulation_options(::Expression{ + <:ScaledSoil, typeof(FD.earth_material)}) = FormulationOptions() + + struct OhmicDielectric{P, O} <: E.InsulationAdmittanceFormulation + parameters::P + options::O + temperatures::Vector{Tuple} + end + OhmicDielectric() = OhmicDielectric((;), FormulationOptions(), Tuple[]) + function IA.insulation_material(selected::OhmicDielectric, functor, workspace) + (; material, frequency, temperature) = functor.input + push!(selected.temperatures, (material.T0, temperature)) + complex(inv(material.rho), 2pi*frequency*8.8541878128e-12*material.eps_r) + end + LineCableModels.formulation_options(::Expression{ + <:OhmicDielectric, typeof(IA.insulation_material)}) = FormulationOptions() + + for (name, parent, operation) in ( + (:InsulationLaw, E.InsulationAdmittanceFormulation, IA.insulation_material), + (:SemiconLaw, E.SemiconAdmittanceFormulation, SA.semicon_material)) + @eval struct $name{P, O} <: $parent + parameters::P + options::O + end + @eval $name(; scale = 2) = $name((scale = scale,), FormulationOptions()) + operation_name=GlobalRef(parentmodule(operation), nameof(operation)) + @eval function $operation_name(selected::$name, functor, workspace) + (; material, frequency, temperature) = functor.input + return complex(oftype(frequency, selected.parameters.scale)/material.rho, + frequency*material.eps_r+temperature) + end + @eval LineCableModels.formulation_options(::Expression{ + <:$name, typeof($operation)}) = FormulationOptions() + end + + struct MeanEarth{P, O} <: EH.AbstractRule + parameters::P + options::O + seen::Vector{Tuple} + end + MeanEarth() = MeanEarth((;), FormulationOptions(), Tuple[]) + function EH.equivalent_material(selected::MeanEarth, ::Val{Kind}, ::Val{S}, ::Val{T}, + functor, workspace) where {Kind, S, T} + (; rho, eps_r, mu_r, pair, frequency) = functor.input + push!(selected.seen, (copy(rho), pair, frequency, workspace)) + soils=2:length(rho) + return EP.EarthMaterial(sum(rho[soils])/length(soils), + sum(eps_r[soils])/length(soils), sum(mu_r[soils])/length(soils)) + end + LineCableModels.formulation_options(::Expression{ + <:MeanEarth, typeof(EH.equivalent_material)}) = FormulationOptions() + + struct SquaredBottomEarth{P, O} <: EH.AbstractRule + parameters::P + options::O + events::Vector{Symbol} + workspaces::Vector{Any} + end + SquaredBottomEarth(events = Symbol[]) = SquaredBottomEarth( + (scale = 100,), FormulationOptions(), events, Any[]) + function EH.equivalent_material( + selected::SquaredBottomEarth, ::Val{Kind}, ::Val{S}, ::Val{T}, + functor, workspace) where {Kind, S, T} + (; rho, eps_r, mu_r) = functor.input + push!(selected.events, :ehem) + push!(selected.workspaces, workspace) + return EP.EarthMaterial(last(rho)^2/selected.parameters.scale, last(eps_r), last(mu_r)) + end + LineCableModels.formulation_options(::Expression{ + <:SquaredBottomEarth, typeof(EH.equivalent_material)}) = FormulationOptions() + LineCableModels.validate(reduction::SquaredBottomEarth, + ::Expression{<:Union{EI.Formula{:unified}, EA.Formula{:unified}}}) = reduction + + struct UserCoaxialShunt{P, O} <: E.ShuntModelFormulation + parameters::P + options::O + response_count::Base.RefValue{Int} + end + UserCoaxialShunt() = UserCoaxialShunt((;), FormulationOptions(), Ref(0)) + function E.internal_shunt_response(selected::UserCoaxialShunt, design::CableDesign, + geometry, T, methods, solutions, design_index) + selected.response_count[]+=1 + response=E.internal_shunt_response(E.ShuntModel.Formula(:equivalent), + design, geometry, T, methods, solutions, design_index) + merge(response, (details = merge(response.details, (requested = :UserCoaxialShunt,)),)) + end + + struct UserCoaxialPipe{P, O} <: E.PipeImpedanceFormulation + parameters::P + options::O + end + UserCoaxialPipe() = UserCoaxialPipe((;), FormulationOptions()) + # It admits coaxial topology, as the built-in formula without a pipe term does. + LineCableModels.validate(design::CableDesign, ::UserCoaxialPipe, backend) = + LineCableModels.validate(design, E.PipeImpedance.Formula(:none), backend) + + # Counter-bearing native types observe choreography without replacing any + # package method or attaching executable values to a selection record. + for (name, parent, owner, operation, id, event) in ( + (:CountedInsulationZ, E.InsulationImpedanceFormulation, E.InsulationImpedance, + E.InsulationImpedance.insulation_impedance, :ametani1980, :local_z), + (:CountedInsulationY, E.InsulationAdmittanceFormulation, + IA, IA.insulation_material, :lossy, :local_y), + (:CountedSemiconY, E.SemiconAdmittanceFormulation, + SA, SA.semicon_material, :lossy, :local_y)) + @eval struct $name{F, P, O} <: $parent + base::F + parameters::P + options::O + events::Vector{Symbol} + workspaces::Vector{Any} + end + @eval function $name(events) + base=$owner.Formula($(QuoteNode(id))) + $name(base, base.parameters, base.options, events, Any[]) + end + op=GlobalRef(owner, nameof(operation)) + @eval function $op(selected::$name, functor, workspace) + push!(selected.events, $(QuoteNode(event))) + push!(selected.workspaces, workspace) + $op(selected.base, functor, workspace) + end + @eval LineCableModels.formulation_options(::Expression{ + <:$name, typeof($operation)}) = FormulationOptions() + end + for (name, parent, owner, operation, event) in ( + (:CountedEarthZ, E.EarthImpedanceFormulation, EI, EI.earth_impedance, :earth_z), + (:CountedEarthP, E.EarthAdmittanceFormulation, + EA, EA.earth_potential_coefficient, :earth_y)) + @eval struct $name{P, O} <: $parent + parameters::P + options::O + equivalent_earth::Nothing + events::Vector{Symbol} + end + @eval function $name(events) + $name((;), FormulationOptions(), nothing, events) + end + op=GlobalRef(owner, nameof(operation)) + @eval function $op(selected::$name, kind::Union{Val{:self}, Val{:mutual}}, + s::Val{2}, t::Val{2}, + functor, workspace) + push!(selected.events, $(QuoteNode(event))) + pair = functor.input.pair + # Manufactured coefficients with diagonal dominance test the stage order, + # independently of any built-in author's numerical implementation. + return $(owner === EI ? :(1e-4 + 1e-3im) : :(1e9)) * + (pair.row == pair.column ? 10 : 1) + end + @eval LineCableModels.formulation_options(::Expression{ + <:$name, typeof($operation)}) = FormulationOptions() + end + + for selected_type in + (LayerImpedance, LayerPotential, HalfSpaceImpedance, HalfSpacePotential, + TypedLayerImpedance, GenericLayerImpedance, + CoupledImpedance, SurfaceLaw, SpectralSurface, + DispersiveEarth, InsulationReactance, ConstantResistivity, ScaledResistivity, + ExponentialResistivity, DispersiveSoil, ScaledSoil, OhmicDielectric, InsulationLaw, SemiconLaw, + MeanEarth, SquaredBottomEarth, UserCoaxialShunt, UserCoaxialPipe, + CountedInsulationZ, CountedInsulationY, + CountedSemiconY, CountedEarthZ, CountedEarthP) + id=Symbol(nameof(selected_type)) + @eval begin + LineCableModels.formula_id(::$selected_type) = $(QuoteNode(id)) + LineCableModels.formula_id(::Type{<:$selected_type}) = $(QuoteNode(id)) + LineCableModels.description(::$selected_type; compact::Bool = false) = $(string(id)) + LineCableModels.description(::Type{<:$selected_type}; compact::Bool = false) = $(string(id)) + LineCableModels.formulation_options(selected::$selected_type) = selected.options + end + if selected_type <: Union{E.EarthImpedanceFormulation, E.EarthAdmittanceFormulation} + @eval Base.NamedTuple(selected::$selected_type) = ( + identifier = formula_id(selected), + parameters = selected.parameters, options = selected.options.data, + equivalent_earth = selected.equivalent_earth === nothing ? nothing : + NamedTuple(selected.equivalent_earth)) + else + @eval Base.NamedTuple(selected::$selected_type) = ( + identifier = formula_id(selected), + parameters = selected.parameters, options = selected.options.data) + end + end +end diff --git a/test/support/golden_fixtures.jl b/test/support/golden_fixtures.jl new file mode 100644 index 000000000..f8ac07f60 --- /dev/null +++ b/test/support/golden_fixtures.jl @@ -0,0 +1,136 @@ +# Fresh scene recipes. Outputs are provisional repeatability records until a +# person accepts their stated validation scope. No saved image is an oracle. +module GoldenFixtures +using LineCableModels +using LineCableModels.ReportBuilder: BenchmarkTableDefinition +using CairoMakie +using Measurements: measurement +using Statistics: mean +include("scenarios.jl") +using .CurrentScenarios +export scene_names, scene, custom_layout_plot, save_pixels, pixel_error, check_scene, defect! +const scene_names=("line_rlcg","line_zy_cartesian","line_zy_polar", + "formulation_comparison","uq_comparison","uncertainty_intervals", + "cable_preview","cable_preview_compact","system_preview","material_scale", + "mc_hist","mc_pdf","mc_ecdf","mc_qq","custom_layout") +const plot_options=(backend=:cairo,display_plot=false,controls=false,open_export=false, + size=(1100,750)) + +function custom_layout_plot(;backend=:cairo,display_plot=false,controls=false) + return LineCableModels.plotwindow(;backend,display_plot,controls,open_export=false, + title="Asymmetric current layout",size=(1100,750),export_name="custom_layout") do canvas + left=Axis(canvas[1,1];title="Quadratic",xlabel="position",ylabel="response") + right=Axis(canvas[1,2];title="Discrete samples",xlabel="index",ylabel="response") + x=[.25,.75,1.5,2.5] + lines!(left,x,x.^2 .+ .3;label="x² + 0.3",color=:navy,linewidth=3) + scatter!(right,[1,2,3],[.4,1.1,.7];label="three samples",color=:orange,markersize=18) + axislegend(left;position=:lt) + axislegend(right;position=:rb) + colsize!(canvas,1,Relative(.65)) + nothing + end +end + +function actual_mc(;trials=128,seed=2027) + design=coaxial_design() + space=Gridspace{CableConstantsProblem}(t->CableConstantsProblem(design;temperature=t), + (Grid(20.0,AbsoluteError(1.0)),)) + return compute(ParametricProblem(space),MonteCarlo(CableConstantsFormulation(); + trials,seed,distribution=:uniform,return_samples=true,return_histograms=true)) +end + +function comparison(;uncertain=false) + if uncertain + build_problem=scale->begin + design=coaxial_design(;scale) + system=build(LineCableSystem,[design,design],[(0.,-1.),(.2,-1.3)]; + connections=[Dict(:core=>1,:sheath=>0),Dict(:core=>2,:sheath=>0)]) + LineParametersProblem(system;temperature=20.,frequencies=[10.,100.,1000.], + earth_props=homogeneous(rho=100.)) + end + space=Gridspace{LineParametersProblem}(build_problem,(Grid(1.,AbsoluteError(.01)),)) + problem=ParametricProblem(space) + selection=Formulation(options=(reduce_bundle=false,kron_reduction=true,ideal_transposition=false)) + run(seed)=compute(problem,MonteCarlo(selection;trials=128,seed, + distribution=:uniform,return_samples=true,return_histograms=false)) + reference=run(2027) + result=run(2039) + metadata=(port_order=["west","east"],) + artifact=report(BenchmarkTableDefinition(((statistics,R,mean),);bands=(:all,)), + (reference=(result=reference,metadata=metadata), + result=(result=result,metadata=metadata))) + return LineCableModels.plot(artifact;ydata=(R,),plot_options...) + end + reference=two_conductor_results() + omega=reshape(2pi.*reference.f,1,1,:) + result=LineParameters(1.1R(reference).+im.*omega.*1.2L(reference), + 1.3G(reference).+im.*omega.*1.4C(reference),reference.f; + details=ComputationDetails(;coordinates=["west","east"],)) + selections=[NamedTuple(Formulation()),NamedTuple(Formulation(earth_impedance=:pollaczek1926))] + points=ParametricResult(nothing,[reference,result], + (problems=[:current],formulations=selections), ComputationDetails((;))) + baseline=(result=reference,metadata=(port_order=["west","east"],formulation=selections[1],axes=nothing)) + artifact=report(BenchmarkTableDefinition((R,);bands=(:all,)),(reference=baseline,result=points)) + return LineCableModels.plot(artifact;ydata=(R,),plot_options...) +end + +# Dispatch is deliberately lazy: selecting one view never constructs another. +function scene(name::AbstractString) + name in scene_names || throw(ArgumentError("unknown provisional view: $name")) + CairoMakie.activate!() + set_theme!(backgroundcolor=:white, fonts=(regular="DejaVu Sans",bold="DejaVu Sans")) + name=="custom_layout" && return custom_layout_plot() + name=="material_scale" && return show_material_scale(;plot_options...) + name=="formulation_comparison" && return comparison() + name=="uq_comparison" && return comparison(;uncertain=true) + if startswith(name,"mc_") + result=actual_mc() + verb=name=="mc_hist" ? Makie.hist : name=="mc_pdf" ? Makie.stairs : + name=="mc_ecdf" ? Makie.ecdfplot : Makie.qqplot + return verb(result,R;plot_options...,length_unit=:base,quantity_units=:base) + end + if name in ("cable_preview","cable_preview_compact","system_preview") + design=coaxial_design() + if name=="system_preview" + system=build(LineCableSystem,[design,coaxial_design(;scale=1.2,name="larger")], + [(0.0,-.3),(.05,-.4)];connections=[Dict(:core=>1,:sheath=>0),Dict(:core=>2,:sheath=>0)]) + handle=preview(system;earth_model=homogeneous(rho=100.0),plot_options...) + ylims!(only(handle.axes),-.45,.05) + return handle + end + options=name=="cable_preview_compact" ? merge(plot_options,(size=(900,350),)) : plot_options + return preview(design;options...) + end + parameters=two_conductor_results() + if name=="uncertainty_intervals" + variants=map((1.0,2.0,3.0)) do width + f=measurement.(parameters.f,width*.01parameters.f) + scale=[width*.01(i+j+k) for i in 1:2,j in 1:2,k in 1:3] + z=complex.(measurement.(real.(Z(parameters)),scale.*real.(Z(parameters))), + measurement.(imag.(Z(parameters)),scale.*imag.(Z(parameters)))) + y=complex.(measurement.(real.(Y(parameters)),scale.*real.(Y(parameters))), + measurement.(imag.(Y(parameters)),scale.*imag.(Y(parameters)))) + LineParameters(z,y,f;details=ComputationDetails(;coordinates=["west","east"],)) + end + return LineCableModels.plot(variants...;ydata=(R,),plot_options..., + length_unit=:base,quantity_units=:base,freq_unit=:base,clip=false, + series_labels=("one sigma","two sigma","three sigma")) + end + requests=name=="line_rlcg" ? (R,L,G,C) : name=="line_zy_polar" ? (abs,angle) : (Z,Y) + return LineCableModels.plot(parameters,requests;plot_options..., + length_unit=:base,quantity_units=:base,freq_unit=:base,clip=false) +end + +function save_pixels(path,handle) + handle.figure.scene.backgroundcolor[]=Makie.RGBAf(1,1,1,1) + CairoMakie.save(path,handle.figure;px_per_unit=1) + # Both sides use the same PNG decoder. No orientation or alignment search. + pixels=CairoMakie.FileIO.load(path) + return cat((round.(UInt8,255 .* channel.(pixels)) for channel in (Makie.red,Makie.green,Makie.blue))...;dims=3) +end +function pixel_error(a,b) + size(a)==size(b) || return Inf + return maximum(abs.(Int16.(a).-Int16.(b))) +end +include("rendering_checks.jl") +end diff --git a/test/support/imports.jl b/test/support/imports.jl new file mode 100644 index 000000000..651db2d21 --- /dev/null +++ b/test/support/imports.jl @@ -0,0 +1,54 @@ +@testsnippet UseBaseParamsSupport begin + using Measurements + using Measurements: measurement, uncertainty, value + using LineCableModels + using LineCableModels.Materials + using LineCableModels.DataModel + using LineCableModels.DataModel.BaseParams +end + +@testsnippet UseDataModelSupport begin + using DataFrames + using Measurements + using Measurements: measurement, uncertainty, value + using LineCableModels + using LineCableModels.Materials + using LineCableModels.DataModel + using LineCableModels.DataModel.BaseParams + using LineCableModels.Earth + using LineCableModels.Engine + using LineCableModels.ImportExport +end + +@testsnippet UseEngineSupport begin + using DataFrames + using Measurements + using Measurements: measurement, uncertainty, value + using LineCableModels + using LineCableModels.DataModel + using LineCableModels.DataModel.BaseParams + using LineCableModels.Earth + using LineCableModels.Engine + using LineCableModels.ParametricBuilder + using LineCableModels.UQ + using LineCableModels.ImportExport +end + +@testsnippet UseNativePlotSupport begin + using DataFrames + using Measurements + using LineCableModels + using LineCableModels.DataModel + using LineCableModels.Earth + using LineCableModels.Engine + using LineCableModels.UQ +end + +@testsnippet UseImportExportSupport begin + using DataFrames + using LineCableModels + using LineCableModels.DataModel + using LineCableModels.Earth + using LineCableModels.Engine + using LineCableModels.ImportExport +end diff --git a/test/support/internal_shunt.jl b/test/support/internal_shunt.jl new file mode 100644 index 000000000..1855427ea --- /dev/null +++ b/test/support/internal_shunt.jl @@ -0,0 +1,37 @@ +# Current local-domain inputs. The open screen permits testing wire and tape charge +# scattering. The independent annular operator controls are in internal_shunt.jl. +function internal_shunt_test_design(; + radius = 0.0003, epsilon = 3.0, tapes = true, count = 6, + suffix = "", core_radius = 0.002, wire_angle = 0.17) + metal=Material(kind = :conductor, rho = 2e-8, mu_r = 1.0, T0 = 20.0, alpha = 0.004) + insulation_material=Material(kind = :insulator, rho = Inf, eps_r = epsilon) + host_material=Material(kind = :insulator, rho = Inf, eps_r = 1.0) + a=core_radius+0.002 + screen_outer=a+2radius + t=0.00015 + b=screen_outer+(tapes ? t : zero(radius)) + open_parts=AbstractCablePart[terminal(Symbol(:middle, suffix), + Group(:round_screen, Region(:wire, Disk(radius), metal); + pattern = Ring(count; r = a+radius, φ0 = wire_angle)))] + if tapes + push!(open_parts, + terminal(Symbol(:middle, suffix), + Group(:strip_screen, Region(:strip, Rectangle(0.0012, t), metal); + pattern = Ring(1; r = screen_outer+t/2)))) + end + origin=Stack( + terminal(Symbol(:inner, suffix), Region(:axial, Disk(core_radius), metal)), + Region(:inner_dielectric, Shell(0.002), insulation_material), + Enclosure(:screen_domain, Stack(open_parts); primitive = Annulus(a, b), fill = host_material), + Region(:outer_dielectric, Shell(0.0008), insulation_material), + terminal(Symbol(:reference, suffix), Region(:closed_screen, Shell(0.0002), metal)), + Region(:cover, Shell(0.0007), insulation_material)) + return build(CableDesign, "open-screen-control"*suffix, origin) +end +function internal_shunt_test_domain(design) + owner=LineCableModels.Engine + blueprint=owner.flatten(LineCableModelsCoaxial(), design) + domains=owner.ShuntModel.internal_shunt_domains([design], [blueprint]) + length(domains)==1 || error("control requires exactly one resolved open-screen domain") + return only(domains), blueprint +end diff --git a/test/support/modal_formula_fixtures.jl b/test/support/modal_formula_fixtures.jl new file mode 100644 index 000000000..dfa7eee21 --- /dev/null +++ b/test/support/modal_formula_fixtures.jl @@ -0,0 +1,41 @@ +# A user-owned modal formula with fixed modal-to-phase maps. Items that need it take this +# module, so the items that use only `FormulaFixtures` do not reach ModalAnalysis. +@testmodule ModalFormulaFixtures begin + using LineCableModels + + struct FixedModalMaps{P, O} <: LineCableModels.AbstractFormulation + parameters::P + options::O + end + FixedModalMaps(voltage::AbstractArray{<:Number, 3}, + current::AbstractArray{<:Number, 3}) = FixedModalMaps( + (voltage = voltage, current = current), FormulationOptions()) + LineCableModels.formulation_options(selected::FixedModalMaps) = selected.options + LineCableModels.formula_id(::FixedModalMaps) = :FixedModalMaps + LineCableModels.formula_id(::Type{<:FixedModalMaps}) = :FixedModalMaps + LineCableModels.description(::FixedModalMaps; compact::Bool = false) = "FixedModalMaps" + LineCableModels.description(::Type{<:FixedModalMaps}; compact::Bool = false) = + "FixedModalMaps" + Base.NamedTuple(selected::FixedModalMaps) = (identifier = :FixedModalMaps, + parameters = selected.parameters, options = selected.options.data) + LineCableModels.description( + ::Type{<:FixedModalMaps}, ::Val{:voltage}, value::AbstractArray; + compact::Bool = false) = "voltage map="*sprint(show, value) + LineCableModels.description( + ::Type{<:FixedModalMaps}, ::Val{:current}, value::AbstractArray; + compact::Bool = false) = "current map="*sprint(show, value) + LineCableModels.Commons.initialize_buffers( + ::FixedModalMaps, ::Type, input, plan, buffers) = buffers + function LineCableModels.ModalAnalysis.decompose!(selected::FixedModalMaps, + workspace, parameters, options) + copyto!(workspace.Tv, selected.parameters.voltage) + copyto!(workspace.Ti, selected.parameters.current) + for frequency in axes(workspace.roots, 2), mode in axes(workspace.roots, 1) + + workspace.roots[mode, frequency] = sqrt((workspace.input.Y[ + :, :, frequency] * workspace.input.Z[:, :, frequency])[ + mode, mode])*workspace.input.root_scale + end + return workspace + end +end diff --git a/test/support/modal_study.jl b/test/support/modal_study.jl new file mode 100644 index 000000000..57da1f396 --- /dev/null +++ b/test/support/modal_study.jl @@ -0,0 +1,63 @@ +@testmodule ModalStudyFixtures begin + using LineCableModels + + # Explicit toy, nondispersive properties. These properties lack measurement or certification + # as broadband cable data. The armor uses an equivalent annulus without discrete wires. + function material(kind,rho,eps_r,mu_r,alpha;tan_delta=0.0) + return Material(kind,rho,eps_r,mu_r,20.0,alpha; + rho_thermal=0.0,theta_max=90.0,tan_delta,sigma_solar=0.0) + end + + function armored_design() + copper=material(:conductor,1.724e-8,1.0,1.0,0.00393) + steel=material(:conductor,1.5e-7,1.0,50.0,0.006) + semicon=material(:semicon,1.0,100.0,1.0,0.0) + insulation_material=material(:insulator,1e14,2.3,1.0,0.0) + bedding_material=material(:insulator,1e12,3.0,1.0,0.0) + jacket_material=material(:insulator,1e12,3.0,1.0,0.0) + return @cable "toy-armored-single-core" begin + @terminal :core begin + core(copper;r=0.012) + screen(semicon;t=0.0005) + insulation(insulation_material;t=0.006) + screen(semicon;t=0.0005) + end + @terminal :screen begin + sheath(copper;t=0.0008) + end + bedding(bedding_material;t=0.002) + @terminal :armor begin + sheath(steel;t=0.0015) + end + jacket(jacket_material;t=0.003) + end + end + + function study_problem(layout::Symbol; frequencies=10.0 .^ range(0,7;length=281)) + design=armored_design() + centers=if layout===:horizontal + ((-0.15,-1.2),(0.0,-1.2),(0.15,-1.2)) + elseif layout===:trefoil + ((-0.075,-1.2),(0.075,-1.2),(0.0,-1.2+sqrt(3)*0.15/2)) + else + throw(ArgumentError("layout must be :trefoil or :horizontal")) + end + connections=[Dict(:core=>3i-2,:screen=>3i-1,:armor=>3i) for i in 1:3] + system=build(LineCableSystem,[design,design,design], + [Pose2(x,y) for (x,y) in centers]; + connections,system_id="toy-armored-$(layout)",line_length=600.0, + environment=homogeneous(rho=100.0,eps_r=10.0,mu_r=1.0)) + problem=LineParametersProblem(system;temperature=20.0, + earth_props=homogeneous(rho=100.0,eps_r=10.0,mu_r=1.0), + frequencies=collect(frequencies)) + return (;design,system,problem,connections,centers) + end + + function study_formulation() + return Formulation(earth_impedance=:unified,earth_admittance=:unified, + shunt_model=:equivalent,insulation_admittance=:lossy, + semicon_admittance=:lossy; + options=(reduce_bundle=false,kron_reduction=false, + ideal_transposition=false)) + end +end diff --git a/test/support/numerical.jl b/test/support/numerical.jl new file mode 100644 index 000000000..67f1df824 --- /dev/null +++ b/test/support/numerical.jl @@ -0,0 +1,53 @@ +@testmodule TestNumerics begin + using LinearAlgebra + + precision_type(::Type{T}) where {T <: AbstractFloat} = T + precision_type(::Type{Complex{T}}) where {T <: AbstractFloat} = T + precision_type(::Type{<:Integer}) = Float64 + precision_type(::Type{<:Rational}) = Float64 + precision_type(value::Number) = precision_type(typeof(value)) + precision_type(values::AbstractArray) = precision_type(eltype(values)) + + magnitude(value::Number) = abs(value) + magnitude(values::AbstractArray) = norm(values) + + absolute_floor(::Type{T}; scale = one(T)) where {T <: AbstractFloat} = sqrt(eps(T)) * + abs(scale) + + function tolerances(actual, expected; factor = 64, scale = nothing) + T = promote_type(precision_type(actual), precision_type(expected)) + reference_scale = if isnothing(scale) + max(magnitude(actual), magnitude(expected), floatmin(T)) + else + convert(T, scale) + end + return (; rtol = factor * eps(T), atol = factor * eps(T) * reference_scale) + end + + function isapprox_scaled(actual, expected; factor = 64, scale = nothing) + tolerance = tolerances(actual, expected; factor, scale) + return isapprox(actual, expected; tolerance...) + end + + function normalized_residual(A, x, b) + numerator = norm(A * x - b) + denominator = max(norm(A) * norm(x) + norm(b), floatmin(Float64)) + return numerator / denominator + end + + function normalized_eigen_residual(A, eigenvalue, eigenvector) + numerator = norm(A * eigenvector - eigenvalue * eigenvector) + denominator = max( + norm(A) * norm(eigenvector) + abs(eigenvalue) * norm(eigenvector), + floatmin(Float64) + ) + return numerator / denominator + end + + function is_positive_semidefinite(matrix; factor = 256) + T = precision_type(matrix) + scale = max(opnorm(matrix), floatmin(T)) + tolerance = factor * eps(T) * max(size(matrix)...) * scale + return minimum(eigvals(Hermitian(matrix))) >= -tolerance + end +end diff --git a/test/support/radial_control.jl b/test/support/radial_control.jl new file mode 100644 index 000000000..cd76f7cea --- /dev/null +++ b/test/support/radial_control.jl @@ -0,0 +1,72 @@ +# Independent radial diffusion control. This solves E''+E'/r-k²E=0 via +# Frobenius recurrences, not the production Bessel functions or surface ratios. +module RadialControl +using LinearAlgebra +export fundamental, surfaces, annular_admittance +function fundamental(z) + iszero(z) && throw(ArgumentError("use regularity at the axis separately")) + t=one(z); u=t; du=zero(z); harmonic=zero(real(z)) + weighted=zero(z); dweighted=zero(z); magnitude=abs(t) + remainder=oftype(abs(z),Inf) + for n in 1:4096 + t*=z^2/(4n^2) + harmonic+=inv(oftype(real(z),n)) + u+=t; du+=2n*t/z + weighted+=harmonic*t; dweighted+=2n*harmonic*t/z + magnitude+=abs(t)*(1+abs(harmonic))*(1+2n/abs(z)) + ratio=abs(z)^2/(4(n+1)^2)*(n+2)/(n+1) + if ratio<1/2 + # The ratios of subsequent terms, harmonic weights and derivative + # weights decrease. This bounds all four omitted series tails. + next=abs(t)*abs(z)^2/(4(n+1)^2) + remainder=next*(1+harmonic+inv(oftype(harmonic,n+1)))*(1+2(n+1)/abs(z))/(1-ratio) + remainder < eps(real(z))*magnitude && break + end + n==4096 && error("inconclusive radial reference: series term limit") + end + logarithm=log(z/2)+oftype(real(z),Base.MathConstants.eulergamma) + v=weighted-logarithm*u + dv=dweighted-u/z-logarithm*du + # Roundoff allowance accounts for summed absolute terms, log and derivatives. + bound=(remainder+4096eps(real(z))*magnitude)*(2+abs(logarithm)+inv(abs(z))) + return (;u,du,v,dv,bound) +end +function surfaces(ri,ro,rho,mu,s) + pi=oftype(rho,π) + k=sqrt(s*mu/rho) + outer=fundamental(k*ro) + if iszero(ri) + value=outer.u/outer.du*s*mu/(2pi*ro*k) + # Denominator perturbation bound for the regular radial solution. + e=outer.bound + eplot isa Makie.Lines,panel.axis.scene.plots) + @assert length(curves)==(name=="uncertainty_intervals" ? 3 : 1) "wrong series count" + for curve in curves + points=curve[1][] + @assert length(points)==3 + expected=[eltype(points)(parameters.f[k],array[i,j,k]) for k in 1:3] + @assert isapprox(points,expected;rtol=8eps(Float32),atol=0) "wrong quantity, units, frequency or matrix entry" + @assert curve.visible[] "required series is hidden" + end + @assert occursin("Frequency",sprint(show,panel.axis.xlabel[])) + @assert !isempty(sprint(show,panel.axis.ylabel[])) + if name=="uncertainty_intervals" + bars=filter(plot->plot isa Makie.Errorbars,panel.axis.scene.plots) + @assert length(bars)==6 + @assert count(p->p.direction[]===:x,bars)==3 + @assert count(p->p.direction[]===:y,bars)==3 + @assert all(p->p.visible[] && p.whiskerwidth[]>0 && p.linewidth[]>0,bars) + for direction in (:x,:y) + selected=filter(p->p.direction[]===direction,bars) + for (width,bar) in enumerate(selected) + expected_error=direction===:x ? width*.01parameters.f : + [width*.01(i+j+k)*array[i,j,k] for k in 1:3] + @assert getindex.(bar[1][],1) ≈ parameters.f rtol=8eps(Float32) + @assert getindex.(bar[1][],2) ≈ array[i,j,:] rtol=8eps(Float32) + @assert getindex.(bar[1][],3) ≈ expected_error rtol=8eps(Float32) + @assert getindex.(bar[1][],4) ≈ expected_error rtol=8eps(Float32) + end + end + end + end + end + elseif startswith(name,"mc_") + result=actual_mc() + samples=vec(only(result.sample_values).R) + histogram=only(only(result.histogram_values).R) + handle=only(handles);axis=only(handle.axes) + @assert isapprox(sum(histogram.density.*diff(histogram.edges)),1;rtol=1e-10) + if name=="mc_hist" + native=only(filter(p->p isa Makie.Hist,axis.scene.plots)) + @assert native[1][]==samples "histogram uses another marginal" + elseif name=="mc_pdf" + native=only(filter(p->p isa Makie.Stairs,native_primitives(axis.scene.plots))) + expected=[eltype(native[1][])(x,y) for (x,y) in zip(histogram.edges,[histogram.density;last(histogram.density)])] + @assert isapprox(native[1][],expected;rtol=8eps(Float32),atol=0) + elseif name=="mc_ecdf" + native=only(filter(p->p isa Makie.Stairs,native_primitives(axis.scene.plots))) + sorted=sort(samples);unique_samples=unique(sorted) + x=[first(sorted)-eps(first(sorted));unique_samples] + y=[count(v->v<=value,sorted)/length(sorted) for value in x] + expected=[eltype(native[1][])(a,b) for (a,b) in zip(x,y)] + @assert isapprox(native[1][],expected;rtol=8eps(Float32),atol=0) "ECDF uses another population" + @assert first(y)==0 && last(y)==1 + else + native=only(filter(p->p isa Makie.Scatter,axis.scene.plots)) + sorted=sort(samples);mass=cumsum(histogram.density.*diff(histogram.edges)) + model=map(eachindex(sorted)) do i + probability=(i-.5)/length(sorted) + bin=searchsortedfirst(mass,probability) + prior=bin==1 ? 0. : mass[bin-1] + histogram.edges[bin]+(probability-prior)/histogram.density[bin] + end + expected=[eltype(native[1][])(x,y) for (x,y) in zip(sorted,model)] + @assert isapprox(native[1][],expected;rtol=8eps(Float32),atol=0) "wrong QQ probability or marginal" + end + elseif name=="uncertainty_intervals" + error("unreachable uncertainty validation") + elseif name in ("cable_preview","cable_preview_compact","system_preview") + handle=only(handles);axis=only(handle.axes) + @assert occursin("m",sprint(show,axis.xlabel[])) && occursin("m",sprint(show,axis.ylabel[])) + @assert count(p->p isa Makie.Poly,axis.scene.plots)>=4 "missing material layer" + elseif name=="material_scale" + @assert length(only(handles).colorbars)==3 "missing material/property category" + elseif name in ("formulation_comparison","uq_comparison") + handle=only(handles) + @assert length(handle.plot_state.order)>=2 "comparison lost an operand" + @assert length(unique(values(handle.plot_state.labels)))>=2 "comparison lost result identity" + @assert length(handle.axes)==4 + elseif name=="custom_layout" + @assert length(only(handles).axes)==2 + @assert [axis.title[] for axis in only(handles).axes] == ["Quadratic","Discrete samples"] + end + return true +end + +function defect!(name,handle) + if name=="material_scale" + bar=first(handle.colorbars) + bar.colormap[]=Makie.to_colormap([:red,:red]) + return "wrong material color" + elseif name=="custom_layout" + xlims!(last(handle.axes),10.,20.) + return "required samples clipped" + end + plots=native_primitives(collect(Iterators.flatten(axis.scene.plots for axis in handle.axes))) + selected=name=="uncertainty_intervals" ? filter(p->p isa Makie.Errorbars,plots) : + occursin("preview",name) ? filter(p->p isa Makie.Poly,plots) : + filter(p->p isa Union{Makie.Lines,Makie.Hist,Makie.Stairs,Makie.ECDFPlot,Makie.Scatter},plots) + isempty(selected) && error("no applicable visible primitive for $name") + if name=="uncertainty_intervals" + foreach(p->p.visible[]=false,selected) + return "uncertainty bars hidden" + end + first(selected).visible[]=false + return occursin("preview",name) ? "material layer removed" : "required series omitted" +end diff --git a/test/support/runner.jl b/test/support/runner.jl new file mode 100644 index 000000000..39840fa1e --- /dev/null +++ b/test/support/runner.jl @@ -0,0 +1,243 @@ +module ValidationTestRunner + +using TestItemRunner, TOML, Test + +include(joinpath(@__DIR__, "taxonomy.jl")) + +# Tags describe purpose or environment, never the observed outcome. Explicit +# selectors can reach every item, including those excluded from ordinary runs. +const ORDINARY_EXCLUDED_TAGS = Set((:quality, :aqua, :visual, :core_only, + :fem_numerical, :pscad_native)) +# The tags of the additional environments. `changed:REF` excludes them too. +const ENVIRONMENT_TAGS = setdiff(ORDINARY_EXCLUDED_TAGS, (:quality,)) + +# The paths changed since the git revision `reference`: modified, added, deleted and +# untracked files, relative to `root`. Both sides of a rename count. +function changed_paths(reference, root) + git(arguments...) = Cmd(["git", "-C", root, arguments...]) + success(pipeline(git("rev-parse", "--verify", "--quiet", reference * "^{commit}"); + stdout=devnull, stderr=devnull)) || error("Unknown git revision for changed: $reference") + tracked = readlines(git("diff", "--name-only", "--no-renames", "--relative", reference)) + untracked = readlines(git("ls-files", "--others", "--exclude-standard")) + return sort!(unique!([tracked; untracked])) +end + +# The earliest owner of the changed `src/` and `ext/` paths, or nothing. +source_owner(paths, owners) = + earliest(Symbol[owner for owner in (path_owner(owners, p) for p in paths) if owner !== nothing]) + +# The items that changes to `paths` can affect, as (file, name) pairs: +# - every `quality` item. +# - every non-slow item whose owner is the earliest owner of a changed `src/` or `ext/` +# path, or a later owner. +# - every item of a changed test file, slow or not, and of the files that use a setup +# defined in a changed test file. +# - after a change under `test/support/`, every non-slow item and every item under +# `test/unit/core/`, which checks the runner. +# Items with an environment tag remain excluded, as in ordinary runs. +function changed_items(paths, items, setups, owners) + floor = source_owner(paths, owners) + support = any(startswith("test/support/"), paths) + files = Set(paths) + shared = Set(name for (name, file) in setups + if file in files && !startswith(file, "test/support/")) + selected = Set{Tuple{String, String}}() + for item in items + any(in(ENVIRONMENT_TAGS), item.tags) && continue + owner = owner_tag(item.tags) + affected = :quality in item.tags || item.file in files || + any(in(shared), item.setups) || + (support && startswith(item.file, "test/unit/core/")) || + (:slow ∉ item.tags && (support || (floor !== nothing && owner !== nothing && + rank(owner) >= rank(floor)))) + affected && push!(selected, (item.file, item.name)) + end + return selected +end + +# The non-quality items `changed:REF` selects. Its quality items are all of them. +function changed_selection(reference, root) + paths = changed_paths(reference, root) + (; items, setups) = inventory(root) + owners = loaded_owners(root) + selected = changed_items(paths, items, setups, owners) + println("changed:", reference, ": ", length(paths), " changed paths; earliest source owner ", + something(source_owner(paths, owners), "none"), "; ", + count(p -> any(i -> i.file == p, items), paths), " changed test files; test/support ", + any(startswith("test/support/"), paths) ? "changed" : "unchanged") + return setdiff(selected, Set((i.file, i.name) for i in items if :quality in i.tags)) +end + +# The revision of a `changed:REF` selector, or nothing. It takes no other selector. +function changed_reference(arguments) + queries = filter(!=("--list"), arguments) + any(startswith("changed:"), queries) || return nothing + length(queries) == 1 || error("changed:REF cannot be combined with other selectors") + reference = chop(only(queries); head=8, tail=0) + isempty(reference) && error("Empty changed: revision") + return reference +end + +# Runs the `ordinary` items, as (file, name) pairs, in this process, then the quality +# selectors in a fresh process, exactly as `tag:quality` runs alone: the reflection +# guards read the live method table, which earlier items may extend. Fails if either +# part fails. +function run_changed(root, ordinary; list=false, quality=["tag:quality"]) + failure = nothing + if isempty(ordinary) + println("No items outside quality selected") + else + try + run_tests(root; list, verbose=true, filter=item -> + (relative(item.filename, root), String(item.name)) in ordinary) + catch caught + caught isa InterruptException && rethrow() + failure = caught + end + end + arguments = [joinpath(root, "test", "runtests.jl"); quality; list ? ["--list"] : String[]] + command = `$(Base.julia_cmd()) --project=$(dirname(Base.active_project())) $arguments` + println("Quality items in a fresh process: ", join(quality, " ")) + flush(stdout) + passed = success(run(ignorestatus(command))) + failure === nothing || throw(failure) + passed || error("The quality items failed in their fresh process") + return nothing +end + +# Runs the selection that `arguments` names, from the test directory `directory`. +function execute(arguments, directory) + root = dirname(rstrip(abspath(directory), ['/', '\\'])) + reference = changed_reference(arguments) + reference === nothing && return run_tests(root; verbose=true, list="--list" in arguments, + filter=selection(arguments, directory)) + return run_changed(root, changed_selection(reference, root); list="--list" in arguments) +end + +function selection(arguments, directory; excluded=ORDINARY_EXCLUDED_TAGS) + queries = filter(!=("--list"), arguments) + any(startswith("--"), queries) && error("Unknown test option; supported option: --list") + changed_reference(arguments) === nothing || error("changed:REF runs through `execute`, ", + "which runs its quality items in a fresh process") + tags = [Symbol(chop(q; head=4, tail=0)) for q in queries if startswith(q, "tag:")] + names = lowercase.(filter(q -> !startswith(q, "tag:"), queries)) + any(isempty, names) && error("Empty test selector") + Symbol("") in tags && error("Empty test tag") + prefix = abspath(directory) * Base.Filesystem.path_separator + return item -> begin + startswith(abspath(item.filename), prefix) || return false + # A search by filename or item name must not accidentally launch a live station. + :pscad_native in item.tags && :pscad_native ∉ tags && return false + isempty(queries) && return isempty(intersect(excluded, item.tags)) + tag_match = isempty(tags) || any(in(item.tags), tags) + name_match = isempty(names) || any(names) do query + occursin(query, lowercase(relpath(item.filename, directory))) || + occursin(query, lowercase(String(item.name))) + end + tag_match && name_match + end +end + +# The items and setups under `root/test`, read without running them. Each item +# has its file relative to `root`, name, tags and setups. `setups` maps each setup +# name to the file that defines it. +function inventory(root) + root = abspath(root) + prefix = joinpath(root, "test", "") + items = @NamedTuple{file::String, name::String, tags::Vector{Symbol}, + setups::Vector{Symbol}}[] + setups = Dict{Symbol, String}() + for file in TestItemRunner.find_test_files(root) + startswith(abspath(file), prefix) || continue + tree = TestItemRunner.JuliaSyntax.parseall(TestItemRunner.JuliaSyntax.SyntaxNode, + read(file, String); filename=file) + found, defined, errors = [], [], [] + TestItemRunner.TestItemDetection.find_test_detail!(tree, found, defined, errors) + isempty(errors) || error("Invalid test item or setup definition in $file") + path = relative(file, root) + for item in found + push!(items, (; file=path, name=String(item.name), tags=item.option_tags, + setups=item.option_setup)) + end + for setup in defined + setups[Symbol(setup.name)] = path + end + end + return (; items, setups) +end + +function validate_config(path) + isfile(path) || return + document = TOML.parsefile(path) + unknown = setdiff(keys(document), ("config-version", "include", "exclude")) + isempty(unknown) || error("Invalid test discovery keys in $path: $(join(unknown, ", "))") + version = get(document, "config-version", 1) + version isa Integer && !(version isa Bool) && version == 1 || + error("Unsupported test discovery config-version in $path: $version") + for key in ("include", "exclude") + patterns = get(document, key, String[]) + patterns isa AbstractVector && all(p -> p isa String, patterns) || + error("Test discovery $key must be an array of strings in $path") + end + return +end + +# `on_start` receives each item name as the item starts. +# `test/tools/owners.jl` uses it to record coverage per item. +function run_tests(root; filter=(_ -> true), verbose=true, list=false, on_start=(_ -> nothing)) + started = time_ns() + root = abspath(root) + validate_config(joinpath(root, "JuliaTestItems.toml")) + # File selection belongs to the installed runner. Excluded + # source cannot introduce a setup even when an included item requests it. + files = TestItemRunner.find_test_files(root) + checked = Set([root]) + for file in files + directory = dirname(file) + while directory != root + if directory ∉ checked + validate_config(joinpath(directory, "JuliaTestItems.toml")) + push!(checked, directory) + end + directory = dirname(directory) + end + # Item extraction alone can overlook invalid source after an item. + TestItemRunner.JuliaSyntax.parseall(TestItemRunner.JuliaSyntax.SyntaxNode, + read(file, String); filename=file) + end + selected = NamedTuple[] + finished = false + # Keep native Test reporting and failure propagation. This factory only + # announces item starts, which identifies its last active scope. + function testset(description; verbose) + if any(item -> item.name == description, selected) + println("Starting [", round((time_ns() - started) / 1e9; digits=2), + "s] ", description) + flush(stdout) + on_start(description) + end + Test.DefaultTestSet(description; verbose) + end + try + result = TestItemRunner.run_tests(root; verbose, testset, filter=item -> begin + accepted = filter(item) + if accepted + push!(selected, (; name=String(item.name), file=relpath(item.filename, root))) + list && println(relpath(item.filename, root), " | ", item.name, + " | ", join(string.(item.tags), ",")) + end + accepted && !list + end) + isempty(selected) && error("No test items selected in $root; check selectors and excluded tags") + finished = true + return result + finally + println(list ? "Listed " : "Selected ", length(selected), " maintained test items in ", + length(unique(item.file for item in selected)), " files; ", + list ? "no test bodies executed" : finished ? "run completed" : "run failed", "; ", + round((time_ns() - started) / 1e9; digits=2), " s elapsed") + flush(stdout) + end +end + +end diff --git a/test/support/scenarios.jl b/test/support/scenarios.jl new file mode 100644 index 000000000..dfbde69a1 --- /dev/null +++ b/test/support/scenarios.jl @@ -0,0 +1,187 @@ +# Current construction recipes. These are inputs and routing controls, never +# numerical snapshots. Scientific expectations belong beside the relevant test. +module CurrentScenarios + export conductor_material, coaxial_design, three_phase_system, two_wire_system, + line_parameters_problem, channel_value, two_conductor_results, cable_monte_carlo_result, + three_bare_wires_problem, three_bare_wires_layouts, preservation_corpus + using LineCableModels + using LineCableModels.DocStringExtensions: TYPEDSIGNATURES + using Statistics: mean + + "Signed wire heights \\[m\\] for the five three-wire interface placements." + const three_bare_wires_layouts = ( + all_air=(1.0, 1.0, 1.0), all_earth=(-1.0, -1.0, -1.0), + air_1=(1.0, -1.0, -1.0), air_2=(-1.0, 1.0, -1.0), + air_3=(-1.0, -1.0, 1.0)) + + """ + $(TYPEDSIGNATURES) + + Construct three identical bare copper wires with separate left-to-right + terminals. This physical fixture selects no analytical or FEM formulation. + + # Keywords + + - `heights`, `horizontal`: three signed vertical and horizontal coordinates \\[m\\]. + - `radius`: wire radius \\[m\\]. + - `copper`: conductor material. Defaults to the materials library's copper. + - `rho`: soil resistivity \\[Ω·m\\]. + - `eps_r`, `mu_r`: soil relative permittivity and permeability \\[dimensionless\\]. + - `temperature`: operating temperature \\[°C\\]. + - `line_length`: line length \\[m\\]. + - `frequencies`: analysis frequencies \\[Hz\\]. + - `name`: physical system identifier. + + # Returns + + - A `LineParametersProblem` with no prescribed longitudinal propagation constant. + """ + function three_bare_wires_problem(; heights=(1.0, 1.0, 1.0), + horizontal=(0.0, 1.0, 2.0), radius=0.0425, + copper=Material(MaterialsLibrary(add_defaults=true), :copper), + rho=0.1, eps_r=1.0, mu_r=1.0, temperature=20.0, + line_length=1.0, frequencies=10.0 .^ (-1:7), name="three_bare_wires") + length(heights) == length(horizontal) == 3 || + throw(ArgumentError("three wire coordinates are required")) + design = build(CableDesign, name, Stack( + Group(:core, Region(:core_metal, Disk(radius), copper)))) + system = build(LineCableSystem, fill(design, 3), + [Pose2(horizontal[i], heights[i]) for i in 1:3]; + connections=[Dict(:core=>i) for i in 1:3], system_id=name, line_length) + return LineParametersProblem(system; temperature, frequencies, + earth_props=homogeneous(; rho, eps_r, mu_r)) + end + + conductor_material() = Material(kind=:conductor, rho=2e-8, eps_r=1.0, + mu_r=1.0, T0=20.0, alpha=0.004) + + function coaxial_design(; scale=1.0, name="concentric-control", nominal_data=nothing) + dielectric = Material(kind=:insulator, rho=1e8, eps_r=3.0, mu_r=1.0) + return build(CableDesign, name, Stack( + terminal(:core, Region(:metal, Disk(0.005scale), conductor_material())), + Region(:dielectric, Shell(0.005scale), dielectric), + terminal(:sheath, Region(:return, Shell(0.001scale), conductor_material())), + Region(:cover, Shell(0.001scale), dielectric)); nominal_data) + end + + function three_phase_system(; line_length=600.0, spacing=0.08, nominal_data=nothing) + designs = [coaxial_design(; name="phase-$phase", nominal_data) for phase in 1:3] + return build(LineCableSystem, designs, + [(-spacing, -1.0), (0.0, -1.0-spacing), (spacing, -1.0)]; + system_id="current-three-phase", line_length, + connections=[Dict(:core=>phase, :sheath=>0) for phase in 1:3]) + end + + function two_wire_system() + designs = [build(CableDesign, "wire-$i", Stack( + terminal(:core, Region(:metal, Disk(0.005), conductor_material())), + Region(:cover, Shell(0.001), Material(kind=:insulator, rho=1e8, eps_r=3.0)))) + for i in 1:2] + return build(LineCableSystem, designs, [(0.0,-1.0), (0.2,-1.3)]; + system_id="current-two-wire", line_length=600.0, + connections=[Dict(:core=>i) for i in 1:2]) + end + + # Passing the system is essential when a consumer checks its terminal identity. + function line_parameters_problem(system=three_phase_system(); frequencies=[50.0]) + return LineParametersProblem(system; temperature=20.0, + earth_props=homogeneous(rho=100.0, eps_r=10.0, mu_r=1.0), frequencies) + end + + # Deliberately asymmetric metadata: this tests routing, not passive physics. + # Every quantity, coordinate and frequency has a distinct declared value. + channel_value(::Val{:R}, i,j,k) = 1e-4*(1+2i+3j+k) + channel_value(::Val{:L}, i,j,k) = 1e-7*(2+i+4j+2k) + channel_value(::Val{:G}, i,j,k) = 1e-9*(3+3i+j+3k) + channel_value(::Val{:C}, i,j,k) = 1e-10*(4+4i+2j+k) + function two_conductor_results(; frequencies=[10.0,100.0,1000.0]) + f=collect(frequencies) + channels=NamedTuple{(:R,:L,:G,:C)}(Tuple( + [channel_value(Val(q),i,j,k) for i in 1:2,j in 1:2,k in eachindex(f)] + for q in (:R,:L,:G,:C))) + omega=reshape(2pi.*f,1,1,:) + return LineParameters(channels.R .+ im.*omega.*channels.L, + channels.G .+ im.*omega.*channels.C, f; basis=:pul, + details=ComputationDetails(;coordinates=["west","east"],)) + end + + # Independent arrays and histogram storage for each observable. This object + # tests publication and retention only. Actual Monte Carlo validation runs compute. + function cable_monte_carlo_result() + samples=(R=reshape([2.0,3.0,5.0,8.0],1,:), + L=reshape([11.0,13.0,17.0,19.0].*1e-6,1,:), + C=reshape([23.0,29.0,31.0,37.0].*1e-10,1,:), + G=reshape([41.0,43.0,47.0,53.0].*1e-9,1,:)) + statistics=map(x->[SampleSummary(vec(x))],samples) + histograms=map(x->[HistogramDensity(vec(x);bins=2)],samples) + representation=CableConstants(mean(samples.R),mean(samples.L), + mean(samples.C),mean(samples.G)) + representation=LineCableModels.materialize(representation,statistics) + formulation=MonteCarlo(Formulation();trials=4,seed=2027, + return_samples=true,return_histograms=true) + return MonteCarloResult(formulation,[representation],[statistics], + [samples],[histograms],UInt64(2027),UInt64[2039],[4]) + end + + # Counters of the equivalence check: `materialized()` runs once per point a + # parametric scenario realizes, and `nominal_data` reaches its cable designs. + const NO_COUNTERS = (materialized=Returns(nothing), nominal_data=nothing) + + # Parametric study: earth resistivity and spacing, combined as `combine` asks, crossed + # with two insulation admittance formulations. + study_space(frequencies, combine, counters) = Gridspace{LineParametersProblem}( + (rho, spacing) -> (counters.materialized(); + LineParametersProblem(three_phase_system(; spacing, counters.nominal_data); + temperature=20.0, earth_props=homogeneous(; rho, eps_r=10.0, mu_r=1.0), + frequencies)), (Grid((100.0, 1000.0)), Grid((0.08, 0.1))); combine) + study_formulations() = Formulation(insulation_admittance=Grid((:default, :lossy))) + + # One uncertain cable spacing [m]. Its evaluation requires the Measurements extension. + uncertain_space(frequencies, counters) = Gridspace{LineParametersProblem}( + spacing -> (counters.materialized(); line_parameters_problem( + three_phase_system(; spacing, counters.nominal_data); frequencies)), + (Grid(0.08, AbsoluteError(0.002)),)) + + """ + $(TYPEDSIGNATURES) + + The preservation corpus has one scenario per computation shape. The allocation + ceilings, the timing comparison and the equivalence check share it. + Each scenario is the tuple of positional arguments of `compute`, with `n` analysis + frequencies between 10 Hz and 100 kHz. The CableConstants problem has its own + single frequency. LinearError and MonteCarlo require the Measurements extension. + New scenarios are appended, never inserted: the allocation counts depend on what a + process computed before. `counters` instruments the parametric scenarios for the + equivalence check and leaves their results unchanged. + """ + function preservation_corpus(n::Integer; counters=NO_COUNTERS) + frequencies = collect(10.0 .^ range(1, 5; length=n)) + coaxial = line_parameters_problem(three_phase_system(); frequencies) + # On an earth of three soil layers, the default earth formulas consume an + # equivalent earth. + layered = LineParametersProblem(three_phase_system(); temperature=20.0, + earth_props=build(EarthModel, (layer(rho=100.0, eps_r=10.0, thickness=0.5), + layer(rho=500.0, eps_r=20.0, thickness=2.0), layer(rho=50.0, eps_r=15.0))), + frequencies) + before_fd = formula(:default; equivalent_earth=formula(:bottommost; order=:before)) + return ( + coaxial=(coaxial, Formulation()), + overhead=(three_bare_wires_problem(; + heights=three_bare_wires_layouts.all_air, frequencies), Formulation()), + buried=(three_bare_wires_problem(; + heights=three_bare_wires_layouts.all_earth, frequencies), Formulation()), + modal=(coaxial, Formulation(), ModalAnalysisFormulation(:default)), + cable_constants=(CableConstantsProblem(coaxial_design()), CableConstantsFormulation()), + product=(ParametricProblem(study_space(frequencies, :product, counters)), + Combinatorial(study_formulations())), + zip=(ParametricProblem(study_space(frequencies, :zip, counters)), + Combinatorial(study_formulations())), + linear_error=(ParametricProblem(uncertain_space(frequencies, counters)), + LinearError(Formulation())), + monte_carlo=(ParametricProblem(uncertain_space(frequencies, counters)), + MonteCarlo(Formulation(); trials=4, seed=2027)), + three_layer=(layered, Formulation()), + three_layer_before_fd=(layered, Formulation(earth_properties=formula(:alipio2014), + earth_impedance=before_fd, earth_admittance=before_fd))) + end +end diff --git a/test/support/sector_control.jl b/test/support/sector_control.jl new file mode 100644 index 000000000..d7d5f0caa --- /dev/null +++ b/test/support/sector_control.jl @@ -0,0 +1,41 @@ +# Independent geometric boundary construction for the rounded disk-wedge intersection. +# Circle centers follow signed-distance constraints to the two wedge lines. +# Green integrals establish area and first moments without production contacts. +module SectorControl +using QuadGK +function boundary(span,base,back,fillet) + pi=oftype(base,π) + theta=span/2 + axis=(base+fillet,zero(base)) + along=-(base+fillet)*cos(theta)+sqrt((back-fillet)^2-((base+fillet)*sin(theta))^2) + upper=axis.+along.*(cos(theta),sin(theta));lower=(upper[1],-upper[2]) + upnormal=(-sin(theta),cos(theta));downnormal=(-sin(theta),-cos(theta)) + upbase=axis.+fillet.*upnormal;downbase=axis.+fillet.*downnormal + uplink=upper.+fillet.*upnormal;downlink=lower.+fillet.*downnormal + beta=atan(upper[2],upper[1]) + arc(c,r,a,b)=t->begin + phi=a+(b-a)*t + (c[1]+r*cos(phi),c[2]+r*sin(phi),-r*sin(phi)*(b-a),r*cos(phi)*(b-a)) + end + line(p,q)=t->(p[1]+t*(q[1]-p[1]),p[2]+t*(q[2]-p[2]),q[1]-p[1],q[2]-p[2]) + return (line(downbase,downlink),arc(lower,fillet,3pi/2-theta,2pi-beta), + arc((zero(base),zero(base)),back,-beta,beta),arc(upper,fillet,beta,pi/2+theta), + line(uplink,upbase),arc(axis,fillet,pi/2+theta,3pi/2-theta)) +end +function moments(span,base,back,fillet) + curves=boundary(span,base,back,fillet) + sums=zeros(typeof(base),4);errors=zeros(typeof(base),4) + goal=precision(base)<=128 ? 1e-8 : precision(base)<=256 ? 1e-10 : 1e-12 + for curve in curves,index in 1:4 + value,estimate=quadgk(zero(base),one(base);rtol=oftype(base,goal),maxevals=10^6) do t + x,y,dx,dy=curve(t) + ((x*dy-y*dx)/2,x^2*dy/2,-y^2*dx/2,hypot(dx,dy))[index] + end + sums[index]+=value;errors[index]+=estimate + end + return (area=sums[1],centroid=(sums[2]/sums[1],sums[3]/sums[1]), + perimeter=sums[4],errors=(area=errors[1],perimeter=errors[4], + centroid=((errors[2]+abs(sums[2]/sums[1])*errors[1])/(sums[1]-errors[1]), + (errors[3]+abs(sums[3]/sums[1])*errors[1])/(sums[1]-errors[1])))) +end +end diff --git a/test/support/taxonomy.jl b/test/support/taxonomy.jl new file mode 100644 index 000000000..fd1520c55 --- /dev/null +++ b/test/support/taxonomy.jl @@ -0,0 +1,122 @@ +# The test taxonomy, defined once. `test/support/runner.jl`, the architecture guards +# and the tools under `test/tools/` include this file. It uses only Base. +# +# Each item outside `quality` has exactly one owner tag, the latest layer, in +# load order, whose code the item exercises. A change in one layer can affect only +# that layer and the layers loaded after it. + +# Owner tags in load order: the core layers, then the extensions. +const OWNERS = (:units, :commons, :materials, :earth, :datamodel, :engine, + :parametric, :uq, :report, :importexport, :pscad, + :measurements, :distributions, :xlsx, :fem, :makie) + +# The core modules in load order, each with its owner tag. Modules without a tag of +# their own take the tag of the preceding owner. +const MODULE_OWNERS = (Units = :units, Commons = :commons, TextDisplay = :commons, + PlotBuilder = :commons, Materials = :materials, Earth = :earth, + DataModel = :datamodel, Engine = :engine, + ParametricBuilder = :parametric, UQ = :uq, ReportBuilder = :report, + ImportExport = :importexport, PSCAD = :pscad) + +const EXTENSION_OWNERS = (LineCableModelsMeasurementsExt = :measurements, + LineCableModelsDistributionsExt = :distributions, LineCableModelsXLSXExt = :xlsx, + LineCableModelsGmshExt = :fem, LineCableModelsMakieExt = :makie, + LineCableModelsCairoMakieExt = :makie, LineCableModelsGLMakieExt = :makie, + LineCableModelsWGLMakieExt = :makie) + +# Each item has at least one kind tag. +const KINDS = (:unit, :integration, :extension, :visual, :quality, :aqua) + +# A path relative to `directory`, with `/` separators on every platform. +relative(path, directory) = join(splitpath(relpath(path, directory)), "/") + +# The files under `directory` that belong to the checkout: the files git tracks and the +# new files it would add, never ignored ones such as local captures under +# `test/fixtures/`. A local run and CI then read the same files. Outside a git work tree, +# every file under `directory`. +function repository_files(directory) + isdir(directory) || return String[] + listing = IOBuffer() + command = `git -C $directory ls-files --cached --others --exclude-standard -z` + if success(pipeline(command; stdout = listing, stderr = devnull)) + names = split(String(take!(listing)), '\0'; keepempty = false) + return sort!(filter(isfile, [joinpath(directory, name) for name in names])) + end + return sort!([joinpath(path, name) for (path, _, names) in walkdir(directory) for name in names]) +end + +rank(owner::Symbol) = something(findfirst(==(owner), OWNERS), 0) +# The first owner tag among `tags`, or nothing. +owner_tag(tags) = (i = findfirst(in(OWNERS), tags); i === nothing ? nothing : tags[i]) +latest(owners) = isempty(owners) ? nothing : OWNERS[maximum(rank, owners)] +earliest(owners) = isempty(owners) ? nothing : OWNERS[minimum(rank, owners)] + +# The literal `include` paths of a source file, in order. +function included(path) + found = String[] + visit(x) = if Meta.isexpr(x, :call) && x.args[1] === :include && length(x.args) == 2 && + x.args[2] isa String + push!(found, normpath(joinpath(dirname(path), x.args[2]))) + elseif x isa Expr + foreach(visit, x.args) + end + visit(Meta.parseall(read(path, String); filename = path)) + return found +end + +# The module a file declares at top level, also behind a docstring. +function declared_module(path) + for x in Meta.parseall(read(path, String); filename = path).args + Meta.isexpr(x, :macrocall) && (x = x.args[end]) + Meta.isexpr(x, :module) && return x.args[2] + end + return nothing +end + +# Each Julia file the package loads, relative to `repository`, with the owner of its +# load position. `src/LineCableModels.jl` includes the core modules in load order. A file +# it includes directly takes the owner of the last module included before it, or the +# first owner. Extensions take their own owners. +function loaded_owners(repository) + found = Dict{String, Symbol}() + function load!(path, owner) + file = relative(path, repository) + haskey(found, file) && error("$file is included more than once") + found[file] = owner + foreach(p -> load!(p, owner), included(path)) + end + entry = joinpath(repository, "src", "LineCableModels.jl") + found[relative(entry, repository)] = first(OWNERS) + owner = first(OWNERS) + for path in included(entry) + name = declared_module(path) + name !== nothing && haskey(MODULE_OWNERS, name) && (owner = MODULE_OWNERS[name]) + load!(path, owner) + end + for (name, extension) in pairs(EXTENSION_OWNERS) + for path in (joinpath(repository, "ext", "$name.jl"), + joinpath(repository, "ext", "$name", "$name.jl")) + isfile(path) && load!(path, extension) + end + end + return found +end + +# The earliest owner loaded from `directory`, or nothing. +directory_owner(owners, directory) = + earliest([owner for (file, owner) in owners if startswith(file, directory * "/")]) + +# The owner of a path under `src/` or `ext/`: the owner of its load position, or, for a +# file the package does not load, the earliest owner loaded from its nearest enclosing +# directory. Nothing for other paths. +function path_owner(owners, path) + haskey(owners, path) && return owners[path] + startswith(path, "src/") || startswith(path, "ext/") || return nothing + directory = dirname(path) + while !isempty(directory) + owner = directory_owner(owners, directory) + owner === nothing || return owner + directory = dirname(directory) + end + return first(OWNERS) +end diff --git a/test/support/unified.jl b/test/support/unified.jl new file mode 100644 index 000000000..a94eb999c --- /dev/null +++ b/test/support/unified.jl @@ -0,0 +1,82 @@ +@testmodule UnifiedFormulaFixtures begin + using LineCableModels + const E=LineCableModels.Engine + const G=LineCableModels.Commons + + # Numerical controls use the production allocator. There is no second array layout. + function buffers(geometry) + T=eltype(geometry.radius) + return G.initialize_buffers(E.EarthImpedance.Formula(:unified), T, (;), (; geometry), + (; observations = nothing)) + end + + # Build a real public problem and workspace. Independent equal-medium controls + # replace completed material values after material evaluation. + # Their artificial air conductivity is confined to this test fixture. + function workspace(geometry, + state, + integration = E.formulation_options(E.SpectralIntegral, ( + method = :quad, options = (rtol = 1e-9,)))) + T=eltype(geometry.radius) + metal=Material(:conductor, T(1.7e-8)) + origin=Pose2(zero(T), zero(T), zero(T)) + designs=[build(CableDesign, + "radial-$i", + Group(:core, origin, + Region(:core, Disk(radius), metal), nothing, nothing, nothing)) + for (i, radius) in enumerate(geometry.radius)] + system=build(LineCableSystem, designs, + [Pose2(x, y) for (x, y) in zip(geometry.horizontal, geometry.height)]; + connections = [Dict(:core=>i) for i in eachindex(designs)], line_length = one(T)) + earth=convert(EarthModel{T}, homogeneous(rho = T(100))) + problem=LineParametersProblem(system; temperature = T(20), + earth_props = earth, frequencies = T[imag(state.jω) / (2pi)]) + selected=Formulation( + earth_impedance = formula(:unified; + options = (Γ = state.Γ, + integration = (method = :quad, options = integration.options))), + earth_admittance = formula(:unified; + options = (Γ = state.Γ, + integration = (method = :quad, options = integration.options))), + options = ( + reduce_bundle = false, kron_reduction = false, ideal_transposition = false)) + execution=E.computation_options(LineCableModelsCoaxial, ComputationOptions()) + blueprints=only(E.flatten(LineCableModelsCoaxial(), problem.system.designs, T, [selected])) + prepared=E.LineParametersWorkspace(problem, selected, execution, blueprints) + E.materials!(prepared, selected) + E.materials!(prepared, selected, 1) + prepared.input.jω[1]=state.jω + for materials in prepared.buffers.earth.calculations + for column in axes(materials.rho, 2), layer in 1:2 + + materials.rho[layer, column]=iszero(state.sigma[layer]) ? T(Inf) : + inv(state.sigma[layer]) + materials.epsilon[layer, column]=state.epsilon[layer] + materials.mu[layer, column]=state.mu[layer] + end + end + return prepared + end + + function calculate(geometry, state, integration) + prepared=workspace(geometry, state, integration) + E.earth!(prepared, 1) + return prepared.buffers + end + + # The calculation's state at the frequency, with the per-conductor arrays that its + # Functor writes into Unified's buffers. + function field_state(geometry, state) + prepared=workspace(geometry, state) + calculation=only(prepared.plan.earth.calculations) + materials=only(prepared.buffers.earth.calculations) + functor=LineCableModels.Functor(calculation.impedance.formula, + (; jω = prepared.input.jω[1], frequency = 1, materials.rho, materials.epsilon, + materials.mu, materials.thickness, calculation.media); + workspace = prepared) + buffers=prepared.buffers + return merge(functor.state, (radius = prepared.plan.geometry.radius, + buffers.radial_argument, buffers.source_logscale, buffers.circumference_average, + buffers.radial_current)) + end +end diff --git a/test/test_tutorial1.jl b/test/test_tutorial1.jl deleted file mode 100644 index 57718cf80..000000000 --- a/test/test_tutorial1.jl +++ /dev/null @@ -1,161 +0,0 @@ -@testitem "examples/tutorial1.jl tests" setup = [defaults] begin - - # Helpers - function material_approx_equal(m::Material, rho, eps_r, mu_r, T0, alpha; atol=1e-12, rtol=1e-8) - return isapprox(m.rho, rho; atol=atol, rtol=rtol) && - isapprox(m.eps_r, eps_r; atol=atol, rtol=rtol) && - isapprox(m.mu_r, mu_r; atol=atol, rtol=rtol) && - isapprox(m.T0, T0; atol=atol, rtol=rtol) && - isapprox(m.alpha, alpha; atol=atol, rtol=rtol) - end - - @testset "initialize and inspect" begin - materials = MaterialsLibrary() # default initialization as in the tutorial - @test materials !== nothing - - # DataFrame conversion - df = DataFrame(materials) - @test isa(df, DataFrame) - @test nrow(df) >= 0 # should be defined (>=0); more specific tests below - - # Check expected columns if present - expected = ["name", "rho", "eps_r", "mu_r", "T0", "alpha"] - @test all(x -> x in string.(names(df)), expected) - end - - @testset "add materials from tutorial" begin - materials = MaterialsLibrary(add_defaults=false) # start clean for deterministic tests - - # Define tutorial materials (subset representative of file) - copper_corrected = Material(1.835e-8, 1.0, 0.999994, 20.0, 0.00393) - aluminum_corrected = Material(3.03e-8, 1.0, 0.999994, 20.0, 0.00403) - epr = Material(1e15, 3.0, 1.0, 20.0, 0.005) - pvc = Material(1e15, 8.0, 1.0, 20.0, 0.1) - - add!(materials, "copper_corrected", copper_corrected) - add!(materials, "aluminum_corrected", aluminum_corrected) - add!(materials, "epr", epr) - add!(materials, "pvc", pvc) - - # Verify that keys exist - for name in ("copper_corrected", "aluminum_corrected", "epr", "pvc") - @test haskey(materials, name) - end - - # DataFrame contains the names - df = DataFrame(materials) - @test "copper_corrected" in df.name - @test "epr" in df.name - end - - @testset "remove duplicate" begin - materials = MaterialsLibrary(add_defaults=false) - epr = Material(1e15, 3.0, 1.0, 20.0, 0.005) - add!(materials, "epr", epr) - - # Add duplicate and then remove it - add!(materials, "epr_dupe", epr) - @test haskey(materials, "epr_dupe") - delete!(materials, "epr_dupe") - @test !haskey(materials, "epr_dupe") - end - - @testset "save and load round-trip (temp file)" begin - materials = MaterialsLibrary(add_defaults=false) - - # Add a small set of materials - copper_corrected = Material(1.835e-8, 1.0, 0.999994, 20.0, 0.00393) - epr = Material(1e15, 3.0, 1.0, 20.0, 0.005) - add!(materials, "copper_corrected", copper_corrected) - add!(materials, "epr", epr) - - tmpfile = tempname() * ".json" - try - # Save to temporary file - save(materials, file_name=tmpfile) - @test isfile(tmpfile) - - # Load into a fresh library - loaded = MaterialsLibrary(add_defaults=false) - load!(loaded, file_name=tmpfile) - - # Keys present after load - @test haskey(loaded, "copper_corrected") - @test haskey(loaded, "epr") - - # Retrieve and compare properties - copper_loaded = get(loaded, "copper_corrected") - @test isa(copper_loaded, Material) - @test material_approx_equal(copper_loaded, 1.835e-8, 1.0, 0.999994, 20.0, 0.00393) - - epr_loaded = get(loaded, "epr") - @test isa(epr_loaded, Material) - @test material_approx_equal(epr_loaded, 1e15, 3.0, 1.0, 20.0, 0.005) - - finally - isfile(tmpfile) && rm(tmpfile) - end - end - - @testset "error handling" begin - # Fresh empty library (no defaults) for deterministic error behavior - empty_lib = MaterialsLibrary(add_defaults=false) - - # get on non-existent key should display alert - @test get(empty_lib, "non_existent_material") === nothing - - - # delete! on non-existent key should throw KeyError - @test_throws KeyError delete!(empty_lib, "non_existent_material") - - # load! from a non-existent file should throw an I/O-related error (SystemError / IOError) - bad_file_lib = MaterialsLibrary(add_defaults=false) - @test_throws Exception load!(bad_file_lib, file_name="this_file_should_not_exist_hopefully_0123456789.json") - end - - @testset "integration-like workflow (safe, uses temp files)" begin - # Recreate the main tutorial workflow but using temporary save path - materials = MaterialsLibrary(add_defaults=false) - - # Add the full tutorial list used in examples (representative) - add!(materials, "copper_corrected", Material(1.835e-8, 1.0, 0.999994, 20.0, 0.00393)) - add!(materials, "aluminum_corrected", Material(3.03e-8, 1.0, 0.999994, 20.0, 0.00403)) - add!(materials, "lead", Material(21.4e-8, 1.0, 0.999983, 20.0, 0.00400)) - add!(materials, "steel", Material(13.8e-8, 1.0, 300.0, 20.0, 0.00450)) - add!(materials, "bronze", Material(3.5e-8, 1.0, 1.0, 20.0, 0.00300)) - add!(materials, "stainless_steel", Material(70.0e-8, 1.0, 500.0, 20.0, 0.0)) - add!(materials, "epr", Material(1e15, 3.0, 1.0, 20.0, 0.005)) - add!(materials, "pvc", Material(1e15, 8.0, 1.0, 20.0, 0.1)) - add!(materials, "laminated_paper", Material(1e15, 2.8, 1.0, 20.0, 0.0)) - add!(materials, "carbon_pe", Material(0.06, 1e3, 1.0, 20.0, 0.0)) - add!(materials, "conductive_paper", Material(18.5, 8.6, 1.0, 20.0, 0.0)) - - # Duplicate add and delete - add!(materials, "epr_dupe", get(materials, "epr")) - @test haskey(materials, "epr_dupe") - delete!(materials, "epr_dupe") - @test !haskey(materials, "epr_dupe") - - tmpfile = tempname() * ".json" - try - save(materials, file_name=tmpfile) - @test isfile(tmpfile) - - reloaded = MaterialsLibrary(add_defaults=false) - load!(reloaded, file_name=tmpfile) - - # verify a representative sample of materials exists after reload - for name in ("copper_corrected", "pvc", "stainless_steel") - @test haskey(reloaded, name) - end - - # Verify copper properties after reload - copper = get(reloaded, "copper_corrected") - @test material_approx_equal(copper, 1.835e-8, 1.0, 0.999994, 20.0, 0.00393) - - finally - isfile(tmpfile) && rm(tmpfile) - end - end - -end \ No newline at end of file diff --git a/test/test_tutorial2.jl b/test/test_tutorial2.jl deleted file mode 100644 index ec78cf9d3..000000000 --- a/test/test_tutorial2.jl +++ /dev/null @@ -1,305 +0,0 @@ -@testitem "examples/tutorial2.jl tests" setup = [defaults] begin - # Replicate the setup from the tutorial - materials = MaterialsLibrary(add_defaults = true) - - # Cable dimensions from the tutorial - num_co_wires = 61 - num_sc_wires = 49 - d_core = 38.1e-3 - d_w = 4.7e-3 - t_sc_in = 0.6e-3 - t_ins = 8e-3 - t_sc_out = 0.3e-3 - d_ws = 0.95e-3 - t_cut = 0.1e-3 - w_cut = 10e-3 - t_wbt = 0.3e-3 - t_sct = 0.3e-3 - t_alt = 0.15e-3 - t_pet = 0.05e-3 - t_jac = 2.4e-3 - - # Test Core and Main Insulation construction - @testset "core and main insulation" begin - material_al = get(materials, "aluminum") - @test material_al isa LineCableModels.Materials.Material - core = ConductorGroup(CircStrands(0.0, Diameter(d_w), 1, 0.0, material_al)) - add!(core, CircStrands, Diameter(d_w), 6, 15.0, material_al) - add!(core, CircStrands, Diameter(d_w), 12, 13.5, material_al) - add!(core, CircStrands, Diameter(d_w), 18, 12.5, material_al) - add!(core, CircStrands, Diameter(d_w), 24, 11.0, material_al) - - @test length(core.layers) == 5 - @test isapprox(core.r_ex * 2, 0.0423, atol = 1e-4) - - material_poly = get(materials, "polyacrylate") - material_sc1 = get(materials, "semicon1") - material_pe = get(materials, "pe") - material_sc2 = get(materials, "semicon2") - - main_insu = InsulatorGroup(Semicon(core, Thickness(t_sct), material_poly)) - add!(main_insu, Semicon, Thickness(t_sc_in), material_sc1) - add!(main_insu, Insulator, Thickness(t_ins), material_pe) - add!(main_insu, Semicon, Thickness(t_sc_out), material_sc2) - add!(main_insu, Semicon, Thickness(t_sct), material_poly) - - @test length(main_insu.layers) == 5 - - core_cc = CableComponent("core", core, main_insu) - @test core_cc.id == "core" - end - - # Build the full cable design step-by-step as in the tutorial - # This also tests the constructors and `add!` methods implicitly - material_al = get(materials, "aluminum") - core = ConductorGroup(CircStrands(0.0, Diameter(d_w), 1, 0.0, material_al)) - add!(core, CircStrands, Diameter(d_w), 6, 15.0, material_al) - add!(core, CircStrands, Diameter(d_w), 12, 13.5, material_al) - add!(core, CircStrands, Diameter(d_w), 18, 12.5, material_al) - add!(core, CircStrands, Diameter(d_w), 24, 11.0, material_al) - - material_poly = get(materials, "polyacrylate") - material_sc1 = get(materials, "semicon1") - material_pe = get(materials, "pe") - material_sc2 = get(materials, "semicon2") - main_insu = InsulatorGroup(Semicon(core, Thickness(t_sct), material_poly)) - add!(main_insu, Semicon, Thickness(t_sc_in), material_sc1) - add!(main_insu, Insulator, Thickness(t_ins), material_pe) - add!(main_insu, Semicon, Thickness(t_sc_out), material_sc2) - add!(main_insu, Semicon, Thickness(t_sct), material_poly) - - core_cc = CableComponent("core", core, main_insu) - - cable_id = "18kV_1000mm2" - datasheet_info = NominalData( - designation_code = "NA2XS(FL)2Y", U0 = 18.0, U = 30.0, - conductor_cross_section = 1000.0, screen_cross_section = 35.0, - resistance = 0.0291, capacitance = 0.39, inductance = 0.3, - ) - cable_design = CableDesign(cable_id, core_cc, nominal_data = datasheet_info) - - @test length(cable_design.components) == 1 - @test cable_design.cable_id == cable_id - - material_cu = get(materials, "copper") - lay_ratio = 10.0 - screen_con = ConductorGroup( - CircStrands(main_insu, Diameter(d_ws), num_sc_wires, lay_ratio, material_cu), - ) - add!(screen_con, Strip, Thickness(t_cut), w_cut, lay_ratio, material_cu) - screen_insu = InsulatorGroup(Semicon(screen_con, Thickness(t_wbt), material_poly)) - sheath_cc = CableComponent("sheath", screen_con, screen_insu) - add!(cable_design, sheath_cc) - - @test length(cable_design.components) == 2 - @test cable_design.components[2].id == "sheath" - - jacket_con = ConductorGroup(Tubular(screen_insu, Thickness(t_alt), material_al)) - jacket_insu = InsulatorGroup(Insulator(jacket_con, Thickness(t_pet), material_pe)) - add!(jacket_insu, Insulator, Thickness(t_jac), material_pe) - add!(cable_design, "jacket", jacket_con, jacket_insu) - - @test length(cable_design.components) == 3 - @test cable_design.components[3].id == "jacket" - - @testset "calculated parameters vs hard-coded values" begin - core_df = DataFrame(cable_design, :baseparams) - - # Hard-coded values from the tutorial - expected_R = 0.0275677 - expected_L = 0.287184 - expected_C = 0.413357 - - # Test R - computed_R = core_df[core_df.parameter .== "R [Ω/km]", :computed][1] - @test isapprox(computed_R, expected_R, atol = 1e-5) - - # Test L - computed_L = core_df[core_df.parameter .== "L [mH/km]", :computed][1] - @test isapprox(computed_L, expected_L, atol = 1e-5) - - # Test C - computed_C = core_df[core_df.parameter .== "C [μF/km]", :computed][1] - @test isapprox(computed_C, expected_C, atol = 1e-5) - end - - @testset "dataframes and library" begin - # Test that DataFrame constructors do not throw errors - @test DataFrame(cable_design, :components) isa DataFrame - @test DataFrame(cable_design, :detailed) isa DataFrame - - # Test CablesLibrary functionality - library = CablesLibrary() - add!(library, cable_design) - @test length(library) == 1 - @test DataFrame(library) isa DataFrame - - # Test saving and loading - mktempdir(joinpath(@__DIR__)) do temp_dir - output_file = joinpath(temp_dir, "cables_library.json") - save(library, file_name = output_file) - @test isfile(output_file) - - loaded_library = CablesLibrary() - load!(loaded_library, file_name = output_file) - @test length(loaded_library) == 1 - @test loaded_library.data[cable_id].cable_id == cable_id - end - end - - @testset "cable system and export" begin - f = 10.0 .^ range(0, stop = 6, length = 10) - earth_params = EarthModel(f, 100.0, 10.0, 1.0) - @test DataFrame(earth_params) isa DataFrame - - x0, y0 = 0.0, -1.0 - xa, ya, xb, yb, xc, yc = trifoil_formation(x0, y0, 0.035) - - cablepos = CablePosition( - cable_design, - xa, - ya, - Dict("core" => 1, "sheath" => 0, "jacket" => 0), - ) - cable_system = LineCableSystem("18kV_1000mm2_trifoil", 1000.0, cablepos) - add!( - cable_system, - cable_design, - xb, - yb, - Dict("core" => 2, "sheath" => 0, "jacket" => 0), - ) - add!( - cable_system, - cable_design, - xc, - yc, - Dict("core" => 3, "sheath" => 0, "jacket" => 0), - ) - - @test length(cable_system.cables) == 3 - @test DataFrame(cable_system) isa DataFrame - - # Test PSCAD export - mktempdir(joinpath(@__DIR__)) do temp_dir - output_file = joinpath(temp_dir, "$(cable_system.system_id)_export.pscx") - result_path = - export_data(:pscad, cable_system, earth_params, file_name = output_file) - @test !isnothing(result_path) - @test isfile(result_path) - # Check if file has content - @test filesize(result_path) > 0 - end - end - - @testset "preview functions" begin - # Test that preview functions execute without error - # Note: This does not check the plot content, only that they don't crash. - @test preview(cable_design, display_plot = false) isa Any - - f = 10.0 .^ range(0, stop = 6, length = 10) - earth_params = EarthModel(f, 100.0, 10.0, 1.0) - x0, y0 = 0.0, -1.0 - xa, ya, xb, yb, xc, yc = trifoil_formation(x0, y0, 0.035) - cablepos = CablePosition( - cable_design, - xa, - ya, - Dict("core" => 1, "sheath" => 0, "jacket" => 0), - ) - cable_system = LineCableSystem("18kV_1000mm2_trifoil", 1000.0, cablepos) - add!( - cable_system, - cable_design, - xb, - yb, - Dict("core" => 2, "sheath" => 0, "jacket" => 0), - ) - add!( - cable_system, - cable_design, - xc, - yc, - Dict("core" => 3, "sheath" => 0, "jacket" => 0), - ) - - end - - @testset "user error handling and robustness" begin - # Test invalid material request - @test get(materials, "unobtanium") === nothing - - # Test invalid geometric parameters - @test_throws ArgumentError CircStrands(0.0, Diameter(-1.0), 1, 0.0, material_al) - @test_throws ArgumentError Insulator(core, Thickness(-1.0), material_pe) - @test_throws ArgumentError CircStrands(core, Diameter(d_w), 1, -1.0, material_al) # Negative lay ratio - @test_throws ArgumentError Strip( - core, - Thickness(-0.1), - w_cut, - lay_ratio, - material_cu, - ) - - # Test empty object creation - @test_throws ArgumentError ConductorGroup() - @test_throws ArgumentError InsulatorGroup() - @test_throws MethodError CableDesign("empty_cable") - @test_throws MethodError LineCableSystem("empty_system", 1000.0) - - # Test trifoil formation with negative radius - @test_throws AssertionError trifoil_formation(0.0, -1.0, -1.0) - - # Test adding a cable at an overlapping position in LineCableSystem - x0, y0 = 0.0, -1.0 - xa, ya, xb, yb, xc, yc = trifoil_formation(x0, y0, 0.1) # Use a valid distance - cablepos = CablePosition(cable_design, xa, ya, Dict("core" => 1)) - cable_system = LineCableSystem("overlap_test", 1000.0, cablepos) - @test_throws ArgumentError add!( - cable_system, - cable_design, - xa, - ya, - Dict("core" => 2), - ) - - # Test conductor at interface - @test_throws ArgumentError CablePosition(cable_design, 0.0, 0.0, Dict("core" => 1)) - - # Test invalid phase mapping - @test_throws ArgumentError CablePosition( - cable_design, - 1.0, - 1.0, - Dict("non_existent_component" => 1), - ) - - - # Test exporting a system where some components are grounded (valid case) - f = 10.0 .^ range(0, stop = 6, length = 10) - earth_params = EarthModel(f, 100.0, 10.0, 1.0) - cablepos_partially_grounded = CablePosition( - cable_design, - xa, - ya, - Dict("core" => 1, "sheath" => 0, "jacket" => 0), - ) - system_partially_grounded = LineCableSystem( - "partially_grounded_system", - 1000.0, - cablepos_partially_grounded, - ) - mktempdir(joinpath(@__DIR__)) do temp_dir - output_file = joinpath(temp_dir, "partially_grounded_export.pscx") - result_path = export_data( - :pscad, - system_partially_grounded, - earth_params, - file_name = output_file, - ) - @test !isnothing(result_path) - @test isfile(result_path) - @test filesize(result_path) > 0 - end - end -end diff --git a/test/test_tutorial3.jl b/test/test_tutorial3.jl deleted file mode 100644 index ccdecc19b..000000000 --- a/test/test_tutorial3.jl +++ /dev/null @@ -1,179 +0,0 @@ -@testitem "examples/tutorial3.jl tests" setup = [defaults] begin - - - mktempdir(joinpath(@__DIR__)) do tmpdir - # Materials - materials = MaterialsLibrary(add_defaults = true) - - @test haskey(materials, "lead") - @test haskey(materials, "steel") - @test haskey(materials, "pp") - - # Cable dimensions - num_ar_wires = 68 - d_w = 3.6649e-3 - t_sc_in = 2e-3 - t_ins = 26e-3 - t_sc_out = 1.8e-3 - t_wbt = 0.3e-3 - t_sc = 3.3e-3 - t_pe = 3e-3 - t_bed = 3e-3 - d_wa = 5.827e-3 - t_jac = 10e-3 - - # Core and main insulation - material_cu = get(materials, "copper") - n = 6 - core = ConductorGroup(CircStrands(0.0, Diameter(d_w), 1, 0.0, material_cu)) - add!(core, CircStrands, Diameter(d_w), 1 * n, 11.0, material_cu) - add!(core, CircStrands, Diameter(d_w), 2 * n, 11.0, material_cu) - add!(core, CircStrands, Diameter(d_w), 3 * n, 11.0, material_cu) - add!(core, CircStrands, Diameter(d_w), 4 * n, 11.0, material_cu) - add!(core, CircStrands, Diameter(d_w), 5 * n, 11.0, material_cu) - add!(core, CircStrands, Diameter(d_w), 6 * n, 11.0, material_cu) - - material_sc1 = get(materials, "semicon1") - main_insu = InsulatorGroup(Semicon(core, Thickness(t_sc_in), material_sc1)) - material_pe = get(materials, "pe") - add!(main_insu, Insulator, Thickness(t_ins), material_pe) - material_sc2 = get(materials, "semicon2") - add!(main_insu, Semicon, Thickness(t_sc_out), material_sc2) - material_pa = get(materials, "polyacrylate") - add!(main_insu, Semicon, Thickness(t_wbt), material_pa) - - core_cc = CableComponent("core", core, main_insu) - cable_id = "525kV_1600mm2" - datasheet_info = NominalData(U = 525.0, conductor_cross_section = 1600.0) - cable_design = CableDesign(cable_id, core_cc, nominal_data = datasheet_info) - - @test length(cable_design.components) == 1 - @test cable_design.components[1].id == "core" - - # Lead screen/sheath - material_lead = get(materials, "lead") - screen_con = ConductorGroup(Tubular(main_insu, Thickness(t_sc), material_lead)) - material_pe_sheath = get(materials, "pe") - screen_insu = - InsulatorGroup(Insulator(screen_con, Thickness(t_pe), material_pe_sheath)) - material_pp_bedding = get(materials, "pp") - add!(screen_insu, Insulator, Thickness(t_bed), material_pp_bedding) - sheath_cc = CableComponent("sheath", screen_con, screen_insu) - add!(cable_design, sheath_cc) - - @test length(cable_design.components) == 2 - @test cable_design.components[2].id == "sheath" - - # Armor and outer jacket components - lay_ratio = 10.0 - material_steel = get(materials, "steel") - armor_con = ConductorGroup( - CircStrands( - screen_insu, - Diameter(d_wa), - num_ar_wires, - lay_ratio, - material_steel, - ), - ) - material_pp_jacket = get(materials, "pp") - armor_insu = - InsulatorGroup(Insulator(armor_con, Thickness(t_jac), material_pp_jacket)) - add!(cable_design, "armor", armor_con, armor_insu) - - @test length(cable_design.components) == 3 - @test cable_design.components[3].id == "armor" - - # Saving the cable design - library = CablesLibrary() - library_file = joinpath(tmpdir, "cables_library.json") - add!(library, cable_design) - save(library, file_name = library_file) - - loaded_library = CablesLibrary() - load!(loaded_library, file_name = library_file) - @test haskey(loaded_library, cable_id) - reloaded_design = get(loaded_library, cable_id) - @test reloaded_design.cable_id == cable_design.cable_id - @test length(reloaded_design.components) == length(cable_design.components) - - # Defining a cable system - f = 1e-3 - earth_params = EarthModel([f], 100.0, 10.0, 1.0) - xp = -0.5 - xn = 0.5 - y0 = -1.0 - cablepos = CablePosition( - cable_design, - xp, - y0, - Dict("core" => 1, "sheath" => 0, "armor" => 0), - ) - cable_system = LineCableSystem("525kV_1600mm2_bipole", 1000.0, cablepos) - add!( - cable_system, - cable_design, - xn, - y0, - Dict("core" => 2, "sheath" => 0, "armor" => 0), - ) - - @test length(cable_system.cables) == 2 - - # FEM calculations - problem = LineParametersProblem( - cable_system, - temperature = 20.0, - earth_props = earth_params, - frequencies = [f], - ) - rho_g = earth_params.layers[end].rho_g[1] - mu_g = earth_params.layers[end].mu_g[1] - skin_depth_earth = abs(sqrt(rho_g / (1im * (2 * pi * f) * mu_g))) - domain_radius = clamp(skin_depth_earth, 5.0, 5000.0) - - opts = ( - force_remesh = true, - force_overwrite = true, - plot_field_maps = false, - mesh_only = false, - save_path = joinpath(tmpdir, "fem_output"), - keep_run_files = false, - verbosity = 0, - ) - - formulation = FormulationSet(:FEM, - impedance = Darwin(), - admittance = Electrodynamics(), - domain_radius = domain_radius, - domain_radius_inf = domain_radius * 1.25, - elements_per_length_conductor = 1, - elements_per_length_insulator = 2, - elements_per_length_semicon = 1, - elements_per_length_interfaces = 5, - points_per_circumference = 16, - mesh_size_min = 1e-6, - mesh_size_max = domain_radius / 5, - mesh_size_default = domain_radius / 10, - mesh_algorithm = 5, - mesh_max_retries = 20, - materials = materials, - options = opts, - ) - - workspace, line_params = compute!(problem, formulation) - - @test line_params isa LineParameters - @test size(line_params.Z) == (2, 2, 1) - @test size(line_params.Y) == (2, 2, 1) - - R = real(line_params.Z[1, 1, 1]) * 1000 - L = imag(line_params.Z[1, 1, 1]) / (2π * f) * 1e6 - C = imag(line_params.Y[1, 1, 1]) / (2π * f) * 1e9 - - # Check if the results match hard-coded benchmarks - @test isapprox(R, 0.01303, atol = 1e-5) - @test isapprox(L, 2.7600, atol = 1e-4) - @test isapprox(C, 0.1851, atol = 1e-4) - end -end diff --git a/test/test_tutorial_2_sector.jl b/test/test_tutorial_2_sector.jl deleted file mode 100644 index 65e69abf3..000000000 --- a/test/test_tutorial_2_sector.jl +++ /dev/null @@ -1,168 +0,0 @@ -@testitem "examples/tutorial2_sector.jl tests" setup = [defaults] begin - # Replicate the setup from the tutorial - - # === Materials === - materials = MaterialsLibrary(add_defaults=true) - pvc = Material(Inf, 8.0, 1.0, 20.0, 0.1) - add!(materials, "pvc", pvc) - copper = get(materials, "copper") - aluminum = get(materials, "aluminum") - - @testset "Material setup" begin - @test get(materials, "pvc") isa LineCableModels.Materials.Material - @test get(materials, "aluminum") isa LineCableModels.Materials.Material - @test get(materials, "copper") isa LineCableModels.Materials.Material - end - - # === Sector (core) geometry === - @testset "Sector core construction" begin - n_sectors = 3 - r_back_mm = 10.24 - d_sector_mm = 9.14 - r_corner_mm = 1.02 - theta_cond_deg = 119.0 - ins_thick = 1.1e-3 - - sector_params = SectorParams( - n_sectors, - r_back_mm / 1000, - d_sector_mm / 1000, - r_corner_mm / 1000, - theta_cond_deg, - ins_thick - ) - - rot_angles = (0.0, 120.0, 240.0) - sectors = [Sector(sector_params, ang, aluminum) for ang in rot_angles] - insulators = [SectorInsulator(sectors[i], ins_thick, pvc) for i in 1:3] - - @test length(sectors) == 3 - @test all(s -> s isa Sector, sectors) - @test length(insulators) == 3 - @test all(i -> i isa SectorInsulator, insulators) - - components = [ - CableComponent("core1", ConductorGroup(sectors[1]), InsulatorGroup(insulators[1])), - CableComponent("core2", ConductorGroup(sectors[2]), InsulatorGroup(insulators[2])), - CableComponent("core3", ConductorGroup(sectors[3]), InsulatorGroup(insulators[3])) - ] - @test length(components) == 3 - @test components[1].id == "core1" - end - - # === Concentric neutral === - @testset "Concentric neutral construction" begin - n_neutral = 30 - r_strand = 0.79e-3 - R_N = 14.36e-3 - R_O = 17.25e-3 - - inner_radius_neutral = R_N - r_strand - outer_jacket_thickness = R_O - (R_N + r_strand) - - neutral_wires = WireArray( - inner_radius_neutral, - Diameter(2*r_strand), - n_neutral, - 0.0, - copper - ) - @test neutral_wires isa WireArray - - neutral_jacket = Insulator(neutral_wires, Thickness(outer_jacket_thickness), pvc) - @test neutral_jacket isa Insulator - - neutral_component = CableComponent("neutral", ConductorGroup(neutral_wires), InsulatorGroup(neutral_jacket)) - @test neutral_component.id == "neutral" - end - - # === Assemble cable design === - @testset "Full cable design assembly" begin - # Re-create components for this testset to be self-contained - n_sectors = 3 - r_back_mm = 10.24 - d_sector_mm = 9.14 - r_corner_mm = 1.02 - theta_cond_deg = 119.0 - ins_thick = 1.1e-3 - sector_params = SectorParams(n_sectors, r_back_mm/1000, d_sector_mm/1000, r_corner_mm/1000, theta_cond_deg, ins_thick) - rot_angles = (0.0, 120.0, 240.0) - sectors = [Sector(sector_params, ang, aluminum) for ang in rot_angles] - insulators = [SectorInsulator(sectors[i], ins_thick, pvc) for i in 1:3] - components = [ - CableComponent("core1", ConductorGroup(sectors[1]), InsulatorGroup(insulators[1])), - CableComponent("core2", ConductorGroup(sectors[2]), InsulatorGroup(insulators[2])), - CableComponent("core3", ConductorGroup(sectors[3]), InsulatorGroup(insulators[3])) - ] - - n_neutral = 30 - r_strand = 0.79e-3 - R_N = 14.36e-3 - R_O = 17.25e-3 - inner_radius_neutral = R_N - r_strand - outer_jacket_thickness = R_O - (R_N + r_strand) - neutral_wires = WireArray(inner_radius_neutral, Diameter(2*r_strand), n_neutral, 0.0, copper) - neutral_jacket = Insulator(neutral_wires, Thickness(outer_jacket_thickness), pvc) - neutral_component = CableComponent("neutral", ConductorGroup(neutral_wires), InsulatorGroup(neutral_jacket)) - - design = CableDesign("NAYCWY_O_3x95_30x2_5", components[1]) - add!(design, components[2]) - add!(design, components[3]) - add!(design, neutral_component) - - @test length(design.components) == 4 - @test design.cable_id == "NAYCWY_O_3x95_30x2_5" - @test design.components[1].id == "core1" - @test design.components[2].id == "core2" - @test design.components[3].id == "core3" - @test design.components[4].id == "neutral" - end - - @testset "DataFrame and preview" begin - # Re-create the full design - n_sectors = 3 - r_back_mm = 10.24 - d_sector_mm = 9.14 - r_corner_mm = 1.02 - theta_cond_deg = 119.0 - ins_thick = 1.1e-3 - sector_params = SectorParams(n_sectors, r_back_mm/1000, d_sector_mm/1000, r_corner_mm/1000, theta_cond_deg, ins_thick) - rot_angles = (0.0, 120.0, 240.0) - sectors = [Sector(sector_params, ang, aluminum) for ang in rot_angles] - insulators = [SectorInsulator(sectors[i], ins_thick, pvc) for i in 1:3] - components = [ - CableComponent("core1", ConductorGroup(sectors[1]), InsulatorGroup(insulators[1])), - CableComponent("core2", ConductorGroup(sectors[2]), InsulatorGroup(insulators[2])), - CableComponent("core3", ConductorGroup(sectors[3]), InsulatorGroup(insulators[3])) - ] - n_neutral = 30 - r_strand = 0.79e-3 - R_N = 14.36e-3 - R_O = 17.25e-3 - inner_radius_neutral = R_N - r_strand - outer_jacket_thickness = R_O - (R_N + r_strand) - neutral_wires = WireArray(inner_radius_neutral, Diameter(2*r_strand), n_neutral, 0.0, copper) - neutral_jacket = Insulator(neutral_wires, Thickness(outer_jacket_thickness), pvc) - neutral_component = CableComponent("neutral", ConductorGroup(neutral_wires), InsulatorGroup(neutral_jacket)) - design = CableDesign("NAYCWY_O_3x95_30x2_5", components[1]) - add!(design, components[2]) - add!(design, components[3]) - add!(design, neutral_component) - - # Test that DataFrame constructors do not throw errors - @test DataFrame(design, :detailed) isa DataFrame - @test DataFrame(design, :components) isa DataFrame - @test DataFrame(design, :baseparams) isa DataFrame - - # Test that preview functions execute without error - @test preview(design, display_plot=false) isa Any - end - - @testset "Error handling" begin - # Test invalid geometric parameters for Sector - @test_throws ArgumentError SectorParams(3, -10.24/1000, 9.14/1000, 1.02/1000, 119.0, 1.1e-3) - @test_throws ArgumentError SectorParams(3, 10.24/1000, -9.14/1000, 1.02/1000, 119.0, 1.1e-3) - @test_throws ArgumentError SectorParams(3, 10.24/1000, 9.14/1000, -1.02/1000, 119.0, 1.1e-3) - @test_throws ArgumentError SectorParams(3, 10.24/1000, 9.14/1000, 1.02/1000, 119.0, -1.1e-3) - end -end diff --git a/test/tools/allocations.jl b/test/tools/allocations.jl new file mode 100644 index 000000000..4ed4185e1 --- /dev/null +++ b/test/tools/allocations.jl @@ -0,0 +1,113 @@ +# Allocation counts and bytes of the preservation corpus, for the allocation ceilings in +# `test/quality/preservation.toml`. Run `julia --project=test test/tools/allocations.jl` +# in a fresh process from the repository root. +# +# It prints the `[allocations]` rows of `test/quality/preservation.toml`. For each +# scenario and frequency count, each `compute` call is warmed up twice and then measured +# three times inside a function. `allocations` is the number of allocated objects (pool, +# big and allocated by `malloc`), which must be identical across the measured calls. +# `bytes` is the minimum over them, because the runtime's byte accounting of buffers from +# `malloc` adds a few bytes on some calls. The counts depend on what the process computed before, so the +# corpus always runs in the same order in a fresh process. The quality item +# `Quality / preservation / allocation ceilings` runs this file. +# +# It also prints `[allowances]` and `[derivative]`, which `preservation.toml` does not +# record. Measurements stores a partial derivative only when it is nonzero. Whether a +# round-off derivative is exactly zero depends on the machine's last bits. A row's +# allowance is the number of uncertain real scalars that the scenario's result publishes. +# Each of them can store one derivative entry more or less. `[derivative]` gives the bytes +# of one entry. +using LineCableModels, Measurements +using LineCableModels.Commons: AbstractParametricResult, AbstractUncertaintyResult + +Base.include(@__MODULE__, joinpath(@__DIR__, "..", "support", "scenarios.jl")) +using .CurrentScenarios: preservation_corpus + +const FREQUENCIES = (2, 4) +const WARMUP = 2 +const CALLS = 3 + +# The objects and bytes of `CALLS` warmed-up calls of `compute` on `arguments`, and the +# result of the last warm-up call. The measured calls only compute. +function measure(arguments) + result = nothing + for _ in 1:WARMUP + result = compute(arguments...) + end + objects, bytes = Int[], Int[] + for _ in 1:CALLS + before = Base.gc_num() + compute(arguments...) + difference = Base.GC_Diff(Base.gc_num(), before) + push!(objects, difference.poolalloc + difference.bigalloc + difference.malloc) + push!(bytes, difference.allocd) + end + return (; objects, bytes, result) +end + +# The uncertain real scalars that a result publishes. Real and imaginary parts count +# separately. +allowance(::Measurement) = 1 +allowance(::Real) = 0 +allowance(value::Complex) = allowance(real(value)) + allowance(imag(value)) +allowance(values::AbstractArray) = sum(allowance, values; init = 0) +allowance(result::LineParameters) = + allowance(result.Z) + allowance(result.Y) + allowance(result.f) +allowance(result::CableConstants) = allowance(result.R) + allowance(result.L) + + allowance(result.C) + allowance(result.G) + allowance(result.frequency) +allowance(result::Union{AbstractParametricResult, AbstractUncertaintyResult}) = + allowance(result.values) + +# One derivative entry takes the difference in bytes between an uncertain value and an +# exact one. +function derivative_bytes() + measurement(1.0, 0.1) + measurement(1.0, 0.0) + return @allocated(measurement(1.0, 0.1)) - @allocated(measurement(1.0, 0.0)) +end + +# The rows of one measured scenario, and a description of its varied object counts or +# nothing. +function scenario_rows(name, n, measured) + prefix = "$name | $n frequencies" + varied = allequal(measured.objects) ? nothing : + "$prefix: allocations $(measured.objects) across calls" + return (["$prefix | allocations" => first(measured.objects), + "$prefix | bytes" => minimum(measured.bytes)], varied) +end + +# The table rows as key => value pairs, the scenarios whose object counts varied, and the +# allowance of each scenario. The allowances come after the whole corpus, so that the +# measured process computes only the corpus. +function rows(counts = FREQUENCIES) + found = Pair{String, Int}[] + varied = String[] + results = Pair{String, Any}[] + for n in counts + corpus = preservation_corpus(n) + for name in keys(corpus) + measured = measure(corpus[name]) + pairs, variation = scenario_rows(name, n, measured) + append!(found, pairs) + variation === nothing || push!(varied, variation) + push!(results, "$name | $n frequencies" => measured.result) + end + end + allowances = [scenario => allowance(result) for (scenario, result) in results] + return (; rows = found, varied, allowances) +end + +function main() + result = rows() + println("[allocations]") + foreach(row -> println(repr(first(row)), " = ", last(row)), result.rows) + println("\n[allowances]") + foreach(row -> println(repr(first(row)), " = ", last(row)), result.allowances) + println("\n[derivative]\nbytes = ", derivative_bytes()) + isempty(result.varied) && return 0 + println(stderr, "Nondeterministic allocation counts:") + foreach(line -> println(stderr, " ", line), result.varied) + return 1 +end + +abspath(PROGRAM_FILE) == (@__FILE__) && exit(main()) diff --git a/test/tools/architecture_inventory.jl b/test/tools/architecture_inventory.jl new file mode 100644 index 000000000..94409b32d --- /dev/null +++ b/test/tools/architecture_inventory.jl @@ -0,0 +1,14 @@ +# Print the live architecture inventory in the format of +# `test/quality/architecture_baseline.toml`. Run +# `julia --project=test test/tools/architecture_inventory.jl` from the repository root. +# Compare the output with the baseline to find entries to delete or lower. +# A new violation is fixed in the source. It is never added to the baseline. +source = normpath(joinpath(@__DIR__, "..", "quality", "architecture.jl")) +document = Meta.parseall(read(source, String); filename = source) +# The quality items and this tool evaluate the same guard module. +definition = only(x for x in document.args + if Meta.isexpr(x, :macrocall) && x.args[1] === Symbol("@testmodule") && + x.args[3] === :ArchitectureGuards) +guards = Module(:ArchitectureGuards) +Core.eval(guards, Expr(:toplevel, definition.args[end].args...)) +print(Base.invokelatest(guards.render, Base.invokelatest(guards.live))) diff --git a/test/tools/baseline_ratchet.jl b/test/tools/baseline_ratchet.jl new file mode 100644 index 000000000..7b338c3c5 --- /dev/null +++ b/test/tools/baseline_ratchet.jl @@ -0,0 +1,317 @@ +# Compare the architecture baseline and the preservation locks with their versions at +# a git revision. Run `julia test/tools/baseline_ratchet.jl REF`. Every table of the +# baseline is a ceiling. `preservation.toml` declares the direction of each table in +# `[directions]`. A ceiling key added or raised since `REF` fails, and a floor key +# removed or lowered since `REF` fails. Moves the other way pass. The measured ceilings +# `[jet]` and `[allocations]` may also rise in a change of `[environment]`, the Julia +# version and `Manifest.toml` they were recorded with. A key of `[allocations]` is a +# scenario of the preservation corpus, and a new scenario cannot hide another scenario's +# count. So a key absent at `REF` passes there when every `[allocations]` key at `REF` is +# still present. A key that appears while another disappears is a rename and fails. The +# check passes for a file absent at `REF`, and a table absent at `REF` is not compared. +# File renames that git detects between `REF` and the working tree, and the module +# renames they imply, are applied to the keys at `REF` first. A module is renamed when git +# renames or moves its entry file `.jl` and the file declares that module on both +# sides. Its full name on each side comes from the entry files of the enclosing +# directories, so an entry file that moves into another module's directory takes that +# module's name as a prefix. +# +# An ownership key has the form `defining module | Owner.function | file`. When only +# `Owner`, the module that defines the extended function, changes and the count stays the +# same, the ratchet treats the key as renamed. +# +# An `[inferred]` floor can fall when `@inferred` lines move to other files. Each removed +# line appears again in another file, unchanged apart from indentation, and these lines +# cover the drop. +using TOML + +const BASELINE = "test/quality/architecture_baseline.toml" +const PRESERVATION = "test/quality/preservation.toml" +# Tables of the preservation file that describe it rather than hold counts. +const METADATA = ("directions", "environment") +# Ceilings measured in one environment. +const MEASURED = ("jet", "allocations") +# Ceilings keyed by the scenarios of the preservation corpus. +const CORPUS = ("allocations",) +# Floors that count, per test file, the lines that contain a marker. These lines can move +# to another file. +const RELOCATABLE = Dict("inferred" => "@inferred") +const REPOSITORY = normpath(joinpath(@__DIR__, "..", "..")) + +git(repository, arguments::AbstractString...) = Cmd(["git", "-C", repository, arguments...]) + +# Nested tables become keys of the form "table | key | counter". +function entries(document, prefix = "") + flat = Dict{String, Any}() + for (key, value) in document + name = isempty(prefix) ? key : string(prefix, " | ", key) + value isa AbstractDict ? merge!(flat, entries(value, name)) : (flat[name] = value) + end + return flat +end + +declares(text, name) = occursin(Regex("(?m)^\\s*(?:bare)?module\\s+" * name * "\\b"), text) + +# The full name of the module that the entry file `path` declares. The modules declared by +# the entry files of the enclosing directories precede its own name, outermost first. An +# entry file `.jl` declares the module of its name. The package root, whose entry file is in +# `src/`, is not named. `entries(directory)` lists the `.jl` files of a directory and +# `source(file)` reads one, on one side of the comparison. +function module_name(path, entries, source) + names = [first(splitext(basename(path)))] + directory, current = dirname(path), path + while !isempty(directory) + files = entries(directory) + index = findfirst(files) do file + file != current && declares(source(file), first(splitext(basename(file)))) + end + if index === nothing + directory = dirname(directory) + else + current = files[index] + directory == "src" && break + pushfirst!(names, first(splitext(basename(current)))) + end + end + return join(names, ".") +end + +# Renamed files, and the module renames of renamed or moved entry files `.jl` that +# declare their module on both sides. +function renames(repository, reference) + fields = split(read(git(repository, "diff", "-M", "--name-status", "-z", reference), + String), '\0'; keepempty = false) + files = Dict{String, String}() + i = 1 + while i <= length(fields) + if startswith(fields[i], 'R') + files[fields[i+1]] = fields[i+2] + i += 3 + else + i += 2 + end + end + previous(file) = read(git(repository, "show", "$reference:$file"), String) + current(file) = read(joinpath(repository, file), String) + previous_entries(directory) = filter(endswith(".jl"), split(read(git(repository, + "ls-tree", "--name-only", reference, "--", directory * "/"), String), '\n'; + keepempty = false)) + current_entries(directory) = isdir(joinpath(repository, directory)) ? + [join((directory, file), "/") for file in readdir(joinpath(repository, directory)) + if endswith(file, ".jl") && isfile(joinpath(repository, directory, file))] : + String[] + modules = Dict{String, String}() + for (old, new) in files + (before, extension), after = splitext(basename(old)), first(splitext(basename(new))) + extension == ".jl" && declares(previous(old), before) && declares(current(new), after) || + continue + name = module_name(old, previous_entries, previous) + renamed = module_name(new, current_entries, current) + name == renamed || (modules[name] = renamed) + end + return (; files, modules) +end + +# Path components change only through file renames. Module names change in the +# other components. +function rename_key(key, renamed) + parts = map(split(key, " | ")) do part + haskey(renamed.files, part) && return renamed.files[part] + occursin('/', part) && return part + for (old, new) in renamed.modules + part = replace(part, Regex("(? new) + end + return part + end + return join(parts, " | ") +end + +# The ceiling keys at the reference whose only change is `Owner`, the module that defines the +# extended function, mapped to their current keys. The table, the defining module, the function +# name, the file and the count are unchanged. +function owner_renames(before, now) + function parts(key) + fields = split(key, " | ") + length(fields) == 4 && occursin('.', fields[3]) || return nothing + owner, name = rsplit(fields[3], '.'; limit = 2) + return (; fields = fields[[1, 2, 4]], owner, name) + end + renamed = Dict{String, String}() + for key in sort!(collect(keys(now))) + haskey(before, key) && continue + current = parts(key) + current === nothing && continue + for old in sort!(collect(keys(before))) + (haskey(now, old) || haskey(renamed, old)) && continue + previous = parts(old) + previous === nothing && continue + previous.fields == current.fields && previous.name == current.name && + previous.owner != current.owner && before[old] == now[key] || continue + renamed[old] = key + break + end + end + return renamed +end + +# The lines removed from each file since `reference` and the lines added to each file, +# without indentation. An untracked file counts as added in full. +function changed_lines(repository, reference) + removed, added = Dict{String, Vector{String}}(), Dict{String, Vector{String}}() + record!(lines, file, line) = + push!(get!(Vector{String}, lines, file), String(lstrip(line))) + path(line, prefix) = + (name = line[5:end]; name == "/dev/null" ? "" : chopprefix(name, prefix)) + old, new, hunk = "", "", false + for line in eachline(git(repository, "diff", "-M", "--unified=0", "--no-color", + "--no-ext-diff", "--src-prefix=a/", "--dst-prefix=b/", reference)) + if startswith(line, "diff --git ") + hunk = false + elseif hunk + startswith(line, '-') && record!(removed, isempty(new) ? old : new, line[2:end]) + startswith(line, '+') && record!(added, new, line[2:end]) + elseif startswith(line, "--- ") + old = path(line, "a/") + elseif startswith(line, "+++ ") + new = path(line, "b/") + elseif startswith(line, "@@") + hunk = true + end + end + untracked = git(repository, "ls-files", "--others", "--exclude-standard", "-z") + for name in split(read(untracked, String), '\0'; keepempty = false) + file = joinpath(repository, name) + isfile(file) && foreach(line -> record!(added, String(name), line), eachline(file)) + end + return (; removed, added) +end + +# Whether each line with `marker` removed from `file` appears again as an added line of +# another file. Each added line matches one removed line, and the matched lines contain +# `marker` at least `drop` times. `unmatched` counts the available added lines per file. +function relocated!(unmatched, removed, file, marker, drop) + found, files = 0, sort!(collect(keys(unmatched))) + for line in get(removed, file, String[]) + occursin(marker, line) || continue + index = findfirst(name -> name != file && get(unmatched[name], line, 0) > 0, files) + index === nothing && return false + unmatched[files[index]][line] -= 1 + found += count(marker, line) + end + return found >= drop +end + +# The direction, "floor" or "ceiling", of each table of `document`, a version of `file`. +function directions(file, document) + file == BASELINE && return Dict(table => "ceiling" for table in keys(document)) + declared = Dict{String, String}(get(document, "directions", Dict{String, Any}())) + for table in keys(document) + table in METADATA || haskey(declared, table) || + error("$file declares no direction for [$table]") + end + all(in(("floor", "ceiling")), values(declared)) || + error("Each direction in $file is \"floor\" or \"ceiling\"") + return declared +end + +# The entries of `file` that moved against their table's direction since `reference`, +# as lines, and the tables new since `reference`, or nothing when `reference` has no +# `file`. A new table belongs to a lock introduced after `reference`, and its keys are +# not compared. +function moved(repository, reference, file) + success(pipeline(git(repository, "rev-parse", "--verify", "--quiet", + reference * "^{commit}"); stdout = devnull)) || + error("Unknown git revision: $reference") + success(pipeline(git(repository, "cat-file", "-e", "$reference:$file"); + stderr = devnull)) || return nothing + renamed = renames(repository, reference) + document = TOML.parse(read(git(repository, "show", "$reference:$file"), String)) + current = TOML.parsefile(joinpath(repository, file)) + direction = merge(directions(file, document), directions(file, current)) + environment(d) = get(d, "environment", Dict{String, Any}()) + rerecorded = file == PRESERVATION && environment(document) != environment(current) + values_of(d) = Dict(key => value for (key, value) in entries(d) + if first(split(key, " | ")) ∉ METADATA) + before = Dict{String, Any}() + for (key, value) in values_of(document) + mergewith!(+, before, Dict(rename_key(key, renamed) => value)) + end + now = values_of(current) + ceilings = Dict(key => value for (key, value) in before + if direction[first(split(key, " | "))] == "ceiling") + for (old, new) in owner_renames(ceilings, now) + before[new] = pop!(before, old) + end + tables = sort!([table for table in keys(current) + if table ∉ METADATA && !haskey(document, table)]) + # A corpus table admits new scenarios while it keeps every scenario of `reference`. + complete = Dict(table => all(key -> haskey(now, key), + (key for key in keys(before) if first(split(key, " | ")) == table)) for table in CORPUS) + lines = String[] + changes = nothing + unmatched = Dict{String, Dict{String, Int}}() + function relocated(table, path, drop) + haskey(RELOCATABLE, table) || return false + if changes === nothing + changes = changed_lines(repository, reference) + for (name, added) in changes.added, line in added + counts = get!(Dict{String, Int}, unmatched, name) + counts[line] = get(counts, line, 0) + 1 + end + end + return relocated!(unmatched, changes.removed, path, RELOCATABLE[table], drop) + end + for key in sort!(collect(union(keys(now), keys(before)))) + table, entry = split(key, " | "; limit = 2) + table in tables && continue + if direction[table] == "ceiling" + rerecorded && table in MEASURED && continue + haskey(now, key) || continue + if !haskey(before, key) + get(complete, table, false) || + push!(lines, string(key, ": ", now[key], " (absent at ", reference, ")")) + continue + end + else + haskey(before, key) || continue + drop = before[key] - get(now, key, 0) + drop > 0 && relocated(table, entry, drop) && continue + haskey(now, key) || + (push!(lines, string(key, ": removed (", before[key], " at ", reference, ")")); continue) + end + (direction[table] == "ceiling" ? now[key] > before[key] : now[key] < before[key]) && + push!(lines, string(key, ": ", now[key], " (", before[key], " at ", reference, ")")) + end + return (; lines, tables) +end + +# The architecture baseline keys added or raised since `reference`. +grown(repository, reference) = moved(repository, reference, BASELINE) + +function main(arguments) + length(arguments) == 1 || error("Usage: julia test/tools/baseline_ratchet.jl REF") + reference = only(arguments) + status = 0 + for file in (BASELINE, PRESERVATION) + result = moved(REPOSITORY, reference, file) + if result === nothing + println("No $file at $reference. Nothing to compare.") + continue + end + isempty(result.tables) || println(file, ": tables introduced since ", reference, ": ", + join(result.tables, ", "), ".") + if isempty(result.lines) + count = length(entries(TOML.parsefile(joinpath(REPOSITORY, file)))) + println(file, ": ", count, " entries, none moved against its direction since ", + reference, ".") + else + println(stderr, file, ": ceilings may only fall and floors only rise. ", + "Entries moved against their direction since ", reference, ":") + foreach(line -> println(stderr, " ", line), result.lines) + status = 1 + end + end + return status +end + +abspath(PROGRAM_FILE) == (@__FILE__) && exit(main(ARGS)) diff --git a/test/tools/durations.jl b/test/tools/durations.jl new file mode 100644 index 000000000..45e2d3a0c --- /dev/null +++ b/test/tools/durations.jl @@ -0,0 +1,86 @@ +# Per-item durations from the logs of complete test runs. Run +# `julia --project=test test/tools/durations.jl LOG...` from the repository root. +# +# The runner prints `Starting [t s] NAME` as each item starts and the elapsed time +# at the end. An item lasts until the next one starts. Its duration includes +# compilation and the effects of the items run before it. The report lists the +# items above 10 s without the `slow` tag, the `slow` items below 5 s and the +# minutes per owner tag. Findings are reported, never applied. Run it on the full +# ordinary log and on the environment logs at every track end. +using TestItemRunner +include(joinpath(@__DIR__, "..", "support", "runner.jl")) + +const REPOSITORY = dirname(dirname(@__DIR__)) +const SLOW = 10.0 +const FAST = 5.0 +const STARTING = r"^Starting \[([0-9.]+)s\] (.+)$" +const ELAPSED = r"maintained test items in \d+ files; run \w+; ([0-9.]+) s elapsed$" + +# Item name => seconds. The last item of a log without a final line has no duration. +function durations(log) + starts = Tuple{String, Float64}[] + finish = nothing + for line in eachline(log) + if (m = match(STARTING, line)) !== nothing + push!(starts, (m[2], parse(Float64, m[1]))) + elseif (m = match(ELAPSED, line)) !== nothing + finish = parse(Float64, m[1]) + end + end + isempty(starts) && error("No item starts in $log") + finish === nothing && println("No final line in $log; its last item has no duration.") + found = Dict{String, Float64}() + for (i, (name, start)) in enumerate(starts) + stop = i < length(starts) ? starts[i+1][2] : finish + stop === nothing || (found[name] = stop - start) + end + return found +end + +minutes(seconds) = round(seconds / 60; digits = 1) + +function report(logs) + items = Dict(item.name => item for item in ValidationTestRunner.inventory(REPOSITORY).items) + measured = Dict{String, Float64}() + for log in logs + found = durations(log) + println(log, ": ", length(found), " items, ", minutes(sum(values(found))), " min") + for (name, seconds) in found + haskey(measured, name) && error("$name appears in more than one log") + measured[name] = seconds + end + end + unknown = sort!([name for name in keys(measured) if !haskey(items, name)]) + isempty(unknown) || println("\nNot in the test files (renamed or removed): ", + join(unknown, "; ")) + row(name) = string(lpad(round(measured[name]; digits = 1), 7), " s ", + items[name].file, " | ", name) + known = sort!([name for name in keys(measured) if haskey(items, name)]; + by = name -> -measured[name]) + untagged = [name for name in known if measured[name] > SLOW && :slow ∉ items[name].tags] + println("\nAbove $(Int(SLOW)) s without `slow` (", length(untagged), ", ", + minutes(sum((measured[name] for name in untagged); init = 0.0)), " min):") + foreach(name -> println(row(name)), untagged) + tagged = [name for name in known if measured[name] < FAST && :slow in items[name].tags] + println("\n`slow` below $(Int(FAST)) s (", length(tagged), "):") + foreach(name -> println(row(name)), tagged) + println("\nMinutes per owner tag, `slow` items and the others:") + owner(name) = something(ValidationTestRunner.owner_tag(items[name].tags), :none) + for tag in (ValidationTestRunner.OWNERS..., :none) + names = [name for name in known if owner(name) === tag] + isempty(names) && continue + slow = [name for name in names if :slow in items[name].tags] + println(rpad(tag, 14), lpad(length(names), 4), " items ", lpad(length(slow), 3), + " slow ", lpad(minutes(sum((measured[name] for name in slow); init = 0.0)), 5), + " min ", lpad(minutes(sum((measured[name] for name in setdiff(names, slow)); + init = 0.0)), 5), " min") + end + return 0 +end + +function main(arguments) + isempty(arguments) && error("Usage: julia --project=test test/tools/durations.jl LOG...") + return report(arguments) +end + +abspath(PROGRAM_FILE) == (@__FILE__) && exit(main(ARGS)) diff --git a/test/tools/equivalence.jl b/test/tools/equivalence.jl new file mode 100644 index 000000000..2677b8719 --- /dev/null +++ b/test/tools/equivalence.jl @@ -0,0 +1,174 @@ +# Equivalence check of the preservation corpus at a git revision and on the working +# tree, compared bitwise. Run +# `julia --project=test test/tools/equivalence.jl REF [ALLOWED]` from the repository root. +# +# REF is prepared as for `performance.jl`. Each side records its own revision's corpus +# with its own recording program, `test/tools/fingerprint.jl`, in a fresh process. A +# revision without that program uses the working tree's program and corpus, and the +# report says which copy each side used. The recording lists every node of the inputs +# and the result of each scenario, with its type and the bits of its value. It ends with +# the laziness counts of the parametric scenarios, as `fingerprint.jl` describes. +# +# `ALLOWED` is an optional file of declarations, one per line, where `#` starts a +# comment. A line `rename | Old => New` renames whole identifiers in the type names, +# path segments and Symbol values recorded at REF before the comparison. A line +# `scenario | path prefix | kind` declares the differences of one kind under a path +# prefix, and an empty prefix declares the whole scenario. The kind is `value` for a +# changed value under the same type, `type` for a changed type and `path` for a node +# found on one side only. Declared differences pass. The report lists renames and +# declarations that match no difference. Any other difference fails. CI does not run +# this tool. +const REPOSITORY = dirname(dirname(@__DIR__)) +const RECORDER = joinpath("test", "tools", "fingerprint.jl") +const KINDS = (:value, :type, :path) +const SHOWN = 5 + +Base.include(@__MODULE__, joinpath(@__DIR__, "worktree.jl")) + +# The scenarios in recorded order, and each scenario's (path => (type, value)) nodes in +# recorded order. +function fingerprints(project, recorder, file) + output(`$(Base.julia_cmd()) --startup-file=no --project=$project $recorder $file`) + order = String[] + nodes = Dict{String, Vector{Pair{String, Tuple{String, String}}}}() + for line in eachline(file) + scenario, path, type, value = String.(split(line, '\t'; limit = 4)) + haskey(nodes, scenario) || (push!(order, scenario); nodes[scenario] = []) + push!(nodes[scenario], path => (type, value)) + end + return (; order, nodes) +end + +const IDENTIFIER = r"^[A-Za-z_][\w!]*$" + +# The renames and the declared differences of an ALLOWED file. +function declarations(file) + renames = Pair{String, String}[] + declared = @NamedTuple{scenario::String, prefix::String, kind::Symbol}[] + file === nothing && return (; renames, declared) + for (number, raw) in enumerate(eachline(file)) + text = strip(first(split(raw, '#'))) + isempty(text) && continue + fields = String.(strip.(split(text, '|'))) + if length(fields) == 2 && fields[1] == "rename" && occursin("=>", fields[2]) + old, new = String.(strip.(split(fields[2], "=>"; limit = 2))) + occursin(IDENTIFIER, old) && occursin(IDENTIFIER, new) || + error("$file:$number: a rename names two identifiers") + push!(renames, old => new) + elseif length(fields) == 3 && Symbol(fields[3]) in KINDS + push!(declared, (; scenario = fields[1], prefix = fields[2], kind = Symbol(fields[3]))) + else + error("$file:$number: expected `rename | Old => New` or " * + "`scenario | path prefix | value, type or path`") + end + end + return (; renames, declared) +end + +# `text` with every rename applied to whole identifiers. `used[i]` records a match. +function renamed(text, renames, used) + for (i, (old, new)) in enumerate(renames) + pattern = Regex("(? new) + end + return text +end + +# Nodes whose value is a type name (an `eltype` row or a type met in the walk) or a +# Symbol. Renames change their values too. +const NAMED = ("Type", "DataType", "UnionAll", "Union", "TypeofBottom", "Symbol") + +# REF's nodes with the renames applied to paths, type names and Symbol values. +renamed(nodes::Vector, renames, used) = [renamed(path, renames, used) => + (renamed(type, renames, used), type in NAMED ? renamed(value, renames, used) : value) + for (path, (type, value)) in nodes] + +# The differing paths of a scenario, with the node on each side: the working tree's +# paths in recorded order, then the paths found only at the revision. +function differences(old, new) + before, after = Dict(old), Dict(new) + paths = [first.(new); [path for path in first.(old) if !haskey(after, path)]] + return [(path, get(before, path, nothing), get(after, path, nothing)) + for path in paths if get(before, path, nothing) != get(after, path, nothing)] +end + +# The kind of a difference is `:value` for a changed value under the same type, `:type` +# for a changed type and `:path` for a node on one side only. +kind((_, a, b)) = a === nothing || b === nothing ? :path : a[1] == b[1] ? :value : :type + +covers(declaration, scenario, change) = declaration.scenario == scenario && + declaration.kind === kind(change) && startswith(first(change), declaration.prefix) + +shown(x) = x === nothing ? "absent" : + first(x[2] == "" ? x[1] : string(x[1], " ", x[2]), 150) + +laziness(nodes) = (i = findfirst(n -> first(n) == "laziness", nodes); + i === nothing ? "absent" : last(last(nodes[i]))) + +function compare(reference, allowed) + (; renames, declared) = declarations(allowed) + return at_revision(reference) do project + recorder = own_copy(dirname(project), RECORDER) + println(reference, ": ", recorder.own ? "its own recording program and corpus" : + "the working tree's recording program and corpus ($reference has no $RECORDER)") + println("working tree: its own recording program and corpus") + directory = mktempdir() + old = fingerprints(project, recorder.path, joinpath(directory, "reference.tsv")) + new = fingerprints(joinpath(REPOSITORY, "test"), joinpath(REPOSITORY, RECORDER), + joinpath(directory, "working.tsv")) + used_renames = falses(length(renames)) + used = falses(length(declared)) + unexpected = 0 + for scenario in unique([new.order; old.order]) + before = renamed(get(old.nodes, scenario, []), renames, used_renames) + nodes = get(new.nodes, scenario, []) + changes = differences(before, nodes) + counts = (laziness(before), laziness(nodes)) + lazy = counts == ("absent", "absent") ? "" : first(counts) == last(counts) ? + "; laziness: " * last(counts) : "; laziness: $(first(counts)) → $(last(counts))" + if isempty(changes) + println(rpad(scenario, 16), "identical (", length(nodes), " nodes", lazy, ")") + continue + end + free = filter(changes) do change + matches = [i for (i, d) in enumerate(declared) if covers(d, scenario, change)] + used[matches] .= true + isempty(matches) + end + unexpected += length(free) + tally = [count(c -> kind(c) === k, changes) for k in KINDS] + println(rpad(scenario, 16), length(changes), " of ", length(nodes), " nodes differ: ", + tally[1], " values, ", tally[2], " types, ", tally[3], " paths", + length(free) == length(changes) ? "" : " ($(length(changes) - length(free)) declared)", + lazy) + patterns = [replace(first(c), r"\[[^\]]*\]" => "[*]") for c in changes] + println(" by path: ", join(["$p $(count(==(p), patterns))" + for p in first(unique(patterns), 12)], ", "), + length(unique(patterns)) > 12 ? ", …" : "") + listed = isempty(free) ? changes : free + for k in KINDS, (path, a, b) in first(filter(c -> kind(c) === k, listed), SHOWN) + println(" ", k, " ", path, ": ", shown(a), " → ", shown(b)) + end + end + for (i, (old, new)) in enumerate(renames) + used_renames[i] || println("Rename that matches nothing: ", old, " => ", new) + end + for (i, d) in enumerate(declared) + used[i] || println("Declared but not found: ", d.scenario, " | ", d.prefix, " | ", d.kind) + end + return unexpected + end +end + +function main(arguments) + 1 <= length(arguments) <= 2 || + error("Usage: julia --project=test test/tools/equivalence.jl REF [ALLOWED]") + unexpected = compare(arguments[1], get(arguments, 2, nothing)) + unexpected == 0 && return 0 + println(stderr, unexpected, " undeclared differences.") + return 1 +end + +abspath(PROGRAM_FILE) == (@__FILE__) && exit(main(ARGS)) diff --git a/test/tools/fingerprint.jl b/test/tools/fingerprint.jl new file mode 100644 index 000000000..b250b3b3e --- /dev/null +++ b/test/tools/fingerprint.jl @@ -0,0 +1,158 @@ +# The recording program of the equivalence check (`equivalence.jl`). It records the +# preservation corpus of the revision that contains it. Each revision records with its +# own version of this file. +# Run `julia --project=test test/tools/fingerprint.jl DESTINATION`. +# +# For each scenario of `preservation_corpus(2)` it writes one line per node of the +# inputs and the result, with the scenario, the path, the type and the value. Numbers are +# written as their bits, a Measurement as value and error. Each type name is written as +# `Owner.Name`, where `Owner` is the module that defines it directly, whatever Main +# imports. Base and Core types are bare. Anonymous functions are written as +# `var"#closure"`, because their numbers change when unrelated source changes, and no +# type alias is used. Random identities (UUIDs) are numbered by first appearance within +# a scenario, which keeps which nodes share one. Dictionaries are walked in sorted key +# order. An object met again records the path where it was first met. A last pass +# counts, per parametric scenario, the points it materializes and the blueprint lowerings +# of its designs. +using LineCableModels, Measurements +import LineCableModels.Engine as EN + +Base.include(@__MODULE__, joinpath(@__DIR__, "..", "support", "scenarios.jl")) +using .CurrentScenarios: preservation_corpus + +# Counts blueprint lowerings of designs whose nominal data is a counter, then runs the +# production lowering, as `test/integration/formulation_grid.jl` does. +struct LoweringCounter + calls::Base.RefValue{Int} +end +function EN.flatten(engine::LineCableModelsCoaxial, + design::CableDesign{T, R, G, NamedTuple{(:counter,), Tuple{LoweringCounter}}}, + ::Type{S}, methods::NamedTuple, solutions::Vector, design_index::Int) where { + T <: Real, R <: AbstractCablePart, G <: LineCableModels.DataModel.CableGeometry, S <: Real} + design.nominal_data.counter.calls[] += 1 + return invoke(EN.flatten, + Tuple{LineCableModelsCoaxial, CableDesign, Type{S}, NamedTuple, Vector, Int}, + engine, design, S, methods, solutions, design_index) +end + +const CLOSURE = "var\"#closure\"" + +bare(m::Module) = Base.moduleroot(m) in (Base, Core) + +# A type parameter that is a value, such as a Symbol or a tuple. +parameter(x) = x isa Type || x isa TypeVar ? typename(x) : + x isa Tuple ? string("(", join(parameter.(x), ", "), length(x) == 1 ? ",)" : ")") : + repr(x) + +function typename(@nospecialize(T)) + T isa TypeVar && return string(T.name) + T isa Union && return string("Union{", join(typename.(Base.uniontypes(T)), ", "), "}") + T isa UnionAll && return string(typename(T.body), " where ", T.var.name) + T isa Core.TypeofVararg && + return string("Vararg{", isdefined(T, :T) ? typename(T.T) : "", "}") + T isa DataType || return repr(T) + name = string(T.name.name) + owner = parentmodule(T) + if startswith(name, "#") + function_name = match(r"^#([^#]+)$", name) + function_name === nothing && return CLOSURE + return string("typeof(", bare(owner) ? "" : string(nameof(owner), "."), + function_name[1], ")") + end + qualified = bare(owner) ? name : string(nameof(owner), ".", name) + isempty(T.parameters) && return qualified + return string(qualified, "{", join(parameter.(T.parameters), ", "), "}") +end + +bits(x) = bytes2hex(reinterpret(UInt8, [x])) +flat(text) = replace(text, r"\s+" => " ") + +# `state.seen` maps each mutable object met to its first path. `state.uuids` numbers the +# random identities (gridpoint source ids) by first appearance. +function walk!(io, path, x, state) + line(value) = println(io, path, '\t', typename(typeof(x)), '\t', value) + if x isa Base.UUID + line(string("uuid #", get!(state.uuids, x, length(state.uuids) + 1))) + elseif x isa Measurement + line(string(bits(Measurements.value(x)), " ± ", bits(Measurements.uncertainty(x)))) + elseif x isa Complex + line("") + walk!(io, path * ".re", real(x), state) + walk!(io, path * ".im", imag(x), state) + elseif x isa Number && isbitstype(typeof(x)) + line(bits(x)) + elseif x isa Union{AbstractString, Symbol, Char, Nothing, Missing} + line(repr(x)) + elseif x isa Type + line(typename(x)) + elseif x isa Module + line(string(nameof(x))) + elseif x isa Ptr + line("pointer") + elseif ismutable(x) && haskey(state.seen, x) + line("same as " * state.seen[x]) + else + ismutable(x) && (state.seen[x] = path) + if x isa AbstractDict + line(string(length(x))) + for key in sort!(collect(keys(x)); by = key -> sprint(show, key)) + walk!(io, string(path, "[", flat(sprint(show, key)), "]"), x[key], state) + end + elseif x isa AbstractArray + line(string(size(x))) + for index in eachindex(x) + isassigned(x, index) && walk!(io, string(path, "[", index, "]"), x[index], state) + end + else + line("") + for name in fieldnames(typeof(x)) + isdefined(x, name) && walk!(io, string(path, ".", name), getfield(x, name), state) + end + end + end + return nothing +end + +function counted(name) + materialized = Ref(0) + counter = LoweringCounter(Ref(0)) + counters = (materialized = () -> (materialized[] += 1), nominal_data = (; counter)) + compute(preservation_corpus(2; counters)[name]...) + return (materialized[], counter.calls[]) +end + +function main(destination) + open(destination, "w") do io + corpus = preservation_corpus(2) + for name in keys(corpus) + arguments = corpus[name] + out = IOBuffer() + state = (seen = IdDict{Any, String}(), uuids = Dict{Base.UUID, Int}()) + try + for (i, argument) in enumerate(arguments) + println(out, "inputs[", i, "].eltype\tType\t", typename(eltype(argument))) + walk!(out, "inputs[$i]", argument, state) + end + result = compute(arguments...) + println(out, "result.eltype\tType\t", typename(eltype(result))) + walk!(out, "result", result, state) + catch error + println(out, "result\tfailed\t", first(flat(sprint(showerror, error)), 300)) + end + for text in eachline(IOBuffer(take!(out))) + println(io, name, '\t', text) + end + end + # The instrumented pass follows, so it cannot change the recorded values. + for name in (:product, :zip, :linear_error, :monte_carlo) + value = try + join(counted(name), " materializations, ") * " lowerings" + catch error + "failed: " * first(flat(sprint(showerror, error)), 300) + end + println(io, name, "\tlaziness\tcounts\t", value) + end + end +end + +abspath(PROGRAM_FILE) == (@__FILE__) && main(only(ARGS)) diff --git a/test/tools/owners.jl b/test/tools/owners.jl new file mode 100644 index 000000000..2d18c77af --- /dev/null +++ b/test/tools/owners.jl @@ -0,0 +1,133 @@ +# Derive the owner of each test item from the `src/` and `ext/` code it executes. +# +# Record a selection under coverage, from the repository root, once per environment: +# `julia --project=test --code-coverage=@$PWD --code-coverage=/tmp/trace.info test/tools/owners.jl record DIR [SELECTORS...]`. +# +# Each item start dumps and clears the coverage counters, so `DIR/items.tsv` holds one +# line per item: name, test file and the executed files with their executed line counts. +# The package is loaded before the first item, so loading is not attributed to it. A +# `@testmodule` is evaluated once, by the first item that uses it, and its executions +# count for that item only. The `--code-coverage=FILE.info` flag keeps Julia from +# writing `.cov` files next to the sources at exit. Then report with +# `julia --project=test test/tools/owners.jl report DIR...`. +# +# Each executed file counts at its load position (`path_owner` in +# `test/support/taxonomy.jl`). The item's executed owner is the latest of them. The +# report lists every recorded item with its executed owner, its owner tag and the +# executed files at that owner, then the items whose owner tag is earlier than the +# code they execute. Items that run code only in child processes have an empty record. +# The report never changes tags. Running it at a track end is optional. +using TestItemRunner +include(joinpath(@__DIR__, "..", "support", "runner.jl")) + +const REPOSITORY = dirname(dirname(@__DIR__)) + +# The files under `src/` and `ext/` with at least one executed line, and how many. +function executed(tracefile) + found = Dict{String, Int}() + file = nothing + for line in eachline(tracefile) + if startswith(line, "SF:") + path = ValidationTestRunner.relative(normpath(line[4:end]), REPOSITORY) + file = startswith(path, "src/") || startswith(path, "ext/") ? path : nothing + elseif file !== nothing && startswith(line, "DA:") + hits = parse(Int, split(line[4:end], ',')[2]) + hits > 0 && (found[file] = get(found, file, 0) + 1) + end + end + return found +end + +function record(directory, selectors) + Base.JLOptions().code_coverage == 0 && + error("Run `record` with --code-coverage=@ --code-coverage=.info") + mkpath(directory) + items = joinpath(directory, "items.tsv") + isfile(items) && error("$items exists; record into an empty directory") + tracefile = joinpath(directory, "item.info") + @eval Main using LineCableModels + files = Dict{String, String}() + current = Ref{Union{Nothing, String}}(nothing) + function dump() + ccall(:jl_write_coverage_data, Cvoid, (Cstring,), tracefile) + if current[] !== nothing + found = executed(tracefile) + line = join(sort!(["$file=$count" for (file, count) in found]), ';') + open(io -> println(io, current[], '\t', files[current[]], '\t', line), items, "a") + end + rm(tracefile; force = true) + ccall(:jl_clear_coverage_data, Cvoid, ()) + end + select = ValidationTestRunner.selection(selectors, joinpath(REPOSITORY, "test")) + filter = item -> begin + accepted = select(item) + accepted && (files[String(item.name)] = + ValidationTestRunner.relative(item.filename, REPOSITORY)) + accepted + end + dump() + try + ValidationTestRunner.run_tests(REPOSITORY; filter, verbose = true, + on_start = name -> (dump(); current[] = name)) + finally + dump() + end +end + +# Item name => test file, executed owner, and the executed files at that owner with +# their executed line counts, most first. +function executed_owners(directories, owners) + found = Dict{String, @NamedTuple{file::String, owner::Union{Nothing, Symbol}, + evidence::Vector{Pair{String, Int}}}}() + for directory in directories, line in eachline(joinpath(directory, "items.tsv")) + name, file, entries = split(line, '\t') + files = map(split(entries, ';'; keepempty = false)) do entry + path, count = rsplit(entry, '='; limit = 2) + String(path) => parse(Int, count) + end + owner = ValidationTestRunner.latest( + [ValidationTestRunner.path_owner(owners, first(f)) for f in files]) + evidence = sort!([f for f in files + if ValidationTestRunner.path_owner(owners, first(f)) === owner]; by = last, rev = true) + haskey(found, name) && error("$name is recorded more than once") + found[name] = (; file, owner, evidence) + end + return found +end + +function report(directories) + owners = ValidationTestRunner.loaded_owners(REPOSITORY) + recorded = executed_owners(directories, owners) + items = ValidationTestRunner.inventory(REPOSITORY).items + tagged = Dict(item.name => ValidationTestRunner.owner_tag(item.tags) for item in items) + label(owner) = owner === nothing ? "-" : String(owner) + evidence(record) = join(["$path=$count" for (path, count) in first(record.evidence, 3)], ", ") + row(name, record) = string(rpad(label(record.owner), 14), + rpad(label(get(tagged, name, nothing)), 14), record.file, " | ", name, + isempty(record.evidence) ? "" : " [" * evidence(record) * "]") + names = sort!(collect(keys(recorded)); by = name -> (recorded[name].file, name)) + println("Recorded items (", length(names), "): executed owner, owner tag, ", + "file | name [executed files at that owner]") + foreach(name -> println(row(name, recorded[name])), names) + unknown = [name for name in names if !haskey(tagged, name)] + isempty(unknown) || + println("\nNot in the test files (renamed or removed): ", join(unknown, "; ")) + earlier = [name for name in names if haskey(tagged, name) && recorded[name].owner !== nothing && + ValidationTestRunner.rank(something(tagged[name], :none)) < + ValidationTestRunner.rank(recorded[name].owner)] + println("\nOwner tag earlier than the executed code (", length(earlier), "):") + foreach(name -> println(row(name, recorded[name])), earlier) + return 0 +end + +const USAGE = "Usage: owners.jl record DIR [SELECTORS...] | owners.jl report DIR..." + +function main(arguments) + length(arguments) >= 2 || error(USAGE) + command, rest = arguments[1], arguments[2:end] + command == "record" && return (record(rest[1], rest[2:end]); 0) + command == "report" && return report(rest) + error(USAGE) +end + +abspath(PROGRAM_FILE) == (@__FILE__) && exit(main(ARGS)) diff --git a/test/tools/performance.jl b/test/tools/performance.jl new file mode 100644 index 000000000..7214915f2 --- /dev/null +++ b/test/tools/performance.jl @@ -0,0 +1,279 @@ +# Timing comparison of the preservation corpus at a git revision and on the working +# tree, on this machine. Run +# `julia --project=test test/tools/performance.jl REF [--frequencies N] [--rounds R] [--seconds S]` +# from the repository root. +# +# REF is checked out in a temporary git worktree. Its test environment starts from this +# repository's `Manifest.toml`, resolved again for REF's projects, so both sides use the +# same dependency versions wherever REF allows them. The report lists any that differ. +# Each side runs its own revision's corpus, `preservation_corpus` in +# `test/support/scenarios.jl` with `N` frequencies (default 4). A revision without one +# uses the working tree's corpus. The report says which copy each side used and whether +# the two corpus files differ. +# +# One worker process per side holds its corpus for the whole run. `taskset` pins both +# workers to the same logical CPU, a performance core on a hybrid machine +# (`/sys/devices/cpu_core/cpus`), never CPU 0 or its sibling. The sides alternate and +# never run together, and they share identical hardware. Without `taskset` they run +# unpinned, with a notice. Each scenario is warmed up once on each side and then sampled +# with BenchmarkTools in `R` rounds (default 6) of `S` seconds per side (default 0.5), +# with a garbage collection before each round. The side that goes first alternates. Load +# on the machine only ever adds time. Each side keeps its minimum over all samples, and +# the medians are printed for information only. The allocation ceilings in +# `test/quality/preservation.toml` guard the allocations behind garbage collection +# exactly. +# +# A scenario whose round minima spread by more than 2 % on either side is unstable and +# has no verdict. Separate builds of identical code differ by a few percent, because each +# worktree builds REF's package image again. A stable scenario more than 10 % slower +# than REF is a slowdown, and one 5 to 10 % slower is a possible slowdown below the +# resolution of the tool. A slowdown, or a scenario that fails on the working tree, exits +# with 1. A possible slowdown or an unstable scenario exits with 2: no verdict, and not a +# regression. Otherwise the tool exits with 0. A scenario that cannot run at REF is +# reported as not comparable. A failing scenario is never retried. The tool prints the +# 1-minute load average at the start and the end. Both revisions run on the machine that +# runs the tool, a developer machine or a CI runner. Other load on that machine makes +# scenarios unstable. The CI workflow `.github/workflows/timing.yml` runs this tool. +const REPOSITORY = dirname(dirname(@__DIR__)) +const CORPUS = joinpath("test", "support", "scenarios.jl") +const POSSIBLE = 0.05 +const SLOWER = 0.10 +const STABILITY = 0.02 + +# The worker builds its corpus once and prints the scenario names on one line, or +# `failed` and the error. It then answers each request `scenario seconds` with one line: +# the sample times [ns] of that many seconds of samples, or `failed` and the error. +const WORKER = raw""" +using LineCableModels, Measurements, BenchmarkTools +include(ARGS[1]) +using .CurrentScenarios: preservation_corpus + +flat(error) = first(replace(sprint(showerror, error), r"\s+" => " "), 200) + +# A scenario's benchmark, compiled and warmed up on first use. +function benchmark(arguments) + compute(arguments...) + # BenchmarkTools collects garbage before each round (`gctrial`), so the scenarios that + # allocate heavily start every round with samples free of collections. + trial = @benchmarkable(compute($arguments...), evals = 1, samples = 100_000) + BenchmarkTools.warmup(trial; verbose = false) + return trial +end + +function serve(corpus) + benchmarks = Dict{Symbol, Any}() + for line in eachline(stdin) + name, seconds = split(line) + reply = try + scenario = Symbol(name) + trial = get!(() -> benchmark(corpus[scenario]), benchmarks, scenario) + join(run(trial; seconds = parse(Float64, seconds)).times, ' ') + catch error + "failed " * flat(error) + end + println(reply) + flush(stdout) + end +end + +corpus = try + preservation_corpus(parse(Int, ARGS[2])) +catch error + println("failed ", flat(error)) + exit() +end +println(join(keys(corpus), ' ')) +flush(stdout) +serve(corpus) +""" + +Base.include(@__MODULE__, joinpath(@__DIR__, "worktree.jl")) + +# Logical CPU ids from a list such as "0-15,18". +function cpu_list(text) + ids = Int[] + for part in split(strip(text), ','; keepempty = false) + bounds = parse.(Int, split(part, '-')) + append!(ids, first(bounds):last(bounds)) + end + return ids +end + +# The logical CPU both workers share, or nothing when `taskset` is missing. It is a +# performance core of a hybrid machine, or any CPU otherwise, and never CPU 0 or its +# sibling. +function shared_cpu(; taskset = Sys.which("taskset"), + performance = "/sys/devices/cpu_core/cpus", + siblings = "/sys/devices/system/cpu/cpu0/topology/thread_siblings_list", + cpus = Sys.CPU_THREADS) + taskset === nothing && return nothing + candidates = isfile(performance) ? cpu_list(read(performance, String)) : collect(0:cpus-1) + avoided = isfile(siblings) ? union(cpu_list(read(siblings, String)), [0]) : [0] + eligible = setdiff(candidates, avoided) + return isempty(eligible) ? nothing : first(eligible) +end + +struct Worker + process::Base.Process + errors::String + scenarios::Vector{String} + failure::Union{Nothing, String} +end + +# Starts a worker on `corpus` in `project`, pinned to `cpu` unless it is nothing. +# `finish` collects its first line. +function launch(project, corpus, n, cpu) + errors = tempname() + command = julia(project, WORKER, corpus, n) + cpu === nothing || (command = `taskset -c $cpu $command`) + process = open(pipeline(command; stderr = errors), "r+") + return (process, errors) +end + +function finish((process, errors)) + line = readline(process) + isempty(line) && + error("The timing worker stopped:\n" * read(errors, String)) + startswith(line, "failed ") && return Worker(process, errors, String[], line[8:end]) + return Worker(process, errors, split(line), nothing) +end + +function request(worker, scenario, seconds) + println(worker.process, scenario, " ", seconds) + flush(worker.process) + line = readline(worker.process) + isempty(line) && + error("The timing worker stopped:\n" * read(worker.errors, String)) + return startswith(line, "failed ") ? line[8:end] : parse.(Float64, split(line)) +end + +milliseconds(t) = string(round(t / 1e6; sigdigits = 4), " ms") +median(samples) = (s = sort(samples); n = length(s); isodd(n) ? s[(n + 1) ÷ 2] : + (s[n ÷ 2] + s[n ÷ 2 + 1]) / 2) + +# Scenario => side (1 = REF, 2 = working tree) => all samples and the minimum of each +# round, or the error of the first failure. Rounds alternate which side goes first. A +# failed scenario is not retried. +function interleave(workers, rounds, seconds) + measured = Dict{String, Vector{Any}}() + for scenario in last(workers).scenarios + found = Any[(samples = Float64[], minima = Float64[]) for _ in 1:2] + if !(scenario in first(workers).scenarios) + found[1] = something(first(workers).failure, "absent") + end + for round in 1:rounds, side in (isodd(round) ? (1, 2) : (2, 1)) + found[side] isa String && continue + reply = request(workers[side], scenario, seconds) + if reply isa String + found[side] = reply + else + append!(found[side].samples, reply) + push!(found[side].minima, minimum(reply)) + end + end + measured[scenario] = found + end + return measured +end + +# The verdict on a scenario from its change against REF and the round spreads of both +# sides: `:unstable` when a spread exceeds 2 %, else `:slower` above 10 %, `:possible` +# between 5 and 10 %, otherwise `:same`. +verdict(change, spreads) = maximum(spreads) > STABILITY ? :unstable : + change > SLOWER ? :slower : change > POSSIBLE ? :possible : :same + +const VERDICTS = Dict(:unstable => " unstable: no verdict", + :slower => " slower than $(round(Int, 100SLOWER)) %", + :possible => " possible slowdown, below the tool's resolution", :same => "") + +# 1 for a slowdown or a failure on the working tree, otherwise 2 for a possible slowdown +# or an unstable scenario, otherwise 0. +status(verdicts) = any(in((:slower, :failed)), verdicts) ? 1 : + any(in((:possible, :unstable)), verdicts) ? 2 : 0 + +# The relative spread of the round minima of one side. +spread(minima) = (maximum(minima) - minimum(minima)) / minimum(minima) +percent(x) = string(round(100x; digits = 1), " %") +load() = round(first(Sys.loadavg()); digits = 2) + +function compare(reference; n = 4, rounds = 6, seconds = 0.5) + working = joinpath(REPOSITORY, "test") + println("Load average (1 min) at the start: ", load()) + return at_revision(reference) do project + corpus = own_copy(dirname(project), CORPUS; marker = "function preservation_corpus(") + println(reference, ": ", corpus.own ? "its own corpus" : + "the working tree's corpus ($reference has no preservation corpus)") + println("working tree: its own corpus") + corpus.own && read(corpus.path) != read(joinpath(REPOSITORY, CORPUS)) && + println("Notice: the corpus file differs between the two sides.") + cpu = shared_cpu() + println(cpu === nothing ? + "Notice: taskset or an eligible CPU is missing; the workers run unpinned." : + "Both workers are pinned to logical CPU $cpu.") + println("Timing $n frequencies in $rounds interleaved rounds of $(seconds) s per side.") + started = (launch(project, corpus.path, n, cpu), + launch(working, joinpath(REPOSITORY, CORPUS), n, cpu)) + workers = map(finish, started) + try + last(workers).failure === nothing || + error("The working tree's corpus fails: " * last(workers).failure) + measured = interleave(workers, rounds, seconds) + verdicts = Pair{String, Symbol}[] + println(rpad("scenario", 17), lpad("$reference min", 14), lpad("working min", 14), + lpad("change", 10), lpad("spread", 16), " medians, for information") + for scenario in last(workers).scenarios + old, new = measured[scenario] + if new isa String + println(rpad(scenario, 17), "fails on the working tree: ", new) + push!(verdicts, scenario => :failed) + elseif old isa String + println(rpad(scenario, 17), lpad("—", 14), + lpad(milliseconds(minimum(new.samples)), 14), + " not comparable at $reference: ", old) + else + change = minimum(new.samples) / minimum(old.samples) - 1 + spreads = (spread(old.minima), spread(new.minima)) + found = verdict(change, spreads) + push!(verdicts, scenario => found) + println(rpad(scenario, 17), lpad(milliseconds(minimum(old.samples)), 14), + lpad(milliseconds(minimum(new.samples)), 14), lpad(percent(change), 10), + lpad(percent(first(spreads)) * " / " * percent(last(spreads)), 16), " ", + milliseconds(median(old.samples)), " / ", milliseconds(median(new.samples)), + VERDICTS[found]) + end + end + return verdicts + finally + for worker in workers + close(worker.process) + wait(worker.process) + end + println("Load average (1 min) at the end: ", load()) + end + end +end + +function main(arguments) + usage = "Usage: julia --project=test test/tools/performance.jl REF " * + "[--frequencies N] [--rounds R] [--seconds S]" + isempty(arguments) && error(usage) + reference, options = arguments[1], arguments[2:end] + n, rounds, seconds = 4, 6, 0.5 + while !isempty(options) + length(options) >= 2 || error(usage) + option, value = popfirst!(options), popfirst!(options) + option == "--frequencies" ? (n = parse(Int, value)) : + option == "--rounds" ? (rounds = parse(Int, value)) : + option == "--seconds" ? (seconds = parse(Float64, value)) : error(usage) + end + verdicts = compare(reference; n, rounds, seconds) + named(kinds) = join([s for (s, v) in verdicts if v in kinds], ", ") + code = status(last.(verdicts)) + code == 1 && println(stderr, "Slower than ", round(Int, 100SLOWER), " % or failing: ", + named((:slower, :failed))) + code == 2 && println(stderr, "Possible slowdown or unstable, no verdict: ", + named((:possible, :unstable))) + return code +end + +abspath(PROGRAM_FILE) == (@__FILE__) && exit(main(ARGS)) diff --git a/test/tools/regenerate_goldens.jl b/test/tools/regenerate_goldens.jl new file mode 100644 index 000000000..24b730e8c --- /dev/null +++ b/test/tools/regenerate_goldens.jl @@ -0,0 +1,28 @@ +# Validate selection before importing a renderer or constructing any scene. +get(ENV,"LINECABLEMODELS_UPDATE_PLOT_REFERENCES","false")=="true" || + error("set LINECABLEMODELS_UPDATE_PLOT_REFERENCES=true to generate provisional images") +requested=get(ENV,"LINECABLEMODELS_PLOT_REFERENCE","") +names=("line_rlcg","line_zy_cartesian","line_zy_polar","formulation_comparison", + "uq_comparison","uncertainty_intervals","cable_preview","cable_preview_compact", + "system_preview","material_scale","mc_hist","mc_pdf","mc_ecdf","mc_qq","custom_layout") +requested in names || throw(ArgumentError("select one current provisional view; unknown selection: $requested")) +using SHA, TOML +include(joinpath(@__DIR__,"..","support","golden_fixtures.jl")) +using .GoldenFixtures +output=mktempdir(;prefix="lcm-rendering-provisional-",cleanup=false) +println("Provisional output: ",output) +open(joinpath(output,"identity.toml"),"w") do io + TOML.print(io,Dict("status"=>"provisional","view"=>requested,"julia"=>string(VERSION), + "project"=>Base.active_project(),"threads"=>Threads.nthreads(), + "recipe_sha256"=>bytes2hex(open(sha256,joinpath(@__DIR__,"..","support","golden_fixtures.jl"))), + "source_commit"=>readchomp(`git -C $(dirname(dirname(@__DIR__))) rev-parse HEAD`))) +end +constructed=scene(requested) +handles=constructed isa AbstractVector ? constructed : [constructed] +for (index,handle) in enumerate(handles) + name=length(handles)==1 ? requested : requested*"-page$index" + save_pixels(joinpath(output,name*".png"),handle) +end +check_scene(requested,handles) +println("Saved ",length(handles)," current pages; all pages belong to the named view") +println("Saved provisional view: ",requested) diff --git a/test/tools/worktree.jl b/test/tools/worktree.jl new file mode 100644 index 000000000..9e46953c0 --- /dev/null +++ b/test/tools/worktree.jl @@ -0,0 +1,60 @@ +# A git revision checked out in a temporary worktree, for the tools that compare it with +# the working tree (`performance.jl`, `equivalence.jl`). Include this file after +# defining `REPOSITORY`. + +git(arguments...) = Cmd(["git", "-C", REPOSITORY, arguments...]) +julia(project, program, arguments...) = + `$(Base.julia_cmd()) --startup-file=no --project=$project -e $program $arguments` + +# The standard output of `command`, which must succeed. +function output(command) + out, err = IOBuffer(), IOBuffer() + process = run(pipeline(ignorestatus(command); stdout = out, stderr = err)) + success(process) || error("Command failed: $(command)\n$(String(take!(err)))") + return String(take!(out)) +end + +const VERSIONS = raw""" +using Pkg +for (_, p) in Pkg.dependencies() + p.version === nothing || println(p.name, "\t", p.version) +end +""" +versions(project) = Dict(split(line, '\t') for line in eachline(IOBuffer(output(julia(project, VERSIONS))))) + +# Runs `f(project)` with the test project of `reference` in a temporary worktree. Its +# environment starts from this repository's `Manifest.toml`, resolved again for +# `reference`. This copy overwrites a `Manifest.toml` that `reference` commits. The tool +# prints the dependency versions that then differ from the working tree. The worktree +# is removed afterwards. +function at_revision(f, reference) + success(pipeline(git("rev-parse", "--verify", "--quiet", reference * "^{commit}"); + stdout = devnull)) || error("Unknown git revision: $reference") + directory = mktempdir() + run(pipeline(git("worktree", "add", "--detach", "--quiet", directory, reference); + stderr = devnull)) + try + manifest = joinpath(REPOSITORY, "Manifest.toml") + isfile(manifest) && cp(manifest, joinpath(directory, "Manifest.toml"); force = true) + project = joinpath(directory, "test") + println("Preparing $reference in a temporary worktree...") + output(julia(project, "using Pkg; Pkg.resolve(); Pkg.instantiate()")) + before, after = versions(project), versions(joinpath(REPOSITORY, "test")) + differing = sort!([name for name in keys(after) + if haskey(before, name) && before[name] != after[name]]) + println(isempty(differing) ? "Dependency versions: identical on both sides." : + "Dependency versions that differ at $reference: " * + join(["$name $(before[name]) (working tree $(after[name]))" for name in differing], ", ")) + return f(project) + finally + run(pipeline(git("worktree", "remove", "--force", directory); stderr = devnull)) + end +end + +# The revision's own copy of the file `relative` when it has one (containing `marker` +# when given), otherwise the working tree's copy. `own` tells which. +function own_copy(directory, relative; marker = nothing) + path = joinpath(directory, relative) + own = isfile(path) && (marker === nothing || occursin(marker, read(path, String))) + return (; path = own ? path : joinpath(REPOSITORY, relative), own) +end diff --git a/test/unit/commons/consts.jl b/test/unit/commons/consts.jl new file mode 100644 index 000000000..37df4c7ed --- /dev/null +++ b/test/unit/commons/consts.jl @@ -0,0 +1,16 @@ +@testitem "Commons / vacuum constants are evaluated in the requested type" tags=[:unit, :commons] begin + using LineCableModels.Commons: vacuum_permittivity, vacuum_permeability + + # Rational arithmetic keeps the decimal definition exact. + @test vacuum_permittivity(Rational{BigInt}) == 88541878128 // big(10)^22 + for T in (Float32, Float64, BigFloat) + @test vacuum_permittivity(T) isa T + @test vacuum_permeability(T) isa T + end + @test vacuum_permeability(BigFloat) ≈ 4 * big(π) / big(10)^7 rtol = 4eps(BigFloat) + # The Float64 evaluation of 88541878128 × 10⁻²² is one ulp below the decimal literal. + @test vacuum_permittivity(Float64) == prevfloat(8.8541878128e-12) + @test vacuum_permeability(Float64) == 4π * 1e-7 + @test inv(sqrt(vacuum_permittivity(BigFloat) * vacuum_permeability(BigFloat))) ≈ + 299792458 rtol = 1e-9 +end diff --git a/test/unit/commons/formulas.jl b/test/unit/commons/formulas.jl new file mode 100644 index 000000000..3f02aed2b --- /dev/null +++ b/test/unit/commons/formulas.jl @@ -0,0 +1,76 @@ +@testitem "Commons / formula options / one option projection for every formula family" tags=[:unit, :engine] begin + using LineCableModels.Commons: formulation_options, Expression + const E = LineCableModels.Engine + const EH = LineCableModels.Earth.EquivalentHomogeneous + soil = E.EarthPair(1, 2, (-1.0, -2.0), 1.0, (2, 2)) + self = E.EarthPair(1, 1, (-1.0, -1.0), 0.0, (2, 2); radius = 0.01) + + # One entry per distinct expression, in order of first appearance. Equal expressions + # share their options. + selected = E.EarthImpedance.Formula(:default; + options = (integration = (method = :quad, options = (;)),)) + expressions = [Expression(selected, pair) for pair in (self, soil, self)] + projected = formulation_options(selected, expressions) + @test projected.expressions == unique(expressions) + @test map(expression -> first(expression.arguments), projected.expressions) == + [Val(:self), Val(:mutual)] + for (expression, options) in zip(projected.expressions, projected.options) + @test options.data.integration.method === Val(:quad) + @test keys(options.data) == keys(formulation_options(expression).data) + end + + # An unused supplied section raises an error with the formula identifier. + unused = E.EarthImpedance.Formula(:default; options = (unknown = 1,)) + @test_throws "unused formulation options (:unknown,) for :$(formula_id(unused))" formulation_options( + unused, (Expression(unused, soil),)) + internal = E.InternalImpedance.Formula(:default) + @test_throws "unused formulation options (:unknown,) for :$(formula_id(internal))" E.InternalImpedance.Formula( + :default; options = (unknown = 1,)) + + # An equivalent-earth reduction declares its equivalent_material expression for a pair. + rule = EH.Formula(:bottommost) + reduced = Expression(rule, soil) + @test reduced.method === EH.equivalent_material + @test reduced.arguments == (Val(:mutual), Val(2), Val(2)) + @test only(formulation_options(rule, (reduced,)).options) == formulation_options(reduced) +end + +@testitem "Commons / Functor / one evaluation point of a formula" tags=[:unit, :engine] begin + using LineCableModels.Commons: Functor, Expression, formulation_options, AbstractFormulation + const E = LineCableModels.Engine + # Without a method of its own, a formula does not share values, and its state is empty. + struct PlainFormula <: AbstractFormulation end + plain = Functor(PlainFormula(), (; value = 1.0)) + @test plain.formula === PlainFormula() && plain.input.value == 1.0 + @test isempty(plain.state) + selected = E.EarthImpedance.Formula(:wedepohl1973) + rho, epsilon, mu = [Inf, 100.0], 8.8541878128e-12 .* [1, 10], fill(4pi*1e-7, 2) + jω = complex(0.0, 100pi) + pair = E.EarthPair(1, 2, (-1.0, -2.0), 0.75, (2, 2)) + expression = Expression(selected, pair) + options = only(formulation_options(selected, (expression,)).options) + # The earth formula's Functor of one pair, built as the standalone call builds it. + functor = Functor(selected, (; jω, thickness = nothing, rho, epsilon, mu, options), (;)) + @test functor.formula === selected && functor.input.rho === rho + # A conductor pair extends the input and keeps the formula and the state. + point = Functor(functor, (; pair, physical = pair)) + @test point.formula === selected && point.state === functor.state + @test point.input.pair === pair && point.input.rho === rho + # The expression evaluates at the point, as the standalone call does. + @test expression(point, nothing) == selected(rho, epsilon, mu, jω, pair) + # The existence check matches the Functor and the workspace that the evaluation passes. + @test validate(expression) === expression + @test_throws ArgumentError validate( + Expression(selected, E.EarthPair(1, 2, (10.0, 12.0), 0.75, (1, 1)))) +end + +@testitem "Commons / formula registry / a family lists its identifiers by its Formula type" tags=[:unit, :engine] begin + using LineCableModels.Commons: formulas + const EI = LineCableModels.Engine.EarthImpedance + registered = formulas(EI.Formula) + @test registered isa Tuple{Vararg{Symbol}} + @test :default in registered && :unified in registered + # Any member type of the family lists the same identifiers. + @test formulas(typeof(EI.Formula(:unified))) === registered + @test_throws MethodError formulas(LineParametersFormulation) +end diff --git a/test/unit/commons/gridspace.jl b/test/unit/commons/gridspace.jl new file mode 100644 index 000000000..7567e6d44 --- /dev/null +++ b/test/unit/commons/gridspace.jl @@ -0,0 +1,281 @@ +@testitem "Grid / finite sources" tags=[:unit, :commons] setup=[ + UseEngineSupport] begin + using Random + import LineCableModels.ParametricBuilder as PB + + deterministic=PB.Grid((1, 3, 2)) + @test PB.Grid(deterministic) === deterministic + @test collect(PB.Grid(1:3)) == [1, 2, 3] + @test deterministic[2] == 3 + @test extrema(deterministic) == (1, 3) + @test size(deterministic) == (3,) + + relative=PB.Grid((-10.0, 20.0), (5.0, 10.0)) + absolute=PB.Grid((10.0, 20.0), PB.AbsoluteError((0.5, 2.0))) + @test length(relative) == 4 + @test relative[4] isa PB.UncertainValue + @test extrema(relative) == (-11.0, 22.0) + @test extrema(absolute) == (8.0, 22.0) + @test_throws BoundsError relative[0] + @test_throws BoundsError absolute[5] + + zero_sigma=PB.UncertainValue(4, 0) + @test rand(MersenneTwister(1), zero_sigma) === 4.0 + uncertain=PB.UncertainValue(10.0, 2.0) + @test isfinite(rand(MersenneTwister(10), uncertain; distribution = :normal)) + @test abs(rand(MersenneTwister(10), uncertain; distribution = :uniform) - 10.0) <= + 2sqrt(3.0) + @test rand( + MersenneTwister(10), uncertain; + distribution = (_rng, nominal, sigma)->nominal+3sigma + ) == 16.0 + @test_throws ArgumentError rand( + MersenneTwister(1), uncertain; distribution = :cauchy) + @test isfinite(rand(MersenneTwister(1), relative)) + @test rand(MersenneTwister(1), PB.Grid(3.0)) == 3.0 + @test rand(MersenneTwister(1), PB.Grid((:a, :b, :c))) in (:a, :b, :c) + @test rand(PB.Grid(:fixed)) === :fixed + + @test_throws ArgumentError PB.Grid(1.0, -1.0) + @test_throws ArgumentError PB.Grid(Inf, 1.0) + @test_throws ArgumentError PB.Grid(1.0, PB.AbsoluteError(-1.0)) + @test_throws ArgumentError PB.Grid(:symbol, 1.0) + @test_throws ArgumentError PB.Grid(:symbol, PB.AbsoluteError(1.0)) +end + +@testitem "Gridspace / product and zip" tags=[:unit, :commons] setup=[ + UseEngineSupport] begin + using Random + import LineCableModels.ParametricBuilder as PB + + product_space=PB.Gridspace{Tuple}( + tuple, + (PB.Grid((1, 2, 3)), PB.Grid((10, 20))) + ) + @test (@inferred collect(product_space)) == [ + (1, 10), (2, 10), (3, 10), + (1, 20), (2, 20), (3, 20) + ] + @test length(product_space) == 6 + @test size(product_space) == (6,) + @test eltype(product_space) === Tuple{Int, Int} + @test Base.IteratorEltype(typeof(product_space)) isa Base.HasEltype + @test isconcretetype(eltype(product_space)) + @test PB.Grid(product_space) === product_space + + zip_space=PB.Gridspace{Tuple}( + tuple, + (PB.Grid((1, 2, 3)), PB.Grid((10, 20, 30)), PB.Grid(:fixed)); + combine = :zip + ) + @test collect(zip_space) == [ + (1, 10, :fixed), + (2, 20, :fixed), + (3, 30, :fixed) + ] + @test length(zip_space) == 3 + @test_throws DimensionMismatch PB.Gridspace{Tuple}( + tuple, + (PB.Grid((1, 2)), PB.Grid((10, 20, 30))); + combine = :zip + ) + + child=PB.Gridspace{Tuple}( + tuple, + (PB.Grid((1, 2)), PB.Grid((10, 20))); + combine = :zip + ) + parent=PB.Gridspace{Tuple}( + tuple, + (child, PB.Grid((:a, :b))) + ) + @test collect(parent) == [ + ((1, 10), :a), + ((2, 20), :a), + ((1, 10), :b), + ((2, 20), :b) + ] + @test collect(PB.Gridspace{Tuple}( + tuple, + (child, PB.Grid((:a, :b))); + combine = :zip + )) == [((1, 10), :a), ((2, 20), :b)] + + zero_argument_product = PB.Gridspace{Tuple}(tuple, ()) + @test (@inferred LineCableModels._combinations(zero_argument_product)) == ((),) + @test (@inferred collect(zero_argument_product)) == [()] + zero_argument_zip = PB.Gridspace{Tuple}(tuple, (); combine=:zip) + @test (@inferred LineCableModels._combinations(zero_argument_zip)) == ((),) + @test (@inferred collect(zero_argument_zip)) == [()] + @test (@inferred collect(zip_space)) == [(1, 10, :fixed), (2, 20, :fixed), (3, 30, :fixed)] + @test (@inferred collect(child)) == [(1, 10), (2, 20)] + @test isempty(PB.Gridspace{Tuple}(tuple, (PB.Grid(()),))) + empty_space=PB.Gridspace{Tuple}(tuple, (PB.Grid(()),)) + @test eltype(empty_space) === Any + @test Base.IteratorEltype(typeof(empty_space)) isa Base.EltypeUnknown + @test_throws ArgumentError PB.Gridspace{Tuple}(tuple, (1, 2)) + @test_throws ArgumentError PB.Gridspace{String}( + identity, (PB.Grid((1, 2)),) + ) + @test_throws ArgumentError PB.Gridspace{Tuple}( + tuple, (PB.Grid(1),); combine = :outer) + @test !applicable(getindex, product_space, 1) + @test rand(MersenneTwister(1), product_space) in collect(product_space) + @test rand(product_space) in collect(product_space) + zipped_draws=Set( + rand(MersenneTwister(seed), zip_space) for seed in 1:20 + ) + @test zipped_draws ⊆ Set(collect(zip_space)) + @test_throws ArgumentError rand( + MersenneTwister(1), + PB.Gridspace{Tuple}(tuple, (PB.Grid(()),)) + ) +end + +@testitem "Gridspace / composition conformance" tags=[:unit, :commons] setup=[ + UseEngineSupport] begin + using Random + import LineCableModels.ParametricBuilder as PB + + mutable struct CountingGrid{V <: Tuple}<:PB.AbstractGrid + vals::V + iterate_calls::Int + length_calls::Int + end + CountingGrid(values::Tuple)=CountingGrid(values, 0, 0) + function Base.iterate(grid::CountingGrid, state...) + grid.iterate_calls+=1 + return iterate(grid.vals, state...) + end + function Base.length(grid::CountingGrid) + grid.length_calls+=1 + return length(grid.vals) + end + + product_left=PB.Grid((1, 2, 3)) + product_right=PB.Grid((10, 20)) + product=PB.Gridspace{Tuple}(tuple, (product_left, product_right)) + @test collect(product) == [ + (1, 10), (2, 10), (3, 10), + (1, 20), (2, 20), (3, 20) + ] + + reused=PB.Grid((1, 2)) + reused_product=PB.Gridspace{Tuple}(tuple, (reused, reused)) + separate_product=PB.Gridspace{Tuple}( + tuple, + (PB.Grid((1, 2)), PB.Grid((1, 2))) + ) + @test collect(reused_product) == [(1, 1), (2, 1), (1, 2), (2, 2)] + @test collect(reused_product) == collect(separate_product) + @test collect(PB.Gridspace{Tuple}( + tuple, + (reused, reused); + combine = :zip + )) == [(1, 1), (2, 2)] + + nested_zip=PB.Gridspace{Tuple}( + tuple, + ( + PB.Gridspace{Tuple}( + tuple, + (PB.Grid((1, 2, 3)), PB.Grid((10, 20, 30))); + combine = :zip + ), + PB.Grid(:fixed) + ); + combine = :zip + ) + @test collect(nested_zip) == [ + ((1, 10), :fixed), + ((2, 20), :fixed), + ((3, 30), :fixed) + ] + + left=CountingGrid(Tuple(1:2_000)) + right=CountingGrid(Tuple(2_001:4_000)) + counted_product=PB.Gridspace{Tuple}(tuple, (left, right)) + @test eltype(counted_product) === Any + @test Base.IteratorEltype(typeof(counted_product)) isa Base.EltypeUnknown + @test length(counted_product) == 4_000_000 + @test left.iterate_calls == 0 + @test right.iterate_calls == 0 + + zipped=PB.Gridspace{Tuple}(tuple, (left, right); combine = :zip) + left.iterate_calls=0 + right.iterate_calls=0 + @test length(zipped) == 2_000 + @test left.iterate_calls == 0 + @test right.iterate_calls == 0 + @test length(collect(zipped)) == 2_000 + @test 2_000 <= left.iterate_calls <= 2_001 + @test 2_000 <= right.iterate_calls <= 2_001 + + materializations=Ref(0) + build=(values...)->(materializations[]+=1; values) + @test_throws DimensionMismatch PB.Gridspace{Tuple}( + build, + (PB.Grid((1, 2)), PB.Grid((10, 20, 30))); + combine = :zip + ) + @test materializations[] == 0 + + @test_throws ArgumentError PB.Gridspace{Tuple}(tuple, ((1, 2),)) + @test !applicable(getindex, product, 1) + @test rand(Random.Xoshiro(0x1234), product) in collect(product) +end + +@testitem "Gridspace / inference and allocation checks" tags=[:unit, :commons] setup=[ + UseEngineSupport] begin + using Random + import LineCableModels.ParametricBuilder as PB + + struct ScalarTarget + value::Float64 + end + struct BuildScalarTarget end + (::BuildScalarTarget)(value)=ScalarTarget(value) + + function deterministic_sum(space, repetitions) + total=0.0 + for _ in 1:repetitions, value in space + + total+=value.value + end + return total + end + + function realization_sum(rng, point, repetitions) + total=0.0 + for _ in 1:repetitions + total+=LineCableModels.realize(rng, point, :normal).value + end + return total + end + + deterministic=PB.Gridspace{ScalarTarget}( + BuildScalarTarget(), + (PB.Grid((1.0, 2.0, 3.0)),) + ) + deterministic_point=first(PB.points(deterministic)) + @test eltype(deterministic) === ScalarTarget + @test Base.IteratorEltype(typeof(deterministic)) isa Base.HasEltype + @test @inferred(first(deterministic)) == ScalarTarget(1.0) + @test @inferred(PB.materialize(deterministic_point)) == ScalarTarget(1.0) + + uncertain=PB.Gridspace{ScalarTarget}( + BuildScalarTarget(), + (PB.Grid(1.0, PB.AbsoluteError(0.1)),) + ) + uncertain_point=first(PB.points(uncertain)) + @test eltype(uncertain) === Any + @test Base.IteratorEltype(typeof(uncertain)) isa Base.EltypeUnknown + rng=Random.Xoshiro(0x1234) + @test @inferred(LineCableModels.realize(rng, uncertain_point, :normal)) isa ScalarTarget + + deterministic_sum(deterministic, 1) + realization_sum(rng, uncertain_point, 1) + @test @allocated(deterministic_sum(deterministic, 10_000)) == 0 + rng=Random.Xoshiro(0x1234) + @test @allocated(realization_sum(rng, uncertain_point, 10_000)) == 0 +end diff --git a/test/unit/commons/logging.jl b/test/unit/commons/logging.jl new file mode 100644 index 000000000..e0f95f700 --- /dev/null +++ b/test/unit/commons/logging.jl @@ -0,0 +1,65 @@ +@testitem "Commons / logging / verbosity respects module ancestry and caller logging controls" tags=[:unit, :commons] begin + using Logging + const LCM = LineCableModels + @test LCM.verbosity((default = 0, progress = 1, Engine = 2)) == + (default = 0, progress = 1, Engine = 2) + for invalid in ((progress = 1,), (default = 3,), (default = 1.5,), 1) + @test_throws ArgumentError LCM.verbosity(invalid) + end + log = Test.TestLogger(min_level = Logging.Debug) + with_logger(LCM.VerbosityLogger(log, ( + default = 0, progress = 1, Engine = 2, EarthAdmittance = 0))) do + @info "progress" _group=:progress + @info "quiet" + @debug "ancestor" _module=LCM.Engine.EarthImpedance + @info "engine quiet" _module=LCM.Engine.EarthAdmittance + @warn "warning" + @info "module-less quiet" _module=nothing + @warn "module-less warning" _module=nothing + @info "module-less progress" _module=nothing _group=:progress + end + @test getproperty.(log.logs, :message) == ["progress", "ancestor", "warning", + "module-less warning", "module-less progress"] + # This parent deliberately leaves level filtering to min_enabled_level. + struct ThresholdLogger <: AbstractLogger + records::Vector{String} + end + Logging.min_enabled_level(::ThresholdLogger) = Logging.Warn + Logging.catch_exceptions(::ThresholdLogger) = false + Logging.shouldlog(::ThresholdLogger, level, source, group, id) = group !== :excluded + Logging.handle_message(logger::ThresholdLogger, level, message, args...; + kwargs...) = push!(logger.records, string(message)) + parent = ThresholdLogger(String[]) + filter = LCM.VerbosityLogger(parent, (default = 2, progress = 2)) + @test !Logging.catch_exceptions(filter) + @test !Logging.shouldlog(filter, Logging.Info, @__MODULE__, :progress, :test) + with_logger(filter) do + @info "blocked by minimum" _group=:progress + @warn "blocked by predicate" _group=:excluded + @warn "accepted" + end + @test parent.records == ["accepted"] + evaluations = Ref(0) + with_logger(LCM.VerbosityLogger(NullLogger(), (default = 2, progress = 2))) do + @info "discarded" expensive=(evaluations[]+=1) + end + @test evaluations[] == 0 + @test_throws ErrorException with_logger(filter) do + @warn error("caller exception marker") + end +end + +@testitem "Commons / logging / FEM file destination does not bypass the parent threshold" tags=[:extension, :fem] begin + using Gmsh, Logging + const FEM = Base.get_extension(LineCableModels, :LineCableModelsGmshExt) + parent = Test.TestLogger(min_level = Logging.Warn, respect_maxlog = false) + file = Test.TestLogger(min_level = Logging.Debug, respect_maxlog = false) + filter = LineCableModels.VerbosityLogger(parent, (default = 2, progress = 1)) + with_logger(FEM.FEMTeeLogger(filter, file)) do + @info "file only" _group=:progress + @debug "file debug" + @warn "both" + end + @test getproperty.(parent.logs, :message) == ["both"] + @test getproperty.(file.logs, :message) == ["file only", "file debug", "both"] +end diff --git a/test/unit/commons/matrixops.jl b/test/unit/commons/matrixops.jl new file mode 100644 index 000000000..9589c1195 --- /dev/null +++ b/test/unit/commons/matrixops.jl @@ -0,0 +1,191 @@ +@testitem "Commons / matrix reductions / reorder, Kron, bundle and transposition invariants" tags=[:unit, :commons] setup=[ + UseEngineSupport, + TestNumerics +] begin + using LinearAlgebra + const Commons=LineCableModels.Commons + + phase_map=[2, 0, 1, 2, 0, 1] + @test Commons.reorder_indices(phase_map) == [1, 3, 4, 6, 2, 5] + matrix=ComplexF64[4 1 2; 1 5 3; 2 3 8] + reduction_map=[1, 2, 0] + expected=matrix[1:2, 1:2]-matrix[1:2, 3:3]* + inv(matrix[3:3, 3:3])*matrix[3:3, 1:2] + @test TestNumerics.isapprox_scaled(kron_reduce(matrix, reduction_map), expected) + destination=zeros(ComplexF64, 2, 2) + @test Commons.kron_reduce!(matrix, reduction_map, destination) === nothing + @test TestNumerics.isapprox_scaled(destination, expected) + + # Complex asymmetric entries and ordered, noncontiguous indices expose + # accidental conjugation, symmetry assumptions, and reordered terminals. + for T in (Float32, Float64, BigFloat) + ordered = Complex{T}[8+im 1-2im 2+im 3; 2+3im 9-im 1 2im; + 1 3+im 10+2im 2; 4im 1-im 3+im 11] + keep, eliminate = [4, 1], [3, 2] + reduced = zeros(Complex{T}, 2, 2) + actual = Commons.kron_reduce!(ordered, keep, eliminate, reduced, + similar(reduced), similar(reduced), similar(reduced)) + expected_ordered = ordered[keep, keep] - ordered[keep, eliminate] * + (ordered[eliminate, eliminate] \ ordered[eliminate, keep]) + @test actual === reduced + @test actual ≈ expected_ordered + @test kron_reduce(ordered, [2, 0, 3, 0]) ≈ ordered[[1, 3], [1, 3]] - + ordered[[1, 3], [2, 4]] * (ordered[[2, 4], [2, 4]] \ ordered[[2, 4], [1, 3]]) + @test kron_reduce(ordered, [1, 2, 3, 4]) == ordered + aliased = copy(ordered) + @test Commons.kron_reduce!(aliased, [1, 2, 3, 4], aliased) === nothing + @test aliased == ordered + end + @test_throws SingularException kron_reduce(ComplexF64[1 2; 3 0], [1, 0]) + @test_throws DimensionMismatch kron_reduce(matrix, [1, 0]) + + bundled, merged_map=Commons.merge_bundles!(copy(matrix), [1, 1, 0]) + @test merged_map == [1, 0, 0] + change_of_basis=Matrix{ComplexF64}(I, 3, 3) + change_of_basis[1, 2]=-1 + @test bundled == transpose(change_of_basis) * matrix * change_of_basis + + unconnected=ComplexF64[4 1 2; 1 5 3; 2 3 8] + unchanged, unconnected_map=Commons.merge_bundles!(copy(unconnected), [0, 0, 1]) + @test unchanged == unconnected + @test unconnected_map == [0, 0, 1] + + mixed=ComplexF64[6 1 2 3; 1 7 4 5; 2 4 8 6; 3 5 6 9] + mixed_basis=Matrix{ComplexF64}(I, 4, 4) + mixed_basis[1, 2]=-1 + mixed_result, mixed_map=Commons.merge_bundles!(copy(mixed), [2, 2, 0, 0]) + @test mixed_result == transpose(mixed_basis)*mixed*mixed_basis + @test mixed_map == [2, 0, 0, 0] + @test_throws ArgumentError Commons.merge_bundles!(ones(2, 3), [1, 1]) + @test Commons.bundle_operations([2, 0, 1, 2, 1, 2]) == [(1, 4), (3, 5), (1, 6)] + + # Each option combination fixes the retained rows of the reordered terminals + # [1, 3, 4, 6, 2, 5] with phases [2, 1, 2, 1, 0, 0]. + for (reduce_bundle, kron_reduction, keep, eliminate, retained, bundles) in ( + (true, true, [1, 2], [3, 4, 5, 6], [2, 1], [(1, 3), (2, 4)]), + (true, false, [1, 2, 5, 6], [3, 4], [2, 1, -1, -1], [(1, 3), (2, 4)]), + (false, true, [1, 2, 3, 4], [5, 6], [2, 1, 2, 1], Tuple{Int, Int}[]), + (false, false, collect(1:6), Int[], [2, 1, 2, 1, 0, 0], Tuple{Int, Int}[])) + plan=Commons.ReductionPlan(phase_map; reduce_bundle, kron_reduction, + ideal_transposition = false) + @test plan.permutation == [1, 3, 4, 6, 2, 5] + @test (plan.keep, plan.eliminate, plan.phase_map, plan.bundles) == + (keep, eliminate, retained, bundles) + @test plan.indices == plan.permutation[keep] + buffers=Commons.ReductionBuffers{ComplexF64}(plan) + @test size(buffers.potential) == (length(keep), length(keep)) + @test size(buffers.factor) == (length(eliminate), length(eliminate)) + end + + matrix=[Float64(2i+3j+i*j) for i in 1:3, j in 1:3] + circulant=copy(matrix) + @test Commons.ideal_transposition!(circulant) === circulant + @test all(circulant[i, j] == circulant[mod1(i + 1, 3), mod1(j + 1, 3)] + for i in 1:3, j in 1:3) + @test sum(circulant) ≈ sum(matrix) + @test_throws DimensionMismatch Commons.ideal_transposition!(ones(2, 3)) +end + +@testitem "Commons / matrix reductions / passive network constrained solves" tags=[:unit, :commons] begin + using LinearAlgebra + const Commons=LineCableModels.Commons + incidence=[1.0 0 0 1 1 0;0 1 0 -1 0 1;0 0 1 0 -1 -1] + for f in (10.0,100.0,1000.0) + s=2pi*im*f + r=collect(1:6).*1e-3;l=collect(7:12).*1e-6 + g=collect(1:2:11).*1e-9;c=collect(2:2:12).*1e-10 + z=inv(incidence*Diagonal(inv.(r.+s.*l))*transpose(incidence)) + y=incidence*Diagonal(g.+s.*c)*transpose(incidence) + p=s*inv(y) + # Set the eliminated conductor's voltage or potential to zero in the + # original equations. Solve for the constrained currents or charges + # without constructing a Schur complement. + for matrix in (z,p) + excitation=Matrix{ComplexF64}(I,3,3)[:,1:2] + currents=matrix\excitation + expected=inv(currents[1:2,:]) + @test kron_reduce(matrix,[1,2,0]) ≈ expected rtol=1e-10 atol=0 + end + function reduced(phase_map; reduce_bundle) + plan=Commons.ReductionPlan(phase_map; reduce_bundle, kron_reduction=true, + ideal_transposition=false) + Zr=zeros(ComplexF64,2,2);Yr=similar(Zr) + Commons.reduce_line_matrices!(Zr,Yr,z,p,s,plan,Commons.ReductionBuffers{ComplexF64}(plan)) + return Zr,Yr + end + Zr,Yr=reduced([1,2,0];reduce_bundle=false) + @test Zr ≈ inv((z\Matrix{ComplexF64}(I,3,3)[:,1:2])[1:2,:]) rtol=1e-10 + @test Yr ≈ s*((p\Matrix{ComplexF64}(I,3,3)[:,1:2])[1:2,:]) rtol=1e-10 + Zb,Yb=reduced([1,1,2];reduce_bundle=true) + equal_potentials=[1.0 0;1 0;0 1] + @test Zb ≈ inv(transpose(equal_potentials)*(z\equal_potentials)) rtol=1e-10 + @test Yb ≈ s*transpose(equal_potentials)*(p\equal_potentials) rtol=1e-10 + end +end + +@testitem "Commons / matrix reductions / admittance inversion diagnostics" tags=[:unit, :commons] begin + using LinearAlgebra + const Commons=LineCableModels.Commons + plan=Commons.ReductionPlan([1, 2]; reduce_bundle=false, kron_reduction=false, + ideal_transposition=false) + buffers=Commons.ReductionBuffers{ComplexF64}(plan) + Zr, Yr=zeros(ComplexF64, 2, 2), zeros(ComplexF64, 2, 2) + s=2pi*50im + for potential in (ComplexF64[2.0 0.25; 0.25 1.5], ComplexF64[3.0+0.1im 0.5; 0.5 2.0+0.2im]) + record=Commons.reduce_line_matrices!(Zr, Yr, potential, potential, s, plan, buffers, + Val(true)) + @test Zr == potential + @test potential * Yr ≈ s * I + @test record.residual ≤ 100eps(Float64) + @test Commons.reduce_line_matrices!(Zr, Yr, potential, potential, s, plan, + buffers) === nothing + @test potential * Yr ≈ s * I + end + # An overflowing condition estimate is not a failed solve. The physical + # inverse is finite and remains the result of the original LU operation. + ill_conditioned=ComplexF64[1e-200 0; 0 1e200] + record=@test_logs (:warn, r"condition estimate is not finite") begin + Commons.reduce_line_matrices!(Zr, Yr, ill_conditioned, ill_conditioned, one(s), plan, + buffers, Val(true)) + end + @test Yr == inv(ill_conditioned) + @test isinf(record.condition_number) + @test record.residual ≤ 100eps(Float64) + @test_throws ArgumentError Commons.reduce_line_matrices!(Zr, Yr, ill_conditioned, + fill(ComplexF64(NaN), 2, 2), s, plan, buffers, Val(true)) + @test_throws SingularException Commons.reduce_line_matrices!(Zr, Yr, ill_conditioned, + zeros(ComplexF64, 2, 2), s, plan, buffers, Val(true)) +end + +@testitem "Commons / matrix reductions / ideal transposition averages P before inversion" tags=[:unit, :commons] begin + using LinearAlgebra + const Commons=LineCableModels.Commons + # Symmetric and not circulant. Its cyclic diagonals have the means 5, 2 and 2. + P=ComplexF64[6 2 1; 2 5 3; 1 3 4] + averaged=ComplexF64[5 2 2; 2 5 2; 2 2 5] + s=2pi*50im + plan=Commons.ReductionPlan([1, 2, 3]; reduce_bundle=false, kron_reduction=false, + ideal_transposition=true) + Zr, Yr=zeros(ComplexF64, 3, 3), zeros(ComplexF64, 3, 3) + Commons.reduce_line_matrices!(Zr, Yr, P, P, s, plan, Commons.ReductionBuffers{ComplexF64}(plan)) + @test Zr == averaged + # The inverse of `averaged` is `[7 -2 -2; -2 7 -2; -2 -2 7] / 27`. + @test Yr ≈ s*ComplexF64[7 -2 -2; -2 7 -2; -2 -2 7]/27 + @test Yr ≈ s*inv(averaged) + # Averaging Y after the inversion is the convention this test rejects. + @test !isapprox(Yr, Commons.ideal_transposition!(s*inv(P)); rtol=1e-2) +end + +@testitem "Commons / matrix reductions / buffers extend the calculation record" tags=[:unit, :commons] begin + const Commons=LineCableModels.Commons + plan=Commons.ReductionPlan([1, 2, 2, 0]; reduce_bundle=true, kron_reduction=true, + ideal_transposition=false) + existing=(destination = zeros(ComplexF64, 2, 2),) + buffers=Commons.initialize_buffers(plan, ComplexF64, (;), (;), existing) + @test keys(buffers) == (:destination, :reduction) + @test buffers.destination === existing.destination + @test buffers.reduction isa Commons.ReductionBuffers{ComplexF64} + @test size(buffers.reduction.ordered) == (4, 4) + @test size(buffers.reduction.potential) == (length(plan.keep), length(plan.keep)) + @test size(buffers.reduction.factor) == (length(plan.eliminate), length(plan.eliminate)) +end diff --git a/test/unit/commons/observation_quantity.jl b/test/unit/commons/observation_quantity.jl new file mode 100644 index 000000000..79eb43f18 --- /dev/null +++ b/test/unit/commons/observation_quantity.jl @@ -0,0 +1,78 @@ +@testitem "Commons / fallback observations preserve inferred values and scalar counts" tags=[:unit, :commons] begin + const G = LineCableModels.Commons + struct ObservationSource{T} + values::T + end + G.observe(source::ObservationSource, ::typeof(R)) = source.values + G.observe(source::ObservationSource, ::typeof(R), index::Int) = source.values[index] + G.basis(::ObservationSource) = :pul + + for value in (Float32(1), 1.0, big"1.0", missing, [1.0, 2.0]) + source = ObservationSource(value) + product = @inferred G.observation_quantity(source, R) + @test (@inferred G._validate_observed_quantity(product)) === nothing + @test product.coordinates.extent == (value isa AbstractArray ? size(value) : ()) + @test typeof(product.values) === typeof(value) + value isa AbstractArray && @test product.values !== value + expected = G.detach(value, LineCableModels.Units.scale_factor( + LineCableModels.Units.native_unit(product.quantity, :pul), product.unit)) + @test isequal(product.values, expected) + malformed = merge(product, (coordinates=merge(product.coordinates, (extent=(3,),)),)) + @test_throws DimensionMismatch G._validate_observed_quantity(malformed) + end + # Native map may narrow a union element type based on the actual values. + # Preserve missing values without requiring one concrete array return type. + mixed = G.observation_quantity(ObservationSource([missing, 2.0]), R) + @test ismissing(first(mixed.values)) + @test mixed.values[2] == G.observation_quantity(ObservationSource(2.0), R).values + @test (@inferred G._validate_observed_quantity(mixed)) === nothing + source = ObservationSource([1.0, 2.0]) + selected = @inferred G.observation_quantity(source, (R, 2)) + scalar = @inferred G.observation_quantity(ObservationSource(2.0), R) + @test selected.values == scalar.values + @test selected.coordinates.indices == (2,) + @test (@inferred G._validate_observed_quantity(selected)) === nothing + for request in (:R, [R], nothing) + @test_throws ArgumentError G.observation_quantity(source, request) + end + @test_throws MethodError G.observation_quantity(source, ()) + @test_throws BoundsError G.observation_quantity(source, (R, 3)) +end + +@testitem "Commons / detached thresholds retain precision and uncertainty dependencies" tags=[:unit, :measurements] begin + using Measurements + const G = LineCableModels.Commons + shared = measurement(0.5, 0.01) + cutoffs = (real=Float32[0.25, 0.5], imaginary=(value=shared, absent=nothing), + unavailable=missing, complex=ComplexF32(0.25, 0.5)) + detached = @inferred G.detach(cutoffs, 1000.0) + @test detached.real == Float32[250, 500] + @test eltype(detached.real) === Float32 + @test detached.real !== cutoffs.real + @test detached.complex === ComplexF32(250, 500) + @test detached.imaginary.absent === nothing + @test detached.unavailable === missing + @test uncertainty(detached.imaginary.value - 1000shared) == 0 + detached.real[1] = 0 + @test cutoffs.real[1] == 0.25f0 +end + +@testitem "Commons / observation equality retains uncertainty identity and collection shape" tags=[:unit, :measurements] begin + using Measurements + const G = LineCableModels.Commons + shared = measurement(1.0, 0.1) + independent = measurement(1.0, 0.1) + @test isequal(shared, independent) + @test (@inferred G._same_observed_values(shared, shared)) + @test !(@inferred G._same_observed_values(shared, independent)) + for wrap in (identity, x -> [x], x -> (x, missing), x -> (value=x, absent=nothing), + x -> Union{Missing,typeof(x)}[missing, x]) + @test G._same_observed_values(wrap(shared), wrap(shared)) + @test !G._same_observed_values(wrap(shared), wrap(independent)) + end + @test G._same_observed_values([NaN, -0.0], [NaN, -0.0]) + @test !G._same_observed_values([0.0], [-0.0]) + @test !G._same_observed_values([1.0, 2.0], reshape([1.0, 2.0], 1, 2)) + @test !G._same_observed_values((x=shared,), (y=shared,)) + @test !G._same_observed_values((shared,), (shared, shared)) +end diff --git a/test/unit/commons/owned_records.jl b/test/unit/commons/owned_records.jl new file mode 100644 index 000000000..37cd652d5 --- /dev/null +++ b/test/unit/commons/owned_records.jl @@ -0,0 +1,127 @@ +@testitem "Commons / owned records / payload identity and value semantics" tags=[:unit, :commons] begin + payload = (rtol=1e-8, nested=(enabled=true,), values=[1.0, 2.0], data=:field) + @test (@inferred FormulationOptions(payload)).data === payload + @test (@inferred ComputationOptions(payload)).data === payload + @test (@inferred ComputationDetails(payload)).data === payload + @test FormulationOptions !== ComputationOptions !== ComputationDetails + @test LineCableModels.FormulaDefinition === LineCableModels.Commons.FormulaDefinition + @test LineCableModels.Expression === LineCableModels.Commons.Expression + + for Record in (FormulationOptions, ComputationOptions, ComputationDetails) + value = Record(payload) + @test !(value isa NamedTuple) + @test fieldtype(typeof(value), :data) === typeof(payload) + @test value.data === payload + @test value.data.values === payload.values + @test value.data.data === :field + @test Record(; payload...).data === payload + @test Record().data === NamedTuple() + @test Record((other=3,)).data === (other=3,) + @test_throws MethodError Record(Dict(:rtol=>1e-8)) + @test_throws MethodError Record((1, 2)) + @test_throws MethodError Record(3) + @test_throws MethodError Record{NamedTuple}(payload) + @test_throws MethodError Record(value) + for Other in (FormulationOptions, ComputationOptions, ComputationDetails) + Other === Record && continue + @test_throws MethodError Record(Other(payload)) + @test value != Other(payload) + @test !isequal(value, Other(payload)) + end + @test value != payload + @test !isequal(value, payload) + for (a, b) in (((values=[1, 2],), (values=[1.0, 2.0],)), + ((x=NaN,), (x=NaN,)), ((x=0.0,), (x=-0.0,)), + ((x=missing,), (x=missing,))) + @test isequal(Record(a) == Record(b), a == b) + @test isequal(Record(a), Record(b)) == isequal(a, b) + isequal(a, b) && @test hash(Record(a)) == hash(Record(b)) + end + bounded = NamedTuple{(:shunt_model,), Tuple{NamedTuple}}(((iterations=4,),)) + @test fieldtype(typeof(Record(bounded).data), :shunt_model) === NamedTuple + @test occursin(string(nameof(Record)), sprint(show, value)) + end + + retained = ComputationDetails(payload) + payload.values[1] = 7.0 + @test retained.data.values[1] == 7.0 +end + +@testitem "Commons / owned records / owner-specific option normalization" tags=[:unit, :engine] begin + using LineCableModels.Commons: formulation_options, computation_options + resolved = @inferred formulation_options(LineParametersFormulation, FormulationOptions()) + @test resolved isa FormulationOptions + @test resolved.data == (reduce_bundle=true, kron_reduction=true, ideal_transposition=false) + execution = @inferred computation_options(LineCableModelsCoaxial, ComputationOptions()) + @test execution isa ComputationOptions + @test execution.data.trace === Val(false) + @test execution.data.output_basis === Val(:pul) + @test_throws MethodError formulation_options(LineParametersFormulation, (;)) + @test_throws MethodError formulation_options(LineParametersFormulation, ComputationOptions()) + @test_throws MethodError computation_options(LineCableModelsCoaxial, (;)) + @test_throws MethodError computation_options(LineCableModelsCoaxial, FormulationOptions()) + @test_throws ArgumentError formulation_options(LineParametersFormulation, FormulationOptions(unknown=true)) + @test_throws ArgumentError computation_options(LineCableModelsCoaxial, ComputationOptions(trace="yes")) + @test_throws ArgumentError computation_options(LineCableModelsCoaxial, ComputationOptions(ui=true)) + @test_throws TypeError formula(:default; options=ComputationDetails()) +end + +@testitem "Commons / owned records / explicit transport and passive persistence" tags=[:unit, :measurements] setup=[TestFixtures] begin + using Serialization + const IO = LineCableModels.ImportExport + payload = (resolution=(enabled=true,), values=[1.0, 2.0]) + for Record in (FormulationOptions, ComputationOptions, ComputationDetails) + record = Record(payload) + @test_throws MethodError keys(record) + @test_throws MethodError iterate(record) + @test_throws MethodError NamedTuple(record) + @test_throws MethodError merge(record, (;)) + for encoded in (IO.serialize_value(record), IO.serialize_value(record, Val(:scientific))) + @test encoded["__type__"] == string(nameof(Record)) + restored = IO.deserialize_value(encoded) + @test typeof(restored) === typeof(record) + @test isequal(restored, record) + end + buffer = IOBuffer() + serialize(buffer, record) + seekstart(buffer) + @test isequal(deserialize(buffer), record) + end + + problem = CableConstantsProblem(TestFixtures.coaxial_design()) + formulation = CableConstantsFormulation() + scalar = @inferred compute(problem, formulation; options=ComputationOptions()) + bundled = @inferred compute(problem, [formulation, formulation]) + @test isconcretetype(eltype(bundled)) + @test all(value -> typeof(value) === typeof(scalar), bundled) + @test scalar == compute(problem, formulation; options=(;)) + @test details(scalar) isa ComputationDetails + @test fieldtype(typeof(scalar), :details) === typeof(details(scalar)) + @test fieldtype(typeof(details(scalar).data), :shunt_model) === NamedTuple + restored_scalar=IO.deserialize_value(IO.serialize_value(scalar)) + @test typeof(restored_scalar) === typeof(scalar) + @test fieldtype(typeof(details(restored_scalar).data), :shunt_model) === NamedTuple + @test_throws TypeError compute(problem, formulation; options=FormulationOptions()) + @test_throws MethodError ParametricProblem(problem, (;)) + + for outer in (LinearError(formulation; options=(retain_details=true,)), + MonteCarlo(formulation; trials=2, seed=13, retain_details=true, return_samples=true)) + result = compute(ParametricProblem(problem), outer) + @test fieldtype(typeof(result), :details) === typeof(details(result)) + @test isconcretetype(eltype(result.values)) + encoded = IO.serialize_value(result) + retained = IO.deserialize_value(encoded["details"]) + @test retained isa NamedTuple + records = outer isa LinearError ? retained.points : first(retained.trials) + @test all(record -> record isa NamedTuple, records) + restored = IO.deserialize_value(encoded) + @test isequal(details(restored), details(result)) + @test first(restored) == first(result) + @test restored.formulation isa NamedTuple + @test fieldtype(typeof(details(first(restored)).data), :shunt_model) === NamedTuple + restored_records=outer isa LinearError ? details(restored).data.points : + first(details(restored).data.trials) + @test all(record -> fieldtype(typeof(record.data), :shunt_model) === NamedTuple, + restored_records) + end +end diff --git a/test/unit/commons/uncertainty.jl b/test/unit/commons/uncertainty.jl new file mode 100644 index 000000000..ba2548792 --- /dev/null +++ b/test/unit/commons/uncertainty.jl @@ -0,0 +1,30 @@ +@testitem "Commons / nominal and uncertainty / deterministic, complex and array values" tags=[:unit, :commons] begin + # A minimal uncertain number with its own scalar nominal value and uncertainty. + struct UncertainProbe <: Real + center::Float64 + sigma::Float64 + end + LineCableModels.nominal(value::UncertainProbe) = value.center + LineCableModels.uncertainty(value::UncertainProbe) = value.sigma + + for value in (3, 2.5f0, 1.5, 4.0 + 5.0im) + @test nominal(value) === value + end + @test nominal(complex(UncertainProbe(1.0, 0.1), UncertainProbe(2.0, 0.2))) === + complex(1.0, 2.0) + values = [UncertainProbe(1.0, 0.1), UncertainProbe(2.0, 0.2)] + nominals = nominal(values) + @test nominals isa Vector{Float64} + @test nominals == [1.0, 2.0] + @test values == [UncertainProbe(1.0, 0.1), UncertainProbe(2.0, 0.2)] + + @test uncertainty(3) === 0 + @test uncertainty(2.5f0) === 0.0f0 + @test uncertainty(1.5) === 0.0 + @test uncertainty(complex(UncertainProbe(1.0, 0.1), UncertainProbe(2.0, 0.2))) === + complex(0.1, 0.2) + sigmas = uncertainty(values) + @test sigmas isa Vector{Float64} + @test sigmas == [0.1, 0.2] + @test_throws MethodError uncertainty("not numeric") +end diff --git a/test/unit/commons/validate.jl b/test/unit/commons/validate.jl new file mode 100644 index 000000000..1be10b66a --- /dev/null +++ b/test/unit/commons/validate.jl @@ -0,0 +1,99 @@ +@testitem "Commons / validate / materialized inputs / actionable failures" tags=[:unit, :parametric] setup=[ + TestFixtures +] begin + using RequiredInterfaces + + material=Material( + kind = :conductor, + rho = 1.7241e-8, + eps_r = 1.0, + mu_r = 1.0, + T0 = 20.0, + alpha = 0.00393 + ) + @test validate(material) === material + @test parentmodule(validate) === LineCableModels.Commons + @test_throws MethodError validate((answer = 42,)) + + struct UnvalidatedEarthDescription<:AbstractEarthModel end + @test_throws RequiredInterfaces.NotImplementedError validate( + UnvalidatedEarthDescription() + ) + + material_error=try + Material( + kind = :conductor, + rho = -1.0, + eps_r = 1.0, + mu_r = 1.0, + T0 = 20.0, + alpha = 0.0 + ) + nothing + catch error + error + end + @test material_error isa DomainError + @test occursin("Material.rho", sprint(showerror, material_error)) + @test occursin("-1.0", sprint(showerror, material_error)) + + layer_error=try + layer(rho = 100.0, thickness = 0.0) + nothing + catch error + error + end + @test layer_error isa DomainError + @test occursin("EarthLayer.thickness", sprint(showerror, layer_error)) + @test occursin("0.0", sprint(showerror, layer_error)) + + design=TestFixtures.coaxial_design() + @test validate(design) === design + damaged_design=TestFixtures.coaxial_design() + pop!(damaged_design.terminal_map) + design_error=try + validate(damaged_design) + nothing + catch error + error + end + @test design_error isa DimensionMismatch + @test occursin("CableDesign.terminal_map", sprint(showerror, design_error)) + + system=TestFixtures.three_phase_system() + @test validate(system) === system + system.connection_order[1]=99 + system_error=try + validate(system) + nothing + catch error + error + end + @test system_error isa DimensionMismatch + @test occursin("LineCableSystem.connection_order[1]", sprint(showerror, + system_error)) + + struct CheckedProblem<:AbstractProblemDefinition + value::Int + end + function LineCableModels.validate(problem::CheckedProblem) + problem.value>0||throw(DomainError( + problem.value, + "CheckedProblem.value must be positive" + )) + return problem + end + space=Gridspace{CheckedProblem}(CheckedProblem, (Grid((1, -1)),)) + points=LineCableModels.points(space) + first_point, state=iterate(points) + @test LineCableModels.materialize(first_point) == CheckedProblem(1) + second_point=first(iterate(points, state)) + point_error=try + LineCableModels.materialize(second_point) + nothing + catch error + error + end + @test point_error isa DomainError + @test occursin("CheckedProblem.value", sprint(showerror, point_error)) +end diff --git a/test/unit/commons/validate_ownership.jl b/test/unit/commons/validate_ownership.jl new file mode 100644 index 000000000..f5366482d --- /dev/null +++ b/test/unit/commons/validate_ownership.jl @@ -0,0 +1,35 @@ +@testitem "Commons / validate / owned inputs validate without mutation" tags=[:unit, :parametric] setup=[TestFixtures] begin + using RequiredInterfaces, Serialization + const E=LineCableModels.Engine + @test parentmodule(validate) === LineCableModels.Commons + @test !applicable(validate, (answer=42,)) + for root in (AbstractMaterial, AbstractEarthModel, AbstractProblemDefinition) + @test RequiredInterfaces.isInterface(root) + @test Tuple(RequiredInterfaces.functions(RequiredInterfaces.getInterface(root))) == (validate,) + end + design=TestFixtures.coaxial_design() + system=TestFixtures.three_phase_system() + problem=TestFixtures.line_parameters_problem(system) + values=(TestFixtures.conductor_material(), homogeneous(rho=100.0), + layer(rho=200.0), design, system, problem, CableConstantsProblem(design), + TestFixtures.two_conductor_results()) + for input in values + owner=parentmodule(typeof(input)) + @test which(validate, Tuple{typeof(input)}).module === owner + bytes(x)=let buffer=IOBuffer(); Serialization.serialize(buffer,x); take!(buffer); end + before=bytes(input) + @test applicable(validate,input) + @test validate(input) === input + @test bytes(input)==before + end + # Construction and compute must reject damaged current inputs. The public + # materialization path for new problem implementors is tested in behavior.jl. + problem.frequencies[1]=-1 + @test_throws DomainError validate(problem) + @test_throws DomainError compute(problem,Formulation()) + bad=TestFixtures.coaxial_design() + pop!(bad.terminal_map) + snapshot=copy(bad.terminal_map) + @test_throws DimensionMismatch validate(bad) + @test bad.terminal_map == snapshot +end diff --git a/test/unit/core/architecture.jl b/test/unit/core/architecture.jl new file mode 100644 index 000000000..8441e3257 --- /dev/null +++ b/test/unit/core/architecture.jl @@ -0,0 +1,38 @@ +# Ownership is checked on actual loaded bindings by quality/explicit_imports.jl. +# Lowering and reuse counts run through compute in integration/formulation_grid.jl +# and integration/line_parameters.jl. Source spelling, directory layout and +# absence of superseded helpers are not current architecture requirements. +@testitem "Core / architecture / earth models are immutable values" tags=[:unit, :parametric] begin + earth=@earth begin + layer(rho = 100.0, thickness = 5.0) + layer(rho = 500.0) + end + earth_layer=layer(rho = 50.0) + + @test !ismutabletype(typeof(earth)) + @test eltype(typeof(earth)) === Float64 + @test fieldtype(typeof(earth), :layers) === + NTuple{3, EarthLayer{Float64}} + @test !hasmethod(add!, Tuple{typeof(earth), typeof(earth_layer)}) + @test_throws MethodError setindex!(earth.layers, earth_layer, 2) +end + +@testitem "Core / selectors / owner-local symbol facades" tags=[:unit, :importexport] begin + const Engine=LineCableModels.Engine + const ImportExport=LineCableModels.ImportExport + const WirePatterns=LineCableModels.ParametricBuilder.WirePatterns + + @test which(Engine.Formulation, (Symbol,)).module === Engine + @test which(ImportExport.export_data, (Symbol, Nothing)).module === ImportExport + @test which(ImportExport.import_data, (Symbol, Nothing)).module === ImportExport + @test which(Engine.LineParameters, (Symbol, String)).module === ImportExport + @test which( + getindex, + (WirePatterns.WireEstimate{Float64, WirePatterns.HexaPattern{Float64}}, Symbol) + ).module === WirePatterns + + @test Engine.Formulation() isa Engine.LineParametersFormulation + @test_throws ArgumentError("unknown backend formulation :unregistered") Engine.Formulation(:unregistered) + @test_throws MethodError ImportExport.export_data(:unregistered, nothing) + @test_throws MethodError ImportExport.import_data(:unregistered, nothing) +end diff --git a/test/unit/core/interfaces.jl b/test/unit/core/interfaces.jl new file mode 100644 index 000000000..42cdedbdd --- /dev/null +++ b/test/unit/core/interfaces.jl @@ -0,0 +1,43 @@ +@testitem "Core / interfaces / domain and deterministic uncertainty fallbacks" tags=[:unit, :engine] begin + @test domain(Int) === nothing + @test domain(1) === nothing + @test PhaseDomain() isa LineCableModels.LineParamsDomain + @test_throws MethodError ModalDomain() + @test PhaseDomain !== ModalDomain + + @test nominal(3.0 + 4.0im) == 3.0 + 4.0im + @test nominal([1.0, 2.0]) == [1.0, 2.0] + @test uncertainty(3.0) == 0.0 + @test_throws MethodError uncertainty("not numeric") + value=(answer = 42,) + @test_throws MethodError validate(value) +end + +@testitem "Core / docstrings / sanitized method-list origins" tags=[:unit, :engine] begin + using DocStringExtensions + + method=which(LineCableModels.domain, (Int,)) + @test !isabspath(LineCableModels._method_path(method)) + @test !occursin(pkgdir(LineCableModels), LineCableModels._method_path(method)) + + binding=Docs.Binding(LineCableModels.Engine, :domain) + doc=first(values(Docs.meta(LineCableModels.Engine)[binding].docs)) + buffer=IOBuffer() + @test DocStringExtensions.format(LineCableModels.METHODLIST, buffer, doc) === nothing + rendered=String(take!(buffer)) + @test occursin("domain", rendered) + @test occursin(LineCableModels._method_path(method), rendered) + @test !occursin(pkgdir(LineCableModels), rendered) +end + +@testitem "Core / owner-local numerics / transforms and conductivity" tags=[:unit, :engine] begin + using LinearAlgebra + const Engine=LineCableModels.Engine + + @test LineCableModels.ModalAnalysis.offdiagonal_ratio(Diagonal([1.0, 2.0])) == 0.0 + @test_throws DimensionMismatch LineCableModels.ModalAnalysis.offdiagonal_ratio(zeros(2, 3)) + + @test Engine.conductivity(Inf) == 0.0 + @test isinf(Engine.conductivity(0.0)) + @test Engine.conductivity(4.0) == 0.25 +end diff --git a/test/unit/core/plotting.jl b/test/unit/core/plotting.jl new file mode 100644 index 000000000..a58839b65 --- /dev/null +++ b/test/unit/core/plotting.jl @@ -0,0 +1,69 @@ +@testitem "Plotting / core loading / shell handles and domain geometry" tags=[:unit, :parametric] setup=[ + TestFixtures +] begin + using LineCableModels + + @test isdefined(LineCableModels, :PlotBuilder) + @test parentmodule(LineCableModels.plot) === LineCableModels.PlotBuilder + @test parentmodule(LineCableModels.preview) === LineCableModels.PlotBuilder + @test parentmodule(LineCableModels.show_material_scale) === + LineCableModels.PlotBuilder + + figure=Ref(:native_figure) + axes=(:axis_1, :axis_2) + controls=Dict{Symbol, Any}(:reset=>:native_button) + legend=Ref(:native_legend) + colorbars=(Ref(:native_colorbar),) + plot_state=(semantic_groups = (:a, :b),) + plot_handle=UIPlot( + figure, + axes; + controls, + legend, + colorbars, + plot_state, + export_name = "native", + open_export = false + ) + @test plot_handle.figure === figure + @test plot_handle.title === nothing + @test plot_handle.axes == Any[axes...] + @test plot_handle.controls === controls + @test plot_handle.legend === legend + @test isempty(plot_handle.panel_legends) + @test only(plot_handle.colorbars) === only(colorbars) + @test plot_handle.plot_state === plot_state + + design=TestFixtures.coaxial_design() + placed=first(design.geometry.regions) + shapes=LineCableModels.DataModel.preview_shapes(placed) + @test !isempty(shapes) + @test all(shape -> shape isa LineCableModels.DataModel.PreviewShape, shapes) + @test all(shape -> shape.material === placed.source.material, shapes) + @test all(shape -> shape.tag === placed.source.tag, shapes) + + copper=Material(kind = :conductor, rho = 1.7241e-8) + matrix=Material(kind = :insulator, rho = Inf, eps_r = 2.3) + packed=terminal( + :preview_core, + stranded( + copper; + shape = Disk(0.5), + boundary = Disk(1.5), + fill = matrix + ) + ) + fill=last(LineCableModels.DataModel.resolve( + LineCableModels.DataModel.EmptyBoundary(), packed + ).regions) + @test any(fill.placement.patterns) do entry + entry.pattern isa LineCableModels.DataModel.EnclosureBoundary + end + perforated=only(LineCableModels.DataModel.preview_shapes(fill)) + @test length(perforated.geometry.interiors) == 7 + @test eltype(perforated.geometry.interiors) !== Any + + ranges=LineCableModels.DataModel.material_property_ranges(design) + @test keys(ranges) == (:rho, :mu_r, :eps_r) + @test all(range -> range isa Tuple{<:Real, <:Real}, values(ranges)) +end diff --git a/test/unit/core/test_harness.jl b/test/unit/core/test_harness.jl new file mode 100644 index 000000000..deba5a6d0 --- /dev/null +++ b/test/unit/core/test_harness.jl @@ -0,0 +1,142 @@ +@testitem "Core / test discovery rejects excluded setups malformed source and empty selection" tags=[:unit, :units, :slow] begin + helper = joinpath(pkgdir(LineCableModels), "test", "support", "runner.jl") + include(helper) + directory = joinpath(pkgdir(LineCableModels), "test") + item(file, name, tags) = (; filename=joinpath(directory, file), name, tags) + ordinary = item("unit/current.jl", "unresolved current item", [:unit]) + sampling = item("integration/sampling.jl", "current sampling item", [:integration]) + native = item("extensions/study.jl", "unresolved native study", [:extension, :fem_numerical]) + select(args) = ValidationTestRunner.selection(args, directory) + @test select(String[])(ordinary) + @test select(String[])(sampling) + @test !select(["--list"])(native) + @test select(["tag:integration"])(sampling) + @test select(["tag:fem_numerical"])(native) + @test select(["extensions/study"])(native) + @test select(["tag:integration", "integration/"])(sampling) + @test !select(["tag:fem_numerical", "integration/"])(native) + @test select(["tag:unit", "tag:fem_numerical", "CURRENT", "STUDY"])(ordinary) + @test !select(["tag:unit"])(merge(ordinary, (; filename=joinpath(dirname(directory), "outside.jl")))) + @test_throws ErrorException select(["--unknown"]) + @test_throws ErrorException select(["tag:"]) + project = dirname(Base.active_project()) + function invoke(root; selected=true, list=false) + code = "include($(repr(helper))); ValidationTestRunner.run_tests($(repr(root)); filter=(_ -> $selected), list=$list)" + output = IOBuffer() + process = run(pipeline(ignorestatus(`$(Base.julia_cmd()) --startup-file=no --project=$project -e $code`); + stdout=output, stderr=output); wait=false) + status = timedwait(() -> process_exited(process), 60.0; pollint=0.05) + if status === :timed_out + kill(process) + wait(process) + error("test-discovery subprocess exceeded 60 seconds") + end + wait(process) + return success(process), String(take!(output)) + end + mktempdir() do root + write(joinpath(root, "Project.toml"), "[deps]\n") + write(joinpath(root, "JuliaTestItems.toml"), "config-version = 1\nexclude = [\"excluded/**\"]\n") + excluded = mkpath(joinpath(root, "excluded")) + marker = joinpath(root, "unexpected-setup") + write(joinpath(excluded, "setup.jl"), """ + @testmodule ChosenSetup begin + write($(repr(marker)), "excluded") + const answer = :wrong + end + @testmodule ExcludedOnly begin + write($(repr(marker)), "excluded-only") + end + """) + active = joinpath(root, "active.jl") + body_marker = joinpath(root, "body-executed") + write(active, """ + @testmodule ChosenSetup begin + const answer = :current + end + @testitem "included item" setup=[ChosenSetup] begin + write($(repr(body_marker)), "executed") + @test ChosenSetup.answer === :current + end + """) + ok, output = invoke(root; list=true) + @test ok + @test occursin("Listed 1 maintained test items in 1 files; no test bodies executed", output) + @test !isfile(body_marker) + @test !isfile(marker) + + ok, output = invoke(root) + @test ok + @test occursin("Selected 1 maintained test items", output) + @test !isfile(marker) + @test isfile(body_marker) + @test occursin("Starting [", output) + @test occursin("run completed", output) + @test occursin("s elapsed", output) + + write(active, "@testitem \"failed item\" begin\n@test false\nend\n") + ok, output = invoke(root) + @test !ok + @test occursin("Selected 1 maintained test items in 1 files; run failed", output) + + ok, output = invoke(root; selected=false) + @test !ok + @test occursin("No test items selected", output) + + write(active, """ + @testitem "missing setup" setup=[ExcludedOnly] begin + @test true + end + """) + ok, output = invoke(root) + @test !ok + @test occursin("Test setup ExcludedOnly is not defined", output) + @test !isfile(marker) + + write(active, "@testitem \"valid body\" begin\n@test true\nend\nend\n") + ok, output = invoke(root) + @test !ok + @test occursin("ParseError", output) + @test occursin("active.jl", output) + + write(joinpath(root, "JuliaTestItems.toml"), "config-version = 1\nexlcude = [\"excluded/**\"]\n") + ok, output = invoke(root) + @test !ok + @test occursin("Invalid test discovery keys", output) + end +end + +@testitem "Core / changed: runs quality in a fresh process" tags=[:unit, :units, :slow] begin + root = pkgdir(LineCableModels) + # Before quality runs, the parent process gains a method inside a package module, + # defined from a test file (this one). The placement guard reports such a method when + # it shares the process. In the fresh process, the guard passes. + runner = joinpath(root, "test", "support", "runner.jl") + planted = joinpath(root, "test", "unit", "core", "test_harness.jl") + program = """ + using TestItemRunner + include($(repr(runner))) + import LineCableModels + include_string(LineCableModels.PSCAD, "planted_probe(::Val{:planted}) = 1", $(repr(planted))) + method = only(methods(LineCableModels.PSCAD.planted_probe)) + println("PLANTED ", String(method.file)) + ValidationTestRunner.run_changed($(repr(root)), + Set([("test/unit/units/units.jl", "Units / locked public vocabulary")]); + quality = ["Quality / architecture / placement"]) + println("CHANGED RUN PASSED") + """ + output = IOBuffer() + command = `$(Base.julia_cmd()) --project=$(dirname(Base.active_project())) -e $program` + process = run(pipeline(ignorestatus(command); stdout = output, stderr = output); wait = false) + status = timedwait(() -> process_exited(process), 600.0; pollint = 0.1) + status === :timed_out && kill(process) + wait(process) + text = String(take!(output)) + @test status === :ok + @test success(process) + @test occursin("PLANTED $planted", text) + @test count("Selected 1 maintained test items in 1 files; run completed", text) == 2 + @test occursin("Quality items in a fresh process: Quality / architecture / placement", text) + @test occursin("CHANGED RUN PASSED", text) + success(process) || println(text) +end diff --git a/test/unit/datamodel/annular_strand.jl b/test/unit/datamodel/annular_strand.jl new file mode 100644 index 000000000..4e6b701f2 --- /dev/null +++ b/test/unit/datamodel/annular_strand.jl @@ -0,0 +1,149 @@ +@testitem "DataModel / a complete bent strip retains annular geometry and lay" tags=[:unit, :importexport] begin + const DM = LineCableModels.DataModel + const EN = LineCableModels.Engine + const IE = LineCableModels.ImportExport + copper = Material(:conductor, 1.72e-8, 1.0, 1.0) + strand = Rectangle(0.3e-3, 0.1e-3) + center = Disk(0.2e-3) + body = stranded(copper; shape=strand, center=center, + boundary=Disk(0.6e-3), lay=LayRatio(12)) + design = build(CableDesign, "annular-last-strip", terminal(:core, body)) + metal = filter(region -> region.source.material.kind === :conductor, + design.geometry.regions) + @test length(metal) == 34 + final_strip = last(metal) + @test final_strip.source.primitive == strand + @test final_strip.primitive isa Annulus + @test area(final_strip.primitive) ≈ area(strand) + # A partial course member keeps its exact curved boundary. Directional + # support must agree with that geometric boundary after a rigid placement too. + bent = first(member.primitive for member in metal if member.primitive isa DM.BentStrip) + for shape in (bent, DM.resolve(Pose2(0.002, -0.003, 0.47), bent)) + @test area(shape) ≈ area(strand) + @test DM.perimeter(shape) ≈ + shape.span * (shape.ro + shape.ri) + 2(shape.ro - shape.ri) + @test DM.thickness(shape) ≈ shape.ro - shape.ri + @test DM.boundary(shape) === shape + points = DM.tessellate(shape; points_per_arc=2049) + for angle in (shape.at.φ, shape.at.φ + pi / 2, shape.at.φ + pi, -0.63) + projection = maximum(point -> point[1] * cos(angle) + point[2] * sin(angle), points) + @test DM.support(shape, angle) ≈ projection atol=2e-10 rtol=0 + end + end + @test perimeter(final_strip.primitive) ≈ + 2pi * (final_strip.primitive.ri + final_strip.primitive.ro) + @test only(final_strip.placement.patterns).pattern.course == 5 + @test only(final_strip.placement.patterns).member == 34 + for member in metal[2:end] + @test only(member.paths).radius ≈ (r_in(member.primitive) + r_ex(member.primitive)) / 2 + @test overlength(only(member.paths).path, only(member.paths).radius) ≈ sqrt(1 + (pi / 12)^2) + end + @test IE.deserialize_value(IE.serialize_value(design)) == design + @test length(design.geometry.regions) == length(metal) + @test outer_radius(design) == final_strip.primitive.ro + @test outer_radius(design) < 0.6e-3 + @test all(region -> only(region.placement.patterns).pattern.boundary == + design.geometry.outer, metal) + blueprint = @inferred EN.flatten(LineCableModelsCoaxial(), design, Float64) + row = only(blueprint.conductors) + @test row.r_ex == outer_radius(design) + @test row.num_wires == length(metal) + @test row.cross_section ≈ area(center) + 33area(strand) + expected_resistance = copper.rho / (area(center) + 33area(strand) / sqrt(1 + (pi / 12)^2)) + @test row.resistance ≈ expected_resistance + @test isfinite(row.gmr) && 0 < row.gmr < row.r_ex + + shifted = build(CableDesign, "shifted-annular-last-strip", + assembly(at(terminal(:core, body), Pose2(0.002, -0.003, 0.47)))) + shifted_row = only(EN.flatten(LineCableModelsCoaxial(), shifted, Float64).conductors) + @test shifted_row.resistance ≈ row.resistance + @test shifted_row.gmr ≈ row.gmr + @test shifted_row.turns_per_length ≈ row.turns_per_length + + designs = build(CableDesign, "annular-grid", terminal(:core, + stranded(copper; shape=strand, center=center, + boundary=Disk(Grid((0.59e-3, 0.6e-3))), lay=LayRatio(12)))) + @test designs isa Gridspace{CableDesign} + @test length(designs) == 2 + rows = [only(EN.flatten(LineCableModelsCoaxial(), selected, Float64).conductors) + for selected in designs] + @test [selected.num_wires for selected in rows] == [33, 34] + @test last(rows).resistance ≈ row.resistance + @test last(rows).gmr ≈ row.gmr +end + +@testitem "DataModel / annular mutual GMD uses area integration, not centroid distance" tags=[:unit, :datamodel] begin + using QuadGK + const DM = LineCableModels.DataModel + outer = Annulus(1e-3, 2e-3) + integral = first(quadgk(radius -> 2radius * log(radius), outer.ri, outer.ro; + rtol=1e-12)) + expected = exp(integral / (outer.ro^2 - outer.ri^2)) + for inner in (Disk(0.4e-3), Disk(0.4e-3, Pose2(0.2e-3, -0.3e-3)), + Annulus(0.1e-3, 0.8e-3), + Rectangle(0.4e-3, 0.6e-3, Pose2(0.1e-3, 0.2e-3, 0.4))) + @test DM.geometric_mean_distance(outer, inner) ≈ expected rtol=2e-12 + @test DM.geometric_mean_distance(inner, outer) ≈ expected rtol=2e-12 + end + distant = Annulus(0.3e-3, 0.6e-3, Pose2(0.01, 0.0)) + @test DM.geometric_mean_distance(outer, distant) ≈ 0.01 + @test DM.geometric_mean_distance(distant, outer) ≈ 0.01 + @test_throws ArgumentError DM.geometric_mean_distance(Disk(0.4e-3), Disk(0.4e-3)) + for T in (Float32, Float64) + annulus = Annulus(T(1e-3), T(1.0001e-3)) + center = Disk(T(0.5e-3)) + distance = @inferred DM.geometric_mean_distance(annulus, center) + @test distance isa T + @test annulus.ri <= distance <= annulus.ro + reference = setprecision(BigFloat, 256) do + inner, outer = BigFloat(annulus.ri), BigFloat(annulus.ro) + exp((outer^2 * log(outer) - inner^2 * log(inner)) / + (outer^2 - inner^2) - big"0.5") + end + @test distance ≈ T(reference) rtol=8eps(T) + end +end + +@testitem "DataModel / disk plus a uniform annular strand equals a solid conductor" tags=[:unit, :parametric] begin + const EN = LineCableModels.Engine + copper = Material(:conductor, 1.72e-8, 1.0, 1.0) + center = Disk(0.2e-3) + strand = Rectangle(1.4e-3, 0.1e-3) + radius = sqrt(center.r^2 + area(strand) / pi) + design = build(CableDesign, "one-annular-strand", terminal(:core, + stranded(copper; shape=strand, center=center, boundary=Disk(radius)))) + row = only(EN.flatten(LineCableModelsCoaxial(), design, Float64).conductors) + # Independent uniform-current limiting case: there is no material or + # current-density discontinuity between the center and its one full ring. + @test row.num_wires == 2 + @test row.cross_section ≈ pi * radius^2 + @test row.resistance ≈ copper.rho / (pi * radius^2) + @test row.gmr ≈ radius * exp(-1 / 4) rtol=2e-12 + solid_design = build(CableDesign, "equivalent-solid", terminal(:core, + solid(copper, Disk(radius)))) + solid_row = only(EN.flatten(LineCableModelsCoaxial(), solid_design, Float64).conductors) + @test row.material.rho ≈ solid_row.material.rho + @test row.material.mu_r ≈ solid_row.material.mu_r rtol=2e-11 + + dielectric = Material(:insulator, Inf, 2.3, 1.0) + insulated = map((design.origin, solid_design.origin)) do origin + build(CableDesign, "uniform-current-limit", Stack( + origin, + insulation(dielectric; t=0.2e-3), + terminal(:sheath, Region(:metal_screen, Shell(0.05e-3), copper)), + )) + end + constants = map(source -> compute(CableConstantsProblem(source)), insulated) + for field in (:R, :L, :C, :G) + @test getproperty(first(constants), field) ≈ getproperty(last(constants), field) rtol=2e-10 + end + parameters = map(insulated) do source + system = build(LineCableSystem, source, Pose2(0.0, -1.0); + connections=Dict(:core=>1, :sheath=>0)) + problem = LineParametersProblem(system; earth_props=homogeneous(rho=100.0), + frequencies=[0.1, 50.0, 1e6]) + compute(problem) + end + @test first(parameters).Z ≈ last(parameters).Z rtol=2e-10 + @test first(parameters).Y ≈ last(parameters).Y rtol=2e-10 +end diff --git a/test/unit/datamodel/assembly_lowering.jl b/test/unit/datamodel/assembly_lowering.jl new file mode 100644 index 000000000..7af38e412 --- /dev/null +++ b/test/unit/datamodel/assembly_lowering.jl @@ -0,0 +1,149 @@ +@testitem "DataModel / assembly placement preserves local conductor lowering" tags=[:unit, :importexport, :slow] begin + const DM = LineCableModels.DataModel + const EN = LineCableModels.Engine + const IE = LineCableModels.ImportExport + copper = Material(:conductor, 1.72e-8, 1.0, 1.0) + dielectric = Material(:insulator, 1e14, 2.3, 1.0) + engine = LineCableModelsCoaxial() + wire = Disk(0.5e-3) + single = terminal(:core, solid(copper, wire)) + placed = at(single, Pose2(0.01, 0.0)) + for constructor in (assembly, cores) + @test only(constructor(single).item).item == single + @test only(constructor(placed).item) == placed + gridded = constructor(at(single, Pose2(Grid((0.01, 0.02)), 0.0))) + @test gridded isa Gridspace{Assembly} + @test length(gridded) == 2 + @test [only(item.item).at.x for item in gridded] == [0.01, 0.02] + end + @test_throws ArgumentError assembly(single, single; pattern=Ring(2; r=0.01)) + @test_throws ArgumentError assembly(single; names=(:a,)) + @test_throws ArgumentError cores(single; r=0.01) + @test_throws ArgumentError cores(single; n=3, names=(:a, :b, :c)) + bodies = ( + solid(copper, Disk(1e-3)), + stranded(copper; shape=wire, boundary=Disk(5wire.r), lay=LayRatio(12)), + stranded(copper; shape=wire, boundary=Disk(sqrt(19) * wire.r), compact=true), + stranded(copper; shape=Rectangle(0.3e-3, 0.1e-3), center=Disk(0.2e-3), + boundary=Disk(0.59e-3)), + stranded(copper; shape=Disk(0.35e-3), + boundary=Sector(span=2pi/3, r_base=0.6e-3, r_back=4e-3, fillet=0.2e-3)), + ) + for body in bodies, count in (1, 3) + names = ntuple(index -> Symbol(:phase_, index), count) + member = terminal(:core, body, insulation(dielectric; t=0.5e-3)) + pattern = Ring(count; r=0.01, φ0=0.27) + repeated = build(CableDesign, "repeated", assembly(member; pattern, names)) + poses = DM.placements(pattern, DM.resolve(EmptyBoundary(), member), nothing) + explicit = build(CableDesign, "explicit", assembly(( + at(terminal(name, body, insulation(dielectric; t=0.5e-3)), pose) + for (name, pose) in zip(names, poses))...)) + actual = @inferred EN.flatten(engine, repeated, Float64) + expected = @inferred EN.flatten(engine, explicit, Float64) + @test actual.assembly_ranges == expected.assembly_ranges == [i:i for i in 1:count] + for (left, right) in zip(actual.conductors, expected.conductors) + @test left.terminal === right.terminal + for field in (:r_in, :r_ex, :cross_section, :num_wires, :turns_per_length, + :resistance, :alpha, :gmr, :position) + @test all(isapprox.(getproperty(left, field), getproperty(right, field))) + end + end + @test actual.dielectrics == expected.dielectrics + @test IE.deserialize_value(IE.serialize_value(repeated)) == repeated + @test all(region -> last(region.placement.patterns).owner === DM.Assembly, + repeated.geometry.regions) + relocated = DM.resolve(Pose2(0.004, -0.006, 0.31), first(repeated.geometry.regions)) + @test last(relocated.placement.patterns).owner === DM.Assembly + @test last(relocated.placement.patterns).member == 1 + end + + # The same Assembly becomes a strand course when a Group coalesces it. + center = terminal(:core, solid(copper, wire)) + member = terminal(:wire, solid(copper, wire)) + pattern = Ring(6; r=2wire.r) + outer = assembly(member; pattern, names=ntuple(index -> Symbol(:wire_, index), 6)) + merged = terminal(:core, assembly(center, outer)) + direct = Group(:core, Region(:solid, wire, copper); pattern) + merged_design = build(CableDesign, "coalesced", merged) + direct_design = build(CableDesign, "direct", center, direct) + @test DM.radial_components(merged_design, Float64) == DM.radial_components(direct_design, Float64) + @test all(region -> all(entry -> entry.owner !== DM.Assembly, region.placement.patterns), + merged_design.geometry.regions) + + # A helical placement retains the physical overlength independently of + # whether its child is a strand or a complete coaxial core. + helix = Helix(LayRatio(12)) + helical = build(CableDesign, "helical-assemblies", assembly(member; + pattern=Ring(3; r=0.01), names=(:a, :b, :c), path=helix)) + rows = EN.flatten(engine, helical, Float64).conductors + @test all(row -> row.num_wires == 0, rows) + @test all(row -> row.resistance ≈ copper.rho / area(wire) * overlength(helix, 0.01), rows) + @test all(region -> only(region.paths).path === helix, helical.geometry.regions) + @test all(region -> only(region.paths).radius ≈ 0.01, helical.geometry.regions) + prototype = terminal(:core, bodies[2]) + isolated = build(CableDesign, "stranded", prototype) + helical_stranded = build(CableDesign, "helical-stranded", assembly(prototype; + pattern=Ring(3; r=0.01), names=(:a, :b, :c), path=helix)) + base = only(EN.flatten(engine, isolated, Float64).conductors) + wound = EN.flatten(engine, helical_stranded, Float64).conductors + @test all(row -> row.resistance ≈ base.resistance * overlength(helix, 0.01), wound) +end + +@testitem "Engine / repeated coaxials agree with explicit placements and remain Gridable" tags=[:integration, :parametric] begin + const DM = LineCableModels.DataModel + const EN = LineCableModels.Engine + copper = Material(:conductor, 1.72e-8, 1.0, 1.0) + dielectric = Material(:insulator, 1e14, 2.3, 1.0) + parts = (solid(copper, Disk(1e-3)), insulation(dielectric; t=1e-3)) + member = terminal(:core, parts...) + names = (:a, :b, :c) + spaces = build(CableDesign, "assembly-grid", cores(member; + n=3, r=Grid((0.01, 0.02)), names)) + @test spaces isa Gridspace{CableDesign} + @test length(spaces) == 2 + for (design, radius) in zip(spaces, (0.01, 0.02)) + poses = DM.placements(Ring(3; r=radius), DM.resolve(EmptyBoundary(), member), nothing) + explicit = build(CableDesign, "explicit", assembly(( + at(terminal(name, parts...), pose) for (name, pose) in zip(names, poses))...)) + systems = map((design, explicit)) do source + build(LineCableSystem, [source], [Pose2(0.0, -1.0)]; + connections=[Dict(:a=>1, :b=>2, :c=>3)]) + end + problems = map(system -> LineParametersProblem(system; + earth_props=homogeneous(rho=100.0), frequencies=[50.0, 1000.0]), systems) + actual = compute(first(problems)) + expected = compute(last(problems)) + @test actual.Z.values == expected.Z.values + @test actual.Y.values == expected.Y.values + @test EN.flatten(LineCableModelsCoaxial(), design, Float64).assembly_ranges == [1:1, 2:2, 3:3] + end + # Combining the core declaration and placement axes uses the same + # Gridspace rule, including a zip across the nested Ring declaration. + radii = (0.01, 0.02) + angles = (0.0, 0.3) + wires = (0.5e-3, 1e-3) + members = terminal(:core, solid(copper, Disk(Grid(wires)))) + zipped = cores(members; n=3, r=Grid(radii), φ0=Grid(angles), names, combine=:zip) + @test zipped isa Gridspace{Assembly} + @test collect(zipped) == [cores(terminal(:core, solid(copper, Disk(wire))); + n=3, r=radius, φ0=angle, names) for (wire, radius, angle) in zip(wires, radii, angles)] + product = cores(members; n=3, r=Grid(radii), φ0=Grid(angles), names) + @test length(product) == 8 + @test all(cores(terminal(:core, solid(copper, Disk(wire))); + n=3, r=radius, φ0=angle, names) in product + for wire in wires, radius in radii, angle in angles) +end + +@testitem "Gmsh FEM / assembly placement does not coarsen solid-core mesh size" tags=[:extension, :fem] begin + using Gmsh + const DM = LineCableModels.DataModel + extension = Base.get_extension(LineCableModels, :LineCableModelsGmshExt) + copper = Material(:conductor, 1.72e-8, 1.0, 1.0) + for primitive in (Disk(1e-3), Rectangle(1e-3, 0.5e-3), Ellipse(1e-3, 0.5e-3)) + member = terminal(:core, solid(copper, primitive)) + isolated = build(CableDesign, "isolated", member) + repeated = build(CableDesign, "repeated", cores(member; n=3, r=0.01, names=(:a, :b, :c))) + expected = extension._fem_region_mesh_size(only(isolated.geometry.regions)) + @test all(region -> extension._fem_region_mesh_size(region) == expected, repeated.geometry.regions) + end +end diff --git a/test/unit/datamodel/baseparams/dielectrics.jl b/test/unit/datamodel/baseparams/dielectrics.jl new file mode 100644 index 000000000..a9a477329 --- /dev/null +++ b/test/unit/datamodel/baseparams/dielectrics.jl @@ -0,0 +1,63 @@ +@testitem "BaseParams / dielectrics / coaxial kernels" tags=[:unit, :measurements] setup=[ + UseBaseParamsSupport +] begin + ε0=8.8541878128e-12 + capacitance=shunt_capacitance(0.01, 0.02, 2.3) + conductance=shunt_conductance(0.01, 0.02, 1e9) + @test capacitance ≈ 2π*ε0*2.3/log(2) + @test conductance ≈ 2π/(1e9*log(2)) + @test equivalent_eps(capacitance, 0.02, 0.01) ≈ 2.3 + @test equivalent_conductivity(conductance, 0.01, 0.02) ≈ 1e-9 + @test shunt_capacitance(0.01, 0.02, 3.0) > capacitance + @test shunt_conductance(0.01, 0.02, 1e8) > conductance + + for call in ( + ()->shunt_capacitance(0.0, 0.02, 2.3), + ()->shunt_capacitance(0.02, 0.01, 2.3), + ()->shunt_capacitance(0.01, 0.02, -1.0), + ()->shunt_conductance(0.01, 0.02, 0.0), + ()->equivalent_eps(-1e-10, 0.02, 0.01), + ()->equivalent_conductivity(-1e-9, 0.01, 0.02) + ) + @test_throws DomainError call() + end + + for T in (Float32, Float64, BigFloat) + C=shunt_capacitance(T(0.01), T(0.02), T(2.3)) + G=shunt_conductance(T(0.01), T(0.02), T(1e9)) + @test C isa T + @test G isa T + @test equivalent_eps(C, T(0.02), T(0.01)) isa T + @test equivalent_conductivity(G, T(0.01), T(0.02)) isa T + end + + uncertain=shunt_capacitance( + measurement(0.01, 1e-4), + measurement(0.02, 1e-4), + measurement(2.3, 1e-2) + ) + @test uncertain isa Measurement{Float64} + @test uncertainty(uncertain) > 0 + + omega=2π*50 + first_admittance=complex(2e-9, omega*3e-10) + second_admittance=complex(7e-9, omega*8e-10) + expected=inv(inv(first_admittance)+inv(second_admittance)) + combined=series_shunt_admittance(2e-9, 3e-10, 7e-9, 8e-10, omega) + @test combined.conductance ≈ real(expected) + @test combined.capacitance ≈ imag(expected)/omega + @test series_shunt_admittance( + Float32(2e-9), + Float32(3e-10), + Float32(7e-9), + Float32(8e-10), + Float32(omega) + ).conductance isa Float32 + @test_throws DomainError series_shunt_admittance( + 2e-9, + 3e-10, + 7e-9, + 8e-10, + 0.0 + ) +end diff --git a/test/unit/datamodel/baseparams/geometry.jl b/test/unit/datamodel/baseparams/geometry.jl new file mode 100644 index 000000000..d6f1c1ee4 --- /dev/null +++ b/test/unit/datamodel/baseparams/geometry.jl @@ -0,0 +1,54 @@ +@testitem "BaseParams / geometry / ring and helical invariants" tags=[:unit, :datamodel] begin + const BP = LineCableModels.DataModel.BaseParams + using LinearAlgebra: norm + + for T in (Float32, Float64, BigFloat) + setprecision(BigFloat, 128) do + radius = T(0.5e-3) + inner, outer, ratio = T(0.01), T(0.02), T(12) + mean_diameter, pitch_length, overlength = @inferred BP.helix(inner, outer, ratio) + @test (mean_diameter, pitch_length, overlength) isa NTuple{3,T} + @test mean_diameter == inner + outer + @test pitch_length == ratio * mean_diameter + @test overlength ≈ sqrt(one(T) + (T(pi) / ratio)^2) + @test all(isapprox.(BP.helix(2inner, 2outer, ratio), + (2mean_diameter, 2pitch_length, overlength))) + @test BP.helix(inner, outer, zero(T)) == (mean_diameter, zero(T), one(T)) + + center = (T(0.02), T(-0.03)) + positions = BP.wire_coordinates(6, radius, radius; C=center) + @test eltype(positions) === Tuple{T,T} + @test length(positions) == 6 + @test all(position -> hypot(position[1] - center[1], position[2] - center[2]) ≈ 2radius, + positions) + for index in eachindex(positions) + neighbour = positions[mod1(index + 1, length(positions))] + @test norm(collect(positions[index]) - collect(neighbour)) ≈ 2radius + end + @test BP.wire_coordinates(6, radius, radius, center) == positions + @test BP.wire_coordinates(1, radius, inner; C=center) == [center] + @test isempty(BP.wire_coordinates(0, radius, inner)) + @test BP.wire_coordinates(6, radius, radius)[1] == (2radius, zero(T)) + + turns = T(4) + correction = BP.solenoid_factor(turns, inner, outer) + @test correction isa T + @test correction > one(T) + @test BP.solenoid_factor(zero(T), inner, outer) == one(T) + @test BP.solenoid_factor(2turns, inner, outer) - 1 ≈ 4(correction - 1) + @test BP.solenoid_factor(turns / 2, 2inner, 2outer) ≈ correction + end + end + @test BP.helix(Float32(0.01), big"0.02", 12) isa NTuple{3,BigFloat} + @test_throws DomainError BP.helix(-0.01, 0.02, 12) + @test_throws DomainError BP.helix(0.02, 0.01, 12) + @test_throws DomainError BP.helix(0.01, 0.02, -12) + @test_throws DomainError BP.helix(0.01, Inf, 12) + @test_throws DomainError BP.wire_coordinates(-1, 0.001, 0.01) + @test_throws DomainError BP.wire_coordinates(6, 0.0, 0.01) + @test_throws DomainError BP.wire_coordinates(6, 0.001, -0.01) + @test_throws DomainError BP.wire_coordinates(6, 0.001, 0.01; C=(NaN, 0.0)) + @test_throws DomainError BP.solenoid_factor(-4.0, 0.01, 0.02) + @test_throws DomainError BP.solenoid_factor(4.0, -0.01, 0.02) + @test_throws DomainError BP.solenoid_factor(4.0, 0.02, 0.01) +end diff --git a/test/unit/datamodel/baseparams/inductance.jl b/test/unit/datamodel/baseparams/inductance.jl new file mode 100644 index 000000000..c9e574568 --- /dev/null +++ b/test/unit/datamodel/baseparams/inductance.jl @@ -0,0 +1,42 @@ +@testitem "BaseParams / inductance / GMR and equivalent permeability" tags=[:unit, :datamodel] setup=[ + UseBaseParamsSupport +] begin + for T in (Float32, Float64, BigFloat) + radius=tubular_gmr(T(0.02), T(0.01), T(1.5)) + @test equivalent_mu(radius, T(0.02), T(0.01)) ≈ T(1.5) + end + + @test equivalent_mu(tubular_gmr(0.02, 0.0, 1.0), 0.02, 0.0) ≈ 1.0 + @test equivalent_mu(0.02, 0.02, 0.02) ≈ 0.0 + @test_throws DomainError equivalent_mu(-0.01, 0.02, 0.01) + @test_throws DomainError equivalent_mu(0.015, 0.01, 0.02) + + lay_radius=0.01 + wire_radius=0.001 + wire_count=12 + coordinates=[( + lay_radius*cos(2π*index/wire_count), + lay_radius*sin(2π*index/wire_count) + ) + for index in 0:(wire_count - 1)] + @test strand_gmr(coordinates, wire_radius, 1.0) ≈ + strand_gmr(lay_radius, wire_count, wire_radius, 1.0) + @test_throws ArgumentError strand_gmr(Tuple{Float64, Float64}[], wire_radius, 1.0) + @test_throws ArgumentError strand_gmr([(0.0, 0.0), (0.0, 0.0)], wire_radius, 1.0) + + first_gmr=0.004 + second_gmr=0.012 + distance=0.009 + combined=equivalent_gmr(first_gmr, 2.0, second_gmr, 2.0, distance) + @test combined ≈ first_gmr^0.25*second_gmr^0.25*sqrt(distance) + @test equivalent_gmr( + Float32(first_gmr), + Float32(2), + Float32(second_gmr), + Float32(2), + Float32(distance) + ) isa Float32 + @test_throws DomainError equivalent_gmr(0.0, 1.0, second_gmr, 1.0, distance) + @test_throws DomainError equivalent_gmr(first_gmr, 0.0, second_gmr, 1.0, distance) + @test_throws DomainError equivalent_gmr(first_gmr, 1.0, second_gmr, 1.0, 0.0) +end diff --git a/test/unit/datamodel/baseparams/numeric_types.jl b/test/unit/datamodel/baseparams/numeric_types.jl new file mode 100644 index 000000000..c34808ab9 --- /dev/null +++ b/test/unit/datamodel/baseparams/numeric_types.jl @@ -0,0 +1,64 @@ +@testitem "BaseParams / numeric matrix / precision and exactness" tags=[:unit, :datamodel] setup=[ + UseBaseParamsSupport, + TestNumerics +] begin + for T in (Float32, Float64, BigFloat) + setprecision(BigFloat, 128) do + r_in=T(0.01) + r_ex=T(0.02) + rho=T(1.7241e-8) + + resistance=tubular_resistance( + r_in, + r_ex, + rho + ) + capacitance=shunt_capacitance(r_in, r_ex, T(2.3)) + conductance=shunt_conductance(r_in, r_ex, T(1e9)) + + @test resistance isa T + @test capacitance isa T + @test conductance isa T + + @test TestNumerics.isapprox_scaled( + resistance, + rho / (T(π) * (r_ex^2 - r_in^2)) + ) + @test TestNumerics.isapprox_scaled( + conductance, + T(2) * T(π) / (T(1e9) * log(r_ex / r_in)) + ) + @test capacitance > zero(T) + end + end + + @test parallel(1 // 2, 1 // 3) == 1 // 5 + complex_result=parallel(ComplexF32(1, 2), ComplexF32(3, 4)) + @test complex_result isa ComplexF32 + + normalized=shunt_capacitance(Float32(0.01), 0.02, big"2.3") + @test normalized isa BigFloat +end + +@testitem "BaseParams / numeric matrix / cross-precision convergence" tags=[:unit, :datamodel] setup=[ + UseBaseParamsSupport +] begin + # Independent electrostatic Dirichlet solution v=log(b/r)/log(b/a): + # integrate normal displacement on the inner circular boundary. + oracle=setprecision(BigFloat,256) do + 2big(pi)*big"8.8541878128e-12"*3/log(big".01"/big".005") + end + + approximations=( + shunt_capacitance(Float32(.005), Float32(.01), Float32(3.0)), + shunt_capacitance(.005, .01, 3.0), + setprecision(BigFloat, 128) do + shunt_capacitance(big".005", big".01", big"3") + end + ) + errors=map(value->abs(BigFloat(value)-oracle)/abs(oracle), approximations) + @test errors[2] <= errors[1] + @test errors[3] <= errors[2] + @test errors[1] < sqrt(eps(Float32)) + @test errors[2] < sqrt(eps(Float64)) +end diff --git a/test/unit/datamodel/baseparams/resistance.jl b/test/unit/datamodel/baseparams/resistance.jl new file mode 100644 index 000000000..20fab22a5 --- /dev/null +++ b/test/unit/datamodel/baseparams/resistance.jl @@ -0,0 +1,408 @@ +@testitem "BaseParams / tubular_resistance / reference-state equations" tags=[:unit, :measurements] setup=[ + UseBaseParamsSupport, TestNumerics] begin + using Measurements + + r_in=0.01 + r_ex=0.02 + rho=1.7241e-8 + expected=rho/(π*(r_ex^2-r_in^2)) + @test isapprox( + tubular_resistance(r_in, r_ex, rho), + expected; + atol = TestNumerics.absolute_floor(Float64) + ) + @test isinf(tubular_resistance(r_in, r_in, rho)) + @test tubular_resistance(r_in, r_in + eps(), rho) > 0 + @test tubular_resistance(r_in, 0.03, rho) < + tubular_resistance(r_in, 0.015, rho) + @test tubular_resistance(r_in, r_ex, 1e-7) > + tubular_resistance(r_in, r_ex, 1e-8) + + result32=tubular_resistance( + Float32(r_in), + Float32(r_ex), + Float32(rho) + ) + @test result32 isa Float32 + + uncertain=tubular_resistance( + measurement(r_in, 1e-6), + measurement(r_ex, 1e-6), + measurement(rho, 1e-10) + ) + @test uncertain isa Measurement{Float64} + @test uncertainty(uncertain) > 0 + +end + +@testitem "BaseParams / strip_resistance / reference-state equations" tags=[:unit, :measurements] setup=[ + UseBaseParamsSupport, TestNumerics] begin + using Measurements + + thickness=0.002 + width=0.05 + rho=1.7241e-8 + expected=rho/(thickness*width) + @test isapprox( + strip_resistance(thickness, width, rho), + expected; + atol = TestNumerics.absolute_floor(Float64) + ) + @test isinf(strip_resistance(0.0, width, rho)) + @test isinf(strip_resistance(thickness, 0.0, rho)) + @test strip_resistance(2thickness, width, rho) < + strip_resistance(thickness, width, rho) + @test strip_resistance(thickness, width, 1e-7) > + strip_resistance(thickness, width, 1e-8) + + result32=strip_resistance( + Float32(thickness), + Float32(width), + Float32(rho) + ) + @test result32 isa Float32 + + uncertain=strip_resistance( + measurement(thickness, 1e-6), + measurement(width, 1e-5), + measurement(rho, 1e-10) + ) + @test uncertain isa Measurement{Float64} + @test uncertainty(uncertain) > 0 + +end + +@testitem "BaseParams / parallel / resistance identities" tags=[:unit, :measurements] setup=[ + UseBaseParamsSupport, TestNumerics] begin + @testset "Basic Functionality" begin + # Test with real numbers (Float64) + Z1_real = 5.0 + Z2_real = 10.0 + expected_real = 1 / (1 / Z1_real + 1 / Z2_real) + result_real = parallel(Z1_real, Z2_real) + @test result_real isa Float64 + @test isapprox(result_real, expected_real; atol = TestNumerics.absolute_floor(Float64)) + @test isapprox(result_real, 3.3333333333333335; atol = TestNumerics.absolute_floor(Float64)) + + # Test with complex numbers (Complex{Float64}) + Z1_complex = 3.0 + 4.0im + Z2_complex = 8.0 - 6.0im + expected_complex = 1 / (1 / Z1_complex + 1 / Z2_complex) + @test isapprox(parallel(Z1_complex, Z2_complex), + expected_complex; atol = TestNumerics.absolute_floor(Float64)) + end + + @testset "Edge Cases" begin + # Zero impedance (short circuit) + @test isapprox(parallel(0.0, 10.0), 0.0; atol = TestNumerics.absolute_floor(Float64)) + @test isapprox(parallel(10.0, 0.0), 0.0; atol = TestNumerics.absolute_floor(Float64)) + @test isapprox(parallel(0.0, 0.0), 0.0; atol = TestNumerics.absolute_floor(Float64)) + @test isapprox(parallel(0.0 + 0.0im, 5.0 + 5.0im), + 0.0 + 0.0im; atol = TestNumerics.absolute_floor(Float64)) + + # Infinite impedance (open circuit) + @test isapprox(parallel(Inf, 10.0), 10.0; atol = TestNumerics.absolute_floor(Float64)) + @test isapprox(parallel(10.0, Inf), 10.0; atol = TestNumerics.absolute_floor(Float64)) + @test isapprox(parallel(Inf, Inf), Inf; atol = TestNumerics.absolute_floor(Float64)) + + # NaN propagation + @test isnan(parallel(NaN, 10.0)) + @test isnan(parallel(10.0, NaN)) + + # Equal and opposite impedances (Z1 = -Z2), leading to singularity + result_inf = parallel(10.0, -10.0) + @test isinf(real(result_inf)) + result_nan = parallel(3.0 + 4.0im, -3.0 - 4.0im) + @test isnan(real(result_nan)) && isnan(imag(result_nan)) + end + + + @testset "Physical Behavior" begin + # Parallel resistance is always less than the smallest individual resistance + @test parallel(10.0, 20.0) < 10.0 + + # Symmetry: parallel(Z1, Z2) == parallel(Z2, Z1) + @test isapprox(parallel(7.0, 13.0), + parallel(13.0, 7.0); atol = TestNumerics.absolute_floor(Float64)) + + # If Z1 == Z2, the result is Z1 / 2 + @test isapprox(parallel(8.0, 8.0), 4.0; atol = TestNumerics.absolute_floor(Float64)) + end + + @testset "Type Stability & Promotion" begin + + # Int and Float64 -> Float64 + result_mixed_real = parallel(5, 10.0) + @test result_mixed_real isa Float64 + @test isapprox(result_mixed_real, 1 / (1 / 5.0 + 1 / 10.0); atol = TestNumerics.absolute_floor(Float64)) + + # Float64 and Complex{Float64} -> Complex{Float64} + result_mixed_complex = parallel(10.0, 3.0 + 4.0im) + @test result_mixed_complex isa Complex{Float64} + expected_mixed_complex = 1 / (1 / (10.0 + 0.0im) + 1 / (3.0 + 4.0im)) + @test isapprox(result_mixed_complex, expected_mixed_complex; + atol = TestNumerics.absolute_floor(Float64)) + + # Both Measurement -> Measurement + Z1m = measurement(5.0, 0.1) + Z2m = measurement(10.0, 0.2) + @test parallel(Z1m, Z2m) isa Measurement + + # Mixed: measurement and Float64 -> Measurement + @test parallel(Z1m, 10.0) isa Measurement + @test parallel(5.0, Z2m) isa Measurement + end + + @testset "Uncertainty Quantification with Measurements.jl" begin + # Mixed Case 1: first argument is a Measurement + Z1_meas = measurement(5.0, 0.1) + Z2_float = 10.0 + result_mixed1 = parallel(Z1_meas, Z2_float) + expected_mixed1 = 1 / (1 / Z1_meas + 1 / Z2_float) + @test result_mixed1 isa Measurement{Float64} + @test isapprox(value(result_mixed1), value(expected_mixed1); + atol = TestNumerics.absolute_floor(Float64)) + @test isapprox(uncertainty(result_mixed1), uncertainty(expected_mixed1); + atol = TestNumerics.absolute_floor(Float64)) + + # Mixed Case 2: second argument is a Measurement + Z1_float = 5.0 + Z2_meas = measurement(10.0, 0.2) + result_mixed2 = parallel(Z1_float, Z2_meas) + expected_mixed2 = 1 / (1 / Z1_float + 1 / Z2_meas) + @test result_mixed2 isa Measurement{Float64} + @test isapprox(value(result_mixed2), value(expected_mixed2); + atol = TestNumerics.absolute_floor(Float64)) + @test isapprox(uncertainty(result_mixed2), uncertainty(expected_mixed2); + atol = TestNumerics.absolute_floor(Float64)) + + # Fully Promoted Case: both inputs are Measurements + result_full_meas = parallel(Z1_meas, Z2_meas) + expected_full_meas = 1 / (1 / Z1_meas + 1 / Z2_meas) + @test result_full_meas isa Measurement{Float64} + @test isapprox(value(result_full_meas), value(expected_full_meas); + atol = TestNumerics.absolute_floor(Float64)) + @test isapprox(uncertainty(result_full_meas), uncertainty(expected_full_meas); + atol = TestNumerics.absolute_floor(Float64)) + + # Fully Promoted Complex Case + Z1_cplx_meas = measurement(3.0, 0.1) + measurement(4.0, 0.2)im + Z2_cplx_meas = measurement(8.0, 0.3) - measurement(6.0, 0.4)im + result_cplx_meas = parallel(Z1_cplx_meas, Z2_cplx_meas) + expected_cplx_meas = 1 / (1 / Z1_cplx_meas + 1 / Z2_cplx_meas) + @test result_cplx_meas isa Complex{Measurement{Float64}} + @test isapprox(value(real(result_cplx_meas)), value(real(expected_cplx_meas)); + atol = TestNumerics.absolute_floor(Float64)) + @test isapprox(value(imag(result_cplx_meas)), value(imag(expected_cplx_meas)); + atol = TestNumerics.absolute_floor(Float64)) + @test isapprox(uncertainty(real(result_cplx_meas)), + uncertainty(real(expected_cplx_meas)); atol = TestNumerics.absolute_floor(Float64)) + @test isapprox(uncertainty(imag(result_cplx_meas)), + uncertainty(imag(expected_cplx_meas)); atol = TestNumerics.absolute_floor(Float64)) + end +end +@testitem "BaseParams / equivalent_alpha / temperature identity" tags=[:unit, :measurements] setup=[ + UseBaseParamsSupport, TestNumerics] begin + @testset "equivalent_alpha: Basic Functionality (Copper & Aluminum)" begin + alpha1 = 0.00393 # Copper + R1 = 0.5 + alpha2 = 0.00403 # Aluminum + R2 = 1.0 + expected = (alpha1 * R2 + alpha2 * R1) / (R1 + R2) + result = equivalent_alpha(alpha1, R1, alpha2, R2) + @test isapprox(result, expected; atol = TestNumerics.absolute_floor(Float64)) + end + + @testset "equivalent_alpha: Edge Case - Zero Resistance" begin + alpha1 = 0.00393 + R1 = 0.0 + alpha2 = 0.00403 + R2 = 1.0 + expected = alpha1 # Only R2 matters + result = equivalent_alpha(alpha1, R1, alpha2, R2) + @test isapprox(result, expected; atol = TestNumerics.absolute_floor(Float64)) + + alpha1 = 0.00393 + R1 = 0.5 + alpha2 = 0.00403 + R2 = 0.0 + expected = alpha2 # Only R1 matters + result = equivalent_alpha(alpha1, R1, alpha2, R2) + @test isapprox(result, expected; atol = TestNumerics.absolute_floor(Float64)) + end + + @testset "equivalent_alpha: Edge Case - Very Large Resistance" begin + alpha1 = 0.00393 + R1 = 1e12 + alpha2 = 0.00403 + R2 = 1.0 + expected = (alpha1 * R2 + alpha2 * R1) / (R1 + R2) + result = equivalent_alpha(alpha1, R1, alpha2, R2) + @test isapprox(result, expected; atol = TestNumerics.absolute_floor(Float64)) + end + + @testset "equivalent_alpha: Type Stability & Promotion" begin + alpha1 = 0.00393 + R1 = 0.5 + alpha2 = 0.00403 + R2 = 1 + result = equivalent_alpha(alpha1, R1, alpha2, R2) + @test typeof(result) == typeof(1.0) + + # Mixed Int/Float + result2 = equivalent_alpha(0, 1, 1, 1.0) + @test typeof(result2) == typeof(1.0) + end + + @testset "equivalent_alpha: Uncertainty Quantification (Measurements.jl)" begin + alpha1 = measurement(0.00393, 1e-5) + R1 = measurement(0.5, 1e-3) + alpha2 = measurement(0.00403, 1e-5) + R2 = measurement(1.0, 1e-3) + result = equivalent_alpha(alpha1, R1, alpha2, R2) + # Check value + expected_val = (value(alpha1) * value(R2) + value(alpha2) * value(R1)) / + (value(R1) + value(R2)) + @test isapprox(value(result), expected_val; atol = TestNumerics.absolute_floor(Float64)) + # Check uncertainty propagation (should be nonzero) + @test uncertainty(result) > 0 + end + + @testset "equivalent_alpha: Equivalent temperature coefficient for parallel resistors" begin + + # Example values (Copper and Aluminum) + alpha1 = 0.00393 # Copper + R1 = 0.5 + alpha2 = 0.00403 # Aluminum + R2 = 1.0 + + expected = (alpha1 * R2 + alpha2 * R1) / (R1 + R2) + + # Edge case: identical conductors + alpha = 0.00393 + R = 1.0 + @test isapprox(equivalent_alpha(alpha, R, alpha, R), alpha; + atol = TestNumerics.absolute_floor(Float64)) + + # Edge case: one resistance much larger than the other + @test isapprox(equivalent_alpha(0.003, 1e6, 0.005, 1.0), + 0.005; atol = TestNumerics.absolute_floor(Float64)) + @test isapprox(equivalent_alpha(0.003, 1.0, 0.005, 1e6), + 0.003; atol = TestNumerics.absolute_floor(Float64)) + + # Type promotion and Measurements.jl propagation + using Measurements: ±, value, uncertainty + m1 = 0.00393 ± 0.00001 + m2 = 0.00403 ± 0.00001 + r1 = 0.5 ± 0.01 + r2 = 1.0 ± 0.01 + + @testset "Type Promotion with Measurements.jl" begin + # Base case: all Float64 + @test equivalent_alpha(alpha1, R1, alpha2, R2) isa Float64 + + # Fully promoted: all Measurement + res = equivalent_alpha(m1, r1, m2, r2) + @test res isa Measurement{Float64} + @test isapprox(value(res), expected; atol = TestNumerics.absolute_floor(Float64)) + # Uncertainty should be nonzero + @test uncertainty(res) > 0 + + # Mixed case 1: first argument is Measurement + res = equivalent_alpha(m1, R1, alpha2, R2) + @test res isa Measurement{Float64} + @test isapprox(value(res), expected; atol = TestNumerics.absolute_floor(Float64)) + + # Mixed case 2: middle argument is Measurement + res = equivalent_alpha(alpha1, r1, alpha2, R2) + @test res isa Measurement{Float64} + @test isapprox(value(res), expected; atol = TestNumerics.absolute_floor(Float64)) + + # Mixed case 3: last argument is Measurement + res = equivalent_alpha(alpha1, R1, alpha2, r2) + @test res isa Measurement{Float64} + @test isapprox(value(res), expected; atol = TestNumerics.absolute_floor(Float64)) + end + + # Physically unusual but valid: zero resistance (should return NaN) + @test isnan(equivalent_alpha(0.003, 0.0, 0.005, 0.0)) + + # Large values + @test isapprox(equivalent_alpha(1e-3, 1e6, 2e-3, 2e6), + (1e-3 * 2e6 + 2e-3 * 1e6) / (1e6 + 2e6); atol = TestNumerics.absolute_floor(Float64)) + end +end # End of test file +@testitem "BaseParams / equivalent_rho / resistivity identity" tags=[:unit, :measurements] setup=[ + UseBaseParamsSupport, TestNumerics] begin + @testset "Basic Functionality" begin + # Example from docstring: `R=0.01` Ω, `r_ext=0.02` m, `r_in=0.01` m + result = equivalent_rho(0.01, 0.02, 0.01) + expected = 0.01 * π * (0.02^2 - 0.01^2) + @test isapprox(result, expected; atol = TestNumerics.absolute_floor(Float64)) + @test result > 0 + end + + @testset "Edge Cases" begin + # Zero resistance + result = equivalent_rho(0.0, 0.02, 0.01) + @test isapprox(result, 0.0; atol = TestNumerics.absolute_floor(Float64)) + # Zero thickness (r_ext == r_in) + result = equivalent_rho(0.01, 0.01, 0.01) + @test isapprox(result, 0.0; atol = TestNumerics.absolute_floor(Float64)) + # Very large radii + result = equivalent_rho(0.01, 1e6, 1e3) + expected = 0.01 * π * (1e6^2 - 1e3^2) + @test isapprox(result, expected; atol = TestNumerics.absolute_floor(Float64)) + # Inf/NaN + @test isnan(equivalent_rho(NaN, 0.02, 0.01)) + @test isnan(equivalent_rho(0.01, NaN, 0.01)) + @test isnan(equivalent_rho(0.01, 0.02, NaN)) + @test isinf(equivalent_rho(Inf, 0.02, 0.01)) + end + + @testset "Numerical Consistency" begin + # Float32 vs Float64 + r = equivalent_rho(Float32(0.01), Float32(0.02), Float32(0.01)) + d = equivalent_rho(0.01, 0.02, 0.01) + @test isapprox(r, d; atol = TestNumerics.absolute_floor(Float64)) + end + + @testset "Physical Behavior" begin + # Increases with resistance + r1 = equivalent_rho(0.01, 0.02, 0.01) + r2 = equivalent_rho(0.02, 0.02, 0.01) + @test r2 > r1 + # Increases with area + r3 = equivalent_rho(0.01, 0.03, 0.01) + @test r3 > r1 + end + + @testset "Type Stability & Promotion" begin + using Measurements + # All Float64 + r1 = equivalent_rho(0.01, 0.02, 0.01) + @test typeof(r1) == Float64 + # All Measurement + r2 = equivalent_rho(measurement(0.01, 1e-4), measurement(0.02, 1e-5), measurement(0.01, 1e-5)) + @test r2 isa Measurement{Float64} + # Mixed inputs with `R` as a `Measurement` + r3 = equivalent_rho(measurement(0.01, 1e-4), 0.02, 0.01) + @test r3 isa Measurement{Float64} + # Mixed: radius_ext_con as Measurement + r4 = equivalent_rho(0.01, measurement(0.02, 1e-5), 0.01) + @test r4 isa Measurement{Float64} + # Mixed: radius_in_con as Measurement + r5 = equivalent_rho(0.01, 0.02, measurement(0.01, 1e-5)) + @test r5 isa Measurement{Float64} + end + + @testset "Uncertainty Quantification" begin + using Measurements + R = measurement(0.01, 1e-4) + r_ext = measurement(0.02, 1e-5) + r_in = measurement(0.01, 1e-5) + result = equivalent_rho(R, r_ext, r_in) + @test result isa Measurement{Float64} + @test uncertainty(result) > 0 + end +end diff --git a/test/unit/datamodel/bounded_uncertainty.jl b/test/unit/datamodel/bounded_uncertainty.jl new file mode 100644 index 000000000..3f6b9b169 --- /dev/null +++ b/test/unit/datamodel/bounded_uncertainty.jl @@ -0,0 +1,127 @@ +@testitem "DataModel / implicit power-cell and strand derivatives" tags=[:unit, :extension, :measurements] begin + using Measurements + const DM = LineCableModels.DataModel + derivative(v, z) = v isa Measurement ? Measurements.derivative(v, z) : 0.0 + function geometry(s, q, shift, angle, dx, count) + boundary = [(-s,-s),(1.3s,-s),(1.3s,0.8s),(-s,0.8s)] + sites = [(-0.55s+shift,-0.2s),(0.65s,-0.1s),(0s,0.5s),(0s,-0.65s)][1:count] + a = DM.signed_polygon_area(boundary) + targets = count == 2 ? [a*q,a*(1-q)] : count == 3 ? + [a*q,0.2a,a*(0.8-q)] : [a*q,0.2a,0.15a,a*(0.65-q)] + transform(p) = (dx+cos(angle)*p[1]-sin(angle)*p[2],sin(angle)*p[1]+cos(angle)*p[2]) + return transform.(boundary), transform.(sites), targets + end + for count in (2,3,4), axis in 1:5 + base = [1.0,0.4,0.0,0.2,0.1] + z = measurement(base[axis],0.01) + values = Any[base...]; values[axis] = z + args = geometry(values...,count) + cells, weights = DM.balance_power_cells(args...) + @test nominal(weights[end]) == 0 + @test derivative(weights[end],z) == 0 + for h in (1e-5,3e-6) + hi, lo = copy(base), copy(base) + hi[axis] += h; lo[axis] -= h + chi = first(DM.balance_power_cells(geometry(hi...,count)...)) + clo = first(DM.balance_power_cells(geometry(lo...,count)...)) + for (i,cell) in enumerate(cells) + a = DM.signed_polygon_area(cell) + @test nominal(a) ≈ nominal(args[3][i]) rtol=1e-9 + @test derivative(a,z) ≈ derivative(args[3][i],z) atol=1e-8 + for (v,p,m) in zip(DM.polygon_centroid(cell), + DM.polygon_centroid(chi[i]),DM.polygon_centroid(clo[i])) + @test derivative(v,z) ≈ (p-m)/(2h) atol=1e-7 rtol=1e-5 + end + end + end + end + function strand_geometry(s,width,a,angle,dx) + ca, sa = cos(angle), sin(angle) + cell = [(dx+ca*x-sa*y,sa*x+ca*y) for (x,y) in + [(-0.2s,-0.5s),(width*s,-0.5s),(width*s,0.5s),(-0.2s,0.5s)]] + return cell, a*s^2 + end + for axis in 1:5, occupancy in (0.4,0.6) + base = [1.0,0.4,occupancy,0.2,0.1] + z = measurement(base[axis],0.001) + values = Any[base...]; values[axis] = z + cell, a = strand_geometry(values...) + # Full occupancy is tested only along support-preserving directions. + occupancy == 0.6 && axis in (2,3) && continue + points = DM.area_preserving_strand(cell,a;angle=values[4]) + for p in points, i in eachindex(cell) + left, right = cell[i],cell[mod1(i+1,length(cell))] + @test nominal((right[1]-left[1])*(p[2]-left[2])- + (right[2]-left[2])*(p[1]-left[1])) >= -1e-12 + end + @test nominal(DM.signed_polygon_area(points)) ≈ nominal(a) atol=1e-10 + @test derivative(DM.signed_polygon_area(points),z) ≈ derivative(a,z) atol=1e-7 + for h in (1e-5,3e-6) + hi, lo = copy(base), copy(base) + hi[axis] += h; lo[axis] -= h + phi = DM.area_preserving_strand(strand_geometry(hi...)...;angle=hi[4]) + plo = DM.area_preserving_strand(strand_geometry(lo...)...;angle=lo[4]) + for (v,p,m) in zip(DM.polygon_centroid(points), + DM.polygon_centroid(phi),DM.polygon_centroid(plo)) + @test derivative(v,z) ≈ (p-m)/(2h) atol=1e-7 rtol=1e-5 + end + end + end + z = measurement(1.0,0.1) + cell = [(-z,-z),(z,-z),(z,z),(-z,z)] + cells, weights = DM.balance_power_cells(cell,[(0.,0.)],[4z^2]) + @test only(cells) == cell + @test only(weights) == 0 + # Inventory transitions are discrete. No continuous derivative claims are + # made across the packing threshold where the next complete course appears. + @test DM.course_count(18-1e-5,1.) == 1 + @test DM.course_count(18+1e-5,1.) == 2 +end + +@testitem "DataModel / bounded construction retains scale pose and fillet derivatives" tags=[:unit, :extension, :measurements] begin + using Measurements + const DM = LineCableModels.DataModel + derivative(v,z) = v isa Measurement ? Measurements.derivative(v,z) : 0.0 + copper = Material(kind=:conductor,rho=1.72e-8) + function design(kind,s,rotation,dx,fillet) + part = if kind === :sector + boundary = resolve(EmptyBoundary(),Sector(span=pi/3, + r_base=fillet*0.006s,r_back=0.006s,fillet=fillet*0.006s)) + _, polygons, _ = DM.sector_courses(boundary,Disk(0.0007s)) + return [resolve(Pose2(dx,0.,rotation),p) for p in polygons] + elseif kind === :rectangle + stranded(copper;center=Disk(0.0002s), + shape=Rectangle(0.0003s,0.0001s),boundary=Disk(0.0006s)) + else + stranded(copper;shape=Disk(0.0002s),boundary=Disk(0.0009s),compact=true) + end + cable = build(CableDesign,"derivative",assembly(at(terminal(:core,part), + Pose2(dx,0.,rotation)))) + return [r.primitive for r in cable.geometry.regions] + end + for kind in (:circle,:sector,:rectangle), axis in 1:4 + kind !== :sector && axis == 4 && continue + base = [1.,0.2,0.001,0.04] + z = measurement(base[axis],0.001) + values = Any[base...]; values[axis] = z + shapes = design(kind,values...) + plain = design(kind,base...) + @test length(shapes) == length(plain) + for (p,p0) in zip(shapes,plain) + @test nominal(area(p)) ≈ area(p0) rtol=1e-9 + @test all(isapprox.(nominal.(centroid(p)),centroid(p0);atol=1e-12)) + end + for h in (1e-5,3e-6) + hi, lo = copy(base), copy(base) + hi[axis] += h; lo[axis] -= h + plus, minus = design(kind,hi...), design(kind,lo...) + @test length(plus) == length(minus) == length(shapes) + for (p,pplus,pminus) in zip(shapes,plus,minus) + @test derivative(area(p),z) ≈ (area(pplus)-area(pminus))/(2h) atol=1e-12 rtol=1e-5 + for (v,vp,vm) in zip(centroid(p),centroid(pplus),centroid(pminus)) + @test derivative(v,z) ≈ (vp-vm)/(2h) atol=2e-8 rtol=1e-4 + end + end + end + end +end diff --git a/test/unit/datamodel/clearance.jl b/test/unit/datamodel/clearance.jl new file mode 100644 index 000000000..55a917cb2 --- /dev/null +++ b/test/unit/datamodel/clearance.jl @@ -0,0 +1,217 @@ +@testmodule ClearanceFixtures begin + using LineCableModels + + function cable(radius = 0.01; offset = Pose2(0, 0)) + copper = Material(kind = :conductor, rho = 1.7e-8) + return build(CableDesign, "clearance-test", + Group(:core, Region(:metal, Disk(radius), copper); at = offset)) + end + + function pair(radius, separation; y = -1.0) + design = cable(radius) + return build(LineCableSystem, [design, design], + [Pose2(-separation / 2, y), Pose2(separation / 2, y)]; + connections = [(core = 1,), (core = 2,)]) + end + + function gaps(system) + shapes = [resolve(pose, boundary(design.geometry)) + for (pose, design) in zip(system.positions, system.designs)] + [hypot(centroid(shapes[i])[1] - centroid(shapes[j])[1], + centroid(shapes[i])[2] - centroid(shapes[j])[2]) - + shapes[i].r - shapes[j].r + for i in eachindex(system.positions) for j in 1:(i - 1)] + end +end + +@testitem "DataModel / exterior clearance / touching, overlap and reconstruction" tags=[:unit, :importexport] setup=[ClearanceFixtures] begin + using LineCableModels + import LineCableModels.ImportExport as IE + fixture=ClearanceFixtures + design=fixture.cable() + declared=trefoil(design; center = at(0.0, -1.0), spacing = 0.02, + connections = (core = (1, 2, 3),)) + system=@test_logs (:warn, r"Cable placements adjusted") build(LineCableSystem, declared) + @test all(>=(1.0e-6), fixture.gaps(system)) + @test maximum(fixture.gaps(system)) - minimum(fixture.gaps(system)) < 1e-14 + @test sum(p -> p.x, system.positions) ≈ sum(p -> p.x, system.input_positions) atol=1e-15 + @test sum(p -> p.y, system.positions) ≈ sum(p -> p.y, system.input_positions) + @test getproperty.(declared, :pose) == system.input_positions + @test all(==(design), system.designs) + @test validate(system) === system + rebuilt=@test_logs build(LineCableSystem, system.designs, system.positions; + connections = system.connections) + @test rebuilt.positions == system.positions + decoded=@test_logs IE.deserialize_value(IE.serialize_value(system)) + @test decoded.positions == system.positions + @test decoded.input_positions == system.input_positions + @test decoded.clearances == system.clearances + + + reversed=@test_logs (:warn, r"Cable placements adjusted") build(LineCableSystem, reverse(declared)) + @test getproperty.(reverse(reversed.positions), :x) ≈ getproperty.(system.positions, :x) atol=1e-14 + @test getproperty.(reverse(reversed.positions), :y) ≈ getproperty.(system.positions, :y) atol=1e-14 + @test_logs fixture.pair(0.01, 0.03) + @test_logs (:warn, r"Cable placements adjusted") fixture.pair(0.01, prevfloat(0.02)) + @test_throws DomainError fixture.pair(0.01, 0.02 - 1e-10) + @test_throws DomainError fixture.pair(0.01, 0.0) + + # Several neighbors must be resolved together, not by a one-pass pair push. + line=@test_logs (:warn, r"Cable placements adjusted") build(LineCableSystem, + fill(design, 5), [Pose2(0.02i, -1) for i in 1:5]) + @test minimum(fixture.gaps(line)) >= 1e-6 + line.positions[2]=line.positions[1] + @test_throws DomainError validate(line) + + # A tangent interface is resolved at problem construction, where earth is known. + neutral=build(LineCableSystem, design, Pose2(0, -0.01)) + problem=@test_logs (:warn, r"Cable placements adjusted") LineParametersProblem( + neutral; earth_props = homogeneous(rho = 100.0), frequencies = [50.0]) + @test -only(problem.system.positions).y - outer_radius(design) >= 1e-6 + @test only(neutral.positions).y == -0.01 + @test_throws DomainError LineParametersProblem( + build(LineCableSystem, design, Pose2(0, -0.009)); + earth_props = homogeneous(rho = 100.0), frequencies = [50.0]) + small_precision=@test_logs (:warn, r"Cable placements adjusted") fixture.pair( + 0.01f0, 0.02f0; y = -1.0f0) + @test only(fixture.gaps(small_precision)) >= 1e-6 +end + +@testitem "DataModel / exterior clearance / propagated radii and correlated at coordinates" tags=[:extension, :measurements] setup=[ClearanceFixtures] begin + using LineCableModels + using Measurements: measurement, derivative + fixture=ClearanceFixtures + radius=measurement(0.01, 1e-4) + shared_shift=measurement(0.3, 0.004) + design=fixture.cable(radius) + declarations=[ + (@at design (shared_shift - radius, -1.0) core=1), + (@at design (shared_shift + radius, -1.0) core=2) + ] + system=@test_logs (:warn, r"Cable placements adjusted") build(LineCableSystem, declarations) + @test nominal(system.clearances[1, 2]) ≈ 1e-6 + uncertainty(radius) + @test nominal(only(fixture.gaps(system))) >= nominal(system.clearances[1, 2]) + @test uncertainty(only(fixture.gaps(system))) < 1e-14 + @test derivative(sum(p -> p.x, system.positions) / 2, shared_shift) ≈ 1 + @test derivative(system.positions[2].x - system.positions[1].x, radius) ≈ 2 + + independent=measurement(0.02, 0.003) + positional=@test_logs (:warn, r"Cable placements adjusted") fixture.pair(0.01, independent) + @test nominal(positional.clearances[1, 2]) ≈ 1e-6 + 0.003 + @test uncertainty(only(fixture.gaps(positional))) < 1e-14 + + left=fixture.cable(measurement(0.01, 1e-4)) + right=fixture.cable(measurement(0.01, 2e-4)) + radii=@test_logs (:warn, r"Cable placements adjusted") build(LineCableSystem, + [left, right], [Pose2(-0.01, -1), Pose2(0.01, -1)]) + @test nominal(radii.clearances[1, 2]) ≈ 1e-6 + hypot(1e-4, 2e-4) + + # Local cross-section poses contribute to the exterior envelope as well. + local_offset=measurement(0.002, 5e-4) + eccentric=fixture.cable(0.01; offset = Pose2(local_offset, 0)) + @test uncertainty(outer_radius(eccentric)) ≈ 5e-4 + local_system=@test_logs (:warn, r"Cable placements adjusted") build(LineCableSystem, + [eccentric, eccentric], [Pose2(-0.01, -1), Pose2(0.01, -1)]) + @test nominal(local_system.clearances[1, 2]) ≈ 1e-6 + 0.0005 + @test nominal(only(fixture.gaps(local_system))) >= + nominal(local_system.clearances[1, 2]) + zero_offset=fixture.cable(0.01; offset = Pose2(measurement(0.0, 5e-4), 0)) + zero_system=@test_logs (:warn, r"Cable placements adjusted") build(LineCableSystem, + [zero_offset, zero_offset], [Pose2(-0.01, -1), Pose2(0.01, -1)]) + @test all(isfinite, nominal.(zero_system.clearances)) + @test nominal(zero_system.clearances[1, 2]) >= 1e-6 + 0.0005 + @test isfinite(uncertainty(only(fixture.gaps(zero_system)))) + compensated=fixture.cable(radius; + offset = Pose2(0.002-(radius-nominal(radius)), 0)) + @test uncertainty(outer_radius(compensated)) < 1e-14 + compensated_system=@test_logs (:warn, r"Cable placements adjusted") build( + LineCableSystem, + [compensated, compensated], [Pose2(-radius, -1), Pose2(radius, -1)]) + @test nominal(compensated_system.clearances[1, 2]) ≈ 1e-6 + uncertainty(radius) +end + +@testitem "UQ / exterior clearance / retained budgets, extreme draws and bounded warnings" tags=[:integration, :measurements] setup=[ClearanceFixtures] begin + using LineCableModels + using Measurements + using Random + fixture=ClearanceFixtures + DM=LineCableModels.DataModel + space=Gridspace{LineCableSystem}( + (radius, spacing)->fixture.pair(radius, spacing), + (Grid(0.01, AbsoluteError(1e-4)), Grid(0.021, AbsoluteError(0.004)))) + point=only(LineCableModels.points(space)) + context=DM.collect_clearance_requirements(point) + @test context.records[1].clearances[1, 2] > 0.004 + @test DM._CLEARANCE_CONTEXT[] === nothing + for (radius, spacing) in ((0.0108, 0.015), (0.0112, 0.006), (0.009, 0.045), (0.01, 0.0)) + result=@test_logs DM.with_clearance(context) do + LineCableModels.realize(point, (radius, spacing)) + end + @test only(fixture.gaps(result)) >= context.records[1].clearances[1, 2] + @test result.clearances == context.records[1].clearances + end + @test context.adjustments[] == 3 + @test DM._CLEARANCE_CONTEXT[] === nothing + @test_throws DomainError DM.with_clearance(context) do + LineCableModels.realize(point, (-0.01, 0.02)) + end + @test DM._CLEARANCE_CONTEXT[] === nothing + @test_logs (:warn, r"Sampled cable placements adjusted") rand(MersenneTwister(8), space) + zipped=Gridspace{LineCableSystem}(space.build, space.grids; combine = :zip) + @test_logs (:warn, r"Sampled cable placements adjusted") rand(MersenneTwister(8), zipped) + + # Even a draw that swaps air-earth sides must remain pairwise feasible. + design=fixture.cable() + reference=[Pose2(0, 0.1), Pose2(0, -0.1)] + required=[0.001 0.1; 0.1 0.001] + resolved, _, _=DM.clearance_geometry([design, design], + [Pose2(0, -0.1), Pose2(0, 0.1)]; reference, required, + interface = true, sampling = true) + @test resolved[1].y > 0 && resolved[2].y < 0 + @test abs(resolved[1].y - resolved[2].y) - 0.02 >= 0.1 + + # Retain the side of the composed local @at, not the sampled local offset. + # An unrelated second system with the same default ID must not replace it. + local_space=Gridspace{LineParametersProblem}( + offset->begin + local_design=fixture.cable(0.001; offset = Pose2(0, offset)) + buried=build(LineCableSystem, local_design, Pose2(0, -0.01)) + build(LineCableSystem, local_design, Pose2(0, 0.3)) + LineParametersProblem(buried; earth_props = homogeneous(rho = 100.0), + frequencies = [50.0]) + end, + (Grid(0.0, AbsoluteError(0.05)),)) + local_point=only(LineCableModels.points(local_space)) + local_context=DM.collect_clearance_requirements(local_point) + local_problem=@test_logs DM.with_clearance(local_context) do + LineCableModels.realize(local_point, (0.15,)) + end + exterior=resolve(only(local_problem.system.positions), + boundary(only(local_problem.system.designs).geometry)) + @test -centroid(exterior)[2] - exterior.r >= 0.050001 + @test length(local_context.references) <= 3 + + # Exercise the actual MC loop with owned engine results and forced inward draws. + constructed=LineCableSystem[] + problems=Gridspace{LineParametersProblem}( + spacing->begin + system=fixture.pair(0.01, spacing) + push!(constructed, system) + LineParametersProblem(system; earth_props = homogeneous(rho = 100.0), + frequencies = [50.0]) + end, + (Grid(0.021, AbsoluteError(0.002)),)) + inner=Formulation(options = (kron_reduction = false, + reduce_bundle = false, ideal_transposition = false)) + formulation=MonteCarlo(inner; trials = 3, seed = 8, + distribution = (_rng, mean, sigma)->mean-4sigma, + options = (retain_details = true,)) + sampled=@test_logs (:warn, r"Sampled cable placements adjusted") compute( + ParametricProblem(problems), formulation) + @test sampled.trial_counts == [3] + @test isempty(only(sampled.details.data.failures)) + @test only(sampled.details.data.clearance).adjustments == 3 + @test length(constructed) == 4 # one uncertain geometry, three draws + @test all(system -> nominal(only(fixture.gaps(system))) >= 0.002001, constructed) + @test DM._CLEARANCE_CONTEXT[] === nothing +end diff --git a/test/unit/datamodel/geometry_v1.jl b/test/unit/datamodel/geometry_v1.jl new file mode 100644 index 000000000..a54591cbb --- /dev/null +++ b/test/unit/datamodel/geometry_v1.jl @@ -0,0 +1,153 @@ +@testitem "DataModel / v1 geometry / primitive and resolved geometry" tags=[:unit, :parametric] begin + const DM=LineCableModels.DataModel + + @test DM.Disk(2) isa DM.AbstractPrimitive{Float64} + @test DM.Rectangle(4.0f0, 2) isa DM.Rectangle{Float32} + @test DM.Ellipse(3, 2) isa DM.Ellipse{Float64} + @test DM.Sector(span = π / 2, r_base = 0.2, r_back = 2, fillet = 0.1) isa DM.Sector + @test DM.Annulus(1, 2) isa DM.Annulus + @test DM.Shell(0.5) isa DM.Shell + @test_throws DomainError DM.Disk(0) + @test_throws DomainError DM.Rectangle(-1, 2) + @test_throws DomainError DM.Ellipse(1, Inf) + @test_throws DomainError DM.Sector(span = 0, r_base = 0.2, r_back = 2) + @test_throws DomainError DM.Sector(span = 1, r_base = -1, r_back = 2) + @test_throws DomainError DM.Annulus(2, 1) + @test_throws DomainError DM.Shell(0) + + disk=@inferred DM.resolve(DM.EmptyBoundary(), DM.Disk(2.0)) + @test disk isa DM.Disk + @test DM.boundary(disk) === disk + @test DM.area(disk) ≈ 4π + @test DM.centroid(disk) == (0.0, 0.0) + @test DM.support(disk, 0.37) == 2.0 + @test DM.r_in(disk) == 0.0 + @test DM.r_ex(disk) == 2.0 + @test DM.thickness(disk) == 2.0 + + annulus=@inferred DM.resolve(disk, DM.Shell(0.5)) + @test annulus == DM.Annulus(2.0, 2.5) + @test DM.boundary(annulus) == DM.Disk(2.5) + @test DM.area(annulus) ≈ π * (2.5^2 - 2.0^2) + @test DM.r_in(annulus) == 2.0 + @test DM.r_ex(annulus) == 2.5 + @test DM.thickness(annulus) == 0.5 + @test_throws MethodError DM.resolve( + DM.resolve(DM.EmptyBoundary(), DM.Rectangle(2, 1)), + DM.Shell(0.1) + ) + + rectangle=DM.resolve(DM.EmptyBoundary(), DM.Rectangle(4, 2)) + @test DM.area(rectangle) == 8.0 + @test DM.centroid(rectangle) == (0.0, 0.0) + @test DM.support(rectangle, 0) == 2.0 + @test DM.support(rectangle, π / 2) ≈ 1.0 + @test !hasmethod(DM.r_in, Tuple{typeof(rectangle)}) + @test_throws MethodError DM.r_in(rectangle) + + ellipse=DM.resolve(DM.EmptyBoundary(), DM.Ellipse(3, 2)) + @test DM.area(ellipse) ≈ 6π + @test DM.perimeter(ellipse) ≈ 15.865439589290588 + @test DM.support(ellipse, 0) == 3.0 + @test DM.support(ellipse, π / 2) ≈ 2.0 + ellipse_shell=DM.resolve(ellipse, DM.Shell(0.5)) + @test ellipse_shell isa DM.ShellShape + @test ellipse_shell.outer isa DM.EllipseOffset + @test DM.support(ellipse_shell.outer, 0) ≈ 3.5 + @test DM.support(ellipse_shell.outer, π / 2) ≈ 2.5 + @test DM.area(ellipse_shell.outer) ≈ + DM.area(ellipse) + DM.perimeter(ellipse) * 0.5 + π * 0.5^2 + @test DM.perimeter(ellipse_shell.outer) ≈ DM.perimeter(ellipse) + π + @test DM.centroid(ellipse_shell.outer) == DM.centroid(ellipse) + @test length(DM.tessellate(ellipse_shell.outer; points_per_arc = 64)) == 64 + + sector=DM.resolve(DM.EmptyBoundary(), + DM.Sector(span = π / 2, r_base = 0.2, r_back = 2, fillet = 0.1)) + @test DM.area(sector) > 0 + @test DM.r_in(sector) == 0.2 + @test DM.r_ex(sector) == 2.0 + @test DM.support(sector, 0) ≈ 2.0 + @test DM.centroid(sector)[1] > 0 + @test abs(DM.centroid(sector)[2]) <= 16eps(Float64) + + polygon=DM.Polygon(((0, 0), (2, 0), (2, 1), (0, 1))) + resolved_polygon=DM.resolve(DM.EmptyBoundary(), polygon) + @test DM.area(resolved_polygon) == 2.0 + @test DM.centroid(resolved_polygon) == (1.0, 0.5) + @test DM.support(resolved_polygon, 0) == 2.0 + @test_throws DomainError DM.Polygon(((0, 0), (1, 0), (2, 0))) + + converted=convert( + DM.AbstractPrimitive{Float32}, + DM.Rectangle(4.0, 2.0) + ) + @test converted isa DM.Rectangle{Float32} + @test converted.w === 4.0f0 +end + +@testitem "DataModel / v1 geometry / pose and Gridspace materialization" tags=[:unit, :parametric] begin + using Random + const DM=LineCableModels.DataModel + const PB=LineCableModels.ParametricBuilder + + parent=DM.Pose2(1, 2, π / 2) + child=DM.Pose2(3, 0, π / 2) + composed=parent * child + @test composed.x ≈ 1.0 + @test composed.y ≈ 5.0 + @test composed.φ ≈ π + # A pose rotates a local point, then translates it. + @test parent((0.0, 0.0)) == (1.0, 2.0) + @test collect(parent((1.0, 0.0))) ≈ [1.0, 3.0] + @test collect(composed((2.0, -1.0))) ≈ collect(parent(child((2.0, -1.0)))) + + placed=DM.resolve(parent, DM.Rectangle(4, 2)) + @test DM.area(placed) == 8.0 + @test DM.centroid(placed) == (1.0, 2.0) + @test DM.support(placed, 0) ≈ 2.0 + + poses=PB.Pose2(x = PB.Grid((0.0, 1.0)), y = 2.0, φ = PB.Grid((0.0, π / 2))) + @test poses isa PB.Gridspace{DM.Pose2} + @test length(poses) == 4 + @test all(pose -> pose isa DM.Pose2, poses) + + disks=PB.Disk(PB.Grid((1.0f0, 2.0f0))) + @test disks isa PB.Gridspace{DM.Disk} + @test collect(disks) == collect(DM.Disk.((1.0f0, 2.0f0))) + @test rand(MersenneTwister(7), disks) in collect(disks) + + rectangles=PB.Rectangle(PB.Grid((1.0, 2.0)), 3.0) + @test rectangles isa PB.Gridspace{DM.Rectangle} + @test collect(rectangles) == + [DM.Rectangle(1.0, 3.0), DM.Rectangle(2.0, 3.0)] +end + +@testitem "DataModel / geometry / independent SI primitive controls" tags=[:unit, :datamodel] begin + using QuadGK + DM=LineCableModels.DataModel + pose=Pose2(.02,-.03,pi/6) + ellipse_perimeter,error=quadgk(t->hypot(.006*sin(t),.003*cos(t)),0.0,2pi;rtol=1e-12) + cases=((Disk(.005),pi*.005^2,2pi*.005), + (Annulus(.005,.01),pi*(.01^2-.005^2),2pi*(.01+.005)), + (Rectangle(.012,.004),.012*.004,2*(.012+.004)), + (Ellipse(.006,.003),pi*.006*.003,ellipse_perimeter)) + @test error<=1e-10*ellipse_perimeter/4 + for (primitive,expected_area,expected_perimeter) in cases + shape=resolve(EmptyBoundary(),primitive) + @test area(shape) ≈ expected_area rtol=1e-10 atol=0 + @test perimeter(shape) ≈ expected_perimeter rtol=1e-10 atol=0 + placed=resolve(pose,shape) + @test area(placed) ≈ expected_area rtol=1e-10 + @test perimeter(placed) ≈ expected_perimeter rtol=1e-10 + @test collect(centroid(placed)) ≈ [pose.x,pose.y] rtol=1e-10 + for phi in (0.0,pi/7,pi/2,pi) + @test support(placed,phi) ≈ pose.x*cos(phi)+pose.y*sin(phi)+support(shape,phi-pose.φ) rtol=1e-10 + end + end + BP=DM.BaseParams + for temperature in (20.0,70.0) + rho=2e-8*(1+.004*(temperature-20)) + @test BP.tubular_resistance(0.0,.005,rho) ≈ rho/(pi*.005^2) rtol=1e-10 + end + @test BP.tubular_gmr(.005,0.0,1.0) ≈ .005exp(-.25) rtol=1e-10 +end diff --git a/test/unit/datamodel/line_cable_system.jl b/test/unit/datamodel/line_cable_system.jl new file mode 100644 index 000000000..aeab3a4be --- /dev/null +++ b/test/unit/datamodel/line_cable_system.jl @@ -0,0 +1,68 @@ +@testitem "DataModel / LineCableSystem / completed immutable state" tags=[:unit, :parametric] setup=[ + UseDataModelSupport, + TestFixtures +] begin + design=TestFixtures.coaxial_design() + mapping(phase) = Dict( + terminal=>(index==1 ? phase : 0) + for (index, terminal) in enumerate(design.terminal_order) + ) + system=build( + LineCableSystem, + [design, design], + [Pose2(0.0, -1.0, 0.0), Pose2(0.1, -1.0, 0.0)]; + connections = [mapping(1), mapping(2)], + system_id = "line", + line_length = 1000.0 + ) + + @test system isa LineCableSystem{Float64} + @test ncables(system) == 2 + @test nphases(system) == 2 + @test system.designs == [design, design] + @test system.positions == [Pose2(0.0, -1.0, 0.0), Pose2(0.1, -1.0, 0.0)] + @test system.connection_order == vcat( + [mapping(1)[terminal] for terminal in design.terminal_order], + [mapping(2)[terminal] for terminal in design.terminal_order] + ) + @test length(system.geometry) == 2 * length(design.geometry.regions) + @test validate(system) === system + + neutral=build( + LineCableSystem, + design, + Pose2(0.0, 0.0, 0.0); + environment = nothing, + system_id = "neutral" + ) + @test only(neutral.positions) == Pose2(0.0, 0.0, 0.0) + @test neutral.environment === nothing + @test_throws DomainError build( + LineCableSystem, + design, + Pose2(0.0, 0.0, 0.0); + environment = :interface + ) + + @test_throws DomainError build( + LineCableSystem, + [design, design], + [Pose2(0.0, -1.0, 0.0), Pose2(0.001, -1.0, 0.0)]; + connections = [mapping(1), mapping(2)] + ) + @test_throws DimensionMismatch build( + LineCableSystem, + [design, design], + [Pose2(0.0, -1.0, 0.0)] + ) + + @test_throws DomainError build( + LineCableSystem, design, Pose2(0.0, -1.0, 0.0); line_length = 0.0 + ) + @test_throws ArgumentError build( + LineCableSystem, + design, + Pose2(0.0, -1.0, 0.0); + system_id = "" + ) +end diff --git a/test/unit/datamodel/physical_tree_v1.jl b/test/unit/datamodel/physical_tree_v1.jl new file mode 100644 index 000000000..b702195f6 --- /dev/null +++ b/test/unit/datamodel/physical_tree_v1.jl @@ -0,0 +1,525 @@ +@testitem "DataModel / v1 physical tree / Region and Stack" tags=[:unit, :parametric] begin + const DM=LineCableModels.DataModel + const PB=LineCableModels.ParametricBuilder + copper_props=LineCableModels.Material(kind = :conductor, rho = 1.7241e-8) + insulator_props=LineCableModels.Material( + kind = :insulator, rho = 1.0e14, eps_r = 2.3 + ) + + core=DM.Region(:core, DM.Disk(0.01), copper_props) + insulation=DM.Region(:insulation, DM.Shell(0.005), insulator_props) + @test core isa DM.AbstractCablePart + @test core.tag === :core + @test core.material === copper_props + @test (@inferred DM.resolve(DM.EmptyBoundary(), core)).regions[1].primitive == + DM.Disk(0.01) + @test_throws ArgumentError DM.Region(Symbol(""), DM.Disk(1), copper_props) + @test_throws ArgumentError DM.Region(:bad, DM.Disk(1), :copper) + + stack=PB.Stack(core, insulation) + @test stack.items == DM.AbstractCablePart[core, insulation] + resolved=DM.resolve(DM.EmptyBoundary(), stack) + @test length(resolved.regions) == 2 + @test resolved.regions[1].primitive == DM.Disk(0.01) + @test resolved.regions[2].primitive == DM.Annulus(0.01, 0.015) + @test DM.boundary(resolved) == DM.Disk(0.015) + @test_throws ArgumentError DM.Stack(DM.AbstractCablePart[]) + @test_throws MethodError DM.resolve(DM.EmptyBoundary(), DM.Stack(insulation)) + + nested=PB.Stack(PB.Stack(core), insulation) + nested_resolved=DM.resolve(DM.EmptyBoundary(), nested) + @test DM.boundary(nested_resolved) == DM.Disk(0.015) + + radii=PB.Grid((0.01, 0.02)) + cores=PB.Region(:core, PB.Disk(radii), copper_props) + spaces=PB.Stack(cores, insulation) + @test spaces isa PB.Gridspace{DM.Stack} + @test length(spaces) == 2 + @test all(space -> space isa DM.Stack, spaces) + @test DM.r_ex(DM.boundary(DM.resolve(DM.EmptyBoundary(), first(spaces)))) == 0.015 +end + +@testitem "DataModel / v1 physical tree / primitive extension is local dispatch" tags=[:unit, :parametric] begin + import LineCableModels.DataModel as DM + + struct CapsuleDefinition{T <: Real} <: DM.AbstractPrimitive{T} + radius::T + end + struct Capsule{T <: Real} <: DM.AbstractPrimitive{T} + radius::T + at::DM.Pose2{T} + end + + DM.resolve(::DM.EmptyBoundary, + definition::CapsuleDefinition) = Capsule( + definition.radius, + DM.Pose2(0, 0, 0) + ) + DM.resolve(at::DM.Pose2, primitive::Capsule) = Capsule(primitive.radius, at * + primitive.at) + DM.boundary(primitive::Capsule) = primitive + DM.area(primitive::Capsule) = pi * primitive.radius^2 + DM.centroid(primitive::Capsule) = (primitive.at.x, primitive.at.y) + DM.support(primitive::Capsule, + φ::Real) = primitive.at.x * cos(φ) + + primitive.at.y * sin(φ) + primitive.radius + DM.support(primitive::Capsule) = hypot(primitive.at.x, primitive.at.y) + + primitive.radius + + conductor=Material(kind = :conductor, rho = 1.7241e-8) + design=build( + CableDesign, + "local-extension", + Group(:core, Region(:capsule, CapsuleDefinition(0.01), conductor)) + ) + @test only(design.geometry.regions).primitive == + Capsule(0.01, DM.Pose2(0, 0, 0)) + @test design.terminal_order == [:core] +end + +@testitem "DataModel / v1 physical tree / natural and compacted circular strands" tags=[:unit, :parametric] begin + copper=Material(kind = :conductor, rho = 1.7241e-8) + disk=Disk(0.5e-3) + natural_boundary=Disk(5disk.r) + compact_boundary=Disk(sqrt(19) * disk.r) + + natural=stranded( + copper; + shape = disk, + lay = (LayRatio(15), LayRatio(11)), + boundary = natural_boundary + ) + compacted=stranded( + copper; + shape = disk, + compact = true, + boundary = compact_boundary + ) + @test natural isa Enclosure + declarations=natural.item.item.items + @test all(item -> item isa Group, declarations) + @test getproperty.(declarations, :name) == fill(:strand, 2) + @test declarations[1].path === nothing + @test declarations[2].path == ( + Helix(LayRatio(15)), Helix(LayRatio(11)) + ) + @test all(item -> item.pattern === nothing, declarations) + + natural_design=build( + CableDesign, "natural-circular-strands", terminal(:core, natural) + ) + compacted_design=build( + CableDesign, "compacted-circular-strands", terminal(:core, compacted) + ) + natural_conductors=filter( + region -> region.source.material.kind === :conductor, + natural_design.geometry.regions + ) + @test length(natural_conductors) == 19 + @test all(region -> region.primitive isa Disk, natural_conductors) + @test all(region -> region.primitive isa LineCableModels.DataModel.Polygon, + compacted_design.geometry.regions) + @test sum(area, getproperty.(natural_conductors, :primitive)) ≈ + 19pi * disk.r^2 + + strand_centers=centroid.(getproperty.( + natural_conductors, + :primitive + )) + @test hypot(first(strand_centers)...) <= 1e-15 + @test length(unique(strand_centers)) == 19 + @test [last(region.placement.patterns).member + for region in natural_conductors] == collect(1:19) + flattened_circular=only(LineCableModels.DataModel.flatten( + natural_design, + 50.0 + )).conductor + resistances=[copper.rho / area(disk) * prod( + entry -> overlength(entry.path, entry.radius), + region.paths; + init = 1.0 + ) for region in natural_conductors] + weights=inv.(resistances) ./ sum(inv, resistances) + log_gmr=sum(eachindex(strand_centers)) do left + value=weights[left]^2 * log(disk.r * exp(-copper.mu_r / 4)) + for right in (left + 1):length(strand_centers) + value += 2weights[left] * weights[right] * + log(hypot( + strand_centers[left][1] - strand_centers[right][1], + strand_centers[left][2] - strand_centers[right][2] + )) + end + value + end + @test flattened_circular.gmr ≈ exp(log_gmr) + @test_throws DimensionMismatch build( + CableDesign, + "wrong-schedule", + terminal(:core, stranded( + copper; shape = disk, lay = (LayRatio(11),), boundary = natural_boundary + )) + ) + @test_throws ArgumentError stranded( + copper; + shape = Ellipse(0.5e-3, 0.25e-3), + boundary = Disk(2e-3) + ) + @test stranded( + copper; + center = disk, + shape = Rectangle(0.35e-3, 0.8e-3), + boundary = Disk(2e-3) + ) isa Group +end + +@testitem "DataModel / v1 physical tree / Enclosure and class conveniences" tags=[:unit, :parametric] begin + const DM=LineCableModels.DataModel + copper=LineCableModels.Material(kind = :conductor, rho = 1.7241e-8) + oil=LineCableModels.Material(kind = :insulator, rho = 1.0e12, eps_r = 2.2) + semicon=LineCableModels.Material(kind = :semicon, rho = 1000.0, eps_r = 1000.0) + + core_region=LineCableModels.core(copper; r = 1.0, tag = :core_metal) + core=LineCableModels.Group(:core, core_region) + wall=LineCableModels.sheath(copper; t = 0.5, tag = :pipe_wall) + pipe=LineCableModels.Enclosure( + :pipe, + core; + primitive = LineCableModels.Disk(3.0), + fill = oil, + wall + ) + resolved=DM.resolve(DM.EmptyBoundary(), pipe) + @test unique(filter(!isnothing, getproperty.(resolved.regions, :terminal))) == [:core] + @test getproperty.(getproperty.(resolved.regions, :source), :tag) == + [:core_metal, :pipe_fill, :pipe_wall] + @test DM.support(DM.boundary(resolved)) == 3.5 + @test resolved.regions[2].primitive == DM.Annulus(1.0, 3.0) + + insulation=LineCableModels.insulation(oil; t = 0.2, tag = :insulation) + screen=LineCableModels.screen(semicon; t = 0.1, tag = :screen) + @test insulation.material.kind === :insulator + @test screen.material.kind === :semicon + @test_throws ArgumentError LineCableModels.core(oil; r = 1.0, tag = :bad) + @test_throws ArgumentError LineCableModels.insulation(copper; t = 1.0, tag = :bad) + @test_throws ArgumentError LineCableModels.screen(oil; t = 1.0, tag = :bad) + + fill=LineCableModels.filler( + oil, + LineCableModels.Annulus(1.0, 3.0); + tag = :fill + ) + explicit=LineCableModels.Enclosure( + :duct, + core; + primitive = LineCableModels.Disk(3.0), + fill + ) + @test getproperty.( + getproperty.(DM.resolve(DM.EmptyBoundary(), explicit).regions, :source), + :tag + ) == [:core_metal, :fill] + + materials=LineCableModels.Grid((copper, oil)) + varying=LineCableModels.core(materials; r = 1.0, tag = :varying) + @test varying isa LineCableModels.Gridspace{DM.Region} + @test first(varying).material === copper + @test_throws ArgumentError collect(varying) + + nested=LineCableModels.Enclosure( + :outer, + pipe; + primitive = LineCableModels.Disk(5.0), + fill = oil + ) + @test DM.support(DM.boundary(DM.resolve(DM.EmptyBoundary(), nested))) == 5.0 +end + +@testitem "DataModel / v1 physical tree / Enclosure material occupancy" tags=[:unit, :importexport] begin + const DM=LineCableModels.DataModel + copper=Material(kind = :conductor, rho = 1.7241e-8) + matrix=Material(kind = :insulator, rho = Inf, eps_r = 2.3, mu_r = 1.0) + insulation_material=Material(kind = :insulator, rho = 1.0e12, eps_r = 2.4) + bundle=terminal( + :core, + stranded( + copper; + shape = Disk(0.5), + boundary = Disk(1.5), + fill = matrix + ) + ) + resolved=DM.resolve(DM.EmptyBoundary(), bundle) + fill=last(resolved.regions) + + @test length(resolved.regions) == 8 + @test fill.primitive isa DM.DifferenceShape + @test length(fill.primitive.holes) == 7 + @test DM.area(fill.primitive) ≈ π * 1.5^2 - 7π * 0.5^2 + @test all(region -> region.terminal === :core, resolved.regions[1:7]) + @test fill.terminal === nothing + @test DM.boundary(resolved) == Disk(1.5) + + annular=Enclosure( + :annular_matrix, + Region(:occupied_annulus, Annulus(0.5, 1.0), copper); + primitive = Disk(1.5), + fill = matrix + ) + annular_fill=last(DM.resolve(DM.EmptyBoundary(), annular).regions) + @test annular_fill.primitive isa DM.DifferenceShape + @test DM.area(annular_fill.primitive) ≈ + π * 1.5^2 - π * (1.0^2 - 0.5^2) + + armor=Group( + :armor, + Region(:armor_wires, Disk(0.1), copper); + pattern = Ring(6; r = 1.1) + ) + annular_stage=Enclosure( + :armor_matrix, + armor; + primitive = Annulus(1.0, 1.2), + fill = matrix + ) + staged_design=build( + CableDesign, + "annular-filled-stage", + Group(:core, Region(:inner_core, Disk(1.0), copper)), + annular_stage + ) + staged_fill=last(staged_design.geometry.regions) + @test staged_fill.primitive isa DM.DifferenceShape + @test staged_fill.primitive.outer isa Annulus + @test length(staged_fill.primitive.holes) == 6 + @test sum(DM.area, staged_design.geometry.regions) ≈ + DM.area(staged_design.geometry.outer) + @test DM.boundary(staged_design.geometry) == Disk(1.2) + + discontinuous_stage=Enclosure( + :discontinuous_matrix, + armor; + primitive = Annulus(1.01, 1.21), + fill = matrix + ) + continued = build( + CableDesign, + "discontinuous-annular-stage", + Group(:core, Region(:inner_core, Disk(1.0), copper)), + discontinuous_stage + ) + @test last(continued.geometry.regions).primitive.outer.ri == 1.0 + @test sum(DM.area, continued.geometry.regions) ≈ DM.area(continued.geometry.outer) + + bare_design=build( + CableDesign, + "bare-bundle", + terminal( + :core, + stranded( + copper; + shape = Disk(0.5), + boundary = Disk(1.5) + ) + ) + ) + packed_design=build(CableDesign, "packed-bundle", bundle) + bare=only(DM.flatten(bare_design, 50.0)) + reduced=only(DM.flatten(packed_design, 50.0)) + @test reduced.conductor == bare.conductor + @test reduced.dielectric == bare.dielectric + + IE=LineCableModels.ImportExport + restored=IE.deserialize_value(IE.serialize_value(packed_design)) + @test restored == packed_design + restored_fill=last(restored.geometry.regions).primitive + @test restored_fill isa DM.DifferenceShape + @test length(restored_fill.holes) == 7 + + insulated=build( + CableDesign, + "packed-insulated", + bundle, + Region(:insulation, Shell(0.2), insulation_material) + ) + component=only(DM.flatten(insulated, 50.0)) + @test length(component.dielectric.layers) == 1 + @test only(component.dielectric.layers).r_in ≈ 1.5 + @test only(component.dielectric.layers).r_ex ≈ 1.7 + + nested=Group(:core, bundle; pattern = Ring(2; r = 4.0)) + nested_design=build(CableDesign, "nested-packed", nested) + @test only(DM.flatten(nested_design, 50.0)).conductor.num_wires == 14 + + lossy=Material(kind = :insulator, rho = 1.0e12, eps_r = 2.3, mu_r = 1.0) + magnetic=Material(kind = :insulator, rho = Inf, eps_r = 2.3, mu_r = 2.0) + semiconducting=Material(kind = :semicon, rho = 1.0e3, eps_r = 10.0) + lossy_design=build( + CableDesign, + "lossy-matrix", + terminal( + :core, + stranded( + copper; + shape = Disk(0.5), + boundary = Disk(1.5), + fill = lossy + ) + ) + ) + magnetic_design=build( + CableDesign, + "magnetic-matrix", + terminal( + :core, + stranded( + copper; + shape = Disk(0.5), + boundary = Disk(1.5), + fill = magnetic + ) + ) + ) + semiconducting_design=build( + CableDesign, + "semiconducting-matrix", + terminal( + :core, + stranded( + copper; + shape = Disk(0.5), + boundary = Disk(1.5), + fill = semiconducting + ) + ) + ) + wound_matrix_design=build( + CableDesign, + "longitudinal-matrix-path", + Group(:wound_core, bundle; path = Helix(LayRatio(10.0))) + ) + lossy_reduced=only(DM.flatten(lossy_design, 50.0)) + @test lossy_reduced.conductor == bare.conductor + @test lossy_reduced.dielectric == bare.dielectric + lossy_fill=only(filter(region -> region.primitive isa DM.DifferenceShape, + lossy_design.geometry.regions)) + @test lossy_fill.source.material == lossy + @test lossy_fill.source.material.rho == 1.0e12 + @test_throws ArgumentError DM.flatten(magnetic_design, 50.0) + @test_throws ArgumentError DM.flatten(semiconducting_design, 50.0) + wound=only(DM.flatten(wound_matrix_design, 50.0)) + @test wound.conductor.resistance > reduced.conductor.resistance +end + +@testitem "DataModel / v1 physical tree / placement, Group, and Assembly" tags=[:unit, :parametric] begin + const DM=LineCableModels.DataModel + copper=LineCableModels.Material(kind = :conductor, rho = 1.7241e-8) + wire=LineCableModels.Region(:wire, LineCableModels.Disk(0.5), copper) + + ring=LineCableModels.Ring(6; r = 2.0, φ0 = π / 6) + poses=LineCableModels.placements(ring, wire.primitive, nothing) + @test length(poses) == 6 + @test all(pose -> hypot(pose.x, pose.y) ≈ 2.0, poses) + + helix=LineCableModels.Helix(LineCableModels.LayRatio(10); dir = -1) + @test LineCableModels.pitch(helix, 2.0) == 40.0 + @test LineCableModels.angle(helix, 2.0) ≈ atan(4π, 40.0) + @test LineCableModels.overlength(helix, 2.0) > 1 + fixed=LineCableModels.Helix(LineCableModels.Pitch(20.0)) + @test LineCableModels.pitch(fixed, 3.0) == 20.0 + angled=LineCableModels.Helix(LineCableModels.LayAngle(π / 4)) + @test LineCableModels.pitch(angled, 2.0) ≈ 4π + + strand=LineCableModels.Group( + :strand, + wire; + pattern = LineCableModels.Ring(6; r = 1.0), + path = helix + ) + strand_resolution=DM.resolve(DM.EmptyBoundary(), strand) + @test unique(getproperty.(strand_resolution.regions, :terminal)) == [:strand] + @test length(strand_resolution.regions) == 6 + @test all(region -> region.terminal === :strand, strand_resolution.regions) + @test all( + region -> prod( + entry -> LineCableModels.overlength(entry.path, entry.radius), + region.paths; + init = 1.0 + ) > 1, + strand_resolution.regions + ) + @test DM.support(DM.boundary(strand_resolution)) == 1.5 + + rope=LineCableModels.Group( + :phase, + strand; + pattern = LineCableModels.Ring(3; r = 4.0) + ) + rope_resolution=DM.resolve(DM.EmptyBoundary(), rope) + @test unique(getproperty.(rope_resolution.regions, :terminal)) == [:phase] + @test length(rope_resolution.regions) == 18 + @test all(region -> region.terminal === :phase, rope_resolution.regions) + + core=LineCableModels.Group(:core, wire) + phases=LineCableModels.Assembly( + core; + pattern = LineCableModels.Ring(3; r = 5.0, φ0 = π / 2), + names = (:a, :b, :c) + ) + phase_resolution=DM.resolve(DM.EmptyBoundary(), phases) + @test getproperty.(phase_resolution.regions, :terminal) == [:a, :b, :c] + + indexed=LineCableModels.Assembly( + core; + pattern = LineCableModels.Lattice(nx = 2, ny = 1, dx = 3.0, dy = 0.0), + names = (:phase_1, :phase_2) + ) + @test getproperty.( + DM.resolve(DM.EmptyBoundary(), indexed).regions, + :terminal + ) == [:phase_1, :phase_2] + @test_throws DimensionMismatch DM.resolve( + DM.EmptyBoundary(), + LineCableModels.Assembly(core; pattern = ring, names = (:a, :b)) + ) +end + +@testitem "DataModel / construction / concrete boundaries" tags=[:unit, :parametric] begin + using LineCableModels + + conductor=Material(kind = :conductor, rho = 1.7241e-8) + dielectric=Material(kind = :insulator, rho = 1.0e14, eps_r = 2.3) + root=Stack( + Group(:phase, Region(:core, Disk(0.01), conductor)), + Region(:insulation, Shell(0.003), dielectric) + ) + make_design() = build(CableDesign, "concrete-construction", root) + design=make_design() + make_system() = build( + LineCableSystem, + design, + Pose2(0.0, -1.0); + connections = (phase = 1,) + ) + system=make_system() + problem=LineCableModels.Engine.LineParametersProblem( + system; + earth_props = LineCableModels.Earth.EarthModel(100.0), + frequencies = [50.0] + ) + + execution=computation_options(LineCableModelsCoaxial, ComputationOptions((;))) + blueprints=LineCableModels.Engine.CableBlueprint{eltype(problem)}[LineCableModels.Engine.flatten( + LineCableModelsCoaxial(), + source, + eltype(problem)) + for source in + problem.system.designs] + @test (@inferred LineCableModels.Engine.LineParametersWorkspace( + problem, + Formulation(), + execution, + blueprints + )) isa LineCableModels.Engine.LineParametersWorkspace{Float64} + + +end diff --git a/test/unit/datamodel/placement_geometry.jl b/test/unit/datamodel/placement_geometry.jl new file mode 100644 index 000000000..29e0677e0 --- /dev/null +++ b/test/unit/datamodel/placement_geometry.jl @@ -0,0 +1,153 @@ +@testitem "DataModel / explicit placement geometry and area constraints" tags=[:unit, :datamodel] begin + # A placement is a geometry operation: the caller retains its primitives. + ellipse = Ellipse(0.2e-3, 0.1e-3) + ring = Ring(5; r=2e-3, φ0=0.27, span=pi) + positions = placements(ring, ellipse, nothing) + @test length(positions) == 5 + for (index, position) in enumerate(positions) + angle = 0.27 + (index - 1) * pi / 4 + @test position.x ≈ 2e-3 * cos(angle) + @test position.y ≈ 2e-3 * sin(angle) + @test position.φ ≈ angle + end + @test_throws DomainError placements(Ring(20; r=0.2e-3), ellipse, nothing) + @test_throws ArgumentError placements(Ring(5), ellipse, nothing) + @test_throws ArgumentError placements(Ring(capacity(); r=2e-3), ellipse, nothing) + + unmoved = only(placements(nothing, ellipse, nothing)) + @test (unmoved.x, unmoved.y, unmoved.φ) == (0, 0, 0) + explicit = [Pose2(1e-3, 2e-3, 0.1), Pose2(-2e-3, -1e-3, 0.2)] + copied = placements(explicit, ellipse, nothing) + @test copied == explicit + @test copied !== explicit + @test_throws ArgumentError placements(Pose2[], ellipse, nothing) + + wire = Disk(0.2e-3) + for span in (pi, 2pi), gap in (0.0, 0.15) + available = Ring(capacity(); r=2e-3, span, gap_frac=gap) + count = capacity(available, wire, nothing) + packed = placements(Ring(count; r=2e-3, span, gap_frac=gap), wire, nothing) + @test length(packed) == count + @test all(hypot(a.x - b.x, a.y - b.y) >= 2wire.r * (1 + gap) - 1e-12 + for (index, a) in enumerate(packed) for b in packed[(index + 1):end]) + @test_throws DomainError placements( + Ring(count + 1; r=2e-3, span, gap_frac=gap), wire, nothing) + end + + strip = Rectangle(0.3e-3, 0.1e-3) + inner = 1e-3 + radius = inner + strip.h / 2 + for fraction in (0.65, 1.0) + factor = FillFactor(fraction) + placements_value = placements(Ring(12; r=radius, φ0=0.19), strip, factor) + outer = sqrt(inner^2 + 12area(strip) / (pi * fraction)) + @test length(placements_value) == 12 + resolved = [value.primitive for value in placements_value] + @test all(value -> area(value) ≈ area(strip), resolved) + @test all(value -> r_in(value) ≈ inner, resolved) + @test all(value -> r_ex(value) ≈ outer, resolved) + @test sum(area, resolved) / (pi * (outer^2 - inner^2)) ≈ fraction + @test [value.pose.φ for value in placements_value] ≈ + [0.19 + (index - 1) * 2pi / 12 for index in 1:12] + count = capacity(Ring(capacity(); r=radius), strip, factor) + available_area = pi * ((radius + strip.h / 2)^2 - inner^2) + @test count * area(strip) <= fraction * available_area + @test (count + 1) * area(strip) > fraction * available_area + end + @test_throws ArgumentError placements(Ring(12), strip, FillFactor(1)) + @test_throws ArgumentError placements( + Ring(capacity(); r=radius), strip, FillFactor(1)) + @test_throws DomainError capacity( + Ring(capacity(); r=radius, gap_frac=0.1), strip, FillFactor(1)) + @test capacity(Ring(capacity(); r=0), strip, FillFactor(0.8)) == 0 +end + +@testitem "DataModel / contextual capacity respects resolved member extents" tags=[:unit, :parametric] begin + sector = Sector(span=2pi / 3, r_base=0.6e-3, r_back=5e-3, fillet=0.2e-3) + @test capacity(Ring(capacity(); r=0), sector, nothing) == 3 + origins = placements(Ring(3; r=0), sector, nothing) + @test length(origins) == 3 + @test all(pose -> iszero(pose.x) && iszero(pose.y), origins) + @test getproperty.(origins, :φ) ≈ [0.0, 2pi / 3, 4pi / 3] + copper = Material(kind=:conductor, rho=1.72e-8) + dielectric = Material(kind=:insulator, rho=Inf, eps_r=2.3) + geometry = resolve(EmptyBoundary(), + terminal(:core, core(copper; r=0.5e-3), insulation(dielectric; t=0.2e-3))) + for item in (sector, geometry), span in (pi, 2pi), gap in (0.0, 0.1) + shape = item isa Sector ? resolve(EmptyBoundary(), item) : boundary(item) + width = item isa Sector ? support(shape, pi / 2) + support(shape, -pi / 2) : + 2support(shape) + radius = 20e-3 + available = Ring(capacity(); r=radius, span, gap_frac=gap) + count = capacity(available, item, nothing) + minimum_angle = 2asin(width * (1 + gap) / (2radius)) + expected = floor(Int, span / minimum_angle) + (span == 2pi ? 0 : 1) + @test count == expected + placed = placements(Ring(count; r=radius, span, gap_frac=gap), item, nothing) + @test length(placed) == count + @test all(hypot(a.x - b.x, a.y - b.y) >= width * (1 + gap) - 1e-12 + for (index, a) in enumerate(placed) for b in placed[(index + 1):end]) + @test_throws DomainError placements( + Ring(count + 1; r=radius, span, gap_frac=gap), item, nothing) + end + pattern = Fill(r=3e-3, φ=pi / 6) + @test placements(pattern, geometry, nothing) == + placements(pattern, Disk(support(boundary(geometry))), nothing) +end + +@testitem "DataModel / contextual ring dispatch preserves inferred geometry" tags=[:unit, :datamodel] begin + const DM = LineCableModels.DataModel + copper = Material(kind=:conductor, rho=1.72e-8) + for T in (Float32, Float64, BigFloat) + part = DM.Region(:wire, DM.Disk(T(0.001)), copper) + child = DM.resolve(DM.EmptyBoundary(), part) + for context in (DM.EmptyBoundary(), DM.Disk(T(0.01))) + fixed = Ring(6; r=T(0.02)) + resolved = @inferred DM._contextual_pattern(fixed, part, child, nothing, context) + @test resolved.n == 6 + @test resolved.r == fixed.r + available = Ring(capacity(); r=T(0.02)) + filled = @inferred DM._contextual_pattern(available, part, child, nothing, context) + @test filled.n == capacity(available, part.primitive, nothing) + derived = @inferred DM._contextual_pattern(Ring(6), part, child, nothing, context) + @test derived.r == (context isa DM.EmptyBoundary ? zero(T) : support(context)) + part.primitive.r + end + @test (@inferred DM._contextual_pattern(nothing, part, child, nothing, DM.EmptyBoundary())) === nothing + end +end + +@testitem "DataModel / polar placement preserves explicit radial and angular intent" tags=[:unit, :datamodel] begin + wire = Disk(0.1e-3) + for origin in (0.0, 0.5e-3), span in (pi, 2pi) + pattern = Polar(; nr=3, nφ=6, r0=origin, dr=1e-3, φ0=0.27, span) + poses = placements(pattern, wire, nothing) + @test length(poses) == (iszero(origin) ? 13 : 18) + @test poses == placements(pattern, wire, nothing) + for shell in 0:2 + radius = origin + shell * 1e-3 + row = filter(pose -> isapprox(hypot(pose.x, pose.y), radius; atol=1e-12), poses) + @test length(row) == (iszero(radius) ? 1 : 6) + step = span / (span == 2pi ? 6 : 5) + for (index, pose) in enumerate(row) + angle = 0.27 + (index - 1) * step + @test hypot(pose.x - radius * cos(angle), pose.y - radius * sin(angle)) <= 1e-12 + @test pose.φ ≈ angle + end + end + @test all(hypot(a.x - b.x, a.y - b.y) >= 2wire.r - 1e-12 + for (index, a) in enumerate(poses) for b in poses[(index + 1):end]) + end +end + +@testitem "DataModel / minimum radius distinguishes centered holes from displaced boundaries" tags=[:unit, :datamodel] begin + const DM = LineCableModels.DataModel + for shape in (Annulus(1.0, 2.0), DM.BentStrip(1.0, 2.0, 0.5)) + @test (@inferred DM._minimum_radius(shape)) == 1.0 + end + for moved in (Disk(1.0, Pose2(3.0, 0.0)), Ellipse(1.0, 0.5, Pose2(3.0, 0.0)), + Annulus(0.5, 1.0, Pose2(3.0, 0.0))) + @test (@inferred DM._minimum_radius(moved)) ≈ 2.0 + end + @test (@inferred DM._minimum_radius(Disk(1.0))) == 0.0 + @test r_in(Disk(1.0, Pose2(3.0, 0.0))) == 0.0 +end diff --git a/test/unit/datamodel/presentation.jl b/test/unit/datamodel/presentation.jl new file mode 100644 index 000000000..000f6ea9b --- /dev/null +++ b/test/unit/datamodel/presentation.jl @@ -0,0 +1,95 @@ +@testitem "DataModel / v1 presentation / model display and result table" tags=[:unit, :report] setup=[ + UseDataModelSupport, + TestFixtures +] begin + design=TestFixtures.coaxial_design() + system=TestFixtures.three_phase_system() + @test hasfield(typeof(design), :origin) + @test hasproperty(design, :origin) + @test build(CableDesign, design.cable_id, design.origin; + nominal_data=design.nominal_data) == design + + for object in (design.origin, first(design.geometry.regions), design, system) + @test !isempty(sprint(show, MIME("text/plain"), object)) + end + + constants=CableConstants(design) + observed=observables(constants,(R,L,C,G)) + tables=LineCableModels.ReportBuilder.tabulate(observed).constants + @test keys(tables)==(:R,:L,:C,:G) + for (quantity,table) in zip((R,L,C,G),tables) + @test nrow(table)==1 + @test names(table)==["frequency";string.(constants.cores)] + @test table.frequency==[constants.frequency] + @test collect(table[1,2:end])==observe(observed,quantity) + end + @test_throws r"tabulate" DataFrame(observed) + + design_display=sprint(show, MIME("text/plain"), design) + @test contains(design_display, design.cable_id) + @test contains(design_display, "regions $(length(design.geometry.regions))") + @test contains(design_display, "origin ") + system_display=sprint(show, MIME("text/plain"), system) + @test contains(system_display, system.system_id) + @test contains(system_display, "cables") + + library=CablesLibrary() + @test library isa AbstractDict{String, CableDesign} + @test keytype(typeof(library)) === String + @test valtype(typeof(library)) === CableDesign + @test eltype(typeof(library)) === Pair{String, CableDesign} + @test pairs(library) === library + datasheet_record=DatasheetInfo( + designation_code = "sample", + U0 = Float32(12), + U = 20.0, + resistance = 1 + ) + @test add!(library, design; datasheet = datasheet_record) === library + @test library[design.cable_id] === design + @test collect(library) isa Vector{Pair{String, CableDesign}} + @test datasheet(library, design.cable_id) == datasheet_record + @test get(library, "missing", nothing) === nothing + @test get(() -> design, library, "missing") === design + @test get(() -> design, library, design.cable_id) === design + @test_throws MethodError get(library, "missing") + @test !haskey(library, Symbol(design.cable_id)) + @test get(library, Symbol(design.cable_id), nothing) === nothing + @test_throws KeyError library[Symbol(design.cable_id)] + @test propertynames(datasheet_record) == (:designation_code, :U0, :U, :resistance) + datasheet_display=sprint(show, MIME("text/plain"), datasheet_record) + @test contains(datasheet_display, "U0") + @test contains(datasheet_display, "12 kV") + @test contains(datasheet_display, "1 Ω/km") + library_display=sprint(show, MIME("text/plain"), library) + @test contains(library_display, design.cable_id) + @test contains(library_display, "1 design") + + copied=copy(library) + @test copied isa CablesLibrary + @test copied !== library + @test copied.data !== library.data + @test copied.datasheets !== library.datasheets + @test copied[design.cable_id] === design + @test datasheet(copied, design.cable_id) == datasheet_record + @test delete!(copied, design.cable_id) === copied + @test haskey(library, design.cable_id) + + blank=empty(library) + @test blank isa CablesLibrary + @test isempty(blank) + @test isempty(blank.datasheets) + + @test_throws ArgumentError add!(library, design) + @test_throws ArgumentError setindex!(library, design, "wrong-id") + @test setindex!(library, design, design.cable_id) === library + @test datasheet(library, design.cable_id) == DatasheetInfo(design.nominal_data) + @test delete!(library, "missing") === library + @test delete!(library, :missing) === library + @test delete!(library, design.cable_id) === library + @test !haskey(library.datasheets, design.cable_id) + @test add!(library, design; datasheet = datasheet_record) === library + @test empty!(library) === library + @test isempty(library) + @test isempty(library.datasheets) +end diff --git a/test/unit/datamodel/rectangular_packing.jl b/test/unit/datamodel/rectangular_packing.jl new file mode 100644 index 000000000..419392be2 --- /dev/null +++ b/test/unit/datamodel/rectangular_packing.jl @@ -0,0 +1,152 @@ +@testitem "DataModel / rectangular packing hands off its occupied boundary" tags=[:unit, :importexport] begin + const DM = LineCableModels.DataModel + const EN = LineCableModels.Engine + const IE = LineCableModels.ImportExport + copper = Material(kind=:conductor, rho=1.72e-8) + dielectric = Material(kind=:insulator, rho=Inf, eps_r=2.3) + center, strand = Disk(0.2e-3), Rectangle(0.3e-3,0.1e-3) + exact = sqrt(center.r^2 + 33area(strand)/pi) + for limit in (0.59e-3, exact, exact/sqrt(1-1e-6), exact/sqrt(1-3e-6), + exact/sqrt(1-6e-6), 0.6e-3) + body = stranded(copper; center=center, shape=strand, boundary=Disk(limit), + lay=LayRatio(12), fill=dielectric) + @test body isa Group + @test body.boundary == Disk(limit) + standalone = build(CableDesign,"rectangular-boundary",terminal(:core,body)) + regions = standalone.geometry.regions + occupied = last(regions).primitive.ro + @test outer_radius(standalone) == occupied + @test all(r -> r.source.material == copper && r.source.tag === :wire, regions) + @test all(r -> area(r.primitive) ≈ area(r.source.primitive), regions) + @test sum(area, regions) ≈ area(standalone.geometry.outer) + @test all(r -> only(r.placement.patterns).pattern.boundary == + standalone.geometry.outer, regions) + @test IE.deserialize_value(IE.serialize_value(standalone)) == standalone + insulated = build(CableDesign,"rectangular-insulated", + terminal(:core,body,insulation(dielectric;t=0.2e-3))) + @test last(insulated.geometry.regions).primitive.ri == occupied + @test outer_radius(insulated) ≈ occupied + 0.2e-3 + row = only(EN.flatten(LineCableModelsCoaxial(),insulated,Float64).conductors) + @test row.r_ex == occupied + @test row.cross_section ≈ sum(area,regions) + @test all(r -> r.source.tag !== :stranded_fill, insulated.geometry.regions) + end + # Explicit enclosures remain physical even for sub-micrometer layers. + body = stranded(copper;center=center,shape=strand,boundary=Disk(0.6e-3)) + for thickness in (1e-9, 4e-6) + wrapped = build(CableDesign,"explicit-wrap",Enclosure(:paper, + terminal(:core,body);primitive=Disk(exact+thickness),fill=dielectric)) + layer = last(wrapped.geometry.regions) + @test layer.source.tag === :paper_fill + @test layer.primitive isa Annulus + @test layer.primitive.ri ≈ exact + @test DM.thickness(layer.primitive) ≈ thickness + end + rectangles = stranded(copper;center=center, + shape=Rectangle(Grid((0.3e-3,0.31e-3)),0.1e-3),boundary=Disk(0.6e-3)) + @test rectangles isa Gridspace{Group} + @test all(part -> part isa Group, rectangles) + mixed = stranded(copper;center=center, + shape=Grid((strand,Disk(0.1e-3))),boundary=Disk(0.6e-3)) + @test first(mixed) isa Group + @test last(collect(mixed)) isa Enclosure +end + +@testitem "DataModel / filled circular courses continue rectangular cores" tags=[:unit, :parametric] begin + const DM = LineCableModels.DataModel + const EN = LineCableModels.Engine + copper = Material(kind=:conductor,rho=1.72e-8) + matrix = Material(kind=:insulator,rho=1e10,eps_r=3.0) + dielectric = Material(kind=:insulator,rho=Inf,eps_r=2.3) + limit, wire_radius, count = 0.6e-3, 0.05e-3, 12 + body = stranded(copper;center=Disk(0.2e-3),shape=Rectangle(0.3e-3,0.1e-3), + boundary=Disk(limit)) + core = terminal(:core,body) + occupied = outer_radius(build(CableDesign,"core",core)) + for radius in (nothing, limit+wire_radius) + ring = Group(:core,Region(:round_wire,Disk(wire_radius),copper); + pattern=Ring(count;r=radius)) + filled = Enclosure(:ring_matrix,ring; + primitive=Annulus(limit,limit+2wire_radius),fill=matrix) + design = build(CableDesign,"rectangular-ring",core,filled, + insulation(dielectric;t=0.1e-3)) + metal = filter(r -> r.source.tag === :round_wire,design.geometry.regions) + @test length(metal) == count + expected_radius = something(radius,occupied+wire_radius) + @test all(r -> hypot(centroid(r.primitive)...) ≈ expected_radius,metal) + fill_region = only(filter(r -> r.source.tag === :ring_matrix_fill, + design.geometry.regions)) + @test fill_region.source.material == matrix + @test fill_region.primitive isa DM.DifferenceShape + @test fill_region.primitive.outer.ri == occupied + @test fill_region.primitive.outer.ro == limit+2wire_radius + @test length(fill_region.primitive.holes) == count + @test area(fill_region.primitive) ≈ + pi*((limit+2wire_radius)^2-occupied^2)-count*pi*wire_radius^2 + @test sum(area,design.geometry.regions) ≈ area(design.geometry.outer) + @test all(r -> r.source.tag !== :stranded_fill,design.geometry.regions) + row = only(EN.flatten(LineCableModelsCoaxial(),design,Float64).conductors) + @test row.cross_section ≈ pi*occupied^2+count*pi*wire_radius^2 + # The transfer of geometry also works after a rigid placement of the complete stack. + shifted = build(CableDesign,"rectangular-ring-placed", + assembly(at(Stack(core,filled,insulation(dielectric;t=0.1e-3)), + Pose2(0.01,-0.02,0.37)))) + @test sum(area,shifted.geometry.regions) ≈ area(shifted.geometry.outer) + end + ring = Group(:core,Region(:round_wire,Disk(wire_radius),copper); + pattern=Ring(count;r=limit+wire_radius)) + # Only an otherwise feasible, concentric material fill can own the gap. + @test_throws DomainError build(CableDesign,"overlapping-ring",core, + Enclosure(:ring_matrix,ring; + primitive=Annulus(occupied-wire_radius,limit+2wire_radius),fill=matrix)) + @test_throws DomainError build(CableDesign,"offset-ring",core, + Enclosure(:ring_matrix,ring;at=Pose2(wire_radius,0.), + primitive=Annulus(limit,limit+2wire_radius),fill=matrix)) + @test_throws DomainError build(CableDesign,"explicit-fill-region",core, + Enclosure(:ring_matrix,ring;primitive=Annulus(limit,limit+2wire_radius), + fill=Region(:specified_fill,Annulus(limit,limit+2wire_radius),matrix))) +end + +@testitem "DataModel / rectangular occupied boundaries reconstruct under uncertainty" tags=[:extension, :measurements] begin + using Measurements, Random + const DM = LineCableModels.DataModel + copper = Material(kind=:conductor,rho=1.72e-8) + dielectric = Material(kind=:insulator,rho=Inf,eps_r=2.3) + space = build(CableDesign,"rectangular-uncertain",terminal(:core, + stranded(copper;center=Disk(0.2e-3), + shape=Rectangle(Grid(0.3e-3,AbsoluteError(1e-6)),0.1e-3), + boundary=Disk(Grid(0.6e-3,AbsoluteError(3e-6)))), + insulation(dielectric;t=0.2e-3))) + propagated = only(space) + @test uncertainty(outer_radius(propagated)) > 0 + for design in (propagated, (rand(MersenneTwister(seed),space) for seed in 1:12)...) + metal = filter(r -> r.source.material.kind === :conductor,design.geometry.regions) + occupied = last(metal).primitive.ro + @test last(design.geometry.regions).primitive.ri == occupied + @test sum(area,metal) ≈ pi*occupied^2 + @test all(r -> r.source.tag !== :stranded_fill,design.geometry.regions) + @test all(r -> only(r.placement.patterns).pattern.boundary.r == occupied,metal) + end + body = stranded(copper;center=Disk(0.2e-3), + shape=Rectangle(Grid(0.3e-3,AbsoluteError(1e-6)),0.1e-3), + boundary=Disk(0.6e-3)) + for radius in (nothing,0.65e-3) + ring = Group(:core,Region(:round_wire,Disk(0.05e-3),copper); + pattern=Ring(12;r=radius)) + filled = Enclosure(:ring_matrix,ring; + primitive=Annulus(0.6e-3,0.7e-3),fill=dielectric) + mixed = build(CableDesign,"uncertain-mixed-core",terminal(:core,body),filled, + insulation(dielectric;t=0.1e-3)) + for design in (only(mixed),(rand(MersenneTwister(seed),mixed) for seed in 1:12)...) + strips = filter(r -> r.source.primitive isa Rectangle,design.geometry.regions) + occupied = last(strips).primitive.ro + fill_region = only(filter(r -> r.source.tag === :ring_matrix_fill, + design.geometry.regions)) + wires = filter(r -> r.source.tag === :round_wire,design.geometry.regions) + @test fill_region.primitive.outer.ri == occupied + @test all(r -> hypot(centroid(r.primitive)...) ≈ + something(radius,occupied+0.05e-3),wires) + @test sum(area,design.geometry.regions) ≈ area(design.geometry.outer) + end + end +end diff --git a/test/unit/datamodel/sector.jl b/test/unit/datamodel/sector.jl new file mode 100644 index 000000000..517bc4ff7 --- /dev/null +++ b/test/unit/datamodel/sector.jl @@ -0,0 +1,355 @@ +@testitem "DataModel / Sector / exact geometry and conformal shells" tags=[:unit, :parametric, :slow] begin + const DM=LineCableModels.DataModel + + primitive=Sector( + span = 2pi/3, + r_base = .001, + r_back = .008, + fillet = .0005 + ) + + shape=DM.resolve(DM.EmptyBoundary(), primitive) + @test shape isa DM.SectorShape + include(joinpath(pkgdir(LineCableModels),"test/support/sector_control.jl")) + controls=[setprecision(BigFloat,bits) do + SectorControl.moments(2big(pi)/3,big".001",big".008",big".0005") + end for bits in (128,256,512)] + expected=last(controls) + for name in (:area,:perimeter) + target=getproperty(expected,name); budget=1e-10*abs(target) + u=getproperty(expected.errors,name)+abs(target-getproperty(controls[2],name)) + @test u<=budget/4 + @test abs(getproperty(DM,name)(shape)-target)+u<=budget + end + @test DM.centroid(shape)[1] ≈ expected.centroid[1] rtol=1e-10 + @test abs(DM.centroid(shape)[2])<=1e-10*primitive.r_back + previous_error=Ref(Inf) + for count in (32,64,128,256,512) + polygon=DM.tessellate(shape;points_per_arc=count) + polygon_area=abs(sum(p[1]*q[2]-p[2]*q[1] for (p,q) in + zip(polygon,circshift(polygon,-1))))/2 + error=abs(polygon_area-expected.area) + @test error length(coarse) + @test all(point -> point isa NTuple{2, Float64}, coarse) + @test all(point -> point isa NTuple{2, Float64}, dense) + @test !occursin("GeometryBasics", string(typeof(coarse))) + @test !occursin("Makie", string(typeof(coarse))) + + for angle in range(-pi, pi; length = 37) + sampled=maximum(point -> point[1] * cos(angle) + point[2] * sin(angle), dense) + @test DM.support(shape, angle) + 2e-7 >= sampled + end + coarse_angle=0.31 + coarse_support=maximum( + point -> point[1] * cos(coarse_angle) + point[2] * sin(coarse_angle), + coarse + ) + @test DM.support(shape, coarse_angle) - coarse_support > 0 + + pose=Pose2(.02,-.03,pi/6) + placed=DM.resolve(pose, shape) + local_center=DM.centroid(shape) + expected=( + pose.x + cos(pose.φ) * local_center[1] - sin(pose.φ) * local_center[2], + pose.y + sin(pose.φ) * local_center[1] + cos(pose.φ) * local_center[2] + ) + @test collect(DM.centroid(placed)) ≈ collect(expected) + @test primitive == shape.primitive == placed.primitive + + thickness=0.5e-3 + shell=DM.resolve(shape, Shell(thickness)) + @test shell isa DM.ShellShape + @test shell.outer.primitive.span == primitive.span + @test shell.outer.primitive.r_base == primitive.r_base - thickness + @test shell.outer.primitive.r_back == primitive.r_back + thickness + @test shell.outer.primitive.fillet == primitive.fillet + thickness + @test DM.area(shell) == DM.area(shell.outer) - DM.area(shell.inner) + @test DM.perimeter(shell) == + DM.perimeter(shell.inner) + DM.perimeter(shell.outer) + @test DM.area(shell.outer) ≈ + DM.area(shape) + DM.perimeter(shape) * thickness + pi * thickness^2 + @test DM.perimeter(shell.outer) ≈ DM.perimeter(shape) + 2pi * thickness + + inner_area=DM.area(shell.inner) + outer_area=DM.area(shell.outer) + inner_center=DM.centroid(shell.inner) + outer_center=DM.centroid(shell.outer) + expected_shell_center=( + (outer_area * outer_center[1] - inner_area * inner_center[1]) / + (outer_area - inner_area), + (outer_area * outer_center[2] - inner_area * inner_center[2]) / + (outer_area - inner_area) + ) + @test collect(DM.centroid(shell)) ≈ collect(expected_shell_center) + + second=DM.resolve(DM.boundary(shell), Shell(0.25e-3)) + @test second.inner == shell.outer + @test second.outer.primitive.r_back == primitive.r_back + 0.75e-3 + @test second.outer.primitive.fillet == primitive.fillet + 0.75e-3 + @test_throws DomainError DM.resolve(shape, Shell(primitive.r_base + eps())) + @test_throws DomainError Sector(span = 0.2, r_base = 0.0, r_back = 1.0, fillet = 0.95) +end + +@testitem "DataModel / Sector / terminal assembly and Gridspace" tags=[:unit, :parametric] begin + const DM=LineCableModels.DataModel + + copper=Material(kind = :conductor, rho = 1.7241e-8, mu_r = 0.999994, + alpha = 0.00393) + insulation_material=Material(kind = :insulator, rho = 1.97e14, eps_r = 2.5) + primitive=Sector( + span = 2pi / 3, + r_base = .001, + r_back = .008, + fillet = .0005 + ) + phase=terminal( + :phase, + Region(:core, primitive, copper), + Region(:insulation, Shell(0.5e-3), insulation_material) + ) + root=assembly( + phase; + pattern = Ring(3; r = 0), + names = (:a, :b, :c) + ) + design=build(CableDesign, "sectorized", root) + @test design.terminal_order == [:a, :b, :c] + @test count(!isnothing, getproperty.(design.geometry.regions, :terminal)) == 3 + @test count(region -> region.primitive isa DM.SectorShape, + design.geometry.regions) == 3 + @test count(region -> region.primitive isa DM.ShellShape, + design.geometry.regions) == 3 + @test !(design.geometry.outer isa DM.Disk) + @test_throws MethodError DM.resolve(design.geometry.outer, Shell(0.5e-3)) + + first_region=first(design.geometry.regions) + @test first_region.source.primitive == primitive + preview_shape=only(DM.preview_shapes(first_region)) + @test length(preview_shape.geometry.exterior) > 6 + @test all(point -> all(isfinite, point), preview_shape.geometry.exterior) + + conductor_shapes=[region.primitive + for region in design.geometry.regions + if region.terminal !== nothing] + @test getproperty.(getproperty.(conductor_shapes, :at), :φ) ≈ + [0.0, 2pi / 3, 4pi / 3] + + divergent=terminal( + :phase, + Region( + :core, + Sector( + span = deg2rad(119), + r_base = .001, + r_back = .008, + fillet = .0005 + ), + copper + ) + ) + @test_throws DomainError build(CableDesign, "divergent-sector-sides", assembly( + divergent; + pattern = Ring(3; r = 0), + names = (:a, :b, :c) + )) + + touching=terminal( + :phase, + Region( + :core, + Sector(span = 2pi / 3, r_base = 0, r_back = 10e-3), + copper + ) + ) + @test_throws DomainError build(CableDesign, "touching-bare-sectors", assembly( + touching; + pattern = Ring(3; r = 0), + names = (:a, :b, :c) + )) + + insulation_thickness=0.2e-3 + insulated_contact=terminal( + :phase, + Region( + :core, + Sector( + span = 2pi / 3, + r_base = insulation_thickness / sin(pi / 3), + r_back = 10e-3 + ), + copper + ), + insulation(insulation_material; t = insulation_thickness) + ) + contacted=build(CableDesign, "insulated-sector-contact", assembly( + insulated_contact; + pattern = Ring(3; r = 0), + names = (:a, :b, :c) + )) + @test contacted.geometry.outer isa DM.AssemblyShape + + varied=Sector( + span = Grid((deg2rad(118.0), deg2rad(119.0))), + r_base = Grid((1.00e-3, 1.10e-3)), + r_back = Grid((10.24e-3, 10.34e-3)), + fillet = Grid((0.9e-3, 1.02e-3)) + ) + varied_shell=Shell(Grid((0.4e-3, 0.5e-3))) + varied_phase=terminal( + :phase, + Region(:core, varied, copper), + Region(:insulation, varied_shell, insulation_material) + ) + designs=build(CableDesign, "sector-grid", varied_phase) + @test designs isa Gridspace{CableDesign} + @test eltype(designs) === Any + @test Base.IteratorEltype(typeof(designs)) isa Base.EltypeUnknown + @test length(designs) == 32 + @test all(design -> design isa CableDesign, designs) + @test all(design -> design.origin isa DM.Group, designs) +end + +@testitem "DataModel / Sector / member-local equivalent-area flattening" tags=[:unit, :importexport] begin + const DM=LineCableModels.DataModel + + copper=Material(kind = :conductor, rho = 1.7241e-8, mu_r = 0.999994, + alpha = 0.00393) + xlpe=Material(kind = :insulator, rho = 1.97e14, eps_r = 2.5) + primitive=Sector( + span = 2pi / 3, + r_base = .001, + r_back = .008, + fillet = .0005 + ) + phase=terminal( + :phase, + Region(:core, primitive, copper), + Region(:insulation, Shell(0.5e-3), xlpe) + ) + design=build(CableDesign, "sector-input", assembly( + phase; + pattern = Ring(3; r = 0), + names = (:a, :b, :c) + )) + + components=DM.flatten(design, 50.0) + @test getproperty.(components, :name) == [:a, :b, :c] + source_shape=first(design.geometry.regions).primitive + shell_shape=design.geometry.regions[2].primitive + source_area=DM.area(source_shape) + equivalent_radius=sqrt(source_area / pi) + outer_radius=sqrt(DM.area(shell_shape.outer) / pi) + expected_position=DM.centroid(source_shape) + component=first(components) + @test component.conductor.cross_section ≈ source_area + @test component.conductor.r_in == 0 + @test component.conductor.r_ex ≈ equivalent_radius + @test component.conductor.resistance ≈ copper.rho / source_area + @test component.conductor.gmr ≈ + equivalent_radius * exp(-copper.mu_r / 4) + @test collect(component.conductor.position) ≈ collect(expected_position) + @test component.dielectric.r_in ≈ equivalent_radius + @test component.dielectric.r_ex ≈ outer_radius + @test component.dielectric.cross_section ≈ DM.area(shell_shape) + @test component.dielectric.shunt_capacitance ≈ + DM.shunt_capacitance(equivalent_radius, outer_radius, xlpe.eps_r) + @test component.dielectric.shunt_conductance ≈ + DM.shunt_conductance(equivalent_radius, outer_radius, xlpe.rho) + @test DM.tubular_resistance( + component.conductor.r_in, + component.conductor.r_ex, + component.conductor.material.rho + ) ≈ component.conductor.resistance + @test DM.tubular_gmr( + component.conductor.r_ex, + component.conductor.r_in, + component.conductor.material.mu_r + ) ≈ component.conductor.gmr + @test DM.shunt_capacitance( + component.dielectric.r_in, + component.dielectric.r_ex, + component.dielectric.material.eps_r + ) ≈ component.dielectric.shunt_capacitance + @test DM.shunt_conductance( + component.dielectric.r_in, + component.dielectric.r_ex, + component.dielectric.material.rho + ) ≈ component.dielectric.shunt_conductance + @test component.dielectric.material.mu_r ≈ xlpe.mu_r + + magnetic_inner=Material(kind = :insulator, rho = 1e12, eps_r = 2.0, mu_r = 1.5) + magnetic_outer=Material(kind = :insulator, rho = 2e12, eps_r = 3.0, mu_r = 2.5) + magnetic_layers=[ + (r_in = 0.01, r_ex = 0.015, material = magnetic_inner), + (r_in = 0.015, r_ex = 0.02, material = magnetic_outer) + ] + radial_mu=( + magnetic_inner.mu_r*log(0.015/0.01) + + magnetic_outer.mu_r*log(0.02/0.015) + )/log(0.02/0.01) + @test DM.equivalent_dielectric_permeability( + magnetic_layers, + 4.0, + 0.01, + 0.02 + ) ≈ radial_mu*DM.solenoid_factor(4.0, 0.01, 0.02) + + problem=LineParametersProblem( + design, + Pose2(0, -1); + connections = (a = 1, b = 2, c = 3), + earth_props = homogeneous(rho = 100), + frequencies = [50.0] + ) + flattened=homogenize(design) + @test flattened.origin isa DM.Assembly + @test flattened.terminal_order == design.terminal_order + flattened_conductors=filter( + region->region.source.material.kind===:conductor, + flattened.geometry.regions + ) + @test all(zip( + DM.centroid.(getproperty.(flattened_conductors, :primitive)), + getproperty.(getproperty.(components, :conductor), :position) + )) do (actual, expected) + isapprox(actual[1], expected[1]) && isapprox(actual[2], expected[2]) + end + # Packed sectors flatten correctly, but their equivalent circles overlap. + # The complete earth model requires disjoint exterior circumferences. + @test_throws r"exterior circumferences must not overlap" compute(problem) + separated=build(CableDesign, "separated-sector-input", assembly( + phase; pattern = Ring(3; r = 0.02), names = (:a, :b, :c))) + separated_problem=LineParametersProblem(separated, Pose2(0, -1); + connections = (a = 1, b = 2, c = 3), earth_props = problem.earth_props, + frequencies = problem.frequencies) + parameters=compute(separated_problem; options = (trace = true,)) + @test size(parameters.Z) == (3, 3, 1) + @test size(parameters.Y) == (3, 3, 1) + @test all(isfinite, parameters.Z) + @test all(isfinite, parameters.Y) + @test details(parameters).data.trace.cable_map == [1, 2, 3] + + encoded=LineCableModels.ImportExport.serialize_value(design) + @test encoded["origin"]["item"]["item"]["items"][1]["primitive"]["kind"] == + "sector" + serialized=sprint(show, encoded) + for derived in ( + "contacts", "geometry", "equivalent_radius", "resistance", "gmr", + "shunt_capacitance", "shunt_conductance" + ) + @test !occursin(derived, serialized) + end + restored=LineCableModels.ImportExport.deserialize_value(encoded) + @test restored == design +end + +# Historical sector outputs are retired. Current equivalent-area and shell +# checks above use directly established geometry and constitutive identities. diff --git a/test/unit/datamodel/stranding.jl b/test/unit/datamodel/stranding.jl new file mode 100644 index 000000000..d85a79d6c --- /dev/null +++ b/test/unit/datamodel/stranding.jl @@ -0,0 +1,140 @@ +@testitem "DataModel / stranding / complete-course capacity at roundoff boundaries" tags=[:unit, :parametric] begin + import LineCableModels.DataModel as DM + + for F in (Float32, Float64), courses in 1:12, strand_area in F.((1e-8, 1.0, 1e8)) + inventory = F(3courses * (courses + 1)) + for ratio in (inventory, inventory - 4eps(inventory), inventory + 4eps(inventory)) + @test DM.course_count(ratio * strand_area, strand_area) == courses + end + # A real area deficit must still exclude the incomplete outer course. + insufficient = (inventory - 256eps(inventory)) * strand_area + if courses == 1 + @test_throws DomainError DM.course_count(insufficient, strand_area) + else + @test DM.course_count(insufficient, strand_area) == courses - 1 + end + end + + # The 320/380 kV cable examples have exactly enough copper for 127 wires. + # Before the fix, the inverse returned 5.999999999999999 and lost 36 wires. + radius = 3.66e-3 / 2 + wire = Disk(radius) + boundary = Disk(sqrt(127) * radius) + sites = DM.circular_courses(boundary, wire, wire, true) + @test length(sites) == 126 + @test [count(site -> site.course == k, sites) for k in 1:6] == 6 .* (1:6) + + copper = Material(kind=:conductor, rho=1.7241e-8) + design = @cable "full-127-wire-core" begin + @terminal :core begin + stranded(copper; shape=wire, boundary=boundary, compact=true) + end + end + wires = filter(region -> region.terminal === :core, design.geometry.regions) + @test length(wires) == 127 + @test all(region -> isapprox(area(region), area(wire); rtol=5e-12), wires) + @test sum(area, wires) ≈ area(boundary) rtol=5e-12 + @test all(region -> DM.support(region.primitive) <= boundary.r * (1 + 5e-6), wires) +end + +@testitem "DataModel / stranding / clipped disks conserve area inside cells" tags=[:unit, :measurements] begin + using Measurements: measurement, value, uncertainty + import LineCableModels.DataModel as DM + + cell = [(-1.0, -1.0), (1.0, -1.0), (1.0, 1.0), (-1.0, 1.0)] + for target in (pi / 4, 3.0, 3.9, 4.0) + strand = DM.area_preserving_strand(cell, target) + @test DM.signed_polygon_area(strand) ≈ target rtol=5e-13 + @test all(point -> maximum(abs, point) <= 1 + 1e-12, strand) + end + circular = DM.area_preserving_strand(cell, pi / 4) + radii = [hypot(point...) for point in circular] + @test maximum(radii) - minimum(radii) < 1e-12 + @test maximum(radii) ≈ 0.5 rtol=3e-4 + @test DM.area_preserving_strand(cell, 4.0) == cell + @test_throws DomainError DM.area_preserving_strand(cell, 4.1) + + # A tolerance crossing must never extrapolate a polygon edge. + near_edge = [(-1.0, 1e-15), (1.0, 1e-12), (0.0, -1.0)] + clipped = DM.clip_halfplane(near_edge, (0.0, 1.0), 0.0) + @test all(point -> -1 <= point[1] <= 1, clipped) + + target = measurement(3.0, 0.03) + propagated = DM.signed_polygon_area(DM.area_preserving_strand(cell, target)) + @test value(propagated) ≈ value(target) rtol=5e-13 + @test uncertainty(propagated) ≈ uncertainty(target) rtol=1e-6 +end + +@testitem "DataModel / stranding / sector bundle identity, containment and symmetry" tags=[:unit, :datamodel] begin + import LineCableModels.DataModel as DM + + function vertices(shape) + c, s = cos(shape.at.φ), sin(shape.at.φ) + return [(shape.at.x + c * p[1] - s * p[2], + shape.at.y + s * p[1] + c * p[2]) for p in shape.points] + end + + # Separating-axis check uses the actual conductor polygons, independently + # of the power diagram which allocated their space. + function separated(left, right, tolerance) + for polygon in (left, right), index in eachindex(polygon) + p, q = polygon[index], polygon[mod1(index + 1, length(polygon))] + normal = (q[2] - p[2], p[1] - q[1]) + norm = hypot(normal...) + norm <= tolerance && continue + a = [point[1] * normal[1] + point[2] * normal[2] for point in left] + b = [point[1] * normal[1] + point[2] * normal[2] for point in right] + min(maximum(a) - minimum(b), maximum(b) - minimum(a)) <= + tolerance * norm && return true + end + return false + end + + boundaries = ( + (Sector(deg2rad(118), 1.5e-3, 8e-3, 0.6e-3), 0.55e-3, 0.7423123065), + (Sector(2pi / 3, 0.6e-3, 5e-3, 0.2e-3), 0.35e-3, 0.6661730464), + (Sector(pi / 2, 0.6e-3, 5e-3, 0.2e-3), 0.35e-3, 0.8852644112) + ) + for (definition, radius, expected_fill) in boundaries + boundary = DM.resolve(DM.EmptyBoundary(), definition) + for a in (radius, sqrt(0.93 * area(boundary) / (37pi))) + members, shapes, courses = DM.sector_courses(boundary, Disk(a)) + @test courses == 3 + @test length(shapes) == 37 + @test [count(m -> m.course == k, members) for k in 0:3] == [1, 6, 12, 18] + @test all(isapprox.(first(members).site, + DM.polygon_centroid(DM.sector_polygon(boundary)))) + @test all(k -> [m.member for m in members if m.course == k] == 1:(6k), 1:3) + @test maximum(abs.(area.(shapes) ./ (pi * a^2) .- 1)) < 5e-12 + @test 37pi * a^2 / area(boundary) ≈ + (a == radius ? expected_fill : 0.93) rtol=1e-9 + polygons = vertices.(shapes) + tolerance = 1e-9 * definition.r_back + @test all(polygon -> all(p -> DM.clearance(boundary, p) >= -tolerance, + polygon), polygons) + @test all(separated(polygons[i], polygons[j], tolerance) + for i in eachindex(polygons) for j in (i + 1):length(polygons)) + + for k in 0:3 + indices = findall(m -> m.course == k, members) + @test all(indices) do i + center = DM.centroid(shapes[i]) + any(indices) do j + mirror = DM.centroid(shapes[j]) + hypot(center[1] - mirror[1], center[2] + mirror[2]) < tolerance + end + end + end + end + + pose = Pose2(0.012, -0.008, 0.43) + original, shapes, _ = DM.sector_courses(boundary, Disk(radius)) + moved, rotated, _ = DM.sector_courses(DM.resolve(pose, boundary), Disk(radius)) + @test getproperty.(moved, :course) == getproperty.(original, :course) + @test all(eachindex(shapes)) do i + expected = DM.centroid(DM.resolve(pose, shapes[i])) + actual = DM.centroid(rotated[i]) + hypot(expected[1] - actual[1], expected[2] - actual[2]) < 1e-9 + end + end +end diff --git a/test/unit/datamodel/three_bare_wires.jl b/test/unit/datamodel/three_bare_wires.jl new file mode 100644 index 000000000..7e125001c --- /dev/null +++ b/test/unit/datamodel/three_bare_wires.jl @@ -0,0 +1,36 @@ +@testitem "DataModel / reusable three bare wire placements" tags=[:unit, :importexport] setup=[TestFixtures] begin + layouts = TestFixtures.three_bare_wires_layouts + @test length(layouts) == 5 + for (name, heights) in pairs(layouts) + problem = TestFixtures.three_bare_wires_problem(; heights, name=string(name)) + system = problem.system + @test getproperty.(system.positions, :x) == [0.0, 1.0, 2.0] + @test getproperty.(system.positions, :y) == collect(heights) + @test system.positions == system.input_positions + @test system.connection_order == [1, 2, 3] + @test system.terminal_order == [(cable=i, terminal=:core) for i in 1:3] + @test length(system.geometry) == 3 + @test all(design -> design.terminal_order == [:core], system.designs) + @test system.line_length == 1.0 + @test problem.temperature == 20.0 + @test problem.frequencies == [0.1, 1.0, 10.0, 100.0, 1e3, 1e4, 1e5, 1e6, 1e7] + @test problem.earth_props.layers[2].rho == 0.1 + @test problem.earth_props.layers[2].eps_r == 1.0 + @test problem.earth_props.layers[2].mu_r == 1.0 + mktempdir() do directory + path = joinpath(directory, "problem.json") + export_data(:json, problem; file_name=path) + restored = import_data(:json, LineParametersProblem; file_name=path) + @test restored.system.positions == system.positions + @test restored.system.terminal_order == system.terminal_order + @test restored.frequencies == problem.frequencies + end + end + changed = TestFixtures.three_bare_wires_problem(; radius=0.01, rho=100.0, + horizontal=(0.0, 2.0, 4.0), heights=(2.0, -3.0, -4.0), frequencies=[50.0]) + @test getproperty.(changed.system.positions, :x) == [0.0, 2.0, 4.0] + @test getproperty.(changed.system.positions, :y) == [2.0, -3.0, -4.0] + @test changed.earth_props.layers[2].rho == 100.0 + @test changed.frequencies == [50.0] + @test_throws ArgumentError TestFixtures.three_bare_wires_problem(; heights=(1.0,)) +end diff --git a/test/unit/datamodel/vdeparse.jl b/test/unit/datamodel/vdeparse.jl new file mode 100644 index 000000000..0ae77702a --- /dev/null +++ b/test/unit/datamodel/vdeparse.jl @@ -0,0 +1,51 @@ +@testitem "DataModel / VDE parser / ordered tokens and residue" tags=[:unit, :datamodel] setup=[ + UseDataModelSupport +] begin + parser=LineCableModels.DataModel.vdeparse + + cases=( + ( + "2XS(F)2Y 3x240/25 12/20kV RM/V", + Dict( + :conductor=>"copper conductor", + :insulation=>"cross-linked PE (XLPE)", + :waterblocking=>"longitudinally water-proof protection", + :outer_sheath=>"PE outer sheath", + :cores=>"3", + :conductor_cross_section=>"240", + :metallic_screen_cross_section=>"25", + :voltage=>"12/20 kV", + :conductor_type=>"round, stranded, compact" + ) + ), + ( + "A2XS(FL)KL2Y 1x630 18/30kV RE", + Dict( + :conductor=>"aluminium conductor", + :sheath=>"aluminium sheath", + :cores=>"1", + :conductor_type=>"round, solid" + ) + ), + ("N2XSY 1x150", Dict(:designation=>"DIN VDE standard", :cores=>"1")) + ) + + for (code, expected) in cases + parsed=parser(code) + @test all(key -> parsed[key] == expected[key], keys(expected)) + end + + @test isempty(parser(" \u00a0 ")) + @test LineCableModels.DataModel.decode_type("RRM") == "round, stranded" + @test_logs (:warn, r"Unknown conductor type") begin + @test LineCableModels.DataModel.decode_type("RZ") == "round" + end + @test_logs (:warn, r"Unparsed stub residue") begin + parsed = parser("2XSQ 1x10") + @test parsed[:unparsed_stub] == "Q" + end + @test_logs (:warn, r"Unparsed trailing") begin + parsed = parser("2XSY 1x10 unsupported") + @test parsed[:unparsed] == "unsupported" + end +end diff --git a/test/unit/description.jl b/test/unit/description.jl new file mode 100644 index 000000000..a7f1f0f94 --- /dev/null +++ b/test/unit/description.jl @@ -0,0 +1,273 @@ +@testitem "Descriptions / partial recipes retain only declared ordered children" tags=[:unit, :importexport] begin + E = LineCableModels.Engine + for selected in ( + Formulation(earth_impedance=(earth=:unified,)), + Formulation(earth_admittance=(mixed=:unified, air=:unified)), + Formulation(earth_impedance=(;))) + record = NamedTuple(selected) + for slot in (:earth_impedance, :earth_admittance) + children = getproperty(selected.methods, slot) + children isa NamedTuple || continue + routes = [last(scope)[2] for (scope, _) in pairs(selected) + if length(last(scope)) == 2 && first(last(scope)) == slot] + @test Tuple(routes) == keys(children) + @test keys(getproperty(record.methods, slot)) == keys(children) + end + end + record = NamedTuple(Formulation(earth_impedance=(earth=:unified,))) + malformed = merge(record, (methods=merge(record.methods, + (earth_impedance=(ocean=record.methods.earth_impedance.earth,),)),)) + @test_throws ArgumentError pairs(LineParametersFormulation, malformed) +end + +@testitem "Descriptions / owner dispatch survives composed reports and saved declarations" tags=[:unit, :pscad, :slow] setup=[FormulaFixtures] begin + using DataFrames, Statistics + using LineCableModels.ReportBuilder: BenchmarkTableDefinition + import LineCableModels: description, formula_id, formulation_options + E=LineCableModels.Engine + + # Equal text must not become an identity, and a new leaf must appear in the real + # report without being added to a reader or plotting lookup table. + struct TestEarthLeaf{ID} <: E.EarthImpedanceFormulation end + formula_id(::TestEarthLeaf{ID}) where {ID} = ID + description(::Type{<:TestEarthLeaf{ID}};compact::Bool=false) where {ID} = + compact ? "Display-"*string(ID) : "Test-owned explanation shared by distinct equations" + description(value::TestEarthLeaf;compact::Bool=false) = description(typeof(value);compact) + formulation_options(::TestEarthLeaf) = FormulationOptions() + Base.NamedTuple(value::TestEarthLeaf) = (identifier=formula_id(value),parameters=(;),options=(;)) + normal=Formulation() + leaves=(TestEarthLeaf{:TestAlpha}(),TestEarthLeaf{:TestBeta}()) + choices=[E.LineParametersFormulation(merge(normal.methods,(earth_impedance=leaf,)), + normal.options,merge(normal.definitions,(earth_impedance=leaf,))) for leaf in leaves] + push!(choices,normal) + @test description(LineCableModelsCoaxial())==description(LineCableModelsCoaxial)=="coaxial" + @test description(LineCableModelsFEM())==description(LineCableModelsFEM)=="FEM" + @test description(Formulation(:pscad))==description(PSCAD.PSCADFormulation)=="PSCAD" + pscad=Formulation(:pscad;options=(base_frequency=60.0,)) + pscad_capture=E.completed_formulation(pscad) + @test first(pscad_capture.formulation_fields.Z).value==description(PSCAD.PSCADFormulation;compact=true) + @test any(control -> control.text==description(PSCAD.PSCADFormulation,Val(:base_frequency),60.0;compact=true), + first(pscad_capture.formulation_fields.Z).control_fields) + @test description(PSCAD.PSCADFormulation,Val(:reduce_bundle),false;compact=true)== + description(LineParametersFormulation,Val(:reduce_bundle),false;compact=true) + for value in (normal,LineCableModelsFEM(),Formulation(:pscad),MonteCarlo(normal),LinearError(normal)) + @test description(typeof(value))==description(value) + end + labels=description([MonteCarlo(normal),LinearError(normal)];roles=[:reference,:result]) + @test labels == ["Reference · Monte Carlo", "LEP"] + @test all(label->!occursin("earth Z",label),labels) + explicit_default=Formulation(earth_impedance=:unified) + @test formula_id(normal,R)==formula_id(explicit_default,R) + @test description([normal,explicit_default];quantity=R)==["coaxial","coaxial"] + @test description([normal];quantity=R)==description([explicit_default];quantity=R) + + f=[0.1,50.,100.,1e3,1e6,1e7] + z=reshape(complex.(collect(1.:24.),collect(101.:124.)),2,2,6) + y=reshape(complex.(collect(201.:224.),collect(301.:324.)),2,2,6)*1e-6 + ports=["a","b"] + ref=LineParameters(z,y,f;details=ComputationDetails(;coordinates=ports,E.completed_formulation(LineCableModelsFEM())...,)) + source_id=LineCableModels.Commons.gridpoint_id().source_id + completed(value,selection,index)=E.retain_gridpoint(value, + LineCableModels.Commons.gridpoint_id(;source_id,formulation_index=index); + fields=E.completed_formulation(selection)) + points=[completed(LineParameters(scale*z,y,f;details=ComputationDetails(;coordinates=ports,)),choices[index],index) + for (index,scale) in enumerate((1.1,1.2,1.3))] + results=ParametricResult(nothing,points,(problems=[:one],formulations=choices), ComputationDetails((;))) + source=(reference=ref,result=results) + result=report(BenchmarkTableDefinition((R,X,G,B);bands=(:all,:dc,:harmonic,:narrow,:wide)),source) + @test any(contains("TestAlpha"),result.tables.formulations.label) + @test any(contains("TestBeta"),result.tables.formulations.label) + observed_labels=LineCableModels.Commons.observation_labels(result.observed;request=R) + @test occursin(description(leaves[1];compact=true),observed_labels[1]) + @test all(label -> !occursin("internal Z",label),observed_labels) + @test length(observed_labels)==length(result.observed) + @test length(result.observed)==3 + @test result.observed[1].gridpoint.formulations==NamedTuple(choices[1]) + labels=description(choices[[3,1]];quantity=R) + @test occursin("earth Z=Unified",labels[1]) && occursin("earth Z=Display-TestAlpha",labels[2]) + @test all(label->!occursin("internal Z",label),labels) + @test all(feature -> !occursin("earth Y",join(feature.relative.formula)), + filter(feature -> feature.quantity in (:R,:X),result.tables.features)) + + for order in (:before,:after) + native=Formulation(earth_impedance=formula(:carson1926; + equivalent_earth=formula(:default;order))) + label=only(description([native];quantity=R)) + @test occursin(string(order)*" FrequencyDependent",label) + @test !occursin("FormulaDefinition{",label) + end + routed=Formulation( + earth_impedance=(air=:carson1926,earth=:pollaczek1926,mixed=:lucca1994), + earth_admittance=formula(:default;options=(integration=(method=:quad,options=(rtol=1e-9,)),))) + routed_result=ParametricResult(nothing,[completed(points[1],routed,1),completed(points[2],normal,2)], + (problems=[:one],formulations=[routed,normal]), ComputationDetails((;))) + routed_report=report(BenchmarkTableDefinition((R,B);bands=(:all,)), + (reference=ref,result=routed_result)) + @test any(contains("earth Z(air)=Carson"),first(routed_report.tables.features).relative.formula) + # Inspection retains a native selection without executing its equation. + selected_inner=FormulaFixtures.SurfaceLaw() + single=Formulation(insulation_admittance=FormulaFixtures.InsulationLaw(), + internal_impedance=selected_inner) + single_data=ParametricResult(nothing,[completed(points[1],single,1)], + (problems=[:one],formulations=[single]), ComputationDetails((;))) + single_report=report(BenchmarkTableDefinition((R,B);bands=(:all,)), + (reference=ref,result=single_data)) + single_label=only(last(single_report.tables.features).relative.formula) + @test occursin(description(single.methods.insulation_admittance;compact=true),single_label) + @test !occursin("scale",single_label) # a constant control is not a legend difference + insulation=only(filter(field -> field.meaning==(:insulation_admittance,),only(single_report.observed).gridpoint.formulation_fields.Y)) + @test any(control -> last(control.scope)===:scale,insulation.control_fields) + @test !occursin("insulation Y",only(first(single_report.tables.features).relative.formula)) + @test isempty(selected_inner.evaluations) && isempty(selected_inner.state_inputs) + fem_labels=description([LineCableModelsFEM(),LineCableModelsFEM(options=(physics=:quasi_fw,))]; + roles=[:reference,:reference]) + @test occursin("quasi-tem",first(fem_labels)) && occursin("quasi-fw",last(fem_labels)) + @test description([missing];roles=[:reference])==["Reference · method unavailable"] + + # A programming error is not absent metadata. The real composed consumer + # must execute the owned description and propagate its failure. + struct BrokenEarthLeaf <: E.EarthImpedanceFormulation end + formula_id(::BrokenEarthLeaf)=:BrokenTestLeaf + description(::BrokenEarthLeaf;compact::Bool=false)=throw(ArgumentError("test-owned description failed")) + formulation_options(::BrokenEarthLeaf)=FormulationOptions() + Base.NamedTuple(value::BrokenEarthLeaf)=(identifier=formula_id(value),parameters=(;),options=(;)) + broken=E.LineParametersFormulation(merge(normal.methods,(earth_impedance=BrokenEarthLeaf(),)), + normal.options,merge(normal.definitions,(earth_impedance=BrokenEarthLeaf(),))) + bad=ParametricResult(nothing,[points[1]],(problems=[:one],formulations=[broken]), ComputationDetails((;))) + @test_throws r"test-owned description failed" E.completed_formulation(broken) + @test only(report(BenchmarkTableDefinition(),(reference=ref,result=bad)).observed).gridpoint.formulations==NamedTuple(choices[1]) + + # Compare retained values and coordinates, not merely dimensions or file bytes. + for point in result.observed,row in point.errors + selected=filter(term -> term.result_id==row.result_id && term.request==row.request && + term.band==row.band && term.normalization==row.normalization,eachrow(result.tables.terms)) + for term in selected + @test term.response==ports[term.row] && term.excitation==ports[term.column] + @test isequal(term.absolute_rms,row.absolute[term.row,term.column]) + @test isequal(term.relative_rms_percent,100row.relative[term.row,term.column]) + end + end + @test Z(ref)==z && Y(ref)==y && frequencies(ref)==f + for row in eachrow(result.tables.maxima) + evidence=filter(term -> term.result_id==row.result_id && term.request==row.request && term.band==row.band && term.normalization==row.normalization,eachrow(result.tables.terms)) + largest=only(filter(term -> term.response==row.absolute_term_response && + term.excitation==row.absolute_term_excitation,evidence)) + @test row.maximum_absolute_rms==largest.absolute_rms + end + rendered=sprint(show,MIME"text/plain"(),result) + # New leaf descriptions must also reach the composed human-readable report. + @test all(leaf->occursin(description(leaf;compact=true),rendered),leaves) +end + +@testitem "Descriptions / quantity identities ignore unrelated slots and retain composite controls" tags=[:unit, :pscad] setup=[FormulaFixtures] begin + a=Formulation(earth_impedance=:saad1996) + b=Formulation(earth_impedance=:xue2018) + @test formula_id(a,Y)==formula_id(b,Y) + @test formula_id(a,Z)!=formula_id(b,Z) + @test formula_id(a,nothing)!=formula_id(b,nothing) + routed=Formulation(earth_impedance=(air=:default,earth=:default,mixed=:xue2018)) + other=Formulation(earth_impedance=(air=:default,earth=:default,mixed=:lucca1994)) + @test formula_id(routed,R)!=formula_id(other,R) + @test formula_id(routed,B)==formula_id(other,B) + internal=Formulation(internal_impedance=FormulaFixtures.SurfaceLaw()) + @test formula_id(internal,R)!=formula_id(Formulation(),R) + @test formula_id(internal,Y)==formula_id(Formulation(),Y) + overridden=Formulation(earth_admittance=formula(:default; + options=(integration=(method=:quad,options=(rtol=1e-9,)),))) + @test formula_id(overridden,Y)!=formula_id(Formulation(),Y) + @test formula_id(overridden,Z)==formula_id(Formulation(),Z) + @test formula_id(MonteCarlo(a),Y)!=formula_id(LinearError(a),Y) + @test ismissing(formula_id(missing,Y)) + @test description([Formulation()];quantity=Y)==[ + "shunt geometry=equivalent annular layer; insulation Y=Lossless; semicon Y=Lossless; earth Y=Unified; soil law=Constant; temperature law=Linear"] +end + +@testitem "Descriptions / detached differences use compact owner dispatch" tags=[:unit, :importexport, :slow] begin + import LineCableModels: description,formula_id,formulation_options + using LineCableModels.Engine: completed_formulation,retain_gridpoint + using LineCableModels.Commons: gridpoint_id,observation_labels,observation_groups + E=LineCableModels.Engine + IO=LineCableModels.ImportExport + raw=LineParameters(fill(1.0+2im,1,1,3),fill(3e-6+4e-6im,1,1,3),[1.,10.,100.]) + source_id=gridpoint_id().source_id + function point(selection,index;rho=100.,problem=1) + fields=merge(completed_formulation(selection),(inputs=(radius=.01,rho=rho, + field_descriptions=(radius=(name="radius",unit="m"),rho=(name="electrical resistivity",unit="Ω·m"))),)) + ObservedResult(retain_gridpoint(raw,gridpoint_id(;source_id,problem_index=problem,formulation_index=index);fields)) + end + a=Formulation(earth_impedance=:unified,earth_admittance=:unified) + fem=Formulation(:fem) + points=[point(a,1),point(fem,2)] + for request in (R,X,G,B) + method=request in (R,X) ? a.methods.earth_impedance : a.methods.earth_admittance + @test observation_labels(points;request)==[description(method;compact=true),description(fem;compact=true)] + end + @test all(field -> field.meaning ∉ ((:earth_impedance,),(:earth_admittance,)),points[2].gridpoint.formulation_fields.all) + physical=[point(a,1),point(a,1;rho=500.,problem=2)] + @test observation_labels(physical;request=R)==["electrical resistivity=100.0 Ω·m","electrical resistivity=500.0 Ω·m"] + @test length(observation_groups(physical;request=R))==2 + other=Formulation(earth_impedance=:pollaczek1926) + mixed=[physical[1],point(other,2;rho=500.,problem=2)] + labels=observation_labels(mixed;request=R) + @test occursin(description(other.methods.earth_impedance;compact=true),labels[2]) + @test occursin("500.0 Ω·m",labels[2]) + reductions=[point(Formulation(options=(kron_reduction=choice,)),i) for (i,choice) in enumerate((false,true))] + @test observation_labels(reductions;request=R)==[ + description(LineParametersFormulation,Val(:kron_reduction),v;compact=true) for v in (false,true)] + physics=[Formulation(:fem;options=(physics=choice,)) for choice in (:quasi_tem,:quasi_fw)] + @test observation_labels([point(f,i) for (i,f) in enumerate(physics)];request=B)==[ + description(LineCableModelsFEM,Val(:physics),f.options.data.physics;compact=true) for f in physics] + reordered=Formulation(options=(ideal_transposition=false,kron_reduction=true,reduce_bundle=true)) + @test observation_labels([point(a,1),point(reordered,2)];request=R)==fill(description(a.methods.earth_impedance;compact=true),2) + + layer(rho)=(rho=rho,field_descriptions=(rho=(name="electrical resistivity",unit="Ω·m"),)) + layered=[ObservedResult(merge(p.gridpoint,(inputs=(layers=[layer(100.),layer(rho)],),)),p.quantities,p.errors,p.timings) + for (p,rho) in zip(physical,(200.,300.))] + @test observation_labels(layered;request=R)==["electrical resistivity[2]=200.0 Ω·m","electrical resistivity[2]=300.0 Ω·m"] + + nested_radius=[ObservedResult(merge(p.gridpoint,(inputs=(designs=[(regions=[( + r=radius,field_descriptions=(r=(name="radius",unit="m"),))],)],),)), + p.quantities,p.errors,p.timings) + for (p,radius) in zip(physical,(0.005,0.01))] + @test observation_labels(nested_radius;request=R)== + ["radius[1][1]=0.005 m","radius[1][1]=0.01 m"] + + gamma=Formulation(earth_impedance=formula(:unified;options=(Γ=1.0,))) + gamma_labels=observation_labels([point(a,1),point(gamma,2)];request=R) + @test all(occursin("Γ=",label) for label in gamma_labels) + @test occursin(description(typeof(gamma.methods.earth_impedance),Val(:Γ),1.0;compact=true),gamma_labels[2]) + gamma_samples=[0.1,0.2,0.3] + gamma_vector=Formulation(earth_impedance=formula(:unified;options=(Γ=gamma_samples,)), + earth_admittance=formula(:unified;options=(Γ=gamma_samples,))) + for request in (R,B) + gamma_vector_labels=observation_labels([point(a,1),point(gamma_vector,2)];request) + selected=request===R ? gamma_vector.methods.earth_impedance : gamma_vector.methods.earth_admittance + @test occursin(description(typeof(selected),Val(:Γ),gamma_samples;compact=true),last(gamma_vector_labels)) + end + @test description(E.EarthImpedance.Formula{:default};compact=true)==description(a.methods.earth_impedance;compact=true) + @test description(E.EarthAdmittance.Formula{:default};compact=true)==description(a.methods.earth_admittance;compact=true) + + # New scientific owner, deliberately unrelated identifier and compact text. + const compact_calls=Bool[] + const poison=Ref(false) + struct DetachedNamingLeaf <: E.EarthImpedanceFormulation end + formula_id(::DetachedNamingLeaf)=:fixture_unrelated_identifier + function description(::DetachedNamingLeaf;compact::Bool=false) + poison[] && error("live description reopened") + push!(compact_calls,compact) + compact ? "Independent field solution" : "Verbose explanation for this fixture" + end + formulation_options(::DetachedNamingLeaf)=FormulationOptions() + Base.NamedTuple(::DetachedNamingLeaf)=(identifier=:fixture_unrelated_identifier,parameters=(;),options=(;)) + f=E.LineParametersFormulation(merge(a.methods,(earth_impedance=DetachedNamingLeaf(),)),a.options, + merge(a.definitions,(earth_impedance=DetachedNamingLeaf(),))) + extended=point(f,3) + @test !isempty(compact_calls) && all(compact_calls) + poison[]=true + Z(raw) .= NaN + expected=[description(a.methods.earth_impedance;compact=true),"Independent field solution"] + @test observation_labels([points[1],extended];request=R)==expected + restored=IO.deserialize_value(IO.serialize_value(extended)) + @test observation_labels([points[1],restored];request=R)==expected + @test restored.gridpoint==extended.gridpoint +end diff --git a/test/unit/earth/ehem.jl b/test/unit/earth/ehem.jl new file mode 100644 index 000000000..eb01b60da --- /dev/null +++ b/test/unit/earth/ehem.jl @@ -0,0 +1,85 @@ +@testitem "Earth / explicit reduction and FrequencyDependent order through public compute" tags=[:unit, :parametric, :slow] setup=[ + TestFixtures, FormulaFixtures] begin + const E=LineCableModels.Engine + const EP=LineCableModels.Earth + system=TestFixtures.three_phase_system() + earth=build(EP.EarthModel, ( + EP.EarthLayer(100.0, 10.0, 1.0, 0.5), EP.EarthLayer(500.0, 20.0, 1.0))) + problem=LineParametersProblem(system; earth_props = earth, frequencies = [50.0]) + reduced=compute(problem, + Formulation( + earth_impedance = formula(:default; equivalent_earth = formula(:default)), + earth_admittance = formula(:default; equivalent_earth = formula(:default)))) + # A homogeneous formula on a layered earth uses the `:default` reduction unless one is given. + unspecified=compute(problem, Formulation()) + @test unspecified.Z.values == reduced.Z.values && unspecified.Y.values == reduced.Y.values + homogeneous_problem=LineParametersProblem( + system; earth_props = homogeneous(rho = 500.0, eps_r = 20.0), frequencies = [50.0]) + @test reduced.Z.values ≈ compute(homogeneous_problem, Formulation()).Z.values + events=Symbol[] + law=FormulaFixtures.DispersiveEarth(scale = 50.0, events = events) + reduction=FormulaFixtures.SquaredBottomEarth(events) + for order in (:before, :after) + empty!(events) + empty!(reduction.workspaces) + empty!(law.seen) + sequence=order===:before ? EP.EquivalentHomogeneous.BeforeFD(reduction) : + EP.EquivalentHomogeneous.AfterFD(reduction) + selected=Formulation(earth_properties = law, + earth_impedance = formula(:default; equivalent_earth = sequence), + earth_admittance = formula(:default; equivalent_earth = sequence)) + result=compute(problem, selected) + expected_rho=order===:before ? 1250.0 : 625.0 + expected=compute( + LineParametersProblem( + system; earth_props = homogeneous(rho = expected_rho, eps_r = 20.0), + frequencies = [50.0]), + Formulation()) + @test result.Z.values ≈ expected.Z.values + pairs=count(==(:ehem), events) + @test pairs > 0 + workspace=first(reduction.workspaces) + @test workspace isa E.LineParametersWorkspace + @test all(w -> w === workspace, reduction.workspaces) + @test all(record -> last(record) === workspace, law.seen) + @test events == (order===:before ? repeat([:ehem, :fd], pairs) : + vcat(fill(:fd, length(earth.layers)-1), fill(:ehem, pairs))) + @test details(result).data.formulations.methods.earth_impedance.equivalent_earth.order === + order + @test details(result).data.formulations.methods.earth_admittance.equivalent_earth.order === + order + @test details(result).data.formulations.methods.earth_properties.identifier === + :DispersiveEarth + @test details(result).data.formulations.methods.earth_impedance.equivalent_earth.rule.identifier === + :SquaredBottomEarth + end +end + +@testitem "Earth / a missing reduction expression fails before any evaluation" tags=[:unit, :engine] setup=[ + TestFixtures] begin + const EH = LineCableModels.Earth.EquivalentHomogeneous + const E = LineCableModels.Engine + struct UnimplementedReduction <: EH.AbstractRule + parameters::NamedTuple + options::FormulationOptions + end + LineCableModels.formula_id(::UnimplementedReduction) = :UnimplementedReduction + LineCableModels.formulation_options(::LineCableModels.Expression{ + UnimplementedReduction, typeof(EH.equivalent_material)}) = FormulationOptions() + rule = UnimplementedReduction((;), FormulationOptions()) + pair = LineCableModels.Engine.EarthPair(1, 1, (-1.0, -1.0), 0.0, (2, 2); radius=0.01) + model = homogeneous(rho=100.0, eps_r=10.0) + expression = LineCableModels.Expression(rule, pair) + @test expression.selection === rule + message = "formula :UnimplementedReduction has no expression for :self, 2, 2" + @test_throws message LineCableModels.validate(expression) + @test_throws message rule([Inf, 100.0], [1.0, 10.0], [1.0, 1.0], model, pair, 50.0) + # The plan checks the reduction's expressions before the frequency loop. + LineCableModels.validate(reduction::UnimplementedReduction, ::LineCableModels.Expression{ + <:Union{E.EarthImpedance.Formula, E.EarthAdmittance.Formula}}) = reduction + problem = LineParametersProblem(TestFixtures.three_phase_system(); earth_props = model, + frequencies = [50.0]) + reduced = formula(:default; equivalent_earth = EH.AfterFD(rule)) + @test_throws message compute(problem, + Formulation(earth_impedance = reduced, earth_admittance = reduced)) +end diff --git a/test/unit/earth/equivalent_earth_inputs.jl b/test/unit/earth/equivalent_earth_inputs.jl new file mode 100644 index 000000000..6288a81d4 --- /dev/null +++ b/test/unit/earth/equivalent_earth_inputs.jl @@ -0,0 +1,40 @@ +@testitem "Earth / explicit reduction validates inputs and admits a user-owned rule" tags=[:unit, :engine] setup=[FormulaFixtures] begin + const EP = LineCableModels.Earth + const EH = EP.EquivalentHomogeneous + const E = LineCableModels.Engine + rule = EH.Formula(:default) + @test EH.Formula(rule) === rule + @test occursin("Bottommost", description(rule)) + @test occursin("before", description(EH.BeforeFD(:default))) + @test occursin("after", description(EH.AfterFD(:default))) + @test EH.rule(EH.BeforeFD(rule)) === rule + @test EH.Formula(formula(:default)) isa typeof(rule) + @test_throws ArgumentError EH.Formula(formula(:default; order = :before)) + @test_throws ArgumentError EH.Formula(formula(:default; equivalent_earth = formula(:default))) + @test_throws ArgumentError EH.AbstractSequence(formula(:default; equivalent_earth = formula(:default))) + @test_throws ArgumentError EH.Formula(:default;parameters=(rho=10.0,)) + model = build(EP.EarthModel, (EP.EarthLayer(100.0, 10.0, 1.0, 0.5), + EP.EarthLayer(500.0, 20.0, 1.0))) + pair = E.EarthPair(1, 2, (-0.25, -1.0), 0.75, (2, 3)) + rho = [Inf, 100.0, 500.0] + epsilon = [1.0, 10.0, 20.0] + mu = [1.0, 1.0, 1.0] + @test_throws DimensionMismatch rule(rho[1:2], epsilon, mu, model, pair, 50.0) + for frequency in (0.0, -50.0, Inf, NaN) + @test_throws DomainError rule(rho, epsilon, mu, model, pair, frequency) + end + original = (copy(rho), copy(epsilon), copy(mu)) + selected=FormulaFixtures.MeanEarth() + @test EH.Formula(selected) === selected + material = selected(rho, epsilon, mu, model, pair, 50.0) + @test length(selected.seen) == 1 + @test (material.rho, material.eps_r, material.mu_r) == (300.0, 15.0, 1.0) + @test (rho, epsilon, mu) == original + converted = convert(EP.EarthMaterial{Float32}, material) + @test eltype(converted) === Float32 + @test eltype(typeof(converted)) === Float32 + @test (converted.rho, converted.eps_r, converted.mu_r) == (300.0f0, 15.0f0, 1.0f0) + @test convert(EP.EarthMaterial{Float32}, converted) === converted + restored = convert(EP.EarthMaterial{Float64}, converted) + @test (restored.rho, restored.eps_r, restored.mu_r) == (material.rho, material.eps_r, material.mu_r) +end diff --git a/test/unit/earth/formulas.jl b/test/unit/earth/formulas.jl new file mode 100644 index 000000000..5ff33f161 --- /dev/null +++ b/test/unit/earth/formulas.jl @@ -0,0 +1,64 @@ +@testitem "Earth / static and user-owned frequency-dependent relations" tags=[:unit, :measurements] setup=[FormulaFixtures] begin + using Measurements: measurement,uncertainty + const EP=LineCableModels.Earth + relation=EP.FrequencyDependent.Formula(:default) + for T in (Float32,Float64,BigFloat) + material=EP.EarthMaterial(T(100),T(10),T(1)) + @test relation(material,T(50)) === material + end + material=EP.EarthMaterial(measurement(100.0,2.0),measurement(10.0,0.2),measurement(1.0,0.01)) + @test uncertainty(relation(material,50.0).rho)==2.0 + selected=FormulaFixtures.DispersiveEarth() + @test EP.FrequencyDependent.Formula(selected) === selected + @test selected(EP.EarthMaterial(100.0,10.0,1.0),100.0).rho==50.0 + @test only(selected.seen)[2]==100.0 + @test_throws DomainError selected(EP.EarthMaterial(100.0,10.0,1.0),0.0) + @test_throws ArgumentError EP.FrequencyDependent.Formula(:default;parameters=(unknown=1,)) +end + +@testitem "Earth / soil coefficients are checked at construction" tags=[:unit, :earth] begin + const FD=LineCableModels.Earth.FrequencyDependent + for id in FD.formulas(FD.Formula) + selected=FD.Formula(id) + for name in keys(selected.parameters), value in ("bad",true,NaN,Inf,1+im) + @test_throws ArgumentError FD.Formula(id;parameters=NamedTuple{(name,)}((value,))) + end + end + for (id,name,value) in ((:longmire1975,:corner_scale,0.0), + (:visacro1987,:normalization_frequency,-1.0), + (:visacro2012,:frequency_boundary,0.0), + (:messier1985,:epsilon_infinity,-1.0), + (:portela1999,:exponent,1.0),(:alipio2014,:exponent,-1.0)) + @test_throws ArgumentError FD.Formula(id;parameters=NamedTuple{(name,)}((value,))) + end +end + +@testitem "Earth / published dispersive relations match numerical references" tags=[:unit, :earth] begin + const EP=LineCableModels.Earth + const FD=EP.FrequencyDependent + material=EP.EarthMaterial(100.0,10.0,1.0) + # Independently evaluated from the published expressions documented by + # each formula, for rho_0 = 100 ohm m and f = 10 kHz. + references=( + alipio2014=(98.085845947624506,409.88833451812962), + cigre2019=(98.078364240070741,408.02591429681854), + datsios2019=(99.953087658615345,77.228148054469685), + longmire1975=(91.26463633360153,548.99524080259334), + messier1985=(97.1029453119354,544.28505232978239), + portela1999=(97.229937848620864,1088.2832591726246), + scott1967=(101.39113857366789,542.00089040162334), + visacro1987=(71.779429127136169,815.10931718426775), + visacro2012=(98.649670481737814,192.20336879472805), + ) + for (identifier,(rho,eps_r)) in pairs(references) + evaluated=FD.Formula(identifier)(material,1.0e4) + @test evaluated.rho ≈ rho rtol=2e-14 + @test evaluated.eps_r ≈ eps_r rtol=2e-14 + @test evaluated.mu_r === material.mu_r + end + + visacro=FD.Formula(:visacro2012) + @test visacro(material,50.0) == visacro(material,100.0) + datsios=FD.Formula(:datsios2019) + @test datsios(material,50.0).eps_r == datsios(material,3000.0).eps_r +end diff --git a/test/unit/earth/properties.jl b/test/unit/earth/properties.jl new file mode 100644 index 000000000..597f8567c --- /dev/null +++ b/test/unit/earth/properties.jl @@ -0,0 +1,199 @@ +@testsnippet current_earth begin + const EP = LineCableModels.Earth +end + +@testitem "Earth / ephemeral material validation and promotion" tags=[:unit, :measurements] setup=[ + current_earth, +] begin + using Measurements: Measurement, measurement, uncertainty, value + + material=EP.EarthMaterial(100, 10.0f0, 1) + @test material isa EP.EarthMaterial{Float64} + @test material isa LineCableModels.AbstractMaterial + @test (material.rho, material.eps_r, material.mu_r) == (100.0, 10.0, 1.0) + @test convert(EP.EarthMaterial{Float64}, material) === material + @test EP.EarthMaterial(100.0, -1.0, 1.0).eps_r == -1.0 + + uncertain=EP.EarthMaterial( + measurement(100.0, 2.0), + measurement(10.0, 0.5), + measurement(1.0, 0.01) + ) + @test uncertain isa EP.EarthMaterial{Measurement{Float64}} + @test value(uncertain.rho) == 100.0 + @test uncertainty(uncertain.eps_r) == 0.5 + + @test_throws DomainError EP.EarthMaterial(0.0, 10.0, 1.0) + @test_throws DomainError EP.EarthMaterial(NaN, 10.0, 1.0) + @test_throws DomainError EP.EarthMaterial(100.0, 0.0, 1.0) + @test_throws DomainError EP.EarthMaterial(100.0, 10.0, Inf) +end + +@testitem "Earth / static layer validation and promotion" tags=[:unit, :measurements] setup=[ + current_earth, +] begin + using Measurements: Measurement, measurement, uncertainty, value + + layer=EP.EarthLayer(100, 10.0f0, 1, 20.0) + @test layer isa EP.EarthLayer{Float64} + @test (layer.rho, layer.eps_r, layer.mu_r, layer.thickness) == + (100.0, 10.0, 1.0, 20.0) + + layer32=EP.EarthLayer(100.0f0, 10.0f0, 1.0f0) + @test layer32 isa EP.EarthLayer{Float32} + @test isinf(layer32.thickness) + @test convert(EP.EarthLayer{Float32}, layer32) === layer32 + + layer_measurement=convert(EP.EarthLayer{Measurement{Float64}}, layer) + @test value(layer_measurement.rho) == 100.0 + @test uncertainty(layer_measurement.rho) == 0.0 + @test value(layer_measurement.thickness) == 20.0 + + promoted=EP.EarthLayer( + measurement(100.0, 2.0), 10.0, 1.0, measurement(20.0, 0.1) + ) + @test promoted isa EP.EarthLayer{Measurement{Float64}} + @test uncertainty(promoted.rho) == 2.0 + @test uncertainty(promoted.thickness) == 0.1 + + @test_throws DomainError EP.EarthLayer(0.0, 10.0, 1.0) + @test_throws DomainError EP.EarthLayer(NaN, 10.0, 1.0) + @test_throws DomainError EP.EarthLayer(100.0, 0.0, 1.0) + @test_throws DomainError EP.EarthLayer(100.0, 10.0, Inf) + @test_throws DomainError EP.EarthLayer(100.0, 10.0, 1.0, 0.0) +end + +@testitem "Earth / immutable static model ownership" tags=[:unit, :measurements] setup=[ + current_earth, +] begin + using Measurements: Measurement, measurement, uncertainty, value + + model=@inferred build(EP.EarthModel, + ( + EP.EarthLayer(100.0, 10.0, 1.0, 20.0), + EP.EarthLayer(200.0, 15.0, 1.0, 40.0), + EP.EarthLayer(500.0, 20.0, 1.0) + )) + @test model isa EP.EarthModel{Float64} + @test !ismutabletype(typeof(model)) + @test !model.vertical_layers + @test model.layers isa NTuple{4, EP.EarthLayer{Float64}} + @test length(model.layers) == 4 + @test isinf(first(model.layers).rho) + @test isinf(first(model.layers).thickness) + @test isinf(last(model.layers).thickness) + @test !hasfield(typeof(model), :frequencies) + @test !hasfield(typeof(model), :freq_dependence) + @test getproperty.(model.layers, :rho) == (Inf, 100.0, 200.0, 500.0) + @test !applicable(add!, model, EP.EarthLayer(800.0, 25.0, 1.0)) + @test_throws MethodError setindex!( + model.layers, EP.EarthLayer(800.0, 25.0, 1.0), 2 + ) + @test_throws ArgumentError build( + EP.EarthModel, ( + EP.EarthLayer(100.0, 10.0, 1.0), + EP.EarthLayer(800.0, 25.0, 1.0, 10.0) + )) + + converted=@inferred convert( + EP.EarthModel{Float32}, EP.EarthModel(100.0, 10.0, 1.0) + ) + @test converted isa EP.EarthModel{Float32} + @test convert(EP.EarthModel{Float32}, converted) === converted + + uncertain=convert(EP.EarthModel{Measurement{Float64}}, model) + @test uncertain isa EP.EarthModel{Measurement{Float64}} + @test value(uncertain.layers[2].rho) == 100.0 + @test uncertainty(uncertain.layers[2].rho) == 0.0 + + vertical=build(EP.EarthModel, + ( + EP.EarthLayer(100.0, 10.0, 1.0), + EP.EarthLayer(200.0, 15.0, 1.0, 20.0), + EP.EarthLayer(500.0, 20.0, 1.0) + ); + vertical_layers = true) + @test vertical.vertical_layers + @test length(vertical.layers) == 4 + @test_throws ArgumentError EP.EarthModel( + 100.0, 10.0, 1.0; thickness = 20.0, vertical_layers = true + ) +end + +@testitem "Earth / concise display" tags=[:unit, :earth] setup=[current_earth] begin + model=build(EP.EarthModel, ( + EP.EarthLayer(100.0, 10.0, 1.0, 20.0), + EP.EarthLayer(500.0, 20.0, 1.0) + )) + + shown=sprint(show, "text/plain", model) + @test contains(shown, "EarthModel · 2 earth layers") + @test contains(shown, "air") + @test contains(shown, "layer 1") + @test contains(shown, "basement") + @test contains(shown, "ρ=100 Ω·m") + @test !contains(shown, "frequency") +end + +@testitem "Earth / static constitutive pass-through" tags=[:unit, :measurements] setup=[ + current_earth, +] begin + using Measurements: Measurement, measurement, uncertainty, value + + model=build(EP.EarthModel, ( + EP.EarthLayer(100.0, 10.0, 1.0, 20.0), + EP.EarthLayer(500.0, 20.0, 2.0) + )) + frequencies=[50.0, 60.0, 1.0e3] + formulation=LineCableModels.Engine.Formulation() + @test formula_id(formulation.methods.earth_properties) === :constant + material=EP.EarthMaterial(model.layers[2]) + @test constitutive(nothing, material, first(frequencies)) === material + properties=LineCableModels.Engine.materials!( + (rho = zeros(3, 3), eps_r = zeros(3, 3), mu_r = zeros(3, 3)), + formulation.methods.earth_properties, model, frequencies + ) + static_rho=repeat(collect(getproperty.(model.layers, :rho)), 1, length(frequencies)) + static_eps_r=repeat( + collect(getproperty.(model.layers, :eps_r)), 1, length(frequencies) + ) + static_mu_r=repeat( + collect(getproperty.(model.layers, :mu_r)), 1, length(frequencies) + ) + + @test size(properties.rho) == (3, 3) + @test size(properties.eps_r) == (3, 3) + @test size(properties.mu_r) == (3, 3) + @test properties.rho[:, 1] == [Inf, 100.0, 500.0] + @test all(properties.rho[:, column] == properties.rho[:, 1] + for column in axes(properties.rho, 2)) + @test properties.rho == static_rho + @test properties.eps_r == static_eps_r + @test properties.mu_r == static_mu_r + + model32=EP.EarthModel(100.0f0, 10.0f0, 1.0f0) + properties32=LineCableModels.Engine.materials!( + (rho = zeros(Float32, 2, 2), eps_r = zeros(Float32, 2, 2), + mu_r = zeros(Float32, 2, 2)), + formulation.methods.earth_properties, model32, Float32[50, 60] + ) + @test eltype(properties32.rho) === Float32 + @test eltype(properties32.eps_r) === Float32 + @test eltype(properties32.mu_r) === Float32 + + uncertain_model=EP.EarthModel( + measurement(100.0, 2.0), measurement(10.0, 0.5), measurement(1.0, 0.01) + ) + uncertain_frequency=measurement.([50.0, 60.0], 0.0) + uncertain=LineCableModels.Engine.materials!( + (rho = zeros(Measurement{Float64}, 2, 2), + eps_r = zeros(Measurement{Float64}, 2, 2), + mu_r = zeros(Measurement{Float64}, 2, 2)), + formulation.methods.earth_properties, uncertain_model, uncertain_frequency + ) + @test eltype(uncertain.rho) === Measurement{Float64} + @test value(uncertain.rho[2, 1]) == 100.0 + @test uncertainty(uncertain.rho[2, 1]) == 2.0 + @test uncertainty(uncertain.eps_r[2, 1]) > 0 + @test uncertainty(uncertain.mu_r[2, 1]) > 0 +end diff --git a/test/unit/earth/reduction_selections.jl b/test/unit/earth/reduction_selections.jl new file mode 100644 index 000000000..b64a8d4bc --- /dev/null +++ b/test/unit/earth/reduction_selections.jl @@ -0,0 +1,31 @@ +@testitem "Analytical selections / explicit earth reduction preserves formula and indexed geometry" tags=[:unit, :parametric] begin + const EP = LineCableModels.Earth + copper = Material(:conductor, 1.72e-8, 1.0) + design = build(CableDesign, "explicit-reduction", terminal(:core, + solid(copper, Disk(0.004)), insulation(Material(:insulator, 1e14, 2.3); t = 0.002))) + earth = build(EP.EarthModel, (EP.EarthLayer(100.0, 10.0, 1.0, 0.5), + EP.EarthLayer(500.0, 20.0, 1.0))) + for height in (2.0, -1.0) + system = build(LineCableSystem, [design, design], [Pose2(0, height), Pose2(0.75, height)]; + connections = [Dict(:core => 1), Dict(:core => 2)]) + original = LineParametersProblem(system; earth_props = earth, frequencies = [50.0]) + equivalent = LineParametersProblem(system; + earth_props = homogeneous(rho = 500.0, eps_r = 20.0), frequencies = [50.0]) + for identifier in (:default, :unified), admittance_id in (:default, :unified) + requested = Formulation(earth_impedance = formula(identifier; + equivalent_earth = formula(:default)), + earth_admittance = formula(admittance_id; equivalent_earth = formula(:default))) + reduced = compute(original, requested) + direct = compute(equivalent, Formulation(earth_impedance = identifier, + earth_admittance = admittance_id)) + @test Z(reduced) ≈ Z(direct) rtol = 1e-10 + @test Y(reduced) ≈ Y(direct) rtol = 1e-10 + @test details(reduced).data.formulations.methods.earth_impedance.identifier === + (identifier === :default ? :unified : identifier) + @test details(reduced).data.formulations.methods.earth_admittance.identifier === + (admittance_id === :default ? :unified : admittance_id) + @test length(original.earth_props.layers) == 3 + @test original.earth_props === earth + end + end +end diff --git a/test/unit/engine/analytical_earth_impedance.jl b/test/unit/engine/analytical_earth_impedance.jl new file mode 100644 index 000000000..a4fa43dbc --- /dev/null +++ b/test/unit/engine/analytical_earth_impedance.jl @@ -0,0 +1,160 @@ +@testitem "Engine / analytical earth impedances / source equations and scalar types" tags=[:unit, :engine] begin + const E = LineCableModels.Engine + # Independently evaluated with mpmath at 80 decimal digits from Saad (5)/(6), + # Wedepohl-Wilcox (7)/(8), and PSCAD (8-28)/(8-34). All lengths are in m. + fixtures = ( + (:saad1996, :self, (-1, -1), 0, (2, 2), 1, + "0.000049436050542009708503711029366076531443284669219897", "0.00071885090587064612822779262048186031909301811589599"), + (:saad1996, :mutual, (-1, -2), 0.75, (2, 2), 1, + "0.000049479207377905126939332583978927107226053797161543", "0.00041543494490419351553063029561713015092742757804909"), + (:saad1996, :mutual, (-1, -2), 0, (2, 2), 1, + "0.000049479424322246073989373455794913818704375130284438", "0.00042945544041135657926423194846619726345300197040710"), + (:wedepohl1973, :self, (-1, -1), 0, (2, 2), 2.5, + "0.00012383531262692624137104164156086318287741590034191", "0.0017249149591946593457986273924888321274339160401549"), + (:wedepohl1973, :mutual, (-1, -2), 0.75, (2, 2), 2.5, + "0.00012406794143358087068884689359206882972041322921760", "0.00096625258207122531418241849265399990355136359011624"), + (:gary1976, :self, (10, 10), 0, (1, 1), 1, + "0.000048477544121804667108869118283828124463065274088964", "0.00072467343587872156465893473991564770639800889262178"), + (:gary1976, :mutual, (10, 12), 0.75, (1, 1), 1, + "0.000048391816886193307359343300382265063290248575131592", "0.00038772498399398139118419994348600879217263733480040"), + (:lucca1994, :mutual, (10, -2), 0.75, (1, 2), 1, + "0.000048694756966967542207217998198776082068243035825963", "0.00027361164043745607069020182417817933205189613540840"), + (:lucca1994, :mutual, (-2, 10), 0.75, (2, 1), 1, + "0.000048694756966967542207217998198776082068243035825963", "0.00027361164043745607069020182417817933205189613540840")) + # The standalone call and the expression evaluated on the pair's Functor agree. + function evaluate(selected, rho, epsilon, mu, s, pair) + value = @inferred selected(rho, epsilon, mu, s, pair) + expression = LineCableModels.Expression(selected, pair) + options = only(formulation_options(selected, (expression,)).options) + functor = LineCableModels.Functor(selected, (; jω = s, thickness = nothing, pair, + physical = pair, rho, epsilon, mu, options), (;)) + @test (@inferred expression(functor, nothing)) == value + return value + end + for T in (Float32, Float64, BigFloat), + (id, kind, heights, x, layers, μr, re, im) in fixtures + + μ0 = 4T(pi) * T(10)^(-7) + ε0 = parse(T, "8.8541878128e-12") + s = complex(zero(T), 100T(pi)) + pair = E.EarthPair(1, kind === :self ? 1 : 2, T.(heights), T(x), layers; + radius = kind === :self ? T(1)/100 : nothing) + value = evaluate(E.EarthImpedance.Formula(id), T[Inf, 100], [ε0, 10ε0], + [μ0, T(μr)*μ0], s, pair) + @test value isa Complex{T} + @test isfinite(value) + T === Float32 || (@test value ≈ complex(parse(T, re), parse(T, im)) rtol=5e-14) + end + # Changing radius affects only the explicit self distance for this expression. + μ0, ε0, s = 4pi*1e-7, 8.8541878128e-12, 100pi*im + gary = E.EarthImpedance.Formula(:gary1976) + self(r) = gary([Inf, 100.0], [ε0, 10ε0], [μ0, μ0], s, + E.EarthPair(1, 1, (10.0, 10.0), 0.0, (1, 1); radius = r)) + @test self(0.02)-self(0.01) ≈ -s*μ0/(2pi)*log(2) rtol=5e-14 +end + +@testitem "Engine / analytical earth impedances / uncertainty dependencies" tags=[:unit, :measurements] begin + using Measurements + const E = LineCableModels.Engine + μ0, ε0, s = 4pi*1e-7, 8.8541878128e-12, 100pi*im + function evaluate(id, rho, heights, layers) + pair = E.EarthPair(1, 1, heights, 0.0, layers; radius = 0.01) + id === :lucca1994 && (pair = E.EarthPair(1, 2, heights, 0.75, layers)) + selected = E.EarthImpedance.Formula(id) + T = typeof(rho) + return @inferred selected(T[Inf, rho], T[ε0, 10ε0], T[μ0, μ0], Complex{T}(s), pair) + end + rho = measurement(100.0, 2.0) + for (id, heights, layers) in ((:saad1996, (-1.0, -1.0), (2, 2)), + (:wedepohl1973, (-1.0, -1.0), (2, 2)), + (:gary1976, (10.0, 10.0), (1, 1)), (:lucca1994, (10.0, -2.0), (1, 2))) + z = evaluate(id, rho, heights, layers) + shared = evaluate(id, rho, heights, layers) + independent = evaluate(id, measurement(100.0, 2.0), heights, layers) + @test z isa Complex{Measurement{Float64}} + @test Measurements.value(real(z)) ≈ real(evaluate(id, 100.0, heights, layers)) + @test Measurements.value(imag(z)) ≈ imag(evaluate(id, 100.0, heights, layers)) + @test iszero(uncertainty(real(z-shared))) + @test iszero(uncertainty(imag(z-shared))) + @test uncertainty(real(z-independent)) > 0 + @test uncertainty(imag(z-independent)) > 0 + end + # Gary self: p ∝ sqrt(rho), dZ/drho = (s μ0 / 2π) p / (2rho(h+p)). + z = evaluate(:gary1976, rho, (10.0, 10.0), (1, 1)) + p = sqrt(100/(s*μ0)) + sensitivity = s*μ0/(2pi) * p/(200*(10+p)) + @test Measurements.derivative(real(z), rho) ≈ real(sensitivity) rtol=2e-13 + @test Measurements.derivative(imag(z), rho) ≈ imag(sensitivity) rtol=2e-13 +end + +@testitem "Engine / analytical earth impedances / public computations and material transport" tags=[:unit, :parametric, :slow] setup=[FormulaFixtures] begin + using LinearAlgebra + const E=LineCableModels.Engine + metal=Material(:conductor, 1.72e-8, 1.0) + dielectric=Material(:insulator, Inf, 2.3) + design=build(CableDesign, "analytical-earth", + terminal(:core, + core(metal; r = 0.004), insulation(dielectric; t = 0.002))) + options=(reduce_bundle = false, kron_reduction = false, ideal_transposition = false) + for heights in ((10.0, 12.0), (-1.0, -2.0), (10.0, -2.0)) + system=build(LineCableSystem, fill(design, 2), + [Pose2(0, heights[1]), Pose2(0.75, heights[2])]; + connections = [Dict(:core=>1), Dict(:core=>2)]) + problem=LineParametersProblem(system; earth_props = homogeneous(rho = 100.0), + frequencies = [50.0, 500.0]) + for buried in (:saad1996, :wedepohl1973) + selected=Formulation( + earth_impedance = (air = :gary1976, earth = buried, mixed = :lucca1994), + earth_admittance = :ideal; options) + result=compute(problem, selected; options = (trace = true,)) + trace=details(result).data.trace + @test size(Z(result)) == size(Y(result)) == (2, 2, 2) + @test result.f == problem.frequencies + @test all(isfinite, Z(result)) && all(isfinite, Y(result)) + @test occursin("Gary", description(selected, Z)) + for k in 1:2 + @test Z(result)[:, :, k] ≈ trace.Z[:, :, k] + @test Y(result)[:, :, k] ≈ + 2pi*problem.frequencies[k]*im*inv(trace.P[:, :, k]) + @test trace.P[:, :, k] ≈ trace.Pin[:, :, k]+trace.Pg[:, :, k] + end + # The existing frequency loop supplies the law once per soil and frequency. + fd=FormulaFixtures.DispersiveEarth() + dispersive=Formulation(earth_impedance = selected.methods.earth_impedance, + earth_admittance = :ideal, earth_properties = fd; options) + actual=compute(problem, dispersive; options = (trace = true,)) + @test length(fd.seen) == 2 + for (k, frequency) in enumerate(problem.frequencies) + reference_problem=LineParametersProblem(system; + earth_props = homogeneous(rho = 100/(1+frequency/100)), frequencies = [frequency]) + reference=compute(reference_problem, selected) + @test Z(actual)[:, :, k] ≈ Z(reference)[:, :, 1] + @test Y(actual)[:, :, k] ≈ Y(reference)[:, :, 1] + end + end + # One Unified quantity still constructs its complete system when the other + # quantity uses a direct equation. Compare each quantity to its full run. + full=compute(problem, Formulation(; options)) + unified_Z=compute(problem, Formulation(earth_impedance = :unified, + earth_admittance = :ideal; options)) + unified_P=compute(problem, + Formulation( + earth_impedance = (air = :gary1976, earth = :saad1996, mixed = :lucca1994), + earth_admittance = :unified; options)) + @test Z(unified_Z) == Z(full) + @test Y(unified_P) == Y(full) + if all(<(0), heights) + partial=Formulation(earth_impedance = (earth = :saad1996,), + earth_admittance = (earth = :ideal,); options) + reference=compute(problem, partial) + space=Formulation(earth_impedance = Grid((:saad1996, :wedepohl1973)), + earth_admittance = :ideal; options) + results=compute(problem, space) + @test length(results) == 2 + @test Z(results[1]) == Z(reference) + @test Y(results[1]) == Y(reference) + @test Z(results[2]) == Z(compute(problem, + Formulation(earth_impedance = :wedepohl1973, earth_admittance = :ideal; options))) + end + end +end diff --git a/test/unit/engine/benchmark.jl b/test/unit/engine/benchmark.jl new file mode 100644 index 000000000..b343c41f1 --- /dev/null +++ b/test/unit/engine/benchmark.jl @@ -0,0 +1,125 @@ +@testitem "Engine / line parameters / RMS benchmark" tags=[:unit, :report] setup=[] begin + using Test + using DataFrames + using LineCableModels + using LineCableModels.Engine + + frequencies_value=[1.0, 10.0] + impedance=Array{ComplexF64, 3}(undef, 2, 2, 2) + admittance=similar(impedance) + impedance[:, :, 1]=[1+2im 2+0im; 3+0im 4+1im] + impedance[:, :, 2]=[2+3im 4+0im; 6+0im 8+2im] + admittance[:, :, 1]=[2im 1im; 1im 4im] + admittance[:, :, 2]=[4im 2im; 2im 8im] + reference=LineParameters(PhaseDomain, impedance, admittance, frequencies_value) + + identical=compare(reference, reference) + @test identical.Z.absolute == zeros(2, 2) + @test identical.Z.relative == zeros(2, 2) + @test identical.Y.absolute == zeros(2, 2) + @test identical.Y.relative == zeros(2, 2) + + result=LineParameters( + PhaseDomain, + impedance .+ reshape(ComplexF64[1, 2, 3, 4], 2, 2, 1), + admittance .+ reshape(ComplexF64[2im, 3im, 4im, 5im], 2, 2, 1), + frequencies_value + ) + comparison=compare(reference, result) + comparison_observables=observables( + comparison, + ( + (Z, absolute_error), + (Z, relative_error), + (Y, absolute_error), + (Y, relative_error) + ) + ) + @test comparison_observables isa ObservedResult + @test comparison_observables.quantities[1].values == + 1_000comparison.Z.absolute + @test comparison_observables.quantities[2].values == + comparison.Z.relative + @test comparison_observables.quantities[3].values == + 1_000comparison.Y.absolute + @test comparison_observables.quantities[4].values == + comparison.Y.relative + @test comparison_observables.quantities[1].values !== + comparison.Z.absolute + @test comparison_observables.quantities[4].values !== + comparison.Y.relative + @test comparison.Z.absolute ≈ [1.0 3.0; 2.0 4.0] + @test comparison.Z.relative[1, 1] ≈ + sqrt(2 / sum(abs2, impedance[1, 1, :])) + @test comparison.Z.relative[2, 2] ≈ + sqrt(32 / sum(abs2, impedance[2, 2, :])) + @test comparison.Y.absolute ≈ [2.0 4.0; 3.0 5.0] + @test comparison.Y.relative[1, 1] ≈ + sqrt(8 / sum(abs2, admittance[1, 1, :])) + + zero_reference=LineParameters( + PhaseDomain, + zeros(ComplexF64, 2, 2, 2), + zeros(ComplexF64, 2, 2, 2), + frequencies_value + ) + @test all(ismissing, compare(zero_reference, zero_reference).Z.relative) + @test all(ismissing, compare(zero_reference, result).Z.relative) + + different_frequency=LineParameters( + PhaseDomain, impedance, admittance, [1.0, 11.0] + ) + @test_throws ArgumentError compare(reference, different_frequency) + + different_size=LineParameters( + PhaseDomain, + zeros(ComplexF64, 3, 3, 2), + zeros(ComplexF64, 3, 3, 2), + frequencies_value + ) + @test_throws DimensionMismatch compare(reference, different_size) + + total=LineParameters( + PhaseDomain, impedance, admittance, frequencies_value; basis = :total + ) + @test_throws ArgumentError compare(reference, total) + + modal_source=LineParameters( + PhaseDomain, + cat([1+2im 0; 0 4+1im], [2+3im 0; 0 8+2im]; dims = 3), + cat([2im 0; 0 4im], [4im 0; 0 8im]; dims = 3), + frequencies_value + ) + modal=compute( + ModalAnalysisProblem(modal_source), + ModalAnalysisFormulation(:default) + ) + @test_throws ArgumentError compare(reference, modal) + + displayed=LineParametersBenchmark( + RMSError([2.0e-20 1.0e-5; 2.0e-5 3.0e-5], [0.5 0.01; 0.02 0.03]), + RMSError([3.0e-20 2.0e-8; 3.0e-8 4.0e-8], [0.4 0.001; 0.002 0.003]) + ) + requests=( + (Z, absolute_error), + (Z, relative_error), + (Y, absolute_error), + (Y, relative_error) + ) + retained=observables(displayed,requests) + table=LineCableModels.ReportBuilder.tabulate(retained,(Z,absolute_error)) + @test names(table)==["index","value"] + @test size(observe(retained,Z,absolute_error))==(2,2) + @test observe(retained,Z,absolute_error)[1,1]≈2.0e-17 + @test observe(retained,Z,relative_error)[1,1]==0.5 + @test observe(retained,Y,relative_error)[1,2]==0.001 + @test displayed.Z.relative[1,1]==0.5 + @test first(table.value)≈2.0e-17 + unclipped=observables(displayed,requests;clip=false) + @test observe(unclipped,Z,absolute_error)[1,1]≈2.0e-17 + columns=LineCableModels.ReportBuilder.observation_columns(table) + @test keys(columns)==(:value,) + @test LineCableModels.Units.label(columns.value.unit)=="Ω/km" + relative=LineCableModels.ReportBuilder.tabulate(retained,(Z,relative_error)) + @test LineCableModels.Units.label(LineCableModels.ReportBuilder.observation_columns(relative).value.unit)=="" +end diff --git a/test/unit/engine/benchmark_observations.jl b/test/unit/engine/benchmark_observations.jl new file mode 100644 index 000000000..e66243197 --- /dev/null +++ b/test/unit/engine/benchmark_observations.jl @@ -0,0 +1,13 @@ +@testitem "Comparison / indexed observations retain numerical-zero unavailability" tags=[:unit, :engine] begin + using LineCableModels.Engine: absolute_error, relative_error + a = reshape(ComplexF64[1, 2, 3, 4], 2, 2, 1) + z = zeros(ComplexF64, 2, 2, 1) + reference = LineParameters(PhaseDomain, a, z, [50.0]) + compared_result = LineParameters(PhaseDomain, 2a, z, [50.0]) + result = LineCableModels.Engine.compare(reference, compared_result) + @test basis(result) === :pul + @test observe(result, Z, absolute_error, 2, 1) == 2.0 + @test observe(result, Z, relative_error, 2, 1) == 1.0 + @test ismissing(observe(result, Y, absolute_error, 2, 1)) + @test ismissing(observe(result, Y, relative_error, 2, 1)) +end diff --git a/test/unit/engine/blueprint_fill.jl b/test/unit/engine/blueprint_fill.jl new file mode 100644 index 000000000..4b7fcec8a --- /dev/null +++ b/test/unit/engine/blueprint_fill.jl @@ -0,0 +1,45 @@ +@testitem "Blueprint / finite-resistivity interstitial fill" tags=[:unit, :parametric] begin + import LineCableModels.Engine as Engine + import LineCableModels.DataModel as DM + + copper = Material(kind=:conductor, rho=1.7241e-8) + pe = Material(kind=:insulator, rho=1.97e14, eps_r=2.3) + design = @cable "finite-pe-fill" begin + @terminal :core begin + stranded(copper; shape=Disk(0.5e-3), boundary=Disk(2.5e-3), fill=pe) + insulation(pe; t=1e-3) + end + @terminal :sheath begin + sheath(copper; t=0.2e-3) + end + end + fills = filter(r -> r.primitive isa DM.DifferenceShape, design.geometry.regions) + @test length(fills) == 1 + @test only(fills).source.material == pe + @test isfinite(only(fills).source.material.rho) + + blueprint = Engine.flatten(LineCableModelsCoaxial(), design, Float64) + @test getproperty.(blueprint.conductors, :terminal) == [:core, :sheath] + @test blueprint.assembly_ranges == [1:2] + @test only(blueprint.dielectrics).material == pe + @test only(blueprint.dielectrics).r_in ≈ 2.5e-3 + @test only(blueprint.dielectrics).r_ex ≈ 3.5e-3 + + constants = CableConstants(design; frequency=50.0, + formulation=CableConstantsFormulation(insulation_admittance=:lossy)) + row = only(constants) + @test all(isfinite, (row.R, row.L, row.C, row.G)) + @test row.G > 0 + @test row.G ≈ 2pi / (pe.rho * log(3.5 / 2.5)) + @test only(CableConstants(design; frequency=50.0)).G == 0 + + system = @system "finite-pe-fill" begin + @at design (0.0, -1.0) core=1 sheath=2 + end + problem = LineParametersProblem(system; earth_props=homogeneous(rho=100.0), frequencies=[50.0]) + parameters = compute(problem) + @test size(parameters.Z) == (2, 2, 1) + @test size(parameters.Y) == (2, 2, 1) + @test all(isfinite, parameters.Z) + @test all(isfinite, parameters.Y) +end diff --git a/test/unit/engine/comparison_bands.jl b/test/unit/engine/comparison_bands.jl new file mode 100644 index 000000000..37ecfe95b --- /dev/null +++ b/test/unit/engine/comparison_bands.jl @@ -0,0 +1,199 @@ +@testitem "Engine / RMS bands slice stored frequencies and retain comparison meaning" tags=[:unit, :engine] begin + using Test + using LineCableModels + using LineCableModels.Engine: compare, RMSError, LineParametersBenchmark + f = [0.1, 1.0, 40.0, 60.0, 100.0, 1000.0, 2000.0, 3000.0, 1e6] + z = reshape(ComplexF64.(1:length(f)), 1, 1, :) + y = (0.1+0.2im) .* z + a = LineParameters(PhaseDomain, z, y, f) + b = LineParameters(PhaseDomain, z .* 1.1, y .* 1.2, f) + original = deepcopy((a.Z.values, a.Y.values, b.Z.values, b.Y.values)) + full = compare(a, b) + @test full isa LineParametersBenchmark + @test only(full.Z.relative) ≈ 0.1 + @test only(full.Y.relative) ≈ 0.2 + @test full.Y.details.data.sample_count == length(f) + for (band, indices) in + ((:dc, 1:5), (:harmonic, 3:7), (:narrow, 6:9), ((50.0, 2500.0), 3:7)) + result = compare(a, b; band) + @test result.Z.details.data.indices == indices + @test result.Z.details.data.actual_bounds == (f[first(indices)], f[last(indices)]) + @test result.Z.details.data.sample_count == length(indices) + sliced_a = LineParameters(PhaseDomain, z[:, :, indices], y[:, :, indices], f[indices]) + sliced_b = LineParameters(PhaseDomain, 1.1z[:, :, indices], 1.2y[:, :, indices], f[indices]) + @test result.Z.absolute == compare(sliced_a, sliced_b).Z.absolute + @test result.Y.relative == compare(sliced_a, sliced_b).Y.relative + end + @test compare(a, b; band = :harmonic, fundamental = 60, harmonics = 50).Z.details.data.indices == + 4:8 + @test compare(a, b; band = (0.0, 1.0)).Z.details.data.indices == 1:2 + @test compare(a, b; band = (1000.0, 2e6)).Z.details.data.indices == 6:9 + @test compare(a, b; band = (55.0, 65.0)).Z.details.data.indices == 4:4 + @test compare(a, b; band = (55.0, 65.0)).Z.details.data.sample_count == 1 + for band in (:wide, (2e6, 3e6), (0.001, 0.01)) + result = compare(a, b; band) + @test all(ismissing, result.Z.absolute) + @test all(ismissing, result.Y.relative) + @test all(==(:no_samples), result.Z.details.data.status) + @test result.Z.details.data.sample_count == 0 + @test result.Z.details.data.actual_bounds === (missing, missing) + @test result.Z.details.data.reason isa String + end + wider_f = [1e6, 1.1e6, 2e6] + wider = LineParameters(PhaseDomain, ones(ComplexF64, 1, 1, 3), ones(ComplexF64, 1, 1, 3), wider_f) + @test compare(wider, wider; band = :wide).Z.details.data.indices == 2:3 + @test_throws ArgumentError compare(a, b; band = :unknown) + @test_throws ArgumentError compare(a, b; band = (100.0, 50.0)) + @test_throws ArgumentError compare(a, b; fundamental = 0) + @test_throws ArgumentError compare(a, b; harmonics = 0) + @test_throws ArgumentError compare(a, b; atol = -1) + @test_throws ArgumentError compare(a, b; unsupported = (Y = true,)) + unavailable = compare(a, b; unsupported = (Y = "Backend does not provide this observable",)) + @test all(ismissing, unavailable.Y.relative) + @test only(unavailable.Y.details.data.status) === :unsupported + @test only(unavailable.Z.relative) ≈ 0.1 + declared = LineParameters(PhaseDomain, z, y, f; + details = ComputationDetails(;comparison_unsupported = (G = "Dielectric conduction not represented",),)) + @test only(compare(declared, b, G).details.data.status) === :unsupported + @test only(compare(declared, b).Y.details.data.status) === :compared + # Numerical-zero classification uses only the selected band. Values elsewhere in the sweep are excluded. + tiny = fill(1e-15+1e-15im, 1, 1, length(f)) + signal = copy(tiny) + signal[1, 1, end] = 1.0 + quiet = LineParameters(PhaseDomain, tiny, tiny, f) + noisy = LineParameters(PhaseDomain, signal, signal, f) + @test ismissing(only(compare(quiet, noisy; band = :dc).Y.relative)) + @test only(compare(quiet, noisy; band = :dc).Y.details.data.status) === :reference_sample_below_tolerance + @test ismissing(only(compare(quiet, noisy).Y.relative)) + @test ismissing(only(compare(quiet, noisy).Y.absolute)) + @test only(compare(quiet, noisy; band = :dc, atol = (R=0,X=0,G=0,B=0)).Y.details.data.status) === :compared + zero = LineParameters( + PhaseDomain, zeros(ComplexF64, 1, 1, length(f)), zeros(ComplexF64, 1, 1, length(f)), f) + @test ismissing(only(compare(zero, noisy).Y.relative)) + tiny_capacitance = reshape(complex.(zeros(length(f)), 2π .* f .* 1e-17), 1, 1, :) + negligible = LineParameters(PhaseDomain, z, tiny_capacitance, f) + @test ismissing(only(compare(zero, negligible).Y.relative)) + @test ismissing(only(compare(zero, negligible).Y.absolute)) + @test ismissing(only(compare(zero, negligible, C).relative)) + @test only(compare(zero, negligible; atol = (C = 1e-18, G = 0.0)).Y.details.data.status) === + :reference_below_tolerance + @test only(compare(quiet, noisy; band = :dc, atol = (G = 1e-16, B = 1e-16,)).Y.details.data.status) === + :compared + for quantity in (R, L, C, G) + result = compare(a, b, quantity; band = :harmonic) + @test result isa RMSError + @test result.details.data.quantity === nameof(quantity) + @test result.details.data.indices == 3:7 + end + @test (a.Z.values, a.Y.values, b.Z.values, b.Y.values) == original +end + +@testitem "Engine / RMS normalization retains ordered operands and every sample" tags=[:unit, :engine] begin + using LineCableModels.Engine: compare + function parameters(values) + tensor = reshape(ComplexF64.(values), 1, 1, :) + LineParameters(PhaseDomain, tensor, tensor, collect(1.0:length(values))) + end + a, b = parameters([1, 100]), parameters([2, 100]) + @test only(compare(a, b).Y.relative) ≈ sqrt(1/10001) + @test only(compare(a, b; normalization = :pointwise).Y.relative) ≈ sqrt(1/2) + @test only(compare(b, a).Y.relative) ≈ sqrt(1/10004) + @test only(compare(b, a; normalization = :pointwise).Y.relative) ≈ sqrt(0.25/2) + @test compare(a, b).Y.absolute == compare(a, b; normalization = :pointwise).Y.absolute + @test compare(a, b).Y.details.data.normalization === :reference_rms + @test compare(a, b; normalization = :pointwise).Y.details.data.normalization === :pointwise + for normalization in (:reference_rms, :pointwise) + @test only(compare(parameters([im, 2im]), parameters([-im, -2im]); normalization).Y.relative) ≈ + 2 + @test ismissing(only(compare(parameters([0, 0]), parameters([0, 0]); normalization, atol = (R=0,X=0,G=0,B=0)).Y.relative)) + @test ismissing(only(compare(parameters([0, 0]), parameters([0, 1]); normalization).Y.relative)) + @test ismissing(only(compare(a, b; normalization, band = :wide).Y.relative)) + @test ismissing(only(compare(a, b; normalization, unsupported = (Y = "unavailable",)).Y.relative)) + end + # An unusable denominator does not change the selected sample population. + @test ismissing(only(compare(parameters([0, 1]), parameters([0, 2]); normalization = :pointwise).Y.relative)) + @test ismissing(only(compare(parameters([0, 1]), parameters([1, 1]); normalization = :pointwise).Y.relative)) + @test ismissing(only(compare(parameters([0, 1]), parameters([1, 1])).Y.relative)) + @test_throws ArgumentError compare(a, b; normalization = :unknown) +end + +@testitem "Engine / both RMS metrics require two significant operands at every selected sample" tags=[:unit, :engine] begin + using LineCableModels.Engine: compare + tensor(values) = reshape(ComplexF64.(values), 1, 1, :) + signal = tensor([2.0, -3.0, 4im]) + original = copy(signal) + tolerance = [0.1, 0.2, 0.3] + cases = (([0, 0, 0], :below_tolerance), + ([0.01, -0.02, 0.03im], :below_tolerance), + ([2, -0.2, 4im], :sample_below_tolerance), + ([2, -3, 0.1im], :sample_below_tolerance)) + for normalization in (:reference_rms, :pointwise), (values, suffix) in cases + quiet = tensor(values) + for (reference, result, role) in ((signal, quiet, :result), (quiet, signal, :reference)) + error = compare(reference, result; normalization, atol=tolerance) + @test ismissing(only(error.relative)) + @test ismissing(only(error.absolute)) + @test only(error.details.data.status) === Symbol(role, :_, suffix) + @test occursin("no samples were omitted", only(error.details.data.normalization_reason)) + @test error.details.data.sample_count == 3 + @test quiet == tensor(values) + end + end + for normalization in (:reference_rms, :pointwise) + significant = tensor([0.1001, -0.2001, 0.3001im]) + @test only(compare(significant, significant; normalization, atol=tolerance).relative) == 0 + @test only(compare(signal, -signal; normalization, atol=tolerance).relative) ≈ 2 + @test ismissing(only(compare(tensor([0]), tensor([0]); normalization).relative)) + @test only(compare(signal, signal; normalization, atol=0).relative) == 0 + end + @test signal == original + + # A quiet result invalidates only bands containing its quiet samples. + f = [1.0, 50.0, 1e3, 1e7] + z = ones(ComplexF64, 1, 1, length(f)) + y = tensor(2π .* f .* 1e-8im) + tiny_y = tensor(2π .* f .* 1e-17im) + reference = LineParameters(PhaseDomain, z, y, f) + result = LineParameters(PhaseDomain, z, tiny_y, f) + @test only(compare(reference, result, Y).details.data.status) === :result_below_tolerance + @test ismissing(only(compare(reference, result, C).relative)) + @test only(compare(reference, result, Y; atol=(C=1e-18, G=0)).relative) ≈ 1-1e-9 + mixed_y = copy(y) + mixed_y[1, 1, 1] = tiny_y[1, 1, 1] + mixed = LineParameters(PhaseDomain, z, mixed_y, f) + for normalization in (:reference_rms, :pointwise) + for band in (:all, :dc) + error = compare(reference, mixed, Y; band, normalization) + @test ismissing(only(error.relative)) + @test only(error.details.data.status) === :result_sample_below_tolerance + @test ismissing(only(error.absolute)) + end + for band in (:harmonic, :narrow, :wide) + error = compare(reference, mixed, Y; band, normalization) + @test only(error.relative) == 0 + @test only(error.details.data.status) === :compared + end + end + @test reference.Y.values == y + @test result.Y.values == tiny_y +end + +@testitem "Engine / lossless conductance normalization preserves measured differences" tags=[:unit, :engine] begin + using LineCableModels.Engine: compare + frequencies = [50.0, 500.0] + z = ones(ComplexF64, 2, 2, 2) + y = fill(1e-4im, 2, 2, 2) + y[2, 2, :] .+= 1e-6 + reference = LineParameters(PhaseDomain, z, y, frequencies) + result = LineParameters(PhaseDomain, z, y .+ 1e-14, frequencies) + conductance = compare(reference, result, G) + @test ismissing(conductance.relative[1, 1]) + @test ismissing(conductance.absolute[1, 1]) + @test conductance.details.data.status[1, 1] === :reference_below_tolerance + @test conductance.details.data.normalization_reason[1, 1] isa String + @test conductance.relative[2, 2] ≈ 1e-8 + @test conductance.absolute[2, 2] ≈ 1e-14 + @test conductance.details.data.normalization_reason[2, 2] === nothing + @test all(!ismissing, compare(reference, result).Y.relative) + @test reference.Y.values == y +end diff --git a/test/unit/engine/constitutive_defaults.jl b/test/unit/engine/constitutive_defaults.jl new file mode 100644 index 000000000..be692efa7 --- /dev/null +++ b/test/unit/engine/constitutive_defaults.jl @@ -0,0 +1,72 @@ +@testitem "Engine / explicit lossless defaults and lossy radial admittance" tags=[:unit, :parametric] begin + using Test + using LineCableModels + const E = LineCableModels.Engine + + @test formula(:default) isa LineCableModels.FormulaDefinition + ε0 = 8.8541878128e-12 + for (owner, kind) in ((E.InsulationAdmittance, :insulator), + (E.SemiconAdmittance, :semicon)) + material = Material(kind, 1.0, 100.0; tan_delta = 0.02) + lossless = owner.Formula(:default) + lossy = owner.Formula(:lossy) + @test formula_id(lossless) === :lossless + for frequency in (0.1, 50.0, 1.0e6) + ω = 2π * frequency + actual = @inferred constitutive(lossless, material, frequency, 20.0) + @test real(actual) == 0 + @test imag(actual) ≈ ω * ε0 * material.eps_r + actual = @inferred constitutive(lossy, material, frequency, 20.0) + εstar = ε0 * material.eps_r * (1 - im * material.tan_delta) + + 1 / (im * ω * material.rho) + @test actual ≈ im * ω * εstar + end + end + + # Ametani (2004), Fig. 4 geometry and Eqs. (14)-(15), not a regenerated baseline. + b, c, r0 = 30.45e-3, 35.45e-3, 71.15e-3 + screen = Material(:semicon, 1.0, 1000.0) + insulation_material = Material(:insulator, Inf, 3.1) + design = build(CableDesign, + "ametani-radial", + terminal(:core, + solid(Material(:conductor, 1.82e-8), Disk(b)), + LineCableModels.screen(screen; t = c - b), + insulation(insulation_material; t = r0 - c))) + selected = E.CableConstantsFormulation( + insulation_admittance = :lossy, semicon_admittance = :lossy) + for frequency in (50.0, 60.0) + ω = 2π * frequency + εs = ε0 * screen.eps_r + 1 / (im * ω * screen.rho) + ys = im * ω * 2π * εs / log(c / b) + yi = im * ω * 2π * ε0 * insulation_material.eps_r / log(r0 / c) + expected = inv(inv(ys) + inv(yi)) + result = compute(E.CableConstantsProblem(design; frequency), selected) + @test only(result.G) ≈ real(expected) + @test only(result.C) ≈ imag(expected) / ω + lossless = compute(E.CableConstantsProblem(design; frequency)) + @test only(lossless.G) == 0 + end + + # Export equivalencing keeps DC conduction distinct from polarization loss. + # Match the original layers at the requested export frequency, not all f. + polar = Material(:insulator, 2.0e9, 3.1; tan_delta = 0.04) + screen = Material(:semicon, 1.0e5, 1000.0; tan_delta = 0.02) + source = build(CableDesign, + "polarization-equivalence", + terminal(:core, + solid(Material(:conductor, 1.82e-8), Disk(b)), + LineCableModels.screen(screen; t = c - b), + insulation(polar; t = r0 - c))) + for frequency in (50.0, 60.0) + equivalent = homogenize(source) + actual = compute(E.CableConstantsProblem(source; frequency), selected) + reconstructed = compute(E.CableConstantsProblem(equivalent; frequency), selected) + @test reconstructed.C ≈ actual.C + @test reconstructed.G ≈ actual.G + reduced = only(LineCableModels.DataModel.flatten(source, frequency)).dielectric.material + dc_g = inv(log(c / b) * screen.rho / (2π) + log(r0 / c) * polar.rho / (2π)) + @test 2π / (reduced.rho * log(r0 / b)) ≈ dc_g + @test reduced.tan_delta > 0 + end +end diff --git a/test/unit/engine/default_identity.jl b/test/unit/engine/default_identity.jl new file mode 100644 index 000000000..9d0040f5b --- /dev/null +++ b/test/unit/engine/default_identity.jl @@ -0,0 +1,38 @@ +@testitem "Engine / retained defaults and PSCAD comparison inventory" tags=[:unit, :engine] begin + @test Formulation() isa LineParametersFormulation + const E=LineCableModels.Engine + const EP=LineCableModels.Earth + expected = ( + (E.InternalImpedance, + (:default, :schelkunoff1934, :wedepohl1973), :schelkunoff1934), + (E.InsulationImpedance, (:default, :ametani1980), :ametani1980), + (E.EarthImpedance, + (:default, :unified, :carson1926, :pollaczek1926, :gary1976, + :wedepohl1973, :saad1996, :ametani2009, :lucca1994, :wise1934, :xue2018), :unified), + (E.InsulationAdmittance, (:default, :lossless, :lossy), :lossless), + (E.SemiconAdmittance, (:default, :lossless, :lossy), :lossless), + (E.EarthAdmittance, + (:default, :unified, :ideal, :pollaczek1926, :wise1948, :xue2018), :unified), + (E.PipeImpedance, (:default, :none), :none), (EP.FrequencyDependent, + (:default, :constant, :alipio2014, :cigre2019, :datsios2019, + :longmire1975, :messier1985, :portela1999, :scott1967, + :visacro1987, :visacro2012), :constant), + ( + EP.EquivalentHomogeneous, (:default, :bottommost), :bottommost), + (LineCableModels.ModalAnalysis, + (:default, :chrysochos2014, :vieira2026, :wedepohl1996), :chrysochos2014), + (LineCableModels.Materials.TemperatureDependent, (:default, :linear), :linear), + (E.ShuntModel, (:default, :equivalent, :boundary), :equivalent)) + for (owner, ids, target) in expected + @test Set(owner.formulas(owner.Formula)) == Set(ids) + for id in ids + selected=owner.Formula(id) + @test formula_id(selected) === (id === :default ? target : id) + end + @test_throws ArgumentError owner.Formula(:RemovedAuthor) + end + @test Formulation().methods.earth_impedance.equivalent_earth === nothing + @test_throws ArgumentError Formulation(earth_admittance = :IdealGround) + @test formula_id(Formulation(earth_impedance = formula(:default; equivalent_earth = formula(:default))).methods.earth_impedance.equivalent_earth.rule) === + :bottommost +end diff --git a/test/unit/engine/earth_inference.jl b/test/unit/engine/earth_inference.jl new file mode 100644 index 000000000..5848a800d --- /dev/null +++ b/test/unit/engine/earth_inference.jl @@ -0,0 +1,68 @@ +@testitem "Engine / earth calculation tuples preserve part types across layouts" tags=[:unit, :parametric] setup=[FormulaFixtures] begin + const E=LineCableModels.Engine + design=build(CableDesign, "typed earth equations", terminal(:core, + core(Material(:conductor, 1.72e-8); r=0.004), + insulation(Material(:insulator, Inf, 2.3); t=0.002))) + selections=(Formulation(), + Formulation(earth_impedance=(air=:gary1976, earth=:saad1996, mixed=:lucca1994), + earth_admittance=:ideal), + Formulation(earth_impedance=FormulaFixtures.selection(E.EarthImpedance), + earth_admittance=FormulaFixtures.selection(E.EarthAdmittance))) + execution=E.computation_options(LineCableModelsCoaxial, ComputationOptions()) + for selected in selections, heights in ((1.0, 1.0, 1.0), (1.0, -1.0, -2.0)) + system=build(LineCableSystem, fill(design, 3), + [Pose2(Float64(i-1), height) for (i, height) in enumerate(heights)]; + connections=[Dict(:core=>i) for i in 1:3]) + problem=LineParametersProblem(system; frequencies=[50.0], earth_props=homogeneous(rho=100.0)) + blueprints=only(E.flatten(LineCableModelsCoaxial(), problem.system.designs, + Float64, [selected])) + workspace=E.LineParametersWorkspace(problem, selected, execution, blueprints) + calculations=workspace.plan.earth.calculations + materials=workspace.buffers.earth.calculations + @test calculations isa Tuple + @test materials isa Tuple + @test length(calculations)==length(materials) + @test all(isconcretetype, fieldtypes(typeof(calculations))) + for calculation in calculations + @test calculation.parts isa Tuple + @test all(isconcretetype, fieldtypes(typeof(calculation.parts))) + for part in calculation.parts + @test isconcretetype(fieldtype(typeof(part), :expression)) + @test fieldtype(typeof(part), :expression) <: LineCableModels.Expression + end + end + @test (@inferred E._solve!(workspace, selected, calculations, materials)) === workspace + @test all(isfinite, workspace.buffers.Zout) + @test all(isfinite, workspace.buffers.Yout) + end +end + +@testitem "Engine / fixed public options retain phase and modal result types" tags=[:unit, :parametric] setup=[TestFixtures] begin + function fixed_options(problem, selected, ::Val{Basis}, ::Val{Trace}, ::Val{Timing}) where {Basis, Trace, Timing} + compute(problem, selected; + options=(output_basis=Basis, trace=Trace, timing=Timing, verbosity=(default=0,))) + end + function phase_and_modal(problem, selected) + phase=compute(problem, selected; options=(output_basis=:pul, trace=false, timing=false)) + modal=compute(ModalAnalysisProblem(phase), ModalAnalysisFormulation(:default); + options=(rotate=true,)) + return phase, modal + end + problem=TestFixtures.line_parameters_problem(TestFixtures.two_wire_system(); frequencies=[50.0, 500.0]) + selected=Formulation(options=(reduce_bundle=false, kron_reduction=false, ideal_transposition=false)) + reference=@inferred fixed_options(problem, selected, Val(:pul), Val(false), Val(false)) + for basis in (:pul, :total), trace in (false, true), timing in (false, true) + result=@inferred fixed_options(problem, selected, Val(basis), Val(trace), Val(timing)) + multiplier=basis===:pul ? 1.0 : problem.system.line_length + @test Z(result)==multiplier.*Z(reference) + @test Y(result)==multiplier.*Y(reference) + @test frequencies(result)==frequencies(reference) + @test haskey(details(result).data, :trace)==trace + @test haskey(details(result).data, :timing)==timing + end + phase, modal=@inferred phase_and_modal(problem, selected) + @test Z(phase)==Z(reference) + restored=transform(PhaseDomain, modal) + @test Z(restored)≈Z(phase) + @test Y(restored)≈Y(phase) +end diff --git a/test/unit/engine/earth_interactions.jl b/test/unit/engine/earth_interactions.jl new file mode 100644 index 000000000..c7940feae --- /dev/null +++ b/test/unit/engine/earth_interactions.jl @@ -0,0 +1,162 @@ +@testitem "Engine / indexed reuse compares current material values and uncertainty" tags=[:unit, :measurements] setup=[ + TestFixtures, EarthInteractionFixtures] begin + using Measurements + const E=LineCableModels.Engine + const F=EarthInteractionFixtures + # Equal plain floating-point infinities do not need subtraction (Inf-Inf is NaN). + @test E.same_physical_state(BigFloat(Inf), BigFloat(Inf)) + @test !E.same_physical_state(BigFloat(Inf), BigFloat(-Inf)) + problem=TestFixtures.three_bare_wires_problem( + heights = (-1.0, -1.0, -1.0), frequencies = [50.0], rho = 100.0) + selected=F.PairImpedance() + work=F.workspace(problem, selected) + calculation=first(work.plan.earth.calculations) + # A pair that computes its value has `reuse_from` equal to its own index. + @test count(((index, entry),) -> entry.reuse_from==index, enumerate(calculation.pairs))==3 + E.earth!(work, 1) + @test length(selected.calls)==3 + original=copy(work.buffers.Zearth) + materials=first(work.buffers.earth.calculations) + # The final diagonal shares geometry with the first two, but its material changes. + materials.rho[2, 9]=200.0 + empty!(selected.calls) + E.earth!(work, 1) + @test length(selected.calls)==4 + @test work.buffers.Zearth[3, 3]≈original[3, 3]+(1e-4+1e-3im) + materials.rho[2, 9]=100.0 + empty!(selected.calls) + E.earth!(work, 1) + @test length(selected.calls)==3 + @test work.buffers.Zearth==original + + # A later pair can reuse the second material class in the same candidate chain. + materials.rho[2, 5]=materials.rho[2, 9]=200.0 + empty!(selected.calls) + E.earth!(work, 1) + @test length(selected.calls)==4 + @test work.buffers.Zearth[2, 2]==work.buffers.Zearth[3, 3] + @test work.buffers.Zearth[1, 1]==original[1, 1] + + uncertain_problem=TestFixtures.three_bare_wires_problem( + heights = (-1.0, -1.0, -1.0), frequencies = [50.0], rho = measurement(100.0), + radius = measurement(0.0425)) + uncertain=F.workspace(uncertain_problem) + selected=first(uncertain.plan.earth.calculations).impedance.formula + materials=first(uncertain.buffers.earth.calculations) + rho=measurement(100.0, 1.0) + independent=measurement(100.0, 1.0) + materials.rho[2, :].=rho + materials.rho[2, 9]=independent + E.earth!(uncertain, 1) + @test length(selected.calls)==4 + @test uncertainty(imag(uncertain.buffers.Zearth[1, 2]-uncertain.buffers.Zearth[2, 1]))==0 + @test uncertainty(imag(uncertain.buffers.Zearth[1, 1]-uncertain.buffers.Zearth[3, 3]))≈sqrt(2)*1e-5 + materials.rho[2, 9]=2rho-rho + empty!(selected.calls) + E.earth!(uncertain, 1) + @test length(selected.calls)==3 + @test uncertainty(imag(uncertain.buffers.Zearth[1, 1]-uncertain.buffers.Zearth[3, 3]))==0 + + height=measurement(-1.0, 0.001) + independent_height=measurement(-1.0, 0.001) + geometry_problem=TestFixtures.three_bare_wires_problem( + heights = (height, independent_height, height), frequencies = [50.0], rho = 100.0, + radius = measurement(0.0425)) + geometry_work=F.workspace(geometry_problem) + geometry_calculation=first(geometry_work.plan.earth.calculations) + @test geometry_calculation.pairs[5].reuse_from==5 + @test geometry_calculation.pairs[9].reuse_from==1 +end + +@testitem "Engine / unified shares indexed source coefficients before its complete solve" tags=[:unit, :parametric, :slow] setup=[UnifiedFormulaFixtures] begin + const E=LineCableModels.Engine + positions=[(column*0.1905, -1.1335-row*0.19035) for row in 0:2 for column in -1:1] + geometry=(horizontal = first.(positions), + height = last.(positions), radius = fill(0.027358, 9)) + controls=E.formulation_options(E.SpectralIntegral, ( + method = :quad, options = (rtol = 1e-8,))) + for (frequency, gamma) in ((50.0, 0im), (500.0, 1e-4+2e-4im)) + state=(jω = 2pi*im*frequency, Γ = gamma, sigma = (0.0, 0.01), + epsilon = (8.8541878128e-12, 10*8.8541878128e-12), mu = (4pi*1e-7, 4pi*1e-7)) + reused=UnifiedFormulaFixtures.workspace(geometry, state, controls) + separate=UnifiedFormulaFixtures.workspace(geometry, state, controls) + calculation=only(reused.plan.earth.calculations) + @test count(((index, entry),) -> entry.reuse_from==index, + enumerate(calculation.pairs))==27 + # Every pair of the separate workspace is computed from its own index. + pairs=only(separate.plan.earth.calculations).pairs + for index in eachindex(pairs) + pairs[index]=merge(pairs[index], (reuse_from = index,)) + end + E.earth!(reused, 1) + E.earth!(separate, 1) + @test length(unique(reused.buffers.earth.pairs.representatives))==27 + @test length(unique(separate.buffers.earth.pairs.representatives))==81 + for name in (:axial_field, :source_potential, :current_map, + :enclosed_impedance, :enclosed_potential) + @test getproperty(reused.buffers, name)==getproperty(separate.buffers, name) + end + end +end + +@testitem "Engine / ordinary earth formulations reuse through public compute" tags=[:unit, :parametric] setup=[ + TestFixtures, EarthInteractionFixtures] begin + const F=EarthInteractionFixtures + problem=TestFixtures.three_bare_wires_problem(heights = (-1.0, -1.0, -1.0), + frequencies = [50.0, 500.0], rho = 100.0) + impedance, potential=F.PairImpedance(), F.PairPotential() + selected=Formulation(earth_impedance = impedance, earth_admittance = potential; + options = ( + reduce_bundle = false, kron_reduction = false, ideal_transposition = false)) + result=compute(problem, selected; options = (trace = true, verbosity = (default = 0,))) + @test length(impedance.calls) == 6 + @test length(potential.calls) == 6 + trace=details(result).data.trace + for frequency in eachindex(problem.frequencies), row in 1:3, column in 1:3 + coefficient=10(row==column)+3+abs(row-column)+4*0.0425+ + 1+problem.frequencies[frequency]/1000 + @test trace.Zg[row, column, frequency] ≈ coefficient*(1e-4+1e-3im) + @test trace.Pg[row, column, frequency] ≈ coefficient*1e9 + end + @test all(isfinite, Z(result)) && all(isfinite, Y(result)) +end + +@testitem "Engine / indexed integral reuse retains descriptions and logical positions" tags=[:unit, :parametric] setup=[ + TestFixtures, EarthInteractionFixtures] begin + using Logging + const E=LineCableModels.Engine + const F=EarthInteractionFixtures + problem=TestFixtures.three_bare_wires_problem( + heights = (-1.0, -1.0, -1.0), frequencies = [50.0], rho = 100.0) + for description in (nothing, "earth integral"), trace in (false, true), + positions in (false, true) + selected=F.IntegralImpedance(description; positions) + work=F.workspace(problem, selected; trace) + logger=Test.TestLogger() + with_logger(logger) do + E.earth!(work, 1) + end + # Self and mutual are different indexed equations, each evaluated once. + @test selected.calls[]==2 + @test all(==(work.buffers.Zearth[1]), work.buffers.Zearth) + @test length(logger.logs)==9 + @test isempty(work.buffers.earth.pairs.warnings) + if positions + @test Set((record.kwargs[:context].receiver, record.kwargs[:context].source) + for record in logger.logs)==Set(Tuple.(CartesianIndices(work.buffers.Zearth))) + else + @test all(record->record.kwargs[:context]===description, logger.logs) + end + trace||continue + records=work.trace.integrals + @test length(records)==9 + @test all(record->record.value==work.buffers.Zearth[1], records) + if positions + @test Set((record.context.receiver, record.context.source) + for record in records) == + Set(Tuple.(CartesianIndices(work.buffers.Zearth))) + else + @test all(record->record.context===description, records) + end + end +end diff --git a/test/unit/engine/formula_architecture.jl b/test/unit/engine/formula_architecture.jl new file mode 100644 index 000000000..d5ac65e28 --- /dev/null +++ b/test/unit/engine/formula_architecture.jl @@ -0,0 +1,554 @@ +@testitem "Engine / indexed formula declarations and immutable source domains" tags=[:unit, :engine] begin + const E = LineCableModels.Engine + const EI = E.EarthImpedance + const EA = E.EarthAdmittance + const Expression = LineCableModels.Expression + air = E.EarthPair(1, 2, (10.0, 12.0), 1.0, (1, 1)) + soil = E.EarthPair(1, 2, (-1.0, -2.0), 1.0, (2, 2)) + mixed = E.EarthPair(1, 2, (10.0, -2.0), 1.0, (1, 2)) + self = E.EarthPair(1, 1, (-1.0, -1.0), 0.0, (2, 2); radius = 0.01) + @test validate(self) === self + @test self.radius == 0.01 && iszero(self.separation) + @test_throws DomainError validate(E.EarthPair(1, 1, (-1.0, -1.0), 0.0, (2, 2))) + @test_throws ArgumentError validate(E.EarthPair( + 1, 1, (-1.0, -1.0), 0.01, (2, 2); radius = 0.01)) + @test_throws ArgumentError validate(E.EarthPair( + 1, 1, (-1.0, -2.0), 0.0, (2, 2); radius = 0.01)) + @test_throws ArgumentError validate(E.EarthPair(1, 2, (1.0, 2.0), 1.0, (2, 2))) + # The signatures of a formula's methods are its only declaration of the earth layers + # it handles. A missing expression fails before the frequency loop. + earth = homogeneous(rho = 100.0) + for owner in (EI, EA) + selected = owner.Formula(:default) + @test Expression(selected, air).arguments[2:3] == (Val(1), Val(1)) + @test Expression(selected, soil).arguments[2:3] == (Val(2), Val(2)) + @test first(Expression(selected, self).arguments) === Val(:self) + @test first(Expression(selected, soil).arguments) === Val(:mutual) + @test first(Expression(selected, mixed).arguments) === Val(:mutual) + # A layer without declared options does not have an expression either. + @test_throws ArgumentError formulation_options(selected, (Expression(selected, + E.EarthPair(1, 2, (-1.0, -2.0), 1.0, (2, 3))),)) + for pair in (air, soil, mixed, self) + expression = Expression(selected, pair) + @test validate(expression, earth.layers) === expression + end + author=owner.Formula(:xue2018) + @test_throws "the earth model has 2 layers and formula :xue2018 is defined up to layer 2; it has no expression for a mutual interaction from layer 1 to layer 2" validate( + Expression(author, mixed), earth.layers) + @test Expression(selected, air) isa Expression + @test Expression(selected, air).selection === selected + end + for id in (:ametani2009, :lucca1994) + selected = EI.Formula(id) + @test first(Expression(selected, mixed).arguments) === Val(:mutual) + @test validate(Expression(selected, mixed), earth.layers) isa Expression + @test_throws ArgumentError validate(Expression(selected, air), earth.layers) + @test_throws ArgumentError validate(Expression(selected, soil), earth.layers) + @test_throws ArgumentError validate(Expression(selected, self), earth.layers) + end + vertical=E.EarthPair(1, 2, (-1.0, -2.0), 0.0, (2, 2)) + @test first(Expression(EI.Formula(:saad1996), vertical).arguments) === Val(:mutual) + @test first(Expression(EI.Formula(:saad1996), self).arguments) === Val(:self) + @test first(Expression(EI.Formula(:wedepohl1973), vertical).arguments) === Val(:mutual) + @test first(Expression(EI.Formula(:wedepohl1973), self).arguments) === Val(:self) + @test_throws ArgumentError validate(Expression(EI.Formula(:pollaczek1926), air), earth.layers) + @test_throws "formula :carson1926 is defined up to layer 1" validate( + Expression(EI.Formula(:carson1926), soil), earth.layers) +end + +@testitem "Engine / the plan reads an earth formula's media from its expression signatures" tags=[:unit, :engine, :slow] setup=[FormulaFixtures] begin + const M=FormulaFixtures + const E=M.E + const EP=M.EP + const EH=M.EH + material=Material(kind = :conductor, rho = 1.7241e-8) + design=build(CableDesign, "media-decision", + Stack(Group(:phase, Region(:core, Disk(0.01), material)))) + two=homogeneous(rho = 100.0, eps_r = 10.0) + three=build(EP.EarthModel, ( + EP.EarthLayer(100.0, 10.0, 1.0, 0.5), EP.EarthLayer(200.0, 20.0, 1.0))) + four=build(EP.EarthModel, (EP.EarthLayer(100.0, 10.0, 1.0, 0.5), + EP.EarthLayer(200.0, 20.0, 1.0, 0.5), EP.EarthLayer(300.0, 30.0, 1.0))) + execution=computation_options(LineCableModelsCoaxial, ComputationOptions((;))) + function plan(heights, earth, formulation) + system=build(LineCableSystem, fill(design, length(heights)), + [(Float64(i), h) for (i, h) in enumerate(heights)]; + connections = [(phase = i,) for i in eachindex(heights)]) + problem=LineParametersProblem(system; earth_props = earth, frequencies = [50.0]) + blueprints=E.CableBlueprint{Float64}[E.flatten(LineCableModelsCoaxial(), d, Float64) + for d in problem.system.designs] + workspace=E.LineParametersWorkspace(problem, formulation, execution, blueprints) + return workspace.plan.earth.calculations, workspace.buffers.earth.calculations + end + overhead, buried, deep=(10.0, 12.0), (-0.25, -0.3), (-0.25, -1.5) + # A formula whose expressions stop at layer 2 sees a two-layer earth as it is. + calculations, materials=plan(overhead, two, Formulation()) + @test only(calculations).earth isa EP.EarthModel + @test size(only(materials).rho, 1) == 2 && only(materials).thickness === nothing + # On more layers it consumes the `:default` reduction unless the formula specifies one. + calculations, materials=plan(overhead, three, Formulation()) + @test only(calculations).earth isa EH.AfterFD + @test formula_id(EH.rule(only(calculations).earth)) === :bottommost + @test size(only(materials).rho, 1) == 2 && only(materials).thickness === nothing + explicit=formula(:default; equivalent_earth = formula(:default; order = :before)) + calculations, _=plan(overhead, two, + Formulation(earth_impedance = explicit, earth_admittance = explicit)) + @test only(calculations).earth isa EH.BeforeFD + # A formula with an expression above layer 2 consumes the layered earth. + layered=Formulation(earth_impedance = M.selection(M.EI), earth_admittance = M.selection(M.EA)) + calculations, materials=plan(deep, three, layered) + @test all(calculation -> calculation.earth isa EP.EarthModel, calculations) + @test all(buffer -> size(buffer.rho, 1) == 3 && buffer.thickness == [Inf, 0.5, Inf], + materials) + _, materials=plan(overhead, two, layered) + @test all(buffer -> size(buffer.rho, 1) == 2 && buffer.thickness === nothing, materials) + # Beyond its highest layer, a layered formula has no expression. + @test_throws "the earth model has 4 layers and formula :LayerImpedance is defined up to layer 3" plan( + (-0.25, -0.75, -1.5), four, layered) + # The plan rejects a missing expression or an unadmitted reduction before the frequency loop. + @test_throws "the earth model has 2 layers and formula :saad1996 is defined up to layer 2; it has no expression for a self interaction from layer 1 to layer 1" plan( + overhead, two, Formulation(earth_impedance = formula(:saad1996))) + @test_throws "does not admit equivalent-earth reduction :bottommost" plan( + buried, three, Formulation(earth_impedance = formula(:saad1996))) +end + +@testitem "Engine / a layered earth formula is never reduced without an explicit reduction" tags=[:unit, :engine, :slow] setup=[FormulaFixtures] begin + const M=FormulaFixtures + const EP=M.EP + const EH=M.EH + material=Material(kind = :conductor, rho = 1.7241e-8) + design=build(CableDesign, "layered-safety", + Stack(Group(:phase, Region(:core, Disk(0.01), material)))) + # Conductors in air, in the first earth layer and in the second. + system=build(LineCableSystem, fill(design, 3), [(1.0, 2.0), (2.0, -0.25), (3.0, -1.5)]; + connections = [(phase = i,) for i in 1:3]) + three=build(EP.EarthModel, ( + EP.EarthLayer(100.0, 10.0, 1.0, 0.5), EP.EarthLayer(200.0, 20.0, 1.0))) + problem=LineParametersProblem(system; earth_props = three, frequencies = [50.0]) + recorded(result, slot)=getproperty(details(result).data.formulations.methods, slot).equivalent_earth + requested(result, slot)=get(getproperty(details(result).data.formulations.requested, slot), + :equivalent_earth, nothing) + potential=M.selection(M.EA) + # The layered fixture sees all three media, as do a formula with typed runtime arguments + # and a formula with generic layers. The record of each keeps `equivalent_earth` at + # `nothing`. + typed, generic=M.TypedLayerImpedance(), M.GenericLayerImpedance() + empty!(M.calls) + for impedance in (M.selection(M.EI), typed, generic) + result=compute(problem, Formulation(earth_impedance = impedance, earth_admittance = potential)) + @test all(isfinite, result.Z.values) && all(isfinite, result.Y.values) + @test recorded(result, :earth_impedance) === nothing + @test recorded(result, :earth_admittance) === nothing + end + @test !isempty(M.calls) && all(call -> length(call[7]) == 3, M.calls) + @test !isempty(typed.media) && all(==(3), typed.media) + @test !isempty(generic.media) && all(==(3), generic.media) + # A homogeneous formula on the same earth consumes the `:default` reduction and records + # it as an explicit one would. The requested selection stays as declared. + unspecified=compute(problem, Formulation()) + explicit=formula(:default; equivalent_earth = formula(:default)) + given=compute(problem, Formulation(earth_impedance = explicit, earth_admittance = explicit)) + @test unspecified.Z.values == given.Z.values && unspecified.Y.values == given.Y.values + for slot in (:earth_impedance, :earth_admittance) + @test recorded(unspecified, slot).order === :after + @test recorded(unspecified, slot).rule.identifier === :bottommost + @test recorded(unspecified, slot) == recorded(given, slot) + @test requested(unspecified, slot) === nothing + end + # An explicit reduction on a layered formula applies, and the record shows it. + empty!(M.calls) + reduced=M.selection(M.EI; equivalent_earth = EH.AfterFD(:default)) + result=compute(problem, Formulation(earth_impedance = reduced, earth_admittance = potential)) + @test recorded(result, :earth_impedance).rule.identifier === :bottommost + @test recorded(result, :earth_admittance) === nothing + impedance_calls=filter(call -> call[1] === :EarthImpedance, M.calls) + @test !isempty(impedance_calls) && all(call -> length(call[7]) == 2, impedance_calls) +end + +@testitem "Engine / common earth functors retain evaluated material values" tags=[:unit, :engine] begin + const E=LineCableModels.Engine + μ0, ε0=4pi*1e-7, 8.8541878128e-12 + rho, epsilon, mu=[Inf, 100.0], [ε0, 10ε0], [μ0, μ0] + jω=complex(0.0, 2pi*50) + pair=E.EarthPair(1, 2, (-1.0, -2.0), 0.75, (2, 2)) + for (owner, id) in ((E.EarthImpedance, :wedepohl1973), (E.EarthAdmittance, :ideal)) + selected=owner.Formula(id) + # The Functor of an earth calculation keeps its input as given, adds its family's + # destination matrix and has an empty state. + matrices=(Zearth = zeros(ComplexF64, 2, 2), Pearth = zeros(ComplexF64, 2, 2)) + functor=LineCableModels.Functor(selected, (; rho, epsilon, mu, jω); + workspace = (buffers = matrices,)) + @test functor.formula === selected + @test functor.input.mu === mu && functor.input.epsilon === epsilon + @test only(functor.input.destinations) === + (owner === E.EarthImpedance ? matrices.Zearth : matrices.Pearth) + @test isempty(functor.state) + @test isfinite(selected(rho, epsilon, mu, jω, pair)) + @test_throws ArgumentError owner.Formula(:default; parameters = (unknown = 1,)) + @test_throws DimensionMismatch selected( + [Inf, 100.0, 999.0], [ε0, 10ε0, 20ε0], [μ0, μ0, μ0], jω, pair) + end + @test LineCableModels.ComputationOptions === LineCableModels.Commons.ComputationOptions + @test !(ComputationOptions() isa NamedTuple) +end + +@testitem "Engine / earth field formulas consume material values without constitutive dispatch" tags=[:unit, :engine] begin + const E = LineCableModels.Engine + rho = [1e8, 100.0] + epsilon = 8.8541878128e-12 .* [1.5, 10.0] + mu = 4pi * 1e-7 .* [1.25, 2.5] + s = 100pi * im + for (owner, ids, heights, layers) in ( + (E.EarthImpedance, (:carson1926, :gary1976, :wise1934), (1.0, 1.5), (1, 1)), + (E.EarthImpedance, + (:pollaczek1926, :saad1996, :wedepohl1973, :xue2018, :unified), + (-1.0, -1.5), (2, 2)), + (E.EarthImpedance, (:ametani2009, :lucca1994), (1.0, -1.5), (1, 2)), + (E.EarthAdmittance, (:wise1948,), (1.0, 1.5), (1, 1)), + (E.EarthAdmittance, (:pollaczek1926, :xue2018, :unified), (-1.0, -1.5), (2, 2))) + pair = E.EarthPair(1, 2, heights, 0.4, layers) + for id in ids + selected = owner.Formula(id) + # Unified solves the whole system and runs only in a computation. + id === :unified && continue + if id in (:gary1976, :saad1996, :wedepohl1973, :lucca1994) + @test isfinite(selected(rho, epsilon, mu, s, pair)) + else + @test_throws "not yet implemented" selected(rho, epsilon, mu, s, pair) + end + end + end + @test mu == 4pi * 1e-7 .* [1.25, 2.5] + @test epsilon == 8.8541878128e-12 .* [1.5, 10.0] +end + +@testitem "Engine / indexed finite-layer methods determine admission and ordered assembly" tags=[:unit, :parametric, :slow] setup=[FormulaFixtures] begin + using LinearAlgebra + const M=FormulaFixtures + const E=M.E + const EI=M.EI + const EA=M.EA + const EP=M.EP + const Expression=LineCableModels.Expression + empty!(M.calls) + inventories=(EI.formulas(EI.Formula), EA.formulas(EA.Formula)) + selected=M.selection(EI; options = (integration = (method = :quad, options = (;)),)) + heights=(2.0, -0.25, -1.5) + pairs=[E.EarthPair(t, s, (heights[s], heights[t]), s==t ? 0.0 : 1.0, + (s, t); radius = s==t ? 0.01 : nothing) for s in 1:3 for t in 1:3] + projected=formulation_options(selected, [Expression(selected, pair) for pair in pairs]) + @test isempty(M.calls) # Structural preflight does not execute kernels. + @test count(options -> haskey(options.data, :integration), projected.options) == 1 + @test projected.options[1].data.integration.method === Val(:quad) + # Unconsumed integral controls. + @test_throws ArgumentError formulation_options(selected, + [Expression(selected, pair) for pair in pairs[2:end]]) + absent=E.EarthPair(1, 2, (-2.0, -3.0), 1.0, (3, 4)) + four=build(EP.EarthModel, (EP.EarthLayer(100.0, 10.0, 1.0, 0.5), + EP.EarthLayer(200.0, 20.0, 1.0, 0.5), EP.EarthLayer(300.0, 30.0, 1.0))) + message="the earth model has 4 layers and formula :LayerImpedance is defined up to layer 3; it has no expression for a mutual interaction from layer 3 to layer 4" + @test_throws message E.validate(Expression(selected, absent), four.layers) + @test isempty(M.calls) + + material=Material(kind = :conductor, rho = 1.7241e-8) + design=build(CableDesign, "indexed-fixture", + Stack(Group(:phase, Region(:core, Disk(0.01), material)))) + system=build(LineCableSystem, fill(design, 3), + [(Float64(i), h) for (i, h) in enumerate(heights)]; + connections = [(phase = i,) for i in 1:3]) + earth=build(EP.EarthModel, ( + EP.EarthLayer(100.0, 10.0, 1.0, 0.5), EP.EarthLayer(200.0, 20.0, 1.0))) + problem=LineParametersProblem(system; earth_props = earth, frequencies = [50.0, 500.0]) + fd=M.DispersiveEarth(exponent = 0) + fd_calls=fd.seen + formulation=Formulation(earth_impedance = selected, earth_admittance = M.selection(EA), + earth_properties = fd, + options = (ideal_transposition = false,)) + result=compute(problem, formulation; options = (trace = true,)) + @test length(fd_calls) == 4 + execution=computation_options(LineCableModelsCoaxial, ComputationOptions((;))) + T=eltype(problem) + blueprints=E.CableBlueprint{T}[E.flatten(LineCableModelsCoaxial(), d, T) + for d in problem.system.designs] + workspace=E.LineParametersWorkspace(problem, formulation, execution, blueprints) + @test length(fd_calls) == 4 # Allocation did not reevaluate material laws. + @test workspace.buffers.quadrature !== nothing + @test length(M.calls) == 36 + for (_, _, row, column, layers, geometry, rho, physical) in M.calls + @test layers == (column, row) + @test geometry == (heights[column], heights[row]) + @test rho == [Inf, 50.0, 100.0] + @test physical.layers == layers && physical.heights == geometry + end + for k in eachindex(problem.frequencies), t in 1:3, s in 1:3 + # Declared manufactured rule, evaluated independently of the probe method. + coefficient=11s+17t+3t+5s+problem.frequencies[k]/100+(s==t ? 101 : 0) + @test details(result).data.trace.Zg[t, s, k] ≈ coefficient*(1e-4+1e-3im) rtol=3e-6 + @test details(result).data.trace.Pg[t, s, k] ≈ coefficient*1e9 + end + @test result.Z.values[1, 2, 1] ≈ (11*2+17+3+5*2+0.5)*(1e-4+1e-3im) + @test result.Z.values[2, 1, 1] ≈ (11+17*2+3*2+5+0.5)*(1e-4+1e-3im) + @test result.Y.values[:, :, 1] ≈ 2pi * 50im * inv(details(result).data.trace.P[:, :, 1]) + # A full-layer impedance and a separately reduced homogeneous potential + # consume different material inventories in the same solve. + buried=build(LineCableSystem, fill(design, 3), + [(1.0, -0.1), (2.0, -0.25), (3.0, -1.5)]; + connections = [(phase = i,) for i in 1:3]) + buried_problem=LineParametersProblem(buried; earth_props = earth, frequencies = [50.0]) + for order in (:before, :after) + empty!(M.calls) + empty!(fd_calls) + hybrid=Formulation(earth_impedance = M.selection(EI), + earth_admittance = formula(:unified; equivalent_earth = formula(:default; order)), + earth_properties = fd, + options = (ideal_transposition = false,)) + value=compute(buried_problem, hybrid) + @test all(isfinite, value.Z.values) && all(isfinite, value.Y.values) + @test length(M.calls) == 9 + @test all(record -> length(record[7]) == 3, M.calls) + @test Set(record[5] for record in M.calls) == Set(((2, 2), (2, 3), (3, 2), (3, 3))) + @test length(fd_calls) == (order === :before ? 11 : 2) + @test details(value).data.formulations.methods.earth_impedance.equivalent_earth === + nothing + @test details(value).data.formulations.methods.earth_admittance.equivalent_earth.order === + order + end + @test (EI.formulas(EI.Formula), EA.formulas(EA.Formula)) === inventories +end + +@testitem "Engine / unrelated coupled formula calculates in the main workspace without quadrature" tags=[:unit, :parametric] setup=[ + TestFixtures, FormulaFixtures] begin + const E=LineCableModels.Engine + const M=FormulaFixtures + problem=TestFixtures.three_bare_wires_problem(heights = (-1.0, -1.0, -1.0), + frequencies = [50.0, 50.0, 500.0]) + selected=Formulation(earth_impedance = M.CoupledImpedance(), + earth_admittance = M.selection(M.EA), + options = ( + reduce_bundle = false, kron_reduction = false, ideal_transposition = false)) + execution=computation_options(LineCableModelsCoaxial, ComputationOptions(trace = true)) + blueprints=[E.flatten(LineCableModelsCoaxial(), d, Float64) + for d in problem.system.designs] + workspace=E.LineParametersWorkspace(problem, selected, execution, blueprints) + @test workspace.buffers.coupled isa Matrix{ComplexF64} + @test !haskey(workspace.buffers, :quadrature) + @test isempty(selected.methods.earth_impedance.solves) + @test (@inferred E._solve!(workspace, selected)) === workspace + @test length(selected.methods.earth_impedance.solves)==3 + @test isempty(workspace.trace.integrals) + for k in 1:3, p in 1:3, q in 1:3 + @test workspace.trace.Zg[p, q, k]≈2π * im * problem.frequencies[k] * 1e-6 * + (6+p+2q+(p==q ? 3 : 0)) + end + E._solve!(workspace, selected) + @test length(selected.methods.earth_impedance.solves)==6 + other=E.LineParametersWorkspace(problem, selected, execution, blueprints) + @test other.buffers.coupled !== workspace.buffers.coupled + @test compute(problem, selected).Z.values≈workspace.buffers.Zout +end + +@testitem "Engine / one workspace overwrites its buffers independently of layout" tags=[:unit, :parametric] setup=[TestFixtures] begin + const E=LineCableModels.Engine + selection=Formulation(options = (reduce_bundle = false, kron_reduction = false, + ideal_transposition = false)) + execution=computation_options(LineCableModelsCoaxial, ComputationOptions(trace = true)) + layouts=((1.0, 1.0, 1.0), (-1.0, -1.0, -1.0), (1.0, -1.0, -1.0), + (-1.0, 1.0, -1.0), (-1.0, -1.0, 1.0)) + problems=map( + heights->TestFixtures.three_bare_wires_problem(; + heights, frequencies = [50.0, 50.0, 500.0]), + layouts) + workspaces=map(problems) do problem + blueprints=only(E.flatten(LineCableModelsCoaxial(), problem.system.designs, + Float64, [selection])) + E.LineParametersWorkspace(problem, selection, execution, blueprints) + end + @test all(w -> typeof(w) === typeof(first(workspaces)), workspaces) + two_wire_problem=TestFixtures.line_parameters_problem(TestFixtures.two_wire_system(); + frequencies = [50.0]) + two_wire_blueprints=only(E.flatten(LineCableModelsCoaxial(), + two_wire_problem.system.designs, Float64, [selection])) + two_wire_workspace=E.LineParametersWorkspace(two_wire_problem, selection, + execution, two_wire_blueprints) + @test typeof(two_wire_workspace) === typeof(first(workspaces)) + for (w, problem) in zip(workspaces, problems) + @test length(w.buffers.earth.calculations) == 1 + calculation=only(w.plan.earth.calculations) + @test calculation.impedance.pairs == calculation.admittance.pairs + # Poison only numerical buffers. Identity matrices and geometry are inputs. + for array in (w.buffers.Zearth, w.buffers.Pearth, w.buffers.Zprimitive, + w.buffers.Pprimitive, w.buffers.rho_cond, w.buffers.dielectric_admittivity, + w.buffers.earth.evaluated...) + fill!(array, NaN) + end + for array in ( + w.buffers.axial_field, w.buffers.source_potential, w.buffers.current_map, + w.buffers.enclosed_impedance, w.buffers.enclosed_potential, w.buffers.current_factor, + w.buffers.current_rhs, w.buffers.radial_argument, w.buffers.source_logscale, + w.buffers.circumference_average, w.buffers.radial_current, w.buffers.earth_spectrum...) + fill!(array, NaN) + end + for materials in w.buffers.earth.calculations + foreach(a->fill!(a, NaN), (materials.rho, materials.epsilon, materials.mu)) + end + E._solve!(w, selection) + first_result=E._finish(w, problem, selection, execution.data.output_basis) + Z, Y=copy(first_result.Z.values), copy(first_result.Y.values) + @test all(isfinite, Z) && all(isfinite, Y) + @test Z[:, :, 1] == Z[:, :, 2] && Y[:, :, 1] == Y[:, :, 2] + # Γ=0: one Z and one voltage integral per ordered interaction. A second + # solve for the potential consumer would double this observed work. + @test length(w.trace.integrals) == 2*3^2*3 + fill!(w.buffers.Zearth, NaN) + fill!(w.buffers.Pearth, NaN) + E._solve!(w, selection) + @test w.buffers.Zout == Z && w.buffers.Yout == Y + @test first_result.Z.values == Z && first_result.Y.values == Y + @test first_result.Z.values !== w.buffers.Zout + @test details(first_result).data.trace.Zg !== w.trace.Zg + @test details(first_result).data.trace.integrals !== w.trace.integrals + @test length(w.trace.integrals) == 2*3^2*3 # Trace belongs to this solve only. + # Public scans remain sorted. Revisit the actual material-earth stages + # in reverse index order to expose any prior-frequency readiness state. + for frequency in (3, 2, 1) + E.materials!(w, selection, frequency) + E.earth!(w, frequency) + @test w.buffers.Zearth == w.trace.Zg[:, :, frequency] + @test w.buffers.Pearth == w.trace.Pg[:, :, frequency] + end + end + first, second=workspaces[1:2] + for field in (:Zearth, :Pearth, :Zprimitive, :Pprimitive) + @test getproperty(first.buffers, field) !== getproperty(second.buffers, field) + end + @test first.buffers.axial_field !== second.buffers.axial_field + @test first.buffers.quadrature.segments !== second.buffers.quadrature.segments +end + +@testitem "Engine / coupled material admission precedes integration and publication" tags=[:unit, :parametric] setup=[TestFixtures] begin + const E=LineCableModels.Engine + problem=TestFixtures.three_bare_wires_problem(frequencies = [50.0]) + selected=Formulation() + execution=computation_options(LineCableModelsCoaxial, ComputationOptions(trace = true)) + blueprints=only(E.flatten(LineCableModelsCoaxial(), problem.system.designs, Float64, [selected])) + w=E.LineParametersWorkspace(problem, selected, execution, blueprints) + E.materials!(w, selected) + E.materials!(w, selected, 1) + fill!(w.buffers.axial_field, NaN) + materials=only(w.buffers.earth.calculations) + materials.rho[2, 1]=-1 + @test_throws DomainError E.earth!(w, 1) + @test isempty(w.trace.integrals) + @test all(isnan, w.buffers.axial_field) +end + +@testitem "Engine / distinct coupled controls perform separate complete calculations" tags=[:unit, :parametric, :slow] setup=[TestFixtures] begin + problem=TestFixtures.three_bare_wires_problem(heights = (1.0, -1.0, -1.0), frequencies = [50.0]) + z=formula(:unified; options = (integration = (options = (rtol = 1e-8,),),)) + p=formula(:unified; options = (integration = (options = (rtol = 1e-9,),),)) + selected=Formulation(earth_impedance = z, earth_admittance = p, + options = ( + reduce_bundle = false, kron_reduction = false, ideal_transposition = false)) + actual=compute(problem, selected; options = (trace = true,)) + trace=details(actual).data.trace + @test length(trace.integrals) == 2*2*3^2 + for (definition, field) in ((z, :Z), (p, :Y)) + reference=compute(problem, + Formulation(earth_impedance = definition, + earth_admittance = definition, options = (reduce_bundle = false, + kron_reduction = false, ideal_transposition = false))) + @test getproperty(actual, field).values ≈ getproperty(reference, field).values rtol=1e-12 + end +end + +@testitem "Engine / fixed material evaluation follows allocation and repeats per solve" tags=[:unit, :parametric] setup=[ + TestFixtures, FormulaFixtures] begin + const E=LineCableModels.Engine + const M=FormulaFixtures + td, fd=M.ScaledResistivity(), M.DispersiveEarth(exponent = 0) + problem=TestFixtures.three_bare_wires_problem(frequencies = [50.0, 500.0]) + selected=Formulation(temperature_dependence = td, earth_properties = fd) + execution=computation_options(LineCableModelsCoaxial, ComputationOptions()) + blueprints=only(E.flatten(LineCableModelsCoaxial(), problem.system.designs, Float64, [selected])) + w=E.LineParametersWorkspace(problem, selected, execution, blueprints) + @test isempty(td.seen) && isempty(fd.seen) + E._solve!(w, selected) + @test length(td.seen) == length(w.input.cable.conductor_materials) + @test length(fd.seen) == length(problem.frequencies) + @test all(record -> last(record) === w, td.seen) + @test all(record -> last(record) === w, fd.seen) + E._solve!(w, selected) + @test length(td.seen) == 2length(w.input.cable.conductor_materials) + @test length(fd.seen) == 2length(problem.frequencies) +end + +@testitem "Engine / material laws and numerical options do not change physical routing" tags=[:unit, :parametric] setup=[ + TestFixtures, FormulaFixtures] begin + const E=LineCableModels.Engine + base=TestFixtures.line_parameters_problem(frequencies = [50.0, 1e5]) + fd=FormulaFixtures.DispersiveEarth(exponent = 0) + @test_throws ArgumentError compute(base, + Formulation(earth_impedance = :carson1926, earth_properties = fd)) + # Unsupported equation fails when invoked, after legitimate material stages. + empty!(fd.seen) + selected=Formulation(earth_properties = fd) + changed=compute(base, selected) + material=base.earth_props.layers[2] + equivalent=LineParametersProblem(base.system; temperature = base.temperature, + earth_props = homogeneous(rho = material.rho/2, eps_r = material.eps_r, mu_r = material.mu_r), + frequencies = base.frequencies) + expected=compute(equivalent) + @test changed.Z.values ≈ expected.Z.values rtol=1e-10 + @test changed.Y.values ≈ expected.Y.values rtol=1e-10 + @test Set(record[2] for record in fd.seen)==Set(base.frequencies) + @test details(changed).data.formulations.methods.earth_properties.identifier === + :DispersiveEarth + reference=compute(base) + for rtol in (1e-7, 1e-9) + result=compute(base, + Formulation( + earth_impedance = formula( + :default; options = (integration = ( + method = :quad, options = (; rtol)),)), + earth_admittance = formula( + :default; options = (integration = ( + method = :quad, options = (; rtol)),)))) + @test result.Z.values ≈ reference.Z.values rtol=3e-6 + @test result.Y.values ≈ reference.Y.values rtol=3e-6 + controls=details(result).data.formulations.methods.earth_impedance.options.integration + @test controls.method === :quad + @test controls.options.rtol === rtol + end +end + +@testitem "Engine / consumer earth state preserves scalar types and uncertainty" tags=[:unit, :measurements] setup=[FormulaFixtures] begin + using Measurements + const E=LineCableModels.Engine + for T in (Float32, Float64, BigFloat) + rho=T[Inf, 100] + epsilon=T(8.8541878128e-12) .* T[1, 10] + mu=fill(T(4pi*1e-7), 2) + pair=E.EarthPair(1, 2, (-one(T), -T(2)), T(0.75), (2, 2)) + s=complex(zero(T), T(2)*T(pi)*T(50)) + for owner in (E.EarthImpedance, E.EarthAdmittance), method in (:quad,) + + selected=FormulaFixtures.selection(owner; scale = one(T)) + result=selected(rho, epsilon, mu, s, pair) + @test result isa Complex{T} + @test isfinite(result) + end + end + rho=measurement.([Inf, 100.0], [0.0, 1.0]) + epsilon=measurement.(8.8541878128e-12 .* [1, 10], 0.0) + mu=measurement.(fill(4pi*1e-7, 2), 0.0) + s=complex(measurement(0.0, 0.0), measurement(2pi*50, 0.0)) + pair=E.EarthPair(1, 2, (-1.0, -2.0), 0.75, (2, 2)) + for owner in (E.EarthImpedance, E.EarthAdmittance) + value=FormulaFixtures.selection(owner; scale = rho[2])( + rho, epsilon, mu, s, pair) + @test value isa Complex{Measurement{Float64}} + @test isfinite(value) + @test uncertainty(real(value)) > 0 + @test uncertainty(abs(value)) > 0 + end +end diff --git a/test/unit/engine/formula_execution.jl b/test/unit/engine/formula_execution.jl new file mode 100644 index 000000000..ac9e82314 --- /dev/null +++ b/test/unit/engine/formula_execution.jl @@ -0,0 +1,461 @@ +@testitem "Engine / explicit equation recipes are complete only for required cases" tags=[:unit, :parametric] setup=[TestFixtures] begin + const E=LineCableModels.Engine + omitted=Formulation() + absent=Formulation(earth_impedance = nothing, earth_admittance = nothing, + internal_impedance = nothing) + for slot in (:earth_impedance, :earth_admittance, :internal_impedance) + @test formula_id(getproperty(absent.methods, slot)) == + formula_id(getproperty(omitted.methods, slot)) + end + buried=TestFixtures.three_bare_wires_problem(heights = (-1.0, -1.0, -1.0), frequencies = [50.0]) + overhead=TestFixtures.three_bare_wires_problem(frequencies = [50.0]) + options=(reduce_bundle = false, kron_reduction = false, ideal_transposition = false) + partial=Formulation(earth_impedance = (earth = formula(:unified),), + earth_admittance = (earth = formula(:unified),); options) + expected=compute(buried, Formulation(; options)) + actual=compute(buried, partial) + @test Z(actual) == Z(expected) + @test Y(actual) == Y(expected) + @test_throws ArgumentError compute(overhead, partial) + @test_throws ArgumentError compute(buried, Formulation(earth_impedance = (;))) + @test_throws ArgumentError compute(buried, + Formulation(earth_impedance = (earth = nothing,))) + excess=Formulation( + earth_impedance = (air = formula(:carson1926), earth = formula(:unified)), + earth_admittance = (air = formula(:wise1948), earth = formula(:unified)); options) + @test Z(compute(buried, excess)) == Z(expected) + @test Y(compute(buried, excess)) == Y(expected) + @test_throws ArgumentError Formulation(earth_impedance = (soil = formula(:unified),)) + @test_throws ArgumentError Formulation(earth_admittance = formula(:unknown)) + disabled=Formulation(earth_properties = nothing, temperature_dependence = nothing) + @test disabled.methods.earth_properties === nothing + @test disabled.methods.temperature_dependence === nothing +end + +@testitem "Engine / unimplemented earth equations are explicit numerical stubs" tags=[:unit, :engine] begin + const E = LineCableModels.Engine + for (owner, operation) in ((E.EarthImpedance, E.EarthImpedance.earth_impedance), + (E.EarthAdmittance, E.EarthAdmittance.earth_potential_coefficient)) + @test formula_id(owner.Formula(:default)) === :unified + implemented = owner === E.EarthImpedance ? + ( + :default, :unified, :saad1996, :wedepohl1973, :gary1976, :lucca1994) : + (:default, :unified, :ideal) + earth = homogeneous(rho = 100.0) + for identifier in setdiff(owner.formulas(owner.Formula), implemented) + selected = owner.Formula(identifier) + @test formula_id(selected) === identifier + @test !isempty(description(selected)) + # Stubs fail before reading a functor or executing any numerics: a declared + # stub when called, an undeclared case when the plan checks the earth model. + for pair in (E.EarthPair(1, 2, (1.0, 2.0), 1.0, (1, 1)), + E.EarthPair(1, 2, (-1.0, -2.0), 1.0, (2, 2))) + expression = LineCableModels.Expression(selected, pair) + @test expression.method === operation + @test_throws ArgumentError begin + validate(expression, earth.layers) + expression(nothing, nothing) + end + end + end + end +end + +@testitem "Engine / prescribed propagation is a unified formulation option" tags=[:unit, :measurements, :slow] setup=[TestFixtures] begin + const E=LineCableModels.Engine + problem=TestFixtures.three_bare_wires_problem(frequencies = [50.0, 500.0]) + options=(reduce_bundle = false, kron_reduction = false, ideal_transposition = false) + make(gamma)=Formulation( + earth_impedance = formula(:unified; options = (Γ = gamma,)), + earth_admittance = formula(:unified; options = (Γ = gamma,)); options) + implicit=compute(problem, Formulation(; options)) + zero_gamma=compute(problem, make(0.0im)) + @test Z(zero_gamma) == Z(implicit) + @test Y(zero_gamma) == Y(implicit) + scalar=compute(problem, make(1e-4im)) + aligned=compute(problem, make(fill(1e-4im, 2))) + @test Z(scalar) == Z(aligned) + @test Y(scalar) == Y(aligned) + @test_throws DimensionMismatch compute(problem, make([1e-4im])) + @test_throws ArgumentError make(NaN) + @test_throws ArgumentError make([0im, complex(Inf)]) + for invalid in (true, [], (0, 1), "0", [0, NaN]) + @test_throws ArgumentError make(invalid) + end + @test_throws ArgumentError Formulation( + earth_impedance = formula(:unified; parameters = (Γ = 1e-4im,))) + method=LineCableModels.Expression(make(0).methods.earth_impedance, + E.EarthAdmittance.source_coefficients, Val(:self), Val(1), Val(1)) + @test formulation_options(method).data.Γ == 0 + @test formulation_options(method, FormulationOptions(Γ = 2e-4im)).data.Γ == 2e-4im + # The formula's construction validates Γ once. Its expressions take it as supplied. + @test_throws ArgumentError E.EarthImpedance.Formula(:unified; options = (Γ = NaN,)) + @test_throws ArgumentError formulation_options(method, FormulationOptions(integration = 1)) + using Measurements + rho=measurement(0.1, 0.001) + material_problem=TestFixtures.three_bare_wires_problem(; rho, + radius = oftype(rho, 0.0425), frequencies = [50.0, 500.0]) + uncertain=compute(material_problem, make(1e-4im)) + @test eltype(Z(uncertain)) === Complex{Measurement{Float64}} + @test any(>(0), uncertainty.(real.(Z(uncertain)))) + @test E.same_physical_state([rho], [rho]) + @test !E.same_physical_state([rho], [measurement(0.1, 0.001)]) + record=LineCableModels.ImportExport.serialize_value(aligned) + restored=LineCableModels.ImportExport.deserialize_value(record) + @test details(restored).data.formulations.methods.earth_impedance.options.Γ == + fill(1e-4im, 2) + prescribed=[1e-4im, 2e-4im] + sweep=compute(problem, make(prescribed)) + # Run scalar samples in reverse call order. Prescriptions follow the + # existing frequency coordinates. Both sides retain their original order. + for index in reverse(eachindex(problem.frequencies)) + single=LineParametersProblem(problem.system; earth_props = problem.earth_props, + temperature = problem.temperature, frequencies = [problem.frequencies[index]]) + result=compute(single, make(prescribed[index])) + @test Z(result)[:, :, 1] == Z(sweep)[:, :, index] + @test Y(result)[:, :, 1] == Y(sweep)[:, :, index] + end + setprecision(BigFloat, 128) do + mixed_precision=compute(problem, LineParametersFormulation[ + make(1e-4im), make(big"0.0001"*im)]) + @test eltype(Z(mixed_precision[1])) === ComplexF64 + @test eltype(Z(mixed_precision[2])) === Complex{BigFloat} + @test Z(mixed_precision[2]) == Z(compute(problem, make(big"0.0001"*im))) + end +end + +@testitem "Engine / completed prescriptions are detached without replacing uncertainty sources" tags=[:unit, :measurements] setup=[ + TestFixtures, FormulaFixtures] begin + using Measurements + E, IE=LineCableModels.Engine, LineCableModels.ImportExport + problem=TestFixtures.three_bare_wires_problem(frequencies = [50.0, 500.0]) + original=[1e-4+2e-4im, 3e-4-1e-4im] + supplied=copy(original) + selection=formula(:unified; options = (Γ = supplied,)) + formulation=Formulation(earth_impedance = selection, earth_admittance = selection) + first_result=compute(problem, formulation) + first_Z, first_Y=copy(Z(first_result)), copy(Y(first_result)) + record=details(first_result).data.formulations + for family in (:earth_impedance, :earth_admittance) + @test getproperty(record.methods, family).options.Γ == original + @test getproperty(record.requested, family).options.Γ == original + @test getproperty(record.methods, family).options.Γ !== + getproperty(formulation.methods, family).options.data.Γ + end + for family in (:earth_impedance, :earth_admittance) + getproperty(formulation.methods, family).options.data.Γ[2]=7e-4im + end + supplied[2]=7e-4im + second_result=compute(problem, formulation) + @test Z(first_result) == first_Z && Y(first_result) == first_Y + @test Z(second_result) != first_Z || Y(second_result) != first_Y + restored=IE.deserialize_value(IE.serialize_value(first_result)) + for retained in (first_result, restored), + family in (:earth_impedance, :earth_admittance) + + data=details(retained).data.formulations + @test getproperty(data.methods, family).options.Γ == original + @test getproperty(data.requested, family).options.Γ == original + @test Z(retained) == first_Z && Y(retained) == first_Y + end + rho=measurement(1.0, 0.01) + custom=FormulaFixtures.selection(E.EarthImpedance; scale = rho) + retained=NamedTuple(Formulation(earth_impedance = custom)) + @test retained.methods.earth_impedance.parameters.scale === rho + @test uncertainty(retained.methods.earth_impedance.parameters.scale-rho) == 0 +end + +@testitem "Engine / prescribed options govern coupled sharing without entering physical UQ" tags=[:unit, :measurements, :slow] setup=[TestFixtures] begin + using Measurements + E=LineCableModels.Engine + prescribed=[1e-4+2e-4im, 3e-4-1e-4im] + problem=TestFixtures.three_bare_wires_problem(frequencies = [50.0, 500.0]) + select(gamma; + controls = (;))=formula(:unified; + options = (Γ = gamma, integration = (method = :quad, options = controls))) + form(z, + p)=Formulation(earth_impedance = z, earth_admittance = p; + options = ( + reduce_bundle = false, kron_reduction = false, ideal_transposition = false)) + joined=compute(problem, form(select(prescribed), select(copy(prescribed))); + options = (trace = true,)) + changed=prescribed .* 2 + separate=compute(problem, form(select(prescribed), select(changed)); options = (trace = true,)) + other=compute(problem, form(select(changed), select(changed)); options = (trace = true,)) + @test length(details(separate).data.trace.integrals) == + 2length(details(joined).data.trace.integrals) + @test Z(separate) == Z(joined) + @test Y(separate) == Y(other) + controls=compute( + problem, form(select(prescribed), select(prescribed; controls = (rtol = 1e-9,))); + options = (trace = true,)) + @test length(details(controls).data.trace.integrals) == + 2length(details(joined).data.trace.integrals) + selected=form(select(prescribed), select(prescribed)) + space=Gridspace{LineParametersProblem}( + rho->TestFixtures.three_bare_wires_problem(; rho, + radius = oftype(rho, 0.0425), frequencies = [50.0, 500.0]), + (Grid(0.1, AbsoluteError(0.001)),)) + sampled=compute(ParametricProblem(space), + MonteCarlo(selected; trials = 3, seed = 314, distribution = :normal, + return_samples = true, retain_details = true)) + @test sampled.trial_counts == [3] + @test all(isempty, details(sampled).data.failures) + for trial in only(details(sampled).data.trials), + family in (:earth_impedance, :earth_admittance) + + @test getproperty(trial.data.formulations.methods, family).options.Γ == prescribed + end + linear=compute(ParametricProblem(space), LinearError(selected)) + @test any(>(0), uncertainty.(real.(Z(first(linear))))) + @test selected.methods.earth_impedance.options.data.Γ == prescribed +end + +@testitem "Engine / numerical declarations follow selected types and indexed equations" tags=[:unit, :engine] setup=[FormulaFixtures] begin + const E=LineCableModels.Engine + const II=E.InternalImpedance + const EI=E.EarthImpedance + const Expression=LineCableModels.Expression + const M=FormulaFixtures + internal=Expression(II.Formula(:default), II.internal_impedance, Val(:outer)) + external=Expression( + EI.Formula(:default), E.EarthAdmittance.source_coefficients, Val(:self), Val(1), Val(1)) + @test formulation_options(internal)==FormulationOptions() + @test formulation_options(external).data.integration.method === :quad + @test_throws ArgumentError formulation_options( + internal, FormulationOptions(integration = (method = :quad,))) + @test_throws ArgumentError computation_options( + LineCableModelsCoaxial, ComputationOptions((integration_method = :quad,))) + # The custom selection uses existing admission and equation generics. It is + # not a changed implementation of the built-in's claimed scientific identity. + custom=M.selection(EI) + pair=E.EarthPair(1, 2, (-1.0, -1.0), 1.0, (2, 2)) + expression=Expression(custom, pair) + @test expression.selection === custom + @test isempty(only(formulation_options(custom, (expression,)).options).data) + integrating=M.selection(EI; options = (integration = (method = :quad,),)) + @test_throws ArgumentError formulation_options(integrating, + (Expression(integrating, pair),)) + rho=[Inf, 100.0] + epsilon=8.8541878128e-12 .* [1, 10] + mu=fill(4pi*1e-7, 2) + value=custom(rho, epsilon, mu, 100.0im, pair; thickness = [Inf, Inf]) + @test isfinite(value) + @test formula_id(custom) !== formula_id(external.selection) +end + +@testitem "Engine / required indexed consumers alone initialize formula storage" tags=[:unit, :parametric] setup=[ + TestFixtures, FormulaFixtures] begin + const E=LineCableModels.Engine + const M=FormulaFixtures + buried=TestFixtures.three_bare_wires_problem(heights = (-1.0, -1.0, -1.0), frequencies = [50.0]) + active=M.selection(E.EarthImpedance; layers = 2) + unused=M.selection(E.EarthImpedance; options = (integration = (method = :quad,),), layers = 2) + potential=M.selection(E.EarthAdmittance) + selected=Formulation(earth_impedance = (air = unused, earth = active), earth_admittance = potential) + first_result=compute(buried, selected) + @test isempty(unused.initialized) + required, numerical=only(active.initialized) + @test required == fill((2, 2), 9) + @test numerical === nothing + saved=copy(Z(first_result)) + second_result=compute(buried, selected) + @test Z(first_result) == saved == Z(second_result) + @test length(active.initialized)==2 + @test active.initialized[2][2] === nothing + @test isempty(unused.initialized) + # The same concrete formula needs numerical storage for its (1,1) equation. + overhead=TestFixtures.three_bare_wires_problem(frequencies = [50.0]) + compute(overhead, Formulation(earth_impedance = active, earth_admittance = potential)) + required, numerical=last(active.initialized) + @test required == fill((1, 1), 9) + @test !isempty(numerical.segments) + # Each computation provisions its own storage. + compute(overhead, Formulation(earth_impedance = active, earth_admittance = potential)) + @test last(active.initialized)[2].segments !== numerical.segments +end + +@testitem "Engine / formula initialization preserves another owner's buffer identity" tags=[:unit, :engine] setup=[FormulaFixtures] begin + const G=LineCableModels.Commons + buffers=(destination = zeros(ComplexF64, 2, 2),) + @test G.initialize_buffers((nothing,), Float64, (;), (;), buffers) === buffers + @test_throws ArgumentError G.initialize_buffers( + (FormulaFixtures.BufferReplacement(),), + Float64, (;), (;), buffers) +end + +@testitem "Engine / public internal surfaces consume spectral options and initialized workspace" tags=[:unit, :parametric] setup=[ + TestFixtures, FormulaFixtures] begin + using QuadGK + const E=LineCableModels.Engine + const II=E.InternalImpedance + const M=FormulaFixtures + @test_throws ArgumentError II.Formula(:default; options = (integration = (method = :quad,),)) + standalone_workspace=(buffers = LineCableModels.Commons.initialize_buffers( + E.SpectralIntegral, Val(:quad), Float64, (;), (;), (;)),) + args=(0.008, 0.01, 1.7241e-8, 1.0, 100.0im) + for method in (:quad,) + selected=M.SpectralSurface(method) + values=II.surface_impedances(selected, args...; workspace = standalone_workspace) + @test values.outer ≈ 5e-5 rtol=3e-6 + @test values.inner == 3e-5 + 1e-6im && values.transfer == 1e-6 + @test last(selected.seen) === (Val(method), standalone_workspace) + end + problem=TestFixtures.line_parameters_problem(frequencies = [50.0]) + results=map((:quad,)) do method + selected=M.SpectralSurface(method) + result=compute(problem, Formulation(internal_impedance = selected)) + @test !isempty(selected.seen) + @test first(selected.seen)[1] === :initialize + evaluations=filter(record->record[1]===Val(method), selected.seen) + @test !isempty(evaluations) + @test all(record -> record[2] === first(evaluations)[2], evaluations) + @test first(evaluations)[2] isa E.LineParametersWorkspace + @test !isempty(first(evaluations)[2].buffers.quadrature.segments) + result + end + @test all(isfinite, only(results).Z.values) + @test all(isfinite, only(results).Y.values) + + selected=M.SpectralSurface() + local_problem=CableConstantsProblem(TestFixtures.coaxial_design()) + local_formulation=CableConstantsFormulation(internal_impedance = selected) + local_result=compute(local_problem, local_formulation) + @test first(selected.seen)[1] === :initialize + local_evaluations=filter(record->record[1]===Val(:quad), selected.seen) + @test !isempty(local_evaluations) + @test first(local_evaluations)[2] isa E.CableConstantsWorkspace + @test all(record->record[2] === first(local_evaluations)[2], local_evaluations) + @test all(isfinite, resistance(local_result)) +end + +@testitem "Engine / surface current basis reproduces concentric wall contributions" tags=[:unit, :parametric] setup=[ + TestFixtures, FormulaFixtures] begin + using LinearAlgebra + const E = LineCableModels.Engine + const II = E.InternalImpedance + declared = (inner = 7.0+3im, outer = 11.0+5im, transfer = 2.0+1im) + selected = FormulaFixtures.SurfaceLaw(coefficients = declared) + formulation = Formulation(internal_impedance = selected, + insulation_impedance = FormulaFixtures.InsulationReactance(0)) + cable = E.LocalCableData(E.flatten(LineCableModelsCoaxial(), TestFixtures.coaxial_design())) + actual = zeros(ComplexF64, 2, 2) + E.cable_impedance!(actual, cable, fill(2e-8, 2), formulation.methods, 100pi*im) + # Surface currents are (-contained, contained + wall). The solid core's + # outer impedance is an additional contribution to its own terminal. + B = [-1.0 1.0; 0.0 1.0] + declared_matrix = [declared.inner declared.transfer; declared.transfer declared.outer] + expected = B * declared_matrix * transpose(B) + Diagonal([declared.outer, 0im]) + @test actual ≈ expected + + selected = II.Formula(:default) + builtin = Formulation(insulation_impedance = FormulaFixtures.InsulationReactance(0)) + for frequency in (1e-4, 50.0, 1e5) + coefficients = II.surface_impedances(selected, 0.008, 0.01, 1.7241e-8, 1.0, + complex(0.0, 2pi*frequency)) + W = [coefficients.inner coefficients.transfer; + coefficients.transfer coefficients.outer] + lifted = B * W * transpose(B) + s = complex(0.0, 2pi*frequency) + wall = II.surface_impedances(selected, cable.r_in[2], cable.r_ext[2], + 2e-8, cable.mu_r_cond[2], s) + solid = II.surface_impedances(selected, cable.r_in[1], cable.r_ext[1], + 2e-8, cable.mu_r_cond[1], s) + E.cable_impedance!(actual, cable, fill(2e-8, 2), builtin.methods, s) + wall_matrix = [wall.inner wall.transfer; wall.transfer wall.outer] + @test actual ≈ B * wall_matrix * transpose(B) + Diagonal([solid.outer, 0im]) + if frequency == 1e-4 + resistance = 1.7241e-8 / (pi * (0.01^2 - 0.008^2)) + @test real(lifted[2, 2]) ≈ resistance rtol=1e-10 + @test abs(real(lifted[1, 1])) < 1e-10resistance + @test abs(real(lifted[1, 2])) < 1e-10resistance + end + end +end + +@testitem "Earth / artificial material values are distinct from source restrictions" tags=[:unit, :engine] setup=[FormulaFixtures] begin + const M=FormulaFixtures + material=M.EP.EarthMaterial(100.0, -10.0, 1.0) + @test material.eps_r == -10 + @test M.EP.EarthLayer <: M.EP.AbstractEarthLayer <: M.EP.AbstractEarthModel + @test M.EP.EarthMaterial <: M.EP.AbstractEarthMaterial <: + LineCableModels.AbstractMaterial + rho=[Inf, 100.0, 200.0] + epsilon=8.8541878128e-12 .* [1, -10, -20] + mu=fill(4pi*1e-7, 3) + pair=M.E.EarthPair(1, 2, (-0.25, -1.5), 1.0, (2, 3)) + @test isfinite(M.selection(M.EI)(rho, epsilon, mu, 100.0im, pair; + thickness = [Inf, 0.5, Inf])) + @test_throws DomainError M.EI.Formula(:default)( + rho[1:2], epsilon[1:2], mu[1:2], 100.0im, + M.E.EarthPair(1, 2, (-0.25, -1.5), 1.0, (2, 2))) +end + +@testitem "Engine / internal consumers request only their actual surface kinds" tags=[:unit, :parametric] setup=[FormulaFixtures] begin + const II=LineCableModels.Engine.InternalImpedance + selected=FormulaFixtures.SurfaceLaw(kinds = (:outer,)) + @test keys(II.surface_impedances( + selected, 0.0, 0.01, 1.7e-8, 1.0, 100im)) == (:outer,) + # Called directly, the inner surface impedance has no method for this formula. + @test_throws MethodError II.surface_impedances( + selected, 0.008, 0.01, 1.7e-8, 1.0, 100im) + empty!(selected.state_inputs) + empty!(selected.evaluations) + copper=Material(kind = :conductor, rho = 1.7e-8) + dielectric=Material(kind = :insulator, rho = Inf, eps_r = 2.3) + design=build(CableDesign, "outer-only", + terminal(:core, solid(copper, Disk(0.01)), insulation(dielectric; t = 0.002))) + system=build(LineCableSystem, design, Pose2(0.0, 5.0); connections = Dict(:core=>1)) + problem=LineParametersProblem(system; frequencies = [50.0], earth_props = homogeneous(rho = 100.0)) + result=compute(problem, Formulation(internal_impedance = selected)) + @test all(isfinite, result.Z.values) + @test keys(details(result).data.formulations.methods.internal_impedance.options)==(:outer,) + @test length(selected.state_inputs)==1 + @test only(selected.evaluations)[2] === Val(:outer) +end + +@testitem "Engine / each metal constructs shared surface state once per frequency" tags=[:unit, :parametric] setup=[ + TestFixtures, FormulaFixtures] begin + selected=FormulaFixtures.SurfaceLaw() + problem=TestFixtures.line_parameters_problem(frequencies = [50.0, 100.0]) + result=compute(problem, Formulation(internal_impedance = selected)) + expected=sum(length(design.terminal_order) + for design in problem.system.designs)*length(problem.frequencies) + @test length(selected.state_inputs)==expected + @test allunique(selected.evaluations) + @test count(record -> record[2] === Val(:outer), selected.evaluations)==expected + @test any(record -> record[2] === Val(:inner), selected.evaluations) + @test any(record -> record[2] === Val(:transfer), selected.evaluations) + @test all(isfinite, result.Z.values) +end + +@testitem "Engine / first tubular primitive requests all surfaces without extra assembly terms" tags=[:unit, :parametric] setup=[FormulaFixtures] begin + M=FormulaFixtures + copper=Material(kind = :conductor, rho = 1.7e-8) + dielectric=Material(kind = :insulator, rho = Inf, eps_r = 2.3) + design=build(CableDesign, "single-hollow-wall", + Group(:wall, Region(:metal, Annulus(0.008, 0.01), copper)), + Region(:insulation, Annulus(0.01, 0.012), dielectric)) + problem=CableConstantsProblem(design) + first=M.SurfaceLaw() + result=compute(problem, CableConstantsFormulation(internal_impedance = first)) + @test length(first.state_inputs) == 1 + @test Set(last.(first.evaluations)) == Set((Val(:inner), Val(:outer), Val(:transfer))) + @test only(first.state_inputs)[1] == 0.008 + changed=M.SurfaceLaw(coefficients = ( + inner = 100+20im, outer = 2+1im, transfer = 40+30im)) + other=compute(problem, CableConstantsFormulation(internal_impedance = changed)) + @test other.R == result.R + @test other.L == result.L + # The geometry needs an expression for each surface impedance. A formula that lacks one + # fails before the loop evaluates anything. + outer_only=M.SurfaceLaw(kinds = (:outer,)) + @test_throws "formula :SurfaceLaw has no expression for :inner" compute(problem, + CableConstantsFormulation(internal_impedance = outer_only)) + @test_throws "formula :SurfaceLaw has no expression for :inner" compute( + LineParametersProblem(build(LineCableSystem, design, Pose2(0.0, -1.0); + connections = (wall = 1,)); earth_props = homogeneous(rho = 100.0), + frequencies = [50.0]), Formulation(internal_impedance = outer_only)) + @test isempty(outer_only.state_inputs) && isempty(outer_only.evaluations) +end + diff --git a/test/unit/engine/formulations.jl b/test/unit/engine/formulations.jl new file mode 100644 index 000000000..62468a39a --- /dev/null +++ b/test/unit/engine/formulations.jl @@ -0,0 +1,141 @@ +@testitem "Engine / insulation formulations / analytical limits across precision" tags=[:unit, :engine] setup=[ + UseEngineSupport, + TestNumerics, + FormulaFixtures +] begin + + impedance_formulation=InsulationImpedance.Formula(:default) + admittance_formulation=InsulationAdmittance.Formula(:default) + @test description(impedance_formulation) == + "Ametani coaxial-insulation magnetic impedance (1980)" + @test occursin("lossless", lowercase(description(admittance_formulation))) + @test formula_id(impedance_formulation) === :ametani1980 + @test formula_id(admittance_formulation) === :lossless + @test :default in InsulationImpedance.formulas(InsulationImpedance.Formula) + @test all(in(InsulationAdmittance.formulas(InsulationAdmittance.Formula)), (:lossless, :lossy, :default)) + @test all(in(SemiconAdmittance.formulas(SemiconAdmittance.Formula)), (:lossless, :lossy, :default)) + # Current lossless radial fields: `H=I/(2pi*r)` and + # `E=V/(r*log(b/a))`. Integrate their magnetic and electric energy. + for T in (Float32, Float64, BigFloat) + setprecision(BigFloat, 128) do + r_in=parse(T, "0.005") + r_ex=parse(T, "0.01") + relative_permeability=one(T) + relative_permittivity=T(3) + angular_frequency=T(2)*T(π)*T(50) + s=Complex{T}(zero(T), angular_frequency) + + impedance=impedance_formulation( + r_in, + r_ex, + relative_permeability, + s + ) + material=Material( + :insulator, T(1.0e12), relative_permittivity, one(T), + T(20), zero(T) + ) + evaluated=admittance_formulation(material, T(50), T(20)) + potential=LineCableModels.Engine.potential_coefficient( + r_in, r_ex, evaluated, s + ) + @test impedance isa Complex{T} + @test potential isa Complex{T} + @test TestNumerics.isapprox_scaled( + impedance, + Complex{T}( + zero(T), + angular_frequency*T(4)*T(pi)*parse(T,"1e-7")*relative_permeability*log(r_ex/r_in)/(T(2)*T(pi)) + ) + ) + @test TestNumerics.isapprox_scaled( + potential, + Complex{T}(log(r_ex/r_in)/(T(2)*T(pi)*parse(T,"8.8541878128e-12")*relative_permittivity)) + ) + @test iszero(impedance_formulation(zero(T), r_ex, one(T), s)) + @test iszero(impedance_formulation(r_ex, r_ex, one(T), s)) + @test iszero(LineCableModels.Engine.potential_coefficient( + zero(T), r_ex, evaluated, s + )) + @test iszero(LineCableModels.Engine.potential_coefficient( + r_ex, r_ex, evaluated, s + )) + end + end + + experimental_impedance=FormulaFixtures.InsulationReactance() + experimental_admittance=FormulaFixtures.InsulationLaw() + @test @inferred(experimental_impedance(0.01, 0.02, 1.0, 2.0im)) == 4.0im + @test_throws DomainError experimental_impedance(-0.01, 0.02, 1.0, 2.0im) + @test_throws DomainError experimental_impedance(0.01, 0.02, -1.0, 2.0im) + material=Material(:insulator, 1.0e12, 2.3, 1.0, 20.0, 0.0) + @test imag(@inferred(experimental_admittance(material, 50.0, 20.0))) ≈ 135.0 + @test_throws ArgumentError InsulationImpedance.Formula(:default; parameters = (bad = true,)) + @test_throws ArgumentError InsulationAdmittance.Formula(:lossy; parameters = (bad = true,)) +end + +@testitem "Engine / internal impedance / passivity and solid-conductor limits" tags=[:unit, :engine] setup=[ + UseEngineSupport,FormulaFixtures +] begin + formulation=InternalImpedance.Formula(:default) + @test occursin("Schelkunoff", description(formulation)) + @test InternalImpedance.formula_id(formulation) === :schelkunoff1934 + @test :default in InternalImpedance.formulas(InternalImpedance.Formula) + + r_in=0.005 + r_ex=0.01 + rho=1.7241e-8 + relative_permeability=1.0 + s=ComplexF64(0.0, 2π*50.0) + args=(r_in, r_ex, rho, relative_permeability, s) + surfaces=@inferred InternalImpedance.surface_impedances(formulation, + Val((:inner, :outer, :transfer)), args...) + inner=(@inferred InternalImpedance.surface_impedances(formulation, Val((:inner,)), + args...)).inner + outer=(@inferred InternalImpedance.surface_impedances(formulation, Val((:outer,)), + args...)).outer + transfer=(@inferred InternalImpedance.surface_impedances(formulation, Val((:transfer,)), + args...)).transfer + @test (inner, outer, transfer) == values(surfaces) + @test all(isfinite, (inner, outer, transfer)) + @test real(inner) > 0 + @test real(outer) > 0 + @test real(transfer) > 0 + @test imag(inner) >= 0 + @test imag(outer) >= 0 + + solid=@inferred InternalImpedance.surface_impedances(formulation, + Val((:inner, :outer, :transfer)), 0.0, r_ex, rho, relative_permeability, s) + @test iszero(solid.inner) + @test iszero(solid.transfer) + solid_outer=solid.outer + @test isfinite(solid_outer) + @test real(solid_outer) > 0 + # The state stores the Bessel values that inner, outer and transfer impedances share. + functor=LineCableModels.Functor(formulation, + (; r_in, r_ex, rho, mu_r = relative_permeability, jω = s)) + @test functor.state.i1_ex == LineCableModels.Engine.special_besselix(1, functor.state.w_ex) + @test functor.state.i0_in == LineCableModels.Engine.special_besselix(0, functor.state.w_in) + # An unknown kind has no method. The plan checks the needed kinds before the loop. + @test_throws MethodError InternalImpedance.surface_impedances(formulation, + Val((:unsupported,)), args...) + +end + +@testitem "Engine / a backend formulation is built by its identity" tags=[:unit, :pscad] begin + backends = (coaxial = LineParametersFormulation, cable_constants = CableConstantsFormulation, + fem = LineCableModelsFEM, pscad = PSCAD.PSCADFormulation) + for (tag, backend) in pairs(backends) + built = Formulation(tag) + @test built isa backend && formula_id(built) === tag + # A saved record stores the same identity in `backend`. + @test NamedTuple(built).backend === tag + end + # `Formulation` passes its keywords to the backend's constructor. + @test Formulation(:coaxial; earth_impedance = :carson1926).methods.earth_impedance isa + LineCableModels.Engine.EarthImpedance.Formula{:carson1926} + # Each backend has one identity. `ModalAnalysisFormulation` builds a modal analysis. + for tag in (:LineCableModelsFEM, :PSCAD, :modal, :unknown) + @test_throws ArgumentError("unknown backend formulation :$tag") Formulation(tag) + end +end diff --git a/test/unit/engine/homogeneous_selections.jl b/test/unit/engine/homogeneous_selections.jl new file mode 100644 index 000000000..13444d919 --- /dev/null +++ b/test/unit/engine/homogeneous_selections.jl @@ -0,0 +1,136 @@ +@testitem "Engine / homogeneous selections retain each indexed formula through assembly" tags=[:unit, :parametric, :slow] setup=[FormulaFixtures] begin + const E=LineCableModels.Engine + # Manufactured potential coefficients isolate the selection-routing check. + potential=FormulaFixtures.selection(E.EarthAdmittance) + material=Material(kind = :conductor, rho = 1.7241e-8) + dielectric=Material(kind = :insulator, rho = 1e14, eps_r = 2.3) + design=build(CableDesign, + "selection-fixture", + Stack(Group(:phase, + Region(:core, Disk(0.01), material)), + Region(:insulation, Annulus(0.01, 0.012), dielectric))) + positions=[(0.0, 10.0), (1.0, 12.0), (2.0, -1.0), (3.0, -2.0)] + system=build(LineCableSystem, fill(design, 4), positions; + connections = [(phase = i,) for i in 1:4]) + problem=LineParametersProblem(system; earth_props = homogeneous(rho = 100.0), frequencies = [ + 50.0, 500.0]) + # Different parameterizations of one native type remain distinct selections. + M=FormulaFixtures + empty!(M.calls) + native_choices=(air=M.selection(E.EarthImpedance;scale=1.0,layers=2), + earth=M.selection(E.EarthImpedance;scale=2.0,layers=2), + mixed=M.selection(E.EarthImpedance;scale=3.0,layers=2)) + custom=compute(problem,Formulation(earth_impedance=native_choices, + earth_admittance=potential,options=(ideal_transposition=false,));options=(trace=true,)) + impedance_calls=filter(record->record[1] === :EarthImpedance,M.calls) + @test length(impedance_calls)==32 + for k in eachindex(problem.frequencies), row in 1:4, column in 1:4 + s=positions[column][2]>0 ? 1 : 2 + t=positions[row][2]>0 ? 1 : 2 + scale=s==t ? Float64(s) : 3.0 + coefficient=11s+17t+3row+5column+problem.frequencies[k]/100+(row==column ? 101 : 0) + @test details(custom).data.trace.Zg[row,column,k] ≈ scale*coefficient*(1e-4+1e-3im) + end + @test map(value -> value.identifier, details(custom).data.formulations.methods.earth_impedance)== + (air=:HalfSpaceImpedance,earth=:HalfSpaceImpedance,mixed=:HalfSpaceImpedance) + @test details(custom).data.formulations.requested.earth_impedance.earth.parameters.scale==2.0 + + # Partial Unified publication must still use the complete physical system. + # The other entries come from the explicit manufactured equation above. + hybrid=compute(problem, Formulation( + earth_impedance=merge(native_choices, (mixed=formula(:default),)), + earth_admittance=potential, options=(ideal_transposition=false,)); + options=(trace=true,)) + complete=compute(problem, Formulation(earth_impedance=:default, + earth_admittance=potential, options=(ideal_transposition=false,)); + options=(trace=true,)) + for p in 1:4, q in 1:4 + reference=sign(last(positions[p]))==sign(last(positions[q])) ? custom : complete + @test details(hybrid).data.trace.Zg[p,q,:] ≈ + details(reference).data.trace.Zg[p,q,:] rtol=1e-10 + end + + # Potential coefficients use the same selection syntax for air, earth + # and mixed conductor pairs. + empty!(M.calls) + potential_choices=(air=M.selection(E.EarthAdmittance;scale=1.0,layers=2), + earth=M.selection(E.EarthAdmittance;scale=2.0,layers=2), + mixed=M.selection(E.EarthAdmittance;scale=3.0,layers=2)) + independent_y=compute(problem,Formulation(earth_impedance=native_choices, + earth_admittance=potential_choices,options=(ideal_transposition=false,));options=(trace=true,)) + potential_calls=filter(record->record[1] === :EarthAdmittance,M.calls) + @test count(record->record[5]==(1,1),potential_calls)==8 + @test count(record->record[5]==(2,2),potential_calls)==8 + @test count(record->record[5][1]!=record[5][2],potential_calls)==16 + for row in 1:4, column in 1:4 + s=positions[column][2]>0 ? 1 : 2 + t=positions[row][2]>0 ? 1 : 2 + scale=s==t ? Float64(s) : 3.0 + @test details(independent_y).data.trace.Pg[row,column,:] ≈ scale .* details(custom).data.trace.Pg[row,column,:] + end + @test independent_y.Z.values==custom.Z.values +end + +@testitem "Engine / scalar and homogeneous shorthand preserve numerical values and model selections" tags=[:unit, :parametric] setup=[TestFixtures, FormulaFixtures] begin + const E=LineCableModels.Engine + problem=TestFixtures.line_parameters_problem(frequencies = [50.0, 500.0]) + scalar=compute(problem, Formulation()) + same=(air = formula(:default), earth = formula(:default), mixed = formula(:default)) + shorthand=compute(problem, Formulation(earth_impedance = same, earth_admittance = same)) + @test shorthand.Z.values == scalar.Z.values + @test shorthand.Y.values == scalar.Y.values + partial=Formulation(earth_impedance=(air=formula(:default),)) + @test keys(partial.methods.earth_impedance) === (:air,) + @test_throws ArgumentError compute(problem, partial) + @test_throws ArgumentError Formulation(earth_admittance = merge(same, (other = formula(:default),))) + reordered=Formulation(earth_impedance = ( + mixed = same.mixed, earth = same.earth, air = same.air)) + @test keys(reordered.methods.earth_impedance) === (:air, :earth, :mixed) + @test keys(reordered.definitions.earth_impedance) === (:air, :earth, :mixed) + @test_throws "homogeneous selection is not defined for source in layer 2 and target in layer 3" E.Expression( + reordered.methods.earth_impedance, E.EarthPair(1, 2, (-0.25, -1.5), 1.0, (2, 3))) + model=build(E.EarthModel, + (LineCableModels.Earth.EarthLayer(100.0, 10.0, 1.0, 0.5), + LineCableModels.Earth.EarthLayer(200.0, 20.0, 1.0))) + layered=LineParametersProblem(problem.system; earth_props = model, frequencies = [50.0]) + # The cables lie about 1 m deep, below the 0.5 m first earth layer: in layer 3. A recipe + # describes a homogeneous earth. On more layers its slot takes one equivalent earth, + # decided before `air`, `earth` and `mixed` resolve on its layers 1 and 2. + recipe=compute(layered, Formulation(earth_impedance = same, earth_admittance = same)) + explicit=formula(:default; equivalent_earth = formula(:default)) + single=compute(layered, Formulation(earth_impedance = explicit, earth_admittance = explicit)) + @test recipe.Z.values == single.Z.values && recipe.Y.values == single.Y.values + for slot in (:earth_impedance, :earth_admittance) + reductions=map(route -> route.equivalent_earth, + getproperty(details(recipe).data.formulations.methods, slot)) + @test keys(reductions) == (:air, :earth, :mixed) + @test allequal(reductions) && first(reductions).rule.identifier === :bottommost + end + # A multilayer formula is used alone, and the formulas of a recipe agree on its reduction. + multilayer=merge(same, (earth = FormulaFixtures.selection(E.EarthImpedance),)) + @test_throws "formula :LayerImpedance is multilayer and is used alone" compute(layered, + Formulation(earth_impedance = multilayer)) + conflicting=merge(same, ( + air = formula(:default; equivalent_earth = formula(:default; order = :before)), + earth = formula(:default; equivalent_earth = formula(:default; order = :after)))) + @test_throws "the formulas of an earth recipe give different equivalent earths" compute( + layered, Formulation(earth_impedance = conflicting)) + air_system=build(LineCableSystem, problem.system.designs, + [Pose2(i, 10.0) for i in eachindex(problem.system.designs)]; + connections = [Dict(:core=>i, :sheath=>0) + for i in eachindex(problem.system.designs)]) + overhead_layered=LineParametersProblem(air_system; earth_props = model, frequencies = [50.0]) + # A homogeneous formula on a layered earth uses the `:default` reduction unless one is given. + unspecified=compute(overhead_layered, + Formulation(earth_impedance = same, earth_admittance = same)) + explicit=formula(:default; equivalent_earth = formula(:default)) + expected=compute(overhead_layered, + Formulation(earth_impedance = explicit, earth_admittance = explicit)) + @test unspecified.Z.values == expected.Z.values && unspecified.Y.values == expected.Y.values + # Scalar EHEM remains explicit and can consume the full physical soil inventory. + reduced=compute(layered, + Formulation( + earth_impedance = formula(:default; equivalent_earth = formula(:default)), + earth_admittance = formula(:default; equivalent_earth = formula(:default)))) + @test all(isfinite, reduced.Z.values) && all(isfinite, reduced.Y.values) +end diff --git a/test/unit/engine/ideal_earth_potential.jl b/test/unit/engine/ideal_earth_potential.jl new file mode 100644 index 000000000..baa23335d --- /dev/null +++ b/test/unit/engine/ideal_earth_potential.jl @@ -0,0 +1,49 @@ +@testitem "Engine / ideal external potential / images, zero cases and types" tags=[:unit, :engine] begin + const E = LineCableModels.Engine + function evaluate(selected, arguments...) + return @inferred selected(arguments...) + end + for T in (Float32, Float64, BigFloat) + μ0, ε0 = 4T(pi)*T(10)^(-7), parse(T, "8.8541878128e-12") + s = complex(zero(T), 100T(pi)) + formula = E.EarthAdmittance.Formula(:ideal) + for (kind, heights, x, layers, reference) in ( + (:self, (10, 10), 0, (1, 1), + "136627009046.11958786519963276511094116675099612472"), + (:mutual, (10, 12), 0.75, (1, 1), + "41930305761.703608584723801139014535060706292407094"), + (:self, (-1, -1), 0, (2, 2), "0"), + (:mutual, (-1, -2), 0.75, (2, 2), "0"), + (:mutual, (10, -2), 0.75, (1, 2), "0"), + (:mutual, (-2, 10), 0.75, (2, 1), "0")) + pair = E.EarthPair(1, kind === :self ? 1 : 2, T.(heights), T(x), layers; + radius = kind === :self ? T(1)/100 : nothing) + value = evaluate(formula, T[Inf, 100], [ε0, 10ε0], [μ0, μ0], s, pair) + @test value isa Complex{T} + @test iszero(imag(value)) + if reference == "0" + @test iszero(value) + elseif T !== Float32 + @test real(value) ≈ parse(T, reference) rtol=5e-14 + end + end + end +end + +@testitem "Engine / ideal potential / geometric uncertainty" tags=[:unit, :measurements] begin + using Measurements + const E=LineCableModels.Engine + ε0, μ0=8.8541878128e-12, 4pi*1e-7 + function potential(radius) + T=typeof(radius) + pair=E.EarthPair(1, 1, (T(10), T(10)), T(0), (1, 1); radius) + return @inferred E.EarthAdmittance.Formula(:ideal)(T[Inf, 100], T[ε0, 10ε0], + T[μ0, μ0], Complex{T}(100pi*im), pair) + end + radius=measurement(0.01, 0.0001) + coefficient=potential(radius) + @test Measurements.derivative(real(coefficient), radius) ≈ -1/(2pi*ε0*0.01) + @test iszero(uncertainty(real(coefficient-potential(radius)))) + @test uncertainty(real(coefficient-potential(measurement(0.01, 0.0001)))) > 0 + @test iszero(uncertainty(imag(coefficient))) +end diff --git a/test/unit/engine/insulation_admittance.jl b/test/unit/engine/insulation_admittance.jl new file mode 100644 index 000000000..e08040ede --- /dev/null +++ b/test/unit/engine/insulation_admittance.jl @@ -0,0 +1,450 @@ +@testitem "Engine / dielectric admittance / analytical layer and lossless limit" tags=[:unit, :parametric] setup=[ + UseEngineSupport, TestNumerics] begin + formulation=InsulationAdmittance.Formula(:lossy) + lossless=InsulationAdmittance.Formula(:default) + semicon=SemiconAdmittance.Formula(:lossy) + + for T in (Float32, Float64, BigFloat) + setprecision(BigFloat, 128) do + typed(value)=parse(T, value) + r_inner=typed("0.005") + r_outer=typed("0.01") + resistivity=typed("1.0e8") + relative_permittivity=typed("3.0") + angular_frequency=T(2)*T(π)*T(50) + frequency_point=Complex{T}(zero(T), angular_frequency) + material=Material( + :insulator, resistivity, relative_permittivity, one(T), + T(20), zero(T) + ) + evaluated=formulation(material, T(50), T(20)) + coefficient=LineCableModels.Engine.potential_coefficient( + r_inner, r_outer, evaluated, frequency_point + ) + ε0=one(T)*T(88541878128)*(one(T)*T(10))^(-22) + circumference=T(2)*(one(T)*T(π)) + log_ratio=log(r_outer/r_inner) + capacitance=circumference*ε0*relative_permittivity/log_ratio + conductance=circumference*inv(resistivity)/log_ratio + recovered_admittance=frequency_point/coefficient + # Radial current integration gives 2πκ/log(b/a). This expectation + # is derived here, without reading engine output or saved values. + expected_conductance=conductance + expected_capacitance=capacitance + @test TestNumerics.isapprox_scaled( + real(recovered_admittance), + expected_conductance + ) + @test TestNumerics.isapprox_scaled( + imag(recovered_admittance)/angular_frequency, + expected_capacitance + ) + end + end + + r_in=0.005 + r_ex=0.01 + rho=1e8 + eps_r=3.0 + s=Complex(0.0, 2π*50.0) + epsilon0=8.8541878128e-12 + + log_ratio=log(r_ex/r_in) + capacitance=2π*epsilon0*eps_r/log_ratio + conductance=2π/(rho*log_ratio) + material=Material(:insulator, rho, eps_r, 1.0, 20.0, 0.0) + coefficient=LineCableModels.Engine.potential_coefficient( + r_in, r_ex, formulation(material, 50.0, 20.0), s) + + @test s / coefficient ≈ conductance + s * capacitance + @test real(s / coefficient) > 0 + @test imag(s / coefficient) > 0 + + r_mid=0.007 + second_rho=2e8 + second_eps=5.0 + first_material=Material(:semicon, rho, eps_r, 1.0, 20.0, 0.0) + second_material=Material(:insulator, second_rho, second_eps, 1.0, 20.0, 0.0) + # Independent radial-current integration supplies the series expectation. + first_admittance=2π*(inv(rho)+s*epsilon0*eps_r)/log(r_mid/r_in) + second_admittance=2π*(inv(second_rho)+s*epsilon0*second_eps)/log(r_ex/r_mid) + series_admittance=inv(inv(first_admittance)+inv(second_admittance)) + E=LineCableModels.Engine + design=build(CableDesign, "series-dielectric", Stack( + terminal(:core, core(Material(:conductor, 1.7241e-8); r = r_in)), + Region(:screen, Annulus(r_in, r_mid), first_material), + Region(:insulation, Annulus(r_mid, r_ex), second_material))) + input=E.LocalCableData(E.flatten(LineCableModelsCoaxial(), design)) + methods=Formulation(insulation_admittance = formulation, + semicon_admittance = semicon).methods + admittivity=zeros(ComplexF64, 2) + E.dielectric!(admittivity, input, methods, 50.0, 20.0) + layers=zeros(ComplexF64, 2) + potential, admittance=zeros(ComplexF64, 1, 1), zeros(ComplexF64, 1, 1) + E.cable_potential!(potential, input, admittivity, s, layers, + zeros(ComplexF64, 1), zeros(ComplexF64, 1)) + E.cable_admittance!(admittance, input, admittivity, s, layers) + @test only(potential) ≈ s/series_admittance + @test only(admittance) ≈ series_admittance + + lossless_κ=lossless(material, 50.0, 20.0) + @test iszero(real(lossless_κ)) + lossless_coefficient=LineCableModels.Engine.potential_coefficient( + r_in, r_ex, lossless_κ, s + ) + formula_epsilon0=one(r_in)*88541878128*(one(r_in)*10)^(-22) + @test lossless_coefficient ≈ Complex( + log(r_ex/r_in)/(2*(one(r_in)*π)*formula_epsilon0*eps_r) + ) +end + +@testitem "Engine / dielectric laws promote inputs and enforce composable output" tags=[:unit, :measurements] setup=[FormulaFixtures] begin + using Measurements: measurement, uncertainty + const E=LineCableModels.Engine + const M=FormulaFixtures + for (owner, + kind, + selected) in ((E.InsulationAdmittance, :insulator, M.InsulationLaw()), + (E.SemiconAdmittance, :semicon, M.SemiconLaw())) + @test owner.Formula(selected) === selected + @test_throws ArgumentError owner.Formula(:lossy; parameters = (unrecognized = true,)) + material=Material(kind, 100.0f0, 2.3f0, 1.0f0, 20.0f0, 0.0f0) + for (frequency, + temperature) in ( + (50.0f0, 20.0f0), (50.0, 20), (50, 20.0f0), (BigFloat(50), 20.0)) + T=promote_type(eltype(material), typeof(float(frequency)), typeof(float(temperature))) + promoted=convert(Material{T}, material) + actual=@inferred constitutive(selected, material, frequency, temperature) + expected=complex(T(2)/promoted.rho, T(frequency)*promoted.eps_r+T(temperature)) + @test actual==expected + @test actual==selected(material, frequency, temperature) + @test actual isa Complex{T} + @test isfinite(E.potential_coefficient( + T(0.005), T(0.01), actual, complex(zero(T), T(2π)*T(frequency)))) + end + for frequency in (0.0, -1.0, Inf, NaN) + @test_throws DomainError constitutive(selected, material, frequency, 20.0) + end + for temperature in (Inf, NaN) + @test_throws DomainError constitutive(selected, material, 50.0, temperature) + end + # Both dielectric implementations use the same validation checks. The check + # returns the law's value. The formula call converts it to Complex{T} above. + @test validate(1+2im, selected, Float64) === 1+2im + @test validate(1.0f0+2.0f0im, selected, Float64) === 1.0f0+2.0f0im + @test_throws ArgumentError validate(1.0+2.0im, selected, Float32) + @test_throws ArgumentError validate(big"1.0"+big"2.0"*im, selected, Float64) + uncertain=complex(measurement(1.0, 0.1), measurement(2.0, 0.2)) + @test_throws ArgumentError validate(uncertain, selected, Float64) + @test uncertainty(real(validate(uncertain, selected, typeof(real(uncertain)))))==0.1 + for invalid in (NaN, Inf, true, [1.0], "bad") + @test_throws DomainError validate(invalid, selected, Float64) + end + @test material.rho === 100.0f0 + @test material.eps_r === 2.3f0 + end +end + +@testitem "Engine / dielectric admittance / strict layers reach direct computation" tags=[:unit, :measurements] setup=[ + UseEngineSupport, TestNumerics] begin + using LinearAlgebra + + function two_terminal_problem(; + uncertain = false, + frequencies = [1.0e-3, 50.0, 1.0e6] + ) + copper=Material(:conductor, 1.7241e-8, 1.0, 1.0, 20.0, 0.00393) + rho_1=uncertain ? measurement(2.0e11, 2.0e10) : 2.0e11 + eps_1=uncertain ? measurement(2.4, 0.12) : 2.4 + thickness_1=uncertain ? measurement(3.0e-3, 1.5e-4) : 3.0e-3 + dielectric_1=Material(:semicon, rho_1, eps_1, 1.0, 20.0, 0.0) + dielectric_2=Material( + :insulator, 8.0e10, 3.6, 1.0, 20.0, 0.0; + tan_delta = 0.012 + ) + outer_dielectric=Material(:insulator, 1.0e14, 2.3, 1.0, 20.0, 0.0) + + core_outer=0.010 + first_outer=core_outer+thickness_1 + insulation_outer=first_outer+4.0e-3 + sheath_outer=insulation_outer+1.0e-3 + jacket_outer=sheath_outer+2.0e-3 + design=build( + CableDesign, + "parallel-rc-test", + Stack(AbstractCablePart[ + Group(:core, Region(:core_conductor, Annulus(0.0, core_outer), copper)), + Region(:insulation_1, Annulus(core_outer, first_outer), dielectric_1), + Region(:insulation_2, Annulus(first_outer, insulation_outer), dielectric_2), + Group( + :sheath, + Region( + :sheath_conductor, + Annulus(insulation_outer, sheath_outer), + copper + )), + Region( + :outer_dielectric, + Annulus(sheath_outer, jacket_outer), + outer_dielectric + ) + ]) + ) + system=build( + LineCableSystem, + design, + Pose2(0.0, -1.0, 0.0); + connections = Dict(:core=>1, :sheath=>2), + system_id = "parallel-rc-test", + line_length = 1000.0 + ) + earth=EarthModel(100.0, 10.0, 1.0) + return LineParametersProblem( + system; + temperature = 20.0, + earth_props = earth, + frequencies + ) + end + + formulation=Formulation(; + insulation_admittance = formula(:default), + semicon_admittance = formula(:lossy), + earth_admittance = :default, + options = ( + reduce_bundle = false, + kron_reduction = false, + ideal_transposition = true + ) + ) + problem=two_terminal_problem() + execution=computation_options(LineCableModelsCoaxial, ComputationOptions((;))) + blueprints=LineCableModels.Engine.CableBlueprint{eltype(problem)}[LineCableModels.Engine.flatten( + LineCableModelsCoaxial(), + design, + eltype(problem)) + for design in problem.system.designs] + workspace=LineParametersWorkspace( + problem, formulation, execution, blueprints) + input=workspace.input + cable=input.cable + @test getproperty.(cable.dielectric_materials, :kind) == + [:semicon, :insulator, :insulator] + @test cable.semicon_indices == [1] + @test cable.insulation_indices == [2, 3] + @test cable.dielectric_materials[2].tan_delta == 0.012 + @test @inferred(LineCableModels.Engine.dielectric!( + workspace.buffers.dielectric_admittivity, + cable, + formulation.methods, + input.freq[1], + input.temperature + )) === workspace.buffers.dielectric_admittivity + parameters=compute(problem, formulation; options = (trace = true,)) + trace=details(parameters).data.trace + public_parameters=compute(problem, formulation) + @test public_parameters.Z.values == parameters.Z.values + @test public_parameters.Y.values == parameters.Y.values + @test cable.dielectric_ranges == [1:2, 3:3] + @test cable.r_layer_in[2] ≈ cable.r_layer_ext[1] + @test size(parameters.Y) == (2, 2, 3) + + for frequency_index in eachindex(input.freq) + s=input.jω[frequency_index] + function layer_coefficient(layer_index) + material=cable.dielectric_materials[layer_index] + relation=material.kind===:semicon ? + formulation.methods.semicon_admittance : + formulation.methods.insulation_admittance + evaluated=constitutive( + relation, material, input.freq[frequency_index], input.temperature) + return LineCableModels.Engine.potential_coefficient( + cable.r_layer_in[layer_index], + cable.r_layer_ext[layer_index], + evaluated, + s + ) + end + inner=sum(layer_coefficient, cable.dielectric_ranges[1]) + outer=sum(layer_coefficient, cable.dielectric_ranges[2]) + @test trace.Pin[:, :, frequency_index] ≈ [inner+outer outer + outer outer] + + admittance=parameters.Y.values[:, :, frequency_index] + @test admittance ≈ transpose(admittance) + @test all(isfinite, real.(admittance)) + @test all(isfinite, imag.(admittance)) + conductance=real.(admittance) + tolerance=1.0e-9*max(opnorm(conductance), eps()) + @test minimum(eigvals(Symmetric(conductance))) >= -tolerance + end + + function component_coefficient(component_index, frequency_index) + s=input.jω[frequency_index] + coefficient=zero(s) + for layer_index in cable.dielectric_ranges[component_index] + material=cable.dielectric_materials[layer_index] + relation=material.kind===:semicon ? + formulation.methods.semicon_admittance : + formulation.methods.insulation_admittance + evaluated=constitutive( + relation, material, input.freq[frequency_index], input.temperature) + coefficient+=LineCableModels.Engine.potential_coefficient( + cable.r_layer_in[layer_index], + cable.r_layer_ext[layer_index], + evaluated, + s + ) + end + return coefficient + end + near_dc=input.jω[1]/component_coefficient(1, 1) + high_frequency=input.jω[end]/component_coefficient(1, length(input.jω)) + @test real(near_dc) > 0 + @test imag(near_dc) > 0 + @test imag(high_frequency) > real(high_frequency) + + uncertain_parameters=compute(two_terminal_problem(uncertain = true), formulation) + @test uncertainty(real(uncertain_parameters.Y[1, 1, 2])) > 0 + @test uncertainty(imag(uncertain_parameters.Y[1, 1, 2])) > 0 + + shifted_reference=compute( + two_terminal_problem(frequencies = [1.0e-3, 50.0]), + formulation + ) + direct_reference=compute( + two_terminal_problem(frequencies = [50.0]), + formulation + ) + @test shifted_reference.Y.values[:, :, 2] == + direct_reference.Y.values[:, :, 1] + + total=compute( + problem, + formulation; + options = (output_basis = :total,) + ) + @test basis(parameters) === :pul + @test basis(total) === :total + @test total.Z.values ≈ problem.system.line_length .* parameters.Z.values + @test total.Y.values ≈ problem.system.line_length .* parameters.Y.values +end + +@testitem "Engine / lossy dielectric / Gridspace Monte Carlo samples before assembly" tags=[:unit, :measurements] setup=[ + UseEngineSupport, TestNumerics] begin + using Statistics + import LineCableModels.ParametricBuilder as PB + + copper=PB.Material( + kind = :conductor, + rho = 1.7241e-8, + eps_r = 1.0, + mu_r = 1.0, + T0 = 20.0, + alpha = 0.00393 + ) + dielectric=PB.Material( + kind = :insulator, + rho = Grid(2.0e11, 10.0), + eps_r = Grid(2.4, 5.0) + ) + outer_dielectric=PB.Material(kind = :insulator, rho = 1.0e14, eps_r = 2.3) + thickness=Grid(7.0e-3, 5.0) + build_design=function (resolved_dielectric, resolved_thickness) + core_outer=0.010 + insulation_outer=core_outer+resolved_thickness + sheath_outer=insulation_outer+1.0e-3 + jacket_outer=sheath_outer+2.0e-3 + return build( + CableDesign, + "parallel-rc-mc", + Stack(AbstractCablePart[ + Group(:core, Region(:core_conductor, Annulus(0.0, core_outer), copper)), + Region( + :core_insulation, + Annulus(core_outer, insulation_outer), + resolved_dielectric + ), + Group( + :sheath, + Region( + :sheath_conductor, + Annulus(insulation_outer, sheath_outer), + copper + )), + Region( + :outer_insulation, + Annulus(sheath_outer, jacket_outer), + outer_dielectric + ) + ]) + ) + end + design=Gridspace{CableDesign}(build_design, (dielectric, thickness)) + system=build( + LineCableSystem, + design, + Pose2(0.0, -1.0, 0.0); + connections = Dict(:core=>1, :sheath=>2), + system_id = "parallel-rc-mc", + line_length = 1000.0 + ) + problem=LineParametersProblem( + system; + temperature = 20.0, + earth_props = PB.homogeneous(rho = 100.0, eps_r = 10.0), + frequencies = [50.0, 500.0] + ) + formulation=Formulation(; + insulation_admittance = formula(:lossy), + options = ( + reduce_bundle = false, + kron_reduction = false, + ideal_transposition = true + ) + ) + monte_carlo=MonteCarlo( + formulation; + trials = 12, + distribution = :normal, + seed = 20260812, + return_samples = true + ) + + parametric_problem=ParametricProblem(problem) + sampled=compute(parametric_problem, monte_carlo) + @test sampled isa MonteCarloResult{<:LineParameters} + @test size(only(samples(sampled)).G) == (2, 2, 2, 12) + @test std(only(samples(sampled)).G[1, 1, 1, :]) > 0 + @test std(only(samples(sampled)).C[1, 1, 1, :]) > 0 + + retained=samples(sampled) + sample_matrix=vcat( + reshape(only(retained).R, :, 12), + reshape(only(retained).L, :, 12), + reshape(only(retained).G, :, 12), + reshape(only(retained).C, :, 12) + ) + means=vec(mean(sample_matrix; dims = 2)) + centered=sample_matrix .- means + empirical_covariance=centered*transpose(centered)/11 + @test all(isfinite, empirical_covariance) + @test any(!iszero, empirical_covariance) + + repeated=compute(parametric_problem, monte_carlo) + for component in (:R, :L, :G, :C) + @test getproperty(only(samples(repeated)), component) == + getproperty(only(samples(sampled)), component) + end + + total=compute( + ParametricProblem(problem, ComputationOptions((output_basis = :total,))), + monte_carlo + ) + @test basis(only(sampled)) === :pul + @test basis(only(total)) === :total + for component in (:R, :L, :G, :C) + @test getproperty(only(samples(total)), component) ≈ + 1000.0 .* getproperty(only(samples(sampled)), component) + end +end diff --git a/test/unit/engine/internal_precision.jl b/test/unit/engine/internal_precision.jl new file mode 100644 index 000000000..86d01977d --- /dev/null +++ b/test/unit/engine/internal_precision.jl @@ -0,0 +1,63 @@ +@testitem "Engine / internal impedance / radial diffusion implementation" tags=[:unit, :engine, :slow] begin + include(joinpath(pkgdir(LineCableModels),"test/support/radial_control.jl")) + const II=LineCableModels.Engine.InternalImpedance + selected=II.Formula(:default) + for inner in (0.0,.003), f in (10.0,1000.0), T in (Float64,BigFloat) + inputs=(T(inner),T(.005),T(2e-8),one(T),2T(pi)*im*T(f)) + # One reference per represented input serves all three coefficients. + references=map((256,512)) do bits + setprecision(BigFloat,bits) do + ri,ro,rho,mur,s=map(x->x isa Complex ? Complex{BigFloat}(x) : BigFloat(x),inputs) + RadialControl.surfaces(ri,ro,rho,4big(pi)*big"1e-7"*mur,s) + end + end + surfaces=II.surface_impedances(selected,Val((:inner,:outer,:transfer)),inputs...) + @testset "$T $kind ri=$inner f=$f" for kind in (:inner,:outer,:transfer) + actual=getproperty(surfaces,kind) + expected=getproperty(last(references),kind) + bound=last(references).bound+abs(expected-getproperty(first(references),kind)) + @test actual isa Complex{T} + for component in (real,imag) + target=component(expected) + if iszero(target) + @test iszero(component(actual)) + else + budget=1e-6*abs(target) + @test bound<=budget/4 + @test abs(component(actual)-target)+bound<=budget + end + end + end + end + # Derive the approach to DC from the same independently bounded radial + # solution. Exact DC equality is not required at a positive frequency. + previous=Ref(Inf) + for f in (1.0,.1,.01) + reference=setprecision(BigFloat,256) do + RadialControl.surfaces(big"0",big".005",big"2e-8",4big(pi)*big"1e-7",2big(pi)*im*f) + end + value=II.surface_impedances(selected,Val((:outer,)),0.0,.005,2e-8,1.0,2pi*im*f).outer + rdc=2e-8/(pi*.005^2); ldc=4pi*1e-7/(8pi) + remainder=abs(real(reference.outer)-rdc)+abs(imag(reference.outer)/(2pi*f)-ldc) + @test remaindervalue isa ComplexF32,delegated) + @test length(observed.observations)==3 + @test all(record->record[2] === workspace,observed.observations) + @test all(record->record[1] === first(observed.observations)[1],observed.observations) + input=first(observed.observations)[3] + @test (input.r_in,input.r_ex,input.rho,input.mu_r,input.jω) === args32 + + problem=TestFixtures.line_parameters_problem(frequencies=[50.,500.]) + original=compute(problem,Formulation()) + explicit=compute(problem,Formulation(internal_impedance=:default)) + @test Z(explicit)==Z(original) && Y(explicit)==Y(original) + cable_problem=CableConstantsProblem(first(problem.system.designs);frequency=50.) + local_default=compute(cable_problem,CableConstantsFormulation()) + local_explicit=compute(cable_problem,CableConstantsFormulation(internal_impedance=:default)) + @test local_default.R==local_explicit.R && local_default.L==local_explicit.L + changed=compute(problem,Formulation(internal_impedance=M.SurfaceLaw(;coefficients))) + @test Z(changed)!=Z(original) && Y(changed)==Y(original) + @test details(changed).data.formulations.methods.internal_impedance.identifier === :SurfaceLaw + formulations=Formulation(internal_impedance=Grid((formula(:default),custom));combine=:zip) + @test length(formulations)==2 + @test collect(formulations)[2].methods.internal_impedance === custom + native=Formulation(internal_impedance=:default) + for source in (native,MonteCarlo(native),LinearError(native)) + @test occursin("internal Z",only(description([source];quantity=R))) + @test !occursin("internal Z",only(description([source];quantity=B))) + end +end diff --git a/test/unit/engine/internal_shunt.jl b/test/unit/engine/internal_shunt.jl new file mode 100644 index 000000000..d2b987d6c --- /dev/null +++ b/test/unit/engine/internal_shunt.jl @@ -0,0 +1,133 @@ +@testitem "Engine / internal shunt / geometry and local operator" tags=[:unit, :parametric] begin + using LinearAlgebra + E = LineCableModels.Engine + boundary = E.ShuntModel.Formula(:boundary) + include(joinpath(pkgdir(LineCableModels), "test", "support", "internal_shunt.jl")) + design = internal_shunt_test_design() + domain, bp = internal_shunt_test_domain(design) + @test domain.terminals == 1:3 + @test length(domain.wires) == 6 + @test length(domain.tapes) == 1 + @test domain.material.eps_r == 1 + @test only(domain.left).material.eps_r == 3.0 + @test only(domain.right).material.eps_r == 3.0 + renamed = internal_shunt_test_design(suffix = "_renamed") + other, _ = internal_shunt_test_domain(renamed) + @test E.ShuntModel._shunt_domain_equal(domain, other) + values = E.ShuntModel._shunt_values(domain, Formulation().methods) + @test values[1:3] ≈ [domain.a, domain.b, domain.material.eps_r] + host = only(filter( + region->region.primitive isa LineCableModels.DataModel.DifferenceShape && + region.primitive.outer isa Annulus, + design.geometry.regions)) + incomplete = merge((primitive = host.primitive, source = host.source), + (primitive = LineCableModels.DataModel.DifferenceShape(host.primitive.outer, + Base.front(host.primitive.holes)),)) + @test E.ShuntModel._shunt_host_domain(design, bp, incomplete, 1, 0, Float64) === nothing + duplicated = merge(incomplete, + (primitive = LineCableModels.DataModel.DifferenceShape( + host.primitive.outer, ( + first(host.primitive.holes), Base.front(host.primitive.holes)...)),)) + @test E.ShuntModel._shunt_host_domain(design, bp, duplicated, 1, 0, Float64) === nothing + g = @inferred E.ShuntModel._shunt_data(values, domain) + level = (wire = 32, order = 16, quadrature = 128, modes = 256) + result = @inferred E.ShuntModel._shunt_capacitance(boundary, g; level, audit = true) + production = E.ShuntModel._shunt_capacitance(boundary, g; level) + @test production.C == result.C + @test production.diagnostic.boundary_residual === nothing + @test result.C ≈ transpose(result.C) rtol=1e-5 + @test minimum(eigvals(Symmetric(result.C))) > 0 + @test sum(result.C[1, :]) > 0 + @test result.diagnostic.boundary_residual < 0.06 + @test result.state === nothing + @test result.diagnostic.matrix_bytes <= E.ShuntModel.INTERNAL_SHUNT_MATRIX_BYTES + # The explicit budget for the matrix is retained. The inherited warmed + # allocation number had no current performance requirement and is retired. + doubled = merge(g, + (epsilon = 2g.epsilon, + left = [merge(l, (epsilon = 2l.epsilon,)) for l in g.left], + right = [merge(l, (epsilon = 2l.epsilon,)) for l in g.right], + Rleft = g.Rleft/2, Rright = g.Rright/2, Rtotal = g.Rtotal/2)) + @test E.ShuntModel._shunt_capacitance(boundary, doubled; level).C ≈ 2result.C rtol=1e-8 + rotation = 0.317 + rotated = merge(g, + ( + wires = [merge(w, + (x = w.x*cos(rotation)-w.y*sin(rotation), + y = w.x*sin(rotation)+w.y*cos(rotation))) for w in g.wires], + tapes = [merge(t, (phi = t.phi+rotation,)) for t in g.tapes])) + @test E.ShuntModel._shunt_capacitance(boundary, rotated; level).C ≈ result.C rtol=2e-4 + @test_throws BoundarySolveError E.ShuntModel._shunt_capacitance(boundary, g; + level = (wire = 100000, order = 16, quadrature = 128, modes = 1)) + formulation=Formulation(shunt_model = formula(:boundary; options = (resolution = level,))) + blueprints = only(E.flatten(LineCableModelsCoaxial(), [design, renamed], Float64, [formulation])) + @test E.LocalCableData(blueprints).shunt_details.solves == 1 + @test blueprints[1].shunt[1].C === blueprints[2].shunt[1].C + @test isempty(E.flatten(LineCableModelsCoaxial(), design, + Formulation(insulation_admittance = :lossy)).shunt) + cable = E.LocalCableData(first(blueprints)) + p, y = zeros(ComplexF64, 3, 3), zeros(ComplexF64, 3, 3) + layers = zeros(ComplexF64, length(bp.dielectrics)) + coefficients, tails = zeros(ComplexF64, 3), zeros(ComplexF64, 3) + local_shunt = cable.shunt + admittivity = similar(layers) + E.dielectric!(admittivity, cable, Formulation().methods, 50.0, 20.0) + E.cable_potential!(p, cable, admittivity, 100pi*im, + layers, coefficients, tails) + E.cable_admittance!(y, cable, admittivity, 100pi*im, layers) + @test y ≈ 100pi*im*inv(p) rtol=1e-10 + # Warm the measured entry points directly: the enclosing cable calculation + # may inline them without compiling a standalone call. + E._shunt_potential!(p, local_shunt) + E._shunt_admittance!(y, local_shunt, 100pi*im) + @test @allocated(E._shunt_potential!(p, local_shunt)) == 0 + @test @allocated(E._shunt_admittance!(y, local_shunt, 100pi*im)) == 0 +end + +@testitem "Engine / internal shunt / nested shields and reduction" tags=[:unit, :engine] begin + using LinearAlgebra + E = LineCableModels.Engine + # 2 local domains share a closed shield (terminal 3). The outer domain's + # inner-anchor charge includes all terminals enclosed by that shield. + a = (Diagonal([2.0, 3.0])+[1.0, -1.0]*[1.0 -1.0]) .* 1e-9 + b = (Diagonal([5.0, 7.0])+2*[1.0, -1.0]*[1.0 -1.0]) .* 1e-9 + blocks = [E.InternalShuntBlock(1:5, 1:3, a, inv(a)), + E.InternalShuntBlock(1:5, 3:5, b, inv(b))] + # Outermost jacket to the external reference remains a radial contribution. + p = fill(inv(7e-9), 5, 5) + y = zeros(ComplexF64, 5, 5) + s = 100pi*im + y[5, 5] = s*7e-9 + E._shunt_potential!(p, blocks) + E._shunt_admittance!(y, blocks, s) + @test y ≈ s*inv(p) rtol=1e-12 + # Bundling terminals sums their charges and imposes equal potentials. + incidence = [1 0 0; 0 1 0; 0 1 0; 0 0 1; 0 0 1] + bundled = transpose(incidence)*y*incidence + @test bundled ≈ transpose(bundled) + @test minimum(eigvals(Symmetric(imag.(bundled)))) > 0 +end + +@testitem "Engine / internal shunt / multiple independent open terminals" tags=[:unit, :parametric] begin + using LinearAlgebra + E = LineCableModels.Engine + boundary = E.ShuntModel.Formula(:boundary) + include(joinpath(pkgdir(LineCableModels), "test", "support", "internal_shunt.jl")) + original, _ = internal_shunt_test_domain(internal_shunt_test_design(tapes = false)) + # Independent test of the numerical terminal map, not permission to bypass + # the existing public coaxial lowering rules for radial conductor ordering. + wires = [merge(wire, (terminal = iseven(index) ? 2 : 3,)) + for (index, wire) in pairs(original.wires)] + domain = E.ShuntModel.InternalShuntDomain( + original.design, 1:4, 1:4, original.a, original.b, + original.material, original.left, original.right, wires, original.tapes) + @test domain.terminals == 1:4 + @test sort(unique(getproperty.(domain.wires, :terminal))) == [2, 3] + g = E.ShuntModel._shunt_data(E.ShuntModel._shunt_values(domain, Formulation().methods), domain) + result = E.ShuntModel._shunt_capacitance(boundary, g; level = ( + wire = 32, order = 16, quadrature = 128, modes = 256)) + @test size(result.C) == (3, 3) + @test result.C ≈ transpose(result.C) rtol=1e-6 + @test result.C[2, 3] < 0 + @test minimum(eigvals(Symmetric(result.C))) > 0 +end diff --git a/test/unit/engine/internal_shunt_math.jl b/test/unit/engine/internal_shunt_math.jl new file mode 100644 index 000000000..2073f7bf7 --- /dev/null +++ b/test/unit/engine/internal_shunt_math.jl @@ -0,0 +1,133 @@ +@testitem "Engine / internal shunt / independent Green and integrated-face controls" tags=[:unit, :engine] begin + using LinearAlgebra + E = LineCableModels.Engine + boundary = E.ShuntModel.Formula(:boundary) + # Independent Dirichlet-annulus separated solution, not the reflection form. + h = (; a = 1.0, b = 3.0, epsilon = 1.0, Rleft = 0.0, Rright = 0.0, + Rtotal = log(3.0), left = NamedTuple[], right = NamedTuple[]) + k = E.ShuntModel._shunt_kernel_coefficients(h, 256) + @testset "Local Green function controls" begin + z, s=1.4cis(0.3), 2.2cis(1.2) + x, y, L=log(abs(z)), log(abs(s)), log(3.0) + resolved=false + for order in (32, 64, 128, 256, 512) + exact=min(x, y)*(L-max(x, y))/L + magnitude=abs(exact) + for m in 1:order + term=2sinh(m*min(x, y))*sinh(m*(L-max(x, y))) / + (m*sinh(m*L))*cos(m*(angle(z)-angle(s))) + exact+=term; + magnitude+=abs(term) + end + ratio=exp(-abs(y-x)) + tail=ratio^(order+1)/((order+1)*(1-ratio)*(1-exp(-2L))) + coefficients=E.ShuntModel._shunt_kernel_coefficients(h, order) + actual=E.ShuntModel._shunt_kernel(z, s, h, coefficients) + budget=1e-10abs(exact) + uncertainty=tail+(2order+20)*eps(Float64)*magnitude + if uncertainty<=budget/4 + @test abs(actual-exact)+uncertainty<=budget + resolved=true + end + @test E.ShuntModel._shunt_kernel(z, s, h, coefficients) ≈ + E.ShuntModel._shunt_kernel(s, z, h, coefficients) atol=1e-12 + @test abs(E.ShuntModel._shunt_kernel(cis(0.3), s, h, coefficients))<1e-12 + @test abs(E.ShuntModel._shunt_kernel(3cis(0.3), s, h, coefficients))<1e-12 + end + @test resolved + h2 = merge(h, (epsilon = 2.0, Rtotal = log(3.0)/2)) + @test E.ShuntModel._shunt_kernel(1.4cis(0.2), 2.1cis(1.1), h2, + E.ShuntModel._shunt_kernel_coefficients(h2, 256)) ≈ + E.ShuntModel._shunt_kernel(1.4cis(0.2), 2.1cis(1.1), h, k)/2 + targets, sources = [1.3cis(0.2), 1.4cis(2.1)], [2.1cis(1.1), 2.5cis(-0.4)] + @test E.ShuntModel._shunt_kernel_matrix(boundary, targets, sources, h, k) ≈ + [E.ShuntModel._shunt_kernel(z, s, h, k) for z in targets, s in sources] + regular = E.ShuntModel._shunt_kernel_matrix(boundary, targets, sources, h, k; regular = true) + @test regular ≈ [E.ShuntModel._shunt_kernel(z, s, h, k; regular = true) + for z in targets, s in sources] + @test E.ShuntModel._shunt_kernel_matrix(boundary, targets, sources, h, k) ≈ + regular-[log(abs(z-s)) for z in targets, s in sources] + @test isfinite(E.ShuntModel._shunt_kernel( + 2cis(0.3), 2cis(0.3), h, k; regular = true)) + layers = [(ri = 1.0, ro = 1.2, epsilon = 3.0), (ri = 1.2, ro = 2.0, epsilon = 3.0)] + @test E.ShuntModel._shunt_load(layers, 5) ≈ + E.ShuntModel._shunt_load([(ri = 1.0, ro = 2.0, epsilon = 3.0)], 5) + @test E.ShuntModel._shunt_load(layers, 1e-8) ≈ 3/log(2) rtol=1e-12 + + # End-to-end charge extraction control: a complete metal annulus must + # recover the two exact coaxial capacitances and perfect shielding. + theta = 2pi .* (0:255) ./ 256 + targets = [1.8cis.(theta); 2.2cis.(theta)] + sources = [1.98cis.(theta); 2.02cis.(theta)] + kernel = E.ShuntModel._shunt_kernel_matrix(boundary, targets, sources, h, k) + q = kernel \ + hcat(-E.ShuntModel._shunt_core_voltage.(targets, Ref(h)), ones(length(targets))) + screen = vec(sum(q; dims = 1)) + core = [1/h.Rtotal, 0.0] - + vec(transpose(E.ShuntModel._shunt_core_voltage.(sources, Ref(h)))*q) + capacitance = vcat(transpose(core), transpose(screen)) + left, right = 1/log(1.8), 1/log(3/2.2) + @test capacitance ≈ [left -left; -left left+right] rtol=1e-9 + end + @testset "Four-face tape controls" begin + @test E.ShuntModel._shunt_junction_exponent(1.0, 1.0) ≈ 2/3 + @test E.ShuntModel._shunt_junction_exponent(1.0, 32.3) ≈ + 2/pi*acos(sqrt(32.3/(2*33.3))) + @test 0.5 < E.ShuntModel._shunt_junction_exponent(1.0, 32.3) < 2/3 + for (alpha, beta) in ((0.0, 0.0), (-1/3, -1/3), (-0.49, -1/3)), + order in (16, 32, 64, 128) + + nodes, weights = E.ShuntModel._shunt_gauss_jacobi(order, alpha, beta) + values = reduce(hcat, [E.ShuntModel._shunt_jacobi(t, alpha, beta, 8) + for t in nodes]) + @test sum(weights) ≈ 1 + @test all(weights .> 0) + @test dot(nodes, weights) ≈ (beta-alpha)/(alpha+beta+2) atol=1e-14 + @test values*weights ≈ [1.0; zeros(8)] atol=1e-13 + end + nodes, weights = E.ShuntModel._shunt_gauss_jacobi(64, 0.0, 0.0) + values = transpose(reduce(hcat, [E.ShuntModel._shunt_jacobi(t, 0.0, 0.0, 3) + for t in nodes])) + face = (; kind = :end, radius = 0.0, phi = 0.0, span = 0.0, mid = 2.0, half = 0.1, + alpha = 0.0, beta = 0.0, p = 3, nodes, weights, beta_norm = 1.0, + weighted = weights .* values, points = complex.(2 .+ 0.1 .* nodes)) + moments, _ = E.ShuntModel._shunt_log_moments(2.0+0im, face) + @test moments ≈ [log(0.1)-1, 0, 1/3, 0] atol=2e-10 + endpoint, _ = E.ShuntModel._shunt_log_moments(2.1+0im, face) + @test endpoint[1] ≈ log(0.2)-1 atol=2e-10 + for distance in (0.1, 1e-5) + near, _ = E.ShuntModel._shunt_log_moments(2.0+im*distance, face) + exact = log(hypot(0.1, distance))-1+distance/0.1*atan(0.1/distance) + @test near[1] ≈ exact atol=2e-10 + end + h = (; a = 1.0, b = 3.0, epsilon = 1.0, Rleft = 0.0, Rright = log(4/3)/4, + Rtotal = log(3)+log(4/3)/4, left = NamedTuple[], + right = [(ri = 3.0, ro = 4.0, epsilon = 4.0)]) + k = E.ShuntModel._shunt_kernel_coefficients(h, 512) + for (z, s) in ((1.3cis(0.2), 2.1cis(1.1)), (3cis(0.2), 2.5cis(-0.4))) + split = E.ShuntModel._shunt_kernel(z, + s, + h, + k; + split_images = true)- + (log(abs(z-s))+k.ra0*log(abs(h.a^2/conj(z)-s))+ + k.rb0*log(abs(h.b^2/conj(z)-s)))/h.epsilon + @test split ≈ E.ShuntModel._shunt_kernel(z, s, h, k) atol=2e-14 + end + # Direct and coincident nearest image produce the interface coefficient. + difference = E.ShuntModel._shunt_kernel(3.0+0im, 3cis(1e-7), h, k) - + E.ShuntModel._shunt_kernel(3.0+0im, 3cis(2e-7), h, k) + @test difference ≈ (2/5)*log(2) rtol=1e-5 + # The charge measure contains no extra face-length Jacobian. Scaling all + # radii and target-source positions leaves the potential columns unchanged. + targets = [1.4cis(0.1), 2.5cis(0.3)] + original = E.ShuntModel._shunt_tape_columns(boundary, targets, [face], h, k).columns + scaled_h = merge(h, (a = 10h.a, b = 10h.b, + right = [(ri = 30.0, ro = 40.0, epsilon = 4.0)])) + scaled_face = merge(face, ( + mid = 10face.mid, half = 10face.half, points = 10face.points)) + scaled = E.ShuntModel._shunt_tape_columns(boundary, 10targets, [scaled_face], scaled_h, + E.ShuntModel._shunt_kernel_coefficients(scaled_h, 512)).columns + @test scaled ≈ original atol=1e-12 + end +end diff --git a/test/unit/engine/line_observations.jl b/test/unit/engine/line_observations.jl new file mode 100644 index 000000000..61bbe33f3 --- /dev/null +++ b/test/unit/engine/line_observations.jl @@ -0,0 +1,46 @@ +@testitem "Engine / observations / complex, diagonal, and frequency selections" tags=[:unit, :engine] begin + using LinearAlgebra: diag + frequency = [0.1, 50.0, 1e6] + order = reshape(collect(1.0:12.0), 2, 2, 3) + resistance = order .* 1e-4 + inductance = order .* 1e-7 + conductance = order .* 1e-9 + capacitance = order .* 1e-10 + omega = reshape(2pi .* frequency, 1, 1, :) + impedance = resistance .+ im .* omega .* inductance + admittance = conductance .+ im .* omega .* capacitance + + for result_basis in (:pul, :total) + line = LineParameters(copy(impedance), copy(admittance), frequency; basis=result_basis) + series, shunt = line.Z, line.Y + @test basis(typeof(line)) === result_basis + @test basis(typeof(series)) === result_basis + @test basis(typeof(shunt)) === result_basis + for (selector, standalone, values) in ( + (Z, series, impedance), (R, series, resistance), + (X, series, omega .* inductance), (L, series, inductance), + (Y, shunt, admittance), (G, shunt, conductance), + (B, shunt, omega .* capacitance), (C, shunt, capacitance)) + @test (selector, diag) in observables(typeof(line)) + @test (selector, diag) in observables(typeof(standalone)) + expected = [values[row, row, sample] for row in 1:2, sample in 1:3] + @test @inferred(observe(line, selector, diag)) ≈ expected + @test @inferred(observe(line, selector, diag, [2, 1], [3, 1])) ≈ expected[[2, 1], [3, 1]] + @test @inferred(observe(line, selector, diag, 2, 3)) ≈ expected[2, 3] + @test observe(line, selector, 2, 1) ≈ values[2, 1, :] + arguments = selector in (L, C) ? (frequency,) : () + @test observe(standalone, selector, diag, arguments...) ≈ expected + @test observe(standalone, selector, diag, arguments..., [2, 1], [3, 1]) ≈ expected[[2, 1], [3, 1]] + end + for (selector, standalone, values) in ((Z, series, impedance), (Y, shunt, admittance)), + transform in (abs, angle) + expected = transform.(values) + @test @inferred(observe(line, selector, transform)) ≈ expected + @test @inferred(observe(standalone, selector, transform, 2, 1, [3, 1])) ≈ expected[2, 1, [3, 1]] + end + @test C(line, 2, 1) ≈ capacitance[2, 1, :] + @test Z(line) == impedance + @test Y(line) == admittance + @test frequencies(line) == frequency + end +end diff --git a/test/unit/engine/modal_tracking.jl b/test/unit/engine/modal_tracking.jl new file mode 100644 index 000000000..06b4ee6a6 --- /dev/null +++ b/test/unit/engine/modal_tracking.jl @@ -0,0 +1,115 @@ +@testitem "ModalAnalysis / repeated eigenvalues preserve the modal subspace" tags=[:unit, :engine] begin + using LinearAlgebra + + frequencies=[50.0,55.0,60.0,65.0,70.0] + impedance=zeros(ComplexF64,3,3,length(frequencies)) + admittance=similar(impedance) + principal_axes=Matrix{Float64}[] + for (index,angle) in enumerate(range(0.0,0.3;length=length(frequencies))) + rotation=[cos(angle) 0 sin(angle);0 1 0;-sin(angle) 0 cos(angle)] + push!(principal_axes,rotation) + impedance[:,:,index]=(1+2im).*(rotation*Diagonal([1.0,1.0,5.0])*transpose(rotation)) + admittance[:,:,index]=(1e-8+4e-7im).*Matrix(I,3,3) + end + phase=LineParameters(impedance,admittance,frequencies) + modal=compute(ModalAnalysisProblem(phase),ModalAnalysisFormulation(:default)) + maps=operators(modal) + for index in eachindex(frequencies) + vectors=maps.Ti[:,:,index] + product=admittance[:,:,index]*impedance[:,:,index] + eigenvalues=gamma(modal)[:,index].^2 + @test norm(product*vectors-vectors*Diagonal(eigenvalues))<= + 1e-8*norm(product)*norm(vectors) + for mode in 1:3 + paired=impedance[:,:,index]*vectors[:,mode] + @test maps.Tv[:,mode,index]≈paired/norm(paired) + end + target=(1e-8+4e-7im)*(1+2im) + repeated=findall(value->abs(value-target)<=1e-8*abs(target),eigenvalues) + @test length(repeated)==2 + actual_basis=vectors[:,repeated] + actual_projector=actual_basis*pinv(actual_basis) + expected_basis=principal_axes[index][:,1:2] + @test actual_projector≈expected_basis*transpose(expected_basis) rtol=1e-8 + end + rebuilt=LineCableModels.ModalAnalysis.transform(PhaseDomain,modal) + @test rebuilt.Z.values≈impedance rtol=1e-12 + @test rebuilt.Y.values≈admittance rtol=1e-12 + @test phase.Z.values==impedance + @test phase.Y.values==admittance + + for selector in (2,2:4,[5,2],:) + indices=selector isa Integer ? (selector:selector) : selector + selected=modal[selector] + selected_maps=operators(selected) + @test selected.f==frequencies[indices] + @test selected.Z.values==modal.Z.values[:,:,indices] + @test selected.Y.values==modal.Y.values[:,:,indices] + @test selected_maps.Tv==maps.Tv[:,:,indices] + @test selected_maps.Ti==maps.Ti[:,:,indices] + @test gamma(selected)==gamma(modal)[:,indices] + selected_phase=LineCableModels.ModalAnalysis.transform(PhaseDomain,selected) + @test selected_phase.Z.values≈impedance[:,:,indices] rtol=1e-12 + @test selected_phase.Y.values≈admittance[:,:,indices] rtol=1e-12 + segment=PropagationParameters(modal;line_length=25.0) + selected_segment=segment[selector] + @test selected_segment.parameters.f==selected.f + @test gamma(selected_segment)==gamma(selected) + @test H(selected_segment)==exp.(-gamma(selected).*25.0) + end + selected=modal[2:4] + voltage_before=copy(maps.Tv) + current_before=copy(maps.Ti) + operators(selected).Tv[1,1,1]+=1 + operators(selected).Ti[1,1,1]+=1 + @test maps.Tv==voltage_before + @test maps.Ti==current_before +end + +@testitem "ModalAnalysis / limited iteration reports matched and unrecovered results" tags=[:unit, :engine] begin + using LinearAlgebra + frequencies=[50.0,100.0,200.0,400.0] + impedance=zeros(ComplexF64,2,2,length(frequencies)) + admittance=similar(impedance) + for (index,angle) in enumerate((0.0,0.4,0.8,1.2)) + rotation=[cos(angle) -sin(angle);sin(angle) cos(angle)] + impedance[:,:,index]=rotation*Diagonal(ComplexF64[ + 1+index*im,3+2index*im])*transpose(rotation) + admittance[:,:,index]=rotation*Diagonal(ComplexF64[ + 1e-8+index*1e-7im,2e-8+index*3e-7im])*transpose(rotation) + end + phase=LineParameters(impedance,admittance,frequencies) + matched=compute(ModalAnalysisProblem(phase),ModalAnalysisFormulation(:default; + options=(iteration=(max_iterations=1,),))) + diagnostic=details(matched).data.modal.diagnostics + @test !isempty(diagnostic.fallback_frequencies) + @test diagnostic.fallback_frequencies==diagnostic.missed_frequencies + @test size(diagnostic.eigen_residual)==(2,length(frequencies)) + @test all(isfinite,diagnostic.eigen_residual) + @test all(value -> value===nothing || value isa Bool,diagnostic.converged) + @test all(value -> value===nothing || value>=0,diagnostic.iterations) + for index in eachindex(frequencies) + vectors=Ti(matched)[:,:,index] + product=admittance[:,:,index]*impedance[:,:,index] + eigenvalues=gamma(matched)[:,index].^2 + @test norm(product*vectors-vectors*Diagonal(eigenvalues))<= + 1e-9*norm(product)*norm(vectors) + @test all(isfinite,matched.Z.values[:,:,index]) + @test all(isfinite,matched.Y.values[:,:,index]) + end + rebuilt=LineCableModels.ModalAnalysis.transform(PhaseDomain,matched) + @test rebuilt.Z.values≈impedance rtol=1e-12 + @test rebuilt.Y.values≈admittance rtol=1e-12 + unassisted=compute(ModalAnalysisProblem(phase),ModalAnalysisFormulation(:default; + options=(iteration=(max_iterations=1,fallback=:none),))) + @test !isempty(details(unassisted).data.modal.diagnostics.missed_frequencies) + @test isempty(details(unassisted).data.modal.diagnostics.fallback_frequencies) + @test all(isfinite,unassisted.Z.values) + @test all(isfinite,unassisted.Y.values) + selected=unassisted[[4,2]] + @test details(selected).data.modal.diagnostics.missed_frequencies==[1,2] + @test details(selected).data.modal.diagnostics.z_coupling== + details(unassisted).data.modal.diagnostics.z_coupling[[4,2]] + @test details(selected).data.modal.diagnostics.eigen_residual== + details(unassisted).data.modal.diagnostics.eigen_residual[:,[4,2]] +end diff --git a/test/unit/engine/observable_resolution.jl b/test/unit/engine/observable_resolution.jl new file mode 100644 index 000000000..fe68d9fd9 --- /dev/null +++ b/test/unit/engine/observable_resolution.jl @@ -0,0 +1,144 @@ +@testitem "Engine / physical cutoffs shared by observations and comparisons" tags=[:unit, :engine, :slow] begin + using LinearAlgebra: diag + using LineCableModels.Engine: compare + const GR=LineCableModels.Commons + f=[1.,1e3,1e7] + z=fill(1.0+im,2,2,3) + y=fill(1e-13+1e-18im,2,2,3) + y[2,2,:].=1e-4 .+ 2π.*f.*1e-9im + source=LineParameters(z,y,f) + saved=deepcopy((z,y,f)) + selected=((G,1,1,:),(B,1,1,:)) + a=ObservedResult(source,selected;length_unit=:base) + b=ObservedResult(source,selected;length_unit=:kilo) + @test all(iszero,observe(a,G)) + @test observe(b,G)==1000observe(a,G) + @test observe(ObservedResult(source,selected;clip=false,length_unit=:base),G)==real.(y[1,1,:]) + @test all(iszero,observe(a,B)) + polar=ObservedResult(source,((Y,abs,1,1,:),(Y,angle,1,1,:))) + @test all(iszero,observe(polar,Y,abs)) + @test all(ismissing,observe(polar,Y,angle)) + @test observe(ObservedResult(source,selected;atol=(G=0.,B=0.),length_unit=:base),G)==real.(y[1,1,:]) + @test observe(ObservedResult(source),G)[1,1,:]==observe(b,G) + diagonal=ObservedResult(source,((G,diag,:,:),(B,diag,:,:));length_unit=:base) + @test observe(diagonal,G,diag)[1,:]==observe(a,G) + @test observe(a,G,1,1,2:3)==observe(a,G)[2:3] + product=GR.observation_product(a,G) + @test product.thresholds.kind===:declared_floor + @test count(product.engineering_zero)==3 + @test_throws ArgumentError ObservedResult(source,selected;atol=1e-12) + @test_throws ArgumentError ObservedResult(source,selected;atol=(oops=0.,)) + for q in (R,X,L,G,B,C,Z,Y) + comparison=compare(source,source,q) + @test comparison.details.data.resolution.unit==LineCableModels.Units.native_unit(q,:pul) + end + @test compare(source,source,B).details.data.atol≈2π.*f.*compare(source,source,C).details.data.atol + @test compare(source,source,X).details.data.atol≈2π.*f.*compare(source,source,L).details.data.atol + @test (z,y,f)==saved +end + +@testitem "Engine / cutoff boundaries and unassessed components" tags=[:unit, :engine, :slow] begin + using LineCableModels.Engine: compare + const GR=LineCableModels.Commons + for T in (Float32,Float64,BigFloat) + cutoff=T(1e-12) + f=T[1,1e3,1e7] + g=reshape(T[cutoff/2,cutoff,2cutoff],1,1,:) + z=fill(complex(one(T),one(T)),1,1,3) + y=complex.(g,zero(T)) + a=LineParameters(z,y,f);b=LineParameters(z,2y,f) + base=observe(ObservedResult(a;length_unit=:base,atol=(G=cutoff,)),G) + @test eltype(base)===T + @test vec(base)==T[0,0,2cutoff] + @test observe(ObservedResult(a;atol=(G=cutoff,)),G)≈1000base + for normalization in (:reference_rms,:pointwise), (left,right) in ((a,b),(b,a)) + error=compare(left,right,G;normalization,atol=cutoff) + @test ismissing(only(error.relative)) + @test ismissing(only(error.absolute)) + @test Base.nonmissingtype(eltype(error.absolute))===T + @test only(error.details.data.unresolved_samples).reference>0 + @test only(error.details.data.unresolved_samples).result>0 + end + @test !ismissing(only(compare(a,b,G;band=:wide,atol=cutoff).relative)) + total=LineParameters(10z,10y,f;basis=:total) + @test_throws ArgumentError ObservedResult(total) + @test observe(ObservedResult(total;atol=(R=0.,X=0.,G=10cutoff,B=0.)),G)≈10base + @test compare(a,b,G;atol=0).relative≈fill(one(T),1,1) + small=fill(T(1e-30),1,1,3) + @test only(compare(small,2small).absolute)>0 + @test only(compare(small,2small).relative)≈one(T) + end + f=[1.,10.,100.] + z=fill(1.0+im,2,2,3);y=fill(2e-12+1e-6im,2,2,3) + a=LineParameters(z,copy(y),f);b=LineParameters(z,copy(y),f) + b.Y.values[2,2,:].=1e20 + @test observe(ObservedResult(a),G,1,1,:)==observe(ObservedResult(b),G,1,1,:) + standalone=ShuntAdmittance(fill(1e-18im,2,2,3)) + raw=ObservedResult(standalone;length_unit=:base) + @test observe(raw,B)==fill(1e-18,2,2,3) + @test GR.observation_product(raw,B).thresholds.kind===:unassessed + @test all(iszero,observe(ObservedResult(standalone;frequencies=f),B)) + @test all(iszero,observe(ObservedResult(standalone;atol=(B=1e-12,)),B)) + @test_throws ArgumentError ObservedResult(a;frequencies=[1.,2.,3.]) + @test_throws DimensionMismatch ObservedResult(standalone;frequencies=[1.]) + for invalid in (-1,Inf,NaN,true,"bad",(G=-1.,)) + @test_throws ArgumentError ObservedResult(a;atol=invalid) + end + a.Y.values[1,1,1]=NaN+im + @test ismissing(observe(ObservedResult(a),G,1,1,1)) + @test ismissing(compare(a,b,G).absolute[1,1]) + @test !ismissing(compare(a,b,G;band=(10.,100.)).absolute[1,1]) + dc=ObservedResult(LineParameters(z,y,[0.,10.,100.]),(R,L,G,C)) + @test ismissing(observe(dc,C,1,1,1)) + @test all(isfinite,observe(dc,C,1,1,2:3)) + single=LineParameters(ones(ComplexF32,1,1,3),fill(ComplexF32(1e-6),1,1,3),f) + double=LineParameters(ones(ComplexF64,1,1,3),fill(complex((Float64(Float32(1e-12))+1e-12)/2),1,1,3),f) + @test all(iszero,observe(ObservedResult(double),G)) + @test ismissing(only(compare(single,double,G).relative)) + @test ismissing(only(compare(double,single,G).relative)) + @test all(iszero,observe(ObservedResult(single;atol=(G=1e100,)),G)) +end + +@testitem "UQ / clipping zeros uncertainty only for unresolved values" tags=[:extension, :measurements, :slow] begin + using Measurements + using LineCableModels.Commons: detach,observation_resolution + summary=LineCableModels.UQ.SampleSummary([1e-18,2e-18,3e-18]) + detached=detach(summary,1000.) + @test detached.std==1000summary.std>0 + @test detached.min<=detached.q05<=detached.median<=detached.q95<=detached.max + @test detached.mean==1000summary.mean + @test detached.n==summary.n + for T in (Float32,Float64,BigFloat) + cutoff=T(1e-12) + original=measurement.(T[cutoff/2,cutoff/2,cutoff,2cutoff],T[cutoff/2,2cutoff,cutoff/2,2cutoff]) + y=reshape(complex.(original,zero.(original)),1,1,:) + source=LineParameters(copy(y),y,T[1,10,100,1000]) + for length_unit in (:base,:kilo) + factor=length_unit===:base ? T(1) : T(1000) + observed=ObservedResult(source;length_unit,atol=(G=cutoff,)) + values=vec(observe(observed,G)) + @test eltype(values)===eltype(original) + @test Measurements.value.(values)≈factor.*T[0,0,0,2cutoff] + @test Measurements.uncertainty.(values)≈factor.*T[0,0,0,2cutoff] + @test all(iszero,Measurements.value.(values[1:3])) + @test all(iszero,Measurements.uncertainty.(values[1:3])) + @test iszero(Measurements.uncertainty(values[4]-factor*original[4])) + @test Measurements.uncertainty.(real.(vec(Y(source))))==Measurements.uncertainty.(original) + raw=vec(observe(ObservedResult(source;length_unit,clip=false),G)) + @test Measurements.value.(raw)==factor.*Measurements.value.(original) + @test all(iszero,Measurements.uncertainty.(raw.-factor.*original)) + end + end + value=complex(measurement(1e-18,2e-12),measurement(1e-18,3e-12)) + resolution=observation_resolution(value,Y;atol=(G=1e-12,B=1e-12)) + @test resolution.unresolved && resolution.available + observed=ObservedResult(LineParameters(fill(value,1,1,1),fill(value,1,1,1),[1.]), + ((Y,abs),(Y,angle));atol=(G=1e-12,B=1e-12),length_unit=:base) + @test ismissing(only(observe(observed,Y,angle))) + @test nominal(only(observe(observed,Y,abs)))==0 + @test iszero(uncertainty(only(observe(observed,Y,abs)))) + for value in (NaN,Inf,missing) + r=observation_resolution(Union{Missing,Float64}[value],G) + @test !only(r.unresolved) && !only(r.available) + end +end diff --git a/test/unit/engine/observations.jl b/test/unit/engine/observations.jl new file mode 100644 index 000000000..1a694eeba --- /dev/null +++ b/test/unit/engine/observations.jl @@ -0,0 +1,118 @@ +@testitem "Engine / observations / native selectors and detached publication" tags=[:unit, :uq] setup=[ + UseEngineSupport +] begin + using LinearAlgebra: diag + using Statistics: mean + + const U=LineCableModels.Units + + frequency=[50.0, 100.0] + resistance=reshape([1.0, 2.0], 1, 1, 2) .* 1.0e-4 + inductance=reshape([3.0, 4.0], 1, 1, 2) .* 1.0e-7 + conductance=reshape([5.0, 6.0], 1, 1, 2) .* 1.0e-8 + capacitance=reshape([7.0, 8.0], 1, 1, 2) .* 1.0e-10 + angular=reshape(2π .* frequency, 1, 1, :) + impedance=complex.(resistance, inductance .* angular) + admittance=complex.(conductance, capacitance .* angular) + parameters=LineParameters(impedance, admittance, frequency) + + @test observables(typeof(parameters)) == ( + frequencies, Z, Y, R, X, L, G, B, C, + (Z, abs), (Z, angle), (Y, abs), (Y, angle), + (Z, diag), (Y, diag), (R, diag), (X, diag), + (L, diag), (G, diag), (B, diag), (C, diag) + ) + @test @inferred(observe(parameters, Z, 1, 1, 1)) === impedance[1, 1, 1] + @test @inferred(observe(parameters, R, 1, 1, 1)) === resistance[1, 1, 1] + @test @inferred(observe(parameters, L, 1, 1, 1)) ≈ inductance[1, 1, 1] + @test @observe(R[1, 1, :]) == (R, 1, 1, Colon()) + @test @observe((Z, abs)[:, :, :]) == + (Z, abs, Colon(), Colon(), Colon()) + @test @observe((Z, angle)[:, :, :]) == + (Z, angle, Colon(), Colon(), Colon()) + @test LineCableModels.Commons.request_identity(@observe(R[:, :, :])) === R + @test LineCableModels.Commons.request_indices(@observe(R[:, :, :])) == + (Colon(), Colon(), Colon()) + @test LineCableModels.Commons.request_identity(@observe((Z, abs)[:, :, :])) == + (Z, abs) + @test @observe(parameters, Z[1, 1, :]) == impedance[1, 1, :] + @test @observe(parameters, (Z, abs)[1, 1, :]) == abs.(impedance[1, 1, :]) + @test @observe(parameters, (Z, angle)[1, 1, :]) == angle.(impedance[1, 1, :]) + i, j, samples=1, 1, 1:2 + @test @observe(parameters, L[i, j, samples]) ≈ inductance[1, 1, :] + @test @observe(parameters, Z[1, 1]) == impedance[1, 1, :] + @test @observe((Z, diag)[:, :]) == (Z, diag, Colon(), Colon()) + # 3 selectors include owner-defined statistical requests. The macro + # preserves them. Whether a result supports the request belongs to dispatch. + @test @observe((statistics, R, mean)[1, 1]) == (statistics, R, mean, 1, 1) + @test_throws ArgumentError macroexpand( + @__MODULE__, + :(@observe (Z, abs, angle, identity)[1, 1, :]) + ) + observe(parameters, Z, 1, 1, 1) + observe(parameters, R, 1, 1, 1) + @test @allocated(observe(parameters, Z, 1, 1, 1)) == 0 + @test @allocated(observe(parameters, R, 1, 1, 1)) == 0 + @test Z(parameters, 1, 1, 1) === observe(parameters, Z, 1, 1, 1) + @test L(parameters, 1, 1, :) ≈ observe(parameters, L, 1, 1, :) + + requests=((R,1,1,:),(X,1,1,:)) + target=U.units(:milli,:ohm;per=(:kilo,:meter)) + observed=observables(parameters,requests;quantity_units=(R=target,)) + @test observed isa ObservedResult + @test observe(observed,R)≈1.0e6.*resistance[1,1,:] + @test first(observed.quantities).unit==target + polar=ObservedResult(parameters,((Z,abs,1,1,:),(Z,angle,1,1,:))) + @test observe(polar,Z,angle)≈rad2deg.(angle.(impedance[1,1,:])) + prefixed=ObservedResult(parameters,requests;quantity_units=(R=:micro,)) + @test first(prefixed.quantities).unit==U.units(:micro,:ohm;per=(:kilo,:meter)) + @test LineCableModels.Commons.validate_observables(parameters,requests)==(R,X) + @test Base.ispublic(LineCableModels.Commons,:validate_observables) + @test !isdefined(LineCableModels,:validate_observables) + first(observed.quantities).values[1]=0 + first(observed.quantities).coordinates.frequencies[1]=0 + @test frequencies(parameters)[1]==50 + @test R(parameters,1,1,1)==resistance[1,1,1] + @test observables(parameters) isa ObservedResult + @test_throws MethodError observables(parameters,Dict(:resistance=>R)) + @test_throws MethodError observables(parameters,(invalid=identity,)) + @test_throws DimensionMismatch observables(parameters,requests;units=(target,)) + @test_throws ArgumentError observables(parameters,requests;quantity_units=(R=U.units(:base,:farad),)) + @test LineCableModels.Commons.detach(eps(Float64)/2,1.0)==eps(Float64)/2 + + series=SeriesImpedance(impedance) + shunt=ShuntAdmittance(admittance) + @test observe(series, Z) === series.values + @test observe(series, Z, abs, 1, 1, :) == abs.(impedance[1, 1, :]) + @test observe(series, L, frequency) ≈ inductance + @test_throws DimensionMismatch observe(series, L, [50.0]) + @test_throws DomainError observe(series, L, [0.0, 100.0]) + @test observe(shunt, Y) === shunt.values + @test observe(shunt, Y, angle, 1, 1, :) == angle.(admittance[1, 1, :]) + @test observe(shunt, C, frequency) ≈ capacitance + @test_throws DimensionMismatch observe(shunt, C, [50.0]) + @test_throws DomainError observe(shunt, C, [50.0, 0.0]) +end + +@testitem "Engine / observations / detached records and explicit tables" tags=[:unit, :report] begin + using DataFrames, Tables + using LineCableModels.ReportBuilder: tabulate + const GR=LineCableModels.Commons + constants=CableConstants([:a,:b],[1e-4,2e-4],[3e-7,4e-7],[5e-10,6e-10],[7e-9,8e-9],50.) + observed=observables(constants;length_unit=:base,quantity_units=:base) + @test !Tables.istable(typeof(observed)) + @test length(observed.quantities)==4 + @test collect(tabulate(observed,R)[1,2:end])==constants.R + @test collect(tabulate(observed,L)[1,2:end])==constants.L + @test names(tabulate(observed,R))==["frequency","a","b"] + table=tabulate(observed,R) + table[1,:a]=99 + @test constants.R==[1e-4,2e-4] + @test observe(observed,R)==constants.R + @test_throws r"tabulate" DataFrame(observed) + @test sprint(show,MIME"text/plain"(),observed)==sprint(show,observed) + @test occursin("4 quantities",sprint(show,observed)) + @test_throws MethodError iterate(observed) + nullable=Union{Missing,Float64}[missing,eps()/4,Inf,-Inf,NaN,1.] + @test isequal(GR.detach(nullable,2.0),[missing,eps()/2,Inf,-Inf,NaN,2.]) +end diff --git a/test/unit/engine/observed_foundation.jl b/test/unit/engine/observed_foundation.jl new file mode 100644 index 000000000..71fc33b07 --- /dev/null +++ b/test/unit/engine/observed_foundation.jl @@ -0,0 +1,119 @@ +@testitem "ObservedResult / operand eligibility precedes RMS" tags=[:unit, :engine, :slow] begin + using LineCableModels.Engine: compare + using LineCableModels.Commons: observation_resolution + using LinearAlgebra: norm + for T in (Float32,Float64,BigFloat) + cutoff=T(1//10^10) + frequency=T[1,10,100] + resistance=reshape(T[-2cutoff,-cutoff,zero(T)],1,1,3) + classified=observation_resolution(resistance,R;atol=(R=cutoff,),frequencies=frequency) + @test vec(classified.unresolved)==[false,true,true] + @test all(classified.available) + @test all(observation_resolution(zero.(resistance),R;atol=0).unresolved) + a=LineParameters(PhaseDomain,complex.(resistance,one(T)),fill(complex(one(T),one(T)),1,1,3),frequency) + b=LineParameters(PhaseDomain,complex.(2resistance,one(T)),a.Y,frequency) + for normalization in (:reference_rms,:pointwise) + result=compare(a,b,R;atol=(R=cutoff,),normalization) + @test ismissing(only(result.absolute)) + @test ismissing(only(result.relative)) + end + # A valid error can be smaller than the physical operand floor. + original=fill(T(1e-3),1,1,3) + changed=original .+ T(1e-11) + if any(changed .!= original) + result=compare(complex.(original,zero(T)),complex.(changed,zero(T)),R; + frequencies=frequency,result_basis=:pul,atol=(R=cutoff,)) + @test 0 < only(result.absolute) < cutoff + @test only(result.absolute) ≈ norm(vec(changed-original))/sqrt(T(3)) + end + @test only(compare(a,a,R;band=(1,1),atol=(R=cutoff,)).absolute)==0 + @test only(compare(a,a,R;band=(1,1),atol=(R=cutoff,)).relative)==0 + end + # Components, not a magnitude approximation, decide complex zero. + z=reshape(ComplexF64[8e-11+8e-11im, 1.1e-10+0im],1,1,2) + resolution=observation_resolution(z,Z;frequencies=[1.,2.],atol=(R=1e-10,X=1e-10)) + @test vec(resolution.unresolved)==[true,false] + @test_throws ArgumentError observation_resolution(z,Z;atol=(Z=1e-10,)) + @test_throws ArgumentError observation_resolution(z,Z;atol=(X=1e-10,L=1e-15,)) + a=LineParameters(PhaseDomain,fill(1.0+1im,1,1,2),fill(1.0+1im,1,1,2),[0.,1.]) + @test ismissing(only(compare(a,a,L).absolute)) + @test only(compare(a,a,R).absolute)==0 + a.Z.values[1,1,1]=complex(NaN,0) + @test ismissing(only(compare(a,a,R).absolute)) + @test only(compare(a,a,R;band=(1.,1.)).absolute)==0 +end + +@testitem "ObservedResult / declared inputs survive completion without tracing" tags=[:unit, :parametric] setup=[TestFixtures] begin + using LineCableModels.Commons: observation_gridpoint,observation_labels + problem=TestFixtures.line_parameters_problem() + scalar=compute(problem) + retained=observation_gridpoint(scalar) + @test retained.inputs.temperature==20.0 + @test retained.inputs.earth_props.layers[2].rho==100.0 + @test retained.inputs.system.line_length==600.0 + @test retained.inputs.system.designs[1].origin.items[1].item.items[1].primitive.r==0.005 + @test retained.id.problem_index==retained.id.formulation_index==1 + @test retained.formulations==scalar.details.data.formulations + calls=Ref(0) + space=Gridspace{LineParametersProblem}(rho -> begin + calls[]+=1 + LineParametersProblem(problem.system;earth_props=EarthModel(rho),frequencies=[50.]) + end,(Grid([100.,200.]),)) + results=compute(ParametricProblem(space),Combinatorial(Formulation())) + @test calls[]==2 + descriptions=observation_gridpoint.(collect(results)) + @test calls[]==2 + @test [d.id.problem_index for d in descriptions]==[1,2] + @test [d.inputs.earth_props.layers[2].rho for d in descriptions]==[100.,200.] + @test all(d -> d.inputs.system.designs[1].origin.items[1].item.items[1].primitive.r==0.005,descriptions) + descriptions[1].inputs.system.connection_order[1]=-100 + @test results[1].details.data.inputs.system.connection_order[1] != -100 + @test observation_gridpoint(results[2]).inputs.earth_props.layers[2].rho==200. + labels=observation_labels(observables(results);request=R) + @test length(unique(labels))==2 + @test calls[]==2 + radius_calls=Ref(0) + radius_space=Gridspace{CableConstantsProblem}(scale -> begin + radius_calls[]+=1 + CableConstantsProblem(TestFixtures.coaxial_design(;scale)) + end,(Grid([1.,2.]),)) + radius_results=compute(ParametricProblem(radius_space),Combinatorial(CableConstantsFormulation())) + radius_points=observables(radius_results) + radius_labels=observation_labels(radius_points;request=R) + @test length(unique(radius_labels))==2 + @test radius_calls[]==2 + @test getproperty.(getproperty.(getproperty.(radius_points,:gridpoint),:id),:problem_index)==[1,2] + constants=compute(CableConstantsProblem(problem.system.designs[1])) + @test observation_gridpoint(constants).inputs.design.origin.items[1].item.items[1].primitive.r==0.005 +end + +@testitem "ObservedResult / clipping gives exact zero without uncertainty" tags=[:unit, :measurements] begin + using Measurements, Calculus + shared=Measurements.measurement(5e-13,1e-20) + x=[shared,2shared] + constants=CableConstants([:first,:second],ones(2),ones(2),ones(2),x,50.) + observed=ObservedResult(constants,(G,);length_unit=:base) + shifted=observe(observed,G) + @test Measurements.value.(shifted)==zeros(2) + @test Measurements.uncertainty.(shifted)==zeros(2) + @test Measurements.uncertainty(shifted[2]-2shifted[1])==0 + @test Measurements.uncertainty(shifted[1]-shared)==Measurements.uncertainty(shared) + @test observe(constants,G)==x + untrimmed=ObservedResult(constants,(G,);length_unit=:base,clip=false) + @test observe(untrimmed,G)==x + @test all(only(observed.quantities).available) + @test all(only(observed.quantities).engineering_zero) + scalar=ObservedResult(constants,((G,1),);length_unit=:base) + @test iszero(Measurements.value(observe(scalar,G))) + @test iszero(Measurements.uncertainty(observe(scalar,G))) + unassessed=ObservedResult(constants,(L,);length_unit=:base,quantity_units=:base, + atol=(X=1e-6,)) + @test only(unassessed.quantities).thresholds.kind===:unassessed + @test observe(unassessed,L)==ones(2) + # Native Measurements.isfinite checks the nominal value, not its spread. + undefined=abs(complex(Measurements.measurement(0.,1.),Measurements.measurement(0.,2.))) + @test !LineCableModels.Engine.resolution_available(undefined) + unavailable=ObservedResult(CableConstants(1.,1.,1.,undefined),(G,)) + @test ismissing(only(observe(unavailable,G))) + @test !only(only(unavailable.quantities).available) +end diff --git a/test/unit/engine/observed_workflow.jl b/test/unit/engine/observed_workflow.jl new file mode 100644 index 000000000..2d7b4f07d --- /dev/null +++ b/test/unit/engine/observed_workflow.jl @@ -0,0 +1,160 @@ +@testitem "ObservedResult / primary representations and quantity tables" tags=[:unit, :measurements] begin + using DataFrames, LinearAlgebra, Measurements + using LineCableModels.ReportBuilder: tabulate + using LineCableModels.Commons: observation_requests + line=LineParameters(reshape(ComplexF64.(1:12),2,2,3),fill(3.0+4im,2,2,3),[0.,1.,2.]) + observed=ObservedResult(line) + @test_throws ArgumentError ObservedResult(line,(R,)) + @test_throws ArgumentError ObservedResult(line,(X,L)) + @test_throws DimensionMismatch ObservedResult(line,((R,1,1,:),(X,2,2,:))) + @test observation_requests(line,(R,);complete_pairs=true).displayed==((R,:,:,:),) + @test length(ObservedResult(line,(R,);complete_pairs=true).quantities)==4 + table=tabulate(observed,R) + @test size(table)==(3,5) + @test names(table)==["frequency","[1,1]","[1,2]","[2,1]","[2,2]"] + @test table[1,Symbol("[1,2]")]==3000 + @test table[1,Symbol("[2,1]")]==2000 + line.Z.values .= -1 + @test observe(observed,R)[1]==1000 + @test tabulate(observed,(R,2,1,:))[!,2]==[2000,6000,10000] + @test observe(observed,R,2,1,:)==[2000,6000,10000] + for requests in ((R,X),(R,L),((Z,abs),(Z,angle)),(G,B),(G,C),((Y,abs),(Y,angle))) + @test length(ObservedResult(line,requests).quantities)==4 + end + dc=LineParameters(fill(1.0+0im,1,1,1),fill(1.0+0im,1,1,1),[0.]) + proxies=ObservedResult(dc,(R,L,G,C)) + @test ismissing(only(observe(proxies,L))) + @test only(observe(proxies,R))==1000 + scalar=ObservedResult(dc,((R,1,1,1),(L,1,1,1),(G,1,1,1),(C,1,1,1))) + @test ismissing(observe(scalar,L)) + shared=measurement(0.,.2) + uncertain=LineParameters(fill(complex(shared,2shared),1,1,2),fill(1.0+2im,1,1,2),[1.,2.]) + polar=ObservedResult(uncertain,((Z,abs),(Z,angle))) + @test all(ismissing,observe(polar,Z,abs)) + @test all(ismissing,observe(polar,Z,angle)) + @test polar.quantities[1].missing_reason[1]===:undefined_first_order_magnitude + @test uncertainty(polar.quantities[1].unavailable_components.imaginary[1]-2shared)==0 + @test_throws ArgumentError observe(observed,L) +end + +@testitem "ObservedResult / benchmark association survives filtering" tags=[:unit, :report] begin + using LineCableModels.Engine: compare, retain_gridpoint + using LineCableModels.Commons: gridpoint_id, observation_groups + using LineCableModels.ReportBuilder: BenchmarkTableDefinition + base=LineParameters(fill(1.0+2im,2,2,3),fill(3.0+4im,2,2,3),[1.,2.,3.]) + reference=retain_gridpoint(base,gridpoint_id()) + source=gridpoint_id().source_id + results=[retain_gridpoint(LineParameters(base.Z.*factor,base.Y,[1.,2.,3.]), + gridpoint_id(source_id=source,problem_index=index)) for (index,factor) in enumerate((1.1,1.2))] + errors=compare(reference,results,[R,L];bands=(:all,)) + timings=[(result_id=LineCableModels.Commons.observation_gridpoint(result).id,seconds=index) + for (index,result) in enumerate(results)] + observed=observables(reverse(results);comparisons=errors,timings) + @test length(observed)==2 + @test observed[1].gridpoint.id.problem_index==2 + @test observed[1].timings.seconds==2 + @test all(error -> error.result_id==observed[1].gridpoint.id,observed[1].errors) + @test all(error -> error.maxima.absolute.value>0,observed[1].errors) + ref=ObservedResult(reference) + artifact=report(BenchmarkTableDefinition(),observed[1:1];reference=ref) + @test artifact.reference===ref + @test artifact.observed[1].gridpoint.id.problem_index==2 + @test size(artifact.tables.terms,1)==8 + @test all(==(2),artifact.tables.terms.problem_index) + @test only(artifact.tables.execution.seconds)==2 + @test only(artifact.tables.execution.result_source)==string(observed[1].gridpoint.id.source_id) + @test only(artifact.tables.execution.result_point)==2 + @test only(artifact.tables.execution.reference_source)==string(ref.gridpoint.id.source_id) + equal_timings=[(result_id=point.gridpoint.id,seconds=1.,scope=:compute_call_wall) for point in observed] + equal_observed=observables(reverse(results);comparisons=errors,timings=equal_timings) + timing_report=report(BenchmarkTableDefinition(),equal_observed;reference=ref) + @test timing_report.tables.execution.seconds==[1.,1.] + @test timing_report.tables.execution.result_point==[2,1] + @test timing_report.tables.execution.result_formulation==[1,1] + @test_throws ArgumentError ObservedResult(first(results);timings=first(timings[2:2])) + results[2].Z.values .= NaN + @test all(isfinite,skipmissing(artifact.tables.terms.absolute_rms)) + @test_throws ArgumentError report(BenchmarkTableDefinition((C,)),observed;reference=ref) + @test length(observation_groups(observed;request=R))==2 +end + +@testitem "ObservedResult / UQ products and archive-wide dependencies" tags=[:unit, :measurements, :slow] begin + using Statistics, Measurements + using LineCableModels.ReportBuilder: tabulate + using LineCableModels.Commons: gridpoint_id + f=[1.,2.,3.] + base=LineParameters(fill(1.0+2im,2,2,3),fill(3.0+4im,2,2,3),f) + trials=(R=reshape(collect(1.:60.),2,2,3,5),L=fill(0.001,2,2,3,5),C=fill(1e-9,2,2,3,5),G=fill(1e-6,2,2,3,5)) + summaries=map(array -> [SampleSummary(vec(array[i,j,k,:])) for i in 1:2,j in 1:2,k in 1:3],trials) + core=LineCableModels.materialize(base,summaries) + core=LineCableModels.Engine.retain_gridpoint(core,gridpoint_id()) + mc=MonteCarloResult(MonteCarlo(Formulation();trials=5,seed=12,return_samples=true), + [core],[summaries],[trials],nothing,UInt64(12),UInt64[13],[5]) + product=ObservedResult(mc,1,((statistics,R,mean),(statistics,R,std))) + @test length(product.quantities)==2 + @test observe(product,statistics,R,mean)≈mean.(summaries.R).*1000 + @test observe(product,statistics,R,std)≈std.(summaries.R).*1000 + @test product.gridpoint.sampling.mean_standard_error.R≈std.(summaries.R)./sqrt(5) + sampled=ObservedResult(mc,1,((samples,R),);length_unit=:base) + selected_samples=ObservedResult(sampled,((samples,R,2,1,[3,1],[5,2]),)) + sample_product=only(selected_samples.quantities) + @test sample_product.values==trials.R[2,1,[3,1],[5,2]] + @test sample_product.coordinates.samples==[3,1] + @test sample_product.coordinates.trials==[5,2] + @test sample_product.coordinates.frequencies==[3.,1.] + all_trials=ObservedResult(mc,1,((samples,R,2,1,[3,1]),);length_unit=:base) + @test only(all_trials.quantities).values==trials.R[2,1,[3,1],:] + retained_trials=ObservedResult(sampled,((samples,R,2,1,[3,1]),)) + @test only(retained_trials.quantities).values==only(all_trials.quantities).values + @test size(tabulate(selected_samples,only(selected_samples.quantities).request),1)==4 + histogram=ObservedResult(mc,1,((histograms,R,1,2,3,3),)) + @test length(histogram.quantities)==1 + distribution=only(histogram.quantities) + @test sum(distribution.values.count)==5 + @test sum(distribution.values.probability)≈1 + @test distribution.distribution.qq!==nothing + converted=only(ObservedResult(histogram;length_unit=:base).quantities) + @test converted.values.lower≈distribution.values.lower./1000 + @test converted.values.density≈distribution.values.density.*1000 + @test converted.values.probability==distribution.values.probability + @test converted.values.count==distribution.values.count + @test converted.distribution.qq.sample≈distribution.distribution.qq.sample./1000 + @test converted.distribution.model_cdf.y==distribution.distribution.model_cdf.y + @test size(tabulate(histogram,distribution.request),1)==3 + trials.R .= -1 + @test all(>(0),distribution.distribution.qq.sample) + shared=measurement(1.,.2) + uncertain=LineParameters(fill(complex(shared,2shared),2,2,3),fill(complex(3shared,4shared),2,2,3),f) + ordinary=report(uncertain) + @test ordinary.observed isa ObservedResult + observed=ordinary.observed + lep=LinearErrorResult(LinearError(Formulation()),[uncertain]) + @test only(report(lep;values=R).tables).Z.R==report(uncertain;values=R).tables.Z.R + @test length(observed.quantities)==4 + @test all(q -> q.statistic===:value,observed.quantities) + @test !haskey(observed.gridpoint,:sampling) + artifact=report([observed];reference=observed) + mktempdir() do directory + for suffix in (".json",".jls") + path=LineCableModels.save(artifact,joinpath(directory,"shared"*suffix)) + restored=LineCableModels.import_data(:observed,path) + a=observe(restored.observed[1],R)[1] + b=observe(restored.reference,R)[1] + x=observe(restored.reference,X)[1] + @test uncertainty(a-b)==0 + @test uncertainty(x-2a)==0 + @test uncertainty(a)>0 + end + end + setprecision(BigFloat,384) do + precise=LineParameters(fill(complex(big"1.234567890123456789012345678901",big"2"),1,1,2), + fill(complex(big"3",big"4"),1,1,2),BigFloat[1,2]) + original=ObservedResult(precise;length_unit=:base) + encoded=LineCableModels.ImportExport.serialize_value(original) + restored=setprecision(BigFloat,128) do + LineCableModels.ImportExport.deserialize_value(encoded) + end + @test precision(observe(restored,R)[1])==384 + @test observe(restored,R)[1]==observe(original,R)[1] + end +end diff --git a/test/unit/engine/options.jl b/test/unit/engine/options.jl new file mode 100644 index 000000000..7374fd6d2 --- /dev/null +++ b/test/unit/engine/options.jl @@ -0,0 +1,120 @@ +@testitem "Engine / option grammar / owner dispatch" tags=[:unit, :engine] setup=[ + UseEngineSupport +] begin + const Commons=LineCableModels.Commons + const Engine=LineCableModels.Engine + + struct UnregisteredFormulation<:Commons.AbstractFormulation end + + @test LineCableModels.FormulationOptions === Commons.FormulationOptions !== NamedTuple + @test LineCableModels.ComputationOptions === Commons.ComputationOptions !== NamedTuple + @test LineCableModels.formulation_options === Commons.formulation_options + @test LineCableModels.computation_options === Commons.computation_options + @test parentmodule(Commons.formulation_options) === Commons + @test parentmodule(Commons.computation_options) === Commons + + formulation_owner=LineParametersFormulation + computation_type=LineCableModelsCoaxial + @test hasmethod( + Commons.formulation_options, + Tuple{Type{LineParametersFormulation}, FormulationOptions} + ) + @test hasmethod( + Commons.computation_options, + Tuple{Type{LineCableModelsCoaxial}, ComputationOptions} + ) + # Passive selections cannot invoke live normalization by accident. + @test_throws MethodError Commons.formulation_options((;)) + retained_options = (reduce_bundle=false, kron_reduction=true, + ideal_transposition=false) + @test_throws MethodError Commons.formulation_options((leaf=formulation_owner => + (options=retained_options,),)) + @test_throws MethodError Commons.computation_options((;)) + @test_throws MethodError Commons.formulation_options(:unregistered, (;)) + @test_throws MethodError Commons.computation_options(:unregistered, ComputationOptions((;))) + @test_throws MethodError Commons.formulation_options( + UnregisteredFormulation, + (;) + ) + @test_throws MethodError Commons.computation_options( + UnregisteredFormulation, ComputationOptions((;))) + @test_throws MethodError Commons.formulation_options( + formulation_owner, Dict{Symbol, Any}()) + @test_throws MethodError Commons.computation_options( + computation_type, Dict{Symbol, Any}()) + @test_throws MethodError Commons.computation_options(computation_type, nothing) + + formulation=@inferred Commons.formulation_options(formulation_owner, FormulationOptions()) + @test formulation.data == ( + reduce_bundle = true, + kron_reduction = true, + ideal_transposition = false + ) + @test_throws ArgumentError Commons.formulation_options( + formulation_owner, FormulationOptions(unknown = true)) + + default_execution=@inferred Commons.computation_options(computation_type, ComputationOptions((;))) + @test default_execution.data.output_basis == Val(:pul) + @test default_execution.data.trace == Val(false) + @test default_execution.data.on_result === nothing + execution=Commons.computation_options( + computation_type, ComputationOptions(( + verbosity = (default = 1, NLsolve = 0), + output_basis = :total, + trace = true + ))) + @test execution.data == ( + verbosity = (default = 1, NLsolve = 0), + output_basis = Val(:total), + trace = Val(true), + on_result = nothing, + timing = false + ) + @test Engine.verbosity(execution, :NLsolve) == 0 + @test Engine.verbosity(execution, :unlisted) == 1 + @test_throws ArgumentError Commons.computation_options( + computation_type, ComputationOptions((unknown = true,))) + @test_throws ArgumentError Commons.computation_options( + computation_type, ComputationOptions((output_basis = :unknown,))) +end + +@testitem "Engine / FEM computation option ownership" tags=[:unit, :engine] begin + using LineCableModels + + formulation = LineCableModelsFEM(options=(physics=:quasi_fw,)) + @test formulation.options isa FormulationOptions + @test formulation.options.data.physics === Symbol("quasi-fw") + @test !haskey(NamedTuple(formulation), :execution) + # Execution settings cannot enter through the formulation, or vice versa. + @test_throws ArgumentError LineCableModelsFEM(options=(frequency_workers=4,)) + @test_throws ArgumentError LineCableModelsFEM(options=(domain_skin_depths=1.5,)) + @test_throws ArgumentError computation_options(LineCableModelsFEM, ComputationOptions((physics=:quasi_fw,))) + @test_throws ArgumentError computation_options(LineCableModelsFEM, ComputationOptions((unknown=true,))) + defaults = @inferred computation_options(LineCableModelsFEM, ComputationOptions((;))) + @test defaults isa ComputationOptions + @test defaults.data.domain_skin_depths === 2.0 + @test computation_options(LineCableModelsFEM, ComputationOptions((;domain_skin_depths=1.5))).data.domain_skin_depths === 1.5 + callback = (problem, index, result) -> nothing + raw = (frequency_workers=Int32(4), solver_threads=Int16(2), + on_result=callback, trace=true, mesh_mode=:remesh, + getdp_executable=SubString("/tmp/getdp", 1), verbosity=(default=1,)) + configured = computation_options(LineCableModelsFEM, ComputationOptions(raw)) + @test keys(configured.data) == keys(defaults.data) + @test configured.data.frequency_workers isa Int + @test configured.data.solver_threads isa Int + @test configured.data.getdp_executable isa String + @test configured.data.on_result === callback + @test configured.data.trace === Val(true) + resources(options) = options.data.frequency_workers * options.data.solver_threads + @test (@inferred resources(configured)) == 8 + @test formulation.options.data.physics === Symbol("quasi-fw") + for invalid in ((ui=1,), (plot_field_maps=:yes,), (keep_run_directory=1,), + (mesh_path="",), (getdp_executable=1,), (log_file="",), + (frequency_workers=true,), (solver_threads=1.5,), (gmsh_verbosity=true,), + (getdp_verbosity=6,), (resume_run_directory="",), (resume_run_directory=:bad,)) + @test_throws ArgumentError computation_options(LineCableModelsFEM, ComputationOptions(invalid)) + end + for invalid in (0, -1, Inf, NaN, true, "2") + @test_throws ArgumentError computation_options(LineCableModelsFEM, ComputationOptions((;domain_skin_depths=invalid))) + end +end diff --git a/test/unit/engine/pipe_formulations.jl b/test/unit/engine/pipe_formulations.jl new file mode 100644 index 000000000..c37057868 --- /dev/null +++ b/test/unit/engine/pipe_formulations.jl @@ -0,0 +1,55 @@ +@testitem "Engine / pipe selections / backend applicability before lowering" tags=[:unit, :parametric] begin + using LineCableModels + const E = LineCableModels.Engine + copper = Material(:conductor, 1.72e-8) + dielectric = Material(:insulator, 1e14, 2.3) + air = Material(:insulator, Inf, 1.0) + first_core = terminal(:a, solid(copper, Disk(0.005)), insulation(dielectric; t = 0.002)) + second_core = terminal(:b, solid(copper, Disk(0.005)), insulation(dielectric; t = 0.002)) + wall = terminal(:pipe, sheath(copper; t = 0.001), insulation(dielectric; t = 0.002)) + design = build(CableDesign, + "pipe-preflight", + pipe( + at(first_core, -0.01, 0), at(second_core, 0.01, 0); + shape = Disk(0.025), fill = air, wall)) + system = build(LineCableSystem, design, Pose2(0.0, -1.0); + connections = Dict(:a=>1, :b=>2, :pipe=>0)) + problem = LineParametersProblem(system; earth_props = homogeneous(rho = 100.0), frequencies = [50.0]) + selected = @inferred E.PipeImpedance.Formula(Val(:default)) + expected = "Pipe-type cable formulation is not yet implemented for the coaxial backend. No pipe formulation is available." + for execute in (() -> compute(problem), () -> compute(CableConstantsProblem(design))) + failure = try + execute() + nothing + catch exception + exception + end + @test failure isa ArgumentError + @test sprint(showerror, failure) == "ArgumentError: $expected" + end + # FEM enclosure support is exercised by its geometry and numerical suites. + @test !haskey(LineCableModelsFEM().methods, :pipe_impedance) + @test_throws MethodError LineCableModelsFEM(pipe_impedance=formula(:default)) + @test_throws ArgumentError E.PipeImpedance.Formula(:UnimplementedPipe2026) + @test_throws ArgumentError E.PipeImpedance.Formula(:default; parameters = (invented = true,)) + @test_throws ArgumentError Formulation(pipe_impedance = formula(:default; order = :before)) + + concentric = build( + CableDesign, "concentric-enclosure", pipe(first_core; + shape = Disk(0.01), fill = air, wall)) + @test validate(concentric, selected, LineCableModelsCoaxial()) === concentric + @test all(isfinite, compute(CableConstantsProblem(concentric)).R) + + # A dielectric duct is not a conducting pipe-return formulation. + ducted = build(CableDesign, + "dielectric-duct", + pipe( + at(first_core, -0.01, 0), at(second_core, 0.01, 0); + shape = Disk(0.025), fill = air, wall = insulation(dielectric; t = 0.002))) + @test validate(ducted, selected, LineCableModelsCoaxial()) === ducted + for constructor in (Formulation, CableConstantsFormulation) + selections = constructor(pipe_impedance = Grid((:default, formula(:default)))) + @test length(selections) == 2 + @test all(value -> value.methods.pipe_impedance isa E.PipeImpedanceFormulation, selections) + end +end diff --git a/test/unit/engine/radial_dielectric.jl b/test/unit/engine/radial_dielectric.jl new file mode 100644 index 000000000..74f0eb185 --- /dev/null +++ b/test/unit/engine/radial_dielectric.jl @@ -0,0 +1,238 @@ +@testitem "Engine / homogeneous dielectric retains constitutive selection across frequency" tags=[:unit, :measurements] begin + using Test + using LineCableModels + using Measurements: measurement, value, uncertainty + const E = LineCableModels.Engine + const DM = LineCableModels.DataModel + const IE = LineCableModels.ImportExport + metal = Material(:conductor, 1.72e-8) + semicon = Material(:semicon, 1e5, 1000.0, 1.0, 20.0, 0.01; tan_delta = 0.02) + dielectric = Material(:insulator, 2e9, 3.1, 1.0, 20.0, 0.02; tan_delta = 0.04) + radii = (0.01, 0.012, 0.02, 0.021) + source = @cable "radial-mixture" begin + @terminal :core begin + solid(metal, Disk(radii[1])) + screen(semicon; t = radii[2]-radii[1]) + insulation(dielectric; t = radii[3]-radii[2]) + screen(semicon; t = radii[4]-radii[3]) + end + end + homogeneous = homogenize(source) + @test length(homogeneous.geometry.regions) == 2 + material = last(homogeneous.geometry.regions).source.material + @test material isa RadialDielectric{Float64} + @test getproperty.(material.materials, :kind) == [:semicon, :insulator, :semicon] + @test !hasproperty(material, :tan_delta) + @test !hasproperty(material, :frequency) + @test @inferred(E.flatten(LineCableModelsCoaxial(), homogeneous, Float64)) isa + E.CableBlueprint{Float64} + @test length(E.flatten(LineCableModelsCoaxial(), homogeneous).dielectrics) == 3 + @test occursin("3 constituents", sprint(show, material)) + @test only(DM.preview_materials(last(homogeneous.geometry.regions))) == material + @test length(DM.preview_shapes(last(homogeneous.geometry.regions))) == 1 + @test IE.deserialize_value(IE.serialize_value(material)) == material + reconstructed = IE.deserialize_value(IE.serialize_value(homogeneous)) + @test last(reconstructed.geometry.regions).source.material == material + + frequencies = 10.0 .^ range(-1, 6; length = 31) + for ins in (:default, :lossy), semi in (:default, :lossy) + + formulation = CableConstantsFormulation(insulation_admittance = ins, + semicon_admittance = semi) + relations = (formulation.methods.insulation_admittance, + formulation.methods.semicon_admittance) + for f in frequencies + original, + reduced = map((source, homogeneous)) do design + input = E.LocalCableData(E.flatten(LineCableModelsCoaxial(), design)) + y = zeros(ComplexF64, 1, 1) + admittivity = zeros(ComplexF64, length(input.dielectric_materials)) + E.dielectric!(admittivity, input, formulation.methods, f, 20.0) + E.cable_admittance!(y, input, admittivity, + complex(0.0, 2π*f), zeros(ComplexF64, length(input.dielectric_materials))) + only(y) + end + @test imag(reduced) ≈ imag(original) rtol=2e-13 + @test real(reduced) ≈ real(original) rtol=2e-13 + kappa = @inferred constitutive(relations, material, f, 20.0) + y = 2π * kappa / log(last(radii)/first(radii)) + @test imag(reduced) ≈ imag(y) rtol=2e-13 + @test real(reduced) ≈ real(y) rtol=2e-13 + if ins === semi === :default + @test real(reduced) == 0 + end + end + for f in (50.0, 60.0) + original = compute(CableConstantsProblem(source; frequency = f), formulation) + reduced = compute(CableConstantsProblem(homogeneous; frequency = f), formulation) + @test reduced.C ≈ original.C rtol=2e-13 + @test reduced.G ≈ original.G rtol=2e-13 + exported = only(LineCableModels.PSCAD._pscad_components(LineCableModels.Engine.flatten(LineCableModelsCoaxial(), homogeneous), f, formulation, 20.0)).dielectric + @test exported.shunt_capacitance ≈ only(original.C) rtol=2e-13 + @test exported.shunt_conductance ≈ only(original.G) rtol=2e-13 + end + end + + # Independent series circuit, not a baseline regenerated by the implementation. + ε0 = 8.8541878128e-12 + weights = log.(collect(radii[2:end]) ./ collect(radii[1:(end - 1)])) + physical = (semicon, dielectric, semicon) + lossless = ( + E.InsulationAdmittance.Formula(:default), E.SemiconAdmittance.Formula(:default)) + lossy = (E.InsulationAdmittance.Formula(:lossy), E.SemiconAdmittance.Formula(:lossy)) + for f in (0.1, 50.0, 60.0, 1e3, 1e6) + expected = inv(sum(q / (2π * (inv(m.rho) + 2π*f*ε0*m.eps_r*(m.tan_delta+im))) + for (q, m) in zip(weights, physical))) + actual = 2π * constitutive(lossy, material, f, 20.0) / sum(weights) + @test actual ≈ expected rtol=2e-13 + end + changed = RadialDielectric( + [Material(m.kind, m.rho*1e4, m.eps_r; tan_delta = m.tan_delta*100) + for m in material.materials], + material.weights) + @test constitutive(lossless, changed, 60.0, 20.0) == + constitutive(lossless, material, 60.0, 20.0) + single = RadialDielectric([dielectric], [0.6]) + subdivided = RadialDielectric([dielectric, dielectric], [0.2, 0.4]) + for f in frequencies + @test constitutive(lossy, single, f, 20.0) ≈ + constitutive(lossy[1], dielectric, f, 20.0) + @test constitutive(lossy, single, f, 20.0) ≈ + constitutive(lossy, subdivided, f, 20.0) + end + for correction in (false, true), f in (50.0, 60.0) + + formulation = CableConstantsFormulation(insulation_admittance = :lossy, + semicon_admittance = :lossy, temperature_dependence = correction ? + formula(:default) : + nothing) + original = compute(CableConstantsProblem(source; frequency = f, temperature = 60.0), formulation) + reduced = compute(CableConstantsProblem(homogeneous; frequency = f, temperature = 60.0), formulation) + @test original.C ≈ reduced.C rtol=2e-13 + @test original.G ≈ reduced.G rtol=2e-13 + expected = inv(sum(q / (2π * (inv(m.rho * (correction ? 1+m.alpha*40 : 1)) + + 2π*f*ε0*m.eps_r*(m.tan_delta+im))) + for (q, m) in zip(weights, physical))) + @test only(original.G) ≈ real(expected) rtol=2e-13 + @test only(original.C) ≈ imag(expected)/(2π*f) rtol=2e-13 + @test 2π * constitutive(lossy, material, f, 60.0; + temperature_dependence = correction ? + formulation.methods.temperature_dependence : nothing) / + sum(weights) ≈ expected rtol=2e-13 + exported = only(LineCableModels.PSCAD._pscad_components(LineCableModels.Engine.flatten(LineCableModelsCoaxial(), homogeneous), f, formulation, 60.0)).dielectric + @test exported.shunt_conductance ≈ real(expected) rtol=2e-13 + @test exported.shunt_capacitance ≈ imag(expected)/(2π*f) rtol=2e-13 + end + @test semicon.T0 == dielectric.T0 == 20.0 + regions = Region(:equivalent, Annulus(0.01, 0.02), Grid((single, subdivided))) + @test regions isa Gridspace{Region} + @test length(regions) == 2 + @test isconcretetype(eltype(regions)) + @test @inferred(first(regions)) isa Region + transported = Gridspace{CableDesign}(homogenize, (Grid((source, source)),)) + @test length(transported) == 2 + @test first(transported) isa CableDesign + @test last(first(transported).geometry.regions).source.material == material + uncertain = @cable "uncertain-radial-mixture" begin + @terminal :core begin + solid(metal, Disk(0.01)) + insulation(Material(:insulator, 1e10, measurement(3.1, 0.1)); t = 0.005) + insulation(dielectric; t = 0.005) + end + end + measured = homogenize(uncertain) + before = compute(CableConstantsProblem(uncertain), CableConstantsFormulation()) + after = compute(CableConstantsProblem(measured), CableConstantsFormulation()) + @test value.(before.C) ≈ value.(after.C) + @test uncertainty.(before.C) ≈ uncertainty.(after.C) + @test only(uncertainty.(after.C)) > 0 + roundtrip = IE.deserialize_value(IE.serialize_value(measured)) + replay = compute(CableConstantsProblem(roundtrip), CableConstantsFormulation()) + @test value.(after.C) ≈ value.(replay.C) + @test uncertainty.(after.C) ≈ uncertainty.(replay.C) + @test convert(RadialDielectric{Float32}, single) isa RadialDielectric{Float32} + @test_throws ArgumentError RadialDielectric([metal], [1.0]) + @test_throws ArgumentError RadialDielectric([dielectric], [-1.0]) + @test_throws DimensionMismatch RadialDielectric([dielectric], [1.0, 2.0]) +end + +@testitem "ImportExport / homogeneous dielectric library retains constituent laws" tags=[:unit, :importexport] begin + using JSON3 + using JSONSchema + const IE = LineCableModels.ImportExport + metal = Material(:conductor, 1.72e-8) + semicon = Material(:semicon, 1e5, 1000.0; tan_delta = 0.02) + dielectric = Material(:insulator, 2e9, 3.1; tan_delta = 0.04) + source = @cable "stored-radial-mixture" begin + @terminal :core begin + solid(metal, Disk(0.01)) + screen(semicon; t = 0.002) + insulation(dielectric; t = 0.008) + end + end + design = homogenize(source) + library = CablesLibrary() + add!(library, design) + schema_path = joinpath(pkgdir(LineCableModels), "schemas", "cable-design.schema.json") + schema = JSONSchema.Schema(JSON3.read(read(schema_path, String), Dict{String, Any})) + mktempdir() do directory + path = joinpath(directory, "homogeneous.json") + save(library; file_name = path) + document = JSON3.read(read(path, String), Dict{String, Any}) + @test JSONSchema.validate(schema, document) === nothing + restored = CablesLibrary() + load!(restored; file_name = path) + actual = restored[design.cable_id] + @test IE.serialize_value(actual) == IE.serialize_value(design) + material = last(actual.geometry.regions).source.material + @test material isa RadialDielectric{Float64} + @test getproperty.(material.materials, :kind) == [:semicon, :insulator] + for selector in (:default, :lossy), frequency in (50.0, 60.0) + + formulation = CableConstantsFormulation(insulation_admittance = selector, + semicon_admittance = selector) + expected = compute(CableConstantsProblem(design; frequency), formulation) + replay = compute(CableConstantsProblem(actual; frequency), formulation) + @test replay.C == expected.C + @test replay.G == expected.G + end + end +end + +@testitem "Gmsh FEM / homogeneous dielectric constitutive response retains constituent kinds" tags=[:extension, :fem] begin + using Gmsh + using LineCableModels + const FEM = Base.get_extension(LineCableModels, :LineCableModelsGmshExt) + const E = LineCableModels.Engine + material = RadialDielectric( + [Material(:semicon, 1e5, 1000.0, 1.0, 20.0, 0.01; tan_delta = 0.02), + Material(:insulator, 2e9, 3.1, 1.0, 20.0, 0.02; tan_delta = 0.04)], + [0.2, 0.6]) + design = @cable "homogeneous-fem" begin + @terminal :core begin + solid(Material(:conductor, 1.72e-8), Disk(0.01)) + insulation(material; t = 0.01) + end + end + system = build(LineCableSystem, design, (0.0, -0.2); connections = Dict(:core=>1)) + problem = LineParametersProblem( + system; frequencies = [0.1, 50.0, 60.0, 1e3, 1e6], temperature = 60.0, + earth_props = homogeneous(rho = 100.0, eps_r = 10.0)) + for ins in (:default, :lossy), semi in (:default, :lossy), correction in (false, true) + formulation = Formulation( + :fem; insulation_admittance = ins, semicon_admittance = semi, + temperature_dependence = correction ? formula(:default) : nothing, options = (ideal_transposition = false,)) + model = FEM._resolved_fem_model(problem, formulation) + passive = only(filter(m -> m.kind === :insulator, model.material_plans)) + @test length(model.material_plans) == 2 + relations = (formulation.methods.insulation_admittance, + formulation.methods.semicon_admittance) + @test passive.admittivity ≈ [constitutive(relations, material, f, 60.0; + temperature_dependence = correction ? + formulation.methods.temperature_dependence : + nothing) for f in problem.frequencies] + if ins === semi === :default + @test all(iszero, real.(passive.admittivity)) + end + end +end diff --git a/test/unit/engine/result_containers.jl b/test/unit/engine/result_containers.jl new file mode 100644 index 000000000..0a05657a4 --- /dev/null +++ b/test/unit/engine/result_containers.jl @@ -0,0 +1,432 @@ +@testitem "Engine / result containers / numeric and selector behavior" tags=[:unit, :report] setup=[ + UseEngineSupport, TestNumerics, TestFixtures] begin + using DataFrames + + design=TestFixtures.coaxial_design() + constants=CableConstants(design) + @test constants isa CableConstants{Float64} + @test all(>(0), constants.R) + @test all(>(0), constants.L) + @test all(>(0), constants.C) + @test all(>=(0), constants.G) + @test basis(constants) === :pul + constants_display=sprint(show, constants) + @test occursin("CableConstants(assemblies=1", constants_display) + @test resistance(constants) === constants.R + @test inductance(constants) === constants.L + @test capacitance(constants) === constants.C + @test conductance(constants) === constants.G + @test observe(constants, R) === constants.R + @test observables(typeof(constants)) == (R, L, C, G) + constants_observables=observables( + constants, + (R, L, C, G) + ) + @test constants_observables isa ObservedResult + @test observe(constants_observables,R)≈1_000constants.R + constants_table=LineCableModels.ReportBuilder.tabulate(constants_observables,R) + @test names(constants_table)==["frequency","core"] + @test constants_table.frequency==[constants.frequency] + @test constants_table.core≈1_000constants.R + @test DataFrames.metadata(constants_table,"basis")===basis(constants) + constants_columns=LineCableModels.ReportBuilder.observation_columns(constants_table) + @test constants_columns.core.assembly==1 + @test LineCableModels.Units.label(constants_columns.core.unit)=="Ω/km" + native_constants_table=DataFrame(constants) + @test names(native_constants_table) == ["core", "R", "L", "C", "G"] + @test native_constants_table.R == constants.R + + frequency=[50.0, 100.0, 200.0] + angular=reshape(2π .* frequency, 1, 1, :) + resistance_values=reshape(collect(1.0:12.0), 2, 2, 3) .* 1.0e-4 + inductance_values=reshape(collect(13.0:24.0), 2, 2, 3) .* 1.0e-7 + conductance_values=reshape(collect(25.0:36.0), 2, 2, 3) .* 1.0e-8 + capacitance_values=reshape(collect(37.0:48.0), 2, 2, 3) .* 1.0e-10 + impedance=complex.(resistance_values, inductance_values .* angular) + admittance=complex.(conductance_values, capacitance_values .* angular) + parameters=LineParameters( + impedance, + admittance, + frequency; + basis = :total + ) + + @test basis(parameters) === :total + @test frequencies(parameters) == frequency + @test nconductors(parameters) == 2 + @test nfrequencies(parameters) == 3 + @test Z(parameters) == impedance + @test Y(parameters) == admittance + @test Z(parameters, 1, 2) == impedance[1, 2, :] + @test Z(parameters, 1, 2, 2) == impedance[1, 2, 2] + @test Y(parameters, 2, 1, 1:2) == admittance[2, 1, 1:2] + @test R(parameters, 1, 2, :) == resistance_values[1, 2, :] + @test X(parameters, 1, 2, 2) == imag(impedance[1, 2, 2]) + @test L(parameters, 1, 2, 2:3) ≈ inductance_values[1, 2, 2:3] + @test G(parameters, 2, 1) == conductance_values[2, 1, :] + @test B(parameters, 2, 1, 2) == imag(admittance[2, 1, 2]) + @test C(parameters, 2, 1, 1:2) ≈ capacitance_values[2, 1, 1:2] + @test series_impedance(parameters) === parameters.Z.values + @test shunt_admittance(parameters) === parameters.Y.values + @test resistance(parameters, 1, 1, 1) == R(parameters, 1, 1, 1) + @test reactance(parameters, 1, 1, 1) == X(parameters, 1, 1, 1) + @test inductance(parameters, 1, 1, 1) ≈ L(parameters, 1, 1, 1) + @test conductance(parameters, 1, 1, 1) == G(parameters, 1, 1, 1) + @test susceptance(parameters, 1, 1, 1) == B(parameters, 1, 1, 1) + @test capacitance(parameters, 1, 1, 1) ≈ C(parameters, 1, 1, 1) + @test L(parameters) ≈ inductance_values + @test C(parameters) ≈ capacitance_values + @test observe(parameters, Z) === parameters.Z.values + @test observe(parameters, Y) === parameters.Y.values + parameter_observables=observables(parameters,((Z,abs,1,2,:),(Z,angle,1,2,:)); + atol=(R=0.,X=0.,G=0.,B=0.)) + @test parameter_observables isa ObservedResult + @test first(parameter_observables.quantities).coordinates.frequencies==frequency + @test observe(parameter_observables,Z,abs)≈abs.(impedance[1,2,:]) + @test observe(parameter_observables,Z,angle)≈rad2deg.(angle.(impedance[1,2,:])) + @test first(parameter_observables.quantities).coordinates.frequencies !== parameters.f + + series=SeriesImpedance(impedance; basis = :total) + shunt=ShuntAdmittance(admittance; basis = :total) + @test series_impedance(series) === series.values + @test shunt_admittance(shunt) === shunt.values + @test Z(series) == impedance + @test Y(shunt) == admittance + @test resistance(series, 1, 2, 2) == resistance_values[1, 2, 2] + @test reactance(series, 1, 2, 2) == imag(impedance[1, 2, 2]) + @test conductance(shunt, 2, 1, 2) == conductance_values[2, 1, 2] + @test susceptance(shunt, 2, 1, 2) == imag(admittance[2, 1, 2]) + for container in (series, shunt) + @test size(container) == (2, 2, 3) + @test size(container, 3) == 3 + @test axes(container) == (Base.OneTo(2), Base.OneTo(2), Base.OneTo(3)) + @test ndims(typeof(container)) == 3 + @test eltype(typeof(container)) == ComplexF64 + @test Base.IndexStyle(typeof(container)) == IndexCartesian() + @test container[1, 2, 2] == container.values[1, 2, 2] + @test basis(typeof(container)) === :total + @test basis(container) === :total + end + @test occursin("2×2×3", sprint(show, series)) + @test occursin("unit=Ω", sprint(show, series)) + @test occursin("2×2×3", sprint(show, shunt)) + @test occursin("unit=S", sprint(show, shunt)) + + reconstructed=LineParameters(series, shunt, frequency) + @test reconstructed.Z === series + @test reconstructed.Y === shunt + @test occursin("LineParameters(phase domain; 2×2×3, basis=:total)", sprint(show, reconstructed)) + detailed=sprint(show, MIME"text/plain"(), reconstructed) + @test occursin("LineParameters · phase domain", detailed) + @test occursin("Z 2×2×3 · Ω", detailed) + @test occursin("Y 2×2×3 · S", detailed) + + promoted=LineParameters( + SeriesImpedance(ComplexF32.(impedance); basis = :total), + shunt, + frequency + ) + @test eltype(promoted) === ComplexF64 + @test_throws ArgumentError SeriesImpedance(impedance; basis = :invalid) + @test_throws ArgumentError ShuntAdmittance(admittance; basis = :invalid) + @test_throws ArgumentError SeriesImpedance{ComplexF64, 1}(impedance) + @test_throws ArgumentError ShuntAdmittance{ComplexF64, 1}(admittance) + @test_throws ArgumentError LineParameters( + impedance, admittance, frequency; basis = :invalid) + @test_throws ArgumentError LineParameters( + SeriesImpedance(impedance; basis = :total), + ShuntAdmittance(admittance; basis = :pul), + frequency + ) + + selected=parameters[2:3] + @test basis(selected) === :total + @test domain(selected) === domain(parameters) + @test frequencies(selected) == frequency[2:3] + @test selected.Z.values == impedance[:, :, 2:3] + @test frequencies(parameters[2]) == [100.0] + @test frequencies(parameters[[3, 1]]) == [200.0, 50.0] + @test frequencies(parameters[:]) == frequency + @test_throws BoundsError parameters[4] + + requests=( + @observe(R[:, :, :]), + @observe(L[:, :, :]), + @observe(G[:, :, :]), + @observe(C[:, :, :]) + ) + retained=ObservedResult(parameters,requests;atol=(R=0.,L=0.,G=0.,C=0.)) + tables=LineCableModels.ReportBuilder.tabulate(retained) + parameter_table=tables.Z.R + @test names(parameter_table)==["frequency","[1,1]","[1,2]","[2,1]","[2,2]"] + @test nrow(parameter_table)==3 + @test collect(parameter_table[1,2:5])==vec(resistance_values[:,:,1])[[1,3,2,4]] + @test DataFrames.metadata(parameter_table,"basis")===basis(parameters) + @test LineCableModels.Units.label(LineCableModels.ReportBuilder.observation_columns(parameter_table)[Symbol("[1,1]")].unit)=="Ω" + subset_table=LineCableModels.ReportBuilder.tabulate(retained,(R,2,1,2:3)) + @test names(subset_table)==["frequency","[2,1]"] + @test subset_table.frequency==frequency[2:3] + @test subset_table[!,2]==resistance_values[2,1,2:3] + @test_throws DimensionMismatch ObservedResult(parameters,((R,1,1,:),(L,2,1,:));atol=(R=0.,L=0.,G=0.,C=0.)) + + zero_frequency=LineParameters( + impedance[:, :, 1:1], + admittance[:, :, 1:1], + [0.0] + ) + @test R(zero_frequency, 1, 1, 1) == real(impedance[1, 1, 1]) + @test X(zero_frequency, 1, 1, 1) == imag(impedance[1, 1, 1]) + @test_throws DomainError L(zero_frequency) + @test_throws DomainError L(zero_frequency, 1, 1) + @test_throws DomainError C(zero_frequency, 1, 1, 1) + @test_throws DomainError C(zero_frequency) + dc=ObservedResult(zero_frequency,(R,L,G,C)) + @test all(ismissing,observe(dc,L)) + @test all(ismissing,observe(dc,C)) + @test observe(dc,R)==1000real.(impedance[:,:,1:1]) + + @test parentmodule(which(DataFrame, (typeof(series),))) !== + LineCableModels.ReportBuilder + @test parentmodule(which(DataFrame, (typeof(shunt),))) !== + LineCableModels.ReportBuilder + + @test_throws DimensionMismatch LineParameters( + zeros(ComplexF64, 2, 3, 1), + zeros(ComplexF64, 2, 3, 1), + [50.0] + ) + @test_throws DimensionMismatch LineParameters( + zeros(ComplexF64, 2, 2, 2), + zeros(ComplexF64, 2, 2, 2), + [50.0] + ) + @test_throws ArgumentError LineParameters( + zeros(ComplexF64, 1, 1, 1), + zeros(ComplexF64, 1, 1, 1), + [Inf] + ) +end + +@testitem "UQ / result products / statistical invariants" tags=[:unit, :distributions] setup=[ + UseEngineSupport, TestNumerics, TestFixtures] begin + using Measurements + using Distributions + using Random + using Statistics + + values=[1.0, 2.0, 3.0, 4.0, 5.0] + summary=SampleSummary(values) + @test summary == SampleSummary(3.0, sqrt(2.5), 1.0, 1.2, 3.0, 4.8, 5.0, 5) + @test SampleSummary([1, 2, 3]).mean === 2.0 + @test_throws ArgumentError SampleSummary(Float64[]) + @test_throws ArgumentError SampleSummary([1.0, Inf]) + @test_throws ArgumentError SampleSummary(2.0, -1.0, 1.0, 1.1, 2.0, 2.9, 3.0, 3) + @test_throws ArgumentError SampleSummary(2.0, 1.0, 1.0, 2.1, 2.0, 2.9, 3.0, 3) + @test_throws ArgumentError SampleSummary(2.0, 1.0, 1.0, 1.1, 2.0, 2.9, 3.0, 0) + + density=HistogramDensity([1.0, 3.0, 5.0], [0.25, 0.25]) + @test HistogramDensity([0, 1], [2]) isa HistogramDensity{Float64} + @test pdf(density, 2.0) == 0.25 + @test density(2.0) == 0.25 + @test pdf(density, 6.0) == 0.0 + @test cdf(density, 0.0) == 0.0 + @test cdf(density, 3.0) == 0.5 + @test cdf(density, 6.0) == 1.0 + @test UQ.cumulative_probability(density, 0.0) == 0.0 + @test UQ.cumulative_probability(density, 3.0) == 0.5 + @test UQ.cumulative_probability(density, 6.0) == 1.0 + @test quantile(density, 0.25) == 2.0 + pairs=UQ.quantile_pairs(density, [4.0, 1.0, 3.0, 2.0]) + @test pairs.sample == [1.0, 2.0, 3.0, 4.0] + @test pairs.model == [1.5, 2.5, 3.5, 4.5] + @test pairs.reference == (1.0, 4.5) + @test minimum(density) == 1.0 + @test maximum(density) == 5.0 + @test isfinite(logpdf(density, 2.0)) + @test isfinite(mean(density)) + @test std(density) > 0 + @test !isempty(modes(density)) + @test isfinite(rand(MersenneTwister(91), density)) + @test_throws DomainError quantile(density, -0.1) + @test_throws ArgumentError UQ.quantile_pairs(density, Float64[]) + @test_throws ArgumentError UQ.quantile_pairs(density, [1.0, Inf]) + @test_throws ArgumentError HistogramDensity([0.0, 1.0], Float64[]) + @test_throws ArgumentError HistogramDensity([0.0, 1.0, 2.0], [1.0]) + @test_throws ArgumentError HistogramDensity([0.0, 1.0], [-1.0]) + @test_throws ArgumentError HistogramDensity([0.0, 1.0], [0.0]) + @test_throws ArgumentError HistogramDensity([0.0, 0.0], [1.0]) + @test_throws ArgumentError HistogramDensity([0.0, Inf], [1.0]) + + # The explicit inputs to each channel are distinguishable and control publication and retention. + # No historical model computation supplies these expectations. + raw_samples=(R=reshape([2.0, 3.0, 5.0, 8.0], 1, :), + L=reshape([11.0, 13.0, 17.0, 19.0].*1e-6, 1, :), + C=reshape([23.0, 29.0, 31.0, 37.0].*1e-10, 1, :), + G=reshape([41.0, 43.0, 47.0, 53.0].*1e-9, 1, :)) + retained_samples=vec(raw_samples.R) + retained_mean=sum(retained_samples)/length(retained_samples) + retained_std=sqrt(sum(abs2, retained_samples .- retained_mean)/ + (length(retained_samples)-1)) + expected_edges=[2.0, 5.0, 8.0] + expected_density=[2/4/3, 2/4/3] + complete=MonteCarloResult( + MonteCarlo(Formulation(); trials=4, seed=2027, + return_samples=true, return_histograms=true), + [LineCableModels.materialize(CableConstants(mean(raw_samples.R), mean(raw_samples.L), + mean(raw_samples.C), mean(raw_samples.G)),map(x->[SampleSummary(vec(x))],raw_samples))], + [map(x->[SampleSummary(vec(x))], raw_samples)], + [raw_samples], [map(x->[HistogramDensity(vec(x); bins=2)], raw_samples)], + UInt64(2027), UInt64[2039], [4]) + @test UQ.root_seed(complete) == UInt64(2027) + @test UQ.point_seed(complete, 1) == UInt64(2039) + @test UQ.trial_count(complete, 1) == 4 + @test UQ.confidence(complete) == 0.95 + @test UQ.cdf_tolerance(complete) == 0.02 + @test UQ.sampling_distribution(complete) === :normal + @test_throws BoundsError UQ.point_seed(complete, 2) + @test_throws BoundsError UQ.trial_count(complete, 2) + sample_only=MonteCarloResult( + complete.formulation, + complete.values, + complete.stats, + complete.sample_values, + nothing, + complete.root_seed, + complete.point_seeds, + complete.trial_counts + ) + histogram_only=MonteCarloResult( + complete.formulation, + complete.values, + complete.stats, + nothing, + complete.histogram_values, + complete.root_seed, + complete.point_seeds, + complete.trial_counts + ) + summaries_only=MonteCarloResult( + complete.formulation, + complete.values, + complete.stats, + nothing, + nothing, + complete.root_seed, + complete.point_seeds, + complete.trial_counts + ) + for source in (complete,sample_only,histogram_only,summaries_only) + @test only(observables(source)) isa ObservedResult + end + + @test all(in(observables(typeof(complete))), ( + R, L, C, G, + (statistics, R), (statistics, L), (statistics, C), (statistics, G), + (samples, R), (samples, L), (samples, C), (samples, G), + (histograms, R), (histograms, L), (histograms, C), (histograms, G) + )) + @test nominal(only(@inferred(observe(complete, R, 1)))) == retained_mean + @test only(@inferred(observe(complete, statistics, R, mean, 1))) == retained_mean + @test only(@inferred(observe(complete, statistics, R, std, 1))) ≈ retained_std + retained_histogram=observe(complete, histograms, R, 1, 1) + @test observe(complete, samples, R, 1, 1, :) == retained_samples + @test retained_histogram.edges == expected_edges + @test retained_histogram.density == expected_density + @test_throws BoundsError observe(complete, samples, R, 2, 1, :) + @test_throws BoundsError observe(complete, samples, R, 1, 2, :) + @test_throws ArgumentError observe(histogram_only, samples, R, 1, 1, :) + @test_throws ArgumentError observe(sample_only, histograms, R, 1, 1) + @test_throws DimensionMismatch MonteCarloResult( + complete.formulation, + complete.values, + [(R = summary, L = summary, C = summary, G = summary)], + complete.sample_values, + complete.histogram_values, + complete.root_seed, + complete.point_seeds, + complete.trial_counts + ) + @test_throws DimensionMismatch MonteCarloResult( + complete.formulation, + complete.values, + complete.stats, + [(R = [1.0], L = [1.0], C = [1.0], G = [1.0])], + complete.histogram_values, + complete.root_seed, + complete.point_seeds, + complete.trial_counts + ) + + retained=ObservedResult(complete,1,((statistics,R,mean,1),(samples,R,1,:),(histograms,R,1)); + units=(:milli,:milli,:milli)) + @test retained.quantities[1].quantity==quantity(R) + @test retained.quantities[1].values==retained_mean*1e6 + @test retained.quantities[2].values==retained_samples.*1e6 + @test retained.quantities[3].distribution.edges==expected_edges.*1e6 + @test retained.quantities[3].values.density≈expected_density./1e6 rtol=4eps(Float64) + + summary_product=only(statistics(complete)) + sample_product=only(samples(complete)) + histogram_product=only(histograms(complete)) + @test keys(summary_product) == (:R, :L, :C, :G) + @test keys(sample_product) == (:R, :L, :C, :G) + @test keys(histogram_product) == (:R, :L, :C, :G) + @test !applicable(observe, summary_product, R) + @test !applicable(observe, sample_product, R, :) + @test !applicable(observe, histogram_product, R) + summary_observed=ObservedResult(complete,1,((statistics,R,1),);quantity_units=(R=:milli,)) + histogram_observed=ObservedResult(complete,1,((histograms,R,1),);units=(:milli,)) + @test length(summary_observed.quantities)==7 + summary_factor=scale_factor(R,basis(complete),first(summary_observed.quantities).unit) + @test observe(summary_observed,statistics,R,mean)==summary_product.R[1].mean*summary_factor + @test observe(summary_observed,statistics,R,std)==summary_product.R[1].std*abs(summary_factor) + @test only(histogram_observed.quantities).distribution.edges==histogram_product.R[1].edges.*summary_factor + @test only(histogram_observed.quantities).values.density≈histogram_product.R[1].density./summary_factor rtol=4eps(Float64) + + frequency=[50.0, 100.0] + impedance=fill(1.0e-4+2.0e-4im, 2, 2, 2) + admittance=fill(3.0e-8+4.0e-8im, 2, 2, 2) + parameters=LineParameters(impedance, admittance, frequency) + sample_values=LineCableModels.Commons.initialize_buffers(MonteCarlo(Formulation()), Float64, + parameters, (; trials = 2), (;)) + LineCableModels.UQ._record_sample!(sample_values, parameters, 1, frequency) + LineCableModels.UQ._record_sample!(sample_values, parameters, 2, frequency) + @test @allocated(LineCableModels.UQ._record_sample!(sample_values, parameters, 2, frequency)) == + 0 + @test sample_values.R[1, 1, 1, :] == fill(1.0e-4, 2) + @test sample_values.L[1, 1, 1, :] == + fill(2.0e-4 / (2π * frequency[1]), 2) + + line_summary=fill(SampleSummary([1.0, 2.0]), size(impedance)) + line_histogram=fill(density, size(impedance)) + line_statistics=(R = line_summary, L = line_summary, C = line_summary, G = line_summary) + line_histograms=(R = line_histogram, L = line_histogram, + C = line_histogram, G = line_histogram) + line_result=MonteCarloResult( + complete.formulation, + [LineCableModels.materialize(parameters,line_statistics)], + [line_statistics], + [sample_values], + [line_histograms], + complete.root_seed, + complete.point_seeds, + [2] + ) + @test observe(line_result, samples, R, 1, 1, 1, 1, :) == fill(1.0e-4, 2) + @test observe(line_result, frequencies, 1, :) == frequency + line_observed=ObservedResult(line_result,1) + @test first(line_observed.quantities).coordinates.frequencies==frequency + @test first(line_observed.quantities).coordinates.frequency_unit==LineCableModels.Units.units(:base,:hertz) + malformed_samples=(R = sample_values.R[:, :, 1:1, :], L = sample_values.L, + C = sample_values.C, G = sample_values.G) + @test_throws DimensionMismatch MonteCarloResult( + complete.formulation, + line_result.values, + [line_statistics], + [malformed_samples], + [line_histograms], + complete.root_seed, + complete.point_seeds, + [2] + ) +end diff --git a/test/unit/engine/retained_earth_formulas.jl b/test/unit/engine/retained_earth_formulas.jl new file mode 100644 index 000000000..fdaf0e9e4 --- /dev/null +++ b/test/unit/engine/retained_earth_formulas.jl @@ -0,0 +1,110 @@ +# Scientific real-axis controls survive withdrawal of the built-in author +# implementations. They exercise the generic callable integration service, not +# a substitute formula implementation or regenerated reference expectation. +@testitem "Engine / callable integration / independent Wise real-axis control" tags=[:unit, :engine] begin + using QuadGK + const E=LineCableModels.Engine + controls=(; rtol = 1e-9, atol = 0.0, maxevals = 10^6) + epsilon=8.8541878128e-12 .* [1.0, 10.0]; + mu=fill(4pi*1e-7, 2); + rho=[Inf, 100.0] + for f in (50.0, 10000.0), self in (true, false) + + heights=self ? (1.0, 1.0) : (1.0, 1.5) + y=self ? 0.0 : 0.4; + H=sum(heights) + distance=self ? 0.005 : hypot(y, heights[1]-heights[2]) + references=[setprecision(BigFloat, bits) do + s=2big(pi)*im*f + e0=big"8.8541878128e-12"; + u0=4big(pi)*big"1e-7" + g0=s^2*u0*e0; + g1=s*u0*(big".01"+s*10*e0) + ratio=g1/g0; + scale=sqrt(g1-g0) + cutoff=big(40)/H + kernel=lambda->exp(-H*lambda)*cos(y*lambda)/(ratio*lambda+sqrt(lambda^2+g1-g0)) + integral, + estimate=quadgk(kernel, big"0", abs(scale/ratio), + abs(scale), inv(big(H)), cutoff; + rtol = big"1e-12", maxevals = 10^6) + # Re(ratio)=10 and Re(sqrt(lambda²+g1-g0))>0, so + # |denominator|>=10lambda: this bounds the omitted real-axis tail. + tail=exp(-H*cutoff)/(10H*cutoff) + value=(log(hypot(big(y), big(H))/big(distance))+2integral)/(2big(pi)*e0) + (value = value, bound = (estimate+tail)/(big(pi)*e0)) + end + for bits in (128, 256, 512)] + expected=last(references) + uncertainty=expected.bound+abs(expected.value-references[2].value) + s=2pi*im*f + g0=s^2*mu[1]*epsilon[1] + g1=s*mu[2]*(inv(rho[2])+s*epsilon[2]) + ratio=g1/g0 + scale=sqrt(g1-g0) + kernel=E.SpectralIntegral(lambda -> + exp(-H*lambda)*cos(y*lambda)/(ratio*lambda+sqrt(lambda^2+g1-g0))) + for numerical in (nothing, LineCableModels.Commons.initialize_buffers( + E.SpectralIntegral, Val(:quad), Float64, (;), (;), (;))) + value, estimate=E.integrate(kernel, Val(:quad), controls, numerical; + points=(abs(scale/ratio), abs(scale), inv(H))) + actual=(log(hypot(y,H)/distance)+2value)/(2pi*epsilon[1]) + for component in (real, imag) + budget=1e-6*abs(component(expected.value)) + @test uncertainty<=budget/4 + @test abs(component(actual-expected.value))+uncertainty<=budget + end + end + end +end + +@testitem "Engine / callable integration / independent Carson real-axis control" tags=[:unit, :engine] begin + using QuadGK + const E=LineCableModels.Engine + controls=(; rtol = 1e-9, atol = 0.0, maxevals = 10^6) + rho=[Inf, 100.0]; + epsilon=8.8541878128e-12 .* [1, 10]; + mu=fill(4pi*1e-7, 2) + for f in (50.0, 10000.0), self in (true, false) + + s=2pi*im*f; + heights=self ? (1.0, 1.0) : (1.0, 1.5) + distance=self ? 0.005 : hypot(0.4, 0.5) + y=self ? 0.0 : 0.4; + h=sum(heights); + image_distance=hypot(h, y) + refs=[setprecision(BigFloat, bits) do + sb=2big(pi)*im*f; + mub=4big(pi)*big"1e-7" + k2=sb*mub/big"100"; + hb=BigFloat(h); + yb=BigFloat(y) + cutoff=big"40"/hb + kernel(t)=exp(-hb*t)*cos(yb*t)/(t+sqrt(t^2+k2)) + value, + estimate=quadgk(kernel, big"0", abs(sqrt(k2)), inv(hb), cutoff; + rtol = bits==128 ? big"1e-8" : bits==256 ? big"1e-10" : big"1e-12", maxevals = 10^6) + # Re sqrt(t²+i a)>0, so |denominator|>=t. + tail=exp(-hb*cutoff)/(hb*cutoff) + expected=sb*mub/(2big(pi))*(log(hypot(hb, yb)/BigFloat(distance))+2value) + (value = expected, bound = abs(sb*mub/big(pi))*(estimate+tail)) + end + for bits in (128, 256, 512)] + expected=last(refs).value + uncertainty=last(refs).bound+abs(last(refs).value-refs[2].value) + k2=s*mu[2]/rho[2] + kernel=E.SpectralIntegral(lambda -> + exp(-h*lambda)*cos(y*lambda)/(lambda+sqrt(lambda^2+k2))) + for numerical in (nothing, LineCableModels.Commons.initialize_buffers( + E.SpectralIntegral, Val(:quad), Float64, (;), (;), (;))) + value, estimate=E.integrate(kernel, Val(:quad), controls, numerical; + points=(abs(sqrt(k2)), inv(h))) + actual=s*mu[1]/(2pi)*(log(image_distance/distance)+2value) + for component in (real, imag) + budget=1e-6*abs(component(expected)) + @test uncertainty<=budget/4 + @test abs(component(actual)-component(expected))+uncertainty<=budget + end + end + end +end diff --git a/test/unit/engine/retained_observations.jl b/test/unit/engine/retained_observations.jl new file mode 100644 index 000000000..fda5a0615 --- /dev/null +++ b/test/unit/engine/retained_observations.jl @@ -0,0 +1,198 @@ +@testitem "ObservedResult / retained re-expression preserves scientific records" tags=[:unit, :measurements] begin + using Measurements + using LineCableModels.Commons: observation_product,gridpoint_id + using LineCableModels.Engine: retain_gridpoint,compare + U=LineCableModels.Units + shared=measurement(1.,0.1) + line=LineParameters(fill(complex(shared,2shared),2,2,3),fill(3.0+4im,2,2,3),[10.,100.,1000.]) + result=retain_gridpoint(line,gridpoint_id()) + reference=retain_gridpoint(line,gridpoint_id()) + comparisons=compare(reference,result,[R];bands=(:all,)) + timings=(result_id=LineCableModels.Commons.observation_gridpoint(result).id,seconds=0.5) + observed=ObservedResult(result;comparisons,timings) + repeated=observables(observed) + @test isequal(repeated.quantities,observed.quantities) + @test repeated.quantities[1].values!==observed.quantities[1].values + @test LineCableModels.Commons.observation_quantity(observed,R)==first(observed.quantities) + @test repeated.errors==observed.errors + @test repeated.timings==observed.timings + @test only(observables([observed])).timings==timings + @test only(observables([observed])).errors==comparisons + selected=ObservedResult(observed,((R,[2,1],[2,1],[3,1]),);length_unit=:base,frequency_unit=:kilo) + product=only(selected.quantities) + @test nominal.(product.values)==ones(2,2,2) + prefix_only=ObservedResult(selected;quantity_units=:milli) + @test only(prefix_only.quantities).unit==U.units(:milli,:ohm;per=(:base,:meter)) + length_only=ObservedResult(prefix_only;length_unit=:kilo) + @test only(length_only.quantities).unit==U.units(:milli,:ohm;per=(:kilo,:meter)) + @test product.coordinates.rows==[2,1] + @test product.coordinates.columns==[2,1] + @test product.coordinates.samples==[3,1] + @test product.coordinates.frequencies==[1.,0.01] + restored=ObservedResult(selected;length_unit=:kilo,frequency_unit=:base) + original=observation_product(observed,(R,[2,1],[2,1],[3,1])) + @test only(restored.quantities).values≈original.values + @test only(restored.quantities).coordinates==original.coordinates + @test only(restored.quantities).thresholds.values≈original.thresholds.values + @test only(restored.quantities).thresholds.unit==original.thresholds.unit + for field in (:available,:engineering_zero,:clipped,:missing_reason) + @test isequal(getproperty(only(restored.quantities),field),getproperty(original,field)) + end + @test restored.errors==comparisons && restored.timings==timings + reported=report(observed;values=@observe(R[[2,1],[2,1],[3,1]]),length_unit=:base,frequency_unit=:kilo) + @test reported.observed.errors==comparisons && reported.observed.timings==timings + @test reported.observed.gridpoint==observed.gridpoint + @test only(reported.observed.quantities).coordinates==product.coordinates + @test uncertainty(observe(reported.observed,R)[1]-product.values[1])==0 + @test uncertainty(only(restored.quantities).values[1]-observe(observed,R)[1])==0 + @test uncertainty(only(restored.quantities).values[1])>0 + @test_throws ArgumentError ObservedResult(observed;atol=0) + @test_throws ArgumentError ObservedResult(observed;clip=false) + @test_throws ArgumentError ObservedResult(observed,(L,)) + @test_throws ArgumentError ObservedResult(observed;frequencies=[1.,2.,3.]) + @test_throws ArgumentError ObservedResult(observed;timings=(seconds=5.,)) + @test_throws ArgumentError ObservedResult(observed,(R,);units=(U.units(:base,:farad),)) + polar=ObservedResult(LineParameters(fill(3.0+4im,1,1,2),fill(1.0+2im,1,1,2),[1.,2.]),((Z,abs),(Z,angle))) + angle_unit=only(filter(p -> p.quantity==U.quantity(Z,angle),polar.quantities)).unit + radians=ObservedResult(polar,((Z,angle),);units=(U.units(:base,:radian),)) + degrees=ObservedResult(radians;units=(angle_unit,)) + @test observe(degrees,Z,angle)≈observe(polar,Z,angle) + @test only(radians.quantities).thresholds==only(degrees.quantities).thresholds + phase_product=only(radians.quantities) + malformed=merge(phase_product,(thresholds=merge(phase_product.thresholds,(unit=phase_product.unit,)),)) + @test_throws ArgumentError ObservedResult(radians.gridpoint,[malformed],[],(;)) +end + +@testitem "ObservedResult / constructor rejects malformed scientific records" tags=[:unit, :importexport] begin + using LineCableModels.Commons: observation_product + using LineCableModels.ReportBuilder: tabulate + source=ObservedResult(LineParameters(fill(1.0+2im,2,2,3),fill(3.0+4im,2,2,3),[1.,2.,3.])) + q=first(source.quantities) + make(product)=ObservedResult(source.gridpoint,[product],source.errors,source.timings) + @test_throws ArgumentError make(Base.structdiff(q,(family=nothing,))) + @test_throws ArgumentError make(merge(q,(family="Z",))) + @test_throws ArgumentError make(merge(q,(unit=LineCableModels.Units.units(:base,:farad),))) + @test_throws DimensionMismatch make(merge(q,(values=ones(2,2,2),))) + @test_throws DimensionMismatch make(merge(q,(values=ones(3,2,2),))) + @test_throws DimensionMismatch make(merge(q,(available=trues(1,1,3),))) + @test_throws ArgumentError make(merge(q,(coordinates=merge(q.coordinates,(rows=[1,1],)),))) + @test_throws ArgumentError make(merge(q,(coordinates=merge(q.coordinates,(rows=[1,3],)),))) + @test_throws DimensionMismatch make(merge(q,(coordinates=merge(q.coordinates,(frequencies=[1.,2.],)),))) + @test_throws ArgumentError make(merge(q,(coordinates=merge(q.coordinates,(frequency_unit=q.unit,)),))) + @test_throws ArgumentError ObservedResult(source.gridpoint,[q,q],[],(;)) + @test_throws ArgumentError make(merge(q,(thresholds=merge(q.thresholds,(values=-1.,)),))) + @test_throws DimensionMismatch make(merge(q,(thresholds=merge(q.thresholds,(values=[0.,0.],)),))) + @test_throws ArgumentError make(merge(q,(coordinates=merge(q.coordinates,(indices=([2,1],:,:),)),))) + encoded=LineCableModels.ImportExport.serialize_value(source) + restored=LineCableModels.ImportExport.deserialize_value(encoded) + @test tabulate(restored).Z.R==tabulate(source).Z.R + # Restoration applies the same validation as native construction. + fields=encoded["payload"]["fields"]["values"] + record=fields[2]["values"][1] + coordinate=record["values"][findfirst(==("coordinates"),record["names"])] + coordinate["values"][findfirst(==("kind"),coordinate["names"])]=LineCableModels.ImportExport.serialize_value(:unknown) + @test_throws ArgumentError LineCableModels.ImportExport.deserialize_value(encoded) +end + +@testitem "ObservedResult / coordinate and unit interpretation is shared" tags=[:unit, :engine] begin + using LineCableModels.Commons: observation_product + z=reshape(complex.(1.:12.,101.:112.),2,2,3) + line=LineParameters(z,2z,[1.,10.,100.]) + a=ObservedResult(line;length_unit=:base) + b=ObservedResult(line,((R,[2,1],[2,1],[1,3]),(X,[2,1],[2,1],[1,3]), + (G,[2,1],[2,1],:),(B,[2,1],[2,1],:));length_unit=:kilo,frequency_unit=:kilo) + products=observation_product((a,b),R) + @test products[2].unit==products[1].unit + @test products[2].coordinates.rows==products[2].coordinates.columns==[1,2] + @test products[2].coordinates.samples==[1,3] + @test products[2].coordinates.frequencies==[1.,100.] + @test products[2].values≈products[1].values[:,:,[1,3]] + @test b.quantities[1].coordinates.rows==[2,1] + @test b.quantities[1].unit!=a.quantities[1].unit + incompatible=ObservedResult(b,((R,[1],:,:),)) + @test_throws DimensionMismatch observation_product((a,incompatible),R) +end + +@testitem "ObservedResult / table definitions retain one hierarchy" tags=[:unit, :report] begin + using DataFrames + using LineCableModels.ReportBuilder: tabulate,LineParametersTableDefinition + source=LineParameters(fill(1.0+2im,2,2,3),fill(3.0+4im,2,2,3),[1.,2.,3.]) + observed=ObservedResult(source) + all_tables=tabulate(observed) + for input in (source,observed) + selected=report(TableReportDefinition((R,)),input).tables + @test keys(selected)==(:Z,) + @test keys(selected.Z)==(:R,) + @test selected.Z.R==all_tables.Z.R + end + for input in (source,observed) + selected=report(LineParametersTableDefinition((R,)),input).tables + @test keys(selected)==(:Z,) + @test keys(selected.Z)==(:R,) + @test selected.Z.R==all_tables.Z.R + end + @test_throws r"quantity tables.*tabulate" DataFrame(observed) + constants=CableConstants([:core,:sheath],[1.,2.],[3.,4.],[5.,6.],[7.,8.],60.) + table=tabulate(ObservedResult(constants;length_unit=:base,quantity_units=:base),R) + @test names(table)==["frequency","core","sheath"] + @test size(table)==(1,3) + @test collect(table[1,:])==[60.,1.,2.] +end + +@testitem "ObservedResult / structured labels and equivalence conflicts" tags=[:unit, :parametric] setup=[TestFixtures] begin + using LineCableModels.Commons: observation_labels,observation_groups,gridpoint_id + using LineCableModels.Engine: retain_gridpoint,completed_inputs,completed_formulation + problem=TestFixtures.line_parameters_problem() + inputs=completed_inputs(problem) + radius=inputs.system.designs[1].origin.items[1].item.items[1].primitive + @test radius.field_descriptions.r==(name="radius",unit="m") + @test inputs.earth_props.layers[2].field_descriptions.rho==(name="electrical resistivity",unit="Ω·m") + base=LineParameters(fill(1.0+2im,1,1,2),fill(3.0+4im,1,1,2),[1.,2.]) + source_id=gridpoint_id().source_id + function point(index,rho,r;selection=Formulation(),value=base) + # Use metadata supplied by the physical owners, even after detachment. + declared=(soil=(rho=rho,field_descriptions=inputs.earth_props.layers[2].field_descriptions), + conductor=(r=r,field_descriptions=radius.field_descriptions)) + ObservedResult(retain_gridpoint(value,gridpoint_id(;source_id,problem_index=index); + fields=merge(completed_formulation(selection),(inputs=declared,)))) + end + points=[point(1,100.,.005),point(2,200.,.01)] + labels=observation_labels(points;request=R) + @test occursin("electrical resistivity=200.0 Ω·m",labels[2]) + @test occursin("radius=0.01 m",labels[2]) + @test !occursin("earth Z",labels[2]) + other=point(1,100.,.005;selection=Formulation(earth_impedance=:pollaczek1926)) + labels=observation_labels([points[1],other];request=R) + @test occursin(description(Formulation().methods.earth_impedance;compact=true),labels[1]) + @test occursin(description(Formulation(earth_impedance=:pollaczek1926).methods.earth_impedance;compact=true),labels[2]) + @test !occursin("internal Z",labels[1]) + @test !occursin("radius",labels[1]) + with_external=observation_labels([points[1],other,ObservedResult(base)];request=R) + @test all(label -> !occursin("internal Z",label),with_external) + @test occursin(description(Formulation().methods.earth_impedance;compact=true),with_external[1]) + @test occursin(description(Formulation(earth_impedance=:pollaczek1926).methods.earth_impedance;compact=true),with_external[2]) + fields=points[1].gridpoint.formulation_fields + altered=merge(first(fields.Z),(summary="owned; punctuation",)) + retained=merge(points[1].gridpoint,(formulation_fields=merge(fields,(Z=[altered;fields.Z[2:end]],)),)) + label=only(observation_labels(ObservedResult(retained,points[1].quantities,[],(;));request=R)) + @test occursin("owned; punctuation",label) + conflicting=point(1,100.,.005;value=LineParameters(fill(2.0+2im,1,1,2),base.Y,[1.,2.])) + @test_throws r"conflicting numerical values" observation_groups([points[1],conflicting];request=R) + @test length(observation_groups(points;request=R))==2 +end + +@testitem "ObservedResult / re-expression preserves retained arbitrary precision" tags=[:unit, :engine] begin + U=LineCableModels.Units + original=setprecision(BigFloat,384) do + source=LineParameters(fill(complex(big"1.234567890123456789012345678901",big"2"),1,1,2), + fill(complex(big"3",big"4"),1,1,2),BigFloat[1,2]) + ObservedResult(source;length_unit=:base) + end + converted=setprecision(BigFloat,128) do + ObservedResult(original,(R,);units=(U.units(:base,:ohm;per=(:kilo,:meter)),)) + end + @test precision(only(converted.quantities).values[1])==384 + setprecision(BigFloat,384) do + @test only(converted.quantities).values[1]==observe(original,R)[1]*1000 + end +end diff --git a/test/unit/engine/spectral_integration.jl b/test/unit/engine/spectral_integration.jl new file mode 100644 index 000000000..4b6acbc09 --- /dev/null +++ b/test/unit/engine/spectral_integration.jl @@ -0,0 +1,164 @@ +@testitem "Engine / callable-only QuadGK integration and scalar behavior" tags=[:unit, :engine] begin + const E=LineCableModels.Engine + options(; + controls...)=E.formulation_options(E.SpectralIntegral, + (method = :quad, options = (; controls...))) + calls=Ref(0) + f=x->begin + calls[]+=1 + (1+im)*exp(-(1.5+im)*x)*cos(x) + end + integral=E.SpectralIntegral(f) + @test calls[]==0 + @test integral.f===f + settings=options() + @test settings.method===Val(:quad) + actual, error=E.integrate(integral, settings.method, settings.options) + exact=(1+im)*(1.5+im)/((1.5+im)^2+1) + @test actual≈exact rtol=1e-8 + @test error>=0 && isfinite(error) + @test_throws ArgumentError E.formulation_options(E.SpectralIntegral, (method = :unknown,)) + for controls in ((max_terms = 10,), (rtol = -1,), (maxevals = 0,), (maxevals = true,), + (rtol = 0, atol = 0), (samples = 16,)) + @test_throws ArgumentError options(; controls...) + end + for method in (:trapz, :cim) + @test_throws ArgumentError E.formulation_options(E.SpectralIntegral, (; method)) + end + for T in (Float32, Float64, BigFloat) + scalar_settings=options(rtol = T===Float32 ? 1e-5 : 1e-9) + scalar_integral=E.SpectralIntegral(x->complex(exp(-2x)*cos(x))) + scalar_value, + scalar_error=E.integrate( + scalar_integral, scalar_settings.method, scalar_settings.options; + coordinate_type = T, points = T[1]) + @test scalar_value isa Complex{T} + @test scalar_error isa T + @test scalar_value≈T(2)/T(5) rtol=2e-5 + end + @test_throws DomainError E.integrate(E.SpectralIntegral(x->NaN), Val(:quad), settings.options) +end + +@testitem "Engine / real-only callable struct and complete transformed integrands" tags=[:unit, :engine] begin + const E=LineCableModels.Engine + struct RealKernel + rate::Float64 + end + (k::RealKernel)(x::Real)=exp(-k.rate*x) + controls=(rtol = 1e-10, atol = 0.0, maxevals = 100000) + value, error=E.integrate(E.SpectralIntegral(RealKernel(2.0)), Val(:quad), controls) + @test value isa Float64 + @test value≈0.5 rtol=1e-10 + @test error<=controls.rtol*abs(value) + for c in (0.1, 2.0, 3cis(0.2)) + f=t->c*exp(-2c*t) + transformed, + estimate=E.integrate(E.SpectralIntegral(f), Val(:quad), controls; points = (1.0,)) + @test transformed≈0.5 rtol=1e-10 + @test estimate<=controls.rtol*abs(transformed) + end +end + +@testitem "Engine / QuadGK reports an unmet target without retry or rejection" tags=[:unit, :engine] begin + using QuadGK: quadgk + const E=LineCableModels.Engine + calls=Ref(0) + f=x->begin + calls[]+=1 + exp(-x)*cos(40x) + end + controls=(rtol = 1e-13, atol = 0.0, maxevals = 15) + actual=@test_logs (:warn, r"QuadGK returned an estimated error") E.integrate( + E.SpectralIntegral(f), Val(:quad), controls; points = (1.0,)) + work=calls[] + calls[]=0 + mapped=t->begin + den=inv(1-t) + f(t*den)*den^2 + end + direct=quadgk(mapped, 0.0, 0.5, 1.0; controls..., norm = E.numerical_magnitude) + @test work==calls[] + @test actual==direct + @test isfinite(first(actual)) + @test last(actual)>controls.rtol*abs(first(actual)) +end + +@testitem "Engine / complete contour integrands preserve half-line endpoints" tags=[:unit, :engine] begin + const E=LineCableModels.Engine + controls=(rtol = 1e-10, atol = 0.0, maxevals = 100000) + # The rational map has a nonzero limiting value at the infinite endpoint. + for angle in (0.0, 0.2), points in ((), Float64[]) + + c=cis(angle) + integral=E.SpectralIntegral(t->c/(1+c*t)^2) + value, error=E.integrate(integral, Val(:quad), controls; points) + @test value≈1 rtol=1e-10 + @test abs(value-1)<=error+4eps(Float64) + end +end + +@testitem "Engine / quadrature normalizes numerical points and reuses storage" tags=[:unit, :engine] begin + const E=LineCableModels.Engine + for T in (Float32, Float64, BigFloat) + controls=(rtol = T===Float32 ? 1e-5 : 1e-9, atol = 0.0, maxevals = 100000) + points=T[2, 0, 1, 2, 1] + retained=copy(points) + integral=E.SpectralIntegral(x->complex(exp(-x))) + buffers=LineCableModels.Commons.initialize_buffers( + E.SpectralIntegral, Val(:quad), T, (;), (;), (;)) + expected=E.integrate(integral, Val(:quad), controls; points, coordinate_type = T) + for _ in 1:2 + value, error=E.integrate(integral, Val(:quad), controls, buffers; points) + @test value isa Complex{T} + @test value≈one(T) rtol=controls.rtol + @test value≈first(expected) rtol=controls.rtol + @test error<=controls.rtol*abs(value) + @test buffers.quadrature.points==T[0, 1, 2] + end + @test points==retained + for invalid in (T[-1], T[Inf], T[NaN], [1im]) + @test_throws DomainError E.integrate( + integral, Val(:quad), controls, buffers; points = invalid) + end + end +end + +@testitem "Engine / complete oscillatory and Bessel integrands" tags=[:unit, :engine] begin + using SpecialFunctions: besselj + const E=LineCableModels.Engine + controls=(rtol = 1e-7, atol = 0.0, maxevals = 1000000) + oscillatory=E.SpectralIntegral(x->complex(exp(-x)*cos(50x))) + value, error=E.integrate(oscillatory, Val(:quad), controls; points = (1.0,)) + exact=complex(inv(1+50.0^2)) + @test value≈exact rtol=1e-7 + @test error<=controls.rtol*abs(value) + @test abs(value-exact)<=max(error, 8eps(Float64)*abs(exact)) + bessel=E.SpectralIntegral(x->complex(exp(-x)*besselj(0, 0.3x))) + @test first(E.integrate(bessel, Val(:quad), controls))≈inv(sqrt(1+0.3^2)) rtol=1e-7 + # The zero-radius identity belongs to the supplied expression, not to a + # special numerical-executor branch. + f=x->exp(-x)*cos(0.2x)/complex(x+0.01) + zero_radius=E.SpectralIntegral(x->f(x)*besselj(0, zero(x))) + @test first(E.integrate(zero_radius, Val(:quad), controls; points = (0.01, 1.0))) ≈ + first(E.integrate(E.SpectralIntegral(f), Val(:quad), controls; points = ( + 0.01, 1.0))) rtol=1e-7 +end + +@testitem "Engine / explicit numerical points resolve displaced narrow peaks" tags=[:unit, :engine] begin + const E=LineCableModels.Engine + controls=(rtol = 1e-7, atol = 0.0, maxevals = 1000000) + for center in (0.371, 9.001), amplitude in (1.0, 1e-8) + + width=center<1 ? 0.003 : 0.0001 + points=[0.0; center .+ width .* [-8, -2, -1, 0, 1, 2, 8]; 1.0] + integral=E.SpectralIntegral(x->complex(amplitude)*exp(-((x-center)/width)^2-x)) + expected=amplitude*sqrt(pi)*width*exp(-center+width^2/4) + actual, _=E.integrate(integral, Val(:quad), controls; points) + @test actual≈expected rtol=1e-6 + end + # A vanishing endpoint is not a license to discard a nonzero integral. + value, + _=E.integrate(E.SpectralIntegral(x->complex(x)*exp(-2x)), Val(:quad), + controls; points = (0.0, 1.0, 2.0)) + @test value≈0.25 rtol=1e-5 +end diff --git a/test/unit/engine/spectral_uncertainty.jl b/test/unit/engine/spectral_uncertainty.jl new file mode 100644 index 000000000..ad4ca8648 --- /dev/null +++ b/test/unit/engine/spectral_uncertainty.jl @@ -0,0 +1,49 @@ +@testitem "Engine / spectral quadrature preserves correlated physical uncertainty" tags=[:unit, :measurements] setup=[UnifiedFormulaFixtures] begin + using Measurements + const E=LineCableModels.Engine + a=measurement(2.0, 0.02) + rate=complex(3+0.2a, one(a)) + amplitude=complex(a, 0.3a) + exact=amplitude/rate + integral=E.SpectralIntegral(λ->amplitude*exp(-rate*λ)) + for method in (:quad,) + controls=E.formulation_options(E.SpectralIntegral, ( + method, options = (rtol = 1e-9,))) + value, + error=E.integrate(integral, controls.method, controls.options; points = (0.0, 1.0)) + @test value isa Complex{Measurement{Float64}} + @test error isa Float64 + @test E.numerical_magnitude(value-exact)<1e-9*E.numerical_magnitude(exact) + for component in (real, imag) + actual=Measurements.derivative(component(value), a) + expected=Measurements.derivative(component(exact), a) + @test actual≈expected rtol=1e-8 + end + end + # A zero nominal value must not erase the derivative during refinement. + perturbation=measurement(0.0, 0.1) + zero_mean=E.SpectralIntegral(λ->complex(perturbation)*exp(-(2+im)*λ)) + for method in (:quad,) + controls=E.formulation_options(E.SpectralIntegral, ( + method, options = (rtol = 1e-8,))) + actual, + _=E.integrate(zero_mean, controls.method, controls.options; points = (0.0, 1.0)) + expected=complex(perturbation)/(2+im) + @test E.numerical_magnitude(actual-expected)<1e-9 + end + # Equal nominal media with independent uncertainties have no nominal + # denominator pole. Metadata must not divide by their uncertain zero. + sigma=[measurement(0.1, 0.001), measurement(0.1, 0.002)] + s=complex(0.0, 2pi*1e4) + epsilon=fill(8.8541878128e-12, 2) + mu=fill(4pi*1e-7, 2) + geometry=(horizontal = measurement.([0.0, 1.0]), + height = measurement.([1.0, -1.0]), radius = measurement.([0.02, 0.03])) + state=UnifiedFormulaFixtures.field_state(geometry, ( + jω = s, Γ = zero(s), sigma, epsilon, mu)) + @test E.earth_denominator_pole(state)===nothing + angle=E.earth_contour_angle(state, pi/6) + @test all(isfinite, + E.earth_spectral_points!(UnifiedFormulaFixtures.buffers(geometry).earth_spectrum, + state, 2.0, 1.0, 0.0, angle)) +end diff --git a/test/unit/engine/transforms.jl b/test/unit/engine/transforms.jl new file mode 100644 index 000000000..48519ba5f --- /dev/null +++ b/test/unit/engine/transforms.jl @@ -0,0 +1,191 @@ +@testitem "ModalAnalysis / uncertain matrices / inferred round trip" tags=[:unit, :measurements] setup=[ + UseEngineSupport, + TestNumerics +] begin + using Measurements + using Test + + impedance=Array{Complex{Measurement{Float64}}, 3}(undef, 2, 2, 1) + admittance=similar(impedance) + for column in 1:2, row in 1:2 + + diagonal=row==column + impedance[row, column, 1]=complex( + measurement(diagonal ? 3.0 : 1.0, 1e-3), + measurement(diagonal ? 5.0 : 0.5, 1e-3) + ) + admittance[row, column, 1]=complex( + measurement(diagonal ? 3e-9 : 1e-9, 1e-12), + measurement(diagonal ? 5e-9 : 0.5e-9, 1e-12) + ) + end + parameters=LineParameters(PhaseDomain, impedance, admittance, [50.0]) + modal=@inferred compute( + ModalAnalysisProblem(parameters), + ModalAnalysisFormulation(:default) + ) + rebuilt=LineCableModels.ModalAnalysis.transform(PhaseDomain,modal) + + @test eltype(modal) === Complex{Measurement{Float64}} + @test TestNumerics.isapprox_scaled( + Measurements.value.(real.(rebuilt.Z.values)), + Measurements.value.(real.(impedance)) + ) + @test any(!iszero, Measurements.uncertainty.(real.(modal.Z.values))) + @test any(!iszero, Measurements.uncertainty.(real.(gamma(modal)))) + segment=PropagationParameters(modal;line_length=10.0) + @test any(!iszero, Measurements.uncertainty.(real.(H(segment)))) +end + +@testitem "ModalAnalysis / tracked eigensystems / numerical invariants" tags=[:unit, :engine] setup=[ + UseEngineSupport, + TestNumerics, + TestAssertions +] begin + using LinearAlgebra + const ModalAnalysis=LineCableModels.ModalAnalysis + + descriptions=(default = "Chrysochos et al. Levenberg–Marquardt modal transformation (2014)",) + + frequencies=[50.0, 100.0] + impedance=zeros(ComplexF64, 2, 2, 2) + admittance=zeros(ComplexF64, 2, 2, 2) + impedance[:, :, 1]=[2+4im 0.2+0.1im; 0.2+0.1im 3+5im] + impedance[:, :, 2]=[2.2+8im 0.21+0.2im; 0.21+0.2im 3.2+10im] + admittance[:, :, 1]=[4+8im -0.5-1im; -0.5-1im 5+9im] .* 1e-9 + admittance[:, :, 2]=[4.2+16im -0.55-2im; -0.55-2im 5.2+18im] .* 1e-9 + parameters=LineParameters(PhaseDomain, impedance, admittance, frequencies) + + for identifier in keys(descriptions) + formulation=ModalAnalysisFormulation(identifier) + @test formula_id(formulation) === :modal + @test formula_id(formulation.formula) === :chrysochos2014 + @test description(formulation.formula) == descriptions[identifier] + transformed=@inferred compute( + ModalAnalysisProblem(parameters), + formulation + ) + maps=operators(transformed) + @test size(maps.Tv) == (2, 2, 2) + @test size(maps.Ti) == (2, 2, 2) + @test domain(transformed) === ModalDomain + @test TestAssertions.all_finite(maps.Tv) + @test TestAssertions.all_finite(maps.Ti) + @test TestAssertions.all_finite(transformed.Z.values) + @test TestAssertions.all_finite(transformed.Y.values) + for frequency_index in eachindex(frequencies) + basis_matrix=maps.Ti[:, :, frequency_index] + @test abs(det(basis_matrix)) > sqrt(eps(Float64)) + for matrix in (transformed.Z.values, transformed.Y.values) + slice=@view matrix[:, :, frequency_index] + @test norm(slice-Diagonal(diag(slice))) <= + 1e-6*max(norm(slice), eps(Float64)) + end + end + rebuilt=LineCableModels.ModalAnalysis.transform(PhaseDomain,transformed) + @test rebuilt.Z.values ≈ impedance rtol = 1e-6 + @test rebuilt.Y.values ≈ admittance rtol = 1e-6 + end + + modal=compute( + ModalAnalysisProblem(parameters), + ModalAnalysisFormulation(:default) + ) + roots=ModalAnalysis.gamma(modal) + @test size(roots)==(2,length(frequencies)) + @test all(isfinite,roots) + @test size(Zc(modal))==size(roots) + @test size(Yc(modal))==size(roots) + @test size(Zc(modal,PhaseDomain))==size(impedance) + @test size(Yc(modal,PhaseDomain))==size(impedance) + assignment=LineCableModels.Commons.initialize_buffers(ModalAnalysis.Formula(:chrysochos2014), + ComplexF64,(;),(; n = 3, nf = 1),(;)).eigenpair_assignment + @test ModalAnalysis.hungarian_assignment!( + [4.0 1.0 3.0; 2.0 0.0 5.0; 3.0 2.0 2.0],assignment) == + [2, 1, 3] + + smooth_frequencies=collect(10.0 .^ range(1, 4; length = 17)) + smooth_impedance=zeros(ComplexF64, 2, 2, length(smooth_frequencies)) + smooth_admittance=similar(smooth_impedance) + for frequency_index in eachindex(smooth_frequencies) + angle=(frequency_index-1)/(length(smooth_frequencies)-1)*(pi/3) + basis_matrix=[cos(angle) -sin(angle); sin(angle) cos(angle)] + modal_impedance=Diagonal(ComplexF64[ + 2 + 0.01frequency_index + 3im, + 4 + 0.02frequency_index + 5im +]) + modal_admittance=Diagonal(ComplexF64[ + (4 + 0.01frequency_index) + 8im, + (8 + 0.02frequency_index) + 12im + ] .* 1e-9) + smooth_impedance[:, :, frequency_index]=basis_matrix*modal_impedance*transpose(basis_matrix) + smooth_admittance[:, :, frequency_index]=basis_matrix*modal_admittance*transpose(basis_matrix) + end + smooth_parameters=LineParameters( + PhaseDomain, + smooth_impedance, + smooth_admittance, + smooth_frequencies + ) + for identifier in keys(descriptions) + transformed=compute( + ModalAnalysisProblem(smooth_parameters), + ModalAnalysisFormulation(identifier) + ) + maps=operators(transformed) + for frequency_index in 2:length(smooth_frequencies), mode in 1:2 + + previous=@view maps.Ti[:, mode, frequency_index - 1] + current=@view maps.Ti[:, mode, frequency_index] + overlap=abs(dot(previous, current))/(norm(previous)*norm(current)) + @test overlap > 0.99 + end + end + + for controls in ((max_iterations = 0,), (convergence = -1,)) + @test_throws ArgumentError ModalAnalysisFormulation( + formula(:default; options = (iteration = controls,))) + end +end + +@testitem "ModalAnalysis / independent maps preserve ordered nonreciprocal entries" tags=[:unit, :engine] setup=[ModalFormulaFixtures] begin + const TR = LineCableModels.ModalAnalysis + const Expression = LineCableModels.Expression + Z = reshape(ComplexF64[2+3im 0.2+0.1im; 0.7+0.3im 4+5im], 2, 2, 1) + Y = reshape(ComplexF64[2+6im -0.4-0.5im; -0.2-0.1im 3+8im] .* 1e-9, 2, 2, 1) + A = ComplexF64[1 0.3; 0.1im 2] + B = ComplexF64[2 0.2im; 0.4 1] + selected=ModalFormulaFixtures.FixedModalMaps(reshape(copy(A),2,2,1),reshape(copy(B),2,2,1)) + phase = LineParameters(PhaseDomain, Z, Y, [50.0]) + modal = compute(ModalAnalysisProblem(phase), + ModalAnalysisFormulation(selected); + options = (offdiagonal_tolerance = 2.0,)) + @test modal.Z.values[:, :, 1] ≈ A \ (Z[:, :, 1] * B) + @test modal.Y.values[:, :, 1] ≈ B \ (Y[:, :, 1] * A) + @test details(modal).data.modal.requested.identifier === :FixedModalMaps + @test details(modal).data.modal.effective.identifier === :FixedModalMaps + @test details(modal).data.modal.identifier === :FixedModalMaps + rebuilt = LineCableModels.ModalAnalysis.transform(PhaseDomain,modal) + @test rebuilt.Z.values ≈ Z + @test rebuilt.Y.values ≈ Y + @test phase.Z.values == Z && phase.Y.values == Y +end + +@testitem "ModalAnalysis / current commuting modes / eigenvalues and reconstruction" tags=[:unit, :engine] begin + using LinearAlgebra + f=[10.0,100.0,1000.0];theta=pi/6 + q=[cos(theta) -sin(theta);sin(theta) cos(theta)] + zs=[q*Diagonal([1+s*1e-3,3+s*2e-3])*transpose(q) for s in 2pi*im.*f] + ys=[q*Diagonal([1e-6+s*1e-9,3e-6+s*2e-9])*transpose(q) for s in 2pi*im.*f] + source=LineParameters(cat(zs...;dims=3),cat(ys...;dims=3),f) + modes=compute(ModalAnalysisProblem(source),ModalAnalysisFormulation()) + restored=LineCableModels.ModalAnalysis.transform(PhaseDomain,modes) + @test Z(restored) ≈ Z(source) rtol=1e-10 + @test Y(restored) ≈ Y(source) rtol=1e-10 + for k in eachindex(f) + s=2pi*im*f[k] + expected=[(1+s*1e-3)*(1e-6+s*1e-9),(3+s*2e-3)*(3e-6+s*2e-9)] + actual=eigvals(Z(modes)[:,:,k]*Y(modes)[:,:,k]) + @test sort(actual;by=abs) ≈ sort(expected;by=abs) rtol=1e-10 + end +end diff --git a/test/unit/engine/unified_earth_return.jl b/test/unit/engine/unified_earth_return.jl new file mode 100644 index 000000000..ec74d4e87 --- /dev/null +++ b/test/unit/engine/unified_earth_return.jl @@ -0,0 +1,561 @@ +# Enclosed-current matrix identities check algebra and conventions. Independent +# cylindrical controls below provide physical validation for equal media. +@testitem "Engine / unified retains five-layout FEM baseline agreement" tags=[:unit, :parametric] setup=[TestFixtures] begin + root=joinpath(pkgdir(LineCableModels), "test/fixtures/reference/three_bare_wires", + "capture-20260917T102438-Nt9nic") + selected=Formulation( + earth_impedance = formula(:unified), earth_admittance = formula(:unified), + options = ( + reduce_bundle = false, kron_reduction = false, ideal_transposition = false)) + for layout in (:all_air, :all_earth, :air_1, :air_2, :air_3) + directory=joinpath(root, "three_bare_wires_$layout") + # The original problem JSON is historical input with problem-owned Γ. + # Rebuild the same physical fixture. The retained numerical CSVs remain fixed. + problem=TestFixtures.three_bare_wires_problem( + heights = TestFixtures.three_bare_wires_layouts[layout]) + @test problem.frequencies==10.0 .^ (-1:7) + result=compute(problem, selected; options = (verbosity = (default = 0,),)) + for (name, selector) in (("Z", Z), ("Y", Y)) + rows=map( + backend->split.(readlines(joinpath(directory, backend, "$name.csv"))[2:end], ','), + ("unified", "fem")) + @test all(a[1:3]==b[1:3] for (a, b) in zip(rows...)) + old, + fem=map(rs->[complex(parse(Float64, r[4]), parse(Float64, r[5])) for r in rs], rows) + values=observe(result, selector) + actual=[values[i, j, k] for k in 1:9 for i in 1:3 for j in 1:3] + @test length(actual)==length(old)==length(fem)==81 + # Preserve the observed worst discrepancy. This does not claim + # exact FEM agreement or impose a solver-side acceptance test. + roundoff=64eps(Float64)*max(maximum(abs, old), maximum(abs, fem)) + @test maximum(abs.(actual-fem))<=maximum(abs.(old-fem))+roundoff + # With Γ=0 the reference correction does not change Z. + name=="Z"&&(@test actual≈old rtol=1e-11 atol=1e-13) + end + end +end + +@testitem "Engine / full earth / enclosed-current matrix identities across layouts" tags=[:unit, :parametric] setup=[UnifiedFormulaFixtures] begin + using LinearAlgebra + const E=LineCableModels.Engine + layouts=([(0.0, -1.0)], [(0.0, 1.0), (0.4, 1.5)], + [(0.0, -1.0), (0.4, -1.5)], [(0.0, 1.0), (0.4, -1.0), (0.9, -1.5)]) + for positions in layouts, f in (50.0, 10000.0) + + n=length(positions) + geometry=(horizontal = first.(positions), height = last.(positions), + radius = [0.005, 0.007, 0.009][1:n]) + for gamma in (n==3 ? (0im, 1e-4*(1+im)) : (0im,)) + s=2pi*im*f; + sigma=[0.0, 0.01]; + epsilon=8.8541878128e-12 .* [1, 10]; + mu=fill(4pi*1e-7, 2) + state=(jω = s, Γ = gamma, sigma, epsilon, mu) + @testset "n=$n / $f Hz / gamma=$gamma / $method" for method in (:quad,) + limits=(; maxevals = 10^6) + controls=E.formulation_options( + E.SpectralIntegral, (method, options = merge((rtol = 1e-9,), limits))) + w=UnifiedFormulaFixtures.calculate(geometry, state, controls) + # Normalized backward errors use independent operand norms. + for (lhs, rhs, scale) in ( + (w.enclosed_potential*w.current_map, + w.source_potential, + norm(w.enclosed_potential)*norm(w.current_map)+norm(w.source_potential)), + (w.enclosed_impedance*w.current_map, + w.axial_field+gamma^2*w.source_potential/s, + norm(w.enclosed_impedance)*norm(w.current_map)+norm(w.axial_field)+abs(gamma^2/s)*norm(w.source_potential)), + ((s*inv(w.enclosed_potential))*w.source_potential, s*w.current_map, + norm((s*inv(w.enclosed_potential)))*norm(w.source_potential)+abs(s)*norm(w.current_map))) + @test norm(lhs-rhs)/scale <= 1e-10 + end + end + end + end +end + +@testitem "Engine / manuscript geometry and reference identities" tags=[:unit, :parametric] setup=[UnifiedFormulaFixtures] begin + using LinearAlgebra + const E=LineCableModels.Engine + metal=Material(:conductor, 1.7e-8) + design=build(CableDesign, "clearance", terminal(:core, core(metal; r = 0.02))) + crossing=build(LineCableSystem, [design], [Pose2(0.0, -0.01)]; connections = [Dict(:core=>1)]) + @test_throws DomainError LineParametersProblem( + crossing; earth_props = homogeneous(rho = 100.0), frequencies = [50.0]) + @test_throws DomainError build(LineCableSystem, [design, design], + [Pose2(0.0, -1.0), Pose2(0.01, -1.0)]; connections = [ + Dict(:core=>1), Dict(:core=>2)]) + geometry=(horizontal = [0.0, 1.0, 2.0], height = [1.2, -0.9, -1.4], + radius = [ + 0.01, 0.025, 0.04]) + s=complex(0.0, 2pi*1e4) + Γ=complex(1e-4, 2e-4) + sigma=[0.0, 0.1] + epsilon=8.8541878128e-12 .* [1.0, 8.0] + mu=4pi*1e-7 .* [1.0, 3.0] + state=(jω = s, Γ, sigma, epsilon, mu) + integration=E.formulation_options(E.SpectralIntegral, ( + method = :quad, options = (rtol = 1e-9,))) + @test_throws ArgumentError E.EarthImpedance.Formula(:unified; parameters = (reference = :deep,)) + @test_throws ArgumentError E.EarthAdmittance.Formula(:unified; parameters = (reference = :interface,)) + # Identical media erase the interface. The unbounded-medium direct field + # provides a closed-form consistency check using production special functions. + sigma=fill(0.1, 2) + epsilon=fill(8.8541878128e-12, 2) + mu=fill(4pi*1e-7, 2) + state=(jω = s, Γ = zero(s), sigma, epsilon, mu) + free=UnifiedFormulaFixtures.calculate(geometry, state, integration) + u=UnifiedFormulaFixtures.field_state(geometry, state) + k=u.k[1] + for p in 1:3, q in 1:3 + + D=p==q ? geometry.radius[p] : + hypot(geometry.horizontal[p]-geometry.horizontal[q], geometry.height[p]-geometry.height[q]) + trace=E.special_besselk(0, k*D)*exp(u.source_logscale[q]) + p==q||(trace*=u.circumference_average[p]*exp(u.source_logscale[p])) + @test free.axial_field[p, q]≈s*mu[1]/(2pi)*trace rtol=1e-8 + geometry.height[p]<0&&(@test free.source_potential[p, q]≈s/(2pi*u.sh[1])*trace rtol=1e-8) + end + # Large electrical sizes require the spectral decay and analytic weight + # in one exponential. This is a consistency check with the production K0 path. + single=(horizontal = [0.0], height = [-1.0], radius = [0.0425]) + s=2pi*1e12im + sigma=fill(10.0, 2) + state=(jω = s, Γ = zero(s), sigma, epsilon, mu) + u=UnifiedFormulaFixtures.field_state(single, state) + direct=E.special_besselkx(0, u.radial_argument[1])*exp(u.source_logscale[1]-u.radial_argument[1]) + K=s*mu[1]/(2pi)*direct + H=s/(2pi*u.sh[1])*direct + L=inv(u.circumference_average[1])-u.radial_current[1]*K + for method in (:quad,) + local integration=E.formulation_options(E.SpectralIntegral, ( + method, options = (rtol = 1e-8,))) + actual=UnifiedFormulaFixtures.calculate(single, state, integration) + @test actual.enclosed_impedance[1, 1]≈K/L rtol=1e-8 + @test actual.enclosed_potential[1, 1]≈H/L rtol=1e-8 + @test (s * inv(actual.enclosed_potential))[1, 1]≈s*L/H rtol=1e-8 + end +end + +@testitem "Engine / raw integral trace and warnings do not reject physical computation" tags=[:unit, :parametric] setup=[TestFixtures] begin + using Logging + const E=LineCableModels.Engine + problem=TestFixtures.three_bare_wires_problem(frequencies = [50.0]) + controls=(integration = (method = :quad, options = (rtol = 1e-14, maxevals = 15)),) + definition=formula(:unified; options = controls) + selected=Formulation(earth_impedance = definition, earth_admittance = definition, + options = ( + reduce_bundle = false, kron_reduction = false, ideal_transposition = false)) + # Public compute installs its normal console logger. Observe its real + # stderr output rather than replacing the outer task logger. + result, + warning=mktemp() do path, io + computed=redirect_stderr(io) do + compute(problem, selected; options = (trace = true, verbosity = (default = 0,))) + end + flush(io); + seekstart(io) + (computed, read(io, String)) + end + @test occursin("QuadGK returned an estimated error", warning) + @test occursin("estimated_error", warning)&&occursin("context", warning) + records=details(result).data.trace.integrals + @test length(records)==18 # Assembly is shared across owners and entry requests. + @test all(r->isfinite(r.value)&&isfinite(r.estimated_error)&&r.estimated_error>=0, records) + @test Set(r.context.formula for r in records)==Set([:unified]) + @test Set(r.context.term for r in records)==Set([:Z, :air_voltage]) + @test all(r->r.context.frequency==50.0, records) + @test Set((r.context.receiver, r.context.source) for r in records) == + Set((p, q) for p in 1:3 for q in 1:3) + @test any(r->r.estimated_error>1e-14*abs(r.value), records) + @test all(isfinite, result.Z.values)&&all(isfinite, result.Y.values) + execution=computation_options(LineCableModelsCoaxial, + ComputationOptions(trace = true, verbosity = (default = 0,))) + blueprints=[E.flatten(LineCableModelsCoaxial(), d, Float64) + for d in problem.system.designs] + w=E.LineParametersWorkspace(problem, Formulation(), execution, blueprints) + E._solve!(w, Formulation()); + E._solve!(w, Formulation()) + @test length(w.trace.integrals)==18 + quiet=compute(problem; options = (trace = false, verbosity = (default = 0,))) + @test !haskey(details(quiet).data, :trace) +end + +@testitem "Engine / public earth defaults preserve uncertainty and reuse matching earth states" tags=[:unit, :measurements] setup=[UnifiedFormulaFixtures] begin + using Measurements, LinearAlgebra + const E=LineCableModels.Engine + radius=measurement(0.0425, 0.0001) + depth=measurement(1.0, 0.001) + spacing=measurement(1.0, 0.001) + rho=measurement(0.1, 0.001) + metal=Material(kind = :conductor, rho = 1.7241e-8) + design=build(CableDesign, "uncertain earth circles", + Stack(Group(:phase, Region(:core, Disk(radius), metal)))) + system=build( + LineCableSystem, fill(design, 2), [(zero(spacing), -depth), (spacing, -depth)]; + connections = [(phase = i,) for i in 1:2]) + problem=LineParametersProblem( + system; earth_props = homogeneous(rho = rho, eps_r = 1.0, mu_r = 1.0), + frequencies = [1e6]) + responses=map((:quad,)) do method + definition=formula(:default; options = (integration = ( + method, options = (rtol = 1e-8,)),)) + selected=Formulation(earth_impedance = definition, earth_admittance = definition, + options = (reduce_bundle = false, kron_reduction = false, + ideal_transposition = false,)) + execution=computation_options(LineCableModelsCoaxial, ComputationOptions((trace = true,))) + T=eltype(problem) + blueprints=E.CableBlueprint{T}[E.flatten(LineCableModelsCoaxial(), d, T) + for d in problem.system.designs] + workspace=E.LineParametersWorkspace(problem, selected, execution, blueprints) + shared=only(workspace.plan.earth.calculations) + @test !isempty(shared.impedance.pairs) && !isempty(shared.admittance.pairs) + @test length(workspace.buffers.earth.calculations) == 1 + @test (@inferred E._solve!(workspace, selected)) === workspace + @test all(isfinite, workspace.buffers.enclosed_impedance) + @test !isempty(workspace.trace.integrals) + @test eltype(workspace.buffers.Zout)===Complex{Measurement{Float64}} + trace=workspace.trace + Ze=trace.Zg[:, :, 1] + Pe=trace.Pg[:, :, 1] + Ye=(2pi*1e6*im)*(Pe\Matrix{eltype(Pe)}(I, 2, 2)) + permutation=workspace.plan.reduction.permutation + primitive=trace.P[permutation, permutation, 1] + actual=workspace.buffers.Yout[:, :, 1] + @test maximum(E.numerical_magnitude.(actual*primitive-(2pi*1e6*im)*I))<1e-6 + for matrix in (Ze, Pe, Ye), parameter in (rho, radius, depth, spacing) + + @test any(matrix) do value + !iszero(Measurements.derivative(real(value), + parameter)) || + !iszero(Measurements.derivative(imag(value), parameter)) + end + end + (Ze, Pe, Ye) + end + @test !E.same_physical_state(rho, measurement(0.1, 0.001)) + @test E.same_physical_state(rho, 2rho-rho) + # Check the propagated physical derivatives against independent central + # perturbations of the material and geometry inputs to the earth-return calculation. + function perturbed(values) + rho, r, h, d=values + geometry=(horizontal = [0.0, d], height = [-h, -h], radius = [r, r]) + s=2pi*1e6im + sigma=[0.0, inv(rho)] + epsilon=fill(8.8541878128e-12, 2) + mu=fill(4pi*1e-7, 2) + state=(jω = s, Γ = zero(s), sigma, epsilon, mu) + integration=E.formulation_options(E.SpectralIntegral, ( + method = :quad, options = (rtol = 1e-10,))) + w=UnifiedFormulaFixtures.calculate(geometry, state, integration) + return w.enclosed_impedance, w.enclosed_potential, (s*inv(w.enclosed_potential)) + end + parameters=(rho, radius, depth, spacing) + for (index, parameter) in enumerate(parameters) + values=Measurements.value.(collect(parameters)) + step=1e-4*values[index] + plus=copy(values) + minus=copy(values) + plus[index]+=step + minus[index]-=step + for (actual, upper, + lower) in + zip(first(responses), perturbed(plus), perturbed(minus)) + for i in eachindex(actual) + derivative=complex(Measurements.derivative(real(actual[i]), parameter), + Measurements.derivative(imag(actual[i]), parameter)) + finite_difference=(upper[i]-lower[i])/(2step) + @test derivative≈finite_difference rtol=1e-4 atol=1e-10 + end + end + end +end + +@testitem "Engine / complete earth scalar types and air-root limit" tags=[:unit, :parametric, :slow] setup=[UnifiedFormulaFixtures] begin + using LinearAlgebra + const E=LineCableModels.Engine + setprecision(BigFloat, 128) do + for T in (Float32, Float64, BigFloat) + geometry=(horizontal = T[0, 1], height = T[1, -1], radius = T[0.02, 0.03]) + s=complex(zero(T), T(2pi*50)) + sigma=T[0, 0.01] + epsilon=T(8.8541878128e-12) .* T[1, 10] + mu=fill(T(4pi*1e-7), 2) + state=(jω = s, Γ = zero(s), sigma, epsilon, mu) + for method in (:quad,) + tolerance=T===Float32 ? 2e-4 : 1e-6 + integration=E.formulation_options(E.SpectralIntegral, (method, + options = (rtol = tolerance,))) + workspace=UnifiedFormulaFixtures.calculate(geometry, state, integration) + @test eltype(workspace.enclosed_impedance)===Complex{T} + for matrix in (workspace.enclosed_impedance, workspace.enclosed_potential, + (s*inv(workspace.enclosed_potential))) + @test all(isfinite, matrix) + end + end + end + for z in (big"0.001"+big"0.002"*im, big"1.0"+big"2.0"*im, + big"10.0"*im, big"300.0"+big"300.0"*im), + order in (0, 1) + + for f in (E.special_besselix, E.special_besseljx, E.special_besselkx) + @test f(order, z)≈f(order, ComplexF64(z)) rtol=1e-12 + end + end + end + geometry=(horizontal = [0.0, 1.0], height = [1.0, -1.0], radius = [0.02, 0.03]) + s=2pi*1000im + sigma=[0.0, 0.01] + epsilon=8.8541878128e-12 .* [1.0, 10.0] + mu=fill(4pi*1e-7, 2) + gamma2=s .* mu .* (sigma .+ s .* epsilon) + Γ=sqrt(gamma2[1]) + gamma2[1]=Γ^2 + state=(jω = s, Γ, sigma, epsilon, mu) + quad=E.formulation_options(E.SpectralIntegral, ( + method = :quad, options = (rtol = 1e-9,))) + reference=UnifiedFormulaFixtures.calculate(geometry, state, quad) + for method in (:quad,) + integration=E.formulation_options(E.SpectralIntegral, ( + method, options = (rtol = 1e-6,))) + actual=UnifiedFormulaFixtures.calculate(geometry, state, integration) + for (value, + expected) in zip( + (actual.enclosed_impedance, actual.enclosed_potential, + (s*inv(actual.enclosed_potential))), ( + reference.enclosed_impedance, reference.enclosed_potential, (s*inv(reference.enclosed_potential)))) + @test value≈expected rtol=1e-5 atol=1e-10 + end + end +end + +@testitem "Engine / full earth / independent equal-medium cylindrical control" tags=[:unit, :parametric] setup=[UnifiedFormulaFixtures] begin + using LinearAlgebra, QuadGK + include(joinpath(pkgdir(LineCableModels), "test/support/radial_control.jl")) + const E=LineCableModels.Engine + # I0/I1 use an independently coded Frobenius recurrence. K0 uses its + # decaying integral, not the engine's selected spectral integrator. + function reference(positions, radii, f) + s=2big(pi)*im*BigFloat(f); + mu=4big(pi)*big"1e-7" + kappa=big".01"+s*3big"8.8541878128e-12"; + k=sqrt(s*mu*kappa) + n=length(radii); + A=zeros(Complex{BigFloat}, n); + F=similar(A) + eA=zeros(BigFloat, n); + eF=similar(eA) + for p in 1:n + x=k*radii[p]; + v=RadialControl.fundamental(x) + A[p]=v.u; + F[p]=2big(pi)*kappa*radii[p]^2*v.du/(x*v.u) + eA[p]=v.bound + eA[p]exp(-z*cosh(t)), big"0", limit; rtol = big"1e-12", maxevals = 10^6) + tail=2exp(-real(z)*exp(limit)/2)/(real(z)*exp(limit)) + T[p, q]=(p==q ? one(k) : A[p])*value + eT[p, q]=p==q ? error+tail : + abs(A[p])*(error+tail)+eA[p]*(abs(value)+error+tail) + end + K=s*mu*T/(2big(pi)); + H=s*T/(2big(pi)*kappa) + L=diagm(inv.(A))-diagm(F)*K + Ze=K/L; + Pe=H/L; + Ye=s*inv(Pe) + # Normwise perturbation bounds for right solves X*L=B. For + # eta=||L^-1||*deltaL<1, deltaX <= + # (deltaB+||X||*deltaL)*||L^-1||/(1-eta). + # Absolute input bounds retain units and apply to either component. + norminf(x)=opnorm(x, Inf) + roundoff(x)=128eps(BigFloat)*norminf(x) + eK=abs(s*mu/(2big(pi)))*norminf(eT)+roundoff(K) + eH=abs(s/(2big(pi)*kappa))*norminf(eT)+roundoff(H) + eL=maximum(eA ./ (abs.(A) .* (abs.(A) .- eA)))+ + maximum(abs, F)*eK+maximum(eF)*(norminf(K)+eK)+roundoff(L) + inverseL=norminf(inv(L)); + eta=inverseL*eL + eta<1||error("inconclusive conditioning of the cylindrical current matrix") + eZe=(eK+norminf(Ze)*eL)*inverseL/(1-eta)+roundoff(Ze) + ePe=(eH+norminf(Pe)*eL)*inverseL/(1-eta)+roundoff(Pe) + inverseP=norminf(inv(Pe)); + etaP=inverseP*ePe + etaP<1||error("inconclusive cylindrical admittance conditioning") + eYe=abs(s)*inverseP^2*ePe/(1-etaP)+roundoff(Ye) + bounds=(K = eK, H = eH, L = eL, Ze = eZe, Pe = ePe, Ye = eYe) + return (; K, H, L, Ze, Pe, Ye, k, bounds) + end + layouts=([(0.0, -1.0)], [(0.0, 1.0), (0.4, 1.5)], [(0.0, -1.0), (0.4, -1.5)], + [(0.0, 1.0), (0.4, -1.0), (0.9, -1.5)]) + for positions in layouts, f in (50.0, 10000.0) + + n=length(positions); + radii=[0.005, 0.007, 0.009][1:n] + refs=[setprecision(BigFloat, bits) do + reference(map(p->BigFloat.(p), positions), BigFloat.(radii), f) + end + for bits in (128, 256, 512)] + expected=last(refs) + geometry=(horizontal = first.(positions), height = last.(positions), radius = radii) + s=2pi*im*f; + sigma=fill(0.01, 2); + epsilon=fill(3*8.8541878128e-12, 2); + mu=fill(4pi*1e-7, 2) + state=(jω = s, Γ = zero(s), sigma, epsilon, mu) + controls=E.formulation_options( + E.SpectralIntegral, (method = :quad, options = (rtol = 1e-10, maxevals = 10^6))) + w=UnifiedFormulaFixtures.calculate(geometry, state, controls) + column_scale=reshape(exp.(abs.(real.(expected.k .* radii))), 1, :) + # Equal-medium impedance and current maps erase the interface. Air + # voltages deliberately retain an interface reference, so only buried + # layouts have the unbounded-medium P/Y used by this independent control. + for quantity in + (all(p->last(p)<0, positions) ? (:K, :H, :L, :Ze, :Pe, :Ye) : (:K, :L, :Ze)) + target=getproperty(expected, quantity) + actual=quantity in (:K, :H, :L) ? + getproperty( + (K = w.axial_field, H = w.source_potential, L = w.current_map, + Ze = w.enclosed_impedance, Pe = w.enclosed_potential), + quantity) ./ column_scale : + quantity===:Ye ? s*inv(w.enclosed_potential) : + getproperty( + (K = w.axial_field, H = w.source_potential, L = w.current_map, + Ze = w.enclosed_impedance, Pe = w.enclosed_potential), + quantity) + for index in eachindex(target), component in (real, imag) + + qstar=component(target[index]); + budget=1e-5*abs(qstar) + u=getproperty(expected.bounds, + quantity)+ + abs(component(target[index]-getproperty(first(refs), quantity)[index])) + @test u<=budget/4 + @test abs(component(actual[index])-qstar)+u<=budget + end + end + end +end + +@testitem "Engine / combined voltage paths equal the manuscript endpoint terms" tags=[:unit, :parametric] setup=[UnifiedFormulaFixtures] begin + const E=LineCableModels.Engine + geometry=(horizontal = [0.0, 1.0], height = [1.2, -0.9], radius = [0.01, 0.025]) + s=complex(0.0, 2pi*1e4) + sigma=[0.0, 0.1] + epsilon=8.8541878128e-12 .* [1.0, 8.0] + mu=4pi*1e-7 .* [1.0, 3.0] + u=UnifiedFormulaFixtures.field_state(geometry, ( + jω = s, Γ = 1e-4+2e-4im, sigma, epsilon, mu)) + for P in (1,), Q in (1, 2) + + hp=abs(geometry.height[P]) + hq=abs(geometry.height[Q]) + radius=geometry.radius[P] + padding=hq/2 + sq=u.source_logscale[Q] + g=(; hp, hq, radius, padding, logscale = sq, + i0minus = E.bessel_i0m1(u.radial_argument[P])) + combined=E.AirVoltageSpectrum{Q, typeof(u), typeof(g)}(u, g) + voltage=E.EarthSpectrum{:voltage, P, Q}(u, ( + hp = hp, hq = hq, logscale = u.source_logscale[P]+sq)) + for λ in (0.001, 0.1, 3.0, 40.0) .* exp(0.03im) + j0=E.SpecialFunctions.besselj(0, radius*λ) + original=u.circumference_average[P]*voltage(λ)*exp(-(hp+hq)*λ) + a0=sqrt(λ^2+u.k2[1]); + ag=sqrt(λ^2+u.k2[2]) + aq=Q==1 ? a0 : ag + original-=ag*j0*exp(sq-hq*aq)/(a0*(u.sh[2]*a0+u.sh[1]*ag)) + actual=combined(λ)*exp(-(hq-padding)*λ) + @test actual≈original rtol=2e-10 atol=1e-20 + end + # At a_receiver=0, I0(κ_receiver*r)=J0(r*λ). The removable + # quotient converges to -h_receiver*J0, including the spectral padding. + roots=ntuple(m->m==P ? complex(-1.0) : u.k2[m], 2) + branch=merge(u, (; k2 = roots)) + kernel=E.AirVoltageSpectrum{Q, typeof(branch), typeof(g)}(branch, g) + a=map(k2->E.outgoing_root(1+k2), roots) + other=a[3 - P] + j0=E.SpecialFunctions.besselj(0, radius) + expected=-hp*other*j0*exp(sq-hq*a[Q])/(u.sh[2]*a[1]+u.sh[1]*a[2]) + @test kernel(1.0)*exp(-(hq-padding))≈expected rtol=1e-12 + end +end + +@testitem "Engine / prescribed longitudinal constants keep spectral contours on their branch" tags=[:unit, :parametric] setup=[UnifiedFormulaFixtures] begin + const E=LineCableModels.Engine + geometry=(horizontal = [0.0, 1.0], height = [1.0, -1.0], radius = [0.02, 0.03]) + s=complex(0.0, 2pi*1e4) + sigma=[0.0, 0.1] + epsilon=8.8541878128e-12 .* [1.0, 8.0] + mu=4pi*1e-7 .* [1.0, 3.0] + controls=E.formulation_options(E.SpectralIntegral, ( + method = :quad, options = (rtol = 1e-9,))).options + for Γ in (0.0+0.0im, 1e-4+2e-4im, 3e-4+1e-4im) + state=UnifiedFormulaFixtures.field_state(geometry, (jω = s, Γ, sigma, epsilon, mu)) + angle=E.earth_contour_angle(state, pi/6) + @test 0kernel(lambda)*exp(-2lambda)*cos(lambda)) + points=E.earth_spectral_points!( + UnifiedFormulaFixtures.buffers(geometry).earth_spectrum, state, 2.0, 1.0, 0.0, 0.0) + push!(points, 1.0) + real_axis, _=E.integrate(integral, Val(:quad), controls; points) + rotated=E.earth_spectral_term(E.EarthImpedance.Formula(:unified), + Val(kind), Val(P), Val(Q), state, 1.0, 1.0, 1.0, 0.0, 0.0, + Val(:quad), controls, UnifiedFormulaFixtures.buffers(geometry)) + @test rotated≈real_axis rtol=1e-7 + end + end +end + +@testitem "Engine / earth kernels keep J0 - 1 accurate and the decaying cosine finite" tags=[:unit, :engine] begin + const E=LineCableModels.Engine + # J₀(z)-1 from its power series in 256-bit arithmetic: an independent reference. + function j0m1(z) + z = big(z) + term = one(z) + result = zero(z) + for k in 1:400 + term *= -(z/2)^2/k^2 + result += term + end + return result + end + for z in (1e-8, 1e-3, 0.3, 2.5, 0.4 + 0.3im, 3.0 - 2.0im, 1e-6im) + reference = j0m1(z) + @test abs(E.bessel_j0m1(z) - reference) <= 4eps()*abs(reference) + end + # Near zero, J₀ - 1 computed as a difference would lose every digit. + @test E.bessel_j0m1(1e-8) ≈ -2.5e-17 rtol=1e-12 + + # e^{-hλ}cos(sλ), with the decay and the oscillation in one exponential. + height, separation = 1.0, 2.0 + for λ in (0.7, 3.0 + 0.5im, 0.2 - 1.5im) + @test E.earth_cosine(height, separation, λ) ≈ + exp(-height*λ)*cos(separation*λ) rtol=1e-14 + end + # Far along a complex ray, cos(sλ) alone overflows. The combined form is finite there. + λ = 400.0 - 400.0im + @test !isfinite(cos(separation*λ)) + combined = E.earth_cosine(height, separation, λ) + reference = exp(-height*big(λ))*cos(separation*big(λ)) + @test isfinite(combined) + @test abs(combined - reference) <= 1e-12*abs(reference) +end diff --git a/test/unit/importexport/atp.jl b/test/unit/importexport/atp.jl new file mode 100644 index 000000000..1ed0a7f7e --- /dev/null +++ b/test/unit/importexport/atp.jl @@ -0,0 +1,141 @@ +@testsnippet deps_export_atp begin + using EzXML +end + +# TODO: test if serialization works properly if uncertain types are used (Measurements) + +@testitem "ImportExport / ATP / LineCableSystem LCC export" tags=[:integration, :importexport] setup=[ + UseImportExportSupport, TestNumerics, + TestFixtures, CableSystemFixture, deps_export_atp] begin + + # 1. ARRANGE & ACT: run the export in a temporary directory + mktempdir() do tmpdir + output_file=joinpath(tmpdir, "atp_export_test.xml") + result_path=export_data(:atp, cable_system, earth_props, file_name = output_file) + expected_file=joinpath( + dirname(output_file), + "$(cable_system.system_id)_$(basename(output_file))" + ) + + # 2. ASSERT: basic file checks (exporter prefixes basename with system_id) + @test result_path == expected_file + @test isfile(expected_file) + + # 3. ASSERT: general XML structure and LCC data + doc=readxml(expected_file) + root_node=root(doc) + + @test nodename(root_node) == "project" + @test root_node["Application"] == "ATPDraw" + + # Find the main LCC component content node + comp_content_node=findfirst("/project/objects/comp/comp_content", root_node) + @test !isnothing(comp_content_node) + + # Verify general parameters like Length, Freq, and Ground Resistivity + @test parse( + Float64, + findfirst("data[@Name='Length']", comp_content_node)["Value"] + ) ≈ cable_system.line_length + @test parse(Float64, findfirst("data[@Name='Freq']", comp_content_node)["Value"]) ≈ + problem_atp.frequencies[1] + @test parse( + Float64, + findfirst("data[@Name='Grnd resis']", comp_content_node)["Value"] + ) ≈ problem_atp.earth_props.layers[end].rho + + # 4. ASSERT: detailed validation of ALL cables and conductors + lcc_node=findfirst("/project/objects/comp/LCC", root_node) + cable_header=findfirst("cable_header", lcc_node) + cable_nodes=findall("cable", cable_header) + + @test length(cable_nodes) == num_phases + + # Loop through each cable exported in the XML and compare it to the source + for (i, cable_node) in enumerate(cable_nodes) + source_design=cable_system.designs[i] + source_position=cable_system.positions[i] + source_components=LineCableModels.DataModel.flatten( + source_design, problem_atp.frequencies[1] + ) + + # Verify position of EACH cable + @test parse(Float64, cable_node["PosX"]) ≈ source_position.x + @test parse(Float64, cable_node["PosY"]) ≈ source_position.y + + # Verify the number of conductor components inside this cable + num_components=length(source_components) + @test parse(Int, cable_node["NumCond"]) == num_components + + conductor_nodes=findall("conductor", cable_node) + @test length(conductor_nodes) == num_components + + # Loop through each conductor component within the cable + for (j, conductor_node) in enumerate(conductor_nodes) + source_component=source_components[j] + conductor=source_component.conductor + dielectric=source_component.dielectric + + expected_radius_in=conductor.r_in + expected_radius_ext=conductor.r_ex + expected_rho=conductor.material.rho + expected_muC=conductor.material.mu_r + expected_epsI=dielectric.material.eps_r + expected_muI=dielectric.material.mu_r + expected_Cext=dielectric.shunt_capacitance + expected_Gext=dielectric.shunt_conductance + + # Assert that every attribute matches the expected value + @test parse(Float64, conductor_node["Rin"]) ≈ expected_radius_in + @test parse(Float64, conductor_node["Rout"]) ≈ expected_radius_ext + @test parse(Float64, conductor_node["rho"]) ≈ expected_rho + @test parse(Float64, conductor_node["muC"]) ≈ expected_muC + @test parse(Float64, conductor_node["muI"]) ≈ expected_muI + @test parse(Float64, conductor_node["epsI"]) ≈ expected_epsI + @test parse(Float64, conductor_node["Cext"]) ≈ expected_Cext + @test parse(Float64, conductor_node["Gext"]) ≈ expected_Gext + end + end + end +end + +@testitem "ImportExport / ATP / LineParameters ZY export" tags=[:integration, :importexport] setup=[ + TestFixtures, deps_export_atp] begin + source=TestFixtures.two_conductor_results() + z,y=copy(Z(source)),copy(Y(source)) + # Distinct self and mutual entries and both signs expose ordering and parsing errors. + z[1,2,:].*=-1 + y[2,1,:].*=-1 + parameters=LineParameters(z,y,frequencies(source)) + mktempdir() do directory + path=joinpath(directory,"matrices.xml") + @test export_data(:atp,parameters;file_name=path)==path + document=readxml(path) + node=root(document) + @test nodename(node)=="ZY" + @test parse(Int,node["NumPhases"])==2 + @test node["ZFmt"]=="R+Xi" + @test node["YFmt"]=="G+Bi" + pattern=r"^([+-]?[0-9.]+E[+-][0-9]+)([+-][0-9.]+E[+-][0-9]+)i$" + for (name,expected) in (("Z",z),("Y",y)) + blocks=findall(name,node) + @test length(blocks)==length(frequencies(source)) + for (k,block) in enumerate(blocks) + @test parse(Float64,block["Freq"])==frequencies(source)[k] + rows=split(strip(nodecontent(block)),'\n') + @test length(rows)==2 + for (i,row) in enumerate(rows) + entries=split(row,',') + @test length(entries)==2 + for (j,entry) in enumerate(entries) + parsed=match(pattern,strip(entry)) + @test parsed!==nothing + parsed===nothing && error("Malformed ATP $name entry [$i,$j,$k]: $entry") + @test parse(Float64,parsed.captures[1])==real(expected[i,j,k]) + @test parse(Float64,parsed.captures[2])==imag(expected[i,j,k]) + end + end + end + end + end +end diff --git a/test/unit/importexport/formats.jl b/test/unit/importexport/formats.jl new file mode 100644 index 000000000..52b04e048 --- /dev/null +++ b/test/unit/importexport/formats.jl @@ -0,0 +1,217 @@ +@testitem "ImportExport / MaterialsLibrary / versioned JSON and trusted JLS" tags=[:integration, :importexport] setup=[ + UseImportExportSupport +] begin + using JSON3 + using Serialization + import LineCableModels.ImportExport as IE + + library=MaterialsLibrary(add_defaults = false) + copper=Material(:conductor, 1.7241e-8, 1.0, 0.999994, 20.0, 0.00393) + add!(library, "copper", copper) + + mktempdir() do directory + restored=MaterialsLibrary(add_defaults = false) + for extension in ("json", "jls") + destination=joinpath(directory, "materials.$extension") + saved=save(library; file_name = destination) + @test saved == destination + @test isfile(saved) + @test filesize(saved) > 0 + + empty!(restored) + @test load!(restored; file_name = saved) === restored + @test collect(keys(restored)) == ["copper"] + @test restored["copper"] == copper + end + + @test_throws ArgumentError save( + library; file_name = joinpath(directory, "materials.dat") + ) + unsupported=joinpath(directory, "materials.txt") + write(unsupported, "not json") + before=restored.data + @test_throws ArgumentError load!(restored; file_name = unsupported) + @test restored.data === before + @test haskey(restored, "copper") + + for (name, payload) in ( + ("not-a-dictionary", 42), + ("invalid-material", Dict("copper"=>42)) + ) + invalid_jls=joinpath(directory, "$name.jls") + serialize(invalid_jls, payload) + before=restored.data + @test_throws ArgumentError load!(restored; file_name = invalid_jls) + @test restored.data === before + @test haskey(restored, "copper") + end + + @test_throws ArgumentError load!( + restored; + file_name = joinpath(directory, "missing.json") + ) + + invalid=IE._json_document(library) + delete!(invalid["materials"]["copper"], "rho") + invalid_path=joinpath(directory, "invalid.json") + open(invalid_path, "w") do io + JSON3.pretty(io, invalid) + end + before=restored.data + @test_throws ArgumentError load!(restored; file_name = invalid_path) + @test restored.data === before + + legacy=joinpath(directory, "legacy.json") + write(legacy, "{\"copper\":{}}") + @test_throws ArgumentError load!(restored; file_name = legacy) + @test restored.data === before + end +end + +@testitem "ImportExport / TRALIN / export and parser behavior" tags=[:integration, :importexport] setup=[ + UseImportExportSupport, + TestFixtures +] begin + import LineCableModels.ImportExport: _block_after_anchor, + _extract_tralin_frequencies, _infer_tralin_order, + clean_variable_list, extract_tralin_variable, + take_complex_list + + system=TestFixtures.three_phase_system() + homogeneous=EarthModel(100.0, 10.0, 1.0) + + mktempdir() do directory + exported=@test_logs (:info, r"TRALIN file saved") export_data( + :tralin, + system, + homogeneous; + freq = [50.0, 500.0], + file_name = joinpath(directory, "case.f05") + ) + @test basename(exported) == "tr_case.f05" + text=read(exported, String) + @test occursin("RUN-IDENTIFICATION,$(system.system_id)", text) + @test occursin("UNIFORM,100.0,1.0,10.0", text) + @test length(collect(eachmatch(r"FREQUENCY", text))) == 2 + @test count(line -> startswith(line, "GROUP,PH-"), readlines(exported)) == 3 + # The current three-phase scenario has two concentric terminals per cable. + @test count(line->startswith(line,"CORE,"),split(text,'\n')) == 3 + @test count(line->startswith(line,"SHEATH,"),split(text,'\n')) == 3 + @test !occursin("ARMOUR,",text) + + layered=build(EarthModel, ( + EarthLayer(80.0, 8.0, 1.0, 4.0), + EarthLayer(300.0, 12.0, 1.0) + )) + layered_path=export_data( + :tralin, + system, + layered; + file_name = joinpath(directory, "layered.f05") + ) + layered_text=read(layered_path, String) + @test occursin("HORIZONTAL", layered_text) + @test occursin("LAYER,TOP,80.0,4.0,1.0,8.0", layered_text) + @test occursin("LAYER,BOTTOM,300.0,,1.0,12.0", layered_text) + + metal=Material(:conductor, 1.7e-8, 1.0, 1.0, 20.0, 0.0039) + dielectric=Material(:insulator, 1.0e14, 2.3, 1.0, 20.0, 0.0) + parts=AbstractCablePart[] + for index in 1:4 + inner=(index-1)*2.0e-3 + terminal=Symbol("terminal_$index") + push!(parts, + Group(terminal, + Region( + Symbol(terminal, :_conductor), + Annulus(inner, inner+1.0e-3), + metal + ))) + push!(parts, + Region( + Symbol(terminal, :_insulation), + Annulus(inner+1.0e-3, inner+2.0e-3), + dielectric + )) + end + # Exercise the third supported component with an explicit current + # construction, independent of the shared two-terminal scenario. + three_design=build(CableDesign,"three-terminal",Stack(parts[1:6])) + three_system=build(LineCableSystem,three_design,Pose2(0.0,-1.0); + connections=Dict(:terminal_1=>1,:terminal_2=>0,:terminal_3=>0)) + three_path=export_data(:tralin,three_system,homogeneous; + file_name=joinpath(directory,"three-terminal.f05")) + component_lines=filter(line->any(label->startswith(line,label*","), + ("CORE","SHEATH","ARMOUR")),readlines(three_path)) + @test first.(split.(component_lines,',')) == ["CORE","SHEATH","ARMOUR"] + @test [parse(Int,split(line,',')[3]) for line in component_lines] == [1,0,0] + + oversized_design=build(CableDesign, "oversized", Stack(parts)) + oversized_system=build( + LineCableSystem, + oversized_design, + Pose2(0.0, -1.0, 0.0); + connections = Dict(name=>index + for (index, name) in enumerate(oversized_design.terminal_order)) + ) + @test_throws ArgumentError export_data( + :tralin, + oversized_system, + homogeneous; + file_name = joinpath(directory, "oversized.f05") + ) + + report=String[ + "CHARACTERISTICS OF ALL CONDUCTORS", + " 1 1 1 1 1 core 0.0 0.1", + "TRALIN package - PAGE 2", + "FREQUENCY OF HARMONIC CURRENT:", + " 1 50.0", + "TRALIN package - PAGE 3", + "GROUND WIRES ELIMINATED", + "POTENTIAL COEFFICIENTS (meghoms.kilometer)" + ] + append!(report, fill("header", 14)) + push!(report, "1 7.0 + j 8.0") + push!(report, "SERIES IMPEDANCES - (ohms/kilometer)") + append!(report, fill("header", 14)) + push!(report, "1 1.0 + j 2.0") + push!(report, "SHUNT ADMITTANCES (microsiemens/kilometer)") + append!(report, fill("header", 14)) + push!(report, "1 3.0 + j 4.0") + push!(report, "SERIES ADMITTANCES (siemens.kilometer)") + + report_path=joinpath(directory, "synthetic.f09") + write(report_path, join(report, '\n')) + @test _infer_tralin_order(report) == 1 + @test _infer_tralin_order(report_path) == 1 + @test _extract_tralin_frequencies(report) == [50.0] + @test _block_after_anchor(report, "FREQUENCY")[1] == " 1 50.0" + @test_throws ArgumentError _block_after_anchor(report, "missing") + @test_throws ArgumentError _infer_tralin_order(["CHARACTERISTICS OF ALL CONDUCTORS"]) + @test_throws ArgumentError _extract_tralin_frequencies(["FREQUENCY OF HARMONIC CURRENT:"]) + @test take_complex_list("4 1.5 + j 2.5E+1") == [4.0, 1.5 + 25.0im] + @test isempty(take_complex_list("header")) + @test clean_variable_list(Any[[1.0, 2.0 + 3.0im]], 2) == + [[2.0 + 3.0im, 0.0im], [0.0im, 0.0im]] + redirect_stdout(devnull) do + @test extract_tralin_variable(["no blocks"], 2, "start", "end") == + zeros(ComplexF64, 2, 2) + end + + parsed_frequencies, parsed_Z, + parsed_Y, parsed_P=LineCableModels.ImportExport.parse_tralin_file(report_path) + @test parsed_frequencies == [50.0] + @test parsed_Z[1, 1, 1] == (1.0 + 2.0im) / 1000 + @test parsed_Y[1, 1, 1] ≈ (3.0 + 4.0im) * 1e-9 + @test parsed_P[1, 1, 1] == (7.0 + 8.0im) * 1e9 + + parameters=LineParameters(report_path) + @test frequencies(parameters) == [50.0] + symbol_parameters=LineParameters(:tralin, report_path) + @test Z(symbol_parameters) == Z(parameters) + @test Y(symbol_parameters) == Y(parameters) + @test_throws ArgumentError LineParameters(report_path; format = :unknown) + @test_throws ArgumentError LineParameters(:unsupported, report_path) + end +end diff --git a/test/unit/importexport/serialization.jl b/test/unit/importexport/serialization.jl new file mode 100644 index 000000000..7b3224044 --- /dev/null +++ b/test/unit/importexport/serialization.jl @@ -0,0 +1,605 @@ +@testitem "ImportExport / v1 scalar and Grid encoding" tags=[:unit, :importexport] setup=[ + UseImportExportSupport +] begin + import LineCableModels.ImportExport as IE + + values=Any[ + nothing, true, 7, + Float16(1.25), Float32(1.25), 1.25, BigFloat("1.25"), + Inf, -Inf, NaN, 2.0 - 3.0im, "cable", :phase, + [1, 2.5, -1.0im], (1.0, 2.0), Dict(:count=>2, :scale=>0.5) + ] + for original in values + decoded=IE.deserialize_value(IE.serialize_value(original)) + if original isa AbstractFloat&&isnan(original) + @test isnan(decoded) + elseif original isa Tuple + @test decoded == collect(original) + elseif original isa AbstractDict + @test decoded == Dict("count"=>2, "scale"=>0.5) + else + @test decoded == original + end + end + + deterministic=Grid((:conductor, :semicon)) + relative=Grid((1.0, 2.0), (1.0, 2.0)) + absolute=Grid((10.0,), AbsoluteError((0.5,))) + for grid in (deterministic, relative, absolute) + restored=IE.deserialize_value(IE.serialize_value(grid)) + @test typeof(restored) === typeof(grid) + @test collect(restored) == collect(grid) + end + + material_record=IE._material_record(Material(kind = :conductor, rho = 1.7e-8)) + material_record["kind"]=Dict("grid"=>["conductor", "semicon"]) + kinds=IE._decode_material_record(material_record) + @test kinds isa Gridspace{Material} + @test getproperty.(collect(kinds), :kind) == [:conductor, :semicon] + + @test_throws ArgumentError IE.deserialize_value(Dict("__type__"=>"Complex")) + @test_throws ArgumentError IE.deserialize_value(Dict( + "__type__"=>"Float64", "special"=>"huge" + )) + @test_throws ArgumentError IE.deserialize_value(Dict( + "type"=>"FutureObject", "value"=>2 + )) +end + +@testitem "ImportExport / resolved formulation records survive scientific JSON transport" tags=[:unit, :importexport] begin + using JSON3 + import LineCableModels.ImportExport as IE + selected = Formulation(earth_impedance=formula(:carson1926; + options=(integration=(method=:quad, options=(rtol=1e-6,)),))) + for formulation in (Formulation(), selected, LinearError(selected), + MonteCarlo(selected; trials=2, seed=0x1234), ModalAnalysisFormulation()) + original = NamedTuple(formulation) + encoded = IE.serialize_value(original, Val(:scientific)) + transported = JSON3.read(JSON3.write(encoded), Dict{String,Any}) + restored = IE.deserialize_value(transported) + @test restored == original + end + for selector in (Val(:homogeneous), Val(:quad), Val(1), Val((:self, 1, 2))) + encoded = IE.serialize_value(selector, Val(:scientific)) + @test IE.deserialize_value(JSON3.read(JSON3.write(encoded), Dict{String,Any})) === selector + end + for seed in (UInt64(0),UInt64(1)<<63,typemax(UInt64)) + original=NamedTuple(MonteCarlo(selected;trials=2,seed)) + encoded=IE.serialize_value(original,Val(:scientific)) + @test IE.deserialize_value(JSON3.read(JSON3.write(encoded),Dict{String,Any})) == original + end +end + +@testitem "ImportExport / physical declarations survive JSON transport" tags=[:unit, :importexport] begin + using JSON3 + import LineCableModels.ImportExport as IE + + copper = Material(kind=:conductor, rho=1.7241e-8) + dielectric = Material(kind=:insulator, rho=1e14, eps_r=2.3) + wire = Region(:wire, Disk(0.5e-3), copper) + path = Helix(LayAngle(0.2); dir=-1, φ0=0.3) + group = Group(:core, wire; pattern=Ring(6; r=2e-3), path) + parts = Stack(wire, insulation(dielectric; t=0.2e-3)) + member = terminal(:core, parts) + enclosure = duct(member; shape=Ellipse(4e-3, 3e-3), fill=dielectric, + wall=insulation(dielectric; t=0.5e-3)) + declarations = ( + Ellipse(4e-3, 3e-3), + Polygon(((-1e-3, -1e-3), (1e-3, -1e-3), (0.0, 1e-3))), + LayAngle(0.2), path, wire, group, parts, enclosure, + ) + for original in declarations + encoded = IE.serialize_value(original) + parsed = JSON3.read(JSON3.write(encoded), Dict{String, Any}) + restored = IE.deserialize_value(parsed) + @test restored == original + @test typeof(restored) === typeof(original) + @test IE.serialize_value(restored) == encoded + end + metadata = (voltage=220.0, description="elliptical duct", positions=(1.0, 2.0)) + @test IE.deserialize_value(IE.serialize_value(metadata)) == Dict( + "voltage"=>220.0, "description"=>"elliptical duct", "positions"=>[1.0, 2.0]) + @test_throws ArgumentError IE.deserialize_value(Dict("__type__"=>"FutureScalar")) + @test_throws ArgumentError IE.deserialize_value(Dict("kind"=>"future_declaration")) + invalid = IE.serialize_value(parts) + invalid["items"][1]["kind"] = "future_physical_node" + @test_throws ArgumentError IE.deserialize_value(invalid) + missing_material = IE.serialize_value(wire) + missing_material["material"] = "not_in_the_library" + @test_throws KeyError IE.deserialize_value(missing_material) +end + +@testitem "ImportExport / enclosed assemblies retain geometry and material ownership" tags=[:unit, :importexport] begin + using JSON3 + import LineCableModels.ImportExport as IE + + copper = Material(kind=:conductor, rho=1.7241e-8) + dielectric = Material(kind=:insulator, rho=1e14, eps_r=2.3) + air = Material(kind=:insulator, rho=Inf, eps_r=1.0) + member = terminal(:core, core(copper; r=0.5e-3), + insulation(dielectric; t=0.2e-3)) + repeated = cores(member; n=3, r=2e-3, names=(:a, :b, :c)) + explicit = assembly( + at(terminal(:a, core(copper; r=0.5e-3)), -2e-3, 0.0), + at(terminal(:b, core(copper; r=0.5e-3)), 2e-3, 0.0), + ) + library = CablesLibrary() + for (id, contents) in (("repeated", repeated), ("explicit", explicit)) + design = build(CableDesign, id, + duct(contents; shape=Ellipse(6e-3, 4e-3), fill=air, + wall=insulation(dielectric; t=0.5e-3))) + add!(library, design) + end + mktempdir() do directory + path = joinpath(directory, "assemblies.json") + save(library; file_name=path) + document = JSON3.read(read(path, String), Dict{String, Any}) + @test length(document["materials"]) == 3 + restored = CablesLibrary() + load!(restored; file_name=path) + @test Set(keys(restored)) == Set(keys(library)) + for id in keys(library) + expected, actual = library[id], restored[id] + # JSON arrays intentionally normalize tuple or vector declarations. + @test JSON3.read(JSON3.write(IE.serialize_value(actual)), Dict{String, Any}) == + JSON3.read(JSON3.write(IE.serialize_value(expected)), Dict{String, Any}) + @test actual.terminal_order == expected.terminal_order + @test actual.terminal_map == expected.terminal_map + @test [region.source.material for region in actual.geometry.regions] == + [region.source.material for region in expected.geometry.regions] + @test [area(region.primitive) for region in actual.geometry.regions] ≈ + [area(region.primitive) for region in expected.geometry.regions] + @test area(boundary(actual.geometry)) ≈ area(boundary(expected.geometry)) + end + end +end + +@testitem "ImportExport / v1 declaration round trip excludes derived state" tags=[:unit, :importexport, :slow] setup=[ + UseImportExportSupport, + TestFixtures +] begin + import LineCableModels.ImportExport as IE + + design=TestFixtures.coaxial_design() + encoded=IE.serialize_value(design) + @test encoded["kind"] == "cable_design" + @test encoded["cable_id"] == design.cable_id + @test !haskey(encoded, "reference_frequency") + @test haskey(encoded, "origin") + @test !haskey(encoded, "root") + @test encoded["origin"] == IE.serialize_value(design.origin) + + function contains_key(value, key) + value isa AbstractDict&&( + haskey(value, key)||any(item->contains_key(item, key), values(value)) + )&&return true + value isa AbstractVector&&return any(item->contains_key(item, key), value) + return false + end + for derived in ( + "geometry", "terminal_order", "terminal_map", "effective", + "connection_order", "engine", "grids" + ) + @test !contains_key(encoded, derived) + end + + restored=IE.deserialize_value(encoded) + @test restored isa CableDesign + @test restored !== design + @test IE.serialize_value(restored) == encoded + @test restored.terminal_order == design.terminal_order + @test getproperty.(restored.geometry.regions, :primitive) == + getproperty.(design.geometry.regions, :primitive) + @test getproperty.(restored.geometry.regions, :terminal) == + getproperty.(design.geometry.regions, :terminal) + @test restored.terminal_map == design.terminal_map + + absent=deepcopy(encoded) + delete!(absent, "origin") + @test_throws r"origin" IE.deserialize_value(absent) + + malformed=deepcopy(encoded) + empty!(malformed["origin"]["items"]) + @test_throws ArgumentError IE.deserialize_value(malformed) + + earth=EarthModel(100.0, 10.0, 1.0) + connections=Dict( + terminal=>(index==1 ? 1 : 0) + for (index, terminal) in enumerate(design.terminal_order) + ) + system=build( + LineCableSystem, + design, + Pose2(0.0, -1.0); + connections, + environment = earth, + system_id = "round-trip", + line_length = 1000.0 + ) + encoded_system=IE.serialize_value(system) + for derived in ("geometry", "terminal_order", "terminal_map", "connection_order") + @test !contains_key(encoded_system, derived) + end + restored_system=IE.deserialize_value(encoded_system) + @test IE.serialize_value(restored_system) == encoded_system + @test restored_system.terminal_order == system.terminal_order + @test restored_system.connection_order == system.connection_order + @test restored_system.environment.vertical_layers == earth.vertical_layers + @test restored_system.environment.layers == earth.layers + + problem=LineParametersProblem( + system; + temperature = 35.0, + earth_props = earth, + frequencies = [0.1, 50.0, 1.0e6] + ) + obsolete = IE.serialize_value(problem) + obsolete["Gamma"] = IE.serialize_value(ComplexF64[0, 1e-5im, 2e-4+3e-4im]) + @test_throws ArgumentError IE.deserialize_value(obsolete) + obsolete["Gamma"] = nothing + @test_throws ArgumentError IE.deserialize_value(obsolete) + mktempdir() do directory + path=joinpath(directory, "problem.json") + @test export_data(:json, problem; file_name = path) == abspath(path) + restored_problem=import_data( + :json, + LineParametersProblem; + file_name = path + ) + @test IE.serialize_value(restored_problem) == IE.serialize_value(problem) + @test restored_problem.system.terminal_order == system.terminal_order + @test restored_problem.system.connection_order == system.connection_order + end + + parametric_system_record=deepcopy(encoded_system) + parametric_system_record["positions"]=IE.serialize_value(Grid(( + Pose2(-0.1, -1.0), + Pose2(0.1, -1.0) + ))) + system_space=IE.deserialize_value(parametric_system_record) + @test system_space isa Gridspace{LineCableSystem} + @test eltype(system_space) === Any + @test Base.IteratorEltype(typeof(system_space)) isa Base.EltypeUnknown + @test getproperty.(collect(system_space), :positions) == [ + [Pose2(-0.1, -1.0)], + [Pose2(0.1, -1.0)] + ] + + copper=Material(kind = :conductor, rho = 1.7241e-8) + compacted=build( + CableDesign, + "compacted-round-trip", + terminal( + :core, + stranded( + copper; + shape = Disk(0.35e-3), + compact = true, + boundary = Disk(sqrt(19) * 0.35e-3) + ) + ) + ) + compacted_record=IE.serialize_value(compacted) + restored_compacted=IE.deserialize_value(compacted_record) + @test IE.serialize_value(restored_compacted) == compacted_record + @test getproperty.(restored_compacted.geometry.regions, :terminal) == + getproperty.(compacted.geometry.regions, :terminal) + @test area.(getproperty.(restored_compacted.geometry.regions, :primitive)) == + area.(getproperty.(compacted.geometry.regions, :primitive)) + @test all( + region -> region.source.primitive isa Disk && + region.primitive isa LineCableModels.DataModel.Polygon, + filter(region -> region.source.material.kind === :conductor, + restored_compacted.geometry.regions) + ) + + packed=build( + CableDesign, + "bounded-round-trip", + terminal( + :core, + stranded( + copper; + shape = Disk(0.35e-3), + boundary = Disk(2e-3) + ) + ) + ) + packed_record=IE.serialize_value(packed) + restored_packed=IE.deserialize_value(packed_record) + @test restored_packed == packed + @test IE.serialize_value(restored_packed) == packed_record + + scheduled=build( + CableDesign, + "scheduled-round-trip", + terminal( + :core, + stranded( + copper; + shape = Disk(0.35e-3), + lay = (LayRatio(12), Pitch(0.1)), + dir = (1, -1), + φ0 = (0.0, 0.2), + boundary = Disk(2e-3) + ) + ) + ) + scheduled_record=IE.serialize_value(scheduled) + restored_scheduled=IE.deserialize_value(scheduled_record) + @test restored_scheduled == scheduled + @test IE.serialize_value(restored_scheduled) == scheduled_record + + sector_boundary=Sector( + span = deg2rad(119.0), + r_base = 1.10e-3, + r_back = 10.24e-3, + fillet = 1.02e-3 + ) + bounded=build( + CableDesign, + "bounded-stranded-round-trip", + terminal( + :core, + stranded( + copper; + shape = Disk(0.35e-3), + boundary = sector_boundary + ) + ) + ) + bounded_record=IE.serialize_value(bounded) + @test bounded_record["origin"]["item"]["items"][1]["item"]["compact"] === nothing + restored_bounded=IE.deserialize_value(bounded_record) + @test restored_bounded == bounded + @test IE.serialize_value(restored_bounded) == bounded_record + bounded_group=only(bounded.origin.item.items).item + @test bounded_group isa Group + @test bounded_group.boundary == sector_boundary + @test bounded_group.compact === nothing + + declarations=( + Ring(capacity(); r = nothing, φ0 = 0.2, gap_frac = 0.03), + Polar(nr = 2, nφ = 6, r0 = 0.0, dr = 2e-3), + Fill(r = 10e-3, φ = pi/6), + Lattice(nx = 2, ny = 3, dx = 10e-3, dy = 12e-3), + Helix(LayRatio(11); dir = -1, φ0 = 0.1), + FillFactor(0.9) + ) + for declaration in declarations + @test IE.deserialize_value(IE.serialize_value(declaration)) == declaration + end + + left=terminal(:left, core(copper; r = 0.8e-3)) + right=terminal(:right, core(copper; r = 1.0e-3)) + explicit=assembly(at(left, -2e-3, 0), at(right, 2e-3, 0; φ = 0.1)) + explicit_record=IE.serialize_value(explicit) + @test explicit_record["kind"] == "assembly" + @test haskey(explicit_record, "members") + @test IE.deserialize_value(explicit_record) == explicit + + repeated=cores( + terminal(:core, core(copper; r = 0.5e-3)); + n = 3, + r = 2e-3, + names = (:a, :b, :c) + ) + repeated_record=IE.serialize_value(repeated) + @test repeated_record["kind"] == "assembly" + @test haskey(repeated_record, "pattern") + @test !haskey(repeated_record, "members") + @test IE.deserialize_value(repeated_record) == repeated + + descriptive=build( + CableDesign, + "descriptive-data", + left; + nominal_data = (rated_voltage = 220.0, designation = "test") + ) + restored_descriptive=IE.deserialize_value(IE.serialize_value(descriptive)) + @test restored_descriptive.nominal_data == descriptive.nominal_data + + library=CablesLibrary() + datasheet_record=DatasheetInfo(designation_code = "MV cable", voltage_kv = 30.0) + add!(library, design; datasheet = datasheet_record) + mktempdir() do directory + for extension in ("json", "jls") + path=joinpath(directory, "library.$extension") + save(library; file_name = path) + restored_library=CablesLibrary() + @test load!(restored_library; file_name = path) === restored_library + @test only(keys(restored_library)) == design.cable_id + @test IE.serialize_value(only(values(restored_library))) == encoded + restored_datasheet=datasheet(restored_library, design.cable_id) + @test restored_datasheet.designation_code == datasheet_record.designation_code + @test restored_datasheet.voltage_kv == datasheet_record.voltage_kv + end + end +end + +@testitem "ImportExport / Draft 2020-12 cable document" tags=[:unit, :importexport] setup=[ + UseImportExportSupport, + TestFixtures +] begin + using JSON3 + using JSONSchema + import LineCableModels.ImportExport as IE + + materials=MaterialsLibrary(add_defaults = false) + add!(materials, "copper", TestFixtures.conductor_material()) + materials_document=IE._json_document(materials) + @test materials_document["\$schema"] == IE.JSON_SCHEMA_DIALECT + @test materials_document["format"] == IE.MATERIALS_SCHEMA + @test materials_document["version"] == "1.0.0" + + cables=CablesLibrary() + add!( + cables, + TestFixtures.coaxial_design(); + datasheet = DatasheetInfo(designation_code = "NA2XS(FL)2Y", U = 30.0) + ) + cables_document=IE._json_document(cables) + @test cables_document["\$schema"] == IE.JSON_SCHEMA_DIALECT + @test cables_document["format"] == IE.CABLES_SCHEMA + @test cables_document["version"] == "1.0.0" + @test cables_document["root"]["kind"] == "cable_library" + @test only(values(cables_document["root"]["cables"]))["datasheet"] == Dict( + "designation_code" => "NA2XS(FL)2Y", + "U" => IE.serialize_value(30.0) + ) + @test length(cables_document["materials"]) < + length(first(values(cables)).geometry.regions) + schema_path=joinpath( + pkgdir(LineCableModels), "schemas", "cable-design.schema.json" + ) + @test isfile(schema_path) + schema=JSONSchema.Schema(JSON3.read(read(schema_path, String), Dict{String, Any})) + @test JSONSchema.validate(schema, cables_document) === nothing + @test JSONSchema.validate(schema, materials_document) === nothing + @test all(value -> haskey(value, "origin") && !haskey(value, "root"), + values(cables_document["root"]["cables"])) + packed_cables=CablesLibrary() + add!(packed_cables, + build( + CableDesign, + "bounded-schema", + terminal( + :core, + stranded( + TestFixtures.conductor_material(); + shape = Disk(0.5e-3), + boundary = Disk(3e-3) + ) + ) + )) + @test JSONSchema.validate( + schema, + IE._json_document(packed_cables) + ) === nothing + + sector_cables=CablesLibrary() + add!(sector_cables, + build( + CableDesign, + "sector-bounded-schema", + terminal( + :core, + stranded( + TestFixtures.conductor_material(); + shape = Disk(0.5e-3), + boundary = Sector( + span = 2pi / 3, + r_base = 0.75e-3, + r_back = 4.0e-3, + fillet = 0.15e-3 + ) + ) + ) + )) + sector_document=IE._json_document(sector_cables) + @test JSONSchema.validate(schema, sector_document) === nothing + invalid_sector=deepcopy(sector_document) + sector_design=only(values(invalid_sector["root"]["cables"])) + sector_design["origin"]["item"]["items"][1]["item"]["compact"]=true + @test JSONSchema.validate(schema, invalid_sector) !== nothing + + earth=EarthModel(100.0, 10.0, 1.0) + design=TestFixtures.coaxial_design() + connections=Dict( + terminal=>(index==1 ? 1 : 0) + for (index, terminal) in enumerate(design.terminal_order) + ) + system=build( + LineCableSystem, + design, + Pose2(0.0, -1.0); + connections, + environment = earth + ) + system_document=IE._json_document(system) + @test JSONSchema.validate(schema, system_document) === nothing + + parametric_document=deepcopy(cables_document) + raw_design=only(values(parametric_document["root"]["cables"])) + function referenced_tags(node, material_name) + node isa AbstractDict||return String[] + tags=String[] + get(node, "kind", nothing)=="region"&& + get(node, "material", nothing)==material_name&& + push!(tags, node["tag"]) + for child_name in ("items", "item", "fill", "wall", "origin") + child=get(node, child_name, nothing) + child isa AbstractVector&&foreach( + item->append!(tags, referenced_tags(item, material_name)), child + ) + child isa AbstractDict&&append!(tags, referenced_tags(child, material_name)) + end + return tags + end + material_names=collect(keys(parametric_document["materials"])) + material_name=first(sort( + material_names; + by = name->length(referenced_tags(raw_design, name)), + rev = true + )) + varied_tags=referenced_tags(raw_design, material_name) + @test length(varied_tags) > 1 + parametric_document["materials"][material_name]["rho"]=IE.serialize_value(Grid(( + 1.7e-8, 2.0e-8))) + @test JSONSchema.validate(schema, parametric_document) === nothing + decoded_materials=IE._document_materials(parametric_document) + design_space=IE._decode_design(raw_design, decoded_materials) + @test design_space isa Gridspace{CableDesign} + @test length(design_space) == 2 + decoded_designs=collect(design_space) + @test all(design->design isa CableDesign, decoded_designs) + for design in decoded_designs + selected_rhos=unique( + region.source.material.rho + for region in design.geometry.regions + if String(region.source.tag) in varied_tags + ) + @test length(selected_rhos) == 1 + end + @test only(unique( + region.source.material.rho + for region in decoded_designs[1].geometry.regions + if String(region.source.tag) in varied_tags + )) != only(unique( + region.source.material.rho + for region in decoded_designs[2].geometry.regions + if String(region.source.tag) in varied_tags + )) + + invalid=deepcopy(cables_document) + delete!(only(values(invalid["root"]["cables"])), "origin") + @test JSONSchema.validate(schema, invalid) !== nothing + + @test_throws ArgumentError IE._json_path("library.archive") + @test endswith(IE._json_path("library"), "library.json") +end + +@testitem "ImportExport / shared material references use the declaration encoder" tags=[:unit, :importexport] begin + import LineCableModels.ImportExport as IE + copper = Material(kind=:conductor, rho=1.72e-8) + dielectric = Material(kind=:insulator, rho=Inf, eps_r=2.3) + wire = Region(:wire, Disk(0.001), copper) + group = Group(:core, wire; pattern=Ring(3; r=0.01)) + design = build(CableDesign, "material-references", group) + material_name(material) = material === copper ? "copper" : "dielectric" + encoded = IE.serialize_value(design; material_name) + @test encoded["origin"]["item"]["material"] == "copper" + @test IE.serialize_value(design)["origin"]["item"]["material"] == IE.serialize_value(copper) + enclosure = duct(terminal(:core, wire); shape=Ellipse(0.01, 0.008), + fill=dielectric, wall=insulation(dielectric; t=0.001)) + enclosed = IE.serialize_value(enclosure; material_name) + @test enclosed["fill"] == "dielectric" + @test enclosed["wall"]["material"] == "dielectric" + @test only(enclosed["item"]["item"]["items"])["material"] == "copper" + library = CablesLibrary() + add!(library, design) + document = IE._json_document(library) + reference = document["root"]["cables"][design.cable_id]["origin"]["item"]["material"] + @test document["materials"][reference] == IE._material_record(copper) + @test length(document["materials"]) == 1 +end diff --git a/test/unit/materials/library.jl b/test/unit/materials/library.jl new file mode 100644 index 000000000..c7e64c7e7 --- /dev/null +++ b/test/unit/materials/library.jl @@ -0,0 +1,135 @@ +@testitem "Materials / Material / numeric normalization and conversion" tags=[:unit, :measurements] setup=[ + UseDataModelSupport +] begin + ordinary=Material( + :conductor, Float32(1.7e-8), big"2.3", 1, 20, Float32(0.004); + rho_thermal = Float32(0.25), theta_max = 95, + tan_delta = Float32(0.01), sigma_solar = 0.5 + ) + @test ordinary isa Material{BigFloat} + @test eltype(ordinary) === BigFloat + @test ordinary.kind === :conductor + + converted=convert(Material{Float32}, ordinary) + @test converted isa Material{Float32} + @test converted.rho ≈ Float32(ordinary.rho) + @test converted.kind === ordinary.kind + + defaulted=Material(:conductor, Float32(1.7e-8)) + @test defaulted isa Material{Float32} + @test defaulted.kind === :conductor + + uncertain=Material( + :conductor, + measurement(1.7e-8, 0.1e-8), + 2.3, + 1.0, + 20.0, + 0.004 + ) + @test eltype(uncertain) <: Measurement + @test uncertainty(uncertain.rho) > 0 + @test_throws ArgumentError Material(Symbol(""), 1.0) +end + +@testitem "Materials / MaterialsLibrary / dictionary and presentation behavior" tags=[:unit, :materials] setup=[ + UseDataModelSupport +] begin + empty_library=MaterialsLibrary(add_defaults = false) + @test empty_library isa AbstractDict{String, Material} + @test keytype(typeof(empty_library)) === String + @test valtype(typeof(empty_library)) === Material + @test eltype(typeof(empty_library)) === Pair{String, Material} + @test pairs(empty_library) === empty_library + @test isempty(empty_library) + @test sprint(show, MIME("text/plain"), empty_library) == + "MaterialsLibrary · 0 materials" + + copper=Material(:conductor, 1.7241e-8, 1.0, 1.0, 20.0, 0.00393) + @test add!(empty_library, :copper, copper) === empty_library + @test empty_library["copper"] === copper + @test collect(empty_library) isa Vector{Pair{String, Material}} + @test collect(keys(empty_library)) == ["copper"] + @test_throws ArgumentError add!(empty_library, "copper", copper) + + fallback=Material(:insulator, Inf, 1.0, 1.0, 20.0, 0.0) + @test get(empty_library, "missing", fallback) === fallback + @test get(() -> fallback, empty_library, "missing") === fallback + @test get(() -> fallback, empty_library, "copper") === copper + @test_throws MethodError get(empty_library, "missing") + @test !haskey(empty_library, :copper) + @test get(empty_library, :copper, fallback) === fallback + @test_throws KeyError empty_library[:copper] + + replacement=Material(:conductor, 1.8e-8, 1.0, 1.0, 20.0, 0.004) + @test setindex!(empty_library, replacement, "copper") === empty_library + @test empty_library["copper"] === replacement + @test (empty_library["copper"] = copper) === copper + @test empty_library["copper"] === copper + empty_library["copper"] = replacement + + copied=copy(empty_library) + @test copied isa MaterialsLibrary + @test copied !== empty_library + @test copied.data !== empty_library.data + @test copied["copper"] === replacement + @test delete!(copied, "copper") === copied + @test haskey(empty_library, "copper") + + blank=empty(empty_library) + @test blank isa MaterialsLibrary + @test isempty(blank) + @test empty!(copied) === copied + @test isempty(copied) + + @test delete!(empty_library, "copper") === empty_library + @test isempty(empty_library) + @test delete!(empty_library, "missing") === empty_library + @test delete!(empty_library, :missing) === empty_library + + defaults=MaterialsLibrary() + @test all(name -> haskey(defaults, name), ("air", "copper", "xlpe", "steel")) + shown=sprint(show, MIME("text/plain"), defaults) + @test contains(shown, "copper") + @test contains(shown, "$(length(defaults)) materials") + @test occursin("ρ", sprint(show, MIME("text/plain"), defaults["copper"])) + @test occursin("nΩ·m", sprint(show, MIME("text/plain"), defaults["copper"])) +end + +@testitem "Materials / presentation / bounded collection summaries" tags=[:unit, :materials] setup=[ + UseDataModelSupport +] begin + library=MaterialsLibrary(add_defaults = false) + for index in 1:7 + add!(library, "material-$index", Material(:conductor, index, 1.0, 1.0, 20.0, 0.0)) + end + @test eltype(typeof(first(values(library)))) === Float64 + @test length(collect(values(library))) == 7 + + summary=sprint(show, MIME"text/plain"(), library) + @test occursin("MaterialsLibrary · 7 materials", summary) + @test occursin("material-1", summary) + + reverse_library=MaterialsLibrary(add_defaults = false) + for index in 7:-1:1 + add!( + reverse_library, + "material-$index", + Material(:conductor, index, 1.0, 1.0, 20.0, 0.0) + ) + end + @test sprint(show, MIME"text/plain"(), reverse_library) == summary + + narrow=sprint(show, MIME"text/plain"(), library; + context = IOContext(stdout, :limit=>true, :displaysize=>(5, 80))) + @test occursin("⋮", narrow) + @test occursin("more materials", narrow) + + # MaterialsLibrary owns deterministic semantic display. Its backing Dict + # retains ordinary Base display. The package leaves Dict methods unchanged. + @test parentmodule(which(show, ( + IO, + MIME"text/plain", + typeof(library.data) + ))) === Base +end diff --git a/test/unit/materials/temperaturedependent.jl b/test/unit/materials/temperaturedependent.jl new file mode 100644 index 000000000..0cfb8edc6 --- /dev/null +++ b/test/unit/materials/temperaturedependent.jl @@ -0,0 +1,62 @@ +@testitem "Materials / temperature law / equations, types and reference calibration" tags=[:unit, :measurements] begin + using Measurements + const TD = LineCableModels.Materials.TemperatureDependent + selected = TD.Formula(formula(:default)) + @test Set(TD.formulas(TD.Formula)) == Set((:default,:linear)) + @test formula_id(selected) === :linear + for T in (Float32, Float64, BigFloat) + material = Material(:conductor, T(1.72e-8), one(T), one(T), T(20), T(0.004)) + for temperature in T.((20, 60, 80)) + rho = @inferred constitutive(selected, material, temperature) + @test rho isa T + @test rho ≈ material.rho * (1 + material.alpha * (temperature - material.T0)) + @test inv(rho) ≈ inv(material.rho) / (1 + material.alpha * (temperature - material.T0)) + end + @test constitutive(nothing, material, T(250)) === material.rho + @test material.rho == T(1.72e-8) + @test material.T0 == T(20) + @test material.alpha == T(0.004) + @test_throws DomainError constitutive(selected, material, T(170)) + @test_throws DomainError constitutive(selected, material, T(-130)) + @test constitutive(selected, material, T(169)) > material.rho + @test_throws DomainError constitutive(nothing, material, T(Inf)) + end + fixed = Material(:conductor, 1.72e-8) + @test constitutive(selected, fixed, 80.0) == fixed.rho + passive = Material(:insulator, Inf, 2.3, 1, 20, -0.003) + @test constitutive(selected, passive, 80.0) == Inf + invalid = Material(:semicon, 1e3, 10, 1, 20, -0.05) + @test_throws DomainError constitutive(selected, invalid, 40.0) + uncertain_material = Material(:conductor, measurement(1.72e-8, 1e-10), + 1, 1, 20, 0.004) + rho = constitutive(selected, uncertain_material, measurement(80.0, 1.0)) + @test nominal(rho) ≈ 1.72e-8 * 1.24 + @test uncertainty(rho) ≈ hypot(1.24e-10, 1.72e-8 * 0.004) + @test uncertainty(uncertain_material.rho) == 1e-10 +end + +@testitem "Materials / temperature law / custom selections and applicability" tags=[:unit, :engine] setup=[FormulaFixtures] begin + const TD=LineCableModels.Materials.TemperatureDependent + const M=FormulaFixtures + selected=M.ConstantResistivity(2e-8) + material=Material(:conductor,1.72e-8,1,1,20,0.004) + @test TD.Formula(selected) === selected + @test constitutive(selected,material,250.0)==2e-8 + @test only(selected.seen)[2:3]==(250.0,nothing) + @test_throws ArgumentError TD.Formula(:unknown) + @test_throws ArgumentError TD.Formula(:default;parameters=(alpha=0.1,)) + @test_throws ArgumentError TD.Formula(:default;options=(integration=(method=:quad,),)) + @test_throws ArgumentError TD.Formula(formula(:default;equivalent_earth=:default)) + for value in (0.0,-1.0,NaN,Inf,1+im,[1.0]) + @test_throws DomainError constitutive(M.ConstantResistivity(value),material,80.0) + end + for constructor in (Formulation,CableConstantsFormulation,LineCableModelsFEM) + @test constructor(temperature_dependence=nothing).methods.temperature_dependence === nothing + @test constructor(temperature_dependence=selected).methods.temperature_dependence === selected + @test_throws ArgumentError constructor(options=(temperature_correction=true,)) + grid=constructor(temperature_dependence=Grid((formula(:default),nothing))) + @test length(grid)==2 + @test first(grid).methods.temperature_dependence isa TD.Formula + @test last(collect(grid)).methods.temperature_dependence === nothing + end +end diff --git a/test/unit/modalanalysis/composition.jl b/test/unit/modalanalysis/composition.jl new file mode 100644 index 000000000..b917c040e --- /dev/null +++ b/test/unit/modalanalysis/composition.jl @@ -0,0 +1,35 @@ +@testitem "ModalAnalysis / composition / any formulation that delivers line parameters" tags=[:unit, :parametric] setup=[TestFixtures] begin + # A test-owned formulation returns the same phase-domain result for every problem. + struct FixedPhase{P <: LineParameters} <: AbstractFormulation + parameters::P + end + function LineCableModels.compute(::LineParametersProblem, formulation::FixedPhase; + options::Union{NamedTuple, ComputationOptions} = ComputationOptions()) + return formulation.parameters + end + frequencies = [50.0, 80.0] + problem = TestFixtures.three_bare_wires_problem(; frequencies) + phase = compute(problem, Formulation()) + fixed = FixedPhase(phase) + modal = ModalAnalysisFormulation(:default) + direct = compute(ModalAnalysisProblem(phase), modal) + same(value) = Z(value) == Z(direct) && Y(value) == Y(direct) && gamma(value) == gamma(direct) + + @test same(compute(problem, fixed, modal)) + space = Gridspace{LineParametersProblem}( + rho -> TestFixtures.three_bare_wires_problem(; rho, frequencies), + (Grid((10.0, 100.0)),)) + @test same(compute(first(LineCableModels.points(space)), fixed, modal)) + for composed in (compute(space, fixed; modal = :default), compute(space, fixed, modal), + compute(ParametricProblem(space), Combinatorial(fixed), modal), + compute(ParametricProblem(space), Combinatorial(fixed); modal = :default)) + @test composed isa ParametricResult + @test length(composed) == 2 + @test all(same, composed) + end + study = compute(ParametricProblem(space), Combinatorial(fixed)) + twostep = compute(Gridspace{ModalAnalysisProblem}(study), modal) + composed = compute(ParametricProblem(space), Combinatorial(fixed), modal) + @test [Z(value) for value in composed] == [Z(value) for value in twostep] + @test [gamma(value) for value in composed] == [gamma(value) for value in twostep] +end diff --git a/test/unit/modalanalysis/computation.jl b/test/unit/modalanalysis/computation.jl new file mode 100644 index 000000000..ad3742f24 --- /dev/null +++ b/test/unit/modalanalysis/computation.jl @@ -0,0 +1,217 @@ +@testitem "ModalAnalysis / retained directions, finite responses, transport, and diagnostics" tags=[:unit, :importexport, :slow] begin + using Test + using LinearAlgebra + using Serialization + import LineCableModels.ModalAnalysis as MA + import LineCableModels.Engine as E + import LineCableModels.Commons as G + + struct FixedModal <: AbstractFormulation + voltage::Array{ComplexF64,3} + current::Array{ComplexF64,3} + roots::Matrix{ComplexF64} + allocations::Base.RefValue{Int} + calculations::Base.RefValue{Int} + common::Base.RefValue{Any} + end + FixedModal(voltage,current,roots,allocations,calculations) = + FixedModal(voltage,current,roots,allocations,calculations,Ref{Any}(nothing)) + LineCableModels.formula_id(::FixedModal)=:fixed_modal + LineCableModels.description(::FixedModal; compact::Bool=false)="fixed_modal" + Base.NamedTuple(::FixedModal)=(identifier=:fixed_modal,) + G.formulation_options(::FixedModal)=FormulationOptions() + function G.initialize_buffers(selected::FixedModal,::Type{T},input,plan,buffers) where {T} + selected.allocations[]+=1 + @test size(buffers.admittance_impedance_product)==(plan.n,plan.n) + @test eltype(buffers.admittance_impedance_product)==T + selected.common[]=buffers + return merge(buffers,(fixed=zeros(ComplexF64,plan.n,plan.n),)) + end + function MA.decompose!(selected::FixedModal,workspace,parameters,options) + selected.calculations[]+=1 + @test all(key -> getproperty(workspace.buffers,key) === + getproperty(selected.common[],key),keys(selected.common[])) + copyto!(workspace.Tv,selected.voltage) + copyto!(workspace.Ti,selected.current) + copyto!(workspace.roots,selected.roots) + return workspace + end + + Tv0=ComplexF64[1+0.2im 0.3-0.4im;0.2+0.1im 1.4-0.2im] + Ti0=ComplexF64[1.2+0.3im 0.2+0.1im;0.4-0.2im 1.1+0.5im] + z=ComplexF64[2+3im,4+1im] + y=ComplexF64[0.1+0.2im,0.2+0.3im] + roots=sqrt.(z.*y) + Zp=Tv0*Diagonal(z)/Ti0 + Yp=Ti0*Diagonal(y)/Tv0 + details0=ComputationDetails((inputs=(system=(line_length=600.0,),),)) + phase=LineParameters(PhaseDomain,SeriesImpedance(reshape(Zp,2,2,1)), + ShuntAdmittance(reshape(Yp,2,2,1)),[50.0],details0) + selected=FixedModal(reshape(Tv0,2,2,1),reshape(Ti0,2,2,1), + reshape(roots,2,1),Ref(0),Ref(0)) + mf=ModalAnalysisFormulation(selected) + modal=compute(ModalAnalysisProblem(phase),mf;options=(rotate=false,)) + @test selected.allocations[]==1 + @test selected.calculations[]==1 + @test Tv(modal)[:,:,1]==Tv0 + @test Ti(modal)[:,:,1]==Ti0 + @test gamma(modal)[:,1]==roots + @test modal.Z.values[:,:,1]≈Diagonal(z) + @test modal.Y.values[:,:,1]≈Diagonal(y) + @test Z(transform(PhaseDomain,modal))[:,:,1]≈Zp + @test Y(transform(PhaseDomain,modal))[:,:,1]≈Yp + @test Zc(modal)[:,1]≈z./roots + @test Yc(modal)[:,1]≈y./roots + @test Zc(modal,PhaseDomain)[:,:,1]≈Tv0*Diagonal(z./roots)/Ti0 + @test Yc(modal,PhaseDomain)[:,:,1]≈Ti0*Diagonal(y./roots)/Tv0 + + segment=PropagationParameters(modal) + @test line_length(segment)==600.0 + short=PropagationParameters(segment;line_length=150.0) + zero_segment=PropagationParameters(segment;line_length=0.0) + @test H(short)[:,1]≈exp.(-roots.*150.0) + @test H(zero_segment)==ones(ComplexF64,2,1) + @test H(zero_segment,PhaseDomain;field=:voltage)[:,:,1]≈Matrix{ComplexF64}(I,2,2) + @test H(short,PhaseDomain;field=:voltage)[:,:,1]≈Tv0*Diagonal(H(short)[:,1])/Tv0 + @test H(short,PhaseDomain;field=:current)[:,:,1]≈Ti0*Diagonal(H(short)[:,1])/Ti0 + @test H(short,PhaseDomain;field=:voltage)[:,:,1]*Zc(short,PhaseDomain)[:,:,1]≈ + Zc(short,PhaseDomain)[:,:,1]*H(short,PhaseDomain;field=:current)[:,:,1] + zero_roots=LineParameters(E.ModalDomain(operators(modal),zeros(ComplexF64,size(gamma(modal)))), + modal.Z,modal.Y,modal.f,modal.details) + first_zero=PropagationParameters(zero_roots;line_length=100.0) + second_zero=PropagationParameters(first_zero;line_length=200.0) + @test H(first_zero)==H(second_zero) + @test details(first_zero).data.gridpoint!=details(second_zero).data.gridpoint + @test selected.calculations[]==1 + + total=LineParameters(PhaseDomain,SeriesImpedance(reshape(Zp.*600,2,2,1);basis=:total), + ShuntAdmittance(reshape(Yp.*600,2,2,1);basis=:total),[50.0],details0) + total_selected=FixedModal(selected.voltage,selected.current,selected.roots.*600, + Ref(0),Ref(0)) + total_modal=compute(ModalAnalysisProblem(total),ModalAnalysisFormulation(total_selected)) + @test gamma(total_modal)[:,1]≈roots.*600 + @test Zc(total_modal)≈Zc(modal) + @test gamma(PropagationParameters(total_modal))≈gamma(segment) + @test H(PropagationParameters(total_modal))≈H(segment) + + phase_results=ParametricResult(Combinatorial(mf),[phase,phase]) + problems=Gridspace{ModalAnalysisProblem}(phase_results) + @test length(problems)==2 + run=compute(problems,mf) + @test length(run)==2 + @test eltype(run)<:LineParameters + @test size.(gamma.(run))==[(2,1),(2,1)] + @test !Base.mightalias(Tv(modal),Tv(run[1])) + @test !Base.mightalias(Ti(modal),Ti(run[1])) + @test !Base.mightalias(gamma(modal),gamma(run[1])) + @test !Base.mightalias(Z(modal),Z(run[1])) + @test !Base.mightalias(details(modal).data.modal.diagnostics.z_coupling, + details(run[1]).data.modal.diagnostics.z_coupling) + @test length(Tv.(run))==2 + @test length(Ti.(run))==2 + @test Zc.(run,Ref(PhaseDomain))[1]==Zc(run[1],PhaseDomain) + @test length(PropagationParameters.(run))==2 + @test length(collect(Gridspace{PropagationParameters}(run)))==2 + segments=collect(Gridspace{PropagationParameters}(run)) + finite_observed=ObservedResult(segments[1],(H,)) + @test finite_observed.gridpoint.source_gridpoint== + details(segments[1]).data.source_gridpoint + @test finite_observed.gridpoint.source_gridpoint== + details(run[1]).data.gridpoint + @test finite_observed.gridpoint.source_gridpoint!= + details(phase).data.gridpoint + rebound=PropagationParameters(segments[1];line_length=75.0) + rebound_observed=ObservedResult(rebound,(H,)) + @test rebound_observed.gridpoint.source_gridpoint== + details(segments[1]).data.gridpoint + @test H.(segments)[1]==H(segments[1]) + @test H.(segments,Ref(PhaseDomain);field=:current)[2]== + H(segments[2],PhaseDomain;field=:current) + segment_lengths=PropagationParameters(modal;line_length=Grid((100.0,200.0))) + @test segment_lengths isa Gridspace{PropagationParameters} + @test line_length.(collect(segment_lengths))==[100.0,200.0] + zipped=Gridspace{PropagationParameters}((parameters,length) -> + PropagationParameters(parameters;line_length=length), + (run,Grid((100.0,200.0)));combine=:zip) + @test line_length.(collect(zipped))==[100.0,200.0] + @test selected.allocations[]==3 + @test selected.calculations[]==3 + variants=ModalAnalysisFormulation(Grid((selected,selected))) + product=compute(problems,variants) + @test length(product)==4 + @test product[1,1]===product[1] + @test product[2,2]===product[4] + @test all(value -> value isa LineParameters,product) + + bad=FixedModal(copy(selected.voltage),copy(selected.current),copy(selected.roots),Ref(0),Ref(0)) + bad.voltage[1,2,1]+=0.25 + bad_form=ModalAnalysisFormulation(bad) + noisy=@test_logs (:warn,r"off-diagonal tolerance") compute( + ModalAnalysisProblem(phase),bad_form; + options=(offdiagonal_tolerance=1e-12,)) + @test noisy.Z.values[1,2,1]!=0 + @test !isempty(details(noisy).data.modal.diagnostics.z_coupling) + singular=FixedModal(zeros(ComplexF64,2,2,1),selected.current,selected.roots,Ref(0),Ref(0)) + @test_throws SingularException compute(ModalAnalysisProblem(phase), + ModalAnalysisFormulation(singular)) + + io=IOBuffer() + Serialization.serialize(io,(modal,short)) + seekstart(io) + loaded_modal,loaded_segment=Serialization.deserialize(io) + @test Z(loaded_modal)==Z(modal) + @test H(loaded_segment)==H(short) + @test Z(transform(PhaseDomain,loaded_modal))[:,:,1]≈Zp + @test selected.calculations[]==7 + no_length=LineParameters(reshape(Zp,2,2,1),reshape(Yp,2,2,1),[50.0]) + unbound=compute(ModalAnalysisProblem(no_length),mf) + @test line_length(unbound)===nothing + @test_throws ArgumentError PropagationParameters(unbound) + @test line_length(PropagationParameters(unbound;line_length=25.0))==25.0 + @test_throws ArgumentError LineCableModels.ImportExport.serialize_value(modal) + + timed_phase=E.retain_gridpoint(phase,details(phase).data.gridpoint; + fields=(timing=(wall_seconds=1.0,),)) + untimed=compute(ModalAnalysisProblem(timed_phase),mf) + @test !haskey(details(untimed).data,:timing) + modal_timed=compute(ModalAnalysisProblem(phase),mf;options=(timing=true,)) + @test haskey(details(modal_timed).data,:timing) + @test details(modal_timed).data.gridpoint==details(modal).data.gridpoint + @test details(modal_timed).data.source_gridpoint==details(modal).data.source_gridpoint + @test !haskey(details(PropagationParameters(modal_timed)).data,:timing) + + voltage_H=Base.Fix2(H,(domain=PhaseDomain,field=:voltage)) + current_H=Base.Fix2(H,(domain=PhaseDomain,field=:current)) + observed=ObservedResult(short,(gamma,Zc,Yc,H,Tv,Ti,voltage_H,current_H,(H,abs)); + complete_pairs=true) + @test observed.gridpoint.source_gridpoint==details(short).data.source_gridpoint + @test length(observed.quantities)==18 + @test G.observation_product(observed,(Tv,real)).coordinates.column_labels==["1","2"] + artifact=report(TableReportDefinition((voltage_H,current_H,Tv,Ti)),observed) + @test keys(artifact.tables.H)==(:H_phase_voltage_real,:H_phase_voltage_imag, + :H_phase_current_real,:H_phase_current_imag) + @test size(artifact[(Tv,real)])==(1,5) + mktempdir() do folder + for extension in (".json",".jls") + saved=joinpath(folder,"modal-observations"*extension) + LineCableModels.save(observed,saved) + restored=LineCableModels.import_data(Val(:observed),saved) + @test length(restored.quantities)==18 + @test observe(restored,(voltage_H,real))≈observe(observed,(voltage_H,real)) + @test observe(restored,(current_H,real))≈observe(observed,(current_H,real)) + @test observe(restored,(H,abs))≈observe(observed,(H,abs)) + @test restored.gridpoint.id==observed.gridpoint.id + @test restored.gridpoint.source_gridpoint==observed.gridpoint.source_gridpoint + finite_path=joinpath(folder,"finite-observations"*extension) + LineCableModels.save(finite_observed,finite_path) + finite_restored=LineCableModels.import_data(Val(:observed),finite_path) + @test finite_restored.gridpoint.source_gridpoint== + finite_observed.gridpoint.source_gridpoint + rebound_path=joinpath(folder,"rebound-observations"*extension) + LineCableModels.save(rebound_observed,rebound_path) + rebound_restored=LineCableModels.import_data(Val(:observed),rebound_path) + @test rebound_restored.gridpoint.source_gridpoint== + rebound_observed.gridpoint.source_gridpoint + end + end +end diff --git a/test/unit/modalanalysis/fixture.jl b/test/unit/modalanalysis/fixture.jl new file mode 100644 index 000000000..a6f8ffc33 --- /dev/null +++ b/test/unit/modalanalysis/fixture.jl @@ -0,0 +1,11 @@ +@testitem "ModalAnalysis / armored study fixture declares nine independent terminals" tags=[:unit, :parametric] setup=[ModalStudyFixtures] begin + using LineCableModels + for layout in (:trefoil,:horizontal) + fixture=ModalStudyFixtures.study_problem(layout;frequencies=[50.0]) + @test fixture.design.terminal_order==[:core,:screen,:armor] + @test length(fixture.system.terminal_order)==9 + @test sort(collect(v for mapping in fixture.connections for v in values(mapping)))==collect(1:9) + @test line_length(fixture.problem.system)==600.0 + @test length(fixture.problem.frequencies)==1 + end +end diff --git a/test/unit/modalanalysis/formula_extension.jl b/test/unit/modalanalysis/formula_extension.jl new file mode 100644 index 000000000..586353059 --- /dev/null +++ b/test/unit/modalanalysis/formula_extension.jl @@ -0,0 +1,53 @@ +@testitem "ModalAnalysis / complete custom formula extension" tags=[:unit, :engine] begin + import LineCableModels.Engine: description + import LineCableModels.Commons: formulation_options, FormulationOptions, initialize_buffers + import LineCableModels.ModalAnalysis: decompose!, Formula + import LineCableModels: Expression + + allocations=Ref(0) + calculations=Ref(0) + description(::Type{<:Formula{:diagonal_example}};compact=false)= + compact ? "diagonal example" : "one-mode diagonal example" + formulation_options(::Expression{<:Formula{:diagonal_example},typeof(decompose!)}) = + FormulationOptions() + function initialize_buffers(::Formula{:diagonal_example},::Type{T},input,plan, + common) where {T<:Complex} + allocations[]+=1 + plan.n==1 || throw(DimensionMismatch("diagonal example requires one mode")) + return merge(common,(diagonal_product=Vector{T}(undef,plan.nf),)) + end + function decompose!(::Formula{:diagonal_example},workspace,parameters::NamedTuple, + options::FormulationOptions) + calculations[]+=1 + product=workspace.buffers.diagonal_product + for k in eachindex(product) + z=workspace.input.Z[1,1,k]/workspace.input.root_scale + y=workspace.input.Y[1,1,k]/workspace.input.root_scale + product[k]=z*y + root=sqrt(product[k]) + (real(root)<0 || (iszero(real(root)) && imag(root)<0)) && (root=-root) + workspace.roots[1,k]=root*workspace.input.root_scale + workspace.Tv[1,1,k]=one(root) + workspace.Ti[1,1,k]=one(root) + workspace.diagnostics.eigen_residual[1,k]=zero(real(root)) + workspace.diagnostics.iterations[1,k]=0 + workspace.diagnostics.converged[1,k]=true + end + return workspace + end + + phase=LineParameters(reshape(ComplexF64[2+im,3+im],1,1,2), + reshape(ComplexF64[1e-6im,2e-6im],1,1,2),[50.0,100.0]; + details=ComputationDetails((inputs=(system=(line_length=10.0,),),))) + selected=ModalAnalysisFormulation(Formula(:diagonal_example)) + modal=compute(ModalAnalysisProblem(phase),selected) + segment=PropagationParameters(modal) + @test allocations[]==1 && calculations[]==1 + @test size(gamma(modal))==(1,2) + @test size(H(segment))==size(gamma(modal)) + @test Tv(modal)==ones(ComplexF64,1,1,2) + @test Ti(modal)==ones(ComplexF64,1,1,2) + @test Z(modal)≈Z(phase) + @test Y(modal)≈Y(phase) + @test all(details(modal).data.modal.diagnostics.converged) +end diff --git a/test/unit/modalanalysis/observables_plotbuilder.jl b/test/unit/modalanalysis/observables_plotbuilder.jl new file mode 100644 index 000000000..907f2bf24 --- /dev/null +++ b/test/unit/modalanalysis/observables_plotbuilder.jl @@ -0,0 +1,214 @@ +@testitem "ModalAnalysis / finite observable components and vector retention" tags=[:unit, :xlsx, :slow] begin + using LinearAlgebra + using DataFrames + using XLSX + using Measurements + import LineCableModels.Engine as E + import LineCableModels.Commons as G + import LineCableModels.Units as U + + function finite_modal(::Type{T}; roots=Complex{T}[1+2im 3+4im 5+6im; + 2+3im 4+5im 6+7im] .* T(1e-3)) where {T<:AbstractFloat} + f=T[10,20,30] + maps=cat([Matrix{Complex{T}}(I,2,2) for _ in f]...;dims=3) + z=zeros(eltype(roots),2,2,3) + y=copy(z) + for k in eachindex(f), mode in 1:2 + z[mode,mode,k]=roots[mode,k] + y[mode,mode,k]=roots[mode,k] + end + modal=LineParameters(E.ModalDomain(ModalOperators(maps,maps),roots), + SeriesImpedance(z),ShuntAdmittance(y),f, + ComputationDetails((inputs=(system=(line_length=T(20),),), + phase_coordinates=["core","sheath"]))) + return PropagationParameters(modal) + end + + for T in (Float32,Float64) + segment=finite_modal(T) + @test alpha(segment)==real.(gamma(segment)) + @test beta(segment)==imag.(gamma(segment)) + @test velocity(segment)≈T(2)*T(π).*reshape(frequencies(segment),1,:)./beta(segment) + @test eltype(velocity(segment))==T + @test alpha(PropagationParameters(segment;line_length=T(5)))==alpha(segment) + @test beta(PropagationParameters(segment;line_length=T(5)))==beta(segment) + @test H(PropagationParameters(segment;line_length=T(5)))!=H(segment) + end + setprecision(256) do + segment=finite_modal(BigFloat) + @test eltype(velocity(segment))==BigFloat + @test precision(velocity(segment)[1,1])>=256 + @test velocity(segment)[1,1]≈2*big(π)*frequencies(segment)[1]/beta(segment)[1,1] + end + @test U.quantity(alpha)==U.quantity(gamma,real) + @test U.quantity(beta)==U.quantity(gamma,imag) + @test U.quantity(gamma,real)!=U.quantity(gamma,imag) + @test U.label(U.display_unit(U.quantity(alpha)))=="Np/km" + @test U.label(U.display_unit(U.quantity(beta)))=="rad/km" + @test U.scale_factor(U.native_unit(U.quantity(alpha)),U.display_unit(U.quantity(alpha)))==1000 + @test U.scale_factor(U.native_unit(U.quantity(beta)),U.display_unit(U.quantity(beta)))==1000 + @test U.label(U.display_unit(U.quantity(velocity)))=="m/s" + @test U.label(U.quantity(Zc,abs))=="Characteristic impedance magnitude" + @test U.label(U.quantity(Tv,angle))=="Modal-to-phase voltage transformation angle" + + segment=finite_modal(Float64) + @test observe(segment,alpha)==alpha(segment) + @test observe(segment,beta)==beta(segment) + @test observe(segment,velocity)==velocity(segment) + @test observe(segment,gamma,real)==alpha(segment) + @test observe(segment,gamma,[2,1],[3,1])==gamma(segment)[[2,1],[3,1]] + @test observe(segment,gamma,real,2,1:3)==real.(gamma(segment)[2,1:3]) + @test observe(segment.parameters,Tv,1,2,1:3)==Tv(segment)[1,2,1:3] + phase_zc=Base.Fix2(Zc,(domain=PhaseDomain,)) + @test observe(segment,phase_zc,real,1,1,1:2)== + real.(Zc(segment,PhaseDomain)[1,1,1:2]) + @test_throws ArgumentError observe(segment, + Base.Fix2(Zc,(domain=:invalid,)),1,1,1) + @test G.observation_requests(segment,(gamma,)).retained== + ((gamma,real,Colon(),Colon()),(gamma,imag,Colon(),Colon())) + @test_throws ArgumentError ObservedResult(segment,((gamma,real),)) + @test length(ObservedResult(segment,((gamma,real),);complete_pairs=true).quantities)==2 + @test G.observation_requests(segment,(alpha,beta)).retained== + ((gamma,real,Colon(),Colon()),(gamma,imag,Colon(),Colon())) + @test_throws ArgumentError ObservedResult(segment,(gamma,alpha,beta)) + observed=ObservedResult(segment) + @test length(observed.quantities)==13 + @test all(product -> product.coordinates.kind===:vector, + filter(q -> q.family in (:gamma,:velocity,:Zc,:Yc,:H),observed.quantities)) + @test G.observation_product(observed,alpha)===G.observation_product(observed,(gamma,real)) + @test G.observation_product(observed,beta)===G.observation_product(observed,(gamma,imag)) + @test observe(observed,alpha)==G.observation_product(observed,(gamma,real)).values + @test G.observation_requests(observed,(alpha,)).displayed==((gamma,real),) + @test_throws ArgumentError G.observation_requests(observed,(alpha,(gamma,real))) + @test_throws ArgumentError report(observed;values=(alpha,(gamma,real))) + @test G.observation_product(observed,alpha).coordinates.positions==[1,2] + @test size(G.observation_product(observed,alpha).values)==(2,3) + selection=@observe alpha[[2,1],[3,1]] + subset=G.observation_product(observed,selection) + @test subset.coordinates.positions==[2,1] + @test subset.coordinates.samples==[3,1] + @test subset.values≈alpha(segment)[[2,1],[3,1]].*1000 + @test G.observation_product(ObservedResult(segment,(selection,);complete_pairs=true),selection).values==subset.values + scalar_request=@observe alpha[2,2] + @test G.observation_product(observed,scalar_request).values≈alpha(segment)[2,2]*1000 + @test G.observation_product(observed,scalar_request).coordinates.positions==[2] + range_request=@observe alpha[1:2,2:3] + @test G.observation_product(observed,range_request).values≈alpha(segment)[1:2,2:3].*1000 + converted=ObservedResult(observed,(alpha,);length_unit=:base) + @test G.observation_product(converted,alpha).values≈alpha(segment) + @test G.observation_product(converted,alpha).available==G.observation_product(observed,alpha).available + native_alpha=U.native_unit(U.quantity(alpha)) + for overrides in ((alpha=native_alpha,),Dict(alpha=>native_alpha)) + acquired=ObservedResult(segment,(alpha,);complete_pairs=true,quantity_units=overrides) + reexpressed=ObservedResult(observed,(alpha,);quantity_units=overrides) + for result in (acquired,reexpressed) + product=G.observation_product(result,alpha) + @test product.unit==native_alpha + @test product.values≈alpha(segment) + @test product.available==G.observation_product(observed,alpha).available + end + end + @test G.observation_product(ObservedResult(observed,(alpha,); + quantity_units=(gamma=:kilo,alpha=native_alpha)),alpha).unit==native_alpha + @test G.observation_product(ObservedResult(observed,(alpha,); + quantity_units=Dict(alpha=>native_alpha, + (gamma,real)=>U.display_unit(U.quantity(alpha)))),alpha).unit!=native_alpha + @test G.observation_product(ObservedResult(observed,(alpha,); + quantity_units=Dict{Any,Any}(:alpha=>U.display_unit(U.quantity(alpha)), + alpha=>native_alpha)),alpha).unit==native_alpha + indexed_alias=@observe alpha[2,:] + indexed_gamma_real=@observe (gamma,real)[2,:] + for result in ( + ObservedResult(segment,(indexed_alias,);complete_pairs=true, + quantity_units=Dict(indexed_alias=>native_alpha)), + ObservedResult(observed,(indexed_alias,); + quantity_units=Dict(indexed_alias=>native_alpha))) + product=G.observation_product(result,indexed_alias) + @test product.unit==native_alpha + @test product.values≈alpha(segment)[2,:] + @test product.available==G.observation_product(observed,indexed_alias).available + end + @test G.observation_product(ObservedResult(observed,(indexed_alias,); + quantity_units=Dict{Any,Any}(indexed_alias=>native_alpha, + (gamma,real)=>U.display_unit(U.quantity(alpha)))),indexed_alias).unit==native_alpha + @test G.observation_product(ObservedResult(observed,(indexed_alias,); + quantity_units=Dict{Any,Any}(indexed_alias=>native_alpha, + indexed_gamma_real=>U.display_unit(U.quantity(alpha)))),indexed_alias).unit!=native_alpha + native_beta=U.native_unit(U.quantity(beta)) + @test G.observation_product(ObservedResult(observed,(beta,); + quantity_units=(beta=native_beta,)),beta).values≈beta(segment) + shorter=ObservedResult(segment[[1,3]],(alpha,);complete_pairs=true) + aligned=G.observation_product([ObservedResult(segment,(alpha,);complete_pairs=true),shorter],alpha) + @test aligned[1].coordinates.samples==[1,2,3] + @test aligned[2].coordinates.samples==[1,2] + @test aligned[2].coordinates.frequencies==[10.0,30.0] + @test length(G.observation_product(observed,(Tv,real)).coordinates.rows)==2 + @test G.observation_product(observed,(Tv,real)).coordinates.column_domain===:ModalDomain + + zero_roots=copy(gamma(segment)) + zero_roots[1,2]=complex(real(zero_roots[1,2]),0.0) + zero=finite_modal(Float64;roots=zero_roots) + unavailable=G.observation_product(ObservedResult(zero,(velocity,)),velocity) + @test ismissing(unavailable.values[1,2]) + @test unavailable.missing_reason[1,2]===:nonfinite_value + @test !unavailable.available[1,2] + + shared=measurement(0.002,0.0001) + uncertain_roots=map(root -> complex(shared,shared+imag(root)),gamma(segment)) + uncertain=finite_modal(Float64;roots=uncertain_roots) + uncertain_observed=ObservedResult(uncertain,(alpha,beta,velocity)) + alpha_value=G.observation_product(uncertain_observed,alpha).values[1,1] + beta_value=G.observation_product(uncertain_observed,beta).values[1,1] + @test iszero(uncertainty(alpha_value-beta_value)) + @test uncertainty(G.observation_product(uncertain_observed,velocity).values[1,1])>0 + indexed_beta=@observe beta[2,:] + uncertain_converted=ObservedResult(uncertain_observed,(indexed_alias,indexed_beta); + quantity_units=Dict(indexed_alias=>native_alpha,indexed_beta=>native_beta)) + @test G.observation_product(uncertain_converted,indexed_alias).unit==native_alpha + @test G.observation_product(uncertain_converted,indexed_beta).unit==native_beta + @test iszero(uncertainty( + G.observation_product(uncertain_converted,indexed_alias).values[1,1]- + G.observation_product(uncertain_converted,indexed_beta).values[1,1])) + + artifact=report(TableReportDefinition((alpha,Tv)),observed) + alpha_table=artifact[alpha] + @test alpha_table===artifact[1,alpha] + @test alpha_table===artifact[(gamma,real)] + @test size(alpha_table)==(3,3) + @test names(alpha_table)==["frequency","Mode 1","Mode 2"] + tv_table=artifact[(Tv,real)] + @test size(tv_table)==(3,5) + @test all(name -> occursin("Conductor",name) && occursin("Mode",name),names(tv_table)[2:end]) + @test occursin("Attenuation constant",sprint(show,MIME"text/plain"(),artifact;context=:limit=>false)) + @test occursin("Mode 1",sprint(show,MIME"text/html"(),artifact)) + gamma_artifact=report(TableReportDefinition((gamma,)),observed) + @test gamma_artifact[alpha]===gamma_artifact[(gamma,real)] + @test gamma_artifact[beta]===gamma_artifact[(gamma,imag)] + mktempdir() do directory + for extension in ("json","jls") + path=joinpath(directory,"modal.$extension") + save(observed,path) + restored=import_data(:observed,path) + @test G.observation_product(restored,alpha).values==G.observation_product(observed,alpha).values + @test observe(restored,beta)==G.observation_product(restored,(gamma,imag)).values + @test report(TableReportDefinition((gamma,)),restored)[alpha] isa DataFrame + @test G.observation_product(restored,(Tv,real)).coordinates.column_labels==["1","2"] + uncertain_path=joinpath(directory,"uncertain.$extension") + save(uncertain_observed,uncertain_path) + uncertain_restored=import_data(:observed,uncertain_path) + a=G.observation_product(uncertain_restored,alpha).values[1,1] + b=G.observation_product(uncertain_restored,beta).values[1,1] + @test iszero(uncertainty(a-b)) + setprecision(256) do + precise=ObservedResult(finite_modal(BigFloat),(alpha,velocity);complete_pairs=true) + precise_path=joinpath(directory,"precise.$extension") + save(precise,precise_path) + precise_restored=import_data(:observed,precise_path) + @test precision(G.observation_product(precise_restored,velocity).values[1,1])>=256 + end + end + output=export_data(:xlsx,ObservedResult(segment,(alpha,);complete_pairs=true); + file_name=joinpath(directory,"modal.xlsx")) + @test all(isfile,output) + end +end diff --git a/test/unit/modalanalysis/public_regressions.jl b/test/unit/modalanalysis/public_regressions.jl new file mode 100644 index 000000000..4b5cf7373 --- /dev/null +++ b/test/unit/modalanalysis/public_regressions.jl @@ -0,0 +1,167 @@ +@testitem "ModalAnalysis / structural state and forward imaginary branch" tags=[:unit, :engine] begin + import LineCableModels.Engine as E + impedance=reshape(ComplexF64[-im],1,1,1) + phase=LineParameters(impedance,copy(impedance),[50.0]) + modal=compute(ModalAnalysisProblem(phase),ModalAnalysisFormulation(:default)) + @test gamma(modal)[1,1]≈im + @test_throws DimensionMismatch E.ModalDomain(operators(modal),zeros(ComplexF64,2,1)) + @test_throws ArgumentError E.ModalDomain((Tv=operators(modal).Tv, + Ti=operators(modal).Ti),zeros(ComplexF64,1,1)) + @test_throws DimensionMismatch ModalOperators(operators(modal).Tv, + zeros(ComplexF64,2,2,1)) + large=reshape(ComplexF64[1,0,0,1],2,2,1) + @test_throws DimensionMismatch LineParameters(modal.domain, + SeriesImpedance(large),ShuntAdmittance(copy(large)),[50.0],ComputationDetails()) + @test_throws ArgumentError PropagationParameters(phase,-1.0,ComputationDetails()) + @test_throws ArgumentError PropagationParameters(modal,-1.0,ComputationDetails()) + @test_throws ArgumentError PropagationParameters(modal,10.0, + ComputationDetails((segment=(line_length=20.0,),))) + @test_throws ArgumentError PropagationParameters(modal,10.0,ComputationDetails()) + bound=PropagationParameters(modal;line_length=10.0) + @test PropagationParameters(bound.parameters,bound.line_length,bound.details) isa + PropagationParameters + @test line_length(PropagationParameters(bound;line_length=20.0))==20.0 +end + +@testitem "ModalAnalysis / derived request acquisition and retained identity" tags=[:unit, :importexport, :slow] begin + import LineCableModels.Commons as G + import LineCableModels.Engine as E + import LineCableModels.Units as U + impedance=reshape(ComplexF64[2+im],1,1,1) + admittance=reshape(ComplexF64[1e-6im],1,1,1) + base=LineParameters(impedance,admittance,[50.0]; + details=ComputationDetails((inputs=(system=(line_length=10.0,),),))) + point=details(base).data.gridpoint + first_phase=E.retain_gridpoint(base,point; + fields=E.completed_formulation(Formulation())) + second_phase=E.retain_gridpoint(base,point; + fields=E.completed_formulation(Formulation(earth_impedance=:unified))) + distinct_phase=E.retain_gridpoint(base,point; + fields=E.completed_formulation(Formulation(earth_impedance=:carson1926))) + first_segment=PropagationParameters(compute(ModalAnalysisProblem(first_phase), + ModalAnalysisFormulation(:default))) + second_segment=PropagationParameters(compute(ModalAnalysisProblem(second_phase), + ModalAnalysisFormulation(:default))) + @test observe(first_segment,H,abs)≈abs.(H(first_segment)) + @test G.observation_request(first_segment,(H,abs)).quantity==U.quantity(H,abs) + @test observe(first_segment,gamma,real)==real.(gamma(first_segment)) + @test observe(first_segment,H,imag)==imag.(H(first_segment)) + requests=((H,abs),(gamma,real),(H,imag)) + first_observed=ObservedResult(first_segment,requests;complete_pairs=true) + second_observed=ObservedResult(second_segment,requests;complete_pairs=true) + @test G.observation_quantity(first_phase,G.observation_requests(first_phase,(LineCableModels.Engine.Z,)).retained[1]).assumptions== + G.observation_quantity(second_phase,G.observation_requests(second_phase,(LineCableModels.Engine.Z,)).retained[1]).assumptions + @test length(G.observation_groups([first_observed,second_observed];request=(H,abs)))==1 + distinct_observed=ObservedResult(PropagationParameters(compute( + ModalAnalysisProblem(distinct_phase),ModalAnalysisFormulation(:default))),requests; + complete_pairs=true) + @test length(G.observation_groups([first_observed,distinct_observed];request=(H,abs)))==2 + @test length(G.observation_labels([first_observed];request=(H,abs)))==1 + alternate_modal=compute( + ModalAnalysisProblem(first_phase),ModalAnalysisFormulation(:default; + options=(iteration=(max_iterations=101,),))) + alternate=ObservedResult(PropagationParameters(alternate_modal),requests; + complete_pairs=true) + labels=G.observation_labels([first_observed,alternate];request=(H,abs)) + @test labels[1]!=labels[2] + @test any(label -> occursin("max_iterations",label),labels) + primary=ObservedResult(first_segment.parameters,(E.R,E.X)) + primary_alternate=ObservedResult(alternate_modal,(E.R,E.X)) + @test primary.quantities[1].assumptions!=primary_alternate.quantities[1].assumptions + @test length(G.observation_groups([primary,primary_alternate];request=E.R))==2 + primary_labels=G.observation_labels([primary,primary_alternate];request=E.R) + @test primary_labels[1]!=primary_labels[2] + @test any(label -> occursin("max_iterations",label),primary_labels) + descriptions=details(first_segment.parameters).data.formulation_fields + @test descriptions.all==descriptions.Z==descriptions.Y + @test descriptions.all !== descriptions.Z && descriptions.all !== descriptions.Y && + descriptions.Z !== descriptions.Y + result_source=G.gridpoint_id().source_id + results=[E.retain_gridpoint(result,G.gridpoint_id(;source_id=result_source, + formulation_index=index)) for (index,result) in enumerate((first_segment.parameters,alternate_modal))] + reference=E.retain_gridpoint(first_segment.parameters,G.gridpoint_id()) + comparisons=E.compare(reference,results,[E.R];bands=(:all,)) + @test isequal(comparisons[1].absolute,comparisons[2].absolute) + @test isequal(comparisons[1].relative,comparisons[2].relative) + @test comparisons[1].assumptions!=comparisons[2].assumptions + compared=observables(results,(E.R,E.X);comparisons) + @test length(G.observation_groups(compared;request=E.R,band=:all, + normalization=:reference_rms,reference=details(reference).data.gridpoint))==2 + artifact=report(LineCableModels.ReportBuilder.BenchmarkTableDefinition((E.R,);bands=(:all,)), + compared;reference=ObservedResult(reference,(E.R,E.X))) + @test size(only(artifact.tables.features).absolute,1)==2 + admittance_cases=[E.retain_gridpoint(base,point; + fields=E.completed_formulation(Formulation(earth_admittance=selection))) + for selection in (:unified,:ideal)] + phase_admittance=[ObservedResult(phase,(E.R,E.X)) for phase in admittance_cases] + @test only(unique(G.observation_labels(phase_admittance;request=E.R))) isa String + modal_admittance=[ObservedResult(compute(ModalAnalysisProblem(phase), + ModalAnalysisFormulation(:default)),(E.R,E.X)) for phase in admittance_cases] + @test length(G.observation_groups(modal_admittance;request=E.R))==2 + admittance_labels=G.observation_labels(modal_admittance;request=E.R) + @test admittance_labels[1]!=admittance_labels[2] + @test any(label -> occursin("Ideal",label),admittance_labels) + modal_impedance=[ObservedResult(compute(ModalAnalysisProblem(phase), + ModalAnalysisFormulation(:default)),(E.G,E.B)) for phase in (second_phase,distinct_phase)] + @test length(G.observation_groups(modal_impedance;request=E.G))==2 + impedance_labels=G.observation_labels(modal_impedance;request=E.G) + @test impedance_labels[1]!=impedance_labels[2] + phase_Zc=Base.Fix2(Zc,(domain=PhaseDomain,)) + target=only(G.unit_targets(((phase_Zc,abs),),:pul; + overrides=Dict(Zc=>:kilo))) + @test target==U.units(:kilo,:ohm) + mktempdir() do folder + path=joinpath(folder,"modal-derived.json") + LineCableModels.save(first_observed,path) + restored=LineCableModels.import_data(Val(:observed),path) + @test observe(restored,(gamma,real))≈observe(first_observed,(gamma,real)) + @test observe(restored,(H,imag))≈observe(first_observed,(H,imag)) + end +end + +@testitem "ModalAnalysis / custom passive controls survive retained labels" tags=[:unit, :engine] begin + import LineCableModels.ModalAnalysis as MA + import LineCableModels.Engine as E + import LineCableModels.Commons as G + + struct ScaleModal <: AbstractFormulation + scale::Float64 + end + LineCableModels.formula_id(::ScaleModal)=:scale_modal + Base.NamedTuple(value::ScaleModal)=(identifier=:scale_modal, + parameters=(scale=value.scale,),options=(;)) + LineCableModels.formulation_options(::ScaleModal)=FormulationOptions() + LineCableModels.description(value::ScaleModal;compact::Bool=false)= + "Basis scale $(value.scale)" + LineCableModels.description(::Type{ScaleModal},::Val{:scale},value::Real; + compact::Bool=false)="scale=$(value)" + G.initialize_buffers(::ScaleModal,::Type,input,plan,buffers)=buffers + function MA.decompose!(selected::ScaleModal,workspace,parameters,options) + workspace.Tv[1,1,1]=selected.scale + workspace.Ti[1,1,1]=1 + workspace.roots[1,1]=sqrt(workspace.input.Z[1,1,1]*workspace.input.Y[1,1,1]) + return workspace + end + + z=reshape(ComplexF64[2+im],1,1,1) + y=reshape(ComplexF64[1e-6im],1,1,1) + base=LineParameters(z,y,[50.0]) + phase=E.retain_gridpoint(base,details(base).data.gridpoint; + fields=E.completed_formulation(Formulation())) + first_modal=compute(ModalAnalysisProblem(phase),ModalAnalysisFormulation(ScaleModal(1.0))) + second_modal=compute(ModalAnalysisProblem(phase),ModalAnalysisFormulation(ScaleModal(2.0))) + @test Tv(first_modal)!=Tv(second_modal) + first=ObservedResult(first_modal,((Tv,abs),E.R,E.X);complete_pairs=true) + second=ObservedResult(second_modal,((Tv,abs),E.R,E.X);complete_pairs=true) + @test length(G.observation_groups([first,second];request=(Tv,abs)))==2 + @test first.quantities[1].assumptions!=second.quantities[1].assumptions + fields=details(first_modal).data.formulation_fields.all + modal_field=only(filter(field -> field.meaning==(:transformation,),fields)) + @test modal_field.value=="Basis scale 1.0" + @test any(control -> control.scope==(:parameters,:scale),modal_field.control_fields) + for request in ((Tv,abs),E.R) + labels=G.observation_labels([first,second];request) + @test labels[1]!=labels[2] + @test all(label -> occursin("scale=",label),labels) + end +end diff --git a/test/unit/modalanalysis/uncertainty.jl b/test/unit/modalanalysis/uncertainty.jl new file mode 100644 index 000000000..6ca0e87a7 --- /dev/null +++ b/test/unit/modalanalysis/uncertainty.jl @@ -0,0 +1,41 @@ +@testitem "ModalAnalysis / fixed nominal branch retains first-order scalar dependencies" tags=[:unit, :measurements] begin + using Measurements + import Measurements: derivative + + x=measurement(2.0,0.1) + phase=LineParameters(reshape([complex(x,zero(x))],1,1,1), + reshape([3.0+0im],1,1,1),[50.0]) + modal=compute(ModalAnalysisProblem(phase),ModalAnalysisFormulation()) + root=real(gamma(modal)[1,1]) + @test nominal(root)≈sqrt(6.0) + @test derivative(root,x)≈3/(2sqrt(6.0)) + characteristic=real(Zc(modal)[1,1]) + @test derivative(characteristic,x)≈1/(2sqrt(6.0)) + segment=PropagationParameters(modal;line_length=10.0) + factor=real(H(segment)[1,1]) + @test derivative(factor,x)≈-10exp(-10sqrt(6.0))*3/(2sqrt(6.0)) + @test uncertainty(factor)>0 +end + +@testitem "ModalAnalysis / undefined first-order magnitude is unavailable" tags=[:unit, :measurements] begin + using Measurements + import LineCableModels.Engine as E + x=measurement(1.0,0.1) + q1=complex(x,zero(x)) + q2=complex(2-x,zero(x)) + zero_q=zero(q1) + coefficients=reshape([q1,zero_q,zero_q,q2],2,2,1) + basis_matrix=ComplexF64[1 1;1 -1]./sqrt(2) + maps=ModalOperators(reshape(copy(basis_matrix),2,2,1), + reshape(copy(basis_matrix),2,2,1)) + modal=LineParameters(E.ModalDomain(maps,reshape([q1,q2],2,1)), + SeriesImpedance(coefficients),ShuntAdmittance(copy(coefficients)),[50.0], + ComputationDetails()) + segment=PropagationParameters(modal;line_length=1.0) + voltage_H=Base.Fix2(H,(domain=PhaseDomain,field=:voltage)) + observed=ObservedResult(segment,((voltage_H,abs,1,2,:),);complete_pairs=true) + product=LineCableModels.Commons.observation_product(observed,(voltage_H,abs,1,2,:)) + @test product.values[1]===missing + @test product.available[1]==false + @test product.missing_reason[1]===:undefined_first_order_magnitude +end diff --git a/test/unit/modalanalysis/vieira2026.jl b/test/unit/modalanalysis/vieira2026.jl new file mode 100644 index 000000000..c17e6024c --- /dev/null +++ b/test/unit/modalanalysis/vieira2026.jl @@ -0,0 +1,245 @@ +@testmodule ModalTrackingExamples begin + using LineCableModels, LinearAlgebra + + function phase_scan(::Type{R} = Float64; clustered = false, angle_step = 0.02) where {R} + f = R[50, 80, 130, 200] + Z = zeros(Complex{R}, 3, 3, length(f)) + Y = similar(Z) + roots = Matrix{Complex{R}}(undef, 3, length(f)) + for k in eachindex(f) + angle = R(angle_step) * (k - 1) + rotation = R[cos(angle) 0 sin(angle); 0 1 0; -sin(angle) 0 cos(angle)] + z = Complex{R}[2 + im * f[k] / 20, + (clustered ? 2 : 4) + im * f[k] / (clustered ? 20 : 10), + 8 + im * f[k] / 5] + y = complex(R(1e-7), R(1e-8) * f[k]) + Z[:, :, k] = rotation * Diagonal(z) * transpose(rotation) + Y[:, :, k] = y * Matrix{R}(I, 3, 3) + roots[:, k] = sqrt.(z .* y) + end + phase = LineParameters(Z, Y, f; + details = ComputationDetails((inputs = (system = (line_length = R(25),),),))) + return phase, roots + end +end + +@testitem "ModalAnalysis / Vieira complex residual, Jacobian and minimum-norm steps" tags=[:unit, :engine] begin + using LinearAlgebra + import LineCableModels.ModalAnalysis as M + formula = M.Formula(:vieira2026) + for R in (Float32, Float64) + T = Complex{R} + S = T[1+im 2-im; 0.5im 3+2im] + x = T[0.8 + 0.2im, 0.3 - 0.1im, 1.2 + 0.4im] + residual = zeros(T, 3) + J = zeros(T, 3, 3) + @test M.eigenpair_residual!(residual, x, S) === residual + @test residual ≈ vcat((S-x[3]*I)*x[1:2], sum(x[1:2] .^ 2)-1) + M.eigenpair_jacobian!(J, x, S) + expected = T[S[1, 1]-x[3] S[1, 2] -x[1]; + S[2, 1] S[2, 2]-x[3] -x[2]; 2x[1] 2x[2] 0] + @test J == expected + if R === Float64 + direction = T[0.1 + 0.3im, 0.2 - 0.2im, -0.4 + 0.1im] + plus, minus = similar(residual), similar(residual) + M.eigenpair_residual!(plus, x+1e-5direction, S) + M.eigenpair_residual!(minus, x-1e-5direction, S) + @test (plus-minus)/2e-5 ≈ J*direction rtol=1e-9 + end + A = T[1 im; 2 2im; 0 0] + b = T[3 + im, 6 + 2im, 4] + solution, projection = zeros(T, 2), zeros(T, 2) + M.minimum_norm!(solution, copy(A), b, projection) + @test solution ≈ T[(3 + im) / 2, (1 - 3im) / 2] + coefficients = zeros(T, 2, 2) + M.minimum_norm!(coefficients, copy(A), hcat(b, 2b), similar(coefficients)) + @test coefficients ≈ hcat(solution, 2solution) + M.minimum_norm!(solution, zeros(T, 3, 2), b, projection) + @test iszero(solution) + + least_squares = LineCableModels.Commons.initialize_buffers( + formula, T, (;), (; n = 2, nf = 1), (;)).least_squares + vector = T[1, 0.1im] + matrix = Matrix(Diagonal(T[1 + 0.2im, 3 + 0.7im])) + tolerance = R === Float64 ? 1e-11 : 1e-5 + value, converged, iterations = M.levenberg_marquardt_step!(formula, + vector, T(0.9+0.1im), matrix, + (convergence = tolerance, max_iterations = 100), least_squares) + @test converged + @test iterations > 0 + @test value ≈ matrix[1, 1] + @test sum(vector .^ 2) ≈ one(T) + @test norm(matrix*vector-value*vector) < tolerance + # An exact eigenpair does not need iterations. A stationary non-root stalls. + value, converged, iterations = M.levenberg_marquardt_step!(formula, + T[1, 0], matrix[1, 1], matrix, (convergence = tolerance, max_iterations = 100), least_squares) + @test converged && iterations == 0 + vector = zeros(T, 2) + _, converged, iterations = M.levenberg_marquardt_step!(formula, + vector, zero(T), matrix, (convergence = tolerance, max_iterations = 2), least_squares) + @test !converged && iterations == 1 + @test iszero(vector) + end + cost = [1.0 2; 2 100] + assignment = zeros(Int, 2) + @test M.greedy_assignment!(assignment, cost) == [1, 2] +end + +@testitem "ModalAnalysis / Vieira tracked modes, ordering and clustered subspaces" tags=[:unit, :engine, :slow] setup=[ModalTrackingExamples] begin + using LinearAlgebra + for R in (Float32, Float64), clustered in (false, true) + + phase, expected_roots=ModalTrackingExamples.phase_scan(R; clustered) + before_Z, before_Y=copy(Z(phase)), copy(Y(phase)) + modal=@inferred compute(ModalAnalysisProblem(phase), ModalAnalysisFormulation(:vieira2026)) + @test eltype(Ti(modal)) === Complex{R} + @test eltype(gamma(modal)) === Complex{R} + @test gamma(modal) ≈ expected_roots[[3, 2, 1], :] + @test all(>=(zero(R)), real.(gamma(modal))) + for k in eachindex(phase.f) + current, voltage=Ti(modal)[:, :, k], Tv(modal)[:, :, k] + @test norm.(eachcol(current)) ≈ ones(R, 3) + @test norm.(eachcol(voltage)) ≈ ones(R, 3) + @test Y(phase)[:, :, k]*Z(phase)[:, :, k]*current ≈ + current*Diagonal(gamma(modal)[:, k] .^ 2) + paired=Z(phase)[:, :, k]*current + @test voltage ≈ paired ./ norm.(eachcol(paired))' + if clustered + angle=R(0.02)*(k-1) + target=R[cos(angle) 0; 0 1; -sin(angle) 0] + repeated=current[:, 2:3] + @test repeated*pinv(repeated) ≈ target*transpose(target) + end + end + rebuilt=transform(PhaseDomain, modal) + @test Z(rebuilt) ≈ before_Z + @test Y(rebuilt) ≈ before_Y + @test Z(phase) == before_Z && Y(phase) == before_Y + diagnostics=details(modal).data.modal.diagnostics + @test all(isfinite, diagnostics.eigen_residual) + @test all(isnothing, diagnostics.iterations[:, 1]) + @test all(x -> x isa Int && x >= 0, diagnostics.iterations[:, 2:end]) + unordered=compute(ModalAnalysisProblem(phase), + ModalAnalysisFormulation(:vieira2026; + options = (tracking = (order_by_velocity = false,),))) + order=sortperm(imag.(gamma(unordered)[:, end]); rev = true) + @test gamma(modal) ≈ gamma(unordered)[order, :] + @test Ti(modal) ≈ Ti(unordered)[:, order, :] + @test isequal(diagnostics.iterations, details(unordered).data.modal.diagnostics.iterations[order, :]) + @test isequal(diagnostics.converged, details(unordered).data.modal.diagnostics.converged[order, :]) + @test diagnostics.eigen_residual ≈ + details(unordered).data.modal.diagnostics.eigen_residual[order, :] + end +end + +@testitem "ModalAnalysis / Vieira same-sample fallback and numerical controls" tags=[:unit, :engine] setup=[ModalTrackingExamples] begin + using LinearAlgebra + import LineCableModels.ModalAnalysis as M + phase, roots=ModalTrackingExamples.phase_scan(; angle_step = 0.4) + selected=ModalAnalysisFormulation(:vieira2026; + options = ( + iteration = (max_iterations = 1,), tracking = (predictor_tolerance = 1e-8,))) + modal=compute(ModalAnalysisProblem(phase), selected) + diagnostics=details(modal).data.modal.diagnostics + @test !isempty(diagnostics.fallback_frequencies) + @test all(in(diagnostics.missed_frequencies), diagnostics.fallback_frequencies) + @test modal.f == phase.f + for k in eachindex(phase.f) + @test Y(phase)[:, :, k]*Z(phase)[:, :, k]*Ti(modal)[:, :, k] ≈ + Ti(modal)[:, :, k]*Diagonal(gamma(modal)[:, k] .^ 2) + end + strict_match=compute(ModalAnalysisProblem(phase), + ModalAnalysisFormulation(:vieira2026; + options = (iteration = (max_iterations = 1,), + tracking = (eigenvalue_tolerance = 1e-15,)))) + @test !isempty(details(strict_match).data.modal.diagnostics.fallback_frequencies) + @test formula_id(ModalAnalysisFormulation().formula) === :chrysochos2014 + @test :vieira2026 in M.formulas(M.Formula) + for controls in ((convergence = 0,), (convergence = NaN,), (max_iterations = 0,), + (max_iterations = true,), (max_iterations = 1.5,), (unused = 1,)) + @test_throws ArgumentError ModalAnalysisFormulation(:vieira2026; options = (iteration = controls,)) + end + for controls in ((predictor_tolerance = 0,), (eigenvalue_tolerance = Inf,), + (cluster_tolerance = -1,), (order_by_velocity = 1,), (max_depth = 12,)) + @test_throws ArgumentError ModalAnalysisFormulation(:vieira2026; options = (tracking = controls,)) + end + @test_throws ArgumentError ModalAnalysisFormulation(:vieira2026; parameters = (s = [1im],)) + branch=LineParameters(reshape(ComplexF64[-im], 1, 1, 1), + reshape(ComplexF64[-im], 1, 1, 1), [50.0]) + @test only(gamma(compute(ModalAnalysisProblem(branch), ModalAnalysisFormulation(:vieira2026)))) ≈ + im +end + +@testitem "ModalAnalysis / Vieira public selection, propagation, observations and uncertainty" tags=[:unit, :measurements, :slow] setup=[ModalTrackingExamples] begin + using Measurements, LinearAlgebra + phase, roots=ModalTrackingExamples.phase_scan() + selected=ModalAnalysisFormulation(:vieira2026) + modal=compute(ModalAnalysisProblem(phase), selected) + segment=PropagationParameters(modal) + @test H(segment) ≈ exp.(-gamma(modal) .* 25) + total=LineParameters(PhaseDomain, SeriesImpedance(Z(phase) .* 25; basis = :total), + ShuntAdmittance(Y(phase) .* 25; basis = :total), phase.f, phase.details) + total_modal=compute(ModalAnalysisProblem(total), selected) + @test gamma(total_modal) ≈ gamma(modal) .* 25 + @test H(PropagationParameters(total_modal)) ≈ H(segment) + @test Zc(total_modal) ≈ Zc(modal) + for selection in (2, 2:3, [4, 2], :) + indices=selection isa Int ? (selection:selection) : selection + retained=modal[selection] + @test gamma(retained) == gamma(modal)[:, indices] + @test Ti(retained) == Ti(modal)[:, :, indices] + @test details(retained).data.modal.diagnostics.eigen_residual == + details(modal).data.modal.diagnostics.eigen_residual[:, indices] + @test H(segment[selection]) == H(segment)[:, indices] + end + observed=observables(segment, (gamma, Zc, H, Tv, Ti)) + @test observe(observed, (gamma, real)) ≈ 1000 .* observe(segment, gamma, real) + tables=report(observed) + @test size(tables[(gamma, real)]) == (length(phase.f), 4) + @test tables[alpha] === tables[(gamma, real)] + mktempdir() do folder + for extension in ("json", "jls") + path=joinpath(folder, "vieira."*extension) + LineCableModels.save(observed, path) + restored=LineCableModels.import_data(Val(:observed), path) + @test observe(restored, (H, real)) == observe(observed, (H, real)) + end + end + x=measurement(2.0, 0.1) + uncertain=LineParameters(reshape([complex(x, zero(x))], 1, 1, 1), + reshape([3.0+0im], 1, 1, 1), [50.0]) + propagated=@inferred compute(ModalAnalysisProblem(uncertain), selected) + root=real(only(gamma(propagated))) + @test nominal(root) ≈ sqrt(6) + @test Measurements.derivative(root, x) ≈ 3/(2sqrt(6)) + factor=real(only(H(PropagationParameters(propagated; line_length = 10.0)))) + @test Measurements.derivative(factor, x) ≈ -10exp(-10sqrt(6))*3/(2sqrt(6)) + source=ParametricResult(Combinatorial(selected), [phase, phase]) + results=compute(Gridspace{ModalAnalysisProblem}(source), selected) + @test length(results) == 2 + @test isconcretetype(eltype(results)) + @test gamma.(results) == [gamma(modal), gamma(modal)] + variants=ModalAnalysisFormulation(Grid((:chrysochos2014, :vieira2026))) + combinations=compute(Gridspace{ModalAnalysisProblem}(source), variants) + @test length(combinations) == 4 + @test [details(result).data.modal.identifier for result in combinations] == + [:chrysochos2014, :chrysochos2014, :vieira2026, :vieira2026] +end + +@testitem "ModalAnalysis / Vieira convenience computation preserves completed upstream results" tags=[:unit, :parametric] setup=[TestFixtures] begin + problem=TestFixtures.three_bare_wires_problem(frequencies = [50.0, 80.0]) + formulation=Formulation() + phase=compute(problem, formulation) + direct=compute(ModalAnalysisProblem(phase), ModalAnalysisFormulation(:vieira2026)) + upstream=Ref(0) + downstream=Ref(0) + combined=compute(problem, formulation; modal = :vieira2026, + options = (trace = true, on_result = (args...)->(upstream[]+=1)), + modal_options = (on_result = (args...)->(downstream[]+=1),)) + @test upstream[] == downstream[] == 1 + @test gamma(combined) == gamma(direct) + @test Ti(combined) == Ti(direct) + @test Z(combined) == Z(direct) + @test haskey(details(combined).data, :trace) + @test !haskey(details(direct).data, :trace) +end diff --git a/test/unit/modalanalysis/wedepohl1996.jl b/test/unit/modalanalysis/wedepohl1996.jl new file mode 100644 index 000000000..92d996fd9 --- /dev/null +++ b/test/unit/modalanalysis/wedepohl1996.jl @@ -0,0 +1,217 @@ +@testitem "ModalAnalysis / Newton eigenpair corrections and same-sample fallback" tags=[:unit, :engine] setup=[ModalTrackingExamples] begin + using LinearAlgebra + import LineCableModels.ModalAnalysis as M + for R in (Float32, Float64) + T=Complex{R} + phase, expected=ModalTrackingExamples.phase_scan(R) + selected=ModalAnalysisFormulation(:wedepohl1996) + modal=@inferred compute(ModalAnalysisProblem(phase), selected) + @test eltype(Ti(modal))===T + @test gamma(modal)≈expected[[3, 2, 1], :] + @test frequencies(modal)==phase.f + @test Z(transform(PhaseDomain, modal))≈Z(phase) + @test Y(transform(PhaseDomain, modal))≈Y(phase) + @test H(PropagationParameters(modal))≈exp.(-25gamma(modal)) + for k in eachindex(phase.f) + @test Y(phase)[:, :, k]*Z(phase)[:, :, k]*Ti(modal)[:, :, k]≈Ti(modal)[:, :, k]*Diagonal(gamma(modal)[:, k] .^ + 2) + paired=Z(phase)[:, :, k]*Ti(modal)[:, :, k] + @test Tv(modal)[:, :, k]≈paired ./ transpose(norm.(eachcol(paired))) + @test norm.(eachcol(Ti(modal)[:, :, k]))≈ones(R, 3) + end + diagnostics=details(modal).data.modal.diagnostics + @test all(isnothing, diagnostics.iterations[:, 1]) + @test all(isfinite, diagnostics.eigen_residual) + work=M.ModalAnalysisWorkspace(phase, selected.formula).buffers.newton + # The kernel consumes an n+1 complex system. Use an independent diagonal problem. + matrix=Matrix(Diagonal(T[1 + 0.2im, 3 + 0.7im, 5 + 0.1im])) + vector=T[1, 0.01im, 0.02] + tolerance=R===Float32 ? R(1e-5) : R(1e-10) + value, converged, iterations=M.newton_eigenpair!(vector, T(0.9+0.1im), matrix, + (convergence = tolerance, max_iterations = 60), work) + @test converged && iterations>0 + @test value≈matrix[1, 1] + @test norm(matrix*vector-value*vector)<10tolerance + @test norm(vector)≈one(R) + _, converged, _=M.newton_eigenpair!(T[1, 0, 0], zero(T), zeros(T, 3, 3), + (convergence = tolerance, max_iterations = 60), work) + @test !converged + end + # Both Newton trajectories reach the same eigenpair. Direct fallback restores rank. + z=cat(Matrix{ComplexF64}(I, 2, 2), ComplexF64[2 -1; -1 2]; dims = 3) + y=cat(Matrix{ComplexF64}(I, 2, 2), Matrix{ComplexF64}(I, 2, 2); dims = 3) + collapsed=compute(ModalAnalysisProblem(LineParameters(z, y, [1.0, 2.0])), + ModalAnalysisFormulation(:wedepohl1996)) + diagnostics=details(collapsed).data.modal.diagnostics + @test diagnostics.fallback_frequencies==[2] + @test diagnostics.missed_frequencies==[2] + @test all(diagnostics.converged[:, 2]) + @test sort(real.(gamma(collapsed)[:, 2]) .^ 2)≈[1, 3] + @test rank(Ti(collapsed)[:, :, 2])==2 + phase, _=ModalTrackingExamples.phase_scan(; angle_step = 0.4) + limited=compute(ModalAnalysisProblem(phase), + ModalAnalysisFormulation(:wedepohl1996; + options = (iteration = (max_iterations = 1,),))) + @test !isempty(details(limited).data.modal.diagnostics.fallback_frequencies) + for k in eachindex(phase.f) + @test Y(phase)[:, :, k]*Z(phase)[:, :, k]*Ti(limited)[:, :, k]≈Ti(limited)[:, :, k]*Diagonal(gamma(limited)[ + :, k] .^ 2) + end + # A zero product is a valid normalized eigensystem. It must not divide by zero. + zero_product=compute( + ModalAnalysisProblem(LineParameters(ones(ComplexF64, 1, 1, 2), + zeros(ComplexF64, 1, 1, 2), [1.0, 2.0])), + ModalAnalysisFormulation(:wedepohl1996)) + @test iszero(gamma(zero_product)) + @test H(PropagationParameters(zero_product; line_length = 10.0))==ones(ComplexF64, 1, 2) + for invalid in ((convergence = 0,), (convergence = NaN,), (max_iterations = 0,), + (max_iterations = true,), (max_iterations = 1.5,), (unused = 1,)) + @test_throws ArgumentError ModalAnalysisFormulation(:wedepohl1996; options = (iteration = invalid,)) + end + @test formula_id(ModalAnalysisFormulation().formula)===:chrysochos2014 + @test :wedepohl1996 in M.formulas(M.Formula) +end + +@testitem "ModalAnalysis / shared paired rotation and sign continuity" tags=[:unit, :importexport] begin + using LinearAlgebra + import LineCableModels.ModalAnalysis as M + import LineCableModels.Engine as E + import LineCableModels.Commons as G + # A 60-degree scalar catches the supplied routine's second application of the angle. + for R in (Float32, Float64) + T=Complex{R} + current=reshape(T[cis(R(pi/3))], 1, 1, 1) + voltage=2im .* current + M.orient_modes!(voltage, current, true) + @test real(only(current))≈one(R) + @test abs(imag(only(current)))<8eps(R) + @test only(voltage)≈2im + @test eltype(current)===T + for exponent in (-1, 1) + scale=R(10)^(exponent*(R===Float32 ? 25 : 200)) + current=reshape(T[scale * cis(R(pi / 3)), -scale * cis(R(pi / 3))], 1, 1, 2) + voltage=copy(current) + M.orient_modes!(voltage, current, true) + @test vec(current) ./ scale ≈ ones(T, 2) + @test all(isfinite, current) + end + original=reshape(T[1 + 2im, 3 - 0.4im, 0.2 + im, 2 - 3im], 2, 2, 1) + current=copy(original) + voltage=3 .* original + M.orient_modes!(voltage, current, true) + for mode in 1:2 + before=original[:, mode, 1] + after=current[:, mode, 1] + minimum_imaginary=(sum(abs2, before)-abs(sum(before .^ 2)))/2 + @test sum(abs2, imag.(after))≈minimum_imaginary rtol=32eps(R) + @test norm(after)≈norm(before) + end + @test voltage≈3current + end + struct FixedOrientation <: AbstractFormulation + voltage::Array{ComplexF64, 3} + current::Array{ComplexF64, 3} + roots::Matrix{ComplexF64} + end + LineCableModels.formula_id(::FixedOrientation) = :fixed_orientation + LineCableModels.description(::FixedOrientation; compact::Bool = false) = "fixed_orientation" + Base.NamedTuple(::FixedOrientation) = (identifier = :fixed_orientation,) + G.formulation_options(::FixedOrientation) = FormulationOptions() + G.initialize_buffers(::FixedOrientation, ::Type, input, plan, buffers) = buffers + function M.decompose!(selected::FixedOrientation, workspace, parameters, options) + copyto!(workspace.Tv, selected.voltage) + copyto!(workspace.Ti, selected.current) + copyto!(workspace.roots, selected.roots) + return workspace + end + f=[10.0, 20.0, 30.0] + C=ComplexF64[1+0.7im 0.3-0.2im; 0.2+0.3im 1-0.4im] + z=ComplexF64[2 + 3im, 4 + im] + y=ComplexF64[0.1 + 0.2im, 0.2 + 0.3im] + impedance=C*Diagonal(z)/C + admittance=C*Diagonal(y)/C + current=cat(C, -C, cis(0.2) .* C; dims = 3) + voltage=copy(current) + roots=repeat(sqrt.(z .* y), 1, 3) + phase=LineParameters( + cat(fill(impedance, 3)...; dims = 3), cat(fill(admittance, 3)...; dims = 3), f; + details = ComputationDetails(( + gridpoint = G.gridpoint_id(), selections = (Z = :example, Y = :example)))) + selected=ModalAnalysisFormulation(FixedOrientation(voltage, current, roots)) + rotated=@inferred compute(ModalAnalysisProblem(phase), selected) + raw=compute(ModalAnalysisProblem(phase), selected; options = (rotate = false,)) + @test details(rotated).data.modal.rotate===true + @test details(raw).data.modal.rotate===false + @test Ti(raw)[:, :, 1]==C + @test Ti(raw)[:, :, 2]==C # the 180-degree flip is removed even with rotation disabled + @test Ti(rotated)[:, :, 1]≈Ti(rotated)[:, :, 2]≈Ti(rotated)[:, :, 3] + @test gamma(rotated)==gamma(raw)==roots + @test Zc(rotated)≈Zc(raw) + @test Yc(rotated)≈Yc(raw) + for result in (rotated, raw) + @test Z(transform(PhaseDomain, result))≈Z(phase) + @test Y(transform(PhaseDomain, result))≈Y(phase) + for k in 2:3, mode in 1:2 + + @test real(dot(Ti(result)[:, mode, k - 1], Ti(result)[:, mode, k]))>=0 + end + end + @test selected.formula.current==current + first_segment=PropagationParameters(rotated; line_length = 1.0) + second_segment=PropagationParameters(raw; line_length = 1.0) + for field in (:current, :voltage) + @test H(first_segment, PhaseDomain; field)≈H(second_segment, PhaseDomain; field) + end + @test Zc(rotated, PhaseDomain)≈Zc(raw, PhaseDomain) + @test Yc(rotated, PhaseDomain)≈Yc(raw, PhaseDomain) + observed=observables.([first_segment, second_segment], Ref((Ti,))) + @test length(G.observation_groups(observed; request = (Ti, real)))==2 + mktempdir() do folder + for extension in ("json", "jls") + path=joinpath(folder, "rotation."*extension) + LineCableModels.save(observed[1], path) + restored=LineCableModels.import_data(Val(:observed), path) + @test observe(restored, (Ti, real))==observe(observed[1], (Ti, real)) + @test G.observation_product(restored, (Ti, real)).assumptions.rotate===true + end + end + for invalid in (0, 1, :yes, nothing) + @test_throws ArgumentError compute(ModalAnalysisProblem(phase), selected; options = (rotate = invalid,)) + end +end + +@testitem "ModalAnalysis / Newton collection, convenience and uncertain consumers" tags=[:unit, :measurements] setup=[ + ModalTrackingExamples, TestFixtures] begin + using Measurements, LinearAlgebra + phase, _=ModalTrackingExamples.phase_scan() + selected=ModalAnalysisFormulation(:wedepohl1996) + source=ParametricResult(Combinatorial(selected), [phase, phase]) + results=compute(Gridspace{ModalAnalysisProblem}(source), selected; options = (rotate = false,)) + @test length(results)==2 && isconcretetype(eltype(results)) + @test all(result->details(result).data.modal.rotate===false, results) + @test gamma(results[1])==gamma(results[2]) + total=LineParameters(PhaseDomain, SeriesImpedance(Z(phase) .* 25; basis = :total), + ShuntAdmittance(Y(phase) .* 25; basis = :total), phase.f, phase.details) + @test H(PropagationParameters(compute(ModalAnalysisProblem(total), selected)))≈H(PropagationParameters(results[1])) + retained=results[1][[4, 2]] + @test gamma(retained)==gamma(results[1])[:, [4, 2]] + @test details(retained).data.modal.rotate===false + observed=observables(PropagationParameters(retained), (gamma, Zc, H, Tv, Ti)) + @test size(report(observed)[alpha])==(2, 4) + x=measurement(2.0, 0.1) + uncertain=LineParameters(reshape([complex(x, 0)], 1, 1, 1), reshape([3+0im], 1, 1, 1), [50.0]) + modal=@inferred compute(ModalAnalysisProblem(uncertain), selected) + @test Measurements.derivative(real(only(gamma(modal))), x)≈3/(2sqrt(6)) + problem=TestFixtures.three_bare_wires_problem(frequencies = [50.0, 80.0]) + phase=compute(problem, Formulation()) + direct=compute(ModalAnalysisProblem(phase), selected; options = (rotate = false,)) + upstream=Ref(0) + downstream=Ref(0) + combined=compute(problem, Formulation(); modal = :wedepohl1996, + options = (on_result = (args...)->(upstream[]+=1),), + modal_options = (rotate = false, on_result = (args...)->(downstream[]+=1))) + @test upstream[]==downstream[]==1 + @test gamma(combined)==gamma(direct) + @test Ti(combined)==Ti(direct) + @test details(combined).data.modal.rotate===false +end diff --git a/test/unit/parametricbuilder/architecture.jl b/test/unit/parametricbuilder/architecture.jl new file mode 100644 index 000000000..076f93c67 --- /dev/null +++ b/test/unit/parametricbuilder/architecture.jl @@ -0,0 +1,168 @@ +@testitem "ParametricBuilder / v1 direct target materialization" tags=[:unit, :parametric] setup=[ + UseEngineSupport +] begin + import LineCableModels.ParametricBuilder as PB + + conductor=PB.Material( + kind = Grid((:conductor,)), + rho = Grid((1.7e-8, 2.8e-8)), + eps_r = 1.0, + mu_r = 1.0 + ) + @test conductor isa Gridspace{Material} + @test length(conductor) == 2 + @test all(material -> material.kind === :conductor, conductor) + + wire=PB.core(conductor; r = 0.01, tag = :wire) + terminal=PB.Group(:core, wire) + design=build(CableDesign, "parametric", terminal) + @test design isa Gridspace{CableDesign} + @test length(design) == 2 + @test all(item -> item isa CableDesign, design) + @test all(item -> item.terminal_order == [:core], design) + + system=build( + LineCableSystem, + design, + PB.at(x = 0.0, y = -1.0); + connections = Dict(:core=>1), + system_id = "parametric-system", + line_length = 100.0 + ) + @test system isa Gridspace{LineCableSystem} + @test length(system) == length(design) + @test all(item -> item isa LineCableSystem, system) + + collection_system=build( + LineCableSystem, + [design, design], + [Pose2(-0.1, -1.0), Pose2(0.1, -1.0)]; + connections = [Dict(:core=>1), Dict(:core=>2)], + system_id = "parametric-collection", + line_length = 100.0 + ) + @test collection_system isa Gridspace{LineCableSystem} + @test length(collection_system) == length(design)^2 + @test all(item -> item isa LineCableSystem, collection_system) + @test all(item -> all(cable -> cable isa CableDesign, item.designs), collection_system) + + problems=LineParametersProblem( + system; + earth_props = PB.homogeneous(rho = 100.0, eps_r = 10.0), + frequencies = [50.0] + ) + @test problems isa Gridspace{LineParametersProblem} + @test length(problems) == 2 + @test all(problem -> problem.system isa LineCableSystem, problems) + @test (@inferred CableDesign first(design)) isa CableDesign + @test (@inferred LineCableSystem first(system)) isa LineCableSystem + + earth_space=PB.homogeneous(rho = Grid((10.0, 100.0)), eps_r = 1.0) + collection_problems=LineParametersProblem( + collection_system, + earth_space; + frequencies = [50.0] + ) + @test collection_problems isa Gridspace{LineParametersProblem} + @test length(collection_problems) == length(collection_system) * length(earth_space) + for space in (conductor, wire, terminal, design, system, problems) + @test eltype(space) === Any || isconcretetype(eltype(space)) + @test !(eltype(space) isa UnionAll) + end + + identifiers=Grid(("first", "second")) + direct=build(CableDesign, "first", first(terminal)) + design_space=build(CableDesign, identifiers, first(terminal)) + @test eltype(design_space) === Any + @test Base.IteratorEltype(typeof(design_space)) isa Base.EltypeUnknown + @test first(design_space).origin == direct.origin + @test first(design_space).terminal_order == direct.terminal_order + @test rand(design_space) isa CableDesign + + atomic=build( + LineCableSystem, + [first(design), first(design)], + [Pose2(-0.1, -1.0), Pose2(0.1, -1.0)]; + connections = [(core = 1,), (core = 2,)] + ) + @test atomic isa LineCableSystem + @test ncables(atomic) == 2 + + nested_identifier=Gridspace{String}( + value->string("nested-", value), + (Grid((1, 2)),) + ) + nested_designs=build(CableDesign, nested_identifier, first(terminal)) + @test nested_designs isa Gridspace{CableDesign} + @test first(nested_designs).cable_id == "nested-1" + + import LineCableModels.DataModel as DM + const resolution_count=Ref(0) + struct CountedDefinition{T <: Real}<:DM.AbstractPrimitive{T} + radius::T + end + struct CountedPrimitive{T <: Real}<:DM.AbstractPrimitive{T} + radius::T + at::DM.Pose2{T} + end + DM.resolve(::DM.EmptyBoundary, definition::CountedDefinition) = begin + resolution_count[]+=1 + CountedPrimitive(definition.radius, DM.Pose2(0, 0, 0)) + end + DM.resolve(at::DM.Pose2, primitive::CountedPrimitive) = CountedPrimitive(primitive.radius, at*primitive.at) + DM.boundary(primitive::CountedPrimitive) = primitive + DM.area(primitive::CountedPrimitive) = pi*primitive.radius^2 + DM.centroid(primitive::CountedPrimitive) = (primitive.at.x, primitive.at.y) + DM.support(primitive::CountedPrimitive, φ::Real) = primitive.at.x*cos(φ)+primitive.at.y*sin(φ)+primitive.radius + DM.support(primitive::CountedPrimitive) = hypot(primitive.at.x, primitive.at.y)+primitive.radius + counted_root=Group( + :core, + Region(:counted, CountedDefinition(0.01), Material( + kind = :conductor, + rho = 1.7e-8 + )) + ) + counted_space=build(CableDesign, Grid(("counted-a", "counted-b")), counted_root) + counted_designs=collect(counted_space) + @test resolution_count[] == 2 + LineParametersProblem( + first(counted_designs), + Pose2(0.0, -1.0); + connections = (core = 1,), + earth_props = PB.homogeneous(rho = 100.0), + frequencies = [1.0, 10.0, 100.0] + ) + @test resolution_count[] == 2 +end + +@testitem "ParametricBuilder / v1 physical conveniences preserve one grammar" tags=[:unit, :parametric] begin + using LineCableModels + import LineCableModels.ParametricBuilder as PB + + copper=Material(kind = :conductor, rho = 1.7e-8) + xlpe=Material(kind = :insulator, rho = 1.0e14, eps_r = 2.3) + core=PB.core(copper; r = 0.01, tag = :core) + insulation=PB.insulation(xlpe; t = Grid((0.003, 0.004)), tag = :insulation) + roots=PB.Stack(PB.Group(:core, core), insulation) + @test roots isa Gridspace{Stack} + + designs=build(CableDesign, "two-thicknesses", roots) + @test outer_radius.(collect(designs)) ≈ [0.013, 0.014] + @test all(design -> design.geometry isa CableGeometry, designs) + + @test PB.at(x = 1, y = -2) == Pose2(1, -2, 0) + design = first(designs) + connections = (core = (1, 2, 3),) + @test length(PB.trefoil( + design; + center = PB.at(0, -1), + spacing = 0.1, + connections + )) == 3 + @test length(PB.hflat(design; spacing = 0.1, connections)) == 3 + @test length(PB.vflat(design; spacing = 0.1, connections)) == 3 +end + +# Retired-name and file-absence scans were development preservation guards. +# The current constructors and single materialization path are exercised above. +# Ownership is checked by ExplicitImports through the loaded implementations. diff --git a/test/unit/parametricbuilder/armor.jl b/test/unit/parametricbuilder/armor.jl new file mode 100644 index 000000000..529489536 --- /dev/null +++ b/test/unit/parametricbuilder/armor.jl @@ -0,0 +1,33 @@ +@testitem "ParametricBuilder / armor grids retain course area and helical resistance" tags=[:unit, :parametric] begin + const DM = LineCableModels.DataModel + copper = Material(kind=:conductor, rho=1.72e-8) + steel = Material(kind=:conductor, rho=1.5e-7, mu_r=80.0) + dielectric = Material(kind=:insulator, rho=Inf, eps_r=2.3) + path = Helix(LayRatio(10); dir=-1, φ0=0.2) + course = armor(steel; shape=Disk(0.1e-3), n=Grid((12, 18)), lay=path) + @test course isa Gridspace{Group} + designs = build(CableDesign, "armor-grid", Stack( + terminal(:core, core(copper; r=1e-3), insulation(dielectric; t=0.5e-3)), + course, + shell(dielectric; t=0.2e-3), + )) + @test designs isa Gridspace{CableDesign} + @test length(designs) == 2 + for (count, design) in zip((12, 18), designs) + wires = filter(region -> region.terminal === :armor, design.geometry.regions) + @test length(wires) == count + @test all(region -> region.primitive isa Disk, wires) + @test all(region -> area(region.primitive) ≈ pi * (0.1e-3)^2, wires) + @test all(region -> only(region.paths).path === path, wires) + @test all(region -> only(region.paths).radius ≈ 1.6e-3, wires) + @test design.terminal_order == [:core, :armor] + @test outer_radius(design) ≈ 1.9e-3 + layers = DM.flatten(design, 50.0) + conductor = only(filter(component -> component.name === :armor, layers)).conductor + @test conductor.cross_section ≈ count * pi * (0.1e-3)^2 + @test conductor.resistance ≈ + steel.rho * sqrt(1 + (pi / 10)^2) / (count * pi * (0.1e-3)^2) + @test all(region -> region.source.material === steel, wires) + end + @test_throws ArgumentError armor(dielectric; shape=Disk(0.1e-3), n=12) +end diff --git a/test/unit/parametricbuilder/conformance.jl b/test/unit/parametricbuilder/conformance.jl new file mode 100644 index 000000000..bc3f1cfb7 --- /dev/null +++ b/test/unit/parametricbuilder/conformance.jl @@ -0,0 +1,46 @@ +@testitem "ParametricBuilder / Gridspace / structural realization" tags=[:unit, :measurements] setup=[ + UseEngineSupport] begin + using Random + using Measurements + import LineCableModels.ParametricBuilder as PB + + struct DuplicateValue end + (::DuplicateValue)(value)=(value, value) + + duplicated=PB.Gridspace{Tuple}( + DuplicateValue(), + (PB.Grid(10.0, PB.AbsoluteError(0.5)),) + ) + duplicated_point=first(PB.points(duplicated)) + propagated=PB.materialize(duplicated_point) + @test propagated[1] === propagated[2] + @test Measurements.cov(propagated[1], propagated[2]) == + Measurements.uncertainty(propagated[1])^2 + + rng=MersenneTwister(42) + @test all(1:32) do _ + draw=LineCableModels.realize(rng, duplicated_point, :normal) + draw[1] === draw[2] + end + + independent=PB.Gridspace{Tuple}( + tuple, + ( + PB.Grid(10.0, PB.AbsoluteError(0.5)), + PB.Grid(10.0, PB.AbsoluteError(0.5)) + ); + combine = :zip + ) + independent_values=PB.materialize(first(PB.points(independent))) + @test independent_values[1] !== independent_values[2] + @test iszero(Measurements.cov(independent_values...)) + + reused=PB.Grid(10.0, PB.AbsoluteError(0.5)) + reused_values=PB.materialize(first(PB.points(PB.Gridspace{Tuple}( + tuple, + (reused, reused); + combine = :zip + )))) + @test reused_values[1] !== reused_values[2] + @test iszero(Measurements.cov(reused_values...)) +end diff --git a/test/unit/parametricbuilder/construction_api.jl b/test/unit/parametricbuilder/construction_api.jl new file mode 100644 index 000000000..9076eac9d --- /dev/null +++ b/test/unit/parametricbuilder/construction_api.jl @@ -0,0 +1,1278 @@ +@testitem "ParametricBuilder / construction API / lowering and terminal ownership" tags=[:unit, :parametric] begin + using LineCableModels + import LineCableModels.DataModel as DM + + copper = Material(kind = :conductor, rho = 1.72e-8) + xlpe = Material(kind = :insulator, rho = 1.0e14, eps_r = 2.3) + semicon = Material(kind = :semicon, rho = 100.0, eps_r = 20.0) + + phase = terminal( + :phase, + core(copper; r = 5e-3), + screen(semicon; t = 0.5e-3), + insulation(xlpe; t = 3e-3) + ) + explicit = Group( + :phase, + layers( + Region(:core, Disk(5e-3), copper), + Region(:screen, Shell(0.5e-3), semicon), + Region(:insulation, Shell(3e-3), xlpe) + ) + ) + @test phase == explicit + + functional = build(CableDesign, "coaxial", phase) + notated = @cable "coaxial" begin + @terminal :phase begin + core(copper; r = 5e-3) + screen(semicon; t = 0.5e-3) + insulation(xlpe; t = 3e-3) + end + end + @test notated == functional + @test functional.terminal_order == [:phase] + @test functional.origin isa Group + @test all(==(1), filter(!iszero, functional.terminal_map)) + @test_throws ArgumentError build( + CableDesign, + "undeclared-terminal", + core(copper; r = 5e-3) + ) + + insulating_tapes = tape( + xlpe; + section = Rectangle(1.0e-3, 0.5e-3), + n = 12, + lay = LayRatio(10) + ) + @test_throws ArgumentError terminal(:insulating_tapes, insulating_tapes) + with_tapes = build(CableDesign, "taped", phase, insulating_tapes) + @test with_tapes.terminal_order == [:phase] + @test count(iszero, with_tapes.terminal_map) > 0 + + distributed_tapes = @distribute tape( + xlpe; + section = Rectangle(1.0e-3, 0.5e-3), + gap_frac = 0.02, + lay = LayRatio(10) + ) + @test distributed_tapes.pattern.n == capacity() + distributed_design = build( + CableDesign, + "distributed-tapes", + phase, + distributed_tapes + ) + tape_regions = filter( + region -> region.source.tag === :tape, + distributed_design.geometry.regions + ) + @test length(tape_regions) == capacity( + Ring(capacity(); r = 8.75e-3, gap_frac = 0.02), + Rectangle(1.0e-3, 0.5e-3), + nothing + ) + @test all(region -> length(region.paths) == 1, tape_regions) + + nested = build( + CableDesign, + "nested-terminal", + terminal(:outer, terminal(:inner, core(copper; r = 2e-3))) + ) + @test nested.terminal_order == [:outer] + + sector_a = terminal( + :a, + core(copper, Sector(span = pi / 3, r_base = 0.5e-3, r_back = 3e-3)), + insulation(xlpe; t = 0.5e-3) + ) + sector_b = terminal( + :b, + core(copper, Sector(span = pi / 4, r_base = 0.4e-3, r_back = 2.5e-3)), + insulation(xlpe; t = 0.4e-3) + ) + explicit_assembly = assembly( + at(sector_a, -5e-3, 0; φ = 0.2), + at(sector_b, 5e-3, 0; φ = -0.3) + ) + notated_assembly = @assembly begin + @at sector_a (-5e-3, 0, 0.2) + @at sector_b (5e-3, 0, -0.3) + end + @test notated_assembly == explicit_assembly + multicore = build(CableDesign, "sectorized", explicit_assembly) + @test multicore.terminal_order == [:a, :b] + @test multicore.geometry.regions[1].primitive isa DM.SectorShape + + separate_screen = build( + CableDesign, + "screened", + terminal(:phase, core(copper; r = 2e-3), insulation(xlpe; t = 1e-3)), + terminal(:screen, screen(copper; t = 0.2e-3)) + ) + @test separate_screen.terminal_order == [:phase, :screen] +end + +@testitem "ParametricBuilder / construction API / public surface and macro lowering" tags=[:unit, :parametric] begin + using LineCableModels + import LineCableModels.DataModel as DM + import LineCableModels.ParametricBuilder as PB + + required = ( + :build, :terminal, :core, :stranded, :milliken, :rope, :cores, :tape, + :insulation, :screen, :sheath, :armor, :bedding, :jacket, :filler, + :pipe, :duct, :at, :trefoil, :hflat, :vflat, :capacity, + :solid, :shell, :wires, :layers, :assembly, :layer, :homogeneous, + :EarthLayer, :EarthModel, + :Region, :Stack, :Group, :Assembly, :Enclosure, :Pose2, + :Disk, :Rectangle, :Ellipse, + :Sector, :Annulus, :Shell, + :Polygon, :Ring, :Polar, :Fill, :Lattice, + :Helix, :LayRatio, :Pitch, :LayAngle, :FillFactor, + Symbol("@cable"), Symbol("@system"), Symbol("@earth"), + Symbol("@terminal"), Symbol("@assembly"), + Symbol("@pipe"), Symbol("@duct"), Symbol("@at"), + Symbol("@hflat"), Symbol("@vflat"), + Symbol("@trefoil"), Symbol("@distribute") + ) + public_names = Set(names(LineCableModels)) + @test all(name -> name in public_names, required) + + @test LineCableModels.Earth isa Module + @test parentmodule(EarthModel) === LineCableModels.Earth + @test Base.ispublic(DM, :BentStrip) + @test Base.ispublic(DM, :BoundedPlacement) + @test Base.ispublic(DM, :EnclosureBoundary) + @test Base.ispublic(DM, :EllipseOffset) + @test LineCableModels.build === DM.build === PB.build + + expansions = ( + :(@cable "x" nominal_data=metadata combine=:zip begin + part + end) => :build, + :(@system "x" line_length=1 begin + placed + end) => :build, + :(@earth begin + layer + end) => :build, + :(@terminal :phase combine=:zip begin + part + end) => :terminal, + :(@assembly combine=:zip begin + left + right + end) => :assembly, + :(@pipe shape=shape fill=fill combine=:zip begin + left + right + end) => :pipe, + :(@duct shape=shape fill=fill combine=:zip begin + left + right + end) => :duct, + :(@at subject (1, 2) φ=3) => :at, + :(@trefoil design spacing=spacing center=(0, 0) combine=:zip phase=( + 1, 2, 3)) => :trefoil, + :(@hflat design spacing=spacing combine=:zip phase=(1, 2, 3)) => :hflat, + :(@vflat design spacing=spacing combine=:zip phase=(1, 2, 3)) => :vflat + ) + for (surface, callee) in expansions + expanded = Base.remove_linenums!(macroexpand(@__MODULE__, surface)) + @test expanded isa Expr && expanded.head === :call + @test expanded.args[1] == GlobalRef(PB, callee) + end + + combined_surfaces = ( + :(@cable "x" combine=:zip begin + part + end), + :(@terminal :phase combine=:zip begin + part + end), + :(@assembly combine=:zip begin + left + right + end), + :(@pipe shape=shape fill=fill combine=:zip begin + part + end), + :(@duct shape=shape fill=fill combine=:zip begin + part + end), + :(@trefoil design spacing=spacing combine=:zip phase=(1, 2, 3)), + :(@hflat design spacing=spacing combine=:zip phase=(1, 2, 3)), + :(@vflat design spacing=spacing combine=:zip phase=(1, 2, 3)) + ) + for surface in combined_surfaces + expanded = Base.remove_linenums!(macroexpand(@__MODULE__, surface)) + parameters = only(filter( + argument -> argument isa Expr && argument.head === :parameters, + expanded.args + )) + @test any(parameters.args) do keyword + keyword isa Expr && keyword.head === :kw && + keyword.args == [:combine, QuoteNode(:zip)] + end + end + + distributed = Base.remove_linenums!(macroexpand( + @__MODULE__, + :(@distribute wires(material; shape = wire, r = radius)) + )) + @test distributed.head === :call + @test distributed.args[1] === :wires + parameters = only(filter( + argument -> argument isa Expr && argument.head === :parameters, + distributed.args + )) + deferred = only(filter( + argument -> argument isa Expr && argument.head === :kw && + argument.args[1] === :n, + parameters.args + )) + @test deferred.args[2] == Expr(:call, GlobalRef(PB, :capacity)) + + copper = Material(kind = :conductor, rho = 1.72e-8) + evaluations = Dict{Symbol, Int}() + once(name, value) = (evaluations[name] = get(evaluations, name, 0) + 1; value) + nested = @cable once(:identifier, "single-evaluation") nominal_data=( + rated_voltage = once(:rated_voltage, 132e3), + ) begin + @terminal once(:terminal, :phase) begin + core( + once(:material, copper); + r = once(:radius, 1e-3) + ) + end + end + @test nested isa CableDesign + @test evaluations == Dict( + :identifier => 1, + :rated_voltage => 1, + :terminal => 1, + :material => 1, + :radius => 1 + ) + @test nested.nominal_data == (rated_voltage = 132e3,) + + system = @system once(:system_id, "single-system-evaluation") line_length=once( + :line_length, 1.0 + ) begin + @at nested (0.0, -1.0) phase=1 + end + @test system isa LineCableSystem + @test system.system_id == "single-system-evaluation" + @test evaluations[:system_id] == 1 + @test evaluations[:line_length] == 1 +end + +@testitem "ParametricBuilder / construction API / earth grammar" tags=[:unit, :parametric] begin + using LineCableModels + + earth = @earth begin + layer(rho = 100.0, eps_r = 10.0, thickness = 5.0) + layer(rho = 500.0, eps_r = 20.0) + end + @test earth isa EarthModel{Float64} + @test !earth.vertical_layers + @test length(earth.layers) == 3 + @test isinf(first(earth.layers).rho) + @test getproperty.(earth.layers[2:end], :rho) == (100.0, 500.0) + @test getproperty.(earth.layers[2:end], :thickness) == (5.0, Inf) + @test layer(rho = 100.0f0) isa EarthLayer{Float32} + + air = layer(rho = Inf, eps_r = 1.0006, mu_r = 1.0) + explicit_air = @earth air_layer=air begin + layer(rho = 100.0) + end + @test first(explicit_air.layers) === air + + earth_model = @inferred homogeneous(rho = 100.0, eps_r = 10.0) + @test earth_model isa EarthModel{Float64} + @test length(earth_model.layers) == 2 + @test earth_model.layers[2].rho == 100.0 + + spaces = @earth begin + layer(rho = Grid((10.0, 100.0)), eps_r = 5.0, thickness = 2.0) + layer(rho = 500.0, eps_r = 20.0) + end + @test spaces isa Gridspace{EarthModel} + @test length(spaces) == 2 + @test [model.layers[2].rho for model in spaces] == [10.0, 100.0] + @test all(model -> model.layers[3].rho == 500.0, spaces) + + @test_throws ArgumentError macroexpand( + @__MODULE__, + :(@earth unsupported=true begin + layer(rho = 100.0) + end) + ) +end + +@testitem "ParametricBuilder / construction API / macro arguments evaluate once" tags=[:unit, :parametric] begin + using LineCableModels + + copper = Material(kind = :conductor, rho = 1.72e-8) + air = Material(kind = :insulator, rho = 1.0e16, eps_r = 1.0) + counts = Dict{Symbol, Int}() + once(name, value) = (counts[name] = get(counts, name, 0) + 1; value) + + design = @cable once(:cable_id, "macro-evaluation") begin + @terminal once(:terminal_name, :phase) begin + core(once(:core_material, copper); r = once(:core_radius, 1e-3)) + end + end + member = terminal(:aux, core(copper; r = 0.5e-3)) + placed_member = at(member, 0.0, 0.0) + + @assembly begin + once(:assembly_first, placed_member) + once(:assembly_second, at(member, 2e-3, 0.0)) + end + @duct shape=once(:duct_shape, Disk(5e-3)) fill=once(:duct_fill, air) begin + once(:duct_member, placed_member) + end + @at once(:at_subject, design) ( + once(:at_x, 0.0), + once(:at_y, -1.0), + once(:at_angle, 0.2) + ) connections = once(:at_connections, (phase = 1,)) + + @trefoil once(:trefoil_design, design) spacing=once(:trefoil_spacing, 0.1) center=( + once(:trefoil_x, 0.0), once(:trefoil_y, -1.0) + ) phase=once(:trefoil_connections, (1, 2, 3)) + @hflat once(:hflat_design, design) spacing=once(:hflat_spacing, 0.1) phase=once( + :hflat_connections, (1, 2, 3)) + @vflat once(:vflat_design, design) spacing=once(:vflat_spacing, 0.1) phase=once( + :vflat_connections, (1, 2, 3)) + + @distribute wires( + once(:wire_material, copper); + shape = once(:wire_definition, Disk(0.2e-3)), + r = once(:wire_radius, 2e-3) + ) + + @test !isempty(counts) + @test all(==(1), values(counts)) +end + +@testitem "ParametricBuilder / construction API / enclosures and macro transparency" tags=[:unit, :parametric] begin + using LineCableModels + import LineCableModels.DataModel as DM + + copper = Material(kind = :conductor, rho = 1.72e-8) + xlpe = Material(kind = :insulator, rho = 1.0e14, eps_r = 2.3) + air = Material(kind = :insulator, rho = 1.0e16, eps_r = 1.0) + concrete = Material(kind = :insulator, rho = 100.0, eps_r = 4.0) + + cell_a = duct( + terminal(:a, core(copper; r = 1e-3), insulation(xlpe; t = 0.5e-3)); + shape = Disk(4e-3), + fill = air + ) + cell_b = duct( + terminal(:b, core(copper; r = 1.2e-3), insulation(xlpe; t = 0.4e-3)); + shape = Disk(4e-3), + fill = air + ) + asymmetric = duct( + at(cell_a, -6e-3, 0), + at(cell_b, 6e-3, 1e-3; φ = 0.2); + shape = Rectangle(20e-3, 12e-3), + fill = concrete + ) + notated = @duct shape=Rectangle(20e-3, 12e-3) fill=concrete begin + @at cell_a (-6e-3, 0) + @at cell_b (6e-3, 1e-3, 0.2) + end + @test notated == asymmetric + @test asymmetric.item isa Assembly + design = build(CableDesign, "asymmetric-ducts", asymmetric) + @test design.terminal_order == [:a, :b] + @test any(region -> region.source.tag === :duct_fill, design.geometry.regions) + + piped = pipe( + at(cell_a, -5e-3, 0), + at(cell_b, 5e-3, 0); + shape = Disk(15e-3), + fill = air + ) + notated_pipe = @pipe shape=Disk(15e-3) fill=air begin + @at cell_a (-5e-3, 0) + @at cell_b (5e-3, 0) + end + @test notated_pipe == piped + + pipe_space = @pipe shape=Disk(Grid((15e-3, 16e-3))) fill=Grid(( + air, + concrete + )) combine=:zip begin + cell_a + end + @test pipe_space isa Gridspace{Enclosure} + @test length(pipe_space) == 2 + + repeated = duct( + cell_a; + formation = Lattice(nx = 2, ny = 1, dx = 12e-3, dy = 0), + shape = Rectangle(24e-3, 12e-3), + fill = concrete + ) + @test repeated.item isa Assembly + @test repeated.item.item === cell_a + @test repeated.item.pattern == Lattice(nx = 2, ny = 1, dx = 12e-3, dy = 0) + + elliptical = duct( + at(cell_a, -6e-3, 0), + at(cell_b, 6e-3, 0); + shape = Ellipse(15e-3, 8e-3), + fill = concrete + ) + elliptical_design = build(CableDesign, "elliptical-duct", elliptical) + @test elliptical_design.geometry.outer isa Ellipse + @test area(elliptical_design.geometry.outer) ≈ pi * 15e-3 * 8e-3 + elliptical_fill = only(filter(elliptical_design.geometry.regions) do region + region.primitive isa DM.DifferenceShape && + region.primitive.outer isa Ellipse + end) + @test elliptical_fill.primitive isa DM.DifferenceShape + @test length(elliptical_fill.primitive.holes) == 2 + @test all(hole -> hole isa Disk, elliptical_fill.primitive.holes) + + elliptical_wall = duct( + at(cell_a, -6e-3, 0), + at(cell_b, 6e-3, 0); + shape = Ellipse(15e-3, 8e-3), + fill = air, + wall = jacket(concrete; t = 2e-3, tag = :concrete_wall) + ) + elliptical_wall_design = build( + CableDesign, "elliptical-duct-wall", elliptical_wall + ) + @test elliptical_wall_design.geometry.outer isa DM.EllipseOffset + wall_region = only(filter(elliptical_wall_design.geometry.regions) do region + region.source.tag === :concrete_wall + end) + @test wall_region.primitive isa DM.ShellShape + @test wall_region.primitive.inner isa Ellipse + @test wall_region.primitive.outer isa DM.EllipseOffset + @test DM.support(wall_region.primitive.outer, 0) ≈ 17e-3 + @test DM.support(wall_region.primitive.outer, pi / 2) ≈ 10e-3 + @test_throws DomainError build( + CableDesign, + "overfilled-elliptical-duct", + duct( + at(cell_a, -14e-3, 0), + at(cell_b, 14e-3, 0); + shape = Ellipse(15e-3, 8e-3), + fill = concrete + ) + ) + + evaluations = Symbol[] + identifier() = (push!(evaluations, :identifier); "single-evaluation") + phase() = (push!(evaluations, :phase); terminal(:phase, core(copper; r = 1e-3))) + built = @cable identifier() begin + """ +This literal is documentation, not a cable part. +""" + nothing + phase() + end + @test built isa CableDesign + @test evaluations == [:identifier, :phase] + + expanded = Base.remove_linenums!(macroexpand( + @__MODULE__, + :(@terminal :phase begin + core(copper; r = 1e-3) + insulation(xlpe; t = 0.5e-3) + end) + )) + @test expanded isa Expr && expanded.head === :call + @test expanded.args[1] == GlobalRef(LineCableModels.ParametricBuilder, :terminal) + @test expanded.args[2] == QuoteNode(:phase) + @test length(expanded.args) == 4 +end + +@testitem "ParametricBuilder / construction API / repetition and compaction" tags=[:unit, :measurements, :slow] begin + using LineCableModels + using Measurements: Measurement, uncertainty + import LineCableModels.DataModel as DM + + copper = Material(kind = :conductor, rho = 1.72e-8) + xlpe = Material(kind = :insulator, rho = 1.0e14, eps_r = 2.3) + + @test LayRatio(13, 12, 11) == + (LayRatio(13), LayRatio(12), LayRatio(11)) + @test LayRatio((13, 12, 11)) == LayRatio(13, 12, 11) + @test Pitch(0.13, 0.12) == (Pitch(0.13), Pitch(0.12)) + @test LayAngle([0.13, 0.12]) == (LayAngle(0.13), LayAngle(0.12)) + @test FillFactor(0.88, 0.9) == (FillFactor(0.88), FillFactor(0.9)) + + round_boundary = Disk(2.5e-3) + round = stranded( + copper; + shape = Disk(0.5e-3), + lay = (LayRatio(12), Pitch(0.2)), + dir = (1, -1), + φ0 = (0.0, 0.1), + boundary = round_boundary + ) + @test round isa Enclosure + round_group = round.item + @test round_group.boundary == round_boundary + @test round_group.pattern === nothing + round_declarations = round_group.item.items + @test length(round_declarations) == 2 + @test all(part -> part.pattern === nothing, round_declarations) + @test round_declarations[1].path === nothing + @test round_declarations[2].path == ( + Helix(LayRatio(12); dir = 1, φ0 = 0.0), + Helix(Pitch(0.2); dir = -1, φ0 = 0.1) + ) + _, round_members, round_shapes = DM.bounded_members(round_group) + @test [count(member -> member.course == course, round_members) + for course in 1:2] == [6, 12] + @test all(shape -> shape isa Disk, round_shapes) + @test all(round_members) do member + member.course == 0 && return true + radius = 0.5e-3 + (2member.course - 1) * 0.5e-3 + isapprox(hypot(member.site[1], member.site[2]), radius) + end + + compact_boundary = Disk(sqrt(19) * 0.5e-3) + normalized = stranded( + copper; + shape = Disk(0.5e-3), + lay = LayRatio(12, 11), + compact = true, + boundary = compact_boundary + ) + normalized_group = normalized.item + @test normalized_group.compact + @test normalized_group.item.items[2].path == ( + Helix(LayRatio(12)), + Helix(LayRatio(11)) + ) + + sector_boundary = Sector( + span = deg2rad(119.0), + r_base = 1.10e-3, + r_back = 10.24e-3, + fillet = 1.02e-3 + ) + resolved_sector = DM.resolve(DM.EmptyBoundary(), sector_boundary) + bounded = stranded( + copper; + shape = Disk(0.5e-3), + lay = LayRatio(12), + boundary = sector_boundary + ) + @test bounded isa Enclosure + bounded_group = bounded.item + @test bounded_group isa Group + @test bounded_group.pattern === nothing + @test bounded_group.path === nothing + @test bounded_group.compact === nothing + @test bounded_group.boundary == sector_boundary + @test bounded_group.item isa Stack + @test length(bounded_group.item.items) == 1 + @test only(bounded_group.item.items).path == Helix(LayRatio(12)) + bounded_design = build( + CableDesign, + "bounded-stranded-core", + terminal(:core, bounded) + ) + bounded_conductors = filter( + region -> region.source.material.kind === :conductor, + bounded_design.geometry.regions + ) + expected_courses = DM.course_count( + DM.area(resolved_sector) - DM.area(Disk(0.5e-3)), DM.area(Disk(0.5e-3)) + ) + expected_bounded_count = 1 + 3expected_courses * (expected_courses + 1) + @test length(bounded_conductors) == expected_bounded_count + @test DM.boundary(bounded_design.geometry).primitive == sector_boundary + @test all(region -> region.primitive isa DM.Polygon, + bounded_conductors) + @test sum(DM.area, bounded_conductors) ≈ + expected_bounded_count * pi * (0.5e-3)^2 + bounded_fills = filter( + region -> region.source.material.kind !== :conductor, + bounded_design.geometry.regions + ) + @test length(bounded_fills) == 1 + @test only(bounded_fills).primitive isa DM.DifferenceShape + @test length(only(bounded_fills).primitive.holes) == + length(bounded_conductors) + @test only(bounded_fills).source.material != copper + @test sum(DM.area, bounded_design.geometry.regions) ≈ + DM.area(bounded_design.geometry.outer) + sector_center = DM.centroid(resolved_sector) + @test isempty(first(bounded_conductors).paths) + @test all(Iterators.drop(bounded_conductors, 1)) do region + radius = only(region.paths).radius + center = DM.centroid(region.primitive) + radius ≈ hypot( + center[1] - sector_center[1], + center[2] - sector_center[2] + ) + end + bounded_component = only(DM.flatten(bounded_design, 50.0)) + @test bounded_component.conductor.r_ex ≈ sqrt(DM.area(resolved_sector) / pi) + @test all(isapprox.( + bounded_component.conductor.position, + DM.centroid(resolved_sector) + )) + member_resistances = map(bounded_conductors) do region + factor = isempty(region.paths) ? 1.0 : + overlength(only(region.paths).path, only(region.paths).radius) + copper.rho / DM.area(region.source.primitive) * factor + end + @test bounded_component.conductor.resistance ≈ + inv(sum(inv, member_resistances)) + + course_members, + course_shapes = DM.sector_members( + resolved_sector, DM.bounded_declarations(bounded_group) + ) + @test [count(member -> member.course == course, course_members) + for course in 1:expected_courses] == 6 .* (1:expected_courses) + @test all(shape -> shape isa DM.Polygon, course_shapes) + @test all(isapprox.(DM.area.(course_shapes), DM.area(Disk(0.5e-3)))) + resolved_courses = [only( + entry.pattern.course + for entry in region.placement.patterns + if entry.pattern isa DM.BoundedPlacement + ) for region in bounded_conductors] + @test [count(==(course), resolved_courses) + for course in 1:expected_courses] == 6 .* (1:expected_courses) + @test [only(entry.member + for entry in region.placement.patterns + if entry.pattern isa DM.BoundedPlacement) + for region in bounded_conductors] == eachindex(bounded_conductors) + @test_throws ArgumentError stranded( + copper; + shape = Disk(0.5e-3), + compact = true, + boundary = sector_boundary + ) + @test_throws ArgumentError stranded( + copper; + center = Disk(0.5e-3), + shape = Disk(0.5e-3), + boundary = sector_boundary + ) + + invalid_sector_space = stranded( + copper; + shape = Disk(0.5e-3), + compact = Grid((nothing, true)), + boundary = sector_boundary + ) + @test first(invalid_sector_space) isa Enclosure + @test_throws ArgumentError collect(invalid_sector_space) + + second_boundary = Sector( + span = deg2rad(118.0), + r_base = 1.0e-3, + r_back = 10.0e-3, + fillet = 0.9e-3 + ) + boundary_space = stranded( + copper; + shape = Disk(0.5e-3), + boundary = Grid((sector_boundary, second_boundary)) + ) + @test boundary_space isa Gridspace{Enclosure} + @test collect(boundary_space) == [stranded( + copper; + shape = Disk(0.5e-3), + boundary + ) for boundary in (sector_boundary, second_boundary)] + + conductor_space = Material( + kind = :conductor, + rho = Grid((1.72e-8, 2.82e-8)) + ) + fill_space = Material( + kind = :insulator, + rho = 1.0e14, + eps_r = Grid((2.3, 3.1)) + ) + sector_formation_space = stranded( + conductor_space; + shape = Disk(Grid((0.45e-3, 0.5e-3))), + lay = Grid((LayRatio(11), LayRatio(13))), + boundary = Grid((sector_boundary, second_boundary)), + fill = fill_space + ) + @test sector_formation_space isa Gridspace{Enclosure} + @test length(sector_formation_space) == 32 + sector_formations = collect(sector_formation_space) + @test all(formation -> formation isa Enclosure, sector_formations) + + uncertain_sector_space = stranded( + copper; + shape = Disk(Grid(0.5e-3, 1.0)), + boundary = sector_boundary + ) + uncertain_sector_design = build( + CableDesign, + "uncertain-sector-core", + terminal(:phase, first(uncertain_sector_space)) + ) + @test eltype(uncertain_sector_design) <: Measurement + @test any( + region -> region.primitive isa DM.Polygon && + uncertainty(DM.area(region.primitive)) > 0, + uncertain_sector_design.geometry.regions + ) + + product_space = stranded( + copper; + shape = Disk(Grid((0.4e-3, 0.5e-3))), + dir = Grid((1, -1)), + lay = LayRatio(12), + boundary = Disk(2.0e-3) + ) + @test product_space isa Gridspace{Enclosure} + @test length(product_space) == 4 + + radii = (0.4e-3, 0.5e-3) + boundaries = Disk.(sqrt.((7, 8)) .* radii) + zipped_space = stranded( + copper; + shape = Grid(Disk.(radii)), + lay = Grid((LayRatio(12), LayRatio(11))), + dir = Grid((1, -1)), + compact = true, + boundary = Grid(boundaries), + combine = :zip + ) + zipped_designs = [build(CableDesign, "zipped-bounded-$index", terminal(:phase, value)) + for (index, value) in enumerate(zipped_space)] + @test length(zipped_designs) == 2 + @test [count(region -> region.source.material.kind === :conductor, + design.geometry.regions) for design in zipped_designs] == [7, 7] + + uncertain_space = stranded( + copper; + shape = Disk(Grid(0.5e-3, 1.0)), + compact = true, + boundary = Disk(sqrt(7) * 0.5e-3) + ) + uncertain_design = build( + CableDesign, + "uncertain-bounded-core", + terminal(:phase, first(uncertain_space)) + ) + @test eltype(uncertain_design) <: Measurement + @test any( + region -> uncertainty(DM.area(region.primitive)) > 0, + uncertain_design.geometry.regions + ) + + @test_throws ArgumentError stranded( + copper; + shape = Disk(0.5e-3), + boundary = "rounded" + ) + + rectangular = stranded( + copper; + center = Disk(0.5e-3), + shape = Rectangle(0.6e-3, 0.8e-3), + boundary = Disk(sqrt((0.5e-3)^2 + 16 * 0.6e-3 * 0.8e-3 / pi)) + ) + rectangular_design = build( + CableDesign, "rectangular-strands", terminal(:phase, rectangular) + ) + rectangular_conductors = filter( + region -> region.source.material.kind === :conductor, + rectangular_design.geometry.regions + ) + @test first(rectangular_conductors).primitive isa Disk + @test all(region -> region.primitive isa DM.BentStrip, + Iterators.drop(rectangular_conductors, 1)) + @test all(region -> DM.area(region.primitive) ≈ DM.area(region.source.primitive), + rectangular_conductors) + @test_throws ArgumentError stranded( + copper; + center = Disk(0.5e-3), + shape = Rectangle(0.6e-3, 0.8e-3), + compact = true, + boundary = Disk(3.0e-3) + ) + + milliken_core = milliken( + copper; + shape = Disk(0.3e-3), + segment = Sector( + span = pi / 3, + r_base = 0.65e-3, + r_back = 4.0e-3, + fillet = 0.15e-3 + ), + segments = 6 + ) + milliken_design = build( + CableDesign, "milliken", terminal(:phase, milliken_core) + ) + @test milliken_design.terminal_order == [:phase] + milliken_conductors = filter( + region -> region.source.material.kind === :conductor, + milliken_design.geometry.regions + ) + @test length(milliken_conductors) > 7 + center_wire = only(filter(milliken_conductors) do region + region.primitive isa Disk && + hypot(DM.centroid(region.primitive)...) < 1e-12 + end) + segment_wires = filter(!=(center_wire), milliken_conductors) + function inner_distance(region) + primitive = region.primitive + cosine = cos(primitive.at.φ) + sine = sin(primitive.at.φ) + points = [( + primitive.at.x + cosine * point[1] - sine * point[2], + primitive.at.y + sine * point[1] + cosine * point[2] + ) for point in primitive.points] + return minimum(eachindex(points)) do index + next = mod1(index + 1, length(points)) + edge = ( + points[next][1] - points[index][1], + points[next][2] - points[index][2] + ) + length_squared = edge[1]^2 + edge[2]^2 + fraction = clamp( + -(points[index][1] * edge[1] + points[index][2] * edge[2]) / + length_squared, + 0, + 1 + ) + hypot( + points[index][1] + fraction * edge[1], + points[index][2] + fraction * edge[2] + ) + end + end + inner_distances = inner_distance.(segment_wires) + @test center_wire.primitive.r ≈ minimum(inner_distances) + @test count( + distance -> isapprox(distance, center_wire.primitive.r), + inner_distances + ) >= 6 + milliken_fills = filter( + region -> region.source.material.kind !== :conductor, + milliken_design.geometry.regions + ) + @test length(milliken_fills) == 1 + @test only(milliken_fills).primitive isa DM.DifferenceShape + @test length(only(milliken_fills).primitive.holes) == + length(milliken_conductors) + @test sum(DM.area, milliken_design.geometry.regions) ≈ + DM.area(milliken_design.geometry.outer) + @test_throws DomainError milliken( + copper; + shape = Disk(0.3e-3), + segment = Sector( + span = pi / 4, + r_base = 0.65e-3, + r_back = 4.0e-3, + fillet = 0.15e-3 + ), + segments = 6 + ) + + compacted = stranded( + copper; + shape = Disk(0.5e-3), + compact = true, + lay = LayRatio(11), + boundary = Disk(sqrt(7) * 0.5e-3) + ) + compacted_design = build( + CableDesign, + "compacted", + terminal(:phase, compacted) + ) + deformed = filter( + region -> region.primitive isa DM.Polygon, + compacted_design.geometry.regions + ) + @test length(deformed) == 7 + @test sum(DM.area, deformed) ≈ 7pi * (0.5e-3)^2 + outer_course = filter(region -> length(region.paths) == 1, deformed) + @test length(outer_course) == 6 + @test all( + region -> only(region.paths).radius ≈ hypot(DM.centroid(region)...), + outer_course + ) + + saturated_boundary = Disk(sqrt(7) * 0.5e-3) + saturated = build( + CableDesign, + "saturated-compaction", + terminal( + :phase, + stranded( + copper; + shape = Disk(0.5e-3), + compact = true, + boundary = saturated_boundary + ) + ) + ) + @test sum(DM.area, saturated.geometry.regions) ≈ DM.area(saturated_boundary) + @test all( + region -> DM.support(region.primitive) <= + saturated_boundary.r * (1 + 5e-6), + saturated.geometry.regions + ) + @test_throws ArgumentError stranded( + copper; + shape = Disk(0.5e-3), + compact = FillFactor(0.9), + boundary = compact_boundary + ) + + direct_capacity = capacity(Ring, 10e-3, 0.5e-3; gap_frac = 0.03) + pattern = Ring(capacity(); r = 10e-3, gap_frac = 0.03) + @test capacity(pattern, Disk(0.5e-3), nothing) == direct_capacity + distributed = @distribute wires( + copper; + shape = Disk(0.5e-3), + r = 10e-3, + gap_frac = 0.03 + ) + @test distributed.pattern.n == capacity() + @test_throws ArgumentError macroexpand( + @__MODULE__, + :(@distribute wires(copper; shape = Disk(1e-3), n = 6, r = 2e-3)) + ) + @test_throws ArgumentError macroexpand( + @__MODULE__, + :(@distribute stranded(copper; shape = Disk(1e-3), boundary = Disk(3e-3))) + ) + + fill_pattern = Fill(r = 2.6e-3, φ = pi / 6) + filled_poses = placements(fill_pattern, Disk(0.5e-3), nothing) + @test length(filled_poses) == 19 + @test capacity(fill_pattern, Disk(0.5e-3), nothing) == 19 + @test first(filled_poses) == Pose2(0, 0, 0) + @test maximum(pose -> hypot(pose.x, pose.y), filled_poses) + 0.5e-3 <= + fill_pattern.r + eps(fill_pattern.r) + + nested = rope( + round; + layers = 1, + n = 6, + lay = LayRatio(9) + ) + scheduled_rope = rope( + round; + layers = 2, + n = (6, 12), + lay = LayRatio(10, 9) + ) + @test getfield.(scheduled_rope.items[2:3], :path) == [ + Helix(LayRatio(10)), + Helix(LayRatio(9)) + ] + nested_design = build( + CableDesign, + "nested-rope", + terminal(:phase, nested) + ) + @test any(region -> length(region.paths) == 2, nested_design.geometry.regions) + @test any(region -> isempty(region.paths), nested_design.geometry.regions) + nested_conductors = filter( + region -> region.source.material.kind === :conductor, + nested_design.geometry.regions + ) + leaf_resistances = map(nested_conductors) do region + primitive = region.source.primitive + base = DM.tubular_resistance( + 0.0, + primitive.r, + region.source.material.rho + ) + foldl(region.paths; init = base) do resistance, entry + resistance * overlength(entry.path, entry.radius) + end + end + expected_resistance = inv(sum(inv, leaf_resistances)) + component = only(LineCableModels.DataModel.flatten( + nested_design, + 50.0 + )) + @test component.conductor.resistance ≈ expected_resistance + + @test_throws DimensionMismatch build( + CableDesign, + "wrong-lay-schedule", + terminal( + :phase, + stranded( + copper; + shape = Disk(0.5e-3), + lay = (LayRatio(10),), + boundary = round_boundary + ) + ) + ) +end + +@testitem "ParametricBuilder / construction API / placement, macros, and Gridspace" tags=[:unit, :parametric] begin + using LineCableModels + import LineCableModels.DataModel as DM + + copper = Material(kind = :conductor, rho = 1.72e-8) + xlpe = Material(kind = :insulator, rho = 1.0e14, eps_r = 2.3) + design = @cable "formation-cable" begin + @terminal :phase begin + core(copper; r = 3e-3) + insulation(xlpe; t = 1e-3) + end + end + + explicit = trefoil( + design; + center = at(0.2, -1.0), + spacing = 0.1, + connections = (phase = (1, 2, 3),) + ) + notated = @trefoil design spacing=0.1 center=(0.2, -1.0) phase=(1, 2, 3) + @test notated == explicit + @test getproperty.(getproperty.(explicit, :connections), :phase) == [1, 2, 3] + @test all(placement -> placement.design === design, explicit) + + moved = at(explicit, 1.0, 2.0; φ = 0.25) + outer = at(1.0, 2.0; φ = 0.25) + @test getproperty.(moved, :pose) == [outer * item.pose for item in explicit] + @test all(placement -> placement.design === design, moved) + @test design.geometry == first(moved).design.geometry + + explicit_connections = (phase = (1, 2, 3),) + @test (@hflat design spacing=0.1 center=(0.2, -1.0) connections=explicit_connections) == + hflat( + design; + spacing = 0.1, + center = at(0.2, -1.0), + connections = explicit_connections + ) + @test (@vflat design spacing=0.1 center=(0.2, -1.0) phase=(1, 2, 3)) == + vflat( + design; + spacing = 0.1, + center = at(0.2, -1.0), + connections = explicit_connections + ) + @test_throws ArgumentError macroexpand( + @__MODULE__, + :(@trefoil design spacing=0.1 connections=explicit_connections phase=(1, 2, 3)) + ) + + placed = @at design (0.0, -1.0) connections = (phase = 1,) + @test placed == at(design, 0.0, -1.0; connections = (phase = 1,)) + @test (@at design (0.0, -1.0) phase=1) == placed + @test (@at design (0.0, -1.0) φ=0.2 connections=(phase = 1,)) == + at(design, 0.0, -1.0; φ = 0.2, connections = (phase = 1,)) + @test_throws ArgumentError macroexpand( + @__MODULE__, + :(@at design (0.0, -1.0, 0.2) φ = 0.3) + ) + @test_throws ArgumentError macroexpand( + @__MODULE__, + :(@at design (0.0, -1.0) connections=(phase = 1,) phase=2) + ) + + system = @system "placed-system" line_length=2.0 begin + @at design (0.0, -1.0) phase=1 + @at design (0.1, -1.0) phase=2 + end + expected_system = build( + LineCableSystem, + [ + at(design, 0.0, -1.0; connections = (phase = 1,)), + at(design, 0.1, -1.0; connections = (phase = 2,)) + ]; + system_id = "placed-system", + line_length = 2.0 + ) + @test system.system_id == expected_system.system_id + @test system.line_length == expected_system.line_length + @test system.designs == expected_system.designs + @test system.positions == expected_system.positions + @test system.connections == expected_system.connections + @test system.terminal_order == expected_system.terminal_order + @test system.terminal_map == expected_system.terminal_map + @test system.connection_order == expected_system.connection_order + + formation_system = @system "formation-system" begin + @hflat design spacing=0.1 phase=(1, 2, 3) + end + @test length(formation_system.designs) == 3 + @test formation_system.connection_order == [1, 2, 3] + + @test_throws ArgumentError macroexpand( + @__MODULE__, + :(@system "invalid" system_id="duplicate" begin + placed + end) + ) + + root_space = terminal( + :phase, + core(copper; r = Grid((2e-3, 3e-3))), + insulation(xlpe; t = 1e-3) + ) + @test root_space isa Gridspace{Group} + designs = build(CableDesign, "grid-cable", root_space) + @test designs isa Gridspace{CableDesign} + @test collect(designs) == [build( + CableDesign, + "grid-cable", + terminal(:phase, core(copper; r), insulation(xlpe; t = 1e-3)) + ) for r in (2e-3, 3e-3)] + @test rand(designs) isa CableDesign + + macro_designs = @cable "macro-grid" begin + @terminal :phase begin + core(copper; r = Grid((2e-3, 3e-3))) + insulation(xlpe; t = 1e-3) + end + end + @test macro_designs isa Gridspace{CableDesign} + @test collect(macro_designs) == [build( + CableDesign, + "macro-grid", + terminal(:phase, core(copper; r), insulation(xlpe; t = 1e-3)) + ) for r in (2e-3, 3e-3)] + + zipped_terminal = @terminal :phase combine=:zip begin + core(copper; r = Grid((2e-3, 3e-3))) + insulation(xlpe; t = Grid((1e-3, 2e-3))) + end + @test zipped_terminal isa Gridspace{Group} + @test length(zipped_terminal) == 2 + + zipped_designs = @cable Grid(("zip-a", "zip-b")) nominal_data=Grid(( + (rating = 1,), + (rating = 2,) + )) combine=:zip begin + zipped_terminal + end + @test zipped_designs isa Gridspace{CableDesign} + @test length(zipped_designs) == 2 + @test getproperty.(collect(zipped_designs), :cable_id) == ["zip-a", "zip-b"] + @test getproperty.(collect(zipped_designs), :nominal_data) == [ + (rating = 1,), + (rating = 2,) + ] + + left_members = terminal(:left, core(copper; r = Grid((1e-3, 1.2e-3)))) + right_members = terminal(:right, core(copper; r = Grid((0.8e-3, 0.9e-3)))) + zipped_assembly = @assembly combine=:zip begin + left_members + right_members + end + @test zipped_assembly isa Gridspace{Assembly} + @test length(zipped_assembly) == 2 + + zipped_formation = @trefoil first(zipped_designs) spacing=Grid(( + 0.08, + 0.1 + )) center=at(x = Grid((0.0, 0.2)), y = -1.0) combine=:zip phase=(1, 2, 3) + @test zipped_formation isa Gridspace{Vector} + @test length(zipped_formation) == 2 + + systems = @system "macro-system" begin + @at macro_designs (0.0, -1.0) phase=1 + end + @test systems isa Gridspace{LineCableSystem} + @test length(systems) == 2 + @test all(system -> system.system_id == "macro-system", systems) + @test all(system -> system.connection_order == [1], systems) + + schedules = Grid(( + LayRatio(10, 11), + LayRatio(12, 13) + )) + strands = stranded( + copper; + shape = Disk(0.5e-3), + lay = schedules, + boundary = Disk(2.5e-3) + ) + @test strands isa Gridspace{Enclosure} + @test length(strands) == 2 + @test stranded( + copper; + shape = Disk(0.5e-3), + lay = LayRatio(10, 11), + boundary = Disk(2.5e-3) + ) isa Enclosure + + repeated_cores = cores( + terminal(:core, core(copper; r = 0.5e-3)); + n = 2, + r = Grid((2e-3, 3e-3)), + names = (:a, :b) + ) + @test repeated_cores isa Gridspace{Assembly} + @test collect(repeated_cores) == [cores( + terminal(:core, core(copper; r = 0.5e-3)); + n = 2, + r, + names = (:a, :b) + ) for r in (2e-3, 3e-3)] + + radii = Grid((2e-3, 3e-3)) + advanced_spaces = ( + Pose2(radii, 0, 0), + Ring(6; r = radii), + Polar(nr = 1, nφ = 6, dr = radii), + Fill(r = radii, φ = pi / 6), + Lattice(nx = 2, ny = 1, dx = radii, dy = 0), + LayRatio(radii), + Pitch(radii), + LayAngle(Grid((0.1, 0.2))), + Helix(LayRatio(10); φ0 = Grid((0.0, 0.1))), + FillFactor(Grid((0.8, 0.9))) + ) + @test all(space -> space isa Gridspace, advanced_spaces) + @test all(==(2), length.(advanced_spaces)) + @test collect(advanced_spaces[2]) == [Ring(6; r) for r in (2e-3, 3e-3)] + @test collect(advanced_spaces[6]) == collect(LayRatio.((2e-3, 3e-3))) + @test collect(advanced_spaces[9]) == [Helix(LayRatio(10); φ0) for φ0 in (0.0, 0.1)] + + local_member = @at terminal(:aux, core(copper; r = 1e-3)) (0.01, 0.02, 0.3) + @test local_member == at( + terminal(:aux, core(copper; r = 1e-3)), + 0.01, + 0.02; + φ = 0.3 + ) + @test local_member.at == DM.Pose2(0.01, 0.02, 0.3) + + @test (@distribute rope( + terminal(:aux, core(copper; r = 0.5e-3)); + layers = 1 + )).items[2].pattern.n == capacity() + + distributed_space = @distribute wires( + copper; + shape = Disk(Grid((0.25e-3, 0.5e-3))), + r = 3e-3 + ) + @test distributed_space isa Gridspace{Group} + resolved_counts = [length(DM.resolve(DM.EmptyBoundary(), group).regions) + for group in distributed_space] + @test resolved_counts == [capacity(Ring, 3e-3, member_radius) + for member_radius in (0.25e-3, 0.5e-3)] +end diff --git a/test/unit/parametricbuilder/formulation_grid.jl b/test/unit/parametricbuilder/formulation_grid.jl new file mode 100644 index 000000000..f0efe0646 --- /dev/null +++ b/test/unit/parametricbuilder/formulation_grid.jl @@ -0,0 +1,231 @@ +@testitem "ParametricBuilder / formulation grids / construction and traversal" tags=[:unit, :uq, :slow] setup=[ + UseEngineSupport,FormulaFixtures, +] begin + internal=FormulaFixtures.SurfaceLaw() + insulation=FormulaFixtures.InsulationReactance(0) + selections=( + internal_impedance = Grid(( + :default, internal)), + insulation_impedance = Grid((:default, + insulation)), + earth_impedance = Grid((:pollaczek1926, :default)), + insulation_admittance = Grid((:lossy, :default)), + semicon_admittance = Grid((:default, :lossy)), + earth_admittance = Grid((:pollaczek1926, :default)), + earth_properties = Grid((nothing, :default)), + temperature_dependence = Grid((nothing, :default)), + pipe_impedance = Grid((:default, formula(:default))), + options = Grid(( + (; ideal_transposition = false), + (; ideal_transposition = true) + )) + ) + for name in keys(selections) + keyword=NamedTuple{(name,)}((getproperty(selections, name),)) + space=Formulation(; keyword...) + @test space isa Gridspace{LineParametersFormulation} + @test length(space) == 2 + @test all(value -> value isa LineParametersFormulation, space) + @test all(space) do value + all( + item -> !(item isa Union{ + AbstractGrid, + Gridspace, + Symbol, + LineCableModels.FormulaDefinition + }), + values(value.methods) + ) && + !(value.options isa Union{AbstractGrid, Gridspace}) + end + end + + product=Formulation( + earth_impedance = Grid((:pollaczek1926, :default)), + earth_admittance = Grid((:pollaczek1926, :default)) + ) + @test length(product) == 4 + @test Set(( + formula_id(value.methods.earth_impedance), + formula_id(value.methods.earth_admittance) + ) for value in product) == Set(( + (:pollaczek1926, :pollaczek1926), + (:unified, :pollaczek1926), + (:pollaczek1926, :unified), + (:unified, :unified) + )) + + zipped=Formulation( + earth_impedance = Grid((:pollaczek1926, :default)), + earth_admittance = Grid((:pollaczek1926, :default)); + combine = :zip + ) + @test length(zipped) == 2 + @test [( + formula_id(value.methods.earth_impedance), + formula_id(value.methods.earth_admittance) + ) for value in zipped] == [ + (:pollaczek1926, :pollaczek1926), + (:unified, :unified) + ] + + broadcast_zip=Formulation( + earth_impedance = Grid((:pollaczek1926, :default)), + earth_admittance = Grid(:pollaczek1926); + combine = :zip + ) + @test length(broadcast_zip) == 2 + @test [formula_id(value.methods.earth_admittance) + for value in broadcast_zip] == fill(:pollaczek1926, 2) + + reductions=Formulation(earth_impedance = Grid(( + formula(:default; equivalent_earth = formula(:default; order = :before)), + formula(:default; equivalent_earth = formula(:default; order = :after))))) + @test length(reductions) == 2 + @test first(reductions).methods.earth_impedance.equivalent_earth isa + LineCableModels.Earth.EquivalentHomogeneous.BeforeFD + @test last(collect(reductions)).methods.earth_impedance.equivalent_earth isa + LineCableModels.Earth.EquivalentHomogeneous.AfterFD + @test all(value -> value.methods.earth_admittance.equivalent_earth === nothing, reductions) + + constants=CableConstantsFormulation( + insulation_admittance = Grid((:default, :lossy)), + ) + @test constants isa Gridspace{CableConstantsFormulation} + @test length(constants) == 2 + + modal=ModalAnalysisFormulation( + Grid((:default, :default)), + ) + @test modal isa Gridspace{ModalAnalysisFormulation} + @test formula_id.(collect(modal)) == [:modal, :modal] + + modal_assumptions=ModalAnalysisFormulation(Grid(( + formula(:default; options = (iteration = (convergence = 1e-4,),)), + formula(:default; options = (iteration = (convergence = 1e-8,),)) + ))) + @test [value.formula.options.data.iteration.convergence for value in modal_assumptions] == + [1e-4, 1e-8] + + fem=LineCableModelsFEM( + options = Grid(( + (; physics = :quasi_tem), + (; physics = :quasi_fw) + )), + ) + @test fem isa Gridspace{LineCableModelsFEM} + @test length(fem) == 2 + + for name in keys(selections) + keyword=NamedTuple{(name,)}((getproperty(selections, name),)) + if name in (:internal_impedance, :insulation_impedance, :earth_impedance, + :earth_admittance, :pipe_impedance) + @test_throws MethodError Formulation(:fem; keyword...) + continue + end + space=Formulation(:fem; keyword...) + @test space isa Gridspace{LineCableModelsFEM} + @test length(space) == 2 + if name!==:options + @test all(value -> haskey(value.definitions, name), space) + end + end + fem_zipped=Formulation(:fem; + insulation_admittance = Grid((:default, :lossy)), + semicon_admittance = Grid((:default, :lossy)), combine = :zip) + @test length(fem_zipped) == 2 + @test [(formula_id(value.methods.insulation_admittance), + formula_id(value.methods.semicon_admittance)) for value in fem_zipped] == + [(:lossless, :lossless), (:lossy, :lossy)] + + struct CountedProblem<:AbstractProblemDefinition + value::Int + end + LineCableModels.validate(problem::CountedProblem)=problem + struct CountedFormulation{ID}<:AbstractFormulation end + struct CountedResult<:AbstractCoreResult + value::Int + end + builds=Ref(0) + make_problem(value)=(builds[]+=1; CountedProblem(value)) + problem_space=Gridspace{CountedProblem}(make_problem, (Grid((1, 2)),)) + formulas=Grid((CountedFormulation{:first}(), CountedFormulation{:second}())) + LineCableModels.compute( + problem::CountedProblem, + ::CountedFormulation{:first}; + options::Union{NamedTuple,ComputationOptions} = ComputationOptions() + )=CountedResult(problem.value) + LineCableModels.compute( + problem::CountedProblem, + ::CountedFormulation{:second}; + options::Union{NamedTuple,ComputationOptions} = ComputationOptions() + )=CountedResult(10problem.value) + + run=compute( + ParametricProblem(problem_space), + Combinatorial(formulas) + ) + @test builds[] == 2 + @test collect(run) == CountedResult.(Int[1, 2, 10, 20]) + @test run[1, 1] == CountedResult(1) + @test run[2, 1] == CountedResult(2) + @test run[1, 2] == CountedResult(10) + @test run[2, 2] == CountedResult(20) + @test run.axes.problems === problem_space + @test run.axes.formulations isa Vector{<:AbstractFormulation} + @test_throws BoundsError run[3, 1] + + # A completed scalar problem is admitted as a singleton without iteration. + scalar=CountedProblem(7) + scalar_problem=ParametricProblem(scalar) + @test length(scalar_problem.space) == 1 + @test first(scalar_problem.space) === scalar + scalar_run=compute(scalar_problem, Combinatorial(formulas)) + @test collect(scalar_run) == CountedResult.(Int[7, 70]) + @test scalar_run[1, 2] == CountedResult(70) + + linear=compute( + ParametricProblem(problem_space), + LinearError(CountedFormulation{:first}()) + ) + @test builds[] == 4 + @test collect(linear) == CountedResult.(Int[1, 2]) + + unindexed=ParametricResult( + Combinatorial(CountedFormulation{:first}()), + CountedResult[CountedResult(1)] + ) + @test isempty(unindexed.axes) + @test_throws ArgumentError unindexed[1, 1] + @test_throws MethodError compute(CountedProblem(1), formulas) + @test_throws ArgumentError Combinatorial(Grid((1, 2))) + @test_throws ArgumentError Combinatorial( + Gridspace{Int}(identity, (Grid((1, 2)),)), + ) + + struct UnsupportedFormulation<:AbstractFormulation end + @test_throws MethodError compute( + CountedProblem(1), + UnsupportedFormulation[UnsupportedFormulation()] + ) + + formulation_builds=Ref(0) + formulation_space=Gridspace{CountedFormulation}( + value -> (formulation_builds[]+=1; value), (formulas,)) + builds[]=0 + automatic=compute(problem_space, formulation_space) + @test builds[] == 2 + @test formulation_builds[] == 2 + @test collect(automatic) == CountedResult.(Int[1, 2, 10, 20]) + @test automatic.axes.problems === problem_space + @test automatic.formulation isa Combinatorial + @test isempty(details(automatic).data) + @test collect(compute(scalar, formulation_space)) == CountedResult.(Int[7, 70]) + @test collect(compute(scalar_problem, formulation_space)) == CountedResult.(Int[7, 70]) + @test_throws MethodError compute(scalar, Gridspace{Int}(identity, (Grid((1, 2)),))) + @test_throws ArgumentError compute(scalar, + Gridspace{CountedFormulation}(identity, (Grid(()),))) + uncertain=Gridspace{CountedFormulation}( + _ -> CountedFormulation{:first}(), (Grid(1.0, AbsoluteError(0.1)),)) + @test_throws ArgumentError compute(scalar, uncertain) +end diff --git a/test/unit/parametricbuilder/gridspace.jl b/test/unit/parametricbuilder/gridspace.jl new file mode 100644 index 000000000..6efaea69a --- /dev/null +++ b/test/unit/parametricbuilder/gridspace.jl @@ -0,0 +1,152 @@ +@testitem "ParametricBuilder / joint input identity and bounded covariance" tags=[:unit, :extension, :measurements] begin + using Measurements, Random, Statistics + source = Grid(1.,10.) + joint = Gridspace{NamedTuple{(:inner,:outer)}}(s->(inner=0.01s,outer=0.012s),(source,)) + p = only(joint) + @test Measurements.cov(p.inner,p.outer) ≈ 0.01*0.01*0.012 + @test uncertainty(p.outer-p.inner) ≈ 0.0002 + a,b = only(Gridspace{Tuple}(tuple,(source,source))) + @test iszero(Measurements.cov(a,b)) + calls = Ref(0) + sampler = (rng,mu,sigma)->(calls[]+=1;mu+sigma*(2rand(rng)-1)) + rand(Xoshiro(2),joint;distribution=sampler) + @test calls[] == 1 + calls[] = 0 + rand(Xoshiro(2),Gridspace{Tuple}(tuple,(source,source));distribution=sampler) + @test calls[] == 2 + for combine in (:product,:zip) + nested = Gridspace{Tuple}(tuple,(joint,Grid((:a,:b)));combine) + @test length(nested) == 2 + @test last.(collect(nested)) == [:a,:b] + @test all(v->uncertainty(first(v).outer/first(v).inner)<1e-14,nested) + end + rng = Xoshiro(42) + draws = [rand(rng,joint;distribution=:uniform) for _ in 1:10_000] + scales = getproperty.(draws,:inner)./0.01 + @test all(s->1-sqrt(3)*0.1 <= s <= 1+sqrt(3)*0.1,scales) + @test all(p->p.outer>p.inner>0,draws) + @test mean(scales) ≈ 1 atol=0.004 + @test std(scales) ≈ 0.1 atol=0.003 + @test mean(scales.^2) ≈ 1.01 atol=0.008 + @test Statistics.cov(getproperty.(draws,:inner),getproperty.(draws,:outer)) ≈ + 0.01*0.01*0.012 rtol=0.06 +end + +@testitem "ParametricBuilder / Material / invariant class and scalar precision" tags=[:unit, :parametric] setup=[ + UseEngineSupport] begin + using Random + import LineCableModels.ParametricBuilder as PB + + material=@inferred PB.Material(kind = :conductor, rho = Float32(1.7e-8)) + @test material isa LineCableModels.Materials.Material{Float32} + @test material.kind === :conductor + + space=PB.Material( + kind = PB.Grid((:conductor, :semicon)), + rho = Float32(1.7e-8) + ) + @test space isa PB.Gridspace{LineCableModels.Materials.Material} + @test eltype(space) === LineCableModels.Materials.Material{Float32} + @test Base.IteratorEltype(typeof(space)) isa Base.HasEltype + @test @inferred(first(space)) isa LineCableModels.Materials.Material{Float32} + @test getproperty.(collect(space), :kind) == [:conductor, :semicon] + @test all(value -> eltype(value) === Float32, space) + sampled=@inferred rand(MersenneTwister(8), space) + @test sampled.kind in (:conductor, :semicon) + @test eltype(sampled) === Float32 + + resistivity_space=PB.Material( + kind = :conductor, + rho = PB.Grid((1.0, 100.0)), + eps_r = 1.0, + mu_r = 1.0, + T0 = 20.0, + alpha = 0.0 + ) + @test eltype(resistivity_space) === + LineCableModels.Materials.Material{Float64} + @test @inferred(first(resistivity_space)) isa + LineCableModels.Materials.Material{Float64} + + mixed_space=PB.Material( + kind = :conductor, + rho = PB.Grid((1.0, 100.0)), + eps_r = PB.Grid(1.0, 5.0), + mu_r = 1.0, + T0 = 20.0, + alpha = 0.0 + ) + @test PB.has_uncertainty(mixed_space) + @test eltype(mixed_space) === Any + @test Base.IteratorEltype(typeof(mixed_space)) isa Base.EltypeUnknown + @test rand(MersenneTwister(9), mixed_space) isa + LineCableModels.Materials.Material{Float64} + + malformed_error=try + PB.Material( + 1.0; + kind = :insulator, + rho = 1.97e14, + eps_r = PB.Grid((2.3,)), + mu_r = 1.0, + T0 = 20.0, + alpha = 0.0 + ) + catch error + error + end + @test malformed_error isa ArgumentError + @test occursin( + "Grid(values, relative_error)", sprint(showerror, malformed_error) + ) +end + +@testitem "ParametricBuilder / Gridspace / recursive point resolution" tags=[:unit, :measurements] setup=[ + UseEngineSupport, TestNumerics] begin + using Random + import LineCableModels.ParametricBuilder as PB + + child=PB.Gridspace{Tuple}(tuple, (PB.Grid(10.0, 5.0), PB.Grid(:tag))) + parent=PB.Gridspace{Tuple}(tuple, (child, PB.Grid((1, 2)))) + point=first(PB.points(parent)) + @test point isa LineCableModels.Gridpoint{Tuple} + @test PB.has_uncertainty(parent) + @test eltype(parent) === Any + @test Base.IteratorEltype(typeof(parent)) isa Base.EltypeUnknown + @test PB.has_uncertainty(point) + @test PB.materialize(point)[1][1] isa Measurement + arguments=LineCableModels.realize_arguments(MersenneTwister(42), point, :normal) + draw=LineCableModels.realize(point, arguments) + @test draw[1][1] isa Float64 + @test draw[1][2] == :tag + @test draw[2] == 1 + + struct MissingGridspaceDefinition end + @test_throws MethodError PB.Gridspace(MissingGridspaceDefinition()) +end + +@testitem "ParametricBuilder / macros / strict and lifted construction" tags=[:unit, :parametric] setup=[ + UseEngineSupport] begin + import LineCableModels.ParametricBuilder as PB + + PB.@gridspace struct MacroVault{T <: Real} + value::T + label::Symbol=:default + end + + @test collect(MacroVault(; value = PB.Grid((1.0, 2.0)), label = :ok)) == [ + MacroVault(1.0, :ok), + MacroVault(2.0, :ok) + ] + scalar=MacroVault(; value = 1.0) + @test scalar isa MacroVault + @test scalar == MacroVault(1.0, :default) + PB.@gridspace struct AtomicCollections{T} + payload::T + label::Symbol=:default + end + matrix=[1.0 2.0; 3.0 4.0] + @test AtomicCollections(; payload = matrix).payload === matrix + varied=AtomicCollections(; payload = matrix, label = PB.Grid((:a, :b))) + @test all(value -> value.payload === matrix, varied) +end diff --git a/test/unit/parametricbuilder/placement_composition.jl b/test/unit/parametricbuilder/placement_composition.jl new file mode 100644 index 000000000..19dd08083 --- /dev/null +++ b/test/unit/parametricbuilder/placement_composition.jl @@ -0,0 +1,153 @@ +@testitem "ParametricBuilder / placement composition preserves geometry and connections" tags=[:unit, :parametric] begin + copper = Material(kind=:conductor, rho=1.72e-8) + part = @terminal :phase begin + core(copper; r=1e-3) + end + design = @cable "placed-wire" begin + part + end + inner = at(0.03, -0.02; φ=0.2) + outer = at(0.4, -1.0; φ=pi / 3) + @test at(inner) === inner + member = at(part, inner) + local_placements = (member, inner) + transformed = @inferred at(local_placements, outer) + @test transformed isa Tuple + @test transformed[1].item === part + @test transformed[1].at == outer * inner + @test transformed[2] == outer * inner + @test at(collect(local_placements), outer) == collect(transformed) + @test member.at == inner + placed_design = at(design, inner; connections=(phase=1,)) + transformed_design = @inferred at((placed_design,), outer) + @test only(transformed_design).design === design + @test only(transformed_design).pose == outer * inner + @test only(transformed_design).connections === placed_design.connections + @test (@inferred at([inner, outer], outer)) == [outer * inner, outer * outer] + + poses = (inner, at(-0.01, 0.02; φ=-0.4)) + tuple_space = Gridspace{Tuple}(pose -> (at(part, pose), pose), (Grid(poses),)) + shifted_tuples = at(tuple_space, outer) + @test shifted_tuples isa Gridspace{Tuple} + @test collect(shifted_tuples) == [at((at(part, pose), pose), outer) for pose in poses] + + connections = (phase=(1, 2, 3),) + family = trefoil(design; spacing=Grid((0.02, 0.04)), connections) + transformed_family = at(family, outer) + @test transformed_family isa Gridspace{Vector} + @test collect(transformed_family) == [at(formation, outer) for formation in family] + offsets = at(Grid((0.1, 0.2)), -1.0) + product = at(family, offsets) + zipped = at(family, offsets; combine=:zip) + @test length(product) == 4 + @test length(zipped) == 2 + @test collect(product) == [at(formation, pose) for pose in offsets for formation in family] + @test collect(zipped) == [at(formation, pose) for (formation, pose) in zip(family, offsets)] + for formation in product + @test all(placed -> placed.design === design, formation) + @test getproperty.(getproperty.(formation, :connections), :phase) == [1, 2, 3] + end + + for formation in (trefoil, hflat, vflat) + named = formation(design; spacing=0.02, connections) + dictionary = formation(design; spacing=0.02, connections=Dict(:phase=>[1, 2, 3])) + individual = formation(design; spacing=0.02, + connections=((phase=1,), (phase=2,), (phase=3,))) + @test getproperty.(dictionary, :pose) == getproperty.(named, :pose) + @test [placed.connections[:phase] for placed in dictionary] == [1, 2, 3] + @test individual == named + for declaration in ((phase=2,), Dict(:phase=>2)) + common = formation(design; spacing=0.02, connections=declaration) + @test [placed.connections[:phase] for placed in common] == [2, 2, 2] + end + for declaration in ((phase=(1, 2),), Dict(:phase=>[1, 2]), ((phase=1,), (phase=2,))) + @test_throws DimensionMismatch formation(design; spacing=0.02, connections=declaration) + end + end +end + +@testitem "ParametricBuilder / placed formations build systems and scalar problems consistently" tags=[:unit, :parametric] begin + copper = Material(kind=:conductor, rho=1.72e-8) + design = @cable "placed-problem" begin + @terminal :core begin + core(copper; r=1e-3) + end + end + soil = homogeneous(rho=100.0) + family = at(hflat(design; spacing=Grid((0.02, 0.04)), + connections=(core=(1, 2, 3),)), Pose2(0.0, -1.0)) + systems = build(LineCableSystem, family; environment=soil, + system_id="placed-system", line_length=250.0) + problems = LineParametersProblem(family; environment=soil, + system_id="placed-system", line_length=250.0, + earth_props=soil, frequencies=[50.0, 1000.0], temperature=40.0) + @test systems isa Gridspace{LineCableSystem} + @test problems isa Gridspace{LineParametersProblem} + @test length(systems) == length(problems) == 2 + for (placed, system, problem) in zip(family, systems, problems) + @test system.positions == getproperty.(placed, :pose) + @test system.environment === soil + @test system.line_length == 250.0 + @test problem.temperature == 40.0 + @test problem.frequencies == [50.0, 1000.0] + @test problem.earth_props === soil + # Grouped placement collections lower to the same physical ordering. + grouped = (placed[1:2], placed[3:3]) + assembled = build(LineCableSystem, grouped; environment=soil, + system_id="placed-system", line_length=250.0) + for built_system in (problem.system, assembled), property in ( + :system_id, :line_length, :designs, :positions, :connections, + :environment, :terminal_order, :terminal_map, :connection_order) + @test getproperty(built_system, property) == getproperty(system, property) + end + direct = LineParametersProblem(assembled; earth_props=soil, + frequencies=problem.frequencies, temperature=problem.temperature) + # Exercise the complete publication path. Its runtime-selected details + # have no single-concrete-return-type guarantee. + actual = compute(problem) + expected = compute(direct) + @test actual.Z.values == expected.Z.values + @test actual.Y.values == expected.Y.values + end + soils = homogeneous(rho=Grid((10.0, 100.0))) + product = LineParametersProblem(systems, soils; frequencies=[50.0]) + zipped = LineParametersProblem(systems, soils; frequencies=[50.0], combine=:zip) + @test length(product) == 4 + @test length(zipped) == 2 + @test [problem.earth_props.layers[2].rho for problem in zipped] == [10.0, 100.0] + @test_throws ArgumentError build(LineCableSystem, ()) + @test_throws ArgumentError build(LineCableSystem, ((),)) + @test_throws ArgumentError build(LineCableSystem, ((design=design, pose=Pose2(0.0, -1.0)),)) +end + +@testitem "ParametricBuilder / placement dispatch keeps target types without evaluating sources" tags=[:unit, :parametric] begin + part = terminal(:core, core(Material(kind=:conductor, rho=1.72e-8); r=0.001)) + design = build(CableDesign, "lazy-placement", part) + calls = Ref(0) + poses = Gridspace{Pose2}(x -> begin + calls[] += 1 + Pose2(x, -1.0) + end, (Grid((0.0, 1.0)),)) + # Lazy sources retain their declared target. The constructor's inferred + # Gridspace type need not fix its eventual iterator element type. + members = at(part, poses) + designs = at(design, poses; connections=(core=1,)) + @test calls[] == 0 + @test members isa Gridspace{LineCableModels.DataModel.AssemblyMember} + @test designs isa Gridspace{NamedTuple} + @test first(members).item === part + @test calls[] == 1 + placed = first(designs) + @test placed.design === design + @test placed.connections === (core=1,) + @test calls[] == 2 + @test (@inferred at(1.0f0, 2.0f0; φ=0.0f0)) isa Pose2{Float32} + @test (@inferred at(part, Pose2(1.0, 2.0))).item === part + @test (@inferred at(design, Pose2(1.0, 2.0); connections=(core=1,))).design === design + @test_throws ArgumentError at(Grid(()), 0.0) + @test_throws ArgumentError at(0.0, Grid(())) + @test_throws ArgumentError at(design, Pose2(0.0, 0.0)) + @test_throws ArgumentError at(part, Pose2(0.0, 0.0); φ=1.0) + @test_throws ArgumentError at(design, Pose2(0.0, 0.0); φ=1.0, connections=(core=1,)) + @test_throws ArgumentError at((Pose2(0.0, 0.0),), Pose2(0.0, 0.0); φ=1.0) +end diff --git a/test/unit/parametricbuilder/region_grid.jl b/test/unit/parametricbuilder/region_grid.jl new file mode 100644 index 000000000..a02d45f28 --- /dev/null +++ b/test/unit/parametricbuilder/region_grid.jl @@ -0,0 +1,81 @@ +@testitem "ParametricBuilder / Region finite inputs preserve targets and product/zip semantics" tags=[:unit, :parametric] begin + const EN = LineCableModels.Engine + tags = (:wire_a, :wire_b) + radii = (1e-3, 2e-3) + resistivities = (1.72e-8, 2.82e-8) + primitives = Disk.(radii) + materials = Material.(Ref(:conductor), resistivities, 1.0, 1.0) + tag_sources = (first(tags), Grid(tags), Gridspace{Symbol}(identity, (Grid(tags),))) + primitive_sources = (first(primitives), Grid(primitives), Disk(Grid(radii))) + material_sources = (first(materials), Grid(materials), + Material(kind=:conductor, rho=Grid(resistivities))) + + for (tag_index, tag) in enumerate(tag_sources), + (primitive_index, primitive) in enumerate(primitive_sources), + (material_index, material) in enumerate(material_sources) + regions = Region(tag, primitive, material) + if (tag_index, primitive_index, material_index) == (1, 1, 1) + @test regions isa Region + @test regions.tag === first(tags) + @test regions.primitive == first(primitives) + @test regions.material == first(materials) + continue + end + @test regions isa Gridspace{Region} + @test Base.IteratorEltype(typeof(regions)) isa Base.HasEltype + @test isconcretetype(eltype(regions)) + @test @inferred(first(regions)) isa eltype(regions) + selected_tags = tag_index == 1 ? tags[1:1] : tags + selected_radii = primitive_index == 1 ? radii[1:1] : radii + selected_rho = material_index == 1 ? resistivities[1:1] : resistivities + expected = vec(collect(Iterators.product(selected_tags, selected_radii, selected_rho))) + actual = [(region.tag, region.primitive.r, region.material.rho) for region in regions] + @test actual == expected + + zipped = Region(tag, primitive, material; combine=:zip) + @test zipped isa Gridspace{Region} + @test length(zipped) == 2 + @test isconcretetype(eltype(zipped)) + @test @inferred(first(zipped)) isa eltype(zipped) + @test [(region.tag, region.primitive.r, region.material.rho) for region in zipped] == + [(tags[tag_index == 1 ? 1 : index], radii[primitive_index == 1 ? 1 : index], + resistivities[material_index == 1 ? 1 : index]) for index in 1:2] + end + + @test_throws DimensionMismatch Region(Grid(tags), Disk(Grid((1e-3, 2e-3, 3e-3))), + first(materials); combine=:zip) + deferred = Region(Grid((:valid, Symbol(""))), first(primitives), first(materials)) + @test deferred isa Gridspace{Region} + @test first(deferred).tag === :valid + @test_throws ArgumentError collect(deferred) + empty_regions = Region(Grid(()), first(primitives), first(materials)) + @test isempty(empty_regions) + @test eltype(empty_regions) === Any + @test Base.IteratorEltype(typeof(empty_regions)) isa Base.EltypeUnknown + + selected = Region(Grid(tags), Disk(Grid(radii)), Grid(materials); combine=:zip) + xlpe = Material(:insulator, Inf, 2.3, 1.0) + designs = @cable "region-grid" begin + @terminal :core begin + selected + insulation(xlpe; t=0.3e-3) + end + @terminal :sheath begin + sheath(first(materials); t=0.1e-3) + end + end + @test designs isa Gridspace{CableDesign} + @test length(designs) == 2 + for (index, design) in enumerate(designs) + @test design isa CableDesign + metal = only(filter(region -> region.source.tag === tags[index], + design.geometry.regions)) + @test metal.source.material.rho == resistivities[index] + @test area(metal.primitive) ≈ pi * radii[index]^2 + blueprint = @inferred EN.flatten(LineCableModelsCoaxial(), design, Float64) + @test length(blueprint.conductors) == 2 + @test first(blueprint.conductors).cross_section ≈ pi * radii[index]^2 + @test first(blueprint.conductors).resistance ≈ + resistivities[index] / (pi * radii[index]^2) + end +end diff --git a/test/unit/parametricbuilder/transport.jl b/test/unit/parametricbuilder/transport.jl new file mode 100644 index 000000000..309afc697 --- /dev/null +++ b/test/unit/parametricbuilder/transport.jl @@ -0,0 +1,72 @@ +@testitem "ParametricBuilder / result transport / owned result spaces" tags=[:unit, :uq] begin + const Commons=LineCableModels.Commons + const PB=LineCableModels.ParametricBuilder + const UQ=LineCableModels.UQ + + struct TransportResult <: Commons.AbstractCoreResult + value::Float64 + end + + struct DownstreamProblem <: Commons.AbstractProblemDefinition + source::TransportResult + end + LineCableModels.validate(problem::DownstreamProblem) = problem + + struct TransportFormulation <: Commons.AbstractFormulation end + + values=TransportResult[TransportResult(1.0), TransportResult(2.0)] + combinatorial=PB.Combinatorial(TransportFormulation()) + source=PB.ParametricResult(combinatorial, values) + transported=PB.Gridspace{DownstreamProblem}(source) + + @test transported.grids === (source,) + @test length(transported) == length(source) + @test eltype(transported) === DownstreamProblem + @test collect(transported) == DownstreamProblem.(values) + + linear=UQ.LinearErrorResult( + UQ.LinearError(TransportFormulation()), + values + ) + uncertain_transport=PB.Gridspace{DownstreamProblem}(linear) + @test uncertain_transport.grids === (linear,) + @test collect(uncertain_transport) == DownstreamProblem.(values) + + unsupported=PB.ParametricResult(TransportFormulation(), values) + error=try + PB.Gridspace{DownstreamProblem}(unsupported) + nothing + catch exception + exception + end + @test error isa ArgumentError + message=sprint(showerror, error) + @test occursin("Gridspace transport from ParametricResult", message) + @test occursin("to problem DownstreamProblem", message) + @test occursin("?Gridspace", message) + @test occursin("electa-git.github.io/LineCableModels.jl/dev/gridspace/", message) + + monte_carlo=UQ.MonteCarloResult( + UQ.MonteCarlo(TransportFormulation(); trials = 1, seed = 1), + TransportResult[TransportResult(1.0)], + [1], + nothing, + nothing, + UInt64(1), + UInt64[1], + [1] + ) + monte_error=try + PB.Gridspace{DownstreamProblem}(monte_carlo) + nothing + catch exception + exception + end + @test monte_error isa ArgumentError + @test occursin("requires a transport for result type", sprint(showerror, + monte_error)) + +end + +# Current result-family transport is exercised above. Retired terminology, +# filenames and exact consumer-method inventories do not define the API. diff --git a/test/unit/parametricbuilder/wire_patterns.jl b/test/unit/parametricbuilder/wire_patterns.jl new file mode 100644 index 000000000..2ffe7426b --- /dev/null +++ b/test/unit/parametricbuilder/wire_patterns.jl @@ -0,0 +1,108 @@ +@testitem "ParametricBuilder / wire patterns / stranded estimates" tags = [:unit, :parametric] begin + const Wires = LineCableModels.ParametricBuilder.WirePatterns + + for awg in (-3, 0, 12, 40) + diameter = Wires.awg_to_d_mm(awg) + area = Wires.awg_to_area_mm2(awg) + @test Wires.d_mm_to_awg(diameter) ≈ awg atol = 64eps(Float64) + @test Wires.area_mm2_to_awg(area) ≈ awg atol = 64eps(Float64) + end + @test Wires.awg_label(-3) == "0000 (4/0)" + @test Wires.awg_label(12) == "12" + + target_area = 1_000.0 + estimate = estimate_stranding(target_area) + @test estimate isa WireEstimate{Float64} + @test estimate.feasible + @test estimate.status === :feasible + @test isempty(estimate.reasons) + for choice in estimate + @test choice.wires == 1 + 3 * choice.layers * (choice.layers - 1) + @test choice.total_area > 0 + @test choice.wire_diameter > 0 + end + match = estimate[:closest_area] + layers = estimate[:fewest_layers] + diameter = estimate[:smallest_diameter] + @test abs(1e6 * match.total_area - target_area) <= + abs(1e6 * diameter.total_area - target_area) + @test layers.layers <= diameter.layers + @test estimate[:fewest_wires].wires == minimum(p.wires for p in estimate) + @test estimate[Val(:closest_area)] === estimate[:closest_area] + + estimate32 = estimate_stranding(Float32(95)) + @test estimate32 isa WireEstimate{Float32} + @test typeof(first(estimate32).total_area) === Float32 + + infeasible = estimate_stranding(1.0e12; awg_min = 40, awg_max = 40) + @test !infeasible.feasible + @test infeasible.status === :infeasible + @test !isempty(infeasible.reasons) + @test infeasible[:closest_area] isa Wires.HexaPattern + + @test_throws DomainError estimate_stranding(0.0) + @test_throws ArgumentError estimate_stranding(10.0; awg_min = 10, awg_max = 9) + @test_throws ArgumentError estimate[:unknown] +end + +@testitem "ParametricBuilder / wire patterns / screened estimates" tags = [:unit, :parametric] begin + const Wires = LineCableModels.ParametricBuilder.WirePatterns + + target_area = 35.0 + lay_diameter = 60.0 + minimum_coverage = 85.0 + estimate = estimate_screen( + target_area, + lay_diameter; + coverage_min = minimum_coverage + ) + + @test estimate isa WireEstimate{Float64} + @test estimate.feasible + for choice in estimate + @test 1e6 * choice.total_area >= target_area + @test minimum_coverage <= choice.coverage <= 100.0 + @test choice.radius == + (choice.lay_diameter + choice.wire_diameter) / 2 + separation = 2 * choice.radius * sinpi(1 / choice.wires) + @test separation >= choice.wire_diameter + end + @test estimate[:fewest_wires].wires <= estimate[:smallest_diameter].wires + @test estimate[:smallest_diameter].wire_diameter <= estimate[:fewest_wires].wire_diameter + + custom = estimate_screen( + target_area, + lay_diameter; + wire_diameters = [1.2], + awg_min = 40, + awg_max = 40, + max_area_overshoot = Inf + ) + @test any(choice -> startswith(choice.awg, "custom"), custom) + @test any(choice -> choice.wire_diameter == 0.0012, custom) + + screen = first(estimate) + @test Wires.maxfill( + Wires.ScreenPattern, + screen.radius, + screen.wire_diameter / 2 + ) >= screen.wires + + infeasible = estimate_screen( + target_area, + lay_diameter; + coverage_min = 99, + coverage_max = 99 + ) + @test !infeasible.feasible + @test !isempty(infeasible.reasons) + @test infeasible[:closest_area] isa Wires.ScreenPattern + + @test_throws DomainError estimate_screen(0.0, lay_diameter) + @test_throws DomainError estimate_screen(target_area, 0.0) + @test_throws DomainError estimate_screen( + target_area, + lay_diameter; + coverage_min = 101.0 + ) +end diff --git a/test/unit/reportbuilder/compact_display.jl b/test/unit/reportbuilder/compact_display.jl new file mode 100644 index 000000000..3f985c414 --- /dev/null +++ b/test/unit/reportbuilder/compact_display.jl @@ -0,0 +1,225 @@ +@testitem "ReportBuilder / compact views preserve values, missing masks and quantities" tags=[:unit, :report] begin + using LineCableModels.ReportBuilder: BenchmarkTableDefinition + using DataFrames + f=[0.1,10.,50.,1e3,1e6,1e7] + reference=LineParameters(fill(1.0+1im,1,1,6),zeros(ComplexF64,1,1,6),f; + details=ComputationDetails(;coordinates=["core"],)) + result=LineParameters(fill(1.2+1im,1,1,6),zeros(ComplexF64,1,1,6),f; + details=ComputationDetails(;coordinates=["core"],)) + artifact=report(BenchmarkTableDefinition((R,G);bands=(:all,:dc,:harmonic,:narrow,:wide)),(;reference,result)) + @test artifact[R]===artifact.tables.quantities[1].Z.R + @test artifact[1,R]===artifact[R] + @test artifact.reference!==nothing + original_id=artifact.observed.gridpoint.id + reference_id=artifact.reference.gridpoint.id + @test all(row -> row.result_id==original_id && row.reference_id==reference_id, + artifact.observed.errors) + before=deepcopy(artifact.tables) + errors=deepcopy(artifact.observed.errors) + for mime in (MIME"text/plain"(),MIME"text/html"()) + text=sprint(show,mime,artifact) + @test occursin("R — Relative RMS [%]",text) + @test occursin("G — Relative RMS [%]",text) + @test occursin("missing",text) + @test occursin("eligible terms only",text) + @test occursin("No controlled performance measurements",text) + @test !occursin("Candidate 1",text) + @test !occursin("configuration 1",text) + @test !occursin("Absolute RMS",text) + @test !occursin(" show(IOContext(io,:limit=>false),MIME"text/html"(), + frame;summary=false,eltypes=false),feature.relative) + mime isa MIME"text/html" && @test occursin(rendered,text) + end + absolute=sprint((io,value) -> show(io,mime,value;metric=:absolute),artifact) + @test occursin("Absolute RMS [Ω/m]",absolute) + @test occursin("Absolute RMS [S/m]",absolute) + @test_throws ArgumentError show(IOBuffer(),mime,artifact;metric=:bogus) + @test_throws ArgumentError show(IOBuffer(),mime,artifact;problem=2) + end + # These compare actual retained values and availability masks, not just table + # shapes: formatting must not clip noise, replace missing or normalize again. + for (prior,after) in zip(before.features,artifact.tables.features) + @test isequal(prior.relative,after.relative) + @test isequal(prior.absolute,after.absolute) + @test propertynames(after.relative)==[:formula,:all,:dc,:harmonic,:narrow,:wide] + end + @test all(ismissing,Matrix(only(filter(f -> f.quantity===:G,artifact.tables.features)).relative[:,2:end])) + @test all(value -> value≈20,Matrix(only(filter(f -> f.quantity===:R,artifact.tables.features)).relative[:,2:end])) + @test isequal(before.terms,artifact.tables.terms) + @test isequal(before.maxima,artifact.tables.maxima) + @test artifact.observed.gridpoint.id==original_id + @test artifact.reference.gridpoint.id==reference_id + @test artifact[R]===artifact.tables.quantities[1].Z.R + for (a,b) in zip(errors,artifact.observed.errors) + @test isequal(a.relative,b.relative) + @test isequal(a.absolute,b.absolute) + @test isequal(a.settings,b.settings) + end + + # Descriptions are user-extensible text. HTML must escape them without using + # rendered text as a scientific key or altering the underlying formula table. + feature=first(artifact.tables.features) + feature.relative.formula[1]="test & option" + html=sprint(show,MIME"text/html"(),artifact) + @test occursin("test <formula> & option",html) + @test !occursin("test ",html) +end + +@testitem "ReportBuilder / ordinary scientific display and bounded previews" tags=[:unit, :measurements, :slow] setup=[TestFixtures] begin + using DataFrames, Measurements, Statistics + const RB=LineCableModels.ReportBuilder + const U=LineCableModels.Units + constants=CableConstants(1e-4,2e-7,3e-10,4e-12;frequency=50) + r=report(constants;values=(R,L,G,C),length_unit=:kilo, + quantity_units=(R=:base,L=:milli,G=:micro,C=:micro)) + original=deepcopy(r.observed.quantities) + originals=map(copy,(r[R],r[L],r[G],r[C])) + normal=IOContext(IOBuffer(),:limit=>true,:displaysize=>(24,80),:color=>false) + for mime in (MIME"text/plain"(),MIME"text/html"()) + rendered=sprint(show,mime,r;context=normal) + for (request,value) in zip((R,L,G,C),("0.1","0.2","0.004","0.3")) + @test occursin(value,rendered) + @test occursin(U.label(metadata(r[request],"unit")),rendered) + end + @test occursin("50.0",rendered) + @test occursin("core",rendered) + @test occursin("Hz",rendered) + @test !occursin("ReportArtifact(",rendered) + @test !occursin("illustration",rendered) + @test !occursin(r"[Cc]andidate|Result 1|Gridpoint 1",rendered) + @test sprint(show,mime,r;context=:compact=>true)==sprint(show,r) + mime isa MIME"text/html" && @test length(collect(eachmatch(r" occursin(label,rendered),labels) + @test !occursin(r"[Cc]andidate|\[1\]|\[2\]",rendered) + end + + z=reshape(complex.(1.:800.,801.:1600.),2,2,200) + source=LineParameters(z,fill(3e-6+4e-6im,2,2,200),collect(1.:200.)) + collection=report([source,source];values=(R,L,G,C),frequency_unit=:kilo) + for size in ((24,80),(40,120),(10,40),(5,30),(2,8),(1,1)) + context=IOContext(IOBuffer(),:limit=>true,:displaysize=>size,:color=>false) + shown=sprint(show,MIME"text/plain"(),collection;context) + @test length(split(shown,'\n'))<=size[1] + @test all(textwidth(line)<=size[2] for line in split(shown,'\n')) + @test !endswith(shown,'\n') + @test !isempty(shown) + end + preview=sprint(show,MIME"text/plain"(),collection;context=normal) + @test occursin("kHz",preview) + @test occursin("omitted",preview) + @test length(collect(eachmatch(r"\b0\.001\b",preview)))==4 + for request in (R,L,G,C) + @test occursin(U.label(metadata(collection[1,request],"quantity")),preview) + end + unlimited=IOContext(IOBuffer(),:limit=>false,:displaysize=>(2,8),:color=>false) + for mime in (MIME"text/plain"(),MIME"text/html"()) + shown=sprint(show,mime,collection;context=unlimited) + @test occursin("800000",shown) || occursin("800000.0",shown) || occursin("800000.",shown) || occursin("800000e",shown) || occursin("8.0e5",shown) + @test occursin("[1,2]",shown) && occursin("[2,1]",shown) + @test occursin("[2]",shown) + @test !occursin(r"[Cc]andidate",shown) + @test !occursin("omitted",shown) + mime isa MIME"text/html" && @test length(collect(eachmatch(r" & sheath",)) + html=sprint(show,MIME"text/html"(),report(labeled)) + @test occursin("Cable <core> & sheath",html) + @test !occursin("Cable ",html) + bare=ReportArtifact(r.observed,nothing,DataFrame(value=[1.,2.]),nothing,nothing) + @test occursin("true,:displaysize=>(12,width),:color=>false) + text=sprint(show,MIME"text/plain"(),wide;context) + @test all(textwidth(line)<=width for line in split(text,'\n')) + @test all(match.match in ("1","3","50",string(large_integer),"77") + for match in eachmatch(r"[0-9]+",text)) + width==72 && @test occursin(string(large_integer),text) + end + for table in (DataFrame(value=Float64[]),DataFrame(value=[missing,missing])) + artifact=ReportArtifact(r.observed,nothing,table,nothing,nothing) + for mime in (MIME"text/plain"(),MIME"text/html"()) + @test !isempty(sprint(show,mime,artifact;context=normal)) + end + end + uq=report(TestFixtures.cable_monte_carlo_result();values=((statistics,R,mean),(statistics,R,std))) + for mime in (MIME"text/plain"(),MIME"text/html"()) + shown=sprint(show,mime,uq;context=normal) + @test occursin("mean",shown) && occursin("std",shown) + end + shared=measurement(1e-4,1e-6) + uncertain=report(CableConstants(shared,2shared,3shared,4shared);values=R,clip=false) + bigsource=LineParameters(fill(complex(-big"1.23456789e-40",big"0"),1,1,2), + fill(complex(big"0",big"1e-6"),1,1,2),BigFloat[50,500]) + precise=report(bigsource;values=R,clip=false) + for artifact in (uncertain,precise) + table=artifact[R] + before=deepcopy(table) + quantities=deepcopy(artifact.observed.quantities) + for mime in (MIME"text/plain"(),MIME"text/html"()) + shown=sprint(show,mime,artifact;context=unlimited) + @test occursin(artifact===uncertain ? "±" : "-",shown) + end + @test isequal(table,before) + @test isequal(artifact.observed.quantities,quantities) + end + @test Base.nonmissingtype(eltype(precise[R][!,2]))===BigFloat +end diff --git a/test/unit/reportbuilder/grammar.jl b/test/unit/reportbuilder/grammar.jl new file mode 100644 index 000000000..5ec260c61 --- /dev/null +++ b/test/unit/reportbuilder/grammar.jl @@ -0,0 +1,393 @@ +@testitem "ReportBuilder / observations and fixed stage order" tags=[:unit, :report] begin + const RB=LineCableModels.ReportBuilder + source=LineParameters(fill(1.0+2im,1,1,2),fill(3.0+4im,1,1,2),[1.,2.]) + observed=ObservedResult(source) + const stages=Symbol[] + struct StageReport <: RB.AbstractReportDefinition end + struct Selected + observed::ObservedResult + end + struct Tabulated + observed::ObservedResult + end + struct Illustrated + tables::Tabulated + end + struct Encoded + illustration::Illustrated + end + RB.select(::StageReport,point::ObservedResult;reference=nothing) = begin + push!(stages,:select); Selected(point) + end + RB.tabulate(::StageReport,point::ObservedResult,selected::Selected;reference=nothing) = begin + @test selected.observed===point + push!(stages,:tabulate); Tabulated(point) + end + RB.illustrate(::StageReport,point,tables::Tabulated;reference=nothing) = begin + @test tables.observed===point + push!(stages,:illustrate); Illustrated(tables) + end + RB.encode(::StageReport,point,tables,illustration::Illustrated;reference=nothing) = begin + @test illustration.tables===tables && tables.observed===point + push!(stages,:encode); Encoded(illustration) + end + RB.write(::StageReport,encoded::Encoded) = begin + push!(stages,:write);encoded + end + artifact=report(StageReport(),observed) + @test stages==[:select,:tabulate,:illustrate,:encode,:write] + empty!(stages) + @test RB.tabulate(StageReport(),observed).observed===observed + @test stages==[:select,:tabulate] + @test artifact.observed===observed + @test artifact.reference===nothing + @test artifact.tables.observed===observed + @test artifact.output.illustration===artifact.illustration + @test_throws TypeError report(TableReportDefinition(),observed;reference=source) + @test_throws MethodError report(StageReport(),source) + illustrated=Ref{Any}(nothing) + definition=TableReportDefinition((R,);illustration=(point;ydata) -> (illustrated[]=point)) + artifact=report(definition,observed) + @test illustrated[]===observed + @test artifact.tables.Z.R[!,2]==[1000.,1000.] + @test RB.tabulate(definition,observed)==artifact.tables + @test report(TableReportDefinition(),source).observed isa ObservedResult +end + +@testitem "ReportBuilder / XLSX numeric quantities and complete matrices" tags=[:integration, :xlsx] begin + using XLSX, Measurements, LinearAlgebra + const RB=LineCableModels.ReportBuilder + impedance=reshape(ComplexF64.(1:8),2,2,2) + impedance[1,2,2]=0.25+0.05im + impedance[2,1,2]=0.5+0.1im + parameters=LineParameters(impedance,fill(3e-6+4e-6im,2,2,2),[50.,500.]) + mktempdir() do directory + artifact=report(XLSXReportDefinition(file_name=joinpath(directory,"full.xlsx")),parameters) + @test length(artifact.output)==4 + @test all(isfile,artifact.output) + resistance=only(filter(path -> endswith(path,"_R.xlsx"),artifact.output)) + XLSX.openxlsx(resistance) do workbook + @test XLSX.sheetnames(workbook)==["values","std","metadata"] + sheet=workbook["values"] + @test sheet["A1"]=="frequency" + @test sheet["C1"]=="[1,2]" + @test sheet["D1"]=="[2,1]" + @test sheet["A2"]==50.0 + @test sheet["C3"]==250.0 + @test sheet["D3"]==500.0 + @test sheet["C3"] isa Number + end + @test length(export_data(:xlsx,parameters;file_name=joinpath(directory,"delegated.xlsx")))==4 + diagonal=LineParameters(cat(Diagonal([1.0+2im,2.0+3im]);dims=3), + cat(Diagonal([3.0+4im,4.0+5im]);dims=3),[50.]) + result=report(XLSXReportDefinition(file_name=joinpath(directory,"diagonal.xlsx")),diagonal) + workbook=XLSX.readxlsx(only(filter(path -> endswith(path,"_R.xlsx"),result.output))) + @test workbook["values"]["C2"]==0 + @test workbook["values"]["D2"]==0 + shared=measurement(1e-4,1e-5) + uncertain=LineParameters(fill(complex(shared,2shared),1,1,2),fill(3e-6+4e-6im,1,1,2),[50.,500.]) + result=report(XLSXReportDefinition(file_name=joinpath(directory,"uncertain.xlsx")),uncertain) + workbook=XLSX.readxlsx(only(filter(path -> endswith(path,"_R.xlsx"),result.output))) + @test workbook["values"]["B2"]≈0.1 + @test workbook["std"]["B2"]≈0.01 + @test workbook["std"]["B2"] isa Number + @test impedance==Z(parameters) + end + definition=XLSXReportDefinition() + @test_throws ArgumentError RB.encode_cell(definition,big"1e400") + @test_throws ArgumentError RB.encode_cell(definition,big"1e-400") + @test RB.encode_cell(definition,1/3)==1/3 + @test ismissing(RB.encode_cell(definition,missing)) + @test Base.ispublic(RB,:XLSXSheet) + @test Base.ispublic(RB,:XLSXWorkbook) +end + +@testitem "ReportBuilder / completed result conveniences use observations" tags=[:unit, :measurements] setup=[TestFixtures] begin + using DataFrames, Measurements, Tables + const RB=LineCableModels.ReportBuilder + constants=CableConstants(1.,2.,3.) + artifact=report(RB.CableConstantsTableDefinition(),constants) + @test artifact.observed isa ObservedResult + @test artifact.tables.constants.R[!,2]==[1000.] + @test !Tables.istable(typeof(artifact.observed)) + @test parentmodule(which(DataFrame,(ObservedResult,)))===RB + mc=TestFixtures.cable_monte_carlo_result() + artifact=report(RB.MonteCarloTableDefinition(),mc) + @test artifact.observed isa Vector{ObservedResult} + @test length(artifact.tables)==length(mc) + @test length(first(artifact.observed).quantities)==8 + @test !Tables.istable(typeof(mc)) + for name in (:TableReportDefinition,:CableConstantsTableDefinition,:LineParametersTableDefinition, + :BenchmarkTableDefinition,:MonteCarloTableDefinition,:XLSXReportDefinition) + @test parentmodule(getproperty(RB,name))===RB + end +end + +@testitem "ReportBuilder / XLSX destinations are preflighted before writes" tags=[:integration, :xlsx] begin + using XLSX + RB=LineCableModels.ReportBuilder + observed=ObservedResult(LineParameters(fill(1.0+2im,1,1,2),fill(3.0+4im,1,1,2),[1.,2.])) + mktempdir() do directory + definition=XLSXReportDefinition(file_name=joinpath(directory,"safe.xlsx")) + books=RB.encode(definition,observed,RB.tabulate(observed),nothing) + open(last(books).destination,"w") do io + write(io,"retained destination") + end + @test_throws r"already exists" RB.write(definition,books) + @test !ispath(first(books).destination) + @test read(last(books).destination,String)=="retained destination" + @test_throws r"already exists" report(definition,observed) + @test length(readdir(directory))==1 + overwrite=XLSXReportDefinition(file_name=definition.file_name,overwrite=true) + written=report(overwrite,observed).output + @test length(written)==4 + @test XLSX.readxlsx(first(written))["values"]["B2"]==1000. + @test length(export_data(:xlsx,observed;file_name=definition.file_name,overwrite=true))==4 + bad=RB.XLSXWorkbook(joinpath(directory,"oversized.xlsx"),[RB.XLSXSheet("values",Matrix{Any}(undef,0,16385))]) + @test_throws DimensionMismatch RB.write(definition,[RB.XLSXWorkbook(joinpath(directory,"first.xlsx"),first(books).sheets),bad]) + @test !ispath(joinpath(directory,"first.xlsx")) + @test !ispath(bad.destination) + @test_throws ArgumentError RB.write(overwrite,[first(books),first(books)]) + end +end + +@testitem "ReportBuilder / source-first quantity tables" tags=[:unit, :measurements, :slow] setup=[TestFixtures] begin + using DataFrames, Statistics, Measurements + using LineCableModels.Commons: observation_product, observation_gridpoint, gridpoint_id + using LineCableModels.Engine: retain_gridpoint + RB=LineCableModels.ReportBuilder + @test LineCableModels.report===RB.report + @test isempty(Test.detect_ambiguities(RB; recursive = true)) + constants=CableConstants(1.0, 2.0, 3.0) + line=TestFixtures.two_conductor_results() + for selection in (nothing, ()) + @test keys(report(constants; values = selection).tables.constants)==(:R, :L, :G, :C) + tables=report(line; values = selection).tables + @test keys(tables.Z)==(:R, :X) + @test keys(tables.Y)==(:G, :B) + end + @test report(constants).tables==report(constants; values = nothing).tables + @test report(line).tables==report(line; values = ()).tables + units=(length_unit = :kilo, + quantity_units = (R = :base, L = :milli, G = :micro, C = :micro)) + requested=(R, L, G, C) + artifact=report(constants; values = requested, units...) + @test artifact.tables.constants.R[!, 2]==[1000.0] + @test keys(artifact.tables)==(:constants,) + @test names(artifact.tables.constants.R)==["frequency", string.(constants.cores)...] + @test artifact.tables.constants.R.frequency==[constants.frequency] + @test report(constants; values = @observe(R[1]), units...).tables.constants.R==artifact.tables.constants.R + @test report(line, requested).tables==report(line; values = requested).tables + @test keys(report(line; values = R).tables.Z)==(:R,) + @test keys(report(line; values = R).tables)==(:Z,) + @test report(line; values = ((Z, real),)).tables==report(line; values = R).tables + @test keys(report(line; values = Z).tables.Z)==(:R, :X) + full=report(line; values = requested) + @test names(full.tables.Z.R)==["frequency", "[1,1]", "[1,2]", "[2,1]", "[2,2]"] + @test full.tables.Z.R[!, 3]≈1000 .* observe(line, R)[1, 2, :] + @test full.tables.Z.R[!, 4]≈1000 .* observe(line, R)[2, 1, :] + @test full.tables.Z.R[!, 3]!=full.tables.Z.R[!, 4] + selected=report(line; values = @observe(R[[2, 1], [2], [3, 1]])) + @test names(selected.tables.Z.R)==["frequency", "[2,2]", "[1,2]"] + @test selected.tables.Z.R.frequency==[1000.0, 10.0] + @test selected.tables.Z.R[!, 2]≈1000 .* observe(line, R)[2, 2, [3, 1]] + @test report(line.Z; values = R, frequencies = frequencies(line)).tables.Z.R==full.tables.Z.R + @test report(line.Y; values = C, frequencies = frequencies(line)).tables.Y.C==full.tables.Y.C + for bad in (:ydata, :rdata, :requests, :quantities) + @test_throws r"values" report(line; NamedTuple{(bad,)}((requested,))...) + end + for source in (line, ObservedResult(line), [ObservedResult(line)]) + @test_throws r"values" report(source, R; values = R) + @test_throws r"values" report(source, R; values = nothing) + @test_throws ArgumentError report(source; frequency_unit = :base, freq_unit = :base) + @test_throws ArgumentError report(source; backend = :cairo) + @test_throws ArgumentError report(source; controls = false) + @test_throws ArgumentError report(source; layout = (1, 1)) + end + @test_throws MethodError report(42) + @test_throws ArgumentError report(line; reference = [line]) + @test_throws ArgumentError report(ObservedResult[]) + source_id=gridpoint_id().source_id + points=[retain_gridpoint(line, gridpoint_id(; source_id, problem_index = i)) + for i in (3, 1)] + for collection in (points, Tuple(points)) + result=report(collection; values = R) + @test getproperty.(getproperty.(result.observed, :gridpoint), + :id)==[observation_gridpoint(point).id for point in points] + @test [point.gridpoint.id.problem_index for point in result.observed]==[3, 1] + @test length(result.tables)==2 + @test result.tables[1].Z.R==result.tables[2].Z.R + end + study=ParametricResult( + nothing, points, (problems = [:one, :two], formulations = [:default]), + ComputationDetails()) + @test report(study; values = R).tables==report(points; values = R).tables + @test [point.gridpoint.id.problem_index for point in report(study).observed]==[3, 1] + @test length(report(points[1:1]).observed)==1 + mixed=report((constants, line)) + @test keys(mixed.tables[1])==(:constants,) + @test keys(mixed.tables[2])==(:Z, :Y) + mc=TestFixtures.cable_monte_carlo_result() + @test length(report(mc).tables)==length(mc) + statistics_report=report(mc; values = @observe((statistics, R, mean)[1])) + @test length(only(statistics_report.observed).quantities)==1 + @test only(statistics_report.tables).statistics.statistics_R_mean[!, 2]≈1000 .* observe( + mc, statistics, R, mean, 1) + @test length(only(report(mc; values = (statistics, R)).observed).quantities)==7 + @test report(mc; values = R).tables[1].constants.R==report(mc).tables[1].constants.R + @test_throws ArgumentError DataFrame(artifact.observed) +end + +@testitem "ReportBuilder / source-first retained units and illustration" tags=[:unit, :report] setup=[TestFixtures] begin + using DataFrames + using LineCableModels.Commons: observation_product + line=TestFixtures.two_conductor_results() + quantities=(R, L, G, C) + unit_options=(length_unit = :base, quantity_units = :base, frequency_unit = :kilo) + observed=ObservedResult(line, quantities; unit_options...) + raw=report(line; values = quantities, unit_options...) + retained=report(observed) + @test retained.observed===observed + @test raw.tables==retained.tables + for selector in quantities + a=observation_product(raw.observed, selector) + b=observation_product(retained.observed, selector) + @test a.values==b.values + @test a.unit==b.unit + @test a.coordinates==b.coordinates + end + @test report(observed; unit_options...).tables==retained.tables + converted=report(observed; length_unit = :kilo, freq_unit = :base) + roundtrip=report(converted.observed; length_unit = :base, frequency_unit = :kilo) + for selector in quantities + a=observation_product(roundtrip.observed, selector) + b=observation_product(observed, selector) + @test a.values≈b.values + @test a.unit==b.unit && a.coordinates==b.coordinates + @test a.available==b.available && a.engineering_zero==b.engineering_zero + end + @test report(observed; values = R).tables.Z.R==retained.tables.Z.R + @test report(observed, R).tables==report(observed; values = R).tables + @test only(report([observed], R).tables).Z.R==retained.tables.Z.R + for source in (observed, [observed], (observed,)), + options in ( + (clip = true,), (clip = nothing,), (atol = nothing,), (frequencies = nothing,)) + + @test_throws ArgumentError report(source; options...) + end + recorded=Ref{Any}(nothing) + renderer=(points; ydata, reference = nothing, + kwargs...)->(recorded[]=(; points, ydata, reference, options = (; kwargs...))) + @test report(line).illustration===nothing + @test report(line).output===nothing + @test_throws ArgumentError report(line; illustration = renderer, unknown = true) + @test_throws ArgumentError report(line; values = R, illustration = renderer, plot_options = (ydata = (L,),)) + @test_throws ArgumentError report(line; plot_options = (backend = :cairo,)) + @test recorded[]===nothing + reference=ObservedResult(line; length_unit = :base) + artifact=report(line; values = R, reference, illustration = renderer, + plot_options = (ydata = (R,), backend = :cairo)) + @test recorded[].points===artifact.observed + @test recorded[].reference===reference + @test recorded[].ydata==((R, :, :, :),) + @test recorded[].options==(backend = :cairo,) + @test artifact.reference===reference + @test isempty(artifact.observed.errors) + @test isempty(artifact.reference.errors) + @test observation_product(reference, R).unit!=observation_product(artifact.observed, R).unit + converted_reference=report(line; values = R, reference, length_unit = :kilo) + @test observation_product(converted_reference.reference, + R).unit==observation_product(converted_reference.observed, R).unit + @test observation_product(reference, R).unit!=observation_product(converted_reference.reference, R).unit + @test report(line; values = R, reference = line).reference isa ObservedResult + report(observed; values = R, illustration = renderer, length_unit = :kilo) + @test recorded[].points isa ObservedResult + @test recorded[].ydata==(R,) + report(observed; illustration = renderer) + @test recorded[].ydata==quantities + @test raw.tables==retained.tables + @test report(TableReportDefinition(quantities), line).tables==report(line; values = quantities).tables + @test report(LineCableModels.ReportBuilder.LineParametersTableDefinition(quantities), + line).tables==report(line; values = quantities).tables +end + +@testitem "ReportBuilder / direct reported quantity access" tags=[:unit, :measurements] setup=[TestFixtures] begin + using DataFrames, Statistics, Measurements + using LineCableModels.Commons: gridpoint_id + const RB=LineCableModels.ReportBuilder + constants=CableConstants(1e-4,2e-7,3e-10,4e-12;frequency=50) + r=report(constants;values=(R,L,G,C),length_unit=:kilo, + quantity_units=(R=:base,L=:milli,G=:micro,C=:micro)) + for (request,expected) in zip((R,L,G,C),(0.1,0.2,0.004,0.3)) + table=r[request] + @test table isa DataFrame + @test table===getproperty(r.tables.constants,nameof(request)) + @test r[1,request]===table + @test r[Int32(1),request]===table + @test r[big(1),request]===table + @test size(table)==(1,2) + @test table.frequency==[50.] + @test table[1,2]≈expected + @test all(T -> T<:Number,eltype.(eachcol(table))) + @test metadata(table,"request")===request + @test metadata(table,"statistic")===:value + end + @test_throws BoundsError r[0,R] + @test_throws BoundsError r[2,R] + @test_throws ArgumentError r[true,R] + @test_throws ArgumentError r[:R] + @test_throws ArgumentError r["R"] + @test_throws ArgumentError r[@observe(R[1])] + + selected=report(constants;values=@observe(R[1])) + @test selected[R]===selected[@observe(R[1])] + @test_throws ArgumentError selected[L] + @test_throws ArgumentError selected[@observe(R[:])] + @test_throws ArgumentError selected[@observe(R[2])] + + z=reshape(complex.(1.:8.,9.:16.),2,2,2) + source=LineParameters(z,fill(3e-6+4e-6im,2,2,2),[50.,500.]) + full=report(source;values=(R,L,G,C)) + @test propertynames(full[R])==[:frequency,Symbol("[1,1]"),Symbol("[1,2]"),Symbol("[2,1]"),Symbol("[2,2]")] + @test full[R][!,3]!=full[R][!,4] + partial=report(source;values=@observe(R[2,1,2:2])) + @test partial[R]===partial[@observe(R[2,1,2:2])] + @test partial[R].frequency==[500.] + @test metadata(partial[R],"coordinates").rows==[2] + @test_throws ArgumentError partial[L] + @test_throws ArgumentError partial[@observe(R[1,1,2:2])] + + points=[ObservedResult(constants;gridpoint=(id=gridpoint_id(problem_index=i),)) for i in (9,3)] + collection=report(points;values=(R,L)) + tables=collection[R] + @test tables isa Vector{DataFrame} + @test all(tables[i]===collection[i,R] for i in 1:2) + @test [metadata(table,"gridpoint_id").problem_index for table in tables]==[9,3] + @test report(points[1:1];values=R)[R] isa Vector{DataFrame} + @test report((constants,);values=R)[R] isa Vector{DataFrame} + @test_throws BoundsError collection[9,R] + incomplete=ReportArtifact(points,nothing,[collection.tables[1],(;L=collection[2,L])],nothing,nothing) + @test_throws r"reported result 2.*problem_index = 3.*available reported requests" incomplete[R] + diagnostic=try incomplete[R]; catch error; sprint(showerror,error); end + @test occursin(repr(points[2].gridpoint.id),diagnostic) + @test !occursin("candidate",diagnostic) + ambiguous=ReportArtifact(r.observed,nothing,(a=r[R],b=copy(r[R])),nothing,nothing) + @test_throws r"ambiguous.*reported result 1" ambiguous[R] + undescribed=ReportArtifact(r.observed,nothing,DataFrame(R=[1.]),nothing,nothing) + @test_throws r"absent quantity descriptors" undescribed[R] + missing_table=copy(r[R]); allowmissing!(missing_table); missing_table[1,2]=missing + missing_report=ReportArtifact(r.observed,nothing,missing_table,nothing,nothing) + @test missing_report[R]===missing_table + + uq=report(TestFixtures.cable_monte_carlo_result();values=((statistics,R,mean),(statistics,R,std))) + @test uq[1,(statistics,R,mean)]!==uq[1,(statistics,R,std)] + @test metadata(uq[1,(statistics,R,mean)],"statistic")===:mean + @test_throws ArgumentError uq[R] + @test_throws ArgumentError uq[(statistics,L,mean)] + + original=deepcopy(r.observed.quantities) + r[R][1,2]=42. + @test isequal(r.observed.quantities,original) + @test observe(constants,R)==[1e-4] + @test r[R][1,2]==42. +end diff --git a/test/unit/reportbuilder/histogram_observations.jl b/test/unit/reportbuilder/histogram_observations.jl new file mode 100644 index 000000000..2725d757d --- /dev/null +++ b/test/unit/reportbuilder/histogram_observations.jl @@ -0,0 +1,76 @@ +@testitem "UQ / histogram observations / explicit binning and retention" tags=[:unit, :measurements] setup=[TestFixtures] begin + using Measurements + cable = TestFixtures.cable_monte_carlo_result() + frequency = [50.0, 1000.0] + scale = reshape(collect(1.0:8.0), 2, 2, 2) + sample_arrays = (R=cat((scale .* trial .* 1e-4 for trial in 1:4)...; dims=4), + L=cat((scale .* trial .* 1e-7 for trial in 1:4)...; dims=4), + C=cat((scale .* trial .* 1e-10 for trial in 1:4)...; dims=4), + G=cat((scale .* trial .* 1e-9 for trial in 1:4)...; dims=4)) + summaries = map(values -> map(CartesianIndices(scale)) do index + SampleSummary(values[Tuple(index)..., :]) + end, sample_arrays) + models = map(values -> map(CartesianIndices(scale)) do index + HistogramDensity(values[Tuple(index)..., :]; bins=2) + end, sample_arrays) + omega = reshape(2pi .* frequency, 1, 1, :) + line = LineParameters(2.5e-4 .* scale .+ im .* omega .* 2.5e-7 .* scale, + 2.5e-9 .* scale .+ im .* omega .* 2.5e-10 .* scale, frequency) + line = LineCableModels.materialize(line,summaries) + matrix = MonteCarloResult(cable.formulation, [line], [summaries], [sample_arrays], + [models], cable.root_seed, cable.point_seeds, cable.trial_counts) + + for (source, indices) in ((cable, (1,)), (matrix, (2, 1, 2))) + samples_only = MonteCarloResult(source.formulation, source.values, source.stats, + source.sample_values, nothing, source.root_seed, source.point_seeds, + source.trial_counts) + model_only = MonteCarloResult(source.formulation, source.values, source.stats, + nothing, source.histogram_values, source.root_seed, source.point_seeds, + source.trial_counts) + summaries_only = MonteCarloResult(source.formulation, source.values, source.stats, + nothing, nothing, source.root_seed, source.point_seeds, source.trial_counts) + for selector in (R, L, C, G) + retained = observe(source, histograms, selector, 1, indices...) + sample = observe(source, samples, selector, 1, indices..., :) + original_edges, original_density = copy(retained.edges), copy(retained.density) + @test @inferred(observe(source, histograms, selector, 1, indices..., nothing)) === retained + @test observe(model_only, histograms, selector, 1, indices..., nothing) === retained + @test observe(model_only, histograms, selector, 1, indices..., 2) === retained + for input in (source, samples_only), bins in (1, 3, 4) + # Rebuilding can promote precision to represent finite support + # and density. Retained-model retrieval above stays inferred. + model = observe(input, histograms, selector, 1, indices..., bins) + expected = HistogramDensity(sample; bins) + @test length(model.density) == bins + @test model.edges == expected.edges + @test model.density == expected.density + @test sum(model.density .* diff(model.edges)) ≈ 1.0 + end + automatic = observe(samples_only, histograms, selector, 1, indices..., nothing) + @test automatic.edges == HistogramDensity(sample).edges + @test automatic.density == HistogramDensity(sample).density + @test_throws r"return_samples=true" observe(model_only, histograms, selector, 1, indices..., 3) + @test_throws ArgumentError observe(summaries_only, histograms, selector, 1, indices..., nothing) + for input in (source, model_only, samples_only), bins in (0, -1) + @test_throws ArgumentError observe(input, histograms, selector, 1, indices..., bins) + end + @test retained.edges == original_edges + @test retained.density == original_density + @test observe(source, samples, selector, 1, indices..., :) == sample + end + end + + constant_samples = map(_ -> fill(2.0, 1, 4), only(cable.sample_values)) + constant_stats = map(_ -> [SampleSummary(fill(2.0, 4))], only(cable.stats)) + constant = MonteCarloResult(cable.formulation, + [LineCableModels.materialize(CableConstants(2.0, 2.0, 2.0, 2.0),constant_stats)], + [constant_stats], [constant_samples], nothing, cable.root_seed, + cable.point_seeds, cable.trial_counts) + for bins in (nothing, 1, 4) + model = observe(constant, histograms, R, 1, 1, bins) + @test length(model.density) == 1 + @test only(diff(model.edges)) > 0 + @test first(model.edges) < 2 < last(model.edges) + @test sum(model.density .* diff(model.edges)) ≈ 1.0 + end +end diff --git a/test/unit/reportbuilder/performance.jl b/test/unit/reportbuilder/performance.jl new file mode 100644 index 000000000..5e3a4bb2f --- /dev/null +++ b/test/unit/reportbuilder/performance.jl @@ -0,0 +1,86 @@ +@testitem "ReportBuilder / recorded performance has explicit scopes and no collection" tags=[:unit, :report] begin + using LineCableModels.ReportBuilder: BenchmarkTableDefinition, tabulate + using DataFrames + definition=BenchmarkTableDefinition() + @test all(isempty, values(LineCableModels.ReportBuilder._timing_tables(nothing))) + source=(backend = :pscad, scope = :compile_call, seconds = 0.0328, + excludes = (:output_readiness, :transfer), reused = false) + execution=( + reference = (backend = :pscad, + timing = (scope = :compute_call_wall, seconds = 12.0, source_timings = source), + reused = true, execution_wall_seconds = 0.01), + result = (backend = :fem, + timing = (scope = :compute_call_wall, seconds = 4.0, + source_timings = (backend = :getdp, scope = :worker_sum, + solve_seconds = 7.0, recovered_columns = 2)))) + measure=(scope = :compute_call_wall, median_seconds = 10.0, bytes = 0, samples = 1, + observations = [( + seconds = 10.0, bytes = 0, reused = false, source_timings = source)], + environment = (threads = 1, workers = 2, instrumented = false), + calculation = (input = "reference workload", trials = 512)) + performance=(reference = measure, + result = merge( + measure, (median_seconds = 1.0, + calculation = (input = "result workload", trials = 0))), + speedup = 10.0, comparable = false, + settings = (samples = 3, seconds = 20.0)) + tables=LineCableModels.ReportBuilder._timing_tables( + (execution = execution, performance = performance, + checksum_verified=missing,workload_verified=true,session=(id="original",))) + @test tables.execution.reused[1]===true + @test tables.execution.execution_wall_seconds[1]==0.01 + @test tables.source_timings.scope==[:compile_call, :worker_sum] + @test tables.source_timings.seconds[1]==0.0328 + @test tables.source_timings.solve_seconds[2]==7.0 + @test tables.performance.samples==[1, 1] + @test all(iszero, tables.performance.allocated_bytes) + @test :allocation_statistic ∉ propertynames(tables.performance) + @test :allocation_scope ∉ propertynames(tables.performance) + @test :performance_policy ∉ keys(tables) + @test all(ismissing,tables.performance.checksum_verified) + @test all(==(true),tables.performance.workload_verified) + @test tables.performance.session_id==["original","original"] + @test only(tables.performance_comparison.reference_over_result)==10.0 + @test !only(tables.performance_comparison.comparable) + @test propertynames(tables.performance_comparison)== + [:reference_over_result,:comparable,:requested_samples,:time_budget_seconds] + @test only(tables.performance_comparison.requested_samples)==3 + @test size(tables.performance_samples, 1)==2 + # Counts retain their measurement meaning: timed repetitions, not MC trials. + @test tables.performance.requested_samples==[3,3] + for table in values(tables),column in eachcol(table),value in column + @test value isa Union{Number,Bool,Symbol,AbstractString,Missing} + end + + parameters=LineParameters(reshape(ComplexF64[1,2],1,1,2), + reshape(ComplexF64[1im,2im],1,1,2),[1.,100.];details=ComputationDetails(;coordinates=["core"],)) + artifact=report(BenchmarkTableDefinition((R,);bands=(:all,:dc)), + (reference=parameters,result=parameters, + measurements=(;execution,performance))) + overview=artifact.tables.overview + @test overview.performance.median_seconds==[10.,1.] + @test overview.performance.samples==[1,1] + @test overview.execution.seconds==[12.,4.] + @test overview.execution.reused[1]===true + @test only(overview.timing_ratio.reference_over_result)==10. + @test !only(overview.timing_ratio.comparable) + @test overview.coverage.band==[:all,:dc] + @test overview.coverage.frequency_count==[2,2] + # Rendering must not substitute tiny PSCAD compile times or accumulated FEM + # worker times for controlled whole-call measurements, or claim a speedup. + for mime in (MIME"text/plain"(),MIME"text/html"()) + displayed=sprint(show,mime,artifact) + @test occursin("Controlled computation measurements",displayed) + @test occursin("not a validated speedup",displayed) + @test occursin("only one timed call",displayed) + @test occursin("not peak memory",displayed) + @test !occursin("compile_call",displayed) + @test !occursin("worker_sum",displayed) + @test !occursin("input_sha",displayed) + @test !occursin("session_id",displayed) + @test occursin("worker_sum",sprint((io,value) -> show(io,mime,value;native_timings=true),artifact)) + end + for frame in values(overview),column in eachcol(frame),value in column + @test value isa Union{Number,Bool,Symbol,AbstractString,Missing} + end +end diff --git a/test/unit/reportbuilder/uncertainty_publication.jl b/test/unit/reportbuilder/uncertainty_publication.jl new file mode 100644 index 000000000..76c752619 --- /dev/null +++ b/test/unit/reportbuilder/uncertainty_publication.jl @@ -0,0 +1,113 @@ +@testitem "UQ / publication / selected matrix coordinates retain axes and units" tags=[:unit, :measurements, :slow] begin + using DataFrames + using Statistics + using Measurements + const UQ = LineCableModels.UQ + const U = LineCableModels.Units + frequencies = [50.0, 1000.0] + positions = reshape(collect(1.0:8.0), 2, 2, 2) + quantities = (R=positions .* 1e-4, L=positions .* 1e-7, + C=positions .* 1e-10, G=positions .* 1e-9) + omega = reshape(2pi .* frequencies, 1, 1, :) + parameters = LineParameters(quantities.R .+ im .* omega .* quantities.L, + quantities.G .+ im .* omega .* quantities.C, frequencies) + summaries = map(quantities) do values + map(value -> UQ.SampleSummary([0.5value, 1.5value]), values) + end + sampled_parameters = LineCableModels.materialize(parameters,summaries) + source = MonteCarloResult(MonteCarlo(Formulation(); trials=2, seed=7), + [sampled_parameters], [summaries], nothing, nothing, UInt64(7), UInt64[11], [2]) + RB=LineCableModels.ReportBuilder + for indices in ((),(:,:,:),(2,:,:),(:,1,:),(:,:,2),(2,1,2),([2,1],[2],[2,1])) + selected=isempty(indices) ? ([1,2],[1,2],[1,2]) : + map(index -> index isa Colon ? [1,2] : index isa Integer ? [index] : collect(index),indices) + observed=ObservedResult(source,1,((statistics,R,indices...),(statistics,C,indices...)); + frequency_unit=:kilo,length_unit=:base,quantity_units=Dict((statistics,C)=>:nano)) + @test length(observed.quantities)==14 + for q in observed.quantities + @test q.coordinates.rows==selected[1] + @test q.coordinates.columns==selected[2] + @test q.coordinates.samples==selected[3] + @test q.coordinates.frequencies==frequencies[selected[3]]./1000 + @test U.label(q.coordinates.frequency_unit)=="kHz" + selector=LineCableModels.Commons.request_identity(q.request)[2] + transform=LineCableModels.Commons.request_identity(q.request)[3] + expected=observe(source,statistics,selector,transform,1,indices...) + factor=selector===C ? 1e9 : 1. + @test q.values≈factor.*expected + table=RB.tabulate(observed,q.request) + @test size(table)==(length(selected[3]),1+length(selected[1])*length(selected[2])) + table[1,2]=-1. + @test first(source.stats[1].R).mean==1e-4 + end + @test observed.gridpoint.sampling.trials==2 + @test observed.gridpoint.sampling.point_seed==UInt64(11) + end + request=@observe (statistics,R,mean)[:,:,:] + capacitance_request=@observe (statistics,C,mean)[[2,1],[2],[2,1]] + overridden=ObservedResult(source,1,(capacitance_request,);length_unit=:base, + quantity_units=Dict(capacitance_request=>:nano,(statistics,C)=>:pico)) + @test only(overridden.quantities).values≈quantities.C[[2,1],[2],[2,1]].*1e9 + explicit=ObservedResult(source,1,(capacitance_request,);length_unit=:base, + units=(:pico,),quantity_units=Dict(capacitance_request=>:nano)) + @test only(explicit.quantities).values≈quantities.C[[2,1],[2],[2,1]].*1e12 + @test only(explicit.quantities).coordinates==only(overridden.quantities).coordinates + @test request==(statistics,R,mean,Colon(),Colon(),Colon()) + @test (@observe source (statistics,R,mean)[1,:,:,:])==observe(source,statistics,R,mean,1,:,:,:) + @test (@observe parameters (Y,abs)[:,:,:])==observe(parameters,Y,abs,:,:,:) + selected=ObservedResult(source,1,(request,(statistics,R,std));length_unit=:base) + @test observe(selected,statistics,R,mean)==quantities.R + @test observe(selected,statistics,R,std)==std.(summaries.R) + @test_throws ArgumentError observe(selected,statistics,X,mean) + for (base,derived) in ((L,X),(C,B)), transform in (mean,std) + @test observe(source,statistics,derived,transform,1)≈omega.*observe(source,statistics,base,transform,1) + acquired=ObservedResult(source,1,((statistics,derived,transform),);length_unit=:base,quantity_units=:base) + @test observe(acquired,statistics,derived,transform)≈observe(source,statistics,derived,transform,1) + end + q05=Base.Fix2(quantile,.05) + @test observe(source,statistics,R,q05,1)==getproperty.(summaries.R,:q05) + @test_throws ArgumentError quantile(first(summaries.R),.1) + @test only(source)===sampled_parameters + + uncertain_z = measurement.(real.(parameters.Z), 1e-6) .+ + im .* measurement.(imag.(parameters.Z), 2e-6) + lep_core = LineParameters(uncertain_z, parameters.Y, frequencies) + lep = LinearErrorResult(LinearError(Formulation()), [lep_core]) + @test only(lep) === lep_core + @test observe(lep, statistics, R, mean, 1) == quantities.R + @test observe(lep, statistics, R, std, 1) == fill(1e-6, 2, 2, 2) + @test observe(lep, statistics, Z, std, 1) ≈ fill(hypot(1e-6, 2e-6), 2, 2, 2) + lep_observed=ObservedResult(lep,1,(request,(statistics,R,std));length_unit=:base) + @test observe(lep_observed,statistics,R,mean)==quantities.R + @test observe(lep_observed,statistics,R,std)==fill(1e-6,2,2,2) + @test_throws ArgumentError ObservedResult(lep,1,((statistics,R,median),)) + IE=LineCableModels.ImportExport + restored_mc=IE.deserialize_value(IE.serialize_value(source)) + @test observe(restored_mc,statistics,R,mean,1) == observe(source,statistics,R,mean,1) + @test NamedTuple(first(only(UQ.statistics(restored_mc)).R)) == NamedTuple(first(summaries.R)) + @test UQ.confidence(restored_mc,1).trials == 2 + restored_lep=IE.deserialize_value(IE.serialize_value(lep)) + @test observe(restored_lep,statistics,R,std,1) == observe(lep,statistics,R,std,1) + shared=measurement(1.0,0.1) + correlated=LineParameters(fill(shared+im*shared,2,2,2),fill(2shared+im*shared,2,2,2),frequencies) + restored=IE.deserialize_value(IE.serialize_value(LinearErrorResult(LinearError(Formulation()),[correlated]))) + @test uncertainty(real(only(restored).Z[1])-real(only(restored).Z[2])) == 0 + @test uncertainty(real(only(restored).Y[1])-2real(only(restored).Z[1])) == 0 + + definition=RB.BenchmarkTableDefinition(((statistics,R,mean),(statistics,R,std),(statistics,B,mean));bands=(:all,)) + artifact=RB.report(definition,(reference=source,result=source)) + @test length(artifact.tables.features)==3 + @test names(first(artifact.tables.features).relative)==["formula","all"] + @test size(artifact.tables.sampling,1)==2 + @test all(iszero,skipmissing(artifact.tables.terms.relative_rms_percent)) + for row in eachrow(artifact.tables.mean_sampling_precision) + selector=getproperty(LineCableModels,row.quantity) + expected=observe(source,statistics,selector,std,1) + @test row.standard_error≈expected[row.index...]/sqrt(2) + @test row.row==row.index[1] && row.column==row.index[2] + @test row.frequency_Hz==frequencies[row.index[3]] + @test row.unit==LineCableModels.Units.label(LineCableModels.Units.native_unit( + LineCableModels.Units.quantity(selector),:pul)) + end + @test all(==(:empirical),[point.gridpoint.uncertainty.estimator for point in artifact.observed]) +end diff --git a/test/unit/reportbuilder/uq_comparisons.jl b/test/unit/reportbuilder/uq_comparisons.jl new file mode 100644 index 000000000..7e5a8aa85 --- /dev/null +++ b/test/unit/reportbuilder/uq_comparisons.jl @@ -0,0 +1,135 @@ +@testitem "UQ / scalar products preserve populations, units and eligibility" tags=[:unit, :measurements, :slow] begin + using Statistics, Measurements, DataFrames, Random + using LineCableModels.Engine: compare, absolute_error, relative_error + using LineCableModels.ReportBuilder: BenchmarkTableDefinition + UQ=LineCableModels.UQ + f=[0.1, 50.0, 100.0, 1000.0, 1e6, 1e7] + angular=reshape(2pi .* f, 1, 1, :) + rng=Xoshiro(12) + draws=randn(rng, 2, 2, length(f), 17) + trials=(R = 1.0 .+ 0.1draws, L = 1e-3 .+ 1e-4draws, + C = 1e-9 .+ 1e-10draws, G = 1e-6 .+ 1e-7draws) + summaries=map(trials) do array + [SampleSummary(vec(array[i, j, k, :])) for i in 1:2, j in 1:2, k in eachindex(f)] + end + means=map(array -> mean.(array), summaries) + core=LineParameters( + means.R .+ im .* angular .* means.L, means.G .+ im .* angular .* means.C, f; + details = ComputationDetails(;coordinates = ["a", "b"],)) + core=LineCableModels.materialize(core,summaries) + histograms=map(trials) do array + [HistogramDensity(vec(array[i, j, k, :]); bins = 3) + for i in 1:2, j in 1:2, k in eachindex(f)] + end + mc=MonteCarloResult( + MonteCarlo(Formulation(); trials = 17, seed = 12, + return_samples = true, return_histograms = true), + [core], [summaries], [trials], [histograms], UInt64(12), UInt64[13], [17]) + expected=dropdims(std(trials.R .+ im .* reshape(angular, 1, 1, :, 1) .* trials.L; dims = 4); dims = 4) + @test observe(mc, statistics, Z, std, 1)≈expected + for stat in (mean, std, minimum, median, maximum, + Base.Fix2(quantile, 0.05), Base.Fix2(quantile, 0.95)) + @test observe( + mc, statistics, X, stat, 1)≈angular .* observe(mc, statistics, L, stat, 1) + @test observe( + mc, statistics, B, stat, 1)≈angular .* observe(mc, statistics, C, stat, 1) + end + retained=ObservedResult(mc,1,((samples,R),(LineCableModels.histograms,R,1,1,1)); + length_unit=:base) + @test length(retained.quantities)==2 + @test first(retained.quantities).values==trials.R + @test only(last(retained.quantities).coordinates.rows)==1 + @test only(mc)===core + @test eachindex(mc)==Base.OneTo(1) + restored=LineCableModels.ImportExport.deserialize_value(LineCableModels.ImportExport.serialize_value(mc)) + @test observe(restored, samples, R, 1)==trials.R + @test NamedTuple(observe( + restored, LineCableModels.histograms, R, 1, 1, 1, 1))==NamedTuple(histograms.R[1]) + @test observe(restored, statistics, C, Base.Fix2(quantile, 0.95), + 1)==observe(mc, statistics, C, Base.Fix2(quantile, 0.95), 1) + @test UQ.confidence(restored, 1).mean_standard_error.R≈std.(summaries.R) ./ sqrt(17) + @test UQ.confidence(restored, 1).spread_estimated + retained=ObservedResult(mc,1,((statistics,R,mean),);frequency_unit=:kilo,length_unit=:kilo) + @test first(retained.quantities).coordinates.frequencies≈f./1000 + @test observe(retained,statistics,R,mean)≈1000means.R + physical_report=report(BenchmarkTableDefinition(((statistics,L,std),(statistics,C,mean))), + (reference=mc,result=mc);observation_options=(length_unit=:base,quantity_units=:base)) + @test length(physical_report.tables.features)==2 + @test observe(only(physical_report.observed),statistics,L,std)≈std.(summaries.L) + @test observe(only(physical_report.observed),statistics,C,mean)≈mean.(summaries.C) + @test all(iszero,skipmissing(physical_report.tables.terms.relative_rms_percent)) + + two=MonteCarloResult( + mc.formulation, [core, core], [summaries, summaries], nothing, nothing, + UInt64(12), UInt64[13, 14], [17, 17]) + @test_throws ArgumentError compare(two, two, (statistics, R, mean)) + @test_throws ArgumentError compare(two, two, (statistics, R, mean); pairing = ( + (1, 1), (2, 1))) + errors=compare(two, two, (statistics, R, mean); pairing = ((1, 2), (2, 1))) + @test length(errors)==2 && + all(error -> all(iszero, observe(error, absolute_error)), errors) + @test_throws ArgumentError report(BenchmarkTableDefinition(((statistics,R,mean),); + pairing=((1,2),(2,1))),(reference=two,result=two)) + percentiles=report(BenchmarkTableDefinition(((statistics,R,median),(statistics,R,Base.Fix2(quantile,.05)))), + (reference=mc,result=mc)) + @test length(percentiles.tables.features)==2 + @test Set(percentiles.tables.terms.statistic)==Set((:median,Symbol("quantile_0.05"))) + @test Set(percentiles.tables.statistics.statistic)==Set((:median,Symbol("quantile_0.05"))) + + for T in (Float32, Float64, BigFloat), quantity in (R, B) + + frequency=T.(f) + a=fill(T(1), 2, 2, length(f)); + b=copy(a) + b[1, 2, 2]=T(1e-30) + error=compare(a, b, quantity; frequencies = frequency, result_basis = :pul) + @test ismissing(observe(error, relative_error)[1, 2]) + @test ismissing(observe(error,absolute_error)[1,2]) + @test eltype(observe(error, absolute_error))===Union{Missing, T} + @test all(ismissing, + observe( + compare(a, b, quantity; frequencies = frequency, + result_basis = :pul, band = (2e7, 3e7)), + relative_error)) + @test all(iszero, + skipmissing(observe( + compare(a, b, quantity; frequencies = frequency, + result_basis = :pul, band = (1e7, 1e7)), + relative_error))) + end + # A physically nonzero LEP uncertainty remains visible and compared even + # when its corresponding mean is zero. Both are separate requests. + uncertain_core=LineParameters( + complex.(measurement.(zeros(2, 2, 6), fill(1e-4, 2, 2, 6))), + zeros(ComplexF64, 2, 2, 6), f) + spread=LinearErrorResult(LinearError(Formulation()), [uncertain_core]) + @test all(iszero, observe(only(compare(spread, spread, (statistics, R, std))), relative_error)) + @test all(ismissing, observe(only(compare(spread, spread, (statistics, R, mean))), relative_error)) + lep=LinearErrorResult(LinearError(Formulation()), [core]) + @test_throws ArgumentError report( + BenchmarkTableDefinition(((statistics, R, median),)), ( + reference = mc, result = lep)) + @test_throws ArgumentError compare(mc, lep, (statistics, R, Base.Fix2(quantile, 0.1))) + @test_throws ArgumentError compare(mc, lep, (statistics, Z, median)) + + @test @inferred(observe(mc, statistics, R, mean, 1)) == means.R + @test @inferred(observe(mc, statistics, B, std, 1)) ≈ angular .* std.(summaries.C) + @test @inferred(observe(mc, statistics, Z, std, 1)) ≈ expected + # Mean/std publication must not visit or copy the optional trial cloud. + large_samples=map(array -> repeat(array; outer = (1, 1, 1, 100)), trials) + large_summaries=map(summaries) do array + map( + summary -> SampleSummary(summary.mean, summary.std, summary.min, summary.q05, + summary.median, summary.q95, summary.max, 1700), + array) + end + large=MonteCarloResult( + MonteCarlo(Formulation(); trials = 1700, seed = 12, return_samples = true), + [core], [large_summaries], [large_samples], nothing, UInt64(12), UInt64[13], [1700]) + definition=BenchmarkTableDefinition(((statistics, R, mean), (statistics, R, std)); bands = (:all,)) + report(definition, (reference = mc, result = mc)) + report(definition, (reference = large, result = large)) + small_bytes=@allocated report(definition, (reference = mc, result = mc)) + large_bytes=@allocated report(definition, (reference = large, result = large)) + @test large_bytes <= small_bytes+16_384 +end diff --git a/test/unit/textdisplay/geometry.jl b/test/unit/textdisplay/geometry.jl new file mode 100644 index 000000000..80e9818a7 --- /dev/null +++ b/test/unit/textdisplay/geometry.jl @@ -0,0 +1,76 @@ +@testitem "TextDisplay / physical declarations retain units and placement origins" tags=[:unit, :parametric] begin + const DM = LineCableModels.DataModel + const TD = LineCableModels.TextDisplay + copper = Material(:conductor, 1.7241e-8) + dielectric = Material(:insulator, Inf, 2.3) + pose = Pose2(2e-3, -3e-3, pi / 6) + sector = Sector(span=2pi / 3, r_base=0.6e-3, r_back=5e-3, fillet=0.2e-3) + member = terminal(:core, solid(copper, sector), insulation(dielectric; t=0.2e-3)) + design = build(CableDesign, "display-sector", member) + placed = at(member, pose) + explicit = assembly(member, placed) + repeated = Assembly(member; at=pose, pattern=Ring(3; r=8e-3), + names=(:a, :b, :c), path=Helix(Pitch(0.25)), compact=FillFactor(0.9)) + wire = Region(:wire, Disk(0.5e-3), copper) + patterned = Group(:core, wire; at=pose, pattern=Ring(6; r=1e-3), + path=Helix(LayAngle(pi / 12)), compact=FillFactor(0.9)) + bounded = stranded(copper; shape=Disk(0.5e-3), boundary=Disk(1.5e-3), compact=true) + enclosure = Enclosure(:duct, member; at=pose, primitive=Disk(10e-3), + fill=Region(:bedding, Disk(10e-3), dielectric), + wall=insulation(dielectric; t=1e-3)) + datasheet_record = DatasheetInfo(U0=76.0, U=132.0, + conductor_cross_section=630.0, screen_cross_section=35.0, + armor_cross_section=50.0, resistance=0.0283, capacitance=0.2, + inductance=0.3, description="Reference construction") + primitives = ( + Disk(1e-3), DM.resolve(pose, Disk(1e-3)), Rectangle(2e-3, 1e-3), + Ellipse(3e-3, 2e-3), Annulus(1e-3, 2e-3), sector, Shell(0.2e-3), + Ring(capacity()), Ring(6; r=1e-3, φ0=pi/6, span=pi, gap_frac=0.1), + Polar(nr=2, nφ=3, r0=1e-3, dr=2e-3, φ0=pi/6, span=pi), + Fill(r=3e-3, φ=pi/6, φ0=pi/12, span=pi), + Lattice(nx=2, ny=3, dx=1e-3, dy=2e-3), + FillFactor(0.9), LayRatio(12), Pitch(0.25), LayAngle(pi/12), + Helix(LayRatio(12); dir=-1, φ0=pi/6), + Helix(Pitch(0.25); dir=-1, φ0=pi/6), + Helix(LayAngle(pi/12); dir=-1, φ0=pi/6), + DM.EmptyBoundary(), DM.EnclosureBoundary(), datasheet_record, + ) + physical_objects = ( + wire, Stack(wire), patterned, Group(:single, wire), bounded, + placed, explicit, repeated, enclosure, design, design.geometry, + design.geometry.regions..., (region.primitive for region in design.geometry.regions)..., + DM.preview_shapes(first(design.geometry.regions))..., + CablesLibrary(), + ) + for object in (primitives..., physical_objects...) + @test !isempty(TD.name(typeof(object))) + @test !isempty(sprint(summary, object)) + compact = sprint(show, object) + @test !occursin('\n', compact) + @test sprint(show, MIME"text/plain"(), object; context=:compact=>true) == compact + @test !endswith(sprint(show, MIME"text/plain"(), object), '\n') + end + + # Scientific descriptions report physical quantities and identities. Implementation + # type parameters are excluded. Check the quantities and identities, not cosmetic whitespace. + @test occursin("mm", sprint(show, Disk(1e-3))) + @test occursin("capacity()", sprint(show, Ring(capacity()))) + @test occursin("at=", sprint(show, DM.resolve(pose, Disk(1e-3)))) + @test occursin("−1", sprint(show, Helix(Pitch(0.25); dir=-1))) + @test occursin("mm²", sprint(show, datasheet_record)) + @test occursin("Ω/km", sprint(show, datasheet_record)) + @test occursin("μF/km", sprint(show, datasheet_record)) + @test occursin("mH/km", sprint(show, datasheet_record)) + @test occursin("kV", sprint(show, datasheet_record)) + @test occursin("Reference construction", sprint(show, datasheet_record)) + @test occursin("2 explicit members", sprint(show, MIME"text/plain"(), explicit)) + @test occursin("250 mm", sprint(show, MIME"text/plain"(), repeated)) + @test occursin("FillFactor", sprint(show, MIME"text/plain"(), patterned; + context=IOContext(IOBuffer(), :displaysize=>(40, 200)))) + @test occursin("fill · Region :bedding", sprint(show, MIME"text/plain"(), enclosure)) + @test occursin("wall", sprint(show, MIME"text/plain"(), enclosure)) + @test occursin("terminal=:core", sprint(show, first(design.geometry.regions))) + @test !occursin("terminal=", sprint(show, last(design.geometry.regions))) + @test occursin("inner", sprint(show, MIME"text/plain"(), last(design.geometry.regions).primitive)) + @test occursin("outer", sprint(show, MIME"text/plain"(), last(design.geometry.regions).primitive)) +end diff --git a/test/unit/textdisplay/policy.jl b/test/unit/textdisplay/policy.jl new file mode 100644 index 000000000..300d8acb3 --- /dev/null +++ b/test/unit/textdisplay/policy.jl @@ -0,0 +1,276 @@ +@testitem "TextDisplay / engineering values and target summaries" tags=[:unit, :engine] begin + const TD=LineCableModels.TextDisplay + const EP=LineCableModels.Earth + + copper=Material(:conductor, 1.7241e-8, 1.0, 0.999994, 20.0, 0.00393) + constants=CableConstants(1.33803e-5, 4.04835e-6, 1.83687e-10) + earth=EP.EarthModel(100.0, 10.0, 1.0) + frequencies=[1.0, 1.0e6] + parameters=LineParameters( + zeros(ComplexF64, 2, 2, 2), + zeros(ComplexF64, 2, 2, 2), + frequencies + ) + + @test TD.engineering(0.00183245, :meter) == "1.83245 mm" + @test TD.engineering(1.7241e-8, :ohm_meter) == "17.241 nΩ·m" + for number in (big"1e-400", big"-1e-400", big"1e400") + rendered, unit = split(TD.engineering(number, :meter)) + @test parse(BigFloat, rendered) ≈ number + @test unit == "m" + end + @test TD.value(Inf) == "∞" + @test TD.value(-Inf) == "−∞" + @test TD.angle(2π) == "2π" + @test TD.truncate_text("abcdef", 4) == "abc…" + @test TD.truncate_text("界面", 3) == "界…" + @test TD.truncate_text("界面", 4) == "界面" + @test TD.truncate_text("abcdef", 1) == "…" + @test TD.truncate_text("abcdef", 0) == "" + @test TD.truncate_text("abcdef", -1) == "" + + @test sprint(show, copper) == + "Material(:conductor; ρ=17.241 nΩ·m, εᵣ=1, μᵣ=0.999994)" + @test sprint(show, constants) == + "CableConstants(assemblies=1, frequency=50.0)" + @test sprint(show, MIME"text/plain"(), earth) == join( + ( + "EarthModel · homogeneous", + "├─ air ρ=∞ εᵣ=1 μᵣ=1", + "└─ earth ρ=100 Ω·m εᵣ=10 μᵣ=1" + ), + '\n') + @test sprint(show, MIME"text/plain"(), parameters) == join( + ( + "LineParameters · phase domain", + "├─ f 2 points · 1 Hz … 1 MHz", + "├─ Z 2×2×2 · Ω/m", + "└─ Y 2×2×2 · S/m" + ), + '\n') + + inactive=Material(:insulator, Inf, 2.3, 1.0, 20.0, 0.0) + inactive_text=sprint(show, MIME"text/plain"(), inactive) + @test !occursin("α", inactive_text) + @test !occursin("tanδ", inactive_text) + @test !endswith(inactive_text, '\n') +end + +@testitem "TextDisplay / tree labels preserve structured and string children" tags=[:unit, :commons] begin + const TD = LineCableModels.TextDisplay + structured = ((label="branch", children=((label="leaf",),)),) + strings = ((label="branch", children=("leaf",)),) + for children in (structured, strings) + io = IOBuffer() + @test (@inferred TD.tree(io, "root", children)) === nothing + @test String(take!(io)) == "root\n└─ branch\n └─ leaf" + end + io = IOBuffer() + @test (@inferred TD.tree(io, "root", ("first", "second"))) === nothing + @test String(take!(io)) == "root\n├─ first\n└─ second" +end + +@testitem "TextDisplay / engineering conversion only recovers unsupported values" tags=[:unit, :commons] begin + const TD = LineCableModels.TextDisplay + struct Unconvertible <: Real end + Base.iszero(::Unconvertible) = false + @test TD._engineering_exponent(Unconvertible()) == 0 + + struct ConversionFailure{E} <: Real + exception::E + end + Base.iszero(::ConversionFailure) = false + Base.Float64(value::ConversionFailure) = throw(value.exception) + @test TD._engineering_exponent(ConversionFailure(InexactError(:Float64, Float64, 1))) == 0 + for exception in (InterruptException(), ErrorException("conversion bug"), + MethodError(sin, (nothing,))) + caught = try + TD._engineering_exponent(ConversionFailure(exception)) + catch error + error + end + @test caught === exception + end +end + +@testitem "TextDisplay / scientific values preserve signs and physical units" tags=[:unit, :engine] begin + const TD = LineCableModels.TextDisplay + const U = LineCableModels.Units + + @test TD.value(0.0) == "0" + @test TD.value(-0.0) == "0" + @test TD.value(NaN) == "NaN" + @test TD.value(1.23456789; sigdigits = 4) == "1.235" + @test TD.value(1.25 + 2.5im) == "1.25 + 2.5im" + @test TD.value(1.25 - 2.5im) == "1.25 − 2.5im" + @test TD.value(complex(1.0, -0.0)) == "1 − 0im" + @test TD.value(complex(Inf, -Inf)) == "∞ − ∞im" + @test TD.value(:unrecorded) == ":unrecorded" + @test_throws ArgumentError TD.value(1.0; sigdigits = 0) + @test_throws ArgumentError TD.value(1.0 + im; sigdigits = 0) + + for (number, unit, expected) in ( + (0.0, :meter, "0 m"), + (-2e-3, :meter, "-2 mm"), + (0.1, :hertz, "100 mHz"), + (1e6, :hertz, "1 MHz"), + (20.0, :celsius, "20 °C"), + (0.00393, :kelvin_inverse, "0.00393 K⁻¹"), + (30.0, :degree, "30°"), + (0.001, :dimensionless, "0.001"), + (Inf, :ohm_meter, "∞ Ω·m"), + (-Inf, :celsius, "−∞ °C"), + (NaN, :dimensionless, "NaN"), + (Inf, :degree, "∞°") + ) + @test TD.engineering(number, unit) == expected + end + @test TD.angle(0.0) == "0°" + @test TD.angle(pi) == "π" + @test TD.angle(-pi) == "−π" + @test TD.angle(-2pi) == "-2π" + @test TD.angle(pi / 6) == "30°" + + for (object, compact, summary_text) in ( + (U.Unit(:ohm, :milli), "mΩ", "Physical unit mΩ"), + (U.units(:micro, :farad; per = (:kilo, :meter)), "μF/km", "Unit expression μF/km"), + (U.UnitExpr(), "", "Unit expression "), + (U.quantity(R), "R · Series resistance", "Series resistance"), + (U.Quantity{:dimensionless}(), "Dimensionless", "Dimensionless") + ) + @test sprint(show, object) == compact + @test sprint(show, MIME"text/plain"(), object) == compact + @test sprint(summary, object) == summary_text + end + @test TD.name(typeof(formula(:default))) == "FormulaDefinition" +end + +@testitem "TextDisplay / bounded structural families" tags=[:unit, :measurements] setup=[ + TestFixtures +] begin + using Measurements + const EP=LineCableModels.Earth + + conductor=Material(:conductor, 1.7241e-8) + insulator=Material(:insulator, Inf, 2.3) + wire=Region(:wire, Disk(1.0e-3), conductor) + group=Group( + :core, + wire; + pattern = Ring(6; r = 2.0e-3), + path = Helix(LayRatio(11.0)) + ) + insulating_layers=ntuple( + index->Region(Symbol(:layer_, index), Annulus(index*1.0e-3, (index+1)*1.0e-3), insulator), + 10) + stack=Stack(group, insulating_layers...) + repeated=Assembly( + group; + pattern = Ring(2; r = 6.0e-3), + names = (:left, :right) + ) + enclosure=Enclosure( + :duct, + group; + primitive = Disk(10.0e-3), + fill = insulator + ) + design=TestFixtures.coaxial_design() + system=TestFixtures.three_phase_system() + earth=build(EP.EarthModel, + ( + EP.EarthLayer(100.0, 10.0, 1.0, 2.0), + EP.EarthLayer(30.0, 15.0, 1.0, 5.0), + EP.EarthLayer(500.0, 8.0, 1.0) + )) + grid=Grid((1.0, 2.0, 3.0, 4.0, 5.0)) + build_calls=Ref(0) + space=Gridspace{CableConstants}( + (r, l, c)->begin + build_calls[]+=1 + CableConstants(r, l, c) + end, + (Grid((1.0, 2.0, 3.0)), Grid((2.0,)), Grid((3.0, 4.0))) + ) + materials=MaterialsLibrary() + cables=CablesLibrary() + add!(cables, design) + parameters=TestFixtures.two_conductor_results() + parametric=ParametricResult(Combinatorial(Formulation()), [parameters]) + monte_carlo=TestFixtures.cable_monte_carlo_result() + + families=( + group, + stack, + repeated, + enclosure, + design.geometry, + design, + system, + earth, + grid, + space, + materials, + cables, + parametric, + parameters, + monte_carlo + ) + family_names=( + :group, + :stack, + :assembly, + :enclosure, + :geometry, + :design, + :system, + :earth, + :grid, + :gridspace, + :materials_library, + :cables_library, + :parametric_result, + :line_parameters, + :monte_carlo_result + ) + + # Semantic width and compactness controls below replace saved terminal pages. + # A renderer may change spacing without inheriting a historical transcript. + for object in families + compact=sprint(show, object) + @test !occursin('\n', compact) + @test !occursin(r"\{[^}]*\}", compact) + @test !endswith(compact, '\n') + + wide_context=IOContext( + IOBuffer(), :compact=>false, :limit=>true, + :displaysize=>(40, 120) + ) + narrow_context=IOContext( + IOBuffer(), :compact=>false, :limit=>true, + :displaysize=>(6, 48) + ) + wide=sprint(show, MIME"text/plain"(), object; context = wide_context) + repeated_wide=sprint(show, MIME"text/plain"(), object; context = wide_context) + narrow=sprint(show, MIME"text/plain"(), object; context = narrow_context) + + @test wide == repeated_wide + @test !endswith(wide, '\n') + @test !endswith(narrow, '\n') + @test length(split(narrow, '\n')) <= 6 + @test all(textwidth(line) <= 48 for line in split(narrow, '\n')) + end + + @test build_calls[] == 0 + @test sprint(show, space) == "CableConstants parameter space · 6 points" + @test occursin("⋮", + sprint( + show, + MIME"text/plain"(), + stack; + context = IOContext( + IOBuffer(), :limit => true, :displaysize => (5, 48) + ) + )) + @test !occursin("0.0 + 0.0im", sprint(show, MIME"text/plain"(), parameters)) +end diff --git a/test/unit/textdisplay/workflows.jl b/test/unit/textdisplay/workflows.jl new file mode 100644 index 000000000..794b3de5d --- /dev/null +++ b/test/unit/textdisplay/workflows.jl @@ -0,0 +1,269 @@ +@testitem "TextDisplay / computation intent and numerical results" tags=[:unit, :parametric] setup=[ + TestFixtures, +] begin + const EN=LineCableModels.Engine + design=TestFixtures.coaxial_design() + problem=CableConstantsProblem(design; temperature = 60.0, frequency = 60.0) + line_problem=LineParametersProblem(TestFixtures.three_phase_system(); + earth_props = homogeneous(rho = 100.0), frequencies = [0.1, 50.0, 1e6]) + formulation=Formulation( + earth_impedance = :default, + insulation_admittance = :lossy, + semicon_admittance = :default + ) + cable_formulation=CableConstantsFormulation(insulation_admittance = :lossy) + parameters=TestFixtures.two_conductor_results() + modal=compute(ModalAnalysisProblem(parameters), ModalAnalysisFormulation()) + benchmark=EN.compare(parameters, parameters) + constants=CableConstants(1e-4, 2e-7, 3e-10, 4e-12) + backend=LineCableModelsFEM(options = (physics = :quasi_fw,)) + backend_options=computation_options(LineCableModelsFEM, ComputationOptions((mesh_mode=:remesh,))) + blueprints=EN.CableBlueprint{eltype(line_problem)}[EN.flatten(LineCableModelsCoaxial(), + design, eltype(line_problem)) + for design in line_problem.system.designs] + workspaces=map((false, true)) do trace + execution=LineCableModels.Commons.computation_options(LineCableModelsCoaxial, ComputationOptions((; + trace))) + EN.LineParametersWorkspace(line_problem, formulation, execution, blueprints) + end + for workspace in workspaces + # Scratch storage is intentionally uninitialized until compute. Give + # the two primitive buffers sentinels before checking display purity. + fill!(workspace.buffers.Zprimitive, 123+im) + fill!(workspace.buffers.Pprimitive, -456+2im) + end + objects=( + problem, line_problem, formulation, cable_formulation, constants, + LineCableModelsCoaxial(), backend, + EN.PhaseDomain(), modal.domain, parameters.Z, parameters.Y, + parameters, modal, benchmark, benchmark.Z, workspaces... + ) + + # REPL inspection identifies the requested computation. It must leave matrix contents undisplayed and avoid solving problems or loading optional backends. + before=(copy(parent(parameters.Z)), copy(parent(parameters.Y))) + for object in objects + compact=sprint(show, object) + @test !occursin('\n', compact) + @test !isempty(sprint(summary, object)) + @test sprint(show, MIME"text/plain"(), object; context = :compact=>true) == compact + for display_size in ((40, 120), (5, 48)) + context=IOContext(IOBuffer(), :limit=>true, :displaysize=>display_size) + shown=sprint(show, MIME"text/plain"(), object; context) + @test !endswith(shown, '\n') + @test length(split(shown, '\n')) <= first(display_size) + @test all(textwidth(line) <= last(display_size) for line in split(shown, '\n')) + end + end + @test parent(parameters.Z) == before[1] + @test parent(parameters.Y) == before[2] + for (index, workspace) in enumerate(workspaces) + @test all(==(123 + im), workspace.buffers.Zprimitive) + @test all(==(-456 + 2im), workspace.buffers.Pprimitive) + @test occursin("frequencies=3", sprint(show, workspace)) + @test occursin(index == 1 ? "disabled" : "enabled", + sprint(show, MIME"text/plain"(), workspace)) + end + shown=sprint(show, MIME"text/plain"(), problem) + @test occursin(design.cable_id, shown) + @test occursin("60 °C", shown) + @test occursin("60 Hz", shown) + shown=sprint(show, MIME"text/plain"(), line_problem) + @test occursin("3 points", shown) + @test occursin("100 mHz", shown) + @test occursin("1 MHz", shown) + shown=sprint(show, MIME"text/plain"(), formulation; + context = IOContext(IOBuffer(), :displaysize=>(40, 160))) + @test occursin("schelkunoff1934", shown) + @test occursin("ametani1980", shown) + @test occursin("semicon_admittance", shown) + @test occursin("unified", shown) + @test occursin("ametani1980", sprint(show, MIME"text/plain"(), cable_formulation)) + @test occursin("Ω/m", sprint(show, MIME"text/plain"(), parameters.Z)) + @test occursin("S/m", sprint(show, MIME"text/plain"(), parameters.Y)) + @test occursin("RMS errors", sprint(show, MIME"text/plain"(), benchmark)) + @test occursin("relative", sprint(show, MIME"text/plain"(), benchmark.Z)) + @test occursin("G=", sprint(show, MIME"text/plain"(), constants)) + @test occursin("remesh", sprint(show, backend_options)) + @test occursin("modal domain", sprint(show, modal)) + @test occursin("Modal domain", sprint(summary, modal.domain)) +end + +@testitem "TextDisplay / wire estimates expose feasibility and physical dimensions" tags=[:unit, :parametric] begin + estimates = ( + estimate_stranding(1000.0), + estimate_stranding(1.0e12; awg_min = 40, awg_max = 40), + estimate_screen(35.0, 60.0; coverage_min = 85.0), + estimate_screen(35.0, 60.0; coverage_min = 99.0, coverage_max = 99.0) + ) + for estimate in estimates + patterns = collect(estimate) + @test occursin("$(length(estimate)) patterns", sprint(summary, estimate)) + @test occursin(string(estimate.status), sprint(show, estimate)) + @test LineCableModels.TextDisplay.name(typeof(estimate)) == "WireEstimate" + @test sprint(show, MIME"text/plain"(), estimate; context = :compact=>true) == + sprint(show, estimate) + shown = sprint(show, MIME"text/plain"(), estimate; + context = IOContext(IOBuffer(), :limit=>false, :displaysize=>(80, 160))) + @test occursin("target", shown) + @test occursin("patterns", shown) + @test occursin(string(estimate.status), shown) + for reason in estimate.reasons + @test occursin(reason, shown) + end + # Patterns share their display method. The ranked endpoints suffice. + for pattern in (first(patterns), last(patterns)) + @test occursin("$(pattern.wires) wires", sprint(summary, pattern)) + compact = sprint(show, pattern) + @test occursin("wires=$(pattern.wires)", compact) + @test occursin("d=", compact) + @test sprint(show, MIME"text/plain"(), pattern) == compact + if hasproperty(pattern, :coverage) + @test occursin("coverage=", compact) + @test occursin("%", compact) + end + end + @test collect(estimate) == patterns + end +end + +@testitem "TextDisplay / lazy parametric and uncertainty intent" tags=[:unit, :distributions] setup=[ + TestFixtures, +] begin + using Measurements + using Distributions + const PB=LineCableModels.ParametricBuilder + const UQ=LineCableModels.UQ + relative=Grid((10.0, 100.0), (1.0, 2.0)) + absolute=Grid((10.0, 100.0), AbsoluteError((0.1, 0.2))) + calls=Ref(0) + design=TestFixtures.coaxial_design() + space=Gridspace{CableConstantsProblem}( + temperature->begin + calls[]+=1 + CableConstantsProblem(design; temperature) + end, + (Grid((20.0, 40.0)),) + ) + nested=Gridspace{CableConstantsProblem}(identity, (space,)) + parametric=ParametricProblem(nested) + inner=CableConstantsFormulation() + combinatorial=Combinatorial(inner) + linear=LinearError(inner; options = (retain_details = true,)) + automatic=MonteCarlo(inner) + explicit=MonteCarlo(inner; trials = 12, seed = 17, distribution = Uniform(-1, 1), + return_samples = true, return_histograms = true) + earth_definition=formula(:default; order = :before) + soil_definition=formula(:default; parameters = (tolerance = 1e-6,)) + constants=CableConstants(1e-4, 2e-7, 3e-10) + results=ParametricResult(combinatorial, [constants]) + empty_results=ParametricResult(combinatorial, typeof(constants)[]) + linear_results=LinearErrorResult(linear, [constants]) + empty_linear=LinearErrorResult(linear, typeof(constants)[]) + mc_results=TestFixtures.cable_monte_carlo_result() + histogram=UQ.HistogramDensity([1.0, 3.0, 5.0], [0.25, 0.25]) + objects=( + relative, absolute, AbsoluteError((0.1, 0.2)), PB.UncertainValue(10.0, 0.1), + Grid((:default, :lossy)), nested, parametric, combinatorial, + linear, automatic, explicit, results, empty_results, + linear_results, empty_linear, mc_results, histogram, + earth_definition, soil_definition, Grid((earth_definition, soil_definition)) + ) + for object in objects + compact=sprint(show, object) + @test !occursin('\n', compact) + @test !isempty(sprint(summary, object)) + @test sprint(show, MIME"text/plain"(), object; context = :compact=>true) == compact + wide=sprint(show, MIME"text/plain"(), object; + context = IOContext(IOBuffer(), :limit=>true, :displaysize=>(40, 140))) + @test !endswith(wide, '\n') + @test !isempty(wide) + end + @test calls[] == 0 + @test occursin("relative error", sprint(show, MIME"text/plain"(), relative)) + @test occursin("%", sprint(show, relative)) + @test occursin("absolute error", sprint(show, MIME"text/plain"(), absolute)) + @test !occursin("%", sprint(show, absolute)) + @test occursin("2 points", sprint(show, nested)) + @test occursin("CableConstantsProblem", sprint(show, nested)) + @test occursin("retained", sprint(show, MIME"text/plain"(), linear)) + @test occursin("DKW-sized", sprint(show, automatic)) + shown=sprint(show, MIME"text/plain"(), explicit) + @test occursin("12", shown) + @test occursin("17", shown) + @test occursin("Uniform", shown) + @test occursin("samples", shown) + @test occursin("histograms", shown) + @test occursin("none", sprint(show, MIME"text/plain"(), empty_results)) + @test occursin("none", sprint(show, MIME"text/plain"(), empty_linear)) + @test occursin("CableConstants", sprint(show, MIME"text/plain"(), linear_results)) + @test occursin("4 per point", sprint(show, MIME"text/plain"(), mc_results)) + @test occursin("2 bins", sprint(show, histogram)) + @test occursin("normalized", sprint(show, MIME"text/plain"(), histogram)) + @test occursin("default", sprint(show, earth_definition)) + @test occursin("before", sprint(show, earth_definition)) + @test occursin("tolerance", sprint(show, soil_definition)) + @test !occursin("{", sprint(show, Grid((earth_definition, soil_definition)))) +end + +@testitem "TextDisplay / report definitions do not publish or write" tags=[:unit, :report] begin + using DataFrames + const RB = LineCableModels.ReportBuilder + calls = Ref(0) + illustration = observed -> begin + calls[] += 1 + :illustration + end + plain = TableReportDefinition((R, L)) + illustrated = TableReportDefinition((R, L); illustration, clip = false) + line = RB.LineParametersTableDefinition( + (@observe(R[:, :, :]),), :base, :kilo, nothing, true) + mc = RB.MonteCarloTableDefinition(:kilo, nothing, false) + table = DataFrame(R = [1.0, 2.0], L = [3.0, 4.0]) + original = copy(table) + observed=ObservedResult(LineParameters(fill(1.0+2im,1,1,2),fill(3.0+4im,1,1,2),[1.,2.])) + artifact=ReportArtifact(observed,nothing,table,nothing,nothing) + illustrated_artifact=ReportArtifact(observed,nothing,table,:illustration,:destination) + + mktempdir() do directory + destination = joinpath(directory, "not-written.xlsx") + workbook = RB.XLSXWorkbook(destination, + [ + RB.XLSXSheet("Z(1,1)", ["frequency" "R"; "50" "1"]), + RB.XLSXSheet("Y(1,1)", ["frequency" "G"; "50" "2"]) + ]) + objects = ( + plain, illustrated, RB.CableConstantsTableDefinition(false), line, + RB.BenchmarkTableDefinition(false), mc, XLSXReportDefinition(), + XLSXReportDefinition(file_name = destination, clip = false), + artifact, illustrated_artifact, workbook + ) + for object in objects + compact = sprint(show, object) + @test !occursin('\n', compact) + @test !isempty(sprint(summary, object)) + @test sprint(show, MIME"text/plain"(), object; context = :compact=>true) == + compact + shown = sprint(show, MIME"text/plain"(), object) + @test !endswith(shown, '\n') + end + @test !ispath(destination) + @test isempty(readdir(directory)) + @test occursin("Z(1,1)", sprint(show, MIME"text/plain"(), workbook)) + @test occursin("Y(1,1)", sprint(show, MIME"text/plain"(), workbook)) + @test occursin("2×2 cells", sprint(show, MIME"text/plain"(), workbook)) + end + @test calls[] == 0 + @test table == original + @test occursin("2 observations", sprint(summary, plain)) + @test !occursin("illustration", sprint(show, MIME"text/plain"(), plain)) + @test occursin("illustration", sprint(show, MIME"text/plain"(), illustrated)) + @test occursin("false", sprint(show, MIME"text/plain"(), illustrated)) + @test occursin("kilo", sprint(show, line)) + @test occursin("false", sprint(show, mc)) + @test occursin("default path", sprint(show, XLSXReportDefinition())) + @test occursin("tables=1", sprint(show, artifact)) + @test !occursin("illustration", sprint(show, artifact)) + @test !occursin("output", sprint(show, artifact)) + @test occursin("illustration=present", sprint(show, illustrated_artifact)) + @test occursin("output=present", sprint(show, illustrated_artifact)) +end diff --git a/test/unit/units/units.jl b/test/unit/units/units.jl new file mode 100644 index 000000000..682a56d97 --- /dev/null +++ b/test/unit/units/units.jl @@ -0,0 +1,237 @@ +@testitem "Units / immutable expressions and compatible conversion" tags = [:unit, :units] begin + const U = LineCableModels.Units + + per_meter = U.units(:base, :ohm; per = (:base, :meter)) + per_kilometer = U.units(:base, :ohm; per = (:kilo, :meter)) + @test per_meter.numerator isa Tuple + @test per_meter.denominator isa Tuple + @test U.label(per_kilometer) == "Ω/km" + @test U.scale_factor(per_meter, per_kilometer) == 1_000.0 + @test U.scale_factor(per_kilometer, per_meter) == 0.001 + @test hash(per_meter) == hash(U.units(:base, :ohm; per = (:base, :meter))) + + composite = U.UnitExpr( + (U.Unit(:ohm), U.Unit(:meter)), + (U.Unit(:second), U.Unit(:meter)) + ) + @test U.label(composite) == "Ω.m/(s.m)" + @test U.label(U.UnitExpr()) == "" + @test U.label(inv(U.units(:base, :hertz))) == "1/Hz" + + @test_throws ArgumentError U.Unit(:custom) + @test_throws ArgumentError U.Unit(:ohm, :unsupported) + @test_throws ArgumentError U.Unit(:degree, :milli) + @test_throws ArgumentError U.scale_factor( + U.units(:base, :ohm), + U.units(:base, :farad) + ) +end + +@testitem "Units / comparison errors preserve dimensions and normalization" tags=[:unit, :engine] begin + const U = LineCableModels.Units + const EN = LineCableModels.Engine + for (selector, unit_symbol) in ((Z, "Ω"), (Y, "S")) + absolute = quantity(selector, EN.absolute_error) + relative = quantity(selector, EN.relative_error) + @test U.symbol(absolute) == "Δ" * symbol(selector) + @test U.symbol(relative) == "ε" * symbol(selector) + @test occursin("absolute error", U.label(absolute)) + @test occursin("relative error", U.label(relative)) + @test U.label(U.native_unit(absolute, :pul)) == "$unit_symbol/m" + @test U.label(U.display_unit(absolute, :pul)) == "$unit_symbol/km" + @test U.scale_factor(U.native_unit(absolute, :pul), U.display_unit(absolute, :pul)) == 1000 + @test U.label(U.display_unit(absolute, :total)) == unit_symbol + for basis in (:pul, :total) + @test U.label(U.native_unit(relative, basis)) == "" + @test U.label(U.display_unit(relative, basis)) == "" + @test U.scale_factor(U.native_unit(relative, basis), U.display_unit(relative, basis)) == 1 + @test U.label(relative, U.display_unit(relative, basis)) == U.label(relative) + end + end + distance = U.Quantity{:distance}() + dimensionless = U.Quantity{:dimensionless}() + phase = U.Quantity{:phase_angle}() + @test U.symbol(distance) == "d" + @test U.label(distance, U.display_unit(distance)) == "Distance [m]" + @test U.native_unit(distance) == U.display_unit(distance) + @test U.symbol(dimensionless) == "" + @test U.label(dimensionless, U.display_unit(dimensionless)) == "Dimensionless" + @test U.symbol(phase) == "∠" + @test U.label(phase, U.display_unit(phase)) == "Phase angle [°]" + @test U.scale_factor(U.native_unit(phase), U.display_unit(phase)) ≈ 180 / pi + @test symbol(Y, abs) == "|Y|" +end + +@testitem "Units / quantity metadata and basis" tags = [:unit, :engine] begin + const U = LineCableModels.Units + + expected = ( + (R, U.Quantity{:series_resistance}, "Series resistance", "R", "Ω/km"), + (X, U.Quantity{:series_reactance}, "Series reactance", "X", "Ω/km"), + (L, U.Quantity{:series_inductance}, "Series inductance", "L", "mH/km"), + (G, U.Quantity{:shunt_conductance}, "Shunt conductance", "G", "S/km"), + (B, U.Quantity{:shunt_susceptance}, "Shunt susceptance", "B", "S/km"), + (C, U.Quantity{:shunt_capacitance}, "Shunt capacitance", "C", "μF/km"), + (Z, U.Quantity{:series_impedance}, "Series impedance", "Z", "Ω/km"), + (Y, U.Quantity{:shunt_admittance}, "Shunt admittance", "Y", "S/km"), + (frequencies, U.Quantity{:frequency}, "Frequency", "f", "Hz") + ) + + for (selector, tag_type, name, notation, unit_text) in expected + tag = U.quantity(selector) + @test tag isa tag_type + @test U.label(tag) == name + @test U.symbol(tag) == notation + @test U.label(U.display_unit(tag, :pul)) == unit_text + @test U.label(tag, U.display_unit(tag, :pul)) == "$name [$unit_text]" + end + + @test U.label(U.native_unit(U.quantity(R), :pul)) == "Ω/m" + @test U.label(U.display_unit(U.quantity(R), :total)) == "Ω" + @test U.label(U.display_unit(U.quantity(C), :total)) == "μF" + @test U.label(U.display_unit(U.quantity(R), :pul; prefix = :milli)) == + "mΩ/km" + @test U.label(U.display_unit(U.quantity(C), :total; prefix = :nano)) == "nF" + @test U.display_unit(U.quantity(R), :pul, nothing) == + U.display_unit(U.quantity(R), :pul) + @test U.label(U.display_unit(U.quantity(R), :pul, :micro)) == "μΩ/km" + explicit=U.units(:nano, :ohm; per = (:base, :meter)) + @test U.display_unit(U.quantity(R), :pul, explicit) === explicit + @test_throws ArgumentError U.display_unit(U.quantity(R), :pul, 1) + @test_throws ArgumentError U.display_unit(U.quantity(R), :invalid) + + @test quantity(R) isa U.Quantity{:series_resistance} + @test label(R) == "Series resistance" + @test symbol(R) == "R" + @test label(native_unit(R, :pul)) == "Ω/m" + @test label(display_unit(R, :pul)) == "Ω/km" + @test label(display_unit(R, :total)) == "Ω" + @test scale_factor(R, display_unit(R)) == 1_000.0 + @test scale_factor(R, :pul, display_unit(R, :pul)) == 1_000.0 +end + +@testitem "Commons / observable unit targets / aligned normalization" tags=[:unit, :engine] begin + const Commons=LineCableModels.Commons + const U=LineCableModels.Units + + requests=(R, (Z, angle), C) + targets=Commons.unit_targets( + requests, + :pul; + length_prefix = :base, + overrides = ( + R = :milli, + Z = U.units(:base, :radian) + ) + ) + tupled=Commons.unit_targets( + requests, + :pul; + length_prefix = :base, + overrides = Dict( + R => :milli, + (Z, angle) => U.units(:base, :radian) + ) + ) + @test targets == tupled + @test all(target -> target isa U.UnitExpr, targets) + @test targets[1] == U.units(:milli, :ohm; per = (:base, :meter)) + @test targets[2] == U.units(:base, :radian) + @test targets[3] == U.units(:micro, :farad; per = (:base, :meter)) + @test Base.ispublic(Commons, :unit_targets) + @test_throws ArgumentError Commons.unit_targets((R,), :pul; overrides = 1) +end + +@testitem "Units / transforms and angular conversion" tags = [:unit, :engine] begin + const U = LineCableModels.Units + + z_magnitude = U.quantity(Z, abs) + z_angle = U.quantity(Z, angle) + y_magnitude = U.quantity(Y, abs) + y_angle = U.quantity(Y, angle) + + @test z_magnitude isa U.Quantity{(:series_impedance, :magnitude)} + @test z_angle isa U.Quantity{(:series_impedance, :phase_angle)} + @test y_magnitude isa U.Quantity{(:shunt_admittance, :magnitude)} + @test y_angle isa U.Quantity{(:shunt_admittance, :phase_angle)} + @test U.label(z_magnitude) == "Series impedance magnitude" + @test U.symbol(y_angle) == "∠Y" + @test U.label(U.native_unit(z_angle)) == "rad" + @test U.label(U.display_unit(z_angle)) == "°" + @test U.scale_factor(U.native_unit(z_angle), U.display_unit(z_angle)) ≈ 180 / π + @test quantity(Z, abs) isa U.Quantity{(:series_impedance, :magnitude)} + @test label(Z, abs) == "Series impedance magnitude" + @test symbol(Z, abs) == "|Z|" + @test symbol(Z, angle) == "∠Z" + @test label(native_unit(Z, angle)) == "rad" + @test label(display_unit(Z, angle)) == "°" + @test label(native_unit(Z, angle, :pul)) == "rad" + @test label(display_unit(Z, angle, :pul)) == "°" + @test scale_factor(Z, angle, display_unit(Z, angle)) ≈ 180 / π + @test scale_factor(Z, angle, :pul, display_unit(Z, angle, :pul)) ≈ 180 / π + @test_throws MethodError U.quantity(Z, :abs) + @test_throws MethodError U.quantity(identity) +end + +@testitem "Units / selector metadata / extension locality" tags = [:unit, :units] begin + const U = LineCableModels.Units + + profile_response() = nothing + U.quantity(::typeof(profile_response)) = U.Quantity{:profile_response}() + U.native_unit(::U.Quantity{:profile_response}) = U.units(:base, :ohm) + U.display_unit(::U.Quantity{:profile_response}) = U.units(:milli, :ohm) + U.label(::U.Quantity{:profile_response}) = "Profile response" + U.symbol(::U.Quantity{:profile_response}) = "u" + + @test quantity(profile_response) isa U.Quantity{:profile_response} + @test label(profile_response) == "Profile response" + @test symbol(profile_response) == "u" + @test native_unit(profile_response) == U.units(:base, :ohm) + @test native_unit(profile_response, :pul) == U.units(:base, :ohm) + @test display_unit(profile_response) == U.units(:milli, :ohm) + @test display_unit(profile_response, :pul) == U.units(:milli, :ohm) + @test scale_factor(profile_response, U.units(:milli, :ohm)) == 1_000.0 + @test scale_factor(profile_response, :pul, U.units(:milli, :ohm)) == 1_000.0 + + unregistered_selector() = nothing + @test_throws MethodError quantity(unregistered_selector) + @test_throws MethodError label(unregistered_selector) + @test_throws MethodError display_unit(unregistered_selector, :pul) +end + +@testitem "Units / locked public vocabulary" tags = [:unit, :units] begin + const U = LineCableModels.Units + + for name in ( + :Unit, :UnitExpr, :Quantity, :units, :quantity, :native_unit, + :display_unit, :scale_factor, :label, :symbol + ) + @test name in names(U) + end + @test isbitstype(U.Quantity{:series_resistance}) + @test fieldcount(U.Quantity{:series_resistance}) == 0 + for name in (:quantity, :native_unit, :display_unit, :scale_factor, :label, :symbol) + @test name in names(LineCableModels) + @test getfield(LineCableModels, name) === getfield(U, name) + end + for name in (:Quantity, :Unit, :UnitExpr) + @test name ∉ names(LineCableModels) + end +end +@testitem "Units / statistical report axes keep counts and probabilities dimensionless" tags=[:unit, :uq] begin + const U = LineCableModels.Units + for (name, caption, symbol) in ((:sample_count, "Count", "n"), + (:probability, "Probability", "p"), + (:cumulative_probability, "Cumulative probability", "F")) + quantity = U.Quantity{name}() + @test U.label(quantity) == caption + @test U.symbol(quantity) == symbol + @test U.native_unit(quantity) == U.display_unit(quantity) == U.units(:base, :dimensionless) + @test U.label(U.display_unit(quantity)) == "" + end + # Density is not a probability: it has its own caption, while the observed + # variable determines the reciprocal unit in the histogram publication. + density = U.Quantity{:probability_density}() + @test U.label(density) == "Probability density" + @test U.symbol(density) == "p" +end diff --git a/test/unit/uq/formulations.jl b/test/unit/uq/formulations.jl new file mode 100644 index 000000000..a9301ab3e --- /dev/null +++ b/test/unit/uq/formulations.jl @@ -0,0 +1,75 @@ +@testitem "UQ / computation options / validated constructors and concrete tuples" tags=[:unit, :uq] begin + using Random + inner = CableConstantsFormulation() + normalize = LineCableModels.computation_options + @test LineCableModels.ComputationOptions !== NamedTuple + + for owner in (Combinatorial, LinearError, MonteCarlo) + keyword = @inferred owner(inner; options=(retain_details=true,)) + positional = @inferred owner(inner, ComputationOptions(retain_details=true)) + @test keyword.options === positional.options + @test normalize(owner, keyword.options) === keyword.options + @test_throws ArgumentError owner(inner; options=(unused=true,)) + @test_throws ArgumentError owner(inner, ComputationOptions(unused=true)) + @test_throws ArgumentError owner(inner, ComputationOptions(retain_details=1)) + # Explicit type parameters must not restore the generated raw constructor. + invalid = ComputationOptions(unused=true) + @test_throws MethodError owner{typeof(inner), typeof(invalid)}(inner, invalid) + @test_throws TypeError owner{typeof(inner), NamedTuple} + end + @test_throws ArgumentError Combinatorial(Grid((1, 2)), ComputationOptions()) + @test_throws ArgumentError Combinatorial( + Gridspace{Int}(identity, (Grid((1, 2)),)), ComputationOptions()) + + plain = (trials=Int16(8), confidence=0.9f0, cdf_tol=0.1f0, + distribution=:uniform, seed=Int16(42), return_samples=true, + return_histograms=true, bins=Int16(2), retain_details=true, + on_error=:resample, max_failures=Int16(5)) + normalized = @inferred normalize(MonteCarlo, ComputationOptions(plain)) + @test normalized.data.trials === 8 + @test normalized.data.seed === UInt64(42) + @test normalized.data.confidence === Float64(0.9f0) + @test normalized.data.cdf_tol === Float64(0.1f0) + @test normalized.data.bins === 2 + @test normalized.data.max_failures === 5 + @test (@inferred MonteCarlo(inner; options=plain)).options === normalized + @test (@inferred MonteCarlo(inner; plain...)).options === normalized + @test (@inferred MonteCarlo(inner; trials=8, options=(seed=42,))).options === + (@inferred MonteCarlo(inner; options=(trials=8, seed=42))).options + @test_throws ArgumentError MonteCarlo(inner; trials=8, options=(trials=8,)) + @test_throws ArgumentError MonteCarlo(inner; unused=true) + @test_throws ArgumentError normalize(MonteCarlo, ComputationOptions((on_error=:resample,))) + @test_throws ArgumentError normalize(MonteCarlo, ComputationOptions((on_error=:ignore,))) + @test_throws ArgumentError normalize(MonteCarlo, ComputationOptions((distribution=:unsupported,))) + + for key in (:trials, :bins, :max_failures) + for value in (true, 0, -1, 1.5, "2", big(typemax(Int)) + 1) + options = NamedTuple{(key,)}((value,)) + @test_throws ArgumentError normalize(MonteCarlo, ComputationOptions(options)) + end + end + @test_throws ArgumentError normalize(MonteCarlo, ComputationOptions((max_failures=nothing,))) + for key in (:confidence, :cdf_tol) + for value in (true, 0, 1, -0.5, NaN, Inf, "0.5", + big(1) - eps(BigFloat), big(2)^(-2000)) + @test_throws ArgumentError normalize(MonteCarlo, ComputationOptions(NamedTuple{(key,)}((value,)))) + end + end + for key in (:return_samples, :return_histograms, :retain_details) + @test_throws ArgumentError normalize(MonteCarlo, ComputationOptions(NamedTuple{(key,)}((1,)))) + end + for seed in (true, -1, 1.5, "42", big(typemax(UInt64)) + 1) + @test_throws ArgumentError normalize(MonteCarlo, ComputationOptions((; seed))) + end + @test normalize(MonteCarlo, ComputationOptions((seed=0,))).data.seed === UInt64(0) + @test normalize(MonteCarlo, ComputationOptions((seed=typemax(UInt64),))).data.seed === typemax(UInt64) + automatic = @inferred MonteCarlo(inner) + @test automatic.options.data.trials === nothing + @test automatic.options.data.seed === nothing + @test automatic.options.data.bins === nothing + + sampler = (rng, center, deviation) -> center + deviation * randn(rng) + custom = @inferred MonteCarlo(inner; options=(distribution=sampler,)) + @test custom.options.data.distribution === sampler + @test fieldtype(typeof(custom.options.data), :distribution) === typeof(sampler) +end diff --git a/test/unit/uq/histograms.jl b/test/unit/uq/histograms.jl new file mode 100644 index 000000000..47944fd88 --- /dev/null +++ b/test/unit/uq/histograms.jl @@ -0,0 +1,39 @@ +@testitem "UQ / histogram bins preserve finite constant and narrow populations" tags=[:unit, :uq] begin + using Statistics + for values in ([3.0,3.0,3.0], [1.0,1.0,nextfloat(1.0)], + [1.0,nextfloat(1.0),nextfloat(1.0,2)], + [0.0,nextfloat(0.0)], [floatmax(Float64),floatmax(Float64)], + [-floatmax(Float64),floatmax(Float64)], + Float32[1,1,nextfloat(1.0f0)]) + original=copy(values) + for bins in (nothing,1,2,32) + density=HistogramDensity(values;bins) + edges=density.edges + @test all(isfinite,edges) + @test all(>(0),diff(edges)) + @test first(edges)<=minimum(values)<=maximum(values)<=last(edges) + @test all(isfinite,density.density) + @test sum(density.density .* diff(edges))≈1 + # Count the original population in each documented half-open bin. + # The final bin alone includes the right endpoint. + expected=[count(x->edges[i]<=x && + (i==length(edges)-1 ? x<=edges[i+1] : x 0 - end - - @testset "calc_equivalent_alpha: Equivalent temperature coefficient for parallel resistors" begin - - # Example values (Copper and Aluminum) - alpha1 = 0.00393 # Copper - R1 = 0.5 - alpha2 = 0.00403 # Aluminum - R2 = 1.0 - - # Analytical result - expected = (alpha1 * R2 + alpha2 * R1) / (R1 + R2) - result = calc_equivalent_alpha(alpha1, R1, alpha2, R2) - @test isapprox(result, expected; atol=TEST_TOL) - - # Edge case: Identical conductors - alpha = 0.00393 - R = 1.0 - @test isapprox(calc_equivalent_alpha(alpha, R, alpha, R), alpha; atol=TEST_TOL) - - # Edge case: One resistance much larger than the other - @test isapprox(calc_equivalent_alpha(0.003, 1e6, 0.005, 1.0), 0.005; atol=TEST_TOL) - @test isapprox(calc_equivalent_alpha(0.003, 1.0, 0.005, 1e6), 0.003; atol=TEST_TOL) - - # Type promotion and Measurements.jl propagation - using Measurements: ±, value, uncertainty - m1 = 0.00393 ± 0.00001 - m2 = 0.00403 ± 0.00001 - r1 = 0.5 ± 0.01 - r2 = 1.0 ± 0.01 - - @testset "Type Promotion with Measurements.jl" begin - # Base case: All Float64 - @test calc_equivalent_alpha(alpha1, R1, alpha2, R2) isa Float64 - - # Fully promoted: All Measurement - res = calc_equivalent_alpha(m1, r1, m2, r2) - @test res isa Measurement{Float64} - @test isapprox(value(res), expected; atol=TEST_TOL) - # Uncertainty should be nonzero - @test uncertainty(res) > 0 - - # Mixed case 1: First argument is Measurement - res = calc_equivalent_alpha(m1, R1, alpha2, R2) - @test res isa Measurement{Float64} - @test isapprox(value(res), expected; atol=TEST_TOL) - - # Mixed case 2: Middle argument is Measurement - res = calc_equivalent_alpha(alpha1, r1, alpha2, R2) - @test res isa Measurement{Float64} - @test isapprox(value(res), expected; atol=TEST_TOL) - - # Mixed case 3: Last argument is Measurement - res = calc_equivalent_alpha(alpha1, R1, alpha2, r2) - @test res isa Measurement{Float64} - @test isapprox(value(res), expected; atol=TEST_TOL) - end - - # Physically unusual but valid: zero resistance (should return NaN) - @test isnan(calc_equivalent_alpha(0.003, 0.0, 0.005, 0.0)) - - # Large values - @test isapprox(calc_equivalent_alpha(1e-3, 1e6, 2e-3, 2e6), (1e-3 * 2e6 + 2e-3 * 1e6) / (1e6 + 2e6); atol=TEST_TOL) - end - -end # End of test file diff --git a/test/unit_BaseParams/test_calc_equivalent_eps.jl b/test/unit_BaseParams/test_calc_equivalent_eps.jl deleted file mode 100644 index f5929d57e..000000000 --- a/test/unit_BaseParams/test_calc_equivalent_eps.jl +++ /dev/null @@ -1,77 +0,0 @@ -@testitem "BaseParams: calc_equivalent_eps unit tests" setup = [defaults] begin - @testset "Basic Functionality" begin - # Example from docstring: C_eq=1e-10 F/m, r_ext=0.01 m, r_in=0.005 m - result = calc_equivalent_eps(1e-10, 0.01, 0.005) - expected = (1e-10 * log(0.01 / 0.005)) / (2 * pi) / ε₀ - @test isapprox(result, expected; atol = TEST_TOL) - @test result > 0 - end - - @testset "Edge Cases" begin - # Zero capacitance - result = calc_equivalent_eps(0.0, 0.01, 0.005) - @test isapprox(result, 0.0; atol = TEST_TOL) - # Collapsing geometry: r_ext == r_in - result = calc_equivalent_eps(1e-10, 0.01, 0.01) - @test isapprox(result, 0.0; atol = TEST_TOL) - # Very large radii - result = calc_equivalent_eps(1e-10, 1e6, 1e3) - expected = (1e-10 * log(1e6 / 1e3)) / (2 * pi) / ε₀ - @test isapprox(result, expected; atol = TEST_TOL) - # Inf/NaN - @test isnan(calc_equivalent_eps(NaN, 0.01, 0.005)) - @test isnan(calc_equivalent_eps(1e-10, NaN, 0.005)) - @test isnan(calc_equivalent_eps(1e-10, 0.01, NaN)) - @test isinf(calc_equivalent_eps(Inf, 0.01, 0.005)) - end - - @testset "Numerical Consistency" begin - # Float32 vs Float64 - r = calc_equivalent_eps(Float32(1e-10), Float32(0.01), Float32(0.005)) - d = calc_equivalent_eps(1e-10, 0.01, 0.005) - @test isapprox(r, d; atol = 1e-6) - end - - @testset "Physical Behavior" begin - # Increases with capacitance - r1 = calc_equivalent_eps(1e-10, 0.01, 0.005) - r2 = calc_equivalent_eps(2e-10, 0.01, 0.005) - @test r2 > r1 - # Increases with log(r_ext/r_in) - r3 = calc_equivalent_eps(1e-10, 0.02, 0.005) - @test r3 > r1 - end - - @testset "Type Stability & Promotion" begin - using Measurements - # All Float64 - r1 = calc_equivalent_eps(1e-10, 0.01, 0.005) - @test typeof(r1) == Float64 - # All Measurement - r2 = calc_equivalent_eps( - measurement(1e-10, 1e-12), - measurement(0.01, 1e-5), - measurement(0.005, 1e-5), - ) - @test r2 isa Measurement{Float64} - # Mixed: C_eq as Measurement - r3 = calc_equivalent_eps(measurement(1e-10, 1e-12), 0.01, 0.005) - @test r3 isa Measurement{Float64} - # Mixed: r_ex as Measurement - r4 = calc_equivalent_eps(1e-10, measurement(0.01, 1e-5), 0.005) - @test r4 isa Measurement{Float64} - # Mixed: r_in as Measurement - r5 = calc_equivalent_eps(1e-10, 0.01, measurement(0.005, 1e-5)) - @test r5 isa Measurement{Float64} - end - - @testset "Uncertainty Quantification" begin - using Measurements - C_eq = measurement(1e-10, 1e-12) - r_ext = measurement(0.01, 1e-5) - r_in = measurement(0.005, 1e-5) - result = calc_equivalent_eps(C_eq, r_ext, r_in) - @test result isa Measurement{Float64} - @test uncertainty(result) > 0 - end -end diff --git a/test/unit_BaseParams/test_calc_equivalent_gmr.jl b/test/unit_BaseParams/test_calc_equivalent_gmr.jl deleted file mode 100644 index 5e6705321..000000000 --- a/test/unit_BaseParams/test_calc_equivalent_gmr.jl +++ /dev/null @@ -1,82 +0,0 @@ -@testitem "BaseParams: calc_equivalent_gmr unit tests" setup = - [defaults, deps_datamodel, defs_materials] begin - @testset "Basic Functionality" begin - # Example from docstring - material_props = Material(1.7241e-8, 1.0, 0.999994, 20.0, 0.00393) - strip = Strip(0.01, Thickness(0.002), 0.05, 10, material_props) - circstrands = CircStrands(0.02, 0.002, 7, 15, material_props) - gmr_eq = calc_equivalent_gmr(strip, circstrands) - @test gmr_eq > 0 - end - - @testset "Edge Cases" begin - # Identical layers (should reduce to geometric mean) - material_props = Material(1.7241e-8, 1.0, 0.999994, 20.0, 0.00393) - part1 = CircStrands(0.01, 0.002, 7, 10, material_props) - part2 = CircStrands(0.01, 0.002, 7, 10, material_props) - gmr_eq = calc_equivalent_gmr(part1, part2) - @test gmr_eq > 0 - # Very large cross-section for new_layer - big_layer = CircStrands(0.02, 0.002, 7, 1e6, material_props) - gmr_eq2 = calc_equivalent_gmr(part1, big_layer) - @test gmr_eq2 > 0 - end - - @testset "Numerical Consistency" begin - # Float32 vs Float64 - material_props = Material(1.7241e-8, 1.0, 0.999994, 20.0, 0.00393) - part1f32 = - CircStrands(Float32(0.01), Float32(0.002), 7, Float32(10), material_props) - part2f32 = - CircStrands(Float32(0.02), Float32(0.002), 7, Float32(15), material_props) - gmr_eq_f32 = calc_equivalent_gmr(part1f32, part2f32) - part1f64 = CircStrands(0.01, 0.002, 7, 10, material_props) - part2f64 = CircStrands(0.02, 0.002, 7, 15, material_props) - gmr_eq_f64 = calc_equivalent_gmr(part1f64, part2f64) - @test isapprox(gmr_eq_f32, gmr_eq_f64, atol = TEST_TOL) - end - - @testset "Physical Behavior" begin - # Equivalent GMR increases as GMD increases - material_props = Material(1.7241e-8, 1.0, 0.999994, 20.0, 0.00393) - part1 = CircStrands(0.01, 0.002, 7, 10, material_props) - part2 = CircStrands(0.02, 0.002, 7, 15, material_props) - part3 = CircStrands(0.03, 0.002, 7, 15, material_props) - gmr_eq1 = calc_equivalent_gmr(part1, part2) - gmr_eq2 = calc_equivalent_gmr(part1, part3) - @test gmr_eq2 > gmr_eq1 - end - - @testset "Type Stability & Promotion" begin - using Measurements - material_props = Material(1.7241e-8, 1.0, 0.999994, 20.0, 0.00393) - part1 = CircStrands(0.01, 0.002, 7, 10, material_props) - part2 = CircStrands(0.02, 0.002, 7, 15, material_props) - mpart1 = CircStrands(measurement(0.01, 1e-4), 0.002, 7, 10, material_props) - mpart2 = CircStrands(0.02, measurement(0.002, 1e-4), 7, 15, material_props) - # All Float64 - res1 = calc_equivalent_gmr(part1, part2) - @test typeof(res1) == Float64 - # All Measurement - res2 = calc_equivalent_gmr(mpart1, mpart2) - @test res2 isa Measurement{Float64} - # Mixed: first argument Measurement - res3 = calc_equivalent_gmr(mpart1, part2) - @test res3 isa Measurement{Float64} - # Mixed: second argument Measurement - res4 = calc_equivalent_gmr(part1, mpart2) - @test res4 isa Measurement{Float64} - end - - @testset "Uncertainty Quantification" begin - using Measurements - material_props = Material(1.7241e-8, 1.0, 0.999994, measurement(20, 10), 0.00393) - part1 = CircStrands(0.01, 0.002, 7, 10, material_props) - part2 = CircStrands(0.02, 0.002, 7, 15, material_props) - mpart1 = CircStrands(measurement(0.01, 1e-4), 0.002, 7, 10, material_props) - mpart2 = CircStrands(0.02, measurement(0.002, 1e-4), 7, 15, material_props) - gmr_eq = calc_equivalent_gmr(mpart1, mpart2) - @test gmr_eq isa Measurement{Float64} - @test uncertainty(gmr_eq) > 0 - end -end diff --git a/test/unit_BaseParams/test_calc_equivalent_lossfact.jl b/test/unit_BaseParams/test_calc_equivalent_lossfact.jl deleted file mode 100644 index 668ffe179..000000000 --- a/test/unit_BaseParams/test_calc_equivalent_lossfact.jl +++ /dev/null @@ -1,78 +0,0 @@ -@testitem "BaseParams: calc_equivalent_lossfact unit tests" setup = [defaults] begin - @testset "Basic Functionality" begin - # Example from docstring: G_eq=1e-8 S·m, C_eq=1e-10 F/m, ω=2π*50 - G_eq = 1e-8 - C_eq = 1e-10 - ω = 2 * pi * 50 - result = calc_equivalent_lossfact(G_eq, C_eq, ω) - expected = G_eq / (ω * C_eq) - @test isapprox(result, expected; atol=TEST_TOL) - @test result > 0 - end - - @testset "Edge Cases" begin - # Zero conductance - result = calc_equivalent_lossfact(0.0, 1e-10, 2 * pi * 50) - @test isapprox(result, 0.0; atol=TEST_TOL) - # Zero capacitance (should be Inf) - result = calc_equivalent_lossfact(1e-8, 0.0, 2 * pi * 50) - @test isinf(result) - # Zero frequency (should be Inf) - result = calc_equivalent_lossfact(1e-8, 1e-10, 0.0) - @test isinf(result) - # Inf/NaN - @test isnan(calc_equivalent_lossfact(NaN, 1e-10, 2 * pi * 50)) - @test isnan(calc_equivalent_lossfact(1e-8, NaN, 2 * pi * 50)) - @test isnan(calc_equivalent_lossfact(1e-8, 1e-10, NaN)) - @test isinf(calc_equivalent_lossfact(Inf, 1e-10, 2 * pi * 50)) - end - - @testset "Numerical Consistency" begin - # Float32 vs Float64 - r = calc_equivalent_lossfact(Float32(1e-8), Float32(1e-10), Float32(2 * pi * 50)) - d = calc_equivalent_lossfact(1e-8, 1e-10, 2 * pi * 50) - @test isapprox(r, d; atol=1e-6) - end - - @testset "Physical Behavior" begin - # Increases with G_eq - r1 = calc_equivalent_lossfact(1e-8, 1e-10, 2 * pi * 50) - r2 = calc_equivalent_lossfact(2e-8, 1e-10, 2 * pi * 50) - @test r2 > r1 - # Decreases with C_eq - r3 = calc_equivalent_lossfact(1e-8, 2e-10, 2 * pi * 50) - @test r3 < r1 - # Decreases with ω - r4 = calc_equivalent_lossfact(1e-8, 1e-10, 2 * pi * 100) - @test r4 < r1 - end - - @testset "Type Stability & Promotion" begin - using Measurements - # All Float64 - r1 = calc_equivalent_lossfact(1e-8, 1e-10, 2 * pi * 50) - @test typeof(r1) == Float64 - # All Measurement - r2 = calc_equivalent_lossfact(measurement(1e-8, 1e-10), measurement(1e-10, 1e-12), measurement(2 * pi * 50, 0.1)) - @test r2 isa Measurement{Float64} - # Mixed: G_eq as Measurement - r3 = calc_equivalent_lossfact(measurement(1e-8, 1e-10), 1e-10, 2 * pi * 50) - @test r3 isa Measurement{Float64} - # Mixed: C_eq as Measurement - r4 = calc_equivalent_lossfact(1e-8, measurement(1e-10, 1e-12), 2 * pi * 50) - @test r4 isa Measurement{Float64} - # Mixed: ω as Measurement - r5 = calc_equivalent_lossfact(1e-8, 1e-10, measurement(2 * pi * 50, 0.1)) - @test r5 isa Measurement{Float64} - end - - @testset "Uncertainty Quantification" begin - using Measurements - G_eq = measurement(1e-8, 1e-10) - C_eq = measurement(1e-10, 1e-12) - ω = measurement(2 * pi * 50, 0.1) - result = calc_equivalent_lossfact(G_eq, C_eq, ω) - @test result isa Measurement{Float64} - @test uncertainty(result) > 0 - end -end diff --git a/test/unit_BaseParams/test_calc_equivalent_mu.jl b/test/unit_BaseParams/test_calc_equivalent_mu.jl deleted file mode 100644 index 8b1672db3..000000000 --- a/test/unit_BaseParams/test_calc_equivalent_mu.jl +++ /dev/null @@ -1,103 +0,0 @@ -@testitem "BaseParams: calc_equivalent_mu unit tests" setup = [defaults] begin - @testset "Basic Functionality" begin - # Example from docstring - gmr = 0.015 - r_ex = 0.02 - r_in = 0.01 - mu_r = calc_equivalent_mu(gmr, r_ex, r_in) - @test isapprox(mu_r, 1.79409188, atol = TEST_TOL) - - # Solid conductor (r_in = 0) - radius_ext_solid = 0.0135 - radius_in_solid = 0.0 - gmr_solid = calc_tubular_gmr(radius_ext_solid, radius_in_solid, 1.0) - mu_r_solid = calc_equivalent_mu(gmr_solid, radius_ext_solid, radius_in_solid) - @test isapprox(mu_r_solid, 1.0, atol = TEST_TOL) - r_ex = -0.01 - r_in = 0.01 - @test_throws ArgumentError calc_equivalent_mu(gmr, r_ex, r_in) - end - - @testset "Edge Cases" begin - # Collapsing geometry: r_in -> r_ex, should be 0 if == gmr - gmr = 0.02 - r_ex = 0.02 - r_in = 0.02 - mu_r = calc_equivalent_mu(gmr, r_ex, r_in) - @test isapprox(mu_r, 0.0, atol = TEST_TOL) - - # Very large radii - gmr = 1e3 - r_ex = 1e3 - r_in = 1e2 - mu_r = calc_equivalent_mu(gmr, r_ex, r_in) - @test isfinite(mu_r) - - # Inf/NaN input - @test isnan(calc_equivalent_mu(NaN, 0.02, 0.01)) - @test isnan(calc_equivalent_mu(0.015, NaN, 0.01)) - @test isnan(calc_equivalent_mu(0.015, 0.02, NaN)) - end - - @testset "Numerical Consistency" begin - # Float32 vs Float64 - gmr = Float32(0.015) - r_ex = Float32(0.02) - r_in = Float32(0.01) - mu_r_f32 = calc_equivalent_mu(gmr, r_ex, r_in) - mu_r_f64 = calc_equivalent_mu(Float64(gmr), Float64(r_ex), Float64(r_in)) - @test isapprox(mu_r_f32, mu_r_f64, atol = TEST_TOL) - end - - @testset "Physical Behavior" begin - # mu_r increases as gmr decreases (for fixed radii) - mu1 = calc_equivalent_mu(0.015, 0.02, 0.01) - mu2 = calc_equivalent_mu(0.012, 0.02, 0.01) - @test mu2 > mu1 - # mu_r decreases as gmr increases - mu3 = calc_equivalent_mu(0.018, 0.02, 0.01) - @test mu3 < mu1 - end - - @testset "Type Stability & Promotion" begin - using Measurements - gmr = 0.015 - r_ex = 0.02 - r_in = 0.01 - mgmr = measurement(gmr, 1e-4) - mrex = measurement(r_ex, 1e-4) - mrin = measurement(r_in, 1e-4) - - # All Float64 - res1 = calc_equivalent_mu(gmr, r_ex, r_in) - @test typeof(res1) == Float64 - # All Measurement - res2 = calc_equivalent_mu(mgmr, mrex, mrin) - @test res2 isa Measurement{Float64} - # Mixed: first argument Measurement - res3 = calc_equivalent_mu(mgmr, r_ex, r_in) - @test res3 isa Measurement{Float64} - # Mixed: second argument Measurement - res4 = calc_equivalent_mu(gmr, mrex, r_in) - @test res4 isa Measurement{Float64} - # Mixed: third argument Measurement - res5 = calc_equivalent_mu(gmr, r_ex, mrin) - @test res5 isa Measurement{Float64} - end - - @testset "Uncertainty Quantification" begin - using Measurements - gmr = measurement(0.015, 1e-4) - r_ex = measurement(0.02, 1e-4) - r_in = measurement(0.01, 1e-4) - mu_r = calc_equivalent_mu(gmr, r_ex, r_in) - # Should propagate uncertainty - @test mu_r isa Measurement{Float64} - @test uncertainty(mu_r) > 0 - end - - @testset "Error Handling" begin - # Only error thrown is for r_ex < r_in - @test_throws ArgumentError calc_equivalent_mu(0.015, 0.01, 0.02) - end -end diff --git a/test/unit_BaseParams/test_calc_equivalent_rho.jl b/test/unit_BaseParams/test_calc_equivalent_rho.jl deleted file mode 100644 index 3a2203147..000000000 --- a/test/unit_BaseParams/test_calc_equivalent_rho.jl +++ /dev/null @@ -1,73 +0,0 @@ -@testitem "BaseParams: calc_equivalent_rho unit tests" setup = [defaults] begin - @testset "Basic Functionality" begin - # Example from docstring: R=0.01 Ω, r_ext=0.02 m, r_in=0.01 m - result = calc_equivalent_rho(0.01, 0.02, 0.01) - expected = 0.01 * π * (0.02^2 - 0.01^2) - @test isapprox(result, expected; atol=TEST_TOL) - @test result > 0 - end - - @testset "Edge Cases" begin - # Zero resistance - result = calc_equivalent_rho(0.0, 0.02, 0.01) - @test isapprox(result, 0.0; atol=TEST_TOL) - # Zero thickness (r_ext == r_in) - result = calc_equivalent_rho(0.01, 0.01, 0.01) - @test isapprox(result, 0.0; atol=TEST_TOL) - # Very large radii - result = calc_equivalent_rho(0.01, 1e6, 1e3) - expected = 0.01 * π * (1e6^2 - 1e3^2) - @test isapprox(result, expected; atol=TEST_TOL) - # Inf/NaN - @test isnan(calc_equivalent_rho(NaN, 0.02, 0.01)) - @test isnan(calc_equivalent_rho(0.01, NaN, 0.01)) - @test isnan(calc_equivalent_rho(0.01, 0.02, NaN)) - @test isinf(calc_equivalent_rho(Inf, 0.02, 0.01)) - end - - @testset "Numerical Consistency" begin - # Float32 vs Float64 - r = calc_equivalent_rho(Float32(0.01), Float32(0.02), Float32(0.01)) - d = calc_equivalent_rho(0.01, 0.02, 0.01) - @test isapprox(r, d; atol=TEST_TOL) - end - - @testset "Physical Behavior" begin - # Increases with resistance - r1 = calc_equivalent_rho(0.01, 0.02, 0.01) - r2 = calc_equivalent_rho(0.02, 0.02, 0.01) - @test r2 > r1 - # Increases with area - r3 = calc_equivalent_rho(0.01, 0.03, 0.01) - @test r3 > r1 - end - - @testset "Type Stability & Promotion" begin - using Measurements - # All Float64 - r1 = calc_equivalent_rho(0.01, 0.02, 0.01) - @test typeof(r1) == Float64 - # All Measurement - r2 = calc_equivalent_rho(measurement(0.01, 1e-4), measurement(0.02, 1e-5), measurement(0.01, 1e-5)) - @test r2 isa Measurement{Float64} - # Mixed: R as Measurement - r3 = calc_equivalent_rho(measurement(0.01, 1e-4), 0.02, 0.01) - @test r3 isa Measurement{Float64} - # Mixed: radius_ext_con as Measurement - r4 = calc_equivalent_rho(0.01, measurement(0.02, 1e-5), 0.01) - @test r4 isa Measurement{Float64} - # Mixed: radius_in_con as Measurement - r5 = calc_equivalent_rho(0.01, 0.02, measurement(0.01, 1e-5)) - @test r5 isa Measurement{Float64} - end - - @testset "Uncertainty Quantification" begin - using Measurements - R = measurement(0.01, 1e-4) - r_ext = measurement(0.02, 1e-5) - r_in = measurement(0.01, 1e-5) - result = calc_equivalent_rho(R, r_ext, r_in) - @test result isa Measurement{Float64} - @test uncertainty(result) > 0 - end -end diff --git a/test/unit_BaseParams/test_calc_gmd.jl b/test/unit_BaseParams/test_calc_gmd.jl deleted file mode 100644 index 8a0bce490..000000000 --- a/test/unit_BaseParams/test_calc_gmd.jl +++ /dev/null @@ -1,105 +0,0 @@ -@testitem "BaseParams: calc_gmd unit tests" setup = - [defaults, deps_datamodel, defs_materials] begin - @testset "Basic Functionality" begin - material_props = Material(1.7241e-8, 1.0, 0.999994, 20.0, 0.00393) - circ_strands = - CircStrands(0.01, Diameter(0.002), 7, 10, material_props, temperature = 25) - tubular = Tubular(0.01, 0.02, material_props, temperature = 25) - gmd = calc_gmd(circ_strands, tubular) - @test gmd > 0 - # Symmetry - gmd2 = calc_gmd(tubular, circ_strands) - @test isapprox(gmd, gmd2, atol = TEST_TOL) - end - - @testset "Edge Cases" begin - material_props = Material(1.7241e-8, 1.0, 0.999994, 20.0, 0.00393) - # Identical objects (should return outer radius) - tubular = Tubular(0.01, 0.02, material_props, temperature = 25) - gmd_same = calc_gmd(tubular, tubular) - @test isapprox(gmd_same, 0.02, atol = TEST_TOL) - # CircStrands with itself - circ_strands = - CircStrands(0.01, Diameter(0.002), 7, 10, material_props, temperature = 25) - gmd_wa = calc_gmd(circ_strands, circ_strands) - @test gmd_wa > 0 - end - - @testset "Numerical Consistency" begin - material_props = Material(1.7241e-8, 1.0, 0.999994, 20.0, 0.00393) - circ_strands_f32 = CircStrands( - Float32(0.01), - Diameter(Float32(0.002)), - 7, - Float32(10), - material_props, - temperature = 25, - ) - tubular_f32 = - Tubular(Float32(0.01), Float32(0.02), material_props, temperature = 25) - gmd_f32 = calc_gmd(circ_strands_f32, tubular_f32) - circ_strands_f64 = - CircStrands(0.01, Diameter(0.002), 7, 10, material_props, temperature = 25) - tubular_f64 = Tubular(0.01, 0.02, material_props, temperature = 25) - gmd_f64 = calc_gmd(circ_strands_f64, tubular_f64) - @test isapprox(gmd_f32, gmd_f64, atol = TEST_TOL) - end - - @testset "Physical Behavior" begin - material_props = Material(1.7241e-8, 1.0, 0.999994, 20.0, 0.00393) - wa1 = CircStrands(0.01, Diameter(0.002), 7, 10, material_props, temperature = 25) - wa2 = CircStrands(0.02, Diameter(0.002), 7, 10, material_props, temperature = 25) - tubular = Tubular(0.01, 0.02, material_props, temperature = 25) - gmd1 = calc_gmd(wa1, tubular) - gmd2 = calc_gmd(wa2, tubular) - @test gmd2 > gmd1 - end - - @testset "Type Stability & Promotion" begin - material_props = Material(1.7241e-8, 1.0, 0.999994, 20.0, 0.00393) - wa = CircStrands(0.01, Diameter(0.002), 7, 10, material_props, temperature = 25) - tub = Tubular(0.01, 0.02, material_props, temperature = 25) - mwa = CircStrands( - 0.01, - Diameter(measurement(0.002, 1e-4)), - 7, - 10, - material_props, - temperature = 25, - ) - mtub = Tubular(0.01, measurement(0.02, 1e-4), material_props, temperature = 25) - # All Float64 - res1 = calc_gmd(wa, tub) - @test typeof(res1) == Float64 - # All Measurement - res2 = calc_gmd(mwa, mtub) - @test res2 isa Measurement{Float64} - # Mixed: first argument Measurement - res3 = calc_gmd(mwa, tub) - @test res3 isa Measurement{Float64} - # Mixed: second argument Measurement - res4 = calc_gmd(wa, mtub) - @test res4 isa Measurement{Float64} - mtub_temp = Tubular(0.01, 0.02, material_props, temperature = measurement(25, 1e-4)) - res5 = calc_gmd(wa, mtub_temp) - @test res5 isa Measurement{Float64} - end - - @testset "Uncertainty Quantification" begin - material_props = Material(1.7241e-8, 1.0, 0.999994, 20.0, 0.00393) - wa = CircStrands(0.01, Diameter(0.002), 7, 10, material_props, temperature = 25) - tub = Tubular(0.01, 0.02, material_props, temperature = 25) - mwa = CircStrands( - measurement(0.01, 1e-4), - Diameter(0.002), - 7, - 10, - material_props, - temperature = 25, - ) - mtub = Tubular(0.01, measurement(0.02, 1e-4), material_props, temperature = 25) - gmd = calc_gmd(mwa, mtub) - @test gmd isa Measurement{Float64} - @test uncertainty(gmd) > 0 - end -end diff --git a/test/unit_BaseParams/test_calc_helical_params.jl b/test/unit_BaseParams/test_calc_helical_params.jl deleted file mode 100644 index f32e08bc2..000000000 --- a/test/unit_BaseParams/test_calc_helical_params.jl +++ /dev/null @@ -1,94 +0,0 @@ -@testitem "BaseParams: calc_helical_params unit tests" setup = [defaults] begin - using Measurements - # --- Basic Functionality --- - @testset "Basic Functionality" begin - r_in = 0.01 - r_ex = 0.015 - lay_ratio = 12.0 - mean_diam, pitch, overlength = calc_helical_params(r_in, r_ex, lay_ratio) - @test isapprox(mean_diam, 0.025, atol = TEST_TOL) - @test isapprox(pitch, 0.3, atol = TEST_TOL) - @test overlength > 1.0 - end - - # --- Edge Cases --- - @testset "Edge Cases" begin - # Zero lay ratio (pitch_length = 0) - m, p, o = calc_helical_params(0.01, 0.015, 0.0) - @test isapprox(m, 0.025, atol = TEST_TOL) - @test isapprox(p, 0.0, atol = TEST_TOL) - @test isapprox(o, 1.0, atol = TEST_TOL) - - # Collapsing geometry (r_in == r_ex) - m2, p2, o2 = calc_helical_params(0.02, 0.02, 10.0) - @test isapprox(m2, 0.04, atol = TEST_TOL) - @test isapprox(p2, 0.4, atol = TEST_TOL) - @test o2 > 1.0 - - # Very large lay ratio - m3, p3, o3 = calc_helical_params(0.01, 0.015, 1e6) - @test isapprox(m3, 0.025, atol = TEST_TOL) - @test isapprox(p3, 25000.0, atol = TEST_TOL) - @test isapprox(o3, 1.0, atol = TEST_TOL) - end - - # --- Numerical Consistency --- - @testset "Numerical Consistency" begin - # Float32 - m, p, o = calc_helical_params(Float32(0.01), Float32(0.015), Float32(12.0)) - @test isapprox(m, 0.025, atol = TEST_TOL) - @test isapprox(p, 0.3, atol = TEST_TOL) - @test o > 1.0 - end - - # --- Physical Behavior --- - @testset "Physical Behavior" begin - # Increasing lay_ratio increases pitch_length - _, p1, _ = calc_helical_params(0.01, 0.015, 10.0) - _, p2, _ = calc_helical_params(0.01, 0.015, 20.0) - @test p2 > p1 - # Overlength approaches 1 as lay_ratio increases - _, _, o1 = calc_helical_params(0.01, 0.015, 1e3) - @test isapprox(o1, 1.0, atol = 1e-5) - end - - # --- Type Stability & Promotion --- - @testset "Type Stability & Promotion" begin - # All Float64 - m, p, o = calc_helical_params(0.01, 0.015, 12.0) - @test typeof(m) == Float64 - @test typeof(p) == Float64 - @test typeof(o) == Float64 - # All Measurement - mM, pM, oM = calc_helical_params( - measurement(0.01, 1e-5), - measurement(0.015, 1e-5), - measurement(12.0, 0.1), - ) - @test mM isa Measurement{Float64} - @test pM isa Measurement{Float64} - @test oM isa Measurement{Float64} - # Mixed: r_in as Measurement - m1, p1, o1 = calc_helical_params(measurement(0.01, 1e-5), 0.015, 12.0) - @test m1 isa Measurement{Float64} - @test p1 isa Measurement{Float64} - @test o1 isa Measurement{Float64} - # Mixed: lay_ratio as Measurement - m2, p2, o2 = calc_helical_params(0.01, 0.015, measurement(12.0, 0.1)) - @test m2 isa Measurement{Float64} - @test p2 isa Measurement{Float64} - @test o2 isa Measurement{Float64} - end - - # --- Uncertainty Quantification --- - @testset "Uncertainty Quantification" begin - rin = measurement(0.01, 1e-5) - rext = measurement(0.015, 1e-5) - lrat = measurement(12.0, 0.1) - m, p, o = calc_helical_params(rin, rext, lrat) - # Check propagated uncertainties are nonzero - @test uncertainty(m) > 0 - @test uncertainty(p) > 0 - @test uncertainty(o) > 0 - end -end diff --git a/test/unit_BaseParams/test_calc_inductance_trifoil.jl b/test/unit_BaseParams/test_calc_inductance_trifoil.jl deleted file mode 100644 index 87f3e4e9f..000000000 --- a/test/unit_BaseParams/test_calc_inductance_trifoil.jl +++ /dev/null @@ -1,88 +0,0 @@ -# test/unit_BaseParams/test_calc_inductance_trifoil.jl - -@testitem "BaseParams: calc_inductance_trifoil unit tests" setup = [defaults] begin - - #= - ## Test Case Setup - Parameters are explicitly separated into positional and keyword arguments - to match the function signature. This makes all test calls clean and robust. - =# - const CANONICAL_POS_ARGS = ( - r_in_co=10e-3, - r_ext_co=15e-3, - rho_co=1.72e-8, - mu_r_co=1.0, - r_in_scr=20e-3, - r_ext_scr=25e-3, - rho_scr=2.82e-8, - mu_r_scr=1.0, - S=100e-3, - ) - - const CANONICAL_KW_ARGS = (rho_e=100.0, f=50.0) - - @testset "Basic functionality: canonical example" begin - L = calc_inductance_trifoil(values(CANONICAL_POS_ARGS)...; CANONICAL_KW_ARGS...) - expected_L = 1.573964832699787e-7 # H/m - @test L ≈ expected_L atol = TEST_TOL - end - - @testset "Physical behavior" begin - L_base = calc_inductance_trifoil(values(CANONICAL_POS_ARGS)...; CANONICAL_KW_ARGS...) - - pos_args_better_screen = merge(CANONICAL_POS_ARGS, (rho_scr=CANONICAL_POS_ARGS.rho_scr / 10,)) - L_better_screen = calc_inductance_trifoil(values(pos_args_better_screen)...; CANONICAL_KW_ARGS...) - @test L_better_screen < L_base - - pos_args_higher_mu = merge(CANONICAL_POS_ARGS, (mu_r_co=CANONICAL_POS_ARGS.mu_r_co * 2,)) - L_higher_mu = calc_inductance_trifoil(values(pos_args_higher_mu)...; CANONICAL_KW_ARGS...) - @test L_higher_mu > L_base - - # Override a keyword argument directly in the call - L_60Hz = calc_inductance_trifoil(values(CANONICAL_POS_ARGS)...; CANONICAL_KW_ARGS..., f=60.0) - @test L_60Hz < L_base - end - - @testset "Edge cases" begin - - pos_args_solid_core = merge(CANONICAL_POS_ARGS, (r_in_co=0.0,)) - L_solid_core = calc_inductance_trifoil(values(pos_args_solid_core)...; CANONICAL_KW_ARGS...) - @test isfinite(L_solid_core) - @test L_solid_core > 0.0 - - pos_args_perfect_screen = merge(CANONICAL_POS_ARGS, (rho_scr=0.0,)) - L_perfect_screen = calc_inductance_trifoil(values(pos_args_perfect_screen)...; CANONICAL_KW_ARGS...) - @test isfinite(L_perfect_screen) - @test L_perfect_screen < calc_inductance_trifoil(values(CANONICAL_POS_ARGS)...; CANONICAL_KW_ARGS...) - end - - @testset "Type stability and promotion with Measurements.jl" begin - # Base values - p_pos = CANONICAL_POS_ARGS - p_kw = CANONICAL_KW_ARGS - - # Create Measurement versions - p_pos_meas = map(x -> x ± (x * 0.01), p_pos) - p_kw_meas = map(x -> x ± (x * 0.01), p_kw) - - # Float case - L_float = calc_inductance_trifoil(values(p_pos)...; p_kw...) - @test L_float isa Float64 - - # Fully promoted case - L_meas_all = calc_inductance_trifoil(values(p_pos_meas)...; p_kw_meas...) - @test L_meas_all isa Measurement{Float64} - @test L_meas_all.val ≈ L_float atol = TEST_TOL - @test L_meas_all.err > 0.0 - - # Mixed case (manual call for clarity) - L_meas_rho_e = calc_inductance_trifoil( - p_pos.r_in_co ± 0.0, p_pos.r_ext_co, p_pos.rho_co, p_pos.mu_r_co, - p_pos.r_in_scr, p_pos.r_ext_scr, p_pos.rho_scr, p_pos.mu_r_scr, p_pos.S; - rho_e=p_kw.rho_e ± 10.0, f=p_kw.f - ) - @test L_meas_rho_e isa Measurement{Float64} - @test L_meas_rho_e.val ≈ L_float atol = TEST_TOL - @test L_meas_rho_e.err > 0.0 - end -end \ No newline at end of file diff --git a/test/unit_BaseParams/test_calc_parallel_equivalent.jl b/test/unit_BaseParams/test_calc_parallel_equivalent.jl deleted file mode 100644 index 64bf971ee..000000000 --- a/test/unit_BaseParams/test_calc_parallel_equivalent.jl +++ /dev/null @@ -1,123 +0,0 @@ -@testitem "BaseParams: calc_parallel_equivalent unit tests" setup = [defaults] begin - - @testset "Basic Functionality" begin - # Test with real numbers (Float64) - Z1_real = 5.0 - Z2_real = 10.0 - expected_real = 1 / (1 / Z1_real + 1 / Z2_real) - result_real = calc_parallel_equivalent(Z1_real, Z2_real) - @test isapprox(result_real, expected_real; atol=TEST_TOL) - @test isapprox(result_real, 3.3333333333333335; atol=TEST_TOL) - - # Test with complex numbers (Complex{Float64}) - Z1_complex = 3.0 + 4.0im - Z2_complex = 8.0 - 6.0im - expected_complex = 1 / (1 / Z1_complex + 1 / Z2_complex) - @test isapprox(calc_parallel_equivalent(Z1_complex, Z2_complex), expected_complex; atol=TEST_TOL) - end - - @testset "Edge Cases" begin - # Zero impedance (short circuit) - @test isapprox(calc_parallel_equivalent(0.0, 10.0), 0.0; atol=TEST_TOL) - @test isapprox(calc_parallel_equivalent(10.0, 0.0), 0.0; atol=TEST_TOL) - @test isapprox(calc_parallel_equivalent(0.0, 0.0), 0.0; atol=TEST_TOL) - @test isapprox(calc_parallel_equivalent(0.0 + 0.0im, 5.0 + 5.0im), 0.0 + 0.0im; atol=TEST_TOL) - - # Infinite impedance (open circuit) - @test isapprox(calc_parallel_equivalent(Inf, 10.0), 10.0; atol=TEST_TOL) - @test isapprox(calc_parallel_equivalent(10.0, Inf), 10.0; atol=TEST_TOL) - @test isapprox(calc_parallel_equivalent(Inf, Inf), Inf; atol=TEST_TOL) - - # NaN propagation - @test isnan(calc_parallel_equivalent(NaN, 10.0)) - @test isnan(calc_parallel_equivalent(10.0, NaN)) - - # Equal and opposite impedances (Z1 = -Z2), leading to singularity - result_inf = calc_parallel_equivalent(10.0, -10.0) - @test isinf(real(result_inf)) - result_nan = calc_parallel_equivalent(3.0 + 4.0im, -3.0 - 4.0im) - @test isnan(real(result_nan)) && isnan(imag(result_nan)) - end - - @testset "Numerical Consistency" begin - Z1f = 5.0 - Z2f = 10.0 - resultf = calc_parallel_equivalent(Z1f, Z2f) - @test resultf isa Float64 - @test isapprox(resultf, 3.33333333; atol=TEST_TOL) - end - - @testset "Physical Behavior" begin - # Parallel resistance is always less than the smallest individual resistance - @test calc_parallel_equivalent(10.0, 20.0) < 10.0 - - # Symmetry: calc_parallel_equivalent(Z1, Z2) == calc_parallel_equivalent(Z2, Z1) - @test isapprox(calc_parallel_equivalent(7.0, 13.0), calc_parallel_equivalent(13.0, 7.0); atol=TEST_TOL) - - # If Z1 == Z2, the result is Z1 / 2 - @test isapprox(calc_parallel_equivalent(8.0, 8.0), 4.0; atol=TEST_TOL) - end - - @testset "Type Stability & Promotion" begin - # Both Float64 -> Float64 - @test calc_parallel_equivalent(5.0, 10.0) isa Float64 - - # Int and Float64 -> Float64 - result_mixed_real = calc_parallel_equivalent(5, 10.0) - @test result_mixed_real isa Float64 - @test isapprox(result_mixed_real, 1 / (1 / 5.0 + 1 / 10.0); atol=TEST_TOL) - - # Float64 and Complex{Float64} -> Complex{Float64} - result_mixed_complex = calc_parallel_equivalent(10.0, 3.0 + 4.0im) - @test result_mixed_complex isa Complex{Float64} - expected_mixed_complex = 1 / (1 / (10.0 + 0.0im) + 1 / (3.0 + 4.0im)) - @test isapprox(result_mixed_complex, expected_mixed_complex; atol=TEST_TOL) - - # Both Measurement -> Measurement - Z1m = measurement(5.0, 0.1) - Z2m = measurement(10.0, 0.2) - @test calc_parallel_equivalent(Z1m, Z2m) isa Measurement - - # Mixed: Measurement and Float64 -> Measurement - @test calc_parallel_equivalent(Z1m, 10.0) isa Measurement - @test calc_parallel_equivalent(5.0, Z2m) isa Measurement - end - - @testset "Uncertainty Quantification with Measurements.jl" begin - # Mixed Case 1: First argument is a Measurement - Z1_meas = measurement(5.0, 0.1) - Z2_float = 10.0 - result_mixed1 = calc_parallel_equivalent(Z1_meas, Z2_float) - expected_mixed1 = 1 / (1 / Z1_meas + 1 / Z2_float) - @test result_mixed1 isa Measurement{Float64} - @test isapprox(value(result_mixed1), value(expected_mixed1); atol=TEST_TOL) - @test isapprox(uncertainty(result_mixed1), uncertainty(expected_mixed1); atol=TEST_TOL) - - # Mixed Case 2: Second argument is a Measurement - Z1_float = 5.0 - Z2_meas = measurement(10.0, 0.2) - result_mixed2 = calc_parallel_equivalent(Z1_float, Z2_meas) - expected_mixed2 = 1 / (1 / Z1_float + 1 / Z2_meas) - @test result_mixed2 isa Measurement{Float64} - @test isapprox(value(result_mixed2), value(expected_mixed2); atol=TEST_TOL) - @test isapprox(uncertainty(result_mixed2), uncertainty(expected_mixed2); atol=TEST_TOL) - - # Fully Promoted Case: Both inputs are Measurements - result_full_meas = calc_parallel_equivalent(Z1_meas, Z2_meas) - expected_full_meas = 1 / (1 / Z1_meas + 1 / Z2_meas) - @test result_full_meas isa Measurement{Float64} - @test isapprox(value(result_full_meas), value(expected_full_meas); atol=TEST_TOL) - @test isapprox(uncertainty(result_full_meas), uncertainty(expected_full_meas); atol=TEST_TOL) - - # Fully Promoted Complex Case - Z1_cplx_meas = measurement(3.0, 0.1) + measurement(4.0, 0.2)im - Z2_cplx_meas = measurement(8.0, 0.3) - measurement(6.0, 0.4)im - result_cplx_meas = calc_parallel_equivalent(Z1_cplx_meas, Z2_cplx_meas) - expected_cplx_meas = 1 / (1 / Z1_cplx_meas + 1 / Z2_cplx_meas) - @test result_cplx_meas isa Complex{Measurement{Float64}} - @test isapprox(value(real(result_cplx_meas)), value(real(expected_cplx_meas)); atol=TEST_TOL) - @test isapprox(value(imag(result_cplx_meas)), value(imag(expected_cplx_meas)); atol=TEST_TOL) - @test isapprox(uncertainty(real(result_cplx_meas)), uncertainty(real(expected_cplx_meas)); atol=TEST_TOL) - @test isapprox(uncertainty(imag(result_cplx_meas)), uncertainty(imag(expected_cplx_meas)); atol=TEST_TOL) - end -end \ No newline at end of file diff --git a/test/unit_BaseParams/test_calc_shunt_capacitance.jl b/test/unit_BaseParams/test_calc_shunt_capacitance.jl deleted file mode 100644 index f8d48d4cc..000000000 --- a/test/unit_BaseParams/test_calc_shunt_capacitance.jl +++ /dev/null @@ -1,78 +0,0 @@ -@testitem "BaseParams: calc_shunt_capacitance unit tests" setup = [defaults] begin - @testset "Basic Functionality" begin - # Example from docstring - r_in = 0.01 - r_ex = 0.02 - epsr = 2.3 - cap = calc_shunt_capacitance(r_in, r_ex, epsr) - @test isapprox(cap, 1.241e-10, atol = TEST_TOL) - # Vacuum (epsr = 1) - cap_vac = calc_shunt_capacitance(0.01, 0.02, 1.0) - @test cap_vac < cap - end - - @testset "Edge Cases" begin - # Collapsing geometry: r_in -> r_ex - cap = calc_shunt_capacitance(0.02, 0.02, 2.3) - @test isinf(cap) || isnan(cap) - # Very large radii - cap = calc_shunt_capacitance(1e2, 1e3, 2.3) - @test isfinite(cap) - # Inf/NaN input - @test isnan(calc_shunt_capacitance(NaN, 0.02, 2.3)) - @test isnan(calc_shunt_capacitance(0.01, NaN, 2.3)) - @test isnan(calc_shunt_capacitance(0.01, 0.02, NaN)) - end - - @testset "Numerical Consistency" begin - # Float32 vs Float64 - cap_f32 = calc_shunt_capacitance(Float32(0.01), Float32(0.02), Float32(2.3)) - cap_f64 = calc_shunt_capacitance(0.01, 0.02, 2.3) - @test isapprox(cap_f32, cap_f64, atol = TEST_TOL) - end - - @testset "Physical Behavior" begin - # Capacitance increases with epsr - c1 = calc_shunt_capacitance(0.01, 0.02, 2.3) - c2 = calc_shunt_capacitance(0.01, 0.02, 3.0) - @test c2 > c1 - # Capacitance decreases as radii get closer - c3 = calc_shunt_capacitance(0.01, 0.011, 2.3) - @test c3 > c1 - end - - @testset "Type Stability & Promotion" begin - using Measurements - r_in = 0.01 - r_ex = 0.02 - epsr = 2.3 - min = measurement(r_in, 1e-4) - mex = measurement(r_ex, 1e-4) - mepsr = measurement(epsr, 1e-2) - # All Float64 - res1 = calc_shunt_capacitance(r_in, r_ex, epsr) - @test typeof(res1) == Float64 - # All Measurement - res2 = calc_shunt_capacitance(min, mex, mepsr) - @test res2 isa Measurement{Float64} - # Mixed: first argument Measurement - res3 = calc_shunt_capacitance(min, r_ex, epsr) - @test res3 isa Measurement{Float64} - # Mixed: second argument Measurement - res4 = calc_shunt_capacitance(r_in, mex, epsr) - @test res4 isa Measurement{Float64} - # Mixed: third argument Measurement - res5 = calc_shunt_capacitance(r_in, r_ex, mepsr) - @test res5 isa Measurement{Float64} - end - - @testset "Uncertainty Quantification" begin - using Measurements - min = measurement(0.01, 1e-4) - mex = measurement(0.02, 1e-4) - mepsr = measurement(2.3, 1e-2) - cap = calc_shunt_capacitance(min, mex, mepsr) - @test cap isa Measurement{Float64} - @test uncertainty(cap) > 0 - end -end diff --git a/test/unit_BaseParams/test_calc_shunt_conductance.jl b/test/unit_BaseParams/test_calc_shunt_conductance.jl deleted file mode 100644 index aa6b96222..000000000 --- a/test/unit_BaseParams/test_calc_shunt_conductance.jl +++ /dev/null @@ -1,78 +0,0 @@ -@testitem "BaseParams: calc_shunt_conductance unit tests" setup = [defaults] begin - @testset "Basic Functionality" begin - # Example from docstring - r_in = 0.01 - r_ex = 0.02 - rho = 1e9 - g = calc_shunt_conductance(r_in, r_ex, rho) - @test isapprox(g, 2.7169e-9, atol = TEST_TOL) - # Lower resistivity increases conductance - g2 = calc_shunt_conductance(0.01, 0.02, 1e8) - @test g2 > g - end - - @testset "Edge Cases" begin - # Collapsing geometry: r_in -> r_ex - g = calc_shunt_conductance(0.02, 0.02, 1e9) - @test isinf(g) || isnan(g) - # Very large radii - g = calc_shunt_conductance(1e2, 1e3, 1e9) - @test isfinite(g) - # Inf/NaN input - @test isnan(calc_shunt_conductance(NaN, 0.02, 1e9)) - @test isnan(calc_shunt_conductance(0.01, NaN, 1e9)) - @test isnan(calc_shunt_conductance(0.01, 0.02, NaN)) - end - - @testset "Numerical Consistency" begin - # Float32 vs Float64 - g_f32 = calc_shunt_conductance(Float32(0.01), Float32(0.02), Float32(1e9)) - g_f64 = calc_shunt_conductance(0.01, 0.02, 1e9) - @test isapprox(g_f32, g_f64, atol = TEST_TOL) - end - - @testset "Physical Behavior" begin - # Conductance increases as rho decreases - g1 = calc_shunt_conductance(0.01, 0.02, 1e9) - g2 = calc_shunt_conductance(0.01, 0.02, 1e8) - @test g2 > g1 - # Conductance increases as radii get closer - g3 = calc_shunt_conductance(0.01, 0.011, 1e9) - @test g3 > g1 - end - - @testset "Type Stability & Promotion" begin - using Measurements - r_in = 0.01 - r_ex = 0.02 - rho = 1e9 - min = measurement(r_in, 1e-4) - mex = measurement(r_ex, 1e-4) - mrho = measurement(rho, 1e7) - # All Float64 - res1 = calc_shunt_conductance(r_in, r_ex, rho) - @test typeof(res1) == Float64 - # All Measurement - res2 = calc_shunt_conductance(min, mex, mrho) - @test res2 isa Measurement{Float64} - # Mixed: first argument Measurement - res3 = calc_shunt_conductance(min, r_ex, rho) - @test res3 isa Measurement{Float64} - # Mixed: second argument Measurement - res4 = calc_shunt_conductance(r_in, mex, rho) - @test res4 isa Measurement{Float64} - # Mixed: third argument Measurement - res5 = calc_shunt_conductance(r_in, r_ex, mrho) - @test res5 isa Measurement{Float64} - end - - @testset "Uncertainty Quantification" begin - using Measurements - min = measurement(0.01, 1e-4) - mex = measurement(0.02, 1e-4) - mrho = measurement(1e9, 1e7) - g = calc_shunt_conductance(min, mex, mrho) - @test g isa Measurement{Float64} - @test uncertainty(g) > 0 - end -end diff --git a/test/unit_BaseParams/test_calc_sigma_lossfact.jl b/test/unit_BaseParams/test_calc_sigma_lossfact.jl deleted file mode 100644 index 960c77d5a..000000000 --- a/test/unit_BaseParams/test_calc_sigma_lossfact.jl +++ /dev/null @@ -1,80 +0,0 @@ -@testitem "BaseParams: calc_sigma_lossfact unit tests" setup = [defaults] begin - @testset "Basic Functionality" begin - # Example from docstring: G_eq=2.7169e-9 S·m, r_in=0.01 m, r_ext=0.02 m - G_eq = 2.7169e-9 - r_in = 0.01 - r_ext = 0.02 - result = calc_sigma_lossfact(G_eq, r_in, r_ext) - expected = G_eq * log(r_ext / r_in) / (2 * pi) - @test isapprox(result, expected; atol = TEST_TOL) - @test result > 0 - end - - @testset "Edge Cases" begin - # Zero conductance - result = calc_sigma_lossfact(0.0, 0.01, 0.02) - @test isapprox(result, 0.0; atol = TEST_TOL) - # Collapsing geometry: r_ext == r_in - result = calc_sigma_lossfact(1e-9, 0.01, 0.01) - @test isapprox(result, 0.0; atol = TEST_TOL) - # Very large radii - result = calc_sigma_lossfact(1e-9, 1e3, 1e6) - expected = 1e-9 * log(1e6 / 1e3) / (2 * pi) - @test isapprox(result, expected; atol = TEST_TOL) - # Inf/NaN - @test isnan(calc_sigma_lossfact(NaN, 0.01, 0.02)) - @test isnan(calc_sigma_lossfact(1e-9, NaN, 0.02)) - @test isnan(calc_sigma_lossfact(1e-9, 0.01, NaN)) - @test isinf(calc_sigma_lossfact(Inf, 0.01, 0.02)) - end - - @testset "Numerical Consistency" begin - # Float32 vs Float64 - r = calc_sigma_lossfact(Float32(1e-9), Float32(0.01), Float32(0.02)) - d = calc_sigma_lossfact(1e-9, 0.01, 0.02) - @test isapprox(r, d; atol = TEST_TOL) - end - - @testset "Physical Behavior" begin - # Increases with G_eq - r1 = calc_sigma_lossfact(1e-9, 0.01, 0.02) - r2 = calc_sigma_lossfact(2e-9, 0.01, 0.02) - @test r2 > r1 - # Increases with log(r_ext/r_in) - r3 = calc_sigma_lossfact(1e-9, 0.01, 0.04) - @test r3 > r1 - end - - @testset "Type Stability & Promotion" begin - using Measurements - # All Float64 - r1 = calc_sigma_lossfact(1e-9, 0.01, 0.02) - @test typeof(r1) == Float64 - # All Measurement - r2 = calc_sigma_lossfact( - measurement(1e-9, 1e-11), - measurement(0.01, 1e-5), - measurement(0.02, 1e-5), - ) - @test r2 isa Measurement{Float64} - # Mixed: G_eq as Measurement - r3 = calc_sigma_lossfact(measurement(1e-9, 1e-11), 0.01, 0.02) - @test r3 isa Measurement{Float64} - # Mixed: r_in as Measurement - r4 = calc_sigma_lossfact(1e-9, measurement(0.01, 1e-5), 0.02) - @test r4 isa Measurement{Float64} - # Mixed: r_ex as Measurement - r5 = calc_sigma_lossfact(1e-9, 0.01, measurement(0.02, 1e-5)) - @test r5 isa Measurement{Float64} - end - - @testset "Uncertainty Quantification" begin - using Measurements - G_eq = measurement(1e-9, 1e-11) - r_in = measurement(0.01, 1e-5) - r_ext = measurement(0.02, 1e-5) - result = calc_sigma_lossfact(G_eq, r_in, r_ext) - @test result isa Measurement{Float64} - @test uncertainty(result) > 0 - end -end diff --git a/test/unit_BaseParams/test_calc_solenoid_correction.jl b/test/unit_BaseParams/test_calc_solenoid_correction.jl deleted file mode 100644 index 245b02dfb..000000000 --- a/test/unit_BaseParams/test_calc_solenoid_correction.jl +++ /dev/null @@ -1,80 +0,0 @@ -@testitem "BaseParams: calc_solenoid_correction unit tests" setup = [defaults] begin - @testset "Basic Functionality" begin - # Example from docstring: 10 turns/m, conductor radius 5 mm, insulator radius 10 mm - result = calc_solenoid_correction(10.0, 0.005, 0.01) - expected = 1.0 + 2 * 10.0^2 * pi^2 * (0.01^2 - 0.005^2) / log(0.01 / 0.005) - @test isapprox(result, expected; atol=TEST_TOL) - @test result > 1.0 - - # Non-helical cable (NaN turns) - result = calc_solenoid_correction(NaN, 0.005, 0.01) - @test result == 1.0 - end - - @testset "Edge Cases" begin - # Zero turns (should be 1.0) - result = calc_solenoid_correction(0.0, 0.005, 0.01) - @test isapprox(result, 1.0; atol=TEST_TOL) - - # Collapsing geometry: radii nearly equal - result = calc_solenoid_correction(10.0, 0.01, 0.010001) - @test result > 1.0 - - # Very large number of turns - result = calc_solenoid_correction(1e6, 0.005, 0.01) - @test result > 1e9 - - # Inf/NaN radii - @test isnan(calc_solenoid_correction(10.0, NaN, 0.01)) - @test isnan(calc_solenoid_correction(10.0, 0.005, NaN)) - @test isinf(calc_solenoid_correction(10.0, 0.0, 0.01)) == false # log(0.01/0) = Inf, but numerator is finite - end - - @testset "Numerical Consistency" begin - # Float32 vs Float64 - r = calc_solenoid_correction(Float32(10.0), Float32(0.005), Float32(0.01)) - d = calc_solenoid_correction(10.0, 0.005, 0.01) - @test isapprox(r, d; atol=TEST_TOL) - end - - @testset "Physical Behavior" begin - # Correction increases with more turns - c1 = calc_solenoid_correction(5.0, 0.005, 0.01) - c2 = calc_solenoid_correction(10.0, 0.005, 0.01) - @test c2 > c1 - # Correction increases with larger insulator radius - c3 = calc_solenoid_correction(10.0, 0.005, 0.02) - @test c3 > c2 - end - - @testset "Type Stability & Promotion" begin - using Measurements - # All Float64 - r1 = calc_solenoid_correction(10.0, 0.005, 0.01) - @test typeof(r1) == Float64 - # All Measurement - r2 = calc_solenoid_correction(measurement(10.0, 0.1), measurement(0.005, 1e-5), measurement(0.01, 1e-5)) - @test r2 isa Measurement{Float64} - # Mixed: num_turns as Measurement - r3 = calc_solenoid_correction(measurement(10.0, 0.1), 0.005, 0.01) - @test r3 isa Measurement{Float64} - # Mixed: radius_ext_con as Measurement - r4 = calc_solenoid_correction(10.0, measurement(0.005, 1e-5), 0.01) - @test r4 isa Measurement{Float64} - # Mixed: radius_ext_ins as Measurement - r5 = calc_solenoid_correction(10.0, 0.005, measurement(0.01, 1e-5)) - @test r5 isa Measurement{Float64} - end - - @testset "Uncertainty Quantification" begin - using Measurements - num_turns = measurement(10.0, 0.1) - radius_ext_con = measurement(0.005, 1e-5) - radius_ext_ins = measurement(0.01, 1e-5) - result = calc_solenoid_correction(num_turns, radius_ext_con, radius_ext_ins) - # Should propagate uncertainty - @test result isa Measurement{Float64} - # Uncertainty should be nonzero - @test uncertainty(result) > 0 - end -end diff --git a/test/unit_BaseParams/test_calc_strip_resistance.jl b/test/unit_BaseParams/test_calc_strip_resistance.jl deleted file mode 100644 index 74827500b..000000000 --- a/test/unit_BaseParams/test_calc_strip_resistance.jl +++ /dev/null @@ -1,74 +0,0 @@ -@testitem "BaseParams: calc_strip_resistance unit tests" setup = [defaults] begin - using Measurements - # --- Basic Functionality --- - @testset "Basic Functionality" begin - thickness = 0.002 - width = 0.05 - rho = 1.7241e-8 - alpha = 0.00393 - T0 = 20.0 - Top = 25.0 - R = calc_strip_resistance(thickness, width, rho, alpha, T0, Top) - @test isapprox(R, 0.00017579785649999996, atol=TEST_TOL) - end - - # --- Edge Cases --- - @testset "Edge Cases" begin - # Zero thickness (should return Inf or error in physical context, but function will return Inf) - R = calc_strip_resistance(0.0, 0.05, 1.7241e-8, 0.00393, 20.0, 25.0) - @test isinf(R) - # Zero width - R = calc_strip_resistance(0.002, 0.0, 1.7241e-8, 0.00393, 20.0, 25.0) - @test isinf(R) - # Large temperature difference (within asserted range) - R = calc_strip_resistance(0.002, 0.05, 1.7241e-8, 0.00393, 20.0, 100.0) - @test R > 0.00017579785649999996 - end - - # --- Numerical Consistency --- - @testset "Numerical Consistency" begin - # Float32 - R = calc_strip_resistance(Float32(0.002), Float32(0.05), Float32(1.7241e-8), Float32(0.00393), Float32(20.0), Float32(25.0)) - @test isapprox(R, 0.00017579785649999996, atol=TEST_TOL) - end - - # --- Physical Behavior --- - @testset "Physical Behavior" begin - # Resistance increases with temperature - R1 = calc_strip_resistance(0.002, 0.05, 1.7241e-8, 0.00393, 20.0, 25.0) - R2 = calc_strip_resistance(0.002, 0.05, 1.7241e-8, 0.00393, 20.0, 75.0) - @test R2 > R1 - # Resistance decreases with increasing cross-section - R3 = calc_strip_resistance(0.002, 0.05, 1.7241e-8, 0.00393, 20.0, 25.0) - R4 = calc_strip_resistance(0.004, 0.05, 1.7241e-8, 0.00393, 20.0, 25.0) - @test R4 < R3 - end - - # --- Type Stability & Promotion --- - @testset "Type Stability & Promotion" begin - # All Float64 - R = calc_strip_resistance(0.002, 0.05, 1.7241e-8, 0.00393, 20.0, 25.0) - @test typeof(R) == Float64 - # All Measurement - Rm = calc_strip_resistance(measurement(0.002, 1e-6), measurement(0.05, 1e-5), measurement(1.7241e-8, 1e-10), measurement(0.00393, 1e-6), measurement(20.0, 0.1), measurement(25.0, 0.1)) - @test Rm isa Measurement{Float64} - # Mixed: thickness as Measurement - R1 = calc_strip_resistance(measurement(0.002, 1e-6), 0.05, 1.7241e-8, 0.00393, 20.0, 25.0) - @test R1 isa Measurement{Float64} - # Mixed: alpha as Measurement - R2 = calc_strip_resistance(0.002, 0.05, 1.7241e-8, measurement(0.00393, 1e-6), 20.0, 25.0) - @test R2 isa Measurement{Float64} - end - - # --- Uncertainty Quantification --- - @testset "Uncertainty Quantification" begin - t = measurement(0.002, 1e-6) - w = measurement(0.05, 1e-5) - r = measurement(1.7241e-8, 1e-10) - a = measurement(0.00393, 1e-6) - t0 = measurement(20.0, 0.1) - top = measurement(25.0, 0.1) - R = calc_strip_resistance(t, w, r, a, t0, top) - @test uncertainty(R) > 0 - end -end diff --git a/test/unit_BaseParams/test_calc_temperature_correction.jl b/test/unit_BaseParams/test_calc_temperature_correction.jl deleted file mode 100644 index b573a13c8..000000000 --- a/test/unit_BaseParams/test_calc_temperature_correction.jl +++ /dev/null @@ -1,79 +0,0 @@ -@testitem "BaseParams: calc_temperature_correction unit tests" setup = [defaults] begin - using Measurements - # Basic Functionality - @testset "Basic Functionality" begin - # Example from docstring: alpha = 0.00393, Top = 75.0, T0 = 20.0 - k = calc_temperature_correction(0.00393, 75.0, 20.0) - @test isapprox(k, 1.2161, atol=1e-4) - - # Default T0 (should use T₀ constant) - k2 = calc_temperature_correction(0.00393, 75.0) - k2_ref = calc_temperature_correction(0.00393, 75.0, T₀) - @test isapprox(k2, k2_ref, atol=TEST_TOL) - end - - # Edge Cases - @testset "Edge Cases" begin - # Zero temperature difference - @test isapprox(calc_temperature_correction(0.00393, 20.0, 20.0), 1.0, atol=TEST_TOL) - # Negative alpha (unusual, but mathematically valid) - @test isapprox(calc_temperature_correction(-0.001, 30.0, 20.0), 0.99, atol=TEST_TOL) - # Large temperature difference within ΔTmax - @test isapprox(calc_temperature_correction(0.00393, 20.0 + (ΔTmax - 1), 20.0), 1 + 0.00393 * (ΔTmax - 1), atol=TEST_TOL) - end - - # Numerical Consistency - @testset "Numerical Consistency" begin - # Float32 - kf = calc_temperature_correction(Float32(0.00393), Float32(75.0), Float32(20.0)) - @test isapprox(kf, 1.2161f0, atol=Float32(1e-4)) - end - - # Physical Behavior - @testset "Physical Behavior" begin - # Correction increases with temperature for positive alpha - k1 = calc_temperature_correction(0.00393, 50.0, 20.0) - k2 = calc_temperature_correction(0.00393, 80.0, 20.0) - @test k2 > k1 - # Correction decreases with temperature for negative alpha - k3 = calc_temperature_correction(-0.001, 50.0, 20.0) - k4 = calc_temperature_correction(-0.001, 80.0, 20.0) - @test k4 < k3 - end - - # Type Stability & Promotion - @testset "Type Stability & Promotion" begin - # All Float64 - kf = calc_temperature_correction(0.00393, 75.0, 20.0) - @test typeof(kf) == Float64 - # All Measurement - αm = measurement(0.00393, 1e-5) - Topm = measurement(75.0, 0.1) - T0m = measurement(20.0, 0.1) - km = calc_temperature_correction(αm, Topm, T0m) - @test km isa Measurement{Float64} - # Mixed: alpha as Measurement - kmix1 = calc_temperature_correction(αm, 75.0, 20.0) - @test kmix1 isa Measurement{Float64} - # Mixed: Top as Measurement - kmix2 = calc_temperature_correction(0.00393, Topm, 20.0) - @test kmix2 isa Measurement{Float64} - # Mixed: T0 as Measurement - kmix3 = calc_temperature_correction(0.00393, 75.0, T0m) - @test kmix3 isa Measurement{Float64} - end - - # Uncertainty Quantification - @testset "Uncertainty Quantification" begin - αm = measurement(0.00393, 1e-5) - Topm = measurement(75.0, 0.1) - T0m = measurement(20.0, 0.1) - km = calc_temperature_correction(αm, Topm, T0m) - # Analytical propagation: k = 1 + α*(Top-T0) - # σ² = (Top-T0)²*σ_α² + α²*σ_Top² + α²*σ_T0² - μ = 1 + 0.00393 * (75.0 - 20.0) - σ2 = (75.0 - 20.0)^2 * 1e-5^2 + 0.00393^2 * 0.1^2 + 0.00393^2 * 0.1^2 - @test isapprox(value(km), μ, atol=TEST_TOL) - @test isapprox(uncertainty(km), sqrt(σ2), atol=TEST_TOL) - end -end diff --git a/test/unit_BaseParams/test_calc_tubular_gmr.jl b/test/unit_BaseParams/test_calc_tubular_gmr.jl deleted file mode 100644 index 2b1de76b6..000000000 --- a/test/unit_BaseParams/test_calc_tubular_gmr.jl +++ /dev/null @@ -1,104 +0,0 @@ -@testitem "BaseParams: calc_tubular_gmr unit tests" setup = [defaults] begin - using Measurements: measurement, value, uncertainty - - @testset "Basic Functionality" begin - # Example from docstring - r_ex = 0.02 - r_in = 0.01 - mu_r = 1.0 - gmr = calc_tubular_gmr(r_ex, r_in, mu_r) - # Manual calculation for expected value - term1 = (r_in^4 / (r_ex^2 - r_in^2)^2) * log(r_ex / r_in) - term2 = (3 * r_in^2 - r_ex^2) / (4 * (r_ex^2 - r_in^2)) - Lin = (μ₀ * mu_r / (2 * π)) * (term1 - term2) - expected = exp(log(r_ex) - (2 * π / μ₀) * Lin) - @test isapprox(gmr, expected; atol = TEST_TOL) - @test gmr > 0 - @test_throws ArgumentError calc_tubular_gmr(r_in, r_ex, mu_r) - @test_throws ArgumentError calc_tubular_gmr(0.0, r_in, mu_r) - end - - @testset "Edge Cases" begin - # Thin shell: r_ex ≈ r_in - r_ex = 0.01 - r_in = 0.01 - mu_r = 1.0 - gmr = calc_tubular_gmr(r_ex, r_in, mu_r) - @test isapprox(gmr, r_ex; atol = TEST_TOL) - - # Infinitely thick tube: r_in ≫ 0, r_in / r_ex ≈ 0 - r_ex = 1.0 - r_in = 1e-12 - mu_r = 1.0 - gmr = calc_tubular_gmr(r_ex, r_in, mu_r) - @test isapprox(gmr, 0.7788; atol = 1e-4) - - # r_in = 0 (solid cylinder) - r_ex = 0.02 - r_in = 0.0 - mu_r = 1.0 - gmr = calc_tubular_gmr(r_ex, r_in, mu_r) - @test isapprox(gmr, 0.7788 * r_ex; atol = 1e-4) - - # r_ex < r_in (should throw) - r_ex = 0.01 - r_in = 0.02 - mu_r = 1.0 - @test_throws ArgumentError calc_tubular_gmr(r_ex, r_in, mu_r) - end - - @testset "Numerical Consistency" begin - # Float64 - gmr1 = calc_tubular_gmr(0.02, 0.01, 1.0) - # Measurement{Float64} - gmr2 = calc_tubular_gmr( - measurement(0.02, 1e-4), - measurement(0.01, 1e-4), - measurement(1.0, 0.01), - ) - @test isapprox(value(gmr2), gmr1; atol = TEST_TOL) - @test uncertainty(gmr2) > 0 - end - - @testset "Physical Behavior" begin - # GMR increases with r_ex - gmr1 = calc_tubular_gmr(0.01, 0.005, 1.0) - gmr2 = calc_tubular_gmr(0.02, 0.005, 1.0) - @test gmr2 > gmr1 - # GMR decreases with mu_r - gmr1 = calc_tubular_gmr(0.02, 0.01, 0.5) - gmr2 = calc_tubular_gmr(0.02, 0.01, 2.0) - @test gmr2 < gmr1 - end - - @testset "Type Stability & Promotion" begin - # All Float64 - gmr = calc_tubular_gmr(0.02, 0.01, 1.0) - @test typeof(gmr) == Float64 - # All Measurement - gmr = calc_tubular_gmr( - measurement(0.02, 1e-4), - measurement(0.01, 1e-4), - measurement(1.0, 0.01), - ) - @test gmr isa Measurement{Float64} - # Mixed: r_ex as Measurement - gmr = calc_tubular_gmr(measurement(0.02, 1e-4), 0.01, 1.0) - @test gmr isa Measurement{Float64} - # Mixed: r_in as Measurement - gmr = calc_tubular_gmr(0.02, measurement(0.01, 1e-4), 1.0) - @test gmr isa Measurement{Float64} - # Mixed: mu_r as Measurement - gmr = calc_tubular_gmr(0.02, 0.01, measurement(1.0, 0.01)) - @test gmr isa Measurement{Float64} - end - - @testset "Uncertainty Quantification" begin - r_ex = measurement(0.02, 1e-4) - r_in = measurement(0.01, 1e-4) - mu_r = measurement(1.0, 0.01) - gmr = calc_tubular_gmr(r_ex, r_in, mu_r) - @test gmr isa Measurement{Float64} - @test uncertainty(gmr) > 0 - end -end diff --git a/test/unit_BaseParams/test_calc_tubular_inductance.jl b/test/unit_BaseParams/test_calc_tubular_inductance.jl deleted file mode 100644 index abde7c96a..000000000 --- a/test/unit_BaseParams/test_calc_tubular_inductance.jl +++ /dev/null @@ -1,94 +0,0 @@ -@testitem "BaseParams: calc_tubular_inductance unit tests" setup = [defaults] begin - using Measurements - # Basic Functionality - @testset "Basic Functionality" begin - # Example from docstring: r_in = 0.01, r_ex = 0.02, mu_r = 1.0 - L = calc_tubular_inductance(0.01, 0.02, 1.0) - expected = 1.0 * μ₀ / (2 * π) * log(0.02 / 0.01) - @test isapprox(L, expected, atol = TEST_TOL) - end - - # Edge Cases - @testset "Edge Cases" begin - # Very thin tube (r_ex ≈ r_in) - r_in = 0.01 - r_ext = 0.010001 - L_thin = calc_tubular_inductance(r_in, r_ext, 1.0) - expected_thin = 1.0 * μ₀ / (2 * π) * log(r_ext / r_in) - @test isapprox(L_thin, expected_thin, atol = TEST_TOL) - # Large radii - L_large = calc_tubular_inductance(1e3, 2e3, 1.0) - expected_large = 1.0 * μ₀ / (2 * π) * log(2e3 / 1e3) - @test isapprox(L_large, expected_large, atol = TEST_TOL) - # mu_r = 0 (non-magnetic) - @test isapprox(calc_tubular_inductance(0.01, 0.02, 0.0), 0.0, atol = TEST_TOL) - end - - # Numerical Consistency - @testset "Numerical Consistency" begin - # Float32 - Lf = calc_tubular_inductance(Float32(0.01), Float32(0.02), Float32(1.0)) - expectedf = Float32(μ₀) / (2.0f0 * Float32(π)) * log(Float32(0.02) / Float32(0.01)) - @test isapprox(Lf, expectedf, atol = Float32(TEST_TOL)) - # Rational - Lr = calc_tubular_inductance(1 // 100, 1 // 50, 1 // 1) - expectedr = (1 // 1) * μ₀ / (2 * π) * log((1 // 50) / (1 // 100)) - @test isapprox(Lr, expectedr, atol = TEST_TOL) - end - - # Physical Behavior - @testset "Physical Behavior" begin - # L increases with mu_r - L1 = calc_tubular_inductance(0.01, 0.02, 1.0) - L2 = calc_tubular_inductance(0.01, 0.02, 2.0) - @test L2 > L1 - # L increases with r_ex - L3 = calc_tubular_inductance(0.01, 0.03, 1.0) - @test L3 > L1 - # L decreases with r_in - L4 = calc_tubular_inductance(0.02, 0.03, 1.0) - @test L4 < L3 - end - - # Type Stability & Promotion - @testset "Type Stability & Promotion" begin - # All Float64 - Lf = calc_tubular_inductance(0.01, 0.02, 1.0) - @test typeof(Lf) == Float64 - # All Measurement - rinm = measurement(0.01, 1e-5) - rextm = measurement(0.02, 1e-5) - murm = measurement(1.0, 1e-3) - Lm = calc_tubular_inductance(rinm, rextm, murm) - @test Lm isa Measurement{Float64} - # Mixed: r_in as Measurement - Lmix1 = calc_tubular_inductance(rinm, 0.02, 1.0) - @test Lmix1 isa Measurement{Float64} - # Mixed: r_ex as Measurement - Lmix2 = calc_tubular_inductance(0.01, rextm, 1.0) - @test Lmix2 isa Measurement{Float64} - # Mixed: mu_r as Measurement - Lmix3 = calc_tubular_inductance(0.01, 0.02, murm) - @test Lmix3 isa Measurement{Float64} - end - - # Uncertainty Quantification - @testset "Uncertainty Quantification" begin - rinm = measurement(0.01, 1e-5) - rextm = measurement(0.02, 1e-5) - murm = measurement(1.0, 1e-3) - Lm = calc_tubular_inductance(rinm, rextm, murm) - # Analytical propagation: L = mu_r * μ₀ / (2π) * log(r_ext/r_in) - μ = 1.0 * μ₀ / (2 * π) * log(0.02 / 0.01) - # Partial derivatives - dL_drin = -murm * μ₀ / (2 * π) * (1 / rinm) / (rextm / rinm) - dL_drext = murm * μ₀ / (2 * π) * (1 / rextm) / (rextm / rinm) - dL_dmurm = μ₀ / (2 * π) * log(0.02 / 0.01) - σ2 = - (value(dL_drin) * uncertainty(rinm))^2 + - (value(dL_drext) * uncertainty(rextm))^2 + - (value(dL_dmurm) * uncertainty(murm))^2 - @test isapprox(value(Lm), μ, atol = TEST_TOL) - @test isapprox(uncertainty(Lm), sqrt(σ2), atol = TEST_TOL) - end -end diff --git a/test/unit_BaseParams/test_calc_tubular_resistance.jl b/test/unit_BaseParams/test_calc_tubular_resistance.jl deleted file mode 100644 index 58ae8ce62..000000000 --- a/test/unit_BaseParams/test_calc_tubular_resistance.jl +++ /dev/null @@ -1,124 +0,0 @@ -@testitem "BaseParams: calc_tubular_resistance unit tests" setup = [defaults] begin - # Basic Functionality - @testset "Basic Functionality" begin - # Example from docstring - r_in = 0.01 - r_ex = 0.02 - rho = 1.7241e-8 - alpha = 0.00393 - T0 = 20.0 - Top = 25.0 - expected = - calc_temperature_correction(alpha, Top, T0) * rho / (π * (r_ex^2 - r_in^2)) - R = calc_tubular_resistance(r_in, r_ex, rho, alpha, T0, Top) - @test isapprox(R, expected, atol = TEST_TOL) - end - - # Edge Cases - @testset "Edge Cases" begin - # Zero thickness (r_in == r_ex): cross-section = 0, expect Inf or error - r_in = 0.01 - r_ext = 0.01 - rho = 1.7241e-8 - alpha = 0.00393 - T0 = 20.0 - Top = 25.0 - # Should return Inf (division by zero) - R = calc_tubular_resistance(r_in, r_ext, rho, alpha, T0, Top) - @test isinf(R) - # Very thin tube (r_ex - r_in ≈ eps) - r_in2 = 0.01 - r_ext2 = 0.01 + eps() - R2 = calc_tubular_resistance(r_in2, r_ext2, rho, alpha, T0, Top) - @test R2 > 0 - # Large radii - R3 = calc_tubular_resistance(1.0, 2.0, rho, alpha, T0, Top) - @test R3 < 1e-8 - # Negative temperature coefficient (mathematically valid) - R4 = calc_tubular_resistance(0.01, 0.02, rho, -0.001, T0, Top) - expected4 = - calc_temperature_correction(-0.001, Top, T0) * rho / (π * (0.02^2 - 0.01^2)) - @test isapprox(R4, expected4, atol = TEST_TOL) - end - - # Numerical Consistency - @testset "Numerical Consistency" begin - # Float32 - Rf = calc_tubular_resistance( - Float32(0.01), - Float32(0.02), - Float32(1.7241e-8), - Float32(0.00393), - Float32(20.0), - Float32(25.0), - ) - expectedf = - calc_temperature_correction(Float32(0.00393), Float32(25.0), Float32(20.0)) * - Float32(1.7241e-8) / (π * (Float32(0.02)^2 - Float32(0.01)^2)) - @test isapprox(Rf, expectedf, atol = Float32(TEST_TOL)) - end - - # Physical Behavior - @testset "Physical Behavior" begin - rho = 1.7241e-8 - alpha = 0.00393 - T0 = 20.0 - Top = 25.0 - # Resistance decreases as cross-section increases - R_small = calc_tubular_resistance(0.01, 0.015, rho, alpha, T0, Top) - R_large = calc_tubular_resistance(0.01, 0.03, rho, alpha, T0, Top) - @test R_large < R_small - # Resistance increases with increasing resistivity - R_lowrho = calc_tubular_resistance(0.01, 0.02, 1e-8, alpha, T0, Top) - R_highrho = calc_tubular_resistance(0.01, 0.02, 1e-7, alpha, T0, Top) - @test R_highrho > R_lowrho - # Resistance increases with increasing temperature (for positive alpha) - R_T1 = calc_tubular_resistance(0.01, 0.02, rho, alpha, T0, 25.0) - R_T2 = calc_tubular_resistance(0.01, 0.02, rho, alpha, T0, 75.0) - @test R_T2 > R_T1 - end - - # Type Stability & Promotion - @testset "Type Stability & Promotion" begin - # All Float64 - Rf = calc_tubular_resistance(0.01, 0.02, 1.7241e-8, 0.00393, 20.0, 25.0) - @test typeof(Rf) == Float64 - # All Measurement - using Measurements - rin_m = measurement(0.01, 1e-6) - rext_m = measurement(0.02, 1e-6) - rho_m = measurement(1.7241e-8, 1e-10) - alpha_m = measurement(0.00393, 1e-5) - T0_m = measurement(20.0, 0.1) - Top_m = measurement(25.0, 0.1) - Rm = calc_tubular_resistance(rin_m, rext_m, rho_m, alpha_m, T0_m, Top_m) - @test Rm isa Measurement{Float64} - # Mixed: first argument as Measurement - Rmix1 = calc_tubular_resistance(rin_m, 0.02, 1.7241e-8, 0.00393, 20.0, 25.0) - @test Rmix1 isa Measurement{Float64} - # Mixed: middle argument as Measurement - Rmix2 = calc_tubular_resistance(0.01, 0.02, rho_m, 0.00393, 20.0, 25.0) - @test Rmix2 isa Measurement{Float64} - # Mixed: last argument as Measurement - Rmix3 = calc_tubular_resistance(0.01, 0.02, 1.7241e-8, 0.00393, 20.0, Top_m) - @test Rmix3 isa Measurement{Float64} - end - - # Uncertainty Quantification - @testset "Uncertainty Quantification" begin - rin_m = measurement(0.01, 1e-6) - rext_m = measurement(0.02, 1e-6) - rho_m = measurement(1.7241e-8, 1e-10) - alpha_m = measurement(0.00393, 1e-5) - T0_m = measurement(20.0, 0.1) - Top_m = measurement(25.0, 0.1) - Rm = calc_tubular_resistance(rin_m, rext_m, rho_m, alpha_m, T0_m, Top_m) - # Analytical propagation (approximate, neglecting correlations): - ΔA = π * (value(rext_m)^2 - value(rin_m)^2) - k = value(calc_temperature_correction(alpha_m, Top_m, T0_m)) - μ = k * value(rho_m) / ΔA - @test isapprox(value(Rm), μ, atol = TEST_TOL) - # Uncertainty should be nonzero and scale with input uncertainties - @test uncertainty(Rm) > 0 - end -end diff --git a/test/unit_BaseParams/test_calc_wirearray_coords.jl b/test/unit_BaseParams/test_calc_wirearray_coords.jl deleted file mode 100644 index 040d99201..000000000 --- a/test/unit_BaseParams/test_calc_wirearray_coords.jl +++ /dev/null @@ -1,166 +0,0 @@ -@testitem "BaseParams: calc_circstrands_coords unit tests" setup = [defaults] begin - - @testset "Basic Functionality" begin - @testset "Standard 6-wire array at origin" begin - let num_wires = 6, radius_wire = 0.001, r_in = 0.01 - lay_radius = r_in + radius_wire # 0.011 - coords = calc_circstrands_coords(num_wires, radius_wire, r_in) - - @test length(coords) == num_wires - @test coords isa Vector{Tuple{Float64, Float64}} - - # Expected coordinates for a 6-wire array (angle step = π/3) - expected = [ - (lay_radius, 0.0), # Angle 0 - (lay_radius * cos(π / 3), lay_radius * sin(π / 3)), # Angle π/3 - (lay_radius * cos(2π / 3), lay_radius * sin(2π / 3)), # Angle 2π/3 - (-lay_radius, 0.0), # Angle π - (lay_radius * cos(4π / 3), lay_radius * sin(4π / 3)), # Angle 4π/3 - (lay_radius * cos(5π / 3), lay_radius * sin(5π / 3)), # Angle 5π/3 - ] - - @test length(coords) == length(expected) - for (coord, exp_coord) in zip(coords, expected) - @test isapprox(coord[1], exp_coord[1]; atol = TEST_TOL) - @test isapprox(coord[2], exp_coord[2]; atol = TEST_TOL) - end - end - end - - @testset "4-wire array with non-zero center" begin - let num_wires = 4, radius_wire = 0.002, r_in = 0.02, C = (0.1, -0.2) - lay_radius = r_in + radius_wire # 0.022 - coords = calc_circstrands_coords(num_wires, radius_wire, r_in, C) - - @test length(coords) == num_wires - - # Expected coordinates for a 4-wire array (angle step = π/2) - expected = [ - (C[1] + lay_radius, C[2]), # Angle 0 - (C[1], C[2] + lay_radius), # Angle π/2 - (C[1] - lay_radius, C[2]), # Angle π - (C[1], C[2] - lay_radius), # Angle 3π/2 - ] - @test length(coords) == length(expected) - for (coord, exp_coord) in zip(coords, expected) - @test isapprox(coord[1], exp_coord[1]; atol = TEST_TOL) - @test isapprox(coord[2], exp_coord[2]; atol = TEST_TOL) - end - end - end - end - - @testset "Edge Cases" begin - @testset "Single wire is always at the center" begin - # A single wire's lay radius is defined as 0. - coords = calc_circstrands_coords(1, 0.001, 0.01) - @test coords == [(0.0, 0.0)] - - C = (10.0, -20.0) - coords_C = calc_circstrands_coords(1, 0.001, 0.01, C) - @test coords_C == [C] - end - - @testset "Zero wires returns an empty vector" begin - coords = calc_circstrands_coords(0, 0.001, 0.01) - @test isempty(coords) - @test coords isa Vector - end - - @testset "Zero radii places all wires at the center" begin - # If lay radius is zero, all wires should be at the center C. - num_wires = 7 - coords = calc_circstrands_coords(num_wires, 0.0, 0.0) - @test length(coords) == num_wires - @test all(c -> c == (0.0, 0.0), coords) - - C = (1.0, 1.0) - coords_C = calc_circstrands_coords(num_wires, 0.0, 0.0, C) - @test length(coords_C) == num_wires - @test all(c -> c == C, coords_C) - end - end - - @testset "Type Stability and Promotion" begin - @testset "Base case: Float64 inputs" begin - coords = calc_circstrands_coords(6, 0.001, 0.01) - @test coords isa Vector{Tuple{Float64, Float64}} - @test eltype(first(coords)) == Float64 - end - - @testset "Fully promoted: All inputs are Measurement" begin - num_wires = 3 - rw = 0.001 ± 0.0001 - ri = 0.01 ± 0.0002 - C = (0.1 ± 0.01, -0.2 ± 0.02) - coords = calc_circstrands_coords(num_wires, rw, ri, C) - - @test coords isa Vector{Tuple{Measurement{Float64}, Measurement{Float64}}} - @test eltype(first(coords)) == Measurement{Float64} - - # Check value and uncertainty propagation for the first wire (angle=0) - lay_radius = rw + ri - expected_x = C[1] + lay_radius - expected_y = C[2] # sin(0) is 0, so lay_radius term is zero - - @test coords[1][1] ≈ expected_x - @test coords[1][2] ≈ expected_y - end - - @testset "Mixed types: radius_wire is Measurement" begin - num_wires = 4 - rw = 0.001 ± 0.0001 - ri = 0.01 # Float64 - C = (0.1, -0.2) # Tuple{Float64, Float64} - coords = calc_circstrands_coords(num_wires, rw, ri, C = C) - - @test coords isa Vector{Tuple{Measurement{Float64}, Measurement{Float64}}} - lay_radius_val = Measurements.value(rw) + ri - - # Wire 1 (angle 0) - @test Measurements.value(coords[1][1]) ≈ C[1] + lay_radius_val atol = TEST_TOL - @test Measurements.value(coords[1][2]) ≈ C[2] atol = TEST_TOL - @test Measurements.uncertainty(coords[1][1]) > 0 - @test Measurements.uncertainty(coords[1][2]) == 0 # sin(0) = 0, no uncertainty propagation - - # Wire 2 (angle π/2) - @test Measurements.value(coords[2][1]) ≈ C[1] atol = TEST_TOL - @test Measurements.value(coords[2][2]) ≈ C[2] + lay_radius_val atol = TEST_TOL - @test isapprox(Measurements.uncertainty(coords[2][1]), 0, atol = TEST_TOL) # cos(π/2) = 0, no uncertainty propagation - @test Measurements.uncertainty(coords[2][2]) > 0 - end - - @testset "Mixed types: r_in is Measurement" begin - num_wires = 4 - rw = 0.001 # Float64 - ri = 0.01 ± 0.0002 - coords = calc_circstrands_coords(num_wires, rw, ri) - - @test coords isa Vector{Tuple{Measurement{Float64}, Measurement{Float64}}} - lay_radius_uncert = Measurements.uncertainty(ri) - @test Measurements.uncertainty(coords[1][1]) ≈ lay_radius_uncert atol = TEST_TOL - end - - @testset "Mixed types: Center C is Measurement" begin - num_wires = 4 - rw = 0.001 # Float64 - ri = 0.01 # Float64 - C = (0.1 ± 0.01, -0.2 ± 0.02) - # Use keyword argument version to test the helper method - coords = calc_circstrands_coords(num_wires, rw, ri; C = C) - - @test coords isa Vector{Tuple{Measurement{Float64}, Measurement{Float64}}} - lay_radius = rw + ri - - # Check uncertainty propagation from center C - @test Measurements.value(coords[1][1]) ≈ Measurements.value(C[1]) + lay_radius atol = - TEST_TOL - @test Measurements.value(coords[1][2]) ≈ Measurements.value(C[2]) atol = - TEST_TOL - @test Measurements.uncertainty(coords[1][1]) ≈ Measurements.uncertainty(C[1]) atol = - TEST_TOL - @test Measurements.uncertainty(coords[1][2]) ≈ Measurements.uncertainty(C[2]) atol = - TEST_TOL - end - end -end diff --git a/test/unit_BaseParams/test_calc_wirearray_gmr.jl b/test/unit_BaseParams/test_calc_wirearray_gmr.jl deleted file mode 100644 index eaf1a7b60..000000000 --- a/test/unit_BaseParams/test_calc_wirearray_gmr.jl +++ /dev/null @@ -1,99 +0,0 @@ -@testitem "BaseParams: calc_circstrands_gmr unit tests" setup = [defaults] begin - using Measurements: measurement, value, uncertainty - - @testset "Basic Functionality" begin - # Example from docstring - lay_rad = 0.05 - N = 7 - rad_wire = 0.002 - mu_r = 1.0 - gmr = calc_circstrands_gmr(lay_rad, N, rad_wire, mu_r) - expected = exp((log(rad_wire * exp(-mu_r / 4) * N * lay_rad^(N - 1)) / N)) - @test isapprox(gmr, expected; atol=TEST_TOL) - @test gmr > 0 - end - - @testset "Edge Cases" begin - # N = 1 (single wire) - lay_rad = 0.05 - N = 1 - rad_wire = 0.002 - mu_r = 1.0 - gmr = calc_circstrands_gmr(lay_rad, N, rad_wire, mu_r) - expected = rad_wire * exp(-mu_r / 4) - @test isapprox(gmr, expected; atol=TEST_TOL) - - # mu_r = 0 (non-magnetic) - lay_rad = 0.05 - N = 7 - rad_wire = 0.002 - mu_r = 0.0 - gmr = calc_circstrands_gmr(lay_rad, N, rad_wire, mu_r) - expected = exp((log(rad_wire * N * lay_rad^(N - 1)) / N)) - @test isapprox(gmr, expected; atol=TEST_TOL) - - # rad_wire = 0 (degenerate wire) - lay_rad = 0.05 - N = 7 - rad_wire = 0.0 - mu_r = 1.0 - gmr = calc_circstrands_gmr(lay_rad, N, rad_wire, mu_r) - @test gmr == 0.0 - - # lay_rad = 0 (all wires at center) - lay_rad = 0.0 - N = 7 - rad_wire = 0.002 - mu_r = 1.0 - gmr = calc_circstrands_gmr(lay_rad, N, rad_wire, mu_r) - expected = exp((log(rad_wire * exp(-mu_r / 4) * N * 0.0^(N - 1)) / N)) - @test gmr == 0.0 - end - - @testset "Numerical Consistency" begin - # Float64 - gmr1 = calc_circstrands_gmr(0.05, 7, 0.002, 1.0) - # Measurement{Float64} - gmr2 = calc_circstrands_gmr(measurement(0.05, 1e-4), 7, 0.002, 1.0) - @test isapprox(value(gmr2), gmr1; atol=TEST_TOL) - @test uncertainty(gmr2) > 0 - end - - @testset "Physical Behavior" begin - # GMR increases with lay_rad - gmr1 = calc_circstrands_gmr(0.01, 7, 0.002, 1.0) - gmr2 = calc_circstrands_gmr(0.05, 7, 0.002, 1.0) - @test gmr2 > gmr1 - # GMR decreases with mu_r - gmr1 = calc_circstrands_gmr(0.05, 7, 0.002, 0.5) - gmr2 = calc_circstrands_gmr(0.05, 7, 0.002, 2.0) - @test gmr2 < gmr1 - end - - @testset "Type Stability & Promotion" begin - # All Float64 - gmr = calc_circstrands_gmr(0.05, 7, 0.002, 1.0) - @test typeof(gmr) == Float64 - # All Measurement - gmr = calc_circstrands_gmr(measurement(0.05, 1e-4), 7, measurement(0.002, 1e-5), measurement(1.0, 0.01)) - @test gmr isa Measurement{Float64} - # Mixed: lay_rad as Measurement - gmr = calc_circstrands_gmr(measurement(0.05, 1e-4), 7, 0.002, 1.0) - @test gmr isa Measurement{Float64} - # Mixed: rad_wire as Measurement - gmr = calc_circstrands_gmr(0.05, 7, measurement(0.002, 1e-5), 1.0) - @test gmr isa Measurement{Float64} - # Mixed: mu_r as Measurement - gmr = calc_circstrands_gmr(0.05, 7, 0.002, measurement(1.0, 0.01)) - @test gmr isa Measurement{Float64} - end - - @testset "Uncertainty Quantification" begin - lay_rad = measurement(0.05, 1e-4) - rad_wire = measurement(0.002, 1e-5) - mu_r = measurement(1.0, 0.01) - gmr = calc_circstrands_gmr(lay_rad, 7, rad_wire, mu_r) - @test gmr isa Measurement{Float64} - @test uncertainty(gmr) > 0 - end -end diff --git a/test/unit_DataModel/test_CableComponent.jl b/test/unit_DataModel/test_CableComponent.jl deleted file mode 100644 index 370b80af7..000000000 --- a/test/unit_DataModel/test_CableComponent.jl +++ /dev/null @@ -1,152 +0,0 @@ -@testitem "DataModel(CableComponent): unit tests" setup = - [defaults, deps_datamodel, defs_materials] begin - # Aliases (no `using` per project policy) - const LM = LineCableModels - const DM = LM.DataModel - const MAT = LM.Materials - - # --- Canonical materials (fallbacks in case `defs_materials` lacks a key) --- - copper = get(materials, "copper", MAT.Material(1.7241e-8, 1.0, 1.0, 20.0, 0.00393)) - aluminum = get(materials, "aluminum", MAT.Material(2.826e-8, 1.0, 1.0, 20.0, 0.00429)) - polyeth = get(materials, "polyethylene", MAT.Material(1e12, 2.3, 1.0, 20.0, 0.0)) - semimat = get(materials, "semicon", MAT.Material(1e3, 3.0, 1.0, 20.0, 0.0)) - - # --- Helpers --------------------------------------------------------------- - make_conductor_group_F = function () - # core: 1 wire at center (diameter d_w) - d_w = 3e-3 - g = DM.ConductorGroup(DM.CircStrands(0.0, DM.Diameter(d_w), 1, 0.0, aluminum)) - # add helical wire layer (defaults: r_in = group.r_ex) - DM.add!(g, DM.CircStrands, DM.Diameter(d_w), 6, 10.0, aluminum) - # add thin strip layer - DM.add!(g, DM.Strip, DM.Thickness(5e-4), 1.0e-2, 15.0, aluminum) - # add tubular sheath (thickness) - DM.add!(g, DM.Tubular, DM.Thickness(1e-3), aluminum) - return g - end - - make_insulator_group_F = function (rin::Real) - # Build from inner radius `rin` outward - g = DM.InsulatorGroup(DM.Insulator(rin, DM.Thickness(4e-3), polyeth)) - DM.add!(g, DM.Semicon, DM.Thickness(5e-4), semimat) - return g - end - - # Small helper for measurement creation - m = x -> measurement(x, 0.1 * x + (x == 0 ? 1e-6 : 0)) - - # --- Input Validation ------------------------------------------------------ - @testset "Input Validation" begin - gC = make_conductor_group_F() - # make insulator group that *does not* start exactly at gC.r_ex - bad_rin = gC.r_ex + 1e-6 - gI_bad = DM.InsulatorGroup(DM.Insulator(bad_rin, DM.Thickness(4e-3), polyeth)) - DM.add!(gI_bad, DM.Semicon, DM.Thickness(5e-4), semimat) - @test_throws ArgumentError DM.CableComponent("bad", gC, gI_bad) - end - - # --- Basic Functionality (Float64 workflow) -------------------------------- - @testset "Basic Functionality (Float64)" begin - gC = make_conductor_group_F() - gI = make_insulator_group_F(gC.r_ex) - cc = DM.CableComponent("core", gC, gI) - - # Type & identity when no promotion needed - @test eltype(cc) == Float64 - @test cc.id == "core" - @test cc.conductor_group === gC - @test cc.insulator_group === gI - - # Geometric continuity (nominal comparison) - @test isapprox( - cc.conductor_group.r_ex, - cc.insulator_group.r_in; - atol = TEST_TOL, - ) - - # Physical sanity - @test cc.conductor_props.rho > 0 - @test cc.conductor_props.mu_r > 0 - @test cc.insulator_props.eps_r > 0 - @test cc.insulator_props.rho > 0 - end - - # --- Edge Cases ------------------------------------------------------------ - @testset "Edge Cases" begin - gC = DM.ConductorGroup(DM.CircStrands(0.0, DM.Diameter(2e-3), 1, 0.0, copper)) - # hairline insulator: nearly zero thickness, but non-zero - gI = DM.InsulatorGroup(DM.Insulator(gC.r_ex, gC.r_ex + 1e-6, polyeth)) - cc = DM.CableComponent("thin", gC, gI) - @test cc.insulator_group.r_ex > cc.conductor_group.r_ex - @test cc.insulator_props.eps_r > 0 - end - - # --- Physical Behavior (relationships that should hold) -------------------- - @testset "Physical Behavior" begin - gC = make_conductor_group_F() - gI = make_insulator_group_F(gC.r_ex) - cc = DM.CableComponent("phys", gC, gI) - - # Conductor alpha propagated - @test isapprox(cc.conductor_props.alpha, gC.alpha; atol = TEST_TOL) - - gI2 = DM.InsulatorGroup(DM.Insulator(gC.r_ex, DM.Thickness(6e-3), polyeth)) - DM.add!(gI2, DM.Semicon, DM.Thickness(5e-4), semimat) - cc2 = DM.CableComponent("phys2", gC, gI2) - @test cc2.insulator_group.shunt_capacitance < cc.insulator_group.shunt_capacitance - end - - # --- Type Stability & Promotion ------------------------------------------- - @testset "Type Stability & Promotion" begin - # Base: both Float64 - gC_F = make_conductor_group_F() - gI_F = make_insulator_group_F(gC_F.r_ex) - cc_F = DM.CableComponent("F", gC_F, gI_F) - @test eltype(cc_F) == Float64 - - # Insulator as Measurement → component promotes - gI_M = DM.coerce_to_T(gI_F, Measurement{Float64}) - cc_PM = DM.CableComponent("PM", gC_F, gI_M) - @test eltype(cc_PM) <: Measurement - @test eltype(cc_PM.conductor_group) <: Measurement - @test eltype(cc_PM.insulator_group) <: Measurement - # original groups untouched - @test eltype(gC_F) == Float64 - @test eltype(gI_F) == Float64 - - # Conductor as Measurement → component promotes - gC_M = DM.coerce_to_T(gC_F, Measurement{Float64}) - cc_MP = DM.CableComponent("MP", gC_M, gI_F) - @test eltype(cc_MP) <: Measurement - - # Both Measurement - cc_MM = DM.CableComponent("MM", gC_M, gI_M) - @test eltype(cc_MM) <: Measurement - - # Mixed raw creation using measurements inside groups - gC_mix = - DM.ConductorGroup(DM.CircStrands(0.0, DM.Diameter(m(3e-3)), 1, 0.0, aluminum)) - DM.add!(gC_mix, DM.Tubular, DM.Thickness(m(5e-4)), copper) - gI_mix = - DM.InsulatorGroup(DM.Insulator(gC_mix.r_ex, DM.Thickness(2e-3), polyeth)) - cc_mix = DM.CableComponent("mix", gC_mix, gI_mix) - @test eltype(cc_mix) <: Measurement - end - - # --- Combinatorial Type Testing (constructor path inside groups) ----------- - @testset "Combinatorial Type Testing" begin - # Base floats - gC = DM.ConductorGroup(DM.CircStrands(0.0, DM.Diameter(2e-3), 1, 0.0, aluminum)) - gI = DM.InsulatorGroup(DM.Insulator(gC.r_ex, DM.Thickness(3e-3), polyeth)) - - # Case A: Float + Measurement (insulator group) - gI_A = DM.coerce_to_T(gI, Measurement{Float64}) - cc_A = DM.CableComponent("A", gC, gI_A) - @test eltype(cc_A) <: Measurement - - # Case B: Measurement + Float (conductor group) - gC_B = DM.coerce_to_T(gC, Measurements.Measurement{Float64}) - cc_B = DM.CableComponent("B", gC_B, gI) - @test eltype(cc_B) <: Measurement - end -end diff --git a/test/unit_DataModel/test_CableDesign.jl b/test/unit_DataModel/test_CableDesign.jl deleted file mode 100644 index aa7ba8528..000000000 --- a/test/unit_DataModel/test_CableDesign.jl +++ /dev/null @@ -1,178 +0,0 @@ -# ------------------------- -# Test fixtures (canonical parts) -# ------------------------- -@testsnippet cable_fixtures begin - - # Aliases - const LM = LineCableModels - const DM = LM.DataModel - const MAT = LM.Materials - using Measurements: measurement - - # metals & dielectrics - copper_props = MAT.Material(1.7241e-8, 1.0, 1.0, 20.0, 0.00393) - alu_props = MAT.Material(2.82e-8, 1.0, 1.0, 20.0, 0.0039) - xlpe_props = MAT.Material(1e10, 2.3, 1.0, 20.0, 0.0) # insulator-like - semi_props = MAT.Material(1e3, 2.6, 1.0, 20.0, 0.0) # semicon-ish - - # geometry - d_wire = 3e-3 # 3 mm - rin0 = 0.0 - - # One of each conductor type - core_wire = DM.CircStrands(rin0, DM.Diameter(d_wire), 1, 0.0, copper_props) - # outer wire layer - outer_wire = - DM.CircStrands(core_wire.r_ex, DM.Diameter(d_wire), 6, 10.0, copper_props) - # strip (placed over outer wire layer) - strip1 = DM.Strip(outer_wire.r_ex, DM.Thickness(0.5e-3), 0.02, 8.0, copper_props) - # tubular (placed over strip) - tube1 = DM.Tubular(strip1.r_ex, DM.Thickness(0.8e-3), copper_props) - - # Build a conductor group with mixed parts - function make_conductor_group() - g = DM.ConductorGroup(core_wire) - add!(g, DM.CircStrands, DM.Diameter(d_wire), 6, 10.0, copper_props) - add!(g, DM.Strip, DM.Thickness(0.5e-3), 0.02, 8.0, copper_props) - add!(g, DM.Tubular, DM.Thickness(0.8e-3), copper_props) - g - end - - # Insulation parts - ins1 = DM.Insulator(tube1.r_ex, DM.Thickness(2.0e-3), xlpe_props) - semi = DM.Semicon(ins1.r_ex, DM.Thickness(0.8e-3), semi_props) - ins2 = DM.Insulator(semi.r_ex, DM.Thickness(2.0e-3), xlpe_props) - - function make_insulator_group() - ig = DM.InsulatorGroup(ins1) - add!(ig, DM.Semicon, DM.Thickness(0.8e-3), semi_props) - add!(ig, DM.Insulator, DM.Thickness(2.0e-3), xlpe_props) - ig - end - - # convenience helpers used in tests - make_groups() = (make_conductor_group(), make_insulator_group()) - m(x, u) = measurement(x, u) -end - -@testitem "DataModel(CableDesign): unit tests" setup = - [defaults, deps_datamodel, defs_materials, cable_fixtures] begin - # ------------------------- - # Tests - # ------------------------- - - @testset "Input Validation" begin - # mismatched radii: force an insulator group that doesn't start at conductor rex - g = make_conductor_group() - bad_ins = DM.InsulatorGroup( - DM.Insulator( - g.r_ex + 1e-4, - DM.Thickness(1e-3), - MAT.Material(1e10, 3.0, 1.0, 20.0, 0.0), - ), - ) - @test_throws ArgumentError DM.CableComponent("core", g, bad_ins) - @test_throws ArgumentError DM.CableDesign("cabA", g, bad_ins) - end - - @testset "Basic Functionality (Float64)" begin - g, ig = make_groups() - @test g.r_ex ≈ ig.r_in atol = TEST_TOL - - # direct component then design - cc = DM.CableComponent("core", g, ig) - @test cc isa DM.CableComponent - @test cc.id == "core" - @test cc.conductor_group === g - @test cc.insulator_group === ig - - des = DM.CableDesign("CAB-001", cc) - @test des isa DM.CableDesign - @test des.cable_id == "CAB-001" - @test length(des.components) == 1 - @test des.components[1].id == "core" - - # wrapper constructor from groups - des2 = DM.CableDesign("CAB-002", g, ig; component_id = "core") - @test des2 isa DM.CableDesign - @test des2.components[1].id == "core" - end - - @testset "Edge Cases" begin - # tiny interface gap within tolerance should pass (uses isapprox in inner ctor) - g, ig = make_groups() - # Nudge insulator inner radius by a few eps of Float64 - ig.r_in = ig.r_in + eps(Float64) * 1 - @test DM.CableComponent("core", g, ig) isa DM.CableComponent - end - - @testset "Physical Behavior" begin - g, ig = make_groups() - cc = DM.CableComponent("core", g, ig) - # Resistivity and GMR-driven equivalent numbers should be positive - @test cc.conductor_props.rho > 0 - @test cc.conductor_group.gmr > 0 - @test cc.insulator_props.eps_r > 0 - @test cc.insulator_props.mu_r > 0 - @test cc.insulator_group.shunt_capacitance > 0 - end - - @testset "Type Stability & Promotion (component creation)" begin - # Build groups in Float64 then promote *only* one group with a Measurement value - gF, igF = make_groups() - - # Create a Measurement insulator by tweaking thickness with uncertainty - igM = DM.coerce_to_T(igF, Measurements.Measurement{Float64}) - ccP = DM.CableComponent("coreM", gF, igM) - @test typeof(ccP.conductor_group.r_ex) <: Measurements.Measurement - @test typeof(ccP.insulator_group.r_ex) <: Measurements.Measurement - - # Creating a design with this mixed component works and holds the component - des = DM.CableDesign("CAB-MIXED", ccP) - @test length(des.components) == 1 - @test des.components[1].id == "coreM" - @test typeof(des.components[1].conductor_group.r_in) <: - Measurements.Measurement - end - - @testset "Design add! (by component & by groups) + overwrite semantics" begin - g, ig = make_groups() - cc = DM.CableComponent("core", g, ig) - des = DM.CableDesign("CAB-ADD", cc) - @test length(des.components) == 1 - - # Add a second component by groups - g2, ig2 = make_groups() - DM.add!(des, "sheath", g2, ig2) - @test length(des.components) == 2 - @test any(c -> c.id == "sheath", des.components) - - # Add another with same id -> overwrite warning - @test length(des.components) == 2 - end - - @testset "Combinatorial Type Testing (Measurement vs Float)" begin - g, ig = make_groups() - - # 1) Base: both Float64 - ccF = DM.CableComponent("cF", g, ig) - @test typeof(ccF.conductor_group.r_in) == Float64 - - # 2) Fully promoted: both Measurement - gM = DM.coerce_to_T(g, Measurements.Measurement{Float64}) - igM = DM.coerce_to_T(ig, Measurements.Measurement{Float64}) - ccM = DM.CableComponent("cM", gM, igM) - @test typeof(ccM.conductor_group.r_in) <: Measurements.Measurement - @test typeof(ccM.insulator_group.r_ex) <: Measurements.Measurement - - # 3a) Mixed: conductor carries Measurement - cc1 = DM.CableComponent("c1", gM, ig) - @test typeof(cc1.conductor_group.r_in) <: Measurements.Measurement - @test typeof(cc1.insulator_group.r_in) <: Measurements.Measurement - - # 3b) Mixed: insulator carries Measurement - cc2 = DM.CableComponent("c2", g, igM) - @test typeof(cc2.conductor_group.r_in) <: Measurements.Measurement - @test typeof(cc2.insulator_group.r_in) <: Measurements.Measurement - end -end diff --git a/test/unit_DataModel/test_CablePosition.jl b/test/unit_DataModel/test_CablePosition.jl deleted file mode 100644 index 73b9f5f84..000000000 --- a/test/unit_DataModel/test_CablePosition.jl +++ /dev/null @@ -1,84 +0,0 @@ -@testsnippet defs_cablepos begin - using Test - using LineCableModels - const DM = LineCableModels.DataModel - const MAT = LineCableModels.Materials - using Measurements - - # ---- helpers ---------------------------------------------------------- - - # Minimal Float64 design with matching interface radii - function _make_design_F64() - mC = MAT.Material(1e-8, 1.0, 1.0, 20.0, 0.0) - mI = MAT.Material(1e12, 2.5, 1.0, 20.0, 0.0) - - cg = DM.ConductorGroup(DM.Tubular(0.010, 0.012, mC)) - ig = DM.InsulatorGroup(DM.Insulator(0.012, 0.016, mI)) - - cc = DM.CableComponent("core", cg, ig) - return DM.CableDesign("CAB", cc) - end - - # Outermost radius of the last component (for placement checks) - _out_radius(des) = max( - des.components[end].conductor_group.r_ex, - des.components[end].insulator_group.r_ex, - ) -end - -@testitem "DataModel(CablePosition): constructor unit tests" setup = - [defaults, defs_cablepos] begin - @testset "Basic construction (Float64)" begin - des = _make_design_F64() - rmax = _out_radius(des) - pos = DM.CablePosition(des, 1.0, rmax + 0.10) # default mapping - - @test pos isa DM.CablePosition - @test DM.eltype(pos) == Float64 - @test pos.design_data === des # no promotion → same object - @test pos.horz == 1.0 - @test pos.vert == rmax + 0.10 - @test length(pos.conn) == length(des.components) - @test any(!iszero, pos.conn) # at least one non-grounded - end - - @testset "Phase mapping (Dict-based)" begin - des = _make_design_F64() - rmax = _out_radius(des) - - # map by component id (unknown ids are rejected, missing ids default to 0) - conn = Dict(des.components[1].id => 1) - pos = DM.CablePosition(des, 0.0, rmax + 0.05, conn) - - @test pos.conn[1] == 1 - @test all(i == 1 ? pos.conn[i] == 1 : pos.conn[i] == 0 for i in 1:length(pos.conn)) - - bad = Dict("does-not-exist" => 1) - @test_throws ArgumentError DM.CablePosition(des, 0.0, rmax + 0.05, bad) - end - - @testset "Geometry validation" begin - des = _make_design_F64() - rmax = _out_radius(des) - - # exactly at z=0 is forbidden - @test_throws ArgumentError DM.CablePosition(des, 0.0, 0.0) - - # inside outer radius (crossing interface) is forbidden - @test_throws ArgumentError DM.CablePosition(des, 0.0, rmax * 0.5) - end - - @testset "Type stability & promotion" begin - desF = _make_design_F64() - rmax = _out_radius(desF) - vertM = measurement(rmax + 0.10, 1e-6) - - posM = DM.CablePosition(desF, 0.0, vertM) - - @test DM.eltype(posM) <: Measurement - @test DM.eltype(posM.design_data) <: Measurement # design promoted with position - @test posM.vert === vertM # identity preserved - @test typeof(posM.horz) <: Measurement # coerced to same scalar type - @test DM.coerce_to_T(posM, DM.eltype(posM)) === posM - end -end diff --git a/test/unit_DataModel/test_ConductorGroup.jl b/test/unit_DataModel/test_ConductorGroup.jl deleted file mode 100644 index ba5034727..000000000 --- a/test/unit_DataModel/test_ConductorGroup.jl +++ /dev/null @@ -1,215 +0,0 @@ -@testsnippet defs_con_group begin - # Canonical geometry and helpers reused across tests - # Materials come from `defs_materials` (e.g., `copper_props`, `materials`) - using Measurements - - const rad_wire0 = 0.0015 # 1.5 mm wire radius - const lay_ratio0 = 12.0 # arbitrary, > 0 - - # A fresh single‑wire core (center conductor) - make_core_group() = ConductorGroup( - CircStrands( - 0.0, - rad_wire0, - 1, - 0.0, - copper_props; - temperature = 20.0, - lay_direction = 1, - ), - ) - - # Convenience: a Float64 Tubular sleeve over the given inner radius - make_tubular_over(rin, t, mat) = Tubular(rin, Thickness(t), mat; temperature = 20.0) - - # Measurement helpers - m(x, u) = measurement(x, u) -end - -@testitem "DataModel(ConductorGroup.add!): unit tests" setup = - [defaults, deps_datamodel, defs_materials, defs_con_group] begin - using Measurements - - @testset "Input Validation (wrapper triggers validate!)" begin - g = make_core_group() - - # Missing required args for CircStrands (radius_wire, num_wires, lay_ratio, material) - @test_throws ArgumentError add!(g, CircStrands) - @test_throws ArgumentError add!(g, CircStrands, rad_wire0) - @test_throws ArgumentError add!(g, CircStrands, rad_wire0, 6) - @test_throws ArgumentError add!(g, CircStrands, rad_wire0, 6, lay_ratio0) - - # Invalid types forwarded to part validator - @test_throws ArgumentError add!(g, CircStrands, "bad", 6, lay_ratio0, copper_props) - @test_throws ArgumentError add!( - g, - CircStrands, - rad_wire0, - "bad", - lay_ratio0, - copper_props, - ) - @test_throws ArgumentError add!(g, CircStrands, rad_wire0, 6, "bad", copper_props) - @test_throws ArgumentError add!( - g, - CircStrands, - rad_wire0, - 6, - lay_ratio0, - "not_a_material", - ) - - # Out of range / geometry violations caught by rules - @test_throws ArgumentError add!( - g, - CircStrands, - -rad_wire0, - 6, - lay_ratio0, - copper_props, - ) - @test_throws ArgumentError add!(g, CircStrands, 0.0, 6, lay_ratio0, copper_props) # radius_wire > 0 - @test_throws ArgumentError add!( - g, - CircStrands, - rad_wire0, - 0, - lay_ratio0, - copper_props, - ) # num_wires > 0 - - # Unknown keyword should be rejected by sanitize/keyword_fields policy (if enforced upstream) - # NOTE: If this currently passes, add rejection in `sanitize` for unknown keywords. - @test_throws ArgumentError add!( - g, - CircStrands, - rad_wire0, - 6, - lay_ratio0, - copper_props; - not_a_kw = 1, - ) - end - - @testset "Basic Functionality (Float64)" begin - g = make_core_group() - @test g isa ConductorGroup - @test length(g.layers) == 1 - @test g.r_in == 0.0 - @test g.r_ex ≈ rad_wire0 atol = TEST_TOL - - # Add another wire layer using Diameter convenience + defaults (r_in auto = g.r_ex) - d_w = 2 * rad_wire0 - g = add!(g, CircStrands, Diameter(d_w), 6, 15.0, copper_props) # lay_direction defaults to 1 - @test g isa ConductorGroup - @test length(g.layers) == 2 - @test g.layers[end] isa CircStrands - @test g.layers[end].lay_direction == 1 # came from keyword_defaults for CircStrands - - # Geometry stacks outward and resistance decreases (parallel) - @test g.r_ex > rad_wire0 - @test g.resistance < g.layers[1].resistance - - # Add an outer tubular sleeve by thickness proxy - outer_before = g.r_ex - g = add!(g, Tubular, Thickness(0.002), copper_props) # temperature default from keyword_defaults(Tubular) - @test g.layers[end] isa Tubular - @test g.r_ex ≈ outer_before + 0.002 atol = TEST_TOL - end - - @testset "Edge Cases" begin - # Very thin sleeve - g = make_core_group() - outer0 = g.r_ex - g = add!(g, Tubular, Thickness(1e-6), copper_props) - @test g.r_ex ≈ outer0 + 1e-6 atol = TEST_TOL - - # CircStrands with lay_ratio very small but positive - g = make_core_group() - g = add!(g, CircStrands, rad_wire0, 3, 1e-6, copper_props) - @test g.layers[end] isa CircStrands - end - - @testset "Physical Behavior" begin - g = make_core_group() - # Add two conductor layers: resistance should drop further - R0 = g.resistance - g = add!(g, CircStrands, rad_wire0, 6, 10.0, copper_props) - R1 = g.resistance - g = add!(g, CircStrands, rad_wire0, 12, 10.0, copper_props) - R2 = g.resistance - @test R1 < R0 - @test R2 < R1 - - # Cross-section should be monotone increasing - @test g.cross_section > 0 - cs = [ - p.cross_section for - p in g.layers if p isa LineCableModels.DataModel.AbstractConductorPart - ] - @test all(>(0), cs) - end - - @testset "Type Stability & Promotion (group)" begin - # Base: purely Float64 group - gF = make_core_group() - @test eltype(gF) == Float64 - @test typeof(gF.r_ex) == Float64 - - # Promote by adding a Measurement argument (e.g., temperature) - gF_before_id = objectid(gF) - gP = add!( - gF, - CircStrands, - rad_wire0, - 6, - 10.0, - copper_props; - temperature = m(20.0, 0.1), - ) - @test gP !== gF # returned a promoted group - @test eltype(gP) <: Measurement - @test typeof(gP.r_ex) <: Measurement - # Original left intact - @test objectid(gF) == gF_before_id - @test length(gF.layers) == 1 - - # In‑place when already Measurement - gM = LineCableModels.DataModel.coerce_to_T(make_core_group(), Measurement{Float64}) - id_before = objectid(gM) - gM2 = add!(gM, CircStrands, m(rad_wire0, 1e-6), 6, 10.0, copper_props) - @test gM2 === gM # mutated in place - @test objectid(gM) == id_before - @test eltype(gM) <: Measurement - end - - @testset "Combinatorial Type Testing (constructor path of added part)" begin - # All Float64 - g = make_core_group() - g1 = add!(g, CircStrands, rad_wire0, 6, 10.0, copper_props) - @test eltype(g1) == Float64 - - # All Measurement (radius_wire, lay_ratio, temperature) - g = make_core_group() - g2 = add!( - g, - CircStrands, - m(rad_wire0, 1e-6), - 6, - m(10.0, 0.1), - copper_props; - temperature = m(20.0, 0.1), - ) - @test eltype(g2) <: Measurement - - # Mixed case A: first numeric arg is Measurement - g = make_core_group() - g3 = add!(g, CircStrands, m(rad_wire0, 1e-6), 6, 10.0, copper_props) - @test eltype(g3) <: Measurement - - # Mixed case B: middle arg (lay_ratio) is Measurement - g = make_core_group() - g4 = add!(g, CircStrands, rad_wire0, 6, m(10.0, 0.1), copper_props) - @test eltype(g4) <: Measurement - end -end diff --git a/test/unit_DataModel/test_Insulator.jl b/test/unit_DataModel/test_Insulator.jl deleted file mode 100644 index 5605cd817..000000000 --- a/test/unit_DataModel/test_Insulator.jl +++ /dev/null @@ -1,149 +0,0 @@ -@testitem "DataModel(Insulator): constructor unit tests" setup = - [defaults, deps_datamodel, defs_materials] begin - - using Measurements - - @testset "Input Validation" begin - # Missing required arguments - @test_throws ArgumentError Insulator() - @test_throws ArgumentError Insulator(r_in = 0.01) - @test_throws ArgumentError Insulator(r_in = 0.01, r_ex = 0.015) - - # Invalid types - @test_throws ArgumentError Insulator( - "foo", - 0.015, - insulator_props, - temperature = 20.0, - ) - @test_throws ArgumentError Insulator( - 0.01, - "bar", - insulator_props, - temperature = 20.0, - ) - @test_throws ArgumentError Insulator( - 0.01, - 0.015, - "not_a_material", - temperature = 20.0, - ) - @test_throws ArgumentError Insulator( - 0.01, - 0.015, - insulator_props, - temperature = "not_a_temp", - ) - - # Out-of-range values - @test_throws ArgumentError Insulator( - -0.01, - 0.015, - insulator_props, - temperature = 20.0, - ) - @test_throws ArgumentError Insulator( - 0.01, - -0.015, - insulator_props, - temperature = 20.0, - ) - - # Geometrically impossible values - @test_throws ArgumentError Insulator( - 0.015, - 0.01, - insulator_props, - temperature = 20.0, - ) - @test_throws ArgumentError Insulator( - 0.01, - 0.01, - insulator_props, - temperature = 20.0, - ) - - # Invalid nothing/missing - @test_throws ArgumentError Insulator( - nothing, - 0.015, - insulator_props, - temperature = 20.0, - ) - @test_throws ArgumentError Insulator( - 0.01, - nothing, - insulator_props, - temperature = 20.0, - ) - @test_throws ArgumentError Insulator(0.01, 0.015, nothing, temperature = 20.0) - @test_throws ArgumentError Insulator( - 0.01, - 0.015, - insulator_props, - temperature = nothing, - ) - end - - @testset "Basic Functionality" begin - i = Insulator(0.01, 0.015, insulator_props, temperature = 20.0) - @test i isa Insulator - @test i.r_in ≈ 0.01 atol = TEST_TOL - @test i.r_ex ≈ 0.015 atol = TEST_TOL - @test i.material_props === insulator_props - @test i.temperature ≈ 20.0 atol = TEST_TOL - @test i.cross_section ≈ π * (0.015^2 - 0.01^2) atol = TEST_TOL - # Measurement type - i2 = Insulator( - measurement(0.01, 1e-5), - measurement(0.015, 1e-5), - insulator_props, - temperature = measurement(20.0, 0.1), - ) - @test i2 isa Insulator - @test value(i2.r_in) ≈ 0.01 atol = TEST_TOL - @test value(i2.r_ex) ≈ 0.015 atol = TEST_TOL - @test value(i2.temperature) ≈ 20.0 atol = TEST_TOL - end - - @testset "Edge Cases" begin - # r_in very close to r_ex - i = Insulator(1e-6, 1.0001e-6, insulator_props, temperature = 20.0) - @test i.r_in ≈ 1e-6 atol = TEST_TOL - end - - @testset "Physical Behavior" begin - # Cross-section increases with r_ex - i_small = Insulator(0.01, 0.012, insulator_props, temperature = 20.0) - i_large = Insulator(0.01, 0.018, insulator_props, temperature = 20.0) - @test i_large.cross_section > i_small.cross_section - # but the capacitance decreases (2 pi eps / log(rex/rin)) - @test i_large.shunt_capacitance < i_small.shunt_capacitance - end - - @testset "Type Stability & Promotion" begin - # All Float64 - i = Insulator(0.01, 0.015, insulator_props, temperature = 20.0) - @test typeof(i.r_in) == Float64 - # All Measurement - iM = Insulator( - measurement(0.01, 1e-5), - measurement(0.015, 1e-5), - insulator_props, - temperature = measurement(20.0, 0.1), - ) - @test typeof(iM.r_in) <: Measurement - # Mixed: r_in as Measurement - iMix1 = - Insulator(measurement(0.01, 1e-5), 0.015, insulator_props, temperature = 20.0) - @test typeof(iMix1.r_in) <: Measurement - # Mixed: temperature as Measurement - iMix2 = - Insulator(0.01, 0.015, insulator_props, temperature = measurement(20.0, 0.1)) - @test typeof(iMix2.temperature) <: Measurement - mmat = Material(1e14, measurement(5.0, 0.1), 1.0, 20.0, 0.0) - iMix3 = Insulator(0.01, 0.015, mmat, temperature = 20.0) - @test typeof(iMix3.shunt_conductance) <: Measurement - end - -end diff --git a/test/unit_DataModel/test_InsulatorGroup.jl b/test/unit_DataModel/test_InsulatorGroup.jl deleted file mode 100644 index b6236a020..000000000 --- a/test/unit_DataModel/test_InsulatorGroup.jl +++ /dev/null @@ -1,121 +0,0 @@ -@testsnippet defs_ins_group begin - using Measurements - # Canonical dielectric material for tests - const ins_props = Material(1e10, 3.0, 1.0, 20.0, 0.0) - - # Fresh inner insulator (Float64) - make_ins_group() = InsulatorGroup( - Insulator(0.02, 0.025, ins_props; temperature = 20.0), - ) - - # Measurement helper - m(x, u) = measurement(x, u) -end - -@testitem "DataModel(InsulatorGroup.add!): unit tests" setup = - [defaults, deps_datamodel, defs_materials, defs_ins_group] begin - using Measurements - - @testset "Input Validation (wrapper triggers validate!)" begin - g = make_ins_group() - - # Missing required args for Insulator: (r_in provided by wrapper), need r_ex, material - @test_throws ArgumentError add!(g, Insulator) - @test_throws ArgumentError add!(g, Insulator, 0.03) - - # Invalid types - @test_throws ArgumentError add!(g, Insulator, "bad", ins_props) - @test_throws ArgumentError add!(g, Insulator, 0.03, "not_a_material") - - # Geometry violations - @test_throws ArgumentError add!(g, Insulator, 0.0, ins_props) # outer cannot be 0 beyond rin - end - - @testset "Basic Functionality (Float64)" begin - g = make_ins_group() - @test g isa InsulatorGroup - @test length(g.layers) == 1 - @test g.r_in ≈ 0.02 atol = TEST_TOL - @test g.r_ex ≈ 0.025 atol = TEST_TOL - - # Add a Semicon by thickness proxy (outer radius = rin + t). r_in defaults to group.r_ex - t = 0.002 - rin_before = g.r_ex - g = add!(g, Semicon, Thickness(t), ins_props; f = 60.0) - @test g.layers[end] isa Semicon - @test g.r_ex ≈ rin_before + t atol = TEST_TOL - end - - @testset "Edge Cases" begin - g = make_ins_group() - tsmall = 1e-6 - re0 = g.r_ex - g = add!(g, Semicon, Thickness(tsmall), ins_props; f = 60.0) - @test g.r_ex ≈ re0 + tsmall atol = TEST_TOL - end - - @testset "Physical Behavior (admittance parallel update)" begin - g = make_ins_group() - # Capture before - C0 = g.shunt_capacitance - G0 = g.shunt_conductance - - # Add another dielectric shell; admittances should combine → values typically decrease - g = add!(g, Insulator, 0.03, ins_props; f = 60.0) - @test g.shunt_capacitance <= C0 # decreasing typical - @test g.shunt_conductance <= G0 # decreasing typical - end - - @testset "Type Stability & Promotion (group)" begin - # Base Float64 group - gF = make_ins_group() - @test eltype(gF) == Float64 - @test typeof(gF.r_ex) == Float64 - - # Promote by Measurement temperature in part defaults - gF_before = objectid(gF) - gP = add!(gF, Insulator, 0.03, ins_props; f = 60.0, temperature = m(20.0, 0.2)) - @test gP !== gF - @test eltype(gP) <: Measurement - @test typeof(gP.r_ex) <: Measurement - @test objectid(gF) == gF_before - @test length(gF.layers) == 1 - - # Already Measurement → in place - gM = LineCableModels.DataModel.coerce_to_T(make_ins_group(), Measurement{Float64}) - id0 = objectid(gM) - gM2 = add!(gM, Semicon, Thickness(m(0.001, 1e-6)), ins_props; f = 60.0) - @test gM2 === gM - @test objectid(gM) == id0 - @test eltype(gM) <: Measurement - end - - @testset "Combinatorial Type Testing" begin - # All Float64 - g = make_ins_group() - g1 = add!(g, Insulator, 0.03, ins_props; f = 60.0) - @test eltype(g1) == Float64 - - # All Measurement - g = make_ins_group() - g2 = add!( - g, - Insulator, - m(0.03, 1e-6), - ins_props; - f = 60.0, - temperature = m(20.0, 0.1), - ) - @test eltype(g2) <: Measurement - - # Mixed A: r_ex is Measurement - g = make_ins_group() - g3 = add!(g, Insulator, m(0.03, 1e-6), ins_props; f = 60.0) - @test eltype(g3) <: Measurement - - # Mixed B: pass Measurement frequency (promotes group by wrapper decision) - g = make_ins_group() - g4 = add!(g, Insulator, 0.03, ins_props; f = m(60.0, 0.5)) - @test eltype(g4) <: Measurement - end -end diff --git a/test/unit_DataModel/test_LineCableSystem.jl b/test/unit_DataModel/test_LineCableSystem.jl deleted file mode 100644 index 0fa4388b8..000000000 --- a/test/unit_DataModel/test_LineCableSystem.jl +++ /dev/null @@ -1,243 +0,0 @@ -@testsnippet defs_linesys begin - using Test - using LineCableModels - const DM = LineCableModels.DataModel - const MAT = LineCableModels.Materials - using Measurements - - # --- helpers ---------------------------------------------------------------- - - # Minimal Float64 design with matching interface radii - function _make_design_F64(; id = "CAB") - mC = MAT.Material(1e-8, 1.0, 1.0, 20.0, 0.0) - mI = MAT.Material(1e12, 2.5, 1.0, 20.0, 0.0) - - cg = DM.ConductorGroup(DM.Tubular(0.010, 0.012, mC)) - ig = DM.InsulatorGroup(DM.Insulator(0.012, 0.016, mI)) - - cc = DM.CableComponent("core", cg, ig) - return DM.CableDesign(id, cc) - end - - # Promote a design to Measurement{Float64} - function _make_design_M(; id = "CABM") - des = _make_design_F64(; id) - return DM.coerce_to_T(des, Measurement{Float64}) - end - - # Outermost radius of the last component (for placement checks) - _out_radius(des) = max( - des.components[end].conductor_group.r_ex, - des.components[end].insulator_group.r_ex, - ) - - # Position with explicit mapping (phase 1 by default) - function _make_position_F64(des; phase::Int = 1) - rmax = _out_radius(des) - conn = Dict(des.components[1].id => phase) - return DM.CablePosition(des, 0.0, rmax + 0.20, conn) - end - - # Measurement position (promotes design through the constructor) - function _make_position_M(desF; phase::Int = 1) - rmax = _out_radius(desF) - vertM = measurement(rmax + 0.25, 1e-6) - conn = Dict(desF.components[1].id => phase) - return DM.CablePosition(desF, 0.0, vertM, conn) - end -end - -@testitem "DataModel(LineCableSystem): constructor & add! unit tests" setup = - [defaults, defs_linesys] begin - # 1) Basic construction from CablePosition (Float64) - @testset "Basic construction (from CablePosition)" begin - des = _make_design_F64() - posF = _make_position_F64(des; phase = 1) - sys = DM.LineCableSystem("SYS", 1000.0, posF) - - @test sys isa DM.LineCableSystem - @test DM.eltype(sys) == Float64 - @test sys.line_length == 1000.0 - @test sys.num_cables == 1 - @test sys.num_phases == 1 - @test sys.cables[1] === posF # identity preserved - @test DM.coerce_to_T(sys, Float64) === sys # no-op coercion - end - - # 2) Loose constructor from CableDesign + coordinates - @testset "Loose constructor (from CableDesign + coords)" begin - des = _make_design_F64() - rmax = _out_radius(des) - conn = Dict(des.components[1].id => 1) - sys2 = DM.LineCableSystem("SYS2", 500.0, des, 0.10, rmax + 0.30, conn) - - @test DM.eltype(sys2) == Float64 - @test sys2.num_cables == 1 - @test sys2.num_phases == 1 - @test sys2.cables[1].design_data === des # built via position constructor - @test typeof(sys2.cables[1].horz) == Float64 - @test typeof(sys2.cables[1].vert) == Float64 - end - - # 3) Phase counting across multiple positions - @testset "Phase accounting" begin - des = _make_design_F64() - pos1 = _make_position_F64(des; phase = 1) - sys = DM.LineCableSystem("SYS-PH", 100.0, pos1) - - # Add a second cable mapped to phase 2 at a non-overlapping position - des2 = _make_design_F64(; id = "CAB2") - r1 = _out_radius(des) - r2 = _out_radius(des2) - dx = r1 + r2 + 0.05 # strictly beyond contact - y = max(r1, r2) + 0.20 - conn2 = Dict(des2.components[1].id => 2) - - pos2 = DM.CablePosition(des2, dx, y, conn2) - sys = DM.add!(sys, pos2) - - @test sys.num_cables == 2 - @test sys.num_phases == 2 - @test all(p -> any(x -> x > 0, p.conn), sys.cables) - end - - # 4) Promotion on add! (Float64 system + Measurement position → promoted system) - @testset "Promotion on add! (Float64 → Measurement)" begin - desF = _make_design_F64() - posF = _make_position_F64(desF) - sysF = DM.LineCableSystem("SYS-PR", 200.0, posF) - - # Measurement position created from a Float64 design (promotes inside) - posM = _make_position_M(desF) - sysP = DM.add!(sysF, posM) # returns promoted system - - @test DM.eltype(sysF) == Float64 - @test DM.eltype(sysP) <: Measurement - @test sysP !== sysF - @test sysP.num_cables == 2 - @test typeof(sysP.cables[1].horz) <: Measurement # existing coerced during promotion - @test typeof(sysP.cables[end].vert) <: Measurement - end - - # 5) No-op add! when already Measurement (system mutates in place) - @testset "No-op add! when types match (Measurement system)" begin - desF = _make_design_F64() - posM0 = _make_position_M(desF) # Measurement position - sysM0 = DM.LineCableSystem("SYS-M", measurement(1000.0, 1e-6), posM0) - - # Add a Float64 position → coerced to Measurement; system should mutate in place - des2 = _make_design_F64(; id = "CAB-F2") - posF2 = _make_position_F64(des2) - sysM1 = DM.add!(sysM0, posF2) - - @test sysM1 === sysM0 - @test DM.eltype(sysM0) <: Measurement - @test sysM0.num_cables == 2 - @test typeof(sysM0.cables[end].horz) <: Measurement - @test typeof(sysM0.line_length) <: Measurement - end - - # 6) Combinatorial type testing (length × cable position) - @testset "Combinatorial type testing (constructors)" begin - desF = _make_design_F64() - posF = _make_position_F64(desF) - posM = _make_position_M(desF) - - lengths = ( - 250.0, - measurement(250.0, 1e-6), - ) - - positions = ( - posF, - posM, - ) - - for L in lengths, p in positions - sys = DM.LineCableSystem("SYS-COMB", L, p) - if (L isa Measurement) || (DM.eltype(p) <: Measurement) - @test DM.eltype(sys) <: Measurement - else - @test DM.eltype(sys) == Float64 - end - # Round-trip no-op coercion at current T - @test DM.coerce_to_T(sys, DM.eltype(sys)) === sys - end - end -end - -@testitem "DataModel(LineCableSystem): promotion safety (intern-proof)" setup = - [defaults, defs_linesys] begin - using Test - using Measurements - - # local helper: make N Float64 positions spaced along x - function _many_positions(des, N::Int) - rmax = _out_radius(des) - conn = Dict(des.components[1].id => 1) - [DM.CablePosition(des, 0.1 * i, rmax + 0.20, conn) for i in 1:N] - end - - @testset "Promote whole system when a single Measurement cable is added" begin - # Build a big Float64 system (N deterministic cables) - N = 200 - desF = _make_design_F64() - possF = _many_positions(desF, N) - - # Build system by adding positions incrementally - sysF = DM.LineCableSystem("SYS-BIG", 1000.0, possF[1]) - for i in 2:N - sysF = DM.add!(sysF, possF[i]) # no promotion; mutates in place - end - - @test DM.eltype(sysF) == Float64 - @test sysF.num_cables == N - @test all(p -> DM.eltype(p) == Float64, sysF.cables) - - # Now the intern adds ONE measurement-typed cable position - posM = _make_position_M(desF) # promotes design inside position ctor - - # Expect: add! returns a promoted system, original unchanged - local sysP - @test_logs (:warn, r"promoted") (sysP = DM.add!(sysF, posM)) - - @test sysP !== sysF - @test DM.eltype(sysP) <: Measurement - - # Original remains Float64 and unchanged - @test DM.eltype(sysF) == Float64 - @test sysF.num_cables == N - - # New system is Measurement everywhere - @test DM.eltype(sysP) <: Measurement - @test sysP !== sysF - @test sysP.num_cables == N + 1 - @test all(p -> DM.eltype(p) <: Measurement, sysP.cables) - @test typeof(sysP.line_length) <: Measurement - - # The new position inside the promoted system is exactly the object we added - @test sysP.cables[end] === posM - @test DM.eltype(sysP.cables[end].design_data) <: Measurement - - # Existing positions were coerced during promotion - @test typeof(sysP.cables[1].vert) <: Measurement - @test DM.eltype(sysP.cables[1].design_data) <: Measurement - end - - @testset "No-op add! when already Measurement" begin - # Start with a Measurement system - desF = _make_design_F64() - posM0 = _make_position_M(desF) - sysM = DM.LineCableSystem("SYS-M", measurement(500.0, 1e-6), posM0) - - # Add a Float64 position → it should be coerced to Measurement and mutate in place - posF1 = _make_position_F64(_make_design_F64()) - sysM2 = DM.add!(sysM, posF1) - - @test sysM2 === sysM - @test DM.eltype(sysM) <: Measurement - @test sysM.num_cables == 2 - @test typeof(sysM.cables[end].horz) <: Measurement - end -end - diff --git a/test/unit_DataModel/test_Semicon.jl b/test/unit_DataModel/test_Semicon.jl deleted file mode 100644 index 4a2c42f30..000000000 --- a/test/unit_DataModel/test_Semicon.jl +++ /dev/null @@ -1,105 +0,0 @@ -@testitem "DataModel(Semicon): constructor unit tests" setup = - [defaults, deps_datamodel, defs_materials] begin - - using Measurements - - @testset "Input Validation" begin - # Missing required arguments - @test_throws ArgumentError Semicon() - @test_throws ArgumentError Semicon(r_in = 0.01) - @test_throws ArgumentError Semicon(r_in = 0.01, r_ex = 0.012) - - # Invalid types - @test_throws ArgumentError Semicon("foo", 0.012, semicon_props, temperature = 20.0) - @test_throws ArgumentError Semicon(0.01, "bar", semicon_props, temperature = 20.0) - @test_throws ArgumentError Semicon( - 0.01, - 0.012, - "not_a_material", - temperature = 20.0, - ) - @test_throws ArgumentError Semicon( - 0.01, - 0.012, - semicon_props, - temperature = "not_a_temp", - ) - - # Out-of-range values - @test_throws ArgumentError Semicon(-0.01, 0.012, semicon_props, temperature = 20.0) - @test_throws ArgumentError Semicon(0.01, -0.012, semicon_props, temperature = 20.0) - - # Geometrically impossible values - @test_throws ArgumentError Semicon(0.012, 0.01, semicon_props, temperature = 20.0) - @test_throws ArgumentError Semicon(0.01, 0.01, semicon_props, temperature = 20.0) - - # Invalid nothing/missing - @test_throws ArgumentError Semicon( - nothing, - 0.012, - semicon_props, - temperature = 20.0, - ) - @test_throws ArgumentError Semicon(0.01, nothing, semicon_props, temperature = 20.0) - @test_throws ArgumentError Semicon(0.01, 0.012, nothing, temperature = 20.0) - @test_throws ArgumentError Semicon(0.01, 0.012, semicon_props, nothing) - end - - @testset "Basic Functionality" begin - s = Semicon(0.01, 0.012, semicon_props, temperature = 20.0) - @test s isa Semicon - @test s.r_in ≈ 0.01 atol = TEST_TOL - @test s.r_ex ≈ 0.012 atol = TEST_TOL - @test s.material_props === semicon_props - @test s.temperature ≈ 20.0 atol = TEST_TOL - @test s.cross_section ≈ π * (0.012^2 - 0.01^2) atol = TEST_TOL - # Measurement type - s2 = Semicon( - measurement(0.01, 1e-5), - measurement(0.012, 1e-5), - semicon_props, - temperature = measurement(20.0, 0.1), - ) - @test s2 isa Semicon - @test value(s2.r_in) ≈ 0.01 atol = TEST_TOL - @test value(s2.r_ex) ≈ 0.012 atol = TEST_TOL - @test value(s2.temperature) ≈ 20.0 atol = TEST_TOL - end - - @testset "Edge Cases" begin - # r_in very close to r_ex - s = Semicon(1e-6, 1.0001e-6, semicon_props, temperature = 20.0) - @test s.r_in ≈ 1e-6 atol = TEST_TOL - end - - @testset "Physical Behavior" begin - # Cross-section increases with r_ex - s_small = Semicon(0.01, 0.011, semicon_props, temperature = 20.0) - s_large = Semicon(0.01, 0.013, semicon_props, temperature = 20.0) - @test s_large.cross_section > s_small.cross_section - end - - @testset "Type Stability & Promotion" begin - # All Float64 - s = Semicon(0.01, 0.012, semicon_props, temperature = 20.0) - @test typeof(s.r_in) == Float64 - # All Measurement - sM = Semicon( - measurement(0.01, 1e-5), - measurement(0.012, 1e-5), - semicon_props, - temperature = measurement(20.0, 0.1), - ) - @test typeof(sM.r_in) <: Measurement - # Mixed: r_in as Measurement - sMix1 = Semicon(measurement(0.01, 1e-5), 0.012, semicon_props, temperature = 20.0) - @test typeof(sMix1.r_in) <: Measurement - # Mixed: temperature as Measurement - sMix2 = Semicon(0.01, 0.012, semicon_props, temperature = measurement(20.0, 0.1)) - @test typeof(sMix2.temperature) <: Measurement - mmat = Material(1000.0, measurement(1000.0, 0.1), 1.0, 20.0, 0.0) - sMix3 = Semicon(0.01, 0.012, mmat, temperature = 20.0) - @test typeof(sMix3.shunt_capacitance) <: Measurement - end - -end diff --git a/test/unit_DataModel/test_Strip.jl b/test/unit_DataModel/test_Strip.jl deleted file mode 100644 index f7cb05d04..000000000 --- a/test/unit_DataModel/test_Strip.jl +++ /dev/null @@ -1,179 +0,0 @@ -@testitem "DataModel(Strip): constructor unit tests" setup = - [defaults, deps_datamodel, defs_materials] begin - - using Measurements - - @testset "Input Validation" begin - # Missing required arguments - @test_throws ArgumentError Strip() - @test_throws ArgumentError Strip(r_in = 0.01) - @test_throws ArgumentError Strip(r_in = 0.01, r_ex = 0.012) - @test_throws ArgumentError Strip(r_in = 0.01, r_ex = 0.012, width = 0.05) - @test_throws ArgumentError Strip( - r_in = 0.01, - r_ex = 0.012, - width = 0.05, - lay_ratio = 10, - ) - - # Invalid types - @test_throws ArgumentError Strip("foo", 0.012, 0.05, 10, copper_props) - @test_throws ArgumentError Strip(0.01, "bar", 0.05, 10, copper_props) - @test_throws ArgumentError Strip(0.01, 0.012, "baz", 10, copper_props) - @test_throws ArgumentError Strip(0.01, 0.012, 0.05, "qux", copper_props) - @test_throws ArgumentError Strip(0.01, 0.012, 0.05, 10, "not_a_material") - @test_throws ArgumentError Strip( - 0.01, - 0.012, - 0.05, - 10, - copper_props, - "not_a_temp", - 1, - ) - @test_throws ArgumentError Strip( - 0.01, - 0.012, - 0.05, - 10, - copper_props, - temperature = 20.0, - "not_a_dir", - ) - - # Out-of-range values - @test_throws ArgumentError Strip(-0.01, 0.012, 0.05, 10, copper_props) - @test_throws ArgumentError Strip(0.01, -0.012, 0.05, 10, copper_props) - @test_throws ArgumentError Strip(0.01, 0.012, -0.05, 10, copper_props) - @test_throws ArgumentError Strip( - 0.01, - 0.012, - 0.05, - 10, - copper_props, - temperature = 20.0, - lay_direction = 0, - ) - @test_throws ArgumentError Strip( - 0.01, - 0.012, - 0.05, - 10, - copper_props, - temperature = 20.0, - lay_direction = 2, - ) - @test_throws ArgumentError Strip( - 0.01, - 0.012, - 0.05, - 10, - copper_props, - temperature = 20.0, - lay_direction = -2, - ) - - # Geometrically impossible values - @test_throws ArgumentError Strip(0.012, 0.01, 0.05, 10, copper_props) - @test_throws ArgumentError Strip(0.01, 0.01, 0.05, 10, copper_props) - - # Invalid nothing/missing - @test_throws ArgumentError Strip(nothing, 0.012, 0.05, 10, copper_props) - @test_throws ArgumentError Strip(0.01, nothing, 0.05, 10, copper_props) - @test_throws ArgumentError Strip(0.01, 0.012, nothing, 10, copper_props) - @test_throws ArgumentError Strip(0.01, 0.012, 0.05, nothing, copper_props) - @test_throws ArgumentError Strip(0.01, 0.012, 0.05, 10, nothing) - @test_throws ArgumentError Strip( - 0.01, - 0.012, - 0.05, - 10, - copper_props, - temperature = nothing, - lay_direction = 1, - ) - @test_throws ArgumentError Strip( - 0.01, - 0.012, - 0.05, - 10, - copper_props, - temperature = 20.0, - lay_direction = nothing, - ) - end - - @testset "Basic Functionality" begin - s = Strip(0.01, 0.012, 0.05, 10, copper_props) - @test s isa Strip - @test s.r_in ≈ 0.01 atol = TEST_TOL - @test s.r_ex ≈ 0.012 atol = TEST_TOL - @test s.width ≈ 0.05 atol = TEST_TOL - @test s.lay_ratio ≈ 10 atol = TEST_TOL - @test s.material_props === copper_props - @test s.temperature ≈ 20.0 atol = TEST_TOL - @test s.lay_direction == 1 - @test s.cross_section ≈ (0.012 - 0.01) * 0.05 atol = TEST_TOL - # measurement type - s2 = Strip( - measurement(0.01, 1e-5), - measurement(0.012, 1e-5), - measurement(0.05, 1e-4), - 10, - copper_props, - temperature = measurement(20.0, 0.1), - ) - @test s2 isa Strip - @test value(s2.r_in) ≈ 0.01 atol = TEST_TOL - @test value(s2.r_ex) ≈ 0.012 atol = TEST_TOL - @test value(s2.width) ≈ 0.05 atol = TEST_TOL - @test value(s2.temperature) ≈ 20.0 atol = TEST_TOL - end - - @testset "Edge Cases" begin - # r_in very close to r_ex - s = Strip(1e-6, 1.0001e-6, 0.05, 10, copper_props) - @test s.r_in ≈ 1e-6 atol = TEST_TOL - # width very small - s2 = Strip(0.01, 0.012, 1e-6, 10, copper_props) - @test s2.width ≈ 1e-6 atol = TEST_TOL - end - - @testset "Physical Behavior" begin - # Resistance should increase with temperature - s20 = Strip(0.01, 0.012, 0.05, 10, copper_props) - s80 = Strip(0.01, 0.012, 0.05, 10, copper_props, temperature = 80.0) - @test s80.resistance > s20.resistance - # Cross-section increases with width - s_small = Strip(0.01, 0.012, 0.01, 10, copper_props) - s_large = Strip(0.01, 0.012, 0.1, 10, copper_props) - @test s_large.cross_section > s_small.cross_section - end - - @testset "Type Stability & Promotion" begin - # All Float64 - s = Strip(0.01, 0.012, 0.05, 10.0, copper_props) - @test typeof(s.r_in) == Float64 - # All measurement - sM = Strip( - measurement(0.01, 1e-5), - measurement(0.012, 1e-5), - measurement(0.05, 1e-4), - measurement(10.0, 0.1), - copper_props, - temperature = measurement(20.0, 0.1), - lay_direction = 1, - ) - @test typeof(sM.r_in) <: Measurement - # Mixed: r_in as measurement - sMix1 = Strip(measurement(0.01, 1e-5), 0.012, 0.05, 10.0, copper_props) - @test typeof(sMix1.r_in) <: Measurement - # Mixed: width as measurement - sMix2 = Strip(0.01, 0.012, measurement(0.05, 1e-4), 10.0, copper_props) - @test typeof(sMix2.width) <: Measurement - mmat = Material(measurement(1.7241e-8, 1e-10), 1.0, 1.0, 20.0, 0.00393) - sMix3 = Strip(0.01, 0.012, 0.05, 10.0, mmat, temperature = 20.0, lay_direction = 1) - @test typeof(sMix3.resistance) <: Measurement - end - -end diff --git a/test/unit_DataModel/test_Tubular.jl b/test/unit_DataModel/test_Tubular.jl deleted file mode 100644 index e13f54ec8..000000000 --- a/test/unit_DataModel/test_Tubular.jl +++ /dev/null @@ -1,113 +0,0 @@ -@testitem "DataModel(Tubular): constructor unit tests" setup = - [defaults, deps_datamodel, defs_materials] begin - # Input Validation - @testset "Input Validation" begin - material = Material(1.7241e-8, 1.0, 1.0, 20.0, 0.00393) - - # Missing required arguments - @test_throws ArgumentError Tubular() - @test_throws ArgumentError Tubular(0.01) - @test_throws ArgumentError Tubular(0.01, 0.02) - # Invalid types - @test_throws ArgumentError Tubular("0.01", 0.02, material) - @test_throws ArgumentError Tubular(0.01, "0.02", material) - @test_throws ArgumentError Tubular(0.01, 0.02, "material") - @test_throws ArgumentError Tubular(0.01, 0.02, material, temperature = "25") - @test_throws ArgumentError Tubular(-0.01, 0.02, material) - @test_throws ArgumentError Tubular(0.01, -0.02, material) - @test_throws ArgumentError Tubular(0.03, 0.02, material) - # Invalid nothing/missing - @test_throws ArgumentError Tubular(nothing, 0.02, material) - @test_throws ArgumentError Tubular(0.01, nothing, material) - @test_throws ArgumentError Tubular(0.01, 0.02, nothing) - @test_throws ArgumentError Tubular(missing, 0.02, material) - @test_throws ArgumentError Tubular(0.01, missing, material) - @test_throws ArgumentError Tubular(0.01, 0.02, material, temperature = missing) - end - - # Basic Functionality - @testset "Basic Functionality" begin - material = Material(1.7241e-8, 1.0, 1.0, 20.0, 0.00393) - t = Tubular(0.01, 0.02, material) - @test t isa Tubular - @test isapprox(t.r_in, 0.01, atol = TEST_TOL) - @test isapprox(t.r_ex, 0.02, atol = TEST_TOL) - @test t.material_props == material - @test isapprox(t.temperature, 20.0, atol = TEST_TOL) - @test isapprox(t.cross_section, π * (0.02^2 - 0.01^2), atol = TEST_TOL) - t2 = Tubular(t, Thickness(0.02), material) - @test t2 isa Tubular - @test isapprox(t2.r_in, t.r_ex, atol = TEST_TOL) - end - - # Edge Cases - @testset "Edge Cases" begin - material = Material(1.7241e-8, 1.0, 1.0, 20.0, 0.00393) - # Very small but positive thickness - eps = 1e-12 - t = Tubular(0.01, 0.01 + eps, material) - @test t.r_ex > t.r_in - @test t.cross_section > 0 - # Inf radii (should error) - @test_throws DomainError Tubular(0.01, Inf, material) - end - - # Physical Behavior - @testset "Physical Behavior" begin - material = Material(1.7241e-8, 1.0, 1.0, 20.0, 0.00393) - t1 = Tubular(0.01, 0.02, material) - t2 = Tubular(0.01, 0.03, material) - @test t2.cross_section > t1.cross_section - @test t2.resistance < t1.resistance - end - - # Type Stability & Promotion - @testset "Type Stability & Promotion" begin - material = Material(1.7241e-8, 1.0, 1.0, 20.0, 0.00393) - m = measurement(0.01, 0.001) - # All Float64 - t1 = Tubular(0.01, 0.02, material) - @test t1.r_in isa Float64 - # All Measurement - mmat = Material(measurement(1.7241e-8, 1e-10), 1.0, 1.0, 20.0, 0.00393) - t2 = Tubular(0.011, 0.021, mmat) - @test t2.r_in isa Measurement - # Mixed: r_in as Measurement - t3 = Tubular(m, 0.02, material) - @test t3.r_in isa Measurement - # Mixed: r_ex as Measurement - t4 = Tubular(0.001, m, material) - @test t4.r_ex isa Measurement - # Mixed: material_props as Measurement - t5 = Tubular(0.01, 0.02, mmat) - @test t5.material_props.rho isa Measurement - end - - @testset "Radius Input Parsing" begin - import LineCableModels.DataModel: _normalize_radii - # inner:Number, outer:Thickness - @test _normalize_radii(Tubular, 0.01, Thickness(0.02)) == (0.01, 0.03) - - # inner:Thickness, outer:Number - rin, rex = _normalize_radii(Tubular, Thickness(0.002), 0.02) - @test isapprox(rin, 0.018; atol = TEST_TOL) - @test isapprox(rex, 0.02; atol = TEST_TOL) - - - # inner:Thickness too large - @test_throws ArgumentError _normalize_radii(Tubular, Thickness(0.03), 0.02) - - # both Thickness → error - @test_throws ArgumentError _normalize_radii( - Tubular, - Thickness(0.001), - Thickness(0.002), - ) - - # diameter on either side collapses in parse: - @test _normalize_radii(Tubular, Diameter(0.02), 0.03) == (0.01, 0.03) - @test _normalize_radii(Tubular, 0.01, Diameter(0.02)) == (0.01, 0.01) - - - end -end diff --git a/test/unit_DataModel/test_WireArray.jl b/test/unit_DataModel/test_WireArray.jl deleted file mode 100644 index 1de8bfda6..000000000 --- a/test/unit_DataModel/test_WireArray.jl +++ /dev/null @@ -1,376 +0,0 @@ -@testitem "DataModel(CircStrands): constructor unit tests" setup = - [defaults, deps_datamodel, defs_materials] begin - - using Measurements - - @testset "Input Validation" begin - # Missing required arguments - @test_throws ArgumentError CircStrands() - @test_throws ArgumentError CircStrands(r_in = 0.01) - @test_throws ArgumentError CircStrands(r_in = 0.01, radius_wire = 0.002) - @test_throws ArgumentError CircStrands( - r_in = 0.01, - radius_wire = 0.002, - num_wires = 7, - ) - @test_throws ArgumentError CircStrands( - r_in = 0.01, - radius_wire = 0.002, - num_wires = 7, - lay_ratio = 10, - ) - - # Invalid types - @test_throws ArgumentError CircStrands( - "foo", - 0.002, - 7, - 10, - copper_props, - temperature = 20.0, - lay_direction = 1, - ) - @test_throws ArgumentError CircStrands( - 0.01, - "bar", - 7, - 10, - copper_props, - temperature = 20.0, - lay_direction = 1, - ) - @test_throws ArgumentError CircStrands( - 0.01, - 0.002, - "baz", - 10, - copper_props, - temperature = 20.0, - lay_direction = 1, - ) - @test_throws ArgumentError CircStrands( - 0.01, - 0.002, - 7, - "qux", - copper_props, - temperature = 20.0, - lay_direction = 1, - ) - @test_throws ArgumentError CircStrands( - 0.01, - 0.002, - 7, - 10, - "not_a_material", - temperature = 20.0, - lay_direction = 1, - ) - @test_throws ArgumentError CircStrands( - 0.01, - 0.002, - 7, - 10, - copper_props, - "not_a_temp", - lay_direction = 1, - ) - @test_throws ArgumentError CircStrands( - 0.01, - 0.002, - 7, - 10, - copper_props, - temperature = 20.0, - "not_a_dir", - ) - - # Out-of-range values - @test_throws ArgumentError CircStrands( - -0.01, - 0.002, - 7, - 10, - copper_props, - temperature = 20.0, - lay_direction = 1, - ) - @test_throws ArgumentError CircStrands( - 0.01, - -0.002, - 7, - 10, - copper_props, - temperature = 20.0, - lay_direction = 1, - ) - @test_throws ArgumentError CircStrands( - 0.01, - 0.002, - 0, - 10, - copper_props, - temperature = 20.0, - lay_direction = 1, - ) - @test_throws ArgumentError CircStrands( - 0.01, - 0.002, - 7, - 10, - copper_props, - temperature = 20.0, - lay_direction = 0, - ) - @test_throws ArgumentError CircStrands( - 0.01, - 0.002, - 7, - 10, - copper_props, - temperature = 20.0, - lay_direction = 2, - ) - @test_throws ArgumentError CircStrands( - 0.01, - 0.002, - 7, - 10, - copper_props, - temperature = 20.0, - lay_direction = -2, - ) - - # Geometrically impossible values - @test_throws ArgumentError CircStrands( - 0.01, - 0.0, - 7, - 10, - copper_props, - temperature = 20.0, - lay_direction = 1, - ) - - # Invalid nothing/missing - @test_throws ArgumentError CircStrands( - nothing, - 0.002, - 7, - 10, - copper_props, - temperature = 20.0, - lay_direction = 1, - ) - @test_throws ArgumentError CircStrands( - 0.01, - nothing, - 7, - 10, - copper_props, - temperature = 20.0, - lay_direction = 1, - ) - @test_throws ArgumentError CircStrands( - 0.01, - 0.002, - nothing, - 10, - copper_props, - temperature = 20.0, - lay_direction = 1, - ) - @test_throws ArgumentError CircStrands( - 0.01, - 0.002, - 7, - nothing, - copper_props, - temperature = 20.0, - lay_direction = 1, - ) - @test_throws ArgumentError CircStrands( - 0.01, - 0.002, - 7, - 10, - nothing, - temperature = 20.0, - lay_direction = 1, - ) - @test_throws ArgumentError CircStrands( - 0.01, - 0.002, - 7, - 10, - copper_props, - temperature = nothing, - lay_direction = 1, - ) - @test_throws ArgumentError CircStrands( - 0.01, - 0.002, - 7, - 10, - copper_props, - temperature = 20.0, - lay_direction = nothing, - ) - end - - @testset "Basic Functionality" begin - w = CircStrands( - 0.01, - 0.002, - 7, - 10, - copper_props, - temperature = 20.0, - lay_direction = 1, - ) - @test w isa CircStrands - @test w.r_in ≈ 0.01 atol = TEST_TOL - @test w.radius_wire ≈ 0.002 atol = TEST_TOL - @test w.num_wires == 7 - @test w.lay_ratio ≈ 10 atol = TEST_TOL - @test w.material_props === copper_props - @test w.temperature ≈ 20.0 atol = TEST_TOL - @test w.lay_direction == 1 - @test w.cross_section ≈ 7 * π * 0.002^2 atol = TEST_TOL - # Measurement type - w2 = CircStrands( - measurement(0.01, 1e-5), - measurement(0.002, 1e-6), - 7, - 10, - copper_props, - temperature = measurement(20.0, 0.1), - lay_direction = 1, - ) - @test w2 isa CircStrands - @test value(w2.r_in) ≈ 0.01 atol = TEST_TOL - @test value(w2.radius_wire) ≈ 0.002 atol = TEST_TOL - @test value(w2.temperature) ≈ 20.0 atol = TEST_TOL - end - - @testset "Edge Cases" begin - # r_in very close to r_ex - w = CircStrands( - 1e-6, - 0.002, - 7, - 10, - copper_props, - temperature = 20.0, - lay_direction = 1, - ) - @test w.r_in ≈ 1e-6 atol = TEST_TOL - # num_wires = 1 (should set r_ex = radius_wire) - w1 = CircStrands( - 0.0, - 0.002, - 1, - 10, - copper_props, - temperature = 20.0, - lay_direction = 1, - ) - @test w1.r_ex ≈ 0.002 atol = TEST_TOL - end - - @testset "Physical Behavior" begin - # Resistance should increase with temperature - w20 = CircStrands( - 0.01, - 0.002, - 7, - 10, - copper_props, - temperature = 20.0, - lay_direction = 1, - ) - w80 = CircStrands( - 0.01, - 0.002, - 7, - 10, - copper_props, - temperature = 80.0, - lay_direction = 1, - ) - @test w80.resistance > w20.resistance - # Cross-section increases with wire radius - w_small = CircStrands( - 0.01, - 0.001, - 7, - 10, - copper_props, - temperature = 20.0, - lay_direction = 1, - ) - w_large = CircStrands( - 0.01, - 0.003, - 7, - 10, - copper_props, - temperature = 20.0, - lay_direction = 1, - ) - @test w_large.cross_section > w_small.cross_section - end - - @testset "Type Stability & Promotion" begin - # All Float64 - w = CircStrands( - 0.01, - 0.002, - 7, - 10.0, - copper_props, - temperature = 20.0, - lay_direction = 1, - ) - @test typeof(w.r_in) == Float64 - # All Measurement - wM = CircStrands( - measurement(0.01, 1e-5), - measurement(0.002, 1e-6), - 7, - measurement(10.0, 0.1), - copper_props, - temperature = measurement(20.0, 0.1), - lay_direction = 1, - ) - @test typeof(wM.r_in) <: Measurement - # Mixed: r_in as Measurement - wMix1 = CircStrands( - measurement(0.01, 1e-5), - 0.002, - 7, - 10.0, - copper_props, - temperature = 20.0, - lay_direction = 1, - ) - @test typeof(wMix1.r_in) <: Measurement - # Mixed: lay_ratio as Measurement - wMix2 = CircStrands( - 0.01, - 0.002, - 7, - measurement(10.0, 0.1), - copper_props, - temperature = 20.0, - lay_direction = 1, - ) - @test typeof(wMix2.lay_ratio) <: Measurement - # material as measurement - mmat = Material(measurement(1.7241e-8, 1e-10), 1.0, 1.0, 20.0, 0.00393) - wMix3 = - CircStrands(0.01, 0.002, 7, 10.0, mmat, temperature = 20.0, lay_direction = 1) - @test typeof(wMix3.resistance) <: Measurement - end - -end diff --git a/test/unit_DataModel/test_equivalent.jl b/test/unit_DataModel/test_equivalent.jl deleted file mode 100644 index 798dc0e7c..000000000 --- a/test/unit_DataModel/test_equivalent.jl +++ /dev/null @@ -1,193 +0,0 @@ -@testsnippet simplify_fixtures begin - # Aliases - const LM = LineCableModels - const DM = LM.DataModel - const MAT = LM.Materials - using Measurements: measurement - - # Basic materials - copper_props = MAT.Material(1.7241e-8, 1.0, 1.0, 20.0, 0.00393) - xlpe_props = MAT.Material(1e10, 2.3, 1.0, 20.0, 0.0) - semi_props = MAT.Material(1e3, 2.6, 1.0, 20.0, 0.0) - - # Geometry helpers - d_wire = 3e-3 - rin0 = 0.0 - - function make_conductor_group() - core = DM.CircStrands(rin0, DM.Diameter(d_wire), 1, 0.0, copper_props) - g = DM.ConductorGroup(core) - add!(g, DM.CircStrands, DM.Diameter(d_wire), 6, 10.0, copper_props) - add!(g, DM.Strip, DM.Thickness(0.5e-3), 0.02, 8.0, copper_props) - add!(g, DM.Tubular, DM.Thickness(0.8e-3), copper_props) - g - end - - function make_insulator_group(conductor_group) - ins1 = DM.Insulator(conductor_group.r_ex, DM.Thickness(2.0e-3), xlpe_props) - ig = DM.InsulatorGroup(ins1) - add!(ig, DM.Semicon, DM.Thickness(0.8e-3), semi_props) - add!(ig, DM.Insulator, DM.Thickness(2.0e-3), xlpe_props) - ig - end - - function make_component(id::AbstractString) - g = make_conductor_group() - ig = make_insulator_group(g) - DM.CableComponent(String(id), g, ig) - end - - function make_design(id::AbstractString; ncomponents::Int = 1) - comps = [make_component(n == 1 ? "core" : "comp$(n)") for n in 1:ncomponents] - des = DM.CableDesign(String(id), comps[1]) - for c in comps[2:end] - add!(des, c.id, c.conductor_group, c.insulator_group) - end - des - end -end - -@testitem "simplify unit tests" setup = - [defaults, deps_datamodel, defs_materials, simplify_fixtures] begin - const DM = LineCableModels.DataModel - - @testset "Input Validation" begin - des = make_design("CAB-V-0") - - # Missing required positional argument - @test_throws MethodError DM.equivalent() - - # Invalid first argument type - @test_throws MethodError DM.equivalent(42) - - # Invalid keyword type for new_id - @test_throws TypeError DM.equivalent(des; new_id = 123) - - - end - - @testset "Basic Functionality" begin - des = make_design("CAB-BASIC"; ncomponents = 2) - des_s = DM.equivalent(des) - - @test des_s isa DM.CableDesign - @test des_s.cable_id == "CAB-BASIC_equivalent" - @test length(des_s.components) == length(des.components) - - # Geometry continuity: outer radius preserved by equivalence - for (orig, simp) in zip(des.components, des_s.components) - @test simp.conductor_group.r_in ≈ orig.conductor_group.r_in atol = - TEST_TOL - @test simp.conductor_group.r_ex ≈ orig.conductor_group.r_ex atol = - TEST_TOL - @test simp.insulator_group.r_ex ≈ orig.insulator_group.r_ex atol = - TEST_TOL - @test simp.id == orig.id - end - - # new_id override - des_s2 = DM.equivalent(des; new_id = "CAB-SIMPLE") - @test des_s2.cable_id == "CAB-SIMPLE" - end - - @testset "Equivalence Preservation" begin - # The simplified design must preserve the component-equivalent properties - des = make_design("CAB-EQ"; ncomponents = 2) - des_s = DM.equivalent(des) - - for (orig, simp) in zip(des.components, des_s.components) - # Compare equivalent material properties (conductor) - @test simp.conductor_props.rho ≈ orig.conductor_props.rho atol = TEST_TOL - @test simp.conductor_props.eps_r ≈ orig.conductor_props.eps_r atol = TEST_TOL - @test simp.conductor_props.mu_r ≈ orig.conductor_props.mu_r atol = TEST_TOL - @test simp.conductor_props.T0 ≈ orig.conductor_props.T0 atol = TEST_TOL - @test simp.conductor_props.alpha ≈ orig.conductor_props.alpha atol = TEST_TOL - - # Compare equivalent material properties (insulator) - @test simp.insulator_props.rho ≈ orig.insulator_props.rho atol = TEST_TOL - @test simp.insulator_props.eps_r ≈ orig.insulator_props.eps_r atol = TEST_TOL - @test simp.insulator_props.mu_r ≈ orig.insulator_props.mu_r atol = TEST_TOL - @test simp.insulator_props.T0 ≈ orig.insulator_props.T0 atol = TEST_TOL - @test simp.insulator_props.alpha ≈ orig.insulator_props.alpha atol = TEST_TOL - - # Compare group lumped parameters (should be preserved by construction) - @test simp.conductor_group.resistance ≈ orig.conductor_group.resistance atol = - TEST_TOL - @test simp.conductor_group.gmr ≈ orig.conductor_group.gmr atol = TEST_TOL - @test simp.insulator_group.shunt_capacitance ≈ - orig.insulator_group.shunt_capacitance atol = TEST_TOL - @test simp.insulator_group.shunt_conductance ≈ - orig.insulator_group.shunt_conductance atol = TEST_TOL - end - end - - @testset "Edge Cases" begin - # Use Measurement geometry to ensure robustness with promoted numeric types - des = make_design("CAB-EDGE") - desM = DM.CableDesign( - "CAB-EDGE-M", - DM.CableComponent( - des.components[1].id, - DM.coerce_to_T( - des.components[1].conductor_group, - Measurements.Measurement{Float64}, - ), - DM.coerce_to_T( - des.components[1].insulator_group, - Measurements.Measurement{Float64}, - ), - ), - ) - - des_sM = DM.equivalent(desM) - @test typeof(des_sM.components[1].conductor_group.r_in) <: - Measurements.Measurement - @test typeof(des_sM.components[1].conductor_group.r_ex) <: - Measurements.Measurement - @test typeof(des_sM.components[1].insulator_group.r_in) <: - Measurements.Measurement - @test typeof(des_sM.components[1].insulator_group.r_ex) <: - Measurements.Measurement - end - - @testset "Physical Behavior" begin - des = make_design("CAB-PHYS") - des_s = DM.equivalent(des) - for comp in des_s.components - @test comp.conductor_props.rho > 0 - @test comp.conductor_group.gmr > 0 - @test comp.insulator_props.eps_r > 0 - @test comp.insulator_group.shunt_capacitance > 0 - end - end - - @testset "Type Stability & Promotion" begin - des = make_design("CAB-TYPES") - cF = des.components[1] - - # Base: Float64 -> Float64 - desF = DM.CableDesign("CAB-F", cF) - sF = DM.equivalent(desF) - @test eltype([sF.components[1].conductor_group.r_in]) == Float64 - - # Fully promoted: Measurement -> Measurement - gM = DM.coerce_to_T(cF.conductor_group, Measurements.Measurement{Float64}) - igM = DM.coerce_to_T(cF.insulator_group, Measurements.Measurement{Float64}) - desM = DM.CableDesign("CAB-M", DM.CableComponent("coreM", gM, igM)) - sM = DM.equivalent(desM) - @test typeof(sM.components[1].conductor_group.r_in) <: Measurements.Measurement - @test typeof(sM.components[1].insulator_group.r_ex) <: - Measurements.Measurement - - # Mixed cases - desC = DM.CableDesign("CAB-C", DM.CableComponent("c1", gM, cF.insulator_group)) - sC = DM.equivalent(desC) - @test typeof(sC.components[1].conductor_group.r_in) <: Measurements.Measurement - @test typeof(sC.components[1].insulator_group.r_in) <: Measurements.Measurement - - desI = DM.CableDesign("CAB-I", DM.CableComponent("c2", cF.conductor_group, igM)) - sI = DM.equivalent(desI) - @test typeof(sI.components[1].conductor_group.r_in) <: Measurements.Measurement - @test typeof(sI.components[1].insulator_group.r_in) <: Measurements.Measurement - end -end diff --git a/test/unit_DataModel/test_uncertainty_covariance.jl b/test/unit_DataModel/test_uncertainty_covariance.jl deleted file mode 100644 index 1c0bb3b78..000000000 --- a/test/unit_DataModel/test_uncertainty_covariance.jl +++ /dev/null @@ -1,80 +0,0 @@ -@testitem "Utils(coerce_to_T): Measurement covariance preservation" setup = [defaults] begin - using Measurements - import Measurements: derivative - - @testset "Exact-type coercion is an identity operation" begin - x = measurement(2.0, 0.1) - y = 3x - - @test coerce_to_T(x, typeof(x)) === x - @test coerce_to_T(y, typeof(y)) === y - @test derivative(coerce_to_T(y, typeof(y)), x) ≈ 3.0 - end - - @testset "Inner precision conversion retains the dependency graph" begin - x = measurement(2.0, 0.1) - y = 3x - x32 = coerce_to_T(x, Measurement{Float32}) - y32 = coerce_to_T(y, Measurement{Float32}) - - @test x32.tag == x.tag - @test y32.tag == y.tag - @test derivative(y32, x32) ≈ Float32(3.0) - end - - @testset "Mixed BaseParams calls retain primitive sensitivities" begin - r_ex = measurement(0.02, 0.001) - rho = measurement(1.7241e-8, 1.0e-9) - R = calc_tubular_resistance(0.0, r_ex, rho, 0.0, 20.0, 20.0) - - @test !iszero(derivative(R, r_ex)) - @test !iszero(derivative(R, rho)) - end -end - -@testitem "DataModel: covariance survives the complete assembly path" setup = [defaults] begin - using Measurements - import Measurements: derivative - copper_props = Material(1.7241e-8, 1.0, 1.0, 20.0, 0.00393) - insulator_props = Material(1.0e14, 2.3, 1.0, 20.0, 0.0) - semicon_props = Material(1000.0, 1000.0, 1.0, 20.0, 0.0) - - diameter = measurement(0.02, 0.001) - insulation_thickness = measurement(0.01, 0.001) - semicon_thickness = measurement(0.005, 0.0005) - - core = Tubular(0.0, Diameter(diameter), copper_props) - insulators = InsulatorGroup( - Insulator(core, Thickness(insulation_thickness), insulator_props), - ) - - @test insulators.r_in === core.r_ex - @test iszero(uncertainty(core.r_ex - insulators.r_in)) - - insulators = add!( - insulators, - Semicon, - Thickness(semicon_thickness), - semicon_props, - ) - - expected_radius = diameter / 2 + insulation_thickness + semicon_thickness - @test value(insulators.r_ex) ≈ value(expected_radius) - @test uncertainty(insulators.r_ex) ≈ uncertainty(expected_radius) - @test derivative(insulators.r_ex, diameter) ≈ 0.5 - @test derivative(insulators.r_ex, insulation_thickness) ≈ 1.0 - @test derivative(insulators.r_ex, semicon_thickness) ≈ 1.0 - - conductors = ConductorGroup(core) - component = CableComponent("core", conductors, insulators) - design = CableDesign("uq-path", component) - position = CablePosition(design, 0.0, -1.0, Dict("core" => 1)) - system = LineCableSystem("uq-path", 1000.0, position) - assembled_radius = - system.cables[1].design_data.components[1].insulator_group.r_ex - - @test system.cables[1].design_data === design - @test derivative(assembled_radius, diameter) ≈ 0.5 - @test derivative(assembled_radius, insulation_thickness) ≈ 1.0 - @test derivative(assembled_radius, semicon_thickness) ≈ 1.0 -end diff --git a/test/unit_Engine/test_parallel_rc_insulation.jl b/test/unit_Engine/test_parallel_rc_insulation.jl deleted file mode 100644 index 7c2802ef6..000000000 --- a/test/unit_Engine/test_parallel_rc_insulation.jl +++ /dev/null @@ -1,316 +0,0 @@ -@testitem "Engine(ParallelRC): analytical layer and lossless limit" setup = [defaults] begin - using LinearAlgebra - - formulation = InsulationAdmittance.ParallelRC() - lossless = InsulationAdmittance.Lossless() - - r_in = 0.010 - r_ex = 0.018 - rho = 2.0e11 - eps_r = 2.4 - s = Complex(0.0, 2π * 50.0) - - log_ratio = log(r_ex / r_in) - capacitance = 2π * ε₀ * eps_r / log_ratio - conductance = 2π / (rho * log_ratio) - p = formulation(r_in, r_ex, rho, eps_r, s) - - @test s / p ≈ conductance + s * capacitance - @test real(s / p) > 0 - @test imag(s / p) > 0 - - # Radially stacked layers are series admittances, hence their potential - # coefficients add before the solver constructs the Maxwell matrix. - r_mid = 0.013 - rho_2 = 8.0e10 - eps_r_2 = 3.6 - p_1 = formulation(r_in, r_mid, rho, eps_r, s) - p_2 = formulation(r_mid, r_ex, rho_2, eps_r_2, s) - y_1 = s / p_1 - y_2 = s / p_2 - y_series = inv(inv(y_1) + inv(y_2)) - @test s / (p_1 + p_2) ≈ y_series - - @test formulation(r_in, r_ex, Inf, eps_r, s) ≈ - lossless(r_in, r_ex, eps_r, s, 0.0) -end - -@testitem "Engine(ParallelRC): physical layers reach the full EMT solve" setup = [defaults] begin - using LinearAlgebra - - function two_terminal_problem(; uncertain = false) - copper = Material(1.7241e-8, 1.0, 1.0, 20.0, 0.00393) - rho_1 = uncertain ? measurement(2.0e11, 2.0e10) : 2.0e11 - eps_1 = uncertain ? measurement(2.4, 0.12) : 2.4 - t_1 = uncertain ? measurement(3.0e-3, 1.5e-4) : 3.0e-3 - dielectric_1 = Material(rho_1, eps_1, 1.0, 20.0, 0.0) - dielectric_2 = Material(8.0e10, 3.6, 1.0, 20.0, 0.0) - outer_dielectric = Material(1.0e14, 2.3, 1.0, 20.0, 0.0) - - core_conductor = ConductorGroup(Tubular(0.0, Diameter(0.020), copper)) - core_insulation = InsulatorGroup( - Insulator(core_conductor, Thickness(t_1), dielectric_1), - ) - core_insulation = add!( - core_insulation, - Insulator, - Thickness(4.0e-3), - dielectric_2, - ) - core = CableComponent("core", core_conductor, core_insulation) - - sheath_conductor = ConductorGroup( - Tubular(core_insulation, Thickness(1.0e-3), copper), - ) - sheath_insulation = InsulatorGroup( - Insulator(sheath_conductor, Thickness(2.0e-3), outer_dielectric), - ) - sheath = CableComponent("sheath", sheath_conductor, sheath_insulation) - - design = CableDesign("parallel-rc-test", core) - design = add!(design, sheath) - position = CablePosition( - design, - 0.0, - -1.0, - Dict("core" => 1, "sheath" => 2), - ) - system = LineCableSystem("parallel-rc-test", 1000.0, position) - frequencies = [1.0e-3, 50.0, 1.0e6] - earth = EarthModel(frequencies, 100.0, 10.0, 1.0) - return LineParametersProblem( - system; - temperature = 20.0, - earth_props = earth, - frequencies = frequencies, - ) - end - - formulation = FormulationSet( - Val(:EMT); - insulation_admittance = InsulationAdmittance.ParallelRC(), - modal_transform = nothing, - options = ( - reduce_bundle = false, - kron_reduction = false, - ideal_transposition = true, - store_primitive_matrices = true, - ), - ) - - problem = two_terminal_problem() - ws, line_parameters = compute!(problem, formulation) - - @test ws.insulator_layer_ranges == [1:2, 3:3] - @test ws.r_ins_layer_in[2] ≈ ws.r_ins_layer_ext[1] - @test size(line_parameters.Y) == (2, 2, 3) - - for k in eachindex(ws.freq) - s = ws.jω[k] - expected_p = sum( - formulation.insulation_admittance( - ws.r_ins_layer_in[layer_idx], - ws.r_ins_layer_ext[layer_idx], - ws.rho_ins_layer[layer_idx], - ws.eps_ins_layer[layer_idx], - s, - ) - for layer_idx in ws.insulator_layer_ranges[1] - ) - @test ws.Pin[1, 1, k] ≈ expected_p - - Y = line_parameters.Y.values[:, :, k] - @test Y ≈ transpose(Y) - @test all(isfinite, real.(Y)) - @test all(isfinite, imag.(Y)) - G = real.(Y) - tolerance = 1.0e-9 * max(opnorm(G), eps()) - @test minimum(eigvals(Symmetric(G))) >= -tolerance - end - - # Conductive leakage dominates the inner radial branch near DC, whereas - # capacitive current dominates it at the top of the frequency sweep. - y_near_dc = ws.jω[1] / ws.Pin[1, 1, 1] - y_high = ws.jω[end] / ws.Pin[1, 1, end] - @test real(y_near_dc) > imag(y_near_dc) - @test imag(y_high) > real(y_high) - - # LEP must survive primitives -> components -> design -> system -> Z/Y. - ws_uq, line_parameters_uq = compute!(two_terminal_problem(uncertain = true), formulation) - y_gap_uq = ws_uq.jω[2] / ws_uq.Pin[1, 1, 2] - @test uncertainty(real(y_gap_uq)) > 0 - @test uncertainty(imag(y_gap_uq)) > 0 - @test uncertainty(real(line_parameters_uq.Y[1, 1, 2])) > 0 - @test uncertainty(imag(line_parameters_uq.Y[1, 1, 2])) > 0 -end - -@testitem "UQ.mc: ParallelRC is sampled before line-parameter assembly" setup = [defaults] begin - using Random - using Statistics - using LineCableModels.ParametricBuilder: - CableBuilder, Conductor, Insulator, Material, Earth, SystemBuilder, at - import LineCableModels.UQ - - copper = Material( - rho = 1.7241e-8, - eps_r = 1.0, - mu_r = 1.0, - T0 = 20.0, - alpha = 0.00393, - ) - dielectric = Material( - rho = (2.0e11, 10.0), - eps_r = (2.4, 5.0), - mu_r = 1.0, - T0 = 20.0, - alpha = 0.0, - ) - outer_dielectric = Material( - rho = 1.0e14, - eps_r = 2.3, - mu_r = 1.0, - T0 = 20.0, - alpha = 0.0, - ) - - parts = [ - Conductor.Solid(:core; d = 0.020, m = copper), - Insulator.Tubular(:core; layers = 1, t = (7.0e-3, 5.0), m = dielectric), - Conductor.Tubular(:sheath; layers = 1, t = 1.0e-3, m = copper), - Insulator.Tubular(:sheath; layers = 1, t = 2.0e-3, m = outer_dielectric), - ] - builder = CableBuilder("parallel-rc-mc", parts; nominal = NominalData()) - spec = SystemBuilder( - "parallel-rc-mc", - builder, - at( - x = 0.0, - y = -1.0, - phases = (:core => 1, :sheath => 2), - ); - length = 1000.0, - temperature = 20.0, - earth = Earth(rho = 100.0, eps_r = 10.0, mu_r = 1.0), - f = [50.0, 500.0], - ) - formulation = FormulationSet( - Val(:EMT); - insulation_admittance = InsulationAdmittance.ParallelRC(), - modal_transform = nothing, - options = ( - reduce_bundle = false, - kron_reduction = false, - ideal_transposition = true, - ), - ) - - result = UQ.mc( - spec, - formulation; - trials = 12, - distribution = :normal, - seed = 20260812, - print_step = 1000, - return_samples = true, - ) - - @test size(result.measurements.Y) == (2, 2, 2) - @test size(result.samples.G) == (2, 2, 2, 12) - @test std(result.samples.G[1, 1, 1, :]) > 0 - @test std(result.samples.C[1, 1, 1, :]) > 0 - @test uncertainty(real(result.measurements.Y[1, 1, 1])) > 0 - @test uncertainty(imag(result.measurements.Y[1, 1, 1])) > 0 - - ω = reshape(2π .* result.f, 1, 1, :) - Rmeas = real.(result.measurements.Z.values) - Lmeas = imag.(result.measurements.Z.values) ./ ω - Gmeas = real.(result.measurements.Y.values) - Cmeas = imag.(result.measurements.Y.values) ./ ω - X = vcat( - reshape(result.samples.R, :, 12), - reshape(result.samples.L, :, 12), - reshape(result.samples.G, :, 12), - reshape(result.samples.C, :, 12), - ) - joint = vcat(vec(Rmeas), vec(Lmeas), vec(Gmeas), vec(Cmeas)) - μ = vec(mean(X; dims = 2)) - centered = X .- μ - empirical_covariance = centered * transpose(centered) / 11 - - @test value.(joint) ≈ μ - @test uncertainty.(joint) ≈ vec(std(X; dims = 2)) - @test Measurements.cov(joint) ≈ empirical_covariance - - for t in (1, 6, 12) - lp_trial = UQ.trial(result, t) - expected_Z = - result.samples.R[:, :, :, t] .+ - im .* ω .* result.samples.L[:, :, :, t] - expected_Y = - result.samples.G[:, :, :, t] .+ - im .* ω .* result.samples.C[:, :, :, t] - @test domain(lp_trial) === domain(result) - @test lp_trial.f == result.f - @test lp_trial.Z.values == expected_Z - @test lp_trial.Y.values == expected_Y - end - @test_throws BoundsError UQ.trial(result, 0) - @test_throws BoundsError UQ.trial(result, 13) - - index_rng = MersenneTwister(9182) - adapter_rng = MersenneTwister(9182) - for _ in 1:5 - t = rand(index_rng, axes(result.samples.R, 4)) - expected = UQ.trial(result, t) - actual = rand(adapter_rng, result) - @test actual.Z.values == expected.Z.values - @test actual.Y.values == expected.Y.values - end - @test rand(result) isa LineParameters - - for component in (:R, :L, :G, :C) - samples = getproperty(result.samples, component) - stats = getproperty(result.stats, component) - for index in CartesianIndices(stats) - trials = @view samples[index, :] - @test stats[index].mean ≈ mean(trials) - @test stats[index].std ≈ std(trials) - end - end - - repeated = UQ.mc( - spec, - formulation; - trials = 12, - distribution = :normal, - seed = 20260812, - print_step = 1000, - return_samples = true, - ) - @test repeated.samples == result.samples - @test value.(repeated.measurements.Z.values) == value.(result.measurements.Z.values) - @test value.(repeated.measurements.Y.values) == value.(result.measurements.Y.values) - @test Measurements.cov(vcat( - vec(real.(repeated.measurements.Z.values)), - vec(imag.(repeated.measurements.Z.values) ./ ω), - vec(real.(repeated.measurements.Y.values)), - vec(imag.(repeated.measurements.Y.values) ./ ω), - )) ≈ empirical_covariance - - single = UQ.mc( - spec, - formulation; - trials = 1, - seed = 20260812, - print_step = 1000, - ) - for component in (:R, :L, :G, :C) - @test all(iszero(stat.std) for stat in getproperty(single.stats, component)) - end - @test all(iszero ∘ uncertainty ∘ real, single.measurements.Z.values) - @test all(iszero ∘ uncertainty ∘ imag, single.measurements.Z.values) - @test all(iszero ∘ uncertainty ∘ real, single.measurements.Y.values) - @test all(iszero ∘ uncertainty ∘ imag, single.measurements.Y.values) - @test_throws ArgumentError UQ.trial(single, 1) - @test_throws ArgumentError rand(MersenneTwister(1), single) -end diff --git a/test/unit_ImportExport/test_export_data_atp.jl b/test/unit_ImportExport/test_export_data_atp.jl deleted file mode 100644 index 0d1e78e86..000000000 --- a/test/unit_ImportExport/test_export_data_atp.jl +++ /dev/null @@ -1,181 +0,0 @@ -@testsnippet deps_export_atp begin - using EzXML -end - -# TODO: test if serialization works properly if uncertain types are used (Measurements) - -@testitem "ImportExport(export_data::atp): export LineCableSystem -> LCC data" setup = - [defaults, cable_system_export, deps_export_atp] begin - - - # 1. ARRANGE & ACT: Run the export in a temporary directory - mktempdir(joinpath(@__DIR__)) do tmpdir - output_file = joinpath(tmpdir, "atp_export_test.xml") - result_path = export_data(:atp, cable_system, earth_props, file_name = output_file) - expected_file = joinpath( - dirname(output_file), - "$(cable_system.system_id)_$(basename(output_file))", - ) - - # 2. ASSERT: Basic file checks (exporter prefixes basename with system_id) - @test result_path == expected_file - @test isfile(expected_file) - @test filesize(expected_file) > 500 - - # 3. ASSERT: General XML structure and LCC data - @info " Performing high-level XML structure checks..." - doc = readxml(expected_file) - root_node = root(doc) - - @test nodename(root_node) == "project" - @test root_node["Application"] == "ATPDraw" - - # Find the main LCC component content node - comp_content_node = findfirst("/project/objects/comp/comp_content", root_node) - @test !isnothing(comp_content_node) - - # Verify general parameters like Length, Freq, and Ground Resistivity - @info " Verifying general LCC data (Length, Freq, Grnd resis)..." - @test parse( - Float64, - findfirst("data[@Name='Length']", comp_content_node)["Value"], - ) ≈ cable_system.line_length - @test parse(Float64, findfirst("data[@Name='Freq']", comp_content_node)["Value"]) ≈ - problem_atp.frequencies[1] - @test parse( - Float64, - findfirst("data[@Name='Grnd resis']", comp_content_node)["Value"], - ) ≈ problem_atp.earth_props.layers[end].base_rho_g - - # 4. ASSERT: Detailed validation of ALL cables and conductors - @info " Verifying all cables and their conductors..." - lcc_node = findfirst("/project/objects/comp/LCC", root_node) - cable_header = findfirst("cable_header", lcc_node) - cable_nodes = findall("cable", cable_header) - - @test length(cable_nodes) == num_phases - - # Loop through each cable exported in the XML and compare it to the source - for (i, cable_node) in enumerate(cable_nodes) - @info " -> Checking Cable #$i..." - source_cable = cable_system.cables[i] - - # Verify position of EACH cable - @test parse(Float64, cable_node["PosX"]) ≈ source_cable.horz - @test parse(Float64, cable_node["PosY"]) ≈ source_cable.vert - - # Verify the number of conductor components inside this cable - num_components = length(source_cable.design_data.components) - @test parse(Int, cable_node["NumCond"]) == num_components - - conductor_nodes = findall("conductor", cable_node) - @test length(conductor_nodes) == num_components - - # Loop through each conductor component within the cable - for (j, conductor_node) in enumerate(conductor_nodes) - source_component = source_cable.design_data.components[j] - cond_group = source_component.conductor_group - cond_props = source_component.conductor_props - ins_group = source_component.insulator_group - ins_props = source_component.insulator_props - - expected_radius_in = cond_group.r_in - expected_radius_ext = cond_group.r_ex - expected_rho = cond_props.rho - expected_muC = cond_props.mu_r - expected_epsI = ins_props.eps_r - expected_muI = ins_props.mu_r - expected_Cext = ins_group.shunt_capacitance - expected_Gext = ins_group.shunt_conductance - - # Assert that every attribute matches the expected value - @test parse(Float64, conductor_node["Rin"]) ≈ expected_radius_in - @test parse(Float64, conductor_node["Rout"]) ≈ expected_radius_ext - @test parse(Float64, conductor_node["rho"]) ≈ expected_rho - @test parse(Float64, conductor_node["muC"]) ≈ expected_muC - @test parse(Float64, conductor_node["muI"]) ≈ expected_muI - @test parse(Float64, conductor_node["epsI"]) ≈ expected_epsI - @test parse(Float64, conductor_node["Cext"]) ≈ expected_Cext - @test parse(Float64, conductor_node["Gext"]) ≈ expected_Gext - end - end - @info " All detailed checks passed!" - end -end - - - - -@testitem "ImportExport(export_data::atp): export LineParameters -> ZY matrices" setup = - [defaults, cable_system_export, deps_export_atp] begin - - - - # 1. RUN THE TEST IN A TEMPORARY DIRECTORY - mktempdir(joinpath(@__DIR__)) do tmpdir - output_file = joinpath(tmpdir, "atp_export_test.xml") - @info " Exporting ATP XML file to: $output_file" - Z_matrix = randn(ComplexF64, num_phases, num_phases, length(freqs)) - Y_matrix = randn(ComplexF64, num_phases, num_phases, length(freqs)) - line_params = LineParameters(Z_matrix, Y_matrix, freqs) - - # Call the function we want to test (use the LineParameters overload and pass freqs) - result_path = export_data( - :atp, - line_params; - file_name = output_file, - cable_system = cable_system, - ) - expected_file = joinpath( - dirname(output_file), - "$(cable_system.system_id)_$(basename(output_file))", - ) - - # 2. BASIC FILE CHECKS - @test result_path == expected_file - @test isfile(expected_file) - @test filesize(expected_file) > 100 - - xml_content = read(expected_file, String) - @test occursin("", xml_content) - - # 3. XML STRUCTURE AND DATA VALIDATION - @info " Performing XML structure checks via XPath..." - xml_doc = readxml(expected_file) - root_node = root(xml_doc) - - @test nodename(root_node) == "ZY" - @test parse(Int, root_node["NumPhases"]) == num_phases - - # Search the whole document for Z blocks (safer) and assert presence before indexing - z_blocks = findall("//Z", xml_doc) - @test !isempty(z_blocks) - @test length(z_blocks) == length(freqs) - - # 4. DETAILED DATA VERIFICATION (for the first frequency) - @info " Verifying numerical data for first frequency..." - first_z_block = z_blocks[1] - @test parse(Float64, first_z_block["Freq"]) ≈ freqs[1] - - z_matrix_rows = split(strip(nodecontent(first_z_block)), '\n') - @test length(z_matrix_rows) == num_phases - - first_row_elements = split(z_matrix_rows[1], ',') - @test length(first_row_elements) == num_phases - number_pattern = r"(-?[\d\.]+E[+-]\d+)" - complex_pattern = Regex("$(number_pattern.pattern)([+-][\\d\\.]+E[+-]\\d+)i") - - match_result = match(complex_pattern, first_row_elements[1]) - - if !isnothing(match_result) - # The captures are now guaranteed to be valid Float64 strings - real_part = parse(Float64, match_result.captures[1]) - imag_part = parse(Float64, match_result.captures[2]) - parsed_z11 = complex(real_part, imag_part) - - expected_z11 = Z_matrix[1, 1, 1] - @test parsed_z11 ≈ expected_z11 rtol = 1e-12 - end - end -end diff --git a/test/unit_ParametricBuilder/test_parametricbuilder.jl b/test/unit_ParametricBuilder/test_parametricbuilder.jl deleted file mode 100644 index c3616f46d..000000000 --- a/test/unit_ParametricBuilder/test_parametricbuilder.jl +++ /dev/null @@ -1,388 +0,0 @@ -@testitem "ParametricBuilder(SystemBuilderSpec): combinatorics + value integrity" setup = - [defaults] begin - # ------------------------------------------------------------------------- - # Shared setup (mirrors your example, but we toggle `unc` per testset) - # ------------------------------------------------------------------------- - using LineCableModels - using LineCableModels.ParametricBuilder: - CableBuilder, build, Conductor, Insulator, Material, Earth, SystemBuilder, at, - make_stranded, make_screened, cardinality - using LineCableModels.DataModel: trifoil_formation, LineCableSystem, CablePosition - using Measurements - - # deterministic frequency grid - f = 10.0 .^ range(0, stop = 6, length = 10) - - # Materials library - materials = MaterialsLibrary(add_defaults = true) - - # deterministic geometry - t_sct = 0.3e-3 - t_sc_in = 0.000768 - t_ins = 0.0083 - t_sc_out = 0.000472 - t_cut = 0.0001 - w_cut = 10e-3 - t_wbt = 0.00094 - t_alt = 0.15e-3 - t_pet = 0.05e-3 - t_jac = 0.0034 - - # nominal data - datasheet_info = NominalData( - designation_code = "NA2XS(FL)2Y", - U0 = 18.0, U = 30.0, - conductor_cross_section = 1000.0, screen_cross_section = 35.0, - resistance = 0.0291, capacitance = 0.39, inductance = 0.3, - ) - - co_w = make_stranded(datasheet_info.conductor_cross_section).best_match - co_n = co_w.layers - co_d = co_w.wire_diameter_m - co_lay = 13.0 - - sc_w = make_screened(datasheet_info.screen_cross_section, 55.3).best_match - sc_n = sc_w.wires - sc_d = sc_w.wire_diameter_m - sc_lay = 10.0 - - # canonical parts builder (ρ/μ grids attached via `unc` in each testset) - function make_parts( - ms_al_uq, - ms_cu, - ms_pe, - ms_xlpe, - ms_sem1, - ms_sem2, - ms_polyacryl; - unc = nothing, - ) - return [ - # CORE conductors: stranded (central + rings) — uses PB coupling semantics. :contentReference[oaicite:1]{index=1} - Conductor.Stranded( - :core; - layers = co_n, - d = (co_d, unc), - n = 6, - lay = (co_lay, unc), - m = ms_al_uq, - ), - - # CORE insulators - Insulator.Semicon(:core; layers = 1, t = (t_sct, unc), m = ms_polyacryl), - Insulator.Semicon(:core; layers = 1, t = (t_sc_in, unc), m = ms_sem1), - Insulator.Tubular(:core; layers = 1, t = (t_ins, unc), m = ms_xlpe), - Insulator.Semicon(:core; layers = 1, t = t_sc_out, m = ms_sem2), - Insulator.Semicon(:core; layers = 1, t = t_sct, m = ms_polyacryl), - - # SHEATH - Conductor.Wires( - :sheath; - layers = 1, - d = (sc_d, unc), - n = sc_n, - lay = (sc_lay, unc), - m = ms_cu, - ), - Conductor.Strip( - :sheath; - layers = 1, - t = (t_cut, unc), - w = (w_cut, unc), - lay = (sc_lay, unc), - m = ms_cu, - ), - Insulator.Semicon(:sheath; layers = 1, t = t_wbt, m = ms_polyacryl), - - # JACKET - Conductor.Tubular(:jacket; layers = 1, t = t_alt, m = ms_al_uq), - Insulator.Tubular(:jacket; layers = 1, t = t_pet, m = ms_pe), - Insulator.Tubular(:jacket; layers = 1, t = t_jac, m = ms_pe), - ] - end - - # formation anchors - x0, y0 = 0.0, -1.0 - xa, ya, xb, yb, xc, yc = trifoil_formation(x0, y0, 0.05) - - # convenience to build SystemBuilder with 3 positions - function make_spec(cbs; dx = (0.0, nothing), dy = (0.0, nothing), - length = (1000.0, nothing), - temperature = (20.0, nothing), earth = Earth(rho = 100.0, eps_r = 10.0, mu_r = 1.0)) - positions = [ - at( - x = xa, - y = ya, - dx = dx, - dy = dy, - phases = (:core=>1, :sheath=>0, :jacket=>0), - ), - at( - x = xb, - y = yb, - dx = dx, - dy = dy, - phases = (:core=>2, :sheath=>0, :jacket=>0), - ), - at( - x = xc, - y = yc, - dx = dx, - dy = dy, - phases = (:core=>3, :sheath=>0, :jacket=>0), - ), - ] - return SystemBuilder( - "trifoil_case", - cbs, - positions; - length = length, - temperature = temperature, - earth = earth, - f = f, - ) - end - - # # helpers to collect all produced problems (channel consumer). - # function collect_all(xs) - # acc = Any[] - # for x in xs - # ; - # push!(acc, x); - # end - # return acc - # end - - # ──────────────────────────────────────────────────────────────────────── - @testset "Baseline: fully deterministic (cardinality=1, value equality)" begin - unc = nothing - # materials - ms_al_uq = Material(materials, "aluminum", rho = unc, mu_r = unc) - ms_al = Material(materials, "aluminum") - ms_cu = Material(materials, "copper") - ms_pe = Material(materials, "pe") - ms_xlpe = Material(materials, "xlpe") - ms_sem1 = Material(materials, "semicon1") - ms_sem2 = Material(materials, "semicon2") - ms_polyacryl = Material(materials, "polyacrylate") - - parts = make_parts(ms_al_uq, ms_cu, ms_pe, ms_xlpe, ms_sem1, ms_sem2, ms_polyacryl; unc) - cbs = CableBuilder("NA2XS(FL)2Y_1000", parts; nominal = datasheet_info) - - # CableBuilder cardinality should be 1 (all scalars). :contentReference[oaicite:2]{index=2} - @test length(cbs) == 1 - - spec = make_spec(cbs; dx = (0.0, nothing), dy = (0.0, nothing), - length = (1000.0, nothing), temperature = (20.0, nothing), - earth = Earth(rho = 100.0, eps_r = 10.0, mu_r = 1.0)) - - # SystemBuilder cardinality is designs × length × positions(dx,dy) × temperature × earth. :contentReference[oaicite:3]{index=3} - @test length(spec) == 1 - - probs = collect(spec) - @test length(probs) == 1 - - prob = probs[1] - - # Check system contents: 3 cables, phase mapping intact. :contentReference[oaicite:4]{index=4} - sys = prob.system - @test sys.num_cables == 3 - # access positions - let cps = sys.cables - @test length(cps) == 3 - # coords exact (deterministic) - @test cps[1].horz == xa && cps[1].vert == ya - @test cps[2].horz == xb && cps[2].vert == yb - @test cps[3].horz == xc && cps[3].vert == yc - # mapping - @test cps[1].conn[1] == 1 && cps[1].conn[2] == 0 && - cps[1].conn[3] == 0 - @test cps[2].conn[1] == 2 - @test cps[3].conn[1] == 3 - end - - # frequencies are carried through, deterministic preview - @test prob.frequencies == f - end - - # ──────────────────────────────────────────────────────────────────────── - @testset "Earth grids & %: ρ×εr×μr×t axes expand correctly" begin - unc = nothing - ms_al_uq = Material(materials, "aluminum", rho = unc, mu_r = unc) - ms_cu = Material(materials, "copper") - ms_pe = Material(materials, "pe") - ms_xlpe = Material(materials, "xlpe") - ms_sem1 = Material(materials, "semicon1") - ms_sem2 = Material(materials, "semicon2") - ms_polyacryl = Material(materials, "polyacrylate") - - parts = make_parts(ms_al_uq, ms_cu, ms_pe, ms_xlpe, ms_sem1, ms_sem2, ms_polyacryl; unc) - cbs = CableBuilder("NA2XS(FL)2Y_1000", parts; nominal = datasheet_info) - @test length(cbs) == 1 - - # ρ: (100, 500, 2) with 10% → values [100, 500] each ±10% (as Measurement) → 2 - # εr: [5, 10] → 2 - # μr: 1.0 → 1 - # t: Inf → 1 - earth = Earth( - rho = ((100.0, 500.0, 2), (10.0)), - eps_r = [5.0, 10.0], - mu_r = 1.0, - t = Inf, - ) - - spec = make_spec(cbs; earth = earth) - # total = 1 (designs) × 1 (len) × (1×1)^3 (positions) × 1 (T) × (2×2×1×1) = 4 - @test length(spec) == 4 - - probs = collect(spec) - @test length(probs) == 4 - - # Verify EarthModel inputs carry Measurement when % is present. - for pr in probs - em = pr.earth_props - # Check first layer nominal scalars hold Measurement type for rho (base value) when % applied - # (Earth layer API stores base_* as T; implementation ensures promotion via resolve_T). - lay1 = em.layers[1] - @test lay1.base_rho_g isa Measurement - @test lay1.base_epsr_g isa Measurement - @test lay1.base_mur_g isa Measurement - end - end - - # ──────────────────────────────────────────────────────────────────────── - @testset "System knobs: length & temperature grids + % expansion" begin - unc = (0.0, 10.0, 2) # use 0% and 10% (length 2) - ms_al_uq = Material(materials, "aluminum", rho = nothing, mu_r = nothing) - ms_cu = Material(materials, "copper") - ms_pe = Material(materials, "pe") - ms_xlpe = Material(materials, "xlpe") - ms_sem1 = Material(materials, "semicon1") - ms_sem2 = Material(materials, "semicon2") - ms_polyacryl = Material(materials, "polyacrylate") - - parts = make_parts(ms_al_uq, ms_cu, ms_pe, ms_xlpe, ms_sem1, ms_sem2, ms_polyacryl; unc = nothing) - cbs = CableBuilder("NA2XS(FL)2Y_1000", parts; nominal = datasheet_info) - @test length(cbs) == 1 - - # length: (1000, (0,10,2)) → [1000, 1000±10%] (2 choices) - # temp: (20, (0,10,2)) → [20, 20±10%] (2 choices) - spec = make_spec(cbs; - length = (1000.0, unc), - temperature = (20.0, unc), - earth = Earth(rho = 100.0, eps_r = 10.0, mu_r = 1.0), - ) - - # total = 1 × 2 × 1 × 2 × 1 = 4 - @test length(spec) == 4 - - probs = collect(spec) - @test length(probs) == 4 - - # Check at least one problem has Measurement length & temperature (promotion path) - # Pull system line_length via internal field (LineCableSystem constructor stores it as T). :contentReference[oaicite:7]{index=7} - found_meas = false - for pr in probs - sys = pr.system - if sys.line_length isa Measurement && pr.temperature isa Measurement - found_meas = true - break - end - end - @test found_meas - end - - # ──────────────────────────────────────────────────────────────────────── - @testset "Position axes: displacement grids & anchor % semantics" begin - ms_al_uq = Material(materials, "aluminum", rho = nothing, mu_r = nothing) - ms_cu = Material(materials, "copper") - ms_pe = Material(materials, "pe") - ms_xlpe = Material(materials, "xlpe") - ms_sem1 = Material(materials, "semicon1") - ms_sem2 = Material(materials, "semicon2") - ms_polyacryl = Material(materials, "polyacrylate") - - parts = make_parts(ms_al_uq, ms_cu, ms_pe, ms_xlpe, ms_sem1, ms_sem2, ms_polyacryl; unc = nothing) - cbs = CableBuilder("NA2XS(FL)2Y_1000", parts; nominal = datasheet_info) - @test length(cbs) == 1 - - # Case A: dx sweep, dy deterministic - specA = make_spec(cbs; - dx = (-0.01, 0.01, 3), # => [-0.01, 0.0, 0.01] about anchor - dy = (0.0, nothing), - earth = Earth(rho = 100.0, eps_r = 10.0, mu_r = 1.0), - ) - # total = 1 × 1 × (3×1)^3 × 1 × 1 = 27 - @test length(specA) == 27 - - # Validate actual x coordinates hit the expected triplet on p1 - xs = Float64[] - for pr in specA - push!(xs, pr.system.cables[1].horz - xa) - end - @test sort!(unique(round.(xs; digits = 5))) == [-0.01, 0.0, 0.01] - - # Case B: anchor % (no displacement sweep) — (nothing, pct) on dx. :contentReference[oaicite:8]{index=8} - specB = make_spec(cbs; - dx = (nothing, (0.0, 10.0, 2)), # anchors become [xa, measurement(xa, 10%)] - dy = (0.0, nothing), - earth = Earth(rho = 100.0, eps_r = 10.0, mu_r = 1.0), - ) - # total = 1 × 1 × (2×1)^3 × 1 × 1 = 8 - @test length(specB) == 8 - - # Confirm produced anchors include a Measurement with std(|xa|*10%) - has_anchor_meas = any( - begin - x = pr.system.cables[1].horz - x isa Measurement && - isapprox(uncertainty(x), abs(xa)*0.10; atol = eps()) # std = |xa|*10% - end for pr in specB - ) - @test has_anchor_meas - end - - # ──────────────────────────────────────────────────────────────────────── - @testset "Material % propagates into designs (ρ with % → Measurement in design tree)" begin - # Attach % to aluminum ρ and μ; all geometry deterministic - ms_al_uq = Material(materials, "aluminum", rho = (1.0, (0.0, 5.0, 2)), mu_r = (1.0, (0.0, 5.0, 2))) - ms_cu = Material(materials, "copper") - ms_pe = Material(materials, "pe") - ms_xlpe = Material(materials, "xlpe") - ms_sem1 = Material(materials, "semicon1") - ms_sem2 = Material(materials, "semicon2") - ms_polyacryl = Material(materials, "polyacrylate") - - parts = make_parts(ms_al_uq, ms_cu, ms_pe, ms_xlpe, ms_sem1, ms_sem2, ms_polyacryl; unc = nothing) - cbs = CableBuilder("NA2XS(FL)2Y_1000", parts; nominal = datasheet_info) - - # designs = 2×2 from ρ(%) × μ(%) on the same MaterialSpec (coupled across parts when equal tuples). :contentReference[oaicite:9]{index=9} - @test length(cbs) == cardinality(cbs) - - # System with deterministic system knobs - spec = make_spec(cbs; earth = Earth(rho = 100.0, eps_r = 10.0, mu_r = 1.0)) - probs = collect(spec) - - # Pick one problem; inspect the first cable's design tree for Measurement presence. - pr = probs[1] - des = pr.system.cables[1].design_data # the concrete CableDesign - # Assert that somewhere in the conductor effective material we see Measurement (ρ or μ). - # We traverse last component conductor props or any material-like fields that match ρ/μ semantics. - found_meas = false - for comp in des.components - # effective conductor/insulator props are Materials.Material - if hasproperty(comp, :conductor_props) - mp = getproperty(comp, :conductor_props) - if (getproperty(mp, :rho) isa Measurement) || - (getproperty(mp, :mu_r) isa Measurement) || - (getproperty(mp, :T0) isa Measurement) || - (getproperty(mp, :alpha) isa Measurement) || - (getproperty(mp, :eps_r) isa Measurement) - found_meas = true - break - end - end - end - @test found_meas - end -end diff --git a/test/unit_ParametricBuilder/test_uq_joint_measurements.jl b/test/unit_ParametricBuilder/test_uq_joint_measurements.jl deleted file mode 100644 index a94addeaa..000000000 --- a/test/unit_ParametricBuilder/test_uq_joint_measurements.jl +++ /dev/null @@ -1,72 +0,0 @@ -@testitem "UQ.mc: joint measurement surrogate retains empirical covariance" setup = [defaults] begin - using LinearAlgebra - using Statistics - using Measurements - using LineCableModels.Commons: PhaseDomain - import LineCableModels.UQ - - ntrials = 5 - tensor_size = (2, 2, 2) - q = Float64[-2, -1, 0, 1, 2] - Rsamp = Array{Float64}(undef, tensor_size..., ntrials) - Lsamp = similar(Rsamp) - Gsamp = similar(Rsamp) - Csamp = similar(Rsamp) - - for (coordinate, index) in enumerate(CartesianIndices(tensor_size)), trial in 1:ntrials - u = q[trial] - Rsamp[index, trial] = coordinate * (10 + u) - Lsamp[index, trial] = coordinate * (20 + 2u) - Gsamp[index, trial] = coordinate * (30 - 3u) - Csamp[index, trial] = coordinate * (40 + 4u) - end - - f = [50.0, 150.0] - lp = UQ._joint_line_parameters(PhaseDomain, Rsamp, Lsamp, Gsamp, Csamp, f) - ω = reshape(2π .* f, 1, 1, :) - Rmeas = real.(lp.Z.values) - Lmeas = imag.(lp.Z.values) ./ ω - Gmeas = real.(lp.Y.values) - Cmeas = imag.(lp.Y.values) ./ ω - - X = vcat( - reshape(Rsamp, :, ntrials), - reshape(Lsamp, :, ntrials), - reshape(Gsamp, :, ntrials), - reshape(Csamp, :, ntrials), - ) - joint = vcat(vec(Rmeas), vec(Lmeas), vec(Gmeas), vec(Cmeas)) - μ = vec(mean(X; dims = 2)) - centered = X .- μ - empirical_covariance = centered * transpose(centered) / (ntrials - 1) - - @test value.(joint) ≈ μ rtol = 4eps() - @test uncertainty.(joint) ≈ vec(std(X; dims = 2)) rtol = 16eps() - @test Measurements.cov(joint) ≈ empirical_covariance rtol = 64eps() - - # Negative covariance and every cross-boundary covariance use the same - # primitive set: Z/Y, entries, complex components, and frequencies. - @test Measurements.cov(Rmeas[1, 1, 1], Gmeas[1, 1, 1]) < 0 - @test !iszero(Measurements.cov(Rmeas[1, 1, 1], Rmeas[2, 1, 1])) - @test !iszero(Measurements.cov(real(lp.Z[1, 1, 1]), imag(lp.Z[1, 1, 1]))) - @test !iszero(Measurements.cov(real(lp.Z[1, 1, 1]), real(lp.Z[1, 1, 2]))) - - # R[2,1,1] is exactly twice R[1,1,1] in every trial. - linear_residual = Rmeas[2, 1, 1] - 2Rmeas[1, 1, 1] - @test iszero(value(linear_residual)) - @test iszero(uncertainty(linear_residual)) - - # A single trial is represented explicitly with zero uncertainty. - one_trial = UQ._joint_line_parameters( - PhaseDomain, - fill(1.0, 1, 1, 1, 1), - fill(2.0, 1, 1, 1, 1), - fill(3.0, 1, 1, 1, 1), - fill(4.0, 1, 1, 1, 1), - [50.0], - ) - @test iszero(uncertainty(real(one_trial.Z[1, 1, 1]))) - @test iszero(uncertainty(imag(one_trial.Z[1, 1, 1]))) - @test iszero(uncertainty(real(one_trial.Y[1, 1, 1]))) - @test iszero(uncertainty(imag(one_trial.Y[1, 1, 1]))) -end diff --git a/test/unit_ParametricBuilder/test_uq_trial_sampler.jl b/test/unit_ParametricBuilder/test_uq_trial_sampler.jl deleted file mode 100644 index e317afab2..000000000 --- a/test/unit_ParametricBuilder/test_uq_trial_sampler.jl +++ /dev/null @@ -1,52 +0,0 @@ -@testitem "UQ.mc: custom trial sampler preserves shared primitive draws" setup = [defaults] begin - using Random - using LineCableModels.ParametricBuilder: - CableBuilder, Conductor, Insulator, Material, build - using LineCableModels.UQ - - conductor = Material(rho = 1.7241e-8, eps_r = 1.0, mu_r = 1.0, - T0 = 20.0, alpha = 0.00393) - insulation = Material(rho = 1.0e14, eps_r = 2.3, mu_r = 1.0, - T0 = 20.0, alpha = 0.0) - base = CableBuilder( - "correlated-trial-sampler", - Conductor.Solid(:core; d = (0.02, 10.0), m = conductor), - Insulator.Tubular(:core; layers = 1, t = (0.005, 10.0), m = insulation), - Conductor.Tubular(:sheath; layers = 1, t = 0.001, m = conductor), - Insulator.Tubular(:sheath; layers = 1, t = 0.001, m = insulation), - nominal = NominalData(designation_code = "correlated-trial-sampler"), - ) - - shared_draws = Tuple{Float64, Float64}[] - function correlated_sampler(_, trial, distribution) - distribution == :normal || error("unexpected distribution") - u = randn() - d = 0.02 * (1 + 0.01u) - t = 0.005 * (1 + 0.01u) - push!(shared_draws, (d, t)) - trial_builder = CableBuilder( - "correlated-trial-$trial", - Conductor.Solid(:core; d = d, m = conductor), - Insulator.Tubular(:core; layers = 1, t = t, m = insulation), - Conductor.Tubular(:sheath; layers = 1, t = 0.001, m = conductor), - Insulator.Tubular(:sheath; layers = 1, t = 0.001, m = insulation), - nominal = NominalData(designation_code = "correlated-trial-$trial"), - ) - return only(build(trial_builder)) - end - - result = UQ.mc( - base; - trials = 8, - seed = 2048, - trial_sampler = correlated_sampler, - return_samples = true, - print_step = 100, - ) - - @test length(shared_draws) == 8 - @test result.samples !== nothing - @test result.stats.R.n == 8 - @test all(isapprox(d / 0.02, t / 0.005; atol = 32eps()) for (d, t) in shared_draws) - @test length(unique(first.(shared_draws))) > 1 -end diff --git a/test/unit_Validation/test_rules_tubular.jl b/test/unit_Validation/test_rules_tubular.jl deleted file mode 100644 index e7f1b205b..000000000 --- a/test/unit_Validation/test_rules_tubular.jl +++ /dev/null @@ -1,26 +0,0 @@ -@testitem "Validation(Tubular): rule order unit test" setup = [defaults] begin - # Use fully-qualified names; do not add extra `using` here. - V = LineCableModels.Validation - T = LineCableModels.DataModel.Tubular - M = LineCableModels.Materials.Material - - r = V._rules(T) - - expected = ( - V.Normalized(:r_in), V.Normalized(:r_ex), - V.Finite(:r_in), V.Nonneg(:r_in), - V.Finite(:r_ex), V.Nonneg(:r_ex), - V.Less(:r_in, :r_ex), - V.Finite(:temperature), - V.IsA{M}(:material_props), - ) - - if r != expected - @error "[Validation] Rule set for Tubular is wrong. Someone ‘helpfully’ changed the bundle order or duplicated rules.\n" * - "Expected exact structural equality with the generated bundle. Fix your traits/extra_rules and stop being clever." - @show expected - @show r - end - - @test r == expected -end diff --git a/test/visual/Project.toml b/test/visual/Project.toml new file mode 100644 index 000000000..8cb2ee3b5 --- /dev/null +++ b/test/visual/Project.toml @@ -0,0 +1,23 @@ +[deps] +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +Calculus = "49dc2e85-a5d0-5ad3-a950-438e2897f1b9" +DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" +Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" +LineCableModels = "dffb7669-8cd4-4628-99dd-7694e356ba37" +Measurements = "eff96d63-e80a-5855-80a2-b1b0885c5ab7" +Reexport = "189a3867-3050-52da-a836-e630ba90ab69" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" + +[sources] +LineCableModels = {path = "../.."} + +[compat] +CairoMakie = "=0.15.13" +Calculus = "0.5" +Distributions = "0.25" +DataFrames = "1" +Measurements = "2" +Reexport = "1" +TestItemRunner = "1.3.1" +julia = "1.12"