From abe355170add20cc0c841e218fed82fe8ffbab51 Mon Sep 17 00:00:00 2001 From: amaurigmartins Date: Thu, 13 Aug 2026 22:12:24 +0200 Subject: [PATCH 001/157] chore(release): prepare version 0.2.0 --- .JuliaFormatter.toml | 9 + .gitattributes | 1 + .github/ISSUE_TEMPLATE/release_approval.yml | 146 -- .github/OSS_PUBLISH_CHECKLIST.md | 49 + .github/workflows/CI.yml | 198 +- .github/workflows/TagBot.yml | 16 +- .github/workflows/create-release.yml | 39 - .github/workflows/fem-release.yml | 41 + .github/workflows/publish-oss-repository.yml | 134 + .github/workflows/todo-to-issue.yml | 50 - .gitignore | 1 + .gitlint | 9 + CHANGELOG.md | 62 +- CITATION.cff | 13 + CONTRIBUTING.md | 19 + Project.toml | 27 +- README.md | 89 +- RELEASE_AUDIT.md | 95 + THIRD_PARTY_NOTICE.md | 26 + binder/Project.toml | 30 - binder/postBuild | 78 - binder/requirements.txt | 3 - codecov.yml | 6 +- docs/deploy.jl | 8 + docs/doctest.jl | 13 + docs/instantiate.jl | 6 + docs/make.jl | 271 +- docs/src/CHANGELOG.md | 63 +- docs/src/cable-builder.md | 512 ---- docs/src/contributing.md | 19 + docs/src/conventions.md | 256 +- docs/src/docstrings.md | 226 +- docs/src/index.md | 28 +- docs/src/reference.md | 119 +- docs/src/tutorials.md | 2 +- examples/tutorial1.jl | 13 +- examples/tutorial2.jl | 82 +- examples/tutorial2_sector.jl | 8 +- examples/tutorial3.jl | 136 +- ext/LineCableModelsCairoMakieExt.jl | 14 + ext/LineCableModelsGLMakieExt.jl | 16 + ext/LineCableModelsGmshExt.jl | 84 + ext/LineCableModelsMakieExt.jl | 79 + ext/LineCableModelsWGLMakieExt.jl | 14 + ext/fem/getdp_frontend/GetDPFrontend.jl | 68 + ext/fem/getdp_frontend/LICENSE | 30 + ext/fem/getdp_frontend/NOTICE.md | 11 + ext/fem/getdp_frontend/constraint.jl | 194 ++ ext/fem/getdp_frontend/formulation.jl | 270 ++ ext/fem/getdp_frontend/function.jl | 1460 +++++++++++ ext/fem/getdp_frontend/functionspace.jl | 245 ++ ext/fem/getdp_frontend/getdp_object.jl | 244 ++ ext/fem/getdp_frontend/group.jl | 311 +++ ext/fem/getdp_frontend/helpers.jl | 156 ++ ext/fem/getdp_frontend/integration.jl | 135 + ext/fem/getdp_frontend/jacobian.jl | 134 + ext/fem/getdp_frontend/postoperation.jl | 182 ++ ext/fem/getdp_frontend/postprocessing.jl | 203 ++ ext/fem/getdp_frontend/problem_definition.jl | 219 ++ ext/fem/getdp_frontend/resolution.jl | 659 +++++ integration/fem/README.md | 16 + integration/fem/runtests.jl | 190 ++ showcase/showcase1.jl | 1697 ++++++------- showcase/start.jl | 18 +- src/LineCableModels.jl | 19 +- src/cablebuilder/CableBuilder.jl | 103 - src/cablebuilder/cabledesign.jl | 65 - src/cablebuilder/enclosure.jl | 69 - src/cablebuilder/grid.jl | 134 - src/cablebuilder/gridspace.jl | 67 - src/cablebuilder/helical.jl | 40 - src/cablebuilder/macros.jl | 226 -- src/cablebuilder/materials.jl | 20 - src/cablebuilder/partbuilder.jl | 110 - src/cablebuilder/primitives.jl | 38 - src/cablebuilder/runme.jl | 157 -- src/cablebuilder/shapes.jl | 38 - src/cablebuilder/solidcore.jl | 58 - src/cablebuilder/stranded.jl | 93 - src/cablebuilder/tubular.jl | 56 - src/cablebuilder/types.jl | 65 - src/commons/Commons.jl | 3 +- src/commons/consts.jl | 1 - src/commons/docstringextension.jl | 27 +- src/datamodel/DataModel.jl | 43 +- src/datamodel/baseparams/BaseParams.jl | 1089 ++++---- src/datamodel/cablecomponent.jl | 296 ++- src/datamodel/cablecomponent/base.jl | 159 +- src/datamodel/cabledesign.jl | 388 ++- src/datamodel/cabledesign/base.jl | 188 +- src/datamodel/cabledesign/dataframe.jl | 489 ++-- src/datamodel/cableslibrary.jl | 61 +- src/datamodel/cableslibrary/base.jl | 18 +- src/datamodel/cableslibrary/dataframe.jl | 20 +- src/datamodel/cableslibrary/vdeparse.jl | 383 ++- src/datamodel/circstrands.jl | 203 +- src/datamodel/conductorgroup.jl | 295 ++- src/datamodel/conductorgroup/base.jl | 2 +- src/datamodel/helpers.jl | 56 +- src/datamodel/insulator.jl | 103 +- src/datamodel/insulatorgroup.jl | 232 +- src/datamodel/insulatorgroup/base.jl | 2 +- src/datamodel/io.jl | 248 +- src/datamodel/linecablesystem.jl | 512 ++-- src/datamodel/linecablesystem/base.jl | 8 +- src/datamodel/linecablesystem/dataframe.jl | 19 +- src/datamodel/macros.jl | 102 +- src/datamodel/nominaldata.jl | 89 +- src/datamodel/nominaldata/base.jl | 2 +- src/datamodel/preview.jl | 2237 ++++++++--------- src/datamodel/radii.jl | 115 +- src/datamodel/rectstrands.jl | 217 +- src/datamodel/sector.jl | 151 +- src/datamodel/sectorinsulator.jl | 24 +- src/datamodel/semicon.jl | 100 +- src/datamodel/strands_handler.jl | 12 +- src/datamodel/strip.jl | 172 +- src/datamodel/tubular.jl | 90 +- src/datamodel/typecoercion.jl | 145 +- src/datamodel/types.jl | 39 +- src/datamodel/validation.jl | 15 +- src/earthprops/EarthProps.jl | 457 ++-- src/earthprops/base.jl | 7 +- src/earthprops/dataframe.jl | 28 +- src/earthprops/fdprops.jl | 8 +- src/earthprops/typecoercion.jl | 9 - src/engine/Engine.jl | 30 +- src/engine/base.jl | 539 ++-- src/engine/dataframe.jl | 364 +-- src/engine/earthadmittance/EarthAdmittance.jl | 5 +- src/engine/earthadmittance/base.jl | 12 +- src/engine/earthadmittance/homogeneous.jl | 334 ++- src/engine/earthimpedance/EarthImpedance.jl | 5 +- src/engine/earthimpedance/base.jl | 12 +- src/engine/earthimpedance/homogeneous.jl | 291 ++- src/engine/ehem/EHEM.jl | 7 +- src/engine/ehem/enforcelayer.jl | 101 +- src/engine/fem/FEM.jl | 138 +- src/engine/fem/cable.jl | 981 ++++---- src/engine/fem/drawing.jl | 983 ++++---- src/engine/fem/encoding.jl | 611 +++-- src/engine/fem/helpers.jl | 104 +- src/engine/fem/identification.jl | 6 +- src/engine/fem/lineparamopts.jl | 43 +- src/engine/fem/materialprops.jl | 126 +- src/engine/fem/mesh.jl | 418 ++- src/engine/fem/meshtransitions.jl | 165 +- src/engine/fem/problemdefs.jl | 283 ++- src/engine/fem/solver.jl | 1730 +++++++------ src/engine/fem/space.jl | 677 +++-- src/engine/fem/types.jl | 73 +- src/engine/fem/visualization.jl | 3 +- src/engine/fem/workspace.jl | 533 ++-- src/engine/helpers.jl | 249 +- .../InsulationAdmittance.jl | 5 +- src/engine/insulationadmittance/lossless.jl | 50 +- src/engine/insulationadmittance/parallelrc.jl | 63 +- .../InsulationImpedance.jl | 6 +- src/engine/insulationimpedance/lossless.jl | 27 +- .../internalimpedance/InternalImpedance.jl | 6 +- src/engine/internalimpedance/scaledbessel.jl | 250 +- src/engine/lineparamopts.jl | 99 +- src/engine/lineparams.jl | 131 +- src/engine/plot.jl | 1828 ++++++-------- src/engine/plotmetadata.jl | 385 +++ src/engine/problemdefs.jl | 435 ++-- src/engine/reduction.jl | 235 +- src/engine/solver.jl | 700 +++--- src/engine/transforms/Transforms.jl | 21 +- src/engine/transforms/eiglevenberg.jl | 594 +++-- src/engine/transforms/fortescue.jl | 62 +- src/engine/types.jl | 7 +- src/engine/workspace.jl | 425 ++-- src/importexport/ImportExport.jl | 15 +- src/importexport/atp.jl | 620 +++-- src/importexport/cableslibrary.jl | 1046 ++++---- src/importexport/deserialize.jl | 456 ++-- src/importexport/materialslibrary.jl | 246 +- src/importexport/pscad.jl | 1658 ++++++------ src/importexport/serialize.jl | 283 ++- src/importexport/tralin.jl | 784 +++--- src/importexport/xlsx.jl | 266 +- src/materials/Materials.jl | 62 +- src/materials/base.jl | 143 +- src/materials/dataframe.jl | 25 +- src/materials/materialslibrary.jl | 105 +- src/materials/typecoercion.jl | 6 +- src/parametricbuilder/ParametricBuilder.jl | 72 +- src/parametricbuilder/base.jl | 485 ++-- src/parametricbuilder/cablebuilderspec.jl | 813 +++--- src/parametricbuilder/determinize.jl | 204 +- src/parametricbuilder/groupspec.jl | 269 +- src/parametricbuilder/materialspec.jl | 95 +- src/parametricbuilder/positionspec.jl | 86 +- src/parametricbuilder/systembuilderspec.jl | 229 +- .../wirepatterns/WirePatterns.jl | 437 ++-- src/plotbuilder/PlotBuilder.jl | 33 +- src/plotbuilder/axisspec.jl | 87 +- .../backendhandler/BackendHandler.jl | 288 +-- src/plotbuilder/pagespec.jl | 667 +++-- src/plotbuilder/parse.jl | 905 ++++--- src/plotbuilder/plothelpers.jl | 55 +- src/plotbuilder/plotspecs.jl | 279 +- .../plotuicomponents/PlotUIComponents.jl | 1221 ++++----- src/plotbuilder/plotuicomponents/callbacks.jl | 1 + src/plotbuilder/seriesspec.jl | 463 ++-- src/plotbuilder/traits.jl | 248 +- src/plotbuilder/types.jl | 54 +- src/plotbuilder/uicomponents/UIComponents.jl | 9 +- src/plotbuilder/uicomponents/actions.jl | 78 +- src/plotbuilder/uicomponents/draw.jl | 70 +- src/plotbuilder/uicomponents/layoutspecs.jl | 116 +- src/plotbuilder/uicomponents/pipeline.jl | 525 ++-- src/plotbuilder/uicomponents/themes.jl | 113 +- src/plotbuilder/uicomponents/types.jl | 106 +- src/plotbuilder/uicomponents/widgets.jl | 36 +- src/plotbuilder/viewspec.jl | 26 +- src/uncertainbessels/UncertainBessels.jl | 228 +- src/unithandler/UnitHandler.jl | 335 ++- src/uq/UQ.jl | 467 ++-- src/uq/dataframe.jl | 76 +- src/uq/distributions.jl | 681 +++-- src/uq/montecarlo.jl | 723 +++--- src/uq/plot.jl | 1269 +++++----- src/uq/plotspecs/mcstatsplotspec.jl | 70 +- src/uq/types.jl | 196 +- src/utils/Utils.jl | 334 ++- src/utils/logging.jl | 84 +- src/utils/macros.jl | 38 +- src/utils/typecoercion.jl | 44 +- src/validation/Validation.jl | 224 +- src/validation/applyrules.jl | 205 +- src/validation/rules.jl | 80 +- test/aqua.jl | 7 +- test/baseparams.jl | 734 +++--- test/datamodel.jl | 1529 ++++++----- test/earthprops.jl | 93 +- test/runtests.jl | 97 +- test/test_tutorial1.jl | 53 +- test/test_tutorial2.jl | 603 +++-- test/test_tutorial3.jl | 179 -- test/test_tutorial_2_sector.jl | 40 +- .../test_calc_equivalent_alpha.jl | 35 +- .../test_calc_equivalent_eps.jl | 140 +- .../test_calc_equivalent_gmr.jl | 152 +- .../test_calc_equivalent_lossfact.jl | 9 +- .../test_calc_equivalent_mu.jl | 182 +- .../test_calc_equivalent_rho.jl | 10 +- test/unit_BaseParams/test_calc_gmd.jl | 197 +- .../test_calc_helical_params.jl | 170 +- .../test_calc_inductance_trifoil.jl | 41 +- .../test_calc_parallel_equivalent.jl | 56 +- .../test_calc_shunt_capacitance.jl | 142 +- .../test_calc_shunt_conductance.jl | 142 +- .../test_calc_sigma_lossfact.jl | 146 +- .../test_calc_solenoid_correction.jl | 6 +- .../test_calc_strip_resistance.jl | 17 +- .../test_calc_temperature_correction.jl | 17 +- test/unit_BaseParams/test_calc_tubular_gmr.jl | 186 +- .../test_calc_tubular_inductance.jl | 173 +- .../test_calc_tubular_resistance.jl | 233 +- .../test_calc_wirearray_coords.jl | 323 ++- .../test_calc_wirearray_gmr.jl | 8 +- test/unit_DataModel/test_CableComponent.jl | 301 ++- test/unit_DataModel/test_CableDesign.jl | 339 ++- test/unit_DataModel/test_CablePosition.jl | 160 +- test/unit_DataModel/test_ConductorGroup.jl | 421 ++-- test/unit_DataModel/test_Insulator.jl | 274 +- test/unit_DataModel/test_InsulatorGroup.jl | 230 +- test/unit_DataModel/test_LineCableSystem.jl | 474 ++-- test/unit_DataModel/test_Semicon.jl | 188 +- test/unit_DataModel/test_Strip.jl | 336 ++- test/unit_DataModel/test_Tubular.jl | 201 +- test/unit_DataModel/test_WireArray.jl | 730 +++--- test/unit_DataModel/test_equivalent.jl | 372 ++- .../test_uncertainty_covariance.jl | 123 +- .../test_parallel_rc_insulation.jl | 559 ++-- .../unit_ImportExport/test_export_data_atp.jl | 340 ++- .../test_parametricbuilder.jl | 781 +++--- .../test_uq_joint_measurements.jl | 125 +- .../test_uq_trial_sampler.jl | 93 +- test/unit_Validation/test_rules_tubular.jl | 40 +- 282 files changed, 33361 insertions(+), 31211 deletions(-) create mode 100644 .JuliaFormatter.toml create mode 100644 .gitattributes delete mode 100644 .github/ISSUE_TEMPLATE/release_approval.yml create mode 100644 .github/OSS_PUBLISH_CHECKLIST.md delete mode 100644 .github/workflows/create-release.yml create mode 100644 .github/workflows/fem-release.yml create mode 100644 .github/workflows/publish-oss-repository.yml delete mode 100644 .github/workflows/todo-to-issue.yml create mode 100644 .gitlint create mode 100644 CITATION.cff create mode 100644 CONTRIBUTING.md create mode 100644 RELEASE_AUDIT.md create mode 100644 THIRD_PARTY_NOTICE.md delete mode 100644 binder/Project.toml delete mode 100644 binder/postBuild delete mode 100644 binder/requirements.txt create mode 100644 docs/deploy.jl create mode 100644 docs/doctest.jl create mode 100644 docs/instantiate.jl delete mode 100644 docs/src/cable-builder.md create mode 100644 docs/src/contributing.md create mode 100644 ext/LineCableModelsCairoMakieExt.jl create mode 100644 ext/LineCableModelsGLMakieExt.jl create mode 100644 ext/LineCableModelsGmshExt.jl create mode 100644 ext/LineCableModelsMakieExt.jl create mode 100644 ext/LineCableModelsWGLMakieExt.jl create mode 100644 ext/fem/getdp_frontend/GetDPFrontend.jl create mode 100644 ext/fem/getdp_frontend/LICENSE create mode 100644 ext/fem/getdp_frontend/NOTICE.md create mode 100644 ext/fem/getdp_frontend/constraint.jl create mode 100644 ext/fem/getdp_frontend/formulation.jl create mode 100644 ext/fem/getdp_frontend/function.jl create mode 100644 ext/fem/getdp_frontend/functionspace.jl create mode 100644 ext/fem/getdp_frontend/getdp_object.jl create mode 100644 ext/fem/getdp_frontend/group.jl create mode 100644 ext/fem/getdp_frontend/helpers.jl create mode 100644 ext/fem/getdp_frontend/integration.jl create mode 100644 ext/fem/getdp_frontend/jacobian.jl create mode 100644 ext/fem/getdp_frontend/postoperation.jl create mode 100644 ext/fem/getdp_frontend/postprocessing.jl create mode 100644 ext/fem/getdp_frontend/problem_definition.jl create mode 100644 ext/fem/getdp_frontend/resolution.jl create mode 100644 integration/fem/README.md create mode 100644 integration/fem/runtests.jl delete mode 100644 src/cablebuilder/CableBuilder.jl delete mode 100644 src/cablebuilder/cabledesign.jl delete mode 100644 src/cablebuilder/enclosure.jl delete mode 100644 src/cablebuilder/grid.jl delete mode 100644 src/cablebuilder/gridspace.jl delete mode 100644 src/cablebuilder/helical.jl delete mode 100644 src/cablebuilder/macros.jl delete mode 100644 src/cablebuilder/materials.jl delete mode 100644 src/cablebuilder/partbuilder.jl delete mode 100644 src/cablebuilder/primitives.jl delete mode 100644 src/cablebuilder/runme.jl delete mode 100644 src/cablebuilder/shapes.jl delete mode 100644 src/cablebuilder/solidcore.jl delete mode 100644 src/cablebuilder/stranded.jl delete mode 100644 src/cablebuilder/tubular.jl delete mode 100644 src/cablebuilder/types.jl create mode 100644 src/engine/plotmetadata.jl delete mode 100644 test/test_tutorial3.jl diff --git a/.JuliaFormatter.toml b/.JuliaFormatter.toml new file mode 100644 index 00000000..7b8b6253 --- /dev/null +++ b/.JuliaFormatter.toml @@ -0,0 +1,9 @@ +style = "sciml" +ignore = [ + "@INPROGRESS", + "bibliography", + "designs", + "ext/fem/getdp_frontend", + "papers", + "temp", +] diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..01cb7b11 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +ext/fem/getdp_frontend/** -whitespace diff --git a/.github/ISSUE_TEMPLATE/release_approval.yml b/.github/ISSUE_TEMPLATE/release_approval.yml deleted file mode 100644 index cdd78726..00000000 --- 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 00000000..3af9d310 --- /dev/null +++ b/.github/OSS_PUBLISH_CHECKLIST.md @@ -0,0 +1,49 @@ +### Repository name + +{{ repository_name }} + +### Version tag + +{{ version_tag }} + +### IP and Legal Requirements + +- [ ] Written consent secured from faculty advisor or PI +- [ ] Software disclosure submitted to tech transfer/IP office +- [ ] Funding/sponsorship contracts reviewed for restrictions or obligations +- [ ] Written consent secured from all co-developers and collaborators +- [ ] Verified no active or pending patents are compromised by open release +- [ ] Verified absence of sensitive data (personal, confidential, regulated tech) +- [ ] Third-party licenses audited and documented to confirm compatibility with the repository +- [ ] All findings and products of the codebase being open-sourced have been published/accepted for publication with scientific journals + +### Code Quality Requirements + +- [ ] Codebase cleaned: no credentials, passwords, API keys, sensitive URLs, or debug code +- [ ] Out-of-the-box functionality confirmed in a clean environment +- [ ] Reproducibility ensured: scripts or datasets included to reproduce main results +- [ ] Dependencies clearly documented (requirements.txt, compiler info, etc.) +- [ ] Basic tests or validation scripts included demonstrating key functionality +- [ ] Peer or self-review performed for code readability and usability + +### Documentation Requirements + +- [ ] Comprehensive README.md created with clear purpose, installation and usage instructions +- [ ] CHANGELOG.md created following proper standards (Keep a Changelog) +- [ ] Citation instructions provided (CITATION.cff, DOI links, publication references) +- [ ] Appropriate LICENSE.md file attached at repository root +- [ ] Funding agencies, institutional support, and contributors properly acknowledged +- [ ] Contributing guidelines included for external collaborators (if applicable) + +### Repository Finalization Requirements + +- [ ] Repository structure clearly organized (src/, examples/, docs/, etc.) +- [ ] Final pre-release sanity check performed (installation from scratch, test examples) +- [ ] Release clearly tagged and versioned (e.g., v1.0.0) with release notes +- [ ] Repository ready to be set to public visibility +- [ ] Release announcement prepared for internal and external channels +- [ ] Repository archived with DOI (optional, via Zenodo), or confirmed not applicable + +### Institutional email + +{{ institutional_email }} diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 1b17600f..f0634465 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,91 +1,187 @@ name: CI + on: push: - branches: - - main - tags: ['*'] + branches: [main] + tags: ["*"] pull_request: workflow_dispatch: + concurrency: - # Skip intermediate builds: always. - # Cancel intermediate builds: only if it is a pull request build. group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} + +permissions: + contents: read + jobs: - test: - if: ${{ !contains(github.event.head_commit.message, 'skip ci') }} - name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - runs-on: ${{ matrix.os }} + core: + name: Core / Julia ${{ matrix.julia }} + runs-on: ubuntu-latest timeout-minutes: 60 - permissions: # needed to allow julia-actions/cache to proactively delete old caches that it has created - actions: write - contents: read strategy: fail-fast: false matrix: - version: - - '1.12' - - 'pre' - os: - - ubuntu-latest - arch: - - x64 + julia: ["1.12", "pre"] steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v6 - uses: julia-actions/setup-julia@v3 with: - version: ${{ matrix.version }} - arch: ${{ matrix.arch }} + version: ${{ matrix.julia }} - 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' + cache-compiled: ${{ matrix.julia != 'pre' }} + 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' + if: matrix.julia == '1.12' - uses: codecov/codecov-action@v7 - if: matrix.version == '1.12' + if: matrix.julia == '1.12' with: files: lcov.info token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: false + fail_ci_if_error: true + + plotting: + name: CairoMakie extension + runs-on: ubuntu-latest + timeout-minutes: 60 + env: + LINECABLEMODELS_TEST_PLOTTING: "true" + steps: + - uses: actions/checkout@v6 + - uses: julia-actions/setup-julia@v3 + with: + version: "1.12" + - uses: julia-actions/cache@v3 + - name: Run plotting extension tests + shell: julia --startup-file=no --color=yes {0} + run: | + using Pkg + Pkg.activate(; temp = true) + Pkg.develop(PackageSpec(path = pwd())) + Pkg.add(PackageSpec(name = "Aqua", version = "0.8")) + Pkg.add(PackageSpec(name = "CairoMakie", version = "0.15")) + Pkg.add(PackageSpec(name = "TestItemRunner", version = "1")) + pushfirst!(LOAD_PATH, pwd()) + include(joinpath(pwd(), "test", "runtests.jl")) + + aqua: + name: Aqua + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v6 + - uses: julia-actions/setup-julia@v3 + with: + version: "1.12" + - uses: julia-actions/cache@v3 + - name: Run Aqua independently + shell: julia --color=yes {0} + run: | + using Pkg + Pkg.activate(; temp = true) + Pkg.develop(PackageSpec(path = pwd())) + Pkg.add("Aqua") + using Aqua, LineCableModels + Aqua.test_all(LineCableModels) + + formatting: + name: SciML formatting + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + - uses: julia-actions/setup-julia@v3 + with: + version: "1.12" + - name: Check JuliaFormatter + shell: julia --color=yes {0} + run: | + using Pkg + Pkg.activate(; temp = true) + Pkg.add(PackageSpec(name = "JuliaFormatter", version = "2")) + using JuliaFormatter + @assert format(pwd(); overwrite = false, verbose = true) + + gitlint: + name: Commit messages + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Install gitlint + run: python -m pip install --disable-pip-version-check gitlint==0.19.1 + - name: Check new commits + shell: bash + env: + EVENT_NAME: ${{ github.event_name }} + BASE_REF: ${{ github.base_ref }} + BEFORE_SHA: ${{ github.event.before }} + CURRENT_SHA: ${{ github.sha }} + run: | + set -euo pipefail + if [[ "${EVENT_NAME}" == "pull_request" ]]; then + range="origin/${BASE_REF}..HEAD" + elif [[ -n "${BEFORE_SHA}" && ! "${BEFORE_SHA}" =~ ^0+$ ]]; then + range="${BEFORE_SHA}..${CURRENT_SHA}" + else + range="${CURRENT_SHA}^..${CURRENT_SHA}" + fi + gitlint --commits "${range}" + + clean-install: + name: Clean installation + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v6 + - uses: julia-actions/setup-julia@v3 + 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 + Pkg.activate(; temp = true) + Pkg.develop(PackageSpec(path = pwd())) + using LineCableModels + @assert Base.get_extension(LineCableModels, :LineCableModelsMakieExt) === nothing + @assert Base.get_extension(LineCableModels, :LineCableModelsGmshExt) === nothing + ' + 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: actions/checkout@v6 - uses: julia-actions/setup-julia@v3 with: - version: '1.12' + version: "1.12" - uses: julia-actions/cache@v3 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 + cache-compiled: "false" + save-always: "false" + - name: Instantiate documentation + run: julia --project=docs --color=yes docs/instantiate.jl - name: Run doctests - shell: julia --project=docs --color=yes {0} - run: | - using Documenter: DocMeta, doctest - using LineCableModels - DocMeta.setdocmeta!(LineCableModels, :DocTestSetup, :(using LineCableModels); recursive=true) - doctest(LineCableModels) - - uses: julia-actions/julia-docdeploy@v1 + run: julia --project=docs --color=yes docs/doctest.jl + - name: Build documentation + run: julia --project=docs --color=yes docs/make.jl + - name: Deploy documentation + if: 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 0cd3114e..d5028103 100644 --- a/.github/workflows/TagBot.yml +++ b/.github/workflows/TagBot.yml @@ -1,25 +1,17 @@ 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' diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml deleted file mode 100644 index d1ae4599..00000000 --- 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/fem-release.yml b/.github/workflows/fem-release.yml new file mode 100644 index 00000000..5ae5648f --- /dev/null +++ b/.github/workflows/fem-release.yml @@ -0,0 +1,41 @@ +name: FEM release gate + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + fem: + name: FEM / Gmsh / GetDP 3.5.0 + runs-on: ubuntu-latest + timeout-minutes: 45 + env: + GETDP_URL: https://getdp.info/bin/Linux/getdp-3.5.0-Linux64c.tgz + GETDP_SHA256: d3c28fa18f20d6147b4c7367d4dd802e9f7ddb58c608688bbb71919dbca8041d + steps: + - uses: actions/checkout@v6 + - uses: julia-actions/setup-julia@v3 + with: + version: "1.12" + - uses: julia-actions/cache@v3 + - name: Download and verify GetDP + shell: bash + run: | + set -euo pipefail + archive="${RUNNER_TEMP}/getdp-3.5.0-Linux64c.tgz" + install_dir="${RUNNER_TEMP}/getdp-3.5.0" + curl --fail --location --output "${archive}" "${GETDP_URL}" + echo "${GETDP_SHA256} ${archive}" | sha256sum --check - + mkdir -p "${install_dir}" + tar -xzf "${archive}" --strip-components=1 -C "${install_dir}" + echo "GETDP_EXECUTABLE=${install_dir}/bin/getdp" >> "${GITHUB_ENV}" + - name: Run FEM integration tests without coverage + shell: julia --project=. --startup-file=no --code-coverage=none --color=yes {0} + run: | + using Pkg + Pkg.activate(; temp = true) + Pkg.develop(PackageSpec(path = pwd())) + Pkg.add("Gmsh") + include(joinpath(pwd(), "integration", "fem", "runtests.jl")) diff --git a/.github/workflows/publish-oss-repository.yml b/.github/workflows/publish-oss-repository.yml new file mode 100644 index 00000000..26a66a2e --- /dev/null +++ b/.github/workflows/publish-oss-repository.yml @@ -0,0 +1,134 @@ +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@v4 + + - 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/todo-to-issue.yml b/.github/workflows/todo-to-issue.yml deleted file mode 100644 index e2cbe000..00000000 --- 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 66201249..5e8579ba 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,7 @@ papers sprint */fem_output .github/*.md +!.github/OSS_PUBLISH_CHECKLIST.md .github/instructions .github/prompts test/jl_* diff --git a/.gitlint b/.gitlint new file mode 100644 index 00000000..09911069 --- /dev/null +++ b/.gitlint @@ -0,0 +1,9 @@ +[general] +contrib = contrib-title-conventional-commits +ignore = body-is-missing, body-min-length + +[title-max-length] +line-length = 72 + +[title-match-regex] +regex = ^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)\([a-z0-9][a-z0-9._/-]*\)(!)?: [a-z0-9].*$ diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e7fa752..ba8a8c88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,31 +1,67 @@ # Changelog -All notable changes to this project will be documented in this file. +All notable 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] -- / +## [0.2.0] - 2026-08-13 + +### Added + +- Julia package extensions for Gmsh and the Makie backends. +- A manual, checksum-verified FEM release gate. +- Aqua, SciML formatting, gitlint, clean-install, and modular documentation + checks. +- Citation and contribution metadata. ### Changed -- Refactored CodeComponent structure and constructors. +- Gmsh and Makie are optional dependencies. Core loading no longer imports + Gmsh, Makie, CairoMakie, GLMakie, WGLMakie, or GetDP. +- The legacy `Engine.FEM` API delegates to the Gmsh extension and emits + deprecation warnings. +- The compatible GetDP frontend is now a private, licensed source snapshot. + GetDP itself must be supplied through `GETDP_EXECUTABLE` or `PATH`. +- Plotting requires the caller to load CairoMakie, GLMakie, or WGLMakie + explicitly. +- Documentation examples and development conventions were consolidated. -### Added +### Removed + +- Binder experiments and Binder-specific notebook bootstrapping. +- The accidental standalone `src/cablebuilder` subsystem. The maintained + `ParametricBuilder.CableBuilder`, sector support, covariance work, and + their tests remain. +- The hard dependency on the external, unregistered GetDP.jl package. +- The obsolete TODO scraper and duplicate tag-release workflow. + +### Migration + +Load optional integrations explicitly: + +```julia +using LineCableModels -- Included interface to run finite element simulations using [Onelab](https://onelab.info/). +using CairoMakie +# preview(...) and plot(...) are now available -- Included basic coverage tests. +using Gmsh +using LineCableModels.Engine.FEM +formulation = FormulationSet(:FEM) +``` -- Included import/export of CableDesigns to JSON files. +Set `GETDP_EXECUTABLE=/absolute/path/to/getdp` or place `getdp` on +`PATH` before invoking the transitional FEM solver. -## [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 00000000..fc1fa22b --- /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/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..df3a1735 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,19 @@ +# 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(".")`. Commit +subjects must satisfy `.gitlint`: a scoped Conventional Commit, a lowercase +description, and no more than 72 characters. + +Keep pull requests focused. Add tests for changed behavior and update public +documentation when an API changes. Optional plotting and FEM integrations must +remain outside core loading and must be checked with their dedicated workflows. +Do not format or otherwise rewrite `ext/fem/getdp_frontend/`; it preserves the +documented upstream snapshot. diff --git a/Project.toml b/Project.toml index 2832822e..342baf3f 100644 --- a/Project.toml +++ b/Project.toml @@ -1,32 +1,27 @@ 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" [deps] -CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" Calculus = "49dc2e85-a5d0-5ad3-a950-438e2897f1b9" Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" 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" @@ -36,16 +31,21 @@ SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" -UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" XLSX = "fdbf4ff8-1666-58a4-91e7-1b58723a45e0" [weakdeps] +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" +Gmsh = "705231aa-382f-11e9-3f0c-b7cb4346fdeb" +Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" WGLMakie = "276b4fcb-3e11-5398-bf8b-a0c2d153d008" -[sources.GetDP] -rev = "main" -url = "https://github.com/Electa-Git/GetDP.jl" +[extensions] +LineCableModelsCairoMakieExt = ["Makie", "CairoMakie"] +LineCableModelsGLMakieExt = ["Makie", "GLMakie"] +LineCableModelsGmshExt = "Gmsh" +LineCableModelsMakieExt = "Makie" +LineCableModelsWGLMakieExt = ["Makie", "WGLMakie"] [compat] Aqua = "0.8" @@ -54,13 +54,12 @@ 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" +GeometryBasics = "0.5" GLMakie = "0.13" -GetDP = "0.1" Gmsh = "0.3.1" JSON3 = "1.14.2" LinearAlgebra = "1.11.0" @@ -71,7 +70,6 @@ Makie = "0.24.6" Measurements = "2.11.0" NLsolve = "4.5.1" Pkg = "1.11.0" -Plots = "1.40.9" Printf = "1.11.0" QuadGK = "2.11.2" Random = "1.11.0" @@ -83,7 +81,6 @@ StatsBase = "0.34.7" Tables = "1.12.1" Test = "1.11.0" TestItemRunner = "1.1.0" -UUIDs = "1.11.0" WGLMakie = "0.13" XLSX = "0.10.4, 0.12" julia = "1.12" diff --git a/README.md b/README.md index 52b95b86..e950728e 100644 --- a/README.md +++ b/README.md @@ -4,55 +4,92 @@ [![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, sector-shaped, screened, and armored cable models. +- Frequency-dependent series impedance and shunt admittance calculations. +- Earth-return, modal transformation, ATPDraw, and PSCAD integration. +- Material and cable libraries with JSON import and export. +- Optional Makie plotting and Gmsh/GetDP finite-element integration. -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 or FEM 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). +## Optional plotting + +Load one Makie backend explicitly before calling `preview`, `plot`, or +`set_backend!`: + +```julia +using LineCableModels +using CairoMakie + +set_backend!(:cairo) +``` + +`GLMakie` and `WGLMakie` are supported in the same way. LineCableModels +never imports or selects a backend dynamically. + +## Transitional FEM integration + +The v0.2 compatibility API requires Gmsh and an external GetDP executable: + +```julia +using LineCableModels +using Gmsh +using LineCableModels.Engine.FEM + +formulation = FormulationSet(:FEM) +``` + +Set `GETDP_EXECUTABLE` to the absolute executable path or make `getdp` +available on `PATH`. The legacy FEM/GetDP API emits deprecation warnings +because it will be simplified in a future release. + +See the [documentation](https://electa-git.github.io/LineCableModels.jl/) and +[examples](examples) for supported workflows. -## 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 private GetDP frontend snapshot retains its own BSD license and provenance +under `ext/fem/getdp_frontend/`. The separate GetDP executable and its +GPL-2.0-or-later license are described in +[THIRD_PARTY_NOTICE.md](THIRD_PARTY_NOTICE.md). Citation metadata is provided +in [CITATION.cff](CITATION.cff). ## Acknowledgements -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/RELEASE_AUDIT.md b/RELEASE_AUDIT.md new file mode 100644 index 00000000..ed83c63d --- /dev/null +++ b/RELEASE_AUDIT.md @@ -0,0 +1,95 @@ +# v0.2.0 release audit + +Audit baseline: `main` at +`88f5de54202f5a97c7556b4ceac662b997f13de2`, before release-preparation +edits. The baseline test evidence was 1,750 package assertions and 19 FEM +integration assertions. + +## P0 — release blockers + +- **Unregistered hard dependency:** GetDP.jl was referenced through + `[sources]`, which is not acceptable for General registration. Resolution: + remove the dependency and vendor the compatible frontend privately from + commit `b1d91b0d8974ea642b772462edcf6e26299fdf0a`, tree + `2a2fe2782b1c17a235a63d3639b99ae012dab1f5`. +- **Optional systems loaded by core:** Makie, Gmsh, and GetDP were hard + dependencies. Resolution: package extensions and explicit activation. +- **FEM coupled to routine coverage:** the external solver integration was + discoverable by the normal test runner. Resolution: a manual release-only, + coverage-disabled gate with a pinned GetDP archive. +- **Publication ambiguity:** two workflows could create releases. Resolution: + TagBot is the sole tag and GitHub-release authority. +- **Registry metadata:** package version and installation guidance targeted a + development checkout. Resolution: version 0.2.0 and clean-install checks. +- **Credential scan:** no private credentials were found. The only tracked + token-shaped literal is the existing public Codecov badge identifier, not an + upload credential. The repository history was not rewritten. +- **Third-party licensing:** the vendored Julia frontend retains its upstream + BSD license and provenance. `THIRD_PARTY_NOTICE.md` also identifies the + registered Gmsh wrapper and the licenses of the separately supplied Gmsh and + GetDP programs. No GetDP binary or downloader is distributed. + +## P1 — correctness and maintenance + +- Aqua was skipped and reported three method ambiguities. The dispatch + intersections are now explicit, and Aqua is unconditional. +- Three plotting exports had no definitions. They were corrected while moving + implementation code to extensions. +- Binder, the TODO-to-issue scraper, the duplicate release template, and the + accidental standalone cable-builder tree were maintained dead weight and + were removed. +- The documentation build used `warnonly`, rewrote source files during an + opaque build phase, and opened a browser locally. It was split into explicit + instantiate, doctest, build, and deploy phases. +- Development instructions tied mathematical documentation to the `calc_` + prefix and required unsustainable cross-reference lists. The maintained + convention now follows physical meaning and contains no “See also” sections. +- GetDP executable resolution could invoke a downloader. The compatibility + frontend now checks `GETDP_EXECUTABLE`, then `PATH`, and otherwise errors. + +## P2 — deliberately deferred + +- Wholesale public renaming, including the historical `calc_` prefix. +- Broad module or type redesign outside the dependency-extension boundary. +- Speculative performance changes and cleanup of nontrivial historical code. +- Simplification of the future FEM API beyond the compatibility facade. + +## Branch evidence + +The abandoned `updates-runner` tips recorded before deletion were: + +- local: `47b53243b8353985665fb08e56389814d0da2410` +- origin: `56bc4ee0b3d987fc56c0b6e752180b6fa7158c0d` + +Local and GitHub branch listings after cleanup contain only `main`, +`refactor/cablebuilder`, and `gh-pages`. + +## Verification evidence + +The following locally executable gates passed on Julia 1.12.6: + +| Gate | Result | +| --- | --- | +| Routine package tests, including unconditional Aqua | 1,734/1,734 assertions; no plotting/FEM/formatter dependency resolved | +| CairoMakie extension tests | 1,744/1,744 assertions | +| Release-only FEM integration | 19/19 assertions with GetDP 3.5.0 | +| GetDP archive verification | SHA-256 `d3c28fa18f20d6147b4c7367d4dd802e9f7ddb58c608688bbb71919dbca8041d` | +| Independent Aqua run | all checks passed, including ambiguities, exports, stale dependencies, compat, piracy, and persistent tasks | +| Documentation doctests | passed | +| Strict documentation build | passed without `warnonly` | +| SciML formatting | JuliaFormatter 2.12.4 returned `true` with `overwrite=false` | +| Commit policy | scoped, lowercase, 72-character gitlint policy validated with gitlint 0.19.1 | +| Optional dependency boundary | core loaded no Makie backend, Gmsh, or GetDP; missing-backend errors were actionable | +| Registry dependency audit | all 32 dependencies and weak dependencies have registered or standard-library UUIDs | + +The baseline and release trees were also compared structurally: every baseline +test expression remains represented, the 19 FEM assertions moved intact to the +manual integration harness, and two plotting lifecycle assertions were added. +The numerical totals of the routine runner changed because FEM discovery was +removed and Aqua was made unconditional, not because covered behavior was +discarded. + +The prerelease-Julia job is defined in CI and must run on GitHub because only +Julia 1.12 is installed locally. The immutable release commit and its clean +commit-based installation result are recorded in the release handoff; a commit +cannot embed its own SHA. diff --git a/THIRD_PARTY_NOTICE.md b/THIRD_PARTY_NOTICE.md new file mode 100644 index 00000000..4a871548 --- /dev/null +++ b/THIRD_PARTY_NOTICE.md @@ -0,0 +1,26 @@ +# Third-party notices + +## Gmsh + +The optional FEM extension uses the registered Gmsh.jl package. Its Julia +wrapper is MIT-licensed; the Gmsh library and executable are distributed under +the GNU General Public License, version 2 or later, with Gmsh's linking +exception. Gmsh is resolved by Julia's package manager and is not vendored in +this repository. See for source and licensing. + +## GetDP frontend snapshot + +The private Julia frontend under `ext/fem/getdp_frontend/` is derived from +Electa-Git/GetDP.jl commit +`b1d91b0d8974ea642b772462edcf6e26299fdf0a`. It is distributed under the BSD +3-Clause License included in that directory. `NOTICE.md` records the upstream +tree and the mechanical compatibility changes. + +## GetDP executable + +The optional FEM integration can invoke a separately installed GetDP +executable. GetDP 3.5.0 is distributed by its authors under the GNU General +Public License, version 2 or later. LineCableModels does not include, download, +or redistribute GetDP binaries or source code. Users provide the executable +through `GETDP_EXECUTABLE` or `PATH` and remain responsible for its license +terms. See for GetDP downloads, source, and licensing. diff --git a/binder/Project.toml b/binder/Project.toml deleted file mode 100644 index 0bdc76be..00000000 --- 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 76f9d749..00000000 --- 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 b8cc8122..00000000 --- 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 index ba3f0068..f551885d 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,3 +1,5 @@ ignore: - - "LineCableModels/src/legacy/**" - + - "src/engine/fem/**" + - "ext/LineCableModelsGmshExt.jl" + - "ext/fem/getdp_frontend/**" + - "integration/fem/**" diff --git a/docs/deploy.jl b/docs/deploy.jl new file mode 100644 index 00000000..e633b932 --- /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 00000000..e5ebc09d --- /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 00000000..29240829 --- /dev/null +++ b/docs/instantiate.jl @@ -0,0 +1,6 @@ +using Pkg + +const ROOT_DIR = normpath(joinpath(@__DIR__, "..")) +Pkg.activate(@__DIR__) +Pkg.develop(PackageSpec(path = ROOT_DIR)) +Pkg.instantiate() diff --git a/docs/make.jl b/docs/make.jl index 05b8db68..f8a388bc 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,204 +1,139 @@ +using Changelog using Documenter using DocumenterCitations +using LineCableModels using Literate -using Pkg -using Changelog - -function get_project_toml() - # Get the current active environment (docs) - docs_env = Pkg.project().path - - # Path to the main project (one level up from docs) - main_project_path = joinpath(dirname(docs_env), "..") - - # Parse the main project's TOML - project_toml = Pkg.TOML.parsefile(joinpath(main_project_path, "Project.toml")) - - return project_toml -end - -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 +using TOML + +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 CANONICAL_URL = "https://electa-git.github.io/LineCableModels.jl" +const TUTORIAL_SOURCE = joinpath(ROOT_DIR, "examples") +const TUTORIAL_OUTPUT = joinpath(DOCS_SRC_DIR, "tutorials") + +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 - - -# 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 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 -function post_process_literate(content) - content = customize_literate_footer(content, "🏠 Back to [Tutorials](@ref)\n") - return content +function tutorial_title(path::AbstractString) + content = read(path, String) + matchobj = match(r"(?m)^#\s+(.+)$", content) + isnothing(matchobj) || + return String(matchobj.captures[1]) + stem = splitext(basename(path))[1] + return titlecase(replace(stem, "_" => " ", "-" => " ")) 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) -end -mkpath(tutorial_output) +function build_tutorials!() + rm(TUTORIAL_OUTPUT; recursive = true, force = true) + mkpath(TUTORIAL_OUTPUT) -for file in readdir(tutorial_source) - if endswith(file, ".jl") + 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 = false, + 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), -) + 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 -# 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) +function generate_maintained_pages!() + Changelog.generate( + Changelog.Documenter(), + joinpath(ROOT_DIR, "CHANGELOG.md"), + joinpath(DOCS_SRC_DIR, "CHANGELOG.md"); + repo = REPOSITORY + ) + cp(joinpath(ROOT_DIR, "TODO.md"), joinpath(DOCS_SRC_DIR, "TODO.md"); force = true) + return nothing end -tutorial_pages = [String(joinpath("tutorials", file)) for file in tutorial_files] - -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, -) - -mathengine = MathJax3( - Dict( - :loader => Dict("load" => ["[tex]/physics"]), - :tex => Dict( - "inlineMath" => [["\$", "\$"], ["\\(", "\\)"]], - "tags" => "ams", - "packages" => ["base", "ams", "autoload", "physics"], - ), - :chtml => Dict( - :scale => 1.1, - ), - ), + quote + using LineCableModels + using LineCableModels.DataModel.BaseParams + end; + recursive = true ) -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, "refs.bib"); style = :numeric) 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 = CANONICAL_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, + "Tutorials" => Any["Contents" => "tutorials.md", tutorials...], "API reference" => "reference.md", "Development" => Any[ - "Validation module"=>"validation.md", - "Docstrings"=>"docstrings.md", - "TODO"=>"TODO.md", - "Changelog"=>"CHANGELOG.md", + "Conventions" => "conventions.md", + "Validation module" => "validation.md", + "Docstrings" => "docstrings.md", + "Contributing" => "contributing.md", + "TODO" => "TODO.md", + "Changelog" => "CHANGELOG.md" ], - "Bibliography" => "bib.md", + "Bibliography" => "bib.md" ], - clean=true, - plugins=[bib], - checkdocs=:exports, - pagesonly=true, - warnonly=true, + clean = true, + plugins = [bibliography], + 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 - +@info "Finished documentation build." diff --git a/docs/src/CHANGELOG.md b/docs/src/CHANGELOG.md index ed9a2d87..cf322a20 100644 --- a/docs/src/CHANGELOG.md +++ b/docs/src/CHANGELOG.md @@ -4,32 +4,67 @@ EditURL = "https://github.com/Electa-Git/LineCableModels.jl/blob/master/CHANGELO # Changelog -All notable changes to this project will be documented in this file. +All notable 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] +## [Unreleased](https://github.com/Electa-Git/LineCableModels.jl/compare/v0.2.0...HEAD) -- / +## [0.2.0] - 2026-08-13 + +### Added + +- Julia package extensions for Gmsh and the Makie backends. +- A manual, checksum-verified FEM release gate. +- Aqua, SciML formatting, gitlint, clean-install, and modular documentation + checks. +- Citation and contribution metadata. ### Changed -- Refactored CodeComponent structure and constructors. +- Gmsh and Makie are optional dependencies. Core loading no longer imports + Gmsh, Makie, CairoMakie, GLMakie, WGLMakie, or GetDP. +- The legacy `Engine.FEM` API delegates to the Gmsh extension and emits + deprecation warnings. +- The compatible GetDP frontend is now a private, licensed source snapshot. + GetDP itself must be supplied through `GETDP_EXECUTABLE` or `PATH`. +- Plotting requires the caller to load CairoMakie, GLMakie, or WGLMakie + explicitly. +- Documentation examples and development conventions were consolidated. -### Added +### Removed + +- Binder experiments and Binder-specific notebook bootstrapping. +- The accidental standalone `src/cablebuilder` subsystem. The maintained + `ParametricBuilder.CableBuilder`, sector support, covariance work, and + their tests remain. +- The hard dependency on the external, unregistered GetDP.jl package. +- The obsolete TODO scraper and duplicate tag-release workflow. -- Included interface to run finite element simulations using [Onelab](https://onelab.info/). +### Migration -- Included basic coverage tests. +Load optional integrations explicitly: + +```julia +using LineCableModels + +using CairoMakie +# preview(...) and plot(...) are now available + +using Gmsh +using LineCableModels.Engine.FEM +formulation = FormulationSet(:FEM) +``` -- Included import/export of CableDesigns to JSON files. +Set `GETDP_EXECUTABLE=/absolute/path/to/getdp` or place `getdp` on +`PATH` before invoking the transitional FEM solver. -## [v0.1.0](https://github.com/Electa-Git/LineCableModels.jl/releases/tag/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](https://github.com/Electa-Git/LineCableModels.jl/releases/tag/v0.1.0) +[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/docs/src/cable-builder.md b/docs/src/cable-builder.md deleted file mode 100644 index 5431de25..00000000 --- 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 00000000..df3a1735 --- /dev/null +++ b/docs/src/contributing.md @@ -0,0 +1,19 @@ +# 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(".")`. Commit +subjects must satisfy `.gitlint`: a scoped Conventional Commit, a lowercase +description, and no more than 72 characters. + +Keep pull requests focused. Add tests for changed behavior and update public +documentation when an API changes. Optional plotting and FEM integrations must +remain outside core loading and must be checked with their dedicated workflows. +Do not format or otherwise rewrite `ext/fem/getdp_frontend/`; it preserves the +documented upstream snapshot. diff --git a/docs/src/conventions.md b/docs/src/conventions.md index 268866c8..44d5e276 100644 --- a/docs/src/conventions.md +++ b/docs/src/conventions.md @@ -1,244 +1,34 @@ -# Package conventions +# Development conventions ---- - -## Function and method names - -### 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 - -```julia -_resolve_ # Primary resolution function -├─ _parse_inputs_ # Type conversion & normalization -└─ _do_resolve_ # Core implementation logic -``` - -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. - -### Calculation vs. computation pattern - -The codebase distinguishes between calculation and computation methods through a clear naming convention: - -#### Calculation methods (`calc_`) - -Functions prefixed with `calc_` handle intermediate steps within a broader computational framework. These methods: - -- 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. - -#### Computation methods (`comp_`) - -Functions prefixed with `comp_` represent higher-level operations that perform multiple calculation steps to achieve a complete analysis. These methods: - -- 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. - -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. - -### Library management pattern - -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. - -#### Library operations naming structure - -```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 -``` - -Each library operation follows a predictable naming convention: - -1. **`store_!`**: Adds or updates objects in the specified library. The exclamation mark indicates that this operation modifies the library state. - -2. **`remove_!`**: Removes an object from the specified library. The exclamation mark indicates that this operation modifies the library state. - -3. **`save_`**: Persists the current state of the library to external storage, typically a file. This operation does not modify the library itself. - -4. **`list_`**: Displays the contents of the library for inspection without modifying its state. - -### Object modification pattern - -For operations that modify components within larger structures (e.g., [`AbstractConductorPart`](@ref) within a parent [`ConductorGroup`](@ref)), the codebase employs the `addto_` prefix: - -```julia -addto_! # Add or modify a subcomponent within a larger component -``` - -The `addto_!` pattern: - -- 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. - -This pattern allows for hierarchical composition of components while maintaining a clear distinction from library management operations. - -### DataFrame view pattern - -The codebase implements a consistent pattern for generating `DataFrame` views of complex objects through a standardized naming convention: +LineCableModels follows the SciML formatter style. Run ```julia -_todf # Convert entity to a `DataFrame` representation +using JuliaFormatter +format(".") ``` -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. - -The `_todf` suffix provides a concise and immediately recognizable identifier for functions that expose object data in tabular format. - ---- - -## Module organization - -### Exports and visibility - -- 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. - -### Module structure - -- 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`. - -### Code navigation - -- Module docstrings should list key exported functions and types. -- All exported items require docstrings. -- Internal implementation details use minimal documentation. - ---- - -## Framework design pattern - -### Core architecture - -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: - -1. **Problem definition**: Defines the physics/mathematical approach -2. **Solver**: Controls execution parameters -3. **Workspace**: Centralizes all state during computation - -This pattern separates *what* is being calculated (problem definition, formulation) from *how* calculations are executed (engine used, solver) and *where* data is stored (workspace). - -### The workspace pattern - -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`). - -A workspace: - -- 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. +before committing. The vendored GetDP frontend under +`ext/fem/getdp_frontend/` is a provenance-pinned compatibility snapshot; its +documented loader and namespace adaptations are excluded from formatting. -### Type hierarchy +Versions follow [Semantic Versioning](https://semver.org/). Public behavior is +kept compatible within a minor release; deprecations must provide a migration +path before removal. -Each module follows a consistent type hierarchy: +Commit subjects use scoped Conventional Commits, start with a lowercase +description, and remain within 72 characters. For example: -```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] +```text +fix(fem): resolve getdp from the configured executable ``` -This hierarchy enables both specialization and shared interfaces. - -### Data flow - -Data flows through the system in a consistent pattern: - -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). - -This pattern applies regardless of the specific module or calculation type. - -### Multi-phase processing - -All modules implement a multi-phase execution pattern with clear separation between phases. For example, the `FEMTools` module follows this pattern: - -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. - -This pattern ensures clean separation of concerns, making the code more maintainable. - -### State management - -State is managed exclusively through the Workspace, which contains: - -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. - -This centralized approach eliminates global state and ensures thread safety. - -### Implementation example: FEMTools.jl - -The FEMTools module exemplifies this pattern: - -- `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. - -### Extension to new modules - -When creating new modules, the following patterns should be followed: - -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. +Changes must include tests at the lowest useful level. Core tests must not load +optional integrations. CairoMakie and FEM/GetDP are verified in separate gates. +Examples in docstrings should be executable and self-contained; examples that +require fixtures, user interfaces, or external executables belong in integration +documentation instead. -This framework ensures consistency, maintainability, and performance across all modules within the package. +Physical quantities state their SI units. A docstring for implemented +physically meaningful mathematics includes a `# Notes` section with the +equation and definitions used by the code. This requirement follows the +implementation, not a naming prefix. diff --git a/docs/src/docstrings.md b/docs/src/docstrings.md index 01ee70ff..5734b4b5 100644 --- a/docs/src/docstrings.md +++ b/docs/src/docstrings.md @@ -1,197 +1,77 @@ # Docstrings -The following docstring standards are generally adopted across the codebase. +Docstrings describe supported behavior, physical meaning, units, and limitations. +This release retains the established package style; it does not require a +wholesale signature or macro rewrite. -## General principles +## Signatures and sections -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. +Place a docstring immediately before its binding. Use an explicit Julia +signature when it is clearer to a user; existing DocStringExtensions signatures +may remain where they render correctly. -## Physical unit formatting +Use only the sections that add information: -All variables corresponding to physical quantities must be annotated with their SI units and according to the following rules: +- `# Arguments` for non-obvious inputs; +- `# Returns` for the returned type, shape, and units; +- `# Notes` for implemented physical mathematics and material assumptions; +- `# Errors` for deliberate exceptions; +- `# Examples` for concise supported syntax. -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\\]`). +Dedicated cross-reference lists are not maintained. -## Mathematical formulation formatting +## Physical quantities -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}``` +State SI units for physically meaningful arguments, fields, and returns. State +when a physical value is dimensionless. Keep the package's existing escaped +unit notation, such as `\\[m\\]`, `\\[Ω·m\\]`, and +`\\[dimensionless\\]`. -## Documentation templates +## Mathematical notes -The subsections below contain templates for different types of code elements. +A method that implements a physically meaningful equation has a `# Notes` +section containing the equation and definitions used by the implementation. +This requirement follows the implementation, regardless of whether the method +name begins with `calc_`. -### Structs +For example: -- **Main docstring:** Use `$(TYPEDEF)` for the signature and `$(TYPEDFIELDS)` to list the fields automatically. Provide a concise description of the struct purpose. +````julia +""" + resistance_per_length(resistivity, area) - ```julia - """ - $(TYPEDEF) +Return conductor resistance per unit length. - Represents a physical entity with specific properties... +# Arguments - $(TYPEDFIELDS) - """ - struct StructName - # Field definitions follow - end - ``` +- `resistivity`: Electrical resistivity \\[Ω·m\\]. +- `area`: Conducting cross-sectional area \\[m²\\]. -- **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. +# Returns +- Resistance per unit length \\[Ω/m\\]. -### Constructors (inside or outside structs) +# Notes -- 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`). +```math +R' = \\frac{\\rho}{A}. +``` +""" +resistance_per_length(resistivity, area) = resistivity / area +```` - ````julia - @doc """ - $(TYPEDSIGNATURES) +Preserve correct existing mathematical Notes. When the implementation and +documentation disagree, inspect and test the implementation before changing +either. - Constructs a [`StructName`](@ref) instance. +## Examples - # Arguments +Prefer `jldoctest` for self-contained public examples. When textual output is +not part of the contract, place assertions before the doctest's `# output` +separator and leave the expected output empty. An example that needs an +external executable, repository fixture, network access, or a graphical +interaction belongs in integration documentation instead of a docstring. - - `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_`. +Use explicit function names in new examples so copied code is readable. +Existing DocStringExtensions example macros may remain when they render valid +Julia syntax. diff --git a/docs/src/index.md b/docs/src/index.md index 3796e339..ad45fca9 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -26,24 +26,38 @@ Depth = 1 ## 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 +``` + +Plotting is optional. Load one backend explicitly before calling `preview` or +`plot`: + +```julia +using LineCableModels +using CairoMakie ``` -Then, in your Julia code, import the package: +The transitional FEM integration is also optional. Load `Gmsh` explicitly and +make a GetDP executable available through `GETDP_EXECUTABLE` or `PATH`: ```julia using LineCableModels +using Gmsh +using LineCableModels.Engine.FEM ``` +The legacy FEM API emits deprecation warnings because it will be simplified in +a future release. + ## License The source code is provided under the [BSD 3-Clause License](https://github.com/Electa-Git/LineCableModels.jl/LICENSE). @@ -51,4 +65,4 @@ The source code is provided under the [BSD 3-Clause License](https://github.com/ --- ```@raw html

Documentation generated using Documenter.jl and Literate.jl.

-``` \ No newline at end of file +``` diff --git a/docs/src/reference.md b/docs/src/reference.md index 596b9ae5..0e662382 100644 --- a/docs/src/reference.md +++ b/docs/src/reference.md @@ -1,121 +1,108 @@ # 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 documents the public API and the documented implementation surface of +`LineCableModels.jl`. ## Contents + ```@contents Pages = ["reference.md"] Depth = 3 ``` ---- +## Core utilities -## Data model ```@autodocs -Modules = [LineCableModels.DataModel] +Modules = [ + LineCableModels, + LineCableModels.Commons, + LineCableModels.UnitHandler, + LineCableModels.Utils, +] Order = [:module, :constant, :type, :function, :macro] Public = true -Private = false +Private = true ``` -### Base parameters (R, L, C, G) +## Data model and materials + ```@autodocs -Modules = [LineCableModels.DataModel.BaseParams] +Modules = [ + LineCableModels.Materials, + LineCableModels.DataModel, + LineCableModels.DataModel.BaseParams, + LineCableModels.EarthProps, +] Order = [:module, :constant, :type, :function, :macro] Public = true -Private = false +Private = true ``` ---- +## Line-parameter engine -## Earth properties ```@autodocs -Modules = [LineCableModels.EarthProps] +Modules = [ + LineCableModels.Engine, + LineCableModels.Engine.EarthAdmittance, + LineCableModels.Engine.EarthImpedance, + LineCableModels.Engine.EHEM, + LineCableModels.Engine.FEM, + LineCableModels.Engine.InsulationAdmittance, + LineCableModels.Engine.InsulationImpedance, + LineCableModels.Engine.InternalImpedance, + LineCableModels.Engine.Transforms, +] Order = [:module, :constant, :type, :function, :macro] Public = true -Private = false +Private = true ``` ---- +## Parametric and uncertainty modeling -## Import & export ```@autodocs -Modules = [LineCableModels.ImportExport] +Modules = [ + LineCableModels.ParametricBuilder, + LineCableModels.ParametricBuilder.WirePatterns, + LineCableModels.UQ, +] Order = [:module, :constant, :type, :function, :macro] Public = true -Private = false +Private = true ``` ---- +## Plot specifications -## Materials library ```@autodocs -Modules = [LineCableModels.Materials] +Modules = [ + LineCableModels.PlotBuilder, + LineCableModels.PlotBuilder.BackendHandler, +] Order = [:module, :constant, :type, :function, :macro] Public = true -Private = false +Private = true ``` ---- +## Import and export -## Utilities ```@autodocs -Modules = [LineCableModels.Utils] +Modules = [LineCableModels.ImportExport] Order = [:module, :constant, :type, :function, :macro] Public = true -Private = false +Private = true ``` ---- +## Uncertainty-aware Bessel functions -## Uncertain Bessels ```@autodocs Modules = [LineCableModels.UncertainBessels] Order = [:module, :constant, :type, :function, :macro] Public = true -Private = false -``` - ---- - -## Private API - -#### Data model -```@autodocs -Modules = [LineCableModels.DataModel] -Order = [:module, :constant, :type, :function, :macro] -Public = false -Private = true -``` - -#### Earth properties -```@autodocs -Modules = [LineCableModels.EarthProps] -Order = [:module, :constant, :type, :function, :macro] -Public = false Private = true ``` -#### Materials library -```@autodocs -Modules = [LineCableModels.Materials] -Order = [:module, :constant, :type, :function, :macro] -Public = false -Private = true -``` +## Index -#### Utilities -```@autodocs -Modules = [LineCableModels.Utils] +```@index +Pages = ["reference.md"] Order = [:module, :constant, :type, :function, :macro] -Public = false -Private = true ``` - ---- - -## Index -```@index -Pages = ["reference.md"] -Order = [:module, :constant, :type, :function, :macro] -``` \ No newline at end of file diff --git a/docs/src/tutorials.md b/docs/src/tutorials.md index 8d0f126c..17b387dd 100644 --- a/docs/src/tutorials.md +++ b/docs/src/tutorials.md @@ -1,6 +1,6 @@ # Tutorials -In this section you will find step-by-step guides to help you get started with the [`LineCableModels.jl`](@ref) package. These guides are designed to be straightforward and practical, covering the most common use cases and features. +In this section you will find step-by-step guides to help you get started with the [`LineCableModels.jl`](@ref) package. These guides are designed to be straightforward and practical, covering the most common use cases and features. ```@contents Pages = Main.tutorial_pages diff --git a/examples/tutorial1.jl b/examples/tutorial1.jl index f38e8660..ed3f025c 100644 --- a/examples/tutorial1.jl +++ b/examples/tutorial1.jl @@ -10,7 +10,7 @@ Beyond showcasing the API, this guide serves as a practical reference by providi **Tutorial outline** ```@contents Pages = [ - "tutorial1.md", + "tutorial1.md", ] Depth = 2:3 ``` @@ -27,8 +27,8 @@ fullfile(filename) = joinpath(@__DIR__, filename); #hide set_verbosity!(0); #hide #= -The [`MaterialsLibrary`](@ref) is a container for storing electromagnetic properties of -different materials used in power cables. By default, it initializes with several common +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. =# @@ -45,7 +45,7 @@ The function [`DataFrame`](@ref) returns a `DataFrame` with all materials and th # ## Adding new materials #= !!! note "Note" - New materials can be added to the library using the [`Material`](@ref) constructor followed by [`add!`](@ref). + New materials can be added to the library using the [`Material`](@ref) constructor followed by [`add!`](@ref). It might be useful to add other conductor materials with corrected properties based on recognized standards [cigre531](@cite) [IEC60287](@cite). =# @@ -81,7 +81,7 @@ add!(materials, "conductive_paper", conductive_paper) # ## Removing materials #= !!! note "Note" - Materials can be removed from the library with the [`delete!`](@ref) function. + Materials can be removed from the library with the [`delete!`](@ref) function. =# # Add a duplicate material by accident: @@ -98,11 +98,10 @@ materials_df = DataFrame(materials) output_file = fullfile("materials_library.json") save(materials, file_name = output_file); - # ## 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. + 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. =# # Initialize a new [`MaterialsLibrary`](@ref) and load from the JSON file: diff --git a/examples/tutorial2.jl b/examples/tutorial2.jl index a0f317ca..337522b7 100644 --- a/examples/tutorial2.jl +++ b/examples/tutorial2.jl @@ -1,7 +1,7 @@ #= # Tutorial 2 - Building a cable design -This tutorial demonstrates how to model a typical medium-voltage single-core power cable +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. =# @@ -9,7 +9,7 @@ using the [`LineCableModels.jl`](@ref) package. The objective is to build a comp **Tutorial outline** ```@contents Pages = [ - "tutorial2.md", + "tutorial2.md", ] Depth = 2:3 ``` @@ -63,7 +63,7 @@ NA2XS(FL)2Y │ │ │ │ │ │ │ └── 2Y: Outer sheath of polyethylene (PE) │ │ └── (FL): Longitudinal watertight protection -│ │ +│ │ │ └── 2XS: XLPE insulation with screen of copper wires └── NA: Aluminum conductor ``` @@ -114,18 +114,18 @@ 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 + 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 ) #hide #= ## 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. + 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 @@ -159,17 +159,17 @@ Each [`CableComponent`](@ref) represents a functional group of the cable (core, ### Conductor groups -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. +The [`ConductorGroup`](@ref) object serves as a specialized container for organizing `AbstractConductorPart` 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 `CircStrands` 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. +The [`InsulatorGroup`](@ref) object organizes `AbstractInsulatorPart` elements in concentric layers, calculating the equivalent capacitance (C) and conductance (G) parameters. #### AbstractInsulatorPart implementations @@ -177,18 +177,17 @@ The [`InsulatorGroup`](@ref) object organizes [`AbstractInsulatorPart`](@ref) el - 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: + 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. + 1. Computing geometry-specific parameters at the `AbstractConductorPart` and `AbstractInsulatorPart` 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. =# - #= ## 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. +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` object, which handles the helical pattern and twisting effects via `calc_helical_params`. =# # Initialize the conductor object and assign the central wire: @@ -197,7 +196,7 @@ 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. + 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. =# # Add the subsequent layers of wires and inspect the object: @@ -215,7 +214,7 @@ the conductor and insulation, eliminating air gaps and reducing field concentrat #= !!! 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 [`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). =# # Inner semiconductive tape: @@ -262,14 +261,14 @@ With the core parts properly defined, the [`CableDesign`](@ref) object is initia # 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] + 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] ) cable_design = CableDesign(cable_id, core_cc, nominal_data = datasheet_info) @@ -290,10 +289,9 @@ The metallic screen (typically copper) serves multiple purposes: # 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), - ) +screen_con = ConductorGroup( + CircStrands(main_insu, Diameter(d_ws), num_sc_wires, lay_ratio, material), +) # Add the equalizing copper tape wrapping the wire screen: add!(screen_con, Strip, Thickness(t_cut), w_cut, lay_ratio, material) @@ -331,7 +329,7 @@ add!(jacket_insu, Insulator, Thickness(t_jac), material) #= !!! 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. + 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. =# # Assign the jacket parts directly to the design: @@ -360,7 +358,7 @@ detailed_df = DataFrame(cable_design, :detailed) ## 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. + 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 `DataFrame`, [`add!`](@ref), and [`save`](@ref). =# # Store the cable design and inspect the library contents: @@ -372,16 +370,15 @@ library_df = DataFrame(library) output_file = fullfile("cables_library.json") save(library, file_name = output_file); - #= ### 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. + 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. =# #= -### 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. =# @@ -399,25 +396,24 @@ earthmodel_df = DataFrame(earth_params) This section ilustrates the construction of a cable system with three identical cables arranged in a trifoil formation. =# - # 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); # Initialize the `LineCableSystem` with the first cable (phase A): cablepos = CablePosition(cable_design, xa, ya, - Dict("core" => 1, "sheath" => 0, "jacket" => 0)) + Dict("core" => 1, "sheath" => 0, "jacket" => 0)) cable_system = LineCableSystem("18kV_1000mm2_trifoil", 1000.0, cablepos) # Add remaining cables (phases B and C): add!(cable_system, cable_design, xb, yb, - Dict("core" => 2, "sheath" => 0, "jacket" => 0)) + Dict("core" => 2, "sheath" => 0, "jacket" => 0)) add!(cable_system, cable_design, xc, yc, - Dict("core" => 3, "sheath" => 0, "jacket" => 0)) + Dict("core" => 3, "sheath" => 0, "jacket" => 0)) #= !!! 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. + 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. =# #= @@ -459,5 +455,5 @@ This tutorial has demonstrated how to: [`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! +ensures that electromagnetic parameters are calculated with high precision. Now you can go ahead and run these cable simulations like a boss! =# diff --git a/examples/tutorial2_sector.jl b/examples/tutorial2_sector.jl index 848b5ac8..1de851b8 100644 --- a/examples/tutorial2_sector.jl +++ b/examples/tutorial2_sector.jl @@ -9,7 +9,7 @@ using the [`LineCableModels.jl`](@ref) package. The objective is to build a comp **Tutorial outline** ```@contents Pages = [ - "tutorial2_sector.md", + "tutorial2_sector.md", ] Depth = 2:3 ``` @@ -47,13 +47,12 @@ We start by defining the materials. We will create a custom aluminum material wi =# # Initialize materials library and add a PVC material -materials = MaterialsLibrary(add_defaults=true) +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 @@ -143,7 +142,7 @@ 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. @@ -169,4 +168,3 @@ This tutorial has demonstrated how to model a three-core cable with sector-shape This detailed modeling capability allows for accurate analysis of various cable configurations. =# - diff --git a/examples/tutorial3.jl b/examples/tutorial3.jl index 2bbfb345..bf4a945c 100644 --- a/examples/tutorial3.jl +++ b/examples/tutorial3.jl @@ -1,7 +1,7 @@ #= # Tutorial 3 - Computing line parameters -This case file demonstrates how to model an armored high-voltage single-core power cable +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). =# @@ -9,7 +9,7 @@ using the [`LineCableModels.jl`](@ref) package. The objective is to build a comp **Tutorial outline** ```@contents Pages = [ - "tutorial3.md", + "tutorial3.md", ] Depth = 2:3 ``` @@ -52,7 +52,7 @@ 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_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 @@ -84,14 +84,13 @@ 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 + 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 ) #hide #= @@ -107,7 +106,7 @@ core = ConductorGroup(CircStrands(0.0, Diameter(d_w), 1, 0.0, material)) 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) + add!(core, CircStrands, Diameter(d_w), i * n_strands, 11.0, material) end core @@ -147,14 +146,14 @@ core_cc = CableComponent("core", core, main_insu) 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] + 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) @@ -188,7 +187,7 @@ add!(cable_design, sheath_cc) 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)) + CircStrands(screen_insu, Diameter(d_wa), num_ar_wires, lay_ratio, material)) # PP layer after armor: material = get(materials, "pp") @@ -218,7 +217,6 @@ components_df = DataFrame(cable_design, :components) Load an existing [`CablesLibrary`](@ref) file or create a new one: =# - library = CablesLibrary() library_file = fullfile("cables_library.json") load!(library, file_name = library_file) @@ -234,7 +232,7 @@ save(library, file_name = library_file); =# #= -### Earth model +### Earth model Define a constant frequency earth model: =# @@ -255,12 +253,12 @@ xp, xn, y0 = -0.5, 0.5, -1.0; # Initialize the `LineCableSystem` with positive pole: cablepos = CablePosition(cable_design, xp, y0, - Dict("core" => 1, "sheath" => 0, "armor" => 0)) + Dict("core" => 1, "sheath" => 0, "armor" => 0)) cable_system = LineCableSystem("525kV_1600mm2_bipole", 1000.0, cablepos) # Add the other pole (negative) to the system: add!(cable_system, cable_design, xn, y0, - Dict("core" => 2, "sheath" => 0, "armor" => 0)) + Dict("core" => 2, "sheath" => 0, "armor" => 0)) #= ### Cable system preview @@ -293,10 +291,10 @@ export_file = export_data(:atp, cable_system, earth_params, file_name = output_f # 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 + cable_system, + temperature = 20.0, # Operating temperature + earth_props = earth_params, + frequencies = f # Frequency for the analysis ); # Estimate domain size based on skin depth in the earth @@ -304,54 +302,54 @@ 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) + 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 + 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 + 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 ); # 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, + 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 ); # Run the FEM solver @@ -371,4 +369,4 @@ Tv, p012 = Fortescue(tol = 1e-5)(p); per_km(p012, 1; mode = :ZY, tol = 1e-9) # Or the corresponding lumped circuit quantities -per_km(p012, 1; mode = :RLCG, tol = 1e-9) \ No newline at end of file +per_km(p012, 1; mode = :RLCG, tol = 1e-9) diff --git a/ext/LineCableModelsCairoMakieExt.jl b/ext/LineCableModelsCairoMakieExt.jl new file mode 100644 index 00000000..275da090 --- /dev/null +++ b/ext/LineCableModelsCairoMakieExt.jl @@ -0,0 +1,14 @@ +module LineCableModelsCairoMakieExt + +using CairoMakie +using LineCableModels + +activate!() = (CairoMakie.activate!(); :cairo) +make_screen(::AbstractString; kwargs...) = nothing + +function __init__() + activate!() + return nothing +end + +end # module LineCableModelsCairoMakieExt diff --git a/ext/LineCableModelsGLMakieExt.jl b/ext/LineCableModelsGLMakieExt.jl new file mode 100644 index 00000000..0a4e20cf --- /dev/null +++ b/ext/LineCableModelsGLMakieExt.jl @@ -0,0 +1,16 @@ +module LineCableModelsGLMakieExt + +using GLMakie +using LineCableModels + +activate!() = (GLMakie.activate!(); :gl) +function make_screen(title::AbstractString; kwargs...) + GLMakie.Screen(; title = String(title), kwargs...) +end + +function __init__() + activate!() + return nothing +end + +end # module LineCableModelsGLMakieExt diff --git a/ext/LineCableModelsGmshExt.jl b/ext/LineCableModelsGmshExt.jl new file mode 100644 index 00000000..fc3e5106 --- /dev/null +++ b/ext/LineCableModelsGmshExt.jl @@ -0,0 +1,84 @@ +module LineCableModelsGmshExt + +using Gmsh +using LineCableModels + +const _GETDP_DEPRECATION_MESSAGE = "The bundled GetDP frontend is a temporary compatibility layer and will be retired with the legacy FEM interface." + +function _warn_getdp_frontend() + Base.depwarn(_GETDP_DEPRECATION_MESSAGE, :GetDPFrontend) + return nothing +end + +module FEMImplementation + +using LineCableModels.Commons +using LineCableModels.Materials +using LineCableModels.EarthProps +using LineCableModels.DataModel +using LineCableModels.Engine +import LineCableModels.Engine: kronify, reorder_M, reorder_indices, merge_bundles!, + AbstractFormulationSet, AbstractImpedanceFormulation, + AbstractAdmittanceFormulation, + compute! +import LineCableModels.Commons: PhaseDomain, ModalDomain, LineParamsDomain, domain +import LineCableModels.Engine: AbstractFormulationOptions, LineParamOptions, build_options, + _COMMON_SYMS +import LineCableModels.DataModel: AbstractCablePart, AbstractConductorPart, + AbstractInsulatorPart +using LineCableModels.Utils: display_path, set_verbosity!, is_headless, to_nominal, + symtrans!, symtrans, line_transpose! +using Measurements +using LinearAlgebra +using Colors +using GeometryBasics: Point, Point2f +using Gmsh + +include("fem/getdp_frontend/GetDPFrontend.jl") +using .GetDPFrontend +using .GetDPFrontend: Problem, get_getdp_executable, add! +const GetDP = GetDPFrontend + +const FEM_SOURCE = joinpath(@__DIR__, "..", "src", "engine", "fem") + +include(joinpath(FEM_SOURCE, "types.jl")) +include(joinpath(FEM_SOURCE, "lineparamopts.jl")) +include(joinpath(FEM_SOURCE, "meshtransitions.jl")) +include(joinpath(FEM_SOURCE, "problemdefs.jl")) +include(joinpath(FEM_SOURCE, "workspace.jl")) +include(joinpath(FEM_SOURCE, "encoding.jl")) +include(joinpath(FEM_SOURCE, "drawing.jl")) +include(joinpath(FEM_SOURCE, "identification.jl")) +include(joinpath(FEM_SOURCE, "mesh.jl")) +include(joinpath(FEM_SOURCE, "materialprops.jl")) +include(joinpath(FEM_SOURCE, "helpers.jl")) +include(joinpath(FEM_SOURCE, "visualization.jl")) +include(joinpath(FEM_SOURCE, "space.jl")) +include(joinpath(FEM_SOURCE, "cable.jl")) +include(joinpath(FEM_SOURCE, "solver.jl")) +include(joinpath(FEM_SOURCE, "base.jl")) + +end # module FEMImplementation + +function Darwin(args...; kwargs...) + _warn_getdp_frontend() + return FEMImplementation.Darwin(args...; kwargs...) +end + +function Electrodynamics(args...; kwargs...) + _warn_getdp_frontend() + return FEMImplementation.Electrodynamics(args...; kwargs...) +end + +MeshTransition(args...; kwargs...) = FEMImplementation.MeshTransition(args...; kwargs...) +function calc_domain_size(args...; kwargs...) + FEMImplementation.calc_domain_size(args...; kwargs...) +end +preview_results(args...; kwargs...) = FEMImplementation.preview_results(args...; kwargs...) + +function formulation_set(; kwargs...) + _warn_getdp_frontend() + return FEMImplementation.formulation_set(; kwargs...) +end + +end # module LineCableModelsGmshExt diff --git a/ext/LineCableModelsMakieExt.jl b/ext/LineCableModelsMakieExt.jl new file mode 100644 index 00000000..3152b74b --- /dev/null +++ b/ext/LineCableModelsMakieExt.jl @@ -0,0 +1,79 @@ +module LineCableModelsMakieExt + +using LineCableModels +using Makie +using Dates: format, now + +const PlotBuilder = LineCableModels.PlotBuilder +const BackendHandler = LineCableModels.PlotBuilder.BackendHandler + +function current_backend_symbol() + name = nameof(Makie.current_backend()) + name === :CairoMakie && return :cairo + name === :GLMakie && return :gl + name === :WGLMakie && return :wgl + return :unknown +end + +renderfig(fig) = display(fig) + +include(joinpath( + @__DIR__, "..", "src", "plotbuilder", "plotuicomponents", "PlotUIComponents.jl")) +include(joinpath(@__DIR__, "..", "src", "plotbuilder", "uicomponents", "UIComponents.jl")) + +using LineCableModels.PlotBuilder: AbstractPlotSpec +using LineCableModels.PlotBuilder.BackendHandler: next_fignum +using .PlotUIComponents: ControlButtonSpec, ControlReaction, ICON_TTF, MI_REFRESH, MI_SAVE, + _make_window, _run_plot_pipeline, with_icon, with_plot_theme + +include(joinpath(@__DIR__, "..", "src", "plotbuilder", "plotspecs.jl")) + +module DataModelPreview + +using Makie +using Colors +using Printf +using Dates +using Statistics +using LineCableModels.DataModel +using LineCableModels.DataModel.BaseParams: calc_circstrands_coords +import LineCableModels.DataModel: AbstractCablePart, preview +using LineCableModels.Utils: is_in_testset, to_nominal +using LineCableModels.PlotBuilder.BackendHandler: current_backend_symbol, ensure_backend!, + next_fignum, renderfig +using ..PlotUIComponents: ICON_TTF, MI_REFRESH, MI_SAVE, gl_screen, with_icon + +include(joinpath(@__DIR__, "..", "src", "datamodel", "preview.jl")) + +end # module DataModelPreview + +module EnginePlots + +using Makie +using Measurements: Measurements +using LineCableModels.Engine +import LineCableModels.Engine: get_description, plot +using LineCableModels.Commons: ModalDomain, PhaseDomain, domain +using LineCableModels.PlotBuilder +using ..PlotUIComponents + +include(joinpath(@__DIR__, "..", "src", "engine", "plot.jl")) + +end # module EnginePlots + +module UQPlots + +using Makie +using Printf +using Distributions +using StatsBase +using LineCableModels.UQ: CableDesignMC, LineParametersMC, LineParametersPDF +using LineCableModels.PlotBuilder +using ..EnginePlots +using ..PlotUIComponents + +include(joinpath(@__DIR__, "..", "src", "uq", "plot.jl")) + +end # module UQPlots + +end # module LineCableModelsMakieExt diff --git a/ext/LineCableModelsWGLMakieExt.jl b/ext/LineCableModelsWGLMakieExt.jl new file mode 100644 index 00000000..42637bd6 --- /dev/null +++ b/ext/LineCableModelsWGLMakieExt.jl @@ -0,0 +1,14 @@ +module LineCableModelsWGLMakieExt + +using LineCableModels +using WGLMakie + +activate!() = (WGLMakie.activate!(); :wgl) +make_screen(::AbstractString; kwargs...) = nothing + +function __init__() + activate!() + return nothing +end + +end # module LineCableModelsWGLMakieExt diff --git a/ext/fem/getdp_frontend/GetDPFrontend.jl b/ext/fem/getdp_frontend/GetDPFrontend.jl new file mode 100644 index 00000000..9f9b6f4d --- /dev/null +++ b/ext/fem/getdp_frontend/GetDPFrontend.jl @@ -0,0 +1,68 @@ +module GetDPFrontend + +const GetDP = @__MODULE__ + +# Include helper functions +include("helpers.jl") + +# Export helper functions +export add_raw_code, comment, make_args, get_getdp_executable + +# Include base classes +include("getdp_object.jl") + +# Export base classes +export AbstractGetDPObject, GetDPObject, SimpleItem, Base_, ObjectItem, CaseItem_, Case_ +export code, add_raw_code!, add_comment!, add! + +# Include main components +include("group.jl") +include("function.jl") +include("constraint.jl") +include("functionspace.jl") +include("jacobian.jl") +include("integration.jl") +include("formulation.jl") +include("resolution.jl") +include("postprocessing.jl") +include("postoperation.jl") +include("problem_definition.jl") + +# Export public API +export Constraint, Formulation, Function, FunctionSpace, Group, Integration +export Jacobian, PostOperation, PostProcessing, Problem, Resolution +export define!, add!, add_list!, add_file!, add_analytic!, add_piecewise!, add_akima! +export add_quantity!, add_equation!, content, add_subspace! +export Region, Global, NodesOf, EdgesOf, FacetsOf, VolumesOf, ElementsOf +export GroupsOfNodesOf, GroupsOfEdgesOf, GroupsOfEdgesOnNodesOf, GroupOfRegionsOf +export EdgesOfTreeIn, FacetsOfTreeIn, DualNodesOf, DualEdgesOf, DualFacetsOf, DualVolumesOf +export get_code, make_problem!, write_file, include!, SystemItem#, write_multiple_problems + +export add_constraint!, add_global_quantity!, error, add_operation!, add_basis_function! +export add_case!, VolSphShell, add_nested_case!, add_space!, add_constant!, assign!, case! +export for_loop! + +# Math Functions +export Exp, Log, Log10, Sqrt +export Sin, Asin, Cos, Acos, Tan, Atan, Atan2 +export Sinh, Cosh, Tanh, TanhC2 +export Fabs, Abs, Floor, Ceil, Fmod, Min, Max, Sign +export Jn, dJn, Yn, dYn +export Cross, Hypot, Norm, SquNorm, Unit, Transpose, Inv, Det, Rotate, TTrace +export Cos_wt_p, Sin_wt_p +export Printf, Rand +export Normal, NormalSource, Tangent, TangentSource +export ElementVol, SurfaceArea, GetVolume, CompElementNum, GetNumElements, ElementNum +export QuadraturePointIndex, AtIndex +export InterpolationLinear, dInterpolationLinear +export InterpolationBilinear, dInterpolationBilinear +export InterpolationAkima, dInterpolationAkima +export Order, Field +export ScalarField, VectorField, TensorField +export ComplexScalarField, ComplexVectorField, ComplexTensorField +export GetCpuTime, GetWallClockTime, GetMemory +export SetNumberRunTime, GetNumberRunTime +export SetVariable, GetVariable +export ValueFromIndex, VectorFromIndex, ValueFromTable + +end # module diff --git a/ext/fem/getdp_frontend/LICENSE b/ext/fem/getdp_frontend/LICENSE new file mode 100644 index 00000000..8bc1e743 --- /dev/null +++ b/ext/fem/getdp_frontend/LICENSE @@ -0,0 +1,30 @@ +BSD 3-Clause License +Copyright (c) 2025, Amauri Martins +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +“AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/ext/fem/getdp_frontend/NOTICE.md b/ext/fem/getdp_frontend/NOTICE.md new file mode 100644 index 00000000..60a6c2e8 --- /dev/null +++ b/ext/fem/getdp_frontend/NOTICE.md @@ -0,0 +1,11 @@ +# GetDP frontend compatibility snapshot + +This directory contains a private compatibility snapshot of the former +`Electa-Git/GetDP.jl` frontend. It is included only by the optional FEM +extension and is not part of the public LineCableModels API. + +- Source commit: `b1d91b0d8974ea642b772462edcf6e26299fdf0a` +- Git tree: `2a2fe2782b1c17a235a63d3639b99ae012dab1f5` +- License: BSD 3-Clause; see `LICENSE` in this directory. +- Modifications: module renamed to `GetDPFrontend`; package artifact loading + and bundled binary installation removed. diff --git a/ext/fem/getdp_frontend/constraint.jl b/ext/fem/getdp_frontend/constraint.jl new file mode 100644 index 00000000..62f1b59a --- /dev/null +++ b/ext/fem/getdp_frontend/constraint.jl @@ -0,0 +1,194 @@ +# Constraint: defining constraints + +using .GetDPFrontend: add_raw_code, comment, make_args + +""" + ConstraintItem + +Represents an individual constraint with cases and optional parameters. +""" +mutable struct ConstraintItem + name::String + type::String + cases::Vector{Dict{String, Any}} + comment::Union{String, Nothing} + for_loop::Union{Nothing, Tuple{String, String}} # (index, range) for For loops + + function ConstraintItem(name; type="Assign", comment=nothing) + new(name, type, Dict{String, Any}[], comment, nothing) + end +end + +""" + Constraint + +Defining constraints. +""" +mutable struct Constraint <: AbstractGetDPObject + name::String + constraints::Vector{ConstraintItem} + comment::Union{String, Nothing} + indent::String + + function Constraint() + new("Constraint", ConstraintItem[], nothing, " "^4) + end +end + +""" + add!(constraint::Constraint, name; type="Assign", comment=nothing) + +Add a constraint to the Constraint object. +Returns the ConstraintItem for adding cases. +""" +function add!(constraint::Constraint, name; type="Assign", comment=nothing) + item = ConstraintItem(name, type=type, comment=comment) + push!(constraint.constraints, item) + return item +end + +""" + assign!(constraint::Constraint, name; comment=nothing) + +Add an Assign-type constraint to the Constraint object (shortcut for add!). +Returns the ConstraintItem for adding cases. +""" +function assign!(constraint::Constraint, name; comment=nothing) + add!(constraint, name, type="Assign", comment=comment) +end + +""" + case!(item::ConstraintItem, region; value=nothing, time_function=nothing, comment=nothing) + +Add a case to a ConstraintItem with specified region and parameters. +""" +function case!(item::ConstraintItem, region; value=nothing, time_function=nothing, comment=nothing) + case_dict = Dict{String, Any}("Region" => region) + if value !== nothing + case_dict["Value"] = value + end + if time_function !== nothing + case_dict["TimeFunction"] = time_function + end + if comment !== nothing + case_dict["Comment"] = comment + end + push!(item.cases, case_dict) +end + +""" + for_loop!(item::ConstraintItem, index, range) + +Set a For loop for the ConstraintItem (e.g., For k In {1:3}). +""" +function for_loop!(item::ConstraintItem, index, range) + item.for_loop = (index, range) + push!(item.cases, Dict("LoopStart" => true)) # Add marker for loop start + return item +end + +""" + add_raw_code!(constraint::Constraint, raw_code, newline=true) + +Add raw code to the Constraint object. +""" +function add_raw_code!(constraint::Constraint, raw_code, newline=true) + # Store raw code as a special ConstraintItem with no cases + item = ConstraintItem("RawCode") + item.cases = [Dict("Raw" => raw_code)] + push!(constraint.constraints, item) +end + +""" + add_comment!(constraint::Constraint, comment_text, newline=true) + +Add a comment to the Constraint object. +""" +function add_comment!(constraint::Constraint, comment_text, newline=true) + add_raw_code!(constraint, comment(comment_text, newline=false), newline) +end + +""" + code(constraint::Constraint) + +Generate GetDP code for a Constraint object. +""" +function code(constraint::Constraint) + code_lines = String[] + push!(code_lines, "\nConstraint{") + + for item in constraint.constraints + # Handle raw code items + if item.name == "RawCode" && length(item.cases) == 1 && haskey(item.cases[1], "Raw") + push!(code_lines, " " * item.cases[1]["Raw"]) + continue + end + + # Add item comment if present + if item.comment !== nothing + push!(code_lines, " " * comment(item.comment, newline=false)) + end + + # Start constraint block + push!(code_lines, " { Name $(item.name); Type $(item.type);") + push!(code_lines, " Case {") + + # Split cases into pre-loop and loop cases + loop_start_idx = findfirst(c -> haskey(c, "LoopStart"), item.cases) + pre_loop_cases = loop_start_idx === nothing ? item.cases : item.cases[1:loop_start_idx-1] + loop_cases = loop_start_idx === nothing ? Dict{String, Any}[] : item.cases[loop_start_idx+1:end] + + # Pre-loop cases + for case in pre_loop_cases + if haskey(case, "Comment") && case["Comment"] !== nothing + push!(code_lines, " " * comment(case["Comment"], newline=false)) + end + if !haskey(case, "Region") || case["Region"] === "" + continue + end + line = " { Region $(case["Region"])" + if haskey(case, "Value") + line *= "; Value $(case["Value"])" + end + if haskey(case, "TimeFunction") + line *= "; TimeFunction $(case["TimeFunction"])" + end + line *= "; }" + push!(code_lines, line) + end + + # Loop cases + if item.for_loop !== nothing + index, range = item.for_loop + push!(code_lines, " For $(index) In {$(range)}") + for case in loop_cases + if haskey(case, "Comment") && case["Comment"] !== nothing + push!(code_lines, " " * comment(case["Comment"], newline=false)) + end + if !haskey(case, "Region") || case["Region"] === "" + continue + end + line = " { Region $(case["Region"])" + if haskey(case, "Value") + line *= "; Value $(case["Value"])" + end + if haskey(case, "TimeFunction") + line *= "; TimeFunction $(case["TimeFunction"])" + end + line *= "; }" + push!(code_lines, line) + end + push!(code_lines, " EndFor") + end + + push!(code_lines, " }") + push!(code_lines, " }") + end + + push!(code_lines, "}") + if constraint.comment !== nothing + return comment(constraint.comment) * "\n" * join(code_lines, "\n") + else + return join(code_lines, "\n") + end +end diff --git a/ext/fem/getdp_frontend/formulation.jl b/ext/fem/getdp_frontend/formulation.jl new file mode 100644 index 00000000..4d3035a2 --- /dev/null +++ b/ext/fem/getdp_frontend/formulation.jl @@ -0,0 +1,270 @@ +# Formulation: building equations +using .GetDPFrontend: add_raw_code, comment, make_args + +""" + Quantity + +A quantity in a formulation. +""" +mutable struct Quantity + name::String + Type::String + NameOfSpace::String + comment::Union{String, Nothing} + + function Quantity(name::String; Type, NameOfSpace, comment = nothing) + new(name, Type, NameOfSpace, comment) + end +end + +function code(q::Quantity) + code_str = "{ Name $(q.name); Type $(q.Type); NameOfSpace $(q.NameOfSpace); }" + if q.comment !== nothing + code_str = comment(q.comment, newline = false) * "\n " * code_str + end + code_str +end + +""" + EquationTerm + +A term in an equation. +""" +mutable struct EquationTerm + term_type::String + term::String + options::Dict + comment::Union{String, Nothing} + + function EquationTerm(term_type, term; comment = nothing, kwargs...) + options = Dict(kwargs) + new(term_type, term, options, comment) + end +end + +function code(term::EquationTerm) + code_lines = String[] + if term.comment !== nothing + push!(code_lines, comment(term.comment, newline = false)) + end + if term.term_type == "Galerkin" + push!(code_lines, "Galerkin { $(term.term);") + # Order: In, Jacobian, Integration + for key in [:In, :Jacobian, :Integration] + if haskey(term.options, key) + push!(code_lines, " $key $(make_args(term.options[key], sep=","));") + end + end + # Include any other options + for (k, v) in term.options + if k ∉ [:In, :Jacobian, :Integration] + push!(code_lines, " $k $(make_args(v, sep=","));") + end + end + push!(code_lines, "}") + elseif term.term_type == "GlobalTerm" + push!(code_lines, "GlobalTerm { $(term.term) ;") + for (k, v) in term.options + push!(code_lines, " $k $(make_args(v, sep=","));") + end + push!(code_lines, "}") + else + Base.error("Unsupported term type: $(term.term_type)") + end + join(code_lines, "\n") +end + +""" + Equation + +An equation in a formulation. +""" +mutable struct Equation + items::Vector{EquationTerm} + + function Equation(; comment = nothing, kwargs...) + new(EquationTerm[]) + end +end + +function code(equation::Equation) + code_lines = ["Equation {"] + for term in equation.items + term_code = code(term) + for line in split(term_code, '\n') + push!(code_lines, " $line") + end + end + push!(code_lines, "}") + join(code_lines, "\n") +end + +""" + add!(equation::Equation, term_type::String, term::String; kwargs...) + +Add an equation term to an equation. +""" +function add!(equation::Equation, term_type::String, term::String; kwargs...) + item = EquationTerm(term_type, term; kwargs...) + push!(equation.items, item) + item +end + +""" + FormulationItem + +An item in a formulation. +""" +mutable struct FormulationItem + Name::String + Type::String + comment::Union{String, Nothing} + items::Vector{Any} # Will be Vector{Union{Quantity,Equation}} + + function FormulationItem(Name, Type; comment = nothing, kwargs...) + new(Name, Type, comment, []) + end +end + +function code(item::FormulationItem) + code_lines = ["{ Name $(item.Name); Type $(item.Type);"] + if item.comment !== nothing + push!(code_lines, " $(comment(item.comment, newline=false))") + end + + # Quantities + quantities = filter(x -> isa(x, Quantity), item.items) + if !isempty(quantities) + push!(code_lines, " Quantity {") + for q in quantities + push!(code_lines, " $(code(q))") + end + push!(code_lines, " }") + end + + # Equation + equations = filter(x -> isa(x, Equation), item.items) + if !isempty(equations) + equation = equations[1] # Assuming only one equation + equation_code = code(equation) + for line in split(equation_code, '\n') + push!(code_lines, " $line") + end + end + + push!(code_lines, "}") + join(code_lines, "\n") +end + +""" + add_quantity!(item::FormulationItem, name::String; kwargs...) + +Add a quantity to a formulation item. +""" +function add_quantity!( + item::FormulationItem, + name::String; + Type, + NameOfSpace, + comment = nothing, +) + case = Quantity(name; Type = Type, NameOfSpace = NameOfSpace, comment = comment) + push!(item.items, case) + case +end + +""" + add_equation!(item::FormulationItem, args...; kwargs...) + +Add an equation to a formulation item. +""" +function add_equation!(item::FormulationItem, args...; kwargs...) + case = Equation(args...; kwargs...) + push!(item.items, case) + case +end + +""" + Formulation + +Building equations. +""" +mutable struct Formulation <: AbstractGetDPObject + name::String + content::String + comment::Union{String, Nothing} + indent::String + items::Vector{FormulationItem} + + function Formulation() + new("Formulation", "", nothing, " "^4, FormulationItem[]) + end +end + +""" + content(formulation::Formulation) + +Get the content of a formulation. +""" +function content(formulation::Formulation) + code_lines = String[] + for item in formulation.items + push!(code_lines, code(item)) + end + join(code_lines, "\n") +end + +""" + add!(formulation::Formulation, Name, Type; kwargs...) + +Add a formulation item to a formulation. +""" +function add!(formulation::Formulation, Name, Type; kwargs...) + o = FormulationItem(Name, Type; kwargs...) + push!(formulation.items, o) + o +end + +""" + add_raw_code!(formulation::Formulation, raw_code, newline=true) + +Add raw code to the Formulation object. +""" +function add_raw_code!(formulation::Formulation, raw_code, newline = true) + formulation.content = add_raw_code(formulation.content, raw_code, newline) +end + +""" + add_comment!(formulation::Formulation, comment_text, newline=true) + +Add a comment to the Formulation object. +""" +function add_comment!(formulation::Formulation, comment_text, newline = true) + add_raw_code!(formulation, comment(comment_text, newline = false), newline) +end + +""" + code(formulation::Formulation) + +Generate GetDP code for a Formulation object. +""" +function code(formulation::Formulation) + code_lines = ["\nFormulation {"] + formulation_content = content(formulation) + if !isempty(formulation_content) + for line in split(formulation_content, '\n') + if !isempty(line) + push!(code_lines, " $line") + end + end + end + if !isempty(formulation.content) + for line in split(formulation.content, '\n') + if !isempty(line) + push!(code_lines, " $line") + end + end + end + push!(code_lines, "}") + join(code_lines, "\n") * "\n" +end diff --git a/ext/fem/getdp_frontend/function.jl b/ext/fem/getdp_frontend/function.jl new file mode 100644 index 00000000..743d8fad --- /dev/null +++ b/ext/fem/getdp_frontend/function.jl @@ -0,0 +1,1460 @@ +# Function: defining functions + +using .GetDPFrontend: add_raw_code, comment, make_args + +""" + Function + +Defining functions. +""" +mutable struct Function <: AbstractGetDPObject + name::String + content::String + comment::Union{String, Nothing} + indent::String + + function Function() + new("Function", "", nothing, " "^4) + end +end + +""" + add!(func::Function, id, expression; comment=nothing) + +Add a simple function to the Function object. +""" +function add!(func::Function, id, expression; comment = nothing) + c = "$(id)() = $(expression);" + func.content *= c + + if comment !== nothing + add_comment!(func, comment; newline = false) + end + + func.content *= "\n" + return id +end + +""" + add!(func::Function, id; expression, arguments=String[], region=String[], comment=nothing) + +Add a function to the Function object with optional region and arguments. +""" +function add!( + func::Function, + id; + expression, + arguments = String[], + region = String[], + comment = nothing, +) + # Format the identifier + if !isempty(region) + region_str = make_args(region) + id_str = "$(id)[Region[$(region_str)]]" + else + id_str = "$(id)[]" + end + + # Format the expression with arguments (if any) + expr = expression + if !isempty(arguments) + # Ensure expression contains placeholders for arguments + for arg in arguments + if !occursin(arg, expr) + Base.error("Argument $arg not found in expression: $expr") + end + end + end + + c = "$(id_str) = $(expr);" + func.content *= c + + if comment !== nothing + add_comment!(func, comment; newline = false) + end + + func.content *= "\n" + return id +end + +""" + add_list!(func::Function, id, expression_list; comment=nothing) + +Add a list of functions to the Function object. +""" +function add_list!(func::Function, id, expression_list; comment = nothing) + c = "$(id)[] = {" + + for (i, expr) in enumerate(expression_list) + if i > 1 + c *= ", " + end + c *= "$(expr)" + end + + c *= "};" + func.content *= c + + if comment !== nothing + add_comment!(func, comment; newline = false) + end + + func.content *= "\n" + return id +end + +""" + add_file!(func::Function, id, filename; comment=nothing) + +Add a function from a file to the Function object. +""" +function add_file!(func::Function, id, filename; comment = nothing) + c = "$(id)[] = Analytic[File[\"$(filename)\"]];" + func.content *= c + + if comment !== nothing + add_comment!(func, comment; newline = false) + end + + func.content *= "\n" + return id +end +""" + add_constant!(func::Function, variable, value; comment=nothing) + +Add a global constant to the Function object. +""" +function add_constant!(func::Function, variable, value; comment = nothing) + c = "$(variable) = $(value);" + func.content *= c + + if comment !== nothing + add_comment!(func, comment; newline = false) + end + + func.content *= "\n" +end + +""" + add_analytic!(func::Function, id, expression; comment=nothing) + +Add an analytic function to the Function object. +""" +function add_analytic!(func::Function, id, expression; comment = nothing) + c = "$(id)[] = Analytic[$(expression)];" + func.content *= c + + if comment !== nothing + add_comment!(func, comment; newline = false) + end + + func.content *= "\n" + return id +end + +""" + add_piecewise!(func::Function, id, x, y; comment=nothing) + +Add a piecewise function to the Function object. +""" +function add_piecewise!(func::Function, id, x, y; comment = nothing) + if length(x) != length(y) + Base.error("x and y must have the same length") + end + + c = "$(id)[] = InterpolationLinear[$x, $y];" + func.content *= c + + if comment !== nothing + add_comment!(func, comment; newline = false) + end + + func.content *= "\n" + return id +end + +""" + add_akima!(func::Function, id, x, y; comment=nothing) + +Add an Akima interpolation function to the Function object. +""" +function add_akima!(func::Function, id, x, y; comment = nothing) + if length(x) != length(y) + Base.error("x and y must have the same length") + end + + c = "$(id)[] = InterpolationAkima[$x, $y];" + func.content *= c + + if comment !== nothing + add_comment!(func, comment; newline = false) + end + + func.content *= "\n" + return id +end + +""" + add_raw_code!(func::Function, raw_code, newline=true) + +Add raw code to the Function object. +""" +function add_raw_code!(func::Function, raw_code, newline = true) + func.content = add_raw_code(func.content, raw_code, newline) +end + +""" + add_comment!(func::Function, comment_text, newline=true) + +Add a comment to the Function object. +""" +function add_comment!(func::Function, comment_text, newline = true) + add_raw_code!(func, comment(comment_text, newline = false), newline) +end +""" + add_space!(function::Function, num_spaces::Int=1) + +Add a specified number of empty lines to the function object's content for spacing in the output. +""" +function add_space!(func::Function, num_spaces::Int = 1) + func.content *= "\n"^num_spaces + return nothing +end +""" + code(func::Function) + +Generate GetDP code for a Function object. +""" +function code(func::Function) + code_lines = String[] + push!(code_lines, "\nFunction{") + + # Add the content directly since it's already formatted + if !isempty(func.content) + for line in split(func.content, '\n') + if !isempty(line) + push!(code_lines, " " * line) + else + push!(code_lines, "") + end + end + end + + push!(code_lines, "}") + + # Add comment if present + if func.comment !== nothing + return comment(func.comment) * "\n" * join(code_lines, "\n") + else + return join(code_lines, "\n") + end +end + +""" + Exp(expression) + +Generate the GetDP string representation for the exponential function `Exp[...]`. +Example: `Exp("x")` returns `"Exp[x]"`. +""" +function Exp(expression) + return "Exp[$(expression)]" +end + +""" + Log(expression) + +Generate the GetDP string representation for the natural logarithm function `Log[...]`. +Example: `Log("x")` returns `"Log[x]"`. +""" +function Log(expression) + return "Log[$(expression)]" +end + +""" + Log10(expression) + +Generate the GetDP string representation for the base-10 logarithm function `Log10[...]`. +Example: `Log10("x")` returns `"Log10[x]"`. +""" +function Log10(expression) + return "Log10[$(expression)]" +end + +""" + Sqrt(expression) + +Generate the GetDP string representation for the square root function `Sqrt[...]`. +Example: `Sqrt("x")` returns `"Sqrt[x]"`. +""" +function Sqrt(expression) + return "Sqrt[$(expression)]" +end +""" + Sin(expression) + +Generate the GetDP string representation for the sine function `Sin[...]`. +""" +function Sin(expression) + return "Sin[$(expression)]" +end + +""" + Asin(expression) + +Generate the GetDP string representation for the arc sine function `Asin[...]`. +Domain: expression in [-1, 1]. Range: [-Pi/2, Pi/2]. (Real valued only). +""" +function Asin(expression) + return "Asin[$(expression)]" +end + +""" + Cos(expression) + +Generate the GetDP string representation for the cosine function `Cos[...]`. +""" +function Cos(expression) + return "Cos[$(expression)]" +end + +""" + Acos(expression) + +Generate the GetDP string representation for the arc cosine function `Acos[...]`. +Domain: expression in [-1, 1]. Range: [0, Pi]. (Real valued only). +""" +function Acos(expression) + return "Acos[$(expression)]" +end + +""" + Tan(expression) + +Generate the GetDP string representation for the tangent function `Tan[...]`. +""" +function Tan(expression) + return "Tan[$(expression)]" +end + +""" + Atan(expression) + +Generate the GetDP string representation for the arc tangent function `Atan[...]`. +Range: [-Pi/2, Pi/2]. (Real valued only). +""" +function Atan(expression) + return "Atan[$(expression)]" +end + +""" + Atan2(y, x) + +Generate the GetDP string representation for the arc tangent function `Atan2[y, x]`. +Computes atan(y/x). Range: [-Pi, Pi]. (Real valued only). +""" +function Atan2(y, x) + return "Atan2[$(y),$(x)]" +end + +# Hyperbolic Functions +""" + Sinh(expression) + +Generate the GetDP string representation for the hyperbolic sine function `Sinh[...]`. +""" +function Sinh(expression) + return "Sinh[$(expression)]" +end + +""" + Cosh(expression) + +Generate the GetDP string representation for the hyperbolic cosine function `Cosh[...]`. +""" +function Cosh(expression) + return "Cosh[$(expression)]" +end + +""" + Tanh(expression) + +Generate the GetDP string representation for the hyperbolic tangent function `Tanh[...]`. +(Real valued only). +""" +function Tanh(expression) + return "Tanh[$(expression)]" +end + +""" + TanhC2(expression) + +Generate the GetDP string representation for the complex hyperbolic tangent function `TanhC2[...]`. +""" +function TanhC2(expression) + return "TanhC2[$(expression)]" +end + +# Basic Math/Rounding Functions +""" + Fabs(expression) + +Generate the GetDP string representation for the absolute value function `Fabs[...]`. +(Real valued only). +""" +function Fabs(expression) + return "Fabs[$(expression)]" +end + +""" + Abs(expression) + +Generate the GetDP string representation for the absolute value/modulus function `Abs[...]`. +(Works for complex numbers). +""" +function Abs(expression) + return "Abs[$(expression)]" +end + +""" + Floor(expression) + +Generate the GetDP string representation for the floor function `Floor[...]`. +Rounds downwards. (Real valued only). +""" +function Floor(expression) + return "Floor[$(expression)]" +end + +""" + Ceil(expression) + +Generate the GetDP string representation for the ceiling function `Ceil[...]`. +Rounds upwards. (Real valued only). +""" +function Ceil(expression) + return "Ceil[$(expression)]" +end + +""" + Fmod(x, y) + +Generate the GetDP string representation for the floating-point remainder function `Fmod[x, y]`. +Remainder of x/y with sign of x. (Real valued only). +""" +function Fmod(x, y) + return "Fmod[$(x),$(y)]" +end + +""" + Min(a, b) + +Generate the GetDP string representation for the minimum function `Min[a, b]`. +(Scalar, real valued only). +""" +function Min(a, b) + return "Min[$(a),$(b)]" +end + +""" + Max(a, b) + +Generate the GetDP string representation for the maximum function `Max[a, b]`. +(Scalar, real valued only). +""" +function Max(a, b) + return "Max[$(a),$(b)]" +end + +""" + Sign(expression) + +Generate the GetDP string representation for the sign function `Sign[...]`. +Returns -1 for expression < 0, 1 otherwise. (Real valued only). +""" +function Sign(expression) + return "Sign[$(expression)]" +end + +# Bessel Functions (assuming GetDP syntax Jn[order, value]) +""" + Jn(order, value) + +Generate the GetDP string representation for the Bessel function of the first kind `Jn[order, value]`. +(Real valued only). +""" +function Jn(order, value) + return "Jn[$(order),$(value)]" +end + +""" + dJn(order, value) + +Generate the GetDP string representation for the derivative of the Bessel function of the first kind `dJn[order, value]`. +(Real valued only). +""" +function dJn(order, value) + return "dJn[$(order),$(value)]" +end + +""" + Yn(order, value) + +Generate the GetDP string representation for the Bessel function of the second kind `Yn[order, value]`. +(Real valued only). +""" +function Yn(order, value) + return "Yn[$(order),$(value)]" +end + +""" + dYn(order, value) + +Generate the GetDP string representation for the derivative of the Bessel function of the second kind `dYn[order, value]`. +(Real valued only). +""" +function dYn(order, value) + return "dYn[$(order),$(value)]" +end + + +# Vector/Tensor Functions +""" + Cross(vector1, vector2) + +Generate the GetDP string representation for the cross product `Cross[vector1, vector2]`. +Arguments must be vectors. +""" +function Cross(vector1, vector2) + return "Cross[$(vector1),$(vector2)]" +end + +""" + Hypot(a, b) + +Generate the GetDP string representation for the hypotenuse function `Hypot[a, b]`. +Computes Sqrt(a^2 + b^2). +""" +function Hypot(a, b) + return "Hypot[$(a),$(b)]" +end + +""" + Norm(expression) + +Generate the GetDP string representation for the norm function `Norm[...]`. +Absolute value for scalar, Euclidean norm for vector. +""" +function Norm(expression) + return "Norm[$(expression)]" +end + +""" + SquNorm(expression) + +Generate the GetDP string representation for the square norm function `SquNorm[...]`. +Equivalent to Norm[expression]^2. +""" +function SquNorm(expression) + return "SquNorm[$(expression)]" +end + +""" + Unit(expression) + +Generate the GetDP string representation for the unit vector function `Unit[...]`. +Computes expression / Norm[expression]. Returns 0 if norm is near zero. +""" +function Unit(expression) + return "Unit[$(expression)]" +end + +""" + Transpose(expression) + +Generate the GetDP string representation for the transpose function `Transpose[...]`. +Expression must be a tensor. +""" +function Transpose(expression) + return "Transpose[$(expression)]" +end + +""" + Inv(expression) + +Generate the GetDP string representation for the inverse tensor function `Inv[...]`. +Expression must be a tensor. +""" +function Inv(expression) + return "Inv[$(expression)]" +end + +""" + Det(expression) + +Generate the GetDP string representation for the tensor determinant function `Det[...]`. +Expression must be a tensor. +""" +function Det(expression) + return "Det[$(expression)]" +end + +""" + Rotate(object, rot_x, rot_y, rot_z) + +Generate the GetDP string representation for the rotation function `Rotate[object, rx, ry, rz]`. +Rotates a vector or tensor `object` by angles `rot_x`, `rot_y`, `rot_z` (radians) around axes x, y, z. +""" +function Rotate(object_to_rotate, rot_x, rot_y, rot_z) + return "Rotate[$(object_to_rotate),$(rot_x),$(rot_y),$(rot_z)]" +end + +""" + TTrace(tensor) + +Generate the GetDP string representation for the tensor trace function `TTrace[...]`. +Expression must be a tensor. +""" +function TTrace(tensor) + return "TTrace[$(tensor)]" +end + +# Special/Time Functions +""" + Cos_wt_p(omega, phase) + +Generate the GetDP string representation for the time function `Cos_wt_p[]{omega, phase}`. +Real: Cos[omega*Time + phase]. Complex: Complex[Cos[phase], Sin[phase]]. +""" +function Cos_wt_p(omega, phase) + # Note the empty [] and double {{}} + return "Cos_wt_p[]{{$(omega),$(phase)}}" +end + +""" + Sin_wt_p(omega, phase) + +Generate the GetDP string representation for the time function `Sin_wt_p[]{omega, phase}`. +Real: Sin[omega*Time + phase]. Complex: Complex[Sin[phase], -Cos[phase]]. +""" +function Sin_wt_p(omega, phase) + # Note the empty [] and double {{}} + return "Sin_wt_p[]{{$(omega),$(phase)}}" +end + +""" + Period(expression, period_const) + +Generate the GetDP string representation for the periodic function `Period[expr]{period_const}`. +Result is always in [0, period_const[. +""" +function _period_expression(expression, period_const) + # Note the {} after [] + return "Period[$(expression)]{$(period_const)}" +end +# Green Functions +""" + Laplace(dim) + +Generate the GetDP string representation for the Laplace Green function `Laplace[]{dim}`. +Result depends on `dim`: r/2 (1D), (1/(2*Pi))*ln(1/r) (2D), 1/(4*Pi*r) (3D). +""" +function Laplace(dim) + return "Laplace[]{{{$(dim)}}}" # Note: empty [], triple {{{}}} +end + +""" + GradLaplace(dim) + +Generate the GetDP string representation for the gradient of the Laplace Green function `GradLaplace[]{dim}`. +Gradient is relative to the destination point (X, Y, Z). +""" +function GradLaplace(dim) + return "GradLaplace[]{{{$(dim)}}}" # Note: empty [], triple {{{}}} +end + +""" + Helmholtz(dim, k0) + +Generate the GetDP string representation for the Helmholtz Green function `Helmholtz[]{dim, k0}`. +Typically exp(j*k0*r)/(4*Pi*r) (3D). `k0` is the wave number. +""" +function Helmholtz(dim, k0) + return "Helmholtz[]{{{$(dim),$(k0)}}}" # Note: empty [], triple {{{}}} +end + +""" + GradHelmholtz(dim, k0) + +Generate the GetDP string representation for the gradient of the Helmholtz Green function `GradHelmholtz[]{dim, k0}`. +Gradient is relative to the destination point (X, Y, Z). +""" +function GradHelmholtz(dim, k0) + return "GradHelmholtz[]{{{$(dim),$(k0)}}}" # Note: empty [], triple {{{}}} +end + +# Type Manipulation Functions +""" + Complex(re1, im1, re2, im2, ...) + +Generate the GetDP string representation for creating a complex value `Complex[re1, im1, ...]`. +Takes an even number of real-valued expressions. +""" +function _complex_expression(args...) # Varargs + arg_string = join(args, ',') + return "Complex[$(arg_string)]" +end + +""" + Re(complex_expr) + +Generate the GetDP string representation for the real part function `Re[...]`. +""" +function Re(complex_expr) + return "Re[$(complex_expr)]" +end + +""" + Im(complex_expr) + +Generate the GetDP string representation for the imaginary part function `Im[...]`. +""" +function Im(complex_expr) + return "Im[$(complex_expr)]" +end + +""" + Conj(complex_expr) + +Generate the GetDP string representation for the complex conjugate function `Conj[...]`. +""" +function Conj(complex_expr) + return "Conj[$(complex_expr)]" +end + +""" + Cart2Pol(complex_expr) + +Generate the GetDP string representation for Cartesian to Polar conversion `Cart2Pol[...]`. +Input: Complex[real, imag]. Output: Complex[amplitude, phase]. +""" +function Cart2Pol(complex_expr) + return "Cart2Pol[$(complex_expr)]" +end + + +# Vector/Tensor Creation +""" + Vector(s0, s1, s2) + +Generate the GetDP string representation for creating a vector `Vector[s0, s1, s2]`. +""" +function _vector_expression(s0, s1, s2) + return "Vector[$(s0),$(s1),$(s2)]" +end + +""" + Tensor(s00, s01, s02, s10, s11, s12, s20, s21, s22) + +Generate the GetDP string representation for creating a tensor from 9 scalars (row-major) `Tensor[...]`. +""" +function Tensor(s00, s01, s02, s10, s11, s12, s20, s21, s22) + return "Tensor[$(s00),$(s01),$(s02),$(s10),$(s11),$(s12),$(s20),$(s21),$(s22)]" +end + +""" + TensorV(v0, v1, v2) + +Generate the GetDP string representation for creating a tensor from 3 row vectors `TensorV[...]`. +""" +function TensorV(v0, v1, v2) + return "TensorV[$(v0),$(v1),$(v2)]" +end + +""" + TensorSym(s00, s01, s02, s11, s12, s22) + +Generate the GetDP string representation for creating a symmetric tensor from 6 scalars `TensorSym[...]`. +T = [s00 s01 s02; s01 s11 s12; s02 s12 s22] +""" +function TensorSym(s00, s01, s02, s11, s12, s22) + return "TensorSym[$(s00),$(s01),$(s02),$(s11),$(s12),$(s22)]" +end + +""" + TensorDiag(s00, s11, s22) + +Generate the GetDP string representation for creating a diagonal tensor from 3 scalars `TensorDiag[...]`. +""" +function TensorDiag(s00, s11, s22) + return "TensorDiag[$(s00),$(s11),$(s22)]" +end + +""" + SquDyadicProduct(vector) + +Generate the GetDP string representation for the square dyadic product `SquDyadicProduct[...]`. +Computes vector * Transpose[vector]. +""" +function SquDyadicProduct(vector) + return "SquDyadicProduct[$(vector)]" +end + +# Component Extraction +""" + CompX(vector) + +Generate the GetDP string representation for getting the X component `CompX[...]`. +""" +function CompX(vector) + return "CompX[$(vector)]" +end + +""" + CompY(vector) + +Generate the GetDP string representation for getting the Y component `CompY[...]`. +""" +function CompY(vector) + return "CompY[$(vector)]" +end + +""" + CompZ(vector) + +Generate the GetDP string representation for getting the Z component `CompZ[...]`. +""" +function CompZ(vector) + return "CompZ[$(vector)]" +end + +""" + CompXX(tensor) + +Generate the GetDP string representation for getting the XX component `CompXX[...]`. +""" +function CompXX(tensor) + return "CompXX[$(tensor)]" +end + +""" + CompYY(tensor) + +Generate the GetDP string representation for getting the YY component `CompYY[...]`. +""" +function CompYY(tensor) + return "CompYY[$(tensor)]" +end + +""" + CompZZ(tensor) + +Generate the GetDP string representation for getting the ZZ component `CompZZ[...]`. +""" +function CompZZ(tensor) + return "CompZZ[$(tensor)]" +end + +""" + CompXY(tensor) + +Generate the GetDP string representation for getting the XY component `CompXY[...]`. +""" +function CompXY(tensor) + return "CompXY[$(tensor)]" +end + +""" + CompYX(tensor) + +Generate the GetDP string representation for getting the YX component `CompYX[...]`. +""" +function CompYX(tensor) + return "CompYX[$(tensor)]" +end + +""" + CompXZ(tensor) + +Generate the GetDP string representation for getting the XZ component `CompXZ[...]`. +""" +function CompXZ(tensor) + return "CompXZ[$(tensor)]" +end + +""" + CompZX(tensor) + +Generate the GetDP string representation for getting the ZX component `CompZX[...]`. +""" +function CompZX(tensor) + return "CompZX[$(tensor)]" +end + +""" + CompYZ(tensor) + +Generate the GetDP string representation for getting the YZ component `CompYZ[...]`. +""" +function CompYZ(tensor) + return "CompYZ[$(tensor)]" +end + +""" + CompZY(tensor) + +Generate the GetDP string representation for getting the ZY component `CompZY[...]`. +""" +function CompZY(tensor) + return "CompZY[$(tensor)]" +end + + +# Coordinate Transformations +""" + Cart2Sph(vector) + +Generate the GetDP string representation for the Cartesian to Spherical transformation tensor `Cart2Sph[...]`. +""" +function Cart2Sph(vector) + return "Cart2Sph[$(vector)]" +end + +""" + Cart2Cyl(vector) + +Generate the GetDP string representation for the Cartesian to Cylindrical transformation tensor `Cart2Cyl[...]`. +""" +function Cart2Cyl(vector) + return "Cart2Cyl[$(vector)]" +end + +# Unit Vectors +""" + UnitVectorX() + +Generate the GetDP string representation for the unit vector in X: `UnitVectorX[]`. +""" +function UnitVectorX() + return "UnitVectorX[]" +end + +""" + UnitVectorY() + +Generate the GetDP string representation for the unit vector in Y: `UnitVectorY[]`. +""" +function UnitVectorY() + return "UnitVectorY[]" +end + +""" + UnitVectorZ() + +Generate the GetDP string representation for the unit vector in Z: `UnitVectorZ[]`. +""" +function UnitVectorZ() + return "UnitVectorZ[]" +end + + +# Coordinate Functions +""" + X() + +Generate the GetDP string representation for the X coordinate: `X[]`. +""" +function X() + return "X[]" +end + +""" + Y() + +Generate the GetDP string representation for the Y coordinate: `Y[]`. +""" +function Y() + return "Y[]" +end + +""" + Z() + +Generate the GetDP string representation for the Z coordinate: `Z[]`. +""" +function Z() + return "Z[]" +end + +""" + XYZ() + +Generate the GetDP string representation for the coordinate vector: `XYZ[]`. +""" +function XYZ() + return "XYZ[]" +end +# Miscellaneous Functions +""" + Printf(expression) + +Generate the GetDP string representation for printing a value during evaluation: `Printf[expression]`. +""" +function Printf(expression) + return "Printf[$(expression)]" +end + +""" + Rand(max_val) + +Generate the GetDP string representation for a pseudo-random number in [0, max_val]: `Rand[max_val]`. +""" +function Rand(max_val) + return "Rand[$(max_val)]" +end + +""" + Normal() + +Generate the GetDP string representation for the element's normal vector: `Normal[]`. +""" +function Normal() + return "Normal[]" +end + +""" + NormalSource() + +Generate the GetDP string representation for the source element's normal vector: `NormalSource[]`. +(Valid in Integral quantity). +""" +function NormalSource() + return "NormalSource[]" +end + +""" + Tangent() + +Generate the GetDP string representation for the element's tangent vector: `Tangent[]`. +(Valid for line elements). +""" +function Tangent() + return "Tangent[]" +end + +""" + TangentSource() + +Generate the GetDP string representation for the source element's tangent vector: `TangentSource[]`. +(Valid in Integral quantity, line elements). +""" +function TangentSource() + return "TangentSource[]" +end + +""" + ElementVol() + +Generate the GetDP string representation for the element's volume (or area/length): `ElementVol[]`. +""" +function ElementVol() + return "ElementVol[]" +end + +""" + SurfaceArea(list_expr::String="") + +Generate the GetDP string representation for surface area calculation: `SurfaceArea[]{list}`. +`list_expr` is a comma-separated string of physical surface tags, or empty for the current surface. +""" +function SurfaceArea(list_expr::String = "") + return "SurfaceArea[]{{{$(list_expr)}}}" # Note: empty [], triple {{{}}} +end + +""" + GetVolume() + +Generate the GetDP string representation for the volume of the current physical group: `GetVolume[]`. +""" +function GetVolume() + return "GetVolume[]" +end + +""" + CompElementNum() + +Generate the GetDP string representation to compare current and source element tags: `CompElementNum[]`. +Returns 0 if identical. +""" +function CompElementNum() + return "CompElementNum[]" +end + +""" + GetNumElements(list_expr::String="") + +Generate the GetDP string representation for counting elements: `GetNumElements[]{list}`. +`list_expr` is a comma-separated string of physical region tags, or empty for the current region. +""" +function GetNumElements(list_expr::String = "") + return "GetNumElements[]{{{$(list_expr)}}}" # Note: empty [], triple {{{}}} +end + +""" + ElementNum() + +Generate the GetDP string representation for the current element's tag: `ElementNum[]`. +""" +function ElementNum() + return "ElementNum[]" +end + +""" + QuadraturePointIndex() + +Generate the GetDP string representation for the current quadrature point index: `QuadraturePointIndex[]`. +""" +function QuadraturePointIndex() + return "QuadraturePointIndex[]" +end + +""" + AtIndex(index_expr, list_expr::String) + +Generate the GetDP string representation for accessing list element by index: `AtIndex[index]{list}`. +`list_expr` is a comma-separated string list. Index is 0-based(? check GetDP docs). +""" +function AtIndex(index_expr, list_expr::String) + return "AtIndex[$(index_expr)]{$(list_expr)}" # Note: []{} +end + +# Interpolation Functions +""" + InterpolationLinear(x_expr, table_list_expr::String) + +Generate the GetDP string representation for linear interpolation: `InterpolationLinear[x]{table}`. +`table_list_expr` is a comma-separated list of x,y pairs: "x1,y1,x2,y2,...". +""" +function InterpolationLinear(x_expr, table_list_expr::String) + return "InterpolationLinear[$(x_expr)]{$(table_list_expr)}" # Note: []{} +end + +""" + dInterpolationLinear(x_expr, table_list_expr::String) + +Generate the GetDP string representation for the derivative of linear interpolation: `dInterpolationLinear[x]{table}`. +""" +function dInterpolationLinear(x_expr, table_list_expr::String) + return "dInterpolationLinear[$(x_expr)]{$(table_list_expr)}" # Note: []{} +end + +""" + InterpolationBilinear(x_expr, y_expr, table_list_expr::String) + +Generate the GetDP string representation for bilinear interpolation: `InterpolationBilinear[x, y]{table}`. +Table format needs checking in GetDP docs. +""" +function InterpolationBilinear(x_expr, y_expr, table_list_expr::String) + return "InterpolationBilinear[$(x_expr), $(y_expr)]{$(table_list_expr)}" # Note: []{} +end + +""" + dInterpolationBilinear(x_expr, y_expr, table_list_expr::String) + +Generate the GetDP string representation for the derivative of bilinear interpolation: `dInterpolationBilinear[x, y]{table}`. +Result is a vector. +""" +function dInterpolationBilinear(x_expr, y_expr, table_list_expr::String) + return "dInterpolationBilinear[$(x_expr), $(y_expr)]{$(table_list_expr)}" # Note: []{} +end + +""" + InterpolationAkima(x_expr, table_list_expr::String) + +Generate the GetDP string representation for Akima interpolation: `InterpolationAkima[x]{table}`. +`table_list_expr` is a comma-separated list of x,y pairs: "x1,y1,x2,y2,...". +""" +function InterpolationAkima(x_expr, table_list_expr::String) + return "InterpolationAkima[$(x_expr)]{$(table_list_expr)}" # Note: []{} +end + +""" + dInterpolationAkima(x_expr, table_list_expr::String) + +Generate the GetDP string representation for the derivative of Akima interpolation: `dInterpolationAkima[x]{table}`. +""" +function dInterpolationAkima(x_expr, table_list_expr::String) + return "dInterpolationAkima[$(x_expr)]{$(table_list_expr)}" # Note: []{} +end + +""" + Order(quantity_name) + +Generate the GetDP string representation for getting interpolation order: `Order[quantity]`. +""" +function Order(quantity_name) + return "Order[$(quantity_name)]" +end + + +# Field Evaluation Functions +""" + Field(eval_point_expr) + +Generate the GetDP string representation for evaluating the last Gmsh field: `Field[eval_point]`. +Typically `eval_point_expr` is `XYZ[]`. +""" +function Field(eval_point_expr) + return "Field[$(eval_point_expr)]" +end + +""" + Field(eval_point_expr, tags_list_expr::String) + +Generate the GetDP string representation for evaluating and summing specific Gmsh fields: `Field[eval_point]{tags_list}`. +`tags_list_expr` is a comma-separated list of field tags. +""" +function Field(eval_point_expr, tags_list_expr::String) + return "Field[$(eval_point_expr)]{$(tags_list_expr)}" # Note: []{} +end + +# Common helper for typed field functions +function _TypedFieldHelper( + func_name, + expression, + expression_cst_list, + timestep, + elmt_interp, +) + interp_flag = Int(elmt_interp) # Convert Bool to 0 or 1 + return "$(func_name)[$(expression), $(timestep), $(interp_flag)]{$(expression_cst_list)}" +end + +""" + ScalarField(expression, expression_cst_list; timestep=0, elmt_interp=true) + +Generate GetDP string for evaluating scalar fields: `ScalarField[expr, ts, interp]{list}`. +""" +function ScalarField( + expression, + expression_cst_list::String; + timestep = 0, + elmt_interp::Bool = true, +) + return _TypedFieldHelper( + "ScalarField", + expression, + expression_cst_list, + timestep, + elmt_interp, + ) +end + +""" + VectorField(expression, expression_cst_list; timestep=0, elmt_interp=true) + +Generate GetDP string for evaluating vector fields: `VectorField[expr, ts, interp]{list}`. +""" +function VectorField( + expression, + expression_cst_list::String; + timestep = 0, + elmt_interp::Bool = true, +) + return _TypedFieldHelper( + "VectorField", + expression, + expression_cst_list, + timestep, + elmt_interp, + ) +end + +""" + TensorField(expression, expression_cst_list; timestep=0, elmt_interp=true) + +Generate GetDP string for evaluating tensor fields: `TensorField[expr, ts, interp]{list}`. +""" +function TensorField( + expression, + expression_cst_list::String; + timestep = 0, + elmt_interp::Bool = true, +) + return _TypedFieldHelper( + "TensorField", + expression, + expression_cst_list, + timestep, + elmt_interp, + ) +end + +""" + ComplexScalarField(expression, expression_cst_list; timestep=0, elmt_interp=true) + +Generate GetDP string for evaluating complex scalar fields: `ComplexScalarField[expr, ts, interp]{list}`. +""" +function ComplexScalarField( + expression, + expression_cst_list::String; + timestep = 0, + elmt_interp::Bool = true, +) + return _TypedFieldHelper( + "ComplexScalarField", + expression, + expression_cst_list, + timestep, + elmt_interp, + ) +end + +""" + ComplexVectorField(expression, expression_cst_list; timestep=0, elmt_interp=true) + +Generate GetDP string for evaluating complex vector fields: `ComplexVectorField[expr, ts, interp]{list}`. +""" +function ComplexVectorField( + expression, + expression_cst_list::String; + timestep = 0, + elmt_interp::Bool = true, +) + return _TypedFieldHelper( + "ComplexVectorField", + expression, + expression_cst_list, + timestep, + elmt_interp, + ) +end + +""" + ComplexTensorField(expression, expression_cst_list; timestep=0, elmt_interp=true) + +Generate GetDP string for evaluating complex tensor fields: `ComplexTensorField[expr, ts, interp]{list}`. +""" +function ComplexTensorField( + expression, + expression_cst_list::String; + timestep = 0, + elmt_interp::Bool = true, +) + return _TypedFieldHelper( + "ComplexTensorField", + expression, + expression_cst_list, + timestep, + elmt_interp, + ) +end + + +# Runtime Information/Control Functions +""" + GetCpuTime() + +Generate GetDP string for getting CPU time: `GetCpuTime[]`. +""" +function GetCpuTime() + return "GetCpuTime[]" +end + +""" + GetWallClockTime() + +Generate GetDP string for getting wall clock time: `GetWallClockTime[]`. +""" +function GetWallClockTime() + return "GetWallClockTime[]" +end + +""" + GetMemory() + +Generate GetDP string for getting memory usage (MB): `GetMemory[]`. +""" +function GetMemory() + return "GetMemory[]" +end + +""" + SetNumberRunTime(value_expr, name::String) + +Generate GetDP string for setting a ONELAB variable at runtime: `SetNumberRunTime[value]{"name"}`. +""" +function SetNumberRunTime(value_expr, name::String) + # Note the escaped quotes inside {} + return "SetNumberRunTime[$(value_expr)]{\"$name\"}" +end + +""" + GetNumberRunTime(name::String; default_value=nothing) + +Generate GetDP string for getting a ONELAB variable at runtime: `GetNumberRunTime["name"]` or `GetNumberRunTime["name"]{default}`. +""" +function GetNumberRunTime(name::String; default_value = nothing) + # Note the escaped quotes inside [] + if default_value === nothing + return "GetNumberRunTime[\"$name\"]" + else + return "GetNumberRunTime[\"$name\"]{$(default_value)}" + end +end + +""" + SetVariable(value_expr, variable_id::String) + +Generate GetDP string for setting a runtime variable: `SetVariable[value]{variable_id}`. + +""" +function SetVariable(value_expr, variable_id::String) + return "SetVariable[$(value_expr)]{\$$(variable_id)}" # Note: \$ escapes $ +end + +""" + GetVariable(variable_id::String; default_value=nothing) + +Generate GetDP string for getting a runtime variable: `GetVariable[]{variable_id}` or `GetVariable[default]{variable_id}`. +""" +function GetVariable(variable_id::String; default_value = nothing) + # Note: \$ escapes $ + if default_value === nothing + return "GetVariable[]{\$$(variable_id)}" + else + return "GetVariable[$(default_value)]{\$$(variable_id)}" + end +end + + +# Index/Table Functions +""" + ValueFromIndex(list_expr::String) + +Generate GetDP string for getting value from index map: `ValueFromIndex[]{list}`. +List format: "entity1, value1, entity2, value2, ...". +""" +function ValueFromIndex(list_expr::String) + return "ValueFromIndex[]{{{$(list_expr)}}}" # Note: empty [], triple {{{}}} +end + +""" + VectorFromIndex(list_expr::String) + +Generate GetDP string for getting vector from index map: `VectorFromIndex[]{list}`. +List format: "entity1, v1x, v1y, v1z, entity2, v2x, ...". +""" +function VectorFromIndex(list_expr::String) + return "VectorFromIndex[]{{{$(list_expr)}}}" # Note: empty [], triple {{{}}} +end + +""" + ValueFromTable(default_expr, table_name::String) + +Generate GetDP string for getting value from PostOperation table: `ValueFromTable[default]{"table_name"}`. +""" +function ValueFromTable(default_expr, table_name::String) + # Note escaped quotes inside {} + return "ValueFromTable[$(default_expr)]{\"$table_name\"}" +end diff --git a/ext/fem/getdp_frontend/functionspace.jl b/ext/fem/getdp_frontend/functionspace.jl new file mode 100644 index 00000000..0f357fc7 --- /dev/null +++ b/ext/fem/getdp_frontend/functionspace.jl @@ -0,0 +1,245 @@ +# FunctionSpace: defining function spaces + +using .GetDPFrontend: add_raw_code, comment, make_args +using .GetDPFrontend: AbstractBase_ + +# FunctionSpace struct +mutable struct FunctionSpace <: AbstractBase_ + name::String + content::String + items::Vector{Dict} + comment::Union{String, Nothing} + indent::String + + function FunctionSpace() + new("FunctionSpace", "", [], nothing, " "^4) + end +end + +# Add a function space item +function add!( + functionspace::FunctionSpace, + name, + operation, + constraint; + comment = nothing, + kwargs..., +) + item = Dict( + :name => name, + :basis_functions => [], + :global_quantities => [], + :constraints => [], + :comment => comment, + :kwargs => Dict(kwargs), + ) + for (k, v) in kwargs + if v !== nothing + item[:kwargs][k] = v + end + end + push!(functionspace.items, item) + functionspace.content = code(functionspace) + return name +end + +# Add a basis function +function add_basis_function!( + functionspace::FunctionSpace, + name, + nameOfCoef, + func; + Support, + Entity, + comment = nothing, + kwargs..., +) + if isempty(functionspace.items) + Base.error("No FunctionSpace items defined. Call add! first.") + end + item = functionspace.items[end] # Target the last added item + bf = Dict( + :name => name, + :nameOfCoef => nameOfCoef, + :function => func, + :Support => Support, + :Entity => Entity, + :kwargs => Dict(kwargs), + :comment => comment, + ) + push!(item[:basis_functions], bf) + functionspace.content = code(functionspace) + return name +end + +# Add a constraint +function add_constraint!( + functionspace::FunctionSpace, + nameOfCoef, + entityType, + nameOfConstraint; + comment = nothing, + kwargs..., +) + if isempty(functionspace.items) + Base.error("No FunctionSpace items defined. Call add! first.") + end + item = functionspace.items[end] # Target the last added item + c = Dict( + :nameOfCoef => nameOfCoef, + :entityType => entityType, + :nameOfConstraint => nameOfConstraint, + :kwargs => Dict(kwargs), + :comment => comment, + ) + push!(item[:constraints], c) + functionspace.content = code(functionspace) + return nameOfCoef +end + +# Add a global quantity +function add_global_quantity!( + functionspace::FunctionSpace, + name, + type; + NameOfCoef, + comment = nothing, + kwargs..., +) + if isempty(functionspace.items) + Base.error("No FunctionSpace items defined. Call add! first.") + end + item = functionspace.items[end] # Target the last added item + gq = Dict( + :name => name, + :type => type, + :NameOfCoef => NameOfCoef, + :kwargs => Dict(kwargs), + :comment => comment, + ) + push!(item[:global_quantities], gq) + functionspace.content = code(functionspace) + return name +end + +function add_raw_code!(functionspace::FunctionSpace, raw_code, newline = true) + functionspace.content = add_raw_code(functionspace.content, raw_code, newline) +end + +function add_comment!(functionspace::FunctionSpace, comment_text, newline = true) + add_raw_code!(functionspace, comment(comment_text; newline = false), newline) +end + +function code(functionspace::FunctionSpace) + code_lines = String[] + push!(code_lines, "\nFunctionSpace {") + + for item in functionspace.items + c = "{ Name $(item[:name])" + for (k, v) in item[:kwargs] + if v !== nothing + c *= "; $k $(make_args(v, sep=","))" + end + end + c *= ";" + if item[:comment] !== nothing + c *= " " * comment(item[:comment], newline = false) + end + push!(code_lines, " " * c) + + # Basis Functions + if !isempty(item[:basis_functions]) + push!(code_lines, " BasisFunction {") + for bf in item[:basis_functions] + bfc = "{ Name $(bf[:name]); NameOfCoef $(bf[:nameOfCoef]); Function $(bf[:function]); Support $(bf[:Support]); Entity $(bf[:Entity])" + for (k, v) in bf[:kwargs] + if k ∉ [:condition, :endCondition] && v !== nothing + bfc *= "; $k $(make_args(v, sep=","))" + end + end + bfc *= "; }" # Ensure each basis function definition closes with } + if bf[:comment] !== nothing + bfc *= " " * comment(bf[:comment], newline = false) + end + if haskey(bf[:kwargs], :condition) + push!(code_lines, " $(bf[:kwargs][:condition])") + push!(code_lines, " " * bfc) + if haskey(bf[:kwargs], :endCondition) + push!(code_lines, " $(bf[:kwargs][:endCondition])") + end + else + push!(code_lines, " " * bfc) + end + end + push!(code_lines, " }") + end + + # Global Quantities + if !isempty(item[:global_quantities]) + push!(code_lines, " GlobalQuantity {") + for gq in item[:global_quantities] + gqc = "{ Name $(gq[:name]); Type $(gq[:type]); NameOfCoef $(gq[:NameOfCoef]);" + for (k, v) in gq[:kwargs] + if k ∉ [:condition, :endCondition] && v !== nothing + gqc *= "; $k $(make_args(v, sep=","))" + end + end + gqc *= "}" + if gq[:comment] !== nothing + gqc *= " " * comment(gq[:comment], newline = false) + end + push!(code_lines, " " * gqc) + end + push!(code_lines, " }") + end + + # Constraints + if !isempty(item[:constraints]) + push!(code_lines, " Constraint {") + for c in item[:constraints] + cc = "{ NameOfCoef $(c[:nameOfCoef]); EntityType $(c[:entityType]); NameOfConstraint $(c[:nameOfConstraint]);" + for (k, v) in c[:kwargs] + if k ∉ [:condition, :endCondition] && v !== nothing + cc *= "; $k $(make_args(v, sep=","))" + end + end + cc *= "}" + if c[:comment] !== nothing + if haskey(c[:kwargs], :condition) + push!(code_lines, " $(c[:kwargs][:condition])") + push!(code_lines, " " * comment(c[:comment], newline = false)) + push!(code_lines, " " * cc) + if haskey(c[:kwargs], :endCondition) + push!(code_lines, " $(c[:kwargs][:endCondition])") + end + else + push!( + code_lines, + " " * cc * " " * comment(c[:comment], newline = false), + ) + end + else + if haskey(c[:kwargs], :condition) + push!(code_lines, " $(c[:kwargs][:condition])") + push!(code_lines, " " * cc) + if haskey(c[:kwargs], :endCondition) + push!(code_lines, " $(c[:kwargs][:endCondition])") + end + else + push!(code_lines, " " * cc) + end + end + end + push!(code_lines, " }") + end + + push!(code_lines, " }") + end + + push!(code_lines, "}") + if functionspace.comment !== nothing + return comment(functionspace.comment) * "\n" * join(code_lines, "\n") * "\n" + else + return join(code_lines, "\n") * "\n" + end +end diff --git a/ext/fem/getdp_frontend/getdp_object.jl b/ext/fem/getdp_frontend/getdp_object.jl new file mode 100644 index 00000000..1dddca76 --- /dev/null +++ b/ext/fem/getdp_frontend/getdp_object.jl @@ -0,0 +1,244 @@ +# Base GetDP objects + +using .GetDPFrontend: add_raw_code, comment, make_args + +""" + GetDPObject + +Base GetDP object that other objects inherit from. +""" +abstract type AbstractGetDPObject end + +mutable struct GetDPObject <: AbstractGetDPObject + name::String + content::String + comment::Union{String,Nothing} + indent::String + + function GetDPObject(name="Group", content="", comment=nothing) + indent = " "^4 # Same as Python's len(self.name) + new(name, content, comment, indent) + end +end + +""" + code(obj::GetDPObject) + +Generate GetDP code for the object. +""" +function code(obj::GetDPObject) + code_lines = String[] + push!(code_lines, "$(obj.name){") + + for line in split(obj.content, '\n') + push!(code_lines, line) + end + + push!(code_lines, "}") + + code_str = join(code_lines, "\n" * obj.indent) * "\n" + + if obj.comment !== nothing + code_str = comment(obj.comment) * "\n" * code_str + end + + return code_str +end + + + + +""" + add_raw_code!(obj::GetDPObject, raw_code, newline=true) + +Add raw code to the object's content. +""" +function add_raw_code!(obj::GetDPObject, raw_code, newline=true) + obj.content = add_raw_code(obj.content, raw_code, newline) +end + +""" + add_comment!(obj::GetDPObject, comment_text, newline=true) + +Add a comment to the object's content. +""" +function add_comment!(obj::GetDPObject, comment_text, newline=true) + add_raw_code!(obj, comment(comment_text; newline=false), newline) +end + +""" + SimpleItem + +A simple item in a GetDP object. +""" +mutable struct SimpleItem + comment::String + code::String + + function SimpleItem(; comment=nothing, kwargs...) + code_str = " { " + + for (k, v) in kwargs + code_str *= " $k " * make_args(v, sep=",") * ";" + end + + code_str *= " } " + + if comment !== nothing + code_str *= GetDP.comment(comment) + end + + new("", code_str) + end +end + +function code(item::SimpleItem) + return item.code +end + +""" + Base_ + +Base class for GetDP objects with items. +""" +abstract type AbstractBase_ end + +mutable struct Base_ <: AbstractBase_ + comment::String + code::String + items::Vector{SimpleItem} + _code0::String + + function Base_(header; comment=nothing, kwargs...) + code_str = header * " { " + + if comment !== nothing + code_str *= GetDP.comment(comment) + end + + code_str *= " \n }" + + new("", code_str, SimpleItem[], code_str) + end +end + +""" + add!(base::Base_, args...; kwargs...) + +Add a SimpleItem to a Base_ object. +""" +function add!(base::Base_, args...; kwargs...) + item = SimpleItem(args...; kwargs...) + s = base.code + n = 10 + base.code = s[1:end-n] * "\n " * item.code * s[end-n+1:end] + push!(base.items, item) + return item +end + +""" + ObjectItem + +An object item in a GetDP object. +""" +mutable struct ObjectItem + Name::String + comment::String + cases::Vector{SimpleItem} + + function ObjectItem(Name; comment=nothing) + new(Name, comment !== nothing ? comment : "", SimpleItem[]) + end +end + +""" + code(obj::ObjectItem) + +Generate GetDP code for the object item. +""" +function code(item::ObjectItem) + case_codes = join([code(case) for case in item.cases], "\n") + _code = "{ Name $(item.Name); Case {\n" * case_codes * "\n} }" + if !isempty(item.comment) + _code *= " " * GetDP.comment(item.comment) + end + return _code +end + +""" + CaseItem_ + +A case item in a GetDP object. +""" +mutable struct CaseItem_ + Region::Any + code::String + + function CaseItem_(Region; comment=nothing, kwargs...) + code_str = "{" * " Region $(Region);" + + for (k, v) in kwargs + code_str *= " $k " * make_args(v, sep=",") * ";" + end + + code_str *= " } " + + if comment !== nothing + code_str *= GetDP.comment(comment) + end + + new(Region, code_str) + end +end + +""" + Case_ + +A case in a GetDP object. +""" +mutable struct Case_ + Name::Union{String,Nothing} + comment::String + code::String + case_items::Vector{CaseItem_} + + function Case_(; Name=nothing, comment=nothing, kwargs...) + case_name = Name === nothing ? "" : Name + code_str = "Case $(case_name) " + code_str *= "{ " + + if comment !== nothing + code_str *= GetDP.comment(comment) + end + + code_str *= " \n }" + + new(Name, "", code_str, CaseItem_[]) + end +end + +""" + add!(case::Case_, args...; kwargs...) + +Add a CaseItem_ to a Case_ object. +""" +function add!(case::Case_, args...; kwargs...) + case_item = CaseItem_(args...; kwargs...) + s = case.code + n = 7 + case.code = s[1:end-n] * "\n " * case_item.code * s[end-n+1:end] + push!(case.case_items, case_item) + return case_item +end + +""" + add!(obj::ObjectItem, args...; kwargs...) + +Add a Case_ to an ObjectItem. +""" +function add!(obj::ObjectItem, args...; kwargs...) + obj._code0 = code(obj) + case = Case_(args...; kwargs...) + push!(obj.cases, case) + return case +end diff --git a/ext/fem/getdp_frontend/group.jl b/ext/fem/getdp_frontend/group.jl new file mode 100644 index 00000000..98c64fed --- /dev/null +++ b/ext/fem/getdp_frontend/group.jl @@ -0,0 +1,311 @@ +# Group: defining topological entities + +using .GetDPFrontend: make_args + +""" + Group + +Defining topological entities. +""" +mutable struct Group <: AbstractGetDPObject + name::String + content::String + comment::Union{String, Nothing} + indent::String + idlist::Vector{String} + + function Group() + new("Group", "", nothing, " "^4, String[]) + end +end + +""" + define!(group::Group, id="domain") + +Define a group. +""" +function define!(group::Group, id = "domain") + if isa(id, Array) + id = join([string(g) for g in id], ", ") + end + + c = "DefineGroup[$(id)];" + group.content *= c * "\n" + return nothing +end +""" + add_raw_code!(group::group, raw_code, newline=true) + +Add raw code to the group object. +""" +function add_raw_code!(group::Group, raw_code, newline = true) + group.content = add_raw_code(group.content, raw_code, newline) +end +""" + add_comment!(group::group, comment_text, newline=true) + +Add a comment to the group object. +""" +function add_comment!(group::Group, comment_text; newline = true) + add_raw_code!(group, comment(comment_text, newline = false), newline) +end +""" + add!(group::Group, id="domain", glist=[1], gtype="Region"; operation="=", comment=nothing, kwargs...) + +Add an expression to the Group object. The default group type is "Region". Use operation="=" to define a group or operation="+=" to extend an existing group. +""" +function add!( + group::Group, + id = "domain", + glist = [1], + gtype = "Region"; + operation = "=", + comment = nothing, + kwargs..., +) + if operation == "=" && id in group.idlist + Base.error("Identifier $(id) already in use.") + elseif operation == "+=" && !(id in group.idlist) + Base.error("Cannot extend non-existing group $(id).") + end + + glist_str = make_args(glist) + + c = "$(id) $(operation) $(gtype)[ $(glist_str) " + + for (k, v) in kwargs + if v !== nothing + c *= ", " * string(k) * " " * make_args(v) + end + end + + c *= "];" + group.content *= c + + if comment !== nothing + add_comment!(group, comment; newline = false) + end + + group.content *= "\n" + if operation == "=" + push!(group.idlist, id) + end + + return id +end + +""" + add_space!(group::Group, num_spaces::Int=1) + +Add a specified number of empty lines to the Group object's content for spacing in the output. +""" +function add_space!(group::Group, num_spaces::Int = 1) + group.content *= "\n" ^ num_spaces + return nothing +end + +""" + code(group::Group) + +Generate GetDP code for a Group object. +""" +function code(group::Group) + code_lines = String[] + push!(code_lines, "\nGroup{") + + if !isempty(group.content) + for line in split(group.content, '\n') + if !isempty(line) + push!(code_lines, " " * line) + else + push!(code_lines, "") # Intended to preserve blank lines + end + end + end + + push!(code_lines, "}") + return join(code_lines, "\n") +end + +""" + Region(group::Group, args...; kwargs...) + +Regions in R1. +""" +function Region(group::Group, args...; kwargs...) + return add!(group, args...; gtype = "Region", kwargs...) +end + +""" + Global(group::Group, args...; kwargs...) + +Regions in R1 (variant of Region used with global BasisFunctions BF_Global and BF_dGlobal). +""" +function Global(group::Group, args...; kwargs...) + return add!(group, args...; gtype = "Global", kwargs...) +end + +""" + NodesOf(group::Group, args...; Not=nothing, kwargs...) + +Nodes of elements of R1 (Not: but not those of R2). +""" +function NodesOf(group::Group, args...; Not = nothing, kwargs...) + return add!(group, args...; gtype = "NodesOf", Not = Not, kwargs...) +end + +""" + EdgesOf(group::Group, args...; Not=nothing, kwargs...) + +Edges of elements of R1 (Not: but not those of R2). +""" +function EdgesOf(group::Group, args...; Not = nothing, kwargs...) + return add!(group, args...; gtype = "EdgesOf", Not = Not, kwargs...) +end + +""" + FacetsOf(group::Group, args...; Not=nothing, kwargs...) + +Facets of elements of R1 (Not: but not those of R2). +""" +function FacetsOf(group::Group, args...; Not = nothing, kwargs...) + return add!(group, args...; gtype = "FacetsOf", Not = Not, kwargs...) +end + +""" + VolumesOf(group::Group, args...; Not=nothing, kwargs...) + +Volumes of elements of R1 (Not: but not those of R2). +""" +function VolumesOf(group::Group, args...; Not = nothing, kwargs...) + return add!(group, args...; gtype = "VolumesOf", Not = Not, kwargs...) +end + +""" + ElementsOf(group::Group, args...; OnOneSideOf=nothing, OnPositiveSideOf=nothing, Not=nothing, kwargs...) + +Elements of regions in R1. + +- OnOneSideOf: only elements on one side of R2 (non-automatic, i.e., both sides if both in R1) +- OnPositiveSideOf: only elements on positive (normal) side of R2 +- Not: but not those touching only its skin R3 (mandatory for free skins for correct separation of side layers) +""" +function ElementsOf( + group::Group, + args...; + OnOneSideOf = nothing, + OnPositiveSideOf = nothing, + Not = nothing, + kwargs..., +) + return add!( + group, + args...; + gtype = "ElementsOf", + OnOneSideOf = OnOneSideOf, + OnPositiveSideOf = OnPositiveSideOf, + Not = Not, + kwargs..., + ) +end + +""" + GroupsOfNodesOf(group::Group, args...; kwargs...) + +Groups of nodes of elements of R1 (a group is associated with each region). +""" +function GroupsOfNodesOf(group::Group, args...; kwargs...) + return add!(group, args...; gtype = "GroupsOfNodesOf", kwargs...) +end + +""" + GroupsOfEdgesOf(group::Group, args...; InSupport=nothing, kwargs...) + +Groups of edges of elements of R1 (a group is associated with each region). +< InSupport: in a support R2 being a group of type ElementOf, i.e., containing elements >. +""" +function GroupsOfEdgesOf(group::Group, args...; InSupport = nothing, kwargs...) + return add!(group, args...; gtype = "GroupsOfEdgesOf", InSupport = InSupport, kwargs...) +end + +""" + GroupsOfEdgesOnNodesOf(group::Group, args...; Not=nothing, kwargs...) + +Groups of edges incident to nodes of elements of R1 (a group is associated with each node). +< Not: but not those of R2) >. +""" +function GroupsOfEdgesOnNodesOf(group::Group, args...; Not = nothing, kwargs...) + return add!(group, args...; gtype = "GroupsOfEdgesOnNodesOf", Not = Not, kwargs...) +end + +""" + GroupOfRegionsOf(group::Group, args...; kwargs...) + +Single group of elements of regions in R1 +(with basis function BF_Region just one DOF is created for all elements of R1). +""" +function GroupOfRegionsOf(group::Group, args...; kwargs...) + return add!(group, args...; gtype = "GroupOfRegionsOf", kwargs...) +end + +""" + EdgesOfTreeIn(group::Group, args...; StartingOn=nothing, kwargs...) + +Edges of a tree of edges of R1 +< StartingOn: a complete tree is first built on R2 >. +""" +function EdgesOfTreeIn(group::Group, args...; StartingOn = nothing, kwargs...) + return add!(group, args...; gtype = "EdgesOfTreeIn", StartingOn = StartingOn, kwargs...) +end + +""" + FacetsOfTreeIn(group::Group, args...; StartingOn=nothing, kwargs...) + +Facets of a tree of facets of R1 +< StartingOn: a complete tree is first built on R2 >. +""" +function FacetsOfTreeIn(group::Group, args...; StartingOn = nothing, kwargs...) + return add!( + group, + args...; + gtype = "FacetsOfTreeIn", + StartingOn = StartingOn, + kwargs..., + ) +end + +""" + DualNodesOf(group::Group, args...; kwargs...) + +Dual nodes of elements of R1. +""" +function DualNodesOf(group::Group, args...; kwargs...) + return add!(group, args...; gtype = "DualNodesOf", kwargs...) +end + +""" + DualEdgesOf(group::Group, args...; kwargs...) + +Dual edges of elements of R1. +""" +function DualEdgesOf(group::Group, args...; kwargs...) + return add!(group, args...; gtype = "DualEdgesOf", kwargs...) +end + +""" + DualFacetsOf(group::Group, args...; kwargs...) + +Dual facets of elements of R1. +""" +function DualFacetsOf(group::Group, args...; kwargs...) + return add!(group, args...; gtype = "DualFacetsOf", kwargs...) +end + +""" + DualVolumesOf(group::Group, args...; kwargs...) + +Dual volumes of elements of R1. +""" +function DualVolumesOf(group::Group, args...; kwargs...) + return add!(group, args...; gtype = "DualVolumesOf", kwargs...) +end diff --git a/ext/fem/getdp_frontend/helpers.jl b/ext/fem/getdp_frontend/helpers.jl new file mode 100644 index 00000000..25a05210 --- /dev/null +++ b/ext/fem/getdp_frontend/helpers.jl @@ -0,0 +1,156 @@ +# Helper functions for GetDP.jl + +""" + add_raw_code(s, raw_code, newline=true) + +Add raw code to a string, optionally with a newline. +""" +function add_raw_code(s, raw_code, newline = true) + nl = newline ? "\n" : "" + return s * nl * raw_code +end + +""" + comment(s, style="short", newline=false) + +Add a comment to a string, optionally with a newline. +""" +function comment(s; style = "short", newline = false) + nl = newline ? "\n" : "" + + if style == "short" + if length(s) > 80 + # Split long comments into multiple lines + lines = split_long_string(s) + return comment(join(lines, "\n"), style = "long", newline = newline) + else + return nl * "// " * s + end + elseif style == "long" + return nl * "/* " * s * " */" + end +end + +""" + split_long_string(s, width=80) + +Split a long string into multiple lines. +""" +function split_long_string(s, width = 80) + lines = String[] + words = split(s) + current_line = "" + + for word in words + if length(current_line) + length(word) + 1 <= width + if isempty(current_line) + current_line = word + else + current_line *= " " * word + end + else + push!(lines, current_line) + current_line = word + end + end + + if !isempty(current_line) + push!(lines, current_line) + end + + return lines +end + +""" + make_args(glist, sep=",", list_char=false) + +Format arguments for GetDP code. +""" + +function make_args(glist; sep::String = ", ", list_char::Bool = false) + sep = sep * " " # Add space after separator if needed + + if isa(glist, Array) + if length(glist) == 1 + return "{" * string(glist[1]) * "}" + else + formatted = join([string(g) for g in glist], sep) + return list_char ? "#{$formatted}" : "{$formatted}" + end + else + return string(glist) + end +end + +# """ +# get_getdp_exe() + +# Get the path to the GetDP executable. +# """ +# function get_getdp_exe() +# macos_getdp_location = "/Applications/Getdp.app/Contents/MacOS/getdp" +# return isfile(macos_getdp_location) ? macos_getdp_location : "getdp" +# end + +""" + array2getdplist(l) + +Convert a Julia array to a GetDP list. +""" +function array2getdplist(l) + return "{" * join([string(item) for item in l], ",") * "}" +end + + +function get_getdp_major_version(getdp_exe = get_getdp_executable()) + out = read(`$getdp_exe --version`, String) + ex = split(strip(out), ".") + return parse(Int, ex[1]) +end + +# """ +# replace_formula(str_in, to_replace, replacement) + +# Replace formulas in a string. +# """ +# function replace_formula(str_in, to_replace, replacement) +# str_in = replace(join(split(str_in)), "{" => "", "}" => "") + +# to_replace = [replace(join(split(r)), "{" => "", "}" => "") for r in to_replace] + +# # This is a simplified version of the Python tokenize approach +# # In a real implementation, we might want to use a proper Julia tokenizer +# for (rold, rnew) in zip(to_replace, replacement) +# str_in = replace(str_in, rold => rnew) +# end + +# return str_in +# end + +""" + get_getdp_executable() + +Get the path to the GetDP executable. +""" +function get_getdp_executable() + # Environment variable takes precedence + env_path = get(ENV, "GETDP_EXECUTABLE", nothing) + if env_path !== nothing + isfile(env_path) || throw( + ArgumentError( + "GETDP_EXECUTABLE points to a missing file: $(repr(env_path)).", + ), + ) + return abspath(env_path) + end + + # Then system installation + system_getdp = Sys.which("getdp") + system_getdp !== nothing && return system_getdp + + throw( + ArgumentError( + "GetDP executable not found. Set GETDP_EXECUTABLE or add getdp to PATH.", + ), + ) +end diff --git a/ext/fem/getdp_frontend/integration.jl b/ext/fem/getdp_frontend/integration.jl new file mode 100644 index 00000000..a34a05d8 --- /dev/null +++ b/ext/fem/getdp_frontend/integration.jl @@ -0,0 +1,135 @@ +# Integration: defining integration methods + +using .GetDPFrontend: comment, make_args + +# IntegrationItemCase for nested Case blocks (e.g., Case { { Type Gauss; Case { ... } } }) +mutable struct IntegrationItemCase + items::Vector{Union{SimpleItem, Pair{String, IntegrationItemCase}}} + comment::String + + function IntegrationItemCase(; comment="") + new([], comment) + end +end + +function add!(case::IntegrationItemCase; kwargs...) + item = SimpleItem(; kwargs...) + push!(case.items, item) + return case +end + +function add_nested_case!(case::IntegrationItemCase; type::String, kwargs...) + nested_case = IntegrationItemCase(; kwargs...) + push!(case.items, type => nested_case) + return nested_case +end + +function code(case::IntegrationItemCase; indent_level::Int=0) + indent = " " ^ indent_level + case_codes = [] + for item in case.items + if item isa SimpleItem + # Indent SimpleItem code (e.g., { GeoElement Point; NumberOfPoints 1; }) + push!(case_codes, indent * " " * code(item)) + elseif item isa Pair + type, nested_case = item + # Start typed block (e.g., { Type Gauss; ) + push!(case_codes, indent * " " * "{ Type $type;") + # Recursively generate nested case code with increased indent + nested_code = code(nested_case; indent_level=indent_level+1) + push!(case_codes, nested_code) + # Close typed block + push!(case_codes, indent * " " * "}") + end + end + # Join case codes with newlines + codes = join(case_codes, "\n") + # Wrap in Case block + _code = indent * "Case {\n" * codes * "\n" * indent * "}" + if !isempty(case.comment) + _code *= " " * GetDP.comment(case.comment) + end + return _code +end +# IntegrationItem for each { Name I1; ... } block +mutable struct IntegrationItem + name::String + comment::String + cases::Vector{IntegrationItemCase} + + function IntegrationItem(name; comment=nothing) + new(name, comment !== nothing ? comment : "", IntegrationItemCase[]) + end +end + +function add!(item::IntegrationItem; kwargs...) + case = IntegrationItemCase(; kwargs...) + push!(item.cases, case) + return case +end + +function code(item::IntegrationItem; indent_level::Int=0) + indent = " " ^ indent_level + case_codes = join([code(case; indent_level=indent_level+1) for case in item.cases], "\n") + _code = indent * "{ Name $(item.name);\n" * case_codes * "\n" * indent * "}" + if !isempty(item.comment) + _code *= " " * GetDP.comment(item.comment) + end + return _code +end +# Integration struct +mutable struct Integration <: AbstractGetDPObject + name::String + content::String + items::Vector{IntegrationItem} + comment::Union{String,Nothing} + + function Integration() + new("Integration", "", IntegrationItem[], nothing) + end +end + +function add!(integration::Integration, name::String; comment=nothing, kwargs...) + item = IntegrationItem(name; comment) + push!(integration.items, item) + update_content!(integration) # Update content when adding an item + return item +end + +function update_content!(integration::Integration) + content = "" + for item in integration.items + content *= code(item) * "\n" + end + integration.content = rstrip(content) +end + +function code(integration::Integration; indent_level::Int=0) + indent = " " ^ indent_level + code_lines = [indent * "\nIntegration {"] + if !isempty(integration.items) + for item in integration.items + for line in split(code(item; indent_level=indent_level+1), '\n') + push!(code_lines, line) + end + end + elseif !isempty(integration.content) + for line in split(integration.content, '\n') + push!(code_lines, indent * " " * line) + end + end + push!(code_lines, indent * "}") + _code = join(code_lines, "\n") + if integration.comment !== nothing + return comment(integration.comment) * "\n" * _code + else + return _code + end +end +function add_raw_code!(integration::Integration, raw_code, newline=true) + integration.content = add_raw_code(integration.content, raw_code, newline) +end + +function add_comment!(integration::Integration, comment_text, newline=true) + integration.comment = comment_text +end diff --git a/ext/fem/getdp_frontend/jacobian.jl b/ext/fem/getdp_frontend/jacobian.jl new file mode 100644 index 00000000..01ca247e --- /dev/null +++ b/ext/fem/getdp_frontend/jacobian.jl @@ -0,0 +1,134 @@ +# Jacobian: defining jacobians + +using .GetDPFrontend: add_raw_code, comment, make_args + +# function code(item::SimpleItem) +# return item.code +# end + +mutable struct CaseItem + regions::Vector{Dict{String, Any}} + comment::Union{String, Nothing} + + function CaseItem(; comment=nothing) + new([], comment) + end +end + +function code(case::CaseItem) + result = "Case {\n" + for region in case.regions + result *= " { Region $(region["Region"]); Jacobian $(region["Jacobian"]);}" + if haskey(region, "comment") && region["comment"] !== nothing + result *= " " * comment(region["comment"]) + end + result *= "\n" + end + result *= "}" + return result +end + +function add!(case::CaseItem; Region, Jacobian, comment=nothing) + push!(case.regions, Dict("Region" => Region, "Jacobian" => Jacobian, "comment" => comment)) + return case +end + +function add!(item::ObjectItem; kwargs...) + case = SimpleItem(; kwargs...) + push!(item.cases, case) + return item +end +function update_code!(item::ObjectItem) + item._code = "{ Name $(item.Name); \n " + + if !isempty(item.cases) + item._code *= "Case {\n" + for case in item.cases + for region in case.regions + item._code *= " { Region $(region["Region"]); Jacobian $(region["Jacobian"]);" + item._code *= "}" + if haskey(region, "comment") && region["comment"] !== nothing + item._code *= " " * comment(region["comment"]) + end + item._code *= "\n" + end + end + item._code *= "}" + end + + item._code *= "}" +end + +# function code(item::ObjectItem) +# case_codes = join([code(case) for case in item.cases], "\n") +# _code = "{ Name $(item.Name); Case {\n" * case_codes * "\n} }" +# if !isempty(item.comment) +# _code *= " " * GetDP.comment(item.comment) +# end +# return _code +# end +# Jacobian struct +mutable struct Jacobian <: AbstractGetDPObject + name::String + content::String + items::Vector{ObjectItem} + comment::Union{String,Nothing} + + function Jacobian() + new("Jacobian", "", ObjectItem[], nothing) + end +end + +function add!(jacobian::Jacobian, name::String; kwargs...) + item = ObjectItem(name; kwargs...) + push!(jacobian.items, item) + update_content!(jacobian) + return item +end + +function VolSphShell(; Rint, Rext, center_X=nothing, center_Y=nothing, center_Z=nothing, power=nothing, inv_inf=nothing) + params = [Rint, Rext] + for param in [center_X, center_Y, center_Z, power, inv_inf] + if param !== nothing + push!(params, param) + end + end + param_str = join(params, ", ") + return "VolSphShell{$param_str}" +end +function update_content!(jacobian::Jacobian) + content = "" + for item in jacobian.items + content *= code(item) * "\n" + end + jacobian.content = rstrip(content) +end + +function code(jac::Jacobian) + code_lines = ["\nJacobian{"] + if !isempty(jac.items) + for item in jac.items + for line in split(code(item), '\n') + push!(code_lines, " " * line) + end + end + elseif !isempty(jac.content) + for line in split(jac.content, '\n') + push!(code_lines, " " * line) + end + end + push!(code_lines, "}") + if jac.comment !== nothing + return comment(jac.comment) * "\n" * join(code_lines, "\n") + else + return join(code_lines, "\n") + end +end + +function add_raw_code!(jacobian::Jacobian, raw_code, newline=true) + jacobian.content = add_raw_code(jacobian.content, raw_code, newline) +end + +function add_comment!(jacobian::Jacobian, comment_text, newline=true) + jacobian.comment = comment_text +end diff --git a/ext/fem/getdp_frontend/postoperation.jl b/ext/fem/getdp_frontend/postoperation.jl new file mode 100644 index 00000000..d0b5b7a8 --- /dev/null +++ b/ext/fem/getdp_frontend/postoperation.jl @@ -0,0 +1,182 @@ +# PostOperation: defining post-operations + +using .GetDPFrontend: add_raw_code, comment, make_args + +""" + OperationItem + +An operation item (Print or Echo) in a post-operation. +""" +mutable struct OperationItem + code::String + comment::Union{String,Nothing} + + function OperationItem(code; comment=nothing) + new(code, comment) + end +end + +function code(item::OperationItem) + if item.comment !== nothing + return comment(item.comment, newline=false) * "\n " * item.code + else + return " " * item.code + end +end + +""" + POBase_ + +A collection of operations (Print, Echo, etc.) in a post-operation. +""" +mutable struct POBase_ + operations::Vector{OperationItem} + raw_codes::Vector{String} + comment::Union{String,Nothing} + + function POBase_(; comment=nothing) + new(OperationItem[], String[], comment) + end +end + +function add_operation!(pobase::POBase_, code; comment=nothing) + item = OperationItem(code; comment=comment) + push!(pobase.operations, item) + item +end + +function add_raw_code!(pobase::POBase_, raw_code) + push!(pobase.raw_codes, raw_code) +end + +function code(pobase::POBase_) + code_lines = String[] + push!(code_lines, "Operation {") + for operation in pobase.operations + push!(code_lines, code(operation)) + end + push!(code_lines, "}") + join(code_lines, "\n") +end + +""" + PostOperationItem + +An item in a post-operation section. +""" +mutable struct PostOperationItem + id::String + NameOfPostProcessing::String + operation::POBase_ + raw_codes::Vector{String} + comment::Union{String,Nothing} + kwargs::Dict + + function PostOperationItem(id; NameOfPostProcessing, comment=nothing, kwargs...) + new(id, NameOfPostProcessing, POBase_(), String[], comment, Dict(kwargs)) + end +end + +function add_operation!(poitem::PostOperationItem, header="Operation"; comment=nothing) + poitem.operation = POBase_(; comment=comment) + poitem.operation +end + +function code(poitem::PostOperationItem) + code_lines = ["{ Name $(poitem.id); NameOfPostProcessing $(poitem.NameOfPostProcessing);"] + if poitem.comment !== nothing + push!(code_lines, " $(comment(poitem.comment, newline=false))") + end + operation_code = code(poitem.operation) + for line in split(operation_code, '\n') + push!(code_lines, " $line") + end + push!(code_lines, "}") + for raw_code in poitem.raw_codes + push!(code_lines, raw_code) + end + join(code_lines, "\n") +end + +""" + PostOperation + +Defining post-operations. +""" +mutable struct PostOperation <: AbstractGetDPObject + name::String + content::String + items::Vector{PostOperationItem} + comment::Union{String,Nothing} + indent::String + raw_codes::Vector{String} + + function PostOperation() + new("PostOperation", "", PostOperationItem[], nothing, " "^4, String[]) + end +end + +""" + add!(postoperation::PostOperation, id, NameOfPostProcessing; kwargs...) + +Add a post-operation item. +""" +function add!(postoperation::PostOperation, id, NameOfPostProcessing; comment=nothing, kwargs...) + item = PostOperationItem(id; NameOfPostProcessing=NameOfPostProcessing, comment=comment, kwargs...) + push!(postoperation.items, item) + postoperation.content = code(postoperation) + item +end + +""" + add_raw_code!(postoperation::PostOperation, raw_code, newline=true) + +Add raw code to the PostOperation object. +""" +function add_raw_code!(postoperation::PostOperation, raw_code, newline=true) + push!(postoperation.raw_codes, raw_code * (newline ? "\n" : "")) +end + +""" + add_raw_code!(poitem::PostOperationItem, raw_code) + +Add raw code to a PostOperationItem. +""" +function add_raw_code!(poitem::PostOperationItem, raw_code) + push!(poitem.raw_codes, raw_code) +end + +""" + add_comment!(postoperation::PostOperation, comment_text, newline=true) + +Add a comment to the PostOperation object. +""" +function add_comment!(postoperation::PostOperation, comment_text, newline=true) + add_raw_code!(postoperation, comment(comment_text, newline=false), newline) +end + +""" + code(postoperation::PostOperation) + +Generate GetDP code for a PostOperation object. +""" +function code(postoperation::PostOperation) + code_lines = ["\nPostOperation {"] + for raw_code in postoperation.raw_codes + push!(code_lines, " " * raw_code) + end + for item in postoperation.items + item_code = code(item) + for line in split(item_code, '\n') + if !isempty(line) + push!(code_lines, " $line") + end + end + end + push!(code_lines, "}") + if postoperation.comment !== nothing + return comment(postoperation.comment) * "\n" * join(code_lines, "\n") * "\n" + else + return join(code_lines, "\n") * "\n" + end +end diff --git a/ext/fem/getdp_frontend/postprocessing.jl b/ext/fem/getdp_frontend/postprocessing.jl new file mode 100644 index 00000000..fb5c9d22 --- /dev/null +++ b/ext/fem/getdp_frontend/postprocessing.jl @@ -0,0 +1,203 @@ +using .GetDPFrontend: add_raw_code, comment, make_args + +""" + QuantityTerm + +A term or integral in a post-processing quantity. +""" +mutable struct QuantityTerm + term_type::String # "Term" or "Integral" + Value::String + options::Dict + comment::Union{String, Nothing} + + function QuantityTerm(; term_type, Value, comment = nothing, kwargs...) + new(term_type, Value, Dict(kwargs), comment) + end +end + +function code(term::QuantityTerm) + code_lines = String[] + if term.term_type == "Term" + # Include Type option (e.g., Global) if specified + type_str = haskey(term.options, :Type) ? "Type $(term.options[:Type]); " : "" + push!(code_lines, "Term { $type_str[ $(term.Value) ];") + # Include In, Jacobian, Integration options in order + for key in [:In, :Jacobian, :Integration] + if haskey(term.options, key) + push!(code_lines, " $key $(make_args(term.options[key], sep=","));") + end + end + push!(code_lines, "}") + elseif term.term_type == "Integral" + push!(code_lines, "Integral { [ $(term.Value) ];") + for key in [:In, :Jacobian, :Integration] + if haskey(term.options, key) + push!(code_lines, " $key $(make_args(term.options[key], sep=","));") + end + end + push!(code_lines, "}") + else + Base.error("Unsupported term type: $(term.term_type)") + end + join(code_lines, "\n") +end + +""" + PostQuantity + +A post-processing quantity with one or more terms. +""" +mutable struct PostQuantity + name::String + terms::Vector{QuantityTerm} + comment::Union{String, Nothing} + + function PostQuantity(name; comment = nothing) + new(name, QuantityTerm[], comment) + end +end + +function add!(quantity::PostQuantity, term_type, Value; comment = nothing, kwargs...) + term = + QuantityTerm(; term_type = term_type, Value = Value, comment = comment, kwargs...) + push!(quantity.terms, term) + term +end + +function code(quantity::PostQuantity) + code_lines = String[] + if quantity.comment !== nothing + push!(code_lines, comment(quantity.comment, newline = false)) + end + push!(code_lines, "{ Name $(quantity.name); Value {") + for term in quantity.terms + term_code = code(term) + for line in split(term_code, '\n') + push!(code_lines, " $line") + end + end + push!(code_lines, "}}") + join(code_lines, "\n") +end + +""" + PostproItem + +An item in a post-processing section. +""" +mutable struct PostproItem + name::String + NameOfFormulation::String + quantities::Vector{PostQuantity} + comment::Union{String, Nothing} + kwargs::Dict + + function PostproItem(name; NameOfFormulation, comment = nothing, kwargs...) + new(name, NameOfFormulation, PostQuantity[], comment, Dict(kwargs)) + end +end + +function add!(item::PostproItem, name::String; comment = nothing) + quantity = PostQuantity(name; comment = comment) + push!(item.quantities, quantity) + quantity +end + +function code(item::PostproItem) + code_lines = ["{ Name $(item.name); NameOfFormulation $(item.NameOfFormulation);"] + if item.comment !== nothing + push!(code_lines, " $(comment(item.comment, newline=false))") + end + push!(code_lines, " PostQuantity {") + for quantity in item.quantities + quantity_code = code(quantity) + for line in split(quantity_code, '\n') + push!(code_lines, " $line") + end + end + push!(code_lines, " }") + push!(code_lines, "}") + join(code_lines, "\n") +end + +""" + PostProcessing + +Defining post-processing. +""" +mutable struct PostProcessing <: AbstractGetDPObject + name::String + content::String + items::Vector{PostproItem} + comment::Union{String, Nothing} + indent::String + + function PostProcessing() + new("PostProcessing", "", PostproItem[], nothing, " "^4) + end +end + +""" + add!(postprocessing::PostProcessing, name::String, NameOfFormulation::String; kwargs...) + +Add a post-processing item. +""" +function add!( + postprocessing::PostProcessing, + name::String, + NameOfFormulation::String; + comment = nothing, + kwargs..., +) + item = PostproItem( + name; + NameOfFormulation = NameOfFormulation, + comment = comment, + kwargs..., + ) + push!(postprocessing.items, item) + postprocessing.content = code(postprocessing) + item +end + +""" + code(postprocessing::PostProcessing) + +Generate GetDP code for a PostProcessing object. +""" +function code(postprocessing::PostProcessing) + code_lines = ["\nPostProcessing {"] + for item in postprocessing.items + item_code = code(item) + for line in split(item_code, '\n') + if !isempty(line) + push!(code_lines, " $line") + end + end + end + push!(code_lines, "}") + if postprocessing.comment !== nothing + return comment(postprocessing.comment) * "\n" * join(code_lines, "\n") * "\n" + else + return join(code_lines, "\n") * "\n" + end +end + +""" + add_raw_code!(postprocessing::PostProcessing, raw_code, newline=true) + +Add raw code to the PostProcessing object. +""" +function add_raw_code!(postprocessing::PostProcessing, raw_code, newline = true) + postprocessing.content = add_raw_code(postprocessing.content, raw_code, newline) +end + +""" + add_comment!(postprocessing::PostProcessing, comment_text, newline=true) + +Add a comment to the PostProcessing object. +""" +function add_comment!(postprocessing::PostProcessing, comment_text, newline = true) + add_raw_code!(postprocessing, comment(comment_text; newline = false), newline) +end diff --git a/ext/fem/getdp_frontend/problem_definition.jl b/ext/fem/getdp_frontend/problem_definition.jl new file mode 100644 index 00000000..4f01110f --- /dev/null +++ b/ext/fem/getdp_frontend/problem_definition.jl @@ -0,0 +1,219 @@ +# Problem: defining the problem + +using .GetDPFrontend: add_raw_code, comment, code +using .GetDPFrontend: Group, Function, Constraint, FunctionSpace, Jacobian, Integration +using .GetDPFrontend: Formulation, Resolution, PostProcessing, PostOperation +""" + Problem + +The main problem definition class that brings together all the components. +""" +mutable struct Problem + _GETDP_CODE::Vector{String} + filename::Union{String,Nothing} + group::Group + function_obj::Vector{Function} + constraint::Constraint + functionspace::FunctionSpace + jacobian::Jacobian + integration::Integration + formulation::Formulation + resolution::Resolution + postprocessing::PostProcessing + postoperation::PostOperation + objects::Vector{String} + + function Problem(; gmsh_major_version=nothing) + _GETDP_CODE = ["// File created with GetDP.jl: https://github.com/Electa-Git/GetDP.jl.\n"] + + group = Group() + function_obj = Function[] # Initialize as empty vector + constraint = Constraint() + functionspace = FunctionSpace() + jacobian = Jacobian() + integration = Integration() + formulation = Formulation() + resolution = Resolution() + postprocessing = PostProcessing() + postoperation = PostOperation() + + objects = [ + "group", + "function_obj", + "constraint", + "functionspace", + "jacobian", + "integration", + "formulation", + "resolution", + "postprocessing", + "postoperation" + ] + + new( + _GETDP_CODE, + nothing, + group, + function_obj, + constraint, + functionspace, + jacobian, + integration, + formulation, + resolution, + postprocessing, + postoperation, + objects + ) + end +end + +""" + get_code(problem::Problem) + +Returns properly formatted GetDP code. +""" +function get_code(problem::Problem) + return join(problem._GETDP_CODE, "") +end + +""" + add_raw_code!(problem::Problem, raw_code, newline=true) + +Add raw code to the Problem object. +""" +function add_raw_code!(problem::Problem, raw_code, newline=true) + problem._GETDP_CODE = [add_raw_code(get_code(problem), raw_code, newline)] +end + +""" + add_comment!(problem::Problem, comment_text, newline=true) + +Add a comment to the Problem object. +""" +function add_comment!(problem::Problem, comment_text, newline=true) + add_raw_code!(problem, comment(comment_text; newline=false), newline) +end + +""" + make_problem!(problem::Problem) + +Generate the GetDP code for all objects in the Problem, including only non-empty components. +""" +function make_problem!(problem::Problem) + for attr in problem.objects + if attr == "function_obj" + for func in problem.function_obj # Iterate over all Function objects + if !isempty(func.content) # Check if functions are defined + push!(problem._GETDP_CODE, code(func)) + end + end + elseif attr == "group" + p = getfield(problem, :group) + if !isempty(p.content) # Check if groups are defined + push!(problem._GETDP_CODE, code(p)) + end + elseif attr == "constraint" + p = getfield(problem, :constraint) + if !isempty(p.constraints) # Check if constraints are defined + push!(problem._GETDP_CODE, code(p)) + end + elseif attr == "functionspace" + p = getfield(problem, :functionspace) + if !isempty(p.content) # Check if items are defined + push!(problem._GETDP_CODE, code(p)) + end + elseif attr == "jacobian" + p = getfield(problem, :jacobian) + if !isempty(p.content) # Check if jacobians are defined + push!(problem._GETDP_CODE, code(p)) + end + elseif attr == "integration" + p = getfield(problem, :integration) + if !isempty(p.content) # Check if integrations are defined + push!(problem._GETDP_CODE, code(p)) + end + elseif attr == "formulation" + p = getfield(problem, :formulation) + if !isempty(p.items) # Check if formulations are defined + push!(problem._GETDP_CODE, code(p)) + end + elseif attr == "resolution" + p = getfield(problem, :resolution) + if !isempty(p.content) # Check if resolutions are defined + push!(problem._GETDP_CODE, code(p)) + end + elseif attr == "postprocessing" + p = getfield(problem, :postprocessing) + if !isempty(p.content) # Check if postprocessings are defined + push!(problem._GETDP_CODE, code(p)) + end + elseif attr == "postoperation" + p = getfield(problem, :postoperation) + if !isempty(p.content) # Check if postoperations are defined + push!(problem._GETDP_CODE, code(p)) + end + end + end +end + +""" + write_file(problem::Problem) + +Write the GetDP code to a file. +""" +function write_file(problem::Problem) + if problem.filename === nothing + problem.filename = tempname() + end + + # Remove file if it exists to ensure a new file is created + isfile(problem.filename) && rm(problem.filename) + + open(problem.filename, "w") do f + write(f, get_code(problem)) + end +end + +""" + include!(problem::Problem, incl_file) + +Include another GetDP file. +""" +function include!(problem::Problem, incl_file) + push!(problem._GETDP_CODE, "\nInclude \"$(incl_file)\";") +end + + +# """ +# write_multiple_problems(problems::Vector{Problem}, filename::String) + +# Write the GetDP code for multiple Problem instances to a single file. +# The version comment is included only once at the top, and each problem's code +# is separated by a comment indicating its index. +# """ +# function write_multiple_problems(problems::Vector{Problem}, filename::String) +# if isempty(problems) +# Base.error("No problems to write.") +# end + +# # Define the version comment once, assuming GetDP.VERSION is accessible +# version = GetDP.VERSION +# version_comment = "// This code was created by GetDP.jl v$(version).\n" + +# open(filename, "w") do f +# # Write the version comment at the top +# write(f, version_comment) + +# # Process each problem +# for (i, problem) in enumerate(problems) +# # Generate the GetDP code for this problem +# make_file!(problem) +# # Skip the version comment (first element) and join the rest +# component_code = join(problem._GETDP_CODE[2:end], "") +# # Add a separator with problem index +# write(f, "\n// Problem $i\n") +# write(f, component_code) +# end +# end +# end diff --git a/ext/fem/getdp_frontend/resolution.jl b/ext/fem/getdp_frontend/resolution.jl new file mode 100644 index 00000000..b33ded4d --- /dev/null +++ b/ext/fem/getdp_frontend/resolution.jl @@ -0,0 +1,659 @@ +# Resolution: defining how to solve the problem +using .GetDPFrontend: add_raw_code, comment, make_args + +""" + SystemItem + +A system item in a resolution. +""" +mutable struct SystemItem + name::String + formulation::String + Type::Union{String, Nothing} + Frequency::Union{String, Nothing} + comment::Union{String, Nothing} + kwargs::Dict + + function SystemItem( + name, + formulation; + Type = nothing, + Frequency = nothing, + comment = nothing, + kwargs..., + ) + new(name, formulation, Type, Frequency, comment, Dict(kwargs)) + end +end +# mutable struct SystemItem +# name::String +# formulation::String +# Type::String +# Frequency::String +# comment::Union{String,Nothing} +# kwargs::Dict + +# function SystemItem(name, formulation; Type, Frequency, comment=nothing, kwargs...) +# new(name, formulation, Type, Frequency, comment, Dict(kwargs)) +# end +# end + +function code(item::SystemItem) + c = "{ Name $(item.name); NameOfFormulation $(item.formulation); Type $(item.Type); Frequency $(item.Frequency);" + for (k, v) in item.kwargs + c *= " $k $(make_args(v, sep=","));" + end + c *= " }" + if item.comment !== nothing + c = comment(item.comment, newline = false) * "\n" * c + end + c +end + +""" + Operation + +An operation in a resolution. +""" +mutable struct Operation + operations::Vector{String} + comment::Union{String, Nothing} + + function Operation(; comment = nothing) + new(String[], comment) + end +end + +function add_operation!(op::Operation, operation::String) + push!(op.operations, operation) +end + +function code(op::Operation) + code_lines = String[] + if op.comment !== nothing + push!(code_lines, comment(op.comment, newline = false)) + end + for operation in op.operations + push!(code_lines, operation) + end + join(code_lines, "\n") +end + +""" + Resolution + +Defining how to solve the problem. +""" +mutable struct Resolution <: AbstractGetDPObject + name::String + content::String + comment::Union{String, Nothing} + indent::String + systems::Vector{SystemItem} + operation::Operation + + function Resolution() + new("Resolution", "", nothing, " "^4, SystemItem[], Operation()) + end +end + +""" + add!(resolution::Resolution, id, system_name; kwargs...) + +Add a resolution with system and operation to the Resolution object. +""" +function add!( + resolution::Resolution, + id::String, + system_name::String; + NameOfFormulation = nothing, + Type, + Frequency, + Operation, + comment = nothing, + kwargs..., +) + @warn "In future versions this function will be deprecated, causing breaking changes. Use a vector of SystemItem instead" + # Use id as resolution name, NameOfFormulation for system formulation + resolution.name = id + formulation = NameOfFormulation !== nothing ? NameOfFormulation : id + system = SystemItem( + system_name, + formulation; + Type = Type, + Frequency = Frequency, + comment = comment, + kwargs..., + ) + push!(resolution.systems, system) + + # Add operations to Operation + for op in Operation + add_operation!(resolution.operation, op) + end + + resolution.content = code(resolution) +end + +""" + add!(resolution::Resolution, id::String, systems::Vector{SystemItem}; Operation::Vector{String}) + +Add a resolution with one or more systems and an operation to the Resolution object. +""" +function add!( + resolution::Resolution, + id::String, + systems::Vector{SystemItem}; + Operation::Vector{String}, +) + # Use id as resolution name + resolution.name = id + + # Add all provided system items + for system in systems + push!(resolution.systems, system) + end + + # Add operations to Operation + for op in Operation + add_operation!(resolution.operation, op) + end + + resolution.content = code(resolution) +end + +# """ +# add!(resolution::Resolution, id, system_name; kwargs...) + +# Add a resolution with system and operation to the Resolution object. +# """ +# function add!( +# resolution::Resolution, +# id, +# system_name; +# NameOfFormulation = nothing, +# Type, +# Frequency, +# Operation, +# comment = nothing, +# kwargs..., +# ) +# # Use id as resolution name, NameOfFormulation for system formulation +# resolution.name = id +# formulation = NameOfFormulation !== nothing ? NameOfFormulation : id +# system = SystemItem( +# system_name, +# formulation; +# Type = Type, +# Frequency = Frequency, +# comment = comment, +# kwargs..., +# ) +# push!(resolution.systems, system) + +# # Add operations to Operation +# for op in Operation +# add_operation!(resolution.operation, op) +# end + +# resolution.content = code(resolution) +# end + +""" + add_raw_code!(resolution::Resolution, raw_code, newline=true) + +Add raw code to the Resolution object. +""" +function add_raw_code!(resolution::Resolution, raw_code, newline = true) + resolution.content = add_raw_code(resolution.content, raw_code, newline) +end + +""" + add_comment!(resolution::Resolution, comment_text, newline=true) + +Add a comment to the Resolution object. +""" +function add_comment!(resolution::Resolution, comment_text, newline = true) + add_raw_code!(resolution, comment(comment_text, newline = false), newline) +end + +""" + code(resolution::Resolution) + +Generate GetDP code for a Resolution object. +""" +function code(resolution::Resolution) + code_lines = ["Resolution {"] + push!(code_lines, " { Name $(resolution.name);") + push!(code_lines, " System {") + for system in resolution.systems + system_code = code(system) + for line in split(system_code, '\n') + push!(code_lines, " " * line) + end + end + push!(code_lines, " }") + push!(code_lines, " Operation {") + operation_code = code(resolution.operation) + for line in split(operation_code, '\n') + push!(code_lines, " " * line * ";") + end + push!(code_lines, " }") + push!(code_lines, " }") + push!(code_lines, "}") + join(code_lines, "\n") * "\n" +end +# System struct +mutable struct System + items::Vector{SystemItem} + code::String + comment::Union{String, Nothing} + + function System(; comment = nothing, kwargs...) + c = "System { " + if comment !== nothing + c *= GetDP.comment(comment) + end + c *= "\n }" + new(SystemItem[], c, comment) + end +end + +function add!(system::System, name, formulation; kwargs...) + item = SystemItem(name, formulation; kwargs...) + s = system.code + n = 10 # Length of "\n }" + system.code = s[1:(end-n)] * "\n " * item.code * s[(end-n+1):end] + push!(system.items, item) + return item +end + +# Operation methods +function generate(op::Operation, system_id) + return "Generate[$system_id]" +end + +function solve(op::Operation, system_id) + return "Solve[$system_id]" +end + +function solve_again(op::Operation, system_id) + return "SolveAgain[$system_id]" +end + +function set_global_solver_options(op::Operation, char_expression) + return "SetGlobalSolverOptions['$char_expression']" +end + +function generate_jac(op::Operation, system_id) + return "GenerateJac[$system_id]" +end + +function solve_jac(op::Operation, system_id) + return "SolveJac[$system_id]" +end + +function generate_separate(op::Operation, system_id) + return "GenerateSeparate[$system_id]" +end + +function generate_only(op::Operation, system_id, expression_cst_list) + return "GenerateOnly[$system_id, $(to_getdp_list(expression_cst_list))]" +end + +function generate_only_jac(op::Operation, system_id, expression_cst_list) + return "GenerateOnlyJac[$system_id, $(to_getdp_list(expression_cst_list))]" +end + +function generate_group(op::Operation) + return "GenerateGroup[]" +end + +function generate_right_hand_side_group(op::Operation) + return "GenerateRightHandSdeGroup[]" +end + +function update(op::Operation, system_id; expression = nothing) + if expression !== nothing + return "Update[$system_id, $expression]" + else + return "Update[$system_id]" + end +end + +function update_constraint(op::Operation, system_id, group_id, constraint_type) + return "UpdateConstraint[$system_id, $group_id, $constraint_type]" +end + +function get_residual(op::Operation, system_id, variable_id) + return "GetResidual[$system_id, $variable_id]" +end + +function get_norm_solution(op::Operation, system_id, variable_id) + return "GetNormSolution[$system_id, $variable_id]" +end + +function get_norm_right_hand_side(op::Operation, system_id, variable_id) + return "GetNormRightHandSide[$system_id, $variable_id]" +end + +function get_norm_residual(op::Operation, system_id, variable_id) + return "GetNormResidual[$system_id, $variable_id]" +end + +function get_norm_increment(op::Operation, system_id, variable_id) + return "GetNormIncrement[$system_id, $variable_id]" +end + +function swap_solution_and_residual(op::Operation, system_id) + return "SwapSolutionAndResidual[$system_id]" +end + +function swap_solution_and_right_hand_side(op::Operation, system_id) + return "SwapSolutionAndRightHandSide[$system_id]" +end + +function init_solution(op::Operation, system_id) + return "InitSolution[$system_id]" +end + +function init_solution1(op::Operation, system_id) + return "InitSolution1[$system_id]" +end + +function create_solution(op::Operation, system_id; expression_cst = nothing) + if expression_cst !== nothing + return "CreateSolution[$system_id, $expression_cst]" + else + return "CreateSolution[$system_id]" + end +end + +function apply(op::Operation, system_id) + return "Apply[$system_id]" +end + +function set_solution_as_right_hand_side(op::Operation, system_id) + return "SetSolutionAsRightHandSide[$system_id]" +end + +function set_right_hand_side_as_solution(op::Operation, system_id) + return "SetRightHandSideAsSolution[$system_id]" +end + +function residual(op::Operation, system_id) + return "Residual[$system_id]" +end + +function copy_solution(op::Operation, system_id, expression; reverse = false) + expr = isa(expression, Array) ? "$(expression[1])()" : "'$expression'" + if reverse + return "CopySolution[$expr, $system_id]" + else + return "CopySolution[$system_id, $expr]" + end +end + +function copy_right_hand_side(op::Operation, system_id, expression; reverse = false) + expr = isa(expression, Array) ? "$(expression[1])()" : "'$expression'" + if reverse + return "CopyRightHandSide[$expr, $system_id]" + else + return "CopyRightHandSide[$system_id, $expr]" + end +end + +function copy_residual(op::Operation, system_id, expression; reverse = false) + expr = isa(expression, Array) ? "$(expression[1])()" : "'$expression'" + if reverse + return "CopyResidual[$expr, $system_id]" + else + return "CopyResidual[$system_id, $expr]" + end +end + +function save_solution(op::Operation, system_id) + return "SaveSolution[$system_id]" +end + +function save_solutions(op::Operation, system_id) + return "SaveSolutions[$system_id]" +end + +function remove_last_solution(op::Operation, system_id) + return "RemoveLastSolution[$system_id]" +end + +function transfer_solution(op::Operation, system_id) + return "TransferSolution[$system_id]" +end + +function transfer_init_solution(op::Operation, system_id) + return "TransferInitSolution[$system_id]" +end + +function evaluate(op::Operation, expression_list) + expr = + isa(expression_list, Array) ? join(string.(expression_list), ", ") : + string(expression_list) + return "Evaluate[$expr]" +end + +function set_time(op::Operation, expression) + return "SetTime[$expression]" +end + +function set_time_step(op::Operation, expression) + return "SetTimeStep[$expression]" +end + +function set_dtime(op::Operation, expression) + return "SetDTime[$expression]" +end + +function set_frequency(op::Operation, system_id, expression) + return "SetFrequency[$system_id, $expression]" +end + +function system_command(op::Operation, expression_char) + return "SystemCommand['$expression_char']" +end + +function error(expression_char) + return "Error['$expression_char']" +end + +function test(op::Operation, expression, resolution_op_1; resolution_op_2 = nothing) + if resolution_op_2 !== nothing + return "Test[$expression {$resolution_op_1} {$resolution_op_2}]" + else + return "Test[$expression {$resolution_op_1}]" + end +end + +function while_loop(op::Operation, expression, resolution_op) + return "While[$expression {$resolution_op}]" +end + +function Break(op::Operation) + return "Break[]" +end + +function sleep(op::Operation, expression) + return "Sleep[$expression]" +end + +function set_extrapolation_order(op::Operation, expression_cst) + return "SetExtrapolationOrder[$expression_cst]" +end + +function print_expr(op::Operation, expression_list; file = nothing, format = nothing) + expr = to_getdp_list(expression_list) + if format !== nothing && file !== nothing + return "Print[$expr, File $file, Format $format]" + else + return "Print[$expr]" + end +end + +function print_sys( + op::Operation, + system_id; + file = nothing, + dof_list = nothing, + timestep = nothing, +) + s = "Print[$system_id" + for (k, v) in Dict("File" => file, "dof_list" => dof_list, "TimeStep" => timestep) + if v !== nothing + if k == "dof_list" + s *= ", $(to_getdp_list(v))" + else + s *= ", $k $v" + end + end + end + s *= "]" + return s +end + +function eigen_solve(op::Operation, system_id, neig, shift_re, shift_im; filter = nothing) + if filter !== nothing + return "EigenSolve[$system_id, $neig, $shift_re, $shift_im, $filter]" + else + return "EigenSolve[$system_id, $neig, $shift_re, $shift_im]" + end +end + +function fourier_transform(op::Operation, system_id, system_id_dest, freq_list) + return "FourierTransform[$system_id, $system_id_dest, $(to_getdp_list(freq_list))]" +end + +function post_operation(op::Operation, post_operation_id) + return "PostOperation[$post_operation_id]" +end + +function gmsh_read(op::Operation, filename; tag = nothing) + if tag !== nothing + return "GmshRead['$filename', $tag]" + else + return "GmshRead['$filename']" + end +end + +function gmsh_write(op::Operation, filename, field) + return "GmshWrite['$filename', $field]" +end + +function gmsh_clear_all(op::Operation) + return "GmshClearAll[]" +end + +function delete_file(op::Operation, filename) + return "DeleteFile['$filename']" +end + +function rename_file(op::Operation, filename, field) + return "RenameFile['$filename', '$field']" +end + +function create_directory(op::Operation, dirname) + return "CreateDirectory['$dirname']" +end + +function mpi_set_comm_self(op::Operation) + return "MPI_SetCommSelf[]" +end + +function mpi_set_comm_world(op::Operation) + return "MPI_SetCommWorld[]" +end + +function mpi_barrier(op::Operation) + return "MPI_Barrier[]" +end + +function mpi_broadcast_fields(op::Operation; file_list = nothing) + if file_list !== nothing + return "MPI_BroadcastFields[$(to_getdp_list(file_list))]" + else + return "MPI_BroadcastFields[]" + end +end + +function mpi_broadcast_variables(op::Operation) + return "MPI_BroadcastVariables[]" +end + +# Placeholder for loop methods (to be expanded based on additional details) +function add_time_loop_theta(op::Operation; kwargs...) + loop_code = "TimeLoopTheta { // Placeholder }" + push!(op.loops, loop_code) + return loop_code +end + +function add_time_loop_newmark(op::Operation; kwargs...) + loop_code = "TimeLoopNewmark { // Placeholder }" + push!(op.loops, loop_code) + return loop_code +end + +function add_iterative_loop(op::Operation; kwargs...) + loop_code = "IterativeLoop { // Placeholder }" + push!(op.loops, loop_code) + return loop_code +end + +# function code(op::Operation) #CHANGE +# code_lines = ["Operation {"] +# for operation in op.operations +# push!(code_lines, " $operation;") +# end +# push!(code_lines, "}") +# join(code_lines, "\n") +# end + + +# ResolutionItem struct +mutable struct ResolutionItem + name::String + code::String + items::Vector{Union{Operation, System}} + comment::Union{String, Nothing} + kwargs::Dict + + function ResolutionItem(name; comment = nothing, kwargs...) + c = "{ Name $name" + for (k, v) in kwargs + c *= "; $k $(make_args(v, sep=","))" + end + c *= "; " + if comment !== nothing + c *= GetDP.comment(comment) + end + c *= "\n}" + new(name, c, Union{Operation, System}[], comment, Dict(kwargs)) + end +end + +function add_operation!(item::ResolutionItem; kwargs...) + op = Operation(; kwargs...) + item.code = item.code[1:(end-2)] * "\n " * op.code * item.code[(end-1):end] + push!(item.items, op) + return op +end + +function add_system!(item::ResolutionItem; comment = nothing, kwargs...) + sys = System(; comment, kwargs...) + item.code = item.code[1:(end-2)] * "\n " * sys.code * item.code[(end-1):end] + push!(item.items, sys) + return sys +end + +function code(item::ResolutionItem) + s = item.code + for case in item.items + s = s[1:(end-2)] * "\n " * code(case) * s[(end-1):end] + end + return s +end diff --git a/integration/fem/README.md b/integration/fem/README.md new file mode 100644 index 00000000..d3aee3b6 --- /dev/null +++ b/integration/fem/README.md @@ -0,0 +1,16 @@ +# FEM release gate + +This directory is intentionally outside `test/`. Routine package tests and +coverage do not discover it. + +Run the gate with Julia 1.12 after installing Gmsh in the active environment and +setting `GETDP_EXECUTABLE` to a GetDP 3.5.0 executable: + +```sh +GETDP_EXECUTABLE=/absolute/path/to/getdp \ + julia --project=. -e 'using Pkg; Pkg.activate(; temp=true); Pkg.develop(path=pwd()); Pkg.add("Gmsh"); include("integration/fem/runtests.jl")' +``` + +The manual GitHub workflow downloads the official Linux archive and checks +SHA-256 `d3c28fa18f20d6147b4c7367d4dd802e9f7ddb58c608688bbb71919dbca8041d` +before running the 19 assertions with coverage disabled. diff --git a/integration/fem/runtests.jl b/integration/fem/runtests.jl new file mode 100644 index 00000000..9812d43f --- /dev/null +++ b/integration/fem/runtests.jl @@ -0,0 +1,190 @@ +using Test +using Gmsh +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 + +const TEST_TOL = 1e-8 + +@testset "FEM integration" 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/showcase/showcase1.jl b/showcase/showcase1.jl index a1538845..004a99b7 100644 --- a/showcase/showcase1.jl +++ b/showcase/showcase1.jl @@ -7,7 +7,7 @@ #> 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" @@ -17,211 +17,87 @@ 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 + #! 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; + using Pkg + Pkg.activate(joinpath(@__DIR__, "..")) + Pkg.instantiate() +end # ╔═╡ 3183ff8d-a9fd-4035-af8a-664bec3606d4 begin - binder_bootstrap!() - using Makie, PlutoUI, Colors - using LineCableModels - using DataFrames - using HypertextLiteral + using CairoMakie, 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 + using Measurements: measurement, value + + # mm + percent → Measurement with absolute σ = (pct/100)*nom + function _with_unc(nom_mm::Real, pct::Real) + measurement(nom_mm/1000, abs(nom_mm/1000) * pct/100) + end + + # 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 @@ -287,138 +163,137 @@ ul#recent{ max-height: none; } """) # ╔═╡ 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)...) + 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 + function 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)) + ) + end + + # 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 + + function LocalImage(x::AbstractString; attrs...) + LocalResource(joinpath(@__DIR__, "assets", "img", x), pairs(attrs)...) + end end; # ╔═╡ e90baf94-c8b8-41aa-8728-e129f7f6881e -@htl( - """ - - - -""" -) - +@htl(""" + + + + """) # ╔═╡ 532cb61b-97b6-43e7-a8f9-3a5f12b8b3f7 @htl(""" @@ -426,7 +301,7 @@ end; /* Nuke the default slideshow arrows (both normal & presentation DOMs) */ #slide_controls, #presentation #slide_controls { - display: none !important; + display: none !important; } @@ -435,11 +310,11 @@ end; const hide = (n) => { try { n.style.display = "none"; n.hidden = true } catch {} } const sc0 = document.getElementById("slide_controls"); if (sc0) hide(sc0) const mo = new MutationObserver(muts => { - for (const m of muts) for (const el of m.addedNodes) { - if (el.nodeType !== 1) continue - if (el.id === "slide_controls") hide(el) - const sc = el.querySelector?.("#slide_controls"); if (sc) hide(sc) - } + for (const m of muts) for (const el of m.addedNodes) { + if (el.nodeType !== 1) continue + if (el.id === "slide_controls") hide(el) + const sc = el.querySelector?.("#slide_controls"); if (sc) hide(sc) + } }) mo.observe(document.body, { childList: true, subtree: true }) @@ -447,159 +322,156 @@ end; # ╔═╡ b16ff72c-872a-4505-9468-6cefd4a8852c -@htl( - """ - - - - - - - -""" -) - +@htl(""" + + + + + + + + """) # ╔═╡ 9fefeafa-63f9-43d0-a2ee-4d4fca170126 begin - @htl(""" - - """) - - anchor(id::AbstractString = "home"; offset_px::Real = 0) = @htl(""" - - """) + @htl(""" + + """) + + anchor(id::AbstractString = "home"; offset_px::Real = 0) = @htl(""" + + """) end; # ╔═╡ fde80e93-1964-4287-acfc-a2da2d4b7d48 @@ -650,30 +522,30 @@ md""" # ╔═╡ 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;")) - """) + 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""" @@ -682,24 +554,24 @@ md""" # ╔═╡ 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;")) - """) + 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""" @@ -709,29 +581,29 @@ md""" # ╔═╡ 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) + 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""" @@ -746,22 +618,21 @@ md""" # ╔═╡ 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) - + 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""" @@ -770,49 +641,49 @@ md""" # ╔═╡ 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 + 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 @@ -824,29 +695,29 @@ md""" # ╔═╡ 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;")) - """) + 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] + 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 @@ -859,7 +730,6 @@ md""" # Initialize materials library with default values: materials = MaterialsLibrary(add_defaults = true) - # ╔═╡ 062439db-1e3f-497e-96c1-e1f65f80399b md""" ## Base RLC quantities @@ -883,76 +753,70 @@ Outer semicon thickness [mm]: $(@bind tt_sc_out PlutoUI.Slider(0.1:0.01:3; defau # ╔═╡ 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 - + 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 @@ -972,8 +836,8 @@ md""" # ╔═╡ 9ddcccbe-86c8-4335-8d65-35af4ce755ab begin - core_df = DataFrame(cable_design, :baseparams) - core_df + core_df = DataFrame(cable_design, :baseparams) + core_df end # ╔═╡ 43ff64cb-1226-4d26-9fdf-8aff03505439 @@ -981,13 +845,13 @@ cable_emt = equivalent(cable_design) # ╔═╡ ae1749c8-0f6d-4487-8857-12826eb57db3 begin - plt2, _ = preview(cable_design; size = (800, 500), backend = backend_sym) + 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 + plt3, _ = preview(cable_emt; size = (800, 500), backend = backend_sym) + plt3 end # ╔═╡ fd1e268a-6520-4dc8-a9ff-32a4854859df @@ -1002,9 +866,9 @@ md""" # ╔═╡ 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 + # 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 @@ -1014,34 +878,34 @@ md""" # ╔═╡ 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) + 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)) + 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 + plt4, _ = preview( + cable_system, + earth_model = earth_params, + zoom_factor = 2.0, + size = (800, 500) + ) + plt4 end # ╔═╡ 39f7460d-8a1e-483d-94f4-14500d6c9ac2 @@ -1064,218 +928,211 @@ Outer semicon thickness [mm]: $(@bind ttt_sc_out PlutoUI.Slider(0.1:0.01:3; defa # ╔═╡ 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)) - + 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 - ) + 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, - ) + 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 + 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) + @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 - + 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 @@ -1288,24 +1145,24 @@ md""" # ╔═╡ 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. -
  • +
  • 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. -
  • + Additional tests and validations using the FEM solver. +
- """ + """ # ╔═╡ 7d77f069-930b-4451-ab7d-0e77b8fd86a7 md""" diff --git a/showcase/start.jl b/showcase/start.jl index 2ec261a6..3c64d79a 100644 --- a/showcase/start.jl +++ b/showcase/start.jl @@ -9,18 +9,18 @@ using Pluto println("Starting Pluto, check your browser...") try - # Run Pluto, telling it which notebook to open - Pluto.run(launch_browser = true) + # 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) + # 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()) + println("\nAn error occurred while trying to run Pluto:") + showerror(stdout, e) + Base.show_backtrace(stdout, catch_backtrace()) finally - println("\nPluto server stopped.") + println("\nPluto server stopped.") end println("Launcher script finished.") diff --git a/src/LineCableModels.jl b/src/LineCableModels.jl index 3a3dae8f..d0aa3b2b 100644 --- a/src/LineCableModels.jl +++ b/src/LineCableModels.jl @@ -9,7 +9,8 @@ export add!, set_verbosity!, set_backend! export Material, MaterialsLibrary # Data model (design + system): -export Thickness, Diameter, WireArray, Strip, Tubular, Semicon, Insulator, Sector, SectorParams, SectorInsulator +export Thickness, Diameter, WireArray, Strip, Tubular, Semicon, Insulator, Sector, + SectorParams, SectorInsulator export ConductorGroup, InsulatorGroup export CableComponent, CableDesign, NominalData export CablesLibrary @@ -21,8 +22,8 @@ export EarthModel # Engine: export LineParametersProblem, - FormulationSet, compute!, SeriesImpedance, ShuntAdmittance, per_km, per_m, kronify, - LineParameters, PhaseDomain, ModalDomain + FormulationSet, compute!, SeriesImpedance, ShuntAdmittance, per_km, per_m, kronify, + LineParameters, PhaseDomain, ModalDomain # Parametric builder: # export make_stranded, make_screened @@ -65,14 +66,16 @@ using .EarthProps: EarthModel # 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 + Insulator, ConductorGroup, InsulatorGroup, CableComponent, CableDesign, + NominalData, + CablesLibrary, CablePosition, LineCableSystem, trifoil_formation, + flat_formation, + preview, equivalent, MaxFill, Sector, SectorParams, SectorInsulator # Submodule `Engine` include("engine/Engine.jl") using .Engine: LineParametersProblem, compute!, LineParameters, SeriesImpedance, - ShuntAdmittance, per_km, per_m, kronify, FormulationSet + ShuntAdmittance, per_km, per_m, kronify, FormulationSet # Submodule `ParametricBuilder` include("parametricbuilder/ParametricBuilder.jl") @@ -88,4 +91,4 @@ using .ImportExport: export_data, load!, save const WireArray = CircStrands # alias for now export WireArray # export aliases -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 b1d0834a..00000000 --- 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 81527814..00000000 --- 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 907a661e..00000000 --- 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 81590db0..00000000 --- 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 26ced1bc..00000000 --- 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 096767e7..00000000 --- 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 c5ea1deb..00000000 --- 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 fcadf291..00000000 --- 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 69cb9a51..00000000 --- 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 f841bf88..00000000 --- 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 58074237..00000000 --- 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 e499451c..00000000 --- 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 81dc4da5..00000000 --- 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 15a30921..00000000 --- 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 18dbd6a4..00000000 --- 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 93b560c3..00000000 --- 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 780fb2e3..f542a9d6 100644 --- a/src/commons/Commons.jl +++ b/src/commons/Commons.jl @@ -3,7 +3,6 @@ module Commons include("docstringextension.jl") include("consts.jl") - export get_description, add!, domain, LineParamsDomain, PhaseDomain, ModalDomain function get_description end @@ -22,4 +21,4 @@ Fallback returns `nothing` for domainless objects. @inline domain(::Type) = nothing @inline domain(x) = domain(typeof(x)) -end \ No newline at end of file +end diff --git a/src/commons/consts.jl b/src/commons/consts.jl index c58b7b1d..ee963126 100644 --- a/src/commons/consts.jl +++ b/src/commons/consts.jl @@ -23,4 +23,3 @@ 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}}} - diff --git a/src/commons/docstringextension.jl b/src/commons/docstringextension.jl index 82b7ba68..94c45be5 100644 --- a/src/commons/docstringextension.jl +++ b/src/commons/docstringextension.jl @@ -1,7 +1,9 @@ using Pkg -using DocStringExtensions: DocStringExtensions, SIGNATURES, TYPEDSIGNATURES, TYPEDEF, TYPEDFIELDS, FIELDS, FUNCTIONNAME, IMPORTS, EXPORTS +using DocStringExtensions: DocStringExtensions, SIGNATURES, TYPEDSIGNATURES, TYPEDEF, + TYPEDFIELDS, FIELDS, FUNCTIONNAME, IMPORTS, EXPORTS -export SIGNATURES, TYPEDSIGNATURES, TYPEDEF, TYPEDFIELDS, FIELDS, FUNCTIONNAME, METHODLIST, IMPORTS, EXPORTS +export SIGNATURES, TYPEDSIGNATURES, TYPEDEF, TYPEDFIELDS, FIELDS, FUNCTIONNAME, METHODLIST, + IMPORTS, EXPORTS """ Override `DocStringExtensions.format` for `METHODLIST`. @@ -16,7 +18,7 @@ function DocStringExtensions.format(::_CleanMethodList, buf, doc) local typesig = doc.data[:typesig] local modname = doc.data[:module] local func = Docs.resolve(binding) - local groups = DocStringExtensions.methodgroups(func, typesig, modname; exact=false) + local groups = DocStringExtensions.methodgroups(func, typesig, modname; exact = false) if !isempty(groups) println(buf) local pkg_root = Pkg.pkgdir(modname) # Use Pkg.pkgdir here @@ -34,15 +36,14 @@ function DocStringExtensions.format(::_CleanMethodList, buf, doc) 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 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 @@ -51,4 +52,4 @@ function DocStringExtensions.format(::_CleanMethodList, buf, doc) println(buf) end return nothing -end \ No newline at end of file +end diff --git a/src/datamodel/DataModel.jl b/src/datamodel/DataModel.jl index 4d964a91..19665b96 100644 --- a/src/datamodel/DataModel.jl +++ b/src/datamodel/DataModel.jl @@ -1,5 +1,5 @@ """ - 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. @@ -15,9 +15,6 @@ The [`DataModel`](@ref) module provides data structures, constructors and utilit $(IMPORTS) -# Exports - -$(EXPORTS) """ module DataModel @@ -36,24 +33,21 @@ export preview, equivalent using ..Commons import ..Commons: add! using ..Utils: - resolve_T, to_certain, to_nominal, is_headless, - is_in_testset, to_lower, to_upper + 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 + 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 +using GeometryBasics: Point, Point2f # Abstract types & interfaces include("types.jl") include("radii.jl") @@ -81,7 +75,6 @@ include("semicon.jl") include("insulatorgroup.jl") include("sectorinsulator.jl") - # Groups include("nominaldata.jl") include("cablecomponent.jl") @@ -93,10 +86,26 @@ include("linecablesystem.jl") # Helpers & overrides include("helpers.jl") -include("preview.jl") include("io.jl") include("typecoercion.jl") +""" + preview(object; kwargs...) + +Preview a cable design or cable system with a loaded Makie backend. + +Load `CairoMakie`, `GLMakie`, or `WGLMakie` before calling this function. +""" +function preview end + +function preview(args...; kwargs...) + throw( + ArgumentError( + "Plotting is optional. Load CairoMakie, GLMakie, or WGLMakie before calling preview.", + ), + ) +end + # Aliases for backward compatibility const WireArray = CircStrands export WireArray diff --git a/src/datamodel/baseparams/BaseParams.jl b/src/datamodel/baseparams/BaseParams.jl index e141eba3..b33d48bd 100644 --- a/src/datamodel/baseparams/BaseParams.jl +++ b/src/datamodel/baseparams/BaseParams.jl @@ -1,5 +1,5 @@ """ - 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. @@ -16,9 +16,6 @@ The [`BaseParams`](@ref) submodule provides fundamental functions for determinin $(IMPORTS) -# Exports - -$(EXPORTS) """ module BaseParams @@ -51,12 +48,13 @@ 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: +# Notes + ```math \\alpha_{eq} = \\frac{\\alpha_1 R_2 + \\alpha_2 R1}{R_1 + R_2} ``` @@ -75,27 +73,28 @@ where ``\\alpha_1``, ``\\alpha_2`` are the temperature coefficients of the condu # Examples -```julia +```jldoctest 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) +alpha_eq = calc_equivalent_alpha(alpha_conductor, R_conductor, alpha_new_part, R_new_part) +@assert alpha_conductor < alpha_eq < alpha_new_part +# output ``` """ function calc_equivalent_alpha(alpha1::T, R1::T, alpha2::T, R2::T) where {T <: REALSCALAR} - return (alpha1 * R2 + alpha2 * R1) / (R1 + R2) + 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), - ) + 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 """ @@ -103,6 +102,8 @@ $(TYPEDSIGNATURES) Calculates the parallel equivalent of two impedances (or series equivalent of two admittances): +# Notes + ```math Z_{eq} = \\frac{Z_1 Z_2}{Z_1 + Z_2} ``` @@ -130,45 +131,43 @@ where ``R_{\\text{dc}}`` is the DC resistance, ``d`` is the diameter of each wir # Examples -```julia +```jldoctest Z1 = 5.0 Z2 = 10.0 -Req = $(FUNCTIONNAME)(Z1, Z2) -println(Req) # Outputs: 3.3333333333333335 +Req = calc_parallel_equivalent(Z1, Z2) +@assert Req ≈ 10 / 3 +# output ``` -# See also - -- [`calc_helical_params`](@ref) """ function calc_parallel_equivalent( - Z1::T, - Z2::T, + 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) + # 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), - ) + T = resolve_T(Z1, Z2) + return calc_parallel_equivalent( + coerce_to_T(Z1, T), + coerce_to_T(Z2, T) + ) end """ @@ -205,41 +204,42 @@ Reference values for `lay_ratio` are given under standard EN 50182 [CENELEC50182 | 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 | +| ACCC/TW | - | 36 (8/12/16) | - | 15/13.5/11.5 | # Examples -```julia +```jldoctest 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] +mean_diam, pitch, overlength = calc_helical_params(r_in, r_ex, lay_ratio) +@assert mean_diam ≈ 0.025 +@assert pitch ≈ 0.3 +@assert overlength > 1.0 +# output ``` """ function calc_helical_params( - r_in::T, - r_ex::T, - lay_ratio::T, + 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 + 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 + 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), - ) + 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 """ @@ -247,6 +247,8 @@ $(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: +# Notes + ```math R = \\rho \\frac{\\ell}{W T} ``` @@ -267,44 +269,41 @@ where ``\\ell`` is the length of the strip, ``W`` is the width, and ``T`` is the # Examples -```julia +```jldoctest 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 Ω +resistance = calc_strip_resistance(thickness, width, rho, alpha, T0, T) +@assert resistance > 0 +# output ``` -# See also - -- [`calc_temperature_correction`](@ref) """ function calc_strip_resistance( - thickness::T, - width::T, - rho::T, - alpha::T, - T0::T, - Top::T, + 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 + 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), - ) + 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 """ @@ -312,10 +311,12 @@ $(TYPEDSIGNATURES) Calculates the temperature correction factor for material properties based on the standard linear temperature model [cigre345](@cite): +# Notes + ```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. +where ``\\alpha`` is the temperature coefficient of the material resistivity, ``T`` is the operating temperature, and ``T_0`` is the reference temperature. # Arguments @@ -329,28 +330,30 @@ where ``\\alpha`` is the temperature coefficient of the material resistivity, `` # Examples -```julia - # Copper resistivity correction (alpha = 0.00393 [1/°C]) - k = $(FUNCTIONNAME)(0.00393, 75.0, 20.0) # Expected output: 1.2161 +```jldoctest +# Copper resistivity correction (alpha = 0.00393 [1/°C]) +k = calc_temperature_correction(0.00393, 75.0, 20.0) +@assert k ≈ 1.21615 +# output ``` """ 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) + @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), - ) + 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 """ @@ -358,6 +361,8 @@ $(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}``: +# Notes + ```math R = \\rho \\frac{\\ell}{\\pi (r_{ext}^2 - r_{in}^2)} ``` @@ -378,43 +383,41 @@ where ``\\ell`` is the length of the conductor, ``r_{in}`` and ``r_{ext}`` are t # Examples -```julia +```jldoctest 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 Ω +resistance = calc_tubular_resistance(r_in, r_ex, rho, alpha, T0, T) +@assert resistance > 0 +# output ``` -# See also - -- [`calc_temperature_correction`](@ref) """ function calc_tubular_resistance( - r_in::T, - r_ex::T, - rho::T, - alpha::T, - T0::T, - Top::T, + 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 + 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), - ) + 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 """ @@ -422,6 +425,8 @@ $(TYPEDSIGNATURES) Calculates the inductance of a tubular conductor per unit length, disregarding skin-effects (DC approximation) [916943](@cite) [cigre345](@cite) [1458878](@cite): +# Notes + ```math L = \\frac{\\mu_r \\mu_0}{2 \\pi} \\log \\left( \\frac{r_{ext}}{r_{in}} \\right) ``` @@ -439,33 +444,31 @@ where ``\\mu_r`` is the relative permeability of the conductor material, ``\\mu_ # Examples -```julia +```jldoctest 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 +L = calc_tubular_inductance(r_in, r_ex, mu_r) +@assert L > 0 +# output ``` -# See also - -- [`calc_tubular_resistance`](@ref) """ function calc_tubular_inductance( - r_in::T, - r_ex::T, - mu_r::T, + r_in::T, + r_ex::T, + mu_r::T ) where {T <: REALSCALAR} - return mu_r * μ₀ / (2 * π) * log(r_ex / r_in) + 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), - ) + 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 """ @@ -484,52 +487,62 @@ Calculates the center coordinates of wires arranged in a circular pattern. - Vector of tuples, where each tuple contains the `(x, y)` coordinates \\[m\\] of the center of a wire. +# Notes + +For wire index ``i = 0, \\ldots, N-1`` and layout radius +``r_l = r_{in} + r_w``, the implementation uses + +```math +x_i = C_x + r_l \\cos\\left(\\frac{2\\pi i}{N}\\right), \\qquad +y_i = C_y + r_l \\sin\\left(\\frac{2\\pi i}{N}\\right). +``` + +For a single wire, ``r_l`` is set to zero. + # Examples -```julia +```jldoctest # 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 +wire_coords = calc_circstrands_coords(7, 0.002, 0.01) +@assert length(wire_coords) == 7 # Create a wire array with custom center position -wire_coords = $(FUNCTIONNAME)(7, 0.002, 0.01, C=(0.5, 0.3)) +wire_coords = calc_circstrands_coords(7, 0.002, 0.01, C=(0.5, 0.3)) +@assert first(wire_coords)[1] ≈ 0.512 +# output ``` -# See also - -- [`LineCableModels.DataModel.CircStrands`](@ref) """ function calc_circstrands_coords( - num_wires::U, - radius_wire::T, - r_in::T, - C::Tuple{T, T}, + 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 + 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, - ) + 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 """ @@ -537,6 +550,8 @@ $(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: +# Notes + ```math Z_d = \\left[Z_a - Z_x\\right] - \\frac{\\left( Z_m - Z_x \\right)^2}{Z_s - Z_x} ``` @@ -565,114 +580,111 @@ where ``Z_a``, ``Z_s`` are the self impedances of the core conductor and the scr # 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 +```jldoctest +L = calc_inductance_trifoil(0.01, 0.015, 1.72e-8, 1.0, 0.02, 0.025, 2.83e-8, 1.0, 0.1; rho_e=50.0, f=50.0) +@assert isfinite(L) +# output ``` -# 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, + 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π) - ω = 2 * π * f - C = μ₀ / (2π) + # Compute simplified earth return depth + DE = 659.0 * sqrt(rho_e / f) - # Compute simplified earth return depth - DE = 659.0 * sqrt(rho_e / f) + # Compute R'_E + RpE = (ω * μ₀) / 8.0 - # 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) - # 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 - # 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) - # 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 - # 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 - # Mutual impedance between phase conductor and screen - Zm = RpE + im * Xs + # Compute GMD + GMD = S # trifoil, for flat use: 2^(1/3) * S - # Compute GMD - GMD = S # trifoil, for flat use: 2^(1/3) * S + # Compute Xap + Xap = (ω * C) * log(DE / GMD) - # Compute Xap - Xap = (ω * C) * log(DE / GMD) + # Equivalent mutual impedances between cables + Zx = RpE + im * Xap - # 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)) - # 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) / ω + # 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₀, + 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), - ) + 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 """ @@ -680,6 +692,8 @@ $(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): +# Notes + ```math GMR = \\sqrt[n] {r n a^{n-1}} ``` @@ -699,34 +713,35 @@ where ``a`` is the layout radius, ``n`` is the number of wires, and ``r`` is the # Examples -```julia +```jldoctest 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] +gmr = calc_circstrands_gmr(lay_rad, N, rad_wire, mu_r) +@assert gmr > 0 +# output ``` """ function calc_circstrands_gmr( - lay_rad::T, - N::Int, - rad_wire::T, - mu_r::T, + 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) + 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), - ) + 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 """ @@ -734,6 +749,8 @@ $(TYPEDSIGNATURES) Calculates the geometric mean radius (GMR) of a tubular conductor, using [6521501](@cite): +# Notes + ```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] ``` @@ -756,52 +773,52 @@ where ``\\mu_r`` is the material magnetic permeability (relative to free space), # Examples -```julia +```jldoctest 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] +gmr = calc_tubular_gmr(r_ex, r_in, mu_r) +@assert r_in < gmr < r_ex +# output ``` """ 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 + 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), - ) + 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 """ @@ -839,46 +856,44 @@ Assumes a tubular geometry for the conductor, reducing to the solid case if `r_i # Examples -```julia +```jldoctest 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] +mu_r = calc_equivalent_mu(gmr, r_ex, r_in) +@assert mu_r > 0 +# output ``` -# 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 + 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), - ) + 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 """ @@ -886,6 +901,8 @@ $(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): +# Notes + ```math C = \\frac{2 \\pi \\varepsilon_0 \\varepsilon_r}{\\log \\left(\\frac{r_{ext}}{r_{in}}\\right)} ``` @@ -903,29 +920,30 @@ where ``\\varepsilon_0`` is the vacuum permittivity, ``\\varepsilon_r`` is the r # Examples -```julia +```jldoctest 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] +capacitance = calc_shunt_capacitance(r_in, r_ex, epsr) +@assert capacitance > 0 +# output ``` """ function calc_shunt_capacitance( - r_in::T, - r_ex::T, - epsr::T, + r_in::T, + r_ex::T, + epsr::T ) where {T <: REALSCALAR} - return 2 * π * ε₀ * epsr / log(r_ex / r_in) + 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), - ) + 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 """ @@ -933,6 +951,8 @@ $(TYPEDSIGNATURES) Calculates the shunt conductance per unit length of a coaxial structure, using the improved model reported in [916943](@cite) [Karmokar2025](@cite) [4389974](@cite): +# Notes + ```math G = \\frac{2\\pi\\sigma}{\\log(\\frac{r_{ext}}{r_{in}})} ``` @@ -950,33 +970,35 @@ where ``\\sigma = \\frac{1}{\\rho}`` is the conductivity of the dielectric/semic # Examples -```julia +```jldoctest 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] +g = calc_shunt_conductance(r_in, r_ex, rho) +@assert g > 0 +# output ``` """ function calc_shunt_conductance(r_in::T, r_ex::T, rho::T) where {T <: REALSCALAR} - return 2 * π * (1 / rho) / log(r_ex / r_in) + 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), - ) + 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): +# Notes + ```math GMR_{eq} = {GMR_{i-1}}^{\\beta^2} \\cdot {GMR_{i}}^{(1-\\beta)^2} \\cdot {GMD}^{2\\beta(1-\\beta)} ``` @@ -997,46 +1019,35 @@ where: - 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, + 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)) + 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 +# 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 + DM = parentmodule(@__MODULE__) # DataModel + return isdefined(DM, :CircStrands) && + (x isa getproperty(DM, :CircStrands)) # no compile-time ref to CircStrands end """ @@ -1067,81 +1078,66 @@ where: 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 + 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. -```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) + 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): +Calculates the solenoid correction factor for magnetic permeability in insulated cables with helical conductors (`CircStrands`), using the formula from Gudmundsdottir et al. [5743045](@cite): + +# Notes ```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})} @@ -1164,39 +1160,42 @@ where: # Examples -```julia +```jldoctest # 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] +correction = calc_solenoid_correction(10, 0.005, 0.01) +@assert correction > 1.0 # Non-helical cable (straight conductor) -correction = $(FUNCTIONNAME)(NaN, 0.005, 0.01) # Expected output: 1.0 [dimensionless] +correction = calc_solenoid_correction(NaN, 0.005, 0.01) +@assert correction == 1.0 +# output ``` """ function calc_solenoid_correction( - num_turns::T, - radius_ext_con::T, - radius_ext_ins::T, + 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 + 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, + 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), - ) + 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 """ @@ -1204,6 +1203,8 @@ $(TYPEDSIGNATURES) Calculates the equivalent resistivity of a solid tubular conductor, using the formula [916943](@cite): +# Notes + ```math \\rho_{eq} = R_{eq} S_{eff} = R_{eq} \\pi (r_{ext}^2 - r_{in}^2) ``` @@ -1222,26 +1223,28 @@ where ``S_{eff}`` is the effective cross-sectional area of the tubular conductor # Examples -```julia -rho_eq = $(FUNCTIONNAME)(0.01, 0.02, 0.01) # Expected output: ~9.42e-4 [Ω·m] +```jldoctest +rho_eq = calc_equivalent_rho(0.01, 0.02, 0.01) +@assert rho_eq > 0 +# output ``` """ function calc_equivalent_rho( - R::T, - radius_ext_con::T, - radius_in_con::T, + 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 + 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), - ) + 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 """ @@ -1249,6 +1252,8 @@ $(TYPEDSIGNATURES) Calculates the equivalent permittivity for a coaxial cable insulation, using the formula [916943](@cite): +# Notes + ```math \\varepsilon_{eq} = \\frac{C_{eq} \\log(\\frac{r_{ext}}{r_{in}})}{2\\pi \\varepsilon_0} ``` @@ -1267,24 +1272,24 @@ where ``\\varepsilon_0`` is the permittivity of free space. # Examples -```julia -eps_eq = $(FUNCTIONNAME)(1e-10, 0.01, 0.005) # Expected output: ~2.26 [dimensionless] +```jldoctest +eps_eq = calc_equivalent_eps(1e-10, 0.01, 0.005) +@assert eps_eq > 1 +# output ``` -# 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) / ε₀ + 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), - ) + 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 """ @@ -1292,6 +1297,8 @@ $(TYPEDSIGNATURES) Calculates the equivalent loss factor (tangent) of a dielectric material: +# Notes + ```math \\tan \\delta = \\frac{G_{eq}}{\\omega \\cdot C_{eq}} ``` @@ -1310,21 +1317,23 @@ where ``\\tan \\delta`` is the loss factor (tangent). # Examples -```julia -loss_factor = $(FUNCTIONNAME)(1e-8, 1e-10, 2π*50) # Expected output: ~0.0318 [dimensionless] +```jldoctest +loss_factor = calc_equivalent_lossfact(1e-8, 1e-10, 2π*50) +@assert loss_factor > 0 +# output ``` """ function calc_equivalent_lossfact(G_eq::T, C_eq::T, ω::T) where {T <: REALSCALAR} - return G_eq / (ω * C_eq) + 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), - ) + 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 """ @@ -1332,6 +1341,8 @@ $(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): +# Notes + ```math \\sigma_{eq} = \\frac{G_{eq}}{2\\pi} \\log(\\frac{r_{ext}}{r_{in}}) ``` @@ -1349,22 +1360,26 @@ where ``\\sigma_{eq} = \\frac{1}{\\rho_{eq}}`` is the conductivity of the dielec # Examples -```julia -Geq = 2.7169e-9 -sigma_eq = $(FUNCTIONNAME)(G_eq, r_in, r_ex) +```jldoctest +G_eq = 2.7169e-9 +r_in = 0.01 +r_ex = 0.02 +sigma_eq = calc_sigma_lossfact(G_eq, r_in, r_ex) +@assert sigma_eq > 0 +# output ``` """ 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) + 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), - ) + 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/cablecomponent.jl b/src/datamodel/cablecomponent.jl index 344ef369..20b7d563 100644 --- a/src/datamodel/cablecomponent.jl +++ b/src/datamodel/cablecomponent.jl @@ -7,130 +7,123 @@ Represents a [`CableComponent`](@ref), i.e. a group of [`AbstractCablePart`](@re $(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. + 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). + 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. + 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 + "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 + ``` + + """ + 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 """ @@ -147,21 +140,20 @@ Weakly-typed constructor that infers the scalar type `T` from the two groups, co - A `CableComponent{T}` where `T` is the resolved scalar type. """ function CableComponent( - id::String, - conductor_group::ConductorGroup, - insulator_group::InsulatorGroup, + 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) + # 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) + # 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) + return CableComponent{T}(id, cgT, igT) end - """ $(TYPEDSIGNATURES) @@ -178,9 +170,9 @@ Constructs the equivalent coaxial conductor as a `Tubular` directly from a `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) + cg = component.conductor_group + temp = component.conductor_props.T0 + return Tubular(cg.r_in, cg.r_ex, component.conductor_props, temp) end """ @@ -199,9 +191,9 @@ Constructs the equivalent coaxial insulation as an `Insulator` directly from a `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) + ig = component.insulator_group + temp = component.insulator_props.T0 + return Insulator(ig.r_in, ig.r_ex, component.insulator_props, temp) end """ @@ -215,19 +207,19 @@ Constructs a single-layer `ConductorGroup{T}` from the computed equivalent 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], - ) + 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 """ @@ -239,8 +231,8 @@ 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)) - +function InsulatorGroup(component::CableComponent{T}) where {T <: REALSCALAR} + InsulatorGroup{T}(Insulator(component)) +end include("cablecomponent/base.jl") diff --git a/src/datamodel/cablecomponent/base.jl b/src/datamodel/cablecomponent/base.jl index 5303190d..7bc85aec 100644 --- a/src/datamodel/cablecomponent/base.jl +++ b/src/datamodel/cablecomponent/base.jl @@ -18,84 +18,83 @@ Defines the display representation of a [`CableComponent`](@ref) object for REPL - 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 + # 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 index 2b5e1922..aafc3cb6 100644 --- a/src/datamodel/cabledesign.jl +++ b/src/datamodel/cabledesign.jl @@ -7,60 +7,55 @@ Represents the design of a cable, including its unique identifier, nominal data, $(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 + "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) + ``` + + """ + @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 """ @@ -69,17 +64,17 @@ $(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(), + 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) + # 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 + 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) + return CableDesign{T}(cable_id, compT; nominal_data = ndT) end """ @@ -89,54 +84,54 @@ Constructs a [`CableDesign`](@ref) instance **from conductor and insulator group 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(), + 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) + 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 + 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, + 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 + comp = CableComponent(component_id, conductor_group, insulator_group) + add!(design, comp) # may return the same or a promoted design end """ @@ -152,119 +147,114 @@ 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 = "", + 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 + 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. +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. +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. +- `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). +- `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 +⚠ 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 = "", + 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 + 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") diff --git a/src/datamodel/cabledesign/base.jl b/src/datamodel/cabledesign/base.jl index 173078d0..4b9c9498 100644 --- a/src/datamodel/cabledesign/base.jl +++ b/src/datamodel/cabledesign/base.jl @@ -1,9 +1,7 @@ - Base.eltype(::CableDesign{T}) where {T} = T Base.eltype(::Type{CableDesign{T}}) where {T} = T - """ $(TYPEDSIGNATURES) @@ -20,97 +18,97 @@ Defines the display representation of a [`CableDesign`](@ref) object for REPL or - 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 + # 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 index e4ad42ad..32b491fc 100644 --- a/src/datamodel/cabledesign/dataframe.jl +++ b/src/datamodel/cabledesign/dataframe.jl @@ -35,220 +35,207 @@ detailed_data = DataFrame(design, :detailed) 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, + 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 + 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 """ @@ -293,44 +280,44 @@ 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, - ] + 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 index 82bd566e..264fbab8 100644 --- a/src/datamodel/cableslibrary.jl +++ b/src/datamodel/cableslibrary.jl @@ -6,45 +6,37 @@ 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} + "Dictionary mapping cable IDs to the respective CableDesign objects." + data::Dict{String, CableDesign} - @doc """ - $(TYPEDSIGNATURES) + @doc """ + $(TYPEDSIGNATURES) - Constructs an empty [`CablesLibrary`](@ref) instance. + Constructs an empty [`CablesLibrary`](@ref) instance. - # Arguments + # Arguments - - None. + - None. - # Returns + # Returns - - A [`CablesLibrary`](@ref) object with an empty dictionary of cable designs. + - A [`CablesLibrary`](@ref) object with an empty dictionary of cable designs. - # Examples + # Examples - ```julia - # Create a new, empty library - library = $(FUNCTIONNAME)() - ``` + ```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 + """ + 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. @@ -64,20 +56,13 @@ design = CableDesign("example", ...) # Initialize CableDesign with required fiel 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 + 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 02341d1d..2faf4c72 100644 --- a/src/datamodel/cableslibrary/base.jl +++ b/src/datamodel/cableslibrary/base.jl @@ -1,7 +1,9 @@ # 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, value::CableDesign, key::String) + (lib.data[key] = value) +end Base.iterate(lib::CablesLibrary, state...) = iterate(lib.data, state...) Base.keys(lib::CablesLibrary) = keys(lib.data) Base.values(lib::CablesLibrary) = values(lib.data) @@ -38,14 +40,8 @@ missing_design = $(FUNCTIONNAME)(library, "nonexistent_id") println(missing_design === nothing) # Prints true ``` -# See also - -- [`CablesLibrary`](@ref) -- [`CableDesign`](@ref) -- [`add!`](@ref) -- [`delete!`](@ref) """ -function Base.get(library::CablesLibrary, cable_id::String, default=nothing) +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] @@ -81,10 +77,6 @@ $(FUNCTIONNAME)(library, "example") haskey(library, "example") # Returns false ``` -# See also - -- [`CablesLibrary`](@ref) -- [`add!`](@ref) """ function Base.delete!(library::CablesLibrary, cable_id::String) if haskey(library, cable_id) @@ -94,4 +86,4 @@ function Base.delete!(library::CablesLibrary, cable_id::String) @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 +end diff --git a/src/datamodel/cableslibrary/dataframe.jl b/src/datamodel/cableslibrary/dataframe.jl index 8f755020..e9b5f77a 100644 --- a/src/datamodel/cableslibrary/dataframe.jl +++ b/src/datamodel/cableslibrary/dataframe.jl @@ -30,23 +30,17 @@ 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) - ] + 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, + cable_id = collect(ids), + nominal_data = nominal_data, + components = components ) return (df) -end \ No newline at end of file +end diff --git a/src/datamodel/cableslibrary/vdeparse.jl b/src/datamodel/cableslibrary/vdeparse.jl index 43e7446b..d8cf6ff6 100644 --- a/src/datamodel/cableslibrary/vdeparse.jl +++ b/src/datamodel/cableslibrary/vdeparse.jl @@ -2,88 +2,88 @@ # ─────────────────────────── 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 (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" + ) ) # Canonical 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 (e.g., …XSH…) + :sheath, + :armouring, + :outer_sheath, + :grounding ] # ─────────────────────── Regex builders ──────────────────────── @@ -92,68 +92,67 @@ escape_for_rx(s) = replace(s, r"([.^$|?*+\[\]{}\\])" => "\\\\\1") # Return the non-capturing alternation **as a string**, e.g. "(?: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 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 (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 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"^([RSEMOH]{1,3})(?:\s*/\s*V)?\b" # RM, SE, 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", 'S' => "sector", '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} + vdeparse(code::AbstractString) -> Dict{Symbol,String} 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 @@ -162,78 +161,78 @@ Parses VDE/DIN 0271/0276 cable codes: Only parsed keys are returned; defaults are materialized when omitted. """ 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 -> 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 end # # ───────────────────────── Smoke tests ───────────────────────── @@ -248,5 +247,3 @@ end # for ex in exs # println(ex, " → ", vdeparse(ex)) # end - - diff --git a/src/datamodel/circstrands.jl b/src/datamodel/circstrands.jl index e58380ce..88d297f0 100644 --- a/src/datamodel/circstrands.jl +++ b/src/datamodel/circstrands.jl @@ -6,32 +6,32 @@ Represents an array of wires equally spaced around a circumference of arbitrary $(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 + "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 """ @@ -62,66 +62,57 @@ 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, + 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, - ) + 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) @@ -134,37 +125,41 @@ 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 +function Validation.coercive_fields(::Type{CircStrands}) + (:r_in, :radius_wire, :lay_ratio, :material_props, :temperature) +end # not :num_wires, :lay_direction # accept proxies for radii Validation.is_radius_input(::Type{CircStrands}, ::Val{:r_in}, - x::AbstractCablePart) = true + x::AbstractCablePart) = true Validation.is_radius_input(::Type{CircStrands}, ::Val{:r_ex}, - x::Diameter) = true + 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), +function 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), + # counts and geometry params + IntegerField(:num_wires), Positive(:num_wires), + Finite(:lay_ratio), Nonneg(:lay_ratio), - # material type - IsA{Material}(:material_props), + # material type + IsA{Material}(:material_props), - # lay direction constraint (pin to -1 or +1) - OneOf(:lay_direction, (-1, 1)), -) + # lay direction constraint (pin to -1 or +1) + OneOf(:lay_direction, (-1, 1)) + ) +end -maxfill(::Type{CircStrands}, rin::Real, rw::Real) = - rin == 0 ? 1 : floor(Int, π / asin(rw / (rin + rw))) +function maxfill(::Type{CircStrands}, rin::Real, rw::Real) + rin == 0 ? 1 : floor(Int, π / asin(rw / (rin + rw))) +end # 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) +function Validation.parse(::Type{CircStrands}, nt) + 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 diff --git a/src/datamodel/conductorgroup.jl b/src/datamodel/conductorgroup.jl index b18332cc..40a8cfb7 100644 --- a/src/datamodel/conductorgroup.jl +++ b/src/datamodel/conductorgroup.jl @@ -3,9 +3,9 @@ $(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 +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 @@ -23,84 +23,82 @@ $(TYPEDFIELDS) # 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 + "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) @@ -135,10 +133,10 @@ normalizing proxies, and **promoting** the group’s numeric type if required. - 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. + - 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 @@ -148,44 +146,35 @@ 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..., + 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 + # 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 """ @@ -196,43 +185,43 @@ Runs Validation → parsing, then coerces fields to the group’s `T` and update equivalent properties and book-keeping. """ function _do_add!( - group::ConductorGroup{Tg}, - C::Type{<:AbstractConductorPart}, - args...; - kwargs..., + 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 + # 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 index 3d4998d3..cd488603 100644 --- a/src/datamodel/conductorgroup/base.jl +++ b/src/datamodel/conductorgroup/base.jl @@ -1,4 +1,4 @@ import Base: eltype eltype(::ConductorGroup{T}) where {T} = T -eltype(::Type{ConductorGroup{T}}) where {T} = T \ No newline at end of file +eltype(::Type{ConductorGroup{T}}) where {T} = T diff --git a/src/datamodel/helpers.jl b/src/datamodel/helpers.jl index ab6e8802..ed981f3b 100644 --- a/src/datamodel/helpers.jl +++ b/src/datamodel/helpers.jl @@ -26,19 +26,19 @@ 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" + @assert r_ext > 0 "External radius must be positive" - d = r_ext / cos(deg2rad(30)) - xa = x0 - ya = y0 + d * sin(deg2rad(90)) + 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)) + xb = x0 + d * cos(deg2rad(210)) + yb = y0 + d * sin(deg2rad(210)) - xc = x0 + d * cos(deg2rad(330)) - yc = y0 + d * sin(deg2rad(330)) + xc = x0 + d * cos(deg2rad(330)) + yc = y0 + d * sin(deg2rad(330)) - return xa, ya, xb, yb, xc, yc + return xa, ya, xb, yb, xc, yc end """ @@ -74,25 +74,25 @@ 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 + 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 +# 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 + 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 diff --git a/src/datamodel/insulator.jl b/src/datamodel/insulator.jl index fdaf822b..fd32d7ba 100644 --- a/src/datamodel/insulator.jl +++ b/src/datamodel/insulator.jl @@ -6,24 +6,24 @@ Represents an insulating layer with defined geometric, material, and electrical $(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 + "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 """ @@ -50,37 +50,35 @@ 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, + 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, - ) + 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) @@ -94,8 +92,7 @@ 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::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 @@ -103,9 +100,9 @@ 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) +function Validation.parse(::Type{Insulator}, nt) + 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 diff --git a/src/datamodel/insulatorgroup.jl b/src/datamodel/insulatorgroup.jl index aa57d36b..dac8f3b4 100644 --- a/src/datamodel/insulatorgroup.jl +++ b/src/datamodel/insulatorgroup.jl @@ -12,64 +12,64 @@ properties that represent the composite behavior of the entire assembly, stored $(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 + "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 @@ -104,10 +104,10 @@ Adds a new part to an existing [`InsulatorGroup`](@ref) object and updates its e - 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. + - 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 @@ -117,42 +117,36 @@ 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..., + 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 + # 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 """ @@ -166,38 +160,38 @@ provided frequency. Returns the mutated group (same object). """ function _do_add!( - group::InsulatorGroup{Tg}, - C::Type{<:AbstractInsulatorPart}, - args...; - f::Number = f₀, - kwargs..., + group::InsulatorGroup{Tg}, + C::Type{<:AbstractInsulatorPart}, + args...; + f::Number = f₀, + kwargs... ) where {Tg} - # Materialize keyword args into a NamedTuple - kw = (; kwargs...) + # 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...) + # 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 + # 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) / ω + # 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 + # 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 + 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 index f4bf7185..cc6ed791 100644 --- a/src/datamodel/insulatorgroup/base.jl +++ b/src/datamodel/insulatorgroup/base.jl @@ -1,3 +1,3 @@ Base.eltype(::InsulatorGroup{T}) where {T} = T -Base.eltype(::Type{InsulatorGroup{T}}) where {T} = T \ No newline at end of file +Base.eltype(::Type{InsulatorGroup{T}}) where {T} = T diff --git a/src/datamodel/io.jl b/src/datamodel/io.jl index 7b45456a..2861ef6f 100644 --- a/src/datamodel/io.jl +++ b/src/datamodel/io.jl @@ -1,49 +1,49 @@ # 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) + # 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 + # 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 + # 2. Does it exist in the shape payload? + if hasfield(typeof(part), :shape) + return hasproperty(getfield(part, :shape), sym) + end - return false + return false end function Base.propertynames(part::AbstractCablePart, private::Bool = false) - top_fields = fieldnames(typeof(part)) + top_fields = fieldnames(typeof(part)) - if hasfield(typeof(part), :shape) - shape_payload = getfield(part, :shape) - shape_fields = propertynames(shape_payload, private) + 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 + # Merge and deduplicate the fields + return Tuple(unique((top_fields..., shape_fields...))) + end - return top_fields + return top_fields end """ @@ -62,39 +62,38 @@ Defines the display representation of an [`AbstractCablePart`](@ref) object for - 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 + # 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) @@ -111,45 +110,44 @@ Defines the display representation of a [`ConductorGroup`](@ref) or [`InsulatorG - 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 + 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 """ @@ -168,26 +166,26 @@ Print the specified fields of an object in a compact format. - 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 + 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 index 481a0aa8..79c9abed 100644 --- a/src/datamodel/linecablesystem.jl +++ b/src/datamodel/linecablesystem.jl @@ -7,84 +7,81 @@ Represents a physically defined cable with position and phase mapping within a s $(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 + "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) + ``` + + """ + 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 """ @@ -93,46 +90,46 @@ $(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, + 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) + @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 """ @@ -143,75 +140,71 @@ Represents a cable system configuration, defining the physical structure, cables $(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 + "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 + ``` + + """ + @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 """ @@ -220,16 +213,16 @@ $(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, + 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), - ) + T = resolve_T(line_length, cable) + return LineCableSystem{T}( + system_id, + coerce_to_T(line_length, T), + coerce_to_T(cable, T) + ) end """ @@ -238,15 +231,15 @@ $(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, + 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) + 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 @@ -257,19 +250,19 @@ 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 + 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 """ @@ -308,47 +301,42 @@ $(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 + # 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 """ @@ -358,14 +346,14 @@ Convenience `add!` that accepts a cable design and coordinates (and optional map 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, + 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 + pos = CablePosition(cable, horz, vert, conn) + return add!(system, pos) # may mutate or return a promoted system end include("linecablesystem/dataframe.jl") diff --git a/src/datamodel/linecablesystem/base.jl b/src/datamodel/linecablesystem/base.jl index 327c4d38..f80ee23e 100644 --- a/src/datamodel/linecablesystem/base.jl +++ b/src/datamodel/linecablesystem/base.jl @@ -8,7 +8,7 @@ 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)]", + "LineCableSystem \"$(system.system_id)\": [line_length=$(system.line_length), num_cables=$(system.num_cables), num_phases=$(system.num_phases)]" ) # Print cable definitions @@ -23,13 +23,13 @@ function Base.show(io::IO, ::MIME"text/plain", system::LineCableSystem) 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))]", + "$(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 +end diff --git a/src/datamodel/linecablesystem/dataframe.jl b/src/datamodel/linecablesystem/dataframe.jl index c51cef71..efa67c34 100644 --- a/src/datamodel/linecablesystem/dataframe.jl +++ b/src/datamodel/linecablesystem/dataframe.jl @@ -29,10 +29,6 @@ println(df) # │ "Cable2" │ 0.35 │ -1.25 │ core: 2, sheath: 0 │ ``` -# See also - -- [`LineCableSystem`](@ref) -- [`CablePosition`](@ref) """ function DataFrame(system::LineCableSystem)::DataFrame cable_ids = String[] @@ -47,16 +43,17 @@ function DataFrame(system::LineCableSystem)::DataFrame 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)], - ", ", + ["$(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 + cable_id = cable_ids, + horz = horz_coords, + vert = vert_coords, + phase_mapping = mappings ) return data -end \ No newline at end of file +end diff --git a/src/datamodel/macros.jl b/src/datamodel/macros.jl index 6e90425c..195eb7e8 100644 --- a/src/datamodel/macros.jl +++ b/src/datamodel/macros.jl @@ -19,8 +19,8 @@ Determines the promoted numeric element type for convenience constructors of com 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))...) +@inline _promotion_T(::Type{C}, ntv, _order::Tuple) where {C} = resolve_T((getfield(ntv, k) +for k in coercive_fields(C))...) """ $(TYPEDSIGNATURES) @@ -44,13 +44,11 @@ Builds the positional argument tuple to feed the **typed core** constructor, coe 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 - )...) +@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) @@ -82,11 +80,12 @@ 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)") +function _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)") +end using MacroTools: postwalk """ @@ -143,42 +142,41 @@ const _DEFS_TUBULAR = (T₀,) - `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 + 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 index 4e75ece0..b828e786 100644 --- a/src/datamodel/nominaldata.jl +++ b/src/datamodel/nominaldata.jl @@ -6,38 +6,38 @@ Stores nominal electrical and geometric parameters for a cable design. $(TYPEDFIELDS) """ -struct NominalData{T<:REALSCALAR} +struct NominalData{T <: REALSCALAR} "Cable designation as per DIN VDE 0271/0276." - designation_code::Union{Nothing,String} + designation_code::Union{Nothing, String} "Rated phase-to-earth voltage \\[kV\\]." - U0::Union{Nothing,T} + U0::Union{Nothing, T} "Rated phase-to-phase voltage \\[kV\\]." - U::Union{Nothing,T} + U::Union{Nothing, T} "Cross-sectional area of the conductor \\[mm²\\]." - conductor_cross_section::Union{Nothing,T} + conductor_cross_section::Union{Nothing, T} "Cross-sectional area of the screen \\[mm²\\]." - screen_cross_section::Union{Nothing,T} + screen_cross_section::Union{Nothing, T} "Cross-sectional area of the armor \\[mm²\\]." - armor_cross_section::Union{Nothing,T} + armor_cross_section::Union{Nothing, T} "Base (DC) resistance of the cable core \\[Ω/km\\]." - resistance::Union{Nothing,T} + resistance::Union{Nothing, T} "Capacitance of the main insulation \\[μF/km\\]." - capacitance::Union{Nothing,T} + capacitance::Union{Nothing, T} "Inductance of the cable (trifoil formation) \\[mH/km\\]." - inductance::Union{Nothing,T} + 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} + 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, @@ -47,7 +47,7 @@ struct NominalData{T<:REALSCALAR} armor_cross_section, resistance, capacitance, - inductance, + inductance ) end end @@ -60,37 +60,38 @@ Weakly-typed constructor that infers the target scalar type `T` from the **provi 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, + 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) + 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)), + 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 index 035a933e..8325ee9e 100644 --- a/src/datamodel/nominaldata/base.jl +++ b/src/datamodel/nominaldata/base.jl @@ -1,4 +1,4 @@ # Scalar-type query Base.eltype(::NominalData{T}) where {T} = T -Base.eltype(::Type{NominalData{T}}) where {T} = T \ No newline at end of file +Base.eltype(::Type{NominalData{T}}) where {T} = T diff --git a/src/datamodel/preview.jl b/src/datamodel/preview.jl index b722b03e..9534694d 100644 --- a/src/datamodel/preview.jl +++ b/src/datamodel/preview.jl @@ -3,122 +3,118 @@ 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_MIN = 1e-9 # for legend floor const RHO_METAL_MAX = 1e-6 const RHO_SEMIMETAL = 1e-4 -const RHO_SEMI_MAX = 1e3 +const RHO_SEMI_MAX = 1e3 const RHO_LEAKY_MAX = 1e8 -const RHO_MAX = 1e10 # for legend ceiling +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 + 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 + 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 + 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 + 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), + 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 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), - ) +function _interpolate_gradient(colors::Vector{<:Colorant}, t::Real) + 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)) + 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) +function _overlay(a::Colors.RGBA, b::Colors.RGBA) + 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 + 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 + 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 # ---------------------------- @@ -126,1159 +122,1140 @@ end # ---------------------------- # μ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, α) + μ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, α) + ε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) + 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) + ρ = 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) + 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)) + # 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 + 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 + # 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) + 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) + θ = 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, + 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) + # 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) + xo = x0 .+ rex .* cos.(θ_out) + yo = y0 .+ rex .* sin.(θ_out) - xi = x0 .+ rin .* cos.(θ_in) - yi = y0 .+ rin .* sin.(θ_in) + 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]) + # 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) + 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, + 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) + # 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, + 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 + # 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) + # 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) + xo = x0 .+ rex .* cos.(θ_out) + yo = y0 .+ rex .* sin.(θ_out) - xi = x0 .+ rin .* cos.(θ_in) - yi = y0 .+ rin .* sin.(θ_in) + 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]) + # 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) + 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, + 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 () + 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)) + 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 + 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 + 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, + 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 + 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(), + 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 + 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 + 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, + ρ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 + 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 + 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 + 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/radii.jl b/src/datamodel/radii.jl index 963e7c41..872ab258 100644 --- a/src/datamodel/radii.jl +++ b/src/datamodel/radii.jl @@ -5,7 +5,7 @@ Resolves radius parameters for cable components, converting from various input f 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. +2. Then delegates to specialized `_do_resolve_radius` implementations based on the component type. # Arguments @@ -22,14 +22,9 @@ This function serves as a high-level interface to the radius resolution system. - `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) +@inline _normalize_radii(::Type{T}, rin, rex) where {T} = _do_normalize_radii( + _parse_radius_operand(rin, T), _parse_radius_operand(rex, T), T) """ $(TYPEDSIGNATURES) @@ -57,10 +52,6 @@ radius = $(FUNCTIONNAME)(Thickness(5.0), ...) # From thickness object $(METHODLIST) -# See also - -- [`Diameter`](@ref) -- [`Thickness`](@ref) """ function _parse_radius_operand end @@ -72,59 +63,87 @@ function _parse_radius_operand end # 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))", - ), - ) +@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}, + r_in::Number, + r_ex::Number, + ::Type{T} ) where {T} - return r_in, r_ex + return r_in, r_ex end @inline function _do_normalize_radii( - r_in::Number, - thickness::Thickness, - ::Type{T}, + r_in::Number, + thickness::Thickness, + ::Type{T} ) where {T} - return r_in, (r_in + thickness.value) + 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 + +# These intersections make the intended `Thickness` semantics explicit for +# strand layers and keep dispatch unambiguous. +@inline _do_normalize_radii(r_in::Number, thickness::Thickness, + ::Type{AbstractStrandsLayer}) = (r_in, r_in + thickness.value) + +@inline function _do_normalize_radii( + thickness::Thickness, + r_ex::Number, + ::Type{AbstractStrandsLayer} +) + r_in = r_ex - thickness.value + r_in >= 0 || throw( + ArgumentError( + "[AbstractStrandsLayer] thickness $(thickness.value) exceeds outer radius $(r_ex).", + ), + ) + return r_in, r_ex end @inline function _do_normalize_radii( - r_in::Number, - radius_wire::Number, - ::Type{AbstractStrandsLayer}, + ::Thickness, + ::Thickness, + ::Type{AbstractStrandsLayer} ) - return r_in, r_in + (2 * radius_wire) + throw( + ArgumentError( + "[AbstractStrandsLayer] cannot specify thickness for both inner and outer radii.", + ), + ) 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 + 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.", - ), - ) + 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 index 2f3a373a..790d2535 100644 --- a/src/datamodel/rectstrands.jl +++ b/src/datamodel/rectstrands.jl @@ -4,22 +4,22 @@ $(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 + "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 """ @@ -30,20 +30,20 @@ Represents a concentric layer of rectangular strands with defined geometric, mat $(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 + "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} @@ -108,62 +108,59 @@ 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, + 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, - ) + # 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 _REQ_RECTSTRANDS = ( + :r_in, :thickness, :width, :num_wires, :lay_ratio, :material_props) const _OPT_RECTSTRANDS = (:temperature, :lay_direction) const _DEFS_RECTSTRANDS = (T₀, 1) @@ -173,39 +170,41 @@ 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) +function Validation.coercive_fields(::Type{RectStrands}) + (:r_in, :thickness, :width, :lay_ratio, :material_props, :temperature) +end Validation.is_radius_input( - ::Type{RectStrands}, - ::Val{:r_in}, - x::AbstractCablePart, + ::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) +Validation.maxfill(::Type{RectStrands}, rin::Real, w::Real) = floor(Int, 2 * π * rin / w) + +function 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 + ) +end + +function Validation.parse(::Type{RectStrands}, nt) + 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 +@construct RectStrands _REQ_RECTSTRANDS _OPT_RECTSTRANDS _DEFS_RECTSTRANDS diff --git a/src/datamodel/sector.jl b/src/datamodel/sector.jl index c76255db..1627b04d 100644 --- a/src/datamodel/sector.jl +++ b/src/datamodel/sector.jl @@ -5,7 +5,7 @@ Holds the geometric parameters that define the shape of a sector conductor. $(TYPEDFIELDS) """ -struct SectorParams{T<:REALSCALAR} +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\\]." @@ -19,37 +19,43 @@ struct SectorParams{T<:REALSCALAR} "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) + 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) +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) +function Validation.coercive_fields(::Type{SectorParams}) + (:r_back, :d_sector, :r_corner, :theta_cond_deg, :d_insulation) +end # --- Geometry Logic Storage --- -function _validate_sector_params(n_sectors, r_back, d_sector, r_corner, theta_cond_deg, d_insulation) +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")) + 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)")) + 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)")) + throw(ArgumentError("[SectorParams] d_sector ($d_sector) must be less than r_back ($r_back)")) end # 3. Geometric feasibility (Discriminant check) @@ -57,12 +63,12 @@ function _validate_sector_params(n_sectors, r_back, d_sector, r_corner, theta_co 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.")) + 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) @@ -85,22 +91,25 @@ 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, + # 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) + _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), -) +function 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) + ) +end @construct SectorParams _REQ_SECTOR_PARAMS @@ -111,7 +120,7 @@ Represents a single sector-shaped conductor with defined geometric and material $(TYPEDFIELDS) """ -struct Sector{T<:REALSCALAR} <: AbstractConductorPart{T} +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\\]." @@ -131,18 +140,17 @@ struct Sector{T<:REALSCALAR} <: AbstractConductorPart{T} "Geometric mean radius (GMR) of the sector (approximated) \\[m\\]." gmr::T "Calculated vertices defining the polygon shape." - vertices::Vector{Point{2,T}} + vertices::Vector{Point{2, T}} "Geometric centroid of the sector shape." - centroid::Point{2,T} + centroid::Point{2, T} end - function Sector( - params::SectorParams{T}, - rotation_angle_deg::T, - material_props::Material{T}; - temperature::T=T₀, -) where {T<:REALSCALAR} + 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) @@ -167,12 +175,13 @@ function Sector( 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 + 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 @@ -190,12 +199,10 @@ function Sector( resistance, gmr, rotated_vertices, - centroid, + centroid ) end - - # --- Geometric Helper Functions (internal) --- function _calculate_sector_geometry(p::SectorParams) @@ -208,16 +215,17 @@ function _calculate_sector_geometry(p::SectorParams) 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 + 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 + 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 + #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 @@ -256,22 +264,22 @@ function _calculate_sector_geometry(p::SectorParams) 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]) + 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) + 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) + return (Nodes = nodes, Centers = centers, Params = p) end function _generate_arc_points(center, radius, start_angle, end_angle, num_points) @@ -285,12 +293,13 @@ function _generate_arc_points(center, radius, start_angle, end_angle, num_points 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) + 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] + 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 +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 @@ -304,7 +313,8 @@ function _calculate_sector_polygon_points(params; num_arc_points=30) # increase 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]) + _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 @@ -318,7 +328,9 @@ function _calculate_sector_polygon_points(params; num_arc_points=30) # increase @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]) + 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 @@ -328,7 +340,9 @@ function _calculate_sector_polygon_points(params; num_arc_points=30) # increase @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]) + 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 @@ -336,7 +350,9 @@ function _calculate_sector_polygon_points(params; num_arc_points=30) # increase @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]) + 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 @@ -349,8 +365,7 @@ function _rotate_point(p::Point2f, angle_rad::Real) 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} +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" @@ -365,13 +380,13 @@ function _shoelace_area(vertices::AbstractVector{Point{2,T}}) where {T<:Real} return abs(area) / T(2) end -function _calculate_polygon_centroid(vertices::AbstractVector{Point{2,T}}) where {T<:Real} +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) @@ -394,4 +409,4 @@ function _calculate_polygon_centroid(vertices::AbstractVector{Point{2,T}}) where Cy /= (6 * signed_area) return Point2f(Cx, Cy) -end \ No newline at end of file +end diff --git a/src/datamodel/sectorinsulator.jl b/src/datamodel/sectorinsulator.jl index 8f13ea6b..8d60b65b 100644 --- a/src/datamodel/sectorinsulator.jl +++ b/src/datamodel/sectorinsulator.jl @@ -5,7 +5,7 @@ Represents an insulating layer surrounding a sector-shaped conductor. $(TYPEDFIELDS) """ -struct SectorInsulator{T<:REALSCALAR} <: AbstractInsulatorPart{T} +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\\]." @@ -28,13 +28,12 @@ struct SectorInsulator{T<:REALSCALAR} <: AbstractInsulatorPart{T} 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} + 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) @@ -53,8 +52,6 @@ function SectorInsulator( # end # end - - #outer_area = PolygonOps.area(outer_vertices) outer_area = _shoelace_area(outer_vertices) @debug "SectorInsulator inner area: $(inner_area*1e6) mm²" @@ -85,9 +82,10 @@ function SectorInsulator( ) end -# --- Geometric Helper Functions (internal) --- +# --- 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} +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.") @@ -116,7 +114,7 @@ function _calculate_offset_polygon(vertices::Vector{Point{2, T}}, thickness::T) # 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) @@ -128,4 +126,4 @@ function _calculate_offset_polygon(vertices::Vector{Point{2, T}}, thickness::T) end return new_vertices -end \ No newline at end of file +end diff --git a/src/datamodel/semicon.jl b/src/datamodel/semicon.jl index bc1380f8..ad18f70d 100644 --- a/src/datamodel/semicon.jl +++ b/src/datamodel/semicon.jl @@ -6,24 +6,24 @@ Represents a semiconducting layer with defined geometric, material, and electric $(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 + "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 """ @@ -55,37 +55,35 @@ println(semicon_layer.shunt_conductance) # Expected output: Conductance in [S· ``` """ function Semicon( - r_in::T, - r_ex::T, - material_props::Material{T}, - temperature::T, + 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, - ) + 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) @@ -107,9 +105,9 @@ 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) +function Validation.parse(::Type{Semicon}, nt) + 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 diff --git a/src/datamodel/strands_handler.jl b/src/datamodel/strands_handler.jl index e9be77de..1804860b 100644 --- a/src/datamodel/strands_handler.jl +++ b/src/datamodel/strands_handler.jl @@ -17,12 +17,12 @@ Fallback method for the [`maxfill`](@ref) interface. Throws an explicit error in - 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...)`.", - ), - ) + 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) diff --git a/src/datamodel/strip.jl b/src/datamodel/strip.jl index 70e475b7..87b504f8 100644 --- a/src/datamodel/strip.jl +++ b/src/datamodel/strip.jl @@ -6,32 +6,32 @@ Represents a flat conductive strip with defined geometric and material propertie $(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 + "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 """ @@ -62,59 +62,50 @@ 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, + 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, - ) + 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) @@ -127,8 +118,9 @@ 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 +function Validation.coercive_fields(::Type{Strip}) + (:r_in, :r_ex, :width, :lay_ratio, :material_props, :temperature) +end # not :lay_direction # accept proxies for radii Validation.is_radius_input(::Type{Strip}, ::Val{:r_in}, x::AbstractCablePart) = true @@ -136,19 +128,21 @@ 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), -) +function Validation.extra_rules(::Type{Strip}) + ( + IsA{Material}(:material_props), + OneOf(:lay_direction, (-1, 1)), + Finite(:lay_ratio), + Nonneg(:lay_ratio), + Finite(:width), + Positive(:width) + ) +end # 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) +function Validation.parse(::Type{Strip}, nt) + 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 diff --git a/src/datamodel/tubular.jl b/src/datamodel/tubular.jl index ee7b29fd..21475550 100644 --- a/src/datamodel/tubular.jl +++ b/src/datamodel/tubular.jl @@ -6,20 +6,20 @@ Represents a tubular or solid (`r_in=0`) conductor with geometric and material p $(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 + "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 """ @@ -47,39 +47,33 @@ 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, + 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, - ) + 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) @@ -101,12 +95,10 @@ 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) +function Validation.parse(::Type{Tubular}, nt) + 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 index 144a57d3..c28e60e9 100644 --- a/src/datamodel/typecoercion.jl +++ b/src/datamodel/typecoercion.jl @@ -1,111 +1,112 @@ @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 + 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)) + 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 + 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) +function coerce_to_T(p::AbstractConductorPart{S}, ::Type{T}) where {S, T} + _rebuild_part_typed_core(p, T) +end 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, - ) + 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(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, - ) + 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), - ) + 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 + 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) + 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 - ) + 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) + 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 a3de41e3..a4245927 100644 --- a/src/datamodel/types.jl +++ b/src/datamodel/types.jl @@ -9,12 +9,12 @@ 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 + "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 """ @@ -25,12 +25,12 @@ 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 + "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 """ @@ -62,7 +62,6 @@ Subtypes implement specific configurations: """ abstract type AbstractStrandsLayer{T} <: AbstractConductorPart{T} end - """ $(TYPEDEF) @@ -74,16 +73,14 @@ Subtypes implement specific configurations: """ 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).", - ), - ) + 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 AbstractShapeGeometry end diff --git a/src/datamodel/validation.jl b/src/datamodel/validation.jl index 0e630423..d45e5279 100644 --- a/src/datamodel/validation.jl +++ b/src/datamodel/validation.jl @@ -19,8 +19,9 @@ Default policy for **inner** radius raw inputs: accept proxies that expose an ou 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) +function is_radius_input(::Type{T}, ::Val{:r_in}, p::AbstractCablePart) where {T} + hasproperty(p, :r_ex) +end """ $(TYPEDSIGNATURES) @@ -78,9 +79,9 @@ Defaults may be a `NamedTuple` or a `Tuple` zipped against `Validation.keyword_f 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) + 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/earthprops/EarthProps.jl b/src/earthprops/EarthProps.jl index bb9e75ca..f3e9961e 100644 --- a/src/earthprops/EarthProps.jl +++ b/src/earthprops/EarthProps.jl @@ -1,5 +1,5 @@ """ - LineCableModels.EarthProps + 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. @@ -14,16 +14,13 @@ The [`EarthProps`](@ref) module provides functionality for modeling and computin $(IMPORTS) -# Exports - -$(EXPORTS) """ module EarthProps # Export public API export CPEarth, - EarthLayer, - EarthModel + EarthLayer, + EarthModel # Module-specific dependencies using ..Commons @@ -42,28 +39,28 @@ Represents one single earth layer in an [`EarthModel`](@ref) object, with base a $(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 + "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 """ @@ -94,48 +91,44 @@ 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, + 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, - ) + 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, + 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, - ) + 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 """ @@ -146,21 +139,21 @@ Represents a multi-layered earth model with frequency-dependent properties, and $(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 + "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 """ @@ -192,81 +185,77 @@ 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, + 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], - ) + # 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, + 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), - ) + 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 """ @@ -329,9 +318,9 @@ 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) + $(FUNCTIONNAME)(vert_earth_model, frequencies, 120, 12, 1, t=Inf) catch e - println(e) # Error: Cannot add consecutive vertical layers with infinite thickness. + println(e) # Error: Cannot add consecutive vertical layers with infinite thickness. end # Fix: Set a finite thickness to the currently rightmost layer @@ -342,106 +331,102 @@ $(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), + 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 + 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, + 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 + # 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 +end # module EarthProps diff --git a/src/earthprops/base.jl b/src/earthprops/base.jl index 6f8a165f..ac1b4fd0 100644 --- a/src/earthprops/base.jl +++ b/src/earthprops/base.jl @@ -48,7 +48,7 @@ function Base.show(io::IO, ::MIME"text/plain", model::EarthModel) # 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)", + "EarthModel with $(num_layers-1) $(orientation) earth $(layer_word) ($(model_type)) and $(num_freq_samples) frequency $(freq_word)" ) # Print layers in treeview style @@ -69,7 +69,7 @@ function Base.show(io::IO, ::MIME"text/plain", model::EarthModel) "$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]", + "t=$thickness_str]" ) end @@ -78,5 +78,4 @@ function Base.show(io::IO, ::MIME"text/plain", model::EarthModel) formulation_tag = get_description(model.freq_dependence) println(io, " Frequency-dependent model: $(formulation_tag)") end - -end \ No newline at end of file +end diff --git a/src/earthprops/dataframe.jl b/src/earthprops/dataframe.jl index 1b2e3a03..7fd96fa6 100644 --- a/src/earthprops/dataframe.jl +++ b/src/earthprops/dataframe.jl @@ -25,17 +25,17 @@ 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 + 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 diff --git a/src/earthprops/fdprops.jl b/src/earthprops/fdprops.jl index 61521303..416afd36 100644 --- a/src/earthprops/fdprops.jl +++ b/src/earthprops/fdprops.jl @@ -17,7 +17,6 @@ Represents an earth model with constant properties (CP), i.e. frequency-invarian struct CPEarth <: AbstractFDEMFormulation end get_description(::CPEarth) = "Constant properties (CP)" - """ $(TYPEDSIGNATURES) @@ -51,12 +50,9 @@ 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} + base_mur_g::T) where {T <: REALSCALAR} # Preallocate for performance n_freq = length(frequencies) @@ -78,6 +74,6 @@ function (f::CPEarth)(frequencies::AbstractVector, base_rho_g, base_epsr_g, base 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(base_mur_g, T) ) end diff --git a/src/earthprops/typecoercion.jl b/src/earthprops/typecoercion.jl index 3e54f3c5..e446860f 100644 --- a/src/earthprops/typecoercion.jl +++ b/src/earthprops/typecoercion.jl @@ -22,10 +22,6 @@ 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 @@ -63,10 +59,6 @@ machinery. ℓ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. @@ -82,4 +74,3 @@ function coerce_to_T(layer::EarthLayer, ::Type{T}) where {T} coerce_to_T(layer.mu_g, T) ) end - diff --git a/src/engine/Engine.jl b/src/engine/Engine.jl index 9931cf32..0d81e7b6 100644 --- a/src/engine/Engine.jl +++ b/src/engine/Engine.jl @@ -1,7 +1,7 @@ """ - 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. +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. # Overview @@ -18,16 +18,13 @@ The [`Engine`](@ref) module provides the main functionalities of the [`LineCable $(IMPORTS) -# Exports - -$(EXPORTS) """ module Engine # Export public API export LineParametersProblem, - LineParameters, SeriesImpedance, ShuntAdmittance, per_km, - per_m, kronify + LineParameters, SeriesImpedance, ShuntAdmittance, per_km, + per_m, kronify export EMTFormulation, FormulationSet, LineParamOptions export compute!, plot @@ -90,12 +87,29 @@ include("workspace.jl") # Computation methods include("solver.jl") include("reduction.jl") -include("plot.jl") # Override I/O methods include("base.jl") +include("plotmetadata.jl") include("dataframe.jl") +""" + plot(object; kwargs...) + +Plot computed line parameters with a loaded Makie backend. + +Load `CairoMakie`, `GLMakie`, or `WGLMakie` before calling this function. +""" +function plot end + +function plot(args...; kwargs...) + throw( + ArgumentError( + "Plotting is optional. Load CairoMakie, GLMakie, or WGLMakie before calling plot.", + ), + ) +end + # Submodule `FEM` include("fem/FEM.jl") diff --git a/src/engine/base.jl b/src/engine/base.jl index 59e4d582..5dbc4fa4 100644 --- a/src/engine/base.jl +++ b/src/engine/base.jl @@ -32,24 +32,26 @@ ResultsView: pretty, non-mutating renderer with zero-clipping and units. - `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 + 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), +function 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) + ) +end # --- Scalar formatting with zero-clipping ------------------------------------- @@ -59,251 +61,251 @@ _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) + 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") + # 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) +function _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) +end # 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) + 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") + 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 +function _repr_any(x, tol) + x isa Complex ? _repr_complex(x, tol) : + x isa Measurements.Measurement ? _repr_meas(x, tol) : _repr_real(x, tol) 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)) + (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 +function _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) end +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 + 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, + 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 + 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 + 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", - ")", - ) + 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", - ")", - ) + 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) @@ -322,24 +324,24 @@ 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], + 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) + 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) + 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 @@ -348,47 +350,46 @@ _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, " (±)") + 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 + 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/dataframe.jl b/src/engine/dataframe.jl index af557947..195890ba 100644 --- a/src/engine/dataframe.jl +++ b/src/engine/dataframe.jl @@ -10,92 +10,92 @@ _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 + 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) + 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 + 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) + 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, + 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 + 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, + 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 + 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( @@ -126,9 +126,9 @@ end # end """ - DataFrame(Z::SeriesImpedance; freqs=nothing, mode=:RLCG, coord=:cart, - freq_unit=:base, length_unit=:kilo, quantity_units=nothing, - tol=sqrt(eps(Float64))) + 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 @@ -141,34 +141,34 @@ correspond to conductor indices. - `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, + 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), - ) + 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))) + 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. @@ -176,117 +176,117 @@ 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, + 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), - ) + 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) + 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) + 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 + 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))) + 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, + 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).", - ) + # --- 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, - ) + # --- 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, - ) + 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 + return df_z, df_y end diff --git a/src/engine/earthadmittance/EarthAdmittance.jl b/src/engine/earthadmittance/EarthAdmittance.jl index 8905cb2b..0d94b279 100644 --- a/src/engine/earthadmittance/EarthAdmittance.jl +++ b/src/engine/earthadmittance/EarthAdmittance.jl @@ -1,13 +1,10 @@ """ - LineCableModels.Engine.EarthAdmittance + LineCableModels.Engine.EarthAdmittance # Dependencies $(IMPORTS) -# Exports - -$(EXPORTS) """ module EarthAdmittance diff --git a/src/engine/earthadmittance/base.jl b/src/engine/earthadmittance/base.jl index 69adbf0a..d0012c7f 100644 --- a/src/engine/earthadmittance/base.jl +++ b/src/engine/earthadmittance/base.jl @@ -1,7 +1,7 @@ @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 + 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 diff --git a/src/engine/earthadmittance/homogeneous.jl b/src/engine/earthadmittance/homogeneous.jl index 2fbd7b08..d61fcb21 100644 --- a/src/engine/earthadmittance/homogeneous.jl +++ b/src/engine/earthadmittance/homogeneous.jl @@ -1,62 +1,64 @@ 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 + "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} + 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), - ) +function 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), + ) +end 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} + 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), - ) +function 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), + ) +end 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} + 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), - ) +function 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), + ) +end get_description(::Images) = "Electrostatic images" from_kernel(f::Images) = f.kernel @@ -66,18 +68,18 @@ from_kernel(f::Images) = f.kernel # 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}, + 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")) + 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)( @@ -92,137 +94,133 @@ end # 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}, + ::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ω) + 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 _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"))) +@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", - ), - ) + 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 + @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}, + ::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) - + 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/earthimpedance/EarthImpedance.jl b/src/engine/earthimpedance/EarthImpedance.jl index 098ca9e8..01bff96c 100644 --- a/src/engine/earthimpedance/EarthImpedance.jl +++ b/src/engine/earthimpedance/EarthImpedance.jl @@ -1,13 +1,10 @@ """ - LineCableModels.Engine.EarthImpedance + LineCableModels.Engine.EarthImpedance # Dependencies $(IMPORTS) -# Exports - -$(EXPORTS) """ module EarthImpedance diff --git a/src/engine/earthimpedance/base.jl b/src/engine/earthimpedance/base.jl index 3df1a4b4..684bc632 100644 --- a/src/engine/earthimpedance/base.jl +++ b/src/engine/earthimpedance/base.jl @@ -1,8 +1,8 @@ @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 + 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 diff --git a/src/engine/earthimpedance/homogeneous.jl b/src/engine/earthimpedance/homogeneous.jl index c4caa662..192a47e1 100644 --- a/src/engine/earthimpedance/homogeneous.jl +++ b/src/engine/earthimpedance/homogeneous.jl @@ -1,84 +1,85 @@ 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 + "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} + 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), - ) +function 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), + ) +end 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} + 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), - ) +function 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), + ) +end 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} + 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), - ) +function 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), + ) +end 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}, + 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")) + 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)( @@ -93,114 +94,112 @@ end # 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}, + ::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ω) + 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 _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"))) +@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", - ), - ) + 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 + @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}, + ::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) + 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/ehem/EHEM.jl b/src/engine/ehem/EHEM.jl index bf63e9c8..d29ff559 100644 --- a/src/engine/ehem/EHEM.jl +++ b/src/engine/ehem/EHEM.jl @@ -1,13 +1,10 @@ """ - LineCableModels.Engine.EHEM + LineCableModels.Engine.EHEM # Dependencies $(IMPORTS) -# Exports - -$(EXPORTS) """ module EHEM @@ -23,4 +20,4 @@ using Measurements include("enforcelayer.jl") -end # module EHEM \ No newline at end of file +end # module EHEM diff --git a/src/engine/ehem/enforcelayer.jl b/src/engine/ehem/enforcelayer.jl index 86da5f68..1d8a0045 100644 --- a/src/engine/ehem/enforcelayer.jl +++ b/src/engine/ehem/enforcelayer.jl @@ -7,34 +7,34 @@ An EHEM formulation that creates a homogeneous earth model by enforcing the prop $(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 + "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 + 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 """ @@ -50,38 +50,37 @@ Builds a 2-layer (air + one enforced earth layer) data pack as three matrices with row 1 = air, row 2 = enforced earth layer. """ function (f::EnforceLayer)( - model::EarthModel, - freq::AbstractVector{<:REALSCALAR}, - ::Type{T}, + model::EarthModel, + freq::AbstractVector{<:REALSCALAR}, + ::Type{T} ) where {T <: REALSCALAR} + nL = length(model.layers) + nF = length(freq) - 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.", + ) - 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] - Lair = model.layers[1] - Lsel = model.layers[layer_idx] + ρ = Matrix{T}(undef, 2, nF) + ε = similar(ρ) + μ = similar(ρ) - ρ = 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]) - @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 - ρ[2, j] = T(Lsel.rho_g[j]) - ε[2, j] = T(Lsel.eps_g[j]) - μ[2, j] = T(Lsel.mu_g[j]) - end - - return ρ, ε, μ + return ρ, ε, μ end # """ @@ -133,4 +132,4 @@ end # # Return a new vector containing only these two layers # return [air_data, earth_data] -# end \ No newline at end of file +# end diff --git a/src/engine/fem/FEM.jl b/src/engine/fem/FEM.jl index 8b1b1805..315b3280 100644 --- a/src/engine/fem/FEM.jl +++ b/src/engine/fem/FEM.jl @@ -1,73 +1,89 @@ """ - LineCableModels.Engine.FEM + 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. +Provide the deprecated compatibility facade for the optional finite-element +integration. Load `Gmsh` before constructing or running an FEM formulation. +""" +module FEM + +export Darwin, Electrodynamics, FormulationSet, MeshTransition, calc_domain_size, + compute!, preview_results + +import ...Engine: FormulationSet, compute! + +const _DEPRECATION_MESSAGE = "The current FEM interface is deprecated and will be replaced by a simplified API in a future release." -# Overview +_parent_package() = parentmodule(parentmodule(@__MODULE__)) -- 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. +function _extension() + ext = Base.get_extension(_parent_package(), :LineCableModelsGmshExt) + ext === nothing && throw( + ArgumentError( + "FEM is optional. Load Gmsh with `using Gmsh` before using LineCableModels.Engine.FEM.", + ), + ) + return ext +end -# Dependencies +function _warn_fem(symbol::Symbol) + Base.depwarn(_DEPRECATION_MESSAGE, symbol) + return nothing +end -$(IMPORTS) +function Darwin(args...; kwargs...) + _warn_fem(:Darwin) + return _extension().Darwin(args...; kwargs...) +end -# Exports +function Electrodynamics(args...; kwargs...) + _warn_fem(:Electrodynamics) + return _extension().Electrodynamics(args...; kwargs...) +end + +function MeshTransition(args...; kwargs...) + _warn_fem(:MeshTransition) + return _extension().MeshTransition(args...; kwargs...) +end -$(EXPORTS) """ -module FEM + calc_domain_size(earth_model, frequencies; min_radius=5.0, max_radius=5000.0) + +Estimate the radial FEM domain size from the most resistive finite earth layer. + +# Arguments + +- `earth_model`: Earth model containing frequency-dependent resistivity and + permeability. +- `frequencies`: FEM frequencies in hertz. + +# Returns + +- Domain radius in metres, clamped to `min_radius:max_radius`. + +# Notes + +The implementation evaluates the magnitude of the complex skin depth at the +first frequency, + +```math +\\delta = \\left|\\sqrt{\\frac{\\rho}{\\mathrm{j}\\,2\\pi f\\mu}}\\right|, +``` + +using the finite earth layer with the largest resistivity. +""" +function calc_domain_size(args...; kwargs...) + _warn_fem(:calc_domain_size) + return _extension().calc_domain_size(args...; kwargs...) +end + +function preview_results(args...; kwargs...) + _warn_fem(:preview_results) + return _extension().preview_results(args...; kwargs...) +end -# 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 +function FormulationSet(::Val{:FEM}; kwargs...) + _warn_fem(:FormulationSet) + return _extension().formulation_set(; kwargs...) +end end # module FEM diff --git a/src/engine/fem/cable.jl b/src/engine/fem/cable.jl index d540f430..4ace7ea5 100644 --- a/src/engine/fem/cable.jl +++ b/src/engine/fem/cable.jl @@ -24,74 +24,74 @@ $(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" + # 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 """ @@ -120,116 +120,111 @@ $(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) + 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 """ @@ -258,195 +253,189 @@ $(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 + 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 """ @@ -455,8 +444,8 @@ $(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) + 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] @@ -469,7 +458,8 @@ function _make_cablepart!(workspace::FEMWorkspace, part::Sector, # 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) + 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))" @@ -482,12 +472,13 @@ function _make_cablepart!(workspace::FEMWorkspace, part::Sector, 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) + 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 + 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 @@ -499,40 +490,43 @@ function _make_cablepart!(workspace::FEMWorkspace, part::Sector, 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_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] + 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) + 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) + 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) + 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 + 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) @@ -566,134 +560,129 @@ $(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) + 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_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 + 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 - 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) - + # 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 index 654d85a4..ea5e29e3 100644 --- a/src/engine/fem/drawing.jl +++ b/src/engine/fem/drawing.jl @@ -4,107 +4,105 @@ 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 + 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) @@ -128,7 +126,7 @@ 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) + return gmsh.model.occ.add_point(x, y, z) end """ @@ -154,61 +152,61 @@ 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, + 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 + # 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 """ @@ -233,58 +231,55 @@ 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, + 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 - 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 + return tag, mesh_points, marker, arc_markers end - """ $(TYPEDSIGNATURES) @@ -308,84 +303,83 @@ 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, + 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 + # 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) @@ -410,14 +404,14 @@ 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) + # 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 """ @@ -445,11 +439,11 @@ 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) + 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) + return gmsh.model.occ.add_circle_arc(p1, pc, p2) end """ @@ -474,7 +468,7 @@ 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) + return gmsh.model.occ.add_circle(x, y, 0.0, radius) end """ @@ -498,160 +492,160 @@ 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]) + # 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, + 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 + # 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" + @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)] + 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() @@ -686,12 +680,12 @@ function _densify_vertices(vertices::Vector{<:Point}, max_len::Number) 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) @@ -703,24 +697,26 @@ function _densify_vertices(vertices::Vector{<:Point}, max_len::Number) return new_vertices end -function draw_polygon_with_hole(outer_vertices::Vector{<:Point}, inner_vertices::Vector{<:Point}, max_edge_length::Number) - +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_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_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() @@ -733,57 +729,55 @@ function draw_polygon_with_hole(outer_vertices::Vector{<:Point}, inner_vertices: 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 + # Check if cable_idx is empty + if isempty(cable_idx) + Base.error("Cable index vector cannot be empty") + 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] + # 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 - # Calculate centroid - centroid_x = sum(horz_coords) / length(horz_coords) - centroid_y = sum(vert_coords) / length(vert_coords) + # 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] - # Find the maximum distance from centroid to any cable's edge - max_distance = 0.0 - characteristic_len = Inf + # Calculate centroid + centroid_x = sum(horz_coords) / length(horz_coords) + centroid_y = sum(vert_coords) / length(vert_coords) - for idx in cable_idx - cable_position = cable_system.cables[idx] + # Find the maximum distance from centroid to any cable's edge + max_distance = 0.0 + characteristic_len = Inf - # Calculate distance from centroid to cable center - distance_to_center = sqrt( - (cable_position.horz - centroid_x)^2 + (cable_position.vert - centroid_y)^2, - ) + for idx in cable_idx + cable_position = cable_system.cables[idx] - # Get the outermost component (last component in the vector) - if !isempty(cable_position.design_data.components) - last_component = cable_position.design_data.components[end] + # Calculate distance from centroid to cable center + distance_to_center = sqrt( + (cable_position.horz - centroid_x)^2 + (cable_position.vert - centroid_y)^2, + ) - outer_radius = last_component.insulator_group.r_ex + # Get the outermost component (last component in the vector) + if !isempty(cable_position.design_data.components) + last_component = cable_position.design_data.components[end] - insulator_radius_in = last_component.insulator_group.layers[end].r_in - last_layer_thickness = outer_radius - insulator_radius_in + 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 + # 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) + return (centroid_x, centroid_y, max_distance, characteristic_len) end - """ $(TYPEDSIGNATURES) @@ -812,21 +806,21 @@ 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 + 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) @@ -840,12 +834,13 @@ function draw_polygon(vertices::Vector{<:Point}, max_edge_length::Number) 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)] + 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() @@ -859,5 +854,3 @@ function draw_polygon(vertices::Vector{<:Point}, max_edge_length::Number) return surface_tag, marker end - - diff --git a/src/engine/fem/encoding.jl b/src/engine/fem/encoding.jl index 93da41c0..a5d639ac 100644 --- a/src/engine/fem/encoding.jl +++ b/src/engine/fem/encoding.jl @@ -4,7 +4,7 @@ Implements the unified SCCCOOGMMM scheme for all entity types. """ """ - encode_physical_group_tag(surface_type, entity_num, component_num, material_group, material_id) + 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. @@ -24,7 +24,7 @@ Encode entity information into a single integer ID using the unified SCCCOOGMMM ```julia # Cable core conductor with material ID 5 -tag = encode_physical_group_tag(1, 1, 1, 1, 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) @@ -34,62 +34,62 @@ 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, + 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 + # 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_physical_group_tag(tag) Decode a physical group tag into its component parts. @@ -105,84 +105,84 @@ Decode a physical group tag into its component parts. ```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)) +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) + 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, + 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 + # 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) + 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 + # 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]) + # 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) + return (curve_type, layer_idx, sequence_num) end """ - get_material_group(part) + get_material_group(part) Get the material group (conductor or insulator) for a cable part based on its type. @@ -201,17 +201,17 @@ 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 + 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_material_group(earth_model, layer_idx) Get the material group (conductor or insulator) for an earth layer. @@ -232,17 +232,17 @@ group = get_material_group(earth_model, 2) # Layer 2 is earth -> Returns 1 (con ``` """ 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 + # 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) + get_or_register_material_id(workspace, material) Find or create a unique ID for a material within the current workspace. @@ -262,52 +262,51 @@ 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 + # 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, + 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 - + # 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) @@ -333,153 +332,153 @@ Format: cable_X___layer__[_wire_N][_phase_M] ```julia name = $(FUNCTIONNAME)( - cable_idx=1, - component_id="core", - group_type=1, - part_type="wire", - layer_idx=2, - wire_idx=3, - phase=1 + 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, + 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 + # 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 + # 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)" + # 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 + # 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 index 0de6a980..1bcc8558 100644 --- a/src/engine/fem/helpers.jl +++ b/src/engine/fem/helpers.jl @@ -3,7 +3,6 @@ Utility functions for the FEMTools.jl module. These functions provide various utilities for file management, logging, etc. """ - """ $(TYPEDSIGNATURES) @@ -25,7 +24,6 @@ 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) @@ -58,14 +56,14 @@ function setup_paths(cable_system::LineCableSystem, formulation::FEMFormulation) admittance_file = joinpath(case_dir, "$(case_id)_$(admittance_res).pro") # Return compiled dictionary of paths - paths = Dict{Symbol,String}( + 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, + :admittance_file => admittance_file ) @debug "Paths configured: $(join(["$(k): $(v)" for (k,v) in paths], ", "))" @@ -93,16 +91,16 @@ Clean up files based on configuration flags. $(FUNCTIONNAME)(paths, solver) ``` """ -function cleanup_files(paths::Dict{Symbol,String}, opts::NamedTuple) +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) + 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) + rm(paths[:geo_file], force = true) @info "Removed existing geometry file: $(display_path(paths[:geo_file]))" end end @@ -113,7 +111,7 @@ function cleanup_files(paths::Dict{Symbol,String}, opts::NamedTuple) for file in readdir(paths[:case_dir]) if endswith(file, ".pro") filepath = joinpath(paths[:case_dir], file) - rm(filepath, force=true) + rm(filepath, force = true) @info "Removed existing problem file: $(display_path(filepath))" end end @@ -123,7 +121,7 @@ function cleanup_files(paths::Dict{Symbol,String}, opts::NamedTuple) for file in readdir(paths[:results_dir]) filepath = joinpath(paths[:results_dir], file) if isfile(filepath) - rm(filepath, force=true) + rm(filepath, force = true) end end @info "Cleared existing results in: $(display_path(paths[:results_dir]))" @@ -132,19 +130,16 @@ function cleanup_files(paths::Dict{Symbol,String}, opts::NamedTuple) end function read_results_file( - fem_formulation::Union{AbstractImpedanceFormulation,AbstractAdmittanceFormulation}, - workspace::FEMWorkspace; - file::Union{String,Nothing}=nothing, + fem_formulation::Union{AbstractImpedanceFormulation, AbstractAdmittanceFormulation}, + workspace::FEMWorkspace; + file::Union{String, Nothing} = nothing ) - - results_path = - joinpath(workspace.paths[:results_dir], lowercase(fem_formulation.resolution_name)) + 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))")) + 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) @@ -153,8 +148,8 @@ function read_results_file( # Read all lines from file lines = readlines(filepath) - n_rows = - sum([length(c.design_data.components) for c in workspace.problem_def.system.cables]) + 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) @@ -167,14 +162,13 @@ function read_results_file( # 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]) + 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) @@ -219,10 +213,10 @@ function map_verbosity_to_gmsh(verbosity::Int) end function calc_domain_size( - earth_params::EarthModel, - f::Vector{<:Float64}; - min_radius=5.0, - max_radius=5000.0, + 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) @@ -244,20 +238,19 @@ 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))") + freq_dir = joinpath(dirname(results_dir), "results_f=$(round(frequency, sigdigits=6))") if isdir(results_dir) - mv(results_dir, freq_dir, force=true) + 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)) + readdir(workspace.paths[:case_dir], join = true)) for f in case_files - mv(f, joinpath(freq_dir, basename(f)), force=true) + mv(f, joinpath(freq_dir, basename(f)), force = true) end end catch e @@ -268,48 +261,23 @@ 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)) + 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? +# 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 +function _resolve_getdp_path(::NamedTuple) + path = GetDP.get_getdp_executable() + _is_valid_getdp_exe(path) || + Base.error( + "GetDP failed its -info probe at $(repr(path)). " * + "Set GETDP_EXECUTABLE to a working executable or add getdp to PATH.", + ) + return path +end diff --git a/src/engine/fem/identification.jl b/src/engine/fem/identification.jl index b5c61b06..6a9ccd22 100644 --- a/src/engine/fem/identification.jl +++ b/src/engine/fem/identification.jl @@ -72,7 +72,6 @@ function process_fragments(workspace::FEMWorkspace) @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) @@ -159,10 +158,11 @@ function identify_by_marker(workspace::FEMWorkspace) 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}}() + 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 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 diff --git a/src/engine/fem/lineparamopts.jl b/src/engine/fem/lineparamopts.jl index e8eba978..108de3fd 100644 --- a/src/engine/fem/lineparamopts.jl +++ b/src/engine/fem/lineparamopts.jl @@ -1,34 +1,35 @@ Base.@kwdef struct FEMOptions <: AbstractFormulationOptions - common::LineParamOptions = LineParamOptions() + 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 + "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 + "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 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))")) + 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 +function Base.get(o::FEMOptions, s::Symbol, default) + hasproperty(o, s) ? getproperty(o, s) : default +end 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 index 9599143b..50c48a23 100644 --- a/src/engine/fem/materialprops.jl +++ b/src/engine/fem/materialprops.jl @@ -25,30 +25,30 @@ 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) + # 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 """ @@ -71,57 +71,55 @@ 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) + # 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))" + 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)" + 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) - earth_props = workspace.problem_def.earth_props - num_layers = length(earth_props.layers) - - if layer_idx <= num_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) + # 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 + 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) + # 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 + 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 + 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 index e621fec3..8d0cec03 100644 --- a/src/engine/fem/mesh.jl +++ b/src/engine/fem/mesh.jl @@ -33,69 +33,69 @@ depth = $(FUNCTIONNAME)(1.7241e-8, 1.0, 50.0) 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) + # 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 + # Constants + mu_0 = 4e-7 * π # Vacuum permeability - # Calculate skin depth - # δ = sqrt(ρ / (π * f * μ_0 * μ_r)) - return sqrt(rho / (π * freq * mu_0 * mu_r)) + # 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 + # 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, + r_in::Number, + r_ex::Number, + material::Material, + num_elements::Int, + workspace::FEMWorkspace ) - # Extract geometric properties - thickness = r_ex - r_in + # Extract geometric properties + thickness = r_ex - r_in - # Extract problem_def parameters - formulation = workspace.formulation - mesh_size = thickness / num_elements + # 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) + # 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 + return mesh_size end """ @@ -118,34 +118,31 @@ $(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)]" + 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 """ @@ -168,18 +165,18 @@ $(FUNCTIONNAME)(workspace) ``` """ function generate_mesh(workspace::FEMWorkspace) - # Generate 2D mesh - gmsh.model.mesh.generate(2) + # Generate 2D mesh + gmsh.model.mesh.generate(2) - # Get mesh statistics - nodes = gmsh.model.mesh.get_nodes() - elements = gmsh.model.mesh.get_elements() + # 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])) + 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" + @info "Mesh generation completed" + @info "Created mesh with $(num_nodes) nodes and $(num_elements) elements" end """ @@ -204,148 +201,147 @@ $(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" - + # 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) + # Initialize Gmsh model and set parameters + initialize_gmsh(workspace) - # Create geometry - @info "Creating domain boundaries..." - make_space_geometry(workspace) + # Create geometry + @info "Creating domain boundaries..." + make_space_geometry(workspace) - @info "Creating cable geometry..." - make_cable_geometry(workspace) + @info "Creating cable geometry..." + make_cable_geometry(workspace) - # Synchronize the model - gmsh.model.occ.synchronize() + # Synchronize the model + gmsh.model.occ.synchronize() - # Boolean operations - @info "Performing boolean operations..." - process_fragments(workspace) + # Boolean operations + @info "Performing boolean operations..." + process_fragments(workspace) - # Entity identification and entity assignment - @info "Identifying entities after fragmentation..." - identify_by_marker(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) + # Physical group assignment + @info "Assigning physical groups..." + assign_physical_groups(workspace) - # Mesh sizing - @info "Setting up mesh sizing..." - config_mesh_options(workspace) + # Mesh sizing + @info "Setting up mesh sizing..." + config_mesh_options(workspace) - # Mesh generation - @info "Generating mesh..." - generate_mesh(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 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]) + # 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 + 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 + # 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 index 1ef9bed8..736b8e80 100644 --- a/src/engine/fem/meshtransitions.jl +++ b/src/engine/fem/meshtransitions.jl @@ -6,103 +6,102 @@ Defines a mesh transition region for improved mesh quality in earth/air regions $(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} + "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") + 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 + # 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 + 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, + 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)) + (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") + # 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") + 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)) + # 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 + # 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 + 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 + # 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 + # 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, - ) + 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 index 70b6022f..e15d6744 100644 --- a/src/engine/fem/problemdefs.jl +++ b/src/engine/fem/problemdefs.jl @@ -31,8 +31,6 @@ # # The one-line constructor to "promote" a NamedTuple # FEMOptions(opts::NamedTuple) = FEMOptions(; opts...) - - """ $(TYPEDEF) @@ -42,149 +40,148 @@ 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 + "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 = (;), +function formulation_set(; 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, - ) + # 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 index baed15ab..9ee6e046 100644 --- a/src/engine/fem/solver.jl +++ b/src/engine/fem/solver.jl @@ -1,872 +1,858 @@ - -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 - + +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 index 51b771ea..787009cb 100644 --- a/src/engine/fem/space.jl +++ b/src/engine/fem/space.jl @@ -23,345 +23,340 @@ $(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" - + @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 index 1710f6e3..5f060a28 100644 --- a/src/engine/fem/types.jl +++ b/src/engine/fem/types.jl @@ -3,10 +3,10 @@ $(TYPEDEF) Abstract base type for workspace containers in the FEM simulation framework. -Workspace containers maintain the complete state of a simulation, including +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 +Concrete implementations should provide state tracking for all phases of the simulation process from geometry creation through results analysis. """ abstract type AbstractWorkspace end @@ -26,12 +26,12 @@ 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 + "Encoded physical tag \\[dimensionless\\]." + physical_group_tag::Int + "Name of the elementary surface." + elementary_name::String + "Target mesh size \\[m\\]." + mesh_size::Float64 end """ @@ -42,10 +42,10 @@ 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 + "Core entity data." + core::CoreEntityData + "Reference to original cable part." + cable_part::T end """ @@ -56,10 +56,10 @@ 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 + "Core entity data." + core::CoreEntityData + "Material properties of the domain." + material::Material end """ @@ -70,10 +70,10 @@ 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 + "Core entity data." + core::CoreEntityData + "Material properties of the domain." + material::Material end """ @@ -84,10 +84,10 @@ 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 + "Gmsh entity tag (will be defined after boolean fragmentation)." + tag::Int32 + "Entity-specific data." + data::T end """ @@ -118,24 +118,23 @@ entity = $(FUNCTIONNAME)(1, domain_data) ``` """ function GmshObject(tag::Integer, data::T) where {T <: AbstractEntityData} - return GmshObject{T}(Int32(tag), data) + 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 + 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 + problem::GetDP.Problem + resolution_name::String - function Electrodynamics() - return new(GetDP.Problem(), "Electrodynamics") - end -end \ No newline at end of file + function Electrodynamics() + return new(GetDP.Problem(), "Electrodynamics") + end +end diff --git a/src/engine/fem/visualization.jl b/src/engine/fem/visualization.jl index b884e403..814e6d81 100644 --- a/src/engine/fem/visualization.jl +++ b/src/engine/fem/visualization.jl @@ -23,7 +23,6 @@ $(FUNCTIONNAME)(workspace) ``` """ function preview_mesh(workspace::FEMWorkspace) - if gmsh.is_initialized() == 0 gmsh.initialize() @debug "Initialized Gmsh for mesh preview" @@ -157,4 +156,4 @@ function preview_results(workspace::FEMWorkspace, pos_file::String) finally gmsh.finalize() end -end \ No newline at end of file +end diff --git a/src/engine/fem/workspace.jl b/src/engine/fem/workspace.jl index eaf54bd4..f9e3256b 100644 --- a/src/engine/fem/workspace.jl +++ b/src/engine/fem/workspace.jl @@ -1,4 +1,4 @@ -import ..Engine: _get_earth_data +import LineCableModels.Engine: _get_earth_data """ $(TYPEDEF) @@ -9,274 +9,271 @@ This is the main container that maintains all state during the simulation proces $(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 + "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 + 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/helpers.jl b/src/engine/helpers.jl index 586fc5b2..1d35d906 100644 --- a/src/engine/helpers.jl +++ b/src/engine/helpers.jl @@ -7,155 +7,153 @@ 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 + # 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, + functor::AbstractEHEMFormulation, + earth_model::EarthModel, + freq::Vector{<:REALSCALAR}, + T::DataType ) - return functor(earth_model, freq, T) + return functor(earth_model, freq, T) end """ -Default method for when no EHEM formulation is provided. +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 = μ) + 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 + 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 + 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 + 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) + 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 + # 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 + # 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 + return A end # function _to_phase!( @@ -179,4 +177,3 @@ end # return M # end - diff --git a/src/engine/insulationadmittance/InsulationAdmittance.jl b/src/engine/insulationadmittance/InsulationAdmittance.jl index f99ea759..dd37b2b2 100644 --- a/src/engine/insulationadmittance/InsulationAdmittance.jl +++ b/src/engine/insulationadmittance/InsulationAdmittance.jl @@ -1,13 +1,10 @@ """ - LineCableModels.Engine.InsulationAdmittance + LineCableModels.Engine.InsulationAdmittance # Dependencies $(IMPORTS) -# Exports - -$(EXPORTS) """ module InsulationAdmittance diff --git a/src/engine/insulationadmittance/lossless.jl b/src/engine/insulationadmittance/lossless.jl index 79f3369a..7ecd0b0f 100644 --- a/src/engine/insulationadmittance/lossless.jl +++ b/src/engine/insulationadmittance/lossless.jl @@ -2,37 +2,35 @@ 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, + 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 - 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 - # Constants - eps_i = T(ε₀) * epsr_i - - - return Complex{T}(log(r_ex / r_in) / (2π * eps_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}, + 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], - ) + 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 index f5f3ba24..0d87343b 100644 --- a/src/engine/insulationadmittance/parallelrc.jl +++ b/src/engine/insulationadmittance/parallelrc.jl @@ -25,8 +25,9 @@ admittance calculation. """ struct ParallelRC <: InsulationAdmittanceFormulation end -get_description(::ParallelRC) = - "Parallel-RC insulation (constant conductivity and permittivity)" +function get_description(::ParallelRC) + "Parallel-RC insulation (constant conductivity and permittivity)" +end """ (formulation::ParallelRC)(r_in, r_ex, rho, eps_r, s) @@ -59,40 +60,40 @@ 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}, + 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 + 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 + 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) + return s / (conductance + s * capacitance) end @inline function potential_coefficient( - f::ParallelRC, - ws, - component_idx::Int, - s::Complex{T}, + 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 + 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 60854f40..770ce4da 100644 --- a/src/engine/insulationimpedance/InsulationImpedance.jl +++ b/src/engine/insulationimpedance/InsulationImpedance.jl @@ -1,20 +1,16 @@ """ - LineCableModels.Engine.InsulationImpedance + LineCableModels.Engine.InsulationImpedance # Dependencies $(IMPORTS) -# Exports - -$(EXPORTS) """ module InsulationImpedance # Export public API export Lossless - # Module-specific dependencies using ...Commons import ...Commons: get_description diff --git a/src/engine/insulationimpedance/lossless.jl b/src/engine/insulationimpedance/lossless.jl index b70e3a8f..ea91092e 100644 --- a/src/engine/insulationimpedance/lossless.jl +++ b/src/engine/insulationimpedance/lossless.jl @@ -3,20 +3,19 @@ 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}, + 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 - 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 - # 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 + return Complex{T}(jω * mu_i * log(r_ex / r_in) / 2π) +end diff --git a/src/engine/internalimpedance/InternalImpedance.jl b/src/engine/internalimpedance/InternalImpedance.jl index 6d839dd8..774f8002 100644 --- a/src/engine/internalimpedance/InternalImpedance.jl +++ b/src/engine/internalimpedance/InternalImpedance.jl @@ -1,13 +1,10 @@ """ - LineCableModels.Engine.InternalImpedance + LineCableModels.Engine.InternalImpedance # Dependencies $(IMPORTS) -# Exports - -$(EXPORTS) """ module InternalImpedance @@ -26,4 +23,3 @@ using ...Utils: _to_σ include("scaledbessel.jl") end # module InternalImpedance - diff --git a/src/engine/internalimpedance/scaledbessel.jl b/src/engine/internalimpedance/scaledbessel.jl index 8dafd717..ecc3f110 100644 --- a/src/engine/internalimpedance/scaledbessel.jl +++ b/src/engine/internalimpedance/scaledbessel.jl @@ -2,156 +2,144 @@ 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}, + 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")) + 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}, + ::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 - + # 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}, + ::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)) + # 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}, + ::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 + # Constants + mu_c = T(μ₀) * mur_c + sigma_c = _to_σ(rho_c) - w_in = m * r_in + # Calculate the reciprocal of the skin depth + m = sqrt(jω * mu_c * sigma_c) + w_ex = m * r_ex - sc_in = exp(abs(real(w_in)) - w_ex) - sc_ex = exp(abs(real(w_ex)) - w_in) - sc = sc_in / sc_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) - # Bessel function terms with uncertainty handling - N = 1.0 / sc_ex + else + w_in = m * r_in - D = - (besselix(1, w_ex)) * - (besselkx(1, w_in)) - - sc * - (besselix(1, w_in)) * - (besselkx(1, w_ex)) + sc_in = exp(abs(real(w_in)) - w_ex) + sc_ex = exp(abs(real(w_ex)) - w_in) + sc = sc_in / sc_ex - return Complex{T}((1 / (2π * r_in * r_ex * sigma_c)) * (N / D)) + # Bessel function terms with uncertainty handling + N = 1.0 / sc_ex - end + 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/lineparamopts.jl b/src/engine/lineparamopts.jl index d3e03c04..09fac9f2 100644 --- a/src/engine/lineparamopts.jl +++ b/src/engine/lineparamopts.jl @@ -1,23 +1,22 @@ 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 + "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 @@ -29,60 +28,58 @@ _to_nt(::Nothing) = (;) 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 _select_keys(nt::NamedTuple, allowed::Set{Symbol}) + (; (k => v for (k, v) in pairs(nt) if k in allowed)...) +end function build_options(::Type{O}, opts; - strict::Bool = true, + strict::Bool = true ) where {O <: AbstractFormulationOptions} + nt = _to_nt(opts) - 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) + 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 + 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...) + 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) +function build_options( + ::Type{O}, + c::LineParamOptions; + kwargs... +) where {O <: AbstractFormulationOptions} + O(; common = c) +end # 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") + 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 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))")) + 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 +function Base.get(o::EMTOptions, s::Symbol, default) + hasproperty(o, s) ? getproperty(o, s) : default +end asnamedtuple(o::EMTOptions) = (; (k=>getproperty(o, k) for k in propertynames(o))...) - - - - - diff --git a/src/engine/lineparams.jl b/src/engine/lineparams.jl index 5c09a92e..34e8d62e 100644 --- a/src/engine/lineparams.jl +++ b/src/engine/lineparams.jl @@ -1,9 +1,9 @@ struct SeriesImpedance{T} <: AbstractArray{T, 3} - values::Array{T, 3} # n×n×nfreq, units: Ω/m + 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 + values::Array{T, 3} # n×n×nfreq, units: S/m end """ @@ -14,54 +14,55 @@ Represents the frequency-dependent line parameters (series impedance and shunt a $(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) + "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 + function LineParameters( + Z::SeriesImpedance{T}, + Y::ShuntAdmittance{T}, + f::AbstractVector{U} + ) where {T <: COMPLEXSCALAR, U <: REALSCALAR} + LineParameters(PhaseDomain, Z, Y, f) + end end SeriesImpedance(A::AbstractArray{T, 3}) where {T} = SeriesImpedance{T}(Array(A)) @@ -75,23 +76,23 @@ $(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) - +function 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) +end # 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) - +function LineParameters( + Z::AbstractArray{Tc, 3}, + Y::AbstractArray{Tc, 3}, + f::AbstractVector{U} +) where {Tc <: COMPLEXSCALAR, U <: REALSCALAR} + LineParameters(PhaseDomain, Z, Y, f) +end # """ # $(TYPEDSIGNATURES) diff --git a/src/engine/plot.jl b/src/engine/plot.jl index b9fb3eec..a55bcab0 100644 --- a/src/engine/plot.jl +++ b/src/engine/plot.jl @@ -1,1179 +1,797 @@ using Makie -import Makie: plot +import LineCableModels.Engine: 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 +const _ICON_FN = (icon; text = nothing, kwargs...) -> with_icon( + icon; text = text === nothing ? "" : text, kwargs...) -struct ComponentMetadata - component::Symbol - quantity::Symbol - symbol::String - title::String - axis_label::String - unit::UnitSpec -end +using LineCableModels.Engine: LP_FIG_SIZE, UnitSpec, ComponentMetadata, + get_description, get_symbol, get_unit_symbol, parent_kind, + metric_exponent, + prefix_symbol, quantity_scale, length_scale, frequency_scale, + unit_text, + length_unit_text, composite_unit, frequency_axis_label, + normalize_quantity_units, + resolve_quantity_prefix, resolve_conductors, collect_indices, + components_for, + component_values, reactance_to_l, reactance_to_c, legend_label 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, ")") + 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 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, - ), - ) + 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 + 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, + 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 + 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, + 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 + 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, + 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 + 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, + 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 + 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, + 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 + 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..., + 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) + 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..., + 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) + 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..., + 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) + 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 + 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..., + 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]) + 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, + 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 + 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, + 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) + 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 + 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, - ) + # ---- 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 + 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/plotmetadata.jl b/src/engine/plotmetadata.jl new file mode 100644 index 00000000..90f7e027 --- /dev/null +++ b/src/engine/plotmetadata.jl @@ -0,0 +1,385 @@ +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 + +function get_description(::SeriesImpedance) + ( + impedance = "Series impedance", + resistance = "Series resistance", + inductance = "Series inductance" + ) +end + +get_symbol(::SeriesImpedance) = ( + impedance = "Z", + resistance = "R", + inductance = "L" +) + +get_unit_symbol(::SeriesImpedance) = ( + impedance = "Ω", + resistance = "Ω", + inductance = "H" +) + +function get_description(::ShuntAdmittance) + ( + admittance = "Shunt admittance", + conductance = "Shunt conductance", + capacitance = "Shunt capacitance" + ) +end + +get_symbol(::ShuntAdmittance) = ( + admittance = "Y", + conductance = "G", + capacitance = "C" +) + +function get_unit_symbol(::ShuntAdmittance) + ( + admittance = "S", + conductance = "S", + capacitance = "F" + ) +end + +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 diff --git a/src/engine/problemdefs.jl b/src/engine/problemdefs.jl index cf1279d8..3754fd18 100644 --- a/src/engine/problemdefs.jl +++ b/src/engine/problemdefs.jl @@ -7,153 +7,149 @@ Represents a line parameters computation problem for a given physical cable syst $(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 + "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 """ @@ -164,81 +160,80 @@ Represents the electromagnetic transient (EMT) formulation set for cable or line $(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 + "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 = (;), + 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, - ) + 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/reduction.jl b/src/engine/reduction.jl index 8ceed953..5a4ebedc 100644 --- a/src/engine/reduction.jl +++ b/src/engine/reduction.jl @@ -1,150 +1,149 @@ 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 + 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 + 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 + 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] + 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 + kronify(M, phase_map) + Kron elimination """ function kronify( - M::Matrix{Complex{T}}, - phase_map::Vector{Int}, + M::Matrix{Complex{T}}, + phase_map::Vector{Int} ) where {T <: REALSCALAR} - keep = findall(!=(0), phase_map) - eliminate = findall(==(0), phase_map) + 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] + M11 = M[keep, keep] + M12 = M[keep, eliminate] + M21 = M[eliminate, keep] + M22 = M[eliminate, eliminate] - return M11 - (M12 * inv(M22)) * M21 + return M11 - (M12 * inv(M22)) * M21 end """ - kronify!(M, phase_map, Mred) - Kron's angry little brother (in-place) + 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}}, + 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) + 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 + 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")) + 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 + # 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 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 + # -------- 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/solver.jl b/src/engine/solver.jl index 6a029bce..4ae91635 100644 --- a/src/engine/solver.jl +++ b/src/engine/solver.jl @@ -1,370 +1,370 @@ function compute!( - problem::LineParametersProblem{T}, - formulation::EMTFormulation, + 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 + 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 + 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 + 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 + ρ = @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, + 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 + @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 in 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, + 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 + # 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/transforms/Transforms.jl b/src/engine/transforms/Transforms.jl index f2540a63..9b2f384b 100644 --- a/src/engine/transforms/Transforms.jl +++ b/src/engine/transforms/Transforms.jl @@ -1,13 +1,10 @@ """ - LineCableModels.Engine.Transforms + LineCableModels.Engine.Transforms # Dependencies $(IMPORTS) -# Exports - -$(EXPORTS) """ module Transforms @@ -19,7 +16,8 @@ using ...Commons import ...Commons: get_description, PhaseDomain, ModalDomain import ...Utils: symtrans, symtrans!, offdiag_ratio, to_nominal import ..Engine: - AbstractTransformFormulation, LineParameters, SeriesImpedance, ShuntAdmittance + AbstractTransformFormulation, LineParameters, SeriesImpedance, + ShuntAdmittance # using Measurements @@ -27,18 +25,17 @@ using LinearAlgebra # using GenericLinearAlgebra using NLsolve - include("fortescue.jl") include("eiglevenberg.jl") function (F::AbstractTransformFormulation)( - lp::LineParameters{Tc, U, ModalDomain}, + lp::LineParameters{Tc, U, ModalDomain}, ) where {Tc <: COMPLEXSCALAR, U <: REALSCALAR} - throw( - ErrorException( - "Not yet implemented: inverse $(nameof(typeof(F)))( ::LineParameters{<:COMPLEXSCALAR,<:REALSCALAR,ModalDomain} )", - ), - ) + 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 index d20050b1..351b32d5 100644 --- a/src/engine/transforms/eiglevenberg.jl +++ b/src/engine/transforms/eiglevenberg.jl @@ -1,13 +1,15 @@ struct Levenberg <: AbstractTransformFormulation - tol::BASE_FLOAT + 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)" +function get_description( + ::Levenberg, +) + "Levenberg–Marquardt (frequency-tracked eigen decomposition)" +end """ $(TYPEDSIGNATURES) @@ -31,67 +33,65 @@ transformation matrices and a **modal-domain** `LineParameters` holding the """ function (f::Levenberg)( - lp::LineParameters{Tc, U, PhaseDomain}, + 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 + 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 #= --------------------------------------------------------------------------- @@ -101,171 +101,169 @@ 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}, + 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 + 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, + 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 + # 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 + 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 @@ -274,94 +272,92 @@ end # 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}, + 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 + 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 + 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 + @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 index 2d8ecf9f..9ce0412f 100644 --- a/src/engine/transforms/fortescue.jl +++ b/src/engine/transforms/fortescue.jl @@ -1,5 +1,5 @@ struct Fortescue <: AbstractTransformFormulation - tol::BASE_FLOAT + tol::BASE_FLOAT end # Convenient constructor with default tolerance Fortescue(; tol::BASE_FLOAT = BASE_FLOAT(1e-4)) = Fortescue(tol) @@ -11,44 +11,42 @@ $(TYPEDSIGNATURES) Functor implementation for `Fortescue`. """ function (f::Fortescue)( - lp::LineParameters{Tc, U, PhaseDomain}, + 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) + _, 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]) + @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' + 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 + 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) + 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' + 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 index ec6582df..2a1cabd5 100644 --- a/src/engine/types.jl +++ b/src/engine/types.jl @@ -20,18 +20,17 @@ abstract type EarthAdmittanceFormulation <: AbstractAdmittanceFormulation end abstract type AbstractTransformFormulation <: AbstractFormulationSet end """ - FormulationSet(...) + 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). +Abstract type representing different equivalent homogeneous earth models (EHEM). Used in the multi-dispatch implementation of `_calc_ehem_properties!`. # Currently available formulations @@ -40,5 +39,3 @@ Abstract type representing different equivalent homogeneous earth models (EHEM). abstract type AbstractEHEMFormulation <: AbstractFormulationSet end abstract type AbstractFormulationOptions end - - diff --git a/src/engine/workspace.jl b/src/engine/workspace.jl index 2e4ecb49..8f01222a 100644 --- a/src/engine/workspace.jl +++ b/src/engine/workspace.jl @@ -9,86 +9,84 @@ for all subsequent computational steps. $(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} + "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) @@ -96,144 +94,137 @@ Initializes and populates the [`EMTWorkspace`](@ref) by normalizing a [`LineParametersProblem`](@ref) into flat, type-stable arrays. """ function init_workspace( - problem::LineParametersProblem{T}, - formulation::EMTFormulation, + 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, - ) + 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/importexport/ImportExport.jl b/src/importexport/ImportExport.jl index 75bcaa6a..6ab99cdf 100644 --- a/src/importexport/ImportExport.jl +++ b/src/importexport/ImportExport.jl @@ -1,5 +1,5 @@ """ - 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. @@ -18,9 +18,6 @@ and proper handling of Julia-specific types like `Measurement` objects and `Inf` $(IMPORTS) -# Exports - -$(EXPORTS) """ module ImportExport @@ -36,8 +33,9 @@ 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 + InsulatorGroup, CircStrands, RectStrands, Strip, Tubular, Semicon, + Insulator, + LineCableSystem, NominalData import ..Engine: LineParameters, SeriesImpedance, ShuntAdmittance using Measurements using EzXML @@ -60,8 +58,9 @@ Export [`LineCableModels`](@ref) data for use in different EMT-type programs. $(METHODLIST) """ # function export_data end -export_data(backend::Symbol, args...; kwargs...) = - export_data(Val(backend), args...; kwargs...) +function export_data(backend::Symbol, args...; kwargs...) + export_data(Val(backend), args...; kwargs...) +end include("serialize.jl") include("deserialize.jl") diff --git a/src/importexport/atp.jl b/src/importexport/atp.jl index fa21e80a..64df3ce3 100644 --- a/src/importexport/atp.jl +++ b/src/importexport/atp.jl @@ -21,23 +21,23 @@ present in the groups/components at the time of export.* 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`: +2. For each [`LineCableModels.DataModel.CablePosition`](@ref) in `cable_system.cables`: * Write a `` element with: - * `NumCond` = number of [`CableComponent`](@ref)s in the design, - * `Rout` = outermost radius of the design (m), - * `PosX`, `PosY` = cable coordinates (m). + * `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. + * `Rin`, `Rout` — from the component’s conductor group, + * `rho` — conductor equivalence via [`LineCableModels.DataModel.BaseParams.calc_equivalent_rho`](@ref), + * `muC` — conductor relative permeability via [`LineCableModels.DataModel.BaseParams.calc_equivalent_mu`](@ref), + * `muI` — insulator relative permeability (taken from the first insulating layer’s material), + * `epsI` — insulation relative permittivity via [`LineCableModels.DataModel.BaseParams.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`. @@ -67,207 +67,198 @@ Units are printed in the XML file according to the ATPDraw specifications: ```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") + file_name = "system_id_export.xml") println("Exported to: ", file) ``` -# See also - -* [`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, + 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 + 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 end - - # 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 +# I DON´T TEST THEREFORE GO TO THE GARBAGE # """ # read_atp_data(file_name::String, cable_system::LineCableSystem) @@ -371,7 +362,6 @@ function read_data end # return Ze_reordered + Zi_reordered # end - """$(TYPEDSIGNATURES) Export calculated [`LineParameters`](@ref) (series impedance **Z** and shunt admittance **Y**) to an **compliant** `ZY` XML file. @@ -423,132 +413,114 @@ file2 = $(FUNCTIONNAME)(:atp, lp; cable_system = sys) println("Exported ZY to: ", file2) # => "\$(sys.system_id)_ZY_export.xml" ``` -# See also - -* [`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, + 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 + # 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 + end end diff --git a/src/importexport/cableslibrary.jl b/src/importexport/cableslibrary.jl index 421cbc52..b1d0b6ba 100644 --- a/src/importexport/cableslibrary.jl +++ b/src/importexport/cableslibrary.jl @@ -14,34 +14,33 @@ The format is determined by the file extension: - The absolute path of the saved file, or `nothing` on failure. """ function save( - library::CablesLibrary; - file_name::String = "cables_library.json", + 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 + 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 """ @@ -59,11 +58,11 @@ but can be faster and preserves exact types. - 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) + # 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) end """ @@ -79,19 +78,19 @@ Saves the [`CablesLibrary`](@ref) to a JSON file using the custom serialization - 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) + # 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 """ @@ -111,33 +110,33 @@ The format is determined by the file extension: - The modified [`CablesLibrary`](@ref) instance. """ function load!( - library::CablesLibrary; # Type annotation ensures it's the correct object - file_name::String = "cables_library.json", + 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 + 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 end """ @@ -154,24 +153,24 @@ into the provided library object. - 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 + 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 end """ @@ -188,232 +187,233 @@ using the detailed, sequential reconstruction logic. - 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 + # 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 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. +Helper function to reconstruct a [`ConductorGroup`](@ref) or [`InsulatorGroup`](@ref) object with the first layer of the respective `AbstractCablePart`. 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). +- A reconstructed [`ConductorGroup`](@ref) object with the initial `AbstractCablePart`. # 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 + 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") + 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) + 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 + 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 end """ @@ -443,238 +443,234 @@ This function handles the sequential process of building cable designs: - Error if reconstruction fails at any step. """ function _reconstruct_cabledesign( - cable_id::String, - design_data::Dict, + 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 + @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 end diff --git a/src/importexport/deserialize.jl b/src/importexport/deserialize.jl index 88551db2..9dbc4799 100644 --- a/src/importexport/deserialize.jl +++ b/src/importexport/deserialize.jl @@ -1,38 +1,37 @@ @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 + 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 + 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. +into a Julia `Type` object. Resolution order: 1) If fully-qualified (contains '.') and not parametric, walk modules (no eval). @@ -52,39 +51,39 @@ Resolution order: - `Error` if the type cannot be resolved. """ 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 + 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 @@ -109,76 +108,75 @@ identified by `__julia_type__`. Ensures plain dictionaries use Symbol keys. - 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 + 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 end """ @@ -204,11 +202,10 @@ 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 +function get_as(d::AbstractDict, key::Union{Symbol, AbstractString}, default, ::Type{T}) where {T} + v = get(d, key, default) + v === missing ? missing : coerce_to_T(_deserialize_value(v), T) +end """ $(TYPEDSIGNATURES) @@ -228,101 +225,100 @@ if the keyword attempt fails with a specific `MethodError`. - `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) - - 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)) - - # @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 - - 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 - - # --- Attempt 2: Positional Constructor (Fallback) --- - # @info "Attempting positional construction for $T" # Debug logging - fields_in_order = fieldnames(T) - positional_args = [] - - 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 - - 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 - - # @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 - - # This line should ideally not be reached - error( - "Failed to construct object of type $T using both keyword and positional methods.", - ) + # 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) + 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)) + + # @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 + + 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 + + # --- Attempt 2: Positional Constructor (Fallback) --- + # @info "Attempting positional construction for $T" # Debug logging + fields_in_order = fieldnames(T) + positional_args = [] + + 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 + + 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 + + # @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 + + # This line should ideally not be reached + error( + "Failed to construct object of type $T using both keyword and positional methods.", + ) end diff --git a/src/importexport/materialslibrary.jl b/src/importexport/materialslibrary.jl index 64241049..66b02c5d 100644 --- a/src/importexport/materialslibrary.jl +++ b/src/importexport/materialslibrary.jl @@ -11,31 +11,29 @@ Saves a [`MaterialsLibrary`](@ref) to a JSON file. - The absolute path of the saved file, or `nothing` on failure. """ function save( - library::MaterialsLibrary; - file_name::String = "materials_library.json", + 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 + # 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 end """ @@ -51,22 +49,22 @@ Internal function to save the [`MaterialsLibrary`](@ref) to JSON. - 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) + # 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 """ @@ -82,41 +80,37 @@ Modifies the library in-place. # Returns - The modified [`MaterialsLibrary`](@ref) instance. -# See also -- [`MaterialsLibrary`](@ref) """ function load!( - library::MaterialsLibrary; - file_name::String = "materials_library.json", + 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 + 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 end """ @@ -131,63 +125,57 @@ Internal function to load materials from JSON into the library. # 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 + # 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 end diff --git a/src/importexport/pscad.jl b/src/importexport/pscad.jl index c1ab4bc7..ba75492d 100644 --- a/src/importexport/pscad.jl +++ b/src/importexport/pscad.jl @@ -3,7 +3,7 @@ Generates sequential IDs, used for simulation element identification (e.g., PSCA Starts from 100,000,000 and increments. =# let current_id = 100000000 - global _next_id = () -> (id = current_id; current_id += 1; string(id)) + global _next_id = () -> (id = current_id; current_id += 1; string(id)) end """ @@ -30,839 +30,831 @@ 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, + 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 + 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 + end end diff --git a/src/importexport/serialize.jl b/src/importexport/serialize.jl index aae900d3..9c538ae9 100644 --- a/src/importexport/serialize.jl +++ b/src/importexport/serialize.jl @@ -29,69 +29,77 @@ _serializable_fields(obj::T) where {T} = fieldnames(T) # 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 _serializable_fields(::NominalData) + ( + :designation_code, + :U0, + :U, + :conductor_cross_section, + :screen_cross_section, + :armor_cross_section, + :resistance, + :capacitance, + :inductance + ) +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 _serializable_fields(::CircStrands) + ( + :r_in, # Needed for first layer reconstruction + :radius_wire, + :num_wires, + :lay_ratio, + :material_props, + :temperature, + :lay_direction + ) +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 _serializable_fields(::RectStrands) + ( + :r_in, # Needed for first layer reconstruction + :thickness, + :width, + :num_wires, + :lay_ratio, + :material_props, + :temperature, + :lay_direction + ) +end _serializable_fields(::Tubular) = ( - :r_in, # Needed for first layer reconstruction - :r_ex, - :material_props, - :temperature, -) -_serializable_fields(::Strip) = ( - :r_in, # Needed for first layer reconstruction - :r_ex, - :width, - :lay_ratio, - :material_props, - :temperature, - :lay_direction, + :r_in, # Needed for first layer reconstruction + :r_ex, + :material_props, + :temperature ) +function _serializable_fields(::Strip) + ( + :r_in, # Needed for first layer reconstruction + :r_ex, + :width, + :lay_ratio, + :material_props, + :temperature, + :lay_direction + ) +end # Layer Types (Insulator Parts) _serializable_fields(::Insulator) = ( - :r_in, # Needed for first layer reconstruction - :r_ex, - :material_props, - :temperature, + :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, + :r_in, # Needed for first layer reconstruction + :r_ex, + :material_props, + :temperature ) # Group Types - Only serialize the layers needed for reconstruction. @@ -107,7 +115,6 @@ _serializable_fields(::CableDesign) = (:cable_id, :nominal_data, :components) _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 @@ -121,61 +128,59 @@ using the `_serializable_fields` trait. =# # 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 + 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) @@ -190,38 +195,38 @@ using the [`_serializable_fields`](@ref) trait. - 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 + 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 end diff --git a/src/importexport/tralin.jl b/src/importexport/tralin.jl index 05ddf3ec..7a1d66d6 100644 --- a/src/importexport/tralin.jl +++ b/src/importexport/tralin.jl @@ -1,238 +1,235 @@ 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, + 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 + # -- 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 end - # --- 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) + # 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) - # 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 + # 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) end # --- public: extract the frequency vector from the "FREQUENCY OF HARMONIC CURRENT" section --- """ - extract_tralin_frequencies(file_or_lines) -> Vector{Float64} + extract_tralin_frequencies(file_or_lines) -> Vector{Float64} 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\\]. @@ -240,232 +237,225 @@ up to the next page header. Returns a `Vector{Float64}` in \\[Hz\\]. Accepts either a filename (`AbstractString`) or a preloaded `Vector{String}` with file lines. """ 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 + # 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 end """ - parse_tralin_file(filename) + parse_tralin_file(filename) Parse a TRALIN file and extract impedance, admittance, and potential coefficient matrices for multiple frequency samples. """ 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 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 end """ - extract_tralin_variable(fileLines, order, str_init, str_final) + extract_tralin_variable(fileLines, order, str_init, str_final) Extracts matrix data between specified headers in `fileLines`, handling complex formatting. """ 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 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 end - """ - take_complex_list(s) + take_complex_list(s) Parses a string to identify real and complex numbers, with conditional scaling for scientific notation. """ 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) + clean_variable_list(variable_list_number, order) Cleans and arranges extracted list into a proper matrix format. """ 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] + # Trim row label elements and only keep the actual data + 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] + # 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] - # 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 + # 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 - return data_padded + return data_padded end # -- 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) + # Normalize types (ComplexF64 / Float64 by default; tweak if you need Measurements etc.) + 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) 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)")) +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/xlsx.jl b/src/importexport/xlsx.jl index 6e50767f..10ea0e15 100644 --- a/src/importexport/xlsx.jl +++ b/src/importexport/xlsx.jl @@ -7,155 +7,157 @@ 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 stringify(x::Measurements.Measurement) + @sprintf("%.12g ± %.6g", Measurements.value(x), Measurements.uncertainty(x)) +end function df_to_strings(df::DataFrame) - DataFrame((name => stringify.(df[!, name]) for name in names(df))...; copycols = false) + 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 + 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 + 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, + ::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 + # ---- 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/materials/Materials.jl b/src/materials/Materials.jl index 3bf59963..0a7bf6e7 100644 --- a/src/materials/Materials.jl +++ b/src/materials/Materials.jl @@ -1,5 +1,5 @@ """ - 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. @@ -15,9 +15,6 @@ The [`Materials`](@ref) module provides functionality for managing and utilizing $(IMPORTS) -# Exports - -$(EXPORTS) """ module Materials @@ -39,27 +36,26 @@ Defines electromagnetic and thermal properties of a material used in cable model $(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 - + "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 """ @@ -79,14 +75,14 @@ coerces values to `T`, and calls the strict numeric kernel. - `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), - ) + 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 include("materialslibrary.jl") diff --git a/src/materials/base.jl b/src/materials/base.jl index fdc7d1b9..306a0598 100644 --- a/src/materials/base.jl +++ b/src/materials/base.jl @@ -3,15 +3,15 @@ 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::String) + (lib.data[key] = value) +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) - """ $(TYPEDSIGNATURES) @@ -37,22 +37,16 @@ library = MaterialsLibrary() $(FUNCTIONNAME)(library, "copper") ``` -# 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." + 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." end - - """ $(TYPEDSIGNATURES) @@ -74,17 +68,13 @@ library = MaterialsLibrary() material = $(FUNCTIONNAME)(library, "copper") ``` -# See also - -- [`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 + material = get(library.data, name, default) + if material === nothing + @warn "Material '$name' not found in the library; returning default." + end + return material end """ @@ -103,20 +93,20 @@ Defines the display representation of a [`Material`](@ref) object for REPL or te - Nothing. Modifies `io` by writing text representation of the material. """ function Base.show(io::IO, ::MIME"text/plain", material::Material) - print(io, "Material with properties: [") + print(io, "Material with properties: [") - # Define fields to display - fields = [:rho, :eps_r, :mu_r, :T0, :alpha] + # Define fields to display + fields = [:rho, :eps_r, :mu_r, :T0, :alpha] - # 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 + # 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 - print(io, "]") + print(io, "]") end """ @@ -135,25 +125,25 @@ Defines the display representation of a [`MaterialsLibrary`](@ref) object for RE - 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 + 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 end """ @@ -172,27 +162,28 @@ Defines the display representation of a [`MaterialsLibrary`](@ref) object for RE - 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 + 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 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)) +function 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)) +end diff --git a/src/materials/dataframe.jl b/src/materials/dataframe.jl index b3fbf645..ea23b565 100644 --- a/src/materials/dataframe.jl +++ b/src/materials/dataframe.jl @@ -20,22 +20,17 @@ 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 - ] + 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 +end diff --git a/src/materials/materialslibrary.jl b/src/materials/materialslibrary.jl index c165b9d4..cdef5e27 100644 --- a/src/materials/materialslibrary.jl +++ b/src/materials/materialslibrary.jl @@ -6,8 +6,8 @@ Stores a collection of predefined materials for cable modeling, indexed by mater $(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 + "Dictionary mapping material names to [`Material`](@ref) objects." + data::Dict{String, Material} # Key: Material name, Value: Material object end """ @@ -30,20 +30,16 @@ Constructs an empty [`MaterialsLibrary`](@ref) instance and initializes with def library = $(FUNCTIONNAME)() ``` -# 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 + @info "Initializing default materials database..." + _add_default_materials!(library) + end - return library + return library end """ @@ -66,46 +62,42 @@ library = MaterialsLibrary() $(FUNCTIONNAME)(library) ``` -# See also - -- [`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(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 end - """ $(TYPEDSIGNATURES) @@ -134,14 +126,13 @@ $(FUNCTIONNAME)(library, "copper", material) ``` """ function add!( - library::MaterialsLibrary, - name::AbstractString, - material::Material, + library::MaterialsLibrary, + name::AbstractString, + material::Material ) - if haskey(library, name) - Base.error("Material $name already exists in the library.") - end - library[String(name)] = material - library + if haskey(library, name) + Base.error("Material $name already exists in the library.") + end + library[String(name)] = material + library end - diff --git a/src/materials/typecoercion.jl b/src/materials/typecoercion.jl index 9cd1b075..742adc10 100644 --- a/src/materials/typecoercion.jl +++ b/src/materials/typecoercion.jl @@ -2,10 +2,10 @@ @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}( +@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 + coerce_to_T(m.alpha, T) +) diff --git a/src/parametricbuilder/ParametricBuilder.jl b/src/parametricbuilder/ParametricBuilder.jl index a9a87fd8..ec7418ba 100644 --- a/src/parametricbuilder/ParametricBuilder.jl +++ b/src/parametricbuilder/ParametricBuilder.jl @@ -17,66 +17,60 @@ 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 + 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 +# 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 + 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) - - 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 - - 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 + 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 + + 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 # 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...)) + 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") diff --git a/src/parametricbuilder/base.jl b/src/parametricbuilder/base.jl index 0a0bfcdf..39ede211 100644 --- a/src/parametricbuilder/base.jl +++ b/src/parametricbuilder/base.jl @@ -3,218 +3,222 @@ 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 + 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 + 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 +function _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 +end # 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) +function _arg_choice_count(a) + (a isa Tuple && length(a) == 2) ? (_choice_count(a[1]) * _choice_count(a[2])) : + _choice_count(a) +end -_args_choice_count(args::Tuple) = - isempty(args) ? 1 : prod(_arg_choice_count(a) for a in args) +function _args_choice_count(args::Tuple) + isempty(args) ? 1 : prod(_arg_choice_count(a) for a in args) +end 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 + 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 + 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 + 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) +function _earth_choice_count(e::EarthSpec) + _choice_count(e.rho) * _choice_count(e.eps_r) * _choice_count(e.mu_r) * + _choice_count(e.t) +end # == Public cardinality API == function cardinality(s::SystemBuilderSpec) - # designs from CableBuilderSpec (uses existing cardinality(cbs::CableBuilderSpec)) - n_builder = cardinality(s.builder) + # 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))) + # 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) + # 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 + 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 + 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 + try + x = take!(ch) + return (x, ch) + catch + return nothing + end end Base.IteratorEltype(::Type{SystemBuilderSpec}) = Base.HasEltype() @@ -224,15 +228,15 @@ 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 + 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 @@ -241,78 +245,79 @@ _vals_pair(p) = collect(_expand_pair(p)) _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), ")") +function _fmt_freqs(f::AbstractVector) + length(f) ≤ 8 ? join(f, ", ") : + string(first(f), " … ", last(f), " (N=", length(f), ")") +end # 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), ", ") + 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), "}", - ) + 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, - ) + 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)) + 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 index b09bd61f..773b6192 100644 --- a/src/parametricbuilder/cablebuilderspec.jl +++ b/src/parametricbuilder/cablebuilderspec.jl @@ -9,17 +9,18 @@ PartSpec: - 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 + 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) +function PartSpec(component::Symbol, part_type::Type, n_layers::Int; + dim, args = (), material::MaterialSpec) + PartSpec(component, part_type, n_layers, dim, args, material) +end """ CableBuilderSpec: @@ -28,432 +29,422 @@ CableBuilderSpec: - nominal::Union{Nothing,DataModel.NominalData} """ struct CableBuilderSpec - cable_id::String - parts::Vector{PartSpec} - nominal::Union{Nothing, DataModel.NominalData} + cable_id::String + parts::Vector{PartSpec} + nominal::Union{Nothing, DataModel.NominalData} +end +function CableBuilder(id::AbstractString, parts::Vector{PartSpec}; nominal = nothing) + CableBuilderSpec(String(id), parts, nominal) 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 + 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 + 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) + 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 + 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} + name::String + choices::Vector{PartChoice} end struct DesignTrace - cable_id::String - components::Vector{ComponentTrace} + 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] + 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] + 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) + 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{: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) - +@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 + 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, + 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 + 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) + 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, + ig::DataModel.InsulatorGroup, + T::Type, + dim_val, + args_pos::Tuple, + mat ) - add!(ig, T, DataModel.Thickness(dim_val), args_pos..., 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 + 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 + 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} + 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 + # 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 @@ -462,48 +453,52 @@ 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) +function 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) +end # tube: no extra args -Tubular(component::Symbol; layers::Int, t, m) = - PartSpec(component, DataModel.Tubular, layers; - dim = _spec(t), args = (), material = m) +function Tubular(component::Symbol; layers::Int, t, m) + PartSpec(component, DataModel.Tubular, layers; + dim = _spec(t), args = (), material = m) +end # 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) +function 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) +end # 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) +function Solid(component::Symbol; d, m) + PartSpec(component, DataModel.Tubular, 1; + dim = _spec(d), args = (), material = m) +end # 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 + @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 @@ -513,11 +508,13 @@ 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) +function Tubular(component::Symbol; layers::Int, t, m) + PartSpec(component, DataModel.Insulator, layers; + dim = _spec(t), args = (), material = m) +end -Semicon(component::Symbol; layers::Int, t, m) = - PartSpec(component, DataModel.Semicon, layers; - dim = _spec(t), args = (), material = m) +function Semicon(component::Symbol; layers::Int, t, m) + PartSpec(component, DataModel.Semicon, layers; + dim = _spec(t), args = (), material = m) +end end diff --git a/src/parametricbuilder/determinize.jl b/src/parametricbuilder/determinize.jl index da2b55c8..9c0a1b33 100644 --- a/src/parametricbuilder/determinize.jl +++ b/src/parametricbuilder/determinize.jl @@ -6,57 +6,57 @@ # 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))) +@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) + 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) @@ -64,35 +64,35 @@ end # ---- 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), - ) + 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, - ) + 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) + parts_det = PartSpec[determinize(p) for p in cbs.parts] + return CableBuilderSpec(cbs.cable_id, parts_det, cbs.nominal) end # ───────────────────────────────────────────────────────────────────────────── @@ -102,49 +102,49 @@ end @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), - ) + 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, - ) + 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, - ) + 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 + 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 diff --git a/src/parametricbuilder/groupspec.jl b/src/parametricbuilder/groupspec.jl index 0b72d62d..a7f7a3d3 100644 --- a/src/parametricbuilder/groupspec.jl +++ b/src/parametricbuilder/groupspec.jl @@ -13,11 +13,11 @@ # 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 + 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 # ───────────────────────────────────────────────────────────────────────────── @@ -25,7 +25,7 @@ end # ───────────────────────────────────────────────────────────────────────────── """ - trifoil(; x0 = 0.0, y0, d, phases) + 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. @@ -34,18 +34,18 @@ 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, - ) + 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) + 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`. @@ -53,19 +53,19 @@ Horizontal flat formation: first cable at `(x0, y0)`, remaining `n-1` cables at `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, - ) + 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) + 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`. @@ -73,15 +73,15 @@ Vertical flat formation: first cable at `(x0, y0)`, remaining `n-1` cables at `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, - ) + 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 # ───────────────────────────────────────────────────────────────────────────── @@ -90,62 +90,61 @@ end # 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 + 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) + _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). @@ -154,61 +153,59 @@ 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) + 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) + _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...)) + 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/materialspec.jl b/src/parametricbuilder/materialspec.jl index f18ab2a0..4a5e03ab 100644 --- a/src/parametricbuilder/materialspec.jl +++ b/src/parametricbuilder/materialspec.jl @@ -1,8 +1,9 @@ # 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) +function _pair_from_nominal(nom, x) + x === nothing ? (nom, nothing) : + (x isa Tuple && length(x)==2) ? x : + (nom, x) +end # -------------------- material spec -------------------- @@ -11,60 +12,62 @@ 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)) + 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 + 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), - ) +function 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) + ) +end # --- 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, + 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), - ) + 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 Material(lib::Materials.MaterialsLibrary, name::AbstractString; kwargs...) + Material(get(lib, name); kwargs...) +end +function Material(lib::Materials.MaterialsLibrary, name::Symbol; kwargs...) + Material(lib, String(name); kwargs...) +end 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 + ρ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 diff --git a/src/parametricbuilder/positionspec.jl b/src/parametricbuilder/positionspec.jl index 8037af38..d4893a55 100644 --- a/src/parametricbuilder/positionspec.jl +++ b/src/parametricbuilder/positionspec.jl @@ -1,9 +1,9 @@ struct PositionSpec <: AbstractPositionSpec - x0::Real - y0::Real - dx::Any - dy::Any - conn::Dict{String, Int} + x0::Real + y0::Real + dx::Any + dy::Any + conn::Dict{String, Int} end # ───────────────────────────────────────────────────────────────────────────── @@ -16,10 +16,10 @@ end # etc. # ───────────────────────────────────────────────────────────────────────────── const _PhaseMapInputs = Union{ - Tuple{Symbol, Any}, - Tuple{String, Any}, - Pair{Symbol, Any}, - Pair{String, Any}, + Tuple{Symbol, Any}, + Tuple{String, Any}, + Pair{Symbol, Any}, + Pair{String, Any} } # normalize phases input to a splattable tuple of _PhaseMapInputs @@ -29,7 +29,7 @@ _normalize_phase_map(v::AbstractVector) = Tuple(v) _normalize_phase_map(::Nothing) = () """ - make_phase_maps(phases, n::Int) + 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`). @@ -38,42 +38,42 @@ Unified helper to process phase DSL inputs. - 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] + 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) + 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 + # 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]) + # 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/systembuilderspec.jl b/src/parametricbuilder/systembuilderspec.jl index 8329437b..096d08e0 100644 --- a/src/parametricbuilder/systembuilderspec.jl +++ b/src/parametricbuilder/systembuilderspec.jl @@ -14,48 +14,54 @@ include("groupspec.jl") # Earth and system specs # ───────────────────────────────────────────────────────────────────────────── struct EarthSpec - rho::Any; - eps_r::Any; - mu_r::Any; - t::Any + rho::Any + eps_r::Any + mu_r::Any + t::Any +end +function EarthSpec(; rho, eps_r = 1.0, mu_r = 1.0, t = Inf) + EarthSpec(_spec(rho), _spec(eps_r), _spec(mu_r), _spec(t)) 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)) +function Earth(; rho, eps_r = 1.0, mu_r = 1.0, t = Inf) + EarthSpec(_spec(rho), _spec(eps_r), _spec(mu_r), _spec(t)) +end 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} + 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)), - ) + 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) +function 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) +end -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) +function 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) +end # ───────────────────────────────────────────────────────────────────────────── # Internals: expand range/% grammar via ParametricBuilder helpers @@ -64,33 +70,35 @@ SystemBuilder(id::AbstractString, cbs::CableBuilderSpec, # (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 + 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) -) +function _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) + ) +end # 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 + 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))) + spec, pct = g.d + return length(collect(_make_range(spec; pct = pct))) end # ───────────────────────────────────────────────────────────────────────────── @@ -99,68 +107,67 @@ end # 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 + 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 + problems = LineParametersProblem[] + for prob in spec # uses Base.iterate(spec::SystemBuilderSpec) + push!(problems, prob) + end + return problems end diff --git a/src/parametricbuilder/wirepatterns/WirePatterns.jl b/src/parametricbuilder/wirepatterns/WirePatterns.jl index b9f93501..f3893e3b 100644 --- a/src/parametricbuilder/wirepatterns/WirePatterns.jl +++ b/src/parametricbuilder/wirepatterns/WirePatterns.jl @@ -12,7 +12,7 @@ export make_stranded, make_screened # ──────────────────────────────────────────────────────────────────────────── """ - struct HexaPattern + struct HexaPattern Result for a single design choice. @@ -24,15 +24,15 @@ Fields: - `awg::String` — AWG label from the table (informative). """ struct HexaPattern - layers::Int - wires::Int - wire_diameter_m::Float64 - total_area_m2::Float64 - awg::String + layers::Int + wires::Int + wire_diameter_m::Float64 + total_area_m2::Float64 + awg::String end """ - struct ScreenPattern + struct ScreenPattern Screen wires design. @@ -46,13 +46,13 @@ Fields: - `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 + wires::Int + wire_diameter_m::Float64 + lay_diameter_m::Float64 + radius_m::Float64 + total_area_m2::Float64 + coverage_pct::Float64 + awg::String end # ──────────────────────────────────────────────────────────────────────────── @@ -62,10 +62,10 @@ end _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 _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) +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)) @@ -74,21 +74,21 @@ 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) + 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 + 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." @@ -100,27 +100,27 @@ stranded_area_mm2(n::Real; fill_factor::Real = 0.94) = fill_factor * awg_to_area # ---- 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), + (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) + make_stranded(target_area_m2::Real; nmin::Integer=-3, nmax::Integer=40) Compute hexagonal-pattern strand layouts that approximate or meet the target metallic cross-section, imposing allowed total-wire ranges by target area. @@ -131,91 +131,91 @@ Inputs: 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). + 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). + Fallback: within allowed, pick smallest diameter with largest A < target (then smallest L). """ 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 - - return (; best_match, min_layers, min_diam) + @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 + + return (; best_match, min_layers, min_diam) end # ──────────────────────────────────────────────────────────────────────────── @@ -223,10 +223,10 @@ end # ──────────────────────────────────────────────────────────────────────────── """ - 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) + 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: @@ -247,98 +247,95 @@ Arguments: Returns: - `min_wires` — minimal N (≥ min_wires) that satisfies both CSA & coverage & geometry; - tie-break: smaller d, then smaller excess area. + 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. + tie-break: smaller excess area. - `best_match`— among all feasible combos, area closest to A_req_m2; tie: smaller N, then smaller d. """ 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)]) - - return (; min_wires, min_diam, best_match) + 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)]) + + return (; min_wires, min_diam, best_match) end - end # module diff --git a/src/plotbuilder/PlotBuilder.jl b/src/plotbuilder/PlotBuilder.jl index f09b8975..c09237a9 100644 --- a/src/plotbuilder/PlotBuilder.jl +++ b/src/plotbuilder/PlotBuilder.jl @@ -11,10 +11,6 @@ export make_render, RenderSpec 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") @@ -23,24 +19,17 @@ 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} + make_render(::Type{S}, obj; kwargs...) where {S<:AbstractPlotSpec} 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 + 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 @@ -48,15 +37,15 @@ actual windows/layouts. """ 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)") + Tdispatch = dispatch_on(S) + obj isa Tdispatch || + Base.error("Spec $(S) cannot dispatch on $(typeof(obj)); expected $(Tdispatch)") - raw = parse_kwargs(S, obj; kwargs...) - norm = resolve_input(S, raw) - pags = make_pages(S, norm) # ::Vector{PageSpec} + raw = parse_kwargs(S, obj; kwargs...) + norm = resolve_input(S, raw) + pags = make_pages(S, norm) # ::Vector{PageSpec} - return RenderSpec(S, pags) + return RenderSpec(S, pags) end end # module PlotBuilder diff --git a/src/plotbuilder/axisspec.jl b/src/plotbuilder/axisspec.jl index ef507132..c70463e3 100644 --- a/src/plotbuilder/axisspec.jl +++ b/src/plotbuilder/axisspec.jl @@ -1,41 +1,41 @@ """ - make_axes(::Type{S}, nt::NamedTuple) where {S<:AbstractPlotSpec} + 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) + (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) + dims = geom_axes(S) - xaxis = nothing - yaxis = nothing - zaxis = nothing + 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 + 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) + 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 + 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) + return (xaxis = xaxis, yaxis = yaxis, zaxis = zaxis) end """ @@ -45,39 +45,38 @@ 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, "]") + 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) + 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) + 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}, + ::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) + 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 index 3d9765f7..4ffd6e76 100644 --- a/src/plotbuilder/backendhandler/BackendHandler.jl +++ b/src/plotbuilder/backendhandler/BackendHandler.jl @@ -1,241 +1,119 @@ """ -Makie backend handler for LineCableModels. + LineCableModels.PlotBuilder.BackendHandler -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. +Coordinate optional Makie backends without loading them from the core package. """ module BackendHandler -using Makie -using UUIDs -using ...Utils: is_headless +export backend_available, current_backend_symbol, ensure_backend!, make_screen, + next_fignum, renderfig, set_backend!, with_backend -export set_backend!, - ensure_backend!, current_backend_symbol, renderfig, with_backend, make_screen +const _BACKEND_EXTENSIONS = Dict( + :cairo => :LineCableModelsCairoMakieExt, + :gl => :LineCableModelsGLMakieExt, + :wgl => :LineCableModelsWGLMakieExt +) -# --------------------------------------------------------------------------- -# Backend registry -# --------------------------------------------------------------------------- +const FIG_NO = Base.Threads.Atomic{Int}(1) -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"), -) +_parent_package() = parentmodule(parentmodule(@__MODULE__)) -_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]) +function _makie_extension() + return Base.get_extension(_parent_package(), :LineCableModelsMakieExt) 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 +function _backend_extension(backend::Symbol) + name = get(_BACKEND_EXTENSIONS, backend, nothing) + name === nothing && throw( + ArgumentError("Unknown backend :$(backend). Use :cairo, :gl, or :wgl."), + ) + return Base.get_extension(_parent_package(), name) 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 +"Return whether `backend` has been explicitly loaded." +backend_available(backend::Symbol) = _backend_extension(backend) !== nothing + +"Return the active Makie backend as `:cairo`, `:gl`, `:wgl`, `:unknown`, or `:none`." +function current_backend_symbol() + ext = _makie_extension() + return ext === nothing ? :none : ext.current_backend_symbol() end -"""Activate a specific backend (:cairo, :gl, :wgl). +""" + set_backend!(backend; force=false) + +Activate an explicitly loaded Makie backend. -In headless, :gl/:wgl requests fall back to :cairo unless `force=true`. -No `using` required by callers. +`force` is retained for source compatibility and has no effect. """ 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) + ext = _backend_extension(backend) + ext === nothing && throw( + ArgumentError( + "Backend :$(backend) is not loaded. Run `using $(_backend_package(backend))` first.", + ), + ) + return ext.activate!() 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 +function _backend_package(backend::Symbol) + backend === :cairo && return "CairoMakie" + backend === :gl && return "GLMakie" + backend === :wgl && return "WGLMakie" + throw(ArgumentError("Unknown backend :$(backend). Use :cairo, :gl, or :wgl.")) end -""" - with_backend(f, backend; force=false) - -Temporarily activate `backend`, run `f()`, then restore the previous backend. +"Ensure that an explicitly loaded backend is active." +function ensure_backend!(backend::Union{Nothing, Symbol} = nothing) + backend !== nothing && return set_backend!(backend) + current = current_backend_symbol() + current in keys(_BACKEND_EXTENSIONS) && return current + throw( + ArgumentError( + "No Makie backend is active. Load CairoMakie, GLMakie, or WGLMakie first.", + ), + ) +end -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. -""" +"Run `f` with an explicitly loaded backend and restore the previous backend." 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 + previous = current_backend_symbol() + set_backend!(backend; force = force) + try + return f() + finally + if previous in keys(_BACKEND_EXTENSIONS) && previous != backend + set_backend!(previous; force = force) + 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..., +"Create a backend-specific screen when supported." +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...) + ext = _backend_extension(backend) + return ext === nothing ? nothing : ext.make_screen(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. +function make_screen(backend::Symbol, title::AbstractString; kwargs...) + make_screen(title; backend, kwargs...) +end -- 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. -""" +"Display a Makie figure through the loaded plotting extension." 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 + ext = _makie_extension() + ext === nothing && throw( + ArgumentError( + "Makie is not loaded. Load CairoMakie, GLMakie, or WGLMakie first.", + ), + ) + return ext.renderfig(fig) 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 +end # module BackendHandler diff --git a/src/plotbuilder/pagespec.jl b/src/plotbuilder/pagespec.jl index 29f95dc8..364b6335 100644 --- a/src/plotbuilder/pagespec.jl +++ b/src/plotbuilder/pagespec.jl @@ -1,5 +1,5 @@ """ - make_pages(::Type{S}, nt, views) where {S<:AbstractPlotSpec} + make_pages(::Type{S}, nt, views) where {S<:AbstractPlotSpec} Packs ViewSpec values into PageSpec payloads. @@ -11,210 +11,207 @@ 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}, + ::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] + 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 + 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 + 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,) + idx_raw = index_keys(S) + idx = idx_raw isa Tuple ? idx_raw : (idx_raw,) - has_i = :i in idx - has_j = :j in idx + has_i = :i in idx + has_j = :j in idx - Ni = 1 - Nj = 1 + Ni = 1 + Nj = 1 - (!has_i && !has_j) && return Ni, Nj + (!has_i && !has_j) && return Ni, Nj - dims_raw = geom_axes(S) - dims = dims_raw isa Tuple ? dims_raw : (dims_raw,) - obj = nt.obj + 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 + 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 + datakey = getfield(nt, dim) + datakey isa Symbol || continue - arr = container_array(S, obj, dim, datakey) + arr = container_array(S, obj, dim, datakey) - arr isa AbstractArray || continue + 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 && 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 + if (!has_i || Ni > 1) && (!has_j || Nj > 1) + break + end + end - return Ni, Nj + 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, + ::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 + 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) + is_modal(obj) Return `true` iff `domain(obj)` is a modal-like tag. """ @@ -225,7 +222,7 @@ Return `true` iff `domain(obj)` is a modal-like tag. # -------------------------------------------------------------------------- """ - index_pairs(::Type{S}, nt) where {S<:AbstractPlotSpec} + 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`. @@ -233,80 +230,77 @@ 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`. + * 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)`. + * 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, + ::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 + 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} + make_pages(::Type{S}, nt) where {S<:AbstractPlotSpec} Top-level figure builder for spec `S`. @@ -321,139 +315,136 @@ It then delegates to `make_pages(::Type{S}, ::Val{mode}, nt, axes)` where - :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. + per (i,j), overlaying all fields in each view. """ function make_pages( - ::Type{S}, - nt::NamedTuple, + ::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) + 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, + ::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) + 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, + ::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,) + idx_raw = index_keys(S) + idx = idx_raw isa Tuple ? idx_raw : (idx_raw,) - has_i = :i in idx - has_j = :j in idx + has_i = :i in idx + has_j = :j in idx - all_series = SeriesSpec[] + 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 + 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 + 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) + 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, + ::Type{S}, + ::Val{:overlay_fields}, + nt::NamedTuple, + axes::NamedTuple ) where {S <: AbstractPlotSpec} - _, kfield, field_keys = get_fields(S, nt, axes) + _, kfield, field_keys = get_fields(S, nt, axes) - all_series = SeriesSpec[] + 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 + 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) + 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, + ::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) + 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 index ca11e121..c1d1fe81 100644 --- a/src/plotbuilder/parse.jl +++ b/src/plotbuilder/parse.jl @@ -1,167 +1,163 @@ # 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, + ::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 + # AxisSpec selector keys: (:x, :y, :z) + select_fields = dims - semantic_keys = (input_keys..., idx..., select_fields...) - allowed = (semantic_keys..., renderer_keys...) + 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_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) + spec = NamedTuple(spec_pairs) + renderer = NamedTuple(renderer_pairs) - return spec, renderer + return spec, renderer end # merge_defaults – “how does this spec fill in the blanks?” function merge_defaults( - ::Type{S}, - obj, - spec::NamedTuple, - renderer::NamedTuple, + ::Type{S}, + obj, + spec::NamedTuple, + renderer::NamedTuple ) where {S <: AbstractPlotSpec} + idefault = input_defaults(S, obj) + bdefault = renderer_defaults(S, obj) - idefault = input_defaults(S, obj) - bdefault = renderer_defaults(S, obj) + spec_merged = merge(idefault, spec) + renderer_merged = merge(bdefault, renderer) - spec_merged = merge(idefault, spec) - renderer_merged = merge(bdefault, renderer) - - return spec_merged, renderer_merged + 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}}, + ::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 + # 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, + ::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 + 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, + ::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 + 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 @@ -170,270 +166,267 @@ end # 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, + ::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 + # 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,) + 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} + 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. + `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)`. + `renderer_defaults(S, obj)`. 3. Normalize indices (`index_keys(S)` / `ranged_keys(S)`) to the allowed - types and fill in defaults. + 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. + - 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) + (; 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) + # 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...)) +function parse_kwargs(::Type{S}, obj; kwargs...) where {S <: AbstractPlotSpec} + parse_kwargs(S, obj, (; kwargs...)) +end """ Resolve raw inputs into a normalized NamedTuple understood by `make_pages`. @@ -447,91 +440,91 @@ This is where a spec implements its own mini-grammar: 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)) + 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 index 802f71ff..6caa3575 100644 --- a/src/plotbuilder/plothelpers.jl +++ b/src/plotbuilder/plothelpers.jl @@ -6,31 +6,32 @@ 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 ..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 +using LineCableModels.PlotBuilder: AbstractPlotSpec +using ..LineCableModelsMakieExt: + _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 index 4c8f0ca4..63aceb8b 100644 --- a/src/plotbuilder/plotspecs.jl +++ b/src/plotbuilder/plotspecs.jl @@ -1,31 +1,31 @@ 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 + 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, + 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) + 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 + # 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 """ @@ -33,135 +33,134 @@ 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 + # --- 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 + 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, + 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 + 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 + 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 index ed2e8510..d54cb3a0 100644 --- a/src/plotbuilder/plotuicomponents/PlotUIComponents.jl +++ b/src/plotbuilder/plotuicomponents/PlotUIComponents.jl @@ -6,7 +6,7 @@ using Makie # package's `__init__` breaks incremental compilation on Julia 1.12. using Printf: @sprintf -import ..PlotBuilder.BackendHandler: current_backend_symbol, _pkgid +import LineCableModels.PlotBuilder.BackendHandler: current_backend_symbol, make_screen # ----------------------------------------------------------------------------- # Constants @@ -42,126 +42,132 @@ 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") - +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}} + 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 + 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} + 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) +function ControlReaction(; + status_string = nothing, + button_color = nothing, + button_label = nothing, + timeout = 1.5 +) + ControlReaction(status_string, button_color, button_label, timeout) +end struct ControlButtonSpec - label::Union{Nothing, String} - icon::Union{Nothing, String} - action::Function - on_success::Union{Nothing, ControlReaction} - on_failure::Union{Nothing, ControlReaction} + label::Union{Nothing, String} + icon::Union{Nothing, String} + action::Function + on_success::Union{Nothing, ControlReaction} + on_failure::Union{Nothing, ControlReaction} +end + +function 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) 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, + 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 + +function 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 + ) +end 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, - ) + 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 + +function 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 + ) +end 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 + 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 # ----------------------------------------------------------------------------- @@ -170,154 +176,155 @@ end """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 + if current_backend_symbol() == :gl + return make_screen(:gl, 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=-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 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)) + ) +end 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, + 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, - ) + 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 + 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, + 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 + 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, + 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 + +function _configure_theme!( + 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) + theme_for(ctx; mode = mode) +end function with_plot_theme( - f::Function, - ctx::PlotBackendContext; - mode::Union{Nothing, Symbol} = nothing, + 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 + 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 # ----------------------------------------------------------------------------- @@ -325,81 +332,80 @@ end # ----------------------------------------------------------------------------- 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, + 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 + 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, + 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) + layout = fig_ctx.figure.layout - 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.rowgap!(layout, GRID_ROW_GAP) + Makie.colgap!(layout, GRID_COL_GAP) - Makie.colsize!(layout, 1, Makie.Relative(1.0)) - Makie.colsize!(layout, 2, Makie.Fixed(legend_panel_width)) + 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.rowgap!(fig_ctx.legend_grid, LEGEND_GAP) - Makie.colgap!(fig_ctx.legend_grid, 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()) + Makie.rowsize!(fig_ctx.legend_grid, 1, Makie.Auto()) + Makie.rowsize!(fig_ctx.legend_grid, 2, Makie.Auto()) - return fig_ctx + return fig_ctx end function _make_canvas!( - fig_ctx::PlotFigureContext; - axis_ctor = Makie.Axis, - axis_options::NamedTuple = NamedTuple(), + fig_ctx::PlotFigureContext; + axis_ctor = Makie.Axis, + axis_options::NamedTuple = NamedTuple() ) - axis = axis_ctor(fig_ctx.canvas_node; axis_options...) - return axis + axis = axis_ctor(fig_ctx.canvas_node; axis_options...) + return axis end # ----------------------------------------------------------------------------- @@ -407,209 +413,207 @@ end # ----------------------------------------------------------------------------- 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, + 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 + 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 + 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 + 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) + 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 + 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 + 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 + 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, + 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 + 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") - + # non-breaking space keeps the row height while looking empty + update_status!(ctx, "\u00A0") end # ----------------------------------------------------------------------------- @@ -618,118 +622,117 @@ end """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 + 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..., + 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 + 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 + 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 = "", + 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 + 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 + 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 + chan = ensure_statusbar!(ctx) + chan[] = String(message) + return chan end # ----------------------------------------------------------------------------- @@ -737,65 +740,63 @@ end # ----------------------------------------------------------------------------- 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, + 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, - ) + 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...) @@ -809,10 +810,10 @@ 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 + 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 index e69de29b..8b137891 100644 --- a/src/plotbuilder/plotuicomponents/callbacks.jl +++ b/src/plotbuilder/plotuicomponents/callbacks.jl @@ -0,0 +1 @@ + diff --git a/src/plotbuilder/seriesspec.jl b/src/plotbuilder/seriesspec.jl index 767fa267..f4a1a88c 100644 --- a/src/plotbuilder/seriesspec.jl +++ b/src/plotbuilder/seriesspec.jl @@ -4,7 +4,7 @@ # -------------------------------------------------------------------------- """ - axis_transform(::Type{S}, ::Val{dim}, ::Val{datakey}, nt, axis::AxisSpec, data) where {S,dim,datakey} + 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. @@ -16,239 +16,236 @@ Per-spec hook to post-process the sliced axis data *before* unit scaling. 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.", - ) +function axis_transform( + ::Type{S}, + ::Val{dim}, + ::Val{datakey}, + nt::NamedTuple, + axis::AxisSpec, + data +) where {S <: AbstractPlotSpec, dim, datakey} + 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} + 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`. + (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. + is the last array dimension. * Optionally unwrap child fields using `select_field(S, Val(dim))` if it is - non-`nothing` and elements are NamedTuples. + 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}, + ::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 + 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}, + ::Type{S}, + dim::Symbol, + nt::NamedTuple, + axis::Union{AxisSpec, Nothing} ) where {S <: AbstractPlotSpec} - axis === nothing && return nothing + axis === nothing && return nothing - # axis selector: nt.x / nt.y / nt.z - selector = getfield(nt, dim)::Symbol + # 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)) + # 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) + # 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)).", - ) + # 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)).", - ) + 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 + # 4) unit scaling + sf = scale_factor(axis.quantity, axis.units) + return sf .* transformed end """ - make_series(::Type{S}, nt, axes) where {S<:AbstractPlotSpec} + make_series(::Type{S}, nt, axes) where {S<:AbstractPlotSpec} Builds the vector of SeriesSpec for the given spec and resolved input `nt`. @@ -258,32 +255,32 @@ 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, + ::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 + 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 diff --git a/src/plotbuilder/traits.jl b/src/plotbuilder/traits.jl index bcff4a49..34937c04 100644 --- a/src/plotbuilder/traits.jl +++ b/src/plotbuilder/traits.jl @@ -1,5 +1,4 @@ - # ----------------------------------------------------------------------------- # Spec-level traits (configuration surface) # ----------------------------------------------------------------------------- @@ -16,8 +15,8 @@ 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{MySpec}) = (:x,) # only x can log + enable_logscale(::Type{OtherSpec}) = (:x,:y) # x and y """ enable_logscale(::Type{S}) where {S <: AbstractPlotSpec} = () @@ -26,7 +25,7 @@ Domain/container type this spec expects to dispatch on. Example: - dispatch_on(::Type{MyRPlotSpec}) = LineParameters + dispatch_on(::Type{MyRPlotSpec}) = LineParameters """ dispatch_on(::Type{S}) where {S <: AbstractPlotSpec} = Any @@ -39,12 +38,12 @@ decision into the grammar layer. default_figsize(::Type{S}) where {S <: AbstractPlotSpec} = (800, 400) """ - axis_quantity(::Type{S}, ::Val{dim}) where {S<:AbstractPlotSpec, dim} + 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}) + 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`. @@ -52,21 +51,24 @@ 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}() +function axis_quantity(::Type{S}, dim::Symbol) where {S <: AbstractPlotSpec} + QuantityTag{:unknown}() +end -axis_quantity(::Type{S}, ::Val{dim}) where {S <: AbstractPlotSpec, dim} = - axis_quantity(S, dim) +function axis_quantity(::Type{S}, ::Val{dim}) where {S <: AbstractPlotSpec, dim} + axis_quantity(S, dim) +end -axis_quantity( - ::Type{S}, - ::Val{dim}, - ::Val{datakey}, -) where {S <: AbstractPlotSpec, dim, datakey} = - axis_quantity(S, dim) +function axis_quantity( + ::Type{S}, + ::Val{dim}, + ::Val{datakey} +) where {S <: AbstractPlotSpec, dim, datakey} + axis_quantity(S, dim) +end """ - index_keys(::Type{S}) where {S<:AbstractPlotSpec} + 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.). @@ -77,7 +79,7 @@ over frequencies should typically override this to `(:i, :j)`. index_keys(::Type{S}) where {S <: AbstractPlotSpec} = () """ - ranged_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. @@ -92,20 +94,19 @@ Default: empty tuple (no ranged indices). ranged_keys(::Type{S}) where {S <: AbstractPlotSpec} = () """ - geom_axes(::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} + 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. @@ -113,7 +114,7 @@ Return the default plot title for this spec, given the resolved input `nt`. default_title(::Type{S}, nt::NamedTuple) where {S <: AbstractPlotSpec} = "" """ - legend_labels(::Type{S}, nt) 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 @@ -121,7 +122,6 @@ by `make_series(S, nt)`. """ legend_labels(::Type{S}, nt::NamedTuple) where {S <: AbstractPlotSpec} = String[] - # ----------------------------------------------------------------------------- # Quantity-level unit and label hooks (using UnitHandler) # ----------------------------------------------------------------------------- @@ -133,16 +133,16 @@ 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) - +function axis_unit(::Type{S}, q::QuantityTag, dim::Symbol) where {S <: AbstractPlotSpec} + display_unit(q) +end # -------------------------------------------------------------------------- # Input / backend grammar traits # -------------------------------------------------------------------------- """ - input_kwargs(::Type{S}) where {S<:AbstractPlotSpec} + input_kwargs(::Type{S}) where {S<:AbstractPlotSpec} Plot-level *semantic* kwargs understood by this spec. @@ -152,7 +152,7 @@ These describe what is plotted or how the data is selected/sliced input_kwargs(::Type{S}) where {S <: AbstractPlotSpec} = () """ - renderer_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). """ @@ -166,7 +166,7 @@ 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{MySpec}, ::Val{:y}) = :stats """ data_container(::Type{S}, ::Val{dim}) where {S <: AbstractPlotSpec, dim} = nothing @@ -176,7 +176,7 @@ data_container(::Type{S}, ::Val{dim}) where {S <: AbstractPlotSpec, dim} = nothi select_field(::Type{S}, ::Val{dim}) where {S <: AbstractPlotSpec, dim} = nothing """ - input_defaults(::Type{S}, obj) where {S<:AbstractPlotSpec} + input_defaults(::Type{S}, obj) where {S<:AbstractPlotSpec} Defaults for semantic kwargs declared in `input_kwargs(S)`. @@ -186,7 +186,7 @@ from `obj` contents). input_defaults(::Type{S}, obj) where {S <: AbstractPlotSpec} = NamedTuple() """ - renderer_defaults(::Type{S}, obj) where {S<:AbstractPlotSpec} + renderer_defaults(::Type{S}, obj) where {S<:AbstractPlotSpec} Defaults for figure kwargs declared in `renderer_kwargs(S)`. @@ -198,18 +198,18 @@ 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. + :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. + :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 @@ -217,86 +217,106 @@ 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 +function has_complex_qty( + ::Type{S}, + ::Val{dim}, + ::Val{datakey} +) where {S <: AbstractPlotSpec, dim, datakey} + false +end + +function complex_as( + ::Type{S}, + ::Val{dim}, + ::Val{datakey} +) where {S <: AbstractPlotSpec, dim, datakey} + (:re, :im, :abs, :angle) +end + +function complex_as_default( + ::Type{S}, + ::Val{dim}, + ::Val{datakey} +) where {S <: AbstractPlotSpec, dim, datakey} + :re +end # 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)) +function axis_quantity( + ::Type{S}, + ::Val{dim}, + ::Val{datakey}, + ::Val{as} +) where {S <: AbstractPlotSpec, dim, datakey, as} + axis_quantity(S, Val(dim), Val(datakey)) +end # 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}() +function axis_quantity( + ::Type{S}, + ::Val{dim}, + ::Val{:Z}, + ::Val{:re} +) where {S <: AbstractPlotSpec, dim} + QuantityTag{:resistance}() +end +function axis_quantity( + ::Type{S}, + ::Val{dim}, + ::Val{:Z}, + ::Val{:im} +) where {S <: AbstractPlotSpec, dim} + QuantityTag{:reactance}() +end # 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)}() +function axis_quantity( + ::Type{S}, + ::Val{dim}, + ::Val{:Y}, + ::Val{:re} +) where {S <: AbstractPlotSpec, dim} + QuantityTag{:conductance}() +end +function axis_quantity( + ::Type{S}, + ::Val{dim}, + ::Val{:Y}, + ::Val{:im} +) where {S <: AbstractPlotSpec, dim} + QuantityTag{:susceptance}() +end + +function axis_quantity( + ::Type{S}, + ::Val{dim}, + ::Val{:Z}, + ::Val{:abs} +) where {S <: AbstractPlotSpec, dim} + QuantityTag{(:impedance, :abs)}() +end +function axis_quantity( + ::Type{S}, + ::Val{dim}, + ::Val{:Z}, + ::Val{:angle} +) where {S <: AbstractPlotSpec, dim} + QuantityTag{(:impedance, :angle)}() +end + +function axis_quantity( + ::Type{S}, + ::Val{dim}, + ::Val{:Y}, + ::Val{:abs} +) where {S <: AbstractPlotSpec, dim} + QuantityTag{(:admittance, :abs)}() +end +function axis_quantity( + ::Type{S}, + ::Val{dim}, + ::Val{:Y}, + ::Val{:angle} +) where {S <: AbstractPlotSpec, dim} + QuantityTag{(:admittance, :angle)}() +end diff --git a/src/plotbuilder/types.jl b/src/plotbuilder/types.jl index 8e3d1f74..460f40b3 100644 --- a/src/plotbuilder/types.jl +++ b/src/plotbuilder/types.jl @@ -10,18 +10,18 @@ AxisSpec is the fully decided axis descriptor used by plot areas (views). - `scale` : :linear or :log10 """ struct AxisSpec - dim::Symbol - quantity::QuantityTag - units::Units - label::String - scale::Symbol + dim::Symbol + quantity::QuantityTag + units::Units + label::String + scale::Symbol end # -------------------------------------------------------------------------- # Payload hierarchy: series → view → figure → renderer # -------------------------------------------------------------------------- """ - SeriesSpec + SeriesSpec Single plot primitive (one Makie call). @@ -33,15 +33,15 @@ Fields: - `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} + kind::Symbol + xdata::Union{Nothing, AbstractVector{<:Number}} + ydata::Union{Nothing, AbstractArray{<:Number}} + zdata::Union{Nothing, AbstractArray{<:Number}} + label::Union{Nothing, String} end """ - ViewSpec + ViewSpec One plot view / axis system. @@ -58,16 +58,16 @@ Fields: - `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 + xaxis::Union{Nothing, AxisSpec} + yaxis::Union{Nothing, AxisSpec} + zaxis::Union{Nothing, AxisSpec} + title::String + series::Vector{SeriesSpec} + key::NamedTuple end """ - PageSpec + PageSpec One logical figure / window. @@ -79,15 +79,15 @@ Fields: - `kwargs` : figure-level backend options (e.g. figsize) """ struct PageSpec - title :: String - size :: Tuple{Int, Int} - layout :: Symbol - views :: Vector{ViewSpec} - kwargs :: NamedTuple + title::String + size::Tuple{Int, Int} + layout::Symbol + views::Vector{ViewSpec} + kwargs::NamedTuple end """ - RenderSpec{S} + RenderSpec{S} Final product of the grammar pipeline for spec type `S`. @@ -99,6 +99,6 @@ 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} + spec::Type{S} + figures::Vector{PageSpec} end diff --git a/src/plotbuilder/uicomponents/UIComponents.jl b/src/plotbuilder/uicomponents/UIComponents.jl index 8570c07b..4943a32d 100644 --- a/src/plotbuilder/uicomponents/UIComponents.jl +++ b/src/plotbuilder/uicomponents/UIComponents.jl @@ -3,14 +3,15 @@ module UIComponents using Makie import ..BackendHandler +using ..PlotUIComponents: PlotAssembly import ..PlotBuilder: AbstractPlotSpec, RenderSpec, PageSpec, ViewSpec, SeriesSpec, AxisSpec -export build, export_svg!, - UIContext, UILayoutSpec, UIContainerSpec, UISlotSpec, - UIFigure, UIPanel, PlotAssembly +export build, + UIContext, UILayoutSpec, UIContainerSpec, UISlotSpec, + UIFigure, UIPanel, PlotAssembly -export build_context, display! +export display! include("themes.jl") include("types.jl") diff --git a/src/plotbuilder/uicomponents/actions.jl b/src/plotbuilder/uicomponents/actions.jl index 9b0ef4a4..361c9563 100644 --- a/src/plotbuilder/uicomponents/actions.jl +++ b/src/plotbuilder/uicomponents/actions.jl @@ -1,67 +1,67 @@ function action_set_status!(ctx::UIContext, msg::AbstractString) - if ctx.status !== nothing - ctx.status[] = String(msg) - end - return nothing + 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. +# 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. +# 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? +# 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 + # 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.") + # 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...") + 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) + 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]) + 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) + # 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 + 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 + 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 index 3ec32ee7..47bcaff8 100644 --- a/src/plotbuilder/uicomponents/draw.jl +++ b/src/plotbuilder/uicomponents/draw.jl @@ -3,52 +3,52 @@ # ------------------------- function draw!(axis, s::SeriesSpec; kwargs...) - draw!(Val(s.kind), axis, s; 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[] + @warn "Unsupported plot kind :$kind" + return Any[] end function draw!(::Val{:line}, axis, s::SeriesSpec; kwargs...) - (s.xdata === nothing || s.ydata === nothing) && return Any[] + (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 + 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[] + (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 + 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 + (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 diff --git a/src/plotbuilder/uicomponents/layoutspecs.jl b/src/plotbuilder/uicomponents/layoutspecs.jl index 3a3f9f6c..46f24409 100644 --- a/src/plotbuilder/uicomponents/layoutspecs.jl +++ b/src/plotbuilder/uicomponents/layoutspecs.jl @@ -3,73 +3,73 @@ # ------------------------- function make_layout(::Val{layout}) where {layout} - error("make_layout(Val(:$layout)) not implemented") + 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) - ), - ) + # 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 - ), - ), + # 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 - ), - ), + # 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, - ), - ), + # 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), - ), - ] + # 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), - ], - ) + # 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), - ]) + cs = Dict(:root => Any[ + Makie.Relative(1.0), + Makie.Fixed(LEGEND_WIDTH) + ]) - return UILayoutSpec(:single, [root], slots, rs, cs) + return UILayoutSpec(:single, [root], slots, rs, cs) end -make_layout(::Val{:grid}) = make_layout(Val(:single)) \ No newline at end of file +make_layout(::Val{:grid}) = make_layout(Val(:single)) diff --git a/src/plotbuilder/uicomponents/pipeline.jl b/src/plotbuilder/uicomponents/pipeline.jl index 27663177..57d169e6 100644 --- a/src/plotbuilder/uicomponents/pipeline.jl +++ b/src/plotbuilder/uicomponents/pipeline.jl @@ -3,40 +3,40 @@ # ------------------------- 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..., + 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, - ) + 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 # ------------------------- @@ -44,46 +44,46 @@ end # ------------------------- function build( - r::RenderSpec{S}; - backend = nothing, - display::Bool = true, - kwargs..., + r::RenderSpec{S}; + backend = nothing, + display::Bool = true, + kwargs... ) where {S} - ctx = make_context(; backend = backend, display = display, kwargs...) - assemblies = UIPlot[] + ctx = make_context(; backend = backend, display = display, kwargs...) + assemblies = UIPlot[] - Makie.with_theme(ctx.theme) do - for page in r.figures # page is PageSpec + Makie.with_theme(ctx.theme) do + for page in r.figures # page is PageSpec - # A. Architect - layout_s = make_layout(Val(page.layout)) + # A. Architect + layout_s = make_layout(Val(page.layout)) - # B. Constructor (Shell) - uifig = build_figure(ctx, page, layout_s) + # 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 + # 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) + # 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) + 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) + # E. Assembly + assem = UIPlot(S, ctx, page, uifig, panels, widgets_dict) + push!(assemblies, assem) - if display - display!(ctx, assem) - end - end - end - return assemblies + if display + display!(ctx, assem) + end + end + end + return assemblies end # ------------------------- @@ -92,219 +92,220 @@ end # 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] + 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) + 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) + 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 + 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) + 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 + 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 + if ctx.interactive && ctx.window !== nothing + display(ctx.window, assem.uifig.figure) + else + BackendHandler.renderfig(assem.uifig.figure) + end +end diff --git a/src/plotbuilder/uicomponents/themes.jl b/src/plotbuilder/uicomponents/themes.jl index 53f554d3..daaf97d5 100644 --- a/src/plotbuilder/uicomponents/themes.jl +++ b/src/plotbuilder/uicomponents/themes.jl @@ -34,75 +34,76 @@ const EXPORT_EXTENSION = "svg" # Material UI icons # ----------------------------------------------------------------------------- const MI_REFRESH = "\uE5D5" # Material Icons: 'refresh' -const MI_SAVE = "\uE161" # Material Icons: 'save' +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)), - ) +function 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)) + ) +end """ - make_theme(; interactive::Bool, use_latex_fonts::Bool) + 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 + 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, - ), - ) + # 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 + # 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 + # 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...) + return Makie.Theme(; config...) end diff --git a/src/plotbuilder/uicomponents/types.jl b/src/plotbuilder/uicomponents/types.jl index bc0f70db..f8906be9 100644 --- a/src/plotbuilder/uicomponents/types.jl +++ b/src/plotbuilder/uicomponents/types.jl @@ -3,93 +3,93 @@ # ------------------------- struct UIContainerSpec - name::Symbol - parent::Union{Nothing, Symbol} - at::Tuple # Relaxed from strict Union types to generic Tuple - layout::NamedTuple + 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) +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) + 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) +function UISlotSpec(name, parent, at; layout = (;), attrs = (;)) + UISlotSpec(name, parent, at, layout, attrs) +end struct UILayoutSpec - name::Symbol - containers::Vector{UIContainerSpec} - slots::Vector{UISlotSpec} - rowsizes::Dict{Symbol, Vector{Any}} - colsizes::Dict{Symbol, Vector{Any}} + 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 + 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) +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 + 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) +function UIToggleSpec(label, active, action; attrs = (;)) + UIToggleSpec(label, active, action, attrs) +end # ------------------------- # 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 + 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} + 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} + 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 + spec::DataType + ctx::UIContext + page::PageSpec + uifig::UIFigure + panels::Vector{UIPanel} + widgets::Dict{Symbol, Any} +end diff --git a/src/plotbuilder/uicomponents/widgets.jl b/src/plotbuilder/uicomponents/widgets.jl index df2664af..2009ab3f 100644 --- a/src/plotbuilder/uicomponents/widgets.jl +++ b/src/plotbuilder/uicomponents/widgets.jl @@ -3,24 +3,24 @@ # ------------------------- function controls_default(::Type{S}, ctx::UIContext, page, panels) where {S} - !ctx.interactive && return UIWidgetSpec[] + !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"), - ), - ] + 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[] + return UIWidgetSpec[] end # ------------------------- @@ -28,7 +28,7 @@ end # ------------------------- 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 + w = controls_default(S, ctx, page, panels) + append!(w, controls_custom(S, ctx, page, panels)) + return w +end diff --git a/src/plotbuilder/viewspec.jl b/src/plotbuilder/viewspec.jl index 65f5d46f..904bc613 100644 --- a/src/plotbuilder/viewspec.jl +++ b/src/plotbuilder/viewspec.jl @@ -1,5 +1,5 @@ """ - make_views(::Type{S}, nt, axes, series) where {S<:AbstractPlotSpec} + make_views(::Type{S}, nt, axes, series) where {S<:AbstractPlotSpec} Groups SeriesSpec into ViewSpec values. @@ -14,18 +14,18 @@ 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}, + ::Type{S}, + nt::NamedTuple, + axes::NamedTuple, + series::Vector{SeriesSpec} ) where {S <: AbstractPlotSpec} - title = default_title(S, nt) - key = (;) + title = default_title(S, nt) + key = (;) - xaxis = axes.xaxis - yaxis = axes.yaxis - zaxis = axes.zaxis + 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 + view = ViewSpec(xaxis, yaxis, zaxis, title, series, key) + return ViewSpec[view] +end diff --git a/src/uncertainbessels/UncertainBessels.jl b/src/uncertainbessels/UncertainBessels.jl index 435dfd49..c125e8ff 100644 --- a/src/uncertainbessels/UncertainBessels.jl +++ b/src/uncertainbessels/UncertainBessels.jl @@ -1,5 +1,5 @@ """ - LineCableModels.UncertainBessels + LineCableModels.UncertainBessels Uncertainty-aware wrappers for Bessel functions. @@ -31,13 +31,12 @@ $(IMPORTS) # Exports -$(EXPORTS) # Usage ```julia # do not import SpecialFunctions directly -using LineCableModels.UncertainBessels +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 @@ -50,10 +49,6 @@ I1 = besselix(1, z) # scaled I1 with uncertainty - 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 @@ -68,76 +63,105 @@ 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(ẑ))...], - ), - ), - ẑ, - ) + 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) + 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) +@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(ν, 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) @@ -149,25 +173,53 @@ end @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) +@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) + +function _bessel_doc(name::Symbol, scaled::Bool) + scaling = scaled ? + "Uses the scaled convention defined by `SpecialFunctions` to improve numerical stability." : + "Uses the unscaled convention defined by `SpecialFunctions`." + return """ + $(name)(ν, z) + + Evaluate the Bessel-family function of order `ν` at `z`, with uncertainty + propagation when `z` is a `Measurement` or `Complex{Measurement}`. + + # Arguments + + - `ν`: Bessel-function order. + - `z`: Real or complex argument, optionally containing measurement uncertainty. + + # Returns + + - The function value, with first-order propagated uncertainty when applicable. + + # Notes + + $(scaling) Plain numeric inputs delegate directly to `SpecialFunctions`. + Measurement inputs are linearized at their nominal value using numerical + derivatives. + """ +end + +@doc _bessel_doc(:besselj, false) besselj +@doc _bessel_doc(:bessely, false) bessely +@doc _bessel_doc(:besseli, false) besseli +@doc _bessel_doc(:besselk, false) besselk +@doc _bessel_doc(:besselh, false) besselh +@doc _bessel_doc(:besseljx, true) besseljx +@doc _bessel_doc(:besselyx, true) besselyx +@doc _bessel_doc(:besselix, true) besselix +@doc _bessel_doc(:besselkx, true) besselkx +@doc _bessel_doc(:besselhx, true) besselhx end # module UncertainBessels diff --git a/src/unithandler/UnitHandler.jl b/src/unithandler/UnitHandler.jl index c8c44dc9..0e402403 100644 --- a/src/unithandler/UnitHandler.jl +++ b/src/unithandler/UnitHandler.jl @@ -3,71 +3,71 @@ 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_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, + :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", + :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 + :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) @@ -84,8 +84,8 @@ A single physical unit with a metric prefix. - `prefix` : one of the metric prefix symbols (:base, :kilo, :milli, ...). """ @kwdef struct Unit - name::Symbol = :dimensionless - prefix::Symbol = :base + name::Symbol = :dimensionless + prefix::Symbol = :base end """ @@ -95,8 +95,8 @@ A composite unit, e.g., "Ω/km". - `per` : denominator units. """ @kwdef struct Units - base::Vector{Unit} = [Unit()] # dimensionless by default - per::Vector{Unit} = Unit[] + base::Vector{Unit} = [Unit()] # dimensionless by default + per::Vector{Unit} = Unit[] end # -------------------------------------------------------------------------- @@ -108,22 +108,22 @@ Convenience constructor for `Units`. Examples: - u1 = units(:base, :ohm) # Ω - u2 = units(:base, :ohm; per = (:kilo, :meter)) # Ω/km + 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, + 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 + 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 # -------------------------------------------------------------------------- @@ -134,9 +134,9 @@ end 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) + prefix_str = _prefix_symbol(u.prefix) + unit_str = get(UNIT_SYMBOL, u.name, String(u.name)) + return string(prefix_str, unit_str) end """ @@ -148,30 +148,30 @@ Render composite `Units` as a readable string: - :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 + 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 # -------------------------------------------------------------------------- @@ -208,9 +208,9 @@ 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 + 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) @@ -228,9 +228,9 @@ 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) - +function get_symbol(::QuantityTag{Q}) where {Q} + Q isa Tuple ? get_symbol(QuantityTag{first(Q)}()) : String(Q) +end # -------------------------------------------------------------------------- # Unit scaling between arbitrary composite units @@ -241,24 +241,24 @@ 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) + 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) + 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) + 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) + return scale_factor(default_unit(q), to_unit) end # -------------------------------------------------------------------------- @@ -268,18 +268,18 @@ end """ Return the net base-10 exponent from the prefixes in a composite unit: - exp = (sum prefix exponents over `base`) - (sum over `per`) + 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 + 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 """ @@ -291,126 +291,99 @@ 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}()) +default_unit(::Val{Q}) where {Q} = default_unit(QuantityTag{Q}()) -display_unit(::Val{Q}) where {Q} = - display_unit(QuantityTag{Q}()) +display_unit(::Val{Q}) where {Q} = display_unit(QuantityTag{Q}()) -get_label(::Val{Q}) where {Q} = - get_label(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)) +default_unit(q::Symbol) = default_unit(Val(q)) -get_label(q::Symbol) = - get_label(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) +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 +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)) +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)) +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)) +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)) +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)) +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)) +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)) +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) +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{(: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" +# 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/uq/UQ.jl b/src/uq/UQ.jl index 25676c48..cbe9e5db 100644 --- a/src/uq/UQ.jl +++ b/src/uq/UQ.jl @@ -1,77 +1,79 @@ module UQ # Export public API -export sample, trial, mc, hist +export sample, trial, mc # Module-specific dependencies using ..Commons: BASE_FLOAT import ..Commons: domain, PhaseDomain, ModalDomain, - LineParamsDomain + LineParamsDomain using ..ParametricBuilder: - MaterialSpec, PartSpec, CableBuilderSpec, SystemBuilderSpec, AbstractPositionSpec, - PositionSpec, PositionGroupSpec, build, iterate, _spec, determinize + MaterialSpec, PartSpec, CableBuilderSpec, SystemBuilderSpec, + AbstractPositionSpec, + PositionSpec, PositionGroupSpec, build, iterate, _spec, + determinize using ..Engine: - EMTFormulation, compute!, LineParameters + 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 + 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 + 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_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`. @@ -79,7 +81,7 @@ Collapse a (spec, pct) pair into `(value, pct_value)` by drawing once from the 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) + (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. @@ -88,149 +90,147 @@ 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, + 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) + 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) - )..., - ) + 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, + 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), - ) + 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) + 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 + 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, + cbs::CableBuilderSpec; + distribution::Symbol = :normal ) - parts = PartSpec[_collapse_part(p, distribution) for p in cbs.parts] - return CableBuilderSpec(cbs.cable_id, parts, cbs.nominal) + 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 + 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, + 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] + 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_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 @@ -238,43 +238,43 @@ 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, - ) + 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, + 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, - ) + # 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(sbs::SystemBuilderSpec; distribution::Symbol = :uniform) -> SystemBuilderSpec Collapse ranges in a `SystemBuilderSpec` using existing helpers. @@ -286,74 +286,71 @@ Rules: - Inner `builder` → `collapse(builder; distribution)`. """ function collapse( - sbs::SystemBuilderSpec; - distribution::Symbol = :normal, + 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, - ) + # 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 - """ - sample(sbs::SystemBuilderSpec; distribution::Symbol = :uniform) + sample(sbs::SystemBuilderSpec; distribution::Symbol = :uniform) Collapse ranges in `sbs` and produce one `LineParametersProblem`. """ function sample( - sbs::SystemBuilderSpec; - distribution::Symbol = :normal, + sbs::SystemBuilderSpec; + distribution::Symbol = :normal ) - ss = collapse(sbs; distribution = distribution) - ch = iterate(ss) - return take!(ch) + 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/dataframe.jl b/src/uq/dataframe.jl index bf3423ec..91dc0902 100644 --- a/src/uq/dataframe.jl +++ b/src/uq/dataframe.jl @@ -1,45 +1,45 @@ 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 + 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], - ) + 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 index 3f532e16..a5eb4016 100644 --- a/src/uq/distributions.jl +++ b/src/uq/distributions.jl @@ -1,17 +1,16 @@ @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 + 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) + trial(res::LineParametersMC, t::Integer) Reconstruct one complete empirical Monte Carlo realization. @@ -48,32 +47,31 @@ 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) + 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) + rand(rng::AbstractRNG, res::LineParametersMC) + rand(res::LineParametersMC) Draw one complete empirical Monte Carlo realization uniformly. @@ -105,20 +103,17 @@ 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)) + 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 @@ -131,69 +126,69 @@ 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, + 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 + 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.") + n = length(x) + n == 0 && error("Empty sample set.") - nb = isnothing(nbins) ? _auto_nbins(x) : nbins + 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) + 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 + return LineParametersPDF(edges, dens) # ctor re-normalizes area end -# Density at x0 +# 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] + i = searchsortedlast(hp.edges, float(x0)) + (i < 1 || i >= length(hp.edges)) && return 0.0 + return hp.dens[i] end """ @@ -202,23 +197,23 @@ 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 + 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 + # 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) + # 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 + # This should be redundant given the initial check, but belt and suspenders. + (i < 1 || i > length(d.dens)) && return 0 - return i + return i end """ @@ -226,22 +221,22 @@ 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 + 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 """ @@ -249,31 +244,31 @@ 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 + 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 # ───────────────────────────────────────────────────────────────────────────── @@ -286,47 +281,47 @@ 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] + # 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] + 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 + 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 + 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) --- @@ -336,180 +331,180 @@ 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 + 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) + 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 + # 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) + 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 + # 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) + 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) + # 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) + # 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] + # 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 + # 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] + # 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) + 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) + # 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) + # 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 + # 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] + m = _raw_moments(d, 3) + m1, m2, m3 = m[1], m[2], m[3] - μ = m1 - μ2 = m2 - μ^2 # Variance + μ = 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 + # 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) + μ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) + # 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] + m = _raw_moments(d, 4) + m1, m2, m3, m4 = m[1], m[2], m[3], m[4] - μ = m1 - μ2 = m2 - μ^2 # Variance + μ = m1 + μ2 = m2 - μ^2 # Variance - # Degenerate case: variance is zero. Return NaN. - if μ2 <= eps(T) - return T(NaN) - end + # Degenerate case: variance is zero. Return NaN. + if μ2 <= eps(T) + return T(NaN) + end - μ4 = m4 - 4*μ*m3 + 6*μ^2*m2 - 3*μ^4 + μ4 = m4 - 4*μ*m3 + 6*μ^2*m2 - 3*μ^4 - kurt = μ4 / μ2^2 - return excess ? (kurt - 3) : kurt + 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. + # 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 + 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. + # 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) + 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] + return [(d.edges[i] + d.edges[i + 1]) / 2 for i in indices] end """ @@ -517,29 +512,29 @@ 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)] + = - Σ ∫_{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 + 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 """ @@ -549,14 +544,13 @@ 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")) + (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) + # Just convert the base e entropy. + return Distributions.entropy(d) / log(b) end - # ───────────────────────────────────────────────────────────────────────────── # Moment Generating & Characteristic Functions # ───────────────────────────────────────────────────────────────────────────── @@ -566,9 +560,9 @@ 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)) + = Σ ∫_{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: @@ -580,32 +574,31 @@ 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 + t == 0 && return one(T) # MGF(0) = 1 - acc = zero(T) + acc = zero(T) - @inbounds for i in 1:length(d.dens) - dens_i = d.dens[i] - dens_i == 0 && continue + @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 + a = d.edges[i] + b = d.edges[i + 1] + w = b - a - # This is the argument to exprel: z = t*w - z = t * w + # 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 + # 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 + # 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) @@ -616,23 +609,23 @@ 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}) + t == 0 && return one(Complex{T}) - acc = zero(Complex{T}) + acc = zero(Complex{T}) - @inbounds for i in 1:length(d.dens) - dens_i = d.dens[i] - dens_i == 0 && continue + @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 + a = d.edges[i] + b = d.edges[i + 1] + w = b - a - z = im * t * w + z = im * t * w - exprel_z = iszero(z) ? one(z) : expm1(z) / z + exprel_z = iszero(z) ? one(z) : expm1(z) / z - acc += dens_i * exp(im * t * a) * w * exprel_z - end - return acc + acc += dens_i * exp(im * t * a) * w * exprel_z + end + return acc end diff --git a/src/uq/montecarlo.jl b/src/uq/montecarlo.jl index fef1cbda..e3179cef 100644 --- a/src/uq/montecarlo.jl +++ b/src/uq/montecarlo.jl @@ -1,7 +1,7 @@ """ - 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) + 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. @@ -32,121 +32,120 @@ 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, + 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, - ) + 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) + 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. @@ -205,262 +204,260 @@ 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, + 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) - + 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}, + 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 + 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}, + ::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) + 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/plot.jl b/src/uq/plot.jl index 7ad8b957..8b78b836 100644 --- a/src/uq/plot.jl +++ b/src/uq/plot.jl @@ -1,709 +1,694 @@ 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 +import LineCableModels.Engine: plot + +using ..EnginePlots: + 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 + 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), - ), - ) + 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 _quantity_metadata(q::Symbol) + get(_MC_META, q) do + Base.error("Unsupported quantity $(q); use one of :R, :L, :C, :G") + end +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 + 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, + 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, - ) + # 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, + 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] + 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) + 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 + 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 + 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, + 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 + 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 + 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, - ) + 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, + 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) + 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", - ) + 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 + 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, - ) + # 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, + 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] + 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, + 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) + 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 index 55df90d3..58d775d7 100644 --- a/src/uq/plotspecs/mcstatsplotspec.jl +++ b/src/uq/plotspecs/mcstatsplotspec.jl @@ -1,6 +1,7 @@ using ..UnitHandler: - QuantityTag, default_unit, display_unit, get_label, get_symbol, scale_factor + QuantityTag, default_unit, display_unit, get_label, get_symbol, + scale_factor using ..PlotBuilder: AbstractPlotSpec, PlotBuilder struct MCStatsPlotSpec <: AbstractPlotSpec end @@ -24,17 +25,22 @@ PlotBuilder.geom_axes(::Type{MCStatsPlotSpec}) = (:x, :y) 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}() +# X is always frequency; Y will depend on user kwarg, so the valid possible quantities are defined below. +function PlotBuilder.axis_quantity(::Type{MCStatsPlotSpec}, ::Val{:x}, ::Val{:f}) + QuantityTag{:freq}() +end +function PlotBuilder.axis_quantity(::Type{MCStatsPlotSpec}, ::Val{:y}, ::Val{:R}) + QuantityTag{:resistance}() +end +function PlotBuilder.axis_quantity(::Type{MCStatsPlotSpec}, ::Val{:y}, ::Val{:L}) + QuantityTag{:inductance}() +end +function PlotBuilder.axis_quantity(::Type{MCStatsPlotSpec}, ::Val{:y}, ::Val{:C}) + QuantityTag{:capacitance}() +end +function PlotBuilder.axis_quantity(::Type{MCStatsPlotSpec}, ::Val{:y}, ::Val{:G}) + QuantityTag{:conductance}() +end PlotBuilder.data_container(::Type{MCStatsPlotSpec}, ::Val{:x}) = nothing # obj.f PlotBuilder.data_container(::Type{MCStatsPlotSpec}, ::Val{:y}) = :stats # obj.stats[Sym] @@ -43,27 +49,27 @@ PlotBuilder.data_container(::Type{MCStatsPlotSpec}, ::Val{:y}) = :stats # Define plot title function PlotBuilder.default_title(::Type{MCStatsPlotSpec}, nt::NamedTuple) - qx = nt.x_quantity - qy = nt.y_quantity - key = nt.field + qx = nt.x_quantity + qy = nt.y_quantity + key = nt.field - y_label = get_label(qy) # "Series resistance" - x_label = get_label(qx) # "Frequency" + y_label = get_label(qy) # "Series resistance" + x_label = get_label(qx) # "Frequency" - return string(y_label, " ", String(key), " vs. ", x_label) + 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 + qy = nt.y_quantity + key = nt.field + i = nt.i + j = nt.j - y_label = get_symbol(qy) + y_label = get_symbol(qy) - entry = string(y_label, "[", i, ",", j, "] ", String(key)) - return [entry] + entry = string(y_label, "[", i, ",", j, "] ", String(key)) + return [entry] end # Semantic knobs: @@ -74,12 +80,14 @@ PlotBuilder.input_kwargs(::Type{MCStatsPlotSpec}) = (:field,) 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 :) -) +function 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 :) + ) +end PlotBuilder.select_field(::Type{MCStatsPlotSpec}, ::Val{:x}) = nothing PlotBuilder.select_field(::Type{MCStatsPlotSpec}, ::Val{:y}) = :field # or :mean directly diff --git a/src/uq/types.jl b/src/uq/types.jl index 3effa9b8..6edc0f67 100644 --- a/src/uq/types.jl +++ b/src/uq/types.jl @@ -2,39 +2,39 @@ # 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 + 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}, + edges::AbstractVector{T}, + dens::AbstractVector{T} ) where {T <: Real} - length(edges) == length(dens) + 1 || - throw(ArgumentError("edges must have length length(dens)+1")) + length(edges) == length(dens) + 1 || + throw(ArgumentError("edges must have length length(dens)+1")) - e = Vector(edges) # Just convert once - d = Vector(dens) + e = Vector(edges) # Just convert once + d = Vector(dens) - # Ensure increasing edges - issorted(e) || throw(ArgumentError("edges must be sorted ascending")) + # 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")) + # 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 + # Only normalize if it's not already 1 (avoids float division noise) + if !(area ≈ 1.0) + d ./= area + end - return LineParametersPDF{T}(e, d) + return LineParametersPDF{T}(e, d) end """ - LineParametersMC{U, D} + LineParametersMC{U, D} Store Monte Carlo summaries and a joint moment-matched line-parameter surrogate. @@ -49,92 +49,92 @@ 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} + "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}, + 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) + 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(::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}} + "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 index 389d398a..e178af70 100644 --- a/src/utils/Utils.jl +++ b/src/utils/Utils.jl @@ -1,5 +1,5 @@ """ - LineCableModels.Utils + 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. @@ -13,9 +13,6 @@ The [`Utils`](@ref) module provides utility functions for the [`LineCableModels $(IMPORTS) -# Exports - -$(EXPORTS) """ module Utils @@ -24,12 +21,12 @@ 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 + to_certain, + percent_to_uncertain, + bias_to_uncertain, + to_upper, + to_lower, + percent_error export _to_σ, _bessel_diff, symtrans!, line_transpose! @@ -38,7 +35,6 @@ using ..Commons using ..UncertainBessels: besselk using Measurements: Measurement, value, uncertainty, measurement, ±, Measurements, result using Statistics -using Plots using LinearAlgebra """ @@ -96,7 +92,7 @@ result = $(FUNCTIONNAME)(y) # Output: 10.0 ``` """ function to_certain(value) - return value isa Measurement ? (Measurements.value(value) ± 0.0) : value + return value isa Measurement ? (Measurements.value(value) ± 0.0) : value end """ @@ -123,7 +119,7 @@ $(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) + measurement(val, (perc * val) / 100) end """ @@ -157,13 +153,13 @@ 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) + # 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 """ @@ -193,11 +189,11 @@ 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 + if m isa Measurement + return Measurements.value(m) + Measurements.uncertainty(m) + else + return NaN + end end """ @@ -227,11 +223,11 @@ 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 + if m isa Measurement + return Measurements.value(m) - Measurements.uncertainty(m) + else + return NaN + end end """ @@ -261,37 +257,38 @@ 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 + 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 +function _coerce_args_to_T(args...) + any(x -> x isa Measurement, args) ? Measurement{BASE_FLOAT} : BASE_FLOAT +end # 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 + 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 + 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 """ @@ -308,35 +305,35 @@ Determines if the current execution environment is headless (without display cap ```julia if $(FUNCTIONNAME)() - # Use non-graphical backend - gr() + # Use non-graphical backend + gr() else - # Use interactive backend - plotlyjs() + # 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 + # 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) + return is_headless() ? basename(file_name) : relpath(file_name) end """ @@ -346,30 +343,28 @@ Checks if the code is running inside a `@testset` by checking if `Test` is loade 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 + # 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. @@ -383,45 +378,44 @@ Apply `f` to every square block of `M` defined by `map`, in-place. 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 + 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) - +function block_transform(M, cmap, f, args...; slice_positions = Int[]) + block_transform!(copy(M), cmap, f, args...; slice_positions = slice_positions) +end # Reciprocity symmetrization — in place symtrans!(A) = (A .= 0.5 .* (A .+ transpose(A)); A) @@ -429,66 +423,64 @@ 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 + 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) + 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)) + 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) + 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)) + 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") diff --git a/src/utils/logging.jl b/src/utils/logging.jl index 3f9ba85a..604787bb 100644 --- a/src/utils/logging.jl +++ b/src/utils/logging.jl @@ -4,56 +4,56 @@ 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 - - +levelfrom(v::Integer)::Logging.LogLevel = v >= 2 ? Logging.Debug : + v == 1 ? Logging.Info : + v == 0 ? Logging.Warn : + Logging.Error struct TimestampLogger <: AbstractLogger - logger::AbstractLogger + logger::AbstractLogger +end + +function Logging.min_enabled_level(logger::TimestampLogger) + Logging.min_enabled_level(logger.logger) +end +function Logging.shouldlog(logger::TimestampLogger, level, _module, group, id) + Logging.shouldlog(logger.logger, level, _module, group, id) 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...) +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 + # 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) + # 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 index 8c6bd6dd..05a4a162 100644 --- a/src/utils/macros.jl +++ b/src/utils/macros.jl @@ -3,7 +3,7 @@ export @parameterize, @measurify using MacroTools macro parameterize(container_expr, union_expr) - # Evaluate the Union type from the provided expression + # 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) @@ -20,7 +20,7 @@ macro parameterize(container_expr, union_expr) # 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 `_` + # 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). @@ -38,10 +38,11 @@ macro parameterize(container_expr, union_expr) end end - # Build the list of new, concrete types - parameterized_types = [substitute_placeholder(container_expr, t) for t in component_types] + # 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 + # Wrap the new types in a single `Union{...}` expression and escape final_expr = Expr(:curly, :Union, parameterized_types...) return esc(final_expr) end @@ -66,13 +67,13 @@ macro measurify(def) end found end - function _relax_container_type(ty, typevars::Set{Symbol}, bounds::Dict{Symbol,Any}) + 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) : + (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...) @@ -86,8 +87,8 @@ macro measurify(def) [: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) + _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 @@ -100,7 +101,7 @@ macro measurify(def) where_items = get(dict, :whereparams, []) typevars = Set{Symbol}() - bounds = Dict{Symbol,Any}() + 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) @@ -168,7 +169,8 @@ macro measurify(def) end # 2) If anchored, CONVERT the anchor only if its T differs (avoid clone-on-noop) - anchor_convert = anchor_sym === nothing ? nothing : quote + anchor_convert = anchor_sym === nothing ? nothing : + quote Tcur = first(typeof($(anchor_sym)).parameters) if Tcur !== TargetType $(anchor_sym) = convert(EarthModel{TargetType}, $(anchor_sym)) @@ -181,7 +183,8 @@ macro measurify(def) # 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] + 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) @@ -196,7 +199,8 @@ macro measurify(def) # ---------------------- # Drop where if no raw typevars remain in wrapper signature - needs_where = any(_contains_tvar(arg, typevars) for arg in [wrapper_pos..., wrapper_kw...]) + needs_where = any(_contains_tvar(arg, typevars) + for arg in [wrapper_pos..., wrapper_kw...]) if !needs_where delete!(wrapper_dict, :whereparams) end @@ -224,7 +228,7 @@ Automatically exports public functions, types, and modules from a module. This i # 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 @@ -235,7 +239,7 @@ macro autoexport() mod = __module__ # Get all names defined in the module, including unexported ones - all_names = names(mod; all=true) + all_names = names(mod; all = true) # List of names to explicitly exclude excluded_names = Set([:eval, :include, :using, :import, :export, :require]) @@ -260,4 +264,4 @@ macro autoexport() end return esc(Expr(:export, public_names...)) -end \ No newline at end of file +end diff --git a/src/utils/typecoercion.jl b/src/utils/typecoercion.jl index cec9066c..3a6277e8 100644 --- a/src/utils/typecoercion.jl +++ b/src/utils/typecoercion.jl @@ -33,9 +33,10 @@ $(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} = +function _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) +end +_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. @@ -81,9 +82,10 @@ 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} = +function _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) +end +_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 @@ -204,15 +206,17 @@ $(FUNCTIONNAME)(missing, Float64) # missing $(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(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) +function _coerce_elt_to_T(m::Measurement, ::Type{R}) where {R <: AbstractFloat} + convert(R, value(m)) +end _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::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 """ @@ -253,10 +257,6 @@ $(FUNCTIONNAME)((; a=1.0, b=2.0), Float32) # (a = 1.0f0, b = 2.0f0) $(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) @@ -267,21 +267,22 @@ coerce_to_T(x::T, ::Type{T}) where {T} = x # exact-type pass-through, no alloca # 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 +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)) +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} = +function 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)) +end # Complex → Real: drop imag -coerce_to_T(x::Complex, ::Type{R}) where {R<:Real} = coerce_to_T(real(x), R) +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) @@ -297,10 +298,11 @@ 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 +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} = +function 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))) +end # --- 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) diff --git a/src/validation/Validation.jl b/src/validation/Validation.jl index 5ce8b846..bee0ab03 100644 --- a/src/validation/Validation.jl +++ b/src/validation/Validation.jl @@ -1,5 +1,5 @@ """ - LineCableModels.Validation + 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. @@ -13,17 +13,15 @@ The [`Validation`](@ref) module implements a trait-driven, three-phase input che $(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 + 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 @@ -170,16 +168,13 @@ 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. +Field-aware acceptance predicate used by `sanitize` to distinguish inner vs. outer radius policies. The default forwards to `is_radius_input(::Type{T}, x)` when no field-specific method is defined. # Arguments @@ -198,10 +193,6 @@ 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) @@ -227,8 +218,9 @@ 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) +function is_radius_input(::Type{T}, ::Val{:r_in}, x::Number) where {T} + (x isa Number) && !(x isa Complex) +end is_radius_input(::Type{T}, ::Val{:r_in}, ::Any) where {T} = false """ @@ -252,8 +244,9 @@ Default policy for **outer** radius raw inputs (annular shells): accept real num 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) +function is_radius_input(::Type{T}, ::Val{:r_ex}, x::Number) where {T} + (x isa Number) && !(x isa Complex) +end is_radius_input(::Type{T}, ::Val{:r_ex}, ::Any) where {T} = false """ @@ -298,29 +291,24 @@ $(FUNCTIONNAME)(X) # => (temperature = T₀, lay_direction = 1) $(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 + 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 """ @@ -353,61 +341,61 @@ 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 + # -- 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 in 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 """ @@ -440,17 +428,17 @@ Generates (at compile time, via a `@generated` function) the tuple of rules to a - 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)..., - )) + :(( + ( + 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 """ @@ -479,22 +467,18 @@ 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 + # 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 index 92e03acd..c410a4c6 100644 --- a/src/validation/applyrules.jl +++ b/src/validation/applyrules.jl @@ -66,16 +66,15 @@ $(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 + 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) @@ -92,9 +91,9 @@ Applies [`Finite`](@ref) to ensure the target field is a finite real number. - 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")) + 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 """ @@ -113,9 +112,9 @@ Applies [`Nonneg`](@ref) to ensure the target field is `≥ 0`. - 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")) + 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 """ @@ -134,9 +133,9 @@ Applies [`Positive`](@ref) to ensure the target field is `> 0`. - 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")) + 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 """ @@ -155,10 +154,10 @@ Applies [`IntegerField`](@ref) to ensure the target field is an `Integer`. - 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))"), - ) + x = getfield(nt, r.name) + x isa Integer || throw( + ArgumentError("[$(_typename(T))] $(r.name) must be Integer, got $(typeof(x))"), + ) end """ @@ -177,12 +176,12 @@ Applies [`Less`](@ref) to ensure `nt[a] < nt[b]`. - 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)")) + 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 """ @@ -201,12 +200,12 @@ Applies [`Greater`](@ref) to ensure `nt[a] > nt[b]`. - 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)")) + 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 """ @@ -225,12 +224,12 @@ Applies [`LessEq`](@ref) to ensure `nt[a] ≤ nt[b]`. - 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)")) + 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 """ @@ -249,12 +248,12 @@ Applies [`GreaterEq`](@ref) to ensure `nt[a] ≥ nt[b]`. - 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)")) + 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 """ @@ -273,9 +272,9 @@ Applies [`IsA{M}`](@ref) to ensure a field is of type `M`. - 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))")) + x = getfield(nt, r.name) + x isa M || + throw(ArgumentError("[$(_typename(T))] $(r.name) must be $(M), got $(typeof(x))")) end """ @@ -294,12 +293,12 @@ Applies [`Normalized`](@ref) to ensure the field has been converted to a numeric - 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))", - ), - ) + x = getfield(nt, r.name) + x isa Number || throw( + ArgumentError( + "[$(_typename(T))] $(r.name) must be normalized Number; got $(typeof(x))", + ), + ) end """ @@ -318,16 +317,16 @@ Applies [`OneOf`](@ref) to ensure the target field is contained in a specified s - 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)", - ), - ) + 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...) + maxfill(::Type{T}, args...) Calculates the maximum physical number of strands that can fit for component `T`. Custom shapes must overload this method. @@ -349,26 +348,26 @@ Applies [`PhysicalFillLimit`](@ref) to ensure the element count does not exceed - 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 +@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 """ @@ -390,29 +389,29 @@ Applies [`Satisfies`](@ref) to evaluate an arbitrary predicate function against ```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." - ) + # ... 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 +@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 index 145eca11..3d69197a 100644 --- a/src/validation/rules.jl +++ b/src/validation/rules.jl @@ -15,8 +15,8 @@ Rule that enforces finiteness of a numeric field. $(TYPEDFIELDS) """ struct Finite <: Rule - "Name of the field to check." - name::Symbol + "Name of the field to check." + name::Symbol end """ @@ -27,8 +27,8 @@ Rule that enforces a field to be non‑negative (`≥ 0`). $(TYPEDFIELDS) """ struct Nonneg <: Rule - "Name of the field to check." - name::Symbol + "Name of the field to check." + name::Symbol end """ @@ -39,8 +39,8 @@ Rule that enforces a field to be strictly positive (`> 0`). $(TYPEDFIELDS) """ struct Positive <: Rule - "Name of the field to check." - name::Symbol + "Name of the field to check." + name::Symbol end """ @@ -51,8 +51,8 @@ Rule that enforces a field to be of an integer type. $(TYPEDFIELDS) """ struct IntegerField <: Rule - "Name of the field to check." - name::Symbol + "Name of the field to check." + name::Symbol end """ @@ -63,10 +63,10 @@ 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 + "Left‑hand field name." + a::Symbol + "Right‑hand field name." + b::Symbol end """ @@ -77,10 +77,10 @@ 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 + "Left‑hand field name." + a::Symbol + "Right‑hand field name." + b::Symbol end """ @@ -91,10 +91,10 @@ Rule that enforces a non‑strict ordering constraint `a ≤ b` between two fiel $(TYPEDFIELDS) """ struct LessEq <: Rule - "Left‑hand field name." - a::Symbol - "Right‑hand field name." - b::Symbol + "Left‑hand field name." + a::Symbol + "Right‑hand field name." + b::Symbol end """ @@ -105,10 +105,10 @@ Rule that enforces a non‑strict ordering constraint `a ≥ b` between two fiel $(TYPEDFIELDS) """ struct GreaterEq <: Rule - "Left‑hand field name." - a::Symbol - "Right‑hand field name." - b::Symbol + "Left‑hand field name." + a::Symbol + "Right‑hand field name." + b::Symbol end """ @@ -119,8 +119,8 @@ 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 + "Name of the field to check." + name::Symbol end """ @@ -131,8 +131,8 @@ Rule that enforces that a field has already been normalized to a numeric value d $(TYPEDFIELDS) """ struct Normalized <: Rule - "Name of the field to check." - name::Symbol + "Name of the field to check." + name::Symbol end """ @@ -143,8 +143,8 @@ Rule that enforces a field to be `in` the set `S`. $(TYPEDFIELDS) """ struct OneOf{S} <: Rule - name::Symbol - set::S + name::Symbol + set::S end """ @@ -155,10 +155,10 @@ Rule that enforces that the number of discrete elements (e.g., wires or strands) $(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}} + "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 """ @@ -170,10 +170,10 @@ Useful for complex, cross-field physics or one-off geometrical constraints witho $(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 + "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/aqua.jl b/test/aqua.jl index cda8ad5d..0d7e03e8 100644 --- a/test/aqua.jl +++ b/test/aqua.jl @@ -1,5 +1,4 @@ -@testitem "Aqua tests" tags=[:skipci] begin - using Aqua - Aqua.test_all(LineCableModels) +@testitem "Aqua tests" begin + using Aqua + Aqua.test_all(LineCableModels) end - diff --git a/test/baseparams.jl b/test/baseparams.jl index e0948610..bc5f50e8 100644 --- a/test/baseparams.jl +++ b/test/baseparams.jl @@ -1,377 +1,361 @@ @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 + @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/datamodel.jl b/test/datamodel.jl index 0f54371c..d3405374 100644 --- a/test/datamodel.jl +++ b/test/datamodel.jl @@ -1,777 +1,762 @@ @testsnippet deps_datamodel begin - using Plots - using EzXML - using Makie: Makie, Figure, Axis + using EzXML + if get(ENV, "LINECABLEMODELS_TEST_PLOTTING", "false") == "true" + using CairoMakie + end 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.") - + 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) + + LineCableModels.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(LineCableModels.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 + + if get(ENV, "LINECABLEMODELS_TEST_PLOTTING", "false") == "true" + println("\nTesting plotting functions...") + + @test set_backend!(:cairo) == :cairo + @test LineCableModels.PlotBuilder.BackendHandler.with_backend( + () -> :rendered, + :cairo + ) == :rendered + + fig, ax = preview( + cable_design, + display_plot = false, + display_legend = true + ) + @test fig isa CairoMakie.Figure + @test ax isa CairoMakie.Axis + fig, ax = preview( + cable_design, + display_plot = false, + display_legend = false + ) + @test fig isa CairoMakie.Figure + @test ax isa CairoMakie.Axis + + fig, ax = preview(cable_system, zoom_factor = 0.5, display_plot = false) + @test fig isa CairoMakie.Figure + @test ax isa CairoMakie.Axis + + println(" Plotting functions executed without errors.") + end + + 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 index 0e2d49c8..66a95846 100644 --- a/test/earthprops.jl +++ b/test/earthprops.jl @@ -5,7 +5,6 @@ end @testitem "EarthProps module" setup = [defaults, defs_earthprops] begin - @testset "FDEM Formulations" begin @testset "CPEarth" begin cp_formulation = EP.CPEarth() @@ -62,7 +61,8 @@ end t = 5.0 formulation = EP.CPEarth() - layer = EP.EarthLayer(frequencies, base_rho_g, base_epsr_g, base_mur_g, t, formulation) + 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 @@ -89,13 +89,13 @@ end end @testset "Finite Thickness Layer" begin - model = EarthModel(frequencies, rho_g, epsr_g, mur_g, t=20.0) + 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) + model = EarthModel(frequencies, rho_g, epsr_g, mur_g, vertical_layers = true) @test model.vertical_layers == true end @@ -104,7 +104,8 @@ end @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) + @test_throws AssertionError EarthModel( + frequencies, rho_g, epsr_g, mur_g, t = -5.0) end end @@ -112,52 +113,53 @@ end frequencies = [50.0, 60.0] @testset "Horizontal Layering" begin - model = EarthModel(frequencies, 100.0, 10.0, 1.0, t=20.0) + 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) + 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) + 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) + @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) + 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) + @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) + 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) + @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 + 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) + @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_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 @@ -173,16 +175,17 @@ end @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) + 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) + 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]") @@ -191,8 +194,8 @@ 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) + 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 @@ -222,27 +225,34 @@ end @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( + "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) + 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( + "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) + 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( + "EarthModel with 2 vertical earth layers (multilayer) and 2 frequency samples", + str_repr) @test occursin("t=5", str_repr) end end @@ -260,7 +270,7 @@ end (freqF, ρM, εM, μM), # <- the one that used to blow up (freqM, ρF, εM, μF), (freqF, ρF, εM, μM), - (freqM, ρM, εF, μF), + (freqM, ρM, εF, μF) ) promT(ρ, ε, μ) = promote_type(typeof(ρ), typeof(ε), typeof(μ)) @@ -304,7 +314,7 @@ end # --- EarthModel: scalars Measurement, freqs Float64 (lift container) --- @testset "EarthModel: freqs Float64, scalars Measurement" begin - model = EarthModel(freqsF, ρM, εM, μM; t=20.0) + 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 @@ -317,7 +327,7 @@ end # --- EarthModel: scalars Float64, freqs Measurement --- @testset "EarthModel: freqs Measurement, scalars Float64" begin - model = EarthModel(freqsM, ρF, εF, μF; t=10.0) + model = EarthModel(freqsM, ρF, εF, μF; t = 10.0) @test length(model.layers) == 2 earth = model.layers[2] Texp = eltype(freqsM) @@ -328,10 +338,10 @@ 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) + 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) + 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 @@ -342,16 +352,16 @@ 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) + 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 + 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 @@ -366,5 +376,4 @@ end end @info "EarthProps tests completed." - end diff --git a/test/runtests.jl b/test/runtests.jl index 0bd85fdf..36690693 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,65 +2,66 @@ using LineCableModels using Test using TestItemRunner +if get(ENV, "LINECABLEMODELS_TEST_PLOTTING", "false") == "true" + using CairoMakie +end + @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 + 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.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) + 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) - 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) + # 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) - # 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)) + # 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) - freqs = [50.0] - earth_props = EarthModel(freqs, 100.0, 10.0, 1.0) - num_phases = cable_system.num_phases + # 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)) - # 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 - ) + 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)) +@run_package_tests(verbose = true) diff --git a/test/test_tutorial1.jl b/test/test_tutorial1.jl index 57718cf8..d8728e54 100644 --- a/test/test_tutorial1.jl +++ b/test/test_tutorial1.jl @@ -1,12 +1,13 @@ @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) + 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 @@ -24,7 +25,7 @@ end @testset "add materials from tutorial" begin - materials = MaterialsLibrary(add_defaults=false) # start clean for deterministic tests + 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) @@ -49,7 +50,7 @@ end @testset "remove duplicate" begin - materials = MaterialsLibrary(add_defaults=false) + materials = MaterialsLibrary(add_defaults = false) epr = Material(1e15, 3.0, 1.0, 20.0, 0.005) add!(materials, "epr", epr) @@ -61,7 +62,7 @@ end @testset "save and load round-trip (temp file)" begin - materials = MaterialsLibrary(add_defaults=false) + 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) @@ -72,12 +73,12 @@ tmpfile = tempname() * ".json" try # Save to temporary file - save(materials, file_name=tmpfile) + save(materials, file_name = tmpfile) @test isfile(tmpfile) # Load into a fresh library - loaded = MaterialsLibrary(add_defaults=false) - load!(loaded, file_name=tmpfile) + loaded = MaterialsLibrary(add_defaults = false) + load!(loaded, file_name = tmpfile) # Keys present after load @test haskey(loaded, "copper_corrected") @@ -86,7 +87,8 @@ # 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) + @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) @@ -99,27 +101,29 @@ @testset "error handling" begin # Fresh empty library (no defaults) for deterministic error behavior - empty_lib = MaterialsLibrary(add_defaults=false) + 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") + 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) + 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, "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)) @@ -138,11 +142,11 @@ tmpfile = tempname() * ".json" try - save(materials, file_name=tmpfile) + save(materials, file_name = tmpfile) @test isfile(tmpfile) - reloaded = MaterialsLibrary(add_defaults=false) - load!(reloaded, file_name=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") @@ -157,5 +161,4 @@ isfile(tmpfile) && rm(tmpfile) end end - -end \ No newline at end of file +end diff --git a/test/test_tutorial2.jl b/test/test_tutorial2.jl index ec78cf9d..b9efaa6c 100644 --- a/test/test_tutorial2.jl +++ b/test/test_tutorial2.jl @@ -1,305 +1,302 @@ @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 + # 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 + if get(ENV, "LINECABLEMODELS_TEST_PLOTTING", "false") == "true" + @test preview(cable_design, display_plot = false) isa Any + end + + 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 ccdecc19..00000000 --- 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 index 65e69abf..53196a2c 100644 --- a/test/test_tutorial_2_sector.jl +++ b/test/test_tutorial_2_sector.jl @@ -2,7 +2,7 @@ # Replicate the setup from the tutorial # === Materials === - materials = MaterialsLibrary(add_defaults=true) + materials = MaterialsLibrary(add_defaults = true) pvc = Material(Inf, 8.0, 1.0, 20.0, 0.1) add!(materials, "pvc", pvc) copper = get(materials, "copper") @@ -72,7 +72,8 @@ 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)) + neutral_component = CableComponent( + "neutral", ConductorGroup(neutral_wires), InsulatorGroup(neutral_jacket)) @test neutral_component.id == "neutral" end @@ -85,7 +86,8 @@ 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) + 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] @@ -101,9 +103,11 @@ 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_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)) + neutral_component = CableComponent( + "neutral", ConductorGroup(neutral_wires), InsulatorGroup(neutral_jacket)) design = CableDesign("NAYCWY_O_3x95_30x2_5", components[1]) add!(design, components[2]) @@ -126,7 +130,8 @@ 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) + 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] @@ -141,9 +146,11 @@ 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_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)) + 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]) @@ -154,15 +161,20 @@ @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 + if get(ENV, "LINECABLEMODELS_TEST_PLOTTING", "false") == "true" + @test preview(design, display_plot = false) isa Any + end 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) + @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/unit_BaseParams/test_calc_equivalent_alpha.jl b/test/unit_BaseParams/test_calc_equivalent_alpha.jl index 945b6ef2..f2612b0f 100644 --- a/test/unit_BaseParams/test_calc_equivalent_alpha.jl +++ b/test/unit_BaseParams/test_calc_equivalent_alpha.jl @@ -1,5 +1,4 @@ @testitem "BaseParams: calc_equivalent_alpha unit tests" setup = [defaults] begin - @testset "calc_equivalent_alpha: Basic Functionality (Copper & Aluminum)" begin alpha1 = 0.00393 # Copper R1 = 0.5 @@ -7,7 +6,7 @@ R2 = 1.0 expected = (alpha1 * R2 + alpha2 * R1) / (R1 + R2) result = calc_equivalent_alpha(alpha1, R1, alpha2, R2) - @test isapprox(result, expected; atol=TEST_TOL) + @test isapprox(result, expected; atol = TEST_TOL) end @testset "calc_equivalent_alpha: Edge Case - Zero Resistance" begin @@ -17,7 +16,7 @@ R2 = 1.0 expected = alpha1 # Only R2 matters result = calc_equivalent_alpha(alpha1, R1, alpha2, R2) - @test isapprox(result, expected; atol=TEST_TOL) + @test isapprox(result, expected; atol = TEST_TOL) alpha1 = 0.00393 R1 = 0.5 @@ -25,7 +24,7 @@ R2 = 0.0 expected = alpha2 # Only R1 matters result = calc_equivalent_alpha(alpha1, R1, alpha2, R2) - @test isapprox(result, expected; atol=TEST_TOL) + @test isapprox(result, expected; atol = TEST_TOL) end @testset "calc_equivalent_alpha: Edge Case - Very Large Resistance" begin @@ -35,10 +34,9 @@ R2 = 1.0 expected = (alpha1 * R2 + alpha2 * R1) / (R1 + R2) result = calc_equivalent_alpha(alpha1, R1, alpha2, R2) - @test isapprox(result, expected; atol=TEST_TOL) + @test isapprox(result, expected; atol = TEST_TOL) end - @testset "calc_equivalent_alpha: Type Stability & Promotion" begin alpha1 = 0.00393 R1 = 0.5 @@ -59,8 +57,9 @@ R2 = measurement(1.0, 1e-3) result = calc_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=TEST_TOL) + expected_val = (value(alpha1) * value(R2) + value(alpha2) * value(R1)) / + (value(R1) + value(R2)) + @test isapprox(value(result), expected_val; atol = TEST_TOL) # Check uncertainty propagation (should be nonzero) @test uncertainty(result) > 0 end @@ -76,16 +75,16 @@ # Analytical result expected = (alpha1 * R2 + alpha2 * R1) / (R1 + R2) result = calc_equivalent_alpha(alpha1, R1, alpha2, R2) - @test isapprox(result, expected; atol=TEST_TOL) + @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) + @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) + @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 @@ -101,31 +100,31 @@ # 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) + @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) + @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) + @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) + @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) + @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 index f5929d57..bdb53785 100644 --- a/test/unit_BaseParams/test_calc_equivalent_eps.jl +++ b/test/unit_BaseParams/test_calc_equivalent_eps.jl @@ -1,77 +1,77 @@ @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 "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 "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 "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 "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 "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 + @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 index 5e670532..43573cb9 100644 --- a/test/unit_BaseParams/test_calc_equivalent_gmr.jl +++ b/test/unit_BaseParams/test_calc_equivalent_gmr.jl @@ -1,82 +1,82 @@ -@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 +@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 "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 "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 "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 "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 + @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 index 668ffe17..dcf8ddcc 100644 --- a/test/unit_BaseParams/test_calc_equivalent_lossfact.jl +++ b/test/unit_BaseParams/test_calc_equivalent_lossfact.jl @@ -6,14 +6,14 @@ ω = 2 * pi * 50 result = calc_equivalent_lossfact(G_eq, C_eq, ω) expected = G_eq / (ω * C_eq) - @test isapprox(result, expected; atol=TEST_TOL) + @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) + @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) @@ -31,7 +31,7 @@ # 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) + @test isapprox(r, d; atol = 1e-6) end @testset "Physical Behavior" begin @@ -53,7 +53,8 @@ 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)) + 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) diff --git a/test/unit_BaseParams/test_calc_equivalent_mu.jl b/test/unit_BaseParams/test_calc_equivalent_mu.jl index 8b1672db..2b4f371e 100644 --- a/test/unit_BaseParams/test_calc_equivalent_mu.jl +++ b/test/unit_BaseParams/test_calc_equivalent_mu.jl @@ -1,103 +1,103 @@ @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) + @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 + # 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) + @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) + # 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 + # 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 "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 "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) + @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 + # 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 "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 + @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 index 3a220314..c0679c33 100644 --- a/test/unit_BaseParams/test_calc_equivalent_rho.jl +++ b/test/unit_BaseParams/test_calc_equivalent_rho.jl @@ -3,21 +3,21 @@ # 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 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) + @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) + @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) + @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)) @@ -29,7 +29,7 @@ # 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) + @test isapprox(r, d; atol = TEST_TOL) end @testset "Physical Behavior" begin diff --git a/test/unit_BaseParams/test_calc_gmd.jl b/test/unit_BaseParams/test_calc_gmd.jl index 8a0bce49..a7f58ae7 100644 --- a/test/unit_BaseParams/test_calc_gmd.jl +++ b/test/unit_BaseParams/test_calc_gmd.jl @@ -1,105 +1,104 @@ -@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 +@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 "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 "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 "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 "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 + @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 index f32e08bc..6ac98a9e 100644 --- a/test/unit_BaseParams/test_calc_helical_params.jl +++ b/test/unit_BaseParams/test_calc_helical_params.jl @@ -1,94 +1,94 @@ @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 + 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) + # --- 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 + # 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 + # 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 + # --- 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 + # --- 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 + # --- 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 + # --- 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 index 87f3e4e9..6a675536 100644 --- a/test/unit_BaseParams/test_calc_inductance_trifoil.jl +++ b/test/unit_BaseParams/test_calc_inductance_trifoil.jl @@ -1,25 +1,24 @@ # 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, + 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) + 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...) @@ -30,30 +29,32 @@ @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,)) + 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,)) + 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) + 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,)) + 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,)) + 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...) + @test L_perfect_screen < + calc_inductance_trifoil(values(CANONICAL_POS_ARGS)...; CANONICAL_KW_ARGS...) end @testset "Type stability and promotion with Measurements.jl" begin @@ -79,10 +80,10 @@ 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 + 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 +end diff --git a/test/unit_BaseParams/test_calc_parallel_equivalent.jl b/test/unit_BaseParams/test_calc_parallel_equivalent.jl index 64bf971e..7abcefea 100644 --- a/test/unit_BaseParams/test_calc_parallel_equivalent.jl +++ b/test/unit_BaseParams/test_calc_parallel_equivalent.jl @@ -1,32 +1,31 @@ @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 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) + @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) + @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) + @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)) @@ -44,7 +43,7 @@ Z2f = 10.0 resultf = calc_parallel_equivalent(Z1f, Z2f) @test resultf isa Float64 - @test isapprox(resultf, 3.33333333; atol=TEST_TOL) + @test isapprox(resultf, 3.33333333; atol = TEST_TOL) end @testset "Physical Behavior" begin @@ -52,10 +51,11 @@ @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) + @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) + @test isapprox(calc_parallel_equivalent(8.0, 8.0), 4.0; atol = TEST_TOL) end @testset "Type Stability & Promotion" begin @@ -65,13 +65,13 @@ # 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) + @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) + @test isapprox(result_mixed_complex, expected_mixed_complex; atol = TEST_TOL) # Both Measurement -> Measurement Z1m = measurement(5.0, 0.1) @@ -90,8 +90,8 @@ 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) + @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 @@ -99,15 +99,15 @@ 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) + @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) + @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 @@ -115,9 +115,11 @@ 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) + @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 +end diff --git a/test/unit_BaseParams/test_calc_shunt_capacitance.jl b/test/unit_BaseParams/test_calc_shunt_capacitance.jl index f8d48d4c..81de87f9 100644 --- a/test/unit_BaseParams/test_calc_shunt_capacitance.jl +++ b/test/unit_BaseParams/test_calc_shunt_capacitance.jl @@ -1,78 +1,78 @@ @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 "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 "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 "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 "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 "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 + @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 index aa6b9622..0a00812c 100644 --- a/test/unit_BaseParams/test_calc_shunt_conductance.jl +++ b/test/unit_BaseParams/test_calc_shunt_conductance.jl @@ -1,78 +1,78 @@ @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 "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 "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 "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 "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 "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 + @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 index 960c77d5..a9203de7 100644 --- a/test/unit_BaseParams/test_calc_sigma_lossfact.jl +++ b/test/unit_BaseParams/test_calc_sigma_lossfact.jl @@ -1,80 +1,80 @@ @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 "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 "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 "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 "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 "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 + @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 index 245b02df..064f3908 100644 --- a/test/unit_BaseParams/test_calc_solenoid_correction.jl +++ b/test/unit_BaseParams/test_calc_solenoid_correction.jl @@ -3,7 +3,7 @@ # 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 isapprox(result, expected; atol = TEST_TOL) @test result > 1.0 # Non-helical cable (NaN turns) @@ -14,7 +14,7 @@ @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) + @test isapprox(result, 1.0; atol = TEST_TOL) # Collapsing geometry: radii nearly equal result = calc_solenoid_correction(10.0, 0.01, 0.010001) @@ -34,7 +34,7 @@ # 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) + @test isapprox(r, d; atol = TEST_TOL) end @testset "Physical Behavior" begin diff --git a/test/unit_BaseParams/test_calc_strip_resistance.jl b/test/unit_BaseParams/test_calc_strip_resistance.jl index 74827500..dad798ff 100644 --- a/test/unit_BaseParams/test_calc_strip_resistance.jl +++ b/test/unit_BaseParams/test_calc_strip_resistance.jl @@ -9,7 +9,7 @@ T0 = 20.0 Top = 25.0 R = calc_strip_resistance(thickness, width, rho, alpha, T0, Top) - @test isapprox(R, 0.00017579785649999996, atol=TEST_TOL) + @test isapprox(R, 0.00017579785649999996, atol = TEST_TOL) end # --- Edge Cases --- @@ -28,8 +28,9 @@ # --- 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) + 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 --- @@ -50,13 +51,17 @@ 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)) + 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) + 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) + 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 diff --git a/test/unit_BaseParams/test_calc_temperature_correction.jl b/test/unit_BaseParams/test_calc_temperature_correction.jl index b573a13c..2fbd15ef 100644 --- a/test/unit_BaseParams/test_calc_temperature_correction.jl +++ b/test/unit_BaseParams/test_calc_temperature_correction.jl @@ -4,29 +4,30 @@ @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) + @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) + @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) + @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) + @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) + @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)) + @test isapprox(kf, 1.2161f0, atol = Float32(1e-4)) end # Physical Behavior @@ -73,7 +74,7 @@ # σ² = (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) + @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 index 2b1de76b..71fd649e 100644 --- a/test/unit_BaseParams/test_calc_tubular_gmr.jl +++ b/test/unit_BaseParams/test_calc_tubular_gmr.jl @@ -1,104 +1,104 @@ @testitem "BaseParams: calc_tubular_gmr unit tests" setup = [defaults] begin - using Measurements: measurement, value, uncertainty + 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 "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) + @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) + # 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_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 + # 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 "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 "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 "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 + @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 index abde7c96..dd008a54 100644 --- a/test/unit_BaseParams/test_calc_tubular_inductance.jl +++ b/test/unit_BaseParams/test_calc_tubular_inductance.jl @@ -1,94 +1,93 @@ @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 + 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 + # 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 + # 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 + # 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 + # 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 + # 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 index 58ae8ce6..eb44dec9 100644 --- a/test/unit_BaseParams/test_calc_tubular_resistance.jl +++ b/test/unit_BaseParams/test_calc_tubular_resistance.jl @@ -1,124 +1,123 @@ @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 + # 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 + # 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 + # 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 + # 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 + # 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 + # 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 index 040d9920..c05ec8cf 100644 --- a/test/unit_BaseParams/test_calc_wirearray_coords.jl +++ b/test/unit_BaseParams/test_calc_wirearray_coords.jl @@ -1,166 +1,161 @@ @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 + @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 index eaf1a7b6..59efd20c 100644 --- a/test/unit_BaseParams/test_calc_wirearray_gmr.jl +++ b/test/unit_BaseParams/test_calc_wirearray_gmr.jl @@ -9,7 +9,7 @@ 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 isapprox(gmr, expected; atol = TEST_TOL) @test gmr > 0 end @@ -21,7 +21,7 @@ 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) + @test isapprox(gmr, expected; atol = TEST_TOL) # mu_r = 0 (non-magnetic) lay_rad = 0.05 @@ -30,7 +30,7 @@ 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) + @test isapprox(gmr, expected; atol = TEST_TOL) # rad_wire = 0 (degenerate wire) lay_rad = 0.05 @@ -55,7 +55,7 @@ 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 isapprox(value(gmr2), gmr1; atol = TEST_TOL) @test uncertainty(gmr2) > 0 end diff --git a/test/unit_DataModel/test_CableComponent.jl b/test/unit_DataModel/test_CableComponent.jl index 370b80af..37e5f140 100644 --- a/test/unit_DataModel/test_CableComponent.jl +++ b/test/unit_DataModel/test_CableComponent.jl @@ -1,152 +1,151 @@ -@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 +@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 index aa7ba852..77330cfe 100644 --- a/test/unit_DataModel/test_CableDesign.jl +++ b/test/unit_DataModel/test_CableDesign.jl @@ -3,176 +3,175 @@ # ------------------------- @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) + # 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 +@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 index 73b9f5f8..ad04b371 100644 --- a/test/unit_DataModel/test_CablePosition.jl +++ b/test/unit_DataModel/test_CablePosition.jl @@ -1,84 +1,84 @@ @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, - ) + 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 +@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 index ba503472..f18da8fc 100644 --- a/test/unit_DataModel/test_ConductorGroup.jl +++ b/test/unit_DataModel/test_ConductorGroup.jl @@ -1,215 +1,214 @@ @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) + # 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 +@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 index 5605cd81..b36a6b45 100644 --- a/test/unit_DataModel/test_Insulator.jl +++ b/test/unit_DataModel/test_Insulator.jl @@ -1,149 +1,145 @@ -@testitem "DataModel(Insulator): constructor unit tests" setup = - [defaults, deps_datamodel, defs_materials] begin +@testitem "DataModel(Insulator): constructor unit tests" setup = [ + defaults, deps_datamodel, defs_materials] begin + using Measurements - 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) - @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" + ) - # 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 + ) - # 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 + ) - # 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 - # 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 "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 "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 + @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 index b6236a02..faeb519c 100644 --- a/test/unit_DataModel/test_InsulatorGroup.jl +++ b/test/unit_DataModel/test_InsulatorGroup.jl @@ -1,121 +1,121 @@ @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) + 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), - ) + # 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) + # 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 +@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 index 0fa4388b..1b4793c8 100644 --- a/test/unit_DataModel/test_LineCableSystem.jl +++ b/test/unit_DataModel/test_LineCableSystem.jl @@ -1,243 +1,243 @@ @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 + 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 +@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 +@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 index 4a2c42f3..b7fbf9d1 100644 --- a/test/unit_DataModel/test_Semicon.jl +++ b/test/unit_DataModel/test_Semicon.jl @@ -1,105 +1,103 @@ -@testitem "DataModel(Semicon): constructor unit tests" setup = - [defaults, deps_datamodel, defs_materials] begin +@testitem "DataModel(Semicon): constructor unit tests" setup = [ + defaults, deps_datamodel, defs_materials] begin + using Measurements - 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) - @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" + ) - # 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) - # 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) - # 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 - # 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 "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 "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 + @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 index f7cb05d0..3bbff58f 100644 --- a/test/unit_DataModel/test_Strip.jl +++ b/test/unit_DataModel/test_Strip.jl @@ -1,179 +1,177 @@ -@testitem "DataModel(Strip): constructor unit tests" setup = - [defaults, deps_datamodel, defs_materials] begin +@testitem "DataModel(Strip): constructor unit tests" setup = [ + defaults, deps_datamodel, defs_materials] begin + using Measurements - 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 + ) - @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" + ) - # 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 + ) - # 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) - # 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 - # 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 "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 "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 + @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 index e13f54ec..479bd846 100644 --- a/test/unit_DataModel/test_Tubular.jl +++ b/test/unit_DataModel/test_Tubular.jl @@ -1,113 +1,110 @@ -@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) +@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 + # 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 + # 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 + # 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 + # 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 + # 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) + @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, 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) - # 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) + ) - # 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 + # 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 index 1de8bfda..29ce4b97 100644 --- a/test/unit_DataModel/test_WireArray.jl +++ b/test/unit_DataModel/test_WireArray.jl @@ -1,376 +1,374 @@ -@testitem "DataModel(CircStrands): constructor unit tests" setup = - [defaults, deps_datamodel, defs_materials] begin +@testitem "DataModel(CircStrands): constructor unit tests" setup = [ + defaults, deps_datamodel, defs_materials] begin + using Measurements - 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 + ) - @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" + ) - # 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 + ) - # 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 + ) - # 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 - # 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 "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 "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 + @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 index 798dc0e7..693843ef 100644 --- a/test/unit_DataModel/test_equivalent.jl +++ b/test/unit_DataModel/test_equivalent.jl @@ -1,193 +1,187 @@ @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 + # 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 +@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 index 1c0bb3b7..49a1c8e8 100644 --- a/test/unit_DataModel/test_uncertainty_covariance.jl +++ b/test/unit_DataModel/test_uncertainty_covariance.jl @@ -1,80 +1,79 @@ @testitem "Utils(coerce_to_T): Measurement covariance preservation" setup = [defaults] begin - using Measurements - import Measurements: derivative + using Measurements + import Measurements: derivative - @testset "Exact-type coercion is an identity operation" begin - x = measurement(2.0, 0.1) - y = 3x + @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 + @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}) + @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 + @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) + @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 + @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) + 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) + 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), - ) + 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)) + @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, - ) + 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 + 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 + 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 + @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 index 7c2802ef..397e9544 100644 --- a/test/unit_Engine/test_parallel_rc_insulation.jl +++ b/test/unit_Engine/test_parallel_rc_insulation.jl @@ -1,316 +1,315 @@ @testitem "Engine(ParallelRC): analytical layer and lossless limit" setup = [defaults] begin - using LinearAlgebra + using LinearAlgebra - formulation = InsulationAdmittance.ParallelRC() - lossless = InsulationAdmittance.Lossless() + 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) + 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) + 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 + @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 + # 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) + @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 + 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) + 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) + 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) + 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 + 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, - ), - ) + 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) + 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) + @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 + 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 + 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) + # 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 + # 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 + 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, - ) + 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, - ), - ) + 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, - ) + 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 + @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 + ω = 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 + @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) + 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 + 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 + 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 + 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) + 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 index 0d1e78e8..628ff233 100644 --- a/test/unit_ImportExport/test_export_data_atp.jl +++ b/test/unit_ImportExport/test_export_data_atp.jl @@ -1,181 +1,175 @@ @testsnippet deps_export_atp begin - using EzXML + 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 +@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 +@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 index c3616f46..60c2dd2e 100644 --- a/test/unit_ParametricBuilder/test_parametricbuilder.jl +++ b/test/unit_ParametricBuilder/test_parametricbuilder.jl @@ -1,388 +1,395 @@ -@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 +@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. + 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). + @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. + @test length(spec) == 1 + + probs = collect(spec) + @test length(probs) == 1 + + prob = probs[1] + + # Check system contents: 3 cables, phase mapping intact. + 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). + 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. + 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). + @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 index a94addea..60b7e3a8 100644 --- a/test/unit_ParametricBuilder/test_uq_joint_measurements.jl +++ b/test/unit_ParametricBuilder/test_uq_joint_measurements.jl @@ -1,72 +1,73 @@ @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 + 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) + 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 + for (coordinate, index) in enumerate(CartesianIndices(tensor_size)), trial in 1:ntrials - 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) ./ ω + 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 - 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) + 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) ./ ω - @test value.(joint) ≈ μ rtol = 4eps() - @test uncertainty.(joint) ≈ vec(std(X; dims = 2)) rtol = 16eps() - @test Measurements.cov(joint) ≈ empirical_covariance rtol = 64eps() + 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) - # 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]))) + @test value.(joint) ≈ μ rtol = 4eps() + @test uncertainty.(joint) ≈ vec(std(X; dims = 2)) rtol = 16eps() + @test Measurements.cov(joint) ≈ empirical_covariance rtol = 64eps() - # 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)) + # 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]))) - # 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]))) + # 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 index e317afab..ff3716e5 100644 --- a/test/unit_ParametricBuilder/test_uq_trial_sampler.jl +++ b/test/unit_ParametricBuilder/test_uq_trial_sampler.jl @@ -1,52 +1,53 @@ @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 + 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"), - ) + 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 + 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, - ) + 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 + @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 index e7f1b205..492eaa02 100644 --- a/test/unit_Validation/test_rules_tubular.jl +++ b/test/unit_Validation/test_rules_tubular.jl @@ -1,26 +1,26 @@ @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 + # 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) + 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), - ) + 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 + 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 + @test r == expected end From dc518db877d6a8a8c26a198a8ff9124f563b6613 Mon Sep 17 00:00:00 2001 From: amaurigmartins Date: Fri, 14 Aug 2026 06:52:52 +0200 Subject: [PATCH 002/157] feat(results): stabilize containers and plotting --- ext/LineCableModelsMakieExt.jl | 203 ++- src/LineCableModels.jl | 34 +- src/commons/Commons.jl | 50 +- src/datamodel/DataModel.jl | 27 +- src/datamodel/cabledesign.jl | 1 + src/datamodel/cabledesign/cableconstants.jl | 106 ++ src/datamodel/cabledesign/dataframe.jl | 70 +- src/datamodel/plotspecs.jl | 564 ++++++++ src/datamodel/preview.jl | 1261 ----------------- src/engine/Engine.jl | 18 +- src/engine/base.jl | 434 +----- src/engine/dataframe.jl | 355 ++--- src/engine/lineparams.jl | 319 +++-- src/engine/plot.jl | 797 ----------- src/engine/plotmetadata.jl | 385 ----- src/engine/plotspecs.jl | 284 ++++ src/engine/transforms/Transforms.jl | 6 +- src/engine/transforms/eiglevenberg.jl | 7 +- src/engine/transforms/fortescue.jl | 7 +- src/plotbuilder/PlotBuilder.jl | 60 +- src/plotbuilder/axisspec.jl | 82 -- src/plotbuilder/pagespec.jl | 450 ------ src/plotbuilder/parse.jl | 530 ------- src/plotbuilder/plothelpers.jl | 37 - src/plotbuilder/plotspecs.jl | 166 --- .../plotuicomponents/PlotUIComponents.jl | 819 ----------- src/plotbuilder/plotuicomponents/callbacks.jl | 1 - src/plotbuilder/seriesspec.jl | 286 ---- src/plotbuilder/traits.jl | 322 ----- src/plotbuilder/types.jl | 113 +- src/plotbuilder/uicomponents/UIComponents.jl | 478 ++++++- src/plotbuilder/uicomponents/actions.jl | 67 - src/plotbuilder/uicomponents/draw.jl | 54 - src/plotbuilder/uicomponents/layoutspecs.jl | 75 - src/plotbuilder/uicomponents/pipeline.jl | 311 ---- src/plotbuilder/uicomponents/themes.jl | 109 -- src/plotbuilder/uicomponents/types.jl | 95 -- src/plotbuilder/uicomponents/widgets.jl | 34 - src/plotbuilder/viewspec.jl | 31 - src/unithandler/UnitHandler.jl | 27 + src/uq/UQ.jl | 22 +- src/uq/dataframe.jl | 88 +- src/uq/distributions.jl | 779 +++------- src/uq/montecarlo.jl | 183 +-- src/uq/plot.jl | 694 --------- src/uq/plotspecs.jl | 259 ++++ src/uq/plotspecs/mcstatsplotspec.jl | 96 -- src/uq/types.jl | 577 ++++++-- test/datamodel.jl | 19 +- test/plotting.jl | 189 +++ test/reference/cable_preview.png | Bin 0 -> 405117 bytes test/reference/generate.jl | 110 ++ test/reference/line_measurements.png | Bin 0 -> 69100 bytes test/reference/line_rlcg.png | Bin 0 -> 67379 bytes test/reference/line_zy_cartesian.png | Bin 0 -> 67379 bytes test/reference/line_zy_polar.png | Bin 0 -> 100971 bytes test/reference/material_scale.png | Bin 0 -> 31334 bytes test/reference/mc_ecdf.png | Bin 0 -> 82175 bytes test/reference/mc_hist.png | Bin 0 -> 66038 bytes test/reference/mc_pdf.png | Bin 0 -> 77470 bytes test/reference/mc_qq.png | Bin 0 -> 78788 bytes test/reference/system_preview.png | Bin 0 -> 288287 bytes .../test_parallel_rc_insulation.jl | 60 +- test/unit_Engine/test_result_containers.jl | 368 +++++ .../test_uq_trial_sampler.jl | 2 +- 65 files changed, 4026 insertions(+), 8495 deletions(-) create mode 100644 src/datamodel/cabledesign/cableconstants.jl create mode 100644 src/datamodel/plotspecs.jl delete mode 100644 src/datamodel/preview.jl delete mode 100644 src/engine/plot.jl delete mode 100644 src/engine/plotmetadata.jl create mode 100644 src/engine/plotspecs.jl delete mode 100644 src/plotbuilder/axisspec.jl delete mode 100644 src/plotbuilder/pagespec.jl delete mode 100644 src/plotbuilder/parse.jl delete mode 100644 src/plotbuilder/plothelpers.jl delete mode 100644 src/plotbuilder/plotspecs.jl delete mode 100644 src/plotbuilder/plotuicomponents/PlotUIComponents.jl delete mode 100644 src/plotbuilder/plotuicomponents/callbacks.jl delete mode 100644 src/plotbuilder/seriesspec.jl delete mode 100644 src/plotbuilder/traits.jl delete mode 100644 src/plotbuilder/uicomponents/actions.jl delete mode 100644 src/plotbuilder/uicomponents/draw.jl delete mode 100644 src/plotbuilder/uicomponents/layoutspecs.jl delete mode 100644 src/plotbuilder/uicomponents/pipeline.jl delete mode 100644 src/plotbuilder/uicomponents/themes.jl delete mode 100644 src/plotbuilder/uicomponents/types.jl delete mode 100644 src/plotbuilder/uicomponents/widgets.jl delete mode 100644 src/plotbuilder/viewspec.jl delete mode 100644 src/uq/plot.jl create mode 100644 src/uq/plotspecs.jl delete mode 100644 src/uq/plotspecs/mcstatsplotspec.jl create mode 100644 test/plotting.jl create mode 100644 test/reference/cable_preview.png create mode 100644 test/reference/generate.jl create mode 100644 test/reference/line_measurements.png create mode 100644 test/reference/line_rlcg.png create mode 100644 test/reference/line_zy_cartesian.png create mode 100644 test/reference/line_zy_polar.png create mode 100644 test/reference/material_scale.png create mode 100644 test/reference/mc_ecdf.png create mode 100644 test/reference/mc_hist.png create mode 100644 test/reference/mc_pdf.png create mode 100644 test/reference/mc_qq.png create mode 100644 test/reference/system_preview.png create mode 100644 test/unit_Engine/test_result_containers.jl diff --git a/ext/LineCableModelsMakieExt.jl b/ext/LineCableModelsMakieExt.jl index 3152b74b..8969e22f 100644 --- a/ext/LineCableModelsMakieExt.jl +++ b/ext/LineCableModelsMakieExt.jl @@ -2,10 +2,9 @@ module LineCableModelsMakieExt using LineCableModels using Makie -using Dates: format, now const PlotBuilder = LineCableModels.PlotBuilder -const BackendHandler = LineCableModels.PlotBuilder.BackendHandler +const BackendHandler = PlotBuilder.BackendHandler function current_backend_symbol() name = nameof(Makie.current_backend()) @@ -15,65 +14,173 @@ function current_backend_symbol() return :unknown end -renderfig(fig) = display(fig) +renderfig(figure) = display(figure) include(joinpath( - @__DIR__, "..", "src", "plotbuilder", "plotuicomponents", "PlotUIComponents.jl")) -include(joinpath(@__DIR__, "..", "src", "plotbuilder", "uicomponents", "UIComponents.jl")) - -using LineCableModels.PlotBuilder: AbstractPlotSpec -using LineCableModels.PlotBuilder.BackendHandler: next_fignum -using .PlotUIComponents: ControlButtonSpec, ControlReaction, ICON_TTF, MI_REFRESH, MI_SAVE, - _make_window, _run_plot_pipeline, with_icon, with_plot_theme - -include(joinpath(@__DIR__, "..", "src", "plotbuilder", "plotspecs.jl")) + @__DIR__, + "..", + "src", + "plotbuilder", + "uicomponents", + "UIComponents.jl" +)) +using .UIComponents + +import LineCableModels.Engine: plot +import LineCableModels.DataModel: preview, show_material_scale + +function _scale_symbol(value) + value isa Symbol && return value + value === Makie.identity && return :linear + value === Makie.log10 && return :log10 + throw(ArgumentError("axis scale must be :linear, :log10, Makie.identity, or Makie.log10")) +end -module DataModelPreview +function plot( + object::LineCableModels.SeriesImpedance, + frequencies; + backend = nothing, + display_plot::Bool = true, + xscale = :linear, + yscale = :linear, + kwargs... +) + render_spec = PlotBuilder.make_render( + LineCableModels.Engine.LineParameterPlotSpec, + object; + frequencies, + xscale = _scale_symbol(xscale), + yscale = _scale_symbol(yscale), + kwargs... + ) + return UIComponents.build(render_spec; backend, display = display_plot) +end -using Makie -using Colors -using Printf -using Dates -using Statistics -using LineCableModels.DataModel -using LineCableModels.DataModel.BaseParams: calc_circstrands_coords -import LineCableModels.DataModel: AbstractCablePart, preview -using LineCableModels.Utils: is_in_testset, to_nominal -using LineCableModels.PlotBuilder.BackendHandler: current_backend_symbol, ensure_backend!, - next_fignum, renderfig -using ..PlotUIComponents: ICON_TTF, MI_REFRESH, MI_SAVE, gl_screen, with_icon +function Makie.plot(object::LineCableModels.SeriesImpedance, frequencies; kwargs...) + plot(object, frequencies; kwargs...) +end -include(joinpath(@__DIR__, "..", "src", "datamodel", "preview.jl")) +function plot( + object::LineCableModels.ShuntAdmittance, + frequencies; + backend = nothing, + display_plot::Bool = true, + xscale = :linear, + yscale = :linear, + kwargs... +) + render_spec = PlotBuilder.make_render( + LineCableModels.Engine.LineParameterPlotSpec, + object; + frequencies, + xscale = _scale_symbol(xscale), + yscale = _scale_symbol(yscale), + kwargs... + ) + return UIComponents.build(render_spec; backend, display = display_plot) +end -end # module DataModelPreview +function Makie.plot(object::LineCableModels.ShuntAdmittance, frequencies; kwargs...) + plot(object, frequencies; kwargs...) +end -module EnginePlots +function plot( + parameters::LineCableModels.LineParameters; + backend = nothing, + display_plot::Bool = true, + xscale = :linear, + yscale = :linear, + kwargs... +) + render_spec = PlotBuilder.make_render( + LineCableModels.Engine.LineParameterPlotSpec, + parameters; + xscale = _scale_symbol(xscale), + yscale = _scale_symbol(yscale), + kwargs... + ) + return UIComponents.build(render_spec; backend, display = display_plot) +end -using Makie -using Measurements: Measurements -using LineCableModels.Engine -import LineCableModels.Engine: get_description, plot -using LineCableModels.Commons: ModalDomain, PhaseDomain, domain -using LineCableModels.PlotBuilder -using ..PlotUIComponents +function Makie.plot(parameters::LineCableModels.LineParameters; kwargs...) + plot(parameters; kwargs...) +end -include(joinpath(@__DIR__, "..", "src", "engine", "plot.jl")) +function _quantity_symbol(expression) + expression isa Symbol && return expression, nothing + if expression isa Expr && expression.head === :ref && length(expression.args) == 4 + return Symbol(expression.args[1]), Tuple(Int.(expression.args[2:4])) + end + throw(ArgumentError("use a quantity Symbol and optional ijk=(i,j,k)")) +end -end # module EnginePlots +function plot( + result::Union{LineCableModels.CableConstantsMC, LineCableModels.LineParametersMC}, + expression = :R; + ijk = nothing, + backend = nothing, + display_plot::Bool = true, + kwargs... +) + quantity, parsed_indices = _quantity_symbol(expression) + selection = ijk === nothing ? parsed_indices : ijk + render_spec = PlotBuilder.make_render( + LineCableModels.UQ.MCDistributionPlotSpec, + result; + quantity, + ijk = selection, + kwargs... + ) + return only(UIComponents.build(render_spec; backend, display = display_plot)) +end -module UQPlots +function Makie.plot( + result::Union{LineCableModels.CableConstantsMC, LineCableModels.LineParametersMC}, + expression = :R; + kwargs... +) + plot(result, expression; kwargs...) +end -using Makie -using Printf -using Distributions -using StatsBase -using LineCableModels.UQ: CableDesignMC, LineParametersMC, LineParametersPDF -using LineCableModels.PlotBuilder -using ..EnginePlots -using ..PlotUIComponents +function preview( + design::LineCableModels.CableDesign; + backend = nothing, + display_plot::Bool = true, + kwargs... +) + render_spec = PlotBuilder.make_render( + LineCableModels.DataModel.CablePreviewPlotSpec, + design; + kwargs... + ) + return only(UIComponents.build(render_spec; backend, display = display_plot)) +end -include(joinpath(@__DIR__, "..", "src", "uq", "plot.jl")) +function preview( + system::LineCableModels.LineCableSystem; + backend = nothing, + display_plot::Bool = true, + kwargs... +) + render_spec = PlotBuilder.make_render( + LineCableModels.DataModel.SystemPreviewPlotSpec, + system; + kwargs... + ) + return only(UIComponents.build(render_spec; backend, display = display_plot)) +end -end # module UQPlots +function show_material_scale( + ; backend = nothing, + display_plot::Bool = true, + kwargs... +) + render_spec = PlotBuilder.make_render( + LineCableModels.DataModel.MaterialScalePlotSpec, + nothing; + kwargs... + ) + return only(UIComponents.build(render_spec; backend, display = display_plot)) +end end # module LineCableModelsMakieExt diff --git a/src/LineCableModels.jl b/src/LineCableModels.jl index d0aa3b2b..b818757b 100644 --- a/src/LineCableModels.jl +++ b/src/LineCableModels.jl @@ -4,6 +4,11 @@ module LineCableModels # ------------------------------------------------------------------------- # Core generics: export add!, set_verbosity!, set_backend! +export basis, domain, frequencies, nconductors, nfrequencies +export Z, Y, R, X, L, G, B, C +export series_impedance, shunt_admittance, + resistance, reactance, inductance, + conductance, susceptance, capacitance # Materials: export Material, MaterialsLibrary @@ -12,17 +17,17 @@ export Material, MaterialsLibrary export Thickness, Diameter, WireArray, Strip, Tubular, Semicon, Insulator, Sector, SectorParams, SectorInsulator export ConductorGroup, InsulatorGroup -export CableComponent, CableDesign, NominalData +export CableComponent, CableDesign, CableConstants, NominalData export CablesLibrary export CablePosition, LineCableSystem -export trifoil_formation, flat_formation, preview, equivalent, MaxFill +export trifoil_formation, flat_formation, preview, show_material_scale, equivalent, MaxFill # Earth properties: export EarthModel # Engine: export LineParametersProblem, - FormulationSet, compute!, SeriesImpedance, ShuntAdmittance, per_km, per_m, kronify, + FormulationSet, compute!, SeriesImpedance, ShuntAdmittance, kronify, LineParameters, PhaseDomain, ModalDomain # Parametric builder: @@ -37,7 +42,12 @@ import DocStringExtensions: DocStringExtensions # Submodule `Commons` include("commons/Commons.jl") -using .Commons: IMPORTS, EXPORTS, add!, PhaseDomain, ModalDomain, domain +using .Commons: IMPORTS, EXPORTS, add!, PhaseDomain, ModalDomain, domain, + basis, Z, Y, R, X, L, G, B, C, + series_impedance, shunt_admittance, + resistance, reactance, inductance, + conductance, susceptance, capacitance, + frequencies, nconductors, nfrequencies # Submodule `UncertainBessels` include("uncertainbessels/UncertainBessels.jl") @@ -54,6 +64,8 @@ include("validation/Validation.jl") # Submodule `PlotBuilder` include("plotbuilder/PlotBuilder.jl") using .PlotBuilder.BackendHandler: set_backend! +using .PlotBuilder: UIPlot, export_svg +export UIPlot, export_svg # Submodule `Materials` include("materials/Materials.jl") @@ -67,21 +79,31 @@ using .EarthProps: EarthModel include("datamodel/DataModel.jl") using .DataModel: Thickness, Diameter, CircStrands, RectStrands, Strip, Tubular, Semicon, Insulator, ConductorGroup, InsulatorGroup, CableComponent, CableDesign, + CableConstants, NominalData, CablesLibrary, CablePosition, LineCableSystem, trifoil_formation, flat_formation, - preview, equivalent, MaxFill, Sector, SectorParams, SectorInsulator + preview, show_material_scale, equivalent, MaxFill, Sector, SectorParams, + SectorInsulator # Submodule `Engine` include("engine/Engine.jl") using .Engine: LineParametersProblem, compute!, LineParameters, SeriesImpedance, - ShuntAdmittance, per_km, per_m, kronify, FormulationSet + ShuntAdmittance, kronify, FormulationSet # Submodule `ParametricBuilder` include("parametricbuilder/ParametricBuilder.jl") # Submodule `UQ` include("uq/UQ.jl") +using .UQ: SampleSummary, RLCG, HistogramPDF, CableConstantsMC, LineParametersMC, + sample, trial, mc, statistics, has_samples, samples, + has_distributions, distribution, surrogate, ntrials, confidence, + mean, std, quantile +export SampleSummary, RLCG, HistogramPDF, CableConstantsMC, LineParametersMC, + sample, trial, mc, statistics, has_samples, samples, + has_distributions, distribution, surrogate, ntrials, confidence, + mean, std, quantile # Submodule `ImportExport` include("importexport/ImportExport.jl") diff --git a/src/commons/Commons.jl b/src/commons/Commons.jl index f542a9d6..552ac138 100644 --- a/src/commons/Commons.jl +++ b/src/commons/Commons.jl @@ -3,12 +3,60 @@ module Commons include("docstringextension.jl") include("consts.jl") -export get_description, add!, domain, LineParamsDomain, PhaseDomain, ModalDomain +export get_description, add!, domain, basis, + Z, Y, R, X, L, G, B, C, + series_impedance, shunt_admittance, + resistance, reactance, inductance, + conductance, susceptance, capacitance, + frequencies, nconductors, nfrequencies, + LineParamsDomain, PhaseDomain, ModalDomain function get_description end function add! end +""" + basis(value) -> Symbol + +Return the physical storage basis of a result container. Supported line- +parameter values return `:per_length` for distributed quantities or `:total` +for quantities integrated over the modeled line length. +""" +function basis end + +""" + Z(parameters[, i, j[, k]]) + +Return series impedance in the canonical units selected by [`basis`](@ref). +Index selection is defined by the concrete result container. +""" +function Z end + +""" + Y(parameters[, i, j[, k]]) + +Return shunt admittance in the canonical units selected by [`basis`](@ref). +Index selection is defined by the concrete result container. +""" +function Y end +function R end +function X end +function L end +function G end +function B end +function C end +function series_impedance end +function shunt_admittance end +function resistance end +function reactance end +function inductance end +function conductance end +function susceptance end +function capacitance end +function frequencies end +function nconductors end +function nfrequencies end + abstract type LineParamsDomain end struct PhaseDomain <: LineParamsDomain end struct ModalDomain <: LineParamsDomain end diff --git a/src/datamodel/DataModel.jl b/src/datamodel/DataModel.jl index 19665b96..5445866d 100644 --- a/src/datamodel/DataModel.jl +++ b/src/datamodel/DataModel.jl @@ -23,15 +23,18 @@ 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 CableComponent, CableDesign, CableConstants # 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 preview, show_material_scale, equivalent # Module-specific dependencies using ..Commons +import ..PlotBuilder +import ..UnitHandler import ..Commons: add! +import ..Commons: basis, R, L, C, resistance, inductance, capacitance using ..Utils: resolve_T, to_certain, to_nominal, is_headless, is_in_testset, to_lower, to_upper @@ -47,7 +50,8 @@ using Measurements using DataFrames using Colors using LinearAlgebra -using GeometryBasics: Point, Point2f +using GeometryBasics: Point, Point2f, Polygon +using Statistics: mean # Abstract types & interfaces include("types.jl") include("radii.jl") @@ -88,6 +92,7 @@ include("linecablesystem.jl") include("helpers.jl") include("io.jl") include("typecoercion.jl") +include("plotspecs.jl") """ preview(object; kwargs...) @@ -106,6 +111,22 @@ function preview(args...; kwargs...) ) end +""" + show_material_scale(; kwargs...) + +Display the resistivity, permeability, and permittivity color scales used by +[`preview`](@ref). Load a Makie backend before calling this function. +""" +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 + # Aliases for backward compatibility const WireArray = CircStrands export WireArray diff --git a/src/datamodel/cabledesign.jl b/src/datamodel/cabledesign.jl index aafc3cb6..80c557bb 100644 --- a/src/datamodel/cabledesign.jl +++ b/src/datamodel/cabledesign.jl @@ -258,4 +258,5 @@ function nonsensify( end include("cabledesign/base.jl") +include("cabledesign/cableconstants.jl") include("cabledesign/dataframe.jl") diff --git a/src/datamodel/cabledesign/cableconstants.jl b/src/datamodel/cabledesign/cableconstants.jl new file mode 100644 index 00000000..fe6f215c --- /dev/null +++ b/src/datamodel/cabledesign/cableconstants.jl @@ -0,0 +1,106 @@ +""" + CableConstants{T} + +Canonical per-unit-length cable constants. + +The fields `R`, `L`, and `C` are stored in Ω/m, H/m, and F/m respectively. +Display conversions belong to `UnitHandler` and presentation adapters. +""" +struct CableConstants{T} + "Series resistance per unit length \\[Ω/m\\]." + R::T + "Series inductance per unit length \\[H/m\\]." + L::T + "Shunt capacitance per unit length \\[F/m\\]." + C::T +end + +function Base.:(==)(left::CableConstants, right::CableConstants) + left.R == right.R && left.L == right.L && left.C == right.C +end + +function CableConstants(R::Real, L::Real, C::Real) + values = promote(R, L, C) + return CableConstants{typeof(first(values))}(values...) +end + +""" + CableConstants(design::CableDesign; S=nothing, rho_e=100.0) + +Compute the scalar cable constants represented by `design`. + +# Arguments + +- `design`: Cable design whose core, shield, and outer geometry are used. +- `S`: Cable separation in metres. The outer diameter is used when omitted. +- `rho_e`: Earth resistivity in Ω·m. + +# Returns + +A [`CableConstants`](@ref) value storing `R` in Ω/m, `L` in H/m, and `C` in +F/m. + +# Notes + +The implemented expressions are the same tubular-resistance, trefoil- +inductance, and coaxial-capacitance expressions historically used by +`DataFrame(design, :baseparams)`. This constructor changes their storage units, +not their physical calculation. +""" +function CableConstants( + design::CableDesign; + S::Union{Nothing, Number} = nothing, + rho_e::Number = 100.0 +) + length(design.components) >= 2 || throw( + ArgumentError("at least two cable components are required"), + ) + + cable_core = design.components[1] + cable_shield = design.components[2] + cable_outer = design.components[end] + separation = if S === nothing + if isnan(cable_outer.insulator_group.r_ex) + 2 * cable_outer.conductor_group.r_ex + else + 2 * cable_outer.insulator_group.r_ex + end + else + S + end + + resistance_value = 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 + ) + inductance_value = 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, + separation; + rho_e = rho_e + ) + capacitance_value = calc_shunt_capacitance( + cable_core.conductor_group.r_ex, + cable_core.insulator_group.r_ex, + cable_core.insulator_props.eps_r + ) + return CableConstants(resistance_value, inductance_value, capacitance_value) +end + +R(constants::CableConstants) = constants.R +L(constants::CableConstants) = constants.L +C(constants::CableConstants) = constants.C +basis(::CableConstants) = :per_length +resistance(constants::CableConstants) = R(constants) +inductance(constants::CableConstants) = L(constants) +capacitance(constants::CableConstants) = C(constants) diff --git a/src/datamodel/cabledesign/dataframe.jl b/src/datamodel/cabledesign/dataframe.jl index 32b491fc..871a2c56 100644 --- a/src/datamodel/cabledesign/dataframe.jl +++ b/src/datamodel/cabledesign/dataframe.jl @@ -43,58 +43,10 @@ function DataFrame( 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 + constants = CableConstants(design; S, rho_e) + R_display = constants.R * 1e3 + L_display = constants.L * 1e6 + C_display = constants.C * 1e9 # Prepare nominal values from CableDesign nominals = [ @@ -104,7 +56,7 @@ function DataFrame( ] # Calculate differences - diffs = map(zip([R, L, C], nominals)) do (computed, nominal) + diffs = map(zip([R_display, L_display, C_display], nominals)) do (computed, nominal) if isnothing(nominal) return missing else @@ -115,7 +67,7 @@ function DataFrame( # Compute the comparison DataFrame data = DataFrame( parameter = ["R [Ω/km]", "L [mH/km]", "C [μF/km]"], - computed = [R, L, C], + computed = [R_display, L_display, C_display], nominal = to_nominal.(nominals) ) @@ -123,10 +75,14 @@ function DataFrame( data[!, "percent_diff"] = diffs # Handle measurement bounds if present - has_error_bounds = !(isnan(to_lower(R)) || isnan(to_upper(R))) + has_error_bounds = !(isnan(to_lower(R_display)) || isnan(to_upper(R_display))) 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)] + data[!, "lower"] = [ + to_lower(R_display), to_lower(L_display), to_lower(C_display) + ] + data[!, "upper"] = [ + to_upper(R_display), to_upper(L_display), to_upper(C_display) + ] # Add compliance column only for rows with non-nothing nominal values data[!, "in_range?"] = map(zip(data.nominal, data.lower, data.upper)) do ( diff --git a/src/datamodel/plotspecs.jl b/src/datamodel/plotspecs.jl new file mode 100644 index 00000000..050c028e --- /dev/null +++ b/src/datamodel/plotspecs.jl @@ -0,0 +1,564 @@ +struct CablePreviewPlotSpec <: PlotBuilder.AbstractPlotSpec end +struct SystemPreviewPlotSpec <: PlotBuilder.AbstractPlotSpec end +struct MaterialScalePlotSpec <: PlotBuilder.AbstractPlotSpec end + +const _RHO_MIN = 1.0e-9 +const _RHO_METAL_MAX = 1.0e-6 +const _RHO_SEMIMETAL_MAX = 1.0e-4 +const _RHO_SEMICON_MAX = 1.0e3 +const _RHO_LEAKY_MAX = 1.0e8 +const _RHO_MAX = 1.0e10 + +const _METAL_COLORS = [ + RGB(0.92, 0.90, 0.86), + RGB(0.89, 0.89, 0.89), + RGB(0.86, 0.89, 0.92), + RGB(0.70, 0.72, 0.75) +] +const _SEMIMETAL_COLORS = [RGB(0.70, 0.72, 0.75), RGB(0.80, 0.75, 0.65)] +const _SEMICON_COLORS = [RGB(1.00, 0.83, 0.40), RGB(0.85, 0.55, 0.18)] +const _LEAKY_COLORS = [RGB(0.42, 0.55, 0.15), RGB(0.13, 0.13, 0.13)] +const _INSULATOR_COLORS = [RGB(0.07, 0.07, 0.07), RGB(0.00, 0.00, 0.00)] +const _MU_COLORS = [RGB(0.20, 0.50, 0.95), RGB(0.56, 0.00, 0.91)] +const _EPS_COLORS = [RGB(0.00, 0.85, 0.70), RGB(0.00, 0.55, 0.90)] + +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) + first_color = RGB(colors[index]) + second_color = RGB(colors[index + 1]) + return RGB( + (1 - fraction) * red(first_color) + fraction * red(second_color), + (1 - fraction) * green(first_color) + fraction * green(second_color), + (1 - fraction) * blue(first_color) + fraction * blue(second_color) + ) +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 _minimum_lightness(color::RGB, minimum::Float64 = 0.07) + hsl = HSL(color) + return RGB(HSL(hsl.h, hsl.s, max(hsl.l, minimum))) +end + +function _base_material_color(resistivity::Real) + if !isfinite(resistivity) + return _INSULATOR_COLORS[end] + elseif resistivity <= _RHO_METAL_MAX + return _gradient(_METAL_COLORS, _log_fraction(max(resistivity, 1.0e-8), 1.0e-8, _RHO_METAL_MAX)) + elseif resistivity <= _RHO_SEMIMETAL_MAX + return _gradient( + _SEMIMETAL_COLORS, + _log_fraction(resistivity, _RHO_METAL_MAX, _RHO_SEMIMETAL_MAX) + ) + elseif resistivity <= _RHO_SEMICON_MAX + return _gradient( + _SEMICON_COLORS, + _log_fraction(resistivity, _RHO_SEMIMETAL_MAX, _RHO_SEMICON_MAX) + ) + elseif resistivity <= _RHO_LEAKY_MAX + return _gradient( + _LEAKY_COLORS, + _log_fraction(resistivity, _RHO_SEMICON_MAX, _RHO_LEAKY_MAX) + ) + end + return _minimum_lightness( + _gradient( + _INSULATOR_COLORS, + _log_fraction(min(resistivity, _RHO_MAX), _RHO_LEAKY_MAX, _RHO_MAX) + ), + ) +end + +function _alpha_composite(background::RGBA, foreground::RGBA) + output_alpha = alpha(foreground) + alpha(background) * (1 - alpha(foreground)) + iszero(output_alpha) && return RGBA(0, 0, 0, 0) + return RGBA( + (red(foreground) * alpha(foreground) + + red(background) * alpha(background) * (1 - alpha(foreground))) / output_alpha, + (green(foreground) * alpha(foreground) + + green(background) * alpha(background) * (1 - alpha(foreground))) / output_alpha, + (blue(foreground) * alpha(foreground) + + blue(background) * alpha(background) * (1 - alpha(foreground))) / output_alpha, + output_alpha + ) +end + +function _material_color(material; alpha_value::Real = 1.0) + resistivity = to_nominal(material.rho) + relative_permittivity = to_nominal(material.eps_r) + relative_permeability = to_nominal(material.mu_r) + base = _minimum_lightness(_base_material_color(resistivity)) + lightness = HSL(base).l + + mu_fraction = clamp(_log_fraction(max(relative_permeability, 1.0), 1.0, 300.0), 0, 1) + mu_tint = _gradient(_MU_COLORS, mu_fraction) + mu_alpha = 0.50 * mu_fraction * (0.6 + 0.4 * (1 - lightness)) + + eps_fraction = clamp(_log_fraction(max(relative_permittivity, 1.0), 1.0, 1000.0), 0, 1) + eps_tint = _gradient(_EPS_COLORS, eps_fraction) + band_weight = resistivity > _RHO_SEMICON_MAX ? 1.0 : + (resistivity > _RHO_METAL_MAX ? 0.6 : 0.35) + eps_alpha = (0.20 + 0.40 * band_weight) * eps_fraction * (0.55 + 0.45 * (1 - lightness)) + + color = _alpha_composite(RGBA(base.r, base.g, base.b, 1.0), RGBA(mu_tint.r, mu_tint.g, mu_tint.b, mu_alpha)) + color = _alpha_composite(color, RGBA(eps_tint.r, eps_tint.g, eps_tint.b, eps_alpha)) + return RGBA(red(color), green(color), blue(color), alpha_value) +end + +_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 Polygon(outer) + inner = reverse(_circle_points(inner_radius, xcenter, ycenter; count)) + return Polygon(outer, [inner]) +end + +function _radial_wedge( + inner_radius, outer_radius, width, center_angle, xcenter, ycenter; count::Int = 32) + angle_width = iszero(inner_radius) ? 0.0 : width / inner_radius + outer_angles = range(center_angle - angle_width / 2, center_angle + angle_width / 2; length = count) + inner_angles = reverse(outer_angles) + xvalues = vcat( + xcenter .+ outer_radius .* cos.(outer_angles), + xcenter .+ inner_radius .* cos.(inner_angles) + ) + yvalues = vcat( + ycenter .+ outer_radius .* sin.(outer_angles), + ycenter .+ inner_radius .* sin.(inner_angles) + ) + return Point2f.(xvalues, yvalues) +end + +function _polygon_series(geometry, label, group, color; stroke = :black, width = 0.5) + return PlotBuilder.SeriesSpec( + :polygon, + nothing, + nothing, + geometry, + label; + attributes = (; color, strokecolor = stroke, strokewidth = width, group) + ) +end + +function _layer_series!(series, layer, label, group, xcenter, ycenter; include_label = true) + color = _material_color(layer.material_props) + first_label = include_label ? label : nothing + if layer isa CircStrands + wire_radius = to_nominal(layer.radius_wire) + lay_radius = layer.num_wires == 1 ? 0.0 : to_nominal(layer.r_in) + coordinates = calc_circstrands_coords( + layer.num_wires, + wire_radius, + lay_radius; + C = (xcenter, ycenter) + ) + for (index, (xvalue, yvalue)) in enumerate(coordinates) + push!( + series, + _polygon_series( + _circle_points(wire_radius, xvalue, yvalue), + index == 1 ? first_label : nothing, + group, + color + ) + ) + end + elseif layer isa RectStrands + for index in 1:layer.num_wires + angle = (index - 1) * 2π / layer.num_wires + geometry = _radial_wedge( + to_nominal(layer.r_in), + to_nominal(layer.r_ex), + to_nominal(layer.width), + angle, + xcenter, + ycenter + ) + push!(series, _polygon_series(geometry, index == 1 ? first_label : nothing, group, color)) + end + elseif layer isa Union{Strip, Tubular, Semicon, Insulator} + geometry = _annulus_polygon( + to_nominal(layer.r_in), + to_nominal(layer.r_ex), + xcenter, + ycenter + ) + push!(series, _polygon_series( + geometry, first_label, group, color; stroke = :transparent, width = 0.0)) + elseif layer isa ConductorGroup + for (index, nested) in enumerate(layer.layers) + _layer_series!(series, nested, label, group, xcenter, ycenter; + include_label = index == 1 && include_label) + end + elseif layer isa Sector + geometry = Point2f[(vertex[1] + xcenter, vertex[2] + ycenter) + for vertex in layer.vertices] + push!(series, _polygon_series(geometry, first_label, group, color)) + elseif layer isa SectorInsulator + outer = Point2f[(vertex[1] + xcenter, vertex[2] + ycenter) + for vertex in layer.outer_vertices] + inner = reverse(Point2f[(vertex[1] + xcenter, vertex[2] + ycenter) + for vertex in layer.inner_sector.vertices]) + push!(series, _polygon_series(Polygon(outer, [inner]), first_label, group, color)) + else + @warn "unsupported cable-preview layer" layer_type = typeof(layer) + end + return series +end + +function _design_series(design, xcenter, ycenter; display_legend::Bool) + series = PlotBuilder.SeriesSpec[] + outer_radius = try + to_nominal(design.components[end].insulator_group.r_ex) + catch + NaN + end + if isfinite(outer_radius) && outer_radius > 0 + push!(series, + _polygon_series(_circle_points(outer_radius, xcenter, ycenter), nothing, + :background, :white; stroke = :transparent, width = 0.0)) + end + function preview_identity(layer, layer_name) + hasproperty(layer, :material_props) || return layer_name, Symbol(layer_name) + material = layer.material_props + rho = to_nominal(material.rho) + mu_r = to_nominal(material.mu_r) + eps_r = to_nominal(material.eps_r) + displayed_rho = isfinite(rho) ? round(Float64(rho); sigdigits = 2) : rho + label = "$layer_name ρ=$displayed_rho" + key = replace("$(layer_name)_$(rho)_$(mu_r)_$(eps_r)", r"[^0-9A-Za-z]+" => "_") + return label, Symbol(key) + end + for component in design.components + for layer in component.conductor_group.layers + layer_name = lowercase(string(nameof(typeof(layer)))) + label, group = preview_identity(layer, layer_name) + _layer_series!(series, layer, label, group, xcenter, + ycenter; include_label = display_legend) + end + for layer in component.insulator_group.layers + layer_name = lowercase(string(nameof(typeof(layer)))) + label, group = preview_identity(layer, layer_name) + _layer_series!(series, layer, label, group, xcenter, + ycenter; include_label = display_legend) + end + end + return series +end + +function _each_material(callback, design) + function visit(layer) + if layer isa ConductorGroup + foreach(visit, layer.layers) + elseif hasproperty(layer, :material_props) + callback(layer.material_props) + end + end + for component in design.components + foreach(visit, component.conductor_group.layers) + foreach(visit, component.insulator_group.layers) + end + return nothing +end + +function _property_ranges(design) + resistivities = Float64[] + permeabilities = Float64[] + permittivities = Float64[] + _each_material(design) do material + resistivity = to_nominal(material.rho) + permeability = to_nominal(material.mu_r) + permittivity = to_nominal(material.eps_r) + isfinite(resistivity) && push!(resistivities, resistivity) + isfinite(permeability) && push!(permeabilities, permeability) + isfinite(permittivity) && push!(permittivities, permittivity) + end + return ( + isempty(resistivities) ? (_RHO_MIN, _RHO_MAX) : extrema(resistivities), + isempty(permeabilities) ? (1.0, 300.0) : extrema(permeabilities), + isempty(permittivities) ? (1.0, 1000.0) : extrema(permittivities) + ) +end + +function _color_samples(function_value, lower, upper; count::Int = 256) + lower_value = max(Float64(lower), floatmin(Float64)) + upper_value = max(Float64(upper), nextfloat(lower_value)) + values = 10.0 .^ range(log10(lower_value), log10(upper_value); length = count) + return [function_value(value) for value in values] +end + +function _colorbar_specs(rho_range, mu_range, eps_range; alpha_value = 1.0) + rho_lower, rho_upper = rho_range + mu_lower, mu_upper = max.(mu_range, 1.0) + eps_lower, eps_upper = max.(eps_range, 1.0) + rho_upper == rho_lower && (rho_upper = nextfloat(rho_lower)) + mu_upper == mu_lower && (mu_upper = nextfloat(mu_lower)) + eps_upper == eps_lower && (eps_upper = nextfloat(eps_lower)) + gray = RGB(0.5, 0.5, 0.5) + dark = RGB(0.1, 0.1, 0.1) + return ( + ( + label = "ρ [Ω·m]", + colormap = _color_samples(_base_material_color, rho_lower, rho_upper), + limits = (0.0, 1.0), + ticks = ([0.0, 1.0], collect(string.((rho_lower, rho_upper)))) + ), + ( + label = "μᵣ", + colormap = _color_samples( + value -> begin + fraction = _log_fraction(value, 1.0, 300.0) + tint = _gradient(_MU_COLORS, fraction) + overlay = RGBA(tint.r, tint.g, tint.b, 0.5 * fraction) + color = _alpha_composite(RGBA(gray.r, gray.g, gray.b, 1.0), overlay) + RGBA(red(color), green(color), blue(color), alpha_value) + end, + mu_lower, + mu_upper + ), + limits = (0.0, 1.0), + ticks = ([0.0, 1.0], collect(string.((mu_lower, mu_upper)))) + ), + ( + label = "εᵣ", + colormap = _color_samples( + value -> begin + fraction = _log_fraction(value, 1.0, 1000.0) + tint = _gradient(_EPS_COLORS, fraction) + overlay = RGBA(tint.r, tint.g, tint.b, 0.6 * fraction) + color = _alpha_composite(RGBA(dark.r, dark.g, dark.b, 1.0), overlay) + RGBA(red(color), green(color), blue(color), alpha_value) + end, + eps_lower, + eps_upper + ), + limits = (0.0, 1.0), + ticks = ([0.0, 1.0], collect(string.((eps_lower, eps_upper)))) + ) + ) +end + +function _distance_axes() + quantity = UnitHandler.QuantityTag{:distance}() + unit = UnitHandler.units(:base, :meter) + return ( + PlotBuilder.AxisSpec(:x, quantity, unit, "y [m]", :linear), + PlotBuilder.AxisSpec(:y, quantity, unit, "z [m]", :linear) + ) +end + +function PlotBuilder.make_render( + ::Type{CablePreviewPlotSpec}, + design::CableDesign; + x_offset::Real = 0.0, + y_offset::Real = 0.0, + size::Tuple{Int, Int} = (900, 700), + display_legend::Bool = true, + display_id::Bool = false, + display_colorbars::Bool = true, + kwargs... +) + isempty(kwargs) || @warn "unused cable-preview keywords" keywords = keys(kwargs) + xaxis, yaxis = _distance_axes() + title = display_id ? "Cable design preview: $(design.cable_id)" : "Cable design preview" + view = PlotBuilder.ViewSpec( + xaxis, + yaxis, + nothing, + title, + _design_series(design, x_offset, y_offset; display_legend), + (; kind = :cable), + attributes = (; aspect = :data) + ) + colorbars = display_colorbars ? _colorbar_specs(_property_ranges(design)...) : () + page = PlotBuilder.PageSpec( + title, + size, + :preview, + [view], + (; + colorbars, + display_legend, + export_name = design.cable_id, + controls = PlotBuilder.control_definitions(xlog = false, ylog = false), + configuration = (; x_offset, y_offset, display_id, display_colorbars) + ) + ) + return PlotBuilder.RenderSpec(CablePreviewPlotSpec, [page]) +end + +function _system_limits(system, zoom_factor) + horizontal = Float64[to_nominal(cable.horz) for cable in system.cables] + vertical = Float64[to_nominal(cable.vert) for cable in system.cables] + radii = Float64[max( + to_nominal(last(cable.design_data.components).conductor_group.r_ex), + to_nominal(last(cable.design_data.components).insulator_group.r_ex) + ) for cable in system.cables] + 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 _earth_colorbars(earth_model) + earth_model === nothing && return () + resistivities = Float64[] + permeabilities = Float64[] + permittivities = Float64[] + for layer in earth_model.layers[2:end] + push!(resistivities, to_nominal(layer.base_rho_g)) + push!(permeabilities, to_nominal(layer.base_mur_g)) + push!(permittivities, to_nominal(layer.base_epsr_g)) + end + isempty(resistivities) && return () + return _colorbar_specs(extrema(resistivities), extrema(permeabilities), + extrema(permittivities); alpha_value = 0.25) +end + +function PlotBuilder.make_render( + ::Type{SystemPreviewPlotSpec}, + system::LineCableSystem; + earth_model = nothing, + zoom_factor = nothing, + size::Tuple{Int, Int} = (900, 700), + display_legend::Bool = true, + display_id::Bool = false, + display_colorbars::Bool = true, + kwargs... +) + isempty(kwargs) || @warn "unused system-preview keywords" keywords = keys(kwargs) + limits = _system_limits(system, zoom_factor) + xaxis, yaxis = _distance_axes() + title = display_id ? "Cable system cross-section: $(system.system_id)" : + "Cable system cross-section" + series = PlotBuilder.SeriesSpec[ + PlotBuilder.SeriesSpec( + :hline, + nothing, + [0.0], + nothing, + nothing; + attributes = (; color = :black, linewidth = 1.5, group = :air_earth) + ), + ] + if earth_model !== nothing && !earth_model.vertical_layers + cumulative_depth = 0.0 + fill_minimum = limits[2][1] - 5.0 + fill_horizontal = (limits[1][1] - 5.0, limits[1][2] + 5.0) + for (index, layer) in enumerate(earth_model.layers[2:end]) + top = cumulative_depth + bottom = if isinf(layer.t) + fill_minimum + else + cumulative_depth -= to_nominal(layer.t) + end + material = (; + rho = layer.base_rho_g, eps_r = layer.base_epsr_g, mu_r = layer.base_mur_g) + geometry = Point2f[ + (fill_horizontal[1], top), + (fill_horizontal[2], top), + (fill_horizontal[2], bottom), + (fill_horizontal[1], bottom) + ] + push!( + series, + _polygon_series( + geometry, + display_legend ? "Earth layer $index" : nothing, + Symbol("earth_$index"), + _material_color(material; alpha_value = 0.25); + stroke = :transparent, + width = 0.0 + ) + ) + end + end + for cable in system.cables + append!( + series, + _design_series( + cable.design_data, + to_nominal(cable.horz), + to_nominal(cable.vert); + display_legend = false + ) + ) + end + view = PlotBuilder.ViewSpec( + xaxis, + yaxis, + nothing, + title, + series, + (; kind = :system), + attributes = (; aspect = :data, limits) + ) + colorbars = display_colorbars ? _earth_colorbars(earth_model) : () + page = PlotBuilder.PageSpec( + title, + size, + :preview, + [view], + (; + colorbars, + display_legend, + export_name = system.system_id, + controls = PlotBuilder.control_definitions(xlog = false, ylog = false), + configuration = (; zoom_factor, display_id, display_colorbars) + ) + ) + return PlotBuilder.RenderSpec(SystemPreviewPlotSpec, [page]) +end + +function PlotBuilder.make_render( + ::Type{MaterialScalePlotSpec}, + ::Nothing = nothing; + size::Tuple{Int, Int} = (800, 400) +) + colorbars = _colorbar_specs((_RHO_MIN, _RHO_MAX), (1.0, 300.0), (1.0, 1000.0)) + page = PlotBuilder.PageSpec( + "Material property color scale", + size, + :material_scale, + PlotBuilder.ViewSpec[], + (; + colorbars, + display_legend = false, + export_name = "material_scale", + controls = PlotBuilder.control_definitions( + reset = false, + xlog = false, + ylog = false, + legend = false, + visibility = false, + zoom = false + ) + ) + ) + return PlotBuilder.RenderSpec(MaterialScalePlotSpec, [page]) +end diff --git a/src/datamodel/preview.jl b/src/datamodel/preview.jl deleted file mode 100644 index 9534694d..00000000 --- a/src/datamodel/preview.jl +++ /dev/null @@ -1,1261 +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) -function _interpolate_gradient(colors::Vector{<:Colorant}, t::Real) - 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 - -function _overlay(a::Colors.RGBA, b::Colors.RGBA) - 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/engine/Engine.jl b/src/engine/Engine.jl index 0d81e7b6..bdca385e 100644 --- a/src/engine/Engine.jl +++ b/src/engine/Engine.jl @@ -23,8 +23,13 @@ module Engine # Export public API export LineParametersProblem, - LineParameters, SeriesImpedance, ShuntAdmittance, per_km, - per_m, kronify + LineParameters, SeriesImpedance, ShuntAdmittance, + Z, Y, R, X, L, G, B, C, + series_impedance, shunt_admittance, + resistance, reactance, inductance, + conductance, susceptance, capacitance, + frequencies, nconductors, nfrequencies, basis, + kronify export EMTFormulation, FormulationSet, LineParamOptions export compute!, plot @@ -35,8 +40,15 @@ using Measurements using LinearAlgebra using ..Commons import ..Commons: get_description, LineParamsDomain, PhaseDomain, ModalDomain, domain +import ..Commons: basis, Z, Y, R, X, L, G, B, C, + series_impedance, shunt_admittance, + resistance, reactance, inductance, + conductance, susceptance, capacitance, + frequencies, nconductors, nfrequencies using ..Utils +using ..UnitHandler +using ..PlotBuilder using ..Materials using ..EarthProps: EarthModel using ..DataModel: LineCableSystem @@ -90,7 +102,7 @@ include("reduction.jl") # Override I/O methods include("base.jl") -include("plotmetadata.jl") +include("plotspecs.jl") include("dataframe.jl") """ diff --git a/src/engine/base.jl b/src/engine/base.jl index 5dbc4fa4..243a09f0 100644 --- a/src/engine/base.jl +++ b/src/engine/base.jl @@ -1,395 +1,91 @@ - 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 -function 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) +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( + D, + SeriesImpedance{T, Basis}(Array(view(lp.Z.values, :, :, selected))), + ShuntAdmittance{T, Basis}(Array(view(lp.Y.values, :, :, selected))), + selected_frequencies ) end -# --- 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 - -function _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) -end - -# 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 -function _repr_any(x, tol) - x isa Complex ? _repr_complex(x, tol) : - x isa Measurements.Measurement ? _repr_meas(x, tol) : _repr_real(x, tol) -end - -# 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 -function _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) -end - -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 +_has_uncertainty_type(::Type{Complex{S}}) where {S} = S <: Measurement +_has_uncertainty_type(::Type) = false - k < nf && print(io, "\n", "---"^10, "\n") - end +@inline function _basis_units(value, per_length_unit, total_unit) + return basis(value) === :per_length ? per_length_unit : total_unit 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( +function Base.show(io::IO, value::SeriesImpedance) + print( io, - "\n… (", - size(Z, 3) - 1, - " more slice", - size(Z, 3) - 1 == 1 ? "" : "s", - ")" + "SeriesImpedance ", + join(size(value), '×'), + " [", + _basis_units(value, "Ω/m", "Ω"), + "]" ) 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( +function Base.show(io::IO, value::ShuntAdmittance) + print( io, - "\n… (", - size(Y, 3) - 1, - " more slice", - size(Y, 3) - 1 == 1 ? "" : "s", - ")" + "ShuntAdmittance ", + join(size(value), '×'), + " [", + _basis_units(value, "S/m", "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, " (±)") + element_type = eltype(lp) + print( + io, + "LineParameters{$element_type} ", + join(size(lp.Z), '×'), + " [", + basis(lp), + "]" + ) + _has_uncertainty_type(element_type) && 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 + impedance_unit = _basis_units(lp, "Ω/m", "Ω") + admittance_unit = _basis_units(lp, "S/m", "S") + println(io, join(size(lp.Z), '×'), " LineParameters [", basis(lp), "]") + println(io, "domain: ", nameof(domain(lp)), ", frequencies: ", nfrequencies(lp)) + println(io, "Z [$impedance_unit], first frequency slice:") + show(io, MIME"text/plain"(), view(lp.Z.values, :, :, 1)) + print(io, "\nY [$admittance_unit], first frequency slice:\n") + show(io, MIME"text/plain"(), view(lp.Y.values, :, :, 1)) end diff --git a/src/engine/dataframe.jl b/src/engine/dataframe.jl index 195890ba..6920f9f9 100644 --- a/src/engine/dataframe.jl +++ b/src/engine/dataframe.jl @@ -2,291 +2,176 @@ 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) +function _frequency_vector(object, provided) + if provided === nothing + return Float64.(collect(axes(object, 3))) end + values = Float64.(collect(provided)) + length(values) == size(object, 3) || throw( + DimensionMismatch("frequency vector length does not match the parameter depth"), + ) + all(isfinite, values) || throw(ArgumentError("frequencies must be finite")) + return values end -function _normalize_quantity_units(units) - return normalize_quantity_units(units) +function _dataframe_components(object, mode::Symbol, coord::Symbol) + mode in (:RLCG, :ZY) || throw(ArgumentError("mode must be :RLCG or :ZY")) + coord in (:cart, :polar) || throw(ArgumentError("coord must be :cart or :polar")) + if mode === :RLCG + object isa SeriesImpedance && return (:R, :L) + object isa ShuntAdmittance && return (:G, :C) + elseif object isa SeriesImpedance + return coord === :cart ? (:Z_re, :Z_im) : (:Z_abs, :Z_angle) + elseif object isa ShuntAdmittance + return coord === :cart ? (:Y_re, :Y_im) : (:Y_abs, :Y_angle) + end + throw(ArgumentError("unsupported line-parameter object $(typeof(object))")) 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 +const _DATAFRAME_COLUMN = Dict( + :R => :R, + :L => :L, + :G => :G, + :C => :C, + :Z_re => :real, + :Z_im => :imag, + :Z_abs => :magnitude, + :Z_angle => :angle, + :Y_re => :real, + :Y_im => :imag, + :Y_abs => :magnitude, + :Y_angle => :angle +) + +function _clip_field(value::Real, tolerance) + isfinite(value) || return value + return abs(value) <= tolerance ? zero(value) : value 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) +function _clip_field(value::Measurements.Measurement, tolerance) + nominal = abs(Measurements.value(value)) <= tolerance ? 0.0 : Measurements.value(value) + uncertainty_value = abs(Measurements.uncertainty(value)) <= tolerance ? 0.0 : + Measurements.uncertainty(value) + return Measurements.measurement(nominal, uncertainty_value) 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), +_clip_field(value, _) = value + +function _dataframe_unit_label(component, object_basis, length_unit, quantity_units) + quantity, target, factor = _component_unit( + component, + object_basis, + length_unit, + quantity_units ) - 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 + return quantity, target, factor, UnitHandler.get_label(target) end function _matrix_dataframes( - obj, - freq_raw::Vector{<:Real}, - comps::Vector{ComponentMetadata}, - units::Dict{Symbol, Symbol}, - length_unit::Symbol, - freq_unit::Symbol, - tol::Real + object, + frequency_values; + mode, + coord, + frequency_unit, + length_unit, + quantity_units, + tolerance ) - nx, ny, _ = size(obj.values) - result = Matrix{DataFrame}(undef, nx, ny) - for i in 1:nx, j in 1:ny + frequency_quantity = UnitHandler.QuantityTag{:freq}() + frequency_target = UnitHandler.units(frequency_unit, :hertz) + frequency_factor = UnitHandler.scale_factor( + UnitHandler.default_unit(frequency_quantity), + frequency_target + ) + displayed_frequency = frequency_values .* frequency_factor + component_names = _dataframe_components(object, mode, coord) + component_arrays = Dict( + component => _component_values(component, object, frequency_values) + for + component in component_names + ) + row_count, column_count, _ = size(object) + frames = Matrix{DataFrame}(undef, row_count, column_count) - slice = @view obj.values[i, j, :] - result[i, j] = _build_dataframe( - slice, - freq_raw, - comps, - units, - length_unit, - freq_unit, - tol + for row in 1:row_count, column in 1:column_count + + frame = DataFrame(_LP_FREQ_COL => displayed_frequency) + unit_map = Dict{Symbol, String}( + _LP_FREQ_COL => UnitHandler.get_label(frequency_target), ) + for component in component_names + _, target, factor, unit_label = _dataframe_unit_label( + component, + basis(object), + length_unit, + quantity_units + ) + values = collect(view(component_arrays[component], row, column, :)) .* factor + column_name = _DATAFRAME_COLUMN[component] + frame[!, column_name] = _clip_field.(values, tolerance) + unit_map[column_name] = unit_label + end + metadata!(frame, "units", unit_map, style = :note) + frames[row, column] = frame end - return result + return frames 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))) + DataFrame(parameters::Union{SeriesImpedance,ShuntAdmittance}; kwargs...) -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. +Convert each matrix entry to a frequency-indexed `DataFrame`. The result is an +`n × n` matrix of frames. `mode=:RLCG` returns physical components and +`mode=:ZY` returns Cartesian or polar components selected by `coord`. -- `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. +Container [`basis`](@ref) determines whether units are per length or total. """ function DataFrame( - Z::SeriesImpedance; + parameters::Union{SeriesImpedance, 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 + tol::Real = sqrt(eps(Float64)) ) - freq_raw = _frequency_vector(Z, freqs) - units = _normalize_quantity_units(quantity_units) - comps = components_for(Z, mode, coord; per_length = per_length) + frequency_values = _frequency_vector(parameters, freqs) return _matrix_dataframes( - Z, - freq_raw, - comps, - units, + parameters, + frequency_values; + mode, + coord, + frequency_unit = freq_unit, length_unit, - freq_unit, - float(tol) + quantity_units, + tolerance = 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. + DataFrame(parameters::LineParameters; kwargs...) -Keyword arguments mirror those of `DataFrame(::SeriesImpedance)`. +Return `(series, shunt)`, two matrices of frequency-indexed `DataFrame`s, +using the frequencies, basis, and domain stored by `parameters`. """ function DataFrame( - Y::ShuntAdmittance; - freqs = nothing, + parameters::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 + tol::Real = sqrt(eps(Float64)) ) - 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, + common = ( + freqs = frequencies(parameters), mode = mode, coord = coord, freq_unit = freq_unit, length_unit = length_unit, quantity_units = quantity_units, - tol = tol, - per_length = per_length + tol = tol ) - - 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 + return DataFrame(parameters.Z; common...), DataFrame(parameters.Y; common...) end diff --git a/src/engine/lineparams.jl b/src/engine/lineparams.jl index 34e8d62e..37c5128b 100644 --- a/src/engine/lineparams.jl +++ b/src/engine/lineparams.jl @@ -1,119 +1,270 @@ -struct SeriesImpedance{T} <: AbstractArray{T, 3} - values::Array{T, 3} # n×n×nfreq, units: Ω/m -end +const LINE_PARAMETER_BASES = (:per_length, :total) -struct ShuntAdmittance{T} <: AbstractArray{T, 3} - values::Array{T, 3} # n×n×nfreq, units: S/m +@inline function _check_basis(value::Symbol) + value in LINE_PARAMETER_BASES || throw( + ArgumentError("basis must be :per_length or :total; got :$value"), + ) + return value end """ -$(TYPEDEF) + SeriesImpedance{T, Basis} -Represents the frequency-dependent line parameters (series impedance and shunt admittance matrices) for a cable or line system. +Store a square series-impedance matrix over frequency. Values use \\[Ω/m\\] +when `Basis` is `:per_length` 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} +end -$(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} + ShuntAdmittance{T, Basis} - @doc """ - $(TYPEDSIGNATURES) +Store a square shunt-admittance matrix over frequency. Values use \\[S/m\\] +when `Basis` is `:per_length` 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} +end - Constructs a [`LineParameters`](@ref) instance. +function SeriesImpedance(A::AbstractArray{T, 3}; basis::Symbol = :per_length) where {T} + _check_basis(basis) + return SeriesImpedance{T, basis}(Array(A)) +end - # Arguments +function ShuntAdmittance(A::AbstractArray{T, 3}; basis::Symbol = :per_length) where {T} + _check_basis(basis) + return ShuntAdmittance{T, basis}(Array(A)) +end - - `Z`: Series impedance matrices \\[Ω/m\\]. - - `Y`: Shunt admittance matrices \\[S/m\\]. - - `f`: Frequencies \\[Hz\\]. +@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 - # Returns +""" + LineParameters{T, U, D, Basis} - - A [`LineParameters`](@ref) object with prelocated impedance and admittance matrices for a given frequency range. +Frequency-dependent series-impedance and shunt-admittance matrices. - # Examples +`Basis` is either `:per_length` or `:total`. Per-length values are stored in +Ω/m and S/m; total values are stored in Ω and S. Frequencies are stored in Hz. +""" +struct LineParameters{ + T <: COMPLEXSCALAR, + U <: REALSCALAR, + D <: LineParamsDomain, + Basis +} + "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} - ```julia - params = $(FUNCTIONNAME)(Z, Y, f) - ``` - """ function LineParameters( ::Type{D}, - Z::SeriesImpedance{T}, - Y::ShuntAdmittance{T}, + Z::SeriesImpedance{T, Basis}, + Y::ShuntAdmittance{T, Basis}, f::AbstractVector{U} - ) where {D <: LineParamsDomain, T <: COMPLEXSCALAR, U <: REALSCALAR} + ) where { + D <: LineParamsDomain, + T <: COMPLEXSCALAR, + U <: REALSCALAR, + Basis + } + _check_basis(Basis) 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)) + size(Z) == size(Y) || throw( + DimensionMismatch("Z and Y must have equal n×n×nfreq dimensions"), + ) + size(Z, 3) == length(f) || throw( + DimensionMismatch("frequency count must match the Z/Y third dimension"), + ) + all(isfinite, f) || throw(ArgumentError("frequencies must be finite")) + return new{T, U, D, Basis}(Z, Y, Vector{U}(f)) end +end - # Backward-compatible constructor: defaults to PhaseDomain - function LineParameters( - Z::SeriesImpedance{T}, - Y::ShuntAdmittance{T}, - f::AbstractVector{U} - ) where {T <: COMPLEXSCALAR, U <: REALSCALAR} - LineParameters(PhaseDomain, Z, Y, f) - end +function LineParameters( + Z::SeriesImpedance{T, Basis}, + Y::ShuntAdmittance{T, Basis}, + f::AbstractVector{U} +) where {T <: COMPLEXSCALAR, U <: REALSCALAR, Basis} + return LineParameters(PhaseDomain, Z, Y, f) +end + +function LineParameters( + Z::SeriesImpedance{TZ, ZBasis}, + Y::ShuntAdmittance{TY, YBasis}, + f::AbstractVector{U} +) where { + TZ <: COMPLEXSCALAR, + TY <: COMPLEXSCALAR, + U <: REALSCALAR, + 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( + ::Type{D}, + Z::AbstractArray{TZ, 3}, + Y::AbstractArray{TY, 3}, + f::AbstractVector{U}; + basis::Symbol = :per_length +) where { + D <: LineParamsDomain, + TZ <: COMPLEXSCALAR, + TY <: COMPLEXSCALAR, + U <: REALSCALAR +} + _check_basis(basis) + element_type = promote_type(TZ, TY) + return LineParameters( + D, + SeriesImpedance(convert(Array{element_type, 3}, Z); basis), + ShuntAdmittance(convert(Array{element_type, 3}, Y); basis), + f + ) end -SeriesImpedance(A::AbstractArray{T, 3}) where {T} = SeriesImpedance{T}(Array(A)) -ShuntAdmittance(A::AbstractArray{T, 3}) where {T} = ShuntAdmittance{T}(Array(A)) +function LineParameters( + Z::AbstractArray{TZ, 3}, + Y::AbstractArray{TY, 3}, + f::AbstractVector{U}; + basis::Symbol = :per_length +) where { + TZ <: COMPLEXSCALAR, + TY <: COMPLEXSCALAR, + U <: REALSCALAR +} + return LineParameters(PhaseDomain, Z, Y, f; basis) +end -# --- Outer convenience constructors ------------------------------------------- +@inline domain(::Type{<:LineParameters{T, U, D}}) where {T, U, D <: LineParamsDomain} = 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 +frequencies(lp::LineParameters) = lp.f +nconductors(lp::LineParameters) = size(lp.Z, 1) +nfrequencies(lp::LineParameters) = length(lp.f) + +series_impedance(lp::LineParameters) = lp.Z +shunt_admittance(lp::LineParameters) = lp.Y +series_impedance(impedance::SeriesImpedance) = impedance +shunt_admittance(admittance::ShuntAdmittance) = admittance """ -$(TYPEDSIGNATURES) + Z(parameters[, i, j[, k]]) + Y(parameters[, i, j[, k]]) -Construct from 3D arrays and frequency vector. Arrays are wrapped -into `SeriesImpedance` and `ShuntAdmittance` automatically. +Return series impedance or shunt admittance. With `(i, j)`, return the complete +frequency response at that matrix position. `k` may be one index, a range, or +`:`. Stored units follow [`basis`](@ref): \\[Ω/m\\] and \\[S/m\\] for +`:per_length`, or \\[Ω\\] and \\[S\\] for `:total`. """ -function 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) +Z(lp::LineParameters) = lp.Z +Y(lp::LineParameters) = lp.Y + +Z(impedance::SeriesImpedance) = impedance.values +Z(impedance::SeriesImpedance, i, j) = view(impedance.values, i, j, :) +Z(impedance::SeriesImpedance, i, j, k) = impedance.values[i, j, k] +Y(admittance::ShuntAdmittance) = admittance.values +Y(admittance::ShuntAdmittance, i, j) = view(admittance.values, i, j, :) +Y(admittance::ShuntAdmittance, i, j, k) = admittance.values[i, j, k] + +R(impedance::SeriesImpedance, args...) = real.(Z(impedance, args...)) +X(impedance::SeriesImpedance, args...) = imag.(Z(impedance, args...)) +G(admittance::ShuntAdmittance, args...) = real.(Y(admittance, args...)) +B(admittance::ShuntAdmittance, args...) = imag.(Y(admittance, args...)) +resistance(impedance::SeriesImpedance, args...) = R(impedance, args...) +reactance(impedance::SeriesImpedance, args...) = X(impedance, args...) +conductance(admittance::ShuntAdmittance, args...) = G(admittance, args...) +susceptance(admittance::ShuntAdmittance, args...) = B(admittance, args...) + +@inline Z(lp::LineParameters, i, j) = view(lp.Z.values, i, j, :) +@inline Y(lp::LineParameters, i, j) = view(lp.Y.values, i, j, :) +@inline Z(lp::LineParameters, i, j, k) = lp.Z.values[i, j, k] +@inline Y(lp::LineParameters, i, j, k) = lp.Y.values[i, j, k] + +R(lp::LineParameters, args...) = real.(Z(lp, args...)) +X(lp::LineParameters, args...) = imag.(Z(lp, args...)) +G(lp::LineParameters, args...) = real.(Y(lp, args...)) +B(lp::LineParameters, args...) = imag.(Y(lp, 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 -# Backward-compatible constructor: defaults to PhaseDomain -function LineParameters( - Z::AbstractArray{Tc, 3}, - Y::AbstractArray{Tc, 3}, - f::AbstractVector{U} -) where {Tc <: COMPLEXSCALAR, U <: REALSCALAR} - LineParameters(PhaseDomain, Z, Y, f) +""" + L(parameters[, i, j[, k]]) + +Return series inductance using the same frequency-selection grammar as +[`Z`](@ref). Units are \\[H/m\\] for `:per_length` and \\[H\\] for `:total`. + +# Notes + +```math +L(f) = \\frac{\\operatorname{Im} Z(f)}{2\\pi f}. +``` + +# Errors + +Throws `DomainError` when a selected frequency is zero. +""" +function L(lp::LineParameters) + 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) - -# 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. + C(parameters[, i, j[, k]]) + +Return shunt capacitance using the same frequency-selection grammar as +[`Y`](@ref). Units are \\[F/m\\] for `:per_length` and \\[F\\] for `:total`. + +# Notes + +```math +C(f) = \\frac{\\operatorname{Im} Y(f)}{2\\pi f}. +``` + +# Errors + +Throws `DomainError` when a selected frequency is zero. """ -@inline domain(::Type{<:LineParameters{T, U, D}}) where {T, U, D <: LineParamsDomain} = D -@inline domain(lp::LineParameters) = domain(typeof(lp)) +function C(lp::LineParameters) + 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 + +L(lp::LineParameters, i, j) = L(lp, i, j, :) +C(lp::LineParameters, i, j) = C(lp, i, j, :) +L(lp::LineParameters, i, j, k) = imag.(Z(lp, i, j, k)) ./ _angular_frequencies(lp, k) +C(lp::LineParameters, i, j, k) = imag.(Y(lp, i, j, k)) ./ _angular_frequencies(lp, k) + +resistance(lp::LineParameters, args...) = R(lp, args...) +reactance(lp::LineParameters, args...) = X(lp, args...) +inductance(lp::LineParameters, args...) = L(lp, args...) +conductance(lp::LineParameters, args...) = G(lp, args...) +susceptance(lp::LineParameters, args...) = B(lp, args...) +capacitance(lp::LineParameters, args...) = C(lp, args...) diff --git a/src/engine/plot.jl b/src/engine/plot.jl deleted file mode 100644 index a55bcab0..00000000 --- a/src/engine/plot.jl +++ /dev/null @@ -1,797 +0,0 @@ - -using Makie -import LineCableModels.Engine: plot - -include("../plotbuilder/plothelpers.jl") - -using Measurements: Measurements - -const _ICON_FN = (icon; text = nothing, kwargs...) -> with_icon( - icon; text = text === nothing ? "" : text, kwargs...) - -using LineCableModels.Engine: LP_FIG_SIZE, UnitSpec, ComponentMetadata, - get_description, get_symbol, get_unit_symbol, parent_kind, - metric_exponent, - prefix_symbol, quantity_scale, length_scale, frequency_scale, - unit_text, - length_unit_text, composite_unit, frequency_axis_label, - normalize_quantity_units, - resolve_quantity_prefix, resolve_conductors, collect_indices, - components_for, - component_values, reactance_to_l, reactance_to_c, legend_label - -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 - -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/plotmetadata.jl b/src/engine/plotmetadata.jl deleted file mode 100644 index 90f7e027..00000000 --- a/src/engine/plotmetadata.jl +++ /dev/null @@ -1,385 +0,0 @@ -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 - -function get_description(::SeriesImpedance) - ( - impedance = "Series impedance", - resistance = "Series resistance", - inductance = "Series inductance" - ) -end - -get_symbol(::SeriesImpedance) = ( - impedance = "Z", - resistance = "R", - inductance = "L" -) - -get_unit_symbol(::SeriesImpedance) = ( - impedance = "Ω", - resistance = "Ω", - inductance = "H" -) - -function get_description(::ShuntAdmittance) - ( - admittance = "Shunt admittance", - conductance = "Shunt conductance", - capacitance = "Shunt capacitance" - ) -end - -get_symbol(::ShuntAdmittance) = ( - admittance = "Y", - conductance = "G", - capacitance = "C" -) - -function get_unit_symbol(::ShuntAdmittance) - ( - admittance = "S", - conductance = "S", - capacitance = "F" - ) -end - -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 diff --git a/src/engine/plotspecs.jl b/src/engine/plotspecs.jl new file mode 100644 index 00000000..67865a51 --- /dev/null +++ b/src/engine/plotspecs.jl @@ -0,0 +1,284 @@ +struct LineParameterPlotSpec <: PlotBuilder.AbstractPlotSpec end + +const _PLOT_QUANTITY = Dict( + :R => (:resistance, :ohm, :base), + :X => (:reactance, :ohm, :base), + :L => (:inductance, :henry, :milli), + :G => (:conductance, :siemens, :base), + :B => (:susceptance, :siemens, :base), + :C => (:capacitance, :farad, :micro), + :Z_re => (:resistance, :ohm, :base), + :Z_im => (:reactance, :ohm, :base), + :Z_abs => ((:impedance, :abs), :ohm, :base), + :Z_angle => ((:impedance, :angle), :degree, :base), + :Y_re => (:conductance, :siemens, :base), + :Y_im => (:susceptance, :siemens, :base), + :Y_abs => ((:admittance, :abs), :siemens, :base), + :Y_angle => ((:admittance, :angle), :degree, :base) +) + +function _quantity_prefix(quantity_units, component::Symbol, semantic, fallback::Symbol) + quantity_units === nothing && return fallback + quantity_units isa Symbol && return quantity_units + if quantity_units isa NamedTuple || quantity_units isa AbstractDict + haskey(quantity_units, component) && return quantity_units[component] + semantic isa Symbol && haskey(quantity_units, semantic) && + return quantity_units[semantic] + return fallback + end + throw(ArgumentError("quantity_units must be a Symbol, NamedTuple, dictionary, or nothing")) +end + +function _component_unit( + component::Symbol, + parameter_basis::Symbol, + length_unit::Symbol, + quantity_units +) + semantic, unit_name, fallback_prefix = _PLOT_QUANTITY[component] + tag = UnitHandler.QuantityTag{semantic}() + prefix = _quantity_prefix(quantity_units, component, semantic, fallback_prefix) + native = UnitHandler.default_unit(tag, parameter_basis) + target = if unit_name === :degree + UnitHandler.units(prefix, unit_name) + elseif parameter_basis === :per_length + UnitHandler.units(prefix, unit_name; per = (length_unit, :meter)) + else + UnitHandler.units(prefix, unit_name) + end + return tag, target, UnitHandler.scale_factor(native, target) +end + +function _indices(selector, count::Int) + selector === nothing && return collect(1:count) + selector isa Colon && return collect(1:count) + selector isa Integer && return [Int(selector)] + selector isa AbstractRange && return collect(Int, selector) + selector isa AbstractVector && return collect(Int, selector) + throw(ArgumentError("conductor selections must be integers, ranges, vectors, `:`, or nothing")) +end + +function _conductor_pairs(object, selector) + row_count, column_count, _ = size(object) + row_selector, column_selector = selector === nothing ? (nothing, nothing) : selector + rows = _indices(row_selector, row_count) + columns = _indices(column_selector, column_count) + all(index -> index in 1:row_count, rows) || throw(BoundsError(object, rows)) + all(index -> index in 1:column_count, columns) || throw(BoundsError(object, columns)) + return [(i, j) for i in rows for j in columns] +end + +function _components(object, mode::Symbol, coord::Symbol) + mode in (:ZY, :RLCG) || throw(ArgumentError("mode must be :ZY or :RLCG")) + coord in (:cart, :polar) || throw(ArgumentError("coord must be :cart or :polar")) + if mode === :RLCG + object isa SeriesImpedance && return (:R, :L) + object isa ShuntAdmittance && return (:G, :C) + elseif object isa SeriesImpedance + return coord === :cart ? (:Z_re, :Z_im) : (:Z_abs, :Z_angle) + elseif object isa ShuntAdmittance + return coord === :cart ? (:Y_re, :Y_im) : (:Y_abs, :Y_angle) + end + throw(ArgumentError("unsupported line-parameter plot object $(typeof(object))")) +end + +function _component_values(component::Symbol, object, frequency_values) + values = object.values + component in (:R, :Z_re) && return real.(values) + component in (:X, :Z_im) && return imag.(values) + component === :L && return imag.(values) ./ reshape(2π .* frequency_values, 1, 1, :) + component in (:G, :Y_re) && return real.(values) + component in (:B, :Y_im) && return imag.(values) + component === :C && return imag.(values) ./ reshape(2π .* frequency_values, 1, 1, :) + component in (:Z_abs, :Y_abs) && return abs.(values) + component in (:Z_angle, :Y_angle) && return angle.(values) .* (180 / π) + throw(ArgumentError("unsupported line-parameter component :$component")) +end + +function _finite_exponent(curves) + maximum_value = 0.0 + for curve in curves, sample in curve + + nominal = abs(Measurements.value(sample)) + isfinite(nominal) && (maximum_value = max(maximum_value, nominal)) + end + iszero(maximum_value) && return 0 + exponent = floor(Int, log10(maximum_value)) + return abs(exponent) < 3 ? 0 : exponent +end + +function _axis_label(quantity, unit, exponent::Int) + label = UnitHandler.get_label(quantity) + unit_label = UnitHandler.get_label(unit) + base = isempty(unit_label) ? label : "$label [$unit_label]" + return exponent == 0 ? base : "$base × 10^$exponent" +end + +function _line_pages( + object, + frequency_values; + mode::Symbol, + coord::Symbol, + freq_unit::Symbol, + length_unit::Symbol, + quantity_units, + con, + fig_size::Tuple{Int, Int}, + xscale::Symbol, + yscale::Symbol +) + length(frequency_values) > 1 || return PlotBuilder.PageSpec[] + size(object, 3) == length(frequency_values) || throw( + DimensionMismatch("frequency count does not match line-parameter samples"), + ) + any(iszero, frequency_values) && mode === :RLCG && + throw( + DomainError(frequency_values, "RLCG plotting is undefined at zero frequency"), + ) + xscale in (:linear, :log10) || throw(ArgumentError("xscale must be :linear or :log10")) + yscale in (:linear, :log10) || throw(ArgumentError("yscale must be :linear or :log10")) + + frequency_quantity = UnitHandler.QuantityTag{:freq}() + frequency_target = UnitHandler.units(freq_unit, :hertz) + scaled_frequency = frequency_values .* UnitHandler.scale_factor( + UnitHandler.default_unit(frequency_quantity), + frequency_target + ) + x_exponent = _finite_exponent((scaled_frequency,)) + displayed_frequency = scaled_frequency ./ 10.0^x_exponent + pairs = _conductor_pairs(object, con) + pages = PlotBuilder.PageSpec[] + + for component in _components(object, mode, coord) + quantity, target_unit, conversion = _component_unit( + component, + basis(object), + length_unit, + quantity_units + ) + values = _component_values(component, object, frequency_values) + curves = [collect(view(values, i, j, :)) .* conversion for (i, j) in pairs] + active = [index + for index in eachindex(curves) + if + any(value -> abs(Measurements.value(value)) > eps(Float64), curves[index])] + active_curves = curves[active] + active_pairs = pairs[active] + y_exponent = _finite_exponent(active_curves) + series = PlotBuilder.SeriesSpec[] + symbol = UnitHandler.get_symbol(quantity) + for (curve, (i, j)) in zip(active_curves, active_pairs) + push!( + series, + PlotBuilder.SeriesSpec( + :line, + displayed_frequency, + curve ./ 10.0^y_exponent, + nothing, + "$symbol[$i,$j]"; + attributes = (; linewidth = 2) + ) + ) + end + title = UnitHandler.get_label(quantity) + xaxis = PlotBuilder.AxisSpec( + :x, + frequency_quantity, + frequency_target, + _axis_label(frequency_quantity, frequency_target, x_exponent), + xscale + ) + yaxis = PlotBuilder.AxisSpec( + :y, + quantity, + target_unit, + _axis_label(quantity, target_unit, y_exponent), + yscale + ) + view_spec = PlotBuilder.ViewSpec( + xaxis, + yaxis, + nothing, + title, + series, + (; component) + ) + push!( + pages, + PlotBuilder.PageSpec( + title, + fig_size, + :single, + PlotBuilder.ViewSpec[view_spec], + (; + component, + x_exponent, + y_exponent, + controls = PlotBuilder.control_definitions(), + configuration = (; + mode, + coord, + freq_unit, + length_unit, + quantity_units, + conductors = con + ) + ) + ) + ) + end + return pages +end + +function PlotBuilder.make_render( + ::Type{LineParameterPlotSpec}, + object::Union{SeriesImpedance, ShuntAdmittance}; + frequencies, + mode::Symbol = :ZY, + coord::Symbol = :cart, + freq_unit::Symbol = :base, + length_unit::Symbol = :kilo, + quantity_units = nothing, + con = nothing, + fig_size::Tuple{Int, Int} = (800, 400), + xscale::Symbol = :linear, + yscale::Symbol = :linear +) + pages = _line_pages( + object, + collect(frequencies); + mode, + coord, + freq_unit, + length_unit, + quantity_units, + con, + fig_size, + xscale, + yscale + ) + return PlotBuilder.RenderSpec(LineParameterPlotSpec, pages) +end + +function PlotBuilder.make_render( + ::Type{LineParameterPlotSpec}, + parameters::LineParameters; + kwargs... +) + impedance = PlotBuilder.make_render( + LineParameterPlotSpec, + parameters.Z; + frequencies = parameters.f, + kwargs... + ) + admittance = PlotBuilder.make_render( + LineParameterPlotSpec, + parameters.Y; + frequencies = parameters.f, + kwargs... + ) + return PlotBuilder.RenderSpec( + LineParameterPlotSpec, + vcat(impedance.figures, admittance.figures) + ) +end diff --git a/src/engine/transforms/Transforms.jl b/src/engine/transforms/Transforms.jl index 9b2f384b..835a9bdf 100644 --- a/src/engine/transforms/Transforms.jl +++ b/src/engine/transforms/Transforms.jl @@ -14,6 +14,7 @@ export Fortescue # Module-specific dependencies using ...Commons import ...Commons: get_description, PhaseDomain, ModalDomain +import ...Commons: basis import ...Utils: symtrans, symtrans!, offdiag_ratio, to_nominal import ..Engine: AbstractTransformFormulation, LineParameters, SeriesImpedance, @@ -29,8 +30,9 @@ include("fortescue.jl") include("eiglevenberg.jl") function (F::AbstractTransformFormulation)( - lp::LineParameters{Tc, U, ModalDomain}, -) where {Tc <: COMPLEXSCALAR, U <: REALSCALAR} + lp::LineParameters{ + Tc, U, ModalDomain, Basis}, +) where {Tc <: COMPLEXSCALAR, U <: REALSCALAR, Basis} throw( ErrorException( "Not yet implemented: inverse $(nameof(typeof(F)))( ::LineParameters{<:COMPLEXSCALAR,<:REALSCALAR,ModalDomain} )", diff --git a/src/engine/transforms/eiglevenberg.jl b/src/engine/transforms/eiglevenberg.jl index 351b32d5..068aaf85 100644 --- a/src/engine/transforms/eiglevenberg.jl +++ b/src/engine/transforms/eiglevenberg.jl @@ -33,8 +33,9 @@ transformation matrices and a **modal-domain** `LineParameters` holding the """ function (f::Levenberg)( - lp::LineParameters{Tc, U, PhaseDomain}, -) where {Tc <: COMPLEXSCALAR, U <: REALSCALAR} + lp::LineParameters{ + Tc, U, PhaseDomain, Basis}, +) where {Tc <: COMPLEXSCALAR, U <: REALSCALAR, Basis} 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")) @@ -86,7 +87,7 @@ function (f::Levenberg)( # _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) + return Ti, LineParameters(ModalDomain, Zm, Ym, lp.f; basis = basis(lp)) # 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), diff --git a/src/engine/transforms/fortescue.jl b/src/engine/transforms/fortescue.jl index 9ce0412f..4317d186 100644 --- a/src/engine/transforms/fortescue.jl +++ b/src/engine/transforms/fortescue.jl @@ -11,8 +11,9 @@ $(TYPEDSIGNATURES) Functor implementation for `Fortescue`. """ function (f::Fortescue)( - lp::LineParameters{Tc, U, PhaseDomain}, -) where {Tc <: COMPLEXSCALAR, U <: REALSCALAR} + lp::LineParameters{ + Tc, U, PhaseDomain, Basis}, +) where {Tc <: COMPLEXSCALAR, U <: REALSCALAR, Basis} _, nph, nfreq = size(lp.Z.values) Tr = typeof(real(zero(Tc))) Tv = fortescue_F(nph, Tr) # unitary; inverse is F' @@ -39,7 +40,7 @@ function (f::Fortescue)( Z012[:, :, k] = Matrix(Diagonal(diag(Zseq))) Y012[:, :, k] = Matrix(Diagonal(diag(Yseq))) end - return Tv, LineParameters(ModalDomain, Z012, Y012, lp.f) + return Tv, LineParameters(ModalDomain, Z012, Y012, lp.f; basis = basis(lp)) end # Unitary N-point DFT (Fortescue) matrix diff --git a/src/plotbuilder/PlotBuilder.jl b/src/plotbuilder/PlotBuilder.jl index c09237a9..935a54bf 100644 --- a/src/plotbuilder/PlotBuilder.jl +++ b/src/plotbuilder/PlotBuilder.jl @@ -1,51 +1,31 @@ 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 +import ..UnitHandler: Units, QuantityTag + +export AbstractPlotSpec, AxisSpec, SeriesSpec, ViewSpec, PageSpec, RenderSpec, UIPlot +export make_render, export_svg + +function control_definitions(; + reset::Bool = true, + export_svg::Bool = true, + xlog::Bool = true, + ylog::Bool = true, + legend::Bool = true, + visibility::Bool = true, + zoom::Bool = true +) + return (; reset, export_svg, xlog, ylog, legend, visibility, zoom) +end -# Submodule `BackendHandler` include("backendhandler/BackendHandler.jl") using .BackendHandler include("types.jl") -include("traits.jl") -include("axisspec.jl") -include("parse.jl") -include("seriesspec.jl") -include("viewspec.jl") -include("pagespec.jl") - -""" - make_render(::Type{S}, obj; kwargs...) where {S<:AbstractPlotSpec} - -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. -""" - -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)") - - raw = parse_kwargs(S, obj; kwargs...) - norm = resolve_input(S, raw) - pags = make_pages(S, norm) # ::Vector{PageSpec} - - return RenderSpec(S, pags) +function make_render(::Type{S}, object; kwargs...) where {S <: AbstractPlotSpec} + throw(MethodError(make_render, (S, object))) end +function export_svg end + end # module PlotBuilder diff --git a/src/plotbuilder/axisspec.jl b/src/plotbuilder/axisspec.jl deleted file mode 100644 index c70463e3..00000000 --- a/src/plotbuilder/axisspec.jl +++ /dev/null @@ -1,82 +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/pagespec.jl b/src/plotbuilder/pagespec.jl deleted file mode 100644 index 364b6335..00000000 --- a/src/plotbuilder/pagespec.jl +++ /dev/null @@ -1,450 +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 c1d1fe81..00000000 --- a/src/plotbuilder/parse.jl +++ /dev/null @@ -1,530 +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 -function parse_kwargs(::Type{S}, obj; kwargs...) where {S <: AbstractPlotSpec} - parse_kwargs(S, obj, (; kwargs...)) -end - -""" -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 6caa3575..00000000 --- a/src/plotbuilder/plothelpers.jl +++ /dev/null @@ -1,37 +0,0 @@ -using Makie - -using Base: basename, mod1 -using Dates: format, now -using Printf: @sprintf - -import LineCableModels.PlotBuilder.BackendHandler: BackendHandler, next_fignum - -using ..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 -using ..LineCableModelsMakieExt: - _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 63aceb8b..00000000 --- a/src/plotbuilder/plotspecs.jl +++ /dev/null @@ -1,166 +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 d54cb3a0..00000000 --- a/src/plotbuilder/plotuicomponents/PlotUIComponents.jl +++ /dev/null @@ -1,819 +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 LineCableModels.PlotBuilder.BackendHandler: current_backend_symbol, make_screen - -# ----------------------------------------------------------------------------- -# 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 - -function ControlReaction(; - status_string = nothing, - button_color = nothing, - button_label = nothing, - timeout = 1.5 -) - ControlReaction(status_string, button_color, button_label, timeout) -end - -struct ControlButtonSpec - label::Union{Nothing, String} - icon::Union{Nothing, String} - action::Function - on_success::Union{Nothing, ControlReaction} - on_failure::Union{Nothing, ControlReaction} -end - -function 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) -end - -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 - -function 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 - ) -end - -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 - -function 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 - ) -end - -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 - return make_screen(:gl, 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. -""" -function 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)) - ) -end - -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 - -function _configure_theme!( - ctx::PlotBackendContext; - mode::Symbol = ctx.interactive ? :interactive : :export -) - theme_for(ctx; mode = mode) -end - -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 8b137891..00000000 --- a/src/plotbuilder/plotuicomponents/callbacks.jl +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/plotbuilder/seriesspec.jl b/src/plotbuilder/seriesspec.jl deleted file mode 100644 index f4a1a88c..00000000 --- a/src/plotbuilder/seriesspec.jl +++ /dev/null @@ -1,286 +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. -""" -function axis_transform( - ::Type{S}, - ::Val{dim}, - ::Val{datakey}, - nt::NamedTuple, - axis::AxisSpec, - data -) where {S <: AbstractPlotSpec, dim, datakey} - 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 diff --git a/src/plotbuilder/traits.jl b/src/plotbuilder/traits.jl deleted file mode 100644 index 34937c04..00000000 --- a/src/plotbuilder/traits.jl +++ /dev/null @@ -1,322 +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}`. -""" -function axis_quantity(::Type{S}, dim::Symbol) where {S <: AbstractPlotSpec} - QuantityTag{:unknown}() -end - -function axis_quantity(::Type{S}, ::Val{dim}) where {S <: AbstractPlotSpec, dim} - axis_quantity(S, dim) -end - -function axis_quantity( - ::Type{S}, - ::Val{dim}, - ::Val{datakey} -) where {S <: AbstractPlotSpec, dim, datakey} - axis_quantity(S, dim) -end - -""" - 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. -""" -function axis_unit(::Type{S}, q::QuantityTag, dim::Symbol) where {S <: AbstractPlotSpec} - display_unit(q) -end - -# -------------------------------------------------------------------------- -# 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) -# -------------------------------------------------------------------------- - -function has_complex_qty( - ::Type{S}, - ::Val{dim}, - ::Val{datakey} -) where {S <: AbstractPlotSpec, dim, datakey} - false -end - -function complex_as( - ::Type{S}, - ::Val{dim}, - ::Val{datakey} -) where {S <: AbstractPlotSpec, dim, datakey} - (:re, :im, :abs, :angle) -end - -function complex_as_default( - ::Type{S}, - ::Val{dim}, - ::Val{datakey} -) where {S <: AbstractPlotSpec, dim, datakey} - :re -end - -# View-aware axis_quantity: fallback keeps existing grammar intact -function axis_quantity( - ::Type{S}, - ::Val{dim}, - ::Val{datakey}, - ::Val{as} -) where {S <: AbstractPlotSpec, dim, datakey, as} - axis_quantity(S, Val(dim), Val(datakey)) -end - -# Z: re/im correspond to R/X -function axis_quantity( - ::Type{S}, - ::Val{dim}, - ::Val{:Z}, - ::Val{:re} -) where {S <: AbstractPlotSpec, dim} - QuantityTag{:resistance}() -end -function axis_quantity( - ::Type{S}, - ::Val{dim}, - ::Val{:Z}, - ::Val{:im} -) where {S <: AbstractPlotSpec, dim} - QuantityTag{:reactance}() -end - -# Y: re/im correspond to G/B -function axis_quantity( - ::Type{S}, - ::Val{dim}, - ::Val{:Y}, - ::Val{:re} -) where {S <: AbstractPlotSpec, dim} - QuantityTag{:conductance}() -end -function axis_quantity( - ::Type{S}, - ::Val{dim}, - ::Val{:Y}, - ::Val{:im} -) where {S <: AbstractPlotSpec, dim} - QuantityTag{:susceptance}() -end - -function axis_quantity( - ::Type{S}, - ::Val{dim}, - ::Val{:Z}, - ::Val{:abs} -) where {S <: AbstractPlotSpec, dim} - QuantityTag{(:impedance, :abs)}() -end -function axis_quantity( - ::Type{S}, - ::Val{dim}, - ::Val{:Z}, - ::Val{:angle} -) where {S <: AbstractPlotSpec, dim} - QuantityTag{(:impedance, :angle)}() -end - -function axis_quantity( - ::Type{S}, - ::Val{dim}, - ::Val{:Y}, - ::Val{:abs} -) where {S <: AbstractPlotSpec, dim} - QuantityTag{(:admittance, :abs)}() -end -function axis_quantity( - ::Type{S}, - ::Val{dim}, - ::Val{:Y}, - ::Val{:angle} -) where {S <: AbstractPlotSpec, dim} - QuantityTag{(:admittance, :angle)}() -end diff --git a/src/plotbuilder/types.jl b/src/plotbuilder/types.jl index 460f40b3..8df757f5 100644 --- a/src/plotbuilder/types.jl +++ b/src/plotbuilder/types.jl @@ -1,14 +1,5 @@ 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 @@ -17,88 +8,72 @@ struct AxisSpec 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 +struct SeriesSpec{X, Y, Z, A <: NamedTuple} kind::Symbol - xdata::Union{Nothing, AbstractVector{<:Number}} - ydata::Union{Nothing, AbstractArray{<:Number}} - zdata::Union{Nothing, AbstractArray{<:Number}} + xdata::X + ydata::Y + zdata::Z label::Union{Nothing, String} + attributes::A 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). +function SeriesSpec(kind::Symbol, xdata, ydata, zdata, label; attributes = (;)) + return SeriesSpec(kind, xdata, ydata, zdata, label, attributes) +end -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 +struct ViewSpec{A <: NamedTuple} xaxis::Union{Nothing, AxisSpec} yaxis::Union{Nothing, AxisSpec} zaxis::Union{Nothing, AxisSpec} title::String series::Vector{SeriesSpec} key::NamedTuple + attributes::A end -""" - PageSpec - -One logical figure / window. +function ViewSpec(xaxis, yaxis, zaxis, title, series, key; attributes = (;)) + return ViewSpec(xaxis, yaxis, zaxis, title, SeriesSpec[series...], key, attributes) +end -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 +struct PageSpec{K <: NamedTuple} title::String size::Tuple{Int, Int} layout::Symbol views::Vector{ViewSpec} - kwargs::NamedTuple + kwargs::K 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. +function PageSpec(title, size, layout, views::AbstractVector, kwargs::NamedTuple) + return PageSpec(title, size, layout, ViewSpec[views...], kwargs) +end -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 + +function RenderSpec(spec::Type{S}, figures::AbstractVector) where {S <: AbstractPlotSpec} + return RenderSpec(spec, PageSpec[figures...]) +end + +""" + UIPlot + +Hold a backend-neutral render specification together with one built figure, +its panels, controls, and backend context. Line-parameter plotting returns a +`Vector{UIPlot}`; previews and statistical plots return one `UIPlot`. +""" +struct UIPlot{S <: AbstractPlotSpec, F, P, W, C} + "Complete backend-neutral render specification." + render::RenderSpec{S} + "Page represented by this handle." + page::PageSpec + "Backend-built figure." + figure::F + "Built axes or panels." + panels::P + "Interactive control objects keyed by purpose." + controls::W + "Active backend and status context." + context::C +end diff --git a/src/plotbuilder/uicomponents/UIComponents.jl b/src/plotbuilder/uicomponents/UIComponents.jl index 4943a32d..fd19836d 100644 --- a/src/plotbuilder/uicomponents/UIComponents.jl +++ b/src/plotbuilder/uicomponents/UIComponents.jl @@ -1,24 +1,474 @@ module UIComponents using Makie +using Measurements +using Dates -import ..BackendHandler -using ..PlotUIComponents: PlotAssembly +import LineCableModels.PlotBuilder +using LineCableModels.PlotBuilder: + AxisSpec, SeriesSpec, ViewSpec, PageSpec, + RenderSpec, UIPlot +const BackendHandler = PlotBuilder.BackendHandler -import ..PlotBuilder: AbstractPlotSpec, RenderSpec, PageSpec, ViewSpec, SeriesSpec, AxisSpec +export build, export_svg -export build, - UIContext, UILayoutSpec, UIContainerSpec, UISlotSpec, - UIFigure, UIPanel, PlotAssembly +const FIGURE_PADDING = (80, 60, 40, 40) +const LEGEND_WIDTH = 220 +const COLORBAR_WIDTH = 160 +const EXPORT_TIMESTAMP_FORMAT = "yyyymmdd_HHMMSS" -export display! +mutable struct UIContext + backend::Symbol + interactive::Bool + window::Any + status::Observable{String} +end -include("themes.jl") -include("types.jl") -include("layoutspecs.jl") -include("actions.jl") -include("draw.jl") -include("widgets.jl") -include("pipeline.jl") +struct UIPanel + view::ViewSpec + axis::Any + plots::Vector{Any} + groups::Dict{Symbol, Vector{Any}} + group_labels::Dict{Symbol, String} +end + +function _theme(; export_mode::Bool = false) + background = export_mode ? :white : :grey95 + return Theme( + backgroundcolor = background, + Axis = ( + titlesize = 15, + xlabelsize = 14, + ylabelsize = 14, + xticklabelsize = 14, + yticklabelsize = 14 + ), + Legend = (; fontsize = 14, labelsize = 14), + Colorbar = (; labelsize = 14, ticklabelsize = 14) + ) +end + +function _context(backend, display) + active = BackendHandler.ensure_backend!(backend) + interactive = display && active in (:gl, :wgl) + window = interactive && active === :gl ? + BackendHandler.make_screen("LineCableModels Plot"; backend = :gl) : nothing + return UIContext(active, interactive, window, Observable("Ready.")) +end + +_scale(symbol::Symbol) = symbol === :log10 ? Makie.log10 : Makie.identity + +function _numeric_values(values) + values === nothing && return nothing, nothing + nominal = Measurements.value.(values) + errors = Measurements.uncertainty.(values) + return nominal, any(error -> !iszero(error), errors) ? errors : nothing +end + +function _without(attributes::NamedTuple, excluded::Tuple) + return (; (key => value for (key, value) in pairs(attributes) if key ∉ excluded)...) +end + +function _draw!(axis, series::SeriesSpec) + attributes = _without(series.attributes, (:group,)) + plots = Any[] + if series.kind === :line + x, xerror = _numeric_values(series.xdata) + y, yerror = _numeric_values(series.ydata) + line = lines!(axis, x, y; label = series.label, attributes...) + push!(plots, line) + if yerror !== nothing + push!( + plots, + errorbars!( + axis, + x, + y, + yerror; + color = :black, + direction = :y, + whiskerwidth = 3, + linewidth = 1 + ) + ) + end + if xerror !== nothing + push!( + plots, + errorbars!( + axis, + x, + y, + xerror; + color = :black, + direction = :x, + whiskerwidth = 3, + linewidth = 1 + ) + ) + end + elseif series.kind === :scatter + x, _ = _numeric_values(series.xdata) + y, _ = _numeric_values(series.ydata) + push!(plots, scatter!(axis, x, y; label = series.label, attributes...)) + elseif series.kind === :histogram + values, _ = _numeric_values(series.xdata) + push!(plots, hist!(axis, values; label = series.label, attributes...)) + elseif series.kind === :stairs + x, _ = _numeric_values(series.xdata) + y, _ = _numeric_values(series.ydata) + push!(plots, stairs!(axis, x, y; label = series.label, attributes...)) + elseif series.kind === :heatmap + push!( + plots, + heatmap!( + axis, + series.xdata, + series.ydata, + series.zdata; + attributes... + ) + ) + elseif series.kind === :polygon + push!(plots, poly!(axis, series.zdata; label = series.label, attributes...)) + elseif series.kind === :hline + push!(plots, hlines!(axis, series.ydata; label = series.label, attributes...)) + else + throw(ArgumentError("unsupported PlotBuilder primitive :$(series.kind)")) + end + return plots +end + +function _axis(parent, view::ViewSpec) + xaxis = view.xaxis + yaxis = view.yaxis + attributes = _without(view.attributes, (:aspect, :limits)) + aspect = get(view.attributes, :aspect, nothing) + axis = Axis( + parent; + xlabel = xaxis === nothing ? "" : xaxis.label, + ylabel = yaxis === nothing ? "" : yaxis.label, + title = view.title, + xscale = xaxis === nothing ? Makie.identity : _scale(xaxis.scale), + yscale = yaxis === nothing ? Makie.identity : _scale(yaxis.scale), + aspect = aspect === :data ? DataAspect() : aspect, + attributes... + ) + plots = Any[] + groups = Dict{Symbol, Vector{Any}}() + group_labels = Dict{Symbol, String}() + for (index, series) in enumerate(view.series) + drawn = _draw!(axis, series) + append!(plots, drawn) + group = get(series.attributes, :group, Symbol("series_$index")) + append!(get!(groups, group, Any[]), drawn) + if series.label !== nothing && !isempty(series.label) + group_labels[group] = series.label + end + end + if haskey(view.attributes, :limits) + xlimits, ylimits = view.attributes.limits + xlims!(axis, xlimits...) + ylims!(axis, ylimits...) + end + return UIPanel(view, axis, plots, groups, group_labels) +end + +function _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 _available_path(page::PageSpec) + base = _sanitize_filename(get(page.kwargs, :export_name, page.title)) + stamp = Dates.format(Dates.now(), EXPORT_TIMESTAMP_FORMAT) + candidate = joinpath(pwd(), "$(base)_$(stamp).svg") + index = 2 + while ispath(candidate) + candidate = joinpath(pwd(), "$(base)_$(stamp)_$(index).svg") + index += 1 + end + return candidate +end + +function _visibility_groups(panels) + groups = Dict{Symbol, Vector{Any}}() + labels = Dict{Symbol, String}() + for panel in panels + for (group, plots) in panel.groups + append!(get!(groups, group, Any[]), plots) + end + merge!(labels, panel.group_labels) + end + return groups, labels +end + +function _colorbars!(slot, descriptors) + isempty(descriptors) && return nothing + grid = GridLayout() + slot[] = grid + colsize!(grid, 1, Relative(1)) + for (row, descriptor) in enumerate(descriptors) + Colorbar( + grid[row, 1]; + colormap = descriptor.colormap, + limits = descriptor.limits, + ticks = descriptor.ticks, + label = descriptor.label, + vertical = false, + width = COLORBAR_WIDTH, + height = 14 + ) + end + return grid +end + +function _legend!(slot, panels) + groups, group_labels = _visibility_groups(panels) + entries = Any[] + labels = String[] + for group in sort!(collect(keys(group_labels)); by = string) + push!(entries, groups[group]) + push!(labels, group_labels[group]) + end + return isempty(entries) ? nothing : Legend(slot, entries, labels; valign = :top) +end + +function _build_page( + render_spec::RenderSpec, + page::PageSpec, + context::UIContext; + controls::Bool, + export_mode::Bool +) + figure = Figure(size = page.size, figure_padding = FIGURE_PADDING) + toolbar_row = controls ? 1 : 0 + canvas_row = toolbar_row + 1 + status_row = canvas_row + 1 + canvas = GridLayout() + figure[canvas_row, 1] = canvas + panel_count = length(page.views) + columns = max(1, ceil(Int, sqrt(panel_count))) + panels = UIPanel[] + for (index, view) in enumerate(page.views) + row = (index - 1) ÷ columns + 1 + column = (index - 1) % columns + 1 + push!(panels, _axis(canvas[row, column], view)) + end + side = GridLayout() + side_column = isempty(page.views) ? 1 : 2 + figure[canvas_row, side_column] = side + if isempty(page.views) + colsize!(figure.layout, 1, Relative(1)) + colsize!(side, 1, Relative(1)) + end + side_row = 1 + legend = nothing + if get(page.kwargs, :display_legend, true) + legend = _legend!(side[side_row, 1], panels) + side_row += 1 + end + colorbars = get(page.kwargs, :colorbars, ()) + if !isempty(colorbars) + _colorbars!(side[side_row, 1], colorbars) + side_row += 1 + end + isempty(page.views) || colsize!(figure.layout, 2, Fixed(LEGEND_WIDTH)) + + widgets = Dict{Symbol, Any}() + plot_reference = Ref{Any}(nothing) + if controls + definitions = get( + page.kwargs, + :controls, + PlotBuilder.control_definitions() + ) + toolbar = GridLayout() + figure[1, 1:2] = toolbar + column = 1 + if definitions.reset + reset = Button(toolbar[1, column], label = "Reset") + column += 1 + widgets[:reset] = reset + on(reset.clicks) do _ + foreach(panel -> autolimits!(panel.axis), panels) + context.status[] = "Axis limits reset" + end + end + if definitions.export_svg + save_button = Button(toolbar[1, column], label = "Export SVG") + column += 1 + widgets[:export_svg] = save_button + on(save_button.clicks) do _ + try + PlotBuilder.export_svg(plot_reference[]) + catch error + context.status[] = sprint(showerror, error) + end + end + end + if definitions.xlog + active = !isempty(panels) && all( + panel -> panel.axis.xscale[] === Makie.log10, + panels + ) + xlog = Toggle(toolbar[1, column], active = active) + column += 1 + Label(toolbar[1, column], "log x") + column += 1 + widgets[:xlog] = xlog + on(xlog.active) do enabled + scale = enabled ? Makie.log10 : Makie.identity + foreach(panel -> panel.axis.xscale[] = scale, panels) + foreach(panel -> autolimits!(panel.axis), panels) + context.status[] = enabled ? + "x-axis scale set to log" : + "x-axis scale set to linear" + end + end + if definitions.ylog + active = !isempty(panels) && all( + panel -> panel.axis.yscale[] === Makie.log10, + panels + ) + ylog = Toggle(toolbar[1, column], active = active) + column += 1 + Label(toolbar[1, column], "log y") + widgets[:ylog] = ylog + on(ylog.active) do enabled + scale = enabled ? Makie.log10 : Makie.identity + foreach(panel -> panel.axis.yscale[] = scale, panels) + foreach(panel -> autolimits!(panel.axis), panels) + context.status[] = enabled ? + "y-axis scale set to log" : + "y-axis scale set to linear" + end + end + definitions.legend && legend !== nothing && (widgets[:legend] = legend) + Label(figure[status_row, 1:2], context.status; halign = :left, fontsize = 11) + rowsize!(figure.layout, 1, Fixed(48)) + rowsize!(figure.layout, canvas_row, Relative(1)) + rowsize!(figure.layout, status_row, Fixed(24)) + end + + built = UIPlot(render_spec, page, figure, panels, widgets, context) + plot_reference[] = built + return built +end + +function build( + render_spec::RenderSpec; + backend = nothing, + display::Bool = true, + controls::Bool = true, + export_mode::Bool = false +) + context = _context(backend, display) + built = UIPlot[] + with_theme(_theme(; export_mode)) do + for page in render_spec.figures + plot = _build_page( + render_spec, + page, + context; + controls, + export_mode + ) + push!(built, plot) + if display + if context.interactive && context.window !== nothing + Base.display(context.window, plot.figure) + else + BackendHandler.renderfig(plot.figure) + end + end + end + end + return built +end + +function _current_scale(scale) + scale === Makie.log10 && return :log10 + scale === Makie.identity && return :linear + throw(ArgumentError("SVG export supports linear and log10 axis scales")) +end + +function _axis_with_scale(spec::Union{Nothing, AxisSpec}, scale) + spec === nothing && return nothing + return AxisSpec(spec.dim, spec.quantity, spec.units, spec.label, _current_scale(scale)) +end + +function _current_limits(axis) + limits = axis.finallimits[] + xlimits = (limits.origin[1], limits.origin[1] + limits.widths[1]) + ylimits = (limits.origin[2], limits.origin[2] + limits.widths[2]) + return xlimits, ylimits +end + +function _current_series(series::SeriesSpec, panel::UIPanel, index::Int) + group = get(series.attributes, :group, Symbol("series_$index")) + visible = all(plot_object -> plot_object.visible[], panel.groups[group]) + attributes = merge(series.attributes, (; visible)) + return SeriesSpec( + series.kind, + series.xdata, + series.ydata, + series.zdata, + series.label; + attributes + ) +end + +function _current_view(view::ViewSpec, panel::UIPanel) + series = [_current_series(item, panel, index) + for (index, item) in enumerate(view.series)] + attributes = merge(view.attributes, (; limits = _current_limits(panel.axis))) + return ViewSpec( + _axis_with_scale(view.xaxis, panel.axis.xscale[]), + _axis_with_scale(view.yaxis, panel.axis.yscale[]), + view.zaxis, + view.title, + series, + view.key; + attributes + ) +end + +function _current_page(plot::UIPlot) + isempty(plot.page.views) && return plot.page + length(plot.page.views) == length(plot.panels) || throw( + DimensionMismatch("built panels no longer match the declarative page"), + ) + views = [_current_view(view, panel) + for (view, panel) in zip(plot.page.views, plot.panels)] + return PageSpec( + plot.page.title, plot.page.size, plot.page.layout, views, plot.page.kwargs) +end + +function PlotBuilder.export_svg(plot::UIPlot; path::Union{Nothing, AbstractString} = nothing) + BackendHandler.backend_available(:cairo) || throw( + ArgumentError("SVG export requires an explicitly loaded CairoMakie; run `using CairoMakie`"), + ) + output = path === nothing ? _available_path(plot.page) : abspath(String(path)) + ispath(output) && throw(ArgumentError("refusing to overwrite existing file: $output")) + plot.context.status[] = "Exporting SVG..." + BackendHandler.with_backend(:cairo) do + one_page = RenderSpec(plot.render.spec, PageSpec[_current_page(plot)]) + exported = build( + one_page; + backend = :cairo, + display = false, + controls = false, + export_mode = true + ) + Makie.save(output, only(exported).figure) + end + plot.context.status[] = "Saved SVG to $(basename(output))" + return output +end + +export_svg(args...; kwargs...) = PlotBuilder.export_svg(args...; kwargs...) end # module UIComponents diff --git a/src/plotbuilder/uicomponents/actions.jl b/src/plotbuilder/uicomponents/actions.jl deleted file mode 100644 index 361c9563..00000000 --- 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 47bcaff8..00000000 --- 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 diff --git a/src/plotbuilder/uicomponents/layoutspecs.jl b/src/plotbuilder/uicomponents/layoutspecs.jl deleted file mode 100644 index 46f24409..00000000 --- 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)) diff --git a/src/plotbuilder/uicomponents/pipeline.jl b/src/plotbuilder/uicomponents/pipeline.jl deleted file mode 100644 index 57d169e6..00000000 --- a/src/plotbuilder/uicomponents/pipeline.jl +++ /dev/null @@ -1,311 +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 diff --git a/src/plotbuilder/uicomponents/themes.jl b/src/plotbuilder/uicomponents/themes.jl deleted file mode 100644 index daaf97d5..00000000 --- a/src/plotbuilder/uicomponents/themes.jl +++ /dev/null @@ -1,109 +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 -# ----------------------------------------------------------------------------- - -function 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)) - ) -end - -""" - 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 f8906be9..00000000 --- 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 -function UISlotSpec(name, parent, at; layout = (;), attrs = (;)) - UISlotSpec(name, parent, at, layout, attrs) -end - -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 -function UIToggleSpec(label, active, action; attrs = (;)) - UIToggleSpec(label, active, action, attrs) -end - -# ------------------------- -# 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 diff --git a/src/plotbuilder/uicomponents/widgets.jl b/src/plotbuilder/uicomponents/widgets.jl deleted file mode 100644 index 2009ab3f..00000000 --- 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 diff --git a/src/plotbuilder/viewspec.jl b/src/plotbuilder/viewspec.jl deleted file mode 100644 index 904bc613..00000000 --- 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 diff --git a/src/unithandler/UnitHandler.jl b/src/unithandler/UnitHandler.jl index 0e402403..00355abf 100644 --- a/src/unithandler/UnitHandler.jl +++ b/src/unithandler/UnitHandler.jl @@ -214,6 +214,33 @@ Example override (outside this module): """ display_unit(q::QuantityTag{Q}) where {Q} = default_unit(q) +function _with_basis(unit::Units, basis::Symbol; length_prefix::Symbol = :base) + basis in (:per_length, :total) || throw( + ArgumentError("basis must be :per_length or :total"), + ) + has_length_denominator = any(item -> item.name == :meter, unit.per) + retained = Unit[item for item in unit.per if item.name != :meter] + if basis === :per_length && has_length_denominator + push!(retained, Unit(name = :meter, prefix = length_prefix)) + end + return Units(base = copy(unit.base), per = retained) +end + +function default_unit(quantity::QuantityTag, basis::Symbol) + _with_basis(default_unit(quantity), basis; length_prefix = :base) +end +function display_unit( + quantity::QuantityTag, + basis::Symbol; + length_prefix::Symbol = :kilo +) + _with_basis(display_unit(quantity), basis; length_prefix) +end + +function scale_factor(quantity::QuantityTag, basis::Symbol, target::Units) + return scale_factor(default_unit(quantity, basis), target) +end + """ Human-readable label for the quantity, without units. diff --git a/src/uq/UQ.jl b/src/uq/UQ.jl index cbe9e5db..0e8ca769 100644 --- a/src/uq/UQ.jl +++ b/src/uq/UQ.jl @@ -1,26 +1,34 @@ module UQ # Export public API -export sample, trial, mc +export SampleSummary, RLCG, HistogramPDF, CableConstantsMC, LineParametersMC +export sample, trial, mc, + statistics, has_samples, samples, has_distributions, distribution, + surrogate, ntrials, confidence, mean, std, quantile # Module-specific dependencies using ..Commons: BASE_FLOAT -import ..Commons: domain, PhaseDomain, ModalDomain, - LineParamsDomain +import ..Commons: domain, basis, frequencies, nconductors, nfrequencies, + 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 + EMTFormulation, compute!, LineParameters, SeriesImpedance, ShuntAdmittance +using ..DataModel: get_outer_radius, CableConstants +using ..UnitHandler +using ..PlotBuilder using Measurements: Measurement, measurement, value, uncertainty using Random, Statistics, DataFrames +import Statistics: mean, std using Distributions: Distributions, ContinuousUnivariateDistribution, Normal, Uniform, cdf, sampler -using StatsBase: fit, Histogram, normalize, quantile, ecdf +using StatsBase: fit, Histogram, normalize, ecdf +import StatsBase +import StatsBase: quantile using LinearAlgebra # Draw once from a "range-like" spec @@ -351,6 +359,6 @@ include("types.jl") include("distributions.jl") include("montecarlo.jl") include("dataframe.jl") -include("plotspecs/mcstatsplotspec.jl") +include("plotspecs.jl") end # module UQ diff --git a/src/uq/dataframe.jl b/src/uq/dataframe.jl index 91dc0902..543975f0 100644 --- a/src/uq/dataframe.jl +++ b/src/uq/dataframe.jl @@ -1,45 +1,63 @@ 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( +function _confidence_columns(summary::SampleSummary, result) + z = Distributions.quantile( + Distributions.Normal(), + 0.5 + confidence(result) / 2 + ) + half_width = z * summary.std / sqrt(ntrials(result)) + relative = half_width / max(abs(summary.mean), eps(typeof(summary.mean))) + return half_width, relative +end + +function DataFrame(result::LineParametersMC) + shape = size(result.statistics.R) + frames = Array{DataFrame, 3}(undef, shape) + for k in axes(frames, 3), j in axes(frames, 2), i in axes(frames, 1) + summaries = ( + statistics(result, :R, i, j, k), + statistics(result, :L, i, j, k), + statistics(result, :C, i, j, k), + statistics(result, :G, i, j, k) + ) + confidence_values = _confidence_columns.(summaries, Ref(result)) + frames[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] + mean = getproperty.(summaries, :mean), + std = getproperty.(summaries, :std), + min = getproperty.(summaries, :min), + q05 = getproperty.(summaries, :q05), + q50 = getproperty.(summaries, :q50), + q95 = getproperty.(summaries, :q95), + max = getproperty.(summaries, :max), + n = fill(ntrials(result), 4), + conf = fill(confidence(result), 4), + ci_half = first.(confidence_values), + ci_rel = last.(confidence_values) ) end - return dfs + return frames end -function DataFrame(res::CableDesignMC) - sR, sL, sC = res.stats.R, res.stats.L, res.stats.C - DataFrame( +function DataFrame(result::CableConstantsMC) + summaries = ( + statistics(result, :R), + statistics(result, :L), + statistics(result, :C) + ) + confidence_values = _confidence_columns.(summaries, Ref(result)) + return 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] + mean = getproperty.(summaries, :mean), + std = getproperty.(summaries, :std), + min = getproperty.(summaries, :min), + q05 = getproperty.(summaries, :q05), + q50 = getproperty.(summaries, :q50), + q95 = getproperty.(summaries, :q95), + max = getproperty.(summaries, :max), + ntrials = fill(ntrials(result), 3), + confidence = fill(confidence(result), 3), + ci_half = first.(confidence_values), + ci_rel = last.(confidence_values) ) end diff --git a/src/uq/distributions.jl b/src/uq/distributions.jl index a5eb4016..6ce8cb2a 100644 --- a/src/uq/distributions.jl +++ b/src/uq/distributions.jl @@ -1,631 +1,258 @@ -@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)", - ), +function HistogramPDF( + edges::AbstractVector{TE}, + density::AbstractVector{TD} +) where {TE <: Real, TD <: Real} + length(edges) == length(density) + 1 || throw( + ArgumentError("edges must contain exactly one more value than density"), ) - 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) -``` - -""" -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)) + isempty(density) && throw(ArgumentError("density must contain at least one bin")) + all(isfinite, edges) || throw(ArgumentError("histogram edges must be finite")) + all(isfinite, density) || throw(ArgumentError("histogram density must be finite")) + all(>=(zero(TD)), density) || + throw(ArgumentError("histogram density must be nonnegative")) + + T = float(promote_type(TE, TD)) + copied_edges = Vector{T}(edges) + copied_density = Vector{T}(density) + widths = diff(copied_edges) + all(>(zero(T)), widths) || throw( + ArgumentError("histogram edges must be strictly increasing"), + ) + area = dot(copied_density, widths) + area > zero(area) || throw(ArgumentError("histogram density must have positive area")) + copied_density ./= area + return HistogramPDF{T}(copied_edges, copied_density) 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}; +function _auto_nbins( + values::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) + isempty(values) && throw(ArgumentError("cannot bin an empty sample")) + sorted_values = sort(float.(values)) + span = last(sorted_values) - first(sorted_values) + if !(isfinite(span) && span > 0) 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) + interquartile_range = StatsBase.quantile(sorted_values, 0.75) - + StatsBase.quantile(sorted_values, 0.25) + if !(isfinite(interquartile_range) && interquartile_range > 0) + return clamp(ceil(Int, sqrt(length(values))), 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 + width = 2 * interquartile_range / cbrt(length(values)) + raw_count = span / width + return isfinite(raw_count) ? + clamp(ceil(Int, raw_count), nbins_min, nbins_max) : nbins_max 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 +function _pdf_from_hist( + values::AbstractVector{<:Real}; + nbins::Union{Int, Nothing} = nothing +) + isempty(values) && throw(ArgumentError("cannot fit a histogram to an empty sample")) + count = isnothing(nbins) ? _auto_nbins(values) : nbins + count > 0 || throw(ArgumentError("nbins must be positive")) + histogram = fit(Histogram, float.(values); nbins = count, closed = :left) + edges = collect(histogram.edges[1]) + density = histogram.weights ./ (length(values) .* diff(edges)) + return HistogramPDF(edges, density) 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 +function _binindex(distribution::HistogramPDF, value::Real) + value < first(distribution.edges) && return 0 + value > last(distribution.edges) && return 0 + value == last(distribution.edges) && return length(distribution.density) + return searchsortedlast(distribution.edges, value) 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] +(distribution::HistogramPDF)(value::Real) = Distributions.pdf(distribution, value) +Distributions.minimum(distribution::HistogramPDF) = first(distribution.edges) +Distributions.maximum(distribution::HistogramPDF) = last(distribution.edges) +function Distributions.insupport(distribution::HistogramPDF, value::Real) + minimum(distribution) <= value <= maximum(distribution) 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] +function Distributions.pdf(distribution::HistogramPDF{T}, value::Real) where {T} + index = _binindex(distribution, value) + return index == 0 ? zero(T) : distribution.density[index] 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 +function Distributions.logpdf(distribution::HistogramPDF, value::Real) + probability = Distributions.pdf(distribution, value) + return probability > 0 ? log(probability) : -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)); +function Distributions.cdf(distribution::HistogramPDF{T}, value::Real) where {T} + value < minimum(distribution) && return zero(T) + value >= maximum(distribution) && return one(T) + index = _binindex(distribution, value) + widths = diff(distribution.edges) + prior = sum( + (distribution.density[j] * widths[j] for j in 1:(index - 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 + return prior + distribution.density[index] * (value - distribution.edges[index]) end -# --- Quantile (Inverse CDF) --- - -""" -Pre-calculates cumulative probabilities for efficient sampling. -This is what `sampler` should actually be doing. -""" -struct LineParametersPDFSampler{T <: Real} <: +struct HistogramPDFSampler{T <: Real} <: Distributions.Sampleable{Distributions.Univariate, Distributions.Continuous} - d::LineParametersPDF{T} - cum_probs::Vector{T} # Cumulative probability at the *end* of each bin + distribution::HistogramPDF{T} + cumulative_probability::Vector{T} 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 +function Distributions.sampler(distribution::HistogramPDF) + probabilities = distribution.density .* diff(distribution.edges) + cumulative = cumsum(probabilities) + cumulative[end] = one(eltype(cumulative)) + return HistogramPDFSampler(distribution, cumulative) 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) +function Distributions.quantile(sampler::HistogramPDFSampler{T}, probability::Real) where {T} + probability <= 0 && return minimum(sampler.distribution) + probability >= 1 && return maximum(sampler.distribution) + index = searchsortedfirst(sampler.cumulative_probability, probability) + prior = index == 1 ? zero(T) : sampler.cumulative_probability[index - 1] + density = sampler.distribution.density[index] + density == 0 && return sampler.distribution.edges[index] + return sampler.distribution.edges[index] + (probability - prior) / density 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) +function Distributions.quantile(distribution::HistogramPDF, probability::Real) + 0 <= probability <= 1 || throw( + DomainError(probability, "probability must lie in [0, 1]"), + ) + Distributions.quantile(Distributions.sampler(distribution), probability) 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) +function Base.rand(rng::AbstractRNG, sampler::HistogramPDFSampler) + 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 -# 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) +function Base.rand(rng::AbstractRNG, distribution::HistogramPDF) + rand(rng, Distributions.sampler(distribution)) 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) +function _raw_moment(distribution::HistogramPDF{T}, order::Integer) where {T} + order >= 0 || throw(ArgumentError("moment order must be nonnegative")) + 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 - - μ3 = m3 - 3*μ*m2 + 2*μ^3 - return μ3 / μ2^(3/2) + return total 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 +function Distributions.moment(distribution::HistogramPDF, order::Integer) + _raw_moment(distribution, order) 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 +Distributions.mean(distribution::HistogramPDF) = _raw_moment(distribution, 1) +function Distributions.var(distribution::HistogramPDF) + value = _raw_moment(distribution, 2) - Distributions.mean(distribution)^2 + return max(value, zero(value)) end +Distributions.std(distribution::HistogramPDF) = sqrt(Distributions.var(distribution)) -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] +function Distributions.mode(distribution::HistogramPDF) + _, index = findmax(distribution.density) + return (distribution.edges[index] + distribution.edges[index + 1]) / 2 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 +function Distributions.modes(distribution::HistogramPDF) + maximum_density = maximum(distribution.density) + indices = findall(value -> value ≈ maximum_density, distribution.density) + return [(distribution.edges[index] + distribution.edges[index + 1]) / 2 + for + index in indices] 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) +"""Return one retained scalar cable-constant trial.""" +function trial(result::CableConstantsMC, index::Integer) + resistance_values = samples(result, :R) + index in eachindex(resistance_values) || throw(BoundsError(result, index)) + return CableConstants( + resistance_values[index], + samples(result, :L)[index], + samples(result, :C)[index] + ) 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 +"""Reconstruct one retained joint line-parameter trial.""" +function trial( + result::LineParametersMC{S, Samples, Distributions, Surrogate}, + index::Integer +) where { + S, + Samples, + Distributions, + T, + U, + D, + Basis, + Surrogate <: LineParameters{T, U, D, Basis} +} + resistance_values = samples(result, :R) + inductance_values = samples(result, :L) + capacitance_values = samples(result, :C) + conductance_values = samples(result, :G) + sample_size = size(resistance_values) + all( + size(values) == sample_size + for + values in (inductance_values, capacitance_values, conductance_values) + ) || + throw(DimensionMismatch("stored R, L, C, and G samples must have equal dimensions")) + sample_size[1] == sample_size[2] || throw( + DimensionMismatch("stored line-parameter matrices must be square"), + ) + sample_size[3] == nfrequencies(result) || throw( + DimensionMismatch("stored samples and frequencies must agree"), + ) + index in axes(resistance_values, 4) || throw(BoundsError(result, index)) + + matrix_count = sample_size[1] + frequency_count = sample_size[3] + scalar_type = eltype(resistance_values) + impedance = Array{Complex{scalar_type}, 3}( + undef, + matrix_count, + matrix_count, + frequency_count + ) + admittance = similar(impedance) + frequency_values = frequencies(result) + for k in 1:frequency_count, j in 1:matrix_count, i in 1:matrix_count + omega = 2π * frequency_values[k] + impedance[i, j, k] = resistance_values[i, j, k, index] + + im * omega * inductance_values[i, j, k, index] + admittance[i, j, k] = conductance_values[i, j, k, index] + + im * omega * capacitance_values[i, j, k, index] end - return acc + return LineParameters( + D, + SeriesImpedance{eltype(impedance), Basis}(impedance), + ShuntAdmittance{eltype(admittance), Basis}(admittance), + frequency_values + ) 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 +function Base.rand(rng::AbstractRNG, result::Union{CableConstantsMC, LineParametersMC}) + has_samples(result) || throw( + ArgumentError("joint sampling requires mc(...; return_samples=true)"), + ) + return trial(result, rand(rng, 1:ntrials(result))) +end - acc += dens_i * exp(im * t * a) * w * exprel_z - end - return acc +function Base.rand(result::Union{CableConstantsMC, LineParametersMC}) + rand(Random.default_rng(), result) end diff --git a/src/uq/montecarlo.jl b/src/uq/montecarlo.jl index e3179cef..6cb423d8 100644 --- a/src/uq/montecarlo.jl +++ b/src/uq/montecarlo.jl @@ -23,7 +23,8 @@ Propagate cable-design uncertainty by Monte Carlo sampling. # Returns -- A `CableDesignMC` containing R, L, and C statistics and Measurements.jl values. +- A `CableConstantsMC` containing R, L, and C statistics, optional samples and + distributions, and a covariance-preserving Measurements.jl surrogate. # Notes @@ -43,9 +44,14 @@ function mc(cbs::CableBuilderSpec; return_pdf::Bool = false, nbins::Union{Int, Nothing} = nothing ) + trials === nothing || trials > 0 || throw(ArgumentError("mc: trials must be positive")) + 0 < conf < 1 || throw(ArgumentError("mc: conf must lie between zero and one")) + tol > 0 || throw(ArgumentError("mc: tol must be positive")) + print_step > 0 || throw(ArgumentError("mc: print_step must be positive")) + distribution in (:normal, :uniform) || throw( + ArgumentError("mc: distribution must be :normal or :uniform"), + ) 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 @@ -77,14 +83,11 @@ function mc(cbs::CableBuilderSpec; 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] + constants = CableConstants(des) @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 + μR[i] = T(constants.R) + μL[i] = T(constants.L) + μC[i] = T(constants.C) end return nothing end @@ -95,31 +98,12 @@ function mc(cbs::CableBuilderSpec; 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) - ] + summaries = CableConstants( + SampleSummary(μR), + SampleSummary(μL), + SampleSummary(μC) + ) + surrogate_value = _joint_cable_constants(μR, μL, μC) # PDFs (optional) pdf_nt = nothing @@ -127,17 +111,19 @@ function mc(cbs::CableBuilderSpec; 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) + pdf_nt = CableConstants(pdfR, pdfL, pdfC) end - # Samples as NamedTuple of vectors (R,L,C) or nothing - samples_nt = return_samples ? (R = μR, L = μL, C = μC) : nothing + # Joint samples as a CableConstants of vectors, or nothing. + samples_value = return_samples ? CableConstants(μR, μL, μC) : nothing - return CableDesignMC{T}( - (R = sR, L = sL, C = sC), + return CableConstantsMC( + summaries, + samples_value, pdf_nt, - samples_nt, - meas + surrogate_value, + ntrials, + T(conf) ) end @@ -184,7 +170,7 @@ 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 +The `surrogate` 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 @@ -214,15 +200,23 @@ function mc( 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 + return_pdf::Bool = false, # histogram PDFs per R/L/C/G and frequency per_length::Bool = true, # scale results per length nbins::Union{Int, Nothing} = nothing ) + 0 < conf < 1 || throw(ArgumentError("mc: conf must lie between zero and one")) + tol > 0 || throw(ArgumentError("mc: tol must be positive")) + print_step > 0 || throw(ArgumentError("mc: print_step must be positive")) + distribution in (:normal, :uniform) || throw( + ArgumentError("mc: distribution must be :normal or :uniform"), + ) seed !== nothing && Random.seed!(seed) - z = quantile(Distributions.Normal(), 0.5 + conf/2) - fvec = sbs.frequencies nfreq = length(fvec) + all(isfinite, fvec) || throw(ArgumentError("mc: frequencies must be finite")) + any(iszero, fvec) && throw( + DomainError(fvec, "mc: L and C are undefined at zero frequency"), + ) trials === nothing || trials > 0 || throw(ArgumentError("mc: trials must be greater than zero")) @@ -262,19 +256,6 @@ function mc( "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 @@ -344,16 +325,16 @@ function mc( # ───────────────────────────────────────────────────────────────────────── # 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) + Rstats = Array{SampleSummary{U}, 3}(undef, nph, nph, nfreq) + Lstats = Array{SampleSummary{U}, 3}(undef, nph, nph, nfreq) + Gstats = Array{SampleSummary{U}, 3}(undef, nph, nph, nfreq) + Cstats = Array{SampleSummary{U}, 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 + Rpdf = return_pdf ? Array{HistogramPDF{U}, 3}(undef, nph, nph, nfreq) : nothing + Lpdf = return_pdf ? Array{HistogramPDF{U}, 3}(undef, nph, nph, nfreq) : nothing + Gpdf = return_pdf ? Array{HistogramPDF{U}, 3}(undef, nph, nph, nfreq) : nothing + Cpdf = return_pdf ? Array{HistogramPDF{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, :] @@ -361,10 +342,10 @@ function mc( gvec = @view Gsamp[j1, j2, k, :] cvec = @view Csamp[j1, j2, k, :] - sR = _stats(rvec) - sL = _stats(lvec) - sG = _stats(gvec) - sC = _stats(cvec) + sR = SampleSummary(rvec) + sL = SampleSummary(lvec) + sG = SampleSummary(gvec) + sC = SampleSummary(cvec) Rstats[j1, j2, k] = sR Lstats[j1, j2, k] = sL @@ -382,16 +363,59 @@ function mc( # 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) + result_basis = per_length ? :per_length : :total + LP_meas = _joint_line_parameters( + Dlp, + Rsamp, + Lsamp, + Gsamp, + Csamp, + fvec; + basis = result_basis + ) @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 + statistics_value = RLCG(Rstats, Lstats, Cstats, Gstats) + distribution_value = return_pdf ? RLCG(Rpdf, Lpdf, Cpdf, Gpdf) : nothing + samples_value = return_samples ? RLCG(Rsamp, Lsamp, Csamp, Gsamp) : nothing + + return LineParametersMC( + statistics_value, + samples_value, + distribution_value, + LP_meas, + ntrials, + U(conf) + ) +end - return LineParametersMC(fvec, stats_nt, pdf_nt, samples_nt, LP_meas) +function _joint_cable_constants( + resistance_samples::AbstractVector{T}, + inductance_samples::AbstractVector{T}, + capacitance_samples::AbstractVector{T} +) where {T <: Real} + sample_count = length(resistance_samples) + length(inductance_samples) == sample_count == length(capacitance_samples) || throw( + DimensionMismatch("R, L, and C sample vectors must have equal lengths"), + ) + sample_count > 0 || throw(ArgumentError("at least one Monte Carlo trial is required")) + + sample_matrix = permutedims(hcat( + resistance_samples, + inductance_samples, + capacitance_samples + )) + means = vec(mean(sample_matrix; dims = 2)) + joint = if sample_count == 1 + map(value -> measurement(value, zero(T)), means) + else + factor = sample_matrix .- means + factor ./= sqrt(sample_count - 1) + latent = [measurement(zero(T), one(T)) for _ in 1:sample_count] + means + factor * latent + end + return CableConstants(joint...) end function _joint_measurements( @@ -444,7 +468,8 @@ function _joint_line_parameters( Lsamp::AbstractArray{U, 4}, Gsamp::AbstractArray{U, 4}, Csamp::AbstractArray{U, 4}, - fvec::AbstractVector{U} + fvec::AbstractVector{U}; + basis::Symbol = :per_length ) where {D <: LineParamsDomain, U <: Real} Rmeas, Lmeas, Gmeas, Cmeas = _joint_measurements(Rsamp, Lsamp, Gsamp, Csamp) nph, _, nfreq = size(Rmeas) @@ -459,5 +484,5 @@ function _joint_line_parameters( 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) + return LineParameters(D, Zmeas, Ymeas, fvec; basis) end diff --git a/src/uq/plot.jl b/src/uq/plot.jl deleted file mode 100644 index 8b78b836..00000000 --- a/src/uq/plot.jl +++ /dev/null @@ -1,694 +0,0 @@ -using Makie -import LineCableModels.Engine: plot - -using ..EnginePlots: - 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() - -function _quantity_metadata(q::Symbol) - get(_MC_META, q) do - Base.error("Unsupported quantity $(q); use one of :R, :L, :C, :G") - end -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.jl b/src/uq/plotspecs.jl new file mode 100644 index 00000000..c5b25b43 --- /dev/null +++ b/src/uq/plotspecs.jl @@ -0,0 +1,259 @@ +struct MCDistributionPlotSpec <: PlotBuilder.AbstractPlotSpec end + +const _MC_PLOT_QUANTITY = Dict( + :R => (:resistance, :ohm, :base), + :L => (:inductance, :henry, :milli), + :C => (:capacitance, :farad, :micro), + :G => (:conductance, :siemens, :base) +) + +function _mc_selection(result::CableConstantsMC, quantity::Symbol, ijk) + ijk === nothing || throw(ArgumentError("CableConstantsMC does not use matrix indices")) + sample_values = has_samples(result) ? samples(result, quantity) : nothing + distribution_value = has_distributions(result) ? distribution(result, quantity) : + nothing + return sample_values, distribution_value, nothing +end + +function _mc_selection(result::LineParametersMC, quantity::Symbol, ijk) + selection = isnothing(ijk) ? (1, 1, 1) : ijk + selection isa NTuple{3, Int} || throw(ArgumentError("ijk must be a tuple (i, j, k)")) + i, j, k = selection + sample_values = has_samples(result) ? samples(result, quantity, i, j, k) : nothing + distribution_value = has_distributions(result) ? + distribution(result, quantity, i, j, k) : nothing + return sample_values, distribution_value, selection +end + +function _mc_target_unit(result, quantity::Symbol, length_unit, quantity_units) + semantic, unit_name, fallback = get(_MC_PLOT_QUANTITY, quantity) do + throw(ArgumentError("quantity must be :R, :L, :C, or :G")) + end + result isa CableConstantsMC && quantity === :G && + throw( + ArgumentError("CableConstantsMC does not contain conductance"), + ) + prefix = if quantity_units === nothing + fallback + elseif quantity_units isa Symbol + quantity_units + elseif haskey(quantity_units, quantity) + quantity_units[quantity] + elseif haskey(quantity_units, semantic) + quantity_units[semantic] + else + fallback + end + result_basis = result isa CableConstantsMC ? :per_length : basis(result) + tag = UnitHandler.QuantityTag{semantic}() + target = result_basis === :per_length ? + UnitHandler.units(prefix, unit_name; per = (length_unit, :meter)) : + UnitHandler.units(prefix, unit_name) + conversion = UnitHandler.scale_factor(tag, result_basis, target) + return tag, target, conversion +end + +function _scaled_distribution(distribution_value::HistogramPDF, conversion) + return HistogramPDF( + distribution_value.edges .* conversion, + distribution_value.density ./ conversion + ) +end + +function _mc_plot_series(mode, data, values, model, bins, normalization) + series = PlotBuilder.SeriesSpec[] + if mode in (:hist, :pdf) + if mode === :hist && data in (:samples, :both) + values === nothing && throw(ArgumentError("samples were not retained")) + push!( + series, + PlotBuilder.SeriesSpec( + :histogram, + values, + nothing, + nothing, + "samples"; + attributes = (; bins, normalization) + ) + ) + end + if mode === :pdf || data in (:pdf, :both) + model === nothing && throw(ArgumentError("distributions were not retained")) + y = [model.density; last(model.density)] + push!( + series, + PlotBuilder.SeriesSpec( + :stairs, + model.edges, + y, + nothing, + "model PDF"; + attributes = (; step = :post, color = :red, linewidth = 2) + ) + ) + end + elseif mode === :ecdf + values === nothing && + throw(ArgumentError("ECDF plotting requires retained samples")) + model === nothing && + throw(ArgumentError("ECDF plotting requires retained distributions")) + lower, upper = extrema(model.edges) + padding = iszero(upper - lower) ? one(lower) : 0.05 * (upper - lower) + x = collect(range(lower - padding, upper + padding; length = 500)) + empirical = StatsBase.ecdf(values) + push!( + series, + PlotBuilder.SeriesSpec( + :line, + x, + Distributions.cdf.(Ref(model), x), + nothing, + "model CDF"; + attributes = (; color = :red, linewidth = 2) + ) + ) + push!( + series, + PlotBuilder.SeriesSpec( + :line, + x, + empirical.(x), + nothing, + "empirical"; + attributes = (; color = :blue, linestyle = :dash, linewidth = 2) + ) + ) + elseif mode === :qq + values === nothing && throw(ArgumentError("Q-Q plotting requires retained samples")) + model === nothing && + throw(ArgumentError("Q-Q plotting requires retained distributions")) + sample_quantiles = sort(values) + probabilities = ((1:length(values)) .- 0.5) ./ length(values) + model_quantiles = Distributions.quantile.(Ref(model), probabilities) + limits = extrema(vcat(sample_quantiles, model_quantiles)) + push!( + series, + PlotBuilder.SeriesSpec( + :scatter, + sample_quantiles, + model_quantiles, + nothing, + "quantiles"; + attributes = (; color = :steelblue, markersize = 6) + ) + ) + push!( + series, + PlotBuilder.SeriesSpec( + :line, + collect(limits), + collect(limits), + nothing, + "perfect fit"; + attributes = (; color = :black, linestyle = :dash, linewidth = 2) + ) + ) + else + throw(ArgumentError("mode must be :hist, :pdf, :ecdf, or :qq")) + end + return series +end + +function PlotBuilder.make_render( + ::Type{MCDistributionPlotSpec}, + result::Union{CableConstantsMC, LineParametersMC}; + quantity::Symbol = :R, + ijk = nothing, + mode::Symbol = :hist, + data::Symbol = :samples, + length_unit::Symbol = :kilo, + quantity_units = nothing, + nbins::Union{Nothing, Int} = nothing, + normalization::Symbol = :none, + fig_size::Tuple{Int, Int} = (800, 400) +) + data in (:samples, :pdf, :both) || throw( + ArgumentError("data must be :samples, :pdf, or :both"), + ) + sample_values, distribution_value, selection = _mc_selection(result, quantity, ijk) + tag, target, conversion = _mc_target_unit( + result, + quantity, + length_unit, + quantity_units + ) + scaled_values = sample_values === nothing ? nothing : + collect(sample_values) .* conversion + scaled_distribution = distribution_value === nothing ? nothing : + _scaled_distribution(distribution_value, conversion) + bin_count = isnothing(nbins) && scaled_values !== nothing ? + _auto_nbins(scaled_values) : nbins + bins = if scaled_distribution !== nothing + scaled_distribution.edges + elseif scaled_values !== nothing + collect(fit(Histogram, scaled_values; nbins = bin_count, closed = :left).edges[1]) + else + Float64[] + end + effective_normalization = data in (:pdf, :both) ? :pdf : normalization + series = _mc_plot_series( + mode, + data, + scaled_values, + scaled_distribution, + bins, + effective_normalization + ) + symbol = UnitHandler.get_symbol(tag) + suffix = selection === nothing ? "" : "[$(join(selection, ','))]" + title = mode === :hist ? "$symbol$suffix histogram" : + mode === :pdf ? "$symbol$suffix probability density" : + mode === :ecdf ? "$symbol$suffix cumulative distribution" : + "$symbol$suffix Q-Q plot" + x_label = mode === :qq ? "sample quantiles [$(UnitHandler.get_label(target))]" : + "$(UnitHandler.get_label(tag)) [$(UnitHandler.get_label(target))]" + y_label = mode === :hist ? String(effective_normalization) : + mode === :pdf ? "density" : + mode === :ecdf ? "cumulative probability" : + "model quantiles [$(UnitHandler.get_label(target))]" + xaxis = PlotBuilder.AxisSpec(:x, tag, target, x_label, :linear) + yaxis = PlotBuilder.AxisSpec( + :y, + mode === :qq ? tag : UnitHandler.QuantityTag{:dimensionless}(), + mode === :qq ? target : UnitHandler.Units(), + y_label, + :linear + ) + view_spec = PlotBuilder.ViewSpec( + xaxis, + yaxis, + nothing, + title, + series, + (; quantity, selection, mode) + ) + page = PlotBuilder.PageSpec( + title, + fig_size, + :single, + PlotBuilder.ViewSpec[view_spec], + (; + quantity, + selection, + mode, + data, + controls = PlotBuilder.control_definitions(), + configuration = (; + quantity, + selection, + mode, + data, + length_unit, + quantity_units, + nbins, + normalization + ) + ) + ) + return PlotBuilder.RenderSpec(MCDistributionPlotSpec, PlotBuilder.PageSpec[page]) +end diff --git a/src/uq/plotspecs/mcstatsplotspec.jl b/src/uq/plotspecs/mcstatsplotspec.jl deleted file mode 100644 index 58d775d7..00000000 --- a/src/uq/plotspecs/mcstatsplotspec.jl +++ /dev/null @@ -1,96 +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. -function PlotBuilder.axis_quantity(::Type{MCStatsPlotSpec}, ::Val{:x}, ::Val{:f}) - QuantityTag{:freq}() -end -function PlotBuilder.axis_quantity(::Type{MCStatsPlotSpec}, ::Val{:y}, ::Val{:R}) - QuantityTag{:resistance}() -end -function PlotBuilder.axis_quantity(::Type{MCStatsPlotSpec}, ::Val{:y}, ::Val{:L}) - QuantityTag{:inductance}() -end -function PlotBuilder.axis_quantity(::Type{MCStatsPlotSpec}, ::Val{:y}, ::Val{:C}) - QuantityTag{:capacitance}() -end -function PlotBuilder.axis_quantity(::Type{MCStatsPlotSpec}, ::Val{:y}, ::Val{:G}) - QuantityTag{:conductance}() -end - -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 -function 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 :) - ) -end - -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/types.jl b/src/uq/types.jl index 6edc0f67..1c09a4c0 100644 --- a/src/uq/types.jl +++ b/src/uq/types.jl @@ -1,140 +1,461 @@ -# ───────────────────────────────────────────────────────────────────────────── -# 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 +""" + SampleSummary{T} + +Store the fixed summary statistics for one scalar Monte Carlo observable. +All fields use the observable's canonical units. +""" +struct SampleSummary{T <: Real} + "Sample mean." + mean::T + "Corrected sample standard deviation." + std::T + "Minimum sampled value." + min::T + "Five-percent quantile." + q05::T + "Median." + q50::T + "Ninety-five-percent quantile." + q95::T + "Maximum sampled value." + max::T +end + +function Base.:(==)(left::SampleSummary, right::SampleSummary) + left.mean == right.mean && left.std == right.std && left.min == right.min && + left.q05 == right.q05 && left.q50 == right.q50 && left.q95 == right.q95 && + left.max == right.max +end + +function SampleSummary(values::AbstractVector{T}) where {T <: Real} + isempty(values) && throw(ArgumentError("cannot summarize an empty sample")) + sigma = length(values) == 1 ? zero(float(first(values))) : Statistics.std(values) + promoted = promote( + Statistics.mean(values), + sigma, + minimum(values), + StatsBase.quantile(values, 0.05), + StatsBase.quantile(values, 0.50), + StatsBase.quantile(values, 0.95), + maximum(values) + ) + return SampleSummary(promoted...) +end + +""" + RLCG{T} + +Group resistance, inductance, capacitance, and conductance values with one +common storage representation. Units follow the basis of the containing +result: \\[Ω/m, H/m, F/m, S/m\\] for `:per_length`, or +\\[Ω, H, F, S\\] for `:total`. +""" +struct RLCG{T} + "Resistance values." + R::T + "Inductance values." + L::T + "Capacitance values." + C::T + "Conductance values." + G::T +end + +function Base.:(==)(left::RLCG, right::RLCG) + left.R == right.R && left.L == right.L && left.C == right.C && left.G == right.G +end + +""" + HistogramPDF(edges, density) + +Piecewise-constant univariate probability density. `edges` has one more entry +than `density`; density is normalized by the constructor. +""" +struct HistogramPDF{T <: Real} <: ContinuousUnivariateDistribution + "Strictly increasing histogram bin edges." + edges::Vector{T} + "Piecewise-constant probability density for each bin." + density::Vector{T} + + function HistogramPDF{T}( + edges::Vector{T}, + density::Vector{T} + ) where {T <: Real} + return new{T}(edges, density) end +end - return LineParametersPDF{T}(e, d) +function Base.:(==)(left::HistogramPDF, right::HistogramPDF) + left.edges == right.edges && left.density == right.density end """ - LineParametersMC{U, D} + CableConstantsMC -Store Monte Carlo summaries and a joint moment-matched line-parameter surrogate. +Store Monte Carlo summaries and optional retained data for scalar cable +constants. `surrogate` preserves the covariance of \\[R, L, C\\] with +`Measurement` values. Canonical units are \\[Ω/m, H/m, F/m\\]. +""" +struct CableConstantsMC{S, Samples, Distributions, Surrogate, T <: Real} + "Per-observable summary statistics." + statistics::CableConstants{S} + "Joint retained samples, or `nothing`." + samples::Samples + "Retained marginal histogram distributions, or `nothing`." + distributions::Distributions + "Covariance-preserving cable constants." + surrogate::Surrogate + "Number of Monte Carlo trials." + ntrials::Int + "Central confidence mass used by the analysis." + confidence::T -# Notes + function CableConstantsMC{S, Samples, Distributions, Surrogate, T}( + statistics::CableConstants{S}, + samples::Samples, + distributions::Distributions, + surrogate::Surrogate, + ntrials::Int, + confidence::T + ) where {S, Samples, Distributions, Surrogate, T <: Real} + return new{S, Samples, Distributions, Surrogate, T}( + statistics, + samples, + distributions, + surrogate, + ntrials, + confidence + ) + end +end -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. +function CableConstantsMC( + statistics::CableConstants{S}, + samples::Samples, + distributions::Distributions, + surrogate::Surrogate, + ntrials::Integer, + confidence::T +) where {S, Samples, Distributions, Surrogate, T <: Real} + ntrials > 0 || throw(ArgumentError("ntrials must be positive")) + 0 < confidence < 1 || throw(ArgumentError("confidence must lie between zero and one")) + all(value -> value isa SampleSummary, (statistics.R, statistics.L, statistics.C)) || + throw(ArgumentError("cable-constant statistics must contain SampleSummary values")) + if samples !== nothing + samples isa CableConstants || throw( + ArgumentError("cable-constant samples must be CableConstants or nothing"), + ) + all(value -> value isa AbstractVector{<:Real}, (samples.R, samples.L, samples.C)) || + throw(ArgumentError("retained cable-constant samples must be real vectors")) + all(length(values) == ntrials for values in (samples.R, samples.L, samples.C)) || + throw(DimensionMismatch("retained cable-constant samples must match ntrials")) + end + if distributions !== nothing + distributions isa CableConstants || throw( + ArgumentError("cable-constant distributions must be CableConstants or nothing"), + ) + all(value -> value isa HistogramPDF, ( + distributions.R, + distributions.L, + distributions.C + )) || + throw(ArgumentError("cable-constant distributions must contain HistogramPDF values")) + end + surrogate isa CableConstants || throw( + ArgumentError("the cable-constant surrogate must be a CableConstants value"), + ) + return CableConstantsMC{S, Samples, Distributions, Surrogate, T}( + statistics, + samples, + distributions, + surrogate, + Int(ntrials), + confidence + ) +end + +""" + LineParametersMC + +Store Monte Carlo summaries and optional retained data for frequency-dependent +line parameters. `surrogate` is a covariance-preserving [`LineParameters`](@ref) +whose canonical units and basis match this result. """ -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 +struct LineParametersMC{S, Samples, Distributions, Surrogate, T <: Real} + "RLCG summary tensors with dimensions conductor × conductor × frequency." + statistics::RLCG{S} + "Joint RLCG sample tensors, or `nothing`." + samples::Samples + "Marginal RLCG histogram distributions, or `nothing`." + distributions::Distributions + "Covariance-preserving line parameters." + surrogate::Surrogate + "Number of Monte Carlo trials." + ntrials::Int + "Central confidence mass used by the analysis." + confidence::T + + function LineParametersMC{S, Samples, Distributions, Surrogate, T}( + statistics::RLCG{S}, + samples::Samples, + distributions::Distributions, + surrogate::Surrogate, + ntrials::Int, + confidence::T + ) where {S, Samples, Distributions, Surrogate, T <: Real} + return new{S, Samples, Distributions, Surrogate, T}( + statistics, + samples, + distributions, + surrogate, + ntrials, + confidence + ) + end +end + function LineParametersMC( - f::Vector{U}, - stats::NamedTuple, - pdf, + statistics::RLCG{S}, + samples::Samples, + distributions::Distributions, + surrogate::Surrogate, + ntrials::Integer, + confidence::T +) where {S, Samples, Distributions, Surrogate, T <: Real} + ntrials > 0 || throw(ArgumentError("ntrials must be positive")) + 0 < confidence < 1 || throw(ArgumentError("confidence must lie between zero and one")) + all(value -> value isa AbstractArray{<:SampleSummary, 3}, + ( + statistics.R, + statistics.L, + statistics.C, + statistics.G + )) || + throw(ArgumentError("R, L, C, and G statistics must be three-dimensional SampleSummary arrays")) + statistic_size = size(statistics.R) + all(size(values) == statistic_size + for values in (statistics.L, statistics.C, statistics.G)) || + throw(DimensionMismatch("R, L, C, and G statistics must have equal dimensions")) + if samples !== nothing + samples isa RLCG || throw( + ArgumentError("line-parameter samples must be RLCG or nothing"), + ) + all(value -> value isa AbstractArray{<:Real, 4}, ( + samples.R, + samples.L, + samples.C, + samples.G + )) || + throw(ArgumentError("retained R, L, C, and G samples must be real four-dimensional arrays")) + sample_size = size(samples.R) + length(sample_size) == 4 || + throw(DimensionMismatch("retained line-parameter samples must be four-dimensional")) + sample_size[4] == ntrials || throw( + DimensionMismatch("retained line-parameter samples must match ntrials"), + ) + all(size(values) == sample_size for values in (samples.L, samples.C, samples.G)) || + throw(DimensionMismatch("R, L, C, and G samples must have equal dimensions")) + sample_size[1:3] == statistic_size || throw( + DimensionMismatch("sample and statistic dimensions must agree"), + ) + end + if distributions !== nothing + distributions isa RLCG || throw( + ArgumentError("line-parameter distributions must be RLCG or nothing"), + ) + all(value -> value isa AbstractArray{<:HistogramPDF, 3}, + ( + distributions.R, + distributions.L, + distributions.C, + distributions.G + )) || + throw(ArgumentError("R, L, C, and G distributions must be three-dimensional HistogramPDF arrays")) + all(size(values) == statistic_size + for values in ( + distributions.R, + distributions.L, + distributions.C, + distributions.G + )) || throw(DimensionMismatch("distribution and statistic dimensions must agree")) + end + surrogate isa LineParameters || throw( + ArgumentError("the line-parameter surrogate must be a LineParameters value"), + ) + size(surrogate.Z) == statistic_size || throw( + DimensionMismatch("surrogate and statistic dimensions must agree"), + ) + return LineParametersMC{S, Samples, Distributions, Surrogate, T}( + statistics, 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}} + distributions, + surrogate, + Int(ntrials), + confidence + ) +end + +const _CABLE_CONSTANT_QUANTITIES = (:R, :L, :C) +const _LINE_PARAMETER_QUANTITIES = (:R, :L, :C, :G) + +@inline function _quantity(container, quantity::Symbol, supported) + quantity in supported || throw( + ArgumentError("unsupported quantity :$quantity; expected one of $(supported)"), + ) + return getproperty(container, quantity) +end + +@inline _selection(value, ::Tuple{}) = value +@inline _selection(value::AbstractArray{<:Any, 3}, indices::Tuple{Int, Int}) = view( + value, indices[1], indices[2], :) +@inline _selection(value::AbstractArray{<:Any, 3}, indices::Tuple{ + Int, Int, Any}) = value[indices...] +@inline _selection(value::AbstractArray{<:Any, 4}, indices::Tuple{Int, Int}) = view( + value, indices[1], indices[2], :, :) +@inline _selection(value::AbstractArray{<:Any, 4}, indices::Tuple{Int, Int, Any}) = view( + value, indices[1], indices[2], indices[3], :) +@inline _selection(value, indices::Tuple) = getindex(value, indices...) + +function statistics(result::CableConstantsMC, quantity::Symbol, indices...) + _selection(_quantity(result.statistics, quantity, _CABLE_CONSTANT_QUANTITIES), indices) +end +function statistics(result::LineParametersMC, quantity::Symbol, indices...) + _selection(_quantity(result.statistics, quantity, _LINE_PARAMETER_QUANTITIES), indices) +end +statistics(result::Union{CableConstantsMC, LineParametersMC}) = result.statistics + +@inline _summary_field(summary::SampleSummary, field::Symbol) = getproperty(summary, field) +function _summary_field(summaries::AbstractArray, field::Symbol) + map(summary -> getproperty(summary, field), summaries) +end + +function Statistics.mean(result::Union{CableConstantsMC, LineParametersMC}, quantity::Symbol, indices...) + _summary_field(statistics(result, quantity, indices...), :mean) +end +function Statistics.std(result::Union{CableConstantsMC, LineParametersMC}, quantity::Symbol, indices...) + _summary_field(statistics(result, quantity, indices...), :std) +end + +has_samples(result::Union{CableConstantsMC, LineParametersMC}) = result.samples !== nothing +function has_distributions(result::Union{CableConstantsMC, LineParametersMC}) + result.distributions !== nothing +end + +function samples(result::CableConstantsMC, quantity::Symbol, indices...) + has_samples(result) || throw( + ArgumentError("samples were not retained; rerun mc(...; return_samples=true)"), + ) + return _selection(_quantity(result.samples, quantity, _CABLE_CONSTANT_QUANTITIES), indices) +end + +function samples(result::LineParametersMC, quantity::Symbol, indices...) + has_samples(result) || throw( + ArgumentError("samples were not retained; rerun mc(...; return_samples=true)"), + ) + return _selection(_quantity(result.samples, quantity, _LINE_PARAMETER_QUANTITIES), indices) +end + +function distribution(result::CableConstantsMC, quantity::Symbol, indices...) + has_distributions(result) || throw( + ArgumentError("distributions were not retained; rerun mc(...; return_pdf=true)"), + ) + return _selection( + _quantity(result.distributions, quantity, _CABLE_CONSTANT_QUANTITIES), + indices + ) +end + +function distribution(result::LineParametersMC, quantity::Symbol, indices...) + has_distributions(result) || throw( + ArgumentError("distributions were not retained; rerun mc(...; return_pdf=true)"), + ) + return _selection( + _quantity(result.distributions, quantity, _LINE_PARAMETER_QUANTITIES), + indices + ) +end + +surrogate(result::Union{CableConstantsMC, LineParametersMC}) = result.surrogate +basis(::CableConstantsMC) = :per_length +ntrials(result::Union{CableConstantsMC, LineParametersMC}) = result.ntrials +confidence(result::Union{CableConstantsMC, LineParametersMC}) = result.confidence +frequencies(result::LineParametersMC) = frequencies(result.surrogate) +function domain( + ::LineParametersMC{S, + Samples, + Distributions, + Surrogate}, +) where { + S, + Samples, + Distributions, + T, + U, + D, + Basis, + Surrogate <: LineParameters{T, U, D, Basis} +} + D +end +function basis( + ::LineParametersMC{S, + Samples, + Distributions, + Surrogate}, +) where { + S, + Samples, + Distributions, + T, + U, + D, + Basis, + Surrogate <: LineParameters{T, U, D, Basis} +} + Basis +end +nconductors(result::LineParametersMC) = nconductors(result.surrogate) +nfrequencies(result::LineParametersMC) = nfrequencies(result.surrogate) + +function _fixed_quantile(summary::SampleSummary, probability::Real) + probability == 0.05 && return summary.q05 + probability == 0.50 && return summary.q50 + probability == 0.95 && return summary.q95 + return nothing +end + +function StatsBase.quantile( + result::Union{CableConstantsMC, LineParametersMC}, + quantity::Symbol, + probability::Real, + indices... +) + 0 <= probability <= 1 || throw(ArgumentError("probability must lie in [0, 1]")) + summary = statistics(result, quantity, indices...) + if summary isa SampleSummary + fixed = _fixed_quantile(summary, probability) + fixed !== nothing && return fixed + elseif probability in (0.05, 0.50, 0.95) + field = probability == 0.05 ? :q05 : probability == 0.50 ? :q50 : :q95 + return _summary_field(summary, field) + end + + if has_samples(result) + values = samples(result, quantity, indices...) + values isa AbstractVector || throw( + ArgumentError("select one scalar observable before requesting an arbitrary quantile"), + ) + return StatsBase.quantile(values, probability) + elseif has_distributions(result) + value = distribution(result, quantity, indices...) + value isa HistogramPDF || throw( + ArgumentError("select one scalar distribution before requesting an arbitrary quantile"), + ) + return Distributions.quantile(value, probability) + end + throw( + ArgumentError( + "only 0.05, 0.50, and 0.95 are stored; retain samples or distributions for other probabilities", + ), + ) end diff --git a/test/datamodel.jl b/test/datamodel.jl index d3405374..bfbf1d21 100644 --- a/test/datamodel.jl +++ b/test/datamodel.jl @@ -665,24 +665,25 @@ end :cairo ) == :rendered - fig, ax = preview( + cable_plot = preview( cable_design, display_plot = false, display_legend = true ) - @test fig isa CairoMakie.Figure - @test ax isa CairoMakie.Axis - fig, ax = preview( + @test cable_plot isa UIPlot + @test cable_plot.figure isa CairoMakie.Figure + @test only(cable_plot.panels).axis isa CairoMakie.Axis + cable_plot = preview( cable_design, display_plot = false, display_legend = false ) - @test fig isa CairoMakie.Figure - @test ax isa CairoMakie.Axis + @test cable_plot isa UIPlot - fig, ax = preview(cable_system, zoom_factor = 0.5, display_plot = false) - @test fig isa CairoMakie.Figure - @test ax isa CairoMakie.Axis + system_plot = preview(cable_system, zoom_factor = 0.5, display_plot = false) + @test system_plot isa UIPlot + @test system_plot.figure isa CairoMakie.Figure + @test only(system_plot.panels).axis isa CairoMakie.Axis println(" Plotting functions executed without errors.") end diff --git a/test/plotting.jl b/test/plotting.jl new file mode 100644 index 00000000..16b9ba95 --- /dev/null +++ b/test/plotting.jl @@ -0,0 +1,189 @@ +@testitem "PlotBuilder Cairo: golden renders, callbacks, and SVG export" setup = [defaults] begin + if get(ENV, "LINECABLEMODELS_TEST_PLOTTING", "false") != "true" + @test_skip "Cairo plotting gate is disabled" + else + using CairoMakie + using Measurements: measurement + + function pixel_error(current, reference) + size(current) == size(reference) || return Inf + channel_error = abs.(Makie.red.(current) .- Makie.red.(reference)) .+ + abs.(Makie.green.(current) .- Makie.green.(reference)) .+ + abs.(Makie.blue.(current) .- Makie.blue.(reference)) + return sum(channel_error) / (3length(channel_error)) + end + + function test_golden(handle::UIPlot, name; tolerance = 0.015) + reference_path = joinpath(pkgdir(LineCableModels), "test", "reference", "$name.png") + @test isfile(reference_path) + reference = CairoMakie.FileIO.load(reference_path) + current = Makie.colorbuffer(handle.figure) + alternatives = ( + reference, + reverse(reference; dims = 1), + reverse(reference; dims = 2), + reverse(reverse(reference; dims = 1); dims = 2) + ) + @test minimum(pixel_error(current, candidate) for candidate in alternatives) < + tolerance + return nothing + end + + 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 + ) + + rlcg = Makie.plot(parameters; mode = :RLCG, backend = :cairo, display_plot = false) + cartesian = Makie.plot( + parameters; + mode = :ZY, + coord = :cart, + backend = :cairo, + display_plot = false + ) + polar = Makie.plot( + parameters; + mode = :ZY, + coord = :polar, + backend = :cairo, + display_plot = false + ) + @test rlcg isa Vector{UIPlot} + @test cartesian isa Vector{UIPlot} + @test polar isa Vector{UIPlot} + @test length(rlcg) == 4 + @test length(cartesian) == 4 + @test length(polar) == 4 + test_golden(first(rlcg), "line_rlcg") + test_golden(first(cartesian), "line_zy_cartesian") + test_golden(first(polar), "line_zy_polar") + + measurement_parameters = LineParameters( + complex.(measurement.(resistance_values, resistance_values .* 0.05), + measurement.(inductance_values .* omega, inductance_values .* omega .* + 0.05)), + complex.(measurement.(conductance_values, conductance_values .* 0.05), + measurement.(capacitance_values .* omega, capacitance_values .* omega .* + 0.05)), + frequency + ) + measurement_plot = first(Makie.plot( + measurement_parameters; + mode = :RLCG, + backend = :cairo, + display_plot = false + )) + @test length(only(measurement_plot.panels).plots) > + length(only(measurement_plot.page.views).series) + test_golden(measurement_plot, "line_measurements") + + handle = first(rlcg) + handle.controls[:xlog].active[] = true + @test only(handle.panels).axis.xscale[] === Makie.log10 + handle.controls[:ylog].active[] = true + @test only(handle.panels).axis.yscale[] === Makie.log10 + handle.controls[:reset].clicks[] += 1 + @test handle.context.status[] == "Axis limits reset" + legend = handle.controls[:legend] + first_entry = first(last(first(legend.entrygroups[]))) + Makie.toggle_visibility!(first_entry) + @test any(plot_object -> !plot_object.visible[], only(handle.panels).plots) + ui_components = Base.get_extension( + LineCableModels, + :LineCableModelsMakieExt + ).UIComponents + current_page = ui_components._current_page(handle) + current_view = only(current_page.views) + @test current_view.xaxis.scale === :log10 + @test current_view.yaxis.scale === :log10 + @test any(series -> !series.attributes.visible, current_view.series) + @test haskey(current_view.attributes, :limits) + Makie.toggle_visibility!(first_entry) + @test all(plot_object -> plot_object.visible[], only(handle.panels).plots) + + mktempdir() do directory + svg_path = joinpath(directory, "line parameters.svg") + exported = export_svg(handle; path = svg_path) + @test exported == abspath(svg_path) + @test filesize(exported) > 100 + @test occursin(" (index == 1 ? 1 : 0) + for + (index, component) in enumerate(design.components)) + ) + system = LineCableSystem("reference-system", 1000.0, position) + earth = EarthModel(frequency, 100.0, 10.0, 1.0) + system_plot = preview( + system; + earth_model = earth, + backend = :cairo, + display_plot = false + ) + @test system_plot isa UIPlot + @test only(system_plot.page.views).attributes.aspect === :data + test_golden(system_plot, "system_preview"; tolerance = 0.025) + + material_plot = show_material_scale(backend = :cairo, display_plot = false) + @test material_plot isa UIPlot + @test isempty(material_plot.page.views) + @test length(material_plot.page.kwargs.colorbars) == 3 + @test collect(keys(material_plot.controls)) == [:export_svg] + test_golden(material_plot, "material_scale") + end +end diff --git a/test/reference/cable_preview.png b/test/reference/cable_preview.png new file mode 100644 index 0000000000000000000000000000000000000000..e732996cd3f824aacdf18aed32aa3962577478bf GIT binary patch literal 405117 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 V}Oq*as5`rldoU9 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¬g51rXgpzA>+0vThljuv9`m zI_cd62}_$sRE39EqQL+`FKI#ac>q3j7iz7sx(+dx+m0|aQPgtLL3MrR)g{(Ge}Tmd zx|-gQYXY(25^PRL{XRBUUbSm{W!;yRLG&iCT76s{XiKQ!m&(kJ&k$=jJY;z{8?5Kw zjIhLnW>wwGx^#kuHR1g$-_*H}0bOE<)p2bnhvC5~yq2OM0z62r4w{?nnf_e5@fTK0 z{mH>M_Z&D7D31n%;MX2Ca^GZOVWCG2&l&3%#q~fU#4zFEnXGb#U*;&|Cm&29uOO|N zX?-vfEjhFaw8a28VzCi>E!~_uR0XAqDzBYeWTz%2{yu$F@u5yka93CPKE1~{Mwf25+~*n|DNr@O9%BYCdQc=97jZb9GWgL%4w%xYde_~p@k_@P^Dh2tha~B z%ZL?_bWKe|$`L>er!syz{DUZYON0^V<&*(-ur|4FHS$l z5fiHbG}sg`Xp>r>=e*2%o)WRq{CTV}H1=Ne+MPB1+gBM)roY#IS`+52RaYtWsx^y^ zi?gaZByp4dkYp@groTa=X%2B3?>Odg$_rNAiv0aejdGir{d+la>bg z#nk>qby99_DE2L_t*uID3(Jc>&&94S=-|}?KKC^D`2!^yu9vu^M1v3GR5z=FC2R9~ z_eZb;BTT9A2ze=j{stNdo0JrwjYTA7$`awf{UdTRvtUTSwD0viempPwVj>ifOP{tORss8)q3pLO1zqR&W91g?>r3X94^|Sk zKKrqH<7<&xn*WY}e+hvTJn$PghJeP~bQeCHe)l^jB2z0vdwybX1(SVoSzeR0iX2hv z5P9>|_UnmPW(X$21b=TJmZy2v{s>3c8SxuWUU4Vf^ZxbLaZh5Af$~||H`#E`e zU2d(D-ot4ZxE)%f3*%2pT>3cT75CXKz`nriwn&u7_+Em@Z(bi&W(1lEVPQ2hDQeW( zx{Zu%?*R`{fX@6_Tnv_iI%GE9VY}wwz{=N^I}&d^>!A^S!j=)Zc>BoYWCJlh;+GQq z#Xz_iB6tyz6^csA%7=u6!dhF^d2#dk`5R&W()L`-x+;jN`is*zHy_^>U`r6}vD0t` zjobu1vCQJFGoF= z%1ljwRR6wAj(83D`T5sG3CAv+43$kykr7if)x&}}*Ulh}@4iBE;ybv;5qWD&U2~k`x}nd+%U-hk!K~H&utskOw~$XKpYaa%Dl3PC2pX639^;Cx3T59x(%#!Q zp(1mD15~%>At2Rh@_O<%OiDjvonY zDAQd8FB$6^+#8OQCu4p|uY9M$MBFz$;wuuh-h8*-=w-;iwpysZi~#KJ;sxO&W!xu! z<1}9T)xNX=AaWrKswgft5d$DPfFqws7^M~9wZIm%`@D^f&F9&U$Mugl*x{}+fW4fINEgxbuo;)j_&jfSv0V`I?08 zYmXTdRC&Sw;Z(&V^<8DRH|Z-WD_9z)3@PxAJ%8@dAx_kBlF4~$QCaQu8ONXHNnj5$ zYVfhxWpjrgSLNfEX8C`{^EEzziDqV3TH7Zk7ovuA_NgZnlrZHgH{dFOw`ANkCy+DI zmGUt+^cUo+(FK=}vBVp;=jSGNX!=K|bucJD$SDjB4QyWfa*ynq6i;}J(2CB>XSFNx z$E=VO8Z!)`gfxJM2%g2Q028v7p#B~>60S^OL(zG|KS9Kp0}-!P7gIAx`zH1;EEcRY zi2oJzCqhVm8!^Dcp%Uo<*E0Z65_cgeSo%F>XwkA1}I7FL8usP~_pq`;k2V1y_A5g%Aea9k=3yax)cdll$>-c6)97o}ZGq zkG7a*FU!63>>!z@s@axm2`u+zD2<#SY?DZ+Du2@cSEX+3Nw-ajD=tOjfAcUVIo?b@ zd33LTfaB-~$-Q?)7>+ydF|ATHLuU*J7LtO(c5tX$#^d94!}eDA&U9HMpsl20%(d?1 z#~bnhB;k%HSY3dfJqr$?(B=1k+4+DE1U7dC(%*?bxIl;$;o>;xO5kreTS!o*2j(D9 zCzP6I#io-BP@$a#yu{dH=Z-*Uho60?%}B%ApP_49U-$I(^)cRsv&yRX-2oU7na_T& z5jI1raKL`)eb%i@J<`~I?EngZyJ(_)=~5j*h+dz5R~9or=0x?vb8-9n(!I;tcA00U zhZT(NICsq~-8f_EX(7PbH_mv+Crr;*US5?W+NaA|!nGBA2uAx732JK2b5SVg@w8XNzIlCu24)$NPiIc}qA9aOQrum`aG4qfNZZ;g{R*)lYv zfcfXZFPM1Q3IF7W$s2bwHCt0{nc!DpO^7r5*VxALL}H!|%Cw(&{lQ4-w51^rsmjox zV&Tz8&N0es0zVGi=SucZkLnLfpGREtPs51)UvvL!k6{wiiM*`+_@8r^@VPaw; zW&*fDqiHFd05cgkqHk!(M+AGUvZltzzj#EHv#XzE$wyp4$09qoa(I#Y@UK@@mYG%O zE*-sSgi6#C6fb-)Q$6|Nr$Q$E8vfd{iGfdbH)gLYI-ENAuBYciWp&%50cz^|UCX#KeG_f8mLYd78$?hw`Q_gdY3JCO&;tf2I2@OxBdFMa>8ii7kx|1x11Au`}>h9 z5C%&txLhvK($QTXE9m|FdVw#q=E>2^7tgk{)uoJ_c@YCYrSTYvpzjd^;KEPTtLN@WjOV4kN+jlb813w)7oO9TVII+p7 zSj<)&)o@dx7#m1wx}dFn4}^&YgLm!>5c6V((80<1%g%FcMiI%Kce()ZjD8WJu--?@ zQZxFduAS%VF%{mXO8;@ePtBZ?0WwWSr!1vKL=a zjNJQ_-I+^15|`WNm&*^adk4K_pvI2*GUV<2f7)``0JXFO5xFh>?nwxDCU<#xXBj0a zWmw_q_Y{-Fyqx6MnautVuF}ZPS<_O#UjW_o`i$OYx}njO8$UW(tlP5{Xz8uM zwLpTsR|P;;7r1CzeSEH!0R{>m_`S&~*KRqQj)qnelPmafQ1gk7FV74jR;K^^cW)8Q zbcN&Dy9u+`oh*`3zs|=?Dy4l4(oJrRJDKzQE>#W!n|l3wj)@ix&yT4cF*0y;MYiVV?FV|Inpq-Uc{DwL2sJK!$IZ3)|x zVyGzm{$gd-%ms#}a`x1}&#X`lQ8&C)WYrI;s#;t7cXUL|#!!sSop3&w(cXRkd$A^O z@y@9bqq~)#icP$SM<$<>@mSvPsM)4rE=gx)5w_zo%>xk%DyqS8`$ZFN$g*J5gp6_B zWMUaH)h&+JMbSesh&lCACwJyWO-*{@0EIzj5ebpHifMVK(1yIyY!Yw_DwFe_!|6B( zO^gS8E&)$Ov+kZsPGLlD8F-UN@ax#pw_B{D<9Ix8jDMlE-uEem5}(=_42pK7ev`nz z@)4ofMTuRN51ew);?fJ~Umozz(M`Vp)1kE>Wnh}&4k;^rw}hg_^Sm~z`vn=8(+wH~ z%3314!UX$m9S%#2GUVZBw)0lk?!V%Y_HIn?@OC#hv+xJ0Kz#m(`?3Go5WGU?N%*Q< z-R>r3?s8>Y`$)0jUR)n5&?p6b7i)d!ehB4I%W12! z(dkEX`~eB+ewYLg>=!rL-*J|s&e_c+EH?HWrbQ^bmseMe17RpvQ{qm$v58IhQ4Mux za%Tq~Si7OwtQfWR_F;H^lOVJf^xM+50CSA0$B#;!1X=M)|TKPh&xR;%ymRD!| zbO`g@#0&qf>$^#RXlsunA|9TG;ijt6i?;rOi2#zTF(+NFte<}zO>SmtPN<^>nsZr! zn)apDE=ux-3@u%5>U@1QcM=`lZpJOHB;Wk&*RBy+*Lg@mDK0L4sf~9Mu_`kS|!UsNCi{Jf&2Wp5tn3(~*mH9S{F74_as zeS6bCFaV;T+kR0yWCH6?A6rP ziaiA1hDP4;d5b2Oovn_C@u;{!_^NOy$`+8r?RAd`8iO*nh)`>tUGQ)AuWZAT2N@Yh zJ8GeBfRqwuT^BPxb>H`=7YKd$@bt9d(q%s2dGBXObOgQD-Roc5dnM#sYn$>B-H=d| zq$0Ne*tK;N88DBO8L)H7e8^k3f9K-&p`rfKQ9pUy5xq-%9m>fq7f2)}$hmpK14P2K zc-$vT#1q_hP(maGVH^m`C=11zbE@$ka~5+;0DZuYU= z{Ufk~P(U=PuyB9ed0!Es&rIC>{8i9a;Pvr_A^%aOP{HVP=xhS2^fF0VEoVba|60)c z`cXglG#7^KF~;@CoLBoUh2MJ+ql_t}wPdTJ}0%(D!FpH)z zW^*Oy?xBneJg_Ny{ZT}?Nf!D?xHIE+;=F;5MK=R{A^dg$fhSKwwRDBTd10Se5lF!G z&Y%%w`hhkveM`V?X(}Z*=?+MLCbqwpChNfW)+H`Mxur0u`7h0LmF4jWEOdSOf)~Os zA7guYRn_(qLp3OnIx?TW@`ewd&8OE`b6<|oGVs2Aucp24`;W6_`PSqOTpM}}uDdok zjmzbxmDNu(n&XlG(+gbr`9wN7olb|#0@V0)j#sf6T29B=k1?Q^(^D9nLuc zebPkF97i~9IObYfYeZ!4!y-tDcr$U9V z;X9J@Z*fslQ>pH4q7o7i5XjAaVUm@bn>VvKnyW;3B_gsVqG;CRjnEQ7SO4iN-TTt49lfaRH)JFU@+-NA3A1=Q5=IYC?z>EM=- zU_xYAy}O6USyk0!1BopisknmpZXv|n8*dz|B;`*s*4p7JcaWoUn=t$P>bPXCy)hRj zQqFwYcI!Qc^veX4o+n5tV`0Q7m#*2L2yslsdTeSdY%dw9sE`LA%{ zGr6DCjl6Zl08jOWT&AYZZU%)qQ`Oyl^WH2yV>;N}pqO^>h>J&uG+!8VbaXt-#YJze zrLTTRIR)++4u`SmR3Rb~6@Uc;FMz)y!rQFdQ=P1de}U#Y@QUF6XV0G}A)5t9WIhbZ z2LS@0Xk9RJI7^P>P&XOv9dv7OIr_khs87f;@B#k$MegG9_wSR}mrKc|tEG3!UE8#4 z#-a6)@QXUXnvd7%1FEjp+gx4QFrzc?ADQqcf#Hr#=ar`TZ|v;)We~=jnklBLx`kl! z`YVNZ@JY+zqgMj*73y>SUr<~Atg0#7i{UEe$`#@8qyZBMwj2ca|5vV<|0!2Ws%mKj zFQtEaGLYnc{t%tv?)bB3xx;ZeOAg-swJ%%?{8q+5mP8l0$Hjg5xS*fRNV&t$A^1Z5 z$(sS+^=)_&h*$9?Hc(0tL-aA&1wq`2C4yf;0e#1CYl@gc0JFS@YY7X0UGUG&pFgkr zc>dLmwcl%n7b69XIJvp^Skvz53(l#!pHH!K=RxW9@e6uf5aswMsDr6Fs6mq z5v@q#JDdPBb85aX6^HurIB6 zJN>S%)5ya|R=qFPku8s#DewE8eQ#D32F51?)83MyE45TsI&$QOP4EA}M7BTO!lPFK zH)3Mo!Hrg{p5M%#{uN@8{RQ3VSa5S@z6^bdAzuZi~#k$mpyem05rP^(&>d zb2G5~3qdPEK6W9{(%w7hUownAvz&&^z|3`zejvGj?jA;EV@hM@`vg= z|5pJ!y@xe?Dg58G9v`dYFIL7925b-GwyJ!rQ38FGQ{Z@GI6vh<1?5FtV#U4LhRC-L zqJP;MM_U?Ibug>N=VmtDFvuhzJA`I#ngSo-S?hAl0d?`OKhN5s!xFPPc>eKmzOmingK?{%<4hCA1QxI!0>d zK1Dr+?Xq)6GBV4`H8lduI^vTanAGP|^LC6^xo~oKIO&~}+y8fUO3z~i0gMA*| zT{0?j%!rxpf7&R2kP9Ae+Oi#nL0+^pG!@mX(|r$p(@bqWrJNq@py9ff%u`!OBwZav z;c&04 zeHI5c>M9%kh^t5)`r;=R7P+={OlzXp1%HkueTWaPY-scOb@f>P=v0tjYGibr&%aNh z#UC)*?w_a7Is=A47xXkvd+_U#JyMRYdVWG%L)XyT=XA<8o|k8p>trnr8y6X$gx-s? zv3Vv?f7tsMr&>(?l#~%x2Ns!?_)V@!rGGp9-o3X=O5)qnHlAptoz1Kibz7R9Z)<7} zahooQB$SPe$$P_jP5HoaQoy+c_q7p)=yROxJ2Fx(oYfWbkuBHMsSbSL=f?&r90y0r zkA;(&@)&-JVrB5wwJikXmE6$=Q6#Q!?ilz`V>Gik!zm~jg6}3CP7BsiV%H=)OqQmk z&cz5v|GJ~UC0#A|lY>-tkRGm9eE40*XPYP$nGVqD@N?|xnjyIB8=^M1eHuMt=Rzh3LfddZI#r<~}vbCwHuiao} zU!R?mW5lYewS5yKqt*pY%?n#-xYw@_*rKuRO}fM2%IRN9eG899JtQ+|pROPI0oxIA zORC;tbLq8{hY#z+f<+l39v`-Y{>w2T^c;jsW(TvNA=MWty9dh?s;n~0SF2lkvRe_T zOAWPP8lu%me7pPJo@!jl1Bn{w` z1=7q$m9_Hf$|Kj4i>Y{3txR4SQ?XUu>a?)1AoNSU38LFrUt~18yf1WR0eQx~ChgA~ zR8-u>$ue#Su?%MRKKthGG`xu+fThH{3_n`>eXCC9%=|jnh(o`=!=hiqG8hd98pqla zTrL{=)${km!y_BdNG<&JQU)PaJ$2t&t?2hp!|K&@!|mZ%xjG#F=cAl6c`=8Y*_$gf zGwq;PN_^4d_x12!CS2Q}nT#&Jt`!&jw9k1cvc6sk9R%4vU(PyTnjv|+>h)diJ^L5k z-8++<&xM7DDYtQ4;8?w3VHteyo{ZEP5vk&P-xcP5`)7Pw{`IBxsA`K^s4AS`5NJE#196v+h(fu>@oVmP>tbHfT3# z+wu294eFeJjuwmXg>hdA*P>nG0h(HRQ|G>M@85m=_;E{B5NQdvrv9yiEyKe%8pZaL zf|hn&CQsybt){{b#IWRq>~*>oD(rTw?>oJ|0GcB$cI1}!{bklrW?#2H5`x{lp7jnk zw$=}_X9tsR?1_F-PzE$k4;#L1P|(SCOS_vE3=i@R$q8U{b0K-?5Mm2w<%%a!Gc$NtrRw7p6hYSz`>pA`tN29Oz6y5`o$$Io;talBAT_I5Ssw@zday zt3c@E*s(~0jfuk7&%N+D)Uf_G*=__2{Z82M`j{vxD*AiOiMVF>_wR3P;(21jB`8RZ z+=0O%tw_yac`yLZNbrU;`G4A}qL?Rg{IEU)r8XP=H>N%l#t*ozVGl@Tiy7fuThWKDK&Y zXhOKQ{2Z5z+ULE>zLvC&zG;o`2m?R=wtbrxPrQhj_VM%i7Z<5zV(=wC(aT_fu+|o4sYXay9lkQvMNc}vc95oMF$5I zS0n5+s60V?E;8)Vv8*%`fult%pLtuSva1T;@ffBCya@x2&Rk*)MKLeV1g++p=ETOg z-8))*)1CLOcgwQek?VBtpU^8wTxn2wdPO0j@rg}2A-quh@HQTrN(%e^`}YaHJVD>c z4!QD#EM;IO^2=WSz8Vj9dHnqLT`9p3VX+^$pKmcx!Mty}`}Ek`s?q*h->s!huU}&u z-!^>lhrq|rU4xUY5s7DBlyPZlhOPR{GX8xU?n^&L-H=NRigcf(<4f|hMDgzT9mnsO z6!8UjePfdY$#zS%>nI;TV%;0?4y}BD^qTN9XynG^SU$l=xJu$d47~gJQTyYYHM}IZ zc621-NCr2giBE%;rv?k3WjPn`y8t~}Y`Bi-_|Y(m=bPm6OqRCI><{0)5|aMru$jev z78Tnj9GQkAaBaSF1p$1s@bJ_v_F_dB3mw3FS6A4cAv%vZ~JMz6|U-*T-@M!qEZ=Zaf)CD_QQg zWrb(I424^5gXyODy+!j6wiKhAr)ArhW8HL}tjc%Xs#H4GypWeR@CL1Wa%Amy?n$EM z`QHc8eVQt+&+`N=3!`CNTc_!{!+ddZrx+Gu@%k0tlj2Q2H@#aZ+$@&ZSnAZuNW&e%=^1=? zD%X^S=Hsdir=YCUA)D}$8i<+1JZ;Z*#3RE zUG4El`!;qZlng>F!3Jt?U}dFrsY-z{L0wvE*vzs0!Fh&k^&2^yeqVo#deYCdGzLd3 z!wJ-f9nQ9ys7W`)Pe+MC(fdLaZ43@WB`ihVJ0RgYZ0sq64=)d|SS{`SFa64g57Q$u z4{bCGJ7GKu4t@LJ*oV@-f3{XRz2>ea{8JUYK9sq?t|kgkfD3w3i^q#wf&5K*$vD0Y z5}$sadoudgr=iER5F^rL$)A2c@jYFMfS)mb9LF-X5qFB?t2+#!QqLQ) zIOE(#SEF+c@iUz^lnF_JZ!yi{NDF9uW@B-t{ZS2W9 z^sPaal}tBS_vPGvTkf$7cCHhE6#$61M`J>22? zUPDWYh5pQ|T;WtXm)qoaMrLNbhvN2~5E;&V`(Z{js2ARHB>20QY1@=pBUG(N5hYNeT3dkazB6)1yXH zbA&I^)|d?gt|aSmHV33P`-;jOB%);S>)mkX-vPIL?8UJ~O8c4S5i&QX+x?~r@pPCl zEco~zagS%W=_J_j-S~4%K$hs3vez5>BzwjObPj#8Xe&Uc;)VqExA)I=d>*_&@Dy;a zR*A^#d?vm{7&_v%3Wfbo4GhO%joJOO!cO~o^zN49OOKBN$R35Vy?Z`3d->5 zGQ4XckHQdvNi<^T^L&G*>&!Bb>PHD9Ntj>BfL}a$9}mEw&{*(kB_<>Ylb_V5O)+qU^)NWe*Y=rzJ$&r2dn_;v2*=FZ*^E*ADkL&xwv z5)3K~7cJ>Zb5a+lc43pXsD0uD>*+3f^61(IfEYAz#BqPtVjTG=#8BB(XOpPPnqiS= zk`bkiQ(H35+{}6R0B@Y|G2xqc;+-g6?c`9MP*Pyeo2!=j{EA$+YT?Cq{XcE({M=t+ z#-xU`1tedZle}>~p_PFfSS*{VZ0+*pVwL$4|K1*CdriHmKWz+)iF!Od!qhpG=%ifEL0p1`N7T>-;T`KARYBzH2o&;|t#A}m$SOV&0DC+)M`|UWK zcc>##G}E|%G8qD^LkxA#Q11rneq97-O5FvZ+7FyZ4(zz>(B*ob8I20C0jgnMXV1>B zf2$d%SFQmUF&p1R?!?HFA%SAmP;WMwwH((#u=+qDpjI^z7 zSj&!3;d8_Memx_DD!s z9=N}!0pw8|o|VTKvIQ7o$ zomo4HWj0LHEZK-*F&) zUijW8aXg5DsJPs7)=BVt^Tdyo5R8eDP62;kDnK@`Ve73Is28Q6?Z77W!aZH5PD^YC!Goa5^9!cEbW z32bsMh6RSIG}$fM2`>Tn2_3)Nd8NptXeUH4cC!`lBiW_H8)rVeKD{zK-BCPR7Asd^ z(-Lm+;QTfCx59Jm!ivpL2b9yr2zl9f$OAlncADdbR|oOj#|YK884`lop6t#1`swx~ zLjyjEf=4VRQ@a2(P`;R!zK0i07SfVx1tG41=WPY#B7Q$+)jyhwvVbuSe`PsQSU>I% zK3;DB*$>$X&tSQYUHI|$XC|&{Ws-Pb>s(LzN;0lZ(~7+wQ~&biatN*+Xi1jI8lV&(Lf*l-y!Dme}X4A_M)Jp(kg3`18=!wwM|3Zf^It9io`_r*fQXFCKf?gp_7r-Z+$m~tIGE;r``I6J zzuQ9%vJn+M{w!V?)(0apfYE?Tw18(hGnC6#axKEWsNixAy6 zARc~k;x2%7zZ5}{mph}AF>+|O_t7q*_$J_LA$qUJHaj>FXheePzl)AlNP zje#lgYRr}J9iq?beRpzPkFP9Ys%b%<1C|UPu#Z8**AnnV72Wl%sLzA%CXBB2AzA#X zzS3#?zPrPF4tb~HqjAF@1gg?@7H|&{_yLSd5*BuZ8(aP5OO6wx*n8``)GW1f;3m7q zLAMuL^XQt3Kg56U`!R>tI=Zj|3eU@(=fY(+xl5BmSRI{vdymKf&$ax09U9ZCbHj>A zH;Eg>O4j_5%b|~NTyx;AJ@n9PE6&ei6Si0drX{R|)0ATzz2LT|`bkG`u*wqk`Hwb$ ziv8Z7b{pkgep*}OA+U{jXW#L(5{X&u^l&!A+==r_#=F|tdg)^CqvQI3YunEu2@mAS zFq6^C>Pu6iNb=g$6260{9%xt@r$h{4ZSFiE_0a2!X1R^3<3QF%T}5XdAZTp`fGCL$ zD~esIAvU}9&hb*Q(^u+}i1RTDrKP>9};^7*$#K$bXqeX~YkTv%$gNK%s_{68N zCw0HBa7{+f^iz3yuQsWbGzkfDg#%8Q?00f5lgdxVF8E>`LyeA_<<_nA{|X8Ii};d+ z6jEvv?8|TW&1zKV#3kQ25u(G#UkTDMz%oL345Oz%JL8xm;`Q#XXDp+M2tC@SMCZM! zBCCguGwcd;x!05~VE_ox3n9_QH|hq5ujjW8=#Mp)`hNe!g4UpAV1PQ^G{nSHXB%9b z5g}*L*b(so@H-GKDD2>HxOw9Zk|v%8v4lgr{nyuUpNoB48N*GnQ>+;@?*$FAsP5hs z!y~ZD?u%=1-p9nwbYwolYVYsgp^q;7nSAToC$Zak0sE?kfQ>RvueRXSE5C!;2cVZ4 zJV1DEXT7=T=Rdiew?#-@h*DD1XNbp_3*Pj6fY5S6BVZ zZtm_j03rzwG)N)lEz^JAbM8rtVt;Fd_X$P3ROry5bexT+b6)D0baV54$78DlmeV)WbKp$9U9NU)3V0Gsu#H|25^C>HW^fqmz;}ohL6u(%G>;Y1@_+er}=b zHr{^4U$tmW4XAr2gLL>>Z|{pSSJCuq9Xkr*lzzF*kb3~G!|1%6dV;x zjB}R6Vbj84<7on^aJxVX6131gQ4zfM8Z;@#kQH4-TT zFrp%2h_Sn^@%tv!S9*FH)eMuMB-{hTA|jgB1K~rb9A5L_*h5c8R#_L7k`qc2wcfbGY~NE9Bc$I?_HaW;;%zjsgBcTNkM zxAytTcBZ=)HH~PjmOnq$RS5{Bjt=FNmEA#ICP_>Mw;wpTedu$20_XFB6IDV^RVj_I zCXa*ZI$>TWm6MkTSm5ZfW82-_+{}>SGxq6IBp^Kex!Ni6dEmV#bWJ1gD+H}FG49md zD1ONFL=8>RgB%~j-ri$vlVqHQrrY9d{mq+DV1Q^`TXB|v zfP(*ck+2h6G`O(u?rCkp%1i^cqO_wW<{dcpsA=O^SPs?t#sMu_`Z{ZbD;X~huc#<( zTu)GX-60nj7i^9%c;tNp z6y=5G1zQODfw%Ar3NqAJ4rcML&8*?}9pzk6G)BZY>C69I&6ZChZ}g62ShL*`6dWO5}KSL0|GJ`F!g z1m9zEyk#ps^w1kOluJx)&z>cO2m1Q@&k{tniARf~vC=g>%nlE{x7DuQ^%(G0VL3_s z^gJQ-OOvxkFi#A-aihVe+zLw%rZ2=>j7K9`@ikeyQUE0 zrv(-Z6(}6vcq`$in1L&H9 zq!f6yZfPvqg4Y?dppXb?*H#d$|6bF<~^<H0F zTHNAEdrvH8!m(NNJ>BDT#-*kk>nx+O`9YHcw>ea}fx5H&(hkojaUNe4EGdefyiXX& z;FUCrA@TEjAkydim(gwzEi^PjK{yC8%+-1~H>G9%<>k!16PPVm{IaZdmUVXZq@r@)w-V&M{Z z@ylMj4nLzegKWU43r$MkAkuo~takW-j=2GvcsJh?mIJ20z>EpPv6}$ zGGcfB!i8+LZ-sc$&a|5-MuvFXctu1aVS4Xo^+ypSQCQEJ9B}*YIrd&hn+#EcfW}^> z%blo;@}BdYXwM>3Q`vB`S~@EC+tq2KI7;qzj)@5kyTOa$VQkDw4Ca?uK5!r84%mcM zMA4fPVv$W;v?NT?d-k8dN+fuFcqNFgYsV#j0fWY^aMtvHmZ+NFo7r8Wd4ap=c*&-VYnd9;9B>X`u4Li*BfvaM=H(LI zD4c5%W|=*0LL!lfj3qdi=wVAp}I-rCe)+1b)CR()7iBa%|8I_zcf$@GgP;COK zbpNwxC`?E#TmAV_Rxd;T2>MkNKf=c|T4^nbxP(sQ z5;8}`5Ii!`JgRggbR|pqa-%&~1z(JbxRyQ*EbWo^-c~u#85*0t;5r;Go_FQCN+ze$ z!7FQN`q#Hj3{-OBtk!({rK@nyRefyL7x;`jH{OS>N|uw$3vn1BDjmKkUVd~rJy-j2 zUMRc>Fe6BRDca~fe4ia(S3gng-N(V3$mkfKoz>J!>axGP>K@;l-%XJIQ5OKxW`ds! z{q8&;IwJTVVTC?jud1fT_`qtN@bMBTmtRz6<6`|>f62iwCYC&@5zZ*1>%I5Sc! zucsH~zu0bt6ys~AMZEcYoIk~CO1%`FC5P(E@hrGIEW}>KJop z!#xAM?c_5zoust2bol}e3rMA^tw;Y)gT;*OPz*us=Kx}Zz{0hqlt+j!r8dv7ux)!P zv$p+-O*K;+L#FbI>hdEh z-26y3GKb^{nPRt1OHy>wrNXeDP^XYAP2A||{Wg8Q?Qoc0MdirA{d1ciKYnanYIy@A zvO$fz-Qr7lIXwaJE0LeH^1zj2r`R%0x?XD$p_Ic8t?RdcavY@*KI%!b9(+uEI%DwY zA)@FsC`T=8E_~TbI3`19efwf{^nSl(P7IH=LI94zdMf}EVnT_%^>tBw#pNb7BxrU4 zje+N;2mqnUcsLTR->|_PKqOIK=hDn^Da7>|;JFg(;0(n&pS4x_)M{tDuD4!$6y1Xc zq#a(?)`^tR?qEs zA)U+P9tz%PqDh*Q@uPQ7J2Z#(1YN(*xcx=?<4b-OqMoHOe!I{8u)7LMp6RrCF zNT0&!@wBfmQT*|803)wf%<-4Ky%;FBW?Wg5r=$B0`c>+;%SB*l)C#GqZ^m-$2*|i- z%N$16es5!u+Kpxf88J`5nyB@}MF=T=BbvGyK%CRvx6qVcJDzJ=RB*a}FY&>>y{T#O zV4eqfjTJgy0!jwcfY@$`C6k);T-AL*5XtJIPV$it7NO<3A{g(~IYpMznB6I*tr5ll z2-Q*{IHi-(kkWSBSfK4}kEG;(x8j@rvlYkW8=DA+3ittTD{n6cIZX@n2eB3l5J~>z zYP}CXSo>$YxVCT1@*hn{N3(XaR?w>LJz36JHPgm8n=0r&h)>HHFYxzj(6dJW@eBYTtF{^OtV8t{}5yLKn8idr>R+1ZG$!T5se!_VVGp zVclSS&v8N@BvztwpMIX+3Ko=Exy?pcNWIt9($E-#ILfTvdINT%wo@%jcxvt^K|a1? zS;q=~rro=~2pDH>!zG-HnvLkG)O#S|5VH1Rg)d7@*Xo#m%yU+QaS{n8Bf)ei9xRJx z$X2U7Q_kN}Z>~ixEMbzHR&X`MRqip}H9eNuB=cj*Ztb`S#BGy&niQW>?{ftG1kHn#(s>5EXE(V( z8VA_7#0QQW0u7o7CyY1us9U41 z+@YD1Wm<$;T&;~ms)kY6`~+)q3vX-ODGKR`=y4t{>jstUK{u(S1mbB(nvFK)F~_rP zZT@p#Zy%whq00P6lJy1(Alwt0Q<13#gbU-+PGuhxL>o`?0xQ|X#wK-?|EZPTi`{K8 zHn^?oePQbPFJkX%isFa;VNDkb2@+9=x)l1ZX$FaWfG^}qEQb~L^jC-ACLf=k?zpX- zA>sAG7@%y;XI!!<1%WBDg&vE`27qCGdU1hmNmGhOmo9aAoJ0h^V)7x7kQz~pMi7J& zqx9obRcYX=&*wQ-ols=ifAwzqfrXW?EXzNua_~QkcT2OzakMB$ImosZo{mHJIf^la??}~x~h2wiPV(PNY*|$Mh z#$}?5MMB{&+?D*qi+TWVEwF)oGFmU|pr6tFRINMJ^vz)E>(RTDjlmoQT^bmV18O27 zzZUD0Jk^R(Dg>jViud;qFnt}DS6Pc8$^X&N<*j@$=~JOiX^-h36Pxf(O>(GiI8UHu zsd~|C%ru8B!H@hu4PXw!fY5lw%C(TC+Fh049CkW`UV&3373$2XZxb~IQC8T#}Za( zi~P%v+8-nh`TR_EjMATANoT&Q?toL|NC z*goya(PRSMDhy`vHFn5u@BH&Yc&aP?2r!|xvlxaaL4EVD^AQA(&I&ietJ2dML+-OL zdBNd18uI{*$0Q=4dahEhHr(TvxLD2@XGu&?yh%seJ> zX6zsTR9U;3`PekYODVwHK-_Pqb#!#Z8X7hi7s{MkeMQ73^pIN>NT*I(DlU{6`22W*CDj7rkCoxTSb094ENU}2H~<^!z-AEEHmTV6 z8Qga^w^YZ{0rT+H)M^b9;3NsD|k?PFjt%%DG^4KfgSi_L}KEui{4+zi#HvC_eK*RX|b84rMimB zdZ0yoVqytAA(z1sjy;WDr_!N|+&ywh#$_l8NjyCuH&ocXWGUCu)IU=4nv!<&Ne2`! z0E|v3{Nd*f?i{C#w`0W|t6chqVec-z>vw{7OZC6qjX>V*;F@FfCsgM6^>;>9!992$ z4mT*QCXn$HPxEQB$v`rYH$?u3F) zatt$qW$=J=9(w+x>Z;`zr@b-9ZB&MZV}H#hw&4waKyAB#qWhW<@$1AmFu3X!DfM42 zzm#q3wS1Grw@ zJT9~jJ`%y>A$XAoFMf{%b|K|7dH&&rU!ehT*_j`>Cx|jz>E9wmSS`hJt1g3qPXMvY zNDh^UR11kGBtAZv-e|?s0PX}z4jx#R{%ExMV;f$e7exF9h`S&AE{9;3%{l+=+;#jJ zD1vy!#G+t|X89W4j8J`2Uwk|o1|zzS*Fr)L;NM$Xe{UvgJskbeZ%vyJTf?2AC+1h~ z3F1p$4>$}J3v^RZnz5isCwMh5^qCjJgzku`(#7R2(tUPgOj3Sv_Z-{M@X(emGYybW zI3npx-1gM~C0VP4S6=X1V!7E>a-$EJZ)rH z-~o0`zNbY?(a8c~Yv}Q%)PyzmB*{xcx~Vd1d~vZ{eESkiM(@9T@0_;Whry|c&l@@@7KZOyywSmNspBPDFvA9bpzRFF?IhKHUZJ65R% zRj9gEP9{)i{v2W3k=LbXmy}$2Me$GT${NS?W&OSOto2`hKkr-_`mrSMB?T+(+lz4&pq*YzLMqIggq z-103?K2EdiNLXn2#TFJ7l_x_@Uk$rZp-qPtNGvQYFeuxkNqO*>%ms1)@*}uE5Aqwn zjbxP%C@Yg72$lqE4o$=ZCcXSPfEaU8C0!97mT=_Oiu?7YKKer6251qQ%M(wI+qSGKk^4=of{{c8#CG!wEn6 zxanG#{l9B%iG4eix2zMdonMdP<*B{_Gt?`Z=;s)M#uE|~)!mFtn7&J}U6zkZr83jq zN!EPois5wV!ssP2D=g?V{lrgS8F_qPd;LWmy@G0;IfgE^-+5T>xfMo z+)i)bq1&_n$%As+fqQNna3g2Q7@EZAYVV9okVKxBI?}id!#@xfyhI93=S-~UmpAb* z3lX|HIrY95OqR#X<6FVS6DY(K{_dNfk6jNVvudWy!(Ve|Z%R(-j$V#NbeJwAg@T2c zFbH|X{mW&xEZ3;-Nrd+mS5&%NPBo%){whKZ|{SiNf$=06DpU;;Dqs3TsPfnh1ox zwows~r^`=0Fa5_x+Pr42twFpmzBN6D%#zx!^@O(7Yt{L_gig4Mo5#bne{q1P`P^H$ ztk6#^S2+(*FdQEPwnLRk!VhdXWUR{9$$EX1wpDk1F&{*jO!NJ=(F^#;4ND{9eSYoq z9$^Js0FnO*8PvS0TJ?{@I=%eOnUiLnG-L0*{VR@^Rot(jdIX{+TqQ(&9K~o^hqN8@ z1ZcD^F9pvD)|uTq!+?pU`96e0ldm>8;h}34QGEL!e&E5}>*Ae)z(u z-Qf4npMIWaL;w4-7_&?5%G0xGUt(nm1byv9X z`6*5Z3z7Ay+ABkFlWc5G?&Wxq@zQ1fwr9naPJ0ZMP!+RlQkI2FMNdD+e!Ks^opfQL zVMK_NUf?Mo^_qPind`tTe0+BH;CIes##x*TM(77fqenDEXvcLm~ zN_Hb46qm^~+KM}cTFU;49B4Nt+PMNj1Cr0Yt&@CyAIBHPpXp|+q6~3^&(gO(4PB1r4G_zN=_Jqz=h3o|0V01ab3E5qoC2;Q zVF!bw!8{cq{&?(lOT@}@Ha#|&K>?FFdTGlago!v^w5nUmPJW{5CDV}iwQu2VeZR0> z$HaKOg{9mDc&&66hW(S@9hmXjMmfPQJhgZBUy7lG;tpkj_n;L}NO+t;6xFtC89wI8 zu;JH44Uh2*FlWYp^5Ea&vUwd}3)KVawyq1@H}}K|8EmI#ZD+y&>~4A!~1@1JvDZ5cVg_4TS`T8E`^XDdVkhJJWLJysXpSJ z$W~L-^{?{_qC|d`A#x4T5fQ0Q8{u0{b;@BT?#LG23#05EsF@o2`otrH6Vzu=&nO_0 zCP#bE9X2sh3Dz|6{r37e_?-vRDHN=y-h0PBH@72T($YZMZFm^VXhFqE9JQCt-ZU!e}=i)*>Jm7RLA>G zI#fyI;D4%HThh^0RXXt{k=ijEp_o3voAMvtAJv2rLy)1K0W$<*Z8*?Pn0Xyv++K%w zCrxZui(X&3x#-M8J&B_B08;x9bx5n`RddrV35gle$K092*86&LtfP>uWz83$NE{_T zzmI)&frW)ZL@-^2*PyqQv~;qB_4;Zczq>clYfaBRvZa z$f_-dYv?)HJ3jVb1qzRss@(yjhL7hK>r*0|2R_pe(%*L^Fzj{72WdWtRn@?3{-uTA z_c?8R-~}M%R4YJWK?v)9wCC_~od-Q>H0-l6Yva8`mL|RaIc_bdZ2x zhaR{Qnbd!c2faSN$~0=F3&OqIyt?AyYttqHdJqA}zT0`AVd!CWz#SSDYWIgjSHu1> zF{M85cZz62$_4lWW+_p4IVdEe1NomSwtc-?^xm%9%7KJtA8-j?W86v)hFro^qoUev zl&8Bp!|@dagxJ{=TUDKYmTA%`kDm1^CiRamnf;jaLl)--!rE9Oq^SV~&S&DAU0a2Y zKzLYzo|>N!D3gSZ=aAFSud~}`MiYsn9S&l4u(>o#Qi3vgDdmHoseYl6uJK=CEGUR( zD%?cBxA`Y6+sb5|KC<6P{g@Y$4>{05|HEngy5wZY&Ar3P@`w?|&eRmDAN&3n`~`sL zGBr$0Ze@JiW812m12!OzYd{HB0_GHr*`D-cvWv6%K6i9aLSIBsj;Or>BN02rxoB_3 zLPl1(DM*xTkVlM~wz<;jX-?Ao^1zOsV%^vsOz!TBT=XB{(zuHA^`GT!j@rk;W4ZoDQ zZVr3(og#UCXhX^oaa&M-AM)sB`PI1_MKMz=!H!RNPdAu<8k(9o9P3B?mvkW-(1;77 zQW+w?9);{LM^X<=6pG9;hSNSgPw|E@9z_ z#OZF{o3gHNBKzM~nO8aO0J6aQWO_-`V@hYL{wp_y#8BwGk1bpMP?tydp;ka1)cgMA zCTeKT61FH?>?T^HGEsAH`A)H5T}@rt-`4(PYeOD^wTR&O*qb+lGP%+X@4mW=-w$J`uqb2p91wpWW{vZot^6sK1>YI zn8Cy`I!j6t(xmc{owYrNE0n2{h9vs-0fX}uUzT~TDJk-?uSaAS9kj?pc zcy7G!NbG&+b#`^1>xd4)s66#(pPGaKPkNMQdh(%*beKfZ&y=GdrE|D+ok8Z@&wsoh zrGpF^o0Jqfx@gz2`)2HQ1~39PDUM|HSzaW*-oy_i*1gEK z?p!ywMYIHvW|^DcxiMJDo`gnPuIlWl>gTsrlEe~e;c2St0*26-jErvNpDq8&|0Nmz zyj`<1Y2{#|*ePx#w?j+wqFUcIlDI$)eziJFL_WlD9qGS+j;$+2@h}E-iEzbBe&vY5 zA*2?r^}nA&1CgZkMKzK;SN+1y+Eq>nZE|#yrGR~;fGKFpOmOb411}CBgdQ0`WDpog z_s?o$*#7%HRp2XO-Ms>?{YtCSpY{(XuC~ko;4=S z*@3wg-9qMi^4`OUQx_G+cZDuMa5cI2W260j17-@z)>M~j$&t0N7#&1Q*Tlkq1_w^! z%-?hemVxhiXZ>+Y-(W0;R0goFB=$K3HM0SBS2}PRz>>VxscP}hI86L_cz6Qw7ZOg& zx{jp93m+#WdNdWtT_s!*U<0mPhCT}MxX%%lS$?8qmEWUg2nEY@zE|dk72iNm(BIo4 zKvUrg<%=htPo-wA#9rJL=8~1ZOZE!<{MID!7q;8pVrfO)+3)vNfcgd-1B*FBcG-!N zduIm|z?!2#Py^D4}JsJm?SIyH) zTpdyv^^FS*gS!=fMW)KR5+?&(zy`GbHAd6j8T_!=1dcv-904zcybePw!LkQ{1E)DL zg2Dwu7ndp`dysBVX!kT4D)^Zbt3U$O#i=CY!a_?Ihy%Hl(K+E}G(=Wy?9Ts4WkhBn zO5XB8syN=Y=2>?hJg0UYYm1b`f8w$hA@!5?n>5-Th8X|}bp3d#%+1a3o%t=_D!^!w3HXKZeVX9zAL$BYN_l724T72bV7ZTMg=g2^*!e zMVol*oO{-17Z?jtW@ZG4EUy$2$*SB;A_em(@iNeH$Tz;sjgU-vQ*uJerST}(R?SWH zheCUX(C#h!p0Zq@rbus<#kn(LhszW!T@vqb;h~W{;zfTrNTG98#OsV*&u zZwdl?jM4qHh3{DT`Xrx7UEy(hNxD}5`?ITzrpA-dxwDif9?}`Dkm$fXwC7c!Ia6YX zDjr~+;X~V_KV%&nQSvl3&O|cEI#Qk#X(uo+8Kt8YvyuS^mSB;3Ogz*Ssr)@3M6*3CEtP6Cnj)uU^1puHGOA|e&#R>5T(y|J$e%0XY zU~G`_TqPyjO#h$PLFEaWWFk(M$eF{f2z4u?6S4X}jp2ykdsbHrenwhYdS|B&MwDlO zH(;fdBS1KL{jF`p?eO{F+VX_+@WUOLwD34*5iPq8#kjc?6ElIWpB(t?4W5vmy*(e% z0DyRLR84I$L!(zFJeAbGJUc^WL}DQZz>F!Lq@0gbZQ%jO+>K3$`$PBebB?~S=+Y2| zmN$31z}9!{FXO_Pb9CX`!~oI`f4qur4IQ8OSnm2~-CM<%n|Q*bqo+eSF5mVlGTm+R zd)WtvSOuv4ZUel+J`SH19(sL_WEeJ5xzs__dbej_K!Qvlp&U@Oxz zKjK5ywVrDiJWu6wGOB1w74XqaeD$S%5bF`xfBk~>(ZWK; z9XnKW+cY8zO}kQni$LWdne}L{umyL1!>OvX)<@sH^M+75F;N?ThvX4uF( z-%nL7|E?YlExEKy%{W(UhlL)0mD8K%J3^|a%@f|R@l`X%{k4{SX_L^xsLZK#U67E4n?*T({*X_o*r98uI0PtTuLvBgPw-pBv=XaafdXx~B{IXqpz_kRkw z3L-4)o?=}?4YP^T75pO@4oTUum#E7`4$5WBk<0{z8fHv)KN5vGivUC}V_MHgsR|~X zM6oSBKh?>y{{QQ*Y(mP`CBV575AQO?B&Mpv@K?cY%J%|sSD!PLnDP0gu-jD zOxbd6Q_8q&u-iNVb4Tk0Z&GtcSE_8HL%wd7Uiu@!w(RKpnJ4?miUW@l`+t4du5+YS zAIM57Zu|dy7zFINWgV_68T4z%2smjyC<1EAn1HZHB-Wf@&lwm)*Va_(^tzBa4+zIH zPEyoWbbU<3YJdhUXBhIm(RQRAxIm|jLf7#=%c918w@FQ?ds{`Mfr-FrIxJ%i@O0=< zvuZ-OL<(g&Q-x<)8CP%{iDm6k+X2JY-uJkj)@Nh=;{LwF+BjUu2*w(bgoTHXg^Nhg zyZl*88Xj#Lm3GK3PxaNrf6R+B8oh-fRctk0`Trs7J>a?S*Z=Xml8{m;G^of*2t{To zo2;y|GonH$lqi(!>`lln85JUXMua4#WQCNiDDi*3?)yHE@A;ko<8dD6+;<20yvOyr zuIKd}9E;&3zrc|u!;xm4b= z$RN~`C9UwxBRnaZ29IvdFgiEW@4T{qN|}EFN~dgI&vf}9=01MW=j!aF8Z-;l zw~afKO~HSf^mzY0RI8&{t*yaBS%baZ@~+Jr($W%zSmLR* z_~+XN`h1K32Xg*%iO7FbsbNvz?>_fR6vf^b6ali;nVI_HAO?jUHD*Q4GU{mGiS-Zu!v)}@DEVkdGvruG20X@Y5ZJi= z(en-JVH#By#^|*tf*7>`2P z48j~IR3yYI*s~bvE8q;Op@Cz8?5Doou9yJ>p}H9^v*N$U94oilZO~e*GzvH#Flan1eInT!DT` zQuql{6(XG2Hzc?FYT8H#O&skxl!JVhhub$I`whx1R`%IzFAP_Q#N|ikY_S9i*8GEx zJBH6{tp#SKxXBaTN1x|8Nkj?h4o>-5PdwW_KiTVD^U&}`6grxLt}9;6VLr*h@|U$4 z*Zs3h99}}$ zi#z}WjkyD5;j)+^9)a;;R6yqL^nhq8n1vJ3Fg|Ts$Cbo{iJnr2nSJsj&f>)0O#aGD z=l*YIX0A)Cui53@mu4Z2RP!S$0Y~}Fd z=S56gi{sBv{l6bQh4I&5fPm|-@ysO`r&7ZRAJjkd;t$O&EhL$qkpsaCYF*R0XN&pe zbTUuvE-We{)g>sEs~lo(Gca4WGs z_N~$1w#~Q4+obnyVZy7G(363UxO~8A;iD3$`DcEv9Ry`EA(Nvs4*%c>aA@w_4|Ngy zx3p=^uU@euj|cOd>FJ(4V+0=b4=I<;Mm;E+X&@4EmS1Gab7GJ&2;&KC&%|&br%RQgvaB@3NnJjx%qewk!Mdj8 z(EMcgy4-I6emt#i7wT=_z9a#yX8$7kT0g z1H?fco$31KmoN9-2OPnu0|1unrdO9oW$;Oe#^`J!K-!jQfuWvqHzzMl8i7~OA;itJyd^OU9qyN%lD@n4Fg~A&v%vrrKF)-!U|w%E3Jo>4KcSf$QizwvM!1 z{#rC8m|E1^OL&UNJo@(|AH6cmxG*@l_+yyoOtGzBM}qhQ$u@xPF%3ItmW<#LjC=^^ zI+PQhd1~v6Gb48;OyMski?T!CLcglA&!ID|HrIkabDKjsGL>89s! z9OPQ51%4@;^0@D4PTP|xIupJ>wq{WTFqnoKZ?&_t7f+!o<|fehr7@@TwYMiJfO|RZ z_98*T^A_v?&??Jg&Tejb5u7nW5zav^W7NrQ=b!qY0#8tweVCU)Gm2WhKqCh=_I70L%Jv*jRIgd;=R>AO$Z}#hc^Ou{1<1ee)Q?aPorg=>A(uP^Z_wKX*&9@ z;2aUefN6d?7k7Fn8vI)PCQIr>fMyq$H;~CqQ;kVd!c6P5*Z4A>QIBA}TFFN;BhOsp z2{h2De&rd!%~rkg`0BZnT*01D*d0XcIq2J!>6d$!iG2wUL#2PG?!Ap<;5zjhGiKnI zI={zbA0D&8`>x?`FuFH5%MkYvexv6nvAC0;?P`}n9V8UMMs;_&zi-75*Qx0`h7<29 zF71?e9<0+NR{|^&$DuE3FQa(8Ly7AJx)ORew)UAJt}JZ1mKs0AyPk>aW^T*K?~TMwi2KkoJ7_0vC$KBnKVQb-Vk%>+&ge+apvB{=HFp)+H9Gu zzC4ABnonNt64@lf5^D;;I&Xq8k~f#4izCrgH(-772XBpKNjtjRn)?~n{BV%KVL_~y zNHOC`wzdpS=wre8lXAkKuKQ6ysJjPu051Jp6MKk|=ZZ?;jwKO!Bv>>SbEy;b427?` zxp^=8G4|$PgB$PAdHMq~nq5layOuFur3ltSD&`Bx*(Uaj@A8uZ=TDRN?*hEF{P1x= zkG|D10@kA`hwo;~nZdr&UB72lHhW%<`SG3MP*^*iWk<#%8B6)Y#BL;dakoKCS#7vpM!x9#RBb!=9b-wrePcdnHR+C)ifFZZ%Ip$q7UWQ z44NVWD*`GeIxhV77!Dm_>qp|^mJw5;2rflwH<@r@t$5P}?*Kl*{&#>O0bg%|Mr{OU zjT}H{AgOSGEt&Mn;yIk@gq!{TiA^6bxHn|K4F_|KP$yB(pn~9pQVw9;^mu0SU=PSZ zWajV`3U5}u+~qFr;m@(PHPhxk?vm1>t*Tv147v5KLGe`ktG-{>W)65r|9Qsk@1)M( z76Rio+03^aCq_P3SAuaQM0^)=1B6*3LW4sgt4_uX&w68ClORc?hk`Yd{N>MG-p~>! z5DN*#_#@wBrg=a~tLH`n2|!VmPo9+rp`16Jh@u;d?Oey+3$JLKaTahRh0N=y4EdS1 z2dqycCW3&v(*RwAr}Cw!ovtJAdm3{+T#09D$Ge``eT*A!OI)&;l73&TUZ?nD@Kwc&e>~w76H1>>q)ezYIt^8{XXh8GIF*Dv zcI^1y`}U5gE6{Hv8~51Nt4AJJ{NkyUf;>q;Got>$c=ph!EXtI4J`$$} z$Spq*D;hAskd$}}Q@8Pr1s$*S2M_W=*uwr*Cbr%@qhv{h&7+Wyp*79zbq5cxjwUrZ z>SDURG=%zC1#TZ|rdeZ`98cV{T_yt_WTu-lZlGI9TwJ_H9E30$hKGhClB<%S%j^!~ ziQhgJhwN90h8LeafXKzMw2vSr?o5NJ3KPKLn#X!DCo8i%`1fsC`O$0)oRW-q7?y}s zc=*c~EZZBYu$vNtG$+1)^(JKP4y!kQ7P$0#go$6N? zK&AywJ^VPS;0T-o)B`NBc&5?vo6QzARK)rBXaDLk{4!XvL5y%Oa$!B-?;7%h0)QhH z{OPn?i&T>mFgv0hu1lf@S`}af*JWI5b2SBzPGAAv@mV+`3@O z5dT+zj6~v#*8n98gk>UVpb^62AB+)yf+Cb8k`!9-pbE$qiA}nd^e>oQ-NsUUvD`kX zqeBxO7pDxlHc1=@q);>9um%1eo8Y0sKqO(w^s?^%>+VJu;fk0 zEIqFj__;Q*jGP~unwpD9Njzi?_o*M>gSHZB$MT<|b~nH`%UJL>WND_ApjQItqMGW6 zU0-V#yTYIzYzA1}hPT3Qwfg4DA3-m=XBX3fq7eUu$|v*ApWLiBDJ6=VQ(zz-tz4N= z2d80X0!$)uw&R7nkA(pF&8CV>tJf}@o133^n0nz}ex1%poa-FLPSHyl_Wc#;+Lk*z zJA3mlGmzUwRq=b}I?W5asvdyor?B-ZM?I@w!t9q>B!>wiubELCAG<N29v` zbM{ttePn{;qBDGzWTm^%^9cGsDGE5L2%T%7{kPdjv8(sjf&P+0PSe5dBSnnmZLjlj zfDCKCAMsjpW~*+^iN#gl9nyY6kcGvlXU10AKmL{Nf8i!uC{mjEtTqxXdVoub{@-3n zm7?o4wbip2u=Mg;6zPjCBVSR7^=)3358kB&RvXLVuQt4TIVZW#uDqX+L|XujHN$k( zIy#M9-Gk5UKAd`-#N$(_4wA^w%xgp%uVs`o`}VE?UTO)&{qBhxpxZX)bYEDU@A9VD z32iOPAuQ!nA`WWb(RgMt?uerj?0wGrxDI&Ez`7OyahU!kz*xETmyyJJX$=~St?cso z7QGkB3mw*^d;@*ay6ekN0)kN=F{_~fVfdK*8D4zZ-#-ziFCly_6a>D-?ryGrp!Ugm zU`5X^cUckhY_Mmj5OB3e>R?dMC)WT2;S7eC{Dvh}jb%UYl`iTm8yYjM;Z#cBBaY{p zy66n`m+mWTPa7N$-R7^X(D4>$IGltgJ#K*+OkBio-QlkG{9xu7&Rag_z{2<@b4YuOS~v!zZmYd9dOx?C>K^RdYSj3BfnHU%W(1HbqeOf+U?UPMYXvH4$Vf1*<6@O(YR|-GRi_Oe^HTH_6rUS4btyaU~bauPJ{;KiI zsT1O19EYyF7a`luA@I>EJtR}ke|g9hBX)To`qp7xI;r_B)YNLO-xoyxW>KE6y}q;H zLoG48(>)R&L>#>O0u8>sd$!&nikc+B1?%MY55VuVuWh41`7vCeu@9M|#M=7J*z>k| z7GbxQrBOh4eL27+iG!ibCj$j}`5h_e>-{6KBJu@=$4&>RVN&9epmAZV!ppT~KB;UT zVy`xwedY)=Wjkw7pojxY63@rKIaLi43$uNJqi*Br(Ktf;ay(a8fS1o!M?IQX9Y4-} zrqKB)`4H$7M|5$X1hPuS8ML>5c_eDW&Uj;Cp@(e4KtwvYY{<%FoE-gE5y#NTkMcMl z6p@SDai+1Y*&ZG%fytFMIbJ5TYUJuVY_4pd{uir+%1gsSH{do{o?;37-+l;s9hKsD zHX1thPE|L7mCFM(DZ%Ud%(BfhY9(r|-MVM7T<_a-K@4!IqD(3O(E=-MwP>uNNpDo%ssZt?eO()?7F0{G9ERK6w{P+*!Sp zp8Ir-$l}318}sUa9{=0DHQZO92iFe#^NwzrO&d>#sMIfn^a5tW(P5~_#n zcolr{IITiUoYc`CXgPGpMN=nI^c3z zKa$8Z7{A=wz-s#X^YOr)GPk;Ob&)!kQZ?W(J)d=m&#>ekxDGVtPtO3`*@S*KO-c3~ zN-xzmCzCk!k0oNT-BCD}Cmdo*L%)8;^{`Laxq_HAWZ%eMb+OQa%NfP!3TRe``|NO~ zwflgD|07`Xn#rb;r6NrkR#IdeN~Rk<5QmBI!~DaxrlXK*~G%K zX_Jg^+5yUYR@Q=!9D;%nioD(LDe^Q`5Jtv9v6| z7(YC_h-HNK`y1D$_n@00*bP{yW&;^}b=jeYh^v*WD-!CPcedgDfg1s( zr{$Mc1FW&{;P^0W9n#AgFNzUfk2ic^^B|S@mNyTRH~FnB@BL9uo;$^n}K4b_KL!0k5eI7 z08qUHRtOsF2p;teyF_tp0$_my1i~mr)SeYLtWC*%~i3ZoC#F1|o8EJ#PFi zv>BrO`ld9#^X1Ttp!1?Ox&~{o5XoAAkfAXuU)FnchS2!Zd;KGGkGd~%lWlAmhbCif z@-=_(jD*dH&qLpE1-t4e;tL;L5C$5I(Mto`690YBCE_p@9Bv-C_{UV3$V(n!+r#3W zYyly{g9i^Z7(oI>2bcWsUj(n@+nsz)Gr7z2a{{|0UGJsFfqV|f7Sp~(KfX3YAHJ3d zU0`eVPpp&Ap(!S_w5eO3yO&ZNhIf7e(xe&uCA#KRa{OPp%`X!%6hybH-Tb&CIJs-& zc$Yh)=>O9^Y>_2uVKPqP^<#m}td~X~!2SAH6ASmEr>X>Mbng(2Spq8)0L7mG+GiVf zKAx+2IL`6xI-^0!U;8 zur4`jw_*=021A=B}FTj}iXTCC?EzSfPa-K&iP*zYN2b$6o%<5kR6o=8IxL&`m&FB}feJgOs*8}!5<2fOq z07Zzvc#JrbyvQgwvB={D6T?==y3s zTi#e`-D@@-mYaPdPBBqX9PS;us&V0P%+dD}T8x}&TZ44HV{>aF-FF1-NUXk@-ut2V zKXypJEU098;1$)36&o{J6##FC`9{oDF%gZ2%pU$C9l|Oo~e#n{d4NY#WX~qi2 zdrNyI)gfa;nk0gW^6fst+0H0FOg6LYkX4^+XQvR!EPyi+Vm z%3=1;E?IwCzD?dAn4$TD4RC#k1_8Eth%$=@QFOfX;@`o&7-MoBLe74$F?FW=&lB z&=w2mTIuyOh?g&{kplk3Au%??K4#v3N=a$9_fC*|;a6=Ehyc|xs?`@_b_qWla^JCi zO6@p*Pm!bP(j~;O?ccNKCKT7OL?m1n^Z;qN#euFDI9j4_Ae#B${(6L0jX{tKVlfdE zEboT7#IZ*mTRt2Per4i82pl{-r*n2uZf1qS-X1~;r1$elrP;K}5)%ygIIOTk%RFVT z6~=eAf?Tgrus>^I&X8JITx>n{=>@UTC>Y|pS7OQvm$R_J9+UP%=bJRJgjBE1FR7u}v62&*6^o0G5U9R8gk(_93Tv>rDksdCnwmjy$6#&9s55 z)$vMcjH*0(#-`NUPSdlq!*cKCJrS*QVVi!stRrw|3nPDjFI*&6+R(&!N;y z(X9RD9B2~)hffH-k&AxnG5f)TiNFkDi*opxeSZUXCe+l~%D}-5DhAKNj-cg%-_*WH z%M%xI1hc}uEtz2EToZF|_v$WR7iC*x63?7C8!(BF4$biIgH1@#* z3HN6nyX`9%w*T0?8ST$B4r}_JBBtcLUlalY>JPuaPO%t^ANvm%pq2~*IJ&z^ovc^< zV9*8^Y2{J=kNTyKN6muztKNen*#-J8;EkMh!Iy`is&zo*h0vbHtWbOQ zP{n-CN4hOdJIOO#(Q@pGc|?hGoV^=F_Pw~ zy!Zazmm{#p{&yW${&)StMKdUIfR*K5(ttzhcneceAvlAwva(xz2023&HN?d3J<>Ac zDRpzpa&l@$8njQWeD2=l#776!YIe+3&a2(Eu9_~+(#>UE3gl!z;>aSOnB)!uL-n;n zoufCI+S6WXYinFPMd`U;Ts#gwPUNz+CH;xa*5)*`w)Q=|yxbyCE_`j*jb9w_*WW-4 z_SDx?QqDr;*M(3a^K)fI=1W>|UG+Z9k@-OIYF`<15GMj%f?$iW8i70`Kv86b9*Yq* zz#BR+aH)V2k3py9fkoRE-HIhE(AN(SS&qTw1==2BlwNmrycJY4IP>V&=6$>#BjU!Z zmCeYps91J2RNq@S*Rr}sj$~S5 z1=j(2hw&X8%32lfZiKP9>T$#MLY)H+#JBmYRmU?_6C0k`Oc4;$_zut!@ZnehR=-@D zW703@-m0!n_YgUke#Htw8CvOkFkZrccsN;$n~`V-lo+_^yQS}JYl+gT^pJXZJgr0U zxA)P6Uo+MfzbDDG+h)?NVQp@8fqlP#Kr~Zx>r453)}7R=dkc!zA6!j&qOGCNnHK79 z`)G$S({<}vOH1qUG~IJb{B3RjZpt5TVDZwRSwNZxDl@p&B~GK8VasoZdjUPgUaI<6 z&CPyz>{M`Oft`M@y~FrctbDXe#q%q+n#?*Ua%RUqvFF5oz2)N>`tH5=DLLtE#%ljg z%%NLf82jcXva`7H&Zg&dE}Gc(|TcliV52-3kRZJdF= zo&hTmRCr>!*OhUqx~prQ3b9ya;8L&d=?TF%ZG*dmaRv@1!WqHZ>e%nGU_L{^Xz`~P z&ha3`#j@%3*1qy90q^(|Sp@I1>3DlDaSi`n2lhOi*;y`nMF@3kXhPud;8F+{X^QP6k7){ z2UcR>?7@d^uSu+kCGXz7-C_vcB{3nYBom>p)QQN-*%QkOO;-#th+_wpBt7ycwy4x` zgcpuO>5%r9t9p2NkZn0OIf>JExv|-mjc!ZCX{+yL{Mh>Y^M!}YuG=u7n!^ngEtB!~ z?9E-;aA5MsA&PfqjV<#W_DIVUe8Xx_cl zZ6g55%oB~l$dqCc6&3aOKDo~fR|pQjxDixGJQubx^zT-8bv?^LPhCIqCPd~CvOAHN zLT(^lKE`Me=dt-{4IJ;XVPI{(SM5F4|CkQ8Y=baGT_+s)b0>cK)-<}aXJ}}swuLji8B|ve zUfwX1Q^{6epRl~iJDW^B)EIv6b->^gyFZjs?u&5ZqnPcEj?aRgvVJcIhtK{11{N7R zT7)R1%H=+L65wy4Ik*<_P~7~$F8R*PZnbVpu!Qvxfy4*t;q8gW&;TXl1^qAjt)HS? zqCyZuZAIndA`>(Py-Yojl_0cfpzpWhvchlpiTqfodl1%{Yf=<TM6gusBy)ks;3#yJhEDv~#XC&>YUG?0@9?qOu&Qc&RI5fx>6dgg>Q z*LAK|XUg5B&kb3nJiMAMWcP(m>v{bCL%+?7_omOP;LDL1mLLoW%`bJ$YAC7YlNvw1 zr7psg<)4aX6bpJ{G-zH{-;r;&?$-~c`%3!f@Sq5sc-fVCv3K?`tZ+|4K6KQ1Q4k<- z4LyPyNIqdNv=vh?M1$(&RG7u?%j}kDxUa$O$^sjl4V!maeEhY^BKI^NLT^^7YkNW! zx$W>c)yV}gOIlDtYyCMW--swy?3O zV{1vMkB4@PV=~kJwV-q#vh;Gk5H=aBSML|xpIRP(v+B2BS0z$Xy7PTdUkAqq)?Hk& z-3UXhs@X_3;en5ro$vZJz`iXSXG#8rpRC&jmhNI%ZPk7)Ij_J7)q_#g)6+A`F`SIb|RTcz=5ltp|#(dSawccO{IZgH*MbT?OgMe35;PS_^f${O`7I?Ii zdUoXEa>B;SKW)H_mVSIlBysAD**VUwhQrEx!bcD7Wfu^jtG*UUx!X!UJHjjChcZoz zg-}>#OZuMMhDy>ebVJgQBAxl4Q?i_ujEJLfh$O0e!1~Dl4k5d#Qjb64UK)mdhK2lS7f45A8ac10aR(ExF8IBom_|FHd=>5OpI`MXBaL_9q)Sg3FW0Ker0536OZoJ};qQ=IqKC$ce z#~;8~lgFQomw$e89d%t4tX%cjnjN(1&+i4xZ!s<~{L&9YDb;gV2P3X|x>&x@bYNdb zdyJCBNii%9Q}7ZR8X58UgXwbW&mx0D@UEezjx+>#@Sf&w_a9N2nfcMsiKC#tgMEWR zOi7G@sJ&TjgW8)%+P+Oq4riX7|9f;Kp2EE*?U$DqsegR$50OnVt-D*i=s2e*CN6Gn zgG2);(R~R-_@zso1({?R5yyUf$9i%b*#E~k!!;Q_SKPcAV61{>rPOV1`we<}l2b|^ zJnLfevcH_ZegaG z#Ea4K`s}!~INz)8`hSVG0}FdjY%yM=l^p>EiZ;+aG)Tm`E*NDMrzf9UZda(f*~GEq zwuyHj!Rg8~q6Il2L#v)fvVAnZVi5Ul?)ewjOxeZXiLI9Rity~Roy#wd4A1S}tM7Zt z-rKOviHGBA%s8$1>C&TOy%JidQThgu;S0tFA&Y?0qGb5Q2Vjl{R0x<=xEYvlJ`SOy zNvGFiVsgGF)oWlNsiHQ5MjSa%@c~Q%UMFRN9I!~cQ-A`ekbPQ<9)sQ%xV9h%PU7k} zQ#66936k3c)YXAA@R(HWl9v7i!lwSXcodC*DQH_FOZKPbVZ@r*G!xH=I;x z%r`aLN5%>cjIib)`GA+?IQDw))u~Tvvi&Pr-A9Q#6nf;B6qKI_25!|&zog z;R6AnOiuQDRxM0;I*xqB<;hb;REdel(tdD*PG)w^jCHWDEYGRKUgMCHlT&R^4--O? zF&@-oboRsPAq=S(5T!;SCoLPCzFhv(E|Y!ca7<2hbp#K?UD%OfdSj6->ZR5 zZ(oU9IM8PdUg~6-(kB)O;dGJ3<$agV6xYNpFE79Qv9Mh_MmU54be!`GJ7B?aaHv7O z3ZBrfZTAuuGaH+IUtgbt^XYw0L&IVt<)E5HB5hwqVqLe)%4e+Ap<#|_2&xtH99a83 zbziK=j1d`W_C>eEid)j7B3ahdHJ5jk9O_vuZ+;n~XZCs`+c-@;l2?~QQ1CB07Z=z< z;@Koa1AaO#`A;@Ugqu&0#z6K&?$&MH0F5fr(R1k|(eyK}1MqS1n{LGdNWw`IxLY*X zg-i7_C~jt8H^Q|m@q?1m>5bHT#Y$+<9u1C+kffI0WX6V$}e*OT3tzl_upR zRs^NP^8J&aU~V_U(f2~Qaym)v1D(-v>XQ`UhqmbWVnN$^;sH)jh}*Lx$mOW6uV2r_ zrNE)s^sA)^7U7!>y2;Vy8g` zW9W3Ji*yaatV%ua+Qh_po1>ja`48yk4UU?$IfCk$(3i-gc;L{$%~)zS0lG{)f#*oWi72*Gs%jIu>SwS^QF`SV|DrlK{nPXndFIdJ*v z)rJord}p`u(zA32V=^F`8f77SdwW6_LR%roOiRp@@v1w{eNmh2d(pl$SDErs69&@` zPFHH{yCbWrj+vKTO~b4W5A}VR)w9tElYl3Z+X1uOsj*hx5>C}|0m(-QD}e)v!o zA#?b^)kxiWspI80{yG#Nn1PwO34IIUB$(VN^|S4k<)%ghKGp6Tg8No<^*elNWlo_4 zl_Izb&D0>av3=!{C$If-JDqb8>C1Wic_pP~_%VthN%k6OM^QIRu1stvqvQ8th1Z!b z0?Pi##6cAp>+#&fcF^c0u5AhY%yGPYhKtWImhOfXj@u%6eftbIDW|i;mKUL`UFB{n z_`>5r^)+}S^<_bcBNqn*^8^`};IqZ{LnwTiz8mZ&?M&Bl+zSnDhWnwCx_TM}QEwid z4S~^C{-xhnarua-8`ok#a1E1oBRElpCMMoaOhiUJKXw!hS_dQAjLeDo14h^g3$p!1 zcMKyHN*vJuP)nhxD7ODdP8U2hVfbFaV0bU-=+w!(dZ9BAq<}5Eb_E6nZQ4(a^D&x} z0n;6`5MdYtD|A4D1;6ogRj50_$ixW`Y>$A(kIodE4QIJzAtM&WshqFqy^_j z8gn7nIG%>4GqL&Rx>d4FD5Fc>qwg!O=j$6b8um4aF(P3hg2s5n%>a8CGcz;7E7rqY z0myU+=P0!B>x7w@nA~sNn3FE~WL#hqRwX`=0No79 zD}rkrOLvLt=`An+$%gn5prOdcclUruJUj@99{&sV%+djDjZE>5kT&n(Lq5>in|B87 z5Ome>^y1Zl>-XF~Y{c>RhxZE@4}ZS34*Fo|J=czZl(+?`7!LtAKio};5eBZ4ran2F z2kZPU{`son<;APn3CqO4&R&-whCN6RA(Nmo_~|S_Xfi?#7S%+S)asM7OxGVf3sD{ zRKvRx`84d(6->D7js5+}or9Qeh$jy^4+oSPt*~%J&z*SO!@g!$V#4(N^nJr6M~V$d zIe2siKs%0E1)FrN$mYkkhOkP>rfZpf0rn22GK?}YO>I`W9=8za#c^$&Kv-JGKK+p7 z;tlIZVrgTP?tT9DS=EeX6Qz)o4xevNN^r5gb=qr4S7eM(j-Ub}{WG^IOY+FhDey+$+Z z=z=9d;KN-nUB#v)Gs-1tl-pFR>7fKA6YA1Tqc#~V0ZAjYJ)pRliDtukt5^ere-7Q_ z^)%bE^eUxs8;H0cia4z>Dfsf*S1jF;LQL$hRu*W2R2N>`lI3hx7L}x=q{C_6=FG0~ zrq-AHmXw5CCO-}QX5a$6M|J8{8wP3mojX;aZ_h6%NO=G#ZXW=K0Sq_9BDy|cJ8|Fi zufa_7wfjW2u~dZdjMO1!+5~B*D6;25@Dic7p$TDNWb}EO2uE%pUNZnh$S9)JjS45! zDGbrq7ra?GU1?6FYjwb>gmUwkf}x>{at?YYY`X7A-`L=QCJwVsz*D12k3?wg2`xDF zL-Dz{zlG1gzAueAhB3;Jp9>HR@~QC1A$RDg=gQg-F77I@+yjHn0_#PbGg_U@w3GyN zC5;1G;pXo1StwxGounXJ%GIs7;nk~cv8UyM(fdDnBEl-|Ue_GnKR5F-vhS4gU=WC7 zi(lfgryrb}Qdhb<+P0fS9+2FVC8yDnE5DBb{6jS4;PBYpSY8D(DS%-=G{trdg8|w0 z;?VK&@zLl@vXV*Lb0g^vQ*%?>@qAVRbr+gW<|i>nkBaRp3e`fRANoyl+xlh>p(k!< zXek+@z6^c~{C7un?H(Ct8U5mO4@7ZBDmH17;*)-V*QL$!X}7#^Qe~ra5E!(WQap1? zd`aZm!S!y>%l7E2+g>|z+TpXCqf;Rd_t68l2MNU$ol7q!cBCxp9z6Rx)lFEw;;`_KNP4yH0N41@LhF?#x6_wL=h2}j)hpz#jEqmR$%c?1H!wy>}q zPxdi>X#5mry-J!!2=+PGSV4^22shTEX$y{|J6I9C{ec_LUHn{t? z?1@ySzOYP*h0UHD?-`{Lmw_3k>(NFl*H~KL*Cz<=`trKKCS&<3%Z~!_o)kN_~>&i| zjdK8@E{Egs5hzANa48O1&T<^NF+-T{$SC{K8ZSx*wE5vd(2UVH79v6vf=xfW{-k!% zhwvod1Cxid^{9GHBqJLJl-T2%y~%Pxqnh~uUSb2-n2ZLf#k)va?_m%mS9&t}y|VC9jikX4 zMx!;7jff1!+wq-2z{zH_osZt65O&eyqL@(fh@)|xlXP?2vFp;dtS7d!=@wEWmJ)E& zBriK&&ANuGX5EbkvhaeZ;& zIzx11_}14G^CS~kU6;QWRl+kBusJY!H!N0GO0EWN+9}P2EHkVlh1_NtF~{VK0I1|j zcCP_KIUfExUYkoD5F9r@#(IRn$KkjwaBWt=drVxzP!H+&9)VfuJl^Lw{p=F=!R@Wd zD!*fbNq_{gQLQXQ7e;hXNk$Uf2T?AD0Lg=w?SqlR%m!uz=6F1@eX47osw8mv;Nb9Z z6G`w0zgIQ8?LZbzGVnny!By8k0mFQ!(bj>YO*>tS&e6tKE+>2j)WQY*EM)Wq1wP8w zYTlD4RX*3o9iglM3Z)8&VsAr^NXlet$fJompll05fp&A@Ej1V?R#Oj>FG3 zE_qbChJOE0nGVqi;gLc*qzO%^aYiGHx}a0B%W;si1K`b$Ln$D9-TfNX^jFv=jw}ED zAj}mWDvIodRghv!Q`LDva~oq;*8f@z5gKku*>f|SYu-tLfvPWm{T{NII(gOZ$dZtV z0UjjKL^^`b@+_;|;oRxa&|0r_%Ty{PS#pi6Yv^C;Ts=>JHbsXmL`08ZxFt+Zbc=Iu z@0z)nxbDT`wwxBI1E*OKLCjGw-#+R;2HrNp60Ko7gCI>a9&~eo%!j-lJouoRBz9|h z`We|`JiC{pV}RWY>}m(o(p#uEZ?*!1mX=xYFTays`%yBG$x+o6!FYA@ojkl73K$XX z{x`U3R3><>RSeyEBbs(_CG=}hCrEr0+hap{8@xm9v*~PG`^)H1Y8Mw5rzJ`Mg9u0W zxdlCBglNU7#);j!+OeCMGmxZ-sC$Wt#QU+taY-n?axvlcYg>K)w6w71a8CLU=^Lj4 zSy~Xj@(wsMx@&zv(SvY28F}&ZQ`!ktk;Y01ui|JXfp~B4byC-yx%l&T{{F#cN^;vE z^g+?k!c@dZlDTm|3od;)^6TeBt$JofBi0|$>F~3F*$OD0{g!ijXK@T>;9w&hNSxcV zLXvH+UIzEmxQ0h&_R+D);FL)Bmf}pLDSFs=nXJB4Me?Po15EK|E} z3PjXh=>Ds>g^WNEmhkycAcaawPY-{baD7iS+n?hV&T5qM-nUX6^q!kVWjD+2bE>uw z0YD_6KspaT)uwy*N8QT58I`nEO0~Jx$XJT(An980Ww4&Ue~a%_LI{R{`g)LyTpB|x zKi~Ag=3+15_9D=VaM=1t;t+%_AeU0*>ODHi9}erO8#iKQ>^I+_zb0(fX;HEA>jK>7 zZeqn=KMsNwaoYyUrt4X_Hdgh{VRBj4{r(~7_VqeRP5_Bl3ld>}Z7VBII4zpj{(9PE zDWn{adsTO3id{EQ_CH*J1|MpMcfeiSNqX`xj(N@!j{S7yzHaC`j|k-U+;FDA}PFPg$QTErAo6ykt2IFIA%+RsTfOduxq zA5|R?fFSC3QdZUZ`abrWC5vubAm1=~-iJ+VTFE7tcA+YD?d6OM@Avctcu_C+PbkT? z`kxlvU)2d`zpMXxNNAe;-cZHGGHgPd4R~VQdQ1P}kqoIu7ss`s`r5aP>BC)qZcXC|@(3u3eFpTDSAym{h`;Ebp*BP0KMm`?wvB`AyE85M=R0BqabSModz zJ?1wob?UHynu1UndcRG#2L{H1W3f#!pSr2^h%?#nj;ZCDG+K$BQtd;W+NM#7x%*VY zgNnQvJR5P`;~iFrk8Z4xDuXVNHfnHkvaOHKiUBW0T4UfjD32fN7pw)`ZG0q1z7{6c zWL$q&@cjZRnwd>^&h31s#v@S&O-Q~2VJP8rOcx^-NoXDYerBlceWqgl@YSbJNpwtH zg}=HEV77Hp!6}TwZ1^yZleZ2 zf3Aw@n0j5>6<;oFCHecu)F_;rs}KTQITXvg?UAu>Y*CO@sbsRQfLuO*Y{Y7QK~dv_ z!)7?VWzATED639paBdW4;}V!nx3swbQqxQcTWC=xz5jW86L#%JQth#{4pScDjQv2G zJvq5Z)XL^7?JS0o2SPh(YK_L_kJDe5(8z7(IyAoZp-SR~x>MCoJohwT*Ul@5Nl<8? zhGSbMwqnF!zGEuzByXhoriHI>2VUQL0p?6BFpNShK9~ReaWjC?J0>Ac!K!4bva5vZ z^c0KvhF})8Kk)Sl`VO}(5+Jfcpi7`~xdyJK%?fP%skl)Q94CBf8;~pwlR}5(#mmTj zb?b!?jgSi!Hh<3)adJK_WWK0j)t1nVlaPB1sM2Vnru{jzJT{nVnZ+x5$%`|DB=wf7 zkQBhM#nrRny7RX!CzLvital|Pv10WL(*+L#)wDc;u$%K#4D``0=jq=vU&z_9t2N1} zUkGFrqTvRB_Zdm@EhPUEN;xX(?3XJq%5Wj8c7P?>4$eG5t3iXyhl?247pF9(niVtl zTxTDibH7+CKI0l6hSZ%?P+p^XEmMR7XvkWjeB9??mw_etUy**LTzJ=?V z&0ra!QF@`pD7eyMb!}lXKsntUB;j&e;-d%%XfTbAsRNNt%Bi44U^XLsDm12`g|)X} zb-|mOx^D*QzE38$^@oof&?Wr}pvEQ_xNi`x6$nq7?;{2Dg3;fRZi=jiTCkm#0s__; z_%j1(Xtet^v4`Kz90Z{#A*mslx~?VPyq<=hfvNC&{3C3dL&HeMN^|3Zn8VjMgMd5? zvNVY?Q6~l?7&1vieutLT=c!V<7N?qf^u3&F@(BS1z9t0JLCuD9D>C9z0%4(#j_2o< z8*a0rJbQG#+fg@+O5V35Uj$1Uv0CMA&Kqudsf)zI0uI1`|G785v{0X>;NH9_CHDqh z((B_AbO99*$Hljg z3|{3{Raf6Cnf$0<^hJ9+pT1%RA($Vi;3D7%4atctnVAv-8G|ghv2o7;r;T>k>k!fY zZ;DFWF_B^4P3{V~N(l)Gi(EKzkC_k3R`fo<4UC5en_g<+4E1y1qY_O!9zOC_Wk^#$ zh2}U$(q^sExiXEg@+B(;#M6y;rf=-`_k2Tqt;N3-Zr4KqX($36xSdb8`aDROLT z`maET%Bhyn^yAW59&OHhVYagms8th0ZiAj!ggS(+jNAftOxUWECC&4Ot0^4FLg8tN z_D}?!hcG;mLB!bb;>m`X()UNhay4i6p-VrJpX5KXj)%L);{F~1E_GC^*Wq~}BU+zT zn9X+T){l$MWco(~Q!{abxPNL`R7gwf$8m~AyKi>9PV=%=$4qbsH=j%TPI^BIT=ojX zaMty!nX)XNcAOtiZ;xSkM4b7VJ{K(atoQ* zvk?kGT9mjeMm==q{$O zGLxrs2x|fVvPV?!1nfs#yK%b4%}kx*ICOdI>Pc)%>|ZQwKM-}p2cWJr)3iy4`h zfy=i8@hr+_YCxkv)}hKi6WIx@Ap>wpJcOor{HpsGFm_Vy*E)aQPtd0X~@!Iu)4ClR4T;cBRj-7(c z^NhEPi?%%cWKDMrHj2wsXWfHlD$n$i2@xq2h+@FnFgPP2$3z%FiY9PN z3Gq1w&~bF>k4uDAIM=EBM$Q{PdHlWvq?Q(8F$)Z6{n_>N#!>0@YdThGT8U#uk>F}=K9t1T#!M8R3+Vu#t| zPG<%gcCxhtF#jLA{^R0`KPZO~QgZOegdp5F-OrGdyjk!(P(pp2URW4$o6L~Dzb96= zJ-{OC3Dw(l=X|-`fqsr77FoiAbB?nXi0DX=_<5lJV(&=@2g_EBnb&{rz7Mt~a#eYW z=Vr%!uESiLiX0~`t1JdDzto1i3%MF2{OvB?pidak0Xg9=*akGW*IvJV-B=DXmw(5=m0&s7N5&POL|ZC0_R=Q}*`k{M{)9Y??IR9naz2T|~n=(iA=WjR*D<`f-)XZ2xma zOMm*Lf5!IAj*zhAdymT%4BGl|)g!)T-lX=fzImEAP0&5z1&`vODV^=%RrAih`aRx_ zJZ~QA2ah&4w;khg*`tuTxBsK{PuC=}e_t+}5?iXhtXyQH&DoKyB1({WP!ZdO;R*$E zWK~y}cjrYiW%oWmi2Y|B7HdpI$c@~`W!G7X$6kBAH*AHP1o6=37cQu1ek^zMK6BW1 zI>!+-1n6?~bZ{d|vAOp2l}|JK_5$o?nE0L`bp?^JFHw@0`z)U=^~om+f>}2YBLvQh(!++QY#<6M{u+neoxTI6>+7G?) zUbK!!tx9)n^Sbg;_4!7%mot(me#-nShVEAb+?CoVXGv#xuIY6(_-x8WFz!N*?nY@? zJzoa&NLD%lI9I=4eYv}p zw~V4ndt3V)B##>xb0+F%YNxa%CMG}WcYaj#zRPW9=B0c$Ld45I>*m_8PpQ_QjW~Tz zXpDY6htK+rQ4d|VTU%#VtoD@tyfXPQWVOV_C!lr|S#k{{Pegp~q-pVVjK3@pde4*3 z+y3_RmqF!;w+EL@Sg)CK*z*}3f7*P^^6B;iCQNL6H2(fP{0GyeaYCm(EBeeIdn-#ORpkN`pxNs8}MR?e5IzI=Mo;} zm96FG=cx{)IZA2EaDJ-!s8)W-nK%WxEBo7QxgG-Dc?aTe0Mm=WgE4|0yUrzfMB!*n z)8Njs0d__E!kP*v=R2>=M(O&d4XjoODM6&H?cA##6X8o=EiN|S{XI1bFa*0F&AzO# zFnW+23v{rW2VL`GWSOr-T&I>@1714jqst!n>Vcngy`v^V9?Pz_XWxr>P2Rxtd zl;_O~QqoG{IiPS}tRU13m*%0fkg%>!{TcJrw5)*qe2u=3Mm%}E>6`*wbbQV_I!MjA zK{NkT;UBV5Nzp%-RQbGyU^5TT8?HyNjEH}+@$fciF4g3}zU+U+#3IDO!S^I=>*3X{ zANe9$6LP6GDV%*&WpL^Kk;!*v`}c;1MFWb})xR;bG}v@>D`_~N;@nYGRZhJgGWjkHXLsTGnG0a30(C3dDxSD` z!}aSIBULH!@W10F!n4dFc^zF{(#U=?p4tBF0n!pxubXiK%>|m^@CY7Jp%|*^ckS|d zhWxc9ZMwbr@L zFqbdxIQGV0jmx}@OfxjfbkGsOYheZ(1Fbb6$?MF$7crVzSp3k)BgFd{qLJ8cTR7|U|syCMJu{@3Srx9-EKx)A-s~exIVhw*DY>0B0>5Bc9 zZwurQ#Dpo*1e-v zQqJP}p>i7vlU(LVt>Ef9E9@U+4NZ7Y_p{IN9#>SIa_A|0@%LZOL$dciP#_MM*`;WDgQzsg6tzU%|l+IC+D2_?!XR(APU1MNbOJkBonnywP&Hp|6>2 z@+~t**rIZgoV+}P7(%?t+}{rjK_z143i9nPf>KBP07BJ$l3j1#`I)xJa4y z*_Zp$+7i#(fiFT>YbfH%C*=C8i>Ai%sP4EOOu*n#h!fbe4-z-P4sX(;DGcP{;Vv#G z#|Ad__MJP8LLHX*)Gd8!B25}}u9#llBK~q=!FH#_X?5So22Dxdv*}Om%|tgxsFFO! zSqqWIA-ZCFI#0|s`IE*$rSPqk{Vp!AbG+{S{{8Lc8iPi3rjB=KKt6~3X;2! z?T887dGpdk>BC+orwl^%m9{^!<)JLLbyQ{$$RB;mzGOZlc|$>Jn-VR(4#(xD5j(xT z>M$?f=k&>u0(X}7$i##+=#FqyOT+F{^V`R=kafQU4evk(`Z8odY&t+U=y6zEvKW6G z>&t2R=@C=ytk{d7g&#k%GBUPno1BSd<>G2okBo>Ini*>dI`6SYnl81!iM$jX@?I;k zVJ$+NM386c=&TjXZt4nxGl`{6{Rn~Q@bPU0bZ1TI5Hx^E!+Mc0veIN6Wi}-HdgMW_ zOw)3PUh^e$&C5U7*bZ!jE139aJ=&l5my;LaLrTc@6ck8-UDlN%(QfJqnd;P|`6Y)_ z-^J) zlbIbjqK(d8nut%sFJ27Aftcpfg1EKPI_RP<3yj3KiMVPRyv5I0pvXKVs?!j(_uxTh zp;NoG)pC2o!s7!9T40&UDBwdC2_sc_y%Yc813ZvIM_3PiD6qWti~`x!G^q&;Ncq_$dU_ z4}noh91?jYBC(TqEp*rYfS}Y$t@h^d+QrVeD?G7`o&)m?%o00X;@tC%ex(<^e$$-! zR8_(wVE2Ll%Jcu!5?h{S&c`Q`+0olxT9;bTUSs93ld}(f$P>GtCGvKUWRcn7)3GzX zDZ|MQm3BUU#H;7Z)OK6S@=Gv{Itj^rjc^OPcYge4mlr1)4jBe$m035SQz~NN-~bQ% z2r213&IbVjW3cDxQPtB6t}{RfdsLg0n&eGn;QpJcP*6m3{q`+jV|krjRYk2G85MOo zy6{u^kuXN^7bRq5c%C85Hw1jP;P{NdtD@@H zkB{&n3TTa1x%#ocv(Q7RG0-J0JIQsgET2-0zLcjVz4~yHq2_f{v+#JQ$TjjK2{ zEU#b7SgV1a0N^Y$3#1k-XN%MdxwtUYL1>F|+oJ}!2h^aW&sey{=pHJmt4mo}u%dMK z#np1@T_0H4Gq5{IiqLq0HK)xO-8w%vmr_*p5X|~~WiI_J`HA4ZlR#t4%~`-$Ca`=Q z*CY>Dj%O^)%)~!)kl2OiG;1m~E{{g4(hKCrytWOL5>fBn`#a>q&%g4ks-aB3zK6(? zJ5%|{QqFPgn4(nvhVHK-7B0@zv?pO}qvO*LuT^AmCoyvIQF&m1H(H zw&J;43mI7(EhKjXclPus;u-P9?w1WK%6YgyXElVp_^nL)n~Ub>Vk>-oR$W!%7sA97 z)DEg1dsI{&5q{y~#Vu`I;8CA93b-qAw<0X`jwE)#TYh`?J^IVb>xsQxPA*Q@Dd|(z zu^6U+WcPjisG>n^TOS|97p}vldKXpu!V2pQfhdsj2jRC&A^x?}kQ)ah%zBtnhZKxJt`% zhKZh}xhChAlex0SU51$v?7JNfr4%YKZ}a^?V!7bp(3JM%$%8d=O4{ImxMQ{x|D3O< z_d0l#ySsC*kVTFmTOi;=Y}^9dUS7(ep2)& zWPfA)2br+gaoXX8H?=;J;+IK44Q}}OkxF}PYziEYJ@B2;(s~ddPxY(?g9ctNOTJhI zOheYO%Be5R1?(6A#$c!!?wRUFzp+Ex4qmuxKk5E1eDnZ$7rZH6V%E(zwTT%X&u`;f zw_^Zk0mX!NmMR-~OFgRpefG;wLHKO2r%<)=tyo8b1i89nl~GTz@ll1^Ma8QBxBzY} zqgFXVjLqdSF@YUL_1+tIvFDd$^f{vwJp zP)e1J?CkVsH~;zWE43Y^@)Xv-jI!|AdHg26_AjuhU&D5CsYTqn`3e@V)i3>SrGrPj z(|e1yslYLC@_X>nzSY$t&*Tc!RdjU~U0eh~+U?_`Fry%`56kV+FeEdW@FPD^%4;Ys zYr*=b0}Feri%Md8FO)xI6CnJLOH9H#`$Z2h7^JD+LUebSFukJWs zf0e=gI~gbi1)W`7hbE@w^{$DMSD38+={az%XFpD8NK?(bI6LWGQF(ly*)AsWm+S=* z!(*2UsP~&%wy?B{r>YvY;)jQpNgLLdkO#(r&(FgB_=UP`*d_enpxF%M_|v-H+p=wE zk!_tj)Z5R7%EC`Uslj;W4EerHx#lT;GxNbhnrANz1fEz;+$866kEI%e>4T)?j%Nxg zS#@C7#Nlm03{T#l!fnVGdzFo>kIz?G)>8KT>3%h;PUYuad-=s*mZ;pRdEljdUPvXg z`32i6{unAlfn%@PsvoG8o9#xuhzgZ0t%>{pU(x@)LO2;cXBA`p*ki9fwd%i1zU9_v zTz!1__aO?3oWjO>>wIx@XTjCuRVS%)f}TCyEy(vqM)SJb zsFA?&A`_|fEjS73|1_d%c$D7WBBzo0pmg=>-vw-(oKP6;5fT!*sS0`^Ho)J+Y#(A$ zG}?E@dL_8nIJwdCO-!+M-yF@%$I_SN*pzU;YHT2(q-+kdb98LD&Zqq66SqBUG#5zg zj2NDoQB({L4mQtw+Q*i7*&oM-Epo4S-91kJdI#?fB1NklK!2@ z{?07|8`@wtI-2lNhBET^X9{YT?zIe-)RNli+tAQccCU!%ziukzpYC2#@{;l(7>eMx z)~Cff9a|?^&8~Xl9vJ4+X@0>ZJBzEnS|V50Ci_Bjt$C>lAm1>3&A<-ZFlBxVvLg~V zP?_)j*iB!|xGf-Xfup6RrC80inl`*S5JW4&1T!MaC{IpKZf=kA>KPlGSZsZ~u37TKIG@NzC3fs*ozCEs1RRs|wTbZ8 zifh=e=`_DS!F7gQJ3Kr*;PGQ_h(F@0ghjCIqUR7G)mGKfxeEp>7Z;bRy82035)>2d z3|4N2bdG&_>D12hZC*SzW-XM#ec?dEt#u_ElcC3r!A-DT_kt-N=j;Of4_aMG{&z=Cg=6ky~J5wF)Z+{oM+hI!*78bhJ#PIUcjkLtAG91k>(p0lk2Q=># zz+wCq?&$y&XJ8#gYJwFNAR*fpl=o?dg@?^AE;qb`y)I-U_BY>w-a#0UWVa`z;vN<- z0rh(rmb~EdZ)1ntjc_o2=eYqY*xO82A$!EPFW8FWQ_MASH9dJN+wo|DU8RC zi{_SWXQzt8ny+tn+o`MpTJuKBNz1*q^G)I^dQzo&S3F<@bm~Pr-M<^Qwsa!tyxanJ z2@WE@hMX&LPwY6V5`}YFvxSpoj2Rur@^9CFYzVl!AkOh%$pn*l=aGxwq|9Y@riZpz z3MKTa^)&W!WHb9;IEAxji?`+-G=}iw_LIaa%fey`b3JhnK}i-A6LYMvw^&8GElEfk zE@wrjK?_QUqG5m|Qm%phaaDa-rp*(VFJ%I1v9Z^z`zwQBgJfF(adjxjF+8{5s1Onz zQng!Ls8*St?H?yQ>-yYL~?c1L=>hOgCwc?un?5N`3)PD3t0_ZQR`a5ob zW}t{AFDIo&Qu>SpSM znYM!*uL%%Kc>Z55F%9jD3y9|8z5T6QC(o>1&wZ)WUs!BP(E2UL3x3b~OE(*kOyK;z z`X@k}nE@q`r--6Zhj3L+<=ztV*N8H*rmELRe#2->ll^%3uI*!7|R`5)c3|Fw1k~3 zJoP+^GYk2WDCX`=ue;z8aQ}h$zo;2M z>mMLeR`vS!<+s*n!lNQ(1168*J7y{9G6=mE=l$oKbMaqr`(VL=K0v8_r}3j6qhW(n z66Vg)T-isds3rg0aX)vD8O@V%P$$SU`I?}qQipt8G)QPmIgaIAo5WFq`2%fK39T&~&x0{MhMisPBpym+ z@55v4cBBsXv%Z~NrO3?N_aSV5e%z{*9noX-$LZv|Cse=S2u+Ga^OI52AO(M#KYhkP z>_4Y5*-1jef&C*hDea7%i{KHlQB37b(2`gYE2CUv=^u3GUO#-k%a|QH z;#TZTu_PaO1Z#L+({IO2-z2;>OD=?HtCRG)iOJ#udjMhFk8AwvEeD5DLIiL}DV@-t z|Ni}3V25c*>=gH#>s=WW$Q`9AYXRX0^!H-3?^lG+uN|>&0JBaU*5tN!b|YFa0sx>3 zS8^geJBnJ+H4w>x+w%xI!|j>l)}76-x?fkGHz1pu^e$ux*J@VXh>Y>+C{kr(s$@H$ zE+y^8GCX+F<7F;8T9SV2lX;B0^4`^P7RgkHUCfn2{ki`@>VF+*g%XzOkCFKbVJa%c z_m)G9^hfA93ZuqUZtxih94k^`XW`-+zke#}1hQ|XrNv(9iX>m)i^tL3N@RwE_igkp zuOv5Dj#c7q?IcP#I1Y9#WIlezJjYqP^K47=vd(ZxQJ&W2Qfx91NKks8y&_5l2UKaKE0+$A1`*Jgy zh~PRqsMDC9PFJUH{kby8EXgJ6D-AcaWOXiBbqZWIM z9{dbxILRau^yZuOw6?V>>3^NI?4QnBe#FQK{s{y)5C0odQZnNsBeeG-8D&S+NQY;p zLayD#A?Zx@pn6xhwo5K^Yo6z?H6Fu_ZiQst=!)?1l*#l)#@M2c7{`?by2OtHrv%6L zioa}W&U0k>Q~ewUxHH5j`95>=vRb^I&&bMf5FPwdk+WmLCD?d@`1dk&B|P(NY>`Q@@~t}4lq{~3?Xl>fjlglECsty`O0nzKZK2^$ZO4P933H(7mk zJnh=*JGrgfHZ-fu=}U>X`{kj}hOb^r+se*0a=kkeknljwAZrxi!XR?l&>M z*TPm{EL?sBTpRA16~ZEl$F0phsAf-^0l?aTm-XsWyn^G6 z2WJ60WIYYUV0GN@LmQ$UiSc2$E#prkXEI-0e0}V?O7eaw5vJn@XlH5BjWjnHbd-Rv zdV8bj{-#u>Dxb})f+?rY1#4?<4!hO`i^st)ta%KE$I~!pQ5+5DIb?pHdRyt9lAoiD zn@07o=%U+|H8p*J6)1j!_sPT7G2(u(3WD!C7Sy+x7`%%*t`b)IVq>cl$^Mh|< z1j8ZNDLCH$1x+_iVa<4J1vNEn&B@4IWD!kOeoBAS^+iQ9ITVEY?C9R{m z-K$MP>`J++0_6Ih%cs9rZ_Y`!QyamjDAy9=(K?{w|f0oc2$X;pyH>o zR#u|(Al!1TmchV!@L(^TktKjqn@=OPvLYf{;c3s-JOd%8md>kG;Tb2HIB8Oc1%+60 zdFHOzm5TV1#cE2fNdZULDME?)uzAcbf^SE^iw19;VozD$r1^gXV)UN@F{<=lz}UpE z^_(j^y+eDJ_Md-xi=mI*@%QUJaRZ3S%&pN}g|4;D+GsAs-rp)@& z6W>J&u8zVR+IX4`y49|8eUtuBX~V*72YcdW>07sW(dg^cB-D??4M4>03{C(fZ}%d< zSIL9H0nwOv>eY^ig?iO3D|^E4-47arfQYaGiXVd$5y+n|u+ngu43)n2+?5P2CjyMP zM!(8O6}$bUCE60K0(p)H5gWSk`FY`uj&0j_?HZmM^R3njij-23^7Is?jE#kSaN&bh zfS}tCYC_vK`kF`-I1UFWOFTPc=x~wp5xD$SG%Hu@$)BC*1Mp)vTM_mkjRA*}G8U4$ zj)>~bI)9}7?f{FXkclt|ab(L=e?0c(be{S2JI^P?xrw&}( zo6o3W*0Ep{Dte>`%$N=NzE0!zV|Hxu ziWt+$lYxFjkRL?V?pc@P8!?@r#t*RJ>%}Pe3SrAFL-Z0ZZe!X@Q=j7f9wxZQ@?H^> zxOPntzryw%Yl{tLU+>?)KkjwQ-kw1~z=s&G)O2;n9BgP*G;55QzSsWbQtSo0>YT&M?yv8> zT9%0jTSCUWkK%V4R>cBp7!0gGJoTlw3H*ESZ`L;r$$J%#ybpJ0I+QxxAT6EJt!AWl zBReHaCfSy_(~bG!U$eBmdZa7E&Jm`pj0IY1O@pNLe=}yqKV3bC=$WXs+f4ib#&R`Z ztv40TaBODuS~YXBziMqwtOQ~n5fx$dedKSoTv>na&CZG&_FUN}096Ou^-@&auUzN5 zznOeVJ4EJ(WkB)k&e4UA7=I`uXrj1W60>B&7|CmY>;&6MszBXdBq=GW>YSG-clDEd z2lTyURUBZl#I>hqMBtFH!cW6?K9EKz>Zb1VCqoStFdI~&d*slo5dV8CSXS9? zWj8V4-YL|rw9Uif=hxbgRQ*BGsvpzr)*q^To9Mh?zL2I_<1PpS8KI-aR?i21B@f}c z1OFUJmDEo282;~)=qLPvB08dX;~4q!MbKrz30%KRDu_{pGIPA^#z5fFDaquzT zjJ*GB@WpAcL$!MK-QJ{_mF;`TG>R-|X#I*$5>S8OV0!|_RoxF|DOpA;Wa@GicXucA z;DPsq>#GHRHh;m^#lAlP_~$4E>D584(>8bh+cNyqMm>qiG5-VfhOQyVnU%HE;K<~- zv|_~v@wD``a%a{%&2=4^CSP?TWI*%1m)Y8T_bAg482n%5`7d~ul(w;tzKBAUYqnh?;YTD+^21b&#E>`}+xF8p=^ z7J8^SUeN^I8V050)eCz))(?%KO*Q=%lMOqX@$Uv_;$1eq4_zx6H#RadUh({Ds`bpe zZmfq>of+a!*t$C|ikO0M`AXFp9ZL{X2;)W~ga z>Zb~L@`P_X9+p{bd_*Px)3;^*)3;d)37vfTQnu1Pc1<<4AVXZ+w_t zIN?N_+SUEO;umkA^_Gt+FzA1BR#wUEA@SS+FWsKr{buJj@|#?iK8^Hc7o^MSN*}Sj zd3iB6t;PJsh)&xM(UqZXdNI85Aot=3%cvkmygA;cfRJipw)jl}8v(+P8SIv+fr_Ib z8VQi$*k7vs{fiQ;xU*kAISq~#KA#<_{wZegp@&uAHh1ll-Dl4_L0IzwE&|S|!&5K` z-Sy+w-GBi85SRxjnP~)L0*_XE@XJHD+Oh5iWgqu;eO}Fj6L=I^*6n&Z$k% zatL`M{>q+RW@cvU`8v(7CX$T76m$6QLUBlx=K-c=aaC3RoBr5cow4a}*+o|<{`mdk zc-PtT?_iGY!p}}T`64&}g*7U9A~+J)_Gu!x5RIeh{8xGB<;ZCBZ&8n(=eIQGnY8#$*V;L-PgCc0Kf6S zK-ynCLA_$1-IBY_{+MMEuC(eL`fVR=xU;W(|J-vnR-tiQx>Q%5*`Xvi&%JbywcF!c zH988cG|;0-w9|e(sGQ%pdij0U*4W<@pdA$C^eqr>xvvc`TI*O)s#3hAyQ4n9xgHLaDh{>UyAEG2w#oazBOslP^t))M#x1Nx#*qc0EmV z-eKtZ_cIF^sXO~H)6Rcexexj7T|ms!kKleyxSArq<;nc>133|L$Ax6WVsKM>?>v-c zwyBq|wM?m==x)TT$2so$dxM=abr>GRkMiV-!v=Ix!n^|JL(ATZxhUgdtsVy&SEDlS@>8foX+kW4Zw7huS zwD}c6Bw_{a$wg+T+kgH1B%c@cw)5)&pyEl1&ws)v;<}#87#T;}uXv^KcbD(PdF8%S z>tYj)EAe6i0KN#mv)7>W=wz;9QP%FzBgfSWs zwjF0JVQ7SWAHNA^MblqvAxf-~0_rGBLy1mQMZh2vpRwNuspLNHEs9eTA2uNB+N3Fqph^WI$r zj{-ZqVZ&5}XpDUOb~i6?AJ*phQza!{!f6Zph0)e%3tGA>n}~gw1C19yHY!cy31rn0 zgm}0X*n|Fv(7om~4gD|5^-M--#>m@%8xkG7@zL7=o5Xrb8J~G`R;}>*bvwkRL%cAF zC`p7NGBPlP-`Tdt@Aa7)Fv^?bvaGf&)gCeJCH{ks#{cUJ4K?gxR`5G5O?-}SgZfw?*Y#Ww3G%5;RYEN<|kR@ zwf|$=!j;HgEo}MA-ByD6%dP(70uXviLNkC4Y4ul+V1(Qbn)8l^Q}L~P_U^4``|y~O z&3CVN^WT!8O7bT*J2Q3nwf&9+l8O|h9mn}SZ}zlkD&M81G-c!nX4;e*TzO91Jw($D zAI4Ayziv&=YirCJe0=@2UZ9u+#?Rws;j`>vfn!eN`uELkk;<~h_Dg-YP`v`cfj>${ z)-ZFJhUP8Yt0)D`4}-n{%X&%}f=KYkDpXZXssUgTK$13*DwmO!QRKKNg(qviAMI3t zzK5?`-uoiEF}Z`Z!ZNs6Knlj>o`ig4Z1-5$*%`QN|Jnx@-N|^#TN>X*q5tf_Y7eAF z^EY_p9&1afSCU0F!5FxO4h1QrXmiub%VU<2Y+L<>wDS$TQF zu{qG~heZ;tkYD3TM#h*M+GplRW3qGe^RI9I{nRRp+;a0_BpiG0(A>a~6D_zkd^qse%1KBsBCrB;ou?wDqQM z-S0F`f3!LKWcXtcag||e3T8@vA;+qb#?Ch6x*$M8Y`m(@aHjpWO?|bOOjEE=@bEO5 zxGv>#@2EO?D*t5BgT;WxldPvB-{f;lYv+w~{x^kM|CvHRrYYvB_na;1S5UIpLw@I` zXHSGuoP3TG%a4O?5StU~h~HF^;*$J>yFpeelHqL*Bxte^CCOZ<8YyNh~HN zCIFiQ78drx)-X6E6!kuLCP583PvQ$d`X@#%KS1wDnD`@t_>r z+1ck_(YiqvsYQbYbWenOf-HrE_RUL+Dd&hsif!Eqr$euF^zQg-S%{lcUrrK~Pd@16 zeLd|=aj<6s!;XE47n_=&sw_R2x(s#4Dv-X4_1Wjm?d=m4_m7(Ihl!*81m_8+(~&5l zaIJ1GP|oWm;#Uwt!aH5(zj}Sf5*U*OrqlWPQ(raL=g%WwSQ6CCRiW+Mx7*GQT_nVQ zL-aq1$2-+oO|^XE{j3Ow&I;cbSf(P-Y?$a-fstE(syafPlDu2(q7t`m?LjP;7zRho zf(N&Nv4_k*xXX#~$xFx7q^grlArK9N}FH@Ay5bsINzbnx+)IGYY^2P)c#WHW3R2iU*? zI7of&1<_O*8qz=J)boLz3h~0befN6dM0M64z%uG;(YaVx;zCvPNjzl|p+dEX$!o*q zVCtiXdN5t1d49tl2)PD0}UdZt-7TGdBK z$CZ=!GjEg4WhGcQSKCkUJt!%bX}MBo-hn9>cW>prLxGvh;U!0(EbQfAW0#wAaLO)! zN}6hDQK}`N#dPx^vn4}h6uq<`<(`0-P*sq1(*&%nID?o_2IY?ETx+s;aFI|Dr@}$I8kYk@k&V0G(M1%sBu24GH`CV=gTq3F* z0*pfb!iO^P@ccd1oz>M>QCYdrpZ&GmYenXR)8q@-cpT#!r6rg!H6NeG`f|51puMQo zR#(SmRo#qCO%p=+LVfPxW>#9-U1-8=!S_b2h|HBMr^L7qfg*5X3m@owyXStM;t)RTb8IVBaO+L+3PWG%U|uVo)azQAW$m=-02U zkbF>zxC*4xdE7lDy0Xi&rbZwFSBzy!^2oBV1^boJaQD{x;Cu%FX!- zBA_*pi?z!<+No?f#+tK}uFsFW2WOC!jEpD^5KgzCD!`>BP4TNb46i#%q|ds0xZ66~ zH$1|Ap}t=H$&(UUIl1et<8KpcZSUYP*=_!M=`tEBz5C$7ajl;N_J`m`OL$}NuXJN! z@EdGjxz&zm`(`F8CgRm}uhps}lIYOIJ+WuU!M~2Xi&e^Jok|~K!-~a4`19Gy0FYqj5R+~{IH$D# z__>D@%)!~$N@Q-9{TYUS`Ot{XUPmyj`>*A*;C?E{!#80Lcg+0DpM&eXUz1rwFeWs}Ihiqg&{8$6iEl+wo#ey8`}0;Nulh6&_ceDr1k?S!pTG3J{r(~0 zTuoI+Vmn(~1HgpVgEfh=;mkynP6#zHl6aNL@7YsTx#B1g#|IvR1fJOH zbGX(A;TxF8T+!+E!k{tOB@E6CJFx-%%|)P`1!d|dwuo5|UWRyK6&K{S3OgpEn(Nor z2(P?g`MlZm9Ic&wI*L%!l>~#Iv(*HUj2v_xk2ME4>|BCz*5#@nAFK#pS*JmdXnG0% z&Qu9Ese`r2Tehuz!3yLp7>*|2KPO|gPnDZ%K`?zzTZzGk^FSV0|6R3S`p6BEP#KvX zsc#``jpZ@i>h){+{ig9D(V?^ys-_UHn9wQh|Sb1WdfdPs?5zUJ`_3+tqQP=ph0aOJe zD$1sGWK&65x&F?IH&#xuSb`I#nS}C+up+^{5euAu5ZeXrc8~jWMLe_-BO^wT4awK6 zd(xmLanx>9ZrQhQA1!09Y73Q36#;|A!i=%@cV*}fn<}a2w{N&~JSi!;R%tDGAJI23 zA;9B#H506JIpv9*|NY0ARGB*IXIwL=gE|y)b z%Og~(U-Nh^KIR{gdSCLSnVTggb;quq{}On1+Y#Fx*X~UFzYa-&KM#Vk6BUJ}9Os*q zYB!<$oaYBQKiHQr9!d|aw?6-f7OA`;~x~nNx-t0f_p|` zbO48iYefcIsLogq`P04QclykZi<0C+0o9g7q0Y848n~ALp1i4X?V7B*Iyr?hm)(zV z*U)b?`bLlv3qJUU5zZ*!0meaf-Vdv^T|Z_rSsw)MKK01#3bs727RJ6WY^I<6?Cg!v z{Rs6yWY%QRdwu#WC$S{gX;wfJJed4O5M@0|w2bHt&3>&pR{dDpdfstbM$?>S%a%lo z?;nE!jHO*=DNnqd&4AD*c?#}ZEiSmDRx0W84<*GnP71PSD{#f2)3TMo8E&8JS0 z2$`Qi$FycU-^KLq@n-1Y?_WZ|AckN?PNZh|k&=@)LT0l%`Xz)?!r8y#`+)&|~$-^Y6vao@Jd`S2<{OGEydojbrGt92;p)YtQZ=?}EsP zE|T98riQYYZ!t3`3jeC0s+8lLD&xy-?r%Bb{rQ5@E#3o)CqoaD9{xA!@x*JQxttEl z3)uJe(XJU>$lG)__s84susFt)!K|Fc*`#ICFatB*Yn2X^C1zr9 zm#=Dn(RWV#t~VvGI}65zx(^@X+#-|pU;LmELI~Pfj4H9Gt;h$xY7bITQ3b;LLsmxS z1gsHI3Lhfu0&8Z=(&N55!M?sbxZcKL+PDw&5YrPjw?LBKR6~F_4&f#}0|UX`K`x&& zsJ4{nQs^*_X2@SK18>C9@k`@@gEXS`pT&L_du?uk%bI5S9H)`7F=S8#p~GwQ2UWY6 zSbV#fwy!u>c1^A-8!e@fA8)#JvB}pn=-R{$3=Su|FYMvedYZy4FE39R`NDD${Rzyi zh?dlIW!_AgOOKJC|0sCb*4FvFtZZyq(77NWZ{wE`2?_O^uuOTvcoWBLpc#IwF^gUM zV9eQ}`qs_mAg{+7uhlziY($#9!{X!R#l^Rx@O*)H2dH(>XUVv@AS`q{yF7bBLgM_K z`Nj1go`ic(N5@sPqs~_en`~%~FKzs;7EpsTyztKf4#IvFBHCPVeF$eKIGD+Ur~xAl z!o7F{KD8CFxu>Lj4?pgP&z}#5WBUsdFu`G^+1S`U^$xF^mp3T!`2__P;cwn35VjG> z!FXK57lPLjHZ?gmFpw1HpJ8Rb;(FKdt5*tS$$>QMyP;khE^eYVM&2}-n{n^;^e-fT zuAb@^io5rE_kfOWF1xN8nc3y!;R_oV(WWU57ibg359Jt(gMG6aSncoGvj@Ou6yC7V z$VkbHo}y!9V-RWO=jVfc^a`YG%wq)O13D{c)T`!30u*A;N5Uj*e#PgM#ivJP81W1` zUTh&S%*S%_z281Pk3LZQD<1dV0^qvPurNT7>`uzcO7&A#X1seW#-%0oSe?S$1*tHs z6&Gh!eIsXfb<^H&vQy!_tHaRENw!j*n%Lm`ne`+)_zF$Tl+@HDC9J-F+6k%le_b}A zHoE$FtB@1(sk7034teD<_7uwI{b@Tsee{{1XmAcxwtJlNUIRiEUj^oDt__imoLq%1 z>Klg=FUzZHYW$#5Fe5D~)`xbKMc zQUpCd8G3t`5-4jWynFVz!upRI2$YXd2pz`QjVGV`s8Bq;^Xk>B zGBfe>yz_$){z`^uM@G0B04^w1CZq35yXhP{467?V5^oHai`4=BL*y}tg7F?J2} z!Os+cP=&RgzG!HmgxVui?{Ric&b_KC_b%YROk*d3$`b*peSNUGU!=subgZGNi5~_j zX>O9mUW2rR6J-Z1oh1BU5n3tH-`B~Y1tHpA;&1{Dt^*HEf!>{g#6DDeC0Hs1`6>$caEueN& zaJ4%Lpi96cB*-hG*GlvhFK;NCDkUo17pQmA;4I{@iiss-6Q+PI3=T3ZDU~(EHs-bw zR2>y&vF0Ya8U#BHT~uL(I*vlQn?c10fia z=TXv2ZhTbd<16ew&2ku=ne&d{U{=G6k=)S2@V+WrW>r-JY!dBk22O~GB%aRu;sTll zzFMSbMWMCexA`ng%uD$4t|6)~fSkMTeHo2mYU=jw4q4TYxk0%W&s4tk9MBm0FCxy; zteq*;_8USPnT`Mn1}%dqyHQb58{rbsztiOOgJh{9k2XXoQ*Gl5rWW(U_3-IaY%2AL|qDs(6WBTyQK+LeI*^&Q7?B{ifO2gS%C_NlXetR5Ta(SL*qi zb(lWs+?P{4a^6U4A^|~ z0DMC>0@RgU*bLVN#h(S0)_~_ks2s9em{a6WFh~X0V@-mLnww>?r~Is4d>0vO-9pT9 zep$-7L>*fC{yy5w?9B+aKL4hY{wAJ3cmYHJ#`^qcx9_|+KBy2wZe^}Y*5Oajxk2uz z$1ApuB~BwUj+#NPM-GxfU&lF7t<^0wLTm7$s-VIvX)h~_I8-8;EM1k2DD_u_=eOg< z8VJD>cY)YRj&^K`I}giG6_rzq_?sXF6uWg6a|37t!+Mhr=i!9^7LhNjQ~r)C>rN@D zg6Gc7{dQ~ZN8AmAbl_0wdwnx37R#SM=fCkm0ES?Gbq2(NMf4Gj9S|2jwh$@6bL_&_ zs3>&-t8uyaLx+iI83gXUd-raD8}4G5&zfMqE^cA5=cV64!>O7&#jB^^osRL# z>NGFEwa6@0KqHdNTEKGj5XBV^jvG1qaxcl9y5bd_#cULzXG^1Bacb=mTc8e2!QtZ} zqwE!z9PBdAkdax6f7&<~7Vp#9|5bK0a=iP^JKxGD#l`e^+p#XSgqkV?$r%i}s;~)X z0NHQga)Elu^=1;n><5$T+i5krZl)7|uhU1jqI_c^5VN|dH$GG+k^gJjir^YxIwQDP z;Ghn$qt`#R6W^0o_b-4^?~*L}r4k`AZD#OPon zMEkU>OBogzcpD7Ajw|AOSmZ&kA1$`ALBusjbL$gCIXL15!cr6gMyYUn7T6BqDx$lw zL$6hwxSZ(xrBq|`vj_>y9;?G-7;%JO4AyRIf1A{hiy2@#&qlM-ft9EL#8RWPvlBZQ z6&19-kmTcn3&aA$7Nz23sIAzO0K0R^jz_|I-Upl8klLLxQz>K+puPNqJV*WkZy4LC zPHp5JS+n-wfgrg8KlUAzJv}XT+tRBxgr7!6;>@I+_+K}q_}#6j2C5nw-4A=F`Fh64 z`aWnXb&pj!)NAdqJG)iu_K3OVNvTRp8b8zEhtd9_&qm1QeDBI79vmJH5Q~#>Zn`ML z*xQp;`X-5C;}9YdYWB?s9-wLdg{h3V8)~ud*hz44;TYauouS#MO7) zJBm|e^`{M$v)uwI^OdAG*xLExc+t~f*$3FOv#LU9&|{Gdci-ZP?K^fZ%uBOZAOq`r z)jBPab&SIa-9bVeA$P-3z5qsFFtfAJvItI)s8z~=!hwdc za<_T^{kWD0lKd6A^SVsc_0ONI8*HtAsiCBFu|`XZ|D&a)N>sFWP(4X$;ns9rSuZ|j zhwUVc!Yts+6I?Ac?S8$PHgjOlGqs=!Xip|{?#P_=7B_i!+&f)0`Y

;F-Vc48NWV5CR25wAEM;IVq$_9tw8>7(3 z70|Y2+Rf4RX70Y+a@C(hR(pQF^LNKt>%95^H|y~*%`hXA@Ixiq!U;Jj(~nvLjN}eh zqb_Nw0CVunLLkR#2ImgM5EHYj8lE0L=ll%Nfk;_6m5b%Ys*jr1w+%qa?EG)&rjMys|GwXFF-=OJ)lV zr-=(4Mn~G8)lR0rHGXHnj5*H=`k%aXcHK7rq;Yj~&B+S}ZR+Nl<4B&v;+{s+BEd>> z0Tu7OlY&2_ETHs{W~K&?)%G@Oo06mg3k0W9b{^z*c4VI}s3{!SdpTULuFFyE0QipD z76fGb({-dYLb`wDn*hJ2%wBU9;Nu>NBV0z4#W<5V#v@E@R{Tjf|5k0)!1Zlw&C#? zOiou2c_q?1YLFQYo2~PF0R!Ot5=jr}^eGAC|GC)8#d2w@!N4iIFey3NoJw8|law~< zHYQ&vn5d@JbCG7ItL^vnzpA!wUx*&sRP;}ce0nhwz=9L$*p=Xg6aKb*CmHwmA!u}I zLqA{+YtmyKiKI?XY1#GU_N85TTD@k?sT6ZX%mngK{{udzmO2?&JITR-vMAGc3-}pK z>UvQj)7VI2ZoIm@L)2=j6qkQ{`BNI}W6KenaKee0Ts|$nhv;tx|F|aDMX{&s8m2Ziv7)G zf@9Q`esum}71E?z(thz)1p{g+;Nc*yOM-p}qRH2i3v_LT@C`lbc|OZ!7>id8)*P(! z@KaX3`rr}GvvG%%^eSd0M^G^p7twu{G=k8H0VCQ}HFRpqcI3d)-ATzNK8n>T@RHKb z*HY1q%0!51SEKU=-|3;IN-;lbYK1q6q}5&~yhNNKN!u0HH zmj%x=#u!U5Y-O~|$wFLWhq(=@bfH8Guxo^@N8oK+CQixh`}r06a_B2-p*GcR$i&Z_ zJlRJA`8am4ni16(2z);k?N#hjOlcTxHV=W4QBfm0qDD}7c*{&A2)1O)#n;YI3On_= zOKUijOv0I3+H1d(acA>N>2uX|oS?L2#-h(lMp9ttBJK12cD}{qRE$2eQW-x-$BjI^U%h`-Xjzc0rqm>oJex zH2?-s%Qqv_R0p6W*SGh8ZBca^rDgS%Iz_NgocB%~G|HayU4#$8Xe#&aPm_f6XijXQ zdg*FnOK%DcM|`p&Nj9w`XD?0#Cl=7HPl3L8J0p2JcY3~l9>~sB6)V1Je=Jv$97@I| zu`DX~H7Lf@o6_cg^yTmC9TrgH_;&xe+pk{@6hOr!sDN~1i>`ZM_oxIgv6#nif%x;iV==!WG&EATRCodHrW=vuJC7FQ`L|7^8K)d&g>ZrUlHszH(f z#Q*H*lwH8L5*|As{VDAwAcbob7{ia1+OIrb0vAXz#3cbE(WL_&x8{p@twIgH�WY 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 V%)a%(hhlT#?(8}9)Qo`5{{jcseop`Z literal 0 HcmV?d00001 diff --git a/test/reference/line_rlcg.png b/test/reference/line_rlcg.png new file mode 100644 index 0000000000000000000000000000000000000000..84d971bea21f95cec9f351cb3b1f8ec3e02fd467 GIT binary patch literal 67379 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

rfI!86QD3H$+t~W3%KV# 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 literal 0 HcmV?d00001 diff --git a/test/reference/line_zy_cartesian.png b/test/reference/line_zy_cartesian.png new file mode 100644 index 0000000000000000000000000000000000000000..84d971bea21f95cec9f351cb3b1f8ec3e02fd467 GIT binary patch literal 67379 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

rfI!86QD3H$+t~W3%KV# z8+&sYi#jnw5CYQxx=bRSqq^lKP-qIrJ*FIgKmaowfjxU#UvZ{*<6w-fL-?k1;syCY zTy{tvcD8+}&uwa=tSrGy!~vS~<7IPdsc?n=t};t4xR#^2GgLA7`<8T;$)8{Ee0`ClZgo=f#Rl?%Xz$1 zI~`)kf42M-gEh;F2&e-&K&^%OpI^1O@Kp=*GJ08aXKHd$<3P|FLT;_V_3+2Wrhww1 zri!k;dew4=N@NuF#vNYn1HlBg$ExNkrUC#TfFX0HqjCmQ`5V}iT3Q%)TMp?YN{VaQ` z4iS#ovbQCL;2whT(N3@~L5wxbc&DEiwOn4AYuCGz}m+kKO@Eq7dOF+s%YdpR{N^@Qi z1~@w!s={*KNwj=oO=a) ztlv1T(DGk$80(bqc$(dV#a z9f%_oby-6#-O+f6-qw2-> zexH(a`N_JLfA(Ak)l*`XS)gdpn*b2U)Zuo;L$nhcS!qpYqp`_ux5wN=Tf($DrBbn| z6UY=F!B@yty{G|~NXeSx77|nnzo0oxIk7k#G+f&E>{b(T-eqd<{R^!gp2pMcGSTi! zKv=OM;4XVpY#(B<#TKwMZ@=HW?D%G(i5e8E|;a8ptIbF;g#V@@Uy1 zWKpG0CvFp#Mlx;yLk~MOElVN%L^EU3%oQpTl2;4&YjYYEv0eCzN8p5jFI`jKr?wKF z2E-clJyR|}2-=#899E(cQH)^}P*F;MX+lXliI8`OsqtM5Q7fT6^LbNFXNNbO8Z18< zBMQpXOIKgSfXu4t;mn$3$*F$8vL@x$(!SzLYF%J2x4@^c4GkCzj(u_0=vce;)Fp^Z zT>}HRXgE%DAX-8%Q50}7&m9B_>EgaA`;2oqCX(-cp#!F1(Dpu@4ZxaV;4F-5vdJ@2 zk!iJNGjefOy2`?J`LhbJM6QekLa(oA1PMgN2?4ej;5!*LmN}3VI1(l3MWDsgwW=Nv zW^cUboY~+Zk`H?KAZ=@oBMyU@Su`f1#^}JQgblO}I`hMU7Lv!jw-+1T;~M32<<{dj zV`LP~4|&4Qa0SmyI?Q@%jxqCyANPD=%5f%8VX5^wiKRF5evx1V-tuxicq}@_hT(J~ z4W^c#fHNkomY2IT5`4i+WEi!DgpNd8$#+Ce2OVsrX0zg!3tv4&ClGXL#15%2MXO;t z>Y++^BJIzTrAuRw1C%t%nduaP;+9dh8=MUASARx_Ub_O1LUXh3d|KKnlwN)rAAaL=3jyZs-#GF zr1$|M62q$yc5K0CLmFzEI%`*cM+u_7DX0#Rqk~37mI7zVS%5!bRPEUxXJ{4PFr7M3 z*0lrc?jKZns;@k*2w5SCQSE?}*untiA$ihnd8`Qj<4#LK#d_+_&a;y~qOJXWZq>6{ z<9XO*PfO4_H3FQ0$cB%=7Oy-zZk(qtPfmQ3ij>CXO-}E1 z2noZnFq?UwxMC94;a?%LKf!!G26zM1F%tvDS4>C|nn?^`o8nDW3gZCuV`uJln&#mZLv{_@A)4px;uiPQzy{(t-Xnf8TndW8B8 zVieoYnN`#JE^R=6Qhn0Bskob(S0K2KL))S2Bn(c(p!N-u+9>02Z0PE5f2A?D-Y_WzK9igM4dwj$s1~*wR zqyQe{$L}2ZtvG=^HwRJFG$t=jfJU*>M@5y8NU))T+h;qN@t044PAf6-m#3oK%%qaE zLVma}yZ{v<1&yhLD002dZ@iQ3BybDU%mP!Z99n(`99rD1!noH!8|m#puk+&f5NQ_n!{+RG9l1_HSaggwow-k{}hF9 zN;n#YIO$}%87pOH%GAam4;vWvZo({TCb2OZ zLPhPps?B5(H5J}98l+Q9IxO9@SbIcF2a#9EYhtdT*wk)?*KzNKal9y9Pd2M5@ArdN`UZ%)I*xudDo|> zU$OuauUxY&t2^l=>N4rj)EP?zhGpr71jSi0b>rPvq;b%mhM}{JC&JY=fZLG3{6?i78^hGnQE@#!gBU=^~BE!@Dy&puF-lgYkV%@I0G*?AWpS@zCeV z)%0>Zwn0GCh7k+|V1-0`9V}(b!pO!OX3aa?ZG!ZKLa_s#s;d3`y!zy5l12%We@UW1 z0|X}0b7qh#Zn5;!B(Ig^BTRX1qBd_rSwu(a;v2B-*e0<;28LprG++E`n#(NKpV^1{ z(m{h~aeiq1iXexa%Wx6|&9Fm_p)5l<1e-mNUkkX6A!&vNQDO}w&#IX^i$z(T{JbC+ z%+Jk?9bH!n>2q@e(A=BvL9ko`3IlbcC!68y$%UX}?!ZJ~q2kOT;c*M~8OD6=DH0ql zbn#Qi?)GN0ZLm!w$0d9R#c2NoGJ^*DjP=ekelOlHktnsoX;gb8HUcjLHfkacXP@<> zzAamIsRT~20}fs*8?f=0vAeOYvSnDv7Jl3G;wzGpgBXcs9U4G_9S z9O~{1+>$8$`Wrtp53alWN|KP(B}?Ak*_-d*a_J7L33rCwA;T^IY-jli%jNSFP@D1- z?-`fR9aMdHhCa(Qr3GR<z@4SMhQ=~cosqhVma zT}P9MECcoV>6DjuU&cFV>Y6qzJ86aUj>~~zIeR2g;5CSAPDn~k=7BBrv7rSwCUX4} z#m(buAvrSEE|z3wCgsd1QbnT}Yx5buj@)(YgV$15H3lYt?WZoE2VmeAXu)0O z8p0Lu($8gah{$c~@q_>(DV6?li6&9?Y1C;$*NHcrFA+#0M;hKWN z4ub{22)l`Q4=QZY+#!{_#7IU#^{!r+*c_V|kluL5F@OgOw2Zs^cDc=5W90(8OiGuA zlV27ee(6@EM-G277W(}g^k_PaVTc7mpUO!DBuVK?q=?z{+TKQW-lHxs7ph1m6%WPf z2Xg)bGaEMuwRRMn`*K&kd=|?nskXalIMhNBTDJgw=4ITaIL30du#?Yb!WkqlS4ydH zN)(L}PzW4b6LGsQmB}E3TN>e}IAho*g6mek1W13xtF2mmMMX(Ty zUbZ#8aDhQ7*)W5%%?c+5=@rOM z+qn|M9m%D{Q>7iq25i?WHa|wzqhcW9$lMPT!&KQBiUeG+MTz$KP^~p~^j*}6(jCNW zB%opb8E!%ScZAGvipB=11)SBR&e}l?=BJPrJU+xOK7sPlgEDp@rE3r;g=lc-%%z$F zYSZm679)dc(Wjhy$%(u=W>uQW`1bUPRC8qrZEjLdk8DmRKN-~uKb(TadpZ7H>p-J2 z-pa{>*`R)! z5qa*=eDH3$4U$%zcYyP47wto`cRzBZnBdpa|)#YbeV43BQ8f31(ux0+JJ}3QkZK`N0Ad zm`rgKtTRCHPv-3P3Mh!BJWCk4@8t`zHjV!rfwsH;z`+AnU@OOqH#wUDHAO(sx>q2< zlfv5|78L#9#6Ji5aXx?jGV7=N+gOPOCX=I;asr#I@)0Y!*wn7{pYC*m+pNFpdho9w zZ}4gOfLiA{YEl*rRy;?+}K}(>ZXb zaKJ#Y6qYkY(m?9`j~Mf=fc9Duo}jQO@FY>-Fw^d*CdaZ8qVW5lCklRdioA0)pu$jS zdR6%<2?^B{>Xp(5j~PjLyNjow@=b$yhP-XzS)KEN)PzC<%ep|eQhB)nG>iziAO^LefW>Z(%&PagjK z0ND{G1YjSa$s{b1bAY4<#8k1hQ^;VejPKN;AvI{I-J`^rRpepxteZheRvMOQ*B< z__7xIGI+48WO=zsVj{E|iDqK=3&7Var@WY;hMOSwXBz0&{l%%*)4DzV>{No!A2I$f z9$)!;I9&V|$^58q^11;c3WA7`y1wtoU7bt;TPqdocSyq@y#gXrhUQ}+Gr82C%S87q zln<(r=-=Nig9Z0z_I3)R^I$NU*NbISKrVy3V))J(x>igUf-deHwGND|$&7<*C>0GO z#vm*Wd%<>EOc_Yc&Q#zX(~DA|=+{s+Xl5WGJUU)smXO*asC3thM%RxR;1G)-!+=)tw--?=s1;=47o05QkHFnh__#Br01XWKL8S$U07<^4p`P? z0PwyMJmVrNZ~sR^=$+*Wb%_yzPm-c}+_+&0AZiXGpm{orI^_dSWcvYQ=L8_=G!<>^ z5e@}0CNI?}&d)VF-Flsv6Y4ri1h=tA>J7$8lg=&(``9u@U0qVXP(YCqq#0Kbk|=%# zsJbvSj`p}1Evicb=F+Q$1=B(@Y*4^r!{4Gg%~$=cu?O|ze9AcCSJ)8LXH%MTN^yTT z0h~m(mC~N=!=xpzJ+MBvvvlO{J1tZa%M8krJNmiF6o*TWPoPAPf#?KOS|6;y3SsAM zrrZ^p$t+I90Tqj%-b;NQE(Wz*Ii`yeE-;4v2$&<|7C}xXG;-R|nK*?3t1_iHPy*nJ zpc`XGAlok%k`k>H5d5q8E!H#gFfrC-nTB@j{rBJh$Q00mW!#jA5A(ANj0@3P2G9F6 zCQLA$CK3Rn^21Xkcm3+C4PUibUU+Y^VsoZYa5oFXr!-fhrjM7lBX0}2M=Wz`o$%-B zV#*;2+YO|;R2no)UimP)!%Tl6WKBdJC2o%%7+Ljyh6q*lBO9LtTBAU9CNLEOF0!yJ(Jy#49xd51*#@}0wq|hJ%iMvvh zS}BGxs_aXbaByo9)sUIygoveSy;|_zzQ7A<+fa;I^9^$($&et)McVsJ6Ll{U1o6zA z#b9`al#SRPA4W(Y^My)h=OUHZ>s(KAtO^W2AOcym6=zQOC4k&t1r+jp$@mMh~iAG@}BWv0EuY*0Spq} zVXn;DPq;_895ZHH(&zsb-J~o$6g(ld7^pXqsHDGNsZxA*V%ls9QKJk5&XsR~<&zUv2@nxjLAVd=NnHW%X?O-{o&;z?v#+ASTnrq_>nANdJk}Xeo^#c{S zOhClIlR=>=wSrOzuz!L^%CzC0jBynlD7JEY@hl54O~?b!u?*naxoCRuFS1!`V*jVx zkk#g7eE69udtd9EV?4gNT=3;AJnX#Vpjrox)l?iHH)su|BuV@YV&y}; zE}#GyKW_5ZQDF+Uf%FgC%9$=VvQpn6RM?R6pTKJ?VEivVCwn|$HW0fIbIaG!+Er8h zfHx|9+Z+Ue(elvw^@{xZvE#?Z>Gu^ZiNO+X3k|VE^maiBVePaGa>_uKkiczto>2h`!4b8g+LBkvZ=-N32`e=W&*6pN*Hv|eM5zg9S0 zT=`84on!2K-ToP=tsWeM+lf=AW1WhFW#eLwdcjLU^K~O z8M11ETHz`}%WPRS^vl25bLmvj%48|>yQEbKas*5Nw?AY8XuiS}Hsu2vFzaPoSVJ&^ zX~oA`{xaz#d&Oj|1nZUYmfDq8t^EV187f=o{6pgzg|vmBIhAJ;QR+2ECjSz3syv1K zk~gc~N^1vfT;+T@(}MI$BTab{)Qasd1GGPD)j}3iQ%VAE+Qlq;Guepz~siI(83&K zIfh%BnQm1ED-miG3VyxPR{|8cB_df^*M+g}6qubsT4kFx9i%+{(bbnPJS&PdAY)`G zQ*ls&ly{2oiceW2H5B9_n0Xf#5xpsl*s^su>vqP!Kz*TVHOWOlq8+qsyi&%*3$B#2DxtuEG}A*gdFvi>-7UHpa}-&2 z%D0k-pAGk0m?}rx^kdOy(mZXT#B(D^BlC{ZToOA{!56g(-%Yzas!8PE;$t(Bj&TTs z6}_0@P(7s4IlpPZCQ?u0fyO&7ucf;B%eQ~)W22uDg;pLoTzSGho~F!b)d>?B>7G^- zvwwK<({lOUTGzL@WQU;y%6vHEuq5}Ce{r;#KUz7Etjzwr10=sOvgKm}$%;uS&z3Tj zuY^eX78T92t^1NjIiB`YUTgA`N_$KwX+Q8*U24$<%z&pS9ds_}R?%A*{fYz~ zk9Ahu0kI%1{_yn}4lu~dLyC{BOE0lc=>0tHF&rMt3TlpaPW9XKOrDHSsbt{xuIH0XC7Fpg-* zGS>!z8XzD-({q_16#eoH?uawrEsm}HtQ3>B8<^}9TZC!z6hxACb=6{`9k?xFHX-6K zAMh2m9am2Cb&kHdE;x{x=(m8zt_Nh=`C2bDk%qvGePN~J8QX~d_pKo*)c8{Wl@wHM zjLO@~-9Q@8&Om!vuq4dbrcxxa5;U&P%EdTBGazvw;A^Gvm;e5!3a`<%FWWVQ5#4cy zcNO_bGtz#;kJ*l~mNU(yIS3xkS?1EBEkZUlLpBxYr3IL`g^ELVTq2SaSd!`@m>8p#X|3+2T{uXd|G8 zkW~v09RH#j&5>scOV*1>%BI*z9+TUPIcx8wICRS_2H}e~w3=e&LPRMV^GUf~nE0tAOM8@*2vHMfTs|NX8)yfWhW9s1l_xF-+9#ct5%pER6&Oh%m$a3%nv* zog$E*VHq}eZf|1mn2p+TYFSm?+BQTlM874X00eh=aWg~p=fzCq;8nHHp8Y3uB9r5- zv%>m>>NevEacH~?eS;U4cT)PQgn%XG26@Jrp~^N}XC69{)l-$e3fhE&lq>}-pkrbi z8ZxC{ciwon@qoSkNKLlbmQorCOG*$wig=v-_3tAo!@6RJk+6YKn$KitK`tP5 z8LBQv41v5i(59&}F8|(pORz@)A`#7Qi^Ll(uTFB$=HTxN4p=AmA;6wLjAT3>jlmfFzrpZF#_OR(^Xw+2uLn%W4=L}WPa8v=*!rb<*@yC4V-8>1js{2}D^_kg=-Z^9pyIIcmc)}KQley) zT!E2xER?gNHS1d<&Ig7~LSJeGTZ;ADf$BswY+_I#2IHNwh@#|5KpY|&EQ!_Z zUeIT!%aiHr53FZ;D}xD|Oa|M`@Uh-tPS3~kG+-;h=*EJDiusF;`%3QY4e14BhK0Mu ztC?lyT|LbEWp0(a-1klZx<#!Z3i8rwxibLD0xl&qtbh}ieL#cqHTk0;ewfh!txSq8 zyc#0lm}YV?tw3oM+Ys99N|iCgC1a-Rfule05Q+=f@Ihn%*{~hX8&t6_$ zwzAi?H8i-sm#4gZu2w4V^pNkPRgCPc#)w&1uBH9SNh$SOkGiy2BEcBOU@zK!9Zi?vuK>H`&;(+_Y(PL&_Y?NJXU-KH4eDzSb1)p+7yJ z7$G&KY%51J9hho*-lxTKmuilVjwz9mk(DE8C4%4iw}0a$OPr@Q1?Ii%ercsm{Kcnl zUfazvS!T0YHbL^i2-%(xAMe(w({wSJY4l4DOH4`m_;zb=e=8TD)ruaF3NWl2S?prM z-o5=55PWPfQmAmIcj?ll69QIh7|55@;9-q#(xKfaqcdDuieTrWTp;ha7n=~KOknUH zUuVvoIV{&@a!_nr9BS7*h%U*|-+%AM;t=yU&9n5lG3C6*J_&qlMcV;Q2nlD-Bo(`u zb}KOcvR6YuB4()X3`^vmSCmfb9T2b?v9Rsxjd^)_9vAno$25~kwz*wj4neAc;3jh!1!&Lq_ASc9YA#m?JGF)ElfA%l$a9R3qQbf(v zFq!qJ-$rjBzQ8;+J&4b;vF*8aEx<{F2|wZMAIQQ%L@~5^3o>%$sh)Y6YTXyaPP+GC zTM&tCzV{K)`&jN6O&a}2!V_{}TWFhEzSESGUh1{}gD8u|UC#RFKh^OcU$XF6Pn zFRcKMC88%uMgdiYB;i{GthVO`@zDimoj;!^UOzhjOraBoeXj5{#~tJD_80Lh1`d&sLkxG!MztC(jZ*>F2H!87Q$Zgs4 zC{N&HMgs$iswvk=klP_UE*YN7{dV;lxudW6&BW%c%Md8Y)ip6L$`=8C_+gM$W}d(& z^1mnD7{`2!+%X&=K&0)l4++)4~^ksnv7 z+HN>ahZi$FkySYjAm6JGd`3+VbV7^>Ees4IiD?<6i6*S;RTnvgP~ePc?sB-M@XDg? z4c9>jGuK58mTl!G+L5~0xByD@04Tzg?A zh}3RJS;9VE(f~^4MvjE7l>r+0Xfk<3!U<~B;fxLik|!i4@{ncMFQl3X_>bz^6m z-8G+dCi@j$t?A{U68DO!7QutpI4OZ@^6aMIf`-H%<7|Xnjw4@ul*S*shJWND*%U)}Zdwr?=NV7=LrY=N|6vwT-v}b;i2Ujq4o* z2}Y|$zfCcrGd?&2Lhv;GheqgX4+<#x(%`8TUh*@Vp0H8~E+C(zL?T<0A{IJc(H#vb zajz+KR}1j1ORQ9E&l%>-+t`Sjs}Zbq7_>Un<1DU55Z(%ZH{_+m8{-$S<1{%1DRiAY z`G?k$2cr$?5hIIhs?CYWlg7Xjv-UDMQ+V*#sX2u?%R_SldM$5`IdXAq-u$*mTpC?V zxMIw9EMH|jw?l-jcpXX+^YYp0m*Rnm=b(b!MZ|czd3gcI?3To&j>;N(AD8J6Qjx)N zZ*?=4VgG{oEn9k>6z4`NP$=beP0_$X`2eSu0S-@M3NI@azJkYq0h`z+novD$qkt`| zJemDc!4-W6pku|ytfwI+ZO;@jUBsaePuzkD}M0 zg~j030gi6M_U*lyik$hq70JMvLhvukCzXkjN-T|CtkS5&R@hyN{FcE#T7ji|O+rmg z&LEXhK;*U+j3~Pv{j&9R0!m7M9I~OCeK8S?*xa{o;;QGre91K3GC(U_s)wl1v<{vZ zY?_iqVXPq)^2cM8_R*AFc?84<90h!qSSTXVU)G$C%N7$`E_0IfdnWZ;5FZoMnJ!YQ zzuPG3dP6K4x#xc8qTidxT7~XucxKWpjKLullYxeej=%ABfPDk#bix4eYqc`inZbQjjTxv_%D4hEpbkvQhto5T`9JB(crjU6}xkrNa7a-*R zVU#Y`OVq9}iLWhVQtMxxsa%9CnI`>7NitRclZKRf^iS&1y{7*=Kc1S-iQtq%hMi^J z-6=13zjzeeHl83%KFAQFWH5Lw_hf{GwbX@zVhR{r%Oze+{MJH^uRi;!Z-ywQ~&2ya^oTDvE##K{aOL>T~*s+GY|5)ZLKFZq^burD>&gz9q7@-fLMIV979g?p*2s*f9fwq>y~R_(^4Kju zt3hFm;1NcoLUQE7FJs>rak==qYCHfPm*)&VYnjJc(-`eNG7WKYB`9tC$A#0{NYwff z!H_-D2O~scV%nRh?_4xkE&Sb%j3m_VkxBEwryhjEU!&NmJ+I}D<2%riI6Tku+;@(! z{FhipEvt#J=KgAtL13B$-!JJB;n2y2prpwh;a0)9X5n55dCAi#M`Z*j!H-zv-<6Rr zwMo%dN-vu;_|-q3ESuw7xpHMRj@6B_faM@9y56p2QqGJ@EzJL-C()K{c?`^1^iQ3P z;zDJcc4NoB=Nn}YDCh?nWKm3gNk%zDY8nGW_;~J^G?6Z^AUu=#dL;FfB)vN65wvL- z+2+;cvMd}6zo;IuT2KOVL=QzfkcEY?qXey=K0MJ(1)=8sd_%%3!eb-U)TGV-A*XdHWA)<70rw ze~o6BnMR2}CnN=MS1F$|vhfA(S;#{M75f{7A8Ta3fm<&KC#P)Hk%h|2;z4)98X)t` znbnehgV?)+7Q(o-&0rNe`lh}A>ScGIO?l9LX(QHgn5t?J#`M~R0g5G0cXo`yq%ZtN zW2G$+Uj~y|F>-`p(FHqZu}9GD943mMo4{Y*DeMAeri)EG@Es7{K1E;p>i0l;WD|yTi)$RUd+a z%GUa3y$+T=a|(63+H~vRZCgTCYSeG1D^P2Kcvawi)+Zj|$P?k6-5k5y?B=$HT_e9Z z>myjr%(V&plroI9eCZ`*?^}T9p0&6NS*pUC1T4NLt3$I083VlHMUXmN=fGX??)YD6YEe z%qE7qs|X~{^u_1d^o%gZ#!Ey_LoE7EO2D;gfCBXjZ^+0Jvsl>x0(r;*GBmz}CYng~ zsM7`AMT8Ud4VnS(%U3QVGU~Vf`b&Ef634jBvsm1uun7F&MV^~Pdz4j2Z1g359%IG zib?(1B(b2zFnlD`mlf#X!RuNxS5-}l2}kmh^jqiXnYYQJvY*`q5kot6SuJ$$WtloF!(VcwwC zz!*F@4%kWDGgN91Heb3UUrxLJd+^9^{vk|cNWF3O( zI!K(gQT)#|9XvMNm%Pc(YN&uMq2bSWQeyJqbm-zq!dcy<{m+%QJ%p}LR6=*gQoQ0= zQh^)5oK!(7?#blLt%ku!^0y<-i8ki1!->116R(Ew7m96pdnUEa%SCl|< zKT#St$avV2NdKaamht}%R+MOV|IVoc34D--DiJtZ2|fj|xvBe!LxKxwsN z;c-aNN2>fIlLAvONlQ^eE)=Z%XayUa`h~mx-dlug2dFF}^YPryQRUU%dqICAx2BwD+tr^(7QF3W__czxlT}M+Ahy_)i1l5VGi|58n2Ryw zBjK4$T9#|p4Xh-b3OpCscmA(?9`+E8S01%qhBl%m`KjKv0qS`HT7!a;D_5yfe4Cm7 zLF8q9!Q*>lo0)LUAL$wN=nMq<=9rJU`)GA?BW{h|eTzSW4O;}nsb5oTdN4wu1`zW2 z@@hZ3Wv#Jjh)cxv+CfL2ZFKYY-WVGCreP}L$l8tp7mkaEV~#uW88Iy+qH&RzsC>ne zIJ1O;s+FU6yMucX5+`kHv@vPp6jv@h2tqw%i65irfj8P8!allHw;z3`?X3 zGJAR^(!}Za&-4Z{&C1FmD(Tl*6*)hYdacdcgoJKFW_hwn$*^4}&?I`hczdsK=6n20 zAGE)Djh5MwmRnN6xOi#KU};KH4wM=DlNdOHiZuMg3zkFCdF;+u^){A2W@QP8ff&LsGbXq*HWB=V&89yx z{Q2GCjctR86w+L%P7>ejr0{DM%+$}Ko~faCM^-N1v@W-2eX(*?Ad&OIrkcWz5O=# zBMS1RE`o33=FP4#k_+7s$PD0HgS8>s zEIje*D4^;>3S*|GDtAsmd-0WH&o*)rFV%4e6HbNMn0l)w^%AeF1kO&tW`gv=zvZ2w z7G<6e3>(!Ogg`J=h9_at{mL&lQF6_fk)?( zx4@&5;WR2Sad1T0aTEtgqDeljZ6?BE$wh`^+xb)^G&I49>I5_?V_@k2Yugt$+CBo* zwxxU)4g?7#G9N0Vd`I*EtrwKf((Tfr!#B<>LD%l0w9ljFOsE6wx=3TraYi@C$%stkh2awmr5J-n zqqAn24&MLI+U?|n)RR|udqcT$~E8B}v6T9M3heJn>*iv|1 z{Sx`N(DRG6Xl7aW)}S)w%M!yrr^Ot?0Q{^30gSgCZw2lWCKfSr-}AA=V=$ePyeWQ>}UMA2CD{A*g+M+06K43MFq1jDb5FoqRs41+l}Bafs7 zJ4nqiWIG+Y0Uk!_OI2a`@h~CLbdcvrIV$t8&?+O(fP3r7-SxHh=cfO;iPm3kqVNyK zr$3&9dy@w;hPaUG`JCjjo{;y(vpyQ(naxZ1yO`x8ds+S$*XYXs;`dL8H<@EGe;-`(p$aW9)4b&Z-i zQX_3NGa6UN0#1V2Qg3v`XvDpA<}E-%;I4j!%p$eBcXgbPV0SSt&CwKi@L{SeCwC zQ-3Cy+}VTMzz~JBRoeCBXp{kTHe%z`>x*|K0T%P#l0VjloL=&SMmjt-GKHgTbee2_ z%Y9Gg==9}5%?$ohAs%G4l;qH+R#r_L%sMn(PF7}jhr^0GAUG?f#u;uAXn zPV=;Je}L*1pe;J4(V;Xhjim-eHzzT3nS=I%u5?@)&$tc-BAg&{9yAvISN$@q5_n(Y zH{W|#USpK}1(WHd9x(`Wxq3@p@G1~ES7e{RKl5*$AT9=6@}XOSQqo`C?DLHQX0QT4 z?{!YDw40oM=0^}7zXaD1%1@1jug3CL#tTj)GS|gU%2)ybdk5#~V7FA?J7<%*# zLb>str%J+l-4zKBv{0E6f_bcYZk*aq>5_hs8=^*qs@5A4BdlPte*KtfFp%^A6$u*R zvBaC@DpyVb)Qn@f>aYmPc^&wthp~$`iBb-HJPqClJ0u#5c=cu+N5ov7y;T0Fo&n$bxLRlu`}6(fJ1s9K};5gV;5kIN)joF*{6b=%RucCjf>t^ z9Tk{7uv#6<`9{yi5D51ifn_5_NV1MV`g;iV$iI(!58EdzC_JD~TSQCA{!7r4`zjsF zsa2LqylrEMq4~3m9N3YdER-NeD-~>UJuTWvZY72o?c4Ogn<-VMGH=NiqS1#kXVqJ0?Rz(2LXD z{x)(@xm-z*kHS}6D@_a8#L~Vvwj6w<6f<|YU_U&2kd6b#G6dVQH)8YycgFB}BF4#v zef%<@L4^oT#5?o*1(_pp{HDR!OKZ4EQeDlPV3q5nxWb`{aH7cNsFGL!?5zI5Pkf1K z>EOV$ERmc=inEC9IELdA&j9#?iyTWlDMI<0p?G2)neC1yw9(dj&>za&L%cY#Wgv@< z{pFu)>a85#9&}A6K1|EkFy=|n2_l#$d&aP7B+KTdtONL!y%S9~=lhI+Y$h@$v+9sl zb8DK~?cR>cUSu_abVeT{oE7))#;P8OMu?Ax$aG8Ue<3Bb2f`wlgYe%?&PrV;zus*D z*V^liR?|EXvKUKj9n}h(*bnwY`#%g0Tqb!^U@y_(0sXz6;d;P4!2p^nN+1ZOCw(yZ zvEX}>ro3`Fk1T-!vOG_qkvt{T3iVLRs!b32R+(yx{Rrlgf3+4^Pr#GMCqYj;1~td@ z-}1{$dHjv~NOgF+YAa}?N&~&6#vaDnK?M^*a+UI6$0c7KSBK}H82zjsd7J8JWM=xe z{@z_r8>cfu+P=77MvyG+o#)p7*)M4SM{ifx8X1hsmH?AfzYdU>O?QZc9(SO#|pK~f5;gQP9_Ja2&vQ`mMm@a?WM{IFkJ(zts-mmFpmI0s9j;>9nh;qr(}h`r9Ez2bnt06-fJP zL1ouG*GY(&a5TtZRT2Uod21PHNr+mb(N>HOBLw+!fEvzvNZh3p)L99F8p+hmE*s1v zsvg(slB@kmH~@0X@WQQm`#-MQQHmvI*>3A8dx%_}fy96^GM_VbE0^^GAcZ!YAUN{LYUY z3*sq4mgW(R!g7JTJ-Y9G{##!difOQ=KO(CCv~prz1KHr+fBF4eKf9SLmUwyeYQ?>c zvK#iV;85DNY;^hedyQVc`An~~AAFXv+1b58!)CF8{m!-cu;H@IMvXgroj7|e@b@S2 zE^FP7HlJX#zf#36?Oyk2RJvpDCeKd&KJV*yXS_b-)1k*E_1~1|k4nBH<=-fcg6yvZ z94zg;vUS8omWEA>uWT@;8%hYr!Fo`#*McL;G*O?7dl#mO{l4>rm8lGBBS9M7`#L+9 z+O+4vI}mxky1a7%(~zpMG#0L2Uf<;sXE{@EdgO-Q4N+A!rzS!T8OeKF3jSOZn5R~D z=+v1roBP9Ntjo4`LNz;w2llW#)zloMd-K3A^BlloI9_>_Q&pD241V(($zrx2BHo?5 zcDd=448jr?7L^!J*R!>K$8Qo$=>XAF;!g93XCEH4CM6}3ts13I&0ww;AGT>z6NdD4 zKK*nB^&oP&>F$An4(fVxe)_4iH+Ab5KTH(#{>#;U8dXQ%XE`>fL<{Hf#PuX2HznZ$ zpw5~x`d3sgnO}{y8VE z_?o8V!E;dAQ_4&?KBpe9it~j;JtFoo*$V$JAMdL#hOiwg%B=>v!N;jHnj)oN@{}WG zzT?%9bMfj;WiA>{j#4Dej0Hw?V)vA-P+@v=1fgwO!=ei3O~>`@-@h7zl<<)$xpigz z8ye3@6>6&MDmoMr3Z?OX@kwN?+HIfRdmZXs=gCx*O_8#e!%X5|{-j{;hT~YYj+!#g zW{dXh@ULD?4=ipzL;8U$As(=gytq)RqXmV>5GRmqn0M=3r30 zDwH!a7xAxS$F}GJnZXHNt$TGWCVv_G2~>|EzP>f*xC6@GKDWN)z9DG)!u$2>_c&wK zi*Ip*e7EkDDN`z+L*DO9fwFq@=CZr5^xz75AWqIanx0;gKl=F8pflIAB5vyfXLJ3WJA(^={o;vvL(xunOFvN;N@Yi!PS2PMPB*!Yh@6Mw! zZuI)yyFIvgo;@+8 z%Z;T=mK^3;&+~io;MV0a87}>1{jIO}+rvm{YA!=`NadmMO)_BbmpYgOL82wEF*+tj z^A{Musjw|(xx2ZQ!zX&hQ|x5Q>tdoHUN*%8VPPmjFlQFiD^HE3R*6z6oP%qI&CK#Y z4AL--qc9uvz_y_IA^+q-IDCAT)D{R}4~CB%N%5LHf$xiyF%29c(+{?I!rdTKOjK+KO7WTD4!-Y@Gmh$Z8aF zj~*SO)a)M4{538i;Q-$ApLHOG#bEK!k^gR9Y+AP5g1SfTOGQV_Sj*1SKpNF7JVB_x9TjGP`YFrONqknmk-zrB+xF;ky*JS(e0y%`5T>j3SwDfL?P zeGT_!&YfGgR;_6;Qrs`;Nsq}b4_D)(Pd-_W(?Qwl)#o7^N5S(=s7LSK*Y(AfLZ>nm zU0su%i&1^aSAy7Jn>W$JvVX9j?$LqgY0m^A>Z=x2deFf%kKVJVDuetENo5dwe+iqI zH!ujx%v+2-d@-iUTu%zsTNu`b<=CUe^3weD!T3FUqC&ya2GcGaD>!)c=nN8c6vwe0 zV2<(6y^}vtF~QkTCPs(hbSI&_pxL3Yg|1->YG6J1x)2!Y7V;BciXewSHqh_GA3oPG^;*C}SpUy6s%;w+%J+K-mJT?g2_ zVJxYfJ9p+BKYH{nnNa4rdwZY64$>Yeci11EDvpt_hfO zm{)r5x8II%Ro~z{|5^cDj-L?khjVnyf+Gr1@QyeRX+N_wSIEoKLrY zSc;o}{C7Vvf4PqhWM(;$zj(OnWsdb*Ir}v87WJMJmwe+tctEc_J{3Il%s_=}YARVF z0|~%^^L5M0o4;a(%VhpJJ}xfx{qp6fH6MvIuMy?#5gkqjZSh3umX}kTaKh+&@@ix! zj8buhW5FR#Qwk&YX||12nGdMJ9(c;R!i3VPKxc3GMZtJ&HU3@=^w6!J++37-G&{Q@ zW+lN-bL+uS3&2b8|H+3E8SH77R1Qsgb>p#4wnEoGJmA6TTKXRH@#yRy;ywCTp#DIX3&x4fvZj9oV{Z8I3JxQ z*u}-=7L|Esy*hP@%Nik7{A2Zd2!QiVg$DJ}v`Q|-7iWbiz?%zZl2<_yLt=$j|x0%P5r4`3^U3q;A`` zZD9&dYu%@t2@%jpBhEVjZ*KeuCyVL=Bmqa1_m^C)FI;$l z-)IW>$Bv;$08&X6!!{TyOB(Ry2|)5L&dwjTX|pmU#1utsIRO@oVs+W~-s>RmGM%sK z;W2#s1V1Ymz{4J*4s_M_E^J1uaVoh_n$-H-cph2}rOZ0d-9wZ>VUB4wG&A@rXA6{oEYTTH$ zYp0QywqR`O4@caG{T>eFyWmJ7Zv@!@jn5HkTU_7{(F#SPq)H{|QNyeS1Zs_oeWnN^ z;;6yuI7C&npIj85%cS!blizSStzBGDd)@*e)hS0OmbaWou|QE90NZg0PcK|x53@9m zk+Htfk9he%onCdCPok0knaPFkfp6cYM;k@|%(&xNdip{Cel6m*%N5BF?b>kMPS3i#EQexhjO?{mm3%z)Ql4Nm2`;h#esLtdw>`THF^g9xD-1O zS$?v@U(2FV%FopkXQnM__NYsvzqfd7_LaX4^C^hEpi<`it#fRiYGw&Wicp zx6S*L^Y(xTA3A&(w)AK4ohLBm*o6-lhee=kSgd312!I1Q+~DmWfBZ31ybo}oLzra4 z$mtN?dV&MLIArM1Eo1L^f$>kzCGs2@y47vb-@erhKJS0}GjD8XGvNSpfW>k(pxL>QvpwuVXE51S9x}gxVRtaZ9y>Oo@X$H0TXgFu!ey8|pdo0`+ zg6XY@Bz}jtoze!FqaIigCq^toieL7F4~|TI{jDxtx}Y2_!=^yB5US>=U*9)t)jBr> z=1&NmI0w3jcdI(dMJf&&H`Qa8K7DM0B0l}P1>M&q93wxfQ)j{A#T{KBIc^xRuUcZ( zcg_1#p*X>6SlQ6c%d49UL259z4|-qAm#0JTkE3+6=K*qVM0f9@r&qd+9BD_&Z9s73 z)7;z#4y=)%y@{5k)|^Xl^(G)m`GR7O+EI)9A`K>%5j9)1Sb7nf-BFtHnPFkITee(Q zso7B1MTZCZP-P>+TAFfV<)_OLyh|2CAn($x+gkNvpds_J{5wT%=hc5d^2*0>w(!7` zAR?2hFucWx%8d2a+aV9A%V&b=EoSh?@5eQ;x1SH&p0n2%9N9<_xuMVZ(-3klrOo63 z#4aOrpY&9Z?p|GWkrn}2g02yb28;R-#Lzgwd5s3_jY@$~_Mbmoqv{(`iRM1&788$w zhzh9NN7W%_cfv0lOYFfy)ggCh@8}^z&8(ukI6qj=OO~s!?ZwMyL8Fn82Pt2ifKQf1ZIdhU9_Vl zivJrQnSYQdRH;?qM=-iSs$KgV>LvwRmyeIObsGIs zDauLuOnX8bEJ~NjEK;pSmNgAn0Evc{=-$JJgR$Roj&dqT7t`-5bD)S;qwB`-JY84e z2=qO!v;-?Y@Wp=YrS3g=z_Xji;yweHEAPhj>r~0b7<-ZqLZKU|(_x1bReh1$p})BK z)Sf{BHXErJir@_)SaGMN+#@ta*!vU{lyDi;coOFFlZY~8n+R?Yq?QNmgOXNJD(rCk z$^YHUvBcifWYeUE1Rv@YUP11;>1^lweVOyU_u+?g`mK@;f6`K3t34*!Y>botDEHxq zE@Q?t4}4y-RH-41yr1=EJm3RWQfvDv2LeU{4M+P?_tkFHXm-Q19HgP&qp5h(m?S=H z?%d8vQwW0KCo`_5UAeLX{Sw#uq%k$g0W!aFT}KXbT}A&z&!YAJ^FO~q6v==77ishQ zpZ^9zaOD5UU%gpI0$q7n25*F_ej_(`*0XU>z{Zqzg|i6?p}`P(RO%a`)4)265+B1T zZOZWa?XbfK{vo*lqo%mV_3M|Qyth0d12!pF?#b)4ir;Zy-~Hhj2`^LS{p9XB1YdOJ z@qWxB*fMVkc|6UR`&~(KBw+TN1xsZ9{P`jAw8uF71t$`L&QaI-r=K`+lMcM_9VC5d zynqGy&S;eI(F^BN=S=spdE?DD2hqRRYumQE38M>UHD4n=jM;gIB%l6pGrZ z&;k~G{p+q7L|1nCX|zEHX~g>M3akSToXG?>jUbIRR|!xrRW-=NzEQt}<7njvXwtS2 zhAGsc3e^E#E>*ov6uAs@r&L^Xkh{&U?D$SB>FXgsN)#K48 z{s1zqFl1UXib*U*j=_nxFZInu8bF=fYWb+|z^=#vr}<%QQ;%M{w48pX8IwZV%=n(b zmE28Jsp>HTU(K+Fqw8kD)mjXBsgfX5U97xX_4iLFG165NL^nq;?KF6FUSZ}_O^30%<^@Q_eFi0R|AyDwifp>ia@Q!#gPV(5!CW-8y()oiZNgH}SL zCR)p_fw#J$S?N52#$*pXphkT*hX9yFp=ThaUB^o18eX-WyV<`62?`6GYf?2dBXMgA zp)xfukg}lv_-nm=2TQv-%=Cwy2Mn#hB_B0Av%U(z-BI%1Eo%=n$Q8OD%1cI z9wNH^o?{sD0>MBKjeMuGL7K~sP}iN)Z8o(EMXP0{Mc=nq0P?FvGjjlD2Nb4l!?WjN z(=c-9wKt;_zMu{?#fSbImWf_Ila()ak8P}~u}aIiu&?H;{zg-3i!8QW42yxfsy#@6 zo;~I0Bs7oIe-FOKANdwcXmGzO8TiNb#F~ZuPBr~KT@VIJHckOGz|PiC5oGqJyoZ|N z6p&-$wOyX@^9p1{vmtOSRKdv*@c~8dX!LEu<2VrE9a>nB3{D|ENtr!s=FHWrSBL1i zLDmvvN^+8I55)gcRa(qucD0$8@#Dn&#$=cH#tj<0oxzESu={llzaP$4IfAyOsSe^&)*C-z!nH>ynw?fl#++l{3q$l)8_YR?FVLY%GmJGnif~J;~lSv=Vl9b`a~g04}p#kMe6YOwlpm!drnT zoN)=Rk(89wZv^NIllNH-7c5!QiPfHafO_|HZ-8(c`tuk%D0^o0h3jmTk?8zB65%Ph zE8h`wu9LWXAX(RT9F?qr65`20T9BY6G22legaP|DIm*AeDNkApP4Ww@GeF4J+>?%faR>NECDnG*cs$(J1W=RAJA6R-Lc z$~^$)Ezq^#YQ!^2jUbwpW1diT-wa%uRbT{{VM5s&KTD2hfL}M7(smdr=-(_T8vJMF znLXycSFYS=Y>W7fAk2to24Xb=qi1@`Wn?D;HPLC2#E$AY$izHcmNz(VcJ)RA=guS=)l*l)7*>1AD2lk6BtKUDAZv%- z^&EJvHNq6WdDg?XY_5G~M*ccA1@nc3sgh1t zhU@s3iX>>^_cI1c;}JF`eBJ-Ls2Wi4g~^n32+oMQpO|I+C(feSv#wnSON`{;8Guek zWEni7o#g>R!a>6PCfx{(n%_G-Y08j|3*EBli}B8%ODVof)_Yn+o2D4c}SN zU%=Aiq+4~KG5n%BNdl1y`|L%W+azX+vCHtyty;KN#?z!wV3ack0X#XKS4}B(yotUc z20CLDNV8v;pdiP6XjmPC0NIaZa#|a&a94Ruyl?vNgcDF#^V2W__EJ0VG$5k!=yC~) zbb>w-IuEWIJs5L2wb0^j@D&51_RNt(`I#h}E+#yZMV-dT_8Y#(x5Ys~$i34jg zf2OE^)|(-oUP=jfaw|lhTV61DYeOKurGNbZJ$WPVl*N1rN%c1Wg0Q(l40kj72Tb~| zt(j5nIu*5&5E$e&ibcz}Hy>$KN-q6a=Bx{nvYIY-VgYl0&g7V_0!#l#MUO{?76&!e zVA_UE?EmhG*V;T9+Qw6IQLkmtfmH~weXsxg^XJ|S(oYDur0ezBdg1XtU2qJftsDzR zZKv5QpotP5)yey@tV%?^nxiR()~FFeXv|=H|N6u4{gdAMbIWu2$JtSw%=%&BYkX{Z zjiY)eE)iaUgVtOE{6lQyuIJ@-8^L;hl$TW!@E3*V@q@``}o}@jm#@9?uGZ*}rAd zlR&|Dq^VPJZtLRiFxLF)!j|HqN5wa$HH0m!oTP%8S;Jy^dZB(4+uwno*HJ8hY*gr)q?l z^6c5O0Os)3kxdmVc>CRVk1%-DkbVMqV!R*uRm7PTp~wVq2Y9IPy6OK*3O*2l>pq6@ zgXLa~`$!$ngEZ~Amv%JK3Lp>w7@naij?#!0Kq^L)ZMs(fuvxQ&c;dM69Vx_zCw61v zJ^GHVsPde(_gDg!5YREHy0QFT7Kh%}fd~)uUb|`21#o^4LypV&`RAilLaCIwnkQsr zEP^wvP9-X3++96h6^|4qyxTCcY5A&EPr(0tq9Sj@hG?EzRVu)X6My@qE5Q(6a4FnZ zY^h2g9YG6v5U=G9G^eWKKrAzVqeQp|@tOX1Kh_=x0ikx{<|eX$euT>L(6s=p zNsr;3u`^QeBWRpJEuPh_o>QK_2e$^&?ESVcAGQS$eN!i4b$}5Rw-K;V)i(4dXILH5 zdiES}Alt-UZzVSt5NU6FcrRP``hB-Mn7(B)q8$DM1Z; z2w3R1Ya!N=t`))0C=)oasj^U?#*OFDR{3LD-j-Mc_B|spp0M?d+20Vx^I`>1#znx& zio#wx{F8BZSno6`z(d4`7C&64l~ZkGe*=j$iuuQrI#m9ah*Q`|Ja~(Bu?~lWaDM-B z;~#_n7y-#@=DvMxOM`*7M!6@<2i9vK*_k0piBhFL0v;fHsJ6rwvn0drn@nXx!QQ6K>A9img)8E~x_@K$Z`2JUwtyj)yQ7bD~L-+JLI;b4#! z79%>EIF0s!Xxx^;Pu>Et${Ex)^Gm9J@-nX=uJK17eaiz7SP25vitR`ckpYfi zVo_P06K3{nXzb`(o@aR`vilYsL1L9ll@UbJ zkw_oFdu@6HdN5L5m)>HSR#d&QC%||7#y~vovMnefaU8A1!+%r$Z4!)F-?Xw zA~Uy|_^7@m1w1&~%m9KC0hB6Jc5#&MsVQ3-mwbc8*CHxuAd6fUh-Y^=dMw6FJY<{) z3v80^+5h2hzje0kyrjkkII+uCZWsE$7ndm65S!@atz8$Hfet&t=_I!=CO7=cH(OpF zJ>UNq!6wV2Vbc760kHOe?Y-{$PS{=&X2j5*Y~Gx+gw4HWOZk>9TM}}u%e&ly{AE*T z9y5c8e9l-oh+r)$9CJ6FQ>0uw_yJK_*bQdArx0NLo8P>1>RfS{c+%xxI5T>ig?-jw|@?P z{N#ySUod=PtS=KrA@PNgvJg`XoP)7Tgm}4NPth@Cya(h5Byb(68}2=N^bx5To%%G6 zl$?z`BnSRy24%^3AT4`MglVW3QGHUL!ffgJPr6Rv_5FUJ|Ff&NUAcB`EyfZGyx595 zQiR+h)W*bsUTj73!PHXqm?J(LxIxEeFnb1ipHp;!&Fu*0*{)!ksv?Nbml>36PRt=A zr>Sg51qIz=c3-1Kix234P_=&|WQ#IIH6$3(A@zkcb(_65s&FAA41P)>IDwg)>JKva z4T`P9V<=qYaKoTmg(Qjn5HlR%mzseIZ;AxsIRzkh1UH8X+<9DXSd+b}*8=PTfyob+ zK+OMZfJx8ZW%d&S5yufN8qbEKk6z;>iyYXMYhoZMe}np1^#)F*Q{CzJg}YJksbq~RE_eY(6E_`5tmguB*B9^0`~r+xw&;pW}?(#6G) z=9i?XB0?keO{8ECP|FM3{NAH4IQ)oLujZgb(e|udkKK87KSZw(xOK@;sD($c(*>4J zKqA=mIsb#|izTg2GzgcpT6Dw~hGkQ(nNb=T3j;8bsbwt=_LRNNOQUeob*9dB07r?3*?EK0zwU{O8l;nm#3 zCE^^Gmip&hRDu;zSgjUNH-FhOH8AaHu9Fdu^jZc)AT|(B0A|ro?4ZkFd$eDbi-M$I z6k#u^DZ@gDY%?QK%}b`xvSmep4u5IKp}G#D=?teS3e5%Ztw&WHNzvMA1l||FT-q@Y zIr0N=aW~-v(1g~@sa~@)L^h*<_ML%oiZ*zc8@@GaJ$MVOLgY_$66_kCKX4z<^ZQ7; zr{fF`IaF&HTaqwL=%ZMdO#%A-q2k_Rn4yj`pmN*b)>5Ltm%&-%JsX>u#x8*w76g+j zmVghjYCg>>k*lE?Od|+~x9Hq#Lic0wVKADFYkD@yxO>Kt%!r-XW!S}jIOS=^Dtk;< z3{)KGzg&e1hnb29@q<01z(TR@fFD+r=Add~7<+P;mjlxqPhz$LZU!TbdlY7_*QV}5 z!((notZS~I6TPPjY>H;hM=49uOvH8n>Z?Y413aS6F3y`@aUfs}?xT z365$R7RwyZ`@jV4eZqlfeZ%4DrU+Y;n+9P*BfSSC@A=XwDZ{@&Z8taO;r~ zaO-Y5uvfANc+d!F^)qmP(?j5p`2*nMtrXx2&NFi?Cj*y8Jbd*Ew0HtIm;4ww`kV^g zJs<%LbkO=)5H6Jj?gawY5=y{965t{QXpC%Zsq0Nr#vZL^@9Tpw^%CFmX{;BMCt(2yo@<#dV! z8*uT@W8m>bD!?H_;E*D45e8^R8n{A8=@qbOXgZptXkjr!&bBIGI&cf80q}xYc=mqM zpRS;Ei37N_7&u*ib}G<2hk>isc7Zmj>;jI1fM)Wi{sML^{eTGmdKI;Vst0IA0*MF0Q* literal 0 HcmV?d00001 diff --git a/test/reference/line_zy_polar.png b/test/reference/line_zy_polar.png new file mode 100644 index 0000000000000000000000000000000000000000..04a23ddc499ec1b1a67cce1dd99bf94e301a0bcd GIT binary patch literal 100971 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ð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 KKQqo^rxvYDpQ#>53g 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 Zu(&-VvZ-A2?|ClM^_IWServ}c{|{(zg0%nu literal 0 HcmV?d00001 diff --git a/test/reference/mc_ecdf.png b/test/reference/mc_ecdf.png new file mode 100644 index 0000000000000000000000000000000000000000..145a077cb5beb518ef5b5e008ce57e22639697eb GIT binary patch literal 82175 zcmeFad0fryyEbed5{altQKCs2P|<)SO`6bPNrMU{wK9}ah)ShdMUkN*LjzJ#(mbgU z3ZY>wq0m5z)O(yO_WgU`z2E)WpXd4P=lSD#e17ixrq=4auIs$c^Ei*=IIrJHqm9~A zx#n<<8Z~OF&W3d+qee|wJ!;h0`I9E#Z@eC+S>j)#-A%ODj!LT)`iMV_b6jJvX4I(6 zuqlH!RW!$Jy{H_W2j8*Z4 z$*vnL+((U?7eK#9hq1fu@%m9Z>(*@Y7~NSo#k+2Q+wgFC$L%`jN4xg;-*r%&9C1Ct zPUi5;nBYCEXY9z0PfzNW?ibgZAuh$-kj9c)&@Q?%T(e>=SEheWu8PQkp}e8tAD@SZ zn;!i7oIN>W&-owse?3bzoz45tuZS5Xrvm@^wPAAG`gQ;ON;>W)Ci>4WlcJn|UdhWV zY18!2D+2?gh(|+u#+n@dS>C2`9jnr!9!IzL|(vdI;~hlWDPA9 zqy9#gxl_cYV$0xS0Zn83o28b)8{F_O0YA4+a??~5l>}ocT;O*&yudXkL)ya>a7>HA!|5)tGkG{kXdu< zg(x%gM%JIr=G*^gpVQX7undvs{OyKH)_-^0NXFdAQofPJ*i$Buho3rl`bL(Ag;VaU z5P4M+d8C>vu6yIe|ys&4^;-Y(}D&&w_n(|uyQ}V z7t7Wh!N8Qi<^Hh2AOah0!+Df6?!WKPLX-?ud|m9W({#G>(;L~$v?Ee zDt2iYzAk zEL!A$?8dhHG4oa(X#UWcDYjLYe|Ws0$wkT7*m#|m7Vo@y2JtKQ)uKXRoA9tk;YcfpuX8noyn5ZaCCQ~7_tIKup_wR^;n5@}OOHQc7x3u(r zcQlXj@b45FTI#5%no!e~v2;m*{Yl5kQ*}V| z8CVOP0ltZx-5g5Fa%$-LR&VHj5zjl^U#{UXG|--ElFY^2V|mwEF#W+#DSl~n_49FY z0<5B>q@=EN-`~Mn!LuXYh0a^$svjqJ^uvdpR$iZ%e|m9K&uZc{q2`ZQ63Ze^pVoT! z;BfBr`V@n+QTtm>C*6Ha0nG?&M6#yw{{AkTIV+q4+d9$< zQj9OJuh?E~sb^ynx5}F8TYH~y@ooDSkQMa>eb)UH@0@WKDlwi(`jSxuSy|dXZ}EW z#(qvtldsEHg{~j#QTuRNkAr3WXOK)bs z)m?*_YP_>+rlvo)4Nj1dvg;9kE03cmXD&8r`uwgSacRL|q=J3beEzSm_ZF~!cBE?$ z8UFh5wY_?2L6ZI%1xC;szX_SNJjMP~+q%m|$^|s7>ePM+^}erZJ33LgxgtVLwWmVt zQM?jQ`qN~+`iq(q9cmLQ%TCRGdE1c(kCIT=_xbh9EGPcZt?s z%V18m%Jt?zi$N{j(^6VdiwxqHJIy{dXL&20-@Ch%Kd^%Bc+>t%Q?9bDotB@GR+Z}Yv}e}8YNzwK!Fw7#A`qh5?mi>3R@H^s+Klq1Q= z28?$yG+o>j7Vjc)VuM>`ypyNb5S^{q;2x;F`7b&AsJ{ zjtr#LO~@$q5Y`1Wldl97CwYi8|vHNDlVM=?KFR951|O!W2j zm3@)x-rn{7dnq1H55a!2nq7qd)ab~5=hOD>W4-d1dmXYv8=ReG?8-t4Gd+79c0ap% z)a?3}n^%!`0;bKgtxZr-?R}(}P&YWx>-Ox1MHI4hL__@E2bCY|uefmwDjbh@xpz|I zX^O$zdj~#Og$OE|XS>WpTrFOz^+Pi%0OoO`LHe-nR*JYzGbkgm(S zk;TV~uW?k_ClgS;tMB0AU5x5P6PM!WJN}j|SsqTwGFw9j(y=z-;{3&i=XaC@@=}37 z;%{T-UbW1U!jb0>Q|l8@dg~!JKJ!J-Kzm~ETd%Ja&OXDx4`P8%1_sVwu;8fA@1MNW zr?0k&k=<6+-`(~y!)EN6GiPwjjjn8*_w(CF&Gy=J-B_=VUtbH_^MAY*TfTgjwu8f> z{6pW1x<6bFdwRn{kySN-_ZM8gn=dp##3yd{63g)IhYqbAJAu2oDc2n(?C}*t5uv?# z-4ir^f1CN^ov((xynNp9&@b<S zQ~b)tb4A8kyEmg+mKO~F-kWy+%gUbDd7g^cqXjZDA-rPiyO3#n9x2vkARAt~eA%<* z1i#1E{2v<(3`!}brCa5@s{Z_Nd70CT5Dg~kUmlam>=)bL@xuPuRbyqB2Lld0J>A_4 zupr%S<+l(2I&gRIyN#b;Wl2^bcQt0(O{62-vvU6@zC@p&lA)GayPsUv=8=7-$)i?6 zwZ^$AC*tvABi%W6+yFm=wGJNr2dY(Mvsr!?&Tq~YE_wbtLNE6G`C5dSMW*d|l%fkr z>YJ`_Id$OkD_#+iFnRypmpN`?B8R{6zRuoT-q!8fTccEub^FwiUKg`ut9gNs8V3cLJuDrm&DYl9sE!{%88E-?Ff|mp`k0@eC(YD_O!RRm)}t_ebL5*4OWhI=h^h8^c(eT2P)F0 z8|J*)C|*87wnN2f(`=V*3JMD5fDM21YDg^-IE32yuqk~kmuj9oj#A6#4d>Zynbi74|^=PZflP9yXc0ZRw zMs=f9)ZgFldXd7!Z2Ms?9S0PIc;D)UtIC<;3%^t~$9x*9NPc;5zcih@RR_N;O#j}| zpy>2MMt!K?j+Qc6_q0UCs;_y);R`e?YHCWIQ10{-)z8@!`vWPJKE9}V5{t;$`_4{h zPW_rCy@eIJ>dX9egik$(7C=MKKLsR&=ddjGyP%cgijieX}HhD~urRh8|B z#}~17luKnx8n*k^biTZu+g&zSM_)f_KGKxxx5t_ZEB6PD9y?(=KYsw)W|}mRUtk zttBUcmM3X9q-{f5=TT~M9lLASE)+z+%BaOr(a|M%KmirzLeKpE)?od!%O-38>ie9F zl$L67dl7b=zSFb!eWuHsm5084DCFPneYCB1)z=*#pC-#}e~_T`&U+@R(fhi@u*>UD z&8ay31Hn~~REm@2iJI)$aWlKi5=S28x=7yPk-)W0scU`q7W`g;AA@pu?C98uPcv*Z zfiKEiOM}m>*fXcHvU1<2XD63zz1#ftYgofA`)UC>>rtUdN)x8eS%*DUIrv5Bx>@@B ztOl#L=0d-HAD^DUrWz;f&g!YItZbpvxn#*HcWH>d*&RM>D9pKF9dTr2ERfrmCY3EWElcsK%<(uV9}5>PYW@1v&S?%Rha%7pMywJ%=0h_5Ih zyeIGo>-$ZZnq&Us%|Vgi*-LCIBgIvR`r8PB9yu~9&&rcM3*FS@Y17`nIruJF`Q^P; zQPS*&!xlRnwhnqvHH#uxS*o*T%j}z5vlpqT2x@n{xJfCKT^+kvUOoaCkKzEaTZvH8 zLn5tkYRs?+5muX{{pQ|&oEdQx{F0J&1V^#nyB=Q{&1qKf!rNH0yL(=*Keuv!#a@I2 zVwg)i+vUx45ESJjkRN*~87i!%>yw+CE3|T7;HkMvJ@4c9G6B9t#l!>=dIMAEt_VT? zId<$=|Cjq;CrzDNdL6}N--pLX(GN`%QhwN5r%{KR>6Xh-Rv#X87dqJFHjx6V10~lH zbq^g=#=egx4}N`F)uX#{<9k0I^@mRie0}|=%?mO^hRHz#lsoV~UQpdz*{LCoFk(r0 z`Ai`peOFgG#G5jDafFbTY;9Ld@#IO9n%AoKHQFKhH*ZE%tq&8f!8=y?4)uz(wzlqk zc}rYSP>{X@<({x4{!*gi!Qo$5ab$w|mo8eka9p{8(i>V*9pLqk?+P>mctq%_E^IIG zQAXPh#I6vEVCyR7-#&fDjAIRZvfr!+Hp}_-<-U%gp(6qz56{brjgCJ4`m+NP_`c7t zPUoQ}>TKLt*22u)`Ra}&KhD-$QFM)mfA!I^$FtK1L^O(_n_l%L#}DO}`DTBE+X4b` z=ms90SMzE_amIo#Oh^69*x#{52>sKdg6L>IA!O|1$B)k+UhK4Fyb4n^R+`-}$n#RA zx#Rsc@Eai)a|BlIlR&cCbk*o66;HIJdFKy(l1$lnuBzTp!(jJrsi?(h^d9zaPs6yMi7E699LT?XsED`ExNGEJo z+>I{+)iiNzs2 znS%Q}8zZI=Uhw4XS!EdCiYiqMUGe14sO)qzgy*LU|kySbIW z9Qn?_EhXON()_U#rzIg}^#A-BNl;q{O;bcf1h=qi+%eVP-|CT<5)*xg&eW;@3IPUU z_4N0*h2ZF704va|HlJL4z8SUO5r7$O4ey*eRUe*QR!J=Mn@H>ClkfGz zp)5p@(41Ra*^~6`8bX8y3I=ioy@1qQSqLPUEetPruEyi(kU2={}nx?qkRQ6dJ zApNoA3`1)3x0cU5L!ViizGgnjyLfd1Xk zm-r)jRK+Z*zW?<>Fh~PWB<4T>GsS%$jAXUgRi_0N9k{U|bO7he%7&sc)MG(#3~>Zl z=e@pMym||@zU_*8Ze(a;x9j2QX6NMnTSXNWqfi9KQo8)ukS;Q?eAmAQD{c~nU7;DR_Y{MA4~jg98iQlr(PlI?j; z)iV;`-hw@wC@N~AFw*dI5tD|3IEjl(G#$yhwcA6k?D2!cUR>_&=MJ_qbGtlLZ*0vD zy|=%k_3Du7pzzjgR|JP8;N=9oMrP1sC4eEPnilpbZNe9Z19VlTYHg0 zZ~Oc{#Om4gCQC+k^DLPUG3TBPH{?m zg^3vhN10AFcjwcj$v9+u?}1?x>e35_s<03ek8mDyV+8DSE^#v>Rya2vDMlL{AbcpW z=vzla7=ne_*(e_w8F_VUwrnWM{{-zYVKs^E4^+n!%Atg*qN2hec4)RNqXKQ7XJ=+P zt=|0upG8$w6Y%z?LW0G!?8AdkCnV5MbEDMH2XLp(Eux+PuiMHY)((MK`NI*R);66OW#l>5|e^Kd62un;90pEfW*!&zN z4@*)8WcTs$#j*Q8J!?USc?oU9vOTY&Is46G?GQp+)_#KD>Vh27Q!xt1dbGG$r}aa5 zcp{Ih45Z7=+Mpgu`w=GkStil~T#voOy;w46{(OVNo0oC18vr>Ulk zAN=myO2}soB_8a(^1ct#onG8nuyW;`a#YrSuvTvSzBHXVMjW6rVo(_!EC9@v`9GT6 zdBAusE&xIyWR;a-YatSPu9mGUH$iG#js(|@ECD--X$QW(Jtc+~yN6wE8i#!nq{lZx zFNHJ!I#2o2vs6tRB&Sqx4ymcBjB9i<9BSs!$%vLTb?o%XksFz4Vqk zLuXd?Pa{531dN0_-{#LvN4YbK11KvhE*=2$`ZC{3XHT)eD4d++wVzRs6^VrpvVKnOAyfGY|%!}kp!J9&P|ZEwupD}q1T;djVfrD@r( z4M6+RiA7QIn5XU?=iXl1Lb!{5=S(Li28epY6W+OV^?*sKm&cA@Ie#cfV+M9~pvkp( zst4-cW1x0`e7EEN{!Dcw*~@=QD3An~2@*$|KPbt^Xn4?I79$CGg7Ky*f+RuQ5+cDQ zeB-&5@?hJ5ka0MsJ$dp(nTWVz#0)}$Ylg6JXah=T8HWQUpvBemRBve}I!0Y7JMI^j zJyTLYX!fj03kaFi8Hbvq@bYd;Xy8Gd?F&6MxG-7yeCSw38UC>X7p{=<6>`M{u?gJ;%`lL=_Yw^XNNJQ)+Om&GS6u@rd)Vkz!!Ykx6fXD4)I5 z>JhjUSAeq7v?juyEZ#XvE{{$~n57As-0=qU1Q5U*fNYD0(oay1=v8rl&)x10+9eM~ zGq~+uM_RF4Ysuu=#duc=21<^;$zU|z38@{5MrEPKZ-7uAQWJGaGiJ`TxPM^Dh<4&b zrw~oEjCsZp5EVUTbN4oPbW|T3KgIRV?&m?x&DLB`v8r!9+vf@4M7O-yx|eM`2rYyW z?2`JW{NCq}jLE*iQU=D>As3a2po;*{K0qp0YsQOK>f@zXt%{fa3~Ia7>E$i^1ZB5l zl*)vjr++Cg`fBJd{_Md;KA#8v*J}hc-C8?3f{WX_o4Y-5V)@GOU;p|h;;8-r0|a}~ zF{N7NuXKO^sIaZu2-OZN9fmB-5lq*m9m9^&rr}6ckbI>aDx$Fw^j%kXcZ&&lipTg! zXO;Gbww0Wm>5Y_`_q*>kXqXddm7nCgJA!})JWQmW_;+Yk zHP<#@kHc|5lbE#a{sF#4;^HTIDPLGY&QeBa5L-#Ma0Ez01^RG1Cloi_xhS2i^}Ob*zfjW=%*7=D?A#5M=oQR z`BES+MK9SJ0}8AQd(sUSD+L~=a< zSCvgnOq3Ig19+k$B94klNpWd=cql)3@L-3dBc7*f08ifINaR0$*mMyQ5vfgAA70c9 z5P+g$nxcPELb1&Hrg*{C4n5<{8^JRjRT(Z*UA}3{TA_M`HZ?J@?yK>JC%`@w^3Ixd zLQGXvX#4ig=~N|pItW#J4WZ$V{^LIxdIdP z;KWM5#GVuk53NFJXbq!k?|T0YmcS(L{+D~fs4EE2TA-Jg_YW?8VL3QBm;;zEp^pPI zfWD{_jJV37ZxVa+zt4t>)UuYexwi5!(?|=Fvu}#0kMiX|ICPU+Pw70HdCe+|Z@<`J zcrd@9;K??$aumeiXpO>AWAm<=rH2-mmj~>Aek}sLL5e}VgubOE2aK8|CntB8%rtEN zfR;D=?ICGuFNkLbp^6nc-&!Wn?{%B0zuhOYS~BBC@a*ppt(Gb%%t*%@BQZ;Ez#CCD z{?C5Kz|Wsgp->UI1~|Rz-2*iu<3o}6!6%8wbJUgV^Kod(QDZ8uL-8T;E;6zR1mKxf z2W7_#D!B;JLO{Gy?Qiz;$VX5p>&D6&fcdGcd-qNxw)%vaoZR$u$XWyeNKrs9xC`)e zU>-6e+O9;L8admNNl70_gX(RsE60Km!P(wke%1K0Ikwxu}0G#pzzMSJH?3E;2t>u77yiOM_H3W0Xf1tO^2i_Z=& zl$m7bUA?`e(o)4BTmfE*S3qEcv3*FPrJj#`c67^ikveAc&S|lF zv0G+Iuvd_~BSfBWF=T^3Tbbk578o3Sc|JaXEQ)##Lza@U8BPz*BPm-H-&Lojquzmh zfN6kT7Z?`i2o;FBN<>5p)aVPq#l-TzA7S}vkwT$6Ma2S8(-|q8D8d3zYylbk9z}XF zB7$l?_Q}2}$4&Xrk1wav)0a}|U%Pg#GLtn41n4Db*@K_=`WAQK=Mv08U){Fz#SK9# z68+~pvR3wqoP_=s;8or0dfLmk^pxTec)Iy!wiDNPT%*y>zqeqVhyC6V$ zkh%vnEy&Wq>H`f0Lnq9vt>+u6edi}R>ey*$o=E3Gb|3>8Tz1@{ z=yYfk!7TG%L&>6^0((x<{S4GI(z~Fx6VoFN$c4qFaNc-cRp*R?y~pRQhJLYa_x9NU zXD6t6#o)XX@$^27It|aB9ryNqTnjM{eU38t4;4f8fraQd%gW0ql14|gh1mL#=IiMX ziU1Bls)E^Cuj9Z87<;|@AAbB;{Wjrn2qTPL7vy9Wde$acf(sF&1Sa4D1Qt>ONG%7O zgO=u`8Co(hsFPVw#lRI)VPTg-G6@D zh3~AWsCdZHN`d|JunhDp#GwG*6Agq^L!_oFpe4H8lUXq)$+}2;+@LKhrDbHe1F~YM z3j(D|{2BV$o^lFV*)sf*xKp6l!{3Y%GL^)hURdK-aUE0Bmi@=Hu-{0TOoxtZVB7g^N zhbR=JIGL??Ln0pu_+`Z$Yhu)dSsmQmT+CkF<&m@F$@i+R9}a8@DTKB*1u&F=!J%JY zNf6dQxAHWWk!bZ7H!L`2CRFC9pz?aIF>)1_=5>u(n*VJEGUR3CzdOz^Bhdq`*!%Vo z+LBbGi(}@k+-Hp!_74oWT?D-IK9;c`Y(L{|LliDcGHj zxlA_k&>DkXV?(i?L-376nW1EGJaY-0!|@X*$h|<;9VAFm5!7erdmA0o{%+oguO_HUn_2jfh&*@xJGLvL~`pGWN&DQ#dwXQgw^ zlInV9YDn}bTuGk4@feN}%<_hQcA%Y)M~G!2Kj#${6?qJHmwWcN1XQN$L@ff;^e-3j zO+Zw-gIprg$qc8FXs+xx2j&iBK{y2^N~bnFDykU#1th?=X*clFZZvbC?*$MtAmLa_ zAX$hTK{cgbooH>(=c%Rve>hv8pC1?Bb$JtHhMYTd@kU~fS!Vb4?LwFQFizf1075US zA7NUL&DS=a1alAoz{*Uuyt8u+bp>Dt;TW+2uGwL4?{*RAkX#6q1W;Udpn-)~4w^4` z#FA>{)hs8o3Dbl&tXnsZ{~8Wn$eiU|+K{54mocs)iJ$x_zz6&Rz};d0nMojH2j{`idDaF082{JB zW?TAfsb>0c>XERpixP0$GIANRFn^SgWCBuI50DLN1M4{o52@Q6Z1eEQX!KZe$VyCBa>3f`4pjX(7@ET^ib?TywvPJPuvW zg^X)R4#>mBfxKem3%P&r>wAa@1efu_;GEP!e}izz@cQ9G_;nhTt5p7vmZFAE{TBC*u9K)4X9PlluHik_0>zXCmO-1xOndAhV-ZtCuKUaIYP9fOSK$#1@>8G%M4pTUA|u*Lc)PSc#*f z3kxs5Ojzj%gPhd3{^^_E1{{j;Y1&ep7%26CZ#g)NOSQ=4$St+-B%(I$f+3Fo6WUmS zRMTslBuOC#FdP3e*L`s)sy}nVm@#7lLqj>ZksQBn@J-vmC<6XxV@>swpDz>!bEKmT z3hcuVEsT$HRAaF$#|&;`nALJT%sH9MmKT$UISAd8GP#OvHSEJX>*pU?F?Ecq`IKDY z;Me6w(a0fb_9R5kT%>;i6cV)_#|V!=o&nbd`h`&qn>vhgDc*u2lqyqv1oKcrlbiez zF~wP(r_HSB!AZut>4xojFs}++3hxG zPc_dJg{JV{c5@m-5cvpDKa^zf@_7g6|LZd)31YMYG@*nGJyZB_J=%7{py*5OQ7>V~ zLGNGyCn-m<3=u)!^ykqdoI?H%#k?FDim72UX*MF5Zz_j? zu>Z4L>w@M|5I~ZLH(&>6M5aNNQ+=5GDXffy1ngYRop>c>|7Bihq%j*tb1y#uvmArp z>1!j=2U$0e&mQ6R=RH}knh7T?aDs{6D+*Q$E6?g7Y*`6}niE|y|ZIteQEHfxd z*y8+S+O(UZIwq02y6jIs3Wf^G+kf5vvW2zm#oM|ofq|RkCUDmO&p&2`(8R>wUmM2T zjqJ|IS4|29{(go2{eNHjXEpxsGW`vU|D7Sx<7;bcOB5iR{qds?`cue6Yh2C)f!U_R z!0B=55L1!VYd~pzaJ8Zm5`jTdFh8Q1vDw{_HV;aD;WDsbU@hd-(9Dy;U~ty|`xTSw zk;_T-PQeHd=?OzzFb?RWzLAncn2oa%8?LMtIc`8H{`M>QWfv9 zV3r=99zZRQv29z#R=?-F#ZmIS$bmAi6xKcn7nRSS&qDE`Sqtg0_?M(90InyrI?2=v z7mlFo$~g3WH&m0*)SR3+sAS|IWPRN=G&sOFd$#RvFhjnM#IW+H_s>tk3U-^x`wn?? zO_?GOvuH?=?Z15E$YahrMF}h#@{ul}%h_A1!-KtdT;808&SD4q?)07?%h7PLhB+=N zGx~3-Yv{s8UTpG25HGISeE;*^*W?OX0FH^n8KJ7)o-cnT0vPtla6*31J391s|! z>!NSSfE|UDEzt5g@WOch{CK6iudi$o8c|vg7}uc%UNyZk4f|ye{)C1OB0Z+>3EN3iOK{vw6IKn( zr=0+W>g>-gc&ya6kld4A=(Px#gMX7h{BvJtKD?0qzkZpTARrNsQB5NI2;wtQq+Bf|l-o?LM0xm)1nps*pE5xg%=Z{AGi<|e?qP);tqnxcUSkQ5AF z&}fJ5ZMU}d^Z~(w!~-aRG{|VgI&cu6#cM)be5Kt&6qp48b(i!XAoWX z2henVAj|c^F~ve66~v3>a&vO5zrqj?SYyp}b0ZCu$O@=xc2%{ti}0C|NN4c!7J?-s zuGQ1iGY%7R{U8#}VM9O?r2((TRiGJg%o(*H)A4W*S*@)_&*PTunn8pW|5B?GD`YST zkAuO6c=uFCTprGWTx_~$znPacWQa~dS;l%7}UeihRs`PhkS@*-SJmHCn zb;)>tdGCH#6v*|k6y`obl13I zNx~c4E@PvO%>ryNQ9Hz}bX3Z5lbR2jHw1~+fW}jz??4OaRD)!!ymF;2I96U_)MI;T zRu>72e3U{$LOC-)u7n84!_7WM*S)({i-Ig6At7frR-1+;q29TxfZn73hJHB>wwtug zFh%1nN_J;4)kxyRV6O!nYc*IY@?wOBhBj3N1qD$s!2`_LhzLTXDh@3cnaW)fc672L zB8W_4NH!wE5R-}{eL~s^Doc#hI8V2oUn+p}56LI?TG}Sda`M!v;u;zvQMYd^5-kV1 z9qZK#**YpJ%Fh5BN$oIxB0exv%;M&jo}M1`$rDii#j&{fm+_`e$0gnMM&qqZ!g)Tw^jAc)rMDz%#ddP|R4$7y%t`&(T!n??^+3ZZPf1Ohya+*Raq*b)S z6qdjWXMwb|G$LI<)0;9FAqMc!kirK#rdb!Ng0Yd&5qch?5rHL0i#T!O1ZqEX2@M%Q zPznR-dy?E6clT_SnLUL@ad6rhc^dx#>iyNn$dXE{K$P?e`#y&4EQOmXMq`TFqawqiOh4hk-n=Wj@-I6Eg&@sW`9W(1cWGk^xd_8JNb5MG-u+!N%2%m=Q!4cjBx zVkjF?>KGjKcJArzrRsqS$TlI~9eyMl+gd@H1{nzD#+HER2*PZrT8$8>+L2|cqo>!# z#DkVNrRT4Q3Y2St7xM1_e3U_-kR<`y^>RL@)jT{rtWgkW()i8d zjR_Bzfky;1Ih6_moV){2K5TXI-VPEnGCME_7e^Lo)QpVwFJD6Q;f-_|7hGBdb>t+o&PTc>p56xw8(XZ#=y2%I}GXaD&+98MRFjM=hcT-qZu0Q*a#uq>>w%}) z_@?D@eRu?#5fy04CgD680Aj)*O$tR*5UI#jRYf~8%_s5jT)`1*>;N<;dJ!J>l5YSY z3eccr?J+4%6d|Qij3wD(>*8=Dfi~nSD=RAwPzq!$p&lgH0g=fDPl}9Zmua3eLu064 ztdBC{E<}mnS1f!j32NjvZgeBYMqvnEcQ=uFySJ|q-<$hA-aZ4|iukRNgIv~4UZ$?D zPX5|RNE71h_I8?+M+3Qp8v&c7?{8z2Zl$JMh1ouw4_dh>Ogq8+Xz1c1OA|-BFdW*Z z!#5F~g7Qr_U1&u4`He;TXN1Pb&&|Gvrimox+t87zQHS3_NfU(y$(#lV8A|TD&{GEfQ;Zx%A3@eOLTjBF%OKNX3J4+3esGXjRBP_Z;p~5!yEbJ#fiHFM>T1VE2}5%UYW9g6u>kuA%D9n{yp-(eI8f zt*)Npg=*Kfg&ruMIOExlj*c+ER{^5S**iM^*=K1v`c&89-PMBxf!TG=>4E49i9Fg2 zrnZW@y7K7r=R+A9R3s>X2+y-xS_&~NRt-tr0IUkt2O0+Oje(D)`_uJw+gGp{kN^Xk zpm8q7Zu;P1Jf07&v=^zxSeO(p)x7+CHzxB^M#gkgxbl%Gk``Wnu8TIVL2BW`<9Me` zOwGJV+g6R@0V7%RG9(Ab{;n39tlDaTaIsf$YaQ3;a{u!Rm(9Y$!i}r&NBhd^Y6yQb zz3>+!$G?e-VwrVqU|_)51bKlzS!{N;672ScX!AGhq9q4>ffq!HCIogNwM$YrnSBk3 zUN;I8_gN$|)8|G%vNT4X_)f7s=% zoO^K?%dyBh$mngl#E?Mlwf%|NS`RP~ndUN$ULdYWj7>qa+l9dh8=QjB{G1$RWGVXk zYg~qs&2{kL!OqgsQe$8{@CI*Dq;qD|;e89unXcgJLqAZKxCey@H#b-x!n92(PCKHJjXuy}u)iCaP#->Fx@t!HC0xHC!r<7o-HVb- z`jQv$b1CcJe==Wxq%xbYP^bwj61a#VadAmVYG@=% zQv;;_t5Z>*jHgfzTb`S3FSX6Irld!tTR!*{8_GR>pk?CJyjDPdE40ltGB>Y)SD(yb z*+D>r)=MQNPXaM2+E?)b^P(1$O_Q81O8{jvv(cyi{P8tx7wl&A1!)}Z>MR=21}teq zciISrb4*mJ?E+=xxJd3|YZ!Lm1RqyV*M|V@*4Ea>L877oYkaoiJFwRR)hGr&!4BMyGC@7zsTOW2=D| z3hNy*&E-5YYoc(F)jS2%Oizu{yT>4;K_6Eka&ou8CqkSFaFXpIaBrO31kBQfr%jud zfMF*zIA|l~pwFH`7htWxJr)NUx0uM~V_=x*T~HrSFi}4wgB`%(S0iT$(go6$cuk!A zSTi#-eXMJFVc}@twU4;>YCZ-8z;c5OTn~PZ-SY9{$$YekRGEMR$+LtqR*f`oiwvkb zz>10k^@3^jDYzV?8Dm(@Xop!aOE%tzgV6)ye<(-z^>=HpV;(>pT}+7dcg&m9`XEuK zz`+XiB3TIu7E?yHbuf(pHQfTA*Jf$nx#f#NRUzs3{RvLMX%LzwqZ&BzExr<%F2IH3$8IhC|M|l8=V*~r^v@Kb4>ZJy6rYMn#59jFARL7&!7Fs=HZZ3s zQhmhZFqUGtzYUgp4ux|0vKT1^u|$bozb=i@Wl_RX%MkV7zPyPC&lCm(AoJ82rn% zpwWAZAPi_~?xL$PkRcG~k<`%w2*R~JHkmAmFiu~oHKIJ z;$AZY3$j*Frp1*7xFClz+RYNeoI{4xAGU?AlFPk+-(3G+IBoH=gb1qMu<1n!Ydq5g;-kV4}2H+77t4F(cuJh{tvT z%PmI5LE%L2rwqo}$%xSEXI6b%q2 zs8OYcB-0!!U?DoC1)Ba;x6`!1R?rmU@Xv+EliV1Py4fjbnLtPQYJzm%21*2oF7V_@BF8C-f?g-t4$VF_% zTU${&*)=t%K&x1j-(coKsm9Ewtb~YUW}#!(f|p(56v{bvFFF~E z1Ln_7g~$~q8T?S`B*TD=o&ZzpwqY0xp%Y7UuI#4$4ci`_T^@XkE@$ZZ0CEIeSF#M= zhH(=0)HMq?*%W~cB@?caF0eRRl0YCOEhB=_geJc|$4{65H*CdoAo@Av0+XTn1~Yt& z5IykI7ne~r#r<@QQ~#eQU_XHs7n9Js76^KYbhIsU2nW1n?djcb^Hfm%+L_dGaI80+ zQgxo%i+}qiB|v1u-(L}2Wh3q5$k&sO$&b{xk*}v(|8INgkguw$D(6`uw(3Eg5pAd{ zE*{I9n~6>ookG~Z^Mlx4fmB;?8I%q%q}#!R<@NP5Bjzr^C^Rq;u#@Zl{aUQ`8+3Jx z${HH@@f84LundrhyL)=pvepCt!~QnW(9jS?Y+%(bbD2QKULdl-itqNB=F=m##{Rn> zIZ}w|pZ~2E6h;>00&M4>I}R8;fV9{y(if>Sgd#_E8uS1sIdlH8VL$-5lWvbAPMk)< z!BjeBp<6re(K;Cml&Q4m)iQF=;!Z3-l%#(?3Uk*4QRZd{3UL~~>J55eS`oM`pm&fD zuG9xXMJ0DHP?`C?eUeD&xIYZC(cfRW%dktfG?hbhbN~r0hjLt7#*kW$1~m!xRboJp zJnT%-USK+erpPh;Yl9*NJKRVs$ayo#_@jZ*)u39>$k~j+t%Q^mJ8(JCW=uF*b2I@I z5||t|(l{ylIl9b&njuud5oL(86wx)S*HwvQd>Q^>m*c1fcChn_-8=YoqmGM<3pWhp zsN}GV*1$DRQ@{VLgvrXWypplV6Vw9Q>;;{L&MeF+3zC8}*()qC=2ZjXGQ;vF6v_Y? z?MD4w1UL%L;o*bPFaz*5y6@l5wghIwRbwX%fbJ#rpqrvuMf*Dw*W&%NZ}hM(*PxJD z-$PxM2u7cY1h*J7Q;_}Wh`QXpFhB6*#imP_7s!P(r*IvM6R29Ua?Y5slG*=*U*JA} zWO3i6$)e!`RnmbwZ6_t=asDs_(SQ5SCu8Un4LB)IkBBGhZ5$MY96rDjT+0TacQFr# zG-n2|*Wf(5wiO=_oEk*Ez@|yiorokrm(kqXdaUA9cw{90xv!;YJSw-Vo7=?;7o?i? z=~B(e=uN9VXYqNAt&KJV&!VF6}GH7SNZKm1Te? zn6d&V=$}R3@REBSD+TIu;U;ik$$09~b0NG{(QJ7SnBj*=7_pqpf=}=IH?tHze}4L3 zGfU6+@5nF2MwOP1M;`+2FLws2X#Ye=B$Lfd+|Ql0A!)#@x%j$5EaL~t)=a*`5*o+xIabsFyAzh zl%&>A&sBOcFdJ~Z+3NCy;q;wp>z0{G%|X6?t}SwS9qK1xF<62+k|CHaT)2>?%jJ=Y z(#pV2d!U0K038g1?D8^vV>>wXMF4J3!NNR24=G#z^5q%K-O9mN3qwdzvMLM(kVKf` z*cWA}NunlhNP7n8M3(4duscZxiY_!uJ5&4x9iDYVk+tXi_^pm~xn@`n_~G`wv+{ln ze*T1Q+lYN?AdF38uNa12fwV#s)OaPD^9>s|;8MpExE&LiwuCM#!I)2uC8QMcjsS&q zgC6g}AZQ!f%fJm5w2BymT!92bDh)rswgtw*jy>O9N*5(_B5#oG{Fb||Y>lm`Z*$w& z;Mr^5J>YY6Si5l=mbwE%$z?!skans=H6+LRi-ichuv-%i8vY2SMbakxm`14wsDn~4)O$#U#p1+xS%@=#AUh-3^sUH zpjy&j1R)3}Q!@dj5=%&8ESzPe2^AI|eF~lE@r5;Gb$4TC1c;v%p)K5UF+kWBV2c{i zyl+qS2tVHfUVVb^TgUoZiIjK`N+aFOKzC}uQv|L&B?;ojjCc?}b3xyX?Cy*gX(!4> z0-kLe!Yljx?LIUKB6)QXu9n_%qO;{W|{zbqmOfo)2FOU{vBS15SnBR1Ti2;0b5nO1PVbYl(+H-}PJyh!=2haL=hl3N;M~L9W~g(VcXT6wExM4s(iU#MW0m?5@t+;Sq%*Su|ia?L1#89zBQgZSl{UcN7_K$URoHPaLYLufR3co*$HeDDx>w2uo zLl?vXxDH^rZvfP|vR9uBnkAac22wePraua_fh#WLqRV9w+NHRo>mf)iy5B1WRfE(l z*cw*ggkM3Pp~+QfTn`3&^hn(&k`e>`^v!+3hdwNTz#L$Jkx~+VQB`(8WTb0Nym4s{ z%>vx=#_S`mp&uQOrXEx2YauH2!y9IS%h3PkDO|=tjIh)GZy$^S9WrS+S(n7Pj#4>p zkfG~Q_i>#GAiMx)9n^p!cVnqVp_jhB=Np&kJ!bUiz>4i~ z4@{p|v&8AE)y{f<>F>0_kcSk&+v}roTm%S!iV#ZIVZfV6z7G`d6&O};!Hr1o$&iI( zfwhC<=cuHrY66%7=eLJ{Y(q8(+=Yo68owp^8&OEpp95JKQT<53%8q2JS|n|lwMeJviSo7v<8e@`IM!3t99t^Yb4;} z_aJ!*$&hG~jQj(9PS?ul^UE@?zoJJ><1{jjAfVGGwR(;2g9nBv16R)df_iAyPxKyW)Fo6zK#qycCP4{+cV+jJ#!gB01VP*#+p zYy{$fQzcJ9y8`ARdV1|TDdCaEJ%X9z5i#8IA#iZp??FZAot^LML_9nmuKM~g$IUu( zz9>_gM(|<@^;F?kj}{MO1?mtovv>NbR^A+22H6i>#K9}t}|?|>KC@bF7A;F9Q2 zfX#egC4Sk5@q3GISQwdChXUhx;lQlHINUgXQco{0JKYT%nn~l2#AvJmG7s8( zwk14&Hgv%+-KUOOn16e)VEzG-(9nu^mFLyEK3sO~n(u3rzw%N1#Nq%k(Vl~?5cW=C zcXwxz!PWxI;R^KsjYRR8LI{er&9an)A6%dw$t?^HV|ELRRgy*5vdVM&!S4fdJ$D9? zSD-+buYe2=m5P~#eN=(_bOjDv31kW0h{!*jO4Y@yfx|We74O!#$mV^U@hFjJi?lDY zyPU7Ohvx!34><&!Wv%iy{878b<>h(fNwlRe=;NxwWN_orY}k*Z_hAMV*i#2#53tY* z-kDuzgquHLG;D+~iSE{dTY-j#t1~Tc0^!Xf)Px#bn?*KR0L4athFQE3PydY4L>HF^ z5AtYhCtV5-IqufhT){hZ;lLjAJClBX*90cJ28uQgb_u$zWf?vxp}%^7E@yBjr1Yct zBV~w;Mo;01i@<{#pT(ooWgv8GTQaB?x;Fwr$+^FH3!I4(nBZLmht(gsOV$yMGMVAx zX@boO>bs0HER-}fAS#;tho<@)YqOreye((Xg#h8>@70i*EwF0#`$tEwZ3g^X)r(t4 zkQboIkgE#WK(Yp%EPNS+M?nSJega7dqpu@WpFGUVlj`*pr=FrQnfd{mDTN!6E`0zK z8W0KQnEWm{2?T-wz2co_;ci!u-zRaA4E#RC3c}-K8Ur1LuD8+0T~~5ubXz-AAP7T# zHE{XB+oHKHi8Bp>^vibb79B2r!kZe1TPTHng-lHP$NKvAcWHijF_o8hbg`TVDlQSp z(1}Tt;U z17}=?osooydi$(~E4lCXLyYaAWJl ze6QkyA*}?XNr%24yQq25*2|Kmpz}kkYv*(bSH4UB@jMP;r(*y`!s* z=@ROM{Vy!&yUEH1G=ci7MTrXliuM1n_T^zUu5J6GInrn>8de(6Aj2v{qqIWNV2lPt zXi^BNG^h-zC4`h%B(p*h%}6L^j8G{RrG#kyo!7Hs@Av(V@A&@t?c=kL_uJNb*7MxY zJzUp$o#%NOBd#Xu%_&fzDBHrDn&c0rDEs5!=vkfX$TAdO>Ul_+3o(tNi1LcN?v5ko z6=_OS5TM?Hmo6!LIh}d}Ts*wJXsoDEnCZDp|4#RYXCiHFkXynOs`NrUrgQD!XoU>oo4gX0`rd_wh+^vHvID4VJicV zvNbimA5HXr>#^YWWNuuM#l8`Fco-YxG_Q{!@*_WVS5n85*Bmh$Q0u$*9{*uBhJCT4 zj}RVf6)O3Nm3$G{Z@NEnt@q2NYe9o8JkW@^3v`zHTlI+~s8N!M@_wxM;3*tRMonIq zi`qv#th~U2Z+e85Fb@HSGG;y4;Rw7Wk|0P!XTXOhBgCDgm8rPtG5Udm@8BGJ{3;kt zl7z#;9;yy@);idZlc8az*@0NV7dNpGeX#=PX~?-ErtPK{SY(U0aE{_^3Ly63ggMaf z{=nmoH>TGup1gAhyZL$m*he<-P-53asxAoGW8w06M8k)%Q$19yzkky8RG93!k0mdn zp6IV)bOs?L9o$LO%Y>OhlVMrxbP9BE^1N%B3v3Ay&&L!61TT&|f4-nUa*Gfiuopn7 z%mnpnV22QsTf`QVH-Lb8X67ZYMn`6s z$k?#AUJ;j;NBMqE)!j1T`IQ4SV-*A~=~4D%*cgTH8;taTUH7Qqs>{=Kb(Xp+&4uCp zS8%Vl&g}9WF?H~kudI?=_4y5D3)08YZ~Mq9(LHDdekzT?6|P6J5vdLuntN!m3VnZZ z|MypYSu|p@=$P%ID$+H6Uk4wUF)*h~k@@~G{`begt89|KvU&vj#f5^R*r(T2>2Enw z%->pVJU-Q=uUFS?u=(l2Tb=7OTV|bGB8}^(@lD1a>1w1ALFi)!@I%>Ps0_>7}N|ORbJrS3lNI-s16N z`R`XbC%1Yn$%tk@7$foVe%?c7L=N%C)Ls<-#m?r;0EftJZJcu}Mk`u5?G$>oQUW$x z2};3%Jyq>*ljn_OenXr88_F`t!3#tcwaPU1DN6KoV&TZv7s|R+vf1Cv=^1(sFmF>U z%sYNy$N)w0M@P)u#@*={=TjWF3$HG)M~QjXA3^;at%|g`X!z4Sxv1QNp#>*RfZ7!r z)XCx4jh;CSm*af<_lE3uHK2Xu>eab-wiRkFmfS5Fj!~#xO+VYboWJT#S*8NXQif04 zzN6si+^_((TXyr?f_S|J7lz?#+`wi1pJUGxFbS6A{rZ6S{kvAS#{Qz{uMa~PuL!4y z%@?Eh!846C*HDr(70JKp;r&3-yRSzoN-D)p-M?1)+)72dx$xo4iw&2Ov@@1E>9nDJ zBTm`F+G^G}*0&a^dY8?ndD+?VACIbYW1D{E_VN2h36*L1T)ra%upd6W_*}UT|2_Y? zTFs%wC)e66xv_^mWdmla_t#s@)i(EEO*?Y{_zf{fGD6o)Hb10wrmXa8;LIP*5;n6q z3AL*mDr9#L32m#3#dG}lB1GS`RmRV5{E&zy2ist7uv;~nJy5>9_TkeD!{OzW(D~D) zX$kwMJyt<#ezh)k_QL0vR~)C7U!0sp%&0Y1{r)sM^Q)o%x7js<0+oKUmD|+gG(%Y& zqgOCyD?|o2_c2(aCGf$raq+-;grc;R9q7+=>U%XJ|2~5Qw~%1DgVu&(oX#7}vv5Qf zO&0(70IoWD_xBgNmkZwCAL{lbhShjvI~?7p;;Bc)NYL3+u7Ye_4C>U}#}FvYNt(4Y zhF3F=nV5c67ahgkWx*LiHpR~!tF-f1M4TL+o~1a0zj~d#UTNdijbKaN+^B5*eFv2kB?m5j&=khD*tHm(!(9*%6N%wcxhvcnZ zmev+r-k>u$=*fcC=@cRy$IUMsENy-Gn?dIqOv15!!OCXN4b>9YvNZ$SA&972LNWA=)m@ypq0_z0tqg`Xj((!LP0HbH*TmXD*#J!L?bds zpL_C!=BF1X26npB^{7w}R|+Ek;y68v6tk%j{P9{a1&Ibh#+Zng4+Ck_MaV%-EGTHl zAw;1*a<(;IpgYv!_r{$Cqa@5!*%e{QLn!0UqIMjoz2X=){}3`#MEao%KDsMtRfT}I zD5d~Q$jysX60p^91N(^>bY!1HXHuTfaPJa>`qowS=71a#@dwr$YXBiDY<|8FsQSU! z*t}~G#_VM&it}GoQp@QBCpRo!k{QE}7%RDS>C+qIS!&9h&SnpgJK)}dDJj?f1H7kJ zSWj(2c1FNIDGd@yQbXl1XghGfkG# z>vvo%q%apmx>$tK(z4>=uRVa^ku;Q!Mk1lkgqY0`#ebOPd8=+1(w^Nc#f|(y%72@!`1OwL*Q}xWE;ZeaHk5|e z##(as?8e(DM?V@uH0Qyjk^S?zomfaPWf!P`rbFU~fhYiuMlumEf(xJtQ5v*BD?2yms+k7p1JAgO&E=|PVoY3OQ$6(~HG3eASjGzT53!UN=J z2J{254y$?-dMOfO`wb1s>Q5xVRd@#4^MGd@oDaT@3Q|IV#EC(@+e&8sa@_I-nJ59d zKs{;Q;{Lor%#k4$F`3|F#*!g9ty>$!>B&d~F0Qcz0?Q$Um4J3PfPZNf^vXE`GN~~z|g2=n5MQtM{+uapAns1P`8tI&A(}D#WvI`3r zV3^UQ3IXFnd=(zCGN`YoaJ8G;CV{$!JLqSS7xIE zSau358=A$Co(<+0zV1I!?;k*9X~vJ9&RS^O3I(8?~oJaA;A4|Np%GK3s0qu?a(a_4(RxYLqX*s``LI!Y%O)avc4!hRc7#|!MkID2hz|-_+(L%|*?X5> z1NX|68gl?6`mINlC0!bFe1NPc$ygr5!Z}>YL;ZJV88*;VdCwITOmUB1IcnD14G}$h zyj9`cdvRx*g8lbTK1I|#E}j(7KvHZ+kYm&W@|48AeSCo92&Lvcx;_Z|Mr148VSt4m zX#iBbLye9WI2HPXpGt0oWa7sCvAjQq6OGWss|?KU8Fo2mOkz+XJ6Q@*lG$$K^;%qB zq11khC^|QfIBpUPFGC4N@(c(P?dqfU%K$F;Z2(E{Zv%uwdLSuuJAzyL336*U{C**z zL8>bvtusb8iWKS@abw9-mg}d%BqZg5=BKZ27~VHtQ;d61b0umq**no{qb9+7Af6r- z29DJPpaX?e%mO-UCx_`7jX$>Na?`G+AbIfk@wxLb!YEundlp4i5bPmH>BLn>?JtK_ zAF=O<5(fGu)e#UnueAYz*5uE)zd67k_X9dPVPj@G-yd8PrzJEpFo)6-zCl4cMXRSXPmrKql;eXCZceGQId%kpFyX8-MyMy#wMC&aZ}m$fDD+(MLh zH@=^jaCl<%w+GdkA7hXN;pDQ-4%kK&FK`WrS=~jxwN%*zvPNhD`#I6nn1x1M?s$Ox z#D14NOT0un4-olyqm2Qz7ik|BWj+GEdFVhuU0B@>i?rz_2tVz01ZGiHlk0cx&kIwL zlM;`8jgCN*CRLr-8fkZPLgnV+@IVYRwdBey$EZ-BEpkQ0UPgH@vfgVcpyUQ)yEzn3 zQ}>$dY{?ryJgA!v8G>O?H=oCnsiuXGW^$@$QD&z)S!gO?1#-S64j?sO#?U`STbC_+ zElyWLM*_v`9=vSr%&LK|gdKQNM^MBn6LwIiJCf7k_SM~(vCSK%q!x_Oni_Pe9dR^8 zvX8*OJXxOdl9>2>m!jg3yik{}Ej0;&h7^W=QQ>IK^1SoSg2G3Nn!x0uj*dWNPN7{0 zS!>WHYsoUj^~sXd;5mQ<guTZZ((j8DUPG1eGR#7J- zoO(V_ro3uJ{O()MQ74#$_GZxG=RF=WtumLRe!?91=Vo$j!&yl!tf+4bHOxa@|LVFj zmENCMg08_v#M-ctMZ7F>K}hQBUQ4Ps)O}-lp~}Wq1F$&VARHxyB4qm~9%*Oo!(n!S zNnp6VP#q+$wY3^C=B6De=)aMl)g8l@*UB~O6?k{s#8!sOt2qNc85I{n$|YX#tnOAb z`qNIlV9=NeLNRzwgiN@A+^Xfy{f(`F{=BwBOyH06)e^GQ1=QGr`db3QqoO}5v-)s! z*)o^INd{s?FT3e5hJecP_!>Dck`x7I-%VK0Gl_2ef2b=Ke#*TvQxEkpZ1lSa zXbLFoXu3w2nm79~#i}H(1>k~#;sFDsumJm#GIG~YRfV&`N=iN(KS&1kO&>f~vZJH0 z@u`5t{*coLEV%XobdHihpDbk2Osf@twws>JZi1R}rM+RE(M0LMit!k)#%Z#s;t zf%Ote&t*h)Q3QU(1#Dl01{su9(Cp|}@*oG}VO|piicBFrV^y@lZt{gdB>NrG+Hd_; zPeimdm_%)6%tUZB(WVMiQq~TY8%Wb;&qKj8hBt^N3aCxB$2MGsNBm0aT1wp$aL;KyiGWn=#T9P4EtG}mrWDp#LGZSZ%@JaHre2F1c!yo zi-*&1({BF29$vdhcG6Oyc_aoqtI=3IMuxG41;G@CpGZl#2;9inoK`Ru-N`?i{t*3t z&5;%NXAfUdh%_=9X>j1(No!8-*eV*Kim?yXa^MEr@3}ZOFnHG>mqO9kH}L7=S?F+TrCM0*>6>E<85CV##CjHdW zJ@U%aN8j`+UEBO-2HwRWo-OO{H(|dP$?=P%>Wfreeyn5n1yF7-?25V>Xk8kg`YH*6rj$r^gXlD)AILEgN{P1OhbsJX5uyuQ(Rt) zj+oTn5-~E4lSi#VoQ7;A93~`O6tLfeMPF1>;s!cDg^LKMV-gbf;U@M(Y1#pGt3K`( zX_uirqkTko^-#h~vqhQ9;v$ z!#{GZ>9~NE_lB3oE$=~g@-bCzcsIJYcdN#7fLeQwp+KDkHTW?EG^!lKR{V~1lsVJ3 z7P428z7mbq2I}c#AiYF1M+rDR1)*{aIjI2bCiw+^jM}`R2Pis>5xy!j&r{pccL=KMK30c91O`=k1%=w4A%x6+laQsD^ViB-wRLsUh>_BfH@XBKWsu(_G~~$8{Oe9^V2aWhGbL@JU&%AWt*>mw-38okS2j9)+Ea6EHZgN zwO1RIoMG6+zRe}f0t=INGr@E}O}{U~>1$=xr^pKr-JQKtnSl{`@z@e8L88JBqzdja zhi<$&db;!5sWsO)KaQAA|)x%u)h@~wduxy8kh&FPX=k~bkHH1&V-qB>hWyRAr-q#E>l zn`iaNK}6qoWS~88;H`iY6xho%@;x&03Wl1oOcUnnghz}80LX`QRgJYmqS-Z~6a#vK zd_WF8A9}uSjni`2(?hLiaQIOrl0k0EugEe1TwUHz~;g882+GgT=?(JXXl3|&aA$0B@!xZgfVLqqnMwx#mpx2b9^-C z1pPv?Mo!V~%>c`FeN=q72a^};j4+gwoIaR&8B+%GRNN4+qI@H3hiDDbAPSbe#uKs~ znbdN0ZPo74N;(JFLq_Lk`aHco1()UeDG6H6evA{N2vL{KW6V+VaV6Ru1&}#~s!K zdVTv4+v5$W9}9A3F^{avz{bd8C@7Y0NiS7W9rWpmDtz^IH;!=JF_B+f;fU~|_vSgL zEuP+;URys0L;#Wfqa$|N_a>c~-VejyI`|(jJO$jdiztABxP=`S4Fr||9dsJP=^pCYfVQXZvNa%ge> zprOWRtmxn=_nJrx^PVYtpI<9>UF^Ndm5w#f&kI#lc(+&3M{E~L)0X3^EK?m+SDSlp zctPz$%bb$VpHEYU&iE;ZIbV%B(raCf#ga6Ew5;&*$HAY0OdLscO&}FmVoU_9Mvmev zj+zOkQL z!jfw*LlZ?%ZrOH2^V13-b4cyq^puxkHo4g3V4`H1hRFPX{=(MZDQL*>l@fL-GU}qQ zpPv?)?s+TxJiLJEZf83L#|@y>k41`iYov`xc7bSPeUybuw|{2ixYwDn?D@yje4lGf z#i_Bz>WBrt#NCrNAT<&dOmQes3+)k$#iROw>jf@DNysGF-XE2y17lk6k1o_)F-mwy zNN-Ptd{pk68~ouMt|YIAhGz8Ab(o)X@7vNGBCYCNWef^dO+vsVr0hdRxAmE;Fkes+ zjgeWxR-p9D{?f8Tv^A!(_M%)Q8WZ@2s}>UrzZ{wRs|8hGv#LA4C>xBV2eX=tXlEOe zBhqDdv;_4%Lbxp!Q6OkyOa^aIFC)UCCSM$`)iG=&rpmmXk4ht|3(#TnKWhlMSd^Br zl@t+F@g3qJ+zl^qIjrMUf|%kcOfzwWuZA3zWX-tR9_)%C12o@@j5W3p4(QhN(OMLC zaYAP2dP^(XHMufNG304|XrE^j+lj6x8h)4YMs! zB8j1gD}xh|M;uq0j4#fEC^0=M%G_$<8S(Ny|1OWB$6+X_m$U?ms9IpgGw0wv4(=Ni zNz2DbT1=ttLSL^Y@>c;NbuBOoGEpL=9XTkWj#vp_dpj5ZE*P7wMsAyypw4H!7e8%b zGTAEOkxm54%f*K<<&X@HGuEm>1%z5z=;0-+g0-ip#iG*EgEgDy z9qDoNl&o!Ku+A%Y=)^HG7fXIEcC905ya%`v#MF=Ald@2F=)rUW<3h>~#@P53tFVW3 zx-CiH&Hnj0Z`A>$!_4$V#aS&F2ZkQNP)aEYoo%m2wTGmgv^WsZV@mQ|(ebeQ_Lr*g z;gPa}Xb6kf7jul+U=PXRP$iZzp?So+0I|{qgnJSNWLUB&2X2HxkRJGmrvcXESLK)Y zb*oqUs{)ryxWAuHulEYf=HrgdXEov_8nc}wq$D*dcqn9d=ZIEa=w5x~bhP0fFcZk5 zlUmI$y6nh#jM$2Lk>Y)59bR-f3`p!q{Ybm>a)T8_2MHTniPOxoml482g85H3(?w(r z;z^=o)|>j&>G=|m5=#PECtyDKS$*9`e;XHr8c0oT2WCjte&jH z#aj;q8E1IL#4(XkW1pbqz2_s>CgdS{AjKQA-`H1PauiG2xo8ix_SNT3pA6U*ckZTF zMAp}k>H=kjaV0{-bI&bgDm%EH8id;fK2m??q&BqDZpA#3i83cCH`pttn75u!mHa=a zu5W6uJ?Pz0ry+f}OB;bR;JB3a4*tJg8p?&}vC9u2M=Hdy-D-ccfeuzq`mD4lCB06kER-|vmn%35W1!tH+=Oq!|1gJ9%djw!ojf+?cli<@us4`uE z$(I+XZ0p(F@LqAzWmZYqoPu);L3N{7M%8thTu?`&%vESALg(5Fm&Zipd`yl^{h;A` zDg}4I0?@?rz&CV=yc$#Q ziic+!mC~drHTfQ4)V+%;9GGU_AY%DGUpS}irpIz5Ii=L@4rQwX{6RHa&4eKlXnkJk zF96IKy>7ARdK355Y^QrWxaPYcq5V`PnwJ#CCS&cm6Ci==C2lO>)^ya~kR& zUl3+hxuD6%rKCvRZU0 z1WRtQlNw@Jw3} z=-{Ffz#=NTk|`qS;AZo%F~|Nnex7=HG}?lG;Z{1%x*|Nz8vWcb|pa* zOd~auyW5dtTf3^n+Qv=X8z?b0Xz<99s1z4+=7#QQ2_G{Pi4n6@^JJCOB2n%_7nr#~ zHOP@*5$c@eHbDA!Vp9TlW}_O|2;Vro2D)L_e8D9*J})PS=_tu-iqk=b$*dn!RH|OG ztscmM_m48>O-g1D(9A=!P0jt<<8a#a`@Uh=B9K;_SO`y8IWIxWp(;E7AQB;%3p?C*_*YPudb+{LBP&CYf_}LYg(->MKfyMdfb(vs`xDB4mt0g}x|w1Tcz1i=hd#*& z#ohP~R_Zzgm$}$Hq73Td?0{KO+uohPd6H3brjp@X&#e7KUqBTOg!jF)Z?ye*hS0yZ zPC8|wD%L--Yj9t-Wir@L5D>$sl1m+)fY9all8OSc@m0v4Py)1M77?d&>7pyU@nm&J zq7HHgA}AUpV#*xr*tQZ8SxZC|fNkPR3_cfP53M>d^DvzNsMrZ$PdD9b0m#qumJm?} z)7^+N=uTK54nzurP@AUx3vnbV$WZiZvSBNXgO)lTgN}34UcvA#0D zS%@-KSQ#urHe8NlK}G?~-)huxO1VOcG^T?(A*t!ol?Xzv>{ zgh}_2Ma)2CIEhSJ+M6HsQY9LlTDeIik3`f14^xBze>SZ6j_=Bzu29Dg`9`@owlwH2Dq=p;1jHlVqp6A{ zicT6t6yr#uOEfLw`cr&MrB(v%Ntb{Vtqs&HlzoTN83&9V2U#X1kaDMLJ3f!kY+vTz zs0NlDA3ZuciGQ;>IC#lWhIXcSXz^mOCmsW+)V>8^+tt+%Wr&M`gO@DOK^Ox;t%kB^ zrIWRh7C3JK31#;svegrb_Zg*5P(L5$hETFp=GomHTj2*%Y(uhE%F0kvp_m=T+@~<2 z#79d^mDjxldA5@CxH{s(T?^RY(?hX8)9h^di;V}P)X zRL~4?&m)RTBX)ecIUE_t7WF`T0~C4>QKA`AOe5;hOIRt! zpHzDU__yQ?#UcJsy)bo&^w*rRmv4*H5OcR6+BSwq44ErZ&u&O1sUtJ-VeWchLl=`B zM+6sF?b{71cr2U(Rtw(V%v0P7$k&)XFCkj7x@=DJvMJ0odt*mCqF;kCMq(Q#>`@$6 zWGaqnr{Mcoj|hU~8;PR+`@k*_@Ve*D1G=ycp#?H2vGxDrsOq{3`hBGm2moan1Q|QN z-ip9D!K>*BN@v?X?w|Hsk5F=S^i!V*?g%%|d8*g$<(|9BPX9M%Mal`D;KH->MuMWH zefid>QLtAbG6;ojASWS>U5J;8jWhtGhWcmI zH;&F23BAUY4Uu*^vmjpZctgnaX4EC6(tWyZzW6pB*+nCk$_R>xJ&u`arIB(V;9K{{aRpq%j>X+(& z@0!`y@}Xtz_Nf*}AM3xrvRH_;#AHb9N)rtn)d&{nyx_}=yH?)CxgUib`>ltrzb=o* zznxz37_Cgu0Lnl|AnXQ-A&!x6NW9eMI={QU+7;{VY4Dy@o?v2tjAzymkgkZvZI_bY zzj(_J$YXAjQ_u^C#os>k^?aZ$Od+WI8L7zkDQ5*@qqlGDP~_hx z0zQEqbBmt*GMZ zp83ZnCu8M8#-<9w25Z!rIPdr*8xz|1Wye8k>~r~+@cn(ej!oeYY(3U#48>i<0Ruto zyBTpqX8x))!?A~24Gjs>S3F=T8cYX!zH_d#s+qiQfUJ6ce=J>IIuXDjHs%kvPz|d< zef-5b776)#Eg+0=8F$r>80eIDVr!T&*n2Fx!yew(t28 z#6n*_C*kaTmtwhwrwA!AaFS;+Y((&ZqJ}sKM0P-Y^E(^iq;^jCCha?|rgXSaJA1T) zFGkK~1MU)Ll-rHG}e&~a_glhBb;S3vC$0FUMT+4Kv%Pz=TZq9%MI zq9S1bCbHYb0JDf{gStF*u};9oh~xh#z){D|)H*0S9;wDAV<0@TN))RaTDL7tuTAEGmY}hhYP}#slVK=n;4!aa0cd@FSG|8)Bm$B_!nyCZnr*@% zgvWMZ3D^oP27v-~DF|lC=P1qr_oRoY8H>8d-f{2&Bgvkw0vwU**8i1Ed zlRK6=mCeVtQ3C<;-Qbifd%V(y;>N#0Swl;uBz!m!%mr7*Fhj1!#XO@X9ptspG9DJJ z*sHqLDY)=LK47#6d5c$e$2c6@YAB*m>f>8t5CNiK;6)DsY{TT|~V>W<}ej(S0!GiI3G) z4oV`~0AR`htO^wvm-9Ujq382+aKZKacyT7*$~-Y+J~`9yziUp?M(Slp=*e zh56%`D>)3E9ag3%X`Wv}(yYv}y>(n~(Cw`e^5Y}zu8f({%XajW=3n#n0M42=AS9$v zbfvHmZwJI|0u3XukmEr)dYZM>i)LE;E1*Y_w;<(fgmI-2aB8Sy+TU*dV1i=ibGrq$ zrkcr8c%{JE-LT)&Bo`Lowx{xVVW9R(`L;b&Cv!rulCw@g8cAJ7hy+609OyA+;1sk! zKwJ_4zuSU%Y~|ikzV1K#^INU zvP%T#cEQ8uf3s7`06{$?$|jwjPl z69WoXC>D`yGnSNFix3atQM8T^;B*mZKaC;&2617c4S_5%oG9AsBB5Z@OPCI^IB%Tj zoNLLE*xD^gY4W4PP#Py}VjxwP!y>v*2xSXVCs~MB7gz-LauF^=W+G~8YFNF+uArea z!7oBi@**K39QG9odO+5xfq>*J@Dxk#k0kTcaWaXM1zwwLK79Bz#|Vz){nc9AHN%wL zlZZM>=vl>aBnvX>pxTg6JFcOz7t=x-hH9256jZ=~B@67V1MxU*^wBBXGqYM4Z;31O z7Kh|=j(9OUAJHwi(ej0;EnIA=oGDbPB z;{W*4wZ$%{;>vGMto>yA7};Z|{b^LU8G)j*YnM59;>>$c|_j$O&wbU7oVBrLad7y=s(LA@$i}N zOK)f;(SH{FKfZcbEKbFHB`i7Bg~ zzBf1pFvWOdRgKGMHABi4Puyf+$w%Lz4~q3tJpI0S>~7y7&LIA4`*Zl=5LSX5VgxE{ z!yUZ^ppck@E69v1z6>(jkE&2}suR#v&E^Nk<H?{<<^~;4r}2h;uzRbhfZj%B)lLvDvi+PHSoYUnTNgCK#tU37-3k- zTca=$Q%ngH9*+ei4+E*OtxEx3jQ+u$;@jU_zIafQ;;9klr_>DS8IKNe zsVR^U+iheZgp~0Nk~XjhsbJWHd?92Tn19$ThZ9x@o;izHqx3vPtEBvV{N>CL97jGU zsabeD-ov$ND%Xm#q7nBo~LZa-doz*mnyiVJRL5@mvIj}fn{P)O~ z9e4R#lc>jdz9-miw}IHT10*Mdqncd+A<%2(xiCdq~UtmLQ^|~3#%a72*vCe|0pRlvNBH#lY!8;Jb zM~#Hb1)w|T(188hajH>40Y_m4MbGV%fob5G=1`oP*>DQ!r-SR2I|FC#$baP|3oJQ) zvRKJ1MCur=9|ednFT)Q4(YWk0D8-6DUoDVSrFx`-DD`XspERPglaEW=*LJQQGWqLt%y6aEC}gd<$8 z3@WhX*}>&mtuGL6cqY=@HoOMRPY^6Oj%W)Edhmbaf4Kw=m(Q_j5#W$6&S5rI)*)=H11Qp_8Wp<70IVL@Mb-^aG|YP}rk;?y z3kF6WG&$h&(*X&>Lp(T20~D8$wi7^WtJ#>KZXZ~jS+_fL%^B zqJXT&;O-fXkEw9Zhh_JU+WsS$3U-IIh=zxO_~Z85y#fa-H#4OWv%%w;~ua^eDVgS+`{Pu zK|zolek`IDIHid*De$Jf2a>2&;E*8jik{JCNtF-CDKk+p2M5E_cDBrZTPvxPW}KB{ z5ZZzinDE1Igx)|P*2M9{u$SAQ4jqpjlAJNNEQe?VQyr(Q3P+wGh-zI!IXU^fNNG%8 zxnVN(mA)?JEcRy&{&#m#nTi@>pmI$Ocz(UOyka!RO7Px2<#sv2b7@^wgOC>qNkrP6 z7Dj2{l@k#O!4xDeu`bNCDJ8y~-MOBSxM)LMRkO@oB~C{2bn*HJl0^@9%^9<6T^w<~ zKRz%!VaVTs+N6O2QeCtvbZ8r-hGj4WCsz$ZC2j5OVtM8$sNs=|8=5na;2fTy42Fqr z;0(ttBl)u|0F^#)Cp}NX1%|xiumZ^`2a}v8H>k{x5cQD(3Px|({@t(b*!#2&PF!A9 zGQB!V{Uk4p9rSw;9;Uxk$I%MHmPmb`5c9JTgu(3OTle=;h&oz`olNC4^vXfJN|$AT zcC^RhoL_fpp_SM``=xi z@)+I$FKKab+(~LxgK1BMA(B<&GWGbQ4%#(!!Jt2gyW8-eR5|h{K0c8%g#@ft2lb4v z+$4dlGjU+uJ#8pU5Q9d!@YVDc!{`X-N9jOF_ zILm4Q_yrKtUw1DQ8<_c#UN>I}4>BmGRVE^uZv^ikYQ&WWBLvLzMD}Wl%4u9&?(zZK5HHR@c_f%VkrjTWxPR9O~O$!kn2Oao+Q3mLMFM0A53U%Fb7DZ`toZ7 zZ*LBr_JL&$7FWw%>_z>=5mu@zpdxeB)=3mhM0c3UtOkzVG)z34ja|P1w0e@_p)!mt zFKNQ4aG#H$;8D3fr&E63(Sc8nGrTXZ)?uW)r2WKEvK#ZP9Wl^&5?=U1drr1iSPRa> zCWe5`efL)+*xbO3udM86;&9IUW0_oq@ejh>C<2=4cR}Yrh4Yh!L0&%3Y-rxjUwNoC z7<6p}ftlU8j#*%~_Xu#s&Vu)MpL_!tBs%-{s&Lqd zf%6C93k9CarbilXvI@HV{dOxTRw~dfaPt1`Wm~7Q-Ddr zn;R%KH9TkPy4<)WIEz0a-=iLJtWP-EPeUd{P#Jgz-&X-wkp)|5<2!NSL>~{B{Wpxr ziBbpb1;J1qzK($&RFl&;8YqCng~(@DmEkH{pTyr!tQ-MGk#rwIAd(x&P;p#C2}Hu!b#Yj%_U{w7i+BDEeR4d?x9J+9FLE-X+dLmYvjD4~NUfky zXk4J^A4(Jo>qkw4<&EOJGL5*n)*v-C6OFLqN5zLga0Zg?bAC}cWDsAolC9+VZ?8^H z$-$WX2Z<(`0UJRXK$4d zQZeD>AAS|e+FUh4g?}z$7>L{^$|T2mk5F8f6eL+u(fT#3Fm=%on0&myt4#Pjp2#5( zZz~R)07BN+9{5daU4hVoyqB_Zj1g1}4<=ENatvQHyQJSqa}}W4gB6jS$<*)DcWzs~ zu5twP4*eQ1BTte!!c5X)6EsFuccAWsun@_AFxKqV<<@Tx3LicWi&e#` zz^BatHCDq6BlQQ0pv<8cr2!aO$=K*#2{A+=WrvwceMU~WnC!`wGnr8=PbMV@bXun1 zqcSIC=e18IB*^$)8s%FkO%?}rhxog!+45j}&WUZGj*{|>Y}-N60}#E3i5hyfG~nUj z-0jat%J98@_ZJoi8P-yuhUnPh>@`eh5T$u6N z3m>0W0M&(^jR{8#_$w}nDg{$U#}hNM6ygSf+nKO#venWl5R_8$awhE;Yq&SO`qOcj z=4CaL-6_UuWHMO9;YpN4!7&3eV01zpf(YXQ1(itPZlUtin`-neg+&HD)-*Ym5F8bZRyyp6PpcJQ4$XyViKlc^+1?B4VJ~LA?qdAbfkNXhNn^K?F|WRIwE6&PpQ;6`YQu6=DTnq z;N3;6V_*kVc|cN$HKhOA4gobajslBfs6Qb8iix0>V^5G46hQT5-TN7k;ttbhSzlTP zUo6L+jMxTRDBIvTbY`9u4?aJzPa?Uz1KF&Yau>w;R5wIz<`f(jh|GlXQ05p~0w0H( zv63BW5#@VN9;-g)5W^V3jUmsEN5K(n*^)$rvP2dJI5?ZDb!xFw_+IT|*9-}ru*EBr z+dAuws_&0>xhG!p{E`IfELa5m#ZjjQUdy2@=0wp-X~_v6=%idqa2Bw49xv<-`%Q|SGn zj-^Msh>6RLJ+=Q}${eGN0jUEG@FV0c$hEH^R9N!hYcRdw2hizPW1l*3>xUsI=Ds-3 zzr5mLoFK@dWRdNJupFiWunsF=o#n~^2l#qh0+c_+!tF*EbarDShzkh1Ns~`kr;6D?;>d%0iKD}T zU#MR0q6|rBL4?^tX`d3KvE!`6nDQ^aL4`6w2x-=m6LX*?iiC7TW8Wce3Nq_SSaE;^ zLs<@j%=6n5;RnkJyA>Gz)v$PcTYD_eY!OEc$-~x4l(~r^jbiPe zye4%L=w!wyf)^o0oq)Z8Ou$Hq%A9JFOCKF6fX?{w(LxSq7r+CodMyY3IzPV=cS{>E zN|ti_Ar&NM^9SpM_GX+f$EGmaGb~YYeEQIwm19<~EdW{xaxdgOpN@lZ#|#j!cmnnn zwd_+Wzz(pl=9?1P4-xpBvHXEY`Q6w`i3}D{!6jZ#+Gk|M2sSo14z!th=$@xtis=z; zg%M)|MOS3yhX@s>E*xZW0QP|Vw5@Rg2m`@jVv&;upesz%rUOVbl(4R_PE2xbGFFKU z9y9p#oPJ&CgE66ooB0oD_?&o>*bU&+B+jw2XBI>EEUL7lqc^OPNvDb}-O^Tt zi2*gT<{z(mxxBh!Z4@emrHQS%gJKf-7$LV8`E=eH9*;29?9R{Q2}MW59K$o0rQmN~ z#f)DH#P540o3d0te#Xeao(;?z;My<4_5_}QAr5e=&@!AVe2c>Rt6Mj`%6~GgV6P4o zZB;klI=4Doau~uic^CqHLurOwtUwI3mJ%dT9|t8%b0LGud{07va?mEfIRh5xg$7^Q zn4q#Y{D_ldGy!25kUH|qB4Z0gh~!leZSZLXgyHmFxjr}EY z2$;9N7jrnzaE-XUyF@}mCA&Uxhc>1Gna5$T*vC?XI1Fi0Z&Hox2%|AyZ;7BjR-zLm zcI(#DI|lr>gCZxgZFI>*$w!K!KqM7&;HJ%uJBKg+o3{ftEO?gv2pHFX3vn6HUlnWu zvw0bw@5szA8P$9%mRHV@4}d!U#LC_ycFz<96CrDhbp6G2=W^DcG=$ZhgPHLbzP+)Q zVetUEk1x;&W2vR_I=&~bwSi3+J3>(y``U`z#`y2dnhH$dgTSY;i;@ilf1tzRPj@w9 zjLNohR-}34_pA?jVPXq1wD}LnK!Dz$o3{dB(g!9^xg>AUC5!!jr^@`cJbDy5Y@$Z` z;fXHQro|Im2h!_|Br>J$nYP)&zTk;>=5oingfJ7f*7L!98?rF)YPQ*s~kEIG7#>p zP>sREJOLsNy;UdO=J$WMigrL$OHtOKsy(@@9MN7uF)`7uAHh5<;`+2Q4ciX?k3;zC z9P3bF=%i6AhG*u8UFF3Ev%SNbj@+41?67i7otDmlO^1%mxmEn=v}0Y}!Yw*?1{`S* z+5F{q_PUSvH;&Q^+4rIMus2`Mg;)aIzk$@E8J%z~w1zVDE6XmgcZQH;t@PhT0nwKS z8F#l|lPw~mG5)WPC_E@DP}XoLf(S(e-1|UN0yXDD;M|0lm#`hbxp}7cxEH9@{6Vts z9f(5JaI9PQKKrTT7n^2yLaHUDZZJ}Ka4=Zcd{0rNTHnR#kthHOzP|O|J(y{Mm-XLi zJJ-(}V_pSFjhbI~k2pTf%i_&(CQ{^$ZNE1h{)H$6B&>f?mECpHW~?ZXAi8e`j0;BI z2NN$XsFo2y+1{}fgx(4iRFJ;?nnVw(n;jg-4{{o-2u`1vQJGK19z#*2twGwj8pH<1 zz!?m7)CJ;Tr>)dHSIQU`EXZo#W<sNC1t$aG8&hapdg)arV3l>KRTv z2JDfa;Hx$q>9rZRc=g+<@Swvu$$5upHqvlCp9pY$p2E-C&D;6S6#Hk1bG-`a5#l*ME0|PTs$b!w=)ZhyvT_asRd( zrEGjY44RuqXpwCViwA{JR?kWIXJpCx-y9S8cwy4UqlW{vuJ3{%57FoOi} z!#I0D@W+}$y<7$qGz9luEfMQq@J~+L8WO}|YkPz$=IgO=fp$j1%_PBRW^_$}TiNJo zGAXkq;tl0#K$yuBMUp16w8O5L2DFPzcpQ;uG5#o>IoX}!Cukwn)((9*!xO2M#%98twP$eKJ-Mx!RqRAgwteB1!BOIBP+LSLTzWRwHNu4jnR{nvHU-SW;HSv2UpwC}fjtu=c!Vt00G0+9{)t?UCU zuRIaRibTWv#~Wy!S}=kj?@ve9rulrdVC>()k*=_l5_H_&c!suJ>MaPi5Ebgk$q2h2 zK}!oxX-xq25|%kA<&!qEFbdpB{wq0ON%0_fOjA88Eur)x20b8>04FIy%C(8_Mxi|z zO=Pf7c8^k$5wU@mOuAxa{{555mZ=c%)VCJEd>j0K{#ND3h=GXt_?V<5K-pg(2WXps zBx4hrNT)#xhvJtt3TwC&vr)-BY;V@}R+`bNVSbu?B#1h`#iRdf1nOn3n_P@$Y*7Y~ zzG&`MJurhyg=VyR_=bYSGo%rqi!fWG%)S4mZqA8-B#6Q_9{MWBqjTtjMaa?%1JKpc zat9Mc?bEv59RPYU!Y3;$4K=wCI5n4@{+lBc)OWrL(Kb1jfYMomP8jt>VCYH%Cx`5e z=$$YJ6}H9mMJu-nVLqdh4ljDRo+NB$%$$MoU#F52flxLOZuUg_7Uvp8ftLI!78szh{Hdt&pj zY47}GUqP3RDlj{1wu&@S@rZau2!|`8Kx#Ks{+`lm!E>iN9ywuq|DS0MtC?s92$@K5 zxCw=Oq-Ue3c{_^qw5NiyMT8$puJ&70G{a?8dHVFx?oNcz;*PIvp90SNm24~0;gw8x zsWSc>SdtV#i8>EZxsPb)a6C0YFihm9q@Z4d9p{~LW|7?$9KNs$nX^%Gqna9cXULms z>t*Z1WSu|Tv%j+W(}ULs2vCX8Orll7fRxZlz)Xxg7*I)o;`89xPF__=W-ZrjE|h+H zc7B{C%gV$A##x*OrDZnOQ7(u<45_&DN0|ctSc>FtewC+o zWFx6Jz|A2!yPuT#X^15lqzKMLxcLGh)zA|%{mQG zpfE6icem7jnfkOxfRNAGi7-JWtMLIcxyJfQJ0J{!8%SZQoyeFSgSGE64lN)-r{J$s z?|4)Yq9Utq$`SMFZ51J_aJ~l+pA^qZoiRU2a_08j&o2rMI@iUsrAub=dE|_}7yf$V z8$pQg(5Nqg#{!=bSz&oOf7{|pUZ+904ox;fZ17P(H04S7#f9(s)z|WP#=+I`tU-@Y zj_1UjiW+7rT7G%qx|=F^i<1K-@nKR;z&>xV`z1`mWn6&~ou9F&$UkkH9JeZX{;sv`O| zpe_^8*Tug&{+-|{tfEq%xghc*TcIc_;2T`R1||iVijWAveQYS-UWLozKYw_@1= zl&kBcyW5U9rz`)x=-H8fFIsf%*m0NM0QAENF)<`Xd$U({hy=g?5n3P_EN6Zn41|x~ zN{25k2RR%B>I=Z+kf5X&Y6!-NIW_WKNk_~s1sHWUa9lvr&ZRf z{&SZl9*`^PyXE%w_yM|@uw2-vQ~I9|>N6kIE%F&0>T5TD2pFWR*FM|zL>LqdiDAa` z2LJQP>HoUqOk}|)w&s86wGfGly1Ge?m-2Tk+g7;Hd!0K^FvrMz!-mNF7NINeHYMn5 zjjt}5j+P0M3X+hdpw{Yr>+kb-Xfv<1r#{GZ=T2F4bW%p!LUJ><_Wwx&7WSqvNn6{h zLyN0KjQ+lg+N>h{?~;jK?I?j?^-)zcw*Uh!*KFvnXZ*JFG|9FeLKo%)C``Z`#wuO6 z?qnzoKo+murEK=kuV2=el8#OEDjkF11Q@HKcNX5h%gYJg?7yFB6hjv#UcI<5>MURY zg?w@|{B6`Hvgl&4N$5Wx;?RfIzPUk;S+BGWTm7qU)jJoNOoe>mzaYE!?vp;i;mx2WqoA~kL0cCyJ_NL@eG-wDPUX@}r`=3wH9k*of7REE- z$wVU>tLlv+&dklL|9h2MN31WE4<2yP`_Z^yrM1YnZ!w32z##dvK#Fb^V*Ij?M=R{= z`WlM3PIBYa_^Cq@cMfx1_0NoZ#mqRE`e?ut>(@6Z8H_|k(EHQr&*!A2b!qvBn<_l0u4+mPGI|^@07?#e zKNn+;}&T-hc=e0*jQdSdj#*-A2%YbJ}IK( zdtOUQ=)%*^_rx=QyZ(7o=DZEehn-_Sju2jJy-<3!o2iL_V&VP#^wRw6?*G~@RNmp8 z8bt7)?T3V5)vKJ{YZG_9Tx9tB+X3bA5;8J>CsFLdRajz@62b=2F5cTc-U_3i%Dv%! zRo~&+gzV$*jTU9_KNrr9F34*vdoGAhCOX-o$x@2lKT_c54&?stZN5C@^}5UUY~dW(z4IPXH8q?~iR-kJ(;iG-vtf2~LtjXAIc9*$;sR&XvKVcmKUo z!ZO@Y+kvjm4cIQ)zBlI9g>-zce!n9RbF=@SSi4dzt=_<7sR>I5BI37`RfM-#@u8Ub zzgLM&m_?WQg=kVqU$(b<$_MrM*6U`0xiv7pP}g&=ulh0&v_^TO=L*6C5!gGVCDnu)KVD zf=N)X{%W^;r(RQ$l{z?gaQgf+f^H)ti#YBc9(8Mtx<*oIc3e{F1GvBtO|=58<?S$D~$32}u6Aih1&kX&5-`=G4g4egz&Yd&Km1y(k&7|Nj8u|3O z;P+3M;!@&eFw8~v2SfFLJ<(b`QQcN_>S0>Zf^AOR&W%$?pG8}ZNHlAZ#$chd;XQCH zJEjiYH2|C>mYjI?H-BoPOKhaGFJCUA1`K4mZfz|Ie}L5xhGROcPz?AfKY5BlYu5=! z|Kt<1cCz0eo~XN7E1+kJV)vsQ<3+< zaBUJA6Jayx2-+@i7d;XdP4%n|dhrsDa=CltpT4~BvvF!>^Hl1&_rv7x{!^kYtg^`z z8N349d%!a|*+jdHxc)0Wsb?S3`SLe!iZOqv1GD-Ukm2ruoLPuobMB?hTYR6*pLPvW zbMk9Js`R1idq+$V#XW;V`+k1S0(;E6YJeh4ny_9szxo^XsH{~VByEsv)YE9Qgjk7& zva+%TzI5$G z-vyh+#QVlf@bK^;TU)$Q?DCF5k3)06_<1*;LHx2$;cv(gnb3oF++mUm?BG=RmHClr zS$ub(QN+-(_v^p=s9*vYQitxvxS`gL0Q<#@eXDW>wx7?RHrKL;Bcr}yM0kLOmimPl z7hc=Q6+VK;*wN3#d_SR?k<`e4ek5QTI**G@nL5>VPjvLz>1AbQOGb?vh3h3)nwbq6 zmhpD})=JN5FJ8QO{_2%)8ahRGOhq%<(xX4{CI)a4G$l=!EZaRe*{#}i8GKUBBBVOz zD9#FqH&!xMl0rpYCe|+b)Tx27cA8S6)YlhnyWKrKEo$x0UAlDU+O^pG_bqUFW=MmE z;(aOSd^n(qcJN;@Z=A7})U2L7ZQ81+efxORA@-oh$JQw5zIPvQOZ&#pXKh3Zu)p>H zlf8q+>(r@Jx;Jj#EI)Dh@URKk1{3z7b?JnO6DRtqONlyfGCYxR@%C*)YYTMaJ}WRt z*n6Dfw<*5kDzc{yc-s~JBfC~+yJ}mhyuAEuUtfLTCSw*ZTo{>P1%`@ot^GPr&vBC` zanGI804H>YxplbeoahBkPGB?djEst6=6Y=lZD@cpb>{8gzuzwHtYWfR--pOJ_)JYnEBy8y1!6MWUyjTVk0O2H#@LM^r!;`x)c9-%Lr@{ z$!s#+l63+H!z4KTftcITQ5sZ&{op5TlzL_zWk)4E;JrOEfi0o^M z3MEUcY$ugamO>O!Y6{sW6j@TTWJ!`r*-K;zZ3rcWq~dwqWoFL#pE+~pfBnyMJ=gU- zPuDeP=Cpi&-|y%9`P|Fy3F^&F!=L z;g6&ognQ|S@Xm;lBQ1qAlELu1P#BGWck={8{sB^6rwO7;zVg8>w?sZkMiame$%eJN zf8RbPVAt8&V}^I8{lS=qsPjYYYm!DU`g$LqgjDu|jJ^(U!AQd&>-1dY%VRmR`kI_8Bp1H|Ftb$e$o|F8qLq=&Y5Gu z5aGbBTZ9#4u-Gx;TGHx4X4lapl* z{Q6x#MMg#jy?XVkQUB(`{gfZmv&4^w=9oMdfvTIY81!vwZuNRCQ>vbY zTz$U$RTBShbw(WU`ZgW5`ltDp|It51sJieO|6s>bs&4nlkt4%%O+N9sVUxGz5HZ~p}>sC2t?AW+Gr*?LG z_mAFOOpU-1Z+EdY=Iy(fw<}3?>ljQSu;LCrRk~&83c89{l6RnhLiCL$Ja`{Ecoh_B+rM+mlqkC}-HMQ* z6EF?Zhf+!Bhco1Uu!u6%+}a1BHrlZ?7i4Ij%PuV!DO_RXy2YxF=aFc~wu3IDv)dr| zI@J2s3>;|4IOpf4o|c*@4Z;h<+J*hfpl?uu# z-Beb-pF}6N&(pUu%(A`#yMYLpjs=W|)bK9mb6x&yv5e83gN6W?Zpmd8Ar|CHUqCC~`1gDvn(+ z)6A?T$0IZ}v=%YUh|!}Ly?;500CxyCxn;|i>pS{vyh(9o;A7^|FdRsqXq0R0*tt`# zLBwE53psADo!vdCo-$ZEk~oNfPlpVEvEq4l98ENG7b1IuQNXIDvmh9C1$3ta{pnPN*$q?QE8GkB%b6iT*EKQ5OX7=lWqRO zg8r2!JXuQ2OaI}SCJnr_MvflcRG}7;{Oly(HqsIUu@5;r&s}DFSAJ}X<*dpc3V!_l zI~j38!?z;IwwKoca$8&5O)7(SV6^l-EgLar9gVX7yiYo_O{Pqd z*~kN=K}^I4HD(^)M#SAnp%#r;wtTV#PkV29iN|^`=*-jau!_>P#I7+&gs5(YmO-e{ z-Ngbh3a^3c;Opou3g6>Fth*kPWzU(Bv_FRVE|_tTOnYgYvdg}*f$bxW^5|n^oL}0L zIL#XIEP6Q|j=p*|k4{Ph9p%K=ABxHSL_@ZTLM@r6Oi?ucZD}tkdeZ0P?LF+~CK*nJ z8ElhA?@k=AREfwt6rD3IM6G8oFX$tsM?(xo@hkDjBBQ-2u_j^DT>TL{q&P2 z5+I_kAxac;=cTconH6TF)XE%}=TDxrJ#&J!efX%LIEKT{&~D97v(Bq^eeBXf2#&&zZ$ zE&99K3@B&X*Fo z;iQQZSE*M^eBLG%!i>4{@TL@Fg+33#!!B`COXVZy&h5atG;QGZZdr^VKtg4^h990exMXY@nST(g*KpAL zM(nYprjcvJRndUQK(E@?4?w8khF*FPfFeq~8XZP$dL|OI%r^=S5BG&X`!i~8k<#DT zsE1rMssei(-2d}pWl`u$)aNNX-OkWuHocqh5nrBzM(J~ku%#cSRwvufzEyu*&8 zgbkm-{T9-7Npuivrr*6QV=t{+K`fQH!v0KECM#@eB%Z1|b7o$UF^R+gTOosN09{as zx4=Z>G%QK|r4NDK!O*7>wzi#+1gFcW{`d(K>XWZ&hcc!91k=Cp3ZbZT3MW`vU1%Cf zWa}dWaV6im{lTYL6Md|hr)31{O46#TL7FD4l$hnE#VGO8DD^h=?%ff{??uS`D$KUU zGLg-L62Dcnr{NpRe6opCrzSBCk&(C0P5;B|@3U(VF~}A0#mEN>PM$h-XZQb>#jtYC z57{QkqNW0~xrPG~bUv7t%!suioJq_N=36!cN5rq!{tFj&0WIXiLg}ckz9C?;v2i>r zxoIvfV@!Xmb3W5|F8tV|Xq3V?eFr%tl6FZHIUEPN=cJ= z(MULnkRP2tYehJDQ6e8pf@u!CtWb^G}IjMC^A2#dG=W zjDjV;c}{18XkdXGBw&#MobTT|=e~ag2p)PQ{G7Ku$j#~Ql^-s=OKi>Ykcqr*MINda zRUZSnMfgLhPh_|k!POYlW$7Nd^tLp|>8xfYqw+$2`K3l0?i|x`aw&~aC#({vygZcd zo0h1;Y^5vDiZPz5V5DjVGw2g7l_Wc%j7L;dvU6 zqggZ#@$BmNMyeS4?|0HD0RxuRqRtm(Lg-6m8;3KyjG5{0k2aZxP`P?JIyw??(4Q+q zj%6ldByCA8(}-6<@%?v$}WEdM9smk5#?OHK_!<=by=YBNyLE1n976=DRk)4F!=Zi}=?kaUDi zr9ldKBO6k9LiO9v`;$NIaB!BRqgJSFu5dWTwa3qx(I)933OS)TB*lYiW9{T}H*bcX zJ-Z!3sKmtNsjjB-Np@sJ2K zyZ>}SjNxU5lIWlll7AgQ&rwI2metb>9t@+^@ZYRuC6R*G3^aYn8!!6s#`H;~-ABD=&xY4v_UG$QpRP=l zM6MmlObMZU$rd?aCuQdGGZ^zOquxzr@NCs4p<%?r+sDVYD!||+hO$!htZ3A)U*E*U z#G+N-1xhnOD3fGYp`)f%@F1$Tj46?Cs-cNwp*e;Kkl7+b4SEupQ;k}uPM=)3vaXBY z>C=7n$<~vMz)#ds0++WZ+H=IT++o}ayIU$6{D*TV#)GiM*2J|92??PUyr!|dZ{U)1 zsep&y)C3P4sM){QmrqH0WDSDicO#6uY;)$3X#GU=?%jLhq)Drh|Fx8>wP9vuUDCtE zlYE3eIGcj9j$ionl^@atf-o{#69R?}79fgG`@6v|H)Tu9&Uc!fNr(WngZ$G?43P^N zTK-Lv;s_4V?Jc|pa6OO=5K?WD0DY2#AXet(IB@$h1_q%-gtB{|GCiK?Z9=v}$c(?#fY6vx~OQxm?SyY33E2v46CNB+!4f~!3QJjug8KH%KyB@GF(gUYtt}`8KFct<6cp`6fH)>h@ElQ{IjRhuN zvX`s*Q>RVy4kLP3{Ziy5iNi;?;()!PJ)KH-jPK+rQ#{ovRTuI%6H%b!vRnV~*5vAc z^e*IW<({X)e!wg2`0=FNcP=k|lXBvWrcU)r$_c)BQDr0l8ps1bd^iorIO1-4y5&0; zUFVRcZ=aa*4ZkRQTaLI84rpb9Du@)16yG#I0BaWVuJw7l@_P~d_qkL6Fb#m(fBKE9 z4}vMANgp>6B|$nh?}RLeB3P(DQ||S(2Bx%@Pfecxb=Z+3@u|0OuL|Q~pNB{fZ7=CjT`W>y(Bzj{rR}Jo@l<)2Bt+VWX7f~HReNLhp;x#c|LDF2jlzkn_ zl4*d}nDXD5+&v{!xpoMP8c@BcRSrdEb5*6RtPsjdz9nZ;bL6N|sk_PVG$?9Gxo|80 zeNG}3KnY6_v>*WEakmd=ZrkU@1E$=dk@Y-E%u!mCQzV`Zq*qVnWfOh1C zFMs`o5DJ3EHZ~nI!3Qprg^xl`A!KaIDx?hyL?`E1nRLn&|m*#jEH4rzOdZJ||pEw-Yb%=A24QG6|u z7*iCmfFyLBUp5k9EIQ*aljr}@gO4{cX_o1Dc6tyi9MPMF^p=7wd5TOxLz z#0=3S89e+$L)}Z%>bxzLOrbDk%Hg$%dJv(b+~A)Upn+Gufu_jDcwv8?|Y177Mm#e7Ff{_aw z!YgFgGE6#$-0){glgX3sY$3XmQoU=;Obvc*JikVq@BAE3Z3^t-^qlh| zWe#Kk*Cpq?b6{++19=n%?4>s~1xhrB4|hGs)z>Az{$K?fZ3<$ye&fddy%5y9oc&>$ zAmUxSc0F|X@VSn-JaFuo4^~Ag2E5y(&j59ilD!j_=r7G&+``~sQ;^w^ z!-u`2qjd%>dU?AY=xvLQf9VQaUW0m(tri)eEj~#gcl{2 zVLW3us5CHNtc#b$2*w{Ot0Nz#&w=LGs*qs?kl{i4R#xppN#bD|c}x0)&{}*mA{Xqo zGuoU@8aEy~etbPb3FiDPQ6!Hx)L7_+|Y7h)b=&(Uvk9^+Gfq`o~{qyhTzAp!bAZU#1P5MgoGu)2F zC&VVQa{@}#kU=ih=WODXDWj(HdO4nPP^9E)jnlsQeCk2l=}5MuA0r+l;vBq6Xg3f zV#$K?Ej=5m`iAi|`}VaTwMl8HogmsFp`lhJZlZDmej*~ZQ~7qpM?H_Wzj6VXs-rO8 z$w`~y0w_Iwy;Kkm^g>`YNz`izZvA>$)9OwxZ5{PM3Iihcq1KITh~kRq~AlDiEjKi0s+&~k@~`tzS|17?ZIk$#8)QhatR$|8yZK1DL|=k!2qGHnPwb+mk_|gq z2AbXE;oBH9ziaH6F+&Vukw%$>l-P$_^F-J;zRsNfz`TDuk9Yi{Qp*1kG=+Jt4)*Afj*$)C$ zEGHE*1<8oGl9=cuLviRQgD(?ZUU+_P&t^YGB0{836(dL z=T_iCUYfps#{mO&aAnDsMJw>mcFq!~RI7fQ0fE+F{k|f(w|B20cs1udB!-S_!5ySj zf9-#?7P;@26N2a86ylJmTj~Q+TuDf9JerYVD!RSGfrW=EWT}F6!`gu>d(CAfz!o)l zG&wzF@*+N_30dgjwHr4Qn5??G6BJ>QHfC-XU_d;q&I!v2kEj${($}TD5P1 z7OA(o9#K)fo;-OHSX^3~H1Cv=sp(ZJ14w?-=ijJ$Ap*WMFT)S+P#eZn`H<)taoqvh#nXHGl4U@T}N zmF<1-12P^xGVY21B+~pAGM>ra-adofWDSxbMIz4o8V`@iQxrUU%)GGP5_6RCiiw#} z@;Xux6CHd+>)#XC3o{Dv+Lb&|$-d6%`j>o&x$q z7vFOR_*@pD{sl}1T+fF)lsuon!iYHiY1+jF%+y-z-XmnA)@G(n3xM;h*WT zZo7#bcv)I9!0zPy`wd)mWWmD7?45ez@$1&FpI~8UH=5Lz6+}^3_Q2)Km)RQa@uh@? zJ*G#mN}p#7U*)fVi20HwOGb~5jEtJ#=#-=bfoiIkt}^;WIgsyVK&B1Oo9W+sC`vxd%8J{p_hk$1%Jg6ZQC2w#hC|*Y8(=z;!NOWMA&L?ms1Id9jwbs?Zk9P zip>$!=n&Z1*nR2JrT31{+MGTwL?a+s;u3sI2Ow*}8(vaG`lFN@w?Ki;> zm%o;#cI4jnT+DKD4;@~2uMfC)# zZx=&!Wkm?lI8?tKJD$H@&|gQV5jn=TZ7qt6bOsJ2jYuN4h+a{qEpdQG)B}A}QzeXl zpdCdo#%xD`fO7NKcvX>kg@rfS6Oq%71*S8u3*OcZViDl#27Ex=)?^zi4!oFG294!y*c-gUxGOTw=VoXu*g}~eROv#{q}~lb zq^GBcWn{8zo;bhc zF2osEcvs#T4<0mb+H^RP1+PrVWF{tS30oLjWQA|n(b1XMc-E|0Lbk(pfNJG3r$=Tg z6U{YMRP@}tSG9R0_68y)_6dfnOuaaCsE(bT9Rr*bkn?hMa2VniD^E_|yE+&HXmS9a z1i4p9vfADV%2rfaGj5a6R>}#KumG!Bt47)a-&81iQzNvwM0X9@> z5z}YP2pfYE7MuDx1L7?SeZc=L`|h0k;?b-o4s|F;m)m#_)i<9T}IbV%USX5eShsTaSch0Esz<~qVsdaw( zsXgpXI3lD|4ojAZ_6$W)WlVV2?ANca!H=br!xEb?{had#xBxP_K-^eKNmVmkSJis$ z+q*aF(W8eH%O5vXQ&S5y*qy?Vxw7ux|L{Xy1%=!*GFpYFCdla!s|V!peCWH6A3GD= zKi$V98NS_AMdjwgtb&3Gu>Xq7r?5*q8}7Lt+d)&)@6x3qZuS28q9eYzT)1!nUz$CW z&|^zckpqug)}0jqhI{@GpSVyWIQg)YM~`;m+ue?EO>V8MI}0L%hOw1Z{kvcK5vu8Vs zl>C42WBs45Bdf=6CsC&(I8`9Nh(3Ql2U}}wIA|pFQbY!x@-uqU8k`nU%Jg}8_lag4 zAga23%Bn3w>jd%awEajYK0eOXBz)evvvucy4IFNT@bGXV?~({|HDAJ3t0R!rs#m7M zUmo1w3xQ>Gh;W;PSB10nFP2kC-B_Zxd3O)Rxr?L?b(bEVP#m-=i|3vZeGQUxB|>ds z=lOjN7d{!Ijej(k`NoC}hO^DQxN@g3zevMmaGDwPiPyaNyrI?53st=$|!wLI^yS@;6x|FkWp^J2E;CTpt!$BM1*sm zo&sMWYVY&6B+D?#%lbgj*rn%=z~#Y`<;1^Z!r*X9O}%P2 zTvhdv6XZGv=i(KYl;j2FO-4ebI5|7#LjQe60Tfj11kMx&?W+CzJACafLQPall!z5P zDv&iR`d&bLPke6MSVJM=!FN~s!pI(p2^oLArn{k`A&yC=0!X_R906I|HUnhv)}gCg zyPJ!pBIB*zpI6;|t8S?aqP$mVmeQ1ZD)q_Y`2x2ApDrzYY>5*TI>1-O<;#~NDYL$! z9F5}fB1-@CIB!~pOfPp8oI7@0`tezN&(F;~*9VXzVIo2;U4Ld!HsL`3C2y4OAD@+!p1uXji&WRD26IVT z27k4x^&=)`5YpPS5%Pk{1v<9L5`aRoBhFkJGv8AJ{_IjCNhf8PHeGGien`@GvU?Vy z2JWLUiI;DBaYcVm5LfGk3vY2x5^cdR#|awWlZ2c_g(7p=Mmy{I^HH#zUCA9IOf^h$ zp>0Tm+%E6}s9o6LHaq(MZ5+$#%8>Cc-B=|-u7(q;%#LblPD(0ZI!c1oJ~jI6Ss6=L zOF?PX?%i!dL`Bk9RqrOWWi5b9I$J%mK+N~ysRO$*rwCfFDEtrMVh*K5 zBxfaK0;TXW;&y~6LT#3rK$lUn9BsC|w9po&%k+yYTAI&tuk1sZCbUtW#pw9VL51lA zg+ieAB;6Ahgyh;R(t=7BLJ0D>b&J>BzF0sG)Cqt^V2Yt;8(Z|84NIAax9kRmOM1*9 zOvvgJeci>c1j$8Z$M)T+ehQpJ#6)RYiTpq(Xj4eP+^_f-62tfiE-IYIFybS5>R_TH zljAen#7k~S2~dXuWRhMpr+U=LE_$cEAmQ^wC7(!io8eFaWRW2VPko#<;pi?|^poURsCa0!-V5 zgU87WlSEPwk`GSIxCJf{Q)SUEBm znFA&Bo5|(m?k<)+Z!M+|*B%5*H}Aojg{lm|_yw?%D$gb?kUxB5IG)%UX)%$h^`Pw5 z^Lg1M^gqT+B^vWkR8;hg76Xv5cd*S%pp=)eg3O4jH_nz(Skld9YPvpsUMEruRaI5D zig%->O-pho3Q2+k@kk~}VAjsUO|zS=TrlltRXHU(P;5^WefSV^^vabH$Wd-4xGA+F zHUD)VCV(h)xy3#9htdU72Fy9>*t9gQOtdRJ;j>P8 zKT#Ev=Et~4kE(bVjr~(DOX#iJjCV)_SJPIlQZOiRC)K;!{Y(#J%_6_Ht1@A-{3dEs9&BTXaym(P!D8#xQd-D93Fdmwchf6Oy z8Rw*VAYwIZ*A_%fau{h8BNx|{RF{nad_2~`K)^I3peqSQ#XO0{t6JYYH|f6m^!f7% zxZdn2&tovS{otx4SXr{|<@^{C*T}J1yLN36?@D?OKy?hmFkr?;M#CTm$X=atWQ+Xb zFTmc!>R^AhM5rJk3}#nt>(j27wl8m z+rrEB=N8iN$-G++5Y`Ze7>@^W#X`x_v+ayu5fD5lwv4+CVjyM}6RX~w+z$fn1OO*` zqop??kY=|t!mt|~&{@FT-t+HmGJ*jteTl5D2hfZ75P&0>M)r@RhlY(-aa~)U_2!L! z7y)?7InX3btyET*WAEY!0<~m#3eAZ|)Fyq4uw~k`>s-U8lB7GeS02<&@&54n_1MJfqKaMyp8i!6&G#lD#Ah6mcHmC?Pmd+y}P^ze? zko2^3KwWdptCZO&GB^@9il7FLPfPg7b5KhcuwCo~S;3ktVzAzfF)9DM%bk|&U*hU4 zs$zm(2d0M^tBFq)8AUnC0IM9NI6taX!PpisQX+|oozB9gQfj7A3eNFyqEEY7$#p(fl>=_iiBJIcA}9*}$mmay163fC_L z>IJ^mt#k9{%^>~G6nTY~?M9Kt(}?^tkSfX*T$3QiOMB5k2VcR*k{Dr=s@|SiIoa8a zqHd~wgxexWtRGKbf(%$X4R$!1FzGB6CZOei^m&u1ObJtATcP~pje(m@Yt&G!7&~I9NkM!c@GOX zs*jHlq+07ZX4fR338hk*QT*|vgo=*|ewN)q$ieLf8)Q>3lcc>x!dSYKi*<{yc2MFa zM4W=Fq9CPN4ppEU7L{Uz0tCmq;WKs$;g@nvUEWJ*`{3YUp$?Gb`x2w?+P%AMDfM$7 zIoSMvdPkyFzmmsw$;-+bYDS?0(4WZm=5hEO7J$=AGu_H;DL&uI#CA$xbp!_-xJ*Q> z4dF)18zfh&#$dihEwS}Dq+CXeW;(JQX?}!YB5qldTA6Kv^_PBWs*)B0=c2EwM6KLZfjllV(*NiN=%1;f@E?9tt@d2>VHKSr&z0kR~? zOQ@v6AxuG{T$!XPww3@-Vi%e6*f5p;tzj^7@N(Jgs}=NkKlTBuDD4I!Fq~T0ArS3) z3u8Vgo`&+~5zPA$e!e%c_;w@y=<S!FpD{s){&;4`cwhY(C@#c)L)xa;SlGoPfeprUXNsk>fIXZ zSlTp+66c5|mLPSno@zWneV0BM>cWvg9eH!_9%@{gL+vVX2Qz6(+f&Ikf@NYSq0AoL z?H4ZSQWYLEX3V8$+1bNbi%a1IZ%?Gu8xar0h^We4fDzD`1`;Z>9&?LMW!}`Ntc%ZIwVmWGXM0_#BbGbp;uhLVI(8++^4v>9i8P5fjYB z(C`-aK`^e^!9o*FizTbd9aYV1L6Q&+1jXDUs0XXaD%wozbcxgy0pefa0Z} zi8)Xrr6t|T(pfc;*dYuXL0q1gsnfszq&M^qFaN)E?DR3*`!gU zA!ItFxfMC8U!P&Yq;*MTknw4A=5!JM)PlZ?VgX!+xeDJ#0#<37>M3gE>QN7_(|IQdX5` zNGl+R{%iNY|9`O~vD7W0JV|Irj|N^i5(_J{+bZRGXHsjfUGBr1LG6V>D zvZ{$v^oe7Kq#i`l?^OAD@l5y1#XwfqfFY7&4k1@80Vb*wMDb2g3#6l;kV552d%Mer^r=G00#b>)htG7qO>dhaM=$x>wGO~IM&&!F| zbpNsfAa?{WNP2Zu|PcUOZ3;I&BX zSFBiZ=qf3Nh^Al&WqO$VHjE`c8edM!CBQ^*do{y7lz-L}?=uH|IH71f@P|iSLc%(} zWfd{)7&V-nw{LF|O1vvAeL*@!eC)wv7XJbaWD&ijpcM(BY^goi#ziE53Mwipl%d3J z2yTTlvOG0S3#F{hv=az&N`&G;1}}IX^%q^5_)Kz1NJVc!3CkowO06U5-m$TI6vy;; z=``kS<1Lc0qcSDA)QV+NqE;h~hw8TouF4D@mM(n(OhZyI(VsS9D3Luago!eE@L;)1 z!82kzV_LBZyxO9VAD<637j=ulgQcj7BbyRFeJoWP#f?A4vzW*ZBxt%6Sx7)dVfiSC zCw&*lBNyJtbXm5{CmoO7#b_5AKZ-+0Zh1D2m<3^1x5ra!8ER%?3qGarI z!j3nnwR%t~re^EGwYjv6{gZ(U0zXpWG80F2|Nb_bk1+i%$gF%?YIbZ2TQG@P?S^7f znWIJlE)dp(I(P&;hRzrhsOz-|DZvhhu*;}N&4xD-XnShf-1NiEdCj?)G~F>W(UPP| ztOt@>)Ox!ZQZpwWj0eNRJzFAi6ii81Nuyv%H&4&u^}X(HKBVk~Q8Ek( zuu0T0Qcg@eCMO71kQ0`ya3ot-j5{U7kt11xME7BA^8PD6X^PwqH3cAniTB8ag*Oej z6O2Vu5;6xIdV%@K96}l29%x6OfQ-&!dFk7_?0y;UpRg!oAVYge9Z>l2CbUUq-VRAc z^el1w{BdU(0_?oBW9I!B?nEplUypZJgWfY&yc@|=^OQ2>U6uh?tlXxJ`fD&y37ln% z1kuBNns-^JPMv2U*|vlMSU;;%)6}=n;7i`wrALqF1Ur&c=yt)eQ#B}pZJ|xnC9GI# z`6*Z=pd|<)C09FazD+PQuI$q%c@A_NL|YA0e(4?>7?L>O2C`N;0IK_r*juD9=!;~N z&ZfC3V*u-h-%gTRg;WHB@eL$00%22~$D}7p{s9{P96HY>xl&;w+Y^cl>vjdrDoLeI zW2nXnIhKgrcn>WBLd(h~wZy>E4%ZI0jX7ix8;+(Z#3|W7xiFR(o2=&9P$fM#T_KCm zw5gz?3m|jvHiKKoVTOGPWvpb7vLrb2i1HE(Gtax?t|YDjRLGbXY4sy)StVh?dEkyO zO9*F1*qUWeWwy%^s%uJ;ssh(UL`CJeO+G$zHwC*!O`2S$HX%ox6ziMxcj3)y3snD_ralZcm;S=(l!W*ug#7FyL)S=YfzSSC%i?z) z8a`v7gus|qp(_G3N1s1mf^esC%7VT^Ig-u=!s$6wOaw9m50v-|XXyxcTmclYHb++jF$*fz{80NJIe;8_0gZw9+`WEySs%JRNMZ#MYCjKk(Ui6S$S% ziih9!?Hgb$lrrvjk%Vveh_NK* zr4*WTX@fNCEgi@@O+`27`Dai);y+G$gGmkI0HibI|-J*_y!j%PmjfEd4W(WJM zr9v$I%=4m5^{_m0Ktu7;@73nsp_BaoC5q)L3sz89ao`N4wO zAkBh>lArj)WkZZ*2pEQgqoep&Vb7e9%z>Nr4VoLFf1C-9ljn(qJtJDBw1= zh=5XZAd*0iBxHrW4|m-fucaj{5hC7VV%8{XK7#9$)JTBC=}my5G#vd?@^#685jvVQ z53LCKbKw2163t7IMV8DJ=7_js+~;#VULL1}!09&co*%IEfk`)R_>jp(VFjyFma<}7 z45`<_gKCIu$q#E2H2ILwxcNL-5PDZ~A=2CCb03~mmoT>IS)34+{kd)cJtA@O@gBt$ z`G)T8*_Py|-wh{bkh(XP3#(S2yd?$tMQfTyH02SC1>y(PBs{FVPUml5|4*=n{{Pd;yZ^tF5%!PoHbVk< zGLXO|8*NJ%9&aBImac)upz_=Al|8i`I^xPGl0xb?RRE^MV`vI!6N??5di>_w4TKL8 zA%oagpBeu5>en8CB6)L&ehL0Ey7DnFLPWJpN;L3f0)GB^4eXVwRI%!RV%0#jQod>3 zk3?SWmV0WfyE`fhmtk&-TJi7auxt>eX{Wuoja%?3AUEYwi2v=g*H=7&c~(|mchDd$ zVKo*D9UhM>oHjBCj9MFMER)&mN3o?~x`|U~txSL!VMsV5wo+t93Lb7LhDXeOUM7i> zskIn*0iiNJ)4d2hCP_~PcNuyVe-J=mh3Cbb{x&OQMAwEaLF`amVy%V^8!jM3@S&5X zgizXI4j2~Lh-iQk@`{Qb#;c3a(Ij=iGD)N(*eYSe3!V+Bp4EJ}7h1&Qnky>it@X8&cwLr)zC#T1bNq5J~&|;C?g64f`|qo_T=R)Vqf4MT`9V&&zFuIDjyj?HE1A# zmE#pi{PTEvht?i&VtEc#Svd-EiY=CYV*S-$BKT-$IMS$yLPqC!A^6BhKB@i zTmN(W;M-H0c&@G&T(hTZM6C@Y7QgH`aPcd@wkE&SSvPd-y+#@xeQZ-zw~X_!NzRxv z<>%sJK@Jfsz}6R+O9F*#gT=KA6N`?KEcrNHSyy#Q`6h?diCn1{kNwVXXMj9 z+hy{|k?W@S*tSR4b)Tt5XRr9+tSrlwUn$!x)i6)@ zs2}&%*52o!bM!z$!blRhhXeErEi~3o;B+@pMN+eW(I2o8^y$E<;s}LSrI=! zwm$EGAY~R`P+b-oBk`Zyyddw9B_ox@S|T#yz;;=AkaS z;Rk@lwvQS;IUwsMo_qM1j{9JvzQS;h>oaiRk#tC!U6GAG&O%RH_CFehUdT0INfSbYzx zVyo5`qS&?G0|p#Ml1F1k%(BUDv9l;w-oqXWwKl;@OEw18x|@v@?4 ztC4RwMQ`=7qetgBI80o*a;4MR={t7so@{4#kldo@@%u-7eH*Y*e)JfM*u&(hQ@geD zc}4Evos`3iy30Kom8}@ej#F>tbNJ**6Y7RWhv)R}y7!h^T2WC1e7>61clzwDyqjN6 zoG=PKXt?+AnKM&?rne=(C{b5a8%@{uIoFcVpWiGw4|nAr1SXv~)hnDqa4?F1nDTXo1>6&*_2S;C9=|!)u@K4>4XVB6sbEI+uDXO^X13tpI!Q_@ZG9)ZTdTnXy7$`=JBZo1qEp8HTw34 zuY1+3p{Q7=X0tjqe|>RZ6Mp?+ja3Os-^@yl8WBpro`V`S6s^B4YmFM0tQ5XIT#Xv0 zo7#Q5Y>gTlwZ2`iMhy!s&)>eSk@$b$;z8~;mYPp6+vlaRM{Z>7NP`jK!z|YPFKyI) AB>(^b literal 0 HcmV?d00001 diff --git a/test/reference/mc_hist.png b/test/reference/mc_hist.png new file mode 100644 index 0000000000000000000000000000000000000000..049a4192f383e8648898eda6fe8be67bd622d9f9 GIT binary patch literal 66038 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 literal 0 HcmV?d00001 diff --git a/test/reference/mc_pdf.png b/test/reference/mc_pdf.png new file mode 100644 index 0000000000000000000000000000000000000000..fd2728551994d83031f43c1e1fcc4f704eb04ed6 GIT binary patch literal 77470 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 literal 0 HcmV?d00001 diff --git a/test/reference/mc_qq.png b/test/reference/mc_qq.png new file mode 100644 index 0000000000000000000000000000000000000000..db3479763763c5f73ef30a72cf98d2acdca43ce7 GIT binary patch literal 78788 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

4eLBUdJ*zf0xD|EeGn>xXy5p^ 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 z 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 bbL$7{_dnIm?%bogQqRG|2j#y!;qCtb>RI9# literal 0 HcmV?d00001 diff --git a/test/reference/system_preview.png b/test/reference/system_preview.png new file mode 100644 index 0000000000000000000000000000000000000000..4d36acab5f34a1bf31335d276d4bbce60be33dd1 GIT binary patch literal 288287 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"nBaE82u|29`1XdcFPjj^ag{zvOUZjckU@ugjcZ} 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 Sw+@fd4<;w2F1xxU`{*ArTf~b1 literal 0 HcmV?d00001 diff --git a/test/unit_Engine/test_parallel_rc_insulation.jl b/test/unit_Engine/test_parallel_rc_insulation.jl index 397e9544..b13728db 100644 --- a/test/unit_Engine/test_parallel_rc_insulation.jl +++ b/test/unit_Engine/test_parallel_rc_insulation.jl @@ -215,18 +215,18 @@ end return_samples = true ) - @test size(result.measurements.Y) == (2, 2, 2) + @test size(surrogate(result).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 + @test uncertainty(real(surrogate(result).Y[1, 1, 1])) > 0 + @test uncertainty(imag(surrogate(result).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) ./ ω + ω = reshape(2π .* frequencies(result), 1, 1, :) + Rmeas = real.(surrogate(result).Z.values) + Lmeas = imag.(surrogate(result).Z.values) ./ ω + Gmeas = real.(surrogate(result).Y.values) + Cmeas = imag.(surrogate(result).Y.values) ./ ω X = vcat( reshape(result.samples.R, :, 12), reshape(result.samples.L, :, 12), @@ -249,7 +249,8 @@ end expected_Y = result.samples.G[:, :, :, t] .+ im .* ω .* result.samples.C[:, :, :, t] @test domain(lp_trial) === domain(result) - @test lp_trial.f == result.f + @test frequencies(lp_trial) == frequencies(result) + @test basis(lp_trial) == basis(result) @test lp_trial.Z.values == expected_Z @test lp_trial.Y.values == expected_Y end @@ -269,7 +270,7 @@ end for component in (:R, :L, :G, :C) samples = getproperty(result.samples, component) - stats = getproperty(result.stats, component) + stats = getproperty(result.statistics, component) for index in CartesianIndices(stats) trials = @view samples[index, :] @test stats[index].mean ≈ mean(trials) @@ -287,13 +288,13 @@ end 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 value.(surrogate(repeated).Z.values) == value.(surrogate(result).Z.values) + @test value.(surrogate(repeated).Y.values) == value.(surrogate(result).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) ./ ω) + vec(real.(surrogate(repeated).Z.values)), + vec(imag.(surrogate(repeated).Z.values) ./ ω), + vec(real.(surrogate(repeated).Y.values)), + vec(imag.(surrogate(repeated).Y.values) ./ ω) )) ≈ empirical_covariance single = UQ.mc( @@ -304,12 +305,29 @@ end print_step = 1000 ) for component in (:R, :L, :G, :C) - @test all(iszero(stat.std) for stat in getproperty(single.stats, component)) + @test all(iszero(stat.std) for stat in getproperty(single.statistics, 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 all(iszero ∘ uncertainty ∘ real, surrogate(single).Z.values) + @test all(iszero ∘ uncertainty ∘ imag, surrogate(single).Z.values) + @test all(iszero ∘ uncertainty ∘ real, surrogate(single).Y.values) + @test all(iszero ∘ uncertainty ∘ imag, surrogate(single).Y.values) @test_throws ArgumentError UQ.trial(single, 1) @test_throws ArgumentError rand(MersenneTwister(1), single) + + total = UQ.mc( + spec, + formulation; + trials = 1, + seed = 20260812, + print_step = 1000, + return_samples = true, + per_length = false + ) + @test basis(single) === :per_length + @test basis(total) === :total + for component in (:R, :L, :G, :C) + @test mean(total, component, 1, 1, 1) ≈ + 1000.0 * mean(single, component, 1, 1, 1) + end + @test basis(UQ.trial(total, 1)) === :total end diff --git a/test/unit_Engine/test_result_containers.jl b/test/unit_Engine/test_result_containers.jl new file mode 100644 index 00000000..7f45929a --- /dev/null +++ b/test/unit_Engine/test_result_containers.jl @@ -0,0 +1,368 @@ +@testitem "Result containers: numeric parity and selector grammar" setup = [defaults] begin + using DataFrames + + library = CablesLibrary() + load!(library; file_name = joinpath(pkgdir(LineCableModels), "test", "cable_test.json")) + design = first(values(library.data)) + constants = CableConstants(design) + + # Plain numerical fixture captured before the result-container migration. + @test constants.R ≈ 2.7567652874268654e-5 rtol = 2eps() + @test constants.L ≈ 2.8718381083175005e-7 rtol = 2eps() + @test constants.C ≈ 4.1335723330313053e-10 rtol = 2eps() + @test basis(constants) === :per_length + @test resistance(constants) === constants.R + @test inductance(constants) === constants.L + @test capacitance(constants) === constants.C + + baseparams = DataFrame(design, :baseparams) + @test baseparams.computed ≈ [ + constants.R * 1.0e3, + constants.L * 1.0e6, + constants.C * 1.0e9 + ] + + frequency = [50.0, 100.0, 200.0] + omega = 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 .* omega) + admittance = complex.(conductance_values, capacitance_values .* omega) + parameters = LineParameters(impedance, admittance, frequency; basis = :total) + + @test @inferred(basis(parameters)) === :total + @test @inferred(frequencies(parameters)) == frequency + @test @inferred(nconductors(parameters)) == 2 + @test @inferred(nfrequencies(parameters)) == 3 + @test Z(parameters) == impedance + @test Y(parameters) == admittance + @test Z(parameters, 1, 2) == impedance[1, 2, :] + @test Z(parameters, 1, 2, :) == impedance[1, 2, :] + @test Z(parameters, 1, 2, 2:3) == impedance[1, 2, 2:3] + @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, :) == imag.(impedance[1, 2, :]) + @test L(parameters, 1, 2, :) ≈ inductance_values[1, 2, :] + @test G(parameters, 2, 1) == conductance_values[2, 1, :] + @test B(parameters, 2, 1) == imag.(admittance[2, 1, :]) + @test C(parameters, 2, 1) ≈ capacitance_values[2, 1, :] + @test series_impedance(parameters) === parameters.Z + @test shunt_admittance(parameters) === parameters.Y + @test resistance(parameters, 1, 1) == R(parameters, 1, 1) + @test reactance(parameters, 1, 1) == X(parameters, 1, 1) + @test inductance(parameters, 1, 1) == L(parameters, 1, 1) + @test conductance(parameters, 1, 1) == G(parameters, 1, 1) + @test susceptance(parameters, 1, 1) == B(parameters, 1, 1) + @test capacitance(parameters, 1, 1) == C(parameters, 1, 1) + @test abs.(Z(parameters, 1, 1)) == abs.(impedance[1, 1, :]) + @test angle.(Y(parameters, 1, 1)) == angle.(admittance[1, 1, :]) + + selected = @inferred 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 basis(parameters[2]) === :total + @test frequencies(parameters[:]) == frequency + + series = SeriesImpedance(impedance; basis = :total) + shunt = ShuntAdmittance(admittance; basis = :total) + @test Z(series, 1, 1, :) == impedance[1, 1, :] + @test R(series, 1, 1) == resistance_values[1, 1, :] + @test X(series, 1, 1) == imag.(impedance[1, 1, :]) + @test Y(shunt, 1, 1, :) == admittance[1, 1, :] + @test G(shunt, 1, 1) == conductance_values[1, 1, :] + @test B(shunt, 1, 1) == imag.(admittance[1, 1, :]) + + series_frames, shunt_frames = DataFrame(parameters) + @test DataFrames.metadata(series_frames[1, 1], "units")[:R] == "Ω" + @test DataFrames.metadata(series_frames[1, 1], "units")[:L] == "mH" + @test DataFrames.metadata(shunt_frames[1, 1], "units")[:C] == "μF" + + 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 G(zero_frequency, 1, 1, 1) == real(admittance[1, 1, 1]) + @test B(zero_frequency, 1, 1, 1) == imag(admittance[1, 1, 1]) + @test_throws DomainError L(zero_frequency) + @test_throws DomainError C(zero_frequency, 1, 1, 1) + + @test_throws DimensionMismatch LineParameters( + zeros(ComplexF64, 2, 3, 1), + zeros(ComplexF64, 2, 3, 1), + [50.0] + ) + @test_throws DimensionMismatch LineParameters( + zeros(ComplexF64, 2, 2, 1), + zeros(ComplexF64, 3, 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] + ) + @test_throws ArgumentError LineParameters( + SeriesImpedance(zeros(ComplexF64, 1, 1, 1); basis = :per_length), + ShuntAdmittance(zeros(ComplexF64, 1, 1, 1); basis = :total), + [50.0] + ) +end + +@testitem "Monte Carlo containers: optional storage and joint trials" setup = [defaults] begin + using Random + using Measurements: measurement + + 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) + @test SampleSummary([1, 2, 3]).mean === 2.0 + + model = HistogramPDF([1.0, 3.0, 5.0], [0.25, 0.25]) + @test HistogramPDF([0, 1], [2]) isa HistogramPDF{Float64} + @test_throws DomainError quantile(model, -0.1) + constants_statistics = CableConstants(summary, summary, summary) + constants_samples = CableConstants(values, 2values, 3values) + constants_distributions = CableConstants(model, model, model) + constants_surrogate = CableConstants( + measurement(3.0, 1.0), + measurement(6.0, 2.0), + measurement(9.0, 3.0) + ) + constants_result = CableConstantsMC( + constants_statistics, + constants_samples, + constants_distributions, + constants_surrogate, + 5, + 0.95 + ) + + @test @inferred(statistics(constants_result)) === constants_statistics + @test @inferred(statistics(constants_result, :R)) === summary + @test @inferred(mean(constants_result, :R)) == 3.0 + @test @inferred(std(constants_result, :R)) == sqrt(2.5) + @test quantile(constants_result, :R, 0.05) == 1.2 + @test quantile(constants_result, :R, 0.50) == 3.0 + @test quantile(constants_result, :R, 0.95) == 4.8 + @test quantile(constants_result, :R, 0.25) == 2.0 + @test has_samples(constants_result) + @test has_distributions(constants_result) + @test samples(constants_result, :L) == 2values + @test distribution(constants_result, :C) === model + @test surrogate(constants_result) === constants_surrogate + @test basis(constants_result) === :per_length + @test ntrials(constants_result) == 5 + @test confidence(constants_result) == 0.95 + @test @inferred(trial(constants_result, 3)) == CableConstants(3.0, 6.0, 9.0) + @test rand(MersenneTwister(42), constants_result) isa CableConstants + @test_throws BoundsError trial(constants_result, 0) + @test_throws ArgumentError CableConstantsMC( + constants_statistics, + (R = values, L = values, C = values), + constants_distributions, + constants_surrogate, + 5, + 0.95 + ) + + no_storage = CableConstantsMC( + constants_statistics, + nothing, + nothing, + constants_surrogate, + 5, + 0.95 + ) + @test !has_samples(no_storage) + @test !has_distributions(no_storage) + @test_throws ArgumentError samples(no_storage, :R) + @test_throws ArgumentError distribution(no_storage, :R) + @test_throws ArgumentError quantile(no_storage, :R, 0.25) + @test_throws ArgumentError trial(no_storage, 1) + @test_throws ArgumentError rand(MersenneTwister(1), no_storage) + + frequency = [50.0, 100.0] + sample_count = 5 + resistance_samples = reshape(collect(1.0:10.0), 1, 1, 2, sample_count) .* 1.0e-3 + inductance_samples = resistance_samples .* 1.0e-3 + capacitance_samples = resistance_samples .* 1.0e-6 + conductance_samples = resistance_samples .* 1.0e-4 + summarize(samples_array) = map( + index -> SampleSummary(view(samples_array, index.I..., :)), + CartesianIndices(size(samples_array)[1:3]) + ) + line_statistics = RLCG( + summarize(resistance_samples), + summarize(inductance_samples), + summarize(capacitance_samples), + summarize(conductance_samples) + ) + line_samples = RLCG( + resistance_samples, + inductance_samples, + capacitance_samples, + conductance_samples + ) + line_distributions = RLCG( + fill(model, 1, 1, 2), + fill(model, 1, 1, 2), + fill(model, 1, 1, 2), + fill(model, 1, 1, 2) + ) + mean_R = mean(resistance_samples; dims = 4)[:, :, :, 1] + mean_L = mean(inductance_samples; dims = 4)[:, :, :, 1] + mean_C = mean(capacitance_samples; dims = 4)[:, :, :, 1] + mean_G = mean(conductance_samples; dims = 4)[:, :, :, 1] + omega = reshape(2π .* frequency, 1, 1, :) + surrogate_parameters = LineParameters( + complex.(measurement.(mean_R, 0.0), measurement.(mean_L .* omega, 0.0)), + complex.(measurement.(mean_G, 0.0), measurement.(mean_C .* omega, 0.0)), + frequency; + basis = :total + ) + line_result = LineParametersMC( + line_statistics, + line_samples, + line_distributions, + surrogate_parameters, + sample_count, + 0.90 + ) + + @test basis(line_result) === :total + @test domain(line_result) === PhaseDomain + @test frequencies(line_result) == frequency + @test nconductors(line_result) == 1 + @test nfrequencies(line_result) == 2 + @test statistics(line_result, :R, 1, 1, 2) == line_statistics.R[1, 1, 2] + @test mean(line_result, :L, 1, 1, :) == getproperty.(line_statistics.L[1, 1, :], :mean) + @test samples(line_result, :C, 1, 1, 2) == capacitance_samples[1, 1, 2, :] + @test distribution(line_result, :G, 1, 1, 1) === model + @test quantile(line_result, :R, 0.95, 1, 1, 1) == line_statistics.R[1, 1, 1].q95 + @test quantile(line_result, :R, 0.25, 1, 1, 1) == + quantile(resistance_samples[1, 1, 1, :], 0.25) + + retained_trial = @inferred trial(line_result, 4) + @test basis(retained_trial) === :total + @test domain(retained_trial) === domain(line_result) + @test frequencies(retained_trial) == frequency + @test R(retained_trial) == resistance_samples[:, :, :, 4] + @test L(retained_trial) ≈ inductance_samples[:, :, :, 4] + @test C(retained_trial) ≈ capacitance_samples[:, :, :, 4] + @test G(retained_trial) == conductance_samples[:, :, :, 4] + @test rand(MersenneTwister(9), line_result) isa LineParameters + + distribution_only = LineParametersMC( + line_statistics, + nothing, + line_distributions, + surrogate_parameters, + sample_count, + 0.90 + ) + @test quantile(distribution_only, :R, 0.25, 1, 1, 1) == 2.0 + @test_throws ArgumentError trial(distribution_only, 1) + @test_throws ArgumentError statistics(line_result, :Z) + @test_throws ArgumentError samples(line_result, :Z) + @test_throws ArgumentError distribution(line_result, :Z) + @test_throws ArgumentError LineParametersMC( + RLCG(zeros(1, 1, 2), zeros(1, 1, 2), zeros(1, 1, 2), zeros(1, 1, 2)), + nothing, + nothing, + surrogate_parameters, + sample_count, + 0.90 + ) +end + +@testitem "PlotBuilder: result RenderSpec semantics" setup = [defaults] begin + frequency = [50.0, 500.0, 900.0] + 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)) + omega = reshape(2π .* frequency, 1, 1, :) + parameters = LineParameters( + complex.(resistance_values, inductance_values .* omega), + complex.(conductance_values, capacitance_values .* omega), + frequency + ) + + render = LineCableModels.PlotBuilder.make_render( + LineCableModels.Engine.LineParameterPlotSpec, + parameters; + mode = :RLCG, + coord = :cart, + con = (1:2, 1:2) + ) + @test render.spec === LineCableModels.Engine.LineParameterPlotSpec + @test length(render.figures) == 4 + @test getproperty.(only.(getproperty.(render.figures, :views)), :title) == [ + "Series resistance", + "Series inductance", + "Shunt conductance", + "Shunt capacitance" + ] + @test all(page -> page.layout === :single, render.figures) + @test all(page -> only(page.views).xaxis.label == "Frequency [Hz]", render.figures) + @test only(render.figures[1].views).yaxis.label == "Series resistance [Ω/km]" + @test only(render.figures[2].views).yaxis.label == "Series inductance [mH/km]" + @test length(only(render.figures[1].views).series) == 4 + @test all(series -> series.kind === :line, only(render.figures[1].views).series) + @test first(only(render.figures[1].views).series).label == "R[1,1]" + @test render.figures[1].kwargs.controls == + LineCableModels.PlotBuilder.control_definitions() + @test render.figures[1].kwargs.configuration.mode === :RLCG + @test render.figures[1].kwargs.configuration.coord === :cart + @test render.figures[1].kwargs.configuration.length_unit === :kilo + @test render.figures[1].kwargs.configuration.conductors == (1:2, 1:2) + + polar = LineCableModels.PlotBuilder.make_render( + LineCableModels.Engine.LineParameterPlotSpec, + parameters; + mode = :ZY, + coord = :polar + ) + @test length(polar.figures) == 4 + @test occursin("impedance magnitude", lowercase(only(polar.figures[1].views).yaxis.label)) + @test occursin("angle", lowercase(only(polar.figures[2].views).yaxis.label)) + + summary = SampleSummary([1.0, 2.0, 3.0, 4.0]) + model = HistogramPDF([1.0, 3.0, 5.0], [0.25, 0.25]) + mc_result = CableConstantsMC( + CableConstants(summary, summary, summary), + CableConstants([1.0, 2.0, 3.0, 4.0], [1.0, 2.0, 3.0, 4.0], [1.0, 2.0, 3.0, 4.0]), + CableConstants(model, model, model), + CableConstants(2.5, 2.5, 2.5), + 4, + 0.95 + ) + for mode in (:hist, :pdf, :ecdf, :qq) + mc_render = LineCableModels.PlotBuilder.make_render( + LineCableModels.UQ.MCDistributionPlotSpec, + mc_result; + quantity = :R, + mode, + data = :both + ) + @test length(mc_render.figures) == 1 + @test only(mc_render.figures).kwargs.mode === mode + @test only(mc_render.figures).kwargs.controls.export_svg + @test only(mc_render.figures).kwargs.configuration.mode === mode + @test !isempty(only(only(mc_render.figures).views).series) + end +end diff --git a/test/unit_ParametricBuilder/test_uq_trial_sampler.jl b/test/unit_ParametricBuilder/test_uq_trial_sampler.jl index ff3716e5..28689bcb 100644 --- a/test/unit_ParametricBuilder/test_uq_trial_sampler.jl +++ b/test/unit_ParametricBuilder/test_uq_trial_sampler.jl @@ -47,7 +47,7 @@ @test length(shared_draws) == 8 @test result.samples !== nothing - @test result.stats.R.n == 8 + @test ntrials(result) == 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 From 090f1d71d7bc8ff5ccdbfe83359c85513709bada Mon Sep 17 00:00:00 2001 From: amaurigmartins Date: Fri, 14 Aug 2026 06:53:21 +0200 Subject: [PATCH 003/157] docs(results): document result and plotting migration --- CHANGELOG.md | 13 +++++++++++++ README.md | 25 ++++++++++++++++++++++++- docs/src/CHANGELOG.md | 13 +++++++++++++ docs/src/reference.md | 34 ++++++++++++++++++++++++++++++++++ examples/tutorial3.jl | 19 +++++++++++++++---- 5 files changed, 99 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba8a8c88..d7ac4a1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ and versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Aqua, SciML formatting, gitlint, clean-install, and modular documentation checks. - Citation and contribution metadata. +- Type-stable `CableConstants`, `SampleSummary`, `RLCG`, `CableConstantsMC`, + `LineParametersMC`, and `HistogramPDF` result containers. +- A single declarative PlotBuilder renderer with interactive legends and + one-click, non-overwriting SVG export. ### Changed @@ -28,6 +32,10 @@ and versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Plotting requires the caller to load CairoMakie, GLMakie, or WGLMakie explicitly. - Documentation examples and development conventions were consolidated. +- Line-parameter results now carry an explicit `:per_length` or `:total` basis, + and use `Z`, `Y`, `R`, `X`, `L`, `G`, `B`, and `C` accessors consistently. +- `preview` and statistical plots return one `UIPlot`; line-parameter plots + return `Vector{UIPlot}`. ### Removed @@ -37,6 +45,8 @@ and versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). their tests remain. - The hard dependency on the external, unregistered GetDP.jl package. - The obsolete TODO scraper and duplicate tag-release workflow. +- The old `ResultsView`, `CableDesignMC`, `LineParametersPDF`, `plotmetadata`, + duplicated plotting UIs, and direct Makie renderers. ### Migration @@ -48,6 +58,9 @@ 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 + using Gmsh using LineCableModels.Engine.FEM formulation = FormulationSet(:FEM) diff --git a/README.md b/README.md index e950728e..b08aaae3 100644 --- a/README.md +++ b/README.md @@ -49,10 +49,33 @@ using LineCableModels using CairoMakie set_backend!(:cairo) + +plots = plot(line_parameters) # Vector{UIPlot}, one page per quantity +export_svg(first(plots); path = "series_resistance.svg") ``` `GLMakie` and `WGLMakie` are supported in the same way. LineCableModels -never imports or selects a backend dynamically. +never imports or selects a backend dynamically. `preview` and Monte Carlo +distribution plots return one `UIPlot`; line-parameter plots always return a +`Vector{UIPlot}`. The Export SVG control preserves the current declarative plot +state and requires CairoMakie to have been loaded explicitly. + +## Result access + +`CableConstants` stores canonical per-metre R/L/C values. `LineParameters` +stores its frequency domain and either a `:per_length` or `:total` basis: + +```julia +basis(line_parameters) +R(line_parameters, 1, 1) # complete frequency response +Z(line_parameters, 1, 1, 2:5) # selected frequency samples +abs.(Z(line_parameters, 1, 1)) +``` + +Monte Carlo results use first-class `CableConstantsMC` and `LineParametersMC` +containers. Use `statistics`, `mean`, `std`, `quantile`, `samples`, `trial`, +`distribution`, and `surrogate`; joint `trial`/`rand` calls require retained +samples. ## Transitional FEM integration diff --git a/docs/src/CHANGELOG.md b/docs/src/CHANGELOG.md index cf322a20..bdd0a140 100644 --- a/docs/src/CHANGELOG.md +++ b/docs/src/CHANGELOG.md @@ -20,6 +20,10 @@ and versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Aqua, SciML formatting, gitlint, clean-install, and modular documentation checks. - Citation and contribution metadata. +- Type-stable `CableConstants`, `SampleSummary`, `RLCG`, `CableConstantsMC`, + `LineParametersMC`, and `HistogramPDF` result containers. +- A single declarative PlotBuilder renderer with interactive legends and + one-click, non-overwriting SVG export. ### Changed @@ -32,6 +36,10 @@ and versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Plotting requires the caller to load CairoMakie, GLMakie, or WGLMakie explicitly. - Documentation examples and development conventions were consolidated. +- Line-parameter results now carry an explicit `:per_length` or `:total` basis, + and use `Z`, `Y`, `R`, `X`, `L`, `G`, `B`, and `C` accessors consistently. +- `preview` and statistical plots return one `UIPlot`; line-parameter plots + return `Vector{UIPlot}`. ### Removed @@ -41,6 +49,8 @@ and versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). their tests remain. - The hard dependency on the external, unregistered GetDP.jl package. - The obsolete TODO scraper and duplicate tag-release workflow. +- The old `ResultsView`, `CableDesignMC`, `LineParametersPDF`, `plotmetadata`, + duplicated plotting UIs, and direct Makie renderers. ### Migration @@ -52,6 +62,9 @@ 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 + using Gmsh using LineCableModels.Engine.FEM formulation = FormulationSet(:FEM) diff --git a/docs/src/reference.md b/docs/src/reference.md index 0e662382..f3739340 100644 --- a/docs/src/reference.md +++ b/docs/src/reference.md @@ -3,6 +3,40 @@ This page documents the public API and the documented implementation surface of `LineCableModels.jl`. +## Result containers + +`CableConstants` stores canonical per-metre R/L/C values. `LineParameters` +stores frequency-dependent Z/Y matrices together with their domain and either a +`:per_length` or `:total` basis. Selecting matrix indices without a frequency +index returns the complete frequency response. + +```jldoctest +julia> using LineCableModels + +julia> f = [50.0, 100.0]; + +julia> z = reshape(ComplexF64[1 + 2im, 3 + 4im], 1, 1, 2); + +julia> y = reshape(ComplexF64[5 + 6im, 7 + 8im], 1, 1, 2); + +julia> parameters = LineParameters(z, y, f); + +julia> basis(parameters) +:per_length + +julia> R(parameters, 1, 1) == [1.0, 3.0] +true + +julia> Z(parameters, 1, 1, 2) +3.0 + 4.0im +``` + +Monte Carlo calculations return `CableConstantsMC` or `LineParametersMC`. +Use `statistics`, `mean`, `std`, and `quantile` for summaries; `samples` and +`trial` for retained joint trials; `distribution` for retained marginal +histograms; and `surrogate` for the covariance-preserving Measurements.jl +representation. `trial` and `rand` deliberately require retained joint samples. + ## Contents ```@contents diff --git a/examples/tutorial3.jl b/examples/tutorial3.jl index bf4a945c..b970d6ee 100644 --- a/examples/tutorial3.jl +++ b/examples/tutorial3.jl @@ -355,8 +355,10 @@ F = FormulationSet(:FEM, # Run the FEM solver @time ws, p = compute!(problem, F); -# Display computation results -per_km(p, 1; mode = :RLCG, tol = 1e-9) +# Display computation results in per-kilometre units +series_rl, shunt_gc = DataFrame(p; mode = :RLCG, length_unit = :kilo, tol = 1e-9) +series_rl[1, 1] +shunt_gc[1, 1] # Export ZY matrices to ATPDraw output_file = fullfile("ZY_export.xml") @@ -366,7 +368,16 @@ export_file = export_data(:atp, p; file_name = output_file, cable_system = cable Tv, p012 = Fortescue(tol = 1e-5)(p); # Inspect the transformed matrices -per_km(p012, 1; mode = :ZY, tol = 1e-9) +series_zy, shunt_zy = DataFrame(p012; mode = :ZY, length_unit = :kilo, tol = 1e-9) +series_zy[1, 1] +shunt_zy[1, 1] # Or the corresponding lumped circuit quantities -per_km(p012, 1; mode = :RLCG, tol = 1e-9) +series_rl012, shunt_gc012 = DataFrame( + p012; + mode = :RLCG, + length_unit = :kilo, + tol = 1e-9 +) +series_rl012[1, 1] +shunt_gc012[1, 1] From 969ee73e12985be9852a6bf825d0cb72167ccb1a Mon Sep 17 00:00:00 2001 From: amaurigmartins Date: Fri, 14 Aug 2026 06:53:41 +0200 Subject: [PATCH 004/157] ci(plotting): add manual gl visual gate --- .github/workflows/plotting-manual.yml | 38 +++++++++++++++ integration/plotting/manual_gl.jl | 67 +++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 .github/workflows/plotting-manual.yml create mode 100644 integration/plotting/manual_gl.jl diff --git a/.github/workflows/plotting-manual.yml b/.github/workflows/plotting-manual.yml new file mode 100644 index 00000000..11192a95 --- /dev/null +++ b/.github/workflows/plotting-manual.yml @@ -0,0 +1,38 @@ +name: Manual GL plotting gate + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + gl-plotting: + name: GLMakie UI and Cairo SVG export + runs-on: ubuntu-latest + timeout-minutes: 45 + env: + LINECABLEMODELS_GL_ARTIFACTS: manual-gl-artifacts + + steps: + - uses: actions/checkout@v6 + - uses: julia-actions/setup-julia@v3 + with: + version: "1.12" + - uses: julia-actions/cache@v3 + - name: Install explicit plotting backends + shell: julia --startup-file=no --color=yes {0} + run: | + using Pkg + Pkg.activate(joinpath(pwd(), ".manual-gl-env")) + Pkg.develop(PackageSpec(path = pwd())) + Pkg.add(PackageSpec(name = "GLMakie", version = "0.13")) + Pkg.add(PackageSpec(name = "CairoMakie", version = "0.15")) + - name: Exercise the GL UI and one-click SVG export + run: xvfb-run --auto-servernum julia --startup-file=no --project=.manual-gl-env integration/plotting/manual_gl.jl + - name: Upload visual evidence + uses: actions/upload-artifact@v4 + with: + name: linecablemodels-gl-plotting + path: manual-gl-artifacts + if-no-files-found: error diff --git a/integration/plotting/manual_gl.jl b/integration/plotting/manual_gl.jl new file mode 100644 index 00000000..8422a7d1 --- /dev/null +++ b/integration/plotting/manual_gl.jl @@ -0,0 +1,67 @@ +using Test +using LineCableModels +using GLMakie +using CairoMakie + +const ARTIFACT_DIRECTORY = abspath(get( + ENV, + "LINECABLEMODELS_GL_ARTIFACTS", + joinpath(pwd(), "manual-gl-artifacts") +)) +mkpath(ARTIFACT_DIRECTORY) + +set_backend!(:gl) + +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; mode = :RLCG, backend = :gl, display_plot = true) +handle = first(plots) + +@testset "manual GL plotting gate" begin + @test plots isa Vector{UIPlot} + @test length(plots) == 4 + @test sort!(collect(keys(handle.controls))) == + [:export_svg, :legend, :reset, :xlog, :ylog] + @test handle.context.backend === :gl + + handle.controls[:xlog].active[] = true + handle.controls[:ylog].active[] = true + @test only(handle.panels).axis.xscale[] === Makie.log10 + @test only(handle.panels).axis.yscale[] === Makie.log10 + + legend = handle.controls[:legend] + entry = first(last(first(legend.entrygroups[]))) + Makie.toggle_visibility!(entry) + @test any(plot_object -> !plot_object.visible[], only(handle.panels).plots) + Makie.toggle_visibility!(entry) + + handle.controls[:reset].clicks[] += 1 + @test handle.context.status[] == "Axis limits reset" + + GLMakie.save(joinpath(ARTIFACT_DIRECTORY, "gl-ui.png"), handle.figure) + cd(ARTIFACT_DIRECTORY) do + before = Set(readdir()) + handle.controls[:export_svg].clicks[] += 1 + after = Set(readdir()) + created = filter(name -> endswith(name, ".svg"), collect(setdiff(after, before))) + @test length(created) == 1 + svg_path = joinpath(ARTIFACT_DIRECTORY, only(created)) + @test filesize(svg_path) > 100 + @test occursin(" Date: Fri, 14 Aug 2026 08:15:50 +0200 Subject: [PATCH 005/157] fix(results): close parity and plotting contract gaps --- integration/plotting/manual_gl.jl | 12 +- src/datamodel/cabledesign/cableconstants.jl | 26 +- src/datamodel/plotspecs.jl | 7 + src/engine/dataframe.jl | 29 +- src/engine/lineparams.jl | 16 + src/engine/plotspecs.jl | 100 ++---- src/engine/transforms/eiglevenberg.jl | 8 +- src/plotbuilder/uicomponents/UIComponents.jl | 7 +- src/unithandler/UnitHandler.jl | 142 +++++++- src/uq/distributions.jl | 20 +- src/uq/montecarlo.jl | 18 +- src/uq/plotspecs.jl | 35 +- src/uq/types.jl | 65 +++- test/plotting.jl | 167 ++++++++- test/reference/generate.jl | 6 +- test/reference/pre_refactor/README.md | 18 + test/reference/pre_refactor/cable_preview.png | Bin 0 -> 425114 bytes test/reference/pre_refactor/line_rlcg.png | Bin 0 -> 47531 bytes .../pre_refactor/line_zy_cartesian.png | Bin 0 -> 51787 bytes test/reference/pre_refactor/line_zy_polar.png | Bin 0 -> 87770 bytes test/reference/result_fixtures.jl | 57 +++ test/unit_DataModel/test_plot_specs.jl | 99 ++++++ .../test_parallel_rc_insulation.jl | 21 ++ test/unit_Engine/test_result_containers.jl | 334 +++++++++++++++++- 24 files changed, 1011 insertions(+), 176 deletions(-) create mode 100644 test/reference/pre_refactor/README.md create mode 100644 test/reference/pre_refactor/cable_preview.png create mode 100644 test/reference/pre_refactor/line_rlcg.png create mode 100644 test/reference/pre_refactor/line_zy_cartesian.png create mode 100644 test/reference/pre_refactor/line_zy_polar.png create mode 100644 test/reference/result_fixtures.jl create mode 100644 test/unit_DataModel/test_plot_specs.jl diff --git a/integration/plotting/manual_gl.jl b/integration/plotting/manual_gl.jl index 8422a7d1..4a79fa5a 100644 --- a/integration/plotting/manual_gl.jl +++ b/integration/plotting/manual_gl.jl @@ -1,7 +1,6 @@ using Test using LineCableModels using GLMakie -using CairoMakie const ARTIFACT_DIRECTORY = abspath(get( ENV, @@ -28,6 +27,17 @@ parameters = LineParameters( plots = Makie.plot(parameters; mode = :RLCG, backend = :gl, display_plot = true) handle = first(plots) +@testset "manual GL plotting gate without CairoMakie" begin + @test !LineCableModels.PlotBuilder.BackendHandler.backend_available(:cairo) + handle.controls[:export_svg].clicks[] += 1 + @test occursin("load CairoMakie", handle.context.status[]) + @test Base.get_extension(LineCableModels, :LineCableModelsCairoMakieExt) === nothing + @test LineCableModels.PlotBuilder.BackendHandler.current_backend_symbol() === :gl +end + +using CairoMakie +set_backend!(:gl) + @testset "manual GL plotting gate" begin @test plots isa Vector{UIPlot} @test length(plots) == 4 diff --git a/src/datamodel/cabledesign/cableconstants.jl b/src/datamodel/cabledesign/cableconstants.jl index fe6f215c..5df87047 100644 --- a/src/datamodel/cabledesign/cableconstants.jl +++ b/src/datamodel/cabledesign/cableconstants.jl @@ -48,10 +48,10 @@ inductance, and coaxial-capacitance expressions historically used by not their physical calculation. """ function CableConstants( - design::CableDesign; + design::CableDesign{T}; S::Union{Nothing, Number} = nothing, rho_e::Number = 100.0 -) +) where {T} length(design.components) >= 2 || throw( ArgumentError("at least two cable components are required"), ) @@ -77,17 +77,19 @@ function CableConstants( 20.0, 20.0 ) + inductance_type = promote_type(T, typeof(separation), typeof(rho_e)) inductance_value = 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, - separation; - rho_e = rho_e + coerce_to_T(cable_core.conductor_group.r_in, inductance_type), + coerce_to_T(cable_core.conductor_group.r_ex, inductance_type), + coerce_to_T(cable_core.conductor_props.rho, inductance_type), + coerce_to_T(cable_core.conductor_props.mu_r, inductance_type), + coerce_to_T(cable_shield.conductor_group.r_in, inductance_type), + coerce_to_T(cable_shield.conductor_group.r_ex, inductance_type), + coerce_to_T(cable_shield.conductor_props.rho, inductance_type), + coerce_to_T(cable_shield.conductor_props.mu_r, inductance_type), + coerce_to_T(separation, inductance_type), + coerce_to_T(rho_e, inductance_type), + coerce_to_T(f₀, inductance_type) ) capacitance_value = calc_shunt_capacitance( cable_core.conductor_group.r_ex, diff --git a/src/datamodel/plotspecs.jl b/src/datamodel/plotspecs.jl index 050c028e..55c48eb4 100644 --- a/src/datamodel/plotspecs.jl +++ b/src/datamodel/plotspecs.jl @@ -403,6 +403,13 @@ function PlotBuilder.make_render( end function _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[to_nominal(cable.horz) for cable in system.cables] vertical = Float64[to_nominal(cable.vert) for cable in system.cables] radii = Float64[max( diff --git a/src/engine/dataframe.jl b/src/engine/dataframe.jl index 6920f9f9..3ca946a4 100644 --- a/src/engine/dataframe.jl +++ b/src/engine/dataframe.jl @@ -14,20 +14,6 @@ function _frequency_vector(object, provided) return values end -function _dataframe_components(object, mode::Symbol, coord::Symbol) - mode in (:RLCG, :ZY) || throw(ArgumentError("mode must be :RLCG or :ZY")) - coord in (:cart, :polar) || throw(ArgumentError("coord must be :cart or :polar")) - if mode === :RLCG - object isa SeriesImpedance && return (:R, :L) - object isa ShuntAdmittance && return (:G, :C) - elseif object isa SeriesImpedance - return coord === :cart ? (:Z_re, :Z_im) : (:Z_abs, :Z_angle) - elseif object isa ShuntAdmittance - return coord === :cart ? (:Y_re, :Y_im) : (:Y_abs, :Y_angle) - end - throw(ArgumentError("unsupported line-parameter object $(typeof(object))")) -end - const _DATAFRAME_COLUMN = Dict( :R => :R, :L => :L, @@ -84,9 +70,17 @@ function _matrix_dataframes( frequency_target ) displayed_frequency = frequency_values .* frequency_factor - component_names = _dataframe_components(object, mode, coord) + component_names = UnitHandler.line_components( + _line_parameter_kind(object), + mode, + coord + ) component_arrays = Dict( - component => _component_values(component, object, frequency_values) + component => UnitHandler.line_component_values( + component, + object.values, + frequency_values + ) for component in component_names ) @@ -136,6 +130,9 @@ function DataFrame( quantity_units = nothing, tol::Real = sqrt(eps(Float64)) ) + isfinite(tol) && tol >= 0 || throw( + ArgumentError("tol must be finite and nonnegative"), + ) frequency_values = _frequency_vector(parameters, freqs) return _matrix_dataframes( parameters, diff --git a/src/engine/lineparams.jl b/src/engine/lineparams.jl index 37c5128b..a9a75065 100644 --- a/src/engine/lineparams.jl +++ b/src/engine/lineparams.jl @@ -16,6 +16,14 @@ when `Basis` is `:per_length` 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 :per_length or :total; got $(repr(Basis))"), + ) + _check_basis(Basis) + return new{T, Basis}(values) + end end """ @@ -27,6 +35,14 @@ when `Basis` is `:per_length` 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 :per_length or :total; got $(repr(Basis))"), + ) + _check_basis(Basis) + return new{T, Basis}(values) + end end function SeriesImpedance(A::AbstractArray{T, 3}; basis::Symbol = :per_length) where {T} diff --git a/src/engine/plotspecs.jl b/src/engine/plotspecs.jl index 67865a51..e9dec480 100644 --- a/src/engine/plotspecs.jl +++ b/src/engine/plotspecs.jl @@ -1,52 +1,18 @@ struct LineParameterPlotSpec <: PlotBuilder.AbstractPlotSpec end -const _PLOT_QUANTITY = Dict( - :R => (:resistance, :ohm, :base), - :X => (:reactance, :ohm, :base), - :L => (:inductance, :henry, :milli), - :G => (:conductance, :siemens, :base), - :B => (:susceptance, :siemens, :base), - :C => (:capacitance, :farad, :micro), - :Z_re => (:resistance, :ohm, :base), - :Z_im => (:reactance, :ohm, :base), - :Z_abs => ((:impedance, :abs), :ohm, :base), - :Z_angle => ((:impedance, :angle), :degree, :base), - :Y_re => (:conductance, :siemens, :base), - :Y_im => (:susceptance, :siemens, :base), - :Y_abs => ((:admittance, :abs), :siemens, :base), - :Y_angle => ((:admittance, :angle), :degree, :base) -) - -function _quantity_prefix(quantity_units, component::Symbol, semantic, fallback::Symbol) - quantity_units === nothing && return fallback - quantity_units isa Symbol && return quantity_units - if quantity_units isa NamedTuple || quantity_units isa AbstractDict - haskey(quantity_units, component) && return quantity_units[component] - semantic isa Symbol && haskey(quantity_units, semantic) && - return quantity_units[semantic] - return fallback - end - throw(ArgumentError("quantity_units must be a Symbol, NamedTuple, dictionary, or nothing")) -end - function _component_unit( component::Symbol, parameter_basis::Symbol, length_unit::Symbol, quantity_units ) - semantic, unit_name, fallback_prefix = _PLOT_QUANTITY[component] - tag = UnitHandler.QuantityTag{semantic}() - prefix = _quantity_prefix(quantity_units, component, semantic, fallback_prefix) - native = UnitHandler.default_unit(tag, parameter_basis) - target = if unit_name === :degree - UnitHandler.units(prefix, unit_name) - elseif parameter_basis === :per_length - UnitHandler.units(prefix, unit_name; per = (length_unit, :meter)) - else - UnitHandler.units(prefix, unit_name) - end - return tag, target, UnitHandler.scale_factor(native, target) + resolved = UnitHandler.line_component_unit( + component, + parameter_basis; + length_unit, + quantity_units + ) + return resolved.quantity, resolved.units, resolved.scale end function _indices(selector, count::Int) @@ -60,6 +26,11 @@ end function _conductor_pairs(object, selector) row_count, column_count, _ = size(object) + selector === nothing || + (selector isa Tuple && length(selector) == 2) || + throw( + ArgumentError("conductor selection must be a tuple (rows, columns) or nothing"), + ) row_selector, column_selector = selector === nothing ? (nothing, nothing) : selector rows = _indices(row_selector, row_count) columns = _indices(column_selector, column_count) @@ -68,32 +39,8 @@ function _conductor_pairs(object, selector) return [(i, j) for i in rows for j in columns] end -function _components(object, mode::Symbol, coord::Symbol) - mode in (:ZY, :RLCG) || throw(ArgumentError("mode must be :ZY or :RLCG")) - coord in (:cart, :polar) || throw(ArgumentError("coord must be :cart or :polar")) - if mode === :RLCG - object isa SeriesImpedance && return (:R, :L) - object isa ShuntAdmittance && return (:G, :C) - elseif object isa SeriesImpedance - return coord === :cart ? (:Z_re, :Z_im) : (:Z_abs, :Z_angle) - elseif object isa ShuntAdmittance - return coord === :cart ? (:Y_re, :Y_im) : (:Y_abs, :Y_angle) - end - throw(ArgumentError("unsupported line-parameter plot object $(typeof(object))")) -end - -function _component_values(component::Symbol, object, frequency_values) - values = object.values - component in (:R, :Z_re) && return real.(values) - component in (:X, :Z_im) && return imag.(values) - component === :L && return imag.(values) ./ reshape(2π .* frequency_values, 1, 1, :) - component in (:G, :Y_re) && return real.(values) - component in (:B, :Y_im) && return imag.(values) - component === :C && return imag.(values) ./ reshape(2π .* frequency_values, 1, 1, :) - component in (:Z_abs, :Y_abs) && return abs.(values) - component in (:Z_angle, :Y_angle) && return angle.(values) .* (180 / π) - throw(ArgumentError("unsupported line-parameter component :$component")) -end +_line_parameter_kind(::SeriesImpedance) = :series +_line_parameter_kind(::ShuntAdmittance) = :shunt function _finite_exponent(curves) maximum_value = 0.0 @@ -127,7 +74,11 @@ function _line_pages( xscale::Symbol, yscale::Symbol ) - length(frequency_values) > 1 || return PlotBuilder.PageSpec[] + all(isfinite, frequency_values) || throw(ArgumentError("frequencies must be finite")) + if length(frequency_values) <= 1 + @warn "Frequency vector has $(length(frequency_values)) sample(s); nothing to plot." + return PlotBuilder.PageSpec[] + end size(object, 3) == length(frequency_values) || throw( DimensionMismatch("frequency count does not match line-parameter samples"), ) @@ -137,6 +88,10 @@ function _line_pages( ) xscale in (:linear, :log10) || throw(ArgumentError("xscale must be :linear or :log10")) yscale in (:linear, :log10) || throw(ArgumentError("yscale must be :linear or :log10")) + xscale === :log10 && any(<=(0), frequency_values) && + throw( + DomainError(frequency_values, "logarithmic frequency axes require positive frequencies"), + ) frequency_quantity = UnitHandler.QuantityTag{:freq}() frequency_target = UnitHandler.units(freq_unit, :hertz) @@ -149,14 +104,19 @@ function _line_pages( pairs = _conductor_pairs(object, con) pages = PlotBuilder.PageSpec[] - for component in _components(object, mode, coord) + components = UnitHandler.line_components(_line_parameter_kind(object), mode, coord) + for component in components quantity, target_unit, conversion = _component_unit( component, basis(object), length_unit, quantity_units ) - values = _component_values(component, object, frequency_values) + values = UnitHandler.line_component_values( + component, + object.values, + frequency_values + ) curves = [collect(view(values, i, j, :)) .* conversion for (i, j) in pairs] active = [index for index in eachindex(curves) diff --git a/src/engine/transforms/eiglevenberg.jl b/src/engine/transforms/eiglevenberg.jl index 068aaf85..5273ef08 100644 --- a/src/engine/transforms/eiglevenberg.jl +++ b/src/engine/transforms/eiglevenberg.jl @@ -17,7 +17,7 @@ $(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). +**modal impedance/admittance** (diagonal per frequency) with the input basis. # Arguments @@ -27,9 +27,9 @@ transformation matrices and a **modal-domain** `LineParameters` holding the # 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\\]. +- `LineParameters`: Modal-domain parameters with the same basis as `lp`: + - Series impedance `Zm` (diagonal per frequency) \\[Ω/m\\] or \\[Ω\\]. + - Shunt admittance `Ym` (diagonal per frequency) \\[S/m\\] or \\[S\\]. """ function (f::Levenberg)( diff --git a/src/plotbuilder/uicomponents/UIComponents.jl b/src/plotbuilder/uicomponents/UIComponents.jl index fd19836d..82a68595 100644 --- a/src/plotbuilder/uicomponents/UIComponents.jl +++ b/src/plotbuilder/uicomponents/UIComponents.jl @@ -449,9 +449,14 @@ end function PlotBuilder.export_svg(plot::UIPlot; path::Union{Nothing, AbstractString} = nothing) BackendHandler.backend_available(:cairo) || throw( - ArgumentError("SVG export requires an explicitly loaded CairoMakie; run `using CairoMakie`"), + ArgumentError( + "SVG export requires CairoMakie; load CairoMakie first with `using CairoMakie`", + ), ) output = path === nothing ? _available_path(plot.page) : abspath(String(path)) + 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")) plot.context.status[] = "Exporting SVG..." BackendHandler.with_backend(:cairo) do diff --git a/src/unithandler/UnitHandler.jl b/src/unithandler/UnitHandler.jl index 00355abf..45c79f1a 100644 --- a/src/unithandler/UnitHandler.jl +++ b/src/unithandler/UnitHandler.jl @@ -70,8 +70,17 @@ const UNIT_SYMBOL = Dict( # 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)) +@inline function _prefix_exp(prefix::Symbol) + return get(METRIC_PREFIX_EXPONENT, prefix) do + throw(ArgumentError("unsupported metric prefix :$prefix")) + end +end + +@inline function _prefix_symbol(prefix::Symbol) + return get(METRIC_PREFIX_SYMBOL, prefix) do + throw(ArgumentError("unsupported metric prefix :$prefix")) + end +end # -------------------------------------------------------------------------- # Core types @@ -193,6 +202,135 @@ struct QuantityTag{Q} end QuantityTag(::Val{Q}) where {Q} = QuantityTag{Q}() QuantityTag(::Type{QuantityTag{Q}}) where {Q} = QuantityTag{Q}() +const _LINE_COMPONENT_QUANTITY = Dict( + :R => (:resistance, :ohm, :base), + :X => (:reactance, :ohm, :base), + :L => (:inductance, :henry, :milli), + :G => (:conductance, :siemens, :base), + :B => (:susceptance, :siemens, :base), + :C => (:capacitance, :farad, :micro), + :Z_re => (:resistance, :ohm, :base), + :Z_im => (:reactance, :ohm, :base), + :Z_abs => ((:impedance, :abs), :ohm, :base), + :Z_angle => ((:impedance, :angle), :degree, :base), + :Y_re => (:conductance, :siemens, :base), + :Y_im => (:susceptance, :siemens, :base), + :Y_abs => ((:admittance, :abs), :siemens, :base), + :Y_angle => ((:admittance, :angle), :degree, :base) +) + +""" + line_components(kind, mode, coordinate) + +Return the physical components selected for a series (`kind=:series`) or +shunt (`kind=:shunt`) quantity. `mode` is `:RLCG` or `:ZY`; `coordinate` is +`:cart` or `:polar`. +""" +function line_components(kind::Symbol, mode::Symbol, coordinate::Symbol) + kind in (:series, :shunt) || + throw(ArgumentError("kind must be :series or :shunt")) + mode in (:RLCG, :ZY) || throw(ArgumentError("mode must be :RLCG or :ZY")) + coordinate in (:cart, :polar) || + throw(ArgumentError("coordinate must be :cart or :polar")) + if mode === :RLCG + return kind === :series ? (:R, :L) : (:G, :C) + elseif kind === :series + return coordinate === :cart ? (:Z_re, :Z_im) : (:Z_abs, :Z_angle) + else + return coordinate === :cart ? (:Y_re, :Y_im) : (:Y_abs, :Y_angle) + end +end + +""" + line_component_quantity(component) + +Return the semantic name and quantity tag, physical unit name, and default +display prefix for a line-parameter component. +""" +function line_component_quantity(component::Symbol) + semantic, unit_name, prefix = get(_LINE_COMPONENT_QUANTITY, component) do + throw(ArgumentError("unsupported line-parameter component :$component")) + end + return (; + semantic, + tag = QuantityTag{semantic}(), + unit_name, + prefix + ) +end + +function _line_quantity_prefix(quantity_units, component, semantic, fallback) + quantity_units === nothing && return fallback + quantity_units isa Symbol && return quantity_units + if quantity_units isa NamedTuple || quantity_units isa AbstractDict + haskey(quantity_units, component) && return quantity_units[component] + semantic isa Symbol && haskey(quantity_units, semantic) && + return quantity_units[semantic] + return fallback + end + throw(ArgumentError("quantity_units must be a Symbol, NamedTuple, dictionary, or nothing")) +end + +""" + line_component_unit(component, basis; length_unit=:kilo, quantity_units=nothing) + +Resolve the display unit and numeric scale factor for a line-parameter +component. `length_unit` is the metric prefix of the denominator for +per-length quantities. `quantity_units` may override numerator prefixes with a +single symbol or with component/semantic keys in a named tuple or dictionary. +""" +function line_component_unit( + component::Symbol, + basis::Symbol; + length_unit::Symbol = :kilo, + quantity_units = nothing +) + quantity = line_component_quantity(component) + prefix = _line_quantity_prefix( + quantity_units, + component, + quantity.semantic, + quantity.prefix + ) + prefix isa Symbol || throw( + ArgumentError("quantity unit prefixes must be symbols"), + ) + target = if quantity.unit_name === :degree + units(prefix, quantity.unit_name) + elseif basis === :per_length + units(prefix, quantity.unit_name; per = (length_unit, :meter)) + else + units(prefix, quantity.unit_name) + end + return (; + quantity = quantity.tag, + units = target, + scale = scale_factor(quantity.tag, basis, target) + ) +end + +""" + line_component_values(component, values, frequencies) + +Extract a Cartesian, polar, or RLCG component from complex line-parameter +values. Inductance and capacitance are undefined at zero frequency. +""" +function line_component_values(component::Symbol, values, frequencies) + component in (:R, :Z_re) && return real.(values) + component in (:X, :Z_im) && return imag.(values) + if component in (:L, :C) + any(iszero, frequencies) && throw( + DomainError(frequencies, "$component is undefined at zero frequency"), + ) + return imag.(values) ./ reshape(2π .* frequencies, 1, 1, :) + end + component in (:G, :Y_re) && return real.(values) + component in (:B, :Y_im) && return imag.(values) + component in (:Z_abs, :Y_abs) && return abs.(values) + component in (:Z_angle, :Y_angle) && return angle.(values) .* (180 / π) + throw(ArgumentError("unsupported line-parameter component :$component")) +end + """ Native (storage/computational) unit for a quantity. diff --git a/src/uq/distributions.jl b/src/uq/distributions.jl index 6ce8cb2a..11baef1c 100644 --- a/src/uq/distributions.jl +++ b/src/uq/distributions.jl @@ -2,26 +2,8 @@ function HistogramPDF( edges::AbstractVector{TE}, density::AbstractVector{TD} ) where {TE <: Real, TD <: Real} - length(edges) == length(density) + 1 || throw( - ArgumentError("edges must contain exactly one more value than density"), - ) - isempty(density) && throw(ArgumentError("density must contain at least one bin")) - all(isfinite, edges) || throw(ArgumentError("histogram edges must be finite")) - all(isfinite, density) || throw(ArgumentError("histogram density must be finite")) - all(>=(zero(TD)), density) || - throw(ArgumentError("histogram density must be nonnegative")) - T = float(promote_type(TE, TD)) - copied_edges = Vector{T}(edges) - copied_density = Vector{T}(density) - widths = diff(copied_edges) - all(>(zero(T)), widths) || throw( - ArgumentError("histogram edges must be strictly increasing"), - ) - area = dot(copied_density, widths) - area > zero(area) || throw(ArgumentError("histogram density must have positive area")) - copied_density ./= area - return HistogramPDF{T}(copied_edges, copied_density) + return HistogramPDF{T}(Vector{T}(edges), Vector{T}(density)) end function _auto_nbins( diff --git a/src/uq/montecarlo.jl b/src/uq/montecarlo.jl index 6cb423d8..ba10fe26 100644 --- a/src/uq/montecarlo.jl +++ b/src/uq/montecarlo.jl @@ -268,6 +268,7 @@ function mc( # Monte Carlo over FULL frequency vector: one LineParameters per trial # ───────────────────────────────────────────────────────────────────────── Dlp = domain(first_lp) + source_basis = basis(first_lp) for i in 1:ntrials if i == 1 @@ -286,18 +287,25 @@ function mc( "mc: inconsistent LineParameters domain across trials" ), ) + basis(lp) === source_basis || throw( + DomainError( + basis(lp), + "mc: inconsistent LineParameters basis 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 + Zscaled, Yscaled = if source_basis === :per_length + per_length ? (lp.Z.values, lp.Y.values) : + (lp.Z.values .* ws.line_length, lp.Y.values .* ws.line_length) else - Zscaled = lp.Z.values .* ws.line_length - Yscaled = lp.Y.values .* ws.line_length + per_length ? + (lp.Z.values ./ ws.line_length, lp.Y.values ./ ws.line_length) : + (lp.Z.values, lp.Y.values) end @inbounds for j1 in 1:nph, j2 in 1:nph, k in 1:nfreq diff --git a/src/uq/plotspecs.jl b/src/uq/plotspecs.jl index c5b25b43..a4016ca1 100644 --- a/src/uq/plotspecs.jl +++ b/src/uq/plotspecs.jl @@ -1,12 +1,5 @@ struct MCDistributionPlotSpec <: PlotBuilder.AbstractPlotSpec end -const _MC_PLOT_QUANTITY = Dict( - :R => (:resistance, :ohm, :base), - :L => (:inductance, :henry, :milli), - :C => (:capacitance, :farad, :micro), - :G => (:conductance, :siemens, :base) -) - function _mc_selection(result::CableConstantsMC, quantity::Symbol, ijk) ijk === nothing || throw(ArgumentError("CableConstantsMC does not use matrix indices")) sample_values = has_samples(result) ? samples(result, quantity) : nothing @@ -26,31 +19,20 @@ function _mc_selection(result::LineParametersMC, quantity::Symbol, ijk) end function _mc_target_unit(result, quantity::Symbol, length_unit, quantity_units) - semantic, unit_name, fallback = get(_MC_PLOT_QUANTITY, quantity) do + quantity in (:R, :L, :C, :G) || throw(ArgumentError("quantity must be :R, :L, :C, or :G")) - end result isa CableConstantsMC && quantity === :G && throw( ArgumentError("CableConstantsMC does not contain conductance"), ) - prefix = if quantity_units === nothing - fallback - elseif quantity_units isa Symbol - quantity_units - elseif haskey(quantity_units, quantity) - quantity_units[quantity] - elseif haskey(quantity_units, semantic) - quantity_units[semantic] - else - fallback - end result_basis = result isa CableConstantsMC ? :per_length : basis(result) - tag = UnitHandler.QuantityTag{semantic}() - target = result_basis === :per_length ? - UnitHandler.units(prefix, unit_name; per = (length_unit, :meter)) : - UnitHandler.units(prefix, unit_name) - conversion = UnitHandler.scale_factor(tag, result_basis, target) - return tag, target, conversion + resolved = UnitHandler.line_component_unit( + quantity, + result_basis; + length_unit, + quantity_units + ) + return resolved.quantity, resolved.units, resolved.scale end function _scaled_distribution(distribution_value::HistogramPDF, conversion) @@ -175,6 +157,7 @@ function PlotBuilder.make_render( data in (:samples, :pdf, :both) || throw( ArgumentError("data must be :samples, :pdf, or :both"), ) + nbins === nothing || nbins > 0 || throw(ArgumentError("nbins must be positive")) sample_values, distribution_value, selection = _mc_selection(result, quantity, ijk) tag, target, conversion = _mc_target_unit( result, diff --git a/src/uq/types.jl b/src/uq/types.jl index 1c09a4c0..abf77a0b 100644 --- a/src/uq/types.jl +++ b/src/uq/types.jl @@ -19,6 +19,34 @@ struct SampleSummary{T <: Real} q95::T "Maximum sampled value." max::T + + function SampleSummary{T}( + mean::T, + std::T, + min::T, + q05::T, + q50::T, + q95::T, + max::T + ) where {T <: Real} + values = (mean, std, min, q05, q50, q95, max) + all(isfinite, values) || throw(ArgumentError("summary values must be finite")) + std >= zero(T) || + throw(ArgumentError("summary standard deviation must be nonnegative")) + min <= q05 <= q50 <= q95 <= max || throw( + ArgumentError("summary quantiles must be ordered between min and max"), + ) + min <= mean <= max || throw( + ArgumentError("summary mean must lie between min and max"), + ) + return new{T}(mean, std, min, q05, q50, q95, max) + end +end + +function SampleSummary( + mean::Real, std::Real, min::Real, q05::Real, q50::Real, q95::Real, max::Real) + values = promote(mean, std, min, q05, q50, q95, max) + return SampleSummary{typeof(first(values))}(values...) end function Base.:(==)(left::SampleSummary, right::SampleSummary) @@ -29,6 +57,7 @@ end function SampleSummary(values::AbstractVector{T}) where {T <: 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), @@ -71,7 +100,7 @@ end Piecewise-constant univariate probability density. `edges` has one more entry than `density`; density is normalized by the constructor. """ -struct HistogramPDF{T <: Real} <: ContinuousUnivariateDistribution +struct HistogramPDF{T <: AbstractFloat} <: ContinuousUnivariateDistribution "Strictly increasing histogram bin edges." edges::Vector{T} "Piecewise-constant probability density for each bin." @@ -80,8 +109,27 @@ struct HistogramPDF{T <: Real} <: ContinuousUnivariateDistribution function HistogramPDF{T}( edges::Vector{T}, density::Vector{T} - ) where {T <: Real} - return new{T}(edges, density) + ) where {T <: AbstractFloat} + length(edges) == length(density) + 1 || throw( + ArgumentError("edges must contain exactly one more value than density"), + ) + isempty(density) && throw(ArgumentError("density must contain at least one bin")) + all(isfinite, edges) || throw(ArgumentError("histogram edges must be finite")) + all(isfinite, density) || throw(ArgumentError("histogram density must be finite")) + all(>=(zero(T)), density) || + throw(ArgumentError("histogram density must be nonnegative")) + copied_edges = copy(edges) + copied_density = copy(density) + widths = diff(copied_edges) + all(>(zero(T)), widths) || throw( + ArgumentError("histogram edges must be strictly increasing"), + ) + area = dot(copied_density, widths) + area > zero(area) || throw( + ArgumentError("histogram density must have positive area"), + ) + copied_density ./= area + return new{T}(copied_edges, copied_density) end end @@ -164,6 +212,12 @@ function CableConstantsMC( surrogate isa CableConstants || throw( ArgumentError("the cable-constant surrogate must be a CableConstants value"), ) + all(value -> value isa Measurement, (surrogate.R, surrogate.L, surrogate.C)) || + throw( + ArgumentError( + "the cable-constant surrogate must contain covariance-preserving Measurement values", + ), + ) return CableConstantsMC{S, Samples, Distributions, Surrogate, T}( statistics, samples, @@ -282,6 +336,11 @@ function LineParametersMC( surrogate isa LineParameters || throw( ArgumentError("the line-parameter surrogate must be a LineParameters value"), ) + eltype(surrogate.Z) <: Complex{<:Measurement} || throw( + ArgumentError( + "the line-parameter surrogate must contain covariance-preserving Complex{Measurement} values", + ), + ) size(surrogate.Z) == statistic_size || throw( DimensionMismatch("surrogate and statistic dimensions must agree"), ) diff --git a/test/plotting.jl b/test/plotting.jl index 16b9ba95..c9189e79 100644 --- a/test/plotting.jl +++ b/test/plotting.jl @@ -63,6 +63,24 @@ @test length(rlcg) == 4 @test length(cartesian) == 4 @test length(polar) == 4 + series_plots = Makie.plot( + parameters.Z, + frequency; + mode = :ZY, + backend = :cairo, + display_plot = false + ) + shunt_plots = Makie.plot( + parameters.Y, + frequency; + mode = :RLCG, + backend = :cairo, + display_plot = false + ) + @test series_plots isa Vector{UIPlot} + @test shunt_plots isa Vector{UIPlot} + @test length(series_plots) == 2 + @test length(shunt_plots) == 2 test_golden(first(rlcg), "line_rlcg") test_golden(first(cartesian), "line_zy_cartesian") test_golden(first(polar), "line_zy_polar") @@ -97,6 +115,7 @@ first_entry = first(last(first(legend.entrygroups[]))) Makie.toggle_visibility!(first_entry) @test any(plot_object -> !plot_object.visible[], only(handle.panels).plots) + Makie.xlims!(only(handle.panels).axis, 100.0, 300.0) ui_components = Base.get_extension( LineCableModels, :LineCableModelsMakieExt @@ -107,6 +126,7 @@ @test current_view.yaxis.scale === :log10 @test any(series -> !series.attributes.visible, current_view.series) @test haskey(current_view.attributes, :limits) + @test collect(current_view.attributes.limits[1]) ≈ [100.0, 300.0] Makie.toggle_visibility!(first_entry) @test all(plot_object -> plot_object.visible[], only(handle.panels).plots) @@ -118,7 +138,23 @@ @test occursin(" 100 + end end summary = SampleSummary([1.0, 2.0, 3.0, 4.0]) @@ -131,7 +167,11 @@ [1.0, 2.0, 3.0, 4.0] ), CableConstants(distribution_model, distribution_model, distribution_model), - CableConstants(2.5, 2.5, 2.5), + CableConstants( + measurement(2.5, 0.0), + measurement(2.5, 0.0), + measurement(2.5, 0.0) + ), 4, 0.95 ) @@ -148,6 +188,55 @@ test_golden(mc_plot, "mc_$mode") end + line_samples = reshape(collect(1.0:12.0), 1, 1, 3, 4) + summarize(values) = map( + index -> SampleSummary(view(values, index.I..., :)), + CartesianIndices(size(values)[1:3]) + ) + line_statistics = RLCG( + summarize(line_samples), + summarize(line_samples .* 1.0e-3), + summarize(line_samples .* 1.0e-6), + summarize(line_samples .* 1.0e-4) + ) + line_distributions = RLCG( + fill(distribution_model, 1, 1, 3), + fill(distribution_model, 1, 1, 3), + fill(distribution_model, 1, 1, 3), + fill(distribution_model, 1, 1, 3) + ) + line_mc = LineParametersMC( + line_statistics, + RLCG( + line_samples, + line_samples .* 1.0e-3, + line_samples .* 1.0e-6, + line_samples .* 1.0e-4 + ), + line_distributions, + LineParameters( + measurement_parameters.Z.values[1:1, 1:1, :], + measurement_parameters.Y.values[1:1, 1:1, :], + frequency + ), + 4, + 0.95 + ) + for mode in (:hist, :pdf, :ecdf, :qq) + line_mc_plot = Makie.plot( + line_mc, + :R; + ijk = (1, 1, 2), + mode, + data = :both, + backend = :cairo, + display_plot = false + ) + @test line_mc_plot isa UIPlot + @test line_mc_plot.page.kwargs.selection == (1, 1, 2) + @test line_mc_plot.page.kwargs.mode === mode + end + library = CablesLibrary() load!(library; file_name = joinpath(pkgdir(LineCableModels), "test", "cable_test.json")) design = first(values(library.data)) @@ -157,6 +246,12 @@ @test length(cable_plot.page.kwargs.colorbars) == 3 @test sort!(collect(keys(cable_plot.controls))) == [:export_svg, :legend, :reset] + cable_legend = cable_plot.controls[:legend] + material_entry = first(last(first(cable_legend.entrygroups[]))) + Makie.toggle_visibility!(material_entry) + @test any(plot_object -> !plot_object.visible[], only(cable_plot.panels).plots) + Makie.toggle_visibility!(material_entry) + @test all(plot_object -> plot_object.visible[], only(cable_plot.panels).plots) test_golden(cable_plot, "cable_preview"; tolerance = 0.025) position = CablePosition( @@ -179,11 +274,81 @@ @test only(system_plot.page.views).attributes.aspect === :data test_golden(system_plot, "system_preview"; tolerance = 0.025) + zoomed_system_plot = preview( + system; + earth_model = earth, + zoom_factor = 0.5, + backend = :cairo, + display_plot = false + ) + default_limits = only(system_plot.page.views).attributes.limits + zoomed_limits = only(zoomed_system_plot.page.views).attributes.limits + @test zoomed_limits[1][2] - zoomed_limits[1][1] < + default_limits[1][2] - default_limits[1][1] + @test zoomed_limits[2][2] - zoomed_limits[2][1] < + default_limits[2][2] - default_limits[2][1] + material_plot = show_material_scale(backend = :cairo, display_plot = false) @test material_plot isa UIPlot @test isempty(material_plot.page.views) @test length(material_plot.page.kwargs.colorbars) == 3 @test collect(keys(material_plot.controls)) == [:export_svg] test_golden(material_plot, "material_scale") + + primitive_axis = LineCableModels.PlotBuilder.AxisSpec( + :x, + LineCableModels.UnitHandler.QuantityTag{:dimensionless}(), + LineCableModels.UnitHandler.Units(), + "index", + :linear + ) + primitive_view = LineCableModels.PlotBuilder.ViewSpec( + primitive_axis, + LineCableModels.PlotBuilder.AxisSpec( + :y, + LineCableModels.UnitHandler.QuantityTag{:dimensionless}(), + LineCableModels.UnitHandler.Units(), + "index", + :linear + ), + nothing, + "Heatmap primitive", + [ + LineCableModels.PlotBuilder.SeriesSpec( + :heatmap, + [1.0, 2.0], + [1.0, 2.0], + [1.0 2.0; 3.0 4.0], + nothing + ), + ], + (; kind = :primitive) + ) + primitive_page = LineCableModels.PlotBuilder.PageSpec( + "Heatmap primitive", + (400, 300), + :single, + [primitive_view], + (; + display_legend = false, + controls = LineCableModels.PlotBuilder.control_definitions( + reset = false, + export_svg = false, + xlog = false, + ylog = false, + legend = false + ) + ) + ) + primitive_render = LineCableModels.PlotBuilder.RenderSpec( + LineCableModels.DataModel.MaterialScalePlotSpec, + [primitive_page] + ) + primitive_plot = only(ui_components.build( + primitive_render; + backend = :cairo, + display = false + )) + @test length(only(primitive_plot.panels).plots) == 1 end end diff --git a/test/reference/generate.jl b/test/reference/generate.jl index c7bda226..8f3f6f68 100644 --- a/test/reference/generate.jl +++ b/test/reference/generate.jl @@ -59,7 +59,11 @@ mc_result = CableConstantsMC( [1.0, 2.0, 3.0, 4.0] ), CableConstants(distribution_model, distribution_model, distribution_model), - CableConstants(2.5, 2.5, 2.5), + CableConstants( + measurement(2.5, 0.0), + measurement(2.5, 0.0), + measurement(2.5, 0.0) + ), 4, 0.95 ) diff --git a/test/reference/pre_refactor/README.md b/test/reference/pre_refactor/README.md new file mode 100644 index 00000000..02ba86c4 --- /dev/null +++ b/test/reference/pre_refactor/README.md @@ -0,0 +1,18 @@ +# Pre-refactor plotting references + +These images were rendered from commit +`abe355170add20cc0c841e218fed82fe8ffbab51`, immediately before the result and +PlotBuilder consolidation. They are retained as the human visual-parity +baseline; the PNG files in the parent directory are the executable tolerant +goldens for the consolidated renderer. + +The preserved line-parameter renderer contained two unqualified calls to +`value` that prevented it from rendering in its recorded environment. For +baseline capture only, the temporary checkout changed those calls to +`Measurements.value`. No plot data, specification, theme, layout, or style was +changed. + +- `line_rlcg.png`: series-resistance page. +- `line_zy_cartesian.png`: Cartesian series-impedance page. +- `line_zy_polar.png`: polar series-impedance-magnitude page. +- `cable_preview.png`: cable-design preview. diff --git a/test/reference/pre_refactor/cable_preview.png b/test/reference/pre_refactor/cable_preview.png new file mode 100644 index 0000000000000000000000000000000000000000..85c7b20a37beedba768bb0fd7d84c94ee707d197 GIT binary patch literal 425114 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

Ba>uH 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

m6SJjvrH@aV$P?K%REH zBt+dRELs{8w`A_^+wH26Wqj3Kraq9$>mnNJ?ifK!>MNkEsIRPpPLA;8J=qZ1qvene zMd>#PehsQ_f?OhvSy&K8^WPieMPpC`;=>bv`S(Vcm}6O?&OjAOB6caY2ljM9i;Kwk?0?TYc|qA?p7o|#c} zfUtt_OvS=CGB=k25()A_S}n3J5Btk2pjX|k9}<|t{VG4VyrD9Vsjepzhqzsc9v6E~ z;v-kOr)pPJK0MWEfF-*mly&w~KaK#{Zoai`Cz0=rVPnH_rj&FB_8Bk6*u3JR-~&%>{+0w!Lwh^s8zQ=UX*7FePugoeAf3<5PXaP zQhg|M`8lUgcqT{iXe(YaG^}TqJQO8jLAh<`K{HJ633Bk8NsPRRlP-snFIn4Ot$uG7 zmqvG?6YW>e71&2d;wJ&Nu^h-Hv(fYoDl20!Q$?bd(^&I(ZJUYLk2YB;sUPDD!z^33 zO_P56-hwqY43>w+6RvQK9KsN{JTzoC|7B8*`^t%g_4TbF9}tBya1daA^{4)6{k!p$ zk=*e?!Z6{3$F0@RFQr@P^&MPYYr1Rw9-jlm|K-hh`&o~Dw1Rddndt=o>G(yZ;f#ir3nZ5z=PhsZuO8e&hdAA2*<{n~}UMn)!_Dz4&uTgT^A>|fkw zvA9a+&+Ml^M@^P`vLpRcYRPjo_M6gs$Dr@7hrah!e*W+PoHr;B()U?>e9Z&x(hV~J zwLhmzS#jOK%y}Vu?oQ}S#^_>gOSy3QTE%e}7V>YyljVg)v}h{RIisT{7JQ#+_mD?E ziIKkCOJ;lN8pnQsZKBazX~}Hp(^-?AtaDM=cq>@DYmGO*fA_4!z5Icz=$YXAx0ay- zFjEFd2A`r4ydBUgb^@-#LBV+$bFKa~EH&^{{Vr#+U__3;@-ZfRDA9@?hIc^OPt0ER z%6#ZR&VEOE2p{}xR>9e(;1Jy8qO0c zVKPz--A#!}%mj>pvTA50UxZw%x_hgJD{pmNne#T)YVF#<%m2p(_%=Pc&yu4qE_R%p zYJx_>T!szRy@}??J~*Iod`D@Zo$o<0D{{ZR$iW27hJw}T*cidskqM?&3S$f6Y(Xgm zK76Y8#}lQS8=k*@esB3=j|Go-`;R5X`Bs#D;fpm@Rd;#BZ4;Mdzvkw?HV4Lw-QEVB zJ5j>CO1c1keK-Yy?pD>dol}xeewBE$r0*z`h2qDSoHLjswPSXc?3%6f>7_=X!JsH7 z_?#Di3^GH?6m|Rdd4lN)@(&SD#9dcg`wgrw?LSR|6VsE2DJkpYzlzv9IzKBwRw2OV zWYv`WfSn1WjDbe zzHRB;e#M6$poocqJt=sbgRuC50ktUZ+BC)Jig%B&YB9khtd?O@oQ*r06pnfjVHY7r zak?wVV*Xosi47rWlO|pw&O|6{vk8J8+$h17s=BwA>LOuxUv;;v7bH@=AR(kAP~LkU)0?wzJ?`sW3Q8~Xc@P}#0Hz`4h7dG z<HZtdnhCS#zl|PLRRL zX>Xdh@VztFFs7|@j{z(}Ej(l*6TBJ~7|FZz_3bio`vDi=_gQz}w{PF-BltRj=4A{C z&*{B(^#P*_85j&+4^!t#H8481ymi|e>IPK(RV5^Qcki)lj#Nu;&6jnrMI6JdU}Oy=`>mPYWf?iF$yUw*dttyK1$`}=`-s4 zlC8SEJCpbG(W7;Ih`cMJrwYVYpdei7`uqFI)3dS~z-U^9#**mLiP0gk&J<8hmv}7i z!HYl)^qph7)-?!wN=&t+!4U=Fb36y-cNA>5$=DPnSbp&lXCXm3_(?C8$PEh61;)rb zQpd*Rj#djl?RA{%yTRst7MVu%;%I4%KKWhc)?TEDn4RTpR)2obG&waTODenb7}sX( z(V5V_H^QYp_8o1yuwUn_Bi*U*=}t4_{$Q^7DqBmE-ElP);Pn$t>*W)?^?9JWxajxJtH%6BEF%z zf*;xhT8Hm3GAZ~S`@vow-!5R)ICxz}ob28dN4hyzt9G-3L?zwtea?#-tSbT`8t_`$zp073 z0_54zCAkE`(L);Y@IXr5L~umz-g8L?#8i?=5bethL@}RBOZJZyj{7#2y_)e`mc!I> zaqnJ^QGtPoyCeu+V)GGJd1nxsMLd4gmr38QdK3}`|p%7}s6DZ%RlGO3prk~DG)s+uvfSbKgRq<oN`=_qaZ-D{Cq-CI=)%gs^XpI7qTpuL*xLotem0 zOgKXU{X19kk&e=5HyRFs{GeE9{dtw0goFg?QxCQI(@l(x&r5gYT))1;AoKx~i3Y?{ ztO^na7i&wtq~7-86w)d8_fLGyVke0IwV}-5XJL{z*;{xVF+hoO9b>@U<0Q8+4x73{}bJ@ zkNQJHGoGx&LdBz{J9Wl5!qkr^J{h0g0fL81w9sg6A{j~EhMSWtc-b{C*)#U}& z`Vl=HLE-V!$xb68#_3MMqfMMEV9jZ~_3b4Q!XOGQKKoY@J|R)J4}SwVju?pjt=X~p1fbn0y>=>L)G%m7JB(@OlnDu z8rq3%9=x-4e>E#w&Rfs_D^^#ssvX&58}#DPq1MEti%D-EomamYYGifwR%jAqw6?a_ z#WfO=KBb~NZ!BD(*M!CG=sl2bT~M6_YJx;aNl96v4Dtv;i$gO9Bh4!4xtqju^YQ?a zF_He!A|?cEyc=w0x6mK=$J3L`H7lhGdGd>ppCYOIEA{CQPfaCRTe~qbHp18uswyRM zGg6z}ZBz9W?xdcYeYK-5&rS(bsoAOO4R&~i%bWBT97sye10SHfwVVipAPx&(C_T7d zWND_NUf^N^b@W9v|HRif;(*U&Gu1ECVBr$$rk~({LsbcKW4-zHqH| z-MWkI*EcaxGxq@71*cjTR0Q`(Sc^1au^l<%d;-dIKv0W}pz=}j#J1dB7@nXyc{8a} ztfr#kcK>^ib}&XzF}dp!tS^TlE!UaerUc)3BnwduFo%%-MH#}Xwkgl^T?^7AtN zH}g?>E0`cVo9%hdg{-DUVV%k1=%{Iq1?Q}qxGE`QyDD&{LV_y4A%GXtXqq#2AZm4?-5hm+8zgG31G+xl64GN}F4^0w5wbC?1&*K=vEL7I zNqqXToSGb3XUZKb9@6jqn~ak3KAx!#@-yHK8xGPd;T2J;|0e$AFk9<02WBFo6-R!? z*i-;`jF-$z%>jmJG;cTrtEDSr#N>@y2m+YVr53SNmSngPcJNIOKKZm(|=+jo2>nAMI?e-x4BrH@0IDKqn=)@6&%#S zMztnf*G+3+nQS zY5SB(Cec9I6phOr2IlFfL10n%?RqT(1FHbrsc=5()9%z5BgtSy+$9X~;QtZz-ho`N z{r~u>9Hl};GRoeotdL0dPNA%Zt?Zd1MY6NWibz%{BV?7bS5^`sGf_f{h~MMYxj*0E zANL>U?i{^c@9TQb$H1h_yuZ~_&jMql#hAtRpd^3O@+&r3(^i-4W;@$rv#amr-rfzo z2y$4)(%13+<3$F(T z=xiG8s!nk0XxxDQl?}Pf>@6#wDNbFWrsJqn;ZI+`SA3 zUqKD@J;tj=%N<|(N!}GGpti{U+!%IPZJpvWF_Q-tdXcN}VqND$|D96qOC+>$fHkug z7>V3?CvZT>=*>ky`;J11=`O{gR*cPvAsSJld@Y|-1zkKTpC1c7VpX1-%PTFtyA-@v zRiCKFm`B_k99+>!Ng)@%eh_o$xuX=C0cSpo*2xyTjhlzIZQG31$EReH`OdzBlwum| znkS}J2T+I&MW&_cC@PZR0xL2PMu&^l&hWF&0FqB+|tTvqt> z^UjaYPu?jDCyzgH0I;L`g3&dM9V zggDhyRZr$)jvU*F@d0l*<{;0mEt-V@|0Dt>SkMfKRtudOD|LS3;gL7bzg9ghm_6zI_>2$Be!)YgoL9<7b=PLT zeruMiOpH@>)-h)maXRVCM>Nm5qfg0Db0fQaKL1(rPzy+Zdj>fFCoRwiC#ZfU8swfTn-mrx=aoQMc+>>U7W!|hN< zEM*8MmEZUD`LoyMR`wvs!G4C&GYD2Z6;*Vu&|=eXZGNQe5Gt6~7ku&Y^TV}q@(One zrBsj+z>B229382XBuo);v~_E#}uoj_~j zn~%~Y0O#@fUseb_;@mSsn2E~oTg}H;;XM9^48z(-#RtWX_@hxyr$(pl&QU4dReA=6gJ8pHqe*t5qK%fE#7U&!D8Se^4qcvA7$yVT<- zYnOB~yT5xrkntk6nVe8nX9c?%%a_eg2)3W1!omfg-8T!0U#W1Pl#{MSb>x;GaI5?d+_K&(APQEtJn^fo!?bH<`Dar}IPe_C!rgt1hBbzH!Q^EFGw9 z&dS64+rFJx&k<*f9RIOfKW8oNNy;gpbo5!9_%vVK zozg2UL@s#$%zO=oVm5h3hM1sG#wjV*)oT9s79n&`)8NkO@-0GcmG8;M&l<^aw2jxR-!If|u&A~kMFK;g&Na7xX@CKO+4Yv7%qU+>`K3&n^nJwNkx@P2NacTebj%OEZy?O_*j`}VdtrDTZ$ z!>2cS9kfSmK3;RCr1YV5aVfrbXo&AF`rl#i+v7j>tod@t)&-K1Z{H5I0RL0bLUrd^ zDPj5TIj`O~?}9m!{AE_=oc3?iIvlGzt^#MNc9Sg1O2Qj>i4$8_Z`AK}QDx#$xMUhxt>U;`q4Z znFEp^26a0c64~h2ig-GQ-|qFPJSKz3*XD+WO>;p5K^_pL$AALZFME&9!ErUvzDf=we8zz8xFBwL{WGqk@)@sbVKQn zdkFkJs284d>Xv7Kr^J&(WWC3kd`% zz)LBn$ICA}I;wbKw|TzGiWbXUQLiOB=u{;5niG;DH-5)zF%Dpo!UH-}DzUA6@oQHT z*2d7XS}L@#uy~3p=jPhl!MNM_o<5y`B-WY>%>PcQ5i_-CQB<Dhv>0A1y44Z_F7)AsFaMkws4pXdpTC1uc^O% zY{-5=htNI`-fbpyxgqNp50Duc0>HC?azJb_uzD!~8>Pf;_RNT)g#`zwvV=uL0Rm;u zGjkP0PIRm7DN-@x5bs~ZD41A1;6G>@9%dpOctxPDtlswewZGifMhu~r4k9{+N<0o& zuD2(N?jQ~Xt2SsEH91#`IN-7amdSEG=ExA+lcnH+sLNt@a$Y8ekp>#pB z>zk?VXrZ`+LqCszz*uAWHIJ`rQJGCQt+0OiT*cu0z1g|6oxJ1p%;cHjsr$y5pc~2d zQ<0E!dKGp)t~eJVVmt7HGES@J`fW@oA`lu01GsnCnMr!gA1txb&McmN|L_D>JS-q~ z4nU~Q3Y*W3>*L&fdjMzN__IMUnTX+nX@%1iNvQ5kbgDePoX30Lnies1?rEvE6je(ovD?y7Ub zAD%BX!$%l%vQO^M9Y?ionke)nu$*AO;;ex_=52Sk{2b6|j~>^)#9TQ}tLmNo`IQ;c zNdsw{tUdq6s%E0&r>ooY+NQhaG4alaVci(TV|(LCm5BLjroYAG|L#YKWYY0{%aRlY zTM;K#Z$)IBWUBjv3mk9PYSnuqi}6uF1`EprHuWgBLzI);{daG@r#(TXD77;07eu>f z*Y2PU9v&7`CEnSZU$tMi2D@r*bS`dZc*&leIK%sdm#dpeA@GhO#o<4}rD1!8%Ju{B zlV#4W$EQEHf8V}ap&eEP^@!jKAb+{`>z^YQ$?gNDe>d+KG!Ww=g8gULSXX*Yh{GMwrQwC> zJfOYA9mNa=D1z3V%-$z`d`@6`Hx9?p=`IG0V^M<^6(6Ab8w%Y(GJ~c)v(Y8&H}Ohl{~Ho{a9GkjfM#u zDm1EIiw(ZdzK`rw)zMI44H(zEl|+-Ddj>%F%>e0ca+fkBf_UFxZ+ zHR=q7e|A~}1h3N88^ym_aLeJB$-N%c=r^~v{TZzFIgZ_7(^nW>WDHFuOw7%N_Lz8n z9ld{i%j3b#>zvlhU<~)~0GZ!?WCD8GlqWyFK#M~BvP#|niqdA10yU&f7 z{d_WXIvUFO((d!kgzymjDy<>3Z#8nm`jGm5CK|;m0vrFK0dpg#_3=>QgEH5^`A8Sz z<*m79)3IMMkYnuUReg%oNXdksi&PumC${N5uwRI}Sdn**gd%VU-8OEj+RG^dYE6PQ zlK%YBzu!_vKLWUVTtIgt`WL@LE+;)ll6g=>xIDjFDJl1vv)F|c^kNZD@K}JqpMj+- z0sOgISV>w=7$1Hu8*l+V2e|AvKy*84fZ>fuWrdY&N*i=?E5E&%BlHE{LwW_Twm~bq z_Zg-hR$UJ|W(h&?drVUG#}}#<(2?OHHFTIggRmfJYisK@!px?mux|7E;T!WuiT~aF zvvLl>-7lGC7a{$~{otA(J^&C8OrR*7FS|kuW}ZUUZ+_Jrz1`9AQQB88**kEt$uK3b zSIV)|&C=cDn3Xie1YGAtSxWW;qs~E#cnenR`0Cr&hkq!`&3hc@>!O>?{x?aIk$LN6 zYpZKy=&uN+%SS$6EaW^i_fngtRXylq9P6cH&SBf1wCJHR& zx4MN3pw-*(JER^>1)Ptdm@hZ8M>rj&g#+7=V-x+6bUAg9<4zL~mRM*d!ro)M{61CH zAeCU!y%o`fkq^juV*pDW+gp9*AN5CZ9QEW6n=SikqQ^by%lc_kv~rXpT9^CZz56uv zRmWaS%0Zs=qV`f%>A0qm_Q~kdJL>@~o=?U|2Fe&!^%O}u4EPIPwe+3M(aU?+VjMBY%z#dY*z1qoafog%k#Q>S~#DE{!${cp;*yB(o04?1M6CYf& zQzj-%4rphm?=m{P#0!l3VitKimxe?9MC!Z07D+ZI_o)iDo)oG-c2F&U!rLO=ihbs5 zAB%OF?P)LBP^o_k9w)jwCsn7XY(>tqeH`*s&IORM^J-Du?cr~xRp(#)po)cKIA-dr z(C;KJg_}WsTv6?ZS3N4s%C?}}BeQ)XT;K6wO$30*Gy9r+b?r-=eX01_Y)vb$T?jQB zAjO@8bWW_$JY;<}EaMDzFUWzoB=Tmla| z`1L2?G@M?b*y%0Tag5DFi3}7ElZ)JDMOxLuiyc{C9z?buRpIgoKXfd-NLcTjlf-qc z{hK#$>KW)iT(a}XrjUv`8)|Z0jdVsK*6q$3Df_RhpPVsu=U21g41PDJc!iHIE^kHT z;ND@kUagdH>>D66NucjVZAWWQAdAkp0fr~#a`foYVaObm>Om8IcBov~8f%(UFviW^ zLq^7mZ@&bZb7o+urfB@HMRWme57#37jHv)MU)(7%3Gt|Nr(b4G&&+8=a_*x`?!C8T zr-L8MzIl7)=+aU>cLnk(ciLt(a;^7krnX*!d|d-yi(%6hD3zUju?$sBg<1`VJOzfQiJV_x+^{tH=xxwFSRv$6|VEk4)M?qvI zVvyXh1f0FAQe7>NE~*YSQS1bW2EImoe0)K{ z_mp#`J-;+}HR;Gll9rpP93t)fTCDwbuxv&}??J(nJPP5ct+(P5?B`5OPuZIba`^|x zM{)?Va_wa` zRPt4CNKE(`^CS2C+9S{XC9+i7h_6I!kCYnwcuuao&XrjPu=bk7YA0=J!1cb2GAgzd@h}&<`?Co zpCDa3ffqYwh73Yk-jQIkV&Jo@lzfy4Nl`j38I%q`zg!XXBMt1mZS3JC zlv;lrDbXM>A*X5ns}9ub9LhRYR&8p6Bf>xFU1zYRr#DeINDgnG7lVh^P(uhPStBQ1C0xqP%^TpKqy}a1`8&Zx!fJ7U~lwu`WAQsUVkna%#=byX;nK z$>PzSx)Sb`$(!fMtq2#e#zr^?qD4UY~y31IC|PNk?BU6)>Te#T>HVkcm0Ym1Y_7pvf=N23me zidy4CH^1L1V+JM{Z%6pji0eLmPEP)tf=7jWnSC9p&CH(N-=W&pJa*=|^A{SmMKaKUa7BNo-cirmmpeOK zO6r%+DlM`-A*|SLl5OKPBD{?ln_!$_bb!gz1p2&uXVHVz&Fvj3Q-d4cK5s+9Q$l;& z{;}Lpxs^cg_$o*_Q@&-p;DM0PP-pO`xy8h2IQLLeg72Ruy7qP8Vxou_1jpkz2-d&X z5(k=blOSxg%#VDo{F1k=)O~YvQ!~X$v`c1rZ`X-mOfCe|X`x*6wZ+oof~ub%>L3Ut zN=P0d(K@lB6W7Kz3B#*E`u*XHXQao%BPjLaTqQ_bLb`1rySgoIM!l_FKEG=FRabV0R~NR?7|>0`o5pKs zs72pl*`>G|xODd0(c$7&*Wh0fToTf|p`CLz@BuD2+#-slwGH!<3SB5!KDfzlQ4V&(5M6nqkav|I1%(S$kGz+w7E-YWO9z7yK zB#b6ZPjxX9C1qx2o)i%g4Gs^MZ^_8a0N9sBN-A;BBG}bOT^rqE&3=zMUj80=b*)Km z^LC3xJz?qyUf}(YqdxxB*ju*Ki>}Vdog*X=gg|^Hb$)h~$I#7hbma{*8O$>`3w1 zy&3&B@ZdYkIolj?UqCflQA5f(T0cHKC7~lYW>j2E`m|T=>gRo(T4Muw0+She?ljUS2?UADoba>A5ckL6RC@9YiodtW?F!# z8yg!t<|K)0U(w&!cS*!6(+%K=zIX4&d+;&f3^3~^ed5`sE>p)AefdOxrQ4~t%F@sX zi>*)WIWop2riz_yh~HH*&}mVZKIP*dc|6H;FI{GLP)_$-1`7*oD4XNKeW_DJksKBO z;8oV+I1bGbE|YmlXGx~!f0NpSqT}Q9a`GP)<{p;PQegMEdG5SXKI~ynG zbky`IT#nw1l1$ast1B6t56TGYQ8N-W$fvDD9>aiISK7Bk4l7raASiVtFghH(3oUoG z#$mA#cgH#aCPbaQZhVyfR&6e;ZYIV+Fa1NBC~3b{Oi*;5gOe~+nRya2(` zxTAcPowVR~d}OInef5s0tHlzx|B-x!QJGrw(?L5Eb$Ua?|3w`~6dc=80O=ZCXv{ir zm_gaif<$QLIqBp^&Bk}?6;+%?t6-OcHZ>hRlwVv_JU-cG8bcfD|8rKw133s2g)1&#s`p{x# z%7u4|mgZ8~;9GT^5fNrKweY5H^@X_^?Fi+(_5L%D3Evm~s6D?Z8=Vcqn%6$6VXI z)N)QRTQ?tSoH=|;E44qN+1Gm*1K#`lO?AeeQr^bW_YQ?N+j zvhgdm;Z{bzMUepNL-fH7W~|4B6Es?^N}^(tHN5Qv_~N)lcamh_b>u?xh{t3pZ#T|qPlHk{%GaAp+guK0%Jubqdfdy{(ju%lH5fgfxyDYv($MU_d*LL^ zkMVdUnJDmWtLvXJQpf)xgzjhK=TH$(V!u@;vIX{ulMwkeNT$uY>hOGg%6{D2XZ>sW z4{bI1XpY#Ni*>c$FpKOP82DR+MqMb%zESQ#2x1`jJIfOPWj=XUiNrC&7Ix3BUFmsw z4B5TtGkElsl+dKPzv6^6cXZF0*eAGLsGUI*n>{%4z&}yNsm+0hJ;n8E+Cdd%4+*}m zmAMwTM{`w|OT;G*jN3mfea=*#Qp{wej>LcG_ucQrp@rmW1c?Pu(;mLtZIh1B+`=U* zap1qI^l#oe>ER$$aQijtHW8+T(rC@eQ$9PWl*piZmWz8jxxpcvPI0Vegb@YluEm{C zQo;?gXDpORT~gVkVYEZGcQ3ZEguQDlA2)XbfF&k~sB0cG+ObcbrbmJ_fflB-sVNY` z#Ic3XpT|eOJ<>S<(>@TV7}fhxpvoG#$c2Z8Z~Tep<pcDr;CS&Wl**vY?o4Mo%(i7NkZ35 z(xWQ8BzQ0R$*DmM7nCz8#lJXX9>U~iqAC&YSbj@>a zH|T7%cz#J(^R-rA5Jv#lcr^TMu|uGu`ot5}Z^aIQ3WzG0pxgrj_GC+OaY=A!_>>zi z7Q7!Wl?+C5kY(`+3uk*?Vwsu4xTn2(Wr0V(iQ=h%}cz)j*@9 zKcv-Fzh69K%F`(SP-T4{TZ$yrzNbb7!{ruaS4X^Kk{uXc-A{ISWA^N{kx?tH-qE3j6NodQm5@Ud|Nkc=( zA*t|m*0*iUx;;H-0`wc+&r57-_?&S0w}BP~NYx#ZP1WE6iprl4gDY0P#MQrWcYLU~ zlj3lO=Ynj-+Un{)diqx_XR|-Qf4D;(DsgpAU)=|sxiA61HV1!p`%G50HSovHYxwvm zh4xWV-HTSb6WBXE92TvltPGXP(7%(8w`Z^v#U;$Nz;)r^;&9@^368(qWWKyT6VtZi8FgB_Hu{_FeTibCY{{wBcRGar5kIL zTOHy(iS+&aHz~3ZwG znA1mT&#@eH8awIX^GncNdGt;X>M;VE9Cz+Qu<`m+4v)j50mepJR9_{_Dpops=n#Qp;eej1kM;x4ahZ&wgCZEHcZf>0k#(CMj3&+T<7s!CIX z^?u&l_^mSs7+dl?6@-pT9xg|^$Y5g!rSJ&!!W7l)Bp9nae7ou{c#~CPE0BbOHqiL7 zmXa-^w2i~_9&pk+A7RjDH~gJU-Tm`-DuUU}vMVJ5gJCpPx-b4INi6OWiS4xenVpeA ztY3}Pqd5>p>viO|G}A_EbTeKDM!3wiC#JMuTcQo&O(i8I1dVht;_uy9D`)q_e0}LN zd&GY|0Z&V(%9A5W-{V99+*~!-8k^E>*cmTyH>~{U9zYA%Uz?r7q%$| zsO+Q(5oGYxl2RhIHIvh1PBJ&f2e-imIO@b~mt`vGYkJ)!4Y8yNhYSDKcVF>3*9l3OJ zpBPW-1;=z0Da+!QUj75^FcsoZ8LB>ZZi1+_Je`~pj5#Gl7VNf`_#bONxD$;|%6aUi zJiw@7Cf?pMuUKC(NjgD@=K@d5jPY}q-p}35oTN>eRAQ1T6ediPi%aC`l^kg$N0Unt z-3%_U8Wm9Fm(d>!O<)e_Z>X>z+>r9$X{vPIhR0VKTC|rxT}ItQ8l{^QV60|Jk49cm zNpWn#K0~~jzqx6CA>waT<-^?hL&bcG36@)#tOAm|5;pQqP5Nmf#6n5ICU^V`^`y5L zX*$C(wYl)tlG&B(Zf5(Dr9zq;NR=~b%bK;4#Dcf|SzmtX^NUT5T77YmoALe;XSVE; z^8!Ez`xkk5cRZa7%-~4#sSQAgpRpSsAO9|qNI>g9zkkNAUa~x7TD>|Rnj-vP*_W!k z&uLt7_P^j&OX$=pP|7x&dJf;~A*PgUZ1o`HP1r%Aq^(z{=u#`JzFO#p%0<(CeR0jD zK2EB|tXaHPK6B>!Lz<3`4)0n^{s`IUG*YRL*hN_U=8Ktz=dTOSm_`wgWw?a|VR{XDI0bbRc?v>&R4tjfpL+z1xvoOyijVA5? zTDI+Eae3iwEp1uiHqVX7$amLsmdG$x{!{diqVBjijXo`Vw#v!E!F#Q*IU5_6R9ffB z@pW=Ly+G|l9Ya01Id#rN?uT@2I$k4jN^M2et%;hc65*aBHcX0n?}5L zR|4KNRQp~VOKRQdloS*23<&b^j0Sp=zP_LyaS4fs+i@FGwVT!$z+&arhDF+=SYp~^ zG4xy*l1d~qBG{)o)T|hwNDR7n?-cJl-&HEWzIR%SSbouy_WaiV=a=BCrlvd8#G!qB zVXBP9$QHDq&LW zCFVUY^qlmOZnU4^IZ+^C*w= zbtiK%O5VX;mtSyId1^3&U15#guuQ3$Gl0v9Yk6k0Gf%(E1r6zGKXmSGpa?%bFObp# zq>tg#OWOtqrNBymNOE=5s0l2GD2`y)%C+f5pb3JjzE$twTyyKSa(FSzrKmgUjqwI# z*+YBu_4PlwN$S36ZRNwDK=LA-bs(o)V!Lc*rMY!n9c|qE(lt>D$IUR;?pe2Cmo~9? zaHae;>M5-5^hJ*^TRTex`eB`f4;l84cYlrerfnsA@vDFTFz5KxZFV9>?UWPK@rZ?b z!J>y@Je`896908~`L*34DI?R0osV51G_9yXLv!HFxq*|BzW=yzhxR%x{uCK=leQ&a z+P(XK16|Ao^cfRk5;U$}>SJ^U!l9pByxA~p(?&5kKF=ZN;ls*pQd;KwQAZl+;)Mnn zZAj6n^+fB(1ekcA7O)n9EA6+c1=*ip*P}*i&f2u0bu}!R*7ETFBW`Uj#!$KP&KN56 z176;fvRwE0`xxv%nEW0osg1uXT z`~`X0|9^aNWn%a4J(YdX*Ory#GO>Ffxu0ZP$*Y}20wH1^3CXJ6~21R4zisgYHhnh(chBUa8YnQt&{9gI$PhI`&w-z?nuhXdL=$fzyhg=uA=ZN!h zdqWQo3Pg%d*RHykm2L|oso>);O&iZu*iRRf>SN+Ut0VUs!sj8%nV&SN{U$JzI%Y1i z^R%g9{owhHy+|z%@?`NG7l%{a%XaMoo}T*s(K{Dwhr2d+1b*%>IqqxG^}q+W7Jsjf zr8`-6$%Nt#mcO?BbN6pTy3q1XLxt?+75eN zUFV}kHJhYZl+8q-FmW2PFJJu8sxMPxMsH6X*h1~EZQxN%cO}XCNMrabiQz7`4?XW{ zek}0|oOhz6Q-Fa6)-ohJ|9z;cD5f$$Cr)9OH|aCea-^M@A` z+}ch*veQ=Gdbu{As4)AW2cW?!Nbyp7!1FJ;v^t^?pGEPr%B~~3#^Fy&Zqcqqgr!m? z=9@o`N~b;`EE*&>FWe-#mdp#&SFH)lJL;XS8WF9pv#ch2#B=vMP~?*{X9j<{%U~jw zad&8;E@sVB;L-&5!hQzo`aww1yR4h;0k9eIO%K0K{4r5+jo#1 zN#^AhoH_$HJ_vUaBW7ZhYJdNuLpCC@M#ub&$~DzpLPWgycb#OMEreQ7-^A3<5 z*r4-%67pR7!5o={v?59dyA+Mo&nJZ>jwHEu+lXz8|7a~{0!O@Ww}H`QLt=hxc#9{4)Y+7EvFIC|E| zsPXS6Q>bpk<=&z7IN6#TnTocyv(R9C`~JNu?rS&1Zn}%=HkAhoTTJ$%L$`$0*!-yP zE$A8a#GhDhmovQ9bpplS<`Y0nH*dBH$}CQbpmg+mZnIG9LzGQOWPx&-_)j%cMdGp| zw*2+G3Lh1~W@4s=J8itAOHO4UNhhZe&)uaLyN4OXv^$lkle*_t#KgrPE@|c_4xY4r z8Js-$SqCd!2h-oCjZiwSxV(BWUam40D=(K9!garsZYS&~^KR8 ztiv}asifV%5FWO(qg(19q8l5P4+z#q0DnUPe@Q&3!#=V2!=(ngp-QZ?y5onpUMgrN z$s`i-N+m;3m#JOG&p4pqFcf(9rMyvAa%K~!jkC^kv{7{Yo{{^E;+S0OF&9p8O97+O zAp9T#54QMuqV4lQ`LS(S<4nt$BOp>gTx2s2OX>DyF4;vre6E3NmsF) zwdfl0f!-=p*0Z6n^FzPb@yJN&+F&yex8j6V7+Qj(%w!+lZ#A*G^DsgEMPX*tY9X@N z$mn>)=eOH)$8{!%bA1Tm-h38@*LW08!bbJb;42@w9;%zE>@sL+x{H!BTM*WBl&W3L zlYdYnSSuQ`MYR=Na2gJ}4VG|!BAVF0fDU~|1CukSu3qwdPgYDa zcIG_ZS=}o-U3M4Qko?dnQAwpqJCAK~)+P6~QbM&IALYo>Ro;JTv$+=*2E<;du=e+r z51gDZS>~281hBBYGo|4*MvW=OoF$Z*joEh>7ce#s)zP2ZN9!Jcw`d00gQUv}Sf#VONVqdlS zv(CZEIo@*EcU%o?%LtjJU37?AC_V|*mdd3N{?9#GqV~5ttjsqLc@nNBh<+~{BwQwU zJ$G35!_9igA@^-m=h}Hmpo%K+Om6;Ovwp$FoylSR+`+srC%NMN3y-g@$bOX*h5uIn z>XgP8Wd91X$7ueI37q1dmC0gD6Q7m-C)a+{9}s@AQ|ISgQJwSF4$5ctLlS?x5NO&> zVjD2*HL(4{#=tW1mFa403yC_^z?<}%KVHhRwUTj0WQmmBwe`f@LV+o}?Hkpog&$k( zbh#dQ%fwcTenx?Yvt3zk(xuhand~{qr7}ZX@(>LTv=a#rAklyI-IG@hb`ls=ZT=!k zv?n;D=x6agS$`S4(Z~eHjwjHjy#L5uf3}{(8O;#~ZhG($4(J0l$h!{4b4}2*Ltx7^l#OMn$T~qtM~c!vFX^T_v>E%G|TS# zf3LkLE*7!sc+iA+gQ*A!oqB%%5-fsxAwY#!1P}(>10F1EY<<&SX$aRSB05(B!}|34 z$`yia7`=Y@(qN1Z#?cQR<>Uk{?&o&h`t!?mBK%knb*(bSk!RrkRXG)JOnfc( zh>CzVF7_?M=M^Ip68ZF&4|j+PK$pfg7v&0mRaXgd)^leJ0sy+m2r{t4%D4Z~r#i)? z*HX+F1RfI?7Z0oPl3A!*CkQ3iwkLNqntF~Z7<&B+*tvXuzspeq{g{!#))=1lCXl&W z0~ojmlLhXM%Z*9L*3cdj+688Win!djp(gv1>7}19j&)xtyO#HBWc}8mi@vW>P6q*T zMGNZxz4DefzecxSV%B<_8Z)fL!5QOgSJrk9g9)LG8Mnqa(>rkFA*&r+6O5vwa{JO3 zT<6CVLKx_xrNn%zIVXz!yPlSoR{MP~#`0m8Jt!__%*b}{HUn7z!IC*uyA5$`Ty1H2 znO^QP^)ErgdOV8CgJ{+u=wpEynt+L$u#VvRPGku(7uV&3a?T=zNCOB~DG@drG;#yX zuM5Ep^E4Z$!)9gH^F$(lj<58UifLwiJ-9pVvy1Th@uL7 zmq0Rj2*OY)0bV{|;Y0CiknkH;xN&<;P|rPwtw&1muf5sjq<7sw`1Y*s{3t_$z@w9zAjo? zDhh4xnKPC}Dnj*4oom-uKLs9T2oF0bo>b;o_}Hh)qtvRb%)R}AD5ZaQGgvqnmYQhp zIUw*7oAuuN$zH!w%K==uo%paN(K*Exu-3 z;Kz>h8_2#UGE;KLMjQyZO<%+vU8dsBrIRo}x!461`mQ~DUJIg08G%i0%iVCzlp}{Z zF;Wn3PnJuoI|H3kc((RPLeVZmBHhoF+-v&!XQqr;MoFbKzFN61&_k1{m zFk(xl%RZj{Yub$>W!J4o{XGn^v2oZW8U;?ZCAfYclLzTq6ChUSp{JL35WFntkF2h( z9mNO=WD%$1&{!s9=b)fOfVrW(!U}g-Z3udu1eo1r^ya!C!v3e+><4B5j{^%n41sWh zPAY_=`Ox1xxL-%+6)?T$Az+8K&SVz~4kBg{xEWDHTt>Lr%AtQ@!}clx;2Qjj5yM1X zl6cgd6nC@!D_$t!j}1-``>)rOa^E|TFA%0pM9t&p?;o{}rNVvujj6{yt>7}e@zv4S z9KR5eNcHT~{sur=I@jF9KO0Y1lbvln&PR#P)^vv$iDf8i#0}`P%vk>j;M(aTwWgFK zbI#%#kMEaUcEIBp&aihctOI;8gN zPT{EE*W5TZaN&la;ozW*(3_$jONSPZCgnKfyefF~IQGuiHH5_wj2i%30(i;6Ap6iEwoTdE)%%)#eeYa&*)Kp8kJLcIY;#sTd?93jy%n4nrB zu4-Id+!K>ZY64mhO?+)s;!f)TJQGwQ&r{~O68Ex|# zz1G_|zL0p+)|N#Wq_&wJCMmue5*fO8*UTxRG(n*z3fl)lWbAOomTYx(wT+ack>G9A{^L=LZU{*vD40~ef2n?vq=c~M{a!c21%bVMIa=xiY$&CBnVMN z$a%Mw+(7s}1FBhOy!A;%rqWJ|pq7EpvLTGObU@Ig8PC%2Pvg~6OUJrpAuRE+;|lF1 zhZ*YU`!=%Yw^(Diw9^HR7k`Kj`)>U~LfNDv1obg7XvKor>&K^^#Im8l)ZYG3$#i?e zz(54vK5}x?MP4AR5gd*{Fp5h%sTB!BL)Ga>0nx2wC0v|ZIcBgtvc5p1$4zykf_N}u zmW${vTq1Fs=)K_Il~Yh6k3-c9H1XwA#R-f*0$y;$^E12YiG|Iw`J*_F#;2J zX)lH#Y|yuZB@7{UH-v~p$jT9qjfi>$20$ALG5h?WLdFI*9D`V5BIA?{(LRFb(Li_6 z0IM6vpM=>C@K(6H}JbBj2vLYDD(a04!AlN!*9g| zUJ}}fzfs1;7G$`808S8|!lV>TK|}Qcd%Qp+BE&R6IjjC&zh@Zdm+}p*ILz_@MYMW~ zpMYz9nwQug-;L9cbJ$$WusDGP^q4wy_Acrv}5|=W2qydjaK}8 zT~iBlhS|DC*&x>tFX)PD>y5-8f20$81y?lm9EZ(+KF85EqUw@3tly)*^NER(s2Uet^`>PD^TB8He$sf zfLV2>wB&S~8~*v74>yWl20)F$`yjTK#Yj6u!w5gK`c|{*_N)EGT|+uMdU-<< zwiCRn047}OyYCFUvpdK&86d>dnYbw({Uso{EueUppUWmlo@4w_Dv^6xPgLv=#53DC{+t7qTK?K&C3u? z3}N>rDjEX{+}UCaHBCZtEjT-y8>PaGC91(`MA`d<2qtDhOR^_?H&Yf}WZ&kuJCMrr^{w%M)V^qarDwGD)$q))GF-+)6*ISX;7cH^f2-=tx4 zDTyq8iYsREVP#d7EAUQ4pE2>cV|FO}Sk3jHCn1NiZ*yar%e*n1n^&z1c)A>e0tV(J z6CvxV);J(z{H;2SmpV#$#cWQe)pNkgAV6+uwf&bJCey3&a9=FW&ic!xQ}P z)%O@f20VT0p_iu@d@ffvlN3wtPeZe!_mA8ZU{3z=!wuC}(>SL_WZRUt&4mjWu+_Q? zCG!_|Jk5W$(?e+c^!@qavgD}D#v>l>2Y;(5FFk~I5%CS84xb&VIezr0+);;l9`ytv zEqOrMFSYbc379@l0VI+Nve$}ipA?|G!AH2)4LR0j8HUEY&A)wc(??DvyLXY*wAMR- zx3~7wY_IhO>|J4pm*AP3Q^+HHHrKwnHDp5HuFN1vAlamljbAiX=A8)Vbac+RF7qe=(gdA!1-_y zlEYvs1A=&|P(6+hl1kqn!ZLjy_D*x)N<}u=h%G%vBng0~O!mdPo7Jtbh$9*!;S1Gc zU2l9H$SeKBAs?_qhKioAZ*kwWU7`=xSuRHPN@E_=ESl zD$Cz(F3zRdUc{MT-l;oxOEL0$AtXzIT zYuwTv^UV-1a&mFWU;$MS^h7lP=Dv%5Wy;QAK$GsjH)wAC956CaUg7XL$e3LMA7^ z0nN0dp;Q;iC(M`Hgqv2U-a8jwsA-iWEUCU~_<(7feQg|oHE3%6h?*Xg%;S_id~ z#UA_C)mFTSA5(x0!FbaOBjJdJe<|g}jRT&Zp1Y%Q?@?QVy&q1tpX+qKhv|p%J(L|mWB7cVmO_*x?H7EsbD zfM$W#0(>HZbq69OuWklOfIMy+ti#5@CpM{Yx($lqcL`W0+_&r*+TUIukd zx6;M(fXXhM9J?8cG-4^oyk&P6vE|wy7f{XOwz`( zxdFlz7YY;)uPu(#aA9nG89qIPIS(q%qdwCD@KRF^{P~r~sFY{H2)_eyiYp)73CTOS zoxqom4XcKL0MrEdA(!z5kPPH^rRUs+L@pU&>FV)mT!~9FC9HcF_LqcZ0SzexRW<8V=P82*~L-gxh@QkFXH+FQhG!^$h-%hST?#^t*ViY|mg3Mb$;gy%4rgA(3h zA}T)aP{^S@2s!RlvhTkz9T+LYAC$ns0xJa-aYsk^Y7(H1BhJpwgo|ewlT@@67_6mU zyCCYH;hs;}C@|xLjszedm0bPhU}ko+zV$;AG2uOY_#X7NefS>ptD6~7dlg%5FUpLa z!i|zrCYz4+Bl&xyl37@yRG;eG9-&K1x)q$-c_3I({qpyXhCLzg2U)3ucb7vvD*7Tu zf`nfO*56UUViE*jw7^`rCGexZLr9U&%rphMK{hmA_+lqqp$JL)7Q75!_w}FlnmFHv zIY}w0%a8#A2H5l4@TUbDeuKT;brE}`KC{!NMXFwoj7jz{Q1)+Gj=k1y$lph37AoUY zPg_VShi#m@ZY9Cimep?)d1=hOCv5P(Yq9gV&)Hl(08IjAeSSM3A>qI@lAnLo&F{x| z`4Y>K5ZW4^voFM4Dq()b5H91r8U(3Y*To*rn{itK$0p_96X6J9Na_d+eb&! zK*)85T0X3FUuKgt0Q#slp-i63XtEP;dW7)MgL?kdSXbct8f#&tP-zIRJHjTC&^|l@ z<5TFA1fpjW0lg_+z*M=e*2jkK-2q5IuX{k)rZ&0!)@YHbIp$B;x#G}y1egvTf-)O9 z(T%UaOXJbakc<}?x|Hr%r;56mP~`MF3;VK6h4r6%gMRzzj|!{s#hJArW~%&1P=NY2 zKkzd3^-WEI2$avDbRs*~9g0icQ+JU$&mgyc9nRIAz-OO->U0$9^q9W1L&MMXs@xgerHg*FqR zssIP%3P@++P%JTv#9_x87#;r&=E1oE%GBSjKLBP zN&NJa^SWYJ$R(aj+_$i@h<-G%Jig|7Sk^nJ#(xKGO*P)X<(EEN5)u-CI-ZK*(^?@C z5W2xAGmB9(LZa;ymH;Co{ohv~ZO_t3cw7;>=jYF#Huj}Fw7WGyK<}Q53G!Krg%0ZX znT}+Wn%~Fkwr+;!>F2$DVkip8Zct$0&iD+(W4Hz-KxX`iWh+f>s;vK0IDrp6Cr{JM zd07SE#hagpjvLlpW>-_YPKbBnpCI@|qYODYbHoGhcQxGb;drB*-*ASUSvH#O=g+W1 z$KyAWMBMpsjm_sP$TX8A4{H#1xr*h7F&B&i0CqR@iqx_EAp}h!_>kpTw@p%7@@?hW zok?Q$e&GgfRWQwSm#_=@l%4i`A5Ic@I3(Kh+Y1adAdv&eyh+XVIJAeNK8*r5xBhtY zUH5{(i5e1@v%7oNd0p#LBgRrAaxO!|LG+s!px2(+6j2Fdzhe%=WAy={C_-Un__8@F z6FeDMy-{mmvO=ID32}lGCsNUY5Gud#3jaj$pDT)v+!v9b4>hWvHf0RZyF3CnIQgQ1 z{fz5(R#)j_1q`FF^j9w~FPk^Ny{z}-Jm8zFZ&OO|-S1@-dHel}-=#N$Em7-~%&f(l zwzGrHYr=-wLetc})Lx8`gky|5;2N<%?XsEmRRxddx9i80ytlJOeV&+xU&H^X@xaZb z$oeE|bsuZe zYeHeFX^wE{4GY_=3G0MF)!4sxJ47ca`$%Lf2UMsuA8^XOPBFS1&Z64v`fXThx-+#6 zKnJhY@2ofkPf zXBSK`%dY6dghd_mb1rby01l#NWYlnR5y8>j=ZRBC%7%U*j*c37YO-9AjDk8ucb-1K z=C_fWHVl=BM2m`!jQDkzXPh}WXOUX!6#-r^Da%5oP!T7f7RT35sQaOn%IUu%V2I}i zRiR1LH-vvtki!XVc>1hXX`_O>8`!K4S1j)2zWFB(Dg}*|%XF~%r2?(w zF^UbOpWRi+56+N-m7N=j>Cdl)%oKx$=-ON^Az6u&hJSstFvR-N2<4p3rVzF6u=-Bt z|5U7{h1ZRIE9~uCi65IAuDeAcoQ>IoD!#x$%o6zx1gz&*gY}BODSRB0CRttAMR7&M zrYJlwdSZqpJ&z(g4-1J|1tL7@m#lm`*=&CVP{mbcUICO zk&&H@XxN)5RH(=*L{>Ig*-5AnDSL+`DJnBcnO#xF_c*(*`+od>e|^87$K(Fo*XJtZ z{d%3}a~#j(c^trop_tbK`~s%;^yC14Ii})1%M){-M-~`U98_NMG$D0VR$i`?ragtwzuO2~B0qh6fpf|yame20isdT>x-0CJ;2=NP@ z?=3`+2(xZ2{4(i-nUr+a|iBnw#6@g(4z%)8{f4oD400cmL*_ z_=x}E0vKvQaoqI2`0>~LC?bv=fffk*vyD**F*=WJbu@5uuoKM(qZ(AdL*Ku*L(Aw{0Ueh7hp=hHy1;K}BnleDnjK1TmjFi_+aUGmmK{%M|{2XUZxF4Cf`BhOCzo(!gZ zKIlU=@cK|Rqnpr8SENF%qm+9xbHR`osM_kvbug`=W%ANg+!SF2Ccv361}b~@j2@sW zv>_N} z*0YVfy{F;UjrIx4S9OY_u@(*vw@XU)K79BP(6&hQ>4JvZ+VwOvG{(FV62#*jJ-G^G z0Eqkz1k>@i)j_HiG_E`V6Ng{z=%dTCS!c@9MlVf%cce3V$a9@q>5b$Hw=jdNat7UX zj8NVlnmxu9k*=R7@r!HUT6tQs-7w-p?Z@++J>vNM3^d@N<(UKztxjxhVW8L0it9v@ z00IIBbhTvQuOI{ZSSBj3N^r;6rY(TCSq`(a{lz{15qRpiiVm)EvnW z2JBjUAJ6~)bCG^>9l2L?lrL&-mCil>Nk27ugD`ek_Y13{w`jgsziqfFgM1+OiEUx= z?dQ5e{)u-*>jCtpFYrH%W1tncwlWnxVjR}w(r2qzc_%tLVg9jW+rjq-W*=hD4xs3M zq7N3t&f;RZa}ICZcfl1H%v!wdJ!-H`A*n5Sy=w{2@+R~!lxNGZQA7Mc>bj=xGs>-d~VzU z=OS=a)A3k=!;o48Fd*ntxaqgheO+AseF+&$-O*8Gzw@#ZG&_JXkGnn1>0wK{{p`f^ zn_^5tdM|NSP`Sv1rNRc&mqBnc0W3tkSb63Ln5RCNNh!MM$IHN&Dr!9jTxp1H87M~PZnUA8sYrB3|64(7q^45&-Z&{*%X4dI8x^Htb3xgK8LlrK z$yU90Ko}G<<`sXL;^Aro2P|gk$xr?VpS-5ds17W*nV~}A_#}Fr__UBqG}HYV?|1p% z-A@sLH?;L=GS57M1(JY3_@O6$~Pz5PH<+=0W6@vw(9qGm90O z*So;UP#!-K+~>`BF2MpXFTzPYIX>dq$^@YO|_FWsFG1>rdTuC%` zP8)ffc!e{I8e6`aG$KCXY(4q9&)irOq&J&QLU)O|RumCxit8UB7hb=IIH-`(fxz$;S1Yl!rzqe3FxQt@0pPAddnS zVY;*Iau-iI1NNRd$?J6l8BZk3`F}_6vFn-XJs8m!`s7o-yA;N)Vf?Z^>sXlmnz1+(b znC$Fzw3R}_MQ^gwj3|sp#YP_0yGLnonbSo7b`*0=yRLK!7f`Q+w zzVSmM^qQwV3fX~v42_S!lxvn|S-@PY4D&C*`xZZv4q94Ubv?V}fT*m7h~dG!^U1cj z*80Gf)5n63_?#-z$)`1Xn0--f&D9m|#v4y+5*|O0n&ruwXcm*_s#bgW*$TmW9Y+N2 zX5_r5B3D%HMd4+KV`tA700$O>h*^<1VVQxS^ck^`m`{McO$qTGB2|aT2-o}V z*xmQRDP)RhO8O6?J1Sq&Wsi!xx-LA?@1R2_lcc;U4wnXt95obd<#Km128LbHmwqs? z=FrNq0zE&U)#1K5cg2|2(}L@_R0iv%y-YI5l;_l)et}`j=UcnIOkXASS=#KNJl`E0 z7b-V|diMjeSxsMa(6j1lae?5G5k_(r5_w>8TaY%fvf+}vi7BfglK@6eH7ITBpDd_ zP<2~l%(+v>JsgL2raskYVbcP1g`32P&(W!einy17lv5C68mXyQpL|yi^Tss@fY_0& zMuOZ4V^I;rL2xHb;Rr#-e4TKGD!N9+o^q|`gTTrAl;||!`o^*41}Rc@9&CXFCF@R0F%htMC2$p)cQVrxc1^KQg6s`LcAQc9Q9br@P2i$HT)$f+m@fXrP!h zDt4EcV-`7@-}5@_lKMa2`+{!Y-t3&=evug|Hcj5|cN~HHoj8K#z4{s-p;_`ol*?)@>~_Vpc|!xI8@ zzlOav61Q_}gWBzr4|(<;j*^j#W*TR3XL1l` zc*Fkv``ZcthQrhHBFw8r&z8MJ?r72l;w0_-u?;f#&0zoqB+Y6z5Hm;2!4|&e7dC;u zfbN#ixsd;f^(yf^heseWv*44KmL>wdkTSl2o)NQxP=z!O?I3zy#3)JBF_Um}h8C?W z5cXLJX@Oy0jhYB9c!7#zu#ecCi-OKKtH-{p@4b0sd{n@}9nl-3aLDOcK6fNiJA6oB zo%=HQ<|&WI8*1U>`yy3ndFq}YKWbTimzujUN;t~wmDc&j4=pGok@m>1h;lPz_x_kQ z-XMB^KKI&w-(yEs1zag?aut8Q)$`_oQ53Xm#qkjV1`8g?bi@h{EztOjdGs`G@?BK` z&TgdxhFuGlSHzA?4cwk%7Au|5(n}%>Y&V z$SY4fJ@fEp*p+?CUi(6lD=zPat~H)^JxUHDlT9Ir6o*G84geD^u@7TM06d^+{6`EhPk?&?G~<9%3Lf`1Eh@T;KPLKU=-9IdH@ zk|>4&u9}JMbg%Hf;^%)dLjGMV|Lq=DS(goXIgI!7PkHlyyb5bAq3CI-7tCGjA|$jd*$Ro|wcuQzzEq@*O{ z*saUZHNFFM>(RA(HByrSI7Lb%1+%Xkf>BSC{X-S82YF9zl+`mId z#SFcu@R;N5#~!YYSM&)r@H5EZ+_3h->JME4w(loW#b;TU*K#H<#nVo+ZQ z$T6yD2|-Nk#vqD9sw#ZeArN?hNYY{*3s^~xQ3)S}&X#uZ^Ygz-k?TOYci`$jBCvO? zd-duL$y(S|fSXrSI43R%1!ggA`%upOi@~|~0puiF#2{b|B7|7l*ysXMgJVVj5FI{D zbTZL6qQ{~lUsr(a2>{d;MI(?F2&u)nUcPJ%_?gtjDc&pbpmSg_EqLyk6wn>LhiBto zz+AV3d;}c<#=w>9mX&|I5d3;mi3(|{NYH_bQ>WF`geWxg4Khle)bns3Uhm2ly`ACe z)vL3$t*w54yayVCB1ej1a&NfJ3fvWf2q~?RXnc6 zpUW;dPm8pG;TopRj!pEI(%jxImWinj)=R?id!YPU3`e9#M{oW9v7P_Tr(^HBjxh?J z^rxwAtYAB`dF&!8(W>|(eh5mEDBH0nywHgQ(Ew`h0rzPRzZZe&=ibqiadRSoK8z3_ z+q2eG`^n9X#&(gZ0;a(y@C1)MM!@L?%6tDLRf+XAb<{-*w1mLIcHqXx=bptwf1>BV z@4r!O)%xf{wTxP{Nb@a1Y--WZkC$-Wj*`W4>RvK})J>Y|?ry(J4ql&s|J*UZn^sBl z#nGEII~2ZX?TP?(<2Zmcv#*bx`^p{mV}@hed*?ht!^izF|3J$L3J@K?mn_Ei-Ep`K zsG_s*X0O@4SY0`cMrXMGwtE%Zk-MIk{1#~)AD{O8^|je%fXfhL*aT@ig>LJ1A_@%S zyD!H+fV+qIyRhJ1a$bE4tR1gW;=eGExR>LzKJL$e4F1Ei*Hem65QxuJW_I=*NQ{^% zD?IbwUR~JeEuWz?R|UUR)U|IO?yAfDRM(xh(^NRkSk@h0DSNg&M7WOWiY@o6wKr@azcy?i$*J20n|(=t=Ea+3^(AOpAtOv z5K{2lMlYcYqNJuCnj*9m1hQEna2g+(9fRppQ)2O7gK(Cv7qKCy4uG;u8={^%d*FrYTb zH~SF70y_giN|any6GH(>3qN`AqDU^m2|juMINcF{91L0}CZ!LZ(mQ3Z(ZYTB79Zuh zt!!_yod!&+OcvJzVj$)`=yOOG_ynT_)`R97@FT3|$0KmOeK{zv*~0gGeyTTi+xHI= z*8LmUq#PA4B)^A2wJ*t4+9VJcoav}s+-JN7*bonhdU zbol4&#%<%;e=dLTw0%>l?Y;by`F5-*H{Te4ORR7!JQgqP(ed@AMbf=<{>-~F!JDX$ z;8P=p|91;>|Ld)2#u4LDXWarKI{vzx0Z<){rf4h!-N$HDkPDZ8ZxJ$h&;*{MT~8j< zZX!_);h7;2cs^GgEJ2{$NKxI@j=R1Pu!{_GNI2|(|e;Up~srUn?&9w_ss zdG+cQqd#VRkIa@&U!Lky2Ofl;g95i&rNCe=%kAycxK_!r*+VC*Z!{G*j}YbZri@4- z@z{iTH7}5mG~TE=v{_l(_-ceI7c{(AQ<7KzTN_~{^+~Hi4r021Wd4LlMfn|{uL53h zvU9}cpuMnz#_N*3;x+}3YDOA<$~A=@zA|NI@`l}24HOo7`QRoOrlOK#v>}yR_F8;n z!M2m`y0Gu$5)s+rEob6&mL-S@|(9ze|cFopz&wy3}#F%m}!$On(NRsO9cyxAT$`8XM@zqCIg(r#^y^fU8Ag@y=01%#gkywkxu|PIP&5VY2aDe$9 zYdWw=+unfN+&N5-2^G$y-Kr-Qf8Eb+{-7?5bbY07EO-^H*DMp z4R}8!)hU81?mFbQCIl>$C?qtUPARs1!;PVwAV;QyUk^h@wEL_Z!0Be1e{Pcziq%r-mmx>jc61-`WfR zdun)3nazG4Jm$1|>6T>XfCodx<*E87=7Md_6jWm&$B{Ra3%0H*rX=4vc=*Yk{Y7@& ze8hvYZpOl{Ptw08%c_+YsP4o-witef4Vao3FMiBSO4^ADToafn}sJ{Y8Oz31& z?{OnFaG_Wj_uX)!8)THez!k-> zJ-FhxX+v+WUy6xlWxjrVN4~<9R)k>mftmv|G!D%0;H0u_#(o^c$IvJ6wqilYd``{l&!czzdU*Z(~nkI zgIsWP8#$MVg%JjR4bvY)@)HnJZ8&{CK_m!ir3H)gnP>EAHOAr{(T` z;L23LS;xa&f15gx0j&OMn{W*70}^owqb~|P-_I%I56(-(CPevcEwpjAX{?=hQ;boi zy&tDbd9p3jDl6jOBxx2<{`i5L#$&cU)B#DpG5349>4Si+yhtX11lah*oH$jLm;Q~= zzab|(QV_oy*g#Oyaf{g#-yv5Hjk!egr=3pbr&iRStFyDxw~ew<0Fp2OX2)ZdX$I0` zs~lm2BPiM|Kida@=!9W8Xnomnp<8})C7?vcaA6x#@D%ag0{+a;%NxJz!_)L0dw~aE zy-$a=CrZXS`>*~Lm>@0CSfRm^y!)mWwH)n$(49tBv^2Nyn) z%!Ogj!)4V&j9K)@K@{;rUBfD6u`b!l?Fo+^dlC-1z94={OP43;|NJvq zQ(b+pCQmu^>tx@PWT%0vaFuV0DL4bsdk&DztQLc(@SxavgJV-M{dsrzokiwJ7{jV- z-SN45SC5_{D)9F0VVpl~?&_4E0{l1?6N5L@TTI_H6^5&2GAg6U$hV;p zx+|uQt$o6)Lq$<34CYVPRh{_u6I|g}Q&SAzC!ZO1o zw>;1A;pvKhVxn+BbR&PtBiBFMJ%9dC$2TGSPeAvskf|$X2D<&3EE0)t!BrA?fY^KT zsUxfBKfjZJ=6nA7(f*B`u)nPs!Yceg=aj(9sKxH>+$j&F;3UBc~9=dL9 z%gwWjG3xn6V<|kAd=^>`nWLAG;u?T!_5JSVgFm>yB&Ka|G8~Pt-ExrKjl!%H=+B|#Z5Y+UZ4T%0uIM>7jBDVfx9uDx<0rcL9s3`Vd30XZ zB z$F@tCusf;&GSqR<;U+qdgm_*5V@hS;yUlzcVfQ%O3@c`A=!bn)d(cvXJ$e|xay|N> zfMgEJEsHMVTCcR;JUo}kt#aoC$S*6*;1EM1MJwsRtG6jcK7;WxV8bU423>`#M^BeJ zwd0ec{#z0Q9-j+3&^`wP8$j!*G|WyF*y~F-I;L7=ivsQ^m2UpuFcu!6K*d;jryz8D z#Ar)1lQ{pemuJ*}Z%Jj^p1*79DcxKcdmd z^`erJVH70jCXb;45Aa!In6DRi-x@KB_Y^jt0iTly{gHA)8BI*uQz zqH?>fUl)3N6Pn4?)Fx9c@mX&cmrzVplnaJ_ZkRX9jqA>ns%)vD*i6>7BzAb$!Cg_E zGvauhj2#!ZN?4l5+!zq1-N11_ps;!D(7Wx-jy-??kAiO~2wS+mGOs;m-w&r5I1t-! z=WkdvlY&}P;Y}J|q$v^A8!vXZ#)zbY65t5D16lRsTU=3WFm2e;TbPNa-7pgMZg}&3 zax^h91)Uq%6=6$}?T9Qq7gFWo_&hLtx&afmOqAD4kL)SVaUbDEV~mcJuqURcr!hYD z+y8TE(e-O@p*A{50xyXK#m4f>Jv^h?c;_6zT{6Ecy@VQwtkYsbnsc&TGKMGJf!Xq; zV2amo!51sPg<QJLM)C&rmHBi&x{TpJ_OM@lD$@NX{)>xy?S-*RQue|~F5ywP560`f zJZrCvLx4E5=aiXNN=!(QK|$6dK;JLlHf;R2C@LVxT^eF6wcptcItNtFh$Z6Lck^gb zd?zI#rNf{A?WL^ePaq?=PVx>kHMNak3B0bOBUV#F0*pY@$a~Q~2ko3hl~;rh3vY@T zEjGw?-xZ)1z&UW^V1ZieCo)F)OVk|G1VP2ZZiBq(Y#r5>KsH5WYQWc+7|jw1+O`n4 z?QMyEdP4QDG4gy{u;^n@a7+-z3BbpZQ&0mXsIha(NB^gGp}cuI8HZeLI(X2B;$`YQ zbu^m&2C{f2Rl9u28;Uu*F8L2)%JULZ>pY9kretd z5KNLfAN`||C>@HmiwF~(=f}2Pz73&l(;idChUwI!t#ix2S}DWEU!i);HGY0*@e(E| z7!Nh32h9C_vpKHWcaWL7@RH!aa7Q_V^9Aun&eD`{T()D7KD{Rfxw6P~ZRe!7-wmA84HxhU`6=u%EfdOa+5oGj552V;OGl#zOZmG!^m!8g1Ii-2T)foVKY z@L^3nK&puGW5=~L)5M$F+w0$}1q2e z{aMe;8^<aTQ6SPe-+~br>w_ir~ zZwl?Yv$kTz3H+m= z{h7VrkO9m_0tXlVESfb#VXYR_=n2f6QM6)d>f;*}V=U02IPHRrC2 zVvO+Ky*q>jS2;(<%tg5m`{AvwbBW*SYG=e6haLti+JakPXiSUJ6Me6S@{#(+MzH;B z;0xMRh;+$Gf1NTn*N$-`gjDH-M`6KLrK`nqmNnkDEB&SHdqZ@AG)GLu;ey?0u@o z+qPO%FD=f8g^XwtG#)1jE4QV0g^cLcZ8Ca_9haPo2u1XyeZ3-+pO4%-FLwA zF1!U@MSO73$y}b^ICZn|n0t4QTiSUUK4w9{^*m)c(hbc@i0N(MTcx==z1CPM`GDh} z#rCAgaQ{xKaK-y}0@a=Cxzdi`>g425q#N}2xpOwpUwM_#t(zHGjZBLF6Jr$*wpbFlMH*R^JbPqAE03-hDGq7;-c~W* zx92k4kxG-C_=EFO`TMCoS+{Mu z#=hvYFrT)kUrfP=BI|ZG-0nI*;7*(Lbyy_bX{*vrPteY-u84NL=nR8s1rT-xEh)|6 z0A}OQ^*-y}^E@wIQv%H@aB6NR075H3u`*4-_=)>qYt=&1ItuE~!bCmmi|7E6(I8=f zCK9V5MHwoKp0$Ur1+-faKY!7YdHvQasjL4O-E$wJVz=x}mB+$-Fkh&}G*h&YzGa)R z7*;66u%~?s{t5gzZRtoM(0X?a-o7We(4Di21_h!y>~D$mmsn!`O{J%w4f@?xw+kOE z`e3E87+X8I@$jmgl2XTnOL`h7?MN`~JIl?Utgkd=mL{bcl{m$kTr6QCqrn#yVbqj~ zBmB7wIB1OMIW@E>e7eTOYSP#=!m85A=A(Q^hq(%6P=AY?^$fq$eot0bqqteI+dNGX zB5BN~VmCfQ5rp`ce~ZyvfZ;4#$>29j0DFySq_N&T(G5x5OeCX)gqdOE4Aqh`vgZzH z<)U0(-fN8e8t!-mNLr~4t5Y)yyi79M%W}Y$k|fm=X$t)v(2%OZqj@*o6MH<;U(5r~ zf5_8~uo!D)%_xk%YG9N0LP>N>&H*u$UzrHN_ z;wCZGr{N5VN;P-A51tjB_s>hsP8)QoC^C95a+8)`y6>iO!rompw?#7S*gWLn3cweI zl|a_`fU|@L0EHI!YdFkUP&1btRnm&RU3nBb5lWOhMK^g$-)7?6P#!(l!gE;vVn*z} zhycOWNGq57s_UMwllHEZ(j*BTW9XZ}ONVcNWLaekW!sv>JbXB|bHqz64g$8kmL?4P)ZVVIyyiQ&|B07QB?J9<2N^}{oMJ&+psMhpC$RkTTQSHUbdNPFqZKA&wDC7aS zNzHwE?k6T>QnhpDx+f7*!Le&uuKee{gInNvZII$;lxAk08*EdZ>$fFofkxJ~B3nWO0lGZ=YrJrxBF^av)iKt}-9Eu&eTH%+*H$Jdl zS=evH#cy!?C0@l&t(U>ESwfayE*yUJ_f@RRcT^IS6<#l96w|aBHck@cS~NF_jQ9>X zsZjdR`huZnDmkjZEjz06Xbi8+2H#1M8edx9ewVVHmf7X~CtBM|+5X<>(t3emq5$5Agl$?)Lx6i-cIpMAvsbLJ=cJcpx@FK0)WF=cgnxongO%eC);${omL^jdBd?85Pcd%c}MV-<||7rRcg8@p;PN}U%k^ldr z{u=ik)i{*mbO^Mq8Yvz>%a+}WC(cv-f^wyfON4Sm`KUqvxRKRn+oesny79>Ab4Kby zgYUg)-~CqtvcxFJ?hU1h&6CrMA8vS&9ZvmxgS7n)Z$<8fO~G<-P1TRwuRXbjX)?Ih z8DK`Zmk&4*d)u0@?WP-+>5Wt6Ygw?Wtp-wzOxmqRQ2?8-24AjAJDIfDwd z#q%P^_S+ODy~Rqd)~^!Uv8kqxEA{A%<9M#^h+?Gps6{;Mwt1=2+mlI7o633pUkaUB zU|4I*(M<0l5A#btU9WDN&z~0TEhIg9B5AgFR_Qxv@ae9Ya>kPG1jo_?qL7e0CjwlA zKLVBCSq`iw$7!r?7A0Yyl8Q!iSliNQ9cR;?(jnyzE+hFtIZLa95~C)OCyowRolPJw zfhKHm?hEoAMM(P9(uZvxHlnd&Cg^k|-TSMqC!*XfcKuVzqNJc;@-g{v7|`I)KS623 zx`mph<-U+qWZSxir^C=jvV{1S`||RX_m{;le@vekiu)SuIWs->rt|>R20>79hhp`S z-{agd-VuR1^XlX3pS;$Sln7&-uz`vz*qYIbYLH-oRv~v)hihAkTnf#Rg*Ao1DmpXO z`F(xYN%-QvAYL>9-?POu&v@$j{uEUPepANlCV|;Ta|T4$0a%F)wtQLGz__4fbCt1nVfX+vlQRB&xLZF?9dWSL1Wiyb&8I=fDleo3DDuu&7GOGj>5XGxQ! z01kO;fS`A=Q9-KRNL@hiy_B9XWJVV-;*Xr3W>=;IV-P#Miz89mEU@k-(i0gv88!k4 z8_IOut>zWbI3wuLCfSfQ@5 zA^VFda=XJIN8S>BH3?%qQ$`%%EW@I7QZ)fP8ZJUG@@)8dl+s7XZR%|_)FUKd>~SQbY2rA%?N-$9uAkz;WXq^j>pXByC5XPU6Vxa9@TD53 zQVYf+On-kC*kNaosiOYQy)!*c@xg+tYU&+4hZF1Xe2DjntsUj)^?%8Arfjz=f=80p zrvG`aXHR2S7b{b78Xoc86Fg$Hk^Z+@QjB<6{W(hegLlZ@w&Xml60=@+#j^B+2W{+_ zaz}?fnL#~1-_4fTelx=D$GeDACOaBI`JXwiv6KYE1~Z;Wo~~RA5mEr4Sp&-qv<~*M zj;ouv!HbbxyOx5i&|n%4goAF^xiHi@7CgHSyQN;gr3JNTW}_HY4UPM*jdxfyv+wL8 z;2sts8z3yK)O}xnkvVJ~8FP9c9(OVIZ&wsetteb^(GBtFW z@Xm4j@n&rX+QjuH*NX0fCLu!_!tGv<*KlKG_?e{e=brnXs!7lH3sC;hwq=9uS{}|N zzbtpuq~msBR;`ux334haA4Fd$?zl_BNi?acg~k?)IY2}>4v~{Z zeN+M_UW>d4^xQy$F}ucQfI4Um3*MP|LfAF1LhXaOiXT0%8Z86EF^o_=SfI#v4FrBN z+Xc6>rnVLqICNNzH!ChF@ zp;yvz+%zOhIaB~nyzNkH5kyqPJS3xQ?^cNf&<`aX7brh^e%ph3=4fs9j?Z8@cRhCM zi3CB67EsN}*QZZEarEhe_Kpsy(**k_jVEeX`DLDNRR&=pLaJ-qG zR~+B>O!2?%m?A*6g40i5+EQ<%&?$5}N8S;j4v<8bhP&%oG)uBH-96@6$POfQc!Ft9 zl)G$D?tZ4Pe&o+J!2y%0(0l5-MHjslY~Ovq48nuNs2qytNgmF$Y+E7kpztOw1`esC zl(XwfKZK&*#_-#klliPDGnufk6AwxpjS(CCG%zwR%#CTzLkUP8EqMHXXLdy8B_=ST zvhg^UAWmr6n<~GaG)=$^e?`GZ4C_tU4_9=C!UEwi13n)7^5xdV$DLaIVF^9v)%Ha& zo=CHa*yH{;oeqo}@dQmu7VnIkK*EyODIt5yW?m!HSbU^lC zlgbH$6s;|$`8D6qlr91T#draOi3S!Oh(LsR{M`4^>nhusQm@wgRD~9bp%!dp4BYRu zUt2FvF@j4;JOS>@Eq6uRQC_@@OVWtqlVH(7q;5;vw@F{0OyG8V&#zTxWSH9m@;~Ty zTRAtN8LhKLY{@-Wv7KzVpqrDj>u2G%?@T>%%!EyT=gfGyKE;lMbHC=e_wHrTT^TlD zEROQ`zn7Og5_6Pe&PHL_WP@+zQ5{B7Qd>$&ZZ7}qQjYqAa|0$F9CCzwgaL`X4kM3h zI>Hj>IOJ52cZ+TWH~ETT)wP4=mp~^?kg}^mEj2@kJ;*}Dqz}_TprD`TCQCYsL0dqn zt#}!T7@7u=5{E@ji6E_U7NBqvFgl zkxRg@S&V6@a;Q5Mckly)P|%*p`Z)+Knh59;>A!zwc>EghXetK9Q#M2hs%luFeuVW4 zq-TG+Xwh>CsV|Qbo-uw)_wBx?GdJo%rgkNNQIXNH`m@;rj(A%Y=-;$q-N>)j~NlSki(4$@>7Y<-u5O0liF94W>0s~*x zj=FRK|0jJG(h|Y6Kx7aAVo4rubsznVwTS4qW;z715Jtt_LL_ zQ?wj!0`q)m*Ky}zMT0c9y${d53G}4S$OF~LSv#c}3|)^u1^XrPFw-t`o1t%7N<5?M zL5sq9RNG0P^RN;9cy3;BFSdB&;P-%PKNY}IqzG9)H8*FI@SGQxvVTJ%W2#;z`UQ5P(0qXxvtifSnHYh=fLJS<8!guq+u^Z!W8IWJdD`~ZV0)cuHxWP3O zoCGLVP-y6=Suz$L$P>X2v4vhn&_{Umw16RLRPMsN+iL*|dz$H{)xPfkFW;?x-X=J2 zI>*S8^>)4Qt>ByYlp+`XI7%6nez>;-w9Q5ozE>1Re`*k7j|qM;XJIjMTYhC*?CKc8vZv;lg=El2~nCs9o6;l+~HZ` z6e)A>+N>%jq?(ui-3*B)VtFW5u;HYP{Zb8owDj(Dj?k-2iS7YqPS8!?vHQWO1(V17T+r zF$~{CSl}K+3x3-6{bRvj-SEw{Zj1BD%ew|mK*Hm?RRq018Ema+plB>3jRV#s#&wNc;oW21wf$U;X8Ta39c%>|74jK6bSIqh9vC z4?}6!X?7pD^pz3u?48K=&8VJOjAqR1$?AqOpFhWe11xYMj$jft-jdxdA4Fd-QA}Td zdvNN@tuBfU8&0GvUJQBQUStmWN}WplbDQ-OowDO}A+M}Ubf6~?B7(1nE8D%`20uW< zg5D*O=<()A83@d*u`p(>M$5^1WKzizWuV;+l;Gk1pLKNg znWCEK$6=^R%p_#;^m1#E&Lo!mNVPDY5jH&zrB?J*b(X)Ki^A23pbqHuOJ9e;t`4|) z(?w1?F%!4IPXL?l#7n8{!djF< zj^t^a8>|VyadDIP@uBodW1`;H@ZDf~ZmzZo$EfIZ0ZVd^J*R*GCHLlS+Zsk)bj0zG zQF|nP)~;nquD9nrP{x7B__D)EMY~6eU6GzXYY8AvmYsXELTagV?~yEYptpMRtXyw~ zU8A5;jr{Mz8O3coLcOsP*0as3__z3Y;04S+@~0hXz!PaIL|qNp zx_1u@LS8wQa;nlR+mw7jdl=E@D6OYoKRzkFBa+9=O@VT0c}ZN-C`N~o0*=ig8r`sS zuyBhEC(mNsve_5qq)ebvj5q)fo-_d=)B%;O)?=59*9V3 z*4E-;FyKt8p#3sNh<1l`s0*BMW(YmuZz3Q?>|d19QgU?9<%;AlDclV%;f4`n zJP0sKC_)#UTie=*awAsrXVB%(y?_+htyuKO{IKw*fd+o853|%-d4ZC%B zn7<88-O9xe0q@MqV@d8FRMgcEX@kuf zp6izjhjjFrGPO`BZBOs>sPU`&$=?GaMT_aDHLJ4ELO zA(gFHgAoB&59(zjK@&ZZiC9E5&qtUf7=Axc9sgN)-vO{zjL zhJ(>UHnvN#$&GfTZyACj4_@^Um>e28v)S|8e^{UqtX%m`lqEZ{ zeY^7Eb$7SN^3PtXZQI6xrj2>?a&W9Eg!YL`)a zN7opGxHbZ^*ZMHjG3bwqp!{p*hGJ8pTPj3!-=3|SwVh6%%KhrONVJ9!f$}9ti{MHr ze{m^S)zqwS0$MCHZ3(L%C5w{Q}uHn0b$rwdSrC5W_=7wv$2xD&=8 zm`$Ut5A8(+jhDSDjrJJth9q0^7<+-w5vjJRsz0Jv|C!_jUHv)UJz15Xda-)vBY43J zK*&f9{Grr28$5e=^m{=_G(%m*R_cgz<*|U}T_7ZdW=Nsn^b>bPnHkLQirgkn{;+C} z(ZkZOEsh6Urm+EMdR%E2ywegAU&~@<)UP2&vWdGnY~7U&EIx(DqknAj``<0HyH) z)N{~9DQJ-ossfJ0j1V}F#!yfk^g>&WVCPPFek7PXl_nyBK*m5k&!M+)_`(4RNzLvu zG@H2V+!bEm4@oB`-@WSqNW@G}u=in9d~`q;&T3So%U14+%gcvWR=m!tmp51p4zfgd zkB+=;PsIJ60JufLU4id{*@P@rwI7-`LGMT_*3M4deN4}_u0NZ8e?!7;qTeSxlDAp) z^yx$0HkGx^COkuFwx|%N#e(zF?b*T-cvSH^E0Mz;9y@=!hr>yh4Kb?35hBm`+RvU- zSo4nu5?w;}1Cg_igCv21Yah$=m1+F%;(2*ajk1fmV1Wk4^~&U4R{ zisD)2_YfTwq6LAwK`c{<(lWr%uWxc|v0@MXuwi3DStl>>*RO2w{biQtyW_5<1Z-M> zQOJ02;qKM*+iv-(q~RCauyG^oj2pm6Z(AlJOZxgVU}bzZlbxw!AWPw7tt|I(BSoMs1Yk zpbKNiPc4p_x%#^zI=)vydgf1Cyj*gr^YYMo|97b?v37Y0hcsY&kt_gyhWvF5rT8>4Z6(?kKwE6^9Z-@%+n{6h z2?FY17cN!R@2|rCwC-D3CH;vE3x=H#&uc;;AlM<8I%Mhz>xyV}&BI;rB0*Id@Dcu~ z80<0%=>-Bg#HID?#Z4uSsZiq@!o-G*4iM?%CkdDUh7;~Pmdg7OeJmt5Zl;-rlqU3X z<%ZS4dF%kZ*axqwUnLllR+4{F3yf+l`fjpO&w*|f*4V{|um6@J8*=Ph*`$sZfTJLAbv)Pj~Q_Th1Z{!Hj z*(Egem3%>d#Y_Yw0%f<~ZFb0cM3_qX^4O%2T8zkFq~MwS^$@VkOK>D-A)2i7%*`%j z93K%(PhSuQ%`CB!%z58Z6%&tJ5y?kfZxW5OWtTf&BO+M32Hxla$2@qIe9zx)tZM#@ z{F|A?RF5O93j#Rov*$gmuiqKvUxz+F_`~-skJf*-?2-w8m28e;Q=A=MhK>t|;tx)} z%0B<$GC{{hTF~Mg(2!1CtA2ray%9(TvIb^UlH@ z&R!eB@IQ2OPx2P;z~qd8Mcj~+AnppQ>JVY(M|wr-oPDh>#_7*d0n#yO~Nn!^XMn9~QXRDu9uU2_YoUI|z*22S)^ctt1YRkS1gB09BARu%;()-qAtCcU^2@y64n+10+j`nBWk+n_*CtWvVZQ8HG#vBh>s| zn5^d-nNZ)>Bvgbb0506w1G5OI867?RwxGz)k~z4pYgj$GHIC0 znBI4j4kHsrEhVQ(oIu=QhfZTQFl3FCBguAq*+>(i8UvZf?Gh59hQ%Yh2@FNU&&D2{ zH@n+^%~n1A>H+~7`|W+c&6X`6z|1;w!V{WHlxsPy^wMApd|RzIKr zOucBdK=Bawqush>HmAJb3BVD%LS(0_ps0*g=vg3;^Jnwc9d}2aJYZix=wjC5og~Un zV@@aOX;stm==wM7kguI-wGeV5+A%H*opgCB%I1m?b4)wds6{XytXRi)c1p5@rVH#` z{37SE9J(J=5CP>&BPnvzdOyFuDL55YtFqEu(ji?Nh%UUQd+QDt0N!3p69W4j6wS!@ zvK~xVf?MuZ&2Cx+Qz?k;W+Dp_ruo$6?_}5)0&Sn#H(__OFdRh97Ke$g>EGXBpgZ}% zl~FF^?VK|ld3ok7DIyp`1?Q#ltI`_-MHj*To03i$s-J``GOR)L|COGH;$hf?QD0vF zZ)iZz^*t`e?U6`s+Bv#@Q_GF`Vj;(BorDHVx&d~fJ`>g09PBm@RCLM2EDcz z6b(`5P;1Y9>BxpU%w&Ig6mm}&L~l0cydU*-;yjRU8}w`zJFw?{>K<-M8=NJ6(nnNitww1w$p_iG}_NwIn3++41%%win_ zOZ%^iDb{ z$&*H5hpJx%`w9ytI260F=VXrGAIqTB+1<^n`Q#uFM!d^OfH(hkL?fh+Glno^K(o6D ziyk^3$fOav6RFP7RiyPkzj_68>_Bj>@}PBzYfkEXyi4NvF@MkVD}?I|SB z1QbAF9dW>QYzta9g+jb<@B?W%H)LHc`QRvHFIu`I_CwIEqp4p&m9#}@S1QIo>K6-t zw)x^@&z7}>0r3pScFr`LBBLCGCqn3FM&oVR%y|;Bimw0J2047iG=%^n)-M{nIlm`3 z0uXeFLcnJ7slfWH|Ee!M@v%Tyxddqz|2sgWRSJ>Dx%7OtL+mS-_NU(u$6@3_3EXp>a0l_p^FPBR>5@aC+O*_D#&?K{|_N9rdQ4AUTGj&Bb`|^1`y?K$y zHa|!?0T&l3*0`LPmCjEU)c?Bk#KK<98=Xgwaf^jpu|_qIBx1XV5YO>m`BA=Ot~KO# zh)f#E@Dr^tH|J(& z6D#L|Du_t$id>o76Qrw;S7G9+oBhfil+>F@YGBC{pB?4dOS8&2r~WFaa*UoyMLaxr z$byGi^y~-$DX?0G$se9x-{XlLLqpFjcnA!SsuTYfn$6i-DP#6()V%6s zYiliufFZ1N)P9?u2S)VcPQXU`!RhC0B=Ut&&UUVkYcixS>%|NZZM1ie<<{Y#yqJOf*4quDR5=b_pb9NmcNaj#tXw257=*C`@F{kLGxLU!iMskrjmO_U&@_Mlc*6ai zqcrjHc>lyia@u)WBA9&8-QK^k$8>s-U{O%;yh5^Exyz_{@Xv4iJF%Pw;fZM&f-sYH zu1vFI5+F-pee_#UFKm_?^LiZIGiDEjZ~-B5#^U_PPhX}}MFEOgU zmSU;-)x_mAo04};do;T*G+?u8;ZMee{~`-|rTzvph1?8C_neUXkakvl4vAo8~` z?z$)jV~ECl-0(OJIiln=82hWMpu9FJ= zQ^b`PJK6#bgAfSK&G!7>3N8&DH{>kQME-<6ewh1D#B+oIm*wAEaVmo896uFBp(e*j zGw2k^6i6D_7&Z*ynz=3Jj?o#d!e_v_idaW)HFz*m|6=P)N=Bu@+g7crYQO2p{wAsK zV4ZUeAf>o3ifmVWWBlx!sGys8!9kVTs_n zOcU`-U(dK>(`Qq~K*JB&tGT-|JzKyU$-C0kHTedG)!)D{=Rfp}-NM*|6<{&lwelar z_dK#V$)sTLa;if*_Dul?1U60S(8#(DwO!0_ssJ=pW}P{ z{+MHqnQ7jm z#rm$ZAiK&ee`sbvZj>b|`!vf7QK{+av_`8rF5>iV-A!y9xp6qg;6-g* zS$Vl5`q@`Qyeij;_}qMH6Q)6`LmP|_^NQ{r`F^9-eqI50-AJ`$rBtLbawet;jF#XFk#KW9g3pq4N3YXB8Wo%H17aFW@Yd(L=gts*dcfPYBdPq z@g}?N51<>>BJ48xwUq@z1_#`H6j3Q@{;NJUKpG*^wMQxCP5uY)7xnl=xA4W#l^#1Y zu`%iXoG6R`*z2Kx>zT^CQ-(u(<~T4{!7NCXi=UCa*hFt|+TxKK8uwMsYg^yF?N2)< zJ?BO>w~&0R!P=Gib~4ri>u$%cT~|f_OwlQw{oQd4H;_^}h-Ry-e zOqW$~Z~_BXCcDk}f%!G`fxWesW`2pe&K>^WF9Tat@lp+@84cl2vsuzr#UHItIR5x- z)IQ6)$-s>q(eDRJio`$)h7ZxLqCd8Sp2XP^7urkTnUZGW!-5yitV&sTvnY9Sagkg) znE5QCpO-*1hklB@HbUmd+KERTj2nn=(_lEWMDTTZ=E1#`5Opg(`W&ctZoZ4Tg17XL z{tq4snWI3??)a~hyY$?4Bhy;FLrIJ!Hl*RuO2~`4d!ahkBN)w)mu&aq)=7-(hiZKj z&~y-t8}IqXyRRQf1;Wn{fEZurB(xRY48Zu%J(MCq4~keV6dU9`!Ay??MSyGz@J}Ci zra1<#bOm6G5i`jYCI2+Yjb2t?bSKRR=(>ZTKjB>E2gQ+Wb)3fH=RX~Ja((_ zYK=n+9r{hE*ClYY$)gBu!JAY^lCfyb?0U4YcTxFf=j4!4cUOu`peDdGGPdN4NyE*) zX>dkremz5+(&o~01%l)%lqQv|G!J=pI9Q4VhHTB*{FZ~ze zQ&%)f;@-11)J!6JUMN2uAaU|5J0;XDbvm@Dt)pY&`@#p35;S2=>JGw|{62Mwo{jBR z(c8MUe;m8gh}7183fSZwGwYKtZXNS0g;;9lFP$Qp_mehdhLon}-n+>0hQ`@1E^}{3 z+TX2xjvdd0-bY2GJ)rW;q`oGwHosW5z+s;_GsDPQmE+7p#0F>=oC!`6&9rlNvDsdH zISteS;v`1IPUz2$!D?Z!A4e3C5dgsZL1J;y1}#k~t+nEqBo@uqDAkB95l)CGR@58h z3ld+!v7Ma1|Cb8@z=p(g)_^j?av&5nK1uDx0ojOV7d*0yiBe}+*g>bBKz8gn0MIE+ zpUL{d!>v(KRb9qh3#?5#%yu1MC5d8=PLz{^TjSOU?l6sL9z8yVe~*uMt`i$WGsiMA zqEankrir!j=xwXM`97zu8;hQ;Uj^d%h|=_%!I;vis?yod@TbFN?9l)D!1=Ge%qPy{ zr2p8J+>HXa{VzF}UOiY|E7`;`E5*CB&g4MIALau{QdSx}W$0$L~9#eg4Dngc^MR z1&s1aJSPQV)%YFkn`n?_Nq!1&_m&W1NsPesVXVuA@{6l;R~c+?%iq# zFhL)u^fJktyNhnLBT+7$YP>mm$1!0>WMwtN&(g!wbL{(%@Cb+c=Ru_WM&`)v9q+4r z6AmZWjp691=|FU*Gg|d<7Adu?qcJ9R61KU-`JS!m>awH|7Q<@K&GVvlxV#G>Ig>>L~4Zp5EC21rvY0q7q{&ZBs~(Y z1Dc^qpl}4PslqUqB|I4ZTPNF_n{T*3T@J>OFalG-GyqzVt&-6|IM{Q>`Gza000BJ6 zw~&#M;rzWAH5X5U7~mAo&dv_s+67s@m=Me%Z==I?hBrW9j8%j}vAOVylBp1N)%x0_ z28@BNH!%r4Q`M5XEEHlK%&c(CSVTuHqTQ9!z9!bD#8&A2ZDmH~ZKzzfN71qz`4W?a z&yDHROOX!(9!P$F>)b=v*3;l1D;c*tR^loka~LyVyXJ5VyyK7`Vx0?w2zVJ1S07=H zDCC)h{V?};l$#)yW~4zz)phrz{f7CeMee*rId?_;GvO-L+(J?biaLkJ7O|P7OG0oI zN@=tg7xQa$Lg#a&pua{BXEx!e>pnGaySnoAE9FuA+dFaEjw%W0o&+tZH(8MR$(j~ z3{W`h;ibVtf2s&w#i~A#c`{WfLVze4rk#Q*35jz(179>uR`t>5F3Uj=n}_(BmAJ@A zI(Wm@=mFeX21(aD4yPivJ^@`FtD_;x^JV4sA;4?fZ(Gc&go_9u4Q_sCYyH7>;p%f_%5^gn;+>18;-7S&oj zIhLt5@mP;hsK~z3M(A*QL?M2A-096E7#4SeOzYx}$g@a~B580x{GQHKB0oB&Y7#jw8Rmza!o6)Kv46;ina!?e`dXXg%qL%3|!TfzMjE3;W z_WQ5;!BQw(%iiFX;DKL<{?T)g5J%=snUFzrdbGNGuU(0`x23)~e>S~x z5$cUx%{d1xo+TzoFqg#L*M%IBYxDYJeEzF41i3sbY5urUkis9He6sL2 z!nZt%K}uWPzCfHZGW95~+j}@7&&`dg532!yY6zyyLPnPByC=+5dK&us-B#4lEU93- zc<%t9ih0S6pfE-W$n87>CSrH~{>jmZqq8+t?qCrd5is)P0(0x-%TVHdk%Ggq?7^|> zGyoY3WDao$#D~kjf7z`Kzefto?HwIS3Y+K*3`jC{vJ>0}Fh1n+>a|x)myAk5q|LVX z{(E-3h$Ne!CIq_ygKy^N1*SO;;hQ}J&{3y8o|YhJ6>=orcwD?ll3;;&1-aABG{MIu z#il>0Lf$w28AkBMIR`(BB_19q-4$LFTJNMW@pvaSv(1Q7HodD%YwEUb-luU{lu=oV z=y%!oPSe7)dghv8hT8wVtCsfmFlbglB5pucO7KS*T|~98-!m2&*Z}PVh)ygOEM_^B zma;MHFs}Jf1zZyJ8})o+@DyP;#afw?nxfsG0}EWL%GfSXwFuq3gY$xXS?eqr3xXss zv$*6W3B!a3wTxlyr3+w!5lU}Fht2+%WfvvU^zZ}PJ7|Ij!1v-sZx=74u z3o7#V8lNqM1c?AW%lzMxt8MnL76dQVZjTy|ZqFVl)uC_|u-C64cyc{Cb2GSNtcx_c zw2C*0banloXUguwki_8>lE9uL_&cF{vY<*lnagRg!b&YN{qQ{HQl{jKZK^AP-Khhcj0yTw4a($xP@f0 zTViKS_D;5|Eq>_^ojk!6(|T5IZm#>&t12O&64|(fYG{?Wk*9|?&z4k>(?e967k+$- z>`0S{1W1!%THwQq>^h<(M^QnT!>G~D8pEGQx#-}2(6OVSaxmP3%Oq;5<%RNH4;arC|IQ{ zEAvMMglRM@yjz*9YQ0F59nJuEZh}IG?3jL4#VS`f9Exi@qQyxPi`Jb4DS-UyZ3$s< zCK^jX0Kv?Cc{sWv5dC5KQqHvgxo|d>DGX!hhem~`%8GT7H>jO(0{H+o%S`{`NnNEJ`1p@%_JtBGM?9g{t$jG z;KOYpr7_qy>{=Ape%v}Qp3mUoI^8J?twds);$)o&2 z(?dC1AZ(d9j+8DBaQvZE3Pm7!XYzrj9!zxN*9F=RV8Vx~$LMk5APv0iWrmSirR(q^ z%vH`xE+F`^vDFC0DNU7CS2ei=F%-psE$XXp>VGC)s&vgIsPfhJ*a`1mw;fgB8JX&#iVh5kU zVqA%x#;6m!yth`e*X||1ww;8W^g+8P$EN)lAKJosL1x(vT+Nd_F}9L(91c`#s#(Sh z;MHtFiDD^Om81s~H|1v{(n4|nx4k(&Lrw$8G7G~M~{Yo4xw@(?L$7Z}e8D})FOFbmY^r9Zu_jq^*rUCOV*Spl0*dVN^a^E)Nu zk5}7ipZ@tKhv0zkI)s$i&WM(|@makXym7Wv=>>Cp{}R52VOH;5<;$e=RXPfJJLo=I zn8<)e0$1UnENP$5lH|U`rA*$me~MEcGx4Q`7<#yFiI>|fNvd7vyIEf+>aL~M&b=ls{k}yfF=o=%9yI(j5~>Z9;D_7k3v50O+7wUgd_I5%%QdPUtIw;7oPeCb8`iV{a?Iod5JHNaoxWUO}r8hHdrOh~z+niyn8j)iJiFqzhk;l{-->t;}w~ z!&})A!|$&TxAQaaF4+8i+Yb*kwrj{T!6Mp5@jsSn$McN^iS!tc>1L{ABEq|I=0uGA zN|h^jewTLV4krn>N$^RFzq!vKM=pC#N5FadCR>6YpUp&_o_R@*;XQ9A#;_nsRm#dC zE34MnlqBlS0~=rpM~r0E)*Qr#13^~1@_U+}yf0MHWJ-q0<0PC2ZmJC`7aE>qdSEKv zd%@mN$atWa`KNgy3_8;Ftv?L;NuA$cgtWn?&kQGy)SWzMq5t{nzA_v2Hvd+8dbJ24 z-UGC=VM{#dCKCu~T3e?i#STXN2fj8H&0^5(!%Pn6|2KPRXAynio#39EgF zk+ofOGc42@VP56Ufok(Qir>Pw<)>Q5f{2!)8|?tBc&Bh;QgE zHL)};w2H{f+w^M&jKuTb-ln$273{Zd+zh0%38e{AFC4+RRef5s+>#*5!JfvBJ&d#N zq*OBI*05aX!T7Yw_d7dSq_Nan`5qYVI&k{RbMPw3U=}wbY}9!S^LP6G8jo!Lq;z&a zp*MpZHGUb47<+`d62C@f49 z%#;a}=-!y(9Fu#TozDt*cyso6S{YKUHC$&P$OE~lv<^Dh=w=;>5;$;xwya6dYX`-; z3Z0<;TWv9boz8uJB|t{s1nGvO8X=NEZ=}ZadjL*0II(N34ZOrHg|nx7TPK9gY;_%b z?O+cv7RKxP*G0iS^U$>w+)-^mxf70>p63mt+M>UbpjM3gsWm^pCZZsQ+ESajwWbSH zG3?l!3-5mEh3+;$D^;wCiy_bPyCTJ=1luo_UvUo%Gr{5+UVEKa*KKeD%FN+K7i?GG?(F)|9n(X{;Y1+7El;ijghjlOnL$%!(jx5{~a8^hoN zWZ}@od{B+NeSP~dsivfzA+Cxgn%!xw$G4aO&`$2mU44pn*n6hsc+soy`Ofv>3HrXxq|ZrKYbdjfobVA#0> zEdrKlKNp=t__AoQhx0HTDuuToQOy24apmNpwJUgOGly|JsRm!sc)l=2NExD{qQgwe z+wRC-U|F7D?s=>&@v%Eu)!W--rbo1HRVOm-veWBw0gt{s;v9bhM)Wcuec^h~RyRH! zf2HImP&u2(O1F1i*{w!I(FPCOi3`@*{C4gAts46=s+0O!&&mAo= zslhd?0o@YL)$>jtw#KwJPWLIz{ftNj&fi;kTr<-`(s5{@=ig?p)!w#aU~Xoxnyze6 zPvqjhjEC357AK`e03qL5M|xB2K=-;;qb z$8P#vQhpTO>HPD6lVgA|DqaU1LegUi%ru=YEAIH321s>at@Ooj@*6fa9r@m8Fj#q6 z4t+Ttr>txe$hCx}751@n7rn%0B;P53%J{&#Is#{=7qrLLR^#kC}|UA zC(kJsnlbrCr9?k}JXZp~bI^Z&ZY{~)QLF1MUGgPn1I)Cw^Z?{MFqNpJ!CzJ>DL$5_ z1wne#ziC|2yO4#{WV8SF`ys3M+d|lS-ipQ}`&{jN*sVo9!8makW1u000_7vls(9}9 zSNE9vFjuIB6izvZ)Z`Ok!0L(*VWIO^d=dBQRdcy)7&G$&;ju3wA}4=+l_L)JXd@NN z!66`VwiuWVR=X>u8-x-Uf~@$-_heUEwpZ5*|>$c#sbK|R#wag)$lADV~t3bCjN@g;$AaONs?~Y_iVOL zP+D7wNvq8w?5sk|ifqkDx1BBN>5GRU!@{(Wm^8b>cCfk~)C_wZ#uMQ1L=+b7JF z+f9ks^6Ni$3A$6i!MVSb*QFwKvC||gLw&)4nD&zo&S{VD^9NM4bKi4Nx2~Od>Sxac z&;V9?cVzcPAUcQyAHmX;!!S+Whlq8I^7M1Q}LY2WIG5Bsk?S0hOt7?Z$uv?iZvT6@4Vs^#UJr>Qb=Dd%cFCB+Ot zg|#FHhKZJ@VOT~lXj}zOQH$qf6N~E>w}%f|h{rY0??*9n@ni@E!Lo{8CBhDag$9=1C%5#BPU}gWRkAI7o<}EZhz7(cs0A8x7M7O>GJ4QG8l_I0wUdE|c2%W5;aC+EMld_M4Hm$Y=S z%mQ_>eb0J4v5w{+n9tDz07DsX-#B}+{Pa6Yv~$fz&>WBD>Yto_woWfBSvfqZg48)> zj{2JU#`9vS(mtX2ePg$EPJuG}hz#l?)v7ZDc7Nno5)x`#vKre?OTc= z@5fHdLw_l09~1ilc6L8uYin!5??lz#>px(SePaIgDqdfQ8y=U~ zO|$L}Hx`_GYBq&6}VsSw+=X8Spk59w9D=eCt&(HPp!ih>BSEcLFV=@7qtHjlI zQr9i+?)ao0BQsaCgCt9Fjs$D*Wain?GXSH+bEbQXb7BQrVxyCEE?uG=JpAQz@PDXjTnKjlND58V*lvX4@qkt-l*1Nxs9fwB0+jo-Spn9Zyx?tSykCs z9Ow4RPPga{0`bI>J8!bZ)C(&MOy<~|uCwg!qxWGV=rq<5nMvT#I4ZvPCcDBDIMMG4 z*fX0Uf9c1~1Bm%LIqpFA1h5f^XSn6CG{JPzL5Y4x-bZ}2|1^DnrVJEWnTJ+a{IsUJ zPS}8tN-juF)nGVOc%cg%tbg||8B=~Dhr&JC8W0rrX|PsqpM|6*tZXyj=KJg0yY61m zgc#?K9mg#}z4L6)j6@0z&fPMl?=gq;$y znVDIsD(dk>IY&ka%ukJs>}m@(&Ngb^3M61R##)Ly(Noq3ewdq_XCQDYhWO6!pBxC- zb9*=Nl%>%dS!B6jthE31FE%73X#*KTdaB>vRsjLm;S~myRU{GOFQhVHc$Vsdx&qw@ z2?>#OdmAu2dmqasPCQ3-aIg`Yz;?b68z1G5;0NlMYWcmZe#o_+ewpKeCwjd4Fz}J z(W7)dwd*G-oyc@XC09~awsJz8Uz|5D8)mK=UVTol{s&CpAK$P5WfZutn3E;H?hK!a zPf6(r%jOZ6(ViU8>C)mg`ZlBpsY0k=oX{K}$^m8XLYJWtsbjsg(^n;q0jC&d|0aLw zH%*kC_%5>Ec=&dpg5sTjh=UzHHwid>fDEF!9kyaDhjA=i500WG1K7)2+S-#K2G0ys zB}+JV_kulfWp&vR79!Ub5E65;K42qZ&HxGMEk0_bL%9)LIn~dv^TQE3aT#vY4sQymNlCN@bcBlx)HjZ3GoLvD+a?D|4BHQIYlWBuN*ooVGZ){jV6A7Q~Sh zJ+uQ}--fhd+63Si7Qgfu5W8U*1L;LCN_Zks`Upd1KpalP516B(ep2l{8nCFG*EiQd zr%6(_&?(%~nC!bw&A6Wgl6(i~(JwC^upW}@4t*Cr4Qrz}|eW3Zddf2hwlb*2jR!^0!a30zp z?b81oF)KB67?Pp`xD2EEKwzZePA_dxqarXu`!lo8|8fBa0s)A=1hE@9R|CpPW+laV zpl|p$JUIb@i1qMqbCUF2X{3UCL&Kv@by+|{{UAs+zISAuFA4I+80*uAI;9i9#?RNG zKVeBaa7NEQ@b}X1-}8z{M41OEVD4vE>d@mkRx-##8HdYulkXAqEF@wWHy%-a9ys04 zD_PEUnC0MDOws|Qu-4EWbYHE1`uERZ<=4|z#)2e76sHQz;%kJ2OvjAqi+S(pVPtW= z<(iN0;{`Kjjop>6s2SYb!PynSJZ@jr9$q7W1p@i`_6G}-S;?(S|My+%wh+&IRu&ga zEp=Q3LF~7W*0vweIhc*Bi&({7{;gK#-c3%WqWSTX9Vv4DQQf)W4r5QHOS74K*EzcS z_)d=X^f}&SGkVf>y&yV4oRwxkryrL)^Vw*bxrg{4(fMYhqe_A0C0dr3nWtDmE7(j| zq_TW_8y4m0i53pbz`n@){Eme->D=fTFDcn4+sz=u&bxeLt6#0%Cc!vr--$0@bghQY zgD}&GI&YxdnS1<2GyTq8T0qy)KJf0pO95c0CSuU_BP0o=y2T>(oLTaKB8tdXfur7p z$ty6RUwbim!mTyGG-rodbYn`@!gN1>5{~xmj22Ob_4O1Kl(=$dWua7GS^aYuZ9Hn6 zDRB9Bp*F!ly9tsU0g#1$Y4ODnUlKCsM2(MGzQXb9pYU`wMCm6OOUL=xUK<9&0Z81s zfHdc>pI_?EB5?0Vhq`2TXOdM>Sa8LE=Z4MJmz;2jEN+3G_kWr`|B0`zBlR{Fb-rY}{m2 z<1Mz6dSem9Y>nt?+nT}>h!ZrNe)t0db?F)UqcIU2pPUR*=yj5$VU~@p7ZYPs^t*#~ zab&SiYks2ZFwU609EK4i(aHoYL_}38oGHOpz0h^y@w1SaJi-fwIhGXg_guK+6PP8E z=s!@-CKAnax~J_r8*IQlSvF5C^(*h^L# zNCslV;rIoJbofT>t(`P{8!B%-p|R%O*Ko7NW7*qh)(jnsYt`O=Z-M~K>7Ar(b-A>j zQC|vVudP4ib?)2+)&ItSa?D&ux5l-}o z_A9YhMA5sFx`W@K$Ndlu=B-W54Xl>VziDpk*4y%hC1{EO|J&E_j|G*eU5nOK5q&Rx z>-fipKX6?;1!t8GG*(sTr|Cg)oBR~nd`HfG9o{_m#cxAP-oE3x7aERD)0mf-3-j>u z@8DRwg@=cT0|8h~KyZgd&|s8S8HV|1ccKJEYuF1(I0AjpFpWbT`oQwhwRW?Iw^LH| zR#%qiW?O=)O~Z@@H&P&X7KJi)=ym*9!gSC{5F~GEx~Gf@TTc};iM^gvU7i2KXb1jF z?j_=jm+v9*7!lckF6FDvjZaU96eJ6o7w*{%Y?@?nL*fc0Y$jNU1ebCC`y1Y9VesR$ zl}aXaQ3!b_Aafz5Rb!)CZ*MO$L7Xbw?iss)R)g{?3-exNPu%TsS{i^Ay)J0-FyLi` zI&umk)Did_`c=8Yec5LVqQlTmK$nXX&avd0WyT{TMiCqqpwl=m3-uA3RtrL^2H%1PC+} zC7tR0ktL5w?I>9Tm;$pNywdRg{ccbu$}J9JVnbdSPp#G%CIa%8R(1u}AA-OOBg*X4 zkG|p0Z^y%B3sU3C0Bj4_h%IGQpWFqQp(XhZle?%pl5}umU4;#d znxUsSAlk?NYl;?y@gvc^RJ2>TMciZ|Q&Y}3GO`yx7ktEAdVa3b5DUEzwJ--;3V1AEOxt``^Y58nFc7TE0=8-6$P z(^+51v)Mj7Z(dNqSr$yC=64$`=@W0Azbso>F5-qduPLC*exis%`JV!YvexSgXU|)GaRc(TqjdA09U&f!BpE^2rGf>s zA?$v_(*RNkumRmY$au75eGquf*Y{|-)=}!6Er{&cq#m~OITCyj3Io$fkgCU9$8T6k z`$v30se(dA;I@*|=znZhI8uh`YAZjR0OAS@=(w2xrNmcrxXEM%}42H*iT z0&3D0Nyx%T0$%}3wz;V(NEqKf1HC%Q0#&u89Nj$s@#m>Ol|`Qh@`VTo8GI>n4+w+w zJr08n$=;I-8yS1O6UyA@RMV#6&T!~UIdUFm0P%?PN9|h4;4~NHpO8~zeicC*Dp2L_vfbDAU zGdH%8+zHT*;nXB3a7t<_K6;`vIRV4&v5ASs8^45k*I8CsS&2FB@`;<$b_Q(XC}QSH zrQ`T=LFwP!5Z0u3Um&%P{(;TLTYNiy9m+EOf2ntSZ+Kq3*v!7`mWDVqXU5bF&c7u~ zvsuK!5JbNV5gpNG-2oHgM5SvtC@H8UC^l~F1u^5P`4bw%mWx-(o?a4C{zk#fY~S-F zAA6yZN#JT|LRqxsWNTrzroB%zTX*vwa27#*LuK{x6vj`m$NGq=5aDFP`SZVJ-2}P~ ziGUaz3&=uEfuAx*Y_uSflOSRmP;xzyD3YcS26-Jb4lX_Vb8T3;$AHGb%ivJHu_2g?KW2L3MC;6NA#d{x@WCs8e^T=6r`eANEW=MO z5$Hcq4JMr5H`KUB?O;zr+|r1vbS@zg!%^e;lLG~3Jv{1OXwofa;&^ILxMi zFG^sPHGvt_I_iO1y+T@E$-OShKrZs&O{ zd)nKQF)VTCaluM?6C%U6hhBSN!~cI@GIMRv9@bf$F|D(&z0c|ThY35sIRAiA;uswB zx>0xg?9Q9 zS5tvs^ufe7N$Ru)ZqW#$bRhQI@J0!_$GDGhCU`@crqYrA7c0!H)*7u0a3v6qg=8>! zM=Wa^y(K_tXw!G&T(3<6MEQY3w(-|jOR~VWhqE6D;8eWLXb7?+rSLhT8gD@9`z$i3}*pb3($PvUcLd}gn3oM4NO!$c)p# zjRWLa{Ln-iN^TsVFmjqDxvIk}fpJ3deWJ1~L5AA6x5&g6_?il?{(DF{lD@JcO{&to zzC1FK3km6-TWF6Yk2jpf(b17S-e|xqC@g$d|(Y3 zzkntEV69lyNzXU7bQAjGhJAf6pF9~YoSS%EW@)e8=V()6;myR76MX&ElzoX-&H%2; zn{00ynj^36v2Hs4@cHY$32((*ke8spqy&rm8b%8>E~oCB=Bs_TelK8kZ<}XTiW%VH zp}MuJFrLO4Cn|2+c$OS3FlWidaGv-?;1zQd_deW@4xnI==UU{p8w8%Km`@jtfBdKc zdMrs_(*>%s(CsMw?y?PfzH%B-qokVjA$U*4xpC1lA0aYjl=QuG+ z@5#6wCrP&fq3r=-@W{`A#n-X1L4+;viFKT#lisBCgT@L0AV>*(1-dPbA zqW{=7N1keQR9$vyH?=5tQU=eSv~Fl_2g)G1d;Q>81BeLTt1B|JpZ(hq-#{V^Nb+R_ z;`Q*L#)=3Cuf(R%HkEDwb+A6M>wr!klP3=fgeVfPU*|p`r_0x{7IA&{K0MC4e#3f{ zpb2kKIu5Oj(*etG8c^W?EUj6;GF$}Bdfy=dn4wCd{e zyv4k7;~1AsduLPl#gip>v|s1h3O!1uOH8ZNQV~fwNhh!1$@yQDB0^#J-e{Zr7NKxk zCz~fPJLAHI2Hmd_gfLQnMV-zma;`4j?clMiC?@jztEZ*$f+wm;tN>8fz} zO52UxzG-uKoTI?UsIm{b+NZ<67>EGn3{}8Y5CRFEE>HiAI5Cu+tyUGbD!z(>su!>) zoI4GMYkW#HN$LhBPq3Gd`}Oth?B9zYZp&SC8{U*-s9oVQ3SxEF^*KA4D=QRVq+IO* zg{o<4N4diP}=cP(y~2E&}HIIXk9V~zF%^w-;?y=joRsD_O0|#9_qry zDNx~NWMm$mUq`Y;7=X30PZQ$Ix{RXP)_=9YCLvqW-4?RdHmS`drzn#r0IX6bdZ?zLq%< zq+))^XgiV)go!>~f(}Gb!mDGipcys5k>MNg#W{lW=hFG_FHowX5@%G*KNeFu*(P>n z#T!_8386FC!Or4=by=`sPJr{7> zCkQenrt#Mu+Qu`m!QDVqjmsD92+4IVA=umAiwF04DPPT=uy6UID-tJH8BObK-r+E; zE>a=qFF&dD#Pfr!|J}h=A~| z#ZCmxneNMS>K6}vckx@XOFs)PZ(=ck_M!RRJ67LW(1GA!-+;?}m!!=ROegk}N$Y_#Dqd0TlLY;TNLAwGm>Qb>b}01n ziFr~dvcuhn-+m~>=4E58lTI|^b$8$|Nbs@UN9qJjxS#xA$GMmIe(2n z9Yzdl$mr_+shxiozrE-d<@jz10t>VBU8N1b7l{oJ1O(Ag+yfCfjBzAxWWUY-zVspc zNs}LZ0$j~}n;Bi1_uG+E;gLfHBCoVw=bUUmKB=F}BpO{}-Z8Kt(yHoz0{i~EPuk+P z_=5F@Av;gZ4P6n}S}lX;E*0PoZ=;}c=T2zbZ7Ewo#pG&!_$FI&myP;#M>}))K&XLF zaFdHNmzH<>)4#tg$sVs?ztOzTi(%cmb$&D(x9r@5Gnn#6qyxZHI9}l**-Z4kNIQ_j z?T;rz1k0B%E!x-D3_@9>m>aN>dcW_-^F%oR;_%H=llM)Cs}N4z=;M%GG~Jr+OJqM7 zhxdJwjzSsf%JIfU>|M{4RS9nlHT4UWhBL)5{(_j}`Ank6ge2_6jP}(CLeyjn!Aeb= zWBO%{zXo5nGqULDOL#Uq`WY680E@~7v}=C@bI@(Dq0{4=9C@^9c7DE*9Yt+1^1CtD zG61l*OUaF`=kM>RH(v-iHNIZvgza>LH`GaKjolyUf+T+=QX`=B5ngZdInrEO|BY(F|$(gud0gU9D1X| z*2GyPw#ih+p69l3wAM*a3*8sf)Pa)0vwC?}5mmStqA9M(Uy6k)!>~+)Vz3@6g*m(C zm-(u`8WHU67)TJID{f3bJ#@&Ks21+^3qBpJLw|R>t& z_vQ%eaRNbs9getZmwhjeegX3jKY=b;>Y?55TgPzy5m$em*uC}5*>AouOFC`q8_z|% zsHQLJ@I&kVo2Ba*c5G*3>-Yec3C*UhY$G=sZnT7;yM%~fJ!s*?zU<%ig^|_Kx+|B# zo8fnuJp>GESWW7l0V!rb^&|_K6|Hjo8rh)O_8U2lQx2YkzEP=%4*eS5xG`goQ9R92 zwVmrKERJ~W++I!{u*EF(=2aT?{Lr$Bdwcgh-gb%=eO^DEKO{HY)F_=|{9Tfhzz@ySf$S60m9g{#7FjS#~t zbT3PFKM*`52Fg4AjxnF_>ECZBB|UFKaE_yt`_MB@j>{cn2n10yz7kwiu5ZbBPf0mI0Li6cEWHRG$ z2mB%H(YeEUY%2iKmG@y;{5XeccNL7?cZ{og1&DWonR(FUUpK%oC<^~r$d_tYv z-?oXPM$^xVZJ)Y@i=eyfl~l*{KmW#1LIY=YX*y@+g!iTQvhhl@|17@uJs87(Tw51p z(n+E)JhiBF2v@!j+`&+7ZkZ~`uB54Yd{>K2O73v}!-_piMSE2DY`w8tCaf>m%KM0P z=j7bux%(CUoQ^Lyjh%XsUvHZ~g;yD>v5i7`8^uem&4uQER>Dm=yjFTIl2`-_?$1&8 zQgWHu6*jexd-Yiz<&0|S9~3^cS23C=DycWvu4;MnYJScA;4WrL7XBy}mhO{wii+wl z=r847obn?Y4A69%Ubp8iOa=DWI)@THM-69q!>!8~exu`{1{T2%9f zxjU{N4kMQcpeLob(8D(YTlX28J3zBroWdWOoyc{oeU_Q*ehU3RQ741Fx4L)Z#(xLU z8LDfIwHHOzvwB|k-s^W^6uD#Y$nALIm3|ZXQWu6dR_AE_cjooAh94XzHlyBCUk_>< z=w|T(l62_IiYy-gdJ*<*y8uzhn_2ttPGyzj-AO5z*p&6gmbST2ti`3KQERpI4}88I zMjdgtN}m0ob%p5;&ubP6*8MM7ZdDwxa`g}sBReqQ9}9{MhOo%6aod9Y!X|UGumt@q z)rZ4wP19au9@S0{?~Q0pe0Jq~c|C9fO|Mwme=qJ{HjP%M0M)haY#+vdsI^~y^l?^Qa+UjOee3NPi9|;G!$4V+9q4S*O=hm=_;L*17XkzeNU7oUcbX?GU z3-8AFPrNcz|NJWV!fRl4f1C!TRT=D+CkS6UVB-QXzH%=xGq@$V15w$C!}MWQi^S|8 zNnvEJnf=tK+nh@~z|((n!2kgbDi5nZvJh`R;Ib+ZDDMJ8*RKBXLC>!T%ABVuw|?sW zb;k0J*lDSXii)92Uka_9e*XN4s%d+nMuVJOB{AIBJ>c=&`>lhcgyT2)(6F#7ym=Y3 zoQhK|O%&JvF-oufbVhCp}>kP1RbrSBJ^r-fDyn7(kYoc6mpVkpaF}wUC zvbjRAv;KE;saVM~CG%(xe3hz^yeA#gdXo3~cy3juYUb?)Eq2M^Lqn!QigO-EH?I~H zKc+BNP>y!ed(5}FioBeD5Gi3gx#RnccMJe3izl~l+gST7Z*;4^EC#rMII3dQc{bz0 zv9e&|iTkM1ol{Yf-Cu0&1uJww^dV=8|Cb9u#MvbF2EquDY`O1`13%vvwEi$FNH&xt zDxq61SWrPUVbOw5J`q{!_Fnc|wWELzc8&3CMXB4p_~tuy%(b<*+hI@1|7^5~wS|%C z1K-aAq{`yRDJ_i#8>V>1ebEzL5Pn{|q?<~|yl--R#9)M}!5kS1eWat2Iq9*&5&EXoVx^ME0G`(u7!V4Ta zo;#FzH1EE6Rz#mU?Dm$vUB$gS55$?6Y+e=O6k)kN+e5L5Q-){4RyE66(|%2H1YF^v(@_}YcR!4LAE0G1%>xgTqfR36Y5FwX5LlSDC_A-uBsID&o_>r zuKN0`$;xLk4%{HJHkU7fP2mInuJ@n9uK?o5-5pLSY`O8iK7DP)k%*7b1u_vWp3mv# zt;2t&-XSG^Wf1#E8diJ6lm~7zsyT)p8M^`0UkVGt6?u0jCujYwDqG2-!V}%87vBfH zLGM5&a44YmHsz?^DE{%ymtY+UR-RN$N$Mt;y2)ymc9cdqt`0${+6gMXSL0pQyL0kTu!mZuzUOZJ9AM#E=~Nf z*qsr4gcKD5S988+Ja8UKQB>(A^i(%H= zv*jxkiWg}4a1M6nn4I1$aoy1I!033->8LGqTU2JhhfRe4Qeni0 z(-@l*+u52jJ2^42o+Z`_i_W(P3KHeMYY@4+u4rxJ^3vymqQ`%Ky&HV`lNL6gvu41J zcQ^6!67Uk9IvG7q(i}cMK15#)+9Gs;%BRnEzrQHv^Vx)a^bmRtD<||0Xy506w#V9! zHO|S;J_{W3d2`(R46Z=_bn~!ij51z??`{AbiPBAm>8&cA!u9q^`?(YDSqBi=__50I zjo8!^ACg~$lk2P^{EEoa4(-W$eCajKiiwVn_?HttA=45?d>E9$u#E*F)qGiALybvFd0Z=v1V*l+Wq zxv-CmX$O5EHACm5SM~n4dM&4#a>h@U4c;~`-&`Wr^Zzat-J4-~yf44}c=h_#HrCyT zJ=M50+YLsH-q^et_wo?Q49vP$mNe_({K`psU*lcXS@(*cUe5!aVmqyFr0{6(?B5x~ zoZR&t#3-d=oBH*r-^fU4RR^k`jwk%gN*>|{MV5rwB+X=EVnQDIfyjrjvvOT|U%M!L z>h;X&sWWga86)QIW%EmKj-%%+wx9dDRIMvA{VY(3!v+{4$%KJJ$T<ePEScmVY?D()F0I1+;8!@^MGWNxug25`?X87@sw9ak1Pm@W%Q(M z!O0X}!B{nMQ_9=Kn6>B2`ErVqp8xNap2Z7i2V<7-Mh(p!9E_1xh5fhQjeoRi3~KTy z73WB&j7zrB?{mQpGd*s{xlii)3+WlDvB1o|k67je?#ZektREsTby$Nnem!;JEU*goeY5x$v4s)@VLIyJKUzrUdD^>DW7} z%)!BQC<|fzc--JLhAACq9t}TVSw1*F=(skjB7-nypnws(MU|cLG*P!-CuMnXFr`TK z`MqE{<-pZQsHurJHZ~SRuEwB|Id3vYyZGK?6Y8OIsV$5{jh}jmAK(^X{Nq+6vD$U; zNnE6&HytK`r}uvhg&w;)GTH4(2(YytD|l+2>`c=1S$m&~w>0+s$%U_N$1ftVDOP~E z4uS)4rCq>oyQgyo)#Jl%Nru)YVIv=mqqoQuOdJvs5oSd8j^nVw?|%e?=tl1_i!i4$Jmhr=tLrfpB4Qw@=1 zp#`2fisFrUWm|2=3{TcAAq+U$OP1+!3wEpJe;%z&dVvdd1END2T)^Nd007s7bH?2R zRa4)pcg;-+dH?zPh-km+%r0M6fHLo6RGrelVT(<;t_J=tPa$s-| z_WLcJC9Y-G{jXVYk!Q}{zjwd!+u2@v9$p?Uazg!geM|9ChHC`ETg$d|&^0zLVVq&d z&Ty-o92U_>?Lnq_d3YgXw+qVN#5{ABDo8w38$o$vN4?VI$4PG5E|nZqSgtm^JbmIe zuEZ0>6&>{aG(Si0TQnN+7eBmf3ZzqPfo5|_;q&Lu=HP*mJq|Gb^~d?q2Jh3&@8nlz z8(=1!^3lB7Yu|=VKj6_SQf}%}kyM9fZE@%v9fl=pSv_Z;{`&S_chT+m(fc4*FLa#i z?tA(2S?Wb$jeG*<4%eQLIP+GrD*;~V5Y3Kb_u;bdv4BBOvPi1jLPwv$!keZr=ZMM0 z58bd>gTdyuLS!NDDPZ@xY3WuK1RLya$fx=4u~vEtW8GBDU|+tKe>}e~n3kgzuZ67b z!!jnvZ(3zbZIxj~A8jcTEZbhSSykFPhk*fP5fLfOpN;K#{@?b%DiiDJi8V7m zE7t$2|EQsLgxMX=omJH57_8kMZ7I|@IET}V%d!0R`iv#5=o>S)jJNLYRG&4tz$d|8 z?sT^Ij?E*Ms+Idjl1<}t#b-xVzEv-}Jy%h64mW)O3-E-s#4iu4BQV^%(vB6>fjXzR z@U)pl!Jlu(+FFjXy+(Ozi=kL|pW8&8uLQblQUopav~EA^$G?B;d%{{JW#~ zA3V?t4Gn$y0T-uG3eyy1%Bm-H(&N@$-<`JuAz*>7E6c^aF~NhQ$L z-O}GJdUB{SAKj9K-$0JHiRJyMEw49?nphvxj_YY@dd(7 zOEI^Z^Zm?WH(u#IH_M2rKBej&z)Tqxp^;&B!#~ee{rOf+jPDN%h>9ZAIeiLW@$La# zWIjL{D!()mD31ld!VcQH_y)=!a7EqC``CFk{K2t>y`+NmONDd|`5&>k|8L*EeaPE< zmri_YTqj@>83ZzwAhy-?p?L&-d3S3Dc9H8q@imaV=X{AUq3X?XSYk^S4yNido@*MI z8-lz8k&QjEI0rrZ4lh8Pm0OV{?nD3O#JgwY90YviXmq-;!TdyEO0!FrfT>(SdV80a zhJIn6t#k3nxQDOzSbg(SbD2TZHVy+LG&J7xCHubHhNvu@-$(`kxsdQ^u00BZSli^7k3{A~tYOm}AMZp}bO@vgr#4vKcbfF&(lfGz(hu$beku2G zdMkJwJzsvNbP{}pcHtvQP(6L{e9JcI_8Le{dPmGo+Q|BpqURaz=(%0HC37A>V5zTD z*9^Ye7ZJ&SC^DsXpzRK4mU9HBDkZe!9;cV$a7;&QJ}=0~xE8-hCZG1-Gx~oYYtOtb-%~cLaUg)zAjZzFw6bM=A>vw z`fhC_D#hk@&kUip@%+3r`)?P{9SJRP?6esf$smUyM%G<03YEgWrtjiHt>e)-Q;?{* z;DY8S*t8%=%(AkkzL9GDNYdQ+3vq`binJ@gopEJMWj=fu@$%)iSp7oN9gOV3z3OV$ zjm*q6p?I2lK6M_iBZ`@uq?bdgJx!^yxX^8n>U zM8cG3E=HH<#+IlwtQ6UD$f5(5Ow4T_4S8;HymaR8rH4;k1a%-eK}X0l2fn$u=Y$r^ zLYJJ}a`QQZEJ*=@R-(Irt2%M?>{y586OJkR4e&g0-X zdh|e^Ug24i&)c;n(lw*dOqgAVq2pxbMWsuZ$W)hRPDk0ye)wJCI;BcW1l+u>?qL=Y z*};}9VT<|ldIDGzxBL6bo;SY{Fg!p@>#u7jaE+?tyWdTJ|80;dM@t$f2b%?|Mujx1 z#NO6ZXxg=*5pl5QTGZZ}-d=u)x4J9^lkY@^P85AQeKe6G^_HQE@ezsiyEuDXyZ={> zD&%UbByo-k?hjQJ)YVhjqjjdVUpJuT6HF!XDRoi6o+52z;0%9$P1?_08(q%TpRSN* z4!>~-@19jG&0M3>tUj2o7~F$4^@!LFdN_NqC10JAzvt!3Ks-Cwp`jsPx3MiJUd+?0 z3Sn>{_Sw-7xr$MT9FB@*$M-Upm;VL5@3lZgfS0=k91!Eb!q{ z8mUmAlob#X_|o%{0u7%W^dQx6xH3EMbqc@D$McHalMj?i&0FYSh|lx7l|D}y{n}c7 z@bgaz?#_hPJbBjfvt|aN9aHz&lvUOLcgb;d737tXliSW_YwOi?TmG1#u|-hgyE))Z zee@v|y8#uevtCK*GSIS-rxB@QC)qq}`h%G-TyexPqCjU{roLm!BgWA9%(K8tz8^o9 z2aw{caKQTUt8CK^SrwJtXqr}h=j%wxkg^50oA~L)j$k40_Ooyx29l}{C6_ofk8*{C z2UysN=k+y$G-MdB<2ARzIzaV8wHU!`)Vu}4?)ee^?aIK#pBbJY#1!!I?M%kyjwN}k zg}Lf%Vc=G9kc;#C`_)1{iGOPbq-(l5=VZ#xN$I43DzKWf)_O=hKTZ9##$(>|Iyv=@ z53;xvA8oIDnOCLqYWC_;L#Gls=jAs%Pb}ZN$KOqeNs4}TnTwAvt1_3WOi+)_wY;v+ zi~7)E>(c68%1RY=+5c6!)%AsU25d!eVv|jAVOC`^ec7E?{FB`3XT567H7iXRx?<=@ zcGd{0QO|`a1U34pTy1ZgUV2VSv2N&)7xILEdqnv=r`*71z4LqxGxFYibCeYe+&nxv z4-1I3$sAz-6LkSQ7yFc$T;{5VCzgm07pp4qoKn2|o}VMck;qute&H>qPZV02f9*f2 zLp1AdP*-jW^72Sr)t0D!r(tf+plYb6rv~}!O#PIr%jaHZY|4h~y8rl?fEOX;y=c=- zkbD#fDS-31s<2n@8R?A`Yru0idoU@s+9sNZLcr(%ce#e~48LX!y3|NXi|My+wY|+T z&M(p%zsP;i-><_VwXUpSP=AOn=4*6JLzmcS=UiA~#rV+SK1UJZ=2=&lcNRaa>ik!| z5|X*2_X-7$oi)-`w{;geY>Fj0it+J(aM8R4NQeZzLj7Fq=$9Ylc#_2Z-~lH$Z(T>G z*il2<(&i}__#WGripHq4j?jx%X6+=nB%AJw`n$~^RoJWA*|Y4J7WBmp zIk5x-%DyrS3seWyK|C+%Ki`B(pZ_xP{q=1%)>x=awh;34u6t5G|DNdDcrcTZ*FXg4 zSwN|%!7!%?MW|>B4rhdE)zX&m*oG*MA)e*!fE-1bZb|{ThQl=R7b|F9u$zNHYWb zZAtA8s^=_s`}?V>X>MWl9b}ey;%MH=rpS6_^!jh;bHvO%|N7qZx$fGkg?Iospoebt zjQOOYtIbiWcIG@=1_SV^p`R;BU3M5w9EKnBVy{h&gK$`e$GEtjvojy|!#IuY>>vhC zBla?;`)9907>C_sS>TYWXH8Auclg#otI=V@PzBKCuqc zOWoL3_p+%aFhIZK#J24*dwX{rI6{95>wYi)ih7)AbY(YlvQv)WAp(hN!zg7)m*UDS0Obkn6Hg%1CY=#`X1GTJu>Cv8l& z6_spNiJey^)5@(ZrOF6hBtW`s-TduI;-Q&E>F)EOgK%!h>K3gFu)Mqcuc~(=C9Nh=k z1=nXE^bHJRqYp&Ivazx28X2|LV5@pk$D|k%kGz ztrQQKr~qxTaqqPC^&3a%H)i=^X?Ug(drMqgg~T2bbSFBdrfTl)BCrzWJljIf;N{lx z&MWy6d%I)DYQj!QqV8NdnZ9d-_teP)ML`xP-DxK&>p$sa)k9iYzSgG%c_eeHsI$!< z)6GUBa}5NE{Dn#g^ej9=#sLl&6qm{HNlI^5iM?{|)JXv(hPivbPhHBO1?Wyi)tFb>ga+`RX2JZu1Q#3y*&ihsUWSpqC*T1eI z0<*#D$-0g?7q{Z&ljeGo5Oup$R8(klryHoju$M4I0r*6$S7OwUKh7(XP`mEiZ6izz zDqo>!DBA`OEc$29x|AZCzAa1cv;aEg_|G%PlvPO& zv9Ze4t5m9X{510wM#r(4b<|VvL=URSDu;wtFqbR zu{p6v6DSnqrPtN z2quL_OB@LZb8fEM*#V?4ZQH=9m7`rwEiE9+)3 zJdED}J^)8Zor*U}G~C?W^MbSy@{01AHWDR=fLNir!j5ePd5c#~7*z9zzam9Btq5)f z{1DwCZ3tau9+qQyO4HpKuo#{$Ichl_KFb;L!Fgt^dK636N4pb{tSZd9D{o--Cw|N% zOy&^9MQ}jJ1qHX|^xZKuHa=1=Ai!x1K|B;~gRft^<}TB?7T&fuqZ)`m#-YiwQmyvZ zcEVLWny0Kxk&jIxXxjBz8mer2OxXUgrlNur11!7tM5o87sA-!! zv2N-!pNo;9XiXTriw{+@@5{2VlB*4+>(n&2{ufr(s6eg!U&&N=0Oh^m*@$r;rW0y$Tm z@py*Iuocc05Nzs51k~Lea%}3b5hNt`81dxK7Yn#PndEfMa^5E*+#PV&zn+C;^R)oS zcJTyFt*X3THG>5Xra>QM1A(y!b6^;ClS!YYAt1N!G0=4|jF9 zG=>;1e*E);@B@yY9NpXqXZ-N&AQ36}?%Smo>#qCdZm;iV^Y$AiRZkI4UP9iOA zEbs>OpyE+20g5FuK;DTTT?V=Yq^$N+G~cc~ix0)&K%-G5++;P3U~cpk8lj|}D+O|L z(X_0-7?av`RD=^drW!9*1J62oICme(?!~=(xwuYzvbq?TbVsx3|Mm~EHnw)!#A9)N zYG(9>_fNN%*8kB0csXd}8(Hu=wW4qAc!xaVAM3q~d>2r_rWWg6Byqbl0+X(gY#KUO zQpOhC7dWK1uUv*g-{j(g_*p~D1+phKiiSYb>PRFGglOi=@nH}2=~I*V(#Q9aFd|rHb=k9lxvq_5d?SeZiUfIah7F2lAyCQzN}bE{VQp+>c`A_+PPF zC;Fs2#o=Togz+A3F)kIAN-g`AzQuzp@yR?_p3$d;Ehp*YBB{&oc4~O#z!!X^Zj)hc zbywnVm29qsx92BaBfKHejqk>XjyGtCpe2Q%eC-Mxaa<`xbIx^4Dt+C5+fxPkO#S>l zd)r!D$9zBdF7Y0>xN-U*d$`i>b2p~WLQbLp=Ok%734A^w!=km_7S7LuUTem(p7WYz zyXU*e)oqCK3mpvSNTFU(E`m|-!yFAl>;IZ5-J!w3-V&evwNUZ3^+<;F54|3WxzMS^ zr^}$s5D};C_PJGuWOKOpyiDn92L(P6+`w@t#_>t*(HEIU50$NFTP;hVj9>eoGXCF_ z`PQj!vQ1_2%7cQWco?E&>&5Vt@`9`wTX>>VBqFSI?Gmjk^=n$n-m!t+^K4S$A?w+c zn8q&%e!5DUJs{jmlTfu)Jg4gRwYJ+WpWsWLqknd2sIjSOJOOXH^!KP-o2sg+{Q!H& zquDEMZ1f0(a5jIIEw7mvw!ZEQwMok=Ey#U%!cU#v9O;T?_RAJ2Dk-rYnC1m4E|%ti zWIG74?tDeSCM#>w>u;b*SonKi5t)V-uzlwc6FZEX2bWq%B%>r@D<&SNm_yjFg@*0{ z5U%qeHWm_6-Fa1I<*~SS?)c8}E17#)p(ZcB*VVevqllE zd5RnZX1K#tVmV8B<@k8HkCd(J1tBla#J_X->VJrt2+aTc!9(Lyt`fbZH1?4=Rg>*s+GihV$hbaRnC)W={`cq%99(0mRB}2+66ALTT2-bl zsLZCR;#{JC<~(a@NrA;ZFWUX+l$zll-oe8Q$Ml{T{BARri_H zn1Smz?tnNBYM`_-c%!2I5%(61LPd=K+#(lD!+-w31wh+vZMDAt9v>Q2bh7iRV4?Ia znNI&x@YC1CK6RycCgscDe*NaH zY2_#tx$Y^K9P)il=B8mWM|3q zC58dsgVt}My{#?l;ll%%i*b#pUY(pdc6Ih`&?90uuk86RTGgLjVx*r>pWkx09q1~t zJdJD+>$jDKm5QsYtDx&6kmc2v1b_H`Jp0lV(S*rK(MrX#%kSl7Wm&L~JT5ysTz2}; zuOA$el6%?Krc(pff0n78Ow~-6gzKsfqao6tB*k-q9Nv|8Z<5h{3IKKJR!Ey z>nxP{?&0d&O{q27q|SzpZN9Z*-D>A6JZ|a5ErmnKhC`UYz382$#&up-=Cz~IV>UOo zDzLGcU6LrdyFB>c+_LiTAl}-#6}V9D!zW2$h21vU6;Jn^C{Ycd6~WrFO02xX^?l=6 zeZt@^sMF!rJi~!g}nGPPKr&9wvpxUA5!rpJ32a|*vUIcZ!W5V z!Z+cg0=)oYpy9Jl4vn-g4NTN3*<)Eg zSvLgRwDS(N77ihCD<+!Xv=agEe2Ly7^&=X$9nhbO6#@W6c0_knHQs~Zeahw1An{x=| zZ}wd^lPfG}r@xl{>d*1R3ioHFWvQn(rS|c-J1ykK*kp5gXlQ+V39a zkfi$$BnR`Q9bFu*z5y)-j_&h&6S0*2adESE-zQs7)vQnrC5*X>pVY(D*K=w~7^v!m z&3BbS+I@5HX84`odpSRyWk-~pN|eT@^L4*h2A_P{JudvLg#4dOLUI0bg1hEh`94lJ zC+faimil5z^Ww&-QU4VpVTKHj;7eK~(0@nX3}# z{d34L2~?}<;_S-}4KU<{f=$+E>=yOEO{X%av6KW5gWP<`p^eilb&GJ43ni-hw?BW=A73V7CQ7W~zB0m?@)b!b1!o=TW#Oc2w!q0(% zWJg+b4~5XGW~uVB2aU{9f*AsRHazbqGQlNle! z=}q7Gxy#QrU=Ki~OSZPE%9Re9DeRHKd`bTFs(nXwaOBaI>i2q6l_8;d0a*bW{72ASEdiXWjD$& zv6njzahE>Z*!l58O#lYK^^wCC-^WL0HtIe;_kqlMor3sb??!#YEP);CQc+X8t;}Q) zYa$wioRI0p3I%Z>RJgf$hy-XXAu2jgw#LA6lu{)YJO7+*fEQH0C569d8|OODmB*!| zbqFo{1|3d)pSRKlQ!zZZLQzk;=VqgclrDM`QF<@_MJ9Zm8~!7q&vT>Oj}m%?>fGJ z&mVqg=L7}&2~d*{etnSCphJI#7;IYAilvMngf&zOGU7Vn(J@lHy3hIflwEx0!-sda zlkTthG*`WvgERdI|5EAmY1+q^eF7kLSl0m(g}={sLyecAkXiMa!3=l&*^d|2ANPrs zk5?H&c(Rg{(-K+!d76k2=Yd_ux(LG z%c0{*QL(juzWWejdTdG`O-xVM0J=rG|ArdeKD1+hOTSS#Lkbs;6?tU|M+bk^CeL7U zybJQV{w<`^Z+p*wxv0gQcqCEAttO3}m36UbPmJ2Gh=#8-2W#{V&h}AGRyu_dg123p zkd<(@vWd?*?A{X^id2>TW&GCjiaA*@NnXE8D4xa}5JWTs=e0Uz2r+1Sr3cmoh%6?E z=2Aw9a6xrDp!VtniI`A!rq z^)K3E8ik%D0NgOKTA7h(n(mPPK6buf>afh(QDCz6eE9=1C0*fpb^c~|K5k(=8RHpS zpUuK23IU|SwVnS}R?yCwM`I``Kv?J*oa-j!CxwMY4Qdf7XCfkZRv*mJb)(j?6eu=Y z*i!xB@twdGuN!JG90xz@MNYpG^O;#?<;^npHbSi{W6#k3fKE9oN9XmmgHFfvPMWyw z$(StV@?D#KNh2`f8lMS5e*fvUBzL07!elE2sS*pHqJlp|(XKk*zBWuqIA1DZewck% z#)LMn@uyo^jtEa#!5H4)74TIl4VOfT&d$}%jSCdn)2Fl`0$5dmkzpPSiHKlBJOgem z#Btm*DX~My&6{F_`|8g^yXeHC@&BC$E@0-@;EQ}(2$C!T)s3;w<<`W@<1r{c4}Q`MDt&mfx53Yj^Z4=HiVDg{BjJdJg&-K?s0`AJ0hbe+ zVW2rT%82}s9HsrvwU&$+G%M}OYS(BH3&O#}6E=!cPwaJh+y1>+Z2WhZ>ch*UH)y|k z@I7DF{dYWxtOcLD9~IyYqKeH?Qk$O+VP?SvS1*YiO3^j5#53OJK32J~x9DvEQGvMi zf!WMv_L|)`v$=B8Yh_cNT?n;54E*vAKx6;+gX?6QbLHeYF=OTSEDadBU(we$2Z|BB0q?b?;3T%DP8 z$3K);^l_%%k?y1_@$J?go}D9b9R3h8m&VJUne88cm9blVGi=l+eMbC;n+8Yf)|KZ! z#F|!(7A%5x7~g4Pq1wadzsS3`;O1zgKVzP@T;d(i7^P}E{GfQ#`xp-XnA$_9KPQOC ziS)e%LwK_uyS@KHQ~TNCb5;JK-@ScL&GcBDUz+BvEyI^QknQaTX-MR)Thi1+*$QgA z_U>zHZx8i^VcG;zfCyt-Otvx6(KpQz3|n@NVeeiy3#06BE2|VoUO^g)LsEc<@%jBg7WhZBZ^gyC1(DzgQMf( zyUQ;DJ*vX7devdhQC&lWtAo${g>2))%LuiSe*OA2A@EK+?HOrWec9U5QnQOz1Zv3^Td%>go%f)yK*!DzdXppWE?b%SD!za>4L_a>m}2 zFNxL5lFqZOIQ-pt>Ce+>t`5Z}nVwMpWv`EHcnu^|8S*U5r%!Bmzii`OfCbXJ(>lj_ zCTyE84h|JgYlKuTlQmii*xm|AXV`Y@%+Uk8Hd3f=jqUyPALh$j%4cHoKIinLQ#lv;h591&7Q%$4neQyWZ}cd-(yMP`@EH62x%H2rkdXD*D}LDX)^##n(2n`=!Y%mO@9dFB zo=iWmx9cV0`CT1<-@e}7OqOR?##vS;WCd+U3I@12vu{@Gv9PU;cLwubc7TD<|>O2t6|&Zb)6PN_i|>*t3=&7*O?-eXfElO$MWu>%Rx*@xX%MT`vwNA zVa@@w!$jUxF>M_MrO_E80Po!M+x;onU3YeNCL#5FHP8?cn)~Dl40K;wqr%hB(dF>L z)C)!+50QdRE9Tso`A|DW^rw=XTmXzxUTr!3vUz1y#12sf5In)3_Xa{eJK=6RQvOLy zbiJ0`&CRXdAD|4pahq;Oq!O0V@N?m13Q%U9Xkog5KpMHxjVv;TMe2Z;w1IH+JFcwm ztFhV3f9|;iT>t&yH@s-z6A+qi&ddD( z5ywOb)8ogYKXkDqdI}+MwYEIl8)OR$U?q#ds@#FGQnQZQpwQ&U%%CM|)vlEW;x{6h zvS85SICCph-ZZr`Es^-xWgOaUg5tgI{-SpuQoqExz|VfSvsk@-+hHXRl7FB3x(L^{ zy2H&)eIXTNwzAv$_A~X>XwY%bCTD$bN;RZ9blA$-`L=A<+K5`C+gi4y-5eif6m*Tz)gA|>ulUtKMqeH* zG^|DoD`FJC$1HWy?aRr#cklX=85kJ&GDru`X9XZ^_SD`(y!T@) z^qzXmc81UDQG_-5@`*WroP73!!^+92D}qk!VR`wuHB8I)A}kdCI4+;(DeStFB7=h^ zGvHu(VcqlK8^5um_~g~^3CA=?&@KwtJ2><`ID4SPWXHmnv)fMpT^GU`C0wInw{7X@ zs2W1pms_`xBSckcrI%TsN?g1qe9eOlv0#mF-X!%CANR!m)BG^1z475iLa06J%H;Gk zDopzyVy~D;-uO-x9ul&fy6@xlKRTQVtq}>)ZGMBIkK~f0JD)W+avUq5d0?EXV;1cn zTHkuWwv>{3U&u}o?z1_Sp|%Q>ZF^PP)n(FS9y4=}kH^tWSkz`7zvAg4gIk?sTb)H5 z%K=}@v~P28Yak2?@=^27v9oH+>ai*uKa``*$(g0it=y@SgB4D*7p@74`{?GS@%A(k z3UVmn0Y&OMRf4Yug~@v2i%1fgrYm1R%GEl0FY%pa-C6K#VO=yVk&pe-8JZgrbU9^Z zN(w@@Uxas(lQndANB#zeUx%^>Pzu?J*YFMN#h^K+4v}vs@=x+XH=jf8pzP*D*|E#HRUH%YgaI6D`#_iiAnA zbY7R~W5LTCviMHlJoP^I;)VOF{-sOB$$}0npb3$jx(nux1ifHnL}OK+1ZzA!e3!mo zUpKY1q=uef|9#N@WA+Xrg)SqMMl@U_RSfRHlDl`$YAVF0q%>DL2%Wq#e-y6ZzsGU! z_uPA)wD9TDq0S6Bg}CJ8Hbe)GlwTxWD&tj3-|;qg$$8lMgPgX&XM4^zzOmDbx$B#Q z6$K%;gQT<={9oe#KzuL^w1>FNWFWX{g;Oa-ZJjUm})vlVFTb-8OzqsmAj}%G^7@EMYj2etuiU^XX;ktCS(0Le6`qLM;0x z*vx9ZC}0osvi#al$1lTWJw0@yl!qj4E*_WO*wGLiW%K96OTzA#i1~vOsI8;pQ#Hd& zwBS86{RNiWH8Ui-?{}Fd3A4pEsUV(>jMs*Umn$-efL}8|h<)?@1@%_i|&?G0fSI+z2#~?Z^?qGS(y~P@^9tZw>6Yu`zv7)~;Nt}UG z*O&a+MZRd)R8XH*PigNw5`!AQp$ec-?wDflV<$<(dC0GoT#Ec46{0HZA!zV9lZA)- zRYy|@-EYRDoST$-VFh;Grqq)&V-5ykriB6pW+8tA2ye2vxvpCQ!S0=g;r5TNu+aF?&Og7t-G?7y{EpI6zHfi8A4Y5g zHfn=0;2@@(j&MuLxUqfZ`mknrcsOSi(C40J4O1ei?CsmP>!zq1>Ji~K$}9Z8Ha6%a zBqZWJc??UKg?Lh1TMOgwq7VT5WX)CDA4xBLGcS*gy6*ev?*rF2x29HKvRzHCDp)GE zQL=Po{IMt~epsfdx3}eBO)U*I8_R?crOu4~)ENgZsy&lUf)gKW&!h)O>}9k1l#x?d zs9-$yeIk@-@=5%wiE41mEG_NimE~O-rsr-*@3+1;cIk4kc^+*+n1;7z2n@k{kvjD> zc-eZf@dq-#EVCj0YnB@-cv*ifyktlWjq zd694iK6icXASGpe@B9^f=&xWwN*IVrOMi?PIa_2f{`-SuG9uTMq8OCaOoLVQSiFC7 z-@5kGxA4=afYhSBzsP?XnHbIUVPb%a(RFiUyYt7|CWN|jamNx%Hb^H>LKYnS5#tqZ z-@YBrAQst&9RWBRJia`tIm7p%Z(u0z+}~?n!@uf>hJsQ=E?U{2fAtL9*}|j>ndkS9 zpVlJi2nQzGp`w8|J^Q7kq$E7Qw7wC@L5mX4rdt>vbL=)AfZC|^^(44b6K)OeRxH=o z$j`rf+(ko6qkWHF>RNa!N{Y(_iKwWkFXuS&=cafJX@1VpU?b|b(eeAW5-t<0bfV4( z=qa+dx1ZNq`SWMVU7kd){@9z}LUK;k=3vjW z_udD>GOgyXl$64 zIvYr%_^bIZw8twy_52YTTB5K|n3cA5Qc#ssheb><(0`l$gOMcW0vhKJVfUR5`ZQ84 ziB7yfHJCSJJHO&i2CHOZ59{H>vNz8>#STqR$1B0sef-#qrmn7Z=<}8?4;CB8K|I}8 z<8}ci=27{Pg_D1FAKXfTbNflI2v+)}_m;P(OYYFSD{@IY16el%m(OtShErg=SQHp3%Kk@UsbbEVx@egk4+@b;uc;&m02t!4edxuB8>P_HFyc4F8)>YSRfUVsdI) z(>NM|Z5@Fms`jC|cv=WpV53sqv0J>5=5uOh(Fr(vUcPgOrWv0Lu~@|tgK1hBlje#A zE#R=XWL0hfrSMP*Y=3(iTAftNTdah-T4XWVTRW?BS%h2PO9mlSO@B`#rE5 zOY5s{*M86TbX!zgT3N|@XX|qx5!>)t+`H$^^BYo3vcO=76?eFO*$gyiEX-fYuhr&G zP)|D*l=S8#^v$;*U15>~#z0uO63S3$2vD&O*p37Q@87+Tt`Syk#8NboC*0o@w*JD0 z1D8oWDKqm2_}UW2r+6PpFdFNF#gsk(QSZNNA`n_n&dk7oy7%>KMGFfiH8r*Hh=@XE zw8*f=hi7&s9|&J~A1y8}wzjv+YH9685|#l1?Z)ZwaK`ZiR>g;gTVYR9@f)^fy-Yy zAdw}^r`Y(UYWyV*5718DzcA0&c4QA;sDG%8I`bDn1;IgaT;rnRksv^zo?Rp-T9^dYToddl82sw_bgDwd{SGSW;?A^ddWh%<6AWzO8#txP5Hu z$Yt+&6sIk|xk9=9?;mE_MkL3!wzb{&QNH~6@`vymZ?^XB2pM~_aPW@bf`21ovv{EnC>XkuW|N5-k;?{5NV${uh9u;-H!64}}L`APs1 zSNfBalP&G+n*TiU`OOC(N~_t4pp`#jS*HXq%JP@ zfAUWBiLziRpea7y)z;M||KaQU)j@HC$8dV4re0juIQ8Yr$ZIqfv2lq}X6Mw6wVykl z4JX@2@vots_aMfa_~0Tsj-GqtZUg=OM}LjDrOnOFmrl14FS#+VX(0FZegwdl8>u^- zG?mQ$ZoH=E7{U`EE3+GxuW>s+ev`hsM`qRYXEOusuycqq*NoHEC7S}ZcJ=ADuSWFT z?{usXCT4%}>w))$drd_zzk2=pZpCpdxmq?Tu_W>k7$q@E*fU66eT9J@*-HY>-;mox zmQ{RhX?mYrL$IZzT{B_iHZ(M)S71;=3vY3o#yb=wRn8sRJC^2umZ%wnt01rrWIiy% zpSB)WrSPaI#|q@Hxsjl(TU)1eP?VH=S_xZN7+WwUB_}g;K1ZAf5ea)=L9==Eee=*0q8Qc@BT z;RxDIkvN}bFQ|?kJJ$YSU#cipbN3Lg1L(I_d(pHK?FA-UR;xEx4)LZ%VVeAZFRzD| zqp&13(r09qp3rcX_>wE`T^pv$I{miGbQkw+MR%z!Y@5t?=UI+z#jHXef7k3>xGXaV zA1|Z(xwG%N7RLIlVb5-R>sc7je~9zPf9T_)tLMz*h>)+d_v-2?uSJ>*K9cmE7qh>+ z^dD4h4c*h*kEzM*Qku3QCy%_9bKts$1b6qZvlRMd;x*!vlWGFJcb7zuIJD^xRgWoX z3UVK2v}n$%VBp3lbRTZeSHJY9?^v4YKgEBu;`7MT3>o912$GV3Hqt@?B7GMl!IgVrPH7|GuxoeK{AHU(?|`uW+W*_v~)*Ca*O7Ch`1s6X>QJ`VY(3-)dks zoQ!C_-ly&tKN{Y+Ar0vea&-e?MjI2qoRap9Uhq)`XI5ERFpww%gVz{#_bDkU!Qry6 zzh5@}#S3YenA*I5Q(oyja1VuT4DFMxiiZdJ!5c}+>X+zo<{RIP8 zcb#}~M1vwCL=GLAHZnci+}e7FvPQhd#?E$eeMFDX;;FK_`tfoXmnSDB2P%z)a5LM( zDxQ{>ww6$l*!Sn|#-f<;KKr^?54Q&1CV$oY>ZXbeyd>(3)aCt%EC2QYru$5C{<0C~?(-Wk^?>F(}UPd*hxsG<-) z1xkxBSAtq(oUs4@+e3$FaViZ&UFyTE-sty04?%d}+TO4-oB#9k*RM-_K|w)jQa*Qc zlprT;VxE=8XF}>3mln6KiY!>0mH~7F-?aUdW`|9`Z002)(Nr_jqv}8tS@@XCI z>3jnTSaqUr!v=KjjlXN_)YjJ4hfbX91i%&&9-eEN!+!Pa+n^WMuUTHa*wGRl7uNv* z`wiG0{3!JdX~K^ljf*yXi^%qU8P0J^eXUOg)RAaKA87bMr5p?}%>Sh)zM$77<{crQ zh>K323RYYbog)QtF-79!V!mpfp*^9Xi#?3?LQ@V zaql!|a9UjK*GXjQP)IkWKD@CTx1E;IxvbLIdt5ARn==}A?2ygI@A$}g@Bee&n%il^ zR1=gnHKrcvaeK$bgcO!jL^uw8TKPLr(0IT3ljZCd&aD)Ak=d(25tFRtLh<+`vm8-3!t!13GcXlx9$@ca93i5T`? z@6F7QhJqA#M8D`NCj<4m^;^H8eD&6G6i+BGLy2Y;qk2PP6?r2MHn# zmm5K%ypuE%{~>H<78Vu=`)z7B^C3`pfW%B->MZ|^~m8TUB zT6y8>l3~D(Agi#5h<&g+?-(h!CI|{Y(Y4P!eq@-wWeqazU`?KUoiLnI6r?dSfq(tp4D%&}M z)`Uq9E`oGoPIiAzNMg$dKG2eH-n`L1^m%rcxI_@nVD#|3v`oo6f>t1;eHNdZ5daO6dZL64aGps5Hl398p}Fz6B~u`ZmnA4=()Ft`8=k z9xNQ_OqNQ8f{#d{CuC+k=m@Qm=|NnD$m%Hf`R#md`CEW)VIv&O&CJaSB>Y@8?0)a^}({`u&X3lha-A8XFsX;rHPmAPMUulg_TLRUY765Zbb9rQIat zF*?f1o;N;}#}N?pOFuu7%heupCS=P$cawitROt2DEdovqzTia2N&&Y2a26d3mhk(=dVZL@nF6Y+L>gX(oHvq3 zs7=OELDxM*&d~IDn^F()+0O)alyk3dZ7o5j-a0e?J9ig}#lyFZjA@|~9W!^+V%z6F{}C>XP{Q%E$v&X);-!Wbf1s4cgf|2CJ3PCtY;FNcUhh>UtY8= zQ0rD5|Jd>8iJ|+H)77h2X&|9R6)(1Uc@I-}4mgRK?m8lYgO2W^bl|sd^qLvcT|az& zBg}J{*tziASmi*uJCpbDVKfmT3+@yXdPrEr)Y*ETkF_cUZb*$`59+7_-Ml>n3?eB> z(3qGQkQ(5-3l|S8(W9V#DTkKk5brtoqvbgCWs9T3E41i2EF>hPs)m#dI4^dmzkF4Q zP$DpVAmm^sD2u6)YvIcf4|x3=gc-(h{5=ykean{{Z0RB1l!0soH*^4mfda--_$N# z;D)X7(~27n>?rO;;P46$o|e44JQfaR&?g~N`4f5=0=Pv_Fn>z=UVJao zz-#sdE@HlG+9R$NJE3jZR3ONf%_POjC7xXI>fOM>|4}JvL8Y`~$<-4R-rD++{fwvY z2j0^^?5@zHP}{C5t2Ml3nUgxi$UiPYs(Ru_b}TbrGl|He840!0HRMqQy`{0Io(Dvr zGAQ^!_R_SPzUJvW_0J#h_$gUfWI)PASV%%u+JOGWe&{|!0*`6`Lrb|YD_)D z0PaQwg@tWkS3<@h5&z`LNgTdc_n!Zu5%M_2&fZKA2agopi;If|#L5C=vY5Gd1O-6} zhtNhMO46J1V8?Tx-wz)@&NhCur8#Od@(`~{W=Tl|Vf=YS+?6$J`&zm0EQ_$P@QNAH zuHcV`Bno7n7?BEsFJ?X6q{>mJ$bgsd@Y!td=}{`w&qgQl5~P2!fc4I}8!L=4%JK@+ zpUmkvl$5|eyh3F9su7@Epp!_92*$yK{^2$_ShKYBf*I3bwA4T;Z-;~kfas!BP!Pl- z89)i*{2(F=9xSooJ#}j;*h);R?pt5sN3J8le(AeN1Dimwg|l<1z74Jvfze1-)DOUL zV?PK{&J!1F2*ElnuontCcME34m*p;>6j7x+#>#e<+x^@F>6$bt{<7~xOB@(!Ud85h zEG#Y(s~H68jxa!I6j~ufL=#5<@AGBB3v2F_IKU_0ywj_4JbV0jCxBE1Iqtkb-9lC& zp0L4qeM-uM-fPQTeZQH2=*eJsa6mc59G!)y=Bo_Sns@?)k`a(%aY@O-6;EJ7F#u<+ zY@LFV3ymDq%YwhqJHAdlTv`xOkzOcdS^zH+_ib7DBb$(xs(}YNKzEiHZT;wwr(&V9gEa;&ih+0fxw(&z zdE2oF){s#PSS{yD0K~=D$i6Y9*1nDD#EJi4_jM^^;O!&^-ZNR+(Q&D2B%@!^rx-{CO?{p)BgwhB_vfL@0$6}>O+r!fdsHhE(kws`df5d9cd*R*2(FQ+iVh#R@7%cV z^9ToHd3kw75}GrBT+|S74zlIvg3R@X4{j6m#k&#I%EIE0zDAnF)YMcloTd(&9?@8& zZ*07RU|~$fi2a)BIb9Z)kPsBR37GT{V0I!HaBUy0;89^R!2GDVD;9u4$mm4F-Da{^ zY~LL+GfU&z3mW!_h${)NY30$0)NukRfg>oc+oscKw;F=4fN{geIsf}|ed5uHuUX+o zely@W5sC+9=#GeA1)tnaNaZl>SWPY3qMl%Fl<23FPg*0Bblmp_=L;)Bo~5!ZU;Y;1P!hM zKpp$FTfoa5<=_a0jp!Aqs11ru8ZhF6pwln33J9v%vGexMOnqHhb{=+o$+@}1h+`yB z+k1?R!uD^X%XaRsLq-@V4+m=M!!BIY++WO=i`!KEx}Uv|QaPjTB0Rr9GbcPH_MROq^|KJAdp z%-Y-^fb4FGWjgPe4^Zs+s2H0ZK|`@8KE9Q=^HbE5ufiVoXB<>mDe(blVa-X0i<9#x zJgv}>K)#7*PyqEEgp@LvNCz^oiVKLi=3m)8>^s}-?RUMtZ~?{3s}bw!}}WYNcVqJFf~oBXojfxSTH%i?<6OoT7%3a z5>WRS+@n|!B9fVV6tc7l4@n{_&4@LdSQX2V+zYLSG>P2y?Z6kZp<0FG65fQ_=t%CzvP97bPAaia#*B2OCBOrwI)C+ZIOGa+6 znF@+Kf3S~FRHg3XCLevPd4`dj(p*aSkws>ANvKfXS+nDES9e{7ED*R-*PWSiPMt}$aU-!b)zQO*!IG(kH({j6TjA*yU=VqzK@S@bSQ z@sUk*bV|4xwVQReu;k)_j{`hbKLU{UAtdB(7Bm0|Z~!^gps`kNw9IAlaWf({};D)y$hUHvrp>p`WhQO_9UD#M? z28u5NONkU*CAuE2^@E~3TQMaVZtB%fScRb7UhRqhWE_AAWb**{5ahrdf~0IH@9?Kb zp_gx{OcP;-hEixa9-0V19^x;>pjjyY91DPJDEWOueFk&B+ek5P{gj(4bN1%71<>1g zY^GpuzSxx}k_O?S1E8KB`kzH?jy=U6^tH7lfynr%_UccI(rulcV#;<}iNsOyDFL!y zBZW=&$*Yp8hPaA=qaA!lnGRP_K2$U%1?c*d6%GNw?})i6oT~RfV4nzrpy=q@kROrp zH!tJi;I|f3_xn0-NK@;o4P}|+?9qteOg$AVD?6ma$Q2JRjirPR_+u1HoMEFEy~)`B z=yHN*w=(0ob~%=HNgTT>+l|#P1J9!K&tIWCp`Q$;3VR@ITxkUb!Lzml^+V)tNu$09;Nn7#qBd;6-r$+;sO-=pk%?w7P&aY7Z*+@fV}Q1Nk`tF4+gNZS@HsEb@G0t8O=m$sFN`jbI`7AH*J-s)dD&<_P>YHN z_=~V6wVi9WOBM3)kNbw`793Xx`~b?>tp|$?AXGmpqPyqhPXvdK?ee9EQJQcT)6UVs z)Bjl6XOY0en|1ear@L!!?*O`{3}5NnasGHzrqr2xyqkP@{!8aLsl$^MxL&tnPd2FJ z`<`Tde27o3q**vs6Y$Zf;_aalkz2WxJi84LqrLsbQ4)0|gN$6*bI8T9Hm1UrEMOIa z{MzYabw&XAA+tUUAZEXRhu=U&-J9=v{?^sC1SWoD&#?SHHB}L88X-T4+I{7I$A_JC zF*=qwa64>1k?BsE2&^3lHJbe@;w% zKs+VjRTR!rJI+K-ycMgps1JKd&MFTV*V1_4CWd*`($TevP3>=p4^?W~nF7BUQaa`r zR~6Ssm{+Gti9z}Y!+bXZeTY`|9X^5)xEqbh)p(Uf|*_^tW~RmEQiY!sohtqhWgU zzvz<#8AGbTPOvPjsaof>Bs96>kM47@5qj2cijQfi46v14W-dhXi<7 ziY|xU#YJF*0${Q=vIK)ssWmPwt(SbX@avmy**fvdO9kZKz(Kz3A@p|VUr0~xYc66B zz}ym(lM5+nY^{C_CXFTo3)1c$9%J93ARQgm6v3c33ru|0NG}|bpJ7+285tDND4hO+ z9}_x-Y!`!nd{F&W^}+~+f}s%sWCn@TfI&{l0)jHTJ_yA7-CgbnPEyHJF$U8Uo+k5= zGJ3Hl%8IwU6p;Tb1#&$4j#@iOsEZLI11w^!Oc z-9wYvSr#l@Hy=nS-@jFI%>QFH|LSZ8F_2Fwzqlx3V>iD-cUc zl;SUmjV+09_BpSqxwjLyp@b?+&%|BFENPB%%y>(7^%=(Qs0x2>&fA;ozb*oXU?S~VvU=%_-h^#>H+tHHwk2TB}?4g!Xul10RlXJ6qe|FJ(Igl&oFI zmL{Dz7%aw%Xa}*FXx=~m4ZQq&kKXL= znPRIc+ASvttgNgARYoPLc5n^za#a{uElbPM;mSVssB&O9!>g-`(d!!^5+`e?L+4Jej?PBowuhd;IwLuD{IbxI00SRzw6bScc6T z0boE>rP#{dj*fLQ%Z%KK)+B?QDIwpo=}DFaM?qe z%S8W)UfYM+tk>+J7UnHC0WK%p1V;8D!@}P`p5X~!+<;qFnaWrCveu1Gc+D4gPIkK7_n%V1kEKN+9w@L!+nNCe|T4G6(DZOrfRhD&E9o(u=} zDxQ2{6+tx0GUk3?pUbZ=uPk13b8{oNDKOABuBI$u!Y`zh)2jY7$>lDY0gww2@=&7u z&u+x49}@L?Cm3g%ySp*upd0=vz&K+9yA7D?@!+yp$3fG9(_h3oBAca-P4Yn8)s-LU zK+D=lxG$uOwT|k0hyvrgLKmHkq!mBSqY>6ANTvq7QH$DOrE(kNHoUvW1y7$=0TM)L zoxQhbkEEIk|M@)w7ukSbP)O*lL(4k+vjD)L=6?VF(QUv*l*s2m1L73-!^LITdD#lU zX;^bHa!1d7)f558KnQDVYx}W0m)MdoCi34(>F)+>Lt|XjjNefkNmexiAsoli5l<%gmN8xcm^;vMab2rt$yTyP=ibmeIOoR>g=k{EQAL%2xBvhV-$d={ODY{hp+nCRHv1B2R*vv#NPZYU{d(&x|8Ox-(%yRRm`rDVzJ&xY}6*m4T) zHsb(wcei^q4`9yOHx$wBT9<1amtS|DBcBS-I)tg0WqVlMGVkLy$avqLo)NXqXUlZE zp~N@frg!>voU9iD401(As%pv-S`o?{8yn_@ZEc=4XVU@T1V0eW!Om0T#uR41cUPG{ zw(KSpjy6xMzh+>4?y?YG?GtcI_xkmUA2dJehAZ+3D4cx2rAzvHBr?8vAY!J+KiuoVRqe)!G=$sLc6^Q1-{h#?B*wqq9}D&NA&j&rLz`$F7=)fV8_LOD+M( z(op7rB2hk?G+5){u7b%G{!B&MeJr+(}X`_?lVq z=|)3InS;8rTDQ#_=_~f-ho!BBDWuG^a~twAb+zx`OLxn)>e!@_aq-)&JvdBq9rrGN ze;+XJ|Beu(D(%!~P*D86;Bii;*g-jpcB2XXj*hP)Dmk3^vJPW;d8-V}wx#9TOzr>t zIi8bVVwVp_w{$4@ZCm&CEh{6!O3Ycq*vImV`^{(xS)XhXP4KU%!LU4S!sejfl)3XY;PAL0}%IB z5I)Vlq@$;AXd1+^TH4xDgxrfOg`!c1?b>6G{W@esikg^$%nd|E3uS8=PVc8sdkVqq zO$51Bz|^6p&YEOG!f4?&G6qtbaV}DSeqWx}06>j!9Y>;I3lXKMXL$Cl8{j%S0X6zq z_w(5^(91-wyhA}8ISL*zZwqS578&*2lVbS%kv^w)`Jon`k~7Z`_hJI$(1<~y&G+eZ zdy{+2D#}{T#V6?tqYu=cX@>DBNjbAM1}&HXOVRu;s^ov^oKHLHux`wHVXTn1|LOiv zISg^cpkxQXER(h!>tzMeXV+*Fvov??j}|1w9Wj@Y>&+oox*O@{-7ig<+5aQq$FPqz( zR9V5R7UL9jbp+v2>B?=VXU1Mls!Z;G=(kcIr+lCB`~XTiqr&WLR5LT${KV*J9B|o{ ziF)5|QU=z|R1mOn;y4_jRRyG0EF2!+&`@+_HJAo|>z$C`bvnAId3)!$6}!x8G(1As zHI2^!A6TnlMMbP$Bgo^vyuKh~YA39~F3W&PD)7$Wxn#>d@u<**He7p_I8$TmgZBa_ ze_~nb7+A6aH{oi1hZ2y$v8WgoJjga$V0@pT!GKL!N87*ZTimyYRbKzq`?64|_m`Tl z9JyWtmywz8^ybOgAGZ7#=vFxOtyHx;*M0a8Tg=%WYSNhp1@?HbYvvS)O*dc|KPRLS zsycn76^B|7SHpqgxmii$)9~Zt(W6%d0{-on4gm=wCXW@rg-_DSL?JFR5rXFr(;q$3 zilUF+Hrk#}gPd6Ut3RVw=g$H2Te!Tr5Ab)(0F)DUn=KNJLY;QVYL>7Xuq^+q{#Z*O zR@P5r;@?Vufu_B#JUqv1aVmAfvy|x`)ExIrgp!Ix!pZ>g!ILi=M}4Kvke{f-f-JrU zS3xkI&Ru&r@@>Sz_kF@?$z_RYN@c(@&YVy+PLgC_t`(qIBvs5LRDGIWz_`R?Z9RlkZ2I z-G+d)owb4Z3No;;nf9n>^O}Cull{`bTI;xhi{EP%7Um4&!S5m?|10twP~=baby0kF z)_z5T4Ujdrf*6WSj@r)gVfuF^0;^N9=)p=0>5H6!KIMp79~|cXh8CQwBTpUX&KB|4 zIX~XbjJ1`c#Vzjysb@#r&ZH0SuzbM5huSVp8K&6OYf*1&R=+#>dAsf`9O8V*+<$vA z!H{0WWOjmi3a3c`9MB1MGGh(#lF&Gp?ENf~?4SJw5yhM30*4Gkfr+XCN z$sS1+gt&3}DT(;S$K%IktI>*(K@mCMwDB^AUN-@hxDdQMPKb^Qd&GiVXikA8T)Yt* z{}m4!8!FFV($lW%%#fH_p+XN@kK-wR&#~|LatFU=$QHxd=Yd4c=sfL0%VP=0llh^x ze2hJ{y1JdvSp>}^Exbw{|EFJCb;1fr94Y>uV}pQ}N<8%|qKTQN!hYd%epGXEjrTK}u@y090UjV(;! zwA7p~8(RFlRmO!?bxD(J6dR<;#m=@FS$}RGELuKiq0wBIMVobYDt~r>x%o5EArUQ( zl9DmR)k9<>7DGoar7BodPl!*)7BxDBUW6lehS#LcoF1M4h)8U`b>)dL38>e8ueKX| zxy3+vC;^DtdI)CQzzFhS*w~LW0xAx9PNg8SCv#nSq#aUQD+CqCO0%fnz|1V79mWA9 zFrtod@T>a%QqR%^7>kx$;lucz73t$cd3Z>apNRM}gIx%nxdOrsJs`~lE0#uW$DIQz ze%F6yysm@*+FO(qx;Pj-hvY#!2Ztu?Ty=&x7MDc$i8!n$k$@{^)0of=`BN+hcG9NL zj7*vI!x-Omy1T`Snb;#|hek&Ywp5)SEm2e-WU2rLI0oSl@FP$IiC&7_b}R_+>APPGQ}hWU;bVEMXb>YU?2*)!e;4L}%#8nT#p2&v@oVMX-&;`t z@dAx=*p8Bv43)`jMO&Y6Um8SdBuo7J;T{h~h8*t;rLE=#`{$TKBvnR3L&n(TgAts| z99T@9tDzJbO*$6AX(kbslY4@yCB65_`U2u$p^Iy1C=?jF5kdU{s6%hwycrt$Y=>ODXBrvAk zaAx5#0=P=2)-{%JaJwa{1r|u#GeKaG03NmX22@*16qR*!z4^#}m}9v%)wMCb7lXpq zZmSz-*xFKk=Rk2#d54v7usIRm+$^5zx<*ftF-Jgx?~|6s>ROvxcHrm_|+}GXVjv-(N_BIY<1y?@TjhXm$a@WpS6JseLL& zfX9(g)`%|Ovyj7CWyQq*8ij%Qg?5HgPUYs`j!EB%*f6rG7^LxOkr)}*Z%n3$;c8*gUZk~Dtpe=o#rma`X< zGeR;mDVfXNPetIYjsQvES4SnGU0x-@LoUF?VF%ua{UX}gsIdBjNG$a!SIfg(UVA~T zhP}5HZD7xYWA5WRD}9hPt1T%lAEILqaEA z#lMn85);Z19Y5pZidDw@I*V?#YpSx*$8!H}*yeIFg|1CSH&e2h1VfuyGsMwxOB#oY zV!~KxG<7VW*$+;JO_ULWzTD8n2p;^t zsM$vjT!A=XWbSufdT5pA@$ucb;h4?630DNr;0#rUuaCpS!{Kp7h!h;4Int)!=8g>w zmEWpyz#jSNb%iD%$?BT-6o2;?uQIfzP^;m z&SC@`n@Cne1FtEd#kc^37!cq=4LVR*n{Zk#pyjvt?t)Nv;~640go7HtgvF>EzG`)> zl(t|M?8U_I1vYA91O`*UItOG&1K0qfBL}z&eXLfI=ko9ZdFQtg_I-&67FcmDZbE&wGBho5&>31dRY26jW3x%p?x%nfYR%xgFT zes^y9kw%$NaWkjLjcJ4wD{Cjzc0UGtaqI}Gs^!M_@(a*)tJj~4vk0@u^nSrPL zOhEfsqwI^ujReAqUjhz7&=!8mNi%NVG1Q8-IGzLz&+#sF;K0SUD1^Q` z_3hj?0)^Rv-M)vm8ZZ_ozS}fu$HvEJYUHg>R&oNz!3d5d&}nPk1qbF&K{%%nV59|+ zt-#NR)Q-88l|XJ;IVH2B4ZgDMg-=vJBXmQ{@7&w&)YQKSi0fN*M50P{1-=_A{0t}T zmSe4ds-l90c*nRhtbaLKoHXphI6}{Q#1o$#lNbBo0p266RO=K0Rs9#fLHV@;N#2YJ zZSVW*<#?G*xrq8KhiJH~&6Daj35 zN-!EUcpjcs+5FjDKgmcfU6NiigJ;cR=%bav=xSO8k1N9{A_x+M)cj!e{*rrQ++mCdTV~CVI}!QqiWF?EL1X9x>t~;&Z+vNl$;ImDl%v3;8^1 zaBYzWuE6g8eoN9X==j}#v3+|r)`)c7hp$A+?-l@prCKPi^1S1sz(t*bIw~F2 z0auoC$y)$*09wE}%f2DXN0Ctucn&E1QUfca3(;k`9EqU~K1P4Li`k#ZXnBRUF-a&w zu5Z7;Uop(0SJ~B%6BHU4(o&RiZz=eTEM0D1^K=8@3jrx@27bDz?aqC+_Ae|S@5#y& zah0P-gc$X{Ge(PB)KcZxRT2HX07#04Zi~To>PD|pgULiYc>*@y?bxRhY6$9@%vV}0 zNhyhL1_&y^>jNZ!Z~$2q9Pqwsz+wi5+K!G6b6^-0q(daRdAW8;kskGr8FDL!6Y1+s z{`Y7pDN({C#k92UX)xYtjw|FR! z%Zl^eC<;;uMZUA44-Y4xhoYJOy{-~bRI03TeMNbk3acNV85Cv|MxiW6uyHV@T0x#n zvb4O(a4We{80^B{ZXV+PP2h%oeA8$v7(ll7MkFVbjDNRLvLXb|wa8UX1kBWiuaB@is`>6vQ?h&{)HU8X0B>0`2y}}U<2GA+kGK*wi$~$>*FO6G$Ov+B_p%X8bM>#uu zG7xK&VQZIOOo8DL`3|crSzJs^C|4peVSRRrH1P&(N@^OPLh2h7&*tX;xqyGqCQdGz zaJYbjcnHjU3M@F3>jZ@Sp~F(6M)nM>VUKhv`q#_oo;@xTzc+JVXG8aMVbRY|(_>^G z)-2_9HcZjtR&y?-R&WHd{Nw@mh{eU)>JL-PN=$eq_%#tr$R7a96G~*$3up&itgn(D zX9tg%nNvXW3Jje}B;rX>P_Xz7u%~0<;s%nkj6> zp4_arSpNOgxu~!#2IhVfE_;S!rX@)~%q$gcNt$o4rVCDIxBELr+s-4!@(P_;kl^pg zI%&D4&3!va<_7AGzE)av1S{?TINJX)7h>GfsTVl-P_9${`fzlYhA-)k_en}scBru7 z6mLpgkim1JGVQ_#kMA!q=g_A=-sEf9E{t2WXIkg=sgm56g12#XDi4381FX@0e|*@J zDul!~jsn9C$nBIPJ!4}*Nl8T7gFrw;e33LuN>@ql|6Bg+@RQprZfRFcBk@!Bvh=4y}-w z+xw){p9OD(IdK5jdiG`@L)=LsQ9xT5 zEo{!_=qT%06wIWqyVQOPZS!w`F?Kts+-HCG+v4FNWF&pYKp@C`B6*4BQNI1z(* z8)8_YIRe9|`y*f6zFQ}Yc#(mzf3d+eJG-xf7ZEvt`!oiiR#f2|^v=L#@m* zOGNXA2S|Wc;A zQ-`IydGh0Lb8B&*?9{R-CO|uy`$w?c{FouVf&Y*x@%i(zo4~MW0fW`Q4*%B42&~q( z^TcrPuikV_>`kG{WJY(cVjCPOz>XX!Gdh|n#YMvJc}OH zOJ%N+u9=w#5M&IJ*jJKdsx*$TVxYo&Qm_fQeU{hNrQm7vJixk!t0P4-Z6L`(1I{P} zQiB-%8Zj|(er$9!C1yA#IKubCobcx`1pIspz_(vWtE!_z7G%1TzzZ!Tx?KLAVMZ>EhM#m-^dtQTW!3@%hhtv$Ly|3@5(?mPl*iucvV0=PO&c2Ew1aH8k9IyJeOHh$$On{*H|qVv|0Q7+6$GNM@|` zEgB%s)Kj9Afe9qMq2{>787+>MbDe@41FPZ}w=J_|=DT;+y6r0X#!>d%u?Oe-2RpYL zbl|oF^^5L6SvN zQ}{fiZjf-$7tt-(r*sK-SDvGHVK`!B%cdx(bqqaTSXhLYo4pD3AMS*<*PE5fu2>FG zM2O1~gIrncqGTi4Uvy;{M6WhU*q+z^ zkxO-1dJZ+C5v)=s?(e48TJH}OT;#^`$q^#G{Y-i?qS6|FHLJP?3m zZq+=i+#=8s!a66Q-yD*hjJp@?Z@Si!Ihfh^*gxhWb@*yM^|_}zn|=*dV1NSvQMF}7 z;m=mGZ1F{!#w>y#c`zSimZ^)0kxI5p8@;)+w6($>Dk+nrWSm!$UO>R^NcUgrfD0@m zDzZ;18p?1r9UORV7wF)M-*&wBLr3%(R!jNfF9BX-tK}z|to7&Ku$hV7d@+LHM3p_) zE(MQP#E^x&!;*;jXyE;o9-D#S(j5&~%2f=&4TEk|8l4>DuVXO{Mn>Zka{q|WHV+B0 zEpSe99O%*x?`oE@_|)MtW)I2A5UPn zz#|7NrIVG+h_`F2Ij44`m{8TeKA3Rcpc1+unenW~bdb%$u2l>uAGC0SuG+vF)IvxW zQ?1ChF|ZP~f}tzL6W(V^gIqaKQ%*7g6Yj0sneC$NV^Ps}TA86;@<4P0sEaE=HT{WA zpK`7#L%a9+#PDy&y0c6VqiK?Nm1DZxlYin%ODl)%_x7ZFc_XM~ z=uGXePhPaK^tLR>H@MU!3t2dx?wO`~9D!5e)DvX`{K71;TS_CB%XcLw*;5tY=-Oe} z=BW4-UTYskfwFOX$4;VHM@or`otFYlAj{vX--4K6b~HMSPl{cp#y!g6l+gE&V@@Qp zm~>)>J&zXSe|4lAmuxy>Xh_}l0Ih{6D?2sI<}EMJ1jwfmoQ{=AyQ_Z2?UtM^wZMn? z_?kx0%7MXsYck?u2WOaoQvnf_wJxwJ0hu!q&5RSi$i}39M8v)C0y0r=U1uHSlX-(c zPU2AN7jR4sVBp7(Px4h@;4$c{q1r`zI2u7S0cNRRLNSOrF@iq4;#hX z``QKmz(wV78*%i$UTrJ1wPh_>y+bdocSc=!Z7bn{Y6O0LUIG21AEO}~HBWDJ#>B+Q zy&}JtG@z{R8z3eiyZ(jkxx;}#oRaQSO3)^Cbad_j%zvWGv zVTIHLK1uaY-Q5Kt`kQ@3I*JO8)J9;;i#Y(82-YNl+?}j+KY$q+7FG1gmS&l0$=}si z6-Y1H<_B-6NR-5jb)2WMLa1^H4nEW-g-G5kU2o+ET!3D#b`nG$nMKS zQcVH;FAR*gHwF@yfZdY=ztvFu@a)2(e5Pv(YI`eba4`Gr9Ve(2l9`dwt1aRIl>;&U zjn@S?bmk1@k`A@9aCj_l5SkcB895~De|C6Gg25B>@_qt z76Y-^Ngn7gO2G^OJb)3xZqrt zbVqQ(h+lcaLR|Z4 zmMYfDtP(qJ!h?)-%KALD_`b2;0LIEwv^b0H21;>XMG5LrTW0>0xVJ@QuWDQ$M91Xi zCU*%dXlW^V{;}g3*_b@tKiC+Hj!WP$<(P0je`0P;ew!0rrkI6*%JqB=NfK(GK7(+Xgn z2-aNiunej~n^wJ56+$ZD^*gBbDx4d#9)R&%`ymdJm1wu?_Ay78V7UyeW7~s9JC3`h zY&^}i0>hPKTv-hUo$1E3_n9=`_TC>F&PwR)A2mQ)9cxU$i@u5a^qW6q7r5zw;}({# zveeQEVMOD>vts@k%5;CR;dCbi8?KU!AZRG8bu+(fX*IkdVDHO!JgMMSKb`-QnPz#9 zRe4Og$=A;Kf4MXPdNvOQ&KP& z*QFq8X5_p6{!kL`VDO9X2T%7T#9jr7k8ZuXB;ZIyxB57^J_ZqKU`voq?RvbWFwznN zabdumESAhsRfRRCtOkvHVC6r{;h*;%DpTikk?c@|%AXh!s0{3p6XSMuymuXZs$1Bd$^f%^Z)Vv4*&NVc&EeCYR+Y!mz5$C5fZ}buvJMc zl_j)=xk*=)ml5gPyn4P5H_2b&!#57#^CZpxXb=N0la7_Z$b5zIe|^O>U0(}H&KxOg z`34mQpm>{}oho0C6xx!^>a%5bO^G|f6gnwnxfY$DAs0iR2Nb^aPoExu%WJ-Zkt#>z z#Ti&CTLgi7?fm?F_oY52n|-rwegk+Dz;YxD%>B5s^7BKHJhChI3vSqPqLbtGp6nU} zd+Ii+faAxkLcQ+U(fQWX-PS2!@m)iFYZeB3rTILPaEq?)0xM%YNsXRf@Vw0!fV6n< zqX1OcJRF3VrI*#)2_iNuBP-%Y*!P4E*t!kP%vyo8?=tJROeiv|sNr$`2z<0Y6(Fbq z`Q5a|u0(ajX%SG%KyrTHbTQQZ^U~hm>+522z+tyPKe>%bDq}EKlI6HI6vXGS#9=kU z1PsQCI8YM+{LJTmg64Cp)er*$5WytpKH_F6_iKE7;JXqztd#(zZy1MMjr3v5_Ws3a zT~}NOpYfY&xjotR7?J{*ROi74W0T3apRG>93Tqkvkf6}W`b8xaqEuiJDKO{U?S1sf zaBg-bymk@~O1N-rBWUT(@4`9KgnI8kAY=rY+Sm79ZHtP}*kUWuI3Oh)9hI{3rr(K+ z8~fK@-a7sxW}5n!X(rO~(%9JH?5Bl@&-1_oAS|!jf*-% zn50YL{h5_mN`kvoz?)@~-I5B3&=mbB86aLi zC%n8&**V^BhSjn}&ZxuhAEJQcfk)b9GqC$z+!jBeheYHsL)xJGNq;NyqPicAnIZK) z4zWUm%sXC~8naY`xFH1+37iJU%sw5^TJTJ3iJF)^;56%9Oux0goPLWOVMXAb9}McH zx*YMXaTCqW2iR|yxdG)Q1P=e+9WY@S_!IVI*#Iv@zZp)9!4jKqWJmBjo0? zm+NIqxmCGLrsuj!9e(kL5hXM8tI2Jt2o+V;4)AU~wULmJs5uyrLvTdCQZs~*2V0p* zbcU=baF*wUngI6D05md?z1_ir8ErB88Tst|W_JNFcAGO=Sm&3QTic^)kp?RZ@EX|X zE}R638=+z}CxQP$0+f^AkYOUHkis7eu?dL7eh>MZujgr4>CFBgC| z;1;=O4Di$vxc7z0Tz5PHk*pwuR|(u}VX>(UKXQCtJveZAL#Asy1a#hn>7)z7EIklJ zZy`D-a0Ne{dg7|{8muD_6F_2i!^Rs(;AIAWYU}uUnHg*U;GA^6e8c2Ft%~aE%Y6^R z8d96;Q=b&SqP%sC;=xCA=hiKb5LN?gX=xIkr)v6ge%bzAjUfpkD?=Y7&v3AnM@I%U zLJ8UL{C%^v;{j<;GVKbCWO}iS`nqAvVcFKs?&Z;xnw{$SyyZ-I#~%jZ;EKy$y3CzP z;Slou8<|O4ymIOnN+<-|ZwX3xh|`;H*Bt$n_4cVQfdN(?)tR`t^-NgGtgm0%hO6)Q zRxn5XeH{E-DE5`;nJQ0<<)f%}*H?A?57wWkG2F3V*(B^rjAD{!Az+QRJtyGiwglVN zV!ZZtS|E}koe?nvu^{=*MYZ3&U%%ja6y07d4j=Jnol)!CD?&gla5DSbq{uy*0gkU+ z-pYmiq%=^!?`pygH9XDhJUN>oGd;B`;(Xi21{J!KxQMwG)=>2wr=jT6o@YXeX?#t= zxQrFY({87n*$UP1V1I_iY5s7;{W?m1jt986;3}@$)U*Q%7OcavED1hKpsC_@dmNT3)KrWIXK%8sLi&_A zYuBL#xx9oZ%V_D<=>Ff+8y+pi$F6G!=h@T4gvMc5I4@$EQM~^Q8b!gfC9ZjrQ(8LV zEq{5diUj~9paFU^e!5?VJHJKnAFHZB4td+L2MUNqQ>txuJ4~&)f&QRp_OPfJO}{8y zeoY{*D59Wg;bd41eHFX?g-u<<$?p8~y7M%_i!KU&XUr$L%C@U}xG+ROvabXbF$}|9>{Y?9saVf8UNj7{}aB$Qu9xi+2=6)I53Fa0F+)YR1% z>k57JYU*3qbI$E1?fe(@??7I@Ylhgz!HtR7=6SsYT&!Yo;`$lir@0HjRcQgEsd(tK z?uBKicF)*6PsQ#iFODH;E3l#@9C!%Da_f zR`L?id|o#F3zV`;fxu79C!K@0|>-4}-Zy zwX2Vp8~u6;UysNuH(0lfF$aGR48Ez#&`rI4LEdvUdBMBwl4GnUC79Ox{!!Nlo(R@u zcVHP{I*D3av(OqDot(z&pU~bAeF#qjfcGUP?bxU{ZVXnHs zNjW!CV9~FC_xa8K_DE!7qYi@`Rqv;g$6?y(S=lnraPd1qsFN&LC2M+(%mXMuf(kC- z|N54caC||@$yTs1nn8nXE%3j$iARr=K8QbD8IO&%8dW9*_J&bU@*^g?`$`{{&fgdk zGdQQtQ0DNGzjnC>L^OaCq9Bo1RTUeQJE#~$2H@nLwz@W%=y1%M5p^(kyXPlM%<{s= zf&M(g0HYqtFH_@2c7%nLrUFzdnybkOam<)Gb(7&LdBp@P!8Gq;vs14?JW+|bN z#&|uYh%AOI=kts6ovneJYHB!^(ebSM9NX2~;1$c}{ZM%p%;pgrJ=Y=WpB@L`BICRE zW3ZH(R*7VB!es^rs}elQzyDqx!@ioXT^%33#mT95dE4))=w!hQ@JsTYayv3IH*bb_ zAt-p-53Is%Lf~G2?Ax%lhpsW;UM9;gBCH^QwrOZ+YB0d#8e!f6%^o~JQJ38L@LkfA z!Blo2vzoQAu>1avcU}~foYBAtF7>(+HL{s}!Vrs1#NnIBnUhHqtwIWB%wHK@Z*|5M zeFVdtR}lLITQKmF1L7J57=9>{3K*E1OTUgvjco8v!^6VCX%gXn*+l@OY$BgAzpHE2 z&>OhS%+=oq-+sPJcPfU@=v<) zZ{>UQW(MuxPvy>kU%6o7!U}a%)p}5&^B*A(rUWgH5jf%pKMu4dCGo4mDgOenH&T-F2c5 z%6D+_Su`A=#Ky)BthI}VDcukpW$HaIewCWBLbp-F2q+OX7nd(kwE(`bEQLJ>1n2a^ zY~nJ3cpHL!>wIg(G6W91njwSiD>cZgXoPCDWL*er<^H}NOh!gVXtqvmAc6vbbg)*C zoVz${ZkYlIJnvI>HXy+F1hu~{K!3=TaD`G=H}%;Xh&Rz!oIx6?spy1)2{v^Xaqv}$ zX+D(W#KSRsaJ!Egf10b#VY=oB_LNHCpmdUh*yO;g@~gsvbG|h!>zjb%3ED`?CY@$n zW(Li(rn2HtcRg0A!iMgXx8mg=(%%VZwLDjk%gMR+{9IH?EnWjd=raRsukBPGX-3(o zJom_SHJ2XxY`eRLyER=cS#U?=VK?OBGcv~d6(<{$yIR3 zBKpQ=Q{0MJpyhX;`c{sL9L}QXGX|fn!x#2crwk0^7>8PQF5J2vHCQ*^jWd815=Q;& z1+Ye~VRl&dlmPJNd9^i4rc~I+$6BRtgupz*WNWhM^G*xC{(VBihUuw(trw#?{QK)u zA*kQJp_8)*7uEd<26+b-F>xmtK_J%3k&yy#4^G&DP6@6I5csF$2W8OS-X0lCJv|tS zcq~KnJ}|=Uu?%#+EO@@H1X)Hbtjba9D z$Poizh@F4e)>gm^aXL;T>0)kgB(@)NF(n@yID%BA8(K>#5WoRf=B}ZbSS|)&Es1k; z*vMZ^AXBEhdkrDp;|CCIYsmTn*z0FTg%7`6_#f=U zJbeRK^u6uburb5_n>F7g^{_7tEDYkg>b94&xD0n4x8?kr10vqbn!*z53(5;CE1eWw zFgAk+;^3$q{=+L#o+k6Vjq|sSUy4r3bgT<)+x0lYbO{=dxAM*IGRP!GNOyY$Wyi4) zC5@}ces`e7{7EW7&CG+6_}rmGzxHXCUn%p846I@M+sE6{W7DLZwb*)6%g==Wtnn|z zJx~@6!mf>@*_bX3xOQ=-_JUcvCI&)TfUxEA0^Tje!RPgOSwsIG_2n5O10w`c!?Oh^G6MwO zK{aD%WBWaGba5;KzHLwHJw3pnHT!W#cc<}K$s&ocz3*{5n32Q44tufBOzPCJI$kBr{SE-&ew7JAY>o$TJC@VDdf<&m%#|<|lKP-k z;2sM~h|ZtcAqH4klqP!j8;GI%@-%J0$*`T4(@gn=gyhzbvRYi(n9l^3D->>#3WP=R zag>Xq1C-+B`q8M_3yazNw>WY1J_)CnShGx@lS=Gek8&84X;;iDc^RjWYkOjXPMDae zj_=`m1hLC-^ZKhFfz9$WnfmG4u@Yl};f4YM=f05v3U_5TZEUAkVe9(`&+ag2rcO1> z_CClOxFPyp8Miw>y3+|06Th~{b8d&*VrlzZpeHJesNah>rXS)C;m zez|UyxOf%ObUHI-a&k|tueVh;PgNB2L)3`HxW<`YUz5me>0`#Zhu~2cn;*FemYz!A%!G5_%x3Nj; zD_BBnf4Au_%!$T(}8CU#CDQ zHz=el`tOFQM7OVul@m{7yip}JF?ngf(`-jV6xJ{{@UDZyo>Qhn_NR;(vBGQJFCE_# zc_QO8lrug(^;Efb#|p=H$Wgru-Bg%PPl4v25%&L$h?5a_uShM3#$cbB{;MSNB|rd zfhZF%EG}}|4VY*Y>4gE7!7Tv`Xoy{7dS3RPMuA8zdTe}rr*kb9om@B>nj_$N$b!ha z!AETDV1gf{i=S^7>PDCjt+0b*d=z*Y?XZMhofv0iWInGm!o{Ht&G|n}ePvWo>lW?t z7)Xf%(xH;lB^}b;jf8YJNP{RygQS3TcZvcM(hVXYDIF5hNXuK>bKiLH#~s5l?!5|o ze|vpv%{kYcA16NB3-u%?{ge$aEUEeUM7yr1L7`41LN6%tVbLWe_BByG+Ws{xQ?t%E zey`wAwMQLYb?knU(fmn+%1U>R%FM>+yYHNcv*OrzP}&>_Ez zQ0hDK71m#0CNr%r@7T;$#)3CB#qEE0ra(3Lb$Ixp&B<-NixUB)fg6k@6X-llN=I;$ zv){jDVQ&j+nbFyY#~{K|A$Hl>?i;fnN33fBe#p%u;CE+1w(vS>s`ys({C8WHeLY;Q0XT4bxDLd*R({?3tZ zkBsc9yb+FpvX)kG!SBtL5&zZ;8#-CK-khq07uy7z1j_0wE2eR{_>SAdPw1RKIf}n} z>fr&q^;Ek*dvJ=-ugC--OcH4cw!5JvQ2(B5ED5gdJv{@tyh`IrYdSNG4LY?WL#14f zkHG*c-6gPPz)O{KNb=eE%c6~isfd$hz5+@U{+!B8oD~m`0vF4Xa8XwCdt<)mQK{{@ zhW}W7d-NwIM#6Mz=505vYMgSWuChY@upS|+6NlFeNw5!;l#fqzPk*eMe~+{gVg}J? zDTRd6LG^wBH5peGef>d2bo^>#b6g}?5pp-xPR=&VAv67fjRkE@&*8f2`p73CVosX}{QN1& ze~oy7^D*tyWOHnFx2ift$BRjgZEFogFO=3O)e(m<#j7L^|0$EvZU-ZUG{jlO`)oZQ zK2jq{i3P*5wpNi-_X}=tpC6M3GI;KaCH)fRjuTk*!`8$pdtqTAKbTp8!{FPbBx(4C zKwzoH!oq^M1w%ml|9&CxsB4G>mwNEVqB7~e3!UQ^sLIK7^z>o7%Gv8|nSAS1Nd*>C zV}&$9-umTQnLU{}=l8}iQ=jos3@J*A7nKja?&nim-CS&&Q&vkq%Cu@eb6ftu-tmDSCB+}Q(0HW53ZO6MGAL*X^~dHd9^4)2MN%{`5_AWWOOT3FPy_JY#mS}H;r?bDSQ)||Wj<&Zb?cl0A$4Og z1=Y0W6}iQ8YgyN~aNv7X^Oeif?t?pLS9|-7`!K}W*f7A}5|C3PNI)xE#MZCMk7ZF% z-+6hbkgEUi$b;<|d%Ma_k+r@nsnb#q$4@?FxxWJUY31T+CX10Yy z&iX?l;S+gCR9>Kv7_qf*BYT&lqRm{K01eNQd}MmFBvgW|siD^XfS4>zwS?m05H{J6 z(6T(HQsSD$owt1lA@ZMt8pI+)#U5q)8~VhdQF%cM>FjHi|N2oqdCdRs936f--1(&_ zriMEvtRul26_XCuO_12tI$e3423sN9X<^TGh9Ox+L*tcPo`SkXf#ui9N(vNvGGQvg z&3mOceU#`5lyr1j-}?U0Zt~^Jn%enRqc!Elol&mczImIFx#BA)b>r@RIx5U~4ms=g zU}a;i=HHrN^H%s+MI{Y~pF5yXH6LE6h$+m3NWTckF?b|HgwKW)Oq>Mw$RsGdykJQ8 zQ-_x}Zfx(c4c@%qcU>pa0(*$4SXf<{@a}?P__+lG9MubA;Qos> zuKLd~iBFvdNjgC=^Xfsye)<&{$m0lh&h&>*b$4_mHThg1_F2$-W#l29CjeXSUS1T@ zGN@=68Pt=Y_%GJD><70FFu%>EK0O>!B;=<}go>|Bto>|!?KNw55*VF9BAOcV|Av4S z2$#KmC3r~)QCG%7B(LGnUA<{-6-i(;=t(G@K7rl86Ii?k&{B(afSZRo4J9vy zq$t}Py`@a`Ncb|CRI`g@WI}0~=~w#O`qaL6)zARDc_G4MxxQ}FQIX ze8vXT%-rISLL4YcCTq(|qy^Gr++L?>O{sjtjfGr)PtB<#gXptE$u!4i~4uU)y zy^eC+%G1<_Wg&+Z6&2MGg2_Quue=beCH2AkX`@c!;QIQJbNzl~>KQ5EyRL?2Mj}C+w*Lby z?EZolVfD#G0`)Jb*m#pS=D(*-6RdA8P1oVz@lr!g0aF;cK-fiR%5NW5S4GhipaAm{F6CsZ}W{b7SlmkusM@5%0WI_v;d3>1t?xkC6ihETKi%w^H zYA+1th8D?vpAWRA@9prtbhY-)l#cCG)1>jcO6^h2t8`d+1ZLxfZ)0LwLF|m^9Bva6 z^(Lv(z?Z$C=iuaG^1DPak=2I|xByUug}^Aw;`irYB-0CqXTS>#VbPKSFp-6}H*6M4 zEHKhGpu;YYn3&z#r-=8Ed~y@Y03=}SHG3b8=~62I9hVe7)eqjckb0H~1FiM@fc(vAa#6P-0tL>Gq|?C2ip=A07I;j?VB$XShN4OT()_cnJryQ zLPl_;NVpwd_97#CWF!K?g_#_zGP>8;4FFrR zyStmw3Q;X5%dv{IbaX}+lH`kP?qO8P!ER@%E= ze#ZOCKfZppM%h2mSDsv|vY~UXunBKWB85Jjij5628;qj+RzM^DH8+e45+4zxt*bmW zy2o^M^NS-|09Mmc^GXmDZGvY4kM|;_quaKU9>&iRWO?Cl8@$fYC_ja~-zZE9CoOFMT9UmLAwr4ky9J46G!Qom^v|XmBO}}#&<}(ky-vt3Wf$FkR=7U6vP;&BX&cgqN3c+XOw52o())lo7})4`SSA5 zC8Uj$iw3><_R*FPvA72Ef!lre0pO7^)BS5tZlLqQAs_0N%h6uvw{J0ih;IQ7D-938 z%axT}x6#m$n+Y@_`{s5twJn$DSOhE?Lar^ryM``XTWtu8z{QnM4Xz3+a3nw>iA}(f zc)u^FHS|18RyiZh_9`1?abKqEuQ8!&Qt83aaI1 zuyA+-Zv`8$TO=K8v?EBPlz)9J8%0pBv66*rEnQ05V=yF<0IXg`fY5bqj!t4#HnyZQor?`<;v;3zHyi2KdTVjRh7(#?1| zPLIzKELe-B{A0MT27Uj6Lygk2er7-_gAEIZYwDw|sHHS&=EK(n0#3A$kjC$o&|kU4 zio{1DQCrTdayNvZ=GGSc6PT5vdd#e(U9t8LoIS;Kp?o=I1z55l>-a|qqNtFTsnP52 z-M2bY$KEL+(a_tQ#Y@Y?wEAUl2?HH1$5vF4Q$JK}rKiuFe?S`EKRTv7X;E)iA-lDr zZO(^=I5g80)9#4F;kSLKq8^;ooo^bv<0A@w_c3>`EtaZHeNNitt(#>2Y5S{V+Z zTF&ds_U1X#;k32Ah6}`bYr?VVj}?=dS>DAolF0LcF#WlqVGl?rD=RAIfhK`B4$;nF zit*X1w{k{149WIOV~?>xDx5!1KjO7zrV?cz5)qbDI}nw>XampfTa{H zTU*;%6F{i|&=r=a&SnDsG!IU)jaG9SkgX~sfcIC!<|$%b>3M!^x*Bd7fI%1zo%eg7 z-*i3y0f$NwJjv*e?$u(fO{$Sp^b<6wDB!dX3&3f|FD}B1|KpLwU zgre7inI&w+m_B{VA~bO256$47Jyc5L@8$;J+tvwx4_F|Xt;PS~_uUKVy(rO%x!OXt zcltfW4n1jBeM|4R6zo%Uy1tQdk)+aPEDtoZsI(qQ<1QA@Wd}Ht9cBKEq8_&xeEC4}%I)JX~%*yDrtncXW_O-Pn8etpK@EiJtA zsrs4abnO$sYLOh`I_Kt`VtXuVZ~ioB#eo*sYce%AuXgWYd+}=)sZt2W_Ub`}>!t<- zC?hWXsF;L6KVM8E_Rl7(7+}WV6A%*1~Iv*1d*k3;1gh$13*fRg@dF0+qZwMt%(6N`#GCUlkUfg0y`cs=KHC`2Mw0sDcmm- z>%k^MST21JmECMGhZ_5aKa_*vVohcckas}%^(nM_mJ8d1%?qIE?!P=Ec5yvI_Qyet zFscUpBK+%BtMA4}sb0T&{d2_rAH>WDs05&?c`*pt0-%~@DA6J@FCeJ_)soX$KM#22 zi&$Af;@l|^AMnr%Aw(=7H}@vzcTPC8!_z|`b{OoKoFs&9s{|yvV65V_w|Env;MSg= zkc}@;B)fwdOP*a31F2>T@md%p+;1Vq6=CV&n6lGy2czP20yfiiuA!^jKjpfqU2}#H zq~#EV3Mp%9(lFAb4y;iW_P2E3Iz2F%rTnB78@tBzpomWd#U;OiKV3*FXQtmf7!>xD zk2mft37)+irU*JP-x#Ylnf{J>_?sL|GxU(Cl)u>V)XTTouT-OvkQddXarovIlquLrhoM(d|BDUIO-3fbV&K#oF zz!mku?SIt)EtG;O7=8zE=TOnn2?hQO;3vp4f_$vbb7WHsz91+&SxnFav<#MA10gmN z+}telC>jKKZ-5xMk_GetaH5`RDZZ|6;s&^gg`2RnMk|UR~(6>BwdZLMmT?m0M zdY#i1HCc1@U?4TMlFRC)*dyUgC8f-YoVOFBH>L`$v$M^m)a#~dpPNs;>|##K*cXeA zV!Q%xYxHJ2(n+^Nj;#8H;gNiPX?y?Fo8|GTNul)?-c<~M~HDY#Mz>-+U2TpfP=Bs64p-q2 znZN|ZCqQv+K!^H<6caFW2J?ab(pqjZYAPgV6Osbq+!GKHJz$Pgg>S#OgaPcDSlHNF z0Ug1L!hoqy@jMveSN09JhMas;_Sl0kmPzIn1MmH zq~GK%4FQMp$sC#BN(sZzaHX*%K&sF~HK4;_oh|8Ru#rXf<1^9`AO@tpw|}#L}bqAnDiH~OJ}(o^DRuYyj)52 z*^e*}!`eVBDPK0?as%)m#O-LPy_oO3XU89rU6uAd%_N_HO@5o|-R`nBTULFx-_YRl znz^#JiX~@!JXH@W7AbNG-B0EHm=D!=`qL6C*Vvks4F5hh5Ax~riCQbsO?oY}V8xOt z>v2@t2fPnM|KEj+gvK75vv$W**{T&V%O8u11$>-nCcLqcYQas(G zzkj!j!|hWZ|C&{F@x!a);)J$;Ek2m`eXN~^86S{?t>TEn(;6*F*)&TO)*v~pKJS%Mjohl zYU+0SWZ=%QIJ(YGBJhF)vWO6=TZ20)ppFG`@8016$z*1B>)taic!AQcG`Ig{H<#;b zp9lcL+!<^>Y;0^!xInC(GW?878Q*n#CVEWhs&{KhaLM*DJ^d1x{Qz*~0FM6e*&u7lgne(lx7DAOC)c)aAJadI_Syu z*C2bqX1YE;RhiG@@F!$odM!*=Q7s9b-9-ta*3#12<%R7eoAf}4Q>uiLg5y`m!0yB1|-Jp$ft1+HLt z11U2z{H?96NL2uE`{GmhjNJFMSt-+_5AttKo3i4@>D4W;YlMiDe|&2h33&x9WzN~z zOkb_2X<(rT%q%HI77Mc}am(h3L_FBB?jSAxeCn?v?Q8gONF^;cAzHzeHA9j7f76Q4 z-vWs@4Z-tlMeB^gO#8v*Gq@~r}Lm#z!RJG^|Y9g0vCnM_x4>hM14+ft4 zP-YbGUO#6Chv$DTZbJ5Z6q2|C{(JjBuny{6H}T|&xH?-}{BL2}R8&-a>F&PmBOpKr zd_2V0cS{hrkr+T8Y!Bwkxy2r0SXg8Mm$+*iuelZg0xsj!}2 zYO=}U&uCg@#7}TAT__3?n|4M(8uplxG1su|&8T=9qX4A;QOL;1fJ4Aqx;u1HfbGtM zrl_U2i0tzG$aFFD63Nt3hb}5xztIzRhy2mN{zkXKJ>)tH)&kHGz(zpCSm*dSzVO`> zd)O5NHPUdofe`$!J2FLsxWPM8_FZWP1m~JDF->q;1xM6i=H*cn{MEuug@vyRZQlDma%}7dyTg*?dkaD2fIYSf{V$Gw4`$l> z0%&8wY7trwf8a`XRH0u0t!OwoS3*T*<|rk!r!bPe$!u#q9rF9r2F7YE3N@BXO zsdoJaZFOA1PYa7|TGg!5Qh9YGz&t9YI*F@#beUV zI!Ed!cjCjam~otsIRq6_PYhs=0OP$JP@1YCnT`;_2?Gh4fTvh%<2h88==ZU&vG3ks zVdH2W?GJ}KG4fFl1>%yyQAva@exDb)x`6sIm0acoJyl^yXE<3$-@9_efgQTFg5u(X zCh%#!ABDF)st&vXhsASQ5O0u?kzJ578#FD0@160lDc6(pBeR1%Oc1Q~PE&$V;%lY= zDes51KA!YyuOWQ^sl)5v+%oXHd;(SSg3#F57*J2}c+71DJ0j>%DIhHkO5WbQAvO=s zWcP~r?U!%zZoPRU0a@|D#R0YQ5b;ETKx2a%^C2?9^DTDxwT&gBmdDdI!IyM!+5|8q zfF)VY4rviABH5J~LLq?D=wxwTrGZ9d-)*MW@w#MJ9{fhC_z*3qQ_Hmmapxzer+ZD% zBS1W?D71R5Z{DcjK;&Je;SZf#w{8Fe^$1BC@v{cBV&FZOIUG#z28)4K10Dhvx!|(4 z6BNfdaSN_3PYoHmI%Y5=$E@I-)OYQqu9Xg?gB79vfFmd~-J4&;f<#pOLRYvs^5j@Lg z9QijlCcF7+Frt>bC@f*%hoi*|!T;#9C`7KUapQ>m3+OzHUdMb>0Vo5DTqrL(mGt$q z-{#8wbZQDzY5D1~SmJ>m7Mny$>ORj7SHDEG(JHGzyjMD#wIpTXIwVcSXBC!7C`&)i zsyE);#tBD1k`N&*H%@pKT9|N+d6}o6g#Qn^-FK$Mjig;_X!t)^jfDciy~jx^=$&|< zUc=$}oE->c3wjU_5||{|Ex%F1f8TMAO-`;jk|$r-2a_<&q6m;wM9a)<4tJpU!2s9E z`7!lsyD~%nfX6tDCIq-7Vh&0V;9MBP)z6??`xgH0`JX?#5#j-I*~GzV+hZCstvkBN z{rbV^36#-9AS2!tgzyiM@L5yCz!FZc((=9KeE1pb&5YopDcmA#?e;l~wO~o>)&71${SjFNpF)X{uDy_o(05neNijqry`~uf{%-X^W*j0m{f?V zN2WS-*C$VIog54QhbWv=0iw`&e>E@9#cn(eCtp#Sl;8XCHpO*OxLK3|@p^BJA4zi& z*&EW%?ygg(q{1MnCmL3?u{0Z2Yra=N+35Z%?0-5~ZDVIK*P+ZrRqiKf5{lWu<2?!< zGHad#Jn^15@v>OuP8YwW>+9SPntXO!Pynrz+x)ISiU&2B;aPp~?!9be#~of>gZ1N{ z9xd&oe5PW1(~f~>pH7OA93prLNx&mzJ`_cE6JjAj1Y{^MV+}4Bj=(M|OLTPfXt8G0 z@dD7w7-akewv3FCq&&fafj6%>yYl2iX6sUqh<&pG>Ot1pK>zwDba~m_9Xd-rJZ@xi6Keq+azCbN!}@pgXip= zP|P*#!An~KQndY0<>n+O+4*HNP0MxSC| z_wg}eO3$tG-~N-K7Gt3FWog-*UsgJ^{^!bOSY`CR|IIf{v0n3 z2|eA8TAH=p{=SyfwUC_UyzI=vi265p>SJZn)^aZL?nzhJ@oGnG{ine3B^~asf5}V) z+VKud{G$s87(3%={!aQ`l=%egwxY0zudIE)`j)0OJ_AO-hXHV$?kDrt`ra|-eRQ;+ zO_|SlirgUZ3OvTy3`~ryj70RzjEwpLh;c_48;AyzlIuqWt#Cf1eXG%cGF8mxNB=j| zzP-ak6S$cX@{TDLd;~!DEJ(l}iX}ww`oV2A{8tZHoW!`{eJs4j>T+D^y^lyOVOV#C zNt>1RNiGvRE9+`U7=8m5U~-h+?gZfPGs-!VFQ57BSRWic$-y9G_MLgTik6orMZC5Z zadx`86l;0DSZ9I&6tgt{MjzU4FL6`F_)|5FxdO5@OaY}+VCo=SGuJKMzyEwymiL8) zq$#vt;6<8Ds{FamVcRWjHD`}y>0Z1Cmz`qZ^)AH>T1}u?EXu4vm@}r z-QnH=ieH%6+ugFB6({6PeCt35l}4Aui1PKb%0|*VYbV2oUnu_llX885< zp_PV5E)yCy0b@UWW@?h~$RTqRdQh;iF@{`Xux&VC0?)R$QTu~P`cYM*fVPX(a@8X?wiB;4O#@ySg?8Q zn*jU}3IEoPPF+^Y&7cG&6^&P+H?EQUpj3T}^@uHYGl))$4k_5S++27nY)3?=;^VV8 zvZJIuhv$3y)yvQaw;A;6<6yyD@Z5$I40k^`*hfN|_fU*#t?%V6sHSUYN7lDPA1VB( zlQMnL$!XE|;c4OW&z{(s@MdvWLp!rKiunrvQ@vgxGUQJZl9ECGK4%rIM#N*nfK~&t z3LAg(qdBaZrLRu6yC113%r$@cXMDRG-D!A?E3fFR_Soor#RA+k3zYI`X{Mi||9IuC zwtPxh%foZmjq9HBkE+diMMy1dMOyl}fct~AcU9E~0Aow0`Kj6%fqSjzN&9uF&zu`l- zY57Z9Lx#;hMzf@+1;^{&n81J@32nb@e$~5rwm5AWwGX2;ySA$XS0Jr;3ah6unk>R4 z2Yn|X8UR#aLU{Y~O4Fk03r=%#-Mp%b?=2Z>*lue~fQ){F#k?@#>$kwD;C~8+Kgb{K z)h9B9ZNeucT=ytX38#;@Ob0(a=1$}gqmEY2qsejyv)K-7y2kz_jl~Mn0dcjca^m_PNWRf$??St#S{jp#F&^qcj3ck z(i3-J(kPA}n~q|m&Le!jBFyjPs&-HyrDJ%9voEqV%koHK^av}n1YBAm-<(9FRJZe> z%wCQh?_w7R(Zz!Ad)$2>)=g>_`x)@i;m|d!f)++j4g(Tu4vsY0fvV@HPv)@e4_&!3 z{8B=d@tNbt)7^!$gDomNYx5!+(?U8T3t3; zY@w8H*{}_YrbKNy#n>7ntA2<=Tqo0Uno>NmIaPsUXT?QE387%1)!UMe2@p2;GQ8}L zL4BQ5rY=TD@X6DvD!WY$yV<4)3PN1su)pj1ib04qs`5SgwgO`8)|FyS#~Yw?nvs#t z%c!^umRV;esA92519_1LED^Of|ph=d~Zjlk8>G~b_N5>=W$EKA!Csya5F1VeT%B(bd zTVd)(MYonoeS0^}4Ga+886aFW0MY?HyygcBw~h#m7HWF+kh_+1hzLenx4A*XnG;kbS_|pq z&pm2CXOC|>nnCo3g)T>2EWY{z*I%1njgU2=J`N%jC6pAFqk(`wBjB)Iaq&RV$e9yKMS%X3YdK(PuTqu*Az> zoXlz2s>$9=2{!fcAt*{ubuOL5T`o{KetYzeYwV70>^8>w6rj zZ^a+%JroimH&1w1-Z4J@t_sOHgXwmCxRH1e-Y;u^`qlMpiHIx)?@w!zRW&>lVTIpc zU3|bL%lo?A`CZ`I-IS49VFA|n2PZ;oJg8G9TThA!CDh6N(gO4io*E6hR(;@^u*M&a zQF;I3S00$tMq~UE`sc4XK^ocLr+urVbAM8^nwB-m;l^5Q4CMV`{33oGrYoOqg()p1 z+iG@IvlMXh{fh~maIVS$a@1G*OWkdp(6%2G6Gg_wzm6ym3XmvmRWcHBV1mqv7hTGo zM^R$b+8%FX4l#(-Z$Dbt#c7X>u-oYl|dBTjRwY^&+*F*=P#MlNZ~%;9~zuB6~r5y!4qSHg+sB>Hvx_0<0=G z)Fg1$6zAd8hf^(1obk}t1#VQ)WceH-F zb_wbQ?y%p#ruJvgN04M-P)n?(e_%Ic4k{;U)6Yf|jx)(QV9|)XFuxQ819=BQ{WLOn zi*N#r3-UrI@v0F8V`ggpWEy27hE5wg!=V+?32S`{j3;RD6QN*{uX?LIiWDhT87_At z|LaBP?uobsM-?m0zT4KTukh%)L*n0IBwVQCNcV?CdBG6l&Zek{4mM}LeY&YbtiAY5T9~ z9`5S6QCzTSt$579V6xmD?R_~W2L`Ji`VBlQu4kA8{jW|BJdSF{-AO=uCQ+s1VY*M5 zhCg=w`l-!XJ_a#o_{oVoz$=gML$3D#@wlVe^F~!6M}eN2*v_73`ct6;V=P)=awGbV7oqJGZ9DW zY%onG^S5Yimv=dbVH3o-c%p^Pk?F7*X;h=*h}&5IqV1En$NziGO4JejK23#*T)6Q>+)X!C0+S*^^ZtVY5XAD;_l&pNrNtjsU!!&m%HBcsV zF}=ca#LV~1P;ii%T)+{YtkrM}c|28Pr91fj4h*SSXR3k7oMt z0Szw?yFDlQKbqgTze7~CFTApzJUrJvO948!q!m& z=u2u4mh|@{U&*P#rOWe&Tst$J`k$-@x)l`{3>jjQO^J^maF^e^^vq!H@0w*SzT*|& zK>v+8he}bzASZjmX8vo<(gZQ-lN-`8wZ5A$Aef<{p*!L}Db~z)RYU*pB-ca1P@;yb zUG(hO{P*$P0%33VpfzZP zi%SJbS}1SiO#(^aK|?b4g7}o%RzK)eEP!&us`9(?Ez@o49};4m_gGoe^hX*S@aX9R z5WG;QhK`Ae$Zoblx>TpefzxegHO6a)4542bEm^3lFJ~Pr_Nt80v1k``Z|7ihE@Ddz4m{Y&Z?s?T@i; zs22q`yHTZq=Tg4KC@rozlD>U7)SSamFlMDAChiCtKREWG)jZzIw^Mmt^F~3gTkG&J zB;7A%=m65Nb~~-h_x8HT5m!pG<)tX0)zU&$xX$^9fJYGT`~Hg?#%qLl2YLdQr{f@n zZo%dbIgbcTT!@L+2slE?>5|O#|3Nb!14~61usU8|yY8;?X(x}xekf{UgjO$DNlnDO zzO8MhH-X#rH_5Am&|ky;B0u!DOzX9x-&X879pzYC(4>h~Hr);DXl`Meqg$}TpP6ZN*VQl6cd;K0jBtEb8PeDAlsgQaMuc*E)9d!Y;?#Uq9ikI_HDOAH6IF|qfa#Yn6NL8J6QC?@f}lxM}echOuY zC%S(K1P?U?H2$RiFWhf5^0xXvXfJd|3WkJ z6_M&}H3PP0+}^h(9XS{H+3LdmhZ~jX<3C_n4OSB8S4?`WKai&z@38o~l;yb8T&cx? zuvQ1F^3c0OYFHNF@`qtR+rB&Vu|DgMV|&K9{LZ;c)*xIylj;2(P>i~7VZ3r?BZ?^b z;hLN&ko=&n_4UthG3&aFDL$5t6EpSB?>)SHmw%+Gx?^jW>t(-CVoZq#nHMc>FmiDb z67~p{+Rrs1K150EO;2-G78sniXBHMaWL?(31%d_{+1;73Z)zokI})bb6Ro0yY<@tHOkd8;uyhzL>%e} z*EpC7*H}6II(w2fdLbYb9rON8W7$}8ro7^+A!S_2j3-9yWcYjo?Rp#!ASk z-k7Sy@gFOSj!Ae!QK3^>Fh=)>*EdGNR);l1+CK6Jci_NEzxB-E&;zm37V)Ld%YHL< zSPnIYC4xBS9noJII#Mz1N5ot`L*x6~d^=w=?VkPC=YBbmR3hv00`-d($McbD;~OYZ zslpEW<90KY+cKDsHr)4Fw%uw*56fB^ZEta+Fq3Ba#@CB+{Tf_iNs@!aaMem9@f7Wv zB6Vp61!_>*`#}*Hv%n?lO=~>=IhoBjX!N802xLT{@M;AEjGy?^_#kUb>BzWtY5C*h zb>7rF5b3l9tL9PgpM%pwMF%|nw6w#m;nXVzy1v+7X3 zF^-mHdLZ@p_H@7i>t|Y3z$=tR-|U}ml{@0@p@z_!pz+pNiu2$zzq+t;S{h`ud0`+R z7U87M5>KF9s8=z(?sB+I8liiuSd$@FA@%u|UV}@LqTnUAG*ertwTFonPc9$`f$;*4 zeJszjnfs~P7~*2haqgvze-uC|&^P_ZH;sy4DrER6yZlY3F&!5w@retkT{o*U^?bvc zGyWoQr~t=cD7W0~26Ou}jb87n*bEoNe9!hIJ%9gL5J&K3V3*c3Eb5lf#00FuSL; zkoz&Ly|x^orHB1FwcVqRq{1;P4kApQQaq5!kc|Z%$}(J7qyFX}wd1Hr$o`tr*JffJ zza{8?yQQus0xetV-Rs7G4X`jC-?9Ne^U8aT9@tc!MsidVFZ6j|Ha=)hb}trq8lzS1 zh_i-`<4BqP=8iZ(Ie!h!|N8)lc}#8__;FzwOQS|2*#glelzL4fZS}4Mlim-~ZLZMX z<8Tdr75c!J{0OUI%r_1U`9bNUlI{5qdvi)8IJNm6ZH}29_2xEQZgXIN)XS2wVYC@s z*3E1z!q$ilQHi0vH(g3aGerG}`xBM5+wNH+S%E1$egEHN!|KTuk1h$Yv2*)1&<*TT z2VUKC-aVon%7(Z+Wc_*z;>EvO)x^b5OlTqTzt}jjw=r$I5A2T98r6!_Lcoa-*$C}1 zc?6C8V4iu@b_r~l-lLFsZokxtdANfgRvi+2n}%lX4wv0b2JB!Jj4_dLpU|Z8zL@uL zTa_cJJKnxWN-D1n=Lsy5=C@$Y4E_ji-n+C;E^a3_2MmCSa5|YBmVHfZ=vO1lYu{-7 z<~n^hU{)_#Ij65vVmoU=?K{<0P4RRx-O7ar_L&G{lDJ!lqC)#@Z9?1Am|$6{e+dB& zA_)xB8OWH>`tQt>AZOnBe(yQ#1|K8B7mqbS@OA&WuV6FtMc`TzrF8$foglkef5s?4SL>sd z*9QxHK7W)u-!+z_#(|4h1<)xH0nhN8b3<*QN^k~+! z;GsiJm)l;mKF4}Mxtr#~KU|{|Sgl&D4xkyCct~Bq5a*Cc@wg!~?}N~W4{BrzEy=0* zUI zb{hf^QZ{-W=7@M0{J$4)qS?Uhg!}jDo2;0`LR6lo8K?RsJu9!(?K#qmo9}(zVGUO+ zcs8bJEsQ$_2#JAcjSymLMr+8@QEFHfQJ2Zx4cjlwNzsnje8=3Cs^ zP{Tp9dJSQ0pdn1;alXap_A5{F*p9zEl0-nl$q5beVlqnrW38^%DTOPUtmDF`*#QQ& z-&u5HU2$oH+1!qMOyl;mB6rUk?uIlyKqm%ew7C zo~5XzNY*|ufZ3=YRn1Ni4ldBWF%sE^(4Kf7_T<}4SAmmV%Ryd6MQ~DelNro0@HS*p z`8`4j4SXZ3(V}iI!5T17Rmw1gss;o;>Cg@g3SxEsJ{}j-2g~B@@4$lkTcxNM)aoJP z*vbCrFFHDI!GDcay?sdio0{~Ceug;}owYmx6+KpLbpG@4 zKccrqOtfe_0w*U%G-)N3dtGfzx(TqKyreO#Ok^broq)mKtoSA|R}Fnj(-YyU{q!+w zX<5atXbx0Nsw`>M{|++)RFjkOa6(Qn;`PIlbUZq9g#f9q_i^V})_Zol{TT#5lDtpF z|29et zZ@1dhf7c1;&4%?S?Z>$12b<|(sJo>ST1TB1T(C7HT0FL}^+AUb zEaT&9K*9%mnj)}T*BZ9ewbzhzG&YWOif}?z=$<^eAFm=UE+mwq{`%~Q$Bl{XeYN@0 z&#z^SgLEtpUc!{lUeZT`>$DAWA5k41N%|G1o|S)@oy8LU%vEn%S^i z{MYK}esN3eSN!#_??WfOPVfHi{77;qCO&26Gi82$bWGYC3dm>vFFnkG|L^0)2w(5Z zuQrP*A6ulAqoUTZpX?25d-E+%!Z{&LD`dx- z0tfU%lPD!6^<@=C{LwpQ5$x!wcgzN+jwTR8ggxV~`4Si!T3TuV6<~u&K5O*i^uTho z;2wyMDcP!chQa`lxoEOrH6Kh_n490(LG1e+%%G>tl?g=F59DR+=&C;sw9si+MpIDK z8>u2#6#=?Tg1umPWzG%ZU<=*!#Vo!F`rikcEJxJ8Z%H1lNdo{98isbV`JFISHnb8D zkpOke+t>^SpRT2r{UfPTXe4grDgHMc>(vqx4cgoZ!l9PSv%P~XaqduO=@7X6-gNx# z7#NGc26H_x%=Oba$0M0$l-E((W0H~roBd45#ARflKF37LPgibk2x6NwuZzNkYsmb%)uj~e{R-M_Fc|38QYz>CiC$e9T^yHE=XKv}z&a2|rH&K9u{um18;%EFpCHB_#!9*o zC@!8NE3^D`VW zltfMk$fpUWFBO8fyB#jNpFzKMx!j^M^ufu0z-P`%tgoe^@l4ca|41j?zolTLx{?+5 z$HOdX(ZLj1k-K-ug~TI@hHsqqc_hXhx=fvLEjpO}%V~N1?bK$3!R+wrMy^QDC6wB> zn$2-kGKQQKa7BdtN^Igz`DZddwPlj2Y84M43A;OERn+!8^oOL`etU- zzW3?o`(Xj0XJDYiss>_%S&1;g1!Ae6?&LH&K0dG!G@dM;hqU@BCv3$U45GOzdnHv` z3=L;f;4(Cu3LBaS_ONNc&kvgP zIu(Ub&?r85n&t3i}b4T^!Qwj)@B>ILu;Map2iSN@DV|ojCzwg8T|q4 zJ!6Ph@Wy1^q7embuxG=>L$I&H^9uUw_QRi^@RP&lTjt`oUp_1Y3=aS;v zo@FFez%LfW+AvM6c1FAp2)MBn9uvd#HAfm}o8}>_{qes{rn)WqulJwq4KjYAl;)Sy zuS75AvKPKtGj6dO-eyMMn7GD%_)BH1;zGr}Bs^k~vCz;)Y9e~xPU@p<^Oq2>$9Uv1Qq zZb2vEVGVmzCQ~2${0;oh(!lI%&uwwA^I$PR^8~Ow)Z4een28lXOp%pt&-O>0_lJXV%t1|DJl31_Xt>|E)Y5beZ+2g#F%Oe3!=WNOuCv|lasFeUp zu1(i3@5)c%8dS)3Y_Z@_(bWwvE+!rv?C7`&U&$pjm!JU52Nh?-RZCER%HB=jtj&AY z4Ei6ew^IGynTwLiU7W)sB$U>j(H0STzN8Xe|_Z+Cks`(=Wz0c;pE4_#mb%R9XZpgyLDtW5uq~u zds{qH={ayflsFO*f6?0;UAd5^;HS4)fEVz@u7C1i%@NmeZP4(eR><}@Lj2t17cGpZ zn^UYdA1Sxmz|@A8mw7H7G9J*i7Jx1p1P?ZM_GX3Kn;uVf|=34JrWvcjt}i{cbh+4) z<~2H}KVubn`~;&RT6qz^F@=1JhY$Th9OncxFB?01oij~P3^ooY%miu$O8#HIJcY&E zLW5>OOkz$kD0!4f1V<$4G$EL=4oYY$eOc+#gju3>)<4Xza`8lGtM2iW53Z&Dod1Wh zzmDo^joOCMZJ-E9N_U4ycPgoLi3kD`(jnboA>G|wD$)WoM6 z&NyS7zqZ?<0>8EHdCz&xEBJ%_N-~X4S7>2=z1fg!nWTc|2oR?(Pjct?549^5jpiy9 zY{>o6WXxH{YmRj+G-@42g2M1QMp4+=o{bYn7MB?3(U45Y|J*Nj%rSn4Q~!qx0C!D6 zyns6fUS7@HERKX8z}+w@L}lWft=DI>`F(VSKPqn2I`*!S zoxxmP2ACJ_#~UrS^$%Jhwz8hP^cVI*RG*RhOQvjrbhgA`E2~44=Z@C1k<&gDlGt+7 zRZdB@j4b{1r6dsC`hIEE1aEORIwpZJoGw5O7x%#V>LH4oh?Op zULjb97To4m!^C6v1YqbI`({o0_xLyc<&>bsaJ~`*SQn^@hWFn(+B>oi`TY4lAA~Lz zD#^OL3*E-rT7{7S?04^fxOsc1TAr}(d|$rqY~wyd{h5%1iH|S*@lY0a_Ao-mI)&xV zOCWrb1ONS!G^EuR)(*X2n~fXc0K-Mx@UnS4CYlJQ@pFEV5o}FV1i@X!ZE>iDY%w9L zQ-GybukU_Q?IXLAx60xd(MZJN#ouFx#TCVk;X4^N(?XDCjZHCfQ(s?ym<68?grDGK z7!{Gw0H-3s?uezMgBd2AWnVUS?CBr;pp0Hob%GnwWybAx**K(^H7A@OjZ%Z>m9I=z z!^&Y{S@$?E<()fVJR*}8sMyh`uxvp0r1f%$IXgRt7L~6=R7fqot2K3G^oG|b3Q}S& znF@-|sBpSFUKz1?1*`1?(+zr847E%ZMtyo3a`+}?#IjrlheZD~&R9Xe?aFc{^JitK z$Rr{m#y3mA<5C0`g@DN}W;o`UKV;e~2pF>xsElgG-<GsJ}4v_F7pQaTHJ2xgSmAVft{Lb> zbzA|vyBtUh_yTJ2!D_F=Njr|%*%~bz!+!G$$DQ${mij9>u*$IGS?x_*U4Bx;0SZda}olQ;E= zv!#P|x#JhF<^ zBW0t!K{~h5u%y_o4RfI{tu5l@*;fU}%HRL+#Fu;`t`Ga3Ift7bu4F));UfQV}$lgcohuCr6D1fe&eDl9_F5dV%u# zyw-%gE0#Ua)Dc6{55r{Q_6`JbxK1dTaEo7_;048<`{{e1`hsl~g*1%Bf_9q>y{zo) zSOCU@AP-J172qfMh%~LNv!^G=&G;S`M5e_HyRgBnk3q^A7=YKxZPyqC&`c^HGz@t| z;zrxw@4{m7thJfr^+O++B2M(HA)+|L%L6QY`hWt}K7Qf~cBJxyHe8FUO@sj#Rg^)QmwWse#E)a$e8VSX&XqH1g&^JVti?MKD@ z*|sidPA#l{5fW#=2!WW zy8rlH*eRYpUY?M;#k<%XqCQ5}_jm!NGEwWu-HL`3rP%w&^`Mq_RAa1qwK2?#e=}6q z+=!S(gZM5(1dr%D)GmBI`4?q{>|ZQH-a% zP0FZW^iq%>yTalr#Zy?rJXVx%O32AG1+VKjiWP4xvUBp(8R>4C#>A#70f^+L0R#sc z*x7(NRm20G%DooTA$W;a&5=|xN}n@gmYm4-G;f93crgf->y*9Ws zF>!)O;PtGJvzlLIvA19i-?(wAt`!~>GB^}9l<;J1b2we}ZQUU;#DgTkwD=FSd;5pR|93DsLC)&%N%Z};;%hgaS2^AZ%`DDGy(8d!hEbTv zZ=_N7sF^;kjp0a7$S#2}K!dOKR}UWeP%V;-+R)|MwiuLXGd(qAoBvvgz6?(j4c(rY zzZ|RnvgF#MV$Rs;-N{eWH9Y0qatRT)Bk1mnRZD-+%$Hu6P8!QzeYL;dO59B%kTR<0 z5dr`wH@LYV#K|AhL2(?gS%J*PVx$c+XtS#*J?@Q%kyUz#j543`fA0-nJC3$y80caG}&^dM{V7fu_)^We-TP`1f$qmrkD z%2#1|8#b#CHa>iq`vPt#k08x34EPizfbIE#9-VVNX4Gt|3ddH-EydkwbKmxf-1l}% zTLo%S(G+van!bMO_b@}8>%c0(ZSiQ7NZ5pD)~o&vuv^8h&K*Fz;1Be!*0wgkYd3Bc zyLR}vT5jA+bulSRfS;}y-dDg!PG4WFW>3qv>3z^ApK>VIq6_fje9n9R&EhuPlyFQ9 zv@m>8s>izQpS|qmH_D2>Pc&gjr7RIicj&9Vz{M7knif!-F<%lr;d6p2mxG2Zc8s@n z=Uv@h|FLJ^Ey6m#QJvTAJcw2M_%=ZhiY!iA_#2FX*dCf)jcUB-w8>GGi^b!wZG;u? zk7jAfTRF06cdq;K^Q+X_J7wm@q} zVhZxNWZYPWT*L*sOG+g!Prq6Yl~gCY#VGUlE**GNE^O+gf(T>ihYL?%8E}_N43{l> zQ$>SnL2V6c@s9&$FArLshCe+G%9GB~;PRf*f{wrE_K;Y%OIjUrSe81X%zl-k0PydV zIBvD_^X+@q=V!M^zv{0-hSaN}l{?@Dze@)}cI`d2eqYo7V^>yA9)Kihcx-q0fb|a6 z4-vnozb^y>*dEkjm22_qlL#BUyWmZ{Hd)Coez|)C?DTR$7amN;nH)+!0af}W4^Vo< zso3R!5PTCJulACU7u)bcnO`*D+uvWv!V-Tu_w%ym;@HD#toSYPu@`oB!g_>vY9xaQ z9>jAPh(OU1=^L>fdkpQs9dxY4s1SX7-c6z7cU>qd6+h(y#+#e8R7*WKIm^FazuAzQ z=O@hjxe7)(|B%M6ByM83aBKI_PduLeyKp%YjakoRgJS5OqtL!qlhTKol-J1Y8 z!)m<$_YD7XNA=Mad^cd69o=F<{i{U+|5Ds5BK8UG-@P98E}WYl{I-l`7vcQ16St_F zI^PpvNb_jAsxR44l`#<}IWXVpp00QUOwq_V2BmC|u-|awr#?F%JZ}fX&jP(#AwF;i zehFHqU{c|aPuP_qlY9~N{G6qDcQkFh+?;zz>^#1}w)VZkt=cS)fogR|usl!!?k+P7 zvp~BUuQ0_D{n=q&`|+b9ovU56@sr8f_q!jXus+ESS9jWwpPye zwmgT;)Ig0J_az}o$VYzANbTP*Dz;yi^FH5Wgf)~GVp)W)D}+ZpP8K8A62+C!)7;Wh zV%jJ6&3YCB9zomk#&*?PvX5fg?w*r`#8|QS-Pu0(x@@sON4&$zJa)Frcj>t^_UEVy<0<5_w{3u!H9nqNeC*+KhVo4jlq)IT29 zFMI-nD#qkT;!ET-fyco69zJS1x(+$jjMymifb8X0FkHL($dQY;e51gy zM|DO+Zrz1a+-6AAm)^88mm`@k`Sj-K9RpDRE4bQ;^kqR`TI}5<19WTuo`0S=L%DlR zSO@0gG-$H-@P_=L2+D+!D}&--m+e`qa-L5wi0x3ZuxQH-ZfuMcNFI^=+ADfN3{wg$ zDEJ_3uuxN8TU)5oW_D%cmu0dr(4{{}Ns#hEArPy%y5xbu_LQ;0cJ}Qeqf-A@U7h3* zy&?#wLYI<}F+}nWkZRIlC6lAEU=^AsA`M*RQ%0x_fB~8z?E7DzpFRI!a(A}-G&U%X zZ6N!J5sB|dHSmG37|DMNfHHFB+SxUSsAfUXR5+`BZ5P3xc-oAu=OgMGBh2ILdx0a6A!LH~`nZ$-kq zPo1cH68Tph=iQX11bjvHLkO*Z{pw$y{WnQ<$X6$aiu+Mb6}kWFton=*uW(K&?Tbcd z+-g(=do6f$$f}v#Bq%`fj&KR#Q|{|nIrm{i!Y3WEW8=Id3(6PQ8CZG~Qk9s;@F`;afR?(nyFcP{*%{t>T$kS#Sox?;|=HNh7XC!=9k7vicd2DCMpx^G410A57`GVAp#~+@C=@Y+NfW_-d8UYx@mt%<=Pxw)04y_9~ZxMmF=knNI)Om>&3gj6B3xqIh zz#p<+Eh;hx%$S~iB?FN!a!nx?C%r#J5fEi)C<-C2sF7p zm;TA@!6i0dvycQ%2Op)vU;kHE&)+F9EV|hPmy6)?oBtp0D}T`^e@j-s+UEqBqE7sH zE)p2Te9L)T`~%f_N&v095le4_j6^Gn1KY6(4+Vx5MORe#7fm4BVA7{i%O}0Bjp(_p zx-mDf*!~I)5}WMk09MXadJSn`@G^)9LsK`0J(J9na4k=1{_#R@ie#y_IVpaCUtQZumMnKLFn%Tg8j=Jai1zg`b~*o^QQRCUe9ZuU%PnR+gXQMxosobn15A?z z=;t{(OcN3ra|nM95$qQ7rHRL~$rNfHu;%Lu`-4w0$Ln1VST5Ms?A?OsV+J6cIvGA^ni>v2y^nR6H0&G{W3%c=i@_SlZ; zuH>c^ghOiQAKacV*qcMv=A7{uRzj8N$cWtZ^lK}O$X46hxNLeq2WrIjUj0uM{{~gO zf!YzQMHn(NlI0beRUy#Xm`m_>7)73Xk|svjTHjt$UP;HWi31I9DqkS#i)j(qrHOov zFHW@zn0*8T5#MT18EQKfoWxJJD`&NUze>vgB_}hDvJV%C zW&{_<_nTxxS9YDSZUW-1f<~o9E&&ULj?vH zhMZogCx zeNSCoV^-~|^Is6n*kjLbpW-A9pJQy?mP&7LZKl94x7~>~ zv8n-B7QAj+>@?}DeJ*}H zcjN17^R@Gk zb5F6ftE{W5z0(UZA1o!$>1oE9zPuoG@aw241aNz}P$w>Y4jVMMt)uDrRq^0xYfsH{ z&l+-pxWMhdcmDlf5bUVRgb3N-6vfY;InS@8LDP z2t{uyjd_sR?7wC-8QJ|oOHNWVyDwK)76vNRn>&L?NqkEt^)l*jV^9k0*!y|Pdn!e| z_M;cu!tls1!WRrtl%9Sb93P5;aqhsySTLbI@Ia8n4NY;H)HU;dq{9Z0(Hynf*G~ZISbuuMUi~_Wwr|$EmK$9$dHgE! zaj#xqx)G)J7IJcMNz+8^mk9l`?wc2G^H&U#+-^-GnsR)gicgR}lJQDWNv|qYBI6;I zYtjog)1lu{!Q^AapfbbT;~yTVc^D~ooyBg*t} zXB10ESeVatCb+DO8v>dFJNmljr=l#EIvrH;s55m^l+xePHc&sNGeipt>}UA(S!n)9 zk@4pBw-2eXqea!=7bCz{S0EBi+!I%jOqvSyhZoM@xp2B4Z*#EF+cZ=P>bO1-2XvDO+|Q~cmZpy$3H)N{L+ zJ5yOWJ5xf8j`0%TYfE>W(B&>KJ`0)9Q=^Fu!SlTfLl_dJpGT&p_03T?fk`+$BC&#P zC+zXV8!oH9moLfBtLx28VMOTBZad+J7YfgYeKVu)X|vxu?CiwO>sYKb%oE4*L#q( z1We2oKrU#h4LG0xn{2KquC1@D#!AV|thOm06kF>1R-R7$h$O!X9Ar{kFLk>@DJ^w? zAJTNVa%RaSFO#{c(SNE+WO$!@Utq7z!2Pdl+5QB|;o)Kb;bOP>))=urFg#%2kp=#| zW|7OrH21p~rG}sL99ge~XwU;T{U$vs54D~$;9CNhGnrut5%O?QZ@vn<_x54>)3dXa z(xpF7o9XG&A0Y2X5FOdwI}Mfcgzm%J-2v@GV46cy&yl9i8Mj+FNi4Z&|2Rj;^UFuf zBXi9h&Pu&0>1~-tFQFt2*@&q3iBW~r%LUi|3Xh5kx(d$#VOx;X=7_4{b|P>4@@X*XUvm9O zrIfn)sZ(?$W*n9rnIZ0nYYor zVHnB(lz}KVCUo%J=?NqCX{dXT$D(1jvA8Cqv?VVmnK5Y4HgB1oolApV6Xxc|O;&gq z-W8-86`zP3OXQmCQRs5H!^8&TmxW^_M83^~~vn125|MyQ<(%Z|5P?|G!d(z14wDGMuh*?#arf<L@=EktdnF! z@?wI5&?jW*a7(~Dxz+OzIXEgK`AzHtZ{Pd%4-FyAD6Bp3mH_fKnXvae8{Hqf^w5VO z2LG`6EdS+;PKby`GgkX{bHGxERkzgN`{H5^a-Q&6G+u&(e=xalte)3diM9NHF)U9d zlE}qzx+!J(4QCtf;Ir@?mAcR~9@FP!NOh)$|A04}3i~|eNt0JZ*HHhVobscROrnZW z7rcDK&Gy}lOV7uC$J=XL0};|-qwhG*B?o6NDm6{I{r!O%$yBB|4gS)~vIE~CjmvN8 zbnDz;az70gxPJcKLv+YJL=MKJ&+ANhZ3l6jWn`4-{hu=9c@N3m*JZW;_*U$IqyDx`nA~WrZZ6xgE&@9d(UsyaVYHVU; zhYG;BZ@^<15TCl8e2t!xCM>9_tszFTWK=~xqcI~3?>=VXqPNC9$RLxYiAYDxnY*() zX1TpuF~HyX!vwG$i3QRr0sUN-H?}Zr4CBt@wp)+_rtCLnAk_%tl~%bKj_&s(q?QX# z)P9AFGy$c&d44$dy)%EtzEoOxPB4Peu=Gl`T+x5hdfPraZ^q)^`|2=wFM4DxqG@fg zmM|!eHKQs&{JI;(?JOnLY$gds{Y=TtPd$(B{J^Bh)=~|Y%aKSL8y_1SZ6o~ON*9Mz zIwx>dClYmJLXUkQoitVJf)$`8QDmdb!Tq4kf8_mha*{XNX5IL8b~e7DU&U7NzntEC zuYJULIPj|Uz(M85vv39S2qFy@Iq-7e%vNU1z4(svc5X*%%I8f+s@QCm0F^|C-%?x4 z!uDL_{McrGfr`O&>s$Xdv$JE$_P|&2cCU+NNnv4FY&>p{Ad#na8irpe!XTd)s<jq8mdW+U1khADEdBQ~grws42U&X5%j(c$DomY7Xn= zw5fO15ic~4<}WWZ43#kVyThQ`LtaWAjlz87)7*QD=*vR?wdp9~l}-w|?7 z1_XPbzP-1@pz%WotokUxx*j=9OO>qlS2RYo_+(yM$R0uN$3{)&Yj7S5iYcN(X z&X4<^9(ZTOd`7>D0lqDt)s)oFmBPcMqX)|P>~2X#Dd=u?CDDP8rPd0iY-U0pQk{ff z6H8q(VazH>k33&6^Ade=WXx@x3RT%7)pEvx-u_wU`Te3cB%nS8RlUpC&z zb>x!&2wz^!u>QjOV14#8|Nq`We8?SiKOr%m!({BWw{9JVH7+?GVt@>ejU|P~E%75w zCr3+Y!#4H7bI%oBEFL@c5Apve<@v=Xq(;DQ=ic7>o&4#?K@EzP*E<^}hPC;f7M4ye z+tGutQ7?O2EcwG9zc3#vrpbvQBn%3(M#rT8kg*=a^p^6viJkeT;DpOs^B;AFfm#oK zxN5Fq|NhBB;w;$p>XtyYmIot$%>(dOgze$$i-}itx8TBAt@I5-tQSE*VwMHk7tp>5 z=3a8Vce1<$mvq%wQPalBfH*K#wk|{5>opr>PUrxLngoNF^Jx^-WL+|4Vk<$o(ZPR zx6woSvCr3RN|$*&n$mPWY$xJYN`?`laH^$N@&U*CHus<1qWo7YuOiI#@rRoE^Osk$ zx$UIH35uoz9_~`Fvg`Tez?V`plI0-**zUe;x!=CClNn)?~j}NP^>~*6TspDlP zWu7f8a3Q=a3d3SxXg$Y+#d-!Id&#R!K10J)4p`zNR@Yy@vh5(gK_I>7)LB@9Fv8IcA^2(l_6|)z$EWomMo@f zi6KhoCw)R_wDf!7a1NM2m?fO7O3w!n5xTpz)c`WDc&=Aywg<<@NGR0Z%~9_DzO=KW z4Nl`-D(y;VNmhabWpKPS*{Qp*n!IF$5qN8?I%?7_A~`@PjDeE|e%465Kaqpq?b&@D z;)pg@Ps&1ehk&{gCD{EqE&HfvLb^=uQo8ph28dE{+I(J zs^U`?jfRHdTd=#JOsWizkM{po@lw|;IC^r*Cyv}66>pL6|5dCOuvg>wq-jQeTq7l-B?vmzdXStrm~AilyC(SI zwz~wK-m)cRmPz$!)W6wK3KelXMteIasgzv%Mr!bFYj11xM^>@ey5qtzLZ7=aavpABtR_ z&($T2H+y2BD}p4?Qs<)`Pesofk5cHzOG`@;0CoaA@WTN71qu#ig_w@@H$?M-7wIA8 z$pbjR*$_r}UUYtb?g{}}H|mc}uVx;?^OJ?{5`GRzcDtjEr%_Zpyvi<2SIvl!l9*VU=igU(xVClUZvC zy6Kk;1waLiqZMAx*(CDWb}r!Euf}yCS2UD5EODT}e~F)yp%zf(esy=1O<$6VDFi+| zqRnOX7u{FXeEh4L5_E|ZBaT_AUgUD{;sxI&t(=?rdN-$Jv1t07!s z1SUdUp4&6x;w}Su*~3W%B3=?W&Hp>AW|9za(w%y(JWbIGQIWb^@R< zj$h?>Cn{G3HY$m{))H{E@&R-R#l%G1W!a=#TIlE*_Rd=zC`4S_r*~0prcj$kMm_>b z+kSuP33zeHn}c>+Jrnk@o{EsjYv<%N4}Rs~T$WMDqWPsgf!h>0&am)=p-WAZE-#zo zIpFj48! z2H9RkPs&Ub5wm9@p+peC?unh9odpwdzJuirHV#fhs;I|__XZY3pL`^Epxs~W?FCeI zMHm>0OG;$G6qpT`(I8Sx74ecWUh;yyvi$J-+vOI^fnj0n!0`n8<>Z=15pR)Q^goLf z=}%~IhbBjSzkGNW9qM1ahS956^X{RFcGj5(O_y6pA{x%XCk5DmS-1!EzxB%eqO4gG+6n;%F$ebl zlx_W6|7Jb_6*z5{9((JRV?)`dPq29U8HBaE+I;5SW}~*mWwt^V#BK@6-uAw~D}-N~ z83#&zYHBNeU>l-5^n2^Bu>VBVaGbz6uXJ>Du(n>WrDx5Q;62Z4Z_{~{lB@r#8v2sU zoD-3NEibO~?3i&nTSe7hd#r5oyx11=pvDx@GlTA`DW1~^tk95z8K|HRXNS)(PmJ#N z>DN4DB~Z_hK*`3vBNr2iJ3cPBw6cOEyxui6-QdsfOMi3;qcy&zrKOlAWPVV<5O29O zuucjaey|mkOcE3jzy=$9xKv%EVp|U>bHQY~-vqpFO~bFSKZX%Cgo$6auYNP`P&+)j zyS6@*lp_3Xer@gzAEbnL>qCfqk?tzI5?VSsS=;b$u8sT(Dhhc_*O)c*&p97n2UQZl ztFuAY1hJ6*;GzQq5*YGSj+MLJhc81bCPo4IxWOSI*g?%qPr?5X9}K)9^(&eM+??*AvufC2C43%F6mR`GvuvQ+Ou3&Y*K?52|CvlMIz&BU38BFpAZ+ z`C2kB*HDOE;W_2skqz_vFzB?XqG z;;BH#q6fK=!THG^;xPcxJG;!_$+>%YNXN|`1=MDE8Crav1U{tdcc;B|T4v+$AG7UD zy5et{nJfr;U7&Ej~QU}iY!x8`4MsaLDL;y z75`rP%0z!e6yae~%aYKi9H<%M%j26w{h>)%_?TSG2Fi&Q81 z3KRIRp26va1RU#?xw)UPbw*NSfnj3ut8*EKfUJ<-WYM;A%G@$|LlLK%yzV4^EFDr( z_v6FmKTq!?u?!PT@Ill$i4kgAC(-_7QK1(A}y;g7*xY_L?Vr+Z~e#kragP#KO+34_-_R z%+@ ze*B1}jB9?;dIS>@ba@L#;IIs6S`)mF0}#p6il~}@eI`U)%ixD8oda-i2`FgUIXQa; zV2=4nON)pe$h9X$PjFCgYWKm#v6m&oy*i3w7FAOt0t1Dt^39>oI?vpP_o$W{o}&e! zGplO7Xny;bI@oN)ukoFy;}OP*7S#1%EB+6svV>W&32D-~Uih8#1nCS>uec1fYzYd- z1E2_)O0Z|Cean361+OFyWn=7QMw|if{-68v=@umB3e0=;% zBt{?y3|N26&+}MMd?*6J6aI2%0xrngK|6BF2tVlFQU{))k#whpPS73H-U{y8=lU-?{1>-FWi8z_ktO#9frK%?0jLXz{PDw;fJ) zagY?Hybu!6#KZ~<BIkieRw&220_{YDb7vZ-N8kwa4!>!+i}4r3$UOo zIaUIAW-V>*k>|l_m=CBVV*?l*F6UbX{r+ zg|Y#jK`*B7YUDF5kjn@?3KDY;i9NlrJz{2IY3}F)F;E8Lj%ZmLYvVc)l?plht*!0iFIqVN(l$bXww9fj6B!jH51E=kKNWF1i%72vl4Q36gacXcd{N-~M2ICwpDM|G*mJqvwkz}j_)vQ}^q#s7f z<NnY)=Ys=b3_U25LKE zP$lLE25T^zQ6edgFi1fJh9Q6w=SgsKlwQB)poi;fyv|G50>aoEfcpYVu~N4K6ZkM> zfd<{r14Jv3QI3p^Aa5)5Z^I_t@eLs490wxaWTmZXvp)`mq0+dDgA^Z{LzMyzM6v|U zp?aYVeL?Y1yVM&P*PUS9sw%v8XN2255uzz-GqXJK&6 z0y96eq<8PGgED#ya=3tRmI-Zw^3mD=ErMr%`}fx0^n&3IbO>Pw-j9;Nw+M{g#%k_| zGiS*T4CjjB2Y0*C%IAGR!}pmMYfr{pY>!(ud}-X6M>J$`GpkZLySU097rIrZ()})OG6G20f5)t^h1b=?{&@C1}Oxrsmx!TWr z*`sf-{PTFCj}F8I07x`vshRbW8;*h}CRpK}S9=M+{c+=o;*%UN50@9Z+8o|nW^n5U zYq93?z!QP}{^IZH*Pzx*5OiR`#5~3zTk`{2Rcvf5NL~^0Tdq=yoX($6g%qv?y;{$3 zxt#MPy_QyFc($3XYXX0t-J_%B`j_VB9nb-SUfFmlpR=mM{?*1GbqI`Ep4HHJ0V9;h z^Xh$e|wR}yS-z$WSEvzhK(WgrNG zuI)Sp+IdJ=3=0j#^njdIq&I?0A7IrWL#iO$eTleGHBF$iGJ}Bbt;tF($h`s{6F6E^ zQ&Ig_0i7!9Q^bHS{`!y z!1q0Ld48ab+$GSh`#e=qc?hecj;^jO;IWFh?ccPfhJ8{rvE(fbrh?28KpR<>=;$5{yuTl~YRIW=>d3pcdoNYYJTe$ZlFF&dA5zBpOV_``Nfc3WDT zm3J$NTFEm++s2uT_Le(d@g@gA(^Z}q3@t4oduHpXOsCk-Z3vEzLO2SwQg(+K^YNJj zwLCrP0j9-p+zvv;xr)tGPE)J4e^qCKNPLG2xX;<)73_iQ6S0z?{_bqhP@``)@`afu z!e5!1Ajr=7YiOB+6POOZ2WpVam?B(N&CvY;p#UAsGV>t72}wtQtRpU?R!3y*1P6N$ z5|@AeeB+}A(+BB|Z(w}xhSZO*x^IyOsZR=9c_1_xfegd(ctg$<3kwY25W&U?Bj_!U zx5e9A6_X&>f}w$$n#W?;9~VS`h~F)+paxsIqN>GvGNlhz{96pgd>_+gYV$IFdg_p= zyrTzN9h;f&Ee(>g2ulhyKF9zASI2%qs+gNZS+3V<;!|hGCAi4WlQibSJwe zNwfu21(hXIMS=!#@a&IJ3|tyV!|hLw?2hKdS|=i23F4Zy26hv2<5Fzm&FdHRmH zxVWQ}lba4=Yz4pH(9m%6_U$YfUS#v17U`w9aB_1uK{EyZZ*T%|&TAo1Bz*;xa({v0 zPz=`qyw(A5+QBEq6GRWR;PH(MRbXvnWB(g?Qw-I4c>sYIp)`2G-%kJ;6wH9s+}wE3 z0>Of*^XJkT08!REDCHawLW7)vZ{bKEg8?X5 zu5xm7b^>=l-yYbB&XoEti}$t6WaA!LZ(=9x{#WsH(l6h1IoMn27Q-^@$-N7m;{}OB zdOcJ6EN4M_G#AO+(Qa<}Ps_M}&gA0S1r)(EX+6?$iRjp0AJ5=33_QiIe(^Cyg0M-> zsrpO^4OZKi-@nC;K4(_g;JH;G)mcvsQe2@5i+$d*k`ih*wov80xrQ5X={VSm~d!iFc#yg z9U$m*1^GsijjMcD>^YAq!4H?61^~DL0)quF4Jcq6+T;}Enp-ClixNUYcuPu3&UJtw z@I|B|_5h20pU)-fBT=4-s>+Lpu$jem+~b^&P=)!om8|8RE+j^@Xe=o7C+2EEo=3DOIJS4fN_f?^uWfFY zl0zmgTid?BP+KxJH8iBmr@(9vR%vnwwM`60Q+nu7;15FJPjN+s^boZ4MEo{nZt!-2 zVdy^8D`37hL4^aqi44$+dm^#0P;$n`#t_E~G2^+PZ-E1Ztq3JC3?G=upj&(k*ga&< zP1U&JS<@iZo!Q&#tQ1mtDa760@jZDgmsy%cYcM}wZZ_<%T6l}odDd~5%Q)etc-!!QXh0q3Re5yuJ?%>Sc}o)mz0~qU z9x)jk8@4i&9z)o@AenU)pPxazX;Xa$xX%Qi+VDu;`djvD(^>nQRR>htcu@~Nh((1D z4Tq4hEzRe$Bdlx{!nfsqe6uzm&qTRP%A1}Tg@DMpc6~!ck*x%jV%@hK`eh}m= z=jwWM^y}>0U{2TA_{1QjNfNMW2Sa`i{r>)b{NEvEtE&U$*3gSk{4Rp5yu7yhhT-N9 zsw4SwE;tOliyQmoB#-wtEtaIlzZ=Jca5jpZ&}f~+3==G>AGmiU(P_>&Ia+7~6P%k5x7IKk+cfcB6AaKwigxMHsI?yp{ z6oHktyetg`2k1)xj0{1_19T=x&Hzxy=%H}J!7+lkVF0sFh>*lOL1EzoG0?$1+J-+q zHZh?FY1MatoALEUbz>yGJoJez@EaB2Km?6F+&mIA5gah^-}JE^$dv9*6Q=~oYgQ0i zJ$k54FmR;vKpj$XaCTlnQ1bT^aEBs?U>ibmk=!X<_)-SopNaS*@Y~M3K3{y*U;KZ# z04H`(^W-2P7RDnH@AG&xe3o4oU{CqNWB#9pKgFjB1_WXeR}Sd$03Vv)+CreH0R~un zJW7BVqxho>jawlUzTX1;Yx-KZeFn4jL@{#CPI$IjGyIM6Q>uKx*fL~bV4$fc^KM#Z zj=IBCvThY@YASx*!CEt%y!mVM-tbbXagFG-AesHOw{u3Ku4M0Pio>tzCek4EGa%-u zO5oOR>)26ULcfbv(O$)bz%g}wR$qEp_LM=~+tS|dUzCs$fy4EjBF71)e#A)vis%ag zmyd|`vOWg~$IGd)owe!7xHO+sSr8bWY!=mu4x1pz7+MB15Wuho(jVP`rn==E1>J1=J1^`p zqIn@S5;4Nr-d6PX7DL8W=)Jz^m4*6y{Ndo?_z4liL1+Y8@kv=lMbHRGPrV?9jB^0J z@Y0ajJ~G%>@>q_Lu@|vH5*eyxDX^gnIVMKZhp!fnVZ=GjplS8e(ZjF#$|hX zorK5iC!lHYO=s|cp%)yI6-yIiD+MSISLJcGdYpfDZRt#N(ji!IpshD%tLj;_W`tT8 zx$IZmxbQa*cbVN?HXqlqCS0Y-X-Y9n$*AYKljmDLOHz)Bicykq=31rDm5QpD_i>C+ z(b8y+t#{tr-7_j5=;Jn_g2o;=iIX7MgofA%+H5dz^K(Hqc)-hyMnyww?CL`2Wn^ST zqJt_c`Ju5vLVy5{nO|FbY+mbqLBb2e>P%Z}>#Yj7NO-nE`U$-_9v+?~^pNWSXCqcn z@Y3BxL4iU4J?xtiDM+CXVZ-du(2yi7CF>nFM;`E5jXewy^E!jnW*PIE|IdGR82ItX z@J2uJ&AJOt-oWT+o)mPXh~@}#{@@D&rU}pA0p!V|6JXsjzq6wZFalVtVNi;ZStE?G zXhB_3p`mgES{vC9S&GJP5E#oTtCX7gY?RIM^Tpi;tX?YiNOg$}jos00$@6|2x>Gkn zBBI1y=2l0dN6nuf+oK!j`@X8p&QTJKmX^-1u-Ixp{oHE&YLy1t^NT6FUa5InFm;{v zKpcT8N%daQn(1_86SE62E7A$lQhzqq_H^`!J|sYzS=g&dMJ3u{<-!}2u>kWs9)we=%=2xPCqPB<5sn)&n$4EyJ&4gj(|k(Z~1 z69I(^)`grrJlTdVfk2mBTw3~&no6;gsaAl@X7^*+P4_Zq*M`?g%OClo5uES+l~YO; z@pb7-RY>6rfekf9#N*~!Ri?lQIeO0?N5|}Y_yl{(Xdcc<(u|tn`te~5k_BR-4@e_$ zfM?61Nenz&!ANyAXuf@=-MAA+E2!DoNqI{`F-OZ%GdhNUMjKe)+A9DHxhH#_XQ}FawmeCk7;9bb5qT zT2oWwngs6~U>Zo31{+Yw-iQ~lqsgI<>R^K53ErYrFPITkz`x9_w4!1enha#1f$iis z+u7bp@Zb*#3&VPMH4RJ_BKXkZq(jJZ@ymwma`F&bE5S+tRRB60A|gPfJ5$0Avouq^1zGj-G#}U0{^`2t8COxQhTnJz)q_Lb8|_iLkIRq^9?4{;S52VHPiJ zbZh$TNP6utGkPr`b1OiR9=aHca^aNT9{&3>L57>4G?c1$cv|h(FWXHQ11wa0+cwB zt24^RldqqKlU26}Zg(Kyy+%$>K9gHiqzu@30q8p4U4_9Q4yp&wPk<|Z#2{`2nCNC2 zYU9KNiSmy zMFfiV-A~~W_hikN2{+3)IpVv&u-<&9d~}g7wzQm+&Rm#z$K0-OfG4w1JogrZCt8l$ zBO7O-N?eLvs(GBmTt}iCEc6BD<}bc`zn`3U8IWd+%_;(|pOx%Gw&S0YZ|rfDWjTED$lsf;7NVH1j);bJT|6@FfCbwg&qTXyhT& z4AygqT9=ubS;TesKUg9Gz63$XNRSt*SWlB5R)UZd3rsxGO!{>q@Lwb*OdFydq9Ks` zoV-9IF!2SO>^3M7qVv`BFyT#W3PS!uJFx5KF3l*z(d2NXH6|btn<)uxVaN!qTH%GhtvfII%yMUJ|8RbPP6*o z@x=F3ND|CD%K5&{d%NLLm^NqTYA}zxn=|gM2LxZ4+!Mkj7h^OsqTM&E*S_O;)g}{A zz245LJ6+`7l|C&ni`4w`Gvox7=U2sN#kTLiJwHutIU4)gJI{&*Gnn3I{FFEjSf*B; zH|;v{x>;5)x$xj8X?a8@;|u~tr8Kb+4VG8%_-m9QSj+;HD$AfMsZrL{B%s%;^9(?U z!bnuOEjJ(E3e@yy4yeIWkbwsk4@&9}Fa#b(N>fl+SRSlU0iRHZ{RhlOGB8rAbhWo5 zk_a$e#&)?ncb?C$X!7@6pgPOAR%Km)b?QHm<`Dg!4a$V6&C0AF0A1=4;|Mcp@j=i|DtgX<= z^33VavcUo^T2K%Ze2~!7$cdJ)G2uspg6VK=%s*)!E1d8i6fiU>+#A-niAU;%`IcjV zGQ}T6^4(pd{A*iG_Fpq0a;MO$Lsw+Rs#bLqBSa5EGd4Ea=k03m{@FN{?9$QMxsYUA zm+|Pr{e1sG8IEdsN=63tHPTUTrwK=7WQEWP=rHH%3Gne-ptS(@B0Vc>FfPo#O*1o? z2m8w<#T>FU`0ANeRpEqvX?6!DM0O>1B3{QB0aA!g55ao?%tE#>SK83BA$P4sOegEP z^)*m^Sm(hFapq(h&*?w_e+nu4w6wG{CI&W$*$S+7JqzJuFD}mixf&gr(g|ab8?5Up zD=N+X2a>GzgY20KtN0DSZHxNDTAJ!aBbTAI+)@)iffbLpM5D9t{3b$vfwRZp$xVVo zBPMN-k$hhB0#j5#@jL4cU_gaJGlWA-uBh!U)TdYTa1y8lE0-KM;7K0C_AL|_0?akQ z9{i8n<+-2*T%+ko2dat=6AS-Go@$Z5WggQV2 z|KI17r=(i0)xX3^o4Oj#Q+_bMQ z&ri*Sfx(mB4JMHcweE+;v-6T>Nszd%X*=EBN-xhDOENcV z(PjsHQ9EE$2tOj)#or^tCI5zaV86ZaypAv?fo>{t@qNGbE2`9a{Q?)BowHe>o&=Xg zwV=7eUz`69o>@!6lL2!kMIZ9kzRLSbBWegYLQO;Kjk!Iw=(a9u;E%Vs_!a{JVZ`+g zBo6N94;3v<#cl&Su2=8fGs?=$d52U-XNDoJ3*akZ@Q7cANk;7tT&Okb77aJYWE|O`%uCF=mq-} z#7hgH&eKYOR2D!I6Vn;Ws(o@FAd^z)V#-_xY!&_*+(PcYD+8>$#ZO-;+ib=AUUp$$ zdnJe)LydW}(wJ?^BXSaBDmS6PJa)45g?T7z*Kl$*mtD4i{k3;LG@?bL?X*^3xBm5p zTu0Bo?LM>)v}Gq}WMUtw&UJS_Vr8b!-O088tjWMjO`+bTeK*$~U%3+;T=wlL zqQa9}_jKxCezMK_J;uZee)L}^sv?pqcT_0uQi|N%`R)W(56iJJF)iQ>^e_O7t9DJd zwzoO8VM@Tn#5~6b_$n9>x@MSq+!7NKh-JXFsF8`8`HqK&M=CuS(jo~D2968OcjU^= zhdx4D%|0v8*8tYV!@+q32N*B`AOfMy_j#Qz#|2Z0Q#^@87Jty-0VD2u^$VrvP)E?S zU^0TQ>lO?6Z!oCid?R3kPd`y<+oxk&D{T$~BL?mJhBx2^8m(89_kr%onxl}y`jNX{ z^sM*a5L{Rw0oyM&4~&w7p{eC?-(R`|{nhy-;ya|4Bx?QtF!dHdS*~5%@MD39ihy(p z($d|CbV#>IcS=bL3MkUj4bmkYf`CXj2#BO~cen7b%l&>c|ID5(;*H>aow3%jj^INL zC6-Xpk#mbEP<|)U_WiLW!Ds=hpy!0+#&7#O9O6+dQYcm8oAdQm1`ly^OUg={RpsUH zLodg1@6Jrq7&a8O`oBujg#Xk!n|5XH){zLI`36N;k_fW$C1S~bd?R>JQ-dGhty*D& z)3Lg*f$7%((KO9Yw{69{KlF8kpFD}6Op}%|{^WsvKT}dASH#oNUo+dErtR8>Vo3%JQ? z(-%~7ymqZh%*%i3b9+=sJdWT?!pO+Drxwy|xA5lJc)iXTe0E%%Z`mDR zy{F=w|2tbxpSdfaMHugS>4#$GZCIMY)(qAwvmK!t+eLFu*Rh9qymb#J(~oa&8GG3rj25|J47iy|Svw@h)_*Lt6T?hY4999!+>sYCyPr@qADuQBAUge`j)= zfrY8G*6A65ynp%V%ci{>BqzAX4c5T$Z}IE&R+-s|Jk;#WPhg${m+|UUIUXzmgM*cU zi%6*B;{aN~0GkpF??mR`UqE=`g9nD;1GxsGgq+qLUyi4ZAVmXg5N?x^=>cg<9tQR& zG=(qAKY+!_&iwa3Fe%Vghx5PEN`0_E!Ta}{-COfCaz8piaW^`OPhVEs@E2(ay#2Kc zmiCSGUM=dfMdL2nE@V*fQf~HNUXl_-hPTcrA9}GX%@9ghJ>eS$hx^Yl35aXGd zfidK%4U9~&ZlK(K3OH4E%$zLBNe{8(4IT+ORx;2ojl;)B|M@ zc!S;UN65u8K#_WgoRh$5;Bj2K>#{wy*pF}&GN4<2d{GKWK8V{`@b4l-Gq^xLtJ1q+ zf^x&&#f65KHy!{@Hn@9I7GYu*gWMBU{_Pa)jmzFEj+C6;?7W z20}~&N?Sh}0#5^@CA$TdHbSlRgA4BJ79=Lr9ZV(j#FKa&eJW5^0=pDHdgd_Z@8eB% zE;&l=rPSYx4y&Wx@iMlHH`2V+n0p*kXJ%HO1ptE(F{3^0n7@dYiMBr3h@=XJBVicD zwqt=!X43oOW5RHvet6P5B`;lExO!$Y`l}w@_8zHR=LVx}nhHtwo&`dg$ISeZ+3ISp z3mZ~SOE2Y;zmaspqv+Y>#6Iq_jS>fZ5^SlNw3F|%XGH|AXj?N3Wj6!??=Mz!cQuDt z$mxF|Khk|2O+QxRHje^AH7Tg*5z?M`cW38p{qfZN&qm+6^DPdP`zv8Ev7Xk4`u*c< z^X+>iO~=!AIV$a;2V^X70l_o^1ym3&Hg!S>hVpLQ?-Imarhy5jlE||6N{W(^a|19>Pk!==@Wtzy?GphowfSaN3x_1_aP) zzXCd(-J=OR6$+q~FAszK5ytzy@XR&qTv?fznBu;995d0#Cw*>#K_@E+?GQpc68;9N z1uF2g$ys&c1J<|fKZek_RWbQRDY&MECntx1UjhvLpjp{N!cfDYv4(s}CR}FSU>M|O zgNsHtl>fA>uV$Xg-Gk9D%)pRJFc9ZS9Q4@=k(6Ls`U;i|$YTWKp$uIFy3D;3cA(|q zSDFu8hq20~EaJhss%GzG+_pgHS@P=IVz1rtHsEMtJ%>abN5%auTp0unR@4%1Yo4#$ zNKLJOKIK?@#=uZGR%*mAigMTvr_JDPYpkbgG3YzOie3dRn^$s~0H%M1FMQ4T9V zzY);%y?plIvgpK5(zBG&ztmT>gmpxba4p?ahtt8mntSopo(bh;e{(yU+?g%PP_10L zTJn+rGvKmy>%)(av>D#Ld!B0!b-lEd6vE7oqNS%dJlUCL&^ck<+On;qudB1%zw9AE zCJyja(qIQG$tUq3?4#9Rc{j}DQ=5KQVO`yV^&I~A!%&V3GsIN{Lb5p+#bm)7T}KOQ zE5KQk+#Ht`%O&pV;($w$+0-O2@)xeGn8PT^d@vKuW&2XXz5pRXaZ z?AG1HhMm@c5I|JrI>QHs%6VagVorG@E4sMUc)u@WAy@TwQ_#;m8}x@~#FUp|O^xpy z>Fne3bz6NKd(TzW*V3=U8(P&)oGw++CKZ}g%J=~mBVI;&XJ04v<-p^v4BnyXRsf$w z9&{DMsG!`W8&?Ktk%1er%K(jeYc66*)&@BL7!dvdZ*f}r`3{<+x%KrRC72DRrKLq7 znU9FquUl^%t1l4TKgGtjml!l7lqopSqCi_?*71oPENFnkeH$IU`4^n3qu-pLgpmnn z1OEs~=7g!S{zEddewevdJFQ|-LEQ=0y6A}!q!ng}L76yKX@!@?>a?PW@bC~5{^DY0 zD#Yd=erQ%s&}$(WIXJ{Of4B!pse;J#i2|Gyfda12g8$0io}7W;8H`r3_KV%e)fDkveu;ZDVdMVoJ9mGyjwf4{@k>RB zp%vmZ7vY8)jUmf|10S>^XP`3jvgZkvj698sU4(Fgo z_wWrwa^>!m@bA?HI%|Iqoa}nH@>rlSBA!z)iRe>gRL8zlulmk#Up*AVV%ML`9SnUkZ^)Y$kg#|=m=fOUh8MHKcA9N<8Pfds>TU>-n^1_VNY*Fym$ zR(MgOe+^O^$N~BTtg{q=b)@pezYFz+Ka%3Y0BP5EP&CNa2f;edw*ByO_vU2d&Wr0~?6t|6@cXDbdF{S6^O(2~A zWAT)grGJTth&X{H310Vw2gUif$h^fywgi#656pTGHpU|7+ZBF(|3jq8(XNm*H(}SX z=tgOlQ?fHd(_$vHV4-`ob(1!!Q~G&&1$d4Bp}fcMEfDyqPKur8ZrbNZH&osi^|jE5 z8RXmPE3DQ0PW!T_s`U`3;PhxuSFUDUBcn#rh*H{czG@SnQqZz8VQ*!A%n{hR&}{Ch z6bu7|m4F4kqd*uiMX({_14PFmP=*;RIeQpy~0T`2=Dr zaJ=@Y{PPB>;M=SJgiuuaP$+-8CiEbSi!ui zfUeuf$_h~%!UPwF^Vc(=A1?#`AJ9?UaKR5A%oICd)oax_PyuWSc~l0-_Ywl-6pWO` zEN-HpI2^1fP5$|Vc^QVRCxVxJuQOjJ>dPmGWY*O@bdJWdc~O=5t(IupIjJM~?>=jn zqKU)KZ)ubh7wT$fS!I=>G}f#bFfaC%co2U~IM#0OElhLuD`Hkn~bAM-}R8fDn z$^@=h(j|_FtJ3{T&%k!AcTU#oHwgG-9z)F}$c`>BN$AK%g`yHhO2cw^9xu0nFX1s= z6(s=;ue5@~GU)R_#~c~>EMwr+MT@k0p{{5^jpH(#`>)#w(W?Odr+u(K;SZGxrtae2 z|KkFbxgT0~QSkF8!W>!UymCbI>uVf<(D(B5gV2RHPEuUVdNLs-ide`KAE1q{xn>OE zh2EcyuCCSl`FjTkGw?C66H?(K9DwQz`qM6-TVw{=s>(`}-~Vkz5PQ)U&U4F`ZY@v; zR#>Ls&XIm}GsSfqZ;y)cifc4-<_KUkPF5 z;}y`+cg(jZws-b;1m_WK(m;|r1~zs;K&`x!HN&FPtL~Tz3@;MM7$Jg^LOg;}#s~&V zU!Xq~HnOmghQh%F31bH-8Js+`F!6=v0qKGf=Sc^LIdOboErKjz5y+3o2cQCFN z5fQnFhE?JK7$rz5n&7KzT((L3`uaeKr3i8fXbPM3K-*?sseO4L#@vu5QHmU_yeG38v$W4H$!et;-lc`>3bim+76gDvQSIUyXL3kT5C4}~G(80kW9mcE|BoUOm zE{Gv@nyA0B?!RkxZ#RiIY4tT}y{>{=WL!i~8K;$Itud>7O!Vr)MCivCIV^k*^E@Ic z_ilT5a0JDA%6u&%kAid<8n!(FGG@)-_wl#3U&l8ckd(7Zp~$Qp@EP9I;!vbzUAY(a ziSkCW_HQcsEiQ#J2ROzSn-&W)Gqcz;JscSxwnlwWFK86tj1);JJYikDL->YOLR_0O z^`ausDr^pade-Jk55p-t{Pb{}mRlX`P9?G?2hObwq@vxqlL>v|#pU@nY};3}lLWfX z*ll4MHnsP`Q1l#OZwZu@ms^0Yl<@_JS>MJnWEDoa0Q32a_9?->(8WhMx$OZugYEA| z3fUZU;A6JXRKs4k*3>+e-MhYdGUJPp3x-?JcqZ|=1%CP@uli7UW4w81wi*7ai)hd1 z*Q`RlJLCD6T@xw?BXren00Qt-a&@)Duzgl`GAvB;fi5K;jik?-Mn*1e*p(7(z<^0Q z_X=x7r*R!N8>V9xFI=}+`AO{pJ<_u)y^(QucC8dl(D~tXTV1Q1^nR$|mZ?aqDX=XH zrh;-nZiQR__U+pGgx!h*nSJ4zUm{hR7*1K!tC+yis zNo`?RJiEHus5f@nY`f+*k^$VEs2%$P$!+RCV=jk}B+$k>>z2lDu1DPszH`SR9#iP! zF`nJi2fCU5XaQfw5_(^Vew)+JN~=(n4qGwV7LA%^Jyz?Cr&X~fqzp3ejeo{j3wY0ugaXrGi};E&ifK15JxXL)(Gm7!WNV&A<5-Dr9RfHteY z3yFZYvFOWN?ivpWozUfq=u%HI6wHE%<0n$O{|3}Ze&v%0+_Jm*j;~(n|NL?7i*`*! zztB}uNy#fNyKyloDH?hozzAT&_5&l~Gn5#`OgzfIxNx9MBn0 zEJqAf-A~GH;OxaJ|&i|QrKWoi|+#HR*FRMdi9xIRX>jb(IgEWdit6Q04 zKKz*BE6C0#w=kcPs(-Mv`&j-@!;cetBeYbOXq;|5BdZqy!dQ4Nz9y-jeSn6ra_09c z7PYm1+M&eEDDNCuSz8FZ?8FY{xV0Kg{L9zQi^mVH*V9|4z@zmy_*<(oBpxb9twiWn zqp-tXo>?XN2vb^vH99J~r($z^-?UrguJt$`gt(VOVpdq&wQCzw<+0qN{yX_WfuZD& zANxX`2Q#rzXb2jT_~gY|O%J5pk4b*`lX-oFGsqiGM?`YtUrXe;+_Pi?j(D0UyOP1F zEC|G10zO+Z8PMs&F}l02;Gah;5k@GI)AvWS!j%=&T+0Z=0O3);inrSfw*1LnBk4U5 z2;NCHKy+Yz<&4MAwAJ{Cz*U?s3#nUV!NEu(aVJ-2i-2I_qZX)K?e^boX7aV0Ol z>;)!y9jUqxdE-o#_`e-kOtKh%E5@aEr|~y@N=o+0izmr>6iHd;;4tR6!!lyqqD0E8 zUexo&CHr5d?z2`T)<`4N5r-!Gd0fXumMpCuSZ?#?>j+KkP@$XkEmU@;+*G&g8TP{u zel1CsS(?a9)|V{4?d7XgwP18T@ut2^4kSh|GsV2w<3%vt>1i?*(>v_h&M6%go8^Fc z%dFgUXDWz9<0uy&v}D~YQVQU!wAwlUD+z1Nm<50I+xnwM#>8ET&ope(vKHo*gbzC{ z?!LC`oDIg`s}#pyz3X0;GSRn)s#G`POkZ?=*fj3VP5QEOCfrD^h4$xo~4(v!1C`P zlkSsW9LgxDPIidR8Pgj5`KJbRtTXJVvO9`&V?a7pzYUN3zyBVU5V$r7`f`hV8+2T^i4~pNgIR4-HZWp z$`NQck!fq@FL%?}&LUnNuM_3b>nfdS zOOI!ws44>oTf>pSr}&wpKz7*!b(LnhJEt%SFS65^`O(qQxol1%z&Hw%m>A*pc$-4M zey}I+ugZ6Rd+%TF{M4lN%7*5ayWbGc@fz-=cFV|CXAzm~MP3{RX> zM#91mIk$L5W;rW);X%2QTGmrdlkrFzIGEKygFsnjV*-6Za-*4yjIhhK8)y`R08f zUr_P!0k4IEkn_HAV-pp^1#DoeQ==WhDstpLbO~mnX&o3Ad@wl3+ST7}JU)@epm$6H zKf9Ea%u;zTDn4fk(J=8rl)A1hF6W}dnk&b=;vUXH#I+2H!i4Dd-}bvxYS~VNk|so` za)p|uJ}|-@ESJPcO3L5KAF!27{hHNwPj8=tEmtT7X#GSPq6?4`6a+mdccqzB-zAqukW{ zDKIP4)zuO1BF562j&M;_XlUvzfvtYx050s{Q82`&8t-|uuU$mdvdaM`x!Y@uiZoP8 z`m}Ry#<@Kw?js@~$ATEPt{ybNL=!{lwAAp5s3i@$`u`+;eqNcvTI%dld9E$mmZHs1Te;NOMXh_!D@?6V?$iC8+h6I(vQ zLX?bhg{_@$mHZ!?o&|3(pj!+vye*GS$Mo~|k;s_s-aZ;&&lC(<8z*mnY5^D(M`!0< zC)*}o=j#FaPI52nyyFw~rVVv!znU+f^GSUF{ymsfFtBD{$oTKK$~w#MnqO1KE9+N4 zT1W-7mgkC!kKmnD<$&(h)opkQoSYC9sF@b{&fO#YMMIg1U7P^-m&43Y1Lz|V>a=SN zh#dz%zZ0&iiq3^@@X749UMjm;-_IIHGlU-o8qu+e(I@BKOzC>c@7Q2cN=01M90DH0+~$9<;L(_6Qj zgcoN@BQHqxV!e`YD9VUOsmq(vhx9&~U0>=ejWU;8%-h$x9Fn2fPBBw%f0eK5kO^`w zGO&vH@%^vx;8z7ZU7S*Td(ZMiewbUa@Hw7wFeY$jq+3pXApo&|2Hg1qo8Tvd;G2;& zp}YCoI|H74E}MNoZrI(~nX!QQqzdz?;J|#ha?>P1j~M6|Ar)~HViTcPsw1650n>L~ zoC%i=Fhd9dOvV5iz?ku#+hf9IAXYG9NEzFWX2*#o9dW`*AGlD# zVf&{>)Xf-%9Fp7p&;C7!G(V(0w?^))Kvp8^=$PIRnj6}kJZvs3={gtS@ zONEH#TPkLM-Z>sx-q0O%S*s}1r6}>QAINpw9`v@J$FhEl_QDuhdtM^d_k&iLIfxf~ z+W7Zi2t0$;BfExOi)Yt;-79j`IjORsaCxNAtN-x@Iz^f7o>*biuhw&o>Tsu^lGjEn zE5|4x6-B%gATs#pk7+XI;_zER58rwHK-yLgVy_fL+l~}v;y4d}#xd&IwdR6d^%w2Rz!H0GUWb+c z!9H;v1K2Jxt6OLm07;n7KO(p1;PNS^Xvli1(lMlClfU!9hNKKTf_CZDcu8yLu&5dM z>=d`JDqNaT6r)l@^C-B@Y!Qs-y@m>8(I{xGR$m8 z`d8295D>B#XDh^gOe}cv&;9;36+$iOO62moUVF4XuVhHE_*~aZ2nNE3FB(j=UGYQm zPnD*7V~Oa#vW(+6|Id5m8*P-Ss=dRF_~5d*-)`4ur!VByMYtrwJMEo{HhhKni8AKW z@3#3J%@u5-!YLY|U1++#_1I~3hwpiP!K27ky27MiVm^hF9S()9@xL_X7On1R+BBCJP7Pftc>XUm}3 zG6XjoH=uHT?;XScYdD>Eh&*>1Q2|}SRsk2Y_Wu6Zz^;+@M3@ih(}Un{f4QICe(zzG z_0&g@Qkoo|?7{?#s0~UvP<2r8@<&Hw>b(-&rS2b`68zr;jhk@}&8`4te3C48>dt;A z97WSC76(s}wuQ>YiIQ!VFgss0zlon5oA!xxL`QEYrF3*((TZ=}o^zppvExhVL5H(L zaybcqS#mBa^{x#QT>aMkcQZT){`cmWS5z2x)}8u+HK^M|k9icX-@kQg-6AEtyyU#z z+?^Hf`JvCE_^e=LZ@85#ZrGOi)@jY?D4!zkYxUQ1>jD8a9(a$)mEg}Eo?pd%kU_u8 zMJBQoJR@H5pG1 zJ8!}G*}@Mjh6}fCr@ys+R5BXf{p$jXuiY{@M+vs;!;+H)Wn&qeRe`e!yJU1?LPbgK z_<&MS@Lq_7@B8<+B!GmZ-?C$XkZkkIzkW6Am=XD+QxgHrpkY*Gq%Y+70Y-&`39F+}T>BtZkSoFBG9HV=0Y2hT^!DA2QD--@38ssurA2W$yxQYK zV*{}pQDPhdWR+8a$^QNOEaaFXC)_tVixeumJ`e2*v!SCiMF4!)gk|_`f&!d0D&bT?J8~lBYEhk$}8yqI$18@bokG)R~g&ufCI zS->iIngx-~kP{wYF!|W1;D!uAg3ic5*4m zBc-F%Ut3;pR4+2F9hr+=u!>W*8nzbr%SmIGY_NCm3b>R6e}7MS{vD=fU}SXDg4B== zUPl;CF2Z~j>I^w5Dk>PaX##uMOO7vF3jff-p8mxjn<@hC5pUseQuuL~3ZQ2-A})vhK9a72A`aiPL@DQr*XTS3&vzO*(9VdBn+grmf>JB?` z@*gnNifWCU!PJqh?#pOL!+$9gm=z@XsO*mcEi-|c{|Nz zLigkghM*0XQ&t{2S5&7%4{a&fL;$=NP+pFWgM`Ij zyB31H&B(A3ritxiodJQ6n;{7<7jB5p<;Ra7A5l{JX_X7>4>W~=<}W> zn%)oX+m?je4Z@ z_qIwE^y&Ya+48L4XYtvwJ3W-)`08ETzvD&DGj8rm3M;Il$|+G3b{qY-gm2S;^TC;+B#^)qp^Nfm0wZ zFu2P5N-!L-(xu1rNl&*#C<4;0(}Ik2MwAP5mOWwZyI^d_i* zz)V>e{7m0sW=yC5O-$}^rdyEOU`;`Y({q>~*&YXNR8XdjL|#Y#KC#cvOOFHp3itJ1 zA@d+MmaLM>`PhTRIDoKBO~1M*cHl&*>26^I7kbpw05yWrz_^Hmrf%^zJO&v#1`NPX z#}C(|G|F?H=X~=={qKra0NnA}`@Oysv<<;mZ#{M~Xv2#BRQKWO*`!_4beB|>%_Ser zdOCPAN(5+0Z`8Uay}NwL^0lO?X)vKI7Y10nn{9iirIq+N7Q-#!zNt-j5BP%DT-DRV zQ!z$trz9W)khi7=@T{OC8||O@X-^#DQw9ff(Zh2dvgKMnh#jVz?0?>*YC2`~%J0Y5 z7wqOeEJ7)}*T~6}Y3+(y+t_R`#9$EC4+=h5tQVMY89?`jll;Dh7svup9lgDaUBd;| zlO>^n*+%X6ds5hAq4E6kdFfRVP&XIIJR1vEwSEsvh+@qfV!^jCl8cxUp%=l*;bB0#a!!*0MU3gLc;%xdCuOk_#CR*UvE_;vC`}+2Zd?r#G=np}?TTyA6z( zfGk)B&+D20eY1@hMz=ADf`Fpb5EB{M4Ag>8<|(09FWwnjrtWaaX5IRullWQsIk{wX z*S>NdmGf?xPM&&~F;Qw%7Ee|BC?3C)!@J|ne!ZW!QJ?T<;Q~Qj6b)tTr6>HRbCpRqvQ@WpD z%ZL5x(vtPNJr1PvGn&z53BOo_;YJVNp+w8j!!}yozg9QCJkU-Rnb}z?gUZ{DEE3tYsUC+$znlKYDBSs^Ou$m zktC|el2b}sl5Y@87hTWd`jguDUzR-Lx7;+xMwM}6Ok;Z1gp%o|8zf8RgOnEL`JE~} z?iJGGJx=C}EJ=GaT4Ey%CoMbv-3@-MMl}o<6lc4Tkobh<@YM60X2y5-?=Bf1*(mR^ zvu0@!?rbP0lBrOP@~wGL*AvM5-`_+APejmKpIR@{V_`?a!sC5&b};M4!^88rOHFBD zabZCb{8?mlYCPX}Mp7#(-MY!KG-3Bu35XBZqGiOzVMv}#N8Oq)1+kcn#cDb@G!iu-_+rHF$A$wvyJ1r>OOW5bdheHMEcB-&h%|ZX zv(ihC1$!RX|^Do1Y zkXrB&g%>!{e}h+nyO5hMuwg#~X%O+{+_A?-=*XjqX--?jNU##KHqZ||jyKKDzmvVP zd2u6lYuF9fvBrmqR7A=mK6fzEO&3eXR;b5pI$RV`w41XHgGJ9(lB}Iq4llwcdx8tb zqq&IZ zg*t3BT(7E#FG<7|d?bs*={5VWO1?_|gFZZOwZ|llq3eJP)nsY z=lu&}k!>dOgPqNe_n%SF<^LZS05w*+_j&uugluizi!2nGEQ09dNu=}@ANfE&DfO*2 zZz3GO)r?AcIOAGq-WdL>Q>&AmMydw}wzPEj(+8L65tQ~jqjc%TwXD@1MD$v@ZltZP zgkYKRHJMCW-JL1(PfL`apSZJC{zz_jYlW5P*_liNDpwZp*y?uM)>R9&qszbmZua86 zUcg75z5qzUxpT*O>3ka;X_&#{%tS3i;vO7fkZ~U`Cn27xUeMXu+X@c~9wPiPz>2D> zMMXv6YYP!VVZdq1%d;?d=Ie=)p{GcemxMkOF+qI~4-A`H?h#0}VCr9L&>Y*7z#a=D zWGD0KP;e(VFtd`?%F4`qwbY{(Do(s?Y5lOVc|ygbnvBmk#mqu|IF?Y`aStn!7Cn00 zC)uAuT3Qy0WZB`Qf}dF-7?2qpC-b_3l5_D|c3rNBe6Mj`{kk$COvy!VoR@kW+ky@~ zh*EmN)GQ*iOMfC-FwUW}SSIx>IUFee_sPf`O5UFMW6%|Ab6Uwaq}2ZhE}lF=wps~r z@=}>}#Mb-B%{d6WGW3}Bva2;^pNe|t*VeGo7!zW<^tMP9s+C`kY{w6IUBBfvrdsBh z`Gwk-V#=({62l>;Z?DZ| zvF#)mR$NKl@)8RZ@j zgR^08Ro8r*ThWY{sj32>gU8ZXHS{TO^vZ2V1`O{S@b|zhg`IC4@IA))xDY87S6Fvn<9H&-;ih7nl+mdhi^XnLzaTGU! zFo}DAPdyj;WEK{M(xhJgINP6A{xW1pDcUyq$wCSxz%T0IeMwkmavY1%fO5%$GlrhnrZ@&ELHT;54m zIoMl$Tk{@g30FPpeRMok`8S36(!PdPmV+2ULFJLI2drZolPrZArJ3gW8SU&~e%che!qjXmnKPW~&% z8MUPoRMQ63XxOSE4l;Mb$jl;A_1;Qkq=~^LX7A+G0vnUhxz07?rH5%9H#XR7gQMP+ zAGn)d?Y(J36N}D$fNR7Gl`@#a^X`51-oghG-H^QtermsFKWR%~U5i3rttFVn4jNvDQTe7TQYRZQuhG%W!O+$ChV zxKy29zpl!KBptBK@+nY)q355Vz!gi?hm6`43#nks40Im~TH24x_Pduj>ZubyK++%( zq+FC4`ND`HCAUMtQ%vij>{8OjA!%&7;RC-t!-XGP9fxCa%9`4HRt6M&^eF7o6K?Ug1$q)l5k%3tNyXE`Y+7tQ$AOr9Jei8ZVAzpdJ*ehe~Wjoyk z+i`U}Ig*Sg<6mCjyW-0zmp`{Y-PGs`k+|8YR1s+T%vU=lPHypn`F#Wj^Q~Jo;-f^r z^2tb=Qk&?3eJdSF4Xo@}j*jzQ8^W(Xx;dJuXy(rr7!j2&Z!Yy#-!FdD&1P?gT^;4{ zo;@IdhaR#s*1}?DzbOB=FJ3jMLT2togEm)~VUH|xxoNXI@o)_~-pN7`r|0FYVO@P) z+~K6^(nS?M(eu0*P8#n0BX*;o>%Uyx&i6~&Ub{uFpRVe_9$a#mQoC*?F&~{A7BXNy zv-plg>r=GvH_f0&JU)AS4qP^Wr0c&qK37pM#-!AC8B<z4|5OGSS_QvGM~ zg@)^KPEExZ?AD-7|G{br(aS`*t7SoU4+iAmL8BC(sZ#^Vwx5+UBsA$B-uYxg$ z!*@+Y&;eMYy97$5)o?mnO?XPEe9mLA-Mp&WkHvjb_=MrP^!F!d54U{jn~|b zN86W3cw0!uR2|o6M#=}gqRh9|qt=2-bM6&2W%b6jovr5^mhn;5#CEJeA)fJJJ#O#%FFS3FU4ah(@$!RO{Sho zyz|3CyTvAB{7{OPHNFS`Idxa}UHrQyQPI%`tcjl(Odod8;$X>8M%_VP_1;gJJniYT zfZ*!{SDT~>a|*#9so8pT-px|T$Y$)~3=CY1m8p^33H3VhDk82JNZt^Yp$ z@6U`0jHM-FC_nw*r%gVIL{R?sY7_XJ^fP!p+J9dGzeE0FW8|e0F(NUP@M-u`c$vI3 zb)S@J7ZuBkr)6)AuXR#9*82B>^!3i`5jP{mC6LdKU5~g$iAISog-ZaRy#*g&N#TBC zcmyx)`1gS{d>Dg#U3mDv&py?Pe+P?#R1!Mj+c+68JDk0WW49r!y*lwt9Mj@S9~@Mc zxo5(lT^i9^Zi=;r)3VQaR>nEYZOvp%RemnR@P@(=bQ8_3!^!?>%6zOD{S$I0TQ-5R zoTJ2@ax*nQ)jPgAlnkujiRU!9xy^KW2QrZ#@dQ?^b8A*Dp^!kZj7s--?n4PuReK;j zDE-WIe=&EDyC%PL?zNdMrNfd0#Xx1^zTvrn*awb<1~54t0E)@$&8g{j#G1m)JtigVJ z?n6&ex2^(Q@v(Sw+C>9RV>5Z0)8q@<*{v$IDB<2oCiBE!511Yv^S7{aE< z9;nfgcRau!4=h+K$l1y;LGU|k>)_xBgIx2IcJfdE>q|uP;MWu3I)Qr4zXNP2p~UYPx_QzUqPlOMoR`}4lcETn zGrS>kUiLhqDq6@kC|}&L^V294eVm1TZTOiYAMY+pzB?*Q`L?R*#RVyDr%!ZnPh(w> z(_vnactLBnSZwo`ua~VC3M1sVLzg-NR9a|K%ziq)%+qKlpu)nCkcf;GzwthTi*wvj z&Lza@<^8YxiC&@QtIHqnT6D;gNIXz0pu4I7zaKV;U~3B|6*nR8W_#g%`o8dmcVipK zY7IkTx{9i+y54^|+ro%sP0Ao23AAM0-p#FFjJ@~{(d{jEC!gnPJR-Sh zhN4S3vv+2ewkG^ZsmX}$0hRmBM$F#iy*utUOh-xiC;J|@SVyWdG^U4%5^CH3v(*64 zj@KKbB49X6z~=gBzP%pNfpXyny(ekdTnb`tm>aPX4$tqKiS>IqTA)$L^m5G^b|shX z-&=1i&_;%a>{WrqjT00*w>I}5>f7Xt9J%^AD*pHD*gS8pH%$9w@ZsY5k6C9+i#={? zUSp};Pm~*FBgq*17|qb_>9s&4%=mN8^}j}}ph=C`qWJ@a(- z)*Ttn$t}a*CfQdUvANpM2?mec(yh3cQ^Dl~58~(pmisV=>v*F@tT!-0Ir=d)v<;!t z)=lcr*8=7abn=o_P>z({wYSePpQySQ-Pl>p1MMFPpVLe##bZbM&AW*TmpyQvS0_H| z&u=-A{M=s?aAeAdQIOjbUFZ?|3CvS`W^#2&SpO&YepKb#H%znY_XE}~Q=?49gOnAl zE(om=+Dt^-Yct;Zld_Cetw60+QN3*pYM)QR-N9T0C%FvJP^i?)Y42nv!&Qg*Y&s;FpH%welD z>jvid6R~^9FV(*!GgQ%m%QI4VZ#y)Db^vF7b+f(M{kxjCvTYVtqQl+97YW2g8W=gD zN@!&*SD(-dW+mj#)rt8?!yfUZ6A{FFUJQphng0ol<2tlgkJl%`2JUWY6nDhOJJ?v1CZ(w_3e;Xz$@O0D;%KD1xG@H3m{{-HLA zdxPs?)MU@TPHkFF03el#F$WaOkkqw08^A*-(C=KOKki~T1w??M8UWv=t z!2ng}fVw)*4d<{8xzt?Ui)f(ELqm>a^&t-Mpv(Xtb3~+n;VC$<7N})!PLiO^c(f$M ze-AH-_6d85Q>%>fV9{KM<;a)LgHDzInrBx{dUy=B^Ru&!fmE`q!iGF$4D%lYISCJ_ zvKVxC>CjE(a;g+=-$+j}@|d%=jOKTzj|lk||9G^*3MCCWMS zb>C(6Z|j~$pP+|PIF7XiH6xk_6Ks|zx7e$D|MyMub=z2Hd_(uTU0q{DmaMdFtwg=M zfj}n~7Iuw0X9&nTjQaolNuHS@gviea;Lva4bO?4BO-*1I(KLnldkT9CfuRcGKSnc_ zCEE#F8n8Z4zm#9hb~t(|@Pm1`A^fFhP%YZQPL9g3MAY)a=ichR=7?9+yMYPG=UxEk z-)szjKT_E0*w2yvMb2OF`6n{E_<&I>{K>Zgf^EBxVCsm>)VUTvC`jbxlGhF^UL$yq0?8N_4-NgnqM#k#N$;f z#l7oQFng_7Y-vMCpPZ{)h$1gfPx_&@oM<1l&u}i=!d#|ul(<(e)2#P5kIAyW5$(|a zQnz6Iai{6Q85UHsS`4_aHw1sAl9Jx_tU{mVOu80;YQjdq`bRD&*%4>bl56gj$Q?Hm z@kyrI6Wk|PCqCPuHrd`mQvxG(s6typ0+wKSb8$X>wbUFIvzy8+yewU1yebLBHj##g zhAke>Fybft-}3?eM}nldJwJFKAzu807_sFBe@6s5W4ulsLNL*B!3kdsFZN#LHo*%>$2rv-P3_3Dil$kMWCzFc2a?;gJ3HB&O<$HPUD060w zxI4H#r6GsR@vOnOJdz!w+mI`bu8)S5=6$lXw?1>7`rn16fa#`(@?mC7;OIW;=v388ddMC;B$@ud+n(d#Sgw zLlsyyJ)3?mPKrf(g#=cseKDMv(%jqI$MkD|7a57Yt>A^0%!KOE^)?>@i0^yQXJMD0 z$v^(_`M$cL#plLa)SGShye7{1bNW(R!-wl&$bG{p(IhFQvA~CB`EOXv{$g2{YqbYU`H1n-w(f_J zzR$}+*okpdr}vx^o_Sv6@0BF@aV-_v8l5g250A!Cq5E@atS%7vs4Dw}q*2DMV(S`q zu5CZ?YZrOl$Ea>M`b_NanE>7pmY&NEjAm=L^!j*t_N*rg{uMN796{Y{PPS$oL}QT^ zVfE79gXyo?QK~j%3rfr%(&o~_&V+!XM*eNN`D4M_SI*9h(&HaQ!Gz0fZWM)N0RDAi;PU_W{#@5#Nk|GgQ zbAxkZ;U$SJ2b9LfThvVrxC5kV=C*$_HDl$)AI8>{&G+*Z{gIMnj)? z%kA%M5ywEiQ9rhM58i=n3V1uZx@5K9ED*_b$(A1LP7~xRrZY1x{x#{Yc|DI3K{=cI z58*h+S5Qy@7lE?QvL4WtY1O-P0x2yM27{W?5n}MJBN{8X8zA`Q7L*|VS?Lh~tq~&+ z-V$O#f{A$};En}_O*<-?=Ud(4<@b2HVAd=P?e6Zss~1@YmJ?NscJ+s#IW6kYCS;7#SV~OFS;F+q(mc0&t?KH^(m#F)*k!A@Fz(#L@=aQ}v)9j2qGQ z2mzWY`EBo0A1`AXgYb*1~a-*ib$gkyB? z@@rPMd@|%N_Hf-Vap&5Uv9u-R$3E82U|-?cPRq$FXOHL%4ylbyVQoGuJ@O~H-84_J zxTdCDq?;vPPVHnz7T{n0&L5zj)-i%#;v!BvDmq*Ryosqzo5sd{*{So^XS~k}{C?8p z`OnuT*z`HU)&@D zjK9hl5DLH+WZtAWc`gy)GU&9VzejhC@GQ~CWMxfWMS{??7>9PxPldK5v#WW018F&Hc9#QWlLTEzMO-KAc-L;R;20{*&NUH;Vguuu>D^}OACyo{3&%(eq2wH-j5>?e_cuYF_a1LnJ?=+!XPgY}s3Q`Ro zuQ}vdgYrkRPx z!TOxl`N0rh^QSkR;k!Ud%%LRLAl;mWJNLG<*ef24^&sZ&k3i&b ze{*7}&)(f^eQSvP}x!W7$Bobr82pMsdOkdBp z)DX3hhKW8Pm8`FIrX1C4GMv3qrtREurg%bw>hau4f~{&FpT5bMUNVPnMr?`qNH4Ti zcmF4~)BW|g46@dfWziIs3D8;dlx2rJb~76^M%Dyu!4ODcPR&!vL0T?bJRk)O1M?g3 zqTJg!(LaHF6K+BPCnP*Z$OieLi>!QLFF}^ja_#naB;5mq_*vxLIYpRkAQB^ZED<~-kMR0IfJXb|$zv|A81UqW!+-|9 zmC%c-q$yFoABLqfxuL0nq7lY~zX~KXcFyEB*;1q%dL+z#8k8TM9Q&QAO6EDG%TyBn ze@uOKSXXHmt%V{YA>Gp5-O|#HAl)gAq@dERbR*px7q29QoHOziHotUDNFQ=97;Ey(7KqtGJ;6Umm-LE`Dnn_ zk+T8Qa)_1^IXd+-=lwJ#KAFj=Eh`d&x8dsi=<@3C{2AH4GT!>w!W-<&s-{=I^b!i^ ztIw60qAZ)Vjb6fDGJmP^jb&!iW<4-KEbL;p4IL36%0#c9Lbc=pvgVHOkJnh|!AHyk z+#8>PIdYF}MN=;<1r{-x=~!&Awp6-puwLL88XEeZt^#y zEn3UW!tz%E!(X0v@-4v?Ph-h8xtP0p5=66C7 z^U#yxwQlERwl!gkGf^g49y(18ohs3x4)DAz$Z=NNtEB3An@<-_)G=Kud?kD&q%WL& z(r?>cEeWg`Vj-`H*u4TLvvL-ei?vu~C#RRv7y-1Y({>hMYXL$LyWk1pObbj+?Y#Va z6OLr~(4+w*GcGI)I0F1%6BG-;6?94I>}abG(g!^q9l}}=a;Kvx;`ieLs{o?t+P~k2 znN8J?26xth6z(>;7%Cu&R(h=_Tf!R6OehdQAkePBz7Q1+4G??+ATh98Z}N#;*>Oj~ zABH$!Hd4T-UF&hvSESiz*z#^{U4MJ08GF@!x7XYTI{~em$kk1;3mlyUgfA6rq4I;j zNtRsv4KBimH*aeG7OrF+98YH}Vwxfz6p}xFm%z8SY^-iz&>PbHqK8!@HaXq@v5-Ab zs(&q*Zi;5-_Kj^>jZxjfD{v;&3N^Ux^>B#qSzCkF1EqEQcYwAiskw}u93BtpxH#V4 z`cs#sYlf;?o0|2c^%CKd3lcahhB=*;mx;_E(H7q|GLWj%$Z@6#ACd&rm!HW1qX0=2 zXAYLAf*+sqIQWQ(!r@JUO8!i=R@w6^=M^~v0`8!#7CUknsC22q-M;oRM}-e87jmso z$oCnpqqK7vlvd#@+})<0;SHdxNWtGfV4Lf2f7+46WS_>D+0&Xs`_7yIuE%&OW_yV5 z3s4sB%wW$IpygPsT3gy4IQO?$SXu3mh&AVu^2AQG(IIF+i|q(#LO<&_4gUS=e_Vhk z^b)F7LFSpS2IEVfIk|o;Xp+bYK}N)n%l$r9GfPWOtKrA6_53*&qzOv_e*yW~|65|v zv$E1sQzMlCD&Wl9+O5xu;X zMmcpT_79{cFV>< zJqY8dP$feu^ZyZ($L7d%OkvT}zadg9nGhpi7zuJ5BRKUvk;I*f2A~%k&X1m>W`k{yZ#4 zuE96GSo7A&{?DBxHZSavR>j|hBkwQiWh!O+g@zmw(MMZU!3-8}W=6O^z18Rgq-e=H ztInBe!$lCOZwd)<#Pwy1Zhn)R!@pJ+g&}vcoc0HSIy=ZdJ0`W?%p`_=6-T?up&8vV;_(0Oq(pF zQyO`h@KA<#q}cw!7pa%|FSKp-GzMn|Kw*TQk2Ub8KkbYn z>H_^b4;8p8CLl(u_a#mNsxgn4bpF(@K%68H@!>=NvsL#i9~zEUiJ&UavAXYts{YUj zz2s?=Zfu9nwI70LJ)VEh*S+t2M}`8D;$Dwpn4DO}GBmBphHprzvgb`BVXHft)9^-7 z8!Tz-2~{(}9T&vIvs_j7BuGEPJxcR=#joOeUpApq^@EY~&>q+Q7Yb5w@qg$TN2*U} zSP+i#vNX0eP?fYG6MI3dZ0MwCsz6gJXcFz;oJ+h7L#M7pc7a=Ff##N&<@#k8#;#1M~lFFO5$OYqAG zyT7-rk=AGE`oCJ}ezO<9d~2PjGM}pk1}0%mpnRt;T*6G`EE&J)KIHn9n)ww#?&VYbp|5p2uHx zL1gIf^I_XiEw4r0oSTzOgi0$kl;kdg;b!D4%o(3+JRsv*kzHY@6A4Rbl;zXm$l2F{ z9xz~O_-wnL6BXwvL713))dB622CrT)xmXNY%$)2Ap~40?2ov`#Qj_h^)@^vkFzf7- zgE9zEp96#0EeDA~Ad!IWH_}AKWmtE2Cto#yPHbo6lb+{WbqJ{(cZ!}zvdMT;etQ@) z6=@&(&E^f#u;qR{a}ZYyyw_1b9cON_ONY;tk@mE)b76@VGk~_oTU|P9Z0=jeXq!J) zr*2$aqOrr2y8rqQzMwERg1!-J6#f|B1W^`}c7xgCS_0g0GOA<=S|!KrMXANbB`4Pd z%fa?vlXw8@Jw{dr0of=cp48BY2r9^6f(SbhkWdHMmIF-0{&xY?pq+=kg=4@8qBLb? z?;p1TK**C?1$_G@0}$D%VxXsA@FfxQc!vOAfGh&p#nAVZ8j7!(3LqRGJO+hR%mBf4 z%(3K4{e~*{>rB3Cb<05`uH+)!5*jy~AdZqQ?76db5>#9)Ee;Mt($gS*;ern;4EGBO z-;Va&FR8BPfPCB0+$8uJ>J+!Ey&z;C=b7PP{3?0|dYaau^wq)bZW^rXK8^054iQGD ze$6r%8Q7s~tT7%}6a$33*)9z)*Bd^ zs9-uY?%f56YK!YP{;pf2$+RQov);phTpRa$88zCWoC2kB^Y-qZA#}nJ${*E# z@kF#ML%@|R26P6El@rWa%nfOHR`6xiqSQ6AS1Z2n7RuRPvuO|t=haIU(r0Iz>Ad;! zJ-Mxb#cBJS)SU%cPVarjBvj8S7iYJXG&$urUogCtd9AwoNP^TcW6ZL&)B9={nRwDJ zZhB{wZ}(xA(QhAdi(Xflj2cpbRqMyd@onIGQ5gneJN5{`q;z#K zT^`gA0ggxX7ZFT-Q?O#REtOzi+Hb*mERkSt2M2r%n4^}($K)1g@N=B=TqA+%At=6~ zK@iG?#ST2fWie2;Y?z4#HB1wPMD635fyMfRr+k@`_6DR;y=FWvmz{2N=ev|5x?k=n zqr0yj`35Ho;+mLprD|g~BzOwbF6C-1kYPkXss(vMF8lBAz!rIX^7YMEp~B8@gUSdU zPbYBf>~Vj6V=cT{`(U6uCL%Igm!KjF|Lf#02RMH!*~A!G2+Z1B0*Al9$NXu2ghIuT zEm?3kJ>_ZOgRPP}YD4oyt8FRNRp)I<-F#E`QL;^?Gk>FBe_XAGqY8*>C@J`ekx&p0 z*AV9M0Y$5-l1b)u=b4)`;JXD>)qWk9rwuRD;S&P-ub5Oll;P8snR|aqOg)OX48YaF zGY064t4+Kh2z!AEnnMgWyYsCev4ha)HZhIfZM`@Kl)V)bG>O2ptFJ@8hZsI0u#qn{ zGh?wdT-aU7A+C3REB=byuVZXF{b-y>b$l~|hLyI67{qcif^>Al-H^v+=#Y>}aC4^}yQza%rVd{ZimR9K zLE@|a$bM8f+9)PIx)ADvxi&;8hrtkyux!u->7;ElunUJ25eG&Z9AN2*hzoKZrgXsC zgQ5I(B8&&1FzDzV8^a<3!gCWSE}O1^`Rpa&bO@6y7^b%x%^Ls?Q3VQA%l4oOt^;y# z0AC{}4dYd&GR;?Kwg_(||L4_a_m54%(^4CZCCHK8m)6$0@cKj-P7`I|zC{&(C=3CJ zt7<-0S%=F-pf%%7#0FOiD_TWc{*!0jVN-tvh%2l zyXEHl`R+X<%O)=5YbXX;14YgyA=mQxO7F1z&s|OX21Oyb4%8kJ`J8S&EY>O#Pb|A&)&bQkJU}lgGP8TVeDtoACf1*y9FL! z!~|1hxYwX|K&XAN%zCjoY}=h3`bJ&jLeB#qKHzfS!YO6;+oDB5-nuhqv`m*5+zC1_ z&~d9>dnhufKy(UWTrG5YI3~mZ-4-=;Y7j-o>TS_i`%@4TW@yDOc)P3nD_XXVuHH(4 zUUAY6kNvlf@j#drrhO*t3N0z<9$P(l2H)w6TSRJ;^(c9s{7KdObgLupum|-T%*`g7 z{RzG(3sZeT*jX_GB?xFoOvXX_7KC2=L2ee~j?75CsmvTccNptrS=)R4<`{7t zKT)2G6Rez|hk1%mv3V&PKCUf_*p;v9FEzB2MA)A~HSz#v-YRq+7$`_kzhnlpk_m|h z`}v8Iq>Dul^rWu+*851kJ?%LKwdHe34Y3^SzGg~LaE>j6>zMiS;!R2T1N}^E<%5tX zdhsh7aaA*~*%|%+e0{RhNq2eqbksa1dvM+!#WnE4>m^=G#%y3qFq!hW!vN!0C!1@# zO1?tcx3h=8cDHMaKua{mZ*-xp%IC8kKP;_n&eVoO;@fjSS&mgkD3c1lypV43edYP+ z;_v{MMXyovg7?>cp{JKuwaqvKM5sHcd*k__1}IvF`Hha4cAf6FnLn-WkJj~- z)EP(~zc=al=eZD&qt8daw$KbT+K5*L#RxqlbQNN_8&0&QesFTI%Z^}^Eyzy%UFYFK zKkhPP=*pnsd;Kx_{?vif@q+)F<DEIHaV5TfheZTM4m}aj^aC0Obl#kU9tTIB^W(0q-$O=jD_8ECx!Q?@6M=C2oF4 zxu0j-g@*cKF7YrMm1S10b=9$#-RAV}SedVPwl3FNL8kArn4YX+j<3h7QX8%N#&fR; z`cF=U6>zb5?r{zcH(4;IeRM%gm|b5yi>4^c;9O&&Kvphx&UruUM{07MIZX2fX)C8~ z&y6!%rCURz2mw?WKZ)~u5HV-pj+ghoXlTj)dtS$;?2oRD+*h9q)7AyoJh1~gb{csf z__-SzkZ`7u3V&HDRoioCBaGbq(G*-US>9$uXF1pCiAun62fo~29bH|)U>2DS>a!{p z2Eqv91E0K*dsJX}0tOLLI3PEEi6GAJbarU%d$MrPqCYJ%vx3#MGh%VtK-dX<33rS* z5C(g!wv*)rk}ON7lfh3ksr!^FI<0t|ME{%yjn35JGQ6S}aJxy}K_JAyAr~g6K^&<} zL%x3NtqMVn9jR3g7}543g$}Cqr>Oi-W9|PD!rW4W?e+s}X*!tc3H1^@Yi zJuqDWeC#X+0^}69>L8XwAn#I8P#~mzA%96>5XZxZY%8KKHhJ?Bq)Gp)Q8`kJbQ$9+ zf7E(59C%zcy+8wapNB}Fu+yon$FjMtM1`u-dkHg2o3e)J=WzKLdc zq+I#^`>JWb1E!&%8p-B(e??DEdWp=){M_8QzGoGb$jV>TEwT9Ac;5VZGyZ#~8E1jJ z$?;ov&IjhfU=ypi4_;iR7$3S)%Eq=ne1+x?g;7Y$``4PYiG&L~n_a6XFPJWE_|sEF z6?ZH%^ECSsV3q^WG7pwJN7StM@%~ST2CKP7f`ip28F-1R4DlCOu+!-lwcK{ev=SPiw zAgs$$K(SDu)b-JO#{i9*gu2vR_?t9O{#5Qt9M8>~i@YBqVv^5_f@P9Jvm5PtYY2qo zaHo^|g|PhEGad`l-V-Zi06I6f?h7f=zz<^rUfj!blrQ6--!kiWJBU zNHg<|xW@5r-WF*b`^#*mv-;;Rk=>a&qb1hR!f&5fKHAr(czh+mM^!n6x(Ne8nk_|4 zH2~~HFxCW%3In{3He~^;03j#nS-{-v+ZK@VLgS%g0fERh-@hQRTG4zKmX<~gH%y{0 zfw_JbtnQ+E%)b4pU(0Bl10A-fAjAy%LkRr)>(?hRxaPK>dIwdK>3+mIA4lwTKj3Yi zJ2~d1+MjRu85-gqrFXP5bHdIGqfUYjNkfKbQ-0;%Pd6rmeN1^gzpsY;&WC)7F|3>b z1#P-nAEN$w#BE6O<8>sJ{AQnQuH`t5HVR55J>%Hkk4hG%bDMj0&McL4tNqPT4iM-WQWT1-+vL`E;nOnxDc*R3s`p#pU7(vpSVtXqF zt21+r$1rk0yM73DAPoqPF0eMY4~hCJtEgZ^$Cj4b1wHh853?$SUCos&%&ei4VF!#6 zl$NFd+6_-+TKO~1^`pjG(KVb7{?;Nx)`)k*DruSw+wEpNvFYgp<0aNtWfB@6|eRa3vy#suUs@Y~@ zI|Ov*(46%fUa?Qw>vh2d<97n9_S1oPh zhAj6MJM5;bk-TH5s$AFAN@1w00DV#=Xe9dp5TFfJUL@jjEjaB}EKu?}FW%90zX+pb zoUo!(D@m@kVrRr-ZXfH3)Dix*>Mu-YtZu=yG}dOw+3-`+z;|6L!LK^ZPdNhwOy`vV zvKNcv760U{VOi=n{shUo*Fe$rz&^;zQMg3c;6^gZ_&M`Cq{^?vuIC6rT2EW z2uBaSgi4;!a?pUv-34uO$e?3<@ohoDJs6HawFJb4;q}Act^(kSEe7+GY#2!o@Y2FW zhU7jn@>fiR-yXCOchLz3kBM6Hw8~P1&=4xY2>5iXu3#S&{`Y=qKsK`&%0R$z;0U<} z`bKF&o-sSkKP922L>NDVD0X9`&>m2e63l_iB9KlRo~p1__~j0F@f&q+JnT2y-^qy? z?}vIjsnJ=ha;LG?6?YRdpP6u*cM!zwS;>BW_ug`OIFxgyyIT^2A?Jh5&BnjIAAh?6 z*%0ZD;|D$@1l<09D|n>D+MPi>ff}w9Jrljf^qXrHdxoyL^H*~nebcvdruz?(9;o7# z(5sa=xw4`Y4IW&cQ38DjMCQq%YGGhNfgcolrz;@jWdTo)6YPQ!?vqdr0nTa(ts!W3 zAS|DOr57lj(+XO&kj|VN&_zyO4~d+kmViblB5K$P2?-^EX#x0XSb&x=qRoJurV5Q3 zi(zv=)H;Zirv<}c@hlZ17fAPD@S@dnc|-&rsu%H4%cOw30VQ&jQO6=Rm44$Bo9Bd4 zo51rq@i?zM8H?}t^hPoE&1xLAzeA+t6Rj9U6OAJ?$yqj5QZrU#aT1=BbU48WpLHPc zV6=2}rhU$5kL`Y3V8M@)0|S+rfg~kX81FZgM%tlx|v{4(7fjj(u1P4#tiG2Kxul>SjBn|3k4CKv^ zamuO!yx9eHRK7dX6iO?;6_{+FoDDv^X&m4+@xkDU{vV|Aj;^1q8uv%l{1X2pV63F1~jItbA?1&)bMv+0_nFu>{;KWqDH&4E}@Tn1Crq?OT zwdLqxsPCxFCMuKvs-Whw^RGGgn{tG;af?vt;YgexQciA+w)`uk~IT>P%}roeaVjw$b)CB^`+ zd&afDmy}1=|K{qqMCFt`D_{vd&<2bqwZR}6Nd_t;Am!C-XsE&KrKF|~gyD_B ztG}ASWeNwZMIarG5#+F&{|OvQc&cF0Dp9epM8QS*=yU1}<|JAZ;^()S zmzQVk0ussF2*+v+VuXP-k;wVm^4n*Kr4X~wwb)(Ii0(3UXRGlOHTfeVWn)0tHM&>{ zd#0wZE?67)zOzC9Js1$G{uFqRbiNT>p`P#~RWr(v&jFoi2xFLSo~$L#-#nys`}V z7<)bAP=g+!>QH~aMG~*MS4Juu8je*&UH4fpE-IkE$*Q*|tv zb4E`;nb(}kOA+lTO{j^_zBm7~f`8Mezt8uyj(H2|(m#b%a|mDa*CrDotxs*t<_d1v zNW(F1_?^5jP16MnyY`X#FP|%~4u)VU83a}S?`dz(K-~*+Sz&U$zkX$cR3^K_oKW-W zDiQ29LYjc$4#C8LuRAm>Y*7jN9x&{{(gNPzj8c|LC1|5(_?}rKI8q?D1Xw&`6vGKl z5_i!_`?p|A06~R8&xTDtAn{tyRFZ=j!2&oqJGz3rkz}=YAr7H8sUA|-#k2Y1&e2KurmbO?KKrnd=gAe01#Wt zi55YqHUZ!bDf0~ib?2}ir2=AidlNLjozY}NK*pi}4-A3yx3sj>HVYz-2-+h|hZ;sG z4Htj+VA=hw$C7d9p${FJEGv-y``9Kph%M)*Wd7wDX&|+Y-OIG|beoKuLh1$FzM8k- z6)hRp7wRR6TA}S+@TJx?ZcOFk%}U~^qy|-UR`&!b7O@1Q=SLc^#&AE4iFmB>5+3bn z#&a-3Fd8)Pww}w3l^QeO5*m2jo?}5b21MSRJK2tF1lM%C`&5(JYi|Y8-&UJkg+(Y> zsN%x3ZL`7g1|z(;EKTu3GJNT#KW;_vi?)$|qn_}oLny4{K?sA{@@Sbi>s&HVe3pbE z!q@7TQI+=8;iW}_-vvDaa7PSTrl*sygM~l8j{WkLNc8ITGXImHHsP~7_r=&$RO!VI zkue+i#Mo8h;>zpG2HnDZ*tyloY^@p+bBp&C-_LfK^8abiD(yS}o@sbTx4eYVo`2MO6%L*qp z!hDdEWBsh=u+$8cjF#r~ek zB4K7Nl5kFocIhxKxvR^)_jQ~qHf;`>nm+VTW#Z*0ir!gV-zXJ6N!7oyVa3YT_Sbci zX0(C2ds zUu7}ye09+92~aK&g$MNS7YTZ`EOMph*t&9b)!6FVZf}<~X)C|HNDI9y%g-fWrkqb0 zz4~&g=J|(G6u~tue4OCPugy6tc0bOr?CXfg+pqJAQIK-(F1VzI=KnqSLJu@WT03O3pfWDv2~x8uT!5k z@|oThM|P5feiBnWv$NM_5=owXXE~=ZBqZ2Tmd-RnLnQ$`=Ez)jd7WT%k2h zn)bfcCYtmlAm5tlc}tAbY^Ibd4oCGL;JKdGa>Q~Z`L0PyUw9JpM5l*PY6d3#;mUD} zl;*>xpit56h{N4k`(oJN(ufF^QjzR% zBcA9kt|$EZFa_x17O}TXWFnQ|6RxOz>~g z`*IAtNIk9H*nXp?y+G*00V?NnI3k0n-$5vQ?|05^gs40mS=s~A=-`%DhwO}6Xnw*Us{ig&qJNnkuDwq7j;tUJxho7m~gO}f-Wwr%% zZQXFEMjI5CQgynwIQ{B#fTHO3*9BLKfRkelP2(TkVkg2D*+l~JOX~t?snL&^X{l4E z-IXbY$s;*2QSMgi^ruzE@9m%A1k|#PqWrr#;{_+iB=GMpf4_d%c1F0 zL~c7(0ERd+xv#QU0LCJ*_4I{veiT99@19t`U+H|FG0VATor7ruGoed8YH~5F~rj757$Xisq1V?xge=wkUkn*`P3S=paAzOjNJ<5s5ny zf_z?_#C901;BAc~9^yB&?O3_Gv7>!Emx|VDR&&cL!!Y>pxe5JG8s}EJp z_fkM~<18J@r7}86Lg{~T^>xCw3J+qSP*nykNOwQ`g-LgG2wAA67ady?Q zAG;NMXauQKJtT-*g0krly8tko+=WS8rN^F0zEY}EH0f;p<)@4p$4j^Njt(c9MgvwN z9y@RsYB3u4!CJr98P#|=+6PwB52aN3u*#p~;XZpf&hlVi)&XFAagUyWA zAg+{Zw{nqu?6Mw5oJZVqGW*oP58O|NpNGZ&3d&!v^g1MFC^b4bBAFsl3cwH@g3F0K zp@pfaiX&J3Xv*;AYpeD9*8PpPC^|jKX`zmM0pn_7fgi)_Hw;?wb}?VrMH`!SdV|g(bO%P~dHv zZL84@T}}Ri_M3@0If{BB_lpPuBE0Mto-272^Vze~;-l@$Tc=3p<%G$};9M)cWq;G3 z>#ByO*7H<>R+v4qCV-10c2qV}nVOy@bonz?%u>wLF!}ZmoEVC#*a+YX$hrV8F0uNZ zKB!icZn5CG?X!UDb?s<(>Yu@SUcL$ZsxMi~|oZvPwSy_58Y%t$xer0!UeXqfN zXQ8;HJR7LGOg`)8u=ml{l4DJXR)N(p`KE2->w=rnfk34V{yI?XB1Uqx@}Wft$%`~VeixjzMW)x z%Daz_H20LTaa@!*1m%w(F^|MO5xc(sU4Cdu!Rk4FYDdJlt>rdv#X_kY=GI_bBR{ZbAPO-_YK(KniZwSU+KEU&}@d2w<1?x&LwUM-txFz`}q~= zj0i7j3tse)|Kv^wQQ=BmE*;9lSm&a|B&jI&(35v^Q%fByh2(Y*-_}0DYY|pd-?f$Q zE%DhDa*UfjN5eo1#uUF0hz&*$cpYdJmTfesXso+Vnw^YFLu%uR{>yj&;f`jDJS-bc z>bX38rsssz5|VFRQq;cUwzqf|uOUzzGEw4&UhXhOsyOCx?vKvzu}r7?=>;WjjqO+v z0TwOd43V9hhbaRv*0jNlmJyFN&odmf}-Q`;J4}}ax*3)UBxNfPOSvwCDt{erfj9!P3$sajN_N+v}$Mm=LNDNOynW zr@VWY7WK&TTg7ef!UVa@bdzT|6VLChiNE-e@Z$;dxY7gB@S9e*))k$f(gt65$|PaM zIcGOw_nyw?CC3zH#wn7h6I!3~MTjAfdpQZd2++IfIK6A!*0n<$L0Y2QwHK>tH(Q(7 zTg(uEVQIhU5}kdA`GOP4#UACS+%d+0rd`2igH1-Q(7?kmDXrFn^|XO6R9cUkZHGnE3ZfeSInHhxuJe z7pKHwHgX|T&p-Q{sXs!m+Xz=0kBy5NOtzXVd}!(&nLD2|eW#6_f(LhJx%2L`7(FdT zu2>t(nPHivt5F+xuKd#Mv}h65HEi%U+Q1#Ko~~j4Ta6XU`+eX0{+){{+kQ#>*IrMA z)z~^#9%aV8HQ`uz)>?v872K#X{)66K*YRW)`6)Y1OzJzamwF8AXLlZ9-#$~pA6RSK zc)57&nbnD|v3PeTjjZtvUvIJSrm$Z3_N#lyf$*Gz8G91o# zC8!;3-ABn+T1Kxsmekaw;8I))=Qi92t0Cx}=)i@(*Kg7J`JM-H3aU*|HU%`2)|r(?&`8yD@L%pLQ7)OX!(!2;|&9^b#+YW+rc3xDoY)nWyDJ z>QLywS|{E^`27}a#{*f0z4IP$&(nUwOw5iV>yPl5~W>R78!Ik*hXqCK>J-iaU< zq%!{l#UA6L!S(`GInZ3`nxt^mGYzY&%3@RV`ebiFt4MzikF>EODv>T`qEB^VYcf^k;a%*esLEnzxu<~`gyf2pACT^2P!tC4 z1>Oqaid5&~5)va5eU>c}rM{?fySUg$1svy8R7V&Y?n+6GKN;tWQ^9A(rXWUnba;OM zr18!z-`yJ-kB!#i9?w(oSQUQ18>unvgPpOGiZkNhiP02Bp%E^KG z;Kg)Fb*{jDZdz?VKQ!RtE{hiF@I$QE0Ki2H?moykC z`}>I%AFK&6iNf56t6Jl8<211bK1?T{%;YHOe||*XUT^;Kr%sFYgALxjmYCR>&)F+0 zTt%7kN){A7`HxY=Ok~;SY4LXtTz=ma#^@65TTa|{5qOAvll*H}pp-;(iIz-#$H4n< zYDc6(0h1e2$a_CkH3OmEz&z4)60n?A;KA8>oT4;7!h9*sz^OF6zDjh;FQDhm4KHgR zW-kb}cu8orr(TTP(N7O#`ub#Ad1(%9ieQc8Ebe;{sI#T__S)E#YMF@nF42@~*Wjls z1tV@}`{~nYXm2}5)zm#VYRz4D-0vAwBp&D+t76jRgyQAn?wYh}EdF&;*b$C;Po_X2 zE@67wS@$$jKARe4J=u*7UokB-C&hh)T@M1 z*cr>(3-`Y*u)(JjT+DzS;T7?^F2;%wW$;U~9o?K5m!>Mg+<{KBuQ#*4z^``J5r7ZD zXYwzi8Bo6vVSQ;!{j=}*p!c|BWH>}cHF?_oR_GVLw^H1L$S3f1IV0sOeu#=Z4im@bNa5xP7U$}bO% z=bnX!?jrg0U)s={IuP8V%e7p|nk3@G5`OM3r8?iiEkDc^88SfK@O!F|q=$N(&Pp=i zV!R(;IOM*|vqy+Wy14QYA@e>WGa93Jv5ILKRkOCa-gij zxp>5&OsuP`@Bg$~Z_0#qN8L~I_DS|VLbsuq>Uew#o<6YRh>Zz07=+;l!_ zetH79c&rPq0zloT!QVA4VOO_eHeaagyB{PWNi?>^|A%&5Y>aD_$v`e}x_?l(Aryx) zlSJ;~k>Nx}5%cSJM@UT?KRY2HsF+oe8?RRAVr5$;eV@QZ%M}w7N9a4Qf z%P<#(5DO<#&OHw6*3{3lqyZRJts8%&|Sle`IMV0Nh5lVVJ`znWxAZK z)-A!lBTPk2ji6(npmJsb%rcP37P`JT0Ocw5I(yPnSSuA2QWiWno~#6R2nUyj}0Nj^LF{d>9ZFLyR6;5oAjKu4J3dY_%EIXo76uJ}lBB-Py*I+E{iG6+85YEl|X5=C7L$|vE|_wo{u2u3qM zDXePz+v$AhMRKjgM{LpA@9A@Lm09Jr)qi@Xxk-9&GkJu-q@bJs?`;Jl+F*39fuXg(i3Y39Vz=)Hil^;xDVlCXWe(S{?d{Q&+y2V% z{&7f=8>roIZA(#{j%!h_+dE}1q4FATl!oSi@5`9E|6<4x`Yz0>hKt7w0*ND2gpY|) zA1yd*u^+8p_4{2q#HB)=q?ZKoCXhCqPDrQSFD52UQp+hKxyL=m{UC zlk{zx9JCw*sftMzY^0{_ECyp0E{(To@`PSokBKKRzhjG&M;amDYGr$)f}hD6P%shQ zx74wIH1Fu;$VaR(`F-BC9z$GjhLE?g?_=~~`+6Z&N-?iWQog0(>CJ3{zjr&2Ne8$S zYu0`t?7Kmng3_PVFD%g@O#&!91ulH)3XDk|yT(Hwl8uhOP0Jjp^a6~Jf9lpn8}Uq7LH z(mof~cH+Xaz)|qN@|SYJPo05~uCq$3ziVl9EE330d(M3Nd}%Dc%y9J@djhhHsc5Mm zK}usIK)v97V}$HbmguP6w&Ls*HoGo4#s%SY(q^0~^QQY;$e1;GiVc@DpUzU{Dcgv` zU98x%s4V>)o(sfg4!Y>YJCNuY*<029d|obCzi@_TW^SQoS_|ID78Aa;QSR3lJV9oZ zA19deW(;I&HdyMzf`9y#8RLR<)sZ>p9sNSkJ=>bF%4Gk6>F>F`2mcDfDru;8(quA7 zE+F*+QF3Sp`5EBQGMH^cWg?%e5#jPDQc z8xRP%;kt=rMyr?d5JBYhZdmQrI^1!hwy%q{DyEORecv}q6BmEYiOs(LTUo^z+(J@Ku(i>Tg-oW))H-g)X)QOk&=QK&9pYo)a_?d(>9LWixo-RpC7m-1VqAc zC-{6y-%W?(y3Fqy`}m4hEuPtyo3}-HzkwSw74x4O&JlkK3Bdes*%UPWX&r}7 zrq&uQq&W?!#(z?fu(j|oof?bZ)&FW7>{jx>5n1!!H_SCKU^&+`MsTog#>DafbLB{m zhw7wWr0izOfHVwumclNuM@bv~C$HBe{(GjL`QWpW$GYVBhggc4?^}5Xm-uYcCHtN# zSa#~iTBVJLaOMTJX*1*SHNIT+t4pJhXCFyfG3eb@QvMLg4TjI`=5sz#nS5_%7anVw zg7*E=J&r%Z)a&sAT`f|sMqg~z_LOVaQw{MeSH zww@F<3IK&T{4P;~+zIb+qpTCSsc6J=B1UHqwo_Xl>?mc=Xo&@kn=Wm9h%V=6Mt-LC z2!No%%px}5-(>V2Fnd7Ujw++6`SYidCDhm`Sc(I^i}D*{jlQfSgl6AOy{=_p;Ng;A zM47tfsv_fjJv%D~Ba;@Rm;alQF%)e!Hx3;F0)=KbZ1v_Oc~!;xm@KsCO$X|NG3V;jY-iq9m3LN&erCAq!}pLz{Hz5lAd`}F(~32=C?(+`d|Eo)GY$#t)o`D! zaxGW0iWx+oJ;pEs@8!lS8=QuhJBV@EkLRo}DjH*!wbtnp2~{-$)h}9aWX9VsC7eCW zVl8?6y!%;4Ki*w-&sH0ux##~b;UMk0HGPENs)!yMXTgK;(^+^`(v7Fh^3%xQ!L+vM?s-n0Yy0cDjaz$rl!v(VN)cc$QDmUTVJw$ zuNbTU)Pf*KKy;9PJ=wFcxYy5RbBY}l1?26OaN9D-#Di@sI?Xs#b#?n->BDoS76-eG zHLFK|AXGkl4O?+cTemU({}uU@=J`hLZH=E7O~d*a{@oxxGA?_SM9aG~L4C$FDM8{d zn(UhS>m$e9x?6&(nu1Kf6Y;ArI3nA+A!$WN!tIoPpO<9ykn<0F*0~GDL}6+9YM}KP z9SdV#@s||s(T#}=1$_R0wx7cY2L#H$v!%H^mXnt-6IEEk>NjSyXrjs_F70 z4%I~bc!3TkN*&)qderH7pQ-3*N-z9a zmi=}t|N5PbD^!n@n^&>4chR$}i&OMdk4zh>!>x4XkA?**2qH^;zrA^kNtzgWCHx~M zF|~u%wnYLFa~6+>a}NHxHh;nVm)v{W7joHx<&T}ec$aT=y(S`)6kGWBtm7*h4K_)C z|Ee);0$Tz;QTEx4z)I=(j&I#P>V<5n^d*Il6ZA$uv+tDZ$-)TtUqHhnglvZ?!NJMl zVYwpbb=YvbWI(!>C_^Cx zXwC5YL&=|@cJZ_Bx_wmY;FMn)jjWe@a<>1_a&@;gOBvCq3$FxM7EI`e)i6AGGu6tI)#P(jlKXLEu(yBJ_CcY8%aCakE3Nf zWTe}WDKzSt`mfG#5RYh^;7!l!>W%x25GP(UBIEfJK;UAkIZR}gUh0r5rA}q=+LGEu2ba&GA2xF zEy{Fz;Tb<}T1jM59EQ)e{()D1n8kS!|vsPx6~T$2e;Lca(Z+nt@eAs9#^l8?im%(tl2v>O-wf ztd-qCu;{UX-uC;QAGf6h8~#@t`mW7FlhrTJDnBzxHe~xU2F+gZJ=f03t`1XGFHkj5 zRkHTvucD%74{a%bu9yS6rvGlP3Zf#zcb(;)taQD)MM`wWwBVSet!iKXMAhi9=9Wby znar#}eZ=-Ys!NN(x2t`Lf2Df!OH9fJ;roQS=Bp-bNqj5wtrB{(S17)rs{^b{z@$Ww z6Rj_4*#73|!2ArR!8{X47p?;@WV`iRMXl(>9(?RVv*`U+=U6}U?}ev5D5CFC@Hq0F zQQ|C6ul{)2TTKR&M8wcqVVyUs?~h*ZGzRhs5Dnr42KvvO?*k*lYKy4W{AriW$TvdT zzFvIMS2VQp@`a_v6bXnl*>cA4;(6`A3%e+u55S3uMvFmH)epOfB(@A~H7!*!zS2E$ zv6~-h6&|zl2CM34SnLJG+cEx-g^X_6nUY8_{Z}@{iqM*XA761VKwJ#EJ62F*gxN^i zXuh%S>M!0{s6bWL3n{)W9q;OND7%)qQ=f5gjtC8+o2 z4*%XB{zljp9+o8JPWr#+qYL}?P3Pmf^OE9!xAiyTwxSZ947bIl4)hx~hG?s)TCc{b zj^(SdS)fM#88_{Icv}aA8Wp>IslKhaa31t^pl!udHR*NDL9b|PnoO0Sf zV5Q7<(vrXx<%*{FOYLbM^_L9zMm~Y-uyl6!#Z$(*H@Gz4tSgq+gTL%`bhi0iUPk|S zeh=$rTuEljhq_j3c4034%z)+>j>D4czyXVCFUitV#it8)yW?J!x{Um0^d_y76$^j8 zLnrn|(NMpv`a&nUTP2QSp!anTix#S}Q0^s}h~=v*cf_ZN?8J_kY_i*5guLg|XGv(P z3PU^{4aP3#CnJHSAYtY3d_~X3Hh(JoM<;*g>m&g^O0L_+$=EpLD|>zAew25uf6V55 z;(|z?Sg1iK7vZ%&`#m2e-wUT6cr# z=S6OO+$=-?_o?tVy z`aS#SjMH`|e^$%$Yu0knJ6uGJBJ-0(=TZ-^%F^yM{T^x{0J%yjty47{$o6c%B~@1j z?O()KFAbbfhJRQnBr^*elUhoyd{p$x)DZjy2V6{Fb)O9;tykoSo^`F()HccX+p#AC!t+Bdwe7!iSQrpvza_YtnhF zJF;6X{BFshLiDxr{04`2rh2+g3!ADUng^G4Qrash@voR6J^V_8Gmm3|pjT+S#P)6D zKR%FW_&_LYH?JcjU=HReuvDlV@PfNX21Xo52C?3^X_hiW*s&RKHT^@eJ?zH<>Pwpr zXj=9-iwsS#q>w5+rirz*?zwMJiP~3>(9NwH_g!Nc)k39n{texF>9DVgZcBlAP^j{` z#O=Unda0=sBci8czsXId-i#b=*$pkZxFLK$c1bvxI%t}`kU~&sb&&o*?`Y<7^=rW8 zO-?F{srVCLVxkYeaA!eOymFldCG?vGj?2eDGYnYx410}dI4ax8m&e^7X=w0syh1IB zWV`op=rs>1ZuBu>2#A-9xY!s@wbY*+PHHq1Z_$npQqpGtJNBE+hd-V1+H2-n%<>40$fZGG#`Jm?ZcfP<~5TO1jsH;m9 znoeh&wV=+BoZ~+Q@z2-=W-H>Lfoiyhi1JJ!LU@?DAAfecEG9rE^rlQC`qKcxlHgYz|@hYeyo+nHMJs9C1qVL|%$yGFx) z=RdGLybGn4&j&aAT}CyS5`t;yi8WRtii&1;{KU-#0$~a}EG5fdy4_jBV36H;y5>Nh zl8%k(#`byXR{lMZ)(aqzNC5bs(uhnThh?*mlt>m2I|qJ_eh<})X){QWS9LailEI%4Jikc+4ZOmA zVXmzw9NPvsj{q7NAqY*P20g@RBJn%MB2lF2tB@h^N;P8u|0h zMY1iEKnqaRVSl`{YpdIQezd{d9M)=%AKR&#FZnY5A%(>s;Z5(;`f^HRiPnP-CH?F^ zOWpgLeyS=ZbowRq;Upf4ADJPPScvc4j`R>if@fP%KEt=^3v#`uj5sXm z{W@U8PtOORL7h^Ow)whd1mGUy{0U)$`jqW}1XXdI`gh_N5rUzjNrLFNp4DIDRVbzq zQ=yo~BL-N_Q(Mc+_lIHI$fw7WG9jHtWhb*D&zT@cLgtgc#lfq};^m(eUCKLcRz+)Z zRo`A{xA<2~iB#@pN@ic+hdkl)*7+V(u_>=5*A^jQNr?^itWRR2tw-ub{zbl?!iO`8}?EcBFi1ozBIXU*VoTq~o<%5I8H=(pEVx`&k;>N(gRE!ry$J z*vadj&$LNupR%LoJl+P4nV&x~@Hp0<+%Adxe7p|=-1&?kT~w@tcWPAV?Ov^6d!O)# zdNd+>R?|H1Tpjio{9qekKm+i^+<5^t34+ux5TM%VNq!IRs#JLb6KJNX@SU5qx+POT z8dmuwyFoBRLgAjlh7;x`_5Y$SM-Q-X8S)_~oJh6K8R1!qyUz!vPlbSq#WfVVxpCDD=vVFCa@fMV zl~osX%+ba5iwFNWsVW~lhdUL5;wBbZr%l$|MYzPX( z_M3JA%F134iOXt03!pHJ7F#Z)P+B`br!SP#)HjrDV>K&;pc3*6(=q=&E7T~DD&|-W zeLTmP@GyM!N9|tBKAm=DocA#DTU=xeb5vq3$vf9GTy|Rwg`UuC-Nkxdmpjr-ip+Az z!?ZBCmb6~Z@D&9K_eHMgh<=}>hDGJDhN9%GbviAF0p*ISdn0>3x2Qw{`$5p7yV=P&e#um_3}mF27{B`<=!G) z98ZY4+k#-_twh4%+MkcSHV=HfSEu&8Kn1h$?tC6p>t3MmMOA;TRT6@BQL>tmoN8OY zWa(-r)}I1!Sj;YR$vCz|vk<>@Xk;gF;p-W@LaJ?YLwhVxIt9i-`HS;oiO+g4K~s=d6SuUa*GI}eX|EU_$#=2ex1Ntp%9Om{>~RV@-%4T3gG@91-EMdp;G1c< zmY&Z&6y?*-H+`NVvS>7LdBa<1Mexd7_A6VLdR@L2?Y7Fow`U*U0lq@)BnPwGu1+?i zD?z~cWAxQi*AwJrM0WXJS`z%BRfe@or!xt!ry6RR#K6Z8*5NhZn-u?IVQv-sJ{gSC zl$x8PmYfzkz@iKulg>Y2B8Szkq~EQ%W6(8qG3)M5o8tz}7O=Ctqy0EO*U*+^;XTLjL{iEik-RWTTk}hCSnP**-1HozrVuJ!3ZR#YiD!`noe$ zlVk2FhHuEObG?ZaSiOD;83;E^X}OXUOndWROhzdps5CedIsx?&?|Cc{*hN7z;fTl9vEDMM#{o$dfSfpuNxnO@#~!NtjzLA{9uU)+xnQZ zgvbMn4~7|n_cm@a_XLr>ou{UlE=GSp5xcCt0;$`f?JJ3TSUi42mFcH%D2FG{%(kddL z7dYyK0Isat`M&;@`h$>*U%Y3{a(9+#n97JXzP><$xFtxhLEG=Li(P_rlZl1KS;xlg znG&E}1Ym6;9+(tpX+OY`+?`U7id(*(k%VxsSASqp#u!|$94Sz+pR2a$E3j?`e-ZS> z{i|qa8Mej5vE%Jq?T+@U%4P7{tvy?=nzNB@$?$fYL{2srns=9V#b(Q>P0~F;uMk*@ z(2trmSrJy#h$k6o-5PO$9R@H=6wSUM7K@<{Q&_s)adU5RVVQ^QzQIC#6#}MtS*jIv z7ehSY4cl&9pKb{OO*0^tD;9Vn1*CJSsHt^H0&j0`tu)=<>xcoc01_(7(OsQ zskQ_}U1=;FmeaD2AfKx^PfK=tG5eZ1p}Ycod60&UOmaSI5TgQO_3EC3|DtQqdlF`S1Tb-RKEle9OlK%La+5XWE?gd_uY9MH%fbvye*ynad#TCYU`oeU<|PFc?&AQMyS_23D!!1^|C z*t4Fi>vr`->&}x67TDv1wj>`qI3@GFWvM!Slw)l#FKuD@dzMVi71K^^NRMz#lU zK>Bv1@Vrue?DdvooqR8uLCved)k5F}uT?nf99Pmj{}^(IWEQ!#Bq960xBmbR z)>OGPi@%cxA~|wZRaIyr8-YYvG)av$sS6K%&d_}d42J)WI+Vw->W z{YUf0vc}%#swm6K*3MA9!3}(w`%C8^@j!77g89G1Ox0!K;~t*F{hJTaha|WC+UYng z_Lc*VND@z2rt(TkrvlRc@Nf6Ug43CR5@07uh6q?F*f5ew_U~C;S)bO<$PAW{%(V6O zABcZ=YCWL+AwKuh#<{Z(O3-ro=JAvLV=7`G;T98h%Oh!WhHh088TcD9mkmEyC_1-~ zj^qYT!pl9PLpgkfzUU9Ci|3U?h*c;zDUvmIi1&U`!AN5pXN>3V3*rbL*wxxc^Ym0p z@v`9Mu6h!ZhpsK|@Z#Jcxo5j_r9eA!iFe8#@m|fV5s1BpsZ-bFDHYMQObte1!Iltv z_OeUgAQcR@8waU#IiCSzI@aB`nGLiwXRPC6q_~T|0xW#wq!x!CKXqPRculc-e{I5#rvNO58 zw%#eyC~xGwV0{0ul@pYw8F@q>AWvV;Y#w3VIlp4wQw87lYxao3VeZ15`_kcCGgPnM!(3It1^cvB?=#nf^i`9A%V@1rX}v7Pft9*^3(yy ztCTQkcTWV6WZMHyRo-aK-$TU4W;ZTk-i7ESk4R-twYJDb3WbiEn|nNq-9p)V-VTWtQ!@RQ)Y1BQsn`M&k*gT;X-PK z$1?%!+g2bAbp@rR=uKJ?=oh}Zxl{{Q98RyXvD2q_UbUpcPQ*itszP?UQh|%@kpO`4 zf9C!w{m!yERA#jrYhYx|;y&_WTUg2XDo{DT<<1eA{h8Bx{IP1I8>5!+m)Y?}V=re1hArIQoCJLi5TgEI9WyzjjUOfG`@vKSem%C+FKG zy+eZ_!hvfWc5Im8Y&7@MVuOwlLi-??)s3$iBea!c>$Y&Yy_gGM;VMPi%J}smQ?Dzy z@qDg8GKtr>gu_eP?|8i~R%WU=M`|bL^ngG@W6nj1hq({~^7i2$L{ zYTPKihw{6_IjeMR$WQSs`J-zIqDGCWv2PkSnB*LI9(QlD@j_KtTSWOtQK%shle~vWi8SB0~j`n?hb6Z zR`H#<7Nz-lD^u3s7V#$CK?n&6{C%7|VHPa(OZ%VUQ!k&5$;9KOlt~pLrG2~lVkXJJ7V7%U>$}TI%6b|+98ASelf!CB@2qDBmji-@LNK-2AaOJE zv+|jCRsZCgpO#Fzs9(p{TB>!6kdZVh*h$YMQz)3EbK7$y#^Zwnp9Mse8xzmvl#}8%gIEgR9oc3!&No|QPwWP+ zB&`MTYvx6S4O}Wc@3CT2P#VT-?S!W=+ zWzT-m(!S!tEZ^T{PIGWpw99Jkak?Gg7lXh3@4XO7;Ditc_jK4Z(u2-fR-QbWOK2~F z&Be9t=?4E5+1bv7kwHbd%>z^5NfHxlYLbr*XtH)#DLC zYzr@x#QrwTS${(8huYAfy`GUWHG4t`f$l3qxx6n}@B(2ZEc!Vk>8>m>510IY*nCUS zu^~DBO2WPN{69m3U5UMMVkPI3Tcc%M)*hv=LMHuTnbR}%&#wU);qN24tJQnw<9C}QPtDZxei&O|1I!c8E%kN z9w$7VYFEHD=`69C>}o>_5fO>H_xA2q<>nq9dH3J-H;^_>VTYU~44*sdu#|~nAR%bc zDkqbMijM4P!1VU+uFm<4IYKJ#!|c~jO5x7FT>{M=d$$hzcC|_F4r?sMon$P{UFZu8 zlq+)Uqb8MD0l@hMx%UNf2ytf`PfwjulXs1UJ}OUWokQ+i)NeYz;Q2vK;t2R?%80Jv z(A9uvo3r+7WRLm5lDpb;sI<9=3GYPqFl|&XAH8>_k_=0vO*yB2FMiw!JMGC^EXtaB zKYjs%#Ey2H9H;Wlu_x}649bypO{pIWa%~$H3-9b$C^0d(JkIH0u}*UHV0i9AKwy|1nr;hr1^K!~tMC-g@cyl}I==^l(S=O}1VjV5mQ7WX zmLT8h@RhyR$&5_wC0$64m`y|SH<1{z9Hh02IUqV!(pVn1kfgCa8PO)Q(=%y4Z9{F< zS5qQ+H5DzJr+6#)aYbbtFJDYCi+ZK@%(XE-)WyMEA%19&qGGLskx9J}_E=B>Bviz; z*?9dCSH$;tCx=<#!S6qEzmAA&yNS6yALggYU zbeQm|d!;Fo(si3$d9T`$ZcnZ@^9VJ61X+rXH=1ECEnd7@`dMLp3>_jL*ga;F;v@DE zYDYi_f!5e?C&QMoS#lzsP1|_u9rgo(8B+0h&hYWD^75Ojz8B=iopAr>0+@vsMkwX1 z7f0QG**9U{(yDr0&x7Z@K)iqK%$v*WLYKf2d}rIcyI_wC`#e}hUKV1mj=2H7QJ_4^ zk}r54g|?~E?Xp13(Tx8kzo+6Z^_QIid2p;xuS(#o%I~Y)okH$oJkdl1ZF|>&U#eYj zRKWpj>NP5-=klj;eA7Q8skhn%g|3bxs~jxmQDJ}LrCehrc;}asb2X+NPTCfFmlMRKoludpguauPQ^bp`$itd+l($Ot8B)})4>3s4f z=1fLyC+$yDGQVU4Qc+S6B{-fMuMp{+4&{>#sVk!sxQ(Vmj7^AEHuNx|me1-j*SYUJ zS0tq!*HD0il}G~{f^%#P5}Q-w&a1wRqXeOkXW>DCg2snSLG%}=-TQdN!z_e|KS}vt z)whg~vJ}m67d<)kc|wJY{mrsAxGkG4J zc>7vhLXbGD7Rg$Eef?Ri5zp*?G7Fc$4)(xc_l?REM)$d;7))ec2`Fh<_qNsl{d*vj ztiNxFf>9trlR`aXU<3e5J^6lSi zSKd2jbFpfToS;%qh8oqVGGT3ex1Ky%DMQg1s`W4Fj>bQ9xWLQB0t;q^*QYEP!^x|s zvn*hADqf!-OpLi;Iv#<=}$$U*#-qg!M%6f zEua0@18zMBKt0Z+RR3=~`_|?279xJSxjHn7cCR zz~&`>0q$vfr9y$CB;EyjRr<41^@^jcF_SVyn*O#4G7T%7=KScqb`T55vh|396MObV zwZDfAS{Ytc3Aslf@&%{=8Pwhx?!QaTlK=Dq08{Ni2l!}T9P!MLY`O8X=}5f`QnT6v z^^aGtU>8m>P#_l81GK1T{WXDjRrr-DP}=h{g#Tm!8Pmtt$K}RX5r}yQiHV}-$lJJp z6c`Copp;r0(Z0OwjH4$S^$Sx;a{(|T{iq_$;cE}4Og#2JxY;`^id%VFSHZN4Y>>@C|9SX+(ElTY{nLckleFUa+Xs&Yl)5Q z$AzVG=I#KMV93<&R&GLcbgtf{s|>~$k-OZ!+1p*JiP+)zIZA*W1*gdaF<9*}e~Ezl zIEELzy?Q(lLhsr;X6sjwvWH!K4j_B!nJnr9o#aV^8FKb}Qz%-_x3*7#F-6t`Z*#*T zsrvA4>%8A-e`D#YRJJ3&jJ=Ge1tajk}lr!#~P6ec-Jm~`#VbWkuSLS!| zQy%Y<^|?zOz<~fEVGr_=+cY1UTiN`x3giiKz%y%64{Wqfb{*Lg)W)+mQ930+*nTT) z`U(SMAgPxWVFdR*f&F3D@$?S@yblY><5X0SJtUWh@o28yv*5&ZsyO5;l>xXRLv8)D z(Jwg$3 z7Te1`3bwGvgT&PMzxJC_@|N=gy{s>}vfe=A786q;RvUKYpF^5Gt=o4>? z^(2$0|_OY-)B~WL$?JTx; zla)Q(07!!$zlu3VW*T5cGOD*Qr}85Gn)iTHh-mYp$yRRcJ8H~m2TOSRVK zt>8igtV0C<`mbT`63iGaJ)NAU(k>>nX3!QjwYeW7?~A7Im#}W`{S8l|18m>EpfU{K zkS-f+-79Jg)cVx;Do6ZOvkB8??)9xYIo0d_%D z!7Wn}pEXIc^%o&@i%mc0T}++(IdID7lY~$H!hKrOk`Ac9ipkx z48ep2jm)4t#pKT!uKG1=dS$pEccQ+d7Z#FtNK z%0@$tb+G=RK50S2?&KI|cXb#PIgrW{@cqx5`%!&t0S&&^4F&Z>2mpmdg8$AEC!y)KIiGzi<6zaFd%bSU!r0f(^e2?*O(_q_zba=5^7SutF3GKG(?TQsI zYA4oA(T~R|!=<}Cp1x*jUZwJ{aY3&Vm$E%^Uigb-S2|BmSc})wT}#ioDD56*c7}ep zbM`?F3rr(Q=+vcwFYHlJkTWl___Hw4j-Or(M$p=$e?#U23O%@z!Hc!8NP|KHb&gjn zz!X2q0UW5^wa?qrdHscT_Aa(V3Y`@cM9jO?~P1Kn4 zp*jaLJTCiJIPt72x(V!-l5+C$RwutZO?giSDk?2ze`*UAOfi-3$#VBN=&TQu{D`uH zvu4i;YpTJ_C06z8-hLS+$hV}y0Po3pxe2a_3q@Fo(=80bwQa$FpUbhApxk9K(6bI* zHtCO9Z{$fOsF9J2>Q=;O&KcJK5|lrX+NN8R&N$Gf!&bIGF`ZQJh_BsN{|5Wi4ft^z z0wlk-06j6-JrU^oy5uo)JPX$?oh+HA zC1YxHbUw>uR%@9|RpTl#j2@9C zEwg>f+Sa-wzwTLcNqm)ddVr(PZ1V{`yXC#V7lT@-eudGIx?*tPM%63ElkvKyLas|{ zQ1x1a+>@EdVxnD#@T!giD}cz_Na0DCIsCY18}gJ_Q-FPJ37DPzs=Qg!qEnsqA(@pYG!94{`)mCB%K@mfyl_MtJXeV)vB zwKsB1_$n?3O3cKTo0Qb^gU#(Q16Vsr4=b5Bs&-7LYTy6&N+cd%iBv$f<<-iLH6GWI zoc#DWH46B@x#GWn0Jc6oP?_VgieXq)HNI$MvwKdhL~?~?4Vq)?hsNNgFjuDAwkM}s z;~`xojcrcL2d%3Rg#rjY%rR6??;Xges~7TV>DIu>v2Z@cF@S$Fph6%6zzXxL8z$SP zg3km^G}BPMRj>pdoBfVjZ4c59canA^!I^WFFs!;$0l>xy4P&dqCoM-ayU* zIdNCOJuZRG>~O#*HlD>Ee-NKNQ)n`V6>s+Iw5NusKYvIA7M5pyX+(wTX8h8Y?vm#Y z8~^PeJ|7`c7_g8t@)bShFH7Xm8F%^drlJ9s5p>XmrR-tI)GW5>qfomo&f4nWx?OtK z*Q*L*D5Tm~G1Viae;xKvmj$3U%?>3J92EaJV_8>w<&+Tkk|8ftLnbVPD!POAeOU+R z+=Qhcuv@F84)&Q<`8u0PDU#YICdTnyE6%&5@5gB4%qI2INp38WxBOekw9|!YvL&0> zg9K3&mTJ||ezsIu3Y!0xCA>ypV6%*Jvj!K$;yQRsW>EKLpM;9uY6;k0K~QOxO42L~ zVkGf7V$h}Dl%To(*o*btnD5+k?w>F|>DymNwwjA=Lve+c&c6(y3sKsUu{Bq1!XuG# zj%5t@8sx~SUa@O9j7f3}A)t9pl+T_cnSxg{7|c;H`KeCU*?#Hyd1`H6cq$sK&bys; zb+jD*Y8Cb#->z(a;8K3eVNDbc!)x;?ahESg*WO!}71ZhwQPjnmLF`4GnLTze!-ZLH z#qoQtW&1iTy3)rY1Rh< z2!$1o+xaC)V+B@ni@?8!;rRHrk-X7m zoSLncC61gw9l}1TR(cuW#56y+uzp@U3vtDXWiDZ^Tco0?Cwv|tj82G;noA`OhA0#c zcw(s(z{24G6KYNN2bAIAi_-5@a3pX3y8dOh|0s88#JmP*yI70!QpT&*zGTTQuplY7 zkiZtKdn;6?5*m+-Qg=&G1(#(ff@-YQ8fVoR%eLGeHi#3HCl?UUIGE;fQ+;V#1^8Uj z$>M)9BBp`+gBZ*b+VyPvsDTQRLgB4ZMhdGC1qsoE&%8_CCF0Zz=9cA(FUT}V!Ghwh z+##_HT}D-phkx)i2-C%~tv`42-%hmi4j|8<;|Ued(%VnU$yI57I^4uc65B6g`YJW< zQy1cEmD4Gn)2-34A3|^nZ%dJB1Dd+)FQo{ZA%xb-%q1*do9etX?*Dk~K)Q@rFtO!O z1q8f;)w2Rku+fr-%XTPLo4knPujv}&_l(Z7$pLyu6L#AN?H%ljosH`%Pm0c}i7?m?VioZ6=wO zzX$r>(ee;fse#^HcthY0OYYT*q#299l_U3Ycf^Qa$v}ye9>=(z-wuc^7QGYKzD*C7#Af`57eGU0rrk6dRJSO5_}^&MrkL zP?mF3eb>(2oLujJj~hxxMh?uysrqum`hQUCq4O}2Bt$ST+QZ>|U z<-PD3;9*MImv(lNQwzEgyk%R~yM$At#TAs@Cg*>&b66N`wTjXlv$~_`OQB_|M@BX3 z2q<5>WFkNu4O2|DY};~pj3*pmh}f&|FPmfbQYnNuC({kb3&G`~%8^CZ@9C(f< zi$!U(9o=!O(xEsv5yYF3C86#d4;vH*;}^ z+AWpX-m8#9C1sVJ`8_6O$REHu0em-c52u8JK0eM25bRj{NEh>AaloyB&1sqgm)pw{ z5kUKz&?imV_Ez_KfM~irI46ce{f7%*DYq6mW6;h$2lg7jG}iH2NoOX%6~>Wl=sQFB zJs@P;aAZ`5OGWV1`gT=!NOO3lqQnV$LBYE&XNQq7DFhpvnIn%49`T6tbhvp!@Amc5h?#;L4s8$$ZiO69MbUz)S z;B%7XD&)I*4I6r9DHX(z>%&RWPKe22Nn#_>3fzAqs8QeqQ(Ul`IhHLnU+rGKP1+|K z{clIR@bK8orP5%&?wT6rV7?(4E;GEjh@t@Ca0X@gcJ^OI1xCU>cl2Hi1Qx6;iB*u``mT&=~6EmsHKcj}X4xWsu#Iu1* zdaa4QRz?SV&(k~l$9Z!R^LGi|<0W5A6y~n);pCJbbQ(f#fp#58g~i@Y^&=Jf&qA*) zB)fUtsdHt;l;5;{7c|YG%3S9tBCDP z5deV8cmVuVjy!U={GL+*o8 zAG^6RAu#v@2oxCdWyt4U`lV9^EJzbYmLIy!pm_+qv_`wnyeMF}8$+r;*-8qIHMCP}sYUV~)hLLJbt2t_}ckkM2n zK(hf>IZ}z1yaUl$JsRBQZ6b*639CW&V8^?**p4od(jOfB|hg#EpLdWY zH3haS8LDp7oWHR^#tuOTDCt&#R^BtrXXh8$1+qnOw!O{({3_o z{bEH#K-C=#jy~O+g*DQkG&qJ~qqf>xlTGjfmb2Zpc?z|jCvaJVeiE@SQ4x(Mb9Mk% zB49R>0v&jAMrvl;QFFvQoi_sc1tx7*?q(E7cvQD|r!vMigKgh#wXsAhiBl zyz%xAFT-M8`}VV)T8B-QD)1#TvwVM_*Q7S@skOIzLE*FbV zE?`1&Td8(W0G1Ahk{hnvJKz9pF6u!HN%RZh;!ttZ!{qALaNlC}(Npw&&{dk83#!more%$>hP|${8Kwu772C#?Fh4v;-z%dIJWZ zB3wi9#-c3+x@<2EV3U0Di0(5pmi@F8PRlw_&|!&SqJQP5iNeI&w8bUF%5qoX60IST zexPUq{lhJO7y(B%@NjUyJ>n)#7SYH^CyUz1{v+cMFP`w;cN)S7NI}rO%PH2~mz$yL1!x^7DM8SyJ9*9ba6BJDLjbirnGX6I1wjXA8>ob_EGWi(&J=mQI zD%H$a6G^n`*_k+Ks^8IT%>^O-@!3*Lc5>^@8>Vj~hg1`0C|>^mwEP3U?@;T;-6f3` z(lYse=Zf8*!R^v=#&xW;Cux;MSVB6|{KPp()tP3@ehz=+%~U?z)a>wOinuhc})jpgF2=yRag5 z{`2IVgmYkED>5i3uV1s5!|J#zyToL+k!xql{Zl@Fq>PM5e^sGDF!L`tMp0G*WW--o zyL|3ZG8`U3vwG`eleAGvZEbChEo(%?xJ*E&0~UO^AA`>_0cfDu@)b8%@WyK+e|$7` z*y?-jneV(xb1x!hQy4@I!;B-*#sWJG}?)JyYmndL`sR|?4KjuhT%|LDDN>%QcE;Bo5u%$QLFs8GwRO%^F8q7(>FWJ?o0W+~wZ3=E=b4LJG7#%he@Sy%BJ*dWe1r0&o9uIis3;!w$X?5`930Jgd3>s>2 zmv2A=7^g0$-u>w4rNmp(Lr>1HZZ%`az}JEk@FMCW2R`I=_M7F7J4y*0*5Xz9ilr)r znG~8m`?m6F>aY7ag1Qrz%yfqL6hKZJ%a-SRYKJJBgbKTQU2&f+=JR=zz=96N|tH?53^{a>S+r!NgVEzEGg33_0Lw-8iWJ85uh~ zs9r-gc-`4u?s|pT-qHT=ZOaOQMQL);_*^R3yhlQ*Sf@AG>ByNkN_5(K=|hoH#;6IW z%`T@sNvhNtO|+?E9zuaA=H&w@AK|zn3NyzQKJPVXjAQ zA(X97$1lTf8lS&@^#5W{Wa(|z~MT-=us^r zv{j9zuJwpa&y^jmhR_o4@rJ9dn;jYh{PvsQ^IwX7wmngU#z)I`)T=Zirdn0+S-7Qy zbH;q2D_~=*u}Y7eSmf=>DIVzQvZBG1z88;0<#6HT|Bbis7K|P$9F#t4634Sioxow$ z9-6cH3$W*zF-uhTciFGHRuf$Co+hA*3rO}{+BYTf!P1K03Ur-8cm+~!kuSOX>vOmOrO(- z;&FISxfH!Ldo8Nhhv$xGNr9T3Ou_ml3ZjUQ{yj3H$EqiGG)`hq+_UlO)_H%8>+t$cgs^Yj_6K%u}{zL*a4YaP}#d8{gie-z?np%o=M&G7?Gl_iTYV%!mFN_F_R zTUakbx&nJ;zs~AzG=$t-UX4A0aS8xd=u$jQm+!CmRHSXirW7)61jWGk^jU(GlqGVE zj+ZWUvR`?Gk{x!Apg1(qh;RS@$|eATH-p6(*j&F5FBsURW3H$;tmPvVz#A^? zjWiig$9pfwrowhi57C*AVC}#rFNHl)=Vc5Ckg3TSxC(?30Z_ z{TU%NCKDZj`$%9r%&&Dv+;ojhp=D43En_n zXo!k}<$^>3KD~41IPSK{^m>U77^LGL+j8zEeG=&3_-6T+9xj-wyl03F$*eG@-oxiG`F)MXp9vXEfR#g_GLwR#6&MA;uI#qbk-#3Oc_2sI^P?$t zYSbry_#Tv~iw*KAy6DYez3b?}Qz@b3+jh`fUtxpI>^U+9JFiz0IbBIp0@Ts6>c_nq z-UNOxrK(gDxF=}R8jhh?59IOus!dw2Cn6e-+Xn)xN~81p5>&5v38&)Hl^G)pEAo|o z82Sl$`o!}|W%1bez^6#W6?od97tj%iV=3b>YkAqc=n<-rGYr54XoUIP8~<0n4QY@a z+d3daN%fPPGwzm&D2JJGg^9a17JD-BJs6hYD_XT`x2@ahOl+=}s@MH=3K?Fl`JRv} zDz_~*y&lA%fH3j*gWT_-{`F)u{&o{qTfiUzJld?q+%zH8^X(l_@W8qXXLT?^bE1A} z>!ng@^A;Kbqu-_rBaLjS++Pz^n#@IQ;xvZ>ys)xdtq3+|wH`U?L}FyFmC^P`-Ie*y z{po`;YK11!p#BiBjUwbD9pHqNgn#Wx_`w7sJjQ9d%x z_El_~h}@T46ZMAI`g0M#JROj|UPScxc6|x#vUt-Y-@1{R+_@)IGL_G8 ziKHN$y~DKM;2(}5nx`HFak~d9je(8dCzk5beIXEI49wo5f`T*ktCT7W$G)LO6mD8v zzi6ymV1Ojgca*I;i&hd!2G9%w4EUSBNIKHXZgt?cs1)M2+^$!JfrZ=IG!YT5dsO;@ z%y{`vL2VIXvmJ5-v@Ox6eKu4g>4je3bb7HinZY_xoopV+e%k)*k6#aQtKRg{)l@2A z(*QbS;6u)5-+Iutp$Jsr1uO;w#^a9({3t}jI)lMArE|P}k&LO4jE~BqRRp4wxqY{a z7D|{DLQkY)eKAdquV{|k5C{AKm=>laX(oW}6^+e{=<>p^v1S9a-0q~-;f&^mpWiJ} zC}1yUl{AWiXaDa7JwhAuKFRRUD`>?=QShxl)NxJEH zcH?0?uV+YRCJU?OROmxC zv2sqVee%`6ve0dmaZKn=G8sG8^Y}d7KZobzagZYpwc$HUFY|T~C7O7R~t#*fD zkpq?Zt`Hm+&s%4(ECBY$Z;2nTp?F3BN`}ZdaE-phjV;zltCXnoNCc|}wsjhW5fS>= zfGyXoD3R|kdNTAZgfr1G{sOhQ4Rjy$;E6aqJtMLe@mcEEb!d1jE_a?HpoF9YMZxDU zUf3RvysCHH8}I_k1q3a(!B;0ZhvtgNC9MN9)S*@!NtQwLv$n1rMu736+O1I@TK9bC zsi|BZ|0_`f;tT}sU}&4c?@ZN28}9134@N1zD9ig4ERbIC)6^SYOm^cquBDPZ=? zS3oanG7%J{MjrnW=Ye;!N3b9f$Po8r@cQ^ezjqWu5VQnp$Mv27sZ@0^bDL$>!4-aU zoKYTt+5+R;e1bcDV4UKoO&Anne3p}%Hfk0q^9i5?_o#qJ)^&|0kSTWuQ*o~quw1D9 zL91LW3$w3ZFREFBHeDcE$;W~P;0eFj6oB}D!{nNJdzggBnrBGu> z#)c3rF0vZ@{)@8D(b}3URz==dS)u{YTKM{3<%BYg?J=4+VAFOi*R5yjbZ-u$OA!b} zwpoui7y5zc@|8#fWDkUo<;Q`tJUtav;Kj0M0Dl>ui`~A!2p}*)j6&iwD5cRg z)7~M)HcIVnPsVd?$-QfD$6NS?1~mWM^Dk? z16Ndx?7_IfF>-6!JbBh;ED_zk>tUA%-$_D#0KTgY9=!fb^4pgl{(Y&c_PG{s>(n=* z%(bQNSfKyWU)#?0OEjKl0N!E%X!<9aO@<=538j{_Kao%0!i!XoYugU<%ab_kG2wH3o*lEkR5rO^{YP$K7ima zX`Okk=qUIARV8DwJ-i>3tk5SK#2g}~>KWvi^I>m4?4cn7L)s~eOP`8Z23sEb=+FVl zdj>%tA(dOGJ4GlFtRTBdoZs6xT$F%vVRVN#f#b2X9DeBSQ-%VZOANw~BD*JA;QGeMd}h<7ZdoL@V9DM#C%F^9T@G zOV>p18G9KFuL~iY&IQbD5M8?8Q5N1i)B9Zu)$0qr$j7tRpuSu!%RP^PDE>1N63UcBz~t}RkC zkFL+Js;;);37^y5$+av}Do9WN^|Vja2-h3^&9`sJ>+?OE<6=zZI-MhapS$-_5gjVE-?z3V~jW$+c%QPgW2?dlI zO&d4f?g%`?5NLkz+*hl7si=5G@i)Q0f6{y2w1?odZ2vcV%-#)Q#Ilg`bx&4>Nng<^ zv&aZF*oZytwTmgy3InW#)-<70vJ5eUlWh|1U)pN;z@V?aj!{4$H9Q;@*cjvekEpkR zs=9l=heafm?gr`ZZji2vh;%ndcY{iIcd4YLgmibOh;&?1x*Ohu&-eF#S?F>R*Uvp? z&YYP&GkbG?z}C~Mvm*g~a839 zs~^2})-bED_c5?LHt-?uyNYa7)OPyl@tclkb~3`0@X{Pc>mxOSE=vZjaa}SDK%S2JB7Nq}rX`u%%si|}&m`Yboya@0E*G9pH|N}J`aKf-Q}nAQ=S*;b;j zxhY=i&Fs_|&GRgI-0D$jnY(xB1A^v4fPCK`YIiFiwx9v>IEB+aYHv9G;ObMqYeKlMM64{cWhg<9@QKcvN{jtItj7B# zKZ#4vzD*xp2a2_?+94hdrEI2o`u+`-Bm5vyS{Jg6s)|x@ruiYN*>JbB}os z368Y5HQWeyQl{)-k>sZ7h;f(b%*xTPtQ>16ajK11ZWF*f)Cr=Cp2UnRY9jGwPuqQf7@OGY zR|9qM=RfbNvXn1M^M2es z)3b7SKME2glTyb|P8BCx3}5<%+}L{z-maBB6%VqX+m@~ieWsG&<>_^>tz~*NIEk|$ zv`x|DN;;k;c;N$lf?wu^S0g1O$JcsIh5*!XLbTj@*G5V?Op{4z4*T_UbU6wlY{!vGsFCA9^uwzg05sN)j{wX=ox^6q~Bz11lL$Y2Ltu zqK2udUA70a>QwKi^4wiQ8S4+%r`8olb%J~zMWLI&)xFXqEcE&-{I2PIRaO6)FY70< zk3f8P$_h-CyW}@FX998S_iEI%K=l6qEpz3!T(4t;7}1WPslwAg75-6E8+nD6*7g=M z4|eG~I#)Xh%A7vi=yoj!gx@GA2IPqP63-kt7yv5BOaltX@+z6Sq-K3R^-u4FpmRSlVxaGUFI_ z#s;StbJh&}7$p<(2P*@^WWMy@+3AT-`$Qk&q}-Pvpih7nh~1)zo;^c*yi>7jyLW?x zldjH3h|Uao}a|s-3qWx-59$JHx|@ja5$;3<@XFM@J3$M$ZGWMp;_O-;u)104h|qqTyfx^bw7G-QYCZLVqH!>{9!d- zoU`6?oGTgIr_%yq#<4lQR{Np(_>fBG-24GpvK(Rf&}=j_)2j$>V!3!eof}D7=iOgK z4R)Q+d5Gt_Pg-(RCmjOIYZ~XSJCkApyW6i^ZU}N!(&xuv{TkQY=6ASkZOAquWa6q# zIWeNk!yWNX;3-?9zXsfJy8o8M{HDl9JR(M9hqoAy!tnJE{@|T0v7J!pQn@q5{n}(b zl{e2+$fhBCD*qBd`L73J{JXz58tz%9r>7T{l~L2vhb2t7qQne+8k!Yn!!}gDCr{qf z6Iaq8Iep$WdYaQo*S zrv7;cpfFA)_l*&>NasH};y$61eo9@}X|+fKPIzF&Q(q~7fgnCAYky#1T)O=z+I)xE zcHeaf7^%*v5YR}zsRGhlnFmtyvD6N=6#dT=hK8%5L#ezzJobAYIn5_o#1#cC(RjSA z(oXLwPGyFTTn-g=N8N0UR5VLhW#hYNjd@bjgne+UL&m=4ts*iasaKY7BQ?XY_DxOj zFKG4}4YCzW2X_}HU~@<)5Mc`FISOe1p!;#OX46S&p`vBI)rCV;R9q$l4CcM*nKHCW zD?u)@dZmUJ7L^zkVKI*CCAUa64RwEZu(8neMrLS_rP10)$Kl~&Gt*@Zm%iO|wzx+!iF85zn6p8EU0VSzH;s?&yB&@o78ZUY@M=AlTt93!UfF4Tpfq6hnItJK@LXu)MyHSI2&Tkpv?f5gUAT__ zZt`AJoj9`)xRGPMt8x(B*R)yvV-wdO-Z_hHUd8VJD}R{Gl|E@iM$6lQG6$!;d&>yNnb%uASlX;sf46q3G~QQ7O-`gld^5)7 zAtrz~mMWHW#g-XO<0AZVYN^%VrdNf6*l z%;sxt6;s(hL7p@!C%#E7IQEfr10t=p2-eA^rRV}Hp@%Ez-CvEiwV@@iU1kmrj{4p5 zCJ!JnI}5mZFPFSom4%L90Q!KtHM}

R%I{ocwE-4jZ1+y#!@;_Y2~K>m6mlxcaRS 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 literal 0 HcmV?d00001 diff --git a/test/reference/pre_refactor/line_rlcg.png b/test/reference/pre_refactor/line_rlcg.png new file mode 100644 index 0000000000000000000000000000000000000000..abed9d1dcb6d4fb0657f536faa58f06b07c51a08 GIT binary patch literal 47531 zcmeFa2UwI@*DY9Tt1Sk)RluASEIe6TF|XbL%FpZxi z&55LsZ}{Ea74E$_`rN&DNEj#o`nB%B&%@u|DV;HT`1>0x+=stqygj#YdQPdQjy9NIqzjoljft8lk zlI@RgZgFUQ_b$nzGRorbKYxwWiuE3x{rCO*t*=XhlZ%RqrVh@zdGlu0iH}k7$;ruI z523(2#6Ev1y{NO(md33ty{ed09()QkD=3UzzoV@>q4NIu*(Th~qh=+S zm(yDq`}ik~VlWzhTXX#VHAB|{dx2=(G^ZKxqEnAxKQ0(9+75IDKd&*!^$fD zSbLqY#L}fp@fL<#&N7_aZTx}KkhQYGCCPN18+sbXoS(B+gUQkMQ*w07T6L)I`r4zf zMHepIq#eBaaCP>(T#lO?W8iS#E1%uI0Rc-nxMDun3%fV;Zas10L^8WJIg+l55otzA z#H6D%bX{GF*+r|8tBOWGB5K|V;>?o zQEI`d_HEh&{U0AG@7(Ei=FFK{a_sB5&R<%a0*w5lqoX}mIChx}FI`&9#)G7186Fg&D{&IrCm$xal zHa2BB%DZ;agD|li=!@(ycBtB9lpcO~*@buJFR`xjQ?A(Nzwte1-tp^`m z@XOB5_E_zqq8k(e}r2c_{?t;Y^&EG!0Y3tw+kZ|t|zr?Cl z+DmtZTuPA8Ni@IbJ~{dLVDB3f*+63nSy>Is>Ui~&CvQs|-i`?gnbX_T6JuSU)moo@ z%h0h!I5R6NE+)t6GakK3a_7g7l@*aH_Pw1ZQo5--@OTouy}e(xw?(^}~pH9v=`sPCX-Fx>;+HyWW(^SuPcvt=Lkz&%TSH^U#Js6j}ckgzUZ=Y}R zB>fPcy^gv=$J6oxuf=INNMMO+Ej|0NEoZtg5!s*K*6L;-OOUs3z3z16%cn!9&zvdN zwzf`;QVk5pnk$UEabu76+O=zEt+lq!7{<3$ zMoUOYsH9g^RCHl}imzX<|MAVEnltlVFf`j@W8S~Fsdapp^I5A3wXJ=w!QmCz~ z8{{(3t%H+V4)*uFJHcVmb_hzb=ZhCFlG)|qr$4nniN@V$A4ogT0Sj;~d^S6Bl2) zbo=7%7pgENm|C{B$%v_`$3Gl6{l_2XShR#)_iUJn|7;B8uu`HBvckK%>{#dLD;fp{ z24dM>4haoqYE?$5aZ@cc{a#fk=<)IKRWua`*sm@uEaaOqrMkw*HDR8DW3x3vQ*_`8 zGaB;Gi{<5Yoxgly8X8{dc>Vb1LA*&rRrMUZCQ(XHGfK4_d7;~>^ojxp_t%VPiCHc{YUCO+n?OBthm0jPhqw7)3VFU558(_jCe5LRsVC5)hf%!H}==vI(*gT(fQe{ zyWXZe5*HEK*4@h0+^M0V(N(3nTMQ#C%*wQlbo|_r;4;uxSnaiVdsU8;L({8Q&DPTT z8P}q(I4YNym(x?Rnmki>>;C=wvx0BmUY|4Y=}A#Z3HRL06_j_b^)Kh8fbhf5bNdiF zP3#fm&d*(^J#FbOt$q8h$vbq!ryqI}cj!qvyX@L;E{}d6JJAkno0XZJ-Ij3cnFf}p z{fC!IQu-NtkVONOFn$yhLgnq1t*x!a3+eT5O6S%m75XfdkdfJ~eSVf)MeyNVvHA1o zQ{;Vd_cTJDzht6ePTYsa2Q(d$<&VBT=Pzv_u2-M!pmWQ*UW=nG6}tX}2Blo2BuOlZ z?$^PG*&kn*^eui}8p`p0Tc53iJ4}`hRGqcL^qgaNYvj3elTN$4iz9exFI>0~sr_nQ zs>Nas&QYxGDJWHyWTYr(QL$AgQD5EMJm%%gm&WRQ_r@S(h>D5r6!F;^Dd*CEyq3lf zk3L#NEvUK3Pl9jqWTw7;h$P>{iE;=2{uxoa&{)>ueZyar_9&PkQ5A|KLCECwCri$m zJJ;j`V20&`3v-jX(Yxo{w^r@kwQH9=G6t>f{{EA!>({TF<357cpGa1=ZLgDKWn^Z? zB9g~xCj?EIzaf62kaR)y)6An>TrW#NP>|hR7FJYP=*2|JqC=(~er||)O)*7QR@T@n zG&Eu17WZbP7ypQe2+2AKb1TMEou@CC|A%#9_VSj*!SVVd=q9Gn`!^>gs#;TpK%4u(?`Y7KvtwgpiQX+zlu9N=r*82QAzG zd(f&waR~RUB}rHgAb;#Xd$H2R9j(IT2NG8t`1bDji(X6?fk4rELCwVGQkHI=hxgk z_@FBNkRB3{TA-{_x}fCVYgmKDY{VAF&cdYv^X5r={{H)Dsl7Kg8oBiHK%1`&mA&sb zR>iY4F=s0+lX9Z4@>{Ga%W!p!S~y9$8fk1U1!NjSZKIPPPFp|EmEut3nk#R2Ns6DJ zpH7GH;makAgR6*Ja^=bu`D1S{BYsCXr8#!9YaJWUuYT3iBBqBZSTk++`Y`;rnB92) ze9#KB3zC4Ni}e6jvTR=bj)g13L7=wm>uM<~F81N#l}#^ShJPUljPG|}xNsqva6ADV zZJfw)pu5JT*jQE!*^=^7U9NL>a!N|m+qdyFV%os@0De^fk}PI|E|ZdGZHk$R-_@&0 z6%=@^Ow7zA#Ko0?GA=POm*iXrPlhTuX>0fP_Gane7nMlzYRmkVkdI&*?TYUWd0bHb2ztB)AGcz+D-ckID_dk62P>$?o|MKq% z?N2i`vyZh`{5EF1F9Lu!B^y9Gs%h?@o4MHY)Oh|P|A2siWAC0NJU{xX;Me-hquc$& znHon7U3yKh$hLoOZ20R`a#M423}P-P21~oB8b>p3YHAYJ3y?9=J~eJ~RieSV?v~2! zwES_l=~JhMCwG;udsl!-s5E!|@jdATD*>xGqF8Vk35xeLcRkLY#VWgW`}S?)Vyjgg zvqx7bm-KyV=S>?d39hG1Zp;_nxlB`73QnK2=ybOZ8z~I-G`7~IaSTs>P#RE2QrA3^ zXpkNC`0-vkAJ$Kp73O#Sc|ck!-oj_CG`|*(r)?rIcdnG)Uw{22Uh4ENC;EzlQ&H=S zGxPoP|2j3^q!{b(x$97l2Wp-;{mkUf&Q4>mfPm=BmuHL}J9YwAz{cMFRDSU5<4JLo zO;mEatMyP()c|x676bZk2GAGWTr7$%mm^4n26-+9>BuG5E7mx4gv0}8@qirAur_WS`I_oAL>Okow#nTc^?DSfsEk-E zFW-6*HA0|=&!tNX^sqKHA6-#k&U#ZCs)>=sjEm4t&}|7Ya`8&&={YWCl&g=x8<3Kc zGS9h7z5CsBqelv#L)NcfZzuDge{I+Hzw@cIQho0HITSF=Hz+9XOVf)NVJ~O6hh^_n zRrQrZ5*LUD8suFX@Ai?pV)1~*&mVnLDkdtr1H;HD;-^BjM{}loSYCgVOkc6lU^rp( zm%vf86kTqIM?{$TAvZ*9zZkdu;yeQA1Ij8YKB%EQ8gafaUcic9{QUhzByd7n^fI|U z;k!cCguT?qfVR~n)gVVx8I+XUr;Kb?J|F8X?4_}>akEQgWMtu8go8zkHp}F6dFKxH zSz^ivU_m8>u5(~x1RVinQcw2U65fwQ#Y1^4z|e80k?TO(L(>$GqSDf8M4+^zZ+6i{ z(9J!W>CQht(TNGXFmr`z^F4k=Ef<$u56w(l6U^b3P1yi6Rwo=CTMdV=;OnH~v9Kl` zK$r4IUj7mPw7yyEwM1I(ngcdsZ1v%9;wdn^cRK6MA93Vj|LI8zM+e-q65dgB(X@{#w_;+gT3p5;HQ^psG+a zDap@QO4m)difw4v=eG^3*O;c*z#GLOb>JD`fi&kY$EbM9FMRm0>d#*nzd|-bjTf8B zu8Ij8>VKn1^(KH40a^t2m^Fm9mml~8^(Qw$Z*{#V5VmE<(@b6(lQzhTK^|m3Nlze* zjf-#DbkyFSxog*jc*=`1`uh6D9Ft;yz;SI0x_{m@5u#}#dX#ELRP!Pn)O%u}T7a0u zca{Xn;YOr{MMR3?u3g(G2q@*1fZEQYI-b?{{N<}xOLp(xU4Q$8CPgX@WLx{Z-+%vo z`t<47FDef1#mWL?(>#7WV^^rWupVlYIz((kr*=8NG*l@C+5p$xgJ0gYxyV^P-a~PB z!UQUKQP>fzkBJc_6jJRWAW=m(%WuYiZ3#uBV{{`h1{N>>J}bTxPi^Y7X(k+i|Ky)1P13@x#b21SHvHw~5>qPm{DXso z=lucpN@?rXf>tav3ZouG3(-RWGNpw0b^n2}j59gc6RUgC|EX zR0~?UQ%G9BOhqQ=dO^VsR{nzrZHBCS6T}p9({`oV@8B77y*lcc`FbDI67^1rLL&5P;yF%l7 zKfa+uBi>~l{}3Ke=?poiSR2FM-{0?x*v|pXYuN;ZeJW`6;Y82@g^Mtm=vL!dDaI10 zViDD}5soDF4GipG+#QSj+XDInm5U!!L&F=$aUm*uW}b^PJs+GY87tgNx$Xb{H65=3 zHd z2X<#ayPZj^(6g#at0Z{!Zje6S3C+#s_8m`Iz*xyP_V(P<6NO#@dEk*Sn$9f5zrJgM zuc@L&E$+qKwCT6k4+A4ke0&{?7zO629Mgv=r^xHqV`5@8ql>s)rr_Y%8W>C%u#3pH$HPUJ3<(mP-0zg~h1|zS! z4zd&nx_qqc-#%84<+SbQ6BdjR$r_+y8 ztmLip=FM~bkS_$zdM-CceM?P}Q2^Ix_l8)Ixl{+!6=AA%n@s>)vf|PD2t1l%_Fun` z?Q7*E5%L2}Qv+D_L9Ae+Ca(%tn#fv%760L7zT?}Q4~*rUK3TPcuiLa~6YA4))Y)lf zkLESKc~jhq%E1rllb+^qg`tWyzI6AMDzmH3B6@i01oerw@Lu0trHOJY$SUV!={jH) zpO=k|CSCyNs5fGk0Kr!syr>X0-U3`>-;?KqQE%T`u?X(c13ZGXk_x69Q@*ej%TNPP zlxhjieAj+;#lfBkWm!hxz8MR29E*kqKS(| zdx;6w{J_D3Om9%(<~Wsjf{@|yP0vq$JfGY1aw04L{(W&!r@|nm&tnQK)l1M#^G*Ou zn)BiQ?Bp3Bq@W|**ncn8=EY_fkjFr0u}mMBKw~aOkcv4fWh+l5@B3?X`?DN9utArY z$UL}hrWVR|x}RENyT?tQbzMp?y?_niN5Rhn4 zY~#DUpSppB5lO4x&Nov36Z5hd4S(+NP#a$_Hcv3FTwNloJyZ#ie`-ujA>UV%OEpaZE$~ z#GHy>fg3=uWZK%>`z6rYE4#L-F4@FWx%02_^P2&A%kfL5FW3arlAuU}9tc6nM5-ex zuLKwFC#R^WICqWBMg$-3i+g97-b9XpSb=;I;8FK~b!z-bBNBkE}FM4liW#NhUEb-i?}a;ODJ8`pe&r(;!6v~=uwaYoL#;1>!G zpW^_rg6IsH85zyin5f>uDqVG!Nfw~>u`2gI+- zJQ@w=?iHX6h$nL}8*w^GSAf|`#cF!}x(E!lUNXBrE6ubtM2v$nEN%^8HSdm@u^LKB z3jOcz$=fww0w(FE^^R*LRSxSd8j~qj@0K+MQso0P-2Ek4;WdU%YWNk+A^U+odr+X1 zS4g-bE$GDyN>q%d0QyhAnB%9nu?Q-0l6HvMln-)Z7b*A@E@{}bBDh3c<{ziLLA=M;3)pzVY*p~-3cO}? zQBa|=#)*Fz5`M4e{UP}7-OUD>wn|f`Oo`LGmB{clLLCjh!oSei8>9fk_i%u;fwvL@ z=}*^AX4^J-pbS9zGFZE2&CeTq{jf3s!Juamhw`FrRL@)yct zJo(so)Q*G=kf;gz^S<~mEaWi_w@-Yu>+7;G$UVt&-yehcOZp|vY1G5@N$VUsh({+5 z80FT|lv+u^0SX8!Odsw6sWx`_a7F%Md=DWnKswV+F+FcZG!QgDg3w+0LPq9Ds)P~j zlb}iyL&hU@te@SEMe;RA@&_BG^SPt``|&P>@j;!~P>E@4{Od2jTqC6z?Dx!>GbxFf zKDv^Ccz1rDg05?Cu@Rw&dQZfJ7UG(YcVxclx;s%O21OhN)R47D?b@oPP%!L(tgeBu z`TaERVSKcbgXHbwBz05G!+;h^?bAj~YJHvuRjMXWe8mb)@CVh9%$d4$Pbkrhms5s+ zd}9;llpt}e+}kIP0=7y(J((ydSr)G3IWqN&a1Ig+2p1{s*zu>hPGWpGslXU)3y5~a zUxHxADi5pJi-AUvfzZ@lxb$j)v23Asp36Yj-_v%Bf-NCBV*7mOCF94BA6`suiluwT z0=q3|*|$_E3AX^fOX2_|#UY8tuFM_yq{r$)&CiU}Qa{r7`CSq?dtg}-n)oJ7q6&rg z)zdhMxd-Ji71pGDAx?3Yuvo0*&iC*4SiE}ux)l*QnTiAD3P_r)fxfn+|I_~iA47sw4QK&{#qe?NG@p~4!dS^Vs+Teom`#F2o;nGmUS+2~qoW5C!iCaZ$|jyAfh ziuh}`_Qr}7>@C3sgU7|`*%QL+JZCV;HDps$1cRK=$vGwyw=mnr8{YYWqU8wWNlGTLd zh*Bc8H2(vXdN9u2c=~KouuztX&)U*61g66rsUv8Ea~7&05l@5^381}>jt+4tgcb=P zvh3w>ixhxMu%P7{dDEm=kd={W}){#c{4iPW}f>=twDnI%VrrhZo6|pZ5dC5R*uno=;kTC;O294b8+*uqy%xoX>OhEKwRRIl3Il90KgbcI-58D4uSlqhG$ed z;u32Ro&7FfjzLM!Ofoc3k}}BB62zQj9zlV;5|!axMHhW!1#uOfBt!1|7x(NtU+o0) zYc-raS6&O{KsD$GZi?A%<`Q@DWf%&~alkhFR{9~u^&FKmFWaaMgF&2OPL`F@_U&zVej9`F^BX;Sv{ne{(1EVX-HyK?l&3;I ztWN@C`i?v+nC0Cl_G7_S#)(d1?t|7)2^P&1jb zFr27*j*`8o2v}E%>hqWI4r%Ya;yQSOsC*Jfn1Ym1AUPFRBL&(wmo4;j2k%B@xUh=9 zIc}JEN2UESzmuwj^klsJ;QdKd35%;-RQPn6$W(Ahm`Aa;O-15t(v{zLb!mab9=V2( z#wI2f@1Er(H$Tlxf#jm41^1XF>G<9m+1bVQ^#=T#BbEKMKmgT}$tI^6P=e4c_y%&A z2%HY%qjD4UtHcv;UsEMZSjs@ncJ`o(!j`tbO1>_=YcU zSFqN9etMMf`qxOnXn3TxddcPqtB-a)Z|yvHC9bXI`uGqC=ygXbRYB7sfOsu0s&SZPCMrz6ibIqqhiDJ!AG%@wCzkvFYsWlj2UBT84rL{*;BDC{x&21FguY9>jB*l*3d)xc@d48X#a2u3uk5%5!UF^hLTt zEpi8FXlkaR1oOKOx=`lu;lqB_2+4L3gF`ns>qcxlUjYSx=vQJHwSZ%0uePSOHT(!g z*FpOfKabLa%4+dakb)*J;1205#mqu2BoA(1oNtT39?)PQxWZ8{wHZ?QB{30mLzpTg zo`%5<`OO{+w(8GhS%d+Pf9)B^mHtSe>kR<5vsLn({FR8%z8@fUyKnz*8D6w)jJ zSv`T%ROa!%#Cb*zEW6kkq^jbV7$fm_M9#lJ8SHU)q;wLX_#sIn91f0rktYZolxupR zr8QaWFf(E6lJo?B!6TV0DNte>k|diq(0&*}X&6oJ*~F8S$+?2rhKM4{!7x$e>I3mg zsTIYvnn`}`94Xbn;uojPTQBN~M9o<=QY^a7J^u3S(kiS67D)T8k>uHofew zI|Yga@nlkbq&RdN35LfuMPiCL!#fG!T?I&%=<&UH7F9>&?IKI*JJ+FXK`GtqWIv1V z>FsTS{Y2}`l)2<6O|&@Nz3DH(;=een+7T8ID;! zb?VeE_;f74`fK(m84pioMt>rCdf@hyowQ}B9njvs;m#<4Z&d-mk!k!s8*r@(o)r68bkY6oE~)$T z+6>J;++HuKxa?NA(@^3>H_z|G-Ed%J$}=)%j7(RC^!EY%-}U8g6W72>A}%3eFN}JM zq_;$J5Nl~@gu`nfKAtYxIN%4phg{CPj8Wh^W6jBAuy9;BiBI0IYp|OK4 zphcH(If*3@GIdu>+Jcx?1FS#yg3aYo( zVX*?qI|^r}ft;Kixo1e5EeTw)UFhJgCw|`E;ZTf7>do?Qd~mT1Ll>}O&bni&xZzu~ zX3a7tb1!l9dU_l%?@VoN@*)#Ei0fVqGDI$TokJombaElWZPD?43Q3EsT&bn5uAX%n zk&i}@C~F*xw6g^LOpT+jAC~7Eo0_%)u@O55A|OWP$eKBntz6(+=VWE+mcEB|k`orT z3ET}?f{`18$TLJPK<)UwVL-^Fjq7IF#RgGqMiN)vy}JgtO!nK%X(MwxBMbNd94ravC|Ukq#k@ z?jVCTB)J4Y%Yp+pV_0}U>~U#uIv`Xkcl`bR;fsS#Eg>I)n;Rp|H#nFEk@zEOQh>C* z*}1nVz&0HUP;4!j1H0vzh`SZKeF^PxtE)+mC(lj_cu}epkugZ2!xVWXJ+guuqKN%A z1$)ju1Pi<@c}xR3VP$RkZT!4FDpVz#47F!GSdGF2g|BkF2dqP7nZgI*o*@+&95RuA_MNkbZ73}|xasvz^768(^H}?JZbR?;}>B;v7?2#L^(O=C;-0fRby-=&>9%&yGuv1gDHf-2H zWtt^QMB>TgwK+*}}p?mdYF6J%f~y1tT4on1~wJ#diD;3jwnM z79nX@0^9XOgDd4Qa0R09g-U zb0{iOb_RDhOTjVq>2D)JU0-$o{;;~2fUY`MH#plZ$NObrrEWI09qd0rHX&#jS_TH8 zs#Jr)-p)~(08566NhItZO+P%n13 zrQV3up1_$oJuf}c4dE|_#YHG>-pTiy40b`rR`v7qvlSY-d+XS?9@)$|GKGWmu4sA) zK^XOs4vd?Sf8>t5+?r+wgIgeqC+;(J&UCdI4e1W`JWw>okX3ZGRLYr^&Y?;{XX-D% zEYQS-e}9IIr)4Bi9LVS6R#l0@EKi&LtC zt+x({#Z*tIKk{I%EEH9af>nJMliXRVC?To<{t~aoA*DpY{GU1Tdr%R>v=5Ani>uN_ z-L8u46PJ_}0MHO}2z4UCd{xxTM6L#5{u)N1!o(s;${W^f zXipKCoRWG=L`34G0Qkf40>l~u)NIp~T3^s5mSkausk@cdC!YV)vm*=FW+##NxKv^) zj0>l(eFBVn4L%bE5E!=UpP#!LMZtVlQ2)K1H(=%dyFXFW4NXE=#z~MgD};GXbGJey z#9W2G<_Oy;h@%!{$X z{vE{UmG-S#X{WKS;iv3|vSN;+K6$TXmbZ}6Np)13n(FFaP=~94i{6k;7!F|$)((ol z)+e`;+^5VX`&l82Y#5Z~c3<7#_^H;+ez{p`i2BgrKzD_Tj0FrKw%bR>(q=_>cQ;~U zB{5jw^?@4IRt2jCBs;t_BvBtqFS#4Ib%0*fgM@33m2-7<&4HA z3SDJezMk)2fg9OU(1$=?`4rx}w*@ePD%=b66yiX7wIPcH$(neO+nxYl&0XtTZKT2L zJq%r^gK?{TRp>+hYlrW1OK94xS#fX$8mI!EgM!^aaE8zlY%J6Z2pz2X0QtY)H*6vq zmXvvw$&)7&8H0?xA1m=96^nA{;UZjjtfT2zh~Kam;At!tN=w+8c@@OZYtDgn0R}Hk zxVh`X5k1In0Dk~EF;VjuhK@}0J3P-Yx{TCet^jmGVkg=;Egg`LLKnA=SB_U`1QKb2 zMy8lZxRnRGlU$hu4p2^0cMu_ny13i7^&hSOcn-4qbq^0dg9NlcL`Fwj_I5Ub0rmYM zP(3AnF^KqGsBvtH&@e+$l>rRRt%HJKOoapWJQ=7W|Ia(#FbWN|qOgYRM5L!{gXb>W zO>EF<^rT_1!M>3Z>&LuT1<=lwg!LkUm!8m3(0Tx4U`(@CI@;-Thb)uHjJo0p+YL0S`*8?cVtI$&}W zh&?s=lAy3LyWyMTHc^Qm1r?>hG$l5HT+|QdI>ZQr?>9LA9|K6f`u?HT9{%TTrcC)k ze!liH_5TEd^n|Ut%Fp;)@lO?dRykItxF!HmYild2t)&k8M#_32A8>NGjI`>oa8A#F7nM4V3{;OCxv2`vYZgymoj>?~JZUzzYvyFNr$0$O^kmi8pMHu__-Xuj z4h{#P`2)}J&02!-EyGYo3b2CsI&}Ri5%=_MpON9q+ zHY-1K`ErP)0pNDBAcDb(e?Iu>7Lmo)<4_`roVmB~$77-4B^EG)*n12@0>-_NR%mf8 z97v=$uAMUlMq8fR2tB;yI`C!GwQCFh0fvwfinTN~BdT%j#(=KXV4HlKb@KZB=1e(5 z2uE?K_YwGnvJz4%W$7FS-{XT?ZTqo6)XPi+|EU&j*5% zqO`PD59gwyC8gF5od^X#UK16>QOU`&z(FLymwdNU!_>KZ@wRh&RJdF&;H$RRe@vJ*%-8g^Q2!Z}rs67S?rHfD^#ssCpn^`nYQ4ib zk;E-RF=DDv{eU^dCB;Ssh?{;OjVDc)D~MW=nvcLCRtbU_@e*6RmW2wPrycj9*OA;9 z1{|t=_0m;!D)L-ii4gk6rMO*{{%TCing#8Sq@zf5Zdkx4N4XgcK^2y-+{B-M-UEpy zQ3;M10-sk z=~_UwmVQF}gD9dt3~{L;?ZnJjzyuDa=c&G?rVmOWa-&hDi3n`#54TO=>ql3U&^ptm zfzbTzu#DBwkdR0QA|9IPVVI)UsvzTCacmWXzlSx#< z#1tp-1*m0#nI)U80g$5#^a!GJ_6%`ToeBlNh5ACcb6KjkkV;X-S7GJvCV>g?2>_d$X_7Y*_F7xt`eR!OwYVXfGre0+u_G=qRyc z(}8)bxIhg;HXI8!y8@8Fq^O*U%?y98RT3T8a51I;Anvwy~D8_RX@{Nws? zt@t0Vl^TYkqD1g`Ypqs|Vhjdg3^6JDE)MnZQ@SMH3KV>mRl}X%hzFH}S(q~D;hb)N zpeHg-rlFhSkGgQ6W|4c#etOCd3JNk#sl{cO%y9oY8n_6I#-w|{xakF#nEDhhn=~pT z4dpaWJkxz4Dk#P+>3hqn9zPa^B{rq@Cq|EvyNv+>jDSW^cL08|XvJpZ7F`EwTrCPN zEo6IQU8bGRKj207(>n=Kv9XvGY8Y2^&j`BdVMO;FJQ#tqq8i0;nBIC~^7Suzcv!f? z>2u~temybRhWq!ok#XNNWdh@U-lVYlzyJPQ6zm`7l05bg`8HV?ZbxTq&W0)zfc0w! zv;3&xKfGD?Q#2q;3?UkI+-}@Fv35#vseI0n4ogpwIVV1)I>Fk>ch9KAf8G5c!vj-#OL7Dw0h)J=?5!94K zt+u#}I5K^cxt2^QWR22!0r)_>tWczaz{@QwyGHG9B@nf*DME0iZ7X;)>e~p>fnP&5 zhKSgPKCet{9YB|cuB*Za54MAzxKv`Q2{>gf2>+m#9zCe65Te-&y@mS*-nk~h2el2P zOLDCh;t6aWIsZ8Nqq51J5WX#OKS67bgp(nPe?!m4j&KkR)D#6kJ^ZNdbelDgBOpx; z!PM>m{y`J2`r*GbLQ2Xeo2Kc?!D~T#&yd*_%+#?yHH^U<4|m1fB14=!B;X4*EF@4wEM9@4VK5bgSBM z*zV)xvg;E8%?OBr56nVXN<|NP&Zjy{>T8p}Z+%NbSXc>ZnG#%;F3L`7f$%}TMgIc% zd8jL!HcUZli5wiw&fQ!cU2I#Cc%O19d{88sP$epg7K*gAG)iyaaH%{cGZc(Y=!e!y z0bxO!7o}Nh<+h%b=7R&0?lclfN(KL`EAoF-;aqK!cmbWs01D`XbrD+&ywHtq4od{E zIw%T&EgabG<5(~yQTa)P$n#tS7(Tq$Ew9>z%s?6H+m|^%s?%!plKBP(#-hSz>gY&- zJWn8`6oq0tjTaN3G(=&+THJ9%X$>l!XJQ4IqrQ>Dc?0Pr1*flpcK{D;Q6+E!o!1{d z7-*J_B2OThb19I3K}6n@Q`xmk8`J^hq;J##dpzO`Nm+0q*rPF=sij3~3JgU)0Q|%j z0eE7cj%E~0mDCoBrv2et2%(kKkbo!%<&L{$le<81w|>qW%Gog3 za}ec{0r>^74OHCfXUDhv_SBqB6GfdSUSSygNzK&ekU64U})TU|YjZ2W;pKh$}I+yVN+oOm!H zck4?-gYSl_GmbQB>F7k8hJ}R@uY@#=tt_POEwk%*x)vol+5E_P)Y{gDu2r#t4u`x- z&yKF&Dm-x@5NQ+8bZkLb9;(q5L1Gzrr{PO=gFvLv`^pEx1mKY)>R@>HmXMT? z6@t5`B~Hdy+Lr|1w>ji}359gI@Y^unN$YJ*yV!lOomC&-m(%s<`gMKHjy*`OWH-cQ zD?&;E5MU?7$E%X^3}%lUZp)E^v%KMPcZMC6r}-VCjZBF64}g#oiO~!s;*lQDD!b-A zftZF^3DifEIcCf1JLH>!?;RkN`nf7#hvo``<$*tn+L1xmyL=3OZVq<`dS$Kv)()qP zJNZknHA``^N`l;~Rm5c>w#cGHlYl#Si~A&XOQbwsF-QZ@;`+ok4D}x+umUQBb|r)D zQrJvPTDlrGEp+BdbGUJow1lRi6BzY4c#2l^g^|~?Y09K}2I*fd=<%z>)pcu9pXx=| z{s;n_gaW|E(cUV&FQNZ&KBZZg_NPNHe+?RqXk=(L+hbp+Rt-!uCfH(-U0?`SZ-gub zg62qHEPx<_ZyMwZf=0vV)-OUQ8MUfATGlKFCONNA?Q_7jWf_Vf9MiDYO)@niJ4HsTkwak#FV1xPU61HmnYBT5NES^J%j+S z@lf>;XyF63L`MegB3M@hndFtt8ffA53j7l|KApb>+%}pvYVdp7 zugD+)mq)Z0?_x-E#{%mj;}&FTTpPL6$4#1X4HiMFHBfJ&BBMStL}n6SX$u%w3fa}v zv3wii*mpBZ3E681kW-5u>WH{n7zj;Z@xHWwMjv`N?1f>4@J17`E*S$r zp#eakZDt-8eES0IHHdY|CgPh43#9Aep~(0Z&Seozm})SF*yt(N6p{mKcn!ER$Yz7R zH)0|&6F}z`rk?m{N)(z-1vWN7Xp3W6Sj4V6{7jOHB{CtI0=mkQa06=t+Y`he!YC&a zg%_^}f)pc++s?;UtDxLX{B{LyvrWK?P{>e4Mul;hsR9&4rze_;cY5mE+6@Z^&#zRl z()QJkA8jyhG_Ei~$kHesT8_pF6yD7msKT)U$Ke(%v?|&YO9O&41Y0{fObM>Wze2rD zadTskPhHmzjXOwYSfrYOcU7?4i!EQSUc*K^6qN5O@LoySKjw#fXz(_DcsUG6j^F}H zD%*52($c;ONQF&Skn(a>A@N?y{%&}awt!-X{wA;LL<8PdP?+AO-cahOUlr<-BRVFU zRM8z(foVzwszOi0sUJL5cZ9LRsCR=P0Zhous&()svlPX`NyY(uQhaiAJgbx0IP#zagem zse?ZcgFuG9jJx#>(0{`dl+oZQYj+B|Dsdk;{P6l^WXzkCb_#Rz$9I0c8~pn(6`G~* z9k}_k+jq<65AyZXP6SOxKW{J8uNMP5!PqzKpO}Q1P}j7_lRO9VU^na01Ga0*#g6Fy5lz^YXSlVy$M-? zS^~jCiw2iOp>dCm4y-irO_NU$4Zy=Qk5MM* zZao_CCJUWL)U-#*n6d?c75QwmaEG)BCZwG2v*c{pvFfU->uA?#25OvzO^sGrJzfBc z56spjx&hjjkX+QkB_hN3=T^MDsuYv;*@-uE6AZGoVZrhTC4;_JGP$8UWD{}fKh%tj z#J|hb7N9tQfSol1FLQWyV%QB&qcEZi{xEkK|8KB9F#c1vjj*=2guAiy?Q(#a$Tc!N zlniizfMFJ>1hSryLlcciBM)ar)+X}lOq;gY>N$Ut5TTsU$Hp@Yrxo@dX}wl1V$H9NkCoe4h>IXChmpRhJb2xLh9S%&ar(@}@H ze8xWy7BiQM_@UmbL$PGFB593-LW!=rEq_ehuor-LWPOjYV{rQ>5oQF+8i9pwD`v8n z5?cnXN)?kKnphX$-;rC_m)O$21b){jfQwc%xcmOF9NkvTV2_8s8kdmZXMx>diKrjG z4;#TP?RS85Pzr;&M}O4!2#ZsWhqwt{t_;15*trQV6eu|Rc`9~jC4+tQMD zGEVa&3E|rp?ZCR zvF0j>M4*a7+CMciDykLUC3VC??z0U8U7BQLp;kr`Xbm_QH}vhmCe{^TpSTrpQxgt_ z4YB~`$H*0(Kn8*Fg{pp8aykGetR4d%jm*uZov01BARK6dv8UPtUNMxERGy$hrM80H zp+P6YV?Q2l4B`mHj+=W|!eLcEiJEe-^^F*wYpD%voU~sOG-zrGCwK)I66n(ZVLR=e zN1Jqa1?2TB(Y|Zlo<{wfM~=I8QrZ~6P~vtVu&4uELU^s32gR@(1p(};TVQaxw1R>) z_J#={&lv6Fw*v2>_)C;9>Zif3XvG$xyq?p;$AxmhUP@dUw#tbo>I&ryn;$nv4>9o! z4InuZs3$yu%KEcQuau$p&ksg`xHxfYj~+f!LmehJiW0IB(jHmxy8_QNr$4c-JdXtR zqsyaJm#nX{#2p88(!SQ%mIz#>2J}Pnehe=MH%qga(A%)MP@xYmCLj{=WC-N-$DSO@ z@&@oQ0JJ4vEzeAapn+VD~@;L01uv<`r8kU4eYJFkf$E<@d%zn2#x@L|nOVVhOw)|8C9DsAs^v{?SrldrY~Y_RB^8>1lfSEzH+8${}sJfzr>mtT?wrN?UI(I1?8;`FG1-75vgS3GWZn5-#)H zF}Y~mPh)&{UUyR;J!-0L;IhEL&;3S^LIRqE7iPExD_(u`b)+wWsK1frq;H$4Kbmne zzj=12%uuIHG6x!1KK1V$YQa>M#FS+xk(>YI){gV_5XU^q zl|DhdV1Ws%1k*q}YP8xmd^kS+D44Xg$jnTZKg#Upa#)FAw!)HToqPO&ZP_IpOyU-5qE4d4$ z3qwAgUg))BD_&?TABxu^(2JGM^s8*~t!$ajXpq<@R7=GJGCmbq85z9oQ7E)vW`4Ug z@@l>Xp{2B(vV@w7iYK{?Gh5?0e|)T5TPplmXUWS;HSIW{)|)i}3_JaDb1lPLnz)?M zM*Y!cH&4{>V#z>9UC?yFWt9WnP-kRxW$Xn>ToI56%)~XYdvJp`F1WU1$)aNW z8iS?7Yr|&$41)tY`bSQa?1r5bV&V95?YTE!OH1oI?fNEj`~B}I>g>^oNzWh))gAeN z?YF|4SVLwtg0KlEh6X+htbj*sWFb$y*F2lI6CYkfFil|6I3|Q^NL?I^U>>RvE?U7r zZCg+;MNtvehVmZS9a$=%&aR+{Bjc?Bstfr_+HAGhA@}24Dm90%yW#G#KBK#hAIz~S z1Nq@FO3z$}oT=rWw!(*}9t5l1lS8+74z0FCv<3QJKU@b9#14)>7PYXGw_-WUJz^%Q z&_Qq^yHw}#^&Tu(u4#uF7BP*(r?h!7V?gGWZ6=k%6v$|cOC(tGNz-X52Vtj-NEG1C zm4h(Aw^G$43@0^&5lvNiI<&Bnu~SFpcG3u_+Y?xnwz3A_Kz$X$$QH;uBY8Z-=A676 ztRb{nv*YVPvs73%5yVvI{)N0n-PyEd)NdO%=Jn*u*k#diX;dM#j|i92b0VgMgYKnH z5J`f|dw~-uVAAH5Z<4_+%B@^kP=TZxT0#UNcIKd6_pqT)ETSY?QtB~Pvan0m2=bO* zA8~l2==2KEKSBh~v2T)ZBzOnn$!PUssfQ8)MTt^6eYSPRrgnRD5yvTz$cmgiQUdAE z+7|E_Pd1T^m`G*v!3Z2|Z0$u|BpBq8o3P0}C++q6wW-4UJ7fC5334iD9yC6uzHEkX z6VBt<|KS1E&afuY#-t0R?#jPx4xbCYQt_=&OC?67|G7TMZ;QsA>Ad0-BIu zz23;fH$<#DR5(iP*0)Ax8Kd3AZh|BPpS&;i?~G@{S4mw@G!LWO5RF83g~ZGsXx>hK zHTuILnEgq6Cc6>oYQr;!(Z1i#OK$aQ-u9dNkl0}TqzSJ(O-Lvj+j0^GMIHufBg=Vz zcjCxxn9D?X#>0w&sFK|OdUc*U-c*|3V%LglpSE(laW z2`Au!?}A6U*p_HYz|%6MacTq|!SxOL=cIq5iAGd7_%*Vx5NrZrorOIlo9yHTQV?!v zuR~H;!pmb4Z+9vae@dM{y(h`_JhB9Aq_+iZ#A>Fk(ny>E@kf1P-B7l4Ac@i^I8ZM? zh|n0=vz8;R&|<(xZ2aR%d4a)gIqdo+m^df`p2$GRxAcJz_EGRke9^@wrHL6Y`9?-8 zL?%LoKI8?Kf;if@2b*-T;n^hX1jq;KB1B207P3YiZ8}qjpXr?%ldj1bP=jd$5As~L z3t-B@DEobQ_0P%w372vUD)NyBx<)9%^KIT>TV5&@Ik<}3#w+&xCY=g+Kn5yANU})} zpM!yUM|))uP^2(2zrSI8RvDC7+HVItZCL;F#UpRCy5Bst7G^;a)yvl=CIBdm{3Uut z)>a~3A;5lrh6h$;6(BxDtn0|wBBb|&d4VChO^5|*1GLEZA fGH{Q->pN~G|K-0{ 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

$H=sBZbm?2p^P@5AbCz`SQxP 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 literal 0 HcmV?d00001 diff --git a/test/reference/pre_refactor/line_zy_cartesian.png b/test/reference/pre_refactor/line_zy_cartesian.png new file mode 100644 index 0000000000000000000000000000000000000000..a0a0a38f8ebc6727bd3199b7e4654d3976f71aad GIT binary patch literal 51787 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 z zAXo7U(m2)k)CH(uh{iUkL3LXLt;oaEGo_fa$MwZ-ELHO@ey&;x)kCz2(NsP~)oWlX znaX3({9?jpWqjBCqK1#hzdov5i+?TCzqZ8x;pCJxE$qDAhYrjxErRY^ogUpwnla}! z%bS}vYpm^U7D94Rb#pICR7sPJC{lH9#>waTr=VHV?SJ;iy&I=3`=aK{D^ui>?Z!IV K9Q$m}+J6I%8Q=&2 literal 0 HcmV?d00001 diff --git a/test/reference/pre_refactor/line_zy_polar.png b/test/reference/pre_refactor/line_zy_polar.png new file mode 100644 index 0000000000000000000000000000000000000000..971abbf036a90922fa242ff5cd523a5e8f6fc683 GIT binary patch literal 87770 zcmeFZcQ}@P{6DO_y;EsXN+Nr&tco%+vJ;UVO0vmlA}LwfgtDVhR;cVv;=P4+T1XED#v<%Nx62TAFT1snLQ&8;nAb*zm^o|+f_bFr~ zkEuE=`}StNbM4CR#l@et)4N`_JQkwu3*a$TxXW}%h}Gkvg%Sgu_k+W-3Ws_b?hKCX z*s6Er(t)x=hwSv%c{;#bB_D`y-i7s@`$}Hp>Rs}|oX70L zu5_20_SZ&m-`mY|avOu(77GPK!^gGZLI+j4OYb#BORQd&`t2@9!8_s6R|i!-y}UHl z8>BmEv1Q-B)S>3Is>Ua$c23XCkj9hUup8CaJ`gA3GG4(LD(+UGtgNi+$8A)3;oYNS zPTlte8Z%8*+ShK~w#|0DM<#^*T<+sMC4K!Eg-8*mh9{?jZiW49F^Mvr9`EJ8w}(%5 z?-83VUn*0zb7K4%6)SsiIe8^4X|G~FemoQM;i(p*uCDGf9M}Gc1O>&HR9`>8nACH5 z%twx#6;M8X`VpV`hYQ24FJ|T@>i7)Hw=AO=(Z#Iz80FagfvE z3%_FJu3x{tN66~&)~#E4O=@Z8e|7r|HpKhv;Lu@>A83dl`uN;{dm}Z!-2BXxiV~J~ z-Nubg0v*n=MIM`n#(S&A$H$dpPu|_1%Gz($Yb^ zDM=;%{$O)jT5W`|`Q+F4NoStNSd>f-G}wOo$Q9zgkvh(Gex_nO;xmv%qq3*JKYlll z@z*k1x5nAg!ZZnwO-zi8QaFdk*VmU%)t}<1sHu@x_>^_I8E;9sxq34*3(KjW?eeI7)`HKs)GlR)#0S_MRzkK(3xFti%ZEnn?@*!LD5!=D9&&&2Tcv2p0Oi(%)$bd75a-B&$E+Jt%Giiz_ z;k*1<``}6McVFJzw9Ocwn0VndWp=4GQ)X;z%*)3|16!h&XLkvSHi!gK>nLzB%KD_% zh_p-y2-qH?b9I`rTaNY2M4fokGd&3;ik!>LMXa`oz=3dX2LlPH%sDi-yx*aifiO0xP> zUaKBy650w_yP0npb(zLBdm=;}mEH9cOdI0XZP?IwiX;CqqBV&=&u+Nhi;BhO$2ZNA zvN9H7VJ#$pGUB-@!$ff-t(f+{efyYeD*PE=IE?9^kd!=p=+FtAbYp{}_){j8xP;?m zbJqEhjilzk_<#vN<|z0TlcJuc!&byO+37($IFweXuB{!87;k#zXgxL1AcZjVYq!u) zLJ@1mH5-|A6y$$)=-w!FsV!ScBTc6X^~lin`k_=%2mp_mi+}r!){$63T`o}WkKYupY zFtf6%Ubv7@Fx^MJSIqfL5Qpx=pr9Zm^W)dAkKUsZQvUq;Gl8%RDjSwjT+6!=78aJG zudg2v6~$4+sT$r_6K0O8oSvkelkP=IN}OU;YIrz}=94E+m{9ntf;gHH)-SAjWz`Z+ zA5-_(v{%`|AzKco{o#GA)KvLFpTm57Y6?O^Lfj-k^Xx|-GVR}g^u!^0`UJPdg(J?t zo~59O#uXP!&QCTd&dhc!rsCogP3j_%Hf-rQK3E-NpuhTcQ6hjg+_pI!zgK6`e$v$HcjRPaj7jT<*6k-}90k&&st zemNknm<}J7Hg8T<%dzTJRmCxM96DD%J(s6`;>qL3Uy({@GcWk|Rfohj>s*yD5wh;{ zj&$f+OM*~+Cx=cWu5&vj<$){x58DMXcDrs1*rnDgDB-gBxEv-Yw(t6>`%Gb z?2lojVvs3X+02)ln=|!_R%zlm7k-YkpUroA!GEdsG0sl6WV**+ak7#n?J(+y`s&U5 z8sFSp{Q|+yb}m~eH zbN*Ge=lUl|OeA7YXQun3)KtyPlE`+^y8b$bi>lYqb$KF1>!MmRjnw3|XcO%1=_w}* zi}J<7$||RoWuZm_r?)cDU(MJ!?rl+#5fv?Mnn|SN$ub|>ASOf+YEx^v(OqQOQ*2eD zeu-DQ*PS~`R1XWp1&Z6dnxtJ~_xm9AU(JllhbiHYkr zZw^Y-&aq;yMOWfQn|=}>zqG-(Zmn>c9nK=9$l~Pj<2TT*)6gWU23ud4%gV_$DvHlP zE?k@+J^R8e(CNoFCenw+-;h?$g3CFhj=V%osQ+}i@G07E^5MMpRK`e$<4-hSIXsV( z3yGm#yK$o$7UkuoPYn2(Ur%rEKw^BPGQMST;^ay9KmPc`W?{}TkO9R6&8h*JH}%oL zr(!T~w4mX!jK9r6ja2RT2}-f5$?9xVgH5Nh8h{Cs$U@K;&i@EQ3r-FW-WAwW8OX9n z%sG40K9R%7VA5cRzP^uTuE^?eO^%lEFvEv4n<1vwtzWNVirUGCYK_gfQ0l!qT~<-C z`Jj(T46-rMu*8chw?Fc#YG`AlDk-dM*RIX={rdF+`I_Xh-Y({fa3O1(uHvnh1 z1>vfe?_}FPu(0s6kmcY(i9^$0V|_>(EL{&exYO{9=E<=#+NvH)!R<6ODN$|}|dcqcw5W=jUl7O^P!-pw3JHOh&u1(5?5I~mMukW2CsL@@F z)~#LZt9Ew>+hp-J#RM6D1`?~lZ`-MoTmJuV8EIfD~6%xJO6Gc+_TPsDr78shGC zb#+Z5SSy$r7)~k#GOH;Ev7f`6G7ZW$85tQ>*z}ZBl7@k$(vZ2oPn0Jzm|MH zN8wz|w{I6i04Mky#|=^J72-%S(9_f7Zn?-$meWUkA)C~7)N<42)N|Z5Av{uwB4f8u zS`3|lb^?Qg4<{qFl2J}c)S-gg&d=DA62d#tSEGUql7-)049~m_foVKyto`Ja3w)a`Qw!=SjmUTq=eh+cQPX~ z6?U>~pO%*PESUeH)jq(@&D}GJ%3RsptU*!`HI;?rpb%hFl0n)1T!*n3yV1{V$p8n( zXlUN{EM6Gzso(~bH?Is} zLa2~sC4kGUH8XKxD$Q-+*H|~X)Jcs9!7ImskQNpe#;2y5fLRD|)nwCobq1H|haj{l z4ZB>Bn3>7Tb~aNQZ8dPI>|jsSGK}ThH~BNjD+6Gs0Lnc@k_4#QOot$4xsj2Pnc=LCo{Wj8VT^0t+0H!pwjxC>;df-K@HU*JbWM-ntYd9RS}KKD8s!rlV6oyT<*{z z2xqUjt2Rj(QwF)l7j+-zZPI#k3l zUGB}BH%?%%1iHkd9@Svc0TY8%l$HI&=Dtg4PK|cxk^s5Aj;aTloxD%f=`cUP`jZyY ziXXCR#i33s#5L=>ojaFn^By$L@QBUV2S~4YzmFIBT^6SM*KOMLa3Dz6Rf%l;nKOqQ zP;;7Mr8b8=WYvrTpbIf;Oo%3R70K(Dh+{d8mFzT{LH&MVSg{p*cH6^as6Wc>VIs2e zh~<}Suk!PI8{-veSFTz$_+~Y&DrjbS!R(0e2`Q;$%?#t*2A7E%0fLfMbaW!23Ow=j zT%$QZeA%r52p=2B5D(%zP_k1uU;yw=6$K98XN*UotQx$%Q|BGP3IpLW7e?B0xIxUN z)d|!9ax>)KAOW~v54dIIgpNH$5MmmM1Hx0q#l=HDJNEC*A1kFM`9tCv^roT4;YW)} zZ>H@St^lM{qtwg+O{CAx*OP6&6|M zW%u_EetaG)C*i(93FS5b^(!7xX^!$v*gYcu+NX*oW?vmCs)bdoC&~dK_4Ft*US3|x zs;U8K>hS}cy(_1d4hjQxOAZLHBz*$oPM3g|ST-%{IzgxF& zCxRo)Bm-N-lh7Qx1b+_-sjDXK}MYD(4!N<__e`f`2s_T9Ug8cOc& zR10O05B)m-vm*#Oka#Z7E|kaQ{z;#GYz^QYkCHXg2yOP*O#1=o!f^b@j}OInwizLV z2_Hw)R86+qcQD~>sF+pXp+QHdq_Bn{dQb$I31^4{gEp)1J2cpymnExw=1cF4LyV>30VcH+c|f$OXHy+dy>n!*BA;bhfGl7iDt;mj&B?S{|hPkqqJ^`H9I z0=48AcOaXV`ps1vl_e!_r4S)OKtLcQ^6H2X>1%{d+}+8M1dYpzkfKPJADSV6PO&j) z*N2mPdJHv*7M1SQ7h=Ne@cq1|e3aO^!iAaSmlo|wg0J`flUVlg2WM2xpo^v z`6To)Gw2e8##t~fNmJaYe4^k51i6y~Lx5vuXIFSKjoM073w(qew&S#oO=f*VgAp&irz&3Brrf9mP&9T4c_<3pGPwGb)*T7>G$i<;BFew9~t zS|%Xa04uKp-64oLY=>Lbz*`&Klu<=VkJEH?a7ai1X-PP|dGlsXA(~BreI)w{C5A@Q zga8ik_dnQx3~zjSNehs;G6}lMQa=wU>U;oVN))C5AV;E^fJ1`#`8H+LiLr2Sa8QwB zHa*qS07zxN-)_+i6s)GIrxy)fX#3v1PqE=7gVWJUnQJ6Ai$3n+z3}d+x~8Tk_mjA| z!EY@Y>7YPSUWhrut-89qjS$HXRe)zx(1ByIfJq5gM{=ap!O9vUQqjSgD~pOG)Nx!5 zY>JUu^zoHQCaUe*%aPQEgra*{k9D1hIASa7>+4JCs$nmFP+4C74oZ_DAJhse6R`Bw zT-)RYFOPjr(kFJiM`Otcdn)|70l@|o@cvBuQ5{Vpl$Pw~GYeP>1xc(7QR#&3M;hTFwCXLr&O^5qT}TUiTt)|^FCJWO1lMwl-%9;YbMI9E&I8b3Id@iQANt6Hr#gr z-PG*U%cn?G!zmEiWCYNkhd+AsT5Xg#M>5Jz0zJyKI>4ZXA@UWPs6QPY9cj5lFH|QY zlOXgRdeY6k2v+l&fZ~vl@amNa8lj9jz)gLPpvz3AMJWO$8D~7Nc+1^A39w=&Swuv{ z<{&viwdc>DgAFwR{t*3z1cWg?u9c{<@}ay+SHSt66p~6?!nHxd{w}bT5s)+~Qp^gV zQv{B*wP{l^GBL@hlh%YYu{qe%(gLikik}nBwv8R`;OldB*f>$A$W%@KpBHDHVJYswx5KM52P;^RTG%v=Tyi z27=Qh^wzIU8oEu`30|A8r*piR)shnEv2`S)40A0zZ-f&(t`+$3Ap_|knrctasvSyH ze;O)lJw0xe|Eufn%t+p->Q+=cT+pB$BK1KaDn5abAAq%OlI!d1ZFuwz3~c9qU71%j zBq2)bHBmcJ*HnOBy}JKeihf{^^z=`|q%C`?fZZXs=jNQ}1uCkmRh1A_YF7Y;lT;I9 z5PVeR-MMO}rlv`{g$4h_aW^QP8C1FS!M2I5?1tR6{_B&3ru=0`Pc<(ca!( zQ!V9;Qg@k8b4@ma+D14#FRzN3lk+sF)BBnPW}3A)M?*<0!T%*b!!YMC<6Ms z+!wUiG}9$Pt^(D_d3-P0Tt!IarB^8AdWVOf5^ORFRgsX)8GFra%V>n~r1+H%m!AU)-1Z~dnxyd)p9sv?a7nlF zL#`(}PmfpD);=7-Ni4Yys(@}Ic1UPu1zA}yS>-sn-KhJD1bYm?YC%C%QR3p_LWNL~ z`S&`Jjy$qrDeh!hzEy00E4K zStB*vs}d=TPPzmbLb#0@p=WCn?o4PpHqY<{K=mO;#$iVsDVr4>r%(06g3?I+_OM| zq|?ziEFL32tPVlI{y>CsKG=!i#F|j&6X^NF$>hsmn0gLY5 z-b2a6%Fb|==l#*evEdcp{Np$K*|=%SvdBO5b2s=E{Wze-5?c&BrqPRYAV=S zITIb-ap+$vrX);l5ow1|Kf!IRfNYh)*a0u6qBt*p$SJ#u>zcgG8Z<4m)DFd-rbUB$&|Yl5VSeuJLVebi=_M!9orO3Nfv*f zV-KaK3q+kI&4R2k@Z(ZHTVjSRa`3;SoX78=Z5R=P4jnmBHjp{dqCFSOR}Zd5wR2}Y z$~pnnU!Mf&_zggwP=|k$s*}$gj}J^h5D|byunTZJu}mO4`pH55A#r5kfn7I8(nEcT zlp`8kq+{Q1;i8Tr-A&_{q z+7$prq~Q?B23=8BM#eV~hyXSay9$ay=7slkOA;H=R$)&=&pnNf0>Ll|d>^2)YLYmf zKs^eiV3+#ProqXpwE6X2k5o`dQt$9@AYz&tmGMOiBsEejre|k|z_{KQE=+Ntv zKO|kljg{+F3te4EnM7O2ZBR|s)>J21X7(Y4iI$djYWS0e0#SiMZ%FODTreHCgYE2N zv!)~_5Ha#QzK}eLpgJ?vtZQ^O!}wmo!i@Qgi!I7Pqkwx2_Zri5bx8?- zAm9<^0SKvG(1Ni{L``lGQLi`dTF!g@KnBAU55zO_RJ89*!^3SIAY$W*~V8PbS3P z$i!p_M(UFG0AY++V6x*gLuo9$ysAXBCFq*ymwwgZLMmVo;CU35c}p$n9xSCn6g^_k zE{yrO9R`RHs7HPiP)RI#r2_{JFzXh)Vo;1s0e7J)v&c$@Co_ZMK{lve0SXJze4($r zaGA9Wd<>XJlr=u^2u(EEr`kEngh)U_AsT&PF$yZcH5n*C9qM5wP@I@=#Qaslnvy{r z;um(Mz#1jKIgG!@sA^PYkcg43A4o*+a!&z6d5l;f9zVFek`ih#8JIzn$aJ2zBByB% zv_vWsfI@r>QY*1eY*r54cgX>VVe$^TeIqdCeBRVhvmy%i9v)+F08O=UxI?C0#dnrM z4A`y-k^-3x*>l7u0sZZXU*@Gx>aZ-Hla{||Jo;+leoRbErggsp6%i|l6o!a-w$J(d zu@DRkA*?uw1{=p^Pf0I!`b5-QvF_7E=-<0KnxCXvVydmaV zv@%|_K+&16vCQ%i5MJ00sS|7 za}F9K5rC7?z@5H5FPns~U5ULTED)h#L{^x*TY=yX6k$0i%oi%oJO-s3z@#f-SzHQfYilD40#Wm!be{&3 zC7vXdWM;q|)b&8XGSVH0C4gqE+6V(T<$IV^yx5~*3JjnJ_Z!I_9T1CNO)637p#En6O;LMMS;{A>J08Ad1>PM8-~O;VL1 zm7Gj?%u``JLj&_8wi%Ib3y$rq7_=h@h3HSFJ>|Z+d&##0;}GhDjfwuU-DtOHTmvUj zP^UCd&J@U`6=tz)SoZKcP3onKrRDhX}A3v@dju1e> z(`2py3AN=x+&l%l9x^7`1uc)vlG(D5d?R6kp^IE<5&;7B;E@DF2ykQ5+ocO#=Yb-` zsmhEqOc8Rqy9){mLd9Hiz5Jo=u_!7kYC;#0A@dnEYu2t+GR0&_5;A`9B*KJ%{&sIL znFPfsF=ho+Vw7^$CGmo=NuWfHD+JGl^^T5mU`3WDBmaQ>j3RY>2kV(f7|)Z4Bf=)A ziJB78tB4{@#IV=4e-4N5M5&I2qER&oCHMkznb;17L=lCyO_z)uCARyuAx;Pu6cp_A z^z@RbuSA$4!<(42BNLf_K_&&7Z%aSx$}s6xmhuobD?j5uz} zwKZQllqflrr3R=NVMrvlDduRgnGL`}q=|+ANg3^0I-hJYvO?MBsBY_C*YIpuTCWA+ z|3Aa>U$`NiU2_wFiI{iq06y){7j8>fG{;Z=`fosOr{x;Ah@#Fd-^FpQHd#dV`SbNf ze?#ZePb{-e0cfIi+5*{;RQlt~S>iuK9we$hh$4k{6QxEnVo+q5kifezuQz`|1xWAS_i~?4j&FkwjC!_34j`Cmqad93TcMq5TEg-Mtu4?dpUFWgeWOA5HCN4f*$`V{zcC6vK zS4o)${kD94H@G>peQB5<9?UwLvolp@YQ>kunKJQ(=#}etd^ILB1YpqqDiHSJ;42Z} zM#h^yXkAVOPGgn_wj~AQlM$vqXJ9SZ9t7R22d9Ih|8~(wRB)M3#{S9qMHdqphAKk_ z(;qVR`SKDHXO6RZ?Kn)eV#V1FS5RKfxdCk_- zb}8!2ch{`cx+q;w-!8-L|5%k`V_SWf z|2`(5661;$U4AQc`HCT}-4}*e-OawJAUu-e=y1x# zq0(8f;hbmrohx7`8K5fqxXy|Q`_C0)G-A%dh~+O0*upSm^Lu%bNYG!GaAzLn|_~%WKZ#PR`)-S zIfX4>(Q(PBN1E2ZQ~FeC#v=;F#LabY|F|f#32$-2+|V>8a^$Nqx22h7vA4VBX;WOVs+hdg@s00dIUQy8*40t`J z2RGm$}(e)wsB zzGyWKF9e)EwL02ZJo@3LYRg;M8*Vnfl@ob#+b}TbOJ&VbNijzk7Z+{E-oOnTH&RZ# zlh~}Jqf;xXS5Pd9e>scArNhzChmR>8 z^RF}R-ZQd}^5TVqjWp-WJBkcHs`!-$2YJLddSq35h_>AEbmn)RX^0xf91&#(hFIWk ziFFO2hHt&Ig<#^ibaf2&8O`vs9V?YXL5ZIs5Yc` zY$!kTz$Yv7f%o*vU3S!TDsP(NsjQ|SPefy=pDq4Sgc(|7KBbL~!T+wkhHv%`)ZequbU zH%Ru-W32QIbQ5`;qH)F;7_5r6cD3P|hb3I@dYpl~-QRFMH1d`H=gjFJS&nXTxq9U5 zr5_DhS=(65w)*JtNV7y!R~B$)@!6SES$CnUtIwlab*c>^9%*)_Y?;`}uT$ z6rnzhiW-`j@Tg%VEnrs>XL(iCLEnEWg3Okpa|e#sEIVG~y6SA+8#?x|hWl47_J#Bw za@a|wZ-3|j9b=Z;bdXyoI5A_N-zoOa)Vz_d*z(%i(oV}QcO(U!CJ(Dz%(#p08;oH`^+$ytOY@e7$th<38!>_=9Zay}1>NZoY z^l=p5*4DFROZDNI%FD}dbl3Z?w9EaNjLce0T|Ob;I({1^t({+d*rc)@bXNV;<=sZ< zsB3M_gMo4$E)3>~fXY99{FsxQ`>kQNHv4^2@o?BLkZb$Qb!YcP-2#d7fBBMM-*qZk z*Vo^_=+2KHpDu5+9?9wVyYSC;$zAb%qjdc0ku`j?YhT!$cRyHcwCnK><1QvP`s7zD zG%Qvz#`=0zM4i2VT5IQdYnRm$orlZj29DB77%W4>0wyAG`_OBE*T(@U<_x?tj z{f*z^7hmhKHqh-0D^i$AxhV2PjJxXYm0T6U-6!Pl3UsVj&WMd*w3{BFzGEqZv-q^> zhtiRqltOPFa_HXI+x6|^^PA6~bA>A~s>ti=(qc@wbiii(35Uy_e;T<;0LeQo+3$$4-=MTH*pddWV zOQ4KC#%ZNRASbcLnJS}oojtqfPU-c(^IxXYTXMHxqdY3sv*>%z3-lNcGVyF2rK62h zrX6V!LjE%*m)5$P4gJyh?PCAy_XoNl;=S#?JCQS%qHioI;aR?@rmTD%!N^#7DXYB{ zV_TT8=}fO;H{d3xz%Z1OQTuklW-CD6DG}_Mb)9HXMN(qofj;4m=}mXa|8*+#M`+?M z>+VEd&d-~dlqw6}fVdzjcWS-Nl+hJ#;YOa~(sRr9Eof1*d93y<>%VfPH*uw^+V=6= znwq>X3oCNZEz%xR;Lr&8e9hCDo4w$dJ_bqZpzbJMzRZDRZyQF1dGhKNO;ATX{?opcpEFaN9P_2FXZ}Um9?FDZTdq(Tgl7Q`S^yS zFSzkTtf{F<1VgUQqgOk+C6iK9`*Qjt4IsCNUx3z65obDnlXmW!WbfZlp@)aFjf!q5 zYE2%!pl0n#KO|&XS8*ug)e3BtU68-OX5G789N$J_j7@~yK7YDrswqcbVrnuycQJ|O zZnpCkF=LzHoz&*jt^SuMX5Sc=K3i^azlEyid|hWmKtRBLx+rhZJrxy|qQd!4g=0@I zyR1)ZuDWAY#g=7>fgZo8sJc6iO-&_$tcq%CRKPe)cPjtcx%k(=D;k;zk5eY8XnEhO z>lia$1^?MZ7&AJbAIACV&)>IO_-J+Oc|wJx-(G%o*79g9nOg7kV@6?zOLccsTox2B zj@+v^KeRm2yT9e7M$RLvLv-U6j_{h8I5_&@b@ob$&rmW}!Xx=b--c*HbsZ^Obln>A zPwi9m6x#8`&5iNR48K#@ACgjU<=j~WmXn~qefx1;^|G7(trv<7&8sL8)*@;hZ_W!= zN?vq-(~`lRm~{9{Zo}Pj8O5&rgEehGTSY~-e$_7wStUrZy3C-ci?yGE#bmo>%bc2L zIiop7ejA&cw{7wWSyzCGPwTS1_79?>6zQ!01{+p?oLD8bc6ea_nGV^G0)-S$84>rI z{ZAcMA-z7Ucv%|d3AOxFZtms%aqDEMXFO}2ysZ&sQ{aj1hS_U2?v1uj+;{bloX z|IWzn=Of2*Q?|b@aq#oB(y3h2WX+O2KFLQfth@zi{9kp9KG^s0a*y1#XfGeKxITyj z$C$)*YW9#)E6TXD$e5Gr)lQne5cf^{rnQ9KetLMCoIJZ#_c$a&nKLb>!ie+XT1`|toa-xLp(jp#cTgrPeHmOy_ui3lx9B~^710S zUbt0Z*=tM+1b~yO?hcjP;=FJDM^`009r|)lLiEgzZR2r*(`K6c)TnMd8!54G3VUPe za4p{MX{_<+r#aVmCA;`v*Ep=ZB%v*OFyeB!Qc29)3|!*2qJNIK`qs^xfKRwYu(TBf z@N_=wj10WF1bFnrcQeURz3JqNy!w zY`-X`Brl3mUoBnJ^@s9qpD-?VJ1NbK?{~FTb#7nZNJ%O5{O`6c{g(Q_L!K3?Tp~Mq zpNO`T-q7N%@K(wH&c{ou`fI9;r$SDou-27DeB0mm#kc+9#}{`xR^|#C#x1jZT|4&i z*)RQZ3V~|o?ogg28G9q(@+g|szwx=BC2NEJvln6KC8+c74d0TGkUQxo5a_*fCHg=` zubNYNhtoT%Wc{Lw%=JN2Y6P?_Ctk z3~N5wex~>_SLE#<%hY=f$EJ8v4HQ1>OG@@MbRJ#E>gzLJ`0gS8?!$+Zc6R)jdF^|< zo`ar=Y3*-fJn(vh)K~F$C!Q7D`^+Hz-j|D^C@}20y)U@Y@p_ZKYi|Pd&hEHOvrZX6 zYOHWpht*BR9^Z$B-?a-P^un)&jXB&?O8x#Zfn79e zV#m4ro}SJ#H1yfcD@-O~ zOH<<~4Rx-EpneX;&Tbiow>eC)s&!8v4Ng*DIiig?Hz_;z$I$3#Kg#Qq=;-UaqUy?o z`okpv^a%lAV`I~{syOtmEyo|K$L~_dO|`-P_8Pv`0C@jTGF#;=Sbt8A?_R`wzfUvX~5&H116-UrnsCFoMCiq0nq9oc0JFg#?R zIZ+zDkM-_aM{Nam;X^)2UdP#)Yu=b6p4+pQ>@|tI*l?dhKJbeVg^zt-?|? zf1O}wPJ2u0a`-D6`UF^$0T*~>t*p4QU@T?U^Ha?kCUyJtb8~YKGczCmE53R%w(j0@ zsImSS@LCPi z1RQ1sIQIBZi%?wju>3xAhe)Z-t4;YDC>Xn+8(dNNGvQl#Ea+=`k4Z|#ty;Zy5NbGm zU$X8z^FAMs?hr16RRsS0t^-kl@4xJiW)KG+oL{}kAY{kbM;g0&6QgdXDc_vzT_z{j zsl1mi?oniuN_e{dSx&*tXv+XneomPfE1%OW=8K;$w-gfW`+9V>PhrOOjZbGC%&Ti= z>vz>1pQUKt*)uU!X||5LAMvZ=b92S^dnXgq(ir2?)BQFN4hI`PqeuiOcxhOSRF}%Nf?dEZP12(>^9eqDrSw7>=u{$1Lxbb) zEdAvv#}Q`=rEKXhI_~`6-!~2QET8=SeRsY6jEpS$+~0Ks2-v^-%SXSd8}-nNN} z?+U5E=}LL!#qt@>fuS{=P2Yr$yuEw%YD?Sp$4@!rL{99M zakjl4l$6feXUs5Bxbf$^&z~jsXZrMSTJUAOabQ#Ez_q>4tVQ@)YcqddF}m+l)t5%c zR&h$sq+c&Cw6<13{~|v%9o_h%)q`uRR;?23BgoZV&l1<1j=PRA=~@Ce>{t7!YZ>m* zg$lfy2wHd|!FvC|kdO+)6*rG9eS%6vjd2m@h7CopjWZ6=HQZhLA3mBtY$Iie4!&^L zJD<_=tmPuvVtm26KCH&Q##lm3>-~b#+o#7)b`0rd@~wdyaDr-J~&fvD;*41C#3;iu^ww}#pB50yK zk<%}i6%5LBn@ef; zzJho!>lW7$=yM$+}c&bpr0T8(Vst zo}4!5_7|xi-x)cY#dB-hcg}sAV^bzu|LWnjHU^;#To|eGET6T=pHe3`%q&nE7#LuB zRV?Mji@@}BZcIB?VS>C$5<&)U$P$hZw_1mh-CjvS=U<$7QU(TKM4?}EVx|K3<5br_ zId$bog!4!8EASDZpwclidPw%}Uxuk1tLj5R+HWzJAQ(xDMH88PCowrSHHIn75IC{F zOBXj~RpzdX!wijEA7wp{@EvhwDDr*iZE|c`NN%M@jG=U&yI~&ig!;h?3R8H@wo%@^YhLh@yvr? zx{E_@S8yNL*fH^6Z#JJPbQLA50^NSU5+ViC8O_9P4(ID*kDoljKvO9u5m2PjahF~_ z4%N>O&(8`ET-x+5?~|h8`CApzwX$U@XQlhP^#8H^5ScIX()=Lx+@)94S0)O!m#TDr?RV1UGOyK^NR8h`DI>R8 z?ALzr*_OIsZ#;Y^XmKWOk*+V?x)xIEcRdW8ccF{*xGmHz5+h>U9Z9;mZzkg1lG7vh zg;mQpiyW0RHfA6S1Ex~Gea;WVs6_*mY0NX%A?p*8lDY`Kf^uJ3xHww~i+T?RGmp`s zW?;)bkn6niZ59ICDk>}Kj13J}{05~4bT?yf{jk|$_G6nF0jQo;oYHL7#kl$piD=Z^ z!GkC17~IT+j$b-=Eq+jV*9NJOe_>>|lawu{GS2S^2N~jj=IXZh(@AB|GfN!g-htFw zh5j%T?GvVYhv?`qo-9|2CyEqbcAM4G$B@d$>?;S$h?fIzy%-vHNI9JJY*;~#z!XB1 zhKxB zf^fSwYc#Hz@2umUHhIB=F>xhfqbn1E>9*y~P2Q9IyB*K)(oT;UihX;!+^y?P?#_>G z>*ocp^OklSd*5wW`oKCzw=+;f@4SR$Pwa)um-{hNTf!^<%}y8-Z;zind$9Rat}P1` z*c9y?Kiu0V#=2$AGi=u$YU+WnU%yDsy!UaH78u!!XOwDS9 zHv3N8%gI$n`V@Sb^3~_8YxrKl(siaODk`E8vbq&KK5oUJtgS6)ZqBlI?_TGRsL8)( znhVK;Hypl3dq~U39PkyCK3!5$Lg+x-m9o9B@QkB?z`!e*$>4zxjR+!M8hQnA-NI1e z!d*Oj=&x`$bn@EZG0@R`0*HKc#4aY5lCaNX#*S-0*B@7Q(UjZi;pF}?o8@(G;fwdq z4%Zyg8~o0w>AW7Sq$}WLSzq*Jk&>_MW9zT&KACg3O=kCcf1zmeW~|#~Y-GGuAW*l` zPpAr%UumDqq>}6WknT3(wL`=$bAWZ`ZltNIuI`64AA`z9@XG2OZFVvCu$odLs>+=> zv9f#j-n~x}6Ym5&PV|lGL1RVt4-F0d1yA&CP~n`>);l*>{ODMmbt~hQFThhp8a{oh zQc)BwS&bVzC-OSx?^jmJ{(Yrue{QPJiueC)Ebeku_<0P&*e?E?nO(aL9|nC9ed0;4 z$lUNht12FUwW`=YO!Z{{$!}4})a!KL8ud1RvB}=zt628^&ArJ2r;Us(%_+&{BK8OW zdpA++67Yn!)4-GBiHxXu@5sn~`l+wRW$&9{eukHi@A;)q`~`Ep9RAW^@Wc%P1&zey zOvA^I?yJ@+pN)s zuXNttX(bXh+t%*@H^C8i@1QQ-f+M-`$!+%XZ&p32|JyPIt7fFxqt6XN+z{HH)>-M& zxj#1K-5bz%V69xAmv;sd#8Wn@HB#~n`dPcf1vtXM8lI<%(AuZp+)Tah=a^i(K}v^i z*{PyLNv>-)r7DKhF=i)^bR@lcxNx;Xiu0QigIu+w74jFi+PpQh8Gt$*4nK<>o0!uQE(wbFvaY5n6)TK%Om7e!+ zg%D=}D=o5qy-ZZd@>LC`|LDQ(?4>4b+<6~M*DK!1Kx*#;5MT2KAx%Z^a zw)tE!{Lu+KatZk_DRS=IIU4`Kz!H;Tfx_vH_MaUY^YinI!(C?v<|gCCjWC#AOsr;o z)~cA~W?QZbf)D?=;))v%Y$3!&lc{=QDOsZOSePC0w7_HP$AKX;X)4zpYN^&9OH z!z4Q1UxHQSh?uzIHz)7k1YUXfZvubskbvK4$ZhCV(Xp{N(TGB&>2K5+cjEy%@!8qI z70+1j8o+SL01XuQ>~N5ekFOYJ$MNs_D_Ob5s)D)z?`TN!@%gP+lKy#py6y-CVal%;o_O_UYalNevT=LuU6Wd?VzJ$^#32+ z^Y5C!N;q7PRa0qbzaa*ucoJeM7`GDt%e{6kJ$duytN0R1=T>0Zz zYIQ}=L!LA}i;+ZVb>5+2VL4Di?qIO03-^{1IO*$)rr&FIr5dJ}pyd||jCu3x&bBtg z0CVh*$b9jFasncn7jU*d9>Y^ZhL7}Xh5Bjs?Aha4HZ+9?auj=zhcFa}<3?QiJ%20h zQP=;Yt|fZtj<=&DhE*J@oIm& z=eCMX_ZiLZKYuaCw9j;UecjJbyWg&bK-#>kcb(6@!TJfI%kO(}P2b4PSx`Vc*LoB= zV-T!AcFn4rmipsV!f|kgiVr$$w zGl54xeb)m|X6(Zt$NqmNxTd$Ci7Pp8E&z@0#>Pv()hzBEJ>6_<^aifW+}lfSw)3&+ zd~U9p**h%K@J?p&Ka}_v@w4UfipCp=yN^_0Gnkoi<0NGrx5dMr|Xl7SD7+kDsm&DvCk$Ar1wtly^G(o#w?0!W|em--j0nLaw$m>vfh{z2_qveW+*l9K4zZAeQ+yOe=x@N zKqB9u%MczObk1(K|JYsKxf_7o#?`^cL~zRoC+Hr7P9A=?o7hIrZx>?v0&( z2C}-Nfj2QJiH?o!-aq3EJuB9ExPLVtc$C$m*zs8*CE;IQX2{*U(vh9UN6f!{iw6pa zVBPRu;J8Rex^m7;=5dQ7$-%dC2E0uKMP4`9QlBX-ynpE+lO!wk7h{pzrCT&i;wNf& zQe!foZKd>0Oa(atg-{3}1;DIGtcQ-`vM6%tKVH`oNMEM=0^}Lc}~@{J+Rp3WnOZWeBK`-Kki56x8E#Q>ACWJ zu<;M%tB{G^j&81+!)Y&Hy`Je4pD^Uu@MT5)E=70BK9&In^Bu0fcPo=QpFzkkv&X}O z2x2C4@lIE-jydIrUq~yMVaB~-n3hp^e02Y7;AlY%K?u%%%aD=$8V1(U0(s7GgEz$b3

`guXX8UKZ|A({p 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 A-2eap literal 0 HcmV?d00001 diff --git a/test/reference/result_fixtures.jl b/test/reference/result_fixtures.jl new file mode 100644 index 00000000..bf237cc3 --- /dev/null +++ b/test/reference/result_fixtures.jl @@ -0,0 +1,57 @@ +# Plain numeric references generated with pre-refactor commit +# abe355170add20cc0c841e218fed82fe8ffbab51. Keep this file independent of +# result-container constructors so it can detect representation regressions. +( + deterministic = ( + Z = ComplexF64[5.4880794448516206e-5+2.142977488665475e-8im 9.8697096800298e-10+2.0412210697007036e-8im +9.8697096800298e-10+2.0412210697007036e-8im 0.00015680043690510182+2.0400244679077576e-8im;;; + 0.00010581701803644702+0.0007312486483440818im 4.946703278825038e-5+0.0006805799692381052im +4.946703278825038e-5+0.0006805799692381052im 0.00020626488473634772+0.0006799816722038662im;;; + 1.0989335704667937+7.749475469324044im 1.092329841045977+7.076073666991043im +1.092329841045977+7.076073666991043im 1.0923298406717583+7.076073665230811im], + Y = ComplexF64[8.498573503008316e-11+2.005837708478695e-12im -8.498573503008315e-11-2.005837708478695e-12im +-8.498573503008315e-11-2.005837708478695e-12im 0.001245918845839505+8.136032986459829e-9im;;; + 9.044628601230882e-11+9.507085970981924e-8im -9.044628601230882e-11-9.507085970981924e-8im +-9.044628601230882e-11-9.507085970981924e-8im 0.002184899818483714+3.3711726618571345e-6im;;; + 9.044629210284383e-11+0.0019014170799333172im -9.044629210284383e-11-0.0019014170799333172im +-9.044629210284383e-11-0.0019014170799333172im 0.007432851116346678+0.003560745077071688im] + ), + monte_carlo = ( + first = ( + R = [0.00010581696308559305 4.9467006145411205e-5 +4.9467006145411205e-5 0.0002050068683336876;;; + 0.0006056010729546763 0.0004973631614150908 +0.0004973631614150908 0.0006527461800805942], + L = [2.3276367983215606e-6 2.1647427460262348e-6 +2.1647427460262348e-6 2.1628535818044176e-6;;; + 2.0762425678849085e-6 1.9336712487294683e-6 +1.9336712487294683e-6 1.9317832896537e-6], + G = [6.123344818344976e-11 -6.123344818344977e-11 +-6.123344818344977e-11 0.0021854357631945036;;; + 6.123344818344967e-11 -6.123344818344967e-11 +-6.123344818344967e-11 0.00260785373027347], + C = [2.433486812672539e-10 -2.433486812672539e-10 +-2.433486812672539e-10 1.0676616707424507e-8;;; + 2.43348681267254e-10 -2.43348681267254e-10 +-2.43348681267254e-10 4.2197656902851645e-9] + ), + last = ( + R = [0.000105817038981535 4.9467042941667494e-5 +4.9467042941667494e-5 0.00020674439285417576;;; + 0.0006056086585428692 0.0004973668390806429 +0.0004973668390806429 0.0006544855947044511], + L = [2.327636718483685e-6 2.1669638167088495e-6 +2.1669638167088495e-6 2.1650535561149647e-6;;; + 2.0762424333526048e-6 1.9358922928540616e-6 +1.9358922928540616e-6 1.933983250852508e-6], + G = [5.257045483225192e-11 -5.25704548322519e-11 +-5.25704548322519e-11 0.0021846968284390455;;; + 5.2570454832251924e-11 -5.257045483225279e-11 +-5.257045483225279e-11 0.0026068016286336427], + C = [2.2717980049205578e-10 -2.2717980049205573e-10 +-2.2717980049205573e-10 1.0653400291438419e-8;;; + 2.271798004920557e-10 -2.271798004920557e-10 +-2.271798004920557e-10 4.200398281492999e-9] + ) + ) +) diff --git a/test/unit_DataModel/test_plot_specs.jl b/test/unit_DataModel/test_plot_specs.jl new file mode 100644 index 00000000..5ed0c04e --- /dev/null +++ b/test/unit_DataModel/test_plot_specs.jl @@ -0,0 +1,99 @@ +@testitem "PlotBuilder: cable and system preview RenderSpec semantics" setup = [defaults] begin + library = CablesLibrary() + load!(library; file_name = joinpath(pkgdir(LineCableModels), "test", "cable_test.json")) + design = first(values(library.data)) + plot_builder = LineCableModels.PlotBuilder + + cable_render = plot_builder.make_render( + LineCableModels.DataModel.CablePreviewPlotSpec, + design + ) + @test length(cable_render.figures) == 1 + cable_page = only(cable_render.figures) + cable_view = only(cable_page.views) + @test cable_page.layout === :preview + @test cable_view.xaxis.label == "y [m]" + @test cable_view.yaxis.label == "z [m]" + @test cable_view.attributes.aspect === :data + @test all(series -> series.kind === :polygon, cable_view.series) + @test all(series -> haskey(series.attributes, :group), cable_view.series) + @test all(series -> haskey(series.attributes, :color), cable_view.series) + @test any( + series -> hasproperty(series.zdata, :interiors) && + !isempty(series.zdata.interiors), + cable_view.series + ) + @test length(cable_page.kwargs.colorbars) == 3 + @test cable_page.kwargs.display_legend + @test cable_page.kwargs.controls.reset + @test cable_page.kwargs.controls.export_svg + @test !cable_page.kwargs.controls.xlog + @test !cable_page.kwargs.controls.ylog + @test cable_page.kwargs.configuration.x_offset == 0.0 + @test cable_page.kwargs.configuration.y_offset == 0.0 + + cable_without_chrome = plot_builder.make_render( + LineCableModels.DataModel.CablePreviewPlotSpec, + design; + display_legend = false, + display_colorbars = false + ) + @test !only(cable_without_chrome.figures).kwargs.display_legend + @test isempty(only(cable_without_chrome.figures).kwargs.colorbars) + + position = CablePosition( + design, + 0.0, + -0.20, + Dict(component.id => (index == 1 ? 1 : 0) + for (index, component) in enumerate(design.components)) + ) + system = LineCableSystem("render-spec-system", 1000.0, position) + earth = EarthModel([50.0, 100.0], 100.0, 10.0, 1.0) + system_render = plot_builder.make_render( + LineCableModels.DataModel.SystemPreviewPlotSpec, + system; + earth_model = earth + ) + system_page = only(system_render.figures) + system_view = only(system_page.views) + @test system_view.attributes.aspect === :data + @test any(series -> series.kind === :hline, system_view.series) + @test any(series -> series.kind === :polygon, system_view.series) + earth_reference = only(series for series in system_view.series if series.kind === :hline) + @test earth_reference.ydata == [0.0] + @test earth_reference.attributes.color === :black + @test earth_reference.attributes.linewidth == 1.5 + @test length(system_page.kwargs.colorbars) == 3 + @test system_page.kwargs.configuration.zoom_factor === nothing + + zoomed_render = plot_builder.make_render( + LineCableModels.DataModel.SystemPreviewPlotSpec, + system; + earth_model = earth, + zoom_factor = 0.5 + ) + default_limits = system_view.attributes.limits + zoomed_limits = only(only(zoomed_render.figures).views).attributes.limits + @test zoomed_limits[1][2] - zoomed_limits[1][1] < + default_limits[1][2] - default_limits[1][1] + @test zoomed_limits[2][2] - zoomed_limits[2][1] < + default_limits[2][2] - default_limits[2][1] + @test_throws ArgumentError plot_builder.make_render( + LineCableModels.DataModel.SystemPreviewPlotSpec, + system; + zoom_factor = 0.0 + ) + + scale_render = plot_builder.make_render( + LineCableModels.DataModel.MaterialScalePlotSpec, + nothing + ) + scale_page = only(scale_render.figures) + @test scale_page.layout === :material_scale + @test isempty(scale_page.views) + @test length(scale_page.kwargs.colorbars) == 3 + @test !scale_page.kwargs.controls.reset + @test scale_page.kwargs.controls.export_svg + @test !scale_page.kwargs.controls.legend +end diff --git a/test/unit_Engine/test_parallel_rc_insulation.jl b/test/unit_Engine/test_parallel_rc_insulation.jl index b13728db..845cd9ee 100644 --- a/test/unit_Engine/test_parallel_rc_insulation.jl +++ b/test/unit_Engine/test_parallel_rc_insulation.jl @@ -100,10 +100,18 @@ end problem = two_terminal_problem() ws, line_parameters = compute!(problem, formulation) + pre_refactor = include(joinpath( + pkgdir(LineCableModels), + "test", + "reference", + "result_fixtures.jl" + )) @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) + @test line_parameters.Z.values ≈ pre_refactor.deterministic.Z rtol = 8eps() + @test line_parameters.Y.values ≈ pre_refactor.deterministic.Y rtol = 8eps() for k in eachindex(ws.freq) s = ws.jω[k] @@ -214,6 +222,12 @@ end print_step = 1000, return_samples = true ) + pre_refactor = include(joinpath( + pkgdir(LineCableModels), + "test", + "reference", + "result_fixtures.jl" + )) @test size(surrogate(result).Y) == (2, 2, 2) @test size(result.samples.G) == (2, 2, 2, 12) @@ -221,6 +235,13 @@ end @test std(result.samples.C[1, 1, 1, :]) > 0 @test uncertainty(real(surrogate(result).Y[1, 1, 1])) > 0 @test uncertainty(imag(surrogate(result).Y[1, 1, 1])) > 0 + for component in (:R, :L, :G, :C) + retained = getproperty(result.samples, component) + @test retained[:, :, :, 1] ≈ + getproperty(pre_refactor.monte_carlo.first, component) rtol = 8eps() + @test retained[:, :, :, end] ≈ + getproperty(pre_refactor.monte_carlo.last, component) rtol = 8eps() + end ω = reshape(2π .* frequencies(result), 1, 1, :) Rmeas = real.(surrogate(result).Z.values) diff --git a/test/unit_Engine/test_result_containers.jl b/test/unit_Engine/test_result_containers.jl index 7f45929a..ed437a4a 100644 --- a/test/unit_Engine/test_result_containers.jl +++ b/test/unit_Engine/test_result_containers.jl @@ -4,7 +4,9 @@ library = CablesLibrary() load!(library; file_name = joinpath(pkgdir(LineCableModels), "test", "cable_test.json")) design = first(values(library.data)) - constants = CableConstants(design) + constants = @inferred CableConstants(design) + @test @inferred(CableConstants(design; S = 0.25, rho_e = 150.0)) isa + CableConstants{Float64} # Plain numerical fixture captured before the result-container migration. @test constants.R ≈ 2.7567652874268654e-5 rtol = 2eps() @@ -30,7 +32,16 @@ capacitance_values = reshape(collect(37.0:48.0), 2, 2, 3) .* 1.0e-10 impedance = complex.(resistance_values, inductance_values .* omega) admittance = complex.(conductance_values, capacitance_values .* omega) - parameters = LineParameters(impedance, admittance, frequency; basis = :total) + parameters = LineParameters( + impedance, + admittance, + frequency; + basis = :total + ) + typed_series = @inferred SeriesImpedance{ComplexF64, :total}(copy(impedance)) + typed_shunt = @inferred ShuntAdmittance{ComplexF64, :total}(copy(admittance)) + typed_parameters = @inferred LineParameters(typed_series, typed_shunt, frequency) + @test basis(typed_parameters) === :total @test @inferred(basis(parameters)) === :total @test @inferred(frequencies(parameters)) == frequency @@ -49,6 +60,12 @@ @test G(parameters, 2, 1) == conductance_values[2, 1, :] @test B(parameters, 2, 1) == imag.(admittance[2, 1, :]) @test C(parameters, 2, 1) ≈ capacitance_values[2, 1, :] + @test R(parameters, 1, 2, 2:3) == resistance_values[1, 2, 2:3] + @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 @test shunt_admittance(parameters) === parameters.Y @test resistance(parameters, 1, 1) == R(parameters, 1, 1) @@ -60,6 +77,43 @@ @test abs.(Z(parameters, 1, 1)) == abs.(impedance[1, 1, :]) @test angle.(Y(parameters, 1, 1)) == angle.(admittance[1, 1, :]) + unit_handler = LineCableModels.UnitHandler + @test unit_handler.line_components(:series, :RLCG, :cart) == (:R, :L) + @test unit_handler.line_components(:shunt, :RLCG, :polar) == (:G, :C) + @test unit_handler.line_components(:series, :ZY, :polar) == (:Z_abs, :Z_angle) + @test unit_handler.line_components(:shunt, :ZY, :cart) == (:Y_re, :Y_im) + resistance_quantity = unit_handler.line_component_quantity(:R) + @test resistance_quantity.semantic === :resistance + @test resistance_quantity.unit_name === :ohm + @test resistance_quantity.prefix === :base + inductance_unit = unit_handler.line_component_unit(:L, :per_length) + @test unit_handler.get_label(inductance_unit.units) == "mH/km" + @test inductance_unit.scale == 1.0e6 + total_resistance_unit = unit_handler.line_component_unit(:R, :total) + @test unit_handler.get_label(total_resistance_unit.units) == "Ω" + @test total_resistance_unit.scale == 1.0 + @test unit_handler.line_component_values(:Z_abs, impedance, frequency) == + abs.(impedance) + @test unit_handler.line_component_values(:Y_angle, admittance, frequency) == + angle.(admittance) .* (180 / π) + @test_throws ArgumentError unit_handler.line_components(:invalid, :ZY, :cart) + @test_throws ArgumentError unit_handler.line_component_quantity(:invalid) + @test_throws ArgumentError unit_handler.line_component_unit( + :R, + :per_length; + quantity_units = 42 + ) + @test_throws ArgumentError unit_handler.line_component_unit( + :R, + :per_length; + length_unit = :kilometer + ) + @test_throws ArgumentError unit_handler.line_component_unit( + :R, + :per_length; + quantity_units = (; R = 1) + ) + selected = @inferred parameters[2:3] @test basis(selected) === :total @test domain(selected) === domain(parameters) @@ -76,11 +130,18 @@ @test Y(shunt, 1, 1, :) == admittance[1, 1, :] @test G(shunt, 1, 1) == conductance_values[1, 1, :] @test B(shunt, 1, 1) == imag.(admittance[1, 1, :]) + @test_throws ArgumentError SeriesImpedance{ComplexF64, :invalid}( + zeros(ComplexF64, 1, 1, 1) + ) + @test_throws ArgumentError ShuntAdmittance{ComplexF64, 1}( + zeros(ComplexF64, 1, 1, 1) + ) series_frames, shunt_frames = DataFrame(parameters) @test DataFrames.metadata(series_frames[1, 1], "units")[:R] == "Ω" @test DataFrames.metadata(series_frames[1, 1], "units")[:L] == "mH" @test DataFrames.metadata(shunt_frames[1, 1], "units")[:C] == "μF" + @test_throws ArgumentError DataFrame(parameters; tol = -1.0) zero_frequency = LineParameters( impedance[:, :, 1:1], @@ -93,6 +154,7 @@ @test B(zero_frequency, 1, 1, 1) == imag(admittance[1, 1, 1]) @test_throws DomainError L(zero_frequency) @test_throws DomainError C(zero_frequency, 1, 1, 1) + @test_throws DomainError DataFrame(zero_frequency) @test_throws DimensionMismatch LineParameters( zeros(ComplexF64, 2, 3, 1), @@ -124,15 +186,34 @@ end @testitem "Monte Carlo containers: optional storage and joint trials" setup = [defaults] begin using Random using Measurements: measurement + import Distributions values = [1.0, 2.0, 3.0, 4.0, 5.0] - summary = SampleSummary(values) + summary = @inferred SampleSummary(values) @test summary == SampleSummary(3.0, sqrt(2.5), 1.0, 1.2, 3.0, 4.8, 5.0) @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) + @test_throws ArgumentError SampleSummary(2.0, 1.0, 1.0, 2.1, 2.0, 2.9, 3.0) - model = HistogramPDF([1.0, 3.0, 5.0], [0.25, 0.25]) + model = @inferred HistogramPDF([1.0, 3.0, 5.0], [0.25, 0.25]) @test HistogramPDF([0, 1], [2]) isa HistogramPDF{Float64} + @test Distributions.pdf(model, 2.0) == 0.25 + @test Distributions.pdf(model, 6.0) == 0.0 + @test Distributions.cdf(model, 0.0) == 0.0 + @test Distributions.cdf(model, 3.0) == 0.5 + @test Distributions.cdf(model, 6.0) == 1.0 + @test quantile(model, 0.25) == 2.0 + @test isfinite(rand(MersenneTwister(91), model)) @test_throws DomainError quantile(model, -0.1) + @test_throws ArgumentError HistogramPDF([0.0, 1.0], Float64[]) + @test_throws ArgumentError HistogramPDF([0.0, 1.0, 2.0], [1.0]) + @test_throws ArgumentError HistogramPDF([0.0, 1.0], [-1.0]) + @test_throws ArgumentError HistogramPDF([0.0, 1.0], [0.0]) + @test_throws ArgumentError HistogramPDF([0.0, 0.0], [1.0]) + @test_throws ArgumentError HistogramPDF([0.0, Inf], [1.0]) + @test_throws ArgumentError HistogramPDF{Float64}([0.0, 0.0], [1.0]) constants_statistics = CableConstants(summary, summary, summary) constants_samples = CableConstants(values, 2values, 3values) constants_distributions = CableConstants(model, model, model) @@ -141,7 +222,7 @@ end measurement(6.0, 2.0), measurement(9.0, 3.0) ) - constants_result = CableConstantsMC( + constants_result = @inferred CableConstantsMC( constants_statistics, constants_samples, constants_distributions, @@ -162,7 +243,7 @@ end @test has_distributions(constants_result) @test samples(constants_result, :L) == 2values @test distribution(constants_result, :C) === model - @test surrogate(constants_result) === constants_surrogate + @test @inferred(surrogate(constants_result)) === constants_surrogate @test basis(constants_result) === :per_length @test ntrials(constants_result) == 5 @test confidence(constants_result) == 0.95 @@ -177,8 +258,16 @@ end 5, 0.95 ) + @test_throws ArgumentError CableConstantsMC( + constants_statistics, + constants_samples, + constants_distributions, + CableConstants(3.0, 6.0, 9.0), + 5, + 0.95 + ) - no_storage = CableConstantsMC( + no_storage = @inferred CableConstantsMC( constants_statistics, nothing, nothing, @@ -194,6 +283,33 @@ end @test_throws ArgumentError trial(no_storage, 1) @test_throws ArgumentError rand(MersenneTwister(1), no_storage) + constants_samples_only = @inferred CableConstantsMC( + constants_statistics, + constants_samples, + nothing, + constants_surrogate, + 5, + 0.95 + ) + @test has_samples(constants_samples_only) + @test !has_distributions(constants_samples_only) + @test @inferred(samples(constants_samples_only, :R)) === values + @test @inferred(trial(constants_samples_only, 2)) == CableConstants(2.0, 4.0, 6.0) + + constants_distributions_only = @inferred CableConstantsMC( + constants_statistics, + nothing, + constants_distributions, + constants_surrogate, + 5, + 0.95 + ) + @test !has_samples(constants_distributions_only) + @test has_distributions(constants_distributions_only) + @test @inferred(distribution(constants_distributions_only, :L)) === model + @test quantile(constants_distributions_only, :L, 0.25) == 2.0 + @test_throws ArgumentError trial(constants_distributions_only, 1) + frequency = [50.0, 100.0] sample_count = 5 resistance_samples = reshape(collect(1.0:10.0), 1, 1, 2, sample_count) .* 1.0e-3 @@ -233,7 +349,7 @@ end frequency; basis = :total ) - line_result = LineParametersMC( + line_result = @inferred LineParametersMC( line_statistics, line_samples, line_distributions, @@ -247,10 +363,12 @@ end @test frequencies(line_result) == frequency @test nconductors(line_result) == 1 @test nfrequencies(line_result) == 2 - @test statistics(line_result, :R, 1, 1, 2) == line_statistics.R[1, 1, 2] + @test @inferred(statistics(line_result, :R, 1, 1, 2)) == + line_statistics.R[1, 1, 2] @test mean(line_result, :L, 1, 1, :) == getproperty.(line_statistics.L[1, 1, :], :mean) - @test samples(line_result, :C, 1, 1, 2) == capacitance_samples[1, 1, 2, :] - @test distribution(line_result, :G, 1, 1, 1) === model + @test @inferred(samples(line_result, :C, 1, 1, 2)) == + capacitance_samples[1, 1, 2, :] + @test @inferred(distribution(line_result, :G, 1, 1, 1)) === model @test quantile(line_result, :R, 0.95, 1, 1, 1) == line_statistics.R[1, 1, 1].q95 @test quantile(line_result, :R, 0.25, 1, 1, 1) == quantile(resistance_samples[1, 1, 1, :], 0.25) @@ -265,7 +383,7 @@ end @test G(retained_trial) == conductance_samples[:, :, :, 4] @test rand(MersenneTwister(9), line_result) isa LineParameters - distribution_only = LineParametersMC( + distribution_only = @inferred LineParametersMC( line_statistics, nothing, line_distributions, @@ -275,6 +393,33 @@ end ) @test quantile(distribution_only, :R, 0.25, 1, 1, 1) == 2.0 @test_throws ArgumentError trial(distribution_only, 1) + + samples_only = @inferred LineParametersMC( + line_statistics, + line_samples, + nothing, + surrogate_parameters, + sample_count, + 0.90 + ) + @test has_samples(samples_only) + @test !has_distributions(samples_only) + @test @inferred(trial(samples_only, 1)) isa LineParameters + @test quantile(samples_only, :R, 0.25, 1, 1, 1) == + quantile(resistance_samples[1, 1, 1, :], 0.25) + + line_no_storage = @inferred LineParametersMC( + line_statistics, + nothing, + nothing, + surrogate_parameters, + sample_count, + 0.90 + ) + @test !has_samples(line_no_storage) + @test !has_distributions(line_no_storage) + @test @inferred(surrogate(line_no_storage)) === surrogate_parameters + @test_throws ArgumentError quantile(line_no_storage, :R, 0.25, 1, 1, 1) @test_throws ArgumentError statistics(line_result, :Z) @test_throws ArgumentError samples(line_result, :Z) @test_throws ArgumentError distribution(line_result, :Z) @@ -286,6 +431,19 @@ end sample_count, 0.90 ) + @test_throws ArgumentError LineParametersMC( + line_statistics, + line_samples, + line_distributions, + LineParameters( + complex.(mean_R, mean_L .* omega), + complex.(mean_G, mean_C .* omega), + frequency; + basis = :total + ), + sample_count, + 0.90 + ) end @testitem "PlotBuilder: result RenderSpec semantics" setup = [defaults] begin @@ -323,7 +481,13 @@ end @test only(render.figures[2].views).yaxis.label == "Series inductance [mH/km]" @test length(only(render.figures[1].views).series) == 4 @test all(series -> series.kind === :line, only(render.figures[1].views).series) - @test first(only(render.figures[1].views).series).label == "R[1,1]" + first_resistance = first(only(render.figures[1].views).series) + @test first_resistance.label == "R[1,1]" + @test first_resistance.xdata == frequency + @test first_resistance.ydata ≈ vec(resistance_values[1, 1, :]) .* 1.0e3 + @test first_resistance.attributes.linewidth == 2 + @test getproperty.(only(render.figures[1].views).series, :label) == + ["R[1,1]", "R[1,2]", "R[2,1]", "R[2,2]"] @test render.figures[1].kwargs.controls == LineCableModels.PlotBuilder.control_definitions() @test render.figures[1].kwargs.configuration.mode === :RLCG @@ -331,6 +495,43 @@ end @test render.figures[1].kwargs.configuration.length_unit === :kilo @test render.figures[1].kwargs.configuration.conductors == (1:2, 1:2) + series_render = LineCableModels.PlotBuilder.make_render( + LineCableModels.Engine.LineParameterPlotSpec, + parameters.Z; + frequencies = frequency, + mode = :ZY, + coord = :cart, + freq_unit = :kilo, + length_unit = :base, + quantity_units = (; resistance = :milli), + con = ([1], [2]), + xscale = :log10, + yscale = :log10 + ) + @test length(series_render.figures) == 2 + @test all(page -> length(only(page.views).series) == 1, series_render.figures) + @test first(only(series_render.figures[1].views).series).label == "R[1,2]" + @test only(series_render.figures[1].views).xaxis.label == "Frequency [kHz]" + @test only(series_render.figures[1].views).yaxis.label == + "Series resistance [mΩ/m]" + @test only(series_render.figures[1].views).xaxis.scale === :log10 + @test only(series_render.figures[1].views).yaxis.scale === :log10 + + shunt_render = LineCableModels.PlotBuilder.make_render( + LineCableModels.Engine.LineParameterPlotSpec, + parameters.Y; + frequencies = frequency, + mode = :ZY, + coord = :polar, + con = (2, :) + ) + @test length(shunt_render.figures) == 2 + @test all(page -> length(only(page.views).series) == 2, shunt_render.figures) + @test occursin( + "admittance magnitude", + lowercase(only(shunt_render.figures[1].views).yaxis.label) + ) + polar = LineCableModels.PlotBuilder.make_render( LineCableModels.Engine.LineParameterPlotSpec, parameters; @@ -340,6 +541,23 @@ end @test length(polar.figures) == 4 @test occursin("impedance magnitude", lowercase(only(polar.figures[1].views).yaxis.label)) @test occursin("angle", lowercase(only(polar.figures[2].views).yaxis.label)) + @test_throws ArgumentError LineCableModels.PlotBuilder.make_render( + LineCableModels.Engine.LineParameterPlotSpec, + parameters.Z; + frequencies = [50.0, NaN, 900.0] + ) + @test_throws DomainError LineCableModels.PlotBuilder.make_render( + LineCableModels.Engine.LineParameterPlotSpec, + parameters.Z; + frequencies = [0.0, 50.0, 900.0], + xscale = :log10 + ) + @test_throws ArgumentError LineCableModels.PlotBuilder.make_render( + LineCableModels.Engine.LineParameterPlotSpec, + parameters.Z; + frequencies = frequency, + con = 1 + ) summary = SampleSummary([1.0, 2.0, 3.0, 4.0]) model = HistogramPDF([1.0, 3.0, 5.0], [0.25, 0.25]) @@ -347,7 +565,11 @@ end CableConstants(summary, summary, summary), CableConstants([1.0, 2.0, 3.0, 4.0], [1.0, 2.0, 3.0, 4.0], [1.0, 2.0, 3.0, 4.0]), CableConstants(model, model, model), - CableConstants(2.5, 2.5, 2.5), + CableConstants( + measurement(2.5, 0.0), + measurement(2.5, 0.0), + measurement(2.5, 0.0) + ), 4, 0.95 ) @@ -363,6 +585,88 @@ end @test only(mc_render.figures).kwargs.mode === mode @test only(mc_render.figures).kwargs.controls.export_svg @test only(mc_render.figures).kwargs.configuration.mode === mode - @test !isempty(only(only(mc_render.figures).views).series) + kinds = getproperty.(only(only(mc_render.figures).views).series, :kind) + expected_kinds = mode === :hist ? [:histogram, :stairs] : + mode === :pdf ? [:stairs] : + mode === :ecdf ? [:line, :line] : [:scatter, :line] + @test kinds == expected_kinds + if mode === :hist + histogram_series, pdf_series = only(mc_render.figures).views[1].series + @test histogram_series.xdata == [1000.0, 2000.0, 3000.0, 4000.0] + @test histogram_series.attributes.normalization === :pdf + @test pdf_series.xdata == [1000.0, 3000.0, 5000.0] + @test pdf_series.ydata == [0.00025, 0.00025, 0.00025] + @test pdf_series.attributes.color === :red + @test pdf_series.attributes.linewidth == 2 + elseif mode === :qq + scatter_series = first(only(mc_render.figures).views[1].series) + @test scatter_series.attributes.color === :steelblue + @test scatter_series.attributes.markersize == 6 + end + end + @test_throws ArgumentError LineCableModels.PlotBuilder.make_render( + LineCableModels.UQ.MCDistributionPlotSpec, + mc_result; + quantity = :R, + nbins = 0 + ) + + line_samples = reshape(collect(1.0:12.0), 1, 1, 3, 4) + summarize(values) = map( + index -> SampleSummary(view(values, index.I..., :)), + CartesianIndices(size(values)[1:3]) + ) + line_statistics = RLCG( + summarize(line_samples), + summarize(line_samples .* 1.0e-3), + summarize(line_samples .* 1.0e-6), + summarize(line_samples .* 1.0e-4) + ) + line_distributions = RLCG( + fill(model, 1, 1, 3), + fill(model, 1, 1, 3), + fill(model, 1, 1, 3), + fill(model, 1, 1, 3) + ) + measured_parameters = LineParameters( + complex.(measurement.(real.(parameters.Z.values), 0.0), + measurement.(imag.(parameters.Z.values), 0.0)), + complex.(measurement.(real.(parameters.Y.values), 0.0), + measurement.(imag.(parameters.Y.values), 0.0)), + frequency + ) + line_mc = LineParametersMC( + line_statistics, + RLCG( + line_samples, + line_samples .* 1.0e-3, + line_samples .* 1.0e-6, + line_samples .* 1.0e-4 + ), + line_distributions, + LineParameters( + measured_parameters.Z.values[1:1, 1:1, :], + measured_parameters.Y.values[1:1, 1:1, :], + frequency + ), + 4, + 0.95 + ) + for mode in (:hist, :pdf, :ecdf, :qq) + line_mc_render = LineCableModels.PlotBuilder.make_render( + LineCableModels.UQ.MCDistributionPlotSpec, + line_mc; + quantity = :R, + ijk = (1, 1, 2), + mode, + data = :both + ) + @test only(line_mc_render.figures).kwargs.selection == (1, 1, 2) + @test only(line_mc_render.figures).kwargs.mode === mode + @test !isempty(only(only(line_mc_render.figures).views).series) + if mode === :hist + retained = first(only(line_mc_render.figures).views[1].series) + @test retained.xdata == collect(line_samples[1, 1, 2, :]) .* 1.0e3 + end end end From 7b09b7b1a9d874434050f86308d85cd3716b0e72 Mon Sep 17 00:00:00 2001 From: amaurigmartins Date: Fri, 14 Aug 2026 12:00:56 +0200 Subject: [PATCH 006/157] fix(plotting): restore interactive layout and logarithmic ticks --- integration/plotting/manual_gl.jl | 31 +++ integration/plotting/manual_wgl.jl | 205 ++++++++++++++++++ src/engine/plotspecs.jl | 14 +- src/plotbuilder/uicomponents/UIComponents.jl | 193 ++++++++++++++--- test/plotting.jl | 37 ++++ test/reference/cable_preview.png | Bin 405117 -> 477789 bytes test/reference/generate.jl | 206 ++++++++++--------- test/reference/line_measurements.png | Bin 69100 -> 63409 bytes test/reference/line_rlcg.png | Bin 67379 -> 61886 bytes test/reference/line_zy_cartesian.png | Bin 67379 -> 61886 bytes test/reference/line_zy_polar.png | Bin 100971 -> 109129 bytes test/reference/material_scale.png | Bin 31334 -> 27006 bytes test/reference/mc_ecdf.png | Bin 82175 -> 81392 bytes test/reference/mc_hist.png | Bin 66038 -> 58816 bytes test/reference/mc_pdf.png | Bin 77470 -> 71034 bytes test/reference/mc_qq.png | Bin 78788 -> 77619 bytes test/reference/system_preview.png | Bin 288287 -> 321873 bytes test/unit_Engine/test_result_containers.jl | 5 + 18 files changed, 558 insertions(+), 133 deletions(-) create mode 100644 integration/plotting/manual_wgl.jl diff --git a/integration/plotting/manual_gl.jl b/integration/plotting/manual_gl.jl index 4a79fa5a..eb771fdb 100644 --- a/integration/plotting/manual_gl.jl +++ b/integration/plotting/manual_gl.jl @@ -41,9 +41,14 @@ set_backend!(:gl) @testset "manual GL plotting gate" begin @test plots isa Vector{UIPlot} @test length(plots) == 4 + @test all(plot -> plot.context.window !== nothing, plots) + @test length(unique(objectid(plot.context.window) for plot in plots)) == length(plots) @test sort!(collect(keys(handle.controls))) == [:export_svg, :legend, :reset, :xlog, :ylog] @test handle.context.backend === :gl + @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 @@ -72,6 +77,32 @@ set_backend!(:gl) end @test startswith(handle.context.status[], "Saved SVG to ") @test LineCableModels.PlotBuilder.BackendHandler.current_backend_symbol() === :gl + + susceptance = last(Makie.plot( + parameters; + mode = :ZY, + coord = :cart, + backend = :gl, + display_plot = false + )) + susceptance.controls[:ylog].active[] = true + susceptance_axis = only(susceptance.panels).axis + @test susceptance_axis.yscale[] === Makie.log10 + @test susceptance_axis.ytickformat[] === Makie.automatic + @test susceptance_axis.ylabel[] == "Capacitive susceptance [S/km]" + limits = susceptance_axis.finallimits[] + ymin = limits.origin[2] + ymax = ymin + limits.widths[2] + tick_values, tick_labels = Makie.get_ticks( + susceptance_axis.yticks[], + susceptance_axis.yscale[], + susceptance_axis.ytickformat[], + ymin, + ymax + ) + @test length(tick_values) > 2 + @test length(unique(round.(diff(log10.(tick_values)); digits = 8))) > 1 + @test all(label -> label == "" || label isa Makie.RichText, tick_labels) end GLMakie.closeall() diff --git a/integration/plotting/manual_wgl.jl b/integration/plotting/manual_wgl.jl new file mode 100644 index 00000000..2e72c903 --- /dev/null +++ b/integration/plotting/manual_wgl.jl @@ -0,0 +1,205 @@ +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 +using Measurements: measurement + +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", + "/tmp/linecablemodels-wgl-artifacts" +)) + +mkpath(WGL_ARTIFACT_DIRECTORY) +cd(WGL_ARTIFACT_DIRECTORY) +set_backend!(:wgl) + +frequency = [10.0, 50.0, 100.0, 500.0, 1_000.0, 10_000.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 +) + +measurement_parameters = LineParameters( + complex.( + measurement.(resistance_values, resistance_values .* 0.05), + measurement.(inductance_values .* omega, inductance_values .* omega .* 0.05) + ), + complex.( + measurement.(conductance_values, conductance_values .* 0.05), + measurement.(capacitance_values .* omega, capacitance_values .* omega .* 0.05) + ), + frequency +) + +summary = SampleSummary([1.0, 2.0, 3.0, 4.0]) +distribution_model = HistogramPDF([1.0, 3.0, 5.0], [0.25, 0.25]) +mc_result = CableConstantsMC( + CableConstants(summary, summary, summary), + CableConstants( + [1.0, 2.0, 3.0, 4.0], + [1.0, 2.0, 3.0, 4.0], + [1.0, 2.0, 3.0, 4.0] + ), + CableConstants(distribution_model, distribution_model, distribution_model), + CableConstants( + measurement(2.5, 0.5), + measurement(2.5, 0.5), + measurement(2.5, 0.5) + ), + 4, + 0.95 +) + +gallery = Pair{String, UIPlot}[] +function add_pages!(title, handles) + for (index, handle) in enumerate(handles) + push!(gallery, "$title — page $index" => handle) + end + return handles +end + +add_pages!( + "Line parameters: RLCG", + Makie.plot(parameters; mode = :RLCG, backend = :wgl, display_plot = false) +) +add_pages!( + "Line parameters: Z/Y Cartesian", + Makie.plot( + parameters; + mode = :ZY, + coord = :cart, + backend = :wgl, + display_plot = false + ) +) +add_pages!( + "Line parameters: Z/Y polar", + Makie.plot( + parameters; + mode = :ZY, + coord = :polar, + backend = :wgl, + display_plot = false + ) +) +add_pages!( + "Line parameters: measurement error bars", + Makie.plot( + measurement_parameters; + mode = :RLCG, + backend = :wgl, + display_plot = false + ) +) + +for mode in (:hist, :pdf, :ecdf, :qq) + push!( + gallery, + "Monte Carlo: $mode" => Makie.plot( + mc_result, + :R; + mode, + data = :both, + backend = :wgl, + display_plot = false + ) + ) +end + +library = CablesLibrary() +load!(library; file_name = joinpath(pkgdir(LineCableModels), "test", "cable_test.json")) +design = first(values(library.data)) +push!( + gallery, + "Cable preview" => preview(design; backend = :wgl, display_plot = false) +) + +position = CablePosition( + design, + 0.0, + -0.20, + Dict(component.id => (index == 1 ? 1 : 0) + for (index, component) in enumerate(design.components)) +) +system = LineCableSystem("wgl-manual-system", 1000.0, position) +earth = EarthModel(frequency, 100.0, 10.0, 1.0) +push!( + gallery, + "System preview" => preview( + system; + earth_model = earth, + backend = :wgl, + display_plot = false + ) +) +push!( + gallery, + "Material scale" => show_material_scale(backend = :wgl, display_plot = false) +) + +@assert length(gallery) == 23 +@assert all(pair -> pair.second isa UIPlot, gallery) +@assert all(pair -> pair.second.context.backend === :wgl, gallery) + +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, logarithmic axes, legends, visibility, " * + "zoom/pan, previews, material toggling, histograms, PDFs, ECDFs, and Q-Q plots." + ), + 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/src/engine/plotspecs.jl b/src/engine/plotspecs.jl index e9dec480..63222a68 100644 --- a/src/engine/plotspecs.jl +++ b/src/engine/plotspecs.jl @@ -54,11 +54,10 @@ function _finite_exponent(curves) return abs(exponent) < 3 ? 0 : exponent end -function _axis_label(quantity, unit, exponent::Int) +function _axis_label(quantity, unit) label = UnitHandler.get_label(quantity) unit_label = UnitHandler.get_label(unit) - base = isempty(unit_label) ? label : "$label [$unit_label]" - return exponent == 0 ? base : "$base × 10^$exponent" + return isempty(unit_label) ? label : "$label [$unit_label]" end function _line_pages( @@ -100,7 +99,6 @@ function _line_pages( frequency_target ) x_exponent = _finite_exponent((scaled_frequency,)) - displayed_frequency = scaled_frequency ./ 10.0^x_exponent pairs = _conductor_pairs(object, con) pages = PlotBuilder.PageSpec[] @@ -132,8 +130,8 @@ function _line_pages( series, PlotBuilder.SeriesSpec( :line, - displayed_frequency, - curve ./ 10.0^y_exponent, + scaled_frequency, + curve, nothing, "$symbol[$i,$j]"; attributes = (; linewidth = 2) @@ -145,14 +143,14 @@ function _line_pages( :x, frequency_quantity, frequency_target, - _axis_label(frequency_quantity, frequency_target, x_exponent), + _axis_label(frequency_quantity, frequency_target), xscale ) yaxis = PlotBuilder.AxisSpec( :y, quantity, target_unit, - _axis_label(quantity, target_unit, y_exponent), + _axis_label(quantity, target_unit), yscale ) view_spec = PlotBuilder.ViewSpec( diff --git a/src/plotbuilder/uicomponents/UIComponents.jl b/src/plotbuilder/uicomponents/UIComponents.jl index 82a68595..e27a69f5 100644 --- a/src/plotbuilder/uicomponents/UIComponents.jl +++ b/src/plotbuilder/uicomponents/UIComponents.jl @@ -3,6 +3,7 @@ module UIComponents using Makie using Measurements using Dates +using Printf: @sprintf import LineCableModels.PlotBuilder using LineCableModels.PlotBuilder: @@ -12,9 +13,28 @@ const BackendHandler = PlotBuilder.BackendHandler export build, export_svg -const FIGURE_PADDING = (80, 60, 40, 40) -const LEGEND_WIDTH = 220 +const FIGURE_PADDING = (20, 20, 28, 28) const COLORBAR_WIDTH = 160 +const GRID_ROW_GAP = 6 +const GRID_COLUMN_GAP = 6 +const LEGEND_GAP = 4 +const TOOLBAR_HEIGHT = 36 +const STATUSBAR_HEIGHT = 20 +const BUTTON_SIZE = 32 +const BUTTON_ICON_SIZE = 18 +const BACKGROUND_INTERACTIVE = :grey90 +const BACKGROUND_EXPORT = :white +const BUTTON_BACKGROUND = Makie.RGBf(0.94, 0.94, 0.94) +const ICON_COLOR = Makie.RGBAf(0.15, 0.15, 0.15, 1.0) +const MI_REFRESH = "\uE5D5" +const MI_SAVE = "\uE161" +const ICON_FONT = joinpath( + pkgdir(PlotBuilder), + "assets", + "fonts", + "material-icons", + "MaterialIcons-Regular.ttf" +) const EXPORT_TIMESTAMP_FORMAT = "yyyymmdd_HHMMSS" mutable struct UIContext @@ -33,9 +53,10 @@ struct UIPanel end function _theme(; export_mode::Bool = false) - background = export_mode ? :white : :grey95 + background = export_mode ? BACKGROUND_EXPORT : BACKGROUND_INTERACTIVE return Theme( backgroundcolor = background, + fonts = (; icons = ICON_FONT), Axis = ( titlesize = 15, xlabelsize = 14, @@ -43,21 +64,94 @@ function _theme(; export_mode::Bool = false) xticklabelsize = 14, yticklabelsize = 14 ), + Button = (; buttoncolor = BUTTON_BACKGROUND), Legend = (; fontsize = 14, labelsize = 14), Colorbar = (; labelsize = 14, ticklabelsize = 14) ) end -function _context(backend, display) - active = BackendHandler.ensure_backend!(backend) +function _context(active::Symbol, display::Bool, title::AbstractString) interactive = display && active in (:gl, :wgl) window = interactive && active === :gl ? - BackendHandler.make_screen("LineCableModels Plot"; backend = :gl) : nothing + BackendHandler.make_screen( + "Fig. $(BackendHandler.next_fignum()) – $title"; + backend = :gl + ) : nothing return UIContext(active, interactive, window, Observable("Ready.")) end _scale(symbol::Symbol) = symbol === :log10 ? Makie.log10 : Makie.identity +function _linear_tickformat(exponent::Int) + scale = 10.0^exponent + return values -> [@sprintf("%.4g", value / scale) for value in values] +end + +function _log_ticks(vmin, vmax) + isfinite(vmin) && isfinite(vmax) && 0 < vmin <= vmax || return (Float64[], String[]) + values = Float64[] + labels = Any[] + first_exponent = floor(Int, log10(vmin)) + last_exponent = floor(Int, log10(vmax)) + for exponent in first_exponent:last_exponent + decade = 10.0^exponent + for multiplier in 1:9 + value = multiplier * decade + vmin <= value <= vmax || continue + push!(values, value) + push!( + labels, + multiplier == 1 ? + Makie.rich("10", Makie.superscript(string(exponent))) : "" + ) + end + end + return values, labels +end + +function _axis_label(spec::Union{Nothing, AxisSpec}, exponent::Int, scale::Symbol) + spec === nothing && return "" + scale === :log10 && return spec.label + return iszero(exponent) ? spec.label : "$(spec.label) × 10^$exponent" +end + +function _tickformat(exponent::Int, scale::Symbol) + return scale === :log10 ? Makie.automatic : _linear_tickformat(exponent) +end + +_ticks(scale::Symbol) = scale === :log10 ? _log_ticks : Makie.automatic + +function _set_axis_scale!( + axis, spec::Union{Nothing, AxisSpec}, dim::Symbol, exponent::Int, scale::Symbol) + ticks = _ticks(scale) + formatter = _tickformat(exponent, scale) + label = _axis_label(spec, exponent, scale) + if dim === :x + axis.xticks[] = ticks + axis.xtickformat[] = formatter + axis.xlabel[] = label + axis.xscale[] = _scale(scale) + elseif dim === :y + axis.yticks[] = ticks + axis.ytickformat[] = formatter + axis.ylabel[] = label + axis.yscale[] = _scale(scale) + else + throw(ArgumentError("axis dimension must be :x or :y")) + end + return axis +end + +function _icon_label(glyph::AbstractString) + return Makie.rich( + glyph; + font = :icons, + fontsize = BUTTON_ICON_SIZE, + color = ICON_COLOR, + offset = (0, -0.18) + ) +end + function _numeric_values(values) values === nothing && return nothing, nothing nominal = Measurements.value.(values) @@ -139,18 +233,26 @@ function _draw!(axis, series::SeriesSpec) return plots end -function _axis(parent, view::ViewSpec) +function _axis(parent, view::ViewSpec, page::PageSpec) xaxis = view.xaxis yaxis = view.yaxis + x_exponent = get(page.kwargs, :x_exponent, 0) + y_exponent = get(page.kwargs, :y_exponent, 0) + xscale = xaxis === nothing ? :linear : xaxis.scale + yscale = yaxis === nothing ? :linear : yaxis.scale attributes = _without(view.attributes, (:aspect, :limits)) aspect = get(view.attributes, :aspect, nothing) axis = Axis( parent; - xlabel = xaxis === nothing ? "" : xaxis.label, - ylabel = yaxis === nothing ? "" : yaxis.label, + xlabel = _axis_label(xaxis, x_exponent, xscale), + ylabel = _axis_label(yaxis, y_exponent, yscale), title = view.title, - xscale = xaxis === nothing ? Makie.identity : _scale(xaxis.scale), - yscale = yaxis === nothing ? Makie.identity : _scale(yaxis.scale), + xscale = _scale(xscale), + yscale = _scale(yscale), + xticks = _ticks(xscale), + yticks = _ticks(yscale), + xtickformat = _tickformat(x_exponent, xscale), + ytickformat = _tickformat(y_exponent, yscale), aspect = aspect === :data ? DataAspect() : aspect, attributes... ) @@ -233,7 +335,14 @@ function _legend!(slot, panels) push!(entries, groups[group]) push!(labels, group_labels[group]) end - return isempty(entries) ? nothing : Legend(slot, entries, labels; valign = :top) + return isempty(entries) ? nothing : + Legend( + slot, + entries, + labels; + halign = :right, + valign = :top + ) end function _build_page( @@ -244,6 +353,8 @@ function _build_page( export_mode::Bool ) figure = Figure(size = page.size, figure_padding = FIGURE_PADDING) + figure.layout.default_rowgap = Fixed(GRID_ROW_GAP) + figure.layout.default_colgap = Fixed(GRID_COLUMN_GAP) toolbar_row = controls ? 1 : 0 canvas_row = toolbar_row + 1 status_row = canvas_row + 1 @@ -255,9 +366,11 @@ function _build_page( for (index, view) in enumerate(page.views) row = (index - 1) ÷ columns + 1 column = (index - 1) % columns + 1 - push!(panels, _axis(canvas[row, column], view)) + push!(panels, _axis(canvas[row, column], view, page)) end side = GridLayout() + side.default_rowgap = Fixed(LEGEND_GAP) + side.halign = :right side_column = isempty(page.views) ? 1 : 2 figure[canvas_row, side_column] = side if isempty(page.views) @@ -275,7 +388,7 @@ function _build_page( _colorbars!(side[side_row, 1], colorbars) side_row += 1 end - isempty(page.views) || colsize!(figure.layout, 2, Fixed(LEGEND_WIDTH)) + isempty(page.views) || colsize!(figure.layout, 2, Auto(true)) widgets = Dict{Symbol, Any}() plot_reference = Ref{Any}(nothing) @@ -286,10 +399,19 @@ function _build_page( PlotBuilder.control_definitions() ) toolbar = GridLayout() + toolbar.default_colgap = Fixed(4) + toolbar.halign = :left + toolbar.valign = :center figure[1, 1:2] = toolbar column = 1 if definitions.reset - reset = Button(toolbar[1, column], label = "Reset") + reset = Button( + toolbar[1, column]; + label = _icon_label(MI_REFRESH), + width = BUTTON_SIZE, + height = BUTTON_SIZE, + buttoncolor = BUTTON_BACKGROUND + ) column += 1 widgets[:reset] = reset on(reset.clicks) do _ @@ -298,7 +420,13 @@ function _build_page( end end if definitions.export_svg - save_button = Button(toolbar[1, column], label = "Export SVG") + save_button = Button( + toolbar[1, column]; + label = _icon_label(MI_SAVE), + width = BUTTON_SIZE, + height = BUTTON_SIZE, + buttoncolor = BUTTON_BACKGROUND + ) column += 1 widgets[:export_svg] = save_button on(save_button.clicks) do _ @@ -320,8 +448,17 @@ function _build_page( column += 1 widgets[:xlog] = xlog on(xlog.active) do enabled - scale = enabled ? Makie.log10 : Makie.identity - foreach(panel -> panel.axis.xscale[] = scale, panels) + scale = enabled ? :log10 : :linear + foreach( + panel -> _set_axis_scale!( + panel.axis, + panel.view.xaxis, + :x, + get(page.kwargs, :x_exponent, 0), + scale + ), + panels + ) foreach(panel -> autolimits!(panel.axis), panels) context.status[] = enabled ? "x-axis scale set to log" : @@ -338,8 +475,17 @@ function _build_page( Label(toolbar[1, column], "log y") widgets[:ylog] = ylog on(ylog.active) do enabled - scale = enabled ? Makie.log10 : Makie.identity - foreach(panel -> panel.axis.yscale[] = scale, panels) + scale = enabled ? :log10 : :linear + foreach( + panel -> _set_axis_scale!( + panel.axis, + panel.view.yaxis, + :y, + get(page.kwargs, :y_exponent, 0), + scale + ), + panels + ) foreach(panel -> autolimits!(panel.axis), panels) context.status[] = enabled ? "y-axis scale set to log" : @@ -348,9 +494,9 @@ function _build_page( end definitions.legend && legend !== nothing && (widgets[:legend] = legend) Label(figure[status_row, 1:2], context.status; halign = :left, fontsize = 11) - rowsize!(figure.layout, 1, Fixed(48)) + rowsize!(figure.layout, 1, Fixed(TOOLBAR_HEIGHT)) rowsize!(figure.layout, canvas_row, Relative(1)) - rowsize!(figure.layout, status_row, Fixed(24)) + rowsize!(figure.layout, status_row, Fixed(STATUSBAR_HEIGHT)) end built = UIPlot(render_spec, page, figure, panels, widgets, context) @@ -365,10 +511,11 @@ function build( controls::Bool = true, export_mode::Bool = false ) - context = _context(backend, display) + active = BackendHandler.ensure_backend!(backend) built = UIPlot[] with_theme(_theme(; export_mode)) do for page in render_spec.figures + context = _context(active, display, page.title) plot = _build_page( render_spec, page, diff --git a/test/plotting.jl b/test/plotting.jl index c9189e79..b58821cc 100644 --- a/test/plotting.jl +++ b/test/plotting.jl @@ -63,6 +63,8 @@ @test length(rlcg) == 4 @test length(cartesian) == 4 @test length(polar) == 4 + @test rlcg[1].context !== rlcg[2].context + @test rlcg[1].context.status !== rlcg[2].context.status series_plots = Makie.plot( parameters.Z, frequency; @@ -105,6 +107,16 @@ test_golden(measurement_plot, "line_measurements") handle = first(rlcg) + @test handle.figure.scene.backgroundcolor[] == Makie.to_color(:grey90) + padding = handle.figure.layout.alignmode[].padding + @test (padding.left, padding.right, padding.bottom, padding.top) == + (20.0, 20.0, 28.0, 28.0) + @test handle.figure.layout.colsizes[2] == Makie.Auto(true) + @test handle.figure.layout.addedrowgaps == fill(Makie.Fixed(6), 2) + @test handle.figure.layout.addedcolgaps == [Makie.Fixed(6)] + @test handle.controls[:reset].buttoncolor[] == Makie.RGBf(0.94, 0.94, 0.94) + @test occursin("\\ue5d5", sprint(show, handle.controls[:reset].label[])) + @test occursin("\\ue161", sprint(show, handle.controls[:export_svg].label[])) handle.controls[:xlog].active[] = true @test only(handle.panels).axis.xscale[] === Makie.log10 handle.controls[:ylog].active[] = true @@ -130,6 +142,31 @@ Makie.toggle_visibility!(first_entry) @test all(plot_object -> plot_object.visible[], only(handle.panels).plots) + susceptance_handle = last(cartesian) + susceptance_axis = only(susceptance_handle.panels).axis + @test occursin("× 10^-3", susceptance_axis.ylabel[]) + susceptance_handle.controls[:ylog].active[] = true + @test susceptance_axis.yscale[] === Makie.log10 + @test susceptance_axis.ytickformat[] === Makie.automatic + @test susceptance_axis.ylabel[] == "Capacitive susceptance [S/km]" + limits = susceptance_axis.finallimits[] + ymin = limits.origin[2] + ymax = ymin + limits.widths[2] + tick_values, tick_labels = Makie.get_ticks( + susceptance_axis.yticks[], + susceptance_axis.yscale[], + susceptance_axis.ytickformat[], + ymin, + ymax + ) + @test length(tick_values) > 2 + @test length(unique(round.(diff(log10.(tick_values)); digits = 8))) > 1 + @test all(label -> label == "" || label isa Makie.RichText, tick_labels) + @test all( + label -> label == "" || !occursin(r"\d+\.\d+", sprint(show, label)), + tick_labels + ) + mktempdir() do directory svg_path = joinpath(directory, "line parameters.svg") exported = export_svg(handle; path = svg_path) diff --git a/test/reference/cable_preview.png b/test/reference/cable_preview.png index e732996cd3f824aacdf18aed32aa3962577478bf..bee6c6db019bccde2068568d92440f68878e166c 100644 GIT binary patch literal 477789 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

Uz2|+rL|)|L*^TGN^>i= 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�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 (index == 1 ? 1 : 0) + for + (index, component) in enumerate(design.components)) +) +system = LineCableSystem("reference-system", 1000.0, position) +earth = EarthModel(frequency, 100.0, 10.0, 1.0) +save_reference( + "system_preview", + preview( + system; + earth_model = earth, + backend = :cairo, + display_plot = false + ) +) +save_reference( + "material_scale", + show_material_scale(backend = :cairo, display_plot = false) +) diff --git a/test/reference/line_measurements.png b/test/reference/line_measurements.png new file mode 100644 index 0000000000000000000000000000000000000000..d7a8f5d26b378d6de7fd344f218cc951887c8dfd GIT binary patch literal 69100 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

""" diff --git a/src/commons/Commons.jl b/src/commons/Commons.jl index 552ac138..c4969ff4 100644 --- a/src/commons/Commons.jl +++ b/src/commons/Commons.jl @@ -2,6 +2,7 @@ module Commons include("docstringextension.jl") include("consts.jl") +include("retired.jl") export get_description, add!, domain, basis, Z, Y, R, X, L, G, B, C, diff --git a/src/commons/retired.jl b/src/commons/retired.jl new file mode 100644 index 00000000..2b553382 --- /dev/null +++ b/src/commons/retired.jl @@ -0,0 +1,21 @@ +const LEGACY_FEM_SECTOR_BRANCH = "legacy/fem-sector" +const LEGACY_FEM_SECTOR_COMMIT = "b75dd2723f90a83ec090b20605ea42af57f4a9c3" +const LEGACY_FEM_SECTOR_URL = "https://github.com/Electa-Git/LineCableModels.jl.git" +const LEGACY_FEM_SECTOR_INSTALL = "julia --project=. -e \"using Pkg; " * + "Pkg.add(Pkg.PackageSpec(url=ARGS[1], " * + "rev=ARGS[2]))\" " * + LEGACY_FEM_SECTOR_URL * " " * + LEGACY_FEM_SECTOR_COMMIT + +function retired_fem_sector(feature::AbstractString) + throw( + ArgumentError( + "$feature was removed from LineCableModels. " * + "The final FEM and sector-support snapshot is branch " * + "$(LEGACY_FEM_SECTOR_BRANCH) at commit " * + "$(LEGACY_FEM_SECTOR_COMMIT).\n\n" * + "Install that snapshot in the current Julia project with:\n" * + LEGACY_FEM_SECTOR_INSTALL, + ), + ) +end diff --git a/src/datamodel/DataModel.jl b/src/datamodel/DataModel.jl index e0b96d83..804c2708 100644 --- a/src/datamodel/DataModel.jl +++ b/src/datamodel/DataModel.jl @@ -20,8 +20,9 @@ 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 CircStrands, RectStrands, Strip, Tubular # Conductor types +export Semicon, Insulator # Insulator types +export SectorParams, Sector, SectorInsulator # Removed API tombstones export ConductorGroup, InsulatorGroup # Group types export CableComponent, CableDesign, CableConstants # Cable design types export CablePosition, LineCableSystem # System types @@ -35,6 +36,7 @@ import ..PlotBuilder import ..UnitHandler import ..Commons: add! import ..Commons: basis, R, L, C, resistance, inductance, capacitance +import ..Commons: retired_fem_sector using ..Utils: resolve_T, to_certain, to_nominal, is_headless, is_in_testset, to_lower, to_upper @@ -64,6 +66,7 @@ using .BaseParams # Constructors include("macros.jl") include("validation.jl") +include("retired.jl") # Conductors include("strands_handler.jl") @@ -72,13 +75,11 @@ 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") diff --git a/src/datamodel/cableslibrary/vdeparse.jl b/src/datamodel/cableslibrary/vdeparse.jl index d8cf6ff6..ac956923 100644 --- a/src/datamodel/cableslibrary/vdeparse.jl +++ b/src/datamodel/cableslibrary/vdeparse.jl @@ -116,11 +116,11 @@ 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_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", + 'R' => "round", 'O' => "oval", 'E' => "solid", 'M' => "stranded", 'H' => "hollow", 'V' => "compact" ) diff --git a/src/datamodel/conductorgroup.jl b/src/datamodel/conductorgroup.jl index 40a8cfb7..d6bdcb79 100644 --- a/src/datamodel/conductorgroup.jl +++ b/src/datamodel/conductorgroup.jl @@ -13,15 +13,6 @@ properties that represent the composite behavior of the entire assembly. $(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 diff --git a/src/datamodel/plotspecs.jl b/src/datamodel/plotspecs.jl index 2e28f970..1b06ed3f 100644 --- a/src/datamodel/plotspecs.jl +++ b/src/datamodel/plotspecs.jl @@ -206,16 +206,6 @@ function _layer_series!(series, layer, label, group, xcenter, ycenter; include_l _layer_series!(series, nested, label, group, xcenter, ycenter; include_label = index == 1 && include_label) end - elseif layer isa Sector - geometry = Point2f[(vertex[1] + xcenter, vertex[2] + ycenter) - for vertex in layer.vertices] - push!(series, _polygon_series(geometry, first_label, group, color)) - elseif layer isa SectorInsulator - outer = Point2f[(vertex[1] + xcenter, vertex[2] + ycenter) - for vertex in layer.outer_vertices] - inner = reverse(Point2f[(vertex[1] + xcenter, vertex[2] + ycenter) - for vertex in layer.inner_sector.vertices]) - push!(series, _polygon_series(Polygon(outer, [inner]), first_label, group, color)) else @warn "unsupported cable-preview layer" layer_type = typeof(layer) end diff --git a/src/datamodel/rectstrands.jl b/src/datamodel/rectstrands.jl index 790d2535..2c4856c7 100644 --- a/src/datamodel/rectstrands.jl +++ b/src/datamodel/rectstrands.jl @@ -46,23 +46,6 @@ struct RectStrands{T <: REALSCALAR, S <: RectStrandsShape} <: AbstractStrandsLay 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 diff --git a/src/datamodel/retired.jl b/src/datamodel/retired.jl new file mode 100644 index 00000000..a0f9299c --- /dev/null +++ b/src/datamodel/retired.jl @@ -0,0 +1,29 @@ +""" +$(TYPEDSIGNATURES) + +Report that sector-shaped cable support was removed and identify the final +legacy snapshot. +""" +function SectorParams(args...; kwargs...) + retired_fem_sector("SectorParams") +end + +""" +$(TYPEDSIGNATURES) + +Report that sector-shaped cable support was removed and identify the final +legacy snapshot. +""" +function Sector(args...; kwargs...) + retired_fem_sector("Sector") +end + +""" +$(TYPEDSIGNATURES) + +Report that sector-shaped cable support was removed and identify the final +legacy snapshot. +""" +function SectorInsulator(args...; kwargs...) + retired_fem_sector("SectorInsulator") +end diff --git a/src/datamodel/sector.jl b/src/datamodel/sector.jl deleted file mode 100644 index 1627b04d..00000000 --- a/src/datamodel/sector.jl +++ /dev/null @@ -1,412 +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 -function Validation.coercive_fields(::Type{SectorParams}) - (:r_back, :d_sector, :r_corner, :theta_cond_deg, :d_insulation) -end - -# --- 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 - -function 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) - ) -end - -@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 diff --git a/src/datamodel/sectorinsulator.jl b/src/datamodel/sectorinsulator.jl deleted file mode 100644 index 8d60b65b..00000000 --- a/src/datamodel/sectorinsulator.jl +++ /dev/null @@ -1,129 +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 diff --git a/src/datamodel/types.jl b/src/datamodel/types.jl index a4245927..6c8b5913 100644 --- a/src/datamodel/types.jl +++ b/src/datamodel/types.jl @@ -82,5 +82,5 @@ function (::Type{T})(args::Vararg{Any, N}; kwargs...) where {T <: AbstractCableP ) end -### Provisions for the new types currently under development: RectStrandsShape and SectorShape +### Provisions for the new rectangular-strand geometry abstract type AbstractShapeGeometry end diff --git a/src/engine/Engine.jl b/src/engine/Engine.jl index bdca385e..0aed910e 100644 --- a/src/engine/Engine.jl +++ b/src/engine/Engine.jl @@ -40,6 +40,7 @@ using Measurements using LinearAlgebra using ..Commons import ..Commons: get_description, LineParamsDomain, PhaseDomain, ModalDomain, domain +import ..Commons: retired_fem_sector import ..Commons: basis, Z, Y, R, X, L, G, B, C, series_impedance, shunt_admittance, resistance, reactance, inductance, @@ -122,8 +123,8 @@ function plot(args...; kwargs...) ) end -# Submodule `FEM` -include("fem/FEM.jl") +# Removed FEM entry points +include("retired.jl") @reexport using .InternalImpedance: InternalImpedance @reexport using .InsulationImpedance: InsulationImpedance diff --git a/src/engine/fem/FEM.jl b/src/engine/fem/FEM.jl deleted file mode 100644 index 315b3280..00000000 --- a/src/engine/fem/FEM.jl +++ /dev/null @@ -1,89 +0,0 @@ -""" - LineCableModels.Engine.FEM - -Provide the deprecated compatibility facade for the optional finite-element -integration. Load `Gmsh` before constructing or running an FEM formulation. -""" -module FEM - -export Darwin, Electrodynamics, FormulationSet, MeshTransition, calc_domain_size, - compute!, preview_results - -import ...Engine: FormulationSet, compute! - -const _DEPRECATION_MESSAGE = "The current FEM interface is deprecated and will be replaced by a simplified API in a future release." - -_parent_package() = parentmodule(parentmodule(@__MODULE__)) - -function _extension() - ext = Base.get_extension(_parent_package(), :LineCableModelsGmshExt) - ext === nothing && throw( - ArgumentError( - "FEM is optional. Load Gmsh with `using Gmsh` before using LineCableModels.Engine.FEM.", - ), - ) - return ext -end - -function _warn_fem(symbol::Symbol) - Base.depwarn(_DEPRECATION_MESSAGE, symbol) - return nothing -end - -function Darwin(args...; kwargs...) - _warn_fem(:Darwin) - return _extension().Darwin(args...; kwargs...) -end - -function Electrodynamics(args...; kwargs...) - _warn_fem(:Electrodynamics) - return _extension().Electrodynamics(args...; kwargs...) -end - -function MeshTransition(args...; kwargs...) - _warn_fem(:MeshTransition) - return _extension().MeshTransition(args...; kwargs...) -end - -""" - calc_domain_size(earth_model, frequencies; min_radius=5.0, max_radius=5000.0) - -Estimate the radial FEM domain size from the most resistive finite earth layer. - -# Arguments - -- `earth_model`: Earth model containing frequency-dependent resistivity and - permeability. -- `frequencies`: FEM frequencies in hertz. - -# Returns - -- Domain radius in metres, clamped to `min_radius:max_radius`. - -# Notes - -The implementation evaluates the magnitude of the complex skin depth at the -first frequency, - -```math -\\delta = \\left|\\sqrt{\\frac{\\rho}{\\mathrm{j}\\,2\\pi f\\mu}}\\right|, -``` - -using the finite earth layer with the largest resistivity. -""" -function calc_domain_size(args...; kwargs...) - _warn_fem(:calc_domain_size) - return _extension().calc_domain_size(args...; kwargs...) -end - -function preview_results(args...; kwargs...) - _warn_fem(:preview_results) - return _extension().preview_results(args...; kwargs...) -end - -function FormulationSet(::Val{:FEM}; kwargs...) - _warn_fem(:FormulationSet) - return _extension().formulation_set(; kwargs...) -end - -end # module FEM diff --git a/src/engine/fem/base.jl b/src/engine/fem/base.jl deleted file mode 100644 index 47b415fd..00000000 --- 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 4ace7ea5..00000000 --- a/src/engine/fem/cable.jl +++ /dev/null @@ -1,688 +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 ea5e29e3..00000000 --- a/src/engine/fem/drawing.jl +++ /dev/null @@ -1,856 +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 a5d639ac..00000000 --- a/src/engine/fem/encoding.jl +++ /dev/null @@ -1,484 +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 1bcc8558..00000000 --- a/src/engine/fem/helpers.jl +++ /dev/null @@ -1,283 +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 - -function _resolve_getdp_path(::NamedTuple) - path = GetDP.get_getdp_executable() - _is_valid_getdp_exe(path) || - Base.error( - "GetDP failed its -info probe at $(repr(path)). " * - "Set GETDP_EXECUTABLE to a working executable or add getdp to PATH.", - ) - return path -end diff --git a/src/engine/fem/identification.jl b/src/engine/fem/identification.jl deleted file mode 100644 index 6a9ccd22..00000000 --- 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 108de3fd..00000000 --- a/src/engine/fem/lineparamopts.jl +++ /dev/null @@ -1,35 +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) -function Base.get(o::FEMOptions, s::Symbol, default) - hasproperty(o, s) ? getproperty(o, s) : default -end -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 50c48a23..00000000 --- a/src/engine/fem/materialprops.jl +++ /dev/null @@ -1,125 +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 8d0cec03..00000000 --- a/src/engine/fem/mesh.jl +++ /dev/null @@ -1,347 +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 736b8e80..00000000 --- a/src/engine/fem/meshtransitions.jl +++ /dev/null @@ -1,107 +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 e15d6744..00000000 --- a/src/engine/fem/problemdefs.jl +++ /dev/null @@ -1,187 +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 formulation_set(; 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 9ee6e046..00000000 --- a/src/engine/fem/solver.jl +++ /dev/null @@ -1,858 +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 787009cb..00000000 --- a/src/engine/fem/space.jl +++ /dev/null @@ -1,362 +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 5f060a28..00000000 --- a/src/engine/fem/types.jl +++ /dev/null @@ -1,140 +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 diff --git a/src/engine/fem/visualization.jl b/src/engine/fem/visualization.jl deleted file mode 100644 index 814e6d81..00000000 --- a/src/engine/fem/visualization.jl +++ /dev/null @@ -1,159 +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 diff --git a/src/engine/fem/workspace.jl b/src/engine/fem/workspace.jl deleted file mode 100644 index f9e3256b..00000000 --- a/src/engine/fem/workspace.jl +++ /dev/null @@ -1,279 +0,0 @@ -import LineCableModels.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/retired.jl b/src/engine/retired.jl new file mode 100644 index 00000000..5426f274 --- /dev/null +++ b/src/engine/retired.jl @@ -0,0 +1,19 @@ +function FormulationSet(::Val{:FEM}; kwargs...) + retired_fem_sector("FormulationSet(:FEM)") +end + +module FEM + +import ...Commons: retired_fem_sector +import ...Engine: FormulationSet + +export Darwin, Electrodynamics, FormulationSet, MeshTransition, calc_domain_size, + preview_results + +Darwin(args...; kwargs...) = retired_fem_sector("Darwin") +Electrodynamics(args...; kwargs...) = retired_fem_sector("Electrodynamics") +MeshTransition(args...; kwargs...) = retired_fem_sector("MeshTransition") +calc_domain_size(args...; kwargs...) = retired_fem_sector("calc_domain_size") +preview_results(args...; kwargs...) = retired_fem_sector("preview_results") + +end diff --git a/src/importexport/cableslibrary.jl b/src/importexport/cableslibrary.jl index b1d0b6ba..3a51d1c5 100644 --- a/src/importexport/cableslibrary.jl +++ b/src/importexport/cableslibrary.jl @@ -383,30 +383,6 @@ function _reconstruct_partsgroup(layer_data::Dict) 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 diff --git a/test/test_tutorial_2_sector.jl b/test/test_tutorial_2_sector.jl deleted file mode 100644 index 53196a2c..00000000 --- a/test/test_tutorial_2_sector.jl +++ /dev/null @@ -1,180 +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 - - if get(ENV, "LINECABLEMODELS_TEST_PLOTTING", "false") == "true" - @test preview(design, display_plot = false) isa Any - end - 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 From d259070fa2d73e7e4903740c8ff0e06fe458ff2b Mon Sep 17 00:00:00 2001 From: amaurigmartins Date: Sun, 16 Aug 2026 06:19:00 +0200 Subject: [PATCH 035/157] refactor(engine): replace prototype uq and parametric --- .github/workflows/CI.yml | 3 + .gitignore | 3 - CHANGELOG.md | 28 +- Project.toml | 17 +- README.md | 51 +- docs/literate/plotbuilder.jl | 49 +- docs/make.jl | 8 +- docs/src/CHANGELOG.md | 28 +- docs/src/docstrings.md | 34 +- docs/src/index.md | 9 +- docs/src/plotbuilder.md | 49 +- docs/src/reference.md | 177 ++-- docs/src/validation.md | 273 ++--- examples/tutorial1.jl | 48 +- examples/tutorial2.jl | 338 ++++--- examples/tutorial3.jl | 332 ++++--- ext/LineCableModelsDistributionsExt.jl | 157 +++ ext/LineCableModelsMakieExt.jl | 93 +- ext/LineCableModelsMeasurementsExt.jl | 210 ++++ integration/plotting/_gallery_fixtures.jl | 42 +- integration/plotting/manual_gl.jl | 19 +- showcase/showcase1.jl | 86 +- src/LineCableModels.jl | 89 +- src/commons/Commons.jl | 4 +- src/commons/consts.jl | 8 +- src/computation/Computation.jl | 939 ++++++++++++++++++ src/computation/dataframe.jl | 150 +++ src/computation/plotspecs.jl | 656 ++++++++++++ src/datamodel/DataModel.jl | 7 +- src/datamodel/baseparams/BaseParams.jl | 58 +- src/datamodel/cabledesign/cableconstants.jl | 20 +- src/datamodel/cabledesign/dataframe.jl | 89 +- src/datamodel/cableslibrary/vdeparse.jl | 2 +- src/datamodel/circstrands.jl | 9 +- src/datamodel/conductorgroup.jl | 78 +- src/datamodel/insulator.jl | 20 +- src/datamodel/insulatorgroup.jl | 86 +- src/datamodel/plotspecs.jl | 64 +- src/datamodel/radii.jl | 183 +--- src/datamodel/rectstrands.jl | 8 - src/datamodel/semicon.jl | 22 +- src/datamodel/strip.jl | 34 +- src/datamodel/tubular.jl | 18 +- src/datamodel/types.jl | 35 - src/datamodel/validation.jl | 97 +- src/earthprops/EarthProps.jl | 2 - src/engine/Engine.jl | 33 +- src/engine/base.jl | 1 - src/engine/dataframe.jl | 92 +- src/engine/earthadmittance/EarthAdmittance.jl | 1 - src/engine/earthadmittance/homogeneous.jl | 2 +- src/engine/earthimpedance/EarthImpedance.jl | 1 - src/engine/earthimpedance/homogeneous.jl | 2 +- src/engine/ehem/EHEM.jl | 1 - src/engine/helpers.jl | 53 +- .../InsulationAdmittance.jl | 1 - src/engine/insulationadmittance/parallelrc.jl | 30 +- .../InsulationImpedance.jl | 1 - .../internalimpedance/InternalImpedance.jl | 3 +- src/engine/lineparamopts.jl | 131 ++- src/engine/lineparams.jl | 16 +- src/engine/plotspecs.jl | 351 ++++++- src/engine/problemdefs.jl | 43 +- src/engine/retired.jl | 8 +- src/engine/solver.jl | 57 +- src/engine/transforms/Transforms.jl | 1 - src/engine/types.jl | 27 +- src/engine/workspace.jl | 17 +- src/importexport/ImportExport.jl | 1 - src/importexport/deserialize.jl | 15 +- src/importexport/serialize.jl | 9 - src/importexport/xlsx.jl | 3 - src/materials/Materials.jl | 1 - src/materials/materialslibrary.jl | 9 +- src/parametricbuilder/ParametricBuilder.jl | 102 +- src/parametricbuilder/base.jl | 323 ------ src/parametricbuilder/cablebuilderspec.jl | 935 ++++++++--------- src/parametricbuilder/determinize.jl | 150 --- src/parametricbuilder/gridspace/grid.jl | 297 ++++++ src/parametricbuilder/gridspace/gridspace.jl | 403 ++++++++ src/parametricbuilder/gridspace/macros.jl | 256 +++++ src/parametricbuilder/groupspec.jl | 211 ---- src/parametricbuilder/materialspec.jl | 150 +-- src/parametricbuilder/parametricsweep.jl | 202 ---- src/parametricbuilder/positionspec.jl | 284 ++++-- src/parametricbuilder/systembuilderspec.jl | 390 +++++--- src/plotbuilder/PlotBuilder.jl | 6 +- src/plotbuilder/grammar.jl | 3 +- src/plotbuilder/types.jl | 4 +- src/plotbuilder/uicomponents/UIComponents.jl | 14 +- src/uncertainbessels/UncertainBessels.jl | 225 ----- src/unithandler/UnitHandler.jl | 24 +- src/uq/UQ.jl | 364 ------- src/uq/dataframe.jl | 63 -- src/uq/distributions.jl | 240 ----- src/uq/montecarlo.jl | 496 --------- src/uq/plotspecs.jl | 519 ---------- src/uq/types.jl | 520 ---------- src/utils/Utils.jl | 74 +- src/utils/typecoercion.jl | 373 ++----- src/validation/Validation.jl | 22 +- src/validation/rules.jl | 2 +- test/datamodel.jl | 53 +- test/plotting.jl | 151 +-- test/reference/cable_preview.png | Bin 471031 -> 523985 bytes test/reference/custom_layout.png | Bin 103899 -> 102617 bytes test/reference/generate.jl | 46 +- test/reference/line_measurements.png | Bin 62824 -> 96298 bytes test/reference/line_rlcg.png | Bin 61720 -> 96726 bytes test/reference/line_zy_cartesian.png | Bin 61720 -> 106086 bytes test/reference/line_zy_polar.png | Bin 108938 -> 152632 bytes test/reference/mc_ecdf.png | Bin 67511 -> 68368 bytes test/reference/mc_pdf.png | Bin 49213 -> 53258 bytes test/reference/system_preview.png | Bin 306519 -> 305019 bytes test/runtests.jl | 10 +- test/test_tutorial1.jl | 56 +- test/test_tutorial2.jl | 449 ++++----- test/test_tutorial3.jl | 291 ++++++ .../test_calc_equivalent_gmr.jl | 2 +- .../test_calc_equivalent_mu.jl | 2 +- test/unit_BaseParams/test_calc_gmd.jl | 20 +- .../test_calc_helical_params.jl | 2 +- .../test_calc_inductance_trifoil.jl | 34 +- .../test_calc_solenoid_correction.jl | 2 +- test/unit_DataModel/test_CableComponent.jl | 34 +- test/unit_DataModel/test_CableDesign.jl | 30 +- test/unit_DataModel/test_ConductorGroup.jl | 15 +- test/unit_DataModel/test_InsulatorGroup.jl | 10 +- test/unit_DataModel/test_Tubular.jl | 36 +- test/unit_DataModel/test_equivalent.jl | 14 +- test/unit_DataModel/test_plot_specs.jl | 7 + .../test_uncertainty_covariance.jl | 8 +- .../test_parallel_rc_insulation.jl | 446 ++++----- test/unit_Engine/test_result_containers.jl | 701 ++----------- .../test_architecture.jl | 342 +++++++ test/unit_ParametricBuilder/test_gridspace.jl | 162 +++ .../test_parametric_sweep.jl | 95 -- .../test_parametricbuilder.jl | 395 -------- .../test_uq_joint_measurements.jl | 73 -- .../test_uq_trial_sampler.jl | 53 - test/unit_PlotBuilder/test_recipe_grammar.jl | 53 +- 141 files changed, 7720 insertions(+), 8166 deletions(-) create mode 100644 ext/LineCableModelsDistributionsExt.jl create mode 100644 ext/LineCableModelsMeasurementsExt.jl create mode 100644 src/computation/Computation.jl create mode 100644 src/computation/dataframe.jl create mode 100644 src/computation/plotspecs.jl delete mode 100644 src/parametricbuilder/base.jl delete mode 100644 src/parametricbuilder/determinize.jl create mode 100644 src/parametricbuilder/gridspace/grid.jl create mode 100644 src/parametricbuilder/gridspace/gridspace.jl create mode 100644 src/parametricbuilder/gridspace/macros.jl delete mode 100644 src/parametricbuilder/groupspec.jl delete mode 100644 src/parametricbuilder/parametricsweep.jl delete mode 100644 src/uncertainbessels/UncertainBessels.jl delete mode 100644 src/uq/UQ.jl delete mode 100644 src/uq/dataframe.jl delete mode 100644 src/uq/distributions.jl delete mode 100644 src/uq/montecarlo.jl delete mode 100644 src/uq/plotspecs.jl delete mode 100644 src/uq/types.jl create mode 100644 test/test_tutorial3.jl create mode 100644 test/unit_ParametricBuilder/test_architecture.jl create mode 100644 test/unit_ParametricBuilder/test_gridspace.jl delete mode 100644 test/unit_ParametricBuilder/test_parametric_sweep.jl delete mode 100644 test/unit_ParametricBuilder/test_parametricbuilder.jl delete mode 100644 test/unit_ParametricBuilder/test_uq_joint_measurements.jl delete mode 100644 test/unit_ParametricBuilder/test_uq_trial_sampler.jl diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 06955273..019b7371 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -63,6 +63,9 @@ jobs: Pkg.develop(PackageSpec(path = pwd())) Pkg.add(PackageSpec(name = "Aqua", version = "0.8")) Pkg.add(PackageSpec(name = "CairoMakie", version = "0.15")) + Pkg.add(PackageSpec(name = "Calculus", version = "0.5")) + Pkg.add(PackageSpec(name = "Distributions", version = "0.25")) + Pkg.add(PackageSpec(name = "Measurements", version = "2")) Pkg.add(PackageSpec(name = "TestItemRunner", version = "1")) pushfirst!(LOAD_PATH, pwd()) include(joinpath(pwd(), "test", "runtests.jl")) diff --git a/.gitignore b/.gitignore index 4d875990..5e8579ba 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,3 @@ test/jl_* *.sh .vscode/ designs/ -src/parametricbuilder/gridspace/grid.jl -src/parametricbuilder/gridspace/gridspace.jl -src/parametricbuilder/gridspace/macros.jl diff --git a/CHANGELOG.md b/CHANGELOG.md index 50eccbeb..e8888198 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,29 @@ and versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Changed + +- Replaced the prototype parameter and uncertainty pipelines with the typed + `Grid`, `AbstractSpec`, and `Gridspace` grammar. +- Unified ordinary, full-parametric, and conditional Monte Carlo execution + under `compute!(problem, Formulation(); run=...)`. +- Made the declarative builder the default modeling API while retaining strict + materialized constructors through explicit submodule imports. +- 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` produces separate Z and Y figures with real and imaginary + parts in side-by-side panels. + +### Removed + +- Removed the former parameter tuple grammar, duplicate execution entrypoints, + specialized analysis containers, and radial proxy wrapper types. +- Removed the `mode=:ZY`/`:RLCG` and `coord=:cart`/`:polar` keywords from + line-parameter presentation. + ## [0.2.0] - 2026-08-13 ### Added @@ -15,10 +38,7 @@ and versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Aqua, SciML formatting, gitlint, clean-install, and modular documentation checks. - Citation and contribution metadata. -- Type-stable `CableConstants`, `SampleSummary`, `RLCG`, `CableConstantsMC`, - `LineParametersMC`, and `HistogramPDF` result containers. -- A type-stable `ParametricSweep` container for ordered deterministic cases and - their results, ready for later GridSpace integration. +- Type-stable primitive and statistical result containers. - A single declarative PlotBuilder renderer with interactive legends and one-click, non-overwriting SVG export. diff --git a/Project.toml b/Project.toml index 8ebdcbd3..03642893 100644 --- a/Project.toml +++ b/Project.toml @@ -5,11 +5,9 @@ authors = ["Amauri Martins"] git_url = "https://github.com/Electa-Git/LineCableModels.jl" [deps] -Calculus = "49dc2e85-a5d0-5ad3-a950-438e2897f1b9" Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" -Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" EzXML = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615" ForceImport = "9dda63f9-cce7-5873-89fa-eccbb2fffcde" @@ -19,7 +17,6 @@ LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" LoggingExtras = "e6f89c97-d47a-5376-807f-9c37f3926c36" MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" -Measurements = "eff96d63-e80a-5855-80a2-b1b0885c5ab7" NLsolve = "2774e3e8-f4cf-5e23-947b-6d7e65073b56" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" @@ -27,22 +24,27 @@ QuadGK = "1fd47b50-473d-5c70-9696-f719f8f3bcdc" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Reexport = "189a3867-3050-52da-a836-e630ba90ab69" Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" +SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" -StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" XLSX = "fdbf4ff8-1666-58a4-91e7-1b58723a45e0" [weakdeps] +Calculus = "49dc2e85-a5d0-5ad3-a950-438e2897f1b9" CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" +Measurements = "eff96d63-e80a-5855-80a2-b1b0885c5ab7" WGLMakie = "276b4fcb-3e11-5398-bf8b-a0c2d153d008" [extensions] LineCableModelsCairoMakieExt = ["Makie", "CairoMakie"] LineCableModelsGLMakieExt = ["Makie", "GLMakie"] LineCableModelsMakieExt = "Makie" +LineCableModelsMeasurementsExt = ["Measurements", "Calculus"] +LineCableModelsDistributionsExt = "Distributions" LineCableModelsWGLMakieExt = ["Makie", "WGLMakie"] [compat] @@ -72,9 +74,9 @@ QuadGK = "2.11.2" Random = "1.11.0" Reexport = "1.2.2" Serialization = "1.11.0" +SHA = "0.7" SpecialFunctions = "2.5.0" Statistics = "1.11.1" -StatsBase = "0.34.7" Tables = "1.12.1" Test = "1.11.0" TestItemRunner = "1.1.0" @@ -84,8 +86,11 @@ julia = "1.12" [extras] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +Calculus = "49dc2e85-a5d0-5ad3-a950-438e2897f1b9" +Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" +Measurements = "eff96d63-e80a-5855-80a2-b1b0885c5ab7" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" [targets] -test = ["Test", "TestItemRunner", "Aqua"] +test = ["Test", "TestItemRunner", "Aqua", "Calculus", "Distributions", "Measurements"] diff --git a/README.md b/README.md index 616c9194..a1859da6 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,9 @@ material data. - Frequency-dependent series impedance and shunt admittance calculations. - Earth-return, modal transformation, ATPDraw, and PSCAD integration. - Material and cable libraries with JSON import and export. +- One typed `Grid`/`Gridspace` grammar for deterministic and uncertain designs. +- Ordinary, full-parametric, and conditional Monte Carlo execution through + `compute!`. - Optional Makie plotting through explicit backend extensions. ## Installation @@ -39,6 +42,29 @@ Core usage has no plotting dependency: using LineCableModels ``` +The default modeling API is declarative: + +```julia +copper = Material(; rho=1.7241e-8) +xlpe = Material(; rho=1.97e14, eps_r=2.5) + +design = CableBuilder( + "example", + Conductor.Solid(:core; radius=10e-3, material=copper), + Insulator.Tubular(:core; thickness=8e-3, material=xlpe), + Conductor.Tubular(:screen; thickness=1e-3, material=copper), + Insulator.Tubular(:screen; thickness=2e-3, material=xlpe), +) + +constants = compute!(CableConstantsProblem(only(design)), Formulation()) +``` + +`Material` is the materialized type as well as the public declarative +constructor: scalar keywords return one material, while explicit `Grid` inputs +return a `Gridspace{Material}`. The stricter cable and system constructors +remain available from explicit submodules such as `LineCableModels.DataModel` +and `LineCableModels.EarthProps`. + ## Optional plotting Load one Makie backend explicitly before calling `preview`, `plot`, or @@ -55,14 +81,14 @@ export_svg(first(plots); path = "series_resistance.svg") ``` `GLMakie` and `WGLMakie` are supported in the same way. LineCableModels -never imports or selects a backend dynamically. `preview` and Monte Carlo -distribution plots return one `UIPlot`; line-parameter plots always return a +never imports or selects a backend dynamically. `preview` returns one `UIPlot`; +line-parameter plots always return a `Vector{UIPlot}`. The Export SVG control preserves the current declarative plot state and requires CairoMakie to have been loaded explicitly. ## Result access -`CableConstants` stores canonical per-metre R/L/C values. `LineParameters` +`CableConstants` stores R/L/C values per metre. `LineParameters` stores its frequency domain and either a `:per_length` or `:total` basis: ```julia @@ -72,10 +98,21 @@ Z(line_parameters, 1, 1, 2:5) # selected frequency samples abs.(Z(line_parameters, 1, 1)) ``` -Monte Carlo results use first-class `CableConstantsMC` and `LineParametersMC` -containers. Use `statistics`, `mean`, `std`, `quantile`, `samples`, `trial`, -`distribution`, and `surrogate`; joint `trial`/`rand` calls require retained -samples. +Complete parameter traversals return `FullParametricResult{T}`. One conditional +Monte Carlo analysis returns `MonteCarloResult{T}`; multiple outer +configurations therefore return +`FullParametricResult{MonteCarloResult{T}}`. Use `statistics`, `samples`, +`histograms`, `uncertain_value`, and `manifest` to inspect the analysis. +`DataFrame(monte_carlo_result)` renders marginal summaries, while +`plot(monte_carlo_result, :R; mode=:hist, data=:both)` and the `:pdf`, `:ecdf`, +and `:qq` modes display retained distribution information after a Makie package +is loaded. + +Physics and numerical-method choices belong to `Formulation`; execution choices +belong to `ComputeOptions`. For a materialized line system, pass +`options=(output_basis=:total,)` to `compute!` to scale both Z and Y by the line +length. The same formulation can be reused for ordinary, full-parametric, and +Monte Carlo runs. ## Retired FEM and sector support diff --git a/docs/literate/plotbuilder.jl b/docs/literate/plotbuilder.jl index cff237ed..18ee1193 100644 --- a/docs/literate/plotbuilder.jl +++ b/docs/literate/plotbuilder.jl @@ -29,8 +29,8 @@ # # PlotBuilder follows five non-negotiable rules: # -# 1. `make_render` is one generic pipeline. Domain recipes specialize accessors; -# they do not replace the pipeline. +# 1. `make_render` is one generic rendering sequence. Domain recipes specialize +# accessors; they do not replace that sequence. # 2. Recipe variation uses Julia dispatch, including `Val` dispatch for modes, # grouping, and primitive rendering. # 3. `PlotRecipe`, `RenderSpec`, layouts, axes, series, views, and page components @@ -48,24 +48,26 @@ # # The developer API may evolve before LineCableModels 1.0, but changes must keep # the separation between domain recipes, backend-neutral specifications, and -# backend rendering. Architectural tests enforce the single pipeline and the +# backend rendering. Structural tests enforce the single rendering sequence and the # absence of Makie from core specification construction. # # ## Supported recipe families # # | Module | Specification | Input | Entry point | # |:--|:--|:--|:--| -# | `Engine` | `LineParameterPlotSpec` | `SeriesImpedance` and frequencies | `plot(series, frequencies)` | -# | `Engine` | `LineParameterPlotSpec` | `ShuntAdmittance` and frequencies | `plot(shunt, frequencies)` | -# | `Engine` | `LineParameterPlotSpec` | `LineParameters` | `plot(parameters)` | -# | `UQ` | `MCDistributionPlotSpec` | `CableConstantsMC` | `plot(result, quantity)` | -# | `UQ` | `MCDistributionPlotSpec` | `LineParametersMC` | `plot(result, quantity; ijk)` | +# | `Engine` | `LineParameterPlotSpec` | `SeriesImpedance` and frequencies | `plot(series, frequencies[, accessors])` | +# | `Engine` | `LineParameterPlotSpec` | `ShuntAdmittance` and frequencies | `plot(shunt, frequencies[, accessors])` | +# | `Engine` | `LineParameterPlotSpec` | `LineParameters` | `plot(parameters[, accessors])` | +# | `Computation` | `MCDistributionPlotSpec` | `MonteCarloResult` | `plot(result, quantity)` | # | `DataModel` | `CablePreviewPlotSpec` | `CableDesign` | `preview(design)` | # | `DataModel` | `SystemPreviewPlotSpec` | `LineCableSystem` | `preview(system)` | # -# Line-parameter recipes provide RLCG, Cartesian Z/Y, and polar Z/Y pages. UQ -# recipes provide histogram, PDF, ECDF, and Q-Q views. DataModel recipes provide -# cable and system previews. `MaterialScalePlotSpec` is an internal reusable +# `plot(parameters)` produces separate Z and Y pages, each with real and +# imaginary parts in adjacent views. An accessor tuple selects another +# representation, for example `(R, L, G, C)` or `(abs, angle)`. The +# Monte Carlo recipe provides histogram, PDF, empirical/histogram CDF, and Q-Q +# views for retained marginal results. +# DataModel recipes provide cable and system previews. `MaterialScalePlotSpec` is an internal reusable # component for composing and testing the material-property scales used by those # previews; it is not a public plotting entry point. # @@ -159,16 +161,15 @@ end; #hide # and separates scientific choices from renderer choices. recipe = parse_kwargs( LineParameterPlotSpec, - parameters; - mode = :ZY, - coord = :polar, + Z(parameters); + frequencies = frequencies(parameters), + quantities = (abs, angle), export_theme = :publication ) (; object_type = typeof(recipe.object), - mode = recipe.input.mode, - coordinates = recipe.input.coord, + quantities = recipe.input.quantities, export_theme = recipe.renderer.export_theme ) # @@ -187,13 +188,13 @@ recipe = parse_kwargs( # render = make_render( LineParameterPlotSpec, - parameters; - mode = :ZY, - coord = :polar + Z(parameters); + frequencies = frequencies(parameters), + quantities = (abs, angle) ) -# Polar Z/Y produces magnitude and angle pages for both matrix families. -[(page.title, only(page.views).yaxis.label) for page in render.figures] +# Magnitude and angle occupy adjacent views on the impedance page. +[(page.title, getproperty.(page.views, :title)) for page in render.figures] # # The value above proves that the declarative page tree contains the expected # semantics. The first page below is that same `RenderSpec` materialized by the @@ -412,7 +413,7 @@ nothing; #hide # This example validates its array dimensions and grouping through dispatch: # # Each supported grouping has a method. The fallback produces an actionable -# error without putting a grouping conditional in the generic pipeline. +# error without putting a grouping conditional in the generic rendering sequence. profile_grouping(::Val{:overlay}) = Val(:overlay) profile_grouping(::Val{:panels}) = Val(:panels) profile_grouping(::Val{:pages}) = Val(:pages) @@ -553,7 +554,7 @@ nothing; #hide # # Titles and layouts vary by grouping through small dispatch functions. The # recipe still does not construct `ViewSpec` or `PageSpec`; the one generic -# `make_render` pipeline does that from these decisions. +# `make_render` derives them from these decisions. # profile_title(::Val{:overlay}, page_key, view_key) = "Frequency responses" profile_title(::Val{:panels}, page_key, view_key::Int) = "Response $view_key" @@ -753,7 +754,7 @@ documentation_figure(custom) #hide publication = make_render( LineParameterPlotSpec, parameters; - mode = :RLCG, + quantities = (R, L, G, C), export_theme = :publication ) documentation_figure( #hide diff --git a/docs/make.jl b/docs/make.jl index 60ea31b7..d4bddde2 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -9,7 +9,7 @@ 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 CANONICAL_URL = "https://electa-git.github.io/LineCableModels.jl" +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 PLOTBUILDER_SOURCE = joinpath(@__DIR__, "literate", "plotbuilder.jl") @@ -37,7 +37,7 @@ function tutorial_title(path::AbstractString) content = read(path, String) matchobj = match(r"(?m)^#\s+(.+)$", content) isnothing(matchobj) || - return String(matchobj.captures[1]) + return replace(String(matchobj.captures[1]), r"^#+\s*" => "") stem = splitext(basename(path))[1] return titlecase(replace(stem, "_" => " ", "-" => " ")) end @@ -51,7 +51,7 @@ function build_tutorials!() Literate.markdown( joinpath(TUTORIAL_SOURCE, file), TUTORIAL_OUTPUT; - documenter = false, + documenter = true, postprocess = strip_literate_footer ) end @@ -102,7 +102,7 @@ makedocs(; authors = metadata.authors, sitename = "$(metadata.name).jl", format = Documenter.HTML(; - canonical = CANONICAL_URL, + canonical = SITE_URL, edit_link = "main", assets = [ "assets/citations.css", diff --git a/docs/src/CHANGELOG.md b/docs/src/CHANGELOG.md index 12248d0c..a8645fc3 100644 --- a/docs/src/CHANGELOG.md +++ b/docs/src/CHANGELOG.md @@ -11,6 +11,29 @@ and versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased](https://github.com/Electa-Git/LineCableModels.jl/compare/v0.2.0...HEAD) +### Changed + +- Replaced the prototype parameter and uncertainty pipelines with the typed + `Grid`, `AbstractSpec`, and `Gridspace` grammar. +- Unified ordinary, full-parametric, and conditional Monte Carlo execution + under `compute!(problem, Formulation(); run=...)`. +- Made the declarative builder the default modeling API while retaining strict + materialized constructors through explicit submodule imports. +- 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` produces separate Z and Y figures with real and imaginary + parts in side-by-side panels. + +### Removed + +- Removed the former parameter tuple grammar, duplicate execution entrypoints, + specialized analysis containers, and radial proxy wrapper types. +- Removed the `mode=:ZY`/`:RLCG` and `coord=:cart`/`:polar` keywords from + line-parameter presentation. + ## [0.2.0] - 2026-08-13 ### Added @@ -19,10 +42,7 @@ and versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Aqua, SciML formatting, gitlint, clean-install, and modular documentation checks. - Citation and contribution metadata. -- Type-stable `CableConstants`, `SampleSummary`, `RLCG`, `CableConstantsMC`, - `LineParametersMC`, and `HistogramPDF` result containers. -- A type-stable `ParametricSweep` container for ordered deterministic cases and - their results, ready for later GridSpace integration. +- Type-stable primitive and statistical result containers. - A single declarative PlotBuilder renderer with interactive legends and one-click, non-overwriting SVG export. diff --git a/docs/src/docstrings.md b/docs/src/docstrings.md index f4e0e435..eb429204 100644 --- a/docs/src/docstrings.md +++ b/docs/src/docstrings.md @@ -22,7 +22,8 @@ contents remain synchronized with the implementation. colloquialisms, and ambiguous wording. Dedicated cross-reference sections are not maintained. Use an inline link such -as [`CableDesign`](@ref) where a relationship materially helps the explanation. +as [`LineCableModels.DataModel.CableDesign`](@ref) where a relationship +materially helps the explanation. ## Physical unit formatting @@ -45,8 +46,7 @@ physical quantity states its SI unit. Use LaTeX whenever an implementation directly evaluates a mathematical expression, reduction, approximation, or physical law that matters to its -meaning. Put the expression in a `math` block within `# Notes`, followed by the -definitions of symbols that are not already unambiguous from `# Arguments`. +meaning. Put the expression in a `math` block within the method description, followed by the definitions of symbols that are not already unambiguous from `# Arguments`. This requirement follows the implementation, not a function-name convention. Simple accessors, wrappers, dispatch helpers, and bookkeeping functions generally do not need a @@ -58,7 +58,14 @@ Within a Julia docstring, escape LaTeX commands with a second backslash: """ $(TYPEDSIGNATURES) -Return the series impedance at a specified frequency. +Return the series impedance at a specified frequency, evaluated as: + +```math +Z(f) = R + \\mathrm{j} 2 \\pi f L, +``` + +where ``R`` is the resistance, ``L`` is the inductance, and ``f`` is the +frequency. # Arguments @@ -70,17 +77,6 @@ Return the series impedance at a specified frequency. - Complex series impedance `\\[Ω\\]`. -# Notes - -This function implements - -```math -Z(f) = R + \\mathrm{j} 2 \\pi f L, -``` - -where ``R`` is the resistance, ``L`` is the inductance, and ``f`` is the -frequency. - # Examples ```jldoctest @@ -92,7 +88,7 @@ z = $(FUNCTIONNAME)(0.1, 1e-3, 50.0) # [Ω] function series_impedance_at(resistance, inductance, frequency) ```` -Preserve every correct existing mathematical `# Notes` section. If the +Preserve every correct existing mathematical description. If the implementation and its documentation disagree, inspect and test the implementation before changing either. @@ -265,12 +261,12 @@ an ordinary immediately preceding docstring for a method outside it. Start with `$(TYPEDSIGNATURES)` and use the following section order: -1. Description, without a heading. +1. Description, without a heading, including the mathematical formulation when applicable. 2. `# Arguments`. 3. `# Keywords`, when keyword arguments need documentation. 4. `# Returns`. 5. `# Notes`, when the implementation requires assumptions, limitations, or - mathematical explanation. + additional mathematical explanation. 6. `# Errors`, for deliberate exceptions callers should anticipate. 7. `# Examples`. @@ -301,7 +297,7 @@ Describe the function's implemented purpose concisely. # Notes Include only implementation-relevant assumptions, limitations, or -mathematics. +mathematical explanations. # Errors diff --git a/docs/src/index.md b/docs/src/index.md index e9d1b981..5d880e26 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -16,10 +16,15 @@ Depth = 1 ## 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. +- Describes deterministic and uncertain designs through one typed + `Grid`/`Gridspace` grammar, then evaluates them through `compute!`. +- Calculates base cable parameters for solid, tubular or stranded cores, + semiconductors, screens, armors, sheaths, tapes, and water-blocking materials. - 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. +- Computes 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. - 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). diff --git a/docs/src/plotbuilder.md b/docs/src/plotbuilder.md index cab41307..2298ae8b 100644 --- a/docs/src/plotbuilder.md +++ b/docs/src/plotbuilder.md @@ -33,8 +33,8 @@ SemVer guarantees. PlotBuilder follows five non-negotiable rules: -1. `make_render` is one generic pipeline. Domain recipes specialize accessors; - they do not replace the pipeline. +1. `make_render` is one generic rendering sequence. Domain recipes specialize + accessors; they do not replace that sequence. 2. Recipe variation uses Julia dispatch, including `Val` dispatch for modes, grouping, and primitive rendering. 3. `PlotRecipe`, `RenderSpec`, layouts, axes, series, views, and page components @@ -52,24 +52,26 @@ component rather than a separate public entry point. The developer API may evolve before LineCableModels 1.0, but changes must keep the separation between domain recipes, backend-neutral specifications, and -backend rendering. Architectural tests enforce the single pipeline and the +backend rendering. Structural tests enforce the single rendering sequence and the absence of Makie from core specification construction. ## Supported recipe families | Module | Specification | Input | Entry point | |:--|:--|:--|:--| -| `Engine` | `LineParameterPlotSpec` | `SeriesImpedance` and frequencies | `plot(series, frequencies)` | -| `Engine` | `LineParameterPlotSpec` | `ShuntAdmittance` and frequencies | `plot(shunt, frequencies)` | -| `Engine` | `LineParameterPlotSpec` | `LineParameters` | `plot(parameters)` | -| `UQ` | `MCDistributionPlotSpec` | `CableConstantsMC` | `plot(result, quantity)` | -| `UQ` | `MCDistributionPlotSpec` | `LineParametersMC` | `plot(result, quantity; ijk)` | +| `Engine` | `LineParameterPlotSpec` | `SeriesImpedance` and frequencies | `plot(series, frequencies[, accessors])` | +| `Engine` | `LineParameterPlotSpec` | `ShuntAdmittance` and frequencies | `plot(shunt, frequencies[, accessors])` | +| `Engine` | `LineParameterPlotSpec` | `LineParameters` | `plot(parameters[, accessors])` | +| `Computation` | `MCDistributionPlotSpec` | `MonteCarloResult` | `plot(result, quantity)` | | `DataModel` | `CablePreviewPlotSpec` | `CableDesign` | `preview(design)` | | `DataModel` | `SystemPreviewPlotSpec` | `LineCableSystem` | `preview(system)` | -Line-parameter recipes provide RLCG, Cartesian Z/Y, and polar Z/Y pages. UQ -recipes provide histogram, PDF, ECDF, and Q-Q views. DataModel recipes provide -cable and system previews. `MaterialScalePlotSpec` is an internal reusable +`plot(parameters)` produces separate Z and Y pages, each with real and +imaginary parts in adjacent views. An accessor tuple selects another +representation, for example `(R, L, G, C)` or `(abs, angle)`. The +Monte Carlo recipe provides histogram, PDF, empirical/histogram CDF, and Q-Q +views for retained marginal results. +DataModel recipes provide cable and system previews. `MaterialScalePlotSpec` is an internal reusable component for composing and testing the material-property scales used by those previews; it is not a public plotting entry point. @@ -172,16 +174,15 @@ and separates scientific choices from renderer choices. ````@example plotbuilder recipe = parse_kwargs( LineParameterPlotSpec, - parameters; - mode = :ZY, - coord = :polar, + Z(parameters); + frequencies = frequencies(parameters), + quantities = (abs, angle), export_theme = :publication ) (; object_type = typeof(recipe.object), - mode = recipe.input.mode, - coordinates = recipe.input.coord, + quantities = recipe.input.quantities, export_theme = recipe.renderer.export_theme ) ```` @@ -202,16 +203,16 @@ tests the complete declarative path without opening a window: ````@example plotbuilder render = make_render( LineParameterPlotSpec, - parameters; - mode = :ZY, - coord = :polar + Z(parameters); + frequencies = frequencies(parameters), + quantities = (abs, angle) ) ```` -Polar Z/Y produces magnitude and angle pages for both matrix families. +Magnitude and angle occupy adjacent views on the impedance page. ````@example plotbuilder -[(page.title, only(page.views).yaxis.label) for page in render.figures] +[(page.title, getproperty.(page.views, :title)) for page in render.figures] ```` The value above proves that the declarative page tree contains the expected @@ -450,7 +451,7 @@ provided by PlotBuilder and must not be redeclared. A real recipe should use This example validates its array dimensions and grouping through dispatch: Each supported grouping has a method. The fallback produces an actionable -error without putting a grouping conditional in the generic pipeline. +error without putting a grouping conditional in the generic rendering sequence. ````@example plotbuilder profile_grouping(::Val{:overlay}) = Val(:overlay) @@ -613,7 +614,7 @@ nothing #hide Titles and layouts vary by grouping through small dispatch functions. The recipe still does not construct `ViewSpec` or `PageSpec`; the one generic -`make_render` pipeline does that from these decisions. +`make_render` derives them from these decisions. ````@example plotbuilder profile_title(::Val{:overlay}, page_key, view_key) = "Frequency responses" @@ -850,7 +851,7 @@ LaTeX-font theme to the declarative page state. publication = make_render( LineParameterPlotSpec, parameters; - mode = :RLCG, + quantities = (R, L, G, C), export_theme = :publication ) documentation_figure( #hide diff --git a/docs/src/reference.md b/docs/src/reference.md index 299313e8..1cd63bd6 100644 --- a/docs/src/reference.md +++ b/docs/src/reference.md @@ -1,75 +1,137 @@ # API reference -This page documents the public API and the documented implementation surface of -`LineCableModels.jl`. +## Modeling and execution grammar + +The top-level namespace exposes the declarative modeling API. `Grid` is the +only parameter-variation syntax, `Gridspace{Target}` is a lazy space of complete +targets, and `AbstractSpec{Target}` supports both deterministic iteration and +stochastic materialization. + +Ordinary collections remain ordinary constructor values. Wrap a collection in +`Grid` only when it is intended to vary. Reusing the same `Grid` deliberately +couples the selection and stochastic realization wherever that grid appears. + +```julia +earth = Earth( + rho=Grid((10.0, 100.0, 1000.0)), + eps_r=Grid((100.0, 10.0, 5.0)); + combine=:zip, +) +``` -## Result containers +Composition is local to each Gridspace node. The example above admits exactly +three earth configurations; its resolved `EarthParameters` objects can still +participate in a Cartesian product at a parent problem node. -`CableConstants` stores canonical per-metre R/L/C values. `LineParameters` -stores frequency-dependent Z/Y matrices together with their domain and either a -`:per_length` or `:total` basis. Selecting matrix indices without a frequency -index returns the complete frequency response. +All numerical work follows `problem → Formulation → compute!`: -```jldoctest result_containers -julia> using LineCableModels +```julia +compute!(problem, Formulation()) +compute!(problem, Formulation(); run=FullParametric()) +compute!(problem, Formulation(); run=MonteCarlo(trials=1000, cdf_tol=0.02)) +``` -julia> f = [50.0, 100.0]; +Deterministic spaces default to `FullParametric()`. Uncertainty-bearing spaces +require the caller to choose direct propagation or Monte Carlo explicitly. +Monte Carlo enumerates every outer configuration and samples realizations only +within the selected configuration. -julia> z = reshape(ComplexF64[1 + 2im, 3 + 4im], 1, 1, 2); +`Formulation` owns physics and numerical-method choices. The separate +`ComputeOptions` value is shared unchanged by ordinary, full-parametric, and +Monte Carlo execution. For example, a materialized line system can request +total rather than per-length Z/Y matrices without altering its formulation: -julia> y = reshape(ComplexF64[5 + 6im, 7 + 8im], 1, 1, 2); +```julia +compute!(problem, formulation; options=(output_basis=:total,)) +``` -julia> parameters = LineParameters(z, y, f); +`output_basis=:total` scales both impedance and admittance by the materialized +system length. `CableConstantsProblem` has no line length and therefore accepts +only the default `:per_length` basis. -julia> basis(parameters) -:per_length +## Results and provenance -julia> R(parameters, 1, 1) == [1.0, 3.0] -true +`CableConstants` stores R/L/C values per metre. `LineParameters` +stores frequency-dependent Z/Y matrices with their domain and `:per_length` or +`:total` basis. -julia> Z(parameters, 1, 1, 2) -3.0 + 4.0im -``` +A complete configuration traversal returns `FullParametricResult{T}`. A single +conditional Monte Carlo analysis returns `MonteCarloResult{T}`. Applying Monte +Carlo to several outer configurations therefore returns +`FullParametricResult{MonteCarloResult{T}}`. -`ParametricSweep` stores deterministic cases and their results in one ordered, -type-stable collection. Indexing returns a result; `cases` retains the matching -input. A physical accessor accepts the case index before the normal result -selection grammar. Shared `basis`, `domain`, and frequency accessors fail -explicitly if those properties differ between cases. +Use `result`, `statistics`, `samples`, `histograms`, `uncertain_value`, and +`manifest` to inspect results. A calculation manifest contains a stable SHA-256 +hash over the resolved parameterization, original problem assumptions, +formulation, solver identity, execution policy, and calculation options. -```jldoctest result_containers -julia> second = LineParameters(2z, 2y, f); +`DataFrame(result::MonteCarloResult)` renders the stored marginal summaries +without repeating the calculation. Cable-constant results produce one R/L/C +table; line-parameter results produce one R/L/C/G table for every matrix entry +and frequency. The displayed `confidence` and `cdf_tol` values describe the DKW +bound below and must not be interpreted as confidence intervals for the sample +mean. -julia> sweep = ParametricSweep( - [(temperature=20.0,), (temperature=90.0,)], - [parameters, second], - ); +After loading a Makie package, retained samples and histograms can be displayed +through the maintained Monte Carlo recipe: -julia> ncases(sweep) -2 +```julia +using CairoMakie -julia> R(sweep, 2, 1, 1) -2-element Vector{Float64}: - 2.0 - 6.0 +plot(result, :R; mode=:hist, data=:both) +plot(result, :R; mode=:pdf) +plot(result, :R; mode=:ecdf, data=:both) +plot(result, :R; mode=:qq) -julia> frequencies(sweep) -2-element Vector{Float64}: - 50.0 - 100.0 +# Select one line-parameter matrix entry and frequency: +plot(line_result, :L; ijk=(1, 1, 3), mode=:hist) ``` -Monte Carlo calculations return `CableConstantsMC` or `LineParametersMC`. -Use `statistics`, `mean`, `std`, and `quantile` for summaries; `samples` and -`trial` for retained joint trials; `distribution` for retained marginal -histograms; and `surrogate` for the covariance-preserving Measurements.jl -representation. `trial` and `rand` deliberately require retained joint samples. +The `:pdf`, `:ecdf`, and `:qq` views use the retained piecewise-constant +`HistogramPDF`. When only samples were retained, the recipe derives the +histogram needed for presentation. + +When `MonteCarlo(trials=nothing)` is used, the trial count follows a simultaneous +Dvoretzky–Kiefer–Wolfowitz bound. For `M` scalar marginals and confidence +`1-α`, the implementation selects +`ceil(log(2M/α) / (2*cdf_tol^2))` trials. A union bound therefore controls the +largest empirical-CDF deviation among those marginals. `M` is three for cable +constants and four real R/L/G/C upper-triangular coordinates per frequency for +line parameters. `cdf_tol` is not a solver tolerance and does not bound mean +error or the joint distribution. + +## Optional uncertainty integrations + +The core Grid/Gridspace grammar does not load Measurements.jl or +Distributions.jl. Loading Measurements enables direct propagation and +`Measurements.measurement(result::MonteCarloResult)`. Retained joint samples +allow covariance-preserving reconstruction; without retained samples the +conversion warns and reconstructs independent marginal values. Loading +Distributions enables arbitrary compatible univariate distributions as Monte +Carlo samplers and `pdf`/`cdf` evaluation of `HistogramPDF`. A supplied +distribution is standardized to the Grid descriptor's nominal value and +standard deviation, so it must have a finite mean and a positive finite +standard deviation. + +## Strict materialized API + +The eager cable and system model remains the materialized boundary but is not +exported at top level. Import those types explicitly when strict construction +is required. `Material`, `MaterialsLibrary`, and `CablesLibrary` are public +because they are shared by both modeling modes: + +```julia +using LineCableModels: Material +import LineCableModels.DataModel +import LineCableModels.EarthProps + +copper = Material(1.7241e-8, 1.0, 1.0, 20.0, 0.00393) +core = DataModel.Tubular(0.0, copper; radius=10e-3) +``` -`UnitHandler` maps result accessors to physical meaning independently of any -container or plot. For example, `UnitHandler.quantity(Z)` identifies series -impedance, while `UnitHandler.quantity(Z, :re)` identifies series resistance. -The resulting `QuantityTag` selects canonical units, display units, labels, -symbols, and scaling. +Strict radial constructors accept numeric inner/outer radii, or exactly one +named numeric `radius`/`thickness` declaration. Group `add!` uses the current +outer radius as the next inner boundary. ## Contents @@ -84,6 +146,7 @@ Depth = 3 Modules = [ LineCableModels, LineCableModels.Commons, + LineCableModels.Computation, LineCableModels.UnitHandler, LineCableModels.Utils, ] @@ -124,13 +187,12 @@ Public = true Private = true ``` -## Parametric and uncertainty modeling +## Parametric modeling ```@autodocs Modules = [ LineCableModels.ParametricBuilder, LineCableModels.ParametricBuilder.WirePatterns, - LineCableModels.UQ, ] Order = [:module, :constant, :type, :function, :macro] Public = true @@ -158,15 +220,6 @@ Public = true Private = true ``` -## Uncertainty-aware Bessel functions - -```@autodocs -Modules = [LineCableModels.UncertainBessels] -Order = [:module, :constant, :type, :function, :macro] -Public = true -Private = true -``` - ## Index ```@index diff --git a/docs/src/validation.md b/docs/src/validation.md index 55ac109a..7e7f6742 100644 --- a/docs/src/validation.md +++ b/docs/src/validation.md @@ -1,256 +1,100 @@ # Validation module ## Contents + ```@contents Pages = ["validation.md"] Depth = 3 ``` ---- - -This section documents the validation framework used by component constructors. The design is deterministic, non‑magical, and trait‑driven. The flow is: +The strict materialized constructors use a trait-driven validation sequence: ```julia -sanitize(Type, args, kwargs) → parse(Type, nt) → apply rules → construct +sanitize(Type, args, kwargs) → parse(Type, values) → apply rules → construct ``` -The **typed cores** accept **numbers only**. All proxy handling happens in the **convenience constructors** via `validate!`. - ---- - -## Architecture - -### Pipeline - -* **`sanitize(::Type{T}, args::Tuple, kwargs::NamedTuple)`** - - * Rejects wrong arities: exactly `length(required_fields(T))` positionals are expected; optionals must be passed as keywords listed in `keyword_fields(T)`. - * Maps positionals to names using `required_fields(T)`; merges keyword arguments; rejects unknown keywords. - * If `has_radii(T) == true`, checks admissibility of raw radius inputs with `is_radius_input(T, Val(:r_in), x)` and `is_radius_input(T, Val(:r_ex), x)`. The default accepts only real, non‑complex numbers; types may extend to allow proxies. - * Returns a **raw** `NamedTuple`. - -* **`parse(::Type{T}, nt)`** - - * Normalizes raw inputs to canonical representation (e.g., radius proxies → numeric radii) while preserving domain semantics (e.g., uncertainty reset rules). - * Returns a **normalized** `NamedTuple`. - -* **Rule application** +This layer accepts numeric physical state. It does not interpret tuples, +previous/next layer objects, or radial wrapper types. Parameter variation belongs +to `Grid` and declarative construction belongs to `Gridspace` builders. - * `_rules(T)` is generated from traits; evaluates over the normalized `NamedTuple`. - * Standard bundles are injected when traits are `true` (e.g., for radii: `Normalized`, `Finite`, `Nonneg`, `Less`). - * Per‑type extras come from `extra_rules(T)`. +## Radial inputs -* **`validate!(::Type{T}, args...; kwargs...)`** - - * Orchestrates the pipeline. Use this in all convenience constructors. - -### Traits (configuration surface) - -* `has_radii(::Type{T})::Bool` — enables the radii rule bundle and raw acceptance checks. -* `has_temperature(::Type{T})::Bool` — enables finiteness check on `:temperature`. -* `required_fields(::Type{T})::NTuple` — positional keys. -* `keyword_fields(::Type{T})::NTuple` — keyword argument keys. -* `coercive_fields(::Type{T})::NTuple` — values that participate in type promotion and will be coerced (default: `required_fields ∪ keyword_fields`). -* `is_radius_input(::Type{T}, Val(:field), x)::Bool` — raw admissibility predicate for radii inputs; extend to allow proxies by field. -* `extra_rules(::Type{T})::NTuple{K,Rule}` — additional constraints appended to the generated bundle. - -**Import before extending**: +The typed physical constructors accept numeric inner and outer radii: ```julia -import ..Validation: has_radii, has_temperature, required_fields, keyword_fields, - coercive_fields, is_radius_input, parse, extra_rules +Tubular(r_in, r_ex, material) ``` -Failing to import will create shadow functions in your module; the engine will not see your methods. - ---- - -## Rules - -Rules are small value types `struct <: Rule` with an `_apply(::Rule, nt, ::Type{T})` method. All rule methods must: - -* Read data from the **normalized** `NamedTuple` `nt`. -* Throw `ArgumentError` for logical violations; `DomainError` for numerical domain violations (non‑finite). -* Avoid allocations; use `@inline` where appropriate. - -### Standard rules - -* `Normalized(:field)` — field must be numeric post‑parse. -* `Finite(:field)` — `isfinite` must hold. -* `Nonneg(:field)` — value `≥ 0`. -* `Positive(:field)` — value `> 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 +Their narrow named convenience accepts one forward declaration: ```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 +Tubular(r_in, material; radius = r_ex) +Tubular(r_in, material; thickness = delta_r) ``` -Attach via `extra_rules(::Type{X}) = (InRange(:alpha, 0.0, 1.0), ...)`. - ---- - -## Example implementation — `DataModel.Tubular` - -**Typed core** (numbers only): +Exactly one of `radius` and `thickness` is required. Group `add!` methods retain +the useful stacking operation by supplying the group's current numeric outer +radius as the new layer's inner radius: ```julia -function Tubular(r_in::T, r_ex::T, material_props::Material{T}, temperature::T) where {T<:REALSCALAR} +add!(group, Tubular, material; thickness = delta_r) ``` -Rationale: all proxy resolution must happen before reaching the typed core to avoid duplicate parsing and to keep type promotion deterministic. +No layer object is accepted as a radius proxy by the physical constructors. -**Trait configuration**: +## Validation sequence -```julia -const _REQ_TUBULAR = (:r_in, :r_ex, :material_props,) -const _OPT_TUBULAR = (:temperature,) -const _DEFS_TUBULAR = (T₀,) +- `sanitize` checks positional arity, accepted keywords, defaults, and real + numeric radii. +- `parse` normalizes component-specific fields. It is the identity by + default. +- `_rules(T)` assembles the standard trait-driven rules and `extra_rules(T)`. +- `validate!` runs the complete sequence used by convenience constructors. -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 -``` - -* `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. +The principal traits are: -**Raw proxy acceptance**: +- `has_radii(::Type)` for annular radius rules; +- `has_temperature(::Type)` for temperature finiteness; +- `required_fields`, `keyword_fields`, and `keyword_defaults` for constructor + shape; +- `coercive_fields` for numeric promotion; +- `extra_rules` for component-specific constraints. -```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 -``` +## Rules -The inner radius may take an existing cable part (its `r_ex`); the outer radius may take a `Thickness` or `Diameter` wrapper. +Rules are small `Rule` values applied to the normalized `NamedTuple`. Standard +rules include `Normalized`, `Finite`, `Nonneg`, `Positive`, `IntegerField`, +`Less`, `LessEq`, `IsA`, and `OneOf`. Logical violations throw +`ArgumentError`; non-finite numerical values throw `DomainError`. -**Extra rules**: +For an annular component, the core trait setup is: ```julia +Validation.has_radii(::Type{Tubular}) = true +Validation.has_temperature(::Type{Tubular}) = true +Validation.required_fields(::Type{Tubular}) = (:r_in, :r_ex, :material_props) +Validation.keyword_fields(::Type{Tubular}) = (:temperature,) +Validation.keyword_defaults(::Type{Tubular}) = (T₀,) 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₀,) -``` - ---- +`has_radii` adds numeric normalization, finiteness, non-negativity, and the +strict `r_in < r_ex` ordering check. -## Extending traits +## Component checklist -Traits are just methods. Add traits only when behavior toggles; avoid proliferation. +When adding a physical part: -### New feature flags +1. implement the fully typed numeric core; +2. declare required, optional, default, and coercive fields; +3. opt into standard geometry and temperature bundles; +4. add only the component-specific rules; +5. normalize fields in `parse` when necessary; +6. route the convenience constructor through `validate!`. -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. - ---- +Tests should cover arity, rejected strings/complex radii, non-finite and +inverted geometry, type promotion, and equivalence between the convenience and +typed-core construction paths. ## API reference @@ -261,10 +105,9 @@ Public = true Private = true ``` ---- - ## Index + ```@index -Pages = ["validation.md"] -Order = [:module, :constant, :type, :function, :macro] +Pages = ["validation.md"] +Order = [:module, :constant, :type, :function, :macro] ``` diff --git a/examples/tutorial1.jl b/examples/tutorial1.jl index ed3f025c..dd2dc870 100644 --- a/examples/tutorial1.jl +++ b/examples/tutorial1.jl @@ -20,8 +20,8 @@ Depth = 2:3 ## Getting started =# -# Load the package: -using LineCableModels +# Load the public modeling and library-management API: +using LineCableModels: Material, MaterialsLibrary, add!, load!, save, set_verbosity! using DataFrames fullfile(filename) = joinpath(@__DIR__, filename); #hide set_verbosity!(0); #hide @@ -39,7 +39,7 @@ materials = MaterialsLibrary() materials_df = DataFrame(materials) #= -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. +The function `DataFrame` returns a table with all materials and their properties, namely: electrical resistivity, relative permittivity, relative permeability, reference temperature, and temperature coefficient. =# # ## Adding new materials @@ -50,32 +50,50 @@ The function [`DataFrame`](@ref) returns a `DataFrame` with all materials and th It might be useful to add other conductor materials with corrected properties based on recognized standards [cigre531](@cite) [IEC60287](@cite). =# -copper_corrected = Material(1.835e-8, 1.0, 0.999994, 20.0, 0.00393) # Copper with corrected resistivity from IEC 60287-3-2 +copper_corrected = Material( + rho=1.835e-8, + eps_r=1.0, + mu_r=0.999994, + T0=20.0, + alpha=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 +aluminum_corrected = Material( + rho=3.03e-8, + eps_r=1.0, + mu_r=0.999994, + T0=20.0, + alpha=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 +# lead = Material(rho=21.4e-8, eps_r=1.0, mu_r=0.999983, T0=20.0, alpha=0.00400) # Lead or lead alloy # add!(materials, "lead", lead) -# steel = Material(13.8e-8, 1.0, 300.0, 20.0, 0.00450) # Steel +# steel = Material(rho=13.8e-8, eps_r=1.0, mu_r=300.0, T0=20.0, alpha=0.00450) # Steel # add!(materials, "steel", steel) -# bronze = Material(3.5e-8, 1.0, 1.0, 20.0, 0.00300) # Bronze +# bronze = Material(rho=3.5e-8, eps_r=1.0, mu_r=1.0, T0=20.0, alpha=0.00300) # Bronze # add!(materials, "bronze", bronze) -stainless_steel = Material(70.0e-8, 1.0, 500.0, 20.0, 0.0) # Stainless steel +stainless_steel = Material( + rho=70.0e-8, + eps_r=1.0, + mu_r=500.0, + T0=20.0, + alpha=0.0, +) # Stainless steel add!(materials, "stainless_steel", stainless_steel) #= 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. =# -epr = Material(1e15, 3.0, 1.0, 20.0, 0.005) # EPR (ethylene propylene rubber) +epr = Material(rho=1e15, eps_r=3.0, mu_r=1.0, T0=20.0, alpha=0.005) # EPR (ethylene propylene rubber) add!(materials, "epr", epr) -pvc = Material(1e15, 8.0, 1.0, 20.0, 0.1) # PVC (polyvinyl chloride) +pvc = Material(rho=1e15, eps_r=8.0, mu_r=1.0, T0=20.0, alpha=0.1) # PVC (polyvinyl chloride) add!(materials, "pvc", pvc) -laminated_paper = Material(1e15, 2.8, 1.0, 20.0, 0.0) # Laminated paper propylene +laminated_paper = Material(rho=1e15, eps_r=2.8, mu_r=1.0, T0=20.0, alpha=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) +carbon_pe = Material(rho=0.06, eps_r=1e3, mu_r=1.0, T0=20.0, alpha=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) +conductive_paper = Material(rho=18.5, eps_r=8.6, mu_r=1.0, T0=20.0, alpha=0.0) # Conductive paper layer (semicon) add!(materials, "conductive_paper", conductive_paper) # ## Removing materials @@ -129,5 +147,5 @@ This tutorial has demonstrated how to: 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. +The [`MaterialsLibrary`](@ref) provides a traceable way to manage material properties for power cable modeling. Custom [`Material`](@ref) objects can be defined and used to match specific manufacturer data or standards requirements. =# diff --git a/examples/tutorial2.jl b/examples/tutorial2.jl index 337522b7..1919cf4b 100644 --- a/examples/tutorial2.jl +++ b/examples/tutorial2.jl @@ -22,24 +22,24 @@ Single-core power cables have a complex structure consisting of multiple concent 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). +1. Creating a detailed [`CableDesign`](@ref LineCableModels.DataModel.CableDesign) with all its components. +2. Examining the main electrical parameters (R, L, C) of the cable core [`ConductorGroup`](@ref LineCableModels.DataModel.ConductorGroup) and main [`InsulatorGroup`](@ref LineCableModels.DataModel.InsulatorGroup). +3. Examining the equivalent electromagnetic properties of every [`CableComponent`](@ref LineCableModels.DataModel.CableComponent) (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. +4. Assigning [`CableDesign`](@ref LineCableModels.DataModel.CableDesign) objects to a [`LineCableSystem`](@ref LineCableModels.DataModel.LineCableSystem) and exporting the model to PSCAD for EMT analysis. =# #= ## Getting started =# -# Load the package and set up the environment: +# Load the public modeling API and the packages used for presentation: using LineCableModels +import CairoMakie using DataFrames -import LineCableModels.PlotBuilder.BackendHandler: renderfig #hide fullfile(filename) = joinpath(@__DIR__, filename); #hide set_verbosity!(0); #hide -set_backend!(:gl); #hide +set_backend!(:cairo); #hide # Initialize materials library with default values: materials = MaterialsLibrary(add_defaults = true) @@ -122,10 +122,13 @@ df = DataFrame( #hide ) #hide #= -## Using the cable constructors +## Describing the cable -!!! 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. +!!! note "Materialized object hierarchy" + [`CableBuilder`](@ref) is the user-facing description of the cable. When the + description is resolved, the package builds the existing physical hierarchy + shown below. That hierarchy validates every radius and incrementally computes + the equivalent electrical properties used by the numerical methods. ``` CableDesign @@ -151,59 +154,86 @@ CableDesign ### 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. +The materialized [`CableDesign`](@ref LineCableModels.DataModel.CableDesign) is the main container for all cable +components. Users normally obtain it by resolving a [`CableBuilder`](@ref) +description rather than constructing each intermediate object manually. ### Cable components -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. +Each [`CableComponent`](@ref LineCableModels.DataModel.CableComponent) represents a functional group of the cable (core, +sheath, armor, or jacket), organized into conductive and insulating layers with +their respective effective material properties. ### Conductor groups -The [`ConductorGroup`](@ref) object serves as a specialized container for organizing `AbstractConductorPart` elements in layers. It calculates equivalent resistance (R) and inductance (L) values for all contained conductive elements, handling the complexity of different geometrical arrangements. +The conductive layers calculate equivalent resistance (R) and inductance (L) +while retaining the geometry of stranded wires, tubular conductors, and tapes. #### AbstractConductorPart implementations -- The `CircStrands` 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. +- `Conductor.Wires` describes stranded cores and screens with helical patterns + and circular cross-sections. +- `Conductor.Tubular` describes a solid core or an annular conductor. +- `Conductor.Strip` describes a helically wound rectangular tape. ### Insulator groups -The [`InsulatorGroup`](@ref) object organizes `AbstractInsulatorPart` elements in concentric layers, calculating the equivalent capacitance (C) and conductance (G) parameters. +The insulating layers calculate the equivalent capacitance (C) and conductance +(G) of a concentric stack. #### 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. +- `Insulator.Tubular` describes a dielectric layer. +- `Insulator.Semicon` describes a semiconducting layer with intermediate + resistivity and high permittivity. !!! note "Equivalent circuit parameters" - The hierarchical structure enables accurate calculation of equivalent circuit parameters by: + The hierarchy calculates equivalent circuit parameters by: - 1. Computing geometry-specific parameters at the `AbstractConductorPart` and `AbstractInsulatorPart` 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. + 1. Computing geometry-specific parameters for every conductive and insulating layer. + 2. Aggregating these into equivalent parameters for each cable component. + 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 LineCableModels.DataModel.CableComponent) level. The effective properties are stored in dedicated [`Material`](@ref) objects. =# #= ## 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` object, which handles the helical pattern and twisting effects via `calc_helical_params`. +The core consists of a central wire and four concentric AAAC layers with 61 +wires arranged in a (1/6/12/18/24) pattern. The respective lay ratios are +(15/13.5/12.5/11) [CENELEC50182](@cite). `Conductor.Wires` retains the helical +pattern required for the resistance and GMR corrections. =# -# 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)) +# Select reusable materials from the library: +aluminum = Material(materials, :aluminum) +copper = Material(materials, :copper) +polyacrylate = Material(materials, :polyacrylate) +semicon1 = Material(materials, :semicon1) +semicon2 = Material(materials, :semicon2) +pe = Material(materials, :pe) #= !!! 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. + Cable datasheets usually report wire diameters, while the builder accepts + `wire_radius`. The conversion `d_w / 2` therefore remains visible at the + point of use. Subsequent layers are stacked over the current outer radius. =# -# 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) +# Describe the central wire and the four stranded layers: +core_wire_counts = (1, 6, 12, 18, 24) +@assert sum(core_wire_counts) == num_co_wires +core_conductors = ( + Conductor.Wires( + :core; wire_radius=d_w / 2, num_wires=1, lay_ratio=0.0, material=aluminum), + Conductor.Wires( + :core; wire_radius=d_w / 2, num_wires=6, lay_ratio=15.0, material=aluminum), + Conductor.Wires( + :core; wire_radius=d_w / 2, num_wires=12, lay_ratio=13.5, material=aluminum), + Conductor.Wires( + :core; wire_radius=d_w / 2, num_wires=18, lay_ratio=12.5, material=aluminum), + Conductor.Wires( + :core; wire_radius=d_w / 2, num_wires=24, lay_ratio=11.0, material=aluminum), +) #= ### Inner semiconductor @@ -214,16 +244,16 @@ the conductor and insulation, eliminating air gaps and reducing field concentrat #= !!! 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). + Annular layers accept either `radius=...` for an absolute outer radius or + `thickness=...` for a radial increment. Exactly one is required. A thickness + declaration automatically starts at the current outer radius. =# -# Inner semiconductive tape: -material = get(materials, "polyacrylate") -main_insu = InsulatorGroup(Semicon(core, Thickness(t_sct), material)) - -# Inner semiconductor (1000 Ω.m as per IEC 840): -material = get(materials, "semicon1") -add!(main_insu, Semicon, Thickness(t_sc_in), material) +# Describe the inner semiconductive tape and semiconductor (1000 Ω·m as per IEC 840): +inner_layers = ( + Insulator.Semicon(:core; thickness=t_sct, material=polyacrylate), + Insulator.Semicon(:core; thickness=t_sc_in, material=semicon1), +) #= ### Main insulation @@ -232,9 +262,8 @@ XLPE (cross-linked polyethylene) is the standard insulation material for modern medium and high voltage cables due to its excellent dielectric properties. =# -# Add the insulation layer: -material = get(materials, "pe") -add!(main_insu, Insulator, Thickness(t_ins), material) +# Describe the main insulation layer: +main_insulation = Insulator.Tubular(:core; thickness=t_ins, material=pe) #= ### Outer semiconductor @@ -243,24 +272,22 @@ 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) - -# Outer semiconductive tape: -material = get(materials, "polyacrylate") -add!(main_insu, Semicon, Thickness(t_sct), material) +# Describe the outer semiconductor (500 Ω·m as per IEC 840) and tape: +outer_layers = ( + Insulator.Semicon(:core; thickness=t_sc_out, material=semicon2), + Insulator.Semicon(:core; thickness=t_sct, material=polyacrylate), +) -# Group core-related components: -core_cc = CableComponent("core", core, main_insu) +# Assemble all declarations associated with the core component: +core_parts = (core_conductors, inner_layers, main_insulation, outer_layers) #= -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. +With the core parts properly defined, the [`CableDesign`](@ref LineCableModels.DataModel.CableDesign) 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. =# -# Define the nominal values and instantiate the `CableDesign` with the `core_cc` component: +# Record the nominal values reported by the datasheet: cable_id = "18kV_1000mm2" -datasheet_info = NominalData( +datasheet_info = ( designation_code = "NA2XS(FL)2Y", U0 = 18.0, # Phase-to-ground voltage [kV] U = 30.0, # Phase-to-phase voltage [kV] @@ -268,13 +295,19 @@ datasheet_info = NominalData( 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] + inductance = 0.3, # Inductance in trifoil [mH/km] ) -cable_design = CableDesign(cable_id, core_cc, nominal_data = datasheet_info) + +# Resolve the deterministic core description into a materialized cable design: +core_design = only(CableBuilder(cable_id, core_parts; nominal=datasheet_info)) # At this point, it becomes possible to preview the cable design: -plt1, _ = preview(cable_design) -plt1 #hide +plt1 = preview( + core_design, + display_plot=false, #hide + controls=false, #hide +) +plt1.figure #hide #= ### Wire screens @@ -286,27 +319,37 @@ The metallic screen (typically copper) serves multiple purposes: - Provides mechanical protection. =# -# Build the wire screens on top of the previous layer: +# Describe the wire screen on top of the previous component: 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), +sheath_parts = ( + Conductor.Wires( + :sheath; + wire_radius=d_ws / 2, + num_wires=num_sc_wires, + lay_ratio, + material=copper, + ), + Conductor.Strip( + :sheath; + thickness=t_cut, + width=w_cut, + lay_ratio, + material=copper, + ), + Insulator.Semicon(:sheath; thickness=t_wbt, material=polyacrylate), ) -# Add the equalizing copper tape wrapping the wire screen: -add!(screen_con, Strip, Thickness(t_cut), w_cut, lay_ratio, material) - -# Water blocking tape over screen: -material = get(materials, "polyacrylate") -screen_insu = InsulatorGroup(Semicon(screen_con, Thickness(t_wbt), material)) - -# Group sheath components and assign to design: -sheath_cc = CableComponent("sheath", screen_con, screen_insu) -add!(cable_design, sheath_cc) +# Resolve and examine the core plus metallic screen: +screened_design = only(CableBuilder( + cable_id, core_parts, sheath_parts; nominal=datasheet_info)) # Examine the newly added components: -plt2, _ = preview(cable_design) -plt2 #hide +plt2 = preview( + screened_design, + display_plot=false, #hide + controls=false, #hide +) +plt2.figure #hide #= ### Outer jacket components @@ -315,29 +358,36 @@ Modern cables often include an aluminum tape as moisture barrier and PE (polyethylene) outer jacket for mechanical protection. =# -# 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)) - -# PE jacket (outer mechanical protection): -material = get(materials, "pe") -add!(jacket_insu, Insulator, Thickness(t_jac), material) +# Describe the aluminum moisture barrier, bonded PE face, and outer PE jacket: +jacket_parts = ( + Conductor.Tubular(:jacket; thickness=t_alt, material=aluminum), + Insulator.Tubular(:jacket; thickness=t_pet, material=pe), + Insulator.Tubular(:jacket; thickness=t_jac, material=pe), +) #= !!! 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. + A component name groups its conductive and insulating declarations. The + builder resolves components in radial order and uses the previous + component's outer radius as the next component's inner boundary. =# -# Assign the jacket parts directly to the design: -add!(cable_design, "jacket", jacket_con, jacket_insu) +# Resolve the complete cable description: +cable_design = only(CableBuilder( + cable_id, + core_parts, + sheath_parts, + jacket_parts; + nominal=datasheet_info, +)) # Inspect the finished cable design: -plt3, _ = preview(cable_design) -plt3 #hide +plt3 = preview( + cable_design, + display_plot=false, #hide + controls=false, #hide +) +plt3.figure #hide #= ## Examining the cable parameters (RLC) @@ -345,8 +395,22 @@ plt3 #hide 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. =# -# Compare with datasheet information (R, L, C values): -core_df = DataFrame(cable_design, :baseparams) +# Calculate the cable constants explicitly. DataFrame presentation is applied +# only after the numerical result exists: +constants = compute!(CableConstantsProblem(cable_design), Formulation()) + +# Compare the calculated values with the datasheet information in the units +# conventionally used by cable manufacturers: +core_df = DataFrame( + parameter = ["R", "L", "C"], + calculated = [constants.R * 1e3, constants.L * 1e6, constants.C * 1e9], + datasheet = [ + datasheet_info.resistance, + datasheet_info.inductance, + datasheet_info.capacitance, + ], + unit = ["Ω/km", "mH/km", "μF/km"], +) # Obtain the equivalent electromagnetic properties of the cable: components_df = DataFrame(cable_design, :components) @@ -370,11 +434,20 @@ library_df = DataFrame(library) output_file = fullfile("cables_library.json") save(library, file_name = output_file); +# Load the saved design into a fresh library and retrieve it by identifier: +loaded_library = CablesLibrary() +load!(loaded_library, file_name=output_file) +loaded_design = get(loaded_library, cable_id) +loaded_library_df = DataFrame(loaded_library) + #= ### 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. + [`SystemBuilder`](@ref) combines a cable design, its positions, length, + operating temperature, earth properties, and analysis frequencies. Resolving + this description produces the complete line-parameter problem used by + [`compute!`](@ref), preview, and export routines. =# #= @@ -383,12 +456,9 @@ save(library, file_name = output_file); 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. =# -# 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) +# Define a frequency scan and typical homogeneous-soil properties: +f = collect(10.0 .^ range(0, stop=6, length=10)) # 1 Hz to 1 MHz +earth = Earth(rho=100.0, eps_r=10.0, mu_r=1.0) #= ### Three-phase system in trifoil configuration @@ -396,24 +466,41 @@ earthmodel_df = DataFrame(earth_params) This section ilustrates the construction of a cable system with three identical cables arranged in a trifoil formation. =# -# 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); - -# 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) +# Describe three cables touching in trifoil at 1 m burial depth. The spacing is +# the center-to-center distance: +formation = trifoil( + x=0.0, + y=-1.0, + spacing=70e-3, + phases=( + :core => (1, 2, 3), + :sheath => 0, + :jacket => 0, + ), +) -# 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)) +# Combine the loaded design, formation, earth, and frequency scan: +problem = only(SystemBuilder( + "18kV_1000mm2_trifoil", + loaded_design, + formation; + length=1000.0, + temperature=20.0, + earth, + frequencies=f, +)) +cable_system = problem.system +earth_params = problem.earth_props + +# Earth model base properties: +earthmodel_df = DataFrame(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. + The `phases` declaration maps each cable component to its electrical phase. + The core tuple `(1, 2, 3)` assigns phases A, B, and C to the three cables. + Components mapped to phase 0 are grounded and subsequently Kron-eliminated. + Components assigned to the same positive phase are bundled. =# #= @@ -426,8 +513,14 @@ In this section the complete three-phase cable system is examined. system_df = DataFrame(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 +plt4 = preview( + cable_system, + earth_model=earth_params, + zoom_factor=2.0, + display_plot=false, #hide + controls=false, #hide +) +plt4.figure #hide #= ## PSCAD & ATPDraw export @@ -448,12 +541,13 @@ export_file = export_data(:atp, cable_system, earth_params, file_name = output_f This tutorial has demonstrated how to: -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. +1. Describe and preview a complex power cable with multiple concentric layers. +2. Calculate and compare its base parameters (R, L, C) with datasheet values. +3. Save the design, load it in a fresh library, and reuse it. +4. Build and preview a three-phase cable system in trifoil arrangement. +5. Export the physical model for PSCAD and ATPDraw. -[`LineCableModels.jl`](@ref) provides a powerful framework for accurate power cable modeling +[`LineCableModels.jl`](@ref) provides detailed routines for 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! =# diff --git a/examples/tutorial3.jl b/examples/tutorial3.jl index 9112d9ba..cc68d262 100644 --- a/examples/tutorial3.jl +++ b/examples/tutorial3.jl @@ -24,14 +24,10 @@ HVDC cables are constructed around a central conductor enclosed by a triple-extr ## Getting started =# -# Load the package and set up the environment: +# Load the public modeling API and the packages used for presentation: using LineCableModels -using LineCableModels.DataModel: CircStrands -using LineCableModels.Engine -using LineCableModels.Engine.Transforms: Fortescue import CairoMakie using DataFrames -using Printf fullfile(filename) = joinpath(@__DIR__, filename); #hide set_verbosity!(0); #hide set_backend!(:cairo); #hide @@ -98,19 +94,33 @@ df = DataFrame( #hide #= ## Core and main insulation -Initialize the conductor object and assign the central wire: +The conductor has a central wire and six concentric layers. `Conductor.Stranded` +generates the (1/6/12/18/24/30/36) wire pattern while retaining the specified +lay ratio for the helical corrections. =# -material = get(materials, "copper") -core = ConductorGroup(CircStrands(0.0, Diameter(d_w), 1, 0.0, material)) - -# Add the subsequent layers of wires and inspect the object: +# Select reusable materials from the library: +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) + +# Check the reported strand count and describe the complete conductor: 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 +@assert 1 + n_strands * sum(1:n_layers) == num_co_wires +core_conductor = Conductor.Stranded( + :core; + layers=n_layers + 1, + wire_radius=d_w / 2, + num_wires=n_strands, + lay_ratio=11.0, + material=copper, +) #= ### Inner semiconductor @@ -118,8 +128,8 @@ core Inner semiconductor (1000 Ω.m as per IEC 840): =# -material = get(materials, "semicon1") -main_insu = InsulatorGroup(Semicon(core, Thickness(t_sc_in), material)) +inner_semiconductor = + Insulator.Semicon(:core; thickness=t_sc_in, material=semicon1) #= ### Main insulation @@ -127,8 +137,7 @@ main_insu = InsulatorGroup(Semicon(core, Thickness(t_sc_in), material)) Add the insulation layer: =# -material = get(materials, "pe") -add!(main_insu, Insulator, Thickness(t_ins), material) +main_insulation = Insulator.Tubular(:core; thickness=t_ins, material=pe) #= ### Outer semiconductor @@ -136,18 +145,24 @@ add!(main_insu, Insulator, Thickness(t_ins), material) Outer semiconductor (500 Ω.m as per IEC 840): =# -material = get(materials, "semicon2") -add!(main_insu, Semicon, Thickness(t_sc_out), material) +outer_semiconductor = + Insulator.Semicon(:core; thickness=t_sc_out, material=semicon2) # Water blocking (swellable) tape: -material = get(materials, "polyacrylate") -add!(main_insu, Semicon, Thickness(t_wbt), material) - -# Group core-related components: -core_cc = CableComponent("core", core, main_insu) +swellable_tape = + Insulator.Semicon(:core; thickness=t_wbt, material=polyacrylate) + +# Group the declarations associated with the core component: +core_parts = ( + core_conductor, + inner_semiconductor, + main_insulation, + outer_semiconductor, + swellable_tape, +) cable_id = "525kV_1600mm2" -datasheet_info = NominalData( +datasheet_info = ( 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] @@ -155,60 +170,68 @@ datasheet_info = NominalData( screen_cross_section = 1000.0, # [mm²] resistance = nothing, # DC resistance [Ω/km] capacitance = nothing, # Capacitance [μF/km] - inductance = nothing # Inductance in trifoil [mH/km] + inductance = nothing, # Inductance in trifoil [mH/km] ) -cable_design = CableDesign(cable_id, core_cc, nominal_data = datasheet_info) #= ### Lead screen/sheath -Build the wire screens on top of the previous layer: +The lead sheath is described by its radial thickness, followed by the PE inner +sheath and PP bedding. These declarations are stacked over the resolved core. =# -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)) - -# PP bedding: -material = get(materials, "pp") -add!(screen_insu, Insulator, Thickness(t_bed), material) - -# Group sheath components and assign to design: -sheath_cc = CableComponent("sheath", screen_con, screen_insu) -add!(cable_design, sheath_cc) +sheath_parts = ( + Conductor.Tubular(:sheath; thickness=t_sc, material=lead), + Insulator.Tubular(:sheath; thickness=t_pe, material=pe), + Insulator.Tubular(:sheath; thickness=t_bed, material=pp), +) #= ### Armor and outer jacket components =# -# Add the armor wires on top of the previous layer: +# Describe the armor wires and the PP outer jacket: 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)) - -# PP layer after armor: -material = get(materials, "pp") -armor_insu = InsulatorGroup(Insulator(armor_con, Thickness(t_jac), material)) +armor_parts = ( + Conductor.Wires( + :armor; + wire_radius=d_wa / 2, + num_wires=num_ar_wires, + lay_ratio, + material=steel, + ), + Insulator.Tubular(:armor; thickness=t_jac, material=pp), +) -# Assign the armor parts directly to the design: -add!(cable_design, "armor", armor_con, armor_insu) +# Resolve the complete deterministic cable description: +cable_design = only(CableBuilder( + cable_id, + core_parts, + sheath_parts, + armor_parts; + nominal=datasheet_info, +)) # Inspect the finished cable design: -plt1 = preview(cable_design) -plt1 #hide +plt1 = preview( + cable_design, + display_plot=false, #hide + controls=false, #hide +) +plt1.figure #hide #= ## Examining the cable parameters (RLC) =# -# Summarize DC lumped parameters (R, L, C): -core_df = DataFrame(cable_design, :baseparams) +# Calculate and summarize the cable constants explicitly: +constants = compute!( + CableConstantsProblem(cable_design), + Formulation(), +) +core_df = DataFrame(constants) # Obtain the equivalent electromagnetic properties of the cable: components_df = DataFrame(cable_design, :components) @@ -221,13 +244,18 @@ Load an existing [`CablesLibrary`](@ref) file or create a new one: library = CablesLibrary() library_file = fullfile("cables_library.json") -load!(library, file_name = library_file) +isfile(library_file) && 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); +# Verify that the saved cable can be recovered in a fresh session: +loaded_library = CablesLibrary() +load!(loaded_library, file_name=library_file) +loaded_design = get(loaded_library, cable_id) + #= ## Defining a cable system @@ -236,31 +264,41 @@ save(library, file_name = library_file); #= ### Earth model -Define a constant frequency earth model: +Define an earth model over a logarithmic frequency scan. Earth properties are +declared independently of frequency; they are evaluated when the complete +problem is resolved. =# -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) +f = collect(10.0 .^ range(0, stop=6, length=61)) # 1 Hz to 1 MHz +earth = Earth(rho=100.0, eps_r=10.0, mu_r=1.0) #= ### Underground bipole configuration =# -# Define the coordinates for both cables: +# Define the coordinates and phase mapping for both poles: xp, xn, y0 = -0.5, 0.5, -1.0; +positions = ( + at(x=xp, y=y0, phases=(:core => 1, :sheath => 0, :armor => 0)), + at(x=xn, y=y0, phases=(:core => 2, :sheath => 0, :armor => 0)), +) -# 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) - -# Add the other pole (negative) to the system: -add!(cable_system, cable_design, xn, y0, - Dict("core" => 2, "sheath" => 0, "armor" => 0)) +# Build the complete bipole line-parameter problem from the loaded design: +problem = only(SystemBuilder( + "525kV_1600mm2_bipole", + loaded_design, + positions; + length=1000.0, + temperature=20.0, + earth, + frequencies=f, +)) +cable_system = problem.system +earth_params = problem.earth_props + +# Inspect the frequency-dependent earth model produced for this problem: +earthmodel_df = DataFrame(earth_params) #= ### Cable system preview @@ -272,8 +310,14 @@ In this section the complete bipole cable system is examined. system_df = DataFrame(cable_system) # Visualize the cross-section of the three-phase system: -plt2 = preview(cable_system, earth_model = earth_params, zoom_factor = 2.0) -plt2 #hide +plt2 = preview( + cable_system, + earth_model=earth_params, + zoom_factor=2.0, + display_plot=false, #hide + controls=false, #hide +) +plt2.figure #hide #= ## PSCAD & ATPDraw export @@ -288,62 +332,118 @@ output_file = fullfile("atp_export.xml") export_file = export_data(:atp, cable_system, earth_params, file_name = output_file); #= -## EMT calculations -=# - -# 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 -); +## Frequency-dependent line parameters -# Define runtime options for the analytical EMT solver -opts = ( - force_overwrite = true, - save_path = fullfile("lineparams_output"), - verbosity = 0 -); +[`Formulation`](@ref) selects the physical and numerical methods. The default +EMT formulation uses the scaled-Bessel internal-impedance method, lossless +insulation impedance/admittance, and the Papadopoulos earth-return methods. The +same formulation value can be reused for ordinary, parametric, or Monte Carlo +execution. +=# -# Define the EMT formulation with the maintained analytical models -F = FormulationSet( - :EMT; - internal_impedance = InternalImpedance.ScaledBessel(), - insulation_impedance = InsulationImpedance.Lossless(), - earth_impedance = EarthImpedance.Papadopoulos(), - insulation_admittance = InsulationAdmittance.Lossless(), - earth_admittance = EarthAdmittance.Papadopoulos(), - equivalent_earth = EHEM.EnforceLayer(layer = -1), - options = opts -); +# Define the formulation and run the 1 Hz–1 MHz frequency scan: +formulation = Formulation() +@time line_parameters = + compute!(problem, formulation; options=(verbosity=0,)); -# Run the analytical EMT solver -@time ws, p = compute!(problem, F); +# Obtain the series and shunt results in per-kilometre units: +series_rl, shunt_gc = DataFrame( + line_parameters, (R, L, G, C); length_unit=:kilo, tol=1e-9); -# Display computation results in per-kilometre units -series_rl, shunt_gc = DataFrame(p; mode = :RLCG, length_unit = :kilo, tol = 1e-9) +# Display the series resistance and inductance table: series_rl[1, 1] + +# Display the corresponding shunt conductance and capacitance table: shunt_gc[1, 1] +# Plot the R/L and G/C frequency responses on logarithmic frequency axes. The +# accessors select the displayed quantities; each matrix family occupies one +# page with its two quantities side by side: +rlcg_plots = CairoMakie.plot( + line_parameters, + (R, L, G, C); + xscale=:log10, + length_unit=:kilo, + fig_size=(1100, 450), + display_plot=false, #hide + controls=false, #hide +) +rlcg_plots[1].figure #hide + +# The shunt conductance and capacitance responses: +rlcg_plots[2].figure #hide + +# Plot the real and imaginary parts of the complex Z/Y matrices. Each generated +# page places the two components side by side so their frequency dependence can +# be compared directly: +zy_plots = CairoMakie.plot( + line_parameters; + xscale=:log10, + length_unit=:kilo, + fig_size=(1100, 450), + display_plot=false, #hide + controls=false, #hide +) +zy_plots[1].figure #hide + +# The shunt-admittance page uses the same real/imaginary arrangement: +zy_plots[2].figure #hide + # 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); +export_file = export_data( + :atp, line_parameters; file_name=output_file, cable_system); # Obtain the symmetrical components via Fortescue transformation -Tv, p012 = Fortescue(tol = 1e-5)(p); +Tv, sequence_parameters = Fortescue(tol=1e-5)(line_parameters); -# Inspect the transformed matrices -series_zy, shunt_zy = DataFrame(p012; mode = :ZY, length_unit = :kilo, tol = 1e-9) +# Obtain the transformed series and shunt matrices: +series_zy, shunt_zy = DataFrame( + sequence_parameters; length_unit=:kilo, tol=1e-9); + +# Display the transformed series matrix: series_zy[1, 1] + +# Display the transformed shunt matrix: shunt_zy[1, 1] -# Or the corresponding lumped circuit quantities +# Obtain the corresponding lumped circuit quantities: series_rl012, shunt_gc012 = DataFrame( - p012; - mode = :RLCG, - length_unit = :kilo, - tol = 1e-9 -) + sequence_parameters, + (R, L, G, C); + length_unit=:kilo, + tol=1e-9, +); + +# Display the sequence-domain series table: series_rl012[1, 1] + +# Display the sequence-domain shunt table: shunt_gc012[1, 1] + +# Plot the sequence-domain R/L and G/C responses: +sequence_plots = CairoMakie.plot( + sequence_parameters, + (R, L, G, C); + xscale=:log10, + length_unit=:kilo, + fig_size=(1100, 450), + display_plot=false, #hide + controls=false, #hide +) +sequence_plots[1].figure #hide + +# The sequence-domain shunt response: +sequence_plots[2].figure #hide + +#= +## Conclusion + +This tutorial has demonstrated how to: + +1. Describe and preview a detailed armored HVDC cable. +2. Save and reload the cable design. +3. Place two cables in an underground bipole system. +4. Preview and export the physical system for PSCAD and ATPDraw. +5. Compute, tabulate, plot, transform, and export frequency-dependent line parameters. +=# diff --git a/ext/LineCableModelsDistributionsExt.jl b/ext/LineCableModelsDistributionsExt.jl new file mode 100644 index 00000000..f8cc43aa --- /dev/null +++ b/ext/LineCableModelsDistributionsExt.jl @@ -0,0 +1,157 @@ +module LineCableModelsDistributionsExt + +using Distributions +using Random +using Statistics + +import LineCableModels +const PB = LineCableModels.ParametricBuilder +const Computation = LineCableModels.Computation + +function PB._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::Computation.HistogramPDF, 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 + +(distribution::Computation.HistogramPDF)(value::Real) = + Distributions.pdf(distribution, value) +Distributions.minimum(distribution::Computation.HistogramPDF) = + first(distribution.edges) +Distributions.maximum(distribution::Computation.HistogramPDF) = + last(distribution.edges) +Distributions.insupport( + distribution::Computation.HistogramPDF, + value::Real, +) = minimum(distribution) <= value <= maximum(distribution) + +function Distributions.logpdf( + distribution::Computation.HistogramPDF, + value::Real, +) + probability = Distributions.pdf(distribution, value) + return probability > 0 ? log(probability) : -Inf +end + +function Distributions.cdf(distribution::Computation.HistogramPDF, value::Real) + value <= first(distribution.edges) && return 0.0 + value >= last(distribution.edges) && return 1.0 + index = searchsortedlast(distribution.edges, value) + widths = diff(distribution.edges) + prior = index == 1 ? 0.0 : + sum(distribution.density[1:(index - 1)] .* widths[1:(index - 1)]) + return prior + distribution.density[index] * + (value - distribution.edges[index]) +end + +struct HistogramPDFSampler{H,T} <: + Distributions.Sampleable{Distributions.Univariate,Distributions.Continuous} + distribution::H + cumulative_probability::Vector{T} +end + +function Distributions.sampler(distribution::Computation.HistogramPDF) + probabilities = distribution.density .* diff(distribution.edges) + cumulative = cumsum(probabilities) + cumulative[end] = one(eltype(cumulative)) + return HistogramPDFSampler(distribution, cumulative) +end + +function Distributions.quantile(sampler::HistogramPDFSampler, 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 Distributions.quantile( + distribution::Computation.HistogramPDF, + probability::Real, +) + 0 <= probability <= 1 || throw(DomainError( + probability, + "probability must lie in [0, 1]", + )) + return Distributions.quantile(Distributions.sampler(distribution), probability) +end + +function Base.rand(rng::Random.AbstractRNG, sampler::HistogramPDFSampler) + 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 + +Base.rand(rng::Random.AbstractRNG, distribution::Computation.HistogramPDF) = + rand(rng, Distributions.sampler(distribution)) + +function _raw_moment(distribution::Computation.HistogramPDF, 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 + +Distributions.moment(distribution::Computation.HistogramPDF, order::Integer) = + _raw_moment(distribution, order) +Statistics.mean(distribution::Computation.HistogramPDF) = + _raw_moment(distribution, 1) +function Statistics.var(distribution::Computation.HistogramPDF) + variance = _raw_moment(distribution, 2) - Statistics.mean(distribution)^2 + return max(variance, zero(variance)) +end +Statistics.std(distribution::Computation.HistogramPDF) = + sqrt(Statistics.var(distribution)) + +function Distributions.mode(distribution::Computation.HistogramPDF) + _, index = findmax(distribution.density) + return (distribution.edges[index] + distribution.edges[index + 1]) / 2 +end + +function Distributions.modes(distribution::Computation.HistogramPDF) + 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/LineCableModelsMakieExt.jl b/ext/LineCableModelsMakieExt.jl index 8969e22f..e7540353 100644 --- a/ext/LineCableModelsMakieExt.jl +++ b/ext/LineCableModelsMakieExt.jl @@ -38,9 +38,11 @@ end function plot( object::LineCableModels.SeriesImpedance, - frequencies; + frequencies, + quantities::Tuple = (); backend = nothing, display_plot::Bool = true, + controls::Bool = true, xscale = :linear, yscale = :linear, kwargs... @@ -49,22 +51,30 @@ function plot( LineCableModels.Engine.LineParameterPlotSpec, object; frequencies, + quantities, xscale = _scale_symbol(xscale), yscale = _scale_symbol(yscale), kwargs... ) - return UIComponents.build(render_spec; backend, display = display_plot) + return UIComponents.build(render_spec; backend, display = display_plot, controls) end -function Makie.plot(object::LineCableModels.SeriesImpedance, frequencies; kwargs...) - plot(object, frequencies; kwargs...) +function Makie.plot( + object::LineCableModels.SeriesImpedance, + frequencies, + quantities::Tuple = (); + kwargs... +) + plot(object, frequencies, quantities; kwargs...) end function plot( object::LineCableModels.ShuntAdmittance, - frequencies; + frequencies, + quantities::Tuple = (); backend = nothing, display_plot::Bool = true, + controls::Bool = true, xscale = :linear, yscale = :linear, kwargs... @@ -73,21 +83,29 @@ function plot( LineCableModels.Engine.LineParameterPlotSpec, object; frequencies, + quantities, xscale = _scale_symbol(xscale), yscale = _scale_symbol(yscale), kwargs... ) - return UIComponents.build(render_spec; backend, display = display_plot) + return UIComponents.build(render_spec; backend, display = display_plot, controls) end -function Makie.plot(object::LineCableModels.ShuntAdmittance, frequencies; kwargs...) - plot(object, frequencies; kwargs...) +function Makie.plot( + object::LineCableModels.ShuntAdmittance, + frequencies, + quantities::Tuple = (); + kwargs... +) + plot(object, frequencies, quantities; kwargs...) end function plot( - parameters::LineCableModels.LineParameters; + parameters::LineCableModels.LineParameters, + quantities::Tuple = (); backend = nothing, display_plot::Bool = true, + controls::Bool = true, xscale = :linear, yscale = :linear, kwargs... @@ -95,18 +113,23 @@ function plot( render_spec = PlotBuilder.make_render( LineCableModels.Engine.LineParameterPlotSpec, parameters; + quantities, xscale = _scale_symbol(xscale), yscale = _scale_symbol(yscale), kwargs... ) - return UIComponents.build(render_spec; backend, display = display_plot) + return UIComponents.build(render_spec; backend, display = display_plot, controls) end -function Makie.plot(parameters::LineCableModels.LineParameters; kwargs...) - plot(parameters; kwargs...) +function Makie.plot( + parameters::LineCableModels.LineParameters, + quantities::Tuple = (); + kwargs... +) + plot(parameters, quantities; kwargs...) end -function _quantity_symbol(expression) +function _monte_carlo_quantity(expression) expression isa Symbol && return expression, nothing if expression isa Expr && expression.head === :ref && length(expression.args) == 4 return Symbol(expression.args[1]), Tuple(Int.(expression.args[2:4])) @@ -115,37 +138,39 @@ function _quantity_symbol(expression) end function plot( - result::Union{LineCableModels.CableConstantsMC, LineCableModels.LineParametersMC}, - expression = :R; - ijk = nothing, - backend = nothing, - display_plot::Bool = true, - kwargs... + result::LineCableModels.MonteCarloResult, + expression=:R; + ijk=nothing, + backend=nothing, + display_plot::Bool=true, + controls::Bool=true, + kwargs..., ) - quantity, parsed_indices = _quantity_symbol(expression) + quantity, parsed_indices = _monte_carlo_quantity(expression) selection = ijk === nothing ? parsed_indices : ijk render_spec = PlotBuilder.make_render( - LineCableModels.UQ.MCDistributionPlotSpec, + LineCableModels.Computation.MCDistributionPlotSpec, result; quantity, - ijk = selection, - kwargs... + ijk=selection, + kwargs..., ) - return only(UIComponents.build(render_spec; backend, display = display_plot)) + return only(UIComponents.build(render_spec; backend, display=display_plot, controls)) end function Makie.plot( - result::Union{LineCableModels.CableConstantsMC, LineCableModels.LineParametersMC}, - expression = :R; - kwargs... + result::LineCableModels.MonteCarloResult, + expression=:R; + kwargs..., ) - plot(result, expression; kwargs...) + return plot(result, expression; kwargs...) end function preview( - design::LineCableModels.CableDesign; + design::LineCableModels.DataModel.CableDesign; backend = nothing, display_plot::Bool = true, + controls::Bool = true, kwargs... ) render_spec = PlotBuilder.make_render( @@ -153,13 +178,14 @@ function preview( design; kwargs... ) - return only(UIComponents.build(render_spec; backend, display = display_plot)) + return only(UIComponents.build(render_spec; backend, display = display_plot, controls)) end function preview( - system::LineCableModels.LineCableSystem; + system::LineCableModels.DataModel.LineCableSystem; backend = nothing, display_plot::Bool = true, + controls::Bool = true, kwargs... ) render_spec = PlotBuilder.make_render( @@ -167,12 +193,13 @@ function preview( system; kwargs... ) - return only(UIComponents.build(render_spec; backend, display = display_plot)) + return only(UIComponents.build(render_spec; backend, display = display_plot, controls)) end function show_material_scale( ; backend = nothing, display_plot::Bool = true, + controls::Bool = true, kwargs... ) render_spec = PlotBuilder.make_render( @@ -180,7 +207,7 @@ function show_material_scale( nothing; kwargs... ) - return only(UIComponents.build(render_spec; backend, display = display_plot)) + return only(UIComponents.build(render_spec; backend, display = display_plot, controls)) end end # module LineCableModelsMakieExt diff --git a/ext/LineCableModelsMeasurementsExt.jl b/ext/LineCableModelsMeasurementsExt.jl new file mode 100644 index 00000000..70ab21b1 --- /dev/null +++ b/ext/LineCableModelsMeasurementsExt.jl @@ -0,0 +1,210 @@ +module LineCableModelsMeasurementsExt + +using Calculus +using Measurements +using Printf +using SpecialFunctions +using Statistics + +import LineCableModels +const Utils = LineCableModels.Utils +const PB = LineCableModels.ParametricBuilder +const Engine = LineCableModels.Engine +const DataModel = LineCableModels.DataModel +const ImportExport = LineCableModels.ImportExport +const Computation = LineCableModels.Computation + +# Numeric promotion and presentation hooks. +Utils._direct_optional_scalar_type(::Type{<:Measurements.Measurement}) = + Measurements.Measurement{LineCableModels.Commons.BASE_FLOAT} +Utils._hascomplex_type(::Type{<:Measurements.Measurement}) = false +Utils.to_nominal(value::Measurements.Measurement) = Measurements.value(value) +Utils.uncertainty_value(value::Measurements.Measurement) = + Measurements.uncertainty(value) +Utils.to_certain(value::Measurements.Measurement) = + Measurements.measurement(Measurements.value(value), 0.0) +Utils.percent_to_uncertain(value::Real, percent::Real) = + Measurements.measurement(value, abs(value) * percent / 100) +function Utils.bias_to_uncertain( + nominal::Real, + values::AbstractVector{<:Measurements.Measurement}, +) + average = Statistics.mean(values) + bias = abs(nominal - Measurements.value(average)) + return Measurements.measurement( + Measurements.value(average), + Measurements.uncertainty(average) + bias, + ) +end +Utils.to_upper(value::Measurements.Measurement) = + Measurements.value(value) + Measurements.uncertainty(value) +Utils.to_lower(value::Measurements.Measurement) = + Measurements.value(value) - Measurements.uncertainty(value) +Utils.percent_error(value::Measurements.Measurement) = + 100 * Measurements.uncertainty(value) / Measurements.value(value) + +Utils._coerce_elt_to_T(value::Number, ::Type{M}) where {M<:Measurements.Measurement} = + zero(M) + value +Utils._coerce_elt_to_T( + value::Measurements.Measurement, + ::Type{M}, +) where {M<:Measurements.Measurement} = convert(M, value) +Utils._coerce_elt_to_T( + value::Measurements.Measurement, + ::Type{T}, +) where {T<:AbstractFloat} = convert(T, Measurements.value(value)) + +# Gridspace direct propagation. The realization cache in Gridspace guarantees +# that one shared Grid becomes one shared Measurement variable, including +# across nested object boundaries. +PB._direct_value(value::PB.UncertainValue{<:Real}) = + Measurements.measurement(value.nominal, value.sigma) +PB.materialize(value::PB.UncertainValue{<:Real}) = PB._direct_value(value) + +Engine._has_uncertainty_type( + ::Type{Complex{T}}, +) where {T<:Measurements.Measurement} = true +function Engine._clip_field(value::Measurements.Measurement, tolerance) + nominal = abs(Measurements.value(value)) <= tolerance ? + 0.0 : Measurements.value(value) + uncertainty = abs(Measurements.uncertainty(value)) <= tolerance ? + 0.0 : Measurements.uncertainty(value) + return Measurements.measurement(nominal, uncertainty) +end + +function ImportExport._serialize_value(value::Measurements.Measurement) + return Dict( + "__type__" => "Measurement", + "value" => ImportExport._serialize_value(Measurements.value(value)), + "uncertainty" => ImportExport._serialize_value(Measurements.uncertainty(value)), + ) +end +function ImportExport._deserialize_extension(::Val{:Measurement}, value) + nominal = ImportExport._deserialize_value(value["value"]) + uncertainty = ImportExport._deserialize_value(value["uncertainty"]) + return Measurements.measurement(nominal, uncertainty) +end +ImportExport.stringify(value::Measurements.Measurement) = + Printf.@sprintf( + "%.12g ± %.6g", + Measurements.value(value), + Measurements.uncertainty(value), + ) + +# Uncertainty-aware SpecialFunctions methods used by the numerical kernels. +function _lift_complex(function_value, order, value::Complex{<:Measurements.Measurement}) + nominal = Measurements.value(value) + return Measurements.result( + function_value(order, nominal), + vcat( + Calculus.gradient( + point -> real(function_value(order, complex(point...))), + collect(reim(nominal)), + ), + Calculus.gradient( + point -> imag(function_value(order, complex(point...))), + collect(reim(nominal)), + ), + ), + value, + ) +end + +for name in ( + :besselix, :besselkx, :besseljx, :besselyx, :besselhx, + :besseli, :besselk, :besselj, :bessely, :besselh, +) + @eval begin + function SpecialFunctions.$name( + order::Real, + value::Complex{<:Measurements.Measurement}, + ) + return _lift_complex(SpecialFunctions.$name, order, value) + end + end +end + +function _joint_coordinates(matrix::AbstractMatrix{T}) where {T<:Real} + means = vec(Statistics.mean(matrix; dims=2)) + size(matrix, 2) == 1 && return map( + value -> Measurements.measurement(value, zero(T)), means, + ) + factor = matrix .- means + factor ./= sqrt(size(matrix, 2) - 1) + latent = [Measurements.measurement(zero(T), one(T)) for _ in axes(matrix, 2)] + return means + factor * latent +end + +function _independent_coordinates(means, deviations) + return map(Measurements.measurement, means, deviations) +end + +function Measurements.measurement(result::Computation.MonteCarloResult{<:DataModel.CableConstants}) + if result.samples === nothing + @warn "MonteCarloResult has no retained samples; reconstructing independent marginal Measurements and discarding output correlations" + mean = result.representation + sigma = result.uncertain.sigma + return DataModel.CableConstants( + Measurements.measurement(mean.R, sigma.R), + Measurements.measurement(mean.L, sigma.L), + Measurements.measurement(mean.C, sigma.C), + ) + end + matrix = permutedims(hcat( + result.samples.R, + result.samples.L, + result.samples.C, + )) + return DataModel.CableConstants(_joint_coordinates(matrix)...) +end + +function _line_measurements(result::Computation.MonteCarloResult{<:Engine.LineParameters}) + nominal = result.representation + tensor_size = size(nominal.Z) + coordinate_count = prod(tensor_size) + if result.samples === nothing + @warn "MonteCarloResult has no retained samples; reconstructing independent marginal Measurements and discarding output correlations" + means = ( + real.(nominal.Z.values), + imag.(nominal.Z.values) ./ reshape(2π .* nominal.f, 1, 1, :), + real.(nominal.Y.values), + imag.(nominal.Y.values) ./ reshape(2π .* nominal.f, 1, 1, :), + ) + deviations = result.uncertain.sigma + blocks = map( + _independent_coordinates, + means, + (deviations.R, deviations.L, deviations.G, deviations.C), + ) + else + sample_values = result.samples + trial_count = size(sample_values.R, 4) + matrix = vcat( + reshape(sample_values.R, coordinate_count, trial_count), + reshape(sample_values.L, coordinate_count, trial_count), + reshape(sample_values.G, coordinate_count, trial_count), + reshape(sample_values.C, coordinate_count, trial_count), + ) + joint = _joint_coordinates(matrix) + blocks = ntuple(index -> reshape( + joint[((index - 1) * coordinate_count + 1):(index * coordinate_count)], + tensor_size, + ), 4) + end + resistance, inductance, conductance, capacitance = blocks + angular = reshape(2π .* nominal.f, 1, 1, :) + impedance = complex.(resistance, inductance .* angular) + admittance = complex.(conductance, capacitance .* angular) + return Engine.LineParameters( + LineCableModels.domain(nominal), + impedance, + admittance, + nominal.f; + basis=LineCableModels.basis(nominal), + ) +end + +Measurements.measurement(result::Computation.MonteCarloResult{<:Engine.LineParameters}) = + _line_measurements(result) + +end diff --git a/integration/plotting/_gallery_fixtures.jl b/integration/plotting/_gallery_fixtures.jl index 80f7c375..56f6dd60 100644 --- a/integration/plotting/_gallery_fixtures.jl +++ b/integration/plotting/_gallery_fixtures.jl @@ -1,4 +1,6 @@ using LineCableModels +using LineCableModels.DataModel +using LineCableModels.EarthProps using Measurements: measurement function build_manual_plot_gallery( @@ -32,22 +34,23 @@ function build_manual_plot_gallery( ) summary = SampleSummary([1.0, 2.0, 3.0, 4.0]) - distribution_model = HistogramPDF([1.0, 3.0, 5.0], [0.25, 0.25]) - mc_result = CableConstantsMC( + histogram = HistogramPDF([1.0, 3.0, 5.0], [0.25, 0.25]) + mc_result = MonteCarloResult( + CableConstants(2.5, 2.5, 2.5), CableConstants(summary, summary, summary), CableConstants( [1.0, 2.0, 3.0, 4.0], [1.0, 2.0, 3.0, 4.0], - [1.0, 2.0, 3.0, 4.0] - ), - CableConstants(distribution_model, distribution_model, distribution_model), - CableConstants( - measurement(2.5, 0.5), - measurement(2.5, 0.5), - measurement(2.5, 0.5) + [1.0, 2.0, 3.0, 4.0], ), + CableConstants(histogram, histogram, histogram), + nothing, 4, - 0.95 + 0.95, + 0.02, + :normal, + UInt64(1), + (hash="gallery-fixture",), ) gallery = Pair{String, UIPlot}[] @@ -60,14 +63,12 @@ function build_manual_plot_gallery( add_pages!( "Line parameters: RLCG", - Makie.plot(parameters; mode = :RLCG, backend, display_plot, export_theme) + Makie.plot(parameters, (R, L, G, C); backend, display_plot, export_theme) ) add_pages!( "Line parameters: Z/Y Cartesian", Makie.plot( parameters; - mode = :ZY, - coord = :cart, backend, display_plot, export_theme @@ -76,9 +77,8 @@ function build_manual_plot_gallery( add_pages!( "Line parameters: Z/Y polar", Makie.plot( - parameters; - mode = :ZY, - coord = :polar, + parameters, + (abs, angle); backend, display_plot, export_theme @@ -87,8 +87,8 @@ function build_manual_plot_gallery( add_pages!( "Line parameters: measurement error bars", Makie.plot( - measurement_parameters; - mode = :RLCG, + measurement_parameters, + (R, L, G, C); backend, display_plot, export_theme @@ -102,11 +102,11 @@ function build_manual_plot_gallery( mc_result, :R; mode, - data = :both, + data=:both, backend, display_plot, - export_theme - ) + export_theme, + ), ) end diff --git a/integration/plotting/manual_gl.jl b/integration/plotting/manual_gl.jl index bb870d38..b882a052 100644 --- a/integration/plotting/manual_gl.jl +++ b/integration/plotting/manual_gl.jl @@ -25,8 +25,8 @@ parameters = LineParameters( ) plots = Makie.plot( - parameters; - mode = :RLCG, + parameters, + (R, L, G, C); backend = :gl, display_plot = true, open_export = false @@ -46,7 +46,7 @@ set_backend!(:gl) @testset "manual GL plotting gate" begin @test plots isa Vector{UIPlot} - @test length(plots) == 4 + @test length(plots) == 2 @test all(plot -> plot.context.window !== nothing, plots) @test length(unique(objectid(plot.context.window) for plot in plots)) == length(plots) @test sort!(collect(keys(handle.controls))) == @@ -58,13 +58,16 @@ set_backend!(:gl) handle.controls[:xlog].active[] = true handle.controls[:ylog].active[] = true - @test only(handle.panels).axis.xscale[] === Makie.log10 - @test only(handle.panels).axis.yscale[] === Makie.log10 + @test all(panel -> panel.axis.xscale[] === Makie.log10, handle.panels) + @test all(panel -> panel.axis.yscale[] === Makie.log10, handle.panels) legend = handle.controls[:legend] entry = first(last(first(legend.entrygroups[]))) Makie.toggle_visibility!(entry) - @test any(plot_object -> !plot_object.visible[], only(handle.panels).plots) + @test any( + plot_object -> !plot_object.visible[], + Iterators.flatten(panel.plots for panel in handle.panels), + ) Makie.toggle_visibility!(entry) handle.controls[:reset].clicks[] += 1 @@ -86,13 +89,11 @@ set_backend!(:gl) susceptance = last(Makie.plot( parameters; - mode = :ZY, - coord = :cart, backend = :gl, display_plot = false )) susceptance.controls[:ylog].active[] = true - susceptance_axis = only(susceptance.panels).axis + susceptance_axis = last(susceptance.panels).axis @test susceptance_axis.yscale[] === Makie.log10 @test susceptance_axis.ytickformat[] === Makie.automatic @test susceptance_axis.ylabel[] == "Capacitive susceptance [S/km]" diff --git a/showcase/showcase1.jl b/showcase/showcase1.jl index 8cee80a0..ce991117 100644 --- a/showcase/showcase1.jl +++ b/showcase/showcase1.jl @@ -38,6 +38,11 @@ end begin using CairoMakie, PlutoUI, Colors using LineCableModels + using LineCableModels.DataModel + using LineCableModels.EarthProps + using LineCableModels.Materials: MaterialsLibrary + import LineCableModels.DataModel: Insulator + import LineCableModels.Materials: Material using DataFrames using HypertextLiteral end @@ -56,12 +61,12 @@ begin 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"))) + core = ConductorGroup(CircStrands(0, d/2, 1, 0, get(materials, "aluminum"))) for ℓ in 1:n_layers add!( core, CircStrands, - Diameter(d), + d/2, 6*ℓ, pitch_for_layer(ℓ), get(materials, "aluminum") @@ -88,12 +93,12 @@ begin # Insulation group main_insu = InsulatorGroup( - Semicon(core, Thickness(t_sct), get(materials, "polyacrylate")), + Semicon(core.r_ex, get(materials, "polyacrylate"); thickness = t_sct), ) - 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")) + add!(main_insu, Semicon, get(materials, "semicon1"); thickness = t_sc_in) + add!(main_insu, Insulator, get(materials, "pe"); thickness = t_ins) + add!(main_insu, Semicon, get(materials, "semicon2"); thickness = t_sc_out) + add!(main_insu, Semicon, get(materials, "polyacrylate"); thickness = t_sct) core_cc = CableComponent("core", core, main_insu) return core_cc, main_insu @@ -766,8 +771,8 @@ begin lay_ratio = 10 # typical value for wire screens screen_con = ConductorGroup( CircStrands( - main_insu, - Diameter(d_ws), + main_insu.r_ex, + d_ws/2, num_sc_wires, lay_ratio, get(materials, "copper") @@ -777,15 +782,15 @@ begin add!( screen_con, Strip, - Thickness(t_cut), w_cut, lay_ratio, - get(materials, "copper") + get(materials, "copper"); + thickness = t_cut, ) # Water blocking tape over screen: screen_insu = InsulatorGroup( - Semicon(screen_con, Thickness(t_wbt), get(materials, "polyacrylate")), + Semicon(screen_con.r_ex, get(materials, "polyacrylate"); thickness = t_wbt), ) # Group sheath components and assign to design: @@ -793,20 +798,20 @@ begin # Add the aluminum foil (moisture barrier): jacket_con = ConductorGroup( - Tubular(screen_insu, Thickness(t_alt), get(materials, "aluminum")), + Tubular(screen_insu.r_ex, get(materials, "aluminum"); thickness = t_alt), ) # PE layer after aluminum foil: jacket_insu = InsulatorGroup( - Insulator(jacket_con, Thickness(t_pet), get(materials, "pe")), + Insulator(jacket_con.r_ex, get(materials, "pe"); thickness = t_pet), ) # PE jacket (outer mechanical protection): add!( jacket_insu, Insulator, - Thickness(t_jac), - get(materials, "pe") + get(materials, "pe"); + thickness = t_jac, ) cable_id = "showcase" @@ -815,7 +820,7 @@ begin add!(cable_design, "jacket", jacket_con, jacket_insu) backend_sym = :cairo - plt, _ = preview(cable_design; size = (800, 500), backend = backend_sym) + plt = preview(cable_design; size = (800, 500), backend = backend_sym) plt end @@ -836,7 +841,7 @@ md""" # ╔═╡ 9ddcccbe-86c8-4335-8d65-35af4ce755ab begin - core_df = DataFrame(cable_design, :baseparams) + core_df = DataFrame(compute!(CableConstantsProblem(cable_design), Formulation())) core_df end @@ -845,12 +850,12 @@ cable_emt = equivalent(cable_design) # ╔═╡ ae1749c8-0f6d-4487-8857-12826eb57db3 begin - plt2, _ = preview(cable_design; size = (800, 500), backend = backend_sym) + 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 = preview(cable_emt; size = (800, 500), backend = backend_sym) plt3 end @@ -899,7 +904,7 @@ end # ╔═╡ 6ee6d16d-326c-4436-a750-077ecc2b3b9c begin - plt4, _ = preview( + plt4 = preview( cable_system, earth_model = earth_params, zoom_factor = 2.0, @@ -940,8 +945,8 @@ begin # Build the wire screens on top of the previous layer: sscreen_con = ConductorGroup( CircStrands( - main_insu, - Diameter(d_ws), + mmain_insu.r_ex, + d_ws/2, num_sc_wires, lay_ratio, get(materials, "copper") @@ -951,15 +956,15 @@ begin add!( sscreen_con, Strip, - Thickness(t_cut), w_cut, lay_ratio, - get(materials, "copper") + get(materials, "copper"); + thickness = t_cut, ) # Water blocking tape over screen: sscreen_insu = InsulatorGroup( - Semicon(sscreen_con, Thickness(t_wbt), get(materials, "polyacrylate")), + Semicon(sscreen_con.r_ex, get(materials, "polyacrylate"); thickness = t_wbt), ) # Group sheath components and assign to design: @@ -967,20 +972,20 @@ begin # Add the aluminum foil (moisture barrier): jjacket_con = ConductorGroup( - Tubular(sscreen_insu, Thickness(t_alt), get(materials, "aluminum")), + Tubular(sscreen_insu.r_ex, get(materials, "aluminum"); thickness = t_alt), ) # PE layer after aluminum foil: jjacket_insu = InsulatorGroup( - Insulator(jjacket_con, Thickness(t_pet), get(materials, "pe")), + Insulator(jjacket_con.r_ex, get(materials, "pe"); thickness = t_pet), ) # PE jacket (outer mechanical protection): add!( jjacket_insu, Insulator, - Thickness(t_jac), - get(materials, "pe") + get(materials, "pe"); + thickness = t_jac, ) ccable_design = CableDesign(cable_id, ccore_cc; nominal_data = datasheet_info) @@ -1011,19 +1016,14 @@ begin 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 - ) + compute_options = (verbosity = 0,) end; # ╔═╡ cb44ffb8-7e33-4603-a97e-47dbc507f813 begin using LineCableModels.Engine using LineCableModels.Engine.Transforms: Fortescue - F = FormulationSet(:EMT, + F = Formulation(:EMT, internal_impedance = InternalImpedance.ScaledBessel(), insulation_impedance = InsulationImpedance.Lossless(), earth_impedance = EarthImpedance.Papadopoulos(), @@ -1031,7 +1031,6 @@ begin earth_admittance = EarthAdmittance.Papadopoulos(), modal_transform = Transforms.Fortescue(), equivalent_earth = EHEM.EnforceLayer(layer = -1), # Use the last layer as effective earth - options = opts ) end; @@ -1116,22 +1115,21 @@ end # ╔═╡ e8117400-adf3-45e3-bf56-59933f01e6d0 # ╠═╡ show_logs = false begin - @time ws, p012 = compute!(problem, F) + @time p012 = compute!(problem, F; options = compute_options) #Tv, p012 = Fortescue(tol = 1e-5)(p) end; # ╔═╡ cebe81ec-a183-43d7-be36-6627a46de3bf begin fig = plot( - p012; + p012, + (R, L, G, C); backend = :cairo, - mode = :RLCG, length_unit = :kilo, - xscale = log10, - per_length = true + xscale = :log10 ) - fig[(:series_impedance, :resistance)].figure + first(fig).figure end # ╔═╡ c6cdfb66-1405-4208-808b-12f3e0949ed1 diff --git a/src/LineCableModels.jl b/src/LineCableModels.jl index d5886f9f..020b3a22 100644 --- a/src/LineCableModels.jl +++ b/src/LineCableModels.jl @@ -9,31 +9,25 @@ export Z, Y, R, X, L, G, B, C export series_impedance, shunt_admittance, resistance, reactance, inductance, conductance, susceptance, capacitance -export ParametricSweep, cases, results, ncases - -# 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, CableConstants, NominalData -export CablesLibrary -export CablePosition, LineCableSystem -export trifoil_formation, flat_formation, preview, equivalent, MaxFill - -# Earth properties: -export EarthModel +# High-level modeling grammar: +export Grid, AbsoluteError, DeterministicGrid, RelativeGrid, AbsoluteGrid +export AbstractGrid, AbstractUncertainGrid, UncertainValue +export AbstractSpec, Gridspace, Configuration, configurations, materialize +export has_uncertainty, configuration_manifest, nominal, standard_uncertainty +export Material, MaterialsLibrary, Conductor, Insulator, CableBuilder +export at, trifoil, hflat, vflat, Earth, SystemBuilder +export make_stranded, make_screened + +# Materialized results, reusable designs, and presentation: +export CableConstants, CableConstantsProblem, LineParameters, CablesLibrary, preview # Engine: -export LineParametersProblem, - FormulationSet, compute!, SeriesImpedance, ShuntAdmittance, kronify, +export Formulation, compute!, SeriesImpedance, ShuntAdmittance, kronify, LineParameters, PhaseDomain, ModalDomain - -# Parametric builder: -# export make_stranded, make_screened -# export conductor, insulator +export Fortescue +export FullParametric, MonteCarlo, FullParametricResult, MonteCarloResult +export CalculationManifest, ConfigurationFailure, SampleSummary, HistogramPDF, RLCG +export result, statistics, samples, histograms, uncertain_value, manifest # Import/Export: export export_data, save, load! @@ -49,9 +43,6 @@ using .Commons: IMPORTS, EXPORTS, add!, PhaseDomain, ModalDomain, domain, resistance, reactance, inductance, conductance, susceptance, capacitance, frequencies, nconductors, nfrequencies -# Submodule `UncertainBessels` -include("uncertainbessels/UncertainBessels.jl") - # Submodule `Utils` include("utils/Utils.jl") using .Utils: set_verbosity! @@ -70,49 +61,43 @@ export UIPlot, export_svg # Submodule `Materials` include("materials/Materials.jl") +import .Materials using .Materials: Material, MaterialsLibrary # Submodule `EarthProps` include("earthprops/EarthProps.jl") -using .EarthProps: EarthModel +import .EarthProps # Submodule `DataModel` include("datamodel/DataModel.jl") -using .DataModel: Thickness, Diameter, CircStrands, RectStrands, Strip, Tubular, Semicon, - Insulator, ConductorGroup, InsulatorGroup, CableComponent, CableDesign, - CableConstants, - NominalData, - CablesLibrary, CablePosition, LineCableSystem, trifoil_formation, - flat_formation, - preview, equivalent, MaxFill, Sector, SectorParams, - SectorInsulator +using .DataModel: CableConstants, CablesLibrary, preview # Submodule `Engine` include("engine/Engine.jl") -using .Engine: LineParametersProblem, compute!, LineParameters, SeriesImpedance, - ShuntAdmittance, kronify, FormulationSet +using .Engine: compute!, LineParameters, SeriesImpedance, + ShuntAdmittance, kronify, Formulation, CableConstantsProblem +using .Engine.Transforms: Fortescue # Submodule `ParametricBuilder` include("parametricbuilder/ParametricBuilder.jl") -using .ParametricBuilder: ParametricSweep, cases, results, ncases - -# Submodule `UQ` -include("uq/UQ.jl") -using .UQ: SampleSummary, RLCG, HistogramPDF, CableConstantsMC, LineParametersMC, - sample, trial, mc, statistics, has_samples, samples, - has_distributions, distribution, surrogate, ntrials, confidence, - mean, std, quantile -export SampleSummary, RLCG, HistogramPDF, CableConstantsMC, LineParametersMC, - sample, trial, mc, statistics, has_samples, samples, - has_distributions, distribution, surrogate, ntrials, confidence, - mean, std, quantile +using .ParametricBuilder: + Grid, AbsoluteError, DeterministicGrid, RelativeGrid, AbsoluteGrid, + AbstractGrid, AbstractUncertainGrid, UncertainValue, + AbstractSpec, Gridspace, Configuration, configurations, materialize, + has_uncertainty, configuration_manifest, nominal, standard_uncertainty, + Conductor, Insulator, CableBuilder, + at, trifoil, hflat, vflat, Earth, SystemBuilder, + make_stranded, make_screened + +# Unified Gridspace computation policies and typed result grammar. +include("computation/Computation.jl") +using .Computation: + FullParametric, MonteCarlo, FullParametricResult, MonteCarloResult, + CalculationManifest, ConfigurationFailure, SampleSummary, HistogramPDF, RLCG, + result, statistics, samples, histograms, uncertain_value, manifest # Submodule `ImportExport` include("importexport/ImportExport.jl") using .ImportExport: export_data, load!, save -# Aliases for backward compatibility -const WireArray = CircStrands # alias for now -export WireArray # export aliases - end diff --git a/src/commons/Commons.jl b/src/commons/Commons.jl index c4969ff4..13656f1c 100644 --- a/src/commons/Commons.jl +++ b/src/commons/Commons.jl @@ -28,7 +28,7 @@ function basis end """ Z(parameters[, i, j[, k]]) -Return series impedance in the canonical units selected by [`basis`](@ref). +Return series impedance in the units selected by [`basis`](@ref). Index selection is defined by the concrete result container. """ function Z end @@ -36,7 +36,7 @@ function Z end """ Y(parameters[, i, j[, k]]) -Return shunt admittance in the canonical units selected by [`basis`](@ref). +Return shunt admittance in the units selected by [`basis`](@ref). Index selection is defined by the concrete result container. """ function Y end diff --git a/src/commons/consts.jl b/src/commons/consts.jl index ee963126..7648a156 100644 --- a/src/commons/consts.jl +++ b/src/commons/consts.jl @@ -18,8 +18,8 @@ const ΔTmax = 150.0 "Default tolerance for floating-point comparisons, TOL = 1e-6." const TOL = 1e-6 -# Define aliases for the type constraints -using Measurements: Measurement +# Dependency-free numeric interfaces. Optional scalar packages such as +# Measurements.jl participate through ordinary `Real` promotion in extensions. const BASE_FLOAT = Float64 -const REALSCALAR = Union{BASE_FLOAT, Measurement{BASE_FLOAT}} -const COMPLEXSCALAR = Union{Complex{BASE_FLOAT}, Complex{Measurement{BASE_FLOAT}}} +const REALSCALAR = Real +const COMPLEXSCALAR = Complex{T} where {T<:Real} diff --git a/src/computation/Computation.jl b/src/computation/Computation.jl new file mode 100644 index 00000000..6933d616 --- /dev/null +++ b/src/computation/Computation.jl @@ -0,0 +1,939 @@ +module Computation + +export AbstractRunPolicy, FullParametric, MonteCarlo +export FullParametricResult, MonteCarloResult, CalculationManifest +export ConfigurationFailure, SampleSummary, HistogramPDF, RLCG +export result, statistics, samples, histograms, uncertain_value, manifest + +using Random +using SHA +using Statistics + +using ..Commons: + PhaseDomain, basis, domain, frequencies, TYPEDEF, TYPEDFIELDS, + TYPEDSIGNATURES +import ..DataModel +import ..Engine +import ..Engine: compute! +import ..ParametricBuilder +import ..PlotBuilder +import ..UnitHandler +using ..ParametricBuilder: + AbstractSpec, Configuration, UncertainValue, AbsoluteUncertainty, + configurations, configuration_manifest, has_uncertainty, materialize + +struct CableConstantsMaterializer end +function (::CableConstantsMaterializer)(design, separation, earth_resistivity) + return Engine.CableConstantsProblem( + design; + separation, + earth_resistivity, + ) +end + +function Engine.CableConstantsProblem( + design::AbstractSpec{<:DataModel.CableDesign}; + separation=nothing, + earth_resistivity=100.0, + combine::Symbol=:product, +) + return ParametricBuilder.Gridspace{Engine.CableConstantsProblem}( + CableConstantsMaterializer(), + ( + ParametricBuilder._gridspace_axis(design), + ParametricBuilder._gridspace_axis(separation), + ParametricBuilder._gridspace_axis(earth_resistivity), + ), + (:design, :separation, :earth_resistivity); + combine, + ) +end + +""" +$(TYPEDEF) + +Supertype for policies that evaluate a formulation over parameter +configurations or stochastic realizations. +""" +abstract type AbstractRunPolicy end + +""" +$(TYPEDEF) + +Evaluate every configuration admitted by a recursively composed Gridspace. +Invalid configurations fail the traversal by default. `invalid=:skip` retains +an auditable [`ConfigurationFailure`](@ref) for each skipped configuration. + +$(TYPEDFIELDS) +""" +struct FullParametric <: AbstractRunPolicy + "Handling of invalid configurations: `:error` or `:skip`." + invalid::Symbol + + function FullParametric(; invalid::Symbol=:error) + invalid in (:error, :skip) || throw(ArgumentError( + "invalid must be :error or :skip; got :$invalid", + )) + return new(invalid) + end +end + +""" +$(TYPEDEF) + +Sample stochastic realizations conditional on each outer Gridspace +configuration. When `trials === nothing`, the trial count is selected with a +simultaneous Dvoretzky–Kiefer–Wolfowitz bound over the scalar observables. +`cdf_tol` is therefore an absolute empirical-CDF error bound, not a numerical +solver tolerance. With `M` scalar observables and confidence `1-α`, the policy +uses `ceil(log(2M/α) / (2*cdf_tol^2))`, so a union bound controls the maximum +empirical-CDF deviation across those marginals. It does not bound solver error, +mean error, or joint-distribution error. + +$(TYPEDFIELDS) +""" +struct MonteCarlo{D,S} <: AbstractRunPolicy + "Requested number of realizations, or `nothing` for DKW-based sizing." + trials::Union{Nothing,Int} + + "Simultaneous marginal-CDF confidence level." + confidence::Float64 + + "Absolute empirical-CDF deviation used for automatic trial sizing." + cdf_tol::Float64 + + "Sampling distribution or compatible sampler." + distribution::D + + "Root random seed, or `nothing` to obtain one from the system random device." + seed::S + + "Whether complete observable samples are retained." + return_samples::Bool + + "Whether marginal histogram densities are retained." + return_histograms::Bool + + "Requested number of histogram bins, or `nothing` for automatic selection." + bins::Union{Nothing,Int} + + "Handling of invalid configurations: `:error` or `:skip`." + invalid::Symbol + + function MonteCarlo(; + trials::Union{Nothing,Integer}=nothing, + confidence::Real=0.95, + cdf_tol::Real=0.02, + distribution=:normal, + seed::Union{Nothing,Integer}=nothing, + return_samples::Bool=false, + return_histograms::Bool=false, + bins::Union{Nothing,Integer}=nothing, + invalid::Symbol=:error, + ) + trials === nothing || trials > 0 || + throw(ArgumentError("trials must be positive")) + 0 < confidence < 1 || + throw(ArgumentError("confidence must lie between zero and one")) + 0 < cdf_tol < 1 || + throw(ArgumentError("cdf_tol must lie between zero and one")) + bins === nothing || bins > 0 || + throw(ArgumentError("bins must be positive")) + invalid in (:error, :skip) || throw(ArgumentError( + "invalid must be :error or :skip; got :$invalid", + )) + distribution isa Symbol && distribution ∉ (:normal, :uniform) && + throw(ArgumentError( + "unsupported distribution :$distribution; expected :normal, :uniform, a sampler function, or an extension-supported distribution", + )) + actual_seed = seed === nothing ? nothing : UInt64(seed) + return new{typeof(distribution),typeof(actual_seed)}( + trials === nothing ? nothing : Int(trials), + Float64(confidence), + Float64(cdf_tol), + distribution, + actual_seed, + return_samples, + return_histograms, + bins === nothing ? nothing : Int(bins), + invalid, + ) + end +end + +""" +$(TYPEDEF) + +Store descriptive statistics for one scalar Monte Carlo observable. + +$(TYPEDFIELDS) +""" +struct SampleSummary{T<:Real} + "Sample mean, in the physical unit of the observable." + mean::T + + "Sample standard deviation, in the physical unit of the observable." + std::T + + "Minimum sampled value, in the physical unit of the observable." + min::T + + "Fifth sample percentile, in the physical unit of the observable." + q05::T + + "Median sampled value, in the physical unit of the observable." + q50::T + + "Ninety-fifth sample percentile, in the physical unit of the observable." + q95::T + + "Maximum sampled value, in the physical unit of the observable." + max::T + + function SampleSummary( + mean::T, + std::T, + min::T, + q05::T, + q50::T, + q95::T, + max::T, + ) where {T<:Real} + all(isfinite, (mean, std, min, q05, q50, q95, max)) || + throw(ArgumentError("sample summary values must be finite")) + std >= zero(std) || throw(ArgumentError( + "sample standard deviation must be nonnegative", + )) + min <= q05 <= q50 <= q95 <= max || throw(ArgumentError( + "sample quantiles must be ordered between the minimum and maximum", + )) + min <= mean <= max || throw(ArgumentError( + "sample mean must lie between the minimum and maximum", + )) + return new{T}(mean, std, min, q05, q50, q95, max) + end +end + +""" +$(TYPEDSIGNATURES) + +Calculate a [`SampleSummary`](@ref) from a nonempty vector of finite values. +""" +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...) +end + +""" +$(TYPEDEF) + +Represent a normalized piecewise-constant marginal probability density. + +$(TYPEDFIELDS) +""" +struct HistogramPDF{T<:AbstractFloat} + "Bin edges in the physical unit of the observable." + edges::Vector{T} + + "Probability densities, in the reciprocal unit of the observable." + density::Vector{T} + + function HistogramPDF(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(x -> isfinite(x) && x >= 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 + +""" +$(TYPEDSIGNATURES) + +Construct and normalize a piecewise-constant histogram density. +""" +function HistogramPDF( + edges::AbstractVector{<:Real}, + density::AbstractVector{<:Real}, +) + T = promote_type(float(eltype(edges)), float(eltype(density))) + isconcretetype(T) || (T = Float64) + return HistogramPDF(Vector{T}(edges), Vector{T}(density)) +end + +""" +$(TYPEDEF) + +Group resistance, inductance, capacitance, and conductance representations. + +$(TYPEDFIELDS) +""" +struct RLCG{T} + "Resistance representation `\\[Ω/m\\]` or `\\[Ω\\]`, according to result basis." + R::T + + "Inductance representation `\\[H/m\\]` or `\\[H\\]`, according to result basis." + L::T + + "Capacitance representation `\\[F/m\\]` or `\\[F\\]`, according to result basis." + C::T + + "Conductance representation `\\[S/m\\]` or `\\[S\\]`, according to result basis." + G::T +end + +""" +$(TYPEDEF) + +Record the inputs that define one calculation or complete Gridspace traversal +and their stable digest. + +$(TYPEDFIELDS) +""" +struct CalculationManifest{R,P,F,S,E,O} + "Resolved Gridspace parameterization." + resolved_parameterization::R + + "Materialized problem assumptions before evaluation." + problem_assumptions::P + + "Formulation and formulation options." + formulation::F + + "Numerical solver identifier." + solver::S + + "Configuration or realization evaluation policy." + execution_policy::E + + "Computation options supplied to `compute!`." + calculation_options::O + + "SHA-256 digest of the stable manifest serialization." + hash::String +end + +""" +$(TYPEDSIGNATURES) + +Create a reproducible calculation manifest and its SHA-256 digest. +""" +function CalculationManifest( + resolved_parameterization, + problem_assumptions, + formulation, + execution_policy, + calculation_options, +) + solver = string(typeof(formulation)) + payload = ( + resolved_parameterization=resolved_parameterization, + problem_assumptions=problem_assumptions, + formulation=_manifest_tree(formulation), + solver, + execution_policy=_manifest_tree(execution_policy), + calculation_options=_manifest_tree(calculation_options), + ) + digest = bytes2hex(sha256(_stable_bytes(payload))) + return CalculationManifest( + payload.resolved_parameterization, + payload.problem_assumptions, + payload.formulation, + payload.solver, + payload.execution_policy, + payload.calculation_options, + digest, + ) +end + +""" +$(TYPEDEF) + +Record an invalid configuration skipped by explicit request. + +$(TYPEDFIELDS) +""" +struct ConfigurationFailure{C} + "One-based position of the configuration in the traversal." + index::Int + + "Resolved parameter values for the configuration." + configuration::C + + "Qualified exception type name." + exception_type::String + + "Rendered exception message." + message::String +end + +""" +$(TYPEDEF) + +Store the results and parameter values from a complete Gridspace traversal. + +$(TYPEDFIELDS) +""" +struct FullParametricResult{T,C,F,M} <: AbstractVector{T} + "Primitive result for each successfully evaluated configuration." + values::Vector{T} + + "Resolved parameterization corresponding to each value." + configurations::Vector{C} + + "Invalid configurations skipped by explicit request." + failures::Vector{F} + + "Manifest describing the complete traversal." + manifest::M +end + +Base.size(value::FullParametricResult) = size(value.values) +Base.length(value::FullParametricResult) = length(value.values) +Base.getindex(value::FullParametricResult, index::Integer) = value.values[index] +Base.iterate(value::FullParametricResult, state...) = iterate(value.values, state...) +Base.IndexStyle(::Type{<:FullParametricResult}) = IndexLinear() + +""" +$(TYPEDEF) + +Store one Monte Carlo analysis of primitive result type `T`. + +$(TYPEDFIELDS) +""" +struct MonteCarloResult{T,S,Sa,H,U,D,M} + "Complete primitive result reconstructed from the sample means." + representation::T + + "Marginal descriptive statistics in the shape of `T`." + statistics::S + + "Retained realization values, or `nothing`." + samples::Sa + + "Retained marginal histogram densities, or `nothing`." + histograms::H + + "Standard-uncertainty representation in the shape of `T`." + uncertain::U + + "Number of stochastic realizations." + trials::Int + + "Simultaneous marginal-CDF confidence level used for automatic sizing." + confidence::Float64 + + "Absolute empirical-CDF deviation used for automatic sizing." + cdf_tol::Float64 + + "Distribution or sampler used to draw realizations." + distribution::D + + "Random seed used for this configuration." + seed::UInt64 + + "Manifest describing this Monte Carlo analysis." + manifest::M +end + +""" +$(TYPEDSIGNATURES) + +Return the complete primitive representation stored in a computation result. +""" +result(value::FullParametricResult) = value.values +result(value::MonteCarloResult) = value.representation + +""" +$(TYPEDSIGNATURES) + +Return the marginal descriptive statistics of a Monte Carlo result. +""" +statistics(value::MonteCarloResult) = value.statistics + +""" +$(TYPEDSIGNATURES) + +Return retained realizations, or `nothing` when they were not requested. +""" +samples(value::MonteCarloResult) = value.samples + +""" +$(TYPEDSIGNATURES) + +Return retained marginal histogram densities, or `nothing` when they were not +requested. +""" +histograms(value::MonteCarloResult) = value.histograms + +""" +$(TYPEDSIGNATURES) + +Return the standard-uncertainty representation of a Monte Carlo result. +""" +uncertain_value(value::MonteCarloResult) = value.uncertain + +""" +$(TYPEDSIGNATURES) + +Return the calculation manifest associated with a result. +""" +manifest(value::Union{FullParametricResult,MonteCarloResult}) = value.manifest + +# Stable serialization deliberately avoids Julia's session-randomized hash +# and object addresses. Automatic Grid coupling identities are assigned stable +# first-occurrence labels during structural traversal. +mutable struct _ManifestState + automatic_keys::IdDict{Any,Int} +end +_ManifestState() = _ManifestState(IdDict{Any,Int}()) + +function _manifest_tree(value, state::_ManifestState) + if value isa ParametricBuilder.AutomaticGridKey + label = get!(state.automatic_keys, value.token) do + length(state.automatic_keys) + 1 + end + return (automatic_grid=label,) + elseif value isa ParametricBuilder.NamedGridKey + return (named_grid=_manifest_tree(value.value, state),) + elseif value isa Type + return string(value) + elseif value isa Function + names = fieldnames(typeof(value)) + fields = map( + name -> _manifest_tree(getfield(value, name), state), + names, + ) + return ( + function_type=string(typeof(value)), + fields=NamedTuple{names}(fields), + ) + elseif value isa NamedTuple + return NamedTuple{keys(value)}(map(item -> _manifest_tree(item, state), values(value))) + elseif value isa AbstractDict + entries = [ + (_manifest_tree(key, state), _manifest_tree(item, state)) + for (key, item) in pairs(value) + ] + sort!(entries; by=entry -> String(_stable_bytes(first(entry)))) + return (type=string(typeof(value)), entries=tuple(entries...)) + elseif value isa AbstractSet + entries = [_manifest_tree(item, state) for item in value] + sort!(entries; by=entry -> String(_stable_bytes(entry))) + return (type=string(typeof(value)), entries=tuple(entries...)) + elseif value isa Tuple + return map(item -> _manifest_tree(item, state), value) + elseif value isa AbstractArray + return (type=string(typeof(value)), size=size(value), values=map( + item -> _manifest_tree(item, state), Tuple(value), + )) + elseif value isa Union{Nothing,Missing,Bool,Number,Symbol,AbstractString,Char} + return value + elseif isstructtype(typeof(value)) + names = fieldnames(typeof(value)) + fields = map( + name -> _manifest_tree(getfield(value, name), state), + names, + ) + return (type=string(typeof(value)), fields=NamedTuple{names}(fields)) + end + return (type=string(typeof(value)), representation=repr(value)) +end + +_manifest_tree(value) = _manifest_tree(value, _ManifestState()) + +function _stable_bytes(value) + io = IOBuffer() + _write_stable(io, value) + return take!(io) +end + +function _write_stable(io::IO, value) + if value isa NamedTuple + print(io, "named{") + for key in keys(value) + print(io, repr(key), '=') + _write_stable(io, getproperty(value, key)) + print(io, ';') + end + print(io, '}') + elseif value isa Tuple + print(io, "tuple[") + for item in value + _write_stable(io, item) + print(io, ';') + end + print(io, ']') + elseif value isa AbstractArray + print(io, "array", repr(size(value)), '[') + for item in value + _write_stable(io, item) + print(io, ';') + end + print(io, ']') + elseif value isa AbstractFloat + print(io, string(typeof(value)), ':', bitstring(value)) + elseif value isa Complex + print(io, "complex(") + _write_stable(io, real(value)) + print(io, ',') + _write_stable(io, imag(value)) + print(io, ')') + elseif value isa Number + print(io, string(typeof(value)), ':', repr(value)) + else + print(io, string(typeof(value)), ':', repr(value)) + end + return io +end + +function _configuration_failure(index, configuration, exception) + return ConfigurationFailure( + index, + configuration_manifest(configuration), + string(typeof(exception)), + sprint(showerror, exception), + ) +end + +_skippable_configuration_error(exception) = exception isa Union{ + ArgumentError, + AssertionError, + DimensionMismatch, + DomainError, +} + +function _full_result(values, resolved, failures, problem, formulation, run, options) + typed_values = values === nothing ? Union{}[] : values + calculation_manifest = CalculationManifest( + tuple(resolved...), + _manifest_tree(problem), + formulation, + run, + options, + ) + return FullParametricResult( + typed_values, + resolved, + failures, + calculation_manifest, + ) +end + +_append_result(::Nothing, value) = typeof(value)[value] +function _append_result(values::Vector{T}, value) where {T} + value isa T && (push!(values, value); return values) + W = typejoin(T, typeof(value)) + W === Any && throw(ArgumentError( + "configuration results do not share one primitive result grammar", + )) + widened = Vector{W}(undef, length(values) + 1) + copyto!(widened, values) + widened[end] = value + return widened +end + +""" +$(TYPEDSIGNATURES) + +Evaluate a formulation over every configuration admitted by a problem +specification. + +Deterministic specifications use [`FullParametric`](@ref) when `run` is +omitted. Specifications containing uncertainty require an explicit +[`FullParametric`](@ref) or [`MonteCarlo`](@ref) policy. +""" +function compute!( + problem::AbstractSpec{<:Engine.ProblemDefinition}, + formulation::Engine.AbstractFormulation; + run=nothing, + options=Engine.ComputeOptions(), +) + selected_run = if run === nothing + has_uncertainty(problem) && throw(ArgumentError( + "uncertain Gridspaces require explicit run=FullParametric() or run=MonteCarlo(...) execution intent", + )) + FullParametric() + else + run + end + selected_run isa AbstractRunPolicy || throw(ArgumentError( + "run must be FullParametric() or MonteCarlo(...)", + )) + normalized_options = Engine.compute_options(options) + return _compute_gridspace(problem, formulation, selected_run, normalized_options) +end + +function _compute_gridspace(problem, formulation, run::FullParametric, options) + values = nothing + resolved = NamedTuple[] + failures = ConfigurationFailure[] + for (index, configuration) in enumerate(configurations(problem)) + parameterization = configuration_manifest(configuration) + try + materialized = materialize(configuration) + values = _append_result( + values, + Engine.compute!(materialized, formulation; options), + ) + push!(resolved, parameterization) + catch exception + exception isa InterruptException && rethrow() + (run.invalid === :error || !_skippable_configuration_error(exception)) && + rethrow() + push!(failures, _configuration_failure(index, configuration, exception)) + end + end + return _full_result(values, resolved, failures, problem, formulation, run, options) +end + +function _compute_gridspace(problem, formulation, run::MonteCarlo, options) + values = nothing + resolved = NamedTuple[] + failures = ConfigurationFailure[] + root_seed = run.seed === nothing ? rand(Random.RandomDevice(), UInt64) : run.seed + for (index, configuration) in enumerate(configurations(problem)) + parameterization = configuration_manifest(configuration) + configuration_seed = + root_seed ⊻ (UInt64(index - 1) * 0x9e3779b97f4a7c15) + try + values = _append_result( + values, + _monte_carlo( + problem, + configuration, + parameterization, + formulation, + run, + options, + root_seed, + configuration_seed, + ), + ) + push!(resolved, parameterization) + catch exception + exception isa InterruptException && rethrow() + (run.invalid === :error || !_skippable_configuration_error(exception)) && + rethrow() + push!(failures, _configuration_failure(index, configuration, exception)) + end + end + manifest_run = ( + policy=_manifest_tree(run), + actual_root_seed=root_seed, + ) + values === nothing && return _full_result( + values, resolved, failures, problem, formulation, manifest_run, options, + ) + if length(values) == 1 && isempty(failures) + return only(values) + end + return _full_result( + values, + resolved, + failures, + problem, + formulation, + manifest_run, + options, + ) +end + +function _dkw_trials(observables::Integer, confidence::Real, cdf_tol::Real) + observables > 0 || throw(ArgumentError("observable count must be positive")) + alpha = 1 - confidence + return ceil(Int, log(2 * observables / alpha) / (2 * cdf_tol^2)) +end + +_observable_count(::DataModel.CableConstants) = 3 +function _observable_count(value::Engine.LineParameters) + n = size(value.Z, 1) + return 2 * n * (n + 1) * length(value.f) +end + +function _monte_carlo( + problem, + configuration, + parameterization, + formulation, + run, + options, + root_seed, + seed, +) + rng = Random.Xoshiro(seed) + first_problem = rand(rng, configuration; distribution=run.distribution) + first_result = Engine.compute!(first_problem, formulation; options) + ntrials = something( + run.trials, + _dkw_trials(_observable_count(first_result), run.confidence, run.cdf_tol), + ) + draws = Vector{typeof(first_result)}(undef, ntrials) + draws[1] = first_result + for trial in 2:ntrials + realization = rand(rng, configuration; distribution=run.distribution) + draws[trial] = Engine.compute!(realization, formulation; options) + end + aggregated = _aggregate(draws, run) + calculation_manifest = CalculationManifest( + parameterization, + _manifest_tree(problem), + formulation, + merge( + _manifest_tree(run), + ( + actual_trials=ntrials, + actual_root_seed=root_seed, + actual_seed=seed, + ), + ), + options, + ) + return MonteCarloResult( + aggregated.representation, + aggregated.statistics, + aggregated.samples, + aggregated.histograms, + UncertainValue( + aggregated.representation, + aggregated.uncertainty, + AbsoluteUncertainty(), + ), + ntrials, + run.confidence, + run.cdf_tol, + run.distribution, + seed, + calculation_manifest, + ) +end + +function _histogram(values::AbstractVector{<:Real}, bins::Union{Nothing,Int}) + lo, hi = extrema(values) + if lo == hi + width = max(abs(float(lo)) * sqrt(eps(Float64)), sqrt(eps(Float64))) + edges = [float(lo) - width, float(hi) + width] + else + count = something(bins, max(1, ceil(Int, sqrt(length(values))))) + edges = collect(range(float(lo), float(hi); length=count + 1)) + 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 + density = counts ./ (length(values) .* diff(edges)) + return HistogramPDF(edges, density) +end + +function _aggregate(draws::Vector{<:DataModel.CableConstants}, run) + Rs = [value.R for value in draws] + Ls = [value.L for value in draws] + Cs = [value.C for value in draws] + summaries = DataModel.CableConstants( + SampleSummary(Rs), SampleSummary(Ls), SampleSummary(Cs), + ) + representation = DataModel.CableConstants( + summaries.R.mean, summaries.L.mean, summaries.C.mean, + ) + uncertainty = DataModel.CableConstants( + summaries.R.std, summaries.L.std, summaries.C.std, + ) + retained = run.return_samples ? DataModel.CableConstants(Rs, Ls, Cs) : nothing + hist = run.return_histograms ? DataModel.CableConstants( + _histogram(Rs, run.bins), + _histogram(Ls, run.bins), + _histogram(Cs, run.bins), + ) : nothing + return (representation, statistics=summaries, samples=retained, + histograms=hist, uncertainty) +end + +function _rlcg_samples(draws::Vector{<:Engine.LineParameters}) + first_result = first(draws) + dimensions = (size(first_result.Z)..., length(draws)) + Rs = Array{Float64}(undef, dimensions) + Ls = similar(Rs) + Cs = similar(Rs) + Gs = similar(Rs) + angular = reshape(2π .* first_result.f, 1, 1, :) + for (trial, value) in enumerate(draws) + size(value.Z) == size(first_result.Z) || throw(DimensionMismatch( + "Monte Carlo realizations produced incompatible impedance dimensions", + )) + value.f == first_result.f || throw(DimensionMismatch( + "Monte Carlo realizations produced incompatible frequency axes", + )) + @views Rs[:, :, :, trial] .= real.(value.Z.values) + @views Ls[:, :, :, trial] .= imag.(value.Z.values) ./ angular + @views Gs[:, :, :, trial] .= real.(value.Y.values) + @views Cs[:, :, :, trial] .= imag.(value.Y.values) ./ angular + end + return RLCG(Rs, Ls, Cs, Gs) +end + +function _map_observables(function_value, samples::Array{<:Real,4}) + output_size = size(samples)[1:3] + indices = CartesianIndices(output_size) + first_index = first(indices) + first_value = function_value(collect(@view samples[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 samples[index.I..., :])) + end + return output +end + +function _aggregate(draws::Vector{<:Engine.LineParameters}, run) + sample_values = _rlcg_samples(draws) + summaries = RLCG(( + _map_observables(SampleSummary, values) + for values in (sample_values.R, sample_values.L, sample_values.C, sample_values.G) + )...) + + means = RLCG((map(summary -> summary.mean, values) for values in + (summaries.R, summaries.L, summaries.C, summaries.G))...) + deviations = RLCG((map(summary -> summary.std, values) for values in + (summaries.R, summaries.L, summaries.C, summaries.G))...) + first_result = first(draws) + angular = reshape(2π .* first_result.f, 1, 1, :) + mean_Z = complex.(means.R, means.L .* angular) + mean_Y = complex.(means.G, means.C .* angular) + representation = Engine.LineParameters( + domain(first_result), mean_Z, mean_Y, first_result.f; + basis=basis(first_result), + ) + hist = run.return_histograms ? RLCG(( + _map_observables( + values -> _histogram(values, run.bins), + samples, + ) for samples in + (sample_values.R, sample_values.L, sample_values.C, sample_values.G) + )...) : nothing + retained = run.return_samples ? sample_values : nothing + return (representation, statistics=summaries, samples=retained, + histograms=hist, uncertainty=deviations) +end + +include("dataframe.jl") +include("plotspecs.jl") + +end diff --git a/src/computation/dataframe.jl b/src/computation/dataframe.jl new file mode 100644 index 00000000..a8979079 --- /dev/null +++ b/src/computation/dataframe.jl @@ -0,0 +1,150 @@ +import DataFrames: DataFrame, metadata! + +function _mc_unit(quantity::Symbol, result_basis, length_unit, quantity_units) + return UnitHandler.line_component_unit( + quantity, + result_basis; + length_unit, + quantity_units, + ) +end + +function _mc_summary_frame( + result::MonteCarloResult, + quantities::Tuple, + summaries::Tuple, + result_basis::Symbol; + length_unit::Symbol, + quantity_units, +) + resolved_units = map( + quantity -> _mc_unit( + quantity, + result_basis, + length_unit, + quantity_units, + ), + quantities, + ) + scales = getproperty.(resolved_units, :scale) + frame = DataFrame( + quantity=collect(String.(quantities)), + mean=collect(map((summary, scale) -> summary.mean * scale, summaries, scales)), + std=collect(map((summary, scale) -> summary.std * abs(scale), summaries, scales)), + min=collect(map((summary, scale) -> summary.min * scale, summaries, scales)), + q05=collect(map((summary, scale) -> summary.q05 * scale, summaries, scales)), + q50=collect(map((summary, scale) -> summary.q50 * scale, summaries, scales)), + q95=collect(map((summary, scale) -> summary.q95 * scale, summaries, scales)), + max=collect(map((summary, scale) -> summary.max * scale, summaries, scales)), + unit=collect(UnitHandler.get_label.(getproperty.(resolved_units, :units))), + trials=fill(result.trials, length(quantities)), + confidence=fill(result.confidence, length(quantities)), + cdf_tol=fill(result.cdf_tol, length(quantities)), + ) + metadata!( + frame, + "monte_carlo", + ( + trials=result.trials, + confidence=result.confidence, + cdf_tol=result.cdf_tol, + distribution=string(result.distribution), + seed=result.seed, + manifest_hash=result.manifest.hash, + ); + style=:note, + ) + return frame +end + +function _montecarlo_dataframe( + result::MonteCarloResult, + ::DataModel.CableConstants; + length_unit::Symbol, + quantity_units, +) + quantities = (:R, :L, :C) + summaries = map(quantity -> getproperty(result.statistics, quantity), quantities) + return _mc_summary_frame( + result, + quantities, + summaries, + :per_length; + length_unit, + quantity_units, + ) +end + +function _montecarlo_dataframe( + result::MonteCarloResult, + representation::Engine.LineParameters; + length_unit::Symbol, + quantity_units, +) + quantities = (:R, :L, :C, :G) + shape = size(result.statistics.R) + frames = Array{DataFrame,3}(undef, shape) + for index in CartesianIndices(frames) + summaries = map( + quantity -> getproperty(result.statistics, quantity)[index], + quantities, + ) + frames[index] = _mc_summary_frame( + result, + quantities, + summaries, + basis(representation); + length_unit, + quantity_units, + ) + end + return frames +end + +function _montecarlo_dataframe( + result::MonteCarloResult, + representation; + length_unit::Symbol, + quantity_units, +) + throw(ArgumentError( + "DataFrame presentation is not defined for MonteCarloResult{$(typeof(representation))}", + )) +end + +""" +$(TYPEDSIGNATURES) + +Render the marginal summaries of a [`MonteCarloResult`](@ref) without +performing a calculation. + +Cable-constant results produce one table with R, L, and C rows. Line-parameter +results produce an `n × n × n_frequency` array of tables with R, L, C, and G +rows. The DKW confidence and `cdf_tol` columns describe the simultaneous +marginal empirical-CDF bound used for automatic trial sizing; they are not +confidence intervals for the sample mean. + +# Keywords + +- `length_unit`: Metric prefix for the denominator of per-length quantities. + Default: `:kilo`. +- `quantity_units`: Optional numerator-prefix override accepted by + `UnitHandler.line_component_unit`. + +# Returns + +- A `DataFrame` for cable constants, or an array of `DataFrame`s for line + parameters. +""" +function DataFrame( + result::MonteCarloResult; + length_unit::Symbol=:kilo, + quantity_units=nothing, +) + return _montecarlo_dataframe( + result, + result.representation; + length_unit, + quantity_units, + ) +end diff --git a/src/computation/plotspecs.jl b/src/computation/plotspecs.jl new file mode 100644 index 00000000..b2ea32d8 --- /dev/null +++ b/src/computation/plotspecs.jl @@ -0,0 +1,656 @@ +""" +$(TYPEDEF) + +PlotBuilder recipe for marginal Monte Carlo histograms, probability densities, +cumulative distributions, and Q-Q plots. +""" +struct MCDistributionPlotSpec <: PlotBuilder.AbstractPlotSpec end + +struct MCSeriesKey{K} end + +function _mc_plot_exponent(series, field::Symbol) + maximum_value = 0.0 + for item in series + values = field === :x ? item.xdata : item.ydata + values === nothing && continue + for sample in values + sample isa Real || continue + nominal_value = abs(Float64(sample)) + isfinite(nominal_value) && + (maximum_value = max(maximum_value, nominal_value)) + end + end + iszero(maximum_value) && return 0 + exponent = floor(Int, log10(maximum_value)) + return abs(exponent) < 3 ? 0 : exponent +end + +function _mc_selection( + result::MonteCarloResult, + ::DataModel.CableConstants, + quantity::Symbol, + ijk, +) + quantity in (:R, :L, :C) || throw( + ArgumentError("cable-constant quantities are :R, :L, and :C"), + ) + ijk === nothing || throw( + ArgumentError("cable-constant Monte Carlo results do not use matrix indices"), + ) + sample_values = result.samples === nothing ? nothing : + getproperty(result.samples, quantity) + histogram_value = result.histograms === nothing ? nothing : + getproperty(result.histograms, quantity) + return sample_values, histogram_value, nothing +end + +function _mc_selection( + result::MonteCarloResult, + ::Engine.LineParameters, + quantity::Symbol, + ijk, +) + quantity in (:R, :L, :C, :G) || throw( + ArgumentError("line-parameter quantities are :R, :L, :C, and :G"), + ) + selection = ijk === nothing ? (1, 1, 1) : ijk + selection isa NTuple{3,Int} || throw( + ArgumentError("ijk must be a tuple (i, j, k)"), + ) + observable = getproperty(result.statistics, quantity) + checkbounds(observable, selection...) + sample_values = result.samples === nothing ? nothing : collect( + view(getproperty(result.samples, quantity), selection..., :), + ) + histogram_value = result.histograms === nothing ? nothing : + getproperty(result.histograms, quantity)[selection...] + return sample_values, histogram_value, selection +end + +function _mc_selection(result::MonteCarloResult, quantity::Symbol, ijk) + return _mc_selection(result, result.representation, quantity, ijk) +end + +function _mc_target_unit(result::MonteCarloResult, quantity, length_unit, quantity_units) + result_basis = result.representation isa DataModel.CableConstants ? + :per_length : basis(result.representation) + resolved = _mc_unit( + quantity, + result_basis, + length_unit, + quantity_units, + ) + return resolved.quantity, resolved.units, resolved.scale +end + +function _scaled_histogram(histogram::HistogramPDF, conversion) + conversion > zero(conversion) || throw( + ArgumentError("unit conversion must be positive"), + ) + return HistogramPDF( + histogram.edges .* conversion, + histogram.density ./ conversion, + ) +end + +function _mc_histogram_cdf(histogram::HistogramPDF, x::Real) + x <= first(histogram.edges) && return 0.0 + x >= last(histogram.edges) && return 1.0 + bin = clamp(searchsortedlast(histogram.edges, x), 1, length(histogram.density)) + cumulative = bin == 1 ? 0.0 : sum( + histogram.density[1:(bin - 1)] .* diff(histogram.edges[1:bin]), + ) + return clamp( + cumulative + histogram.density[bin] * (x - histogram.edges[bin]), + 0.0, + 1.0, + ) +end + +function _mc_histogram_quantile(histogram::HistogramPDF, 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) + masses = histogram.density .* diff(histogram.edges) + cumulative = cumsum(masses) + bin = something(findfirst(>=(probability), cumulative), length(masses)) + previous = bin == 1 ? 0.0 : cumulative[bin - 1] + density = histogram.density[bin] + iszero(density) && return histogram.edges[bin] + return histogram.edges[bin] + (probability - previous) / density +end + +_mc_empirical_cdf(sorted_values, x) = searchsortedlast(sorted_values, x) / + length(sorted_values) + +function _mc_input_defaults() + return (; + quantity=:R, + ijk=nothing, + mode=:hist, + data=:samples, + length_unit=:kilo, + quantity_units=nothing, + nbins=nothing, + normalization=:none, + ) +end + +PlotBuilder.dispatch_on(::Type{MCDistributionPlotSpec}) = MonteCarloResult +function PlotBuilder.input_kwargs(::Type{MCDistributionPlotSpec}) + return ( + :quantity, + :ijk, + :mode, + :data, + :length_unit, + :quantity_units, + :nbins, + :normalization, + ) +end +PlotBuilder.renderer_kwargs(::Type{MCDistributionPlotSpec}) = (:fig_size,) +PlotBuilder.input_defaults(::Type{MCDistributionPlotSpec}, ::MonteCarloResult) = + _mc_input_defaults() +function PlotBuilder.renderer_defaults( + ::Type{MCDistributionPlotSpec}, + ::MonteCarloResult, +) + return (; fig_size=(800, 400)) +end + +function _mc_required_data(mode::Symbol, data::Symbol) + needs_samples = mode === :qq || + (mode in (:hist, :ecdf) && data in (:samples, :both)) + needs_histogram = mode in (:pdf, :qq) || + (mode in (:hist, :ecdf) && data in (:pdf, :both)) + return needs_samples, needs_histogram +end + +function PlotBuilder.resolve_input( + ::Type{MCDistributionPlotSpec}, + recipe::PlotBuilder.PlotRecipe, +) + input = recipe.input + input.quantity isa Symbol || throw(ArgumentError("quantity must be a Symbol")) + input.mode in (:hist, :pdf, :ecdf, :qq) || throw( + ArgumentError("mode must be :hist, :pdf, :ecdf, or :qq"), + ) + input.data in (:samples, :pdf, :both) || throw( + ArgumentError("data must be :samples, :pdf, or :both"), + ) + input.nbins === nothing || input.nbins isa Int || throw( + ArgumentError("nbins must be an integer or nothing"), + ) + input.nbins === nothing || input.nbins > 0 || throw( + ArgumentError("nbins must be positive"), + ) + input.normalization in (:none, :pdf, :density, :probability) || throw( + ArgumentError("normalization must be :none, :pdf, :density, or :probability"), + ) + recipe.renderer.fig_size isa Tuple{Int,Int} || throw( + ArgumentError("fig_size must be a tuple of two integers"), + ) + + sample_values, histogram_value, selection = _mc_selection( + recipe.object, + input.quantity, + input.ijk, + ) + tag, target, conversion = _mc_target_unit( + recipe.object, + input.quantity, + input.length_unit, + input.quantity_units, + ) + values = sample_values === nothing ? nothing : + collect(sample_values) .* conversion + histogram = histogram_value === nothing ? nothing : + _scaled_histogram(histogram_value, conversion) + + needs_samples, needs_histogram = _mc_required_data(input.mode, input.data) + needs_samples && values === nothing && throw( + ArgumentError("this plot requires retained Monte Carlo samples"), + ) + if needs_histogram && histogram === nothing + values === nothing && throw( + ArgumentError("this plot requires retained samples or histograms"), + ) + histogram = _histogram(values, input.nbins) + end + + sample_histogram = values === nothing ? nothing : _histogram(values, input.nbins) + bins = if input.nbins !== nothing && sample_histogram !== nothing + sample_histogram.edges + elseif histogram !== nothing + histogram.edges + elseif sample_histogram !== nothing + sample_histogram.edges + else + Float64[] + end + effective_normalization = input.data in (:pdf, :both) ? :pdf : + input.normalization + resolved = (; + values, + histogram, + bins, + effective_normalization, + tag, + target, + selection, + ) + return PlotBuilder.PlotRecipe( + recipe.object, + merge(input, resolved), + recipe.renderer, + ) +end + +function PlotBuilder.recipe_mode( + ::Type{MCDistributionPlotSpec}, + recipe::PlotBuilder.PlotRecipe, +) + return Val(recipe.input.mode) +end + +function PlotBuilder.grouping_mode( + ::Type{MCDistributionPlotSpec}, + ::Val, + ::PlotBuilder.PlotRecipe, +) + return Val(:overlay) +end + +_mc_data_facets(::Val{:samples}, sample, density) = (sample,) +_mc_data_facets(::Val{:pdf}, sample, density) = (density,) +_mc_data_facets(::Val{:both}, sample, density) = (sample, density) + +function PlotBuilder.group_facets( + ::Type{MCDistributionPlotSpec}, + ::Val{:hist}, + recipe::PlotBuilder.PlotRecipe, + page_key, +) + return _mc_data_facets( + Val(recipe.input.data), + MCSeriesKey{:samples}(), + MCSeriesKey{:histogram_pdf}(), + ) +end + +function PlotBuilder.group_facets( + ::Type{MCDistributionPlotSpec}, + ::Val{:pdf}, + recipe::PlotBuilder.PlotRecipe, + page_key, +) + return (MCSeriesKey{:histogram_pdf}(),) +end + +function PlotBuilder.group_facets( + ::Type{MCDistributionPlotSpec}, + ::Val{:ecdf}, + recipe::PlotBuilder.PlotRecipe, + page_key, +) + return _mc_data_facets( + Val(recipe.input.data), + MCSeriesKey{:empirical_cdf}(), + MCSeriesKey{:histogram_cdf}(), + ) +end + +function PlotBuilder.group_facets( + ::Type{MCDistributionPlotSpec}, + ::Val{:qq}, + recipe::PlotBuilder.PlotRecipe, + page_key, +) + return (MCSeriesKey{:quantiles}(), MCSeriesKey{:reference}()) +end + +function _mc_values(recipe::PlotBuilder.PlotRecipe) + recipe.input.values === nothing && throw( + ArgumentError("Monte Carlo samples were not retained"), + ) + return recipe.input.values +end + +function _mc_histogram_model(recipe::PlotBuilder.PlotRecipe) + recipe.input.histogram === nothing && throw( + ArgumentError("Monte Carlo histograms were not retained or derived"), + ) + return recipe.input.histogram +end + +function _mc_cdf_grid(recipe::PlotBuilder.PlotRecipe) + limits = if recipe.input.histogram !== nothing + extrema(recipe.input.histogram.edges) + else + extrema(_mc_values(recipe)) + end + lower, upper = limits + padding = iszero(upper - lower) ? max(abs(lower), 1.0) * 0.05 : + 0.05 * (upper - lower) + return collect(range(lower - padding, upper + padding; length=500)) +end + +function _mc_qq_values(recipe::PlotBuilder.PlotRecipe) + values = sort(_mc_values(recipe)) + histogram = _mc_histogram_model(recipe) + probabilities = ((1:length(values)) .- 0.5) ./ length(values) + histogram_values = _mc_histogram_quantile.(Ref(histogram), probabilities) + return values, histogram_values +end + +PlotBuilder.plot_kind( + ::Type{MCDistributionPlotSpec}, ::Val, ::PlotBuilder.PlotRecipe, + page_key, view_key, ::MCSeriesKey{:samples}, +) = :histogram +PlotBuilder.plot_kind( + ::Type{MCDistributionPlotSpec}, ::Val, ::PlotBuilder.PlotRecipe, + page_key, view_key, ::MCSeriesKey{:histogram_pdf}, +) = :stairs +PlotBuilder.plot_kind( + ::Type{MCDistributionPlotSpec}, ::Val, ::PlotBuilder.PlotRecipe, + page_key, view_key, ::MCSeriesKey{:histogram_cdf}, +) = :line +PlotBuilder.plot_kind( + ::Type{MCDistributionPlotSpec}, ::Val, ::PlotBuilder.PlotRecipe, + page_key, view_key, ::MCSeriesKey{:empirical_cdf}, +) = :line +PlotBuilder.plot_kind( + ::Type{MCDistributionPlotSpec}, ::Val, ::PlotBuilder.PlotRecipe, + page_key, view_key, ::MCSeriesKey{:quantiles}, +) = :scatter +PlotBuilder.plot_kind( + ::Type{MCDistributionPlotSpec}, ::Val, ::PlotBuilder.PlotRecipe, + page_key, view_key, ::MCSeriesKey{:reference}, +) = :line + +function PlotBuilder.series_data( + ::Type{MCDistributionPlotSpec}, ::Val, ::Val{:x}, + recipe::PlotBuilder.PlotRecipe, page_key, view_key, + ::MCSeriesKey{:samples}, +) + return _mc_values(recipe) +end + +function PlotBuilder.series_data( + ::Type{MCDistributionPlotSpec}, ::Val, ::Val{:x}, + recipe::PlotBuilder.PlotRecipe, page_key, view_key, + ::MCSeriesKey{:histogram_pdf}, +) + return _mc_histogram_model(recipe).edges +end + +function PlotBuilder.series_data( + ::Type{MCDistributionPlotSpec}, ::Val, ::Val{:y}, + recipe::PlotBuilder.PlotRecipe, page_key, view_key, + ::MCSeriesKey{:histogram_pdf}, +) + density = _mc_histogram_model(recipe).density + return [density; last(density)] +end + +function PlotBuilder.series_data( + ::Type{MCDistributionPlotSpec}, ::Val, ::Val{:x}, + recipe::PlotBuilder.PlotRecipe, page_key, view_key, + ::Union{MCSeriesKey{:histogram_cdf},MCSeriesKey{:empirical_cdf}}, +) + return _mc_cdf_grid(recipe) +end + +function PlotBuilder.series_data( + ::Type{MCDistributionPlotSpec}, ::Val, ::Val{:y}, + recipe::PlotBuilder.PlotRecipe, page_key, view_key, + ::MCSeriesKey{:histogram_cdf}, +) + grid = _mc_cdf_grid(recipe) + histogram = _mc_histogram_model(recipe) + return _mc_histogram_cdf.(Ref(histogram), grid) +end + +function PlotBuilder.series_data( + ::Type{MCDistributionPlotSpec}, ::Val, ::Val{:y}, + recipe::PlotBuilder.PlotRecipe, page_key, view_key, + ::MCSeriesKey{:empirical_cdf}, +) + grid = _mc_cdf_grid(recipe) + values = sort(_mc_values(recipe)) + return _mc_empirical_cdf.(Ref(values), grid) +end + +function PlotBuilder.series_data( + ::Type{MCDistributionPlotSpec}, ::Val, ::Val{:x}, + recipe::PlotBuilder.PlotRecipe, page_key, view_key, + ::MCSeriesKey{:quantiles}, +) + values, _ = _mc_qq_values(recipe) + return values +end + +function PlotBuilder.series_data( + ::Type{MCDistributionPlotSpec}, ::Val, ::Val{:y}, + recipe::PlotBuilder.PlotRecipe, page_key, view_key, + ::MCSeriesKey{:quantiles}, +) + _, values = _mc_qq_values(recipe) + return values +end + +function PlotBuilder.series_data( + ::Type{MCDistributionPlotSpec}, ::Val, + ::Union{Val{:x},Val{:y}}, recipe::PlotBuilder.PlotRecipe, + page_key, view_key, ::MCSeriesKey{:reference}, +) + sample_values, histogram_values = _mc_qq_values(recipe) + return collect(extrema(vcat(sample_values, histogram_values))) +end + +PlotBuilder.legend_label( + ::Type{MCDistributionPlotSpec}, ::Val, ::PlotBuilder.PlotRecipe, + page_key, view_key, ::MCSeriesKey{:samples}, +) = "samples" +PlotBuilder.legend_label( + ::Type{MCDistributionPlotSpec}, ::Val, ::PlotBuilder.PlotRecipe, + page_key, view_key, ::MCSeriesKey{:histogram_pdf}, +) = "model PDF" +PlotBuilder.legend_label( + ::Type{MCDistributionPlotSpec}, ::Val, ::PlotBuilder.PlotRecipe, + page_key, view_key, ::MCSeriesKey{:histogram_cdf}, +) = "model CDF" +PlotBuilder.legend_label( + ::Type{MCDistributionPlotSpec}, ::Val, ::PlotBuilder.PlotRecipe, + page_key, view_key, ::MCSeriesKey{:empirical_cdf}, +) = "empirical CDF" +PlotBuilder.legend_label( + ::Type{MCDistributionPlotSpec}, ::Val, ::PlotBuilder.PlotRecipe, + page_key, view_key, ::MCSeriesKey{:quantiles}, +) = "quantiles" +PlotBuilder.legend_label( + ::Type{MCDistributionPlotSpec}, ::Val, ::PlotBuilder.PlotRecipe, + page_key, view_key, ::MCSeriesKey{:reference}, +) = "perfect fit" + +function PlotBuilder.series_attributes( + ::Type{MCDistributionPlotSpec}, ::Val, recipe::PlotBuilder.PlotRecipe, + page_key, view_key, ::MCSeriesKey{:samples}, +) + return (; + bins=recipe.input.bins, + normalization=recipe.input.effective_normalization, + ) +end +PlotBuilder.series_attributes( + ::Type{MCDistributionPlotSpec}, ::Val, ::PlotBuilder.PlotRecipe, + page_key, view_key, ::MCSeriesKey{:histogram_pdf}, +) = (; step=:post, color=:red, linewidth=2) +PlotBuilder.series_attributes( + ::Type{MCDistributionPlotSpec}, ::Val, ::PlotBuilder.PlotRecipe, + page_key, view_key, ::MCSeriesKey{:histogram_cdf}, +) = (; color=:red, linewidth=2) +PlotBuilder.series_attributes( + ::Type{MCDistributionPlotSpec}, ::Val, ::PlotBuilder.PlotRecipe, + page_key, view_key, ::MCSeriesKey{:empirical_cdf}, +) = (; color=:blue, linestyle=:dash, linewidth=2) +PlotBuilder.series_attributes( + ::Type{MCDistributionPlotSpec}, ::Val, ::PlotBuilder.PlotRecipe, + page_key, view_key, ::MCSeriesKey{:quantiles}, +) = (; color=:steelblue, markersize=6) +PlotBuilder.series_attributes( + ::Type{MCDistributionPlotSpec}, ::Val, ::PlotBuilder.PlotRecipe, + page_key, view_key, ::MCSeriesKey{:reference}, +) = (; color=:black, linestyle=:dash, linewidth=2) + +function _mc_title(recipe::PlotBuilder.PlotRecipe, suffix::AbstractString) + symbol = UnitHandler.get_symbol(recipe.input.tag) + selection = recipe.input.selection + indices = selection === nothing ? "" : "[$(join(selection, ','))]" + return "$symbol$indices $suffix" +end + +PlotBuilder.default_title( + ::Type{MCDistributionPlotSpec}, ::Val{:hist}, recipe::PlotBuilder.PlotRecipe, + page_key, view_key, +) = _mc_title(recipe, "histogram") +PlotBuilder.default_title( + ::Type{MCDistributionPlotSpec}, ::Val{:pdf}, recipe::PlotBuilder.PlotRecipe, + page_key, view_key, +) = _mc_title(recipe, "probability density") +PlotBuilder.default_title( + ::Type{MCDistributionPlotSpec}, ::Val{:ecdf}, recipe::PlotBuilder.PlotRecipe, + page_key, view_key, +) = _mc_title(recipe, "cumulative distribution") +PlotBuilder.default_title( + ::Type{MCDistributionPlotSpec}, ::Val{:qq}, recipe::PlotBuilder.PlotRecipe, + page_key, view_key, +) = _mc_title(recipe, "Q-Q plot") + +function PlotBuilder.axis_quantity( + ::Type{MCDistributionPlotSpec}, ::Val, ::Val{:x}, + recipe::PlotBuilder.PlotRecipe, page_key, view_key, +) + return recipe.input.tag +end +function PlotBuilder.axis_quantity( + ::Type{MCDistributionPlotSpec}, ::Val{:qq}, ::Val{:y}, + recipe::PlotBuilder.PlotRecipe, page_key, view_key, +) + return recipe.input.tag +end +function PlotBuilder.axis_quantity( + ::Type{MCDistributionPlotSpec}, ::Val, ::Val{:y}, + recipe::PlotBuilder.PlotRecipe, page_key, view_key, +) + return UnitHandler.QuantityTag{:dimensionless}() +end + +function PlotBuilder.axis_unit( + ::Type{MCDistributionPlotSpec}, ::Val, ::Val{:x}, + quantity::UnitHandler.QuantityTag, recipe::PlotBuilder.PlotRecipe, + page_key, view_key, +) + return recipe.input.target +end +function PlotBuilder.axis_unit( + ::Type{MCDistributionPlotSpec}, ::Val{:qq}, ::Val{:y}, + quantity::UnitHandler.QuantityTag, recipe::PlotBuilder.PlotRecipe, + page_key, view_key, +) + return recipe.input.target +end +function PlotBuilder.axis_unit( + ::Type{MCDistributionPlotSpec}, ::Val, ::Val{:y}, + quantity::UnitHandler.QuantityTag, recipe::PlotBuilder.PlotRecipe, + page_key, view_key, +) + return UnitHandler.Units() +end + +function _mc_quantity_label(quantity, unit) + unit_label = UnitHandler.get_label(unit) + return isempty(unit_label) ? UnitHandler.get_label(quantity) : + "$(UnitHandler.get_label(quantity)) [$unit_label]" +end + +function PlotBuilder.axis_label( + ::Type{MCDistributionPlotSpec}, ::Val{:qq}, ::Val{:x}, + quantity::UnitHandler.QuantityTag, unit::UnitHandler.Units, + recipe::PlotBuilder.PlotRecipe, page_key, view_key, +) + return "sample quantiles [$(UnitHandler.get_label(unit))]" +end +function PlotBuilder.axis_label( + ::Type{MCDistributionPlotSpec}, ::Val, ::Val{:x}, + quantity::UnitHandler.QuantityTag, unit::UnitHandler.Units, + recipe::PlotBuilder.PlotRecipe, page_key, view_key, +) + return _mc_quantity_label(quantity, unit) +end +function PlotBuilder.axis_label( + ::Type{MCDistributionPlotSpec}, ::Val{:hist}, ::Val{:y}, + quantity::UnitHandler.QuantityTag, unit::UnitHandler.Units, + recipe::PlotBuilder.PlotRecipe, page_key, view_key, +) + normalization = recipe.input.effective_normalization + return normalization === :none ? "count" : String(normalization) +end +PlotBuilder.axis_label( + ::Type{MCDistributionPlotSpec}, ::Val{:pdf}, ::Val{:y}, + quantity::UnitHandler.QuantityTag, unit::UnitHandler.Units, + recipe::PlotBuilder.PlotRecipe, page_key, view_key, +) = "probability density" +PlotBuilder.axis_label( + ::Type{MCDistributionPlotSpec}, ::Val{:ecdf}, ::Val{:y}, + quantity::UnitHandler.QuantityTag, unit::UnitHandler.Units, + recipe::PlotBuilder.PlotRecipe, page_key, view_key, +) = "cumulative probability" +function PlotBuilder.axis_label( + ::Type{MCDistributionPlotSpec}, ::Val{:qq}, ::Val{:y}, + quantity::UnitHandler.QuantityTag, unit::UnitHandler.Units, + recipe::PlotBuilder.PlotRecipe, page_key, view_key, +) + return "model quantiles [$(UnitHandler.get_label(unit))]" +end + +function PlotBuilder.view_key( + ::Type{MCDistributionPlotSpec}, ::Val, + recipe::PlotBuilder.PlotRecipe, page_key, view_key, +) + return (; + quantity=recipe.input.quantity, + selection=recipe.input.selection, + mode=recipe.input.mode, + ) +end + +function PlotBuilder.default_figsize( + ::Type{MCDistributionPlotSpec}, ::Val, + recipe::PlotBuilder.PlotRecipe, page_key, +) + return recipe.renderer.fig_size +end + +function PlotBuilder.axis_exponent( + ::Type{MCDistributionPlotSpec}, ::Val, ::Val{dimension}, + recipe::PlotBuilder.PlotRecipe, page_key, view_key, + series::Vector{PlotBuilder.SeriesSpec}, +) where {dimension} + return _mc_plot_exponent(series, dimension) +end + +function PlotBuilder.page_identity( + ::Type{MCDistributionPlotSpec}, ::Val, + recipe::PlotBuilder.PlotRecipe, page_key, +) + return (; + quantity=recipe.input.quantity, + selection=recipe.input.selection, + mode=recipe.input.mode, + data=recipe.input.data, + ) +end diff --git a/src/datamodel/DataModel.jl b/src/datamodel/DataModel.jl index 804c2708..1d291161 100644 --- a/src/datamodel/DataModel.jl +++ b/src/datamodel/DataModel.jl @@ -19,7 +19,6 @@ $(IMPORTS) module DataModel # Export public API -export Thickness, Diameter # Type definitions export CircStrands, RectStrands, Strip, Tubular # Conductor types export Semicon, Insulator # Insulator types export SectorParams, Sector, SectorInsulator # Removed API tombstones @@ -46,9 +45,7 @@ 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 LinearAlgebra @@ -97,7 +94,7 @@ include("typecoercion.jl") include("plotspecs.jl") """ - preview(object; kwargs...) +$(TYPEDSIGNATURES) Preview a cable design or cable system with a loaded Makie backend. @@ -114,7 +111,7 @@ function preview(args...; kwargs...) end """ - show_material_scale(; kwargs...) +$(TYPEDSIGNATURES) Display the resistivity, permeability, and permittivity color scales used by [`preview`](@ref). This internal helper supports preview development and visual diff --git a/src/datamodel/baseparams/BaseParams.jl b/src/datamodel/baseparams/BaseParams.jl index b33d48bd..580212ef 100644 --- a/src/datamodel/baseparams/BaseParams.jl +++ b/src/datamodel/baseparams/BaseParams.jl @@ -43,7 +43,6 @@ 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 @@ -53,8 +52,6 @@ $(TYPEDSIGNATURES) Calculates the equivalent temperature coefficient of resistance (`alpha`) when two conductors are connected in parallel, by cross-weighted-resistance averaging: -# Notes - ```math \\alpha_{eq} = \\frac{\\alpha_1 R_2 + \\alpha_2 R1}{R_1 + R_2} ``` @@ -102,8 +99,6 @@ $(TYPEDSIGNATURES) Calculates the parallel equivalent of two impedances (or series equivalent of two admittances): -# Notes - ```math Z_{eq} = \\frac{Z_1 Z_2}{Z_1 + Z_2} ``` @@ -247,8 +242,6 @@ $(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: -# Notes - ```math R = \\rho \\frac{\\ell}{W T} ``` @@ -311,8 +304,6 @@ $(TYPEDSIGNATURES) Calculates the temperature correction factor for material properties based on the standard linear temperature model [cigre345](@cite): -# Notes - ```math k(T) = 1 + \\alpha (T - T_0) ``` @@ -361,8 +352,6 @@ $(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}``: -# Notes - ```math R = \\rho \\frac{\\ell}{\\pi (r_{ext}^2 - r_{in}^2)} ``` @@ -425,8 +414,6 @@ $(TYPEDSIGNATURES) Calculates the inductance of a tubular conductor per unit length, disregarding skin-effects (DC approximation) [916943](@cite) [cigre345](@cite) [1458878](@cite): -# Notes - ```math L = \\frac{\\mu_r \\mu_0}{2 \\pi} \\log \\left( \\frac{r_{ext}}{r_{in}} \\right) ``` @@ -474,7 +461,16 @@ end """ $(TYPEDSIGNATURES) -Calculates the center coordinates of wires arranged in a circular pattern. +Calculates the center coordinates of wires arranged in a circular pattern. For +wire index ``i = 0, \\ldots, N-1`` and layout radius +``r_l = r_{in} + r_w``, the implementation uses + +```math +x_i = C_x + r_l \\cos\\left(\\frac{2\\pi i}{N}\\right), \\qquad +y_i = C_y + r_l \\sin\\left(\\frac{2\\pi i}{N}\\right). +``` + +For a single wire, ``r_l`` is set to zero. # Arguments @@ -487,18 +483,6 @@ Calculates the center coordinates of wires arranged in a circular pattern. - Vector of tuples, where each tuple contains the `(x, y)` coordinates \\[m\\] of the center of a wire. -# Notes - -For wire index ``i = 0, \\ldots, N-1`` and layout radius -``r_l = r_{in} + r_w``, the implementation uses - -```math -x_i = C_x + r_l \\cos\\left(\\frac{2\\pi i}{N}\\right), \\qquad -y_i = C_y + r_l \\sin\\left(\\frac{2\\pi i}{N}\\right). -``` - -For a single wire, ``r_l`` is set to zero. - # Examples ```jldoctest @@ -550,8 +534,6 @@ $(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: -# Notes - ```math Z_d = \\left[Z_a - Z_x\\right] - \\frac{\\left( Z_m - Z_x \\right)^2}{Z_s - Z_x} ``` @@ -692,8 +674,6 @@ $(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): -# Notes - ```math GMR = \\sqrt[n] {r n a^{n-1}} ``` @@ -749,8 +729,6 @@ $(TYPEDSIGNATURES) Calculates the geometric mean radius (GMR) of a tubular conductor, using [6521501](@cite): -# Notes - ```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] ``` @@ -901,8 +879,6 @@ $(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): -# Notes - ```math C = \\frac{2 \\pi \\varepsilon_0 \\varepsilon_r}{\\log \\left(\\frac{r_{ext}}{r_{in}}\\right)} ``` @@ -951,8 +927,6 @@ $(TYPEDSIGNATURES) Calculates the shunt conductance per unit length of a coaxial structure, using the improved model reported in [916943](@cite) [Karmokar2025](@cite) [4389974](@cite): -# Notes - ```math G = \\frac{2\\pi\\sigma}{\\log(\\frac{r_{ext}}{r_{in}})} ``` @@ -997,8 +971,6 @@ $(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): -# Notes - ```math GMR_{eq} = {GMR_{i-1}}^{\\beta^2} \\cdot {GMR_{i}}^{(1-\\beta)^2} \\cdot {GMD}^{2\\beta(1-\\beta)} ``` @@ -1137,8 +1109,6 @@ $(TYPEDSIGNATURES) Calculates the solenoid correction factor for magnetic permeability in insulated cables with helical conductors (`CircStrands`), using the formula from Gudmundsdottir et al. [5743045](@cite): -# Notes - ```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})} ``` @@ -1203,8 +1173,6 @@ $(TYPEDSIGNATURES) Calculates the equivalent resistivity of a solid tubular conductor, using the formula [916943](@cite): -# Notes - ```math \\rho_{eq} = R_{eq} S_{eff} = R_{eq} \\pi (r_{ext}^2 - r_{in}^2) ``` @@ -1252,8 +1220,6 @@ $(TYPEDSIGNATURES) Calculates the equivalent permittivity for a coaxial cable insulation, using the formula [916943](@cite): -# Notes - ```math \\varepsilon_{eq} = \\frac{C_{eq} \\log(\\frac{r_{ext}}{r_{in}})}{2\\pi \\varepsilon_0} ``` @@ -1297,8 +1263,6 @@ $(TYPEDSIGNATURES) Calculates the equivalent loss factor (tangent) of a dielectric material: -# Notes - ```math \\tan \\delta = \\frac{G_{eq}}{\\omega \\cdot C_{eq}} ``` @@ -1341,8 +1305,6 @@ $(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): -# Notes - ```math \\sigma_{eq} = \\frac{G_{eq}}{2\\pi} \\log(\\frac{r_{ext}}{r_{in}}) ``` diff --git a/src/datamodel/cabledesign/cableconstants.jl b/src/datamodel/cabledesign/cableconstants.jl index 5df87047..4649a391 100644 --- a/src/datamodel/cabledesign/cableconstants.jl +++ b/src/datamodel/cabledesign/cableconstants.jl @@ -1,10 +1,12 @@ """ - CableConstants{T} +$(TYPEDEF) -Canonical per-unit-length cable constants. +Store cable constants per unit length. The fields `R`, `L`, and `C` are stored in Ω/m, H/m, and F/m respectively. Display conversions belong to `UnitHandler` and presentation adapters. + +$(TYPEDFIELDS) """ struct CableConstants{T} "Series resistance per unit length \\[Ω/m\\]." @@ -25,10 +27,13 @@ function CableConstants(R::Real, L::Real, C::Real) end """ - CableConstants(design::CableDesign; S=nothing, rho_e=100.0) +$(TYPEDSIGNATURES) Compute the scalar cable constants represented by `design`. +The calculation uses tubular resistance, trefoil inductance, and coaxial +capacitance. Presentation is handled separately after the result exists. + # Arguments - `design`: Cable design whose core, shield, and outer geometry are used. @@ -39,15 +44,8 @@ Compute the scalar cable constants represented by `design`. A [`CableConstants`](@ref) value storing `R` in Ω/m, `L` in H/m, and `C` in F/m. - -# Notes - -The implemented expressions are the same tubular-resistance, trefoil- -inductance, and coaxial-capacitance expressions historically used by -`DataFrame(design, :baseparams)`. This constructor changes their storage units, -not their physical calculation. """ -function CableConstants( +function _compute_cable_constants( design::CableDesign{T}; S::Union{Nothing, Number} = nothing, rho_e::Number = 100.0 diff --git a/src/datamodel/cabledesign/dataframe.jl b/src/datamodel/cabledesign/dataframe.jl index 871a2c56..8209714c 100644 --- a/src/datamodel/cabledesign/dataframe.jl +++ b/src/datamodel/cabledesign/dataframe.jl @@ -9,12 +9,8 @@ Extracts and displays data from a [`CableDesign`](@ref). - `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. + - `:components`: Component-level equivalent properties (default). - `: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. @@ -22,75 +18,23 @@ Extracts and displays data from a [`CableDesign`](@ref). # Examples ```julia -# Get basic RLC parameters -data = DataFrame(design) # Default is :baseparams format - # Get component-level data -comp_data = DataFrame(design, :components) +comp_data = DataFrame(design) # 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) +# Compute before rendering numerical cable constants +constants = compute!(CableConstantsProblem(design), Formulation()) +constants_data = DataFrame(constants) ``` """ -function DataFrame( - design::CableDesign, - format::Symbol = :baseparams; - S::Union{Nothing, Number} = nothing, - rho_e::Number = 100.0 -)::DataFrame +function DataFrame(design::CableDesign, format::Symbol=:components)::DataFrame if format == :baseparams - constants = CableConstants(design; S, rho_e) - R_display = constants.R * 1e3 - L_display = constants.L * 1e6 - C_display = constants.C * 1e9 - - # Prepare nominal values from CableDesign - nominals = [ - design.nominal_data.resistance, - design.nominal_data.inductance, - design.nominal_data.capacitance - ] - - # Calculate differences - diffs = map(zip([R_display, L_display, C_display], 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_display, L_display, C_display], - 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_display)) || isnan(to_upper(R_display))) - if has_error_bounds - data[!, "lower"] = [ - to_lower(R_display), to_lower(L_display), to_lower(C_display) - ] - data[!, "upper"] = [ - to_upper(R_display), to_upper(L_display), to_upper(C_display) - ] - - # 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 - + throw(ArgumentError( + "DataFrame(CableDesign, :baseparams) no longer performs a calculation; create a CableConstantsProblem, call compute!, then render DataFrame(constants)", + )) elseif format == :components # Component-level properties properties = [ @@ -187,7 +131,7 @@ function DataFrame( end else Base.error( - "Unsupported format: $format. Use :baseparams, :components, or :detailed", + "Unsupported format: $format. Use :components or :detailed", ) end @@ -197,6 +141,19 @@ end """ $(TYPEDSIGNATURES) +Render already-computed cable constants without performing a calculation. +""" +function DataFrame(constants::CableConstants)::DataFrame + return DataFrame( + parameter=["R", "L", "C"], + value=[constants.R, constants.L, constants.C], + unit=["Ω/m", "H/m", "F/m"], + ) +end + +""" +$(TYPEDSIGNATURES) + Helper function to extract properties from a part for detailed format. # Arguments diff --git a/src/datamodel/cableslibrary/vdeparse.jl b/src/datamodel/cableslibrary/vdeparse.jl index ac956923..a1762600 100644 --- a/src/datamodel/cableslibrary/vdeparse.jl +++ b/src/datamodel/cableslibrary/vdeparse.jl @@ -72,7 +72,7 @@ const MAP = Dict{Symbol, Dict{String, String}}( ) ) -# Canonical order of appearance within the stub +# Expected order of appearance within the stub const ORDER = [ :designation, :conductor, diff --git a/src/datamodel/circstrands.jl b/src/datamodel/circstrands.jl index 88d297f0..baad88a0 100644 --- a/src/datamodel/circstrands.jl +++ b/src/datamodel/circstrands.jl @@ -57,7 +57,7 @@ Constructs a [`CircStrands`](@ref) instance based on specified geometric and mat ```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) +circstrands = $(FUNCTIONNAME)(0.01, 0.001, 7, 10, material_props, temperature=25) println(circstrands.mean_diameter) # Outputs mean diameter in m println(circstrands.resistance) # Outputs resistance in Ω/m ``` @@ -128,12 +128,6 @@ Validation.keyword_defaults(::Type{CircStrands}) = _DEFS_CIRCSTRANDS function Validation.coercive_fields(::Type{CircStrands}) (:r_in, :radius_wire, :lay_ratio, :material_props, :temperature) end # 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 - function Validation.extra_rules(::Type{CircStrands}) ( # radii (post-parse they must be numeric) @@ -156,7 +150,6 @@ function maxfill(::Type{CircStrands}, rin::Real, rw::Real) rin == 0 ? 1 : floor(Int, π / asin(rw / (rin + rw))) end -# normalize proxies -> numbers function Validation.parse(::Type{CircStrands}, nt) rin, rw = _normalize_radii(CircStrands, nt.r_in, nt.radius_wire) (; nt..., r_in = rin, radius_wire = rw) diff --git a/src/datamodel/conductorgroup.jl b/src/datamodel/conductorgroup.jl index d6bdcb79..efc64056 100644 --- a/src/datamodel/conductorgroup.jl +++ b/src/datamodel/conductorgroup.jl @@ -96,10 +96,15 @@ 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. +Add a new conductor part to a [`ConductorGroup`](@ref), validate its numeric +inputs, and promote the group’s numeric type when required. -# Behavior: +This updates `gmr`, `resistance`, `alpha`, `r_ex`, `cross_section`, and +`num_wires`. The new part temperature defaults to the temperature of the +first layer, and its inner radius defaults to the external radius of the +existing group. + +# Behavior 1. Apply part-level keyword defaults. 2. Default `r_in` to `group.r_ex` if absent. @@ -115,26 +120,29 @@ normalizing proxies, and **promoting** the group’s numeric type if required. # 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. +- The updated [`ConductorGroup`](@ref). This is `group` when its numeric type + is unchanged, or a promoted group when the new part requires another numeric + type. !!! 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. + - The current outer radius is supplied automatically as `r_in`. For radial + parts, pass exactly one of `radius` or `thickness`; do not pass a layer + object as a constructor input. + - For uncertain geometries, the existing outer-radius derivative graph is + retained. Adjacent layers therefore share one physical boundary and + cumulative-radius covariance is preserved across 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) +```jldoctest +using LineCableModels.DataModel: CircStrands, ConductorGroup +using LineCableModels.Materials: Material + +material = Material(1.7241e-8, 1.0, 0.999994, 20.0, 0.00393) +conductor = ConductorGroup(CircStrands(0.0, 0.001, 1, 0.0, material)) +conductor = $(FUNCTIONNAME)(conductor, CircStrands, 0.001, 6, 15.0, material) +@assert length(conductor.layers) == 2 +# output ``` """ @@ -152,19 +160,21 @@ function add!( rin = get(kwv, :r_in, group.r_ex) kwv = haskey(kwv, :r_in) ? kwv : merge(kwv, (; r_in = rin)) + part_args, kwv = _resolve_group_radial_args(C, rin, args, kwv) + # 3) Decide target numeric type using *current group + raw inputs* - Tnew = resolve_T(group, rin, args..., values(kwv)...) + Tnew = resolve_T(group, rin, part_args..., values(kwv)...) if Tnew === T # 4a) Fast path: mutate in place - return _do_add!(group, C, args...; kwv...) + return _do_add!(group, C, part_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...) + return _do_add!(promoted, C, part_args...; kwv...) end end @@ -184,7 +194,7 @@ function _do_add!( # Materialize keyword args into a NamedTuple (never poke Base.Pairs internals) kw = (; kwargs...) - # Validate + parse with the part’s own pipeline (proxies resolved here) + # Validate and parse with the part's own numeric input sequence. ntv = Validation.validate!(C, kw.r_in, args...; kw...) # Coerce validated values to group’s T and call strict numeric core @@ -192,6 +202,18 @@ function _do_add!( coerced = _coerced_args(C, ntv, Tg, order) # respects coercive_fields(C) new_part = C(coerced...) + return _append_conductor!(group, new_part) +end + +function _append_conductor!( + group::ConductorGroup{T}, + new_part::AbstractConductorPart{T}, +) where {T} + isapprox(new_part.r_in, group.r_ex) || throw(ArgumentError( + "new conductor layer must start at the group's current outer radius " * + "$(group.r_ex); got $(new_part.r_in)", + )) + # Update equivalent properties group.gmr = calc_equivalent_gmr(group, new_part) group.alpha = calc_equivalent_alpha(group.alpha, group.resistance, @@ -215,4 +237,16 @@ function _do_add!( return group end +function add!( + group::ConductorGroup{T}, + new_part::AbstractConductorPart{U}, +) where {T,U} + target_type = promote_type(T, U) + if target_type === T + return _append_conductor!(group, coerce_to_T(new_part, T)) + end + promoted = coerce_to_T(group, target_type) + return _append_conductor!(promoted, coerce_to_T(new_part, target_type)) +end + include("conductorgroup/base.jl") diff --git a/src/datamodel/insulator.jl b/src/datamodel/insulator.jl index fd32d7ba..95e95520 100644 --- a/src/datamodel/insulator.jl +++ b/src/datamodel/insulator.jl @@ -34,7 +34,7 @@ Constructs an [`Insulator`](@ref) object with specified geometric and material p # Arguments - `r_in`: Internal radius of the insulating layer \\[m\\]. -- `r_ex`: External radius or thickness of the layer \\[m\\]. +- `r_ex`: External radius of the layer \\[m\\]. - `material_props`: Material properties of the insulating material. - `temperature`: Operating temperature of the insulator \\[°C\\]. @@ -91,15 +91,8 @@ 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 function Validation.parse(::Type{Insulator}, nt) rin, rex = _normalize_radii(Insulator, nt.r_in, nt.r_ex) (; nt..., r_in = rin, r_ex = rex) @@ -107,3 +100,14 @@ end # This macro expands to a weakly-typed constructor for Insulator @construct Insulator _REQ_INSULATOR _OPT_INSULATOR _DEFS_INSULATOR + +function Insulator( + r_in::Real, + material_props::Material; + radius::Union{Nothing,Real}=nothing, + thickness::Union{Nothing,Real}=nothing, + temperature::Real=T₀, +) + r_ex = _resolve_outer_radius(Insulator, r_in; radius, thickness) + return Insulator(r_in, r_ex, material_props; temperature) +end diff --git a/src/datamodel/insulatorgroup.jl b/src/datamodel/insulatorgroup.jl index dac8f3b4..3dc94074 100644 --- a/src/datamodel/insulatorgroup.jl +++ b/src/datamodel/insulatorgroup.jl @@ -78,9 +78,14 @@ 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. +Add a new part to an [`InsulatorGroup`](@ref) and update its equivalent +electrical parameters. -# Behavior: +This updates `shunt_capacitance`, `shunt_conductance`, `r_ex`, and +`cross_section`. The new part inner radius defaults to the external radius of +the existing group. + +# Behavior 1. Apply part-level keyword defaults (from `Validation.keyword_defaults`). 2. Default `r_in` to `group.r_ex` if absent. @@ -96,25 +101,34 @@ Adds a new part to an existing [`InsulatorGroup`](@ref) object and updates its e # 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. +- The updated [`InsulatorGroup`](@ref). This is `group` when its numeric type + is unchanged, or a promoted group when the new part requires another numeric + type. !!! 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. + - The current outer radius is supplied automatically as `r_in`. Pass + exactly one of `radius` or `thickness`; do not pass a layer object as a + constructor input. + - For uncertain geometries, the existing outer-radius derivative graph is + retained. Adjacent layers therefore share one physical boundary and + cumulative-radius covariance is preserved across 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) +```jldoctest +using LineCableModels.DataModel: Insulator, InsulatorGroup, Semicon +using LineCableModels.Materials: Material + +material = Material(1e10, 3.0, 1.0, 20.0, 0.0) +insulator_group = InsulatorGroup(Insulator(0.01, 0.015, material)) +insulator_group = $(FUNCTIONNAME)( + insulator_group, + Semicon, + material; + thickness=0.003, +) +@assert length(insulator_group.layers) == 2 +# output ``` """ @@ -133,19 +147,21 @@ function add!( rin = get(kwv, :r_in, group.r_ex) kwv = haskey(kwv, :r_in) ? kwv : merge(kwv, (; r_in = rin)) + part_args, kwv = _resolve_group_radial_args(C, rin, args, kwv) + # 3) Decide target numeric type using *current group + raw inputs + f* - Tnew = resolve_T(group, rin, args..., values(kwv)..., f) + Tnew = resolve_T(group, rin, part_args..., values(kwv)..., f) if Tnew === T # 4a) Fast path: mutate in place - return _do_add!(group, C, args...; f, kwv...) + return _do_add!(group, C, part_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...) + return _do_add!(promoted, C, part_args...; f, kwv...) end end @@ -170,7 +186,7 @@ function _do_add!( # Materialize keyword args into a NamedTuple kw = (; kwargs...) - # Validate + parse with the part’s own pipeline (proxies resolved here) + # Validate and parse with the part's own numeric input sequence. ntv = Validation.validate!(C, kw.r_in, args...; kw...) # Build argument order and coerce validated values to group’s T @@ -178,6 +194,19 @@ function _do_add!( coerced = _coerced_args(C, ntv, Tg, order) # respects coercive_fields(C) new_part = C(coerced...) # call strict numeric core + return _append_insulator!(group, new_part; f) +end + +function _append_insulator!( + group::InsulatorGroup{Tg}, + new_part::AbstractInsulatorPart{Tg}; + f::Number=f₀, +) where {Tg} + isapprox(new_part.r_in, group.r_ex) || throw(ArgumentError( + "new insulator layer must start at the group's current outer radius " * + "$(group.r_ex); got $(new_part.r_in)", + )) + # Parallel admittances at frequency f ω = Tg(2π) * coerce_to_T(f, Tg) Yg = Complex(group.shunt_conductance, ω * group.shunt_capacitance) @@ -194,4 +223,21 @@ function _do_add!( return group end +function add!( + group::InsulatorGroup{T}, + new_part::AbstractInsulatorPart{U}; + f::Number=f₀, +) where {T,U} + target_type = resolve_T(group, new_part, f) + if target_type === T + return _append_insulator!(group, coerce_to_T(new_part, T); f) + end + promoted = coerce_to_T(group, target_type) + return _append_insulator!( + promoted, + coerce_to_T(new_part, target_type); + f=coerce_to_T(f, target_type), + ) +end + include("insulatorgroup/base.jl") diff --git a/src/datamodel/plotspecs.jl b/src/datamodel/plotspecs.jl index 1b06ed3f..3f00ce3c 100644 --- a/src/datamodel/plotspecs.jl +++ b/src/datamodel/plotspecs.jl @@ -212,6 +212,39 @@ function _layer_series!(series, layer, label, group, xcenter, ycenter; include_l return series end +_preview_layer_name(::CircStrands) = "round wires" +_preview_layer_name(::RectStrands) = "sector strands" +_preview_layer_name(::Strip) = "strip" +_preview_layer_name(::Tubular) = "tubular conductor" +_preview_layer_name(::Semicon) = "semiconductor" +_preview_layer_name(::Insulator) = "insulation" +_preview_layer_name(::ConductorGroup) = "conductor group" +_preview_layer_name(layer) = lowercase(string(nameof(typeof(layer)))) + +function _preview_layer_identities(component_id, layers, role::Symbol) + names = _preview_layer_name.(layers) + totals = Dict{String, Int}() + for name in names + totals[name] = get(totals, name, 0) + 1 + end + + seen = Dict{String, Int}() + component_name = uppercasefirst(replace(String(component_id), '_' => ' ')) + identities = Tuple{String, Symbol}[] + for (index, name) in enumerate(names) + occurrence = get(seen, name, 0) + 1 + seen[name] = occurrence + suffix = totals[name] == 1 ? "" : " $occurrence" + label = "$component_name: $name$suffix" + key = replace( + "preview_$(component_id)_$(role)_$index", + r"[^0-9A-Za-z]+" => "_", + ) + push!(identities, (label, Symbol(key))) + end + return identities +end + function _design_series(design, xcenter, ycenter; display_legend::Bool) series = PlotBuilder.SeriesSpec[] outer_radius = try @@ -224,27 +257,24 @@ function _design_series(design, xcenter, ycenter; display_legend::Bool) _polygon_series(_circle_points(outer_radius, xcenter, ycenter), nothing, :background, :white; stroke = :transparent, width = 0.0)) end - function preview_identity(layer, layer_name) - hasproperty(layer, :material_props) || return layer_name, Symbol(layer_name) - material = layer.material_props - rho = to_nominal(material.rho) - mu_r = to_nominal(material.mu_r) - eps_r = to_nominal(material.eps_r) - displayed_rho = isfinite(rho) ? round(Float64(rho); sigdigits = 2) : rho - label = "$layer_name ρ=$displayed_rho" - key = replace("$(layer_name)_$(rho)_$(mu_r)_$(eps_r)", r"[^0-9A-Za-z]+" => "_") - return label, Symbol(key) - end for component in design.components - for layer in component.conductor_group.layers - layer_name = lowercase(string(nameof(typeof(layer)))) - label, group = preview_identity(layer, layer_name) + conductor_layers = component.conductor_group.layers + conductor_identities = _preview_layer_identities( + component.id, + conductor_layers, + :conductor, + ) + for (layer, (label, group)) in zip(conductor_layers, conductor_identities) _layer_series!(series, layer, label, group, xcenter, ycenter; include_label = display_legend) end - for layer in component.insulator_group.layers - layer_name = lowercase(string(nameof(typeof(layer)))) - label, group = preview_identity(layer, layer_name) + insulator_layers = component.insulator_group.layers + insulator_identities = _preview_layer_identities( + component.id, + insulator_layers, + :insulator, + ) + for (layer, (label, group)) in zip(insulator_layers, insulator_identities) _layer_series!(series, layer, label, group, xcenter, ycenter; include_label = display_legend) end diff --git a/src/datamodel/radii.jl b/src/datamodel/radii.jl index 872ab258..b18a13fc 100644 --- a/src/datamodel/radii.jl +++ b/src/datamodel/radii.jl @@ -1,149 +1,58 @@ """ -$(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 `_do_resolve_radius` implementations 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. - -""" -@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) + _normalize_radii(::Type{Part}, r_in, r_ex) +Validate the strict materialized radial boundary. Both values must be real +numbers. Interpretation of diameter, layer objects, or backward thickness +inference is deliberately unsupported. """ -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} +@inline function _normalize_radii(::Type{Part}, r_in::Real, r_ex::Real) where {Part} 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) +@inline function _normalize_radii(::Type{Part}, r_in, r_ex) where {Part} + throw(ArgumentError( + "[$(nameof(Part))] radial inputs must be real numeric values; got " * + "$(typeof(r_in)) and $(typeof(r_ex))", + )) end -# These intersections make the intended `Thickness` semantics explicit for -# strand layers and keep dispatch unambiguous. -@inline _do_normalize_radii(r_in::Number, thickness::Thickness, - ::Type{AbstractStrandsLayer}) = (r_in, r_in + thickness.value) - -@inline function _do_normalize_radii( - thickness::Thickness, - r_ex::Number, - ::Type{AbstractStrandsLayer} -) - r_in = r_ex - thickness.value - r_in >= 0 || throw( - ArgumentError( - "[AbstractStrandsLayer] thickness $(thickness.value) exceeds outer radius $(r_ex).", - ), - ) - return r_in, r_ex -end - -@inline function _do_normalize_radii( - ::Thickness, - ::Thickness, - ::Type{AbstractStrandsLayer} -) - throw( - ArgumentError( - "[AbstractStrandsLayer] cannot specify thickness for both inner and outer radii.", - ), - ) -end +""" + _resolve_outer_radius(::Type{Part}, r_in; radius=nothing, thickness=nothing) -@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 +Resolve one forward radial declaration. Exactly one of `radius` (an absolute +outer radius) and `thickness` (an outward increment) must be supplied. +""" +function _resolve_outer_radius( + ::Type{Part}, + r_in::Real; + radius::Union{Nothing,Real}=nothing, + thickness::Union{Nothing,Real}=nothing, +) where {Part} + xor(radius === nothing, thickness === nothing) || throw(ArgumentError( + "[$(nameof(Part))] provide exactly one of radius or thickness", + )) + r_ex = radius === nothing ? r_in + thickness : radius + r_ex > r_in || throw(ArgumentError( + "[$(nameof(Part))] outer radius must exceed inner radius; got r_in=$r_in, r_ex=$r_ex", + )) + return r_ex 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.", - ), - ) +@inline function _resolve_group_radial_args( + ::Type{Part}, + r_in::Real, + args::Tuple, + kwargs::NamedTuple, +) where {Part} + has_radius = haskey(kwargs, :radius) + has_thickness = haskey(kwargs, :thickness) + (has_radius || has_thickness) || return args, kwargs + Validation.has_radii(Part) || throw(ArgumentError( + "[$(nameof(Part))] radius/thickness keywords are not valid for this part", + )) + radius = has_radius ? kwargs.radius : nothing + thickness = has_thickness ? kwargs.thickness : nothing + r_ex = _resolve_outer_radius(Part, r_in; radius, thickness) + cleaned = Base.structdiff(kwargs, (; radius=nothing, thickness=nothing)) + return (r_ex, args...), cleaned end diff --git a/src/datamodel/rectstrands.jl b/src/datamodel/rectstrands.jl index 2c4856c7..8ff919f6 100644 --- a/src/datamodel/rectstrands.jl +++ b/src/datamodel/rectstrands.jl @@ -157,14 +157,6 @@ function Validation.coercive_fields(::Type{RectStrands}) (:r_in, :thickness, :width, :lay_ratio, :material_props, :temperature) end -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) diff --git a/src/datamodel/semicon.jl b/src/datamodel/semicon.jl index ad18f70d..020d8344 100644 --- a/src/datamodel/semicon.jl +++ b/src/datamodel/semicon.jl @@ -34,7 +34,7 @@ Constructs a [`Semicon`](@ref) instance with calculated electrical and geometric # Arguments - `r_in`: Internal radius of the semiconducting layer \\[m\\]. -- `r_ex`: External radius or thickness of the layer \\[m\\]. +- `r_ex`: External radius of the layer \\[m\\]. - `material_props`: Material properties of the semiconducting material. - `temperature`: Operating temperature of the layer \\[°C\\] (default: T₀). @@ -46,7 +46,7 @@ Constructs a [`Semicon`](@ref) instance with calculated electrical and geometric ```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) +semicon_layer = $(FUNCTIONNAME)(0.01, 0.012, 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] @@ -96,15 +96,8 @@ 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 function Validation.parse(::Type{Semicon}, nt) rin, rex = _normalize_radii(Semicon, nt.r_in, nt.r_ex) (; nt..., r_in = rin, r_ex = rex) @@ -112,3 +105,14 @@ end # This macro expands to a weakly-typed constructor for Semicon @construct Semicon _REQ_SEMICON _OPT_SEMICON _DEFS_SEMICON + +function Semicon( + r_in::Real, + material_props::Material; + radius::Union{Nothing,Real}=nothing, + thickness::Union{Nothing,Real}=nothing, + temperature::Real=T₀, +) + r_ex = _resolve_outer_radius(Semicon, r_in; radius, thickness) + return Semicon(r_in, r_ex, material_props; temperature) +end diff --git a/src/datamodel/strip.jl b/src/datamodel/strip.jl index 87b504f8..f51c6c0e 100644 --- a/src/datamodel/strip.jl +++ b/src/datamodel/strip.jl @@ -42,7 +42,7 @@ Constructs a [`Strip`](@ref) object with specified geometric and material parame # Arguments - `r_in`: Internal radius of the strip \\[m\\]. -- `r_ex`: External radius or thickness of the strip \\[m\\]. +- `r_ex`: External radius 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. @@ -57,7 +57,7 @@ Constructs a [`Strip`](@ref) object with specified geometric and material parame ```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) +strip = $(FUNCTIONNAME)(0.01, 0.012, 0.05, 10, material_props, temperature=25) println(strip.cross_section) # Output: 0.0001 [m²] println(strip.resistance) # Output: Resistance value [Ω/m] ``` @@ -121,13 +121,6 @@ Validation.keyword_defaults(::Type{Strip}) = _DEFS_STRIP function Validation.coercive_fields(::Type{Strip}) (:r_in, :r_ex, :width, :lay_ratio, :material_props, :temperature) end # 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 - function Validation.extra_rules(::Type{Strip}) ( IsA{Material}(:material_props), @@ -139,7 +132,6 @@ function Validation.extra_rules(::Type{Strip}) ) end -# normalize proxies -> numbers function Validation.parse(::Type{Strip}, nt) rin, rex = _normalize_radii(Strip, nt.r_in, nt.r_ex) (; nt..., r_in = rin, r_ex = rex) @@ -147,3 +139,25 @@ end # This macro expands to a weakly-typed constructor for Strip @construct Strip _REQ_STRIP _OPT_STRIP _DEFS_STRIP + +function Strip( + r_in::Real, + width::Real, + lay_ratio::Real, + material_props::Material; + radius::Union{Nothing,Real}=nothing, + thickness::Union{Nothing,Real}=nothing, + temperature::Real=T₀, + lay_direction::Int=1, +) + r_ex = _resolve_outer_radius(Strip, r_in; radius, thickness) + return Strip( + r_in, + r_ex, + width, + lay_ratio, + material_props; + temperature, + lay_direction, + ) +end diff --git a/src/datamodel/tubular.jl b/src/datamodel/tubular.jl index 21475550..3dc61ac3 100644 --- a/src/datamodel/tubular.jl +++ b/src/datamodel/tubular.jl @@ -86,15 +86,8 @@ 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 function Validation.parse(::Type{Tubular}, nt) rin, rex = _normalize_radii(Tubular, nt.r_in, nt.r_ex) (; nt..., r_in = rin, r_ex = rex) @@ -102,3 +95,14 @@ end # This macro expands to a weakly-typed constructor for Tubular @construct Tubular _REQ_TUBULAR _OPT_TUBULAR _DEFS_TUBULAR + +function Tubular( + r_in::Real, + material_props::Material; + radius::Union{Nothing,Real}=nothing, + thickness::Union{Nothing,Real}=nothing, + temperature::Real=T₀, +) + r_ex = _resolve_outer_radius(Tubular, r_in; radius, thickness) + return Tubular(r_in, r_ex, material_props; temperature) +end diff --git a/src/datamodel/types.jl b/src/datamodel/types.jl index 6c8b5913..dd31c1c5 100644 --- a/src/datamodel/types.jl +++ b/src/datamodel/types.jl @@ -1,38 +1,3 @@ -# 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) diff --git a/src/datamodel/validation.jl b/src/datamodel/validation.jl index d45e5279..a5df1007 100644 --- a/src/datamodel/validation.jl +++ b/src/datamodel/validation.jl @@ -1,87 +1,14 @@ """ -$(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 -``` -""" -function is_radius_input(::Type{T}, ::Val{:r_in}, p::AbstractCablePart) where {T} - hasproperty(p, :r_ex) -end - -""" -$(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) + _with_kwdefaults(::Type{Part}, kwargs) + +Merge the strict part constructor's declared keyword defaults with caller +keywords. Radial stacking is handled by group/spec materializers; this +validation layer receives numeric physical state. +""" +@inline function _with_kwdefaults(::Type{Part}, kwargs::NamedTuple) where {Part} + defaults = Validation.keyword_defaults(Part) + defaults === () && return kwargs + values = defaults isa NamedTuple ? defaults : + NamedTuple{Validation.keyword_fields(Part)}(defaults) + return merge(values, kwargs) end diff --git a/src/earthprops/EarthProps.jl b/src/earthprops/EarthProps.jl index f3e9961e..25fa00a2 100644 --- a/src/earthprops/EarthProps.jl +++ b/src/earthprops/EarthProps.jl @@ -27,8 +27,6 @@ using ..Commons using ..Utils: resolve_T import ..Commons: get_description, add! import ..Utils: coerce_to_T -using Measurements - include("fdprops.jl") """ diff --git a/src/engine/Engine.jl b/src/engine/Engine.jl index 0aed910e..05be964f 100644 --- a/src/engine/Engine.jl +++ b/src/engine/Engine.jl @@ -1,17 +1,20 @@ """ 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. +The [`Engine`](@ref) module provides the main numerical functionality of +[`LineCableModels.jl`](index.md). It implements the materialized problems, +formulations, and primitive results used to calculate frequency-dependent +electrical parameters (Z/Y matrices) of line and cable systems. # Overview - Calculation of frequency-dependent series impedance (Z) and shunt admittance (Y) matrices. -- Uncertainty propagation for geometric and material parameters using `Measurements.jl`. +- Direct uncertainty propagation through the optional Measurements extension. - 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. +- Phase and sequence domain calculations for ordinary and uncertain values. - Novel N-layer concentric cable formulation with semiconductor modeling. # Dependencies @@ -22,7 +25,7 @@ $(IMPORTS) module Engine # Export public API -export LineParametersProblem, +export CableConstantsProblem, LineParametersProblem, LineParameters, SeriesImpedance, ShuntAdmittance, Z, Y, R, X, L, G, B, C, series_impedance, shunt_admittance, @@ -30,13 +33,12 @@ export LineParametersProblem, conductance, susceptance, capacitance, frequencies, nconductors, nfrequencies, basis, kronify -export EMTFormulation, FormulationSet, LineParamOptions +export AbstractFormulation, EMTFormulation, Formulation, EMTOptions, ComputeOptions export compute!, plot # Module-specific dependencies using Reexport, ForceImport -using Measurements using LinearAlgebra using ..Commons import ..Commons: get_description, LineParamsDomain, PhaseDomain, ModalDomain, domain @@ -52,7 +54,8 @@ using ..UnitHandler using ..PlotBuilder using ..Materials using ..EarthProps: EarthModel -using ..DataModel: LineCableSystem +using ..DataModel: CableDesign, CableConstants, LineCableSystem +import ..DataModel using ..Utils: levelfrom, TimestampLogger using Logging, LoggingExtras @@ -107,11 +110,23 @@ include("plotspecs.jl") include("dataframe.jl") """ - plot(object; kwargs...) + plot(parameters[, quantities]; kwargs...) + plot(impedance, frequencies[, quantities]; kwargs...) + plot(admittance, frequencies[, quantities]; kwargs...) -Plot computed line parameters with a loaded Makie backend. +Plot computed line parameters with a loaded Makie backend. `quantities` is a +tuple of accessors such as `(R, L, G, C)` or `(abs, angle)`. + +Without an explicit selection, [`LineParameters`](@ref) produces separate +series-impedance and shunt-admittance figures. Each figure places the real part +on the left and the imaginary part on the right. Every selected matrix element +is represented by one data series. Load `CairoMakie`, `GLMakie`, or `WGLMakie` before calling this function. + +# Returns + +- A `Vector{UIPlot}` containing one figure for each selected matrix family. """ function plot end diff --git a/src/engine/base.jl b/src/engine/base.jl index 243a09f0..f9506775 100644 --- a/src/engine/base.jl +++ b/src/engine/base.jl @@ -37,7 +37,6 @@ function Base.getindex( ) end -_has_uncertainty_type(::Type{Complex{S}}) where {S} = S <: Measurement _has_uncertainty_type(::Type) = false @inline function _basis_units(value, per_length_unit, total_unit) diff --git a/src/engine/dataframe.jl b/src/engine/dataframe.jl index a2baa709..41596919 100644 --- a/src/engine/dataframe.jl +++ b/src/engine/dataframe.jl @@ -34,13 +34,6 @@ function _clip_field(value::Real, tolerance) return abs(value) <= tolerance ? zero(value) : value end -function _clip_field(value::Measurements.Measurement, tolerance) - nominal = abs(Measurements.value(value)) <= tolerance ? 0.0 : Measurements.value(value) - uncertainty_value = abs(Measurements.uncertainty(value)) <= tolerance ? 0.0 : - Measurements.uncertainty(value) - return Measurements.measurement(nominal, uncertainty_value) -end - _clip_field(value, _) = value function _dataframe_unit_label(component, object_basis, length_unit, quantity_units) @@ -56,14 +49,16 @@ end function _matrix_dataframes( object, - frequency_values; - mode, - coord, + frequency_values, + component_names; frequency_unit, length_unit, quantity_units, tolerance ) + isempty(component_names) && throw( + ArgumentError("at least one quantity is required for each matrix family"), + ) frequency_quantity = UnitHandler.QuantityTag{:freq}() frequency_target = UnitHandler.units(frequency_unit, :hertz) frequency_factor = UnitHandler.scale_factor( @@ -71,11 +66,6 @@ function _matrix_dataframes( frequency_target ) displayed_frequency = frequency_values .* frequency_factor - component_names = UnitHandler.line_components( - _line_parameter_kind(object), - mode, - coord - ) component_arrays = Dict( component => _line_component_values(Val(component), object, frequency_values) for @@ -111,19 +101,19 @@ function _matrix_dataframes( end """ - DataFrame(parameters::Union{SeriesImpedance,ShuntAdmittance}; kwargs...) +$(TYPEDSIGNATURES) Convert each matrix entry to a frequency-indexed `DataFrame`. The result is an -`n × n` matrix of frames. `mode=:RLCG` returns physical components and -`mode=:ZY` returns Cartesian or polar components selected by `coord`. +`n × n` matrix of frames. The optional accessor tuple selects the displayed +quantities. By default, series impedance is represented by its real and +imaginary parts, and shunt admittance is represented likewise. Container [`basis`](@ref) determines whether units are per length or total. """ function DataFrame( - parameters::Union{SeriesImpedance, ShuntAdmittance}; + parameters::Union{SeriesImpedance, ShuntAdmittance}, + quantities::Tuple = (); freqs = nothing, - mode::Symbol = :RLCG, - coord::Symbol = :cart, freq_unit::Symbol = :base, length_unit::Symbol = :kilo, quantity_units = nothing, @@ -133,11 +123,11 @@ function DataFrame( ArgumentError("tol must be finite and nonnegative"), ) frequency_values = _frequency_vector(parameters, freqs) + components = _resolve_line_components(parameters, quantities) return _matrix_dataframes( parameters, - frequency_values; - mode, - coord, + frequency_values, + components; frequency_unit = freq_unit, length_unit, quantity_units, @@ -146,28 +136,54 @@ function DataFrame( end """ - DataFrame(parameters::LineParameters; kwargs...) +$(TYPEDSIGNATURES) Return `(series, shunt)`, two matrices of frequency-indexed `DataFrame`s, -using the frequencies, basis, and domain stored by `parameters`. +using the frequencies, basis, and domain stored by `parameters`. The optional +accessor tuple must select at least one series and one shunt quantity. """ function DataFrame( - parameters::LineParameters; - mode::Symbol = :RLCG, - coord::Symbol = :cart, + parameters::LineParameters, + quantities::Tuple = (); freq_unit::Symbol = :base, length_unit::Symbol = :kilo, quantity_units = nothing, tol::Real = sqrt(eps(Float64)) ) - common = ( - freqs = frequencies(parameters), - mode = mode, - coord = coord, - freq_unit = freq_unit, - length_unit = length_unit, - quantity_units = quantity_units, - tol = tol + isfinite(tol) && tol >= 0 || throw( + ArgumentError("tol must be finite and nonnegative"), + ) + components = _resolve_line_components(parameters, quantities) + series_components = Tuple( + component for component in components if component in _SERIES_COMPONENTS + ) + shunt_components = Tuple( + component for component in components if component in _SHUNT_COMPONENTS + ) + isempty(series_components) && throw(ArgumentError( + "LineParameters presentation requires a series accessor; use DataFrame(Y(parameters), ...) for a shunt-only table", + )) + isempty(shunt_components) && throw(ArgumentError( + "LineParameters presentation requires a shunt accessor; use DataFrame(Z(parameters), ...) for a series-only table", + )) + frequency_values = frequencies(parameters) + common = (; + frequency_unit = freq_unit, + length_unit, + quantity_units, + tolerance = float(tol), + ) + series = _matrix_dataframes( + Z(parameters), + frequency_values, + series_components; + common..., + ) + shunt = _matrix_dataframes( + Y(parameters), + frequency_values, + shunt_components; + common..., ) - return DataFrame(Z(parameters); common...), DataFrame(Y(parameters); common...) + return series, shunt end diff --git a/src/engine/earthadmittance/EarthAdmittance.jl b/src/engine/earthadmittance/EarthAdmittance.jl index 0d94b279..e67625a9 100644 --- a/src/engine/earthadmittance/EarthAdmittance.jl +++ b/src/engine/earthadmittance/EarthAdmittance.jl @@ -15,7 +15,6 @@ export Papadopoulos using ...Commons import ...Commons: get_description import ..Engine: EarthAdmittanceFormulation -using Measurements: Measurement, value using QuadGK: quadgk using ...Utils: _to_σ, _bessel_diff, to_nominal diff --git a/src/engine/earthadmittance/homogeneous.jl b/src/engine/earthadmittance/homogeneous.jl index d61fcb21..41954d06 100644 --- a/src/engine/earthadmittance/homogeneous.jl +++ b/src/engine/earthadmittance/homogeneous.jl @@ -218,7 +218,7 @@ end 0.0, Inf; rtol = 1e-8, - norm = z -> abs(complex(value(real(z)), value(imag(z)))) + norm = z -> abs(complex(to_nominal(real(z)), to_nominal(imag(z)))) ) Iij *= 2 diff --git a/src/engine/earthimpedance/EarthImpedance.jl b/src/engine/earthimpedance/EarthImpedance.jl index 01bff96c..cedde331 100644 --- a/src/engine/earthimpedance/EarthImpedance.jl +++ b/src/engine/earthimpedance/EarthImpedance.jl @@ -15,7 +15,6 @@ export Papadopoulos using ...Commons import ...Commons: get_description import ..Engine: EarthImpedanceFormulation -using Measurements: Measurement, value using QuadGK: quadgk using ...Utils: _to_σ, _bessel_diff, to_nominal diff --git a/src/engine/earthimpedance/homogeneous.jl b/src/engine/earthimpedance/homogeneous.jl index 192a47e1..5c1ecee4 100644 --- a/src/engine/earthimpedance/homogeneous.jl +++ b/src/engine/earthimpedance/homogeneous.jl @@ -197,7 +197,7 @@ end 0.0, 1.0; rtol = 1e-8, - norm = z -> abs(complex(value(real(z)), value(imag(z)))) + norm = z -> abs(complex(to_nominal(real(z)), to_nominal(imag(z)))) ) Sij *= 2 diff --git a/src/engine/ehem/EHEM.jl b/src/engine/ehem/EHEM.jl index d29ff559..759afe79 100644 --- a/src/engine/ehem/EHEM.jl +++ b/src/engine/ehem/EHEM.jl @@ -16,7 +16,6 @@ using ...Commons using ...EarthProps: EarthModel import ...Commons: get_description import ..Engine: AbstractEHEMFormulation -using Measurements include("enforcelayer.jl") diff --git a/src/engine/helpers.jl b/src/engine/helpers.jl index 1d35d906..d53bc604 100644 --- a/src/engine/helpers.jl +++ b/src/engine/helpers.jl @@ -6,52 +6,7 @@ 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 +_find_common_type(problem::LineParametersProblem) = resolve_T(problem) function _get_earth_data( functor::AbstractEHEMFormulation, @@ -81,9 +36,9 @@ function _get_earth_data(::Nothing, @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])) + ρ[i, j] = coerce_to_T(L.rho_g[j], T) + ε[i, j] = coerce_to_T(L.eps_g[j], T) + μ[i, j] = coerce_to_T(L.mu_g[j], T) end end diff --git a/src/engine/insulationadmittance/InsulationAdmittance.jl b/src/engine/insulationadmittance/InsulationAdmittance.jl index dd37b2b2..9f502e67 100644 --- a/src/engine/insulationadmittance/InsulationAdmittance.jl +++ b/src/engine/insulationadmittance/InsulationAdmittance.jl @@ -16,7 +16,6 @@ using ...Commons import ...Commons: get_description using ...Utils: _to_σ import ..Engine: InsulationAdmittanceFormulation -using Measurements include("lossless.jl") include("parallelrc.jl") diff --git a/src/engine/insulationadmittance/parallelrc.jl b/src/engine/insulationadmittance/parallelrc.jl index 0d87343b..bdf9142a 100644 --- a/src/engine/insulationadmittance/parallelrc.jl +++ b/src/engine/insulationadmittance/parallelrc.jl @@ -1,11 +1,9 @@ """ - ParallelRC +$(TYPEDEF) 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 @@ -30,9 +28,18 @@ function get_description(::ParallelRC) end """ - (formulation::ParallelRC)(r_in, r_ex, rho, eps_r, s) +$(TYPEDSIGNATURES) -Calculate the potential coefficient of one concentric lossy dielectric layer. +Calculate the potential coefficient of one concentric lossy dielectric layer: + +```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``. # Arguments @@ -45,19 +52,6 @@ Calculate the potential coefficient of one concentric lossy dielectric layer. # 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, diff --git a/src/engine/insulationimpedance/InsulationImpedance.jl b/src/engine/insulationimpedance/InsulationImpedance.jl index 770ce4da..c214d8ae 100644 --- a/src/engine/insulationimpedance/InsulationImpedance.jl +++ b/src/engine/insulationimpedance/InsulationImpedance.jl @@ -15,7 +15,6 @@ export Lossless using ...Commons import ...Commons: get_description import ..Engine: InsulationImpedanceFormulation -using Measurements include("lossless.jl") diff --git a/src/engine/internalimpedance/InternalImpedance.jl b/src/engine/internalimpedance/InternalImpedance.jl index 774f8002..1b05b60b 100644 --- a/src/engine/internalimpedance/InternalImpedance.jl +++ b/src/engine/internalimpedance/InternalImpedance.jl @@ -15,9 +15,8 @@ export ScaledBessel using ...Commons import ...Commons: get_description import ..Engine: InternalImpedanceFormulation -using Measurements using LinearAlgebra -using ...UncertainBessels: besselix, besselkx +using SpecialFunctions: besselix, besselkx using ...Utils: _to_σ include("scaledbessel.jl") diff --git a/src/engine/lineparamopts.jl b/src/engine/lineparamopts.jl index 09fac9f2..57d76435 100644 --- a/src/engine/lineparamopts.jl +++ b/src/engine/lineparamopts.jl @@ -1,85 +1,80 @@ -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 +""" +$(TYPEDEF) -# --- Helpers to turn anything into a NamedTuple ---------------------------- +Select the physical reductions and corrections used by an EMT formulation. -_to_nt(nt::NamedTuple) = nt -_to_nt(p::Base.Pairs) = (; p...) -_to_nt(d::AbstractDict) = (; d...) -_to_nt(::Nothing) = (;) +$(TYPEDFIELDS) +""" +Base.@kwdef struct EMTOptions <: AbstractFormulationOptions + "Whether bundled conductors are reduced to an equivalent conductor." + reduce_bundle::Bool=true -# --- Generic key splitter + builder --------------------------------------- + "Whether unassigned conductors are eliminated by Kron reduction." + kron_reduction::Bool=true -const _COMMON_KEYS = Set(fieldnames(LineParamOptions)) + "Whether ideal transposition is applied." + ideal_transposition::Bool=true -function _select_keys(nt::NamedTuple, allowed::Set{Symbol}) - (; (k => v for (k, v) in pairs(nt) if k in allowed)...) + "Whether material resistivity is corrected to the operating temperature." + temperature_correction::Bool=true end -function build_options(::Type{O}, opts; - strict::Bool = true -) where {O <: AbstractFormulationOptions} - nt = _to_nt(opts) +""" +$(TYPEDEF) - own_allowed = Set(filter(!=(:common), fieldnames(O))) - common_nt = _select_keys(nt, _COMMON_KEYS) - own_nt = _select_keys(nt, own_allowed) +Execution options shared by ordinary, full-parametric, and Monte Carlo runs. +`output_basis=:total` scales computed line impedances and admittances by the +materialized system length; cable constants have no system length and therefore +support only `:per_length`. - unknown = setdiff(Set(keys(nt)), union(_COMMON_KEYS, own_allowed)) - if strict && !isempty(unknown) - throw(ArgumentError("Unknown option keys for $(O): $(collect(unknown))")) - end +$(TYPEDFIELDS) +""" +struct ComputeOptions + "Whether unreduced impedance and admittance matrices are retained." + store_primitive_matrices::Bool - return O(; common = LineParamOptions(; common_nt...), own_nt...) -end + "Logging verbosity." + verbosity::Int -# Convenience overloads (accept already-built things) -build_options(::Type{O}, o::O; kwargs...) where {O <: AbstractFormulationOptions} = o -function build_options( - ::Type{O}, - c::LineParamOptions; - kwargs... -) where {O <: AbstractFormulationOptions} - O(; common = c) -end + "Optional path for calculation logs." + logfile::Union{Nothing,String} -# 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") + "Output basis: `:per_length` or `:total`." + output_basis::Symbol + + function ComputeOptions(; + store_primitive_matrices::Bool=true, + verbosity::Integer=0, + logfile::Union{Nothing,AbstractString}=nothing, + output_basis::Symbol=:per_length, + ) + output_basis in (:per_length, :total) || throw(ArgumentError( + "output_basis must be :per_length or :total; got :$output_basis", + )) + return new( + store_primitive_matrices, + Int(verbosity), + logfile === nothing ? nothing : String(logfile), + output_basis, + ) + end 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 +_to_namedtuple(options::NamedTuple) = options +_to_namedtuple(options::Base.Pairs) = (; options...) +_to_namedtuple(options::AbstractDict) = (; options...) +_to_namedtuple(::Nothing) = (;) -@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))")) +function _strict_options(::Type{Options}, options) where {Options} + options isa Options && return options + values = _to_namedtuple(options) + allowed = Set(fieldnames(Options)) + unknown = setdiff(Set(keys(values)), allowed) + isempty(unknown) || throw(ArgumentError( + "unknown options for $Options: $(sort!(collect(unknown)))", + )) + return Options(; values...) end -Base.propertynames(::EMTOptions, ::Bool = false) = (_COMMON_SYMS..., _EMT_OWN..., :common) -function Base.get(o::EMTOptions, s::Symbol, default) - hasproperty(o, s) ? getproperty(o, s) : default -end -asnamedtuple(o::EMTOptions) = (; (k=>getproperty(o, k) for k in propertynames(o))...) +formulation_options(options) = _strict_options(EMTOptions, options) +compute_options(options) = _strict_options(ComputeOptions, options) diff --git a/src/engine/lineparams.jl b/src/engine/lineparams.jl index e0a6ccf6..d35a77b9 100644 --- a/src/engine/lineparams.jl +++ b/src/engine/lineparams.jl @@ -232,17 +232,17 @@ B(lp::LineParameters, args...) = imag.(Y(lp, args...)) end """ - L(parameters[, i, j[, k]]) +$(TYPEDSIGNATURES) Return series inductance using the same frequency-selection grammar as -[`Z`](@ref). Units are \\[H/m\\] for `:per_length` and \\[H\\] for `:total`. - -# Notes +[`Z`](@ref), evaluated as: ```math L(f) = \\frac{\\operatorname{Im} Z(f)}{2\\pi f}. ``` +Units are \\[H/m\\] for `:per_length` and \\[H\\] for `:total`. + # Errors Throws `DomainError` when a selected frequency is zero. @@ -253,17 +253,17 @@ function L(lp::LineParameters) end """ - C(parameters[, i, j[, k]]) +$(TYPEDSIGNATURES) Return shunt capacitance using the same frequency-selection grammar as -[`Y`](@ref). Units are \\[F/m\\] for `:per_length` and \\[F\\] for `:total`. - -# Notes +[`Y`](@ref), evaluated as: ```math C(f) = \\frac{\\operatorname{Im} Y(f)}{2\\pi f}. ``` +Units are \\[F/m\\] for `:per_length` and \\[F\\] for `:total`. + # Errors Throws `DomainError` when a selected frequency is zero. diff --git a/src/engine/plotspecs.jl b/src/engine/plotspecs.jl index c80469e6..54f17c1f 100644 --- a/src/engine/plotspecs.jl +++ b/src/engine/plotspecs.jl @@ -39,14 +39,11 @@ function _conductor_pairs(object, selector) return [(i, j) for i in rows for j in columns] end -_line_parameter_kind(::SeriesImpedance) = :series -_line_parameter_kind(::ShuntAdmittance) = :shunt - function _finite_exponent(curves) maximum_value = 0.0 for curve in curves, sample in curve - nominal = abs(Measurements.value(sample)) + nominal = abs(to_nominal(sample)) isfinite(nominal) && (maximum_value = max(maximum_value, nominal)) end iszero(maximum_value) && return 0 @@ -55,14 +52,71 @@ function _finite_exponent(curves) end struct LinePageKey{K, C} end +struct LineFamilyKey{K} end _line_parent(::LinePageKey{K, C}) where {K, C} = K _line_component(::LinePageKey{K, C}) where {K, C} = C +_line_parent(::LineFamilyKey{K}) where {K} = K + +const _SERIES_COMPONENTS = (:R, :X, :L, :Z_re, :Z_im, :Z_abs, :Z_angle) +const _SHUNT_COMPONENTS = (:G, :B, :C, :Y_re, :Y_im, :Y_abs, :Y_angle) -function _line_page_keys(::Val{K}, ::Val{Mode}) where {K, Mode} - mode, coordinates = Mode +function _line_page_keys(::Val{K}, ::Val{Components}) where {K, Components} + allowed = K === :series ? _SERIES_COMPONENTS : _SHUNT_COMPONENTS return Tuple(LinePageKey{K, component}() - for component in UnitHandler.line_components(K, mode, coordinates)) + for component in Components if component in allowed) +end + +_default_line_components(::LineParameters) = (:Z_re, :Z_im, :Y_re, :Y_im) +_default_line_components(::SeriesImpedance) = (:Z_re, :Z_im) +_default_line_components(::ShuntAdmittance) = (:Y_re, :Y_im) + +function _line_components(::SeriesImpedance, accessor) + accessor === R && return (:R,) + accessor === X && return (:X,) + accessor === L && return (:L,) + accessor === real && return (:Z_re,) + accessor === imag && return (:Z_im,) + accessor === abs && return (:Z_abs,) + accessor === angle && return (:Z_angle,) + accessor === Z && return (:Z_re, :Z_im) + throw(ArgumentError("accessor $(accessor) is not defined for SeriesImpedance presentation")) +end + +function _line_components(::ShuntAdmittance, accessor) + accessor === G && return (:G,) + accessor === B && return (:B,) + accessor === C && return (:C,) + accessor === real && return (:Y_re,) + accessor === imag && return (:Y_im,) + accessor === abs && return (:Y_abs,) + accessor === angle && return (:Y_angle,) + accessor === Y && return (:Y_re, :Y_im) + throw(ArgumentError("accessor $(accessor) is not defined for ShuntAdmittance presentation")) +end + +function _line_components(parameters::LineParameters, accessor) + accessor === Z && return (:Z_re, :Z_im) + accessor === Y && return (:Y_re, :Y_im) + accessor === real && return (:Z_re, :Y_re) + accessor === imag && return (:Z_im, :Y_im) + accessor === abs && return (:Z_abs, :Y_abs) + accessor === angle && return (:Z_angle, :Y_angle) + accessor in (R, X, L) && return _line_components(Z(parameters), accessor) + accessor in (G, B, C) && return _line_components(Y(parameters), accessor) + throw(ArgumentError("accessor $(accessor) is not defined for LineParameters presentation")) +end + +function _resolve_line_components(object, quantities) + quantities isa Tuple || throw(ArgumentError("quantities must be a tuple of accessors")) + selected = isempty(quantities) ? _default_line_components(object) : Tuple( + component for accessor in quantities for component in _line_components(object, accessor) + ) + isempty(selected) && throw(ArgumentError("at least one line-parameter accessor is required")) + length(unique(selected)) == length(selected) || throw( + ArgumentError("line-parameter accessors select duplicate quantities"), + ) + return selected end _line_sources(parameters::LineParameters) = (Z(parameters), Y(parameters)) @@ -78,8 +132,7 @@ _line_source(parameters::LineParameters, ::LinePageKey{:shunt, C}) where {C} = Y function _line_input_defaults(frequencies) return (; frequencies, - mode = :ZY, - coord = :cart, + quantities = (), freq_unit = :base, length_unit = :kilo, quantity_units = nothing, @@ -95,8 +148,7 @@ end function PlotBuilder.input_kwargs(::Type{LineParameterPlotSpec}) ( :frequencies, - :mode, - :coord, + :quantities, :freq_unit, :length_unit, :quantity_units, @@ -124,8 +176,7 @@ end function PlotBuilder.resolve_input(::Type{LineParameterPlotSpec}, recipe::PlotBuilder.PlotRecipe) input = recipe.input - input.mode in (:RLCG, :ZY) || throw(ArgumentError("mode must be :RLCG or :ZY")) - input.coord in (:cart, :polar) || throw(ArgumentError("coord must be :cart or :polar")) + components = _resolve_line_components(recipe.object, input.quantities) input.xscale in (:linear, :log10) || throw( ArgumentError("xscale must be :linear or :log10"), ) @@ -141,9 +192,9 @@ function PlotBuilder.resolve_input(::Type{LineParameterPlotSpec}, recipe::PlotBu throw( DomainError(frequencies, "logarithmic frequency axes require positive frequencies"), ) - input.mode === :RLCG && any(iszero, frequencies) && + any(component -> component in (:L, :C), components) && any(iszero, frequencies) && throw( - DomainError(frequencies, "RLCG plotting is undefined at zero frequency"), + DomainError(frequencies, "inductance and capacitance are undefined at zero frequency"), ) recipe.renderer.fig_size isa Tuple{Int, Int} || throw( ArgumentError("fig_size must be a tuple of two integers"), @@ -158,13 +209,13 @@ function PlotBuilder.resolve_input(::Type{LineParameterPlotSpec}, recipe::PlotBu @warn "Frequency vector has $(length(frequencies)) sample(s); nothing to plot." return PlotBuilder.PlotRecipe( recipe.object, - merge(input, (; frequencies)), + merge(input, (; frequencies, components)), recipe.renderer ) end function PlotBuilder.recipe_mode(::Type{LineParameterPlotSpec}, recipe::PlotBuilder.PlotRecipe) - return Val((recipe.input.mode, recipe.input.coord)) + return Val(recipe.input.components) end function PlotBuilder.grouping_mode( @@ -172,22 +223,56 @@ function PlotBuilder.grouping_mode( mode::Val, recipe::PlotBuilder.PlotRecipe ) - return Val(:faceted_pages) + return Val(:panels) +end + +_line_family_facets(::SeriesImpedance) = (LineFamilyKey{:series}(),) +_line_family_facets(::ShuntAdmittance) = (LineFamilyKey{:shunt}(),) +function _line_family_facets(::LineParameters) + return (LineFamilyKey{:series}(), LineFamilyKey{:shunt}()) +end + +function _line_component_facets( + mode::Val, + recipe::PlotBuilder.PlotRecipe, + ::LineFamilyKey{K}, +) where {K} + return _line_page_keys(Val(K), mode) end -function PlotBuilder.page_facets( +function PlotBuilder.page_keys( ::Type{LineParameterPlotSpec}, mode::Val, - recipe::PlotBuilder.PlotRecipe + ::Val{:panels}, + recipe::PlotBuilder.PlotRecipe, ) length(recipe.input.frequencies) <= 1 && return () - return _line_page_facets(mode, recipe.object) + return Tuple( + family for family in _line_family_facets(recipe.object) + if !isempty(_line_component_facets(mode, recipe, family)) + ) end -_line_page_facets(mode::Val, ::SeriesImpedance) = _line_page_keys(Val(:series), mode) -_line_page_facets(mode::Val, ::ShuntAdmittance) = _line_page_keys(Val(:shunt), mode) -function _line_page_facets(mode::Val, ::LineParameters) - return (_line_page_keys(Val(:series), mode)..., _line_page_keys(Val(:shunt), mode)...) +function PlotBuilder.view_keys( + ::Type{LineParameterPlotSpec}, + mode::Val, + ::Val{:panels}, + recipe::PlotBuilder.PlotRecipe, + page_key::LineFamilyKey, +) + return _line_component_facets(mode, recipe, page_key) +end + +function PlotBuilder.series_keys( + ::Type{LineParameterPlotSpec}, + mode::Val, + ::Val{:panels}, + recipe::PlotBuilder.PlotRecipe, + page_key::LineFamilyKey, + view_key::LinePageKey, +) + source = _line_source(recipe.object, view_key) + return _conductor_pairs(source, recipe.input.con) end function _line_values(recipe::PlotBuilder.PlotRecipe, page_key::LinePageKey) @@ -208,23 +293,6 @@ function _line_values(recipe::PlotBuilder.PlotRecipe, page_key::LinePageKey) return values, conversion end -function PlotBuilder.group_facets( - ::Type{LineParameterPlotSpec}, - mode::Val, - recipe::PlotBuilder.PlotRecipe, - page_key::LinePageKey -) - source = _line_source(recipe.object, page_key) - pairs = _conductor_pairs(source, recipe.input.con) - values, conversion = _line_values(recipe, page_key) - return [pair - for pair in pairs - if any( - sample -> abs(Measurements.value(sample)) > eps(Float64), - view(values, pair[1], pair[2], :) .* conversion - )] -end - function PlotBuilder.axis_quantity( ::Type{LineParameterPlotSpec}, mode::Val, @@ -255,6 +323,24 @@ function PlotBuilder.axis_quantity( return quantity end +function PlotBuilder.axis_quantity( + ::Type{LineParameterPlotSpec}, + mode::Val, + ::Val{:y}, + recipe::PlotBuilder.PlotRecipe, + page_key::LineFamilyKey, + view_key::LinePageKey, +) + return PlotBuilder.axis_quantity( + LineParameterPlotSpec, + mode, + Val(:y), + recipe, + view_key, + nothing, + ) +end + function PlotBuilder.axis_unit( ::Type{LineParameterPlotSpec}, mode::Val, @@ -287,6 +373,26 @@ function PlotBuilder.axis_unit( return target end +function PlotBuilder.axis_unit( + ::Type{LineParameterPlotSpec}, + mode::Val, + ::Val{:y}, + quantity::UnitHandler.QuantityTag, + recipe::PlotBuilder.PlotRecipe, + page_key::LineFamilyKey, + view_key::LinePageKey, +) + return PlotBuilder.axis_unit( + LineParameterPlotSpec, + mode, + Val(:y), + quantity, + recipe, + view_key, + nothing, + ) +end + function PlotBuilder.axis_scale( ::Type{LineParameterPlotSpec}, mode::Val, @@ -337,6 +443,26 @@ function PlotBuilder.series_data( return collect(view(values, series_key[1], series_key[2], :)) .* conversion end +function PlotBuilder.series_data( + ::Type{LineParameterPlotSpec}, + mode::Val, + ::Val{:y}, + recipe::PlotBuilder.PlotRecipe, + page_key::LineFamilyKey, + view_key::LinePageKey, + series_key::Tuple{Int, Int}, +) + return PlotBuilder.series_data( + LineParameterPlotSpec, + mode, + Val(:y), + recipe, + view_key, + nothing, + series_key, + ) +end + function PlotBuilder.legend_label( ::Type{LineParameterPlotSpec}, mode::Val, @@ -356,6 +482,29 @@ function PlotBuilder.legend_label( return "$(UnitHandler.get_symbol(quantity))[$(series_key[1]),$(series_key[2])]" end +function PlotBuilder.legend_label( + ::Type{LineParameterPlotSpec}, + mode::Val, + recipe::PlotBuilder.PlotRecipe, + page_key::LineFamilyKey{K}, + view_key::LinePageKey, + series_key::Tuple{Int, Int}, +) where {K} + symbol = K === :series ? "Z" : "Y" + return "$symbol[$(series_key[1]),$(series_key[2])]" +end + +function PlotBuilder.series_group( + ::Type{LineParameterPlotSpec}, + mode::Val, + recipe::PlotBuilder.PlotRecipe, + page_key::LineFamilyKey{K}, + view_key::LinePageKey, + series_key::Tuple{Int, Int}, +) where {K} + return Symbol("$(K)_$(series_key[1])_$(series_key[2])") +end + function PlotBuilder.series_attributes( ::Type{LineParameterPlotSpec}, mode::Val, @@ -385,6 +534,43 @@ function PlotBuilder.default_title( return UnitHandler.get_label(quantity) end + +function PlotBuilder.default_title( + ::Type{LineParameterPlotSpec}, + mode::Val, + recipe::PlotBuilder.PlotRecipe, + page_key::LineFamilyKey{:series}, + ::Nothing, +) + return "Series impedance" +end + +function PlotBuilder.default_title( + ::Type{LineParameterPlotSpec}, + mode::Val, + recipe::PlotBuilder.PlotRecipe, + page_key::LineFamilyKey{:shunt}, + ::Nothing, +) + return "Shunt admittance" +end + +function PlotBuilder.default_title( + ::Type{LineParameterPlotSpec}, + mode::Val, + recipe::PlotBuilder.PlotRecipe, + page_key::LineFamilyKey, + view_key::LinePageKey, +) + return PlotBuilder.default_title( + LineParameterPlotSpec, + mode, + recipe, + view_key, + nothing, + ) +end + function PlotBuilder.view_key( ::Type{LineParameterPlotSpec}, mode::Val, @@ -395,6 +581,25 @@ function PlotBuilder.view_key( return (; component = _line_component(page_key)) end +function PlotBuilder.view_key( + ::Type{LineParameterPlotSpec}, + mode::Val, + recipe::PlotBuilder.PlotRecipe, + page_key::LineFamilyKey, + view_key::LinePageKey, +) + return (; component = _line_component(view_key)) +end + +function PlotBuilder.layout_spec( + ::Type{LineParameterPlotSpec}, + mode::Val, + recipe::PlotBuilder.PlotRecipe, + page_key::LineFamilyKey, +) + return :grid +end + function PlotBuilder.default_figsize( ::Type{LineParameterPlotSpec}, mode::Val, @@ -404,6 +609,19 @@ function PlotBuilder.default_figsize( return recipe.renderer.fig_size end +function _supports_log_values(samples) + found = false + samples === nothing && return false + for sample in samples + found = true + nominal = to_nominal(sample) + uncertainty = abs(uncertainty_value(sample)) + nominal isa Real && isfinite(nominal) && isfinite(uncertainty) && + nominal - uncertainty > 0 || return false + end + return found +end + function _supports_log(series, dim::Symbol) found = false for item in series @@ -411,8 +629,8 @@ function _supports_log(series, dim::Symbol) samples === nothing && continue for sample in samples found = true - nominal = Measurements.value(sample) - uncertainty = abs(Measurements.uncertainty(sample)) + nominal = to_nominal(sample) + uncertainty = abs(uncertainty_value(sample)) nominal isa Real && isfinite(nominal) && isfinite(uncertainty) && nominal - uncertainty > 0 || return false end @@ -429,7 +647,10 @@ function PlotBuilder.axis_scales( view_key, series::Vector{PlotBuilder.SeriesSpec} ) where {dim} - return _supports_log(series, dim) ? (:linear, :log10) : (:linear,) + supports_log = dim === :x ? + _supports_log_values(recipe.input.frequencies) : + _supports_log(series, dim) + return supports_log ? (:linear, :log10) : (:linear,) end function PlotBuilder.axis_exponent( @@ -446,6 +667,26 @@ function PlotBuilder.axis_exponent( ) end +function PlotBuilder.axis_exponent( + ::Type{LineParameterPlotSpec}, + mode::Val, + dim::Val, + recipe::PlotBuilder.PlotRecipe, + page_key::LineFamilyKey, + view_key::LinePageKey, + series::Vector{PlotBuilder.SeriesSpec}, +) + return PlotBuilder.axis_exponent( + LineParameterPlotSpec, + mode, + dim, + recipe, + view_key, + nothing, + series, + ) +end + function PlotBuilder.page_identity( ::Type{LineParameterPlotSpec}, mode::Val, @@ -455,8 +696,24 @@ function PlotBuilder.page_identity( return (; family = _line_parent(page_key), component = _line_component(page_key), - mode = recipe.input.mode, - coordinates = recipe.input.coord, + components = recipe.input.components, conductors = recipe.input.con ) end + + +function PlotBuilder.page_identity( + ::Type{LineParameterPlotSpec}, + mode::Val, + recipe::PlotBuilder.PlotRecipe, + page_key::LineFamilyKey, +) + return (; + family = _line_parent(page_key), + components = Tuple( + _line_component(component) + for component in _line_component_facets(mode, recipe, page_key) + ), + conductors = recipe.input.con, + ) +end diff --git a/src/engine/problemdefs.jl b/src/engine/problemdefs.jl index 3754fd18..43c97064 100644 --- a/src/engine/problemdefs.jl +++ b/src/engine/problemdefs.jl @@ -155,11 +155,46 @@ 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. +Define an explicit cable-constant calculation for one materialized cable +design. $(TYPEDFIELDS) """ -struct EMTFormulation <: AbstractFormulationSet +struct CableConstantsProblem{D<:CableDesign,S,R<:Real} <: ProblemDefinition + "Materialized cable design to analyze." + design::D + + "Optional center-to-center cable separation `\\[m\\]`." + separation::S + + "Earth resistivity used by the trifoil inductance estimate `\\[Ω·m\\]`." + earth_resistivity::R + + function CableConstantsProblem( + design::D; + separation::Union{Nothing,Real}=nothing, + earth_resistivity::Real=100.0, + ) where {D<:CableDesign} + separation === nothing || separation > zero(separation) || + throw(ArgumentError("separation must be positive")) + earth_resistivity > zero(earth_resistivity) || + throw(ArgumentError("earth_resistivity must be positive")) + return new{D,typeof(separation),typeof(earth_resistivity)}( + design, + separation, + earth_resistivity, + ) + end +end + +""" +$(TYPEDEF) + +Represents the electromagnetic transient (EMT) formulation for cable or line systems, containing all required impedance and admittance models for internal and earth effects. + +$(TYPEDFIELDS) +""" +struct EMTFormulation <: AbstractFormulation "Internal impedance formulation." internal_impedance::InternalImpedanceFormulation "Insulation impedance formulation." @@ -221,7 +256,7 @@ struct EMTFormulation <: AbstractFormulationSet end end -function FormulationSet(::Val{:EMT}; +function Formulation(::Val{:EMT}; internal_impedance::InternalImpedanceFormulation = InternalImpedance.ScaledBessel(), insulation_impedance::InsulationImpedanceFormulation = InsulationImpedance.Lossless(), earth_impedance::EarthImpedanceFormulation = EarthImpedance.Papadopoulos(), @@ -231,7 +266,7 @@ function FormulationSet(::Val{:EMT}; equivalent_earth::Union{AbstractEHEMFormulation, Nothing} = nothing, options = (;) ) - emt_opts = build_options(EMTOptions, options; strict = true) + emt_opts = formulation_options(options) return EMTFormulation(; internal_impedance, insulation_impedance, earth_impedance, insulation_admittance, earth_admittance, modal_transform, equivalent_earth, options = emt_opts diff --git a/src/engine/retired.jl b/src/engine/retired.jl index 5426f274..47f55ad0 100644 --- a/src/engine/retired.jl +++ b/src/engine/retired.jl @@ -1,13 +1,13 @@ -function FormulationSet(::Val{:FEM}; kwargs...) - retired_fem_sector("FormulationSet(:FEM)") +function Formulation(::Val{:FEM}; kwargs...) + retired_fem_sector("Formulation(:FEM)") end module FEM import ...Commons: retired_fem_sector -import ...Engine: FormulationSet +import ...Engine: Formulation -export Darwin, Electrodynamics, FormulationSet, MeshTransition, calc_domain_size, +export Darwin, Electrodynamics, Formulation, MeshTransition, calc_domain_size, preview_results Darwin(args...; kwargs...) = retired_fem_sector("Darwin") diff --git a/src/engine/solver.jl b/src/engine/solver.jl index 4ae91635..da417834 100644 --- a/src/engine/solver.jl +++ b/src/engine/solver.jl @@ -1,16 +1,17 @@ -function compute!( +function _compute_with_workspace( problem::LineParametersProblem{T}, - formulation::EMTFormulation + formulation::EMTFormulation, + execution::ComputeOptions, ) where {T <: REALSCALAR} - lvl = levelfrom(formulation.options.common.verbosity) - sink = isnothing(formulation.options.logfile) ? + lvl = levelfrom(execution.verbosity) + sink = isnothing(execution.logfile) ? ConsoleLogger(stderr, lvl) : TeeLogger(ConsoleLogger(stderr, lvl), - FileLogger(formulation.options.logfile, lvl)) + FileLogger(execution.logfile, lvl)) with_logger(TimestampLogger(sink)) do @info "Preallocating arrays" - ws = init_workspace(problem, formulation) + ws = init_workspace(problem, formulation, execution) nph, nfreq = ws.n_phases, ws.n_frequencies # --- full matrices are built per slice (no 3D alloc) ---------------------- @@ -86,7 +87,7 @@ function compute!( 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 ------------------------------------------------ + # --- per-frequency calculation -------------------------------------------- @info "Starting line parameters computation" for k in 1:nfreq compute_impedance_matrix!(Ztmp, ws, k, formulation) @@ -142,11 +143,51 @@ function compute!( lp = LineParameters(PhaseDomain, Zout, Yout, ws.freq) end + if execution.output_basis === :total + scale = problem.system.line_length + lp = LineParameters( + domain(lp), + lp.Z.values .* scale, + lp.Y.values .* scale, + lp.f; + basis=:total, + ) + end + @info "Line parameters computation completed successfully" return ws, lp end end +function compute!( + problem::LineParametersProblem, + formulation::EMTFormulation; + options=ComputeOptions(), +) + _, result = _compute_with_workspace( + problem, + formulation, + compute_options(options), + ) + return result +end + +function compute!( + problem::CableConstantsProblem, + ::EMTFormulation; + options=ComputeOptions(), +) + execution = compute_options(options) + execution.output_basis === :per_length || throw(ArgumentError( + "CableConstantsProblem has no system length and supports only output_basis=:per_length", + )) + return DataModel._compute_cable_constants( + problem.design; + S=problem.separation, + rho_e=problem.earth_resistivity, + ) +end + @inline function stash!(slice_or_nothing, k::Int, src::AbstractMatrix) slice_or_nothing === nothing && return nothing @views copyto!(slice_or_nothing[:, :, k], src) @@ -298,7 +339,7 @@ function compute_admittance_matrix!( # 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 + stash!(ws.Pg, k, Pext) # --- internal Maxwell coefficients (Ametani tail-sum) ------------------------- pinsfunctor = formulation.insulation_admittance diff --git a/src/engine/transforms/Transforms.jl b/src/engine/transforms/Transforms.jl index 835a9bdf..61c0ba4f 100644 --- a/src/engine/transforms/Transforms.jl +++ b/src/engine/transforms/Transforms.jl @@ -21,7 +21,6 @@ import ..Engine: ShuntAdmittance # -using Measurements using LinearAlgebra # using GenericLinearAlgebra using NLsolve diff --git a/src/engine/types.jl b/src/engine/types.jl index 2a1cabd5..04a7f580 100644 --- a/src/engine/types.jl +++ b/src/engine/types.jl @@ -1,31 +1,38 @@ """ $(TYPEDEF) -Abstract base type for all problem definitions in the [`LineCableModels.jl`](index.md) computation framework. +Abstract type for numerical problem definitions in +[`LineCableModels.jl`](index.md). """ abstract type ProblemDefinition end # Formulation abstract types -abstract type AbstractFormulationSet end +""" +$(TYPEDEF) + +Abstract type for formulations that select physical and numerical methods. +""" +abstract type AbstractFormulation end -abstract type AbstractImpedanceFormulation <: AbstractFormulationSet end +abstract type AbstractImpedanceFormulation <: AbstractFormulation end abstract type InternalImpedanceFormulation <: AbstractImpedanceFormulation end abstract type InsulationImpedanceFormulation <: AbstractImpedanceFormulation end abstract type EarthImpedanceFormulation <: AbstractImpedanceFormulation end -abstract type AbstractAdmittanceFormulation <: AbstractFormulationSet end +abstract type AbstractAdmittanceFormulation <: AbstractFormulation end abstract type InsulationAdmittanceFormulation <: AbstractAdmittanceFormulation end abstract type EarthAdmittanceFormulation <: AbstractAdmittanceFormulation end -abstract type AbstractTransformFormulation <: AbstractFormulationSet end +abstract type AbstractTransformFormulation <: AbstractFormulation end """ - FormulationSet(...) +$(TYPEDSIGNATURES) -Constructs a specific formulation object based on the provided keyword arguments. -The system will infer the correct formulation type. +Construct a formulation selected by `engine`, using the EMT formulation by +default. """ -FormulationSet(engine::Symbol; kwargs...) = FormulationSet(Val(engine); kwargs...) +Formulation(engine::Symbol; kwargs...) = Formulation(Val(engine); kwargs...) +Formulation(; kwargs...) = Formulation(Val(:EMT); kwargs...) """ $(TYPEDEF) @@ -36,6 +43,6 @@ Abstract type representing different equivalent homogeneous earth models (EHEM). - [`EnforceLayer`](@ref): Effective parameters defined according to a specific earth layer. """ -abstract type AbstractEHEMFormulation <: AbstractFormulationSet end +abstract type AbstractEHEMFormulation <: AbstractFormulation end abstract type AbstractFormulationOptions end diff --git a/src/engine/workspace.jl b/src/engine/workspace.jl index 8f01222a..1f3074d3 100644 --- a/src/engine/workspace.jl +++ b/src/engine/workspace.jl @@ -74,17 +74,17 @@ $(TYPEDFIELDS) "Number of cables in the system." n_cables::Int "Full component-based Z matrix (before bundling/reduction)." - Z::Array{Complex{T}, 3} + Z::Union{Nothing,Array{Complex{T},3}} "Full component-based P matrix (before bundling/reduction)." - P::Array{Complex{T}, 3} + P::Union{Nothing,Array{Complex{T},3}} "Full internal impedance matrix (before bundling/reduction)." - Zin::Array{Complex{T}, 3} + Zin::Union{Nothing,Array{Complex{T},3}} "Full internal potential coefficient matrix (before bundling/reduction)." - Pin::Array{Complex{T}, 3} + Pin::Union{Nothing,Array{Complex{T},3}} "Earth impedance matrix (n_cables x n_cables)." - Zg::Array{Complex{T}, 3} + Zg::Union{Nothing,Array{Complex{T},3}} "Earth potential coefficient matrix (n_cables x n_cables)." - Pg::Array{Complex{T}, 3} + Pg::Union{Nothing,Array{Complex{T},3}} end """ @@ -95,9 +95,10 @@ Initializes and populates the [`EMTWorkspace`](@ref) by normalizing a """ function init_workspace( problem::LineParametersProblem{T}, - formulation::EMTFormulation + formulation::EMTFormulation, + compute_options::ComputeOptions, ) where {T} - opts = formulation.options + opts = compute_options system = problem.system n_frequencies = length(problem.frequencies) diff --git a/src/importexport/ImportExport.jl b/src/importexport/ImportExport.jl index 6ab99cdf..9cbd5dd3 100644 --- a/src/importexport/ImportExport.jl +++ b/src/importexport/ImportExport.jl @@ -37,7 +37,6 @@ using ..DataModel: CablesLibrary, CableDesign, CableComponent, ConductorGroup, Insulator, LineCableSystem, NominalData import ..Engine: LineParameters, SeriesImpedance, ShuntAdmittance -using Measurements using EzXML using Dates using Printf # For ATP export diff --git a/src/importexport/deserialize.jl b/src/importexport/deserialize.jl index 9dbc4799..722f3689 100644 --- a/src/importexport/deserialize.jl +++ b/src/importexport/deserialize.jl @@ -107,20 +107,19 @@ identified by `__julia_type__`. Ensures plain dictionaries use Symbol keys. # Returns - The deserialized Julia value. """ +function _deserialize_extension end + 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 + applicable(_deserialize_extension, Val(:Measurement), value) || + throw(ArgumentError( + "deserializing Measurement values requires loading Measurements.jl", + )) + return _deserialize_extension(Val(:Measurement), value) elseif type_marker == "SpecialFloat" # Reconstruct Inf/NaN diff --git a/src/importexport/serialize.jl b/src/importexport/serialize.jl index 9c538ae9..00234dd6 100644 --- a/src/importexport/serialize.jl +++ b/src/importexport/serialize.jl @@ -131,15 +131,6 @@ 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 diff --git a/src/importexport/xlsx.jl b/src/importexport/xlsx.jl index 10ea0e15..5b79820b 100644 --- a/src/importexport/xlsx.jl +++ b/src/importexport/xlsx.jl @@ -7,9 +7,6 @@ stringify(x) = string(x) # fallback (rarely reached) stringify(::Missing) = "" stringify(x::Real) = @sprintf("%.12g", float(x)) -function stringify(x::Measurements.Measurement) - @sprintf("%.12g ± %.6g", Measurements.value(x), Measurements.uncertainty(x)) -end function df_to_strings(df::DataFrame) DataFrame((name => stringify.(df[!, name]) for name in names(df))...; copycols = false) diff --git a/src/materials/Materials.jl b/src/materials/Materials.jl index 0a7bf6e7..de8a7f09 100644 --- a/src/materials/Materials.jl +++ b/src/materials/Materials.jl @@ -24,7 +24,6 @@ export Material, MaterialsLibrary # Module-specific dependencies using ..Commons using ..Utils: resolve_T -using Measurements import ..Commons: add! import ..Utils: coerce_to_T diff --git a/src/materials/materialslibrary.jl b/src/materials/materialslibrary.jl index cdef5e27..6fe4f7b3 100644 --- a/src/materials/materialslibrary.jl +++ b/src/materials/materialslibrary.jl @@ -127,12 +127,13 @@ $(FUNCTIONNAME)(library, "copper", material) """ function add!( library::MaterialsLibrary, - name::AbstractString, + name::Union{AbstractString,Symbol}, material::Material ) - if haskey(library, name) - Base.error("Material $name already exists in the library.") + key = String(name) + if haskey(library, key) + Base.error("Material $key already exists in the library.") end - library[String(name)] = material + library[key] = material library end diff --git a/src/parametricbuilder/ParametricBuilder.jl b/src/parametricbuilder/ParametricBuilder.jl index 189342c2..055000ab 100644 --- a/src/parametricbuilder/ParametricBuilder.jl +++ b/src/parametricbuilder/ParametricBuilder.jl @@ -1,93 +1,33 @@ module ParametricBuilder -# Export public API +export Grid, AbsoluteError, DeterministicGrid, RelativeGrid, AbsoluteGrid +export AbstractGrid, AbstractUncertainGrid, UncertainValue +export AbstractSpec, Gridspace, Configuration, configurations, materialize +export has_uncertainty, configuration_manifest, nominal, standard_uncertainty +export @gridspace, @relax + +export Material, Conductor, Insulator, CableBuilder +export at, trifoil, hflat, vflat, Earth, SystemBuilder export make_stranded, make_screened -export Conductor, Insulator, Material, CableBuilder -export at, trifoil, Earth, SystemBuilder -export determinize -export ParametricSweep, cases, results, ncases -# Module-specific dependencies -using ..Commons -import ..Commons: add!, domain, basis, - Z, Y, R, X, L, G, B, C, - series_impedance, shunt_admittance, - resistance, reactance, inductance, - conductance, susceptance, capacitance, - frequencies, nconductors, nfrequencies -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 +using ..Commons: f₀, T₀, SIGNATURES, TYPEDSIGNATURES, TYPEDEF, TYPEDFIELDS +import ..Commons: add! +import ..Materials +import ..Materials: Material +import ..DataModel +import ..EarthProps +import ..Engine -# 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) - 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 - - 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 - -# 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("gridspace/grid.jl") +include("gridspace/gridspace.jl") +include("gridspace/macros.jl") include("materialspec.jl") include("cablebuilderspec.jl") +include("positionspec.jl") include("systembuilderspec.jl") -include("determinize.jl") -include("parametricsweep.jl") -include("base.jl") -# Submodule `WirePatterns` include("wirepatterns/WirePatterns.jl") -using .WirePatterns +using .WirePatterns: make_stranded, make_screened -end # module ParametricBuilder +end diff --git a/src/parametricbuilder/base.jl b/src/parametricbuilder/base.jl deleted file mode 100644 index 39ede211..00000000 --- a/src/parametricbuilder/base.jl +++ /dev/null @@ -1,323 +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 -function _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 -end - -# 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) -function _arg_choice_count(a) - (a isa Tuple && length(a) == 2) ? (_choice_count(a[1]) * _choice_count(a[2])) : - _choice_count(a) -end - -function _args_choice_count(args::Tuple) - isempty(args) ? 1 : prod(_arg_choice_count(a) for a in args) -end - -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) - -function _earth_choice_count(e::EarthSpec) - _choice_count(e.rho) * _choice_count(e.eps_r) * _choice_count(e.mu_r) * - _choice_count(e.t) -end - -# == 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) -function _fmt_freqs(f::AbstractVector) - length(f) ≤ 8 ? join(f, ", ") : - string(first(f), " … ", last(f), " (N=", length(f), ")") -end - -# 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 index 773b6192..d7b5cfe8 100644 --- a/src/parametricbuilder/cablebuilderspec.jl +++ b/src/parametricbuilder/cablebuilderspec.jl @@ -1,520 +1,547 @@ -# 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 - -function PartSpec(component::Symbol, part_type::Type, n_layers::Int; - dim, args = (), material::MaterialSpec) - PartSpec(component, part_type, n_layers, dim, args, material) -end - -""" -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 -function CableBuilder(id::AbstractString, parts::Vector{PartSpec}; nominal = nothing) - CableBuilderSpec(String(id), parts, nominal) -end - -# --- 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))") +struct PartBuilder{Role,Part,Mode,C,D,A<:Tuple,M} + component::C + layers::Int + dimension::D + args::A + material::M + + function PartBuilder( + ::Val{Role}, + ::Val{Part}, + ::Val{Mode}, + component, + layers, + dimension, + args::A, + material, + ) where {Role,Part,Mode,A<:Tuple} + component isa Symbol || + throw(ArgumentError("part component must be a Symbol")) + layers isa Integer && layers > 0 || + throw(ArgumentError("part layers must be a positive integer")) + Mode in (:radius, :solid) && layers != 1 && throw(ArgumentError( + "absolute radius is valid only for a single layer; use thickness for repeated stacking", + )) + dimension isa Real && dimension > zero(dimension) || + throw(ArgumentError("part radius/thickness must be a positive real number")) + material isa Materials.Material || + throw(ArgumentError("part material must resolve to Materials.Material")) + return new{ + Role, + Part, + Mode, + typeof(component), + typeof(dimension), + A, + typeof(material), + }(component, Int(layers), dimension, args, material) 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 +_part_role(::PartBuilder{Role}) where {Role} = Role +_part_type(::PartBuilder{Role,Part}) where {Role,Part} = Part +_part_mode(::PartBuilder{Role,Part,Mode}) where {Role,Part,Mode} = Mode +_valof(::Val{Value}) where {Value} = Value + +function _simple_part_builder( + role::Val, + part::Val, + mode::Val, + component, + layers, + dimension, + material, +) + return PartBuilder(role, part, mode, component, layers, dimension, (), material) 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) +function _wire_part_builder( + role::Val, + part::Val, + mode::Val, + component, + layers, + wire_radius, + num_wires, + lay_ratio, + material, +) + num_wires isa Integer && num_wires > 0 || + throw(ArgumentError("num_wires must be a positive integer")) + lay_ratio isa Real && lay_ratio >= zero(lay_ratio) || + throw(ArgumentError("lay_ratio must be a nonnegative real number")) + return PartBuilder( + role, + part, + mode, + component, + layers, + wire_radius, + (Int(num_wires), lay_ratio), + material, + ) 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 +function _strip_part_builder( + role::Val, + part::Val, + mode::Val, + component, + layers, + dimension, + width, + lay_ratio, + material, +) + width isa Real && width > zero(width) || + throw(ArgumentError("strip width must be a positive real number")) + lay_ratio isa Real && lay_ratio >= zero(lay_ratio) || + throw(ArgumentError("lay_ratio must be a nonnegative real number")) + return PartBuilder( + role, + part, + mode, + component, + layers, + dimension, + (width, lay_ratio), + material, + ) end -struct ComponentTrace - name::String - choices::Vector{PartChoice} +function _part_space(target, axes::Tuple, names::Tuple; combine::Symbol=:product) + return Gridspace{PartBuilder}( + target, + map(_gridspace_axis, axes), + names; + combine, + ) end -struct DesignTrace - cable_id::String - components::Vector{ComponentTrace} +function _radial_declaration(radius, thickness) + xor(radius === nothing, thickness === nothing) || throw(ArgumentError( + "provide exactly one of radius or thickness", + )) + radius !== nothing && return Val(:radius), radius + return Val(:thickness), thickness 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 +module Conductor -@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 +using ..ParametricBuilder: + Gridspace, PartBuilder, _part_space, _radial_declaration, + _simple_part_builder, _wire_part_builder, _strip_part_builder, _valof +import ...DataModel -# ----- 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) +function Solid( + component::Symbol; + radius, + material, + combine::Symbol=:product, +) + return _part_space( + _simple_part_builder, + ( + Val(:conductor), + Val(DataModel.Tubular), + Val(:solid), + component, + 1, + radius, + material, + ), + (:role, :part, :mode, :component, :layers, :radius, :material); + combine, + ) 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 +function Tubular( + component::Symbol; + layers=1, + radius=nothing, + thickness=nothing, + material, + combine::Symbol=:product, +) + mode, dimension = _radial_declaration(radius, thickness) + return _part_space( + _simple_part_builder, + ( + Val(:conductor), + Val(DataModel.Tubular), + mode, + component, + layers, + dimension, + material, + ), + (:role, :part, :mode, :component, :layers, _valof(mode), :material); + combine, + ) end -function _add_conductor!( - cg::DataModel.ConductorGroup, - T::Type, - dim_val, - args_pos::Tuple, - mat; - layer::Int +function Wires( + component::Symbol; + layers=1, + wire_radius, + num_wires, + lay_ratio=11.0, + material, + combine::Symbol=:product, ) - 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 + return _part_space( + _wire_part_builder, + ( + Val(:conductor), + Val(DataModel.CircStrands), + Val(:wire_radius), + component, + layers, + wire_radius, + num_wires, + lay_ratio, + material, + ), + ( + :role, + :part, + :mode, + :component, + :layers, + :wire_radius, + :num_wires, + :lay_ratio, + :material, + ); + combine, + ) 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) +function Strip( + component::Symbol; + layers=1, + radius=nothing, + thickness=nothing, + width, + lay_ratio=0.0, + material, + combine::Symbol=:product, +) + mode, dimension = _radial_declaration(radius, thickness) + return _part_space( + _strip_part_builder, + ( + Val(:conductor), + Val(DataModel.Strip), + mode, + component, + layers, + dimension, + width, + lay_ratio, + material, + ), + ( + :role, + :part, + :mode, + :component, + :layers, + _valof(mode), + :width, + :lay_ratio, + :material, + ); + combine, + ) end -function _add_insulator!( - ig::DataModel.InsulatorGroup, - T::Type, - dim_val, - args_pos::Tuple, - mat +function Stranded( + component::Symbol; + layers::Int, + wire_radius, + num_wires::Int=6, + lay_ratio=11.0, + material, + combine::Symbol=:product, ) - add!(ig, T, DataModel.Thickness(dim_val), args_pos..., mat) + layers >= 1 || throw(ArgumentError("layers must be at least one")) + central = Wires( + component; + layers=1, + wire_radius, + num_wires=1, + lay_ratio=0.0, + material, + combine, + ) + layers == 1 && return (central,) + rings = Wires( + component; + layers=layers - 1, + wire_radius, + num_wires, + lay_ratio, + material, + combine, + ) + return (central, rings) 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) +end - 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)) +module Insulator - push!(rest_ins_spaces, (pi, m2, d2, a2)) - end +using ..ParametricBuilder: + _part_space, _radial_declaration, _simple_part_builder, _valof +import ...DataModel + +function _annular( + part, + component::Symbol; + layers=1, + radius=nothing, + thickness=nothing, + material, + combine::Symbol=:product, +) + mode, dimension = _radial_declaration(radius, thickness) + return _part_space( + _simple_part_builder, + ( + Val(:insulator), + Val(part), + mode, + component, + layers, + dimension, + material, + ), + (:role, :part, :mode, :component, :layers, _valof(mode), :material); + combine, + ) +end - # ---------------- selection stacks (resolved tuples) ------------- - chosen_c = Vector{NTuple{4, Any}}() - chosen_i = Vector{NTuple{4, Any}}() +Tubular(component::Symbol; kwargs...) = + _annular(DataModel.Insulator, component; kwargs...) +Semicon(component::Symbol; kwargs...) = + _annular(DataModel.Semicon, component; kwargs...) - # ---------------- 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 +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 +function _resolved_outer_radius(builder::PartBuilder, r_in) + mode = _part_mode(builder) + mode === :radius && return builder.dimension + mode === :solid && return builder.dimension + mode === :thickness && return r_in + builder.dimension + throw(ArgumentError("part mode :$mode does not define an annular outer radius")) +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 - ) +function _materialize_part(builder::PartBuilder, r_in, layer::Int) + role = _part_role(builder) + part = _part_type(builder) + mode = _part_mode(builder) + + if part === DataModel.CircStrands + num_wires, lay_ratio = builder.args + layer_count = num_wires == 1 ? 1 : layer * num_wires + return DataModel.CircStrands( + r_in, + builder.dimension, + layer_count, + lay_ratio, + builder.material, ) - # 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) + elseif part === DataModel.Strip + width, lay_ratio = builder.args + r_ex = _resolved_outer_radius(builder, r_in) + return DataModel.Strip( + r_in, + r_ex, + width, + lay_ratio, + builder.material, ) - - # push!(variants, (DataModel.CableComponent(String(ps[1].component), cg, ig), ig)) + elseif part === DataModel.Tubular + mode === :solid && !iszero(r_in) && throw(ArgumentError( + "solid conductors must start at radius zero", + )) + r_ex = _resolved_outer_radius(builder, r_in) + return DataModel.Tubular(r_in, r_ex, builder.material) + elseif part === DataModel.Insulator + r_ex = _resolved_outer_radius(builder, r_in) + return DataModel.Insulator(r_in, r_ex, builder.material) + elseif part === DataModel.Semicon + r_ex = _resolved_outer_radius(builder, r_in) + return DataModel.Semicon(r_in, r_ex, builder.material) end + throw(ArgumentError("unsupported materialized part type $part for role :$role")) +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 +function _component_names(builders::Tuple) + names = Symbol[] + for builder in builders + builder.component in names || push!(names, builder.component) end + return names +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) +function _build_component(component::Symbol, builders::Tuple, base_radius) + conductor_group = nothing + current_radius = base_radius + for builder in builders + builder.component === component && _part_role(builder) === :conductor || + continue + for layer in 1:builder.layers + part = _materialize_part(builder, current_radius, layer) + conductor_group = conductor_group === nothing ? + DataModel.ConductorGroup(part) : + add!(conductor_group, part) + current_radius = conductor_group.r_ex 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) + conductor_group === nothing && + throw(ArgumentError("component :$component has no conductor layers")) + + insulator_group = nothing + current_radius = conductor_group.r_ex + for builder in builders + builder.component === component && _part_role(builder) === :insulator || + continue + for layer in 1:builder.layers + part = _materialize_part(builder, current_radius, layer) + insulator_group = insulator_group === nothing ? + DataModel.InsulatorGroup(part) : + add!(insulator_group, part) + current_radius = insulator_group.r_ex end end + insulator_group === nothing && + throw(ArgumentError("component :$component has no insulator layers")) + return DataModel.CableComponent( + String(component), + conductor_group, + insulator_group, + ) +end - return variants +struct DesignMaterializer{N} + nominal::N 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) +_nominal_data(::Nothing) = nothing +_nominal_data(nominal::DataModel.NominalData) = nominal +_nominal_data(nominal::NamedTuple) = DataModel.NominalData(; nominal...) +_nominal_data(nominal) = throw(ArgumentError( + "nominal data must be a NamedTuple, NominalData, or nothing; got $(typeof(nominal))", +)) + +function (materializer::DesignMaterializer)(identifier, builders...) + isempty(builders) && throw(ArgumentError("CableBuilder requires at least one part")) + parts = tuple(builders...) + names = _component_names(parts) + isempty(names) && throw(ArgumentError("CableBuilder has no components")) + + first_component = _build_component(first(names), parts, 0.0) + nominal = _nominal_data(materializer.nominal) + design = nominal === nothing ? + DataModel.CableDesign(String(identifier), first_component) : + DataModel.CableDesign( + String(identifier), + first_component; + nominal_data=nominal, + ) + for name in Iterators.drop(names, 1) + base_radius = design.components[end].insulator_group.r_ex + add!(design, _build_component(name, parts, base_radius)) end + return design +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 +struct CableDesignSpec{P<:Tuple,N,C} <: AbstractSpec{DataModel.CableDesign} + identifier::String + parts::P + nominal::N + combine::C +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 +_flatten_parts(part::Gridspace{PartBuilder}) = (part,) +function _flatten_parts(parts::Union{Tuple,AbstractVector}) + flattened = () + for part in parts + flattened = (flattened..., _flatten_parts(part)...) end + return flattened end +_flatten_parts(value) = throw(ArgumentError( + "CableBuilder expects part Gridspaces; got $(typeof(value))", +)) """ - 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 +$(TYPEDSIGNATURES) - 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 +Describe a cable design using the radial parts declared by `Conductor` and +`Insulator`. Iterating the returned specification materializes +[`LineCableModels.DataModel.CableDesign`](@ref) objects through the existing +cable-part, group, and component calculations. - dfs(1) - end -end +# Arguments -module Conductor +- `identifier`: Name assigned to each materialized cable design. +- `parts`: One or more part declarations, supplied in radial order. Nested + tuples and vectors of declarations are flattened without changing that + order. -using ..ParametricBuilder: PartSpec, _spec -using ...DataModel: DataModel +# Keywords -# wire: args are (n, lay) -function 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) -end +- `nominal=nothing`: Optional reference data. A named tuple may contain the + fields `designation_code`, `U0`, `U`, `conductor_cross_section`, + `screen_cross_section`, `armor_cross_section`, `resistance`, `capacitance`, + and `inductance`. Voltages are expressed in \\[kV\\], cross-sectional areas in + \\[mm²\\], resistance in \\[Ω/km\\], capacitance in \\[μF/km\\], and inductance in + \\[mH/km\\]. +- `combine=:product`: Local composition rule for direct varying inputs. Use + `:zip` to pair compatible axes. -# tube: no extra args -function Tubular(component::Symbol; layers::Int, t, m) - PartSpec(component, DataModel.Tubular, layers; - dim = _spec(t), args = (), material = m) -end +# Returns -# strip: args are (width, lay) -function 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) -end +- A cable-design specification. Ordinary iteration enumerates deterministic + configurations; `rand` draws a realization from its uncertainty-bearing + configuration. -# solid: inherits inner radius = 0.0, builds from diameter -function Solid(component::Symbol; d, m) - PartSpec(component, DataModel.Tubular, 1; - dim = _spec(d), args = (), material = m) -end +# Errors -# 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) +- Throws `ArgumentError` when no parts are supplied, a value is not a cable + part declaration, or `combine` is neither `:product` nor `:zip`. +- Materialization reports invalid radial geometry and other physical input + errors from the materialized cable model. +""" +function CableBuilder( + identifier::AbstractString, + parts...; + nominal=nothing, + combine::Symbol=:product, +) + combine in (:product, :zip) || + throw(ArgumentError("combine must be :product or :zip")) + flattened = _flatten_parts(parts) + isempty(flattened) && + throw(ArgumentError("CableBuilder requires at least one part")) + return CableDesignSpec( + String(identifier), + flattened, + nominal, + Val(combine), ) - - # 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 - -function Tubular(component::Symbol; layers::Int, t, m) - PartSpec(component, DataModel.Insulator, layers; - dim = _spec(t), args = (), material = m) end -function Semicon(component::Symbol; layers::Int, t, m) - PartSpec(component, DataModel.Semicon, layers; - dim = _spec(t), args = (), material = m) -end +function gridspace(spec::CableDesignSpec) + axes = (_gridspace_axis(spec.identifier), map(_gridspace_axis, spec.parts)...) + names = (:identifier, ntuple(index -> Symbol(:part_, index), length(spec.parts))...) + return Gridspace{DataModel.CableDesign}( + DesignMaterializer(spec.nominal), + axes, + names; + combine=_valof(spec.combine), + ) end diff --git a/src/parametricbuilder/determinize.jl b/src/parametricbuilder/determinize.jl deleted file mode 100644 index 9c0a1b33..00000000 --- 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 diff --git a/src/parametricbuilder/gridspace/grid.jl b/src/parametricbuilder/gridspace/grid.jl new file mode 100644 index 00000000..4e01a40e --- /dev/null +++ b/src/parametricbuilder/gridspace/grid.jl @@ -0,0 +1,297 @@ +import Base: eltype, extrema, getindex, iterate, length, rand, size +import Random + +""" +$(TYPEDEF) + +Supertype for the deterministic and uncertainty-bearing parameter axes created +by [`Grid`](@ref). +""" +abstract type AbstractGrid end + +""" +$(TYPEDEF) + +Supertype for parameter axes whose configurations retain an uncertainty +descriptor. +""" +abstract type AbstractUncertainGrid <: AbstractGrid end + +abstract type AbstractUncertaintyStyle end +struct RelativeUncertainty{T<:Real} <: AbstractUncertaintyStyle + percent::T +end +struct AbsoluteUncertainty <: AbstractUncertaintyStyle end + +""" +$(TYPEDEF) + +A dependency-free uncertainty descriptor. `sigma` is always an absolute +standard deviation. `style` retains whether it originated from a relative or +absolute [`Grid`](@ref) declaration. + +$(TYPEDFIELDS) +""" +struct UncertainValue{T,S<:AbstractUncertaintyStyle,E} + "Nominal parameter value." + nominal::T + + "Absolute standard uncertainty in the same unit as `nominal`." + sigma::E + + "Origin of the uncertainty declaration." + style::S + + function UncertainValue(nominal::T, sigma::E, style::S) where { + T,S<:AbstractUncertaintyStyle,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,S,E}(nominal, sigma, style) + 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. +""" +standard_uncertainty(value::UncertainValue) = value.sigma +uncertainty_style(value::UncertainValue) = value.style + +# A reused Grid instance carries the same automatic key. Separately-created +# equal Grids deliberately do not couple. Named keys provide explicit coupling. +struct AutomaticGridKey + token::Base.RefValue{Nothing} +end +Base.:(==)(left::AutomaticGridKey, right::AutomaticGridKey) = left.token === right.token +Base.isequal(left::AutomaticGridKey, right::AutomaticGridKey) = left == right +Base.hash(key::AutomaticGridKey, seed::UInt) = hash(objectid(key.token), seed) + +struct NamedGridKey{K} + value::K +end + +_grid_key(::Nothing) = AutomaticGridKey(Ref(nothing)) +_grid_key(key) = NamedGridKey(key) + +""" +$(TYPEDEF) + +Represent one axis of deterministic parameter values. + +$(TYPEDFIELDS) +""" +struct DeterministicGrid{V<:Tuple,K} <: AbstractGrid + "Values admitted by the axis." + vals::V + + "Identity used to couple a selection with another axis." + key::K +end + +""" +$(TYPEDEF) + +Represent nominal parameter values and relative standard uncertainties. + +$(TYPEDFIELDS) +""" +struct RelativeGrid{V<:Tuple,P<:Tuple,K} <: AbstractUncertainGrid + "Nominal values admitted by the axis." + vals::V + + "Relative standard uncertainties expressed as percentages." + rel_err::P + + "Identity used to couple a selection with another axis." + key::K + + function RelativeGrid(vals::V, rel_err::P, key::K) where {V<:Tuple,P<:Tuple,K} + _validate_uncertainty(vals, rel_err, "relative") + return new{V,P,K}(vals, rel_err, key) + end +end + +""" +$(TYPEDEF) + +Represent nominal parameter values and absolute standard uncertainties. + +$(TYPEDFIELDS) +""" +struct AbsoluteGrid{V<:Tuple,P<:Tuple,K} <: AbstractUncertainGrid + "Nominal values admitted by the axis." + vals::V + + "Absolute standard uncertainties in the same unit as `vals`." + abs_err::P + + "Identity used to couple a selection with another axis." + key::K + + function AbsoluteGrid(vals::V, abs_err::P, key::K) where {V<:Tuple,P<:Tuple,K} + _validate_uncertainty(vals, abs_err, "absolute") + return new{V,P,K}(vals, abs_err, key) + 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_errors(vals, "absolute") + return new{T}(vals) + end +end + +_grid_values(value::Tuple) = value +_grid_values(value::AbstractArray) = Tuple(value) +_grid_values(value) = (value,) + +function _validate_errors(errors::Tuple, kind::AbstractString) + 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 nothing +end + +function _validate_uncertainty(values::Tuple, errors::Tuple, kind::AbstractString) + isempty(values) && throw(ArgumentError("$kind uncertainty cannot have an empty nominal axis")) + _validate_errors(errors, kind) + 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 nothing +end + +AbsoluteError(value) = AbsoluteError(_grid_values(value)) + +""" +$(TYPEDSIGNATURES) + +Create the sole representation of parameter variation. Collections are axes +only when passed explicitly to `Grid`; constructors otherwise treat them as +ordinary atomic values. Reusing one Grid instance couples its selections. +`key` couples separately-created Grids deliberately. +""" +Grid(grid::AbstractGrid; key=nothing) = key === nothing ? grid : + throw(ArgumentError("cannot replace the coupling key of an existing Grid")) +Grid(value; key=nothing) = DeterministicGrid(_grid_values(value), _grid_key(key)) +Grid(value, error::AbsoluteError; key=nothing) = + AbsoluteGrid(_grid_values(value), error.vals, _grid_key(key)) +Grid(value, relative_error; key=nothing) = + RelativeGrid(_grid_values(value), _grid_values(relative_error), _grid_key(key)) + +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) +Base.IteratorSize(::Type{<:DeterministicGrid}) = Base.HasShape{1}() + +function iterate(grid::RelativeGrid, state...) + item = iterate(Iterators.product(grid.vals, grid.rel_err), state...) + item === nothing && return nothing + (value, percent), next_state = item + sigma = abs(value) * percent / 100 + return UncertainValue(value, sigma, RelativeUncertainty(percent)), 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, AbsoluteUncertainty()), 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),) +Base.IteratorSize(::Type{<:AbstractUncertainGrid}) = Base.HasShape{1}() + +function getindex(grid::AbstractUncertainGrid, index::Integer) + 1 <= index <= length(grid) || throw(BoundsError(grid, index)) + return first(Iterators.drop(grid, index - 1)) +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 + +function _sample_uncertainty( + rng::Random.AbstractRNG, + value::UncertainValue{<:Real,<:AbstractUncertaintyStyle,<: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 + +_sample_uncertainty(rng::Random.AbstractRNG, value::UncertainValue{<:Real}, sampler::Function) = + sampler(rng, value.nominal, value.sigma) + +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 + +rand(value::UncertainValue; kwargs...) = rand(Random.default_rng(), value; kwargs...) + +function rand(rng::Random.AbstractRNG, grid::AbstractGrid; distribution=:normal) + length(grid) == 1 || + throw(ArgumentError("rand(Grid) requires one configuration; select a configuration before sampling")) + value = first(grid) + return value isa UncertainValue ? rand(rng, value; distribution) : value +end + +rand(grid::AbstractGrid; kwargs...) = rand(Random.default_rng(), grid; kwargs...) diff --git a/src/parametricbuilder/gridspace/gridspace.jl b/src/parametricbuilder/gridspace/gridspace.jl new file mode 100644 index 00000000..ac38f895 --- /dev/null +++ b/src/parametricbuilder/gridspace/gridspace.jl @@ -0,0 +1,403 @@ +""" +$(TYPEDEF) + +Typed blueprint interface for staged construction of a concrete `Target`. +""" +abstract type AbstractSpec{Target} end + +target_type(::Type{<:AbstractSpec{Target}}) where {Target} = Target +target_type(spec::AbstractSpec) = target_type(typeof(spec)) + +struct ConstantAxis{T} + value::T +end + +Base.iterate(axis::ConstantAxis) = (axis.value, nothing) +Base.iterate(::ConstantAxis, ::Nothing) = nothing +Base.length(::ConstantAxis) = 1 +Base.getindex(axis::ConstantAxis, index::Integer) = + index == 1 ? axis.value : throw(BoundsError(axis, index)) + +struct GridBinding{K} + key::K + index::Int + cardinality::Int +end + +""" +$(TYPEDEF) + +Represent one resolved deterministic choice while retaining any uncertainty +descriptors for later realization. + +$(TYPEDFIELDS) +""" +struct Configuration{Target,F,V<:Tuple,N<:Tuple,B<:Tuple} + "Callable that constructs `Target` from the resolved axis values." + target::F + + "Resolved values supplied to `target`." + values::V + + "Parameter names corresponding to `values`." + names::N + + "Selections retained for coupled axes." + bindings::B +end + +target_type(::Type{<:Configuration{Target}}) where {Target} = Target +target_type(configuration::Configuration) = target_type(typeof(configuration)) + +""" +$(TYPEDEF) + +A lazy space of complete `Target` configurations. `combine` is local to this +node and may be `:product` or `:zip`. + +$(TYPEDFIELDS) +""" +struct Gridspace{Target,F,A<:Tuple,N<:Tuple,C} <: AbstractSpec{Target} + "Callable that constructs `Target` from one selection of the direct axes." + target::F + + "Direct parameter or object-valued axes." + axes::A + + "Parameter names corresponding to `axes`." + names::N + + "Local composition rule, represented by `Val{:product}` or `Val{:zip}`." + combine::C +end + +""" +$(TYPEDSIGNATURES) + +Construct a lazy space of complete `Target` configurations from `axes`. + +`combine=:product` forms the Cartesian product of the direct axes. +`combine=:zip` pairs axes of equal cardinality while treating singleton axes +as constants. A nested [`Gridspace`](@ref) applies its own composition rule +before entering its parent as one object-valued axis. +""" +function Gridspace{Target}( + target::F, + axes::A, + names::N=(); + combine::Symbol=:product, +) where {Target,F,A<:Tuple,N<:Tuple} + combine in (:product, :zip) || + throw(ArgumentError("combine must be :product or :zip; got :$combine")) + isempty(names) || length(names) == length(axes) || + throw(DimensionMismatch("Gridspace names and axes must have equal lengths")) + normalized_axes = map(_gridspace_axis, axes) + normalized_names = isempty(names) ? + ntuple(index -> Symbol(:arg, index), length(axes)) : names + return Gridspace{ + Target, + F, + typeof(normalized_axes), + typeof(normalized_names), + Val{combine}, + }( + target, + normalized_axes, + normalized_names, + Val(combine), + ) +end + +Gridspace{Target}(axes::Tuple; combine::Symbol=:product) where {Target} = + Gridspace{Target}(Target, axes; combine) + +Grid(space::Gridspace; key=nothing) = key === nothing ? space : + throw(ArgumentError("Gridspace coupling is defined by its child Grids")) + +_gridspace_axis(value::ConstantAxis) = value +_gridspace_axis(value::Union{AbstractGrid,AbstractSpec}) = value +_gridspace_axis(value) = ConstantAxis(value) + +struct AxisSelection{V,K} + value::V + key::K + index::Int + cardinality::Int +end + +"""A resolved axis value retaining its coupling identity for realization.""" +struct ResolvedGridValue{V,K} + value::V + key::K +end + +_axis_cases(axis::ConstantAxis) = axis + +function _axis_cases(grid::AbstractGrid) + return ( + AxisSelection(value, grid.key, index, length(grid)) + for (index, value) in enumerate(grid) + ) +end + +_axis_cases(spec::AbstractSpec) = configurations(spec) + +_axis_value(selection::AxisSelection) = + ResolvedGridValue(selection.value, selection.key) +_axis_value(configuration::Configuration) = configuration +_axis_value(value) = value + +_axis_bindings(selection::AxisSelection) = + (GridBinding(selection.key, selection.index, selection.cardinality),) +_axis_bindings(configuration::Configuration) = configuration.bindings +_axis_bindings(::Any) = () + +_same_grid_key(left, right) = left == right + +function _compatible_bindings(items::Tuple) + bindings = tuple((_axis_bindings(item) for item in items)...) + flat = tuple(Iterators.flatten(bindings)...) + for i in eachindex(flat), j in (i + 1):length(flat) + if _same_grid_key(flat[i].key, flat[j].key) + flat[i].cardinality == flat[j].cardinality || throw(DimensionMismatch( + "coupled Grids have incompatible cardinalities $(flat[i].cardinality) and $(flat[j].cardinality)", + )) + flat[i].index != flat[j].index && return false + end + end + return true +end + +function _merged_bindings(items::Tuple) + groups = tuple((_axis_bindings(item) for item in items)...) + all_bindings = tuple(Iterators.flatten(groups)...) + return tuple(( + binding for (index, binding) in pairs(all_bindings) + if all( + previous -> !_same_grid_key(previous.key, binding.key), + all_bindings[1:(index - 1)], + ) + )...) +end + +function _product_combinations(axes::Tuple) + isempty(axes) && return ((),) + iterators = map(_axis_cases, axes) + return Iterators.product(iterators...) +end + +function _nth(iterator, index::Int) + item = iterate(Iterators.drop(iterator, index - 1)) + item === nothing && throw(BoundsError(iterator, index)) + return item[1] +end + +function _zip_combinations(axes::Tuple, names::Tuple) + isempty(axes) && return ((),) + iterators = map(_axis_cases, axes) + counts = map(length, axes) + target_count = maximum(counts) + for index in eachindex(counts) + counts[index] in (1, target_count) || throw(DimensionMismatch( + "zip axis $(names[index]) has cardinality $(counts[index]); expected 1 or $target_count", + )) + end + return ( + map( + (iterator, count) -> _nth(iterator, count == 1 ? 1 : row), + iterators, + counts, + ) + for row in 1:target_count + ) +end + +_combinations(space::Gridspace{<:Any,<:Any,<:Any,<:Any,Val{:product}}) = + _product_combinations(space.axes) +_combinations(space::Gridspace{<:Any,<:Any,<:Any,<:Any,Val{:zip}}) = + _zip_combinations(space.axes, space.names) + +""" +$(TYPEDSIGNATURES) + +Return a lazy iterator over the resolved configurations admitted by a +[`Gridspace`](@ref). +""" +function configurations(space::Gridspace{Target}) where {Target} + compatible = Iterators.filter(_compatible_bindings, _combinations(space)) + return ( + Configuration{Target}( + space.target, + map(_axis_value, items), + space.names, + _merged_bindings(items), + ) + for items in compatible + ) +end + +# Partial parameter constructor used above while retaining fully concrete field +# parameters in the actual value. +function Configuration{Target}(target::F, values::V, names::N, bindings::B) where { + Target,F,V<:Tuple,N<:Tuple,B<:Tuple +} + return Configuration{Target,F,V,N,B}(target, values, names, bindings) +end + +""" +$(TYPEDSIGNATURES) + +Return the [`Gridspace`](@ref) backing a staged specification. Concrete +specifications implement this method. +""" +function gridspace(spec::AbstractSpec) + throw(MethodError(gridspace, (spec,))) +end + +configurations(spec::AbstractSpec) = configurations(gridspace(spec)) + +_direct_value(value::UncertainValue) = throw(ArgumentError( + "direct materialization of uncertain configurations requires Measurements.jl; load it before using FullParametric", +)) +_direct_value(configuration::Configuration) = materialize(configuration) +_direct_value(value) = value + +function _resolved_direct(value::ResolvedGridValue, cache::Dict) + return get!(cache, value.key) do + _direct_value(value.value) + end +end +_resolved_direct(configuration::Configuration, cache::Dict) = + _materialize(configuration, cache) +_resolved_direct(value, ::Dict) = _direct_value(value) + +function _materialize(configuration::Configuration, cache::Dict) + values = map(value -> _resolved_direct(value, cache), configuration.values) + return configuration.target(values...) +end + +""" +$(TYPEDSIGNATURES) + +Materialize a resolved configuration through its target constructor. +""" +function materialize(configuration::Configuration) + return _materialize(configuration, Dict{Any,Any}()) +end + +function _random_value( + rng::Random.AbstractRNG, + value::ResolvedGridValue, + distribution, + cache::Dict, +) + return get!(cache, value.key) do + value.value isa UncertainValue ? + rand(rng, value.value; distribution) : value.value + end +end +_random_value( + rng::Random.AbstractRNG, + configuration::Configuration, + distribution, + cache::Dict, +) = + _random_materialize(rng, configuration, distribution, cache) +_random_value(::Random.AbstractRNG, value, distribution, ::Dict) = value + +function _random_materialize(rng, configuration, distribution, cache) + values = map( + value -> _random_value(rng, value, distribution, cache), + configuration.values, + ) + return configuration.target(values...) +end + +function Base.rand( + rng::Random.AbstractRNG, + configuration::Configuration; + distribution=:normal, +) + return _random_materialize( + rng, + configuration, + distribution, + Dict{Any,Any}(), + ) +end + +Base.rand(configuration::Configuration; kwargs...) = + rand(Random.default_rng(), configuration; kwargs...) + +function Base.rand(rng::Random.AbstractRNG, spec::AbstractSpec; distribution=:normal) + iterator = configurations(spec) + first_item = iterate(iterator) + first_item === nothing && throw(ArgumentError("cannot sample an empty Gridspace")) + configuration, state = first_item + iterate(iterator, state) === nothing || throw(ArgumentError( + "rand(Gridspace) requires exactly one outer configuration; enumerate configurations and sample one explicitly", + )) + return rand(rng, configuration; distribution) +end + +Base.rand(spec::AbstractSpec; kwargs...) = + rand(Random.default_rng(), spec; kwargs...) + +function Base.iterate(spec::AbstractSpec) + iterator = configurations(spec) + item = iterate(iterator) + item === nothing && return nothing + configuration, state = item + return materialize(configuration), state +end + +function Base.iterate(spec::AbstractSpec, state) + iterator = configurations(spec) + item = iterate(iterator, state) + item === nothing && return nothing + configuration, next_state = item + return materialize(configuration), next_state +end + +Base.IteratorSize(::Type{<:AbstractSpec}) = Base.HasLength() +Base.IteratorEltype(::Type{<:AbstractSpec}) = Base.HasEltype() +Base.eltype(::Type{<:AbstractSpec{Target}}) where {Target} = Target +Base.length(spec::AbstractSpec) = count(_ -> true, configurations(spec)) +Base.size(spec::AbstractSpec) = (length(spec),) +Base.getindex(spec::AbstractSpec, index::Integer) = + first(Iterators.drop(spec, index - 1)) + +""" +$(TYPEDSIGNATURES) + +Return whether a value, configuration, or specification contains an +uncertainty descriptor. +""" +has_uncertainty(value::UncertainValue) = true +has_uncertainty(value::ResolvedGridValue) = has_uncertainty(value.value) +has_uncertainty(configuration::Configuration) = + any(has_uncertainty, configuration.values) +has_uncertainty(::Any) = false +has_uncertainty(spec::AbstractSpec) = + any(has_uncertainty, configurations(spec)) + +_manifest_value(value::UncertainValue) = ( + nominal=value.nominal, + sigma=value.sigma, + style=value.style isa RelativeUncertainty ? :relative : :absolute, +) +_manifest_value(value::ResolvedGridValue) = _manifest_value(value.value) +_manifest_value(configuration::Configuration) = + configuration_manifest(configuration) +_manifest_value(value) = value + +""" +$(TYPEDSIGNATURES) + +Return the named resolved parameterization of a configuration in a stable, +serializable form. +""" +function configuration_manifest(configuration::Configuration) + values = map(_manifest_value, configuration.values) + return NamedTuple{configuration.names}(values) +end diff --git a/src/parametricbuilder/gridspace/macros.jl b/src/parametricbuilder/gridspace/macros.jl new file mode 100644 index 00000000..20865bbc --- /dev/null +++ b/src/parametricbuilder/gridspace/macros.jl @@ -0,0 +1,256 @@ +# These AST helpers are core composition machinery for `@gridspace` and +# `@relax`. 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 + elseif field_expression isa Expr && field_expression.head === :(::) + name = field_expression.args[1] + else + push!(clean_body, argument) + continue + end + push!(clean_body, field_expression) + push!(fields, (; name, 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 _extract_type_parameters(struct_node) + signature = struct_node.args[2] + signature isa Expr && signature.head === :(<:) && + (signature = signature.args[1]) + signature isa Expr && signature.head === :curly || return () + return Tuple(signature.args[2:end]) +end + +function _extract_parametric_supertype(struct_node) + signature = struct_node.args[2] + signature isa Expr && signature.head === :(<:) || return nothing + supertype = signature.args[2] + supertype isa Expr && supertype.head === :curly || return nothing + return supertype.args[1] +end + +function _type_parameter_name(parameter) + parameter isa Symbol && return parameter + parameter isa Expr && parameter.head in (:<:, :>:) && + return parameter.args[1] + return nothing +end + +function _rebuild_ast(expression, old_struct, new_struct, generated) + replaced = _replace_struct(expression, old_struct, new_struct) + return Expr(:block, replaced, generated...) +end + +recast(::Type{T}, value::Number) where {T<:Real} = convert(T, value) +recast(::Type{T}, values::AbstractArray) where {T<:Real} = + map(value -> recast(T, value), values) +recast(::Type{T}, values::Tuple) where {T<:Real} = + map(value -> recast(T, value), values) +recast(::Type{T}, values::NamedTuple) where {T<:Real} = + map(value -> recast(T, value), values) +recast(::Type{<:Real}, value) = value + +_relax_eltype(value::Number) = typeof(value) +_relax_eltype(values::AbstractArray{<:Number}) = eltype(values) +_relax_eltype(values::Tuple) = + isempty(values) ? nothing : _promoted_numeric_type(values) +_relax_eltype(value) = try + candidate = eltype(typeof(value)) + candidate <: Number ? candidate : nothing +catch + nothing +end + +function _promoted_numeric_type(values::Tuple) + types = filter(!isnothing, map(_relax_eltype, values)) + isempty(types) && throw(ArgumentError( + "@relax constructor received no numeric fields to promote", + )) + return promote_type(types...) +end + +""" +$(SIGNATURES) + +Retain the strict positional struct and add a keyword constructor that lifts +only explicit Grid/spec fields into a `Gridspace`. With `Target`, the +generated space materializes that target instead of the vault 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[] + axes = Any[] + for field in fields + push!( + keywords, + field.has_default ? + Expr(:kw, field.name, field.default) : field.name, + ) + push!(axes, :($(GlobalRef(@__MODULE__, :_gridspace_axis))($(field.name)))) + end + push!(keywords, Expr(:kw, :combine, QuoteNode(:product))) + signature = Expr(:call, struct_name, Expr(:parameters, keywords...)) + names = Expr(:tuple, map(field -> QuoteNode(field.name), fields)...) + values = Expr(:tuple, axes...) + constructor = Expr(:function, signature, quote + return $(GlobalRef(@__MODULE__, :Gridspace)){$target}( + $target, + $values, + $names; + combine=combine, + ) + end) + return esc(_rebuild_ast(raw, struct_node, clean_struct, (constructor,))) +end + +""" +$(SIGNATURES) + +Add promoted numeric construction and conversion methods to a parametric +structure. +""" +macro relax(expression) + raw = _strip_escapes(expression) + struct_node = _get_struct_node(raw) + struct_name = _extract_struct_name(struct_node) + fields, _ = _parse_fields(struct_node.args[3]) + parameters = _extract_type_parameters(struct_node) + isempty(parameters) && + throw(ArgumentError("@relax requires a parametric struct")) + scalar_parameter = _type_parameter_name(first(parameters)) + scalar_parameter === nothing && throw(ArgumentError( + "@relax could not identify the leading scalar type parameter", + )) + + field_names = map(field -> field.name, fields) + values = Expr(:tuple, field_names...) + recasts = [ + :($(GlobalRef(@__MODULE__, :recast))(promoted_type, $(field.name))) + for field in fields + ] + target_recasts = [ + :($(GlobalRef(@__MODULE__, :recast))(TargetScalar, value.$(field.name))) + for field in fields + ] + abstract_supertype = _extract_parametric_supertype(struct_node) + + promoted_constructor = quote + @inline function $struct_name($(field_names...)) + promoted_type = + $(GlobalRef(@__MODULE__, :_promoted_numeric_type))($values) + return $struct_name{promoted_type}($(recasts...)) + end + end + concrete_converter = quote + @inline function Base.convert( + ::Type{<:$struct_name{TargetScalar}}, + value::$struct_name, + ) where {TargetScalar<:Real} + return $struct_name{TargetScalar}($(target_recasts...)) + end + @inline Base.eltype(::Type{<:$struct_name{Scalar}}) where {Scalar} = Scalar + @inline Base.eltype(::$struct_name{Scalar}) where {Scalar} = Scalar + @inline $(GlobalRef(@__MODULE__, :recast))( + ::Type{TargetScalar}, + value::$struct_name, + ) where {TargetScalar<:Real} = + $struct_name{TargetScalar}($(target_recasts...)) + end + + abstract_converter = abstract_supertype === nothing ? nothing : quote + @inline function Base.convert( + ::Type{<:$abstract_supertype{TargetScalar}}, + value::$struct_name, + ) where {TargetScalar<:Real} + return $struct_name{TargetScalar}($(target_recasts...)) + end + end + + generated = abstract_converter === nothing ? + (promoted_constructor, concrete_converter) : + (promoted_constructor, concrete_converter, abstract_converter) + rebuilt = _rebuild_ast( + raw, + struct_node, + struct_node, + generated, + ) + return esc(rebuilt) +end diff --git a/src/parametricbuilder/groupspec.jl b/src/parametricbuilder/groupspec.jl deleted file mode 100644 index a7f7a3d3..00000000 --- a/src/parametricbuilder/groupspec.jl +++ /dev/null @@ -1,211 +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/materialspec.jl b/src/parametricbuilder/materialspec.jl index 4a5e03ab..b9035dcf 100644 --- a/src/parametricbuilder/materialspec.jl +++ b/src/parametricbuilder/materialspec.jl @@ -1,73 +1,103 @@ -# Use lib/material nominal; kw is either percent-only or (value,pct) -function _pair_from_nominal(nom, x) - x === nothing ? (nom, nothing) : - (x isa Tuple && length(x)==2) ? x : - (nom, x) +@gridspace Materials.Material @relax struct MaterialParameters{T<:Real} + rho::T + eps_r::T=1.0 + mu_r::T=1.0 + T0::T=20.0 + alpha::T=0.0 end -# -------------------- 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)) +$(TYPEDSIGNATURES) + +Construct electromagnetic and thermal material properties. Scalar property +inputs return a [`Material`](@ref) directly. An explicit [`Grid`](@ref) or +numeric [`AbstractSpec`](@ref) lifts the same declaration to a +[`Gridspace{Material}`](@ref). + +# Keywords + +- `rho`: Electrical resistivity \\[Ω·m\\]. +- `eps_r=1.0`: Relative permittivity \\[dimensionless\\]. +- `mu_r=1.0`: Relative permeability \\[dimensionless\\]. +- `T0=20.0`: Reference temperature \\[°C\\]. +- `alpha=0.0`: Temperature coefficient of resistivity \\[1/°C\\]. +- `combine=:product`: Local composition rule when an input varies. + +# Returns + +- A [`Material`](@ref) for scalar inputs, or a [`Gridspace{Material}`](@ref) + when at least one input is a `Grid` or `AbstractSpec`. """ -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)) --- -function 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) - ) +function Material(; + rho, + eps_r=1.0, + mu_r=1.0, + T0=20.0, + alpha=0.0, + combine::Symbol=:product, +) + combine in (:product, :zip) || + throw(ArgumentError("combine must be :product or :zip; got :$combine")) + values = (rho, eps_r, mu_r, T0, alpha) + any(value -> value isa Union{AbstractGrid,AbstractSpec}, values) && + return MaterialParameters(; rho, eps_r, mu_r, T0, alpha, combine) + return Materials.Material(rho, eps_r, mu_r, T0, alpha) end -# --- 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 + material::Materials.Material; + rho=material.rho, + eps_r=material.eps_r, + mu_r=material.mu_r, + T0=material.T0, + alpha=material.alpha, + combine::Symbol=:product, ) - 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) - ) + return Material(; rho, eps_r, mu_r, T0, alpha, combine) end -# --- 3) From a MaterialsLibrary + name --- -function Material(lib::Materials.MaterialsLibrary, name::AbstractString; kwargs...) - Material(get(lib, name); kwargs...) -end -function Material(lib::Materials.MaterialsLibrary, name::Symbol; kwargs...) - Material(lib, String(name); kwargs...) +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 _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)] +""" +$(TYPEDSIGNATURES) + +Add the single deterministic material described by `spec` to a +[`LineCableModels.MaterialsLibrary`](@ref). The specification is materialized +through the same [`Gridspace`](@ref) grammar used by the cable builder. + +# Arguments + +- `library`: Material library to modify. +- `name`: Material key. +- `spec`: Deterministic parameterized material specification. + +# Returns + +- The modified material library. + +# Errors + +- Throws `ArgumentError` when `spec` contains uncertainty or describes more + than one material configuration. +""" +function add!( + library::Materials.MaterialsLibrary, + name::Union{AbstractString,Symbol}, + spec::AbstractSpec{Materials.Material}, +) + has_uncertainty(spec) && throw(ArgumentError( + "a reusable material-library entry must be deterministic", + )) + length(spec) == 1 || throw(ArgumentError( + "a reusable material-library entry must describe exactly one material; got $(length(spec)) configurations", + )) + return add!(library, String(name), only(spec)) end diff --git a/src/parametricbuilder/parametricsweep.jl b/src/parametricbuilder/parametricsweep.jl deleted file mode 100644 index 82a52e8b..00000000 --- a/src/parametricbuilder/parametricsweep.jl +++ /dev/null @@ -1,202 +0,0 @@ -""" - ParametricSweep{C,R} <: AbstractVector{R} - -Store an ordered deterministic parameter study as paired cases and results. -Indexing and iteration return results; [`cases`](@ref) preserves the input that -produced each result. The container records no Cartesian axes or GridSpace -state. - -All cases must have one concrete type and all results must have one concrete -type. This keeps scalar indexing type-stable while allowing future GridSpace -code to supply the ordered cases. -""" -struct ParametricSweep{C, R} <: AbstractVector{R} - "Ordered inputs evaluated by the parameter study." - cases::Vector{C} - "Ordered results paired one-to-one with `cases`." - results::Vector{R} - - function ParametricSweep{C, R}( - cases::Vector{C}, - results::Vector{R} - ) where {C, R} - length(cases) == length(results) || throw( - DimensionMismatch("parametric cases and results must have equal lengths"), - ) - return new{C, R}(cases, results) - end -end - -function _sweep_vector(name::AbstractString, values) - collected = collect(values) - if isempty(collected) - isconcretetype(eltype(collected)) || throw( - ArgumentError("empty parametric $name must declare a concrete element type"), - ) - return collected - end - concrete_type = typeof(first(collected)) - all(value -> typeof(value) === concrete_type, collected) || throw( - ArgumentError("parametric $name must have one concrete type"), - ) - return Vector{concrete_type}(collected) -end - -@doc """ - ParametricSweep(cases, results) - -Construct an ordered deterministic parameter study. - -# Arguments - -- `cases`: Inputs supplied to the deterministic calculations. -- `results`: Results in the same order as `cases`. - -# Returns - -- A `ParametricSweep` whose scalar indexing and iteration return results. - -# Errors - -Throws `ArgumentError` when either collection is heterogeneous, or when an -empty collection does not declare a concrete element type. -Throws `DimensionMismatch` when the collections have different lengths. - -# Examples - -```julia -sweep = ParametricSweep([:case_a, :case_b], [result_a, result_b]) -first(sweep) == result_a -cases(sweep)[2] == :case_b -``` -""" -function ParametricSweep(cases, results) - case_values = _sweep_vector("cases", cases) - result_values = _sweep_vector("results", results) - return ParametricSweep{eltype(case_values), eltype(result_values)}( - case_values, - result_values - ) -end - -Base.IndexStyle(::Type{<:ParametricSweep}) = IndexLinear() -Base.size(sweep::ParametricSweep) = (length(sweep.results),) -Base.getindex(sweep::ParametricSweep, index::Int) = sweep.results[index] -function Base.getindex(sweep::ParametricSweep, indices::AbstractVector{<:Integer}) - return ParametricSweep(sweep.cases[indices], sweep.results[indices]) -end -function Base.getindex(sweep::ParametricSweep, indices::AbstractRange{<:Integer}) - return ParametricSweep(sweep.cases[indices], sweep.results[indices]) -end -function Base.getindex(sweep::ParametricSweep, ::Colon) - ParametricSweep(copy(sweep.cases), copy(sweep.results)) -end -Base.copy(sweep::ParametricSweep) = sweep[:] - -""" - cases(sweep) - -Return the ordered inputs paired with the results in `sweep`. -""" -cases(sweep::ParametricSweep) = sweep.cases - -""" - results(sweep) - -Return the ordered result vector stored by `sweep`. -""" -results(sweep::ParametricSweep) = sweep.results - -""" - ncases(sweep) - -Return the number of paired cases and results in `sweep`. -""" -ncases(sweep::ParametricSweep) = length(sweep) - -function _common_result_value(accessor, sweep::ParametricSweep, name::Symbol) - isempty(sweep) && throw(ArgumentError("$name is unavailable for an empty sweep")) - value = accessor(first(sweep)) - all(index -> isequal(accessor(sweep[index]), value), 2:length(sweep)) || throw( - ArgumentError("$name varies between cases; use $name(sweep, case_index)"), - ) - return value -end - -""" - basis(sweep[, case_index]) - domain(sweep[, case_index]) - frequencies(sweep[, case_index]) - nconductors(sweep[, case_index]) - nfrequencies(sweep[, case_index]) - -Return a shared result property or the property for one case. The unindexed -form throws when the sweep is empty or the property differs between cases. -""" -basis(sweep::ParametricSweep) = _common_result_value(basis, sweep, :basis) -domain(sweep::ParametricSweep) = _common_result_value(domain, sweep, :domain) -frequencies(sweep::ParametricSweep) = _common_result_value(frequencies, sweep, :frequencies) -nconductors(sweep::ParametricSweep) = _common_result_value(nconductors, sweep, :nconductors) -function nfrequencies(sweep::ParametricSweep) - _common_result_value(nfrequencies, sweep, :nfrequencies) -end - -basis(sweep::ParametricSweep, case_index::Integer) = basis(sweep[Int(case_index)]) -domain(sweep::ParametricSweep, case_index::Integer) = domain(sweep[Int(case_index)]) -function frequencies(sweep::ParametricSweep, case_index::Integer) - frequencies(sweep[Int(case_index)]) -end -function nconductors(sweep::ParametricSweep, case_index::Integer) - nconductors(sweep[Int(case_index)]) -end -function nfrequencies(sweep::ParametricSweep, case_index::Integer) - nfrequencies(sweep[Int(case_index)]) -end - -""" - Z(sweep[, case_index[, i, j[, k]]]) - Y(sweep[, case_index[, i, j[, k]]]) - R(sweep[, case_index[, i, j[, k]]]) - X(sweep[, case_index[, i, j[, k]]]) - L(sweep[, case_index[, i, j[, k]]]) - G(sweep[, case_index[, i, j[, k]]]) - B(sweep[, case_index[, i, j[, k]]]) - C(sweep[, case_index[, i, j[, k]]]) - -Return a component for every result or for one selected case. After -`case_index`, selection follows the accessor grammar of the stored result. For -`LineParameters`, `(i, j)` returns the complete frequency response and `k` may -be a scalar, range, or `:`. Canonical units follow [`basis`](@ref). -""" -Z(sweep::ParametricSweep) = map(Z, sweep.results) -Y(sweep::ParametricSweep) = map(Y, sweep.results) -R(sweep::ParametricSweep) = map(R, sweep.results) -X(sweep::ParametricSweep) = map(X, sweep.results) -L(sweep::ParametricSweep) = map(L, sweep.results) -G(sweep::ParametricSweep) = map(G, sweep.results) -B(sweep::ParametricSweep) = map(B, sweep.results) -C(sweep::ParametricSweep) = map(C, sweep.results) - -Z(sweep::ParametricSweep, case_index::Integer, args...) = Z(sweep[Int(case_index)], args...) -Y(sweep::ParametricSweep, case_index::Integer, args...) = Y(sweep[Int(case_index)], args...) -R(sweep::ParametricSweep, case_index::Integer, args...) = R(sweep[Int(case_index)], args...) -X(sweep::ParametricSweep, case_index::Integer, args...) = X(sweep[Int(case_index)], args...) -L(sweep::ParametricSweep, case_index::Integer, args...) = L(sweep[Int(case_index)], args...) -G(sweep::ParametricSweep, case_index::Integer, args...) = G(sweep[Int(case_index)], args...) -B(sweep::ParametricSweep, case_index::Integer, args...) = B(sweep[Int(case_index)], args...) -C(sweep::ParametricSweep, case_index::Integer, args...) = C(sweep[Int(case_index)], args...) - -series_impedance(sweep::ParametricSweep) = map(series_impedance, sweep.results) -shunt_admittance(sweep::ParametricSweep) = map(shunt_admittance, sweep.results) -function series_impedance(sweep::ParametricSweep, case_index::Integer) - series_impedance(sweep[Int(case_index)]) -end -function shunt_admittance(sweep::ParametricSweep, case_index::Integer) - shunt_admittance(sweep[Int(case_index)]) -end -resistance(sweep::ParametricSweep, args...) = R(sweep, args...) -reactance(sweep::ParametricSweep, args...) = X(sweep, args...) -inductance(sweep::ParametricSweep, args...) = L(sweep, args...) -conductance(sweep::ParametricSweep, args...) = G(sweep, args...) -susceptance(sweep::ParametricSweep, args...) = B(sweep, args...) -capacitance(sweep::ParametricSweep, args...) = C(sweep, args...) diff --git a/src/parametricbuilder/positionspec.jl b/src/parametricbuilder/positionspec.jl index d4893a55..b97e9390 100644 --- a/src/parametricbuilder/positionspec.jl +++ b/src/parametricbuilder/positionspec.jl @@ -1,79 +1,233 @@ -struct PositionSpec <: AbstractPositionSpec - x0::Real - y0::Real - dx::Any - dy::Any - conn::Dict{String, Int} +struct PositionBuilder{Kind,P<:Tuple,C<:Tuple} + parameters::P + connections::C 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} +_position_kind(::PositionBuilder{Kind}) where {Kind} = Kind + +function PositionBuilder(::Val{Kind}, parameters::P, connections::C) where { + Kind,P<:Tuple,C<:Tuple } + all(value -> value isa Real, parameters) || throw(ArgumentError( + "position coordinates and spacing must resolve to real numbers", + )) + return PositionBuilder{Kind,P,C}(parameters, connections) +end -# 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) = () +const _PhaseEntry = Union{ + Pair{Symbol,<:Any}, + Pair{String,<:Any}, + Tuple{Symbol,<:Any}, + Tuple{String,<:Any}, +} -""" - make_phase_maps(phases, n::Int) +_phase_entries(entry::_PhaseEntry) = (entry,) +_phase_entries(entries::Tuple) = entries +_phase_entries(entries::AbstractVector) = Tuple(entries) +_phase_entries(::Nothing) = () -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))", - ) +function _phase_maps(phases, count::Int) + maps = [Dict{String,Int}() for _ in 1:count] + for entry in _phase_entries(phases) + key_raw, value = entry isa Pair ? + (first(entry), last(entry)) : (entry[1], entry[2]) + key = String(key_raw) + if value isa Integer + for map in maps + map[key] = Int(value) end - for i in 1:n - out[i][key] = Int(val_raw[i]) + elseif value isa Tuple || value isa AbstractVector + length(value) == count || throw(DimensionMismatch( + "phase '$key' requires $count assignments; got $(length(value))", + )) + for index in 1:count + value[index] isa Integer || throw(ArgumentError( + "phase assignments must be integers", + )) + maps[index][key] = Int(value[index]) end else - error( - "Invalid phase value for '$key': expected Integer or collection of length $n, got $(typeof(val_raw))", - ) + throw(ArgumentError( + "phase '$key' must be an integer or a collection of $count integers", + )) end end - return out + return Tuple(maps) +end + +_point_builder(x, y, connections) = + PositionBuilder(Val(:point), (x, y), connections) +_trifoil_builder(x, y, spacing, connections) = + PositionBuilder(Val(:trifoil), (x, y, spacing), connections) +_hflat_builder(x, y, spacing, connections) = + PositionBuilder(Val(:hflat), (x, y, spacing), connections) +_vflat_builder(x, y, spacing, connections) = + PositionBuilder(Val(:vflat), (x, y, spacing), connections) + +""" +$(TYPEDSIGNATURES) + +Describe one cable position for use by [`SystemBuilder`](@ref). + +# Keywords + +- `x`: Horizontal coordinate \\[m\\]. +- `y`: Vertical coordinate \\[m\\]. +- `phases=nothing`: Phase or conductor assignments. Supply pairs such as + `:core => 1` and `:sheath => 2`. +- `combine=:product`: Local composition rule for direct varying inputs. Use + `:zip` to pair compatible axes. + +# Returns + +- A [`Gridspace`](@ref) of position declarations. The parent system builder + converts each resolved declaration into a cable position. +""" +function at(; x, y, phases=nothing, combine::Symbol=:product) + connections = _phase_maps(phases, 1) + return Gridspace{PositionBuilder}( + _point_builder, + (_gridspace_axis(x), _gridspace_axis(y), _gridspace_axis(connections)), + (:x, :y, :phases); + combine, + ) 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]) +""" +$(TYPEDSIGNATURES) + +Describe three identical cables arranged in equilateral trifoil formation. +The formation is resolved when the parent [`SystemBuilder`](@ref) materializes +the cable system, so its spacing is checked against the cable outer diameter. + +# Keywords + +- `x=0.0`: Horizontal coordinate of the formation centre \\[m\\]. +- `y`: Vertical coordinate of the formation centre \\[m\\]. +- `spacing`: Centre-to-centre distance between adjacent cables \\[m\\]. +- `phases`: Phase or conductor assignments. Each assignment may contain three + integers, one for each cable, for example `:core => (1, 2, 3)`. +- `combine=:product`: Local composition rule for direct varying inputs. Use + `:zip` to pair compatible axes. + +# Returns + +- A [`Gridspace`](@ref) of trifoil declarations. Each resolved declaration is + one object-valued input to its parent system specification. + +# Errors + +- System materialization throws `ArgumentError` when `spacing` is nonpositive + or smaller than the cable outer diameter. +- Throws `DimensionMismatch` when a phase assignment does not provide three + values. +""" +function trifoil(; + x=0.0, + y, + spacing, + phases, + combine::Symbol=:product, +) + connections = _phase_maps(phases, 3) + return Gridspace{PositionBuilder}( + _trifoil_builder, + (_gridspace_axis(x), _gridspace_axis(y), _gridspace_axis(spacing), _gridspace_axis(connections)), + (:x, :y, :spacing, :phases); + combine, + ) +end + +""" +$(TYPEDSIGNATURES) + +Describe `n` identical cables in a horizontal flat formation. The first cable +is placed at `(x, y)` and subsequent cables are placed at increasing horizontal +coordinates. + +# Keywords + +- `x=0.0`: Horizontal coordinate of the first cable \\[m\\]. +- `y=0.0`: Vertical coordinate shared by all cables \\[m\\]. +- `spacing`: Centre-to-centre distance between adjacent cables \\[m\\]. +- `phases`: Phase or conductor assignments. An assignment may provide one + integer for every cable. +- `n=3`: Number of cables. +- `combine=:product`: Local composition rule for direct varying inputs. Use + `:zip` to pair compatible axes. + +# Returns + +- A [`Gridspace`](@ref) of horizontal flat-formation declarations. + +# Errors + +- Throws `ArgumentError` when `n` is nonpositive. System materialization also + rejects nonpositive spacing and overlapping cables. +- Throws `DimensionMismatch` when a phase assignment does not provide `n` + values. +""" +function hflat(; + x=0.0, + y=0.0, + spacing, + phases, + n::Int=3, + combine::Symbol=:product, +) + n > 0 || throw(ArgumentError("n must be positive")) + connections = _phase_maps(phases, n) + return Gridspace{PositionBuilder}( + _hflat_builder, + (_gridspace_axis(x), _gridspace_axis(y), _gridspace_axis(spacing), _gridspace_axis(connections)), + (:x, :y, :spacing, :phases); + combine, + ) +end + +""" +$(TYPEDSIGNATURES) + +Describe `n` identical cables in a vertical flat formation. The first cable is +placed at `(x, y)` and subsequent cables are placed at decreasing vertical +coordinates. + +# Keywords + +- `x=0.0`: Horizontal coordinate shared by all cables \\[m\\]. +- `y=0.0`: Vertical coordinate of the first cable \\[m\\]. +- `spacing`: Centre-to-centre distance between adjacent cables \\[m\\]. +- `phases`: Phase or conductor assignments. An assignment may provide one + integer for every cable. +- `n=3`: Number of cables. +- `combine=:product`: Local composition rule for direct varying inputs. Use + `:zip` to pair compatible axes. + +# Returns + +- A [`Gridspace`](@ref) of vertical flat-formation declarations. + +# Errors + +- Throws `ArgumentError` when `n` is nonpositive. System materialization also + rejects nonpositive spacing and overlapping cables. +- Throws `DimensionMismatch` when a phase assignment does not provide `n` + values. +""" +function vflat(; + x=0.0, + y=0.0, + spacing, + phases, + n::Int=3, + combine::Symbol=:product, +) + n > 0 || throw(ArgumentError("n must be positive")) + connections = _phase_maps(phases, n) + return Gridspace{PositionBuilder}( + _vflat_builder, + (_gridspace_axis(x), _gridspace_axis(y), _gridspace_axis(spacing), _gridspace_axis(connections)), + (:x, :y, :spacing, :phases); + combine, + ) end diff --git a/src/parametricbuilder/systembuilderspec.jl b/src/parametricbuilder/systembuilderspec.jl index 096d08e0..85427fec 100644 --- a/src/parametricbuilder/systembuilderspec.jl +++ b/src/parametricbuilder/systembuilderspec.jl @@ -1,173 +1,269 @@ -# 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 -function EarthSpec(; rho, eps_r = 1.0, mu_r = 1.0, t = Inf) - EarthSpec(_spec(rho), _spec(eps_r), _spec(mu_r), _spec(t)) +@gridspace @relax struct EarthParameters{T<:Real} + rho::T + eps_r::T=1.0 + mu_r::T=1.0 + thickness::T=Inf end -function Earth(; rho, eps_r = 1.0, mu_r = 1.0, t = Inf) - EarthSpec(_spec(rho), _spec(eps_r), _spec(mu_r), _spec(t)) +""" +$(TYPEDSIGNATURES) + +Describe earth properties independently of analysis frequencies. The +frequency-dependent materialized `EarthModel` is constructed when the parent +problem is materialized. +""" +function Earth(; + rho, + eps_r=1.0, + mu_r=1.0, + thickness=Inf, + combine::Symbol=:product, +) + return EarthParameters(; rho, eps_r, mu_r, thickness, combine) end -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} +function _position_coordinates( + position::PositionBuilder, + design::DataModel.CableDesign, +) + kind = _position_kind(position) + if kind === :point + x, y = position.parameters + return ((x, y, only(position.connections)),) + end + + x, y, spacing = position.parameters + spacing > zero(spacing) || + throw(ArgumentError("formation spacing must be positive")) + minimum_spacing = 2 * DataModel.get_outer_radius(design) + spacing >= minimum_spacing || throw(ArgumentError( + "formation spacing $spacing is smaller than the minimum non-overlap distance $minimum_spacing", + )) + + coordinates = if kind === :trifoil + length(position.connections) == 3 || + throw(DimensionMismatch("trifoil requires three phase maps")) + values = DataModel.trifoil_formation(x, y, spacing / 2) + ((values[1], values[2]), (values[3], values[4]), (values[5], values[6])) + elseif kind === :hflat + ntuple( + index -> (x + (index - 1) * spacing, y), + length(position.connections), + ) + elseif kind === :vflat + ntuple( + index -> (x, y - (index - 1) * spacing), + length(position.connections), + ) + else + throw(ArgumentError("unsupported position kind :$kind")) + end + return ntuple( + index -> ( + coordinates[index][1], + coordinates[index][2], + position.connections[index], + ), + length(position.connections), + ) 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)) +function _materialize_earth( + earth::EarthParameters, + frequencies, +) + return EarthProps.EarthModel( + collect(frequencies), + earth.rho, + earth.eps_r, + earth.mu_r; + t=earth.thickness, ) end -function 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) +function _materialize_earth( + earth::EarthProps.EarthModel, + ::AbstractVector, +) + return earth end -function 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) +struct SystemMaterializer + position_count::Int end -# ───────────────────────────────────────────────────────────────────────────── -# Internals: expand range/% grammar via ParametricBuilder helpers -# ───────────────────────────────────────────────────────────────────────────── -@inline _expand_pair(specpair) = _make_range(specpair[1]; pct = specpair[2]) +function (materializer::SystemMaterializer)(identifier, design, values...) + design isa DataModel.CableDesign || + throw(ArgumentError("SystemBuilder design must resolve to CableDesign")) + expected = materializer.position_count + 4 + length(values) == expected || throw(DimensionMismatch( + "SystemBuilder expected $expected resolved values after the design; got $(length(values))", + )) -# (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 + positions = values[1:materializer.position_count] + line_length = values[materializer.position_count + 1] + temperature = values[materializer.position_count + 2] + earth = values[materializer.position_count + 3] + frequencies = values[materializer.position_count + 4] + + line_length isa Real && line_length > zero(line_length) || + throw(ArgumentError("line length must be a positive real number")) + temperature isa Real || + throw(ArgumentError("temperature must be a real number")) + frequencies isa AbstractVector || + throw(ArgumentError("frequencies must be an ordinary vector")) + + layouts = tuple(( + _position_coordinates(position, design) + for position in positions + )...) + coordinates = tuple(Iterators.flatten(layouts)...) + isempty(coordinates) && + throw(ArgumentError("SystemBuilder requires at least one cable position")) + + x, y, connections = first(coordinates) + system = DataModel.LineCableSystem( + String(identifier), + line_length, + DataModel.CablePosition(design, x, y, connections), + ) + for (next_x, next_y, next_connections) in Iterators.drop(coordinates, 1) + system = add!(system, design, next_x, next_y, next_connections) end -end -function _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) + earth_model = _materialize_earth(earth, frequencies) + return Engine.LineParametersProblem( + system; + temperature, + earth_props=earth_model, + frequencies=collect(frequencies), ) end -# 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 +struct SystemBuilderSpec{D,P<:Tuple,L,T,E,F,C} <: + AbstractSpec{Engine.LineParametersProblem} + identifier::String + design::D + positions::P + line_length::L + temperature::T + earth::E + frequencies::F + combine::C 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))) +_flatten_positions(position::Gridspace{PositionBuilder}) = (position,) +function _flatten_positions(positions::Union{Tuple,AbstractVector}) + flattened = () + for position in positions + flattened = (flattened..., _flatten_positions(position)...) + end + return flattened end +_flatten_positions(value) = throw(ArgumentError( + "SystemBuilder positions must be created by at/trifoil/hflat/vflat; got $(typeof(value))", +)) -# ───────────────────────────────────────────────────────────────────────────── -# 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 +""" +$(TYPEDSIGNATURES) + +Describe a line-cable analysis problem from a cable design, its spatial +arrangement, earth properties, and analysis frequencies. Iterating the +returned specification materializes +[`LineCableModels.Engine.LineParametersProblem`](@ref) objects without running +the numerical analysis. + +# Arguments + +- `identifier`: Name assigned to the materialized cable system. +- `design`: A materialized cable design or a [`CableBuilder`](@ref) + specification. +- `positions`: One position declaration, or a tuple or vector of declarations + created by [`at`](@ref), [`trifoil`](@ref), [`hflat`](@ref), or + [`vflat`](@ref). + +# Keywords + +- `length=1000.0`: Line length \\[m\\]. +- `temperature=20.0`: Conductor temperature \\[°C\\]. +- `earth`: Earth declaration created by [`Earth`](@ref), or a materialized + earth model. +- `frequencies`: Ordinary vector of analysis frequencies \\[Hz\\], or a `Grid` + whose values are complete frequency vectors. +- `combine=:product`: Local composition rule for direct varying inputs. Use + `:zip` to pair compatible axes. + +# Returns + +- A line-parameter problem specification. Pass it to + [`LineCableModels.Engine.compute!`](@ref) with a + [`LineCableModels.Engine.Formulation`](@ref) to evaluate its materialized + configurations. + +# Errors + +- Throws `ArgumentError` for unsupported design, position, earth, frequency, + or composition inputs. +- Materialization reports overlapping positions, invalid line lengths, and + other invalid physical inputs from the materialized cable-system model. +""" +function SystemBuilder( + identifier::AbstractString, + design, + positions; + length=1000.0, + temperature=20.0, + earth, + frequencies, + combine::Symbol=:product, +) + design isa Union{DataModel.CableDesign,AbstractSpec{DataModel.CableDesign}} || + throw(ArgumentError("design must be a materialized CableDesign or CableBuilder spec")) + earth isa Union{EarthProps.EarthModel,AbstractSpec{EarthParameters}} || + throw(ArgumentError("earth must be a materialized EarthModel or Earth spec")) + frequencies isa Union{AbstractVector,AbstractGrid} || throw(ArgumentError( + "frequencies must be an ordinary vector or a Grid of complete vectors", + )) + combine in (:product, :zip) || + throw(ArgumentError("combine must be :product or :zip")) + position_tuple = _flatten_positions(positions) + isempty(position_tuple) && + throw(ArgumentError("SystemBuilder requires at least one position")) + return SystemBuilderSpec( + String(identifier), + design, + position_tuple, + length, + temperature, + earth, + frequencies, + Val(combine), + ) end -function build(spec::SystemBuilderSpec) - problems = LineParametersProblem[] - for prob in spec # uses Base.iterate(spec::SystemBuilderSpec) - push!(problems, prob) - end - return problems +function gridspace(spec::SystemBuilderSpec) + axes = ( + _gridspace_axis(spec.identifier), + _gridspace_axis(spec.design), + map(_gridspace_axis, spec.positions)..., + _gridspace_axis(spec.line_length), + _gridspace_axis(spec.temperature), + _gridspace_axis(spec.earth), + _gridspace_axis(spec.frequencies), + ) + names = ( + :identifier, + :design, + ntuple(index -> Symbol(:position_, index), length(spec.positions))..., + :length, + :temperature, + :earth, + :frequencies, + ) + return Gridspace{Engine.LineParametersProblem}( + SystemMaterializer(length(spec.positions)), + axes, + names; + combine=_valof(spec.combine), + ) end diff --git a/src/plotbuilder/PlotBuilder.jl b/src/plotbuilder/PlotBuilder.jl index 2a81dbaa..edd80df7 100644 --- a/src/plotbuilder/PlotBuilder.jl +++ b/src/plotbuilder/PlotBuilder.jl @@ -6,8 +6,10 @@ Makie extensions render the resulting `RenderSpec` values. """ module PlotBuilder +using DocStringExtensions: TYPEDSIGNATURES + import ..UnitHandler: Units, QuantityTag, display_unit, get_label -import Measurements: value, uncertainty +import ..Utils: to_nominal, uncertainty_value export AbstractPlotSpec, PlotRecipe export AbstractTrackSize, FixedTrack, RelativeTrack, ContentTrack @@ -42,7 +44,7 @@ include("types.jl") include("grammar.jl") """ - export_svg(plot; path=nothing, theme=nothing, open_file=nothing) +$(TYPEDSIGNATURES) Export the current typed state of a `UIPlot` through an explicitly loaded CairoMakie extension. diff --git a/src/plotbuilder/grammar.jl b/src/plotbuilder/grammar.jl index 4af0f072..02407386 100644 --- a/src/plotbuilder/grammar.jl +++ b/src/plotbuilder/grammar.jl @@ -922,7 +922,8 @@ end make_render(::Type{S}, object; kwargs...) Materialize a domain object through the uniform PlotBuilder grammar. Plot -specifications specialize accessors; they do not replace this pipeline. +specifications specialize accessors; they do not replace this rendering +sequence. """ function make_render(::Type{S}, object; kwargs...) where {S <: AbstractPlotSpec} expected = dispatch_on(S) diff --git a/src/plotbuilder/types.jl b/src/plotbuilder/types.jl index 75025154..cd63044d 100644 --- a/src/plotbuilder/types.jl +++ b/src/plotbuilder/types.jl @@ -724,10 +724,10 @@ function _validate_log_axis(view::ViewSpec, axis::AxisSpec) axis.dim === :y ? series.ydata : series.zdata samples === nothing && continue for sample in samples - nominal = value(sample) + nominal = to_nominal(sample) nominal isa Real || continue found = true - lower = nominal - abs(uncertainty(sample)) + lower = nominal - abs(uncertainty_value(sample)) isfinite(nominal) && isfinite(lower) && lower > 0 || throw( DomainError(sample, "logarithmic axes require positive finite data and uncertainty bounds"), ) diff --git a/src/plotbuilder/uicomponents/UIComponents.jl b/src/plotbuilder/uicomponents/UIComponents.jl index 01ac80d9..d918f82f 100644 --- a/src/plotbuilder/uicomponents/UIComponents.jl +++ b/src/plotbuilder/uicomponents/UIComponents.jl @@ -1,11 +1,11 @@ module UIComponents using Makie -using Measurements using Dates using Printf: @sprintf import LineCableModels.PlotBuilder +using LineCableModels.Utils: to_nominal, uncertainty_value using LineCableModels.PlotBuilder: AbstractTrackSize, FixedTrack, RelativeTrack, ContentTrack, GridArea, GridSpec, SlotSpec, @@ -19,7 +19,7 @@ const COLORBAR_WIDTH = 140 const COLORBAR_TICK_LABEL_SIZE = 12 const COLORBAR_LABEL_SIZE = 14 const COLORBAR_END_PADDING = 28 -const SIDE_DOCK_WIDTH = COLORBAR_WIDTH +const LEGEND_DOCK_WIDTH = 220 const GRID_ROW_GAP = 6 const GRID_COLUMN_GAP = 6 const BUTTON_SIZE = 32 @@ -172,11 +172,11 @@ function _axis_values(view::ViewSpec, dim::Symbol; include_uncertainty::Bool = f data = dim === :x ? series.xdata : series.ydata data === nothing && continue for sample in data - nominal = Measurements.value(sample) + nominal = to_nominal(sample) nominal isa Real || continue numeric = Float64(nominal) isfinite(numeric) || continue - uncertainty = abs(Float64(Measurements.uncertainty(sample))) + uncertainty = abs(Float64(uncertainty_value(sample))) if include_uncertainty && isfinite(uncertainty) && !iszero(uncertainty) push!(values, numeric - uncertainty, numeric + uncertainty) else @@ -247,8 +247,8 @@ end function _numeric_values(values) values === nothing && return nothing, nothing - nominal = Measurements.value.(values) - errors = Measurements.uncertainty.(values) + nominal = to_nominal.(values) + errors = uncertainty_value.(values) return nominal, any(error -> !iszero(error), errors) ? errors : nothing end @@ -556,7 +556,7 @@ function _legend_dock_width(page::PageSpec) slot for slot in page.layout.slots if slot.name === colorbar_slot_name) if legend_slot.parent === colorbar_slot.parent && legend_slot.area.columns == colorbar_slot.area.columns - return SIDE_DOCK_WIDTH + return LEGEND_DOCK_WIDTH end end return nothing diff --git a/src/uncertainbessels/UncertainBessels.jl b/src/uncertainbessels/UncertainBessels.jl deleted file mode 100644 index c125e8ff..00000000 --- a/src/uncertainbessels/UncertainBessels.jl +++ /dev/null @@ -1,225 +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 - - -# 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. - -""" -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) - -function _bessel_doc(name::Symbol, scaled::Bool) - scaling = scaled ? - "Uses the scaled convention defined by `SpecialFunctions` to improve numerical stability." : - "Uses the unscaled convention defined by `SpecialFunctions`." - return """ - $(name)(ν, z) - - Evaluate the Bessel-family function of order `ν` at `z`, with uncertainty - propagation when `z` is a `Measurement` or `Complex{Measurement}`. - - # Arguments - - - `ν`: Bessel-function order. - - `z`: Real or complex argument, optionally containing measurement uncertainty. - - # Returns - - - The function value, with first-order propagated uncertainty when applicable. - - # Notes - - $(scaling) Plain numeric inputs delegate directly to `SpecialFunctions`. - Measurement inputs are linearized at their nominal value using numerical - derivatives. - """ -end - -@doc _bessel_doc(:besselj, false) besselj -@doc _bessel_doc(:bessely, false) bessely -@doc _bessel_doc(:besseli, false) besseli -@doc _bessel_doc(:besselk, false) besselk -@doc _bessel_doc(:besselh, false) besselh -@doc _bessel_doc(:besseljx, true) besseljx -@doc _bessel_doc(:besselyx, true) besselyx -@doc _bessel_doc(:besselix, true) besselix -@doc _bessel_doc(:besselkx, true) besselkx -@doc _bessel_doc(:besselhx, true) besselhx - -end # module UncertainBessels diff --git a/src/unithandler/UnitHandler.jl b/src/unithandler/UnitHandler.jl index 012cb47e..3e5f6590 100644 --- a/src/unithandler/UnitHandler.jl +++ b/src/unithandler/UnitHandler.jl @@ -216,7 +216,7 @@ be `:re`, `:im`, `:abs`, or `:angle` for the complex `Z` and `Y` accessors. # Returns -- A `QuantityTag` used to resolve canonical units, display units, labels, and +- A `QuantityTag` used to resolve primary units, display units, labels, and scaling. # Errors @@ -280,28 +280,6 @@ const _LINE_COMPONENT_QUANTITY = Dict( :Y_angle => (Y, :angle, :degree, :base) ) -""" - line_components(kind, mode, coordinate) - -Return the physical components selected for a series (`kind=:series`) or -shunt (`kind=:shunt`) quantity. `mode` is `:RLCG` or `:ZY`; `coordinate` is -`:cart` or `:polar`. -""" -function line_components(kind::Symbol, mode::Symbol, coordinate::Symbol) - kind in (:series, :shunt) || - throw(ArgumentError("kind must be :series or :shunt")) - mode in (:RLCG, :ZY) || throw(ArgumentError("mode must be :RLCG or :ZY")) - coordinate in (:cart, :polar) || - throw(ArgumentError("coordinate must be :cart or :polar")) - if mode === :RLCG - return kind === :series ? (:R, :L) : (:G, :C) - elseif kind === :series - return coordinate === :cart ? (:Z_re, :Z_im) : (:Z_abs, :Z_angle) - else - return coordinate === :cart ? (:Y_re, :Y_im) : (:Y_abs, :Y_angle) - end -end - """ line_component_quantity(component) diff --git a/src/uq/UQ.jl b/src/uq/UQ.jl deleted file mode 100644 index 0e8ca769..00000000 --- a/src/uq/UQ.jl +++ /dev/null @@ -1,364 +0,0 @@ -module UQ - -# Export public API -export SampleSummary, RLCG, HistogramPDF, CableConstantsMC, LineParametersMC -export sample, trial, mc, - statistics, has_samples, samples, has_distributions, distribution, - surrogate, ntrials, confidence, mean, std, quantile - -# Module-specific dependencies -using ..Commons: BASE_FLOAT -import ..Commons: domain, basis, frequencies, nconductors, nfrequencies, - PhaseDomain, ModalDomain, LineParamsDomain -using ..ParametricBuilder: - MaterialSpec, PartSpec, CableBuilderSpec, SystemBuilderSpec, - AbstractPositionSpec, - PositionSpec, PositionGroupSpec, build, iterate, _spec, - determinize -using ..Engine: - EMTFormulation, compute!, LineParameters, SeriesImpedance, ShuntAdmittance -using ..DataModel: get_outer_radius, CableConstants -using ..UnitHandler -using ..PlotBuilder -using Measurements: Measurement, measurement, value, uncertainty -using Random, Statistics, DataFrames -import Statistics: mean, std -using Distributions: - Distributions, ContinuousUnivariateDistribution, Normal, Uniform, cdf, - sampler -using StatsBase: fit, Histogram, normalize, ecdf -import StatsBase -import StatsBase: quantile -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. - -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)`. -""" -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 - -""" - sample(sbs::SystemBuilderSpec; distribution::Symbol = :uniform) - -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("plotspecs.jl") - -end # module UQ diff --git a/src/uq/dataframe.jl b/src/uq/dataframe.jl deleted file mode 100644 index 543975f0..00000000 --- a/src/uq/dataframe.jl +++ /dev/null @@ -1,63 +0,0 @@ -import DataFrames: DataFrame - -function _confidence_columns(summary::SampleSummary, result) - z = Distributions.quantile( - Distributions.Normal(), - 0.5 + confidence(result) / 2 - ) - half_width = z * summary.std / sqrt(ntrials(result)) - relative = half_width / max(abs(summary.mean), eps(typeof(summary.mean))) - return half_width, relative -end - -function DataFrame(result::LineParametersMC) - shape = size(result.statistics.R) - frames = Array{DataFrame, 3}(undef, shape) - for k in axes(frames, 3), j in axes(frames, 2), i in axes(frames, 1) - summaries = ( - statistics(result, :R, i, j, k), - statistics(result, :L, i, j, k), - statistics(result, :C, i, j, k), - statistics(result, :G, i, j, k) - ) - confidence_values = _confidence_columns.(summaries, Ref(result)) - frames[i, j, k] = DataFrame( - quantity = ["R", "L", "C", "G"], - mean = getproperty.(summaries, :mean), - std = getproperty.(summaries, :std), - min = getproperty.(summaries, :min), - q05 = getproperty.(summaries, :q05), - q50 = getproperty.(summaries, :q50), - q95 = getproperty.(summaries, :q95), - max = getproperty.(summaries, :max), - n = fill(ntrials(result), 4), - conf = fill(confidence(result), 4), - ci_half = first.(confidence_values), - ci_rel = last.(confidence_values) - ) - end - return frames -end - -function DataFrame(result::CableConstantsMC) - summaries = ( - statistics(result, :R), - statistics(result, :L), - statistics(result, :C) - ) - confidence_values = _confidence_columns.(summaries, Ref(result)) - return DataFrame( - variable = ["R", "L", "C"], - mean = getproperty.(summaries, :mean), - std = getproperty.(summaries, :std), - min = getproperty.(summaries, :min), - q05 = getproperty.(summaries, :q05), - q50 = getproperty.(summaries, :q50), - q95 = getproperty.(summaries, :q95), - max = getproperty.(summaries, :max), - ntrials = fill(ntrials(result), 3), - confidence = fill(confidence(result), 3), - ci_half = first.(confidence_values), - ci_rel = last.(confidence_values) - ) -end diff --git a/src/uq/distributions.jl b/src/uq/distributions.jl deleted file mode 100644 index 11baef1c..00000000 --- a/src/uq/distributions.jl +++ /dev/null @@ -1,240 +0,0 @@ -function HistogramPDF( - edges::AbstractVector{TE}, - density::AbstractVector{TD} -) where {TE <: Real, TD <: Real} - T = float(promote_type(TE, TD)) - return HistogramPDF{T}(Vector{T}(edges), Vector{T}(density)) -end - -function _auto_nbins( - values::AbstractVector{<:Real}; - nbins_min::Int = 10, - nbins_max::Int = 200 -) - isempty(values) && throw(ArgumentError("cannot bin an empty sample")) - sorted_values = sort(float.(values)) - span = last(sorted_values) - first(sorted_values) - if !(isfinite(span) && span > 0) - return nbins_min - end - interquartile_range = StatsBase.quantile(sorted_values, 0.75) - - StatsBase.quantile(sorted_values, 0.25) - if !(isfinite(interquartile_range) && interquartile_range > 0) - return clamp(ceil(Int, sqrt(length(values))), nbins_min, nbins_max) - end - width = 2 * interquartile_range / cbrt(length(values)) - raw_count = span / width - return isfinite(raw_count) ? - clamp(ceil(Int, raw_count), nbins_min, nbins_max) : nbins_max -end - -function _pdf_from_hist( - values::AbstractVector{<:Real}; - nbins::Union{Int, Nothing} = nothing -) - isempty(values) && throw(ArgumentError("cannot fit a histogram to an empty sample")) - count = isnothing(nbins) ? _auto_nbins(values) : nbins - count > 0 || throw(ArgumentError("nbins must be positive")) - histogram = fit(Histogram, float.(values); nbins = count, closed = :left) - edges = collect(histogram.edges[1]) - density = histogram.weights ./ (length(values) .* diff(edges)) - return HistogramPDF(edges, density) -end - -function _binindex(distribution::HistogramPDF, value::Real) - value < first(distribution.edges) && return 0 - value > last(distribution.edges) && return 0 - value == last(distribution.edges) && return length(distribution.density) - return searchsortedlast(distribution.edges, value) -end - -(distribution::HistogramPDF)(value::Real) = Distributions.pdf(distribution, value) -Distributions.minimum(distribution::HistogramPDF) = first(distribution.edges) -Distributions.maximum(distribution::HistogramPDF) = last(distribution.edges) -function Distributions.insupport(distribution::HistogramPDF, value::Real) - minimum(distribution) <= value <= maximum(distribution) -end - -function Distributions.pdf(distribution::HistogramPDF{T}, value::Real) where {T} - index = _binindex(distribution, value) - return index == 0 ? zero(T) : distribution.density[index] -end - -function Distributions.logpdf(distribution::HistogramPDF, value::Real) - probability = Distributions.pdf(distribution, value) - return probability > 0 ? log(probability) : -Inf -end - -function Distributions.cdf(distribution::HistogramPDF{T}, value::Real) where {T} - value < minimum(distribution) && return zero(T) - value >= maximum(distribution) && return one(T) - index = _binindex(distribution, value) - widths = diff(distribution.edges) - prior = sum( - (distribution.density[j] * widths[j] for j in 1:(index - 1)); - init = zero(T) - ) - return prior + distribution.density[index] * (value - distribution.edges[index]) -end - -struct HistogramPDFSampler{T <: Real} <: - Distributions.Sampleable{Distributions.Univariate, Distributions.Continuous} - distribution::HistogramPDF{T} - cumulative_probability::Vector{T} -end - -function Distributions.sampler(distribution::HistogramPDF) - probabilities = distribution.density .* diff(distribution.edges) - cumulative = cumsum(probabilities) - cumulative[end] = one(eltype(cumulative)) - return HistogramPDFSampler(distribution, cumulative) -end - -function Distributions.quantile(sampler::HistogramPDFSampler{T}, probability::Real) where {T} - probability <= 0 && return minimum(sampler.distribution) - probability >= 1 && return maximum(sampler.distribution) - index = searchsortedfirst(sampler.cumulative_probability, probability) - prior = index == 1 ? zero(T) : sampler.cumulative_probability[index - 1] - density = sampler.distribution.density[index] - density == 0 && return sampler.distribution.edges[index] - return sampler.distribution.edges[index] + (probability - prior) / density -end - -function Distributions.quantile(distribution::HistogramPDF, probability::Real) - 0 <= probability <= 1 || throw( - DomainError(probability, "probability must lie in [0, 1]"), - ) - Distributions.quantile(Distributions.sampler(distribution), probability) -end - -function Base.rand(rng::AbstractRNG, sampler::HistogramPDFSampler) - 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::AbstractRNG, distribution::HistogramPDF) - rand(rng, Distributions.sampler(distribution)) -end - -function _raw_moment(distribution::HistogramPDF{T}, order::Integer) where {T} - order >= 0 || throw(ArgumentError("moment order must be nonnegative")) - 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::HistogramPDF, order::Integer) - _raw_moment(distribution, order) -end -Distributions.mean(distribution::HistogramPDF) = _raw_moment(distribution, 1) -function Distributions.var(distribution::HistogramPDF) - value = _raw_moment(distribution, 2) - Distributions.mean(distribution)^2 - return max(value, zero(value)) -end -Distributions.std(distribution::HistogramPDF) = sqrt(Distributions.var(distribution)) - -function Distributions.mode(distribution::HistogramPDF) - _, index = findmax(distribution.density) - return (distribution.edges[index] + distribution.edges[index + 1]) / 2 -end - -function Distributions.modes(distribution::HistogramPDF) - maximum_density = maximum(distribution.density) - indices = findall(value -> value ≈ maximum_density, distribution.density) - return [(distribution.edges[index] + distribution.edges[index + 1]) / 2 - for - index in indices] -end - -"""Return one retained scalar cable-constant trial.""" -function trial(result::CableConstantsMC, index::Integer) - resistance_values = samples(result, :R) - index in eachindex(resistance_values) || throw(BoundsError(result, index)) - return CableConstants( - resistance_values[index], - samples(result, :L)[index], - samples(result, :C)[index] - ) -end - -"""Reconstruct one retained joint line-parameter trial.""" -function trial( - result::LineParametersMC{S, Samples, Distributions, Surrogate}, - index::Integer -) where { - S, - Samples, - Distributions, - T, - U, - D, - Basis, - Surrogate <: LineParameters{T, U, D, Basis} -} - resistance_values = samples(result, :R) - inductance_values = samples(result, :L) - capacitance_values = samples(result, :C) - conductance_values = samples(result, :G) - sample_size = size(resistance_values) - all( - size(values) == sample_size - for - values in (inductance_values, capacitance_values, conductance_values) - ) || - throw(DimensionMismatch("stored R, L, C, and G samples must have equal dimensions")) - sample_size[1] == sample_size[2] || throw( - DimensionMismatch("stored line-parameter matrices must be square"), - ) - sample_size[3] == nfrequencies(result) || throw( - DimensionMismatch("stored samples and frequencies must agree"), - ) - index in axes(resistance_values, 4) || throw(BoundsError(result, index)) - - matrix_count = sample_size[1] - frequency_count = sample_size[3] - scalar_type = eltype(resistance_values) - impedance = Array{Complex{scalar_type}, 3}( - undef, - matrix_count, - matrix_count, - frequency_count - ) - admittance = similar(impedance) - frequency_values = frequencies(result) - for k in 1:frequency_count, j in 1:matrix_count, i in 1:matrix_count - omega = 2π * frequency_values[k] - impedance[i, j, k] = resistance_values[i, j, k, index] + - im * omega * inductance_values[i, j, k, index] - admittance[i, j, k] = conductance_values[i, j, k, index] + - im * omega * capacitance_values[i, j, k, index] - end - return LineParameters( - D, - SeriesImpedance{eltype(impedance), Basis}(impedance), - ShuntAdmittance{eltype(admittance), Basis}(admittance), - frequency_values - ) -end - -function Base.rand(rng::AbstractRNG, result::Union{CableConstantsMC, LineParametersMC}) - has_samples(result) || throw( - ArgumentError("joint sampling requires mc(...; return_samples=true)"), - ) - return trial(result, rand(rng, 1:ntrials(result))) -end - -function Base.rand(result::Union{CableConstantsMC, LineParametersMC}) - rand(Random.default_rng(), result) -end diff --git a/src/uq/montecarlo.jl b/src/uq/montecarlo.jl deleted file mode 100644 index ba10fe26..00000000 --- a/src/uq/montecarlo.jl +++ /dev/null @@ -1,496 +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 `CableConstantsMC` containing R, L, and C statistics, optional samples and - distributions, and a covariance-preserving Measurements.jl surrogate. - -# 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 -) - trials === nothing || trials > 0 || throw(ArgumentError("mc: trials must be positive")) - 0 < conf < 1 || throw(ArgumentError("mc: conf must lie between zero and one")) - tol > 0 || throw(ArgumentError("mc: tol must be positive")) - print_step > 0 || throw(ArgumentError("mc: print_step must be positive")) - distribution in (:normal, :uniform) || throw( - ArgumentError("mc: distribution must be :normal or :uniform"), - ) - seed !== nothing && Random.seed!(seed) - # 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 - constants = CableConstants(des) - @inbounds begin - μR[i] = T(constants.R) - μL[i] = T(constants.L) - μC[i] = T(constants.C) - 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 - - summaries = CableConstants( - SampleSummary(μR), - SampleSummary(μL), - SampleSummary(μC) - ) - surrogate_value = _joint_cable_constants(μR, μL, μC) - - # 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 = CableConstants(pdfR, pdfL, pdfC) - end - - # Joint samples as a CableConstants of vectors, or nothing. - samples_value = return_samples ? CableConstants(μR, μL, μC) : nothing - - return CableConstantsMC( - summaries, - samples_value, - pdf_nt, - surrogate_value, - ntrials, - T(conf) - ) -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 `surrogate` 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, # histogram PDFs per R/L/C/G and frequency - per_length::Bool = true, # scale results per length - nbins::Union{Int, Nothing} = nothing -) - 0 < conf < 1 || throw(ArgumentError("mc: conf must lie between zero and one")) - tol > 0 || throw(ArgumentError("mc: tol must be positive")) - print_step > 0 || throw(ArgumentError("mc: print_step must be positive")) - distribution in (:normal, :uniform) || throw( - ArgumentError("mc: distribution must be :normal or :uniform"), - ) - seed !== nothing && Random.seed!(seed) - fvec = sbs.frequencies - nfreq = length(fvec) - all(isfinite, fvec) || throw(ArgumentError("mc: frequencies must be finite")) - any(iszero, fvec) && throw( - DomainError(fvec, "mc: L and C are undefined at zero frequency"), - ) - - 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(μ, σ)") - - 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) - source_basis = basis(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" - ), - ) - basis(lp) === source_basis || throw( - DomainError( - basis(lp), - "mc: inconsistent LineParameters basis 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"), - ) - Zscaled, Yscaled = if source_basis === :per_length - per_length ? (lp.Z.values, lp.Y.values) : - (lp.Z.values .* ws.line_length, lp.Y.values .* ws.line_length) - else - per_length ? - (lp.Z.values ./ ws.line_length, lp.Y.values ./ ws.line_length) : - (lp.Z.values, lp.Y.values) - 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{SampleSummary{U}, 3}(undef, nph, nph, nfreq) - Lstats = Array{SampleSummary{U}, 3}(undef, nph, nph, nfreq) - Gstats = Array{SampleSummary{U}, 3}(undef, nph, nph, nfreq) - Cstats = Array{SampleSummary{U}, 3}(undef, nph, nph, nfreq) - - # Optional PDFs: same 3D shape, one distribution per scalar - Rpdf = return_pdf ? Array{HistogramPDF{U}, 3}(undef, nph, nph, nfreq) : nothing - Lpdf = return_pdf ? Array{HistogramPDF{U}, 3}(undef, nph, nph, nfreq) : nothing - Gpdf = return_pdf ? Array{HistogramPDF{U}, 3}(undef, nph, nph, nfreq) : nothing - Cpdf = return_pdf ? Array{HistogramPDF{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 = SampleSummary(rvec) - sL = SampleSummary(lvec) - sG = SampleSummary(gvec) - sC = SampleSummary(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. - result_basis = per_length ? :per_length : :total - LP_meas = _joint_line_parameters( - Dlp, - Rsamp, - Lsamp, - Gsamp, - Csamp, - fvec; - basis = result_basis - ) - - @info "mc[Z,Y]: done" total=ntrials nfreq=nfreq - - statistics_value = RLCG(Rstats, Lstats, Cstats, Gstats) - distribution_value = return_pdf ? RLCG(Rpdf, Lpdf, Cpdf, Gpdf) : nothing - samples_value = return_samples ? RLCG(Rsamp, Lsamp, Csamp, Gsamp) : nothing - - return LineParametersMC( - statistics_value, - samples_value, - distribution_value, - LP_meas, - ntrials, - U(conf) - ) -end - -function _joint_cable_constants( - resistance_samples::AbstractVector{T}, - inductance_samples::AbstractVector{T}, - capacitance_samples::AbstractVector{T} -) where {T <: Real} - sample_count = length(resistance_samples) - length(inductance_samples) == sample_count == length(capacitance_samples) || throw( - DimensionMismatch("R, L, and C sample vectors must have equal lengths"), - ) - sample_count > 0 || throw(ArgumentError("at least one Monte Carlo trial is required")) - - sample_matrix = permutedims(hcat( - resistance_samples, - inductance_samples, - capacitance_samples - )) - means = vec(mean(sample_matrix; dims = 2)) - joint = if sample_count == 1 - map(value -> measurement(value, zero(T)), means) - else - factor = sample_matrix .- means - factor ./= sqrt(sample_count - 1) - latent = [measurement(zero(T), one(T)) for _ in 1:sample_count] - means + factor * latent - end - return CableConstants(joint...) -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}; - basis::Symbol = :per_length -) 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; basis) -end diff --git a/src/uq/plotspecs.jl b/src/uq/plotspecs.jl deleted file mode 100644 index 0e90db2b..00000000 --- a/src/uq/plotspecs.jl +++ /dev/null @@ -1,519 +0,0 @@ -struct MCDistributionPlotSpec <: PlotBuilder.AbstractPlotSpec end - -function _plot_exponent(series, field::Symbol) - maximum_value = 0.0 - for item in series - samples = field === :x ? item.xdata : item.ydata - samples === nothing && continue - for sample in samples - nominal = abs(value(sample)) - nominal isa Real && isfinite(nominal) && - (maximum_value = max(maximum_value, Float64(nominal))) - end - end - iszero(maximum_value) && return 0 - exponent = floor(Int, log10(maximum_value)) - return abs(exponent) < 3 ? 0 : exponent -end - -function _mc_selection(result::CableConstantsMC, quantity::Symbol, ijk) - ijk === nothing || throw(ArgumentError("CableConstantsMC does not use matrix indices")) - sample_values = has_samples(result) ? samples(result, quantity) : nothing - distribution_value = has_distributions(result) ? distribution(result, quantity) : - nothing - return sample_values, distribution_value, nothing -end - -function _mc_selection(result::LineParametersMC, quantity::Symbol, ijk) - selection = isnothing(ijk) ? (1, 1, 1) : ijk - selection isa NTuple{3, Int} || throw(ArgumentError("ijk must be a tuple (i, j, k)")) - i, j, k = selection - sample_values = has_samples(result) ? samples(result, quantity, i, j, k) : nothing - distribution_value = has_distributions(result) ? - distribution(result, quantity, i, j, k) : nothing - return sample_values, distribution_value, selection -end - -function _mc_target_unit(result, quantity::Symbol, length_unit, quantity_units) - quantity in (:R, :L, :C, :G) || - throw(ArgumentError("quantity must be :R, :L, :C, or :G")) - result isa CableConstantsMC && quantity === :G && - throw( - ArgumentError("CableConstantsMC does not contain conductance"), - ) - result_basis = result isa CableConstantsMC ? :per_length : basis(result) - resolved = UnitHandler.line_component_unit( - quantity, - result_basis; - length_unit, - quantity_units - ) - return resolved.quantity, resolved.units, resolved.scale -end - -function _scaled_distribution(distribution_value::HistogramPDF, conversion) - return HistogramPDF( - distribution_value.edges .* conversion, - distribution_value.density ./ conversion - ) -end - -struct MCSeriesKey{K} end - -function _mc_input_defaults() - return (; - quantity = :R, - ijk = nothing, - mode = :hist, - data = :samples, - length_unit = :kilo, - quantity_units = nothing, - nbins = nothing, - normalization = :none - ) -end - -function PlotBuilder.dispatch_on(::Type{MCDistributionPlotSpec}) - Union{CableConstantsMC, LineParametersMC} -end -function PlotBuilder.input_kwargs(::Type{MCDistributionPlotSpec}) - ( - :quantity, - :ijk, - :mode, - :data, - :length_unit, - :quantity_units, - :nbins, - :normalization - ) -end -PlotBuilder.renderer_kwargs(::Type{MCDistributionPlotSpec}) = (:fig_size,) -function PlotBuilder.input_defaults( - ::Type{MCDistributionPlotSpec}, - ::Union{CableConstantsMC, LineParametersMC} -) - _mc_input_defaults() -end -function PlotBuilder.renderer_defaults( - ::Type{MCDistributionPlotSpec}, - ::Union{CableConstantsMC, LineParametersMC} -) - (; fig_size = (800, 400)) -end - -function PlotBuilder.resolve_input(::Type{MCDistributionPlotSpec}, recipe::PlotBuilder.PlotRecipe) - input = recipe.input - input.mode in (:hist, :pdf, :ecdf, :qq) || throw( - ArgumentError("mode must be :hist, :pdf, :ecdf, or :qq"), - ) - input.data in (:samples, :pdf, :both) || throw( - ArgumentError("data must be :samples, :pdf, or :both"), - ) - input.nbins === nothing || input.nbins isa Int || - throw( - ArgumentError("nbins must be an integer or nothing"), - ) - input.nbins === nothing || input.nbins > 0 || - throw(ArgumentError("nbins must be positive")) - recipe.renderer.fig_size isa Tuple{Int, Int} || throw( - ArgumentError("fig_size must be a tuple of two integers"), - ) - - sample_values, distribution_value, selection = _mc_selection( - recipe.object, - input.quantity, - input.ijk - ) - tag, target, conversion = _mc_target_unit( - recipe.object, - input.quantity, - input.length_unit, - input.quantity_units - ) - values = sample_values === nothing ? nothing : collect(sample_values) .* conversion - model = distribution_value === nothing ? nothing : - _scaled_distribution(distribution_value, conversion) - bin_count = input.nbins === nothing && values !== nothing ? _auto_nbins(values) : - input.nbins - bins = if model !== nothing - model.edges - elseif values !== nothing - collect(fit(Histogram, values; nbins = bin_count, closed = :left).edges[1]) - else - Float64[] - end - effective_normalization = input.data in (:pdf, :both) ? :pdf : input.normalization - resolved = (; - values, - model, - bins, - effective_normalization, - tag, - target, - selection - ) - return PlotBuilder.PlotRecipe( - recipe.object, - merge(input, resolved), - recipe.renderer - ) -end - -function PlotBuilder.recipe_mode(::Type{MCDistributionPlotSpec}, recipe::PlotBuilder.PlotRecipe) - return Val(recipe.input.mode) -end - -function PlotBuilder.grouping_mode( - ::Type{MCDistributionPlotSpec}, - mode::Val, - recipe::PlotBuilder.PlotRecipe -) - return Val(:overlay) -end - -_mc_hist_facets(::Val{:samples}) = (MCSeriesKey{:samples}(),) -_mc_hist_facets(::Val{:pdf}) = (MCSeriesKey{:model_pdf}(),) -_mc_hist_facets(::Val{:both}) = (MCSeriesKey{:samples}(), MCSeriesKey{:model_pdf}()) - -function PlotBuilder.group_facets( - ::Type{MCDistributionPlotSpec}, - ::Val{:hist}, - recipe::PlotBuilder.PlotRecipe, - page_key -) - return _mc_hist_facets(Val(recipe.input.data)) -end - -function PlotBuilder.group_facets( - ::Type{MCDistributionPlotSpec}, - ::Val{:pdf}, - recipe::PlotBuilder.PlotRecipe, - page_key -) - return (MCSeriesKey{:model_pdf}(),) -end - -function PlotBuilder.group_facets( - ::Type{MCDistributionPlotSpec}, - ::Val{:ecdf}, - recipe::PlotBuilder.PlotRecipe, - page_key -) - return (MCSeriesKey{:model_cdf}(), MCSeriesKey{:empirical_cdf}()) -end - -function PlotBuilder.group_facets( - ::Type{MCDistributionPlotSpec}, - ::Val{:qq}, - recipe::PlotBuilder.PlotRecipe, - page_key -) - return (MCSeriesKey{:quantiles}(), MCSeriesKey{:reference}()) -end - -function _mc_values(recipe::PlotBuilder.PlotRecipe) - recipe.input.values === nothing && throw(ArgumentError("samples were not retained")) - return recipe.input.values -end - -function _mc_model(recipe::PlotBuilder.PlotRecipe) - recipe.input.model === nothing && - throw(ArgumentError("distributions were not retained")) - return recipe.input.model -end - -function _mc_cdf_grid(recipe::PlotBuilder.PlotRecipe) - model = _mc_model(recipe) - lower, upper = extrema(model.edges) - padding = iszero(upper - lower) ? one(lower) : 0.05 * (upper - lower) - return collect(range(lower - padding, upper + padding; length = 500)) -end - -function _mc_qq_values(recipe::PlotBuilder.PlotRecipe) - values = sort(_mc_values(recipe)) - model = _mc_model(recipe) - probabilities = ((1:length(values)) .- 0.5) ./ length(values) - model_values = Distributions.quantile.(Ref(model), probabilities) - return values, model_values -end - -function PlotBuilder.plot_kind( - ::Type{MCDistributionPlotSpec}, mode::Val, recipe::PlotBuilder.PlotRecipe, - page_key, view_key, ::MCSeriesKey{:samples}) - :histogram -end -function PlotBuilder.plot_kind( - ::Type{MCDistributionPlotSpec}, mode::Val, recipe::PlotBuilder.PlotRecipe, - page_key, view_key, ::MCSeriesKey{:model_pdf}) - :stairs -end -function PlotBuilder.plot_kind( - ::Type{MCDistributionPlotSpec}, mode::Val, recipe::PlotBuilder.PlotRecipe, - page_key, view_key, ::MCSeriesKey{:model_cdf}) - :line -end -function PlotBuilder.plot_kind( - ::Type{MCDistributionPlotSpec}, mode::Val, recipe::PlotBuilder.PlotRecipe, - page_key, view_key, ::MCSeriesKey{:empirical_cdf}) - :line -end -function PlotBuilder.plot_kind( - ::Type{MCDistributionPlotSpec}, mode::Val, recipe::PlotBuilder.PlotRecipe, - page_key, view_key, ::MCSeriesKey{:quantiles}) - :scatter -end -function PlotBuilder.plot_kind( - ::Type{MCDistributionPlotSpec}, mode::Val, recipe::PlotBuilder.PlotRecipe, - page_key, view_key, ::MCSeriesKey{:reference}) - :line -end - -function PlotBuilder.series_data( - ::Type{MCDistributionPlotSpec}, mode::Val, ::Val{:x}, recipe::PlotBuilder.PlotRecipe, - page_key, view_key, ::MCSeriesKey{:samples}) - return _mc_values(recipe) -end -function PlotBuilder.series_data( - ::Type{MCDistributionPlotSpec}, mode::Val, ::Val{:x}, recipe::PlotBuilder.PlotRecipe, - page_key, view_key, ::MCSeriesKey{:model_pdf}) - return _mc_model(recipe).edges -end -function PlotBuilder.series_data( - ::Type{MCDistributionPlotSpec}, mode::Val, ::Val{:y}, recipe::PlotBuilder.PlotRecipe, - page_key, view_key, ::MCSeriesKey{:model_pdf}) - model = _mc_model(recipe) - return [model.density; last(model.density)] -end -function PlotBuilder.series_data( - ::Type{MCDistributionPlotSpec}, mode::Val, ::Val{:x}, recipe::PlotBuilder.PlotRecipe, - page_key, view_key, ::MCSeriesKey{:model_cdf}) - return _mc_cdf_grid(recipe) -end -function PlotBuilder.series_data( - ::Type{MCDistributionPlotSpec}, mode::Val, ::Val{:y}, recipe::PlotBuilder.PlotRecipe, - page_key, view_key, ::MCSeriesKey{:model_cdf}) - x = _mc_cdf_grid(recipe) - return Distributions.cdf.(Ref(_mc_model(recipe)), x) -end -function PlotBuilder.series_data( - ::Type{MCDistributionPlotSpec}, mode::Val, ::Val{:x}, recipe::PlotBuilder.PlotRecipe, - page_key, view_key, ::MCSeriesKey{:empirical_cdf}) - return _mc_cdf_grid(recipe) -end -function PlotBuilder.series_data( - ::Type{MCDistributionPlotSpec}, mode::Val, ::Val{:y}, recipe::PlotBuilder.PlotRecipe, - page_key, view_key, ::MCSeriesKey{:empirical_cdf}) - x = _mc_cdf_grid(recipe) - return StatsBase.ecdf(_mc_values(recipe)).(x) -end -function PlotBuilder.series_data( - ::Type{MCDistributionPlotSpec}, mode::Val, ::Val{:x}, recipe::PlotBuilder.PlotRecipe, - page_key, view_key, ::MCSeriesKey{:quantiles}) - values, _ = _mc_qq_values(recipe) - return values -end -function PlotBuilder.series_data( - ::Type{MCDistributionPlotSpec}, mode::Val, ::Val{:y}, recipe::PlotBuilder.PlotRecipe, - page_key, view_key, ::MCSeriesKey{:quantiles}) - _, values = _mc_qq_values(recipe) - return values -end -function PlotBuilder.series_data( - ::Type{MCDistributionPlotSpec}, mode::Val, dim::Union{Val{:x}, Val{:y}}, - recipe::PlotBuilder.PlotRecipe, page_key, view_key, ::MCSeriesKey{:reference}) - sample_values, model_values = _mc_qq_values(recipe) - return collect(extrema(vcat(sample_values, model_values))) -end - -function PlotBuilder.legend_label( - ::Type{MCDistributionPlotSpec}, mode::Val, recipe::PlotBuilder.PlotRecipe, - page_key, view_key, ::MCSeriesKey{:samples}) - "samples" -end -function PlotBuilder.legend_label( - ::Type{MCDistributionPlotSpec}, mode::Val, recipe::PlotBuilder.PlotRecipe, - page_key, view_key, ::MCSeriesKey{:model_pdf}) - "model PDF" -end -function PlotBuilder.legend_label( - ::Type{MCDistributionPlotSpec}, mode::Val, recipe::PlotBuilder.PlotRecipe, - page_key, view_key, ::MCSeriesKey{:model_cdf}) - "model CDF" -end -function PlotBuilder.legend_label( - ::Type{MCDistributionPlotSpec}, mode::Val, recipe::PlotBuilder.PlotRecipe, - page_key, view_key, ::MCSeriesKey{:empirical_cdf}) - "empirical" -end -function PlotBuilder.legend_label( - ::Type{MCDistributionPlotSpec}, mode::Val, recipe::PlotBuilder.PlotRecipe, - page_key, view_key, ::MCSeriesKey{:quantiles}) - "quantiles" -end -function PlotBuilder.legend_label( - ::Type{MCDistributionPlotSpec}, mode::Val, recipe::PlotBuilder.PlotRecipe, - page_key, view_key, ::MCSeriesKey{:reference}) - "perfect fit" -end - -function PlotBuilder.series_attributes( - ::Type{MCDistributionPlotSpec}, mode::Val, recipe::PlotBuilder.PlotRecipe, - page_key, view_key, ::MCSeriesKey{:samples}) - return (; - bins = recipe.input.bins, normalization = recipe.input.effective_normalization) -end -function PlotBuilder.series_attributes( - ::Type{MCDistributionPlotSpec}, mode::Val, recipe::PlotBuilder.PlotRecipe, - page_key, view_key, ::MCSeriesKey{:model_pdf}) - return (; step = :post, color = :red, linewidth = 2) -end -function PlotBuilder.series_attributes( - ::Type{MCDistributionPlotSpec}, mode::Val, recipe::PlotBuilder.PlotRecipe, - page_key, view_key, ::MCSeriesKey{:model_cdf}) - return (; color = :red, linewidth = 2) -end -function PlotBuilder.series_attributes( - ::Type{MCDistributionPlotSpec}, mode::Val, recipe::PlotBuilder.PlotRecipe, - page_key, view_key, ::MCSeriesKey{:empirical_cdf}) - return (; color = :blue, linestyle = :dash, linewidth = 2) -end -function PlotBuilder.series_attributes( - ::Type{MCDistributionPlotSpec}, mode::Val, recipe::PlotBuilder.PlotRecipe, - page_key, view_key, ::MCSeriesKey{:quantiles}) - return (; color = :steelblue, markersize = 6) -end -function PlotBuilder.series_attributes( - ::Type{MCDistributionPlotSpec}, mode::Val, recipe::PlotBuilder.PlotRecipe, - page_key, view_key, ::MCSeriesKey{:reference}) - return (; color = :black, linestyle = :dash, linewidth = 2) -end - -function _mc_title(recipe::PlotBuilder.PlotRecipe, suffix::AbstractString) - symbol = UnitHandler.get_symbol(recipe.input.tag) - selection = recipe.input.selection - indices = selection === nothing ? "" : "[$(join(selection, ','))]" - return "$symbol$indices $suffix" -end - -function PlotBuilder.default_title( - ::Type{MCDistributionPlotSpec}, ::Val{:hist}, recipe::PlotBuilder.PlotRecipe, - page_key, view_key) - _mc_title(recipe, "histogram") -end -function PlotBuilder.default_title( - ::Type{MCDistributionPlotSpec}, ::Val{:pdf}, recipe::PlotBuilder.PlotRecipe, - page_key, view_key) - _mc_title(recipe, "probability density") -end -function PlotBuilder.default_title( - ::Type{MCDistributionPlotSpec}, ::Val{:ecdf}, recipe::PlotBuilder.PlotRecipe, - page_key, view_key) - _mc_title(recipe, "cumulative distribution") -end -function PlotBuilder.default_title( - ::Type{MCDistributionPlotSpec}, ::Val{:qq}, recipe::PlotBuilder.PlotRecipe, - page_key, view_key) - _mc_title(recipe, "Q-Q plot") -end - -function PlotBuilder.axis_quantity( - ::Type{MCDistributionPlotSpec}, mode::Val, ::Val{:x}, recipe::PlotBuilder.PlotRecipe, - page_key, view_key) - return recipe.input.tag -end -function PlotBuilder.axis_quantity( - ::Type{MCDistributionPlotSpec}, ::Val{:qq}, ::Val{:y}, recipe::PlotBuilder.PlotRecipe, - page_key, view_key) - return recipe.input.tag -end -function PlotBuilder.axis_quantity( - ::Type{MCDistributionPlotSpec}, mode::Val, ::Val{:y}, recipe::PlotBuilder.PlotRecipe, - page_key, view_key) - return UnitHandler.QuantityTag{:dimensionless}() -end -function PlotBuilder.axis_unit( - ::Type{MCDistributionPlotSpec}, mode::Val, ::Val{:x}, - quantity::UnitHandler.QuantityTag, recipe::PlotBuilder.PlotRecipe, page_key, view_key) - return recipe.input.target -end -function PlotBuilder.axis_unit( - ::Type{MCDistributionPlotSpec}, ::Val{:qq}, ::Val{:y}, - quantity::UnitHandler.QuantityTag, recipe::PlotBuilder.PlotRecipe, page_key, view_key) - return recipe.input.target -end -function PlotBuilder.axis_unit( - ::Type{MCDistributionPlotSpec}, mode::Val, ::Val{:y}, - quantity::UnitHandler.QuantityTag, recipe::PlotBuilder.PlotRecipe, page_key, view_key) - return UnitHandler.Units() -end - -function PlotBuilder.axis_label( - ::Type{MCDistributionPlotSpec}, ::Val{:qq}, ::Val{:x}, - quantity::UnitHandler.QuantityTag, - unit::UnitHandler.Units, recipe::PlotBuilder.PlotRecipe, page_key, view_key) - return "sample quantiles [$(UnitHandler.get_label(unit))]" -end -function PlotBuilder.axis_label( - ::Type{MCDistributionPlotSpec}, mode::Val, ::Val{:x}, - quantity::UnitHandler.QuantityTag, - unit::UnitHandler.Units, recipe::PlotBuilder.PlotRecipe, page_key, view_key) - return "$(UnitHandler.get_label(quantity)) [$(UnitHandler.get_label(unit))]" -end -function PlotBuilder.axis_label( - ::Type{MCDistributionPlotSpec}, ::Val{:hist}, ::Val{:y}, - quantity::UnitHandler.QuantityTag, - unit::UnitHandler.Units, recipe::PlotBuilder.PlotRecipe, page_key, view_key) - return String(recipe.input.effective_normalization) -end -function PlotBuilder.axis_label( - ::Type{MCDistributionPlotSpec}, ::Val{:pdf}, ::Val{:y}, - quantity::UnitHandler.QuantityTag, - unit::UnitHandler.Units, recipe::PlotBuilder.PlotRecipe, page_key, view_key) - return "density" -end -function PlotBuilder.axis_label( - ::Type{MCDistributionPlotSpec}, ::Val{:ecdf}, ::Val{:y}, - quantity::UnitHandler.QuantityTag, - unit::UnitHandler.Units, recipe::PlotBuilder.PlotRecipe, page_key, view_key) - return "cumulative probability" -end -function PlotBuilder.axis_label( - ::Type{MCDistributionPlotSpec}, ::Val{:qq}, ::Val{:y}, - quantity::UnitHandler.QuantityTag, - unit::UnitHandler.Units, recipe::PlotBuilder.PlotRecipe, page_key, view_key) - return "model quantiles [$(UnitHandler.get_label(unit))]" -end - -function PlotBuilder.view_key( - ::Type{MCDistributionPlotSpec}, mode::Val, recipe::PlotBuilder.PlotRecipe, - page_key, view_key) - return (; - quantity = recipe.input.quantity, - selection = recipe.input.selection, - mode = recipe.input.mode - ) -end - -function PlotBuilder.default_figsize( - ::Type{MCDistributionPlotSpec}, mode::Val, recipe::PlotBuilder.PlotRecipe, page_key) - return recipe.renderer.fig_size -end - -function PlotBuilder.axis_exponent( - ::Type{MCDistributionPlotSpec}, mode::Val, ::Val{dim}, - recipe::PlotBuilder.PlotRecipe, page_key, view_key, - series::Vector{PlotBuilder.SeriesSpec}) where {dim} - return _plot_exponent(series, dim) -end - -function PlotBuilder.page_identity( - ::Type{MCDistributionPlotSpec}, mode::Val, - recipe::PlotBuilder.PlotRecipe, page_key) - return (; - quantity = recipe.input.quantity, - selection = recipe.input.selection, - mode = recipe.input.mode, - data = recipe.input.data - ) -end diff --git a/src/uq/types.jl b/src/uq/types.jl deleted file mode 100644 index abf77a0b..00000000 --- a/src/uq/types.jl +++ /dev/null @@ -1,520 +0,0 @@ -""" - SampleSummary{T} - -Store the fixed summary statistics for one scalar Monte Carlo observable. -All fields use the observable's canonical units. -""" -struct SampleSummary{T <: Real} - "Sample mean." - mean::T - "Corrected sample standard deviation." - std::T - "Minimum sampled value." - min::T - "Five-percent quantile." - q05::T - "Median." - q50::T - "Ninety-five-percent quantile." - q95::T - "Maximum sampled value." - max::T - - function SampleSummary{T}( - mean::T, - std::T, - min::T, - q05::T, - q50::T, - q95::T, - max::T - ) where {T <: Real} - values = (mean, std, min, q05, q50, q95, max) - all(isfinite, values) || throw(ArgumentError("summary values must be finite")) - std >= zero(T) || - throw(ArgumentError("summary standard deviation must be nonnegative")) - min <= q05 <= q50 <= q95 <= max || throw( - ArgumentError("summary quantiles must be ordered between min and max"), - ) - min <= mean <= max || throw( - ArgumentError("summary mean must lie between min and max"), - ) - return new{T}(mean, std, min, q05, q50, q95, max) - end -end - -function SampleSummary( - mean::Real, std::Real, min::Real, q05::Real, q50::Real, q95::Real, max::Real) - values = promote(mean, std, min, q05, q50, q95, max) - return SampleSummary{typeof(first(values))}(values...) -end - -function Base.:(==)(left::SampleSummary, right::SampleSummary) - left.mean == right.mean && left.std == right.std && left.min == right.min && - left.q05 == right.q05 && left.q50 == right.q50 && left.q95 == right.q95 && - left.max == right.max -end - -function SampleSummary(values::AbstractVector{T}) where {T <: 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), - StatsBase.quantile(values, 0.05), - StatsBase.quantile(values, 0.50), - StatsBase.quantile(values, 0.95), - maximum(values) - ) - return SampleSummary(promoted...) -end - -""" - RLCG{T} - -Group resistance, inductance, capacitance, and conductance values with one -common storage representation. Units follow the basis of the containing -result: \\[Ω/m, H/m, F/m, S/m\\] for `:per_length`, or -\\[Ω, H, F, S\\] for `:total`. -""" -struct RLCG{T} - "Resistance values." - R::T - "Inductance values." - L::T - "Capacitance values." - C::T - "Conductance values." - G::T -end - -function Base.:(==)(left::RLCG, right::RLCG) - left.R == right.R && left.L == right.L && left.C == right.C && left.G == right.G -end - -""" - HistogramPDF(edges, density) - -Piecewise-constant univariate probability density. `edges` has one more entry -than `density`; density is normalized by the constructor. -""" -struct HistogramPDF{T <: AbstractFloat} <: ContinuousUnivariateDistribution - "Strictly increasing histogram bin edges." - edges::Vector{T} - "Piecewise-constant probability density for each bin." - density::Vector{T} - - function HistogramPDF{T}( - edges::Vector{T}, - density::Vector{T} - ) where {T <: AbstractFloat} - length(edges) == length(density) + 1 || throw( - ArgumentError("edges must contain exactly one more value than density"), - ) - isempty(density) && throw(ArgumentError("density must contain at least one bin")) - all(isfinite, edges) || throw(ArgumentError("histogram edges must be finite")) - all(isfinite, density) || throw(ArgumentError("histogram density must be finite")) - all(>=(zero(T)), density) || - throw(ArgumentError("histogram density must be nonnegative")) - copied_edges = copy(edges) - copied_density = copy(density) - widths = diff(copied_edges) - all(>(zero(T)), widths) || throw( - ArgumentError("histogram edges must be strictly increasing"), - ) - area = dot(copied_density, widths) - area > zero(area) || throw( - ArgumentError("histogram density must have positive area"), - ) - copied_density ./= area - return new{T}(copied_edges, copied_density) - end -end - -function Base.:(==)(left::HistogramPDF, right::HistogramPDF) - left.edges == right.edges && left.density == right.density -end - -""" - CableConstantsMC - -Store Monte Carlo summaries and optional retained data for scalar cable -constants. `surrogate` preserves the covariance of \\[R, L, C\\] with -`Measurement` values. Canonical units are \\[Ω/m, H/m, F/m\\]. -""" -struct CableConstantsMC{S, Samples, Distributions, Surrogate, T <: Real} - "Per-observable summary statistics." - statistics::CableConstants{S} - "Joint retained samples, or `nothing`." - samples::Samples - "Retained marginal histogram distributions, or `nothing`." - distributions::Distributions - "Covariance-preserving cable constants." - surrogate::Surrogate - "Number of Monte Carlo trials." - ntrials::Int - "Central confidence mass used by the analysis." - confidence::T - - function CableConstantsMC{S, Samples, Distributions, Surrogate, T}( - statistics::CableConstants{S}, - samples::Samples, - distributions::Distributions, - surrogate::Surrogate, - ntrials::Int, - confidence::T - ) where {S, Samples, Distributions, Surrogate, T <: Real} - return new{S, Samples, Distributions, Surrogate, T}( - statistics, - samples, - distributions, - surrogate, - ntrials, - confidence - ) - end -end - -function CableConstantsMC( - statistics::CableConstants{S}, - samples::Samples, - distributions::Distributions, - surrogate::Surrogate, - ntrials::Integer, - confidence::T -) where {S, Samples, Distributions, Surrogate, T <: Real} - ntrials > 0 || throw(ArgumentError("ntrials must be positive")) - 0 < confidence < 1 || throw(ArgumentError("confidence must lie between zero and one")) - all(value -> value isa SampleSummary, (statistics.R, statistics.L, statistics.C)) || - throw(ArgumentError("cable-constant statistics must contain SampleSummary values")) - if samples !== nothing - samples isa CableConstants || throw( - ArgumentError("cable-constant samples must be CableConstants or nothing"), - ) - all(value -> value isa AbstractVector{<:Real}, (samples.R, samples.L, samples.C)) || - throw(ArgumentError("retained cable-constant samples must be real vectors")) - all(length(values) == ntrials for values in (samples.R, samples.L, samples.C)) || - throw(DimensionMismatch("retained cable-constant samples must match ntrials")) - end - if distributions !== nothing - distributions isa CableConstants || throw( - ArgumentError("cable-constant distributions must be CableConstants or nothing"), - ) - all(value -> value isa HistogramPDF, ( - distributions.R, - distributions.L, - distributions.C - )) || - throw(ArgumentError("cable-constant distributions must contain HistogramPDF values")) - end - surrogate isa CableConstants || throw( - ArgumentError("the cable-constant surrogate must be a CableConstants value"), - ) - all(value -> value isa Measurement, (surrogate.R, surrogate.L, surrogate.C)) || - throw( - ArgumentError( - "the cable-constant surrogate must contain covariance-preserving Measurement values", - ), - ) - return CableConstantsMC{S, Samples, Distributions, Surrogate, T}( - statistics, - samples, - distributions, - surrogate, - Int(ntrials), - confidence - ) -end - -""" - LineParametersMC - -Store Monte Carlo summaries and optional retained data for frequency-dependent -line parameters. `surrogate` is a covariance-preserving [`LineParameters`](@ref) -whose canonical units and basis match this result. -""" -struct LineParametersMC{S, Samples, Distributions, Surrogate, T <: Real} - "RLCG summary tensors with dimensions conductor × conductor × frequency." - statistics::RLCG{S} - "Joint RLCG sample tensors, or `nothing`." - samples::Samples - "Marginal RLCG histogram distributions, or `nothing`." - distributions::Distributions - "Covariance-preserving line parameters." - surrogate::Surrogate - "Number of Monte Carlo trials." - ntrials::Int - "Central confidence mass used by the analysis." - confidence::T - - function LineParametersMC{S, Samples, Distributions, Surrogate, T}( - statistics::RLCG{S}, - samples::Samples, - distributions::Distributions, - surrogate::Surrogate, - ntrials::Int, - confidence::T - ) where {S, Samples, Distributions, Surrogate, T <: Real} - return new{S, Samples, Distributions, Surrogate, T}( - statistics, - samples, - distributions, - surrogate, - ntrials, - confidence - ) - end -end - -function LineParametersMC( - statistics::RLCG{S}, - samples::Samples, - distributions::Distributions, - surrogate::Surrogate, - ntrials::Integer, - confidence::T -) where {S, Samples, Distributions, Surrogate, T <: Real} - ntrials > 0 || throw(ArgumentError("ntrials must be positive")) - 0 < confidence < 1 || throw(ArgumentError("confidence must lie between zero and one")) - all(value -> value isa AbstractArray{<:SampleSummary, 3}, - ( - statistics.R, - statistics.L, - statistics.C, - statistics.G - )) || - throw(ArgumentError("R, L, C, and G statistics must be three-dimensional SampleSummary arrays")) - statistic_size = size(statistics.R) - all(size(values) == statistic_size - for values in (statistics.L, statistics.C, statistics.G)) || - throw(DimensionMismatch("R, L, C, and G statistics must have equal dimensions")) - if samples !== nothing - samples isa RLCG || throw( - ArgumentError("line-parameter samples must be RLCG or nothing"), - ) - all(value -> value isa AbstractArray{<:Real, 4}, ( - samples.R, - samples.L, - samples.C, - samples.G - )) || - throw(ArgumentError("retained R, L, C, and G samples must be real four-dimensional arrays")) - sample_size = size(samples.R) - length(sample_size) == 4 || - throw(DimensionMismatch("retained line-parameter samples must be four-dimensional")) - sample_size[4] == ntrials || throw( - DimensionMismatch("retained line-parameter samples must match ntrials"), - ) - all(size(values) == sample_size for values in (samples.L, samples.C, samples.G)) || - throw(DimensionMismatch("R, L, C, and G samples must have equal dimensions")) - sample_size[1:3] == statistic_size || throw( - DimensionMismatch("sample and statistic dimensions must agree"), - ) - end - if distributions !== nothing - distributions isa RLCG || throw( - ArgumentError("line-parameter distributions must be RLCG or nothing"), - ) - all(value -> value isa AbstractArray{<:HistogramPDF, 3}, - ( - distributions.R, - distributions.L, - distributions.C, - distributions.G - )) || - throw(ArgumentError("R, L, C, and G distributions must be three-dimensional HistogramPDF arrays")) - all(size(values) == statistic_size - for values in ( - distributions.R, - distributions.L, - distributions.C, - distributions.G - )) || throw(DimensionMismatch("distribution and statistic dimensions must agree")) - end - surrogate isa LineParameters || throw( - ArgumentError("the line-parameter surrogate must be a LineParameters value"), - ) - eltype(surrogate.Z) <: Complex{<:Measurement} || throw( - ArgumentError( - "the line-parameter surrogate must contain covariance-preserving Complex{Measurement} values", - ), - ) - size(surrogate.Z) == statistic_size || throw( - DimensionMismatch("surrogate and statistic dimensions must agree"), - ) - return LineParametersMC{S, Samples, Distributions, Surrogate, T}( - statistics, - samples, - distributions, - surrogate, - Int(ntrials), - confidence - ) -end - -const _CABLE_CONSTANT_QUANTITIES = (:R, :L, :C) -const _LINE_PARAMETER_QUANTITIES = (:R, :L, :C, :G) - -@inline function _quantity(container, quantity::Symbol, supported) - quantity in supported || throw( - ArgumentError("unsupported quantity :$quantity; expected one of $(supported)"), - ) - return getproperty(container, quantity) -end - -@inline _selection(value, ::Tuple{}) = value -@inline _selection(value::AbstractArray{<:Any, 3}, indices::Tuple{Int, Int}) = view( - value, indices[1], indices[2], :) -@inline _selection(value::AbstractArray{<:Any, 3}, indices::Tuple{ - Int, Int, Any}) = value[indices...] -@inline _selection(value::AbstractArray{<:Any, 4}, indices::Tuple{Int, Int}) = view( - value, indices[1], indices[2], :, :) -@inline _selection(value::AbstractArray{<:Any, 4}, indices::Tuple{Int, Int, Any}) = view( - value, indices[1], indices[2], indices[3], :) -@inline _selection(value, indices::Tuple) = getindex(value, indices...) - -function statistics(result::CableConstantsMC, quantity::Symbol, indices...) - _selection(_quantity(result.statistics, quantity, _CABLE_CONSTANT_QUANTITIES), indices) -end -function statistics(result::LineParametersMC, quantity::Symbol, indices...) - _selection(_quantity(result.statistics, quantity, _LINE_PARAMETER_QUANTITIES), indices) -end -statistics(result::Union{CableConstantsMC, LineParametersMC}) = result.statistics - -@inline _summary_field(summary::SampleSummary, field::Symbol) = getproperty(summary, field) -function _summary_field(summaries::AbstractArray, field::Symbol) - map(summary -> getproperty(summary, field), summaries) -end - -function Statistics.mean(result::Union{CableConstantsMC, LineParametersMC}, quantity::Symbol, indices...) - _summary_field(statistics(result, quantity, indices...), :mean) -end -function Statistics.std(result::Union{CableConstantsMC, LineParametersMC}, quantity::Symbol, indices...) - _summary_field(statistics(result, quantity, indices...), :std) -end - -has_samples(result::Union{CableConstantsMC, LineParametersMC}) = result.samples !== nothing -function has_distributions(result::Union{CableConstantsMC, LineParametersMC}) - result.distributions !== nothing -end - -function samples(result::CableConstantsMC, quantity::Symbol, indices...) - has_samples(result) || throw( - ArgumentError("samples were not retained; rerun mc(...; return_samples=true)"), - ) - return _selection(_quantity(result.samples, quantity, _CABLE_CONSTANT_QUANTITIES), indices) -end - -function samples(result::LineParametersMC, quantity::Symbol, indices...) - has_samples(result) || throw( - ArgumentError("samples were not retained; rerun mc(...; return_samples=true)"), - ) - return _selection(_quantity(result.samples, quantity, _LINE_PARAMETER_QUANTITIES), indices) -end - -function distribution(result::CableConstantsMC, quantity::Symbol, indices...) - has_distributions(result) || throw( - ArgumentError("distributions were not retained; rerun mc(...; return_pdf=true)"), - ) - return _selection( - _quantity(result.distributions, quantity, _CABLE_CONSTANT_QUANTITIES), - indices - ) -end - -function distribution(result::LineParametersMC, quantity::Symbol, indices...) - has_distributions(result) || throw( - ArgumentError("distributions were not retained; rerun mc(...; return_pdf=true)"), - ) - return _selection( - _quantity(result.distributions, quantity, _LINE_PARAMETER_QUANTITIES), - indices - ) -end - -surrogate(result::Union{CableConstantsMC, LineParametersMC}) = result.surrogate -basis(::CableConstantsMC) = :per_length -ntrials(result::Union{CableConstantsMC, LineParametersMC}) = result.ntrials -confidence(result::Union{CableConstantsMC, LineParametersMC}) = result.confidence -frequencies(result::LineParametersMC) = frequencies(result.surrogate) -function domain( - ::LineParametersMC{S, - Samples, - Distributions, - Surrogate}, -) where { - S, - Samples, - Distributions, - T, - U, - D, - Basis, - Surrogate <: LineParameters{T, U, D, Basis} -} - D -end -function basis( - ::LineParametersMC{S, - Samples, - Distributions, - Surrogate}, -) where { - S, - Samples, - Distributions, - T, - U, - D, - Basis, - Surrogate <: LineParameters{T, U, D, Basis} -} - Basis -end -nconductors(result::LineParametersMC) = nconductors(result.surrogate) -nfrequencies(result::LineParametersMC) = nfrequencies(result.surrogate) - -function _fixed_quantile(summary::SampleSummary, probability::Real) - probability == 0.05 && return summary.q05 - probability == 0.50 && return summary.q50 - probability == 0.95 && return summary.q95 - return nothing -end - -function StatsBase.quantile( - result::Union{CableConstantsMC, LineParametersMC}, - quantity::Symbol, - probability::Real, - indices... -) - 0 <= probability <= 1 || throw(ArgumentError("probability must lie in [0, 1]")) - summary = statistics(result, quantity, indices...) - if summary isa SampleSummary - fixed = _fixed_quantile(summary, probability) - fixed !== nothing && return fixed - elseif probability in (0.05, 0.50, 0.95) - field = probability == 0.05 ? :q05 : probability == 0.50 ? :q50 : :q95 - return _summary_field(summary, field) - end - - if has_samples(result) - values = samples(result, quantity, indices...) - values isa AbstractVector || throw( - ArgumentError("select one scalar observable before requesting an arbitrary quantile"), - ) - return StatsBase.quantile(values, probability) - elseif has_distributions(result) - value = distribution(result, quantity, indices...) - value isa HistogramPDF || throw( - ArgumentError("select one scalar distribution before requesting an arbitrary quantile"), - ) - return Distributions.quantile(value, probability) - end - throw( - ArgumentError( - "only 0.05, 0.50, and 0.95 are stored; retain samples or distributions for other probabilities", - ), - ) -end diff --git a/src/utils/Utils.jl b/src/utils/Utils.jl index e178af70..5591e91b 100644 --- a/src/utils/Utils.jl +++ b/src/utils/Utils.jl @@ -21,6 +21,7 @@ export resolve_T, coerce_to_T, is_headless, is_in_testset, display_path export set_verbosity! export to_nominal, + uncertainty_value, to_certain, percent_to_uncertain, bias_to_uncertain, @@ -32,8 +33,7 @@ export _to_σ, _bessel_diff, symtrans!, line_transpose! # Module-specific dependencies using ..Commons -using ..UncertainBessels: besselk -using Measurements: Measurement, value, uncertainty, measurement, ±, Measurements, result +using SpecialFunctions: besselk using Statistics using LinearAlgebra @@ -63,7 +63,6 @@ $(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 @@ -91,9 +90,7 @@ y = 10.0 result = $(FUNCTIONNAME)(y) # Output: 10.0 ``` """ -function to_certain(value) - return value isa Measurement ? (Measurements.value(value) ± 0.0) : value -end +to_certain(value) = value """ $(TYPEDSIGNATURES) @@ -118,8 +115,8 @@ $(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) +function percent_to_uncertain(::Any, ::Any) + throw(ArgumentError("percent_to_uncertain requires the Measurements.jl extension")) end """ @@ -152,14 +149,8 @@ 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) +function bias_to_uncertain(::Any, ::Any) + throw(ArgumentError("bias_to_uncertain requires the Measurements.jl extension")) end """ @@ -188,13 +179,7 @@ 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 +to_upper(::Number) = NaN """ $(TYPEDSIGNATURES) @@ -222,13 +207,7 @@ 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 +to_lower(::Number) = NaN """ $(TYPEDSIGNATURES) @@ -256,40 +235,21 @@ 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 +percent_error(::Number) = NaN + +"""Return absolute scalar uncertainty; deterministic values return zero.""" +uncertainty_value(value::Number) = zero(to_nominal(value)) +uncertainty_value(::Any) = 0.0 @inline _nudge_float(x::AbstractFloat) = isfinite(x) && x == trunc(x) ? nextfloat(x) : x #redundant and I dont care function _coerce_args_to_T(args...) - any(x -> x isa Measurement, args) ? Measurement{BASE_FLOAT} : BASE_FLOAT + return resolve_T(args...) end -# 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 +_coerce_scalar_to_T(x, ::Type{T}) where {T} = coerce_to_T(x, T) -# 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 +_coerce_array_to_T(A::AbstractArray, ::Type{T}) where {T} = coerce_to_T(A, T) """ $(TYPEDSIGNATURES) diff --git a/src/utils/typecoercion.jl b/src/utils/typecoercion.jl index 3a6277e8..25d12f09 100644 --- a/src/utils/typecoercion.jl +++ b/src/utils/typecoercion.jl @@ -1,331 +1,82 @@ -""" -$(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. +"""Optional numeric extensions specialize this direct scalar trait.""" +_direct_optional_scalar_type(::Type) = nothing -# 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 -function _hasmeas_type(::Type{T}) where {T <: Tuple} - any(_hasmeas_type, Base.unwrap_unionall(T).parameters) +function _optional_scalar_type(type::Type) + return _optional_scalar_type(type, Set{Any}()) end -_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 +function _optional_scalar_type(type::Type, visited::Set{Any}) + type in visited && return nothing + push!(visited, type) + direct = _direct_optional_scalar_type(type) + direct === nothing || return direct + unwrapped = Base.unwrap_unionall(type) + unwrapped isa DataType || return nothing + for parameter in unwrapped.parameters + parameter isa Type || continue + candidate = _optional_scalar_type(parameter, visited) + candidate === nothing || return candidate + end + return nothing +end -$(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 -function _hascomplex_type(::Type{T}) where {T <: Tuple} - any(_hascomplex_type, Base.unwrap_unionall(T).parameters) -end -_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{<:AbstractArray{T}}) where {T} = _hascomplex_type(T) +_hascomplex_type(::Type{T}) where {T<:Tuple} = any( + parameter -> parameter isa Type && _hascomplex_type(parameter), + Base.unwrap_unionall(T).parameters, +) +_hascomplex_type(::Type{NamedTuple{Names,T}}) where {Names,T} = + _hascomplex_type(T) _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 + resolve_T(args...) -- 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}} -``` +Resolve the common scalar used by strict materialized constructors. Core +numbers normalize to `Float64`; optional scalar packages participate through +extension methods for `_optional_scalar_type`. """ 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 + optional = nothing + for type in types + optional = _optional_scalar_type(type) + optional === nothing || break end + scalar = optional === nothing ? BASE_FLOAT : optional + return any(_hascomplex_type, types) ? Complex{scalar} : scalar 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) -function _coerce_elt_to_T(m::Measurement, ::Type{R}) where {R <: AbstractFloat} - convert(R, value(m)) -end -_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. +_coerce_elt_to_T(value::Number, ::Type{T}) where {T<:AbstractFloat} = + convert(T, value) +_coerce_elt_to_T(value::Bool, ::Type{T}) where {T<:AbstractFloat} = value +_coerce_elt_to_T(::Nothing, ::Type) = nothing +_coerce_elt_to_T(::Missing, ::Type) = missing +_coerce_elt_to_T(value::Union{Bool,Symbol,String,Function,DataType}, ::Type) = value +_coerce_elt_to_T(value, ::Type) = value -# 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) - -""" 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 -function 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)) +coerce_to_T(value::T, ::Type{T}) where {T} = value +coerce_to_T(value::Real, ::Type{C}) where {P,C<:Complex{P}} = + C(coerce_to_T(value, P)) +coerce_to_T(value::Complex{P}, ::Type{Complex{P}}) where {P} = value +function coerce_to_T(value::Complex, ::Type{Complex{P}}) where {P} + return Complex{P}(coerce_to_T(real(value), P), coerce_to_T(imag(value), P)) end - -# 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 -function 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))) +coerce_to_T(value::Complex, ::Type{T}) where {T<:Real} = + coerce_to_T(real(value), T) +coerce_to_T(value::Number, ::Type{T}) where {T} = _coerce_elt_to_T(value, T) +coerce_to_T(values::AbstractArray{T}, ::Type{T}) where {T} = values +coerce_to_T(values::AbstractArray, ::Type{T}) where {T} = + map(value -> coerce_to_T(value, T), values) +coerce_to_T(values::Tuple{Vararg{T}}, ::Type{T}) where {T} = values +coerce_to_T(values::Tuple, ::Type{T}) where {T} = + map(value -> coerce_to_T(value, T), values) +function coerce_to_T(tuple_value::NamedTuple{Names}, ::Type{T}) where {Names,T} + return NamedTuple{Names}( + map(value -> coerce_to_T(value, T), values(tuple_value)), + ) end - -# --- 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) +coerce_to_T(value, ::Type{T}) where {T} = _coerce_elt_to_T(value, T) diff --git a/src/validation/Validation.jl b/src/validation/Validation.jl index bee0ab03..cff391b8 100644 --- a/src/validation/Validation.jl +++ b/src/validation/Validation.jl @@ -1,7 +1,10 @@ """ 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. +The [`Validation`](@ref) module implements a trait-driven, three-phase input +checking sequence for component constructors in `LineCableModels`. Inputs are +first *sanitized* (arity and shape checks), then *parsed* into their normalized +numeric representation, and finally validated by a generated set of rules. # Overview @@ -150,7 +153,8 @@ 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`). +Trait predicate that defines admissible raw radius inputs during `sanitize`. +Only real, non-complex numeric radii are accepted. # Arguments @@ -226,7 +230,7 @@ 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. +Default policy for **outer** radius inputs (annular shells): accept real numbers. # Arguments @@ -252,7 +256,7 @@ is_radius_input(::Type{T}, ::Val{:r_ex}, ::Any) where {T} = false """ $(TYPEDSIGNATURES) -Internal helper that canonicalizes `keyword_defaults(T)` into a `NamedTuple` +Internal helper that normalizes `keyword_defaults(T)` into a `NamedTuple` keyed by `keyword_fields(T)`. Accepts: - `()` → returns an empty `NamedTuple()`. @@ -314,7 +318,7 @@ 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. +Performs input checks and shapes the input into a `NamedTuple`. Responsibilities: arity validation, positional→named mapping, required field presence, and acceptance of real numeric radius inputs via `is_radius_input` when `has_radii(T)` is true. # Arguments @@ -401,7 +405,9 @@ 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. +Parse and normalize inputs produced by [`sanitize`](@ref) into the form expected +by the rules. The default is identity; component code may override this method +for component-specific normalization. # Arguments @@ -444,7 +450,9 @@ 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. +Runs the full validation sequence for a component type: `sanitize` (arity and +input checks), `parse` (normalization), then application of the generated +rule set. Intended to be called from convenience constructors. # Arguments diff --git a/src/validation/rules.jl b/src/validation/rules.jl index 3d69197a..bdb93b94 100644 --- a/src/validation/rules.jl +++ b/src/validation/rules.jl @@ -126,7 +126,7 @@ 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. +Rule that enforces that a field has already been normalized to a numeric value during parsing. $(TYPEDFIELDS) """ diff --git a/test/datamodel.jl b/test/datamodel.jl index bfbf1d21..c5fd415c 100644 --- a/test/datamodel.jl +++ b/test/datamodel.jl @@ -166,7 +166,7 @@ end println("Constructing core conductor group...") material_alu = get(materials, "aluminum") - core = ConductorGroup(CircStrands(0.0, Diameter(d_w), 1, 0.0, material_alu)) + core = ConductorGroup(CircStrands(0.0, d_w / 2, 1, 0.0, material_alu)) @test core isa ConductorGroup @test length(core.layers) == 1 @test core.r_in == 0 @@ -174,20 +174,20 @@ end @test core.resistance > 0 @test core.gmr > 0 - add!(core, CircStrands, Diameter(d_w), 6, 15.0, material_alu) + add!(core, CircStrands, d_w / 2, 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) + add!(core, CircStrands, d_w / 2, 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) + add!(core, CircStrands, d_w / 2, 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) + add!(core, CircStrands, d_w / 2, 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 @@ -199,7 +199,7 @@ end 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)) + main_insu = InsulatorGroup(Semicon(core.r_ex, material_sc_tape; thickness=t_sct)) @test main_insu isa InsulatorGroup @test length(main_insu.layers) == 1 @test main_insu.r_in ≈ final_core_radius @@ -207,24 +207,24 @@ end # Inner semiconductor material_sc1 = get(materials, "semicon1") - add!(main_insu, Semicon, Thickness(t_sc_in), material_sc1) + add!(main_insu, Semicon, material_sc1; thickness=t_sc_in) @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) + add!(main_insu, Insulator, material_pe; thickness=t_ins) @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) + add!(main_insu, Semicon, material_sc2; thickness=t_sc_out) @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) + add!(main_insu, Semicon, material_sc_tape; thickness=t_sct) @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 @@ -255,8 +255,8 @@ end material_cu = get(materials, "copper") screen_con = ConductorGroup( CircStrands( - main_insu, - Diameter(d_ws), + main_insu.r_ex, + d_ws / 2, num_sc_wires, lay_ratio_screen, material_cu @@ -270,17 +270,17 @@ end add!( screen_con, Strip, - Thickness(t_cut), w_cut, lay_ratio_screen, - material_cu + material_cu; + thickness=t_cut, ) @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)) + screen_insu = InsulatorGroup(Semicon(screen_con.r_ex, material_wbt; thickness=t_wbt)) @test screen_insu.r_ex ≈ final_screen_con_radius + t_wbt final_screen_insu_radius = screen_insu.r_ex @@ -294,17 +294,17 @@ end 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)) + jacket_con = ConductorGroup(Tubular(screen_insu.r_ex, material_alu; thickness=t_alt)) @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)) + jacket_insu = InsulatorGroup(Insulator(jacket_con.r_ex, material_pe; thickness=t_pet)) @test jacket_insu.r_ex ≈ final_jacket_con_radius + t_pet # PE jacket - add!(jacket_insu, Insulator, Thickness(t_jac), material_pe) + add!(jacket_insu, Insulator, material_pe; thickness=t_jac) @test jacket_insu.r_ex ≈ final_jacket_con_radius + t_pet + t_jac final_jacket_insu_radius = jacket_insu.r_ex @@ -317,7 +317,9 @@ end final_jacket_insu_radius println("Checking DataFrame...") - @test DataFrame(cable_design, :baseparams) isa DataFrame + cable_constants = compute!(CableConstantsProblem(cable_design), Formulation()) + @test DataFrame(cable_constants) isa DataFrame + @test_throws ArgumentError DataFrame(cable_design, :baseparams) @test DataFrame(cable_design, :components) isa DataFrame @test DataFrame(cable_design, :detailed) isa DataFrame @@ -692,18 +694,9 @@ end # Reuse the fully constructed cable_design println(" Testing DataFrame...") - df_core = DataFrame(cable_design, :baseparams) + df_core = DataFrame(compute!(CableConstantsProblem(cable_design), Formulation())) @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 names(df_core) == ["parameter", "value", "unit"] @test nrow(df_core) == 3 df_comp = DataFrame(cable_design, :components) diff --git a/test/plotting.jl b/test/plotting.jl index 8646128d..2adefc82 100644 --- a/test/plotting.jl +++ b/test/plotting.jl @@ -45,52 +45,53 @@ ) rlcg = Makie.plot( - parameters; - mode = :RLCG, + parameters, + (R, L, G, C); backend = :cairo, display_plot = false, open_export = false ) cartesian = Makie.plot( parameters; - mode = :ZY, - coord = :cart, backend = :cairo, display_plot = false ) polar = Makie.plot( - parameters; - mode = :ZY, - coord = :polar, + parameters, + (abs, angle); backend = :cairo, display_plot = false ) @test rlcg isa Vector{UIPlot} @test cartesian isa Vector{UIPlot} @test polar isa Vector{UIPlot} - @test length(rlcg) == 4 - @test length(cartesian) == 4 - @test length(polar) == 4 + @test length(rlcg) == 2 + @test length(cartesian) == 2 + @test length(polar) == 2 + @test all(handle -> length(handle.panels) == 2, cartesian) + @test all(handle -> length(handle.panels) == 2, polar) + @test all(handle -> length(handle.panels) == 2, rlcg) @test rlcg[1].context !== rlcg[2].context @test rlcg[1].context.status !== rlcg[2].context.status series_plots = Makie.plot( - parameters.Z, + Z(parameters), frequency; - mode = :ZY, backend = :cairo, display_plot = false ) shunt_plots = Makie.plot( - parameters.Y, - frequency; - mode = :RLCG, + Y(parameters), + frequency, + (G, C); backend = :cairo, display_plot = false ) @test series_plots isa Vector{UIPlot} @test shunt_plots isa Vector{UIPlot} - @test length(series_plots) == 2 - @test length(shunt_plots) == 2 + @test length(series_plots) == 1 + @test length(shunt_plots) == 1 + @test length(only(series_plots).panels) == 2 + @test length(only(shunt_plots).panels) == 2 test_golden(first(rlcg), "line_rlcg") test_golden(first(cartesian), "line_zy_cartesian") test_golden(first(polar), "line_zy_polar") @@ -105,18 +106,18 @@ frequency ) measurement_plots = Makie.plot( - measurement_parameters; - mode = :RLCG, + measurement_parameters, + (R, L, G, C); backend = :cairo, display_plot = false ) measurement_plot = first(measurement_plots) - @test length(only(measurement_plot.panels).plots) > - length(only(measurement_plot.page.views).series) + @test length(first(measurement_plot.panels).plots) > + length(first(measurement_plot.page.views).series) test_golden(measurement_plot, "line_measurements") - measurement_conductance = measurement_plots[3] - measurement_conductance_axis = only(measurement_conductance.panels).axis + measurement_conductance = measurement_plots[2] + measurement_conductance_axis = first(measurement_conductance.panels).axis measurement_limits = measurement_conductance_axis.finallimits[] measurement_ymin = measurement_limits.origin[2] measurement_ymax = measurement_ymin + measurement_limits.widths[2] @@ -140,7 +141,7 @@ @test handle.controls[:reset].buttoncolor[] == Makie.RGBf(0.94, 0.94, 0.94) @test occursin("\\ue5d5", sprint(show, handle.controls[:reset].label[])) @test occursin("\\ue161", sprint(show, handle.controls[:export_svg].label[])) - line_axis = only(handle.panels).axis + line_axis = first(handle.panels).axis initial_line_limits = line_axis.finallimits[] initial_xlabel = line_axis.xlabel[] handle.controls[:xlog].active[] = true @@ -161,8 +162,11 @@ legend = handle.controls[:legend] first_entry = first(last(first(legend.entrygroups[]))) Makie.toggle_visibility!(first_entry) - @test any(plot_object -> !plot_object.visible[], only(handle.panels).plots) - Makie.xlims!(only(handle.panels).axis, 100.0, 300.0) + @test any( + plot_object -> !plot_object.visible[], + Iterators.flatten(panel.plots for panel in handle.panels), + ) + Makie.xlims!(first(handle.panels).axis, 100.0, 300.0) ui_components = Base.get_extension( LineCableModels, :LineCableModelsMakieExt @@ -181,17 +185,20 @@ @test publication_export_theme[:Axis][:titlesize][] == 15 @test publication_export_theme[:Axis][:xticklabelsize][] == 14 current_page = ui_components._current_page(handle) - current_view = only(current_page.views) + current_view = first(current_page.views) @test current_view.xaxis.scale === :log10 @test current_view.yaxis.scale === :log10 @test any(series -> !series.visible, current_view.series) @test current_view.limits !== nothing @test collect(current_view.limits[1]) ≈ [100.0, 300.0] Makie.toggle_visibility!(first_entry) - @test all(plot_object -> plot_object.visible[], only(handle.panels).plots) + @test all( + plot_object -> plot_object.visible[], + Iterators.flatten(panel.plots for panel in handle.panels), + ) susceptance_handle = last(cartesian) - susceptance_axis = only(susceptance_handle.panels).axis + susceptance_axis = last(susceptance_handle.panels).axis @test susceptance_axis.ylabel[] isa Makie.RichText @test occursin("−3", sprint(show, susceptance_axis.ylabel[])) initial_susceptance_limits = susceptance_axis.finallimits[] @@ -218,8 +225,8 @@ @test susceptance_axis.ylabel[] isa Makie.RichText @test susceptance_axis.finallimits[] == initial_susceptance_limits - conductance_handle = rlcg[3] - conductance_axis = only(conductance_handle.panels).axis + conductance_handle = rlcg[2] + conductance_axis = first(conductance_handle.panels).axis conductance_handle.controls[:ylog].active[] = true conductance_limits = conductance_axis.finallimits[] @test conductance_limits.origin[2] == 1.0e-6 @@ -234,7 +241,8 @@ @test conductance_ticks == [1.0e-6, 1.0e-5] @test all(label -> label isa Makie.RichText, conductance_labels) - capacitance_axis = only(rlcg[4].panels).axis + conductance_handle.controls[:ylog].active[] = false + capacitance_axis = last(rlcg[2].panels).axis capacitance_limits = capacitance_axis.finallimits[] @test capacitance_limits.origin[2] == 0.38 @test capacitance_limits.origin[2] + capacitance_limits.widths[2] ≈ 0.42 @@ -267,7 +275,7 @@ @test first_default != second_default @test dirname(first_default) == directory @test occursin( - r"^series_resistance_\d{8}_\d{6}(?:_\d+)?\.svg$", + r"^series_impedance_\d{8}_\d{6}(?:_\d+)?\.svg$", basename(first_default) ) @test occursin("rgb(100%, 100%, 100%)", read(first_default, String)) @@ -295,31 +303,32 @@ rm(fallback_export) summary = SampleSummary([1.0, 2.0, 3.0, 4.0]) - distribution_model = HistogramPDF([1.0, 3.0, 5.0], [0.25, 0.25]) - mc_result = CableConstantsMC( + histogram = HistogramPDF([1.0, 3.0, 5.0], [0.25, 0.25]) + mc_result = MonteCarloResult( + CableConstants(2.5, 2.5, 2.5), CableConstants(summary, summary, summary), CableConstants( [1.0, 2.0, 3.0, 4.0], [1.0, 2.0, 3.0, 4.0], - [1.0, 2.0, 3.0, 4.0] - ), - CableConstants(distribution_model, distribution_model, distribution_model), - CableConstants( - measurement(2.5, 0.0), - measurement(2.5, 0.0), - measurement(2.5, 0.0) + [1.0, 2.0, 3.0, 4.0], ), + CableConstants(histogram, histogram, histogram), + nothing, 4, - 0.95 + 0.95, + 0.02, + :normal, + UInt64(1), + (hash="plot-fixture",), ) for mode in (:hist, :pdf, :ecdf, :qq) mc_plot = Makie.plot( mc_result, :R; mode, - data = :both, - backend = :cairo, - display_plot = false + data=:both, + backend=:cairo, + display_plot=false, ) @test mc_plot isa UIPlot @test !haskey(mc_plot.controls, :xlog) @@ -329,47 +338,52 @@ line_samples = reshape(collect(1.0:12.0), 1, 1, 3, 4) summarize(values) = map( - index -> SampleSummary(view(values, index.I..., :)), - CartesianIndices(size(values)[1:3]) + index -> SampleSummary(collect(view(values, index.I..., :))), + CartesianIndices(size(values)[1:3]), ) line_statistics = RLCG( summarize(line_samples), summarize(line_samples .* 1.0e-3), summarize(line_samples .* 1.0e-6), - summarize(line_samples .* 1.0e-4) + summarize(line_samples .* 1.0e-4), ) - line_distributions = RLCG( - fill(distribution_model, 1, 1, 3), - fill(distribution_model, 1, 1, 3), - fill(distribution_model, 1, 1, 3), - fill(distribution_model, 1, 1, 3) + line_histograms = RLCG( + fill(histogram, 1, 1, 3), + fill(histogram, 1, 1, 3), + fill(histogram, 1, 1, 3), + fill(histogram, 1, 1, 3), ) - line_mc = LineParametersMC( + line_mc = MonteCarloResult( + LineParameters( + Z(parameters)[1:1, 1:1, :], + Y(parameters)[1:1, 1:1, :], + frequency, + ), line_statistics, RLCG( line_samples, line_samples .* 1.0e-3, line_samples .* 1.0e-6, - line_samples .* 1.0e-4 - ), - line_distributions, - LineParameters( - measurement_parameters.Z.values[1:1, 1:1, :], - measurement_parameters.Y.values[1:1, 1:1, :], - frequency + line_samples .* 1.0e-4, ), + line_histograms, + nothing, 4, - 0.95 + 0.95, + 0.02, + :normal, + UInt64(2), + (hash="line-plot-fixture",), ) for mode in (:hist, :pdf, :ecdf, :qq) line_mc_plot = Makie.plot( line_mc, :R; - ijk = (1, 1, 2), + ijk=(1, 1, 2), mode, - data = :both, - backend = :cairo, - display_plot = false + data=:both, + backend=:cairo, + display_plot=false, ) @test line_mc_plot isa UIPlot @test line_mc_plot.page.key.selection == (1, 1, 2) @@ -519,7 +533,8 @@ colorbar_box = colorbar.layoutobservables.computedbbox[] @test legend.halign[] === :left @test legend_box.origin[1] ≈ colorbar_box.origin[1] atol = 1 - @test legend_box.widths[1] ≈ colorbar_box.widths[1] atol = 1 + @test legend_box.widths[1] ≈ ui_components.LEGEND_DOCK_WIDTH atol = 1 + @test colorbar_box.widths[1] ≈ ui_components.COLORBAR_WIDTH atol = 1 line_method = which( ui_components.draw!, ( diff --git a/test/reference/cable_preview.png b/test/reference/cable_preview.png index e99a7efa658cf2812ea6cd3b517109052de22e2e..1fcc5b0d7bfe4e1316c455ced93ac6acfbd1c10b 100644 GIT binary patch literal 523985 zcmc$`2RPRK|30kU-Bgi6q=AsFkWpC;n+wUv9!Z2kM%_hHkt9*FvMwt{Hf1D~EftZK z6=h{4p7X8oz3=bu`9H`1IiCM7%joSx0)PM zSE8YDWv8LJaG8c?8eh85OGD$tPeapxf`&%?E)5NX-TfRj34G%(yFuClw`#A2b8{0D-+#DeX0J*U$i9B&upm>VRg8_TK4VRc;E={BiUT*0qjixKxL|~}`1Nc5GycWD|4y^@ z#HNLR{z1#XYQdj>EZ^jP!TObTMg{+>cQ$*DC2KV-SrH z>=0b6DYNmK+}T`x{1&;;zb{=fd-6`R{@vhNTu@L@Q2D?(mFs`;4H60Szw|_9_KTQg zHshsqoHl#z;Ug+4D=GB2rakwRE*6} zcaOU|v1=t#@S2M+Y%?qS94iLK|MLl(@CnA7ycgo;v|U`Xt4jD+Eg`QS89DQNMK7#0 zSspKsk6#${+PwbVTbn&gxbD41u>JgH*S=~&qfetXCE~GevN%&>BXz%7hAxTA`Cq*E z|Icpi@ApL-toq-Lp#9GrazEs3!Y%xM_5N}5<D7~t832eE5w&mf5WbjH>rlHqm9YO_fpc&4(dvd zyxi{Ed1EgfBO}-El44rb;@9`ZG&ocq+*Y}^aksF`h^d#iH!I+l9KutBmR-lKYudgWK%^$?zz*y-|bIwsn(M>`up#NA>!wTlBk@VoHv^X z-#juZcHHCrpThaEx`bBs5?3SAe?r`_<3F zudzie&Bdo=n3S!Vn!zSpWB*j0o#6`E+VSmqFM2m){WqegB@>9fP#yMnqtS_2f zDKwt_iEc7Ns6GTKDEoUgK310^ge1Q{Tcn%gn4o-LG&2AE1lP`;awMhkHdNy;t@*AU zS2-NNempRB`SNAUu9pGSJ_#0oycGg0wqDG!_vY_+n_pY+mH3crd{CG2XWbtBn#ERLM4Y<%qU6)V&S-)E4j z7N&IUScrMm{e%!9i+(p@>xM(W-z{RAfAQkGK0eEDuu2<{0^&>0oo@b`aj-d4MZupX zsA+(b?>gR)<8;z#@I7U3t8;Yd-qR_eFO1XTQSqjy2U5qw-yn$oSvY7TkTj9cFD*Oy z_2csuvMN$j57B|{@aj`Ha47d0Cu*lYSifPzII`eR5z%Dzrdj+@H^Un1pk!rb#l_8C zU%HW*ndkR)s{YutkoGIk#Gewd@Y4VP{WV@5;R*<}Frcin_MNi?3oy-qli_SCIrkTz3KL0EZUP6mC zw*$rD`vlRH%)^;}8+<-~3ixQ@)p>kI|9io*>;MK|;HLok%%AL{&H7*~7N$%I!cPIw zy*Ag_YM1-rjCj}|EXjcC6FTggkEIU;-~%jD`UDg;52Pp*6iG8PvtPik93P3_|Bd&& z@ddQeZe0B)Z|pc0#(E(Z1GgMpxf9tn3fw_4FyO_%iup~Syfe+vrEI{eA(ylDcoD;Y zqUc&Bt?CVYAt*R_WzeXz1Q#D4!|yxWPucwEqG|pc0RR4>|2HV<&ujkoarU2={J#KW ze_r!{=woIK_ndA`qUYAKtbQQL#lv$Oo2sL87o~`fl{FP9{exqR*cn-Ft)!dfhyVcr zwH}j3Dn%SkO;Jrv&ARz%GBx_vt@Wgn%}!5zNkx;Ra8yfuPkQ|Lu_>dVsn1a1oJV_g zBDxSMr}@J#nEKymB+lPQd;%FfKF;YpHoa=#HY_*zHccjO10CJNt}au_>E3AThWLQl z4I?8XV{c9T%jxiqk`mc4{{)RVYt$scA3wf$YBxVUMcG2nz%Y^!@BXv&nw6R`ydY^V?Ei6DboE5DG8k>nJa6G!kZIQ3#gbNLf&Vd=9_FW+Q)ROa;{C4-n_08L_ zm2Sfwh26e;H@-Gnfqbgqsph9j*BJP;)YWg%ZtCdhxE$L3=J@HK34O6-{-$-&zrKBG zb#G*J)M>0&O~kfE zN9o=kilbw?_nIwK1P7%Na8lTP+A;ZfrgCX%sjrVu&3Kbxs!_3bg44j899q;G0^B=F zuZ?4C2p*yy&rCsyjseggXg&Xhf+o(}00=ual>4Q>jMcLNcS0KXFJ74(TxY0P6*_M)(B^vg+F6Jm{sy~LySl>Uf@IQF)V8dgxqjK`{ z{SVF$sWqkOQ$h|0vRe(jNfso~CkG)&3CYWocpWNo6n#X?$S88#wrve)KsT7h)Y;hB za^_~glQI=5<`AohiGfkb{Q8Vim^j7T$P!CJpg;bIMkkKPD)raL`OkjKEG)Oq=+|?f z>JBCi&CK`e)!Jx(DeCw@(@8Y+X@mtuT|LVDb!fu5FRhH?j`2ms#S}GsHrYu}>?0*i z+-a!*$vdgOnq4o}594IpZ8T{%F9PCv$*2FU#-Y9)8^A-MPo zCZlE$`^uPj)a6`UTwI4s8M;rmWZ45F1i1l@vU^rb&*g2~zMWEgbpwa+=~ju|`Z=1p zX9sT;x2qO=uWpQ#S%wc$xq9^~hS8C|_8qb$oMH>yzgyQt92lACiwo!4zFj8v<;z2o zb2FnO!66|DXw0&)KzT}`!iVRB_5RxVV6`s4wR6NGLs!JQfivu|s_M;Kw{8hLd{PYY znEP>#a{JDm*ldR$41bm3_;3S*2U@8H$(x1Dd+`}>kX%lCj^5#^y|h*;o@4^^nk!FF zw~^Ge>L|I&C~6l`Rfek)wt36{1B*S^CEBFq@`%8 zszxkhao#T`CPuYvD{vEG|WD|z87Z^?yiN> zBe+G{Lyy2zd3kTCnXfWt=H?YBVLLpLT@|lh-7cJ&ag&_+TKqo8DN#{QPOhT5x@n@# zGj(dT=VWW13(1qJ(x<1IZ-<2J@?>=%3wYtTg&SQ^!`?oP)NC<_Prb>Rt?F)Wc{4$` zZ*vL>Y0RR8k%{hg*gkD`^h^x^Hgye+aTE<|6h=5QF}n7Iof~V=P_b%=R}&;T5*O}K z7qDHyG-tX$QRGb5AxgSw`Fe7(4FH@OmbC|CU%gVCV#1h#$m!Hq@%-lHPaqNL(d->U`*nLrpD@3M9vNAg;f==l}-W}4+9iy zILep*5b-ZLT(V(AV3o{|?-Nb;#T-m^pW1}T`!Of@YNhDy^2Aqz(X}bvpFUXwD+7*+ z2ja@P(W^Ia-fZ2HmFCvb*(q}By>Q5xGiSQyft4Fj))@zf?$q>j&BD3aT$HjM8W=rh z#y`K)_?gCh^JyIa@S!ZOJFsxisrT_*tBP+LZKA#!?kp2QahO6mx@4O-EZ>-FXt-p_ zl2B>S0?guFWP-wdCZf)XZ^>*<24H{5Aug_~ZQv5)@qKVFF0`U+*mmiP?^s6}Z%<{I+@16vOhEj3$y>4XulLB<8 z6KT?mM(rfsGxyw=I|)fiih=A(b>F`frV6=?zTWF3E0=$9>E^cCSQbgw z-jb^urV%a~n6N@xojX^L4}Ca}-f>F>O`pt;^{DlZGn4j&p^2@lQzvgfi($!$mtFDh z*0}mz8t55g;4pS=+jg*UX5<QJdFdOkkC1eU2DR6v)*c%wF9;sBeZVq#*Zk(BgX zw(JhyvhVE6cDwkfmKHq>g@>iE((f})1}oy0|3IdmN&+-!xVX5~E7)(w(QeF)s{kYsw?tZiTd`C zDvC)SSGp?@BPxJG(%rk8LTn3voZY!&hb7p(`T3gE8qAD|)L6xkC=z#?amrgrpWoYJ zcS#ZV_y#Z}MJPnj%op{ER|D+9I8aEraoWo`AXMwNg8Uc$tbFy&rwfw-LN|M}g(G(> z+5lXFZr$3Zfe^sm#sN;0k9GP>bBT!^ql}kxg!lSO&)!4;k#2MN@L|FHsZaM@yMweH zQ_v%Xk=JCcVhM1qdw5t7a|#Io>{*?c4$s0McdW*OwHd70OJfpDXRuo!PtCwvK)sk^nSX>Xq?i6O z@%(;z%z_@Nqh3RfnXI5|G-ye%`rGhPsMKz8GlSHMWB>$M~^cNHX1B24B zV)g1Em2fHdhD2??4`-WCVe}lC`qq3p!~OeNls7GJqu!i>6b`yHLi1FrrR(1*&xMp<6(y zm?dNwcIPKyXZP`f$IP$-;T)y*o&9{X5V4MH260*8ShnRH$v;7_D^~{e@_-XZ_MUh~ zgNrFAF_K|ayp)ugo(>Sz6hQN?FBmP}+&f*6fIJL2JNQ0EIZV9opagiFoDN@ZQ^8d- zeEDOy?(i9=5FT}Br@$V%oP02h#wFdt= z()7qk26RB5jmPYi1yW+OcjS{Plo`S`O)+6|N%97{5X<~N3u*!}4)#r9!!bP35x|t8i z7wW4;Yb7GOD}x90X1e)2)#SX5;&H9OPk{+&i40K&`K~#GPn$IZj5ZdS%gil6FGS)XHnu%U-qrtOEWK+I)U^|9q7;auivov9?f zZ1vftt2RYA1cZg{_Jj^~?AS4CyvL8}kxcte`H=PN*UuwL!BZ_+YO&ac-m4rBZmIwu z0|Fz=Gw?%GM-LtPIhryQ&DU9g^NU;<7Q=b(U?YfL(B?Upk5s%J9-g|={+(HHf*`t7 zK%q&QUqV)v&T2;Cns>~icKc_gzJC^S|Mt#8{2o*dRONW&hxk22;uPlf0!9(*hZTAr zQ{CcWieK{$w(YRMq>dtv@X&&5s#G3xgU6e7pcGyRIDLa zJ)#_K1Z-9P1IhCZ5eF89y~gzASbzyHmdd$nS3n_#yf9^OwaR7fcD-(Bu!^zAZL{T0 z7fi*XVMhSCQ_L~N{!*Dza)-oU-xZJneAJ}e7qve)3yKU~Ml%G_~d*_2TxLDWcBJ>||%R#uz2I`}k8uNIe-qRcxM%maeMFwi9I*H^d4C zk~Al46$_ku82BX!?4t^rv1Y1)WNArBd_=^4DNIF}_iq-r-`pi7W$;_-(_21|8@cxE z(ZU25VD;{?8R`L+tT99py(l$r#8(j`_B5t<(=;9IHai*f(d%O&_y9~KIP_HI^ivZX z@?0DsG90CJ4?8>d*02#aI*Je!-Dk=T4;}KMZria#cGh*gKL!g)Rk@2$cGivUfn`Lrn}6y_>96b*_Rz(jvjYU&dp`&l6L@v41aUZ$gi z%nw~}-|9@2l$2Q3M#+toS5;{N7k2Sso#z8lkMrjuv#}i@i(HODY^WuhQS8jaDpSa- zAUcEl$nBOI2UKH;!4sPimVy9+#uWYIv4l#{`+-)D4OEr8xA*iN>1(A?U}30Sp~6<% zJj3>#)3Uav^2WYNK3>^|a7{n=#eiHE{i%14<>Kle9^Np;y?ZwWKLY?h#XmoqXaj_i zDGaca8!Q2t-=XnzpGN9VLw+FaLYR`Q>}Bxp6fG_Gdd#+T3=DC;WF(+y1OL832L>H+ ziZX`ARBmW$`mjm?jiUR@t)slPgs74NC`#bf&*4OCk|J0i83mSPXFWw*KL&kFuVvX&|2)ceSnakoh|a` zEQ*WJC(Rj^SCP;&z&9Y&G((V=@}Q0G6ynn>(t(Ahm_SHaH>K?ML@^iFCTb2E52f~1 zh){Q``}c38UQlEe6u2>>$A){(9wYDrgTU>X#sm!oN-b8CV2gIUmqeLnFmOIVg3rAA zL2|tn04b4pz_3mM(2$Zy_C^jx7jvE<0Zq1*`oP+~n06X`)@|>? zgx?F8d#4ad)aQn9eA$MU2hJoVB4R6rtSDg_kd5ewGXO!m2xce`n)faXJva5r^`P3jn0&C+y82V5&PxqohUqOiM~e-&Ea zA&m38JPBYe^!Q<_n`s-?>e90%ERk^FnCwu;17Dj5qSvkD%ZmVSF%LzkT)04l0TIOb0-GFy>3pHx(!S_BE>UfcVGS?jmK+(Bea+wAU* zQxLMr!k=`?eRd{w`!I^Bmkp%tRrzi+< z^GFNS6BX^X5)7UNA)YY}lNmvrl>h^b<2&oY2|0cr%;c7k&|?P+0FKFexT92%TpWg+ z5eDcB^ZO2tn23Zbq@@C@l*gfA6L#mR&AV;ksJsLe%`4N-N>7ma;R1Pw%3!`Gh^c0{ zF0z8`%J<_DPbku)PJbdn0n&?C1Fb&Yp+`9cCW3Kv94@dBsNvd}*O6gWWw^w#=gU~* zF|!i@0c^h06Ck2?9_CoTdmO{!_VoZ+id%hP3*KB->Cf;m$B63 zekn-A=1n>08|1?|5Gr8eO-d#jeLRw`QUCDatMb=3MM7@pgy#koGyr|Q5LKEROh8Yu z`4g*|$>AJWT}(qITpkk_3*MgOD*?`kBO<{8&=fZS)KKVHSW<>QO1v1#8Xrv$C0kXq~m92XH#EdUcbwK0LiE)0kugt%P#Wl zm)6~^!Id!}%Jt#`F)*Sjgk3{j;ruBnk7(=ZB>`lIp#OKQg+mBM z4aqxEFd!g+(8DtXreQ+MC2@zW3#Np$0m*41b5bszku5es-{`a9=sv?#_*yP135u3NF^cl7vbaA=w^UnjJyN@@{ z5A6O@(}!j;_C%n1|17uy#4fnp4uGs)fz|yXVe1Idj#BT$URzsBM6`zd^XDkUXCwLT ztqHk(WEj^pa)*j}XufM%StTf*&L6)6h9#B2|94L;29SLXVC*!BR{=v-5|eG#fgUOr z3~Y3&uR6q*RdX(;qs$rz|ui61Z21Xgz zb}t6U&u;}NFRU}Dr`rkuYHk4{ck#iBLHsteQ`Meagw91(+z=BRi%dG)V~sjUECT=% z5HAHOWM0T0t=f0w&>>Z)`MCp>IcX1--Rg3nR7iFa{I+r&3LBx)NWllo#Ragig%BXs z5~J4vkNR3*@*wz+pdR!V4fGvHHYlvb&1Qw&e2;lbnFE&wht1=t=x8VL;o;#>Aq&45 z0#gj(4B_fW^#{zK&xfo{1fT3Dw@HjIFnf@JpX@mqMxb{B74!izkx{5IT461rbS(tW ze%2v~h!BV1Bsmb{D6?<{9me*JxWIfiUnfEECLo8896lTXHjC&S4x;Bu3w9KDX z8{#m=?G!Y3V&oDec?VQPnl?oac?M(018@as*$J4f`X`4w(ZJ`YFF3`<2b;Aq@-i4> zrTd#RQ;DhtZUx?|1hr`SoS~cnVGw{2@*%apGz2}SSsQWSl4IrWOag@EytVE}LDo8#CQ1-R0Sa$jE&#GL_f9(3mp6Eq9j?^k)~`1i>Z@hLD;mi!(>3HF^OKMsY5B8ORMc7wQMZv+|HslUD)<`1QjXtE6b zn0o87$#qcRp#9)juawgld(W|eu%h}d-9h3T04T9Z^VRBWI1%h=p2-QJ%8_Ordom?k z9u0vahs_Eq#GB6l9L>z$c^9-@?PnMdh{UEi+Ed+>=Q8TpNYS3BZGMb~&)qIWu>aQ! zc5rZcNux;MzjbhhH7BCwShV_&QI79Bf~6Ui!dCg|Mf~o5W%c&UoWyae_Q|t zqJkh)2pxpgBY*(B^)@(==M(+H+12UKq zW*x1T3|YTFaNgizSBq+3_3_z7BAP0K7Xi_g1BSYGyT8P?K%-wSaFJj(@`-cw_^$X* zf6h;L%*E$F1BHC|A8zv5We8Y!n60T;)r1_uKgPj-7|Sorv86U~<7ZYN5BjVA{XX&R zKi`17uckCsZ37KWycnr#zkbcz&|eU*byXLN2p2D}7O{|lK3TBesbk!g#5^D;xpf-V z58(WOs(MM$L-x>oRD@!P68YL>o_C&q%l~hFnG183CsXrdp(w2o0RCqt=OEo89_ZRzIdn2!C{*RyY1mrm&vSzKe)7bNtwM%H8P5s}l9i4w#fMbda(zQW9K25H>JxA`l$$UoW=-a3biHw0$@`7W`v37J9?7j|`6P3HLNu zww6WHjN~T}51A@gtXZ>hVLj&iRHz55S*7&AwAIbQJgMP`T0M0gxDGmlmeP0ycPgQc zG-4IkeGNDLy)m@3=%_^AA=EQ^q9Z1NzJ>%1LLi_0{5BG!)c~;sK&VYd$pd6tFvm?k z0!n-LDC#a1ZH#)AAOD)kWbC?Nny z#QD^ue=ZDNZya8Pfr9Tt8tA-BX87_TQ$qv0rGl8W4uwp6vcDmLs6vZpGXEUEXs`qa zPEbX4Ot8Vc3~GVG5rw|00E_{hk*KQZHk=!q{rF zl@OmQ&`LqGIPHPufr3#b9aduEOmT&Aof1M`4Z4U(dQOlDVQ}dSJ(*GqUWAb4luwYO zR52lu-6&qYmq`3*x52jq1EbMQb^&3{ejECbhzhR_TQG630PRr|Q*w@Mm%mMiSzL;AQX)$&lju;g79{>%Jl5q&6|v%mVer027`BWk}d; za0A3LN>EDK%(&05@G5@vFu5@W&$=h8q0M1v~t!8olA8pMp?8z<0(1Zt3)Nc>4~ zoe)MBy#CzU^IsPRkG4yD%u)0iN@E zgPzsb#ZK7>T}ids)ckVlw&NLVHAO|ao`;9zVjQ!E%6dFnnmP++X9bS!`}o66eBVA8 z*Gf<%yf0o{usR_**$mXx=jKd*032b))zy*{%%DA;ot)93RRyUvTwRs8H3jeB`{`DJhKsJp^e z>kA4BB$CkGF?0=o{koNt(+8|J^r{O*B_)g9!3>!Ind{{^{^ic0eE$Sgcbk^105nF_ zw`&E^(_&S^mB9ooU$Z8b4WtNpTW6>q$gcwypd?_a@Wxc|0Jweli|5?E{(fsm#9}G< zP@PQMEvR_1G3t@25fK-__}oA^kAVVzWZlSx;F34#?&;aU$mkmd4r6SvnZX_7?FQgN z7GJytCadT9aFxiZtFK4<>HukSaNW9f&*50>`;faAWcUhkpgLSoM|=ALRaII{_UvqI z3%ssg{cD<6FY8(1>^L8%^#w*<-l5e&@Td-pm&ee%xD z-6szJ%WmYgype@P8N$*9L*b#LAjeoecWmE&sq`8H{7B`%t5ZUX4rt^%6xfx0w$z>tu`XV32W_U&6QOhsqDeLhBqZXo=kt32@V zp+nCQcMJ>+d3kv?V&}i{;8Hqql;ZB)yZpe-u4BE=L1(vVMU;mdV^9H1YX6GUWtq2xPe8yQUJDXdx@c(*Y7MO`RK#( z1{Cni^6~@iA3yHbICSKQF$y0iDr+hBjDOD_c?-}az`eJjp%b?nWFZCcOW93mnF4}> z#wZm~Vz$W0$T&htorHs4HScTDN}J}VtA&Jw9Hk~dE+Q|Hm0fJ;K2DD+)(P*TLfMB8 zVX&q}_r80=;lFM7?i=uR(ym`$gisa`5|U{Hmzj`~5{vbfm6iQCFmSC3H>H#BDuRiB zBb)$9x*5JWI8fJsqYIZ%1t2CJjINCkf;x|J=Ag3j8YBy<;<7MkkO9HLSB$|bUiR{e z6at|Md*&t~^HtawVegQT&1j_{@4b-|v%u#O(T&zu{rwdp)f3{AlOb+-V}^*{z{GUa z-CfcbL^X?+t}Y`3uin-jJ1(OX8JnAX7ocg__f&29JUEC0BDA))w!Qf5D>x}30J8=v zBw9D$T`>pt70FtafU2sCmo0k&X=V(3Y3I<8vYFXVXwqf%^&8k&7`XmaNwPl@X|}C- zSN%{-m5gu;mKnM{JUlwNXLrAN@uK%hTAC3UDf#8%M6&>a8XcW&r}vGLy>IKbAD z=d!`U!2zQ7Ghj)uPe$Mndm(p3KT1oBL{*eX3JqnP`SIfzYUzadnXctPCgB6f57X1>N-@!72j}NYg7f1|NJv1f*X&rV<{|Lg1vz)7UJLwuvAt)k&$djlV~=W);URS%g7K}jlk^$O^gFs z1#D7M52TC7`s%G-Vv$ti8?9Si2X+tj0IQh6Bm|KHW<+_Y`j$t~cw-?_3YWZjv(ZZ{ zQLYDUSSK=-lhF7>7N8@ObkY_kYaKONykv>M`ENEut@%gY+$2!j<<~JWT`OvCzIWjQ z4X8#XRJUn}jK16W`DxMR8BH*|3-93KTH%G8M!D+n09RH$`{O$lK=!?R8TRel2l!L- zF2{-c;>C;cnp|VMbso{Mu_*j^2twAlp^Td7*#H(M%f54|FU$)gTLA~V0Rv1;O>1aR zo;>;L)ho+GuCC%Mf{v7;er1Paqv%3HLK+$xBD4~%j-;fd2$S-}3H<4Uyql;8RC7e| z^u9@EQYtSk^?GqgMMVK=5cr)m5YXt57V0()4GoEkirx$glGD_ro4;)ahATy7Wy?H0 zJ?Ws4nVOlQc0Gd%iAnUpIyyR8lkA+Fx|@L4bbvS}XgVz_2M#O{$DY4^_s+=Lx&miG zMeRP)zz(-4P9@wN1N$mQVIQbmbxFsBE<5Wb{If(ZA1;9nK?DPNTux3XTrf48(DwBT z+%|h9C-2tM($dR5vt-M%Wy@GfT3cD<%+1+X0}Dj5LBYaCN9z=NNDefm_DmpmQ6V2a zejK&y-P^Y{H}Ospc6N3j4-jC17JY80g-u8(F!oJ~{yxU-+qW-5{n-8J>C;aKpiIdc z8L{1<-?Zr^Mi(WlgE-_bPB%BVuV24Dd;cEZ;e(+1LimjqBBk&_C^9e5<*ay^n3x!E zJ$T@^jg9RhNlbnW2et1W87%_g$1LGY771y;eC5jGw{P!4V&*q4S&o>y1W0gDLixYi zyfJKC0F!HBgguNRcq0w|kyzz0-wqTRa~zdJoI9d^INzL}cBRuzzT=HAFh(_6*MeB5 z_4D(SSe2oxq@j`6C@h<-6&;_D(0(9Yceidb3H%rV{8dQ_MLa$+F}k3D1s!E5uAtz7 z!n$8~4aBvUV zT4!)dNJxAIP&Inn+#GFk>eOpcc|JF8bdyWQEYj80g@d7TfVOBDZkWYw-aTG&`t)f8 z!GA;6(b3UPG;B;GX%K-7Jf_T8D@@P;itg>(xA{$9 z(gHJ9N#6hh@&&2L(gmcfNufjnu0#p|ty@`RaX5iivR%iyo<4orqJkjuFKTOJH8C;S z5YRgM+Y3OYQhlyoZAVw<1X|!16g;?&mi8H{1q$T>dpJnuVZ}}h3yZ37X@7hiV8AD} zsmaNYot?!=`z-Itx2|NDmTiP~>NUzSa59Kl2@ z2z&gh)vH}@V)aRti9*#96%&gDnYv`po;@(5-@JSGkewa>1e{7fMep8)yf|ZT-wwJS znc%goys{F_p9!CIKP+r2k-YL{Wo2|k1PuvMSjh_`IzNAYm5(Tx2n-E9qT%4^D9eF5 zih!%XiFrQ7u&^qcUqIk_b+td3MOu1#cq9K>!OF_&2>7FljkE(t4^C2zpq1FJj*h=^ z^u%i!j_>W{=3a?{2&UO@8wbY~EJ~Dm0)ZYdH#M=++}n__0TtH` z@&Hip3f%J$IIV57KF^-*U&AcAAB6*@i_^{Bz1IHBnWEO#2M9;-=Ke*Ejr53`P<31$ zz%x4#Hnb-Krx?ks^#l?c%rSBY4=(gY@CX1z(LwWi`~LkI7;jx89VUkK|7XUOe8;w4 z_4E4#g2xR;c~c9EQLrzVGp`mvYk?)z2AJajmZp}LAgU1fngbvk zYH%`P6}0lDD*%x(;8`Z&SVK5YtE%ehy~12Tx)+wEx8cnjL9+@vSh4x}`L~LSZgPj- z&0|==Tnf^E`CorU?m|;o&&>P^$uouPBBLLEDh%@`tozT=)zwJ)lM<$Vjhg!T`%%9K)=n09QsnYL2HM{#$~(g zyYLDKScl=`pii0CivP^f_?wCZP9P&6k6`C8ofGVjSrwgu6ql3~KM-rT0t1l*az>c% zfvE(5>BFO=w}bZTfTl`1cvfz1XJ_Xne6etLnj6iF6eq33V+C$v))}EM@C*bKC?rtL_vw1i`(L2zb$@jC2@FS<-FCQKqjZR9szFonOmWinn zPAbeMD-hhmF=&Bdumxdce+F~^emyAB=8k`$210A-CS@b55jk=*>HOA9Jii)zs zh?G$vx+v(AtVkOINCAy^?ASp|OM3xL5(ZLcq&L_~a_(9_x^`-D3hY zdG$c@9-yZmB>454Hy=jpm_|Jf`B`L3Dk^-v;Y6kZdqRv=wk=uDQBrDqP!mB6ID=|) z(FEm#E+%B#A_}_xZGro~X$%9Cz0v;8P?sk6Ast6xNEgKS@w5j;Br8&&mk>iZQ-|XR z1hr11alU(ciV6HO7pOW+SG4QaU97?sPyrm_{GrJD=-Vctgh|>g}1+~ zrR6@kdJre5QpM4@f^8xq^j`k{YbQn~CKTdezH~l+e$;}z2jpWj&^1^lJ3KT47jxZD zH<=v+6Q(gEOG_U=TnTd97zu*;*15Vrk)xg;4ydDT3QO+?_sz&Bb}Q6jS=}$0yK7By{%SMJS zIEldBMWz1pE;@R9Un11us+9(y)XP1)`smT4dU|@xSFI|ktqt%7pLY?N3c^p+zGE>; zeoKziN;ECzEn7BYmb-K3j<6RFogy>91swE|zV~0nY0L5B$4MZu5-ACPIbwpw-`>?# zQcNH2?>5ed??8nWCu5@U?b zS;e&H`iqfo-(FyDk`BaNy=Kjt<-^*l|0=2^^qj5#jfcn&NQnF!5tA|D0pq{NCBlrU ztfv20^b(>a&imhMgC8i-koi}sATRAmi2T<}OXiHm`%lC8x^qspyPKZ;i2sp)y~=0q zHL5-*A74*6JXyyGmcz#>{P)|KgTtD?Hm?7NYkMHrc<##?P#Lx#3Y46jMCgNpZfoDx z(AR%GII@C^iyN3drl7#@cxGj7wuaM{+;fSwyLNNS^bCDqq{CU%?DG@6zThD2rIUq| z0Z(O(jBO60(FoYKG6TvsSkv!5o?cQ@LsPF6%lS%CNr0a}B1m3_`-Zm`<7#$xc5;0` z?_T)Muhn7^f}nr9l`A?u>3ZE`!6QeG;sgKv4|ECgor)s~@ktLcqCGyIS!10c@z?e1 z*DYgeYu>+FrF3+4vaVH4dPB{T=;!G{ONBIqbghKb_Xri&6|Lt!x{~Mh?nmj5o)xt) z2mNDhA4vV$*~w)vJay@+t!p~n^3ip8bqf8)Gx=F}qXy*Y3<|5pnM(DzdAjg*Aoz8v z+S&?M$2P~9nMte|eUqH__)$qw5%Wq12l5$@(;walq91(!G{$`&)B9t`>#N=~c7A%j zj@+|z=q4s6jpN5FrH=s{NJ&YN#kdjkb2)sZkAWo0o|@{?f6cLXj%#X(cKPe-?R;}w zOT3!0v@~V$-Hud>y6%y~R=Z5U=JM?3d1TvsXU(3$G~0UbHG|FdDaj&x-n>cAv{Lwf zc8`v(&Q`M0!w>m|g}YwwWv_$nW%jC8%5*3i9l-+lX06We+rMNR9PF@DDDe7co{^E! z@?6)TmKlkWkq1o5SM% zmM!-7XNnr%ZN_`Qb?UuQSawT@W#a0?~=UHR#$gONoD)^h#5vT z+KyB$9ZgvywROWBDiNtTZ=5A6-rmvWbMMK@p=t@4;Hn`}$Im`cc>eRg5DMw$la^+oy*KCV&~=5LDrbrM%@Y<6 zo~Ki|e`nJ2;S;~Eb;I_;k6eYZdLNrs?wWCJUx>Hg-`ePQ*||}q}a;obqu zlh^P@S-Fzr`Xrh&xYumix9#_v`OJm?%*)}on60rhT}>+)L_OKO9CppWW$E72+e+$r zIQaarCTnEnqfCr!s~q%mhxI0gB$|?$By*hN!ZJjD-)~9%uXLL%%hfli@&fhM&{uz7 z{Kv{TVAcYc4QwS}a*wGW)YRiqvrp^Q$vjp01k967ldVK^4EKX-$)ik1|JxV?DgHQoEzg1c~95VuX;xBQ5o143&9*==Zl!?RP6Fi$|3)?KuuRt*DN3p8f5x%osahpJy z-|&Ff%7&(5hN zgzFK^l^{YAQd4^jfY))DX+2n!%a+!^t}?ve`oMy+4a)W%!HfMkcHH#nUV-sPMPGkQ zLLclwaT^NER<6J`Egc)Ya<0(Bd1Aodsrl)A?$2mgW#}5XbkoD@=tKNrghSQV)*izX zvqo`3bKRCLuR-erg7LyZFLux;Rzq9+3UmYjS2;o^;*nwgX=wt0`w#-J0$XliWvvFr z0>YE^iGBDpeuWbfmn;xFRasd^xJ%@`z|8&pR!ukNvJc?r_F925Ig_}mmLPJhg!vDjY_dWMZq zi|wyA8~c}SaQM8bM_nEL5tk}u70efSc1m!daBhtk>hq+lOMAU1jmM&;C(NyV=i{+0 z5)BoVD-usEki_|Nk+qn2Ux8jE^a&_FEZ=4fgiv_%@E8e+!=P_)xN?6j?3;wPz<^V` z7b+HxkC8b=)MaD`=vX|ck7aqS=C72IPe@GcM4lLTbYAuICnfmYNoydO^Jd*$wAkbJ z<4+^h6P#0ypPR|g-`qP;iF#rL24w#F42`gkgl2{Lbtx#?o(X4H*A5I#&@Zq0_?&=d zBD@3KCtQ00w{8`Iza|7M&Y>#aM7#jP)L1T*iMN!H733s@BqZD7yz#$n)ajOD|wH+W~3s`LkyiaQ+x-R}HyQOuwq4;^ON; zx#ojZsDR8WAS`SO0}c`Ae=e0(Q_7Zktr^@qq#y=N4D$XgN>E3MW(tu7J1_zvBr5gs z<6Gb&U^OnUt}bh6z&0`9u@l!Ip02%h`*sO5(=4|sK^#Vl#!)OHzvAHr%*@P|*P;F1 z0L@584qD*zAF|DjO3xL;D}1>F6@<_#Y@xotihV8$D~X#=I?VPQ%Bt$R2wY)#-1^nS zsKDaIQS_B*z;;b1X)d|__&PBhdn4v&kM+C*4zXF6BpQzn&A8Q6)i0}eX-u{n0AWci)9lQrbGI`oU#eC4#ss(sMs|~ zS23c%LSC#n2T}uPUtfVc{cG*oI1ItP*ZlmJL~5!UkX zkOGOt3F-L&VyAWjG|@1sW+Vw!NB}J`>Kxm#b7w71auZz(r?1!X=;z#q?P3(ZtOz*W z58{GYSXhW;g;Te%AgMymj`#vWeFh%x4xB9Mz2@s{3@%T9K$NV2{0U#p+DfQ! zcz;Gg6y}1t+4FQbbZUgD5mT!@HpOGQj{}RgY2R5M_^jZj@$v?ABAN-4s?8ZUl)S^EPnNBRTz$nT_9`}$R#lJr4UXDjaO@b?i@Me z#OeC&b9pqev>+?+tjR}?ed>@a2p8ID=>z?Kv_f+ zF#<-p8xj(E8npm!jb-Q0pC|i^I26Zc8-Xu=6}B%JrV;Yw0X%5q>cOADmJY(B&D%!{ z-0>$;g_w9iZh`XdGD<()_5gn$-3sTI+hqF{PGlrm&p-RDl9qIzc@2wLQDn4&s7F7Y zNCr?$Nn0|g0iXQySKsC`Ks3y8asGPn*?##|?(4KIEap)HnX&Kn&TldiJG2r@i;DgR zvqq?UDU?jyf(*_>?c(9-0)J{w?kF)f)*IyVO+1a(|2%m@FPfK$tu1*34>72O%SYoV z_v-uNPQEzgO+Xnu^<-x`&^M%`Vq=#=oQAkjmXG8s2J5Z{_Lm6&VTe4y2RB)UVmXE9 z7u^+i;v#vx6M`@7a9;A!2Mn>nn?SA+s}-tk@9p4uCl(#x2^=i;sh}o>pme~1t8B7l z=~7VT#W+)&<Go7FxjoZ%@RXVF*PCKd0QKK z7L9!>7H3Iw3ziVzD#Tl|& zp`pI0c}g)+QGcVS*~2JF2S8HoiJfUY1K#`1u zq$F`3Lbt#}7;brQVq_GA9>utxp5BWPd^*RE*VDr1LIhgUvJud*h=ogqd1JR$cwVIL1ZEfTWCDO z@{oB@?LJ{+!C#XHOVB|z;mpg=|7-d36OffrtRucao#KQ6laO2j#55=a53DL7@KJq- zERb3_`yCDe4hk@#G%G4rgaMxdd-)uRIoJjk-0}#i&d7CBbMq)?JQiRIww`rS2|w)# z&JgVy1Mc1~sro@;KR9ZjX&nY6inXga1|*)jY3?K*p1;11w|75_oOC%~p9gCLS=XUP z_+T9&lubtC=1O5MQ*|7CAGECReFisdzzD=k-dMzW`Wq)5Ni{e-)KdO~_&A7_2Ek2a z{4-8YF>Kl?dI#`~Bn1?%tZ)>fsK9`L1M|KXqJhi94I&QU+u{$S1dt0JKzH*6CepI! z&o4qJ$8r$V;|qeB@Ld2>(GWqmL97KZM?+PH;6of&jFpXzLEaDJ;-W19Jwcmyd7uf` z99B{STfYYF%6rvj0>V);SVi5Zg+T?I;R$oZ76mS{Ruk~v1^(Wv*RIJJ85^2MFqDhj-8b*q z;lnWV11^gL^OI&R))J?bl);r^Eg}VQqBk3ub_GBLyfs;8x>tyUV3zQK1Bav#qp^vJ zj0_$-!^z7lpXTwy6}`Lq3#b$vJU0TRukn~TN64y@DfAMK0d3r{VFNm<3>*PU$&K;` zlE1wG&|!t45ij+{(0>AA#3iDm!Uw|0&Ch=`wQx2RhBikK_o|bS|1n9zqC^BA1qC7K z2{;x>;`rp7>sYrs9Y_emJ0vBy5P=tHx5o%B#VF!XgY8?)`RmrczCM(mbsIN&V<^U{ zq7x-P>u}n2Db}7v4u&Zxe`Ywi3DGoilYsGJ_*O8AZ2m(yJfH<(P{O?L1tRs`xnBNm z`fk+i`i`?MV(ZRNtWP_~hY~nB_NBV=qRi$!rpw`q-%nOog%80<>J`*kuo}?m|mpWPmrVK_^4!z4}=I+!vgd|Xl|?+w#Q0?cr90FIzPOe`&5{=lE3Bxk4Ky~i_VKA~^mG>Dp}=4EJoA9BwrwDA4UcSFNnV4FjPt$!r)3IcE;kOqJ?naAx1X|nl1;v=?Q;PUj7$B9&67Gz8AsO zt7c_o)ojARBP;^Oh7M&&FW;3OAwR?c4oIQ>%NIhPkUcLcu_Ln))&q1>pfXnLlYWF%ysnf1N#%TR4cI?Pc9&W9$L)ER-*Juy3)yEJcoDU zAOOxv8yg#FkZ>^uU=ADN5D-{LVCju+*yFLv2T{J(-U<#bLxlxEgK=Y2o4gWsR0Y`4 zNX~V^9dn=zQ~P=FRYM#iB0;gAdV778fy=>>#Im9N>lJ#0DWGiq4dlFR4f9q&HH?>} zUiM;NBjMa_M<*nPgH7U&oWw@AM2>v0x~}dKY$=*K;r`?x9zx`ni&3w|@n}b4C<6?K zg`HhO;yxbxrK+m>3>*UKtHfu23ELiBYblD<2lu7mQ~0Z!m+c3Oj?<+hqg7#_TMGR_ z{*yHYD}C;5_G#9A6NAwiA)60eGW-}6Z15u2P#XLXVN^>3E3v-^D?)$e>bTEs{8p6T=RTy^E`*?^AhLYlZKuTHozKG3>k$dab3x*z3UZh8& z5`~3rnE;F-$5=4(5N9L`VToaBQxgNK2K?$ePi;06c5n5ubjx+5Aj5c6ONcQw9i2lc zTBtcK0}#HW63A28N%cg5mijTKfzjKG&;WQc>pIW>i@o>$$NG)?htm+U_a-|lWXqnX znM7tXva?BM_6j+L?7jC2At5_E*;!ekB83w7>s_DgdwqYn|A704`|&_U%6Ytx<9My- zPym?ff9nnnUs50sY(g<;=H#RW2LcFw%FCZWN5NCqxCTES6#NhXk-qh z4t@nejROw98@BQb;fK^wK;nQDqZuAZ+bA+JBgkuNdI6jP`fkvhkb|6@Xr-#~Z;x?> zVdu6XwZ!39i@Dfc%e7B1waK-8o|IT~T*NSQG7(o__j0}9jZ9DL{&mQEk(o{e`vhz% z6eN4_&#!@!0HcIN8fXiULVa}r)F?h!C_v0bA+RU)3~@3);L`9z#gYfj8c>L;1}rt$X#{!*5OMB9&wI13x_SxFXe$W#@augL{eYZr0KSei zo1u{i*qdyqq~1UdVE|+*Ab)M%fLRjk<5mzu0AMv>USt6zgUEGrf__albgc!2;O2x3 zH3f=NI2Oz>`eN`u!k~kVj{Nj+aZlG-6U1>sCje;HNDP`E1dwV56s~~qAIi#seg5ZPl1b2fM#Ki7dO-nY3aHDF1^yf4}LSBDYDGABhzq8WnU& z(P3KglE-_O$r*!%6qg?DJKU=g!6xnsm$(@@B7ztrREgy9P*_`S^Zp}ZUl$tV8Xt4p z$q?qAeWj7kgw!1gU(J&7zQWsdF0Qw0IUPEW2%VuA@Y=DEd^77e*pX?c z7VgQzL-`x%eD|>dP@bQgdp^2b?%q9mq#FngG~cWVO^F!jCkh0d`hpgLt&I(%q+|v# zAmgCVMI;2DJ@J=@237aeD1WYj`R|WZ>d>DWW!tF>85&dQ^vVu8KJOVKWFJ9$h<6KO)`LZ*@mmQWo zG@f@9*^Qy(4}!FrsKAz5ngyn*FaklCeYGGz0vV=?ib^(+#N39(!dqdG%nmgvWQTR| zMF>2~1_o3-JUkHfA(iUAL}4rgWsCBPR=_V3knJE)M!KL_oi+JPSV4?fLEmnal~g=f zzRbXPwtLB8PCEiV=j>{4`*3YCpsl~`%sb(Pw^?26%(pUKJn?D0`$bv8i?W-o0D4S; zzK66flvoInQ03FcMgmN1ltD$qbx3H+8Qw>=0oDg~P*`3(Y0s;-9>3|jcZUIF%RJbeQNUshQj9f(_^M>B4TRl8QB&>uZDJ}!@-#`mK z9E26_Lo(+6ytGt3RWK51vE(DM7y1ffLltDEVS6ng96LW zlm zRbAZ`bBjn1ALnLceIbxgOM^~9jZUJBTYM2y8+ip4 zkoQ{!bub1w*W=%L{{34zyVt>15v9m4_d>0-9)s0k8C~7~UM4)%gt&iCuap0_wn0A^ z?BKg+KiG69C$Z#|Iilpu0r$DL*OacwT^d>(QAvV@d5eWA+Df)gUqw}g<*HQd+mMUI zt1FRl-RQB9cGOrpayqbVrXb&YiC=~To78AM4pS)=)dJ#TwZ`BU8g>ve?x_d2xhQ(~6A*6)h0YU|n8x|_Q#9yFRcxWyuN4Lhzk zc)?CSiM4YXtK&Z5WUhIgAxcA&O_C?(eZD0+fjBZQhLv9mU(11;++f`Xg}z92SPbC;zJ*;@b|-fy}B^)OAkL- zP+}^}C+e=GyC|?qtVTw8^e_zy@D7D4>eiGAmy_dZ)ZEv3C|d66P)e7k*j1`OMtUVR zmuV0ON1&F)EcJ=)c%}p!<|B1n%lz*aonf*T78isXe!LeFyrpBL>|sI&z3Mwt9|-Zo zu|wo+$giXpno(lwtLQQV6zq>so@7C0zQZ39l~sOKJc7ySxl2Y z!Io`FenCDu^|`v8D^m%o|7~wK6N;FQ6-BHHFjNb+q>XJxovWHePd?>y6?QJ> z;CBai&?_X*SRxr%Q3|0kSg#|pl4Q6vPRXhN4BZi$x;3HKbsC0aXJk(#UiqpZ>~j*e zIA;&JyrVfjRi>bTxsHl+4~hNZQmuH+H9hXyAlvhWsOP=AJ*%D*b=h3u;~p8mdXzXz z%dSIC5UJ{x(aB@OSUoZF^?zfyBT?p{~%$ZjYPz&5oWsAyvmi|J;)tu>q z_hF%A7hiE@g+;(IM%3T{2k*6lgEtt4Msv>|P^B$|Rc;Fk3g0%=Q{tmkI9juPXuq0O z)%vdWuBq8OVATgqx(`!7!i>uO8-CamHZs$qC$@kagNyk$VC5D37qle}=Vo8D-^Unf z-zV-aD8OpxMzh4HWNO-};^=8G6XND7yWeyaX*O})Oifes*TjD;HUpQKe*_CkVRSF- z&wHP=Vw=kBrpo7?rEPbF@4D9BCL>Gh4wJ95(B_`z5q3x=i+1R!oT)C+yJNtrn#8h0 zE^*nm^ip)L^E3H|U>28KjKpvGJp3c1N#F8m1jREF$+MR$V||u&muDx$H+zA3`iy~u z#B?-Tn)WTlK1o!FC=o8MDh>XvS?4E0Ct3mmVvz=i=?raE_VwtPQBS zrzlz#?Tu*UHi}9)#SCC_&yvf5Bh^k(X2mMP9-g`IYK%uDc|P62+?e+@>>5f+FL{5v zR=hiccW)V+_QaN&zyi8471sv!E_c{^tkjA-8<;c+Ue+wT5N_35AXaVfX-qDGKNKt9 zkXu5Xnrp68p`vm^ie|ZrB0keNuP)xnr@^NYtMpb7drS6YnK4iWxsCaVdl;|@u`ch0 z`gsLo7zS6=Uu><%G_>7o9H+G@;2G`NGrxb!_^zsp$+hr=70p5|Zvq_j_kQ;xqK$7$ zw{}k$3L^*2itfr{wBg{q=nE)waxiy7@wXh3*R0kbxo^`{)k_ttZXGE6;+vjgIakUsaQNXY?&oy5d=|O$7GBeBakk zYTs_p+D(+0xz`+hi{i)a=%ZfLabPqjBXU%3`>9+tZOGnqxwc5AQ-3@?k;{(zY%>K8*_#@By+oK8@h6F}w?= zqB2I)aUwefLYoCbO9f)1a>0!$n5HB@J}ObS;&}y2#TLMQSZZXa-|Qx|J41R!a@=Q> zt3hJMg)0A5f{VQ-izS~1@rpSrf~K!J@PZhJ10@VKYOGE9=N|1cUE_}zuA(UF3e2Io zGc{S;UXBY{f;gzU(~P|4i`y`LoYKFcZn=&pB9QH5`oAfHg|gz`r(-H)GWGZjh%Woh zFx7qtdpRL9Rn^KHlRWxT)=2ckTCNrdW^i(3L)fq^EP%)z&f0&(Bvop-&GB%0*Mim7yyoaTpuzz1~r+)fSZRmfilSg zvH`II8YHm7xO82k{J0m84;hqQjQ#K{9jv)Kd|K>|KEVX3O@Y{ZrM#*{tAHDLr&gcm zs7o9x3SJeEoA&i;k&Sja@cxb+dayW_sZld3?Q_m=qGf`WH%;L#tzI`ySfpZ7|6u>s z>+IomJ+W6+r#w^%Xz%xak@Rh}VD}+TzzR7px}yo9FmY1%d15lNJAS-M$fL!8IOKgo z=_#__uItk^D(H~^Qx1Ha_*V{mDSKUhBurJ6zXfH)(0{FCUw@y0lneVFurq zOWxuBb-1OmNZ-~s*e|E6i?4>Z!E4a|XxJTFD(S#|oHVd*7b~QzrxJ@|5)w`ke1vnY zQh_*Q@2!NSmjk~jDc3Fv9a3J*5fzoo(HAxFb2s!|nn+w~s(5YPjhbT4A9#2UN1yGP zh%&&8A_Dk<<0&9;=Gt+I;@d$&9hKWJTiW3#7=OGrDyEvx4hfu*FMy;U)E0Db@1S!T zxp3&rQ#Zq6bCls3m`w2>VDVRq3OvOMtIXRbQlH3|G@aIJt4i?>AZhsMZ8u9@@~|SW z5MurZaVGDOJY?lLKK6yJ#MCFRqbyW3175K|VqHj3E$6}PKc}b-uNhhPNb6X3&fM)C z9^M@1L*AFW(sWbMIDVGlg zbE0Tj1_h$bW%cfmD#~i}a6|?DU4L#aTFX?n!;>$Mpd0lp{Pg6UMeoz~Tia6s-F|1M zjG(`BA107}wd&UH;CSXbvhav+{b1tgYEEj;K&IsNM={994#e++M8Q%^)v22DdjlWOx233JT~?k7E@?A_JuuuIqmhF zlCLrC!-o$=cks`5oP$gOvJa;8bkDB~gS&^PGz3Hb&uI!xhdl1yQ#5wqM$$;H~RXwmF=pr*$-jPuBvJ%RtYW=1N7eIKb8g7 z&I7;`{IVFA{!ZP}VEq=FWi|Tfnp26n{julk>BdYYryIRNOkDZBg=n&5St|$5txVE! z<9-(LRc+SDU`qofWZ5q%>T2s=fd`?@1FNcx_;!__xU(|wn1uB1t~-N$$N>6qak;e2 z>C9TO((@xRB8vd#5n)CVvq!NE7T1Q^jfv8B zjgB!Bxl2B!k6apEU};I%iYM*l!^Ow>Z1x#q`s3{qhmxG!=G3S{&Ve*eEX7XR61IRF z3}lr{d7Wgm&q)*B7N6uCsBdY8v0p0cj;tWv8>QaIw-4r zv*qq8;QcGb+MwB$6@aS}f1gPHoxeB!ks=q8vE1_xy!+8NL5A^(vnxtng^>%C5VWx| zgGoQF@msz*hxub$%~MJIESM!HPiMp4@{tD-O;o#;&m^xurvVxjt-;DJ73{9XNtJnL zd>)lAx)RTta>kTX${AIfW5``xdP%V+yO z%(IOvNM+GEb{omIFD_Hq&c({n`>fl3JlkZ(?UE4{V_{=?mX#JAqQFO*Z*)&e9uxom z{Y9z#qI_p)ZSB#>{uH5KN~;|z=b3#Xvbj-oi!3ocgxi(Gb(1(tHiJ$qOxqwH|ZAnp6T2MvkvT)89Ib zOP1iOZuiTcsyvBGGQ=0WtNp!sX^@9PbdSnda)qg9cY`X(0P?9o-o!KD~>rS z=q)X77vlgQUTIs*bKyiCV^bw<6YOq}0)+!Cy2;Il>uo%0kDUf@=L+gt*d)uQqa~lU zhR~XG<3m^PhGfjU1HobG-`o&d^UI&cq`j;@O>-(Puk2iNUnV%3X|^Vi$|6{Pixt|{ z`tw7(*J?&%^V0nHrte8cTQhBc?}5KLNaxxAtYt`NhQ#B8E%wJwTesCG6hvLdb#(d% z-m#G4th!HAiQ5n|Yf{O@o%oU}=18K@ws<}>cTERgtyInE|H2D1a~8Bju}NHwcD#1% zuF+ShW{lY%>&1d%Qq?2nCg&@u&(H54ROT(ivH7B49Hy)`Qo#~h!EmSZ6X>~kQX z-^76)rvCHm^;MPK>k=}6zsVQ^IkOSa=N|eUP4O%h-MP#W)#)*HPtnm4AAAW(hE(MITOdxCCK4al9OK&gbw?&Q^8YEzI^Ug&ID18 zO2qY_l^fA*+mYyaG1f^f9u54=zl3j#_amn z*Q@bG>(vh3IIyZm$ITYzo7$szC^Xi#*{>g;hx)F{Q@*ddj^C2;lLgi3bP-wA&yuFK zIiF}^_0pM>97|w9cjWHLksEvwERPE1Fwm~nFGpT4_BYe#QBK zvp)!#gf=!8$qpyiml)-t%N!iiCAuDw0g%?kd4= zvD?tdNShALEHUZVdUy4@aDr${8r-e7c%Z#b7Tq1CSeMJenGso93K|pD0RdES?YZB9 ziO{n13lO;zPl)Dy72)%xIlPljVr#PQGTq*e^l3k9vT@MFR{jkGn9u;61ed`Rht6Yg zNQBS*XbZrcm*aZnmHVQCbY1Q@h>5BI)k|KMX_-+|8TZ(QDm`8#z1Yy{f?~|&X`Pn1 z3|$|wQGHoug{N3qUkrzeTJ%UBG)`0LICB=cQz_^!JcJ-ZVM%9^WmTAi%FbTe-fHvNaLeXI-T!v++V%chM$S1;(*5Zh zTHikcnz%6X+xJ_fp+P6U<_q+`{tS{W9vcTb%6#r`&e3{GoH3vZua3U`FaXAOCr<~~ zRb!tsaeZRn7{Af*8?c37mhJ{x!)c31A1EK3pGdQrG%XpR7HtpMPI8=e9F_3GMkAaL(ci)qKAl|&kv>l z2}R3OO8+8}W{&fQ=BeOsqNED_z;EB3d*6=M`84A6}0M!!#vgdYO4}PglNBkZ4pIM(uC2y<` z7u-md@)MW9xsu}Eap&~(VD(^YAsQU5I?Afr`&YGY4goU+W@5EZ6nqDV&(LZ|5eU+ z3(hpI$9sdv3jMY3Y>&IB5~b65T?~r*rguG+2pD=cm%I3IgXt7~3>vI>pkAxdj|gFQ zSZp9^UyO4;c~RKE55kg`lbnU>QdvHZEg2{C#{Z`&;m`9gT@@`!OiccMw3xNt079Ho z9wDn-A69eh{pr0GdRHSKoNQ{R3p-Gvu(9jeQN%;b!otZ;8(m+e3Za+I3=JI`i(tj9^ARB`|U$o7092c`CPhWa=c4-qxti| zV3S#ox?^h%!pa+)fhlbWvea{QkG`MGTUQrzF81=_etErdh%2^6IxM=1D_$0U_EW_B z$Csf8cZ~<%VEmzy@D!XeNP)o@SWI-R97==DK?+P)CxEjj&ODh*j2|v`JI9$ay1R3a zjSEM(g(PhO_1ugyL>@yf=bYa|UT+r9<&%4>*a~u_LeFdJtgg>-KGHV63)kQYA#@{s zN5rKT>tc&xRM5WVhxup1o5<-ql~-de*)%sm)V4MXN6M%Lzf6~zc&uioob=1A z;zJX<&OK;!Um@o~I{G)ZcIuA1zMjVXmuH2TUNM<6fT1@6RRHjx3X2?I=wPIeOiV$a zG{m$KM4?7Md{A<5;0B>LnEMX?n-p}SN?sL)DZ@2=chNT(+B?oCEbOdfLf7MD7-i&q zZ?0-dXH0|>^wO>7O6mj+yrllHJJ-h^nnf(YL>qZWOW==y_k={n6Gv;&lvvu0YdT^p2t;>*4(2W!wu0Ys6n&A#Rk~A&+ONKxkCe=#3g5Z9dN?D=9Y7 z(~EnT{)8M52HrYn+NY&Luu5=@9dZHkG3}4p1QUW0;~E!C`$l8~ou5R|FFOnB%IgT> z_gdAM$Lq44?lIvz@UEF=aWjqXC~K3UYjr5y{#^7X*c{p)*D^5rLk2Bdle*Pvap*km zq|zl1h041vjqZpFO6Wx=CZI+#+oo&WC^j;D@RiLshE3+hzYF_2)^b}kh*i(hDo};V zijp#t9<0_jZ)T*ADEbX|Qv9rI$$3tvtLS^TCV0OG$6wt;4X?`gm`yvXoUiaVhvo!Eu|PNw}o zOHF98iza0kpWZvr_^LUQf1Y^9F_%2kKW(OU}PoT z13%FwGl9$M)48Ud+QD18iNgvLf`CBn0}ZI*x8HfqzbysgGKd3;LL+Tz5MJHjtLsy4 zKVqi%e*79ds{O>3>N_M@FEmc>kTt(q{(&`4Ljmt_8zwoGq>FHFs{Sm5y|$qrI#&x(+f?z{7t)#E9o^&5BcAU zgIUbL9>RyKe6_Fo<@I+@OM^n@sGnpy_q6*#Mn`PS(xAuA4f2Wz0bH18KVc?)vl{V0 zMwOuON0v%(d&;hm&y3;g;EJ`-i=7z$5ssVX^Lt`kk9;-XUlY3^R@bY)Ph6|59(kls zkT4=1-EW-{y7(2c*D8Zo`#R8!sg~H;xyG4W_>z2i8V70z=#I6XerRvnS&2UWv(V1W zFNJ|5xJRtm7tb3*7m(ZN<6fPjH25%T6TYcoS7UrM5`QEeD!(Y>0Rf zz-SdRz+4nzCIM?@>?1-R-mYvxa8Ll)TmyXC>XRKE#+LNJ%u1DBnvSiPzLsv`C~qQqvWL6=T;K>@C`Qc3jXJU{=z&PO`+ z2#*#qpv?Eg(gwl{VML+7ea3jHM_TJOcpF9`n;d1`s%P&$;`VK7WIeC+D(6=)5Twy7 zqZ_j8%{34+L?n;9!ZSPLsihpS5Gpo+{L^vO)ju226gyPr78WF>CW%Aqv-ofC8UA2+ za#i|F{`}jgsw|kxUUmmUWAGS@3txQADfhg;SDgIqxa5 zXbrajgAt#z`9~dT8+A7X>eXwA<9^6K4lOVWGY7@Y8p0*I)N7)H$vW084}P5^^#(%MpL1 zTA-xnR<`{#yyvxi3EvtlL=eU>^n?u&`j(27vwv z@w`hf-O(R^V61q7r6%xpa4$DBG`K+92;rdB0&WtvLJxFD;}~hx{*@K!d<@*bBQ7(X zasi@=FqSTDNU8l^8=t!KEM2;!KkcegM%3WeC*)x&o%c05rHgL#Rq-=orX$;kCgN9}* zXDa74r}F&2e^8Z+CuC>FhCmQlXjt9H={Xd}S)xyRMbAJH%|eQjO|--UMb(i(bBHR$jQY9Y-F=T4J>loRY9{uQ z()q)4Z?0^we?R{nI+Sk9Z``P!jZuV~cW+g@{iK4P=F^(`mkoQz$=e2wbz|@6pE7Yd z@XTv663QHH4RCQC?fyC3hhF;LlHVc!m-?}PhnfTz6A9UoU_!eADw=EIV8fFcaQYaC zTtT*}t$AMR9qs9NxUPdON@X4%pLff$~9@pxjZMMG-|T@$pule zigH3S{2HGk@RzYRF|9-|#`A?a%3O^2X6k$5B%bZ&dJBWBI+!%vLAS$PwpL$8myeXp z65Aw+k0r9&l33W|yi&*$`nMGLqr?Z}`{PRpC=ijjOEnHK%~G^}>rPFUB$+G2}*fExdsJPZ(So^_Wg6f`9?-+!Zk znSa>HY5TUxINfL6Ygp3lX2rehS(OISU0aMx1WVBKd*vrlXv=gWxJc%>k;_$e9}pln zy}tfA_&Ra<@6EuCkT5@QZ$~1I95sd&CocdciKs$D$R31gw%1bNWzlqSS^WldtU9p7 zGBh+)QCGJFxT&$*kPv8Ygmxj7I8^!w))lz|!M^--Ke@dcoPZtMe&`{zOCO->BmE>q zE(c_N#>dAIM!m?q^M%KcAJb}TYZD>#0&Txv2MG1W{SK(g0_yoHV5osGjaM}_2sz?0 zC=?)SbXt->NOzBYrI1?;*as-kH%~1D`-wp)i+hnK3=4|Z4)!H5B`}i*yTJfvX!+uK z44{!IzgMI%Q7|;5z6>JUPe2+Q%$Q{$?L;V92z?H80^u*d^4uK&2~Y%pjb?EXg>miO zdyhlm)ZWt53maTBhkMf)EHUA9q%r>hjg9>wkwxgPgGU%iY3cYq0LCI_5!&#=kg-4Q zXHaec(j}omQZtBmLO2A{b1f_lsN^D-#AV=vd;`)X8qlktRQt`l4biAXnC=K#^}UYP zSmwyaUrlr1XymlE(n1CbRTuC$#PXD4R>FNDM`#7{PDBtMv}h42ZxDY(sQX~8KpP2t zeCJVcQx#1IIY|T&3|&8DdJ^PZ4)q!2^55zJDYYaG<>|-APW;GLaX)Tu&SxuHx^hJ6 z_L*atS|V)czP68QjV8_1iscK(~OSt~22pXF|rx@-B_))QX{JvHuErVL{FHJ4uR}Wqta;!n{ zB(>r$CBi#VVY;_R)v*;t;^m0)Z=JhA_y&uHa6RD0Iq9X1TWr}A$5ioH3QS9f6PdVu zPjIF&M0Ul_U9Qztteusa;N#{DV6_@6%M|-y%sl?DH&u4<$+*Tb=27>g{&$;l0*^ifg38KN4v! zQQ*B$S**{rL90HU=>N2+et+MIt-7lIIf1l6ply&ukD|fjO9c;p6ZxkCNH)kNVR$VU zm2iC65}@p9#fCf{=UO9Bsz9RfJXoZ;zXvL~jh&qwngyRMQo#!ZeUCxQBg%}j{tyQ5 zfRUy>g$9r_ST4B~gLL`cv_+f?kn)HDy)oIt`@axsSenR8JR;2eujjsxfnSJSBF)KROhE z`pd;# zrzq0;bJo7xv^^7?*W&N<^~oZF?O!Z zHYh5CAX0R(Y%nH2D1prSARG=zO(O97svVFAkvtU9 zq6GGo^C01FO1f+=q;nE{~&^Tx71XK@W*M3$YY>a;dmEp$#u@~JguhXx- zkKx#u_S4J zlJ0bwT7S4239)yEVx0)YZy*6lI=NA>Y?mbSRoNvDlm(a7o{*Pg|$sJFh zm`wU?VJYfuCX7Yri2d2h8V{x{yMRJNH$eX(J_JfM%Xha_K))kbtD@p>P>#)Geo67= ze!vU{$o{A)3wfta8`Z_-e)c1sB(1A@`C_2Z4tHY7>ltf`Xi>OV%jL%INcjeHW!V@? zjt;`Y#W*5dIhs*&%+7l2^K6~*0!%~^^?}Kd9t+A`xG6)ejsGT)c2%a z;1uozsYBk!DTTa7Y?6e$oLO!bPp#!_yU$*SX@_DeiDb3JwS5``SFe{`X-1hKzxznl z+rJYd@^R^FKtNal!)rA|JI?TgIo9wg)0Ez}!#{z8RWF7SPZEvb`)|T0*BSe%MvhL1 z>O)U_@3y7k6{Jmj-t)9!6S0Sd?363K%G}c7{Q0kRkp{bOx^2xnbC6e zEg`OE?%~@e^Xa;(h{8g)kUW4KJ6*r`_P1KH^LLLax2bn2P3gRU2eXz9$QB35?a;xs z+&thSfO7jM=fC+gh&%nISHQ&zD{KwHrWL%`K{VSHvCK!=igEJK|09xa~@(}Gsp8^=Z+ zpoW49jc75D_d)Z52p6+PpW7Hbyg&+q7NR4thOF39thrv)R z5F`@ZCNxA4!U#}`Ur7201GmW<=)Fj&@%h@891IwL<>OqF}z30PzB@fG)^o zn|EK%fj-fON;-@(!Dbx5Oh9w71PV-jBd|g6!OE`?>OEjp>e$r*GjDAqAcUidQ!q8_KG>ax_cvlD9nY{~VKYhqgwttaa_W2(y zfDm`-00F=yPQEPqj{yHO0%6`jxVu(*&C2PARFvIM?K?W^enuC1Q-{o%XA)&v7h;TR zR%JSfqhuH(zI+cfz1)spKbonzVP2yjZpN@Cl^%C<2YZn zrME01>9S%jS3P14{t%G`pdOEhG4+?)Xe%Oebc~JUTpnU)?TcM*-gVqhaN9~s)sj`c z`>Mc`btdC&e|AYpq{Nq|V3Cb>h1oN60;#34a)NGxfW^a|`1Bn`%oG{9b5|X(m~p9? zL^;hB!=`gZtD~A~?kc8FigU=xetyHrgq6!kh{g59EbiRQvuAB?=(>+*8}t6IXU2|L z*h=G-0SGOUf&2f#t9}KP4ke|pLupk|@2Nm`{#PP8)VDI(x#WNd# z$h}ovidExBZ!Wkw*nuGkO|x3~;Zxfn!$dS58<8^t@qcV5C!8LUu0v#pfI2-36L&sP zN`c%H5g-RUW85K9#j89#cp@O<(6!76?000YX@%S%Yp%<}AX72>`TymohOTJrq#Y|b z!67zlAbB+Eu-qAn9AjWlqroWx(NCxM+2NljRYdwmO<V1_muCC!u9j z03v|KRd5f(9oY5h$KExn9>)onlK-==il@I_T*DcI<{bp;Cs>cie$^Q z^Rx$xCk1Nr8JFd>jYUoHX{fxaQQJ>PtT4x!~LYm)+7 z_y|OK#p zBH;iAg-D%<=yXnPZ>ex5MnO-TqHgBh+Rd9cUqGdYXcfY6L$C~_HX*O=<$;kOoKcqo zNI&_2ZI8(K&Dw(`5eP}tf*CH%B`||A8i9Wg5|)8rsRI%E0n~5sWmhnt9Cd)vNfWr2 zA)@TF1CEV|QwRuB4JY~^Eni07=cP`*$%7SJ=Nb#j9F8Z@L-zn|x&PmVvYZ4mWuU>} z2)*59RnR#@4g&~&tN@KzYws|I`2I(V|<#R!+$m*3Hwx~;3D{6H6vbq$JdT_A_`R+*p-E;%Nv4sLOH zXT)mCVqC;LB3{~#sfuGKCm>-9#t+sER$|UWlah98>gZhXghu7i%rsU^Oz}B_Hy5zv z!^^Sd1$>qSH*IgrHq=Gd_Nemeky$*NwXG@*v%|MM>kHgruwDPUtZaM>*Cfayl3+^} zerMQ_lr^zFwCLKqt78+FBagZF`!@rmQlubUc1nPfE4q2YEuKp;d=;YocpF5$Eo5TOCwZSWlEp17!KYO;YW1=MTL zQd0-kV5A;92PhNhwfsb?Q641LIl=} zkS24z-gZDNoh<3|6X{@mX*<04e}Yhsyncn=$J4>vrw6A#f-??SyA&TvNS4ja-iy4y zVcti`wkS%jML>{>s#Uo6L-BRrQ5Z>Rkxew#B43!oVV+ZwLMvXT3i_$0lY$c;+?(ej z11^DnFI-qPPK#0>A8*jX1V{_|umFWIY)h*CLZq-jq-CqZ!{u4A?u}Bp==F=p{AaJ) zLxrz*Zixs2I9awtH~S@KamNr!8O=wjUM%FD9z|`R5d3)-Q+2TH8c)?Hb8V_-3Y?OX z3i+m&&^fK$$LSm3%I3K#`R=V!7zyzwUJ3%+a$-FDf4Qd{Ad0p`elX0Rb~}^ISw`;PmvgUJ1?O?#&EgSW&xL$Y9I} z459hQMe-#k+t}Tzmnd6m$NRaiFWG%g`)ts5=BD@cEFJ#kFPoAnHeZK}@9;I55(ocg z)z;P>>ZvetX-cR3ff=P5)!#p)Xd|Jht>0oMx#+mi`PJ&(Qq5xmGl3KYnG+H~4vrM5 zU>m%zGx`2K(#taf=6nSTdYK>=2Mq8DG&nTD2Idia^o7e!i=7=Z-=hNaTQD+_!@RWR!)tm>@=w5FA{~eos zuu?{<-CdGyJTl{A&dx2@aXlEGO(2=wJImdiA59+n_*Y-++6FY_sXOwkYtTfE=f(Lcb;mCfd$CQMtEFOkzO4itZYuNJ*gEu>7If z&Ds!bu%2|>e*={fe5>*mkm->G3q6+`)G-$^F~?K;`c$H#qiw4|5%@c386*AwRB*() z(grM8rsvT6(G0^ zCBQex@)0p#B)@_o!|3)8#4ygtu{qRsqHq}uX0(8HSIbR8POb!=rHJ1f6!b`XIacrZR)bL3@UQ>a4B zt|nlRar#KrUagrtSZUXFy}cU z9!1vrOl_Sc*gv)ud=xxmStKOFi~EM_6LUE%tv3j%sQP~ZyglP1?AgvW*D=cq<-AD} z%l)-NUS?6F%)u}7^0a;ZZ`P~Fh+#?+mX|i;?0zHOpJ;w&L(lw=I5YD^<6SN2lAwSR zPxE?FE@3Dvd`5l!*bStwuGSBQc1FMRccy2kJdKz-+g2og&OsCsoBJ0D z8sdpap6nSpRr~(1g9e4%95mKPzAQi#@iO9uTebh->%tYVx=CX}y`QC-_V#3_iSv(x znw1z5<-Vq?ETQhoMIW5^%!$;be53t-Pn1-a7A|!vuB^7~y}9tc2~Gi|V5Vi!iHfwt z5}nJf7U@l1QJ=%8E0g2;)0{^beJ7Q0g(*}Ykf|nKDCKl^bNORw5IVLQsWz^yGpEo| zaCPYnUYu!vCR94@(74)Z#kEWq73a_jIvuHxr zrEkDgM&)9i}z2?AnLQpBo~PB!TQE*k;7fkKI%J>7zk;j3e1 zmq%awXab^JtA7D*)q11C>cge0 zAIibSLH+_D6VeCXDcGd_xlAD7yK?bMi#4W})GrRJ1ggI%r2+(PgUDeocV0sLnqj`J zZUn}EiB_Ny*U1CoepWEMt4G{<8=(n0+XXha;6@4-B2*%dw2;tjOtFEZT325jTOzC8BQ!T1s~l5)!(8P(sVoyDhV)M+q7)h{4%Iu)~0i zXdV#`0(Bn1N;W-Qd`$-}bGFfVKv{_TpNNi)jUn)iZ6WadMDm2JvlDC3$wp?Ch-4Z_ z{uqO&8B}M8l{uV>seSPMcKvl=i%6})grX@GM2AxolaraT=uk08)rR%3kOL$l_YW|y9#K)Jr4oj`mbd0WC4%iH%Q>w&%@#ERcjzoY;7LbA1Hj=Dzp{g~-Q zf`0~aN&SavcIjX`>%&`mMw)8IP1CM+myI-c8K;Tg9bhz0Op0Ja!*CQ}zT(-^#}ro# z>b}_5Q`5241wPH|MevaoZlbiU?dD?k?K5w3*)uIaS2X^`JGO^_SW^uUgMeHJn2ZPG z`6wVK+C$SG0m%X8%7Ynpo=Dypa&QnaOmGi|AKzK%G6DTt9w0pbi~?I-=;9-e-XEY7 z7zmo^us0A8&u#G1acOa0J`djq#wHVJ1gpyp-@|n+s&f+R{_!zT=Cc7ixgL-+L#h2g zOuYqEm20;)ylo{_x)o_@kPaoKC8WDsq}u>NTDn2Hq#Ft8?hZk^rArb1`RF<4``_Wa z$Jk@bVy$P*d)_myfHD9=b=egk?Iw~$VRDbOQJ|S$1z9oB_>oxPHwG9z!UQrrHr8(% zHsk=l&cfjt8Ve+6%=Ec(oT8sKQ zxdnPnYb?_BujzRbgN->_`;un7zHpae#5apsu+yQ}HPx_H+1=-VU-LQ4sEp^@&9vCx zo=$?T(I{s>>MB`c12tvym09$iS(|;)mq*9v^pR>~x)NL=Fg?wnN>uz{($T>$DK>i# zYCGVuY#R*`eB8%)^mNg$xbt0U2?|}pQvz$*DY^VHUDn^acG)y^c`HiB9B!B-X%z*e z&rYi+wlP>`6Q~wj%otY}w_DNg6t{@_+`qr%rHy)=slMe%v^{Tb`qt+r{o5NK$9=q7 z16=c^{px%Y`~%o02u%Z0bEU9fYUv*o=ff=`{S;dw6sqn?U+agjL9zKj=gs*eyPxsx zQ%nMgrQR65=d#07L_+<1hFzVXFrO5&H9E8HCbV2w=*_WrIRot?&(f3T@r}j<2lkr%&%gDr4L{jPM5;v5UUns3ygp@^5HD`=H%zMTESb=WQ0x~ zfj5BgSTXqGf{lFj5M1B=F#3K?1j5DJk>C*nBD_dq1QBC$5OaS#x$WqKK_Y2w%?RRj zx}9N^=L$g34VKJkTDM1rQ~OgEg@a)767>U@kPtDV?ghb0Yp6`wIXGalUp)jf3~}*I z5Z4CTKX57D1xryjhYcm@QzQ|?lq`HfPd>1RM67Q?KzW|S{nRE7%t)|6pARssFyrU5 zhA0Hl+<{QY*$7_}{M!+4*csQ&{IVrLsQy4;9sqy=iR{G+5S1o%U3)kKHv(v4E=xkP zPK)g|PGf-9Oto152AICgL$Lr71w&!5_gbLpX7%&)`+q^62Y?)xvtCZs<3T#89h%A= zHivL9!|yN!P!C(zMb_83f_#uQkeF0FLgg?#Iwu$C3n+WCu+qL#3^UQ+^HL9y%x~0X zPUDP|&l&jw<6F?+-0dBIBc3(<`bikYUdv*Lg0gxa*~RWHWU0UeX1Nj9CVja+{dv@m zB0FUID!q>H&^WcWd3QX9nSJ85^2=vAI7JKTe;}N0df^=fF)~KJv|Di(a2sMP;&}4# zcEe4a$d{JW9-pkfVh%}%Jz(T{U(f&b9s%LUEH$tE;+wwBVj9(37&MQY{}cJ|UYH=A zj$E9ytK^o0;Y-{X)2i*|z~ZZ3phr5V$qOcy&ECV{H3@}%^uXS_P`_~N)u3^0<8N<0 zHNoO4LG+Eg-+>x8n1tg%mSqAs-{DOxSFbxzpBM;}d>qEwa3Mh^OEZYlBkW_hk6_+f1U}P_Hlz0c6Pe zQQ;fW!tvp){x=tFAU^OT3z5_VGmQt}L{bHz?0WLEcNw`CAcz0Gn?P>wb6OB!MZlaE zpcHKgeiSY#5CLDXMbrzBrvm>EFb0fQ$|NdN?Kg0=?O2A34y*v0WIz-dw$oD!aF~r@ zRIQ|C`=X)s*gynJC z{H`#q@$(Bb94j-k$3R_v^W3JED;)FWJZP#-T|G@XXB}@c{+|4`k4FJO#j?MNbu;aj zdVZ2IV{`z5ndwW`7KU~kw_o7vr*~fB`#o&^cS%WQ#qDSr`>+!!`8V(0DtSYZ#U6dU z&+dG8k;Est%e$cdp18vk1qqTDzTwBAMmS=nm_ZyJb*gPCkFvO3>9C@up548Oq(L!E z>VI_LPTC8@3yHe>Yms+_O+R7s1ide>znV2H5l_RI@bYDL-h=CF6lV{9i|Rcwr>z*Z zsZaecueYmw`^RtS1j{Ja3VC{^JCbU;4AogXVDGyF;`waU1Hv$y}WUW&Ax! zPXa=e-@G3*T}lSEX@mXUrlomN#c&)g4+eupA66pr9adR7Lh9&>U_X6{{j5n25s(7=|DL z4>K?x`mvqqy>N(AbrEehB>lYLChFr zIKRVbp$7Hult2*?XIuF&HkRs*nDYG#0Uh9sJ2Yo`A*BNSxB!8>S4U7ajC)9@a1adrL_^XaLU z7Jr%#xdTlw~i7Xcek3C4iVnCL8&Pk z;(PN&=*zZ>gu0kgX_4z6Sj@Vxl?&8^z-7CEiYM_(V&B{D<(8=z;(_W?=_mA8Rjs$ZruV#X;kn`aw^bv_2l%F;pm>c&L^M@}v)mO=u-vw@a6i1|bxfHM$)7*=6xk_9U1 z;8;@(Fo(GBpw)~RXo1V*&>$3C02(kvz=z}hX!V_2g*^PHQIrJ8sY;}Fb)eG z@Gohx0!4pKGpMrJ4|aDSz#=pmG5_JVUAhe?_z~Q3;K)z}Zzm4CA&mQzdcj%|WZT%o zjbf3~k$0yy9QHrKWP*J#n5<5KMAvwW?C+D{hbfO2uteaOX8>9dG1uXi2&;mfkMnY* z3Uhof6j_RYJBg-CP$B)IUFx%{?HM{%!{eQIaIp7~juoYnmrBm~^xG9!wa-vUq>iXJ zQu~e}DKZ}xN~9@w0zZ;I@BhKrQG!x8`>}EDowCX2C*r}xDRMhRrl2P8YIACHQ&3bw zhW2a!_TtmAhq9VnADWlb^C>8V>V!Ws-}a=lfJI;BFfhH z+&uaEs=x4EpBGd!P{b14l3n%vzRoVSh1>ltU4+N?S4ud^$hZnjpT|?HG$*k*Jq3 zJ-E`ws1=!Fhv?;j{-hlp9Ty3CJ=9Mu1b_FDwE!R>3>6PK!LNcn#D#2BJg0X!ug9Mk zIlHpDO(o8F{I;^3tw_wwq~hyMO5kzxSqQuD9p!QG-Pm&?w(Gqw>3rkwpTgicigarv zh#)^Cvpujy--n$y;!FcrZ^9rS7_30)gTT3=@YU=FC>Z1`J!o)=1w9kM>lJo#_~70S zD-1TOaK9*|2!dRTSdW(@Y*P@CV&k#7;7Oy(y&JdNR4SHKvThv;g&VnvzE0( zGdzj>3+&m(V5GedgXdz0O=T!i#2}}_I{}LpDw#Nu5K{iCxD;Vu+lzA-M6g;mkq7$H z8}J$k@D>qbu0()9d83lBHH!Y-t#cs+{)Y4O^P0URPBYH{-UAb!$Lsby$kH9TT7VG) z*g$b8qSC}M5wIG3B$#pDf<*lQM(~@(aIz6?;+Yz!oxVsRAtA6KYo53HvA*sH#Tq4o zM+Y1#%rR}Np&J6h28e@A44f1+yYL38fI5fh=~ID(iFpf z31QsJ>=h&4a97tkER>+zAUZ1Z?Xz z)l(Wb^bw<)&p3Dr^#`-7F0;#nZn`P37QiT-6mfHLb$y|sA?2p^h2HRF^3N7`Wk_js z-!LhuD`w59*bk<4`IqAhxn^(UST)NM>APck>PI-LxIF6LTAAIV&CeFO>peyy=n=e_ z)jvqzF&91W^=p8V2U{+j(87Zo3S0~M1lfo%R=&3)N{9PfKE3)YVWq)#MlMJv9n`D0 z`t&%2xZ@(!iNdd-@im(NdjGc=Pw!v$a5(aY(?-`{SGyODjStD>KkTD^PkYB;GB&aA zxQBfsp3tnWST4|bSSrj43bowXH914^DOgmksEuptxDPFRToIAY$r zV9FcqLY2Zj*dAl3J?Oe$)7JX$STKN_A^=796dmBpLOA54Fzo{)n_*?Zf%PPChr>2y zNEtSz2*hL)F{%VhPqW7oVfaA3f^A4j9Sn6Eet+Moii73)aF~F{FB=9p1@JWw#vvGJ z#9Qz_6s@qO97nu}V6&;tX4rAJfRcwN4leqFrT=|R@HYe$YYe;D{q0Eb#)L|R1S!qn zI(T%u;UC;%m#m<;I8>0~&)dpbH@x`o!2=~R*))8;q~6zWu|UtUj!PuWhKIsycf7a$ zuAEB~Lmg`!uQ6!v(b_camB@6VJT_fs*Uy~zSNx85PfkosBxtu^YHA8Sefq#ER3obW zXX)=r4@a_UBbPGq7$=&+C|!Dv8{%fPT^Z(|3GuAp0)lF~933TA4ewNM{EFmbx_j5> zv#OQU`{Ir?Ojp+Rj7J9QTiPAlrWK`xwFnra!!2Hqji=-mreWPBKTA7}%^5c9u%OBK zyicAFP^ACa__Y>{dY{?rKc9Kv<3J*9(s0|80mkE<&8XqNcrrogrVcl)HwB{NYl4=( zeOR>QFAcBVzpAdFsnHe2@XBBSKZa#Z_Lt*^pSOrYAt}Yt#^yy`F=RKd@GyxR~O>%tR(B-IP0>pB5Pa!A{@1|!RAVek%#NC zP~Px;LgJn;X2TzEZ;PdBjz!YR@&yMhRiHgOywSIp6G? znj-3x=%Qg|h!cSkj;29HXsIJ*kuvJRd9Atm=IO>XiF>Qy>2GH?bJaKZ^_!U2G)G32 z|3k?S#l}-GLQi^1&RWO!RkzgHW>8z$*bKulD{%~4Npq?6kIpW1@wZRVDXY8hhJ8h& zn&FJlK~0?fDEq2KuX6B(jdyYC`Uv3@$M{6H)jsm`SB}13$ij5))m$I1ls09~I689U z{QYx|{kLSD7knXO#ei(J-~4t*2ff?#S0EvlrNRV?Gk;u9jHehN8}4PQ<)^)43buT( zJtLrQFhWQ$>2RBN@Y)`W>WiEaeCZ^pTcK!`!yzG&vDBpBtiSM?woa7RFkD><^Zaqw zLg4a!_i{8|(c&Saf?Sq7lzR#)FPPU^Z3?Yj|u5EOq{XWM0_|yEsh4p1$-&WsWHG~Tkv8u_pJ%~sPKyB7? z>a6MCG;rlw43813aoRmjfMc5GuB09IbGy!8&%aY~of)|GJ|c&W*@KX1S>oscE5HM@ zySl!J&TxW(2BwshL{r3iA*4s=8w}Kfz^VqWl=LK7gZ6bT!|0VEWlv+B8du*%qiY$p z#2MLFZ@j4D0Fc+%4`F#TbhGt#E}PTq5Wh7z*xLG|t3+lTG&$75zfT@~YyVhYj|M%q zg{C8~CLPt?+Y#43dAVl~RB>s<%BgAH$nRw6e&pmtO16CwvG$e*qSjb-yz#o))iVZ(r9bREold2E5|=MZ51m+x0txP+7>lv^BK}8zxMB#oSglkxbC!^ zbm4SY_mJ{if5?r4pINE7aUl)o_d(&EAZUx%`EJ&*-!3NHR+twx?`1UVS9_!J!o&E` z{_iOt|4)W^n46pD@~h#ZBTL8g-EKo?iS%W9J-d27+E#Q+V2&aSQP4T3#DcyROZUk| z(9FKO`YdVXmSvm0`MQEEO|a?Wa0D*?)v{2J^Tz6ral< zQL2Z(43klo;K&;ipaQIg!hN6c86#%nL_eJ_*m1>x)$3wg@JjDEE(oDqH81!7_3NhN z&SZCP$VHp?-Xh3D&~)DO;8Jo9lcBB**N76AlEqEbYaC~~yNuUZ=e0xCg8ded7QR0d zW&V_XRsCxAsNCTQ012$Yq>bzL`yUw zzheqAU#?SED`^JqUgjv#{W`39mzgQ%bGXy~it2k{XLh?x7)9U9r4n{4%H^X-%^+`) zosXo_YaC&w$;`-(b;+-B=^cIEDJ9?O4=)(dvu#(F;&bccbI!3RS?_7d=Z!2atw0sZ zA#k>3=Nee^?pM9+?4Nn2GSO_O+bpDw&dA7BS;&Ky@oB097PGV?VqMwpOe@(Gsx3-Qxr{Q``$8v@9^TSTgQ1djOo{g*i z7LwJ3H$H^p$=U5Mm_=FZ$X;gGr&FL&)0S|WtF`tocQZ9Cr9can7Wro0N1MMoq0`WC zD@Hz$gbmy_RStf|zbd`hEI-8DmY9?M&aIKuzA~Ic+$k9RzA3V#d*9joPk{Gt`-S>v zCwDgp-~pNZ+OB-P>FxLBds!%z#-p2w@4S53pxwHCS!BGe?wFpjcf3FuC?oN|!G1l3 zVFdXini`5X;s|l;_F3};{$S5vLQWB=bJZ)I#Gff7bDCu(C>{I=`|^amjzzDs1_gPY z&e3c`94~`kb_U~m_N>3t6msB3gpB<>tcA%6%G(+6&-wxw7)+HfR0msXah5X9XKpFz zY+*J!_jz=%WHrmECfGg|Xs5(%#@Q&V2+Q1&Es)6@O=J5NmIxu4Z?>dEBXrmMlFv6d z;m1HI7T|4vv3a)qr_Xz9p=|tz&4g)CX)cN#&f&9=u7+DLU2#luQE2iR7R@a&c+E^s z!WG-V4SbY6#=aMTAj_%fZK*ZZm__r(OdX3lvMHB_6CzqvGi z6eJV0cI1j-Z1}^r`tIkdbnl!}6IPd|>$9$BIJNY+{}MO+Q$6ip>XxxOzu>*t?HP`U zh$27tlFT4q_aQ3c;0Oy#=-I}i)@8=G38(Pu*|&`PsV!mT@kAxX<1@Ed5##)zgJ!fKdj-HYAIl4$xF2&B%J5-I!`>UlOgAqtI5yE9gSWW@_v#Uq7Ns zDDe{(?=OA5NoDDXC=K@d`%9@6uMo`u5^;vzmBvr2IW(ah_B8{9ka0+#-WMCW;LjQ9 zPRQv=`%>^eFULAt&!dfM%*2u$s)>lx`ScddcGyiPZ@>U#q0&hv!%c|KYVY@4?@FEn z5g4yC0E8GLZBj_l;2L3mFo8#=jh#EnT;lgusO90?QK8FcSk2g(ZJ9QluYY(l(94Ni zr`>-z{c17l4%VsVcOF8b_!oIE4T<>y({y|HC{u=*$KBgW$1PFM>lQLVIJcLIzPJ(I z*-|JE7Zy@?*wp<`5Xa_jn3K946O37_hU{X7Q4D@HVIs!hedL2Sf9!|FjE@FTx|V8x zO@Zec5~3cIDoE8q%K4FKF|2s4*RB2%!V<`<^WZem(8^8aS-D>7OY+j6`G?_(SY4DHl9R&6qA@@&QvPk+q~ z-0+$b6Bb5Sd6e?wOZGRcwE%*bx?Gfmc+^t;@^gmE=CBScV%;_nnEFHOk>+Y=uweR2 z^ynF7o+%SRg8*{XSdmX;_E;?{*7GD0fxjS+mh)dd2NWKSyw$!WtZG_;4_e%}CY{|; zk7Up_b4u#tDCZiyT7PsVG!Czm-u0a{)$P+7j&VJpfI=CME0J*~m{`!~KRgsB*5%6K z7>^TsZPinXe}%^UFNlPaLng9)i>R^@$A ze|YoPN%cDdr&dqMqacr27Qy4N2C zjfYdar|$1{tr1m=DqH|2oV>+<&+=4_TT-49W!#umt%%2dOTalSpLcxGmKJ#fOkck@ zW|9p}910VLUB4FY%4%yP$9IIHY{7s$Sh~mUnD_-4LhDOX?@E0XDb={NWYrfGG}BE; z$YEztZ%ige9cXA7B7a@us#zXb4ugmCZ4AKM{W_9aD9pQPi-g6rdg7J%{CiebIE#y6 z1f-MER%*W|-EE2fKF}%k=1sxcfXc!6>P|MlgY7`y$sX_%uKr!2aW);TL!R=Wla)X` zWItI}9y_Y=;$uE<#lh_6E9R=p|KQ|%AKO9xg(un6@c4foC_R2EZXnYSJUWdxW(U8@P`07H^kUCU_9G3 z>m?p)lu~kQ%D8CVuo!AsIyfwE zaoP})Y`QJhiYcBqHWb&@CFx#UXU6o3jEU}&3|zsqz3R9wE?9W6F)Utjv6rzCfP$7^9d{Bv2%uK9tst+T>62!5=}FB)+pm8iuK^ z)uXPy6gtTa+AhN2?9q&vrPYCkXDT&rjz2Utxx`at!&MT*jV47W-9654nX8W(s%Pz0 zseRS8GHN}%gN6UZl$pr$4~`e;9(lU$zTIGa$P)T4O?>ZX3k+5HZz*D7{a-#tTl)Kz z2+nM;nNsz;hPYvpK6q>Ky@qRIT5-Jf1MRcL?Cl?K--(bv6yWs3N!je6=}D!lSm8K4 z*_IGj5sYBP>gi!9dNKJR{PF2yN~0{Btz!vUu@9eA68d+FSt>Inf?OxCa|c&egnlY| zhUCPeB#MZxjZP=8v?$miHI=tY+#4K3qmy0`^$ z3O`Zjt4G^};T9EQ5*im4VAFw^0upERDG@1Pi3F?q7C1(cUruBPGJ~fSjuY8zo+J> z)H*fee<9;K(3<}QrTV_2BC1jpN5>M%1sx3W;zM4QUbg8q8ITX8t*WKpr=30iS=IUF zld*el4 zeqS{G<3j>z*WWrw1R{T4)O8|oTJJZXBg)vR`jc8mEylBde#!r0I#YDdHa1~Bylqjj zL0}mQJ~cNNwi5+X1sq;fjhm;(>1h_IORP7Hjp^v=tv+=woxfz=XXAGKP0_xSKvnu6 zU{0|;ljA7g+#oL2moX2&k%6&{G=qqW| zr}5|N?%o#}`DZs>>o5!sT}k4~*om*0pvw7MGWc~#JYd+Ned#A#BW^#$Z>~0)u2VXD zXgx&onC)@80xf`(bca{+doGvu8P{&)^NPJmroE-F2lNq_D;$M79mUvkvDjI{0GkBFvI#DB>YS@N> z+n-g2pe=ose>wUc&n3) zGQ6|6q_|zk2oy;$`1yGU2J>-D^b2e#Hn2Zrh$$+;E`;W{q1}yGTlUQIU7TkiyK2IeB=e%XfBlbUac`Dz1gQmTtX`7~%$o=P)Bf|#pcE)?F{)YhCKmOCY4}@b{0VN~g3hy@BI{lh^-A-1ERbrR zSa23O{AOg-sqg8A6kb%3ZVg>o;Z=LU~SJ}_AqvyRQ8su5XCqa`TBS| zwO8DssE_x5m&f+zb5W0|rCGkX)8M>ogM92WMnDH~*e)8xp)2+3jfIhVCVKj;lW^37TN_5RVsZ^Y)i<5Szw~fq^g5*_6(MMiy z(V1r{pV-)$-$u#I&@wvv%Vss>TS?sMO1~eM`ng)zE~!JZD*V@f`LY2lkbratbhDYB zLHy=vaEb-H2dwnE%hN>yz{_TS!DuXh&Rc|T8E7wDVEfJ5xs39~%0f>sr;S}O{tEiF zwV#v0;h0Qz?7!GEHWcW1-B&5ZcM?{%E1K6A21&UUZXW-XsFWp{thJ+dt%F8ah_t2U zg_hN1wb$eM`7eLW2zos{{?ZR1R#RL*{oN_PF+jdlBF_D@@k`PC!53PixoYv-aWeml z1;D~|a{8h~w<4P?W_9Zp8pZuPoQgXo8|O?BW?jr;RFQ+_^@4whZs;25OPrS9KHmD? z|EcjBm#Z_GLJqbU$SMKAqZKx_W^*amU>hu)qqJW_X+&+uyiWs}Hd9Hft8_wbVql_x zRJmGoZ3tyR{;hXzj#33*?`V<1tJ`~WVep2tt)Xh;Jx(@k_q54Vj1Rr`RlZqWO#T_h z%RwW<7ev_x^Ae%?{vAE4B`BpVZ6n{`udq$Oeta&Vvg#qCZ%J+&i}pcC0x%viM~#*U z2v3P_`DJJ~j_UMSljchw2~Q3$LWeYnU5r1DK5Qq9pGVDdp8E!0YJG1k*!&$c^O~r>Q*bo|~G0LdA{YVA42{R?`2 zSMmkxl9OLotf^+iLXu4O3e+&=GDy<-KP+WI{Aw0_YpJCIDPh~)7Ek2Tywq{mXPqmU zc&|j4A@`J*r_A)NY+`_NUL(Gh{5>jB(Xw#0G`qbYo)SF81R*)n)|sDWS6v4PHl*SP zOM=NB@uD@iS6?X%7fNYq>$~EhM%z_OKWokLmQ>sT!NIpHMz+cKkRdXUr$e1EGdfNt87?A7@{Yun;@ID*s(4}%cee3iKvDnPOu0vP;aZ+)c?bp|nFplt- z#zP9wc^`&%Ics4{Vp(oO2~R7;sKT269p9C6t*5WUYevpP#3=T|Bt73g$t(tG&l=aA z$fg7_&|ClroG`UqXmYZzP|Aaa6`9;LR29bI;#aSfJsZ4dzkmIEtp7eZiGL3cK_cN6 zs`j09djcs-p2VeQT-ZqOom}%TX;hS^o*uMo`6BHF@xELxCulEU7w7k`;d<@mEWeYc zYKKdl%}xr|-rSB`G%8FV_W7_;Zs`13wVO@6xU$$5Bup2%Pu|6Dln{ubw^M#CeJOK~ ze#}^zz4AmPQt=}r0~y21a7m*W^^v8aqd6|;{r`9pNs+?hEW^aG*u9w%?bp$o$SOw= z4Xrc)J@o=P9ux6?&Wa`a;HV6o?6)%ue1R?zxZMi|-4l*3)Lt4GpG_A3lg7!cWu(en ziHV6*RM7lbDcyJe*2(1)LI2ohBZVQA!lYLjzVX&sOu_6$pfxvIwNlr&*}ApS3^tA9 zE#boGPw~-G(wg+6GRlq|X>+k14PNh0Z7;HDa8Qst9Qos1+VQunZ18JIr=aSK=_@C? zrzEZ%6~%5naa#%%b)_!`Nro38B2AMMQkHc!L?k?c#ZM~d85%Ci@`kairQdzXKwnvj zdJw96mzY>ucWzcyHRho9JdU$@WW@X9$J^@V=B)=bbdMJdM+h8!Z-uLUYAqcpcgTF# zb;rn+s^;&3jjg5>HIl5@!km95&4+xS$xf#uE*Qx?AsUHOfy5K&yg1l+s|JAuRrsap z`aE5hg8(Od8PgQLWf&;YDP!=mgQr4!w)5?Z2ZS4DWRCxM>qyU4EJZtisB4%a_g(r= zs`pQCx;+Z{q!l4-WJ0F7G?UgX{S#lrVs#}ZE^fbXI&FQL;VsT4=ZjJ?bka6HayH)h z4Uxj}QsYIjg6~z0Oplf4)wBNE#lVqxP|N-Af}Z=Lf!E_HmL+r(@QQk7PJ7hYbF< zH~G;{C4vjgf8tM!Yyv!0W7YO};QiApam2)G$j2nZ2tTUKac}uBO^Ue6dERDI$`c1kp-v9a-KrJ2+~$2`rFEt;F7x zp6#zmm-ayli&*MuVv=7lc`j6v%q)eSegn^Cb!MQie!uIAjT2pDt-*XVak~7o(<<)T z2)8M_ky7UP=0gS&xo_R|cRD2*DiwFq;C}q_x$Kip;ZNf?Ef0TjY_|?5Un?#xT6Xv( znW3sa{!J6gYi*8qM_-}5rgTe_wf%WPdAOECQ_39QkKrM+I29B#qeo9yO<)zrX)^4; zyv1uDGn1f+;_vV07m&fz+0|#<;=NbPX+)RYljNnzhyaQK;9#(vIGtoCFJ+h}W>iW55``dm`< zWAViJ$BR;`OYy6|Ka-5-dWDrTitU8OILMxoJiY~H(n@2FG?AHn0bXg3fbCS9T~=m) zwTAnJnz=P0Z^6%H`$GbD9)_1J@-YKL#Sa}_j7-B>l{Qjqw~p9aFhXj zhfA-DsM@uus;RMnb`^M6c}Po3-_6ZJtiu5Y2LeMKpl@}ShrmTgMwHM5F3;_tPIn2X zq^3Rql9w;g7570_lKghz`~QjM1PuP}2Cxg1(?ixumgk8kJl z{7rEssD?SU_eFwLqB9A^!q!$IjF-MCQZ0tRb-E)}(;WQqk0IX5i^+Ey;@qB`-ji2` zcMIly>Jk}r+_`)M^ya^u8$F_uq@3p@&z3Fl_8rc7MK*SRh;@a87pZH`4Rk)V-2?0PO4LU)Gue6?UKI@L_t|Z^P^yW& z$hBy@g~n|Kx9|N;zZ^ehb^pl5bjTDJl{L;Zq4@R29~$#!JnM*dzGVq0gCBxNVNj*~MjdkYju5_L!Ez zXnRaIUExmiFNZ7{MJJT~A(q@PXfMriwIYMFzi(9^A6`-WFsYO|XvR%|)0DHucIsAX z;ND)DW3||_iOdpOM&38QC!UgsUsxUaG|ZxMyWYi{H7L$sax}x;W8v1!_V?IH7jh0s zsq^WwOoszw%h~c<@ z*)m3K0M*7js`t+)i46Di3u(>7M3w<5y`B7KVm}_yms4XFtsR-~CgRi}xZibkVZdx8 zprowKuyHk+ZSfEH)^r_-qM~9FrNBJP9KCwaN}`6UttEkpCvS*yRt6o0@)I-bZGZMB z*Az-~{H3Xc&D6ucw=>!@u;Twc$)Z`qrJFx!9Z9ZVj`gn3<>MGp#;)0`(+x*PK9rKq zOziO~sY#mYa1}9r#t{jFN5+|ko=EZeL-Uy+d8@gm*^--uaU$f;jLZU{>bT)Dd3f|w zga}W^ah69&Rr7jet5FM!#ZFeBDYy6Rtkw&<$l8Nr(_h!G7nv@jBv_2E7>|(n`}=2O zAb}Unv?%G!MWm9SYK-po_XIW|kk+M+duX6t`wgBrb0*-&i1}S301Iie*oGcOuOg-mj|?*Hi!>`E44&a> zgpfWw+HU>GAzWNuH2*TZa|wFSmY$~jE(Kvu(?Kb29&b2&w^t+_StGY=>qFAyvtov~ zwKVe+U*~Lh>=#~i=h&^%($oLv=dB?xuY?!fR50Z6x&Px(Qv-LI9;Q@mLzZ}4`ODezOmzJDTM1qI2-z&x$mwxssx&~2O z$eLFjPDB*0VytYQa8!brR1Sf9wDJt?a|&AO$gVDS^W!h~GfOIP6*+M`W;?TWz(CapJzFweef_2#v%%CG zNl8ga{{Hbp7w*)zEC6pr;O_v}-QV9Ys;tZco+wcNNjW(=0}<^wVD_E<`ZbiJNPYkQ z{Zk--_yqsUEC5gLjpww3;|?DHkbUg0W)lz?^x|T3K#lD( z3P={f?*jsRpeAEhDp>G<3c=fF@R`=8E14n_x$S&_PeUFliD+3vqX0?y9sz+L6m%dX zmZs_M&gY9smI-Pd2&B5HNeGuoy9eq%Bs|y7bbP&-c zV6gSuLawQ>{@w*c6A)Ku1;aC_YS@gruxDmwPD=s&ECw`8_@u49y+J`loKVe#{Y&$r zASmMU=AwNIWmeqbVd!(y@m$2MO!^L?cz0$!l6M%Xyh|^JlmkN_q=Y;1`aD_piuAjY z3Co52?N5?MwbBYX5s2c==g$v7UG$rOq=kfg*hguq=BY+pE2~|%27Xp7?zWMN*Dd{1 z=pMExzna(&@lC9)MJ1!+jv9y`Fg5YK`*Y-01mXoJ3$QD=+%ADgmBc}>lyMy`pZ zW|DL=l#+zw$BW91SPBP2IbUG1mSpi8>6EGIu1y(uz`sTFuQZs zAimw<21d9;AWVk~Ri!ALAP`@_<^>@zoFRpmqyoGwpcZ~>Z}$cKFiiXPB~f6P^8-X^m9Pm3y8w&? zRAMq`dFWY?SG)!?Fu-UL7lkl4KHi$dulo8G3#__~06o79Dr05V-{TM!J}*cj$i)Z) zDuj%-43Uk9>%t|8sN8@~2GqbU!r2;k@hMM3+=#)c4LE(*9a^cbk7Y-C zq3^qCZU9eT-$)tcuWsDyLB#JgshS^rWe*X}iVly84rX!K(!`%zVc@ge{QM$L*UTzJ zC}rtou3{DjSeh>l_X{i=9uib&o=0w_66Y@Hn#mb-^Tec^q;|`t(&5r+{G?tgav3d* z$QeaedZTjI+5&=K&Tvre*rImxaS%_d_;(Xs3w_aHu^`pUW_{($SEvfHf!-}5L&*XeJgTLy zA62HMO_rHIDc{=uyPpt1p1<%ch`uBh!{1izSVpDj^VbWa&&a8{Yc91`uPP7R1;7f4-{ze z((!<38a$JQ=g*&i8y$_bg)A^r>w@J4XHvTb7#+7!QJZ1z2)P-!^t4U~D>74+ujD}f z5>6@t)PXYsj5{viRz)*v0i^^B;mYKx6sCgC5ht{$5Y0Z%pf)6cCgq9w2v{xo04F^x zA))6Mnfp_i^dt8H6No$q!$qK91hkuk#Ke2viSGaC&jxEhI+QKnjaDQR|D3&DOKN7z2TZ-#{1XJzvq|Dh4qjE z+@8l??!Fl%TiO>{sREPMPcLI+jLs-;oZEV$`o1Ei?s$WyMdT<%9=dCSkX=GndkqFF zFH>OVuar5wK2h#Q_SNwg+p5>eX-&VTtUygmeMee$xjCI=x74baNAh5nyps>-W(ceb zcIw#t#9!~Zmu^w>ZH_K(lv}LX^UT_-G&QdiPxyjiYMinsXplf*=nB#Ue4gj{*PuAj z_5stOBA^_hAsG|W2Mo+J;0ruh8!sf|w4V6^^lV)aCQ?;ZeE|v^)1NvGk(2@q@Q0C- zw9(*e0c7p>&wyEmP+`Z$#v*=$z5+h9tBs9~2>lL7vO~EqA9ulb_wfcQuI@mIk<`UU zAcx)sIR^N;k;?#nOz9BBia=ft6hhG9ClTWgTZCkEv@u!>T0+42DFPQ4#E`wa0_GT? zT*qbe<3Bi%wm>}1c)i#LA1DTRS1iD5@A?CJt9&l|*S+8u7J>3`S1*uOAjyh_kjfYv zQ!zD7O~pY&au?JXom!8D{oyPQE>5j7}Rgg;v}M#Ooq}GDysmh=Ig%(zw?x z%#Xpri26PV317Fcu%L_Mbz+1^SyfYG3!{^b@j|(3=ynj<2!V@J1M8WpMmfPhEl3v+ z3SwWhzT$czMFD8=i#~hyj2)f_R4@n$3Unl#5!M-y=7H(d4ultcRzv7LYW|I;qfLj8 zx82Kruyu`?=U&4zF)^PUPgneU97w=}A1mzxD8#O*a9jzJfgpeI%O36s2U3T1~ zPvA_Z#iD}u_~5)8!0t4wD=Tf?2n!XyKgdrYQjGzvH68GY_5XE5VZg)$fJ*BOFkGec zl@EY<$pXRzbiY9_4ixiS#>S$cG%&ETk`aYqf>O}-XyYX!d<5?Eh-r)_*qEqR**tvX zqn=L%bV119ifSSv*AaH}4lrIMI*FGp$@2CDc-j)J&CTIsTtE%wX z*x1m1&QuG7x?V*^h5gnR1h@#O&CJKI$9SQPtn;qI4u7;nOnX1^Lt`yYCjd) znr~2GKoejL^&4QZDE?|pXuoHL%N5tXEpvN12g8t+z)GWB=&C!18;65Auij_j{;9{8 zkLVe%8hq)^cm6uctj_)W^QXtfWyRt-?8^>S(fe;5Qiu>_%uA?J%U+v)R6~{^?DqIs zC0`orPAxeK&dCUaZbA+G?35p9kvfi~A`n65FXZB$*r#Sch`;nz_9CZ^#9-x>+;F~k zaYzc;R0%hnw4K_m{#$=JEf#M`Ynn2DQnYrV2{c&lf99BWvse3e%tnVoLLd3;{X9IG zD0RcLt$sh5NM*~<9mg>5)&X>`)blWR0q#?rizF+8Iah4k*udgf=xKJoj_!Y&d(o)XC%aby>QC=uW!y&!_Ck3D1tIRoj~{FM!0n~a40``SI&Ld7 z9lQ#9xs2Mi9Uw5yj2s5=s}BV0AkZ~@dxeXOOE<8*{0SmN0ZITXP)gRKz$~sQ26+*< zX`P`8&gnv2#UZ#5iwB4zXe9Sp%gu&;pvlt@gkl6RNJ+L0e~yVj84Mqhv{PCJjayy# z+F44cZG=31a7{|xQ5!1k91>G{3VJZ%SkVq@p zJ&4iDInMy0lpmn;+1k`}^_g?xzqL_vL~La1so9S0e3wzK7gU@RlT$-=iqx|kbC`fp zg5O%%gCCWk?kAR&!g}XEk6Wcl)hit(suk62OJ)_dN1@N1M*+k&>1l-5{xdg>4t)8 zmn~g5svDLdM*ZQMsKcfm?HPtq94p<|P@8~`%ihKYPhKkfb39p&;PWKNYN^W6_a-Xw z`YnV88dQsIpO~!^R8>{I0KnVh)wa{a6N=M53E5e6(U2s6MuD2H@~Yd$PRTw_L9SI} zj>bnP0TXVgw{M7kK)6}Er!22(dK;`)fByMxsr5Bj{rDIui3^kA`dfy6?W`2W2*7>* z_8A8Ad{W_5GF8rdKD8mG$(4xA7hc8 z-m;1)ymbX99@J=&XWx{ZF={?pcEFq|c_X|Ht!;ELA~ltdZaA--SxPc1+J%4UOQfcp zzfjh=d?+bD-cOyCjWi21^T7~#D_^|=_AEM+{)7~hlmE266>`$k-rPdeMBsBHb9UIp zids{p#UEwKd^fhN`t?|XJ4Z<=_vOKnUj}=@_m2dLR4EZ1_+t%ZBBS!=wP;a{km~>HVMA@ zuIlRQJ$e}6IH^}W3_>VFrtLk2ss1P4X!l+^*bCi(CX^=nLq^)s|`llxcZ* zffVrK;-VM)9Wyht1b(+Dn6_gP5(45-JVyz!*wl_dei0J5UI4DUiAmaX&;jx!Gf;Kj zK|xsn$tff(KcM0vZV`a5Tc0R?BIWnWj2EEY*@FB9V@jKobz#rjX9PjA!bX4;P3gV1zPubFl-V-W zg`sGNQy2#Tozui&%j(0&+@jUU%GGXFj0+6Jc!+-krCV7c z)3~)wD~2xe{I`ymO@Wb+guFncSpZ5g6CK6Cm@k(Rfqc#h)Eh5LdHG_7+E@L{`LRXCHfm;emG?-QQkbG5EatM(2VS+&c#|9JY!u&mbS>!TbC0VR}>5Rp=(J48iLLb^-o?hZjgLQ+IZO6iae zK}xzoI;0z<9za6EcW%z_|9&}ofd}@p_w2c6)~vM#d4~bzlLRcy{Q%}etyQ{mw#)T+ zX}{OndCep~k52i1w#X(LEFuK2KwH_KCd$_ARKiSYpB3 z>_PSbu(M$btOyec%#)q9pHVQk2Lw+XDwXBsWw094GcXVZpob06h`ZoQbrW>u5EFK& zW`V=!O=>C(u;DiZ!H)(Q=K`FQ34Ynxcjw8;$$1@DNx-^P4`4`SBM69#g1mekoHe`- zD@1>fH#|qcaxk9XVa5ecA|h|Fph8uX0SMJep#S~>O9yZ&tE$vW%}KpB3;&A~;*`GS z{qDWMeUsm&@FA?&GZiH&lykx)QkmieF2KztG5%3FbHp;{O5nq0xtm+r=Xjf08812} zTUKp^I-<>aAh&V+h1L=sr4gO{wJUBsWa1V-(hQRtc?k*a3bFOwYjmj2o%1;jk!)po z<)%X13fWAi=&jwGt2w_vL=wrxaP#jQL(ie&s_T^#zTLrI1(Ikh6LEVQb=}xX&NfW z=iq<#h&h~XH%G_byze7R)DdFnYgRep!Ym**>r?(Kw!aAtUZD$Yc3VUUW*XhR>M!TS}M z&CTyO!I{=b{MobAl$3w5puN|A3?Lqh27;_nV&dZ?B_<}$A|NJirosms=gdq-K$-!f z*xFfEzyT}(x(gCO>R?NaBQ#H=Sg_-}}G}8oWED z!6OWUVisI8$a$?_rKH?OO)G@BY^I`M9$r_@35ext1CbF{?&d0Z!5U-FHp9JY|Y5Xs9E6{41H^s?b;|84yb}$ znrN=ix+^d)dvgQkR)PEat;7QTUaC7Eq7f^Zo@ z)9-Qs3t3J38G_z|9Vl2i9N_~NgkT>SbKnyYJaTm8J`el)(li0~VRJs(XCr3Oo`X7v zWarmVXcX}AFMGqBB>@--e*62*KvR7d%iTBynFV~0X`P?eOqY|{aYIT7%>mGCG9pX%%1hPDb^EeO2z1EiUHVG4w!w4_bFj+iJYZM`;=9TWq zMQ#2X5L&J9xLTqd%yx&+Zj_(1?lu?JO9}oapm^xh?S2_?eYfABe_EL4MaD*Tf4y5e zwX)hEkbIalqz@ik8Be_^<@<~oIg(Q>C390m6;#!w40PCr-b$5qYUG?}tNeZSEEd** zWzVf@)y+#1sqYs7A8-Bb{}fhLG!-GboXGN{huU;Pg8WmzhF{_82$U!O@eknS$DK?# zddt0@6Fs(WF=xxCH%aE}`Ahl8s5R8NtlWlAFpg(r!u!HgDc8D4u3s4jR;P3!Qx|7v zIVM9+fStCV3z1u%5jvLs z7gpdkb6)G4{pUDbd5b|hVobxfRFuRmRnGQB}Z z&FCXjGx^}xLNIAhccNFN;+;Md2Zr$8B~xX;ppR*9=tV#G{mRIYQfMl@TfvKetuCc& z)_6RPOE^!v*^MI|BUpWGc?syA^=)kdlQa<$1%;V!L74>{hGwmFTjptgjr!eBgISpj zOzUJ}Z@)(We0e^h)FE?mr40m?MTXngi`6OiDDU6oVF~3?a(?`;(1j~=L%(Z&wJ&N(R_38VV>G)6hUq!8T2jBbfh)ewLN(`uXz) zR*rHzIDEAB8NN#SsR46&%2)-EFzZ-X zXK!P&8OE;Di;GW0wf+k4PGRM))Oj8q|FYxttAAZD_Ab@;mh_lgC@YtWMrTg>v(AVL z+YP04&OWBjmKe9dnMZbarS)kF_xKh)yZ>ra?j8C2zQpW3a}`d!r;cXv0PO^*0VK|7 z3IHPr2?J=Jgs{TGQOMdR!}-*agL%)p!OS*P4XoE%RX``R^>TN_2qBY_U}FV#YI=rH zv4PDM{*2ORv5VAi_6hw$sgk>Yd`?MCmy1)%Q@-SUA@29)O@GDwx>uD|R zjRjVPrp8s08?HZpid8d;lQD^97VJBHd`RGUeBLgKrP-L(_IU7FKz1`e{=ZcNJ`U}X zeQ8Q};mRrnp*2P@2f&9=)wV))Gz>#}NNe>#L-elw4pX>GmoL-6h@C@4MFkDvpm~o( z)?k2s1BNpLwqc4{if_?eadEL3_O-MjL&mnYN69_;;Gzv~kt}UJJz?c`i|jAH|I-!6 zCj!~b<@omys(S`IeXEs?a#Dey7BJcrh=Q&M!-=KV1VyYA*Mgu#P5Y`oqc7vS3I{z-u zlz*&9`vs^Q{T@}-goy0io`5*MHewyvNZ5~j$T`pdNfBf zhC@M+T-Iw^(w{4br9w|n7fkjR+7&hPCXVXins+;^_Sl8W8hs^>#2;&vF9eU0T}UTT zHM4MzDYPxx$JBMZQ+@Lm6ruECT%MACCP3vM*!*C6bv17Px3*wxWG;FOoCnBxJt9Tu zyD`&0*Df+`NV|^OVYA<+mBYKZnbUrd;@6PYr0}CYi2h&uytX&33gy-q0;(%iFec=P zyhW27&EbhZlsi86O{c~p;TT8NtxusC&w{D4rExazv22C+F?z~ z_xbiHv;Y7w>swG4p?y0`Kd70IKgjMH+D~p{XP3+hgn-)sV#C>n7J9>=FkdlOldt-4 zT>!wE`pE&bf;Ecy88lreRN$aiwY9O)f$t$|+!4?I{{AK`nnWXMR60R~0B|@5JF#G* ziBNSY10$h#I~{@pZVBZbEqKYn;AN`{W;o`<`LALAF4zC%%NJz84KqnS$Td(O6vF&z zi%&oRP3Uu<*L1-WuY(F06Ly~l*5DV?b0y{@@7)1qgi!nt^%*GjsDS~;d4w?nB8qH5 z58!}oD9ngw&0Ug=Gf8oMe>N!Wn!v)*eiS+#g(n=%lnM@2^46YDSw!9u`F0_`(pTe9j9k*B^_A}!; zNsC#kqz)%+$~t-FwpRxlp_{PeuMX%Ff8RhDWmoWIw!SUCVl0ij(e_2CBH{ujH2*_F zd{=`O$MV(v^HwSre(~TjLD*EZ{rwC(JBLf;evm(`&Y^)MR%W8`);~!9&l?#a@Ss@d zgOeALk$_q;g%D}rPk{9s48_14t$A`X5%@TA@D86yL6D$#6oz%ELjbNYP-`-t*W8Du z99>!+4vaLwAd&C_s3)Y*JQ}KR^p9aJ1T=17Nu4SC`7G39wU7eqIL!uw5Xor)HJrvj zKAk}(uN^R^6P}r#u7%p4&wlAD*jQ2n>enOzP^W_hqVlN{7&pXwHXX_Z=U&`%z?vA$)slpr zSi==gEKtMMHZ}Re#P6HqS^!|3z;}YlIhBlz%t;808GAm9T5Api)x z5b{5aIf)EmfL{qKP7$V`2{w1&TZ+R0bHWpUxLinhhH&bl#0>bHrj{0FZ!aAPNc#r| zawM?71FgV<={(E@sKE{u`X{(SnCwC7&mPmSEPmm?ux<0}ZX0mzOh= zf`NVImK$nIbs#x4CIUv@2o8h7z455YW=$WBC*nv9v}eh7vj+bHlHE zY;)}zVfTlFUnZ*Q{8lw`QN;_AR#ebxS3soLe}L39kjS-3zN5 z9^tb&aV7fcm;?9Y&HmSm<<>s$E~_k!mgAP(iMTB!6yt@1x%6+=7sf>eZ@Yr8Ep453 z^3vLPu_7K*`=AN75_)HvnzhBwaOYY`7wwPHVR`A)nIays!sF31coTxYv@~Lolp4=+ zdIv~GGgy?Bm3KBKg2|MyVrq2HX(uF%Rlba!S<)Rg7_BUCu$`N@UkGe4L9 zRsR`5UH5F-bmtzn>qnB-Hx_*tvql5Xbh>{2+Oy3*ytJA>IQs7WhX;Er6d5bmzK?Qf zz;ABsO>I^oy)N_`YMJTOy-&(7t|_xbH!of$o?Vz}TedR-_9zk0-uh&^K{Lv-4~B%$*2qQMuulzpe_@ zIVwbxrJh|@Sfq$EUsWqvFzZ*olO`4OK!?+mOUTVW*|2lP{xJUvWb6KHl~7`v73qu5 zDb+;e#n^o?)(j8tX;}HY_=eRnEbUN!nW&6945dL=ak9o_0`Umr&Lf>pP=rj*+wO;f>0sc2s7HP*$A=V0)VP@FsF74M5qPx~g@t{K!9X_Z+$i|G&%Av*|&JB_DTlZuAvu zyY%)F90Zr~u4MgA+26P3{=;AHtH~tP{ZzM>rTyY1;cn1~Sr1ffvM-sh^%xhc3WXXy zuGc2V@U{<13WpoD)c59Q*H|C4D$#u<^-(TZIF_c!R4&KnxPGspemwrhS4+7uFc;sP z{tkm4?)VcSx6|Z-?!%h) z%F&YBhw*t`=s(`~eA5evn$)vt?yKo6s7$^Oj}1gm!((!yKAq2&{ln*%6Sa-^Lo3HX z8os^t;|cwyuZYXKX<-?mQ=-?S_4wH#`k|$vFT_^=mWn<6oRP^jSoUG_);-#=!Fe+I z+A5~!@e_~rH=GFYuX&%oFdx6fX-p== zos!-p`;MCSoG?*aWLA4-*^Ly#R6WLf0`aLAYGyWr^@7Yfh@KN$1$<0m7#8@J1T}$y zLF1xT&`kObzhSqvtC2w@YW8z?N0mL|_p+S6Kg=`gAx)YK0J0A2D#)xPLG$0|DxongmjwO-|Dyu>(?`4aztePIe}I23Ej=1 zjWB*&?JBy^!B_U7Zr|;Lfpz^A__>duNzIAJij<-w3`jy&4riHlbtomyG31N0nWWiNIMZ6c7>yKH-5EVn{RlW$&;jAFC=Sj@NZ& zRH(ZC5e%rE4As;80b7|0(^G6?hIndVKe$L39cNfAl4Ynt%tGm zMd|=`I6bnJ^X3!3033`W#M6%q{`(g@tVgbLU{(am6@{^-wbcy8&u4Le^?&ZZu#wDw z9U!68KA*^I|BLq=BSUe{Xu#L(V$tk^rk)Xs;HHmWfbpnS?4Ds+Iq~-U@O!(y;{|8_ z?2I(GpSz5-HZ%M${oA<#zlu16Y;1_cHm14;$Eh_dZ7EQE66Jul)R1Z9Y0h^E`(ORh zs2r2moiWy*l}_3)iwi6ZxI`Ff`DD_dB~8wKcoSUw|;>kPTcM+U7K4lwA zd6ch1lgrEuHK80kSuna6jS=WxG)~O=U;O4i91K{W!q^99C2Zm+^yl}MpH-1@v9{BO z#?&w^*vu$k+ZHK$FBLydf!m<;qFc@!=IVgB4Cn zdO@A(lligWOX;kXc(fU2zXGZ0C<$u}EG$Ia1L_v<4(hRzpuEx0Znr!X`$nrq^si++ zX1kCYNcoa!Uzp;O3yl}?TN;C&>M9WCe+Z9iAEK|BpQ&dRt6>XzA3{mQRnkN@M4+8= zTPRuUAnk+gmhQmvGe|NzCfz08Z-lCvK75k-k;S>=@mDBIC9`8}zFl1W+3a-k6C(O3 zaMWC@up+E=H5xj%~2$$zM(J`Lz?ECqBacT)jXV`r_;Pc$%+!)hJpBmnpr;rmB?F|@X% z)h;-EF6Bfi4A{8tXKmif)V`yX(J@~WQY)a2ZVqEZzkkNlug6%obKyl>BnHY2NBxvW zxp$1zVWY~sw-5i=E-YVzXC3JgT-G&ZC5DmPzF&zy434QuDGYbW<0U^849Y9X?I*2~ zRO1{(x)tmd5FH49)cHxzTE1p1A>gb~Wor-#p8pLo9M*P@(~pJ?0>a()Hu}EV(R+** z#{@uwf;#AEz0ay!vWjjy8wM5geto(_A{E~3v!uC(b zrw>0{3@BGB9N-qK-yn}4m&@ilx~7mOoeCJiQU2PK3C|6RlNQ)>Ny=q-_TKqYo#V`B zL{0S*!K0-U(MbeQg_f4$#noKz+!ea}U&?4cej-CEV2As1a^vX(Qn$jw-U5C0x#_DB z4=#kRXto61=I6tcjNyx66579(zTwN7)yX=xs>Ntp>WF9j#X?p{g`;5ph2!}Brf!`> zR-IeV6WBtR-4-#Hogu|}!(>h>D%Br3qDUnplLpiYZBGPI%D6?x9T}n}{NiVmRR%tZ zKe`R@JoZq@CC3zh-zhTp_z#Rz#cXC4X1qXxx8bnzRCe1in)I5gyF2YrvZdU+oRWKB zdIV>Pi@CikdEA&RkxxxqYwO$R9tWt=T%V}uUJ9!(C@N}v50%EydX?12KYRw07pKZ! z-Z{q@@JMLm!5D!&E8Xoa4Qy`5btcV28a7P#>D%EsIV8MVH{R(ZcRWc=&q2wm!^m>~ z^+#pLls|)W{e7$`ESQD9)qM}9LB+<}S#JhYvS6-U+RCTm5})iEl|MM#Ys~7Kd>;vi z$y$_?4a>`ZI}N0Eal*>61Y>heZP52Y=ng9|g%fl#hJfI}zKe|i zJXf;K8oPcPy}Y8JTn!01j4L=zSO5FI2=T?_aD6PWbPo87~-2 zBgkOrFy?`M!IQT1bmWpMw{ki7o$oxaJYaM!3EAU!_~hluXUMGnIK=OQ)B1RF*;7fG z*{TVlgFo$g#(S=C4H&19;o%?Ii+;l+BN&-t_eK?%Y+Xn3vwTz(thi(3obQGk7LPEHJr9fXwXPUjQs5@&O73 z$w(VOU*(|J8yRUGjrOL~H@J|{yU$c*QGR&@j;I$8S3%oMu^ zg3Ru}%TaN%+aDBXqBWq~#_Yaz|I_rejW3EWUtYBOBTpzM(evT)r~|JP=4_*=qcq-2 zUmU~nt!wtq$@kFBfHCaVJp$T&di5elA(Y!EQ=tHPT!2}-GRa{&lMX;@z-3^_t_)?0 zm{2{Z3nK9aohdYOHWn-uc@&>+21od=J4~iEnmP204$n*}o{f+YmBU~#pFRJc%+1Xg z^3A!GKEK$cFVD~-xA;Jh*i(wlCVKy#hQO09O^>?nL+{fpl1QTK^fz7+{ zSjkBp5w$AkOI8j`|CdGnrk{TPe9Jzbpv3#Quo%@w$H?r-{3~qE%;$*-8`a)1;%sbt z!`N`(C*O4PBfzoh^^*^wE$9WpUed%{7HSRrf8@ah|UFf zdB;5Wcp`6$jq;WA@1x=k68dg_HrFY)Im0HocWlfFzTE4AzqjTj`tLEtuoi4@X|F$? z)N?qX80hMm0HCTQMIB|JX% z*+YIais13PpWmXIa(R`U&t;n04e!4uR7In18!UR%M248HWQD#5f9c?4A}wOaw4G&{ z{6-#l5YaKl#55jK-Tv-bGU<;>9!Q4!GBBhIMIzPpRW*VH+Nq_f7Ff0^l8$E)L%``3 zK^Tp5QNpE9Yd{eR7Son-vFh%^&(%R;}aF_O|@aVY5iuRn1uWMhtXxO)zTUh9+ z(BS#sXZ{4ww&AWtYg{$8xD(y@ zJj7zZJ^kW3cs*$Dd{o}ev(bnbcdnyhGFZ?rT%D4rqtu$8{ZzYTu~V4=>)k-%(%CJ1 zVY}<6))Hp=(7CJvz_<>W9=QLgB9af8^@WV0=@i#f`@X7P`p|7a^Ua_C)(P!pV0=Ey*`|s_y=_pNPJt#~3tG z>wPVDcWV=)aQf_zEcSJH4BV?oCMvK@7z_WVqNy1dtL*VnHepMmXRJo);pWW(fJSknbp_EXj_u3GFIC4kDs?!=veK4g6{Z)x%? zu~x-inr^+ zxPxz?iWL-B5KHvQJq=sGe`aVTV{d%Td!oL=!4zX#$VeLA`D=oU@0wHi7f5OG6`rp@ z$BJ=e)~lWsJZ9{GIVS0kJRZ=-)R->4d)*Pu7x?3ctYWwBL5hj6M_Fm&@z52$WjWn{ zX-Z}E9n)KRO?T`qZ@h65zj7VM`gKMB-bzkp#6#E#ux9Tk@?L^F|COBt2kqH|9|WE# zFi}Sw2jJs^5kU|P!Dc4cfTx#|nu-L{u;3W~5fRnt?O`_r(cJ0nxw9Yj#7M7xwKbe^ ze$7YCxtGT9q%hj+i=wi9PG8JWJNF|qy})FJOVBhX>9fZS+dAIR=sJ1Ts-JZ8xe-g& zU1pM7w-}$wDUv9LNPVmb12hS}2-IDET8j%+e(wCmH(b;%y}Bg%G1^(%iJ|!#!CPla zpVYDs&i4;IZXL(|PEzRHWnfU{2iYH(CeDcV=cz;ie<3^~B3Sa0%=^UVKCXO<1UyT3 zJI5Ph;bHDEzSFgzX?l5 zkBShy^nn5r&22#j3^Agn+S=E^=3uv-dkU(#naNCe`4A@+cnoOu+ldAebc4r$A!rgC zfjv7j1=BrXCT|+Sn3O@S@b)7j+S5dGiQ{^Q(R9E(5pjuG|Kn^q4y!+$aTFA=3aWmu zJD+do0=8|j!_=(dF~hIb!1yXcS2v>obqT{e+k5w>>+0Ky(n_7K3C{Gfk-TSYX=)0& z%%i(+(|uZ}KT?&Dlr-ucE2$i>zVsPV8^^nX4d@5MW5UJn z(})4+T<7O7nd(qZ_hAbf@FSO?*b}~zES1IHh`U4=?4p>OBvN%({X_BL26{uIfcz_< zqHQH6Awfe?Qc_YxIe}jU>`cTV1fl!_$N=CKeY0<+_SKRC6sX_LK}!#cGfWIS}Q@yLbgkL|fe(R$b4sKwpJ=oOTfo{y;W-4?cr=e`2Uyr_Ev}8 z-UO;huvc0c^(yG@Q{lMqiqdZR%G?WSp166J=d$GDdZJNFT94E!t1ouKJX@b>37CZ> zfl>@VhBRh)c=%oSMD4i}brAzmozWY~YPP!f*jjB=N8Q2iCs8=nYCL!N*Kej+jzBPe zNggo0bO|?=H@d`_8qSp>Djr(OM+ zkq$1BV7k*c^rU09g`}xdvy-06p}B{f(7W~(t&%-?o3!`({ep_!~N*YK=*bCo1J3ZIorjq-&H5|N^4W!5N8<`xc3&b#MV37%4+ z5%Iuo$vHP5`7kPF5j{Gxl81hGhRS4q=+|jg_&Pk9#-P2j>0R|pW7+KMhF}0GHNoX= zF2WoMJGJZit@^%ciWGB~^72PLkW(Z|%}6n>9_9=6!-O)lsONl6HF8jtV0}-5>%p)`PYpT9A zJRN9Q><*?wbKQ?cMT=ZJU8UoATQ>>{6F_r0C?kYkFO<)GH-Xgd~AVRiN%)eLS{K-@pK!V;vJ7hZevbZ2bxj+Tikl*s<;hTVvHPPZ#IT zw_QUjY?WN{wVz)XK|TCLjtCoR*n#*{53kx=e}B*nDI|;G!cBMr;o~CrKd&{0fI3id z$bl5OqW!931oju@IZzb%z0%Jdac5*+7;I(E?&G@V}5N)EA#{S)|wkk&-{` zg`ZkkkpNZ~tOGWx^*V<%Lcj_u01hvc0qi9PGA!D@3gmv^5WZ#9c$(eAo+56Q6@~MCuUr!R=Aa3>(I^?h%a|T9e3zZzvHApupX}5c` z%&|0oce%UhVnN=SYj5k+?cZ&6AJh@N|K~kt_VYvnW@h)&7x<2)rLazI>kdbZ6oMoG zV5y-LLII8B$4w{~B0h`?u&aWAr9~wA`4cq!X)=jjKp}*Yy(&L2?Ia_abl_Pc@s1a= z^8o=n@C`wI=L60%h-{4n@{#bJWgw}d6x$1$<7gf$O4tpm12k7Sc~U_Di0rvXAP5kt z#d83J^`!URc|jzd1}frz&z%EmUqB2PtoMQl{b0)bO4-Xp?nrnShJn+CxYyuY(J($v zAqg*{9(Z9nySep&>I*wOGO`KWgV?gQtN7uqnS+N-iD=qCt!n1M1OTa{krZ+R{%$@| z8H2V(g9xhMlq)hRIoSo|28b>giIz8mNM5zr7@y@1>>3B2te$d_;WI>KN`+Y82JrI5 zF3?{wVA_IB283Uv)(1i&TE$xgj7Ow^04bF7V!Ts})A1h%_^{G&&rE<73*2mwLFt0M zN4`DSR3Qz8wlgF&MoMWVXz4)2KO{b3u5UB;;7hhTK|)8QH!w*7`!M(KL@z(atuLqkVlRm`6Hr73lwE45HhWL*k z4+g(;-=Rd|?dtcwL#zD%7Zlo&$wti9Ys$Q{ew8w=rZ|y7<>k&z3)AXZ*PqZZ>#@hQ zENTxBy0KiqdoI`+kT)os`-KWl6n#Sjn_q(#A304ra>px>0|JYLky$8=u9LnOb0@1p zK!=I5sj9oon9Z2le-ddNVqBZ=+~O-DeZmH0loAaH6Vl|KDM>J{q0F`mRTApq6ZeP`@wwF&gw`w6p-)oAnK)J zxp9jCSlD(Nt8$Bi)cOOc;7G*;ZhVobu>e;#8(Z63K<9=_3bz7BAf)}K7V+H#4YTqW z)mm^YV=y%v{PNkLJrV>vga8m#gZo459i^y+7 zR=dZfN=C6mF6w}63D;fw#c5*#DC%bN)Dp(Uz)Q0%V>|6(t%GQXanYBDde=bl7PoA@zIvS3FfdEWxbV zh(kq>^W!NlHG276yLZV zEB|U!@E?|lnDu^wwh{-Y0f^Rr-wZZgH&T&-HY+{HuV8gYY`y7?p2?G8OM+g4WvxXl z%{cM{+ja9M4>oUpE;HNdEdy->7?VCs7EAitN7!FT3mr?Agp}0n=P~RC4A{uVPoI4n zvJV6gGz_)x!6Fhx0mxy%G+=8$4^I?Ccp(&|*Z?6AR5-5HnZxr J?< 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®f?yGX7B|M-H(eFM&~f&hs*k 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 literal 471031 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

rF9+8oG?qC~m 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

YS*p{TIcu<( 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 z�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%0AdrRmOGUeqQ*?E`EQAs;b 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ʲ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 ZOUw`b)H6FbpGH18dPwD9#{N@R{|(JaU9JEC diff --git a/test/reference/custom_layout.png b/test/reference/custom_layout.png index 00cd9dc2239d7d15cd49b18b9053204c34ad1766..8d59b523fdd7941b3958635c3efc5697ac615e2e 100644 GIT binary patch literal 102617 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 literal 103899 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

P4lzAAVcu3ug)!2=+oTGGQ$8mLJ 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%0D**|LCEVKFW(BTRL01$bf=QPL%hd8SAsy76yi;S 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%0DV0T}65 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 literal 62824 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 literal 61720 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

&DR^2LOwi=(Mte0s}8&xTgKSrycJ1?%hMjw?z|+ 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 Y+%$N?FT>u~)!uyV?N^gu8U4xs1nxIC8UO$Q diff --git a/test/reference/line_zy_cartesian.png b/test/reference/line_zy_cartesian.png index 9ae32ba4ca9fc53c9f43eb2bf1c8c29ad3b53587..5c827fe928b83a8741a051ed16888fd870c29afd 100644 GIT binary patch literal 106086 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

h6CqqL3Djv(ih567dDvFD?>>PadD+^F$0vHX_ z$BMFt6S=+t0i>RDGR*S02>%V{AI-CXdNkq{XE&RPawc7j6KvQGMOaT(R<Sq(BLyLIkC`tiQFe zsH7zR8L&>pK!1O%Br=ilxb%hvuz}1_fk~MLT7b6pSZkKo>Gy(uat3&Da#3NS_j?TG zo-3Go^b|`3xU_gW%VU&qZ@oLV&*A0X;}^%w$+;6(PPZY6Ekt}3q&*(-q*m-!GRbg~ z&M6TUUXi--@wn{lsLjq_-%Ds?4Bf%L;7D8!58q{T*YIgEpaI!g6B84JIEjZDsv@I!YBmm_?ur);_GPvN1Pm7hmIM0csEv_Zfq zw0f}TN`>ViU@1Dif%UTiM^vIue%G}{ji-Z3G6FECK)*00<3 zSm2er&8E1dBvT8`atEwoejIJwfl~3n!QTEls#X?`2+dfFHuiLnzW!?@@5nY1K^dY_ zyY2&h)#lBcah5wN^|x#4|EYn7Jxds8axjV7xNLrY{_|N!dm6&g+1LP#3Jz`$#>q_> z)%K2#a<`0fJpPB;P*6F@yZJq

7cD)0bYH~2 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 literal 61720 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

&DR^2LOwi=(Mte0s}8&xTgKSrycJ1?%hMjw?z|+ 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 Y+%$N?FT>u~)!uyV?N^gu8U4xs1nxIC8UO$Q diff --git a/test/reference/line_zy_polar.png b/test/reference/line_zy_polar.png index 371cc2eb1f90c06935528d07ca5614bfea5ecdeb..dd0a4a2226f55afee5a63dd80773891c53e2db2a 100644 GIT binary patch literal 152632 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 `McBIo;sRoq67)Mt7^ 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

uA?<-Zlf;)$9eoS326`<9}JT6zMaAV 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 z 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%0AfdotA=I@QgAmQ`p&z>aJgqi3D9&Kl34L5Xk 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